前端信号处理(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中或者保存音频,需要申请读写权限

<!-- 文件读写需要用到此权限 -->
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />

targetSdkVersion 版本高于30,需要增加权限

<uses-permission android:name="android.permission.MANAGE_EXTERNAL_STORAGE" />
 
if (Build.VERSION.SDK_INT >= 30) {
    if (!Environment.isExternalStorageManager()) {
        Intent intent = new Intent("android.settings.MANAGE_ALL_FILES_ACCESS_PERMISSION");
        startActivity(intent);
    }
}

3.3.混淆

DuiLite SDK混淆文件

三. 引擎运行流程图

 

 

四. 功能使用

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());

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"]
* 还需要设置唤醒词相应的阈值{@link #setThreshold(float[])} 和 {@link #setLowThreshold(float[])}

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 aILocalSignalAndWakeupIntent = new AILocalSignalAndWakeupIntent();
// 设置音频保存路径,会保存原始多声道音频(in_时间戳.pcm)和经过beamforming后的单声道音频(out_时间戳.pcm)
// aILocalSignalAndWakeupIntent.setSaveAudioFilePath("/sdcard/speech");
aILocalSignalAndWakeupIntent.setUseCustomFeed(false);
mEngine.start(aILocalSignalAndWakeupIntent);

详细参数说明AILocalSignalAndWakeupIntent

参数名
取值
说明
是否必须
默认值
setUseCustomFeed(boolean useCustomFeed)

boolean

用户调用 feedData 方法输入音频数据

默认false,使用内部录音机

true 用户调用 feedData 方法输入音频数据

false
setInputContinuousAudio(boolean inputContinuousAudio) boolean

设置是否输入实时的长音频,默认接受长音频为true(如果是一二级唤醒,即每个唤醒词独立且非实时,则需要设置为false,如果不设置会影响性能)
当设置为false时,每次送一段音频段都会给予是否唤醒的反馈,如果没有被唤醒,则抛出wakeupWord:null, confidence:0的信息

 

true

4.3动态设置参数

详细参数说明AILocalSignalAndWakeupListener

参数名

取值

说明

示例

setDynamicParam(Map<String, ?> dynamicParam)

MAP

动态设置唤醒参数,可以在引擎初始化成功后动态设置

Map<String, Object> dynamicParam = new HashMap<>();
ynamicParam.put("env", "words=ni hao xiao le;thresh=0.45;major=1;"); maxVolumeState 用于设置大音量状态,需配置AEC资源。启用大音量检测功能时,在每次 feed 之前调用,0 表示非大音量,1 表示大音量
dynamicParam.put("maxVolumeState", 1);
//动态设置开关,1为开,0为关
dynamicParam.put("wakeupSwitch", 0);
dynamicParam.put("doa", 90);
mEngine.setDynamicParam(dynamicParam);

// 不需要唤醒的话可以关闭唤醒功能,START之前设置
HashMap<String, Object> map = new HashMap<>();
map.put("wakeupSwitch", 0);
localSignalAndWakeupEngine.setDynamicParam(map);

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