react-native-audio-concat 0.5.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
@@ -160,6 +161,149 @@ class AudioConcatModule(reactContext: ReactApplicationContext) :
160
161
  }
161
162
  }
162
163
 
164
+ // Helper class to manage MediaCodec decoder reuse
165
+ private class ReusableDecoder {
166
+ private var decoder: MediaCodec? = null
167
+ private var currentMimeType: String? = null
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
+ }
222
+
223
+ fun getOrCreateDecoder(mimeType: String, format: MediaFormat): MediaCodec {
224
+ // Check if we can reuse the existing decoder
225
+ if (decoder != null && currentMimeType == mimeType && formatsCompatible(currentFormat, format)) {
226
+ // Flush the decoder to reset its state
227
+ try {
228
+ decoder!!.flush()
229
+ val type = if (isHardwareDecoder) "HW" else "SW"
230
+ Log.d("AudioConcat", " ↻ Reused $type decoder for $mimeType")
231
+ return decoder!!
232
+ } catch (e: Exception) {
233
+ Log.w("AudioConcat", "Failed to flush decoder, recreating: ${e.message}")
234
+ release()
235
+ }
236
+ }
237
+
238
+ // Need to create a new decoder
239
+ release() // Release old one if exists
240
+
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
+ }
253
+
254
+ decoder = newDecoder
255
+ currentMimeType = mimeType
256
+ currentFormat = format
257
+
258
+ return newDecoder
259
+ }
260
+
261
+ private fun formatsCompatible(format1: MediaFormat?, format2: MediaFormat): Boolean {
262
+ if (format1 == null) return false
263
+
264
+ // Check key format properties
265
+ return try {
266
+ format1.getInteger(MediaFormat.KEY_SAMPLE_RATE) == format2.getInteger(MediaFormat.KEY_SAMPLE_RATE) &&
267
+ format1.getInteger(MediaFormat.KEY_CHANNEL_COUNT) == format2.getInteger(MediaFormat.KEY_CHANNEL_COUNT)
268
+ } catch (e: Exception) {
269
+ false
270
+ }
271
+ }
272
+
273
+ fun release() {
274
+ decoder?.let {
275
+ try {
276
+ it.stop()
277
+ it.release()
278
+ } catch (e: Exception) {
279
+ Log.w("AudioConcat", "Error releasing decoder: ${e.message}")
280
+ }
281
+ }
282
+ decoder = null
283
+ currentMimeType = null
284
+ currentFormat = null
285
+ }
286
+ }
287
+
288
+ // Thread-safe decoder pool for parallel processing
289
+ private class DecoderPool {
290
+ private val decoders = ConcurrentHashMap<Long, ReusableDecoder>()
291
+
292
+ fun getDecoderForCurrentThread(): ReusableDecoder {
293
+ val threadId = Thread.currentThread().id
294
+ return decoders.getOrPut(threadId) {
295
+ Log.d("AudioConcat", " Created decoder for thread $threadId")
296
+ ReusableDecoder()
297
+ }
298
+ }
299
+
300
+ fun releaseAll() {
301
+ decoders.values.forEach { it.release() }
302
+ decoders.clear()
303
+ Log.d("AudioConcat", "Released all pooled decoders")
304
+ }
305
+ }
306
+
163
307
  private fun extractAudioConfig(filePath: String): AudioConfig {
164
308
  val extractor = MediaExtractor()
165
309
  try {
@@ -200,6 +344,10 @@ class AudioConcatModule(reactContext: ReactApplicationContext) :
200
344
  private val channelCount: Int
201
345
  private val maxChunkSize: Int
202
346
 
347
+ // Performance tracking
348
+ private var totalBufferWaitTimeMs = 0L
349
+ private var bufferWaitCount = 0
350
+
203
351
  init {
204
352
  this.sampleRate = sampleRate
205
353
  this.channelCount = channelCount
@@ -217,8 +365,9 @@ class AudioConcatModule(reactContext: ReactApplicationContext) :
217
365
  val samplesPerFrame = 1024
218
366
  val bytesPerSample = channelCount * 2 // 16-bit PCM
219
367
  val optimalBufferSize = samplesPerFrame * bytesPerSample
220
- // Use at least the optimal size, but allow for some overhead
221
- val bufferSize = (optimalBufferSize * 1.5).toInt().coerceAtLeast(16384)
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)
222
371
  outputFormat.setInteger(MediaFormat.KEY_MAX_INPUT_SIZE, bufferSize)
223
372
 
224
373
  // Store for use in encodePCMChunk
@@ -236,14 +385,23 @@ class AudioConcatModule(reactContext: ReactApplicationContext) :
236
385
  fun encodePCMChunk(pcmData: ByteArray, isLast: Boolean = false): Boolean {
237
386
  // Split large PCM data into smaller chunks that fit in encoder buffer (use configured size)
238
387
  var offset = 0
388
+ var buffersQueued = 0 // Track queued buffers for batch draining
239
389
 
240
390
  while (offset < pcmData.size) {
241
391
  val chunkSize = minOf(maxChunkSize, pcmData.size - offset)
242
392
  val isLastChunk = (offset + chunkSize >= pcmData.size) && isLast
243
393
 
244
394
  // Feed PCM data chunk to encoder (reduced timeout for better throughput)
395
+ val bufferWaitStart = System.currentTimeMillis()
245
396
  val inputBufferIndex = encoder.dequeueInputBuffer(1000)
397
+ val bufferWaitTime = System.currentTimeMillis() - bufferWaitStart
398
+
246
399
  if (inputBufferIndex >= 0) {
400
+ if (bufferWaitTime > 5) {
401
+ totalBufferWaitTimeMs += bufferWaitTime
402
+ bufferWaitCount++
403
+ }
404
+
247
405
  val inputBuffer = encoder.getInputBuffer(inputBufferIndex)!!
248
406
  val bufferCapacity = inputBuffer.capacity()
249
407
 
@@ -260,14 +418,20 @@ class AudioConcatModule(reactContext: ReactApplicationContext) :
260
418
  encoder.queueInputBuffer(inputBufferIndex, 0, actualChunkSize, presentationTimeUs, flags)
261
419
 
262
420
  offset += actualChunkSize
421
+ buffersQueued++
263
422
  } else {
423
+ totalBufferWaitTimeMs += bufferWaitTime
424
+ bufferWaitCount++
264
425
  // Buffer not available, drain first
265
426
  drainEncoder(false)
427
+ buffersQueued = 0 // Reset counter after forced drain
266
428
  }
267
429
 
268
- // Drain encoder output periodically
269
- if (offset < pcmData.size || !isLastChunk) {
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) {
270
433
  drainEncoder(false)
434
+ buffersQueued = 0
271
435
  }
272
436
  }
273
437
 
@@ -326,6 +490,14 @@ class AudioConcatModule(reactContext: ReactApplicationContext) :
326
490
  }
327
491
  }
328
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
+
329
501
  fun finish() {
330
502
  // Signal end of stream (reduced timeout)
331
503
  val inputBufferIndex = encoder.dequeueInputBuffer(1000)
@@ -336,6 +508,9 @@ class AudioConcatModule(reactContext: ReactApplicationContext) :
336
508
  // Drain remaining data
337
509
  drainEncoder(true)
338
510
 
511
+ // Log encoder performance stats
512
+ Log.d("AudioConcat", "Encoder stats: ${getEncoderStats()}")
513
+
339
514
  encoder.stop()
340
515
  encoder.release()
341
516
 
@@ -356,58 +531,48 @@ class AudioConcatModule(reactContext: ReactApplicationContext) :
356
531
  return input
357
532
  }
358
533
 
534
+ val startTime = System.currentTimeMillis()
359
535
  val inputSampleCount = input.size / (2 * channelCount) // 16-bit = 2 bytes per sample
360
536
  val outputSampleCount = (inputSampleCount.toLong() * outputSampleRate / inputSampleRate).toInt()
361
537
  val output = ByteArray(outputSampleCount * 2 * channelCount)
362
538
 
363
- // Use fixed-point arithmetic (16.16 format) to avoid floating-point operations
364
- // This provides 3-5x performance improvement
365
- val step = ((inputSampleRate.toLong() shl 16) / outputSampleRate).toInt()
366
- var srcPos = 0
539
+ // Helper function to read a sample with bounds checking
540
+ fun readSample(sampleIndex: Int, channel: Int): Int {
541
+ val clampedIndex = sampleIndex.coerceIn(0, inputSampleCount - 1)
542
+ val idx = (clampedIndex * channelCount + channel) * 2
543
+ val unsigned = (input[idx].toInt() and 0xFF) or (input[idx + 1].toInt() shl 8)
544
+ return if (unsigned > 32767) unsigned - 65536 else unsigned
545
+ }
367
546
 
368
- for (i in 0 until outputSampleCount) {
369
- val srcIndex = srcPos shr 16
370
- val fraction = srcPos and 0xFFFF // Fractional part in 16-bit fixed-point
547
+ // Use floating-point for better accuracy than fixed-point
548
+ val ratio = inputSampleRate.toDouble() / outputSampleRate.toDouble()
371
549
 
372
- // Boundary check: ensure we don't go beyond input array
373
- if (srcIndex >= inputSampleCount - 1) {
374
- break
375
- }
550
+ for (i in 0 until outputSampleCount) {
551
+ val srcPos = i * ratio
552
+ val srcIndex = srcPos.toInt()
553
+ val fraction = srcPos - srcIndex // Fractional part (0.0 to 1.0)
376
554
 
377
555
  for (ch in 0 until channelCount) {
378
- // Get current and next sample indices
379
- val idx1 = (srcIndex * channelCount + ch) * 2
380
- val idx2 = ((srcIndex + 1) * channelCount + ch) * 2
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
- }
556
+ // Linear interpolation with floating-point precision
557
+ val s1 = readSample(srcIndex, ch).toDouble()
558
+ val s2 = readSample(srcIndex + 1, ch).toDouble()
389
559
 
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
393
-
394
- // Linear interpolation using integer arithmetic
395
- // interpolated = s1 + (s2 - s1) * fraction
396
- // fraction is in 16.16 format, so we shift right by 16 after multiplication
397
- val interpolated = s1 + (((s2 - s1) * fraction) shr 16)
560
+ // Linear interpolation: s1 + (s2 - s1) * fraction
561
+ val interpolated = s1 + (s2 - s1) * fraction
398
562
 
399
563
  // Clamp to 16-bit range
400
- val clamped = interpolated.coerceIn(-32768, 32767)
564
+ val clamped = interpolated.toInt().coerceIn(-32768, 32767)
401
565
 
402
- // Convert back to unsigned and write (little-endian)
566
+ // Write to output (little-endian)
403
567
  val outIdx = (i * channelCount + ch) * 2
404
568
  output[outIdx] = (clamped and 0xFF).toByte()
405
569
  output[outIdx + 1] = (clamped shr 8).toByte()
406
570
  }
407
-
408
- srcPos += step
409
571
  }
410
572
 
573
+ val elapsedTime = System.currentTimeMillis() - startTime
574
+ Log.d("AudioConcat", " Resampled ${inputSampleRate}Hz→${outputSampleRate}Hz, ${input.size / 1024}KB→${output.size / 1024}KB in ${elapsedTime}ms")
575
+
411
576
  return output
412
577
  }
413
578
 
@@ -425,33 +590,107 @@ class AudioConcatModule(reactContext: ReactApplicationContext) :
425
590
 
426
591
  when {
427
592
  inputChannels == 1 && outputChannels == 2 -> {
428
- // Mono to Stereo: duplicate the channel
429
- for (i in 0 until sampleCount) {
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) {
430
633
  val srcIdx = i * 2
431
634
  val dstIdx = i * 4
432
635
  output[dstIdx] = input[srcIdx]
433
636
  output[dstIdx + 1] = input[srcIdx + 1]
434
637
  output[dstIdx + 2] = input[srcIdx]
435
638
  output[dstIdx + 3] = input[srcIdx + 1]
639
+ i++
436
640
  }
437
641
  }
438
642
  inputChannels == 2 && outputChannels == 1 -> {
439
- // Stereo to Mono: average the channels
440
- for (i in 0 until sampleCount) {
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) {
441
686
  val srcIdx = i * 4
442
687
  val dstIdx = i * 2
443
-
444
688
  val left = (input[srcIdx].toInt() and 0xFF) or (input[srcIdx + 1].toInt() shl 8)
445
689
  val right = (input[srcIdx + 2].toInt() and 0xFF) or (input[srcIdx + 3].toInt() shl 8)
446
-
447
- val leftSigned = if (left > 32767) left - 65536 else left
448
- val rightSigned = if (right > 32767) right - 65536 else right
449
-
450
- // Use bit shift instead of division for better performance (x / 2 = x >> 1)
451
- val avg = ((leftSigned + rightSigned) shr 1).coerceIn(-32768, 32767)
452
-
690
+ val avg = (((if (left > 32767) left - 65536 else left) + (if (right > 32767) right - 65536 else right)) shr 1)
453
691
  output[dstIdx] = (avg and 0xFF).toByte()
454
692
  output[dstIdx + 1] = (avg shr 8).toByte()
693
+ i++
455
694
  }
456
695
  }
457
696
  else -> {
@@ -477,7 +716,8 @@ class AudioConcatModule(reactContext: ReactApplicationContext) :
477
716
  targetSampleRate: Int,
478
717
  targetChannelCount: Int,
479
718
  latch: CountDownLatch,
480
- cache: PCMCache
719
+ cache: PCMCache,
720
+ decoderPool: DecoderPool? = null
481
721
  ) {
482
722
  try {
483
723
  // Check cache first
@@ -497,6 +737,7 @@ class AudioConcatModule(reactContext: ReactApplicationContext) :
497
737
  var decoder: MediaCodec? = null
498
738
  val decodedChunks = mutableListOf<ByteArray>()
499
739
  var totalBytes = 0L
740
+ val shouldReleaseDecoder = (decoderPool == null) // Only release if not using pool
500
741
 
501
742
  try {
502
743
  extractor.setDataSource(filePath)
@@ -531,9 +772,17 @@ class AudioConcatModule(reactContext: ReactApplicationContext) :
531
772
  extractor.selectTrack(audioTrackIndex)
532
773
 
533
774
  val mime = audioFormat.getString(MediaFormat.KEY_MIME)!!
534
- decoder = MediaCodec.createDecoderByType(mime)
535
- decoder.configure(audioFormat, null, null, 0)
536
- decoder.start()
775
+
776
+ // Use decoder pool if available, otherwise create new decoder
777
+ decoder = if (decoderPool != null) {
778
+ val reusableDecoder = decoderPool.getDecoderForCurrentThread()
779
+ reusableDecoder.getOrCreateDecoder(mime, audioFormat)
780
+ } else {
781
+ val newDecoder = MediaCodec.createDecoderByType(mime)
782
+ newDecoder.configure(audioFormat, null, null, 0)
783
+ newDecoder.start()
784
+ newDecoder
785
+ }
537
786
 
538
787
  val bufferInfo = MediaCodec.BufferInfo()
539
788
  var isEOS = false
@@ -596,8 +845,11 @@ class AudioConcatModule(reactContext: ReactApplicationContext) :
596
845
  }
597
846
 
598
847
  } finally {
599
- decoder?.stop()
600
- decoder?.release()
848
+ // Only stop/release decoder if not using pool
849
+ if (shouldReleaseDecoder) {
850
+ decoder?.stop()
851
+ decoder?.release()
852
+ }
601
853
  extractor.release()
602
854
  }
603
855
  } catch (e: Exception) {
@@ -613,10 +865,13 @@ class AudioConcatModule(reactContext: ReactApplicationContext) :
613
865
  encoder: StreamingEncoder,
614
866
  isLastFile: Boolean,
615
867
  targetSampleRate: Int,
616
- targetChannelCount: Int
868
+ targetChannelCount: Int,
869
+ reusableDecoder: ReusableDecoder? = null
617
870
  ) {
871
+ val startTime = System.currentTimeMillis()
618
872
  val extractor = MediaExtractor()
619
873
  var decoder: MediaCodec? = null
874
+ val shouldReleaseDecoder = (reusableDecoder == null) // Only release if not reusing
620
875
 
621
876
  try {
622
877
  extractor.setDataSource(filePath)
@@ -651,9 +906,16 @@ class AudioConcatModule(reactContext: ReactApplicationContext) :
651
906
  extractor.selectTrack(audioTrackIndex)
652
907
 
653
908
  val mime = audioFormat.getString(MediaFormat.KEY_MIME)!!
654
- decoder = MediaCodec.createDecoderByType(mime)
655
- decoder.configure(audioFormat, null, null, 0)
656
- decoder.start()
909
+
910
+ // Use reusable decoder if provided, otherwise create a new one
911
+ decoder = if (reusableDecoder != null) {
912
+ reusableDecoder.getOrCreateDecoder(mime, audioFormat)
913
+ } else {
914
+ val newDecoder = MediaCodec.createDecoderByType(mime)
915
+ newDecoder.configure(audioFormat, null, null, 0)
916
+ newDecoder.start()
917
+ newDecoder
918
+ }
657
919
 
658
920
  val bufferInfo = MediaCodec.BufferInfo()
659
921
  var isEOS = false
@@ -706,9 +968,22 @@ class AudioConcatModule(reactContext: ReactApplicationContext) :
706
968
  }
707
969
 
708
970
  } finally {
709
- decoder?.stop()
710
- decoder?.release()
971
+ // Only stop/release decoder if we created it locally (not reusing)
972
+ if (shouldReleaseDecoder) {
973
+ decoder?.stop()
974
+ decoder?.release()
975
+ }
711
976
  extractor.release()
977
+
978
+ // Performance metrics
979
+ val elapsedTime = System.currentTimeMillis() - startTime
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)")
712
987
  }
