@siteed/expo-audio-studio 2.18.5 → 2.18.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -7,6 +7,14 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [2.18.6] - 2026-03-06
11
+ ### Fixed
12
+ - fix(ios): `resetToDefaultDevice` now correctly resets the engine tap when switching back to the system default input (was a no-op due to `deviceId=nil` guard)
13
+ - fix(ios): recovery path after a failed device switch no longer produces silent audio (missing tap reinstall before engine restart)
14
+ - fix(ios): `setupNowPlayingInfo` no longer overrides user-configured audio session options (e.g. whisper-mode / no-Bluetooth configs)
15
+ - fix(ios): `selectInputDevice` now syncs `deviceId` into `recordingSettings` before updating the engine, ensuring the port lookup succeeds
16
+ - fix(ios): phone-call auto-resume handler respects user-configured `categoryOptions` instead of hardcoded `[.allowBluetooth, .mixWithOthers]`
17
+ - fix(ios): `AudioDeviceManager.prepareAudioSession` preserves existing session options when already `.playAndRecord`
10
18
 
11
19
  ## [2.18.5] - 2026-02-23
12
20
  ### Changed
@@ -87,11 +87,15 @@ class AudioDeviceManager {
87
87
  do {
88
88
  let session = AVAudioSession.sharedInstance()
89
89
 
90
- // Configure with options needed for Bluetooth detection
91
- try session.setCategory(.playAndRecord, mode: .default, options: [.allowBluetooth, .allowBluetoothA2DP, .mixWithOthers])
92
-
93
- // Activate the session
94
- try session.setActive(true, options: .notifyOthersOnDeactivation)
90
+ // Preserve existing session configuration if already set up for recording
91
+ if session.category == .playAndRecord {
92
+ Logger.debug("AudioDeviceManager", "Session already .playAndRecord, preserving categoryOptions")
93
+ try session.setActive(true, options: .notifyOthersOnDeactivation)
94
+ } else {
95
+ // Configure with options needed for Bluetooth detection
96
+ try session.setCategory(.playAndRecord, mode: .default, options: [.allowBluetooth, .allowBluetoothA2DP, .mixWithOthers])
97
+ try session.setActive(true, options: .notifyOthersOnDeactivation)
98
+ }
95
99
 
96
100
  // Give the system a moment to detect Bluetooth devices if needed
97
101
  // Minimal delay that still allows devices to be detected
@@ -217,7 +217,8 @@ class AudioStreamManager: NSObject, AudioDeviceManagerDelegate {
217
217
  // Configure audio session
218
218
  do {
219
219
  let session = AVAudioSession.sharedInstance()
220
- try session.setCategory(.playAndRecord, mode: .default, options: [.allowBluetooth, .mixWithOthers])
220
+ let resumeOptions: AVAudioSession.CategoryOptions = self.recordingSettings?.ios?.audioSession?.categoryOptions ?? [.allowBluetooth, .mixWithOthers]
221
+ try session.setCategory(.playAndRecord, mode: .default, options: resumeOptions)
221
222
  try session.setActive(true, options: .notifyOthersOnDeactivation)
222
223
 
223
224
  // Resume if we're still recording and paused
@@ -251,15 +252,9 @@ class AudioStreamManager: NSObject, AudioDeviceManagerDelegate {
251
252
  }
252
253
 
253
254
  private func setupNowPlayingInfo() {
254
- // Configure audio session for background audio
255
+ // Session is already configured by configureAudioSession(); do not override it here.
255
256
  audioSession = AVAudioSession.sharedInstance()
256
- do {
257
- try audioSession?.setCategory(.playAndRecord, mode: .default, options: [.allowBluetooth, .mixWithOthers])
258
- try audioSession?.setActive(true)
259
- } catch {
260
- Logger.debug("AudioStreamManager", "Failed to configure audio session: \(error)")
261
- }
262
-
257
+
263
258
  // Setup Now Playing info
264
259
  notificationView = MPNowPlayingInfoCenter.default()
265
260
  updateNowPlayingInfo(isPaused: false)
@@ -1683,37 +1678,59 @@ class AudioStreamManager: NSObject, AudioDeviceManagerDelegate {
1683
1678
  return outputBuffer
1684
1679
  }
1685
1680
 
1681
+ /// Performs the full engine stop → reset → reinstall tap → restart cycle for a device switch.
1682
+ /// Pass `nil` to revert to the system-default input.
1683
+ /// This is the single source of truth for all device-switch paths (Bug 1 + Bug 2 fixes).
1684
+ public func performDeviceSwitch(port: AVAudioSessionPortDescription?) {
1685
+ let wasRunning = audioEngine.isRunning
1686
+ do {
1687
+ if wasRunning { audioEngine.stop() }
1688
+ audioEngine.inputNode.removeTap(onBus: 0)
1689
+ try AVAudioSession.sharedInstance().setPreferredInput(port)
1690
+ Thread.sleep(forTimeInterval: 0.15)
1691
+ audioEngine.reset()
1692
+ _ = installTapWithHardwareFormat()
1693
+ if wasRunning {
1694
+ audioEngine.prepare()
1695
+ try audioEngine.start()
1696
+ lastEmissionTime = Date()
1697
+ lastEmissionTimeAnalysis = Date()
1698
+ Logger.debug("AudioStreamManager", "Device switch complete; engine restarted (port: \(port?.portName ?? "default"))")
1699
+ }
1700
+ } catch {
1701
+ Logger.debug("AudioStreamManager", "Device switch failed: \(error.localizedDescription)")
1702
+ if wasRunning {
1703
+ do {
1704
+ _ = installTapWithHardwareFormat() // Bug 2 fix: reinstall tap in recovery path
1705
+ audioEngine.prepare()
1706
+ try audioEngine.start()
1707
+ Logger.debug("AudioStreamManager", "Engine recovery after device switch succeeded")
1708
+ } catch {
1709
+ Logger.debug("AudioStreamManager", "Engine recovery failed: \(error.localizedDescription)")
1710
+ }
1711
+ }
1712
+ }
1713
+ }
1714
+
1686
1715
  /// Attempts to update the audio session with the preferred input device from current settings.
1687
1716
  /// Called externally when the device selection changes.
1688
- /// Note: Avoids changing sample rate or buffer duration while engine might be running.
1689
1717
  public func updateAudioSessionWithCurrentSettings() {
1690
1718
  guard let settings = self.recordingSettings, let deviceId = settings.deviceId else {
1691
1719
  Logger.debug("Cannot update audio session preference, settings or deviceId missing")
1692
1720
  return
1693
1721
  }
1694
-
1722
+
1695
1723
  let session = AVAudioSession.sharedInstance()
1696
-
1697
- // Find the requested device port
1698
1724
  let selectedPort = session.availableInputs?.first { port in
1699
- // Normalize IDs for comparison, especially for Bluetooth
1700
- let portNormalizedId = deviceManager.normalizeBluetoothDeviceId(port.uid)
1701
- let requestedNormalizedId = deviceManager.normalizeBluetoothDeviceId(deviceId)
1702
- return portNormalizedId == requestedNormalizedId
1725
+ deviceManager.normalizeBluetoothDeviceId(port.uid) == deviceManager.normalizeBluetoothDeviceId(deviceId)
1703
1726
  }
1704
-
1705
- if let portToSet = selectedPort {
1706
- do {
1707
- try session.setPreferredInput(portToSet)
1708
- Logger.debug("Attempted to set preferred input to: \(portToSet.portName) (ID: \(portToSet.uid))")
1709
- // We add a small delay hoping the system applies the change before potential next operations
1710
- Thread.sleep(forTimeInterval: 0.1)
1711
- } catch {
1712
- Logger.debug("Failed to set preferred input device \(portToSet.portName): \(error.localizedDescription)")
1713
- }
1714
- } else {
1715
- Logger.debug("Could not find device with ID \(deviceId) to set as preferred input.")
1727
+
1728
+ guard let portToSet = selectedPort else {
1729
+ Logger.debug("Could not find device with ID \(deviceId)")
1730
+ return
1716
1731
  }
1732
+
1733
+ performDeviceSwitch(port: portToSet)
1717
1734
  }
1718
1735
 
1719
1736
  /// Stops the current audio recording.
@@ -759,6 +759,8 @@ public class ExpoAudioStreamModule: Module, AudioStreamManagerDelegate, AudioDev
759
759
  AsyncFunction("selectInputDevice") { (deviceId: String, promise: Promise) in
760
760
  Logger.debug("ExpoAudioStreamModule", "selectInputDevice called with ID: \(deviceId)")
761
761
  self.deviceManager.selectInputDevice(deviceId, promise: promise)
762
+ // Sync deviceId into recordingSettings so updateAudioSessionWithCurrentSettings can find the port
763
+ self.streamManager.recordingSettings?.deviceId = deviceId
762
764
  // Update the audio recorder if recording is in progress or prepared
763
765
  if self.streamManager.isRecording || self.streamManager.isPrepared {
764
766
  Logger.debug("ExpoAudioStreamModule", "selectInputDevice: Calling updateAudioSessionWithCurrentSettings because recording/prepared.")
@@ -776,11 +778,15 @@ public class ExpoAudioStreamModule: Module, AudioStreamManagerDelegate, AudioDev
776
778
  Logger.debug("ExpoAudioStreamModule", "resetToDefaultDevice called.")
777
779
  self.deviceManager.resetToDefaultDevice { success, error in
778
780
  if success {
781
+ // Clear stored deviceId so updateAudioSessionWithCurrentSettings won't bail early
782
+ self.streamManager.recordingSettings?.deviceId = nil
779
783
  if self.streamManager.isRecording || self.streamManager.isPrepared {
780
- Logger.debug("ExpoAudioStreamModule", "resetToDefaultDevice: Calling updateAudioSessionWithCurrentSettings because recording/prepared.")
781
- self.streamManager.updateAudioSessionWithCurrentSettings()
784
+ Logger.debug("ExpoAudioStreamModule", "resetToDefaultDevice: Performing device switch to system default.")
785
+ // Bug 1 fix: call performDeviceSwitch(nil) directly — updateAudioSessionWithCurrentSettings
786
+ // would bail immediately because deviceId is now nil.
787
+ self.streamManager.performDeviceSwitch(port: nil)
782
788
  } else {
783
- Logger.debug("ExpoAudioStreamModule", "resetToDefaultDevice: Not calling updateAudioSessionWithCurrentSettings because not recording/prepared.")
789
+ Logger.debug("ExpoAudioStreamModule", "resetToDefaultDevice: Not recording/prepared, no engine action needed.")
784
790
  }
785
791
  promise.resolve(true)
786
792
  } else {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@siteed/expo-audio-studio",
3
- "version": "2.18.5",
3
+ "version": "2.18.6",
4
4
  "description": "Comprehensive audio processing library for React Native and Expo with recording, analysis, visualization, and streaming capabilities across iOS, Android, and web",
5
5
  "license": "MIT",
6
6
  "type": "commonjs",