--- name: flutter-soloud-idioms version: 2 description: 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. --- # 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 ```dart import 'package:flutter_soloud/flutter_soloud.dart'; Future 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 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 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 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 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 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 `` 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:** uses `miniaudio` with runtime dynamic loading for ALSA, PulseAudio, and JACK (default `LinuxAudioBackend.auto` prioritizes ALSA -> PulseAudio -> JACK). Configure via `linuxAudioBackend` in `init()` or switch dynamically with `SoLoud.instance.setLinuxAudioBackend(backend)`. No compile-time ALSA headers required. - **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` 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. ## 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: ```sh 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).