Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 29 additions & 0 deletions lib/src/bindings/bindings_player.dart
Original file line number Diff line number Diff line change
Expand Up @@ -985,6 +985,35 @@ abstract class FlutterSoLoud {
bool average = false,
});

// ///////////////////////////////////////
// output capture
// ///////////////////////////////////////

/// Enables or disables capture of the final mixed output.
///
/// Captured samples are interleaved float32 PCM in the engine output format.
@mustBeOverridden
PlayerErrors setOutputCaptureEnabled(
bool enabled, {
int maxBufferedFrames = 44100,
});

/// Reads up to [maxFrames] captured frames.
///
/// The returned list contains interleaved float32 samples. Its length is
/// `framesRead * channels`, where `channels` comes from
/// [getOutputCaptureFormat].
@mustBeOverridden
Float32List readOutputCapture(int maxFrames);

/// Returns captured frames currently available to read.
@mustBeOverridden
int getOutputCaptureAvailableFrames();

/// Returns the capture sample rate and channel count.
@mustBeOverridden
({PlayerErrors error, int sampleRate, int channels}) getOutputCaptureFormat();

/////////////////////////////////////////
/// Mixing Bus
/// https://solhsa.com/soloud/mixbus.html
Expand Down
107 changes: 107 additions & 0 deletions lib/src/bindings/bindings_player_ffi.dart
Original file line number Diff line number Diff line change
Expand Up @@ -2388,6 +2388,113 @@ class FlutterSoLoudFfi extends FlutterSoLoud {
)
>();

// ///////////////////////////////////////
// output capture
// ///////////////////////////////////////

@override
PlayerErrors setOutputCaptureEnabled(
bool enabled, {
int maxBufferedFrames = 44100,
}) {
final error = _setOutputCaptureEnabled(enabled, maxBufferedFrames);
return PlayerErrors.values[error];
}

late final _setOutputCaptureEnabledPtr =
_lookup<
ffi.NativeFunction<ffi.UnsignedInt Function(ffi.Bool, ffi.UnsignedInt)>
>('setOutputCaptureEnabled');
late final _setOutputCaptureEnabled = _setOutputCaptureEnabledPtr
.asFunction<int Function(bool, int)>();

@override
Float32List readOutputCapture(int maxFrames) {
final format = getOutputCaptureFormat();
if (format.error != PlayerErrors.noError ||
format.channels <= 0 ||
maxFrames <= 0) {
return Float32List(0);
}

final out = calloc<ffi.Float>(maxFrames * format.channels);
final framesRead = calloc<ffi.UnsignedInt>();
try {
final error = _readOutputCapture(out, maxFrames, framesRead);
if (PlayerErrors.values[error] != PlayerErrors.noError ||
framesRead.value == 0) {
return Float32List(0);
}

final sampleCount = framesRead.value * format.channels;
return Float32List.fromList(out.asTypedList(sampleCount));
} finally {
calloc
..free(out)
..free(framesRead);
}
}

late final _readOutputCapturePtr =
_lookup<
ffi.NativeFunction<
ffi.UnsignedInt Function(
ffi.Pointer<ffi.Float>,
ffi.UnsignedInt,
ffi.Pointer<ffi.UnsignedInt>,
)
>
>('readOutputCapture');
late final _readOutputCapture = _readOutputCapturePtr
.asFunction<
int Function(ffi.Pointer<ffi.Float>, int, ffi.Pointer<ffi.UnsignedInt>)
>();

@override
int getOutputCaptureAvailableFrames() {
return _getOutputCaptureAvailableFrames();
}

late final _getOutputCaptureAvailableFramesPtr =
_lookup<ffi.NativeFunction<ffi.UnsignedInt Function()>>(
'getOutputCaptureAvailableFrames',
);
late final _getOutputCaptureAvailableFrames =
_getOutputCaptureAvailableFramesPtr.asFunction<int Function()>();

@override
({PlayerErrors error, int sampleRate, int channels})
getOutputCaptureFormat() {
final sampleRate = calloc<ffi.UnsignedInt>();
final channels = calloc<ffi.UnsignedInt>();
try {
final error = _getOutputCaptureFormat(sampleRate, channels);
return (
error: PlayerErrors.values[error],
sampleRate: sampleRate.value,
channels: channels.value,
);
} finally {
calloc
..free(sampleRate)
..free(channels);
}
}

late final _getOutputCaptureFormatPtr =
_lookup<
ffi.NativeFunction<
ffi.UnsignedInt Function(
ffi.Pointer<ffi.UnsignedInt>,
ffi.Pointer<ffi.UnsignedInt>,
)
>
>('getOutputCaptureFormat');
late final _getOutputCaptureFormat = _getOutputCaptureFormatPtr
.asFunction<
int Function(ffi.Pointer<ffi.UnsignedInt>, ffi.Pointer<ffi.UnsignedInt>)
>();

/////////////////////////////////////////
/// Mixing Bus
/// https://solhsa.com/soloud/mixbus.html
Expand Down
28 changes: 28 additions & 0 deletions lib/src/bindings/bindings_player_web.dart
Original file line number Diff line number Diff line change
Expand Up @@ -1207,6 +1207,34 @@ class FlutterSoLoudWeb extends FlutterSoLoud {
return samples;
}

// ///////////////////////////////////////
// output capture
// ///////////////////////////////////////

@override
PlayerErrors setOutputCaptureEnabled(
bool enabled, {
int maxBufferedFrames = 44100,
}) {
return PlayerErrors.notImplemented;
}

@override
Float32List readOutputCapture(int maxFrames) {
return Float32List(0);
}

@override
int getOutputCaptureAvailableFrames() {
return 0;
}

