@siteed/expo-audio-studio 2.9.0 → 2.10.1
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 +13 -1
- package/android/build.gradle +9 -0
- 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/audiostream/AudioProcessorInstrumentedTest.kt +197 -0
- package/android/src/androidTest/java/net/siteed/audiostream/AudioRecorderInstrumentedTest.kt +541 -0
- package/android/src/androidTest/java/net/siteed/audiostream/integration/BufferDurationIntegrationTest.kt +324 -0
- package/android/src/androidTest/java/net/siteed/audiostream/integration/OutputControlIntegrationTest.kt +340 -0
- package/android/src/androidTest/java/net/siteed/audiostream/integration/README.md +95 -0
- package/android/src/androidTest/java/net/siteed/audiostream/integration/run_integration_tests.sh +28 -0
- package/android/src/main/java/net/siteed/audiostream/AudioFormatUtils.kt +264 -13
- package/android/src/main/java/net/siteed/audiostream/AudioProcessor.kt +3 -13
- package/android/src/main/java/net/siteed/audiostream/AudioRecorderManager.kt +118 -55
- package/android/src/main/java/net/siteed/audiostream/LogUtils.kt +32 -4
- package/android/src/main/java/net/siteed/audiostream/RecordingConfig.kt +50 -15
- package/android/src/test/java/net/siteed/audiostream/AudioFileHandlerTest.kt +279 -0
- package/android/src/test/java/net/siteed/audiostream/AudioFormatUtilsTest.kt +273 -0
- package/android/src/test/resources/chorus.wav +0 -0
- package/android/src/test/resources/generate_test_audio.py +94 -0
- 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/build/cjs/ExpoAudioStream.types.js.map +1 -1
- package/build/cjs/ExpoAudioStream.web.js +37 -34
- package/build/cjs/ExpoAudioStream.web.js.map +1 -1
- package/build/cjs/WebRecorder.web.js +12 -10
- package/build/cjs/WebRecorder.web.js.map +1 -1
- package/build/cjs/useAudioRecorder.js.map +1 -1
- package/build/esm/ExpoAudioStream.types.js.map +1 -1
- package/build/esm/ExpoAudioStream.web.js +37 -34
- package/build/esm/ExpoAudioStream.web.js.map +1 -1
- package/build/esm/WebRecorder.web.js +12 -10
- package/build/esm/WebRecorder.web.js.map +1 -1
- package/build/esm/useAudioRecorder.js.map +1 -1
- package/build/types/ExpoAudioStream.types.d.ts +54 -22
- package/build/types/ExpoAudioStream.types.d.ts.map +1 -1
- package/build/types/ExpoAudioStream.web.d.ts.map +1 -1
- package/build/types/WebRecorder.web.d.ts.map +1 -1
- package/ios/AudioNotificationManager.swift +2 -6
- package/ios/AudioStreamManager.swift +116 -50
- package/ios/ExpoAudioStream.podspec +6 -0
- package/ios/ExpoAudioStreamModule.swift +11 -8
- package/ios/ExpoAudioStudioTests/AudioFileHandlerTests.swift +338 -0
- package/ios/ExpoAudioStudioTests/AudioFormatUtilsTests.swift +331 -0
- package/ios/ExpoAudioStudioTests/AudioTestHelpers.swift +130 -0
- package/ios/ExpoAudioStudioTests/Info.plist +22 -0
- package/ios/ExpoAudioStudioTests/SimpleAudioTest.swift +98 -0
- package/ios/ExpoAudioStudioTests/TestAudioGenerator.swift +75 -0
- package/ios/RecordingSettings.swift +53 -22
- package/ios/tests/integration/buffer_duration_test.swift +185 -0
- package/ios/tests/integration/output_control_test.swift +322 -0
- package/ios/tests/integration/run_integration_tests.sh +27 -0
- package/ios/tests/standalone/audio_processing_test.swift +144 -0
- package/ios/tests/standalone/audio_recording_test.swift +277 -0
- package/ios/tests/standalone/audio_streaming_test.swift +249 -0
- package/ios/tests/standalone/standalone_test.swift +144 -0
- package/package.json +140 -133
- package/src/ExpoAudioStream.types.ts +66 -22
- package/src/ExpoAudioStream.web.ts +43 -38
- package/src/WebRecorder.web.ts +13 -10
- package/src/useAudioRecorder.tsx +1 -1
- package/android/src/main/test/java/net/siteed/audiostream/AudioProcessorTest.kt +0 -56
- package/ios/siteedexpoaudiostudio.xcodeproj/project.xcworkspace/contents.xcworkspacedata +0 -7
- package/ios/siteedexpoaudiostudio.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist +0 -8
- /package/plugin/build/{index.d.ts → index.d.cts} +0 -0
|
@@ -552,8 +552,8 @@ class AudioRecorderManager(
|
|
|
552
552
|
|
|
553
553
|
if (!initializeAudioRecord(promise)) return
|
|
554
554
|
|
|
555
|
-
if (recordingConfig.
|
|
556
|
-
if (recordingConfig.
|
|
555
|
+
if (recordingConfig.output.compressed.enabled && !initializeCompressedRecorder(
|
|
556
|
+
if (recordingConfig.output.compressed.format == "aac") "aac" else "opus",
|
|
557
557
|
promise
|
|
558
558
|
)) return
|
|
559
559
|
|
|
@@ -593,9 +593,9 @@ class AudioRecorderManager(
|
|
|
593
593
|
"sampleRate" to recordingConfig.sampleRate,
|
|
594
594
|
"mimeType" to mimeType,
|
|
595
595
|
"compression" to if (compressedFile != null) bundleOf(
|
|
596
|
-
"mimeType" to if (recordingConfig.
|
|
597
|
-
"bitrate" to recordingConfig.
|
|
598
|
-
"format" to recordingConfig.
|
|
596
|
+
"mimeType" to if (recordingConfig.output.compressed.format == "aac") "audio/aac" else "audio/opus",
|
|
597
|
+
"bitrate" to recordingConfig.output.compressed.bitrate,
|
|
598
|
+
"format" to recordingConfig.output.compressed.format,
|
|
599
599
|
"size" to 0,
|
|
600
600
|
"compressedFileUri" to compressedFile?.toURI().toString()
|
|
601
601
|
) else null
|
|
@@ -712,11 +712,25 @@ class AudioRecorderManager(
|
|
|
712
712
|
AudioFormat.CHANNEL_IN_STEREO
|
|
713
713
|
}
|
|
714
714
|
|
|
715
|
-
|
|
715
|
+
val minBufferSize = AudioRecord.getMinBufferSize(
|
|
716
716
|
recordingConfig.sampleRate,
|
|
717
717
|
channelConfig,
|
|
718
718
|
audioFormat
|
|
719
719
|
)
|
|
720
|
+
|
|
721
|
+
// Calculate buffer size based on bufferDurationSeconds if provided
|
|
722
|
+
bufferSizeInBytes = recordingConfig.bufferDurationSeconds?.let { bufferDuration ->
|
|
723
|
+
val bytesPerSample = when (recordingConfig.encoding) {
|
|
724
|
+
"pcm_8bit" -> 1
|
|
725
|
+
"pcm_16bit" -> 2
|
|
726
|
+
"pcm_32bit" -> 4
|
|
727
|
+
else -> 2
|
|
728
|
+
}
|
|
729
|
+
val requestedSize = (bufferDuration * recordingConfig.sampleRate *
|
|
730
|
+
bytesPerSample * recordingConfig.channels).toInt()
|
|
731
|
+
// Use the larger of requested size or minimum buffer size
|
|
732
|
+
maxOf(requestedSize, minBufferSize)
|
|
733
|
+
} ?: minBufferSize
|
|
720
734
|
|
|
721
735
|
when {
|
|
722
736
|
bufferSizeInBytes == AudioRecord.ERROR -> {
|
|
@@ -818,16 +832,24 @@ class AudioRecorderManager(
|
|
|
818
832
|
private fun initializeRecordingResources(fileExtension: String, promise: Promise): Boolean {
|
|
819
833
|
try {
|
|
820
834
|
streamUuid = java.util.UUID.randomUUID().toString()
|
|
821
|
-
audioFile = createRecordingFile(recordingConfig)
|
|
822
835
|
totalDataSize = 0
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
836
|
+
|
|
837
|
+
// Only create file if primary output is enabled
|
|
838
|
+
if (recordingConfig.output.primary.enabled) {
|
|
839
|
+
audioFile = createRecordingFile(recordingConfig)
|
|
840
|
+
|
|
841
|
+
FileOutputStream(audioFile, true).use { fos ->
|
|
842
|
+
audioFileHandler.writeWavHeader(
|
|
843
|
+
fos,
|
|
844
|
+
recordingConfig.sampleRate,
|
|
845
|
+
recordingConfig.channels,
|
|
846
|
+
AudioFormatUtils.getBitDepth(recordingConfig.encoding)
|
|
847
|
+
)
|
|
848
|
+
}
|
|
849
|
+
} else {
|
|
850
|
+
// Set audioFile to null when primary output is disabled
|
|
851
|
+
audioFile = null
|
|
852
|
+
LogUtils.d(CLASS_NAME, "Skipping primary file creation - primary output is disabled")
|
|
831
853
|
}
|
|
832
854
|
|
|
833
855
|
if (recordingConfig.showNotification) {
|
|
@@ -860,16 +882,18 @@ class AudioRecorderManager(
|
|
|
860
882
|
- Sample Rate: ${recordingConfig.sampleRate} Hz
|
|
861
883
|
- Channels: ${recordingConfig.channels}
|
|
862
884
|
- Encoding: ${recordingConfig.encoding}
|
|
885
|
+
- Buffer Duration: ${recordingConfig.bufferDurationSeconds?.let { "${it}s" } ?: "default"}
|
|
886
|
+
- Primary Output: ${recordingConfig.output.primary.enabled}
|
|
863
887
|
- Data Emission Interval: ${recordingConfig.interval}ms
|
|
864
888
|
- Analysis Interval: ${recordingConfig.intervalAnalysis}ms
|
|
865
889
|
- Processing Enabled: ${recordingConfig.enableProcessing}
|
|
866
890
|
- Keep Awake: ${recordingConfig.keepAwake}
|
|
867
891
|
- Show Notification: ${recordingConfig.showNotification}
|
|
868
892
|
- Show Waveform: ${recordingConfig.showWaveformInNotification}
|
|
869
|
-
- Compressed Output: ${recordingConfig.
|
|
870
|
-
${if (recordingConfig.
|
|
871
|
-
- Compressed Format: ${recordingConfig.
|
|
872
|
-
- Compressed Bitrate: ${recordingConfig.
|
|
893
|
+
- Compressed Output: ${recordingConfig.output.compressed.enabled}
|
|
894
|
+
${if (recordingConfig.output.compressed.enabled) """
|
|
895
|
+
- Compressed Format: ${recordingConfig.output.compressed.format}
|
|
896
|
+
- Compressed Bitrate: ${recordingConfig.output.compressed.bitrate}
|
|
873
897
|
""".trimIndent() else ""}
|
|
874
898
|
- Auto Resume: ${recordingConfig.autoResumeAfterInterruption}
|
|
875
899
|
- Output Directory: ${recordingConfig.outputDirectory ?: "default"}
|
|
@@ -974,29 +998,45 @@ class AudioRecorderManager(
|
|
|
974
998
|
compressedRecorder = null
|
|
975
999
|
|
|
976
1000
|
// Log compressed file status if enabled
|
|
977
|
-
if (recordingConfig.
|
|
1001
|
+
if (recordingConfig.output.compressed.enabled) {
|
|
978
1002
|
val compressedSize = compressedFile?.length() ?: 0
|
|
979
1003
|
LogUtils.d(CLASS_NAME, "Compressed File validation - Size: $compressedSize bytes, Path: ${compressedFile?.absolutePath}")
|
|
980
1004
|
}
|
|
981
1005
|
|
|
982
|
-
val result =
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
"
|
|
994
|
-
"
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1006
|
+
val result = if (!recordingConfig.output.primary.enabled) {
|
|
1007
|
+
// When primary output is disabled, return minimal info
|
|
1008
|
+
bundleOf(
|
|
1009
|
+
"fileUri" to "",
|
|
1010
|
+
"filename" to "stream-only",
|
|
1011
|
+
"durationMs" to duration,
|
|
1012
|
+
"channels" to recordingConfig.channels,
|
|
1013
|
+
"bitDepth" to AudioFormatUtils.getBitDepth(recordingConfig.encoding),
|
|
1014
|
+
"sampleRate" to recordingConfig.sampleRate,
|
|
1015
|
+
"size" to totalDataSize,
|
|
1016
|
+
"mimeType" to mimeType,
|
|
1017
|
+
"createdAt" to System.currentTimeMillis(),
|
|
1018
|
+
"compression" to null
|
|
1019
|
+
)
|
|
1020
|
+
} else {
|
|
1021
|
+
bundleOf(
|
|
1022
|
+
"fileUri" to audioFile?.toURI().toString(),
|
|
1023
|
+
"filename" to audioFile?.name,
|
|
1024
|
+
"durationMs" to duration,
|
|
1025
|
+
"channels" to recordingConfig.channels,
|
|
1026
|
+
"bitDepth" to AudioFormatUtils.getBitDepth(recordingConfig.encoding),
|
|
1027
|
+
"sampleRate" to recordingConfig.sampleRate,
|
|
1028
|
+
"size" to fileSize,
|
|
1029
|
+
"mimeType" to mimeType,
|
|
1030
|
+
"createdAt" to System.currentTimeMillis(),
|
|
1031
|
+
"compression" to if (compressedFile != null) bundleOf(
|
|
1032
|
+
"size" to compressedFile?.length(),
|
|
1033
|
+
"mimeType" to if (recordingConfig.output.compressed.format == "aac") "audio/aac" else "audio/opus",
|
|
1034
|
+
"bitrate" to recordingConfig.output.compressed.bitrate,
|
|
1035
|
+
"format" to recordingConfig.output.compressed.format,
|
|
1036
|
+
"compressedFileUri" to compressedFile?.toURI().toString()
|
|
1037
|
+
) else null
|
|
1038
|
+
)
|
|
1039
|
+
}
|
|
1000
1040
|
promise.resolve(result)
|
|
1001
1041
|
|
|
1002
1042
|
// Reset the timing variables
|
|
@@ -1155,12 +1195,12 @@ class AudioRecorderManager(
|
|
|
1155
1195
|
else -> totalRecordedTime
|
|
1156
1196
|
}
|
|
1157
1197
|
|
|
1158
|
-
val compressionBundle = if (recordingConfig.
|
|
1198
|
+
val compressionBundle = if (recordingConfig.output.compressed.enabled) {
|
|
1159
1199
|
bundleOf(
|
|
1160
1200
|
"size" to (compressedFile?.length() ?: 0),
|
|
1161
|
-
"mimeType" to if (recordingConfig.
|
|
1162
|
-
"bitrate" to recordingConfig.
|
|
1163
|
-
"format" to recordingConfig.
|
|
1201
|
+
"mimeType" to if (recordingConfig.output.compressed.format == "aac") "audio/aac" else "audio/opus",
|
|
1202
|
+
"bitrate" to recordingConfig.output.compressed.bitrate,
|
|
1203
|
+
"format" to recordingConfig.output.compressed.format
|
|
1164
1204
|
)
|
|
1165
1205
|
} else null
|
|
1166
1206
|
|
|
@@ -1257,8 +1297,16 @@ class AudioRecorderManager(
|
|
|
1257
1297
|
private fun recordingProcess() {
|
|
1258
1298
|
try {
|
|
1259
1299
|
LogUtils.i(CLASS_NAME, "Starting recording process...")
|
|
1260
|
-
|
|
1261
|
-
|
|
1300
|
+
|
|
1301
|
+
// Only use FileOutputStream if primary output is enabled
|
|
1302
|
+
val fos = if (recordingConfig.output.primary.enabled && audioFile != null) {
|
|
1303
|
+
FileOutputStream(audioFile, true)
|
|
1304
|
+
} else {
|
|
1305
|
+
null
|
|
1306
|
+
}
|
|
1307
|
+
|
|
1308
|
+
try {
|
|
1309
|
+
// Write audio data directly to the file (if not skipping)
|
|
1262
1310
|
val audioData = ByteArray(bufferSizeInBytes)
|
|
1263
1311
|
LogUtils.d(CLASS_NAME, "Entering recording loop")
|
|
1264
1312
|
|
|
@@ -1318,7 +1366,10 @@ class AudioRecorderManager(
|
|
|
1318
1366
|
}
|
|
1319
1367
|
|
|
1320
1368
|
if (bytesRead > 0) {
|
|
1321
|
-
|
|
1369
|
+
// Only write to file if primary output is enabled
|
|
1370
|
+
if (fos != null) {
|
|
1371
|
+
fos.write(audioData, 0, bytesRead)
|
|
1372
|
+
}
|
|
1322
1373
|
totalDataSize += bytesRead
|
|
1323
1374
|
|
|
1324
1375
|
accumulatedAudioData.write(audioData, 0, bytesRead)
|
|
@@ -1377,10 +1428,16 @@ class AudioRecorderManager(
|
|
|
1377
1428
|
}
|
|
1378
1429
|
}
|
|
1379
1430
|
}
|
|
1431
|
+
} finally {
|
|
1432
|
+
// Close the file output stream if it was opened
|
|
1433
|
+
fos?.close()
|
|
1380
1434
|
}
|
|
1381
|
-
|
|
1382
|
-
|
|
1383
|
-
|
|
1435
|
+
|
|
1436
|
+
// Update the WAV header to reflect the actual data size (only if file was created)
|
|
1437
|
+
if (recordingConfig.output.primary.enabled) {
|
|
1438
|
+
audioFile?.let { file ->
|
|
1439
|
+
audioFileHandler.updateWavHeader(file)
|
|
1440
|
+
}
|
|
1384
1441
|
}
|
|
1385
1442
|
|
|
1386
1443
|
} catch (e: Exception) {
|
|
@@ -1403,7 +1460,7 @@ class AudioRecorderManager(
|
|
|
1403
1460
|
val positionInMs =
|
|
1404
1461
|
(from * 1000) / (recordingConfig.sampleRate * recordingConfig.channels * (if (recordingConfig.encoding == "pcm_8bit") 8 else 16) / 8)
|
|
1405
1462
|
|
|
1406
|
-
val compressionBundle = if (recordingConfig.
|
|
1463
|
+
val compressionBundle = if (recordingConfig.output.compressed.enabled) {
|
|
1407
1464
|
val compressedSize = compressedFile?.length() ?: 0
|
|
1408
1465
|
val eventDataSize = compressedSize - lastEmittedCompressedSize
|
|
1409
1466
|
|
|
@@ -1563,6 +1620,12 @@ class AudioRecorderManager(
|
|
|
1563
1620
|
|
|
1564
1621
|
@RequiresApi(Build.VERSION_CODES.Q)
|
|
1565
1622
|
private fun initializeCompressedRecorder(fileExtension: String, promise: Promise): Boolean {
|
|
1623
|
+
// Skip compressed recording if compressed output is not enabled
|
|
1624
|
+
if (!recordingConfig.output.compressed.enabled) {
|
|
1625
|
+
LogUtils.d(CLASS_NAME, "Skipping compressed recorder initialization - compressed output is disabled")
|
|
1626
|
+
return true
|
|
1627
|
+
}
|
|
1628
|
+
|
|
1566
1629
|
try {
|
|
1567
1630
|
// Pass true to indicate this is a compressed file
|
|
1568
1631
|
compressedFile = createRecordingFile(recordingConfig, isCompressed = true)
|
|
@@ -1576,15 +1639,15 @@ class AudioRecorderManager(
|
|
|
1576
1639
|
|
|
1577
1640
|
compressedRecorder?.apply {
|
|
1578
1641
|
setAudioSource(MediaRecorder.AudioSource.MIC)
|
|
1579
|
-
setOutputFormat(if (recordingConfig.
|
|
1642
|
+
setOutputFormat(if (recordingConfig.output.compressed.format == "aac")
|
|
1580
1643
|
MediaRecorder.OutputFormat.AAC_ADTS
|
|
1581
1644
|
else MediaRecorder.OutputFormat.OGG)
|
|
1582
|
-
setAudioEncoder(if (recordingConfig.
|
|
1645
|
+
setAudioEncoder(if (recordingConfig.output.compressed.format == "aac")
|
|
1583
1646
|
MediaRecorder.AudioEncoder.AAC
|
|
1584
1647
|
else MediaRecorder.AudioEncoder.OPUS)
|
|
1585
1648
|
setAudioChannels(recordingConfig.channels)
|
|
1586
1649
|
setAudioSamplingRate(recordingConfig.sampleRate)
|
|
1587
|
-
setAudioEncodingBitRate(recordingConfig.
|
|
1650
|
+
setAudioEncodingBitRate(recordingConfig.output.compressed.bitrate)
|
|
1588
1651
|
setOutputFile(compressedFile?.absolutePath)
|
|
1589
1652
|
prepare()
|
|
1590
1653
|
}
|
|
@@ -1687,7 +1750,7 @@ class AudioRecorderManager(
|
|
|
1687
1750
|
|
|
1688
1751
|
// Choose extension based on whether this is a compressed file
|
|
1689
1752
|
val extension = if (isCompressed) {
|
|
1690
|
-
config.
|
|
1753
|
+
config.output.compressed.format.lowercase()
|
|
1691
1754
|
} else {
|
|
1692
1755
|
"wav"
|
|
1693
1756
|
}
|
|
@@ -1752,8 +1815,8 @@ class AudioRecorderManager(
|
|
|
1752
1815
|
if (!initializeBufferSize(dummyPromise)) return false
|
|
1753
1816
|
if (!initializeAudioRecord(dummyPromise)) return false
|
|
1754
1817
|
|
|
1755
|
-
if (recordingConfig.
|
|
1756
|
-
if (recordingConfig.
|
|
1818
|
+
if (recordingConfig.output.compressed.enabled && !initializeCompressedRecorder(
|
|
1819
|
+
if (recordingConfig.output.compressed.format == "aac") "aac" else "opus",
|
|
1757
1820
|
dummyPromise
|
|
1758
1821
|
)) return false
|
|
1759
1822
|
|
|
@@ -10,6 +10,16 @@ object LogUtils {
|
|
|
10
10
|
// Format: [ExpoAudioStudio:ClassName]
|
|
11
11
|
private const val TAG_PREFIX = "ExpoAudioStudio"
|
|
12
12
|
|
|
13
|
+
// Check if we're running in a test environment
|
|
14
|
+
private val isInTest: Boolean by lazy {
|
|
15
|
+
try {
|
|
16
|
+
Class.forName("org.junit.Test")
|
|
17
|
+
true
|
|
18
|
+
} catch (e: ClassNotFoundException) {
|
|
19
|
+
false
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
13
23
|
/**
|
|
14
24
|
* Logs a debug message with a consistent format.
|
|
15
25
|
*
|
|
@@ -17,7 +27,11 @@ object LogUtils {
|
|
|
17
27
|
* @param message The message to log
|
|
18
28
|
*/
|
|
19
29
|
fun d(className: String, message: String) {
|
|
20
|
-
|
|
30
|
+
if (isInTest) {
|
|
31
|
+
println("D/$TAG_PREFIX:$className: $message")
|
|
32
|
+
} else {
|
|
33
|
+
Log.d("$TAG_PREFIX:$className", message)
|
|
34
|
+
}
|
|
21
35
|
}
|
|
22
36
|
|
|
23
37
|
/**
|
|
@@ -28,7 +42,12 @@ object LogUtils {
|
|
|
28
42
|
* @param throwable Optional throwable to include in the log
|
|
29
43
|
*/
|
|
30
44
|
fun e(className: String, message: String, throwable: Throwable? = null) {
|
|
31
|
-
|
|
45
|
+
if (isInTest) {
|
|
46
|
+
println("E/$TAG_PREFIX:$className: $message")
|
|
47
|
+
throwable?.printStackTrace()
|
|
48
|
+
} else {
|
|
49
|
+
Log.e("$TAG_PREFIX:$className", message, throwable)
|
|
50
|
+
}
|
|
32
51
|
}
|
|
33
52
|
|
|
34
53
|
/**
|
|
@@ -39,7 +58,12 @@ object LogUtils {
|
|
|
39
58
|
* @param throwable Optional throwable to include in the log
|
|
40
59
|
*/
|
|
41
60
|
fun w(className: String, message: String, throwable: Throwable? = null) {
|
|
42
|
-
|
|
61
|
+
if (isInTest) {
|
|
62
|
+
println("W/$TAG_PREFIX:$className: $message")
|
|
63
|
+
throwable?.printStackTrace()
|
|
64
|
+
} else {
|
|
65
|
+
Log.w("$TAG_PREFIX:$className", message, throwable)
|
|
66
|
+
}
|
|
43
67
|
}
|
|
44
68
|
|
|
45
69
|
/**
|
|
@@ -49,7 +73,11 @@ object LogUtils {
|
|
|
49
73
|
* @param message The message to log
|
|
50
74
|
*/
|
|
51
75
|
fun i(className: String, message: String) {
|
|
52
|
-
|
|
76
|
+
if (isInTest) {
|
|
77
|
+
println("I/$TAG_PREFIX:$className: $message")
|
|
78
|
+
} else {
|
|
79
|
+
Log.i("$TAG_PREFIX:$className", message)
|
|
80
|
+
}
|
|
53
81
|
}
|
|
54
82
|
|
|
55
83
|
/**
|
|
@@ -4,6 +4,45 @@ import android.media.AudioFormat
|
|
|
4
4
|
import android.os.Build
|
|
5
5
|
import java.io.File
|
|
6
6
|
|
|
7
|
+
// New output configuration structure
|
|
8
|
+
data class OutputConfig(
|
|
9
|
+
val primary: PrimaryOutput = PrimaryOutput(),
|
|
10
|
+
val compressed: CompressedOutput = CompressedOutput()
|
|
11
|
+
) {
|
|
12
|
+
data class PrimaryOutput(
|
|
13
|
+
val enabled: Boolean = true,
|
|
14
|
+
val format: String = "wav"
|
|
15
|
+
)
|
|
16
|
+
|
|
17
|
+
data class CompressedOutput(
|
|
18
|
+
val enabled: Boolean = false,
|
|
19
|
+
val format: String = "aac",
|
|
20
|
+
val bitrate: Int = 128000
|
|
21
|
+
)
|
|
22
|
+
|
|
23
|
+
companion object {
|
|
24
|
+
fun fromMap(map: Map<String, Any?>?): OutputConfig {
|
|
25
|
+
if (map == null) return OutputConfig()
|
|
26
|
+
|
|
27
|
+
val primaryMap = map.getTypedMap<Any?>("primary") { true }
|
|
28
|
+
val compressedMap = map.getTypedMap<Any?>("compressed") { true }
|
|
29
|
+
|
|
30
|
+
val primary = PrimaryOutput(
|
|
31
|
+
enabled = primaryMap.getBooleanOrDefault("enabled", true),
|
|
32
|
+
format = primaryMap.getStringOrDefault("format", "wav")
|
|
33
|
+
)
|
|
34
|
+
|
|
35
|
+
val compressed = CompressedOutput(
|
|
36
|
+
enabled = compressedMap.getBooleanOrDefault("enabled", false),
|
|
37
|
+
format = compressedMap.getStringOrDefault("format", "aac").lowercase(),
|
|
38
|
+
bitrate = compressedMap.getNumberOrDefault("bitrate", 128000)
|
|
39
|
+
)
|
|
40
|
+
|
|
41
|
+
return OutputConfig(primary = primary, compressed = compressed)
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
7
46
|
data class RecordingConfig(
|
|
8
47
|
val sampleRate: Int = Constants.DEFAULT_SAMPLE_RATE,
|
|
9
48
|
val channels: Int = 1,
|
|
@@ -17,14 +56,13 @@ data class RecordingConfig(
|
|
|
17
56
|
val showWaveformInNotification: Boolean = false,
|
|
18
57
|
val notification: NotificationConfig = NotificationConfig(),
|
|
19
58
|
val features: Map<String, Boolean> = emptyMap(),
|
|
20
|
-
val
|
|
21
|
-
val compressedFormat: String = "opus",
|
|
22
|
-
val compressedBitRate: Int = 24000,
|
|
59
|
+
val output: OutputConfig = OutputConfig(),
|
|
23
60
|
val autoResumeAfterInterruption: Boolean = false,
|
|
24
61
|
val outputDirectory: String? = null,
|
|
25
62
|
val filename: String? = null,
|
|
26
63
|
val deviceId: String? = null,
|
|
27
64
|
val deviceDisconnectionBehavior: String? = null,
|
|
65
|
+
val bufferDurationSeconds: Double? = null,
|
|
28
66
|
) {
|
|
29
67
|
companion object {
|
|
30
68
|
fun fromMap(options: Map<String, Any?>?): Result<Pair<RecordingConfig, AudioFormatInfo>> {
|
|
@@ -45,19 +83,17 @@ data class RecordingConfig(
|
|
|
45
83
|
val notificationMap = options.getTypedMap<Any?>("notification") { true }
|
|
46
84
|
val notificationConfig = NotificationConfig.fromMap(notificationMap)
|
|
47
85
|
|
|
48
|
-
// Parse
|
|
49
|
-
val
|
|
50
|
-
val
|
|
51
|
-
val compressedFormat = (compressionMap["format"] as? String)?.lowercase() ?: "aac"
|
|
52
|
-
val compressedBitRate = (compressionMap["bitrate"] as? Number)?.toInt() ?: 128000
|
|
86
|
+
// Parse output config
|
|
87
|
+
val outputMap = options.getTypedMap<Any?>("output") { true }
|
|
88
|
+
val outputConfig = OutputConfig.fromMap(outputMap)
|
|
53
89
|
|
|
54
90
|
// Validate bitrate if compression is enabled
|
|
55
|
-
if (
|
|
91
|
+
if (outputConfig.compressed.enabled) {
|
|
56
92
|
when {
|
|
57
|
-
|
|
93
|
+
outputConfig.compressed.bitrate < 8000 -> return Result.failure(
|
|
58
94
|
IllegalArgumentException("Bitrate must be at least 8000 bps")
|
|
59
95
|
)
|
|
60
|
-
|
|
96
|
+
outputConfig.compressed.bitrate > 960000 -> return Result.failure(
|
|
61
97
|
IllegalArgumentException("Bitrate cannot exceed 960000 bps")
|
|
62
98
|
)
|
|
63
99
|
}
|
|
@@ -102,9 +138,7 @@ data class RecordingConfig(
|
|
|
102
138
|
showWaveformInNotification = options.getBooleanOrDefault("showWaveformInNotification", false),
|
|
103
139
|
notification = notificationConfig,
|
|
104
140
|
features = features,
|
|
105
|
-
|
|
106
|
-
compressedFormat = compressedFormat,
|
|
107
|
-
compressedBitRate = compressedBitRate,
|
|
141
|
+
output = outputConfig,
|
|
108
142
|
autoResumeAfterInterruption = options.getBooleanOrDefault("autoResumeAfterInterruption", false),
|
|
109
143
|
outputDirectory = outputDirectory?.let {
|
|
110
144
|
it.replace(Regex("^file://"), "")
|
|
@@ -113,7 +147,8 @@ data class RecordingConfig(
|
|
|
113
147
|
},
|
|
114
148
|
filename = options["filename"] as? String,
|
|
115
149
|
deviceId = deviceId,
|
|
116
|
-
deviceDisconnectionBehavior = deviceDisconnectionBehavior
|
|
150
|
+
deviceDisconnectionBehavior = deviceDisconnectionBehavior,
|
|
151
|
+
bufferDurationSeconds = (options["bufferDurationSeconds"] as? Number)?.toDouble(),
|
|
117
152
|
)
|
|
118
153
|
|
|
119
154
|
// Validate sample rate and channels
|