Mixer output capture
flutter_soloud taps the master mixer output: everything the engine mixes (all voices, buses, and active global filters) is copied into a native circular buffer and delivered to Dart as a broadcast Stream<Uint8List> of audio chunks. This is a capture of the output the engine produces — it is not an input device. flutter_soloud has no microphone recording; for mic input use another package. Capture runs on all platforms; on web it requires the WebAssembly build (--wasm).
Minimal example
Record 5 seconds of the mix to a raw PCM file:
import 'dart:io';
import 'package:flutter_soloud/flutter_soloud.dart';
Future<void> recordMix(String outputPath) async {
await SoLoud.instance.init();
final sink = File(outputPath).openWrite();
final stream = SoLoud.instance.startMixerOutputStream(
format: MixerOutputFormat.pcmS16le,
);
final sub = stream.listen(sink.add);
final sound = await SoLoud.instance.loadAsset('assets/music.mp3');
SoLoud.instance.play(sound);
await Future<void>.delayed(const Duration(seconds: 5));
// Stop first: the tail of the buffer is flushed into the stream
// synchronously on stop, so listeners still attached get the last bytes.
SoLoud.instance.stopMixerOutputStream();
await sub.cancel();
await sink.close();
}
The API shape
All on SoLoud.instance (and mirrored on SoLoudIsolate.instance, see below):
Stream<Uint8List> startMixerOutputStream({MixerOutputFormat format = MixerOutputFormat.pcmF32le, int sampleRate = -1, int channels = -1, int bufferSizeBytes = 1024 * 1024, int notificationThresholdBytes = 4096, int chunkPCMFrames = -1})— starts capture and returns a broadcast stream.sampleRate/channelsof-1follow the engine config. ThrowsSoLoudNotInitializedExceptionif the engine is not initialized. Calling it while already capturing returns the existing stream.void stopMixerOutputStream()— stops capture, flushes remaining buffer bytes into the stream synchronously, then closes the stream.bool get isMixerOutputStreamRunning— whether capture is active.Uint8List getMixerOutputWavHeader()— the finalized 44-byte WAV header; only meaningful withMixerOutputFormat.wavafter stopping (returns an empty list otherwise).enum MixerOutputFormat—pcmF32le(default,pcmS8,pcmS16le,pcmS32le,opus,vorbis,flac,wav. Helpers:isPcm,bytesPerSample(0 for compressed),bytesPerFrame(channels).
How this differs from what models assume from other packages:
- There is no
record()/startRecording()and noAudioRecorder— the API is named after the mixer output. You are recording playback, not a device. - No
onAudio/dataAvailablecallback like the Web AudioScriptProcessorNode/AudioWorklet— the result is an ordinary DartStream; youlistento it. - Unlike just_audio/audioplayers there is no file-target convenience API ("record to path"). You always write the chunks yourself.
- The chunk size is driven by
notificationThresholdBytes(compressed formats, and PCM whenchunkPCMFramesis -1) or bychunkPCMFrames(PCM only, fixed-size chunks of exactlychunkPCMFrames * channels * bytesPerSamplebytes; must be -1 or >= 2048). There is no per-chunk timestamp or duration metadata — just bytes. - Compressed formats (
opus,vorbis,flac) require the plugin to be built with the Xiph libraries;wavand the PCM formats are always available.
Traps
- Not microphone input. If the user asks to record the mic, this API is the wrong tool regardless of how it is named. flutter_soloud has no input capture.
- WAV header is a placeholder until stop. With
MixerOutputFormat.wavthe stream starts with a 44-byte header whoseRIFF/datasize fields are zero. AfterstopMixerOutputStream(), callgetMixerOutputWavHeader()and overwrite the first 44 bytes of the file, or players report duration 0 / refuse to play:SoLoud.instance.stopMixerOutputStream(); await sub.cancel(); await sink.close(); final header = SoLoud.instance.getMixerOutputWavHeader(); if (header.length == 44) { final raf = File(path).openSync(mode: FileMode.writeOnlyAppend) ..setPositionSync(0) ..writeFromSync(header); await raf.close(); } - Cancel the subscription after stop, not before.
stop()flushes the buffer tail into the stream with synchronous delivery; cancelling first loses the final chunk (which for compressed formats carries the encoder tail). chunkPCMFramesis PCM-only. Passing it with opus/vorbis/flac/wav trips an assert in debug builds. Compressed formats always usenotificationThresholdBytes.- Main-isolate stalls create capture gaps. The native side waits for Dart to advance the read position; a 500 ms synchronous block on the listening isolate produces a ~500 ms gap between chunks. Don't do heavy synchronous work (file sync writes on huge chunks, JSON parsing) in the listener; enlarge
bufferSizeBytesto absorb bursts, or capture from a worker isolate (below). - Init ordering.
startMixerOutputStreambeforeawait SoLoud.instance.init()throwsSoLoudNotInitializedException.deinit()auto-stops capture and closes the stream. - Filters are baked in. Active global filters (e.g.
pitchShiftFilter) apply to the captured output — the capture is post-mix, post-filter.
Capturing from a worker isolate
The engine is a C++ singleton that can only be initialized on the main isolate. For heavy consumers (encoding, network streaming), run the capture loop in a spawned isolate via SoLoudIsolate.instance, which exposes startMixerOutputStream, stopMixerOutputStream, isMixerOutputStreamRunning, getMixerOutputWavHeader, plus readSamplesFromFile/readSamplesFromMem. SoLoudIsolate has no init/deinit, no loading, no playback — those stay on the main isolate.
// Main isolate first:
await SoLoud.instance.init();
// ... then inside the spawned isolate (entry point needs
// @pragma('vm:entry-point')):
final stream = SoLoudIsolate.instance.startMixerOutputStream(
format: MixerOutputFormat.pcmS16le,
chunkPCMFrames: 2048,
);
final sub = stream.listen((chunk) { /* encode / send */ });
// on shutdown message:
await sub.cancel();
SoLoudIsolate.instance.stopMixerOutputStream();
SoLoudIsolate is marked @experimental; silence lints with // ignore_for_file: experimental_member_use where needed.
More depth
- Demo recording the mix to a file with format picker and WAV header patching:
example/lib/mixer_capture/mixer_capture.dartin the flutter_soloud repo. - Demo of capture in a spawned isolate (including surviving a blocked main isolate):
example/lib/mixer_capture/isolate_capture_test.dart. - Doc page:
docs/advanced/mixer_output_capture.mdx(mixer output capture guide).
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).