diff --git a/apps/common-app/src/singletons/index.ts b/apps/common-app/src/singletons/index.ts index 191a5d20a..727cc909b 100644 --- a/apps/common-app/src/singletons/index.ts +++ b/apps/common-app/src/singletons/index.ts @@ -3,5 +3,5 @@ import { AudioContext, AudioRecorder } from 'react-native-audio-api'; export const audioContext = new AudioContext(); export const audioRecorder = new AudioRecorder({ androidInputPreset: 'voiceCommunication', - iosVoiceProcessing: true, + iosVoiceProcessing: false, }); diff --git a/apps/fabric-example/ios/FabricExampleTests/AudioPlayerTests.mm b/apps/fabric-example/ios/FabricExampleTests/AudioPlayerTests.mm index a0f16ee62..d39e4e61a 100644 --- a/apps/fabric-example/ios/FabricExampleTests/AudioPlayerTests.mm +++ b/apps/fabric-example/ios/FabricExampleTests/AudioPlayerTests.mm @@ -22,13 +22,7 @@ class IOSAudioPlayer : public CommonPlayer { public: - IOSAudioPlayer( - const std::function &renderAudio, - float sampleRate, - int channelCount, - std::atomic ¤tRenders, - std::weak_ptr context, - std::mutex *driverMutex); + using CommonPlayer::CommonPlayer; ~IOSAudioPlayer() override; bool start() override; @@ -43,10 +37,12 @@ [[nodiscard]] double getOutputLatency() const override; protected: - NativeAudioPlayer *audioPlayer_; - std::atomic flushOverflowNextPull_; - int pendingSavedCount_; - DSPAudioBuffer pendingSaved_; + NativeAudioPlayer *createNativePlayer(); + + NativeAudioPlayer *audioPlayer_ = createNativePlayer(); + std::atomic flushOverflowNextPull_{false}; + int pendingSavedCount_{0}; + DSPAudioBuffer pendingSaved_{RENDER_QUANTUM_SIZE, channelCount_, sampleRate_}; }; } // namespace audioapi @@ -239,7 +235,8 @@ - (void)cleanup channelCount, currentRendersStorage_, std::weak_ptr{}, - nullptr) {} + nullptr, + AudioContextLatencyHint::INTERACTIVE) {} NativeAudioPlayer *replaceAudioPlayer(NativeAudioPlayer *audioPlayer) { NativeAudioPlayer *previous = audioPlayer_; @@ -321,7 +318,8 @@ - (NativeAudioPlayer *)createPlayerWithRenderCallCount:(NSInteger *)renderCallCo } } sampleRate:48000 - channelCount:2]; + channelCount:2 + preferredIOBufferFrames:RENDER_QUANTUM_SIZE]; } - (void)assertStartLikeOperationRunsGraphForSelector:(SEL)selector @@ -384,8 +382,9 @@ - (void)testRenderBlockInvokesRenderAudioWhenBufferCountMatches renderedFrameCount = static_cast(numFrames); renderedBufferList = outputBuffer; } - sampleRate:48000 - channelCount:2]; + sampleRate:48000 + channelCount:2 + preferredIOBufferFrames:RENDER_QUANTUM_SIZE]; TestAudioOutput output(2, 64, 0.0f); BOOL isSilence = NO; AudioTimeStamp timestamp = {}; diff --git a/packages/audiodocs/docs/core/audio-context.mdx b/packages/audiodocs/docs/core/audio-context.mdx index c2ee43fcf..fa776ece2 100644 --- a/packages/audiodocs/docs/core/audio-context.mdx +++ b/packages/audiodocs/docs/core/audio-context.mdx @@ -21,6 +21,7 @@ constructor(options?: AudioContextOptions) | Parameter | Type | Default | | | :---: | :---: | :----: | :---- | | `sampleRate` | `number` | - | The preferred sample rate for the context. | +| `latencyHint` | `'interactive' \| 'balanced' \| 'playback'` | `'interactive'` | What the context should optimize its output stream for. `interactive` requests the lowest latency the platform offers; `balanced` and `playback` trade output latency for a deeper buffer, which favours glitch-free sustained playback. | #### Errors diff --git a/packages/react-native-audio-api/android/src/main/cpp/audioapi/android/core/AudioPlayer.cpp b/packages/react-native-audio-api/android/src/main/cpp/audioapi/android/core/AudioPlayer.cpp index d03a04a2b..3f869ec6c 100644 --- a/packages/react-native-audio-api/android/src/main/cpp/audioapi/android/core/AudioPlayer.cpp +++ b/packages/react-native-audio-api/android/src/main/cpp/audioapi/android/core/AudioPlayer.cpp @@ -15,6 +15,22 @@ namespace audioapi { +namespace { + +PerformanceMode performanceModeFor(AudioContextLatencyHint latencyHint) { + switch (latencyHint) { + case AudioContextLatencyHint::INTERACTIVE: + return PerformanceMode::LowLatency; + case AudioContextLatencyHint::BALANCED: + return PerformanceMode::None; + case AudioContextLatencyHint::PLAYBACK: + return PerformanceMode::PowerSaving; + } + return PerformanceMode::LowLatency; +} + +} // namespace + bool AudioPlayer::openAudioStream() { std::scoped_lock lock(streamMutex_); AudioStreamBuilder builder; @@ -22,7 +38,7 @@ bool AudioPlayer::openAudioStream() { builder.setSharingMode(SharingMode::Exclusive) ->setFormat(AudioFormat::Float) ->setFormatConversionAllowed(true) - ->setPerformanceMode(PerformanceMode::LowLatency) + ->setPerformanceMode(performanceModeFor(latencyHint_)) ->setChannelCount(channelCount_) ->setSampleRateConversionQuality(SampleRateConversionQuality::Medium) ->setFramesPerDataCallback(RENDER_QUANTUM_SIZE) diff --git a/packages/react-native-audio-api/common/cpp/audioapi/AudioAPIModuleInstaller.h b/packages/react-native-audio-api/common/cpp/audioapi/AudioAPIModuleInstaller.h index ba15c738b..21d9df37d 100644 --- a/packages/react-native-audio-api/common/cpp/audioapi/AudioAPIModuleInstaller.h +++ b/packages/react-native-audio-api/common/cpp/audioapi/AudioAPIModuleInstaller.h @@ -6,6 +6,7 @@ #include #include #include +#include #include #include #include @@ -69,7 +70,7 @@ class AudioAPIModuleInstaller { return jsi::Function::createFromHostFunction( *jsiRuntime, jsi::PropNameID::forAscii(*jsiRuntime, "createAudioContext"), - 1, + 2, [jsCallInvoker, audioEventHandlerRegistry]( jsi::Runtime &runtime, const jsi::Value &thisValue, @@ -77,8 +78,14 @@ class AudioAPIModuleInstaller { size_t count) -> jsi::Value { auto sampleRate = static_cast(args[0].getNumber()); + auto latencyHint = AudioContextLatencyHint::INTERACTIVE; + if (count > 1 && args[1].isString()) { + latencyHint = + js_enum_parser::latencyHintFromString(args[1].getString(runtime).utf8(runtime)); + } + auto audioContextHostObject = std::make_shared( - sampleRate, audioEventHandlerRegistry, &runtime, jsCallInvoker); + sampleRate, audioEventHandlerRegistry, &runtime, jsCallInvoker, latencyHint); return jsi::Object::createFromHostObject(runtime, audioContextHostObject); }); diff --git a/packages/react-native-audio-api/common/cpp/audioapi/HostObjects/AudioContextHostObject.cpp b/packages/react-native-audio-api/common/cpp/audioapi/HostObjects/AudioContextHostObject.cpp index 65917a3e5..99aba6380 100644 --- a/packages/react-native-audio-api/common/cpp/audioapi/HostObjects/AudioContextHostObject.cpp +++ b/packages/react-native-audio-api/common/cpp/audioapi/HostObjects/AudioContextHostObject.cpp @@ -13,9 +13,10 @@ AudioContextHostObject::AudioContextHostObject( float sampleRate, const std::shared_ptr &audioEventHandlerRegistry, jsi::Runtime *runtime, - const std::shared_ptr &callInvoker) + const std::shared_ptr &callInvoker, + AudioContextLatencyHint latencyHint) : BaseAudioContextHostObject( - std::make_shared(sampleRate, audioEventHandlerRegistry), + std::make_shared(sampleRate, audioEventHandlerRegistry, latencyHint), runtime, callInvoker) { addGetters(JSI_EXPORT_PROPERTY_GETTER(AudioContextHostObject, outputLatency)); diff --git a/packages/react-native-audio-api/common/cpp/audioapi/HostObjects/AudioContextHostObject.h b/packages/react-native-audio-api/common/cpp/audioapi/HostObjects/AudioContextHostObject.h index 470bf2a35..15819a0c6 100644 --- a/packages/react-native-audio-api/common/cpp/audioapi/HostObjects/AudioContextHostObject.h +++ b/packages/react-native-audio-api/common/cpp/audioapi/HostObjects/AudioContextHostObject.h @@ -1,9 +1,11 @@ #pragma once #include +#include #include #include + #include namespace audioapi { @@ -17,7 +19,8 @@ class AudioContextHostObject : public BaseAudioContextHostObject { float sampleRate, const std::shared_ptr &audioEventHandlerRegistry, jsi::Runtime *runtime, - const std::shared_ptr &callInvoker); + const std::shared_ptr &callInvoker, + AudioContextLatencyHint latencyHint); ~AudioContextHostObject() override; JSI_HOST_FUNCTION_DECL(close); 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 5c3ec50e3..64bb92468 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 @@ -49,6 +49,15 @@ std::string filterTypeToString(BiquadFilterType type) { } } +AudioContextLatencyHint latencyHintFromString(const std::string &hint) { + if (hint == "balanced") + return AudioContextLatencyHint::BALANCED; + if (hint == "playback") + return AudioContextLatencyHint::PLAYBACK; + + return AudioContextLatencyHint::INTERACTIVE; +} + OverSampleType overSampleTypeFromString(const std::string &type) { if (type == "2x") return OverSampleType::OVERSAMPLE_2X; diff --git a/packages/react-native-audio-api/common/cpp/audioapi/HostObjects/utils/JsEnumParser.h b/packages/react-native-audio-api/common/cpp/audioapi/HostObjects/utils/JsEnumParser.h index 8af08d9cd..90e9fdd57 100644 --- a/packages/react-native-audio-api/common/cpp/audioapi/HostObjects/utils/JsEnumParser.h +++ b/packages/react-native-audio-api/common/cpp/audioapi/HostObjects/utils/JsEnumParser.h @@ -1,6 +1,7 @@ #pragma once #include +#include #include #include #include @@ -29,4 +30,6 @@ std::string distanceModelToString(DistanceModelType model); DistanceModelType distanceModelFromString(const std::string &model); ChannelInterpretation channelInterpretationFromString(const std::string &interpretation); std::string contextStateToString(ContextState state); +/// Interactive, the spec default, for an unrecognised string; a browser would throw a TypeError. +AudioContextLatencyHint latencyHintFromString(const std::string &hint); } // namespace audioapi::js_enum_parser 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 95d2a8954..42de93042 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 @@ -24,8 +24,10 @@ using PlatformAudioPlayer = IOSAudioPlayer; AudioContext::AudioContext( float sampleRate, - const std::shared_ptr &audioEventHandlerRegistry) + const std::shared_ptr &audioEventHandlerRegistry, + AudioContextLatencyHint latencyHint) : BaseAudioContext(sampleRate, audioEventHandlerRegistry), + latencyHint_(latencyHint), isInitialized_(false), onErrorEvent_(audioEventHandlerRegistry) { // Context starts SUSPENDED with no audio-thread consumer. Let the producer @@ -50,7 +52,8 @@ void AudioContext::initialize(const AudioDestinationNode *destination) { destination_->getChannelCount(), currentRenders_, std::static_pointer_cast(shared_from_this()), - &driverMutex_); + &driverMutex_, + latencyHint_); } bool AudioContext::tryStartDriver() { diff --git a/packages/react-native-audio-api/common/cpp/audioapi/core/AudioContext.h b/packages/react-native-audio-api/common/cpp/audioapi/core/AudioContext.h index 16b8efcc6..3b8701a38 100644 --- a/packages/react-native-audio-api/common/cpp/audioapi/core/AudioContext.h +++ b/packages/react-native-audio-api/common/cpp/audioapi/core/AudioContext.h @@ -2,6 +2,7 @@ #include #include +#include #include #include #include @@ -17,7 +18,8 @@ class AudioContext : public BaseAudioContext { public: explicit AudioContext( float sampleRate, - const std::shared_ptr &audioEventHandlerRegistry); + const std::shared_ptr &audioEventHandlerRegistry, + AudioContextLatencyHint latencyHint); ~AudioContext() override; DELETE_COPY_AND_MOVE(AudioContext); @@ -47,6 +49,7 @@ class AudioContext : public BaseAudioContext { private: std::shared_ptr audioPlayer_; + AudioContextLatencyHint latencyHint_; std::atomic isInitialized_{false}; /// Audio I/O callback thread increments around each platform render callback; /// control thread waits on suspend/close. diff --git a/packages/react-native-audio-api/common/cpp/audioapi/core/CommonPlayer.h b/packages/react-native-audio-api/common/cpp/audioapi/core/CommonPlayer.h index eeecc9099..6328f5b99 100644 --- a/packages/react-native-audio-api/common/cpp/audioapi/core/CommonPlayer.h +++ b/packages/react-native-audio-api/common/cpp/audioapi/core/CommonPlayer.h @@ -1,5 +1,6 @@ #pragma once +#include #include #include #include @@ -23,7 +24,8 @@ class CommonPlayer { int channelCount, std::atomic ¤tRenders, std::weak_ptr context, - std::mutex *driverMutex) + std::mutex *driverMutex, + AudioContextLatencyHint latencyHint) : renderAudio_(renderAudio), renderBuffer_( std::make_shared(RENDER_QUANTUM_SIZE, channelCount, sampleRate)), @@ -31,7 +33,8 @@ class CommonPlayer { channelCount_(channelCount), currentRenders_(currentRenders), context_(std::move(context)), - driverMutex_(driverMutex) {} + driverMutex_(driverMutex), + latencyHint_(latencyHint) {} DELETE_COPY_AND_MOVE(CommonPlayer); virtual ~CommonPlayer() = default; @@ -65,6 +68,7 @@ class CommonPlayer { /// The owning context's driver mutex; `nullptr` disables stream-failure reporting. std::mutex *driverMutex_; std::atomic isRunning_{false}; + AudioContextLatencyHint latencyHint_; }; } // namespace audioapi diff --git a/packages/react-native-audio-api/common/cpp/audioapi/core/types/AudioContextLatencyHint.h b/packages/react-native-audio-api/common/cpp/audioapi/core/types/AudioContextLatencyHint.h new file mode 100644 index 000000000..9fe640d71 --- /dev/null +++ b/packages/react-native-audio-api/common/cpp/audioapi/core/types/AudioContextLatencyHint.h @@ -0,0 +1,9 @@ +#pragma once + +#include + +namespace audioapi { + +enum class AudioContextLatencyHint : std::uint8_t { INTERACTIVE, BALANCED, PLAYBACK }; + +} // namespace audioapi diff --git a/packages/react-native-audio-api/ios/audioapi/ios/core/IOSAudioPlayer.h b/packages/react-native-audio-api/ios/audioapi/ios/core/IOSAudioPlayer.h index 7162a201c..5af4777b7 100644 --- a/packages/react-native-audio-api/ios/audioapi/ios/core/IOSAudioPlayer.h +++ b/packages/react-native-audio-api/ios/audioapi/ios/core/IOSAudioPlayer.h @@ -8,6 +8,7 @@ typedef struct objc_object AudioBufferList; #endif // __OBJC__ #include +#include #include #include @@ -21,13 +22,7 @@ namespace audioapi { class IOSAudioPlayer : public CommonPlayer { public: - IOSAudioPlayer( - const std::function &renderAudio, - float sampleRate, - int channelCount, - std::atomic ¤tRenders, - std::weak_ptr context, - std::mutex *driverMutex); + using CommonPlayer::CommonPlayer; ~IOSAudioPlayer() override; DELETE_COPY_AND_MOVE(IOSAudioPlayer); @@ -51,12 +46,17 @@ class IOSAudioPlayer : public CommonPlayer { /// is kept (max 128 frames) and played at the start of the next callback. void deliverOutputBuffers(AudioBufferList *outputData, int numFrames); - NativeAudioPlayer *audioPlayer_; + /// Builds the native player and points its render and stream-failure blocks at this object. + /// Runs as a member initializer, before the members below exist: the blocks only capture + /// `this` and must not be invoked until construction has finished. + NativeAudioPlayer *createNativePlayer(); + + NativeAudioPlayer *audioPlayer_ = createNativePlayer(); /// Set from main thread on start/resume; consumed on audio thread to drop stale pending audio. std::atomic flushOverflowNextPull_{false}; /// Frames valid at the front of each `pendingSaved_[ch]` (0 … RENDER_QUANTUM_SIZE). int pendingSavedCount_{0}; - DSPAudioBuffer pendingSaved_; + DSPAudioBuffer pendingSaved_{RENDER_QUANTUM_SIZE, channelCount_, sampleRate_}; }; } // namespace audioapi diff --git a/packages/react-native-audio-api/ios/audioapi/ios/core/IOSAudioPlayer.mm b/packages/react-native-audio-api/ios/audioapi/ios/core/IOSAudioPlayer.mm index 6eb862e92..ea186b324 100644 --- a/packages/react-native-audio-api/ios/audioapi/ios/core/IOSAudioPlayer.mm +++ b/packages/react-native-audio-api/ios/audioapi/ios/core/IOSAudioPlayer.mm @@ -18,6 +18,20 @@ namespace { +/// In frames of the session's own rate, which may differ from the context's. +int preferredIOBufferFramesFor(AudioContextLatencyHint latencyHint) +{ + switch (latencyHint) { + case AudioContextLatencyHint::INTERACTIVE: + return RENDER_QUANTUM_SIZE; + case AudioContextLatencyHint::BALANCED: + return 8 * RENDER_QUANTUM_SIZE; + case AudioContextLatencyHint::PLAYBACK: + return 32 * RENDER_QUANTUM_SIZE; + } + return RENDER_QUANTUM_SIZE; +} + void reportStreamFailToContext( std::mutex *driverMutex, const std::weak_ptr &context, @@ -37,40 +51,29 @@ void reportStreamFailToContext( } // namespace -IOSAudioPlayer::IOSAudioPlayer( - const std::function &renderAudio, - float sampleRate, - int channelCount, - std::atomic ¤tRenders, - std::weak_ptr context, - std::mutex *driverMutex) - : CommonPlayer( - renderAudio, - sampleRate, - channelCount, - currentRenders, - std::move(context), - driverMutex), - audioPlayer_(nullptr), - pendingSaved_(RENDER_QUANTUM_SIZE, channelCount, sampleRate) +NativeAudioPlayer *IOSAudioPlayer::createNativePlayer() { RenderAudioBlock renderAudioBlock = ^(AudioBufferList *outputData, int numFrames) { deliverOutputBuffers(outputData, numFrames); }; - audioPlayer_ = [[NativeAudioPlayer alloc] initWithRenderAudio:renderAudioBlock - sampleRate:sampleRate - channelCount:channelCount_]; + NativeAudioPlayer *nativePlayer = + [[NativeAudioPlayer alloc] initWithRenderAudio:renderAudioBlock + sampleRate:sampleRate_ + channelCount:channelCount_ + preferredIOBufferFrames:preferredIOBufferFramesFor(latencyHint_)]; std::mutex *driverMutexForCallback = driverMutex_; std::weak_ptr weakContext = context_; IOSAudioPlayer *player = this; - audioPlayer_.onStreamFail = ^{ + nativePlayer.onStreamFail = ^{ // Called only once the context, which owns this player, is locked alive. reportStreamFailToContext(driverMutexForCallback, weakContext, [player] { return player->isRunning_.load(std::memory_order_acquire) && !player->isRunning(); }); }; + + return nativePlayer; } IOSAudioPlayer::~IOSAudioPlayer() diff --git a/packages/react-native-audio-api/ios/audioapi/ios/core/NativeAudioPlayer.h b/packages/react-native-audio-api/ios/audioapi/ios/core/NativeAudioPlayer.h index e172e3442..8b19ddda9 100644 --- a/packages/react-native-audio-api/ios/audioapi/ios/core/NativeAudioPlayer.h +++ b/packages/react-native-audio-api/ios/audioapi/ios/core/NativeAudioPlayer.h @@ -16,7 +16,8 @@ typedef void (^RenderAudioBlock)(AudioBufferList *outputBuffer, int numFrames); - (instancetype)initWithRenderAudio:(RenderAudioBlock)renderAudio sampleRate:(float)sampleRate - channelCount:(int)channelCount; + channelCount:(int)channelCount + preferredIOBufferFrames:(int)preferredIOBufferFrames; - (bool)start; diff --git a/packages/react-native-audio-api/ios/audioapi/ios/core/NativeAudioPlayer.m b/packages/react-native-audio-api/ios/audioapi/ios/core/NativeAudioPlayer.m index 7593b7738..d119921f7 100644 --- a/packages/react-native-audio-api/ios/audioapi/ios/core/NativeAudioPlayer.m +++ b/packages/react-native-audio-api/ios/audioapi/ios/core/NativeAudioPlayer.m @@ -2,6 +2,11 @@ #import #import +@interface NativeAudioPlayer () { + int _preferredIOBufferFrames; +} +@end + @implementation NativeAudioPlayer - (void)detachSourceNodeIfAttached:(AudioEngine *)audioEngine @@ -36,9 +41,11 @@ - (bool)startPlaybackGraph:(AudioEngine *)audioEngine - (instancetype)initWithRenderAudio:(RenderAudioBlock)renderAudio sampleRate:(float)sampleRate channelCount:(int)channelCount + preferredIOBufferFrames:(int)preferredIOBufferFrames { if (self = [super init]) { self.sampleRate = sampleRate; + _preferredIOBufferFrames = preferredIOBufferFrames; self.channelCount = channelCount; self.renderAudio = [renderAudio copy]; @@ -68,9 +75,13 @@ - (bool)start AudioSessionManager *sessionManager = [AudioSessionManager sharedInstance]; assert(audioEngine != nil); + // AVAudioEngine adopts a new buffer duration only when it starts, so it is requested first. + [sessionManager setPreferredIOBufferFrames:_preferredIOBufferFrames]; + NSError *error = nil; if (![sessionManager ensureActive:false error:&error]) { NSLog(@"Error while activating audio session for playback: %@", [error debugDescription]); + [sessionManager resetPreferredIOBufferFrames]; return false; } @@ -82,11 +93,18 @@ - (bool)start // // Currently we are restarting because we do not see any significant performance issue and case when // you will need to start and stop player very frequently - return [self startPlaybackGraph:audioEngine]; + if (![self startPlaybackGraph:audioEngine]) { + [sessionManager resetPreferredIOBufferFrames]; + return false; + } + + return true; } - (void)stop { + [[AudioSessionManager sharedInstance] resetPreferredIOBufferFrames]; + AudioEngine *audioEngine = [AudioEngine sharedInstance]; if (audioEngine != nil) { [self detachSourceNodeIfAttached:audioEngine]; @@ -96,21 +114,13 @@ - (void)stop - (bool)resume { - AudioEngine *audioEngine = [AudioEngine sharedInstance]; - AudioSessionManager *sessionManager = [AudioSessionManager sharedInstance]; - assert(audioEngine != nil); - - NSError *error = nil; - if (![sessionManager ensureActive:false error:&error]) { - NSLog(@"Error while re-activating audio session for playback: %@", [error debugDescription]); - return false; - } - - return [self startPlaybackGraph:audioEngine]; + return [self start]; } - (void)suspend { + [[AudioSessionManager sharedInstance] resetPreferredIOBufferFrames]; + AudioEngine *audioEngine = [AudioEngine sharedInstance]; assert(audioEngine != nil); diff --git a/packages/react-native-audio-api/ios/audioapi/ios/system/AudioSessionManager.h b/packages/react-native-audio-api/ios/audioapi/ios/system/AudioSessionManager.h index 77af2b55c..8f584e76a 100644 --- a/packages/react-native-audio-api/ios/audioapi/ios/system/AudioSessionManager.h +++ b/packages/react-native-audio-api/ios/audioapi/ios/system/AudioSessionManager.h @@ -39,6 +39,13 @@ /// Switches the manager into external owner mode and stops all session mutations. - (void)disableSessionManagement; +/// Asks the session for an IO buffer of @p frames (in frames of the session's own rate). +/// One buffer duration serves the whole process and nothing is tracked per caller: the latest +/// request simply replaces the previous one. +- (void)setPreferredIOBufferFrames:(int)frames; +/// Asks for one render quantum again, the default of an interactive context. +- (void)resetPreferredIOBufferFrames; + - (NSNumber *)getDevicePreferredSampleRate; - (NSNumber *)getSystemVolume; - (NSString *)inputDiagnosticsSnapshot; diff --git a/packages/react-native-audio-api/ios/audioapi/ios/system/AudioSessionManager.mm b/packages/react-native-audio-api/ios/audioapi/ios/system/AudioSessionManager.mm index f825a06da..fca4ed10d 100644 --- a/packages/react-native-audio-api/ios/audioapi/ios/system/AudioSessionManager.mm +++ b/packages/react-native-audio-api/ios/audioapi/ios/system/AudioSessionManager.mm @@ -2,6 +2,8 @@ #import #import +#include + @interface AudioSessionManager () - (id)microphoneUsageDescriptionValue; @@ -51,6 +53,9 @@ + (instancetype)sharedInstance - (void)cleanup { + // The preference outlives this manager. + [self resetPreferredIOBufferFrames]; + self.audioSession = nil; } @@ -63,6 +68,28 @@ - (bool)areDesiredOptionsSet self.audioSession.allowHapticsAndSystemSoundsDuringRecording == self.allowHapticsAndSounds); } +- (void)setPreferredIOBufferFrames:(int)frames +{ + if (!self.shouldManageSession) { + return; + } + + double duration = frames / self.audioSession.sampleRate; + + NSError *error = nil; + if (![self.audioSession setPreferredIOBufferDuration:duration error:&error]) { + NSLog( + @"[AudioSessionManager] Error while requesting IO buffer duration %f s: %@", + duration, + [error debugDescription]); + } +} + +- (void)resetPreferredIOBufferFrames +{ + [self setPreferredIOBufferFrames:audioapi::RENDER_QUANTUM_SIZE]; +} + - (bool)configureAudioSession:(NSError **)outError { if (!self.shouldManageSession || [self areDesiredOptionsSet]) { diff --git a/packages/react-native-audio-api/src/AudioAPIModule/globals.d.ts b/packages/react-native-audio-api/src/AudioAPIModule/globals.d.ts index a3f23c4d4..7a630effd 100644 --- a/packages/react-native-audio-api/src/AudioAPIModule/globals.d.ts +++ b/packages/react-native-audio-api/src/AudioAPIModule/globals.d.ts @@ -7,11 +7,18 @@ import type { IAudioBuffer, IOfflineAudioContext, } from '../jsi-interfaces'; -import type { AudioRecorderOptions, FileInfo } from '../types'; +import type { + AudioContextLatencyCategory, + AudioRecorderOptions, + FileInfo, +} from '../types'; /* eslint-disable no-var */ declare global { - var createAudioContext: (sampleRate: number) => IAudioContext; + var createAudioContext: ( + sampleRate: number, + latencyHint?: AudioContextLatencyCategory + ) => IAudioContext; var createOfflineAudioContext: ( numberOfChannels: number, length: number, diff --git a/packages/react-native-audio-api/src/core/AudioContext.ts b/packages/react-native-audio-api/src/core/AudioContext.ts index d69973851..5c8718cbe 100644 --- a/packages/react-native-audio-api/src/core/AudioContext.ts +++ b/packages/react-native-audio-api/src/core/AudioContext.ts @@ -22,7 +22,8 @@ export default class AudioContext extends BaseAudioContext { super( globalThis.createAudioContext( - options?.sampleRate || AudioManager.getDevicePreferredSampleRate() + options?.sampleRate || AudioManager.getDevicePreferredSampleRate(), + options?.latencyHint ) ); diff --git a/packages/react-native-audio-api/src/types.ts b/packages/react-native-audio-api/src/types.ts index 56d4c8aab..cce583707 100644 --- a/packages/react-native-audio-api/src/types.ts +++ b/packages/react-native-audio-api/src/types.ts @@ -45,8 +45,19 @@ export type OscillatorType = | 'triangle' | 'custom'; +export type AudioContextLatencyCategory = + | 'balanced' + | 'interactive' + | 'playback'; + export interface AudioContextOptions { sampleRate?: number; + /** + * What the context should optimize its output stream for; defaults to + * `'interactive'`. See the platform table in the docs. Numeric hints are not + * supported yet. + */ + latencyHint?: AudioContextLatencyCategory; } export interface OfflineAudioContextOptions { diff --git a/packages/react-native-audio-api/src/web-core/AudioContext.web.ts b/packages/react-native-audio-api/src/web-core/AudioContext.web.ts index a7745d5b3..370f939a5 100644 --- a/packages/react-native-audio-api/src/web-core/AudioContext.web.ts +++ b/packages/react-native-audio-api/src/web-core/AudioContext.web.ts @@ -36,7 +36,10 @@ export default class AudioContext implements BaseAudioContext { assertSupportedSampleRate(options.sampleRate); } - this.context = new window.AudioContext({ sampleRate: options?.sampleRate }); + this.context = new window.AudioContext({ + sampleRate: options?.sampleRate, + latencyHint: options?.latencyHint, + }); this.sampleRate = this.context.sampleRate; this.destination = new AudioDestinationNode(this, this.context.destination); diff --git a/packages/react-native-audio-api/wpt_tests/src/jsi_install.cpp b/packages/react-native-audio-api/wpt_tests/src/jsi_install.cpp index dccc27672..06c1fe776 100644 --- a/packages/react-native-audio-api/wpt_tests/src/jsi_install.cpp +++ b/packages/react-native-audio-api/wpt_tests/src/jsi_install.cpp @@ -8,6 +8,8 @@ #include #include #include +#include +#include #include #include #include @@ -23,6 +25,7 @@ namespace { using audioapi::AudioBuffer; using audioapi::AudioBufferHostObject; using audioapi::AudioContextHostObject; +using audioapi::AudioContextLatencyHint; using audioapi::AudioEventHandlerRegistry; using audioapi::AudioEventHandlerRegistryHostObject; using audioapi::IAudioEventHandlerRegistry; @@ -201,11 +204,19 @@ void installAudioContextBinding( } const auto sampleRate = static_cast(args[0].getNumber()); + + auto latencyHint = AudioContextLatencyHint::INTERACTIVE; + if (count > 1 && args[1].isString()) { + latencyHint = audioapi::js_enum_parser::latencyHintFromString( + args[1].getString(rt).utf8(rt)); + } + auto hostObject = std::make_shared( sampleRate, eventRegistry, &rt, - callInvoker); + callInvoker, + latencyHint); return makeContextObject(rt, hostObject); });