diff --git a/packages/react-native-audio-api/common/cpp/audioapi/HostObjects/BaseAudioContextHostObject.cpp b/packages/react-native-audio-api/common/cpp/audioapi/HostObjects/BaseAudioContextHostObject.cpp index e66d8feaf..fadced0ca 100644 --- a/packages/react-native-audio-api/common/cpp/audioapi/HostObjects/BaseAudioContextHostObject.cpp +++ b/packages/react-native-audio-api/common/cpp/audioapi/HostObjects/BaseAudioContextHostObject.cpp @@ -47,7 +47,6 @@ BaseAudioContextHostObject::BaseAudioContextHostObject( addGetters( JSI_EXPORT_PROPERTY_GETTER(BaseAudioContextHostObject, destination), JSI_EXPORT_PROPERTY_GETTER(BaseAudioContextHostObject, listener), - JSI_EXPORT_PROPERTY_GETTER(BaseAudioContextHostObject, state), JSI_EXPORT_PROPERTY_GETTER(BaseAudioContextHostObject, sampleRate), JSI_EXPORT_PROPERTY_GETTER(BaseAudioContextHostObject, currentTime)); @@ -85,11 +84,6 @@ JSI_PROPERTY_GETTER_IMPL(BaseAudioContextHostObject, listener) { return jsi::Object::createFromHostObject(runtime, listener_); } -JSI_PROPERTY_GETTER_IMPL(BaseAudioContextHostObject, state) { - return jsi::String::createFromUtf8( - runtime, js_enum_parser::contextStateToString(context_->getState())); -} - JSI_PROPERTY_GETTER_IMPL(BaseAudioContextHostObject, sampleRate) { return {context_->getSampleRate()}; } diff --git a/packages/react-native-audio-api/common/cpp/audioapi/HostObjects/BaseAudioContextHostObject.h b/packages/react-native-audio-api/common/cpp/audioapi/HostObjects/BaseAudioContextHostObject.h index 5457ec121..d79425db0 100644 --- a/packages/react-native-audio-api/common/cpp/audioapi/HostObjects/BaseAudioContextHostObject.h +++ b/packages/react-native-audio-api/common/cpp/audioapi/HostObjects/BaseAudioContextHostObject.h @@ -27,7 +27,6 @@ class BaseAudioContextHostObject : public HostObject { JSI_PROPERTY_GETTER_DECL(destination); JSI_PROPERTY_GETTER_DECL(listener); - JSI_PROPERTY_GETTER_DECL(state); JSI_PROPERTY_GETTER_DECL(sampleRate); JSI_PROPERTY_GETTER_DECL(currentTime); diff --git a/packages/react-native-audio-api/common/cpp/audioapi/HostObjects/utils/JsEnumParser.cpp b/packages/react-native-audio-api/common/cpp/audioapi/HostObjects/utils/JsEnumParser.cpp index bcfa85d5b..ece4c803f 100644 --- a/packages/react-native-audio-api/common/cpp/audioapi/HostObjects/utils/JsEnumParser.cpp +++ b/packages/react-native-audio-api/common/cpp/audioapi/HostObjects/utils/JsEnumParser.cpp @@ -152,19 +152,6 @@ AudioEvent audioEventFromString(const std::string &event) { throw std::invalid_argument("Unknown audio event: " + event); } -std::string contextStateToString(ContextState state) { - switch (state) { - case ContextState::SUSPENDED: - return "suspended"; - case ContextState::RUNNING: - return "running"; - case ContextState::CLOSED: - return "closed"; - default: - throw std::invalid_argument("Unknown context state"); - } -} - std::string channelCountModeToString(ChannelCountMode mode) { switch (mode) { case ChannelCountMode::MAX: diff --git a/packages/react-native-audio-api/common/cpp/audioapi/core/AudioContext.cpp b/packages/react-native-audio-api/common/cpp/audioapi/core/AudioContext.cpp index fed66eb8a..252520412 100644 --- a/packages/react-native-audio-api/common/cpp/audioapi/core/AudioContext.cpp +++ b/packages/react-native-audio-api/common/cpp/audioapi/core/AudioContext.cpp @@ -129,7 +129,7 @@ bool AudioContext::resume(const std::shared_ptr> &p } if (result) { - // Visible RUNNING is applied in the promise resolve task (CallInvoker). + // Visible RUNNING is applied inside the resolver ContextPromiseResolver::resolve(promise); } else { ContextPromiseResolver::reject(promise, "Failed to resume audio context."); @@ -155,7 +155,7 @@ bool AudioContext::suspend(const std::shared_ptr> & processAudioEvents(); } - // Visible SUSPENDED is applied in the promise resolve task (CallInvoker). + // Visible SUSPENDED is applied inside the resolver ContextPromiseResolver::resolve(promise); return true; } 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 327c51c8c..e31f2a215 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 @@ -25,14 +25,6 @@ AudioScheduledSourceNode::AudioScheduledSourceNode( onEndedEvent_(context->getAudioEventHandlerRegistry()) {} void AudioScheduledSourceNode::start(double when) { -#if !RN_AUDIO_API_TEST - if (std::shared_ptr context = context_.lock()) { - if (auto *audioContext = dynamic_cast(context.get())) { - audioContext->start(); - } - } -#endif // RN_AUDIO_API_TEST - playbackState_ = PlaybackState::SCHEDULED; startTime_ = when; } diff --git a/packages/react-native-audio-api/src/core/AudioContext.ts b/packages/react-native-audio-api/src/core/AudioContext.ts index 8bf61ba42..8941e656c 100644 --- a/packages/react-native-audio-api/src/core/AudioContext.ts +++ b/packages/react-native-audio-api/src/core/AudioContext.ts @@ -1,3 +1,4 @@ +import { InvalidStateError } from '../errors'; import { assertSupportedSampleRate } from '../utils/validation'; import { AudioTagHandle } from '../Audio/types'; import { IAudioContext } from '../jsi-interfaces'; @@ -28,17 +29,44 @@ export default class AudioContext extends BaseAudioContext { } async close(): Promise { + if (this._state === 'closed') { + throw new InvalidStateError('Cannot close a closed audio context.'); + } + + this._state = 'closed'; return (this.context as IAudioContext).close(); } async resume(): Promise { + if (this._state === 'closed') { + throw new InvalidStateError('Cannot resume a closed audio context.'); + } + + this._state = 'running'; return (this.context as IAudioContext).resume(); } async suspend(): Promise { + if (this._state === 'closed') { + throw new InvalidStateError('Cannot suspend a closed audio context.'); + } + + this._state = 'suspended'; return (this.context as IAudioContext).suspend(); } + /** + * @internal Called by AudioScheduledSourceNode.start(). The native driver + * can start implicitly from the first scheduled source, with no promise to + * carry the transition, so this publishes the state that follows. + */ + public override markRunningOnSourceStart(): void { + if (this._state === 'suspended') { + this._state = 'running'; + (this.context as IAudioContext).resume(); + } + } + createMediaElementSource( mediaElement: AudioTagHandle ): MediaElementAudioSourceNode { diff --git a/packages/react-native-audio-api/src/core/AudioScheduledSourceNode.ts b/packages/react-native-audio-api/src/core/AudioScheduledSourceNode.ts index 61f4bb243..45c27dce7 100644 --- a/packages/react-native-audio-api/src/core/AudioScheduledSourceNode.ts +++ b/packages/react-native-audio-api/src/core/AudioScheduledSourceNode.ts @@ -25,6 +25,7 @@ export default class AudioScheduledSourceNode extends AudioNode { this.hasBeenStarted = true; (this.node as IAudioScheduledSourceNode).start(when); + this.context.markRunningOnSourceStart(); } public stop(when: number = 0): void { diff --git a/packages/react-native-audio-api/src/core/BaseAudioContext.ts b/packages/react-native-audio-api/src/core/BaseAudioContext.ts index d47251a11..aa20c53ef 100644 --- a/packages/react-native-audio-api/src/core/BaseAudioContext.ts +++ b/packages/react-native-audio-api/src/core/BaseAudioContext.ts @@ -38,14 +38,24 @@ export default class BaseAudioContext { this.sampleRate = context.sampleRate; } + protected _state: ContextState = 'suspended'; + public get currentTime(): number { return this.context.currentTime; } public get state(): ContextState { - return this.context.state; + return this._state; } + /** + * @internal Called by AudioScheduledSourceNode.start(). No-op here: only + * AudioContext overrides it, since only AudioContext's native driver can + * start implicitly from a source's start() call. OfflineAudioContext only + * starts rendering from an explicit startRendering() call. + */ + public markRunningOnSourceStart(): void {} + public async decodeAudioData( input: DecodeDataInput, fetchOptions?: RequestInit diff --git a/packages/react-native-audio-api/src/core/OfflineAudioContext.ts b/packages/react-native-audio-api/src/core/OfflineAudioContext.ts index 9d922774f..d715a84d7 100644 --- a/packages/react-native-audio-api/src/core/OfflineAudioContext.ts +++ b/packages/react-native-audio-api/src/core/OfflineAudioContext.ts @@ -6,7 +6,6 @@ import AudioBuffer from './AudioBuffer'; import BaseAudioContext from './BaseAudioContext'; export default class OfflineAudioContext extends BaseAudioContext { - private isSuspended: boolean; private isRendering: boolean; private duration: number; @@ -41,7 +40,6 @@ export default class OfflineAudioContext extends BaseAudioContext { throw new NotSupportedError('Invalid constructor arguments'); } - this.isSuspended = false; this.isRendering = false; } @@ -52,14 +50,13 @@ export default class OfflineAudioContext extends BaseAudioContext { ); } - if (!this.isSuspended) { + if (!(this._state === 'suspended')) { throw new InvalidStateError( 'Cannot resume an OfflineAudioContext that is not suspended' ); } - this.isSuspended = false; - + this._state = 'running'; return (this.context as IOfflineAudioContext).resume(); } @@ -80,9 +77,15 @@ export default class OfflineAudioContext extends BaseAudioContext { ); } - this.isSuspended = true; + if (this._state === 'closed') { + throw new InvalidStateError('the rendering is already finished'); + } - return (this.context as IOfflineAudioContext).suspend(suspendTime); + const result = await (this.context as IOfflineAudioContext).suspend( + suspendTime + ); + this._state = 'suspended'; + return result; } async startRendering(): Promise { @@ -91,10 +94,11 @@ export default class OfflineAudioContext extends BaseAudioContext { } this.isRendering = true; - + this._state = 'running'; const audioBuffer = await ( this.context as IOfflineAudioContext ).startRendering(); + this._state = 'closed'; return new AudioBuffer(audioBuffer); } diff --git a/packages/react-native-audio-api/src/jsi-interfaces.ts b/packages/react-native-audio-api/src/jsi-interfaces.ts index 6fb1e6d05..78ea40f8f 100644 --- a/packages/react-native-audio-api/src/jsi-interfaces.ts +++ b/packages/react-native-audio-api/src/jsi-interfaces.ts @@ -14,7 +14,6 @@ import type { ChannelSplitterOptions, ConstantSourceOptions, ConvolverOptions, - ContextState, DelayOptions, FileInfo, GainOptions, @@ -40,7 +39,6 @@ export interface IOscillatorOptions extends Omit< export interface IBaseAudioContext { readonly destination: IAudioDestinationNode; readonly listener: IAudioListener; - readonly state: ContextState; readonly sampleRate: number; readonly currentTime: number; readonly decoder: IAudioDecoder;