使用MediaCodec和MediaMuxer编写PCM编码的声音,并带有沉默差距
我正在尝试在Android上制作一个简单的“单击轨道” - 以文件为文件。我有一个用于声音的PCM编码数据,并且有一些有限的间隙序列作为输入(表示为click> clicktrack
类)。我想要一个可播放的.m4a
文件作为输出,并在正确呈现的差距上重复该声音。
问题在于我以半浪费状态获取文件 - 它在开始时会尽快播放声音的所有重复,然后在轨道的持续时间内保持沉默。轨道的持续时间恰好是正确的,因此演示时间似乎是正确的。
现在代码:
fun render(clickTrack: ClickTrack, onProgress: (Float) -> Unit, onFinished: () -> Unit): File? {
var muxer: MediaMuxer? = null
var codec: MediaCodec? = null
try {
val audioFormat = MediaFormat.createAudioFormat(MediaFormat.MIMETYPE_AUDIO_AAC, 44100, 2)
.apply {
setInteger(MediaFormat.KEY_BIT_RATE, 96 * 1024)
}
val outputFile = File.createTempFile("click_track_export", ".m4a", context.cacheDir)
muxer = MediaMuxer(outputFile.path, MediaMuxer.OutputFormat.MUXER_OUTPUT_MPEG_4)
val codecName = MediaCodecList(MediaCodecList.REGULAR_CODECS).findEncoderForFormat(audioFormat)!!
codec = MediaCodec.createByCodecName(codecName)
codec.configure(audioFormat, null, null, MediaCodec.CONFIGURE_FLAG_ENCODE)
codec.start()
// Converts click track to sequence of sound buffers (all the same) with
// timestamps (computed using gaps) for convenience. Gaps are not presented
// in buffers in order to conserve memory
val samples = clickTrack.toSamples()
val bytesToWrite = samples.sumOf { it.data.data.size.toLong() }
val bufferInfo = MediaCodec.BufferInfo()
var bytesWritten = 0L
var index = 0
var endOfInput = samples.isEmpty()
var endOfOutput = samples.isEmpty()
var sample = samples.getOrNull(index)
var sampleBuffer: ByteBuffer? = null
while (!endOfInput || !endOfOutput) {
if (!endOfInput) {
if (sampleBuffer == null || !sampleBuffer.hasRemaining()) {
sample = samples[index]
sampleBuffer = ByteBuffer.wrap(samples[index].data.data)
++index
}
sample!!
sampleBuffer!!
val inputBufferIndex = codec.dequeueInputBuffer(0L)
if (inputBufferIndex >= 0) {
val inputBuffer = codec.getInputBuffer(inputBufferIndex)!!
while (sampleBuffer.hasRemaining() && inputBuffer.hasRemaining()) {
inputBuffer.put(sampleBuffer.get())
++bytesWritten
}
onProgress(bytesWritten.toFloat() / bytesToWrite)
endOfInput = !sampleBuffer.hasRemaining() && index == samples.size
codec.queueInputBuffer(
inputBufferIndex,
0,
inputBuffer.position(),
sample.timestampUs,
if (endOfInput) MediaCodec.BUFFER_FLAG_END_OF_STREAM else 0
)
}
}
if (!endOfOutput) {
val outputBufferIndex = codec.dequeueOutputBuffer(bufferInfo, 0L)
if (outputBufferIndex >= 0) {
val outputBuffer = codec.getOutputBuffer(outputBufferIndex)!!
muxer.writeSampleData(0, outputBuffer, bufferInfo)
codec.releaseOutputBuffer(outputBufferIndex, false)
} else if (outputBufferIndex == MediaCodec.INFO_OUTPUT_FORMAT_CHANGED) {
// Not using `audioFormat` because of https://developer.android.com/reference/android/media/MediaCodec#CSD
muxer.addTrack(codec.outputFormat)
muxer.start()
}
endOfOutput = bufferInfo.flags and MediaCodec.BUFFER_FLAG_END_OF_STREAM != 0
}
}
return outputFile
} catch (t: Throwable) {
Timber.e(t, "Failed to render track")
} finally {
try {
codec?.stop()
} catch (t: Throwable) {
Timber.e(t, "Failed to stop code")
} finally {
codec?.release()
}
try {
muxer?.stop()
} catch (t: Throwable) {
Timber.e(t, "Failed to stop muxer")
} finally {
muxer?.release()
}
onFinished()
}
return null
}
// Classes descriptions
class Sample(
val data: PcmData,
val timestampUs: Long,
)
class PcmData(
val pcmEncoding: Int,
val sampleRate: Int,
val channelCount: Int,
val data: ByteArray,
)
I'm trying to make a simple "click track"-to-file renderer on Android. I have a PCM encoded data for a sound and some finite gap sequence as an input (represented as ClickTrack
class). I want a playable .m4a
file as an output with that sound repeating over the gaps rendered properly.
The problem is that I'm getting a file in semi-corrupted state - it plays all repetitions of the sound in the beginning as fast as it can and then the silence for the duration of the track. The duration of the track happens to be correct, so it seems that presentation times are correct.
Now the code:
fun render(clickTrack: ClickTrack, onProgress: (Float) -> Unit, onFinished: () -> Unit): File? {
var muxer: MediaMuxer? = null
var codec: MediaCodec? = null
try {
val audioFormat = MediaFormat.createAudioFormat(MediaFormat.MIMETYPE_AUDIO_AAC, 44100, 2)
.apply {
setInteger(MediaFormat.KEY_BIT_RATE, 96 * 1024)
}
val outputFile = File.createTempFile("click_track_export", ".m4a", context.cacheDir)
muxer = MediaMuxer(outputFile.path, MediaMuxer.OutputFormat.MUXER_OUTPUT_MPEG_4)
val codecName = MediaCodecList(MediaCodecList.REGULAR_CODECS).findEncoderForFormat(audioFormat)!!
codec = MediaCodec.createByCodecName(codecName)
codec.configure(audioFormat, null, null, MediaCodec.CONFIGURE_FLAG_ENCODE)
codec.start()
// Converts click track to sequence of sound buffers (all the same) with
// timestamps (computed using gaps) for convenience. Gaps are not presented
// in buffers in order to conserve memory
val samples = clickTrack.toSamples()
val bytesToWrite = samples.sumOf { it.data.data.size.toLong() }
val bufferInfo = MediaCodec.BufferInfo()
var bytesWritten = 0L
var index = 0
var endOfInput = samples.isEmpty()
var endOfOutput = samples.isEmpty()
var sample = samples.getOrNull(index)
var sampleBuffer: ByteBuffer? = null
while (!endOfInput || !endOfOutput) {
if (!endOfInput) {
if (sampleBuffer == null || !sampleBuffer.hasRemaining()) {
sample = samples[index]
sampleBuffer = ByteBuffer.wrap(samples[index].data.data)
++index
}
sample!!
sampleBuffer!!
val inputBufferIndex = codec.dequeueInputBuffer(0L)
if (inputBufferIndex >= 0) {
val inputBuffer = codec.getInputBuffer(inputBufferIndex)!!
while (sampleBuffer.hasRemaining() && inputBuffer.hasRemaining()) {
inputBuffer.put(sampleBuffer.get())
++bytesWritten
}
onProgress(bytesWritten.toFloat() / bytesToWrite)
endOfInput = !sampleBuffer.hasRemaining() && index == samples.size
codec.queueInputBuffer(
inputBufferIndex,
0,
inputBuffer.position(),
sample.timestampUs,
if (endOfInput) MediaCodec.BUFFER_FLAG_END_OF_STREAM else 0
)
}
}
if (!endOfOutput) {
val outputBufferIndex = codec.dequeueOutputBuffer(bufferInfo, 0L)
if (outputBufferIndex >= 0) {
val outputBuffer = codec.getOutputBuffer(outputBufferIndex)!!
muxer.writeSampleData(0, outputBuffer, bufferInfo)
codec.releaseOutputBuffer(outputBufferIndex, false)
} else if (outputBufferIndex == MediaCodec.INFO_OUTPUT_FORMAT_CHANGED) {
// Not using `audioFormat` because of https://developer.android.com/reference/android/media/MediaCodec#CSD
muxer.addTrack(codec.outputFormat)
muxer.start()
}
endOfOutput = bufferInfo.flags and MediaCodec.BUFFER_FLAG_END_OF_STREAM != 0
}
}
return outputFile
} catch (t: Throwable) {
Timber.e(t, "Failed to render track")
} finally {
try {
codec?.stop()
} catch (t: Throwable) {
Timber.e(t, "Failed to stop code")
} finally {
codec?.release()
}
try {
muxer?.stop()
} catch (t: Throwable) {
Timber.e(t, "Failed to stop muxer")
} finally {
muxer?.release()
}
onFinished()
}
return null
}
// Classes descriptions
class Sample(
val data: PcmData,
val timestampUs: Long,
)
class PcmData(
val pcmEncoding: Int,
val sampleRate: Int,
val channelCount: Int,
val data: ByteArray,
)
如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

