DTMF(Dual Tone Multi Frequency) 雙音多頻,由高頻群和低頻群組成,高低頻群各包含4個頻率;兩個頻率波形合成按鍵信號(0-9 * # A B C D)。
SIP中檢測DTMF信號的方法:SIPINFO、RFC2833、INBAND;至於這些是什么我這個外行純屬熱鬧;拿兩個手機互打電話,中途按下的按鍵嘟嘟的聲音就是直接通過話音來傳輸DTMF信號,屬於INBAND(帶內檢測)吧。
拿Adobe Audition打開手機上的電話錄音文件,可以直觀的肉眼看到整齊的DTMF信號,分析一下就能很快GET到此信號的解碼、編碼原理。
在線測試地址:在線測試
【圖1】簡單粗暴合成的PCM信號雜波較多,但和華為手機打出來的錄音信號差不多(他們雜波少點)
一、前言
1.1 HTML5實現DTMF的一些動機
我的GitHub開源庫 Recorder 功能日漸豐富,最近又有項目可能會用到DTMF的解碼功能,所以就用js實現了一下,本着易於移植的目的,相關代碼都是簡單的純js代碼,移植到別的語言非常方便。
涉及到三個源碼,個個小巧:
- FFT:lib.fft.js 111行(代碼+空行+注釋)
- DTMF解碼:dtmf.decode.js 192行(代碼+空行+注釋)
- DTMF編碼:dtmf.encode.js 191行(代碼+空行+注釋)
自評:高性能💪、准確度高💪、誤識別率低💪;歡迎到 在線測試,下載別的一個軟件 dtmf2num(命令行) 來對比傷害一下。
1.2 一些有效場景
(1) 10086
查話費請按1,嘟(你按了一個1),您的話費余額為9億9千萬……不能否認,這些能力的實現是建立在DTMF信號的編解碼之上。
(2) 軟電話
透過某些渠道,比如在你服務器上的程序擁有了自動撥打電話的能力,你希望通過用戶按下某些按鍵后實現一些功能,比如輸入密碼,這樣你的服務器端程序就需要帶上DTMF解碼功能。
(3) 小玩具
寫一些小玩具把玩。嘿哈🙃。
二、DTMF頻率按鍵對照表
低頻群\高頻群 (hz) | 1209 | 1336 | 1477 | 1633 |
---|---|---|---|---|
697 | 1 | 2 | 3 | A |
770 | 4 | 5 | 6 | B |
852 | 7 | 8 | 9 | C |
941 | * | 0 | # | D |
三、DTMF信號解碼 得到按鍵值
3.1 先學會手工解碼
觀察上面【圖1】,一個長的PCM音頻中,每個按鍵信號頻譜中都能清晰的看到兩條非常亮的橫線(對應此頻率的信號能量非常強),Adobe Audition中定位到需要分析的時間位置,然后點擊菜單:窗口->頻率分析(Alt+Z),顯示頻率信息得到兩個最高的頻率;這兩個最高頻率就是上面頻率對照表中的頻率值(取最接近的值):低頻703hz約等於697,高頻1203hz約等於1209,查表可知此信號對應的按鍵為“1”。
3.2 了解一些原理
並非專業,看看就好。
(1) 調整PCM采樣率基本不會干擾到DTMF信號
我說的。因為DTMF信號的最高頻率是1633hz,遠低於常見的8000
(頻率最高4000hz)、44100
(頻率最高22050hz)采樣率對應的最高識別頻率。
(2) 降低采樣率有利於識別DTMF信號
我說的。比如:8000
采樣率就包含了0-4000hz的頻率信號,44100
采樣率包含了 0 - 22050hz 的頻率信號,相當於44100
比8000
多了 4000 - 22050hz 的和DTMF信號無關的頻率,而且是占大頭。多出來的這些頻率最直觀的提現就是增大了計算量(指數級吧)。
以此類推,如果我們將PCM的最高頻率控制在比1633高點,那么將會大幅減少計算量,比如限制最高2000hz頻率,對應的采樣率就是4000
,比8000
還小了一倍,把高頻信號全部切掉,參考下面【圖2】。
(3) 普通話音很難剛好湊成DTMF信號
至少人家是這么說的。剛好有那么一個聲音持續了一段時間,並且這個聲音的最高兩個頻率剛好在DTMF對照表里面,概率不會太高吧。
取決於解碼算法的好壞,同一段音頻,可能有的解碼器會錯誤識別出20個按鍵信號,有的可能只錯誤識別出2個按鍵信號(比如我寫的解碼器,哈😆)。
3.3 實現軟件解碼
軟解碼最直觀的實現就是將【2.1 手工解碼】按順序用程序實現就行了,簡單粗暴,不需要更多的原理和基礎知識。軟解js源碼:dtmf.decode.js
(1) 降低PCM的采樣率
為了減少計算量,和突出DTMF信號的頻率,我們將任何PCM數據的采樣率降低到4000,此時的PCM中包含了 0 - 2000hz 的頻率。可以采用最簡單的重采樣辦法:隔幾個數據抽取一個數據;比如16000采樣率降到4000,每4個采樣取一個即可。此處理性能消耗忽略不計。
【圖2】4000采樣率下兩個頻率就非常突出了(Audition頻譜里面要到右側刻度右鍵降低分辨率,不然4000的采樣率是一坨一坨的頻譜)
(2) 如何找到那兩條橫線
如上面【圖2】中,一個按鍵信號的頻譜中有兩個能量非常強的頻率(很亮的兩條橫線),對應的就是DTMF的低頻和高頻,這兩頻率是會持續一段時間的;因此我們只要發現PCM內存在兩個最強的頻率,並且這兩個頻率在DTMF頻率表中,那么我們就可以假設此時間位置可能有一個DTMF按鍵信號(注意是可能有,並非一定是一個按鍵信號)。
那我們現在只需要計算一下某個時間段內是否有2個最大頻率信號在DTMF頻率表內即可實現判斷;計算方法除了用FFT(快速傅里葉變換)外,更常用的是Goertzel算法,本着入門到放棄的原則,我們采用更通用的FFT來計算頻率,Goertzel就放棄學習了。
似乎FFT運算會帶來性能問題,不過對於短的PCM計算來說,也是可以忽略不計的,並且我們已經降低了采樣率(計算量指數級下降);這里給一個數據:一個4分30秒的mp3進行一次DTMF解碼總消耗的時間300ms不到,共進行了約( 4.5*60 * 1000ms ) / 16ms = 16875
次FFT計算 (其中16ms是下面滑動窗口一次滑動時長距離),fftSize=256。
(3) 用FFT將時域信號轉成頻率信號
FFT又是一個復雜的東西,還好有很多代碼可以借(copy)鑒。參考js代碼:lib.fft.js
FFT需要提供一個fftSize,越大對頻率的分辨率越高,比如fftSize=1024
,分辨率為:4000/1024 = 3.90625hz
(4000是PCM的采樣率)。FFT計算一次后會輸出Int[512]
的數組,數組內第一個點的頻率就是 1 * 3.90625 = 3.90625 hz
,最后一個點的頻率就是 512 * 3.90625 = 2000 hz
;數組內的每個值就是對應頻率的信號強度值(可轉換成分貝),越大信號越強。
但這個分辨率並非越大越好,因為你提供的fftSize越大,每次計算就需要提供同等數量的PCM采樣數據,fftSize=1024
就要提供1024/4000*1000 = 256ms
的PCM數據;這樣問題就產生了:我們單個DTMF信號音的持續時間可能就是 40 - 100 ms,256ms覆蓋的數據區間就太長了甚至可能被覆蓋了兩個按鍵信號也不一定;因此我們要調低分辨率。
調低后的折中結果就是:fftSize=256
,分辨率為4000/256 = 15.625 hz
(相對於 3.90625hz 分辨率降低了4倍),不能再低了,再低分辨率就識別不出信號到底是DTMF頻率表中的哪個值了。此時每次計算需要的PCM數據時長為256/4000*1000 = 64ms
,能夠很好的保證區間內只有一個按鍵信號。
(4) 粗暴的FFT掃盪模式:滑動窗口,不放過任何可能的信號
我們不能簡單的把PCM切分N段(256個采樣為一段),然后每段進行一次FFT計算,這樣會大概率將一個信號拆分到兩段數據中,導致檢測不到這個信號。因此我們計算FFT時應當采用滑動窗口模式,每次將計算窗口往前滑動一點點,這樣就能保證所有的數據都能被至少完整的計算一次。
可以將每次滑動大小設為窗口大小的1/4,即256個采樣為窗口大小,每次FFT計算時往前滑動256 / 4 = 64
個采樣(64/4000*1000 = 16 ms
),這樣就能完美的覆蓋到所有信號,看下面【圖3】。
【圖3】下面這種不停滑動的窗口,能很好覆蓋所有信號區域,缺點就是1次計算要變成4次計算;上面這種雖然只要一次計算,但覆蓋能力太差
(5) 連續出現的相同信號即為有效按鍵
只出現一次的信號不能代表這是一個有效的DTMF按鍵信號,我們累計連續出現3次的相同信號才判定為有效信號。因此我們能夠識別到的最小按鍵音時長為:256/4000*1000 = 64ms
, 64 / 4 = 16 ms
, 16 * (3-0.999999🤔) ≈ 32 ms
。更長的按鍵音時長無限制,因為連續相同的只會算一個按鍵信號。
另外還需要區分兩個按鍵之間的間隙,我們定義累計出現3個以上沒有信號的區域,下一個信號才算新的按鍵信號,這樣就能區分多次按同一個鍵,因此兩個信號理論上最小的間隔時長為:16 * 3 + 16 * 3 = 96 ms
,但實際計結果3次是最小的邊界,按3+1次以上才容錯性更好,最佳間隔應當是16 * 4 + 16 * 4 = 128 ms
以上,意思就是按下一個鍵后,下一個鍵要128ms以后再按(生成信號)。
不停的向后計算,直到PCM結尾,我們就能把所有DTMF信號找出來了,並且我們還能比較准確的轉換出這些信號的位置。然后測試一下:准確度高,誤識別率低,性能還可以,效果很不錯(升職加薪😆)。
四、DTMF信號編碼 生成按鍵PCM音頻信號
並非專業,看看就好。有了解碼的基礎后,來編寫信號生成代碼就簡單的了。我們只要將兩個頻率的波形生成出來,然后合並到一起,再按一定的間隔將多個信號擺放到PCM中即可;實際的代碼也就是按這套邏輯寫的,信號編碼js源碼:dtmf.encode.js
4.1 Mix:兩個音頻信號的混合
不管是生成單個按鍵信號,還是將按鍵信號混合到語音PCM流中,都涉及到信號的混合這種操作,似乎又是一個高深的東西;要 IFFT 計算么?先不管如何復雜,先來一個簡單的混音算法來用的試試看:c = (a+b)/2
就這么簡單粗暴,不過這個線性求平均值合成的聲音雜音頗大。
最后采用 c = a + b - (a * b / ±0x7FFF)
,混音后的音質非常好,來自這篇文章,最終源碼閱讀上面 dtmf.encode.js 中的Mix
函數。
4.2 生成單個按鍵信號
源碼閱讀上面 dtmf.encode.js 中的Recorder.DTMF_Encode
函數。比如要生成“1”鍵的信號,查表得到低頻697 hz
、高頻1209 hz
,然后分別生成兩個頻率的正弦波PCM信號,將兩個PCM用Mix
函數混合到一起即可得到“1”鍵的信號。
這個生成代碼也是出奇的簡單,不過受限於Mix
函數采用的簡單混音算法,兩個頻率正弦波疊加后的雜波有點多,看上面【圖1】兩個最大的頻率兩邊的雜波信號也非常強,不過還好並不影響識別。
4.3 連續多個按鍵信號混合到語音PCM流中
這個才是實際實用的函數:上面 dtmf.encode.js 中的EncodeMix.prototype.mix(pcms,sampleRate,index)
,不管你一次性按下多少個按鍵,混音函數會按部就班的一個一個的混合到語音流中,並且保證按鍵之間的間隔能被解碼程序正確識別。
這個代碼也算簡單,總共做了兩件事:延遲 + 調用Mix
函數,其中Mix調用實際是替換PCM並不是兩個PCM混音。
最后來個動圖收尾吧:
= 完 =