@override
({PlayerErrors error, int sampleRate, int channels})
getOutputCaptureFormat() {
return (error: PlayerErrors.notImplemented, sampleRate: 0, channels: 0);
}

/////////////////////////////////////////
/// Mixing Bus
/// https://solhsa.com/soloud/mixbus.html
Expand Down
163 changes: 163 additions & 0 deletions lib/src/soloud.dart
Original file line number Diff line number Diff line change
Expand Up @@ -293,6 +293,9 @@ interface class SoLoud {
/// The channels the engine was initialized with.
Channels _channels = Channels.stereo;

StreamController<Float32List>? _outputCaptureController;
Timer? _outputCaptureTimer;

/// Initializes the audio engine.
///
/// Run this before anything else, and `await` its result in a try/catch.
Expand Down Expand Up @@ -455,6 +458,7 @@ interface class SoLoud {
/// or inside "AppLifecycleListener.onExitRequested".
void deinit() {
_log.finest('deinit() called');
_stopOutputCaptureStream();
_nativeCallbacksInitialized = false;
_controller.soLoudFFI.disposeNativeCallables();
_controller.soLoudFFI.disposeAllSound();
Expand Down Expand Up @@ -3073,6 +3077,165 @@ interface class SoLoud {
return samples;
}

/// Creates a stream of captured master output with its audio format.
///
/// This is the simplest API for output capture. Listen to the returned
/// [stream] to enable native capture; cancel the subscription to disable it.
///
/// [maxBufferedFrames] controls the native ring buffer size.
/// [chunkFrames] controls the maximum frames emitted in a single stream
/// event.
/// [interval] controls how often Dart drains the native capture buffer.
({int sampleRate, int channels, Stream<Float32List> stream}) outputCapture({
int maxBufferedFrames = 44100,
int chunkFrames = 2048,
Duration interval = const Duration(milliseconds: 20),
}) {
if (!isInitialized) {
throw const SoLoudNotInitializedException();
}
final format = _getOutputCaptureFormat();
return (
sampleRate: format.sampleRate,
channels: format.channels,
stream: _outputCaptureStream(
maxBufferedFrames: maxBufferedFrames,
chunkFrames: chunkFrames,
interval: interval,
),
);
}

Stream<Float32List> _outputCaptureStream({
int maxBufferedFrames = 44100,
int chunkFrames = 2048,
Duration interval = const Duration(milliseconds: 20),
}) {
if (!isInitialized) {
throw const SoLoudNotInitializedException();
}
if (maxBufferedFrames <= 0) {
throw ArgumentError.value(
maxBufferedFrames,
'maxBufferedFrames',
'Must be greater than zero.',
);
}
if (chunkFrames <= 0) {
throw ArgumentError.value(
chunkFrames,
'chunkFrames',
'Must be greater than zero.',
);
}

late final StreamController<Float32List> controller;

void drainCapture() {
if (!isInitialized || controller.isClosed) {
_stopOutputCaptureStream();
return;
}

try {
final availableFrames = _getOutputCaptureAvailableFrames();
if (availableFrames <= 0 || controller.isClosed) {
return;
}

final framesToRead = availableFrames < chunkFrames
? availableFrames
: chunkFrames;
final samples = _readOutputCapture(framesToRead);
if (samples.isNotEmpty) {
controller.add(samples);
}
} catch (error, stackTrace) {
controller.addError(error, stackTrace);
_stopOutputCaptureStream();
}
}

controller = StreamController<Float32List>(
onListen: () {
if (_outputCaptureController != null) {
controller.addError(
StateError('An output capture stream is already active.'),
);
unawaited(controller.close());
return;
}

_outputCaptureController = controller;
try {
_setOutputCaptureEnabled(
true,
maxBufferedFrames: maxBufferedFrames,
);
} catch (error, stackTrace) {
controller.addError(error, stackTrace);
_stopOutputCaptureStream();
return;
}
_outputCaptureTimer = Timer.periodic(interval, (_) => drainCapture());
},
onCancel: () {
if (_outputCaptureController == controller) {
_stopOutputCaptureStream(closeController: false);
}
},
);
return controller.stream;
}

void _setOutputCaptureEnabled(
bool enabled, {
int maxBufferedFrames = 44100,
}) {
final error = _controller.soLoudFFI.setOutputCaptureEnabled(
enabled,
maxBufferedFrames: maxBufferedFrames,
);
if (error != PlayerErrors.noError) {
throw SoLoudCppException.fromPlayerError(error);
}
}

Float32List _readOutputCapture(int maxFrames) {
return _controller.soLoudFFI.readOutputCapture(maxFrames);
}

int _getOutputCaptureAvailableFrames() {
return _controller.soLoudFFI.getOutputCaptureAvailableFrames();
}

({int sampleRate, int channels}) _getOutputCaptureFormat() {
final format = _controller.soLoudFFI.getOutputCaptureFormat();
if (format.error != PlayerErrors.noError) {
throw SoLoudCppException.fromPlayerError(format.error);
}
return (sampleRate: format.sampleRate, channels: format.channels);
}

void _stopOutputCaptureStream({bool closeController = true}) {
_outputCaptureTimer?.cancel();
_outputCaptureTimer = null;

if (_controller.soLoudFFI.isInited()) {
final error = _controller.soLoudFFI.setOutputCaptureEnabled(false);
if (error != PlayerErrors.noError &&
error != PlayerErrors.notImplemented) {
_logPlayerError(error, from: 'setOutputCaptureEnabled(false)');
}
}

final controller = _outputCaptureController;
_outputCaptureController = null;
if (closeController && controller != null && !controller.isClosed) {
unawaited(controller.close());
}
}

/////////////////////////////////////////
/// Mixing Bus
/// How it works:
Expand Down
Loading