(二)Android音频录制研究

上一篇实现了Android端文字的传输 点击打开链接,由于此系列要实现Android端语音的传输,所以这篇就先研究一下Android端语音的录制。先上效果图吧:

这是主页就是几个按钮:音频的录制分为文件录制和字节流录制,

(1)文件采用Media Record录制和Media Player播放

(2)字节流采用Audio Record录制和Audio Track播放

(3)音量可视化就是实时获取音量大小,显示到屏幕上面

(4)简单实现声音的变速,加速播放和减速播放

上代码:

(1)文件录制

需要说明的是录音JNI函数不具备线程安全性,所以采用了单线程的线程池

executorService = Executors.newSingleThreadExecutor();

因为录音线程在子线程,录音失败和成功与主线程交互,采用了Handler

mainThreadHandler = new Handler(Looper.getMainLooper());

tvSpeak.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;

}

return true;

}

});

private void startRecord() {

tvSpeak.setText("正在说话");

//提交后台任务,执行录音逻辑

executorService.submit(new Runnable() {

@Override

public void run() {

//释放之前录音的recorder

releaseRecorder();

//执行录音逻辑,如果失败 提示用户

if (!doStart()) {

recordFail();

}

}

});

}

private void stopRecord() {

tvSpeak.setText("按住说话");

//提交后台任务,执行停止逻辑

executorService.submit(new Runnable() {

@Override

public void run() {

//执行停止录音逻辑,失败就要提醒用户

if (!doStop()) {

recordFail();

}

//释放recorder

releaseRecorder();

}

});

}

private boolean doStart() {

try {

//创建mediaRecorder

mediaRecorder = new MediaRecorder();

//创建录音文件

mAudioFile = new File(Environment.getExternalStorageDirectory().getAbsolutePath() + "/MyUdpDemo/" + System.currentTimeMillis() + ".m4a");

mAudioFile.getParentFile().mkdirs();

mAudioFile.createNewFile();

//配置Media Recorder

mediaRecorder.setAudioSource(MediaRecorder.AudioSource.MIC);

mediaRecorder.setOutputFormat(MediaRecorder.OutputFormat.MPEG_4);

mediaRecorder.setAudioSamplingRate(44100);

mediaRecorder.setAudioEncoder(MediaRecorder.AudioEncoder.AAC);

mediaRecorder.setAudioEncodingBitRate(96000);

//设置录音文件的位置

mediaRecorder.setOutputFile(mAudioFile.getAbsolutePath());

//开始录音

mediaRecorder.prepare();

mediaRecorder.start();

//记录开始录音时间 用于统计时长

mStartRecordTime = System.currentTimeMillis();

} catch (IOException e) {

e.printStackTrace();

return false;

}

return true;

}

private boolean doStop() {

//停止录音

try {

mediaRecorder.stop();

//记录停止时间

mStopRecordTime=System.currentTimeMillis();

//只接受超过三秒的录音

final int second = (int) (mStopRecordTime - mStartRecordTime)/1000;

if (second > 3) {

mainThreadHandler.post(new Runnable() {

@Override

public void run() {

tvLog.setText(tvLog.getText() + "\n录音成功" + second + "秒");

}

});

}

//停止成功

} catch (Exception e) {

e.printStackTrace();

return false;

}

return true;

}

private void recordFail() {

mAudioFile = null;

//要在主线程执行

mainThreadHandler.post(new Runnable() {

@Override

public void run() {

Toast.makeText(FileActivity.this, "录音失败", Toast.LENGTH_SHORT).show();

}

});

}

private void releaseRecorder() {

//检查mediaRecorder不为空

if (mediaRecorder != null) {

mediaRecorder.release();

mediaRecorder = null;

}

}

录音成功后,下面就是播放了:

@OnClick(R.id.play)

public void onViewClicked() {

if (mAudioFile != null && !isPlaying) {

play.setText("停止");

executorService.submit(new Runnable() {

@Override

public void run() {

startPlay(mAudioFile);

}

});

} else {

play.setText("播放");

executorService.submit(new Runnable() {

@Override

public void run() {

stopPlay();

}

});

}

}

private void startPlay(File audioFile) {

//配置播放器

mMediaPlayer = new MediaPlayer();

try {

//设置声音文件

mMediaPlayer.setDataSource(audioFile.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();

return true;

}

});

//配置音量 是否循环

mMediaPlayer.setVolume(1, 1);

