github.com
fdk-aacとそのgoバインディングのgo-fdkaacを使ってやってみたので、試行錯誤過程のメモ。
ffmpegみたいなツールを用いた変換方法はネット上にゴロゴロしているのに、スタンドアロンなライブラリを使ってデコードをするとなると情報がめちゃくちゃ限られてくる。
特にaacのデコーダ実装はデータ部にADTSというフレーム情報を保持するヘッダがあることが前提の話ばかりで、m4a/mp4に含まれるraw aacというヘッダ情報のないaacのデコードに関する情報はかなり見つけるのが難しい。
m4aとraw aacの関係性
まず大前提としてmpeg4の構造を知る必要がある。以下の記事が詳しかった。
qiita.com
データ構造のうち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からフレームサイズを取り出す部分の実装は次のようになる。
type frameSizes struct {
frameOffset uint
size uint
buffer []byte
}
func newFrameSizes(reader *atom.Mp4Reader) (*frameSizes, error) {
const stszHeaderOffset = 12 + 8
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
}
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
}
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)
}
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進数のデータがそれにあたる。
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