绑定邮箱获取回复消息
由于您还没有绑定你的真实邮箱,如果其他用户或者作者回复了您的评论,将不能在第一时间通知您!
发布评论
评论(1)
原来,我误解了
呈现时间
queueInputBuffer中的参数
方法。它 正如我想的那样,为您写下沉默框架。如果您碰巧拥有B框架等,这只是用于AV同步和订购的编码器/Muxer的提示。对于只有音频文件,我将其制作到全部0L
,并且效果很好。这实际上是错误的,并且没有在Android棉花糖上工作。无论哪种方式,您都应计算足够的演示时间。
另一个错误是编写沉默,这不是PCM帧大小的倍数(即样本尺寸 *通道计数)。如果您不这样做,则最终会有音频故障。
因此,最后我得到了此代码,用于生成完整的
bytearray
准备MediaCodec
消耗:Turned out I misunderstood
presentationTimeUs
parameter inqueueInputBuffer
method. It DOES NOT write silence frames for you as I thought. It's just a hint for encoder/muxer for av synchronization and ordering if you happen to have B-frames and such.For audio only file I made it all0L
and it worked perfectly fine.This is actually wrong and didn't work on Android Marshmallow. You should compute adequate presentation time either way.
Another mistake was writing silence that is not a multiple of PCM frame size (that is sample size * channel count). If you don't do this, you will have audio glitches in the end.
So in the end I got this code for generating complete
ByteArray
ready forMediaCodec
to consume: