@siteed/audio-studio 3.1.0 → 3.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +10 -1
- package/README.md +97 -50
- package/android/src/androidTest/java/net/siteed/audiostudio/AudioFinalMetadataContractInstrumentedTest.kt +190 -0
- package/android/src/androidTest/java/net/siteed/audiostudio/AudioRecorderInstrumentedTest.kt +29 -83
- package/android/src/androidTest/java/net/siteed/audiostudio/AudioRecorderPerformanceInstrumentedTest.kt +17 -1
- package/android/src/androidTest/java/net/siteed/audiostudio/OpusRangeDecodeRegressionInstrumentedTest.kt +186 -0
- package/android/src/main/java/net/siteed/audiostudio/AudioProcessor.kt +473 -380
- package/android/src/main/java/net/siteed/audiostudio/AudioStudioModule.kt +53 -10
- package/android/src/main/java/net/siteed/audiostudio/AudioTrimmer.kt +174 -212
- package/build/cjs/AudioAnalysis/AudioAnalysis.types.js.map +1 -1
- package/build/cjs/AudioAnalysis/extractPreview.js +92 -15
- package/build/cjs/AudioAnalysis/extractPreview.js.map +1 -1
- package/build/cjs/AudioAnalysis/extractPreviewBars.js +134 -0
- package/build/cjs/AudioAnalysis/extractPreviewBars.js.map +1 -0
- package/build/cjs/errors/AudioExtractionError.js +127 -0
- package/build/cjs/errors/AudioExtractionError.js.map +1 -0
- package/build/cjs/index.js +6 -1
- package/build/cjs/index.js.map +1 -1
- package/build/esm/AudioAnalysis/AudioAnalysis.types.js.map +1 -1
- package/build/esm/AudioAnalysis/extractPreview.js +92 -15
- package/build/esm/AudioAnalysis/extractPreview.js.map +1 -1
- package/build/esm/AudioAnalysis/extractPreviewBars.js +128 -0
- package/build/esm/AudioAnalysis/extractPreviewBars.js.map +1 -0
- package/build/esm/errors/AudioExtractionError.js +122 -0
- package/build/esm/errors/AudioExtractionError.js.map +1 -0
- package/build/esm/index.js +2 -0
- package/build/esm/index.js.map +1 -1
- package/build/types/AudioAnalysis/AudioAnalysis.types.d.ts +79 -0
- package/build/types/AudioAnalysis/AudioAnalysis.types.d.ts.map +1 -1
- package/build/types/AudioAnalysis/extractPreview.d.ts +2 -2
- package/build/types/AudioAnalysis/extractPreview.d.ts.map +1 -1
- package/build/types/AudioAnalysis/extractPreviewBars.d.ts +12 -0
- package/build/types/AudioAnalysis/extractPreviewBars.d.ts.map +1 -0
- package/build/types/errors/AudioExtractionError.d.ts +24 -0
- package/build/types/errors/AudioExtractionError.d.ts.map +1 -0
- package/build/types/index.d.ts +3 -0
- package/build/types/index.d.ts.map +1 -1
- package/ios/AudioProcessor.swift +99 -0
- package/ios/AudioStudioModule.swift +63 -0
- package/package.json +7 -7
- package/src/AudioAnalysis/AudioAnalysis.types.ts +82 -0
- package/src/AudioAnalysis/extractPreview.ts +118 -17
- package/src/AudioAnalysis/extractPreviewBars.ts +193 -0
- package/src/errors/AudioExtractionError.ts +167 -0
- package/src/index.ts +10 -0
|
@@ -15,7 +15,7 @@ import java.nio.ByteBuffer
|
|
|
15
15
|
import kotlin.math.min
|
|
16
16
|
|
|
17
17
|
class AudioTrimmer(
|
|
18
|
-
private val context: Context,
|
|
18
|
+
private val context: Context,
|
|
19
19
|
private val fileHandler: AudioFileHandler
|
|
20
20
|
) {
|
|
21
21
|
companion object {
|
|
@@ -27,6 +27,10 @@ class AudioTrimmer(
|
|
|
27
27
|
fun onProgress(progress: Float, bytesProcessed: Long, totalBytes: Long)
|
|
28
28
|
}
|
|
29
29
|
|
|
30
|
+
private fun numberOption(options: Map<String, Any>, key: String): Int? {
|
|
31
|
+
return (options[key] as? Number)?.toInt()
|
|
32
|
+
}
|
|
33
|
+
|
|
30
34
|
/**
|
|
31
35
|
* Trims audio file based on the provided options
|
|
32
36
|
*/
|
|
@@ -48,19 +52,19 @@ class AudioTrimmer(
|
|
|
48
52
|
try {
|
|
49
53
|
// Resolve the input file URI
|
|
50
54
|
val inputUri = Uri.parse(fileUri)
|
|
51
|
-
|
|
55
|
+
|
|
52
56
|
// Get audio file metadata
|
|
53
57
|
val retriever = MediaMetadataRetriever()
|
|
54
58
|
retriever.setDataSource(context, inputUri)
|
|
55
|
-
|
|
59
|
+
|
|
56
60
|
// Extract audio format information
|
|
57
61
|
val audioFormat = getAudioFormat(retriever)
|
|
58
62
|
Log.d(TAG, "Source audio format: $audioFormat")
|
|
59
|
-
|
|
63
|
+
|
|
60
64
|
// Validate and process output format options
|
|
61
65
|
val formatOptions = outputFormat ?: emptyMap()
|
|
62
66
|
val outputFormatType = (formatOptions["format"] as? String)?.lowercase() ?: "wav"
|
|
63
|
-
|
|
67
|
+
|
|
64
68
|
// Validate format and provide consistent fallback
|
|
65
69
|
val effectiveFormatType = if (outputFormatType !in listOf("wav", "aac", "opus")) {
|
|
66
70
|
Log.w(TAG, "Unsupported format '$outputFormatType'. Falling back to 'aac'")
|
|
@@ -68,38 +72,38 @@ class AudioTrimmer(
|
|
|
68
72
|
} else {
|
|
69
73
|
outputFormatType
|
|
70
74
|
}
|
|
71
|
-
|
|
75
|
+
|
|
72
76
|
// Validate and normalize format-specific parameters
|
|
73
|
-
val sampleRate = (formatOptions
|
|
77
|
+
val sampleRate = numberOption(formatOptions, "sampleRate")?.coerceIn(8000, 48000)
|
|
74
78
|
?: audioFormat.sampleRate
|
|
75
|
-
val channels = (formatOptions
|
|
79
|
+
val channels = numberOption(formatOptions, "channels")?.coerceIn(1, 2)
|
|
76
80
|
?: audioFormat.channels
|
|
77
|
-
val bitDepth = (formatOptions
|
|
81
|
+
val bitDepth = numberOption(formatOptions, "bitDepth")?.coerceIn(8, 32)
|
|
78
82
|
?: audioFormat.bitDepth
|
|
79
|
-
val bitrate = (formatOptions
|
|
83
|
+
val bitrate = numberOption(formatOptions, "bitrate")?.coerceIn(8000, 320000)
|
|
80
84
|
?: 128000
|
|
81
|
-
|
|
85
|
+
|
|
82
86
|
Log.d(TAG, "Output format parameters: format=$effectiveFormatType, sampleRate=$sampleRate, " +
|
|
83
87
|
"channels=$channels, bitDepth=$bitDepth, bitrate=$bitrate")
|
|
84
|
-
|
|
88
|
+
|
|
85
89
|
// Determine the appropriate extension and format
|
|
86
90
|
val extension = when (effectiveFormatType) {
|
|
87
91
|
"wav" -> "wav"
|
|
88
92
|
"opus" -> "opus"
|
|
89
93
|
else -> "m4a" // Use m4a extension for AAC to match iOS
|
|
90
94
|
}
|
|
91
|
-
|
|
95
|
+
|
|
92
96
|
Log.d(TAG, "Using output extension: $extension")
|
|
93
|
-
|
|
97
|
+
|
|
94
98
|
// Create output file
|
|
95
99
|
val outputFile = if (outputFileName != null) {
|
|
96
100
|
File(context.filesDir, "$outputFileName.$extension")
|
|
97
101
|
} else {
|
|
98
102
|
fileHandler.createAudioFile(extension)
|
|
99
103
|
}
|
|
100
|
-
|
|
104
|
+
|
|
101
105
|
Log.d(TAG, "Created output file: ${outputFile.absolutePath}")
|
|
102
|
-
|
|
106
|
+
|
|
103
107
|
// Determine the time ranges to process based on the mode
|
|
104
108
|
val timeRanges = when (mode) {
|
|
105
109
|
"single" -> {
|
|
@@ -112,34 +116,34 @@ class AudioTrimmer(
|
|
|
112
116
|
// For remove mode, we need to invert the ranges
|
|
113
117
|
val invertedRanges = mutableListOf<Map<String, Long>>()
|
|
114
118
|
var lastEndTime = 0L
|
|
115
|
-
|
|
119
|
+
|
|
116
120
|
ranges?.sortedBy { it["startTimeMs"] }?.forEach { range ->
|
|
117
121
|
val start = range["startTimeMs"] ?: 0L
|
|
118
122
|
val end = range["endTimeMs"] ?: audioFormat.durationMs
|
|
119
|
-
|
|
123
|
+
|
|
120
124
|
if (start > lastEndTime) {
|
|
121
125
|
invertedRanges.add(mapOf("startTimeMs" to lastEndTime, "endTimeMs" to start))
|
|
122
126
|
}
|
|
123
127
|
lastEndTime = end
|
|
124
128
|
}
|
|
125
|
-
|
|
129
|
+
|
|
126
130
|
if (lastEndTime < audioFormat.durationMs) {
|
|
127
131
|
invertedRanges.add(mapOf("startTimeMs" to lastEndTime, "endTimeMs" to audioFormat.durationMs))
|
|
128
132
|
}
|
|
129
|
-
|
|
133
|
+
|
|
130
134
|
invertedRanges
|
|
131
135
|
}
|
|
132
136
|
else -> throw IllegalArgumentException("Invalid mode: $mode")
|
|
133
137
|
}
|
|
134
|
-
|
|
138
|
+
|
|
135
139
|
// Check if we need format conversion
|
|
136
|
-
val needFormatChange = formatOptions["sampleRate"] != null ||
|
|
137
|
-
formatOptions["channels"] != null ||
|
|
140
|
+
val needFormatChange = formatOptions["sampleRate"] != null ||
|
|
141
|
+
formatOptions["channels"] != null ||
|
|
138
142
|
formatOptions["bitDepth"] != null
|
|
139
|
-
|
|
143
|
+
|
|
140
144
|
// Check if input is WAV format
|
|
141
145
|
val isWavInput = audioFormat.mimeType == "audio/wav" || audioFormat.mimeType == "audio/x-wav"
|
|
142
|
-
|
|
146
|
+
|
|
143
147
|
// Optimized approach based on input/output formats
|
|
144
148
|
if (isWavInput && extension == "wav" && !needFormatChange) {
|
|
145
149
|
// Fast path for WAV-to-WAV with no format changes
|
|
@@ -152,12 +156,12 @@ class AudioTrimmer(
|
|
|
152
156
|
// Need to decode and possibly re-encode
|
|
153
157
|
Log.d(TAG, "Using decode/encode path for non-WAV input or format conversion")
|
|
154
158
|
val config = DecodingConfig(
|
|
155
|
-
targetSampleRate =
|
|
156
|
-
targetChannels =
|
|
157
|
-
targetBitDepth =
|
|
159
|
+
targetSampleRate = sampleRate,
|
|
160
|
+
targetChannels = channels,
|
|
161
|
+
targetBitDepth = bitDepth,
|
|
158
162
|
normalizeAudio = false
|
|
159
163
|
)
|
|
160
|
-
|
|
164
|
+
|
|
161
165
|
if (extension == "wav") {
|
|
162
166
|
// For any format to WAV conversion
|
|
163
167
|
Log.d(TAG, "Processing to WAV with possible format conversion")
|
|
@@ -172,7 +176,7 @@ class AudioTrimmer(
|
|
|
172
176
|
// For compressed output formats (AAC, Opus)
|
|
173
177
|
Log.d(TAG, "Processing to compressed format: $extension")
|
|
174
178
|
val tempWavFile = File(context.filesDir, "temp_${System.currentTimeMillis()}.wav")
|
|
175
|
-
|
|
179
|
+
|
|
176
180
|
try {
|
|
177
181
|
// First decode to WAV
|
|
178
182
|
processToWav(
|
|
@@ -186,20 +190,20 @@ class AudioTrimmer(
|
|
|
186
190
|
}
|
|
187
191
|
}
|
|
188
192
|
)
|
|
189
|
-
|
|
193
|
+
|
|
190
194
|
// Now encode to the target format
|
|
191
195
|
if (extension == "opus") {
|
|
192
196
|
val audioProcessor = AudioProcessor(context.filesDir)
|
|
193
197
|
val audioData = audioProcessor.loadAudioFromAnyFormat(
|
|
194
198
|
tempWavFile.absolutePath,
|
|
195
199
|
DecodingConfig(
|
|
196
|
-
targetSampleRate =
|
|
197
|
-
targetChannels =
|
|
200
|
+
targetSampleRate = sampleRate,
|
|
201
|
+
targetChannels = channels,
|
|
198
202
|
targetBitDepth = 16,
|
|
199
203
|
normalizeAudio = false
|
|
200
204
|
)
|
|
201
205
|
) ?: throw IOException("Failed to load WAV file")
|
|
202
|
-
|
|
206
|
+
|
|
203
207
|
encodeToOpus(
|
|
204
208
|
audioData,
|
|
205
209
|
outputFile,
|
|
@@ -231,23 +235,23 @@ class AudioTrimmer(
|
|
|
231
235
|
}
|
|
232
236
|
}
|
|
233
237
|
}
|
|
234
|
-
|
|
238
|
+
|
|
235
239
|
// Get output file metadata
|
|
236
240
|
val outputFileSize = outputFile.length()
|
|
237
241
|
val outputDurationMs = calculateOutputDuration(timeRanges)
|
|
238
|
-
|
|
242
|
+
|
|
239
243
|
// Extract audio format details
|
|
240
244
|
val extractor = MediaExtractor()
|
|
241
245
|
try {
|
|
242
246
|
extractor.setDataSource(outputFile.absolutePath)
|
|
243
|
-
|
|
247
|
+
|
|
244
248
|
// Initialize variables that will be populated from the file or user options
|
|
245
249
|
val outputBitrate: Int
|
|
246
250
|
|
|
247
251
|
// First try to get values from the output file
|
|
248
252
|
if (extractor.trackCount > 0) {
|
|
249
253
|
val format = extractor.getTrackFormat(0)
|
|
250
|
-
|
|
254
|
+
|
|
251
255
|
outputBitrate = if (format.containsKey(MediaFormat.KEY_BIT_RATE)) {
|
|
252
256
|
format.getInteger(MediaFormat.KEY_BIT_RATE)
|
|
253
257
|
} else {
|
|
@@ -258,14 +262,14 @@ class AudioTrimmer(
|
|
|
258
262
|
// If we can't get from the file, use user options or defaults
|
|
259
263
|
outputBitrate = bitrate
|
|
260
264
|
}
|
|
261
|
-
|
|
265
|
+
|
|
262
266
|
// Determine the correct MIME type
|
|
263
267
|
val mimeType = when (extension) {
|
|
264
268
|
"m4a" -> "audio/mp4" // Use audio/mp4 for AAC to match iOS
|
|
265
269
|
"opus" -> "audio/ogg" // Use audio/ogg for Opus
|
|
266
270
|
else -> "audio/wav"
|
|
267
271
|
}
|
|
268
|
-
|
|
272
|
+
|
|
269
273
|
val result = mutableMapOf<String, Any>(
|
|
270
274
|
"uri" to outputFile.absolutePath,
|
|
271
275
|
"filename" to outputFile.name,
|
|
@@ -278,7 +282,7 @@ class AudioTrimmer(
|
|
|
278
282
|
"requestedFormat" to (formatOptions["format"] as? String ?: "wav"), // Add the originally requested format
|
|
279
283
|
"actualFormat" to extension // Add the actual format used
|
|
280
284
|
)
|
|
281
|
-
|
|
285
|
+
|
|
282
286
|
// Add compression info if not WAV
|
|
283
287
|
if (extension != "wav") {
|
|
284
288
|
result["compression"] = mapOf(
|
|
@@ -287,20 +291,20 @@ class AudioTrimmer(
|
|
|
287
291
|
"size" to outputFileSize
|
|
288
292
|
)
|
|
289
293
|
}
|
|
290
|
-
|
|
294
|
+
|
|
291
295
|
Log.d(TAG, "Audio trim completed in ${System.currentTimeMillis() - startTime}ms")
|
|
292
296
|
return result
|
|
293
297
|
} catch (e: Exception) {
|
|
294
298
|
Log.e(TAG, "Error reading output file metadata: ${e.message}")
|
|
295
299
|
// Continue with basic metadata if extractor fails
|
|
296
|
-
|
|
300
|
+
|
|
297
301
|
// Determine the correct MIME type
|
|
298
302
|
val mimeType = when (extension) {
|
|
299
303
|
"m4a" -> "audio/mp4" // Use audio/mp4 for AAC to match iOS
|
|
300
304
|
"opus" -> "audio/ogg" // Use audio/ogg for Opus
|
|
301
305
|
else -> "audio/wav"
|
|
302
306
|
}
|
|
303
|
-
|
|
307
|
+
|
|
304
308
|
val result = mutableMapOf<String, Any>(
|
|
305
309
|
"uri" to outputFile.absolutePath,
|
|
306
310
|
"filename" to outputFile.name,
|
|
@@ -313,7 +317,7 @@ class AudioTrimmer(
|
|
|
313
317
|
"requestedFormat" to (formatOptions["format"] as? String ?: "wav"),
|
|
314
318
|
"actualFormat" to extension
|
|
315
319
|
)
|
|
316
|
-
|
|
320
|
+
|
|
317
321
|
// Add compression info if not WAV
|
|
318
322
|
if (extension != "wav") {
|
|
319
323
|
result["compression"] = mapOf(
|
|
@@ -322,7 +326,7 @@ class AudioTrimmer(
|
|
|
322
326
|
"size" to outputFileSize
|
|
323
327
|
)
|
|
324
328
|
}
|
|
325
|
-
|
|
329
|
+
|
|
326
330
|
Log.d(TAG, "Audio trim completed in ${System.currentTimeMillis() - startTime}ms")
|
|
327
331
|
return result
|
|
328
332
|
} finally {
|
|
@@ -332,7 +336,7 @@ class AudioTrimmer(
|
|
|
332
336
|
// Ignore
|
|
333
337
|
}
|
|
334
338
|
}
|
|
335
|
-
|
|
339
|
+
|
|
336
340
|
} catch (e: Exception) {
|
|
337
341
|
Log.e(TAG, "Error trimming audio", e)
|
|
338
342
|
throw e
|
|
@@ -348,7 +352,7 @@ class AudioTrimmer(
|
|
|
348
352
|
}
|
|
349
353
|
return totalDurationMs
|
|
350
354
|
}
|
|
351
|
-
|
|
355
|
+
|
|
352
356
|
/**
|
|
353
357
|
* Optimized version of processWavFile that directly copies bytes from input to output
|
|
354
358
|
* without decoding the entire file
|
|
@@ -362,111 +366,111 @@ class AudioTrimmer(
|
|
|
362
366
|
// Get input file path from URI
|
|
363
367
|
val inputPath = inputUri.path ?: throw IOException("Invalid input URI")
|
|
364
368
|
val inputFile = File(inputPath)
|
|
365
|
-
|
|
369
|
+
|
|
366
370
|
if (!inputFile.exists()) {
|
|
367
371
|
throw IOException("Input file does not exist: $inputPath")
|
|
368
372
|
}
|
|
369
|
-
|
|
373
|
+
|
|
370
374
|
// Create output file if it doesn't exist
|
|
371
375
|
if (!outputFile.exists() && !outputFile.createNewFile()) {
|
|
372
376
|
throw IOException("Failed to create output file: ${outputFile.path}")
|
|
373
377
|
}
|
|
374
|
-
|
|
378
|
+
|
|
375
379
|
// Use AudioProcessor to determine actual WAV header length
|
|
376
380
|
val audioProcessor = AudioProcessor(context.filesDir)
|
|
377
381
|
val headerSize = audioProcessor.getWavHeaderSize(inputFile.absolutePath) ?: 44 // Default to 44 if we can't determine
|
|
378
|
-
|
|
382
|
+
|
|
379
383
|
// Read WAV header to get format information using 'use' pattern
|
|
380
384
|
val headerBuffer = FileInputStream(inputFile).use { inputStream ->
|
|
381
385
|
ByteArray(headerSize).also { buffer ->
|
|
382
386
|
inputStream.read(buffer)
|
|
383
387
|
}
|
|
384
388
|
}
|
|
385
|
-
|
|
389
|
+
|
|
386
390
|
// Parse header to get format info
|
|
387
391
|
val sampleRate = ByteBuffer.wrap(headerBuffer, 24, 4).order(java.nio.ByteOrder.LITTLE_ENDIAN).int
|
|
388
392
|
val channels = ByteBuffer.wrap(headerBuffer, 22, 2).order(java.nio.ByteOrder.LITTLE_ENDIAN).short.toInt()
|
|
389
393
|
val bitDepth = ByteBuffer.wrap(headerBuffer, 34, 2).order(java.nio.ByteOrder.LITTLE_ENDIAN).short.toInt()
|
|
390
|
-
|
|
394
|
+
|
|
391
395
|
// Get file duration using MediaMetadataRetriever for consistency
|
|
392
396
|
val retriever = MediaMetadataRetriever()
|
|
393
397
|
retriever.setDataSource(inputFile.absolutePath)
|
|
394
398
|
val durationMsStr = retriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_DURATION)
|
|
395
399
|
val fileDurationMs = durationMsStr?.toLong() ?: 0
|
|
396
400
|
retriever.release()
|
|
397
|
-
|
|
401
|
+
|
|
398
402
|
// Create output file with WAV header
|
|
399
403
|
FileOutputStream(outputFile).use { outputStream ->
|
|
400
404
|
fileHandler.writeWavHeader(outputStream, sampleRate, channels, bitDepth)
|
|
401
|
-
|
|
405
|
+
|
|
402
406
|
// Process each time range
|
|
403
407
|
val bytesPerSample = bitDepth / 8
|
|
404
408
|
val bytesPerFrame = bytesPerSample * channels
|
|
405
409
|
val buffer = ByteArray(BUFFER_SIZE - (BUFFER_SIZE % bytesPerFrame)) // Ensure buffer size is multiple of frame size
|
|
406
|
-
|
|
410
|
+
|
|
407
411
|
var totalBytesProcessed = 0L
|
|
408
412
|
val totalRangeDuration = calculateOutputDuration(timeRanges)
|
|
409
413
|
var currentRangeProcessed = 0L
|
|
410
|
-
|
|
414
|
+
|
|
411
415
|
var lastUpdateTime = 0L
|
|
412
416
|
val updateIntervalMs = 100L // Update progress every 100ms
|
|
413
|
-
|
|
417
|
+
|
|
414
418
|
for (range in timeRanges) {
|
|
415
419
|
val startTimeMs = range["startTimeMs"] ?: 0
|
|
416
420
|
val endTimeMs = range["endTimeMs"] ?: fileDurationMs // Use actual file duration instead of Long.MAX_VALUE
|
|
417
|
-
|
|
421
|
+
|
|
418
422
|
// Calculate byte positions
|
|
419
423
|
val startByte = headerSize + ((startTimeMs * sampleRate * bytesPerFrame) / 1000)
|
|
420
424
|
val endByte = headerSize + ((endTimeMs * sampleRate * bytesPerFrame) / 1000)
|
|
421
|
-
|
|
425
|
+
|
|
422
426
|
val rangeSize = endByte - startByte
|
|
423
427
|
val rangeDuration = endTimeMs - startTimeMs
|
|
424
|
-
|
|
428
|
+
|
|
425
429
|
// Read and write the range using 'use' pattern
|
|
426
430
|
FileInputStream(inputFile).use { rangeInputStream ->
|
|
427
431
|
if (rangeInputStream.skip(startByte) != startByte) {
|
|
428
432
|
throw IOException("Failed to skip to position $startByte in input file")
|
|
429
433
|
}
|
|
430
|
-
|
|
434
|
+
|
|
431
435
|
var bytesRead: Int
|
|
432
436
|
var rangeProcessed = 0L
|
|
433
|
-
|
|
437
|
+
|
|
434
438
|
while (rangeInputStream.read(buffer).also { bytesRead = it } > 0 && rangeProcessed < rangeSize) {
|
|
435
439
|
// Ensure we don't read past the range
|
|
436
440
|
val bytesToWrite = min(bytesRead.toLong(), rangeSize - rangeProcessed).toInt()
|
|
437
|
-
|
|
441
|
+
|
|
438
442
|
outputStream.write(buffer, 0, bytesToWrite)
|
|
439
|
-
|
|
443
|
+
|
|
440
444
|
rangeProcessed += bytesToWrite
|
|
441
445
|
totalBytesProcessed += bytesToWrite
|
|
442
|
-
|
|
446
|
+
|
|
443
447
|
// Calculate progress based on time for consistency with compressed audio
|
|
444
448
|
val currentTimeInRange = (rangeProcessed * 1000) / (sampleRate * bytesPerFrame)
|
|
445
|
-
|
|
449
|
+
|
|
446
450
|
// Calculate overall progress directly
|
|
447
451
|
val overallProgress = (currentRangeProcessed + currentTimeInRange).toFloat() / totalRangeDuration
|
|
448
|
-
|
|
452
|
+
|
|
449
453
|
val currentTime = System.currentTimeMillis()
|
|
450
454
|
if (currentTime - lastUpdateTime >= updateIntervalMs) {
|
|
451
455
|
progressCallback(overallProgress * 100, bytesToWrite.toLong(), totalRangeDuration)
|
|
452
456
|
lastUpdateTime = currentTime
|
|
453
457
|
}
|
|
454
|
-
|
|
458
|
+
|
|
455
459
|
// Break if we've read the entire range
|
|
456
460
|
if (rangeProcessed >= rangeSize) {
|
|
457
461
|
break
|
|
458
462
|
}
|
|
459
463
|
}
|
|
460
464
|
}
|
|
461
|
-
|
|
465
|
+
|
|
462
466
|
currentRangeProcessed += rangeDuration
|
|
463
467
|
}
|
|
464
468
|
}
|
|
465
|
-
|
|
469
|
+
|
|
466
470
|
// Update WAV header with correct file size
|
|
467
471
|
fileHandler.updateWavHeader(outputFile)
|
|
468
472
|
}
|
|
469
|
-
|
|
473
|
+
|
|
470
474
|
/**
|
|
471
475
|
* Optimized version of processToWav that processes audio ranges more efficiently
|
|
472
476
|
*/
|
|
@@ -478,74 +482,32 @@ class AudioTrimmer(
|
|
|
478
482
|
progressListener: ProgressListener?
|
|
479
483
|
) {
|
|
480
484
|
val audioProcessor = AudioProcessor(context.filesDir)
|
|
481
|
-
|
|
482
|
-
val mimeType = MediaMetadataRetriever().apply {
|
|
483
|
-
setDataSource(context, inputUri)
|
|
484
|
-
}.extractMetadata(MediaMetadataRetriever.METADATA_KEY_MIMETYPE)
|
|
485
|
-
mimeType == "audio/wav" || mimeType == "audio/x-wav"
|
|
486
|
-
} catch (e: Exception) {
|
|
487
|
-
false
|
|
488
|
-
}
|
|
489
|
-
|
|
485
|
+
|
|
490
486
|
// Create output file with WAV header
|
|
491
487
|
FileOutputStream(outputFile).use { outputStream ->
|
|
492
488
|
// We'll write the header at the end when we know the total size
|
|
493
489
|
var totalBytes = 0L
|
|
494
490
|
var totalProgress: Float
|
|
495
491
|
val totalRanges = timeRanges.size
|
|
496
|
-
|
|
492
|
+
|
|
497
493
|
// Process each time range
|
|
498
494
|
for ((index, range) in timeRanges.withIndex()) {
|
|
499
495
|
val startTimeMs = range["startTimeMs"] ?: 0
|
|
500
496
|
val endTimeMs = range["endTimeMs"] ?: 0
|
|
501
497
|
val rangeDuration = endTimeMs - startTimeMs
|
|
502
|
-
|
|
498
|
+
|
|
503
499
|
Log.d(TAG, "Processing range $index: $startTimeMs-$endTimeMs ms")
|
|
504
|
-
|
|
505
|
-
// Load just this range
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
// For compressed audio, use the new optimized method
|
|
516
|
-
audioProcessor.decodeAudioRangeToPCM(
|
|
517
|
-
fileUri = inputUri.toString(),
|
|
518
|
-
startTimeMs = startTimeMs,
|
|
519
|
-
endTimeMs = endTimeMs
|
|
520
|
-
)?.let { decodedData ->
|
|
521
|
-
// Apply any format conversion if needed
|
|
522
|
-
if (config.targetSampleRate != null && config.targetSampleRate != decodedData.sampleRate ||
|
|
523
|
-
config.targetChannels != null && config.targetChannels != decodedData.channels) {
|
|
524
|
-
|
|
525
|
-
// Need to resample or convert channels
|
|
526
|
-
val resampledData = audioProcessor.processAudio(
|
|
527
|
-
decodedData.data,
|
|
528
|
-
decodedData.sampleRate,
|
|
529
|
-
decodedData.channels,
|
|
530
|
-
config.targetSampleRate ?: decodedData.sampleRate,
|
|
531
|
-
config.targetChannels ?: decodedData.channels,
|
|
532
|
-
config.normalizeAudio
|
|
533
|
-
)
|
|
534
|
-
|
|
535
|
-
AudioProcessor.AudioData(
|
|
536
|
-
data = resampledData,
|
|
537
|
-
sampleRate = config.targetSampleRate ?: decodedData.sampleRate,
|
|
538
|
-
channels = config.targetChannels ?: decodedData.channels,
|
|
539
|
-
bitDepth = decodedData.bitDepth,
|
|
540
|
-
durationMs = decodedData.durationMs
|
|
541
|
-
)
|
|
542
|
-
} else {
|
|
543
|
-
// No conversion needed
|
|
544
|
-
decodedData
|
|
545
|
-
}
|
|
546
|
-
}
|
|
547
|
-
} ?: throw IOException("Failed to load audio range $startTimeMs-$endTimeMs")
|
|
548
|
-
|
|
500
|
+
|
|
501
|
+
// Load just this range through AudioProcessor's canonical range path. It handles
|
|
502
|
+
// WAV and compressed inputs, target sample rate/channel/bit depth conversion, and
|
|
503
|
+
// final-byte metadata consistently for trimAudio and extractAudioData.
|
|
504
|
+
val audioData = audioProcessor.loadAudioRange(
|
|
505
|
+
fileUri = inputUri.toString(),
|
|
506
|
+
startTimeMs = startTimeMs,
|
|
507
|
+
endTimeMs = endTimeMs,
|
|
508
|
+
config = config
|
|
509
|
+
) ?: throw IOException("Failed to load audio range $startTimeMs-$endTimeMs")
|
|
510
|
+
|
|
549
511
|
// For the first range, write the WAV header
|
|
550
512
|
if (index == 0) {
|
|
551
513
|
fileHandler.writeWavHeader(
|
|
@@ -555,23 +517,23 @@ class AudioTrimmer(
|
|
|
555
517
|
audioData.bitDepth
|
|
556
518
|
)
|
|
557
519
|
}
|
|
558
|
-
|
|
520
|
+
|
|
559
521
|
// Write the PCM data for this range
|
|
560
522
|
outputStream.write(audioData.data)
|
|
561
523
|
totalBytes += audioData.data.size
|
|
562
|
-
|
|
524
|
+
|
|
563
525
|
// Update progress
|
|
564
526
|
val rangeProgress = (index + 1).toFloat() / totalRanges
|
|
565
527
|
totalProgress = rangeProgress * 100
|
|
566
528
|
progressListener?.onProgress(totalProgress, audioData.data.size.toLong(), rangeDuration)
|
|
567
|
-
|
|
529
|
+
|
|
568
530
|
Log.d(TAG, "Range $index processed: ${audioData.data.size} bytes, ${audioData.durationMs} ms")
|
|
569
531
|
}
|
|
570
532
|
}
|
|
571
|
-
|
|
533
|
+
|
|
572
534
|
// Update WAV header with correct file size
|
|
573
535
|
fileHandler.updateWavHeader(outputFile)
|
|
574
|
-
|
|
536
|
+
|
|
575
537
|
Log.d(TAG, "WAV file created successfully: ${outputFile.absolutePath}")
|
|
576
538
|
}
|
|
577
539
|
|
|
@@ -586,18 +548,18 @@ class AudioTrimmer(
|
|
|
586
548
|
) {
|
|
587
549
|
// Increase MediaCodec buffer size
|
|
588
550
|
val largerInputBufferSize = 65536 // 64KB
|
|
589
|
-
|
|
551
|
+
|
|
590
552
|
Log.d(TAG, "Encoding WAV to AAC: ${inputWavFile.absolutePath} -> ${outputAacFile.absolutePath}")
|
|
591
|
-
|
|
553
|
+
|
|
592
554
|
// Get WAV file details
|
|
593
555
|
val audioProcessor = AudioProcessor(context.filesDir)
|
|
594
556
|
val audioFormat = audioProcessor.getAudioFormat(inputWavFile.absolutePath)
|
|
595
557
|
?: throw IOException("Failed to get audio format from WAV file")
|
|
596
|
-
|
|
597
|
-
val sampleRate = formatOptions
|
|
598
|
-
val channels = formatOptions
|
|
599
|
-
val bitrate = formatOptions
|
|
600
|
-
|
|
558
|
+
|
|
559
|
+
val sampleRate = numberOption(formatOptions, "sampleRate") ?: audioFormat.sampleRate
|
|
560
|
+
val channels = numberOption(formatOptions, "channels") ?: audioFormat.channels
|
|
561
|
+
val bitrate = numberOption(formatOptions, "bitrate") ?: 128000
|
|
562
|
+
|
|
601
563
|
// Load the entire WAV file as PCM data
|
|
602
564
|
val audioData = audioProcessor.loadAudioFromAnyFormat(
|
|
603
565
|
inputWavFile.absolutePath,
|
|
@@ -608,22 +570,22 @@ class AudioTrimmer(
|
|
|
608
570
|
normalizeAudio = false
|
|
609
571
|
)
|
|
610
572
|
) ?: throw IOException("Failed to load WAV file")
|
|
611
|
-
|
|
573
|
+
|
|
612
574
|
// Set up MediaCodec for AAC encoding
|
|
613
575
|
val mediaFormat = MediaFormat.createAudioFormat(MediaFormat.MIMETYPE_AUDIO_AAC, sampleRate, channels)
|
|
614
576
|
mediaFormat.setInteger(MediaFormat.KEY_BIT_RATE, bitrate)
|
|
615
577
|
mediaFormat.setInteger(MediaFormat.KEY_AAC_PROFILE, android.media.MediaCodecInfo.CodecProfileLevel.AACObjectLC)
|
|
616
578
|
mediaFormat.setInteger(MediaFormat.KEY_MAX_INPUT_SIZE, largerInputBufferSize)
|
|
617
|
-
|
|
579
|
+
|
|
618
580
|
val encoder = android.media.MediaCodec.createEncoderByType(MediaFormat.MIMETYPE_AUDIO_AAC)
|
|
619
581
|
encoder.configure(mediaFormat, null, null, android.media.MediaCodec.CONFIGURE_FLAG_ENCODE)
|
|
620
582
|
encoder.start()
|
|
621
|
-
|
|
583
|
+
|
|
622
584
|
// Set up MediaMuxer for MP4 container
|
|
623
585
|
val muxer = MediaMuxer(outputAacFile.absolutePath, MediaMuxer.OutputFormat.MUXER_OUTPUT_MPEG_4)
|
|
624
586
|
var trackIndex = -1
|
|
625
587
|
var muxerStarted = false
|
|
626
|
-
|
|
588
|
+
|
|
627
589
|
try {
|
|
628
590
|
val bufferInfo = android.media.MediaCodec.BufferInfo()
|
|
629
591
|
val timeoutUs = 10000L
|
|
@@ -632,18 +594,18 @@ class AudioTrimmer(
|
|
|
632
594
|
val totalBytes = audioData.data.size.toLong()
|
|
633
595
|
var allInputSubmitted = false
|
|
634
596
|
var encoderDone = false
|
|
635
|
-
|
|
597
|
+
|
|
636
598
|
// Calculate bytes per frame
|
|
637
599
|
val bytesPerSample = audioData.bitDepth / 8
|
|
638
600
|
val bytesPerFrame = bytesPerSample * audioData.channels
|
|
639
601
|
val frameSizeInBytes = 65536 // 64KB buffer instead of smaller chunks
|
|
640
|
-
|
|
602
|
+
|
|
641
603
|
// Process the PCM data in larger chunks
|
|
642
604
|
var inputOffset = 0
|
|
643
|
-
|
|
605
|
+
|
|
644
606
|
var lastUpdateTime = 0L
|
|
645
607
|
val updateIntervalMs = 100L
|
|
646
|
-
|
|
608
|
+
|
|
647
609
|
while (!encoderDone) {
|
|
648
610
|
// Submit input data if we have any left
|
|
649
611
|
if (!allInputSubmitted) {
|
|
@@ -651,21 +613,21 @@ class AudioTrimmer(
|
|
|
651
613
|
if (inputBufferIndex >= 0) {
|
|
652
614
|
val inputBuffer = encoder.getInputBuffer(inputBufferIndex)
|
|
653
615
|
inputBuffer?.clear()
|
|
654
|
-
|
|
616
|
+
|
|
655
617
|
// Calculate how many bytes to read
|
|
656
618
|
val bytesToRead = if (inputOffset + frameSizeInBytes <= audioData.data.size) {
|
|
657
619
|
frameSizeInBytes
|
|
658
620
|
} else {
|
|
659
621
|
audioData.data.size - inputOffset
|
|
660
622
|
}
|
|
661
|
-
|
|
623
|
+
|
|
662
624
|
if (bytesToRead > 0) {
|
|
663
625
|
// Copy data to the input buffer
|
|
664
626
|
inputBuffer?.put(audioData.data, inputOffset, bytesToRead)
|
|
665
|
-
|
|
627
|
+
|
|
666
628
|
// Calculate presentation time in microseconds
|
|
667
629
|
val frameDurationUs = (bytesToRead * 1000000L) / (sampleRate * bytesPerFrame)
|
|
668
|
-
|
|
630
|
+
|
|
669
631
|
// Submit the input buffer
|
|
670
632
|
encoder.queueInputBuffer(
|
|
671
633
|
inputBufferIndex,
|
|
@@ -674,12 +636,12 @@ class AudioTrimmer(
|
|
|
674
636
|
presentationTimeUs,
|
|
675
637
|
0
|
|
676
638
|
)
|
|
677
|
-
|
|
639
|
+
|
|
678
640
|
// Update state
|
|
679
641
|
presentationTimeUs += frameDurationUs
|
|
680
642
|
inputOffset += bytesToRead
|
|
681
643
|
totalBytesProcessed += bytesToRead
|
|
682
|
-
|
|
644
|
+
|
|
683
645
|
// Report progress
|
|
684
646
|
val progress = (totalBytesProcessed * 100f) / totalBytes
|
|
685
647
|
val currentTime = System.currentTimeMillis()
|
|
@@ -700,7 +662,7 @@ class AudioTrimmer(
|
|
|
700
662
|
}
|
|
701
663
|
}
|
|
702
664
|
}
|
|
703
|
-
|
|
665
|
+
|
|
704
666
|
// Get encoded output
|
|
705
667
|
val outputBufferIndex = encoder.dequeueOutputBuffer(bufferInfo, timeoutUs)
|
|
706
668
|
if (outputBufferIndex == android.media.MediaCodec.INFO_OUTPUT_FORMAT_CHANGED) {
|
|
@@ -716,25 +678,25 @@ class AudioTrimmer(
|
|
|
716
678
|
} else if (outputBufferIndex >= 0) {
|
|
717
679
|
// Got encoded data
|
|
718
680
|
val encodedData = encoder.getOutputBuffer(outputBufferIndex)
|
|
719
|
-
|
|
681
|
+
|
|
720
682
|
if (encodedData != null) {
|
|
721
683
|
if ((bufferInfo.flags and android.media.MediaCodec.BUFFER_FLAG_CODEC_CONFIG) != 0) {
|
|
722
684
|
// Codec config data, not actual media data
|
|
723
685
|
bufferInfo.size = 0
|
|
724
686
|
}
|
|
725
|
-
|
|
687
|
+
|
|
726
688
|
if (bufferInfo.size > 0 && muxerStarted) {
|
|
727
689
|
// Adjust buffer info offset and size for the buffer
|
|
728
690
|
encodedData.position(bufferInfo.offset)
|
|
729
691
|
encodedData.limit(bufferInfo.offset + bufferInfo.size)
|
|
730
|
-
|
|
692
|
+
|
|
731
693
|
// Write to muxer
|
|
732
694
|
muxer.writeSampleData(trackIndex, encodedData, bufferInfo)
|
|
733
695
|
}
|
|
734
|
-
|
|
696
|
+
|
|
735
697
|
// Release the output buffer
|
|
736
698
|
encoder.releaseOutputBuffer(outputBufferIndex, false)
|
|
737
|
-
|
|
699
|
+
|
|
738
700
|
// Check if we're done
|
|
739
701
|
if ((bufferInfo.flags and android.media.MediaCodec.BUFFER_FLAG_END_OF_STREAM) != 0) {
|
|
740
702
|
encoderDone = true
|
|
@@ -742,10 +704,10 @@ class AudioTrimmer(
|
|
|
742
704
|
}
|
|
743
705
|
}
|
|
744
706
|
}
|
|
745
|
-
|
|
707
|
+
|
|
746
708
|
// Make sure we report 100% progress
|
|
747
709
|
progressListener?.onProgress(100f, totalBytes, totalBytes)
|
|
748
|
-
|
|
710
|
+
|
|
749
711
|
} finally {
|
|
750
712
|
// Clean up resources
|
|
751
713
|
try {
|
|
@@ -754,7 +716,7 @@ class AudioTrimmer(
|
|
|
754
716
|
} catch (e: Exception) {
|
|
755
717
|
Log.w(TAG, "Error releasing encoder: ${e.message}")
|
|
756
718
|
}
|
|
757
|
-
|
|
719
|
+
|
|
758
720
|
if (muxerStarted) {
|
|
759
721
|
try {
|
|
760
722
|
muxer.stop()
|
|
@@ -762,14 +724,14 @@ class AudioTrimmer(
|
|
|
762
724
|
Log.w(TAG, "Error stopping muxer: ${e.message}")
|
|
763
725
|
}
|
|
764
726
|
}
|
|
765
|
-
|
|
727
|
+
|
|
766
728
|
try {
|
|
767
729
|
muxer.release()
|
|
768
730
|
} catch (e: Exception) {
|
|
769
731
|
Log.w(TAG, "Error releasing muxer: ${e.message}")
|
|
770
732
|
}
|
|
771
733
|
}
|
|
772
|
-
|
|
734
|
+
|
|
773
735
|
Log.d(TAG, "WAV to AAC encoding completed successfully")
|
|
774
736
|
}
|
|
775
737
|
|
|
@@ -783,7 +745,7 @@ class AudioTrimmer(
|
|
|
783
745
|
progressListener: ProgressListener?
|
|
784
746
|
) {
|
|
785
747
|
Log.d(TAG, "Encoding to Opus: ${outputFile.absolutePath}")
|
|
786
|
-
|
|
748
|
+
|
|
787
749
|
try {
|
|
788
750
|
// Check if Opus codec is available
|
|
789
751
|
val codecList = android.media.MediaCodecList(android.media.MediaCodecList.REGULAR_CODECS)
|
|
@@ -791,10 +753,10 @@ class AudioTrimmer(
|
|
|
791
753
|
.filter { it.isEncoder && it.supportedTypes.contains(MediaFormat.MIMETYPE_AUDIO_OPUS) }
|
|
792
754
|
.map { it.name }
|
|
793
755
|
.firstOrNull()
|
|
794
|
-
|
|
756
|
+
|
|
795
757
|
if (opusCodecName == null) {
|
|
796
758
|
Log.w(TAG, "Opus encoder not available, falling back to AAC")
|
|
797
|
-
|
|
759
|
+
|
|
798
760
|
// Create a temporary WAV file
|
|
799
761
|
val tempWavFile = File(context.filesDir, "temp_${System.currentTimeMillis()}.wav")
|
|
800
762
|
try {
|
|
@@ -808,14 +770,14 @@ class AudioTrimmer(
|
|
|
808
770
|
audioData.channels,
|
|
809
771
|
audioData.bitDepth
|
|
810
772
|
)
|
|
811
|
-
|
|
773
|
+
|
|
812
774
|
// Write PCM data
|
|
813
775
|
outputStream.write(audioData.data)
|
|
814
776
|
}
|
|
815
|
-
|
|
777
|
+
|
|
816
778
|
// Update WAV header with correct file size
|
|
817
779
|
audioFileHandler.updateWavHeader(tempWavFile)
|
|
818
|
-
|
|
780
|
+
|
|
819
781
|
// Now we can call encodeWavToAac with the temp file
|
|
820
782
|
encodeWavToAac(
|
|
821
783
|
tempWavFile,
|
|
@@ -831,23 +793,23 @@ class AudioTrimmer(
|
|
|
831
793
|
}
|
|
832
794
|
return
|
|
833
795
|
}
|
|
834
|
-
|
|
796
|
+
|
|
835
797
|
// Set up MediaCodec for Opus encoding
|
|
836
|
-
val sampleRate = formatOptions
|
|
837
|
-
val channels = formatOptions
|
|
838
|
-
|
|
798
|
+
val sampleRate = numberOption(formatOptions, "sampleRate") ?: audioData.sampleRate
|
|
799
|
+
val channels = numberOption(formatOptions, "channels") ?: audioData.channels
|
|
800
|
+
|
|
839
801
|
// Determine appropriate bitrate based on content type and channels
|
|
840
802
|
// For voice: 8-24kbps for mono, 16-32kbps for stereo is typically sufficient
|
|
841
803
|
val defaultBitrate = if (channels > 1) 32000 else 16000 // Lower defaults for voice
|
|
842
|
-
val bitrate = formatOptions
|
|
843
|
-
|
|
804
|
+
val bitrate = numberOption(formatOptions, "bitrate") ?: defaultBitrate
|
|
805
|
+
|
|
844
806
|
// Determine if this is voice content based on sample rate and/or explicit flag
|
|
845
807
|
val isVoiceContent = formatOptions["isVoice"] as? Boolean ?: (sampleRate <= 16000)
|
|
846
|
-
|
|
808
|
+
|
|
847
809
|
val mediaFormat = MediaFormat.createAudioFormat(MediaFormat.MIMETYPE_AUDIO_OPUS, sampleRate, channels)
|
|
848
810
|
mediaFormat.setInteger(MediaFormat.KEY_BIT_RATE, bitrate)
|
|
849
811
|
mediaFormat.setInteger(MediaFormat.KEY_MAX_INPUT_SIZE, 65536)
|
|
850
|
-
|
|
812
|
+
|
|
851
813
|
// Set complexity - lower for voice (faster encoding, still good quality)
|
|
852
814
|
// Complexity range is 0-10, with 10 being highest quality but slowest
|
|
853
815
|
val complexity = if (isVoiceContent) 5 else 7
|
|
@@ -876,12 +838,12 @@ class AudioTrimmer(
|
|
|
876
838
|
val encoder = android.media.MediaCodec.createByCodecName(opusCodecName)
|
|
877
839
|
encoder.configure(mediaFormat, null, null, android.media.MediaCodec.CONFIGURE_FLAG_ENCODE)
|
|
878
840
|
encoder.start()
|
|
879
|
-
|
|
841
|
+
|
|
880
842
|
// Set up MediaMuxer for Opus container (using OGG container)
|
|
881
843
|
val muxer = MediaMuxer(outputFile.absolutePath, MediaMuxer.OutputFormat.MUXER_OUTPUT_OGG)
|
|
882
844
|
var trackIndex = -1
|
|
883
845
|
var muxerStarted = false
|
|
884
|
-
|
|
846
|
+
|
|
885
847
|
try {
|
|
886
848
|
val bufferInfo = android.media.MediaCodec.BufferInfo()
|
|
887
849
|
val timeoutUs = 10000L
|
|
@@ -890,18 +852,18 @@ class AudioTrimmer(
|
|
|
890
852
|
val totalBytes = audioData.data.size.toLong()
|
|
891
853
|
var allInputSubmitted = false
|
|
892
854
|
var encoderDone = false
|
|
893
|
-
|
|
855
|
+
|
|
894
856
|
// Calculate bytes per frame
|
|
895
857
|
val bytesPerSample = audioData.bitDepth / 8
|
|
896
858
|
val bytesPerFrame = bytesPerSample * audioData.channels
|
|
897
859
|
val frameSizeInBytes = 65536 // 64KB buffer instead of smaller chunks
|
|
898
|
-
|
|
860
|
+
|
|
899
861
|
// Process the PCM data in chunks
|
|
900
862
|
var inputOffset = 0
|
|
901
|
-
|
|
863
|
+
|
|
902
864
|
var lastUpdateTime = 0L
|
|
903
865
|
val updateIntervalMs = 100L
|
|
904
|
-
|
|
866
|
+
|
|
905
867
|
while (!encoderDone) {
|
|
906
868
|
// Submit input data if we have any left
|
|
907
869
|
if (!allInputSubmitted) {
|
|
@@ -909,21 +871,21 @@ class AudioTrimmer(
|
|
|
909
871
|
if (inputBufferIndex >= 0) {
|
|
910
872
|
val inputBuffer = encoder.getInputBuffer(inputBufferIndex)
|
|
911
873
|
inputBuffer?.clear()
|
|
912
|
-
|
|
874
|
+
|
|
913
875
|
// Calculate how many bytes to read
|
|
914
876
|
val bytesToRead = if (inputOffset + frameSizeInBytes <= audioData.data.size) {
|
|
915
877
|
frameSizeInBytes
|
|
916
878
|
} else {
|
|
917
879
|
audioData.data.size - inputOffset
|
|
918
880
|
}
|
|
919
|
-
|
|
881
|
+
|
|
920
882
|
if (bytesToRead > 0) {
|
|
921
883
|
// Copy data to the input buffer
|
|
922
884
|
inputBuffer?.put(audioData.data, inputOffset, bytesToRead)
|
|
923
|
-
|
|
885
|
+
|
|
924
886
|
// Calculate presentation time in microseconds
|
|
925
887
|
val frameDurationUs = (bytesToRead * 1000000L) / (sampleRate * bytesPerFrame)
|
|
926
|
-
|
|
888
|
+
|
|
927
889
|
// Submit the input buffer
|
|
928
890
|
encoder.queueInputBuffer(
|
|
929
891
|
inputBufferIndex,
|
|
@@ -932,12 +894,12 @@ class AudioTrimmer(
|
|
|
932
894
|
presentationTimeUs,
|
|
933
895
|
0
|
|
934
896
|
)
|
|
935
|
-
|
|
897
|
+
|
|
936
898
|
// Update state
|
|
937
899
|
presentationTimeUs += frameDurationUs
|
|
938
900
|
inputOffset += bytesToRead
|
|
939
901
|
totalBytesProcessed += bytesToRead
|
|
940
|
-
|
|
902
|
+
|
|
941
903
|
// Report progress
|
|
942
904
|
val progress = (totalBytesProcessed * 100f) / totalBytes
|
|
943
905
|
val currentTime = System.currentTimeMillis()
|
|
@@ -958,7 +920,7 @@ class AudioTrimmer(
|
|
|
958
920
|
}
|
|
959
921
|
}
|
|
960
922
|
}
|
|
961
|
-
|
|
923
|
+
|
|
962
924
|
// Get encoded output
|
|
963
925
|
val outputBufferIndex = encoder.dequeueOutputBuffer(bufferInfo, timeoutUs)
|
|
964
926
|
if (outputBufferIndex == android.media.MediaCodec.INFO_OUTPUT_FORMAT_CHANGED) {
|
|
@@ -974,25 +936,25 @@ class AudioTrimmer(
|
|
|
974
936
|
} else if (outputBufferIndex >= 0) {
|
|
975
937
|
// Got encoded data
|
|
976
938
|
val encodedData = encoder.getOutputBuffer(outputBufferIndex)
|
|
977
|
-
|
|
939
|
+
|
|
978
940
|
if (encodedData != null) {
|
|
979
941
|
if ((bufferInfo.flags and android.media.MediaCodec.BUFFER_FLAG_CODEC_CONFIG) != 0) {
|
|
980
942
|
// Codec config data, not actual media data
|
|
981
943
|
bufferInfo.size = 0
|
|
982
944
|
}
|
|
983
|
-
|
|
945
|
+
|
|
984
946
|
if (bufferInfo.size > 0 && muxerStarted) {
|
|
985
947
|
// Adjust buffer info offset and size for the buffer
|
|
986
948
|
encodedData.position(bufferInfo.offset)
|
|
987
949
|
encodedData.limit(bufferInfo.offset + bufferInfo.size)
|
|
988
|
-
|
|
950
|
+
|
|
989
951
|
// Write to muxer
|
|
990
952
|
muxer.writeSampleData(trackIndex, encodedData, bufferInfo)
|
|
991
953
|
}
|
|
992
|
-
|
|
954
|
+
|
|
993
955
|
// Release the output buffer
|
|
994
956
|
encoder.releaseOutputBuffer(outputBufferIndex, false)
|
|
995
|
-
|
|
957
|
+
|
|
996
958
|
// Check if we're done
|
|
997
959
|
if ((bufferInfo.flags and android.media.MediaCodec.BUFFER_FLAG_END_OF_STREAM) != 0) {
|
|
998
960
|
encoderDone = true
|
|
@@ -1000,10 +962,10 @@ class AudioTrimmer(
|
|
|
1000
962
|
}
|
|
1001
963
|
}
|
|
1002
964
|
}
|
|
1003
|
-
|
|
965
|
+
|
|
1004
966
|
// Make sure we report 100% progress
|
|
1005
967
|
progressListener?.onProgress(100f, totalBytes, totalBytes)
|
|
1006
|
-
|
|
968
|
+
|
|
1007
969
|
} finally {
|
|
1008
970
|
// Clean up resources
|
|
1009
971
|
try {
|
|
@@ -1012,7 +974,7 @@ class AudioTrimmer(
|
|
|
1012
974
|
} catch (e: Exception) {
|
|
1013
975
|
Log.w(TAG, "Error releasing encoder: ${e.message}")
|
|
1014
976
|
}
|
|
1015
|
-
|
|
977
|
+
|
|
1016
978
|
if (muxerStarted) {
|
|
1017
979
|
try {
|
|
1018
980
|
muxer.stop()
|
|
@@ -1020,21 +982,21 @@ class AudioTrimmer(
|
|
|
1020
982
|
Log.w(TAG, "Error stopping muxer: ${e.message}")
|
|
1021
983
|
}
|
|
1022
984
|
}
|
|
1023
|
-
|
|
985
|
+
|
|
1024
986
|
try {
|
|
1025
987
|
muxer.release()
|
|
1026
988
|
} catch (e: Exception) {
|
|
1027
989
|
Log.w(TAG, "Error releasing muxer: ${e.message}")
|
|
1028
990
|
}
|
|
1029
991
|
}
|
|
1030
|
-
|
|
992
|
+
|
|
1031
993
|
Log.d(TAG, "Opus encoding completed successfully")
|
|
1032
994
|
} catch (e: Exception) {
|
|
1033
995
|
Log.e(TAG, "Error encoding to Opus: ${e.message}", e)
|
|
1034
|
-
|
|
996
|
+
|
|
1035
997
|
// Fall back to AAC if Opus encoding fails
|
|
1036
998
|
Log.w(TAG, "Opus encoding failed, falling back to AAC")
|
|
1037
|
-
|
|
999
|
+
|
|
1038
1000
|
// Create a temporary WAV file
|
|
1039
1001
|
val tempWavFile = File(context.filesDir, "temp_${System.currentTimeMillis()}.wav")
|
|
1040
1002
|
try {
|
|
@@ -1050,7 +1012,7 @@ class AudioTrimmer(
|
|
|
1050
1012
|
outputStream.write(audioData.data)
|
|
1051
1013
|
}
|
|
1052
1014
|
audioFileHandler.updateWavHeader(tempWavFile)
|
|
1053
|
-
|
|
1015
|
+
|
|
1054
1016
|
// Encode to AAC
|
|
1055
1017
|
encodeWavToAac(
|
|
1056
1018
|
tempWavFile,
|
|
@@ -1063,7 +1025,7 @@ class AudioTrimmer(
|
|
|
1063
1025
|
tempWavFile.delete()
|
|
1064
1026
|
}
|
|
1065
1027
|
}
|
|
1066
|
-
|
|
1028
|
+
|
|
1067
1029
|
throw IOException("Failed to encode to Opus: ${e.message}", e)
|
|
1068
1030
|
}
|
|
1069
1031
|
}
|
|
@@ -1075,16 +1037,16 @@ class AudioTrimmer(
|
|
|
1075
1037
|
// Estimate channels from bitrate and sample rate if not directly available
|
|
1076
1038
|
if (it > sampleRate * 16) 2 else 1
|
|
1077
1039
|
} ?: 1
|
|
1078
|
-
|
|
1040
|
+
|
|
1079
1041
|
// Bit depth is often not directly available, assume 16-bit as default
|
|
1080
1042
|
val bitDepth = 16
|
|
1081
|
-
|
|
1043
|
+
|
|
1082
1044
|
// Get duration in milliseconds
|
|
1083
1045
|
val durationMs = retriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_DURATION)?.toLongOrNull() ?: 0L
|
|
1084
|
-
|
|
1046
|
+
|
|
1085
1047
|
// Get MIME type
|
|
1086
1048
|
val mimeType = retriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_MIMETYPE) ?: "audio/mpeg"
|
|
1087
|
-
|
|
1049
|
+
|
|
1088
1050
|
return AudioFormat(sampleRate, channels, bitDepth, durationMs, mimeType)
|
|
1089
1051
|
}
|
|
1090
1052
|
|
|
@@ -1096,4 +1058,4 @@ class AudioTrimmer(
|
|
|
1096
1058
|
val durationMs: Long = 0,
|
|
1097
1059
|
val mimeType: String = "audio/mpeg"
|
|
1098
1060
|
)
|
|
1099
|
-
}
|
|
1061
|
+
}
|