🎙️

【Flutter】モダンなAudioVisualizationのパッケージ audio_flux を試す

に公開

概要

今回は再生されたサウンドや、マイクからの入力などをいい感じに可視化してくれるFlutter パッケージの audio_flux を試してみました。

audio_flux

https://pub.dev/packages/audio_flux

リアルタイムのオーディオデータをキャプチャして表示するFlutter用の多機能オーディオビジュアライゼーションパッケージ。再生にflutter_soloud、録音にflutter_recorderを使い、従来のCustomPainterレンダリングとshader_bufferを使った高性能なシェーダーベースのビジュアライゼーションの両方を提供します。

作業環境

  • MBA M3 Sequoia 15.6.1
  • flutter 3.35.1
  • iOSシュミレータ iOS 18.1
  • Androidエミュレータ API 34

インストール

audio_flux と依存している flutter_soloudflutter_recorderのパッケージが必要になります

  • pubspec.yaml
dependencies:
  audio_flux: ^1.2.1
  flutter_soloud: ^3.3.0
  flutter_recorder: ^1.1.2

サンプル音源を可視化する実装

早速何かサンプル音源を再生して、audio_fluxを試してみたいと思います。音源はリポジトリの example にある こちら を使います。

音源を assets/audio/ElectroNebulae.mp3 にダウンロードしときます。 pubspec.yaml に以下も忘れず設定しときます。

flutter:
  assets:
    # ...
    - assets/audio/

次に audio_flux_screen.dart という名前でデモ画面を以下内容で作成します。

import 'package:audio_flux/audio_flux.dart';
import 'package:flutter/material.dart';
import 'package:flutter_soloud/flutter_soloud.dart';

class AudioFluxScreen extends StatefulWidget {
  const AudioFluxScreen({super.key});

  
  State<AudioFluxScreen> createState() => _AudioFluxScreenState();
}

class _AudioFluxScreenState extends State<AudioFluxScreen> {
  final soloud = SoLoud.instance;

  
  void initState() {
    super.initState();

    initSoLoud('assets/audio/ElectroNebulae.mp3');
  }

  
  void dispose() {
    soloud.deinit();
    super.dispose();
  }

  Future<void> initSoLoud(String audioAsset) async {
    try {
      await soloud.init(bufferSize: 1024, channels: Channels.stereo);
      soloud.setVisualizationEnabled(true);

      await soloud.play(
        await soloud.loadAsset(audioAsset, mode: LoadMode.disk),
        looping: true,
      );
    } on Exception catch (e) {
      debugPrint(e.toString());
    }
  }

  
  Widget build(BuildContext context) {
    return Scaffold(
      backgroundColor: Colors.green,
      appBar: AppBar(
        leading: IconButton(
          icon: Icon(Icons.arrow_back, color: Colors.white),
          onPressed: () => Navigator.of(context).pop(),
        ),
        title: const Text(
          'Audio Flux',
          style: TextStyle(color: Colors.white, fontWeight: FontWeight.bold),
        ),
        backgroundColor: Colors.green,
      ),
      body: Padding(
        padding: const EdgeInsets.all(8.0),
        child: Column(
          children: [
            Text(
              "Audio visualizer for Flutter which uses flutter_soloud, flutter_recorder, and shader_buffer",
              style: TextStyle(color: Colors.white, fontSize: 13, height: 1.4),
            ),
            const SizedBox(height: 20),
            const Text(
              "Waveform Visualization",
              style: TextStyle(
                color: Colors.white,
                fontWeight: FontWeight.bold,
              ),
            ),
            SizedBox(
              width: double.infinity,
              height: 250,
              child: AudioFlux(
                fluxType: FluxType.waveform,
                dataSource: DataSources.soloud,
                modelParams: ModelParams(
                  waveformParams: WaveformPainterParams(
                    barsWidth: 2,
                    barSpacingScale: 0.5,
                    chunkSize: 1,
                  ),
                ),
              ),
            ),
          ],
        ),
      ),
    );
  }
}

こちらをiOSシュミレータで動かすと👇の様に表示されます。(実際にはaudioファイルが再生されています)

image1.gif

実装の詳細を見ていきたいと思います。

flutter_soloud

audioファイルを再生している箇所を見ていきたいと思います。こちらは flutter_soloud Pubを使って再生しています。

final soloud = SoLoud.instance;

