什么是 PCM 格式

声音从模拟信号转化为数字信号的技术,经过采样、量化、编码三个过程将模拟信号数字化。

  • 采样

    顾名思义,对模拟信号采集样本,该过程是从时间上对信号进行数字化,例如每秒采集 44100 次,即采样频率 44.1 khz

  • 量化

    既然是将音频数字化,那就需要使用二进制来表示声音的每一个样本。例如每个样本使用 16 位长度来表示,即音频的位深度为 16 位

  • 编码

    编码就是按照一定的格式记录采样和量化后的数据,比如顺序存储或压缩存储等

编码后经由不同的算法,音频被保存为不同的格式,例如 MP3、AAC 等,而 PCM 就是最为原始的一种格式,PCM 数据是音频的裸数据格式,不经过任何压缩。

从零到一:使用 AudioTrack 支持 PCM 格式音频的播放

AudioTrack 只支持播放 PCM 编码格式的音频流,平时使用的 MediaPlayer 支持 MP3、AAC 等多种音频格式,其内部也是将 MP3 格式文件使用 framework 层创建的解码器解码为 PCM 裸数据,再经由 AudioTrack 播放的。封装过的 Mediaplayer 的 API 是简单好用,但许多细节我们却无法掌控,而使用 AudioTrack,除了播放之外,我们还可以对数据源做许多有意思的操作,二者各有优劣之处。

先通过构造函数来了解 AudioTrack(版本为API26):

public AudioTrack(AudioAttributes attributes, AudioFormat format, int bufferSizeInBytes, int mode, int sessionId) {...
}

几个参数介绍一下:

  • AudioAttributes

    定义音频的类型,包括音乐、通知、闹钟等,调节音量时也会根据不同的类型进行调节

  • AudioFormat

    定义音频的格式,可配置声道数(单通道、多通道)、编码格式(每个采样数据位深度,8bit、16bit等)、采样率

  • bufferSizeInBytes

    AudioTrack 内部的音频缓冲区的大小,该缓冲区的值不能低于一帧“音频帧”(Frame)的大小,即:采样率 x 位深 x 采样时间 x 通道数,采样时间一般取 2.5ms~120ms 之间,AudioTrack 类提供了 getMinBufferSize() 方法来计算该值

  • mode

    AudioTrack 的两种播放模式,MODE_STATIC 和 MODE_STREAM,前者直接将数据加载进内存,后者是按照一定的间隔不断地写入数据

API26 下原有的两个构造函数已经被标为废弃,建议使用 Builder 来构造 AudioTrack 对象:

private fun createAudioTrack(): AudioTrack {val format = AudioFormat.Builder().setChannelMask(AudioFormat.CHANNEL_OUT_STEREO).setSampleRate(44100).setEncoding(AudioFormat.ENCODING_PCM_16BIT).build()return AudioTrack.Builder().setAudioFormat(format).build()
}

网上介绍 AudioTrack 时常见的如何构造 bufferSizeInBytes也不用关心了,Builder 类会替我们默认生成。

构造出对象后,在调用 play() 函数开启播放后,只要开启一个线程不断地从源文件中读取数据并调用 AudioTrack 的 write() 函数向手机端音频输出设备传输数据,即可播放 PCM 音频。

使用 ffmpeg 将 MP3 转为 PCM

Mac 下安装 ffmpeg:brew install ffmpeg

使用 ffmpeg 将 mp3 转换为 pcm:ffmpeg -i xxx.mp3 -f s16le -ar 44100 -ac 2 -acodec pcm_s16le xxx.pcm

  • -i 制定输入文件
  • -f 指定输出编码格式为16byte小端格式
  • -ar 指定输出采样率
  • -ac 指定输出通道数
  • acodec 指定解码格式
  • xxx.pcm 为输出文件

这里也提供一下我使用的 PCM 文件:hurt-johnny cash.pcm 44.1khz 双通道 16位深

PCM 录制:AudioRecord

MediaRecorder 录制集成了编码、压缩等功能,AudioRecord 录制的是 PCM 格式的音频文件。

同样的,先从构造函数来认识 AudioRecord:

public AudioRecord(AudioAttributes attributes, AudioFormat format, int bufferSizeInBytes, int sessionId) {...
}

接触过 AudioTrack,对 AudioRecord 一定不会感到陌生,其构造函数参数与 AudioTrack 几乎如出一辙,这里就不多说了。

AudioRecord 的 API 与 AudioTrack 也是遥相呼应的,在调用函数 startRecording() 开启录制后,只要开启一个后台线程不断地调用 read() 函数从手机端的音频输入设备(麦克风等)读取音频数据,并写入本地文件,即可实现音频的录制。

扩展支持的音频格式: WAV

