@siteed/audio-studio 3.1.1 → 3.2.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.
Files changed (96) hide show
  1. package/CHANGELOG.md +375 -4
  2. package/android/src/main/java/net/siteed/audiostudio/AudioStreamDecoder.kt +852 -0
  3. package/android/src/main/java/net/siteed/audiostudio/AudioStudioModule.kt +167 -3
  4. package/android/src/main/java/net/siteed/audiostudio/Constants.kt +4 -0
  5. package/build/cjs/errors/AudioStreamError.js +161 -0
  6. package/build/cjs/errors/AudioStreamError.js.map +1 -0
  7. package/build/cjs/errors/AudioStreamError.test.js +82 -0
  8. package/build/cjs/errors/AudioStreamError.test.js.map +1 -0
  9. package/build/cjs/index.js +7 -1
  10. package/build/cjs/index.js.map +1 -1
  11. package/build/cjs/streamAudioData.js +534 -0
  12. package/build/cjs/streamAudioData.js.map +1 -0
  13. package/build/cjs/utils/audioProcessing.js +14 -10
  14. package/build/cjs/utils/audioProcessing.js.map +1 -1
  15. package/build/esm/errors/AudioStreamError.js +156 -0
  16. package/build/esm/errors/AudioStreamError.js.map +1 -0
  17. package/build/esm/errors/AudioStreamError.test.js +80 -0
  18. package/build/esm/errors/AudioStreamError.test.js.map +1 -0
  19. package/build/esm/index.js +3 -1
  20. package/build/esm/index.js.map +1 -1
  21. package/build/esm/streamAudioData.js +527 -0
  22. package/build/esm/streamAudioData.js.map +1 -0
  23. package/build/esm/utils/audioProcessing.js +14 -10
  24. package/build/esm/utils/audioProcessing.js.map +1 -1
  25. package/build/types/errors/AudioStreamError.d.ts +25 -0
  26. package/build/types/errors/AudioStreamError.d.ts.map +1 -0
  27. package/build/types/errors/AudioStreamError.test.d.ts +2 -0
  28. package/build/types/errors/AudioStreamError.test.d.ts.map +1 -0
  29. package/build/types/index.d.ts +5 -1
  30. package/build/types/index.d.ts.map +1 -1
  31. package/build/types/streamAudioData.d.ts +119 -0
  32. package/build/types/streamAudioData.d.ts.map +1 -0
  33. package/build/types/utils/audioProcessing.d.ts +2 -2
  34. package/build/types/utils/audioProcessing.d.ts.map +1 -1
  35. package/ios/AudioProcessingHelpers.swift +10 -5
  36. package/ios/AudioStreamDecoder.swift +614 -0
  37. package/ios/AudioStudioModule.swift +186 -3
  38. package/package.json +163 -146
  39. package/scripts/README.md +58 -0
  40. package/src/errors/AudioStreamError.test.ts +92 -0
  41. package/src/errors/AudioStreamError.ts +199 -0
  42. package/src/index.ts +24 -0
  43. package/src/streamAudioData.ts +758 -0
  44. package/src/utils/audioProcessing.ts +25 -14
  45. package/android/src/androidTest/assets/chorus.wav +0 -0
  46. package/android/src/androidTest/assets/jfk.wav +0 -0
  47. package/android/src/androidTest/assets/osr_us_000_0010_8k.wav +0 -0
  48. package/android/src/androidTest/assets/recorder_hello_world.wav +0 -0
  49. package/android/src/androidTest/java/net/siteed/audiostudio/AudioFinalMetadataContractInstrumentedTest.kt +0 -190
  50. package/android/src/androidTest/java/net/siteed/audiostudio/AudioProcessorInstrumentedTest.kt +0 -197
  51. package/android/src/androidTest/java/net/siteed/audiostudio/AudioRecorderInstrumentedTest.kt +0 -487
  52. package/android/src/androidTest/java/net/siteed/audiostudio/AudioRecorderPerformanceInstrumentedTest.kt +0 -250
  53. package/android/src/androidTest/java/net/siteed/audiostudio/OpusRangeDecodeRegressionInstrumentedTest.kt +0 -186
  54. package/android/src/androidTest/java/net/siteed/audiostudio/integration/AudioFocusStrategyIntegrationTest.kt +0 -332
  55. package/android/src/androidTest/java/net/siteed/audiostudio/integration/BufferDurationIntegrationTest.kt +0 -324
  56. package/android/src/androidTest/java/net/siteed/audiostudio/integration/CompressedOnlyOutputTest.kt +0 -253
  57. package/android/src/androidTest/java/net/siteed/audiostudio/integration/DeviceDisconnectionFallbackTest.kt +0 -218
  58. package/android/src/androidTest/java/net/siteed/audiostudio/integration/EventEmissionIntervalTest.kt +0 -120
  59. package/android/src/androidTest/java/net/siteed/audiostudio/integration/M4aFormatTest.kt +0 -345
  60. package/android/src/androidTest/java/net/siteed/audiostudio/integration/OutputControlIntegrationTest.kt +0 -340
  61. package/android/src/androidTest/java/net/siteed/audiostudio/integration/PcmStreamingDurationTest.kt +0 -252
  62. package/android/src/androidTest/java/net/siteed/audiostudio/integration/README.md +0 -95
  63. package/android/src/androidTest/java/net/siteed/audiostudio/integration/run_integration_tests.sh +0 -43
  64. package/android/src/test/java/net/siteed/audiostudio/AndroidCallStateTest.kt +0 -37
  65. package/android/src/test/java/net/siteed/audiostudio/AndroidEventEmitterTest.kt +0 -28
  66. package/android/src/test/java/net/siteed/audiostudio/AudioFileHandlerTest.kt +0 -279
  67. package/android/src/test/java/net/siteed/audiostudio/AudioFocusStrategyTest.kt +0 -249
  68. package/android/src/test/java/net/siteed/audiostudio/AudioFormatTest.kt +0 -151
  69. package/android/src/test/java/net/siteed/audiostudio/AudioFormatUtilsTest.kt +0 -273
  70. package/android/src/test/java/net/siteed/audiostudio/DeviceDisconnectionFallbackUnitTest.kt +0 -140
  71. package/android/src/test/java/net/siteed/audiostudio/InterruptionAutoResumePolicyTest.kt +0 -49
  72. package/android/src/test/resources/chorus.wav +0 -0
  73. package/android/src/test/resources/generate_test_audio.py +0 -94
  74. package/android/src/test/resources/jfk.wav +0 -0
  75. package/android/src/test/resources/osr_us_000_0010_8k.wav +0 -0
  76. package/android/src/test/resources/recorder_hello_world.wav +0 -0
  77. package/ios/AudioStudioTests/AudioFileHandlerTests.swift +0 -338
  78. package/ios/AudioStudioTests/AudioFormatUtilsTests.swift +0 -331
  79. package/ios/AudioStudioTests/AudioTestHelpers.swift +0 -130
  80. package/ios/AudioStudioTests/CompressedOnlyOutputTests.swift +0 -334
  81. package/ios/AudioStudioTests/EventEmissionIntervalTests.swift +0 -105
  82. package/ios/AudioStudioTests/Info.plist +0 -22
  83. package/ios/AudioStudioTests/README.md +0 -39
  84. package/ios/AudioStudioTests/SimpleAudioTest.swift +0 -98
  85. package/ios/AudioStudioTests/TestAudioGenerator.swift +0 -75
  86. package/ios/tests/README.md +0 -41
  87. package/ios/tests/integration/buffer_and_fallback_test.swift +0 -178
  88. package/ios/tests/integration/buffer_duration_test.swift +0 -185
  89. package/ios/tests/integration/compressed_only_output_test.swift +0 -271
  90. package/ios/tests/integration/output_control_test.swift +0 -322
  91. package/ios/tests/integration/run_integration_tests.sh +0 -37
  92. package/ios/tests/opus_support_test_macos.swift +0 -154
  93. package/ios/tests/standalone/audio_processing_test.swift +0 -144
  94. package/ios/tests/standalone/audio_recording_test.swift +0 -277
  95. package/ios/tests/standalone/audio_streaming_test.swift +0 -249
  96. package/ios/tests/standalone/standalone_test.swift +0 -144
