前言,音视频这块,确实比较难入门,本着学习的态度,我这边也跟着 Android 音视频开发入门指南 打怪升级,留下个脚印,大家共勉。

音视频 系列文章
Android 音视频开发(一) – 使用AudioRecord 录制PCM(录音);AudioTrack播放音频
Android 音视频开发(二) – Camera1 实现预览、拍照功能
Android 音视频开发(三) – Camera2 实现预览、拍照功能
Android 音视频开发(四) – CameraX 实现预览、拍照功能
Android 音视频开发(五) – 使用 MediaExtractor 分离音视频,并使用 MediaMuxer合成新视频(音视频同步)
音视频工程

今天要完成的功能如下;

  1. 使用AudioRecord 进行录音
  2. 生成 wav 格式的音频,并进行播放
  3. 使用 AudioTrack 播放 pcm 格式音频 (Stream 和 static 模式)

由于声音不好上动图,只能来一张静图了,具体代码看工程:音视频学习Demo

一、基础知识

首先,我们先要了解声音是怎么被保存的起来的。在我们的世界中,声音是连续不断的,是一种模拟信号,那如何把声音保存起来呢?计算机能识别的就是二进制,所以,对声音这种模拟信号,采用数字化,即转换成数字信号,就能保存了。

从上面知道,声音是一种波,有自己的振幅和频率,如果要保存声音,就要保存各个时间点上的振幅;而数字信号并不能保存所有时间点的振幅,事实上,并不需要保存连续的信号,就可以还原到人耳可接受的声音;根据奈奎斯特定律:为了不失真地恢复模拟信号,采样频率应该不小于模拟信号频谱中最高频率的2倍

音频数据的承载方式,最常用的就是 脉冲编码调制,即 PCM
根据上面的分析,PCM 的采集步骤可以以下步骤:

模拟信号 -> 采样 -> 量化 -> 编码 -> 数字信号

1.1 采样率

上面提到,采样率要大于原声波频率的2倍,人耳能听到的最高频率为 20khz,所以,为了满足人耳的听觉要求,采样率至少为40khz,通常就是为 44.1khz,更高则是 48 khz。一般我们都采用 44.1khz 即可达到无损音质。

1.2 采样位数

上面说到模拟信号是连续的样本值,而数字信号一般是不连续的,所以模拟信号量化,只能取一个近似的整数值,为了记录这些振幅值,采样器会采用一个固定的位数来记录这些振幅值,通常有 8 位,16位,32位。

位数 最小值 最大值
8 0 255
16 -32468 32767
32 -2147483648 2147483648

位数越大,记录的值越准确,还原度越高。

最后就是编码了,数字信号由0,1组成的,因此,需要将振幅转换成一系列 0和1进行存储,也就是编码,最后得到的数据就是数字信号:一串0和1组成的数据:

1.3 声道数

指支持能 不同发声(注意是不同声音) 的音响的个数。

  • 单声道:一个声道
  • 双声道:2个声道
  • 立体声:2个声道
  • 立体声(4声道):4个声道

二. 使用 AudioRecord 录音

上面了解音频的基础知识后,我们接着使用 AudioRecord 来录制原始数据,即 PCM 数据;
当手机的硬件录音之后,AudioRecord 可以从该硬件提取音频资源;读取的方法可以使用 read() 方法来实现。那么我们现在开始,首先,先申请好权限:

<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" /><uses-permission android:name="android.permission.RECORD_AUDIO" />

在 button 的down 事件开始录音,在up 的时候停止录音:

