app/src/main/kotlin/space/subread/anki/AudioCut.kt (6800 bytes)
1 package space.subread.anki 2 3 import android.content.Context 4 import android.media.AudioFormat 5 import android.media.MediaCodec 6 import android.media.MediaExtractor 7 import android.media.MediaFormat 8 import androidx.core.net.toUri 9 import space.subread.anki.core.Loudness 10 import java.io.File 11 import java.nio.ByteOrder 12 13 /** 14 * Cuts a clip out of an audio file: the decoder gives the PCM of the range, [AacEncoder] 15 * writes it. The file can be a `content://` Uri, an `http(s)://` URL (the extractor reads 16 * the range alone, with range requests), or a `data:` Uri. 17 */ 18 object AudioCut { 19 20 private const val TIMEOUT_US = 10_000L 21 22 /** The clip from [startMs] to [endMs] of [source], as AAC in [out]. Null when nothing could be decoded. */ 23 fun cut(context: Context, source: String, startMs: Long, endMs: Long, out: File): File? { 24 if (endMs <= startMs) return null 25 val extractor = MediaExtractor() 26 var temp: File? = null 27 try { 28 val uri = source.toUri() 29 when (uri.scheme?.lowercase()) { 30 "http", "https" -> extractor.setDataSource(source) 31 "content" -> extractor.setDataSource(context, uri, null) 32 "data" -> { 33 val bytes = Media.bytes(context, source) ?: return null 34 temp = File.createTempFile("cut", ".bin", context.cacheDir).apply { writeBytes(bytes) } 35 extractor.setDataSource(temp.path) 36 } 37 else -> return null 38 } 39 val track = (0 until extractor.trackCount).firstOrNull { 40 extractor.getTrackFormat(it).getString(MediaFormat.KEY_MIME)?.startsWith("audio/") == true 41 } ?: return null 42 val format = extractor.getTrackFormat(track) 43 format.setInteger(MediaFormat.KEY_PCM_ENCODING, AudioFormat.ENCODING_PCM_16BIT) 44 extractor.selectTrack(track) 45 extractor.seekTo(startMs * 1000, MediaExtractor.SEEK_TO_PREVIOUS_SYNC) 46 val decoded = decode(extractor, format, startMs * 1000, endMs * 1000) ?: return null 47 val (pcm, sampleRate, channels) = decoded 48 // Less than a tenth of a second is no sentence: the range was outside the file. 49 // Silence is no sentence either: the times of the sender missed the speech. 50 if (pcm.size < sampleRate / 10 * channels || Loudness.isSilent(pcm)) return null 51 AacEncoder.encode(pcm, sampleRate, channels, out) 52 return out 53 } catch (e: Exception) { 54 return null 55 } finally { 56 extractor.release() 57 temp?.delete() 58 } 59 } 60 61 /** The PCM of the range, with the sample rate and the channels that the decoder gave. */ 62 private fun decode(extractor: MediaExtractor, format: MediaFormat, startUs: Long, endUs: Long): Triple<ShortArray, Int, Int>? { 63 val mime = format.getString(MediaFormat.KEY_MIME) ?: return null 64 val decoder = MediaCodec.createDecoderByType(mime) 65 decoder.configure(format, null, null, 0) 66 decoder.start() 67 var sampleRate = format.getInteger(MediaFormat.KEY_SAMPLE_RATE) 68 var channels = format.getInteger(MediaFormat.KEY_CHANNEL_COUNT) 69 val pcm = Pcm() 70 var inputDone = false 71 var outputDone = false 72 val info = MediaCodec.BufferInfo() 73 try { 74 while (!outputDone) { 75 if (!inputDone) { 76 val index = decoder.dequeueInputBuffer(TIMEOUT_US) 77 if (index >= 0) { 78 val buffer = decoder.getInputBuffer(index)!! 79 val size = extractor.readSampleData(buffer, 0) 80 val time = extractor.sampleTime 81 if (size < 0 || time > endUs) { 82 decoder.queueInputBuffer(index, 0, 0, 0, MediaCodec.BUFFER_FLAG_END_OF_STREAM) 83 inputDone = true 84 } else { 85 decoder.queueInputBuffer(index, 0, size, time, 0) 86 extractor.advance() 87 } 88 } 89 } 90 val index = decoder.dequeueOutputBuffer(info, TIMEOUT_US) 91 when { 92 index == MediaCodec.INFO_OUTPUT_FORMAT_CHANGED -> { 93 val changed = decoder.outputFormat 94 sampleRate = changed.getInteger(MediaFormat.KEY_SAMPLE_RATE) 95 channels = changed.getInteger(MediaFormat.KEY_CHANNEL_COUNT) 96 } 97 index >= 0 -> { 98 if (info.size > 0) { 99 val buffer = decoder.getOutputBuffer(index)!! 100 buffer.position(info.offset) 101 buffer.limit(info.offset + info.size) 102 val shorts = buffer.order(ByteOrder.LITTLE_ENDIAN).asShortBuffer() 103 val frames = shorts.remaining() / channels 104 // The part of this buffer that is inside the clip. 105 val bufferStartUs = info.presentationTimeUs 106 val bufferEndUs = bufferStartUs + frames * 1_000_000L / sampleRate 107 val first = if (bufferStartUs >= startUs) 0 else ((startUs - bufferStartUs) * sampleRate / 1_000_000L).toInt().coerceIn(0, frames) 108 val last = if (bufferEndUs <= endUs) frames else ((endUs - bufferStartUs) * sampleRate / 1_000_000L).toInt().coerceIn(0, frames) 109 if (last > first) { 110 shorts.position(first * channels) 111 val chunk = ShortArray((last - first) * channels) 112 shorts.get(chunk) 113 pcm.add(chunk) 114 } 115 } 116 decoder.releaseOutputBuffer(index, false) 117 if (info.flags and MediaCodec.BUFFER_FLAG_END_OF_STREAM != 0) outputDone = true 118 } 119 } 120 } 121 } finally { 122 runCatching { decoder.stop() } 123 decoder.release() 124 } 125 return Triple(pcm.toArray(), sampleRate, channels) 126 } 127 128 /** A growing array of samples. */ 129 private class Pcm { 130 private var data = ShortArray(1 shl 16) 131 private var size = 0 132 133 fun add(chunk: ShortArray) { 134 if (size + chunk.size > data.size) data = data.copyOf(maxOf(data.size * 2, size + chunk.size)) 135 System.arraycopy(chunk, 0, data, size, chunk.size) 136 size += chunk.size 137 } 138 139 fun toArray(): ShortArray = data.copyOf(size) 140 } 141 }