🎛️

AndroidでFFmpegを利用し無音部分を切り取る

2022/11/29に公開

はじめに

こんにちは!VoicyでAndroidエンジニアをしているおぎです。
現在は主にVoicyのリスナー向けアプリの開発を担当しています。
今回は生放送などのアーカイブ投稿時に冒頭の無音部分などを自動で編集できたら便利ではと思い、Android端末でFFmpegを利用し無音部分を切り取る方法について記事にしました。

FFmpegKit

モバイル開発でのFFmpeg利用にはMobileFFmpegライブラリが有名かと思いますが、こちらは2022年11月にリポジトリがアーカイブとなりました。今後はメンテナンスが継続されているFFmpegKitライブラリを利用するのが良さそうです。
FFmpegKitはAndroidやiOSに加え、FlutterやReact Nativeのサポートも統合されています。
ライセンスはLGPL-3.0で公開されています。
https://github.com/arthenica/ffmpeg-kit

FFmpegKitをAndroidプロジェクトに導入

FFmpegKitの導入はMobileFFmpegの際と同様にアプリケーションのbuild.gradleにライブラリ依存関係を追加するだけで導入出来ます。

build.gradle
dependencies {
    implementation 'androidx.core:core-ktx:1.7.0'
    ~~省略~~
+   implementation 'com.arthenica:ffmpeg-kit-full:5.1'
    testImplementation 'junit:junit:4.13.2'
    androidTestImplementation 'androidx.test.ext:junit:1.1.3'
    androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0'
}

事前準備

まず、事前準備として冒頭部分に無音がある音源を用意しました。
冒頭に約2秒の無音部分があります。

sample_voice_silent.mp3

音声メディア情報を取得する

次に今回利用する音声ファイルの情報を確認してみます。
今回は便宜上サンプルの音源(sample_voice_silent.mp3)をAndroid StudioのDevice File Explorerを使って事前に/data/user/0/アプリケーションID/files配下へ直接配置しました。
FFmpegKit.execute("コマンド")という形式でFFmpegのコマンドを実行出来ます。
また、情報を確認するだけならFFprobeKitにgetMediaInformationメソッドがあるのでこちらがオススメです。

MainFragment.kt
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)
+       val filePath = context?.filesDir?.path + File.separator + "sample_voice_silent.mp3"
+	val mediaInformation = FFmpegKit.execute("-i  $filePath -f null -")
        //val mediaInformation = FFprobeKit.getMediaInformation(filePath)
+        Log.d("VOICE INFO BEFORE", mediaInformation.logsAsString)
}

ログには以下の情報が出力されました。sample_voice_silent.mp3は、
長さ:08.07秒
フォーマット:mp3
チャンネル:ステレオ
の音源であることが分かります。

Log
D/VOICE INFO BEFORE: ffmpeg version n5.1.2 Copyright (c) 2000-2022 the FFmpeg developers
      built with Android (7155654, based on r399163b1) clang version 11.0.5 
      ~~省略~~
    [mp3 @ 0x758022f97dc0] Estimating duration from bitrate, this may be inaccurate
    Input #0, mp3, from '/data/user/0/com.example.ffmpeg_practice/files/sample_voice_silent.mp3':
      Metadata:
        TSS             : GarageBand 10.4.7
        iTunNORM        :  00001F65 00001F65 0000745F 0000745F 00000A4E 00000A4E 00007E86 00007E86 00000843 00000843
        iTunSMPB        :  00000000 00000210 00000A50 0000000000056220 00000000 0001EB6E 00000000 00000000 00000000 00000000 00000000 00000000
      Duration: 00:00:08.07, start: 0.000000, bitrate: 132 kb/s
      Stream #0:0: Audio: mp3, 44100 Hz, stereo, fltp, 128 kb/s
    Stream mapping:
      Stream #0:0 -> #0:0 (mp3 (mp3float) -> pcm_s16le (native))
    ~~省略~~
D/VOICE INFO BEFORE: video:0kB audio:1390kB subtitle:0kB other streams:0kB global headers:0kB muxing overhead: unknown

冒頭の無音部分を切り取る

では、FFmpegのコマンドが使えることがわかったので、音声ファイルの冒頭にある無音部分を切り取ってみます!
FFmpegのドキュメントを参照するとsilenceremoveという無音部分を削除する便利なコマンドがあるのでこちらを使っていきましょう。
以下のコードでは、
start_periods=1 オーディオの先頭でトリミングを行う
start_duration=0 オーディオのトリミングを停止する前に無音を残す時間は0とする
stop_threshold=-50d 閾値を-50dBとする
というパラメータを設定しています。

MainFragment.kt
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)
        val filePath = context?.filesDir?.path + File.separator + "sample_voice_silent.mp3"
        val mediaInformation = FFmpegKit.execute("-i  $filePath -f null -")
        //val mediaInformation = FFprobeKit.getMediaInformation(filePath)
        Log.d("VOICE INFO BEFORE", mediaInformation.logsAsString)

+       val outputFilePath = context?.filesDir?.path + File.separator + "output.mp3"
+       val removeSilence = FFmpegKit.execute("-i  $filePath -af silenceremove=start_periods=1:start_duration=0:stop_threshold=-50dB $outputFilePath")
+	val outputmediaInformation = FFmpegKit.execute("-i  $outputFilePath -f null -")
        //val outputMediaInformation = FFprobeKit.getMediaInformation(outputFilePath)
+       Log.d("VOICE INFO AFTER", outputMediaInformation.toString())
    }

出力は以下のようになりました。
音声の長さが06.11秒と短くなっていることが分かります。

Log
D/VOICE INFO AFTER: FFmpegSession{sessionId=3, 
	~~省略~~
    Input #0, mp3, from '/data/user/0/com.example.ffmpeg_practice/files/output.mp3':
      Metadata:
        TSS             : GarageBand 10.4.7
        iTunNORM        :  00001F65 00001F65 0000745F 0000745F 00000A4E 00000A4E 00007E86 00007E86 00000843 00000843
        iTunSMPB        :  00000000 00000210 00000A50 0000000000056220 00000000 0001EB6E 00000000 00000000 00000000 00000000 00000000 00000000
        encoder         : Lavf59.27.100
      Duration: 00:00:06.11, start: 0.025057, bitrate: 128 kb/s
      Stream #0:0: Audio: mp3, 44100 Hz, stereo, fltp, 128 kb/s
        Metadata:
          encoder         : Lavc59.37
    Stream mapping:
      Stream #0:0 -> #0:0 (mp3 (mp3float) -> pcm_s16le (native))
	~~省略~~

波形で確認してみましょう!
冒頭部分にあった無音部分が削除されています。

silenceremove適用前後の比較

まとめ

今回は、AndroidでFFmpegKitを用いてFFmpegを利用する手順を学ぶことが出来ました。
FFmpegはコマンドが大変豊富にあり、deesserというサ行などの歯擦音の抑制やSpeech Normalizerのフィルターなどがあり様々なユースケースで選択肢の一つになりそうだと感じました。
パラメータの調整をさらに知っていけば色々なことが出来そうです!

Discussion