最开始提到过音频会被编码成不同的格式,而常见的压缩编码格式 WAV 格式可能是与 PCM 数据最为接近的一种格式。WAV 编码不会进行压缩操作,它只在 PCM 数据格式前加上 44 字节(并不一定严格是 44 字节)来描述音频的基本信息,例如采样率、声道数、数据格式等。来看看 WAV 文件头的格式:

WAV 文件头格式

长度(字节) 内容
4 "RIFF" 字符串
4 从下个地址开始到文件尾的总字节数(音频 data 数据长度 + 44 -8)
4 "WAVE"
4 "fmt "(最后有一个空格)
4 过渡字节(一般为00000010H),若为00000012H则说明数据头携带附加信息(见“附加信息”)
2 格式种类,1 表示为PCM形式的声音数据
2 通道数,单声道为1,双声道为2
4 采样率
4 波形音频数据传送速率,其值为通道数×每秒数据位数×每样本的数据位数/8。播放软件利用此值可以估计缓冲区的大小。
2 每个采样需要的字节数,其值为通道数×位深度/8。播放软件需要一次处理多个该值大小的字节数据,以便将其值用于缓冲区的调整。
2 位深度
4 "data"
4 DATA数据长度

了解了 WAV 文件头的格式,我们可以尝试自己写一个解析 WAV 文件头的方法,结合上文的 AudioTrack 播放 PCM 的内容来看,只要获取到音频的采样率、位深度与声道数就可以播放该音频。自然也就可以播放内容是 PCM 格式的 WAV 文件。

在此之前需要一个 WAV 文件用作测试,可以使用 ffmpeg 将之前转换的 PCM 格式音频转码成 WAV 格式。

使用 ffmpeg 将 PCM 转为 WAV

ffmpeg -i xxx.pcm -f s16le -ar 44100 -ac 2 xxx.wav

也分享一下我使用的 WAV 文件:hurt-johnny cash.wav 44.1khz 双通道 16位深

解析 WAV 文件头

根据上面提到的 WAV 文件头格式,定义一个类用于存放文件头数据:

public class WaveHeader {private String riff; // "RIFF"private int totalLength; //音频 data 数据长度 + 44 -8private String wave; // "WAVE"private String fmt; // "fmt "private int transition; //过渡字节,一般为0x00000010private short type; // PCM:1private short channelMask; // 单声道:1,双声道:2private int sampleRate; //采样率private int rate;  // 波形音频数据传送速率,其值为通道数×每秒数据位数×每样本的数据位数/8private short sampleLength; // 每个采样需要的字节数,其值为通道数×位深度/8private short deepness; //位深度private String data; // "data"private int dataLength; //data数据长度...
}

定义好文件头后,我们使用 BufferedInputStream 从本地文件输入流中挨个字节读取数据即可。

byte 字节转字符串

private static String readString(InputStream inputStream, int length) {byte[] bytes = new byte[length];try {inputStream.read(bytes);return new String(bytes);} catch (IOException e) {e.printStackTrace();return null;}
}

byte 字节转 short、int

需要注意的是 WAV 头中的字节数组是经过反转的,例如表示单通道的字节数组为{1, 0},其中 1 为低位字节,即原始的字节为 [0, 1],转换为二进制为 0000 0000 0000 0001,即十进制的 1,代表单通道。

