Android麦克风探测器

Android麦克风探测器,第1张

一、问题背景

目前麦克风是否可用的检测方案过于简单,容易出现检测不准确的情况。针对此问题做了如下优化。

推荐集成方在使用麦克风的采集和上行功能前要检测麦克风是否可用。

二、麦克风探测器功能介绍

1、录音。包括采样率、声源、采样位数、声道数等参数适配,保存音频文件生成pcm或者wav,获取音频的分贝值。

2、麦克风权限是否授予检测。

3、麦克风当前是否被占用检测。

4、音频数据的合法性检测。采集到的音频有没有被系统静默处理。

检测方案如下图所示:

三、录音机录音以及生成音频文件

系统录音机AudioRecord正常启动是麦克风占用检测的前提,系统录音机正常启动需要给录音机传入正确的参数,包括声源、采样率、声道数、采样位数,录音机才会创建成功。根据AudioRecord提供的API以及以往开发经验,绝大部分(具体数据没有统计)机型传入以下参数系统录音机能创建成功:

mAudioSource = MediaRecorder.AudioSource.MIC;
mSampleRateInHz = 16000;
mChannelConfig = AudioFormat.CHANNEL_IN_MONO;
mAudioFormat = AudioFormat.ENCODING_PCM_16BIT

极其个别机型录音参数需要适配,目前的适配:

//1、魅族、OPPO、realme的Android10系统,开启无障碍时的参数
if (context != null
    && (DeviceUtil.isMeiZu() || DeviceUtil.isOppo() || DeviceUtil.isRealMe())
    && (DeviceUtil.getSdkIntVersion() == 29)
    && DeviceUtil.isAccessibilityEnabled(context.getApplicationContext())) {
  mAudioSource = MediaRecorder.AudioSource.VOICE_RECOGNITION;
  mChannelConfig = AudioFormat.CHANNEL_CONFIGURATION_MONO;
}

 读麦克风返回的数据会先缓存在内存中,最后一次读取的数据(index为负数)返回后,可保存生成pcm或者wav方便测试。麦克风录音流程图如下所示:

四、麦克风权限检测

调用系统api即可,不复杂

public static boolean isAudioPermissionGranted(Context context) {
  if (context == null) {
    return false;
  }
  PackageManager packageManager = context.getPackageManager();
  String packageName = context.getPackageName();
  return packageManager.checkPermission(Manifest.permission.RECORD_AUDIO, packageName)
      == PackageManager.PERMISSION_GRANTED;
}

五、麦克风占用检测以及音频合法性检测

      麦克风占用情况分Android 10以及Android 10以上,以及Android 10以下,Android 10以及Android 10 以上系统麦克风共享,多个进程可同时从麦克风中读取到数据,但只有一个进程读取到的数据是有效的,其他进程读取的音频数据会被静默处理(音频无效,都是0),优先级规则如下:

特权应用(Google助理、无障碍服务)高于普通应用(第三方应用、系统内置的录音机等)具有可见前台界面的应用比后台应用具有更高的优先级相较于从非隐私敏感源捕获音频的应用,从隐私敏感源(音频源为CAMCORDER或VOICE_COMMUNICATION)捕获音频的应用有着更高的优先级如果两个优先级相同的后台应用都在捕获音频,则后开始的那个优先级更高

       两个普通应用都要录音时,只有一个应用接收音频,另一个应用会受到静默处理,此种场景麦克风共享规则如下:

如果两个应用都不具备隐私敏感性,则由界面位于顶部的应用接收音频。如果两个应用都没有界面,则较晚开始者接收音频。

如果其中一个应用具备隐私敏感性,则由其接收音频,另一个应用则会受到静默处理,即使后者由界面位于顶部或较晚开始捕获也是如此。

如果两个应用都具备隐私敏感性,则由最晚开始捕获的应用接收音频,另一个应用则会受到静默处理。

     其他三种组合:Google助理+普通应用,无障碍服务+普通应用,语音通话+普通应用,麦克风共享规则参考Google开发者文档:共享音频输入  |  Android 开发者  |  Android Developers

     Android 10以下的系统麦克风不能共享,一个应用在录音时,另一个应用可成功创建audioRecord但在startRecording()后的返回值不是RECORDSTATE_RECORDING,可以判断当前麦克风有没有被占用。

     因此综合Android 10以及以上,以及Android10 以下的系统机制,麦克风是否可用判断规则如下:

无录音权限时,麦克风不可用;有录音权限时,startRecording()后如果是recording状态的话,说明要么是android10以下的系统并且当前没有其他进程在录音,要么是android10以及10以上的系统。上述两种情况,都录取800毫秒的音频数据,判断300到800毫秒之间的音频数据,如果有一次读取到的数据是有效的,我们就认为麦克风可正常使用。如果300到800毫秒之间采集到的音频都被静默处理了,说明我们的进程目前优先级低,此时虽然可以采集到声音,但采集到的音频无效,我们也认为当前麦克风不可用;有录音权限时,如果startRecording() 之后录音机不是recording状态的话,说明当前系统是Android 10以下,并且有其他进程在录音,此时认定麦克被占用。

    此外,麦克风检测添加了超时机制,从开始检测计时如果1秒后还没有拿到检测结果的话,超时机制启动,给集成方返回检测结果(麦克风可用)。

    代码示意如下:

