@siteed/audio-studio 3.2.0-beta.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 (85) hide show
  1. package/CHANGELOG.md +356 -5
  2. package/android/src/main/java/net/siteed/audiostudio/AudioStreamDecoder.kt +306 -94
  3. package/android/src/main/java/net/siteed/audiostudio/AudioStudioModule.kt +39 -6
  4. package/build/cjs/errors/AudioStreamError.js +9 -0
  5. package/build/cjs/errors/AudioStreamError.js.map +1 -1
  6. package/build/cjs/errors/AudioStreamError.test.js +22 -1
  7. package/build/cjs/errors/AudioStreamError.test.js.map +1 -1
  8. package/build/cjs/streamAudioData.js +99 -32
  9. package/build/cjs/streamAudioData.js.map +1 -1
  10. package/build/cjs/utils/audioProcessing.js +14 -10
  11. package/build/cjs/utils/audioProcessing.js.map +1 -1
  12. package/build/esm/errors/AudioStreamError.js +9 -0
  13. package/build/esm/errors/AudioStreamError.js.map +1 -1
  14. package/build/esm/errors/AudioStreamError.test.js +22 -1
  15. package/build/esm/errors/AudioStreamError.test.js.map +1 -1
  16. package/build/esm/streamAudioData.js +99 -32
  17. package/build/esm/streamAudioData.js.map +1 -1
  18. package/build/esm/utils/audioProcessing.js +14 -10
  19. package/build/esm/utils/audioProcessing.js.map +1 -1
  20. package/build/types/errors/AudioStreamError.d.ts.map +1 -1
  21. package/build/types/streamAudioData.d.ts +5 -0
  22. package/build/types/streamAudioData.d.ts.map +1 -1
  23. package/build/types/utils/audioProcessing.d.ts +2 -2
  24. package/build/types/utils/audioProcessing.d.ts.map +1 -1
  25. package/ios/AudioStreamDecoder.swift +191 -100
  26. package/ios/AudioStudioModule.swift +48 -9
  27. package/package.json +163 -146
  28. package/scripts/README.md +58 -0
  29. package/src/errors/AudioStreamError.test.ts +29 -2
  30. package/src/errors/AudioStreamError.ts +14 -0
  31. package/src/streamAudioData.ts +146 -42
  32. package/src/utils/audioProcessing.ts +25 -14
  33. package/android/src/androidTest/assets/chorus.wav +0 -0
  34. package/android/src/androidTest/assets/jfk.wav +0 -0
  35. package/android/src/androidTest/assets/osr_us_000_0010_8k.wav +0 -0
  36. package/android/src/androidTest/assets/recorder_hello_world.wav +0 -0
  37. package/android/src/androidTest/java/net/siteed/audiostudio/AudioFinalMetadataContractInstrumentedTest.kt +0 -190
  38. package/android/src/androidTest/java/net/siteed/audiostudio/AudioProcessorInstrumentedTest.kt +0 -197
  39. package/android/src/androidTest/java/net/siteed/audiostudio/AudioRecorderInstrumentedTest.kt +0 -487
  40. package/android/src/androidTest/java/net/siteed/audiostudio/AudioRecorderPerformanceInstrumentedTest.kt +0 -250
  41. package/android/src/androidTest/java/net/siteed/audiostudio/OpusRangeDecodeRegressionInstrumentedTest.kt +0 -186
  42. package/android/src/androidTest/java/net/siteed/audiostudio/integration/AudioFocusStrategyIntegrationTest.kt +0 -332
  43. package/android/src/androidTest/java/net/siteed/audiostudio/integration/BufferDurationIntegrationTest.kt +0 -324
  44. package/android/src/androidTest/java/net/siteed/audiostudio/integration/CompressedOnlyOutputTest.kt +0 -253
  45. package/android/src/androidTest/java/net/siteed/audiostudio/integration/DeviceDisconnectionFallbackTest.kt +0 -218
  46. package/android/src/androidTest/java/net/siteed/audiostudio/integration/EventEmissionIntervalTest.kt +0 -120
  47. package/android/src/androidTest/java/net/siteed/audiostudio/integration/M4aFormatTest.kt +0 -345
  48. package/android/src/androidTest/java/net/siteed/audiostudio/integration/OutputControlIntegrationTest.kt +0 -340
  49. package/android/src/androidTest/java/net/siteed/audiostudio/integration/PcmStreamingDurationTest.kt +0 -252
  50. package/android/src/androidTest/java/net/siteed/audiostudio/integration/README.md +0 -95
  51. package/android/src/androidTest/java/net/siteed/audiostudio/integration/run_integration_tests.sh +0 -43
  52. package/android/src/test/java/net/siteed/audiostudio/AndroidCallStateTest.kt +0 -37
  53. package/android/src/test/java/net/siteed/audiostudio/AndroidEventEmitterTest.kt +0 -28
  54. package/android/src/test/java/net/siteed/audiostudio/AudioFileHandlerTest.kt +0 -279
  55. package/android/src/test/java/net/siteed/audiostudio/AudioFocusStrategyTest.kt +0 -249
  56. package/android/src/test/java/net/siteed/audiostudio/AudioFormatTest.kt +0 -151
  57. package/android/src/test/java/net/siteed/audiostudio/AudioFormatUtilsTest.kt +0 -273
  58. package/android/src/test/java/net/siteed/audiostudio/DeviceDisconnectionFallbackUnitTest.kt +0 -140
  59. package/android/src/test/java/net/siteed/audiostudio/InterruptionAutoResumePolicyTest.kt +0 -49
  60. package/android/src/test/resources/chorus.wav +0 -0
  61. package/android/src/test/resources/generate_test_audio.py +0 -94
  62. package/android/src/test/resources/jfk.wav +0 -0
  63. package/android/src/test/resources/osr_us_000_0010_8k.wav +0 -0
  64. package/android/src/test/resources/recorder_hello_world.wav +0 -0
  65. package/ios/AudioStudioTests/AudioFileHandlerTests.swift +0 -338
  66. package/ios/AudioStudioTests/AudioFormatUtilsTests.swift +0 -331
  67. package/ios/AudioStudioTests/AudioStreamDecoderTests.swift +0 -128
  68. package/ios/AudioStudioTests/AudioTestHelpers.swift +0 -130
  69. package/ios/AudioStudioTests/CompressedOnlyOutputTests.swift +0 -334
  70. package/ios/AudioStudioTests/EventEmissionIntervalTests.swift +0 -105
  71. package/ios/AudioStudioTests/Info.plist +0 -22
  72. package/ios/AudioStudioTests/README.md +0 -39
  73. package/ios/AudioStudioTests/SimpleAudioTest.swift +0 -98
  74. package/ios/AudioStudioTests/TestAudioGenerator.swift +0 -75
  75. package/ios/tests/README.md +0 -41
  76. package/ios/tests/integration/buffer_and_fallback_test.swift +0 -178
  77. package/ios/tests/integration/buffer_duration_test.swift +0 -185
  78. package/ios/tests/integration/compressed_only_output_test.swift +0 -271
  79. package/ios/tests/integration/output_control_test.swift +0 -322
  80. package/ios/tests/integration/run_integration_tests.sh +0 -37
  81. package/ios/tests/opus_support_test_macos.swift +0 -154
  82. package/ios/tests/standalone/audio_processing_test.swift +0 -144
  83. package/ios/tests/standalone/audio_recording_test.swift +0 -277
  84. package/ios/tests/standalone/audio_streaming_test.swift +0 -249
  85. package/ios/tests/standalone/standalone_test.swift +0 -144