713
988
  }
714
989
 
@@ -833,6 +1108,10 @@ class AudioConcatModule(reactContext: ReactApplicationContext) :
833
1108
  val latch = CountDownLatch(optimizedFiles.size)
834
1109
  val sequenceCounter = AtomicInteger(0)
835
1110
 
1111
+ // Create decoder pool for reuse across threads
1112
+ val decoderPool = DecoderPool()
1113
+ Log.d("AudioConcat", "Created decoder pool for parallel processing ($numThreads threads)")
1114
+
836
1115
  try {
837
1116
  // Submit decode tasks for unique files only
838
1117
  optimizedFiles.forEachIndexed { optIndex, (index, filePath) ->
@@ -842,7 +1121,7 @@ class AudioConcatModule(reactContext: ReactApplicationContext) :
842
1121
  sequenceCounter.addAndGet(1000000)
843
1122
 
844
1123
  Log.d("AudioConcat", "Starting parallel decode [$index]: $filePath")
845
- parallelDecodeToQueue(filePath, pcmQueue, fileSequenceStart, targetSampleRate, targetChannelCount, latch, cache)
1124
+ parallelDecodeToQueue(filePath, pcmQueue, fileSequenceStart, targetSampleRate, targetChannelCount, latch, cache, decoderPool)
846
1125
 
847
1126
  // Mark end with duplicate count
848
1127
  val repeatCount = consecutiveDuplicates[optIndex] ?: 1
@@ -900,6 +1179,7 @@ class AudioConcatModule(reactContext: ReactApplicationContext) :
900
1179
  Log.d("AudioConcat", "All parallel decode tasks completed")
901
1180
 
902
1181
  } finally {
1182
+ decoderPool.releaseAll()
903
1183
  executor.shutdown()
904
1184
  }
905
1185
  }
