こんにちは、ぎもちんです。
音声ライブ配信アプリ Voice Pococha の Flutter アプリ開発をしています。
Voice Pococha では昨年、 C 言語による低レイヤな音声処理を行うための音声基盤を導入・リリースしました。
この記事では、そのような音声基盤をアプリに導入した背景や、技術選定、実装手法についてお話しします。
音声基盤とは
音声基盤とは Voice Pococha アプリで音声処理を行うためのフレームワークを指します。
Voice Graph と呼ばれており、カラオケ機能追加に伴って実装・導入されました。
以下の3点を主軸に、 C 言語で開発されています
- Kotlin/Swift との相互運用
- 低いメンテナンスコスト
- 高い拡張性とパフォーマンス
カラオケ機能要件
音声基盤の説明をする前に、まずは音声基盤を導入する理由となった、カラオケ機能の要件について説明します。
(音声に関する処理のみを抜粋)
必要な機能要件は以下のとおりです。
- マイク音声とカラオケ音源のミキシング
- カラオケ音源の再生
- キー変更
- エコーエフェクト
これ以外に、音ズレ防止を目的として以下の非機能要件も挙げられました。
- カラオケ音源の再生タイミングと、ミキシングタイミングの同期
- 低レイテンシーな音声処理
音声処理は Flutter や Dart においてあまり充実したサポートがなく、これらを 0 から作る必要がありました。
iOS には音声処理の標準機能が多数あるため実装できそうでしたが、 Android は機能が限られており、この要件を満たすことが困難でした。
そこで、 Flutter 同様に単一コードで音声処理ができる基盤が必要と考えました。
これが、音声基盤の導入に至る背景となります。
技術選定と設計
音声基盤のアイデアをチームと相談し、実現可能性を調べることになりました。
フルスクラッチ実装は避けたいため、まずはライブラリの選定から行いました。
ライブラリの選定
上述のカラオケ機能要件を満たすことを最低限に考えつつ、将来的な拡張性、パフォーマンス面、メンテナンスコストも交えてライブラリ選定を行いました。
音声処理の柔軟性
Voice Pococha は音声ライブ配信アプリのため、将来的に音声処理が拡張されるのも不思議ではありません。
そのためライブラリ選定では、「機能要件を満たせるかどうか」よりも「音声基盤という下地が作れるか」を念頭に探しました。
もともと DirectShow や AVAudioEngine といった技術には知見があったため、これに近いグラフ・ノードの概念で音声処理ができることが理想と考えました。
パフォーマンスと最適化余地
音声処理は一般に、1秒あたりに数万の音声サンプルに対して処理するため、低レイテンシーが求められる場面ではCPU負荷が高まりやすいです。
そこでパフォーマンス悪化を抑えるため、以下の2点を観点として考えました。
- 低いCPU負荷で音声処理ができる/しやすい
- CPU負荷が調整しやすい
メンテナンスコスト
音声処理は、デジタル信号処理やハードウェアに関する専門的な知識が求められることがあります。
しかし Voice Pococha のクライアントアプリエンジニアは、基本的に Flutter アプリをメインに開発しています。
そのため、ドメイン固有の知識は要求せずに使えるライブラリを探しました。
また Android と iOS に対応する必要があり、できれば単一のコードベースで書けることを理想としました。
選定結果
以上の機能要件と観点をもとに候補を洗い出し、 miniaudio というライブラリを使うことにしました。 miniaudio は C 言語で作成されているクロスプラットフォーム音声処理ライブラリです。
1つのヘッダファイルだけで、 iOS/macOS/Android/Windows/Linux といった主要プラットフォームに対応しています。
観点と照らし合わせると、以下のようになります
- カラオケ機能要件
- 「キー変更」に必要なピッチシフト以外の機能は全て内蔵
- 音声処理の柔軟性
- グラフ・ノードを用いた音声処理システムを内蔵
- パフォーマンスと最適化余地
- NEON等の拡張命令を用いた最適化がされているため、CPU負荷を下げやすい
- 柔軟な設定ができるためパフォーマンス最適化を行いやすい。一方で細かい調整が必要になる
- メンテナンスコスト
- 単一コードで全ての処理が組めるため、コード量を抑えられる
- C 言語のため、慣れていない人にとっては読みづらい
ほぼ全ての必要な機能が内蔵されている、まさに夢のようなライブラリです。
ライセンス上の問題もなかったため、こちらのライブラリを使うことに決めました。
言語の選定
次に Voice Graph をどのような言語で開発すべきか考えました。
miniaudio には必要な機能が一通り揃っていますが、それらをカラオケ機能向けに組み合わせるプログラムが必要です。
結果として以下の理由から、 Voice Graph も C 言語を使用することにしました。
- miniaudio が C 言語で作られていること
- Kotlin/Swift と相互運用できる言語が主に C/C++ であること
C 言語はメモリ安全面が不安ですが、Rust 等はチーム内に経験者がいないため避けることにしました。
その代わりに Voice Graph 全体が単一言語での実装となり、メンテナンスコストの削減につながると判断しました。
設計
上述のライブラリと言語を用いて、 Voice Graph を作成することにしました。
各言語・プラットフォーム間の繋ぎ込みは以下のようになります。
- C 言語で実装した Voice Graph を、 Android と iOS 向けにクロスコンパイルする
- Android では JNI1 を介して Voice Graph を扱う
- iOS では Swift から直接 Voice Graph を扱う
- Flutter からは MethodChannel を用いる
以下が Voice Graph を導入する前と後での音声処理の比較になります。
実装の結果
音声処理において Flutter と C 言語を掛け合わせた実装は社内・社外ともにほとんど前例がなく、とてもチャレンジングな実装でした。
miniaudio 自体が多機能でバグが少ない点に大きく助けられ、幸い Voice Graph や miniaudio に起因する不具合がほとんど無いまま開発を終えることができました。
実装工数
iOS と Android それぞれで実装する場合と比較して、 30~40% ほどの工数が削減できたと思います。
期待値としては 50% の削減を目指していましたが、OSやハードウェア依存の対応や、ビルド環境整備などが追加で発生したためです。
メンテナンスコスト
メンテナンスコストについても、以下の点から大きく削減できたと考えています。
- 単一コードのため、そもそものコード量が少ない
- 特殊な最適化処理等は miniaudio 内部に隠蔽されていて、専門知識がさほど要求されない
結果として C 言語と Flutter を組み合わせる特殊な実装になりましたが、
高いパフォーマンスを維持しつつ、単一コードで音声処理ができる仕組みを作れました。
最後に
音声基盤の設計と実装を通じて、チャレンジングな実装に取り組めたことや、 miniaudio という素晴らしいライブラリと出会えたことを嬉しく思っています。
余談ですがこの経験をもとに、個人的に
coast_audio
という音声処理ライブラリを開発しています。
Swift/Kotlin/Flutter に依存せず、 Dart の FFI2 と miniaudio を用いて音声処理を行うライブラリで、 Voice Graph と近いアプローチで開発されています。
この記事や上述のライブラリが、 Flutter アプリ開発で同様の悩みを抱える方の一助となれば幸いです。
最後まで読んでいただき、ありがとうございます!
この記事をシェアしていただける方はこちらからお願いします。