FFmpegを使用してAAC(mp42)からAAC(M4A)に変換する方法の備忘録です。

AACの音声部分はそのままに、コンテナだけを変える目的の内容です。

本記事で記載しているコマンド類はWSL 2上で動かしているUbuntuで実行しています。

現状のAACデータの確認方法

ffprobe コマンドを使用します。

ffprobe -hide_banner input.m4a

Metadata: の中の major_brand の部分に規格情報が記載されているようです。

手元で確認した mp42, M4A のファイルはどちらとも、音声ストリーム(Stream #0:0(und): Audio)の部分は Audio: aac (LC) (mp4a / 0x6134706D) となっていました。

ffprobeの出力例(mp42)

Windows 11に付属している「メディア プレーヤー」で取り込んだAACファイルでの出力例です。

[mov,mp4,m4a,3gp,3g2,mj2 @ 0x5561cd3355c0] stream 0, timescale not set
Input #0, mov,mp4,m4a,3gp,3g2,mj2, from 'input.m4a':
  Metadata:
    major_brand     : mp42
    minor_version   : 0
    compatible_brands: mp41isom

    ...省略

  Duration: 00:02:09.45, start: 0.000000, bitrate: 270 kb/s
  Stream #0:0(und): Audio: aac (LC) (mp4a / 0x6134706D), 44100 Hz, stereo, fltp, 256 kb/s (default)
    Metadata:
      creation_time   : 2023-12-24T07:08:36.000000Z
      handler_name    : SoundHandler
      vendor_id       : [0][0][0][0]
  Stream #0:1: Video: mjpeg (Baseline), yuvj444p(pc, bt470bg/unknown/unknown), 800x800, 90k tbr, 90k tbn, 90k tbc (attached pic)

ffprobeの出力例(M4A)

iTunesで取り込み(変換)したAACファイルでの出力例です。

[mov,mp4,m4a,3gp,3g2,mj2 @ 0x562f83d785c0] stream 0, timescale not set
Input #0, mov,mp4,m4a,3gp,3g2,mj2, from 'input.m4a':
  Metadata:
    major_brand     : M4A
    minor_version   : 0
    compatible_brands: M4A mp42isom

    ...省略

  Duration: 00:00:18.30, start: 0.047889, bitrate: 222 kb/s
  Stream #0:0(und): Audio: aac (LC) (mp4a / 0x6134706D), 44100 Hz, stereo, fltp, 123 kb/s (default)
    Metadata:
      creation_time   : 2012-03-19T22:50:14.000000Z
      vendor_id       : [0][0][0][0]
  Stream #0:1: Video: png, rgba(pc), 300x300, 90k tbr, 90k tbn, 90k tbc (attached pic)

変換方法

ffmpeg コマンドでの変換時に -c:a copy-c:v copy オプションを付与して m4a として出力することで、AAC(mp42)からAAC(M4A)に変換することができました。

ffmpeg -i input.m4a -c:a copy -c:v copy output.m4a

オプションについて:

  • -c:a copy: 音声ストリーム(Stream #0:0(und): Audio)の再エンコードを行わずにコピーするためのオプションです。
  • -c:v copy: 動画ストリーム(Stream #0:1: Video)の再エンコードを行わずにコピーするためのオプションです。ジャケット画像はここに格納されているようです。

出力先の拡張子が m4a だと変換時に「Output #0, ipod」の出力があり、ipod のフォーマットとして書き出されていそうです。

変換したファイルに対する ffprobe コマンドの結果は以下のようになりました。major_brandM4A が含まれていて、音声ストリームの部分(Stream #0:0(und): Audio)も変換前と同じことが確認できました。

[mov,mp4,m4a,3gp,3g2,mj2 @ 0x56431bd705c0] stream 0, timescale not set
Input #0, mov,mp4,m4a,3gp,3g2,mj2, from 'input.m4a':
  Metadata:
    major_brand     : M4A
    minor_version   : 512
    compatible_brands: M4A isomiso2

    ...省略

  Duration: 00:02:09.45, start: 0.000000, bitrate: 270 kb/s
  Stream #0:0(und): Audio: aac (LC) (mp4a / 0x6134706D), 44100 Hz, stereo, fltp, 256 kb/s (default)
    Metadata:
      handler_name    : SoundHandler
      vendor_id       : [0][0][0][0]
  Stream #0:1: Video: mjpeg (Baseline), yuvj444p(pc, bt470bg/unknown/unknown), 800x800, 90k tbr, 90k tbn, 90k tbc (attached pic)

念の為、変換元と変換後のファイルで位相を反転させて再生させると無音になりました。そのため、上記の変換では音声部分は変わっていないと言えます。仮に音声部分の再エンコードが発生すると再圧縮により主に高域での差分が生まれます。

特定のディレクトリ直下のファイルを一括変換する

アルバムのディレクトリごとに変換したかったため、アルバムのディレクトリ直下に convert/ ディレクトリを作成し、そこに変換後のファイルが生成されるコマンドを組みました。

# Ubuntu(WSL 2)
find . -maxdepth 1 -name '*.m4a' -type f -print0 | xargs -0 -I{} ffmpeg -y -i {} -c:a copy -c:v copy convert/{}

変換後に問題がなければ変換元のファイルに手動で上書きします。

どのアルバムが対象かどうかは、以下のGoのプログラムを作成して検索しました。

ソースコード
// Description: カレントディレクトリ配下にある特定の拡張子のファイルをffprobeに渡し、指定した形式以外のmajor_brandのファイル名を出力するやつ
// Example: ミュージックフォルダ配下にある*.m4a形式のファイルを検索し、major_brandがM4A以外のファイルを出力する
// Require: Go 1.24 以降・PATH に 'ffprobe' コマンドが通っていること
package main

import (
	"fmt"
	"os"
	"os/exec"
	"path/filepath"
	"slices"
	"strings"
)

var (
	ignoreFormats = []string{
		"M4A",
	}
	target = "*.m4a"
)

func main() {
	if err := run(); err != nil {
		fmt.Fprintf(os.Stderr, "error: %v\n", err)
		os.Exit(1)
	}
}

func run() error {
	if _, err := exec.LookPath("ffprobe"); err != nil {
		return fmt.Errorf("exec.LookPath: %+w", err)
	}

	wd, err := os.Getwd()
	if err != nil {
		return fmt.Errorf("os.Getwd: %+w", err)
	}

	files := []string{}
	if err := filepath.Walk(wd, func(path string, info os.FileInfo, err error) error {
		if err != nil {
			return err
		}

		if info.IsDir() {
			return nil
		}
		if filepath.Ext(info.Name()) == ".m4a" {
			files = append(files, path)
		}

		return nil
	}); err != nil {
		return fmt.Errorf("filepath.Walk: %+w", err)
	}

	for _, file := range files {
		cmd := exec.Command("ffprobe", file)

		out, err := cmd.CombinedOutput()
		if err != nil {
			fmt.Fprintf(os.Stderr, "exec.Command.CombinedOutput: %s", err)
			continue
		}

		majorBrand := searchMajorBrand(string(out))
		if slices.Contains(ignoreFormats, majorBrand) {
			continue
		}
		fmt.Println(file)
	}

	return nil
}

func searchMajorBrand(out string) string {
	lines := strings.SplitSeq(out, "\n")
	for line := range lines {
		if !strings.Contains(line, "major_brand") {
			continue
		}

		splittedLine := strings.Split(line, ":")
		if len(splittedLine) < 2 {
			continue
		}
		return strings.TrimSpace(splittedLine[1])
	}
	return ""
}

筆者の環境で変換が必要になった経緯

先日購入した「WALKMAN NW-S310」に(MusicBee経由で)楽曲を追加したところ、特定のAACファイルだけメタデータが読み込まれないことが判明しました。

手元の環境だと、iTunesで取り込みを行ったAACファイルはメタデータが読み込めて、Windows 11付属の「メディア プレーヤー」で取り込んだAACファイルだとメタデータが読み込めないことがわかりました。

WALKMAN用に提供されている公式ツールを使えばよしなにしてくれると思うのですが、普段使いしているライブラリのプレイリストをそのまま転送したかったため、AACファイルのフォーマットだけを変換する方法を調べました。

本記事に記載した変換を行うことで、該当AACファイルのメタデータをWALKMAN側で読み込める状態になりました(一度WALKMANから該当ファイルを削除してから再転送する必要があります)。

変換について試行錯誤している中で見つけた、以下の記事に助けられました。ありがとうございました。