@siteed/expo-audio-studio 2.9.0 → 2.10.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 (64) hide show
  1. package/CHANGELOG.md +9 -1
  2. package/android/build.gradle +9 -0
  3. package/android/src/androidTest/assets/chorus.wav +0 -0
  4. package/android/src/androidTest/assets/jfk.wav +0 -0
  5. package/android/src/androidTest/assets/osr_us_000_0010_8k.wav +0 -0
  6. package/android/src/androidTest/assets/recorder_hello_world.wav +0 -0
  7. package/android/src/androidTest/java/net/siteed/audiostream/AudioProcessorInstrumentedTest.kt +197 -0
  8. package/android/src/androidTest/java/net/siteed/audiostream/AudioRecorderInstrumentedTest.kt +541 -0
  9. package/android/src/androidTest/java/net/siteed/audiostream/integration/BufferDurationIntegrationTest.kt +324 -0
  10. package/android/src/androidTest/java/net/siteed/audiostream/integration/OutputControlIntegrationTest.kt +340 -0
  11. package/android/src/androidTest/java/net/siteed/audiostream/integration/README.md +95 -0
  12. package/android/src/androidTest/java/net/siteed/audiostream/integration/run_integration_tests.sh +28 -0
  13. package/android/src/main/java/net/siteed/audiostream/AudioFormatUtils.kt +264 -13
  14. package/android/src/main/java/net/siteed/audiostream/AudioProcessor.kt +3 -13
  15. package/android/src/main/java/net/siteed/audiostream/AudioRecorderManager.kt +118 -55
  16. package/android/src/main/java/net/siteed/audiostream/LogUtils.kt +32 -4
  17. package/android/src/main/java/net/siteed/audiostream/RecordingConfig.kt +50 -15
  18. package/android/src/test/java/net/siteed/audiostream/AudioFileHandlerTest.kt +279 -0
  19. package/android/src/test/java/net/siteed/audiostream/AudioFormatUtilsTest.kt +273 -0
  20. package/android/src/test/resources/chorus.wav +0 -0
  21. package/android/src/test/resources/generate_test_audio.py +94 -0
  22. package/android/src/test/resources/jfk.wav +0 -0
  23. package/android/src/test/resources/osr_us_000_0010_8k.wav +0 -0
  24. package/android/src/test/resources/recorder_hello_world.wav +0 -0
  25. package/build/cjs/ExpoAudioStream.types.js.map +1 -1
  26. package/build/cjs/ExpoAudioStream.web.js +37 -34
  27. package/build/cjs/ExpoAudioStream.web.js.map +1 -1
  28. package/build/cjs/WebRecorder.web.js +12 -10
  29. package/build/cjs/WebRecorder.web.js.map +1 -1
  30. package/build/esm/ExpoAudioStream.types.js.map +1 -1
  31. package/build/esm/ExpoAudioStream.web.js +37 -34
  32. package/build/esm/ExpoAudioStream.web.js.map +1 -1
  33. package/build/esm/WebRecorder.web.js +12 -10
  34. package/build/esm/WebRecorder.web.js.map +1 -1
  35. package/build/types/ExpoAudioStream.types.d.ts +54 -22
  36. package/build/types/ExpoAudioStream.types.d.ts.map +1 -1
  37. package/build/types/ExpoAudioStream.web.d.ts.map +1 -1
  38. package/build/types/WebRecorder.web.d.ts.map +1 -1
  39. package/ios/AudioNotificationManager.swift +2 -6
  40. package/ios/AudioStreamManager.swift +116 -50
  41. package/ios/ExpoAudioStream.podspec +6 -0
  42. package/ios/ExpoAudioStreamModule.swift +11 -8
  43. package/ios/ExpoAudioStudioTests/AudioFileHandlerTests.swift +338 -0
  44. package/ios/ExpoAudioStudioTests/AudioFormatUtilsTests.swift +331 -0
  45. package/ios/ExpoAudioStudioTests/AudioTestHelpers.swift +130 -0
  46. package/ios/ExpoAudioStudioTests/Info.plist +22 -0
  47. package/ios/ExpoAudioStudioTests/SimpleAudioTest.swift +98 -0
  48. package/ios/ExpoAudioStudioTests/TestAudioGenerator.swift +75 -0
  49. package/ios/RecordingSettings.swift +53 -22
  50. package/ios/tests/integration/buffer_duration_test.swift +185 -0
  51. package/ios/tests/integration/output_control_test.swift +322 -0
  52. package/ios/tests/integration/run_integration_tests.sh +27 -0
  53. package/ios/tests/standalone/audio_processing_test.swift +144 -0
  54. package/ios/tests/standalone/audio_recording_test.swift +277 -0
  55. package/ios/tests/standalone/audio_streaming_test.swift +249 -0
  56. package/ios/tests/standalone/standalone_test.swift +144 -0
  57. package/package.json +140 -133
  58. package/src/ExpoAudioStream.types.ts +66 -22
  59. package/src/ExpoAudioStream.web.ts +43 -38
  60. package/src/WebRecorder.web.ts +13 -10
  61. package/android/src/main/test/java/net/siteed/audiostream/AudioProcessorTest.kt +0 -56
  62. package/ios/siteedexpoaudiostudio.xcodeproj/project.xcworkspace/contents.xcworkspacedata +0 -7
  63. package/ios/siteedexpoaudiostudio.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist +0 -8
  64. /package/plugin/build/{index.d.ts → index.d.cts} +0 -0
