@siteed/expo-audio-studio 2.13.1 → 2.14.0

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.
@@ -112,6 +112,10 @@ class AudioStreamManager: NSObject, AudioDeviceManagerDelegate {
112
112
 
113
113
  // Add the stopping flag to the class properties
114
114
  private var stopping: Bool = false
115
+
116
+ // Performance optimization: Cache file sizes during recording
117
+ private var cachedWavFileSize: Int64 = 0
118
+ private var cachedCompressedFileSize: Int64 = 0
115
119
 
116
120
  /// Initializes the AudioStreamManager
117
121
  override init() {
@@ -819,6 +823,7 @@ class AudioStreamManager: NSObject, AudioDeviceManagerDelegate {
819
823
  let header = createWavHeader(dataSize: 0)
820
824
  self.fileHandle?.write(header)
821
825
  self.totalDataSize = Int64(WAV_HEADER_SIZE) // Initialize size with header size
826
+ self.cachedWavFileSize = Int64(WAV_HEADER_SIZE) // Initialize cached size
822
827
  Logger.debug("AudioStreamManager", "File handle opened and initial header written for \(url.path). Initial size: \(self.totalDataSize)")
823
828
  } catch {
824
829
  Logger.debug("AudioStreamManager", "Error creating/opening file handle: \(error.localizedDescription)")
@@ -1462,6 +1467,8 @@ class AudioStreamManager: NSObject, AudioDeviceManagerDelegate {
1462
1467
  try handle.write(contentsOf: dataToWrite)
1463
1468
  // Update total size state
1464
1469
  self.totalDataSize += Int64(dataToWrite.count)
1470
+ // Cache WAV file size for performance
1471
+ self.cachedWavFileSize = self.totalDataSize
1465
1472
  } catch {
1466
1473
  Logger.debug("BG Write Error: Failed to seek/write: \(error.localizedDescription)")
1467
1474
  }
@@ -1698,35 +1705,52 @@ class AudioStreamManager: NSObject, AudioDeviceManagerDelegate {
1698
1705
  return nil
1699
1706
  }
1700
1707
 
1701
- if recordingSettings?.showNotification == true {
1702
- // Stop and clean up timer safely
1708
+ // PERFORMANCE OPTIMIZATION: Capture current state for immediate return
1709
+ let capturedFileURL = recordingFileURL
1710
+ let capturedSettings = recordingSettings
1711
+ let capturedWavFileSize = cachedWavFileSize
1712
+ let capturedCompressedFileSize = cachedCompressedFileSize
1713
+ let capturedTotalDataSize = totalDataSize
1714
+ let capturedCompressedURL = compressedFileURL
1715
+
1716
+ // PERFORMANCE OPTIMIZATION: Move all slow operations to background
1717
+ let capturedShowNotification = recordingSettings?.showNotification == true
1718
+
1719
+ // Queue notification and audio session cleanup for background
1720
+ DispatchQueue.global(qos: .utility).async { [weak self] in
1721
+ guard let self = self else { return }
1722
+
1723
+ if capturedShowNotification {
1724
+ // Clean up notifications on main queue but don't wait
1725
+ DispatchQueue.main.async {
1726
+ self.mediaInfoUpdateTimer?.invalidate()
1727
+ self.mediaInfoUpdateTimer = nil
1728
+
1729
+ // Clean up notification manager
1730
+ self.notificationManager?.stopUpdates()
1731
+ self.notificationManager = nil
1732
+
1733
+ // Clean up media controls
1734
+ UIApplication.shared.endReceivingRemoteControlEvents()
1735
+ self.remoteCommandCenter?.pauseCommand.isEnabled = false
1736
+ self.remoteCommandCenter?.playCommand.isEnabled = false
1737
+ self.notificationView?.nowPlayingInfo = nil
1738
+ }
1739
+ }
1740
+
1741
+ // Reset audio session in background
1742
+ do {
1743
+ try AVAudioSession.sharedInstance().setActive(false, options: .notifyOthersOnDeactivation)
1744
+ } catch {
1745
+ Logger.debug("Background: Error deactivating audio session: \(error)")
1746
+ }
1747
+
1748
+ // Reset audio engine in background
1703
1749
  DispatchQueue.main.async {
1704
- self.mediaInfoUpdateTimer?.invalidate()
1705
- self.mediaInfoUpdateTimer = nil
1706
-
1707
- // Clean up notification manager
1708
- self.notificationManager?.stopUpdates()
1709
- self.notificationManager = nil
1710
-
1711
- // Clean up media controls
1712
- UIApplication.shared.endReceivingRemoteControlEvents()
1713
- self.remoteCommandCenter?.pauseCommand.isEnabled = false
1714
- self.remoteCommandCenter?.playCommand.isEnabled = false
1715
- self.notificationView?.nowPlayingInfo = nil
1750
+ self.audioEngine.reset()
1716
1751
  }
1717
1752
  }
1718
1753
 
1719
- // Reset audio session safely
1720
- do {
1721
- try AVAudioSession.sharedInstance().setActive(false, options: .notifyOthersOnDeactivation)
1722
- } catch {
1723
- Logger.debug("Error deactivating audio session: \(error)")
1724
- // Continue with cleanup despite session errors
1725
- }
1726
-
1727
- // Reset audio engine
1728
- audioEngine.reset()
1729
-
1730
1754
  guard let settings = recordingSettings else {
1731
1755
  Logger.debug("Recording settings is nil.")
1732
1756
  stopping = false // Reset stopping flag before returning nil
@@ -1737,40 +1761,25 @@ class AudioStreamManager: NSObject, AudioDeviceManagerDelegate {
1737
1761
  if !settings.output.primary.enabled {
1738
1762
  let durationMs = Int64(finalDuration * 1000)
1739
1763
 
1740
- // Check for compressed output even when primary is disabled
1764
+ // Check for compressed output using cached size
1741
1765
  var compression: CompressedRecordingInfo?
1742
- if settings.output.compressed.enabled, let compressedURL = compressedFileURL {
1743
- let compressedPath = compressedURL.path
1744
- if FileManager.default.fileExists(atPath: compressedPath) {
1745
- do {
1746
- let compressedAttributes = try FileManager.default.attributesOfItem(atPath: compressedPath)
1747
- let compressedSize = compressedAttributes[FileAttributeKey.size] as? Int64 ?? 0
1748
-
1749
- Logger.debug("""
1750
- Compressed File validation (primary disabled):
1751
- - Path: \(compressedPath)
1752
- - Format: \(compressedFormat)
1753
- - Size: \(compressedSize) bytes
1754
- - Bitrate: \(compressedBitRate) bps
1755
- """)
1756
-
1757
- if compressedSize > 0 {
1758
- compression = CompressedRecordingInfo(
1759
- compressedFileUri: compressedURL.absoluteString,
1760
- mimeType: compressedFormat == "aac" ? "audio/aac" : "audio/opus",
1761
- bitrate: compressedBitRate,
1762
- format: compressedFormat,
1763
- size: compressedSize
1764
- )
1765
- } else {
1766
- Logger.debug("Warning: Compressed file exists but is empty")
1767
- }
1768
- } catch {
1769
- Logger.debug("Failed to validate compressed file: \(error)")
1770
- }
1771
- } else {
1772
- Logger.debug("Warning: Compressed file not found at path: \(compressedPath)")
1773
- }
1766
+ if settings.output.compressed.enabled,
1767
+ let compressedURL = capturedCompressedURL,
1768
+ capturedCompressedFileSize > 0 {
1769
+ compression = CompressedRecordingInfo(
1770
+ compressedFileUri: compressedURL.absoluteString,
1771
+ mimeType: compressedFormat == "aac" ? "audio/aac" : "audio/opus",
1772
+ bitrate: compressedBitRate,
1773
+ format: compressedFormat,
1774
+ size: capturedCompressedFileSize
1775
+ )
1776
+
1777
+ Logger.debug("""
1778
+ Compressed File (cached - primary disabled):
1779
+ - Format: \(compressedFormat)
1780
+ - Size: \(capturedCompressedFileSize) bytes
1781
+ - Bitrate: \(compressedBitRate) bps
1782
+ """)
1774
1783
  }
1775
1784
 
1776
1785
  let result = RecordingResult(
@@ -1804,140 +1813,79 @@ class AudioStreamManager: NSObject, AudioDeviceManagerDelegate {
1804
1813
  return result
1805
1814
  }
1806
1815
 
1807
- guard let fileURL = recordingFileURL else {
1816
+ guard let fileURL = capturedFileURL else {
1808
1817
  Logger.debug("Recording file URL is nil.")
1809
1818
  stopping = false // Reset stopping flag before returning nil
1810
1819
  return nil
1811
1820
  }
1812
1821
 
1813
- // Reset stopping flag before returning
1814
- let result = createRecordingResult(fileURL: fileURL, settings: settings, finalDuration: finalDuration)
1815
- stopping = false
1822
+ // PERFORMANCE OPTIMIZATION: Create result immediately with cached values
1823
+ let durationMs = Int64(finalDuration * 1000)
1816
1824
 
1817
- // Return after all cleanup tasks are completed
1818
- return result
1819
- }
1820
-
1821
- /// Creates a RecordingResult from the finished recording
1822
- /// - Parameters:
1823
- /// - fileURL: The URL of the recording file
1824
- /// - settings: The settings used for recording
1825
- /// - finalDuration: The final duration of the recording
1826
- /// - Returns: A RecordingResult object or nil if validation fails
1827
- private func createRecordingResult(fileURL: URL, settings: RecordingSettings, finalDuration: TimeInterval) -> RecordingResult? {
1828
- // Validate WAV file
1829
- let wavPath = fileURL.path
1830
- do {
1831
- // Check if WAV file exists
1832
- let wavFileAttributes = try FileManager.default.attributesOfItem(atPath: wavPath)
1833
- let wavFileSize = wavFileAttributes[FileAttributeKey.size] as? Int64 ?? 0
1834
-
1835
- Logger.debug("""
1836
- WAV File validation:
1837
- - Path: \(wavPath)
1838
- - Exists: true
1839
- - Size: \(wavFileSize) bytes
1840
- - Duration: \(finalDuration) seconds
1841
- - Expected minimum size: \(WAV_HEADER_SIZE) bytes (WAV header)
1842
- """)
1843
-
1844
- // Use the final totalDataSize tracked by the background queue
1845
- let finalDataChunkSize = self.totalDataSize - Int64(WAV_HEADER_SIZE)
1846
- if finalDataChunkSize <= 0 {
1847
- Logger.debug("Recording file data chunk size is zero or negative (\(finalDataChunkSize) bytes), likely no audio data was recorded successfully after header")
1848
- // Optionally delete the empty file?
1849
- // try? FileManager.default.removeItem(at: fileURL)
1850
- return nil
1851
- }
1852
-
1853
- // Update the WAV header with the correct final file size
1854
- updateWavHeader(fileURL: fileURL, totalDataSize: finalDataChunkSize)
1855
- Logger.debug("Final WAV header updated. Data chunk size: \(finalDataChunkSize)")
1856
-
1857
- // Validate compressed file if enabled
1858
- var compression: CompressedRecordingInfo?
1859
- if let compressedURL = compressedFileURL {
1860
- let compressedPath = compressedURL.path
1861
- if FileManager.default.fileExists(atPath: compressedPath) {
1862
- let compressedAttributes = try FileManager.default.attributesOfItem(atPath: compressedPath)
1863
- let compressedSize = compressedAttributes[FileAttributeKey.size] as? Int64 ?? 0
1864
-
1865
- Logger.debug("""
1866
- Compressed File validation:
1867
- - Path: \(compressedPath)
1868
- - Format: \(compressedFormat)
1869
- - Size: \(compressedSize) bytes
1870
- - Bitrate: \(compressedBitRate) bps
1871
- """)
1872
-
1873
- if compressedSize > 0 {
1874
- compression = CompressedRecordingInfo(
1875
- compressedFileUri: compressedURL.absoluteString,
1876
- mimeType: compressedFormat == "aac" ? "audio/aac" : "audio/opus",
1877
- bitrate: compressedBitRate,
1878
- format: compressedFormat,
1879
- size: compressedSize
1880
- )
1881
- } else {
1882
- Logger.debug("Warning: Compressed file exists but is empty")
1883
- }
1884
- } else {
1885
- Logger.debug("Warning: Compressed file not found at path: \(compressedPath)")
1886
- }
1887
- }
1888
-
1889
- let durationMs = Int64(finalDuration * 1000)
1890
-
1891
- let result = RecordingResult(
1892
- fileUri: fileURL.absoluteString,
1893
- filename: fileURL.lastPathComponent,
1894
- mimeType: mimeType,
1895
- duration: durationMs,
1896
- size: wavFileSize,
1897
- channels: settings.numberOfChannels,
1898
- bitDepth: settings.bitDepth,
1899
- sampleRate: settings.sampleRate,
1900
- compression: compression
1825
+ // Check compressed output
1826
+ var compression: CompressedRecordingInfo?
1827
+ if capturedSettings?.output.compressed.enabled == true,
1828
+ let compressedURL = capturedCompressedURL,
1829
+ capturedCompressedFileSize > 0 {
1830
+ compression = CompressedRecordingInfo(
1831
+ compressedFileUri: compressedURL.absoluteString,
1832
+ mimeType: compressedFormat == "aac" ? "audio/aac" : "audio/opus",
1833
+ bitrate: compressedBitRate,
1834
+ format: compressedFormat,
1835
+ size: capturedCompressedFileSize
1901
1836
  )
1837
+ }
1838
+
1839
+ // Create result with cached values - no file system access
1840
+ let result = RecordingResult(
1841
+ fileUri: fileURL.absoluteString,
1842
+ filename: fileURL.lastPathComponent,
1843
+ mimeType: mimeType,
1844
+ duration: durationMs,
1845
+ size: capturedWavFileSize,
1846
+ channels: capturedSettings?.numberOfChannels ?? 1,
1847
+ bitDepth: capturedSettings?.bitDepth ?? 16,
1848
+ sampleRate: capturedSettings?.sampleRate ?? 44100,
1849
+ compression: compression
1850
+ )
1851
+
1852
+ // Perform file operations asynchronously after returning result
1853
+ DispatchQueue.global(qos: .utility).async { [weak self] in
1854
+ guard let self = self else { return }
1902
1855
 
1903
- Logger.debug("""
1904
- Recording completed successfully:
1905
- - WAV file: \(fileURL.lastPathComponent)
1906
- - Size: \(wavFileSize) bytes
1907
- - Duration: \(durationMs)ms
1908
- - Sample rate: \(settings.sampleRate)Hz
1909
- - Bit depth: \(settings.bitDepth)-bit
1910
- - Channels: \(settings.numberOfChannels)
1911
- - Compressed: \(compression != nil ? "yes" : "no")
1912
- """)
1913
-
1914
- // Additional cleanup
1915
- recordingFileURL = nil
1916
- lastBufferTime = nil
1917
- lastValidDuration = nil
1918
- compressedRecorder = nil
1919
- compressedFileURL = nil
1920
- recordingSettings = nil
1921
- startTime = nil
1922
- totalPausedDuration = 0
1923
- currentPauseStart = nil
1924
- lastEmissionTime = nil
1925
- lastEmissionTimeAnalysis = nil
1926
- lastEmittedSize = 0
1927
- lastEmittedSizeAnalysis = 0
1928
- lastEmittedCompressedSize = 0
1929
- accumulatedData.removeAll()
1930
- accumulatedAnalysisData.removeAll()
1931
- recordingUUID = nil
1932
-
1933
- return result
1856
+ // Update WAV header in background
1857
+ let finalDataChunkSize = capturedTotalDataSize - Int64(WAV_HEADER_SIZE)
1858
+ if finalDataChunkSize > 0 {
1859
+ self.updateWavHeader(fileURL: fileURL, totalDataSize: finalDataChunkSize)
1860
+ Logger.debug("Background: WAV header updated. Data chunk size: \(finalDataChunkSize)")
1861
+ }
1934
1862
 
1935
- } catch {
1936
- Logger.debug("Failed to validate recording files: \(error)")
1937
- return nil
1863
+ // Cleanup
1864
+ self.recordingSettings = nil
1865
+ self.startTime = nil
1866
+ self.totalPausedDuration = 0
1867
+ self.currentPauseStart = nil
1868
+ self.lastEmissionTime = nil
1869
+ self.lastEmissionTimeAnalysis = nil
1870
+ self.lastEmittedSize = 0
1871
+ self.lastEmittedSizeAnalysis = 0
1872
+ self.lastEmittedCompressedSize = 0
1873
+ self.accumulatedData.removeAll()
1874
+ self.accumulatedAnalysisData.removeAll()
1875
+ self.recordingUUID = nil
1876
+ self.totalDataSize = 0
1877
+ self.cachedWavFileSize = 0
1878
+ self.cachedCompressedFileSize = 0
1879
+ self.recordingFileURL = nil
1880
+ self.compressedFileURL = nil
1881
+ self.fileHandle = nil
1938
1882
  }
1883
+
1884
+ stopping = false
1885
+ return result
1939
1886
  }
1940
1887
 
1888
+
1941
1889
  // MARK: - AudioDeviceManagerDelegate Implementation
1942
1890
 
1943
1891
  func audioDeviceManager(_ manager: AudioDeviceManager, didDetectDisconnectionOfDevice disconnectedDeviceId: String) {
@@ -2297,6 +2245,19 @@ extension AudioStreamManager: AVAudioRecorderDelegate {
2297
2245
  Logger.debug("Compressed recording finished - success: \(flag)")
2298
2246
  if !flag {
2299
2247
  delegate?.audioStreamManager(self, didFailWithError: "Compressed recording failed to complete")
2248
+ } else {
2249
+ // Update cached compressed file size when recording finishes
2250
+ if let compressedURL = compressedFileURL {
2251
+ do {
2252
+ let attributes = try FileManager.default.attributesOfItem(atPath: compressedURL.path)
2253
+ if let size = attributes[.size] as? Int64 {
2254
+ cachedCompressedFileSize = size
2255
+ Logger.debug("Cached compressed file size: \(size) bytes")
2256
+ }
2257
+ } catch {
2258
+ Logger.debug("Failed to cache compressed file size: \(error)")
2259
+ }
2260
+ }
2300
2261
  }
2301
2262
  }
2302
2263
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@siteed/expo-audio-studio",
3
- "version": "2.13.1",
3
+ "version": "2.14.0",
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",
@@ -89,7 +89,8 @@ export class AudioDeviceManager {
89
89
 
90
90
  // Track temporarily disconnected devices
91
91
  private temporarilyDisconnectedDevices: Set<string> = new Set()
92
- private disconnectionTimeouts: Map<string, NodeJS.Timeout> = new Map()
92
+ private disconnectionTimeouts: Map<string, ReturnType<typeof setTimeout>> =
93
+ new Map()
93
94
  private readonly DISCONNECTION_TIMEOUT_MS = 5000 // 5 seconds
94
95
 
95
96
  constructor(options?: { logger?: ConsoleLike }) {
package/src/index.ts CHANGED
@@ -38,5 +38,6 @@ export {
38
38
  useSharedAudioRecorder,
39
39
  }
40
40
 
41
+ // Export all types
41
42
  export type * from './AudioAnalysis/AudioAnalysis.types'
42
43
  export type * from './ExpoAudioStream.types'