From 327732b1a33f65881f23ee4feb47108f007efa4f Mon Sep 17 00:00:00 2001 From: Louis Vetter Date: Sun, 2 Aug 2026 14:46:11 +0200 Subject: [PATCH] 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