Android AudioRecord和AudioTrack實現音頻PCM數據的采集和播放,并讀寫音頻wav文件

  • win7
  • Android Studio 3.0.1

本文目的:使用 AudioRecord 和 AudioTrack 完成音頻PCM數據的采集和播放,并讀寫音頻wav文件

本文鏈接 - Android音頻PCM數據的采集和播放,讀寫音頻wav文件

準備工作

Android提供了AudioRecord和MediaRecord。MediaRecord可選擇錄音的格式。
AudioRecord得到PCM編碼格式的數據。AudioRecord能夠設置模擬信號轉化為數字信號的相關參數,包括采樣率和量化深度,同時也包括通道數目等。

PCM

PCM是在由模擬信號向數字信號轉化的一種常用的編碼格式,稱為脈沖編碼調制,PCM將模擬信號按照一定的間距劃分為多段,然后通過二進制去量化每一個間距的強度。
PCM表示的是音頻文件中隨著時間的流逝的一段音頻的振幅。Android在WAV文件中支持PCM的音頻數據。

WAV

WAV,MP3等比較常見的音頻格式,不同的編碼格式對應不通過的原始音頻。為了方便傳輸,通常會壓縮原始音頻。
為了辨別出音頻格式,每種格式有特定的頭文件(header)。
WAV以RIFF為標準。RIFF是一種資源交換檔案標準。RIFF將文件存儲在每一個標記塊中。
基本構成單位是trunk,每個trunk由標記位,數據大小,數據存儲,三個部分構成。

PCM打包成WAV

PCM是原始音頻數據,WAV是windows中常見的音頻格式,只是在pcm數據中添加了一個文件頭。

起始地址 占用空間 本地址數字的含義
00H 4byte RIFF,資源交換文件標志。
04H 4byte 從下一個地址開始到文件尾的總字節數。高位字節在后面,這里就是001437ECH,換成十進制是1325036byte,算上這之前的8byte就正好1325044byte了。
08H 4byte WAVE,代表wav文件格式。
0CH 4byte FMT ,波形格式標志
10H 4byte 00000010H,16PCM,我的理解是用16bit的數據表示一個量化結果。
14H 2byte 為1時表示線性PCM編碼,大于1時表示有壓縮的編碼。這里是0001H。
16H 2byte 1為單聲道,2為雙聲道,這里是0001H。
18H 4byte 采樣頻率,這里是00002B11H,也就是11025Hz。
1CH 4byte Byte率=采樣頻率*音頻通道數*每次采樣得到的樣本位數/8,00005622H,也就是22050Byte/s=11025*1*16/2
20H 2byte 塊對齊=通道數*每次采樣得到的樣本位數/8,0002H,也就是 2 == 1*16/8
22H 2byte 樣本數據位數,0010H即16,一個量化樣本占2byte。
24H 4byte data,一個標志而已。
28H 4byte Wav文件實際音頻數據所占的大小,這里是001437C8H即1325000,再加上2CH就正好是1325044,整個文件的大小。
2CH 不定 量化數據

AudioRecord

AudioRecord可實習從音頻輸入設備記錄聲音的功能。得到PCM格式的音頻。
讀取音頻的方法有read(byte[], int, int)read(short[], int, int)read(ByteBuffer, int)
可根據存儲方式和需求選擇使用這項方法。

需要權限<uses-permission android:name="android.permission.RECORD_AUDIO" />

AudioRecord 構造函數

public AudioRecord(int audioSource, int sampleRateInHz, int channelConfig, int audioFormat, int bufferSizeInBytes)

  • audioSource 音源設備,常用麥克風MediaRecorder.AudioSource.MIC
  • samplerateInHz 采樣頻率,44100Hz是目前所有設備都支持的頻率
  • channelConfig 音頻通道,單聲道還是立體聲
  • audioFormat 該參數為量化深度,即為每次采樣的位數
  • bufferSizeInBytes 可通過getMinBufferSize()方法確定,每次從硬件讀取數據所需要的緩沖區的大小。
獲取wav文件

若要獲得wav文件,需要在PCM基礎上增加一個header。可以將PCM文件轉換成wav,這里提供一種PCM與wav幾乎同時生成的思路。

PCM與wav同時創建,給wav文件一個默認的header。錄制線程啟動后,同時寫PCM與wav。
錄制完成時,重新生成header,利用RandomAccessFile修改wav文件的header。

AudioTrack

使用AudioTrack播放音頻。初始化AudioTrack時,要根據錄制時的參數進行設定。

代碼示例

工具類WindEar實現音頻PCM數據的采集和播放,與讀寫音頻wav文件的功能。

  • AudioRecordThread 使用AudioRecord錄制PCM文件,可選擇同時生成wav文件
  • AudioTrackPlayThread 使用AudioTrack播放PCM或wav音頻文件的線程
  • WindState 表示當前狀態,例如是否在播放,錄制等等