private static final int DETECT_DURATION = 800; // 开启录音后,读取800毫秒数据
private static final int DETECT_START_TIME = 300; // 检测300-800毫秒之间的数据
private static final int TIME_OUT_PERIOD = 1000;//超时检测时间
 
MicStatus micStatus = null;
  try {
    isDetecting = true;
    if (!MicUtil.isAudioPermissionGranted(mContext)) {//无权限
      micStatus = new MicStatus(ErrorCode.MIC_STATUS_NOT_GRANTED,
          ErrorMessage.MIC_STATUS_NOT_GRANTED_MSG);
    } else {//有权限
      mRecorderWrapper.start();
      final long startTime = System.currentTimeMillis();
      int audioNum = 0;
      while (System.currentTimeMillis() - startTime <= DETECT_DURATION) {
        RecorderWrapper.ReadAudioData readAudioData = mRecorderWrapper.read();
        AudioData audioData = null;
        if (readAudioData != null) {
          int ret = readAudioData.getReadLength();
          byte[] audioBytes = readAudioData.getReadBytes();
          double decibel =
              AudioUtil.getVoiceDecibel(ByteShortUtil.byteArray2ShortArray(audioBytes));
          audioNum++;
          audioData = new AudioData(audioBytes, audioNum, decibel);
          // 第一次读取到数据后再回调onStart
          if (Math.abs(audioNum) == 1) {
            if (mAudioEventListener != null) {
              mAudioEventListener.onStart(String.valueOf(System.currentTimeMillis()));
            }
          }
          if ((System.currentTimeMillis() - startTime > DETECT_START_TIME)) {//检测300-800毫秒的数据
            if (mAudioEventListener != null) {
              mAudioEventListener.onNext(audioData);
            }
 
            boolean isAudioAvailable = AudioUtil.isAvailable(readAudioData.getReadBytes());
            //300 - 800ms内读取到一次有效数据,就认为麦克风可用
            if (isAudioAvailable) {
              micStatus = new MicStatus(ErrorCode.MIC_STATUS_AVAILABLE,
                  ErrorMessage.MIC_STATUS_AVAILABLE);
              break;
            }
          }
        } else {//兜底策略
          micStatus =
              new MicStatus(ErrorCode.MIC_STATUS_AVAILABLE, ErrorMessage.MIC_STATUS_AVAILABLE);
          break;
        }
      }
      // 800毫秒读到的数据都无效
      if (micStatus == null) {
        micStatus = new MicStatus(ErrorCode.MIC_STATUS_NOT_AVAILABLE,
            ErrorMessage.MIC_STATUS_NOT_AVAILABLE + "能读到静默数据");
      }
    }
  } catch (KeAudioError keAudioError) {
    if (keAudioError != null) {
      micStatus = new MicStatus(keAudioError.getErrorCode(), keAudioError.getErrorMessage());
    }
  } finally {
    mRecorderWrapper.release();
    mHandler.removeCallbacks(mRunnable);
    if (mMicDetectorListener != null) {
      mMicDetectorListener.onDetectMicStatus(micStatus);
    }
    isDetecting = false;
    if (mAudioEventListener != null) {
      mAudioEventListener.onComplete();
    }
  }
六、使用
// 判断麦克风是否可用
public static void isMicAvailable(Context context, final Action1 action1) {
  if (context == null) {
    if (action1 != null) {
      action1.call(new MicStatus(ErrorCode.MIC_STATUS_NOT_AVAILABLE, "context is null"));
    }
    return;
  }
 
  new MicDetector(context, new MicDetector.MicDetectorListener() {
    @Override
    public void onDetectMicStatus(MicStatus micStatus) {
      if (micStatus == null) {
        micStatus = new MicStatus(ErrorCode.MIC_STATUS_NOT_AVAILABLE, "未知原因");
      }
      if (action1 != null) {
        action1.call(micStatus);
      }
    }
  }, null).detectMicStatus();
}
七、埋点

是否授予了权限、读取到的音频数据是否有效、是否启动了超时机制、从开始检测到拿到检测结果的耗时,这些我们都做好了埋点工作。

八、注意事项

1、直接开线程不友好,考虑单一线程池。

2、某些机型录音会失败,需要结合机型信息收集设备可用的录音参数,做埋点。

3、极其个别机型还需要录音参数适配,两种方案:2中收集的录音参数,客户端做适配策略;或者反编译最新版本的wx app,参考wx在最新机型以及特殊机型的录音参数适配策略。

4、检测结束(不论是正常的结束,还是检测超时)一定要释放麦克风,避免麦克风占用问题,而且释放麦克风时要日志埋点用来证明麦克风确实被释放了。

欢迎分享,转载请注明来源:内存溢出

原文地址: https://outofmemory.cn/web/992260.html

(0)
打赏 微信扫一扫 微信扫一扫 支付宝扫一扫 支付宝扫一扫
上一篇 2022-05-21
下一篇 2022-05-21

发表评论

登录后才能评论

评论列表(0条)

保存