//从输入流中读取 2 个字节并转换为 short
private static short readShort(InputStream inputStream) {byte[] bytes = new byte[2];try {inputStream.read(bytes);//{1, 0}return (short) ((bytes[0] & 0xff) | ((bytes[1] & 0xff) << 8));} catch (IOException e) {e.printStackTrace();return 0;}
}
//从输入流中读取 4 个字节并转换为 int
private static int readInt(InputStream inputStream) {byte[] bytes = new byte[4];try {inputStream.read(bytes);return (int) ((bytes[0] & 0xff) | ((bytes[1] & 0xff) << 8) | ((bytes[2] & 0xff) << 16) | ((bytes[3] & 0xff) << 24));} catch (IOException e) {e.printStackTrace();return 0;}
}

从代码中可以看到,字节转换为整型数据类型(short、int 等)时,需要先与 0xff 做与(&)操作,这是为什么呢?

类型转换时 byte & 0xff 的原因

要究其原因,首先需要搞清楚计算机中的原码、反码和补码,这个在之前的文章《Bitmap 图像灰度变换原理浅析》中计算 int 数据类型的值范围时也提到过。下面举个栗子来验证一下,类型转换前 & 0xff 到底有什么用:

假定有一个 byte 数组:[0, 0, -1, 0],想求得这 4 个字节代表的 int 值得大小,重点看第三个字节,其值为 -1,byte 占一个字节 8 位,易得:

  • 原码:1000 0001
  • 反码:1111 1110
  • 补码:1111 1111

显而易见,这个 byte 数组代表的 int 值的二进制值为:

0000 0000 0000 0000 1000 0001 0000 0000

只要对 -1 左移 8 位即可。so easy!

byte a = -1;
int b = a << 8; //结果是 -256

值为负数就说明以上转换必然是错的。因为我们想要的结果是 0000 0000 0000 0000 1000 0001 0000 0000,该二进制最高位符号位为 0,结果必然是一个正数。

我们知道计算机在运算时使用的是补码,则 (byte)-1 << 8 运算时,计算机会对 -1 的补码 1111 1111 做位移操作,结果为 1111 1111 0000 0000,其原码为 1000 0001 0000 0000,先记作 16位原码A,当该值赋值给 int 类型的变量时,int 类型占 4 个字节 32 位,则需要对原因的 16 位值做位扩展,负数在位扩展时会对多出的高位补 1(正数补 0),则扩展后的值为 1111 1111 1111 1111 1111 1111 0000 0000,转为原码为 1000 0000 0000 0000 0000 0001 0000 0000,记作 32位原码B,与上面的 16 位原码做比较可以发现,二者最高位都是 1,低位的值也相同,两个二进制的值在十进制上是一致的,这就是负数补码高位补 1 的原因:为了保持十进制的一致性。不难理解这样做的原因,否则 short 类型强制为 int 类型后值就发生变化了,这明显是不可接受的。

回到我们的需求,我们这里并不需要保持十进制的一致性,所以要先与 0xff 做与运算,因为 0xff 是十六进制 32 位,二进制值为 32 个 1,-1 & 0xff 时,8 位与 32 位运算时,8 位的需要先在高位补 0 补齐到 32 位才会做运算,所以
-1 & 0xff 的结果为补码:1111 1111 前面带 24 个 0,最高位为正数,再对结果做位移操作,得到的二进制值补码为:0000 0000 0000 0000 1111 1111 0000 0000,因为是正数,原码与补码相同,该二进制值为 65280。可以验证下:

byte a = -1;
int c = (a & 0xff) << 8; //结果是 65280

概括一下就是:

  • 类型转换时补码位扩展(例如 2 个字节转 4 个字节,即 short 转 int)的规则:正数高位补 0,负数高位补1,以此保持十进制的一致性

  • 运算时,补码高位统一补 0

这里也附上将 byte 转二进制(补码)的方法:

public static String binary(byte bytes, int radix){byte[] bytes1 = new byte[1];bytes1[0] = bytes;return new BigInteger(1, bytes1).toString(radix);// 这里的1代表正数
}

顺序读取,构造 WavHeader

接着只要使用 InputStream 从目标音频中顺序读取各个参数的值并构造 WavHeader 即可,因为 Header 成员变量众多,所以考虑用建造者模式来构建 Header:

WaveHeader.Builder builder = new WaveHeader.Builder().setRiff(readString(dis, 4)).setTotalLength(readInt(dis)).setWave(readString(dis, 4)).setFmt(readString(dis, 4))...

另外上面提到过,并非所以 WAV 文件头都是标准的 44 个字节,例如我上面提供的 ffmpeg 转码后的 WAV 文件,其文件头的长度就是 78 个字节。对于文件头长度不一致的问题,我的解决方法是从 37 个字节开始,2 个 2 个字节地读取,直到读取到“da”和“ta”,之后再往后读取 4 个字节的 int 值作为 data 数据长度。读取到 header 后,后面播放的就不用说了,复用上面播放 PCM 的代码即可。

需要说明的是我只是从网上随机下载了几个 wav 格式音频测试了下是可以正常播放的,并没有经过广泛验证和对常见的 WAV 文件头格式的考证,所以可能还存在兼容问题。

经过这些以上的学习以及众多资料的查阅,对 Android 端音频开发有了一些小小的认识。后面还会学习一下使用 LAME 将 PCM 转码为 MP3,并实现一些真正意义上的音频播放器的基础功能等。再后面会学习一些视频方面的知识,包括 MediaExtractor、MediaMuxer 解析、封装 MP4 文件、OpenGL ES 渲染图像、MediaCodec 对音视频的硬编、硬解等,并使用一些流行的开源项目例如 ffmpeg 实现一些炫酷的视频处理功能,希望可以在 Android 音视频开发这一块能有所深入,学习过程中的一些收获和困惑也会坚持记录下来。

此路迢迢,与君共勉。

以上源码见 Github

作者:扬州慢_
链接:https://www.jianshu.com/p/a59e8f51d6f2
来源:简书
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

Android 音视频开发(一):PCM 格式音频的播放与采集相关推荐

  1. Android音视频开发:MediaRecorder录制音频

    Android 多媒体框架针对音频录制提供了两种方法:MediaRecorder和AudioRecord. 区别 MediaRecorder:录制的音频文件是经过压缩后的,需要设置编码器,并且录制的音 ...

  2. Android 音视频开发(二):使用 AudioRecord 采集音频PCM并保存到文件(学习笔记)

    关于 AudioRecord Android SDK 提供了两套音频采集的API,分别是:MediaRecorder 和 AudioRecord,前者是一个更加上层一点的API,它可以直接把手机麦克风 ...

  3. Android音视频开发,详说PCM音频重采样、PCM编码

    直播伴音,两种数据能否合在一起?不能叠加在一起 会有噪音 合并以后 再去编码推流 直播的例子 客户端播放器,可以开启多个播放器 对于我们重采样 很多时候就是为了统一格式,就是为了要合并这个流,去推送, ...

  4. Android 音视频开发(一) -- 使用AudioRecord 录制PCM(录音);AudioTrack播放音频

    前言,音视频这块,确实比较难入门,本着学习的态度,我这边也跟着 Android 音视频开发入门指南 打怪升级,留下个脚印,大家共勉. 音视频 系列文章 Android 音视频开发(一) – 使用Aud ...

  5. Android音视频学习系列(五) — 掌握音频基础知识并使用AudioTrack、OpenSL ES渲染PCM数据

    系列文章 Android音视频学习系列(一) - JNI从入门到精通 Android音视频学习系列(二) - 交叉编译动态库.静态库的入门 Android音视频学习系列(三) - Shell脚本入门 ...

  6. Android音视频开发之音频录制和播放

    Android音视频开发之音频录制和播放 1.封装音频录制工具类: public class RecorderAudioManagerUtils {private static volatile Re ...

  7. 【Android音视频开发】音频编码原理

    文章变更表 文章版本号 变更内容 变更日期 备注 0.0.1 创建 2022/9/29 初版 0.0.2 补充编码原理和音频格式等内容 2022/9/30 1. 前言 在[Android音视频开发] ...

  8. android音频开发6,Android 音视频开发(一) : 通过三种方式绘制图片

    想要逐步入门音视频开发,就需要一步步的去学习整理,并积累.本文是音视频开发积累的第一篇. 对应的要学习的内容是:在 Android 平台绘制一张图片,使用至少 3 种不同的 API,ImageView ...

  9. Android 音视频开发学习思路

    Android 音视频开发这块目前的确没有比较系统的教程或者书籍,网上的博客文章也都是比较零散的.只能通过一点点的学习和积累把这块的知识串联积累起来. 初级入门篇: Android 音视频开发(一) ...

最新文章

  1. SpringBoot高级篇MongoDB之修改基本使用姿势
  2. 计算机专业申请,申请计算机专业
  3. vrrp广播风暴_企业园区网MSTP+VRRP组合
  4. SQL用了Union后的排序问题
  5. 【Python小游戏】当当当当 万众瞩目得《滑雪大冒险》来啦~(附源码)
  6. php如何获取上传文件的后缀?
  7. Eclipse python 开发环境配置
  8. MFC中获取任务栏大小
  9. 解释性语言和汇编性语言对比
  10. 颜宁:给实验室博士的一些忠告
  11. namespace for c++
  12. 收藏 | 程序员生涯指南,在GitHub上获3.6万星
  13. 前锋php人工智能_人工智能除了学习php,还应该学什么?
  14. C语言左移右移操作符详解
  15. matlab逆变器原理,MATLAB中的单相全桥逆变器电路建模与仿真
  16. Re5:读论文 TWAG: A Topic-guided Wikipedia Abstract Generator
  17. 09SpringBoot web 错误处理
  18. 【技术白皮书】第一章:基于深度学习的文本检测与识别的技术背景
  19. 为什么不是每个人都过着他梦想中的生活呢?
  20. 百度松果 买礼物(贪心)

热门文章

  1. 十大Netflix原创喜剧类喜剧片(2020年6月)
  2. ubuntu16.04系统下谷歌浏览器不能拖入下载好的扩展程序离线包
  3. 5.2.4 ERP系统中C#应用实例(1)
  4. CVPR 2020 论文总结:Weakly Supervised Semantic Point Cloud Segmentation: Towards 10×Fewer Labels
  5. 【py脚本】(小记)Lableme 的json转yolo的txt(仅方框)/多边形labelme转coco/关键点检测labelme转coco/yolo-voc-yolo
  6. linux 配置oracle instantclient
  7. 除了labview你还知道哪些工业控制领域的软件?
  8. -- 某大学生写给女朋友的信
  9. Gridview利用DataFormatString属性设置数据格式
  10. 2022年秋季期寒假总结