PCM文件的讀寫采用FileOutputStreamFileInputStream

generateWavFileHeader方法可以生成wav文件的header

import android.content.Context;
import android.media.AudioFormat;
import android.media.AudioManager;
import android.media.AudioRecord;
import android.media.AudioTrack;
import android.media.MediaRecorder;
import android.os.Environment;
import android.os.Handler;
import android.os.Looper;
import android.util.Log;

import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.RandomAccessFile;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.Locale;

/**
 * 音頻錄制器
 * 使用 AudioRecord 和 AudioTrack API 完成音頻 PCM 數據的采集和播放,并實現讀寫音頻 wav 文件
 * 檢查權限,檢查麥克風的工作放在Activity中進行
 * Created by Rust on 2018/2/24.
 */
public class WindEar {
    private static final String TAG = "rustApp";
    private static final String TMP_FOLDER_NAME = "AnWindEar";
    private static final int RECORD_AUDIO_BUFFER_TIMES = 1;
    private static final int PLAY_AUDIO_BUFFER_TIMES = 1;
    private static final int AUDIO_FREQUENCY = 44100;

    private static final int RECORD_CHANNEL_CONFIG = AudioFormat.CHANNEL_IN_STEREO;
    private static final int PLAY_CHANNEL_CONFIG = AudioFormat.CHANNEL_OUT_STEREO;
    private static final int AUDIO_ENCODING = AudioFormat.ENCODING_PCM_16BIT;

    private AudioRecordThread aRecordThread;           // 錄制線程
    private volatile WindState state = WindState.IDLE; // 當前狀態
    private File tmpPCMFile = null;
    private File tmpWavFile = null;
    private OnState onStateListener;
    private Handler mainHandler = new Handler(Looper.getMainLooper());

    /**
     * PCM緩存目錄
     */
    private static String cachePCMFolder;

    /**
     * wav緩存目錄
     */
    private static String wavFolderPath;

    private static WindEar instance = new WindEar();

    private WindEar() {

    }

    public static WindEar getInstance() {
        if (null == instance) {
            instance = new WindEar();
        }
        return instance;
    }

    public void setOnStateListener(OnState onStateListener) {
        this.onStateListener = onStateListener;
    }

    /**
     * 初始化目錄
     */
    public static void init(Context context) {
        // 存儲在App內或SD卡上
//        cachePCMFolder = context.getFilesDir().getAbsolutePath() + File.separator + TMP_FOLDER_NAME;
        cachePCMFolder = Environment.getExternalStorageDirectory().getAbsolutePath() + File.separator
                + TMP_FOLDER_NAME;

        File folder = new File(cachePCMFolder);
        if (!folder.exists()) {
            boolean f = folder.mkdirs();
            Log.d(TAG, String.format(Locale.CHINA, "PCM目錄:%s -> %b", cachePCMFolder, f));
        } else {
            for (File f : folder.listFiles()) {
                boolean d = f.delete();
                Log.d(TAG, String.format(Locale.CHINA, "刪除PCM文件:%s %b", f.getName(), d));
            }
            Log.d(TAG, String.format(Locale.CHINA, "PCM目錄:%s", cachePCMFolder));
        }

        wavFolderPath = Environment.getExternalStorageDirectory().getAbsolutePath() + File.separator
                + TMP_FOLDER_NAME;
//        wavFolderPath = context.getFilesDir().getAbsolutePath() + File.separator + TMP_FOLDER_NAME;
        File wavDir = new File(wavFolderPath);
        if (!wavDir.exists()) {
            boolean w = wavDir.mkdirs();
            Log.d(TAG, String.format(Locale.CHINA, "wav目錄:%s -> %b", wavFolderPath, w));
        } else {
            Log.d(TAG, String.format(Locale.CHINA, "wav目錄:%s", wavFolderPath));
        }
    }

    /**
     * 開始錄制音頻
     */
    public synchronized void startRecord(boolean createWav) {
        if (!state.equals(WindState.IDLE)) {
            Log.w(TAG, "無法開始錄制,當前狀態為 " + state);
            return;
        }
        try {
            tmpPCMFile = File.createTempFile("recording", ".pcm", new File(cachePCMFolder));
            if (createWav) {
                SimpleDateFormat sdf = new SimpleDateFormat("yyMMdd_HHmmss", Locale.CHINA);
                tmpWavFile = new File(wavFolderPath + File.separator + "r" + sdf.format(new Date()) + ".wav");
            }
            Log.d(TAG, "tmp file " + tmpPCMFile.getName());
        } catch (IOException e) {
            e.printStackTrace();
        }
        if (null != aRecordThread) {
            aRecordThread.interrupt();
            aRecordThread = null;
        }
        aRecordThread = new AudioRecordThread(createWav);
        aRecordThread.start();
    }

