在具体使用 AudioRecord 采集音频之前,简单了解下 PCM。
什么是 PCM?
我们知道,声音本身是模拟信号,而计算机只能处理离散的数字信号,要在计算机中处理声音,就需要将声音数字化,这个过程叫模数转换(A/D 变换) 。模数转换直接生成的二进制序列称为 PCM(pulse code modulation, 脉冲编码调制)数据。它是数字音频在计算机、光盘、数字电话和其他数字音频应用中的标准形式。
要将模拟信号转为 PCM 时,需要将声音量化,我们一般从如下几个维度描述一段声音:
采样频率:每秒钟采集声音样本的次数,它用赫兹(Hz)来表示。
采样频率越高,声音的质量也就越好,声音的还原也就越真实,但同时它占的资源比较多。由于人耳的分辨率很有限,太高的频率并不能分辨出来。在 16 位声卡中有 22KHz、44KHz 等几级,,其中,22KHz 相当于普通 FM 广播的音质,44KHz 已相当于 CD 音质了,目前的常用采样频率都不超过 48KHz。
采样位数:表示每次采样的精度,也可以说是声卡的分辨率。位数越多,能记录的范围就越大。
声道数:很好理解,有单声道和立体声之分,单声道的声音只能使用一个喇叭发声(有的也处理成两个喇叭输出同一个声道的声音),立体声的 PCM 可以使两个喇叭都发声(一般左右声道有分工) ,更能感受到空间效果。
时长:采样的时长

