flutter_soloud setup
flutter_soloud is an FFI plugin around the SoLoud C++ engine: the native code is compiled automatically by Dart build hooks when you depend on the package, so setup is mostly pubspec + a few platform bits (one <script> tag on web, ALSA dev package on Linux, minimum SDK versions). Unlike audioplayers/just_audio there is no per-player instance — everything goes through the singleton SoLoud.instance, which must be init()ed before use and deinit()ed on shutdown.
Minimal example
import 'package:flutter_soloud/flutter_soloud.dart';
Future<void> main() async {
// Optional: pick the output device before init.
final devices = SoLoud.instance.listPlaybackDevices(); // works pre-init
await SoLoud.instance.init(
// device: devices.firstWhere((d) => d.isDefault),
sampleRate: 44100,
bufferSize: 2048,
channels: Channels.stereo,
lowLatency: true,
automaticCleanup: false,
);
final sound = await SoLoud.instance.loadAsset('assets/audio/click.mp3');
SoLoud.instance.play(sound);
// Later, on shutdown:
SoLoud.instance.deinit();
}
Adding the package
flutter pub add flutter_soloud
Native C/C++ sources are compiled by Dart build hooks (hooks/code_assets/native_toolchain_c are transitive deps of the package). No CMake, CocoaPods script phases, or compiler flags are needed.
Platform setup
The API shape
All of these live on the singleton SoLoud.instance (import 'package:flutter_soloud/flutter_soloud.dart').
Future<void> init({PlaybackDevice? device, bool automaticCleanup = false, int sampleRate = 44100, int bufferSize = 2048, Channels channels = Channels.stereo, bool lowLatency = true, AndroidAAudioAttributes androidAAudioAttributes = AndroidAAudioAttributes.mediaMusic, int? devicePeriodFrames, int? renderAheadFrames}) — initializes the engine. Throws on failure (e.g. SoLoudCppException, SoLoudNoPlaybackDevicesFoundCppException); it does not return a PlayerErrors status, so await it in try/catch.
void deinit() / Future<void> deinitAsync() — stops the engine and disposes all resources including sounds. deinit blocks the calling thread; prefer deinitAsync where you can await it.
bool get isInitialized — synchronous readiness check.
List<PlaybackDevice> listPlaybackDevices() — safe to call before init(). Returns PlaybackDevice(id, isDefault, name).
Future<void> changeDevice({PlaybackDevice? newDevice}) — switches output while running; omit newDevice to select the system default. Await it — the swap runs off the UI isolate.
Future<void> stopAudioDevice({bool force = false}) / Future<void> startAudioDevice() — stop/start only the output device; loaded sounds, voices, and filter state are preserved and playback resumes where it left off.
AudioDeviceState getAudioDeviceState() — cheap sync read: uninitialized | stopped | started | starting | stopping. Safe before init().
void setAudioDeviceIdleTimeout(Duration? timeout) — when no unpaused voices remain: Duration.zero stops the device ASAP, a positive duration keeps it alive that long (default 500 ms), null keeps it running indefinitely (Android wakelock). No effect on web.
Divergences from what models trained on audioplayers/just_audio assume:
- One global engine, no
AudioPlayer() instances. Call SoLoud.instance.init() once, early; every other call throws SoLoudNotInitializedException before that.
play() is synchronous and returns a SoundHandle immediately — it cannot report device-start failures; subscribe to SoLoud.instance.audioDeviceStartFailures for those.
init() while already initialized deinitializes and reinitializes, stopping all sounds and unloading all files.
init() options that are native-only (silently ignored on web): lowLatency, androidAAudioAttributes (Android-only, and only when lowLatency: false), devicePeriodFrames, renderAheadFrames.
automaticCleanup: true makes the engine purge its temp directory of loaded sound files occasionally — relevant for apps that load many files from the network.
Render-Ahead Ring (ultra-low latency with large mix buffers)
On native platforms (Android, iOS, macOS, Windows, Linux), you can decouple the hardware output period from the engine mix buffer by setting renderAheadFrames > 0:
await SoLoud.instance.init(
bufferSize: 2048, // Large DSP/mixing quantum for CPU headroom
devicePeriodFrames: 512, // Hardware device callback period (~11 ms @ 44.1kHz)
renderAheadFrames: 1536, // Mix-ahead depth (e.g. bufferSize - devicePeriodFrames)
);
- How it works: The engine pre-mixes audio
renderAheadFrames ahead into an internal ring buffer. When reactive calls like play() or playScheduled() occur, audio is mixed retroactively into the not-yet-played section of the ring, giving near-instantaneous keypress-to-sound latency (~11 ms) without risking audio underruns from tiny mix buffers.
- Inspection getters:
bool get isRenderAheadEnabled — true when enabled on native platforms.
Duration getPlayheadTime() — true playhead time reaching the speaker right now (equals getEngineTime() when disabled or on web).
Duration getOutputLatency() — estimated output latency (ring depth + device period; Duration.zero when disabled or on web).
- Caveats: Ignored on web. Ended-voice callbacks may fire up to
renderAheadFrames earlier than without the ring. Unseekable/un-snapshotable streams (released push streams, pull streams, speechText) degrade gracefully to standard buffer boundaries.
Traps
- Don't await a return code from
init(). It returns Future<void> and throws; older docs suggest a PlayerErrors return. Trust the code.
- Calling
init() again (e.g. after hot restart) wipes all loaded sounds — guard with isInitialized if you only want to init once.
- Web without the
<script> tag fails at init() with confusing WASM-module errors. The tag must be present, and the WASM assets only load from assets/packages/flutter_soloud/web/….
- Web: don't pass
--web-header COOP/COEP flags together with flutter run --wasm — the dev server already sends COOP/COEP for WasmGC and the conflicting duplicated headers block the plugin's worker threads (ERR_BLOCKED_BY_RESPONSE). See references/web.md.
changeDevice is desktop-mostly: Android, iOS, and Web support only the default output device; listPlaybackDevices() there returns just the default.
- A device stopped via
stopAudioDevice() or the idle timeout stays stopped across changeDevice() — the replacement device only starts if the old one was running.
- Linux build fails with
alsa/asoundlib.h: No such file — install libasound2-dev; the error is from the native build hook, not Dart.
- On web,
loadUrl() hits CORS (Access-Control-Allow-Origin missing) unless the server allows it, and local files can't be read — use loadMem() instead.
- Per-sound filters are not supported on web (global filters are).
Shrinking binaries: excluding Xiph libs
The Opus/Ogg/Vorbis/FLAC decoders are bundled by default (600–3000 KB per binary). If you only need MP3/WAV/synthesis, exclude them in the app's pubspec.yaml:
hooks:
user_defines:
flutter_soloud:
no_xiph_libs: true
setBufferStream()/readSamplesFrom*() with Opus/Vorbis/FLAC then throw; everything else works. On web, edit web/compile_wasm.sh in the package (NO_XIPH_LIBS="1") and rebuild the WASM yourself (requires Emscripten, Linux/macOS). Old shell-env/Podfile/Gradle config no longer applies under build hooks.
Logging
The plugin logs via package:logging (logger name flutter_soloud.SoLoud). Nothing is printed until you attach a listener:
import 'package:logging/logging.dart';
import 'dart:developer' as dev;
Logger.root.level = Level.FINE;
Logger.root.onRecord.listen((r) => dev.log(r.message, name: r.loggerName));
More depth
- references/web.md — the two WASM builds, COOP/COEP headers, dev-server pitfalls, CORS.
- references/audio_context.md — background playback with
audio_service + audio_session (media notification, lock-screen controls, ducking), with required iOS/Android manifest changes.
- Demos:
example/lib/output_device/output_device.dart (device enumeration and switching), example/lib/audio_context/audio_context.dart (full audio_service integration), example/lib/main.dart (minimal init/play).
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-setup-23description: Teaches how to add flutter_soloud to a Flutter app, configure each platform (web script tag and COOP/COEP headers, Linux ALSA, Android/iOS/macOS minimum versions), initialize and deinitialize the engine, shrink binaries by excluding the Xiph libs, set up logging, and enumerate/switch output devices. Use when a user asks to install flutter_soloud, initialize SoLoud, set up web/background-audio prerequisites, reduce binary size, or switch the audio output device.4---56# flutter_soloud setup78flutter_soloud is an FFI plugin around the SoLoud C++ engine: the native code is compiled automatically by Dart build hooks when you depend on the package, so setup is mostly pubspec + a few platform bits (one `<script>` tag on web, ALSA dev package on Linux, minimum SDK versions). Unlike audioplayers/just_audio there is no per-player instance — everything goes through the singleton `SoLoud.instance`, which must be `init()`ed before use and `deinit()`ed on shutdown.910## Minimal example1112```dart13import 'package:flutter_soloud/flutter_soloud.dart';1415Future<void> main() async {16 // Optional: pick the output device before init.17 final devices = SoLoud.instance.listPlaybackDevices(); // works pre-init1819 await SoLoud.instance.init(20 // device: devices.firstWhere((d) => d.isDefault),21 sampleRate: 44100,22 bufferSize: 2048,23 channels: Channels.stereo,24 lowLatency: true,25 automaticCleanup: false,26 );2728 final sound = await SoLoud.instance.loadAsset('assets/audio/click.mp3');29 SoLoud.instance.play(sound);3031 // Later, on shutdown:32 SoLoud.instance.deinit();33}34```3536## Adding the package3738```sh39flutter pub add flutter_soloud40```4142Native C/C++ sources are compiled by [Dart build hooks](https://dart.dev/tools/hooks) (`hooks`/`code_assets`/`native_toolchain_c` are transitive deps of the package). No CMake, CocoaPods script phases, or compiler flags are needed.4344## Platform setup4546- **Web** — add to the `<body>` of `web/index.html`:47 ```html48 <script src="assets/packages/flutter_soloud/web/init_soloud.js" defer></script>49 ```50 The script auto-picks between the multi-threaded (AudioWorklet) and single-threaded (ScriptProcessorNode) WASM builds based on whether the page is cross-origin isolated. Details in [references/web.md](references/web.md).51- **Linux** — requires ALSA headers: `sudo apt-get install libasound2-dev` (Debian/Ubuntu), `pacman -S alsa-lib` (Arch), `zypper install alsa-devel` (openSUSE).52- **Android** — `minSdk = 21` (the plugin sets this in its own `build.gradle`; your app-level `minSdkVersion` must be >= 21).53- **iOS** — deployment target iOS 13.0+; **macOS** — 10.15+. Native assets are compiled and bundled for both CocoaPods and SPM projects.5455## The API shape5657All of these live on the singleton `SoLoud.instance` (`import 'package:flutter_soloud/flutter_soloud.dart'`).5859- `Future<void> init({PlaybackDevice? device, bool automaticCleanup = false, int sampleRate = 44100, int bufferSize = 2048, Channels channels = Channels.stereo, bool lowLatency = true, AndroidAAudioAttributes androidAAudioAttributes = AndroidAAudioAttributes.mediaMusic, int? devicePeriodFrames, int? renderAheadFrames})` — initializes the engine. **Throws on failure** (e.g. `SoLoudCppException`, `SoLoudNoPlaybackDevicesFoundCppException`); it does not return a `PlayerErrors` status, so `await` it in try/catch.60- `void deinit()` / `Future<void> deinitAsync()` — stops the engine and disposes all resources including sounds. `deinit` blocks the calling thread; prefer `deinitAsync` where you can await it.61- `bool get isInitialized` — synchronous readiness check.62- `List<PlaybackDevice> listPlaybackDevices()` — **safe to call before `init()`**. Returns `PlaybackDevice(id, isDefault, name)`.63- `Future<void> changeDevice({PlaybackDevice? newDevice})` — switches output while running; omit `newDevice` to select the system default. Await it — the swap runs off the UI isolate.64- `Future<void> stopAudioDevice({bool force = false})` / `Future<void> startAudioDevice()` — stop/start only the output device; loaded sounds, voices, and filter state are preserved and playback resumes where it left off.65- `AudioDeviceState getAudioDeviceState()` — cheap sync read: `uninitialized | stopped | started | starting | stopping`. Safe before `init()`.66- `void setAudioDeviceIdleTimeout(Duration? timeout)` — when no unpaused voices remain: `Duration.zero` stops the device ASAP, a positive duration keeps it alive that long (default 500 ms), `null` keeps it running indefinitely (Android wakelock). No effect on web.6768Divergences from what models trained on audioplayers/just_audio assume:6970- One global engine, no `AudioPlayer()` instances. Call `SoLoud.instance.init()` once, early; every other call throws `SoLoudNotInitializedException` before that.71- `play()` is synchronous and returns a `SoundHandle` immediately — it cannot report device-start failures; subscribe to `SoLoud.instance.audioDeviceStartFailures` for those.72- `init()` while already initialized **deinitializes and reinitializes**, stopping all sounds and unloading all files.73- `init()` options that are **native-only** (silently ignored on web): `lowLatency`, `androidAAudioAttributes` (Android-only, and only when `lowLatency: false`), `devicePeriodFrames`, `renderAheadFrames`.74- `automaticCleanup: true` makes the engine purge its temp directory of loaded sound files occasionally — relevant for apps that load many files from the network.7576### Render-Ahead Ring (ultra-low latency with large mix buffers)7778On native platforms (Android, iOS, macOS, Windows, Linux), you can decouple the hardware output period from the engine mix buffer by setting `renderAheadFrames > 0`:7980```dart81await SoLoud.instance.init(82 bufferSize: 2048, // Large DSP/mixing quantum for CPU headroom83 devicePeriodFrames: 512, // Hardware device callback period (~11 ms @ 44.1kHz)84 renderAheadFrames: 1536, // Mix-ahead depth (e.g. bufferSize - devicePeriodFrames)85);86```8788- **How it works**: The engine pre-mixes audio `renderAheadFrames` ahead into an internal ring buffer. When reactive calls like `play()` or `playScheduled()` occur, audio is mixed **retroactively** into the not-yet-played section of the ring, giving near-instantaneous keypress-to-sound latency (~11 ms) without risking audio underruns from tiny mix buffers.89- **Inspection getters**:90 - `bool get isRenderAheadEnabled` — true when enabled on native platforms.91 - `Duration getPlayheadTime()` — true playhead time reaching the speaker right now (equals `getEngineTime()` when disabled or on web).92 - `Duration getOutputLatency()` — estimated output latency (ring depth + device period; `Duration.zero` when disabled or on web).93- **Caveats**: Ignored on web. Ended-voice callbacks may fire up to `renderAheadFrames` earlier than without the ring. Unseekable/un-snapshotable streams (released push streams, pull streams, `speechText`) degrade gracefully to standard buffer boundaries.9495## Traps9697- **Don't await a return code from `init()`.** It returns `Future<void>` and throws; older docs suggest a `PlayerErrors` return. Trust the code.98- Calling `init()` again (e.g. after hot restart) wipes all loaded sounds — guard with `isInitialized` if you only want to init once.99- **Web without the `<script>` tag fails at `init()`** with confusing WASM-module errors. The tag must be present, and the WASM assets only load from `assets/packages/flutter_soloud/web/…`.100- **Web: don't pass `--web-header` COOP/COEP flags together with `flutter run --wasm`** — the dev server already sends COOP/COEP for WasmGC and the conflicting duplicated headers block the plugin's worker threads (`ERR_BLOCKED_BY_RESPONSE`). See [references/web.md](references/web.md).101- **`changeDevice` is desktop-mostly**: Android, iOS, and Web support only the default output device; `listPlaybackDevices()` there returns just the default.102- A device stopped via `stopAudioDevice()` or the idle timeout **stays stopped across `changeDevice()`** — the replacement device only starts if the old one was running.103- **Linux build fails with `alsa/asoundlib.h: No such file`** — install `libasound2-dev`; the error is from the native build hook, not Dart.104- On web, `loadUrl()` hits CORS (`Access-Control-Allow-Origin` missing) unless the server allows it, and local files can't be read — use `loadMem()` instead.105- Per-sound filters are not supported on web (global filters are).106107## Shrinking binaries: excluding Xiph libs108109The Opus/Ogg/Vorbis/FLAC decoders are bundled by default (600–3000 KB per binary). If you only need MP3/WAV/synthesis, exclude them in the **app's** `pubspec.yaml`:110111```yaml112hooks:113 user_defines:114 flutter_soloud:115 no_xiph_libs: true116```117118`setBufferStream()`/`readSamplesFrom*()` with Opus/Vorbis/FLAC then throw; everything else works. On web, edit `web/compile_wasm.sh` in the package (`NO_XIPH_LIBS="1"`) and rebuild the WASM yourself (requires Emscripten, Linux/macOS). Old shell-env/Podfile/Gradle config no longer applies under build hooks.119120## Logging121122The plugin logs via `package:logging` (logger name `flutter_soloud.SoLoud`). Nothing is printed until you attach a listener:123124```dart125import 'package:logging/logging.dart';126import 'dart:developer' as dev;127128Logger.root.level = Level.FINE;129Logger.root.onRecord.listen((r) => dev.log(r.message, name: r.loggerName));130```131132## More depth133134- [references/web.md](references/web.md) — the two WASM builds, COOP/COEP headers, dev-server pitfalls, CORS.135- [references/audio_context.md](references/audio_context.md) — background playback with `audio_service` + `audio_session` (media notification, lock-screen controls, ducking), with required iOS/Android manifest changes.136- Demos: `example/lib/output_device/output_device.dart` (device enumeration and switching), `example/lib/audio_context/audio_context.dart` (full `audio_service` integration), `example/lib/main.dart` (minimal init/play).137138## Keeping this skill current139140This 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:141142```sh143dart run flutter_soloud:skills --check144```145146It 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).