在 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位元組。
摘自維基百科
起始位址 | 區塊名稱 | 區塊大小 | 端序 | 區塊内容 | 備注 |
區塊編号 | 4 | 大 | “RIFF” |
| 總區塊大小 | | 小 | = N+36 | N:音頻資料的總位元組數;36:從下一個位址開始到頭檔案尾的總位元組數 |
8 | 檔案格式 | | | “WAVE” |
12 | 子區塊1辨別 | | | “fmt ” (最後有一個空格) |
16 | 子區塊1大小 | | | |
20 | 音頻格式 | 2 | | 1(PCM) |
22 | 聲道數量 | | | 1(單聲道)2(立體聲) |
24 | 取樣頻率(采用頻率) | | | 取樣點/秒(Hz) |
28 | 位元(組)率 | | | = 取樣頻率 * 位元深度 / 8 | Byte率 = 采樣頻率 音頻通道數 每次采樣得到的樣本位數 / 8 |
32 | 區塊對齊 | | | |
36 | 子區塊2辨別 | | | “data” |
40 | 子區塊2大小 | | | 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 播放音頻
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上的位址
疑惑
- 采樣位數是如何擷取的?