前幾篇的文章都是camera下采集視訊資料進行顯示,儲存下來的檔案也是h264格式的,並沒有包含音訊資料,所以多多少少有點單調的感覺。沒有聲音的視訊是沒有靈魂的,所以最近了解了一下音訊相關的開發,給視訊注入靈魂。
1. 基礎知識
開始音訊學習之前,有必要先了解一下基礎知識,因為在音訊開發過程中,經常會涉及到這些。掌握了這些重要的概念,在學習中很多引數的配置會更容易理解。
-
PCM編碼格式
首先看看百度百科給出的解釋:PCM 脈衝編碼調製是Pulse Code Modulation的縮寫。脈衝編碼調製是數字通訊的編碼方式之一。主要過程是將話音、影象等模擬訊號每隔一定時間進行取樣,使其離散化,同時將抽樣值按分層單位四捨五入取整量化,同時將抽樣值按一組二進位制碼來表示抽樣脈衝的幅值。 這個解釋得非常抽象,反正我是沒看懂⊙﹏⊙。簡單的來說就是將聲音數字化,轉換為二進位制序列,這樣就可以把聲音儲存下來了,而儲存它的容器可以是mp3,wav等等容器。
-
音訊採集輸入源
這個就相當於宣告孩子他媽是誰,也就是說聲音的源頭在哪兒。可選的型別以常量的形式定義在 MediaRecorder.AudioSource 類中 ,比較常用的是下面幾個
- MediaRecorder.AudioSource.CAMCORDER 設定錄音來源於同方向的相機麥克風相同,若相機無內建相機或無法識別,則使用預設的麥克風
- MediaRecorder.AudioSource.DEFAULT 預設音訊源
- MediaRecorder.AudioSource.MIC 設定錄音來源為主麥克風
- MediaRecorder.AudioSource.VOICE_CALL設定錄音來源為語音撥出的語音與對方說話的聲音
- MediaRecorder.AudioSource.VOICE_COMMUNICATION 攝像頭旁邊的麥克風
- MediaRecorder.AudioSource.VOICE_RECOGNITION 語音識別
-
取樣率
我們把取樣到的一個個靜止畫面再以取樣率同樣的速度回放時,看到的就是連續的畫面。同樣的道理,把以44.1kHZ取樣率記錄的CD以同樣的速率播放時,就能聽到連續的聲音。顯然,這個取樣率越高,聽到的聲音和看到的影象就越連貫。當然,人的聽覺和視覺器官能分辨的取樣率是有限的,基本上高於44.1kHZ取樣的聲音,絕大部分人已經覺察不到其中的分別了。 而目前44100Hz是唯一可以保證相容所有Android手機的取樣率 。所以,如果不是特殊裝置和用途,這個值建議設定為44100。
-
通道數
聲道數一般表示聲音錄製時的音源數量或回放時相應的揚聲器數量。常用的有:單通道和雙通道。 可選的值以常量的形式定義在 AudioFormat 類中,常用的是 CHANNEL_IN_MONO(單通道),CHANNEL_IN_STEREO(雙通道)
-
量化精度
- 聲音的位數就相當於畫面的顏色數,表示每個取樣的資料量,當然資料量越大,回放的聲音越準確,不至於把開水壺的叫聲和火車的鳴笛混淆。同樣的道理,對於畫面來說就是更清晰和準確,不至於把血和西紅柿醬混淆。不過受人的器官的機能限制,16位的聲音和24位的畫面基本已經是普通人類的極限了,電話就是3kHZ取樣的7位聲音,而CD是44.1kHZ取樣的16位聲音,所以CD就比電話更清楚。
- 對於一個取樣點,需要用二進位制數字來表示,這個二進位制的精度可以是:4bit、8bit、16bit、32bit。 位數越多,表示的聲音就越精細,聲音的質量就越好。不過資料量也會變大。
-
幀間隔
音訊不像視訊那樣,有一幀一幀的概念。它是約定一個時間為單位,然後這個時間內的資料為一幀,這個時間被稱為取樣時間。這個時間沒有特別的標準,要看具體的編解碼器。
2. 音訊錄製
瞭解音訊開發相關基礎知識之後,我們就可以開始使用android提供的相關API實現音訊的錄製了,android提供了兩套音訊錄製的API:
MediaRecorder
:比較上層的API
,它可以直接把手機麥克風的音訊資料進行編碼然後儲存成檔案。使用簡單,但是支援的格式有限,並且不支援對音訊進行進一步的處理,例如變聲、混音等。AudioRecord
:比較底層的一個API
,能夠得到原始的PCM
音訊資料。由於我們得到的是原始的PCM
資料,我們可以對音訊進行進一步的處理,例如編碼、混音和變聲等。
這裡主要介紹的是使用AudioRecord
進行錄製,MediaRecorder
的錄製比較簡單,就不做過多介紹了。首先看看AudioRecord的建構函式:
public AudioRecord(int audioSource, int sampleRateInHz, int channelConfig, int audioFormat, int bufferSizeInBytes)
複製程式碼
需要傳入5個引數,分別是輸入源,取樣率,通道數,量化精度,音訊緩衝區的大小 。其中最後一個也是最重要的一個引數,它代表音訊緩衝區的大小,該緩衝區的值不能低於一幀音訊幀的大小,
一幀音訊幀大小 = 取樣率 x 位寬 x 取樣時間 x 通道數 ,這個值不用我們自己計算,AudioRecord 類提供了一個幫助你確定這個值的函式 :
public static int getMinBufferSize(int sampleRateInHz, int channelConfig, int audioFormat)
複製程式碼
當建立好AudioRecord物件之後,就可以開始進行音訊資料的採集了,控制採集的開始和停止的方法是下面這兩個函式:
public void startRecording()
public void stop()
複製程式碼
開始採集之後,通過執行緒迴圈取走音訊資料:
public int read(byte[] audioData, int offsetInBytes, int sizeInBytes)
複製程式碼
最終錄製的程式碼如下,注意該方法需要在子執行緒中執行:
private File mAudioFile;
private FileOutputStream mAudioFileOutput;
private boolean isRecording = false;
private int sampleRate = 44100;//所有android系統都支援 取樣率
//單聲道輸入
private int channelConfig = AudioFormat.CHANNEL_IN_MONO;
//PCM_16是所有android系統都支援的 16位的聲音就是人類能聽到的極限了,再高就聽不見了 位數越高聲音越清晰
private int autioFormat = AudioFormat.ENCODING_PCM_16BIT;
private int recordBufSize = 0; // 宣告recoordBufffer的大小欄位
private AudioRecord audioRecord = null;
private boolean startAudioRecord(String fileName) {
isRecording = true;
mAudioFile = new File(mPath+fileName+System.currentTimeMillis()+".pcm");
if (!mAudioFile.getParentFile().exists()){
mAudioFile.getParentFile().mkdirs();
}
try {
mAudioFile.createNewFile();
//建立檔案輸出流
mAudioFileOutput = new FileOutputStream(mAudioFile);
//計算audioRecord能接受的最小的buffer大小
recordBufSize = AudioRecord.getMinBufferSize(sampleRate,
channelConfig,
autioFormat);
Log.e(TAG, "最小的buffer大小: " + recordBufSize);
audioRecord = new AudioRecord(MediaRecorder.AudioSource.MIC,
sampleRate,
channelConfig,
autioFormat, recordBufSize);
//初始化一個buffer 用於從AudioRecord中讀取聲音資料到檔案流
byte data[] = new byte[recordBufSize];
//開始錄音
audioRecord.startRecording();
while (isRecording){
//只要還在錄音就一直讀取
int read = audioRecord.read(data,0,recordBufSize);
if (read > 0){
mAudioFileOutput.write(data,0,read);
}
}
//stopRecorder();
} catch (IOException e) {
e.printStackTrace();
stopAudioRecord();
return false;
}
return true;
}
public boolean stopAudioRecord(){
isRecording = false;
if (audioRecord != null){
audioRecord.stop();
audioRecord.release();
audioRecord = null;
}
try {
mAudioFileOutput.flush();
mAudioFileOutput.close();
} catch (IOException e) {
e.printStackTrace();
return false;
}
return true;
}
複製程式碼
3. 音訊播放
播放pcm格式的音訊和錄製不同的地方有三個:
一個是建立AudioTrack物件,和AudioRecord的構造方法不同,第一個引數是音樂型別,AudioManager.STREAM_MUSIC表示用揚聲器播放,最後多出的一個引數是播放模式,一般使用AudioTrack.MODE_STREAM,適用於大多數的場景,將audio buffers從java層傳遞到native層即返回。 如果audio buffers佔用記憶體多,應該使用MODE_STREAM。 比如播放時間很長的聲音檔案, 比如音訊檔案使用高取樣率等:
public AudioTrack(int streamType, int sampleRateInHz, int channelConfig, int audioFormat, int bufferSizeInBytes, int mode)
複製程式碼
另外兩個是把錄製的startRecording()和read()方法換成下面兩個:
public void play()
public int write(byte[] audioData, int offsetInBytes, int sizeInBytes)
複製程式碼
最終播放pcm的程式碼如下:
private FileInputStream mAudioPlayInputStream;
public void doPlay(File audioFile) {
if (audioFile.isDirectory() || !audioFile.exists()) return;
mIsPlaying = true;
//配置播放器
//音樂型別,揚聲器播放
int streamType= AudioManager.STREAM_MUSIC;
//錄音時採用的取樣頻率,所以播放時同樣的取樣頻率
int sampleRate=44100;
//單聲道,和錄音時設定的一樣
int channelConfig=AudioFormat.CHANNEL_OUT_MONO;
//錄音時使用16bit,所以播放時同樣採用該方式
int audioFormat=AudioFormat.ENCODING_PCM_16BIT;
//流模式
int mode= AudioTrack.MODE_STREAM;
//計算最小buffer大小
int minBufferSize = AudioTrack.getMinBufferSize(sampleRate,channelConfig,audioFormat);
byte data[] = new byte[minBufferSize];
//構造AudioTrack 不能小於AudioTrack的最低要求,也不能小於我們每次讀的大小
mAudioTrack = new AudioTrack(streamType,sampleRate,channelConfig,audioFormat,
Math.max(minBufferSize,data.length),mode);
//從檔案流讀資料
try{
//迴圈讀資料,寫到播放器去播放
mAudioPlayInputStream = new FileInputStream(audioFile);
//迴圈讀資料,寫到播放器去播放
int read;
//只要沒讀完,迴圈播放
mAudioTrack.play();
while (mIsPlaying){
int ret = 0;
read = mAudioPlayInputStream.read(data);
if (read > 0){
ret = mAudioTrack.write(data,0,read);
}
//mAudioFileOutput.write(data,0,read);
//檢查write的返回值,處理錯誤
switch (ret){
case AudioTrack.ERROR_INVALID_OPERATION:
case AudioTrack.ERROR_BAD_VALUE:
case AudioManager.ERROR_DEAD_OBJECT:
Log.d(TAG, "doPlay: 失敗,錯誤碼:"+ret);
return;
default:
break;
}
}
}catch (Exception e){
e.printStackTrace();
//讀取失敗
Log.d(TAG, "doPlay: 失敗");
}finally {
stopPlay();
Log.d(TAG, "結束播放");
}
}
public void stopPlay() {
mIsPlaying = false;
//播放器釋放
if (mAudioTrack != null) {
mAudioTrack.stop();
mAudioTrack.release();
mAudioTrack = null;
}
//關閉檔案輸入流
if (mAudioPlayInputStream != null) {
try {
mAudioPlayInputStream.close();
} catch (IOException ex) {
ex.printStackTrace();
}
}
}
複製程式碼
最後貼上所有的程式碼:
public class AudioUtil {
private static final String TAG = "AudioUtil";
private AudioRecord audioRecord = null; // 宣告 AudioRecord 物件
private int recordBufSize = 0; // 宣告recoordBufffer的大小欄位
//所有android系統都支援 取樣率:取樣率越高,聽到的聲音和看到的影象就越連貫
// 基本上高於44.1kHZ取樣的聲音,絕大部分人已經覺察不到其中的分別了
private int sampleRate = 44100;
//單聲道輸入
private int channelConfig = AudioFormat.CHANNEL_IN_MONO;
//PCM_16是所有android系統都支援的 16位的聲音就是人類能聽到的極限了,再高就聽不見了 位數越高聲音越清晰
private int autioFormat = AudioFormat.ENCODING_PCM_16BIT;
private long mStartTimeStamp;
private File mAudioFile;
private String mPath = ContentValue.MAIN_PATH + "/AudioSimple/";
private FileOutputStream mAudioFileOutput; //儲存錄音檔案
private FileInputStream mAudioPlayInputStream; //播放錄音檔案
private boolean isRecording = false;
private static AudioUtil audioUtil;
private boolean mIsPlaying;
private AudioTrack mAudioTrack;
private String mRecordFileName; //錄音儲存的檔案路徑
private String mPlayFileName; //播放錄音的檔案路徑
private Runnable mAudioRunnableTask = new Runnable() {
@Override
public void run() {
boolean result = startAudioRecord(mRecordFileName);
if (result){
Log.e(TAG, "錄音結束");
}else {
Log.e(TAG, "錄音失敗");
}
}
};
private Runnable mAudioPlayRunnableTask = new Runnable() {
@Override
public void run() {
File file = new File(mPlayFileName);
doPlay(file);
}
};
private AudioUtil(){}
public static AudioUtil getInstance(){
if (audioUtil == null){
synchronized (AudioUtil.class){
if (audioUtil == null){
audioUtil = new AudioUtil();
}
}
}
return audioUtil;
}
private AudioEncoder mAudioEncoder;
private boolean startAudioRecord(String fileName) {
isRecording = true;
mStartTimeStamp = System.currentTimeMillis();
mAudioFile = new File(mPath+fileName+mStartTimeStamp+".pcm");
if (!mAudioFile.getParentFile().exists()){
mAudioFile.getParentFile().mkdirs();
}
try {
mAudioFile.createNewFile();
//建立檔案輸出流
mAudioFileOutput = new FileOutputStream(mAudioFile);
//計算audioRecord能接受的最小的buffer大小
recordBufSize = AudioRecord.getMinBufferSize(sampleRate,
channelConfig,
autioFormat);
Log.e(TAG, "最小的buffer大小: " + recordBufSize);
audioRecord = new AudioRecord(MediaRecorder.AudioSource.MIC,
sampleRate,
channelConfig,
autioFormat, recordBufSize);
//初始化一個buffer 用於從AudioRecord中讀取聲音資料到檔案流
byte data[] = new byte[recordBufSize];
//開始錄音
audioRecord.startRecording();
while (isRecording){
//只要還在錄音就一直讀取
int read = audioRecord.read(data,0,recordBufSize);
if (read > 0){
mAudioFileOutput.write(data,0,read);
}
}
//stopRecorder();
} catch (IOException e) {
e.printStackTrace();
stopAudioRecord();
return false;
}
return true;
}
public void doPlay(File audioFile) {
if(audioFile !=null){
mIsPlaying = true;
//配置播放器
//音樂型別,揚聲器播放
int streamType= AudioManager.STREAM_MUSIC;
//錄音時採用的取樣頻率,所以播放時同樣的取樣頻率
int sampleRate=44100;
//單聲道,和錄音時設定的一樣
int channelConfig=AudioFormat.CHANNEL_OUT_MONO;
//錄音時使用16bit,所以播放時同樣採用該方式
int audioFormat=AudioFormat.ENCODING_PCM_16BIT;
//流模式
int mode= AudioTrack.MODE_STREAM;
//計算最小buffer大小
int minBufferSize = AudioTrack.getMinBufferSize(sampleRate,channelConfig,audioFormat);
byte data[] = new byte[minBufferSize];
//構造AudioTrack 不能小於AudioTrack的最低要求,也不能小於我們每次讀的大小
mAudioTrack = new AudioTrack(streamType,sampleRate,channelConfig,audioFormat,
Math.max(minBufferSize,data.length),mode);
//從檔案流讀資料
try{
//迴圈讀資料,寫到播放器去播放
mAudioPlayInputStream = new FileInputStream(audioFile);
//迴圈讀資料,寫到播放器去播放
int read;
//只要沒讀完,迴圈播放
mAudioTrack.play();
while (mIsPlaying){
int ret = 0;
read = mAudioPlayInputStream.read(data);
if (read > 0){
ret = mAudioTrack.write(data,0,read);
}
//mAudioFileOutput.write(data,0,read);
//檢查write的返回值,處理錯誤
switch (ret){
case AudioTrack.ERROR_INVALID_OPERATION:
case AudioTrack.ERROR_BAD_VALUE:
case AudioManager.ERROR_DEAD_OBJECT:
Log.d(TAG, "doPlay: 失敗,錯誤碼:"+ret);
return;
default:
break;
}
}
}catch (Exception e){
e.printStackTrace();
//讀取失敗
Log.d(TAG, "doPlay: 失敗");
}finally {
stopPlay();
Log.d(TAG, "結束播放");
}
}
}
public void startRecord(String fileName){
this.mRecordFileName = fileName;
new Thread(mAudioRunnableTask).start();
}
public void startPlay(String fileName){
this.mPlayFileName = fileName;
new Thread(mAudioPlayRunnableTask).start();
}
public boolean stopAudioRecord(){
isRecording = false;
if (audioRecord != null){
audioRecord.stop();
audioRecord.release();
audioRecord = null;
}
if (mAudioEncoder != null){
mAudioEncoder.stopEncodeAac();
}
try {
mAudioFileOutput.flush();
mAudioFileOutput.close();
} catch (IOException e) {
e.printStackTrace();
return false;
}
return true;
}
public boolean isRecording(){
return isRecording;
}
public boolean isPlaying(){
return mIsPlaying;
}
public void stopPlay(){
mIsPlaying = false;
//播放器釋放
if(mAudioTrack != null){
mAudioTrack.stop();
mAudioTrack.release();
mAudioTrack = null;
}
//關閉檔案輸入流
if(mAudioPlayInputStream !=null){
try {
mAudioPlayInputStream.close();
} catch (IOException ex) {
ex.printStackTrace();
}
}
}
private AudioListener mAudioListener;
public void setAudioListener(AudioListener listener){
this.mAudioListener = listener;
}
public interface AudioListener{
void onRecordFinish();
void onPlayFinish();
}
}
複製程式碼