安卓界面刷新24幀/s 每一幀是16ms 主線程16ms的執行限制 主線耗時操作導致16ms執行不完 導致卡頓問題
文件模式開啓錄音耗時20-30ms 定製錄音耗時30-50ms
字節流需要循環讀寫數據 必須再後臺線程
主線程和後臺線程狀態同步
後臺線程再循環中讀狀態值,主線程改變狀態值讓後臺線程退出。
不需要synchronized 互斥訪問
需要volatile保證主線程的修改後臺線程可見
避免錄音JNI函數閃退
JNI函數不能多線程調用
MediaRecorder : perpare() start() stop() reset() release()
AudioRecord : startRecording() read() stop() release()
以上都屬於JNI函數
語音參數
文件模式
//配置 MediaRecorder
//從麥克風採集
mediaRecorder.setAudioSource(MediaRecorder.AudioSource.MIC);
//保存文件爲mp4格式
mediaRecorder.setOutputFormat(MediaRecorder.OutputFormat.MPEG_4);
//採樣頻率
mediaRecorder.setAudioSamplingRate(44100);
//通用的AAC編碼格式
mediaRecorder.setAudioEncoder(MediaRecorder.AudioEncoder.AAC);
//音質比較好的編碼頻率
mediaRecorder.setAudioEncodingBitRate(96000);
//設置錄音文件的位置
mediaRecorder.setOutputFile(mAudioFile.getAbsolutePath());
setAudioSource
1.麥克風 MediaRecorder.AudioSource.MIC
2.語音識別 VOICE_RECOGNITION
3.語音通話 VOICE_COMMUNICATION 如果系統支持 迴音消除噪音抑制
setOutputFormat /setAudioEncoder
1.文件容器 MediaRecorder.OutputFormat.MPEG_4 文件頭信息
2.聲音編碼 MediaRecorder.AudioEncoder.AAC 裏面的數據編碼格式
setAudioSamplingRate
1.說話聲音是模擬信號 需要採樣爲數字信號(01)
2.採樣頻率越高(密集) 數據越大 音質越好
3.常用頻率 8kHz 11.025kHz 22.05kHz 16kHz 37.8kHz 44.1kHz 48kHz 96kHz 192kHz 其中44.1安卓手機都支持
setAudioEncodingBitRate
1.聲音編碼,碼率越大,壓縮越小,音質越好
2.AAC HE(High Efficiency) : 32kbps~96kbps 碼率越低 帶寬越小 音質一般
3.AAC LC(Low Complexity): 96kbps~192kbps 平衡低碼率 高音質
字節流模式
/**
* 配置AudioRecord
*/
int audioSource = MediaRecorder.AudioSource.MIC;
int sampleRate = 44100;
//單聲道輸入
int channelConfig = AudioFormat.CHANNEL_IN_MONO;
//PCM 16 是所有安卓支持
int audioFormat = AudioFormat.ENCODING_PCM_16BIT;
//獲取緩衝區 計算Audiorecord 內部buffer最小大小
int minBufferSize = AudioRecord.getMinBufferSize(sampleRate,channelConfig,audioFormat);
//創建AudioRecord對象 buffer 不能小於最低要求 也不能小於我們每次讀取的大小
mAudioRecord = new AudioRecord(audioSource, sampleRate, channelConfig, audioFormat
, Math.max(minBufferSize, BUFFER_SIZE));
channelConfig
1.音頻的採集和播放可以疊加
2.同時從多個音頻源採集,分別輸出到不同的揚聲器
3.單聲道(Mono)和雙聲道(Stereo)比較常見
audioFormat
1.量化精度:原始PCM數據,每個採樣點的數據大小
2.4bit ,8bit,16bit,32bit... 位數越多,音質越好,數據越大
3.常用16bit 兼容所有安卓手機
聲音播放
同樣需要注意線程切換問題,狀態值的原子性volatile。還有JNI函數調用對異常的處理。
文件模式
設置聲音文件
mMediaPlayer.setDataSource(audioFile.getAbsolutePath())
配置音量 是否循環
mMediaPlayer.setVolume(1,1); 0~1 的範圍
mMediaPlayer.setLooping(false); 支持循環播放
準備,開始
mMediaPlayer.prepare();
mMediaPlayer.start();
字節流模式
音樂類型 揚聲器播放
int streamType = AudioManager.STREAM_MUSIC
錄音時採用的採樣頻率,所以播放時使用同樣的採樣
int sampleRate = 44100
MONO表示單聲道,錄音輸入單聲道,播放用的時候保持一致
int channelConfig = Audioformat.CHANNEL_OUT_MONO
錄音時使用16bit數據位寬 所以播放的時候使用同樣的格式
int audioFormat = AudioFormat.ENCODING_PCM_16BIT
流模式
int mode =AudioTrack.MODE_STREAM
模式mode 就是java和native層數據傳輸模式
流模式:AudioTrack.MODE_STREAM
用流的形式一直從java層write寫入native層
靜態模式:AudioTrack.MODE_STATIC
在調用play之前一次性把數據寫到native層
文件模式和字節流模式的主要代碼
文件模式
package com.tencent.myapplication;
import androidx.appcompat.app.AppCompatActivity;
import android.annotation.SuppressLint;
import android.media.MediaPlayer;
import android.media.MediaRecorder;
import android.os.Bundle;
import android.os.Environment;
import android.os.Handler;
import android.os.Looper;
import android.util.Log;
import android.view.MotionEvent;
import android.view.View;
import android.widget.Button;
import android.widget.TextView;
import android.widget.Toast;
import java.io.File;
import java.io.IOException;
import java.util.concurrent.Executor;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class FileActivity extends AppCompatActivity {
private static final String TAG = FileActivity.class.getSimpleName();
private ExecutorService mExecutorService ;
private MediaRecorder mediaRecorder;
private File mAudioFile;
private long mStartRecordTime , mStopRecordTime;
private Handler mMainHandler ;
private TextView tv_file;
private Button btn_speech;
private Button btn_play;
//主線程和後臺播放線程數據同步 使用volatile
private volatile boolean mIsPlaying;
private MediaPlayer mMediaPlayer;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_file);
init();
}
@SuppressLint("ClickableViewAccessibility")
private void init() {
//主線程處理ui任務
mMainHandler = new Handler(Looper.getMainLooper());
//錄音jni 函數 不是線程安全的 所以使用單線程處理任務
mExecutorService = Executors.newSingleThreadExecutor();
tv_file = findViewById(R.id.tv_file);
btn_speech = findViewById(R.id.btn_speech);
btn_play = findViewById(R.id.btn_play);
tv_file.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
}
});
btn_play.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
//檢查當前狀態防止重複播放
if (mAudioFile != null && !mIsPlaying) {
//設置當前播放狀態
mIsPlaying = true;
//提交後臺任務 開始播放
mExecutorService.submit(new Runnable() {
@Override
public void run() {
doPlay(mAudioFile);
}
});
}
}
});
// btn_speech.setOnClickListener(new View.OnClickListener() {
// @Override
// public void onClick(View v) {
//
// }
// });
btn_speech.setOnTouchListener(new View.OnTouchListener() {
@Override
public boolean onTouch(View v, MotionEvent event) {
switch (event.getAction()){
case MotionEvent.ACTION_DOWN:
startRecord();
break;
case MotionEvent.ACTION_UP:
case MotionEvent.ACTION_CANCEL:
stopRecord();
break;
default:break;
}
return false;
}
});
}
/**
* 停止執行錄音邏輯
*/
private void stopRecord() {
//修改ui狀態
//提交後臺任務
mExecutorService.submit(new Runnable() {
@Override
public void run() {
//執行停止錄音邏輯 如果失敗
if (!doStop()){
recordFail();
}
//釋放recorder
releaseRecorder();
}
});
}
/**
* 停止執行錄音 執行失敗返回false
* @return
*/
private boolean doStop() {
try {
//停止錄音
mediaRecorder.stop();
//記錄停止時間,統計時長
mStopRecordTime = System.currentTimeMillis();
//只接受指定時長的錄音 這裏是超過3秒
int second = (int) ((mStopRecordTime - mStartRecordTime) / 1000);
if (second > 3 ){
//修改ui 在主線程執行
runOnUiThread(new Runnable() {
@SuppressLint("SetTextI18n")
@Override
public void run() {
tv_file.setText(tv_file.getText() + "\n錄音成功" + second +"秒");
}
});
// mMainHandler.post(new Runnable() {
// @Override
// public void run() {
//
// }
// });
}
} catch (RuntimeException e) {
e.printStackTrace();
//捕獲異常,避免閃退,返回false 提醒用戶失敗
return false;
}
//停止成功
return true;
}
/**
* 開始執行錄音邏輯
*
*/
private void startRecord() {
//修改ui狀態
//提交後臺任務
mExecutorService.submit(new Runnable() {
@Override
public void run() {
//釋放之前錄音的 recorder
releaseRecorder();
//執行錄音邏輯 如果失敗提示用戶
if (!doStart()){
recordFail();
}
}
});
}
/**
* 啓動錄音邏輯
* 判斷執行錄音是否成功 執行失敗返回false
* @return
*/
private boolean doStart() {
try {
//創建MediaRecorder
mediaRecorder = new MediaRecorder();
//創建錄音文件
mAudioFile = new File(Environment.getExternalStorageDirectory().getAbsolutePath() + "/AudioDemo/" + System.currentTimeMillis() + ".m4a");
mAudioFile.getParentFile().mkdirs();
mAudioFile.createNewFile();
Log.e(TAG, "doStart: mAudioFile ="+mAudioFile.getAbsolutePath());
//配置 MediaRecorder
//從麥克風採集
mediaRecorder.setAudioSource(MediaRecorder.AudioSource.MIC);
//保存文件爲mp4格式
mediaRecorder.setOutputFormat(MediaRecorder.OutputFormat.MPEG_4);
//採樣頻率
mediaRecorder.setAudioSamplingRate(44100);
//通用的AAC編碼格式
mediaRecorder.setAudioEncoder(MediaRecorder.AudioEncoder.AAC);
//音質比較好的編碼頻率
mediaRecorder.setAudioEncodingBitRate(96000);
//設置錄音文件的位置
mediaRecorder.setOutputFile(mAudioFile.getAbsolutePath());
//開始錄音
// prepare start 都會拋出IllegalStateException 所以catch中添加runtimeException
mediaRecorder.prepare();
mediaRecorder.start();
//記錄開始錄音的時間,用於統計時長
mStartRecordTime = System.currentTimeMillis();
} catch (IOException | RuntimeException e) {
e.printStackTrace();
//捕獲異常,避免閃退,返回false 提醒用戶失敗
return false;
}
//啓動成功
return true;
}
/**
* 錄音錯誤處理
* 執行失敗 提示用戶
*/
private void recordFail() {
mAudioFile = null;
//todo 這裏用handler 處理ui任務好 還是runOnUiThread 好???
runOnUiThread(new Runnable() {
@Override
public void run() {
Toast.makeText(FileActivity.this, "語音錄製失敗", Toast.LENGTH_SHORT).show();
}
});
// mMainHandler.post(new Runnable() {
// @Override
// public void run() {
// Toast.makeText(FileActivity.this, "語音錄製失敗", Toast.LENGTH_SHORT).show();
//
// }
// });
}
/**
* 釋放recorder
*/
private void releaseRecorder() {
//檢查MediaRecorder 不爲null
if (mediaRecorder != null){
mediaRecorder.release();
mediaRecorder = null;
}
}
@Override
protected void onDestroy() {
super.onDestroy();
//停止後臺任務 防止內存泄漏
mExecutorService.shutdownNow();
releaseRecorder();
stopPlay();
}
/**
* 實際播放的邏輯
* @param mAudioFile
*/
private void doPlay(File mAudioFile) {
//配置播放器 MediaPlayer
mMediaPlayer = new MediaPlayer();
try {
//設置聲音文件 告訴播放器播放什麼
mMediaPlayer.setDataSource(mAudioFile.getAbsolutePath());
//設置監聽回調 播放問題?
//完成播放的監聽配置
mMediaPlayer.setOnCompletionListener(new MediaPlayer.OnCompletionListener() {
@Override
public void onCompletion(MediaPlayer mp) {
//播放結束 釋放播放器
stopPlay();
}
});
//播放出錯的監聽配置
mMediaPlayer.setOnErrorListener(new MediaPlayer.OnErrorListener() {
@Override
public boolean onError(MediaPlayer mp, int what, int extra) {
//提示用戶
playFail();
//釋放播放器
stopPlay();
//錯誤已經處理返回true
return true;
}
});
//配置音量 是否循環
mMediaPlayer.setVolume(1,1);
mMediaPlayer.setLooping(false); //不循環
//準備 開始
mMediaPlayer.prepare();
mMediaPlayer.start();
}catch (RuntimeException | IOException e){
//異常處理 防止閃退
e.printStackTrace();
//提示用戶
playFail();
//釋放播放器
stopPlay();
}
}
/**
* 提醒用戶播放失敗
*/
private void playFail() {
//主線程 toast 提示
mMainHandler.post(new Runnable() {
@Override
public void run() {
Toast.makeText(FileActivity.this, "播放失敗", Toast.LENGTH_SHORT).show();
}
});
}
/**
* 停止播放的邏輯
*/
private void stopPlay() {
//重置播放狀態
mIsPlaying = false;
//釋放播放器
if (mMediaPlayer != null){
//重置監聽器 防止內存泄漏
mMediaPlayer.setOnCompletionListener( null);
mMediaPlayer.setOnErrorListener(null);
mMediaPlayer.stop();
mMediaPlayer.reset();
mMediaPlayer.release();
mMediaPlayer = null;
}
}
}
字節流模式
package com.tencent.myapplication;
import androidx.appcompat.app.AppCompatActivity;
import android.annotation.SuppressLint;
import android.media.AudioFormat;
import android.media.AudioManager;
import android.media.AudioRecord;
import android.media.AudioTrack;
import android.media.MediaRecorder;
import android.os.Bundle;
import android.os.Environment;
import android.os.Handler;
import android.os.Looper;
import android.util.Log;
import android.view.View;
import android.widget.Button;
import android.widget.TextView;
import android.widget.Toast;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class StreamActivity extends AppCompatActivity {
private static final String TAG = StreamActivity.class.getSimpleName();
private TextView tv_stream;
private Button btn_stream;
//錄音狀態 volatile 保證多線程內存同步
private volatile boolean mIsRecording;
//播放狀態
private volatile boolean mIsPlaying;
private ExecutorService mExecutorService;
private Handler mMainTheadHandler;
//buffer 不能太大 避免oom 2k
private static final int BUFFER_SIZE = 2048;
//讀取字節數據
private byte[] mBuffer;
private File mAudioFile;
private long mStartRecordTime,mStopRecordTime;
private FileOutputStream mFileOutputStream;
private AudioRecord mAudioRecord;
private Button btn_stream_speech;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_stream);
init();
}
@Override
protected void onDestroy() {
super.onDestroy();
mExecutorService.shutdownNow();
}
private void init() {
mBuffer = new byte[BUFFER_SIZE];
//錄音JNI函數 不具備線程安全性 所以用單線程
mExecutorService = Executors.newSingleThreadExecutor();
mMainTheadHandler = new Handler(Looper.getMainLooper());
btn_stream = findViewById(R.id.btn_stream);
tv_stream = findViewById(R.id.tv_stream);
btn_stream_speech = findViewById(R.id.btn_stream_speech);
btn_stream.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
//根據當前狀態,改變ui 執行開始或者定製錄音
if(mIsRecording){
//改變ui狀態
btn_stream.setText("開始");
//改變錄音狀態
mIsRecording = false;
// //提交後臺任務,執行停止邏輯
// mExecutorService.submit(new Runnable() {
// @Override
// public void run() {
// //執行開停止錄音邏輯 失敗提示用戶
// }
// });
}else{
//改變ui狀態
btn_stream.setText("停止");
//改變錄音狀態
mIsRecording = true;
//提交後臺任務,執行錄音邏輯
mExecutorService.submit(new Runnable() {
@Override
public void run() {
//執行開始錄音邏輯 失敗提示用戶
if (!startRecord()){
recordFail();
}
}
});
}
}
});
btn_stream_speech.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
//檢查播放狀態 防止重複播放
if (mAudioFile != null && !mIsPlaying){
//設置當前爲播放狀態
mIsPlaying = true;
//在後臺線程提交播放任務
mExecutorService.submit(new Runnable() {
@Override
public void run() {
doPlay(mAudioFile);
}
});
}
}
});
}
/**
* 播放錄音文件邏輯
* @param mAudioFile
*/
private void doPlay(File mAudioFile) {
//配置播放器
//音樂播放類型 揚聲器播放
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);
//構造AudioTrack
AudioTrack audioTrack = new AudioTrack(streamType,sampleRate,channelConfig,audioFormat,
//不能小於AudioTrack的最低要求 也不能小於我們每次讀的大小
Math.max(minBufferSize,BUFFER_SIZE),mode);
FileInputStream inputStream = null;
try {
//從文件流讀數據
inputStream = new FileInputStream(mAudioFile);
//循環讀取數據 寫到播放器去播放
int read;
while ((read = inputStream.read(mBuffer))> 0){
int ret = audioTrack.write(mBuffer,0,read);
//檢查 write返回值 錯誤處理
switch (ret){
case AudioTrack.ERROR_INVALID_OPERATION:
case AudioTrack.ERROR_BAD_VALUE:
case AudioManager.ERROR_DEAD_OBJECT:
playFail();
return;
default:
break;
}
}
}catch (RuntimeException | IOException e){
e.printStackTrace();
}finally {
//重置播放狀態
mIsPlaying = false;
//關閉文件輸入流
if (inputStream != null){
coloseQuietly(inputStream);
}
//播放器釋放
resetQuietly(audioTrack);
}
//循環讀數據寫到播放器去播放
//錯誤處理防止閃退
}
//錯誤處理
private void playFail() {
mAudioFile = null;
//toast 提示用戶
mMainTheadHandler.post(new Runnable() {
@Override
public void run() {
Toast.makeText(StreamActivity.this, "播放失敗", Toast.LENGTH_SHORT).show();
}
});
}
private void resetQuietly(AudioTrack audioTrack) {
try {
audioTrack.stop();
audioTrack.release();
}catch (RuntimeException e){
e.printStackTrace();
}
}
private void coloseQuietly(FileInputStream inputStream) {
try {
inputStream.close();
}catch (IOException e){
e.printStackTrace();
}
}
/**
* 錄音錯誤的處理
*/
private void recordFail() {
mMainTheadHandler.post(new Runnable() {
@Override
public void run() {
Toast.makeText(StreamActivity.this,"錄音失敗",Toast.LENGTH_SHORT).show();
//重置錄音狀態,修改ui
mIsRecording = false;
btn_stream.setText("開始");
}
});
}
/**
* 啓動錄音邏輯
* @return
*/
private boolean startRecord() {
try {
//創建錄音文件
mAudioFile = new File(getExternalCacheDir().getPath() +"/AudioDemo/" +
System.currentTimeMillis() +".pcm");
mAudioFile.getParentFile().mkdirs();
mAudioFile.createNewFile();
// if (!mAudioFile.exists()){
// boolean mkdirs = mAudioFile.mkdirs();
// Log.e(TAG, "startRecord: mkdirs="+mkdirs );
// if (!mkdirs){
// try {
// throw new IOException("file is not create");
// } catch(IOException e){
// e.printStackTrace();
// }
//
// }
// }
//創建文件輸出流
mFileOutputStream = new FileOutputStream(mAudioFile);
/**
* 配置AudioRecord
*/
int audioSource = MediaRecorder.AudioSource.MIC;
int sampleRate = 44100;
//單聲道輸入
int channelConfig = AudioFormat.CHANNEL_IN_MONO;
//PCM 16 是所有安卓支持
int audioFormat = AudioFormat.ENCODING_PCM_16BIT;
//獲取緩衝區 計算Audiorecord 內部buffer最小大小
int minBufferSize = AudioRecord.getMinBufferSize(sampleRate,channelConfig,audioFormat);
//創建AudioRecord對象 buffer 不能小於最低要求 也不能小於我們每次讀取的大小
mAudioRecord = new AudioRecord(audioSource, sampleRate, channelConfig, audioFormat
, Math.max(minBufferSize, BUFFER_SIZE));
//開始錄音
mAudioRecord.startRecording();
//記錄開始錄音時間 統計時長
mStartRecordTime = System.currentTimeMillis();
//循環讀取數據,寫到輸出流中
while (mIsRecording){
//只要還在錄音狀態,就一直讀取數據
int read = mAudioRecord.read(mBuffer,0,BUFFER_SIZE);
if (read > 0){
//讀取成功 寫到文件中
mFileOutputStream.write(mBuffer,0,read);
}else{
//讀取失敗 返回false 提示用戶
return false;
}
}
//退出循環 通過mIsRecording判斷 停止錄音 釋放資源
return stopRecord();
} catch (IOException | RuntimeException e) {
e.printStackTrace();
//捕獲異常 避免閃退 返回false 提示用戶
Log.e(TAG, "startRecord: exception = "+e.toString());
return false;
} finally {
//釋放 AudioRecord資源
if (mAudioRecord != null){
mAudioRecord.release();
}
}
// return true;
}
/**
* 結束錄音邏輯
* @return
*/
private boolean stopRecord() {
try {
//停止錄音 關閉文件輸出流
mAudioRecord.stop();
mAudioRecord.release();
mAudioRecord = null;
mFileOutputStream.close();
//記錄結束時間,統計錄音時長
mStopRecordTime = System.currentTimeMillis();
//大於3秒成功 改變ui
int second = (int)((mStopRecordTime - mStartRecordTime) /1000);
if (second > 3){
mMainTheadHandler.post(new Runnable() {
@SuppressLint("SetTextI18n")
@Override
public void run() {
tv_stream.setText(tv_stream.getText() + "\n錄音成功" + second + " 秒 ");
}
});
}else {
mMainTheadHandler.post(new Runnable() {
@Override
public void run() {
Toast.makeText(StreamActivity.this,"當前錄製時間不足3秒",Toast.LENGTH_SHORT).show();
}
});
}
} catch (IOException e) {
e.printStackTrace();
//捕獲異常 避免閃退 返回false 提示用戶
return false;
}
return true;
}
}