This repository has been archived on 2025-05-21. You can view files and clone it, but cannot push or open issues or pull requests.
mp3-to-text/public/extensions/dtmf.decode.js
2025-04-20 18:46:48 +08:00

268 lines
7.9 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/*
录音 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 信号检查因子取值123默认为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=16ms16ms*(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;idxFloat<pcmData.length;pos++,idxFloat+=stepFloat){
//简单抽样
arr[pos]=pcmData[Math.round(idxFloat)];
};
pcmData=arr;
sampleRate=4000;
var freqStep=sampleRate/bufferSize;//分辨率
var logMin=20;//粗略计算信号强度最小值此值是先给0再根据下面的Math.log(fv)多次【测试】(下面一个log)出来的
/****循环处理所有数据,识别出所有按键信号****/
for(var i0=0; i0+bufferSize<=pcmData.length; i0+=windowSize){
var arr=pcmData.subarray(i0,i0+bufferSize);
var freqs=fft.transform(arr);
var freqPs=[];
var fv0=0,p0=0,v0=0,vi0=0, fv1=0,p1=0,v1=0,vi1=0;//查找高群和低群
for(var i2=0;i2<freqs.length;i2++){
var fv=freqs[i2];
var p=Math.log(fv);//粗略计算信号强度
freqPs.push(p);
var v=(i2+1)*freqStep;
if(p>logMin){
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;i2<vi1;i2++){
var v=freqPs[i2];
if(v && v<pMin){//0不作数
pMin=v;
minI=i2;
};
};
var xMax=(p0-pMin)*0.5//允许幅度变化最大值
//V左侧下降段
var curMin=p0;
for(var i2=vi0;isV&&i2<minI;i2++){
var v=freqPs[i2];
if(v<=curMin){
curMin=v;
}else if(v-curMin>xMax){
isV=0;//下降段检测到过度上升
};
};
//V右侧上升段
var curMax=pMin;
for(var i2=minI;isV&&i2<vi1;i2++){
var v=freqPs[i2];
if(v>=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;i<freqs.length;i++){
var xb=Math.abs(freqs[i]-freq);
if(idxb>xb){
idxb=xb;
if(xb<freqStep*2){//最多2个分辨率内误差
idx=i;
};
};
};
return idx;
};
}));