前端信号处理(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