mMediaPlayer.setLooping(false);

//准备 开始

mMediaPlayer.prepare();

mMediaPlayer.start();

} catch (RuntimeException e) {

e.printStackTrace();

//异常处理防止闪退

playFail();

} catch (IOException e) {

e.printStackTrace();

}

}

private void stopPlay() {

//重置播放状态

isPlaying = false;

play.setText("播放");

if (mMediaPlayer != null) {

mMediaPlayer.setOnCompletionListener(null);

mMediaPlayer.setOnErrorListener(null);

mMediaPlayer.stop();

mMediaPlayer.reset();

mMediaPlayer.release();

mMediaPlayer = null;

}

}

private void playFail() {

mainThreadHandler.post(new Runnable() {

@Override

public void run() {

Toast.makeText(FileActivity.this, "播放失败", Toast.LENGTH_SHORT).show();

}

});

}

在onDestroy方法里面注销

@Override

protected void onDestroy() {

super.onDestroy();

//activity销毁时停止后台任务 避免后台任务

executorService.shutdown();

releaseRecorder();

stopPlay();

}

至此文件的录制和播放就结束了

(2)字节流录制

@OnClick({R.id.btnStart, R.id.play})

public void onViewClicked(View view) {

switch (view.getId()) {

case R.id.btnStart:

if (mIsRecording) {

btnStart.setText("开始");

mIsRecording = false;

} else {

btnStart.setText("停止");

mIsRecording = true;

executorService.submit(new Runnable() {

@Override

public void run() {

if (!startRecord()) {

recordFail();

}

}

});

}

break;

case R.id.play:

//检查播放状态 防止重复播放

if (mAudioFile != null && !isPlaying) {

isPlaying = true;

executorService.submit(new Runnable() {

@Override

public void run() {

startPlay(mAudioFile);

}

});

}

break;

}

}

private boolean startRecord() {

try {

//创建录音文件

mAudioFile = new File(Environment.getExternalStorageDirectory().getAbsolutePath() + "/MyUdpDemo/" + System.currentTimeMillis() + ".pcm");

mAudioFile.getParentFile().mkdirs();

mAudioFile.createNewFile();

//创建文件输出流

fileOutputStream = new FileOutputStream(mAudioFile);

//配置Audio Record

int minBufferSize = AudioRecord.getMinBufferSize(44100, AudioFormat.CHANNEL_IN_MONO, AudioFormat.ENCODING_PCM_16BIT);

//buffer不能小于最低要求,也不能小于我们每次读取的大小

mAudioRecord = new AudioRecord(MediaRecorder.AudioSource.MIC, 44100, AudioFormat.CHANNEL_IN_MONO, AudioFormat.ENCODING_PCM_16BIT, Math.max(minBufferSize, BUFFERSIZE));

//开始录音

mAudioRecord.startRecording();

//记录开始录音时间,用于统计时长

mStartTime = System.currentTimeMillis();

//循环读取数据,写到输出流中

while (mIsRecording) {

int read = mAudioRecord.read(buffer, 0, BUFFERSIZE);

//返回值是这次读到了多少

if (read > 0) {

//读取失败

fileOutputStream.write(buffer, 0, read);

} else {

//读取失败

return false;

}

}

//退出循环,停止录音,释放资源

return stopRecord();

} catch (IOException e) {

e.printStackTrace();

return false;

} finally {

//释放Audio Record

if (mAudioRecord != null) {

mAudioRecord.release();

}

}

}

private boolean stopRecord() {

try {

//停止录音 关闭文件输出流

mAudioRecord.stop();

mAudioRecord.release();

mAudioRecord = null;

fileOutputStream.close();

//记录结束时间 统计时长

mStopTime = System.currentTimeMillis();

final int second = (int) ((mStopTime - mStartTime) / 1000);

//大于3秒的成功 在主线程改变UI

if (second > 3) {

mMainHandler.post(new Runnable() {

@Override

public void run() {

tvLog.setText(tvLog.getText() + "\n录音成功" + second + "秒");

}

});

}

} catch (IOException e) {

e.printStackTrace();

return false;

}

return true;

}

private void recordFail() {

mMainHandler.post(new Runnable() {

@Override

public void run() {

Toast.makeText(StreamActivity.this, "录音失败", Toast.LENGTH_SHORT).show();

//重置录音状态 UI状态

mIsRecording = false;

btnStart.setText("开始");

}

});

}

