Skip to content

Commit

Permalink
fix: fixed bluetooth recording for simultaneous (#7)
Browse files Browse the repository at this point in the history
  • Loading branch information
demchuk-alex authored Jan 7, 2025
1 parent 9dd8fad commit d9eb1ea
Show file tree
Hide file tree
Showing 10 changed files with 158 additions and 56 deletions.
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,7 @@ const audioSubscription = ExpoPlayAudioStream.subscribeToAudioEvents(async (even
// audioSubscription.remove();
```
### Simultaneous Recording and Playback
### Simultaneous Recording and Playback (⚠️ IOS only for now)
```javascript
import { ExpoPlayAudioStream } from 'expo-audio-stream';
Expand Down
21 changes: 18 additions & 3 deletions example/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,21 @@ export default function App() {
console.log(audio.data.slice(0, 100));
};

const playEventsListenerSubscriptionRef = useRef<Subscription | undefined>(undefined);

useEffect(() => {
playEventsListenerSubscriptionRef.current = ExpoPlayAudioStream.subscribeToSoundChunkPlayed(async (event) => {
console.log(event);
});

return () => {
if (playEventsListenerSubscriptionRef.current) {
playEventsListenerSubscriptionRef.current.remove();
playEventsListenerSubscriptionRef.current = undefined;
}
}
}, []);

return (
<View style={styles.container}>
<Text>hi</Text>
Expand All @@ -51,7 +66,7 @@ export default function App() {
</View>
<Button
onPress={async () => {
await ExpoPlayAudioStream.playAudio(sampleA, turnId2);
await ExpoPlayAudioStream.playSound(sampleA, turnId2);
}}
title="Play sample A"
/>
Expand All @@ -68,7 +83,7 @@ export default function App() {
}
const sampleRate =
Platform.OS === "ios" ? IOS_SAMPLE_RATE : ANDROID_SAMPLE_RATE;
const { recordingResult, subscription } = await ExpoPlayAudioStream.startRecording({
const { recordingResult, subscription } = await ExpoPlayAudioStream.startMicrophone({
interval: RECORDING_INTERVAL,
sampleRate,
channels: CHANNELS,
Expand All @@ -86,7 +101,7 @@ export default function App() {
<Button
onPress={async () => {

await ExpoPlayAudioStream.stopRecording();
await ExpoPlayAudioStream.stopMicrophone();
if (eventListenerSubscriptionRef.current) {
eventListenerSubscriptionRef.current.remove();
eventListenerSubscriptionRef.current = undefined;
Expand Down
8 changes: 4 additions & 4 deletions example/ios/Podfile.lock
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
PODS:
- boost (1.83.0)
- DoubleConversion (1.1.6)
- EXAV (14.0.7):
- EXAV (13.10.6):
- ExpoModulesCore
- ReactCommon/turbomodule/core
- EXConstants (15.4.5):
Expand All @@ -21,7 +21,7 @@ PODS:
- React-NativeModulesApple
- React-RCTAppDelegate
- ReactCommon/turbomodule/core
- ExpoPlayAudioStream (0.1.34):
- ExpoPlayAudioStream (0.2.6):
- ExpoModulesCore
- FBLazyVector (0.73.6)
- FBReactNativeSpec (0.73.6):
Expand Down Expand Up @@ -1263,14 +1263,14 @@ EXTERNAL SOURCES:
SPEC CHECKSUMS:
boost: d3f49c53809116a5d38da093a8aa78bf551aed09
DoubleConversion: fea03f2699887d960129cc54bba7e52542b6f953
EXAV: afa491e598334bbbb92a92a2f4dd33d7149ad37f
EXAV: e4f6137431ddc4cb025895046bfefa9612025c35
EXConstants: 988aa430ca0f76b43cd46b66e7fae3287f9cc2fc
EXFont: f20669cb266ef48b004f1eb1f2b20db96cd1df9f
Expo: e01a77c6fa4bc80a6d1bb949cda1d12d21044abd
ExpoFileSystem: eecaf6796aed0f4dd20042dc2ca2cac6c4bc1185
ExpoKeepAwake: 0f5cad99603a3268e50af9a6eb8b76d0d9ac956c
ExpoModulesCore: 61dc57c6e2a35f2f84baf488146db624e03af4cd
ExpoPlayAudioStream: 3776d74190cf96a2b8a973f7fde0974e301d3110
ExpoPlayAudioStream: 233109b91fea037369c508f3eb3a08097193ba68
FBLazyVector: f64d1e2ea739b4d8f7e4740cde18089cd97fe864
FBReactNativeSpec: 9f2b8b243131565335437dba74923a8d3015e780
fmt: ff9d55029c625d3757ed641535fd4a75fedc7ce9
Expand Down
34 changes: 21 additions & 13 deletions example/ios/expoaudiostreamexample.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
archiveVersion = 1;
classes = {
};
objectVersion = 46;
objectVersion = 54;
objects = {

/* Begin PBXBuildFile section */
Expand Down Expand Up @@ -325,14 +325,18 @@
CLANG_ENABLE_MODULES = YES;
CODE_SIGN_ENTITLEMENTS = expoaudiostreamexample/expoaudiostreamexample.entitlements;
CURRENT_PROJECT_VERSION = 1;
DEVELOPMENT_TEAM = 2TGRH35N8D;
ENABLE_BITCODE = NO;
GCC_PREPROCESSOR_DEFINITIONS = (
"$(inherited)",
"FB_SONARKIT_ENABLED=1",
);
INFOPLIST_FILE = expoaudiostreamexample/Info.plist;
IPHONEOS_DEPLOYMENT_TARGET = 13.4;
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks";
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
);
MARKETING_VERSION = 1.0;
OTHER_LDFLAGS = (
"$(inherited)",
Expand All @@ -358,9 +362,13 @@
CLANG_ENABLE_MODULES = YES;
CODE_SIGN_ENTITLEMENTS = expoaudiostreamexample/expoaudiostreamexample.entitlements;
CURRENT_PROJECT_VERSION = 1;
DEVELOPMENT_TEAM = 2TGRH35N8D;
INFOPLIST_FILE = expoaudiostreamexample/Info.plist;
IPHONEOS_DEPLOYMENT_TARGET = 13.4;
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks";
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
);
MARKETING_VERSION = 1.0;
OTHER_LDFLAGS = (
"$(inherited)",
Expand Down Expand Up @@ -426,16 +434,16 @@
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 13.4;
LD_RUNPATH_SEARCH_PATHS = "/usr/lib/swift $(inherited)";
LD_RUNPATH_SEARCH_PATHS = (
/usr/lib/swift,
"$(inherited)",
);
LIBRARY_SEARCH_PATHS = "$(SDKROOT)/usr/lib/swift\"$(inherited)\"";
MTL_ENABLE_DEBUG_INFO = YES;
ONLY_ACTIVE_ARCH = YES;
OTHER_CFLAGS = "$(inherited)";
OTHER_CPLUSPLUSFLAGS = "$(inherited)";
OTHER_LDFLAGS = (
"$(inherited)",
" ",
);
OTHER_LDFLAGS = "$(inherited) ";
REACT_NATIVE_PATH = "${PODS_ROOT}/../../node_modules/react-native";
SDKROOT = iphoneos;
USE_HERMES = true;
Expand Down Expand Up @@ -484,15 +492,15 @@
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 13.4;
LD_RUNPATH_SEARCH_PATHS = "/usr/lib/swift $(inherited)";
LD_RUNPATH_SEARCH_PATHS = (
/usr/lib/swift,
"$(inherited)",
);
LIBRARY_SEARCH_PATHS = "$(SDKROOT)/usr/lib/swift\"$(inherited)\"";
MTL_ENABLE_DEBUG_INFO = NO;
OTHER_CFLAGS = "$(inherited)";
OTHER_CPLUSPLUSFLAGS = "$(inherited)";
OTHER_LDFLAGS = (
"$(inherited)",
" ",
);
OTHER_LDFLAGS = "$(inherited) ";
REACT_NATIVE_PATH = "${PODS_ROOT}/../../node_modules/react-native";
SDKROOT = iphoneos;
USE_HERMES = true;
Expand Down
2 changes: 1 addition & 1 deletion ios/AudioController.swift
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ public class AudioController {
private let audioFormat = AVAudioFormat(commonFormat: .pcmFormatFloat32, sampleRate: 16000.0, channels: 1, interleaved: false)

private var bufferQueue: [(buffer: AVAudioPCMBuffer, promise: RCTPromiseResolveBlock)] = []
private let bufferAccessQueue = DispatchQueue(label: "com.kinexpoaudiostream.bufferAccessQueue") // Serial queue for thread-safe buffer access
private let bufferAccessQueue = DispatchQueue(label: "com.expoaudiostream.bufferAccessQueue") // Serial queue for thread-safe buffer access

private var isPlayingQueueA: Bool = false // Indicates which queue is currently in use for playback

Expand Down
2 changes: 1 addition & 1 deletion ios/AudioSessionManager.swift
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ class AudioSessionManager {
private let audioFormat = AVAudioFormat(commonFormat: .pcmFormatFloat32, sampleRate: 16000.0, channels: 1, interleaved: false)

private var bufferQueue: [(buffer: AVAudioPCMBuffer, promise: RCTPromiseResolveBlock, turnId: String)] = []
private let bufferAccessQueue = DispatchQueue(label: "com.kinexpoaudiostream.bufferAccessQueue") // Serial queue for thread-safe buffer access
private let bufferAccessQueue = DispatchQueue(label: "com.expoaudiostream.bufferAccessQueue") // Serial queue for thread-safe buffer access


internal var recordingFileURL: URL?
Expand Down
44 changes: 28 additions & 16 deletions ios/ExpoPlayAudioStreamModule.swift
Original file line number Diff line number Diff line change
Expand Up @@ -6,15 +6,23 @@ let audioDataEvent: String = "AudioData"
let soundIsPlayedEvent: String = "SoundChunkPlayed"

public class ExpoPlayAudioStreamModule: Module, AudioStreamManagerDelegate, MicrophoneDataDelegate, SoundPlayerDelegate {
private let audioController = AudioController()
private let audioSessionManager = AudioSessionManager()
private lazy var audioController: AudioController = AudioController()

private lazy var audioSessionManager: AudioSessionManager = {
let audioSessionManager = AudioSessionManager()
audioSessionManager.delegate = self
return audioSessionManager
}()

private lazy var microphone: Microphone = {
let microphone = Microphone()
microphone.delegate = self
return microphone
}()

private lazy var soundPlayer: SoundPlayer = {
let soundPlayer = SoundPlayer()
soundPlayer.delegate = self
return soundPlayer
}()

Expand All @@ -26,13 +34,6 @@ public class ExpoPlayAudioStreamModule: Module, AudioStreamManagerDelegate, Micr
// Defines event names that the module can send to JavaScript.
Events([audioDataEvent, soundIsPlayedEvent])

OnCreate {
print("Setting up Audio Session Manager")
audioSessionManager.delegate = self
microphone.delegate = self
soundPlayer.delegate = self
}

/// Asynchronously starts audio recording with the given settings.
///
/// - Parameters:
Expand Down Expand Up @@ -245,15 +246,26 @@ public class ExpoPlayAudioStreamModule: Module, AudioStreamManagerDelegate, Micr
}

private func ensureInittedAudioSession() throws {
if self.inittedAudioSession { return }
let audioSession = AVAudioSession.sharedInstance()
try audioSession.setCategory(
.playAndRecord, mode: .voiceChat,
options: [.defaultToSpeaker, .allowBluetooth, .allowBluetoothA2DP, .mixWithOthers])
try audioSession.setActive(true)
inittedAudioSession = true
if self.inittedAudioSession { return }

self.promptForMicrophoneModes()
let audioSession = AVAudioSession.sharedInstance()
try audioSession.setCategory(
.playAndRecord, mode: .voiceChat,
options: [.defaultToSpeaker, .allowBluetooth, .allowBluetoothA2DP, .mixWithOthers])
try audioSession.setActive(true)
inittedAudioSession = true
}


private func promptForMicrophoneModes() {
guard #available(iOS 15.0, *) else {
return
}

AVCaptureDevice.showSystemUserInterface(.microphoneModes)
}

/// Handles the reception of audio data from the AudioStreamManager.
///
/// - Parameters:
Expand Down
18 changes: 6 additions & 12 deletions ios/Microphone.swift
Original file line number Diff line number Diff line change
Expand Up @@ -57,13 +57,6 @@ class Microphone {
private static let desiredInputFormat = AVAudioFormat(commonFormat: .pcmFormatInt16, sampleRate: sampleRate, channels: 1, interleaved: false)!
private let audioPlaybackFormat = AVAudioFormat(commonFormat: .pcmFormatFloat32, sampleRate: 16000.0, channels: 1, interleaved: false)

init() {
if #available(iOS 15.0, *) {
AVCaptureDevice.showSystemUserInterface(.microphoneModes)
} else {
// Fallback on earlier versions
}
}

private func setupVoiceProcessing() {
self.isMuted = false
Expand All @@ -82,7 +75,7 @@ class Microphone {
// that they will be supported in all environments.
// Notably, echo cancellation doesn't seem to work in the iOS simulator.
try self.inputNode.setVoiceProcessingEnabled(true)
//try outputNode.setVoiceProcessingEnabled(true)
try outputNode.setVoiceProcessingEnabled(true)
} catch {
print("Error setting voice processing: \(error)")
return
Expand Down Expand Up @@ -152,19 +145,20 @@ class Microphone {


func startRecording(settings: RecordingSettings, intervalMilliseconds: Int) -> StartRecordingResult? {
if !self.isVoiceProcessingEnabled {
setupVoiceProcessing()
}
guard !isRecording else {
Logger.debug("Debug: Recording is already in progress.")
return StartRecordingResult(error: "Recording is already in progress.")
}

if audioEngine.isRunning {
if self.audioEngine != nil && audioEngine.isRunning {
Logger.debug("Debug: Audio engine already running.")
audioEngine.stop()
}

if !self.isVoiceProcessingEnabled {
setupVoiceProcessing()
}

var newSettings = settings // Make settings mutable

// Determine the commonFormat based on bitDepth
Expand Down
Loading

0 comments on commit d9eb1ea

Please sign in to comment.