react-native-audio-concat 0.5.0 → 0.6.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.
|
@@ -160,6 +160,87 @@ class AudioConcatModule(reactContext: ReactApplicationContext) :
|
|
|
160
160
|
}
|
|
161
161
|
}
|
|
162
162
|
|
|
163
|
+
// Helper class to manage MediaCodec decoder reuse
|
|
164
|
+
private class ReusableDecoder {
|
|
165
|
+
private var decoder: MediaCodec? = null
|
|
166
|
+
private var currentMimeType: String? = null
|
|
167
|
+
private var currentFormat: MediaFormat? = null
|
|
168
|
+
|
|
169
|
+
fun getOrCreateDecoder(mimeType: String, format: MediaFormat): MediaCodec {
|
|
170
|
+
// Check if we can reuse the existing decoder
|
|
171
|
+
if (decoder != null && currentMimeType == mimeType && formatsCompatible(currentFormat, format)) {
|
|
172
|
+
// Flush the decoder to reset its state
|
|
173
|
+
try {
|
|
174
|
+
decoder!!.flush()
|
|
175
|
+
Log.d("AudioConcat", " Reused decoder for $mimeType")
|
|
176
|
+
return decoder!!
|
|
177
|
+
} catch (e: Exception) {
|
|
178
|
+
Log.w("AudioConcat", "Failed to flush decoder, recreating: ${e.message}")
|
|
179
|
+
release()
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
// Need to create a new decoder
|
|
184
|
+
release() // Release old one if exists
|
|
185
|
+
|
|
186
|
+
val newDecoder = MediaCodec.createDecoderByType(mimeType)
|
|
187
|
+
newDecoder.configure(format, null, null, 0)
|
|
188
|
+
newDecoder.start()
|
|
189
|
+
|
|
190
|
+
decoder = newDecoder
|
|
191
|
+
currentMimeType = mimeType
|
|
192
|
+
currentFormat = format
|
|
193
|
+
|
|
194
|
+
Log.d("AudioConcat", " Created new decoder for $mimeType")
|
|
195
|
+
return newDecoder
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
private fun formatsCompatible(format1: MediaFormat?, format2: MediaFormat): Boolean {
|
|
199
|
+
if (format1 == null) return false
|
|
200
|
+
|
|
201
|
+
// Check key format properties
|
|
202
|
+
return try {
|
|
203
|
+
format1.getInteger(MediaFormat.KEY_SAMPLE_RATE) == format2.getInteger(MediaFormat.KEY_SAMPLE_RATE) &&
|
|
204
|
+
format1.getInteger(MediaFormat.KEY_CHANNEL_COUNT) == format2.getInteger(MediaFormat.KEY_CHANNEL_COUNT)
|
|
205
|
+
} catch (e: Exception) {
|
|
206
|
+
false
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
fun release() {
|
|
211
|
+
decoder?.let {
|
|
212
|
+
try {
|
|
213
|
+
it.stop()
|
|
214
|
+
it.release()
|
|
215
|
+
} catch (e: Exception) {
|
|
216
|
+
Log.w("AudioConcat", "Error releasing decoder: ${e.message}")
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
decoder = null
|
|
220
|
+
currentMimeType = null
|
|
221
|
+
currentFormat = null
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
// Thread-safe decoder pool for parallel processing
|
|
226
|
+
private class DecoderPool {
|
|
227
|
+
private val decoders = ConcurrentHashMap<Long, ReusableDecoder>()
|
|
228
|
+
|
|
229
|
+
fun getDecoderForCurrentThread(): ReusableDecoder {
|
|
230
|
+
val threadId = Thread.currentThread().id
|
|
231
|
+
return decoders.getOrPut(threadId) {
|
|
232
|
+
Log.d("AudioConcat", " Created decoder for thread $threadId")
|
|
233
|
+
ReusableDecoder()
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
fun releaseAll() {
|
|
238
|
+
decoders.values.forEach { it.release() }
|
|
239
|
+
decoders.clear()
|
|
240
|
+
Log.d("AudioConcat", "Released all pooled decoders")
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
|
|
163
244
|
private fun extractAudioConfig(filePath: String): AudioConfig {
|
|
164
245
|
val extractor = MediaExtractor()
|
|
165
246
|
try {
|
|
@@ -356,58 +437,48 @@ class AudioConcatModule(reactContext: ReactApplicationContext) :
|
|
|
356
437
|
return input
|
|
357
438
|
}
|
|
358
439
|
|
|
440
|
+
val startTime = System.currentTimeMillis()
|
|
359
441
|
val inputSampleCount = input.size / (2 * channelCount) // 16-bit = 2 bytes per sample
|
|
360
442
|
val outputSampleCount = (inputSampleCount.toLong() * outputSampleRate / inputSampleRate).toInt()
|
|
361
443
|
val output = ByteArray(outputSampleCount * 2 * channelCount)
|
|
362
444
|
|
|
363
|
-
//
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
445
|
+
// Helper function to read a sample with bounds checking
|
|
446
|
+
fun readSample(sampleIndex: Int, channel: Int): Int {
|
|
447
|
+
val clampedIndex = sampleIndex.coerceIn(0, inputSampleCount - 1)
|
|
448
|
+
val idx = (clampedIndex * channelCount + channel) * 2
|
|
449
|
+
val unsigned = (input[idx].toInt() and 0xFF) or (input[idx + 1].toInt() shl 8)
|
|
450
|
+
return if (unsigned > 32767) unsigned - 65536 else unsigned
|
|
451
|
+
}
|
|
367
452
|
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
val fraction = srcPos and 0xFFFF // Fractional part in 16-bit fixed-point
|
|
453
|
+
// Use floating-point for better accuracy than fixed-point
|
|
454
|
+
val ratio = inputSampleRate.toDouble() / outputSampleRate.toDouble()
|
|
371
455
|
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
456
|
+
for (i in 0 until outputSampleCount) {
|
|
457
|
+
val srcPos = i * ratio
|
|
458
|
+
val srcIndex = srcPos.toInt()
|
|
459
|
+
val fraction = srcPos - srcIndex // Fractional part (0.0 to 1.0)
|
|
376
460
|
|
|
377
461
|
for (ch in 0 until channelCount) {
|
|
378
|
-
//
|
|
379
|
-
val
|
|
380
|
-
val
|
|
381
|
-
|
|
382
|
-
// Read 16-bit samples (little-endian)
|
|
383
|
-
val sample1 = (input[idx1].toInt() and 0xFF) or (input[idx1 + 1].toInt() shl 8)
|
|
384
|
-
val sample2 = if (idx2 + 1 < input.size) {
|
|
385
|
-
(input[idx2].toInt() and 0xFF) or (input[idx2 + 1].toInt() shl 8)
|
|
386
|
-
} else {
|
|
387
|
-
sample1
|
|
388
|
-
}
|
|
389
|
-
|
|
390
|
-
// Convert to signed 16-bit
|
|
391
|
-
val s1 = if (sample1 > 32767) sample1 - 65536 else sample1
|
|
392
|
-
val s2 = if (sample2 > 32767) sample2 - 65536 else sample2
|
|
462
|
+
// Linear interpolation with floating-point precision
|
|
463
|
+
val s1 = readSample(srcIndex, ch).toDouble()
|
|
464
|
+
val s2 = readSample(srcIndex + 1, ch).toDouble()
|
|
393
465
|
|
|
394
|
-
// Linear interpolation
|
|
395
|
-
|
|
396
|
-
// fraction is in 16.16 format, so we shift right by 16 after multiplication
|
|
397
|
-
val interpolated = s1 + (((s2 - s1) * fraction) shr 16)
|
|
466
|
+
// Linear interpolation: s1 + (s2 - s1) * fraction
|
|
467
|
+
val interpolated = s1 + (s2 - s1) * fraction
|
|
398
468
|
|
|
399
469
|
// Clamp to 16-bit range
|
|
400
|
-
val clamped = interpolated.coerceIn(-32768, 32767)
|
|
470
|
+
val clamped = interpolated.toInt().coerceIn(-32768, 32767)
|
|
401
471
|
|
|
402
|
-
//
|
|
472
|
+
// Write to output (little-endian)
|
|
403
473
|
val outIdx = (i * channelCount + ch) * 2
|
|
404
474
|
output[outIdx] = (clamped and 0xFF).toByte()
|
|
405
475
|
output[outIdx + 1] = (clamped shr 8).toByte()
|
|
406
476
|
}
|
|
407
|
-
|
|
408
|
-
srcPos += step
|
|
409
477
|
}
|
|
410
478
|
|
|
479
|
+
val elapsedTime = System.currentTimeMillis() - startTime
|
|
480
|
+
Log.d("AudioConcat", " Resampled ${inputSampleRate}Hz→${outputSampleRate}Hz, ${input.size / 1024}KB→${output.size / 1024}KB in ${elapsedTime}ms")
|
|
481
|
+
|
|
411
482
|
return output
|
|
412
483
|
}
|
|
413
484
|
|
|
@@ -477,7 +548,8 @@ class AudioConcatModule(reactContext: ReactApplicationContext) :
|
|
|
477
548
|
targetSampleRate: Int,
|
|
478
549
|
targetChannelCount: Int,
|
|
479
550
|
latch: CountDownLatch,
|
|
480
|
-
cache: PCMCache
|
|
551
|
+
cache: PCMCache,
|
|
552
|
+
decoderPool: DecoderPool? = null
|
|
481
553
|
) {
|
|
482
554
|
try {
|
|
483
555
|
// Check cache first
|
|
@@ -497,6 +569,7 @@ class AudioConcatModule(reactContext: ReactApplicationContext) :
|
|
|
497
569
|
var decoder: MediaCodec? = null
|
|
498
570
|
val decodedChunks = mutableListOf<ByteArray>()
|
|
499
571
|
var totalBytes = 0L
|
|
572
|
+
val shouldReleaseDecoder = (decoderPool == null) // Only release if not using pool
|
|
500
573
|
|
|
501
574
|
try {
|
|
502
575
|
extractor.setDataSource(filePath)
|
|
@@ -531,9 +604,17 @@ class AudioConcatModule(reactContext: ReactApplicationContext) :
|
|
|
531
604
|
extractor.selectTrack(audioTrackIndex)
|
|
532
605
|
|
|
533
606
|
val mime = audioFormat.getString(MediaFormat.KEY_MIME)!!
|
|
534
|
-
|
|
535
|
-
decoder
|
|
536
|
-
decoder
|
|
607
|
+
|
|
608
|
+
// Use decoder pool if available, otherwise create new decoder
|
|
609
|
+
decoder = if (decoderPool != null) {
|
|
610
|
+
val reusableDecoder = decoderPool.getDecoderForCurrentThread()
|
|
611
|
+
reusableDecoder.getOrCreateDecoder(mime, audioFormat)
|
|
612
|
+
} else {
|
|
613
|
+
val newDecoder = MediaCodec.createDecoderByType(mime)
|
|
614
|
+
newDecoder.configure(audioFormat, null, null, 0)
|
|
615
|
+
newDecoder.start()
|
|
616
|
+
newDecoder
|
|
617
|
+
}
|
|
537
618
|
|
|
538
619
|
val bufferInfo = MediaCodec.BufferInfo()
|
|
539
620
|
var isEOS = false
|
|
@@ -596,8 +677,11 @@ class AudioConcatModule(reactContext: ReactApplicationContext) :
|
|
|
596
677
|
}
|
|
597
678
|
|
|
598
679
|
} finally {
|
|
599
|
-
decoder
|
|
600
|
-
|
|
680
|
+
// Only stop/release decoder if not using pool
|
|
681
|
+
if (shouldReleaseDecoder) {
|
|
682
|
+
decoder?.stop()
|
|
683
|
+
decoder?.release()
|
|
684
|
+
}
|
|
601
685
|
extractor.release()
|
|
602
686
|
}
|
|
603
687
|
} catch (e: Exception) {
|
|
@@ -613,10 +697,13 @@ class AudioConcatModule(reactContext: ReactApplicationContext) :
|
|
|
613
697
|
encoder: StreamingEncoder,
|
|
614
698
|
isLastFile: Boolean,
|
|
615
699
|
targetSampleRate: Int,
|
|
616
|
-
targetChannelCount: Int
|
|
700
|
+
targetChannelCount: Int,
|
|
701
|
+
reusableDecoder: ReusableDecoder? = null
|
|
617
702
|
) {
|
|
703
|
+
val startTime = System.currentTimeMillis()
|
|
618
704
|
val extractor = MediaExtractor()
|
|
619
705
|
var decoder: MediaCodec? = null
|
|
706
|
+
val shouldReleaseDecoder = (reusableDecoder == null) // Only release if not reusing
|
|
620
707
|
|
|
621
708
|
try {
|
|
622
709
|
extractor.setDataSource(filePath)
|
|
@@ -651,9 +738,16 @@ class AudioConcatModule(reactContext: ReactApplicationContext) :
|
|
|
651
738
|
extractor.selectTrack(audioTrackIndex)
|
|
652
739
|
|
|
653
740
|
val mime = audioFormat.getString(MediaFormat.KEY_MIME)!!
|
|
654
|
-
|
|
655
|
-
decoder
|
|
656
|
-
decoder
|
|
741
|
+
|
|
742
|
+
// Use reusable decoder if provided, otherwise create a new one
|
|
743
|
+
decoder = if (reusableDecoder != null) {
|
|
744
|
+
reusableDecoder.getOrCreateDecoder(mime, audioFormat)
|
|
745
|
+
} else {
|
|
746
|
+
val newDecoder = MediaCodec.createDecoderByType(mime)
|
|
747
|
+
newDecoder.configure(audioFormat, null, null, 0)
|
|
748
|
+
newDecoder.start()
|
|
749
|
+
newDecoder
|
|
750
|
+
}
|
|
657
751
|
|
|
658
752
|
val bufferInfo = MediaCodec.BufferInfo()
|
|
659
753
|
var isEOS = false
|
|
@@ -706,9 +800,14 @@ class AudioConcatModule(reactContext: ReactApplicationContext) :
|
|
|
706
800
|
}
|
|
707
801
|
|
|
708
802
|
} finally {
|
|
709
|
-
decoder
|
|
710
|
-
|
|
803
|
+
// Only stop/release decoder if we created it locally (not reusing)
|
|
804
|
+
if (shouldReleaseDecoder) {
|
|
805
|
+
decoder?.stop()
|
|
806
|
+
decoder?.release()
|
|
807
|
+
}
|
|
711
808
|
extractor.release()
|
|
809
|
+
val elapsedTime = System.currentTimeMillis() - startTime
|
|
810
|
+
Log.d("AudioConcat", " Decoded file in ${elapsedTime}ms")
|
|
712
811
|
}
|
|
713
812
|
}
|
|
714
813
|
|
|
@@ -833,6 +932,10 @@ class AudioConcatModule(reactContext: ReactApplicationContext) :
|
|
|
833
932
|
val latch = CountDownLatch(optimizedFiles.size)
|
|
834
933
|
val sequenceCounter = AtomicInteger(0)
|
|
835
934
|
|
|
935
|
+
// Create decoder pool for reuse across threads
|
|
936
|
+
val decoderPool = DecoderPool()
|
|
937
|
+
Log.d("AudioConcat", "Created decoder pool for parallel processing ($numThreads threads)")
|
|
938
|
+
|
|
836
939
|
try {
|
|
837
940
|
// Submit decode tasks for unique files only
|
|
838
941
|
optimizedFiles.forEachIndexed { optIndex, (index, filePath) ->
|
|
@@ -842,7 +945,7 @@ class AudioConcatModule(reactContext: ReactApplicationContext) :
|
|
|
842
945
|
sequenceCounter.addAndGet(1000000)
|
|
843
946
|
|
|
844
947
|
Log.d("AudioConcat", "Starting parallel decode [$index]: $filePath")
|
|
845
|
-
parallelDecodeToQueue(filePath, pcmQueue, fileSequenceStart, targetSampleRate, targetChannelCount, latch, cache)
|
|
948
|
+
parallelDecodeToQueue(filePath, pcmQueue, fileSequenceStart, targetSampleRate, targetChannelCount, latch, cache, decoderPool)
|
|
846
949
|
|
|
847
950
|
// Mark end with duplicate count
|
|
848
951
|
val repeatCount = consecutiveDuplicates[optIndex] ?: 1
|
|
@@ -900,6 +1003,7 @@ class AudioConcatModule(reactContext: ReactApplicationContext) :
|
|
|
900
1003
|
Log.d("AudioConcat", "All parallel decode tasks completed")
|
|
901
1004
|
|
|
902
1005
|
} finally {
|
|
1006
|
+
decoderPool.releaseAll()
|
|
903
1007
|
executor.shutdown()
|
|
904
1008
|
}
|
|
905
1009
|
}
|
|
@@ -1032,17 +1136,24 @@ class AudioConcatModule(reactContext: ReactApplicationContext) :
|
|
|
1032
1136
|
}
|
|
1033
1137
|
|
|
1034
1138
|
override fun concatAudioFiles(data: ReadableArray, outputPath: String, promise: Promise) {
|
|
1139
|
+
val totalStartTime = System.currentTimeMillis()
|
|
1140
|
+
Log.d("AudioConcat", "========== Audio Concat Started ==========")
|
|
1141
|
+
|
|
1035
1142
|
try {
|
|
1036
1143
|
if (data.size() == 0) {
|
|
1037
1144
|
promise.reject("EMPTY_DATA", "Data array is empty")
|
|
1038
1145
|
return
|
|
1039
1146
|
}
|
|
1040
1147
|
|
|
1148
|
+
// Parse data
|
|
1149
|
+
val parseStartTime = System.currentTimeMillis()
|
|
1041
1150
|
val parsedData = parseAudioData(data)
|
|
1042
|
-
|
|
1151
|
+
val parseTime = System.currentTimeMillis() - parseStartTime
|
|
1152
|
+
Log.d("AudioConcat", "✓ Parsed ${parsedData.size} items in ${parseTime}ms")
|
|
1043
1153
|
Log.d("AudioConcat", "Output: $outputPath")
|
|
1044
1154
|
|
|
1045
1155
|
// Get audio config from first audio file
|
|
1156
|
+
val configStartTime = System.currentTimeMillis()
|
|
1046
1157
|
var audioConfig: AudioConfig? = null
|
|
1047
1158
|
for (item in parsedData) {
|
|
1048
1159
|
if (item is AudioDataOrSilence.AudioFile) {
|
|
@@ -1056,10 +1167,21 @@ class AudioConcatModule(reactContext: ReactApplicationContext) :
|
|
|
1056
1167
|
return
|
|
1057
1168
|
}
|
|
1058
1169
|
|
|
1059
|
-
|
|
1170
|
+
val configTime = System.currentTimeMillis() - configStartTime
|
|
1171
|
+
|
|
1172
|
+
// Force output sample rate to 24kHz for optimal performance
|
|
1173
|
+
val outputSampleRate = 24000
|
|
1174
|
+
Log.d("AudioConcat", "✓ Extracted audio config in ${configTime}ms: ${audioConfig.channelCount}ch, ${audioConfig.bitRate}bps")
|
|
1175
|
+
Log.d("AudioConcat", "Output sample rate: ${outputSampleRate}Hz (24kHz optimized)")
|
|
1176
|
+
|
|
1177
|
+
// Create modified config with fixed sample rate
|
|
1178
|
+
val outputAudioConfig = AudioConfig(outputSampleRate, audioConfig.channelCount, audioConfig.bitRate)
|
|
1060
1179
|
|
|
1061
1180
|
// Analyze duplicates to determine cache strategy
|
|
1062
|
-
val
|
|
1181
|
+
val analysisStartTime = System.currentTimeMillis()
|
|
1182
|
+
val duplicateAnalysis = analyzeDuplicates(parsedData, outputAudioConfig)
|
|
1183
|
+
val analysisTime = System.currentTimeMillis() - analysisStartTime
|
|
1184
|
+
Log.d("AudioConcat", "✓ Analyzed duplicates in ${analysisTime}ms")
|
|
1063
1185
|
|
|
1064
1186
|
// Create cache instance with intelligent caching strategy
|
|
1065
1187
|
val cache = PCMCache(duplicateAnalysis.duplicateFiles, duplicateAnalysis.duplicateSilence)
|
|
@@ -1070,9 +1192,9 @@ class AudioConcatModule(reactContext: ReactApplicationContext) :
|
|
|
1070
1192
|
outputFile.delete()
|
|
1071
1193
|
}
|
|
1072
1194
|
|
|
1073
|
-
// Create streaming encoder
|
|
1195
|
+
// Create streaming encoder with fixed 24kHz sample rate
|
|
1074
1196
|
val encoder = StreamingEncoder(
|
|
1075
|
-
|
|
1197
|
+
outputSampleRate,
|
|
1076
1198
|
audioConfig.channelCount,
|
|
1077
1199
|
audioConfig.bitRate,
|
|
1078
1200
|
outputPath
|
|
@@ -1096,9 +1218,10 @@ class AudioConcatModule(reactContext: ReactApplicationContext) :
|
|
|
1096
1218
|
|
|
1097
1219
|
// Decide whether to use parallel or sequential processing
|
|
1098
1220
|
val useParallel = audioFileItems.size >= 10 // Use parallel for 10+ files
|
|
1221
|
+
val processingStartTime = System.currentTimeMillis()
|
|
1099
1222
|
|
|
1100
1223
|
if (useParallel) {
|
|
1101
|
-
Log.d("AudioConcat", "Using parallel processing for ${audioFileItems.size} audio files")
|
|
1224
|
+
Log.d("AudioConcat", "→ Using parallel processing for ${audioFileItems.size} audio files")
|
|
1102
1225
|
|
|
1103
1226
|
// Process interleaved patterns optimally
|
|
1104
1227
|
val processedIndices = mutableSetOf<Int>()
|
|
@@ -1121,7 +1244,7 @@ class AudioConcatModule(reactContext: ReactApplicationContext) :
|
|
|
1121
1244
|
val latch = CountDownLatch(1)
|
|
1122
1245
|
val seqStart = AtomicInteger(0)
|
|
1123
1246
|
|
|
1124
|
-
parallelDecodeToQueue(filePath, tempQueue, seqStart,
|
|
1247
|
+
parallelDecodeToQueue(filePath, tempQueue, seqStart, outputSampleRate, audioConfig.channelCount, latch, cache)
|
|
1125
1248
|
|
|
1126
1249
|
// Collect chunks
|
|
1127
1250
|
var collecting = true
|
|
@@ -1203,7 +1326,7 @@ class AudioConcatModule(reactContext: ReactApplicationContext) :
|
|
|
1203
1326
|
parallelProcessAudioFiles(
|
|
1204
1327
|
consecutiveFiles,
|
|
1205
1328
|
encoder,
|
|
1206
|
-
|
|
1329
|
+
outputSampleRate,
|
|
1207
1330
|
audioConfig.channelCount,
|
|
1208
1331
|
cache,
|
|
1209
1332
|
numThreads = optimalThreads
|
|
@@ -1218,7 +1341,7 @@ class AudioConcatModule(reactContext: ReactApplicationContext) :
|
|
|
1218
1341
|
streamEncodeSilence(
|
|
1219
1342
|
durationMs,
|
|
1220
1343
|
encoder,
|
|
1221
|
-
|
|
1344
|
+
outputSampleRate,
|
|
1222
1345
|
audioConfig.channelCount,
|
|
1223
1346
|
cache
|
|
1224
1347
|
)
|
|
@@ -1226,47 +1349,66 @@ class AudioConcatModule(reactContext: ReactApplicationContext) :
|
|
|
1226
1349
|
}
|
|
1227
1350
|
}
|
|
1228
1351
|
} else {
|
|
1229
|
-
Log.d("AudioConcat", "Using sequential processing for ${audioFileItems.size} audio files")
|
|
1352
|
+
Log.d("AudioConcat", "→ Using sequential processing for ${audioFileItems.size} audio files")
|
|
1230
1353
|
|
|
1231
|
-
//
|
|
1232
|
-
|
|
1233
|
-
|
|
1234
|
-
is AudioDataOrSilence.AudioFile -> {
|
|
1235
|
-
val filePath = item.filePath
|
|
1236
|
-
Log.d("AudioConcat", "Item $index: Streaming decode $filePath")
|
|
1354
|
+
// Create a reusable decoder for sequential processing
|
|
1355
|
+
val reusableDecoder = ReusableDecoder()
|
|
1356
|
+
Log.d("AudioConcat", "Created reusable decoder for sequential processing")
|
|
1237
1357
|
|
|
1238
|
-
|
|
1239
|
-
|
|
1240
|
-
|
|
1241
|
-
|
|
1242
|
-
|
|
1243
|
-
|
|
1244
|
-
|
|
1245
|
-
|
|
1246
|
-
|
|
1358
|
+
try {
|
|
1359
|
+
// Process each item sequentially (with decoder reuse)
|
|
1360
|
+
for ((index, item) in parsedData.withIndex()) {
|
|
1361
|
+
when (item) {
|
|
1362
|
+
is AudioDataOrSilence.AudioFile -> {
|
|
1363
|
+
val filePath = item.filePath
|
|
1364
|
+
Log.d("AudioConcat", "Item $index: Streaming decode $filePath")
|
|
1365
|
+
|
|
1366
|
+
val isLastFile = (index == parsedData.size - 1)
|
|
1367
|
+
streamDecodeAudioFile(
|
|
1368
|
+
filePath,
|
|
1369
|
+
encoder,
|
|
1370
|
+
isLastFile,
|
|
1371
|
+
outputSampleRate,
|
|
1372
|
+
audioConfig.channelCount,
|
|
1373
|
+
reusableDecoder
|
|
1374
|
+
)
|
|
1375
|
+
}
|
|
1247
1376
|
|
|
1248
|
-
|
|
1249
|
-
|
|
1250
|
-
|
|
1377
|
+
is AudioDataOrSilence.Silence -> {
|
|
1378
|
+
val durationMs = item.durationMs
|
|
1379
|
+
Log.d("AudioConcat", "Item $index: Streaming silence ${durationMs}ms")
|
|
1251
1380
|
|
|
1252
|
-
|
|
1253
|
-
|
|
1254
|
-
|
|
1255
|
-
|
|
1256
|
-
|
|
1257
|
-
|
|
1258
|
-
|
|
1381
|
+
streamEncodeSilence(
|
|
1382
|
+
durationMs,
|
|
1383
|
+
encoder,
|
|
1384
|
+
outputSampleRate,
|
|
1385
|
+
audioConfig.channelCount,
|
|
1386
|
+
cache
|
|
1387
|
+
)
|
|
1388
|
+
}
|
|
1259
1389
|
}
|
|
1260
1390
|
}
|
|
1391
|
+
} finally {
|
|
1392
|
+
// Release the reusable decoder when done
|
|
1393
|
+
reusableDecoder.release()
|
|
1394
|
+
Log.d("AudioConcat", "Released reusable decoder")
|
|
1261
1395
|
}
|
|
1262
1396
|
}
|
|
1263
1397
|
|
|
1398
|
+
val processingTime = System.currentTimeMillis() - processingStartTime
|
|
1399
|
+
Log.d("AudioConcat", "✓ Processing completed in ${processingTime}ms")
|
|
1400
|
+
|
|
1264
1401
|
// Finish encoding
|
|
1402
|
+
val encodingFinishStartTime = System.currentTimeMillis()
|
|
1265
1403
|
encoder.finish()
|
|
1404
|
+
val encodingFinishTime = System.currentTimeMillis() - encodingFinishStartTime
|
|
1405
|
+
Log.d("AudioConcat", "✓ Encoding finalized in ${encodingFinishTime}ms")
|
|
1266
1406
|
|
|
1267
1407
|
// Log cache statistics
|
|
1268
1408
|
Log.d("AudioConcat", "Cache statistics: ${cache.getStats()}")
|
|
1269
1409
|
|
|
1410
|
+
val totalTime = System.currentTimeMillis() - totalStartTime
|
|
1411
|
+
Log.d("AudioConcat", "========== Total Time: ${totalTime}ms (${totalTime / 1000.0}s) ==========")
|
|
1270
1412
|
Log.d("AudioConcat", "Successfully merged audio to $outputPath")
|
|
1271
1413
|
promise.resolve(outputPath)
|
|
1272
1414
|
|