Android AudioRecord音频采集问题总结

背景

公司自研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系统对这两个参数较为敏感,后续的系统进行了优化。

不管怎样,问题得到解决还是很开心,这里记录一下,希望可以帮到遇到同样问题的同学。

评论

Your browser is out-of-date!

Update your browser to view this website correctly. Update my browser now

×