Recently Written · git

subread-anki

One tap makes an Anki card on Android: word, definition, sentence, screenshot, audio. Works with any app, with SubRead Dictionary and SubRead Overlay.

git clone https://github.com/equwal/subread-anki

Log | Files | Refs


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 }