@@ -0,0 +1,331 @@
1
+ import XCTest
2
+ import AVFoundation
3
+ import Accelerate
4
+ @testable import ExpoAudioStream
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
+ }
@@ -0,0 +1,130 @@
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
+ }
@@ -0,0 +1,22 @@
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>
@@ -0,0 +1,98 @@
1
+ import XCTest
2
+ import AVFoundation
3
+
4
+ class SimpleAudioTest: XCTestCase {
5
+
6
+ func testCreateWAVHeader() {
7
+ // Test creating a basic WAV header
8
+ let sampleRate = 44100
9
+ let channels = 2
10
+ let bitsPerSample = 16
11
+ let dataSize = 1024
12
+
13
+ // Calculate expected values
14
+ let byteRate = sampleRate * channels * (bitsPerSample / 8)
15
+ let blockAlign = channels * (bitsPerSample / 8)
16
+
17
+ // Create header data manually (44 bytes)
18
+ var header = Data()
19
+
20
+ // RIFF chunk
21
+ header.append("RIFF".data(using: .ascii)!)
22
+ var fileSize = UInt32(dataSize + 36).littleEndian
23
+ header.append(Data(bytes: &fileSize, count: 4))
24
+ header.append("WAVE".data(using: .ascii)!)
25
+
26
+ // fmt chunk
27
+ header.append("fmt ".data(using: .ascii)!)
28
+ var fmtSize = UInt32(16).littleEndian
29
+ header.append(Data(bytes: &fmtSize, count: 4))
30
+ var audioFormat = UInt16(1).littleEndian // PCM
31
+ header.append(Data(bytes: &audioFormat, count: 2))
32
+ var numChannels = UInt16(channels).littleEndian
33
+ header.append(Data(bytes: &numChannels, count: 2))
34
+ var sampleRateValue = UInt32(sampleRate).littleEndian
35
+ header.append(Data(bytes: &sampleRateValue, count: 4))
36
+ var byteRateValue = UInt32(byteRate).littleEndian
37
+ header.append(Data(bytes: &byteRateValue, count: 4))
38
+ var blockAlignValue = UInt16(blockAlign).littleEndian
39
+ header.append(Data(bytes: &blockAlignValue, count: 2))
40
+ var bitsPerSampleValue = UInt16(bitsPerSample).littleEndian
41
+ header.append(Data(bytes: &bitsPerSampleValue, count: 2))
42
+
43
+ // data chunk
44
+ header.append("data".data(using: .ascii)!)
45
+ var dataSizeValue = UInt32(dataSize).littleEndian
46
+ header.append(Data(bytes: &dataSizeValue, count: 4))
47
+
48
+ // Verify header size
49
+ XCTAssertEqual(header.count, 44, "WAV header should be 44 bytes")
50
+
51
+ // Verify RIFF header
52
+ let riffHeader = String(data: header[0..<4], encoding: .ascii)
53
+ XCTAssertEqual(riffHeader, "RIFF")
54
+
55
+ // Verify WAVE format
56
+ let waveFormat = String(data: header[8..<12], encoding: .ascii)
57
+ XCTAssertEqual(waveFormat, "WAVE")
58
+
59
+ print("✅ Basic WAV header test passed!")
60
+ }
61
+
62
+ func testSimpleAudioBuffer() {
63
+ // Test creating a simple audio buffer
64
+ let sampleRate = 44100.0
65
+ let duration = 0.1 // 100ms
66
+ let frequency = 440.0 // A4 note
67
+
68
+ let frameCount = Int(sampleRate * duration)
69
+ let format = AVAudioFormat(standardFormatWithSampleRate: sampleRate, channels: 1)!
70
+
71
+ guard let buffer = AVAudioPCMBuffer(pcmFormat: format, frameCapacity: AVAudioFrameCount(frameCount)) else {
72
+ XCTFail("Failed to create audio buffer")
73
+ return
74
+ }
75
+
76
+ buffer.frameLength = AVAudioFrameCount(frameCount)
77
+
78
+ // Generate a simple sine wave
79
+ let channelData = buffer.floatChannelData![0]
80
+ for frame in 0..<frameCount {
81
+ let phase = 2.0 * Double.pi * frequency * Double(frame) / sampleRate
82
+ channelData[frame] = Float(sin(phase) * 0.5)
83
+ }
84
+
85
+ // Verify buffer properties
86
+ XCTAssertEqual(buffer.frameLength, AVAudioFrameCount(frameCount))
87
+ XCTAssertEqual(buffer.format.sampleRate, sampleRate)
88
+ XCTAssertEqual(buffer.format.channelCount, 1)
89
+
90
+ // Verify we have audio data
91
+ let firstSample = channelData[0]
92
+ let lastSample = channelData[frameCount - 1]
93
+ XCTAssertNotEqual(firstSample, 0.0, accuracy: 0.001)
94
+ XCTAssertNotEqual(lastSample, firstSample, accuracy: 0.001)
95
+
96
+ print("✅ Simple audio buffer test passed!")
97
+ }
98
+ }
@@ -0,0 +1,75 @@
1
+ import Foundation
2
+ import AVFoundation
3
+ import Accelerate
4
+
5
+ class TestAudioGenerator {
6
+
7
+ /// Generate a sine wave tone
8
+ static func generateTone(frequency: Double, duration: TimeInterval, sampleRate: Double = 44100) -> AVAudioPCMBuffer? {
9
+ let frameCount = AVAudioFrameCount(duration * sampleRate)
10
+
11
+ guard let buffer = AVAudioPCMBuffer(pcmFormat: AVAudioFormat(standardFormatWithSampleRate: sampleRate, channels: 1)!, frameCapacity: frameCount) else {
12
+ return nil
13
+ }
14
+
15
+ buffer.frameLength = frameCount
16
+
17
+ let channelData = buffer.floatChannelData![0]
18
+ let angleIncrement = 2.0 * .pi * frequency / sampleRate
19
+
20
+ for frame in 0..<Int(frameCount) {
21
+ channelData[frame] = Float(sin(Double(frame) * angleIncrement))
22
+ }
23
+
24
+ return buffer
25
+ }
26
+
27
+ /// Generate white noise
28
+ static func generateWhiteNoise(duration: TimeInterval, sampleRate: Double = 44100) -> AVAudioPCMBuffer? {
29
+ let frameCount = AVAudioFrameCount(duration * sampleRate)
30
+
31
+ guard let buffer = AVAudioPCMBuffer(pcmFormat: AVAudioFormat(standardFormatWithSampleRate: sampleRate, channels: 1)!, frameCapacity: frameCount) else {
32
+ return nil
33
+ }
34
+
35
+ buffer.frameLength = frameCount
36
+
37
+ let channelData = buffer.floatChannelData![0]
38
+
39
+ for frame in 0..<Int(frameCount) {
40
+ channelData[frame] = Float.random(in: -1...1)
41
+ }
42
+
43
+ return buffer
44
+ }
45
+
46
+ /// Load test asset from bundle
47
+ static func loadTestAsset(named name: String) -> AVAudioFile? {
48
+ guard let url = Bundle(for: TestAudioGenerator.self).url(forResource: name, withExtension: "wav") else {
49
+ return nil
50
+ }
51
+
52
+ return try? AVAudioFile(forReading: url)
53
+ }
54
+
55
+ /// Convert AVAudioPCMBuffer to Data
56
+ static func bufferToData(_ buffer: AVAudioPCMBuffer) -> Data? {
57
+ guard let channelData = buffer.floatChannelData else { return nil }
58
+
59
+ let channelCount = Int(buffer.format.channelCount)
60
+ let frameLength = Int(buffer.frameLength)
61
+ let bytesPerFrame = 2 * channelCount // 16-bit audio
62
+
63
+ var data = Data(capacity: frameLength * bytesPerFrame)
64
+
65
+ for frame in 0..<frameLength {
66
+ for channel in 0..<channelCount {
67
+ let sample = channelData[channel][frame]
68
+ let int16Sample = Int16(max(-32768, min(32767, sample * 32767)))
69
+ data.append(contentsOf: withUnsafeBytes(of: int16Sample) { Array($0) })
70
+ }
71
+ }
72
+
73
+ return data
74
+ }
75
+ }