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混音。
最後來個動圖收尾吧:
= 完 =