fdk-aacとそのgoバインディングのgo-fdkaacを使ってやってみたので、試行錯誤過程のメモ。
ffmpegみたいなツールを用いた変換方法はネット上にゴロゴロしているのに、スタンドアロンなライブラリを使ってデコードをするとなると情報がめちゃくちゃ限られてくる。
特にaacのデコーダ実装はデータ部にADTSというフレーム情報を保持するヘッダがあることが前提の話ばかりで、m4a/mp4に含まれるraw aacというヘッダ情報のないaacのデコードに関する情報はかなり見つけるのが難しい。
m4aとraw aacの関係性
まず大前提としてmpeg4の構造を知る必要がある。以下の記事が詳しかった。
データ構造のうちmdatと呼ばれる部分にaacなデータが含まれているが、これがADTSヘッダを持たないraw aacと呼ばれるものになっている。なので、単純なデータ操作(例えばddコマンドなど)でmdatからデータを抜き出してaacファイルとして再生しようとしても、これでは単なる不正なデータとして扱われてしまう*1。ADTSヘッダがないということは、当然代わりの相当するデータがm4aのデータ構造の中に書き込まれているはずで、m4aで言うとstblがそれにあたる。
特にfdk-aacにおいてはフレームサイズというのが非常に重要で、結果を受け取るためにデコーダのメソッドへ参照渡しされるバイト配列のサイズがフレームサイズとイコールで扱われているため、アロケーションされているバイト配列のサイズがフレームサイズと異なっているとエラーがでてしまいデコーダを動かすことができない。フレームサイズが1024とか2048みたいな固定長のサイズならまだしも、実際にはストリーム中のフレームサイズは数バイト単位で変動するため「どのフレームが何バイトか」という情報がなければデコード処理は実行できないということになる。
ADTSであればヘッダ情報の一部としてフレーム長が含まれているのだが、m4aではraw aacなのでそうはいかない。ではどうすればいいのかというと、ADTSヘッダの代わりにstbl配下のstszに格納されているフレームサイズの情報を参照することになる。
デマルチプレクサ
まったく専門外なので正しいことを言えているかは微妙だが、どうやらメディアデータの変換周りではデマルチプレクサと呼ばれるものが基本的には必要らしい。たとえばlibavformatが最もオールインワンなものとして有名っぽい。libav以外にも、いろんな人があるフォーマットに特化したデマルチプレクサを作っていたりする。Node.jsで動くts/mp4/flv用のデマルチプレクサもあった。
Goにもlibavのバインディング実装(imkira/go-libav)があり、そこそこ頑張っているっぽかった。しかし、libavの機能をcgoでラップするという壮大な世界観がメンテしきなかったのかここ数年は放置されているのが現状で、getしてみると実際にはビルドが通らず使いモノにならなかった。もしもlibavを使ってデコーダを実装するなら、自前でcgoを使ってラッパーを書くほうがましかもしれない。aacのデコーダも内蔵されているので、デコーディングライブラリに拘らないのならばfdk-aacを使う必要すらない。
いずれにしても今回のケースではm4a/mp4用のデマルチプレクサだけが使えればよく、そのためだけにlibavを使うのはライブラリ自体を使うために求められる知識が多すぎて疲弊してしまう。
mp4リーダー
内部的にやっていることはlibavと同じなのかもしれないが、もう少しmp4に特化したものがある。例えばサイバーエージェントからはabema/go-mp4がでているし、他にもalfg/mp4というやつもある。どちらのライブラリも、上のQiita記事で解説されているatomsの情報をmp4のバイナリデータからパースして、ある程度参照しやすいような形(構造体とか)に変換してくれる機能*2を持っている。
これとfdk-aacを組み合わせれば、raw aacのデコードに必要な情報(フレームサイズ)をstszから読み取ることができる。今回はalfg/mp4の方を使ってみた*3。
stszからフレームサイズを取り出す部分の実装は次のようになる。
// stszセクションから取り出したraw aacのフレームサイズ情報を保持する構造体 type frameSizes struct { frameOffset uint size uint buffer []byte } // atom.Mp4Readerを用いてstszセクションのデータを読み取り、ヘッダをスキップしたデータ部をframeSizes構造体として抜き出す func newFrameSizes(reader *atom.Mp4Reader) (*frameSizes, error) { const stszHeaderOffset = 12 + 8 // stszのヘッダ部分をskipする分のオフセットサイズ stsz := reader.Moov.Traks[0].Mdia.Minf.Stbl.Stsz stszBuffer := make([]byte, stsz.Size)st if _, err := io.NewSectionReader( stsz.Reader.Reader, stsz.Start+int64(stszHeaderOffset), stsz.Size-int64(stszHeaderOffset), ).Read(stszBuffer); err != nil { return nil, err } return &frameSizes{ frameOffset: 0, buffer: stszBuffer, }, nil } // stszのデータ部にはビッグエンディアンで4バイトごとのデータとして格納されている // binary.BigEndian.Uint32で変換してint64に変換することで10進数データとしてフレームサイズが計算できる。 func (v *frameSizes) Next() *int64 { const uint32byteSize = 4 if len(v.buffer) < int(v.frameOffset) { return nil } frameSize := int64(binary.BigEndian.Uint32(v.buffer[v.frameOffset : v.frameOffset+uint32byteSize])) v.frameOffset += uint32byteSize return &frameSize }
Nextメソッドで次のフレームサイズが計算されて返される間、forループでデコード処理を継続する。
以下抜粋。
offset := int64(mdatOffset) for { nextFrameSize := frameSizes.Next() if nextFrameSize == nil { break } // 計算されたフレームサイズのぶんだけmdatからデータを読み取る part := make([]byte, *nextFrameSize) readCount, err := io.NewSectionReader(v.Mdat.Reader.Reader, offset, *nextFrameSize).Read(part) if err == io.EOF { break } else if err != nil { panic(err) } // mdatから読み取ったデータに対してデコード処理を実行する if pcm, err = d.Decode(part[:readCount]); err != nil { panic(err) } offset += *nextFrameSize if len(pcm) == 0 { continue } pcmWriter.Write(pcm) }
go-fdkaacのDecodeメソッドはフレームサイズが正しくないと0x4002(AAC_DEC_PARSE_ERROR
)をエラーコードとして吐き出してくるが、具体的に何が原因なのかを教えてくれないので非常に難しい。このサンプルコードでは、音声と動画が入っているmp4ファイルのことは考慮していないため、もし音声以外のストリームが含まれていた場合フレームに含まれるデータはもっと複雑なものになる。
fdk-aacのソースコード中にあるエラーコードのコメントはほとんど参考にならないので、エラーが出てしまうとC言語で書かれたfdk-aacの実装を読むほかなくなってしまう。が、もちろんソースコードを読んでわかることの方が少ない...
ASC(Audio Specific Config)の値について
raw aacのデコードでフレームサイズの次に重要なのがASC(Audio Specific Config)と呼ばれる16進数値の組み合わせだ。
ADTSヘッダを持つaacなデータであれば、デコードに必要なオーディオタイプ、サンプリング周波数、チャネル設定などがすべてADTSに保存されているが、raw aacの場合にはデコーダに対して明示的に対象のraw aacをどのようにデコードするのか伝えてやらねばならない。
今回のコードで言うと、以下のデコーダを初期化する箇所で与えている16進数のデータがそれにあたる。
// AAC LC/44100Hz/2channelsなASCの設定でfdk-aacのデコーダを初期化 // (Ref: https://wiki.multimedia.cx/index.php/MPEG-4_Audio#Audio_Specific_Config) d := fdkaac.NewAacDecoder() if err := d.InitRaw([]byte{0x12, 0x10}); err != nil { panic(err) } defer d.Close()
この16進数はビットフラグでデコードの設定を表しているもので、ASCのビットフラグの説明は以下のものしかネットでは見つからない。
5 bits: object type if (object type == 31) 6 bits + 32: object type 4 bits: frequency index if (frequency index == 15) 24 bits: frequency 4 bits: channel configuration var bits: AOT Specific Config
もちろんこのビットフラグを自分で組み立ててもいいが、自分でASCのビットフラグを組み立てなくてもffprobeを使えばmp4/m4aに含まれるaacのASCを知ることができる。
$ ffprobe -show_data -show_streams -hide_banner default.m4a ... 省略 ... [STREAM] index=0 codec_name=aac codec_long_name=AAC (Advanced Audio Coding) profile=LC codec_type=audio codec_time_base=1/44100 codec_tag_string=mp4a codec_tag=0x6134706d sample_fmt=fltp sample_rate=44100 channels=2 channel_layout=stereo bits_per_sample=0 id=N/A r_frame_rate=0/0 avg_frame_rate=0/0 time_base=1/44100 start_pts=0 start_time=0.000000 duration_ts=2015987 duration=45.713991 bit_rate=128424 max_bit_rate=128424 bits_per_raw_sample=N/A nb_frames=1970 nb_read_frames=N/A nb_read_packets=N/A extradata= 00000000: 1210 56e5 00 ..V.. DISPOSITION:default=1 DISPOSITION:dub=0 DISPOSITION:original=0 DISPOSITION:comment=0 DISPOSITION:lyrics=0 DISPOSITION:karaoke=0 DISPOSITION:forced=0 DISPOSITION:hearing_impaired=0 DISPOSITION:visual_impaired=0 DISPOSITION:clean_effects=0 DISPOSITION:attached_pic=0 DISPOSITION:timed_thumbnails=0 TAG:language=und TAG:handler_name=SoundHandler [/STREAM]
ffprobeから出力される結果のうち以下の箇所がASCになっている。
extradata= 00000000: 1210 56e5 00 ..V..
このデータのうち 1210
という箇所を 0x12 0x10
にしてやればよい。先頭のふたつだけがASCになる。
ffprobeでこういうデータが分かるということは、おそらくmp4中のどこかのatomに格納されているのかもしれない。が、今回はそれは調べていない。 ⇒ 調べました (2022/06/19 追記)
izumisy.work