@@ -1032,17 +1312,24 @@ class AudioConcatModule(reactContext: ReactApplicationContext) :
1032
1312
  }
1033
1313
 
1034
1314
  override fun concatAudioFiles(data: ReadableArray, outputPath: String, promise: Promise) {
1315
+ val totalStartTime = System.currentTimeMillis()
1316
+ Log.d("AudioConcat", "========== Audio Concat Started ==========")
1317
+
1035
1318
  try {
1036
1319
  if (data.size() == 0) {
1037
1320
  promise.reject("EMPTY_DATA", "Data array is empty")
1038
1321
  return
1039
1322
  }
1040
1323
 
1324
+ // Parse data
1325
+ val parseStartTime = System.currentTimeMillis()
1041
1326
  val parsedData = parseAudioData(data)
1042
- Log.d("AudioConcat", "Streaming merge of ${parsedData.size} items")
1327
+ val parseTime = System.currentTimeMillis() - parseStartTime
1328
+ Log.d("AudioConcat", "✓ Parsed ${parsedData.size} items in ${parseTime}ms")
1043
1329
  Log.d("AudioConcat", "Output: $outputPath")
1044
1330
 
1045
1331
  // Get audio config from first audio file
1332
+ val configStartTime = System.currentTimeMillis()
1046
1333
  var audioConfig: AudioConfig? = null
1047
1334
  for (item in parsedData) {
1048
1335
  if (item is AudioDataOrSilence.AudioFile) {
@@ -1056,13 +1343,33 @@ class AudioConcatModule(reactContext: ReactApplicationContext) :
1056
1343
  return
1057
1344
  }
1058
1345
 
1059
- Log.d("AudioConcat", "Audio config: ${audioConfig.sampleRate}Hz, ${audioConfig.channelCount}ch, ${audioConfig.bitRate}bps")
1346
+ val configTime = System.currentTimeMillis() - configStartTime
1347
+
1348
+ // Force output sample rate to 24kHz for optimal performance
1349
+ val outputSampleRate = 24000
1350
+ Log.d("AudioConcat", "✓ Extracted audio config in ${configTime}ms: ${audioConfig.channelCount}ch, ${audioConfig.bitRate}bps")
1351
+ Log.d("AudioConcat", "Output sample rate: ${outputSampleRate}Hz (24kHz optimized)")
1352
+
1353
+ // Create modified config with fixed sample rate
1354
+ val outputAudioConfig = AudioConfig(outputSampleRate, audioConfig.channelCount, audioConfig.bitRate)
1060
1355
 
1061
1356
  // Analyze duplicates to determine cache strategy
1062
- val duplicateAnalysis = analyzeDuplicates(parsedData, audioConfig)
1357
+ val analysisStartTime = System.currentTimeMillis()
1358
+ val duplicateAnalysis = analyzeDuplicates(parsedData, outputAudioConfig)
1359
+ val analysisTime = System.currentTimeMillis() - analysisStartTime
1360
+ Log.d("AudioConcat", "✓ Analyzed duplicates in ${analysisTime}ms")
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()
1063
1370
 
1064
1371
  // Create cache instance with intelligent caching strategy
1065
- val cache = PCMCache(duplicateAnalysis.duplicateFiles, duplicateAnalysis.duplicateSilence)
1372
+ val cache = PCMCache(filesToCache, duplicateAnalysis.duplicateSilence)
1066
1373
 
1067
1374
  // Delete existing output file
1068
1375
  val outputFile = File(outputPath)
@@ -1070,9 +1377,9 @@ class AudioConcatModule(reactContext: ReactApplicationContext) :
1070
1377
  outputFile.delete()
1071
1378
  }
1072
1379
 
1073
- // Create streaming encoder
1380
+ // Create streaming encoder with fixed 24kHz sample rate
1074
1381
  val encoder = StreamingEncoder(
1075
- audioConfig.sampleRate,
1382
+ outputSampleRate,
1076
1383
  audioConfig.channelCount,
1077
1384
  audioConfig.bitRate,
1078
1385
  outputPath
@@ -1094,11 +1401,160 @@ class AudioConcatModule(reactContext: ReactApplicationContext) :
1094
1401
  }
1095
1402
  }
1096
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
+
1097
1550
  // Decide whether to use parallel or sequential processing
1098
- val useParallel = audioFileItems.size >= 10 // Use parallel for 10+ files
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)
1553
+ val processingStartTime = System.currentTimeMillis()
1099
1554
 
