代码改变世界

音频PCM数据的采集和播放

2021-09-26 09:00  dreamboy2000  阅读(1190)  评论(0编辑  收藏  举报

在 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 <音频数据从此开始>

端序,即字节顺序

代码实现

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
// 音频数据的大小
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音频

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
/**
* 采样率,现在能够保证在所有设备上使用的采样率是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,因此允许使用子类,但不建议使用这种类型的子类.

使用 AudioTrack 播放音频

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
/**
* 播放,使用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<Void, Void, Void>() {
@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();

}

demo在github上的地址

疑惑

  1. 采样位数是如何获取的?

Thanks