Android音视频开发:音频非压缩编码和压缩编码

音视频在开发中,最重要也是最复杂的就是编解码的过程,在上一篇的《Android音视频开发:踩一踩“门槛”》中,我们说音频的编码根据大小划分有两种:压缩编码和非压缩编码,那到底是怎么实现的这两中编码的呢?这一次就详细了解Android中如何使用这两种方式进行音频编码

前景提要
这里先回顾一下音频的压缩编码和非压缩编码:

非压缩编码:音频裸数据,也即是我们所说的PCM
压缩编码:对数据进行压缩,压缩不能被人耳感知到的冗余信号
因为非压缩编码实在是太大了,所以我们生活中所接触的音频编码格式都是压缩编码,而且是有损压缩,比如 MP3或AAC。
那如何操作PCM数据呢?Android SDK中提供了一套对PCM操作的API:AudioRecord 和 AudioTrack;

由于AudioRecord(录音) 和 AudioTrack(播放)操作过于底层而且过于复杂,所以Android SDK 还提供了一套与之对应更加高级的API:MediaRecorder(录音)和MediaPlayer(播放),用于音视频的操作,当然其更加简单方便。我们这里只介绍前者,通过它来实现对PCM数据的操作。

对于压缩编码,我们则通过MediaCodec和Lame来分别实现AAC音频和Mp3音频压缩编码。话不多说,请往下看!

AudioRecord
由于AudioRecord更加底层,能够更好的并且直接的管理通过音频录制硬件设备录制后的PCM数据,所以对数据处理更加灵活,但是同时也需要我们自己处理编码的过程。

AudioRecord的使用流程大致如下:

根据音频参数创建AudioRecord
调用startRecording开始录制
开启录制线程,通过AudioRecord将录制的音频数据从缓存中读取并写入文件
释放资源
在使用AudioRecord前需要先注意添加RECORD_AUDIO录音权限。

创建AudioRecord
我们先看看AudioRecord构造方法

public AudioRecord (int audioSource,
int sampleRateInHz,
int channelConfig,
int audioFormat,
int bufferSizeInBytes)
复制代码
audioSource,从字面意思可知音频来源,由MediaRecorder.AudioSource提供,主要有以下内容

· CAMCORDER 与照相机方向相同的麦克风音频源

· DEFAULT 默认

· MIC 麦克风音频源

· VOICE_CALL 语音通话

这里采用MIC麦克风音频源

sampleRateInHz,采样率,即录制的音频每秒钟会有多少次采样,可选用的采样频率列表为:8000、16000、22050、24000、32000、44100、48000等,一般采用人能听到最大音频的2倍,也就是44100Hz。

channelConfig,声道数的配置,可选值以常量的形式配置在类AudioFormat中,常用的是CHANNEL_IN_MONO(单声道)、CHANNEL_IN_STEREO(双声道)

audioFormat,采样格式,可选值以常量的形式定义在类AudioFormat中,分别为ENCODING_PCM_16BIT(16bit)、ENCODING_PCM_8BIT(8bit),一般采用16bit。

bufferSizeInBytes,其配置的是AudioRecord内部的音频缓冲区的大小,可能会因为生产厂家的不同而有所不同,为了方便AudioRecord提供了一个获取该值最小缓冲区大小的方法getMinBufferSize。

public static int getMinBufferSize (int sampleRateInHz,
int channelConfig,
int audioFormat)
复制代码
在开发过程中需使用getMinBufferSize此方法计算出最小缓存大小。

切换录制状态
首先通过调用getState判断AudioRecord是否初始化成功,然后通过startRecording切换成录制状态

if (null!=audioRecord && audioRecord?.state!=AudioRecord.STATE_UNINITIALIZED){
audioRecord?.startRecording()
}
复制代码
开启录制线程
thread = Thread(Runnable {
writeData2File()
})
thread?.start()
复制代码
开启录音线程将录音数据通过AudioRecord写入文件

