flutter_soloud idioms
flutter_soloud is a thin Dart FFI wrapper over the SoLoud C++ engine (miniaudio backend). It is a low-level mixer API, not a media player: you load sounds (AudioSource), then spawn playing instances of them (SoundHandle voices). Everything lives on one global engine behind SoLoud.instance. Read this before writing any flutter_soloud code — assumptions carried over from audioplayers, just_audio, or the Web Audio API will produce wrong code.
Minimal example
import 'package:flutter_soloud/flutter_soloud.dart';
Future<void> main() async {
WidgetsFlutterBinding.ensureInitialized();
// MUST be awaited before any other SoLoud call.
await SoLoud.instance.init();
// Load once: an AudioSource is the loaded sample, identified by its
// SoundHash. Loading is async; keep the source around.
final sound = await SoLoud.instance.loadAsset('assets/explosion.mp3');
// play() is SYNCHRONOUS: it spawns a voice and returns its handle
// immediately. The handle controls that one playing instance.
final SoundHandle handle = SoLoud.instance.play(
sound,
volume: 0.8,
looping: false,
);
// ... on teardown (e.g. top-level widget dispose()):
SoLoud.instance.deinit(); // or: await SoLoud.instance.deinitAsync();
}
The API shape
SoLoud.instance — the singleton. One engine exists in C++, so only one SoLoud exists in Dart; do not construct your own.
Future<void> init({PlaybackDevice? device, bool automaticCleanup = false, int sampleRate = 44100, int bufferSize = 2048, Channels channels = Channels.stereo, bool lowLatency = true, ...}) — initializes the engine. Must be awaited before anything else.
bool get isInitialized — synchronous readiness check (false while init is in flight or after failure/deinit).
void deinit() / Future<void> deinitAsync() — teardown: stops the engine and disposes ALL resources, including loaded sounds. Call once at app exit; deinitAsync runs native teardown off the UI thread.
Future<AudioSource> loadAsset(String key, {LoadMode mode = LoadMode.memory, ...}) — also loadFile(String path) (not on web), loadUrl(String url), loadMem(...), loadWaveform(...). All async; all return AudioSource.
SoundHandle play(AudioSource sound, {int busId = 0, double volume = 1, double pan = 0, bool paused = false, bool looping = false, Duration loopingStartAt = Duration.zero, Duration? loopingEndAt, double scale = 1}) — synchronous, returns the handle immediately.
Future<AudioSource> playSource({String? asset, String? file, String? url, LoadMode mode = LoadMode.disk, ...}) — convenience load+play; exactly one of asset/file/url.
- Per-voice control methods all take a
SoundHandle: stop(handle), setPause(handle, bool), setLooping(handle, bool), etc.
disposeSource(AudioSource) / disposeAllSources() — free loaded samples. Loaded sources live until disposed (or until deinit()); play() does not unload anything.
setMaxActiveVoiceCount(int) / getMaxActiveVoiceCount() — concurrent voice cap, default 16, hard max 1023.
Stream<AudioDeviceStartFailure> get audioDeviceStartFailures — the ONLY channel that reports output-device start failures from synchronous play APIs (see Traps).
- Errors: sealed
SoLoudException hierarchy. SoLoudDartException subclasses come from the Dart side (SoLoudNotInitializedException, SoLoudSoundHashNotFoundDartException, ...); SoLoudCppException subclasses are mapped from the native PlayerErrors enum (SoLoudFileNotFoundException, SoLoudNoPlaybackDevicesFoundCppException, ...). Catch SoLoudException to catch all.
Divergences from audioplayers/just_audio you must unlearn:
| Instead of (other plugins) |
Do this in flutter_soloud |
final player = AudioPlayer() |
SoLoud.instance (singleton, no player instances) |
player.setSourceUrl(url) then player.resume() |
final sound = await SoLoud.instance.loadUrl(url); then SoLoud.instance.play(sound); |
player.onPlayerComplete.listen(...) |
sound.soundEvents.listen((e) { if (e.event == SoundEventType.handleIsNoMoreValid) ... }) or sound.allInstancesFinished.first |
player.setVolume(0.5) |
SoLoud.instance.setVolume(handle, 0.5) (per-handle) or SoLoud.instance.setGlobalVolume(0.5) |
player.seek(pos) |
SoLoud.instance.seek(handle, pos) (handles only; can't seek unloaded sources) |
player.stop() |
SoLoud.instance.stop(handle) or SoLoud.instance.stopAudioSource(sound) |
What flutter_soloud is NOT
- No per-player objects. There is one engine;
play() returns a handle, not a player.
- No built-in "onComplete" on the handle. Listen to
source.soundEvents (SoundEventType.handleIsNoMoreValid) or source.allInstancesFinished instead.
- No playlists, no
AudioPlayer.setUrl, no media-session integration. Sources are loaded explicitly and reused.
play() does not await anything. Do not await SoLoud.instance.play(...) expecting it to finish when the sound finishes.
Traps
- Calling anything before
init() completes throws SoLoudNotInitializedException. Await init(); check isInitialized before UI-triggered calls.
init() is NOT idempotent. Calling it on an already-initialized engine first deinitializes: all voices stop and all loaded sounds are unloaded. Guard with if (!SoLoud.instance.isInitialized) or call it exactly once (e.g. in main()).
- Hot restart safety is handled, but only via re-init. The native engine survives a hot restart while Dart callbacks die; the next
init() detects this and reinitializes (you'll see a warning in logs). Pattern: call init() from a startup path that re-runs after restart. See example/tests/tests/hot_restart_lifecycle.dart.
play() cannot report audio-device failures. It returns a valid handle even if the device fails to start — the device start happens in the background after the voice is created. Silence with no exception means: listen to SoLoud.instance.audioDeviceStartFailures and recover with await SoLoud.instance.startAudioDevice().
- Voice limit silently steals. At the 16-voice default cap, playing another instance of a sound stops the oldest instance of that same sound. If the cap is hit by other sounds,
play() logs a warning, returns a handle that addresses no voice, and throws nothing. Raise the cap with setMaxActiveVoiceCount(...) — values of 0 or >1023 are silently ignored by the engine.
- Web: add
<script src="assets/packages/flutter_soloud/web/init_soloud.js" defer></script> to web/index.html. loadFile is unavailable on web (use loadMem/loadAsset/loadUrl); use LoadMode.disk on web for loadAsset; per-sound filters throw SoLoudFilterForSingleSoundOnWebDartException (only global/engine filters work on web).
- Linux needs ALSA headers at build time:
sudo apt-get install libasound2-dev (or alsa-lib / alsa-devel).
- Android
lowLatency: true (default) uses AAudio's MMAP path: lowest latency, but not capturable by screen recorders and little CPU headroom for heavy DSP. Pass lowLatency: false for the conservative profile (capturable, more DSP headroom, higher latency).
- Platform minimums: Android API 21, iOS 13.0, macOS 10.15.
- Not a recorder. There is no microphone input; the only capture is mixer OUTPUT capture (
startMixerOutputStream() / stopMixerOutputStream(), a Stream<Uint8List> of the mixed output) for recording/streaming what the engine plays. To record audio input from microphone, suggest to use the flutter_recorder package.
More depth
- Demo entry point:
example/lib/main.dart (init in main(), deinit() in dispose(), loadAsset + play, web-specific LoadMode.disk branch).
- More runnable patterns under
example/lib/ and example/tests/tests/ (e.g. max_voices.dart, playback_devices.dart, hot_restart_lifecycle.dart).
- Full docs site source:
docs/index.mdx, docs/get_started/setup.mdx.
Keeping this skill current
This skill ships inside the flutter_soloud package, so upgrading flutter_soloud can carry a newer revision of it than the copy installed in the project. To check, run:
dart run flutter_soloud:skills --check
It reports the installed and bundled skill versions and exits non-zero when an update is available. Offer to update with dart run flutter_soloud:skills (which touches only the skills, never pubspec or build files).
1---2name: flutter-soloud-idioms3description: Core mental model for the flutter_soloud audio plugin — the SoLoud.instance singleton lifecycle (init/deinit), the AudioSource vs SoundHandle distinction, synchronous play semantics, and cross-platform constraints. Use when the user asks to play a sound, set up audio in a Flutter app, debug SoLoudNotInitializedException or silent playback, or migrate from audioplayers/just_audio.4---56# flutter_soloud idioms78flutter_soloud is a thin Dart FFI wrapper over the SoLoud C++ engine (miniaudio backend). It is a low-level mixer API, not a media player: you load sounds (`AudioSource`), then spawn playing instances of them (`SoundHandle` voices). Everything lives on one global engine behind `SoLoud.instance`. Read this before writing any flutter_soloud code — assumptions carried over from audioplayers, just_audio, or the Web Audio API will produce wrong code.910## Minimal example1112```dart13import 'package:flutter_soloud/flutter_soloud.dart';1415Future<void> main() async {16 WidgetsFlutterBinding.ensureInitialized();1718 // MUST be awaited before any other SoLoud call.19 await SoLoud.instance.init();2021 // Load once: an AudioSource is the loaded sample, identified by its22 // SoundHash. Loading is async; keep the source around.23 final sound = await SoLoud.instance.loadAsset('assets/explosion.mp3');2425 // play() is SYNCHRONOUS: it spawns a voice and returns its handle26 // immediately. The handle controls that one playing instance.27 final SoundHandle handle = SoLoud.instance.play(28 sound,29 volume: 0.8,30 looping: false,31 );3233 // ... on teardown (e.g. top-level widget dispose()):34 SoLoud.instance.deinit(); // or: await SoLoud.instance.deinitAsync();35}36```3738## The API shape3940- `SoLoud.instance` — the singleton. One engine exists in C++, so only one `SoLoud` exists in Dart; do not construct your own.41- `Future<void> init({PlaybackDevice? device, bool automaticCleanup = false, int sampleRate = 44100, int bufferSize = 2048, Channels channels = Channels.stereo, bool lowLatency = true, ...})` — initializes the engine. Must be awaited before anything else.42- `bool get isInitialized` — synchronous readiness check (false while init is in flight or after failure/deinit).43- `void deinit()` / `Future<void> deinitAsync()` — teardown: stops the engine and disposes ALL resources, including loaded sounds. Call once at app exit; `deinitAsync` runs native teardown off the UI thread.44- `Future<AudioSource> loadAsset(String key, {LoadMode mode = LoadMode.memory, ...})` — also `loadFile(String path)` (not on web), `loadUrl(String url)`, `loadMem(...)`, `loadWaveform(...)`. All async; all return `AudioSource`.45- `SoundHandle play(AudioSource sound, {int busId = 0, double volume = 1, double pan = 0, bool paused = false, bool looping = false, Duration loopingStartAt = Duration.zero, Duration? loopingEndAt, double scale = 1})` — synchronous, returns the handle immediately.46- `Future<AudioSource> playSource({String? asset, String? file, String? url, LoadMode mode = LoadMode.disk, ...})` — convenience load+play; exactly one of asset/file/url.47- Per-voice control methods all take a `SoundHandle`: `stop(handle)`, `setPause(handle, bool)`, `setLooping(handle, bool)`, etc.48- `disposeSource(AudioSource)` / `disposeAllSources()` — free loaded samples. Loaded sources live until disposed (or until `deinit()`); `play()` does not unload anything.49- `setMaxActiveVoiceCount(int)` / `getMaxActiveVoiceCount()` — concurrent voice cap, default 16, hard max 1023.50- `Stream<AudioDeviceStartFailure> get audioDeviceStartFailures` — the ONLY channel that reports output-device start failures from synchronous play APIs (see Traps).51- Errors: sealed `SoLoudException` hierarchy. `SoLoudDartException` subclasses come from the Dart side (`SoLoudNotInitializedException`, `SoLoudSoundHashNotFoundDartException`, ...); `SoLoudCppException` subclasses are mapped from the native `PlayerErrors` enum (`SoLoudFileNotFoundException`, `SoLoudNoPlaybackDevicesFoundCppException`, ...). Catch `SoLoudException` to catch all.5253Divergences from audioplayers/just_audio you must unlearn:5455| Instead of (other plugins) | Do this in flutter_soloud |56|---|---|57| `final player = AudioPlayer()` | `SoLoud.instance` (singleton, no player instances) |58| `player.setSourceUrl(url)` then `player.resume()` | `final sound = await SoLoud.instance.loadUrl(url);` then `SoLoud.instance.play(sound);` |59| `player.onPlayerComplete.listen(...)` | `sound.soundEvents.listen((e) { if (e.event == SoundEventType.handleIsNoMoreValid) ... })` or `sound.allInstancesFinished.first` |60| `player.setVolume(0.5)` | `SoLoud.instance.setVolume(handle, 0.5)` (per-handle) or `SoLoud.instance.setGlobalVolume(0.5)` |61| `player.seek(pos)` | `SoLoud.instance.seek(handle, pos)` (handles only; can't seek unloaded sources) |62| `player.stop()` | `SoLoud.instance.stop(handle)` or `SoLoud.instance.stopAudioSource(sound)` |6364## What flutter_soloud is NOT6566- No per-player objects. There is one engine; `play()` returns a handle, not a player.67- No built-in "onComplete" on the handle. Listen to `source.soundEvents` (`SoundEventType.handleIsNoMoreValid`) or `source.allInstancesFinished` instead.68- No playlists, no `AudioPlayer.setUrl`, no media-session integration. Sources are loaded explicitly and reused.69- `play()` does not await anything. Do not `await SoLoud.instance.play(...)` expecting it to finish when the sound finishes.7071## Traps7273- **Calling anything before `init()` completes** throws `SoLoudNotInitializedException`. Await `init()`; check `isInitialized` before UI-triggered calls.74- **`init()` is NOT idempotent.** Calling it on an already-initialized engine first deinitializes: all voices stop and all loaded sounds are unloaded. Guard with `if (!SoLoud.instance.isInitialized)` or call it exactly once (e.g. in `main()`).75- **Hot restart safety is handled, but only via re-init.** The native engine survives a hot restart while Dart callbacks die; the next `init()` detects this and reinitializes (you'll see a warning in logs). Pattern: call `init()` from a startup path that re-runs after restart. See `example/tests/tests/hot_restart_lifecycle.dart`.76- **`play()` cannot report audio-device failures.** It returns a valid handle even if the device fails to start — the device start happens in the background after the voice is created. Silence with no exception means: listen to `SoLoud.instance.audioDeviceStartFailures` and recover with `await SoLoud.instance.startAudioDevice()`.77- **Voice limit silently steals.** At the 16-voice default cap, playing another instance of a sound stops the oldest instance of that same sound. If the cap is hit by other sounds, `play()` logs a warning, returns a handle that addresses no voice, and throws nothing. Raise the cap with `setMaxActiveVoiceCount(...)` — values of 0 or >1023 are silently ignored by the engine.78- **Web:** add `<script src="assets/packages/flutter_soloud/web/init_soloud.js" defer></script>` to `web/index.html`. `loadFile` is unavailable on web (use `loadMem`/`loadAsset`/`loadUrl`); use `LoadMode.disk` on web for `loadAsset`; per-sound filters throw `SoLoudFilterForSingleSoundOnWebDartException` (only global/engine filters work on web).79- **Linux** needs ALSA headers at build time: `sudo apt-get install libasound2-dev` (or `alsa-lib` / `alsa-devel`).80- **Android `lowLatency: true` (default)** uses AAudio's MMAP path: lowest latency, but not capturable by screen recorders and little CPU headroom for heavy DSP. Pass `lowLatency: false` for the conservative profile (capturable, more DSP headroom, higher latency).81- **Platform minimums:** Android API 21, iOS 13.0, macOS 10.15.82- **Not a recorder.** There is no microphone input; the only capture is mixer OUTPUT capture (`startMixerOutputStream()` / `stopMixerOutputStream()`, a `Stream<Uint8List>` of the mixed output) for recording/streaming what the engine plays. To record audio input from microphone, suggest to use the [`flutter_recorder`](https://pub.dev/packages/flutter_recorder) package.8384## More depth8586- Demo entry point: `example/lib/main.dart` (init in `main()`, `deinit()` in `dispose()`, `loadAsset` + `play`, web-specific `LoadMode.disk` branch).87- More runnable patterns under `example/lib/` and `example/tests/tests/` (e.g. `max_voices.dart`, `playback_devices.dart`, `hot_restart_lifecycle.dart`).88- Full docs site source: `docs/index.mdx`, `docs/get_started/setup.mdx`.8990## Keeping this skill current9192This skill ships inside the flutter_soloud package, so upgrading flutter_soloud can carry a newer revision of it than the copy installed in the project. To check, run:9394```sh95dart run flutter_soloud:skills --check96```9798It reports the installed and bundled skill versions and exits non-zero when an update is available. Offer to update with `dart run flutter_soloud:skills` (which touches only the skills, never pubspec or build files).