/* 录音 Recorder扩展,DTMF(电话拨号按键信号)解码器,解码得到按键值 使用本扩展需要引入lib.fft.js支持 本扩展识别DTMF按键准确度高,误识别率低,支持识别120ms以上按键间隔+30ms以上的按键音,纯js实现易于移植 使用场景:电话录音软解,软电话实时提取DTMF按键信号等 https://github.com/xiangyuecn/Recorder */ (function(factory){ var browser=typeof window=="object" && !!window.document; var win=browser?window:Object; //非浏览器环境,Recorder挂载在Object下面 var rec=win.Recorder,ni=rec.i18n; factory(rec,ni,ni.$T,browser); }(function(Recorder,i18n,$T,isBrowser){ "use strict"; /* 参数: pcmData:[Int16,...] pcm一维数组,原则上一次处理的数据量不要超过10秒,太长的数据应当分段延时处理 sampleRate: 123 pcm的采样率 prevChunk: null || {} 上次的返回值,用于连续识别 返回: chunk:{ keys:[keyItem,...] 识别到的按键,如果未识别到数组长度为0 keyItem:{ key:"" //按键值 0-9 #* time:123 //所在的时间位置,ms } //以下用于下次接续识别 lastIs:"" "":mute {}:match 结尾处是什么 lastCheckCount:0 结尾如果是key,此时的检查次数 prevIs:"" "":null {}:match 上次疑似检测到了什么 totalLen:0 总采样数,相对4khz pcm:[Int16,...] 4khz pcm数据 checkFactor:3 信号检查因子,取值1,2,3,默认为3不支持低于32ms的按键音检测,当需要检测时可以设为2,当信号更恶劣时设为1,这样将会减少检查的次数,导致错误识别率变高 debug:false 是否开启调试日志 } */ Recorder.DTMF_Decode=function(pcmData,sampleRate,prevChunk){ prevChunk||(prevChunk={}); var lastIs=prevChunk.lastIs||""; var lastCheckCount=prevChunk.lastCheckCount==null?99:prevChunk.lastCheckCount; var prevIs=prevChunk.prevIs||""; var totalLen=prevChunk.totalLen||0; var prevPcm=prevChunk.pcm; var checkFactor=prevChunk.checkFactor||0; var debug=prevChunk.debug; var keys=[]; if(!Recorder.LibFFT){ throw new Error($T.G("NeedImport-2",["DTMF_Decode","src/extensions/lib.fft.js"])); }; var bufferSize=256;//小一点每次处理的时长不会太长,也不要太小影响分辨率 var fft=Recorder.LibFFT(bufferSize); /****初始值计算****/ var windowSize=bufferSize/4;//滑动窗口大小,取值为4的原因:64/4=16ms,16ms*(3-1)=32ms,保证3次取值判断有效性 var checkCount=checkFactor||3;//只有3次连续窗口内计算结果相同判定为有效信号或间隔 var muteCount=3;//两个信号间的最小间隔,3个窗口大小 var startTotal=totalLen; /****将采样率降低到4khz,单次fft处理1000/(4000/256)=64ms,分辨率4000/256=15.625hz,允许连续dtmf信号间隔128ms****/ var stepFloat=sampleRate/4000; var newSize=Math.floor(pcmData.length/stepFloat); totalLen+=newSize; var pos=0; if(prevPcm&&prevPcm.length>bufferSize){//接上上次的数据,继续滑动 pos=windowSize*(checkCount+1); newSize+=pos; startTotal-=pos; }; var arr=new Int16Array(newSize); if(pos){ arr.set(prevPcm.subarray(prevPcm.length-pos));//接上上次的数据,继续滑动 }; for(var idxFloat=0;idxFloatlogMin){ if(fv>fv0 && v<1050){ fv0=fv; p0=p; v0=v; vi0=i2; }else if(fv>fv1 && v>1050){ fv1=fv; p1=p; v1=v; vi1=i2; }; }; }; var pv0 =-1, pv1=-1; if(v0>600 && v1<1700 && Math.abs(p0-p1)<2.5){//高低频的幅度相差不能太大,此值是先给个大值再多次【测试】(下面一个log)得出来的 //波形匹配度:两个峰值之间应当是深V型曲线,如果出现大幅杂波,可以直接排除掉 var isV=1; //先找出谷底 var pMin=p0,minI=0; for(var i2=vi0;i2xMax){ isV=0;//下降段检测到过度上升 }; }; //V右侧,上升段 var curMax=pMin; for(var i2=minI;isV&&i2=curMax){ curMax=v; }else if(curMax-v>xMax){ isV=0;//上升段检测到过度下降 }; }; if(isV){ pv0=FindIndex(v0, DTMF_Freqs[0], freqStep); pv1=FindIndex(v1, DTMF_Freqs[1], freqStep); }; }; var key=""; if (pv0 >= 0 && pv1 >= 0) { key = DTMF_Chars[pv0][pv1]; if(debug)console.log(key,Math.round((startTotal+i0)/sampleRate*1000),p0.toFixed(2),p1.toFixed(2),Math.abs(p0-p1).toFixed(2)); //【测试】得出数值 if(lastIs){ if(lastIs.key==key){//有效,增加校验次数 lastCheckCount++; }else{//异常数据,恢复间隔计数 key=""; lastCheckCount=lastIs.old+lastCheckCount; }; }else{ //没有连续的信号,检查是否在100ms内有检测到信号,当中间是断开的那种 if(prevIs && prevIs.old2 && prevIs.key==key){ if(startTotal+i0-prevIs.start<100*sampleRate/1000){ lastIs=prevIs; lastCheckCount=prevIs.old2+1; if(debug)console.warn("接续了开叉的信号"+lastCheckCount); }; }; if(!lastIs){ if(lastCheckCount>=muteCount){//间隔够了,开始按键识别计数 lastIs={key:key,old:lastCheckCount,old2:lastCheckCount,start:startTotal+i0,pcms:[],use:0}; lastCheckCount=1; }else{//上次识别以来间隔不够,重置间隔计数 key=""; lastCheckCount=0; }; }; }; }else{ if(lastIs){//下一个,恢复间隔计数 lastIs.old2=lastCheckCount; lastCheckCount=lastIs.old+lastCheckCount; }; }; if(key){ if(debug)lastIs.pcms.push(arr); //按键有效,并且未push过 if(lastCheckCount>=checkCount && !lastIs.use){ lastIs.use=1; keys.push({ key:key ,time:Math.round(lastIs.start/sampleRate*1000) }); }; //重置间隔数据 if(lastIs.use){ if(debug)console.log(key+"有效按键",lastIs); lastIs.old=0; lastIs.old2=0; lastCheckCount=0; }; }else{ //未发现按键 if(lastIs){ if(debug)console.log(lastIs) //测试,输出疑似key prevIs=lastIs; }; lastIs=""; lastCheckCount++; }; }; return { keys:keys ,lastIs:lastIs ,lastCheckCount:lastCheckCount ,prevIs:prevIs ,totalLen:totalLen ,pcm:pcmData ,checkFactor:checkFactor ,debug:debug }; }; var DTMF_Freqs = [ [697, 770, 852, 941], [1209, 1336, 1477, 1633] ]; var DTMF_Chars = [ ["1", "2", "3", "A"], ["4", "5", "6", "B"], ["7", "8", "9", "C"], ["*", "0", "#", "D"], ]; var FindIndex=function(freq, freqs, freqStep){ var idx=-1,idxb=1000; for(var i=0;ixb){ idxb=xb; if(xb