安卓界面刷新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;
}
}