react-native-audio-concat 0.6.0 → 0.7.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.
|
@@ -7,6 +7,7 @@ import com.facebook.react.bridge.ReadableMap
|
|
|
7
7
|
import com.facebook.react.module.annotations.ReactModule
|
|
8
8
|
import android.media.MediaCodec
|
|
9
9
|
import android.media.MediaCodecInfo
|
|
10
|
+
import android.media.MediaCodecList
|
|
10
11
|
import android.media.MediaExtractor
|
|
11
12
|
import android.media.MediaFormat
|
|
12
13
|
import android.media.MediaMuxer
|
|
@@ -165,6 +166,59 @@ class AudioConcatModule(reactContext: ReactApplicationContext) :
|
|
|
165
166
|
private var decoder: MediaCodec? = null
|
|
166
167
|
private var currentMimeType: String? = null
|
|
167
168
|
private var currentFormat: MediaFormat? = null
|
|
169
|
+
private var isHardwareDecoder: Boolean = false
|
|
170
|
+
|
|
171
|
+
/**
|
|
172
|
+
* Try to create a hardware decoder for better performance
|
|
173
|
+
* Hardware decoders are typically 2-10x faster than software decoders
|
|
174
|
+
*/
|
|
175
|
+
private fun createHardwareDecoder(mimeType: String, format: MediaFormat): MediaCodec? {
|
|
176
|
+
try {
|
|
177
|
+
val codecList = MediaCodecList(MediaCodecList.REGULAR_CODECS)
|
|
178
|
+
|
|
179
|
+
for (codecInfo in codecList.codecInfos) {
|
|
180
|
+
// Skip encoders
|
|
181
|
+
if (codecInfo.isEncoder) continue
|
|
182
|
+
|
|
183
|
+
// Check if this codec supports our mime type
|
|
184
|
+
if (!codecInfo.supportedTypes.any { it.equals(mimeType, ignoreCase = true) }) {
|
|
185
|
+
continue
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
// Hardware decoder identification by vendor prefix
|
|
189
|
+
val isHardware = codecInfo.name.let { name ->
|
|
190
|
+
name.startsWith("OMX.qcom") || // Qualcomm (most common)
|
|
191
|
+
name.startsWith("OMX.MTK") || // MediaTek
|
|
192
|
+
name.startsWith("OMX.Exynos") || // Samsung Exynos
|
|
193
|
+
name.startsWith("OMX.SEC") || // Samsung
|
|
194
|
+
name.startsWith("OMX.hisi") || // Huawei HiSilicon
|
|
195
|
+
name.startsWith("c2.qti") || // Qualcomm C2
|
|
196
|
+
name.startsWith("c2.mtk") || // MediaTek C2
|
|
197
|
+
name.startsWith("c2.exynos") || // Samsung C2
|
|
198
|
+
(name.contains("hardware", ignoreCase = true) &&
|
|
199
|
+
!name.contains("google", ignoreCase = true))
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
if (isHardware) {
|
|
203
|
+
try {
|
|
204
|
+
val codec = MediaCodec.createByCodecName(codecInfo.name)
|
|
205
|
+
codec.configure(format, null, null, 0)
|
|
206
|
+
codec.start()
|
|
207
|
+
|
|
208
|
+
Log.d("AudioConcat", " ✓ Created HARDWARE decoder: ${codecInfo.name}")
|
|
209
|
+
return codec
|
|
210
|
+
} catch (e: Exception) {
|
|
211
|
+
Log.w("AudioConcat", " ✗ HW decoder ${codecInfo.name} failed: ${e.message}")
|
|
212
|
+
// Continue to try next hardware decoder
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
} catch (e: Exception) {
|
|
217
|
+
Log.w("AudioConcat", " Hardware decoder search failed: ${e.message}")
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
return null
|
|
221
|
+
}
|
|
168
222
|
|
|
169
223
|
fun getOrCreateDecoder(mimeType: String, format: MediaFormat): MediaCodec {
|
|
170
224
|
// Check if we can reuse the existing decoder
|
|
@@ -172,7 +226,8 @@ class AudioConcatModule(reactContext: ReactApplicationContext) :
|
|
|
172
226
|
// Flush the decoder to reset its state
|
|
173
227
|
try {
|
|
174
228
|
decoder!!.flush()
|
|
175
|
-
|
|
229
|
+
val type = if (isHardwareDecoder) "HW" else "SW"
|
|
230
|
+
Log.d("AudioConcat", " ↻ Reused $type decoder for $mimeType")
|
|
176
231
|
return decoder!!
|
|
177
232
|
} catch (e: Exception) {
|
|
178
233
|
Log.w("AudioConcat", "Failed to flush decoder, recreating: ${e.message}")
|
|
@@ -183,15 +238,23 @@ class AudioConcatModule(reactContext: ReactApplicationContext) :
|
|
|
183
238
|
// Need to create a new decoder
|
|
184
239
|
release() // Release old one if exists
|
|
185
240
|
|
|
186
|
-
|
|
187
|
-
newDecoder
|
|
188
|
-
newDecoder
|
|
241
|
+
// Try hardware decoder first (2-10x faster)
|
|
242
|
+
var newDecoder = createHardwareDecoder(mimeType, format)
|
|
243
|
+
isHardwareDecoder = (newDecoder != null)
|
|
244
|
+
|
|
245
|
+
// Fallback to software decoder
|
|
246
|
+
if (newDecoder == null) {
|
|
247
|
+
newDecoder = MediaCodec.createDecoderByType(mimeType)
|
|
248
|
+
newDecoder.configure(format, null, null, 0)
|
|
249
|
+
newDecoder.start()
|
|
250
|
+
Log.d("AudioConcat", " ⚠ Created SOFTWARE decoder for $mimeType (no HW available)")
|
|
251
|
+
isHardwareDecoder = false
|
|
252
|
+
}
|
|
189
253
|
|
|
190
254
|
decoder = newDecoder
|
|
191
255
|
currentMimeType = mimeType
|
|
192
256
|
currentFormat = format
|
|
193
257
|
|
|
194
|
-
Log.d("AudioConcat", " Created new decoder for $mimeType")
|
|
195
258
|
return newDecoder
|
|
196
259
|
}
|
|
197
260
|
|
|
@@ -281,6 +344,10 @@ class AudioConcatModule(reactContext: ReactApplicationContext) :
|
|
|
281
344
|
private val channelCount: Int
|
|
282
345
|
private val maxChunkSize: Int
|
|
283
346
|
|
|
347
|
+
// Performance tracking
|
|
348
|
+
private var totalBufferWaitTimeMs = 0L
|
|
349
|
+
private var bufferWaitCount = 0
|
|
350
|
+
|
|
284
351
|
init {
|
|
285
352
|
this.sampleRate = sampleRate
|
|
286
353
|
this.channelCount = channelCount
|
|
@@ -298,8 +365,9 @@ class AudioConcatModule(reactContext: ReactApplicationContext) :
|
|
|
298
365
|
val samplesPerFrame = 1024
|
|
299
366
|
val bytesPerSample = channelCount * 2 // 16-bit PCM
|
|
300
367
|
val optimalBufferSize = samplesPerFrame * bytesPerSample
|
|
301
|
-
//
|
|
302
|
-
|
|
368
|
+
// OPTIMIZATION: Increased buffer size for better throughput
|
|
369
|
+
// Larger buffers reduce dequeue operations and improve encoder efficiency
|
|
370
|
+
val bufferSize = (optimalBufferSize * 4.0).toInt().coerceAtLeast(65536)
|
|
303
371
|
outputFormat.setInteger(MediaFormat.KEY_MAX_INPUT_SIZE, bufferSize)
|
|
304
372
|
|
|
305
373
|
// Store for use in encodePCMChunk
|
|
@@ -317,14 +385,23 @@ class AudioConcatModule(reactContext: ReactApplicationContext) :
|
|
|
317
385
|
fun encodePCMChunk(pcmData: ByteArray, isLast: Boolean = false): Boolean {
|
|
318
386
|
// Split large PCM data into smaller chunks that fit in encoder buffer (use configured size)
|
|
319
387
|
var offset = 0
|
|
388
|
+
var buffersQueued = 0 // Track queued buffers for batch draining
|
|
320
389
|
|
|
321
390
|
while (offset < pcmData.size) {
|
|
322
391
|
val chunkSize = minOf(maxChunkSize, pcmData.size - offset)
|
|
323
392
|
val isLastChunk = (offset + chunkSize >= pcmData.size) && isLast
|
|
324
393
|
|
|
325
394
|
// Feed PCM data chunk to encoder (reduced timeout for better throughput)
|
|
395
|
+
val bufferWaitStart = System.currentTimeMillis()
|
|
326
396
|
val inputBufferIndex = encoder.dequeueInputBuffer(1000)
|
|
397
|
+
val bufferWaitTime = System.currentTimeMillis() - bufferWaitStart
|
|
398
|
+
|
|
327
399
|
if (inputBufferIndex >= 0) {
|
|
400
|
+
if (bufferWaitTime > 5) {
|
|
401
|
+
totalBufferWaitTimeMs += bufferWaitTime
|
|
402
|
+
bufferWaitCount++
|
|
403
|
+
}
|
|
404
|
+
|
|
328
405
|
val inputBuffer = encoder.getInputBuffer(inputBufferIndex)!!
|
|
329
406
|
val bufferCapacity = inputBuffer.capacity()
|
|
330
407
|
|
|
@@ -341,14 +418,20 @@ class AudioConcatModule(reactContext: ReactApplicationContext) :
|
|
|
341
418
|
encoder.queueInputBuffer(inputBufferIndex, 0, actualChunkSize, presentationTimeUs, flags)
|
|
342
419
|
|
|
343
420
|
offset += actualChunkSize
|
|
421
|
+
buffersQueued++
|
|
344
422
|
} else {
|
|
423
|
+
totalBufferWaitTimeMs += bufferWaitTime
|
|
424
|
+
bufferWaitCount++
|
|
345
425
|
// Buffer not available, drain first
|
|
346
426
|
drainEncoder(false)
|
|
427
|
+
buffersQueued = 0 // Reset counter after forced drain
|
|
347
428
|
}
|
|
348
429
|
|
|
349
|
-
//
|
|
350
|
-
|
|
430
|
+
// OPTIMIZATION: Batch drain - only drain every 4 buffers instead of every buffer
|
|
431
|
+
// This reduces overhead while keeping encoder pipeline flowing
|
|
432
|
+
if (buffersQueued >= 4 || isLastChunk || offset >= pcmData.size) {
|
|
351
433
|
drainEncoder(false)
|
|
434
|
+
buffersQueued = 0
|
|
352
435
|
}
|
|
353
436
|
}
|
|
354
437
|
|
|
@@ -407,6 +490,14 @@ class AudioConcatModule(reactContext: ReactApplicationContext) :
|
|
|
407
490
|
}
|
|
408
491
|
}
|
|
409
492
|
|
|
493
|
+
fun getEncoderStats(): String {
|
|
494
|
+
val avgWaitTime = if (bufferWaitCount > 0) {
|
|
495
|
+
String.format("%.2f", totalBufferWaitTimeMs.toFloat() / bufferWaitCount)
|
|
496
|
+
} else "0.00"
|
|
497
|
+
|
|
498
|
+
return "Buffer waits: $bufferWaitCount, Total wait: ${totalBufferWaitTimeMs}ms, Avg: ${avgWaitTime}ms"
|
|
499
|
+
}
|
|
500
|
+
|
|
410
501
|
fun finish() {
|
|
411
502
|
// Signal end of stream (reduced timeout)
|
|
412
503
|
val inputBufferIndex = encoder.dequeueInputBuffer(1000)
|
|
@@ -417,6 +508,9 @@ class AudioConcatModule(reactContext: ReactApplicationContext) :
|
|
|
417
508
|
// Drain remaining data
|
|
418
509
|
drainEncoder(true)
|
|
419
510
|
|
|
511
|
+
// Log encoder performance stats
|
|
512
|
+
Log.d("AudioConcat", "Encoder stats: ${getEncoderStats()}")
|
|
513
|
+
|
|
420
514
|
encoder.stop()
|
|
421
515
|
encoder.release()
|
|
422
516
|
|
|
@@ -496,33 +590,107 @@ class AudioConcatModule(reactContext: ReactApplicationContext) :
|
|
|
496
590
|
|
|
497
591
|
when {
|
|
498
592
|
inputChannels == 1 && outputChannels == 2 -> {
|
|
499
|
-
// Mono to Stereo
|
|
500
|
-
|
|
593
|
+
// OPTIMIZED: Mono to Stereo using batch copy with unrolled loop
|
|
594
|
+
// Process 4 samples at a time for better cache locality
|
|
595
|
+
val batchSize = 4
|
|
596
|
+
val fullBatches = sampleCount / batchSize
|
|
597
|
+
var i = 0
|
|
598
|
+
|
|
599
|
+
// Process batches of 4 samples
|
|
600
|
+
for (batch in 0 until fullBatches) {
|
|
601
|
+
val baseIdx = i * 2
|
|
602
|
+
val baseDst = i * 4
|
|
603
|
+
|
|
604
|
+
// Sample 1
|
|
605
|
+
output[baseDst] = input[baseIdx]
|
|
606
|
+
output[baseDst + 1] = input[baseIdx + 1]
|
|
607
|
+
output[baseDst + 2] = input[baseIdx]
|
|
608
|
+
output[baseDst + 3] = input[baseIdx + 1]
|
|
609
|
+
|
|
610
|
+
// Sample 2
|
|
611
|
+
output[baseDst + 4] = input[baseIdx + 2]
|
|
612
|
+
output[baseDst + 5] = input[baseIdx + 3]
|
|
613
|
+
output[baseDst + 6] = input[baseIdx + 2]
|
|
614
|
+
output[baseDst + 7] = input[baseIdx + 3]
|
|
615
|
+
|
|
616
|
+
// Sample 3
|
|
617
|
+
output[baseDst + 8] = input[baseIdx + 4]
|
|
618
|
+
output[baseDst + 9] = input[baseIdx + 5]
|
|
619
|
+
output[baseDst + 10] = input[baseIdx + 4]
|
|
620
|
+
output[baseDst + 11] = input[baseIdx + 5]
|
|
621
|
+
|
|
622
|
+
// Sample 4
|
|
623
|
+
output[baseDst + 12] = input[baseIdx + 6]
|
|
624
|
+
output[baseDst + 13] = input[baseIdx + 7]
|
|
625
|
+
output[baseDst + 14] = input[baseIdx + 6]
|
|
626
|
+
output[baseDst + 15] = input[baseIdx + 7]
|
|
627
|
+
|
|
628
|
+
i += batchSize
|
|
629
|
+
}
|
|
630
|
+
|
|
631
|
+
// Process remaining samples
|
|
632
|
+
while (i < sampleCount) {
|
|
501
633
|
val srcIdx = i * 2
|
|
502
634
|
val dstIdx = i * 4
|
|
503
635
|
output[dstIdx] = input[srcIdx]
|
|
504
636
|
output[dstIdx + 1] = input[srcIdx + 1]
|
|
505
637
|
output[dstIdx + 2] = input[srcIdx]
|
|
506
638
|
output[dstIdx + 3] = input[srcIdx + 1]
|
|
639
|
+
i++
|
|
507
640
|
}
|
|
508
641
|
}
|
|
509
642
|
inputChannels == 2 && outputChannels == 1 -> {
|
|
510
|
-
// Stereo to Mono
|
|
511
|
-
|
|
643
|
+
// OPTIMIZED: Stereo to Mono with unrolled loop
|
|
644
|
+
val batchSize = 4
|
|
645
|
+
val fullBatches = sampleCount / batchSize
|
|
646
|
+
var i = 0
|
|
647
|
+
|
|
648
|
+
// Process batches of 4 samples
|
|
649
|
+
for (batch in 0 until fullBatches) {
|
|
650
|
+
val baseSrc = i * 4
|
|
651
|
+
val baseDst = i * 2
|
|
652
|
+
|
|
653
|
+
// Sample 1
|
|
654
|
+
var left = (input[baseSrc].toInt() and 0xFF) or (input[baseSrc + 1].toInt() shl 8)
|
|
655
|
+
var right = (input[baseSrc + 2].toInt() and 0xFF) or (input[baseSrc + 3].toInt() shl 8)
|
|
656
|
+
var avg = (((if (left > 32767) left - 65536 else left) + (if (right > 32767) right - 65536 else right)) shr 1)
|
|
657
|
+
output[baseDst] = (avg and 0xFF).toByte()
|
|
658
|
+
output[baseDst + 1] = (avg shr 8).toByte()
|
|
659
|
+
|
|
660
|
+
// Sample 2
|
|
661
|
+
left = (input[baseSrc + 4].toInt() and 0xFF) or (input[baseSrc + 5].toInt() shl 8)
|
|
662
|
+
right = (input[baseSrc + 6].toInt() and 0xFF) or (input[baseSrc + 7].toInt() shl 8)
|
|
663
|
+
avg = (((if (left > 32767) left - 65536 else left) + (if (right > 32767) right - 65536 else right)) shr 1)
|
|
664
|
+
output[baseDst + 2] = (avg and 0xFF).toByte()
|
|
665
|
+
output[baseDst + 3] = (avg shr 8).toByte()
|
|
666
|
+
|
|
667
|
+
// Sample 3
|
|
668
|
+
left = (input[baseSrc + 8].toInt() and 0xFF) or (input[baseSrc + 9].toInt() shl 8)
|
|
669
|
+
right = (input[baseSrc + 10].toInt() and 0xFF) or (input[baseSrc + 11].toInt() shl 8)
|
|
670
|
+
avg = (((if (left > 32767) left - 65536 else left) + (if (right > 32767) right - 65536 else right)) shr 1)
|
|
671
|
+
output[baseDst + 4] = (avg and 0xFF).toByte()
|
|
672
|
+
output[baseDst + 5] = (avg shr 8).toByte()
|
|
673
|
+
|
|
674
|
+
// Sample 4
|
|
675
|
+
left = (input[baseSrc + 12].toInt() and 0xFF) or (input[baseSrc + 13].toInt() shl 8)
|
|
676
|
+
right = (input[baseSrc + 14].toInt() and 0xFF) or (input[baseSrc + 15].toInt() shl 8)
|
|
677
|
+
avg = (((if (left > 32767) left - 65536 else left) + (if (right > 32767) right - 65536 else right)) shr 1)
|
|
678
|
+
output[baseDst + 6] = (avg and 0xFF).toByte()
|
|
679
|
+
output[baseDst + 7] = (avg shr 8).toByte()
|
|
680
|
+
|
|
681
|
+
i += batchSize
|
|
682
|
+
}
|
|
683
|
+
|
|
684
|
+
// Process remaining samples
|
|
685
|
+
while (i < sampleCount) {
|
|
512
686
|
val srcIdx = i * 4
|
|
513
687
|
val dstIdx = i * 2
|
|
514
|
-
|
|
515
688
|
val left = (input[srcIdx].toInt() and 0xFF) or (input[srcIdx + 1].toInt() shl 8)
|
|
516
689
|
val right = (input[srcIdx + 2].toInt() and 0xFF) or (input[srcIdx + 3].toInt() shl 8)
|
|
517
|
-
|
|
518
|
-
val leftSigned = if (left > 32767) left - 65536 else left
|
|
519
|
-
val rightSigned = if (right > 32767) right - 65536 else right
|
|
520
|
-
|
|
521
|
-
// Use bit shift instead of division for better performance (x / 2 = x >> 1)
|
|
522
|
-
val avg = ((leftSigned + rightSigned) shr 1).coerceIn(-32768, 32767)
|
|
523
|
-
|
|
690
|
+
val avg = (((if (left > 32767) left - 65536 else left) + (if (right > 32767) right - 65536 else right)) shr 1)
|
|
524
691
|
output[dstIdx] = (avg and 0xFF).toByte()
|
|
525
692
|
output[dstIdx + 1] = (avg shr 8).toByte()
|
|
693
|
+
i++
|
|
526
694
|
}
|
|
527
695
|
}
|
|
528
696
|
else -> {
|
|
@@ -806,8 +974,16 @@ class AudioConcatModule(reactContext: ReactApplicationContext) :
|
|
|
806
974
|
decoder?.release()
|
|
807
975
|
}
|
|
808
976
|
extractor.release()
|
|
977
|
+
|
|
978
|
+
// Performance metrics
|
|
809
979
|
val elapsedTime = System.currentTimeMillis() - startTime
|
|
810
|
-
|
|
980
|
+
val fileSize = try { File(filePath).length() } catch (e: Exception) { 0L }
|
|
981
|
+
val fileSizeKB = fileSize / 1024
|
|
982
|
+
val decodingSpeedMBps = if (elapsedTime > 0) {
|
|
983
|
+
(fileSize / 1024.0 / 1024.0) / (elapsedTime / 1000.0)
|
|
984
|
+
} else 0.0
|
|
985
|
+
|
|
986
|
+
Log.d("AudioConcat", " ⚡ Decoded ${fileSizeKB}KB in ${elapsedTime}ms (${String.format("%.2f", decodingSpeedMBps)} MB/s)")
|
|
811
987
|
}
|
|
812
988
|
}
|
|
813
989
|
|
|
@@ -1183,8 +1359,17 @@ class AudioConcatModule(reactContext: ReactApplicationContext) :
|
|
|
1183
1359
|
val analysisTime = System.currentTimeMillis() - analysisStartTime
|
|
1184
1360
|
Log.d("AudioConcat", "✓ Analyzed duplicates in ${analysisTime}ms")
|
|
1185
1361
|
|
|
1362
|
+
// Collect all unique audio files for pre-decode caching
|
|
1363
|
+
val allAudioFiles = parsedData.filterIsInstance<AudioDataOrSilence.AudioFile>()
|
|
1364
|
+
.map { it.filePath }
|
|
1365
|
+
.distinct()
|
|
1366
|
+
|
|
1367
|
+
// Merge duplicate files with all unique files for comprehensive caching
|
|
1368
|
+
// This ensures pre-decoded files are always cached, regardless of occurrence count
|
|
1369
|
+
val filesToCache = (duplicateAnalysis.duplicateFiles + allAudioFiles).toSet()
|
|
1370
|
+
|
|
1186
1371
|
// Create cache instance with intelligent caching strategy
|
|
1187
|
-
val cache = PCMCache(
|
|
1372
|
+
val cache = PCMCache(filesToCache, duplicateAnalysis.duplicateSilence)
|
|
1188
1373
|
|
|
1189
1374
|
// Delete existing output file
|
|
1190
1375
|
val outputFile = File(outputPath)
|
|
@@ -1216,12 +1401,160 @@ class AudioConcatModule(reactContext: ReactApplicationContext) :
|
|
|
1216
1401
|
}
|
|
1217
1402
|
}
|
|
1218
1403
|
|
|
1404
|
+
// PRE-DECODE: Parallel decode all unique audio files to cache before processing
|
|
1405
|
+
val uniqueAudioFiles = audioFileItems.map { it.second }.distinct()
|
|
1406
|
+
val filesToPreDecode = uniqueAudioFiles.filter { cache.getAudioFile(it) == null }
|
|
1407
|
+
|
|
1408
|
+
if (filesToPreDecode.isNotEmpty()) {
|
|
1409
|
+
val preDecodeStartTime = System.currentTimeMillis()
|
|
1410
|
+
val cpuCores = Runtime.getRuntime().availableProcessors()
|
|
1411
|
+
val preDecodeThreads = getOptimalThreadCount(filesToPreDecode.size)
|
|
1412
|
+
|
|
1413
|
+
Log.d("AudioConcat", "→ PRE-DECODE: ${filesToPreDecode.size} unique files using $preDecodeThreads threads (CPU cores: $cpuCores)")
|
|
1414
|
+
|
|
1415
|
+
val preDecodeExecutor = Executors.newFixedThreadPool(preDecodeThreads)
|
|
1416
|
+
val preDecodeLatch = CountDownLatch(filesToPreDecode.size)
|
|
1417
|
+
val preDecodePool = DecoderPool()
|
|
1418
|
+
|
|
1419
|
+
try {
|
|
1420
|
+
filesToPreDecode.forEach { filePath ->
|
|
1421
|
+
preDecodeExecutor.submit {
|
|
1422
|
+
try {
|
|
1423
|
+
Log.d("AudioConcat", " Pre-decoding: $filePath")
|
|
1424
|
+
|
|
1425
|
+
// Decode directly to cache without intermediate queue
|
|
1426
|
+
val extractor = MediaExtractor()
|
|
1427
|
+
var totalBytes = 0L
|
|
1428
|
+
val decodedChunks = mutableListOf<ByteArray>()
|
|
1429
|
+
|
|
1430
|
+
try {
|
|
1431
|
+
extractor.setDataSource(filePath)
|
|
1432
|
+
|
|
1433
|
+
var audioTrackIndex = -1
|
|
1434
|
+
var audioFormat: MediaFormat? = null
|
|
1435
|
+
|
|
1436
|
+
for (i in 0 until extractor.trackCount) {
|
|
1437
|
+
val format = extractor.getTrackFormat(i)
|
|
1438
|
+
val mime = format.getString(MediaFormat.KEY_MIME) ?: continue
|
|
1439
|
+
if (mime.startsWith("audio/")) {
|
|
1440
|
+
audioTrackIndex = i
|
|
1441
|
+
audioFormat = format
|
|
1442
|
+
break
|
|
1443
|
+
}
|
|
1444
|
+
}
|
|
1445
|
+
|
|
1446
|
+
if (audioTrackIndex == -1 || audioFormat == null) {
|
|
1447
|
+
throw Exception("No audio track found in $filePath")
|
|
1448
|
+
}
|
|
1449
|
+
|
|
1450
|
+
val sourceSampleRate = audioFormat.getInteger(MediaFormat.KEY_SAMPLE_RATE)
|
|
1451
|
+
val sourceChannelCount = audioFormat.getInteger(MediaFormat.KEY_CHANNEL_COUNT)
|
|
1452
|
+
|
|
1453
|
+
val needsResampling = sourceSampleRate != outputSampleRate
|
|
1454
|
+
val needsChannelConversion = sourceChannelCount != audioConfig.channelCount
|
|
1455
|
+
|
|
1456
|
+
if (needsResampling || needsChannelConversion) {
|
|
1457
|
+
Log.d("AudioConcat", " Parallel decode: $filePath - ${sourceSampleRate}Hz ${sourceChannelCount}ch -> ${outputSampleRate}Hz ${audioConfig.channelCount}ch")
|
|
1458
|
+
}
|
|
1459
|
+
|
|
1460
|
+
extractor.selectTrack(audioTrackIndex)
|
|
1461
|
+
|
|
1462
|
+
val mime = audioFormat.getString(MediaFormat.KEY_MIME)!!
|
|
1463
|
+
val reusableDecoder = preDecodePool.getDecoderForCurrentThread()
|
|
1464
|
+
val decoder = reusableDecoder.getOrCreateDecoder(mime, audioFormat)
|
|
1465
|
+
|
|
1466
|
+
val bufferInfo = MediaCodec.BufferInfo()
|
|
1467
|
+
var isEOS = false
|
|
1468
|
+
|
|
1469
|
+
while (!isEOS) {
|
|
1470
|
+
// Feed input
|
|
1471
|
+
val inputBufferIndex = decoder.dequeueInputBuffer(1000)
|
|
1472
|
+
if (inputBufferIndex >= 0) {
|
|
1473
|
+
val inputBuffer = decoder.getInputBuffer(inputBufferIndex)!!
|
|
1474
|
+
val sampleSize = extractor.readSampleData(inputBuffer, 0)
|
|
1475
|
+
|
|
1476
|
+
if (sampleSize < 0) {
|
|
1477
|
+
decoder.queueInputBuffer(inputBufferIndex, 0, 0, 0, MediaCodec.BUFFER_FLAG_END_OF_STREAM)
|
|
1478
|
+
} else {
|
|
1479
|
+
val presentationTimeUs = extractor.sampleTime
|
|
1480
|
+
decoder.queueInputBuffer(inputBufferIndex, 0, sampleSize, presentationTimeUs, 0)
|
|
1481
|
+
extractor.advance()
|
|
1482
|
+
}
|
|
1483
|
+
}
|
|
1484
|
+
|
|
1485
|
+
// Get output
|
|
1486
|
+
val outputBufferIndex = decoder.dequeueOutputBuffer(bufferInfo, 1000)
|
|
1487
|
+
if (outputBufferIndex >= 0) {
|
|
1488
|
+
val outputBuffer = decoder.getOutputBuffer(outputBufferIndex)!!
|
|
1489
|
+
|
|
1490
|
+
if (bufferInfo.size > 0) {
|
|
1491
|
+
var pcmData = ByteArray(bufferInfo.size)
|
|
1492
|
+
outputBuffer.get(pcmData)
|
|
1493
|
+
|
|
1494
|
+
// Convert channel count if needed
|
|
1495
|
+
if (needsChannelConversion) {
|
|
1496
|
+
pcmData = convertChannelCount(pcmData, sourceChannelCount, audioConfig.channelCount)
|
|
1497
|
+
}
|
|
1498
|
+
|
|
1499
|
+
// Resample if needed
|
|
1500
|
+
if (needsResampling) {
|
|
1501
|
+
pcmData = resamplePCM16(pcmData, sourceSampleRate, outputSampleRate, audioConfig.channelCount)
|
|
1502
|
+
}
|
|
1503
|
+
|
|
1504
|
+
decodedChunks.add(pcmData)
|
|
1505
|
+
totalBytes += pcmData.size
|
|
1506
|
+
}
|
|
1507
|
+
|
|
1508
|
+
decoder.releaseOutputBuffer(outputBufferIndex, false)
|
|
1509
|
+
|
|
1510
|
+
if ((bufferInfo.flags and MediaCodec.BUFFER_FLAG_END_OF_STREAM) != 0) {
|
|
1511
|
+
isEOS = true
|
|
1512
|
+
}
|
|
1513
|
+
}
|
|
1514
|
+
}
|
|
1515
|
+
|
|
1516
|
+
// Cache the decoded data
|
|
1517
|
+
if (decodedChunks.isNotEmpty()) {
|
|
1518
|
+
cache.putAudioFile(filePath, CachedPCMData(decodedChunks, totalBytes))
|
|
1519
|
+
}
|
|
1520
|
+
|
|
1521
|
+
} finally {
|
|
1522
|
+
extractor.release()
|
|
1523
|
+
}
|
|
1524
|
+
|
|
1525
|
+
} catch (e: Exception) {
|
|
1526
|
+
Log.e("AudioConcat", "Error pre-decoding $filePath: ${e.message}", e)
|
|
1527
|
+
} finally {
|
|
1528
|
+
preDecodeLatch.countDown()
|
|
1529
|
+
}
|
|
1530
|
+
}
|
|
1531
|
+
}
|
|
1532
|
+
|
|
1533
|
+
// Wait for all pre-decoding to complete
|
|
1534
|
+
preDecodeLatch.await()
|
|
1535
|
+
preDecodePool.releaseAll()
|
|
1536
|
+
preDecodeExecutor.shutdown()
|
|
1537
|
+
|
|
1538
|
+
val preDecodeTime = System.currentTimeMillis() - preDecodeStartTime
|
|
1539
|
+
Log.d("AudioConcat", "✓ Pre-decode completed in ${preDecodeTime}ms")
|
|
1540
|
+
|
|
1541
|
+
} catch (e: Exception) {
|
|
1542
|
+
Log.e("AudioConcat", "Error during pre-decode: ${e.message}", e)
|
|
1543
|
+
preDecodePool.releaseAll()
|
|
1544
|
+
preDecodeExecutor.shutdown()
|
|
1545
|
+
}
|
|
1546
|
+
} else {
|
|
1547
|
+
Log.d("AudioConcat", "→ All audio files already cached, skipping pre-decode")
|
|
1548
|
+
}
|
|
1549
|
+
|
|
1219
1550
|
// Decide whether to use parallel or sequential processing
|
|
1220
|
-
|
|
1551
|
+
// Parallel processing is beneficial even with few files due to multi-core CPUs
|
|
1552
|
+
val useParallel = audioFileItems.size >= 3 // Use parallel for 3+ files (was 10)
|
|
1221
1553
|
val processingStartTime = System.currentTimeMillis()
|
|
1222
1554
|
|
|
1223
1555
|
if (useParallel) {
|
|
1224
|
-
|
|
1556
|
+
val cpuCores = Runtime.getRuntime().availableProcessors()
|
|
1557
|
+
Log.d("AudioConcat", "→ Using PARALLEL processing for ${audioFileItems.size} audio files (CPU cores: $cpuCores)")
|
|
1225
1558
|
|
|
1226
1559
|
// Process interleaved patterns optimally
|
|
1227
1560
|
val processedIndices = mutableSetOf<Int>()
|
|
@@ -1274,10 +1607,32 @@ class AudioConcatModule(reactContext: ReactApplicationContext) :
|
|
|
1274
1607
|
}
|
|
1275
1608
|
|
|
1276
1609
|
// Encode the pattern: file -> silence -> file -> silence -> ...
|
|
1277
|
-
|
|
1278
|
-
|
|
1610
|
+
// OPTIMIZATION: Batch chunks to reduce encoder call overhead
|
|
1611
|
+
val patternStartTime = System.currentTimeMillis()
|
|
1612
|
+
|
|
1613
|
+
// Combine all chunks from the file into a single buffer
|
|
1614
|
+
val combinedFileBuffer = if (pcmChunks.size > 10) {
|
|
1615
|
+
val totalSize = pcmChunks.sumOf { it.size }
|
|
1616
|
+
val buffer = ByteArray(totalSize)
|
|
1617
|
+
var offset = 0
|
|
1279
1618
|
pcmChunks.forEach { chunk ->
|
|
1280
|
-
|
|
1619
|
+
System.arraycopy(chunk, 0, buffer, offset, chunk.size)
|
|
1620
|
+
offset += chunk.size
|
|
1621
|
+
}
|
|
1622
|
+
Log.d("AudioConcat", " Batched ${pcmChunks.size} chunks into single buffer (${totalSize / 1024}KB)")
|
|
1623
|
+
buffer
|
|
1624
|
+
} else {
|
|
1625
|
+
null
|
|
1626
|
+
}
|
|
1627
|
+
|
|
1628
|
+
repeat(pattern.repeatCount) { iteration ->
|
|
1629
|
+
// Encode file (batched or individual chunks)
|
|
1630
|
+
if (combinedFileBuffer != null) {
|
|
1631
|
+
encoder.encodePCMChunk(combinedFileBuffer, false)
|
|
1632
|
+
} else {
|
|
1633
|
+
pcmChunks.forEach { chunk ->
|
|
1634
|
+
encoder.encodePCMChunk(chunk, false)
|
|
1635
|
+
}
|
|
1281
1636
|
}
|
|
1282
1637
|
|
|
1283
1638
|
// Encode silence (except after the last file)
|
|
@@ -1286,6 +1641,9 @@ class AudioConcatModule(reactContext: ReactApplicationContext) :
|
|
|
1286
1641
|
}
|
|
1287
1642
|
}
|
|
1288
1643
|
|
|
1644
|
+
val patternTime = System.currentTimeMillis() - patternStartTime
|
|
1645
|
+
Log.d("AudioConcat", " Encoded interleaved pattern in ${patternTime}ms")
|
|
1646
|
+
|
|
1289
1647
|
// Mark these indices as processed
|
|
1290
1648
|
pattern.indices.forEach { idx ->
|
|
1291
1649
|
processedIndices.add(idx)
|
|
@@ -1321,16 +1679,71 @@ class AudioConcatModule(reactContext: ReactApplicationContext) :
|
|
|
1321
1679
|
}
|
|
1322
1680
|
|
|
1323
1681
|
if (consecutiveFiles.isNotEmpty()) {
|
|
1324
|
-
|
|
1325
|
-
|
|
1326
|
-
|
|
1327
|
-
|
|
1328
|
-
|
|
1329
|
-
|
|
1330
|
-
|
|
1331
|
-
|
|
1332
|
-
|
|
1333
|
-
|
|
1682
|
+
// OPTIMIZATION: Fast path for cached files - avoid thread pool overhead
|
|
1683
|
+
val allCached = consecutiveFiles.all { (_, filePath) -> cache.getAudioFile(filePath) != null }
|
|
1684
|
+
|
|
1685
|
+
if (allCached) {
|
|
1686
|
+
// Direct encoding from cache without parallel processing overhead
|
|
1687
|
+
val startTime = System.currentTimeMillis()
|
|
1688
|
+
Log.d("AudioConcat", "Fast path: encoding ${consecutiveFiles.size} cached files directly")
|
|
1689
|
+
|
|
1690
|
+
consecutiveFiles.forEach { (itemIdx, filePath) ->
|
|
1691
|
+
val cachedData = cache.getAudioFile(filePath)!!
|
|
1692
|
+
val chunkCount = cachedData.chunks.size
|
|
1693
|
+
|
|
1694
|
+
Log.d("AudioConcat", " File[$itemIdx]: ${cachedData.totalBytes / 1024}KB in $chunkCount chunks")
|
|
1695
|
+
|
|
1696
|
+
val encodeStartTime = System.currentTimeMillis()
|
|
1697
|
+
|
|
1698
|
+
// OPTIMIZATION: Batch all chunks into single buffer to reduce encoder call overhead
|
|
1699
|
+
// Instead of 300+ calls at ~2.5ms each, make 1 call
|
|
1700
|
+
if (chunkCount > 10) {
|
|
1701
|
+
// Many small chunks - combine into single buffer for massive speedup
|
|
1702
|
+
val batchStartTime = System.currentTimeMillis()
|
|
1703
|
+
val combinedBuffer = ByteArray(cachedData.totalBytes.toInt())
|
|
1704
|
+
var offset = 0
|
|
1705
|
+
|
|
1706
|
+
cachedData.chunks.forEach { chunk ->
|
|
1707
|
+
System.arraycopy(chunk, 0, combinedBuffer, offset, chunk.size)
|
|
1708
|
+
offset += chunk.size
|
|
1709
|
+
}
|
|
1710
|
+
|
|
1711
|
+
val batchTime = System.currentTimeMillis() - batchStartTime
|
|
1712
|
+
Log.d("AudioConcat", " Batched $chunkCount chunks in ${batchTime}ms")
|
|
1713
|
+
|
|
1714
|
+
// Single encoder call instead of 300+
|
|
1715
|
+
encoder.encodePCMChunk(combinedBuffer, false)
|
|
1716
|
+
} else {
|
|
1717
|
+
// Few chunks - encode directly (rare case)
|
|
1718
|
+
cachedData.chunks.forEach { chunk ->
|
|
1719
|
+
encoder.encodePCMChunk(chunk, false)
|
|
1720
|
+
}
|
|
1721
|
+
}
|
|
1722
|
+
|
|
1723
|
+
val encodeTime = System.currentTimeMillis() - encodeStartTime
|
|
1724
|
+
val throughputMBps = if (encodeTime > 0) {
|
|
1725
|
+
(cachedData.totalBytes / 1024.0 / 1024.0) / (encodeTime / 1000.0)
|
|
1726
|
+
} else 0.0
|
|
1727
|
+
|
|
1728
|
+
Log.d("AudioConcat", " Encoded in ${encodeTime}ms (${String.format("%.2f", throughputMBps)} MB/s)")
|
|
1729
|
+
}
|
|
1730
|
+
|
|
1731
|
+
val elapsedTime = System.currentTimeMillis() - startTime
|
|
1732
|
+
val totalKB = consecutiveFiles.sumOf { (_, filePath) -> cache.getAudioFile(filePath)!!.totalBytes } / 1024
|
|
1733
|
+
Log.d("AudioConcat", " ⚡ Encoded ${consecutiveFiles.size} cached files (${totalKB}KB) in ${elapsedTime}ms")
|
|
1734
|
+
} else {
|
|
1735
|
+
// Standard parallel processing for non-cached files
|
|
1736
|
+
val optimalThreads = getOptimalThreadCount(consecutiveFiles.size)
|
|
1737
|
+
Log.d("AudioConcat", "Using $optimalThreads threads for ${consecutiveFiles.size} files (CPU cores: ${Runtime.getRuntime().availableProcessors()})")
|
|
1738
|
+
parallelProcessAudioFiles(
|
|
1739
|
+
consecutiveFiles,
|
|
1740
|
+
encoder,
|
|
1741
|
+
outputSampleRate,
|
|
1742
|
+
audioConfig.channelCount,
|
|
1743
|
+
cache,
|
|
1744
|
+
numThreads = optimalThreads
|
|
1745
|
+
)
|
|
1746
|
+
}
|
|
1334
1747
|
audioFileIdx = currentIdx
|
|
1335
1748
|
}
|
|
1336
1749
|
}
|