private fun writeData2File() {
var ret = 0
val byteArray = ByteArray(bufferSizeInBytes)
val file = File(externalCacheDir?.absolutePath + File.separator + filename)

if (file.exists()) {
    file.delete()
} else {
    file.createNewFile()
}
val fos = FileOutputStream(file)
while (status == Status.STARTING) {
    ret = audioRecord?.read(byteArray, 0, bufferSizeInBytes)!!
    if (ret!=AudioRecord.ERROR_BAD_VALUE || ret!=AudioRecord.ERROR_INVALID_OPERATION|| ret!=AudioRecord.ERROR_DEAD_OBJECT){
        fos.write(byteArray)
    }
}
fos.close()

}
复制代码
释放资源
首先停止录制

if (null!=audioRecord && audioRecord?.state!=AudioRecord.STATE_UNINITIALIZED){
audioRecord?.stop()
}
复制代码
然后停止线程

if (thread!=null){
thread?.join()
thread =null
}
复制代码
最后释放AudioRecord

if (audioRecord != null) {
audioRecord?.release()
audioRecord = null
}
复制代码
通过以上一个流程之后,就可以得到一个非压缩编码的PCM数据了。

但是这个数据在音乐播放器上一般是播放不了的,那么怎么验证我是否录制成功呢?当然是使用我们的AudioTrack进行播放看看是不是刚刚我们录制的声音了。

【完整代码-AudioRecord】

AudioTrack
由于AudioTrack是由Android SDK提供比较底层的播放API,也只能操作PCM裸数据,通过直接渲染PCM数据进行播放。当然如果想要使用AudioTrack进行播放,那就需要自行先将压缩编码格式文件解码。

AudioTrack的使用流程大致如下:

根据音频参数创建AudioTrack
调用play开始播放
开启播放线程,循环想AudioTrack缓存区写入音频数据
释放资源
创建AudioTrack
我们来看看AudioTrack的构造方法

public AudioTrack (int streamType,
int sampleRateInHz,
int channelConfig,
int audioFormat,
int bufferSizeInBytes,
int mode,
int sessionId)
复制代码
streamType,Android手机上提供音频管理策略,按下音量键我们会发现由媒体声音管理,闹铃声音管理,通话声音管理等等,当系统有多个进程需要播放音频的时候,管理策略会决定最终的呈现效果,该参数的可选值将以常量的形式定义在类AudioManager中,主要包括以下内容:

· STREAM_VOCIE_CALL:电话声音

· STREAM_SYSTEM:系统声音

· STREAM_RING:铃声

· STREAM_MUSCI:音乐声

· STREAM_ALARM:警告声

· STREAM_NOTIFICATION:通知声

因为这里是播放音频,所以我们选择STREAM_MUSCI。

sampleRateInHz,采样率,即播放的音频每秒钟会有多少次采样,可选用的采样频率列表为:8000、16000、22050、24000、32000、44100、48000等,一般采用人能听到最大音频的2倍,也就是44100Hz。

channelConfig,声道数的配置,可选值以常量的形式配置在类AudioFormat中,常用的是CHANNEL_IN_MONO(单声道)、CHANNEL_IN_STEREO(立体双声道)

audioFormat,采样格式,可选值以常量的形式定义在类AudioFormat中,分别为ENCODING_PCM_16BIT(16bit)、ENCODING_PCM_8BIT(8bit),一般采用16bit。

bufferSizeInBytes,其配置的是AudioTrack内部的音频缓冲区的大小,可能会因为生产厂家的不同而有所不同,为了方便AudioTrack提供了一个获取该值最小缓冲区大小的方法getMinBufferSize。

mode,播放模式,AudioTrack提供了两种播放模式,可选的值以常量的形式定义在类AudioTrack中,一个是MODE_STATIC,需要一次性将所有的数据都写入播放缓冲区中,简单高效,通常用于播放铃声、系统提醒的音频片段;另一个是MODE_STREAM,需要按照一定的时间间隔不间断地写入音频数据,理论上它可以应用于任何音频播放的场景。

sessionId,AudioTrack都需要关联一个会话Id,在创建AudioTrack时可直接使用AudioManager.AUDIO_SESSION_ID_GENERATE,或者在构造之前通过AudioManager.generateAudioSessionId获取。