1100
1555
  if (useParallel) {
1101
- Log.d("AudioConcat", "Using parallel processing for ${audioFileItems.size} audio files")
1556
+ val cpuCores = Runtime.getRuntime().availableProcessors()
1557
+ Log.d("AudioConcat", "→ Using PARALLEL processing for ${audioFileItems.size} audio files (CPU cores: $cpuCores)")
1102
1558
 
1103
1559
  // Process interleaved patterns optimally
1104
1560
  val processedIndices = mutableSetOf<Int>()
@@ -1121,7 +1577,7 @@ class AudioConcatModule(reactContext: ReactApplicationContext) :
1121
1577
  val latch = CountDownLatch(1)
1122
1578
  val seqStart = AtomicInteger(0)
1123
1579
 
1124
- parallelDecodeToQueue(filePath, tempQueue, seqStart, audioConfig.sampleRate, audioConfig.channelCount, latch, cache)
1580
+ parallelDecodeToQueue(filePath, tempQueue, seqStart, outputSampleRate, audioConfig.channelCount, latch, cache)
1125
1581
 
1126
1582
  // Collect chunks
1127
1583
  var collecting = true
@@ -1151,10 +1607,32 @@ class AudioConcatModule(reactContext: ReactApplicationContext) :
1151
1607
  }