switch (event.getAction()) {case MotionEvent.ACTION_DOWN://开始录制startRecord();break;case MotionEvent.ACTION_UP:case MotionEvent.ACTION_CANCEL:if (mAudioThread != null) {mAudioThread.done();}break;#startRecord()
private void startRecord() {//如果存在,先停止线程if (mAudioThread != null) {mAudioThread.done();mAudioThread = null;}//开启线程录制mAudioThread = new AudioThread();mAudioThread.start();}

接着初始化 AudioRecord:

/*** 获取最小 buffer 大小,即一帧的buffer* 采样率为 44100,双声道,采样位数为 16bit*/
minBufferSize = AudioRecord.getMinBufferSize(AUDIO_RATE, AudioFormat.CHANNEL_IN_STEREO, AudioFormat.ENCODING_PCM_16BIT);
//使用 AudioRecord 去录音
record = new AudioRecord(MediaRecorder.AudioSource.MIC,AUDIO_RATE,AudioFormat.CHANNEL_IN_STEREO,AudioFormat.ENCODING_PCM_16BIT,minBufferSize
);

上面通过 AudioRecord.getMinBufferSize() 来获取最小一帧的buffer 大小,这样我们能保证每一帧都能被录制。它的参数如下:

  • sampleRateInHz :采样率,上面说到,想要无损印制,至少 44100hz ,所以这里也是用 44100hz
  • channelConfig:声道,这里采用双声道
  • audioFormat:PCM 采样位数,一般现在的手机斗志16bit的,也足够使用了,所以这里也是使用 16 bit

接着创建 AudioRecord ,参数也不难理解,这里不再赘述。

怎么去录制呢?说白了,就是通过 AudioRecord 的read方法,它会把数据读写到 byte[] 数组中,然后返回写入的大小,根据 byte 就可以保存到文件中了,代码如下:

        @Overridepublic void run() {super.run();FileOutputStream fos = null;try {//没有先创建文件夹File dir = new File(PATH);if (!dir.exists()) {dir.mkdirs();}//创建 pcm 文件File pcmFile = getFile(PATH, "test.pcm");fos = new FileOutputStream(pcmFile);//开始录制record.startRecording();byte[] buffer = new byte[minBufferSize];while (!isDone) {//读取数据int read = record.read(buffer, 0, buffer.length);if (AudioRecord.ERROR_INVALID_OPERATION != read) {//写 pcm 数据fos.write(buffer, 0, read);}}//录制结束record.stop();record.release();fos.flush();} catch (IOException e) {e.printStackTrace();} finally {CloseUtils.close(fos);record.release();}}

首先,使用 record.startRecording() 开始,此时它会开始监听硬件音频数据,然后通过 read() 方法读取数据,接着把它保存到文件中。
然后我们发现,已经保存了音频的原始数据 PCM 文件:

二、Wav 文件

上面只保存了 pcm 文件,但这是原始的 pcm 文件,它是不支持播放的。我们需要将它转换成 wav 这种可以被识别解码的音频格式。
想要把 pcm 格式转换成 wav,只需要在pcm的文件起始位置加上至少44个字节的WAV头信息即可,
这个文件头记录着音频流的编码参数。数据块的记录方式是little-endian字节顺序,来一张官方图:

关于 wav 的说明,这里不重点介绍,它的头部生成方法如下:

/*** 任何一种文件在头部添加相应的头文件才能够确定的表示这种文件的格式,* wave是RIFF文件结构,每一部分为一个chunk,其中有RIFF WAVE chunk,* FMT Chunk,Fact chunk,Data chunk,其中Fact chunk是可以选择的** @param pcmAudioByteCount 不包括header的音频数据总长度* @param longSampleRate    采样率,也就是录制时使用的频率* @param channels          audioRecord的频道数量*/private byte[] generateWavFileHeader(long pcmAudioByteCount, long longSampleRate, int channels) {long totalDataLen = pcmAudioByteCount + 36; // 不包含前8个字节的WAV文件总长度long byteRate = longSampleRate * 2 * channels;byte[] header = new byte[44];header[0] = 'R'; // RIFFheader[1] = 'I';header[2] = 'F';header[3] = 'F';header[4] = (byte) (totalDataLen & 0xff);//数据大小header[5] = (byte) ((totalDataLen >> 8) & 0xff);header[6] = (byte) ((totalDataLen >> 16) & 0xff);header[7] = (byte) ((totalDataLen >> 24) & 0xff);header[8] = 'W';//WAVEheader[9] = 'A';header[10] = 'V';header[11] = 'E';//FMT Chunkheader[12] = 'f'; // 'fmt 'header[13] = 'm';header[14] = 't';header[15] = ' ';//过渡字节//数据大小header[16] = 16; // 4 bytes: size of 'fmt ' chunkheader[17] = 0;header[18] = 0;header[19] = 0;//编码方式 10H为PCM编码格式header[20] = 1; // format = 1header[21] = 0;//通道数header[22] = (byte) channels;header[23] = 0;//采样率,每个通道的播放速度header[24] = (byte) (longSampleRate & 0xff);header[25] = (byte) ((longSampleRate >> 8) & 0xff);header[26] = (byte) ((longSampleRate >> 16) & 0xff);header[27] = (byte) ((longSampleRate >> 24) & 0xff);//音频数据传送速率,采样率*通道数*采样深度/8header[28] = (byte) (byteRate & 0xff);header[29] = (byte) ((byteRate >> 8) & 0xff);header[30] = (byte) ((byteRate >> 16) & 0xff);header[31] = (byte) ((byteRate >> 24) & 0xff);// 确定系统一次要处理多少个这样字节的数据,确定缓冲区,通道数*采样位数header[32] = (byte) (2 * channels);header[33] = 0;//每个样本的数据位数header[34] = 16;header[35] = 0;//Data chunkheader[36] = 'd';//dataheader[37] = 'a';header[38] = 't';header[39] = 'a';header[40] = (byte) (pcmAudioByteCount & 0xff);header[41] = (byte) ((pcmAudioByteCount >> 8) & 0xff);header[42] = (byte) ((pcmAudioByteCount >> 16) & 0xff);header[43] = (byte) ((pcmAudioByteCount >> 24) & 0xff);return header;}

在上面的方法中,通过先后才能下载了 pcm 文件;为了方便,我们可以在 下载 pcm 之前,把头部信息先存储起来,接着再填充 pcm 文件,完成代码如下:

        @Overridepublic void run() {super.run();FileOutputStream fos = null;FileOutputStream wavFos = null;RandomAccessFile wavRaf = null;try {//没有先创建文件夹File dir = new File(PATH);if (!dir.exists()) {dir.mkdirs();}//创建 pcm 文件File pcmFile = getFile(PATH, "test.pcm");//创建 wav 文件File wavFile = getFile(PATH, "test.wav");fos = new FileOutputStream(pcmFile);wavFos = new FileOutputStream(wavFile);//先写头部,刚才是,我们并不知道 pcm 文件的大小byte[] headers = generateWavFileHeader(0, AUDIO_RATE, record.getChannelCount());wavFos.write(headers, 0, headers.length);//开始录制record.startRecording();byte[] buffer = new byte[minBufferSize];while (!isDone) {//读取数据int read = record.read(buffer, 0, buffer.length);if (AudioRecord.ERROR_INVALID_OPERATION != read) {//写 pcm 数据fos.write(buffer, 0, read);//写 wav 格式数据wavFos.write(buffer, 0, read);}}//录制结束record.stop();record.release();fos.flush();wavFos.flush();//修改头部的 pcm文件 大小wavRaf = new RandomAccessFile(wavFile, "rw");byte[] header = generateWavFileHeader(pcmFile.length(), AUDIO_RATE, record.getChannelCount());wavRaf.seek(0);wavRaf.write(header);} catch (IOException e) {e.printStackTrace();Log.d(TAG, "zsr run: " + e.getMessage());} finally {CloseUtils.close(fos, wavFos,wavRaf);}}public void done() {interrupt();isDone = true;}});

可以看到,我们先新建了一个 test.wav 的文件,先写入 头部信息,由于无法确定pcm的大小,先传入0,接着再把 pcm 写入到 wav 文件中,当录制结束,再把 pcm 的文件大小写入header头部即可。

当点击 test.wav 就可以播放啦,就可以听到你自己的骚声音了。

2.1 播放 wav 文件

这里使用Android自带的播放器就可以了,当然你也可以使用 MediaPlayer,使用Android 自带的如下:

    public void playwav(View view) {File file = new File(PATH, "test.wav");if (file.exists()) {Intent intent = new Intent();intent.setAction(Intent.ACTION_VIEW);Uri uri;//Android 7.0 以上,需要使用 FileProvider if (Build.VERSION.SDK_INT > Build.VERSION_CODES.N) {uri = FileProvider.getUriForFile(this, "com.zhengsr.videodemo.fileprovider", file);} else {uri = Uri.fromFile(file.getAbsoluteFile());}intent.setDataAndType(uri, "audio");intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);startActivity(intent);} else {Toast.makeText(this, "请先录制", Toast.LENGTH_SHORT).show();}}

注意,如果是7.0 及以上,不能使用显性的 Uri了,所以需要使用FileProvider,它其实也是一个 contentprovider,记得在 AndroidMinefest 也写上:

新建一个 xml,添加一个 file_paths.xml 文件:

<?xml version="1.0" encoding="utf-8"?>
<paths xmlns:android="http://schemas.android.com/apk/res/android"><external-pathname="com.zhengsr.videodemo.fileprovider"path="/"/>
</paths>

三. 播放 PCM 音频

上面我们通过转换 PCM 为 WAV ,使其变成能够被多媒体解码识别的文件,但如果我想播放 pcm 文件呢?

这里可以通过 AudioTrack 来实现该功能,它为 Android 管理和播放音频的管理类,允许 PCM 音频通过write() 方法将数据流推送到 AudioTrack 来实现音频的播放。(当然也不局限 pcm,其他音频格式也支持的)

AudioTrack 有两种模式:流模式和静态模式

流模式:在流模式,当使用 write() 方法时,会向 AudioTrack 写入连续的数据流,数据会从 Java 层传输到底层,并排队阻塞等待播放;在播放音频块数据时,流模式比较好用:

  • 音频数据过大过长,无法存入内存时
  • 由于音频数据的特性(高采样率,每采样位…),太大而无法装入内存。
  • 接收或生成时,先前排队的音频正在播放。

静态模式:静态模式,它需要一次性把数据写到buffer中,适合小音频,小延迟的音频播放,常用在UI和游戏中比较实用。

这里,我们粉笔用两种模式去读取刚才的录音。

3.1 静态模式

上面说到,静态模式下,需要一次性把音频数据写到buffer中,所以这个 buffer 肯定不能太大,不过我们刚才的录音不算大,所以可以拿到上面录制的 pcm 来实践。

    public void playpcm2(View view) {try {File file = new File(PATH, "test.pcm");InputStream is = new FileInputStream(file);ByteArrayOutputStream baos = new ByteArrayOutputStream();int len;//创建一个数组byte[] buffer = new byte[1024];while ((len = is.read(buffer)) > 0) {//把数据存到ByteArrayOutputStream中baos.write(buffer, 0, len);}//拿到音频数据byte[] bytes = baos.toByteArray();//双声道int channelConfig = AudioFormat.CHANNEL_IN_STEREO;/*** 设置音频信息属性* 1.设置支持多媒体属性,比如audio,video* 2.设置音频格式,比如 music*/AudioAttributes attributes = new AudioAttributes.Builder().setUsage(AudioAttributes.USAGE_MEDIA).setContentType(AudioAttributes.CONTENT_TYPE_MUSIC).build();/*** 设置音频哥特式* 1. 设置采样率* 2. 设置采样位数* 3. 设置声道*/AudioFormat format = new AudioFormat.Builder().setSampleRate(AUDIO_RATE).setEncoding(AudioFormat.ENCODING_PCM_16BIT).setChannelMask(channelConfig).build();//注意 bufferSizeInBytes 使用音频的大小AudioTrack audioTrack = new AudioTrack(attributes,format,bytes.length,AudioTrack.MODE_STATIC, //设置为静态模式AudioManager.AUDIO_SESSION_ID_GENERATE //音频识别id);//一次性写入audioTrack.write(bytes, 0, bytes.length);//开始播放audioTrack.play();} catch (Exception e) {e.printStackTrace();Log.d(TAG, "zsr playpcm2: " + e);}}

注释都比较清晰了,先把 test.pcm 文件的数据取出来,放到 ByteArrayOutputStream,然后再通过 audioTrack.write() 写入到 audiotrack中,点击播放即可。

3.2 流模式

流模式,数据会从 Java 层传输到底层,并排队阻塞等待播放,所以,这里我们开启一个线程,读取数据后等待播放,初始化与 static 模式没啥区别:

        public AudioTrackThread() {int channelConfig = AudioFormat.CHANNEL_IN_STEREO;/*** 设置音频信息属性* 1.设置支持多媒体属性,比如audio,video* 2.设置音频格式,比如 music*/AudioAttributes attributes = new AudioAttributes.Builder().setUsage(AudioAttributes.USAGE_MEDIA).setContentType(AudioAttributes.CONTENT_TYPE_MUSIC).build();/*** 设置音频哥特式* 1. 设置采样率* 2. 设置采样位数* 3. 设置声道*/AudioFormat format = new AudioFormat.Builder().setSampleRate(AUDIO_RATE).setEncoding(AudioFormat.ENCODING_PCM_16BIT).setChannelMask(channelConfig).build();//拿到一帧的最小buffer大小bufferSize = AudioTrack.getMinBufferSize(AUDIO_RATE, channelConfig, AudioFormat.ENCODING_PCM_16BIT);audioTrack = new AudioTrack(attributes,format,bufferSize,AudioTrack.MODE_STREAM,AudioManager.AUDIO_SESSION_ID_GENERATE);//播放,等待数据audioTrack.play();}

就是初始化 AudioTrack 时,由于是流模式,所以大小只需要设置一帧的最小buffer 即可,然后调用 play() 方法去等待数据,当AudioTrack 的 write() 有数据到来时,就会播放音频:

        @Overridepublic void run() {super.run();File file = new File(PATH, "test.pcm");if (file.exists()) {FileInputStream fis = null;try {fis = new FileInputStream(file);byte[] buffer = new byte[bufferSize];int len;while (!isDone && (len = fis.read(buffer)) > 0) {// 写数据到 AudioTrack中,等到播放audioTrack.write(buffer, 0, len);}audioTrack.stop();audioTrack.release();} catch (Exception e) {e.printStackTrace();Log.d(TAG, "zsr run: " + e);} finally {CloseUtils.close(fis);}}}

这样,关于 AudioRecord 和 AudioTrack 就学习完啦,后面继续打怪升级。

参考:
https://developer.android.google.cn/reference/kotlin/android/media/AudioTrack?hl=en
https://www.jianshu.com/p/1749d2d43ecb

Android 音视频开发(一) -- 使用AudioRecord 录制PCM(录音);AudioTrack播放音频相关推荐

  1. 音视频开发系列(27)AudioRecord录制PCM音频

    目录 音频采集API AudioRecord和MediaRecorder介绍 PCM的介绍 AudioRecord的使用(构造.开始录制.停止录制.其他细节点) ffplay播放pcm pcm转为wa ...

  2. Android 音视频开发(一):PCM 格式音频的播放与采集

    什么是 PCM 格式 声音从模拟信号转化为数字信号的技术,经过采样.量化.编码三个过程将模拟信号数字化. 采样 顾名思义,对模拟信号采集样本,该过程是从时间上对信号进行数字化,例如每秒采集 44100 ...

  3. Android 音视频开发(三) -- Camera2 实现预览、拍照功能

    音视频 系列文章 Android 音视频开发(一) – 使用AudioRecord 录制PCM(录音):AudioTrack播放音频 Android 音视频开发(二) – Camera1 实现预览.拍 ...

  4. Android 音视频开发(二) -- Camera1 实现预览、拍照功能

    音视频 系列文章 Android 音视频开发(一) – 使用AudioRecord 录制PCM(录音):AudioTrack播放音频 Android 音视频开发(二) – Camera1 实现预览.拍 ...

  5. Android 音视频开发(六) -- Android Mediaprojection 截屏和录屏

    Android 音视频开发(一) – 使用AudioRecord 录制PCM(录音):AudioTrack播放音频 Android 音视频开发(二) – Camera1 实现预览.拍照功能 Andro ...

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

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

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

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

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

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

  9. 那些年,Android音视频开发那些事儿

    音视频开发的主要应用有哪些? 音频播放器,录音机,语音电话,音视频监控应用,音视频直播应用,音频编辑/处理软件,蓝牙耳机/音箱,等等 1.视频监控类 (JNI+应用层开发) 从硬件到嵌入式再到软件,涉 ...

最新文章

  1. 每天一点Linux --- 目录的可执行权限
  2. nodejs TCP服务器和客户端通信的socket结构
  3. prometheus连续查询_Prometheus 不完全避坑指南
  4. 简述python的编程规范_python编程规范
  5. Opencv——霍夫变换以及遇到的一些问题
  6. 最简单的基于FFmpeg的移动端样例:IOS 视频转码器
  7. 琴生不等式一般形式_001.二次函数、方程和不等式知识点
  8. python 日历查询系统_python 日历
  9. 29. JavaScript - 测试 jQuery
  10. oc55--ARC单个对象的内存管理
  11. 算法设计思维导图(算法设计与分析第二版)
  12. 机房计算机配置思维导图,运用思维导图培养高中学生信息技术学科核心素养
  13. 免费的文字转语音朗读 -API接口
  14. 短文本分类---小白从0到0.3的辛酸历程(上)
  15. 金山词霸2009牛津with SP3完全破解版(含全部本地词库和语音包)
  16. matlab求梯度的原理,matlab 梯度计算原理
  17. Service Mesh Summit 服务网格峰会 2022 正在报名中
  18. ZOJ 3886 Nico Number (线段树)
  19. [Excel] excel随机填充内容/文本/数字
  20. win10录屏软件哪款比较好用?一款不限时长的录屏软件

热门文章

  1. Git中SSH公钥配置
  2. poj 3682 Bookshelf 2——背包问题(dfs)
  3. Django专题二:模型
  4. 【逆元】【bzoj 3823】: 定情信物
  5. 高考报 AI 专业?南大周志华:当然!清华孙茂松:再考虑一下
  6. 目标跟踪 ATOM(ATOM: Accurate Tracking by Overlap Maximization)
  7. 【C语言内功】 LInkList L的辨析
  8. mybatis foreach标签拼接多字段in ,和union
  9. Win11 下蓝牙设备突然消失,不能正常使用解决方法。
  10. 车辆轨迹跟踪画出小车