diff --git a/apps/common-app/src/examples/PlaybackSpeed/constants.ts b/apps/common-app/src/examples/PlaybackSpeed/constants.ts index 2da2de079..bb33612f2 100644 --- a/apps/common-app/src/examples/PlaybackSpeed/constants.ts +++ b/apps/common-app/src/examples/PlaybackSpeed/constants.ts @@ -15,8 +15,8 @@ export const PCM_DATA = PCM_DATA1 + PCM_DATA2 + PCM_DATA3 + PCM_DATA4; export const labelWidth = 80; export const PLAYBACK_SPEED_CONFIG = { - min: 0.25, - max: 3.0, + min: 0.5, + max: 2.0, step: 0.25, default: 1, } as const; diff --git a/apps/fabric-example/ios/Podfile.lock b/apps/fabric-example/ios/Podfile.lock index 4ae35caae..95fdb6ff6 100644 --- a/apps/fabric-example/ios/Podfile.lock +++ b/apps/fabric-example/ios/Podfile.lock @@ -3081,7 +3081,7 @@ SPEC CHECKSUMS: ReactAppDependencyProvider: c91900fa724baee992f01c05eeb4c9e01a807f78 ReactCodegen: 8125d6ee06ea06f48f156cbddec5c2ca576d62e6 ReactCommon: 116d6ee71679243698620d8cd9a9042541e44aa6 - RNAudioAPI: f483edfc07b2343174d27a99e9e40d8b89ca33d1 + RNAudioAPI: 33e9940fd9e8b3694107f461a45fe23eb40c652b RNGestureHandler: 3a73f098d74712952870e948b3d9cf7b6cae9961 RNReanimated: a035264789d1f64cb5adba7085d6aac6e9ec70a7 RNScreens: 6ced6ae8a526512a6eef6e28c2286e1fc2d378c3 diff --git a/packages/audiodocs/docs/sources/audio-buffer-source-node.mdx b/packages/audiodocs/docs/sources/audio-buffer-source-node.mdx index d4ac541a5..a6e1e5538 100644 --- a/packages/audiodocs/docs/sources/audio-buffer-source-node.mdx +++ b/packages/audiodocs/docs/sources/audio-buffer-source-node.mdx @@ -99,6 +99,20 @@ Schedules `AudioBufferSourceNode` to start playback of audio data contained in t #### Returns `undefined`. +## Events + +### `onLoopEnded` + +Sets (or remove) callback that will be fired when buffer source node reached the end of the loop and is looping back to `loopStart`. +You can remove callback by passing `null`. + + +```ts +audioBufferSourceNode.onLoopEnded = () => { //setting callback + console.log("loop ended"); +}; +``` + ## Remarks #### `buffer` diff --git a/packages/react-native-audio-api/common/cpp/audioapi/HostObjects/sources/AudioBufferSourceNodeHostObject.cpp b/packages/react-native-audio-api/common/cpp/audioapi/HostObjects/sources/AudioBufferSourceNodeHostObject.cpp index 72a392ec3..d9cd24d07 100644 --- a/packages/react-native-audio-api/common/cpp/audioapi/HostObjects/sources/AudioBufferSourceNodeHostObject.cpp +++ b/packages/react-native-audio-api/common/cpp/audioapi/HostObjects/sources/AudioBufferSourceNodeHostObject.cpp @@ -19,7 +19,8 @@ AudioBufferSourceNodeHostObject::AudioBufferSourceNodeHostObject( JSI_EXPORT_PROPERTY_SETTER(AudioBufferSourceNodeHostObject, loop), JSI_EXPORT_PROPERTY_SETTER(AudioBufferSourceNodeHostObject, loopSkip), JSI_EXPORT_PROPERTY_SETTER(AudioBufferSourceNodeHostObject, loopStart), - JSI_EXPORT_PROPERTY_SETTER(AudioBufferSourceNodeHostObject, loopEnd)); + JSI_EXPORT_PROPERTY_SETTER(AudioBufferSourceNodeHostObject, loopEnd), + JSI_EXPORT_PROPERTY_SETTER(AudioBufferSourceNodeHostObject, onLoopEnded)); // start method is overridden in this class functions_->erase("start"); @@ -29,6 +30,16 @@ AudioBufferSourceNodeHostObject::AudioBufferSourceNodeHostObject( JSI_EXPORT_FUNCTION(AudioBufferSourceNodeHostObject, setBuffer)); } +AudioBufferSourceNodeHostObject::~AudioBufferSourceNodeHostObject() { + auto audioBufferSourceNode = + std::static_pointer_cast(node_); + + // When JSI object is garbage collected (together with the eventual callback), + // underlying source node might still be active and try to call the + // non-existing callback. + audioBufferSourceNode->clearOnLoopEndedCallback(); +} + JSI_PROPERTY_GETTER_IMPL(AudioBufferSourceNodeHostObject, loop) { auto audioBufferSourceNode = std::static_pointer_cast(node_); @@ -94,6 +105,14 @@ JSI_PROPERTY_SETTER_IMPL(AudioBufferSourceNodeHostObject, loopEnd) { audioBufferSourceNode->setLoopEnd(value.getNumber()); } +JSI_PROPERTY_SETTER_IMPL(AudioBufferSourceNodeHostObject, onLoopEnded) { + auto audioBufferSourceNode = + std::static_pointer_cast(node_); + + audioBufferSourceNode->setOnLoopEndedCallbackId( + std::stoull(value.getString(runtime).utf8(runtime))); +} + JSI_HOST_FUNCTION_IMPL(AudioBufferSourceNodeHostObject, start) { auto when = args[0].getNumber(); auto offset = args[1].getNumber(); diff --git a/packages/react-native-audio-api/common/cpp/audioapi/HostObjects/sources/AudioBufferSourceNodeHostObject.h b/packages/react-native-audio-api/common/cpp/audioapi/HostObjects/sources/AudioBufferSourceNodeHostObject.h index a3040280c..13bfe8af1 100644 --- a/packages/react-native-audio-api/common/cpp/audioapi/HostObjects/sources/AudioBufferSourceNodeHostObject.h +++ b/packages/react-native-audio-api/common/cpp/audioapi/HostObjects/sources/AudioBufferSourceNodeHostObject.h @@ -16,6 +16,8 @@ class AudioBufferSourceNodeHostObject explicit AudioBufferSourceNodeHostObject( const std::shared_ptr &node); + ~AudioBufferSourceNodeHostObject() override; + JSI_PROPERTY_GETTER_DECL(loop); JSI_PROPERTY_GETTER_DECL(loopSkip); JSI_PROPERTY_GETTER_DECL(buffer); @@ -26,6 +28,7 @@ class AudioBufferSourceNodeHostObject JSI_PROPERTY_SETTER_DECL(loopSkip); JSI_PROPERTY_SETTER_DECL(loopStart); JSI_PROPERTY_SETTER_DECL(loopEnd); + JSI_PROPERTY_SETTER_DECL(onLoopEnded); JSI_HOST_FUNCTION_DECL(start); JSI_HOST_FUNCTION_DECL(setBuffer); diff --git a/packages/react-native-audio-api/common/cpp/audioapi/core/sources/AudioBufferBaseSourceNode.cpp b/packages/react-native-audio-api/common/cpp/audioapi/core/sources/AudioBufferBaseSourceNode.cpp index 934a01f55..15b95702c 100644 --- a/packages/react-native-audio-api/common/cpp/audioapi/core/sources/AudioBufferBaseSourceNode.cpp +++ b/packages/react-native-audio-api/common/cpp/audioapi/core/sources/AudioBufferBaseSourceNode.cpp @@ -28,12 +28,7 @@ AudioBufferBaseSourceNode::AudioBufferBaseSourceNode( } AudioBufferBaseSourceNode::~AudioBufferBaseSourceNode() { - if (onPositionChangedCallbackId_ != 0 && - context_->audioEventHandlerRegistry_ != nullptr) { - context_->audioEventHandlerRegistry_->unregisterHandler( - "positionChanged", onPositionChangedCallbackId_); - onPositionChangedCallbackId_ = 0; - } + clearOnPositionChangedCallback(); } std::shared_ptr AudioBufferBaseSourceNode::getDetuneParam() const { diff --git a/packages/react-native-audio-api/common/cpp/audioapi/core/sources/AudioBufferSourceNode.cpp b/packages/react-native-audio-api/common/cpp/audioapi/core/sources/AudioBufferSourceNode.cpp index 802e7cc74..54b0f8d4d 100644 --- a/packages/react-native-audio-api/common/cpp/audioapi/core/sources/AudioBufferSourceNode.cpp +++ b/packages/react-native-audio-api/common/cpp/audioapi/core/sources/AudioBufferSourceNode.cpp @@ -29,6 +29,8 @@ AudioBufferSourceNode::~AudioBufferSourceNode() { buffer_.reset(); alignedBus_.reset(); + + clearOnLoopEndedCallback(); } bool AudioBufferSourceNode::getLoop() const { @@ -123,6 +125,21 @@ void AudioBufferSourceNode::disable() { alignedBus_.reset(); } +void AudioBufferSourceNode::clearOnLoopEndedCallback() { + if (onLoopEndedCallbackId_ == 0 || context_ == nullptr || + context_->audioEventHandlerRegistry_ == nullptr) { + return; + } + + context_->audioEventHandlerRegistry_->unregisterHandler( + "loopEnded", onLoopEndedCallbackId_); + onLoopEndedCallbackId_ = 0; +} + +void AudioBufferSourceNode::setOnLoopEndedCallbackId(uint64_t callbackId) { + onLoopEndedCallbackId_ = callbackId; +} + void AudioBufferSourceNode::processNode( const std::shared_ptr &processingBus, int framesToProcess) { @@ -150,6 +167,15 @@ double AudioBufferSourceNode::getCurrentPosition() const { static_cast(vReadIndex_), buffer_->getSampleRate()); } +void AudioBufferSourceNode::sendOnLoopEndedEvent() { + auto onLoopEndedCallbackId = + onLoopEndedCallbackId_.load(std::memory_order_acquire); + if (onLoopEndedCallbackId != 0) { + context_->audioEventHandlerRegistry_->invokeHandlerWithEventBody( + "loopEnded", onLoopEndedCallbackId_, {}); + } +} + /** * Helper functions */ @@ -217,6 +243,8 @@ void AudioBufferSourceNode::processWithoutInterpolation( playbackState_ = PlaybackState::STOP_SCHEDULED; break; } + + sendOnLoopEndedEvent(); } } @@ -278,6 +306,8 @@ void AudioBufferSourceNode::processWithInterpolation( playbackState_ = PlaybackState::STOP_SCHEDULED; break; } + + sendOnLoopEndedEvent(); } } } diff --git a/packages/react-native-audio-api/common/cpp/audioapi/core/sources/AudioBufferSourceNode.h b/packages/react-native-audio-api/common/cpp/audioapi/core/sources/AudioBufferSourceNode.h index bffe090e9..9f8779858 100644 --- a/packages/react-native-audio-api/common/cpp/audioapi/core/sources/AudioBufferSourceNode.h +++ b/packages/react-native-audio-api/common/cpp/audioapi/core/sources/AudioBufferSourceNode.h @@ -34,6 +34,9 @@ class AudioBufferSourceNode : public AudioBufferBaseSourceNode { void start(double when, double offset, double duration = -1); void disable() override; + void clearOnLoopEndedCallback(); + void setOnLoopEndedCallbackId(uint64_t callbackId); + protected: void processNode(const std::shared_ptr& processingBus, int framesToProcess) override; double getCurrentPosition() const override; @@ -49,6 +52,9 @@ class AudioBufferSourceNode : public AudioBufferBaseSourceNode { std::shared_ptr buffer_; std::shared_ptr alignedBus_; + std::atomic onLoopEndedCallbackId_ = 0; // 0 means no callback + void sendOnLoopEndedEvent(); + void processWithoutInterpolation( const std::shared_ptr& processingBus, size_t startOffset, diff --git a/packages/react-native-audio-api/common/cpp/audioapi/core/sources/AudioScheduledSourceNode.cpp b/packages/react-native-audio-api/common/cpp/audioapi/core/sources/AudioScheduledSourceNode.cpp index e7338ffb0..4869689cf 100644 --- a/packages/react-native-audio-api/common/cpp/audioapi/core/sources/AudioScheduledSourceNode.cpp +++ b/packages/react-native-audio-api/common/cpp/audioapi/core/sources/AudioScheduledSourceNode.cpp @@ -17,12 +17,7 @@ AudioScheduledSourceNode::AudioScheduledSourceNode(BaseAudioContext *context) } AudioScheduledSourceNode::~AudioScheduledSourceNode() { - if (onEndedCallbackId_ != 0 && - context_->audioEventHandlerRegistry_ != nullptr) { - context_->audioEventHandlerRegistry_->unregisterHandler( - "ended", onEndedCallbackId_); - onEndedCallbackId_ = 0; - } + clearOnEndedCallback(); } void AudioScheduledSourceNode::start(double when) { diff --git a/packages/react-native-audio-api/common/cpp/audioapi/events/AudioEventHandlerRegistry.h b/packages/react-native-audio-api/common/cpp/audioapi/events/AudioEventHandlerRegistry.h index a35eb0cdf..e3beee05c 100644 --- a/packages/react-native-audio-api/common/cpp/audioapi/events/AudioEventHandlerRegistry.h +++ b/packages/react-native-audio-api/common/cpp/audioapi/events/AudioEventHandlerRegistry.h @@ -53,8 +53,9 @@ class AudioEventHandlerRegistry : public IAudioEventHandlerRegistry { "volumeChange", }; - static constexpr std::array AUDIO_API_EVENT_NAMES = { + static constexpr std::array AUDIO_API_EVENT_NAMES = { "ended", + "loopEnded", "audioReady", "positionChanged", "audioError", diff --git a/packages/react-native-audio-api/src/core/AudioBufferBaseSourceNode.ts b/packages/react-native-audio-api/src/core/AudioBufferBaseSourceNode.ts index 28a409a75..4b0a37ae1 100644 --- a/packages/react-native-audio-api/src/core/AudioBufferBaseSourceNode.ts +++ b/packages/react-native-audio-api/src/core/AudioBufferBaseSourceNode.ts @@ -8,8 +8,8 @@ import AudioScheduledSourceNode from './AudioScheduledSourceNode'; export default class AudioBufferBaseSourceNode extends AudioScheduledSourceNode { readonly playbackRate: AudioParam; readonly detune: AudioParam; - private positionChangedSubscription?: AudioEventSubscription; - private positionChangedCallback?: (event: EventTypeWithValue) => void; + private onPositionChangedSubscription?: AudioEventSubscription; + private onPositionChangedCallback?: (event: EventTypeWithValue) => void; constructor(context: BaseAudioContext, node: IAudioBufferBaseSourceNode) { super(context, node); @@ -21,7 +21,7 @@ export default class AudioBufferBaseSourceNode extends AudioScheduledSourceNode public get onPositionChanged(): | ((event: EventTypeWithValue) => void) | undefined { - return this.positionChangedCallback; + return this.onPositionChangedCallback; } public set onPositionChanged( @@ -29,19 +29,19 @@ export default class AudioBufferBaseSourceNode extends AudioScheduledSourceNode ) { if (!callback) { (this.node as IAudioBufferBaseSourceNode).onPositionChanged = '0'; - this.positionChangedSubscription?.remove(); - this.positionChangedSubscription = undefined; - this.positionChangedCallback = undefined; + this.onPositionChangedSubscription?.remove(); + this.onPositionChangedSubscription = undefined; + this.onPositionChangedCallback = undefined; return; } - this.positionChangedCallback = callback; - this.positionChangedSubscription = + this.onPositionChangedCallback = callback; + this.onPositionChangedSubscription = this.audioEventEmitter.addAudioEventListener('positionChanged', callback); (this.node as IAudioBufferBaseSourceNode).onPositionChanged = - this.positionChangedSubscription.subscriptionId; + this.onPositionChangedSubscription.subscriptionId; } public get onPositionChangedInterval(): number { diff --git a/packages/react-native-audio-api/src/core/AudioBufferQueueSourceNode.ts b/packages/react-native-audio-api/src/core/AudioBufferQueueSourceNode.ts index bfc923d2a..13bab8779 100644 --- a/packages/react-native-audio-api/src/core/AudioBufferQueueSourceNode.ts +++ b/packages/react-native-audio-api/src/core/AudioBufferQueueSourceNode.ts @@ -24,21 +24,13 @@ export default class AudioBufferQueueSourceNode extends AudioBufferBaseSourceNod (this.node as IAudioBufferQueueSourceNode).clearBuffers(); } - public override start(when: number = 0, offset?: number): void { + public override start(when: number = 0): void { if (when < 0) { throw new RangeError( `when must be a finite non-negative number: ${when}` ); } - if (offset) { - if (offset < 0) { - throw new RangeError( - `offset must be a finite non-negative number: ${offset}` - ); - } - } - (this.node as IAudioBufferQueueSourceNode).start(when); } diff --git a/packages/react-native-audio-api/src/core/AudioBufferSourceNode.ts b/packages/react-native-audio-api/src/core/AudioBufferSourceNode.ts index 7bda1a425..e8dba4a23 100644 --- a/packages/react-native-audio-api/src/core/AudioBufferSourceNode.ts +++ b/packages/react-native-audio-api/src/core/AudioBufferSourceNode.ts @@ -3,8 +3,12 @@ import AudioBufferBaseSourceNode from './AudioBufferBaseSourceNode'; import AudioBuffer from './AudioBuffer'; import { InvalidStateError, RangeError } from '../errors'; import { EventEmptyType } from '../events/types'; +import { AudioEventSubscription } from '../events'; export default class AudioBufferSourceNode extends AudioBufferBaseSourceNode { + private onLoopEndedSubscription?: AudioEventSubscription; + private onLoopEndedCallback?: (event: EventEmptyType) => void; + public get buffer(): AudioBuffer | null { const buffer = (this.node as IAudioBufferSourceNode).buffer; if (!buffer) { @@ -90,4 +94,28 @@ export default class AudioBufferSourceNode extends AudioBufferBaseSourceNode { ) { super.onEnded = callback; } + + public get onLoopEnded(): ((event: EventEmptyType) => void) | undefined { + return this.onLoopEndedCallback; + } + + public set onLoopEnded(callback: ((event: EventEmptyType) => void) | null) { + if (!callback) { + (this.node as IAudioBufferSourceNode).onLoopEnded = '0'; + this.onLoopEndedSubscription?.remove(); + this.onLoopEndedSubscription = undefined; + this.onLoopEndedCallback = undefined; + + return; + } + + this.onLoopEndedCallback = callback; + this.onLoopEndedSubscription = this.audioEventEmitter.addAudioEventListener( + 'loopEnded', + callback + ); + + (this.node as IAudioBufferSourceNode).onLoopEnded = + this.onLoopEndedSubscription.subscriptionId; + } } diff --git a/packages/react-native-audio-api/src/events/types.ts b/packages/react-native-audio-api/src/events/types.ts index 9a64e0ed3..9db18bd13 100644 --- a/packages/react-native-audio-api/src/events/types.ts +++ b/packages/react-native-audio-api/src/events/types.ts @@ -56,6 +56,7 @@ export interface OnAudioReadyEventType { interface AudioAPIEvents { ended: OnEndedEventType; + loopEnded: EventEmptyType; audioReady: OnAudioReadyEventType; positionChanged: EventTypeWithValue; audioError: EventEmptyType; // to change diff --git a/packages/react-native-audio-api/src/interfaces.ts b/packages/react-native-audio-api/src/interfaces.ts index ff79c6237..97af09bd4 100644 --- a/packages/react-native-audio-api/src/interfaces.ts +++ b/packages/react-native-audio-api/src/interfaces.ts @@ -140,6 +140,9 @@ export interface IAudioBufferSourceNode extends IAudioBufferBaseSourceNode { start: (when?: number, offset?: number, duration?: number) => void; setBuffer: (audioBuffer: IAudioBuffer | null) => void; + + // passing subscriptionId(uint_64 in cpp, string in js) to the cpp + onLoopEnded: string; } export interface IAudioBufferQueueSourceNode