Android 音视频开发(一):PCM 格式音频的播放与采集
什么是 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 格式音频的播放与采集相关推荐
- Android音视频开发:MediaRecorder录制音频
Android 多媒体框架针对音频录制提供了两种方法:MediaRecorder和AudioRecord. 区别 MediaRecorder:录制的音频文件是经过压缩后的,需要设置编码器,并且录制的音 ...
- Android 音视频开发(二):使用 AudioRecord 采集音频PCM并保存到文件(学习笔记)
关于 AudioRecord Android SDK 提供了两套音频采集的API,分别是:MediaRecorder 和 AudioRecord,前者是一个更加上层一点的API,它可以直接把手机麦克风 ...
- Android音视频开发,详说PCM音频重采样、PCM编码
直播伴音,两种数据能否合在一起?不能叠加在一起 会有噪音 合并以后 再去编码推流 直播的例子 客户端播放器,可以开启多个播放器 对于我们重采样 很多时候就是为了统一格式,就是为了要合并这个流,去推送, ...
- Android 音视频开发(一) -- 使用AudioRecord 录制PCM(录音);AudioTrack播放音频
前言,音视频这块,确实比较难入门,本着学习的态度,我这边也跟着 Android 音视频开发入门指南 打怪升级,留下个脚印,大家共勉. 音视频 系列文章 Android 音视频开发(一) – 使用Aud ...
- Android音视频学习系列(五) — 掌握音频基础知识并使用AudioTrack、OpenSL ES渲染PCM数据
系列文章 Android音视频学习系列(一) - JNI从入门到精通 Android音视频学习系列(二) - 交叉编译动态库.静态库的入门 Android音视频学习系列(三) - Shell脚本入门 ...
- Android音视频开发之音频录制和播放
Android音视频开发之音频录制和播放 1.封装音频录制工具类: public class RecorderAudioManagerUtils {private static volatile Re ...
- 【Android音视频开发】音频编码原理
文章变更表 文章版本号 变更内容 变更日期 备注 0.0.1 创建 2022/9/29 初版 0.0.2 补充编码原理和音频格式等内容 2022/9/30 1. 前言 在[Android音视频开发] ...
- android音频开发6,Android 音视频开发(一) : 通过三种方式绘制图片
想要逐步入门音视频开发,就需要一步步的去学习整理,并积累.本文是音视频开发积累的第一篇. 对应的要学习的内容是:在 Android 平台绘制一张图片,使用至少 3 种不同的 API,ImageView ...
- Android 音视频开发学习思路
Android 音视频开发这块目前的确没有比较系统的教程或者书籍,网上的博客文章也都是比较零散的.只能通过一点点的学习和积累把这块的知识串联积累起来. 初级入门篇: Android 音视频开发(一) ...
最新文章
- SpringBoot高级篇MongoDB之修改基本使用姿势
- 计算机专业申请,申请计算机专业
- vrrp广播风暴_企业园区网MSTP+VRRP组合
- SQL用了Union后的排序问题
- 【Python小游戏】当当当当 万众瞩目得《滑雪大冒险》来啦~(附源码)
- php如何获取上传文件的后缀?
- Eclipse python 开发环境配置
- MFC中获取任务栏大小
- 解释性语言和汇编性语言对比
- 颜宁:给实验室博士的一些忠告
- namespace for c++
- 收藏 | 程序员生涯指南,在GitHub上获3.6万星
- 前锋php人工智能_人工智能除了学习php,还应该学什么?
- C语言左移右移操作符详解
- matlab逆变器原理,MATLAB中的单相全桥逆变器电路建模与仿真
- Re5:读论文 TWAG: A Topic-guided Wikipedia Abstract Generator
- 09SpringBoot web 错误处理
- 【技术白皮书】第一章:基于深度学习的文本检测与识别的技术背景
- 为什么不是每个人都过着他梦想中的生活呢?
- 百度松果 买礼物(贪心)
热门文章
- 十大Netflix原创喜剧类喜剧片(2020年6月)
- ubuntu16.04系统下谷歌浏览器不能拖入下载好的扩展程序离线包
- 5.2.4 ERP系统中C#应用实例(1)
- CVPR 2020 论文总结:Weakly Supervised Semantic Point Cloud Segmentation: Towards 10×Fewer Labels
- 【py脚本】(小记)Lableme 的json转yolo的txt(仅方框)/多边形labelme转coco/关键点检测labelme转coco/yolo-voc-yolo
- linux 配置oracle instantclient
- 除了labview你还知道哪些工业控制领域的软件?
- -- 某大学生写给女朋友的信
- Gridview利用DataFormatString属性设置数据格式
- 2022年秋季期寒假总结