看到一篇文章说,人的大脑是用于思考的,而不是用于记录事情的,好记心不如烂笔头!
积累在于过程,也是记录自己一段时间的工作状态;以前写的博客总是在完成一个“量”,而未考虑到“质”,所以以后尽量记录一些本人在工作中的实际开发项目或者感悟,不再追求量的多少,而是注重自己学习的过程!
本项目使用的第三方框架比较多,类似IdeaRecode、SoundTouch、SoundStreamWriter等处理声音,因为也是第一次接触该方面的项目,所以也是边学习边编码!
市面上有很多实现该需求的应用,例如:很皮语音包、顽皮语音包等,该需求是由本应用跳转到第三方应用,然后在微信、QQ等进行录音的时候进行自己的语音播放;当时见到该需求也是一脸懵逼的,考虑了很多情况,比如监听其他应用的录音状态、WindowManager上的窗口变化(想到这个是因为微信、QQ录音的时候会 有一个窗口),后面查资料,安卓是只运行一个实例获取到录音器的,也就解释了微信在录音的时候为什么需要一直按着了;再查资料,只需要实现监听变化类即可:
public class MyOnAudioFocusChangeListener implements AudioManager.OnAudioFocusChangeListener {
@Override
public void onAudioFocusChange(int focusChange) {
StringBuilder stringBuilder = new StringBuilder();
stringBuilder.append("当前路由器状态为 : ");
stringBuilder.append(focusChange);
stringBuilder.append("\n检查是否打开或关闭扬声器");
stringBuilder.append(mAudioManager.isSpeakerphoneOn());
stringBuilder.append("\n麦克风");
stringBuilder.append(mAudioManager.isMicrophoneMute());
Log.e("========", stringBuilder.toString());
if (AudioManager.AUDIOFOCUS_LOSS_TRANSIENT == focusChange) {
Log.e("========", "开始播放");
// 实现自己应用内播放
Utils.playVoice();
}
}
}
官网对OnAudioFocusChangeListener的解释为:系统音频焦点在更新时定义的回调接口
录音
录音主要使用AudioRecord来实现录音,通过配置声道、采样位数、采样频率等参数,然后调用AudioRecord.startRecording()、AudioRecord.read()方法来获取到从底层获取到的录音数据,因为录音是需要在子线程中调用,所以stop需要调用线程来取消录音。
public void run() {
/**
* @see AudioRecord#STATE_INITIALIZED
* @see AudioRecord#STATE_UNINITIALIZED
*/
if ((mAudioRecorder != null) && (mAudioRecorder.getState() == STATE_INITIALIZED)) {
Log.d(TAG, "如果录音器不为空,并且已经初始化了则先停止录音,再开始");
try {
mAudioRecorder.stop();
mAudioRecorder.startRecording();
} catch (Exception e) {
e.printStackTrace();
recordFailed(IdealConst.RecorderErrorCode.RECORDER_EXCEPTION_OCCUR);
mAudioRecorder = null;
}
}
/**
*
* @see AudioRecord#RECORDSTATE_STOPPED
* @see AudioRecord#RECORDSTATE_RECORDING
*/
if ((mAudioRecorder != null) &&
(mAudioRecorder.getState() == STATE_INITIALIZED) && (mAudioRecorder.getRecordingState() == RECORDSTATE_STOPPED)) {
Log.e(TAG, "录音器没有权限,或者录音器没有准备好");
recordFailed(IdealConst.RecorderErrorCode.RECORDER_PERMISSION_ERROR);
mAudioRecorder = null;
}
for (int i = 0; i < 2; i++) {
if (mAudioRecorder == null) {
isRecord = false;
break;
}
Log.d(TAG, "读取录音数据");
mAudioRecorder.read(wave, 0, wave.length);
}
while (isRecord) {
Log.d(TAG, "从数组中不停的取录音数据");
int nLen = 0;
try {
nLen = mAudioRecorder.read(wave, 0, wave.length);
} catch (Exception e) {
isRecord = false;
recordFailed(IdealConst.RecorderErrorCode.RECORDER_EXCEPTION_OCCUR);
}
if (nLen == wave.length) {
mCallback.onRecorded(wave);
} else {
recordFailed(IdealConst.RecorderErrorCode.RECORDER_READ_ERROR);
isRecord = false;
}
}
Log.i(TAG, "如果外部调用了停止录音,则调用回调方法,");
unInitializeRecord();
doRecordStop();
}
整个录音逻辑大概就是这样: 先判断录音器是否存在或者初始化,然后开始录音,再调用一个死循环不断的从缓存数组中取数据直到外部调用 isRecord = false
录音播放
录音播放主要是 MediaPlayer ,看下系统提供的API:
static {
System.loadLibrary("media_jni");
native_init();
}
Log.d(TAG, "异步播放"+
"在设置数据源之后,你需要调用prepare() 或者 prepareAsync() ,"+
"对于网络流数据来说,你应该调用prepareAsync(), 调用这个函数"+
"之后,api会立即返回数据,不像同步函数一样会阻塞线程直到有"+
"足够的缓存数据回调!");
public native void prepareAsync() throws IllegalStateException;
Log.d(TAG, "停止播放");
public void stop() throws IllegalStateException {
stayAwake(false);
_stop();
baseStop();
}
Log.d(TAG, "暂停播放, 调用start() 重新播放");
public void pause() throws IllegalStateException {
stayAwake(false);
_pause();
basePause();
}
Log.d(TAG, "调用该方法释放MediaPlayer关联的资源,当你用完音频之后调用该方法"+
"是一个很好的习惯,在Activity或者应用处于onPause状态或者stopped状态时,"+
"应该调用该方法来释放资源,除非需要引用一些资源不释放;如果你的应用持有"+
"引用而没有释放,其他应用程序使用时会调用失败,假设设备有多个编码器,这样"+
"也会使设备处于后台不停的耗电而损耗性能。");
public void release() {
baseRelease();
stayAwake(false);
// .... 释放资源设置
_release();
}
在使用系统的MediaPlayer进行音频播放时,主要是使用上面的几个方法,其中 release() 方法系统介绍了很多,就是告诉我们:当不需要使用时需要调用release() 方法,以便释放不需要的实例和资源,不然会导致设备耗电增多和性能损耗。
播放音频的主要代码如下:
if (mediaPlayer == null) {
this.mediaPlayer = new MediaPlayer();
if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.LOLLIPOP) {
AudioAttributes audioAttributes = new AudioAttributes.Builder()
.setContentType(AudioAttributes.CONTENT_TYPE_MUSIC).build();
mediaPlayer.setAudioAttributes(audioAttributes);
} else {
mediaPlayer.setAudioStreamType(AudioManager.STREAM_MUSIC);
}
mediaPlayer.reset();
mediaPlayer.setDataSource(audioUrl);
mediaPlayer.prepareAsync();
mediaPlayer.setOnPreparedListener(mp -> mp.start());
mediaPlayer.setOnCompletionListener(mp -> {
mp.stop();
mp.release();
});
}
变声
查了很多资料,学NDK的时候也是没花多少心思,导致现在后悔,后面肯定得把ndk捡起来;网上有很多资料讲解了变声这块,使用SoundTouch、Fmod都能实现,搜的话都是实现QQ变声,我这块是使用SoundTouch来实现,因为配置cmake这块不太了解,所以是直接使用了网上编译好的模块:
SoundStreamRunnable对SoundTouch进行了封装,实现了Runnable接口,对文件进行操作:改变声音的参数,或者对文件进行另外保存:
可以看到,修改的主要是tempo、pitchSemi这两个值来对声音进行修改,可以达到想要的效果;变声之后,如果有需求收藏创作的声音,需要上传变声之后的音频,可以使用SoundStreamFileWriter来实现,该类也是继承自SoundStreamRunnable,对声音变声之后的音频流进行写入磁盘操作。