    public synchronized void stopRecord() {
        if (!state.equals(WindState.RECORDING)) {
            return;
        }
        state = WindState.STOP_RECORD;
        notifyState(state);
    }

    /**
     * 播放錄制好的PCM文件
     */
    public synchronized void startPlayPCM() {
        if (!isIdle()) {
            return;
        }
        new AudioTrackPlayThread(tmpPCMFile).start();
    }

    /**
     * 播放錄制好的wav文件
     */
    public synchronized void startPlayWav() {
        if (!isIdle()) {
            return;
        }
        new AudioTrackPlayThread(tmpWavFile).start();
    }

    public synchronized void stopPlay() {
        if (!state.equals(WindState.PLAYING)) {
            return;
        }
        state = WindState.STOP_PLAY;
    }

    public synchronized boolean isIdle() {
        return WindState.IDLE.equals(state);
    }

    /**
     * 音頻錄制線程
     * 使用FileOutputStream來寫文件
     */
    private class AudioRecordThread extends Thread {
        AudioRecord aRecord;
        int bufferSize = 10240;
        boolean createWav = false;

        AudioRecordThread(boolean createWav) {
            this.createWav = createWav;
            bufferSize = AudioRecord.getMinBufferSize(AUDIO_FREQUENCY,
                    RECORD_CHANNEL_CONFIG, AUDIO_ENCODING) * RECORD_AUDIO_BUFFER_TIMES;
            Log.d(TAG, "record buffer size = " + bufferSize);
            aRecord = new AudioRecord(MediaRecorder.AudioSource.MIC, AUDIO_FREQUENCY,
                    RECORD_CHANNEL_CONFIG, AUDIO_ENCODING, bufferSize);
        }

        @Override
        public void run() {
            state = WindState.RECORDING;
            notifyState(state);
            Log.d(TAG, "錄制開始");
            try {
                // 這里選擇FileOutputStream而不是DataOutputStream
                FileOutputStream pcmFos = new FileOutputStream(tmpPCMFile);

                FileOutputStream wavFos = new FileOutputStream(tmpWavFile);
                if (createWav) {
                    byte[] zeroHeader = new byte[44]; // 占位置
                    wavFos.write(zeroHeader);
                }
                aRecord.startRecording();
                byte[] byteBuffer = new byte[bufferSize];
                while (state.equals(WindState.RECORDING) && !isInterrupted()) {
                    int end = aRecord.read(byteBuffer, 0, byteBuffer.length);
                    pcmFos.write(byteBuffer, 0, end);
                    pcmFos.flush();
                    if (createWav) {
                        wavFos.write(byteBuffer, 0, end);
                        wavFos.flush();
                    }
                }
                aRecord.stop(); // 錄制結束
                pcmFos.close();
                wavFos.close();
                if (createWav) {
                    // 修改header
                    RandomAccessFile wavRaf = new RandomAccessFile(tmpWavFile, "rw");
                    byte[] header = generateWavFileHeader(tmpPCMFile.length() - 44, AUDIO_FREQUENCY, aRecord.getChannelCount());
                    Log.d(TAG, "header: " + getHexString(header));
                    wavRaf.seek(0);
                    wavRaf.write(header);
                    wavRaf.close();
                    Log.d(TAG, "tmpWavFile.length: " + tmpWavFile.length());
                }
                Log.i(TAG, "audio tmp PCM file len: " + tmpPCMFile.length());
            } catch (Exception e) {
                Log.e(TAG, "AudioRecordThread:", e);
                notifyState(WindState.ERROR);
            }
            notifyState(state);
            state = WindState.IDLE;
            notifyState(state);
            Log.d(TAG, "錄制結束");
        }

    }

    private static String getHexString(byte[] bytes) {
        StringBuilder sb = new StringBuilder();
        for (byte b : bytes) {
            sb.append(Integer.toHexString(b)).append(",");
        }
        return sb.toString();
    }

    /**
     * AudioTrack播放音頻線程
     * 使用FileInputStream讀取文件
     */
    private class AudioTrackPlayThread extends Thread {
        AudioTrack track;
        int bufferSize = 10240;
        File audioFile = null;

        AudioTrackPlayThread(File aFile) {
            setPriority(Thread.MAX_PRIORITY);
            audioFile = aFile;
            int bufferSize = AudioTrack.getMinBufferSize(AUDIO_FREQUENCY,
                    PLAY_CHANNEL_CONFIG, AUDIO_ENCODING) * PLAY_AUDIO_BUFFER_TIMES;
            track = new AudioTrack(AudioManager.STREAM_MUSIC,
                    AUDIO_FREQUENCY,
                    PLAY_CHANNEL_CONFIG, AUDIO_ENCODING, bufferSize,
                    AudioTrack.MODE_STREAM);
        }

