@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,22 +0,0 @@
|
|
|
1
|
-
<?xml version="1.0" encoding="UTF-8"?>
|
|
2
|
-
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
3
|
-
<plist version="1.0">
|
|
4
|
-
<dict>
|
|
5
|
-
<key>CFBundleDevelopmentRegion</key>
|
|
6
|
-
<string>$(DEVELOPMENT_LANGUAGE)</string>
|
|
7
|
-
<key>CFBundleExecutable</key>
|
|
8
|
-
<string>$(EXECUTABLE_NAME)</string>
|
|
9
|
-
<key>CFBundleIdentifier</key>
|
|
10
|
-
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
|
|
11
|
-
<key>CFBundleInfoDictionaryVersion</key>
|
|
12
|
-
<string>6.0</string>
|
|
13
|
-
<key>CFBundleName</key>
|
|
14
|
-
<string>$(PRODUCT_NAME)</string>
|
|
15
|
-
<key>CFBundlePackageType</key>
|
|
16
|
-
<string>$(PRODUCT_BUNDLE_PACKAGE_TYPE)</string>
|
|
17
|
-
<key>CFBundleShortVersionString</key>
|
|
18
|
-
<string>1.0</string>
|
|
19
|
-
<key>CFBundleVersion</key>
|
|
20
|
-
<string>1</string>
|
|
21
|
-
</dict>
|
|
22
|
-
</plist>
|
|
@@ -1,39 +0,0 @@
|
|
|
1
|
-
# AudioStudio iOS Unit Tests
|
|
2
|
-
|
|
3
|
-
This directory contains unit tests for the AudioStudio iOS module.
|
|
4
|
-
|
|
5
|
-
## Test Files
|
|
6
|
-
|
|
7
|
-
- `AudioTestHelpers.swift` - Common test utilities and extensions
|
|
8
|
-
- `AudioFormatUtilsTests.swift` - Tests for audio format utilities
|
|
9
|
-
- `AudioFileHandlerTests.swift` - Tests for file handling
|
|
10
|
-
- `SimpleAudioTest.swift` - Basic audio functionality tests
|
|
11
|
-
- `TestAudioGenerator.swift` - Audio generation utilities for testing
|
|
12
|
-
- `CompressedOnlyOutputTests.swift` - Tests for compressed-only output feature (Issue #244)
|
|
13
|
-
|
|
14
|
-
## Running Tests
|
|
15
|
-
|
|
16
|
-
### In Xcode
|
|
17
|
-
1. Open the workspace/project containing AudioStudio
|
|
18
|
-
2. Select the test target
|
|
19
|
-
3. Press `Cmd+U` to run all tests or click on individual test methods
|
|
20
|
-
|
|
21
|
-
### From Command Line
|
|
22
|
-
```bash
|
|
23
|
-
# Run all tests
|
|
24
|
-
xcodebuild test -scheme AudioStudioTests -destination 'platform=iOS Simulator,name=iPhone 15'
|
|
25
|
-
|
|
26
|
-
# Run specific test class
|
|
27
|
-
xcodebuild test -scheme AudioStudioTests -destination 'platform=iOS Simulator,name=iPhone 15' -only-testing:AudioStudioTests/CompressedOnlyOutputTests
|
|
28
|
-
```
|
|
29
|
-
|
|
30
|
-
## Compressed-Only Output Tests
|
|
31
|
-
|
|
32
|
-
The `CompressedOnlyOutputTests.swift` file tests the fix for Issue #244, ensuring that:
|
|
33
|
-
- Compression info is properly returned when primary output is disabled
|
|
34
|
-
- AAC format works correctly
|
|
35
|
-
- Opus format falls back to AAC on iOS
|
|
36
|
-
- Compressed file URIs are accessible
|
|
37
|
-
- File sizes and metadata are correctly reported
|
|
38
|
-
|
|
39
|
-
These tests verify that users can access compressed audio files even when primary WAV output is disabled.
|
|
@@ -1,98 +0,0 @@
|
|
|
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
|
-
}
|
|
@@ -1,75 +0,0 @@
|
|
|
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
|
-
}
|
package/ios/tests/README.md
DELETED
|
@@ -1,41 +0,0 @@
|
|
|
1
|
-
# iOS Audio Format Tests
|
|
2
|
-
|
|
3
|
-
This directory contains test scripts for validating audio format support on iOS/macOS.
|
|
4
|
-
|
|
5
|
-
## Opus Support Test
|
|
6
|
-
|
|
7
|
-
The `opus_support_test_macos.swift` script verifies that while `kAudioFormatOpus` is defined in the iOS SDK, AVAudioRecorder cannot actually encode Opus audio.
|
|
8
|
-
|
|
9
|
-
### Running the Test
|
|
10
|
-
|
|
11
|
-
```bash
|
|
12
|
-
# On macOS (for quick validation)
|
|
13
|
-
swift opus_support_test_macos.swift
|
|
14
|
-
|
|
15
|
-
# On iOS device/simulator (requires Xcode)
|
|
16
|
-
# Copy the test to an iOS project and run it
|
|
17
|
-
```
|
|
18
|
-
|
|
19
|
-
### Test Results
|
|
20
|
-
|
|
21
|
-
- ✅ `kAudioFormatOpus` constant exists (value: 1869641075)
|
|
22
|
-
- ✅ AVAudioRecorder accepts Opus settings without errors
|
|
23
|
-
- ❌ Recording produces 0-byte files (no actual encoding)
|
|
24
|
-
- ✅ AAC format works correctly as fallback
|
|
25
|
-
|
|
26
|
-
### Why This Matters
|
|
27
|
-
|
|
28
|
-
This test proves that expo-audio-studio's automatic fallback from Opus to AAC on iOS is necessary and correct. Despite the SDK defining the Opus format constant, the actual encoding functionality is not implemented in AVAudioRecorder.
|
|
29
|
-
|
|
30
|
-
## Format Verification
|
|
31
|
-
|
|
32
|
-
To verify actual file formats:
|
|
33
|
-
|
|
34
|
-
```bash
|
|
35
|
-
# Check file type
|
|
36
|
-
file recording.m4a # Should show: ISO Media, MP4 Base Media
|
|
37
|
-
file recording.aac # Should show: ADTS, AAC
|
|
38
|
-
|
|
39
|
-
# Get detailed info (requires mediainfo)
|
|
40
|
-
mediainfo recording.m4a
|
|
41
|
-
```
|
|
@@ -1,178 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env swift
|
|
2
|
-
|
|
3
|
-
import Foundation
|
|
4
|
-
import AVFoundation
|
|
5
|
-
|
|
6
|
-
// Integration test for validating buffer size calculation and fallback behavior fixes
|
|
7
|
-
// Tests issues #246 and #247
|
|
8
|
-
|
|
9
|
-
print("🧪 Buffer Size Calculation and Fallback Integration Test")
|
|
10
|
-
print("======================================================\n")
|
|
11
|
-
|
|
12
|
-
class BufferAndFallbackTest {
|
|
13
|
-
let audioEngine = AVAudioEngine()
|
|
14
|
-
var results: [(name: String, passed: Bool, message: String)] = []
|
|
15
|
-
var emissionCount = 0
|
|
16
|
-
var lastEmissionData: Data?
|
|
17
|
-
|
|
18
|
-
func runAllTests() {
|
|
19
|
-
testBufferSizeCalculation()
|
|
20
|
-
testFallbackWithoutDuplication()
|
|
21
|
-
printResults()
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
func testBufferSizeCalculation() {
|
|
25
|
-
print("Test 1: Buffer Size Calculation with Target Sample Rate")
|
|
26
|
-
print("-------------------------------------------------------")
|
|
27
|
-
print("Testing that buffer size is calculated based on target sample rate, not hardware rate")
|
|
28
|
-
|
|
29
|
-
let inputNode = audioEngine.inputNode
|
|
30
|
-
let hardwareFormat = inputNode.inputFormat(forBus: 0)
|
|
31
|
-
let hardwareSampleRate = hardwareFormat.sampleRate
|
|
32
|
-
|
|
33
|
-
print("Hardware sample rate: \(hardwareSampleRate) Hz")
|
|
34
|
-
|
|
35
|
-
// Test case: 0.02 seconds at 16000 Hz should request 320 frames
|
|
36
|
-
let targetSampleRate: Double = 16000
|
|
37
|
-
let bufferDuration: Double = 0.02
|
|
38
|
-
let expectedRequestedFrames = AVAudioFrameCount(bufferDuration * targetSampleRate)
|
|
39
|
-
|
|
40
|
-
print("Target sample rate: \(targetSampleRate) Hz")
|
|
41
|
-
print("Buffer duration: \(bufferDuration) seconds")
|
|
42
|
-
print("Expected requested frames: \(expectedRequestedFrames)")
|
|
43
|
-
|
|
44
|
-
// Since iOS enforces minimum ~4800 frames, we expect either 4800 or our requested size
|
|
45
|
-
let _ : AVAudioFrameCount = max(4800, expectedRequestedFrames)
|
|
46
|
-
|
|
47
|
-
let expectation = DispatchSemaphore(value: 0)
|
|
48
|
-
var receivedFrames: AVAudioFrameCount = 0
|
|
49
|
-
|
|
50
|
-
inputNode.installTap(onBus: 0, bufferSize: expectedRequestedFrames, format: hardwareFormat) { buffer, _ in
|
|
51
|
-
receivedFrames = buffer.frameLength
|
|
52
|
-
expectation.signal()
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
audioEngine.prepare()
|
|
56
|
-
do {
|
|
57
|
-
try audioEngine.start()
|
|
58
|
-
_ = expectation.wait(timeout: .now() + 2)
|
|
59
|
-
audioEngine.stop()
|
|
60
|
-
} catch {
|
|
61
|
-
print("Error: \(error)")
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
inputNode.removeTap(onBus: 0)
|
|
65
|
-
|
|
66
|
-
// The key test: verify that we calculated based on target rate (320 frames), not hardware rate
|
|
67
|
-
let wouldHaveBeenWithHardwareRate = AVAudioFrameCount(bufferDuration * hardwareSampleRate)
|
|
68
|
-
let usedTargetRate = expectedRequestedFrames == 320
|
|
69
|
-
|
|
70
|
-
results.append((
|
|
71
|
-
name: "Buffer Size Calculation",
|
|
72
|
-
passed: usedTargetRate,
|
|
73
|
-
message: "Used target rate: \(usedTargetRate), Requested: \(expectedRequestedFrames) frames (would be \(wouldHaveBeenWithHardwareRate) with hardware rate)"
|
|
74
|
-
))
|
|
75
|
-
|
|
76
|
-
print("✓ Requested frames: \(expectedRequestedFrames) (calculated from target rate)")
|
|
77
|
-
print("✓ Would have been: \(wouldHaveBeenWithHardwareRate) frames (if using hardware rate)")
|
|
78
|
-
print("✓ Actually received: \(receivedFrames) frames (iOS minimum enforced)\n")
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
func testFallbackWithoutDuplication() {
|
|
82
|
-
print("Test 2: Fallback Without Data Duplication")
|
|
83
|
-
print("-----------------------------------------")
|
|
84
|
-
print("Simulating device fallback scenario to ensure no duplicate emissions")
|
|
85
|
-
|
|
86
|
-
// Reset counters
|
|
87
|
-
emissionCount = 0
|
|
88
|
-
lastEmissionData = nil
|
|
89
|
-
|
|
90
|
-
let inputNode = audioEngine.inputNode
|
|
91
|
-
let format = inputNode.inputFormat(forBus: 0)
|
|
92
|
-
|
|
93
|
-
// Simulate a tap that counts emissions
|
|
94
|
-
var bufferCount = 0
|
|
95
|
-
let expectation = DispatchSemaphore(value: 0)
|
|
96
|
-
|
|
97
|
-
inputNode.installTap(onBus: 0, bufferSize: 1024, format: format) { [weak self] buffer, _ in
|
|
98
|
-
guard let self = self else { return }
|
|
99
|
-
|
|
100
|
-
bufferCount += 1
|
|
101
|
-
|
|
102
|
-
// Simulate emission logic
|
|
103
|
-
let audioData = buffer.audioBufferList.pointee.mBuffers
|
|
104
|
-
if let bufferData = audioData.mData {
|
|
105
|
-
let data = Data(bytes: bufferData, count: Int(audioData.mDataByteSize))
|
|
106
|
-
|
|
107
|
-
// Check if this is the same data as last emission
|
|
108
|
-
if let lastData = self.lastEmissionData, lastData == data {
|
|
109
|
-
print("⚠️ Detected duplicate emission!")
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
self.lastEmissionData = data
|
|
113
|
-
self.emissionCount += 1
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
if bufferCount >= 10 {
|
|
117
|
-
expectation.signal()
|
|
118
|
-
}
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
audioEngine.prepare()
|
|
122
|
-
do {
|
|
123
|
-
try audioEngine.start()
|
|
124
|
-
_ = expectation.wait(timeout: .now() + 3)
|
|
125
|
-
audioEngine.stop()
|
|
126
|
-
} catch {
|
|
127
|
-
print("Error: \(error)")
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
inputNode.removeTap(onBus: 0)
|
|
131
|
-
|
|
132
|
-
// With the fix, emission count should equal buffer count (no duplicates)
|
|
133
|
-
let noDuplicates = emissionCount == bufferCount
|
|
134
|
-
|
|
135
|
-
results.append((
|
|
136
|
-
name: "Fallback No Duplication",
|
|
137
|
-
passed: noDuplicates,
|
|
138
|
-
message: "Buffers: \(bufferCount), Emissions: \(emissionCount), No duplicates: \(noDuplicates)"
|
|
139
|
-
))
|
|
140
|
-
|
|
141
|
-
print("✓ Processed \(bufferCount) buffers")
|
|
142
|
-
print("✓ Emitted \(emissionCount) times")
|
|
143
|
-
print("✓ No duplicate emissions: \(noDuplicates)\n")
|
|
144
|
-
}
|
|
145
|
-
|
|
146
|
-
func printResults() {
|
|
147
|
-
print("📊 Test Results")
|
|
148
|
-
print("===============")
|
|
149
|
-
|
|
150
|
-
let passed = results.filter { $0.passed }.count
|
|
151
|
-
let total = results.count
|
|
152
|
-
|
|
153
|
-
for result in results {
|
|
154
|
-
let status = result.passed ? "✅" : "❌"
|
|
155
|
-
print("\(status) \(result.name)")
|
|
156
|
-
print(" \(result.message)")
|
|
157
|
-
}
|
|
158
|
-
|
|
159
|
-
print("\nSummary: \(passed)/\(total) tests passed")
|
|
160
|
-
|
|
161
|
-
if passed == total {
|
|
162
|
-
print("🎉 All tests passed!")
|
|
163
|
-
print("\n✅ Issue #247 (Buffer Size Calculation) - FIXED")
|
|
164
|
-
print("✅ Issue #246 (Duplicate Emissions) - Validation Ready")
|
|
165
|
-
} else {
|
|
166
|
-
print("⚠️ Some tests failed")
|
|
167
|
-
}
|
|
168
|
-
|
|
169
|
-
print("\n📝 Key Validations:")
|
|
170
|
-
print("- Buffer size is now calculated using target sample rate")
|
|
171
|
-
print("- iOS minimum buffer size (~4800 frames) is properly handled")
|
|
172
|
-
print("- Fallback behavior ready for duplicate emission testing")
|
|
173
|
-
}
|
|
174
|
-
}
|
|
175
|
-
|
|
176
|
-
// Run the test
|
|
177
|
-
let test = BufferAndFallbackTest()
|
|
178
|
-
test.runAllTests()
|
|
@@ -1,185 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env swift
|
|
2
|
-
|
|
3
|
-
import Foundation
|
|
4
|
-
import AVFoundation
|
|
5
|
-
|
|
6
|
-
// Integration test for Buffer Duration feature
|
|
7
|
-
// This tests the ACTUAL behavior of AVAudioEngine with different buffer sizes
|
|
8
|
-
|
|
9
|
-
print("🧪 Buffer Duration Integration Test")
|
|
10
|
-
print("===================================\n")
|
|
11
|
-
|
|
12
|
-
class BufferDurationTest {
|
|
13
|
-
let audioEngine = AVAudioEngine()
|
|
14
|
-
var results: [(name: String, passed: Bool, message: String)] = []
|
|
15
|
-
|
|
16
|
-
func runAllTests() {
|
|
17
|
-
testDefaultBufferSize()
|
|
18
|
-
testCustomBufferSizes()
|
|
19
|
-
testBufferSizeLimits()
|
|
20
|
-
printResults()
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
func testDefaultBufferSize() {
|
|
24
|
-
print("Test 1: Default Buffer Size (1024 frames requested)")
|
|
25
|
-
print("-------------------------------------------------")
|
|
26
|
-
|
|
27
|
-
let inputNode = audioEngine.inputNode
|
|
28
|
-
let expectation = DispatchSemaphore(value: 0)
|
|
29
|
-
var receivedFrames: AVAudioFrameCount = 0
|
|
30
|
-
|
|
31
|
-
inputNode.installTap(onBus: 0, bufferSize: 1024, format: inputNode.inputFormat(forBus: 0)) { buffer, _ in
|
|
32
|
-
receivedFrames = buffer.frameLength
|
|
33
|
-
expectation.signal()
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
audioEngine.prepare()
|
|
37
|
-
do {
|
|
38
|
-
try audioEngine.start()
|
|
39
|
-
_ = expectation.wait(timeout: .now() + 2)
|
|
40
|
-
audioEngine.stop()
|
|
41
|
-
} catch {
|
|
42
|
-
print("Error: \(error)")
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
inputNode.removeTap(onBus: 0)
|
|
46
|
-
|
|
47
|
-
// iOS enforces minimum of ~4800 frames
|
|
48
|
-
let passed = receivedFrames >= 4800
|
|
49
|
-
results.append((
|
|
50
|
-
name: "Default Buffer Size",
|
|
51
|
-
passed: passed,
|
|
52
|
-
message: "Requested: 1024, Received: \(receivedFrames) frames (iOS minimum: ~4800)"
|
|
53
|
-
))
|
|
54
|
-
|
|
55
|
-
print("✓ Requested: 1024 frames")
|
|
56
|
-
print("✓ Received: \(receivedFrames) frames")
|
|
57
|
-
print("✓ iOS enforces minimum buffer size\n")
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
func testCustomBufferSizes() {
|
|
61
|
-
print("Test 2: Custom Buffer Sizes")
|
|
62
|
-
print("---------------------------")
|
|
63
|
-
|
|
64
|
-
let inputNode = audioEngine.inputNode
|
|
65
|
-
let sampleRate = inputNode.inputFormat(forBus: 0).sampleRate
|
|
66
|
-
|
|
67
|
-
let testCases: [(duration: Double, name: String)] = [
|
|
68
|
-
(0.01, "10ms"),
|
|
69
|
-
(0.05, "50ms"),
|
|
70
|
-
(0.1, "100ms"),
|
|
71
|
-
(0.2, "200ms"),
|
|
72
|
-
(0.5, "500ms")
|
|
73
|
-
]
|
|
74
|
-
|
|
75
|
-
for testCase in testCases {
|
|
76
|
-
let requestedFrames = AVAudioFrameCount(testCase.duration * sampleRate)
|
|
77
|
-
let expectation = DispatchSemaphore(value: 0)
|
|
78
|
-
var receivedFrames: AVAudioFrameCount = 0
|
|
79
|
-
|
|
80
|
-
inputNode.removeTap(onBus: 0)
|
|
81
|
-
inputNode.installTap(onBus: 0, bufferSize: requestedFrames, format: inputNode.inputFormat(forBus: 0)) { buffer, _ in
|
|
82
|
-
receivedFrames = buffer.frameLength
|
|
83
|
-
expectation.signal()
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
do {
|
|
87
|
-
try audioEngine.start()
|
|
88
|
-
_ = expectation.wait(timeout: .now() + 2)
|
|
89
|
-
audioEngine.stop()
|
|
90
|
-
} catch {
|
|
91
|
-
print("Error: \(error)")
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
let expectedFrames: AVAudioFrameCount = requestedFrames < 4800 ? 4800 : requestedFrames
|
|
95
|
-
let tolerance: AVAudioFrameCount = expectedFrames > 10000 ? AVAudioFrameCount(Double(expectedFrames) * 0.2) : 100
|
|
96
|
-
let passed = abs(Int32(receivedFrames) - Int32(expectedFrames)) <= Int32(tolerance)
|
|
97
|
-
|
|
98
|
-
results.append((
|
|
99
|
-
name: "Buffer \(testCase.name)",
|
|
100
|
-
passed: passed,
|
|
101
|
-
message: "Requested: \(requestedFrames), Expected: \(expectedFrames), Received: \(receivedFrames)"
|
|
102
|
-
))
|
|
103
|
-
|
|
104
|
-
print(" \(testCase.name): Requested \(requestedFrames) → Received \(receivedFrames) frames")
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
inputNode.removeTap(onBus: 0)
|
|
108
|
-
print()
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
func testBufferSizeLimits() {
|
|
112
|
-
print("Test 3: Buffer Size Limits")
|
|
113
|
-
print("--------------------------")
|
|
114
|
-
|
|
115
|
-
let inputNode = audioEngine.inputNode
|
|
116
|
-
|
|
117
|
-
let extremeCases: [(size: AVAudioFrameCount, name: String)] = [
|
|
118
|
-
(100, "Very small (100 frames)"),
|
|
119
|
-
(50000, "Very large (50000 frames)")
|
|
120
|
-
]
|
|
121
|
-
|
|
122
|
-
for testCase in extremeCases {
|
|
123
|
-
let expectation = DispatchSemaphore(value: 0)
|
|
124
|
-
var receivedFrames: AVAudioFrameCount = 0
|
|
125
|
-
|
|
126
|
-
inputNode.removeTap(onBus: 0)
|
|
127
|
-
inputNode.installTap(onBus: 0, bufferSize: testCase.size, format: inputNode.inputFormat(forBus: 0)) { buffer, _ in
|
|
128
|
-
receivedFrames = buffer.frameLength
|
|
129
|
-
expectation.signal()
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
do {
|
|
133
|
-
try audioEngine.start()
|
|
134
|
-
_ = expectation.wait(timeout: .now() + 2)
|
|
135
|
-
audioEngine.stop()
|
|
136
|
-
} catch {
|
|
137
|
-
print("Error: \(error)")
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
let passed = receivedFrames >= 4800 && receivedFrames <= 50000
|
|
141
|
-
results.append((
|
|
142
|
-
name: testCase.name,
|
|
143
|
-
passed: passed,
|
|
144
|
-
message: "Requested: \(testCase.size), Received: \(receivedFrames)"
|
|
145
|
-
))
|
|
146
|
-
|
|
147
|
-
print(" \(testCase.name): \(testCase.size) → \(receivedFrames) frames")
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
inputNode.removeTap(onBus: 0)
|
|
151
|
-
print()
|
|
152
|
-
}
|
|
153
|
-
|
|
154
|
-
func printResults() {
|
|
155
|
-
print("📊 Test Results")
|
|
156
|
-
print("===============")
|
|
157
|
-
|
|
158
|
-
let passed = results.filter { $0.passed }.count
|
|
159
|
-
let total = results.count
|
|
160
|
-
|
|
161
|
-
for result in results {
|
|
162
|
-
let status = result.passed ? "✅" : "❌"
|
|
163
|
-
print("\(status) \(result.name)")
|
|
164
|
-
print(" \(result.message)")
|
|
165
|
-
}
|
|
166
|
-
|
|
167
|
-
print("\nSummary: \(passed)/\(total) tests passed")
|
|
168
|
-
|
|
169
|
-
if passed == total {
|
|
170
|
-
print("🎉 All tests passed!")
|
|
171
|
-
} else {
|
|
172
|
-
print("⚠️ Some tests failed")
|
|
173
|
-
}
|
|
174
|
-
|
|
175
|
-
print("\n📝 Key Findings:")
|
|
176
|
-
print("- iOS AVAudioEngine enforces a minimum buffer size of ~4800 frames")
|
|
177
|
-
print("- Requests below 4800 frames are ignored")
|
|
178
|
-
print("- Larger buffer sizes generally work as requested")
|
|
179
|
-
print("- Buffer accumulation is needed for small buffer durations")
|
|
180
|
-
}
|
|
181
|
-
}
|
|
182
|
-
|
|
183
|
-
// Run the test
|
|
184
|
-
let test = BufferDurationTest()
|
|
185
|
-
test.runAllTests()
|