@@ -1,338 +0,0 @@
1
- import XCTest
2
- import AVFoundation
3
- @testable import AudioStudio
4
-
5
- class AudioFileHandlerTests: XCTestCase {
6
-
7
- var tempDir: URL!
8
- var audioProcessor: AudioProcessor!
9
-
10
- // Helper function to create WAV header for tests
11
- private func createWavHeader(sampleRate: Int, channels: Int, bitsPerSample: Int, dataSize: Int) -> Data {
12
- var header = Data()
13
-
14
- let blockAlign = channels * (bitsPerSample / 8)
15
- let byteRate = sampleRate * blockAlign
16
-
17
- // "RIFF" chunk descriptor
18
- header.append(contentsOf: "RIFF".utf8)
19
- header.append(contentsOf: UInt32(36 + dataSize).littleEndianBytes)
20
- header.append(contentsOf: "WAVE".utf8)
21
-
22
- // "fmt " sub-chunk
23
- header.append(contentsOf: "fmt ".utf8)
24
- header.append(contentsOf: UInt32(16).littleEndianBytes) // PCM format requires 16 bytes for the fmt sub-chunk
25
- header.append(contentsOf: UInt16(1).littleEndianBytes) // Audio format 1 for PCM
26
- header.append(contentsOf: UInt16(channels).littleEndianBytes)
27
- header.append(contentsOf: UInt32(sampleRate).littleEndianBytes)
28
- header.append(contentsOf: UInt32(byteRate).littleEndianBytes) // byteRate
29
- header.append(contentsOf: UInt16(blockAlign).littleEndianBytes) // blockAlign
30
- header.append(contentsOf: UInt16(bitsPerSample).littleEndianBytes) // bits per sample
31
-
32
- // "data" sub-chunk
33
- header.append(contentsOf: "data".utf8)
34
- header.append(contentsOf: UInt32(dataSize).littleEndianBytes) // Sub-chunk data size
35
-
36
- return header
37
- }
38
-
39
- // Helper function to update WAV header for tests
40
- private func updateWavHeader(fileURL: URL) {
41
- guard let fileHandle = try? FileHandle(forUpdating: fileURL) else { return }
42
- defer { fileHandle.closeFile() }
43
-
44
- // Get file size
45
- fileHandle.seekToEndOfFile()
46
- let fileSize = fileHandle.offsetInFile
47
-
48
- // Update file size in header (bytes 4-7)
49
- fileHandle.seek(toFileOffset: 4)
50
- var size = UInt32(fileSize - 8).littleEndian
51
- fileHandle.write(Data(bytes: &size, count: 4))
52
-
53
- // Update data chunk size (bytes 40-43)
54
- fileHandle.seek(toFileOffset: 40)
55
- var dataSize = UInt32(fileSize - 44).littleEndian
56
- fileHandle.write(Data(bytes: &dataSize, count: 4))
57
- }
58
- }
59
-
60
- override func setUp() {
61
- super.setUp()
62
-
63
- // Create temporary directory for tests
64
- tempDir = FileManager.default.temporaryDirectory.appendingPathComponent(UUID().uuidString)
65
- try? FileManager.default.createDirectory(at: tempDir, withIntermediateDirectories: true)
66
-
67
- // Initialize AudioProcessor
68
- audioProcessor = AudioProcessor(filesDir: tempDir)
69
- }
70
-
71
- override func tearDown() {
72
- // Clean up temporary directory
73
- try? FileManager.default.removeItem(at: tempDir)
74
-
75
- super.tearDown()
76
- }
77
-
78
- // MARK: - WAV Header Tests
79
-
80
- func testWriteWavHeader_createsValidHeader() {
81
- // Given
82
- let sampleRate = 44100
83
- let channels = 2
84
- let bitsPerSample = 16
85
- let dataSize = 1000
86
-
87
- // When
88
- let header = createWavHeader(
89
- sampleRate: sampleRate,
90
- channels: channels,
91
- bitsPerSample: bitsPerSample,
92
- dataSize: dataSize
93
- )
94
-
95
- // Then
96
- XCTAssertEqual(header.count, 44, "WAV header should be 44 bytes")
97
-
98
- // Verify RIFF header
99
- let riffString = String(data: header[0..<4], encoding: .ascii)
100
- XCTAssertEqual(riffString, "RIFF")
101
-
102
- // Verify WAVE format
103
- let waveString = String(data: header[8..<12], encoding: .ascii)
104
- XCTAssertEqual(waveString, "WAVE")
105
-
106
- // Verify fmt chunk
107
- let fmtString = String(data: header[12..<16], encoding: .ascii)
108
- XCTAssertEqual(fmtString, "fmt ")
109
-
110
- // Verify data chunk
111
- let dataString = String(data: header[36..<40], encoding: .ascii)
112
- XCTAssertEqual(dataString, "data")
113
- }
114
-
115
- func testCreateAudioFile_createsFileWithCorrectSize() {
116
- // Given
117
- let fileName = "test_audio.wav"
118
- let sampleRate = 16000
119
- let channels = 1
120
- let durationMs = 1000
121
-
122
- // When
123
- let result = audioProcessor.createAudioFile(
124
- fileName: fileName,
125
- sampleRate: sampleRate,
126
- channels: channels,
127
- durationMs: durationMs
128
- )
129
-
130
- // Then
131
- XCTAssertTrue(result["success"] as? Bool ?? false)
132
-
133
- let fileUri = result["fileUri"] as? String
134
- XCTAssertNotNil(fileUri)
135
-
136
- if let fileUri = fileUri {
137
- let fileURL = URL(string: fileUri)!
138
- let fileExists = FileManager.default.fileExists(atPath: fileURL.path)
139
- XCTAssertTrue(fileExists)
140
-
141
- // Verify file size
142
- if let attributes = try? FileManager.default.attributesOfItem(atPath: fileURL.path),
143
- let fileSize = attributes[.size] as? Int64 {
144
- let expectedDataSize = sampleRate * channels * 2 * durationMs / 1000
145
- let expectedFileSize = 44 + expectedDataSize // WAV header + data
146
- XCTAssertEqual(fileSize, Int64(expectedFileSize))
147
- }
148
- }
149
- }
150
-
151
- func testDeleteAudioFile_removesExistingFile() {
152
- // Given - Create a file first
153
- let fileName = "test_to_delete.wav"
154
- _ = audioProcessor.createAudioFile(
155
- fileName: fileName,
156
- sampleRate: 16000,
157
- channels: 1,
158
- durationMs: 100
159
- )
160
-
161
- // When
162
- let result = audioProcessor.deleteAudioFile(fileName: fileName)
163
-
164
- // Then
165
- XCTAssertTrue(result["success"] as? Bool ?? false)
166
-
167
- let fileURL = tempDir.appendingPathComponent(fileName)
168
- XCTAssertFalse(FileManager.default.fileExists(atPath: fileURL.path))
169
- }
170
-
171
- func testDeleteAudioFile_handlesNonExistentFile() {
172
- // Given
173
- let fileName = "non_existent.wav"
174
-
175
- // When
176
- let result = audioProcessor.deleteAudioFile(fileName: fileName)
177
-
178
- // Then
179
- XCTAssertFalse(result["success"] as? Bool ?? true)
180
- XCTAssertNotNil(result["error"])
181
- }
182
-
183
- func testGetAudioFiles_returnsCorrectList() {
184
- // Given - Create multiple files
185
- let fileNames = ["file1.wav", "file2.wav", "file3.txt"]
186
-
187
- for fileName in fileNames {
188
- if fileName.hasSuffix(".wav") {
189
- _ = audioProcessor.createAudioFile(
190
- fileName: fileName,
191
- sampleRate: 16000,
192
- channels: 1,
193
- durationMs: 100
194
- )
195
- } else {
196
- // Create non-audio file
197
- let url = tempDir.appendingPathComponent(fileName)
198
- try? "test".write(to: url, atomically: true, encoding: .utf8)
199
- }
200
- }
201
-
202
- // When
203
- let files = audioProcessor.getAudioFiles()
204
-
205
- // Then
206
- XCTAssertEqual(files.count, 2, "Should only return WAV files")
207
- XCTAssertTrue(files.contains("file1.wav"))
208
- XCTAssertTrue(files.contains("file2.wav"))
209
- XCTAssertFalse(files.contains("file3.txt"))
210
- }
211
-
212
- func testClearAudioStorage_removesAllAudioFiles() {
213
- // Given - Create multiple files
214
- let audioFiles = ["audio1.wav", "audio2.wav"]
215
- let otherFiles = ["document.txt", "image.png"]
216
-
217
- for fileName in audioFiles {
218
- _ = audioProcessor.createAudioFile(
219
- fileName: fileName,
220
- sampleRate: 16000,
221
- channels: 1,
222
- durationMs: 100
223
- )
224
- }
225
-
226
- for fileName in otherFiles {
227
- let url = tempDir.appendingPathComponent(fileName)
228
- try? "test".write(to: url, atomically: true, encoding: .utf8)
229
- }
230
-
231
- // When
232
- let result = audioProcessor.clearAudioStorage()
233
-
234
- // Then
235
- XCTAssertTrue(result["success"] as? Bool ?? false)
236
-
237
- // Verify audio files are deleted
238
- for fileName in audioFiles {
239
- let url = tempDir.appendingPathComponent(fileName)
240
- XCTAssertFalse(FileManager.default.fileExists(atPath: url.path))
241
- }
242
-
243
- // Verify other files remain
244
- for fileName in otherFiles {
245
- let url = tempDir.appendingPathComponent(fileName)
246
- XCTAssertTrue(FileManager.default.fileExists(atPath: url.path))
247
- }
248
- }
249
-
250
- func testUpdateWavHeader_updatesFileSizeCorrectly() {
251
- // Given - Create a file
252
- let fileName = "test_update.wav"
253
- let createResult = audioProcessor.createAudioFile(
254
- fileName: fileName,
255
- sampleRate: 16000,
256
- channels: 1,
257
- durationMs: 1000
258
- )
259
-
260
- guard let fileUri = createResult["fileUri"] as? String,
261
- let fileURL = URL(string: fileUri) else {
262
- XCTFail("Failed to create test file")
263
- return
264
- }
265
-
266
- // Simulate appending more data
267
- if let fileHandle = try? FileHandle(forWritingTo: fileURL) {
268
- fileHandle.seekToEndOfFile()
269
- let additionalData = Data(count: 1000)
270
- fileHandle.write(additionalData)
271
- fileHandle.closeFile()
272
- }
273
-
274
- // When
275
- updateWavHeader(fileURL: fileURL)
276
-
277
- // Then - Verify header was updated
278
- if let data = try? Data(contentsOf: fileURL) {
279
- // Check file size in header (bytes 4-7)
280
- let fileSize = data.subdata(in: 4..<8).withUnsafeBytes { $0.load(as: UInt32.self) }
281
- XCTAssertEqual(fileSize, UInt32(data.count - 8))
282
-
283
- // Check data chunk size (bytes 40-43)
284
- let dataSize = data.subdata(in: 40..<44).withUnsafeBytes { $0.load(as: UInt32.self) }
285
- XCTAssertEqual(dataSize, UInt32(data.count - 44))
286
- }
287
- }
288
-
289
- // MARK: - Real File Tests
290
-
291
- func testLoadRealWavFile() {
292
- // Load test asset
293
- guard let testBundle = Bundle(for: type(of: self)).url(forResource: "jfk", withExtension: "wav") else {
294
- XCTFail("Test resource jfk.wav should exist")
295
- return
296
- }
297
-
298
- // Copy to temp directory
299
- let destURL = tempDir.appendingPathComponent("test_jfk.wav")
300
- try? FileManager.default.copyItem(at: testBundle, to: destURL)
301
-
302
- // Load and verify
303
- let audioData = audioProcessor.loadAudioFromAnyFormat(
304
- filePath: destURL.path,
305
- config: nil
306
- )
307
-
308
- XCTAssertNotNil(audioData)
309
-
310
- // JFK.wav is known to be mono, 16kHz, 16-bit
311
- XCTAssertEqual(audioData?.sampleRate, 16000)
312
- XCTAssertEqual(audioData?.channels, 1)
313
- XCTAssertEqual(audioData?.bitDepth, 16)
314
- XCTAssertGreaterThan(audioData?.data.count ?? 0, 0)
315
- }
316
-
317
- func testProcessMultipleRealFiles() {
318
- let testFiles = ["jfk.wav", "recorder_hello_world.wav", "osr_us_000_0010_8k.wav"]
319
-
320
- for fileName in testFiles {
321
- guard let testBundle = Bundle(for: type(of: self)).url(forResource: fileName.replacingOccurrences(of: ".wav", with: ""), withExtension: "wav") else {
322
- print("Skipping \(fileName) - not found in test bundle")
323
- continue
324
- }
325
-
326
- let destURL = tempDir.appendingPathComponent(fileName)
327
- try? FileManager.default.copyItem(at: testBundle, to: destURL)
328
-
329
- let audioData = audioProcessor.loadAudioFromAnyFormat(
330
- filePath: destURL.path,
331
- config: nil
332
- )
333
-
334
- XCTAssertNotNil(audioData, "Should load \(fileName)")
335
- XCTAssertGreaterThan(audioData?.data.count ?? 0, 0, "\(fileName) should have audio data")
336
- }
337
- }
338
- }
@@ -1,331 +0,0 @@
1
- import XCTest
2
- import AVFoundation
3
- import Accelerate
4
- @testable import AudioStudio
5
-
6
- class AudioFormatUtilsTests: XCTestCase {
7
-
8
- // MARK: - Bit Depth Conversion Tests
9
-
10
- func testConvertBitDepth_8to16() {
11
- // Given
12
- let input8bit: [UInt8] = [0, 64, 128, 192, 255]
13
- let expected16bit: [Int16] = [-32768, -16384, 0, 16384, 32767]
14
-
15
- // When
16
- let result = AudioFormatUtils.convertBitDepth(
17
- data: Data(input8bit),
18
- fromBitDepth: 8,
19
- toBitDepth: 16
20
- )
21
-
22
- // Then
23
- let result16bit = result.withUnsafeBytes { Array($0.bindMemory(to: Int16.self)) }
24
- XCTAssertEqual(result16bit, expected16bit)
25
- }
26
-
27
- func testConvertBitDepth_16to8() {
28
- // Given
29
- let input16bit: [Int16] = [-32768, -16384, 0, 16384, 32767]
30
- let expected8bit: [UInt8] = [0, 64, 128, 192, 255]
31
-
32
- // When
33
- var data = Data()
34
- input16bit.forEach { data.append(contentsOf: withUnsafeBytes(of: $0) { Array($0) }) }
35
-
36
- let result = AudioFormatUtils.convertBitDepth(
37
- data: data,
38
- fromBitDepth: 16,
39
- toBitDepth: 8
40
- )
41
-
42
- // Then
43
- let result8bit = Array(result)
44
- XCTAssertEqual(result8bit, expected8bit)
45
- }
46
-
47
- func testConvertBitDepth_16to32() {
48
- // Given
49
- let input16bit: [Int16] = [-32768, 0, 32767]
50
- let expected32bit: [Int32] = [-2147483648, 0, 2147483647]
51
-
52
- // When
53
- var data = Data()
54
- input16bit.forEach { data.append(contentsOf: withUnsafeBytes(of: $0) { Array($0) }) }
55
-
56
- let result = AudioFormatUtils.convertBitDepth(
57
- data: data,
58
- fromBitDepth: 16,
59
- toBitDepth: 32
60
- )
61
-
62
- // Then
63
- let result32bit = result.withUnsafeBytes { Array($0.bindMemory(to: Int32.self)) }
64
- XCTAssertEqual(result32bit, expected32bit)
65
- }
66
-
67
- func testConvertBitDepth_32to16() {
68
- // Given
69
- let input32bit: [Int32] = [-2147483648, 0, 2147483647]
70
- let expected16bit: [Int16] = [-32768, 0, 32767]
71
-
72
- // When
73
- var data = Data()
74
- input32bit.forEach { data.append(contentsOf: withUnsafeBytes(of: $0) { Array($0) }) }
75
-
76
- let result = AudioFormatUtils.convertBitDepth(
77
- data: data,
78
- fromBitDepth: 32,
79
- toBitDepth: 16
80
- )
81
-
82
- // Then
83
- let result16bit = result.withUnsafeBytes { Array($0.bindMemory(to: Int16.self)) }
84
- XCTAssertEqual(result16bit, expected16bit)
85
- }
86
-
87
- func testConvertBitDepth_sameDepth() {
88
- // Given
89
- let input: [Int16] = [100, 200, 300]
90
- var data = Data()
91
- input.forEach { data.append(contentsOf: withUnsafeBytes(of: $0) { Array($0) }) }
92
-
93
- // When
94
- let result = AudioFormatUtils.convertBitDepth(
95
- data: data,
96
- fromBitDepth: 16,
97
- toBitDepth: 16
98
- )
99
-
100
- // Then
101
- XCTAssertEqual(result, data)
102
- }
103
-
104
- // MARK: - Channel Conversion Tests
105
-
106
- func testConvertChannels_monoToStereo() {
107
- // Given
108
- let monoData: [Int16] = [100, 200, 300]
109
- var data = Data()
110
- monoData.forEach { data.append(contentsOf: withUnsafeBytes(of: $0) { Array($0) }) }
111
-
112
- // When
113
- let result = AudioFormatUtils.convertChannels(
114
- data: data,
115
- fromChannels: 1,
116
- toChannels: 2,
117
- bitDepth: 16
118
- )
119
-
120
- // Then
121
- let stereoData = result.withUnsafeBytes { Array($0.bindMemory(to: Int16.self)) }
122
- let expected: [Int16] = [100, 100, 200, 200, 300, 300]
123
- XCTAssertEqual(stereoData, expected)
124
- }
125
-
126
- func testConvertChannels_stereoToMono() {
127
- // Given
128
- let stereoData: [Int16] = [100, 200, 300, 400, 500, 600]
129
- var data = Data()
130
- stereoData.forEach { data.append(contentsOf: withUnsafeBytes(of: $0) { Array($0) }) }
131
-
132
- // When
133
- let result = AudioFormatUtils.convertChannels(
134
- data: data,
135
- fromChannels: 2,
136
- toChannels: 1,
137
- bitDepth: 16
138
- )
139
-
140
- // Then
141
- let monoData = result.withUnsafeBytes { Array($0.bindMemory(to: Int16.self)) }
142
- let expected: [Int16] = [150, 350, 550] // Average of pairs
143
- XCTAssertEqual(monoData, expected)
144
- }
145
-
146
- func testConvertChannels_sameChannels() {
147
- // Given
148
- let input: [Int16] = [100, 200, 300, 400]
149
- var data = Data()
150
- input.forEach { data.append(contentsOf: withUnsafeBytes(of: $0) { Array($0) }) }
151
-
152
- // When
153
- let result = AudioFormatUtils.convertChannels(
154
- data: data,
155
- fromChannels: 2,
156
- toChannels: 2,
157
- bitDepth: 16
158
- )
159
-
160
- // Then
161
- XCTAssertEqual(result, data)
162
- }
163
-
164
- // MARK: - Audio Normalization Tests
165
-
166
- func testNormalizeAudio_quietAudio() {
167
- // Given - Very quiet audio
168
- let quietAudio: [Int16] = [10, -10, 20, -20, 30, -30]
169
- var data = Data()
170
- quietAudio.forEach { data.append(contentsOf: withUnsafeBytes(of: $0) { Array($0) }) }
171
-
172
- // When
173
- let result = AudioFormatUtils.normalizeAudio(
174
- data: data,
175
- bitDepth: 16,
176
- targetLevel: 0.9
177
- )
178
-
179
- // Then
180
- let normalized = result.withUnsafeBytes { Array($0.bindMemory(to: Int16.self)) }
181
-
182
- // Check that audio was amplified
183
- let maxOriginal = quietAudio.map { abs($0) }.max() ?? 0
184
- let maxNormalized = normalized.map { abs($0) }.max() ?? 0
185
-
186
- XCTAssertGreaterThan(maxNormalized, maxOriginal)
187
-
188
- // Check that max is close to target
189
- let targetMax = Int16(Float(Int16.max) * 0.9)
190
- XCTAssertGreaterThan(maxNormalized, Int16(Float(targetMax) * 0.8))
191
- XCTAssertLessThanOrEqual(maxNormalized, targetMax)
192
- }
193
-
194
- func testNormalizeAudio_loudAudio() {
195
- // Given - Already loud audio
196
- let loudAudio: [Int16] = [30000, -30000, 25000, -25000]
197
- var data = Data()
198
- loudAudio.forEach { data.append(contentsOf: withUnsafeBytes(of: $0) { Array($0) }) }
199
-
200
- // When
201
- let result = AudioFormatUtils.normalizeAudio(
202
- data: data,
203
- bitDepth: 16,
204
- targetLevel: 0.9
205
- )
206
-
207
- // Then
208
- let normalized = result.withUnsafeBytes { Array($0.bindMemory(to: Int16.self)) }
209
-
210
- // Check that audio was slightly reduced
211
- let maxOriginal = loudAudio.map { abs($0) }.max() ?? 0
212
- let maxNormalized = normalized.map { abs($0) }.max() ?? 0
213
-
214
- XCTAssertLessThanOrEqual(maxNormalized, maxOriginal)
215
-
216
- // Check that max is close to target
217
- let targetMax = Int16(Float(Int16.max) * 0.9)
218
- XCTAssertGreaterThan(maxNormalized, Int16(Float(targetMax) * 0.8))
219
- XCTAssertLessThanOrEqual(maxNormalized, targetMax)
220
- }
221
-
222
- func testNormalizeAudio_silentAudio() {
223
- // Given - Silent audio
224
- let silentAudio = [Int16](repeating: 0, count: 100)
225
- var data = Data()
226
- silentAudio.forEach { data.append(contentsOf: withUnsafeBytes(of: $0) { Array($0) }) }
227
-
228
- // When
229
- let result = AudioFormatUtils.normalizeAudio(
230
- data: data,
231
- bitDepth: 16,
232
- targetLevel: 0.9
233
- )
234
-
235
- // Then - Should remain silent
236
- let normalized = result.withUnsafeBytes { Array($0.bindMemory(to: Int16.self)) }
237
- XCTAssertTrue(normalized.allSatisfy { $0 == 0 })
238
- }
239
-
240
- // MARK: - Sample Rate Conversion Tests
241
-
242
- func testResampleAudio_upsample() {
243
- // Given
244
- let originalSampleRate = 16000
245
- let targetSampleRate = 44100
246
- let duration = 0.1 // 100ms
247
- let originalSamples = Int(Double(originalSampleRate) * duration)
248
-
249
- // Create a simple sine wave
250
- var sineWave = [Int16]()
251
- for i in 0..<originalSamples {
252
- let value = sin(2.0 * .pi * 440.0 * Double(i) / Double(originalSampleRate))
253
- sineWave.append(Int16(value * 10000))
254
- }
255
-
256
- var data = Data()
257
- sineWave.forEach { data.append(contentsOf: withUnsafeBytes(of: $0) { Array($0) }) }
258
-
259
- // When
260
- let result = AudioFormatUtils.resampleAudio(
261
- data: data,
262
- fromSampleRate: originalSampleRate,
263
- toSampleRate: targetSampleRate,
264
- channels: 1,
265
- bitDepth: 16
266
- )
267
-
268
- // Then
269
- let expectedSamples = Int(Double(targetSampleRate) * duration)
270
- let actualSamples = result.count / 2 // 16-bit = 2 bytes per sample
271
-
272
- // Allow some tolerance due to resampling
273
- XCTAssertGreaterThan(actualSamples, Int(Double(expectedSamples) * 0.95))
274
- XCTAssertLessThan(actualSamples, Int(Double(expectedSamples) * 1.05))
275
- }
276
-
277
- func testResampleAudio_downsample() {
278
- // Given
279
- let originalSampleRate = 44100
280
- let targetSampleRate = 16000
281
- let duration = 0.1 // 100ms
282
- let originalSamples = Int(Double(originalSampleRate) * duration)
283
-
284
- // Create a simple sine wave
285
- var sineWave = [Int16]()
286
- for i in 0..<originalSamples {
287
- let value = sin(2.0 * .pi * 440.0 * Double(i) / Double(originalSampleRate))
288
- sineWave.append(Int16(value * 10000))
289
- }
290
-
291
- var data = Data()
292
- sineWave.forEach { data.append(contentsOf: withUnsafeBytes(of: $0) { Array($0) }) }
293
-
294
- // When
295
- let result = AudioFormatUtils.resampleAudio(
296
- data: data,
297
- fromSampleRate: originalSampleRate,
298
- toSampleRate: targetSampleRate,
299
- channels: 1,
300
- bitDepth: 16
301
- )
302
-
303
- // Then
304
- let expectedSamples = Int(Double(targetSampleRate) * duration)
305
- let actualSamples = result.count / 2 // 16-bit = 2 bytes per sample
306
-
307
- // Allow some tolerance due to resampling
308
- XCTAssertGreaterThan(actualSamples, Int(Double(expectedSamples) * 0.95))
309
- XCTAssertLessThan(actualSamples, Int(Double(expectedSamples) * 1.05))
310
- }
311
-
312
- func testResampleAudio_sameSampleRate() {
313
- // Given
314
- let sampleRate = 44100
315
- let samples: [Int16] = [100, 200, 300, 400, 500]
316
- var data = Data()
317
- samples.forEach { data.append(contentsOf: withUnsafeBytes(of: $0) { Array($0) }) }
318
-
319
- // When
320
- let result = AudioFormatUtils.resampleAudio(
321
- data: data,
322
- fromSampleRate: sampleRate,
323
- toSampleRate: sampleRate,
324
- channels: 1,
325
- bitDepth: 16
326
- )
327
-
328
- // Then - Should return same data
329
- XCTAssertEqual(result, data)
330
- }
331
- }