前端信号处理(Android)
一. 概述
DUI Lite-前端信号处理和本地唤醒可使在无网的情况下,将麦克风阵列采集到的多路音频送内核做aec->beamforming后输出信号增强后的单路音频。同时具有本地唤醒的功能,即在送实时音频的同时监听唤醒状态。具有功效低、唤醒率高的特点。您仅仅需要将下载的SDK嵌入到工程项目中,就可以对应设备进行语音交互。目前支持家居双麦(dual)/线性四麦(line4)/环形四麦(circle4)/环形六麦(circle6)
二. 集成
2.1准备
jar:DUI-lite-SDK-for-Android-xxx.jar
so:libsspe.so
资源:可以放在assets目录中,也可以自己手动放在磁盘中,比如/sdcard/
wakeup资源: wakeup_xxxx_comm_xxx.bin
sspe资源:
2.2.权限
如果资源放在sdcard中或者保存音频,需要申请读写权限
targetSdkVersion 版本高于
30
,需要增加权限
3.3.混淆
三. 引擎运行流程图
四. 功能使用
4.1初始化参数说明
详细参考AILocalSignalAndWakeupConfig
参数名
|
取值
|
说明
|
是否必须
|
默认值
|
---|---|---|---|---|
setSspeResource(String sspeResource) | String |
sspe 资源, 包含 AEC BSS 等,不同项目含义有所差别 如在 sd 里设置为绝对路径 如/sdcard/speech/***.bin 如在 assets 里设置为名称
|
必须 | NA |
setWakeupWord(String[] wakeupWord, int[] majors) | String,int[] |
wakeupWord:设置唤醒词以及是否作为主唤醒词,主唤醒词为1,副唤醒词为0 唤醒词,如 ["ni hao xiao chi", "ni hao xiao le","bu ding bu ding"] majors:是否是主唤醒词,如 [1,0,0]
|
必须 | NA |
setWakeupResource(String wakeupResource) | int |
唤醒资源 如在 sd 里设置为绝对路径 如/sdcard/speech/***.bin 如在 assets 里设置为名称 |
必须 | NA |
setThreshold(float[] threshold) | int | 设置唤醒词对应阈值,是否需要设置和唤醒资源有关系 | 非 | NA |
setNearWakeupConfig(NearWakeupConfig nearWakeupConfig) | NearWakeupConfig | 就近唤醒的配置,包含 net 和 mds 的配置 | 非 | NA |
4.2开启
详细参数说明AILocalSignalAndWakeupIntent
参数名
|
取值
|
说明
|
是否必须
|
默认值
|
---|---|---|---|---|
setUseCustomFeed(boolean useCustomFeed) |
boolean |
用户调用 feedData 方法输入音频数据 默认false,使用内部录音机 true 用户调用 feedData 方法输入音频数据 |
非 | false |
setInputContinuousAudio(boolean inputContinuousAudio) | boolean |
设置是否输入实时的长音频,默认接受长音频为true(如果是一二级唤醒,即每个唤醒词独立且非实时,则需要设置为false,如果不设置会影响性能)
|
非 | true |
4.3动态设置参数
详细参数说明AILocalSignalAndWakeupListener
参数名 |
取值 |
说明 |
示例 |
---|---|---|---|
setDynamicParam(Map<String, ?> dynamicParam) |
MAP |
动态设置唤醒参数,可以在引擎初始化成功后动态设置 Map<String, Object> dynamicParam = new HashMap<>(); |
// 不需要唤醒的话可以关闭唤醒功能,START之前设置 |
4.4回调说明
AILocalSignalAndWakeupListener里的方法说明
方法名 |
说明 |
---|---|
方法名 |
说明 |
onInit(int status) | 初始化的回调,在主UI线程 |
onError(AIError error) | 发生错误时执行,在主UI线程 |
onWakeup(double confidence, String wakeupWord) | 一次唤醒检测完毕后执行,在主UI线程。wakeupWord返回的唤醒词,confidence返回的唤醒词阈值 |
onNearInformation(String json) | 使用就近唤醒时,就近唤醒会回传一些中间信息 |
onDoaResult(int doa) | 返回唤醒角度 |
onReadyForSpeech() | 录音机启动时调用,在主UI线程 |
onRawDataReceived(byte[] buffer, int size) | 原始音频数据返回,多声道pcm数据,在SDK内部子线程 |
onResultDataReceived(byte[] buffer, int size, int wakeup_type) | 经过beamforming模块处理后的音频数据返回,1声道pcm数据,在SDK内部子线程。wakeup_type 唤醒类型 0:非唤醒状态; 1:主唤醒词被唤醒; 2副唤醒词被唤醒 |
onVprintCutDataReceived(int dataType, byte[] data, int size) | 输出给声纹的前端信号处理后的音频或唤醒字符串,只在唤醒+声纹一起使用时需关注,且直接透传给AILocalVprintEngine |
onAgcDataReceived(byte[] buffer, int size) | 送agc模块后的音频 |
onInputDataReceived(byte[] data, int size) | 算法内核的原始输入音频 |
onOutputDataReceived(byte[] data, int size) | 特定资源下抛出的处理后的音频,区别于beamforming音频 |
onEchoDataReceived(byte[] data, int size) | 带回路的资源消除回路后的音频数据 |
onSevcDoaResult(int doa) | 输出信号处理后语音通信的beam index信息 |
onSevcNoiseResult(String retString) |
输出信号处理估计噪声最大的beam index 信息和该方向的音量信息,为 json 字符串 {{"chans": 0,"db":56.625889}} |
onMultibfDataReceived(byte[] data, int length, int index) | 输出多路beam音频数据以及对应的通道index信息 |
onEchoVoipDataReceived(byte[] data, int length) | 输出经过回声消除的送给VoIP使用的音频数据 |
五. 示例代码
private void initSignalProcessingEngine() { AILocalSignalAndWakeupConfig config = new AILocalSignalAndWakeupConfig(); // 设置 sspe 资源后 AEC 和 beamforming 资源即使设置也无效。 sspe 处理后的音频从 onResultDataReceived 方法回调 switch (DUILiteSDK.getAudioRecorderType()) { case DUILiteConfig.TYPE_COMMON_ECHO: config.setSspeResource(SampleConstants.ECHO_RES); //设置线性双麦bf资源 config.setWakeupWord( new String[]{ "ni hao xiao le" }, new int []{ 1 }); config.setEchoChannelNum( 1 ); // 设置参考音路数 break ; case DUILiteConfig.TYPE_COMMON_DUAL: config.setSspeResource(SampleConstants.SSPE_DUAL_REF0_RES); //设置线性双麦bf资源 config.setWakeupWord( new String[]{ "ni hao xiao le" }, new int []{ 1 }); config.setEchoChannelNum( 2 ); // 设置参考音路数 break ; case DUILiteConfig.TYPE_COMMON_LINE4: config.setSspeResource(SampleConstants.SSPE_LINE4_RES); //设置线性四麦bf资源 config.setWakeupWord( new String[]{ "ni hao xiao le" }, new int []{ 1 }); config.setEchoChannelNum( 2 ); // 设置参考音路数 break ; case DUILiteConfig.TYPE_COMMON_CIRCLE4: config.setSspeResource(SampleConstants.SSPE_CIRCLE4_RES); //设置环形四麦bf资源 config.setWakeupWord( new String[]{ "ni hao xiao le" }, new int []{ 1 }); config.setEchoChannelNum( 2 ); // 设置参考音路数 break ; default : break ; } // 设置唤醒资源,如不设置则不启用唤醒功能,此Activity示例的是动态开关唤醒功能 config.setWakeupResource(SampleConstants.WAKEUP_RES); // 设置唤醒词 和 是否是主唤醒词,1表示主唤醒词,0表示副唤醒词 config.setThreshold( new float []{ 0 .34f}); //设置唤醒词对应的阈值 // config.setLowThreshold(new float[]{0.25f});//设置电视大音量场景下的预唤醒阈值,若非大音量场景下,无需配置 //config.setRollBackTime(1200);//oneshot回退的时间,单位为ms(只有主唤醒词才会回退音频,即major为1) if (WAKEUP_WORDS_ARRAY != null && WAKEUP_MAJORS_ARRAY != null && WAKEUP_THRESH_ARRAY != null ) { config.setWakeupWord(WAKEUP_WORDS_ARRAY, WAKEUP_MAJORS_ARRAY); config.setThreshold(WAKEUP_THRESH_ARRAY); } if (AEC_NUM != - 1 ) { config.setEchoChannelNum(AEC_NUM); } //注册接口开关 默认是true config.setImplMultiBfCk( false ); config.setImplMultiBfCk( true ); config.setImplWakeupCk( true ); config.setImplInputCk( true ); config.setImplOutputCk( true ); config.setImplEchoCk( true ); config.setImplEchoVoipCk( true ); config.setImplAgcCk( true ); config.setImplBfCk( true ); config.setImplDoaCk( true ); config.setImplSevcNoiseCk( true ); config.setImplVprintCutCk( true ); config.setImplSevcDoaCk( true ); mEngine = AILocalSignalAndWakeupEngine.createInstance(); mEngine.init(config, new AILocalSignalAndWakeupListenerImpl()); } @Override protected void onDestroy() { super .onDestroy(); if (mEngine != null ) { mEngine.stop(); mEngine.destroy(); } } private class AILocalSignalAndWakeupListenerImpl implements AILocalSignalAndWakeupListener { @Override public void onInit( int status) { Log.i(TAG, "Init result " + status); if (status == AIConstant.OPT_SUCCESS) { mEt.setText( "初始化成功!" ); // mEngine.setDynamicParam(new String[]{"ni hao xiao le"}, new float[]{0.45f}, new int[]{1}); /*Map<String, Object> dynamicParam = new HashMap<>(); // 动态设置唤醒env dynamicParam.put("env", "words=ni hao xiao le;thresh=0.45;major=1;"); // maxVolumeState 用于设置大音量状态,启用大音量检测功能时,在每次 feed 之前调用,0 表示非大音量,1 表示大音量 dynamicParam.put("maxVolumeState", 0); mEngine.setDynamicParam(dynamicParam); */ AILocalSignalAndWakeupIntent aILocalSignalAndWakeupIntent = new AILocalSignalAndWakeupIntent(); // 设置音频保存路径,会保存原始多声道音频(in_时间戳.pcm)和经过beamforming后的单声道音频(out_时间戳.pcm) // aILocalSignalAndWakeupIntent.setSaveAudioFilePath("/sdcard/speech"); aILocalSignalAndWakeupIntent.setUseCustomFeed( false ); mEngine.start(aILocalSignalAndWakeupIntent); // switchWakeupOnOff(); // readAndFeed(); } else { mEt.setText( "初始化失败!code:" + status); } } @Override public void onError(AIError error) { mEt.append(error.toString() + "\n" ); } @Override public void onWakeup( double confidence, String wakeupWord) { Log.d(TAG, "唤醒成功 " + wakeupWord + " confidence " + confidence); mEt.setText( "唤醒成功 confidence=" + confidence + " wakeupWord = " + wakeupWord); } @Override public void onWakeup(String s) { //do nothing } @Override public void onNearInformation(String s) { //do nothing } @Override public void onDoaResult( int doa) { // sspe 的 doa 并不是唤醒角度,一般不会使用 Log.d(TAG, "sspe 的 doa 并不是唤醒角度,一般不会使用。 doa:" + doa); } @Override public void onReadyForSpeech() { } @Override public void onRawDataReceived( byte [] buffer, int size) { Log.d(TAG, "onRawDataReceived " + size); } @Override public void onResultDataReceived( byte [] buffer, int size, int wakeupType) { Log.d(TAG, "onResultDataReceived " + size + " wakeup_type " + wakeupType); mBfFile.write(buffer); } @Override public void onVprintCutDataReceived( int i, byte [] bytes, int i1) { //do nothing } @Override public void onAgcDataReceived( byte [] bytes, int i) { mAgcFile.write(bytes); //do nothing } @Override public void onInputDataReceived( byte [] bytes, int i) { Log.d(TAG, "onInputDataReceived: bytes " + bytes.length); } @Override public void onOutputDataReceived( byte [] bytes, int i) { Log.d(TAG, "onOutputDataReceived: " + bytes.length); } @Override public void onEchoDataReceived( byte [] bytes, int i) { Log.d(TAG, "onEchoDataReceived: " + bytes.length); } @Override public void onSevcDoaResult( int i) { } @Override public void onSevcNoiseResult(String s) { } @Override public void onMultibfDataReceived( byte [] data, int length, int index) { Log.d(TAG, "onMultibfDataReceived: " ); if (index < BEAMFORMING_CHANNELS) { mMultiFiles.get(index).write(data); } } @Override public void onEchoVoipDataReceived( byte [] data, int size) { mAecVoip.write(data); } } /** * 双麦和环麦支持动态开关唤醒功能 */ public void switchWakeupOnOff() { new Handler().postDelayed( new Runnable() { @Override public void run() { if (mEngine == null ) return ; wakeupSwitch = wakeupSwitch == 0 ? 1 : 0 ; HashMap<String, Object> map = new HashMap<>(); map.put( "wakeupSwitch" , wakeupSwitch); mEngine.setDynamicParam(map); switchWakeupOnOff(); } }, 7000 ); } } |
6.常见问题
6.1.唤醒模型是否支持动态更新唤醒词和阈值?
答:这里首先我们要区分开始多麦唤醒,还是单麦唤醒。
多麦唤醒:唤醒是嵌入sspe,sspe在动态设置参数的时候进行了唤醒内核的start操作,这样符合唤醒内核重新设置唤醒词需要start操作,所以是可以动态更新唤醒词和阈值,普通和e2e都支持;
单麦唤醒:在2.27.0之前支持更新唤醒词和阈值,2.27.0之后只允许更新阈值。这里是因为内核的逻辑更改,在判断唤醒词改变之后,不做start操作直接抛出错误(env need reconfig!),在唤醒词未改变的情况下,阈值依然可以更新。
6.2唤醒大数据上传预唤醒和唤醒音频的逻辑
答:整体上传音频,有两种情况:上传一次和上传两次。整体分一下几种情况:
1)只有预唤醒:只上传预唤醒的音频;
2)只有真实唤醒:上传真实唤醒音频;
3)预唤醒和真实唤醒同时存在:如果间隔时间小于500ms,则上传真唤醒音频,如果间隔时间大于500ms,则真唤醒和预唤醒音频都上传。
6.3唤醒大数据上传音频的长度是多少?
答:3200*35,也就是3.5s的音频,唤醒点前扩3秒,后扩0.5s。
6.4AIWakeupEngine 动态设置setDynamicParam(JSONObject envJson) 格式?
答:JSONObject jsonObject = new JSONObject();
jsonObject.put("env","words=er duo;thresh=0.2;");
6.5多麦唤醒接口回调onWakeup(String),onWakeup(double, String),什么时候调用?
答:onWakeup(String),只要有唤醒数据必回调,
onWakeup(double,String) 依赖于onWakeup(String)中的String数据,如果数据中status 等于0 | 1 | 2 的时候,onWakeup(double,String)回调
更多的接口内容与描述请参阅 javadoc