app/src/main/kotlin/space/subread/anki/AacEncoder.kt (3634 bytes)
1 package space.subread.anki 2 3 import android.media.MediaCodec 4 import android.media.MediaCodecInfo 5 import android.media.MediaFormat 6 import android.media.MediaMuxer 7 import java.io.File 8 import java.nio.ByteOrder 9 10 /** 11 * Writes 16-bit PCM as AAC in an `.m4a`. AAC because every player has it, Anki on iOS 12 * included, and because Android has the encoder on each device since API 16. 13 */ 14 object AacEncoder { 15 16 private const val MIME = MediaFormat.MIMETYPE_AUDIO_AAC 17 private const val TIMEOUT_US = 10_000L 18 19 /** Encodes [pcm], interleaved with [channels] channels at [sampleRate], into [out]. */ 20 fun encode(pcm: ShortArray, sampleRate: Int, channels: Int, out: File) { 21 require(pcm.isNotEmpty()) { "no sound to encode" } 22 val format = MediaFormat.createAudioFormat(MIME, sampleRate, channels).apply { 23 setInteger(MediaFormat.KEY_AAC_PROFILE, MediaCodecInfo.CodecProfileLevel.AACObjectLC) 24 setInteger(MediaFormat.KEY_BIT_RATE, 64_000 * channels) 25 setInteger(MediaFormat.KEY_MAX_INPUT_SIZE, 64 * 1024) 26 } 27 val codec = MediaCodec.createEncoderByType(MIME) 28 codec.configure(format, null, null, MediaCodec.CONFIGURE_FLAG_ENCODE) 29 codec.start() 30 val muxer = MediaMuxer(out.path, MediaMuxer.OutputFormat.MUXER_OUTPUT_MPEG_4) 31 var track = -1 32 var fed = 0 33 var inputDone = false 34 var outputDone = false 35 val info = MediaCodec.BufferInfo() 36 try { 37 while (!outputDone) { 38 if (!inputDone) { 39 val index = codec.dequeueInputBuffer(TIMEOUT_US) 40 if (index >= 0) { 41 val buffer = codec.getInputBuffer(index)!! 42 buffer.clear() 43 // A frame must stay whole: the count is a multiple of the channels. 44 val count = minOf(buffer.capacity() / 2, pcm.size - fed) / channels * channels 45 buffer.order(ByteOrder.LITTLE_ENDIAN).asShortBuffer().put(pcm, fed, count) 46 val timeUs = fed / channels * 1_000_000L / sampleRate 47 fed += count 48 val flags = if (fed >= pcm.size) MediaCodec.BUFFER_FLAG_END_OF_STREAM else 0 49 if (flags != 0) inputDone = true 50 codec.queueInputBuffer(index, 0, count * 2, timeUs, flags) 51 } 52 } 53 val index = codec.dequeueOutputBuffer(info, TIMEOUT_US) 54 when { 55 index == MediaCodec.INFO_OUTPUT_FORMAT_CHANGED -> { 56 track = muxer.addTrack(codec.outputFormat) 57 muxer.start() 58 } 59 index >= 0 -> { 60 val buffer = codec.getOutputBuffer(index)!! 61 val config = info.flags and MediaCodec.BUFFER_FLAG_CODEC_CONFIG != 0 62 if (!config && info.size > 0 && track >= 0) { 63 buffer.position(info.offset) 64 buffer.limit(info.offset + info.size) 65 muxer.writeSampleData(track, buffer, info) 66 } 67 codec.releaseOutputBuffer(index, false) 68 if (info.flags and MediaCodec.BUFFER_FLAG_END_OF_STREAM != 0) outputDone = true 69 } 70 } 71 } 72 } finally { 73 runCatching { codec.stop() } 74 codec.release() 75 runCatching { if (track >= 0) muxer.stop() } 76 muxer.release() 77 } 78 } 79 }