Mixing buses (sub-mixes) in flutter_soloud
A Bus is a virtual mixer channel: sounds are routed through it, so one
volume change or one filter affects everything on the bus. The canonical use
is separate Music / SFX / UI buses behind user volume sliders. Unlike
audioplayers/just_audio (one player per sound, no sub-mix concept), routing is
explicit: you create a bus, play the bus itself on the engine, then play
sounds onto it.
Minimal example
import 'package:flutter_soloud/flutter_soloud.dart';
Future<void> main() async {
await SoLoud.instance.init();
// 1. Create the bus (synchronous, cheap).
final sfxBus = SoLoud.instance.createMixingBus(name: 'SFX');
// 2. REQUIRED: play the bus on the engine. Until this call, the bus
// (and everything routed through it) is silent.
sfxBus.playOnEngine();
// 3. Route sounds through the bus.
final explosion = await SoLoud.instance.loadAsset('assets/explosion.mp3');
sfxBus.play(explosion); // synchronous, returns SoundHandle
// 4. Collective volume: set it on the bus's own handle, not per-sound.
SoLoud.instance.setVolume(sfxBus.soundHandle!, 0.5);
// 5. Shared filter on everything routed through the bus.
sfxBus.filters.echoFilter.activate();
// Cleanup: stops all sounds playing through the bus.
sfxBus.dispose();
}
The API shape
SoLoud.instance.createMixingBus({String name = ''}) → Bus. Synchronous;
registers the bus in the Buses() registry. First bus gets busId == 1;
busId == 0 always means "the main engine output".
Buses() singleton registry: Buses().buses (list of all live buses),
Buses().byName('SFX'), Buses().byId(1). Both lookups throw if not found
unless you pass orElse.
bus.playOnEngine({double volume = 1.0, bool paused = false}) →
SoundHandle. Makes the bus audible; stores the handle in
bus.soundHandle. Calling it again on the same bus stops the earlier
instance (only one bus voice at a time).
- Playing onto a bus — two equivalent ways:
bus.play(sound, {volume, pan, paused, looping, ...}) and its siblings
bus.playClocked, bus.playScheduled, bus.play3d, bus.play3dClocked,
bus.play3dScheduled — thin wrappers that forward to the SoLoud method
of the same name with busId set.
SoLoud.instance.play(sound, busId: sfxBus.busId) — every SoLoud
play method takes an int busId = 0 parameter.
bus.annexSound(handle) — re-parent a live voice (by its SoundHandle)
into this bus, e.g. move a sound already playing on the engine onto a
filtered bus mid-playback.
- Bus volume: there is no
bus.volume property. Use
SoLoud.instance.setVolume(bus.soundHandle!, v) /
SoLoud.instance.getVolume(bus.soundHandle!). This scales every voice
routed through the bus on top of each voice's own volume.
bus.filters — a FiltersSingle scoped to this bus; filters activated
here apply to all routed sounds only (engine-wide filters live on
SoLoud.instance.filters). See skill flutter_soloud-filters.
bus.setChannels({Channels channels = Channels.stereo}) — bus output
channel count (Channels.mono/stereo/quad/surround51/dolby71).
bus.getChannelVolume(channel) — approximate per-channel output level for
VU meters. Requires SoLoud.instance.setVisualizationEnabled(true) first;
returns 0 for out-of-range channels.
bus.getActiveVoiceCount() — voices currently playing through the bus;
returns 0 if the bus isn't playing on the engine.
bus.isActive — true once playOnEngine() succeeded.
bus.dispose() — destroys the bus and stops all sounds routed through
it; removes it from Buses().
Everything above (except createMixingBus lookups and dispose) is
synchronous and returns SoundHandle directly — do not await them.
Traps
- Silent bus: forgetting
playOnEngine() is the #1 failure. Sounds
"play" (handles are created, voices count up) but nothing is heard.
bus.soundHandle stays null until then, so guard with
if (bus.soundHandle != null) before volume/filter-parameter calls.
- Do not invent
bus.volume / bus.setVolume(): they don't exist.
Volume goes through SoLoud.instance.setVolume(bus.soundHandle!, v).
Models coming from audioplayers habitually look for a per-player volume
setter — route them to the bus handle instead.
- No
await on bus play methods: bus.play(...) returns SoundHandle,
not a Future. SoLoud.instance.loadAsset is the async part; playback is
synchronous.
- Disposed bus throws: every
Bus method throws
SoLoudBusDisposedDartException after dispose(). Double-dispose also
throws — null out your reference after disposing.
- Create buses after
await SoLoud.instance.init(): the constructor calls
into the native engine, and playOnEngine() throws
SoLoudBackendNotInitedException when the engine isn't up.
byName/byId throw when missing: they use firstWhere; pass
orElse or check Buses().buses first if the bus may not exist.
- Voice limit: bus voices count against the engine's active-voice cap
(default 16, raise with
SoLoud.instance.setMaxActiveVoiceCount). Hitting
it is not an error — the new sound just doesn't play.
annexSound needs a live handle: annexing a finished/invalid handle is
a no-op on the native side; check sound.handles / isActive first.
- Filters need the bus handle for per-instance parameters: e.g.
bus.filters.pitchShiftFilter.shift(soundHandle: bus.soundHandle).value = 2.5;
after activate(). Plain .shift.value targets the global filter, not the
bus voice.
More depth
references/api.md — full signatures of all six bus play variants, the
Buses registry, exceptions, and the AudioManager (music/SFX/voice +
ducking) pattern.
- Runnable demo:
example/lib/mixing_bus/mixing_bus.dart in this repo —
creates buses, plays/annexes sounds, per-bus volume slider, pitch-shift
filter toggle, live voice counts.
- Upstream concepts: https://solhsa.com/soloud/mixbus.html
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-mixing-bus3description: Teaches how to use flutter_soloud mixing buses (Bus/Buses) to route groups of sounds through a shared sub-mix for collective volume control and shared filters. Use when the user asks for separate music/SFX/UI volume sliders, grouped effects, ducking music under voice, routing sounds between categories at runtime, or a per-category audio mixer in a game or app using flutter_soloud.4---56# Mixing buses (sub-mixes) in flutter_soloud78A `Bus` is a virtual mixer channel: sounds are routed *through* it, so one9volume change or one filter affects everything on the bus. The canonical use10is separate `Music` / `SFX` / `UI` buses behind user volume sliders. Unlike11audioplayers/just_audio (one player per sound, no sub-mix concept), routing is12explicit: you create a bus, play **the bus itself** on the engine, then play13sounds onto it.1415## Minimal example1617```dart18import 'package:flutter_soloud/flutter_soloud.dart';1920Future<void> main() async {21 await SoLoud.instance.init();2223 // 1. Create the bus (synchronous, cheap).24 final sfxBus = SoLoud.instance.createMixingBus(name: 'SFX');2526 // 2. REQUIRED: play the bus on the engine. Until this call, the bus27 // (and everything routed through it) is silent.28 sfxBus.playOnEngine();2930 // 3. Route sounds through the bus.31 final explosion = await SoLoud.instance.loadAsset('assets/explosion.mp3');32 sfxBus.play(explosion); // synchronous, returns SoundHandle3334 // 4. Collective volume: set it on the bus's own handle, not per-sound.35 SoLoud.instance.setVolume(sfxBus.soundHandle!, 0.5);3637 // 5. Shared filter on everything routed through the bus.38 sfxBus.filters.echoFilter.activate();3940 // Cleanup: stops all sounds playing through the bus.41 sfxBus.dispose();42}43```4445## The API shape4647- `SoLoud.instance.createMixingBus({String name = ''})` → `Bus`. Synchronous;48 registers the bus in the `Buses()` registry. First bus gets `busId == 1`;49 `busId == 0` always means "the main engine output".50- `Buses()` singleton registry: `Buses().buses` (list of all live buses),51 `Buses().byName('SFX')`, `Buses().byId(1)`. Both lookups throw if not found52 unless you pass `orElse`.53- `bus.playOnEngine({double volume = 1.0, bool paused = false})` →54 `SoundHandle`. Makes the bus audible; stores the handle in55 `bus.soundHandle`. Calling it again on the same bus stops the earlier56 instance (only one bus voice at a time).57- Playing onto a bus — two equivalent ways:58 - `bus.play(sound, {volume, pan, paused, looping, ...})` and its siblings59 `bus.playClocked`, `bus.playScheduled`, `bus.play3d`, `bus.play3dClocked`,60 `bus.play3dScheduled` — thin wrappers that forward to the `SoLoud` method61 of the same name with `busId` set.62 - `SoLoud.instance.play(sound, busId: sfxBus.busId)` — every `SoLoud`63 play method takes an `int busId = 0` parameter.64- `bus.annexSound(handle)` — re-parent a *live* voice (by its `SoundHandle`)65 into this bus, e.g. move a sound already playing on the engine onto a66 filtered bus mid-playback.67- Bus volume: there is **no** `bus.volume` property. Use68 `SoLoud.instance.setVolume(bus.soundHandle!, v)` /69 `SoLoud.instance.getVolume(bus.soundHandle!)`. This scales every voice70 routed through the bus on top of each voice's own volume.71- `bus.filters` — a `FiltersSingle` scoped to this bus; filters activated72 here apply to all routed sounds only (engine-wide filters live on73 `SoLoud.instance.filters`). See skill `flutter_soloud-filters`.74- `bus.setChannels({Channels channels = Channels.stereo})` — bus output75 channel count (`Channels.mono/stereo/quad/surround51/dolby71`).76- `bus.getChannelVolume(channel)` — approximate per-channel output level for77 VU meters. Requires `SoLoud.instance.setVisualizationEnabled(true)` first;78 returns 0 for out-of-range channels.79- `bus.getActiveVoiceCount()` — voices currently playing through the bus;80 returns 0 if the bus isn't playing on the engine.81- `bus.isActive` — true once `playOnEngine()` succeeded.82- `bus.dispose()` — destroys the bus and **stops all sounds routed through83 it**; removes it from `Buses()`.8485Everything above (except `createMixingBus` lookups and `dispose`) is86synchronous and returns `SoundHandle` directly — do not `await` them.8788## Traps8990- **Silent bus**: forgetting `playOnEngine()` is the #1 failure. Sounds91 "play" (handles are created, voices count up) but nothing is heard.92 `bus.soundHandle` stays `null` until then, so guard with93 `if (bus.soundHandle != null)` before volume/filter-parameter calls.94- **Do not invent `bus.volume` / `bus.setVolume()`**: they don't exist.95 Volume goes through `SoLoud.instance.setVolume(bus.soundHandle!, v)`.96 Models coming from audioplayers habitually look for a per-player volume97 setter — route them to the bus handle instead.98- **No `await` on bus play methods**: `bus.play(...)` returns `SoundHandle`,99 not a `Future`. `SoLoud.instance.loadAsset` is the async part; playback is100 synchronous.101- **Disposed bus throws**: every `Bus` method throws102 `SoLoudBusDisposedDartException` after `dispose()`. Double-dispose also103 throws — null out your reference after disposing.104- **Create buses after `await SoLoud.instance.init()`**: the constructor calls105 into the native engine, and `playOnEngine()` throws106 `SoLoudBackendNotInitedException` when the engine isn't up.107- **`byName`/`byId` throw when missing**: they use `firstWhere`; pass108 `orElse` or check `Buses().buses` first if the bus may not exist.109- **Voice limit**: bus voices count against the engine's active-voice cap110 (default 16, raise with `SoLoud.instance.setMaxActiveVoiceCount`). Hitting111 it is not an error — the new sound just doesn't play.112- **`annexSound` needs a live handle**: annexing a finished/invalid handle is113 a no-op on the native side; check `sound.handles` / `isActive` first.114- **Filters need the bus handle for per-instance parameters**: e.g.115 `bus.filters.pitchShiftFilter.shift(soundHandle: bus.soundHandle).value = 2.5;`116 after `activate()`. Plain `.shift.value` targets the global filter, not the117 bus voice.118119## More depth120121- `references/api.md` — full signatures of all six bus play variants, the122 `Buses` registry, exceptions, and the AudioManager (music/SFX/voice +123 ducking) pattern.124- Runnable demo: `example/lib/mixing_bus/mixing_bus.dart` in this repo —125 creates buses, plays/annexes sounds, per-bus volume slider, pitch-shift126 filter toggle, live voice counts.127- Upstream concepts: https://solhsa.com/soloud/mixbus.html128129## Keeping this skill current130131This 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:132133```sh134dart run flutter_soloud:skills --check135```136137It 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).