美文网首页Android音视频音视频积累知识图谱
Android音视频【九】音频硬编解码pcm&aac&wav

Android音视频【九】音频硬编解码pcm&aac&wav

作者: 后厂村追寻 | 来源:发表于2021-02-14 16:56 被阅读0次

    人间观察

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

    这篇看下音频的硬编解码(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

    相关文章

      网友评论

        本文标题:Android音视频【九】音频硬编解码pcm&aac&wav

        本文链接:https://www.haomeiwen.com/subject/dcwyxltx.html