1152
1608
 
1153
1609
  // Encode the pattern: file -> silence -> file -> silence -> ...
1154
- repeat(pattern.repeatCount) { iteration ->
1155
- // Encode file
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
1156
1618
  pcmChunks.forEach { chunk ->
1157
- encoder.encodePCMChunk(chunk, false)
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
+ }
1158
1636
  }
1159
1637
 
1160
1638
  // Encode silence (except after the last file)
@@ -1163,6 +1641,9 @@ class AudioConcatModule(reactContext: ReactApplicationContext) :
1163
1641
  }
1164
1642
  }
1165
1643
 
1644
+ val patternTime = System.currentTimeMillis() - patternStartTime
1645
+ Log.d("AudioConcat", " Encoded interleaved pattern in ${patternTime}ms")
1646
+
1166
1647
  // Mark these indices as processed
1167
1648
  pattern.indices.forEach { idx ->
1168
1649
  processedIndices.add(idx)
@@ -1198,16 +1679,71 @@ class AudioConcatModule(reactContext: ReactApplicationContext) :
1198
1679
  }
1199
1680
 
1200
1681
  if (consecutiveFiles.isNotEmpty()) {
1201
- val optimalThreads = getOptimalThreadCount(consecutiveFiles.size)
1202
- Log.d("AudioConcat", "Using $optimalThreads threads for ${consecutiveFiles.size} files (CPU cores: ${Runtime.getRuntime().availableProcessors()})")
1203
- parallelProcessAudioFiles(
1204
- consecutiveFiles,
1205
- encoder,
1206
- audioConfig.sampleRate,
1207
- audioConfig.channelCount,
1208
- cache,
1209
- numThreads = optimalThreads
1210
- )
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
+ }
1211
1747
  audioFileIdx = currentIdx
