【Flutter】モダンなAudioVisualizationのパッケージ audio_flux を試す
概要
今回は再生されたサウンドや、マイクからの入力などをいい感じに可視化してくれるFlutter パッケージの audio_flux を試してみました。
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_soloud、flutter_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ファイルが再生されています)
実装の詳細を見ていきたいと思います。
flutter_soloud
audioファイルを再生している箇所を見ていきたいと思います。こちらは flutter_soloud
Pubを使って再生しています。
final soloud = SoLoud.instance;
まずは flutter_soloud
の SoLoud
インスタンスを取得し、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());
}
}
ここで bufferSize
と soloud.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,
),
),
)
実行結果
見た目はアレですが、雰囲気は分かります。
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',
),
),
)
実行結果
面白いですね ✨
入力音源を可視化する実装
次に 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),
],
),
),
);
}
}
実際に動作させた画面がこちらになります。分かりづらいと思いますが、何度かマイクの前で指パッチンしてその度に反応しています。
Discussion