音视频小白如何实现一个录音/播放器

2,246 阅读12分钟

如果你从未接触过音视频开发,但有实现一个录音器、播放器的需求或想法,本文会是一个比较好的入门内容。

本博客是从基础内容到具体的实践,再展现一个简易的整体框架,内容主要有:

1.音频基础知识,可以了解音频知识的基础要素

2.选择合适的录制和播放的实现方式满足需求

3.音频的简易框架,了解Android音频框架的整体设计


音频基础知识

音频的录制与回放

1.录制过程

音频采集设备捕捉声音信息(模拟信号) -> 模-数转化器(ADC)将音频信号转换成计算机能接收的二进制数据 -> 渲染处理(音效调整、过滤)-> 压缩

2.回放过程

读取音频文件 -> 根据录制过程的编码方式进行解码 -> 选取音频回放设备 -> 音频数据信号根据数-模转换器(DAC)变成模拟信号 -> 经过回放设备进行播放

录制和回放过程中很重要的一步是数字信号和模拟信号的转换,比如回放过程中,数字信号需要转换成模拟信号,背后其实就是二进制数据通过DAC一步步转换成波形最后变成模拟信号(以下图借自【硬件科普】音响耳机麦克风这些设备是怎么工作的?音频的采样率和采样精度是什么?):

数字信号转模拟信号.png

采样率(Sample Rate)

采样率表示音频信号每秒的数字快照数。该速率决定了音频文件的频率范围。采样率越高,数字波形的形状越接近原始模拟波形。低采样率会限制可录制的频率范围,这可导致录音表现原始声音的效果不佳。

采样率波形图.png

为了重现给定频率,采样率必须至少是该频率的两倍。例如,CD 的采样率为每秒 44,100 个采样,因此可重现最高为 22,050 Hz 的频率,此频率刚好超过人类的听力极限 20,000 Hz。采样率针对的是信号的时间(频率)特性

为什么采样率要是频率的2倍?

当采样频率设置不合理时,即采样频率少于2倍的信号频率时,会导致原本的高频信号被采样成低频信号

采样定理为 2 倍,为什么经常用 2.56 倍进行采样?

采样深度(Bit Depth)

采样深度是指声音的连续强度被数字表示后可以分为多少级。用N-bit表示,意思是指声音的强度被均分为2^N级。16-bit的话,就是65535级。这是一个很大的数了,人可能也分辨不出六万五千五百三十五分之一的音强差别。也可以说是声卡的分辨率,它的数值越大,分辨率也就越高,所发出声音的能力越强。这里的采样深度主要针对的是信号的强度特性

声道(Sound Channel)

声道是指声音在录制或播放时在不同空间位置采集或回放的相互独立的音频信号,通俗的说声道数就是录音时的麦克风数量,也是播放时的音响数量。 从声音塑造的感官进行区分,分为单声道(Mono)和立体声(Stereo)

1.单声道(Mono)

声音塑造为单一方向的

2.立体声(Stereo)

声音塑造为立体的,现在一般用于指代双声道立体声(2Ch Stereo),区别于环绕立体声

比特率(Bit Rate)

比特是由bit音译而来,指二进制数中的,它是数字信息的最小度量单位。

在数字多媒体领域,比特率是每秒播放连续的音频或视频的比特的数量,是音视频文件的一个属性。此时它相当于术语"数字带宽消耗量或吞吐量",也俗称为"码率"。

比特率的计算:音频的比特率=采样率×位深度×通道数

假设有一段采样频率44.1KHz,采样位数16bit,立体声的PCM音频。也就是说,在产生这段音频时间里,1s内系统采样的次数是44100次,每次采样的数据位数是16位,同时进行2通道采样。这就意味着,系统每秒采集的比特数为44100次×16位×2通道 = 1411200个。根据比特率的定义,这段音频的比特率就是1411.2kbit/s。如果还知道这段PCM音频文件的时长,还可以计算文件的大小:假设文件时长为1分钟,那么文件大小为1411.2kbit/s × 60s = 84672kbit,而1byte=8bit,所以文件大小为10,584,000B = 10,355.9KB = 10.1MB。

字节中的KB、MB、GB的递进关系是1024,而比特率中的kb,Mb,Gb的递进关系则是1000

对于mp3,wav等其他格式的音频文件,文件里还包括了帧头等其他附加信息,所以文件体积还会稍大一些。

PCM(⭐️)

脉冲编码调制(Pulse Code Modulation, PCM)是一种模拟信号的数字化方法。 它是最常用、最简单的波形编码方式,但是也存在其它方法,比如脉冲密度调制(Pulse Density Modulation, PDM)。

PCM编码是最原始的音频编码,其他编码都是在它基础上再次编码和压缩的。

PCM文件是以PCM编码方式存储音频的文件,是未经压缩的原始数字音频文件,通常称为PCM裸流/音频裸数据/raw data。常用文件扩展名是.pcm和.raw,通常它们是不能直接播放的。PCM裸流经过重新编码,封装后,比如变为.wav/.mp3格式,就可以正常播放了。

区分PCM与ADC:

PCM(Pulse Code Modulation)是一种模拟信号的数字化方法,ADC(Analog to Digital Converter)芯片是实现这一方法的器件。

硬编码和软编码

编解码分为硬件编解码和软件编解码。

软件编解码就是指利用CPU的计算能力来进行编解码码,通常如果CPU的能力不是很强的时候,一则编解码速度会比较慢,二则手机可能出现发热现象。但是,由于使用统一的算法,兼容性会很好。

硬件编解码解码,指的是利用手机上专门的解码芯片来加速解码。通常硬解码的解码速度会快很多,但是由于硬解码由各个厂家实现,质量参差不齐,非常容易出现兼容性问题。

编码格式(⭐️)

1.文件格式和编码格式

常见的音频文件都有两部分格式:一是文件格式,二是编码格式。

两者是不同的概念:

  • 文件格式专指存放音频数据的文件的格式,对应文件的扩展名
  • 编码格式则是指音频数据的特定格式,也叫数据格式,音频编码

大部分情况下,一种文件格式对应一种音频编码。但是也有例外,比如.caf的文件格式就能包含MP3LPCM和其他格式编码的音频数据,AAC编码格式对应的文件的扩展名就有.aac.mp4.m4a

2.有损编码和无损编码

理论上说,任何数字音频都是无法完全还原模拟信号的。不过PCM编码是模拟信号转换为数字信号时的原始编码,它代表着数字音频的最佳保真水平,所以PCM编码就约定俗成为"无损编码"。

音频编码是对PCM编码进行了二次编码,是为了减小原始PCM编码的体积,所以也叫它们为压缩编码,对应的文件叫压缩格式

二次编码(压缩)也有两种方式,有损压缩和无损压缩。无损就是指相对PCM编码来说音质相同,有损则是损失了一些音频质量。

3.Android平台中内置的媒体格式支持

Android媒体格式支持.png

Android媒体格式支持2.png

录制与播放方式的选择

Android官方为我们提供了多种录制和播放音频的方式,比如MediaRecorderMediaPlayerAudioRecordAudioTrack,如何去选择合适的方式?

MediaRecorder

MediaRecorder用于录制音频和视频,录音的控制基于一个简单的状态机:

MediaRecorder状态图.png

使用起来也很简单,唯一要注意的就是它的状态变换,使用不当就很容易出现状态错误的异常:

MediaRecorder recorder = new MediaRecorder();
recorder.setAudioSource(MediaRecorder.AudioSource.MIC);
recorder.setOutputFormat(MediaRecorder.OutputFormat.THREE_GPP);  // ->Initialzed
recorder.setAudioEncoder(MediaRecorder.AudioEncoder.AMR_NB);     
recorder.setOutputFile(PATH_NAME);                              // -> DataSourceConfigured
recorder.prepare();                                             // -> Prepared
recorder.start();   // 开始录制,进入录制状态                       // -> Recording
...
recorder.stop();                                                             // -> Initial
recorder.reset();   // 回到setAudioSource() 这一步后,就可以再次使用该对象        // -> Initial
recorder.release(); // 对象已经不能再次使用了                                   // -> Released

MediaPlayer

MediaPlayerMediaRecorder一样,音视频文件和流的播放控制,也是通过状态机进行管理,但会更复杂一些:

MediaPlayer状态图.png

需要注意的是:带有单箭头的圆弧表示同步方法调用,而带有双箭头的圆弧表示异步方法调用。

Uri myUri = ....; // initialize Uri here
MediaPlayer mediaPlayer = new MediaPlayer();                // -> Idle
mediaPlayer.setAudioStreamType(AudioManager.STREAM_MUSIC);   
mediaPlayer.setDataSource(getApplicationContext(), myUri);  // -> Initialized
mediaPlayer.prepare();                                      // -> Prepared
mediaPlayer.start();                                        // -> Started
mediaPlayer.pause()                                         // -> Paused
mediaPlayer.seekTo()                                        // -> Paused
mediaPlayer.stop()                                          // -> Stopped

AudioRecord

1.使用

AudioRecord也是官方提供的,帮助我们记录来自平台音频输入硬件的音频的类。

创建时,AudioRecord 对象初始化其关联的音频缓冲区,它将用新的音频数据填充。这个缓冲区的大小,在构造过程中指定,决定了AudioRecord 在“溢出”尚未读取的数据之前可以记录多长时间。应该从音频硬件中以小于总记录缓冲区大小的块读取数据。

