From e17ddc7aae2ef9c6d203864c3eea971d6e8553ee Mon Sep 17 00:00:00 2001 From: Maciej Makowski <120780663+maciejmakowski2003@users.noreply.github.com> Date: Mon, 2 Jun 2025 20:54:07 +0200 Subject: [PATCH 01/33] chore: released 0.6.1 (#485) Co-authored-by: Maciej Makowski --- apps/fabric-example/ios/Podfile.lock | 12 ++++++------ packages/react-native-audio-api/package.json | 2 +- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/apps/fabric-example/ios/Podfile.lock b/apps/fabric-example/ios/Podfile.lock index bf749a7a..c41ff4eb 100644 --- a/apps/fabric-example/ios/Podfile.lock +++ b/apps/fabric-example/ios/Podfile.lock @@ -1609,7 +1609,7 @@ PODS: - React-logger (= 0.77.1) - React-perflogger (= 0.77.1) - React-utils (= 0.77.1) - - RNAudioAPI (0.6.0): + - RNAudioAPI (0.6.1): - DoubleConversion - glog - hermes-engine @@ -1629,9 +1629,9 @@ PODS: - ReactCodegen - ReactCommon/turbomodule/bridging - ReactCommon/turbomodule/core - - RNAudioAPI/audioapi (= 0.6.0) + - RNAudioAPI/audioapi (= 0.6.1) - Yoga - - RNAudioAPI/audioapi (0.6.0): + - RNAudioAPI/audioapi (0.6.1): - DoubleConversion - glog - hermes-engine @@ -1651,9 +1651,9 @@ PODS: - ReactCodegen - ReactCommon/turbomodule/bridging - ReactCommon/turbomodule/core - - RNAudioAPI/audioapi/ios (= 0.6.0) + - RNAudioAPI/audioapi/ios (= 0.6.1) - Yoga - - RNAudioAPI/audioapi/ios (0.6.0): + - RNAudioAPI/audioapi/ios (0.6.1): - DoubleConversion - glog - hermes-engine @@ -2161,7 +2161,7 @@ SPEC CHECKSUMS: ReactAppDependencyProvider: 41e9fb63606c32cce924653d2d410cb01ec81286 ReactCodegen: 8cae67dac8144aa8d9d5bee12e55ead0fb73537f ReactCommon: 08f4808f02ff115884e870e5cfea689703ff759a - RNAudioAPI: a360b6e9626f23e8960baf015b3264dacaf28b1b + RNAudioAPI: 02f1de634ec4ab0cc8283cffedfe91a22da25127 RNGestureHandler: 8b1080a6db0be82dbca18550d6212b885bfab6b2 RNReanimated: afed763d6c0b1916bfef1dbde4271ec0947dd376 RNScreens: 0d4cb9afe052607ad0aa71f645a88bb7c7f2e64c diff --git a/packages/react-native-audio-api/package.json b/packages/react-native-audio-api/package.json index c622e3fe..dc6ff7ed 100644 --- a/packages/react-native-audio-api/package.json +++ b/packages/react-native-audio-api/package.json @@ -1,6 +1,6 @@ { "name": "react-native-audio-api", - "version": "0.6.0", + "version": "0.6.1", "description": "react-native-audio-api provides system for controlling audio in React Native environment compatible with Web Audio API specification", "bin": { "setup-rn-audio-api-web": "./scripts/setup-rn-audio-api-web.js" From 1f57794487d6aefaa058908b9549c0ab0f40e7c8 Mon Sep 17 00:00:00 2001 From: Viktor Shen Date: Wed, 4 Jun 2025 03:03:00 +0200 Subject: [PATCH 02/33] feat: add haptics support for iOS audio sessions --- .../audiodocs/docs/system/audio-manager.mdx | 173 +++++++++--------- .../audioapi/ios/system/AudioSessionManager.h | 1 + .../ios/system/AudioSessionManager.mm | 24 ++- .../src/system/types.ts | 3 +- 4 files changed, 111 insertions(+), 90 deletions(-) diff --git a/packages/audiodocs/docs/system/audio-manager.mdx b/packages/audiodocs/docs/system/audio-manager.mdx index 7a277534..f6dcdcba 100644 --- a/packages/audiodocs/docs/system/audio-manager.mdx +++ b/packages/audiodocs/docs/system/audio-manager.mdx @@ -6,7 +6,7 @@ import { Optional, ReadOnly, OnlyiOS } from '@site/src/components/Badges'; # AudioManager -The `AudioManager` is a layer of an abstraction between user and a system. +The `AudioManager` is a layer of an abstraction between user and a system. It provides a set of system-specific functions that are invoked directly in native code, by related system. ## Example @@ -16,23 +16,22 @@ import { AudioManager } from 'react-native-audio-api'; import { useEffect } from 'react'; function App() { - // set AVAudioSession example options (iOS only) - AudioManager.setAudioSessionOptions({ - iosCategory: 'playback', - iosMode: 'default', - iosOptions: ['allowBluetooth', 'allowAirPlay'], - }) - - // set info for track to be visible while device is locked - AudioManager.setLockScreenInfo({ - title: 'Audio file', - artist: 'Software Mansion', - album: 'Audio API', - duration: 10, - }); + // set AVAudioSession example options (iOS only) + AudioManager.setAudioSessionOptions({ + iosCategory: 'playback', + iosMode: 'default', + iosOptions: ['allowBluetooth', 'allowAirPlay'], + }); + + // set info for track to be visible while device is locked + AudioManager.setLockScreenInfo({ + title: 'Audio file', + artist: 'Software Mansion', + album: 'Audio API', + duration: 10, + }); useEffect(() => { - // enabling emission of events AudioManager.enableRemoteCommand('remotePlay', true); AudioManager.enableRemoteCommand('remotePause', true); @@ -62,7 +61,6 @@ function App() { } ); - return () => { remotePlaySubscription?.remove(); remotePauseSubscription?.remove(); @@ -76,9 +74,9 @@ function App() { ### `setLockScreenInfo` -| Parameters | Type | Description | -| :---: | :---: | :-----: | -| `info` | [`LockScreenInfo`](/system/audio-manager#lockscreeninfo) | Information to be displayed on the lock screen | +| Parameters | Type | Description | +| :--------: | :------------------------------------------------------: | :--------------------------------------------: | +| `info` | [`LockScreenInfo`](/system/audio-manager#lockscreeninfo) | Information to be displayed on the lock screen | #### Returns `undefined` @@ -90,17 +88,17 @@ Resets all of the lock screen data. ### `setAudioSessionOptions` -| Parameters | Type | Description | -| :---: | :---: | :---- | -| options | [`SessionOptions`](/system/audio-manager#sessionoptions) | Options to be set for AVAudioSession | +| Parameters | Type | Description | +| :--------: | :------------------------------------------------------: | :----------------------------------- | +| options | [`SessionOptions`](/system/audio-manager#sessionoptions) | Options to be set for AVAudioSession | #### Returns `undefined` ### `setAudioSessionActivity` -| Parameters | Type | Description | -| :---: | :---: | :---- | -| enabled | `boolean` | It is used to set/unset AVAudioSession activity | +| Parameters | Type | Description | +| :--------: | :-------: | :---------------------------------------------- | +| enabled | `boolean` | It is used to set/unset AVAudioSession activity | #### Returns promise of `boolean` type, which is resolved to `true` if invokation ended with success, `false` otherwise. @@ -110,17 +108,17 @@ Resets all of the lock screen data. ### `observeAudioInterruptions` -| Parameters | Type | Description | -| :---: | :---: | :---- | -| `enabled` | `boolean` | It is used to enable/disable observing audio interruptions | +| Parameters | Type | Description | +| :--------: | :-------: | :--------------------------------------------------------- | +| `enabled` | `boolean` | It is used to enable/disable observing audio interruptions | #### Returns `undefined` ### `observeVolumeChanges` -| Parameters | Type | Description | -| :---: | :---: | :---- | -| `enabled` | `boolean` | It is used to enable/disable observing volume changes | +| Parameters | Type | Description | +| :--------: | :-------: | :---------------------------------------------------- | +| `enabled` | `boolean` | It is used to enable/disable observing volume changes | #### Returns `undefined` @@ -128,10 +126,10 @@ Resets all of the lock screen data. Enables emition of some system events. -| Parameters | Type | Description | -| :---: | :---: | :---- | -| `name` | [`RemoteCommandEventName`](/system/audio-manager#systemeventname--remotecommandeventname) | Name of an event | -| `enabled` | `boolean` | Indicates the start or the end of event emission | +| Parameters | Type | Description | +| :--------: | :---------------------------------------------------------------------------------------: | :----------------------------------------------- | +| `name` | [`RemoteCommandEventName`](/system/audio-manager#systemeventname--remotecommandeventname) | Name of an event | +| `enabled` | `boolean` | Indicates the start or the end of event emission | #### Returns `undefined` @@ -142,14 +140,13 @@ with proper parameters. ::: - ### `addSystemEventListener` Adds callback to be invoked upon hearing an event. -| Parameters | Type | Description | -| :---: | :---: | :---- | -| `name` | [`SystemEventName`](/system/audio-manager#systemeventname--remotecommandeventname) | Name of an event listener | +| Parameters | Type | Description | +| :--------: | :------------------------------------------------------------------------------------: | :-------------------------------------------------- | +| `name` | [`SystemEventName`](/system/audio-manager#systemeventname--remotecommandeventname) | Name of an event listener | | `callback` | [`SystemEventCallback`](/system/audio-manager#systemeventname--remotecommandeventname) | Callback that will be invoked upon hearing an event | #### Returns [`AudioEventSubscription`](/system/audio-manager#audioeventsubscription) if `enabled` is set to true, `undefined` otherwise @@ -182,20 +179,21 @@ interface BaseLockScreenInfo { type MediaState = 'state_playing' | 'state_paused'; interface LockScreenInfo extends BaseLockScreenInfo { - title?: string; //title of the track - artwork?: string; //uri to the artwork - artist?: string; //name of the artist - album?: string; //name of the album - duration?: number; //duration in seconds - description?: string; // android only, description of the track - state?: MediaState; - speed?: number; //playback rate - elapsedTime?: number; //elapsed time of an audio in seconds +title?: string; //title of the track +artwork?: string; //uri to the artwork +artist?: string; //name of the artist +album?: string; //name of the album +duration?: number; //duration in seconds +description?: string; // android only, description of the track +state?: MediaState; +speed?: number; //playback rate +elapsedTime?: number; //elapsed time of an audio in seconds } -``` + +```` -### `SessionOptions` +### `SessionOptions`
Type definitions @@ -234,9 +232,9 @@ interface SessionOptions { iosOptions?: IOSOption[]; iosCategory?: IOSCategory; } -``` -
+```` + ### `SystemEventName` | `RemoteCommandEventName` @@ -246,55 +244,56 @@ interface SessionOptions { interface EventEmptyType {} interface EventTypeWithValue { - value: number; +value: number; } interface OnInterruptionEventType { - type: 'ended' | 'began'; //if interruption event has started or ended - shouldResume: boolean; //if we should resume playing after interruption +type: 'ended' | 'began'; //if interruption event has started or ended +shouldResume: boolean; //if we should resume playing after interruption } interface OnRouteChangeEventType { - reason: - | 'Unknown' - | 'Override' - | 'CategoryChange' - | 'WakeFromSleep' - | 'NewDeviceAvailable' - | 'OldDeviceUnavailable' - | 'ConfigurationChange' - | 'NoSuitableRouteForCategory'; +reason: +| 'Unknown' +| 'Override' +| 'CategoryChange' +| 'WakeFromSleep' +| 'NewDeviceAvailable' +| 'OldDeviceUnavailable' +| 'ConfigurationChange' +| 'NoSuitableRouteForCategory'; } // visit https://developer.apple.com/documentation/mediaplayer/mpremotecommandcenter?language=objc // for further info interface RemoteCommandEvents { - remotePlay: EventEmptyType; - remotePause: EventEmptyType; - remoteStop: EventEmptyType; - remoteTogglePlayPause: EventEmptyType; // iOS only - remoteChangePlaybackRate: EventTypeWithValue; - remoteNextTrack: EventEmptyType; - remotePreviousTrack: EventEmptyType; - remoteSkipForward: EventTypeWithValue; - remoteSkipBackward: EventTypeWithValue; // iOS only - remoteSeekForward: EventEmptyType; // iOS only - remoteSeekBackward: EventEmptyType; - remoteChangePlaybackPosition: EventTypeWithValue; +remotePlay: EventEmptyType; +remotePause: EventEmptyType; +remoteStop: EventEmptyType; +remoteTogglePlayPause: EventEmptyType; // iOS only +remoteChangePlaybackRate: EventTypeWithValue; +remoteNextTrack: EventEmptyType; +remotePreviousTrack: EventEmptyType; +remoteSkipForward: EventTypeWithValue; +remoteSkipBackward: EventTypeWithValue; // iOS only +remoteSeekForward: EventEmptyType; // iOS only +remoteSeekBackward: EventEmptyType; +remoteChangePlaybackPosition: EventTypeWithValue; } type SystemEvents = RemoteCommandEvents & { - volumeChange: EventTypeWithValue; //triggered when volume level is changed - interruption: OnInterruptionEventType; //triggered when f.e. some app wants to play music when we are playing - routeChange: OnRouteChangeEventType; //change of output f.e. from speaker to headphones, events are always emitted! +volumeChange: EventTypeWithValue; //triggered when volume level is changed +interruption: OnInterruptionEventType; //triggered when f.e. some app wants to play music when we are playing +routeChange: OnRouteChangeEventType; //change of output f.e. from speaker to headphones, events are always emitted! }; type RemoteCommandEventName = keyof RemoteCommandEvents; type SystemEventName = keyof SystemEvents; type SystemEventCallback = ( - event: SystemEvents[Name] +event: SystemEvents[Name] ) => void; -``` + +```` @@ -326,14 +325,14 @@ class AudioEventSubscription { ); } } -``` +```` + ### `PermissionStatus`
-Type definitions -```typescript -type PermissionStatus = 'Undetermined' | 'Denied' | 'Granted'; -``` + Type definitions + ```typescript type PermissionStatus = 'Undetermined' | 'Denied' | 'Granted'; + ```
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 e2ac623e..17754fb8 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 @@ -13,6 +13,7 @@ @property (nonatomic, assign) AVAudioSessionMode sessionMode; @property (nonatomic, assign) AVAudioSessionCategory sessionCategory; @property (nonatomic, assign) AVAudioSessionCategoryOptions sessionOptions; +@property (nonatomic, assign) bool allowHapticsAndSystemSoundsDuringRecording; - (instancetype)init; - (void)cleanup; 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 43878728..3ebb86e5 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 @@ -10,6 +10,7 @@ - (instancetype)init self.sessionCategory = AVAudioSessionCategoryPlayback; self.sessionMode = AVAudioSessionModeDefault; self.sessionOptions = 0; + self.allowHapticsAndSystemSoundsDuringRecording = false; self.hasDirtySettings = true; self.isActive = false; } @@ -34,6 +35,7 @@ - (void)setAudioSessionOptions:(NSString *)category mode:(NSString *)mode option AVAudioSessionCategory sessionCategory = self.sessionCategory; AVAudioSessionMode sessionMode = self.sessionMode; AVAudioSessionCategoryOptions sessionOptions = 0; + bool allowHapticsAndSystemSoundsDuringRecording = false; if ([category isEqualToString:@"record"]) { sessionCategory = AVAudioSessionCategoryRecord; @@ -101,6 +103,10 @@ - (void)setAudioSessionOptions:(NSString *)category mode:(NSString *)mode option if ([option isEqualToString:@"interruptSpokenAudioAndMixWithOthers"]) { sessionOptions |= AVAudioSessionCategoryOptionInterruptSpokenAudioAndMixWithOthers; } + + if ([option isEqualToString:@"allowHapticsAndSystemSoundsDuringRecording"]) { + allowHapticsAndSystemSoundsDuringRecording = true; + } } if (self.sessionCategory != sessionCategory) { @@ -118,6 +124,11 @@ - (void)setAudioSessionOptions:(NSString *)category mode:(NSString *)mode option self.sessionOptions = sessionOptions; } + if (self.allowHapticsAndSystemSoundsDuringRecording != allowHapticsAndSystemSoundsDuringRecording) { + self.hasDirtySettings = true; + self.allowHapticsAndSystemSoundsDuringRecording = allowHapticsAndSystemSoundsDuringRecording; + } + if (self.isActive) { [self configureAudioSession]; } @@ -157,10 +168,11 @@ - (bool)configureAudioSession } NSLog( - @"[AudioSessionManager] configureAudioSession, category: %@, mode: %@, options: %lu", + @"[AudioSessionManager] configureAudioSession, category: %@, mode: %@, options: %lu, allowHaptics: %@", self.sessionCategory, self.sessionMode, - (unsigned long)self.sessionOptions); + (unsigned long)self.sessionOptions, + self.allowHapticsAndSystemSoundsDuringRecording ? @"YES" : @"NO"); NSError *error = nil; @@ -171,6 +183,14 @@ - (bool)configureAudioSession return false; } + if (@available(iOS 13.0, *)) { + [self.audioSession setAllowHapticsAndSystemSoundsDuringRecording:self.allowHapticsAndSystemSoundsDuringRecording error:&error]; + + if (error != nil) { + NSLog(@"Error while setting allowHapticsAndSystemSoundsDuringRecording: %@", [error debugDescription]); + } + } + self.hasDirtySettings = false; return true; } diff --git a/packages/react-native-audio-api/src/system/types.ts b/packages/react-native-audio-api/src/system/types.ts index 0b681a38..a134fcfa 100644 --- a/packages/react-native-audio-api/src/system/types.ts +++ b/packages/react-native-audio-api/src/system/types.ts @@ -25,7 +25,8 @@ export type IOSOption = | 'defaultToSpeaker' | 'allowBluetoothA2DP' | 'overrideMutedMicrophoneInterruption' - | 'interruptSpokenAudioAndMixWithOthers'; + | 'interruptSpokenAudioAndMixWithOthers' + | 'allowHapticsAndSystemSoundsDuringRecording'; export interface SessionOptions { iosMode?: IOSMode; From 897b6fb571d11b1818ce212954ec7f8a9ff391d9 Mon Sep 17 00:00:00 2001 From: Viktor Shen Date: Wed, 4 Jun 2025 03:34:05 +0200 Subject: [PATCH 03/33] revert changes to audio-manager.mdx --- .../audiodocs/docs/system/audio-manager.mdx | 173 +++++++++--------- 1 file changed, 87 insertions(+), 86 deletions(-) diff --git a/packages/audiodocs/docs/system/audio-manager.mdx b/packages/audiodocs/docs/system/audio-manager.mdx index f6dcdcba..7a277534 100644 --- a/packages/audiodocs/docs/system/audio-manager.mdx +++ b/packages/audiodocs/docs/system/audio-manager.mdx @@ -6,7 +6,7 @@ import { Optional, ReadOnly, OnlyiOS } from '@site/src/components/Badges'; # AudioManager -The `AudioManager` is a layer of an abstraction between user and a system. +The `AudioManager` is a layer of an abstraction between user and a system. It provides a set of system-specific functions that are invoked directly in native code, by related system. ## Example @@ -16,22 +16,23 @@ import { AudioManager } from 'react-native-audio-api'; import { useEffect } from 'react'; function App() { - // set AVAudioSession example options (iOS only) - AudioManager.setAudioSessionOptions({ - iosCategory: 'playback', - iosMode: 'default', - iosOptions: ['allowBluetooth', 'allowAirPlay'], - }); - - // set info for track to be visible while device is locked - AudioManager.setLockScreenInfo({ - title: 'Audio file', - artist: 'Software Mansion', - album: 'Audio API', - duration: 10, - }); + // set AVAudioSession example options (iOS only) + AudioManager.setAudioSessionOptions({ + iosCategory: 'playback', + iosMode: 'default', + iosOptions: ['allowBluetooth', 'allowAirPlay'], + }) + + // set info for track to be visible while device is locked + AudioManager.setLockScreenInfo({ + title: 'Audio file', + artist: 'Software Mansion', + album: 'Audio API', + duration: 10, + }); useEffect(() => { + // enabling emission of events AudioManager.enableRemoteCommand('remotePlay', true); AudioManager.enableRemoteCommand('remotePause', true); @@ -61,6 +62,7 @@ function App() { } ); + return () => { remotePlaySubscription?.remove(); remotePauseSubscription?.remove(); @@ -74,9 +76,9 @@ function App() { ### `setLockScreenInfo` -| Parameters | Type | Description | -| :--------: | :------------------------------------------------------: | :--------------------------------------------: | -| `info` | [`LockScreenInfo`](/system/audio-manager#lockscreeninfo) | Information to be displayed on the lock screen | +| Parameters | Type | Description | +| :---: | :---: | :-----: | +| `info` | [`LockScreenInfo`](/system/audio-manager#lockscreeninfo) | Information to be displayed on the lock screen | #### Returns `undefined` @@ -88,17 +90,17 @@ Resets all of the lock screen data. ### `setAudioSessionOptions` -| Parameters | Type | Description | -| :--------: | :------------------------------------------------------: | :----------------------------------- | -| options | [`SessionOptions`](/system/audio-manager#sessionoptions) | Options to be set for AVAudioSession | +| Parameters | Type | Description | +| :---: | :---: | :---- | +| options | [`SessionOptions`](/system/audio-manager#sessionoptions) | Options to be set for AVAudioSession | #### Returns `undefined` ### `setAudioSessionActivity` -| Parameters | Type | Description | -| :--------: | :-------: | :---------------------------------------------- | -| enabled | `boolean` | It is used to set/unset AVAudioSession activity | +| Parameters | Type | Description | +| :---: | :---: | :---- | +| enabled | `boolean` | It is used to set/unset AVAudioSession activity | #### Returns promise of `boolean` type, which is resolved to `true` if invokation ended with success, `false` otherwise. @@ -108,17 +110,17 @@ Resets all of the lock screen data. ### `observeAudioInterruptions` -| Parameters | Type | Description | -| :--------: | :-------: | :--------------------------------------------------------- | -| `enabled` | `boolean` | It is used to enable/disable observing audio interruptions | +| Parameters | Type | Description | +| :---: | :---: | :---- | +| `enabled` | `boolean` | It is used to enable/disable observing audio interruptions | #### Returns `undefined` ### `observeVolumeChanges` -| Parameters | Type | Description | -| :--------: | :-------: | :---------------------------------------------------- | -| `enabled` | `boolean` | It is used to enable/disable observing volume changes | +| Parameters | Type | Description | +| :---: | :---: | :---- | +| `enabled` | `boolean` | It is used to enable/disable observing volume changes | #### Returns `undefined` @@ -126,10 +128,10 @@ Resets all of the lock screen data. Enables emition of some system events. -| Parameters | Type | Description | -| :--------: | :---------------------------------------------------------------------------------------: | :----------------------------------------------- | -| `name` | [`RemoteCommandEventName`](/system/audio-manager#systemeventname--remotecommandeventname) | Name of an event | -| `enabled` | `boolean` | Indicates the start or the end of event emission | +| Parameters | Type | Description | +| :---: | :---: | :---- | +| `name` | [`RemoteCommandEventName`](/system/audio-manager#systemeventname--remotecommandeventname) | Name of an event | +| `enabled` | `boolean` | Indicates the start or the end of event emission | #### Returns `undefined` @@ -140,13 +142,14 @@ with proper parameters. ::: + ### `addSystemEventListener` Adds callback to be invoked upon hearing an event. -| Parameters | Type | Description | -| :--------: | :------------------------------------------------------------------------------------: | :-------------------------------------------------- | -| `name` | [`SystemEventName`](/system/audio-manager#systemeventname--remotecommandeventname) | Name of an event listener | +| Parameters | Type | Description | +| :---: | :---: | :---- | +| `name` | [`SystemEventName`](/system/audio-manager#systemeventname--remotecommandeventname) | Name of an event listener | | `callback` | [`SystemEventCallback`](/system/audio-manager#systemeventname--remotecommandeventname) | Callback that will be invoked upon hearing an event | #### Returns [`AudioEventSubscription`](/system/audio-manager#audioeventsubscription) if `enabled` is set to true, `undefined` otherwise @@ -179,21 +182,20 @@ interface BaseLockScreenInfo { type MediaState = 'state_playing' | 'state_paused'; interface LockScreenInfo extends BaseLockScreenInfo { -title?: string; //title of the track -artwork?: string; //uri to the artwork -artist?: string; //name of the artist -album?: string; //name of the album -duration?: number; //duration in seconds -description?: string; // android only, description of the track -state?: MediaState; -speed?: number; //playback rate -elapsedTime?: number; //elapsed time of an audio in seconds + title?: string; //title of the track + artwork?: string; //uri to the artwork + artist?: string; //name of the artist + album?: string; //name of the album + duration?: number; //duration in seconds + description?: string; // android only, description of the track + state?: MediaState; + speed?: number; //playback rate + elapsedTime?: number; //elapsed time of an audio in seconds } - -```` +``` -### `SessionOptions` +### `SessionOptions`
Type definitions @@ -232,10 +234,10 @@ interface SessionOptions { iosOptions?: IOSOption[]; iosCategory?: IOSCategory; } -```` - +```
+ ### `SystemEventName` | `RemoteCommandEventName`
@@ -244,56 +246,55 @@ interface SessionOptions { interface EventEmptyType {} interface EventTypeWithValue { -value: number; + value: number; } interface OnInterruptionEventType { -type: 'ended' | 'began'; //if interruption event has started or ended -shouldResume: boolean; //if we should resume playing after interruption + type: 'ended' | 'began'; //if interruption event has started or ended + shouldResume: boolean; //if we should resume playing after interruption } interface OnRouteChangeEventType { -reason: -| 'Unknown' -| 'Override' -| 'CategoryChange' -| 'WakeFromSleep' -| 'NewDeviceAvailable' -| 'OldDeviceUnavailable' -| 'ConfigurationChange' -| 'NoSuitableRouteForCategory'; + reason: + | 'Unknown' + | 'Override' + | 'CategoryChange' + | 'WakeFromSleep' + | 'NewDeviceAvailable' + | 'OldDeviceUnavailable' + | 'ConfigurationChange' + | 'NoSuitableRouteForCategory'; } // visit https://developer.apple.com/documentation/mediaplayer/mpremotecommandcenter?language=objc // for further info interface RemoteCommandEvents { -remotePlay: EventEmptyType; -remotePause: EventEmptyType; -remoteStop: EventEmptyType; -remoteTogglePlayPause: EventEmptyType; // iOS only -remoteChangePlaybackRate: EventTypeWithValue; -remoteNextTrack: EventEmptyType; -remotePreviousTrack: EventEmptyType; -remoteSkipForward: EventTypeWithValue; -remoteSkipBackward: EventTypeWithValue; // iOS only -remoteSeekForward: EventEmptyType; // iOS only -remoteSeekBackward: EventEmptyType; -remoteChangePlaybackPosition: EventTypeWithValue; + remotePlay: EventEmptyType; + remotePause: EventEmptyType; + remoteStop: EventEmptyType; + remoteTogglePlayPause: EventEmptyType; // iOS only + remoteChangePlaybackRate: EventTypeWithValue; + remoteNextTrack: EventEmptyType; + remotePreviousTrack: EventEmptyType; + remoteSkipForward: EventTypeWithValue; + remoteSkipBackward: EventTypeWithValue; // iOS only + remoteSeekForward: EventEmptyType; // iOS only + remoteSeekBackward: EventEmptyType; + remoteChangePlaybackPosition: EventTypeWithValue; } type SystemEvents = RemoteCommandEvents & { -volumeChange: EventTypeWithValue; //triggered when volume level is changed -interruption: OnInterruptionEventType; //triggered when f.e. some app wants to play music when we are playing -routeChange: OnRouteChangeEventType; //change of output f.e. from speaker to headphones, events are always emitted! + volumeChange: EventTypeWithValue; //triggered when volume level is changed + interruption: OnInterruptionEventType; //triggered when f.e. some app wants to play music when we are playing + routeChange: OnRouteChangeEventType; //change of output f.e. from speaker to headphones, events are always emitted! }; type RemoteCommandEventName = keyof RemoteCommandEvents; type SystemEventName = keyof SystemEvents; type SystemEventCallback = ( -event: SystemEvents[Name] + event: SystemEvents[Name] ) => void; - -```` +```
@@ -325,14 +326,14 @@ class AudioEventSubscription { ); } } -```` - +``` ### `PermissionStatus`
- Type definitions - ```typescript type PermissionStatus = 'Undetermined' | 'Denied' | 'Granted'; - ``` +Type definitions +```typescript +type PermissionStatus = 'Undetermined' | 'Denied' | 'Granted'; +```
From e0c8babb4200882a7850c6d28c1fd98fc2f05433 Mon Sep 17 00:00:00 2001 From: Viktor Shen Date: Wed, 4 Jun 2025 12:13:57 +0200 Subject: [PATCH 04/33] refactor: change haptics configuration from iosOptions to dedicated iosAllowHaptics field --- .../ios/audioapi/ios/AudioAPIModule.mm | 4 ++-- .../ios/audioapi/ios/system/AudioSessionManager.h | 2 +- .../ios/audioapi/ios/system/AudioSessionManager.mm | 8 ++------ .../src/specs/NativeAudioAPIModule.ts | 3 ++- .../react-native-audio-api/src/system/AudioManager.ts | 3 ++- packages/react-native-audio-api/src/system/types.ts | 4 ++-- 6 files changed, 11 insertions(+), 13 deletions(-) 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 a2468e80..da047da5 100644 --- a/packages/react-native-audio-api/ios/audioapi/ios/AudioAPIModule.mm +++ b/packages/react-native-audio-api/ios/audioapi/ios/AudioAPIModule.mm @@ -97,9 +97,9 @@ - (void)invalidate resolve(@"false"); } -RCT_EXPORT_METHOD(setAudioSessionOptions : (NSString *)category mode : (NSString *)mode options : (NSArray *)options) +RCT_EXPORT_METHOD(setAudioSessionOptions : (NSString *)category mode : (NSString *)mode options : (NSArray *)options allowHaptics : (BOOL)allowHaptics) { - [self.audioSessionManager setAudioSessionOptions:category mode:mode options:options]; + [self.audioSessionManager setAudioSessionOptions:category mode:mode options:options allowHaptics:allowHaptics]; } RCT_EXPORT_METHOD(setLockScreenInfo : (NSDictionary *)info) 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 17754fb8..8db545e6 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 @@ -20,7 +20,7 @@ - (bool)configureAudioSession; - (NSNumber *)getDevicePreferredSampleRate; -- (void)setAudioSessionOptions:(NSString *)category mode:(NSString *)mode options:(NSArray *)options; +- (void)setAudioSessionOptions:(NSString *)category mode:(NSString *)mode options:(NSArray *)options allowHaptics:(BOOL)allowHaptics; - (bool)setActive:(bool)active; - (void)requestRecordingPermissions:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject; - (void)checkRecordingPermissions:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject; 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 3ebb86e5..875bbb26 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 @@ -30,12 +30,12 @@ - (NSNumber *)getDevicePreferredSampleRate return [NSNumber numberWithFloat:[self.audioSession sampleRate]]; } -- (void)setAudioSessionOptions:(NSString *)category mode:(NSString *)mode options:(NSArray *)options +- (void)setAudioSessionOptions:(NSString *)category mode:(NSString *)mode options:(NSArray *)options allowHaptics:(BOOL)allowHaptics { AVAudioSessionCategory sessionCategory = self.sessionCategory; AVAudioSessionMode sessionMode = self.sessionMode; AVAudioSessionCategoryOptions sessionOptions = 0; - bool allowHapticsAndSystemSoundsDuringRecording = false; + bool allowHapticsAndSystemSoundsDuringRecording = allowHaptics; if ([category isEqualToString:@"record"]) { sessionCategory = AVAudioSessionCategoryRecord; @@ -103,10 +103,6 @@ - (void)setAudioSessionOptions:(NSString *)category mode:(NSString *)mode option if ([option isEqualToString:@"interruptSpokenAudioAndMixWithOthers"]) { sessionOptions |= AVAudioSessionCategoryOptionInterruptSpokenAudioAndMixWithOthers; } - - if ([option isEqualToString:@"allowHapticsAndSystemSoundsDuringRecording"]) { - allowHapticsAndSystemSoundsDuringRecording = true; - } } if (self.sessionCategory != sessionCategory) { diff --git a/packages/react-native-audio-api/src/specs/NativeAudioAPIModule.ts b/packages/react-native-audio-api/src/specs/NativeAudioAPIModule.ts index 3f9e44b3..5353310e 100644 --- a/packages/react-native-audio-api/src/specs/NativeAudioAPIModule.ts +++ b/packages/react-native-audio-api/src/specs/NativeAudioAPIModule.ts @@ -12,7 +12,8 @@ interface Spec extends TurboModule { setAudioSessionOptions( category: string, mode: string, - options: Array + options: Array, + allowHaptics: boolean ): void; // Lock Screen Info diff --git a/packages/react-native-audio-api/src/system/AudioManager.ts b/packages/react-native-audio-api/src/system/AudioManager.ts index 22858f26..89b79159 100644 --- a/packages/react-native-audio-api/src/system/AudioManager.ts +++ b/packages/react-native-audio-api/src/system/AudioManager.ts @@ -35,7 +35,8 @@ class AudioManager { NativeAudioAPIModule!.setAudioSessionOptions( options.iosCategory ?? '', options.iosMode ?? '', - options.iosOptions ?? [] + options.iosOptions ?? [], + options.iosAllowHaptics ?? false ); } diff --git a/packages/react-native-audio-api/src/system/types.ts b/packages/react-native-audio-api/src/system/types.ts index a134fcfa..b984c098 100644 --- a/packages/react-native-audio-api/src/system/types.ts +++ b/packages/react-native-audio-api/src/system/types.ts @@ -25,13 +25,13 @@ export type IOSOption = | 'defaultToSpeaker' | 'allowBluetoothA2DP' | 'overrideMutedMicrophoneInterruption' - | 'interruptSpokenAudioAndMixWithOthers' - | 'allowHapticsAndSystemSoundsDuringRecording'; + | 'interruptSpokenAudioAndMixWithOthers'; export interface SessionOptions { iosMode?: IOSMode; iosOptions?: IOSOption[]; iosCategory?: IOSCategory; + iosAllowHaptics?: boolean; } export type MediaState = 'state_playing' | 'state_paused'; From cc693f5c8918729f6aa016e48c73cad71556f8e5 Mon Sep 17 00:00:00 2001 From: Viktor Shen Date: Wed, 4 Jun 2025 20:21:25 +0200 Subject: [PATCH 05/33] fix: remove explicit base class destructor calls in AudioRecorder subclasses --- .../src/main/cpp/audioapi/android/core/AndroidAudioRecorder.cpp | 2 -- .../ios/audioapi/ios/core/IOSAudioRecorder.mm | 2 -- 2 files changed, 4 deletions(-) diff --git a/packages/react-native-audio-api/android/src/main/cpp/audioapi/android/core/AndroidAudioRecorder.cpp b/packages/react-native-audio-api/android/src/main/cpp/audioapi/android/core/AndroidAudioRecorder.cpp index 48f24e15..1663190e 100644 --- a/packages/react-native-audio-api/android/src/main/cpp/audioapi/android/core/AndroidAudioRecorder.cpp +++ b/packages/react-native-audio-api/android/src/main/cpp/audioapi/android/core/AndroidAudioRecorder.cpp @@ -26,8 +26,6 @@ AndroidAudioRecorder::AndroidAudioRecorder( } AndroidAudioRecorder::~AndroidAudioRecorder() { - AudioRecorder::~AudioRecorder(); - if (mStream_) { mStream_->requestStop(); mStream_->close(); diff --git a/packages/react-native-audio-api/ios/audioapi/ios/core/IOSAudioRecorder.mm b/packages/react-native-audio-api/ios/audioapi/ios/core/IOSAudioRecorder.mm index 1283b783..afc46903 100644 --- a/packages/react-native-audio-api/ios/audioapi/ios/core/IOSAudioRecorder.mm +++ b/packages/react-native-audio-api/ios/audioapi/ios/core/IOSAudioRecorder.mm @@ -40,8 +40,6 @@ IOSAudioRecorder::~IOSAudioRecorder() { - AudioRecorder::~AudioRecorder(); - stop(); [audioRecorder_ cleanup]; } From 3e6627a11c83d994da893d198d10ed047c9630b9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20S=C4=99k?= Date: Thu, 5 Jun 2025 09:09:21 +0200 Subject: [PATCH 06/33] Merge pull request #489 from software-mansion/michalsek-patch-1 Update README.md --- README.md | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index ecfc4b74..dfe4bd1a 100644 --- a/README.md +++ b/README.md @@ -36,9 +36,11 @@ check out the [Getting Started](https://docs.swmansion.com/react-native-audio-ap - [![Released in 0.6.0](https://img.shields.io/badge/Released_in-0.6.0-green)](https://github.com/software-mansion/react-native-audio-api/releases/tag/0.6.0)
**System configuration** πŸ› οΈ
Full control of system audio settings, remote controls, lock screen integration and most importantly configurable background modes
-
**Microphone support** πŸŽ™οΈ
+ +-
**Microphone support** πŸŽ™οΈ
Grab audio data from device microphone or connected device, connect it to the audio graph or stream through the internet
-
**Connect audio param** 🀞
+ +-
**Connect audio param** 🀞
Ability to connect Audio nodes to audio params, which will allow for powerful and efficient modulation of audio parameters, creating effects like tremolo, vibrato or complex envelope followers.
- **JS Audio Worklets** 🐎
From 56a353bf55a2197d2e20350645dff435cba72767 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20S=C4=99k?= Date: Thu, 5 Jun 2025 09:10:18 +0200 Subject: [PATCH 07/33] Update README.md (#490) --- README.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index dfe4bd1a..33982a5a 100644 --- a/README.md +++ b/README.md @@ -34,13 +34,13 @@ check out the [Getting Started](https://docs.swmansion.com/react-native-audio-ap Ability to modify playback speed without affecting pitch of the sound
-- [![Released in 0.6.0](https://img.shields.io/badge/Released_in-0.6.0-green)](https://github.com/software-mansion/react-native-audio-api/releases/tag/0.6.0)
**System configuration** πŸ› οΈ
+- [![Released in 0.6.0](https://img.shields.io/badge/Released_in-0.6.0-green)](https://github.com/software-mansion/react-native-audio-api/releases/tag/0.6.0) **System configuration** πŸ› οΈ
Full control of system audio settings, remote controls, lock screen integration and most importantly configurable background modes
--
**Microphone support** πŸŽ™οΈ
+- **Microphone support** πŸŽ™οΈ
Grab audio data from device microphone or connected device, connect it to the audio graph or stream through the internet
--
**Connect audio param** 🀞
+- **Connect audio param** 🀞
Ability to connect Audio nodes to audio params, which will allow for powerful and efficient modulation of audio parameters, creating effects like tremolo, vibrato or complex envelope followers.
- **JS Audio Worklets** 🐎
From c70c467291db1e196e7079b811e53f1e695cf801 Mon Sep 17 00:00:00 2001 From: Maciej Makowski <120780663+maciejmakowski2003@users.noreply.github.com> Date: Thu, 5 Jun 2025 09:38:34 +0200 Subject: [PATCH 08/33] feat: implemented decoding pcm in base64 (#486) Co-authored-by: Maciej Makowski --- .../audioapi/android/core/AudioDecoder.cpp | 23 ++ .../HostObjects/BaseAudioContextHostObject.h | 34 +- .../cpp/audioapi/core/BaseAudioContext.cpp | 11 + .../cpp/audioapi/core/BaseAudioContext.h | 1 + .../cpp/audioapi/core/utils/AudioDecoder.h | 1 + .../common/cpp/audioapi/libs/base64/base64.h | 320 ++++++++++++++++++ .../ios/audioapi/ios/core/AudioDecoder.mm | 23 ++ .../src/core/BaseAudioContext.ts | 12 +- .../react-native-audio-api/src/interfaces.ts | 1 + 9 files changed, 421 insertions(+), 5 deletions(-) create mode 100644 packages/react-native-audio-api/common/cpp/audioapi/libs/base64/base64.h diff --git a/packages/react-native-audio-api/android/src/main/cpp/audioapi/android/core/AudioDecoder.cpp b/packages/react-native-audio-api/android/src/main/cpp/audioapi/android/core/AudioDecoder.cpp index 43713d3b..8f6e9459 100644 --- a/packages/react-native-audio-api/android/src/main/cpp/audioapi/android/core/AudioDecoder.cpp +++ b/packages/react-native-audio-api/android/src/main/cpp/audioapi/android/core/AudioDecoder.cpp @@ -1,4 +1,6 @@ #include +#include +#include #include #include @@ -110,4 +112,25 @@ std::shared_ptr AudioDecoder::decodeWithMemoryBlock( return audioBus; } +std::shared_ptr AudioDecoder::decodeWithPCMInBase64( + const std::string &data) const { + auto decodedData = base64_decode(data, false); + + const auto uint8Data = reinterpret_cast(decodedData.data()); + size_t frameCount = decodedData.size() / 2; + + auto audioBus = std::make_shared(frameCount, 1, sampleRate_); + auto channelData = audioBus->getChannel(0)->getData(); + + for (size_t i = 0; i < frameCount; ++i) { + auto sample = + static_cast((uint8Data[i * 2 + 1] << 8) | uint8Data[i * 2]); + channelData[i] = static_cast(sample); + } + + dsp::multiplyByScalar(channelData, 1.0f / 32768.0f, channelData, frameCount); + + return audioBus; +} + } // namespace audioapi 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 9fc079d3..504ab57d 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 @@ -49,7 +49,8 @@ class BaseAudioContextHostObject : public JsiHostObject { JSI_EXPORT_FUNCTION(BaseAudioContextHostObject, createPeriodicWave), JSI_EXPORT_FUNCTION(BaseAudioContextHostObject, createAnalyser), JSI_EXPORT_FUNCTION(BaseAudioContextHostObject, decodeAudioData), - JSI_EXPORT_FUNCTION(BaseAudioContextHostObject, decodeAudioDataSource)); + JSI_EXPORT_FUNCTION(BaseAudioContextHostObject, decodeAudioDataSource), + JSI_EXPORT_FUNCTION(BaseAudioContextHostObject, decodePCMAudioDataInBase64)); } JSI_PROPERTY_GETTER(destination) { @@ -163,13 +164,14 @@ JSI_HOST_FUNCTION(createBufferQueueSource) { auto promise = promiseVendor_->createPromise([this, sourcePath](std::shared_ptr promise) { std::thread([this, sourcePath, promise = std::move(promise)]() { auto results = context_->decodeAudioDataSource(sourcePath); - auto audioBufferHostObject = std::make_shared(results); if (!results) { promise->reject("Failed to decode audio data source."); return; } + auto audioBufferHostObject = std::make_shared(results); + promise->resolve([audioBufferHostObject = std::move(audioBufferHostObject)](jsi::Runtime &runtime) { auto jsiObject = jsi::Object::createFromHostObject(runtime, audioBufferHostObject); jsiObject.setExternalMemoryPressure(runtime, audioBufferHostObject->getSizeInBytes()); @@ -189,13 +191,14 @@ JSI_HOST_FUNCTION(createBufferQueueSource) { auto promise = promiseVendor_->createPromise([this, data, size](std::shared_ptr promise) { std::thread([this, data, size, promise = std::move(promise)]() { auto results = context_->decodeAudioData(data, size); - auto audioBufferHostObject = std::make_shared(results); if (!results) { promise->reject("Failed to decode audio data source."); return; } + auto audioBufferHostObject = std::make_shared(results); + promise->resolve([audioBufferHostObject = std::move(audioBufferHostObject)](jsi::Runtime &runtime) { auto jsiObject = jsi::Object::createFromHostObject(runtime, audioBufferHostObject); jsiObject.setExternalMemoryPressure(runtime, audioBufferHostObject->getSizeInBytes()); @@ -207,6 +210,31 @@ JSI_HOST_FUNCTION(createBufferQueueSource) { return promise; } + JSI_HOST_FUNCTION(decodePCMAudioDataInBase64) { + auto b64 = args[0].getString(runtime).utf8(runtime); + + auto promise = promiseVendor_->createPromise([this, b64](std::shared_ptr promise) { + std::thread([this, b64, promise = std::move(promise)]() { + auto results = context_->decodeWithPCMInBase64(b64); + + if (!results) { + promise->reject("Failed to decode audio data source."); + return; + } + + auto audioBufferHostObject = std::make_shared(results); + + promise->resolve([audioBufferHostObject = std::move(audioBufferHostObject)](jsi::Runtime &runtime) { + auto jsiObject = jsi::Object::createFromHostObject(runtime, audioBufferHostObject); + jsiObject.setExternalMemoryPressure(runtime, audioBufferHostObject->getSizeInBytes()); + return jsiObject; + }); + }).detach(); + }); + + return promise; + } + protected: std::shared_ptr context_; std::shared_ptr promiseVendor_; diff --git a/packages/react-native-audio-api/common/cpp/audioapi/core/BaseAudioContext.cpp b/packages/react-native-audio-api/common/cpp/audioapi/core/BaseAudioContext.cpp index 7df5ab3f..c9a46082 100644 --- a/packages/react-native-audio-api/common/cpp/audioapi/core/BaseAudioContext.cpp +++ b/packages/react-native-audio-api/common/cpp/audioapi/core/BaseAudioContext.cpp @@ -131,6 +131,17 @@ std::shared_ptr BaseAudioContext::decodeAudioData( return std::make_shared(audioBus); } +std::shared_ptr BaseAudioContext::decodeWithPCMInBase64( + const std::string &data) { + auto audioBus = audioDecoder_->decodeWithPCMInBase64(data); + + if (!audioBus) { + return nullptr; + } + + return std::make_shared(audioBus); +} + AudioNodeManager *BaseAudioContext::getNodeManager() { return nodeManager_.get(); } diff --git a/packages/react-native-audio-api/common/cpp/audioapi/core/BaseAudioContext.h b/packages/react-native-audio-api/common/cpp/audioapi/core/BaseAudioContext.h index c896c9cf..e57d8f41 100644 --- a/packages/react-native-audio-api/common/cpp/audioapi/core/BaseAudioContext.h +++ b/packages/react-native-audio-api/common/cpp/audioapi/core/BaseAudioContext.h @@ -56,6 +56,7 @@ class BaseAudioContext { std::shared_ptr decodeAudioDataSource(const std::string &path); std::shared_ptr decodeAudioData(const void *data, size_t size); + std::shared_ptr decodeWithPCMInBase64(const std::string &data); std::shared_ptr getBasicWaveForm(OscillatorType type); [[nodiscard]] float getNyquistFrequency() const; diff --git a/packages/react-native-audio-api/common/cpp/audioapi/core/utils/AudioDecoder.h b/packages/react-native-audio-api/common/cpp/audioapi/core/utils/AudioDecoder.h index 889cdba2..8b9a2cad 100644 --- a/packages/react-native-audio-api/common/cpp/audioapi/core/utils/AudioDecoder.h +++ b/packages/react-native-audio-api/common/cpp/audioapi/core/utils/AudioDecoder.h @@ -13,6 +13,7 @@ class AudioDecoder { [[nodiscard]] std::shared_ptr decodeWithFilePath(const std::string &path) const; [[nodiscard]] std::shared_ptr decodeWithMemoryBlock(const void *data, size_t size) const; + [[nodiscard]] std::shared_ptr decodeWithPCMInBase64(const std::string &data) const; private: float sampleRate_; diff --git a/packages/react-native-audio-api/common/cpp/audioapi/libs/base64/base64.h b/packages/react-native-audio-api/common/cpp/audioapi/libs/base64/base64.h new file mode 100644 index 00000000..157e6811 --- /dev/null +++ b/packages/react-native-audio-api/common/cpp/audioapi/libs/base64/base64.h @@ -0,0 +1,320 @@ +/* + base64.h + + base64 encoding and decoding with C++. + More information at + https://renenyffenegger.ch/notes/development/Base64/Encoding-and-decoding-base-64-with-cpp + + Version: 2.rc.09 (release candidate) + + Copyright (C) 2004-2017, 2020-2022 RenΓ© Nyffenegger + + This source code is provided 'as-is', without any express or implied + warranty. In no event will the author be held liable for any damages + arising from the use of this software. + + Permission is granted to anyone to use this software for any purpose, + including commercial applications, and to alter it and redistribute it + freely, subject to the following restrictions: + + 1. The origin of this source code must not be misrepresented; you must not + claim that you wrote the original source code. If you use this source code + in a product, an acknowledgment in the product documentation would be + appreciated but is not required. + + 2. Altered source versions must be plainly marked as such, and must not be + misrepresented as being the original source code. + + 3. This notice may not be removed or altered from any source distribution. + + RenΓ© Nyffenegger rene.nyffenegger@adp-gmbh.ch +*/ +/** + * Copyright (C) 2023 Kevin Heifner + * + * Modified to be header only. + * Templated for std::string, std::string_view, std::vector and other char containers. + */ + +#pragma once + +#include +#include +#include +#include + +// Interface: +// Defaults allow for use: +// std::string s = "foobar"; +// std::string encoded = base64_encode(s); +// std::string_view sv = "foobar"; +// std::string encoded = base64_encode(sv); +// std::vector vc = {'f', 'o', 'o'}; +// std::string encoded = base64_encode(vc); +// +// Also allows for user provided char containers and specified return types: +// std::string s = "foobar"; +// std::vector encoded = base64_encode>(s); + +template +RetString base64_encode(const String& s, bool url = false); + +template +RetString base64_encode_pem(const String& s); + +template +RetString base64_encode_mime(const String& s); + +template +RetString base64_decode(const String& s, bool remove_linebreaks = false); + +template +RetString base64_encode(const unsigned char* s, size_t len, bool url = false); + +namespace detail { + // + // Depending on the url parameter in base64_chars, one of + // two sets of base64 characters needs to be chosen. + // They differ in their last two characters. + // +constexpr const char* to_base64_chars[2] = { + "ABCDEFGHIJKLMNOPQRSTUVWXYZ" + "abcdefghijklmnopqrstuvwxyz" + "0123456789" + "+/", + + "ABCDEFGHIJKLMNOPQRSTUVWXYZ" + "abcdefghijklmnopqrstuvwxyz" + "0123456789" + "-_"}; + +constexpr unsigned char from_base64_chars[256] = { + 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, + 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, + 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 62, 64, 62, 64, 63, + 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, 64, 64, 64, 64, 64, 64, + 64, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, + 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 64, 64, 64, 64, 63, + 64, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, + 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, 64, 64, 64, 64, 64, + 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, + 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, + 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, + 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, + 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, + 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, + 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, + 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64 +}; + +inline unsigned int pos_of_char(const unsigned char chr) { + // + // Return the position of chr within base64_encode() + // + + if (from_base64_chars[chr] != 64) return from_base64_chars[chr]; + + // + // 2020-10-23: Throw std::exception rather than const char* + //(Pablo Martin-Gomez, https://github.com/Bouska) + // + throw std::runtime_error("Input is not valid base64-encoded data."); +} + +template +inline RetString insert_linebreaks(const String& str, size_t distance) { + // + // Provided by https://github.com/JomaCorpFX, adapted by Rene & Kevin + // + if (!str.size()) { + return RetString{}; + } + + if (distance < str.size()) { + size_t pos = distance; + String s{str}; + while (pos < s.size()) { + s.insert(pos, "\n"); + pos += distance + 1; + } + return s; + } else { + return str; + } +} + +template +inline RetString encode_with_line_breaks(String s) { + return insert_linebreaks(base64_encode(s, false), line_length); +} + +template +inline RetString encode_pem(String s) { + return encode_with_line_breaks(s); +} + +template +inline RetString encode_mime(String s) { + return encode_with_line_breaks(s); +} + +template +inline RetString encode(String s, bool url) { + return base64_encode(reinterpret_cast(s.data()), s.size(), url); +} + +} // namespace detail + +template +inline RetString base64_encode(const unsigned char* bytes_to_encode, size_t in_len, bool url) { + size_t len_encoded = (in_len + 2) / 3 * 4; + + unsigned char trailing_char = url ? '.' : '='; + + // + // Choose set of base64 characters. They differ + // for the last two positions, depending on the url + // parameter. + // A bool (as is the parameter url) is guaranteed + // to evaluate to either 0 or 1 in C++ therefore, + // the correct character set is chosen by subscripting + // base64_chars with url. + // + const char *base64_chars_ = detail::to_base64_chars[url]; + + RetString ret; + ret.reserve(len_encoded); + + unsigned int pos = 0; + + while (pos < in_len) { + ret.push_back(base64_chars_[(bytes_to_encode[pos + 0] & 0xfc) >> 2]); + + if (pos + 1 < in_len) { + ret.push_back(base64_chars_[((bytes_to_encode[pos + 0] & 0x03) << 4) + + ((bytes_to_encode[pos + 1] & 0xf0) >> 4)]); + + if (pos + 2 < in_len) { + ret.push_back(base64_chars_[((bytes_to_encode[pos + 1] & 0x0f) << 2) + + ((bytes_to_encode[pos + 2] & 0xc0) >> 6)]); + ret.push_back(base64_chars_[bytes_to_encode[pos + 2] & 0x3f]); + } else { + ret.push_back(base64_chars_[(bytes_to_encode[pos + 1] & 0x0f) << 2]); + ret.push_back(trailing_char); + } + } else { + ret.push_back(base64_chars_[(bytes_to_encode[pos + 0] & 0x03) << 4]); + ret.push_back(trailing_char); + ret.push_back(trailing_char); + } + + pos += 3; + } + + return ret; +} + +namespace detail { + +template +inline RetString decode(const String& encoded_string, bool remove_linebreaks) { + static_assert(!std::is_same::value, + "RetString should not be std::string_view"); + + // + // decode(…) is templated so that it can be used with String = const std::string& + // or std::string_view (requires at least C++17) + // + + if (encoded_string.empty()) + return RetString{}; + + if (remove_linebreaks) { + String copy{encoded_string}; + + copy.erase(std::remove(copy.begin(), copy.end(), '\n'), copy.end()); + + return base64_decode(copy, false); + } + + size_t length_of_string = encoded_string.size(); + size_t pos = 0; + + // + // The approximate length (bytes) of the decoded string might be one or + // two bytes smaller, depending on the amount of trailing equal signs + // in the encoded string. This approximation is needed to reserve + // enough space in the string to be returned. + // + size_t approx_length_of_decoded_string = length_of_string / 4 * 3; + RetString ret; + ret.reserve(approx_length_of_decoded_string); + + while (pos < length_of_string) { + // + // Iterate over encoded input string in chunks. The size of all + // chunks except the last one is 4 bytes. + // + // The last chunk might be padded with equal signs or dots + // in order to make it 4 bytes in size as well, but this + // is not required as per RFC 2045. + // + // All chunks except the last one produce three output bytes. + // + // The last chunk produces at least one and up to three bytes. + // + + size_t pos_of_char_1 = pos_of_char(encoded_string.at(pos + 1)); + + // + // Emit the first output byte that is produced in each chunk: + // + ret.push_back(static_cast(((pos_of_char(encoded_string.at(pos + 0))) << 2) + ((pos_of_char_1 & 0x30) >> 4))); + + if ((pos + 2 < length_of_string) && + // Check for data that is not padded with equal signs (which is allowed by RFC 2045) + encoded_string.at(pos + 2) != '=' && + encoded_string.at(pos + 2) != '.' ) { // accept URL-safe base 64 strings, too, so check for '.' also. + // + // Emit a chunk's second byte (which might not be produced in the last chunk). + // + unsigned int pos_of_char_2 = pos_of_char(encoded_string.at(pos + 2)); + ret.push_back(static_cast(((pos_of_char_1 & 0x0f) << 4) + ((pos_of_char_2 & 0x3c) >> 2))); + + if ((pos + 3 < length_of_string) && + encoded_string.at(pos + 3) != '=' && + encoded_string.at(pos + 3) != '.' ) { + // + // Emit a chunk's third byte (which might not be produced in the last chunk). + // + ret.push_back(static_cast(((pos_of_char_2 & 0x03) << 6) + pos_of_char(encoded_string.at(pos + 3)))); + } + } + + pos += 4; + } + + return ret; +} + +} // namespace detail + +template +inline RetString base64_decode(const String& s, bool remove_linebreaks) { + return detail::decode(s, remove_linebreaks); +} + +template +inline RetString base64_encode(const String& s, bool url) { + return detail::encode(s, url); +} + +template +inline RetString base64_encode_pem (const String& s) { + return detail::encode_pem(s); +} + +template +inline RetString base64_encode_mime(const String& s) { + return detail::encode_mime(s); +} diff --git a/packages/react-native-audio-api/ios/audioapi/ios/core/AudioDecoder.mm b/packages/react-native-audio-api/ios/audioapi/ios/core/AudioDecoder.mm index 5d178bef..dcc4b114 100644 --- a/packages/react-native-audio-api/ios/audioapi/ios/core/AudioDecoder.mm +++ b/packages/react-native-audio-api/ios/audioapi/ios/core/AudioDecoder.mm @@ -2,6 +2,8 @@ #import #include +#include +#include #include #include @@ -94,4 +96,25 @@ return audioBus; } + +std::shared_ptr AudioDecoder::decodeWithPCMInBase64(const std::string &data) const +{ + auto decodedData = base64_decode(data, false); + + const auto uint8Data = reinterpret_cast(decodedData.data()); + size_t frameCount = decodedData.size() / 2; + + auto audioBus = std::make_shared(frameCount, 1, sampleRate_); + auto channelData = audioBus->getChannel(0)->getData(); + + for (size_t i = 0; i < frameCount; ++i) { + auto sample = static_cast((uint8Data[i * 2 + 1] << 8) | uint8Data[i * 2]); + channelData[i] = static_cast(sample); + } + + dsp::multiplyByScalar(channelData, 1.0f / 32768.0f, channelData, frameCount); + + return audioBus; +} + } // namespace audioapi diff --git a/packages/react-native-audio-api/src/core/BaseAudioContext.ts b/packages/react-native-audio-api/src/core/BaseAudioContext.ts index fae97976..26b71512 100644 --- a/packages/react-native-audio-api/src/core/BaseAudioContext.ts +++ b/packages/react-native-audio-api/src/core/BaseAudioContext.ts @@ -130,9 +130,17 @@ export default class BaseAudioContext { ); } - async decodeAudioData(arrayBuffer: ArrayBuffer): Promise { + async decodeAudioData(data: ArrayBuffer | string): Promise { + // pcm data in base64 + if (typeof data === 'string') { + return new AudioBuffer( + await this.context.decodePCMAudioDataInBase64(data) + ); + } + + // data in array buffer return new AudioBuffer( - await this.context.decodeAudioData(new Uint8Array(arrayBuffer)) + await this.context.decodeAudioData(new Uint8Array(data)) ); } } diff --git a/packages/react-native-audio-api/src/interfaces.ts b/packages/react-native-audio-api/src/interfaces.ts index 31b51ce8..d2dbdacc 100644 --- a/packages/react-native-audio-api/src/interfaces.ts +++ b/packages/react-native-audio-api/src/interfaces.ts @@ -33,6 +33,7 @@ export interface IBaseAudioContext { createAnalyser: () => IAnalyserNode; decodeAudioDataSource: (sourcePath: string) => Promise; decodeAudioData: (arrayBuffer: ArrayBuffer) => Promise; + decodePCMAudioDataInBase64: (b64: string) => Promise; } export interface IAudioContext extends IBaseAudioContext { From f8042cdd91a1377c3c62a457fe2164dd607b6538 Mon Sep 17 00:00:00 2001 From: Maciej Makowski <120780663+maciejmakowski2003@users.noreply.github.com> Date: Thu, 5 Jun 2025 09:58:30 +0200 Subject: [PATCH 09/33] Cant wait to do that - PAUSE in RN-Audio-API (#491) * feat: implemented pause on AudioBufferQueueSourceNode * ci: yarn format --------- Co-authored-by: Maciej Makowski --- .../AudioBufferQueueSourceNodeHostObject.h | 20 +++++++++++-- .../sources/AudioBufferQueueSourceNode.cpp | 24 +++++++++++++-- .../core/sources/AudioBufferQueueSourceNode.h | 5 ++++ .../core/sources/AudioScheduledSourceNode.cpp | 1 - .../core/sources/AudioScheduledSourceNode.h | 6 ++-- .../ios/audioapi/ios/AudioAPIModule.mm | 4 ++- .../audioapi/ios/system/AudioSessionManager.h | 5 +++- .../ios/system/AudioSessionManager.mm | 10 +++++-- .../src/core/AudioBufferQueueSourceNode.ts | 29 +++++++++---------- .../src/events/types.ts | 7 +---- .../react-native-audio-api/src/interfaces.ts | 7 +++-- 11 files changed, 80 insertions(+), 38 deletions(-) diff --git a/packages/react-native-audio-api/common/cpp/audioapi/HostObjects/AudioBufferQueueSourceNodeHostObject.h b/packages/react-native-audio-api/common/cpp/audioapi/HostObjects/AudioBufferQueueSourceNodeHostObject.h index 94c1cfcc..d5aafc63 100644 --- a/packages/react-native-audio-api/common/cpp/audioapi/HostObjects/AudioBufferQueueSourceNodeHostObject.h +++ b/packages/react-native-audio-api/common/cpp/audioapi/HostObjects/AudioBufferQueueSourceNodeHostObject.h @@ -30,7 +30,8 @@ class AudioBufferQueueSourceNodeHostObject addFunctions( JSI_EXPORT_FUNCTION(AudioBufferQueueSourceNodeHostObject, start), - JSI_EXPORT_FUNCTION(AudioBufferQueueSourceNodeHostObject, enqueueBuffer)); + JSI_EXPORT_FUNCTION(AudioBufferQueueSourceNodeHostObject, enqueueBuffer), + JSI_EXPORT_FUNCTION(AudioBufferQueueSourceNodeHostObject, pause)); } JSI_PROPERTY_GETTER(detune) { @@ -66,12 +67,25 @@ class AudioBufferQueueSourceNodeHostObject JSI_HOST_FUNCTION(start) { auto when = args[0].getNumber(); - auto offset = args[1].getNumber(); auto audioBufferQueueSourceNode = std::static_pointer_cast(node_); - audioBufferQueueSourceNode->start(when, offset); + if (args[1].isUndefined()) { + audioBufferQueueSourceNode->start(when); + } else { + auto offset = args[1].asNumber(); + audioBufferQueueSourceNode->start(when, offset); + } + + return jsi::Value::undefined(); + } + + JSI_HOST_FUNCTION(pause) { + auto audioBufferQueueSourceNode = + std::static_pointer_cast(node_); + + audioBufferQueueSourceNode->pause(); return jsi::Value::undefined(); } diff --git a/packages/react-native-audio-api/common/cpp/audioapi/core/sources/AudioBufferQueueSourceNode.cpp b/packages/react-native-audio-api/common/cpp/audioapi/core/sources/AudioBufferQueueSourceNode.cpp index 422ce5ec..c49fd1bd 100644 --- a/packages/react-native-audio-api/common/cpp/audioapi/core/sources/AudioBufferQueueSourceNode.cpp +++ b/packages/react-native-audio-api/common/cpp/audioapi/core/sources/AudioBufferQueueSourceNode.cpp @@ -47,12 +47,22 @@ std::shared_ptr AudioBufferQueueSourceNode::getPlaybackRateParam() return playbackRateParam_; } -void AudioBufferQueueSourceNode::start(double when, double offset) { +void AudioBufferQueueSourceNode::start(double when) { AudioScheduledSourceNode::start(when); + isPaused_ = false; +} + +void AudioBufferQueueSourceNode::start(double when, double offset) { + start(when); vReadIndex_ = static_cast(context_->getSampleRate() * offset); } +void AudioBufferQueueSourceNode::pause() { + AudioScheduledSourceNode::stop(0.0); + isPaused_ = true; +} + void AudioBufferQueueSourceNode::enqueueBuffer( const std::shared_ptr &buffer, int bufferId, @@ -64,6 +74,14 @@ void AudioBufferQueueSourceNode::enqueueBuffer( } void AudioBufferQueueSourceNode::disable() { + if (isPaused_) { + playbackState_ = PlaybackState::UNSCHEDULED; + startTime_ = -1.0; + stopTime_ = -1.0; + + return; + } + audioapi::AudioNode::disable(); std::string state = "stopped"; @@ -78,6 +96,7 @@ void AudioBufferQueueSourceNode::disable() { context_->audioEventHandlerRegistry_->invokeHandlerWithEventBody( "ended", onEndedCallbackId_, body); + buffers_ = {}; } @@ -116,7 +135,7 @@ void AudioBufferQueueSourceNode::setOnPositionChangedCallbackId( void AudioBufferQueueSourceNode::sendOnPositionChangedEvent() { if (onPositionChangedTime_ > onPositionChangedInterval_) { std::unordered_map body = { - {"value", getStopTime()}, {"bufferId", bufferId_}}; + {"value", position_ + getStopTime()}}; context_->audioEventHandlerRegistry_->invokeHandlerWithEventBody( "positionChanged", onPositionChangedCallbackId_, body); @@ -209,6 +228,7 @@ void AudioBufferQueueSourceNode::processWithoutInterpolation( framesLeft -= framesToCopy; if (readIndex >= buffer->getLength()) { + position_ += buffer->getDuration(); buffers_.pop(); if (buffers_.empty()) { diff --git a/packages/react-native-audio-api/common/cpp/audioapi/core/sources/AudioBufferQueueSourceNode.h b/packages/react-native-audio-api/common/cpp/audioapi/core/sources/AudioBufferQueueSourceNode.h index 791c788c..521c8050 100644 --- a/packages/react-native-audio-api/common/cpp/audioapi/core/sources/AudioBufferQueueSourceNode.h +++ b/packages/react-native-audio-api/common/cpp/audioapi/core/sources/AudioBufferQueueSourceNode.h @@ -23,7 +23,10 @@ class AudioBufferQueueSourceNode : public AudioScheduledSourceNode { [[nodiscard]] std::shared_ptr getDetuneParam() const; [[nodiscard]] std::shared_ptr getPlaybackRateParam() const; + void start(double when) override; void start(double when, double offset); + void pause(); + void enqueueBuffer(const std::shared_ptr &buffer, int bufferId, bool isLastBuffer); void disable() override; @@ -54,11 +57,13 @@ class AudioBufferQueueSourceNode : public AudioScheduledSourceNode { std::queue>> buffers_; int bufferId_ = 0; bool isLastBuffer_ = false; + bool isPaused_ = false; // positionChanged event props: callbackId, update interval in frames, time since last update in frames uint64_t onPositionChangedCallbackId_ = 0; int onPositionChangedInterval_; int onPositionChangedTime_ = 0; + size_t position_ = 0; void processWithPitchCorrection(const std::shared_ptr &processingBus, int framesToProcess); 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 44f0fb24..d2452ed7 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 @@ -132,7 +132,6 @@ void AudioScheduledSourceNode::updatePlaybackInfo( playbackState_ = PlaybackState::STOP_SCHEDULED; handleStopScheduled(); - playbackState_ = PlaybackState::FINISHED; return; } diff --git a/packages/react-native-audio-api/common/cpp/audioapi/core/sources/AudioScheduledSourceNode.h b/packages/react-native-audio-api/common/cpp/audioapi/core/sources/AudioScheduledSourceNode.h index a5bf899a..93a70d87 100644 --- a/packages/react-native-audio-api/common/cpp/audioapi/core/sources/AudioScheduledSourceNode.h +++ b/packages/react-native-audio-api/common/cpp/audioapi/core/sources/AudioScheduledSourceNode.h @@ -23,12 +23,12 @@ class AudioScheduledSourceNode : public AudioNode { // UNSCHEDULED: The node is not scheduled to play. // SCHEDULED: The node is scheduled to play at a specific time. // PLAYING: The node is currently playing. - // FINISHED: The node has finished playing. // STOP_SCHEDULED: The node is scheduled to stop at a specific time, but is still playing. - enum class PlaybackState { UNSCHEDULED, SCHEDULED, PLAYING, FINISHED, STOP_SCHEDULED }; + // FINISHED: The node has finished playing. + enum class PlaybackState { UNSCHEDULED, SCHEDULED, PLAYING, STOP_SCHEDULED, FINISHED }; explicit AudioScheduledSourceNode(BaseAudioContext *context); - void start(double when); + virtual void start(double when); void stop(double when); bool isUnscheduled(); 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 da047da5..17669700 100644 --- a/packages/react-native-audio-api/ios/audioapi/ios/AudioAPIModule.mm +++ b/packages/react-native-audio-api/ios/audioapi/ios/AudioAPIModule.mm @@ -97,7 +97,9 @@ - (void)invalidate resolve(@"false"); } -RCT_EXPORT_METHOD(setAudioSessionOptions : (NSString *)category mode : (NSString *)mode options : (NSArray *)options allowHaptics : (BOOL)allowHaptics) +RCT_EXPORT_METHOD( + setAudioSessionOptions : (NSString *)category mode : (NSString *)mode options : (NSArray *) + options allowHaptics : (BOOL)allowHaptics) { [self.audioSessionManager setAudioSessionOptions:category mode:mode options:options allowHaptics:allowHaptics]; } 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 8db545e6..8c37c9dd 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 @@ -20,7 +20,10 @@ - (bool)configureAudioSession; - (NSNumber *)getDevicePreferredSampleRate; -- (void)setAudioSessionOptions:(NSString *)category mode:(NSString *)mode options:(NSArray *)options allowHaptics:(BOOL)allowHaptics; +- (void)setAudioSessionOptions:(NSString *)category + mode:(NSString *)mode + options:(NSArray *)options + allowHaptics:(BOOL)allowHaptics; - (bool)setActive:(bool)active; - (void)requestRecordingPermissions:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject; - (void)checkRecordingPermissions:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject; 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 875bbb26..10dbcfc3 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 @@ -30,7 +30,10 @@ - (NSNumber *)getDevicePreferredSampleRate return [NSNumber numberWithFloat:[self.audioSession sampleRate]]; } -- (void)setAudioSessionOptions:(NSString *)category mode:(NSString *)mode options:(NSArray *)options allowHaptics:(BOOL)allowHaptics +- (void)setAudioSessionOptions:(NSString *)category + mode:(NSString *)mode + options:(NSArray *)options + allowHaptics:(BOOL)allowHaptics { AVAudioSessionCategory sessionCategory = self.sessionCategory; AVAudioSessionMode sessionMode = self.sessionMode; @@ -180,8 +183,9 @@ - (bool)configureAudioSession } if (@available(iOS 13.0, *)) { - [self.audioSession setAllowHapticsAndSystemSoundsDuringRecording:self.allowHapticsAndSystemSoundsDuringRecording error:&error]; - + [self.audioSession setAllowHapticsAndSystemSoundsDuringRecording:self.allowHapticsAndSystemSoundsDuringRecording + error:&error]; + if (error != nil) { NSLog(@"Error while setting allowHapticsAndSystemSoundsDuringRecording: %@", [error debugDescription]); } diff --git a/packages/react-native-audio-api/src/core/AudioBufferQueueSourceNode.ts b/packages/react-native-audio-api/src/core/AudioBufferQueueSourceNode.ts index 018c4d9c..5d5b0268 100644 --- a/packages/react-native-audio-api/src/core/AudioBufferQueueSourceNode.ts +++ b/packages/react-native-audio-api/src/core/AudioBufferQueueSourceNode.ts @@ -3,8 +3,8 @@ import AudioScheduledSourceNode from './AudioScheduledSourceNode'; import BaseAudioContext from './BaseAudioContext'; import AudioBuffer from './AudioBuffer'; import AudioParam from './AudioParam'; -import { InvalidStateError, RangeError } from '../errors'; -import { OnPositionChangedEventType } from '../events/types'; +import { RangeError } from '../errors'; +import { EventTypeWithValue } from '../events/types'; export default class AudioBufferQueueSourceNode extends AudioScheduledSourceNode { readonly playbackRate: AudioParam; @@ -29,31 +29,30 @@ export default class AudioBufferQueueSourceNode extends AudioScheduledSourceNode ); } - public start(when: number = 0, offset: number = 0): void { + public override start(when: number = 0, offset?: number): void { if (when < 0) { throw new RangeError( `when must be a finite non-negative number: ${when}` ); } - if (offset < 0) { - throw new RangeError( - `offset must be a finite non-negative number: ${offset}` - ); - } - - if (this.hasBeenStarted) { - throw new InvalidStateError('Cannot call start more than once'); + if (offset) { + if (offset < 0) { + throw new RangeError( + `offset must be a finite non-negative number: ${offset}` + ); + } } - this.hasBeenStarted = true; (this.node as IAudioBufferQueueSourceNode).start(when, offset); } + public pause(): void { + (this.node as IAudioBufferQueueSourceNode).pause(); + } + // eslint-disable-next-line accessor-pairs - public set onPositionChanged( - callback: (event: OnPositionChangedEventType) => void - ) { + public set onPositionChanged(callback: (event: EventTypeWithValue) => void) { const subscription = this.audioEventEmitter.addAudioEventListener( 'positionChanged', callback diff --git a/packages/react-native-audio-api/src/events/types.ts b/packages/react-native-audio-api/src/events/types.ts index c078d626..7894abb2 100644 --- a/packages/react-native-audio-api/src/events/types.ts +++ b/packages/react-native-audio-api/src/events/types.ts @@ -12,11 +12,6 @@ export interface OnEndedEventType { bufferId: number | undefined; } -export interface OnPositionChangedEventType { - value: number; - bufferId: number; -} - interface OnInterruptionEventType { type: 'ended' | 'began'; shouldResume: boolean; @@ -64,7 +59,7 @@ export interface OnAudioReadyEventType { interface AudioAPIEvents { ended: OnEndedEventType; audioReady: OnAudioReadyEventType; - positionChanged: OnPositionChangedEventType; + positionChanged: EventTypeWithValue; audioError: EventEmptyType; // to change systemStateChanged: EventEmptyType; // to change } diff --git a/packages/react-native-audio-api/src/interfaces.ts b/packages/react-native-audio-api/src/interfaces.ts index d2dbdacc..7e098412 100644 --- a/packages/react-native-audio-api/src/interfaces.ts +++ b/packages/react-native-audio-api/src/interfaces.ts @@ -85,7 +85,7 @@ export interface IBiquadFilterNode extends IAudioNode { export interface IAudioDestinationNode extends IAudioNode {} export interface IAudioScheduledSourceNode extends IAudioNode { - start(when?: number): void; + start(when: number): void; stop: (when: number) => void; // passing subscriptionId(uint_64 in cpp, string in js) to the cpp @@ -108,7 +108,7 @@ export interface IAudioBufferSourceNode extends IAudioScheduledSourceNode { detune: IAudioParam; playbackRate: IAudioParam; - start: (when?: number, offset?: number, duration?: number) => void; + start: (when: number, offset?: number, duration?: number) => void; } export interface IAudioBufferQueueSourceNode extends IAudioScheduledSourceNode { @@ -120,7 +120,8 @@ export interface IAudioBufferQueueSourceNode extends IAudioScheduledSourceNode { bufferId: number, isLastBuffer: boolean ) => void; - start: (when?: number, offset?: number) => void; + start: (when: number, offset?: number) => void; + pause: () => void; // passing subscriptionId(uint_64 in cpp, string in js) to the cpp onPositionChanged: string; From 3c9d77310e8d58cbd163a8f07fb6843466c3e64a Mon Sep 17 00:00:00 2001 From: Maciej Makowski <120780663+maciejmakowski2003@users.noreply.github.com> Date: Thu, 5 Jun 2025 12:57:38 +0200 Subject: [PATCH 10/33] fix: fixed stop (#492) Co-authored-by: Maciej Makowski --- .../core/sources/AudioBufferQueueSourceNode.cpp | 5 +++++ .../audioapi/core/sources/AudioBufferQueueSourceNode.h | 3 ++- .../audioapi/core/sources/AudioScheduledSourceNode.h | 2 +- .../src/core/AudioBufferQueueSourceNode.ts | 10 ++++++++++ 4 files changed, 18 insertions(+), 2 deletions(-) diff --git a/packages/react-native-audio-api/common/cpp/audioapi/core/sources/AudioBufferQueueSourceNode.cpp b/packages/react-native-audio-api/common/cpp/audioapi/core/sources/AudioBufferQueueSourceNode.cpp index c49fd1bd..d22dbf07 100644 --- a/packages/react-native-audio-api/common/cpp/audioapi/core/sources/AudioBufferQueueSourceNode.cpp +++ b/packages/react-native-audio-api/common/cpp/audioapi/core/sources/AudioBufferQueueSourceNode.cpp @@ -58,6 +58,11 @@ void AudioBufferQueueSourceNode::start(double when, double offset) { vReadIndex_ = static_cast(context_->getSampleRate() * offset); } +void AudioBufferQueueSourceNode::stop(double when) { + AudioScheduledSourceNode::stop(when); + isPaused_ = false; +} + void AudioBufferQueueSourceNode::pause() { AudioScheduledSourceNode::stop(0.0); isPaused_ = true; diff --git a/packages/react-native-audio-api/common/cpp/audioapi/core/sources/AudioBufferQueueSourceNode.h b/packages/react-native-audio-api/common/cpp/audioapi/core/sources/AudioBufferQueueSourceNode.h index 521c8050..2cc28fb3 100644 --- a/packages/react-native-audio-api/common/cpp/audioapi/core/sources/AudioBufferQueueSourceNode.h +++ b/packages/react-native-audio-api/common/cpp/audioapi/core/sources/AudioBufferQueueSourceNode.h @@ -25,6 +25,7 @@ class AudioBufferQueueSourceNode : public AudioScheduledSourceNode { void start(double when) override; void start(double when, double offset); + void stop(double when) override; void pause(); void enqueueBuffer(const std::shared_ptr &buffer, int bufferId, bool isLastBuffer); @@ -63,7 +64,7 @@ class AudioBufferQueueSourceNode : public AudioScheduledSourceNode { uint64_t onPositionChangedCallbackId_ = 0; int onPositionChangedInterval_; int onPositionChangedTime_ = 0; - size_t position_ = 0; + double position_ = 0; void processWithPitchCorrection(const std::shared_ptr &processingBus, int framesToProcess); diff --git a/packages/react-native-audio-api/common/cpp/audioapi/core/sources/AudioScheduledSourceNode.h b/packages/react-native-audio-api/common/cpp/audioapi/core/sources/AudioScheduledSourceNode.h index 93a70d87..cdea0693 100644 --- a/packages/react-native-audio-api/common/cpp/audioapi/core/sources/AudioScheduledSourceNode.h +++ b/packages/react-native-audio-api/common/cpp/audioapi/core/sources/AudioScheduledSourceNode.h @@ -29,7 +29,7 @@ class AudioScheduledSourceNode : public AudioNode { explicit AudioScheduledSourceNode(BaseAudioContext *context); virtual void start(double when); - void stop(double when); + virtual void stop(double when); bool isUnscheduled(); bool isScheduled(); diff --git a/packages/react-native-audio-api/src/core/AudioBufferQueueSourceNode.ts b/packages/react-native-audio-api/src/core/AudioBufferQueueSourceNode.ts index 5d5b0268..3ed6f18b 100644 --- a/packages/react-native-audio-api/src/core/AudioBufferQueueSourceNode.ts +++ b/packages/react-native-audio-api/src/core/AudioBufferQueueSourceNode.ts @@ -47,6 +47,16 @@ export default class AudioBufferQueueSourceNode extends AudioScheduledSourceNode (this.node as IAudioBufferQueueSourceNode).start(when, offset); } + public override stop(when: number = 0): void { + if (when < 0) { + throw new RangeError( + `when must be a finite non-negative number: ${when}` + ); + } + + (this.node as IAudioBufferQueueSourceNode).stop(when); + } + public pause(): void { (this.node as IAudioBufferQueueSourceNode).pause(); } From cddba015d56caf285ae8bbcbbee4f0a6daddf451 Mon Sep 17 00:00:00 2001 From: Maciej Makowski <120780663+maciejmakowski2003@users.noreply.github.com> Date: Thu, 5 Jun 2025 16:12:06 +0200 Subject: [PATCH 11/33] fix: fixed spec alignment on android (#493) Co-authored-by: Maciej Makowski --- .../java/com/swmansion/audioapi/AudioAPIModule.kt | 1 + .../android/src/oldarch/NativeAudioAPIModuleSpec.java | 2 +- .../AudioBufferQueueSourceNodeHostObject.h | 11 ++++++----- .../core/sources/AudioBufferQueueSourceNode.cpp | 11 ++++------- .../core/sources/AudioBufferQueueSourceNode.h | 1 - .../audioapi/core/sources/AudioScheduledSourceNode.h | 2 +- 6 files changed, 13 insertions(+), 15 deletions(-) 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 3cf5dd62..b21b0206 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 @@ -67,6 +67,7 @@ class AudioAPIModule( category: String?, mode: String?, options: ReadableArray?, + allowHaptics: Boolean, ) { // noting to do here } diff --git a/packages/react-native-audio-api/android/src/oldarch/NativeAudioAPIModuleSpec.java b/packages/react-native-audio-api/android/src/oldarch/NativeAudioAPIModuleSpec.java index 73774139..2274af87 100644 --- a/packages/react-native-audio-api/android/src/oldarch/NativeAudioAPIModuleSpec.java +++ b/packages/react-native-audio-api/android/src/oldarch/NativeAudioAPIModuleSpec.java @@ -48,7 +48,7 @@ public NativeAudioAPIModuleSpec(ReactApplicationContext reactContext) { @ReactMethod @DoNotStrip - public abstract void setAudioSessionOptions(String category, String mode, ReadableArray options); + public abstract void setAudioSessionOptions(String category, String mode, ReadableArray options, boolean allowHaptics); @ReactMethod @DoNotStrip diff --git a/packages/react-native-audio-api/common/cpp/audioapi/HostObjects/AudioBufferQueueSourceNodeHostObject.h b/packages/react-native-audio-api/common/cpp/audioapi/HostObjects/AudioBufferQueueSourceNodeHostObject.h index d5aafc63..a14e7853 100644 --- a/packages/react-native-audio-api/common/cpp/audioapi/HostObjects/AudioBufferQueueSourceNodeHostObject.h +++ b/packages/react-native-audio-api/common/cpp/audioapi/HostObjects/AudioBufferQueueSourceNodeHostObject.h @@ -71,13 +71,14 @@ class AudioBufferQueueSourceNodeHostObject auto audioBufferQueueSourceNode = std::static_pointer_cast(node_); - if (args[1].isUndefined()) { - audioBufferQueueSourceNode->start(when); - } else { - auto offset = args[1].asNumber(); - audioBufferQueueSourceNode->start(when, offset); + double offset = -1.0; + + if (args[1].isNumber()) { + offset = args[1].asNumber(); } + audioBufferQueueSourceNode->start(when, offset); + return jsi::Value::undefined(); } diff --git a/packages/react-native-audio-api/common/cpp/audioapi/core/sources/AudioBufferQueueSourceNode.cpp b/packages/react-native-audio-api/common/cpp/audioapi/core/sources/AudioBufferQueueSourceNode.cpp index d22dbf07..f9605faa 100644 --- a/packages/react-native-audio-api/common/cpp/audioapi/core/sources/AudioBufferQueueSourceNode.cpp +++ b/packages/react-native-audio-api/common/cpp/audioapi/core/sources/AudioBufferQueueSourceNode.cpp @@ -47,15 +47,12 @@ std::shared_ptr AudioBufferQueueSourceNode::getPlaybackRateParam() return playbackRateParam_; } -void AudioBufferQueueSourceNode::start(double when) { - AudioScheduledSourceNode::start(when); - isPaused_ = false; -} - void AudioBufferQueueSourceNode::start(double when, double offset) { - start(when); + AudioScheduledSourceNode::start(when); - vReadIndex_ = static_cast(context_->getSampleRate() * offset); + if (offset >= 0.0) { + vReadIndex_ = static_cast(context_->getSampleRate() * offset); + } } void AudioBufferQueueSourceNode::stop(double when) { diff --git a/packages/react-native-audio-api/common/cpp/audioapi/core/sources/AudioBufferQueueSourceNode.h b/packages/react-native-audio-api/common/cpp/audioapi/core/sources/AudioBufferQueueSourceNode.h index 2cc28fb3..b315bcc9 100644 --- a/packages/react-native-audio-api/common/cpp/audioapi/core/sources/AudioBufferQueueSourceNode.h +++ b/packages/react-native-audio-api/common/cpp/audioapi/core/sources/AudioBufferQueueSourceNode.h @@ -23,7 +23,6 @@ class AudioBufferQueueSourceNode : public AudioScheduledSourceNode { [[nodiscard]] std::shared_ptr getDetuneParam() const; [[nodiscard]] std::shared_ptr getPlaybackRateParam() const; - void start(double when) override; void start(double when, double offset); void stop(double when) override; void pause(); diff --git a/packages/react-native-audio-api/common/cpp/audioapi/core/sources/AudioScheduledSourceNode.h b/packages/react-native-audio-api/common/cpp/audioapi/core/sources/AudioScheduledSourceNode.h index cdea0693..80ac6531 100644 --- a/packages/react-native-audio-api/common/cpp/audioapi/core/sources/AudioScheduledSourceNode.h +++ b/packages/react-native-audio-api/common/cpp/audioapi/core/sources/AudioScheduledSourceNode.h @@ -28,7 +28,7 @@ class AudioScheduledSourceNode : public AudioNode { enum class PlaybackState { UNSCHEDULED, SCHEDULED, PLAYING, STOP_SCHEDULED, FINISHED }; explicit AudioScheduledSourceNode(BaseAudioContext *context); - virtual void start(double when); + void start(double when); virtual void stop(double when); bool isUnscheduled(); From 3d0221d7d48f2430ad27627c7acd7938b4f783b7 Mon Sep 17 00:00:00 2001 From: michal Date: Tue, 10 Jun 2025 11:38:38 +0200 Subject: [PATCH 12/33] feat: position event in audio buffer --- .../src/examples/AudioFile/AudioFile.tsx | 7 +++-- .../AudioBufferSourceNodeHostObject.h | 18 +++++++++++- .../core/sources/AudioBufferSourceNode.cpp | 28 ++++++++++++++++++- .../core/sources/AudioBufferSourceNode.h | 9 ++++++ .../src/core/AudioBufferQueueSourceNode.ts | 8 ++++-- .../src/core/AudioBufferSourceNode.ts | 17 +++++++++++ .../src/events/types.ts | 6 ++++ .../react-native-audio-api/src/interfaces.ts | 7 ++++- 8 files changed, 92 insertions(+), 8 deletions(-) diff --git a/apps/common-app/src/examples/AudioFile/AudioFile.tsx b/apps/common-app/src/examples/AudioFile/AudioFile.tsx index 7c1e10dc..4092d67c 100644 --- a/apps/common-app/src/examples/AudioFile/AudioFile.tsx +++ b/apps/common-app/src/examples/AudioFile/AudioFile.tsx @@ -8,6 +8,7 @@ import { } from 'react-native-audio-api'; import { Container, Button, Spacer, Slider } from '../../components'; +import { EventTypeWithValue } from 'react-native-audio-api/lib/typescript/events/types'; const URL = 'https://software-mansion.github.io/react-native-audio-api/audio/voice/example-voice-01.mp3'; @@ -83,7 +84,10 @@ const AudioFile: FC = () => { bufferSourceRef.current.playbackRate.value = playbackRate; bufferSourceRef.current.detune.value = detune; bufferSourceRef.current.connect(audioContextRef.current.destination); - + bufferSourceRef.current.onPositionChanged = (event: EventTypeWithValue) => { + console.log('onPositionChanged event:', event); + }; + // bufferSourceRef.current.onPositionChangedInterval = 200; bufferSourceRef.current.start( audioContextRef.current.currentTime, offset @@ -165,7 +169,6 @@ const AudioFile: FC = () => { remoteChangePlaybackPositionSubscription?.remove(); interruptionSubscription?.remove(); audioContextRef.current?.close(); - AudioManager.resetLockScreenInfo(); }; }, [fetchAudioBuffer]); diff --git a/packages/react-native-audio-api/common/cpp/audioapi/HostObjects/AudioBufferSourceNodeHostObject.h b/packages/react-native-audio-api/common/cpp/audioapi/HostObjects/AudioBufferSourceNodeHostObject.h index a6129228..3f734723 100644 --- a/packages/react-native-audio-api/common/cpp/audioapi/HostObjects/AudioBufferSourceNodeHostObject.h +++ b/packages/react-native-audio-api/common/cpp/audioapi/HostObjects/AudioBufferSourceNodeHostObject.h @@ -29,7 +29,9 @@ class AudioBufferSourceNodeHostObject JSI_EXPORT_PROPERTY_SETTER(AudioBufferSourceNodeHostObject, loop), JSI_EXPORT_PROPERTY_SETTER(AudioBufferSourceNodeHostObject, buffer), JSI_EXPORT_PROPERTY_SETTER(AudioBufferSourceNodeHostObject, loopStart), - JSI_EXPORT_PROPERTY_SETTER(AudioBufferSourceNodeHostObject, loopEnd)); + JSI_EXPORT_PROPERTY_SETTER(AudioBufferSourceNodeHostObject, loopEnd), + JSI_EXPORT_PROPERTY_SETTER(AudioBufferSourceNodeHostObject, onPositionChanged), + JSI_EXPORT_PROPERTY_SETTER(AudioBufferSourceNodeHostObject, onPositionChangedInterval)); // start method is overridden in this class functions_->erase("start"); @@ -120,6 +122,20 @@ class AudioBufferSourceNodeHostObject audioBufferSourceNode->setLoopEnd(value.getNumber()); } + JSI_PROPERTY_SETTER(onPositionChanged) { + auto audioBufferSourceNode = + std::static_pointer_cast(node_); + + audioBufferSourceNode->setOnPositionChangedCallbackId(std::stoull(value.getString(runtime).utf8(runtime))); + } + + JSI_PROPERTY_SETTER(onPositionChangedInterval) { + auto audioBufferSourceNode = + std::static_pointer_cast(node_); + + audioBufferSourceNode->setOnPositionChangedInterval(value.getNumber()); + } + JSI_HOST_FUNCTION(start) { auto when = args[0].getNumber(); auto offset = args[1].getNumber(); 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 79be9a9d..877985db 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 @@ -4,6 +4,7 @@ #include #include #include +#include #include #include @@ -136,6 +137,30 @@ std::mutex &AudioBufferSourceNode::getBufferLock() { return bufferLock_; } +void AudioBufferSourceNode::sendOnPositionChangedEvent() { + if (onPositionChangedTime_ > onPositionChangedInterval_) { + std::unordered_map body = { + {"value", getStopTime()}}; + + context_->audioEventHandlerRegistry_->invokeHandlerWithEventBody( + "positionChanged", onPositionChangedCallbackId_, body); + + onPositionChangedTime_ = 0; + } + + onPositionChangedTime_ += RENDER_QUANTUM_SIZE; +} + +void AudioBufferSourceNode::setOnPositionChangedInterval(int interval) { + onPositionChangedInterval_ = static_cast( + context_->getSampleRate() * static_cast(interval) / 1000); +} + +void AudioBufferSourceNode::setOnPositionChangedCallbackId( + uint64_t callbackId) { + onPositionChangedCallbackId_ = callbackId; +} + void AudioBufferSourceNode::processNode( const std::shared_ptr &processingBus, int framesToProcess) { @@ -153,6 +178,7 @@ void AudioBufferSourceNode::processNode( } handleStopScheduled(); + sendOnPositionChangedEvent(); } else { processingBus->zero(); } @@ -160,7 +186,7 @@ void AudioBufferSourceNode::processNode( double AudioBufferSourceNode::getStopTime() const { return dsp::sampleFrameToTime( - static_cast(vReadIndex_), alignedBus_->getSampleRate()); + static_cast(vReadIndex_), buffer_->getSampleRate()); } /** 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 79577014..46cd7242 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,10 @@ class AudioBufferSourceNode : public AudioScheduledSourceNode { void start(double when, double offset, double duration = -1); void disable() override; + void setOnPositionChangedCallbackId(uint64_t callbackId); + void setOnPositionChangedInterval(int interval); + void sendOnPositionChangedEvent(); + protected: std::mutex &getBufferLock(); void processNode(const std::shared_ptr& processingBus, int framesToProcess) override; @@ -63,6 +67,11 @@ class AudioBufferSourceNode : public AudioScheduledSourceNode { std::shared_ptr buffer_; std::shared_ptr alignedBus_; + // positionChanged event props: callbackId, update interval in frames, time since last update in frames + uint64_t onPositionChangedCallbackId_ = 0; + int onPositionChangedInterval_; + int onPositionChangedTime_ = 0; + void processWithoutPitchCorrection(const std::shared_ptr &processingBus, int framesToProcess); diff --git a/packages/react-native-audio-api/src/core/AudioBufferQueueSourceNode.ts b/packages/react-native-audio-api/src/core/AudioBufferQueueSourceNode.ts index 3ed6f18b..d3de1229 100644 --- a/packages/react-native-audio-api/src/core/AudioBufferQueueSourceNode.ts +++ b/packages/react-native-audio-api/src/core/AudioBufferQueueSourceNode.ts @@ -4,7 +4,7 @@ import BaseAudioContext from './BaseAudioContext'; import AudioBuffer from './AudioBuffer'; import AudioParam from './AudioParam'; import { RangeError } from '../errors'; -import { EventTypeWithValue } from '../events/types'; +import { OnQueuePositionChangedEventType } from '../events/types'; export default class AudioBufferQueueSourceNode extends AudioScheduledSourceNode { readonly playbackRate: AudioParam; @@ -62,9 +62,11 @@ export default class AudioBufferQueueSourceNode extends AudioScheduledSourceNode } // eslint-disable-next-line accessor-pairs - public set onPositionChanged(callback: (event: EventTypeWithValue) => void) { + public set onPositionChanged( + callback: (event: OnQueuePositionChangedEventType) => void + ) { const subscription = this.audioEventEmitter.addAudioEventListener( - 'positionChanged', + 'queuePositionChanged', callback ); diff --git a/packages/react-native-audio-api/src/core/AudioBufferSourceNode.ts b/packages/react-native-audio-api/src/core/AudioBufferSourceNode.ts index 581d9d7d..cc1ce909 100644 --- a/packages/react-native-audio-api/src/core/AudioBufferSourceNode.ts +++ b/packages/react-native-audio-api/src/core/AudioBufferSourceNode.ts @@ -4,6 +4,7 @@ import BaseAudioContext from './BaseAudioContext'; import AudioBuffer from './AudioBuffer'; import AudioParam from './AudioParam'; import { InvalidStateError, RangeError } from '../errors'; +import { EventTypeWithValue } from '../events/types'; export default class AudioBufferSourceNode extends AudioScheduledSourceNode { readonly playbackRate: AudioParam; @@ -83,4 +84,20 @@ export default class AudioBufferSourceNode extends AudioScheduledSourceNode { this.hasBeenStarted = true; (this.node as IAudioBufferSourceNode).start(when, offset, duration); } + + // eslint-disable-next-line accessor-pairs + public set onPositionChanged(callback: (event: EventTypeWithValue) => void) { + const subscription = this.audioEventEmitter.addAudioEventListener( + 'positionChanged', + callback + ); + + (this.node as IAudioBufferSourceNode).onPositionChanged = + subscription.subscriptionId; + } + + // eslint-disable-next-line accessor-pairs + public set onPositionChangedInterval(value: number) { + (this.node as IAudioBufferSourceNode).onPositionChangedInterval = value; + } } diff --git a/packages/react-native-audio-api/src/events/types.ts b/packages/react-native-audio-api/src/events/types.ts index 7894abb2..b108ff7b 100644 --- a/packages/react-native-audio-api/src/events/types.ts +++ b/packages/react-native-audio-api/src/events/types.ts @@ -12,6 +12,11 @@ export interface OnEndedEventType { bufferId: number | undefined; } +export interface OnQueuePositionChangedEventType { + value: number; + bufferId: number; +} + interface OnInterruptionEventType { type: 'ended' | 'began'; shouldResume: boolean; @@ -60,6 +65,7 @@ interface AudioAPIEvents { ended: OnEndedEventType; audioReady: OnAudioReadyEventType; positionChanged: EventTypeWithValue; + queuePositionChanged: OnQueuePositionChangedEventType; audioError: EventEmptyType; // to change systemStateChanged: EventEmptyType; // to change } diff --git a/packages/react-native-audio-api/src/interfaces.ts b/packages/react-native-audio-api/src/interfaces.ts index 7e098412..cf24e97e 100644 --- a/packages/react-native-audio-api/src/interfaces.ts +++ b/packages/react-native-audio-api/src/interfaces.ts @@ -108,7 +108,12 @@ export interface IAudioBufferSourceNode extends IAudioScheduledSourceNode { detune: IAudioParam; playbackRate: IAudioParam; - start: (when: number, offset?: number, duration?: number) => void; + start: (when?: number, offset?: number, duration?: number) => void; + + // passing subscriptionId(uint_64 in cpp, string in js) to the cpp + onPositionChanged: string; + // set how often the onPositionChanged event is called + onPositionChangedInterval: number; } export interface IAudioBufferQueueSourceNode extends IAudioScheduledSourceNode { From 68f7d41e8496cfd9b6ad314804e68548b3360929 Mon Sep 17 00:00:00 2001 From: michal Date: Tue, 10 Jun 2025 12:26:00 +0200 Subject: [PATCH 13/33] fix: refactor param processing --- .../common/cpp/audioapi/core/AudioParam.cpp | 19 ++++++++++++------- .../common/cpp/audioapi/core/AudioParam.h | 1 + 2 files changed, 13 insertions(+), 7 deletions(-) diff --git a/packages/react-native-audio-api/common/cpp/audioapi/core/AudioParam.cpp b/packages/react-native-audio-api/common/cpp/audioapi/core/AudioParam.cpp index dada1b66..cfc999a9 100644 --- a/packages/react-native-audio-api/common/cpp/audioapi/core/AudioParam.cpp +++ b/packages/react-native-audio-api/common/cpp/audioapi/core/AudioParam.cpp @@ -284,15 +284,21 @@ void AudioParam::removeInputNode(AudioNode *node) { } } -std::shared_ptr AudioParam::processARateParam( - int framesToProcess, - double time) { - auto processingBus = audioBus_; +std::shared_ptr AudioParam::calculateInputs( + const std::shared_ptr &processingBus, + int framesToProcess) { processingBus->zero(); if (!inputNodes_.empty()) { processInputs(processingBus, framesToProcess, true); mixInputsBuses(processingBus); } + return processingBus; +} + +std::shared_ptr AudioParam::processARateParam( + int framesToProcess, + double time) { + auto processingBus = calculateInputs(audioBus_, framesToProcess); for (size_t i = 0; i < framesToProcess; i++) { auto sample = getValueAtTime(time + i / context_->getSampleRate()); processingBus->getChannel(0)->getData()[i] += sample; @@ -302,9 +308,8 @@ std::shared_ptr AudioParam::processARateParam( } float AudioParam::processKRateParam(int framesToProcess, double time) { - auto processingBus = processARateParam(framesToProcess, time); - // processingBus is a mono bus - return processingBus->getChannel(0)->getData()[0]; + auto processingBus = calculateInputs(audioBus_, framesToProcess); + return processingBus->getChannel(0)->getData()[0] + getValueAtTime(time); } double AudioParam::getQueueEndTime() { diff --git a/packages/react-native-audio-api/common/cpp/audioapi/core/AudioParam.h b/packages/react-native-audio-api/common/cpp/audioapi/core/AudioParam.h index ad931f1c..d899518e 100644 --- a/packages/react-native-audio-api/common/cpp/audioapi/core/AudioParam.h +++ b/packages/react-native-audio-api/common/cpp/audioapi/core/AudioParam.h @@ -63,6 +63,7 @@ class AudioParam { void updateQueue(ParamChangeEvent &event); void processInputs(const std::shared_ptr& outputBus, int framesToProcess, bool checkIsAlreadyProcessed); void mixInputsBuses(const std::shared_ptr& processingBus); + std::shared_ptr calculateInputs(const std::shared_ptr& processingBus, int framesToProcess); }; } // namespace audioapi From 27c61fcb75278755780837aad3a3e6008e0b00c6 Mon Sep 17 00:00:00 2001 From: michal Date: Tue, 10 Jun 2025 16:54:11 +0200 Subject: [PATCH 14/33] fix: additional conditions to if that handled looping --- .../core/sources/AudioBufferSourceNode.cpp | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) 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 79be9a9d..0db20bb3 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 @@ -247,8 +247,13 @@ void AudioBufferSourceNode::processWithoutInterpolation( size_t framesLeft = offsetLength; - if (loop_ && (readIndex >= frameEnd || readIndex < frameStart)) { - readIndex = frameStart + (readIndex - frameStart) % frameDelta; + // if we are moving towards loop, we do nothing becuase we will achieve it + // otherwise, we wrap to the start of the loop if necessary + if (loop_ && + ((readIndex >= frameEnd && direction == 1) || + (readIndex < frameStart && direction == -1))) { + readIndex = frameStart + + ((long long int)readIndex - (long long int)frameStart) % frameDelta; } while (framesLeft > 0) { @@ -278,7 +283,10 @@ void AudioBufferSourceNode::processWithoutInterpolation( readIndex += framesToCopy * direction; framesLeft -= framesToCopy; - if (readIndex >= frameEnd || readIndex < frameStart) { + // if we are moving towards loop, we do nothing becuase we will achieve it + // otherwise, we wrap to the start of the loop if necessary + if ((readIndex >= frameEnd && direction == 1) || + (readIndex < frameStart && direction == -1)) { readIndex -= direction * frameDelta; if (!loop_) { From 1f1c27b4f253651816d2200eeb840aa247a96cb0 Mon Sep 17 00:00:00 2001 From: michal Date: Tue, 10 Jun 2025 16:57:23 +0200 Subject: [PATCH 15/33] fix: typo --- .../cpp/audioapi/core/sources/AudioBufferSourceNode.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) 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 0db20bb3..fa6ac4c4 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 @@ -247,7 +247,7 @@ void AudioBufferSourceNode::processWithoutInterpolation( size_t framesLeft = offsetLength; - // if we are moving towards loop, we do nothing becuase we will achieve it + // if we are moving towards loop, we do nothing because we will achieve it // otherwise, we wrap to the start of the loop if necessary if (loop_ && ((readIndex >= frameEnd && direction == 1) || @@ -283,7 +283,7 @@ void AudioBufferSourceNode::processWithoutInterpolation( readIndex += framesToCopy * direction; framesLeft -= framesToCopy; - // if we are moving towards loop, we do nothing becuase we will achieve it + // if we are moving towards loop, we do nothing because we will achieve it // otherwise, we wrap to the start of the loop if necessary if ((readIndex >= frameEnd && direction == 1) || (readIndex < frameStart && direction == -1)) { From 36a8ddc2c00f66d65b206e845d153a688138e1a1 Mon Sep 17 00:00:00 2001 From: michal Date: Wed, 11 Jun 2025 11:07:12 +0200 Subject: [PATCH 16/33] feat: added parameter to steer skipping track if setting loopStart --- .../docs/sources/audio-buffer-source-node.mdx | 1 + .../HostObjects/AudioBufferSourceNodeHostObject.h | 15 +++++++++++++++ .../core/sources/AudioBufferSourceNode.cpp | 11 +++++++++++ .../audioapi/core/sources/AudioBufferSourceNode.h | 3 +++ .../src/core/AudioBufferSourceNode.ts | 8 ++++++++ packages/react-native-audio-api/src/interfaces.ts | 1 + 6 files changed, 39 insertions(+) diff --git a/packages/audiodocs/docs/sources/audio-buffer-source-node.mdx b/packages/audiodocs/docs/sources/audio-buffer-source-node.mdx index 0998a9bd..c0603339 100644 --- a/packages/audiodocs/docs/sources/audio-buffer-source-node.mdx +++ b/packages/audiodocs/docs/sources/audio-buffer-source-node.mdx @@ -28,6 +28,7 @@ However, this node is very inexpensive to create, and what is crucial you can re | `buffer` | [`AudioBuffer`](/sources/audio-buffer) | Associated `AudioBuffer`. | | `detune` | [`AudioParam`](/core/audio-param) | [`k-rate`](/core/audio-param#a-rate-vs-k-rate) `AudioParam` representing detuning of oscillation in cents. | | `loop` | `boolean` | Boolean indicating if audio data must be replayed after when end of the associated `AudioBuffer` is reached. | +| `loopSkip` | `boolean` | Boolean indicating if upon setting up `loopStart` we want to skip immediately to the loop start. | | `loopStart` | `number` | Float value indicating the time, in seconds, at which playback of the audio must begin, if loop is true. | | `loopEnd` | `number` | Float value indicating the time, in seconds, at which playback of the audio must end and loop back to `loopStart`, if loop is true. | | `playbackRate` | [`AudioParam`](/core/audio-param) | [`k-rate`](/core/audio-param#a-rate-vs-k-rate) `AudioParam` defining speed factor at which the audio will be played. | diff --git a/packages/react-native-audio-api/common/cpp/audioapi/HostObjects/AudioBufferSourceNodeHostObject.h b/packages/react-native-audio-api/common/cpp/audioapi/HostObjects/AudioBufferSourceNodeHostObject.h index a6129228..d7ae6c0c 100644 --- a/packages/react-native-audio-api/common/cpp/audioapi/HostObjects/AudioBufferSourceNodeHostObject.h +++ b/packages/react-native-audio-api/common/cpp/audioapi/HostObjects/AudioBufferSourceNodeHostObject.h @@ -19,6 +19,7 @@ class AudioBufferSourceNodeHostObject : AudioScheduledSourceNodeHostObject(node) { addGetters( JSI_EXPORT_PROPERTY_GETTER(AudioBufferSourceNodeHostObject, loop), + JSI_EXPORT_PROPERTY_GETTER(AudioBufferSourceNodeHostObject, loopSkip), JSI_EXPORT_PROPERTY_GETTER(AudioBufferSourceNodeHostObject, buffer), JSI_EXPORT_PROPERTY_GETTER(AudioBufferSourceNodeHostObject, loopStart), JSI_EXPORT_PROPERTY_GETTER(AudioBufferSourceNodeHostObject, loopEnd), @@ -27,6 +28,7 @@ class AudioBufferSourceNodeHostObject addSetters( JSI_EXPORT_PROPERTY_SETTER(AudioBufferSourceNodeHostObject, loop), + JSI_EXPORT_PROPERTY_SETTER(AudioBufferSourceNodeHostObject, loopSkip), JSI_EXPORT_PROPERTY_SETTER(AudioBufferSourceNodeHostObject, buffer), JSI_EXPORT_PROPERTY_SETTER(AudioBufferSourceNodeHostObject, loopStart), JSI_EXPORT_PROPERTY_SETTER(AudioBufferSourceNodeHostObject, loopEnd)); @@ -45,6 +47,13 @@ class AudioBufferSourceNodeHostObject return {loop}; } + JSI_PROPERTY_GETTER(loopSkip) { + auto audioBufferSourceNode = + std::static_pointer_cast(node_); + auto loopSkip = audioBufferSourceNode->getLoopSkip(); + return {loopSkip}; + } + JSI_PROPERTY_GETTER(buffer) { auto audioBufferSourceNode = std::static_pointer_cast(node_); @@ -95,6 +104,12 @@ class AudioBufferSourceNodeHostObject audioBufferSourceNode->setLoop(value.getBool()); } + JSI_PROPERTY_SETTER(loopSkip) { + auto audioBufferSourceNode = + std::static_pointer_cast(node_); + audioBufferSourceNode->setLoopSkip(value.getBool()); + } + JSI_PROPERTY_SETTER(buffer) { auto audioBufferSourceNode = std::static_pointer_cast(node_); 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 fa6ac4c4..041c0ffc 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 @@ -46,6 +46,10 @@ bool AudioBufferSourceNode::getLoop() const { return loop_; } +bool AudioBufferSourceNode::getLoopSkip() const { + return loopSkip_; +} + double AudioBufferSourceNode::getLoopStart() const { return loopStart_; } @@ -71,7 +75,14 @@ void AudioBufferSourceNode::setLoop(bool loop) { loop_ = loop; } +void AudioBufferSourceNode::setLoopSkip(bool loopSkip) { + loopSkip_ = loopSkip; +} + void AudioBufferSourceNode::setLoopStart(double loopStart) { + if (loopSkip_) { + vReadIndex_ = loopStart * context_->getSampleRate(); + } loopStart_ = loopStart; } 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 79577014..f431a672 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 @@ -20,6 +20,7 @@ class AudioBufferSourceNode : public AudioScheduledSourceNode { ~AudioBufferSourceNode() override; [[nodiscard]] bool getLoop() const; + [[nodiscard]] bool getLoopSkip() const; [[nodiscard]] double getLoopStart() const; [[nodiscard]] double getLoopEnd() const; [[nodiscard]] std::shared_ptr getDetuneParam() const; @@ -27,6 +28,7 @@ class AudioBufferSourceNode : public AudioScheduledSourceNode { [[nodiscard]] std::shared_ptr getBuffer() const; void setLoop(bool loop); + void setLoopSkip(bool loopSkip); void setLoopStart(double loopStart); void setLoopEnd(double loopEnd); void setBuffer(const std::shared_ptr &buffer); @@ -42,6 +44,7 @@ class AudioBufferSourceNode : public AudioScheduledSourceNode { private: // Looping related properties bool loop_; + bool loopSkip_; double loopStart_; double loopEnd_; std::mutex bufferLock_; diff --git a/packages/react-native-audio-api/src/core/AudioBufferSourceNode.ts b/packages/react-native-audio-api/src/core/AudioBufferSourceNode.ts index 581d9d7d..ef098094 100644 --- a/packages/react-native-audio-api/src/core/AudioBufferSourceNode.ts +++ b/packages/react-native-audio-api/src/core/AudioBufferSourceNode.ts @@ -33,6 +33,14 @@ export default class AudioBufferSourceNode extends AudioScheduledSourceNode { (this.node as IAudioBufferSourceNode).buffer = buffer.buffer; } + public get loopSkip(): boolean { + return (this.node as IAudioBufferSourceNode).loopSkip; + } + + public set loopSkip(value: boolean) { + (this.node as IAudioBufferSourceNode).loopSkip = value; + } + public get loop(): boolean { return (this.node as IAudioBufferSourceNode).loop; } diff --git a/packages/react-native-audio-api/src/interfaces.ts b/packages/react-native-audio-api/src/interfaces.ts index 7e098412..d6967dda 100644 --- a/packages/react-native-audio-api/src/interfaces.ts +++ b/packages/react-native-audio-api/src/interfaces.ts @@ -103,6 +103,7 @@ export interface IOscillatorNode extends IAudioScheduledSourceNode { export interface IAudioBufferSourceNode extends IAudioScheduledSourceNode { buffer: IAudioBuffer | null; loop: boolean; + loopSkip: boolean; loopStart: number; loopEnd: number; detune: IAudioParam; From 3a014a9bfbf01cd30fed1b268e5aac80f912818f Mon Sep 17 00:00:00 2001 From: michal Date: Wed, 11 Jun 2025 11:07:32 +0200 Subject: [PATCH 17/33] fix: format --- .../react-native-audio-api/src/core/AudioBufferSourceNode.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/react-native-audio-api/src/core/AudioBufferSourceNode.ts b/packages/react-native-audio-api/src/core/AudioBufferSourceNode.ts index ef098094..843271ce 100644 --- a/packages/react-native-audio-api/src/core/AudioBufferSourceNode.ts +++ b/packages/react-native-audio-api/src/core/AudioBufferSourceNode.ts @@ -36,7 +36,7 @@ export default class AudioBufferSourceNode extends AudioScheduledSourceNode { public get loopSkip(): boolean { return (this.node as IAudioBufferSourceNode).loopSkip; } - + public set loopSkip(value: boolean) { (this.node as IAudioBufferSourceNode).loopSkip = value; } From 2cb06623194902f8f4b4a365cce95571abc7dbd8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Dydek?= <54865962+mdydek@users.noreply.github.com> Date: Thu, 12 Jun 2025 13:34:19 +0200 Subject: [PATCH 18/33] docs: integrated few-line pages into their main objects (#501) --- .../audiodocs/docs/core/audio-context.mdx | 18 ++++++- .../docs/core/base-audio-context.mdx | 51 +++++++++++++++++-- .../audio-buffer-source-node-options.mdx | 13 ----- .../docs/types/audio-context-options.mdx | 13 ----- .../audiodocs/docs/types/context-state.mdx | 20 -------- .../docs/types/periodic-wave-constraints.mdx | 14 ----- packages/audiodocs/src/css/colors.css | 2 +- 7 files changed, 66 insertions(+), 65 deletions(-) delete mode 100644 packages/audiodocs/docs/types/audio-buffer-source-node-options.mdx delete mode 100644 packages/audiodocs/docs/types/audio-context-options.mdx delete mode 100644 packages/audiodocs/docs/types/context-state.mdx delete mode 100644 packages/audiodocs/docs/types/periodic-wave-constraints.mdx diff --git a/packages/audiodocs/docs/core/audio-context.mdx b/packages/audiodocs/docs/core/audio-context.mdx index e9913cec..be313bd2 100644 --- a/packages/audiodocs/docs/core/audio-context.mdx +++ b/packages/audiodocs/docs/core/audio-context.mdx @@ -11,7 +11,7 @@ It is responsible for supervising and managing audio-processing graph. `new AudioContext()` -[`new AudioContext(options: AudioContextOptions)`](/types/audio-context-options) +[`new AudioContext(options: AudioContextOptions)`](/core/audio-context#audiocontextoptions) #### Errors @@ -39,3 +39,19 @@ It is useful when your application will not use audio for a while. The above method lets resume time progression in audio context that previously has been suspended. #### Returns `Promise`. + + +## Remarks + +### `AudioContextOptions` + +
+ +`AudioContextOptions` is a dictionary object specifies sample rate for the new context. + +```jsx +interface AudioContextOptions { + sampleRate: number; +} +``` +
\ No newline at end of file diff --git a/packages/audiodocs/docs/core/base-audio-context.mdx b/packages/audiodocs/docs/core/base-audio-context.mdx index 600352c3..bbda9d84 100644 --- a/packages/audiodocs/docs/core/base-audio-context.mdx +++ b/packages/audiodocs/docs/core/base-audio-context.mdx @@ -42,7 +42,7 @@ Concept of system-level audio callback does not apply to `OfflineAudioContext`. | `currentTime` | `number` | Double value representing an ever-increasing hardware time in seconds, starting from 0. | | | `destination` | `AudioDestinationNode` | Final output destination associated with the context. | | | `sampleRate` | `number` | Float value representing the sample rate (in samples per seconds) used by all nodes in this context. | | -| `state` | [`ContextState`](/types/context-state) | Enumerated value represents the current state of the context. | | +| `state` | [`ContextState`](/core/base-audio-context#contextstate) | Enumerated value represents the current state of the context. | | ## Methods @@ -84,7 +84,7 @@ The above method lets you create [`AudioBufferSourceNode`](/sources/audio-buffer | Parameters | Type | Description | | :---: | :---: | :---- | -| `pitchCorrection` | [`AudioBufferSourceNodeOptions`](/types/audio-buffer-source-node-options) | Dictionary object that specifies if pitch correction has to be available. | +| `pitchCorrection` | [`AudioBufferSourceNodeOptions`](/core/base-audio-context#audiobuffersourcenodeoptions) | Dictionary object that specifies if pitch correction has to be available. | #### Returns `AudioBufferSourceNode`. @@ -108,7 +108,7 @@ The above method lets you create `PeriodicWave`. | :---: | :---: | :---- | | `real` | `Float32Array` | An array of cosine terms. | | `imag` | `Float32Array` | An array of sine terms. | -| `constraints` | [`PeriodicWaveConstraints`](/types/periodic-wave-constraints) | An object that specifies if normalization is disabled. | +| `constraints` | [`PeriodicWaveConstraints`](/core/base-audio-context#periodicwaveconstraints) | An object that specifies if normalization is disabled. | #### Errors @@ -149,3 +149,48 @@ The above method lets you decode audio data file. It saves file in the device fi #### `currentTime` - Timer starts when context is created, stops when context is suspended. + +### `ContextState` + +
+ +**Acceptable values:** + - `suspended` + + The audio context has been suspended (with one of [`suspend`](/core/audio-context#suspend) or `OfflineAudioContext.suspend`). + + - `running` + + The audio context is running normally. + + - `closed` + + The audio context has been closed (with [`close`](/core/audio-context#close) method). +
+ +### `AudioBufferSourceNodeOptions` + +
+ +`AudioBufferSourceNodeOptions` is a dictionary object specifies if pitch correction algorithm has to be available. + +```jsx +interface AudioBufferSourceNodeOptions { + pitchCorrection: boolean +} +``` +
+ +### `PeriodicWaveConstraints` + +
+ +`PeriodicWaveConstraints` is a dictionary object specifies whether normalization should be disabled during creating periodic wave. If not specified normalization is enabled. +If normalized, periodic wave will have maximum peak value of 1 and minimum peak value of -1. + +```jsx +interface PeriodicWaveConstraints { + disableNormalization: boolean; +} +``` +
\ No newline at end of file diff --git a/packages/audiodocs/docs/types/audio-buffer-source-node-options.mdx b/packages/audiodocs/docs/types/audio-buffer-source-node-options.mdx deleted file mode 100644 index fff7dbf5..00000000 --- a/packages/audiodocs/docs/types/audio-buffer-source-node-options.mdx +++ /dev/null @@ -1,13 +0,0 @@ ---- -sidebar_position: 1 ---- - -# AudioBufferSourceNodeOptions - -`AudioBufferSourceNodeOptions` is a dictionary object specifies if pitch correction algorithm has to be available. - -```jsx -interface AudioBufferSourceNodeOptions { - pitchCorrection: boolean -} -``` diff --git a/packages/audiodocs/docs/types/audio-context-options.mdx b/packages/audiodocs/docs/types/audio-context-options.mdx deleted file mode 100644 index 5f4172f4..00000000 --- a/packages/audiodocs/docs/types/audio-context-options.mdx +++ /dev/null @@ -1,13 +0,0 @@ ---- -sidebar_position: 2 ---- - -# AudioContextOptions - -`AudioContextOptions` is a dictionary object specifies sample rate for the new context. - -```jsx -interface AudioContextOptions { - sampleRate: number; -} -``` diff --git a/packages/audiodocs/docs/types/context-state.mdx b/packages/audiodocs/docs/types/context-state.mdx deleted file mode 100644 index a68e213c..00000000 --- a/packages/audiodocs/docs/types/context-state.mdx +++ /dev/null @@ -1,20 +0,0 @@ ---- -sidebar_position: 5 ---- - -# ContextState - -`ContextState` type represents state of the [`BaseAudioContext`](/core/base-audio-context). - -**Acceptable values:** - - `suspended` - - The audio context has been suspended (with one of [`suspend`](/core/audio-context#suspend) or `OfflineAudioContext.suspend`). - - - `running` - - The audio context is running normally. - - - `closed` - - The audio context has been closed (with [`close`](/core/audio-context#close) method). diff --git a/packages/audiodocs/docs/types/periodic-wave-constraints.mdx b/packages/audiodocs/docs/types/periodic-wave-constraints.mdx deleted file mode 100644 index c9092e40..00000000 --- a/packages/audiodocs/docs/types/periodic-wave-constraints.mdx +++ /dev/null @@ -1,14 +0,0 @@ ---- -sidebar_position: 7 ---- - -# PeriodicWaveConstraints - -`PeriodicWaveConstraints` is a dictionary object specifies whether normalization should be disabled during creating periodic wave. If not specified normalization is enabled. -If normalized, periodic wave will have maximum peak value of 1 and minimum peak value of -1. - -```jsx -interface PeriodicWaveConstraints { - disableNormalization: boolean; -} -``` diff --git a/packages/audiodocs/src/css/colors.css b/packages/audiodocs/src/css/colors.css index bd73bca9..8338e891 100644 --- a/packages/audiodocs/src/css/colors.css +++ b/packages/audiodocs/src/css/colors.css @@ -366,7 +366,7 @@ --swm-background-quote-green: var(--swm-green-light-40); --swm-background-quote-red: var(--swm-red-light-40); --swm-background-quote-yellow: var(--swm-yellow-dark-140); - --swm-background-quote-purple: var(--swm-purple-light-40); + --swm-background-quote-purple: var(--swm-purple-light-80); /* Code snippets */ --swm-border: var(--swm-navy-light-60); From 0c6ba163490b2e14414e889dff34b5b622d1a7e9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Dydek?= <54865962+mdydek@users.noreply.github.com> Date: Thu, 12 Jun 2025 13:37:34 +0200 Subject: [PATCH 19/33] docs: added docs for audio recorder (#502) Co-authored-by: Maciej Makowski <120780663+maciejmakowski2003@users.noreply.github.com> --- .../audiodocs/docs/inputs/_category_.json | 7 ++ .../audiodocs/docs/inputs/audio-recorder.mdx | 90 +++++++++++++++++++ .../audiodocs/docs/sources/_category_.json | 2 +- packages/audiodocs/src/css/colors.css | 4 +- 4 files changed, 100 insertions(+), 3 deletions(-) create mode 100644 packages/audiodocs/docs/inputs/_category_.json create mode 100644 packages/audiodocs/docs/inputs/audio-recorder.mdx diff --git a/packages/audiodocs/docs/inputs/_category_.json b/packages/audiodocs/docs/inputs/_category_.json new file mode 100644 index 00000000..e87e9fbd --- /dev/null +++ b/packages/audiodocs/docs/inputs/_category_.json @@ -0,0 +1,7 @@ +{ + "label": "Inputs", + "position": 4, + "link": { + "type": "generated-index" + } +} diff --git a/packages/audiodocs/docs/inputs/audio-recorder.mdx b/packages/audiodocs/docs/inputs/audio-recorder.mdx new file mode 100644 index 00000000..14f53406 --- /dev/null +++ b/packages/audiodocs/docs/inputs/audio-recorder.mdx @@ -0,0 +1,90 @@ +--- +sidebar_position: 4 +--- + +import AudioNodePropsTable from "@site/src/components/AudioNodePropsTable" +import { Optional, ReadOnly } from '@site/src/components/Badges'; + +# AudioRecorder + +## Constructor + +[`AudioRecorder(options)`](/inputs/audio-recorder#audiorecorderoptions) + +## Example + +```tsx +import { AudioRecorder } from 'react-native-audio-api'; + +function App() { + const recorder = new AudioRecorder({ + sampleRate: 16000, + bufferLengthInSamples: 16000, + }); + + recorder.onAudioReady((event) => { + const { buffer, numFrames, when } = event; + + console.log( + 'Audio recorder buffer ready:', + buffer.duration, + numFrames, + when + ); + }); + + recorder.start(); +} +``` + +## Methods + +### `start` + +The above starts recording. + +#### Returns `undefined`. + +### `stop` + +The above stops recording. + +#### Returns `undefined`. + + +### `onAudioReady` + +The above allows user to set a callback after every portion of data deliverance. + +| Parameters | Type | Description | +| :---: | :---: | :---- | +| `callback` | [(OnAudioReadyEventType => void)](/inputs/audio-recorder#onaudioreadyeventtype) | callback that will be invoked | + +#### Returns `undefined`. + +## Remarks + +### `AudioRecorderOptions` + +
+Type definitions +```typescript +interface AudioRecorderOptions { + sampleRate: number; + bufferLengthInSamples: number; //how many samples to be put in the buffer +} +``` +
+ +### `OnAudioReadyEventType` + +
+Type definitions +```typescript +interface OnAudioReadyEventType { + buffer: AudioBuffer; + numFrames: number; //number of frames in a buffer + when: number; //timestamp +} +``` +
diff --git a/packages/audiodocs/docs/sources/_category_.json b/packages/audiodocs/docs/sources/_category_.json index c3572ca7..7e94200d 100644 --- a/packages/audiodocs/docs/sources/_category_.json +++ b/packages/audiodocs/docs/sources/_category_.json @@ -1,6 +1,6 @@ { "label": "Sources", - "position": 4, + "position": 3, "link": { "type": "generated-index" } diff --git a/packages/audiodocs/src/css/colors.css b/packages/audiodocs/src/css/colors.css index 8338e891..161f1c02 100644 --- a/packages/audiodocs/src/css/colors.css +++ b/packages/audiodocs/src/css/colors.css @@ -176,7 +176,7 @@ --swm-background-quote-blue: var(--swm-blue-light-40); --swm-background-quote-green: var(--swm-green-light-40); --swm-background-quote-red: var(--swm-red-light-40); - --swm-background-quote-yellow: var(--swm-yellow-light-40); + --swm-background-quote-yellow: var(--swm-blue-light-40); --swm-background-quote-purple: var(--swm-purple-light-40); /* Code snippets */ @@ -365,7 +365,7 @@ --swm-background-quote-blue: var(--swm-blue-dark-140); --swm-background-quote-green: var(--swm-green-light-40); --swm-background-quote-red: var(--swm-red-light-40); - --swm-background-quote-yellow: var(--swm-yellow-dark-140); + --swm-background-quote-yellow: var(--swm-navy-light-60); --swm-background-quote-purple: var(--swm-purple-light-80); /* Code snippets */ From 6b5efc6b60a0b3d20732779460a65ca8eefe8af9 Mon Sep 17 00:00:00 2001 From: Maciej Makowski <120780663+maciejmakowski2003@users.noreply.github.com> Date: Thu, 12 Jun 2025 20:01:10 +0200 Subject: [PATCH 20/33] Fix/android/lock screen info (#503) * refactor: refactored AudioFile example * fix: fixed setting artwork * fix: fixed ABQSN onPositionChange event type --------- Co-authored-by: Maciej Makowski --- .../src/examples/AudioFile/AudioFile.tsx | 162 ++++-------------- .../src/examples/AudioFile/AudioPlayer.ts | 115 +++++++++++++ .../audioapi/system/LockScreenManager.kt | 3 - .../system/MediaNotificationManager.kt | 29 +--- .../src/core/AudioBufferQueueSourceNode.ts | 8 +- .../src/events/types.ts | 6 - 6 files changed, 152 insertions(+), 171 deletions(-) create mode 100644 apps/common-app/src/examples/AudioFile/AudioPlayer.ts diff --git a/apps/common-app/src/examples/AudioFile/AudioFile.tsx b/apps/common-app/src/examples/AudioFile/AudioFile.tsx index 4092d67c..48557c49 100644 --- a/apps/common-app/src/examples/AudioFile/AudioFile.tsx +++ b/apps/common-app/src/examples/AudioFile/AudioFile.tsx @@ -1,97 +1,23 @@ -import React, { useCallback, useEffect, useRef, useState, FC } from 'react'; +import React, { useCallback, useEffect, useState, FC } from 'react'; import { ActivityIndicator } from 'react-native'; -import { - AudioBuffer, - AudioContext, - AudioBufferSourceNode, - AudioManager, -} from 'react-native-audio-api'; - -import { Container, Button, Spacer, Slider } from '../../components'; -import { EventTypeWithValue } from 'react-native-audio-api/lib/typescript/events/types'; +import { AudioManager } from 'react-native-audio-api'; +import { Container, Button } from '../../components'; +import AudioPlayer from './AudioPlayer'; const URL = 'https://software-mansion.github.io/react-native-audio-api/audio/voice/example-voice-01.mp3'; -const INITIAL_RATE = 1; -const INITIAL_DETUNE = 0; - -const labelWidth = 80; - const AudioFile: FC = () => { const [isPlaying, setIsPlaying] = useState(false); const [isLoading, setIsLoading] = useState(false); - const [offset, setOffset] = useState(0); - const [playbackRate, setPlaybackRate] = useState(INITIAL_RATE); - const [detune, setDetune] = useState(INITIAL_DETUNE); - - const [audioBuffer, setAudioBuffer] = useState(null); - - const audioContextRef = useRef(null); - const bufferSourceRef = useRef(null); - - const handlePlaybackRateChange = (newValue: number) => { - setPlaybackRate(newValue); - - if (bufferSourceRef.current) { - bufferSourceRef.current.playbackRate.value = newValue; - } - }; - - const handleDetuneChange = (newValue: number) => { - setDetune(newValue); - - if (bufferSourceRef.current) { - bufferSourceRef.current.detune.value = newValue; - } - }; - - const handlePress = async () => { - if (!audioContextRef.current) { - return; - } - + const togglePlayPause = async () => { if (isPlaying) { - bufferSourceRef.current?.stop(audioContextRef.current.currentTime); - AudioManager.setLockScreenInfo({ - state: 'state_paused', - }); - - setTimeout(async () => { - await audioContextRef.current?.suspend(); - }, 5); + AudioPlayer.pause(); } else { - if (!audioBuffer) { - fetchAudioBuffer(); - } - - await audioContextRef.current.resume(); - - AudioManager.setLockScreenInfo({ - state: 'state_playing', - }); + await AudioPlayer.play(); AudioManager.observeAudioInterruptions(true); - - bufferSourceRef.current = audioContextRef.current.createBufferSource({ - pitchCorrection: true, - }); - bufferSourceRef.current.buffer = audioBuffer; - bufferSourceRef.current.onended = (event) => { - setOffset((_prev) => event.value || 0); - }; - bufferSourceRef.current.playbackRate.value = playbackRate; - bufferSourceRef.current.detune.value = detune; - bufferSourceRef.current.connect(audioContextRef.current.destination); - bufferSourceRef.current.onPositionChanged = (event: EventTypeWithValue) => { - console.log('onPositionChanged event:', event); - }; - // bufferSourceRef.current.onPositionChangedInterval = 200; - bufferSourceRef.current.start( - audioContextRef.current.currentTime, - offset - ); } setIsPlaying((prev) => !prev); @@ -100,26 +26,12 @@ const AudioFile: FC = () => { const fetchAudioBuffer = useCallback(async () => { setIsLoading(true); - const buffer = await fetch(URL) - .then((response) => response.arrayBuffer()) - .then((arrayBuffer) => - audioContextRef.current!.decodeAudioData(arrayBuffer) - ) - .catch((error) => { - console.error('Error decoding audio data source:', error); - return null; - }); - - setAudioBuffer(buffer); + await AudioPlayer.loadBuffer(URL); setIsLoading(false); }, []); useEffect(() => { - if (!audioContextRef.current) { - audioContextRef.current = new AudioContext({ initSuspended: true }); - } - AudioManager.setLockScreenInfo({ title: 'Audio file', artist: 'Software Mansion', @@ -129,30 +41,37 @@ const AudioFile: FC = () => { AudioManager.enableRemoteCommand('remotePlay', true); AudioManager.enableRemoteCommand('remotePause', true); - AudioManager.enableRemoteCommand('remoteChangePlaybackPosition', true); + AudioManager.enableRemoteCommand('remoteSkipForward', true); + AudioManager.enableRemoteCommand('remoteSkipBackward', true); AudioManager.observeAudioInterruptions(true); const remotePlaySubscription = AudioManager.addSystemEventListener( 'remotePlay', - (event) => { - console.log('remotePlay event:', event); + () => { + AudioPlayer.play(); } ); const remotePauseSubscription = AudioManager.addSystemEventListener( 'remotePause', + () => { + AudioPlayer.pause(); + } + ); + + const remoteSkipForwardSubscription = AudioManager.addSystemEventListener( + 'remoteSkipForward', (event) => { - console.log('remotePause event:', event); + AudioPlayer.seekBy(event.value); } ); - const remoteChangePlaybackPositionSubscription = - AudioManager.addSystemEventListener( - 'remoteChangePlaybackPosition', - (event) => { - console.log('remoteChangePlaybackPosition event:', event); - } - ); + const remoteSkipBackwardSubscription = AudioManager.addSystemEventListener( + 'remoteSkipBackward', + (event) => { + AudioPlayer.seekBy(-event.value); + } + ); const interruptionSubscription = AudioManager.addSystemEventListener( 'interruption', @@ -166,9 +85,10 @@ const AudioFile: FC = () => { return () => { remotePlaySubscription?.remove(); remotePauseSubscription?.remove(); - remoteChangePlaybackPositionSubscription?.remove(); + remoteSkipForwardSubscription?.remove(); + remoteSkipBackwardSubscription?.remove(); interruptionSubscription?.remove(); - audioContextRef.current?.close(); + AudioManager.resetLockScreenInfo(); }; }, [fetchAudioBuffer]); @@ -177,28 +97,8 @@ const AudioFile: FC = () => { {isLoading && }