@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.
- package/CHANGELOG.md +10 -1
- package/android/src/androidTest/java/net/siteed/audiostream/AudioRecorderPerformanceInstrumentedTest.kt +234 -0
- package/android/src/main/java/net/siteed/audiostream/AudioRecorderManager.kt +52 -17
- package/build/cjs/AudioDeviceManager.js.map +1 -1
- package/build/cjs/index.js.map +1 -1
- package/build/esm/AudioDeviceManager.js.map +1 -1
- package/build/esm/index.js.map +1 -1
- package/build/types/AudioDeviceManager.d.ts.map +1 -1
- package/build/types/index.d.ts.map +1 -1
- package/ios/AudioStreamManager.swift +141 -180
- package/package.json +1 -1
- package/src/AudioDeviceManager.ts +2 -1
- package/src/index.ts +1 -0
|
@@ -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
|
-
|
|
1702
|
-
|
|
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.
|
|
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
|
|
1764
|
+
// Check for compressed output using cached size
|
|
1741
1765
|
var compression: CompressedRecordingInfo?
|
|
1742
|
-
if settings.output.compressed.enabled,
|
|
1743
|
-
|
|
1744
|
-
|
|
1745
|
-
|
|
1746
|
-
|
|
1747
|
-
|
|
1748
|
-
|
|
1749
|
-
|
|
1750
|
-
|
|
1751
|
-
|
|
1752
|
-
|
|
1753
|
-
|
|
1754
|
-
|
|
1755
|
-
|
|
1756
|
-
|
|
1757
|
-
|
|
1758
|
-
|
|
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 =
|
|
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
|
-
//
|
|
1814
|
-
let
|
|
1815
|
-
stopping = false
|
|
1822
|
+
// PERFORMANCE OPTIMIZATION: Create result immediately with cached values
|
|
1823
|
+
let durationMs = Int64(finalDuration * 1000)
|
|
1816
1824
|
|
|
1817
|
-
//
|
|
1818
|
-
|
|
1819
|
-
|
|
1820
|
-
|
|
1821
|
-
|
|
1822
|
-
|
|
1823
|
-
|
|
1824
|
-
|
|
1825
|
-
|
|
1826
|
-
|
|
1827
|
-
|
|
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
|
-
|
|
1904
|
-
|
|
1905
|
-
|
|
1906
|
-
|
|
1907
|
-
|
|
1908
|
-
|
|
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
|
-
|
|
1936
|
-
|
|
1937
|
-
|
|
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.
|
|
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,
|
|
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 }) {
|