它通过从 AudioRecord 对象“拉”(读取)数据来实现的。应用程序负责使用以下三种方法之一及时轮询AudioRecord 对象:read(byte[], int, int)read(short[], int, int)read(ByteBuffer, int)

val fileOutputStream = FileOutputStream(File(filePath), true)
val buffer = ByteArray(mMinReadBufferSize)
try {
    while (isRecording) {
      // 不断拉数据,写入到文件流中
      val read = mAudioRecord.read(buffer, 0, mMinReadBufferSize)
      if (AudioRecord.ERROR_INVALID_OPERATION != read) {
          fileOutputStream.write(buffer)
      }
    }
    fileOutputStream.close()
} catch (e: Exception) {
    Log.e(TAG, "writeRecordDataToFile exception")
    e.printStackTrace()
} finally {
    fileOutputStream.close()
}

2.与MediaRecorder的区别

MediaRecorderAudioRecord都可以录制音频,区别是MediaRecorder录制的音频文件是经过压缩后的,需要设置编码器。

并且录制的音频文件可以用系统自带的音乐播放器播放。而AudioRecord录制的是PCM格式的音频文件,不能直接播放需要用AudioTrack来播放,AudioTrack更接近底层。

当然两者之间还是有紧密的联系的,在用MediaRecorder进行录制音视频时,最终还是会创建AudioRecord用来与AudioFlinger进行交互。

AudioTrack

1.使用

AudioTrack可以实现音频的播放,它允许将 PCM 音频缓冲区流式传输到音频接收器以进行播放。

通过使用write(byte[], int, int) 、 write(short[], int, int)和write(float[], int, int, int)之一将数据“推送”到AudioTrack对象来实现的write(float[], int, int, int)方法。

AudioTrack实例可以在两种模式下运行:静态或流式传输

在流式传输模式(Streaing)下,应用程序使用write()方法之一将连续的数据流写入AudioTrack。当数据从Java 层传输到本机层并排队等待播放时,这些是阻塞和返回的。当播放音频数据块时,流模式最有用。在处理适合内存并且需要以尽可能小的延迟播放的短声音时,应选择静态模式。因此,静态模式将更适合经常播放的 UI 和游戏声音,并且开销可能最小。

val fileInputStream = FileInputStream(filePath)
val buffer = ByteArray(mMinWriteBufferSize)
try {
while (fileInputStream.available() > 0) {
    val readCount = fileInputStream.read(buffer)
    // 如果读到的数据有问题,则跳过本段
    if (readCount == AudioTrack.ERROR_INVALID_OPERATION || readCount == AudioTrack.ERROR_BAD_VALUE) {
        Log.e(TAG, "playRecord: read exception $readCount skip this")
        continue
    }
    if (readCount != 0 && readCount != 1) {
        mAudioTrack.write(buffer, 0, readCount)
    }
 }
} catch (e: Exception) {
    Log.e(TAG, "playRecord exception!")
    e.printStackTrace()
} finally {
    fileInputStream.close()
}

2.与MediaPlayer的区别

MediaPlayerAudioTrack虽然都可以播放声音,但两者还是有很大的区别的。其中最大的区别是MediaPlayer可以播放多种格式的声音文件,例如MP3AACWAVOGGMIDI等。

MediaPlayer会在Framework层创建对应的音频解码器。而AudioTrack只能播放已经解码的PCM流,如果是文件的话只支持wav格式的音频文件,因为wav格式的音频文件大部分都是PCM流AudioTrack不创建解码器,所以只能播放不需要解码的wav文件

当然两者之间还是有紧密的联系的,MediaPlayerFramework层还是会创建AudioTrack,把解码后的PCM裸流传递给AudioTrackAudioTrack再传递给AudioFlinger进行混音,然后才传递给硬件播放。所以MediaPlayer包含了AudioTrack

音频框架

由于Android高度封装,让我们很轻松通过的就可以使用MediaRecorderMediaPlayer实现音频录制、播放的API实现基本功能。

但也正是因为封装太好,屏蔽了太多的细节,给我们去看背后的内部实现增添了难度。音频整体的框架可以简化成这样:

音频框架简易版.png

参考

音频相关的基础知识,这个视频动画做的很好:

【硬件科普】音响耳机麦克风这些设备是怎么工作的?音频的采样率和采样精度是什么?

音频格式和编解码器:

音频格式和编解码器

PCM的解释,这篇文章说的挺详细,推荐阅读:

数字音频基础­­­­­-从PCM说起

如果想对AudioTrack背后的工作机制深入理解,该博客值得一看:

Android 音频系统:从 AudioTrack 到 AudioFlinger

寻找资料时,在gitHub上偶然发现一个音视频学习的笔记仓库,想深入学习的可以关注下:

音频笔记汇总