たくさんの自由帳

再エンコードアプリを作った

投稿日 : | 0 日前

文字数(だいたい) : 5259

どうもこんばんわ。
うーん、特に書くことがない

本題

前回かその前くらいに作った、AV1に再エンコードするアプリ、ちゃんとUI作ってきました。
審査が通るかは分からないですが、通ったらここからアプリをダウンロード出来ます→ https://play.google.com/store/apps/details?id=io.github.takusan23.himaridroid

一応AV1以外にもAVC / HEVC / VP9でエンコード出来ます。
mp4だと勘違いしてどこでも再生できると思われてしまうから嫌な場合、WebMに書き込む処理を自前で実装して追加したので使ってね。
そんな事あるのか知らないけど、考え過ぎか?

Imgur

何回かwebmのバイナリ触ったので何となくは分かってたんですが、それでもめっちゃ大変でした。

Imgur

AV1エンコーダーがtargetSdk 34 (Android 14)からっぽいので、最低 Android バージョン14です。
Imgur

以上です。

おまけ

MediaCodec / MediaExtractor 周りのワナをここに残しておくことにします。
てかこっちが本題です。

変な解像度を入れると VP9 や AV1 のエンコーダーが起動できない

スマホの解像度 1440 x 3120MediaCodecの動画解像度として入れたらエラーになった。
AVC (H.264)なら起動できるのでよくわからない。

1280x720とか1920x1080は起動できるので、おそらくキリがいい?動画の解像度しか受け付けて無さそうな雰囲気。

Process: io.github.takusan23.himaridroid, PID: 31106
java.lang.IllegalArgumentException: 
	at android.media.MediaCodec.native_configure(Native Method)
	at android.media.MediaCodec.configure(MediaCodec.java:2332)
	at android.media.MediaCodec.configure(MediaCodec.java:2248)

WebM コンテナに書き込み機能

webmに入れる機能をつけたわけですが、考えないといけないことが2つあって

  • Androidのコンテナフォーマットに書き込むMediaMuxerWebMの場合AV1を受け付けない
  • 入力がmp4で音声トラックがAACの場合、Opusに再エンコードする必要がある

1つ目は自前でWebMに書き込む処理を書きました。多分動いています。
2つ目ですが、これはAACのデータをデコードした(PCMにした)後、Opusのエンコーダーに突っ込めば終わり。ではないんですね。

AACのほとんどのファイルがサンプリングレート44.1kなんですが、Opus44.1kだとダメらしく、48kにしないといけないみたいなんですよね。
Opusのエンコーダーに入れる前に、デコードしたデータのサンプリングレートを48kに変換する(アップサンプリングする)必要があります。

うーん。自前で作れる気がしないよ、、、
というわけでAndroidmedia3でも使われている?Sonicを使わせてもらうことにしました。ちゃんと動いていそうです!

https://github.com/waywardgeek/sonic

音声データのお話

ちなみに、どうしても自前で変換処理を書きたい + 数学の知識無し(私は数学の知識無い)でやる場合、何に気をつけないといけないか残しておきます。
まずaac / opusのデータは圧縮されているのでデコードしてPCMにしてあげる必要があります。
これは ↑ のSonicでもデコードしたデータを渡す必要があるので、音声データに手を加えるならどっちにしろ必要です。

次にデータの読み方です。とその前に知らないといけないワードが

  • チャンネル数
    • これは音声データがモノラルの場合は1、ステレオの場合は2です。
    • コンテナフォーマットを解析する際に知ることができます。
  • サンプリングレート
    • 大抵の場合、AAC44.1kOpusなら48kだと思います。
    • AAC48kを入れることは出来るらしいですが、Opus44.1kはダメだと思います。同様にコンテナフォーマットの解析でわかると思います。
  • 量子化ビット数
    • これも大抵の場合は16bitだと思います。ハイレゾとかは知らない...

これが分かっていれば多分データを操作することができます、、
今回はそれぞれ、チャンネル数 = 2 / サンプリングレート = 44.1k / 量子化ビット数 = 16bitとして話を進めます。

説明しやすいように先にサンプリングレートから、44.1kですね。
サンプリングレートというのは一秒間に何回音声を記録するかなので、44.1kだと4万4千回音声を記録するわけですね。

で、その記録したデータが何バイトで表現されるかと言うと...
量子化ビット数。今回は16bitなので、バイトに直した2バイトで記録される感じですね。

そして、チャンネル数が2なので、
二回、量子化ビット数で音声が記録され、それをサンプリングレートの回数分繰り返す、そんな感じのデータになっています。
以外にシンプルですね。

2回繰り返すので、最初の2バイトは片耳、次の2バイトはもう方っぽの耳、、、みたいな感じになります。

余談ですが、少なくとも圧縮(エンコードしていない場合)、一秒間の音声データを記録するのに必要なストレージは
2 (チャンネル数) * 44_100 (サンプリングレート) * 2 (量子化ビット数、bit -> byte 単位変換済み) = 176_400
となり、176.4 KB必要らしいです!エンコードってすごい!

例えば

話を戻して、2チャンネルの場合、先に左が来るのか、右が来るのかは忘れちゃいました、、、が、
例えば、片耳だけ聞こえる音声を作りたい場合、

量子化ビット数を見て、例えば16bitなら、
まず2バイトそのままにして、次の2バイト0x00 0x00で埋めて、また次の2バイトはそのままにして、その次は0x00 0x00で埋めて、、、
って繰り返すと見事に片耳だけ聞こえるデータが出来るはずです。

また、これの逆をやることで、片耳だけ聞こえる音声を両耳聞こえるようにできます。(片耳のを無理やり両方流すだけのハリボテ感ありますが)

例えば2

そういえば音は波だと物理で習うらしいです、忘れました。
波らしいので、半分に減らしたり、他の音声(波)を足し算することで2つの音声が同時に流れるみたいなことが出来るはずです。

例えば量子化ビット数が16bitなら、2バイトずつ値を扱えばいいので、
音量を半分に減らしたければ2バイトIntにして、半分にして、それを2バイトのバイト配列に戻して、、、を繰り返せば半分の音量に出来るはずです。

2つの音声を混ぜる際も、量子化ビット数とサンプリングレート、チャンネル数が合っていれば、
それぞれの音声ファイルをPCMにして、それぞれ2バイトずつ取り出してintにして、それぞれのintを足して、2バイトのバイト配列にして混ぜるファイルに書き込んで、次もまた2バイトずつ取り出して足し算して、、、を繰り返せば2つの音声が同時に流れるようなPCMデータが出来るんじゃないですか(?) 足し算の際に2バイト0xFF 0xFF)を超えないようにする必要はあると思います!

例えば3

なので、サンプリングレート変換を数学の知識無し(私は数学の知識無い)で素直にやりたい場合、
44_10016ビット (2バイト)が記録されているのを、48_00016ビット (2バイト)が記録されるようにすれば良いはず。
ただ、48_000 - 44_100 = 3_900、つまり3_900回分のデータが無いので、その間を埋めてあげる必要があります。

それだけなのですが、、、それが難しい、、、

MediaExtractor でサンプリングレートを取得すると半分になる

MediaExtractorAACでエンコードした動画を入れた後、MediaExtractor#getFormatで音声トラックのサンプリングレートを確認しようとしたのですが、、、
なぜかHE-AACのときだけサンプリングレートが半分の状態で値が返ってきました。なんで?

ffprobeで見ても44_100なのですが、なぜかHE-AACMediaExtractorに入れるとサンプリングレートが22_050(半分)、になります。
これだとサンプリングレートの変換処理がおかしくなっちゃうので困った!

// MediaExtractor で取り出した MediaFormat、profile=5 が HE-AAC
// https://developer.android.com/reference/android/media/MediaCodecInfo.CodecProfileLevel#AACObjectHE
{sample-rate=22050, mime=audio/mp4a-latm, profile=5} 

https://stackoverflow.com/questions/33609775/

ちなみに解決策ですが、デコーダーからMediaFormatを取得する口があるので(MediaCodec#getOutputFormat)、
そこからサンプリングレートを取得するとちゃんと44_100が返ってきました。
うーん。こういうの書いておいてほしいなあ。。。

// デコーダーから出てきた MediaFormat、よかった!
{sample-rate=44100, mime=audio/raw}

もしデコードした後に音声ファイルを操作する場合、MediaCodec#getOutputFormatの方を信用すると良いかもしれません、、、
ちなみにサンプリングレート 22_050 + HE-AACでデコーダー(MediaCodec)を作って、サンプリングレート = 44_100PCMを流し込んだんですが、
特に問題なくデコーダーは起動したし、データもエンコードされました。うーん?デコーダー側でHE-AACの時はサンプリングレートを2倍にしてるとかなの?

以上です。MediaCodecMediaExtractor何もわからない。。。