@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,154 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env swift
|
|
2
|
-
|
|
3
|
-
import AVFoundation
|
|
4
|
-
import Foundation
|
|
5
|
-
|
|
6
|
-
// Test script to verify if AVAudioRecorder actually supports Opus encoding
|
|
7
|
-
// This version is macOS-compatible for testing purposes
|
|
8
|
-
|
|
9
|
-
func testOpusSupport() {
|
|
10
|
-
print("Testing AVAudioRecorder Opus Support (macOS test)...")
|
|
11
|
-
print("--------------------------------------------------")
|
|
12
|
-
|
|
13
|
-
// Test 1: Check if kAudioFormatOpus is defined
|
|
14
|
-
let opusFormat = kAudioFormatOpus
|
|
15
|
-
print("✓ kAudioFormatOpus is defined: \(opusFormat) (0x\(String(opusFormat, radix: 16)))")
|
|
16
|
-
|
|
17
|
-
// Convert to FourCC string
|
|
18
|
-
let fourCC = String(format: "%c%c%c%c",
|
|
19
|
-
(opusFormat >> 24) & 0xff,
|
|
20
|
-
(opusFormat >> 16) & 0xff,
|
|
21
|
-
(opusFormat >> 8) & 0xff,
|
|
22
|
-
opusFormat & 0xff)
|
|
23
|
-
print(" FourCC: '\(fourCC)'")
|
|
24
|
-
print()
|
|
25
|
-
|
|
26
|
-
// Test 2: Try to create AVAudioRecorder with Opus settings
|
|
27
|
-
print("Testing AVAudioRecorder with Opus settings...")
|
|
28
|
-
|
|
29
|
-
let documentsPath = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask)[0]
|
|
30
|
-
let opusURL = documentsPath.appendingPathComponent("test_opus.opus")
|
|
31
|
-
let aacURL = documentsPath.appendingPathComponent("test_aac.m4a")
|
|
32
|
-
|
|
33
|
-
// Opus settings
|
|
34
|
-
let opusSettings: [String: Any] = [
|
|
35
|
-
AVFormatIDKey: kAudioFormatOpus,
|
|
36
|
-
AVSampleRateKey: 48000,
|
|
37
|
-
AVNumberOfChannelsKey: 1,
|
|
38
|
-
AVEncoderBitRateKey: 64000
|
|
39
|
-
]
|
|
40
|
-
|
|
41
|
-
// AAC settings for comparison
|
|
42
|
-
let aacSettings: [String: Any] = [
|
|
43
|
-
AVFormatIDKey: kAudioFormatMPEG4AAC,
|
|
44
|
-
AVSampleRateKey: 48000,
|
|
45
|
-
AVNumberOfChannelsKey: 1,
|
|
46
|
-
AVEncoderBitRateKey: 64000
|
|
47
|
-
]
|
|
48
|
-
|
|
49
|
-
// Test Opus recorder
|
|
50
|
-
do {
|
|
51
|
-
let opusRecorder = try AVAudioRecorder(url: opusURL, settings: opusSettings)
|
|
52
|
-
print("✓ Opus recorder created successfully")
|
|
53
|
-
print(" URL: \(opusURL.lastPathComponent)")
|
|
54
|
-
print(" Settings provided: \(opusSettings)")
|
|
55
|
-
print(" Settings after init: \(opusRecorder.settings)")
|
|
56
|
-
print(" Format: \(opusRecorder.format)")
|
|
57
|
-
|
|
58
|
-
// Check if recorder can prepare
|
|
59
|
-
if opusRecorder.prepareToRecord() {
|
|
60
|
-
print("✓ Opus recorder prepared successfully")
|
|
61
|
-
|
|
62
|
-
// Try to record for a brief moment
|
|
63
|
-
if opusRecorder.record() {
|
|
64
|
-
print("✓ Opus recorder started recording")
|
|
65
|
-
Thread.sleep(forTimeInterval: 0.5)
|
|
66
|
-
opusRecorder.stop()
|
|
67
|
-
print("✓ Opus recorder stopped")
|
|
68
|
-
|
|
69
|
-
// Check if file was created
|
|
70
|
-
if FileManager.default.fileExists(atPath: opusURL.path) {
|
|
71
|
-
let attributes = try FileManager.default.attributesOfItem(atPath: opusURL.path)
|
|
72
|
-
let fileSize = attributes[.size] as? Int64 ?? 0
|
|
73
|
-
print("✓ Opus file created: \(fileSize) bytes")
|
|
74
|
-
|
|
75
|
-
// Check file format by reading header
|
|
76
|
-
if fileSize > 0 {
|
|
77
|
-
let fileHandle = try FileHandle(forReadingFrom: opusURL)
|
|
78
|
-
let headerData = fileHandle.readData(ofLength: 32)
|
|
79
|
-
fileHandle.closeFile()
|
|
80
|
-
|
|
81
|
-
print(" File header (hex): \(headerData.map { String(format: "%02X", $0) }.prefix(16).joined(separator: " "))")
|
|
82
|
-
|
|
83
|
-
// Check for common audio file signatures
|
|
84
|
-
if headerData.count >= 4 {
|
|
85
|
-
let signature = headerData.prefix(4)
|
|
86
|
-
if signature.starts(with: "OggS".data(using: .ascii)!) {
|
|
87
|
-
print(" ✓ File has OGG container signature")
|
|
88
|
-
} else if signature.starts(with: [0x00, 0x00, 0x00]) {
|
|
89
|
-
print(" File might be MP4/M4A container")
|
|
90
|
-
} else {
|
|
91
|
-
print(" Unknown file signature")
|
|
92
|
-
}
|
|
93
|
-
}
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
// Clean up
|
|
97
|
-
try FileManager.default.removeItem(at: opusURL)
|
|
98
|
-
} else {
|
|
99
|
-
print("✗ No Opus file was created")
|
|
100
|
-
}
|
|
101
|
-
} else {
|
|
102
|
-
print("✗ Opus recorder failed to start recording")
|
|
103
|
-
}
|
|
104
|
-
} else {
|
|
105
|
-
print("✗ Opus recorder failed to prepare")
|
|
106
|
-
}
|
|
107
|
-
} catch {
|
|
108
|
-
print("✗ Failed to create Opus recorder: \(error)")
|
|
109
|
-
print(" Error details: \(error.localizedDescription)")
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
print()
|
|
113
|
-
|
|
114
|
-
// Test AAC recorder for comparison
|
|
115
|
-
print("Testing AVAudioRecorder with AAC settings (for comparison)...")
|
|
116
|
-
do {
|
|
117
|
-
let aacRecorder = try AVAudioRecorder(url: aacURL, settings: aacSettings)
|
|
118
|
-
print("✓ AAC recorder created successfully")
|
|
119
|
-
print(" Settings after init: \(aacRecorder.settings)")
|
|
120
|
-
print(" Format: \(aacRecorder.format)")
|
|
121
|
-
|
|
122
|
-
if aacRecorder.prepareToRecord() && aacRecorder.record() {
|
|
123
|
-
print("✓ AAC recorder working normally")
|
|
124
|
-
Thread.sleep(forTimeInterval: 0.5)
|
|
125
|
-
aacRecorder.stop()
|
|
126
|
-
|
|
127
|
-
if FileManager.default.fileExists(atPath: aacURL.path) {
|
|
128
|
-
let attributes = try FileManager.default.attributesOfItem(atPath: aacURL.path)
|
|
129
|
-
let fileSize = attributes[.size] as? Int64 ?? 0
|
|
130
|
-
print("✓ AAC file created: \(fileSize) bytes")
|
|
131
|
-
|
|
132
|
-
// Check file header
|
|
133
|
-
let fileHandle = try FileHandle(forReadingFrom: aacURL)
|
|
134
|
-
let headerData = fileHandle.readData(ofLength: 16)
|
|
135
|
-
fileHandle.closeFile()
|
|
136
|
-
|
|
137
|
-
print(" File header (hex): \(headerData.map { String(format: "%02X", $0) }.joined(separator: " "))")
|
|
138
|
-
|
|
139
|
-
try FileManager.default.removeItem(at: aacURL)
|
|
140
|
-
}
|
|
141
|
-
}
|
|
142
|
-
} catch {
|
|
143
|
-
print("✗ Failed to create AAC recorder: \(error)")
|
|
144
|
-
}
|
|
145
|
-
|
|
146
|
-
print()
|
|
147
|
-
print("Test complete!")
|
|
148
|
-
print()
|
|
149
|
-
print("Note: This test runs on macOS which may have different codec support than iOS.")
|
|
150
|
-
print("The results should be validated on an actual iOS device or simulator.")
|
|
151
|
-
}
|
|
152
|
-
|
|
153
|
-
// Run the test
|
|
154
|
-
testOpusSupport()
|
|
@@ -1,144 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env swift
|
|
2
|
-
|
|
3
|
-
import Foundation
|
|
4
|
-
import AVFoundation
|
|
5
|
-
import Accelerate
|
|
6
|
-
|
|
7
|
-
// Simple test framework
|
|
8
|
-
struct TestResult {
|
|
9
|
-
let name: String
|
|
10
|
-
let passed: Bool
|
|
11
|
-
let message: String
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
class AudioProcessingTest {
|
|
15
|
-
var results: [TestResult] = []
|
|
16
|
-
|
|
17
|
-
func assert(_ condition: Bool, _ message: String, file: String = #file, line: Int = #line) {
|
|
18
|
-
let testName = "\(file.split(separator: "/").last ?? ""):\(line)"
|
|
19
|
-
results.append(TestResult(name: testName, passed: condition, message: message))
|
|
20
|
-
if !condition {
|
|
21
|
-
print("❌ FAILED: \(message) at \(testName)")
|
|
22
|
-
}
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
func assertEqual<T: Equatable>(_ a: T, _ b: T, _ message: String = "", file: String = #file, line: Int = #line) {
|
|
26
|
-
let passed = a == b
|
|
27
|
-
let msg = message.isEmpty ? "\(a) should equal \(b)" : message
|
|
28
|
-
assert(passed, msg, file: file, line: line)
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
func assertClose(_ a: Float, _ b: Float, tolerance: Float = 0.001, _ message: String = "", file: String = #file, line: Int = #line) {
|
|
32
|
-
let passed = abs(a - b) < tolerance
|
|
33
|
-
let msg = message.isEmpty ? "\(a) should be close to \(b)" : message
|
|
34
|
-
assert(passed, msg, file: file, line: line)
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
func run() {
|
|
38
|
-
print("🧪 Running iOS Audio Processing Tests...\n")
|
|
39
|
-
|
|
40
|
-
testRMSCalculation()
|
|
41
|
-
testZeroCrossingRate()
|
|
42
|
-
testChannelConversion()
|
|
43
|
-
testBitDepthConversion()
|
|
44
|
-
|
|
45
|
-
// Print summary
|
|
46
|
-
let passed = results.filter { $0.passed }.count
|
|
47
|
-
let total = results.count
|
|
48
|
-
|
|
49
|
-
print("\n📊 Test Summary:")
|
|
50
|
-
print(" Total: \(total)")
|
|
51
|
-
print(" Passed: \(passed)")
|
|
52
|
-
print(" Failed: \(total - passed)")
|
|
53
|
-
|
|
54
|
-
if passed == total {
|
|
55
|
-
print("\n✅ All tests passed!")
|
|
56
|
-
} else {
|
|
57
|
-
print("\n❌ Some tests failed!")
|
|
58
|
-
exit(1)
|
|
59
|
-
}
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
func testRMSCalculation() {
|
|
63
|
-
print("Testing RMS calculation...")
|
|
64
|
-
|
|
65
|
-
// Create a simple sine wave
|
|
66
|
-
let sampleCount = 1024
|
|
67
|
-
var samples = [Float](repeating: 0, count: sampleCount)
|
|
68
|
-
|
|
69
|
-
// Generate 1.0 amplitude sine wave
|
|
70
|
-
for i in 0..<sampleCount {
|
|
71
|
-
samples[i] = sin(Float(i) * 2.0 * .pi / 64.0)
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
// Calculate RMS
|
|
75
|
-
var rms: Float = 0
|
|
76
|
-
vDSP_rmsqv(samples, 1, &rms, vDSP_Length(sampleCount))
|
|
77
|
-
|
|
78
|
-
// For a sine wave, RMS should be approximately 1/sqrt(2) ≈ 0.707
|
|
79
|
-
assertClose(rms, 0.707, tolerance: 0.01, "RMS of sine wave should be ~0.707")
|
|
80
|
-
|
|
81
|
-
print("✓ RMS calculation test completed")
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
func testZeroCrossingRate() {
|
|
85
|
-
print("\nTesting zero crossing rate...")
|
|
86
|
-
|
|
87
|
-
// Create a signal that crosses zero 10 times
|
|
88
|
-
let samples: [Float] = [1, -1, 1, -1, 1, -1, 1, -1, 1, -1, 1]
|
|
89
|
-
|
|
90
|
-
var zcr = 0
|
|
91
|
-
for i in 1..<samples.count {
|
|
92
|
-
if (samples[i] >= 0 && samples[i-1] < 0) || (samples[i] < 0 && samples[i-1] >= 0) {
|
|
93
|
-
zcr += 1
|
|
94
|
-
}
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
assertEqual(zcr, 10, "Should have 10 zero crossings")
|
|
98
|
-
|
|
99
|
-
print("✓ Zero crossing rate test completed")
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
func testChannelConversion() {
|
|
103
|
-
print("\nTesting channel conversion...")
|
|
104
|
-
|
|
105
|
-
// Mono to stereo
|
|
106
|
-
let monoSamples: [Float] = [0.5, -0.5, 0.3, -0.3]
|
|
107
|
-
var stereoSamples = [Float](repeating: 0, count: monoSamples.count * 2)
|
|
108
|
-
|
|
109
|
-
// Simple duplication for mono to stereo
|
|
110
|
-
for i in 0..<monoSamples.count {
|
|
111
|
-
stereoSamples[i * 2] = monoSamples[i]
|
|
112
|
-
stereoSamples[i * 2 + 1] = monoSamples[i]
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
assertEqual(stereoSamples.count, 8, "Stereo should have double the samples")
|
|
116
|
-
assertEqual(stereoSamples[0], monoSamples[0], "Left channel should match mono")
|
|
117
|
-
assertEqual(stereoSamples[1], monoSamples[0], "Right channel should match mono")
|
|
118
|
-
|
|
119
|
-
print("✓ Channel conversion test completed")
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
func testBitDepthConversion() {
|
|
123
|
-
print("\nTesting bit depth conversion...")
|
|
124
|
-
|
|
125
|
-
// 16-bit to float conversion
|
|
126
|
-
let int16Samples: [Int16] = [Int16.max, 0, Int16.min]
|
|
127
|
-
var floatSamples = [Float](repeating: 0, count: int16Samples.count)
|
|
128
|
-
|
|
129
|
-
// Convert
|
|
130
|
-
for i in 0..<int16Samples.count {
|
|
131
|
-
floatSamples[i] = Float(int16Samples[i]) / Float(Int16.max)
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
assertClose(floatSamples[0], 1.0, "Max int16 should convert to ~1.0")
|
|
135
|
-
assertClose(floatSamples[1], 0.0, "Zero should remain zero")
|
|
136
|
-
assertClose(floatSamples[2], -1.0, tolerance: 0.01, "Min int16 should convert to ~-1.0")
|
|
137
|
-
|
|
138
|
-
print("✓ Bit depth conversion test completed")
|
|
139
|
-
}
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
// Run the tests
|
|
143
|
-
let test = AudioProcessingTest()
|
|
144
|
-
test.run()
|
|
@@ -1,277 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env swift
|
|
2
|
-
|
|
3
|
-
import Foundation
|
|
4
|
-
import AVFoundation
|
|
5
|
-
|
|
6
|
-
// Simple test framework
|
|
7
|
-
struct TestResult {
|
|
8
|
-
let name: String
|
|
9
|
-
let passed: Bool
|
|
10
|
-
let message: String
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
class AudioRecordingTest {
|
|
14
|
-
var results: [TestResult] = []
|
|
15
|
-
let testDir: URL
|
|
16
|
-
var audioRecorder: AVAudioRecorder?
|
|
17
|
-
|
|
18
|
-
init() {
|
|
19
|
-
// Create a temporary directory for test files
|
|
20
|
-
let tempDir = FileManager.default.temporaryDirectory
|
|
21
|
-
testDir = tempDir.appendingPathComponent("audio_recording_test_\(UUID().uuidString)")
|
|
22
|
-
try? FileManager.default.createDirectory(at: testDir, withIntermediateDirectories: true)
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
deinit {
|
|
26
|
-
// Clean up test directory
|
|
27
|
-
try? FileManager.default.removeItem(at: testDir)
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
func assert(_ condition: Bool, _ message: String, file: String = #file, line: Int = #line) {
|
|
31
|
-
let testName = "\(file.split(separator: "/").last ?? ""):\(line)"
|
|
32
|
-
results.append(TestResult(name: testName, passed: condition, message: message))
|
|
33
|
-
if !condition {
|
|
34
|
-
print("❌ FAILED: \(message) at \(testName)")
|
|
35
|
-
}
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
func assertEqual<T: Equatable>(_ a: T, _ b: T, _ message: String = "", file: String = #file, line: Int = #line) {
|
|
39
|
-
let passed = a == b
|
|
40
|
-
let msg = message.isEmpty ? "\(a) should equal \(b)" : message
|
|
41
|
-
assert(passed, msg, file: file, line: line)
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
func run() {
|
|
45
|
-
print("🧪 Running iOS Audio Recording Tests...\n")
|
|
46
|
-
|
|
47
|
-
// Request permission first (in a real app)
|
|
48
|
-
setupAudioSession()
|
|
49
|
-
|
|
50
|
-
testBasicWAVRecording()
|
|
51
|
-
testCompressedRecording()
|
|
52
|
-
testRecordingSettings()
|
|
53
|
-
testFileValidation()
|
|
54
|
-
|
|
55
|
-
// Print summary
|
|
56
|
-
let passed = results.filter { $0.passed }.count
|
|
57
|
-
let total = results.count
|
|
58
|
-
|
|
59
|
-
print("\n📊 Test Summary:")
|
|
60
|
-
print(" Total: \(total)")
|
|
61
|
-
print(" Passed: \(passed)")
|
|
62
|
-
print(" Failed: \(total - passed)")
|
|
63
|
-
|
|
64
|
-
if passed == total {
|
|
65
|
-
print("\n✅ All tests passed!")
|
|
66
|
-
} else {
|
|
67
|
-
print("\n❌ Some tests failed!")
|
|
68
|
-
exit(1)
|
|
69
|
-
}
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
func setupAudioSession() {
|
|
73
|
-
#if os(iOS)
|
|
74
|
-
let session = AVAudioSession.sharedInstance()
|
|
75
|
-
do {
|
|
76
|
-
try session.setCategory(.playAndRecord, mode: .default)
|
|
77
|
-
try session.setActive(true)
|
|
78
|
-
print("✓ Audio session configured")
|
|
79
|
-
} catch {
|
|
80
|
-
print("⚠️ Failed to setup audio session: \(error)")
|
|
81
|
-
}
|
|
82
|
-
#else
|
|
83
|
-
print("✓ Audio session setup skipped (macOS)")
|
|
84
|
-
#endif
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
func testBasicWAVRecording() {
|
|
88
|
-
print("\nTesting basic WAV recording...")
|
|
89
|
-
|
|
90
|
-
let wavURL = testDir.appendingPathComponent("test_recording.wav")
|
|
91
|
-
|
|
92
|
-
// Configure recording settings for WAV
|
|
93
|
-
let settings: [String: Any] = [
|
|
94
|
-
AVFormatIDKey: Int(kAudioFormatLinearPCM),
|
|
95
|
-
AVSampleRateKey: 44100.0,
|
|
96
|
-
AVNumberOfChannelsKey: 2,
|
|
97
|
-
AVLinearPCMBitDepthKey: 16,
|
|
98
|
-
AVLinearPCMIsBigEndianKey: false,
|
|
99
|
-
AVLinearPCMIsFloatKey: false
|
|
100
|
-
]
|
|
101
|
-
|
|
102
|
-
do {
|
|
103
|
-
// Create recorder
|
|
104
|
-
audioRecorder = try AVAudioRecorder(url: wavURL, settings: settings)
|
|
105
|
-
assert(audioRecorder != nil, "Recorder should be created")
|
|
106
|
-
|
|
107
|
-
// Prepare and record
|
|
108
|
-
let prepared = audioRecorder!.prepareToRecord()
|
|
109
|
-
assert(prepared, "Recorder should prepare successfully")
|
|
110
|
-
|
|
111
|
-
let started = audioRecorder!.record()
|
|
112
|
-
assert(started, "Recording should start")
|
|
113
|
-
|
|
114
|
-
// Record for a short time
|
|
115
|
-
Thread.sleep(forTimeInterval: 0.5)
|
|
116
|
-
|
|
117
|
-
audioRecorder!.stop()
|
|
118
|
-
|
|
119
|
-
// Verify file exists and has content
|
|
120
|
-
assert(FileManager.default.fileExists(atPath: wavURL.path), "WAV file should exist")
|
|
121
|
-
|
|
122
|
-
let attributes = try FileManager.default.attributesOfItem(atPath: wavURL.path)
|
|
123
|
-
let fileSize = attributes[.size] as? Int64 ?? 0
|
|
124
|
-
assert(fileSize > 44, "WAV file should have content beyond header")
|
|
125
|
-
|
|
126
|
-
// Verify WAV header
|
|
127
|
-
let data = try Data(contentsOf: wavURL)
|
|
128
|
-
let riffHeader = String(data: data[0..<4], encoding: .ascii)
|
|
129
|
-
assertEqual(riffHeader, "RIFF", "Should have RIFF header")
|
|
130
|
-
|
|
131
|
-
let waveFormat = String(data: data[8..<12], encoding: .ascii)
|
|
132
|
-
assertEqual(waveFormat, "WAVE", "Should have WAVE format")
|
|
133
|
-
|
|
134
|
-
print("✓ Basic WAV recording test completed")
|
|
135
|
-
|
|
136
|
-
} catch {
|
|
137
|
-
assert(false, "Recording failed: \(error)")
|
|
138
|
-
}
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
func testCompressedRecording() {
|
|
142
|
-
print("\nTesting compressed recording (AAC)...")
|
|
143
|
-
|
|
144
|
-
let aacURL = testDir.appendingPathComponent("test_recording.m4a")
|
|
145
|
-
|
|
146
|
-
// Configure recording settings for AAC
|
|
147
|
-
let settings: [String: Any] = [
|
|
148
|
-
AVFormatIDKey: Int(kAudioFormatMPEG4AAC),
|
|
149
|
-
AVSampleRateKey: 44100.0,
|
|
150
|
-
AVNumberOfChannelsKey: 2,
|
|
151
|
-
AVEncoderBitRateKey: 128000,
|
|
152
|
-
AVEncoderAudioQualityKey: AVAudioQuality.high.rawValue
|
|
153
|
-
]
|
|
154
|
-
|
|
155
|
-
do {
|
|
156
|
-
// Create recorder
|
|
157
|
-
audioRecorder = try AVAudioRecorder(url: aacURL, settings: settings)
|
|
158
|
-
assert(audioRecorder != nil, "AAC recorder should be created")
|
|
159
|
-
|
|
160
|
-
// Record
|
|
161
|
-
let started = audioRecorder!.record()
|
|
162
|
-
assert(started, "AAC recording should start")
|
|
163
|
-
|
|
164
|
-
Thread.sleep(forTimeInterval: 0.5)
|
|
165
|
-
audioRecorder!.stop()
|
|
166
|
-
|
|
167
|
-
// Verify file
|
|
168
|
-
assert(FileManager.default.fileExists(atPath: aacURL.path), "AAC file should exist")
|
|
169
|
-
|
|
170
|
-
let attributes = try FileManager.default.attributesOfItem(atPath: aacURL.path)
|
|
171
|
-
let fileSize = attributes[.size] as? Int64 ?? 0
|
|
172
|
-
assert(fileSize > 0, "AAC file should have content")
|
|
173
|
-
|
|
174
|
-
// Verify it's a valid audio file by loading it
|
|
175
|
-
let audioFile = try AVAudioFile(forReading: aacURL)
|
|
176
|
-
assert(audioFile.length > 0, "AAC file should have audio frames")
|
|
177
|
-
assertEqual(Int(audioFile.fileFormat.sampleRate), 44100, "Sample rate should match")
|
|
178
|
-
|
|
179
|
-
print("✓ Compressed recording test completed")
|
|
180
|
-
|
|
181
|
-
} catch {
|
|
182
|
-
assert(false, "AAC recording failed: \(error)")
|
|
183
|
-
}
|
|
184
|
-
}
|
|
185
|
-
|
|
186
|
-
func testRecordingSettings() {
|
|
187
|
-
print("\nTesting various recording settings...")
|
|
188
|
-
|
|
189
|
-
// Test different sample rates
|
|
190
|
-
let sampleRates = [8000.0, 16000.0, 44100.0, 48000.0]
|
|
191
|
-
|
|
192
|
-
for sampleRate in sampleRates {
|
|
193
|
-
let url = testDir.appendingPathComponent("test_\(Int(sampleRate))hz.wav")
|
|
194
|
-
|
|
195
|
-
let settings: [String: Any] = [
|
|
196
|
-
AVFormatIDKey: Int(kAudioFormatLinearPCM),
|
|
197
|
-
AVSampleRateKey: sampleRate,
|
|
198
|
-
AVNumberOfChannelsKey: 1,
|
|
199
|
-
AVLinearPCMBitDepthKey: 16,
|
|
200
|
-
AVLinearPCMIsBigEndianKey: false,
|
|
201
|
-
AVLinearPCMIsFloatKey: false
|
|
202
|
-
]
|
|
203
|
-
|
|
204
|
-
do {
|
|
205
|
-
let recorder = try AVAudioRecorder(url: url, settings: settings)
|
|
206
|
-
assert(recorder.prepareToRecord(), "Should prepare at \(sampleRate)Hz")
|
|
207
|
-
|
|
208
|
-
// Verify settings were applied
|
|
209
|
-
let appliedSettings = recorder.settings
|
|
210
|
-
let appliedRate = appliedSettings[AVSampleRateKey] as? Double ?? 0
|
|
211
|
-
assertEqual(appliedRate, sampleRate, "Sample rate should be \(sampleRate)")
|
|
212
|
-
|
|
213
|
-
} catch {
|
|
214
|
-
assert(false, "Failed to create recorder at \(sampleRate)Hz: \(error)")
|
|
215
|
-
}
|
|
216
|
-
}
|
|
217
|
-
|
|
218
|
-
print("✓ Recording settings test completed")
|
|
219
|
-
}
|
|
220
|
-
|
|
221
|
-
func testFileValidation() {
|
|
222
|
-
print("\nTesting file validation and properties...")
|
|
223
|
-
|
|
224
|
-
// Create a test recording
|
|
225
|
-
let url = testDir.appendingPathComponent("validation_test.wav")
|
|
226
|
-
let duration = 1.0 // 1 second
|
|
227
|
-
|
|
228
|
-
let settings: [String: Any] = [
|
|
229
|
-
AVFormatIDKey: Int(kAudioFormatLinearPCM),
|
|
230
|
-
AVSampleRateKey: 16000.0,
|
|
231
|
-
AVNumberOfChannelsKey: 1,
|
|
232
|
-
AVLinearPCMBitDepthKey: 16,
|
|
233
|
-
AVLinearPCMIsBigEndianKey: false,
|
|
234
|
-
AVLinearPCMIsFloatKey: false
|
|
235
|
-
]
|
|
236
|
-
|
|
237
|
-
do {
|
|
238
|
-
audioRecorder = try AVAudioRecorder(url: url, settings: settings)
|
|
239
|
-
audioRecorder!.record()
|
|
240
|
-
|
|
241
|
-
// Record for the specified duration
|
|
242
|
-
Thread.sleep(forTimeInterval: duration)
|
|
243
|
-
audioRecorder!.stop()
|
|
244
|
-
|
|
245
|
-
// Load and validate the file
|
|
246
|
-
let audioFile = try AVAudioFile(forReading: url)
|
|
247
|
-
|
|
248
|
-
// Check duration (should be close to 1 second)
|
|
249
|
-
let recordedDuration = Double(audioFile.length) / audioFile.fileFormat.sampleRate
|
|
250
|
-
assert(abs(recordedDuration - duration) < 0.5, "Duration should be close to \(duration)s (got \(recordedDuration)s)")
|
|
251
|
-
|
|
252
|
-
// Check file format
|
|
253
|
-
assertEqual(Int(audioFile.fileFormat.sampleRate), 16000, "Sample rate should be 16kHz")
|
|
254
|
-
assertEqual(Int(audioFile.fileFormat.channelCount), 1, "Should be mono")
|
|
255
|
-
|
|
256
|
-
// Calculate expected file size
|
|
257
|
-
let expectedDataSize = Int(16000 * duration * 2) // 16kHz * 1s * 2 bytes per sample
|
|
258
|
-
let expectedFileSize = expectedDataSize + 44 // Plus WAV header
|
|
259
|
-
|
|
260
|
-
let attributes = try FileManager.default.attributesOfItem(atPath: url.path)
|
|
261
|
-
let actualFileSize = attributes[.size] as? Int64 ?? 0
|
|
262
|
-
|
|
263
|
-
// Allow some tolerance (macOS may add extra metadata)
|
|
264
|
-
assert(abs(Int(actualFileSize) - expectedFileSize) < 5000,
|
|
265
|
-
"File size should be close to expected (\(actualFileSize) vs \(expectedFileSize))")
|
|
266
|
-
|
|
267
|
-
print("✓ File validation test completed")
|
|
268
|
-
|
|
269
|
-
} catch {
|
|
270
|
-
assert(false, "File validation failed: \(error)")
|
|
271
|
-
}
|
|
272
|
-
}
|
|
273
|
-
}
|
|
274
|
-
|
|
275
|
-
// Run the tests
|
|
276
|
-
let test = AudioRecordingTest()
|
|
277
|
-
test.run()
|