@siteed/audio-studio 3.2.0-beta.1 → 3.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (85) hide show
  1. package/CHANGELOG.md +356 -5
  2. package/android/src/main/java/net/siteed/audiostudio/AudioStreamDecoder.kt +306 -94
  3. package/android/src/main/java/net/siteed/audiostudio/AudioStudioModule.kt +39 -6
  4. package/build/cjs/errors/AudioStreamError.js +9 -0
  5. package/build/cjs/errors/AudioStreamError.js.map +1 -1
  6. package/build/cjs/errors/AudioStreamError.test.js +22 -1
  7. package/build/cjs/errors/AudioStreamError.test.js.map +1 -1
  8. package/build/cjs/streamAudioData.js +99 -32
  9. package/build/cjs/streamAudioData.js.map +1 -1
  10. package/build/cjs/utils/audioProcessing.js +14 -10
  11. package/build/cjs/utils/audioProcessing.js.map +1 -1
  12. package/build/esm/errors/AudioStreamError.js +9 -0
  13. package/build/esm/errors/AudioStreamError.js.map +1 -1
  14. package/build/esm/errors/AudioStreamError.test.js +22 -1
  15. package/build/esm/errors/AudioStreamError.test.js.map +1 -1
  16. package/build/esm/streamAudioData.js +99 -32
  17. package/build/esm/streamAudioData.js.map +1 -1
  18. package/build/esm/utils/audioProcessing.js +14 -10
  19. package/build/esm/utils/audioProcessing.js.map +1 -1
  20. package/build/types/errors/AudioStreamError.d.ts.map +1 -1
  21. package/build/types/streamAudioData.d.ts +5 -0
  22. package/build/types/streamAudioData.d.ts.map +1 -1
  23. package/build/types/utils/audioProcessing.d.ts +2 -2
  24. package/build/types/utils/audioProcessing.d.ts.map +1 -1
  25. package/ios/AudioStreamDecoder.swift +191 -100
  26. package/ios/AudioStudioModule.swift +48 -9
  27. package/package.json +163 -146
  28. package/scripts/README.md +58 -0
  29. package/src/errors/AudioStreamError.test.ts +29 -2
  30. package/src/errors/AudioStreamError.ts +14 -0
  31. package/src/streamAudioData.ts +146 -42
  32. package/src/utils/audioProcessing.ts +25 -14
  33. package/android/src/androidTest/assets/chorus.wav +0 -0
  34. package/android/src/androidTest/assets/jfk.wav +0 -0
  35. package/android/src/androidTest/assets/osr_us_000_0010_8k.wav +0 -0
  36. package/android/src/androidTest/assets/recorder_hello_world.wav +0 -0
  37. package/android/src/androidTest/java/net/siteed/audiostudio/AudioFinalMetadataContractInstrumentedTest.kt +0 -190
  38. package/android/src/androidTest/java/net/siteed/audiostudio/AudioProcessorInstrumentedTest.kt +0 -197
  39. package/android/src/androidTest/java/net/siteed/audiostudio/AudioRecorderInstrumentedTest.kt +0 -487
  40. package/android/src/androidTest/java/net/siteed/audiostudio/AudioRecorderPerformanceInstrumentedTest.kt +0 -250
  41. package/android/src/androidTest/java/net/siteed/audiostudio/OpusRangeDecodeRegressionInstrumentedTest.kt +0 -186
  42. package/android/src/androidTest/java/net/siteed/audiostudio/integration/AudioFocusStrategyIntegrationTest.kt +0 -332
  43. package/android/src/androidTest/java/net/siteed/audiostudio/integration/BufferDurationIntegrationTest.kt +0 -324
  44. package/android/src/androidTest/java/net/siteed/audiostudio/integration/CompressedOnlyOutputTest.kt +0 -253
  45. package/android/src/androidTest/java/net/siteed/audiostudio/integration/DeviceDisconnectionFallbackTest.kt +0 -218
  46. package/android/src/androidTest/java/net/siteed/audiostudio/integration/EventEmissionIntervalTest.kt +0 -120
  47. package/android/src/androidTest/java/net/siteed/audiostudio/integration/M4aFormatTest.kt +0 -345
  48. package/android/src/androidTest/java/net/siteed/audiostudio/integration/OutputControlIntegrationTest.kt +0 -340
  49. package/android/src/androidTest/java/net/siteed/audiostudio/integration/PcmStreamingDurationTest.kt +0 -252
  50. package/android/src/androidTest/java/net/siteed/audiostudio/integration/README.md +0 -95
  51. package/android/src/androidTest/java/net/siteed/audiostudio/integration/run_integration_tests.sh +0 -43
  52. package/android/src/test/java/net/siteed/audiostudio/AndroidCallStateTest.kt +0 -37
  53. package/android/src/test/java/net/siteed/audiostudio/AndroidEventEmitterTest.kt +0 -28
  54. package/android/src/test/java/net/siteed/audiostudio/AudioFileHandlerTest.kt +0 -279
  55. package/android/src/test/java/net/siteed/audiostudio/AudioFocusStrategyTest.kt +0 -249
  56. package/android/src/test/java/net/siteed/audiostudio/AudioFormatTest.kt +0 -151
  57. package/android/src/test/java/net/siteed/audiostudio/AudioFormatUtilsTest.kt +0 -273
  58. package/android/src/test/java/net/siteed/audiostudio/DeviceDisconnectionFallbackUnitTest.kt +0 -140
  59. package/android/src/test/java/net/siteed/audiostudio/InterruptionAutoResumePolicyTest.kt +0 -49
  60. package/android/src/test/resources/chorus.wav +0 -0
  61. package/android/src/test/resources/generate_test_audio.py +0 -94
  62. package/android/src/test/resources/jfk.wav +0 -0
  63. package/android/src/test/resources/osr_us_000_0010_8k.wav +0 -0
  64. package/android/src/test/resources/recorder_hello_world.wav +0 -0
  65. package/ios/AudioStudioTests/AudioFileHandlerTests.swift +0 -338
  66. package/ios/AudioStudioTests/AudioFormatUtilsTests.swift +0 -331
  67. package/ios/AudioStudioTests/AudioStreamDecoderTests.swift +0 -128
  68. package/ios/AudioStudioTests/AudioTestHelpers.swift +0 -130
  69. package/ios/AudioStudioTests/CompressedOnlyOutputTests.swift +0 -334
  70. package/ios/AudioStudioTests/EventEmissionIntervalTests.swift +0 -105
  71. package/ios/AudioStudioTests/Info.plist +0 -22
  72. package/ios/AudioStudioTests/README.md +0 -39
  73. package/ios/AudioStudioTests/SimpleAudioTest.swift +0 -98
  74. package/ios/AudioStudioTests/TestAudioGenerator.swift +0 -75
  75. package/ios/tests/README.md +0 -41
  76. package/ios/tests/integration/buffer_and_fallback_test.swift +0 -178
  77. package/ios/tests/integration/buffer_duration_test.swift +0 -185
  78. package/ios/tests/integration/compressed_only_output_test.swift +0 -271
  79. package/ios/tests/integration/output_control_test.swift +0 -322
  80. package/ios/tests/integration/run_integration_tests.sh +0 -37
  81. package/ios/tests/opus_support_test_macos.swift +0 -154
  82. package/ios/tests/standalone/audio_processing_test.swift +0 -144
  83. package/ios/tests/standalone/audio_recording_test.swift +0 -277
  84. package/ios/tests/standalone/audio_streaming_test.swift +0 -249
  85. package/ios/tests/standalone/standalone_test.swift +0 -144