上面这种构造方法已经被弃用了,现在基本使用如下构造(最小skd 版本需要>=21),参数内容与上基本一致:

public AudioTrack (AudioAttributes attributes,
AudioFormat format,
int bufferSizeInBytes,
int mode,
int sessionId)
复制代码
通过AudioAttributes.Builder设置参数streamType

var audioAttributes = AudioAttributes.Builder()
.setLegacyStreamType(AudioManager.STREAM_MUSIC)
.build()
复制代码
通过AudioFormat.Builder设置channelConfig,sampleRateInHz,audioFormat参数

var mAudioFormat = AudioFormat.Builder()
.setChannelMask(channel)
.setEncoding(audioFormat)
.setSampleRate(sampleRate)
.build()
复制代码
切换播放状态
首先通过调用getState判断AudioRecord是否初始化成功,然后通过play切换成录播放状态

if (null!=audioTrack && audioTrack?.state != AudioTrack.STATE_UNINITIALIZED){
audioTrack?.play()
}
复制代码
开启播放线程
开启播放线程

thread= Thread(Runnable {
readDataFromFile()
})
thread?.start()
复制代码
将数据不断的送入缓存区并通过AudioTrack播放

private fun readDataFromFile() {
val byteArray = ByteArray(bufferSizeInBytes)

val file = File(externalCacheDir?.absolutePath + File.separator + filename)
if (!file.exists()) {
    Toast.makeText(this, "请先进行录制PCM音频", Toast.LENGTH_SHORT).show()
    return
}
val fis = FileInputStream(file)
var read: Int
status = Status.STARTING

while ({ read = fis.read(byteArray);read }() > 0) {
    var ret = audioTrack?.write(byteArray, 0, bufferSizeInBytes)!!
    if (ret == AudioTrack.ERROR_BAD_VALUE || ret == AudioTrack.ERROR_INVALID_OPERATION || ret == AudioManager.ERROR_DEAD_OBJECT) {
        break
    }
}
fis.close()

}
复制代码
释放资源
首先停止播放

if (audioTrack != null && audioTrack?.state != AudioTrack.STATE_UNINITIALIZED) {
audioTrack?.stop()
}
复制代码
然后停止线程

if (thread!=null){
thread?.join()
thread =null
}
复制代码
最后释放AudioTrack

if (audioTrack != null) {
audioTrack?.release()
audioTrack = null
}
复制代码
经过这样几个步骤,我们就可以听到刚刚我们录制的PCM数据声音啦!这就是使用Android提供的AudioRecord和AudioTrack对PCM数据进行操作。

但是仅仅这样是不够的,因为我们生活中肯定不是使用PCM进行音乐播放,那么怎么才能让音频在主流播放器上播放呢?这就需要我们进行压缩编码了,比如mp3或aac压缩编码格式。

【完整代码-AudioTrack】

MediaCodec编码AAC
AAC压缩编码是一种高压缩比的音频压缩算法,AAC压缩比通常为18:1;采样率范围通常是8KHz~96KHz,这个范围比MP3更广一些(MP3的范围一般是:16KHz~48KHz),所以在16bit的采样格式上比MP3更精细。

方便我们处理AAC编码,Android SDK中提供了MediaCodecAPI,可以将PCM数据编码成AAC数据。大概需要以下几个步骤:

创建MediaCodec
为MediaCodec配置音频参数
启动线程,循环往缓冲区送入数据
通过MediaCodec将缓冲区的数据进行编码并写入文件
释放资源
创建MediaCodec
通过MediaCodec.createEncoderByType创建编码MediaCodec

mediaCodec = MediaCodec.createEncoderByType(MediaFormat.MIMETYPE_AUDIO_AAC)
复制代码
配置音频参数
// 配置采样率和声道数
mediaFormat = MediaFormat.createAudioFormat(MINE_TYPE,sampleRate,channel)
// 配置比特率
mediaFormat?.setInteger(MediaFormat.KEY_BIT_RATE,bitRate)
// 配置PROFILE,其中属AAC-LC兼容性最好
mediaFormat?.setInteger(MediaFormat.KEY_AAC_PROFILE, MediaCodecInfo.CodecProfileLevel.AACObjectLC)
// 最大输入大小
mediaFormat?.setInteger(MediaFormat.KEY_MAX_INPUT_SIZE, 10 * 1024)

mediaCodec!!.configure(mediaFormat,null,null,MediaCodec.CONFIGURE_FLAG_ENCODE)
mediaCodec?.start()

inputBuffers = mediaCodec?.inputBuffers
outputBuffers = mediaCodec?.outputBuffers
复制代码
启动线程
启动线程,循环读取PCM数据送入缓冲区

thread = Thread(Runnable {
val fis = FileInputStream(pcmFile)
fos = FileOutputStream(aacFile)
var read: Int
while ({ read = fis.read(byteArray);read }() > 0) {
encode(byteArray)
}
})
thread?.start()
复制代码
AAC编码
将送入的PCM数据通过MediaCodec进行编码,大致流程如下:

通过可用缓存去索引,获取可用输入缓冲区
将pcm数据放入输入缓冲区并提交
根据输出缓冲区索引,获取输出缓冲区
创建输出数据data,并添加ADTS头部信息(有7byte)
将outputBuffer编码后数据写入data(data有7byte偏移)
将编码数据data写入文件
重复以上过程
private fun encode(byteArray: ByteArray){
mediaCodec?.run {
//返回要用有效数据填充的输入缓冲区的索引, -1 无限期地等待输入缓冲区的可用性
val inputIndex = dequeueInputBuffer(-1)
if (inputIndex > 0){
// 根据索引获取可用输入缓存区
val inputBuffer = this@AACEncoder.inputBuffers!![inputIndex]
// 清空缓冲区
inputBuffer.clear()
// 将pcm数据放入缓冲区
inputBuffer.put(byteArray)
// 提交放入数据缓冲区索引以及大小
queueInputBuffer(inputIndex,0,byteArray.size,System.nanoTime(),0)
}
// 指定编码器缓冲区中有效数据范围
val bufferInfo = MediaCodec.BufferInfo()
// 获取输出缓冲区索引
var outputIndex = dequeueOutputBuffer(bufferInfo,0)

    while (outputIndex>0){
        // 根据索引获取可用输出缓存区
        val outputBuffer =this@AACEncoder.outputBuffers!![outputIndex]
        // 测量输出缓冲区大小
        val bufferSize = bufferInfo.size
        // 输出缓冲区实际大小,ADTS头部长度为7
        val bufferOutSize = bufferSize+7
        
        // 指定输出缓存区偏移位置以及限制大小
        outputBuffer.position(bufferInfo.offset)
        outputBuffer.limit(bufferInfo.offset+bufferSize)
        // 创建输出空数据
        val data = ByteArray(bufferOutSize)
        // 向空数据先增加ADTS头部
        addADTStoPacket(data, bufferOutSize)
        // 将编码输出数据写入已加入ADTS头部的数据中
        outputBuffer.get(data,7,bufferInfo.size)
        // 重新指定输出缓存区偏移
        outputBuffer.position(bufferInfo.offset)
        // 将获取的数据写入文件
        fos?.write(data)
        // 释放输出缓冲区
        releaseOutputBuffer(outputIndex,false)
        // 重新获取输出缓冲区索引
        outputIndex=dequeueOutputBuffer(bufferInfo,0)
    }
}

}
复制代码
释放资源
编码完成后,一定要释放所有资源,首先关闭输入输出流

fos?.close()
fis.close()
复制代码
停止编码

if (mediaCodec!=null){
mediaCodec?.stop()
}
复制代码
然后就是关闭线程

if (thread!=null){
thread?.join()
thread =null
}
复制代码
最后释放MediaCodec

if (mediaCodec!=null){
mediaCodec?.release()
mediaCodec = null

mediaFormat = null
inputBuffers = null
outputBuffers = null

}
复制代码
通过以上一个流程,我们就可以得到一个AAC压缩编码的音频文件,可以听一听是不是自己刚刚录制的。我听了一下我自己唱的一首歌,觉得我的还是可以的嘛,也不是那么五音不全~~

  • 2
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

安卓兼职framework应用工程师

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值