在 Android 平台使用 AudioRecord 和 AudioTrack API 完成音频 PCM 数据的采集和播放,并实现读写音频 wav 文件。
音频基础知识
声道数(通道数)
即声音的通道的数目。很好理解,有单声道和立体声之分,单声道的声音只能使用一个喇叭发声(有的也处理成两个喇叭输出同一个声道的声音),立体声的PCM可以使两个喇叭都发声(一般左右声道有分工) ,更能感受到空间效果。
采样位数
即 采样值或取样值(就是将采样样本幅度量化)。它是用来衡量声音波动变化的一个参数,也可以说是声卡的分辨率。它的数值越大,分辨率也就越高,所发出声音的能力越强。
在计算机中采样位数一般有8位和16位之分,但有一点请大家注意,8位不是说把纵坐标分成8份,而是分成2的8次方即256份; 同理16位是把纵坐标分成2的16次方65536份。
采样频率
即取样频率,指 每秒钟取得声音样本的次数。采样频率越高,声音的质量也就越好,声音的还原也就越真实,但同时它占的资源比较多。由于人耳的分辨率很有限,太高的频率并不能分辨出来。在16位声卡中有22KHz、44KHz等几级,其中,22KHz相当于普通FM广播的音质,44KHz已相当于CD音质了,目前的常用采样频率都不超过48KHz。
既然知道了以上三个概念,就可以由下边的公式得出PCM文件所占容量:
存储量= (采样频率 * 采样位数 * 声道 * 时间)/8 (单位:字节数)。
PCM 介绍
目前我们在计算机上进行音频播放都需要依赖于音频文件,音频文件的生成过程是将声音信息采样、量化和编码产生的数字信号的过程,人耳所能听到的声音,最低的频率是从20Hz起一直到最高频率20KHZ,因此音频文件格式的最大带宽是20KHZ。根据奈奎斯特的理论,只有采样频率高于声音信号最高频率的两倍时,才能把数字信号表示的声音还原成为原来的声音,所以音频文件的采样率一般在40~50KHZ,比如最常见的CD音质采样率44.1KHZ。
对声音进行采样、量化过程被称为脉冲编码调制(Pulse Code Modulation),简称PCM。PCM数据是最原始的音频数据完全无损,所以PCM数据虽然音质优秀但体积庞大,为了解决这个问题先后诞生了一系列的音频格式,这些音频格式运用不同的方法对音频数据进行压缩,其中有无损压缩(ALAC、APE、FLAC)和有损压缩(MP3、AAC、OGG、WMA)两种。
WAV
Waveform Audio File Format(WAVE,又或者是因为扩展名而被大众所知的WAV),是微软与IBM公司所开发在个人电脑存储音频流的编码格式,在Windows平台的应用软件受到广泛的支持,地位上类似于麦金塔电脑里的AIFF。 此格式属于资源交换档案格式(RIFF)的应用之一,通常会将采用脉冲编码调制的音频资存储在区块中。也是其音乐发烧友中常用的指定规格之一。由于此音频格式未经过压缩,所以在音质方面不会出现失真的情况,但档案的体积因而在众多音频格式中较为大。
所有的WAV都有一个文件头,这个文件头音频流的编码参数。WAV对音频流的编码没有硬性规定,除了PCM之外,还有几乎所有支持ACM规范的编码都可以为WAV的音频流进行编码。WAV也可以使用多种音频编码来压缩其音频流,不过我们常见的都是音频流被PCM编码处理的WAV,但这不表示WAV只能使用PCM编码,MP3编码同样也可以运用在WAV中,和AVI一样,只要安装好了相应的Decode,就可以欣赏这些WAV了。
在Windows平台下,基于PCM编码的WAV是被支持得最好的音频格式,所有音频软件都能完美支持,由于本身可以达到较高的音质的要求,因此,WAV也是音乐编辑创作的首选格式,适合保存音乐素材。因此,基于PCM编码的WAV被作为了一种中介的格式,常常使用在其他编码的相互转换之中,例如MP3转换成WMA。
wav文件格式
在文件的前44字节放置标头(header),使播放器或编辑器能够简单掌握文件的基本信息,其内容以区块(chunk)为最小单位,每一区块长度为4字节。
摘自维基百科
起始位址 | 区块名称 | 区块大小 | 端序 | 区块内容 | 备注 |
---|---|---|---|---|---|
0 | 区块编号 | 4 | 大 | “RIFF” | |
4 | 总区块大小 | 4 | 小 | = N+36 | N:音频数据的总字节数;36:从下一个地址开始到头文件尾的总字节数 |
8 | 档案格式 | 4 | 大 | “WAVE” | |
12 | 子区块1标识 | 4 | 大 | “fmt ” (最后有一个空格) | |
16 | 子区块1大小 | 4 | 小 | 16 | |
20 | 音频格式 | 2 | 小 | 1(PCM) | |
22 | 声道数量 | 2 | 小 | 1(单声道)2(立体声) | |
24 | 取样频率(采用频率) | 4 | 小 | 取样点/秒(Hz) | |
28 | 位元(组)率 | 4 | 小 | = 取样频率 * 位元深度 / 8 | Byte率 = 采样频率 *音频通道数 *每次采样得到的样本位数 / 8 |
32 | 区块对齐 | 2 | 小 | 4 | |
36 | 子区块2标识 | 4 | 大 | “data” | |
40 | 子区块2大小 | 4 | 小 | N(=位元(组)*秒数 *声道数量) | 音频数据的大小 |
44 | 音频数据 | =N | 小 | <音频数据从此开始> |
端序,即字节顺序
// 音频数据的大小
long totalAudioLen = fileInputStream.getChannel().size();
// wav总区块大小
long totalDataLen = totalAudioLen + 36;
// 声道数量
int channels;
// 采样率
long longSampleRate;
// 位元率
long byteRate = 16 * longSampleRate * channels / 8;
byte[] header = new byte[44];
// RIFF/WAVE header
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);
//WAVE
header[8] = 'W';
header[9] = 'A';
header[10] = 'V';
header[11] = 'E';
// 'fmt ' chunk
header[12] = 'f';
header[13] = 'm';
header[14] = 't';
header[15] = ' ';
// 4 bytes: size of 'fmt ' chunk
header[16] = 16;
header[17] = 0;
header[18] = 0;
header[19] = 0;
// format = 1
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);
// block align
header[32] = (byte) (2 * 16 / 8);
header[33] = 0;
// bits per sample
header[34] = 16;
header[35] = 0;
//data
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);
使用AudioRecord录制pcm音频
/**
* 采样率,现在能够保证在所有设备上使用的采样率是44100Hz, 但是其他的采样率(22050, 16000, 11025)在一些设备上也可以使用。
*/
private static final int SAMPLE_RATE_INHZ = 44100;
/**
* 声道数。CHANNEL_IN_MONO and CHANNEL_IN_STEREO. 其中CHANNEL_IN_MONO是可以保证在所有设备能够使用的。
*/
private static final int CHANNEL_CONFIG = AudioFormat.CHANNEL_IN_MONO;
/**
* 返回的音频数据的格式。 ENCODING_PCM_8BIT, ENCODING_PCM_16BIT, and ENCODING_PCM_FLOAT.
*/
private static final int AUDIO_FORMAT = AudioFormat.ENCODING_PCM_16BIT;
final int minBufferSize = AudioRecord.getMinBufferSize(SAMPLE_RATE_INHZ, CHANNEL_CONFIG, AUDIO_FORMAT);
audioRecord = new AudioRecord(MediaRecorder.AudioSource.MIC, SAMPLE_RATE_INHZ,
CHANNEL_CONFIG, AUDIO_FORMAT, minBufferSize);
final byte data[] = new byte[minBufferSize];
final File file = new File(getExternalFilesDir(Environment.DIRECTORY_MUSIC), "test.pcm");
if (!file.mkdirs()) {
Log.e(TAG, "Directory not created");
}
if (file.exists()) {
file.delete();
}
audioRecord.startRecording();
isRecording = true;
new Thread(new Runnable() {
@Override public void run() {
FileOutputStream os = null;
try {
os = new FileOutputStream(file);
} catch (FileNotFoundException e) {
e.printStackTrace();
}
if (null != os) {
while (isRecording) {
int read = audioRecord.read(data, 0, minBufferSize);
// 如果读取音频数据没有出现错误,就将数据写入到文件
if (AudioRecord.ERROR_INVALID_OPERATION != read) {
try {
os.write(data);
} catch (IOException e) {
e.printStackTrace();
}
}
}
try {
Log.i(TAG, "run: close file output stream !");
os.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}).start();
PCM转WAV
只要加上wav头文件即可。
使用AudioTrack播放pcm音频
AudioTrack 类为java程序实现了控制和播放简单的音频。它允许将 PCM音频流传输到音频接收器进行播放。这是通过将音频数据推给 AudioTrack对象实现的,可以使用 write(byte[], int, int) , write(short[], int, int) 或 write(float[], int, int, int) 方法。
AudioTrack可以在两种模式下运行:static 或 streaming。
在Streaming模式下,应用程序使用其中一种write()方法将连续的数据流写入AudioTrack 。当数据从Java层传输到native层并排队等待播放时,它们会阻塞并返回。在播放音频数据块时,流模式非常有用,例如:
由于声音播放的持续时间太长而不能装入内存,
由于音频数据的特性(高采样率,每个样本的位数……)而不能装入内存
在先前排队的音频正在播放时接收或生成。
在处理能够装入内存的短音时,应选择静态模式,并且需要尽可能以最小的延迟播放。因此,对于经常播放的UI和游戏声音而言,静态模式将是优选的,并且可能具有最小的开销。
一旦创建,AudioTrack对象将初始化其关联的音频缓冲区。在构建过程中指定的这个缓冲区的大小决定了AudioTrack在耗尽数据之前可以播放多长时间。
对于使用静态模式的AudioTrack,此大小是可以从中播放的最大声音大小。
对于流模式,数据将以小于或等于总缓冲区大小的块形式写入音频接收器。AudioTrack不是 final,因此允许使用子类,但不建议使用这种类型的子类.
/**
* 播放,使用stream模式
*/
private void playInModeStream() {
/*
* SAMPLE_RATE_INHZ 对应pcm音频的采样率
* channelConfig 对应pcm音频的声道
* AUDIO_FORMAT 对应pcm音频的格式
* */
int channelConfig = AudioFormat.CHANNEL_OUT_MONO;
final int minBufferSize = AudioTrack.getMinBufferSize(SAMPLE_RATE_INHZ, channelConfig, AUDIO_FORMAT);
audioTrack = new AudioTrack(
new AudioAttributes.Builder()
.setUsage(AudioAttributes.USAGE_MEDIA)
.setContentType(AudioAttributes.CONTENT_TYPE_MUSIC)
.build(),
new AudioFormat.Builder().setSampleRate(SAMPLE_RATE_INHZ)
.setEncoding(AUDIO_FORMAT)
.setChannelMask(channelConfig)
.build(),
minBufferSize,
AudioTrack.MODE_STREAM,
AudioManager.AUDIO_SESSION_ID_GENERATE);
audioTrack.play();
File file = new File(getExternalFilesDir(Environment.DIRECTORY_MUSIC), "test.pcm");
try {
fileInputStream = new FileInputStream(file);
new Thread(new Runnable() {
@Override public void run() {
try {
byte[] tempBuffer = new byte[minBufferSize];
while (fileInputStream.available() > 0) {
int readCount = fileInputStream.read(tempBuffer);
if (readCount == AudioTrack.ERROR_INVALID_OPERATION ||
readCount == AudioTrack.ERROR_BAD_VALUE) {
continue;
}
if (readCount != 0 && readCount != -1) {
audioTrack.write(tempBuffer, 0, readCount);
}
}
} catch (IOException e) {
e.printStackTrace();
}
}
}).start();
} catch (IOException e) {
e.printStackTrace();
}
}
/**
* 播放,使用static模式
*/
private void playInModeStatic() {
// static模式,需要将音频数据一次性write到AudioTrack的内部缓冲区
new AsyncTask() {
@Override
protected Void doInBackground(Void... params) {
try {
InputStream in = getResources().openRawResource(R.raw.ding);
try {
ByteArrayOutputStream out = new ByteArrayOutputStream();
for (int b; (b = in.read()) != -1; ) {
out.write(b);
}
Log.d(TAG, "Got the data");
audioData = out.toByteArray();
} finally {
in.close();
}
} catch (IOException e) {
Log.wtf(TAG, "Failed to read", e);
}
return null;
}
@Override
protected void onPostExecute(Void v) {
Log.i(TAG, "Creating track...audioData.length = " + audioData.length);
// R.raw.ding铃声文件的相关属性为 22050Hz, 8-bit, Mono
audioTrack = new AudioTrack(
new AudioAttributes.Builder()
.setUsage(AudioAttributes.USAGE_MEDIA)
.setContentType(AudioAttributes.CONTENT_TYPE_MUSIC)
.build(),
new AudioFormat.Builder().setSampleRate(22050)
.setEncoding(AudioFormat.ENCODING_PCM_8BIT)
.setChannelMask(AudioFormat.CHANNEL_OUT_MONO)
.build(),
audioData.length,
AudioTrack.MODE_STATIC,
AudioManager.AUDIO_SESSION_ID_GENERATE);
Log.d(TAG, "Writing audio data...");
audioTrack.write(audioData, 0, audioData.length);
Log.d(TAG, "Starting playback");
audioTrack.play();
Log.d(TAG, "Playing");
}
}.execute();
}