private void startPlay(File mAudioFile) {

//配置播放器

//扬声器播放

int streamType = AudioManager.STREAM_MUSIC;

//播放的采样频率 和录制的采样频率一样

int sampleRate = 44100;

//和录制的一样的

int audioFormat = AudioFormat.ENCODING_PCM_16BIT;

//流模式

int mode = AudioTrack.MODE_STREAM;

//录音用输入单声道  播放用输出单声道

int channelConfig = AudioFormat.CHANNEL_OUT_MONO;

int minBufferSize = AudioTrack.getMinBufferSize(sampleRate, channelConfig, audioFormat);

AudioTrack audioTrack = new AudioTrack(streamType, sampleRate, channelConfig, audioFormat, Math.max(minBufferSize, BUFFERSIZE), mode);

audioTrack.play();

//从文件流读数据

FileInputStream fileInputStream = null;

try {

fileInputStream = new FileInputStream(mAudioFile);

int read;

while ((read = fileInputStream.read(buffer)) > 0) {

int ret = audioTrack.write(buffer, 0, read);

//检查write 返回值 错误处理

switch (ret) {

case AudioTrack.ERROR_BAD_VALUE:

case AudioTrack.ERROR_INVALID_OPERATION:

case AudioTrack.ERROR_DEAD_OBJECT:

playFail();

break;

default:

break;

}

}

} catch (RuntimeException | IOException e) {

e.printStackTrace();

playFail();

} finally {

//关闭文件流

isPlaying = false;

if (fileInputStream != null) {

closeQuatily(fileInputStream);

}

resetAudioTrack(audioTrack);

}

}

private void playFail() {

mAudioFile = null;

mMainHandler.post(new Runnable() {

@Override

public void run() {

Toast.makeText(StreamActivity.this, "播放失败", Toast.LENGTH_SHORT).show();

}

});

}

private void resetAudioTrack(AudioTrack audioTrack) {

try {

audioTrack.stop();

audioTrack.release();

} catch (RuntimeException e) {

e.printStackTrace();

}

}

private void closeQuatily(FileInputStream fileInputStream) {

try {

fileInputStream.close();

} catch (IOException e) {

e.printStackTrace();

}

}

@Override

protected void onDestroy() {

super.onDestroy();

executorService.shutdownNow();

}

(3)音频可视化

主要就是获取音量大小,划分等级进行显示

public void getRecordVolume() {

if (mediaRecorder != null) {

}

int maxAmplitude;

//获取音量大小

try {

maxAmplitude = mediaRecorder.getMaxAmplitude();

} catch (RuntimeException e) {

e.printStackTrace();

//异常发生后 用一个随机数代表当前音量大小

maxAmplitude = random.nextInt();

}

final int level = maxAmplitude / (MAXAMPLITUDE / MAXLEVEL);

//把音量规划到五个等级

//把等级显示到UI上面

mainThreadHandler.post(new Runnable() {

@Override

public void run() {

refreshVolume(level);

}

});

//如果仍在录音,就隔一段时间再次获取音量大小

if (isRecording) {

executorService.schedule(new Runnable() {

@Override

public void run() {

getRecordVolume();

}

}, 50, TimeUnit.MILLISECONDS);

}

}

private void refreshVolume(int level) {

for (int i = 0; i < 5; i++) {

imageViewList.get(i).setVisibility(i < level ? View.VISIBLE : View.GONE);

}

}

(4)音频变速播放

添加三个支持的播放采样率

private static final int[] SUPPORTSAMPLERATE = {11025, 22050, 44100};

录制的时候采用中间的频率,点击不同的播放按钮,采用不同的播放频率

case R.id.play:

//检查播放状态 防止重复播放

play(SUPPORTSAMPLERATE[1]);

break;

case R.id.playFast:

//检查播放状态 防止重复播放

play(SUPPORTSAMPLERATE[2]);

break;

case R.id.playSlowly:

//检查播放状态 防止重复播放

play(SUPPORTSAMPLERATE[0]);

break;

到此就结束啦!下一篇将会进行音频的传输研究了

源码地址:点击打开链接

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,098评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,213评论 2 380
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 149,960评论 0 336
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,519评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,512评论 5 364
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,533评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,914评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,574评论 0 256
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,804评论 1 296
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,563评论 2 319
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,644评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,350评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,933评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,908评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,146评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,847评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,361评论 2 342

推荐阅读更多精彩内容