        @Override
        public void run() {
            super.run();
            state = WindState.PLAYING;
            notifyState(state);
            try {
                FileInputStream fis = new FileInputStream(audioFile);
                track.play();
                byte[] aByteBuffer = new byte[bufferSize];
                while (state.equals(WindState.PLAYING) &&
                        fis.read(aByteBuffer) >= 0) {
                    track.write(aByteBuffer, 0, aByteBuffer.length);
                }
                track.stop();
                track.release();
            } catch (Exception e) {
                Log.e(TAG, "AudioTrackPlayThread:", e);
                notifyState(WindState.ERROR);
            }
            state = WindState.STOP_PLAY;
            notifyState(state);
            state = WindState.IDLE;
            notifyState(state);
        }

    }

    private synchronized void notifyState(final WindState currentState) {
        if (null != onStateListener) {
            mainHandler.post(new Runnable() {
                @Override
                public void run() {
                    onStateListener.onStateChanged(currentState);
                }
            });
        }
    }

    public interface OnState {
        void onStateChanged(WindState currentState);
    }

    /**
     * 表示當前狀態
     */
    public enum WindState {
        ERROR,
        IDLE,
        RECORDING,
        STOP_RECORD,
        PLAYING,
        STOP_PLAY
    }

    /**
     * @param out            wav音頻文件流
     * @param totalAudioLen  不包括header的音頻數據總長度
     * @param longSampleRate 采樣率,也就是錄制時使用的頻率
     * @param channels       audioRecord的頻道數量
     * @throws IOException 寫文件錯誤
     */
    private void writeWavFileHeader(FileOutputStream out, long totalAudioLen, long longSampleRate,
                                    int channels) throws IOException {
        byte[] header = generateWavFileHeader(totalAudioLen, longSampleRate, channels);
        out.write(header, 0, header.length);
    }

    /**
     * 任何一種文件在頭部添加相應的頭文件才能夠確定的表示這種文件的格式,
     * wave是RIFF文件結構,每一部分為一個chunk,其中有RIFF WAVE chunk,
     * FMT Chunk,Fact chunk,Data chunk,其中Fact chunk是可以選擇的
     *
     * @param pcmAudioByteCount 不包括header的音頻數據總長度
     * @param longSampleRate    采樣率,也就是錄制時使用的頻率
     * @param channels          audioRecord的頻道數量
     */
    private byte[] generateWavFileHeader(long pcmAudioByteCount, long longSampleRate, int channels) {
        long totalDataLen = pcmAudioByteCount + 36; // 不包含前8個字節的WAV文件總長度
        long byteRate = longSampleRate * 2 * channels;
        byte[] header = new byte[44];
        header[0] = 'R'; // RIFF
        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';//WAVE
        header[9] = 'A';
        header[10] = 'V';
        header[11] = 'E';
        //FMT Chunk
        header[12] = 'f'; // 'fmt '
        header[13] = 'm';
        header[14] = 't';
        header[15] = ' ';//過渡字節
        //數據大小
        header[16] = 16; // 4 bytes: size of 'fmt ' chunk
        header[17] = 0;
        header[18] = 0;
        header[19] = 0;
        //編碼方式 10H為PCM編碼格式
        header[20] = 1; // format = 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);
        //音頻數據傳送速率,采樣率*通道數*采樣深度/8
        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 * channels);
        header[33] = 0;
        //每個樣本的數據位數
        header[34] = 16;
        header[35] = 0;
        //Data chunk
        header[36] = 'd';//data
        header[37] = 'a';
        header[38] = 't';
        header[39] = 'a';
        header[40] = (byte) (pcmAudioByteCount & 0xff);
        header[41] = (byte) ((pcmAudioByteCount >> 8) & 0xff);
        header[42] = (byte) ((pcmAudioByteCount >> 16) & 0xff);
        header[43] = (byte) ((pcmAudioByteCount >> 24) & 0xff);
        return header;
    }
}

參考資料

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容

  • 幾個小時前看電視,大概晚上九點多吧,偶然調到北京臺在播一個為女人重塑形象的節目《哎呦,你真美!》,就多看了兩眼,吸...
    三棵豎閱讀 554評論 3 4
  • 每當爭吵煩悶的源頭關系到錢時,我只是怪自己太無能。 希望你快樂得無暇顧及流言蜚語。
    Joann喵閱讀 124評論 0 0
  • 今天趙雷又發新歌——《無法長大》,深情而纏綿的唱法像是最浪漫的告白。 評論里看到,趙雷在一次音樂節的時候說希望他的...
    無法聚焦閱讀 908評論 1 5