使用 AudioRecord 采集音频
AudioRecord 类是 Android 系统提供的用于实现录音的功能类。
开始录音的时候,AudioRecord 需要初始化一个相关联的声音 buffer, 这个 buffer 主要是用来保存新的声音数据。这个 buffer 的大小,我们可以在对象构造期间去指定。它表明一个 AudioRecord 对象还没有被读取(同步)声音数据前能录多长的音(即一次可以录制的声音容量)。声音数据从音频硬件中被读出,数据大小不超过整个录音数据的大小(可以分多次读出),即每次读取初始化 buffer 容量的数据。
使用 AudioRecord 采集音频的一般步骤:
初始化一个音频缓存大小,该缓存大于等于 AudioRecord 对象用于写声音数据的缓存大小,最小录音缓存可以通过AudioRecord#getMinBufferSize()
方法得到。
构造一个 AudioRecord 对象,需要传入缓冲区大小,如果缓存容量过小,将导致对象构造的失败。
开始录音
创建一个数据流,一边从 AudioRecord 中读取声音数据到初始化的缓存,一边将缓存中数据导入数据流。
关闭数据流
停止录音
初始化缓存大小
可以通过 AudioRecord#getMinBufferSize()
方法得到最小录音缓存大小,传入的参数依次是采样频率、声道数和采样位数。
1 2
| private int mBufferSizeInBytes; mBufferSizeInBytes = AudioRecord.getMinBufferSize(sampleRateInHz, channelConfig, audioFormat);
|
构造 AudioRecord 对象
1 2
| private AudioRecord mAudioRecord; mAudioRecord = new AudioRecord(audioSource, sampleRateInHz, channelConfig, audioFormat, mBufferSizeInBytes);
|
初始化一个 buffer 数组
1
| byte[] audioData = new byte[mBufferSizeInBytes];
|
开始录音
1
| mAudioRecord.startRecording();
|
数据流读写
创建一个数据流,一边从 AudioRecord 中读取声音数据到初始化的 buffer,一边将 buffer 中数据导入数据流。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38
|
private void writeAudioDataToFile() throws IOException { String pcmFilePath = FileUtil.getPcmFilePath(mContext, mPcmFileName); File file = new File(pcmFilePath); if (file.exists()) { file.delete(); } OutputStream bos = null; try { bos = new BufferedOutputStream(new FileOutputStream(file)); byte[] audioData = new byte[mBufferSizeInBytes]; while (mStatus == Status.STATUS_START) { int readSize = mAudioRecord.read(audioData, 0, mBufferSizeInBytes); if (readSize > 0) { try { bos.write(audioData, 0, readSize); if (mRecordStreamListener != null) { mRecordStreamListener.onRecording(audioData, 0, readSize); } } catch (IOException e) { Log.e(TAG, "writeAudioDataToFile", e); } } else { Log.w(TAG, "writeAudioDataToFile readSize: " + readSize); } } bos.flush(); if (mRecordStreamListener != null) { mRecordStreamListener.finishRecord(); } } finally { if (bos != null) { bos.close(); } } }
|
停止录音
1 2 3 4 5
| if (null != mAudioRecord) { mAudioRecord.stop(); mAudioRecord.release(); mAudioRecord = null; }
|
Android SDK 提供了两套音频采集的 API,分别是:MediaRecorder 和 AudioRecord,前者是一个更加上层一点的API,它可以直接把手机麦克风录入的音频数据进行编码压缩(如 AMR、MP3 等)并存成文件,而后者则更接近底层,能够更加自由灵活地控制,可以得到原始的一帧帧 PCM 音频数据。
如果想简单地做一个录音机,录制成音频文件,则推荐使用 MediaRecorder,而如果需要对音频做进一步的算法处理、或者采用第三方的编码库进行压缩、以及网络传输等应用,则建议使用 AudioRecord,其实 MediaRecorder 底层也是调用了 AudioRecord 与 Android Framework 层的 AudioFlinger 进行交互的。直播中实时采集音频自然是要用 AudioRecord 了。
使用 AudioTrack 播放 PCM 音频
AudioTrack 类可以完成 Android 平台 PCM 数据流的播放工作。AudioTrack 有两种数据加载模式:MODE_STREAM 和 MODE_STATIC, 对应着两种完全不同的使用场景。
MODE_STREAM:在这种模式下,通过 write 一次次把音频数据写到 AudioTrack 中。这和平时通过 write 调用往文件中写数据类似,但这种方式每次都需要把数据从用户提供的 Buffer 中拷贝到 AudioTrack 内部的 Buffer 中,在一定程度上会引起延时。为解决这一问题,AudioTrack 就引入了第二种模式。
MODE_STATIC:在这种模式下,只需要在 play 之前通过一次 write 调用,把所有数据传递到 AudioTrack 中的内部缓冲区,后续就不必再传递数据了。这种模式适用于像铃声这种内存占用较小、延时要求较高的文件。但它也有一个缺点,就是一次 write 的数据不能太多,否则系统无法分配足够的内存来存储全部数据。
创建 AudioTrack 播放对象
参数与创建 AudioRecord 有相似之处。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| int channelConfig = AudioFormat.CHANNEL_OUT_MONO; final int minBufferSize = AudioTrack.getMinBufferSize(AUDIO_SAMPLE_RATE_INHZ, channelConfig, AUDIO_ENCODING); mAudioTrack = new AudioTrack( new AudioAttributes.Builder() .setUsage(AudioAttributes.USAGE_MEDIA) .setContentType(AudioAttributes.CONTENT_TYPE_MUSIC) .build(), new AudioFormat.Builder() .setSampleRate(AUDIO_SAMPLE_RATE_INHZ) .setEncoding(AUDIO_ENCODING) .setChannelMask(channelConfig) .build(), minBufferSize, AudioTrack.MODE_STREAM, AudioManager.AUDIO_SESSION_ID_GENERATE);
|
开始播放
MODE_STREAM 模式
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
| private void playAudioData() throws IOException { InputStream dis = null; try { ThreadHelper.getInstance().runOnUiThread(() -> { Toast.makeText(mContext, "播放开始", Toast.LENGTH_SHORT).show(); }); dis = new DataInputStream(new BufferedInputStream(new FileInputStream(mFilePath))); byte[] bytes = new byte[mBufferSizeInBytes]; int length; mAudioTrack.play(); while ((length = dis.read(bytes)) != -1 && mStatus == Status.STATUS_START) { mAudioTrack.write(bytes, 0, length); } ThreadHelper.getInstance().runOnUiThread(() -> { Toast.makeText(mContext, "播放结束", Toast.LENGTH_SHORT).show(); }); } finally { if (dis != null) { dis.close(); } } }
|
MODE_STATIC 模式
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| try { InputStream in = getResources().openRawResource(R.raw.ding); try { ByteArrayOutputStream out = new ByteArrayOutputStream(); for (int b; (b = in.read()) != -1; ) { out.write(b); } mAudioData = out.toByteArray(); } finally { in.close(); } } catch (IOException e) { Log.e(TAG, "Failed to read", e); }
mAudioTrack.write(mAudioData, 0, mAudioData.length); mAudioTrack.play();
|
停止播放
1 2 3 4 5
| if (mAudioTrack != null) { mAudioTrack.stop(); mAudioTrack.release(); mAudioTrack = null; }
|
Android 中播放声音可以用 MediaPlayer 和 AudioTrack,两者都提供了 Java API 供应用开发者使用。
虽然都可以播放声音,但两者还是有很大的区别的。
其中最大的区别是 MediaPlayer 可以播放多种格式的声音文件,例如 MP3,AAC,WAV,OGG,MIDI 等。MediaPlayer 会在 framework 层创建对应的音频解码器,而 AudioTrack 只能播放已经解码的 PCM 流,如果对比支持的文件格式的话则是 AudioTrack 只支持 wav 格式的音频文件,因为 wav 格式的音频文件大部分都是 PCM流。AudioTrack 不创建解码器,所以只能播放不需要解码的 wav 文件。
MediaPlayer 在 framework 层还是会创建 AudioTrack,把解码后的 PCM 数流传递给 AudioTrack,AudioTrack再传递给 AudioFlinger 进行混音,然后才传递给硬件播放,所以是 MediaPlayer 包含了 AudioTrack。
PCM 转 WAV
Waveform Audio File Format(WAVE,又或者是因为 WAV 后缀而被大众所知的),它采用 RIFF(Resource Interchange File Format)文件格式结构。通常用来保存 PCM 格式的原始音频数据,所以通常被称为无损音频。
WAV 和 PCM 的关系
PCM 数据本身只是一个裸码流,它是由声道、采样位数、采样频率、时长共同决定的,因此我们至少要知道其中的三个才能将 PCM 所代表的数据提取出来。
一种常见的方式是使用 WAV 格式定义的规范将 PCM 码流和描述信息封装起来。查看 PCM 和对应 WAV 文件的 hex 文件,可以发现,WAV 文件只是在 PCM 文件的开头多了 44bytes,来表征其声道数、采样频率和采样位数等信息。