まずは flutter_soloudSoLoud インスタンスを取得し、initState 内でaudioファイルを読み込んで再生までを行なっています。

  
  void initState() {
    super.initState();

    initSoLoud('assets/audio/ElectroNebulae.mp3');
  }
  
  Future<void> initSoLoud(String audioAsset) async {
    try {
      await soloud.init(bufferSize: 1024, channels: Channels.stereo);
      soloud.setVisualizationEnabled(true);

      await soloud.play(
        await soloud.loadAsset(audioAsset, mode: LoadMode.disk),
        looping: true,
      );
    } on Exception catch (e) {
      debugPrint(e.toString());
    }
  }

ここで bufferSizesoloud.setVisualizationEnabled(true) を指定することで再生中のオーディオから可視化する為に必要なデータにアクセスできる様にします。

AudioFlux

次に可視化する箇所 AudioFlux の部分を見ていきます。

AudioFlux(
  fluxType: FluxType.waveform,
  dataSource: DataSources.soloud,
  modelParams: ModelParams(
    waveformParams: WaveformPainterParams(
      barsWidth: 2,
      barSpacingScale: 0.5,
      chunkSize: 1,
    ),
  ),
)

AudioFluxに指定できるパラメータ

  • fluxType: 可視化スタイル
    • FluxType.waveform: オーディオ波形をオシロスコープ形式でリアルタイム表示
    • FluxType.fft: 周波数スペクトル解析の可視化
    • FluxType.shader: カスタムGPU加速ビジュアルエフェクト
  • dataSource: Audioソース
    • DataSource.soloud: 再生ストリームからオーディオをキャプチャする
    • DataSource.recorder: マイク/入力からの音声をキャプチャします
  • modelParams: タイプ固有のパラメータで可視化動作を設定する

上記例では可視化スタイルに FluxType.waveform を、Audioソースに DataSource.soloud を指定しています。modelParams では WaveformPainterParams で以下を設定しています。

  • barsWidth : バーのサイズ(ピクセル単位)
  • barSpacingScale : バー間の間隔のサイズ(ピクセル単位)
  • chunkSize : 波形に平均化して追加する新規データの数。数値が大きいほど波形の動きは遅くなる (値は 1 以上 256 以下)

FluxType.fft

次に FluxType.fft を試してみたいと思います。周波数スペクトルとは、信号(音、電波、振動など)に含まれる 周波数成分を分解して、グラフやビジュアルで表示するもの になります。

👇こちらの実装で試してみました。

AudioFlux(
  fluxType: FluxType.fft,
  dataSource: DataSources.soloud,
  modelParams: ModelParams(
    fftParams: FftParams(
      minBinIndex: 5,
      maxBinIndex: 80,
      fftSmoothing: 0.95,
    ),
  ),
)

実行結果

image2.gif

見た目はアレですが、雰囲気は分かります。

FluxType.shader

次にFluxType.shaderを試してみたいと思います。カスタムのshaderで可視化するスタイルになっている様です。shaderファイルはリポジトリのexampleにあるこちらを使って試してみたいと思います。

shaderファイルを assets/shaders/dancing_flutter.frag におき pubspec.yaml に以下を追加しときます。

flutter:
	# ...
  shaders:
    - assets/shaders/dancing_flutter.frag

また、dancing_flutter.frag から include されている common ディレクトリ内のファイルも追加しときます。こちら

assets
└── shaders
    └── common
        ├── common_header.frag
        └── main_shadertoy.frag

👇こちらの実装で試してみました。

AudioFlux(
  fluxType: FluxType.shader,
  dataSource: DataSources.soloud,
  modelParams: ModelParams(
    shaderParams: ShaderParams(
      shaderPath: 'assets/shaders/dancing_flutter.frag',
    ),
  ),
)

実行結果

image3.gif

面白いですね ✨

入力音源を可視化する実装

次に flutter_recorder を利用して入力音源(マイク) からのソースを元に可視化する実装を行なっていきたいと思います。

セットアップ

マイクを使うためのPermissionの設定をAndroid/iOSそれぞれ設定します。

  • Android

AndroidManifest.xml に以下を追加する必要があります。

<uses-permission android:name="android.permission.RECORD_AUDIO" />
  • iOS

Info.plist に以下を追加する必要があります。

<key>NSMicrophoneUsageDescription</key>
<string>Some message to describe why you need this permission</string>

※メッセージの内容はアプリに合わせて記述してください

