@siteed/expo-audio-studio 2.10.1 โ 2.10.2
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
|
@@ -8,6 +8,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
10
|
|
|
11
|
+
## [2.10.2] - 2025-05-31
|
|
12
|
+
### Changed
|
|
13
|
+
- fix: Buffer size calculation and document duplicate emission fix for โฆ (#248) ([204dde5](https://github.com/deeeed/expo-audio-stream/commit/204dde5137620e80c9a22a5a27a395a2149f33f0))
|
|
14
|
+
- chore(expo-audio-studio): release @siteed/expo-audio-studio@2.10.1 ([0acbfc5](https://github.com/deeeed/expo-audio-stream/commit/0acbfc5b145b478cc913baf7fd798573d8c2f305))
|
|
11
15
|
## [2.10.1] - 2025-05-27
|
|
12
16
|
### Changed
|
|
13
17
|
- fix(useAudioRecorder): update intervalId type for better type safety ([dc0021a](https://github.com/deeeed/expo-audio-stream/commit/dc0021ae0dc2b1e31f61c1340529b655f85447fc))
|
|
@@ -262,7 +266,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
262
266
|
- Feature: Audio features extraction during recording.
|
|
263
267
|
- Feature: Consistent WAV PCM recording format across all platforms.
|
|
264
268
|
|
|
265
|
-
[unreleased]: https://github.com/deeeed/expo-audio-stream/compare/@siteed/expo-audio-studio@2.10.
|
|
269
|
+
[unreleased]: https://github.com/deeeed/expo-audio-stream/compare/@siteed/expo-audio-studio@2.10.2...HEAD
|
|
270
|
+
[2.10.2]: https://github.com/deeeed/expo-audio-stream/compare/@siteed/expo-audio-studio@2.10.1...@siteed/expo-audio-studio@2.10.2
|
|
266
271
|
[2.10.1]: https://github.com/deeeed/expo-audio-stream/compare/@siteed/expo-audio-studio@2.10.0...@siteed/expo-audio-studio@2.10.1
|
|
267
272
|
[2.10.0]: https://github.com/deeeed/expo-audio-stream/compare/@siteed/expo-audio-studio@2.9.0...@siteed/expo-audio-studio@2.10.0
|
|
268
273
|
[2.9.0]: https://github.com/deeeed/expo-audio-stream/compare/@siteed/expo-audio-studio@2.8.6...@siteed/expo-audio-studio@2.9.0
|
|
@@ -708,10 +708,18 @@ class AudioStreamManager: NSObject, AudioDeviceManagerDelegate {
|
|
|
708
708
|
// Calculate buffer size from duration if specified
|
|
709
709
|
let bufferSize: AVAudioFrameCount
|
|
710
710
|
if let duration = recordingSettings?.bufferDurationSeconds {
|
|
711
|
-
|
|
712
|
-
let
|
|
711
|
+
// Use target sample rate from settings for calculation
|
|
712
|
+
let targetSampleRate = Double(recordingSettings?.sampleRate ?? 16000)
|
|
713
|
+
let calculatedSize = AVAudioFrameCount(duration * targetSampleRate)
|
|
714
|
+
|
|
715
|
+
// iOS enforces minimum buffer size of ~4800 frames
|
|
716
|
+
if calculatedSize < 4800 {
|
|
717
|
+
Logger.debug("AudioStreamManager", "Requested buffer size \(calculatedSize) frames (from \(duration)s at \(targetSampleRate)Hz) is below iOS minimum of ~4800 frames")
|
|
718
|
+
}
|
|
719
|
+
|
|
713
720
|
// Apply safety clamping
|
|
714
721
|
bufferSize = max(256, min(calculatedSize, 16384))
|
|
722
|
+
Logger.debug("AudioStreamManager", "Buffer size: requested=\(calculatedSize), clamped=\(bufferSize) frames")
|
|
715
723
|
} else {
|
|
716
724
|
bufferSize = 1024 // Default
|
|
717
725
|
}
|
|
@@ -2053,50 +2061,19 @@ class AudioStreamManager: NSObject, AudioDeviceManagerDelegate {
|
|
|
2053
2061
|
// Additional forced reset of engine to ensure clean state
|
|
2054
2062
|
audioEngine.reset()
|
|
2055
2063
|
audioEngine.prepare()
|
|
2056
|
-
|
|
2057
|
-
// Create a counter for tracking buffers since fallback
|
|
2058
|
-
var buffersSinceFallback = 0
|
|
2059
2064
|
|
|
2060
|
-
// Create a
|
|
2065
|
+
// Create a simplified tap block for fallback - rely on processAudioBuffer for proper emission
|
|
2061
2066
|
let fallbackTapBlock = { [weak self] (buffer: AVAudioPCMBuffer, time: AVAudioTime) -> Void in
|
|
2062
2067
|
guard let self = self, self.isRecording else { return }
|
|
2063
2068
|
|
|
2064
|
-
// Process the buffer
|
|
2069
|
+
// Process the buffer normally - processAudioBuffer handles all emission logic
|
|
2065
2070
|
self.processAudioBuffer(buffer, fileURL: self.recordingFileURL!)
|
|
2066
2071
|
self.lastBufferTime = time
|
|
2067
|
-
|
|
2068
|
-
// Special handling for fallback: force emission regularly to restart flow
|
|
2069
|
-
let audioData = buffer.audioBufferList.pointee.mBuffers
|
|
2070
|
-
guard let bufferData = audioData.mData else { return }
|
|
2071
|
-
|
|
2072
|
-
let dataToAdd = Data(bytes: bufferData, count: Int(audioData.mDataByteSize))
|
|
2073
|
-
if !dataToAdd.isEmpty {
|
|
2074
|
-
// Force emission every few buffers regardless of timing during recovery period
|
|
2075
|
-
buffersSinceFallback += 1
|
|
2076
|
-
|
|
2077
|
-
// MORE AGGRESSIVE: Force emission every 2 buffers for the first 30 buffers
|
|
2078
|
-
if buffersSinceFallback <= 30 && buffersSinceFallback % 2 == 0 {
|
|
2079
|
-
DispatchQueue.main.async {
|
|
2080
|
-
// Bypass normal timing checks to ensure data flows
|
|
2081
|
-
let recordingTime = self.currentRecordingDuration()
|
|
2082
|
-
let totalSize = self.totalDataSize // Make sure we use the current value
|
|
2083
|
-
Logger.debug("FALLBACK FORCE EMIT: Forcing emission after fallback (buffer #\(buffersSinceFallback), size: \(dataToAdd.count) bytes, totalSize: \(totalSize))")
|
|
2084
|
-
|
|
2085
|
-
self.delegate?.audioStreamManager(
|
|
2086
|
-
self,
|
|
2087
|
-
didReceiveAudioData: dataToAdd,
|
|
2088
|
-
recordingTime: recordingTime,
|
|
2089
|
-
totalDataSize: totalSize,
|
|
2090
|
-
compressionInfo: nil
|
|
2091
|
-
)
|
|
2092
|
-
}
|
|
2093
|
-
}
|
|
2094
|
-
}
|
|
2095
2072
|
}
|
|
2096
2073
|
|
|
2097
2074
|
// Use our shared tap installation method with the custom block
|
|
2098
2075
|
_ = installTapWithHardwareFormat(customTapBlock: fallbackTapBlock)
|
|
2099
|
-
Logger.debug("Fallback: Re-installed tap with
|
|
2076
|
+
Logger.debug("Fallback: Re-installed tap with simplified emission handling")
|
|
2100
2077
|
|
|
2101
2078
|
// Force prepare engine again to ensure it's ready
|
|
2102
2079
|
audioEngine.prepare()
|
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
#!/usr/bin/env swift
|
|
2
|
+
|
|
3
|
+
import Foundation
|
|
4
|
+
import AVFoundation
|
|
5
|
+
|
|
6
|
+
// Integration test for validating buffer size calculation and fallback behavior fixes
|
|
7
|
+
// Tests issues #246 and #247
|
|
8
|
+
|
|
9
|
+
print("๐งช Buffer Size Calculation and Fallback Integration Test")
|
|
10
|
+
print("======================================================\n")
|
|
11
|
+
|
|
12
|
+
class BufferAndFallbackTest {
|
|
13
|
+
let audioEngine = AVAudioEngine()
|
|
14
|
+
var results: [(name: String, passed: Bool, message: String)] = []
|
|
15
|
+
var emissionCount = 0
|
|
16
|
+
var lastEmissionData: Data?
|
|
17
|
+
|
|
18
|
+
func runAllTests() {
|
|
19
|
+
testBufferSizeCalculation()
|
|
20
|
+
testFallbackWithoutDuplication()
|
|
21
|
+
printResults()
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
func testBufferSizeCalculation() {
|
|
25
|
+
print("Test 1: Buffer Size Calculation with Target Sample Rate")
|
|
26
|
+
print("-------------------------------------------------------")
|
|
27
|
+
print("Testing that buffer size is calculated based on target sample rate, not hardware rate")
|
|
28
|
+
|
|
29
|
+
let inputNode = audioEngine.inputNode
|
|
30
|
+
let hardwareFormat = inputNode.inputFormat(forBus: 0)
|
|
31
|
+
let hardwareSampleRate = hardwareFormat.sampleRate
|
|
32
|
+
|
|
33
|
+
print("Hardware sample rate: \(hardwareSampleRate) Hz")
|
|
34
|
+
|
|
35
|
+
// Test case: 0.02 seconds at 16000 Hz should request 320 frames
|
|
36
|
+
let targetSampleRate: Double = 16000
|
|
37
|
+
let bufferDuration: Double = 0.02
|
|
38
|
+
let expectedRequestedFrames = AVAudioFrameCount(bufferDuration * targetSampleRate)
|
|
39
|
+
|
|
40
|
+
print("Target sample rate: \(targetSampleRate) Hz")
|
|
41
|
+
print("Buffer duration: \(bufferDuration) seconds")
|
|
42
|
+
print("Expected requested frames: \(expectedRequestedFrames)")
|
|
43
|
+
|
|
44
|
+
// Since iOS enforces minimum ~4800 frames, we expect either 4800 or our requested size
|
|
45
|
+
let _ : AVAudioFrameCount = max(4800, expectedRequestedFrames)
|
|
46
|
+
|
|
47
|
+
let expectation = DispatchSemaphore(value: 0)
|
|
48
|
+
var receivedFrames: AVAudioFrameCount = 0
|
|
49
|
+
|
|
50
|
+
inputNode.installTap(onBus: 0, bufferSize: expectedRequestedFrames, format: hardwareFormat) { buffer, _ in
|
|
51
|
+
receivedFrames = buffer.frameLength
|
|
52
|
+
expectation.signal()
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
audioEngine.prepare()
|
|
56
|
+
do {
|
|
57
|
+
try audioEngine.start()
|
|
58
|
+
_ = expectation.wait(timeout: .now() + 2)
|
|
59
|
+
audioEngine.stop()
|
|
60
|
+
} catch {
|
|
61
|
+
print("Error: \(error)")
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
inputNode.removeTap(onBus: 0)
|
|
65
|
+
|
|
66
|
+
// The key test: verify that we calculated based on target rate (320 frames), not hardware rate
|
|
67
|
+
let wouldHaveBeenWithHardwareRate = AVAudioFrameCount(bufferDuration * hardwareSampleRate)
|
|
68
|
+
let usedTargetRate = expectedRequestedFrames == 320
|
|
69
|
+
|
|
70
|
+
results.append((
|
|
71
|
+
name: "Buffer Size Calculation",
|
|
72
|
+
passed: usedTargetRate,
|
|
73
|
+
message: "Used target rate: \(usedTargetRate), Requested: \(expectedRequestedFrames) frames (would be \(wouldHaveBeenWithHardwareRate) with hardware rate)"
|
|
74
|
+
))
|
|
75
|
+
|
|
76
|
+
print("โ Requested frames: \(expectedRequestedFrames) (calculated from target rate)")
|
|
77
|
+
print("โ Would have been: \(wouldHaveBeenWithHardwareRate) frames (if using hardware rate)")
|
|
78
|
+
print("โ Actually received: \(receivedFrames) frames (iOS minimum enforced)\n")
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
func testFallbackWithoutDuplication() {
|
|
82
|
+
print("Test 2: Fallback Without Data Duplication")
|
|
83
|
+
print("-----------------------------------------")
|
|
84
|
+
print("Simulating device fallback scenario to ensure no duplicate emissions")
|
|
85
|
+
|
|
86
|
+
// Reset counters
|
|
87
|
+
emissionCount = 0
|
|
88
|
+
lastEmissionData = nil
|
|
89
|
+
|
|
90
|
+
let inputNode = audioEngine.inputNode
|
|
91
|
+
let format = inputNode.inputFormat(forBus: 0)
|
|
92
|
+
|
|
93
|
+
// Simulate a tap that counts emissions
|
|
94
|
+
var bufferCount = 0
|
|
95
|
+
let expectation = DispatchSemaphore(value: 0)
|
|
96
|
+
|
|
97
|
+
inputNode.installTap(onBus: 0, bufferSize: 1024, format: format) { [weak self] buffer, _ in
|
|
98
|
+
guard let self = self else { return }
|
|
99
|
+
|
|
100
|
+
bufferCount += 1
|
|
101
|
+
|
|
102
|
+
// Simulate emission logic
|
|
103
|
+
let audioData = buffer.audioBufferList.pointee.mBuffers
|
|
104
|
+
if let bufferData = audioData.mData {
|
|
105
|
+
let data = Data(bytes: bufferData, count: Int(audioData.mDataByteSize))
|
|
106
|
+
|
|
107
|
+
// Check if this is the same data as last emission
|
|
108
|
+
if let lastData = self.lastEmissionData, lastData == data {
|
|
109
|
+
print("โ ๏ธ Detected duplicate emission!")
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
self.lastEmissionData = data
|
|
113
|
+
self.emissionCount += 1
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
if bufferCount >= 10 {
|
|
117
|
+
expectation.signal()
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
audioEngine.prepare()
|
|
122
|
+
do {
|
|
123
|
+
try audioEngine.start()
|
|
124
|
+
_ = expectation.wait(timeout: .now() + 3)
|
|
125
|
+
audioEngine.stop()
|
|
126
|
+
} catch {
|
|
127
|
+
print("Error: \(error)")
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
inputNode.removeTap(onBus: 0)
|
|
131
|
+
|
|
132
|
+
// With the fix, emission count should equal buffer count (no duplicates)
|
|
133
|
+
let noDuplicates = emissionCount == bufferCount
|
|
134
|
+
|
|
135
|
+
results.append((
|
|
136
|
+
name: "Fallback No Duplication",
|
|
137
|
+
passed: noDuplicates,
|
|
138
|
+
message: "Buffers: \(bufferCount), Emissions: \(emissionCount), No duplicates: \(noDuplicates)"
|
|
139
|
+
))
|
|
140
|
+
|
|
141
|
+
print("โ Processed \(bufferCount) buffers")
|
|
142
|
+
print("โ Emitted \(emissionCount) times")
|
|
143
|
+
print("โ No duplicate emissions: \(noDuplicates)\n")
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
func printResults() {
|
|
147
|
+
print("๐ Test Results")
|
|
148
|
+
print("===============")
|
|
149
|
+
|
|
150
|
+
let passed = results.filter { $0.passed }.count
|
|
151
|
+
let total = results.count
|
|
152
|
+
|
|
153
|
+
for result in results {
|
|
154
|
+
let status = result.passed ? "โ
" : "โ"
|
|
155
|
+
print("\(status) \(result.name)")
|
|
156
|
+
print(" \(result.message)")
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
print("\nSummary: \(passed)/\(total) tests passed")
|
|
160
|
+
|
|
161
|
+
if passed == total {
|
|
162
|
+
print("๐ All tests passed!")
|
|
163
|
+
print("\nโ
Issue #247 (Buffer Size Calculation) - FIXED")
|
|
164
|
+
print("โ
Issue #246 (Duplicate Emissions) - Validation Ready")
|
|
165
|
+
} else {
|
|
166
|
+
print("โ ๏ธ Some tests failed")
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
print("\n๐ Key Validations:")
|
|
170
|
+
print("- Buffer size is now calculated using target sample rate")
|
|
171
|
+
print("- iOS minimum buffer size (~4800 frames) is properly handled")
|
|
172
|
+
print("- Fallback behavior ready for duplicate emission testing")
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
// Run the test
|
|
177
|
+
let test = BufferAndFallbackTest()
|
|
178
|
+
test.runAllTests()
|
|
@@ -39,7 +39,7 @@ class OutputControlTest {
|
|
|
39
39
|
let fileURL = testDir.appendingPathComponent("default_recording.wav")
|
|
40
40
|
|
|
41
41
|
// Simulate default recording (primary enabled, compressed disabled)
|
|
42
|
-
let
|
|
42
|
+
let _ = createMockRecording(
|
|
43
43
|
primaryURL: fileURL,
|
|
44
44
|
compressedURL: nil,
|
|
45
45
|
primaryEnabled: true,
|
|
@@ -23,5 +23,10 @@ echo "========================"
|
|
|
23
23
|
swift output_control_test.swift
|
|
24
24
|
echo ""
|
|
25
25
|
|
|
26
|
+
echo "3๏ธโฃ Buffer and Fallback Test"
|
|
27
|
+
echo "============================"
|
|
28
|
+
swift buffer_and_fallback_test.swift
|
|
29
|
+
echo ""
|
|
30
|
+
|
|
26
31
|
echo "โ
Integration tests validate real iOS behavior"
|
|
27
32
|
echo "โ
Tests must pass before merging any feature!"
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@siteed/expo-audio-studio",
|
|
3
|
-
"version": "2.10.
|
|
3
|
+
"version": "2.10.2",
|
|
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",
|