android音视频音频硬编解码pcm&aac&wav

Posted 顾修忠

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了android音视频音频硬编解码pcm&aac&wav相关的知识,希望对你有一定的参考价值。

人间观察

时间的流逝总是悄无声息的

这篇看下音频的硬编解码(MediaCodec),主要内容包含

  • AudioRecord采集pcm硬编码为aac
  • mp3硬解码为pcm
  • pcm转为wav格式

为什么介绍这些呢? 因为在直播中音频基本上都是aac格式的,在短视频中比如:添加背景音进行混音,替换背景音乐,视频文件提取音频,剪切音频,插入音频等等都会涉及。所以比较重要,当然也有软编码,后续介绍。

因工作中用不到kotlin,示例代码我采用kotlin进行,顺便练习下

AudioRecord采集pcm硬编码为aac

首先是音频的采集,在android中是用AudioRecord,创建示例为:

audioRecord = AudioRecord(
        MediaRecorder.Audiosource.MIC, SAMPLE_RATE, AudioFormat.CHANNEL_IN_MONO,
        AudioFormat.ENCODING_PCM_16BIT, minBufferSize
    )

函数定义

public AudioRecord(int audioSource, int sampleRateInHz, int channelConfig, int audioFormat,
            int bufferSizeInBytes)
  • audioSource 音频来源,一般是Microphone
  • sampleRateInHz 采样频率,比如16000,44100,具体可以参考上篇
  • channelConfig 声道数,单声道AudioFormat.CHANNEL_IN_MONO(在所有设置上支持),双声道AudioFormat.CHANNEL_IN_STEREO,一般是单声到
  • audioFormat 一个采样点用几位描述,取值有AudioFormat.ENCODING_PCM_16BIT,ENCODING_PCM_8BIT。视情况而定,一般是AudioFormat.ENCODING_PCM_16BIT 2个字节。
  • bufferSizeInBytes 缓存区,需要>=AudioRecord.getMinBufferSize()的大小,否则AudioRecord创建失败。

如何编码为aac呢? 和视频一样用MediaCodec,部分代码如下分为初始化,配置,启动等几个阶段。

//AAC
val format = MediaFormat.createAudioFormat(
    MediaFormat.MIMETYPE_AUDIO_AAC,
    SAMPLE_RATE,
    CHANNEL_COUNT
)
//录音质量
format.setInteger(MediaFormat.KEY_AAC_PROFILE, MediaCodecInfo.CodecProfileLevel.AACObjectLC)
//码率,1s的bit
format.setInteger(MediaFormat.KEY_BIT_RATE, 64_000)

mediaCodec = MediaCodec.createEncoderByType(MediaFormat.MIMETYPE_AUDIO_AAC)
mediaCodec?.configure(format, null, null, MediaCodec.CONFIGURE_FLAG_ENCODE)
mediaCodec?.start()

开始编码

override fun run() 
    super.run()
    audioRecord?.startRecording()
    val bufferInfo = MediaCodec.BufferInfo()
    while (isEncoding) 
        // 1.获取音频
        val buffer = ByteArray(minBufferSize)
        val len: Int? = audioRecord?.read(buffer, 0, buffer.size)
        if (len!! <= 0) 
            continue
        
        // 2.编码
        val index = mediaCodec?.dequeueInputBuffer(10_1000)
        if (index!! >= 0) 
            val inputBuffer = mediaCodec?.getInputBuffer(index)
            inputBuffer!!.clear()
            inputBuffer.put(buffer, 0, len)
            mediaCodec?.queueInputBuffer(index, 0, len, System.nanoTime() / 1000, 0)
        

        // 3.获取编码后的数据进行下一步的处理(比如:推流等)
        var outIndex = mediaCodec?.dequeueOutputBuffer(bufferInfo, 10_000)
        while (outIndex!! >= 0 && isEncoding) 
            mediaCodec?.getOutputBuffer(outIndex)
            val outData = ByteArray(bufferInfo.size)

            // outData 为编码后的aac数据,temp to file
            fileOutputStream.write(outData)

            mediaCodec?.releaseOutputBuffer(outIndex, false)
            outIndex = mediaCodec?.dequeueOutputBuffer(bufferInfo, 0)
        
    
    fileOutputStream.flush()
    fileOutputStream.close()
    Log.d(TAG, "AudioEnCodeThread done")

关于MediaCodec的介绍可以参考Android音视频【三】硬解码播放H264

mp3硬解码为pcm

mp3的解码这里我们也用Android自带的硬解码MediaCodec,也有其它的比如lame,jlayer等开源库。
本示例是解码一个mp3文件,解码流(网络流等)也差不多。
如果要解码mp3文件或者从视频文件中提取音频,要借助MediaExtractor类,选择对应的音频轨道selectTrack,然后不断的提取对应的音频数据readSampleData,把提取的数据交给mediaCodec解码得到pcm数据。

选择对应的音频轨道

mediaExtractor.setDataSource(srcPath)
var index = -1
val count = mediaExtractor.trackCount
for (i in 0 until count) 
    val format = mediaExtractor.getTrackFormat(i)
    if (format.getString(MediaFormat.KEY_MIME)!!.startsWith("audio/")) 
        index = i
    

mediaExtractor.selectTrack(index)

获取完音频轨道的id后再得到音频的配置信息MediaFormat,MediaFormat里有采样率,声道数等,然后进行初始化音频解码器如下:

val format = mediaExtractor.getTrackFormat(index)
val mediaCodec = MediaCodec.createDecoderByType(format.getString(MediaFormat.KEY_MIME)!!)
mediaCodec.configure(format, null, null, 0)
mediaCodec.start()

读取数据并塞给mediaCodec进行解码

val info = MediaCodec.BufferInfo()
while (true) 
    val inputIndex = mediaCodec.dequeueInputBuffer(10 * 1000);
    if (inputIndex >= 0) 
        val sampleTimeUs = mediaExtractor.getSampleTime();
        if (sampleTimeUs == -1L) 
            Log.d(TAG, "break")
            break
        
        info.presentationTimeUs = sampleTimeUs
        info.flags = mediaExtractor.sampleFlags
        info.size = mediaExtractor.readSampleData(buffer, 0)

        val data = ByteArray(buffer.remaining())
        buffer.get(data)

        val inputBuffer = mediaCodec.getInputBuffer(inputIndex)
        inputBuffer!!.clear()
        inputBuffer.put(data)
        mediaCodec.queueInputBuffer(
            inputIndex,
            0,
            info.size,
            info.presentationTimeUs,
            info.flags
        )
        mediaExtractor.advance()
    

    var outputIndex = mediaCodec.dequeueOutputBuffer(info, 10_000)
    while (outputIndex >= 0) 
        val outByteBuffer = mediaCodec.getOutputBuffer(outputIndex)

        // to file
        writePcmChannel.write(outByteBuffer)

        mediaCodec.releaseOutputBuffer(outputIndex, false)
        outputIndex = mediaCodec.dequeueOutputBuffer(info, 0)
    


在进行解码的时候不要丢弃了时间戳和flags。时间戳是为了音视频的同步,虽然在本例中没有用到,但最好还是带上。

pcm转为wav格式

WAV是由微软开发的一种音频格式,WAV文件是在PCM数据的基础上添加一组头信息(大小44个字节),用于描述这个WAV文件的采样率,声道数,采样位数,音频数据大小等信息。这样WAV就可以被一般音频播放器(比如Android的mediaplayer)正确读取并播放,而PCM文件因为只有编码的音频数据,没有其他描述信息,所以无法被一般的音频播放器识别播放。如果想要播放pcm可以用专业的可以播放pcm文件的软件,Android中用Audiotrack进行播放。

WAV文件格式如下,图片来源于网络(https://www.jianshu.com/p/86edb2422b21

可以看到,WAV文件头信息由大小44个字节的数据组成:

private void writeWaveFileHeader(FileOutputStream out, long totalAudioLen,
                                 long totalDataLen, long longSampleRate, int channels, long byteRate)
        throws IOException 
    byte[] header = new byte[44];
    header[0] = 'R'; // RIFF/WAVE header
    header[1] = 'I';
    header[2] = 'F';
    header[3] = 'F';
    // 数据大小
    header[4] = (byte) (totalDataLen & 0xff);
    header[5] = (byte) ((totalDataLen >> 8) & 0xff);
    header[6] = (byte) ((totalDataLen >> 16) & 0xff);
    header[7] = (byte) ((totalDataLen >> 24) & 0xff);
    header[8] = 'W';  //WAVE
    header[9] = 'A';
    header[10] = 'V';
    header[11] = 'E';
    header[12] = 'f'; // 'fmt ' chunk
    header[13] = 'm';
    header[14] = 't';
    header[15] = ' '; //过渡字节
    header[16] = 16;  // 4 bytes
    header[17] = 0;
    header[18] = 0;
    header[19] = 0;
    // 2字节数据,内容为一个短整数,表示格式种类(值为1时,表示数据为线性PCM编码)
    header[20] = 1;   // format = 1
    header[21] = 0;
    header[22] = (byte) channels;  //通道数(单声道为1,双声道为2)
    header[23] = 0;
    //采样率,每个通道的播放速度,用4字节表示 
    header[24] = (byte) (longSampleRate & 0xff);
    header[25] = (byte) ((longSampleRate >> 8) & 0xff);
    header[26] = (byte) ((longSampleRate >> 16) & 0xff);
    header[27] = (byte) ((longSampleRate >> 24) & 0xff);
    //音频数据传送速率,采样率*通道数*采样深度/8
    header[28] = (byte) (byteRate & 0xff);
    header[29] = (byte) ((byteRate >> 8) & 0xff);
    header[30] = (byte) ((byteRate >> 16) & 0xff);
    header[31] = (byte) ((byteRate >> 24) & 0xff);
    // 确定系统一次要处理多少个这样字节的数据,确定缓冲区,通道数*采样位数 用2个字节表示
    header[32] = (byte) (channels * 16 / 8); // block align
    header[33] = 0;
    header[34] = 16;  // bits per sample 每个样本的数据位数
    header[35] = 0;
    header[36] = 'd'; //data
    header[37] = 'a';
    header[38] = 't';
    header[39] = 'a';
    // pcm数据的大小
    header[40] = (byte) (totalAudioLen & 0xff);
    header[41] = (byte) ((totalAudioLen >> 8) & 0xff);
    header[42] = (byte) ((totalAudioLen >> 16) & 0xff);
    header[43] = (byte) ((totalAudioLen >> 24) & 0xff);
    out.write(header, 0, 44);
  

只要按照wav格式的拼接即可。

总结

本文介绍了AudioRecord采集pcm通过MediaCodec硬编码为aac数据。
如何把音频mp3文件解码为PCM数据,以及如何把PCM编码为WAV,有了这些基础后,然后进行音频文件的裁剪,插入,合成,混音等编辑操作和对应的处理原理就比较容易处理了。任何音频的操作都是对pcm数据进行处理。

源码

https://github.com/ta893115871/AudioAACAndMP3

以上是关于android音视频音频硬编解码pcm&aac&wav的主要内容,如果未能解决你的问题,请参考以下文章

Android音视频H265硬编解码&视频通话

Android音视频H265硬编解码&视频通话

基于AudioTrack、AudioRecord获取分贝值、录制时长、PCM解码与编码

Android硬编码——音频编码视频编码及音视频混合

Qt-FFmpeg开发-音频解码为PCM文件

android ndk之opencv+MediaCodec硬编解码来处理视频动态时间水印