次にPermissionの確認要求を行う為に permission_handler パッケージをインストールしときます。 pubspec.yaml に以下を追加します。

dependencies:
  permission_handler: ^12.0.1

実装

audio_flux_recorder.dart という名前でflutter_recorder を利用したデモ画面を作成します。

import 'package:audio_flux/audio_flux.dart';
import 'package:flutter/material.dart';
import 'package:flutter_recorder/flutter_recorder.dart';
import 'package:permission_handler/permission_handler.dart';

class AudioFluxRecorderScreen extends StatefulWidget {
  const AudioFluxRecorderScreen({super.key});

  
  State<AudioFluxRecorderScreen> createState() =>
      _AudioFluxRecorderScreenState();
}

class _AudioFluxRecorderScreenState extends State<AudioFluxRecorderScreen> {
  final recorder = Recorder.instance;

  
  void initState() {
    super.initState();

    _requestMicrophonePermission();
  }

  Future<void> _requestMicrophonePermission() async {
    final status = await Permission.microphone.status;

    if (status.isDenied || status.isRestricted || status.isPermanentlyDenied) {
      final result = await Permission.microphone.request();
      if (result.isGranted) {
        await initRecorder();
      }
    } else if (status.isGranted) {
      await initRecorder();
    }
  }

  
  void dispose() {
    recorder.deinit();
    super.dispose();
  }

  Future<void> initRecorder() async {
    try {
      /// [PCMFormat.f32le] is required for getting audio data to work.
      await recorder.init(sampleRate: 44100, format: PCMFormat.f32le);
      recorder.start();
    } on Exception catch (e) {
      debugPrint(e.toString());
    }
  }

  
  Widget build(BuildContext context) {
    return Scaffold(
      backgroundColor: Colors.orange,
      appBar: AppBar(
        leading: IconButton(
          icon: Icon(Icons.arrow_back, color: Colors.white),
          onPressed: () => Navigator.of(context).pop(),
        ),
        title: const Text(
          'Audio Flux - Recorder',
          style: TextStyle(color: Colors.white, fontWeight: FontWeight.bold),
        ),
        backgroundColor: Colors.orange,
      ),
      body: SingleChildScrollView(
        padding: const EdgeInsets.all(8.0),
        child: Column(
          children: [
            Text(
              "Captures audio from microphone/input",
              style: TextStyle(color: Colors.white, fontSize: 13, height: 1.4),
            ),
            const SizedBox(height: 20),
            const Text(
              "Waveform Visualization",
              style: TextStyle(
                color: Colors.white,
                fontWeight: FontWeight.bold,
              ),
            ),
            SizedBox(
              width: double.infinity,
              height: 250,
              child: AudioFlux(
                fluxType: FluxType.waveform,
                dataSource: DataSources.recorder,
                modelParams: ModelParams(
                  waveformParams: WaveformPainterParams(
                    barsWidth: 2,
                    barSpacingScale: 0.5,
                    chunkSize: 1,
                  ),
                ),
              ),
            ),
            const SizedBox(height: 20),
            const Text(
              "Frequency spectrum analysis visualization",
              style: TextStyle(
                color: Colors.white,
                fontWeight: FontWeight.bold,
              ),
            ),
            SizedBox(
              width: double.infinity,
              height: 250,
              child: AudioFlux(
                fluxType: FluxType.fft,
                dataSource: DataSources.recorder,
                modelParams: ModelParams(
                  fftParams: FftParams(
                    minBinIndex: 5,
                    maxBinIndex: 80,
                    fftSmoothing: 0.95,
                  ),
                ),
              ),
            ),
            const SizedBox(height: 20),
            const Text(
              "Custom GPU-accelerated visual effects",
              style: TextStyle(
                color: Colors.white,
                fontWeight: FontWeight.bold,
              ),
            ),
            SizedBox(
              width: double.infinity,
              height: 250,
              child: AudioFlux(
                fluxType: FluxType.shader,
                dataSource: DataSources.recorder,
                modelParams: ModelParams(
                  shaderParams: ShaderParams(
                    shaderPath: 'assets/shaders/dancing_flutter.frag',
                  ),
                ),
              ),
            ),
            const SizedBox(height: 20),
          ],
        ),
      ),
    );
  }
}

実際に動作させた画面がこちらになります。分かりづらいと思いますが、何度かマイクの前で指パッチンしてその度に反応しています。

image4.gif

Discussion