エムスリーテックブログ

エムスリー(m3)のエンジニア・開発メンバーによる技術ブログです

Flutterで音を奏でよう!リアルタイムオーディオ生成アプリの第一歩

こんにちは、デジスマチームでソフトウェアエンジニアをしている立花です。 最近の趣味は ゆる言語学ラジオ - YouTube を聞くことです。宜しくお願いします。

このブログはデジスマチームブログリレーの7日目の記事です。

Flutterでリアルタイムオーディオ生成

この記事ではFlutterアプリでリアルタイムに音を生成・再生する仕組みを実装し、最終的にAndroid実機で音を鳴らすまでを解説します。 この記事を読むことでFlutterアプリにリアルタイムオーディオ生成機能を組み込むための第一歩が踏み出せるはずです。

Flutterで音の波形を生成・再生する

それでは具体的な実装方法を見ていきましょう。

Githubで良さげなRust製の音声処理ライブラリを見つけたので、今回はオーディオ生成部分をRustで実装する事にします。

使用するライブラリは次の3つです。

cpal

Rustでオーディオ入出力するための低レベルライブラリです。 主要なOS(Linux、Windows、macOS、iOS、Androidなど)に対応しており、プラットフォーム毎のオーディオAPIの違いを吸収してくれます。

github.com

FunDSP

Rust製のデジタル信号処理 (DSP) ライブラリです。 オーディオ処理の流れを数式のように記述でき、音の基本波形を生成するオシレーター、ハイパスやローパスなどのフィルター、ディレイやリバーブなどのエフェクトなどオーディオ処理に必要なコンポーネントが予め用意されています。

github.com

flutter_rust_bridge

Flutter (Dart) とRustのコードを安全に繋ぐためのブリッジコードを自動生成してくれるライブラリです。 非同期処理にも対応しており、双方の言語間でシームレスな関数呼び出しを実現します。

github.com

まずはRust単体で音を鳴らしてみる

Flutterアプリに組み込む前に、まずはRustのみを使ってmacOS上で音を鳴らすプログラムを作ってみます。

FunDSP リポジトリ内のサンプルコードを元に、オルガンの音を再生するプログラムを作成しました。

sound-test/src/main.rs at 0.1.0 · plavelo/sound-test · GitHub

以下のコマンドで音が再生されます。注意点として、リリースモード(--release)で実行しないと音が歪むなど正しく再生されない場合があります。

cargo run --release

生成された音はこんな感じです。(音量注意)

Claude Codeの力でアコースティックギターの音に変更

オルガンの音を鳴らせましたが、せっかくなので他の音も鳴らしたいところです。 しかし、シンセサイザーの実装はそれなりに専門知識が必要なため簡単ではありません。

そんな時はClaude Codeにおまかせ

Claude Codeはシンセサイザーの実装はできるのでしょうか? できます。
Claude Codeは音も理解してくれるのか、すごい。

Claude Codeと次のような対話を通じて、音をアコースティックギターのものに変更して貰いました。 この過程で、弦楽器の音を再現する Karplus-Strongアルゴリズム*1 を採用する事になりました。 このアルゴリズムは FunDSP に既に用意されていたためスムーズに実装できました。

prompt: main.rsはfundspを使用したオルガンの音を鳴らすプログラムです。これをアコースティックギターの音に変更して。
claude: オルガン音からアコースティックギター音に変更しました。
prompt: よりリアルなアコースティックギターの音に改良したいのですが、リアルなギター音のシンセサイザーを実装する上で重要なポイントについて教えて。コードの修正は行わずまず考えて。
(ここで何度かやりとり)
claude: 最も実装しやすく効果的なのはKarplus-Strongアルゴリズムです。物理的に正確で、fundspの既存機能で実装可能です。
prompt: Karplus-Strongアルゴリズムを使用して、よりリアルなアコースティックギターの音に改良して下さい。
claude: Karplus-Strongアルゴリズムを使用してよりリアルなアコースティックギター音に改良しました。

完成した音がこちらです。(音量注意)

だいぶ弦楽器らしい響きになりました。

単音だけでは物足りないので、次のプロンプトでドレミファソラシドを鳴らすように修正を依頼しました。

prompt:  Cメジャースケールを主音から順に1音ずつ鳴らすように変更して下さい。BPM=120で各音の長さは四分音符にして。

こうなりました。

もう少しギターの音を改良できそうな気がしますが、今回はここまでにしてFlutterへの組み込みに進みます。

ここまでのコードは以下で確認できます。

sound-test/src/main.rs at 0.3.1 · plavelo/sound-test · GitHub

Flutterアプリへの組み込み

完成したRustコードをflutter_rust_bridgeを使ってFlutterアプリに組み込み、Android実機で音が再生できることを目指します。

まず、以下のコマンドでFlutterプロジェクトを作成します。

cargo install flutter_rust_bridge_codegen
flutter_rust_bridge_codegen create sound_app

作成されたプロジェクトを確認すると、次のRustコードとDartコードが生成されているのがわかります。

rust/src/api/simple.rs

#[flutter_rust_bridge::frb(sync)]
pub fn greet(name: String) -> String {
    format!("Hello, {name}!")
}

lib/src/rust/api/simple.dart

String greet({required String name}) =>
    RustLib.instance.api.crateApiSimpleGreet(name: name);

