在上一篇初识MediaCodec中,我们认识了MediaCodec,知道了MediaCodec的基本工作流程和开发注意事项,这一篇我将讲述如何利用MediaCodec编解码AAC。
1:MediaCodec实时采集音频并编码
我们将使用 AudioRecord 和 MediaCodec 实现这个功能,关于 AudioRecord 的使用后期我会单独讲述。
为了保证兼容性,推荐的配置是 44.1kHz、单通道、16 位精度。首先创建并配置 AudioRecord 和 MediaCodec。
// 输入源 麦克风
private val AUDIO_SOURCE = MediaRecorder.AudioSource.MIC
// 采样率 44.1kHz,所有设备都支持
private val SAMPLE_RATE = 44100
// 通道 单声道,所有设备都支持
private val CHANNEL_CONFIG = AudioFormat.CHANNEL_IN_MONO
// 精度 16 位,所有设备都支持
private val AUDIO_FORMAT = AudioFormat.ENCODING_PCM_16BIT
// 通道数 单声道
private val CHANNEL_COUNT = 1
// 比特率
private val BIT_RATE = 96000
fun createAudio() {
mBufferSizeInBytes = AudioRecord.getMinBufferSize(MediaRecorder.AudioEncoder.SAMPLE_RATE, MediaRecorder.AudioEncoder.CHANNEL_CONFIG, MediaRecorder.AudioEncoder.AUDIO_FORMAT)
if (mBufferSizeInBytes <= 0) {
throw RuntimeException("AudioRecord is not available, minBufferSize: $mBufferSizeInBytes")
}
Log.i(TAG, "createAudioRecord minBufferSize: $mBufferSizeInBytes")
mAudioRecord = AudioRecord(MediaRecorder.AudioEncoder.AUDIO_SOURCE, MediaRecorder.AudioEncoder.SAMPLE_RATE, MediaRecorder.AudioEncoder.CHANNEL_CONFIG, MediaRecorder.AudioEncoder.AUDIO_FORMAT, mBufferSizeInBytes)
val state: Int = mAudioRecord.getState()
Log.i(TAG, "createAudio state: " + state + ", initialized: " + (state == AudioRecord.STATE_INITIALIZED))
}
@Throws(IOException::class)
fun createMediaCodec() {
val mediaCodecInfo: MediaCodecInfo = CodecUtils.selectCodec(MIMETYPE_AUDIO_AAC)
?: throw RuntimeException(MIMETYPE_AUDIO_AAC + " encoder is not available")
Log.i(TAG, "createMediaCodec: mediaCodecInfo " + mediaCodecInfo.name)
val format = MediaFormat.createAudioFormat(MIMETYPE_AUDIO_AAC, SAMPLE_RATE, CHANNEL_COUNT)
format.setInteger(MediaFormat.KEY_AAC_PROFILE, MediaCodecInfo.CodecProfileLevel.AACObjectLC)
format.setInteger(MediaFormat.KEY_BIT_RATE, BIT_RATE)
mMediaCodec = MediaCodec.createEncoderByType(MIMETYPE_AUDIO_AAC)
mMediaCodec.configure(format, null, null, MediaCodec.CONFIGURE_FLAG_ENCODE)
}
然后开始录音,得到原始音频数据,再编码为 AAC 格式。这个地方会阻塞调用的线程,而且编码比较耗时,一定要在主线程之外调用。
@Throws(IOException::class)
fun start(outFile: File) {
Log.d(TAG, "start() called with: outFile = [$outFile]")
mStopped = false
val fos = FileOutputStream(outFile)
mMediaCodec.start()
mAudioRecord.startRecording()
val buffer = ByteArray(mBufferSizeInBytes)
val inputBuffers: Array<ByteBuffer> = mMediaCodec.getInputBuffers()
val outputBuffers: Array<ByteBuffer> = mMediaCodec.getOutputBuffers()
try {
while (!mStopped) {
val readSize: Int = mAudioRecord.read(buffer, 0, mBufferSizeInBytes)
if (readSize > 0) {
val inputBufferIndex: Int = mMediaCodec.dequeueInputBuffer(-1)
if (inputBufferIndex >= 0) {
val inputBuffer: ByteBuffer = inputBuffers[inputBufferIndex]
inputBuffer.clear()
inputBuffer.put(buffer)
inputBuffer.limit(buffer.size)
mMediaCodec.queueInputBuffer(inputBufferIndex, 0, readSize, 0, 0)
}
val bufferInfo: MediaCodec.BufferInfo = MediaCodec.BufferInfo()
var outputBufferIndex: Int = mMediaCodec.dequeueOutputBuffer(bufferInfo, 0)
while (outputBufferIndex >= 0) {
val outputBuffer: ByteBuffer = outputBuffers[outputBufferIndex]
outputBuffer.position(bufferInfo.offset)
outputBuffer.limit(bufferInfo.offset + bufferInfo.size)
val chunkAudio = ByteArray(bufferInfo.size + 7) // 7 is ADTS size
addADTStoPacket(chunkAudio, chunkAudio.size)
outputBuffer.get(chunkAudio, 7, bufferInfo.size)
outputBuffer.position(bufferInfo.offset)
fos.write(chunkAudio)
mMediaCodec.releaseOutputBuffer(outputBufferIndex, false)
outputBufferIndex = mMediaCodec.dequeueOutputBuffer(bufferInfo, 0)
}
} else {
Log.w(TAG, "read audio buffer error:$readSize")
break
}
}
} finally {
Log.i(TAG, "released")
mAudioRecord.stop()
mAudioRecord.release()
mMediaCodec.stop()
mMediaCodec.release()
fos.close()
}
}
AAC 是一种压缩格式,可以直接使用播放器播放。为了实现流式播放,也就是做到边下边播,我们采用 ADTS 格式。给每帧加上 7 个字节的头信息。加上头信息就是为了告诉解码器,这帧音频长度、采样率、通道是多少,每帧都携带头信息,解码器随时都可以解码播放。我们这里采用单通道、44.1KHz 采样率的头信息配置。
private fun addADTStoPacket(packet: ByteArray, packetLen: Int) {
val profile = 2 //AAC LC
val freqIdx = 4 //44.1KHz
val chanCfg = 1 //CPE
// fill in ADTS data
packet[0] = 0xFF.toByte()
packet[1] = 0xF9.toByte()
packet[2] = ((profile - 1 shl 6) + (freqIdx shl 2) + (chanCfg shr 2)).toByte()
packet[3] = ((chanCfg and 3 shl 6) + (packetLen shr 11)).toByte()
packet[4] = (packetLen and 0x7FF shr 3).toByte()
packet[5] = ((packetLen and 7 shl 5) + 0x1F).toByte()
packet[6] = 0xFC.toByte()
}
2:AAC 解码
我们可以利用 MediaExtractor 和 MediaCodec 来提取编码后的音频数据,并解压成音频源数据。
/**
* AAC 格式解码成 PCM 数据
* @param aacFile
* @param pcmFile
* @throws IOException
*/
@Throws(IOException::class)
fun decodeAacToPcm(aacFile: File, pcmFile: File?) {
val extractor = MediaExtractor()
extractor.setDataSource(aacFile.getAbsolutePath())
var mediaFormat: MediaFormat? = null
for (i in 0 until extractor.getTrackCount()) {
val format: MediaFormat = extractor.getTrackFormat(i)
val mime: String = format.getString(MediaFormat.KEY_MIME)
if (mime.startsWith("audio/")) {
extractor.selectTrack(i)
mediaFormat = format
break
}
}
if (mediaFormat == null) {
Log.e(TAG, "Invalid file with audio track.")
extractor.release()
return
}
val fosDecoder = FileOutputStream(pcmFile)
val mediaMime: String = mediaFormat.getString(MediaFormat.KEY_MIME)
Log.i(TAG, "decodeAacToPcm: mimeType: $mediaMime")
val codec: MediaCodec = MediaCodec.createDecoderByType(mediaMime)
codec.configure(mediaFormat, null, null, 0)
codec.start()
val codecInputBuffers: Array<ByteBuffer> = codec.getInputBuffers()
var codecOutputBuffers: Array<ByteBuffer> = codec.getOutputBuffers()
val kTimeOutUs: Long = 10000
val info: MediaCodec.BufferInfo = MediaCodec.BufferInfo()
var sawInputEOS = false
var sawOutputEOS = false
try {
while (!sawOutputEOS) {
if (!sawInputEOS) {
val inputBufIndex: Int = codec.dequeueInputBuffer(kTimeOutUs)
if (inputBufIndex >= 0) {
val dstBuf: ByteBuffer = codecInputBuffers[inputBufIndex]
val sampleSize: Int = extractor.readSampleData(dstBuf, 0)
if (sampleSize < 0) {
Log.i(TAG, "saw input EOS.")
sawInputEOS = true
codec.queueInputBuffer(inputBufIndex, 0, 0, 0, MediaCodec.BUFFER_FLAG_END_OF_STREAM)
} else {
codec.queueInputBuffer(inputBufIndex, 0, sampleSize, extractor.getSampleTime(), 0)
extractor.advance()
}
}
}
val outputBufferIndex: Int = codec.dequeueOutputBuffer(info, kTimeOutUs)
if (outputBufferIndex >= 0) {
// Simply ignore codec config buffers.
if (info.flags and MediaCodec.BUFFER_FLAG_CODEC_CONFIG !== 0) {
Log.i(TAG, "audio encoder: codec config buffer")
codec.releaseOutputBuffer(outputBufferIndex, false)
continue
}
if (info.size !== 0) {
val outBuf: ByteBuffer = codecOutputBuffers[outputBufferIndex]
outBuf.position(info.offset)
outBuf.limit(info.offset + info.size)
val data = ByteArray(info.size)
outBuf.get(data)
fosDecoder.write(data)
}
codec.releaseOutputBuffer(outputBufferIndex, false)
if (info.flags and MediaCodec.BUFFER_FLAG_END_OF_STREAM !== 0) {
Log.i(TAG, "saw output EOS.")
sawOutputEOS = true
}
} else if (outputBufferIndex == MediaCodec.INFO_OUTPUT_BUFFERS_CHANGED) {
codecOutputBuffers = codec.getOutputBuffers()
Log.i(TAG, "output buffers have changed.")
} else if (outputBufferIndex == MediaCodec.INFO_OUTPUT_FORMAT_CHANGED) {
val oformat: MediaFormat = codec.getOutputFormat()
Log.i(TAG, "output format has changed to $oformat")
}
}
} finally {
Log.i(TAG, "decodeAacToPcm finish")
codec.stop()
codec.release()
extractor.release()
fosDecoder.close()
}
}
好了,最后附上完整的代码:
package com.bnd.myaudioandvideo.utils
import android.media.MediaCodec
import android.media.MediaCodecInfo
import android.media.MediaExtractor
import android.media.MediaFormat
import android.util.Log
import java.io.File
import java.io.FileInputStream
import java.io.FileOutputStream
import java.io.IOException
object AacPcmCodec {
private const val TAG = "AacPcmCodec"
private const val AUDIO_MIME = "audio/mp4a-latm"
private const val AUDIO_BYTES_PER_SAMPLE = (44100 * 1 * 16 / 8).toLong()
/**
* AAC 格式解码成 PCM 数据
* @param aacFile
* @param pcmFile
* @throws IOException
*/
@Throws(IOException::class)
fun decodeAacToPcm(aacFile: File, pcmFile: File) {
val extractor = MediaExtractor()
extractor.setDataSource(aacFile.absolutePath)
var mediaFormat: MediaFormat? = null
var i = 0
val count = extractor.trackCount
while (i < count) {
val format = extractor.getTrackFormat(i)
val mime = format.getString(MediaFormat.KEY_MIME)
if (mime!!.startsWith("audio/")) {
extractor.selectTrack(i)
mediaFormat = format
break
}
i++
}
if (mediaFormat == null) {
Log.e(TAG, "Invalid file with audio track.")
extractor.release()
return
}
val mime = mediaFormat.getString(MediaFormat.KEY_MIME)
Log.i(TAG, "decodeAacToPcm: mimeType: $mime")
val codec = MediaCodec.createDecoderByType(mime!!)
codec.configure(mediaFormat, null, null, 0)
codec.start()
val inputBuffers = codec.inputBuffers
var outputBuffers = codec.outputBuffers
val outBufferInfo = MediaCodec.BufferInfo()
val timeoutUs: Long = 10000
var sawInputEOS = false
var sawOutputEOS = false
var outputBytes: ByteArray? = null
try {
FileOutputStream(pcmFile).use { fosAudio ->
while (!sawOutputEOS) {
if (!sawInputEOS) {
val inputBufIndex = codec.dequeueInputBuffer(timeoutUs)
if (inputBufIndex >= 0) {
val inputBuffer = inputBuffers[inputBufIndex]
val sampleSize = extractor.readSampleData(inputBuffer, 0)
if (sampleSize < 0) {
Log.i(TAG, "saw input EOS.")
sawInputEOS = true
codec.queueInputBuffer(inputBufIndex, 0, 0, 0, MediaCodec.BUFFER_FLAG_END_OF_STREAM)
} else {
codec.queueInputBuffer(inputBufIndex, 0, sampleSize, extractor.sampleTime, 0)
extractor.advance()
}
}
}
val outputBufIndex = codec.dequeueOutputBuffer(outBufferInfo, timeoutUs)
if (outputBufIndex >= 0) {
// Simply ignore codec config buffers.
if (outBufferInfo.flags and MediaCodec.BUFFER_FLAG_CODEC_CONFIG != 0) {
Log.i(TAG, "audio encoder: codec config buffer")
codec.releaseOutputBuffer(outputBufIndex, false)
continue
}
if (outBufferInfo.size > 0) {
val outputBuffer = outputBuffers[outputBufIndex]
outputBuffer.position(outBufferInfo.offset)
outputBuffer.limit(outBufferInfo.offset + outBufferInfo.size)
if (outputBytes == null || outputBytes!!.size < outBufferInfo.size) {
outputBytes = ByteArray(outBufferInfo.size)
}
outputBuffer[outputBytes]
fosAudio.write(outputBytes)
}
codec.releaseOutputBuffer(outputBufIndex, false)
if (outBufferInfo.flags and MediaCodec.BUFFER_FLAG_END_OF_STREAM != 0) {
Log.i(TAG, "saw output EOS.")
sawOutputEOS = true
}
} else if (outputBufIndex == MediaCodec.INFO_OUTPUT_BUFFERS_CHANGED) {
outputBuffers = codec.outputBuffers
Log.i(TAG, "output buffers have changed.")
} else if (outputBufIndex == MediaCodec.INFO_OUTPUT_FORMAT_CHANGED) {
Log.i(TAG, "output format has changed to " + codec.outputFormat)
}
}
}
} finally {
Log.i(TAG, "decodeAacToPcm finish " + pcmFile.absolutePath)
codec.stop()
codec.release()
extractor.release()
}
}
/**
* PCM 数据编码为 AAC 格式
* @param inPcmFile
* @param outAacFile
* @throws IOException
*/
@Throws(IOException::class)
fun encodePcmToAac(inPcmFile: File?, outAacFile: File) {
val audioEncoder = createAudioEncoder()
try {
FileInputStream(inPcmFile).use { fisAudio ->
FileOutputStream(outAacFile).use { fosAudio ->
audioEncoder.start()
var sawInputEOS = false
var sawOutputEOS = false
var presentationTimeUs: Long = 0
var inputBytes: ByteArray? = null
var sumReadInputSize = 0
var outputPresentationTimeUs: Long = 0
val timeoutUs = 10000
val outBufferInfo = MediaCodec.BufferInfo()
val inputBuffers = audioEncoder.inputBuffers
var outputBuffers = audioEncoder.outputBuffers
while (!sawOutputEOS) {
if (!sawInputEOS) {
val inputBufIndex = audioEncoder.dequeueInputBuffer(timeoutUs.toLong())
if (inputBufIndex >= 0) {
val inputBuffer = inputBuffers[inputBufIndex]
inputBuffer.clear()
val bufferSize = inputBuffer.remaining()
if (inputBytes == null) {
inputBytes = ByteArray(bufferSize)
}
val readInputSize = fisAudio.read(inputBytes)
if (readInputSize < 0) {
audioEncoder.queueInputBuffer(inputBufIndex, 0, 0, 0, MediaCodec.BUFFER_FLAG_END_OF_STREAM)
sawInputEOS = true
Log.i(TAG, "saw input EOS.")
} else {
inputBuffer.put(inputBytes, 0, readInputSize)
sumReadInputSize += readInputSize
audioEncoder.queueInputBuffer(inputBufIndex, 0, readInputSize, presentationTimeUs, 0)
presentationTimeUs = (1000000 * (sumReadInputSize.toFloat() / AUDIO_BYTES_PER_SAMPLE)).toLong()
}
}
}
val outputBufIndex = audioEncoder.dequeueOutputBuffer(outBufferInfo, timeoutUs.toLong())
if (outputBufIndex >= 0) {
// Simply ignore codec config buffers.
if (outBufferInfo.flags and MediaCodec.BUFFER_FLAG_CODEC_CONFIG != 0) {
Log.i(TAG, "audio encoder: codec config buffer")
audioEncoder.releaseOutputBuffer(outputBufIndex, false)
continue
}
if (outBufferInfo.size > 0) {
val outBuffer = outputBuffers[outputBufIndex]
outBuffer.position(outBufferInfo.offset)
outBuffer.limit(outBufferInfo.offset + outBufferInfo.size)
if (outputPresentationTimeUs <= outBufferInfo.presentationTimeUs) {
outputPresentationTimeUs = outBufferInfo.presentationTimeUs
val outBufSize = outBufferInfo.size
val outPacketSize = outBufSize + 7
outBuffer.position(outBufferInfo.offset)
outBuffer.limit(outBufferInfo.offset + outBufSize)
val outData = ByteArray(outPacketSize)
addADTStoPacket(outData, outPacketSize)
outBuffer[outData, 7, outBufSize]
fosAudio.write(outData, 0, outData.size)
}
}
audioEncoder.releaseOutputBuffer(outputBufIndex, false)
if (outBufferInfo.flags and MediaCodec.BUFFER_FLAG_END_OF_STREAM != 0) {
sawOutputEOS = true
Log.i(TAG, "saw output EOS.")
}
} else if (outputBufIndex == MediaCodec.INFO_OUTPUT_BUFFERS_CHANGED) {
outputBuffers = audioEncoder.outputBuffers
Log.i(TAG, "output buffers have changed.")
} else if (outputBufIndex == MediaCodec.INFO_OUTPUT_FORMAT_CHANGED) {
Log.i(TAG, "output format has changed to " + audioEncoder.outputFormat)
}
}
}
}
} finally {
Log.i(TAG, "encodePcmToAac: finish " + outAacFile.absolutePath)
audioEncoder.release()
}
}
@Throws(IOException::class)
private fun createAudioEncoder(): MediaCodec {
val codec = MediaCodec.createEncoderByType(AUDIO_MIME)
val format = MediaFormat()
format.setString(MediaFormat.KEY_MIME, AUDIO_MIME)
format.setInteger(MediaFormat.KEY_BIT_RATE, 64000)
format.setInteger(MediaFormat.KEY_CHANNEL_COUNT, 1)
format.setInteger(MediaFormat.KEY_SAMPLE_RATE, 44100)
format.setInteger(MediaFormat.KEY_AAC_PROFILE, MediaCodecInfo.CodecProfileLevel.AACObjectLC)
codec.configure(format, null, null, MediaCodec.CONFIGURE_FLAG_ENCODE)
return codec
}
/**
* Add ADTS header at the beginning of each and every AAC packet.
* This is needed as MediaCodec encoder generates a packet of raw
* Sample rate and channel count are variables.
*/
private fun addADTStoPacket(packet: ByteArray, packetLen: Int) {
val profile = 2 //AAC LC
//39=MediaCodecInfo.CodecProfileLevel.AACObjectELD;
val freqIdx = 4 //44.1KHz
val chanCfg = 1 //CPE
// fill in ADTS data
packet[0] = 0xFF.toByte()
packet[1] = 0xF9.toByte()
packet[2] = ((profile - 1 shl 6) + (freqIdx shl 2) + (chanCfg shr 2)).toByte()
packet[3] = ((chanCfg and 3 shl 6) + (packetLen shr 11)).toByte()
packet[4] = (packetLen and 0x7FF shr 3).toByte()
packet[5] = ((packetLen and 7 shl 5) + 0x1F).toByte()
packet[6] = 0xFC.toByte()
}
}
3:总结
音频开发的知识点还是很多的,学习音频开发需要大家有足够的耐心,一步一个脚印的积累,只有这样才能把音频开发学好。下面推荐几个比较好的博主,希望对大家有所帮助。
- csdn博主:《雷神雷霄骅》
- 51CTO博客:《Jhuster的专栏》