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文件格式.png

可以看到,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

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 157,298评论 4 360
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 66,701评论 1 290
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 107,078评论 0 237
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,687评论 0 202
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,018评论 3 286
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,410评论 1 211
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,729评论 2 310
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,412评论 0 194
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,124评论 1 239
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,379评论 2 242
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 31,903评论 1 257
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,268评论 2 251
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 32,894评论 3 233
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,014评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,770评论 0 192
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,435评论 2 269
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,312评论 2 260

推荐阅读更多精彩内容