天天看點

音頻PCM資料的采集和播放

在 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位元組。

音頻PCM資料的采集和播放

摘自維基百科

起始位址 區塊名稱 區塊大小 端序 區塊内容 備注
區塊編号 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上的位址

疑惑

  1. 采樣位數是如何擷取的?
  • javascript:void(0)