@@ -1,130 +0,0 @@
1
- import Foundation
2
- import AVFoundation
3
- import Accelerate
4
-
5
- extension AVAudioPCMBuffer {
6
-
7
- /// Convert buffer to Data
8
- func toData() -> Data {
9
- let audioFormat = self.format
10
- let channelCount = Int(audioFormat.channelCount)
11
- let frameLength = Int(self.frameLength)
12
-
13
- var data = Data()
14
-
15
- if let floatData = self.floatChannelData {
16
- // Convert float samples to 16-bit PCM
17
- for frame in 0..<frameLength {
18
- for channel in 0..<channelCount {
19
- let sample = floatData[channel][frame]
20
- let int16Sample = Int16(max(-32768, min(32767, sample * 32767)))
21
- data.append(contentsOf: withUnsafeBytes(of: int16Sample) { Array($0) })
22
- }
23
- }
24
- } else if let int16Data = self.int16ChannelData {
25
- // Already 16-bit, just copy
26
- for frame in 0..<frameLength {
27
- for channel in 0..<channelCount {
28
- let sample = int16Data[channel][frame]
29
- data.append(contentsOf: withUnsafeBytes(of: sample) { Array($0) })
30
- }
31
- }
32
- }
33
-
34
- return data
35
- }
36
-
37
- /// Calculate RMS (Root Mean Square) of the buffer
38
- func rms() -> Float {
39
- guard let channelData = self.floatChannelData else { return 0 }
40
-
41
- let channelCount = Int(self.format.channelCount)
42
- let frameLength = Int(self.frameLength)
43
-
44
- var sum: Float = 0
45
- var sampleCount = 0
46
-
47
- for channel in 0..<channelCount {
48
- for frame in 0..<frameLength {
49
- let sample = channelData[channel][frame]
50
- sum += sample * sample
51
- sampleCount += 1
52
- }
53
- }
54
-
55
- return sqrt(sum / Float(sampleCount))
56
- }
57
-
58
- /// Calculate energy of the buffer
59
- func energy() -> Float {
60
- guard let channelData = self.floatChannelData else { return 0 }
61
-
62
- let channelCount = Int(self.format.channelCount)
63
- let frameLength = Int(self.frameLength)
64
-
65
- var sum: Float = 0
66
-
67
- for channel in 0..<channelCount {
68
- for frame in 0..<frameLength {
69
- let sample = channelData[channel][frame]
70
- sum += sample * sample
71
- }
72
- }
73
-
74
- return sum
75
- }
76
- }
77
-
78
- extension Data {
79
-
80
- /// Convert PCM data to float array
81
- func toFloatArray(bitDepth: Int = 16) -> [Float] {
82
- var floats = [Float]()
83
-
84
- switch bitDepth {
85
- case 16:
86
- let samples = self.withUnsafeBytes { $0.bindMemory(to: Int16.self) }
87
- for sample in samples {
88
- floats.append(Float(sample) / Float(Int16.max))
89
- }
90
- case 32:
91
- let samples = self.withUnsafeBytes { $0.bindMemory(to: Int32.self) }
92
- for sample in samples {
93
- floats.append(Float(sample) / Float(Int32.max))
94
- }
95
- default:
96
- break
97
- }
98
-
99
- return floats
100
- }
101
-
102
- /// Calculate RMS from PCM data
103
- func rms(bitDepth: Int = 16) -> Float {
104
- let floats = toFloatArray(bitDepth: bitDepth)
105
- guard !floats.isEmpty else { return 0 }
106
-
107
- let sum = floats.reduce(0) { $0 + $1 * $1 }
108
- return sqrt(sum / Float(floats.count))
109
- }
110
-
111
- /// Calculate energy from PCM data
112
- func energy(bitDepth: Int = 16) -> Float {
113
- let floats = toFloatArray(bitDepth: bitDepth)
114
- return floats.reduce(0) { $0 + $1 * $1 }
115
- }
116
- }
117
-
118
- // Test assertion helpers
119
- extension XCTestCase {
120
-
121
- /// Assert two float values are approximately equal
122
- func XCTAssertApproximatelyEqual(_ value1: Float, _ value2: Float, tolerance: Float = 0.0001, _ message: String = "", file: StaticString = #file, line: UInt = #line) {
123
- XCTAssertLessThanOrEqual(abs(value1 - value2), tolerance, message, file: file, line: line)
124
- }
125
-
126
- /// Assert two double values are approximately equal
127
- func XCTAssertApproximatelyEqual(_ value1: Double, _ value2: Double, tolerance: Double = 0.0001, _ message: String = "", file: StaticString = #file, line: UInt = #line) {
128
- XCTAssertLessThanOrEqual(abs(value1 - value2), tolerance, message, file: file, line: line)
129
- }
130
- }
@@ -1,334 +0,0 @@
1
- import XCTest
2
- import AVFoundation
3
- @testable import AudioStudio
4
-
5
- class CompressedOnlyOutputTests: XCTestCase {
6
-
7
- var audioManager: AudioStreamManager!
8
- var testDelegate: TestAudioStreamDelegate!
9
-
10
- override func setUp() {
11
- super.setUp()
12
- audioManager = AudioStreamManager()
13
- testDelegate = TestAudioStreamDelegate()
14
- audioManager.delegate = testDelegate
15
- }
16
-
17
- override func tearDown() {
18
- audioManager.stopRecording()
19
- audioManager = nil
20
- testDelegate = nil
21
- super.tearDown()
22
- }
23
-
24
- // MARK: - Test Compressed-Only Output (Issue #244)
25
-
26
- func testAACCompressedSampleRateFallsBackForLowRequestedRate() {
27
- XCTAssertEqual(
28
- AudioStreamManager.compatibleAACCompressedSampleRate(
29
- requestedSampleRate: 16000,
30
- sessionSampleRate: 48000
31
- ),
32
- 48000,
33
- "Low requested sample rates should use the active session rate when it is AAC-compatible"
34
- )
35
-
36
- XCTAssertEqual(
37
- AudioStreamManager.compatibleAACCompressedSampleRate(
38
- requestedSampleRate: 16000,
39
- sessionSampleRate: 0
40
- ),
41
- 44100,
42
- "Low requested sample rates should never be passed directly to AVAudioRecorder for AAC"
43
- )
44
- }
45
-
46
- func testAACCompressedSampleRateKeepsCompatibleRequestedRate() {
47
- XCTAssertEqual(
48
- AudioStreamManager.compatibleAACCompressedSampleRate(
49
- requestedSampleRate: 44100,
50
- sessionSampleRate: 48000
51
- ),
52
- 44100,
53
- "Already-compatible requested sample rates should preserve existing behavior"
54
- )
55
-
56
- XCTAssertEqual(
57
- AudioStreamManager.compatibleAACCompressedSampleRate(
58
- requestedSampleRate: 48000,
59
- sessionSampleRate: 44100
60
- ),
61
- 48000,
62
- "High requested sample rates should not be reduced to the session rate"
63
- )
64
- }
65
-
66
- func testCompressedOnlyOutputWithAAC() {
67
- // Given: Recording settings with primary disabled and compressed enabled (AAC)
68
- var settings = RecordingSettings(
69
- sampleRate: 44100,
70
- desiredSampleRate: 44100,
71
- autoResumeAfterInterruption: false
72
- )
73
- settings.numberOfChannels = 1
74
- settings.bitDepth = 16
75
- settings.output.primary.enabled = false
76
- settings.output.compressed.enabled = true
77
- settings.output.compressed.format = "aac"
78
- settings.output.compressed.bitrate = 128000
79
-
80
- let expectation = self.expectation(description: "Recording should complete with compression info")
81
- var capturedCompressionInfo: [String: Any]?
82
- var capturedError: String?
83
-
84
- // When: Start and stop recording
85
- testDelegate.onAudioData = { data, recordingTime, totalDataSize, compressionInfo in
86
- capturedCompressionInfo = compressionInfo
87
- }
88
-
89
- testDelegate.onError = { error in
90
- capturedError = error
91
- }
92
-
93
- // Start recording returns a result that we can check
94
- let startResult = audioManager.startRecording(settings: settings)
95
- XCTAssertNotNil(startResult, "Start recording should return a result")
96
-
97
- // Generate and process some test audio to ensure compression happens
98
- let testBuffer = TestAudioGenerator.generateTone(frequency: 440, duration: 0.1, sampleRate: 44100)
99
- if let buffer = testBuffer {
100
- // Process multiple chunks to ensure we have enough data
101
- for _ in 0..<5 {
102
- audioManager.processAudioBuffer(buffer, time: AVAudioTime(hostTime: mach_absolute_time()))
103
- Thread.sleep(forTimeInterval: 0.1)
104
- }
105
- }
106
-
107
- // Stop recording and get the result
108
- let recordingResult = audioManager.stopRecording()
109
- expectation.fulfill()
110
-
111
- waitForExpectations(timeout: 2.0) { error in
112
- XCTAssertNil(error, "Recording should complete within timeout")
113
- }
114
-
115
- // Then: Verify compression info is returned
116
- XCTAssertNil(capturedError, "No errors should occur during recording")
117
- XCTAssertNotNil(recordingResult, "Recording result should not be nil")
118
- XCTAssertNotNil(recordingResult?.compression, "Compression info should be included")
119
-
120
- if let compression = recordingResult?.compression {
121
- XCTAssertEqual(compression.format, "aac", "Format should be AAC")
122
- XCTAssertEqual(compression.bitrate, 128000, "Bitrate should match settings")
123
- XCTAssertFalse(compression.compressedFileUri.isEmpty, "Compressed file URI should not be empty")
124
- XCTAssertGreaterThan(compression.size, 0, "Compressed file size should be greater than 0")
125
- XCTAssertEqual(compression.mimeType, "audio/aac", "MIME type should be audio/aac")
126
- }
127
-
128
- // Verify main result uses compressed info when primary is disabled
129
- XCTAssertEqual(recordingResult?.fileUri, recordingResult?.compression?.compressedFileUri,
130
- "Main fileUri should use compressed URI when primary is disabled")
131
- XCTAssertEqual(recordingResult?.mimeType, "audio/aac",
132
- "Main mimeType should reflect compressed format")
133
- }
134
-
135
- func testCompressedOnlyOutputWithOpusFallback() {
136
- // Given: Recording settings with primary disabled and compressed enabled (Opus)
137
- var settings = RecordingSettings(
138
- sampleRate: 48000,
139
- desiredSampleRate: 48000,
140
- autoResumeAfterInterruption: false
141
- )
142
- settings.numberOfChannels = 1
143
- settings.bitDepth = 16
144
- settings.output.primary.enabled = false
145
- settings.output.compressed.enabled = true
146
- settings.output.compressed.format = "opus" // Should fallback to AAC on iOS
147
- settings.output.compressed.bitrate = 64000
148
-
149
- let expectation = self.expectation(description: "Recording should complete with AAC fallback")
150
-
151
- // Start recording
152
- let startResult = audioManager.startRecording(settings: settings)
153
- XCTAssertNotNil(startResult, "Start recording should return a result")
154
-
155
- // Generate test audio
156
- if let buffer = TestAudioGenerator.generateTone(frequency: 440, duration: 0.1, sampleRate: 48000) {
157
- for _ in 0..<3 {
158
- audioManager.processAudioBuffer(buffer, time: AVAudioTime(hostTime: mach_absolute_time()))
159
- Thread.sleep(forTimeInterval: 0.1)
160
- }
161
- }
162
-
163
- // Stop recording
164
- let recordingResult = audioManager.stopRecording()
165
- expectation.fulfill()
166
-
167
- waitForExpectations(timeout: 2.0) { error in
168
- XCTAssertNil(error, "Recording should complete within timeout")
169
- }
170
-
171
- // Then: Verify Opus falls back to AAC on iOS
172
- XCTAssertNotNil(recordingResult?.compression, "Compression info should be included")
173
- XCTAssertEqual(recordingResult?.compression?.format, "aac",
174
- "Opus should fallback to AAC on iOS")
175
- XCTAssertEqual(recordingResult?.compression?.bitrate, 64000,
176
- "Bitrate should be preserved from original settings")
177
- }
178
-
179
- func testCompressedFileAccessibility() {
180
- // Given: Recording with compressed output
181
- var settings = RecordingSettings(
182
- sampleRate: 44100,
183
- desiredSampleRate: 44100,
184
- autoResumeAfterInterruption: false
185
- )
186
- settings.numberOfChannels = 1
187
- settings.bitDepth = 16
188
- settings.output.primary.enabled = false
189
- settings.output.compressed.enabled = true
190
- settings.output.compressed.format = "aac"
191
- settings.output.compressed.bitrate = 96000
192
-
193
- let expectation = self.expectation(description: "Compressed file should be accessible")
194
-
195
- // Start recording
196
- let startResult = audioManager.startRecording(settings: settings)
197
- XCTAssertNotNil(startResult, "Start recording should return a result")
198
-
199
- // Generate substantial audio data to ensure file is created
200
- if let buffer = TestAudioGenerator.generateTone(frequency: 440, duration: 0.2, sampleRate: 44100) {
201
- for _ in 0..<5 {
202
- audioManager.processAudioBuffer(buffer, time: AVAudioTime(hostTime: mach_absolute_time()))
203
- Thread.sleep(forTimeInterval: 0.1)
204
- }
205
- }
206
-
207
- // Stop recording
208
- let recordingResult = audioManager.stopRecording()
209
- expectation.fulfill()
210
-
211
- waitForExpectations(timeout: 2.0) { error in
212
- XCTAssertNil(error, "Recording should complete within timeout")
213
- }
214
-
215
- // Then: Verify compressed file is accessible
216
- if let compression = recordingResult?.compression {
217
- let fileURL = URL(string: compression.compressedFileUri)
218
- XCTAssertNotNil(fileURL, "Compressed file URL should be valid")
219
-
220
- if let url = fileURL {
221
- let fileExists = FileManager.default.fileExists(atPath: url.path)
222
- XCTAssertTrue(fileExists, "Compressed file should exist at the specified path")
223
-
224
- // Verify file size matches reported size
225
- if fileExists {
226
- do {
227
- let attributes = try FileManager.default.attributesOfItem(atPath: url.path)
228
- let actualSize = attributes[.size] as? Int64 ?? 0
229
- XCTAssertEqual(actualSize, compression.size,
230
- "Reported size should match actual file size")
231
- } catch {
232
- XCTFail("Failed to get file attributes: \(error)")
233
- }
234
- }
235
- }
236
- } else {
237
- XCTFail("Compression info should not be nil")
238
- }
239
- }
240
-
241
- func testStreamingOnlyWithCompression() {
242
- // Given: Streaming configuration with compression
243
- var settings = RecordingSettings(
244
- sampleRate: 44100,
245
- desiredSampleRate: 44100,
246
- autoResumeAfterInterruption: false
247
- )
248
- settings.numberOfChannels = 1
249
- settings.bitDepth = 16
250
- settings.output.primary.enabled = false
251
- settings.output.compressed.enabled = true
252
- settings.output.compressed.format = "aac"
253
- settings.output.compressed.bitrate = 128000
254
- settings.interval = 100 // Enable streaming with 100ms intervals
255
-
256
- let expectation = self.expectation(description: "Streaming should work with compressed output")
257
- var dataEventCount = 0
258
- var hasCompressionInfo = false
259
-
260
- testDelegate.onAudioData = { data, recordingTime, totalDataSize, compressionInfo in
261
- dataEventCount += 1
262
- if compressionInfo != nil {
263
- hasCompressionInfo = true
264
- }
265
- }
266
-
267
- // Start recording
268
- let startResult = audioManager.startRecording(settings: settings)
269
- XCTAssertNotNil(startResult, "Start recording should return a result")
270
-
271
- // Generate audio data
272
- if let buffer = TestAudioGenerator.generateTone(frequency: 440, duration: 0.1, sampleRate: 44100) {
273
- for _ in 0..<5 {
274
- audioManager.processAudioBuffer(buffer, time: AVAudioTime(hostTime: mach_absolute_time()))
275
- Thread.sleep(forTimeInterval: 0.1)
276
- }
277
- }
278
-
279
- // Stop recording
280
- let recordingResult = audioManager.stopRecording()
281
- expectation.fulfill()
282
-
283
- waitForExpectations(timeout: 2.0) { error in
284
- XCTAssertNil(error, "Recording should complete within timeout")
285
- }
286
-
287
- // Then: Verify streaming worked and compression info is available
288
- XCTAssertGreaterThan(dataEventCount, 0, "Should have received audio data events")
289
- XCTAssertTrue(hasCompressionInfo, "Should have received compression info in data events")
290
- XCTAssertNotNil(recordingResult?.compression, "Compression info should be available in final result")
291
- }
292
- }
293
-
294
- // MARK: - Test Delegate
295
-
296
- class TestAudioStreamDelegate: AudioStreamManagerDelegate {
297
- var onAudioData: ((Data, TimeInterval, Int64, [String: Any]?) -> Void)?
298
- var onError: ((String) -> Void)?
299
- var onAnalysis: ((AudioAnalysisData?) -> Void)?
300
-
301
- func audioStreamManager(
302
- _ manager: AudioStreamManager,
303
- didReceiveAudioData data: Data,
304
- recordingTime: TimeInterval,
305
- totalDataSize: Int64,
306
- compressionInfo: [String: Any]?
307
- ) {
308
- onAudioData?(data, recordingTime, totalDataSize, compressionInfo)
309
- }
310
-
311
- func audioStreamManager(_ manager: AudioStreamManager, didReceiveProcessingResult result: AudioAnalysisData?) {
312
- onAnalysis?(result)
313
- }
314
-
315
- func audioStreamManager(_ manager: AudioStreamManager, didPauseRecording pauseTime: Date) {
316
- // Optional: Handle pause
317
- }
318
-
319
- func audioStreamManager(_ manager: AudioStreamManager, didResumeRecording resumeTime: Date) {
320
- // Optional: Handle resume
321
- }
322
-
323
- func audioStreamManager(_ manager: AudioStreamManager, didUpdateNotificationState isPaused: Bool) {
324
- // Optional: Handle notification state
325
- }
326
-
327
- func audioStreamManager(_ manager: AudioStreamManager, didReceiveInterruption info: [String: Any]) {
328
- // Optional: Handle interruption
329
- }
330
-
331
- func audioStreamManager(_ manager: AudioStreamManager, didFailWithError error: String) {
332
- onError?(error)
333
- }
334
- }
@@ -1,105 +0,0 @@
1
- import XCTest
2
- @testable import AudioStudio
3
-
4
- class EventEmissionIntervalTests: XCTestCase {
5
-
6
- var audioStreamManager: AudioStreamManager!
7
-
8
- override func setUp() {
9
- super.setUp()
10
- audioStreamManager = AudioStreamManager()
11
- }
12
-
13
- override func tearDown() {
14
- audioStreamManager = nil
15
- super.tearDown()
16
- }
17
-
18
- func testIntervalClamping() {
19
- // Test case 1: Interval below minimum (10ms)
20
- let config1 = RecordingConfig()
21
- config1.interval = 10
22
- config1.intervalAnalysis = 10
23
-
24
- audioStreamManager.prepareRecording(with: config1) { error in
25
- XCTAssertNil(error, "Should prepare successfully")
26
- }
27
-
28
- // After our fix, this should be 10ms (0.01s), not 100ms (0.1s)
29
- XCTAssertEqual(audioStreamManager.emissionInterval, 0.01, accuracy: 0.001)
30
- XCTAssertEqual(audioStreamManager.emissionIntervalAnalysis, 0.01, accuracy: 0.001)
31
-
32
- // Test case 2: Interval at old minimum (100ms)
33
- let config2 = RecordingConfig()
34
- config2.interval = 100
35
- config2.intervalAnalysis = 100
36
-
37
- audioStreamManager.prepareRecording(with: config2) { error in
38
- XCTAssertNil(error, "Should prepare successfully")
39
- }
40
-
41
- XCTAssertEqual(audioStreamManager.emissionInterval, 0.1, accuracy: 0.001)
42
- XCTAssertEqual(audioStreamManager.emissionIntervalAnalysis, 0.1, accuracy: 0.001)
43
-
44
- // Test case 3: Interval above minimum (200ms)
45
- let config3 = RecordingConfig()
46
- config3.interval = 200
47
- config3.intervalAnalysis = 200
48
-
49
- audioStreamManager.prepareRecording(with: config3) { error in
50
- XCTAssertNil(error, "Should prepare successfully")
51
- }
52
-
53
- XCTAssertEqual(audioStreamManager.emissionInterval, 0.2, accuracy: 0.001)
54
- XCTAssertEqual(audioStreamManager.emissionIntervalAnalysis, 0.2, accuracy: 0.001)
55
- }
56
-
57
- func testEventEmissionTiming() {
58
- let expectation = self.expectation(description: "Should emit events at correct intervals")
59
- var eventTimestamps: [TimeInterval] = []
60
- let testDuration: TimeInterval = 0.5 // 500ms
61
-
62
- // Configure for 10ms intervals
63
- let config = RecordingConfig()
64
- config.interval = 10
65
- config.intervalAnalysis = 10
66
- config.enableProcessing = true
67
- config.features = ["fft": true]
68
-
69
- // Mock event handler to capture timestamps
70
- audioStreamManager.onAudioData = { _ in
71
- eventTimestamps.append(Date().timeIntervalSince1970)
72
- }
73
-
74
- audioStreamManager.startRecording(with: config) { error in
75
- XCTAssertNil(error, "Should start recording successfully")
76
-
77
- // Record for test duration
78
- DispatchQueue.main.asyncAfter(deadline: .now() + testDuration) {
79
- self.audioStreamManager.stopRecording()
80
-
81
- // Analyze intervals
82
- if eventTimestamps.count > 1 {
83
- var intervals: [TimeInterval] = []
84
- for i in 1..<eventTimestamps.count {
85
- intervals.append((eventTimestamps[i] - eventTimestamps[i-1]) * 1000) // Convert to ms
86
- }
87
-
88
- let avgInterval = intervals.reduce(0, +) / Double(intervals.count)
89
- let minInterval = intervals.min() ?? 0
90
- let maxInterval = intervals.max() ?? 0
91
-
92
- print("Event emission intervals - Avg: \(avgInterval)ms, Min: \(minInterval)ms, Max: \(maxInterval)ms")
93
-
94
- // With the fix, average should be close to 10ms
95
- XCTAssertLessThan(abs(avgInterval - 10), 5, "Average interval should be close to 10ms")
96
- XCTAssertGreaterThan(minInterval, 5, "Minimum interval should be at least 5ms")
97
- }
98
-
99
- expectation.fulfill()
100
- }
101
- }
102
-
103
- waitForExpectations(timeout: testDuration + 1.0)
104
- }
105
- }
@@ -1,22 +0,0 @@
1
- <?xml version="1.0" encoding="UTF-8"?>
2
- <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
3
- <plist version="1.0">
4
- <dict>
5
- <key>CFBundleDevelopmentRegion</key>
6
- <string>$(DEVELOPMENT_LANGUAGE)</string>
7
- <key>CFBundleExecutable</key>
8
- <string>$(EXECUTABLE_NAME)</string>
9
- <key>CFBundleIdentifier</key>
10
- <string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
11
- <key>CFBundleInfoDictionaryVersion</key>
12
- <string>6.0</string>
13
- <key>CFBundleName</key>
14
- <string>$(PRODUCT_NAME)</string>
15
- <key>CFBundlePackageType</key>
16
- <string>$(PRODUCT_BUNDLE_PACKAGE_TYPE)</string>
17
- <key>CFBundleShortVersionString</key>
18
- <string>1.0</string>
19
- <key>CFBundleVersion</key>
20
- <string>1</string>
21
- </dict>
22
- </plist>
@@ -1,39 +0,0 @@
1
- # AudioStudio iOS Unit Tests
2
-
3
- This directory contains unit tests for the AudioStudio iOS module.
4
-
5
- ## Test Files
6
-
7
- - `AudioTestHelpers.swift` - Common test utilities and extensions
8
- - `AudioFormatUtilsTests.swift` - Tests for audio format utilities
9
- - `AudioFileHandlerTests.swift` - Tests for file handling
10
- - `SimpleAudioTest.swift` - Basic audio functionality tests
11
- - `TestAudioGenerator.swift` - Audio generation utilities for testing
12
- - `CompressedOnlyOutputTests.swift` - Tests for compressed-only output feature (Issue #244)
13
-
14
- ## Running Tests
15
-
16
- ### In Xcode
17
- 1. Open the workspace/project containing AudioStudio
18
- 2. Select the test target
19
- 3. Press `Cmd+U` to run all tests or click on individual test methods
20
-
21
- ### From Command Line
22
- ```bash
23
- # Run all tests
24
- xcodebuild test -scheme AudioStudioTests -destination 'platform=iOS Simulator,name=iPhone 15'
25
-
26
- # Run specific test class
27
- xcodebuild test -scheme AudioStudioTests -destination 'platform=iOS Simulator,name=iPhone 15' -only-testing:AudioStudioTests/CompressedOnlyOutputTests
28
- ```
29
-
30
- ## Compressed-Only Output Tests
31
-
32
- The `CompressedOnlyOutputTests.swift` file tests the fix for Issue #244, ensuring that:
33
- - Compression info is properly returned when primary output is disabled
34
- - AAC format works correctly
35
- - Opus format falls back to AAC on iOS
36
- - Compressed file URIs are accessible
37
- - File sizes and metadata are correctly reported
38
-
39
- These tests verify that users can access compressed audio files even when primary WAV output is disabled.