From 327732b1a33f65881f23ee4feb47108f007efa4f Mon Sep 17 00:00:00 2001 From: Louis Vetter Date: Sun, 2 Aug 2026 14:46:11 +0200 Subject: [PATCH 1/3] feat: AudioManager.getSystemVolume() - synchronous system volume read AudioManager can observe volume changes (observeVolumeChanges + the volumeChange event) but has no way to read the volume BEFORE any change has fired - e.g. an app that wants to warn 'your volume is at the floor' at the moment the user presses play cannot know the current level until the user happens to press a volume button. This adds the synchronous read on the exact scale the volumeChange event already reports: - iOS: AVAudioSession.outputVolume - Android: STREAM_MUSIC volume / STREAM_MUSIC max volume (0..1) exposed as AudioManager.getSystemVolume(): number via the TurboModule spec, mirroring getDevicePreferredSampleRate's blocking-synchronous form. --- .../com/swmansion/audioapi/AudioAPIModule.kt | 2 ++ .../audioapi/system/MediaSessionManager.kt | 8 ++++++++ .../ios/audioapi/ios/AudioAPIModule.mm | 5 +++++ .../audioapi/ios/system/AudioSessionManager.h | 1 + .../audioapi/ios/system/AudioSessionManager.mm | 5 +++++ .../src/specs/NativeAudioAPIModule.ts | 1 + .../src/system/AudioManager.ts | 16 ++++++++++++++++ .../react-native-audio-api/src/system/types.ts | 1 + 8 files changed, 39 insertions(+) diff --git a/packages/react-native-audio-api/android/src/main/java/com/swmansion/audioapi/AudioAPIModule.kt b/packages/react-native-audio-api/android/src/main/java/com/swmansion/audioapi/AudioAPIModule.kt index 301854ce1..5411f005d 100644 --- a/packages/react-native-audio-api/android/src/main/java/com/swmansion/audioapi/AudioAPIModule.kt +++ b/packages/react-native-audio-api/android/src/main/java/com/swmansion/audioapi/AudioAPIModule.kt @@ -147,6 +147,8 @@ class AudioAPIModule( MediaSessionManager.observeVolumeChanges(enabled) } + override fun getSystemVolume(): Double = MediaSessionManager.getSystemVolume() + override fun requestRecordingPermissions(promise: Promise) { val permissionRequestListener = PermissionRequestListener(promise) MediaSessionManager.requestRecordingPermissions(permissionRequestListener) diff --git a/packages/react-native-audio-api/android/src/main/java/com/swmansion/audioapi/system/MediaSessionManager.kt b/packages/react-native-audio-api/android/src/main/java/com/swmansion/audioapi/system/MediaSessionManager.kt index af739a8d0..900ad1a04 100644 --- a/packages/react-native-audio-api/android/src/main/java/com/swmansion/audioapi/system/MediaSessionManager.kt +++ b/packages/react-native-audio-api/android/src/main/java/com/swmansion/audioapi/system/MediaSessionManager.kt @@ -83,6 +83,14 @@ object MediaSessionManager { this.notificationRegistry = NotificationRegistry(this.reactContext, this.audioAPIModule) } + /** The system output volume on the volumeChange event's own scale: + * STREAM_MUSIC volume as a 0..1 fraction of its maximum. */ + fun getSystemVolume(): Double { + val current = audioManager.getStreamVolume(AudioManager.STREAM_MUSIC).toDouble() + val max = audioManager.getStreamMaxVolume(AudioManager.STREAM_MUSIC).toDouble() + return if (max > 0) current / max else 0.0 + } + fun getDevicePreferredSampleRate(): Double { val sampleRate = this.audioManager.getProperty(AudioManager.PROPERTY_OUTPUT_SAMPLE_RATE) return sampleRate.toDouble() diff --git a/packages/react-native-audio-api/ios/audioapi/ios/AudioAPIModule.mm b/packages/react-native-audio-api/ios/audioapi/ios/AudioAPIModule.mm index 12206d697..2365ca1a8 100644 --- a/packages/react-native-audio-api/ios/audioapi/ios/AudioAPIModule.mm +++ b/packages/react-native-audio-api/ios/audioapi/ios/AudioAPIModule.mm @@ -92,6 +92,11 @@ - (dispatch_queue_t)methodQueue return [self.audioSessionManager getDevicePreferredSampleRate]; } +RCT_EXPORT_BLOCKING_SYNCHRONOUS_METHOD(getSystemVolume) +{ + return [self.audioSessionManager getSystemVolume]; +} + RCT_EXPORT_BLOCKING_SYNCHRONOUS_METHOD(isFfmpegEnabled) { #if RN_AUDIO_API_FFMPEG_DISABLED 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 472fcce70..77af2b55c 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 @@ -40,6 +40,7 @@ - (void)disableSessionManagement; - (NSNumber *)getDevicePreferredSampleRate; +- (NSNumber *)getSystemVolume; - (NSString *)inputDiagnosticsSnapshot; - (void)requestRecordingPermissions:(RCTPromiseResolveBlock)resolve 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 31262c90e..f825a06da 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 @@ -214,6 +214,11 @@ - (NSNumber *)getDevicePreferredSampleRate return [NSNumber numberWithFloat:[self.audioSession sampleRate]]; } +- (NSNumber *)getSystemVolume +{ + return [NSNumber numberWithFloat:[self.audioSession outputVolume]]; +} + - (NSString *)inputDiagnosticsSnapshot { AVAudioSessionRouteDescription *route = [self.audioSession currentRoute]; diff --git a/packages/react-native-audio-api/src/specs/NativeAudioAPIModule.ts b/packages/react-native-audio-api/src/specs/NativeAudioAPIModule.ts index 7ca36e290..2085f8dee 100644 --- a/packages/react-native-audio-api/src/specs/NativeAudioAPIModule.ts +++ b/packages/react-native-audio-api/src/specs/NativeAudioAPIModule.ts @@ -32,6 +32,7 @@ interface Spec extends TurboModule { observeAudioInterruptions(focusType: AudioFocusType, enabled: boolean): void; activelyReclaimSession(enabled: boolean): void; observeVolumeChanges(enabled: boolean): void; + getSystemVolume(): number; // Permissions requestRecordingPermissions(): Promise; diff --git a/packages/react-native-audio-api/src/system/AudioManager.ts b/packages/react-native-audio-api/src/system/AudioManager.ts index 00a9d8cc6..6aef1ceba 100644 --- a/packages/react-native-audio-api/src/system/AudioManager.ts +++ b/packages/react-native-audio-api/src/system/AudioManager.ts @@ -81,6 +81,22 @@ class AudioManager implements IAudioManager { NativeAudioAPIModule.observeVolumeChanges(enabled); } + /** + * Synchronously reads the current system output volume as a 0..1 value, + * on the same scale the `volumeChange` event reports: AVAudioSession's + * outputVolume on iOS, the STREAM_MUSIC volume as a fraction of its + * maximum on Android. Complements `observeVolumeChanges`, which only + * reports on changes: this answers "what is it right now" before any + * change has fired. + * + * iOS note: a cold read before any volume-change event can report a stale + * or zero value on some iOS versions; treat the event stream as the truth + * once it speaks. + */ + getSystemVolume(): number { + return NativeAudioAPIModule.getSystemVolume(); + } + addSystemEventListener( name: Name, callback: SystemEventCallback diff --git a/packages/react-native-audio-api/src/system/types.ts b/packages/react-native-audio-api/src/system/types.ts index 04aa4128b..d010ed95e 100644 --- a/packages/react-native-audio-api/src/system/types.ts +++ b/packages/react-native-audio-api/src/system/types.ts @@ -80,6 +80,7 @@ export interface IAudioManager { observeAudioInterruptions(enabled: boolean): void; activelyReclaimSession(enabled: boolean): void; observeAudioInterruptions(param: AudioFocusType | boolean | null): void; + getSystemVolume(): number; addSystemEventListener( name: Name, callback: SystemEventCallback From bd8e810cdeed6521eeac71ba144fc27e4a3e901d Mon Sep 17 00:00:00 2001 From: michal Date: Tue, 11 Aug 2026 15:29:10 +0200 Subject: [PATCH 2/3] feat: trim down comment --- .../src/system/AudioManager.ts | 14 +++++--------- .../react-native-audio-api/src/system/types.ts | 1 + .../src/web-system/AudioManager.ts | 1 + 3 files changed, 7 insertions(+), 9 deletions(-) diff --git a/packages/react-native-audio-api/src/system/AudioManager.ts b/packages/react-native-audio-api/src/system/AudioManager.ts index 6aef1ceba..ace424da3 100644 --- a/packages/react-native-audio-api/src/system/AudioManager.ts +++ b/packages/react-native-audio-api/src/system/AudioManager.ts @@ -82,16 +82,12 @@ class AudioManager implements IAudioManager { } /** - * Synchronously reads the current system output volume as a 0..1 value, - * on the same scale the `volumeChange` event reports: AVAudioSession's - * outputVolume on iOS, the STREAM_MUSIC volume as a fraction of its - * maximum on Android. Complements `observeVolumeChanges`, which only - * reports on changes: this answers "what is it right now" before any - * change has fired. + * Synchronously reads the current system output volume as a 0..1 value, on + * the same scale the `volumeChange` event reports * - * iOS note: a cold read before any volume-change event can report a stale - * or zero value on some iOS versions; treat the event stream as the truth - * once it speaks. + * On iOS, a cold read before any volume-change event can report a stale or + * zero value on some OS versions; treat the event stream as the truth once it + * speaks. */ getSystemVolume(): number { return NativeAudioAPIModule.getSystemVolume(); diff --git a/packages/react-native-audio-api/src/system/types.ts b/packages/react-native-audio-api/src/system/types.ts index d010ed95e..b80424fe2 100644 --- a/packages/react-native-audio-api/src/system/types.ts +++ b/packages/react-native-audio-api/src/system/types.ts @@ -91,4 +91,5 @@ export interface IAudioManager { checkNotificationPermissions(): Promise; getDevicesInfo(): Promise; setInputDevice(deviceId: string): Promise; + getSystemVolume(): number; } diff --git a/packages/react-native-audio-api/src/web-system/AudioManager.ts b/packages/react-native-audio-api/src/web-system/AudioManager.ts index e1fdc0ece..f6cbd6a4c 100644 --- a/packages/react-native-audio-api/src/web-system/AudioManager.ts +++ b/packages/react-native-audio-api/src/web-system/AudioManager.ts @@ -11,6 +11,7 @@ const mockSync = class AudioManager implements IAudioManager { getDevicePreferredSampleRate = mockSync(44100); + getSystemVolume = mockSync(1); setAudioSessionActivity = mockAsync(undefined); setAudioSessionOptions = mockSync({}); disableSessionManagement = mockSync({}); From d2e533b99cdb308bd0f6051ae493f74c5ef1e3d2 Mon Sep 17 00:00:00 2001 From: michal Date: Wed, 19 Aug 2026 16:00:16 +0200 Subject: [PATCH 3/3] fix: comments --- packages/audiodocs/docs/system/audio-manager.mdx | 6 ++++++ packages/react-native-audio-api/src/system/types.ts | 1 - 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/packages/audiodocs/docs/system/audio-manager.mdx b/packages/audiodocs/docs/system/audio-manager.mdx index e044b6a97..368d21a96 100644 --- a/packages/audiodocs/docs/system/audio-manager.mdx +++ b/packages/audiodocs/docs/system/audio-manager.mdx @@ -110,6 +110,12 @@ and tries to reactivate the audio session as soon as there is "silence", althoug Internally, the method uses `AVAudioSessionSilenceSecondaryAudioHintNotification` as well as interval polling to check if other audio is playing. +### `getSystemVolume` + +Reads the current system output volume as a 0..1 fraction of its maximum. + +#### Returns `number`. + ### `observeVolumeChanges` | Parameter | Type | Description | diff --git a/packages/react-native-audio-api/src/system/types.ts b/packages/react-native-audio-api/src/system/types.ts index b80424fe2..d010ed95e 100644 --- a/packages/react-native-audio-api/src/system/types.ts +++ b/packages/react-native-audio-api/src/system/types.ts @@ -91,5 +91,4 @@ export interface IAudioManager { checkNotificationPermissions(): Promise; getDevicesInfo(): Promise; setInputDevice(deviceId: string): Promise; - getSystemVolume(): number; }