自作の音楽プレイヤーに AAC(m4a)デコードを追加する!
現在、俺俺 C++ フレームワーク(glfw3_app)で複数のアプリを制作しています。
この中で、音楽プレイヤーがありますが、AAC コーデック(m4a)をサポートしていませんでした。
AAC コーデックをデコードするオープンソースは FAAD2 ライブラリを利用します
FAAD2 is a HE, LC, MAIN and LTP profile, MPEG2 and MPEG-4 AAC decoder.
FAAD2 includes code for SBR (HE AAC) decoding.
FAAD2 is licensed under the GPL.
- 以前はサポートしていました
- 自分で FAAD2 ライブラリーソースコードをビルドして導入していました
- AAC コーデックは、iTune とかで、CD(PCM)を非可逆フォーマットに変換した場合使われるようです
- FAAD2 ライブラリは、MSYS2 の pacman でインストール可能ですが問題があります
- MSYS2 に含まれる FAAD2 ライブラリには、本来含まれるハズの mp4ff ライブラリが含まれていません
- mp4ff ライブラリは、AAC と TAG を包括した(m4a)フォーマットをデコードするライブラリと思います
- mp4ff ライブラリは、FAAD2 のソースコードに含まれているオプションライブラリです
faad2 ライブラリの種類
% pacman -Ss faad
clangarm64/mingw-w64-clang-aarch64-faad2 2.11.1-1
ISO AAC audio decoder (mingw-w64)
mingw32/mingw-w64-i686-faad2 2.11.1-1
ISO AAC audio decoder (mingw-w64)
mingw64/mingw-w64-x86_64-faad2 2.11.1-1 [インストール済み]
ISO AAC audio decoder (mingw-w64)
ucrt64/mingw-w64-ucrt-x86_64-faad2 2.11.1-1
ISO AAC audio decoder (mingw-w64)
clang32/mingw-w64-clang-i686-faad2 2.11.1-1
ISO AAC audio decoder (mingw-w64)
clang64/mingw-w64-clang-x86_64-faad2 2.11.1-1 [インストール済み]
ISO AAC audio decoder (mingw-w64)
faad2 ライブラリの構成
% pacman -Qlq mingw-w64-clang-x86_64-faad2
/clang64/
/clang64/bin/
/clang64/bin/faad.exe
/clang64/bin/libfaad-2.dll
/clang64/bin/libfaad_drm-2.dll
/clang64/include/
/clang64/include/faad.h
/clang64/include/neaacdec.h
/clang64/lib/
/clang64/lib/libfaad.a
/clang64/lib/libfaad.dll.a
/clang64/lib/libfaad_drm.a
/clang64/lib/libfaad_drm.dll.a
/clang64/lib/pkgconfig/
/clang64/lib/pkgconfig/faad2.pc
/clang64/share/
/clang64/share/man/
/clang64/share/man/man1/
/clang64/share/man/man1/faad.1.gz
FAAD2 のソースコードを独自にビルド
仕方無いので、FAAD2 のソースコードを取得して、自分でビルドするようにしました。
※自分が取得したタイミングでは、2.8.8 でした
ソースは「aaa」ディレクトリーに展開するものとします
aaa % tar xfvz faad2-2.8.8.tar.gz
aaa % ls
faad2-2.8.8/
aaa % cd faad2-2.8.8
aaa/faad2-2.8.8 % mkdir build
aaa/faad2-2.8.8/build % cd build
aaa/faad2-2.8.8/build % ../configure
aaa/faad2-2.8.8/build % make
aaa/faad2-2.8.8/build % ls
common/ config.log faad2.spec libfaad/ Makefile stamp-h1
config.h config.status* frontend/ libtool* plugins/
aaa/faad2-2.8.8/build % cd common
aaa/faad2-2.8.8/build/common % ls
Makefile mp4ff/
aaa/faad2-2.8.8/build/common % cd mp4ff
aaa/faad2-2.8.8/build/common/mp4ff % ls
libmp4ff.a libmp4ff_a-mp4ff.o libmp4ff_a-mp4sample.o libmp4ff_a-mp4util.o
libmp4ff_a-mp4atom.o libmp4ff_a-mp4meta.o libmp4ff_a-mp4tagupdate.o Makefile
- configure してビルドすると、common ディレクトリに libmp4ff.a が生成されます
- clang64 環境でも、普通に「configure」を行うと、gcc でビルドされます
- gcc でビルドしたライブラリは、clang でリンク出来て動くのでとりあえず、それで良しとしています
- configure を行う場合に「CC=clang」とすると、clang でコンパイルされますが途中でエラーで止まります
- それでも、ライブラリは生成されていましたが、今回はオーソドックスな方法を選びました
- ここで出来た、ライブラリと、インクルードファイル「mp4ff.h」を自分のフレームワークにコピーして利用します
- 以前の mp4ff ライブラリでは、「cover」画像アートに対する管理が不十分でしたが、ようやく改善されたようです
- それに合わせて、自前の faad2 デコードサポートクラス「aac_io.hpp」も修正を行いました
画像アートに関する問題
- mp4ff では、タグ情報は、文字列として扱われていて、通常一つのタグは数十バイト~数百バイト程度です
- その場合、新たにメモリを確保してコピーしても、大した問題にならないと思えます
- しかし、画像アートデータとなると、数十キロバイト~数百キロバイトになる場合もあるでしょう
- 画像アートデータは、ファイルのどこかに埋め込まれています
- 画像アートデータは、そこそこ巨大なので、本来なら、コピーするなどしないで、ファイルの位置、サイズだけを返せば良いハズです
- player では、コーデックのデコードは、メイン(GUI 操作)部とは別のスレッドで動かしています
- なので、大きなバイナリーをスレッド間で受け渡すのは設計としても良くありません
- 当然メモリを確保してコピーする動作は、全くの無駄です
- PC などメモリに余裕があり、高速な CPU を積むプラットホームでは問題無いかもしれません
- ですが、組み込み機器などの小規模マイコンでは、メモリに余裕が無いので問題があります
- 今回、glfw3_app 環境で、試しているのは、将来的に RX マイコンの音楽プレイヤーでもサポートする事を視野に入れています
- なので、魔改造して、オフセットとサイズだけを返すように改造しようと思います
- 改造するコードは、「mp4ff.h、mp4ffmeta.c」となります
mp4ff.h、mp4ffmeta.c を改造する!
- 基本的には、「cover」タグの場合に、特殊な処理を行うようにしました
- ヘッダーのメンバーに、ファイル位置を示す「pos」を追加しました
- 基本的なポリシーとして、タグの情報は、関数を呼び出す度にコピーと解放が行われています
- 「cover」タグの場合には、画像データをコピーする事をしないで、位置と長さだけをコピーします
メンバーに「top」を追加
/* metadata tag structure */
typedef struct
{
char *item;
char *value;
uint32_t len;
+ uint32_t top;
} mp4ff_tag_t;
static int32_t mp4ff_parse_tag(mp4ff_t *f, const uint8_t parent_atom_type, const int32_t size) 関数内
} else if (parent_atom_type == ATOM_COVER) {
if (data) {free(data);data = NULL;}
len = (uint32_t)(subsize-(header_size+8));
mp4ff_metadata_t *tags = &(f->tags);
uint32_t idx = tags->count;
mp4ff_tag_add_field(tags, "cover", "apic", -1);
tags->tags[idx].len = len;
tags->tags[idx].top = destpos - len;
} else
識別子として、「apic」を戻していますが、画像データの種類に応じて、「jpg、png、bmp」などを返す方がスマートです。
追加 API
int32_t mp4ff_meta_get_coverart2(const mp4ff_t *f, int32_t *top, int32_t *len)
{
if(f == NULL) return 0;
uint32_t i;
for (i = 0; i < f->tags.count; i++)
{
if (!stricmp(f->tags.tags[i].item, "cover"))
{
if(len != NULL) *len = f->tags.tags[i].len;
if(top != NULL) *top = f->tags.tags[i].top;
return 1;
}
}
if(top !=NULL) *top = 0;
if(len != NULL) *len = 0;
/* not found */
return 0;
}
aac_io.hpp でのタグ情報コピー関係
int j = mp4ff_meta_get_num_items(dt.infile);
std::string totaltracks;
std::string totaldiscs;
for(int k = 0; k < j; ++k) {
char* item = nullptr;
char* value = nullptr;
if(mp4ff_meta_get_by_index(dt.infile, k, &item, &value) > 0) {
std::string s = item;
if(s == "title") tag_.at_title() = value;
else if(s == "artist") tag_.at_artist() = value;
else if(s == "writer") tag_.at_writer() = value;
else if(s == "album") tag_.at_album() = value;
else if(s == "track") tag_.at_track() = value;
else if(s == "totaltracks") totaltracks = value;
else if(s == "disc") tag_.at_disc() = value;
else if(s == "totaldiscs") totaldiscs = value;
else if(s == "date") tag_.at_date() = value;
else if(s == "cover" && static_cast<uint8_t>(st) & static_cast<uint8_t>(info_state::apic)) {
int32_t top;
int32_t len;
if(mp4ff_meta_get_coverart2(dt.infile, &top, &len) > 0) {
tag_.at_apic().typ_ = 0;
tag_.at_apic().len_ = len;
tag_.at_apic().ofs_ = top;
strcpy(tag_.at_apic().ext_, "jpg");
}
}
free(item);
free(value);
}
}
if(!totaltracks.empty()) {
tag_.at_track() += '/';
tag_.at_track() + totaltracks;
}
if(!totaldiscs.empty()) {
tag_.at_disc() += '/';
tag_.at_disc() += totaldiscs;
}
※改造した、mp4ff ライブラリのソースは、Github に置いてあります。
glfw3_app におけるオーディオコーデックの扱い
glfw3_app C++ フレームワークでは、画像、オーディオなどのコーデックのサポートはある程度柔軟性を持たせて管理しています。
- コーデックを追加したり除外したりする事が少ない手順で出来るようにする
- DLL などを使って、ダイナミックに行う事は、プラットホームにより手法が異なり複雑となるので、それは行わない
- 単純にソースで、追加、削除を行いコンパイルし直す
- 最近は、boost などでお馴染みの、「variant」を使って、実態を連続定義する手法が流行りです
- しかし、このフレームワークは、それが流行る以前の物で、良くあるインターフェースクラスを使っています
- インターフェースクラスだと、コーデックのインスタンスを「new」して追加します
- インターフェースクラス「i_snd_io」には、最低限必要な API のプロトタイプが定義されています
API | 機能 |
---|---|
void initialize() | 初期化 |
const char* get_file_ext() const | ファイルの拡張子を返す |
bool probe(utils::file_io& fin) | 音楽ファイルの適正を検査 |
bool info(utils::file_io& fin, audio_info& fo, info_state st = info_state::all) | 音楽ファイル情報取得 |
const sound::tag_t& get_tag() const | タグ情報参照 |
bool load(utils::file_io& fin, const std::string& opt = "") | ロード |
bool save(utils::file_io& fout, const std::string& opt = "") | セーブ |
bool open_stream(utils::file_io& fin, int size, audio_info& inf) | ストリームを開く |
const audio get_stream() const | ストリームの取得 |
size_t read_stream(utils::file_io& fin, size_t offset, size_t samples) | ストリームの読み込み |
void close_stream() | ストリームをクローズ |
const audio get_audio() const | オーディオを取得 |
void set_audio(const audio au) | オーディオを設定 |
void destroy() | 廃棄 |
- 「load/save」では、「audio」クラスを経由して、データの受け渡しを行います
- 「audio」クラスは、波形データのアレイを格納するクラスです
- ストリームでは、少しづつ、取得したり、送ったりする事で、リアルタイム性に対応します
snd_files クラスで、初期化時、各コーデッククラスを登録している
void initialize_(const std::string& exts)
{
exts_ = exts;
add_sound_fileio_context_(typename snd_file::snd_io(new wav_io), exts);
// +AAC codec
add_sound_fileio_context_(typename snd_file::snd_io(new aac_io), exts);
// MP3 はタグが、前、後、にあるのか不明な為、検出が難しい為、最後に調べる。
add_sound_fileio_context_(typename snd_file::snd_io(new mp3_io), exts);
}
最初、拡張子でコーデックを検査しますが、最後は、拡張子によらずに、コーデックを検査します
bool probe(utils::file_io& fin, const std::string& ext = "") const
{
size_t n = sndios_.size();
if(!ext.empty()) {
for(size_t i = 0; i < n; ++i) {
const snd_file& io = sndios_[i];
if(check_file_exts_(io.ext, ext)) {
if(io.sio->probe(fin)) return true;
else n = i;
break;
}
}
}
for(size_t i = 0; i < n; ++i) {
if(n != i) {
const snd_file& io = sndios_[i];
if(io.sio->probe(fin)) {
return true;
}
}
}
return false;
}
まとめ
- 「魔改造」は、あまりスマートとは言えないのですが、まぁ、機能としては、目標に叶っています
- 細かい、検証をしていないのですが、一応、タグ情報の取得と展開、コーデックの再生は出来ています
- 最近、プログラムを自分で書かないで AI にやらせるのが流行りですが、今回の場合は徒労に終わるでしょうね
- 自分で、プログラムを作成したり、誰かのプログラムを読む事はそれなりに楽しいです
- ただ、AI もどんどん進化しているので、非常に高度な事が行える時代も来るでしょう
- しかし、プログラムを実装する創造的作業自体が楽しみな人もいるので、AI は使いようですね
- 何でも AI で出来れば良いって訳じゃないです
リンク
ライセンス
自分の作成したファイルは基本 MIT
他の要素は、個々に参照の事