生成されたDartの greet() 関数はFlutter側から自由に呼び出すことができます。

class MyApp extends StatelessWidget {
  const MyApp({super.key});

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      home: Scaffold(
        appBar: AppBar(title: const Text('flutter_rust_bridge quickstart')),
        body: Center(
          child: Text(
            'Action: Call Rust `greet("Tom")`\nResult: `${greet(name: "Tom")}`',
          ),
        ),
      ),
    );
  }
}

実行すると次のような画面が表示され、FlutterからRustの関数を呼び出せていることが確認できました。

rust/src/api/simple.rs に新しいRustの関数を追加した際は、以下のコマンドでブリッジコードを再生成できます。

flutter_rust_bridge_codegen generate

作成したRustのプログラムをFlutterに組み込む

ここから、先ほど作成したRustの音声再生プログラムをFlutterプロジェクトに移植していきます。

まず rust/Cargo.tomlcpal などの依存関係を追加します。

[dependencies]
cpal = "0.16.0"
fundsp = "0.20.0"
anyhow = "1.0.98"

次に、AndroidでNDKのAPIを利用するため、以下のドキュメントを参考にCargo.tomlに設定を追加します。

Android NDK Init | flutter_rust_bridge

[target.'cfg(target_os = "android")'.dependencies]
jni = "0.21.1"
ndk-context = "0.1.1"

rust/src/lib.rs へ以下を追加します。

#[cfg(target_os = "android")]
#[no_mangle]
pub extern "C" fn JNI_OnLoad(vm: jni::JavaVM, res: *mut std::os::raw::c_void) -> jni::sys::jint {
    use std::ffi::c_void;

    let vm = vm.get_java_vm_pointer() as *mut c_void;
    unsafe {
        ndk_context::initialize_android_context(vm, res);
    }
    jni::JNIVersion::V6.into()
}

MainActivity.kt に次のようにAndroid NDKの初期化のためのinitブロックを追加します。

class MainActivity : FlutterActivity() {
    init {
        System.loadLibrary("rust_lib_sound_app")
    }
}

System.loadLibrary() の引数には、次のコマンドで見つかるファイル名のうち先頭のlibと拡張子を除いた部分を指定します。 見つかったファイル名が librust_lib_sound_app.so なら rust_lib_sound_app を指定します。

find build -type f -wholename '*/jniLibs/*.so'

cpal はAAudio API (Android 8.0 / API level 26以降) を利用するため、 android/app/build.gradle.kts のminSdkを 26 に、またndkVersionを 27.0.12077973 に設定します。

android {
    ...
    ndkVersion = "27.0.12077973"
    ...
    defaultConfig {
        ...
        minSdk = 26

作成したシンセサイザーのRustコードを rust/src/api/simple.rsplay() 関数として移植します。

#[flutter_rust_bridge::frb(sync)]
pub fn play() {
   // 略
}

今回移植したRustコードは以下で確認できます。

sound_app/rust/src/api/simple.rs at main · plavelo/sound_app · GitHub

コードを移植後、先ほどのコマンドでブリッジコードを再生成します。

flutter_rust_bridge_codegen generate

すると lib/src/rust/api/simple.dartplay() 関数が生成されるので、これをFlutter側から呼び出します。

void play() => RustLib.instance.api.crateApiSimplePlay();

NDKのAPIを呼び出すRustコードをは、DartのIsolate内でそのまま呼び出すとエラーが発生します。 次のドキュメントを参考に、Isolateの開始と終了時にそれぞれ RustLib.init() RustLib.dispose() を呼び出すよう修正します。

Dart Isolates | flutter_rust_bridge

Future<void> _playSound(void _) async {
  await RustLib.init();
  play();
  RustLib.dispose();
}

class _MyApp extends StatelessWidget {
  const _MyApp();

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      home: Scaffold(
        appBar: AppBar(title: const Text('flutter_rust_bridge quickstart')),
        body: Center(
          child: GestureDetector(
            onTap: () {
              compute(_playSound, null);
            },
            child: Text(
              'Action: Call Rust `greet("Tom")`\nResult: `${greet(name: "Tom")}`',
            ),
          ),
        ),
      ),
    );
  }
}

最後にAndroid Studioのメニューから Run > Flutter Run 'main.dart' in Release Mode を選択し、Android実機でアプリを実行します。 ここでもRelease Modeで実行しないと音が歪んで再生されてしまうため注意して下さい。

これでAndroid実機でアプリを起動し画面上のテキストをタップすると音が鳴るはずです。

今回作成したアプリのコードはこちらから確認できます。

github.com

まとめ

cpal & fundsp & flutter_rust_bridgeとClaude Codeを活用し、Flutterアプリでリアルタイムに音を生成・再生する方法を解説しました。

今回はAndroidでの動作確認まででしたが、使用したライブラリはクロスプラットフォーム対応のためiOSなど他のOSでも実装可能です。

皆さんも是非、自分なりの音を生成して様々なアプリケーションを開発してみてください。

We are Hiring!

エムスリーでは、アプリ・フロントエンド・バックエンドに関わらず、新しい技術に興味のあるエンジニアを募集しています。

エンジニア採用ページはこちら

jobs.m3.com

エンジニア新卒採用サイト! !

fresh.m3recruit.com

カジュアル面談! !

jobs.m3.com