@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,250 +0,0 @@
|
|
|
1
|
-
package net.siteed.audiostudio
|
|
2
|
-
|
|
3
|
-
import android.Manifest
|
|
4
|
-
import android.content.Context
|
|
5
|
-
import android.util.Log
|
|
6
|
-
import androidx.test.ext.junit.runners.AndroidJUnit4
|
|
7
|
-
import androidx.test.platform.app.InstrumentationRegistry
|
|
8
|
-
import androidx.test.rule.GrantPermissionRule
|
|
9
|
-
import expo.modules.kotlin.Promise
|
|
10
|
-
import org.junit.After
|
|
11
|
-
import org.junit.Assert.*
|
|
12
|
-
import org.junit.Assume.assumeTrue
|
|
13
|
-
import org.junit.Before
|
|
14
|
-
import org.junit.Rule
|
|
15
|
-
import org.junit.Test
|
|
16
|
-
import org.junit.runner.RunWith
|
|
17
|
-
import java.io.File
|
|
18
|
-
import java.util.concurrent.CountDownLatch
|
|
19
|
-
import java.util.concurrent.TimeUnit
|
|
20
|
-
import kotlin.system.measureTimeMillis
|
|
21
|
-
|
|
22
|
-
/**
|
|
23
|
-
* Performance tests for measuring stop recording times.
|
|
24
|
-
*/
|
|
25
|
-
@RunWith(AndroidJUnit4::class)
|
|
26
|
-
class AudioRecorderPerformanceInstrumentedTest {
|
|
27
|
-
|
|
28
|
-
@get:Rule
|
|
29
|
-
val grantPermissionRule: GrantPermissionRule = GrantPermissionRule.grant(
|
|
30
|
-
Manifest.permission.RECORD_AUDIO
|
|
31
|
-
)
|
|
32
|
-
|
|
33
|
-
private lateinit var context: Context
|
|
34
|
-
private lateinit var filesDir: File
|
|
35
|
-
private lateinit var audioRecorderManager: AudioRecorderManager
|
|
36
|
-
private lateinit var testEventSender: TestEventSender
|
|
37
|
-
private lateinit var permissionUtils: PermissionUtils
|
|
38
|
-
private lateinit var audioDataEncoder: AudioDataEncoder
|
|
39
|
-
|
|
40
|
-
companion object {
|
|
41
|
-
private const val TAG = "PerformanceTest"
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
// Test event sender to capture events
|
|
45
|
-
private class TestEventSender : EventSender {
|
|
46
|
-
override fun sendExpoEvent(eventName: String, params: android.os.Bundle) {
|
|
47
|
-
// No-op for performance tests
|
|
48
|
-
}
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
@Before
|
|
52
|
-
fun setUp() {
|
|
53
|
-
context = InstrumentationRegistry.getInstrumentation().targetContext
|
|
54
|
-
filesDir = context.filesDir
|
|
55
|
-
testEventSender = TestEventSender()
|
|
56
|
-
permissionUtils = PermissionUtils(context)
|
|
57
|
-
audioDataEncoder = AudioDataEncoder()
|
|
58
|
-
|
|
59
|
-
// Initialize AudioRecorderManager
|
|
60
|
-
audioRecorderManager = AudioRecorderManager.initialize(
|
|
61
|
-
context = context,
|
|
62
|
-
filesDir = filesDir,
|
|
63
|
-
permissionUtils = permissionUtils,
|
|
64
|
-
audioDataEncoder = audioDataEncoder,
|
|
65
|
-
eventSender = testEventSender,
|
|
66
|
-
enablePhoneStateHandling = false,
|
|
67
|
-
enableBackgroundAudio = false
|
|
68
|
-
)
|
|
69
|
-
|
|
70
|
-
// Clean up any existing audio files
|
|
71
|
-
cleanupAudioFiles()
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
@After
|
|
75
|
-
fun tearDown() {
|
|
76
|
-
// Stop any ongoing recording
|
|
77
|
-
if (audioRecorderManager.isRecording) {
|
|
78
|
-
val promise = object : Promise {
|
|
79
|
-
override fun resolve(value: Any?) {}
|
|
80
|
-
override fun reject(code: String, message: String?, cause: Throwable?) {}
|
|
81
|
-
}
|
|
82
|
-
audioRecorderManager.stopRecording(promise)
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
// Clean up
|
|
86
|
-
AudioRecorderManager.destroy()
|
|
87
|
-
cleanupAudioFiles()
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
private fun cleanupAudioFiles() {
|
|
91
|
-
filesDir.listFiles()?.forEach { file ->
|
|
92
|
-
if (file.name.endsWith(".wav") || file.name.endsWith(".aac") || file.name.endsWith(".opus")) {
|
|
93
|
-
file.delete()
|
|
94
|
-
}
|
|
95
|
-
}
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
@Test
|
|
99
|
-
fun measureStopTime_5seconds() {
|
|
100
|
-
runPerformanceTest(5_000L, "5 second recording")
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
@Test
|
|
104
|
-
fun measureStopTime_30seconds() {
|
|
105
|
-
runPerformanceTest(30_000L, "30 second recording")
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
@Test
|
|
109
|
-
fun measureStopTime_1minute() {
|
|
110
|
-
runPerformanceTest(60_000L, "1 minute recording")
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
@Test
|
|
114
|
-
fun measureStopTime_2minutes() {
|
|
115
|
-
runPerformanceTest(120_000L, "2 minute recording")
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
@Test
|
|
119
|
-
fun measureStopTime_5minutes() {
|
|
120
|
-
assumeLongPerformanceTestsEnabled()
|
|
121
|
-
runPerformanceTest(300_000L, "5 minute recording")
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
@Test
|
|
125
|
-
fun measureStopTime_10minutes() {
|
|
126
|
-
assumeLongPerformanceTestsEnabled()
|
|
127
|
-
runPerformanceTest(600_000L, "10 minute recording")
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
@Test
|
|
131
|
-
fun measureStopTime_15minutes() {
|
|
132
|
-
assumeLongPerformanceTestsEnabled()
|
|
133
|
-
runPerformanceTest(900_000L, "15 minute recording")
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
private fun assumeLongPerformanceTestsEnabled() {
|
|
137
|
-
val enabled = InstrumentationRegistry.getArguments()
|
|
138
|
-
.getString("runLongPerformanceTests")
|
|
139
|
-
?.equals("true", ignoreCase = true) == true
|
|
140
|
-
|
|
141
|
-
assumeTrue(
|
|
142
|
-
"Long physical-device performance benchmarks are opt-in. " +
|
|
143
|
-
"Run with -Pandroid.testInstrumentationRunnerArguments.runLongPerformanceTests=true",
|
|
144
|
-
enabled
|
|
145
|
-
)
|
|
146
|
-
}
|
|
147
|
-
|
|
148
|
-
private fun runPerformanceTest(recordingDurationMs: Long, testName: String) {
|
|
149
|
-
val recordingOptions = mapOf(
|
|
150
|
-
"sampleRate" to 44100,
|
|
151
|
-
"channels" to 1,
|
|
152
|
-
"encoding" to "pcm_16bit",
|
|
153
|
-
"interval" to 1000,
|
|
154
|
-
"enableProcessing" to false,
|
|
155
|
-
"showNotification" to false,
|
|
156
|
-
"output" to mapOf(
|
|
157
|
-
"primary" to mapOf("enabled" to true),
|
|
158
|
-
"compressed" to mapOf("enabled" to false)
|
|
159
|
-
)
|
|
160
|
-
)
|
|
161
|
-
|
|
162
|
-
// Start recording
|
|
163
|
-
val startLatch = CountDownLatch(1)
|
|
164
|
-
audioRecorderManager.startRecording(recordingOptions, object : Promise {
|
|
165
|
-
override fun resolve(value: Any?) {
|
|
166
|
-
startLatch.countDown()
|
|
167
|
-
}
|
|
168
|
-
override fun reject(code: String, message: String?, cause: Throwable?) {
|
|
169
|
-
fail("Start recording failed: $message")
|
|
170
|
-
}
|
|
171
|
-
})
|
|
172
|
-
|
|
173
|
-
assertTrue("Recording should start", startLatch.await(5, TimeUnit.SECONDS))
|
|
174
|
-
assertTrue("Recording should be active", audioRecorderManager.isRecording)
|
|
175
|
-
|
|
176
|
-
// Record for specified duration
|
|
177
|
-
Thread.sleep(recordingDurationMs)
|
|
178
|
-
|
|
179
|
-
// Measure stop time
|
|
180
|
-
val stopLatch = CountDownLatch(1)
|
|
181
|
-
var fileSize = 0L
|
|
182
|
-
var stopResult: Map<String, Any>? = null
|
|
183
|
-
|
|
184
|
-
val stopDuration = measureTimeMillis {
|
|
185
|
-
audioRecorderManager.stopRecording(object : Promise {
|
|
186
|
-
override fun resolve(value: Any?) {
|
|
187
|
-
when (value) {
|
|
188
|
-
is android.os.Bundle -> {
|
|
189
|
-
fileSize = value.getLong("size", 0)
|
|
190
|
-
stopResult = bundleToMap(value)
|
|
191
|
-
}
|
|
192
|
-
is Map<*, *> -> {
|
|
193
|
-
@Suppress("UNCHECKED_CAST")
|
|
194
|
-
stopResult = value as? Map<String, Any>
|
|
195
|
-
fileSize = (stopResult?.get("size") as? Long) ?: 0
|
|
196
|
-
}
|
|
197
|
-
}
|
|
198
|
-
stopLatch.countDown()
|
|
199
|
-
}
|
|
200
|
-
override fun reject(code: String, message: String?, cause: Throwable?) {
|
|
201
|
-
fail("Stop recording failed: $message")
|
|
202
|
-
}
|
|
203
|
-
})
|
|
204
|
-
|
|
205
|
-
assertTrue("Stop should complete", stopLatch.await(10, TimeUnit.SECONDS))
|
|
206
|
-
}
|
|
207
|
-
|
|
208
|
-
// Log results
|
|
209
|
-
val fileSizeMB = fileSize / (1024.0 * 1024.0)
|
|
210
|
-
Log.i(TAG, """
|
|
211
|
-
Performance Test: $testName
|
|
212
|
-
- Recording Duration: ${recordingDurationMs}ms
|
|
213
|
-
- Stop Duration: ${stopDuration}ms
|
|
214
|
-
- File Size: ${"%.2f".format(fileSizeMB)}MB
|
|
215
|
-
- Performance: ${if (stopDuration < getTargetTime(recordingDurationMs)) "PASS" else "FAIL"}
|
|
216
|
-
""".trimIndent())
|
|
217
|
-
|
|
218
|
-
println("""
|
|
219
|
-
Performance Test: $testName
|
|
220
|
-
- Recording Duration: ${recordingDurationMs}ms
|
|
221
|
-
- Stop Duration: ${stopDuration}ms
|
|
222
|
-
- File Size: ${"%.2f".format(fileSizeMB)}MB
|
|
223
|
-
- Performance: ${if (stopDuration < getTargetTime(recordingDurationMs)) "PASS" else "FAIL"}
|
|
224
|
-
""".trimIndent())
|
|
225
|
-
|
|
226
|
-
assertFalse("Recording should not be active", audioRecorderManager.isRecording)
|
|
227
|
-
}
|
|
228
|
-
|
|
229
|
-
private fun getTargetTime(recordingDurationMs: Long): Long {
|
|
230
|
-
return when {
|
|
231
|
-
recordingDurationMs <= 5_000 -> 100
|
|
232
|
-
recordingDurationMs <= 30_000 -> 150
|
|
233
|
-
recordingDurationMs <= 60_000 -> 200
|
|
234
|
-
recordingDurationMs <= 300_000 -> 500
|
|
235
|
-
else -> 750
|
|
236
|
-
}
|
|
237
|
-
}
|
|
238
|
-
|
|
239
|
-
private fun bundleToMap(bundle: android.os.Bundle): Map<String, Any> {
|
|
240
|
-
val map = mutableMapOf<String, Any>()
|
|
241
|
-
for (key in bundle.keySet()) {
|
|
242
|
-
val value = bundle.get(key)
|
|
243
|
-
when (value) {
|
|
244
|
-
is android.os.Bundle -> map[key] = bundleToMap(value)
|
|
245
|
-
else -> value?.let { map[key] = it }
|
|
246
|
-
}
|
|
247
|
-
}
|
|
248
|
-
return map
|
|
249
|
-
}
|
|
250
|
-
}
|
|
@@ -1,186 +0,0 @@
|
|
|
1
|
-
package net.siteed.audiostudio
|
|
2
|
-
|
|
3
|
-
import android.Manifest
|
|
4
|
-
import android.os.Build
|
|
5
|
-
import androidx.test.ext.junit.runners.AndroidJUnit4
|
|
6
|
-
import androidx.test.platform.app.InstrumentationRegistry
|
|
7
|
-
import androidx.test.rule.GrantPermissionRule
|
|
8
|
-
import expo.modules.kotlin.Promise
|
|
9
|
-
import org.junit.After
|
|
10
|
-
import org.junit.Assert.assertEquals
|
|
11
|
-
import org.junit.Assert.assertTrue
|
|
12
|
-
import org.junit.Assume.assumeTrue
|
|
13
|
-
import org.junit.Before
|
|
14
|
-
import org.junit.Rule
|
|
15
|
-
import org.junit.Test
|
|
16
|
-
import org.junit.runner.RunWith
|
|
17
|
-
import java.io.File
|
|
18
|
-
import java.net.URI
|
|
19
|
-
import java.util.concurrent.CountDownLatch
|
|
20
|
-
import java.util.concurrent.TimeUnit
|
|
21
|
-
import kotlin.math.abs
|
|
22
|
-
|
|
23
|
-
/**
|
|
24
|
-
* Device-only regression skeleton for compressed Opus range decode. It records a
|
|
25
|
-
* short Opus-only file, then verifies the range loader reports metadata and byte
|
|
26
|
-
* length from final target-converted PCM rather than source decoder output.
|
|
27
|
-
*/
|
|
28
|
-
@RunWith(AndroidJUnit4::class)
|
|
29
|
-
class OpusRangeDecodeRegressionInstrumentedTest {
|
|
30
|
-
@get:Rule
|
|
31
|
-
val grantPermissionRule: GrantPermissionRule = GrantPermissionRule.grant(Manifest.permission.RECORD_AUDIO)
|
|
32
|
-
|
|
33
|
-
private val context = InstrumentationRegistry.getInstrumentation().targetContext
|
|
34
|
-
private val filesDir = context.filesDir
|
|
35
|
-
private lateinit var audioRecorderManager: AudioRecorderManager
|
|
36
|
-
|
|
37
|
-
@Before
|
|
38
|
-
fun setUp() {
|
|
39
|
-
assumeTrue("Opus MediaCodec recording requires Android Q+", Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q)
|
|
40
|
-
audioRecorderManager = AudioRecorderManager.initialize(
|
|
41
|
-
context = context,
|
|
42
|
-
filesDir = filesDir,
|
|
43
|
-
permissionUtils = PermissionUtils(context),
|
|
44
|
-
audioDataEncoder = AudioDataEncoder(),
|
|
45
|
-
eventSender = object : EventSender {
|
|
46
|
-
override fun sendExpoEvent(eventName: String, params: android.os.Bundle) = Unit
|
|
47
|
-
},
|
|
48
|
-
enablePhoneStateHandling = false,
|
|
49
|
-
enableBackgroundAudio = false
|
|
50
|
-
)
|
|
51
|
-
cleanupGeneratedAudio()
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
@After
|
|
55
|
-
fun tearDown() {
|
|
56
|
-
if (::audioRecorderManager.isInitialized && audioRecorderManager.isRecording) {
|
|
57
|
-
runCatching { stopRecordingSync() }
|
|
58
|
-
}
|
|
59
|
-
AudioRecorderManager.destroy()
|
|
60
|
-
cleanupGeneratedAudio()
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
@Test
|
|
64
|
-
fun loadAudioRange_returnsTargetPcmMetadataForGeneratedOpusRecording() {
|
|
65
|
-
startRecordingSync(
|
|
66
|
-
mapOf(
|
|
67
|
-
"sampleRate" to 48_000,
|
|
68
|
-
"channels" to 1,
|
|
69
|
-
"encoding" to "pcm_16bit",
|
|
70
|
-
"interval" to 100,
|
|
71
|
-
"showNotification" to false,
|
|
72
|
-
"output" to mapOf(
|
|
73
|
-
"primary" to mapOf("enabled" to false),
|
|
74
|
-
"compressed" to mapOf(
|
|
75
|
-
"enabled" to true,
|
|
76
|
-
"format" to "opus"
|
|
77
|
-
)
|
|
78
|
-
)
|
|
79
|
-
)
|
|
80
|
-
)
|
|
81
|
-
Thread.sleep(RECORDING_DURATION_MS + 250L)
|
|
82
|
-
val opusFile = resolveCompressedFile(stopRecordingSync())
|
|
83
|
-
|
|
84
|
-
val audioData = AudioProcessor(filesDir).loadAudioRange(
|
|
85
|
-
fileUri = opusFile.absolutePath,
|
|
86
|
-
startTimeMs = 0,
|
|
87
|
-
endTimeMs = REQUESTED_RANGE_MS,
|
|
88
|
-
config = DecodingConfig(
|
|
89
|
-
targetSampleRate = TARGET_SAMPLE_RATE,
|
|
90
|
-
targetChannels = TARGET_CHANNELS,
|
|
91
|
-
targetBitDepth = TARGET_BIT_DEPTH,
|
|
92
|
-
normalizeAudio = false
|
|
93
|
-
)
|
|
94
|
-
)
|
|
95
|
-
|
|
96
|
-
val decoded = requireNotNull(audioData) { "Opus range should decode without BufferOverflowException" }
|
|
97
|
-
assertEquals("sampleRate should describe final converted PCM", TARGET_SAMPLE_RATE, decoded.sampleRate)
|
|
98
|
-
assertEquals("channels should describe final converted PCM", TARGET_CHANNELS, decoded.channels)
|
|
99
|
-
assertEquals("bitDepth should describe final converted PCM", TARGET_BIT_DEPTH, decoded.bitDepth)
|
|
100
|
-
|
|
101
|
-
val expectedBytes = TARGET_SAMPLE_RATE * TARGET_CHANNELS * (TARGET_BIT_DEPTH / 8) * REQUESTED_RANGE_MS / 1_000
|
|
102
|
-
val toleranceBytes = TARGET_SAMPLE_RATE * TARGET_CHANNELS * (TARGET_BIT_DEPTH / 8) / 2
|
|
103
|
-
assertTrue(
|
|
104
|
-
"final PCM byte length should be near requested target duration: expected=$expectedBytes actual=${decoded.data.size}",
|
|
105
|
-
abs(decoded.data.size - expectedBytes) <= toleranceBytes
|
|
106
|
-
)
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
private fun startRecordingSync(recordingOptions: Map<String, Any?>) {
|
|
110
|
-
val latch = CountDownLatch(1)
|
|
111
|
-
var rejected: String? = null
|
|
112
|
-
audioRecorderManager.startRecording(recordingOptions, object : Promise {
|
|
113
|
-
override fun resolve(value: Any?) {
|
|
114
|
-
latch.countDown()
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
override fun reject(code: String, message: String?, cause: Throwable?) {
|
|
118
|
-
rejected = "$code - $message"
|
|
119
|
-
latch.countDown()
|
|
120
|
-
}
|
|
121
|
-
})
|
|
122
|
-
assertTrue("Recording should start within 2 seconds", latch.await(2, TimeUnit.SECONDS))
|
|
123
|
-
rejected?.let { throw AssertionError("Recording start failed: $it") }
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
private fun stopRecordingSync(): Map<String, Any?> {
|
|
127
|
-
val latch = CountDownLatch(1)
|
|
128
|
-
var result: Map<String, Any?>? = null
|
|
129
|
-
var rejected: String? = null
|
|
130
|
-
audioRecorderManager.stopRecording(object : Promise {
|
|
131
|
-
override fun resolve(value: Any?) {
|
|
132
|
-
result = when (value) {
|
|
133
|
-
is android.os.Bundle -> bundleToMap(value)
|
|
134
|
-
is Map<*, *> -> value.entries.associate { it.key.toString() to it.value }
|
|
135
|
-
else -> throw AssertionError("Unexpected stop result type: ${value?.javaClass?.name}")
|
|
136
|
-
}
|
|
137
|
-
latch.countDown()
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
override fun reject(code: String, message: String?, cause: Throwable?) {
|
|
141
|
-
rejected = "$code - $message"
|
|
142
|
-
latch.countDown()
|
|
143
|
-
}
|
|
144
|
-
})
|
|
145
|
-
assertTrue("Recording should stop within 2 seconds", latch.await(2, TimeUnit.SECONDS))
|
|
146
|
-
rejected?.let { throw AssertionError("Recording stop failed: $it") }
|
|
147
|
-
return result ?: throw AssertionError("Stop result should not be null")
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
private fun resolveCompressedFile(result: Map<String, Any?>): File {
|
|
151
|
-
val compression = result["compression"]
|
|
152
|
-
val compressionMap = when (compression) {
|
|
153
|
-
is android.os.Bundle -> bundleToMap(compression)
|
|
154
|
-
is Map<*, *> -> compression.entries.associate { it.key.toString() to it.value }
|
|
155
|
-
else -> emptyMap()
|
|
156
|
-
}
|
|
157
|
-
val uri = compressionMap["compressedFileUri"] as? String
|
|
158
|
-
?: throw AssertionError("Compressed Opus URI should be present in stop result: $result")
|
|
159
|
-
val file = when {
|
|
160
|
-
uri.startsWith("file://") || uri.startsWith("file:") -> File(URI(uri))
|
|
161
|
-
else -> File(uri)
|
|
162
|
-
}
|
|
163
|
-
assertTrue("Generated Opus file should exist: ${file.absolutePath}", file.exists())
|
|
164
|
-
assertTrue("Generated file should be Opus: ${file.name}", file.name.endsWith(".opus"))
|
|
165
|
-
return file
|
|
166
|
-
}
|
|
167
|
-
|
|
168
|
-
private fun bundleToMap(bundle: android.os.Bundle): Map<String, Any?> =
|
|
169
|
-
bundle.keySet().associateWith { key -> bundle.get(key) }
|
|
170
|
-
|
|
171
|
-
private fun cleanupGeneratedAudio() {
|
|
172
|
-
filesDir.listFiles()?.forEach { file ->
|
|
173
|
-
if (file.name.startsWith("recording_") && file.extension in setOf("opus", "wav", "m4a", "aac")) {
|
|
174
|
-
file.delete()
|
|
175
|
-
}
|
|
176
|
-
}
|
|
177
|
-
}
|
|
178
|
-
|
|
179
|
-
companion object {
|
|
180
|
-
private const val RECORDING_DURATION_MS = 2_000L
|
|
181
|
-
private const val REQUESTED_RANGE_MS = 2_000L
|
|
182
|
-
private const val TARGET_SAMPLE_RATE = 16_000
|
|
183
|
-
private const val TARGET_CHANNELS = 1
|
|
184
|
-
private const val TARGET_BIT_DEPTH = 16
|
|
185
|
-
}
|
|
186
|
-
}
|