@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.
- package/CHANGELOG.md +356 -5
- package/android/src/main/java/net/siteed/audiostudio/AudioStreamDecoder.kt +306 -94
- package/android/src/main/java/net/siteed/audiostudio/AudioStudioModule.kt +39 -6
- package/build/cjs/errors/AudioStreamError.js +9 -0
- package/build/cjs/errors/AudioStreamError.js.map +1 -1
- package/build/cjs/errors/AudioStreamError.test.js +22 -1
- package/build/cjs/errors/AudioStreamError.test.js.map +1 -1
- package/build/cjs/streamAudioData.js +99 -32
- package/build/cjs/streamAudioData.js.map +1 -1
- package/build/cjs/utils/audioProcessing.js +14 -10
- package/build/cjs/utils/audioProcessing.js.map +1 -1
- package/build/esm/errors/AudioStreamError.js +9 -0
- package/build/esm/errors/AudioStreamError.js.map +1 -1
- package/build/esm/errors/AudioStreamError.test.js +22 -1
- package/build/esm/errors/AudioStreamError.test.js.map +1 -1
- package/build/esm/streamAudioData.js +99 -32
- package/build/esm/streamAudioData.js.map +1 -1
- package/build/esm/utils/audioProcessing.js +14 -10
- package/build/esm/utils/audioProcessing.js.map +1 -1
- package/build/types/errors/AudioStreamError.d.ts.map +1 -1
- package/build/types/streamAudioData.d.ts +5 -0
- package/build/types/streamAudioData.d.ts.map +1 -1
- package/build/types/utils/audioProcessing.d.ts +2 -2
- package/build/types/utils/audioProcessing.d.ts.map +1 -1
- package/ios/AudioStreamDecoder.swift +191 -100
- package/ios/AudioStudioModule.swift +48 -9
- package/package.json +163 -146
- package/scripts/README.md +58 -0
- package/src/errors/AudioStreamError.test.ts +29 -2
- package/src/errors/AudioStreamError.ts +14 -0
- package/src/streamAudioData.ts +146 -42
- package/src/utils/audioProcessing.ts +25 -14
- package/android/src/androidTest/assets/chorus.wav +0 -0
- package/android/src/androidTest/assets/jfk.wav +0 -0
- package/android/src/androidTest/assets/osr_us_000_0010_8k.wav +0 -0
- package/android/src/androidTest/assets/recorder_hello_world.wav +0 -0
- package/android/src/androidTest/java/net/siteed/audiostudio/AudioFinalMetadataContractInstrumentedTest.kt +0 -190
- package/android/src/androidTest/java/net/siteed/audiostudio/AudioProcessorInstrumentedTest.kt +0 -197
- package/android/src/androidTest/java/net/siteed/audiostudio/AudioRecorderInstrumentedTest.kt +0 -487
- package/android/src/androidTest/java/net/siteed/audiostudio/AudioRecorderPerformanceInstrumentedTest.kt +0 -250
- package/android/src/androidTest/java/net/siteed/audiostudio/OpusRangeDecodeRegressionInstrumentedTest.kt +0 -186
- package/android/src/androidTest/java/net/siteed/audiostudio/integration/AudioFocusStrategyIntegrationTest.kt +0 -332
- package/android/src/androidTest/java/net/siteed/audiostudio/integration/BufferDurationIntegrationTest.kt +0 -324
- package/android/src/androidTest/java/net/siteed/audiostudio/integration/CompressedOnlyOutputTest.kt +0 -253
- package/android/src/androidTest/java/net/siteed/audiostudio/integration/DeviceDisconnectionFallbackTest.kt +0 -218
- package/android/src/androidTest/java/net/siteed/audiostudio/integration/EventEmissionIntervalTest.kt +0 -120
- package/android/src/androidTest/java/net/siteed/audiostudio/integration/M4aFormatTest.kt +0 -345
- package/android/src/androidTest/java/net/siteed/audiostudio/integration/OutputControlIntegrationTest.kt +0 -340
- package/android/src/androidTest/java/net/siteed/audiostudio/integration/PcmStreamingDurationTest.kt +0 -252
- package/android/src/androidTest/java/net/siteed/audiostudio/integration/README.md +0 -95
- package/android/src/androidTest/java/net/siteed/audiostudio/integration/run_integration_tests.sh +0 -43
- package/android/src/test/java/net/siteed/audiostudio/AndroidCallStateTest.kt +0 -37
- package/android/src/test/java/net/siteed/audiostudio/AndroidEventEmitterTest.kt +0 -28
- package/android/src/test/java/net/siteed/audiostudio/AudioFileHandlerTest.kt +0 -279
- package/android/src/test/java/net/siteed/audiostudio/AudioFocusStrategyTest.kt +0 -249
- package/android/src/test/java/net/siteed/audiostudio/AudioFormatTest.kt +0 -151
- package/android/src/test/java/net/siteed/audiostudio/AudioFormatUtilsTest.kt +0 -273
- package/android/src/test/java/net/siteed/audiostudio/DeviceDisconnectionFallbackUnitTest.kt +0 -140
- package/android/src/test/java/net/siteed/audiostudio/InterruptionAutoResumePolicyTest.kt +0 -49
- package/android/src/test/resources/chorus.wav +0 -0
- package/android/src/test/resources/generate_test_audio.py +0 -94
- package/android/src/test/resources/jfk.wav +0 -0
- package/android/src/test/resources/osr_us_000_0010_8k.wav +0 -0
- package/android/src/test/resources/recorder_hello_world.wav +0 -0
- package/ios/AudioStudioTests/AudioFileHandlerTests.swift +0 -338
- package/ios/AudioStudioTests/AudioFormatUtilsTests.swift +0 -331
- package/ios/AudioStudioTests/AudioStreamDecoderTests.swift +0 -128
- package/ios/AudioStudioTests/AudioTestHelpers.swift +0 -130
- package/ios/AudioStudioTests/CompressedOnlyOutputTests.swift +0 -334
- package/ios/AudioStudioTests/EventEmissionIntervalTests.swift +0 -105
- package/ios/AudioStudioTests/Info.plist +0 -22
- package/ios/AudioStudioTests/README.md +0 -39
- package/ios/AudioStudioTests/SimpleAudioTest.swift +0 -98
- package/ios/AudioStudioTests/TestAudioGenerator.swift +0 -75
- package/ios/tests/README.md +0 -41
- package/ios/tests/integration/buffer_and_fallback_test.swift +0 -178
- package/ios/tests/integration/buffer_duration_test.swift +0 -185
- package/ios/tests/integration/compressed_only_output_test.swift +0 -271
- package/ios/tests/integration/output_control_test.swift +0 -322
- package/ios/tests/integration/run_integration_tests.sh +0 -37
- package/ios/tests/opus_support_test_macos.swift +0 -154
- package/ios/tests/standalone/audio_processing_test.swift +0 -144
- package/ios/tests/standalone/audio_recording_test.swift +0 -277
- package/ios/tests/standalone/audio_streaming_test.swift +0 -249
- 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
|
-
}
|