1212
1748
  }
1213
1749
  }
@@ -1218,7 +1754,7 @@ class AudioConcatModule(reactContext: ReactApplicationContext) :
1218
1754
  streamEncodeSilence(
1219
1755
  durationMs,
1220
1756
  encoder,
1221
- audioConfig.sampleRate,
1757
+ outputSampleRate,
1222
1758
  audioConfig.channelCount,
1223
1759
  cache
1224
1760
  )
@@ -1226,47 +1762,66 @@ class AudioConcatModule(reactContext: ReactApplicationContext) :
1226
1762
  }
1227
1763
  }
1228
1764
  } else {
1229
- Log.d("AudioConcat", "Using sequential processing for ${audioFileItems.size} audio files")
1765
+ Log.d("AudioConcat", "Using sequential processing for ${audioFileItems.size} audio files")
1230
1766
 
1231
- // Process each item sequentially (original behavior)
1232
- for ((index, item) in parsedData.withIndex()) {
1233
- when (item) {
1234
- is AudioDataOrSilence.AudioFile -> {
1235
- val filePath = item.filePath
1236
- Log.d("AudioConcat", "Item $index: Streaming decode $filePath")
1767
+ // Create a reusable decoder for sequential processing
1768
+ val reusableDecoder = ReusableDecoder()
1769
+ Log.d("AudioConcat", "Created reusable decoder for sequential processing")
1237
1770
 
1238
- val isLastFile = (index == parsedData.size - 1)
1239
- streamDecodeAudioFile(
1240
- filePath,
1241
- encoder,
1242
- isLastFile,
1243
- audioConfig.sampleRate,
1244
- audioConfig.channelCount
1245
- )
1246
- }
1771
+ try {
1772
+ // Process each item sequentially (with decoder reuse)
1773
+ for ((index, item) in parsedData.withIndex()) {
1774
+ when (item) {
1775
+ is AudioDataOrSilence.AudioFile -> {
1776
+ val filePath = item.filePath
1777
+ Log.d("AudioConcat", "Item $index: Streaming decode $filePath")
1778
+
1779
+ val isLastFile = (index == parsedData.size - 1)
1780
+ streamDecodeAudioFile(
1781
+ filePath,
1782
+ encoder,
1783
+ isLastFile,
1784
+ outputSampleRate,
1785
+ audioConfig.channelCount,
1786
+ reusableDecoder
1787
+ )
1788
+ }
1247
1789
 
1248
- is AudioDataOrSilence.Silence -> {
1249
- val durationMs = item.durationMs
1250
- Log.d("AudioConcat", "Item $index: Streaming silence ${durationMs}ms")
1790
+ is AudioDataOrSilence.Silence -> {
1791
+ val durationMs = item.durationMs
1792
+ Log.d("AudioConcat", "Item $index: Streaming silence ${durationMs}ms")
1251
1793
 
1252
- streamEncodeSilence(
1253
- durationMs,
1254
- encoder,
1255
- audioConfig.sampleRate,
1256
- audioConfig.channelCount,
1257
- cache
1258
- )
1794
+ streamEncodeSilence(
1795
+ durationMs,
1796
+ encoder,
1797
+ outputSampleRate,
1798
+ audioConfig.channelCount,
1799
+ cache
1800
+ )
1801
+ }
1259
1802
  }
1260
1803
  }
1804
+ } finally {
1805
+ // Release the reusable decoder when done
1806
+ reusableDecoder.release()
1807
+ Log.d("AudioConcat", "Released reusable decoder")
1261
1808
  }
1262
1809
  }
1263
1810
 
1811
+ val processingTime = System.currentTimeMillis() - processingStartTime
1812
+ Log.d("AudioConcat", "✓ Processing completed in ${processingTime}ms")
1813
+
1264
1814
  // Finish encoding
1815
+ val encodingFinishStartTime = System.currentTimeMillis()
1265
1816
  encoder.finish()
1817
+ val encodingFinishTime = System.currentTimeMillis() - encodingFinishStartTime
1818
+ Log.d("AudioConcat", "✓ Encoding finalized in ${encodingFinishTime}ms")
1266
1819
 
1267
1820
  // Log cache statistics
1268
1821
  Log.d("AudioConcat", "Cache statistics: ${cache.getStats()}")
1269
1822
 
1823
+ val totalTime = System.currentTimeMillis() - totalStartTime
1824
+ Log.d("AudioConcat", "========== Total Time: ${totalTime}ms (${totalTime / 1000.0}s) ==========")
1270
1825
  Log.d("AudioConcat", "Successfully merged audio to $outputPath")
1271
1826
  promise.resolve(outputPath)
1272
1827
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "react-native-audio-concat",
3
- "version": "0.5.0",
3
+ "version": "0.7.0",
4
4
  "description": "audio-concat for react-native",
5
5
  "main": "./lib/module/index.js",
6
6
  "types": "./lib/typescript/src/index.d.ts",