Skip to content

feat(audio): expose master output capture#466

Open
Slashpaf wants to merge 1 commit into
alnitak:mainfrom
Slashpaf:feature/master-output-tap
Open

feat(audio): expose master output capture#466
Slashpaf wants to merge 1 commit into
alnitak:mainfrom
Slashpaf:feature/master-output-tap

Conversation

@Slashpaf

@Slashpaf Slashpaf commented May 17, 2026

Copy link
Copy Markdown

Description

Adds an opt-in API for capturing the final SoLoud mixed output as interleaved float32 PCM.

The capture point is in the miniaudio backend immediately after soloud->mix(...), before the buffer is submitted to the playback device. This gives applications access to the same final master mix that is being played by the engine.

The implementation does not touch existing code, only new implementation that does not affect flutter_soloud if not enabled. Web returns notImplemented (not using miniaudio).

Public Dart API added:

  • SoLoud.instance.setOutputCaptureEnabled(...)
  • SoLoud.instance.readOutputCapture(maxFrames)
  • SoLoud.instance.getOutputCaptureAvailableFrames()
  • SoLoud.instance.getOutputCaptureDroppedFrames()
  • SoLoud.instance.getOutputCaptureFormat()

Use cases include local streaming, recording, remote monitoring, remote playback, integrations, and external visualization/analysis.

@Slashpaf

Copy link
Copy Markdown
Author

@alnitak Just adding a concrete use case for this PR.

In Audio Forge I use this to stream the exact final SoLoud master mix to remote listeners. The app mixes music, ambience, effects, filters, volume changes, etc. locally, then reads the final interleaved float32 PCM output and sends it to a room server for redistribution.

This is different from capturing one source or using visualization data: the app gets the same final post-mix audio that is sent to the playback device(s).

Minimal usage:

final soloud = SoLoud.instance;

final format = soloud.getOutputCaptureFormat();

soloud.setOutputCaptureEnabled(true, maxBufferedFrames: 44100 * 2);

Timer.periodic(const Duration(milliseconds: 20), (_) {
  final available = soloud.getOutputCaptureAvailableFrames();
  if (available == 0) return;

  final samples = soloud.readOutputCapture(2048); // interleaved float32 PCM
  sendToServer(samples, format.sampleRate, format.channels);
});

// Later:
soloud.setOutputCaptureEnabled(false);

This enables use cases like remote playback, recording the full app mix, monitoring, broadcast rooms, and external audio analysis/visualization, while staying fully opt-in.

@alnitak

alnitak commented May 24, 2026

Copy link
Copy Markdown
Owner

Hi @Slashpaf, sorry for the long wait! I had to think about this because it is a tricky feature.

A while back, I already thought about implementing this feat, but there were some problems to take into account. I thought about a buffer too, but I'd preferred to have a stream for listening to the new audio data (instead of a Timer.periodic for checking).

This should be a nice feature to have, there is also an issue asking for this. Please let me think what we can do about this a little more! For example would be nice to have the audio data in other formats (instead of the default SoLoud interleaved floats32), or maybe already compressed audio with the OGG format.

@Slashpaf

Copy link
Copy Markdown
Author

@alnitak no worries, thanks for the reply.

I agree that stream-based dart API would be nicer than the polls. This way users don't have to build polling loops.

I'll rework that.

For the first version I think keeping raw float32 PCM is best. It's the most flexible base format. Compressed output could be added later on top of this feature?

@alnitak

alnitak commented May 25, 2026

Copy link
Copy Markdown
Owner

Compressed output could be added later on top of this feature?

Yes, I think that could be done later by setting some parameters when you enable capturing, and then filling the buffer with the format chosen.

Anyway, I don't want you to waste your time with something which maybe has a better solution. I think the tricky part is also to pass the data from C to Dart and do this in a way that doesn't interfere with the main UI isolate. A good way, I think, to read audio data from Flutter, is to use something like Filip wrote in his blog here in the FFI to the rescue chapter. Doing that, I think it will also work for the web. For all this, I think the workflow of this PR should be reviewed using very few methods exposed to the user. Maybe just a startCapture, stopCapture and a stream listener to the isolate that works just to acquire new data (?).

Maybe this logic can also be used on the contrary: send audio data to the output mixer. I already saw a discussion for this, but there wasn't a (pro) solution due to the lack of Dart to share memory between isolates/threads (yet).

I also thought that the audio data acquired from Flutter could have something that identifies that chunk, like a sequence number, a timestamp, or a format.

Well, I need to draw some schematics and do some tests for this :).

@Slashpaf

Copy link
Copy Markdown
Author

Okay, understood. I do require this feature so I went ahead and revised the implementation. The API is now much better with a simple outputCapture() stream method:

final capture = SoLoud.instance.outputCapture();

final subscription = capture.stream.listen((samples) {
  // interleaved float32 PCM
});

final sampleRate = capture.sampleRate;
final channels = capture.channels;

// Later:
await subscription.cancel();

Much better to use now.

I also changed the native capture buffer so the audio callback path stays lock-free and does not block the audio thread.

The API should fit either implementation if you decide it should go another way.

Thanks again, I'll push the updated PR.

@Slashpaf Slashpaf force-pushed the feature/master-output-tap branch from 8f73682 to d02362d Compare May 25, 2026 18:18
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants