一、音频概览
声音是振动产生的能量(声波),通过介质传播并能被人或动物听觉器官所感知的波动现象。例如人类说话时通过声带振动产生声波,通过空气传播后到达其他人耳朵。
要采集声音,就需要通过麦克风对声音进行录制。录音就是将声音转为模拟信号或机械记录的过程。而数字录音是进一步将模拟信号经由ADC(模拟数字转换器)将 类比取样成数字记录到存储设备。
在Android
开发音频,就是利用AudioRecoder
、MediaRecoder
录音,通过AudioTrack
、MediaPlayer
播放声音,而这一过程会涉及到很多知识点,例如录制声音过程的采样率、位深、编解码MediaCodec
、FFmpeg
等等。
总结来说:
-
音频录制与播放(
Android
相关类的使用,Framework
调用原理) -
音频处理(去噪、静音检测、回声消除、音效处理、功放/增强、混音分离)
-
音频的编解码和格式转换(
MediaCodec
、FFmpeg
) -
音频传输协议(
SIP\A2DP
、AVRCP
等)
1、PCM
PCM全名叫脉冲编码调制,是一种将模拟信号数字化的方法。PCM将信号的强度依照同样的间距分成数段,然后使用独特的数字记号(二进制)量化。这一过程通常由ADC来实现。作为Android程序员,更多是调用AudioTrack录制音频后,保存为PCM编码格式,也可以再进一步编码,存储成其它格式。
例如下图(正常应该是正弦波):
用绿色圆圈来表示连续的模拟信号,而红色圆点则表示数字信号。PCM过程就是在模拟信号上进行采样和收集,形成断断续续的采样点,也就是数字信号。在X轴方向上,可以理解为对音频的量化是时间,体现在采样率上。在Y轴方向,可以理解为量化是数量,体现为位深。
2、采样率
采样率表示在1秒内对声音的模拟信号采样的次数,假设上面是声音在1秒内的模拟信号,那么采样就只有2次,以频率作为计量单位,所以这里采样率2Hz。由此 ,可以看出采样率越高,采样得到的数字信号数据就越多,声音在播放过程就会越真实越接近原声,但这也占用很大的数据存储。所以需要根据人耳能识别频率的范围和具体的使用场景,将采样率控制在合适的范围内。常用的音频采样率为44.1k Hz
和48k Hz
。
人耳能识别的频率在20~20kHz
,根据奈奎斯特·香农采样定理,采样后的音频要还原成人耳可以识别声音,采样率需要为人耳可以识别频率的2倍,而人耳上限是20KHz
,2倍就是40kHz
。所以常用的音频采样率为44.1k Hz
和48k Hz
。其他常用采样频率:
-
8k Hz
- 电话所用采样率, 对于人的说话已经足够 -
11.025k Hz
- AM调幅广播所用采样率 -
22.050k Hz
和24.000k Hz
- FM调频广播所用采样率 -
32k
Hz - miniDV 数码视频 camcorder、DAT (LP mode)所用采样率 -
44.1k Hz
- 音频 CD, 也常用于 MPEG-1 音频(VCD, SVCD, MP3)所用采样率 -
47.25k Hz
- 商用 PCM 录音机所用采样率 -
48k Hz
- miniDV、数字电视、DVD、DAT、电影和专业音频所用的数字声音所用采样率 -
50k Hz
- 商用数字录音机所用采样率 -
96k
或者192k Hz
- DVD-Audio、一些 LPCM DVD 音轨、BD-ROM(蓝光盘)音轨、和 HD-DVD (高清晰度 DVD)音轨所用所用采样率
3、位深
采样率可以理解为在时间范畴上对模拟信号的量化,如上面X轴上采样数,而位深表示数量上对模拟信号的量化,如上面Y轴的值。例如Android上常用的位深为8位,表示某个采样点上的值,可以用8位进行表示,共有256个值的可能。而声音数据的保存也就是保存每个声音的位数。
4、声道数
比较常见的概念,例如单声道(mono),立体声(stereo),5.1声道,7.1声道等等。多声道以为在采样时候需要同时采样每个声道,这样就会增加数据量。
5、数据大小
通过对采样率、位深、声道数的理解,可以得到下面公式:
- 数据大小=采样率x声道数x(位深÷8)x时长,单位为Byte。
- 采样数 = 采样率 × 音频时长(s)× 声道数
- 音频时长= 采样数 ÷ 采样率 ÷ 声道数,单位为s。
6、编码格式
在存储和传输过程,通常不使用PCM格式,虽然无损以及可以直接被声卡和DAC解析,但存储大小过大,会造成本地和网络资源的浪费。所以通常会进一步编码,进行压缩和添加额外的信息,这就衍生出其他编码格式,如MP3、AAC、FLAC等。
经过编码后的音频数据,一般包含三种信息:编码格式、容器格式、元数据。
编码格式
编码格式分为有损编码和无损编码格式,PCM格式是无损的,而MP3则是有损的。编码后的音频数据不再表示波形采样值,不能直接通过硬件播放,需要通过编解码器解码后才能正常播放。
容器格式
编码后的音频需要和元数据需要一起进行容器封装,才形成最终的音频文件。容器格式一般和文件名称后缀保持一致。例如.mp3、.wav。编码格式与容器格式是相互独立的,也就说,MP3编码的音频是可以用WAV进行封装。
元数据
在容器中用于描述音频数据,辅助编解码,如采样率、长度等。
二、AudioRecord与AudioTrack使用
1. AudioRecord实现录音
在创建AudioRecord
实例前,我们需要通过getMinBufferSize
函数来获取当前AudioRecord
可以使用的最小缓冲区大小。
kotlin
getMinBufferSize(int sampleRateInHz, int channelConfig, int audioFormat)
其中sampleRateInHz
为采样率,channelConfig
为声道,audioFormat
位深。
-
sampleRateInHz
采样率,因为我们这里是录音,所以采用44.1kHz
。可以根据需要选择其他采样率。 -
channelConfig
声道数,这里选择单声道AudioFormat.CHANNEL_IN_MONO
,而立体声为CHANNEL_IN_STEREO
。其他声道可以根据参数结合配置。 -
audioFormat
编码位深,这里选择默认PCM16位编码,AudioFormat.ENCODING_PCM_16BIT
。具体根据业务选择不同的编码位深。
接下来是创建AudioRecord
实例:
java
//采样率 44.1kHz
private val sampleRate = 44100
//声道 单声道
private val channel = AudioFormat.CHANNEL_IN_MONO
//位深
private val pcmBit = AudioFormat.ENCODING_PCM_16BIT
//获取最小缓冲区大小
bufferSize = AudioRecord.getMinBufferSize(
sampleRate, channel, pcmBit
)
//AudioRecord实例
audioRecord = AudioRecord(
MediaRecorder.AudioSource.MIC, //音频来源,麦克风
sampleRate,
channel,
pcmBit,
bufferSize
)
这样就可以通过AudioRecord实例进行录音,下面代码将录音内容保存到文件中。
kotlin
private fun startRecording() {
if (isRecording) return
isRecording = true
//开始录音
audioRecord.startRecording()
GlobalScope.launch(Dispatchers.IO) {
val buffer = ByteArray(bufferSize)
val file = application.externalCacheDir
val saveFile = File(file, "audio_${System.currentTimeMillis()}.pcm")
val ous = FileOutputStream(saveFile)
while (isRecording) {
//音频数据写入缓冲区buffer
val result = audioRecord.read(buffer, 0, bufferSize)
//将缓冲区buffer数据写入本地文件
ous.write(buffer)
}
isRecording = false
ous.close()
audioRecord.stop()
}
}
2. AudioTrack实现音频播放
通过AudioRecord录制到的音频缓存在本地,我们通过AudioTrack将本地音频文件读取出来进行播放。
创建AudioTrack实例,大多数参数和AudioRecord的创建一致。
kotlin
audioTrack = AudioTrack.Builder()
.setAudioAttributes(
AudioAttributes.Builder()
.setUsage(AudioAttributes.USAGE_MEDIA)
.setContentType(AudioAttributes.CONTENT_TYPE_MUSIC)
.build()
)
.setAudioFormat(
AudioFormat.Builder()
.setEncoding(pcmBit)
.setSampleRate(sampleRate)
.setChannelMask(channel)
.build()
)
.setBufferSizeInBytes(bufferSize)
.build()
其中参数channel
要改成AudioFormat.CHANNEL_OUT_MONO
,其他可以保持一致。然后利用AudioTrack
实例读取本地音频进行播放。
kotlin
fun startPlay(path: String) {
if (isPlaying) return
val sourceFile = File(externalCacheDir,path)
if (sourceFile.exists()) {
isPlaying = true
GlobalScope.launch(Dispatchers.IO) {
//开始播放音频
audioTrack!!.play()
val fileInputStream = FileInputStream(sourceFile)
val buffer = ByteArray(bufferSize)
while (isPlaying) {
//将音频文件读取缓冲区buffer
val size = fileInputStream.read(buffer, 0, bufferSize)
if (size <= 0) {
isPlaying = false
continue
}
//将缓冲区buffer写入audioTrack进行播放
audioTrack.write(buffer, 0, bufferSize)
}
audioTrack.stop()
}
} else {
showToast("当前文件不存在:${path}")
}
}
通过代码可以看到,音频通过AudioRecord
录制和AudioTrack
播放是反着来的。同时通过AudioRecord
和AudioTrack
可以看出,作为Android开发者,更多是调用顶层API,配置相关参数,然后交给底层进行录制和播放。所以要熟悉相关音频概念,以便更好的配置参数。
Github Demo路径,包名: com.xxm.mediacodecdemo.audio
本文主要参考学习文章: