miniaudio_dart 1.0.5
miniaudio_dart: ^1.0.5 copied to clipboard
A high-level real-time audio playback library based on Miniaudio.
miniaudio_dart #
A implementation for miniaudio supporting native and web platforms.
Web setup #
Include the loader (do NOT defer) in web/index.html
:
<script src="assets/packages/miniaudio_dart_web/js/miniaudio_dart_web.loader.js"></script>
Inside the onload bootstrap add:
await _miniaudio_dart.loader.load();
This package uses SharedArrayBuffer; enable cross‑origin isolation:
- Cross-Origin-Opener-Policy: same-origin
- Cross-Origin-Embedder-Policy: require-corp
Core concepts #
Component | Purpose |
---|---|
Engine | Playback device + sound management |
Sound | Decoded or raw PCM (supports loop + loop delay) |
StreamPlayer | Low latency push of Float32 interleaved frames |
Recorder | Capture microphone to ring buffer or file |
Generator | Procedural waveform / noise source |
New / changed APIs (device + stream management) #
Playback (output) devices #
await engine.enumeratePlaybackDevices(); // List (name,isDefault)
await engine.selectPlaybackDeviceByIndex(index); // Switch (invalidates sounds)
await engine.switchPlaybackDevice(index); // Simple convenience
await engine.switchPlaybackDeviceAndRebuild(index); // Rebind loaded sounds
await engine.switchPlaybackDevicePreservingMonitoring(
index: index,
monitorPlayer: streamPlayer,
recorder: recorder,
rebindSounds: true,
);
engine.playbackDeviceChanges().listen(...); // Polling stream
Notes:
- Raw switch invalidates existing native sound objects; use one of the “switch*AndRebuild / PreservingMonitoring” helpers for transparent recovery.
- StreamPlayers are recreated internally when using
switchPlaybackDevicePreservingMonitoring
. - Web: enumeration returns a single logical “Default Output”; switching is a no‑op.
Capture (input) devices #
await recorder.enumerateInputDevices(); // List (name,isDefault)
await recorder.switchInputDevicePreservingStream(index);// Minimal interruption
recorder.inputDeviceChanges().listen(...); // Polling stream
Notes:
- Switching input preserves ring buffer when format/channels unchanged.
- Web: single “Default Input”; switching is a no‑op.
Monitoring (record → playback) #
await recorder.initStream(sampleRate: 48000, channels: 1);
recorder.start();
final monitor = StreamPlayer(mainEngine: engine);
await recorder.enableMonitoring(monitor);
monitor.volume = 0.8;
When switching only output device:
await engine.switchPlaybackDevicePreservingMonitoring(
index: newIdx,
recorder: recorder,
monitorPlayer: monitor,
rebindSounds: true,
);
Recorder keeps capturing; monitor player is rebuilt quickly.
Sound looping #
sound.playLooped(delay: Duration(milliseconds: 500));
sound.stop(); // Resets position (loop delay cleared)
sound.pause(); // Keeps position (loop disabled)
Rebinding sounds after device change #
Performed automatically by:
switchPlaybackDeviceAndRebuild
switchPlaybackDevicePreservingMonitoring (rebindSounds: true)
Web implementation is a no‑op (single logical device).
Quick usage snippets #
Engine + Sound #
final engine = Engine();
await engine.init(); // optional periodMs arg
await engine.start();
final audioData = AudioData(bytes: myUint8List); // Provide decoded or encoded data
final sound = await engine.loadSound(audioData);
sound.volume = 0.5;
sound.playLooped(delay: Duration(seconds: 1));
await Future.delayed(Duration(seconds: 3));
sound.stop();
Enumerate & switch output (with rebuild) #
final devices = await engine.enumeratePlaybackDevices();
for (var i=0; i<devices.length; i++) {
print('$i: ${devices[i].$1}${devices[i].$2 ? " (default)" : ""}');
}
await engine.switchPlaybackDeviceAndRebuild(targetIndex);
Recorder streaming + monitoring #
final recorder = Recorder(mainEngine: engine);
await recorder.initStream(sampleRate: 48000, channels: 1);
recorder.start();
final monitor = StreamPlayer(mainEngine: engine);
await recorder.enableMonitoring(monitor);
recorder.stream(intervalMs: 20).listen((chunk) {
// chunk = Float32List interleaved frames
});
Switch input device seamlessly #
final inputs = await recorder.enumerateInputDevices();
await recorder.switchInputDevicePreservingStream(newIndex);
Streams & polling #
Device change polling (output):
engine.playbackDeviceChanges(interval: Duration(seconds: 2))
.listen((list) => print('Playback devices updated: $list'));
Input devices:
recorder.inputDeviceChanges().listen((list) {
print('Input devices: $list');
});
Limitations / platform notes #
- Output device switch requires rebuilding native
ma_engine
; helpers wrap rebind/recreate. - StreamPlayer currently supports only
AudioFormat.float32
. - Format / channel changes during input switch are not auto‑negotiated (stop + re‑init required).
- Web: real multi‑device selection not available; APIs return single logical devices; rebind is no‑op.
- Loop delay not accounted in
Sound.duration
.
Generator example #
final generator = Generator();
await generator.initEngine();
await generator.init(AudioFormat.float32, 2, 48000, 5);
generator.setWaveform(WaveformType.sine, 440.0, 0.4);
generator.start();
generator.stream(chunkSizeMs: 50).listen((buf) {
// Process Float32 samples
});
Building #
git submodule update --init --recursive
cd miniaudio_dart_ffi/src/build
# Web
emcmake cmake ..
cmake --build .
# Native (clean build)
rm -rf *
cmake ..
cmake --build .
Regenerate bindings:
cd ../../..
dart run ffigen
TODO #
- ✅ Sound loop clarity
- ✅ Playback device enumeration / switching
- ✅ Input device enumeration / seamless switching
- ✅ Stream monitoring preservation on output switch
- ❌ Graceful handling when no devices present
Troubleshooting #
Issue | Cause | Fix |
---|---|---|
Silence after output switch | Not using rebuild helper | Use switchPlaybackDeviceAndRebuild |
Recorder stops after output switch | Old manual teardown | Use switchPlaybackDevicePreservingMonitoring |
Web start exception (autoplay) | No user gesture | Call engine.start() after user interaction |
Access violation native | Pointer size mismatch (old FFI) | Rebuild + update binding functions |
License #
(Keep original license notice here if applicable.)