PCM 转 WAV 的实现代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122
| public class PcmToWavUtil {
private int mBufferSize;
private int mSampleRate;
private int mChannel;
public PcmToWavUtil(int sampleRate, int channel, int encoding) { this.mSampleRate = sampleRate; this.mChannel = channel; this.mBufferSize = AudioRecord.getMinBufferSize(mSampleRate, mChannel, encoding); }
public void pcmToWav(String inFilename, String outFilename) { FileInputStream in; FileOutputStream out; long totalAudioLen; long totalDataLen; long longSampleRate = mSampleRate; int channels = mChannel == AudioFormat.CHANNEL_IN_MONO ? 1 : 2; long byteRate = 16 * mSampleRate * channels / 8; byte[] data = new byte[mBufferSize]; try { in = new FileInputStream(inFilename); out = new FileOutputStream(outFilename); totalAudioLen = in.getChannel().size(); totalDataLen = totalAudioLen + 36;
writeWaveFileHeader(out, totalAudioLen, totalDataLen, longSampleRate, channels, byteRate); while (in.read(data) != -1) { out.write(data); } in.close(); out.close(); } catch (IOException e) { e.printStackTrace(); } }
private void writeWaveFileHeader(FileOutputStream out, long totalAudioLen, long totalDataLen, long longSampleRate, int channels, long byteRate) throws IOException { byte[] header = new byte[44]; header[0] = 'R'; header[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'; header[9] = 'A'; header[10] = 'V'; header[11] = 'E'; header[12] = 'f'; header[13] = 'm'; header[14] = 't'; header[15] = ' '; header[16] = 16; header[17] = 0; header[18] = 0; header[19] = 0; header[20] = 1; header[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); header[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 * 16 / 8); header[33] = 0; header[34] = 16; header[35] = 0; header[36] = 'd'; header[37] = 'a'; header[38] = 't'; header[39] = 'a'; header[40] = (byte) (totalAudioLen & 0xff); header[41] = (byte) ((totalAudioLen >> 8) & 0xff); header[42] = (byte) ((totalAudioLen >> 16) & 0xff); header[43] = (byte) ((totalAudioLen >> 24) & 0xff); out.write(header, 0, 44); } }
|
具体源码已经放在 GitHub:AndroidMultiMediaLearning
参考资料