react-native-audio-concat 0.4.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.
|
@@ -57,15 +57,61 @@ class AudioConcatModule(reactContext: ReactApplicationContext) :
|
|
|
57
57
|
val channelCount: Int
|
|
58
58
|
)
|
|
59
59
|
|
|
60
|
+
// Buffer pool for silence generation to reduce memory allocations
|
|
61
|
+
private object SilenceBufferPool {
|
|
62
|
+
private val pool = ConcurrentHashMap<Int, ByteArray>()
|
|
63
|
+
private val standardSizes = listOf(4096, 8192, 16384, 32768, 65536, 131072)
|
|
64
|
+
|
|
65
|
+
init {
|
|
66
|
+
// Pre-allocate common silence buffer sizes
|
|
67
|
+
standardSizes.forEach { size ->
|
|
68
|
+
pool[size] = ByteArray(size)
|
|
69
|
+
}
|
|
70
|
+
Log.d("AudioConcat", "SilenceBufferPool initialized with ${standardSizes.size} standard sizes")
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
fun getBuffer(requestedSize: Int): ByteArray {
|
|
74
|
+
// Find the smallest standard size that fits the request
|
|
75
|
+
val standardSize = standardSizes.firstOrNull { it >= requestedSize }
|
|
76
|
+
|
|
77
|
+
return if (standardSize != null) {
|
|
78
|
+
// Return pooled buffer (already zeroed)
|
|
79
|
+
pool.getOrPut(standardSize) { ByteArray(standardSize) }
|
|
80
|
+
} else {
|
|
81
|
+
// Size too large for pool, create new buffer
|
|
82
|
+
ByteArray(requestedSize)
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
fun clear() {
|
|
87
|
+
pool.clear()
|
|
88
|
+
Log.d("AudioConcat", "SilenceBufferPool cleared")
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
60
92
|
private class PCMCache(
|
|
61
93
|
private val shouldCacheFile: Set<String>,
|
|
62
94
|
private val shouldCacheSilence: Set<SilenceCacheKey>
|
|
63
95
|
) {
|
|
64
96
|
private val audioFileCache = ConcurrentHashMap<String, CachedPCMData>()
|
|
65
97
|
private val silenceCache = ConcurrentHashMap<SilenceCacheKey, ByteArray>()
|
|
66
|
-
private val maxCacheSizeMB = 100 // Limit cache to 100MB
|
|
67
98
|
private var currentCacheSizeBytes = 0L
|
|
68
99
|
|
|
100
|
+
// Dynamic cache size based on available memory
|
|
101
|
+
private val maxCacheSizeBytes: Long
|
|
102
|
+
get() {
|
|
103
|
+
val runtime = Runtime.getRuntime()
|
|
104
|
+
val maxMemory = runtime.maxMemory()
|
|
105
|
+
val usedMemory = runtime.totalMemory() - runtime.freeMemory()
|
|
106
|
+
val availableMemory = maxMemory - usedMemory
|
|
107
|
+
|
|
108
|
+
// Use 20% of available memory for cache, but constrain between 50MB and 200MB
|
|
109
|
+
val dynamicCacheMB = (availableMemory / (1024 * 1024) * 0.2).toLong()
|
|
110
|
+
val cacheMB = dynamicCacheMB.coerceIn(50, 200)
|
|
111
|
+
|
|
112
|
+
return cacheMB * 1024 * 1024
|
|
113
|
+
}
|
|
114
|
+
|
|
69
115
|
fun getAudioFile(filePath: String): CachedPCMData? {
|
|
70
116
|
return audioFileCache[filePath]
|
|
71
117
|
}
|
|
@@ -76,15 +122,16 @@ class AudioConcatModule(reactContext: ReactApplicationContext) :
|
|
|
76
122
|
return
|
|
77
123
|
}
|
|
78
124
|
|
|
79
|
-
// Check cache size limit
|
|
80
|
-
if (currentCacheSizeBytes + data.totalBytes >
|
|
81
|
-
|
|
125
|
+
// Check cache size limit (dynamic)
|
|
126
|
+
if (currentCacheSizeBytes + data.totalBytes > maxCacheSizeBytes) {
|
|
127
|
+
val maxCacheMB = maxCacheSizeBytes / (1024 * 1024)
|
|
128
|
+
Log.d("AudioConcat", "Cache full ($maxCacheMB MB), not caching: $filePath")
|
|
82
129
|
return
|
|
83
130
|
}
|
|
84
131
|
|
|
85
132
|
audioFileCache[filePath] = data
|
|
86
133
|
currentCacheSizeBytes += data.totalBytes
|
|
87
|
-
Log.d("AudioConcat", "Cached audio file: $filePath (${data.totalBytes / 1024}KB)")
|
|
134
|
+
Log.d("AudioConcat", "Cached audio file: $filePath (${data.totalBytes / 1024}KB, total: ${currentCacheSizeBytes / 1024}KB)")
|
|
88
135
|
}
|
|
89
136
|
|
|
90
137
|
fun getSilence(key: SilenceCacheKey): ByteArray? {
|
|
@@ -113,6 +160,87 @@ class AudioConcatModule(reactContext: ReactApplicationContext) :
|
|
|
113
160
|
}
|
|
114
161
|
}
|
|
115
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
|
+
|
|
116
244
|
private fun extractAudioConfig(filePath: String): AudioConfig {
|
|
117
245
|
val extractor = MediaExtractor()
|
|
118
246
|
try {
|
|
@@ -151,6 +279,7 @@ class AudioConcatModule(reactContext: ReactApplicationContext) :
|
|
|
151
279
|
private var totalPresentationTimeUs = 0L
|
|
152
280
|
private val sampleRate: Int
|
|
153
281
|
private val channelCount: Int
|
|
282
|
+
private val maxChunkSize: Int
|
|
154
283
|
|
|
155
284
|
init {
|
|
156
285
|
this.sampleRate = sampleRate
|
|
@@ -163,7 +292,20 @@ class AudioConcatModule(reactContext: ReactApplicationContext) :
|
|
|
163
292
|
)
|
|
164
293
|
outputFormat.setInteger(MediaFormat.KEY_AAC_PROFILE, MediaCodecInfo.CodecProfileLevel.AACObjectLC)
|
|
165
294
|
outputFormat.setInteger(MediaFormat.KEY_BIT_RATE, bitRate)
|
|
166
|
-
|
|
295
|
+
|
|
296
|
+
// Optimized buffer size based on audio parameters
|
|
297
|
+
// Target: ~1024 samples per frame for optimal AAC encoding
|
|
298
|
+
val samplesPerFrame = 1024
|
|
299
|
+
val bytesPerSample = channelCount * 2 // 16-bit PCM
|
|
300
|
+
val optimalBufferSize = samplesPerFrame * bytesPerSample
|
|
301
|
+
// Use at least the optimal size, but allow for some overhead
|
|
302
|
+
val bufferSize = (optimalBufferSize * 1.5).toInt().coerceAtLeast(16384)
|
|
303
|
+
outputFormat.setInteger(MediaFormat.KEY_MAX_INPUT_SIZE, bufferSize)
|
|
304
|
+
|
|
305
|
+
// Store for use in encodePCMChunk
|
|
306
|
+
this.maxChunkSize = bufferSize
|
|
307
|
+
|
|
308
|
+
Log.d("AudioConcat", "Encoder buffer size: $bufferSize bytes (${samplesPerFrame} samples, ${sampleRate}Hz, ${channelCount}ch)")
|
|
167
309
|
|
|
168
310
|
encoder = MediaCodec.createEncoderByType(MediaFormat.MIMETYPE_AUDIO_AAC)
|
|
169
311
|
encoder.configure(outputFormat, null, null, MediaCodec.CONFIGURE_FLAG_ENCODE)
|
|
@@ -173,16 +315,15 @@ class AudioConcatModule(reactContext: ReactApplicationContext) :
|
|
|
173
315
|
}
|
|
174
316
|
|
|
175
317
|
fun encodePCMChunk(pcmData: ByteArray, isLast: Boolean = false): Boolean {
|
|
176
|
-
// Split large PCM data into smaller chunks that fit in encoder buffer
|
|
177
|
-
val maxChunkSize = 65536 // Match KEY_MAX_INPUT_SIZE
|
|
318
|
+
// Split large PCM data into smaller chunks that fit in encoder buffer (use configured size)
|
|
178
319
|
var offset = 0
|
|
179
320
|
|
|
180
321
|
while (offset < pcmData.size) {
|
|
181
322
|
val chunkSize = minOf(maxChunkSize, pcmData.size - offset)
|
|
182
323
|
val isLastChunk = (offset + chunkSize >= pcmData.size) && isLast
|
|
183
324
|
|
|
184
|
-
// Feed PCM data chunk to encoder
|
|
185
|
-
val inputBufferIndex = encoder.dequeueInputBuffer(
|
|
325
|
+
// Feed PCM data chunk to encoder (reduced timeout for better throughput)
|
|
326
|
+
val inputBufferIndex = encoder.dequeueInputBuffer(1000)
|
|
186
327
|
if (inputBufferIndex >= 0) {
|
|
187
328
|
val inputBuffer = encoder.getInputBuffer(inputBufferIndex)!!
|
|
188
329
|
val bufferCapacity = inputBuffer.capacity()
|
|
@@ -221,7 +362,8 @@ class AudioConcatModule(reactContext: ReactApplicationContext) :
|
|
|
221
362
|
|
|
222
363
|
private fun drainEncoder(endOfStream: Boolean) {
|
|
223
364
|
while (true) {
|
|
224
|
-
|
|
365
|
+
// Use shorter timeout for better responsiveness
|
|
366
|
+
val outputBufferIndex = encoder.dequeueOutputBuffer(bufferInfo, if (endOfStream) 1000 else 0)
|
|
225
367
|
|
|
226
368
|
when (outputBufferIndex) {
|
|
227
369
|
MediaCodec.INFO_OUTPUT_FORMAT_CHANGED -> {
|
|
@@ -266,8 +408,8 @@ class AudioConcatModule(reactContext: ReactApplicationContext) :
|
|
|
266
408
|
}
|
|
267
409
|
|
|
268
410
|
fun finish() {
|
|
269
|
-
// Signal end of stream
|
|
270
|
-
val inputBufferIndex = encoder.dequeueInputBuffer(
|
|
411
|
+
// Signal end of stream (reduced timeout)
|
|
412
|
+
val inputBufferIndex = encoder.dequeueInputBuffer(1000)
|
|
271
413
|
if (inputBufferIndex >= 0) {
|
|
272
414
|
encoder.queueInputBuffer(inputBufferIndex, 0, 0, 0, MediaCodec.BUFFER_FLAG_END_OF_STREAM)
|
|
273
415
|
}
|
|
@@ -295,51 +437,48 @@ class AudioConcatModule(reactContext: ReactApplicationContext) :
|
|
|
295
437
|
return input
|
|
296
438
|
}
|
|
297
439
|
|
|
440
|
+
val startTime = System.currentTimeMillis()
|
|
298
441
|
val inputSampleCount = input.size / (2 * channelCount) // 16-bit = 2 bytes per sample
|
|
299
442
|
val outputSampleCount = (inputSampleCount.toLong() * outputSampleRate / inputSampleRate).toInt()
|
|
300
443
|
val output = ByteArray(outputSampleCount * 2 * channelCount)
|
|
301
444
|
|
|
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
|
+
}
|
|
452
|
+
|
|
453
|
+
// Use floating-point for better accuracy than fixed-point
|
|
302
454
|
val ratio = inputSampleRate.toDouble() / outputSampleRate.toDouble()
|
|
303
455
|
|
|
304
456
|
for (i in 0 until outputSampleCount) {
|
|
305
457
|
val srcPos = i * ratio
|
|
306
458
|
val srcIndex = srcPos.toInt()
|
|
307
|
-
val fraction = srcPos - srcIndex
|
|
459
|
+
val fraction = srcPos - srcIndex // Fractional part (0.0 to 1.0)
|
|
308
460
|
|
|
309
461
|
for (ch in 0 until channelCount) {
|
|
310
|
-
//
|
|
311
|
-
val
|
|
312
|
-
val
|
|
313
|
-
|
|
314
|
-
val sample1 = if (idx1 + 1 < input.size) {
|
|
315
|
-
(input[idx1].toInt() and 0xFF) or (input[idx1 + 1].toInt() shl 8)
|
|
316
|
-
} else {
|
|
317
|
-
0
|
|
318
|
-
}
|
|
462
|
+
// Linear interpolation with floating-point precision
|
|
463
|
+
val s1 = readSample(srcIndex, ch).toDouble()
|
|
464
|
+
val s2 = readSample(srcIndex + 1, ch).toDouble()
|
|
319
465
|
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
} else {
|
|
323
|
-
sample1
|
|
324
|
-
}
|
|
325
|
-
|
|
326
|
-
// Convert to signed 16-bit
|
|
327
|
-
val s1 = if (sample1 > 32767) sample1 - 65536 else sample1
|
|
328
|
-
val s2 = if (sample2 > 32767) sample2 - 65536 else sample2
|
|
329
|
-
|
|
330
|
-
// Linear interpolation
|
|
331
|
-
val interpolated = (s1 + (s2 - s1) * fraction).toInt()
|
|
466
|
+
// Linear interpolation: s1 + (s2 - s1) * fraction
|
|
467
|
+
val interpolated = s1 + (s2 - s1) * fraction
|
|
332
468
|
|
|
333
469
|
// Clamp to 16-bit range
|
|
334
|
-
val clamped = interpolated.coerceIn(-32768, 32767)
|
|
470
|
+
val clamped = interpolated.toInt().coerceIn(-32768, 32767)
|
|
335
471
|
|
|
336
|
-
//
|
|
472
|
+
// Write to output (little-endian)
|
|
337
473
|
val outIdx = (i * channelCount + ch) * 2
|
|
338
474
|
output[outIdx] = (clamped and 0xFF).toByte()
|
|
339
475
|
output[outIdx + 1] = (clamped shr 8).toByte()
|
|
340
476
|
}
|
|
341
477
|
}
|
|
342
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
|
+
|
|
343
482
|
return output
|
|
344
483
|
}
|
|
345
484
|
|
|
@@ -379,7 +518,8 @@ class AudioConcatModule(reactContext: ReactApplicationContext) :
|
|
|
379
518
|
val leftSigned = if (left > 32767) left - 65536 else left
|
|
380
519
|
val rightSigned = if (right > 32767) right - 65536 else right
|
|
381
520
|
|
|
382
|
-
|
|
521
|
+
// Use bit shift instead of division for better performance (x / 2 = x >> 1)
|
|
522
|
+
val avg = ((leftSigned + rightSigned) shr 1).coerceIn(-32768, 32767)
|
|
383
523
|
|
|
384
524
|
output[dstIdx] = (avg and 0xFF).toByte()
|
|
385
525
|
output[dstIdx + 1] = (avg shr 8).toByte()
|
|
@@ -408,7 +548,8 @@ class AudioConcatModule(reactContext: ReactApplicationContext) :
|
|
|
408
548
|
targetSampleRate: Int,
|
|
409
549
|
targetChannelCount: Int,
|
|
410
550
|
latch: CountDownLatch,
|
|
411
|
-
cache: PCMCache
|
|
551
|
+
cache: PCMCache,
|
|
552
|
+
decoderPool: DecoderPool? = null
|
|
412
553
|
) {
|
|
413
554
|
try {
|
|
414
555
|
// Check cache first
|
|
@@ -428,6 +569,7 @@ class AudioConcatModule(reactContext: ReactApplicationContext) :
|
|
|
428
569
|
var decoder: MediaCodec? = null
|
|
429
570
|
val decodedChunks = mutableListOf<ByteArray>()
|
|
430
571
|
var totalBytes = 0L
|
|
572
|
+
val shouldReleaseDecoder = (decoderPool == null) // Only release if not using pool
|
|
431
573
|
|
|
432
574
|
try {
|
|
433
575
|
extractor.setDataSource(filePath)
|
|
@@ -462,16 +604,24 @@ class AudioConcatModule(reactContext: ReactApplicationContext) :
|
|
|
462
604
|
extractor.selectTrack(audioTrackIndex)
|
|
463
605
|
|
|
464
606
|
val mime = audioFormat.getString(MediaFormat.KEY_MIME)!!
|
|
465
|
-
|
|
466
|
-
decoder
|
|
467
|
-
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
|
+
}
|
|
468
618
|
|
|
469
619
|
val bufferInfo = MediaCodec.BufferInfo()
|
|
470
620
|
var isEOS = false
|
|
471
621
|
|
|
472
622
|
while (!isEOS) {
|
|
473
|
-
// Feed input to decoder
|
|
474
|
-
val inputBufferIndex = decoder.dequeueInputBuffer(
|
|
623
|
+
// Feed input to decoder (reduced timeout for faster processing)
|
|
624
|
+
val inputBufferIndex = decoder.dequeueInputBuffer(1000)
|
|
475
625
|
if (inputBufferIndex >= 0) {
|
|
476
626
|
val inputBuffer = decoder.getInputBuffer(inputBufferIndex)!!
|
|
477
627
|
val sampleSize = extractor.readSampleData(inputBuffer, 0)
|
|
@@ -485,8 +635,8 @@ class AudioConcatModule(reactContext: ReactApplicationContext) :
|
|
|
485
635
|
}
|
|
486
636
|
}
|
|
487
637
|
|
|
488
|
-
// Get PCM output from decoder and put to queue
|
|
489
|
-
val outputBufferIndex = decoder.dequeueOutputBuffer(bufferInfo,
|
|
638
|
+
// Get PCM output from decoder and put to queue (reduced timeout)
|
|
639
|
+
val outputBufferIndex = decoder.dequeueOutputBuffer(bufferInfo, 1000)
|
|
490
640
|
if (outputBufferIndex >= 0) {
|
|
491
641
|
val outputBuffer = decoder.getOutputBuffer(outputBufferIndex)!!
|
|
492
642
|
|
|
@@ -504,13 +654,13 @@ class AudioConcatModule(reactContext: ReactApplicationContext) :
|
|
|
504
654
|
pcmData = resamplePCM16(pcmData, sourceSampleRate, targetSampleRate, targetChannelCount)
|
|
505
655
|
}
|
|
506
656
|
|
|
507
|
-
//
|
|
508
|
-
decodedChunks.add(pcmData
|
|
657
|
+
// Optimization: avoid unnecessary clone() - store original for caching
|
|
658
|
+
decodedChunks.add(pcmData)
|
|
509
659
|
totalBytes += pcmData.size
|
|
510
660
|
|
|
511
|
-
// Put to queue
|
|
661
|
+
// Put a clone to queue (queue might modify it)
|
|
512
662
|
val seqNum = sequenceStart.getAndIncrement()
|
|
513
|
-
queue.put(PCMChunk(pcmData, seqNum))
|
|
663
|
+
queue.put(PCMChunk(pcmData.clone(), seqNum))
|
|
514
664
|
}
|
|
515
665
|
|
|
516
666
|
decoder.releaseOutputBuffer(outputBufferIndex, false)
|
|
@@ -527,8 +677,11 @@ class AudioConcatModule(reactContext: ReactApplicationContext) :
|
|
|
527
677
|
}
|
|
528
678
|
|
|
529
679
|
} finally {
|
|
530
|
-
decoder
|
|
531
|
-
|
|
680
|
+
// Only stop/release decoder if not using pool
|
|
681
|
+
if (shouldReleaseDecoder) {
|
|
682
|
+
decoder?.stop()
|
|
683
|
+
decoder?.release()
|
|
684
|
+
}
|
|
532
685
|
extractor.release()
|
|
533
686
|
}
|
|
534
687
|
} catch (e: Exception) {
|
|
@@ -544,10 +697,13 @@ class AudioConcatModule(reactContext: ReactApplicationContext) :
|
|
|
544
697
|
encoder: StreamingEncoder,
|
|
545
698
|
isLastFile: Boolean,
|
|
546
699
|
targetSampleRate: Int,
|
|
547
|
-
targetChannelCount: Int
|
|
700
|
+
targetChannelCount: Int,
|
|
701
|
+
reusableDecoder: ReusableDecoder? = null
|
|
548
702
|
) {
|
|
703
|
+
val startTime = System.currentTimeMillis()
|
|
549
704
|
val extractor = MediaExtractor()
|
|
550
705
|
var decoder: MediaCodec? = null
|
|
706
|
+
val shouldReleaseDecoder = (reusableDecoder == null) // Only release if not reusing
|
|
551
707
|
|
|
552
708
|
try {
|
|
553
709
|
extractor.setDataSource(filePath)
|
|
@@ -582,16 +738,23 @@ class AudioConcatModule(reactContext: ReactApplicationContext) :
|
|
|
582
738
|
extractor.selectTrack(audioTrackIndex)
|
|
583
739
|
|
|
584
740
|
val mime = audioFormat.getString(MediaFormat.KEY_MIME)!!
|
|
585
|
-
|
|
586
|
-
decoder
|
|
587
|
-
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
|
+
}
|
|
588
751
|
|
|
589
752
|
val bufferInfo = MediaCodec.BufferInfo()
|
|
590
753
|
var isEOS = false
|
|
591
754
|
|
|
592
755
|
while (!isEOS) {
|
|
593
|
-
// Feed input to decoder
|
|
594
|
-
val inputBufferIndex = decoder.dequeueInputBuffer(
|
|
756
|
+
// Feed input to decoder (reduced timeout for faster processing)
|
|
757
|
+
val inputBufferIndex = decoder.dequeueInputBuffer(1000)
|
|
595
758
|
if (inputBufferIndex >= 0) {
|
|
596
759
|
val inputBuffer = decoder.getInputBuffer(inputBufferIndex)!!
|
|
597
760
|
val sampleSize = extractor.readSampleData(inputBuffer, 0)
|
|
@@ -605,8 +768,8 @@ class AudioConcatModule(reactContext: ReactApplicationContext) :
|
|
|
605
768
|
}
|
|
606
769
|
}
|
|
607
770
|
|
|
608
|
-
// Get PCM output from decoder and feed to encoder
|
|
609
|
-
val outputBufferIndex = decoder.dequeueOutputBuffer(bufferInfo,
|
|
771
|
+
// Get PCM output from decoder and feed to encoder (reduced timeout)
|
|
772
|
+
val outputBufferIndex = decoder.dequeueOutputBuffer(bufferInfo, 1000)
|
|
610
773
|
if (outputBufferIndex >= 0) {
|
|
611
774
|
val outputBuffer = decoder.getOutputBuffer(outputBufferIndex)!!
|
|
612
775
|
|
|
@@ -637,9 +800,14 @@ class AudioConcatModule(reactContext: ReactApplicationContext) :
|
|
|
637
800
|
}
|
|
638
801
|
|
|
639
802
|
} finally {
|
|
640
|
-
decoder
|
|
641
|
-
|
|
803
|
+
// Only stop/release decoder if we created it locally (not reusing)
|
|
804
|
+
if (shouldReleaseDecoder) {
|
|
805
|
+
decoder?.stop()
|
|
806
|
+
decoder?.release()
|
|
807
|
+
}
|
|
642
808
|
extractor.release()
|
|
809
|
+
val elapsedTime = System.currentTimeMillis() - startTime
|
|
810
|
+
Log.d("AudioConcat", " Decoded file in ${elapsedTime}ms")
|
|
643
811
|
}
|
|
644
812
|
}
|
|
645
813
|
|
|
@@ -667,18 +835,32 @@ class AudioConcatModule(reactContext: ReactApplicationContext) :
|
|
|
667
835
|
|
|
668
836
|
// For short silence (< 5 seconds), cache as single chunk
|
|
669
837
|
if (durationMs < 5000) {
|
|
670
|
-
|
|
838
|
+
// Use buffer pool to avoid allocation
|
|
839
|
+
val pooledBuffer = SilenceBufferPool.getBuffer(totalBytes)
|
|
840
|
+
val silenceData = if (pooledBuffer.size == totalBytes) {
|
|
841
|
+
pooledBuffer
|
|
842
|
+
} else {
|
|
843
|
+
// Copy only the needed portion
|
|
844
|
+
pooledBuffer.copyOf(totalBytes)
|
|
845
|
+
}
|
|
671
846
|
cache.putSilence(cacheKey, silenceData)
|
|
672
847
|
encoder.encodePCMChunk(silenceData, false)
|
|
673
848
|
} else {
|
|
674
|
-
// For longer silence, process in chunks without caching
|
|
849
|
+
// For longer silence, process in chunks without caching using pooled buffers
|
|
675
850
|
val chunkSamples = 16384
|
|
676
851
|
var samplesRemaining = totalSamples
|
|
677
852
|
|
|
678
853
|
while (samplesRemaining > 0) {
|
|
679
854
|
val currentChunkSamples = minOf(chunkSamples, samplesRemaining)
|
|
680
855
|
val chunkBytes = currentChunkSamples * bytesPerSample
|
|
681
|
-
|
|
856
|
+
|
|
857
|
+
// Use pooled buffer for chunk
|
|
858
|
+
val pooledBuffer = SilenceBufferPool.getBuffer(chunkBytes)
|
|
859
|
+
val silenceChunk = if (pooledBuffer.size == chunkBytes) {
|
|
860
|
+
pooledBuffer
|
|
861
|
+
} else {
|
|
862
|
+
pooledBuffer.copyOf(chunkBytes)
|
|
863
|
+
}
|
|
682
864
|
|
|
683
865
|
encoder.encodePCMChunk(silenceChunk, false)
|
|
684
866
|
samplesRemaining -= currentChunkSamples
|
|
@@ -686,6 +868,28 @@ class AudioConcatModule(reactContext: ReactApplicationContext) :
|
|
|
686
868
|
}
|
|
687
869
|
}
|
|
688
870
|
|
|
871
|
+
private fun getOptimalThreadCount(audioFileCount: Int): Int {
|
|
872
|
+
val cpuCores = Runtime.getRuntime().availableProcessors()
|
|
873
|
+
val optimalThreads = when {
|
|
874
|
+
cpuCores <= 2 -> 2
|
|
875
|
+
cpuCores <= 4 -> 3
|
|
876
|
+
cpuCores <= 8 -> 4
|
|
877
|
+
else -> 6
|
|
878
|
+
}
|
|
879
|
+
// Don't create more threads than files to process
|
|
880
|
+
return optimalThreads.coerceAtMost(audioFileCount)
|
|
881
|
+
}
|
|
882
|
+
|
|
883
|
+
private fun getOptimalQueueSize(audioFileCount: Int): Int {
|
|
884
|
+
// Dynamic queue size based on number of files to prevent memory waste or blocking
|
|
885
|
+
return when {
|
|
886
|
+
audioFileCount <= 5 -> 20
|
|
887
|
+
audioFileCount <= 20 -> 50
|
|
888
|
+
audioFileCount <= 50 -> 100
|
|
889
|
+
else -> 150
|
|
890
|
+
}
|
|
891
|
+
}
|
|
892
|
+
|
|
689
893
|
private fun parallelProcessAudioFiles(
|
|
690
894
|
audioFiles: List<Pair<Int, String>>, // (index, filePath)
|
|
691
895
|
encoder: StreamingEncoder,
|
|
@@ -721,11 +925,17 @@ class AudioConcatModule(reactContext: ReactApplicationContext) :
|
|
|
721
925
|
i += count
|
|
722
926
|
}
|
|
723
927
|
|
|
724
|
-
val
|
|
928
|
+
val queueSize = getOptimalQueueSize(optimizedFiles.size)
|
|
929
|
+
val pcmQueue = LinkedBlockingQueue<PCMChunk>(queueSize)
|
|
930
|
+
Log.d("AudioConcat", "Using queue size: $queueSize for ${optimizedFiles.size} files")
|
|
725
931
|
val executor = Executors.newFixedThreadPool(numThreads)
|
|
726
932
|
val latch = CountDownLatch(optimizedFiles.size)
|
|
727
933
|
val sequenceCounter = AtomicInteger(0)
|
|
728
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
|
+
|
|
729
939
|
try {
|
|
730
940
|
// Submit decode tasks for unique files only
|
|
731
941
|
optimizedFiles.forEachIndexed { optIndex, (index, filePath) ->
|
|
@@ -735,7 +945,7 @@ class AudioConcatModule(reactContext: ReactApplicationContext) :
|
|
|
735
945
|
sequenceCounter.addAndGet(1000000)
|
|
736
946
|
|
|
737
947
|
Log.d("AudioConcat", "Starting parallel decode [$index]: $filePath")
|
|
738
|
-
parallelDecodeToQueue(filePath, pcmQueue, fileSequenceStart, targetSampleRate, targetChannelCount, latch, cache)
|
|
948
|
+
parallelDecodeToQueue(filePath, pcmQueue, fileSequenceStart, targetSampleRate, targetChannelCount, latch, cache, decoderPool)
|
|
739
949
|
|
|
740
950
|
// Mark end with duplicate count
|
|
741
951
|
val repeatCount = consecutiveDuplicates[optIndex] ?: 1
|
|
@@ -793,6 +1003,7 @@ class AudioConcatModule(reactContext: ReactApplicationContext) :
|
|
|
793
1003
|
Log.d("AudioConcat", "All parallel decode tasks completed")
|
|
794
1004
|
|
|
795
1005
|
} finally {
|
|
1006
|
+
decoderPool.releaseAll()
|
|
796
1007
|
executor.shutdown()
|
|
797
1008
|
}
|
|
798
1009
|
}
|
|
@@ -925,17 +1136,24 @@ class AudioConcatModule(reactContext: ReactApplicationContext) :
|
|
|
925
1136
|
}
|
|
926
1137
|
|
|
927
1138
|
override fun concatAudioFiles(data: ReadableArray, outputPath: String, promise: Promise) {
|
|
1139
|
+
val totalStartTime = System.currentTimeMillis()
|
|
1140
|
+
Log.d("AudioConcat", "========== Audio Concat Started ==========")
|
|
1141
|
+
|
|
928
1142
|
try {
|
|
929
1143
|
if (data.size() == 0) {
|
|
930
1144
|
promise.reject("EMPTY_DATA", "Data array is empty")
|
|
931
1145
|
return
|
|
932
1146
|
}
|
|
933
1147
|
|
|
1148
|
+
// Parse data
|
|
1149
|
+
val parseStartTime = System.currentTimeMillis()
|
|
934
1150
|
val parsedData = parseAudioData(data)
|
|
935
|
-
|
|
1151
|
+
val parseTime = System.currentTimeMillis() - parseStartTime
|
|
1152
|
+
Log.d("AudioConcat", "✓ Parsed ${parsedData.size} items in ${parseTime}ms")
|
|
936
1153
|
Log.d("AudioConcat", "Output: $outputPath")
|
|
937
1154
|
|
|
938
1155
|
// Get audio config from first audio file
|
|
1156
|
+
val configStartTime = System.currentTimeMillis()
|
|
939
1157
|
var audioConfig: AudioConfig? = null
|
|
940
1158
|
for (item in parsedData) {
|
|
941
1159
|
if (item is AudioDataOrSilence.AudioFile) {
|
|
@@ -949,10 +1167,21 @@ class AudioConcatModule(reactContext: ReactApplicationContext) :
|
|
|
949
1167
|
return
|
|
950
1168
|
}
|
|
951
1169
|
|
|
952
|
-
|
|
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)
|
|
953
1179
|
|
|
954
1180
|
// Analyze duplicates to determine cache strategy
|
|
955
|
-
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")
|
|
956
1185
|
|
|
957
1186
|
// Create cache instance with intelligent caching strategy
|
|
958
1187
|
val cache = PCMCache(duplicateAnalysis.duplicateFiles, duplicateAnalysis.duplicateSilence)
|
|
@@ -963,9 +1192,9 @@ class AudioConcatModule(reactContext: ReactApplicationContext) :
|
|
|
963
1192
|
outputFile.delete()
|
|
964
1193
|
}
|
|
965
1194
|
|
|
966
|
-
// Create streaming encoder
|
|
1195
|
+
// Create streaming encoder with fixed 24kHz sample rate
|
|
967
1196
|
val encoder = StreamingEncoder(
|
|
968
|
-
|
|
1197
|
+
outputSampleRate,
|
|
969
1198
|
audioConfig.channelCount,
|
|
970
1199
|
audioConfig.bitRate,
|
|
971
1200
|
outputPath
|
|
@@ -989,9 +1218,10 @@ class AudioConcatModule(reactContext: ReactApplicationContext) :
|
|
|
989
1218
|
|
|
990
1219
|
// Decide whether to use parallel or sequential processing
|
|
991
1220
|
val useParallel = audioFileItems.size >= 10 // Use parallel for 10+ files
|
|
1221
|
+
val processingStartTime = System.currentTimeMillis()
|
|
992
1222
|
|
|
993
1223
|
if (useParallel) {
|
|
994
|
-
Log.d("AudioConcat", "Using parallel processing for ${audioFileItems.size} audio files")
|
|
1224
|
+
Log.d("AudioConcat", "→ Using parallel processing for ${audioFileItems.size} audio files")
|
|
995
1225
|
|
|
996
1226
|
// Process interleaved patterns optimally
|
|
997
1227
|
val processedIndices = mutableSetOf<Int>()
|
|
@@ -1014,7 +1244,7 @@ class AudioConcatModule(reactContext: ReactApplicationContext) :
|
|
|
1014
1244
|
val latch = CountDownLatch(1)
|
|
1015
1245
|
val seqStart = AtomicInteger(0)
|
|
1016
1246
|
|
|
1017
|
-
parallelDecodeToQueue(filePath, tempQueue, seqStart,
|
|
1247
|
+
parallelDecodeToQueue(filePath, tempQueue, seqStart, outputSampleRate, audioConfig.channelCount, latch, cache)
|
|
1018
1248
|
|
|
1019
1249
|
// Collect chunks
|
|
1020
1250
|
var collecting = true
|
|
@@ -1091,13 +1321,15 @@ class AudioConcatModule(reactContext: ReactApplicationContext) :
|
|
|
1091
1321
|
}
|
|
1092
1322
|
|
|
1093
1323
|
if (consecutiveFiles.isNotEmpty()) {
|
|
1324
|
+
val optimalThreads = getOptimalThreadCount(consecutiveFiles.size)
|
|
1325
|
+
Log.d("AudioConcat", "Using $optimalThreads threads for ${consecutiveFiles.size} files (CPU cores: ${Runtime.getRuntime().availableProcessors()})")
|
|
1094
1326
|
parallelProcessAudioFiles(
|
|
1095
1327
|
consecutiveFiles,
|
|
1096
1328
|
encoder,
|
|
1097
|
-
|
|
1329
|
+
outputSampleRate,
|
|
1098
1330
|
audioConfig.channelCount,
|
|
1099
1331
|
cache,
|
|
1100
|
-
numThreads =
|
|
1332
|
+
numThreads = optimalThreads
|
|
1101
1333
|
)
|
|
1102
1334
|
audioFileIdx = currentIdx
|
|
1103
1335
|
}
|
|
@@ -1109,7 +1341,7 @@ class AudioConcatModule(reactContext: ReactApplicationContext) :
|
|
|
1109
1341
|
streamEncodeSilence(
|
|
1110
1342
|
durationMs,
|
|
1111
1343
|
encoder,
|
|
1112
|
-
|
|
1344
|
+
outputSampleRate,
|
|
1113
1345
|
audioConfig.channelCount,
|
|
1114
1346
|
cache
|
|
1115
1347
|
)
|
|
@@ -1117,47 +1349,66 @@ class AudioConcatModule(reactContext: ReactApplicationContext) :
|
|
|
1117
1349
|
}
|
|
1118
1350
|
}
|
|
1119
1351
|
} else {
|
|
1120
|
-
Log.d("AudioConcat", "Using sequential processing for ${audioFileItems.size} audio files")
|
|
1352
|
+
Log.d("AudioConcat", "→ Using sequential processing for ${audioFileItems.size} audio files")
|
|
1121
1353
|
|
|
1122
|
-
//
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
is AudioDataOrSilence.AudioFile -> {
|
|
1126
|
-
val filePath = item.filePath
|
|
1127
|
-
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")
|
|
1128
1357
|
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
|
|
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
|
+
}
|
|
1138
1376
|
|
|
1139
|
-
|
|
1140
|
-
|
|
1141
|
-
|
|
1377
|
+
is AudioDataOrSilence.Silence -> {
|
|
1378
|
+
val durationMs = item.durationMs
|
|
1379
|
+
Log.d("AudioConcat", "Item $index: Streaming silence ${durationMs}ms")
|
|
1142
1380
|
|
|
1143
|
-
|
|
1144
|
-
|
|
1145
|
-
|
|
1146
|
-
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
|
|
1381
|
+
streamEncodeSilence(
|
|
1382
|
+
durationMs,
|
|
1383
|
+
encoder,
|
|
1384
|
+
outputSampleRate,
|
|
1385
|
+
audioConfig.channelCount,
|
|
1386
|
+
cache
|
|
1387
|
+
)
|
|
1388
|
+
}
|
|
1150
1389
|
}
|
|
1151
1390
|
}
|
|
1391
|
+
} finally {
|
|
1392
|
+
// Release the reusable decoder when done
|
|
1393
|
+
reusableDecoder.release()
|
|
1394
|
+
Log.d("AudioConcat", "Released reusable decoder")
|
|
1152
1395
|
}
|
|
1153
1396
|
}
|
|
1154
1397
|
|
|
1398
|
+
val processingTime = System.currentTimeMillis() - processingStartTime
|
|
1399
|
+
Log.d("AudioConcat", "✓ Processing completed in ${processingTime}ms")
|
|
1400
|
+
|
|
1155
1401
|
// Finish encoding
|
|
1402
|
+
val encodingFinishStartTime = System.currentTimeMillis()
|
|
1156
1403
|
encoder.finish()
|
|
1404
|
+
val encodingFinishTime = System.currentTimeMillis() - encodingFinishStartTime
|
|
1405
|
+
Log.d("AudioConcat", "✓ Encoding finalized in ${encodingFinishTime}ms")
|
|
1157
1406
|
|
|
1158
1407
|
// Log cache statistics
|
|
1159
1408
|
Log.d("AudioConcat", "Cache statistics: ${cache.getStats()}")
|
|
1160
1409
|
|
|
1410
|
+
val totalTime = System.currentTimeMillis() - totalStartTime
|
|
1411
|
+
Log.d("AudioConcat", "========== Total Time: ${totalTime}ms (${totalTime / 1000.0}s) ==========")
|
|
1161
1412
|
Log.d("AudioConcat", "Successfully merged audio to $outputPath")
|
|
1162
1413
|
promise.resolve(outputPath)
|
|
1163
1414
|
|