flutter_soloud can capture the master mixer output as a stream of raw audio data. This is useful for:
- Recording the final mixed audio to a file
- Streaming the mixed output to another service or device
- Processing the master output in real-time
The captured data is read from a circular buffer managed by the native engine. The stream emits chunks as they become available, and any remaining data is flushed when capture stops.
Mixer output capture is a new feature. It is supported on all platforms, but the web build requires the WebAssembly module (--wasm).
Call startMixerOutputStream() after initializing SoLoud:
await SoLoud.instance.init();
final stream = SoLoud.instance.startMixerOutputStream(
format: MixerOutputFormat.pcmF32le,
sampleRate: -1, // Use the engine sample rate
channels: -1, // Use the engine channel count
bufferSizeBytes: 1024 * 1024, // 1 MB circular buffer
notificationThresholdBytes: 4096, // Emit a chunk every 4 KB (compressed/PCM threshold mode)
);Subscribe to the stream and write the chunks to a file, socket, or any other consumer:
final subscription = stream.listen((Uint8List chunk) {
// Append the chunk to a file or send it over the network
fileSink.add(chunk);
});While the capture is running, all sounds played through the engine are mixed into the captured output:
final sound = await SoLoud.instance.loadAsset('assets/music.mp3');
await SoLoud.instance.play(sound);When you are done, call stopMixerOutputStream() to stop the capture and flush any remaining data:
SoLoud.instance.stopMixerOutputStream();
await subscription.cancel();
await fileSink.close();The MixerOutputFormat enum controls the format of the captured data:
| Format | Description |
|---|---|
MixerOutputFormat.pcmF32le | 32-bit floating point PCM, little-endian (default) |
MixerOutputFormat.pcmS8 | 8-bit signed PCM |
MixerOutputFormat.pcmS16le | 16-bit signed PCM, little-endian |
MixerOutputFormat.pcmS32le | 32-bit signed PCM, little-endian |
MixerOutputFormat.opus | Opus encoded audio |
MixerOutputFormat.vorbis | Vorbis encoded audio |
MixerOutputFormat.flac | FLAC encoded audio |
MixerOutputFormat.wav | WAV encoded audio (16-bit PCM in a RIFF/WAVE container). Streams incrementally, but the header size fields must be patched after capture stops. |
Compressed formats (opus, vorbis, flac) require the plugin to be built with the Xiph libraries. wav is self-contained and does not require Xiph. See the Without Xiph libs page for more details.
When using MixerOutputFormat.wav, the stream behaves like the other formats: you receive a 44-byte header at the start of the stream, followed by PCM chunks as they are produced. However, the WAV container stores the total PCM size in the RIFF and data chunk headers, so those fields are placeholders until the capture ends.
To produce a valid WAV file, you must:
- Capture the stream as usual.
- Stop the capture with
stopMixerOutputStream(). - Call
SoLoud.instance.getMixerOutputWavHeader()to get the updated 44-byte header. - Overwrite the first 44 bytes of the saved file with that header.
import 'dart:io';
import 'dart:typed_data';
import 'package:flutter_soloud/flutter_soloud.dart';
Future<void> recordWavMixerOutput(String outputPath) async {
await SoLoud.instance.init();
final file = File(outputPath);
final raf = file.openSync(mode: FileMode.writeOnly);
final stream = SoLoud.instance.startMixerOutputStream(
format: MixerOutputFormat.wav,
bufferSizeBytes: 1024 * 1024,
notificationThresholdBytes: 4096,
);
final subscription = stream.listen((Uint8List chunk) {
raf.writeFromSync(chunk);
});
// Play a sound
final sound = await SoLoud.instance.loadAsset('assets/music.mp3');
await SoLoud.instance.play(sound);
// Record for 5 seconds
await Future<void>.delayed(const Duration(seconds: 5));
SoLoud.instance.stopMixerOutputStream();
await subscription.cancel();
await raf.close();
// Patch the WAV header with the final sizes.
final header = SoLoud.instance.getMixerOutputWavHeader();
if (header.length == 44) {
final patchRaf = file.openSync(mode: FileMode.writeOnlyAppend)
..setPositionSync(0)
..writeFromSync(header);
await patchRaf.close();
}
}If you do not patch the header, the WAV file will still contain all the correct audio data, but media players may report a duration of zero or refuse to play it because the data chunk size will be 0.
| Parameter | Description |
|---|---|
format | The desired output format. PCM formats are always available; compressed formats require Xiph libraries. |
sampleRate | The sample rate of the captured audio. Use -1 to match the engine sample rate. |
channels | The number of channels. Use -1 to match the engine channel count. |
bufferSizeBytes | Total size of the circular capture buffer in bytes. A larger buffer allows more time between reads. |
notificationThresholdBytes | Number of bytes that must be available before a chunk is emitted. Used for compressed formats and for PCM when chunkPCMFrames is -1. Smaller values give lower latency but more callbacks. |
chunkPCMFrames | Fixed number of PCM frames per emitted chunk. PCM only; must be -1 or at least 2048. The emitted byte size is chunkPCMFrames * channels * bytesPerSample. |
You can check whether the mixer output capture is currently active:
if (SoLoud.instance.isMixerOutputStreamRunning) {
// Capture is active
}- The captured stream is a broadcast stream. Multiple listeners can subscribe to it, but each listener receives the same chunks independently.
- When the capture is stopped, any remaining data in the circular buffer is flushed synchronously. This ensures that compressed formats (such as Opus or FLAC) are properly finalized.
- For PCM formats, you can request fixed-size chunks by setting
chunkPCMFrames. This is useful for network streaming or any consumer that needs a stable frame size. Compressed formats always usenotificationThresholdBytesbecause the encoder determines the packet boundaries. - If the stream is already running, calling
startMixerOutputStream()again returns the existing stream. - The capture stream is automatically closed when
SoLoud.instance.deinit()is called.
For live streaming or network audio, it is often easier to consume a fixed number of frames per chunk instead of variable-sized threshold chunks. Use chunkPCMFrames to emit chunks of exactly the requested size:
final stream = SoLoud.instance.startMixerOutputStream(
format: MixerOutputFormat.pcmS16le,
sampleRate: 44100,
channels: 2,
chunkPCMFrames: 2048, // Each chunk is 2048 stereo frames
);Each emitted chunk is exactly chunkPCMFrames * channels * bytesPerSample bytes, except for the final tail that is flushed when stopMixerOutputStream() is called. chunkPCMFrames must be -1 or at least 2048. It is only valid for PCM formats; for compressed formats, use notificationThresholdBytes instead.
The native capture thread notifies Dart when a chunk is ready and waits for Dart to advance the read position. If the Dart thread is busy with synchronous work while a chunk is ready, the notification is delayed and the next chunk interval grows by roughly the time Dart was blocked. For example, a 500 ms main-thread stall produces a ~500 ms gap between PCM chunks.
To avoid gaps in the captured stream, keep synchronous work off the Dart thread while capture is running, or use a larger buffer if occasional bursts are acceptable. It is also possible to use an Isolate to run capture flow in a separate thread. The example/lib/mixer_capture/isolate_capture_test.dart example demonstrates how to use the capture flow inside an Isolate other than the main.
import 'dart:io';
import 'dart:typed_data';
import 'package:flutter_soloud/flutter_soloud.dart';
Future<void> recordMixerOutput(String outputPath) async {
await SoLoud.instance.init();
final file = File(outputPath);
final sink = file.openWrite();
final stream = SoLoud.instance.startMixerOutputStream(
format: MixerOutputFormat.pcmS16le,
bufferSizeBytes: 1024 * 1024,
notificationThresholdBytes: 4096,
);
final subscription = stream.listen((Uint8List chunk) {
sink.add(chunk);
});
// Play a sound
final sound = await SoLoud.instance.loadAsset('assets/music.mp3');
await SoLoud.instance.play(sound);
// Record for 5 seconds
await Future<void>.delayed(const Duration(seconds: 5));
SoLoud.instance.stopMixerOutputStream();
await subscription.cancel();
await sink.close();
}