@@ -1,332 +0,0 @@
1
- package net.siteed.audiostudio.integration
2
-
3
- import android.media.AudioManager
4
- import android.content.Context
5
- import androidx.test.ext.junit.runners.AndroidJUnit4
6
- import androidx.test.platform.app.InstrumentationRegistry
7
- import org.junit.Test
8
- import org.junit.Assert.*
9
- import org.junit.Before
10
- import org.junit.After
11
- import org.junit.runner.RunWith
12
- import net.siteed.audiostudio.RecordingConfig
13
- import java.io.File
14
-
15
- /**
16
- * Integration tests for audio focus strategy functionality.
17
- * These tests run on actual Android devices/emulators to validate that
18
- * audio focus strategies work correctly in real scenarios.
19
- */
20
- @RunWith(AndroidJUnit4::class)
21
- class AudioFocusStrategyIntegrationTest {
22
-
23
- private lateinit var context: Context
24
- private lateinit var audioManager: AudioManager
25
- private lateinit var filesDir: File
26
-
27
- @Before
28
- fun setUp() {
29
- context = InstrumentationRegistry.getInstrumentation().targetContext
30
- audioManager = context.getSystemService(Context.AUDIO_SERVICE) as AudioManager
31
- filesDir = context.filesDir
32
- }
33
-
34
- @After
35
- fun tearDown() {
36
- // Clean up any test files
37
- val testFiles = filesDir.listFiles { _, name ->
38
- name.startsWith("test_audio_focus_")
39
- }
40
- testFiles?.forEach { it.delete() }
41
- }
42
-
43
- @Test
44
- fun testRecordingConfigWithBackgroundStrategy() {
45
- val options = mapOf(
46
- "sampleRate" to 44100,
47
- "channels" to 1,
48
- "encoding" to "pcm_16bit",
49
- "keepAwake" to true,
50
- "autoResumeAfterInterruption" to true,
51
- "android" to mapOf(
52
- "audioFocusStrategy" to "background"
53
- )
54
- )
55
-
56
- val result = RecordingConfig.fromMap(options)
57
- assertTrue("Config creation should succeed", result.isSuccess)
58
-
59
- val (config, audioFormat) = result.getOrThrow()
60
-
61
- // Verify audio focus strategy configuration
62
- assertEquals("Audio focus strategy should be background", "background", config.audioFocusStrategy)
63
- assertTrue("keepAwake should be true for background recording", config.keepAwake)
64
- assertTrue("autoResumeAfterInterruption should be true", config.autoResumeAfterInterruption)
65
-
66
- // Verify audio format is properly configured
67
- assertNotNull("Audio format should be created", audioFormat)
68
- assertEquals("MIME type should be audio/wav", "audio/wav", audioFormat.mimeType)
69
- }
70
-
71
- @Test
72
- fun testRecordingConfigWithInteractiveStrategy() {
73
- val options = mapOf(
74
- "sampleRate" to 44100,
75
- "channels" to 1,
76
- "encoding" to "pcm_16bit",
77
- "keepAwake" to false,
78
- "autoResumeAfterInterruption" to true,
79
- "android" to mapOf(
80
- "audioFocusStrategy" to "interactive"
81
- )
82
- )
83
-
84
- val result = RecordingConfig.fromMap(options)
85
- assertTrue("Config creation should succeed", result.isSuccess)
86
-
87
- val (config, audioFormat) = result.getOrThrow()
88
-
89
- // Verify audio focus strategy configuration
90
- assertEquals("Audio focus strategy should be interactive", "interactive", config.audioFocusStrategy)
91
- assertFalse("keepAwake should be false for interactive recording", config.keepAwake)
92
- assertTrue("autoResumeAfterInterruption should be true", config.autoResumeAfterInterruption)
93
-
94
- // Verify audio format is properly configured
95
- assertNotNull("Audio format should be created", audioFormat)
96
- assertEquals("MIME type should be audio/wav", "audio/wav", audioFormat.mimeType)
97
- }
98
-
99
- @Test
100
- fun testRecordingConfigWithCommunicationStrategy() {
101
- val options = mapOf(
102
- "sampleRate" to 16000, // Common speech sample rate
103
- "channels" to 1,
104
- "encoding" to "pcm_16bit",
105
- "keepAwake" to false,
106
- "autoResumeAfterInterruption" to true,
107
- "android" to mapOf(
108
- "audioFocusStrategy" to "communication"
109
- )
110
- )
111
-
112
- val result = RecordingConfig.fromMap(options)
113
- assertTrue("Config creation should succeed", result.isSuccess)
114
-
115
- val (config, audioFormat) = result.getOrThrow()
116
-
117
- // Verify audio focus strategy configuration
118
- assertEquals("Audio focus strategy should be communication", "communication", config.audioFocusStrategy)
119
- assertEquals("Sample rate should be 16000 for speech", 16000, config.sampleRate)
120
- assertFalse("keepAwake should be false", config.keepAwake)
121
- assertTrue("autoResumeAfterInterruption should be true", config.autoResumeAfterInterruption)
122
-
123
- // Verify audio format is properly configured
124
- assertNotNull("Audio format should be created", audioFormat)
125
- assertEquals("MIME type should be audio/wav", "audio/wav", audioFormat.mimeType)
126
- }
127
-
128
- @Test
129
- fun testRecordingConfigWithNoneStrategy() {
130
- val options = mapOf(
131
- "sampleRate" to 44100,
132
- "channels" to 1,
133
- "encoding" to "pcm_16bit",
134
- "keepAwake" to false,
135
- "android" to mapOf(
136
- "audioFocusStrategy" to "none"
137
- )
138
- )
139
-
140
- val result = RecordingConfig.fromMap(options)
141
- assertTrue("Config creation should succeed", result.isSuccess)
142
-
143
- val (config, audioFormat) = result.getOrThrow()
144
-
145
- // Verify audio focus strategy configuration
146
- assertEquals("Audio focus strategy should be none", "none", config.audioFocusStrategy)
147
-
148
- // Verify audio format is properly configured
149
- assertNotNull("Audio format should be created", audioFormat)
150
- assertEquals("MIME type should be audio/wav", "audio/wav", audioFormat.mimeType)
151
- }
152
-
153
- @Test
154
- fun testStrategyOverrideBehavior() {
155
- val options = mapOf(
156
- "sampleRate" to 44100,
157
- "channels" to 1,
158
- "encoding" to "pcm_16bit",
159
- "keepAwake" to true, // This would normally default to background
160
- "android" to mapOf(
161
- "audioFocusStrategy" to "communication" // But we override to communication
162
- )
163
- )
164
-
165
- val result = RecordingConfig.fromMap(options)
166
- assertTrue("Config creation should succeed", result.isSuccess)
167
-
168
- val (config, audioFormat) = result.getOrThrow()
169
-
170
- // Verify that explicit strategy overrides keepAwake defaults
171
- assertEquals("Audio focus strategy should be communication (overriding keepAwake default)", "communication", config.audioFocusStrategy)
172
- assertTrue("keepAwake should still be true", config.keepAwake)
173
-
174
- // Verify audio format is properly configured
175
- assertNotNull("Audio format should be created", audioFormat)
176
- }
177
-
178
- @Test
179
- fun testAudioFocusStrategyWithCompression() {
180
- val options = mapOf(
181
- "sampleRate" to 44100,
182
- "channels" to 1,
183
- "encoding" to "pcm_16bit",
184
- "android" to mapOf(
185
- "audioFocusStrategy" to "background"
186
- ),
187
- "output" to mapOf(
188
- "compressed" to mapOf(
189
- "enabled" to true,
190
- "format" to "aac",
191
- "bitrate" to 128000
192
- )
193
- )
194
- )
195
-
196
- val result = RecordingConfig.fromMap(options)
197
- assertTrue("Config creation should succeed", result.isSuccess)
198
-
199
- val (config, audioFormat) = result.getOrThrow()
200
-
201
- // Verify audio focus strategy works with compression
202
- assertEquals("Audio focus strategy should be background", "background", config.audioFocusStrategy)
203
- assertTrue("Compressed output should be enabled", config.output.compressed.enabled)
204
- assertEquals("Compression format should be aac", "aac", config.output.compressed.format)
205
- assertEquals("Bitrate should be 128000", 128000, config.output.compressed.bitrate)
206
-
207
- // Verify audio format is properly configured
208
- assertNotNull("Audio format should be created", audioFormat)
209
- assertEquals("MIME type should be audio/wav", "audio/wav", audioFormat.mimeType)
210
- }
211
-
212
- @Test
213
- fun testAudioFocusStrategyWithNotifications() {
214
- val options = mapOf(
215
- "sampleRate" to 44100,
216
- "channels" to 1,
217
- "encoding" to "pcm_16bit",
218
- "showNotification" to true,
219
- "showWaveformInNotification" to true,
220
- "android" to mapOf(
221
- "audioFocusStrategy" to "background"
222
- ),
223
- "notification" to mapOf(
224
- "title" to "Recording Audio",
225
- "text" to "Background recording in progress",
226
- "icon" to "ic_mic"
227
- )
228
- )
229
-
230
- val result = RecordingConfig.fromMap(options)
231
- assertTrue("Config creation should succeed", result.isSuccess)
232
-
233
- val (config, audioFormat) = result.getOrThrow()
234
-
235
- // Verify audio focus strategy works with notifications
236
- assertEquals("Audio focus strategy should be background", "background", config.audioFocusStrategy)
237
- assertTrue("Notifications should be enabled", config.showNotification)
238
- assertTrue("Waveform in notification should be enabled", config.showWaveformInNotification)
239
- assertEquals("Notification title should match", "Recording Audio", config.notification.title)
240
- assertEquals("Notification text should match", "Background recording in progress", config.notification.text)
241
- assertEquals("Notification icon should match", "ic_mic", config.notification.icon)
242
-
243
- // Verify audio format is properly configured
244
- assertNotNull("Audio format should be created", audioFormat)
245
- assertEquals("MIME type should be audio/wav", "audio/wav", audioFormat.mimeType)
246
- }
247
-
248
- @Test
249
- fun testAudioFocusStrategyValidation() {
250
- // Test all valid strategies
251
- val strategies = listOf("background", "interactive", "communication", "none")
252
-
253
- for (strategy in strategies) {
254
- val options = mapOf(
255
- "sampleRate" to 44100,
256
- "channels" to 1,
257
- "encoding" to "pcm_16bit",
258
- "android" to mapOf(
259
- "audioFocusStrategy" to strategy
260
- )
261
- )
262
-
263
- val result = RecordingConfig.fromMap(options)
264
- assertTrue("Config creation should succeed for strategy: $strategy", result.isSuccess)
265
-
266
- val (config, _) = result.getOrThrow()
267
- assertEquals("Audio focus strategy should be $strategy", strategy, config.audioFocusStrategy)
268
- }
269
- }
270
-
271
- @Test
272
- fun testInvalidAudioFocusStrategyHandling() {
273
- val options = mapOf(
274
- "sampleRate" to 44100,
275
- "channels" to 1,
276
- "encoding" to "pcm_16bit",
277
- "android" to mapOf(
278
- "audioFocusStrategy" to "invalid_strategy"
279
- )
280
- )
281
-
282
- val result = RecordingConfig.fromMap(options)
283
- assertTrue("Config creation should succeed even with invalid strategy", result.isSuccess)
284
-
285
- val (config, _) = result.getOrThrow()
286
- assertEquals("Invalid strategy should be preserved", "invalid_strategy", config.audioFocusStrategy)
287
- }
288
-
289
- @Test
290
- fun testCompleteAudioFocusConfiguration() {
291
- val options = mapOf(
292
- "sampleRate" to 44100,
293
- "channels" to 1,
294
- "encoding" to "pcm_16bit",
295
- "keepAwake" to true,
296
- "autoResumeAfterInterruption" to true,
297
- "showNotification" to true,
298
- "showWaveformInNotification" to false,
299
- "enableProcessing" to false,
300
- "android" to mapOf(
301
- "audioFocusStrategy" to "communication"
302
- ),
303
- "notification" to mapOf(
304
- "title" to "Voice Call Recording",
305
- "text" to "Call in progress",
306
- "icon" to "ic_call"
307
- )
308
- )
309
-
310
- val result = RecordingConfig.fromMap(options)
311
- assertTrue("Config creation should succeed", result.isSuccess)
312
-
313
- val (config, audioFormat) = result.getOrThrow()
314
-
315
- // Verify complete configuration
316
- assertEquals("Audio focus strategy should be communication", "communication", config.audioFocusStrategy)
317
- assertEquals("Sample rate should be 44100", 44100, config.sampleRate)
318
- assertEquals("Channels should be 1", 1, config.channels)
319
- assertEquals("Encoding should be pcm_16bit", "pcm_16bit", config.encoding)
320
- assertTrue("keepAwake should be true", config.keepAwake)
321
- assertFalse("showWaveformInNotification should be false", config.showWaveformInNotification)
322
- assertTrue("showNotification should be true", config.showNotification)
323
- assertTrue("autoResumeAfterInterruption should be true", config.autoResumeAfterInterruption)
324
- assertFalse("enableProcessing should be false", config.enableProcessing)
325
- assertEquals("Notification title should match", "Voice Call Recording", config.notification.title)
326
- assertEquals("Notification text should match", "Call in progress", config.notification.text)
327
-
328
- // Verify audio format is properly configured
329
- assertNotNull("Audio format should be created", audioFormat)
330
- assertEquals("MIME type should be audio/wav", "audio/wav", audioFormat.mimeType)
331
- }
332
- }
@@ -1,324 +0,0 @@
1
- package net.siteed.audiostudio.integration
2
-
3
- import android.media.AudioFormat
4
- import android.media.AudioRecord
5
- import android.media.MediaRecorder
6
- import android.os.Build
7
- import androidx.test.ext.junit.runners.AndroidJUnit4
8
- import androidx.test.platform.app.InstrumentationRegistry
9
- import org.junit.After
10
- import org.junit.Before
11
- import org.junit.Test
12
- import org.junit.runner.RunWith
13
- import java.util.concurrent.CountDownLatch
14
- import java.util.concurrent.TimeUnit
15
- import kotlin.math.abs
16
-
17
- /**
18
- * Integration test for Buffer Duration feature
19
- * This tests the ACTUAL behavior of Android AudioRecord with different buffer sizes
20
- */
21
- @RunWith(AndroidJUnit4::class)
22
- class BufferDurationIntegrationTest {
23
-
24
- private val context = InstrumentationRegistry.getInstrumentation().targetContext
25
- private val results = mutableListOf<TestResult>()
26
- private var audioRecord: AudioRecord? = null
27
-
28
- data class TestResult(
29
- val name: String,
30
- val passed: Boolean,
31
- val message: String
32
- )
33
-
34
- @Before
35
- fun setup() {
36
- println("🧪 Buffer Duration Integration Test")
37
- println("===================================\n")
38
- }
39
-
40
- @After
41
- fun tearDown() {
42
- audioRecord?.release()
43
- printResults()
44
- }
45
-
46
- @Test
47
- fun testDefaultBufferSize() {
48
- println("Test 1: Default Buffer Size")
49
- println("---------------------------")
50
-
51
- val sampleRate = 48000
52
- val channelConfig = AudioFormat.CHANNEL_IN_MONO
53
- val audioFormat = AudioFormat.ENCODING_PCM_16BIT
54
-
55
- // Get minimum buffer size
56
- val minBufferSize = AudioRecord.getMinBufferSize(sampleRate, channelConfig, audioFormat)
57
- println("✓ Android minimum buffer size: $minBufferSize bytes")
58
-
59
- // Calculate frames from bytes (16-bit = 2 bytes per sample)
60
- val minFrames = minBufferSize / 2
61
- println("✓ Minimum frames: $minFrames")
62
-
63
- // Test with default 1024 frames (2048 bytes)
64
- val requestedBytes = 1024 * 2
65
- val actualBufferSize = if (requestedBytes < minBufferSize) minBufferSize else requestedBytes
66
-
67
- audioRecord = AudioRecord(
68
- MediaRecorder.AudioSource.MIC,
69
- sampleRate,
70
- channelConfig,
71
- audioFormat,
72
- actualBufferSize
73
- )
74
-
75
- val state = audioRecord?.state
76
- val passed = state == AudioRecord.STATE_INITIALIZED
77
-
78
- results.add(TestResult(
79
- name = "Default Buffer Size",
80
- passed = passed,
81
- message = "Requested: 1024 frames, Min required: $minFrames frames, State: ${if (passed) "INITIALIZED" else "UNINITIALIZED"}"
82
- ))
83
-
84
- println("✓ Requested: 1024 frames (${requestedBytes} bytes)")
85
- println("✓ Actual buffer: ${actualBufferSize / 2} frames ($actualBufferSize bytes)")
86
- println("✓ Initialization: ${if (passed) "SUCCESS" else "FAILED"}\n")
87
- }
88
-
89
- @Test
90
- fun testCustomBufferSizes() {
91
- println("Test 2: Custom Buffer Sizes")
92
- println("---------------------------")
93
-
94
- val sampleRate = 48000
95
- val channelConfig = AudioFormat.CHANNEL_IN_MONO
96
- val audioFormat = AudioFormat.ENCODING_PCM_16BIT
97
- val minBufferSize = AudioRecord.getMinBufferSize(sampleRate, channelConfig, audioFormat)
98
-
99
- val testCases = listOf(
100
- 0.01 to "10ms",
101
- 0.05 to "50ms",
102
- 0.1 to "100ms",
103
- 0.2 to "200ms",
104
- 0.5 to "500ms"
105
- )
106
-
107
- for ((duration, name) in testCases) {
108
- val requestedFrames = (duration * sampleRate).toInt()
109
- val requestedBytes = requestedFrames * 2 // 16-bit = 2 bytes
110
- val actualBufferSize = if (requestedBytes < minBufferSize) minBufferSize else requestedBytes
111
-
112
- audioRecord?.release()
113
- audioRecord = AudioRecord(
114
- MediaRecorder.AudioSource.MIC,
115
- sampleRate,
116
- channelConfig,
117
- audioFormat,
118
- actualBufferSize
119
- )
120
-
121
- val state = audioRecord?.state
122
- val passed = state == AudioRecord.STATE_INITIALIZED
123
-
124
- // Test actual read behavior
125
- if (passed) {
126
- audioRecord?.startRecording()
127
- val buffer = ByteArray(requestedBytes)
128
- val bytesRead = audioRecord?.read(buffer, 0, buffer.size) ?: -1
129
- audioRecord?.stop()
130
-
131
- val framesRead = if (bytesRead > 0) bytesRead / 2 else 0
132
-
133
- results.add(TestResult(
134
- name = "Buffer $name",
135
- passed = bytesRead > 0,
136
- message = "Requested: $requestedFrames frames, Read: $framesRead frames"
137
- ))
138
-
139
- println(" $name: Requested $requestedFrames → Read $framesRead frames")
140
- } else {
141
- results.add(TestResult(
142
- name = "Buffer $name",
143
- passed = false,
144
- message = "Failed to initialize AudioRecord"
145
- ))
146
- println(" $name: Failed to initialize")
147
- }
148
- }
149
- println()
150
- }
151
-
152
- @Test
153
- fun testBufferSizeLimits() {
154
- println("Test 3: Buffer Size Limits")
155
- println("--------------------------")
156
-
157
- val sampleRate = 48000
158
- val channelConfig = AudioFormat.CHANNEL_IN_MONO
159
- val audioFormat = AudioFormat.ENCODING_PCM_16BIT
160
-
161
- val extremeCases = listOf(
162
- 100 to "Very small (100 frames)",
163
- 50000 to "Very large (50000 frames)"
164
- )
165
-
166
- for ((frames, name) in extremeCases) {
167
- val requestedBytes = frames * 2
168
- val minBufferSize = AudioRecord.getMinBufferSize(sampleRate, channelConfig, audioFormat)
169
- val actualBufferSize = if (requestedBytes < minBufferSize) minBufferSize else requestedBytes
170
-
171
- audioRecord?.release()
172
- audioRecord = AudioRecord(
173
- MediaRecorder.AudioSource.MIC,
174
- sampleRate,
175
- channelConfig,
176
- audioFormat,
177
- actualBufferSize
178
- )
179
-
180
- val state = audioRecord?.state
181
- val passed = state == AudioRecord.STATE_INITIALIZED
182
-
183
- results.add(TestResult(
184
- name = name,
185
- passed = passed,
186
- message = "Requested: $frames frames, Buffer size: ${actualBufferSize / 2} frames"
187
- ))
188
-
189
- println(" $name: $frames → ${actualBufferSize / 2} frames")
190
- }
191
- println()
192
- }
193
-
194
- @Test
195
- fun testBufferAccumulation() {
196
- println("Test 4: Buffer Accumulation for Small Durations")
197
- println("-----------------------------------------------")
198
-
199
- val sampleRate = 48000
200
- val channelConfig = AudioFormat.CHANNEL_IN_MONO
201
- val audioFormat = AudioFormat.ENCODING_PCM_16BIT
202
- val minBufferSize = AudioRecord.getMinBufferSize(sampleRate, channelConfig, audioFormat)
203
-
204
- // Test very small buffer (20ms = 960 frames)
205
- val targetDuration = 0.02 // 20ms
206
- val targetFrames = (targetDuration * sampleRate).toInt()
207
- val targetBytes = targetFrames * 2
208
-
209
- audioRecord?.release()
210
- audioRecord = AudioRecord(
211
- MediaRecorder.AudioSource.MIC,
212
- sampleRate,
213
- channelConfig,
214
- audioFormat,
215
- minBufferSize // Use minimum buffer size
216
- )
217
-
218
- if (audioRecord?.state == AudioRecord.STATE_INITIALIZED) {
219
- audioRecord?.startRecording()
220
-
221
- // Accumulate small chunks
222
- val accumulator = mutableListOf<ByteArray>()
223
- var totalFrames = 0
224
- val smallBuffer = ByteArray(targetBytes)
225
-
226
- // Read multiple times to accumulate
227
- repeat(5) {
228
- val bytesRead = audioRecord?.read(smallBuffer, 0, smallBuffer.size) ?: -1
229
- if (bytesRead > 0) {
230
- accumulator.add(smallBuffer.copyOf(bytesRead))
231
- totalFrames += bytesRead / 2
232
- }
233
- }
234
-
235
- audioRecord?.stop()
236
-
237
- val passed = totalFrames >= targetFrames
238
- results.add(TestResult(
239
- name = "Buffer Accumulation",
240
- passed = passed,
241
- message = "Target: $targetFrames frames, Accumulated: $totalFrames frames over ${accumulator.size} reads"
242
- ))
243
-
244
- println("✓ Target frames: $targetFrames")
245
- println("✓ Accumulated: $totalFrames frames")
246
- println("✓ Number of reads: ${accumulator.size}")
247
- } else {
248
- results.add(TestResult(
249
- name = "Buffer Accumulation",
250
- passed = false,
251
- message = "Failed to initialize AudioRecord"
252
- ))
253
- }
254
- println()
255
- }
256
-
257
- @Test
258
- fun testDifferentSampleRates() {
259
- println("Test 5: Different Sample Rates")
260
- println("------------------------------")
261
-
262
- val channelConfig = AudioFormat.CHANNEL_IN_MONO
263
- val audioFormat = AudioFormat.ENCODING_PCM_16BIT
264
- val bufferDuration = 0.1 // 100ms
265
-
266
- val sampleRates = listOf(16000, 44100, 48000)
267
-
268
- for (sampleRate in sampleRates) {
269
- val minBufferSize = AudioRecord.getMinBufferSize(sampleRate, channelConfig, audioFormat)
270
- val targetFrames = (bufferDuration * sampleRate).toInt()
271
- val targetBytes = targetFrames * 2
272
- val actualBufferSize = if (targetBytes < minBufferSize) minBufferSize else targetBytes
273
-
274
- audioRecord?.release()
275
- audioRecord = AudioRecord(
276
- MediaRecorder.AudioSource.MIC,
277
- sampleRate,
278
- channelConfig,
279
- audioFormat,
280
- actualBufferSize
281
- )
282
-
283
- val state = audioRecord?.state
284
- val passed = state == AudioRecord.STATE_INITIALIZED
285
-
286
- results.add(TestResult(
287
- name = "Sample Rate ${sampleRate}Hz",
288
- passed = passed,
289
- message = "Buffer duration: ${bufferDuration}s, Frames: ${actualBufferSize / 2}"
290
- ))
291
-
292
- println(" ${sampleRate}Hz: ${if (passed) "SUCCESS" else "FAILED"} - ${actualBufferSize / 2} frames")
293
- }
294
- println()
295
- }
296
-
297
- private fun printResults() {
298
- println("📊 Test Results")
299
- println("===============")
300
-
301
- val passed = results.count { it.passed }
302
- val total = results.size
303
-
304
- for (result in results) {
305
- val status = if (result.passed) "✅" else "❌"
306
- println("$status ${result.name}")
307
- println(" ${result.message}")
308
- }
309
-
310
- println("\nSummary: $passed/$total tests passed")
311
-
312
- if (passed == total) {
313
- println("🎉 All tests passed!")
314
- } else {
315
- println("⚠️ Some tests failed")
316
- }
317
-
318
- println("\n📝 Key Findings:")
319
- println("- Android enforces minimum buffer size via getMinBufferSize()")
320
- println("- Minimum varies by device and sample rate")
321
- println("- Small buffers require accumulation strategy")
322
- println("- AudioRecord handles buffer sizing more flexibly than iOS")
323
- }
324
- }