- win7
- Android Studio 3.0.1
本文目的:使用 AudioRecord 和 AudioTrack 完成音頻PCM數(shù)據(jù)的采集和播放,并讀寫音頻wav文件
本文鏈接 - Android音頻PCM數(shù)據(jù)的采集和播放,讀寫音頻wav文件
準(zhǔn)備工作
Android提供了AudioRecord和MediaRecord。MediaRecord可選擇錄音的格式。
AudioRecord得到PCM編碼格式的數(shù)據(jù)。AudioRecord能夠設(shè)置模擬信號(hào)轉(zhuǎn)化為數(shù)字信號(hào)的相關(guān)參數(shù),包括采樣率和量化深度,同時(shí)也包括通道數(shù)目等。
PCM
PCM是在由模擬信號(hào)向數(shù)字信號(hào)轉(zhuǎn)化的一種常用的編碼格式,稱為脈沖編碼調(diào)制,PCM將模擬信號(hào)按照一定的間距劃分為多段,然后通過二進(jìn)制去量化每一個(gè)間距的強(qiáng)度。
PCM表示的是音頻文件中隨著時(shí)間的流逝的一段音頻的振幅。Android在WAV文件中支持PCM的音頻數(shù)據(jù)。
WAV
WAV,MP3等比較常見的音頻格式,不同的編碼格式對(duì)應(yīng)不通過的原始音頻。為了方便傳輸,通常會(huì)壓縮原始音頻。
為了辨別出音頻格式,每種格式有特定的頭文件(header)。
WAV以RIFF為標(biāo)準(zhǔn)。RIFF是一種資源交換檔案標(biāo)準(zhǔn)。RIFF將文件存儲(chǔ)在每一個(gè)標(biāo)記塊中。
基本構(gòu)成單位是trunk,每個(gè)trunk由標(biāo)記位,數(shù)據(jù)大小,數(shù)據(jù)存儲(chǔ),三個(gè)部分構(gòu)成。
PCM打包成WAV
PCM是原始音頻數(shù)據(jù),WAV是windows中常見的音頻格式,只是在pcm數(shù)據(jù)中添加了一個(gè)文件頭。
| 起始地址 | 占用空間 | 本地址數(shù)字的含義 |
|---|---|---|
| 00H | 4byte | RIFF,資源交換文件標(biāo)志。 |
| 04H | 4byte | 從下一個(gè)地址開始到文件尾的總字節(jié)數(shù)。高位字節(jié)在后面,這里就是001437ECH,換成十進(jìn)制是1325036byte,算上這之前的8byte就正好1325044byte了。 |
| 08H | 4byte | WAVE,代表wav文件格式。 |
| 0CH | 4byte | FMT ,波形格式標(biāo)志 |
| 10H | 4byte | 00000010H,16PCM,我的理解是用16bit的數(shù)據(jù)表示一個(gè)量化結(jié)果。 |
| 14H | 2byte | 為1時(shí)表示線性PCM編碼,大于1時(shí)表示有壓縮的編碼。這里是0001H。 |
| 16H | 2byte | 1為單聲道,2為雙聲道,這里是0001H。 |
| 18H | 4byte | 采樣頻率,這里是00002B11H,也就是11025Hz。 |
| 1CH | 4byte | Byte率=采樣頻率*音頻通道數(shù)*每次采樣得到的樣本位數(shù)/8,00005622H,也就是22050Byte/s=11025*1*16/2
|
| 20H | 2byte | 塊對(duì)齊=通道數(shù)*每次采樣得到的樣本位數(shù)/8,0002H,也就是 2 == 1*16/8
|
| 22H | 2byte | 樣本數(shù)據(jù)位數(shù),0010H即16,一個(gè)量化樣本占2byte。 |
| 24H | 4byte | data,一個(gè)標(biāo)志而已。 |
| 28H | 4byte | Wav文件實(shí)際音頻數(shù)據(jù)所占的大小,這里是001437C8H即1325000,再加上2CH就正好是1325044,整個(gè)文件的大小。 |
| 2CH | 不定 | 量化數(shù)據(jù) |
AudioRecord
AudioRecord可實(shí)習(xí)從音頻輸入設(shè)備記錄聲音的功能。得到PCM格式的音頻。
讀取音頻的方法有read(byte[], int, int), read(short[], int, int) 或 read(ByteBuffer, int)。
可根據(jù)存儲(chǔ)方式和需求選擇使用這項(xiàng)方法。
需要權(quán)限<uses-permission android:name="android.permission.RECORD_AUDIO" />
AudioRecord 構(gòu)造函數(shù)
public AudioRecord(int audioSource, int sampleRateInHz, int channelConfig, int audioFormat, int bufferSizeInBytes)
- audioSource 音源設(shè)備,常用麥克風(fēng)
MediaRecorder.AudioSource.MIC - samplerateInHz 采樣頻率,44100Hz是目前所有設(shè)備都支持的頻率
- channelConfig 音頻通道,單聲道還是立體聲
- audioFormat 該參數(shù)為量化深度,即為每次采樣的位數(shù)
- bufferSizeInBytes 可通過
getMinBufferSize()方法確定,每次從硬件讀取數(shù)據(jù)所需要的緩沖區(qū)的大小。
獲取wav文件
若要獲得wav文件,需要在PCM基礎(chǔ)上增加一個(gè)header。可以將PCM文件轉(zhuǎn)換成wav,這里提供一種PCM與wav幾乎同時(shí)生成的思路。
PCM與wav同時(shí)創(chuàng)建,給wav文件一個(gè)默認(rèn)的header。錄制線程啟動(dòng)后,同時(shí)寫PCM與wav。
錄制完成時(shí),重新生成header,利用RandomAccessFile修改wav文件的header。
AudioTrack
使用AudioTrack播放音頻。初始化AudioTrack時(shí),要根據(jù)錄制時(shí)的參數(shù)進(jìn)行設(shè)定。
代碼示例
工具類WindEar實(shí)現(xiàn)音頻PCM數(shù)據(jù)的采集和播放,與讀寫音頻wav文件的功能。
-
AudioRecordThread使用AudioRecord錄制PCM文件,可選擇同時(shí)生成wav文件 -
AudioTrackPlayThread使用AudioTrack播放PCM或wav音頻文件的線程 -
WindState表示當(dāng)前狀態(tài),例如是否在播放,錄制等等
PCM文件的讀寫采用FileOutputStream和FileInputStream
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 數(shù)據(jù)的采集和播放,并實(shí)現(xiàn)讀寫音頻 wav 文件
* 檢查權(quán)限,檢查麥克風(fēng)的工作放在Activity中進(jìn)行
* 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; // 當(dāng)前狀態(tài)
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) {
// 存儲(chǔ)在App內(nèi)或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, "無法開始錄制,當(dāng)前狀態(tài)為 " + 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(); // 錄制結(jié)束
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, "錄制結(jié)束");
}
}
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);
}
/**
* 表示當(dāng)前狀態(tài)
*/
public enum WindState {
ERROR,
IDLE,
RECORDING,
STOP_RECORD,
PLAYING,
STOP_PLAY
}
/**
* @param out wav音頻文件流
* @param totalAudioLen 不包括header的音頻數(shù)據(jù)總長(zhǎng)度
* @param longSampleRate 采樣率,也就是錄制時(shí)使用的頻率
* @param channels audioRecord的頻道數(shù)量
* @throws IOException 寫文件錯(cuò)誤
*/
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);
}
/**
* 任何一種文件在頭部添加相應(yīng)的頭文件才能夠確定的表示這種文件的格式,
* wave是RIFF文件結(jié)構(gòu),每一部分為一個(gè)chunk,其中有RIFF WAVE chunk,
* FMT Chunk,F(xiàn)act chunk,Data chunk,其中Fact chunk是可以選擇的
*
* @param pcmAudioByteCount 不包括header的音頻數(shù)據(jù)總長(zhǎng)度
* @param longSampleRate 采樣率,也就是錄制時(shí)使用的頻率
* @param channels audioRecord的頻道數(shù)量
*/
private byte[] generateWavFileHeader(long pcmAudioByteCount, long longSampleRate, int channels) {
long totalDataLen = pcmAudioByteCount + 36; // 不包含前8個(gè)字節(jié)的WAV文件總長(zhǎng)度
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);//數(shù)據(jù)大小
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] = ' ';//過渡字節(jié)
//數(shù)據(jù)大小
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;
//通道數(shù)
header[22] = (byte) channels;
header[23] = 0;
//采樣率,每個(gè)通道的播放速度
header[24] = (byte) (longSampleRate & 0xff);
header[25] = (byte) ((longSampleRate >> 8) & 0xff);
header[26] = (byte) ((longSampleRate >> 16) & 0xff);
header[27] = (byte) ((longSampleRate >> 24) & 0xff);
//音頻數(shù)據(jù)傳送速率,采樣率*通道數(shù)*采樣深度/8
header[28] = (byte) (byteRate & 0xff);
header[29] = (byte) ((byteRate >> 8) & 0xff);
header[30] = (byte) ((byteRate >> 16) & 0xff);
header[31] = (byte) ((byteRate >> 24) & 0xff);
// 確定系統(tǒng)一次要處理多少個(gè)這樣字節(jié)的數(shù)據(jù),確定緩沖區(qū),通道數(shù)*采樣位數(shù)
header[32] = (byte) (2 * channels);
header[33] = 0;
//每個(gè)樣本的數(shù)據(jù)位數(shù)
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;
}
}