背景
公司自研app在某型号手台上出现bug,音视频通话过程中经常丢音,例如通话过程中快速数数,从1数到9,对方听到的效果可能是1..3..9,部分数字听不到,音效也很差。
问题定位
通过在后台软交换服务抓包,分析音频流,发现媒体服务器收到的音频流就是不完整的,大概定位到应该是app音频采集的问题。
通过查看源码,找到音频采集相关代码,通过不断摸索,修改了2个地方解决问题。
//int audioSrc = MediaRecorder.AudioSource.MIC;
int audioSrc = MediaRecorder.AudioSource.VOICE_COMMUNICATION;
将音源类型由MIC改为VOICE_COMMUNICATION后,声音效果好了很多,但是仍然没有解决丢音的问题。
通过增加写文件代码,将音频流写入文件,进一步定位问题。
以下是写文件前的准备代码,在主线程运行。
private void prepareRecordFile() {
String saveDir = Environment.getExternalStorageDirectory().getAbsolutePath() + "/hbfec";
File saveDirFile = new File(saveDir);
if(!saveDirFile.exists()) {
saveDirFile.mkdirs();
}
savePath = saveDir + "/" + System.currentTimeMillis() + ".pcm";
//生成PCM文件
file = new File(savePath);
Log.i(TAG,"生成文件");
if (file.exists()) {
Log.i(TAG,"文件" + savePath + "已存在");
} else {
try {
file.createNewFile();
Log.i(TAG,"创建文件");
} catch (IOException e) {
Log.i(TAG,"未能创建");
throw new IllegalStateException("未能创建" + file.toString());
}
}
try {
//输出流
FileOutputStream fos = new FileOutputStream(file, true);
BufferedOutputStream bos = new BufferedOutputStream(fos);
dos = new DataOutputStream(bos);
} catch (Throwable t) {
Log.e(TAG, "录音失败");
}
}
在处理音频流的子线程中添加写文件及关闭写文件的代码。
private Runnable mRunnableRecorder = new Runnable() {
@Override
public void run() {
Log.d(TAG, "===== Audio Recorder (Start) ===== ");
android.os.Process
.setThreadPriority(android.os.Process.THREAD_PRIORITY_URGENT_AUDIO);
mAudioRecord.startRecording();
final int nSize = mAudioFrame.capacity();
byte silenceBuffer[] = new byte[nSize];
int nRead;
if (NgnProxyAudioProducer.super.mValid) {
mProducer.setPushBuffer(mAudioFrame, mAudioFrame.capacity(), false);
mProducer
.setGain(NgnEngine
.getInstance()
.getConfigurationService()
.getInt(
NgnConfigurationEntry.MEDIA_AUDIO_PRODUCER_GAIN,
NgnConfigurationEntry.DEFAULT_MEDIA_AUDIO_PRODUCER_GAIN));
}
// disable Doubango AEC
if (mHasBuiltInAEC) {
final NgnAVSession ngnAVSession = NgnAVSession
.getSession(getSipSessionId());
if (ngnAVSession != null) {
ngnAVSession.setAECEnabled(false);
}
}
while (NgnProxyAudioProducer.super.mValid && mStarted) {
if (mAudioRecord == null) {
break;
}
if (mRoutingChanged) {
Log.d(TAG, "Routing changed: restart() recorder");
mRoutingChanged = false;
unprepare();
if (prepare(mPtime, mRate, mChannels) != 0) {
break;
}
if (!NgnProxyAudioProducer.super.mPaused) {
mAudioRecord.startRecording();
}
}
// To avoid overrun read data even if on pause/mute we have to
// read
if ((nRead = mAudioRecord.read(mAudioFrame, nSize)) > 0) {
Log.i(TAG,"写文件nRead:"+ nRead + "nSize:" + nSize);
//写文件
try {
dos.write(mAudioFrame.array(), 0, nRead);
Log.i(TAG,"写文件");
} catch (IOException e) {
e.printStackTrace();
Log.i(TAG,"写文件异常");
}
if (!NgnProxyAudioProducer.super.mPaused) {
if (mOnMute) { // workaround because Android's
// SetMicrophoneOnMute() is buggy
mAudioFrame.put(silenceBuffer);
mProducer.push(mAudioFrame, silenceBuffer.length);
mAudioFrame.rewind();
} else {
if (nRead != nSize) {
mProducer.push(mAudioFrame, nRead);
Log.w(TAG, "BufferOverflow?");
} else {
mProducer.push();
}
}
}
}
/*int state = mAudioRecord.getRecordingState();
if (state == AudioRecord.RECORDSTATE_STOPPED) {
Log.d(TAG, "===== RECORDSTATE_STOPPED ===== ");
}*/
}
unprepare();
//关闭写文件
try {
dos.close();
Log.i(TAG,"写文件关闭");
} catch (IOException e) {
e.printStackTrace();
Log.i(TAG,"写文件关闭异常");
}
//同步刷新文件目录,解决手台连接Windows电脑后,在目标目录下找不到pcm文件的问题。 MediaScannerConnection.scanFile(NgnApplication.getContext(), new String[] { file.getAbsolutePath() }, null, null);
Log.d(TAG, "===== Audio Recorder (Stop) ===== ");
}
};
通过Cool Edit Pro软件,对pcm文件进行分析,发现录制的音频就是残缺不全的,进一步确认,丢音问题是因为采集导致。
资料查找
通过进一步搜索查找相关资料,发现问题有可能出在缓存大小上面,参考链接,连接中的文章提到AudioRecord初始化时得bufferSize不能随便设置,AudioRecord提供对应的API来获取这个值。
对比源码发现,程序中AudioRecord初始化时使用的并非通过API获取的值,将代码做相应修改后,再次测试,问题解决。
private synchronized int prepare(int ptime, int rate, int channels) {
if (super.mPrepared) {
Log.e(TAG, "already prepared");
return -1;
}
// Use 44100hz for Hovis_Box_V1
if (NgnApplication.isHovis()) {
rate = 44100;
mProducer.setActualSndCardRecordParams(ptime, rate, channels);
}
final boolean aecEnabled = NgnEngine.getInstance()
.getConfigurationService().getBoolean(
NgnConfigurationEntry.GENERAL_AEC,
NgnConfigurationEntry.DEFAULT_GENERAL_AEC);
final int minBufferSize = AudioRecord.getMinBufferSize(rate,
AudioFormat.CHANNEL_IN_MONO, AudioFormat.ENCODING_PCM_16BIT);
final int shortsPerNotif = (rate * ptime) / 1000;
// AEC won't work if there is too much delay
// Too short bufferSize will produce BufferOverflow errors but we don't
// have choice if we want AEC
final float bufferFactor = aecEnabled ? NgnProxyAudioProducer.AUDIO_BUFFER_FACTOR
: NgnProxyAudioProducer.AUDIO_BUFFER_FACTOR;
final int bufferSize = Math.max(
(int) ((float) minBufferSize * bufferFactor),
shortsPerNotif << 1);
mAudioFrame = ByteBuffer.allocateDirect(shortsPerNotif << 1);
mPtime = ptime;
mRate = rate;
mChannels = channels;
Log.d(TAG, "Configure aecEnabled:" + aecEnabled);
// int audioSrc = MediaRecorder.AudioSource.MIC;
int audioSrc = MediaRecorder.AudioSource.VOICE_COMMUNICATION;
if (aecEnabled) {
//audioSrc = MediaRecorder.AudioSource.VOICE_RECOGNITION;
// Do not use built-in AEC
/*
* if(NgnApplication.getSDKVersion() >= 11){ try { final Field f =
* MediaRecorder
* .AudioSource.class.getDeclaredField("VOICE_COMMUNICATION");
* audioSrc = f.getInt(null); mHasBuiltInAEC = true; } catch
* (Exception e) { e.printStackTrace(); } }
*/
}
// Use CAMCORDER audio source for Hovis_Box_V1
if (NgnApplication.isHovis()) {
audioSrc = MediaRecorder.AudioSource.CAMCORDER;
}
// mAudioRecord = new AudioRecord(audioSrc, rate,
// AudioFormat.CHANNEL_IN_MONO, AudioFormat.ENCODING_PCM_16BIT,
// bufferSize);
mAudioRecord = new AudioRecord(audioSrc, rate,
AudioFormat.CHANNEL_IN_MONO, AudioFormat.ENCODING_PCM_16BIT,
minBufferSize);
//录音
prepareRecordFile();
if (mAudioRecord.getState() == AudioRecord.STATE_INITIALIZED) {
super.mPrepared = true;
return 0;
} else {
Log.e(TAG, "prepare(" + mAudioRecord.getState() + ") failed");
super.mPrepared = false;
return -1;
}
}
结论
这个问题只出现在公司较旧的一款手台上,新型手台及普通Android手机并未发现此问题。所以有可能是之前的Android系统对这两个参数较为敏感,后续的系统进行了优化。
不管怎样,问题得到解决还是很开心,这里记录一下,希望可以帮到遇到同样问题的同学。