HTML5實現DTMF(電話撥號按鍵訊號)解碼、編碼,程式碼簡單易於移植

xiangyuecn發表於2020-06-28

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程式碼,移植到別的語言非常方便。

涉及到三個原始碼,個個小巧:

  1. FFT:lib.fft.js 111行(程式碼+空行+註釋)
  2. DTMF解碼:dtmf.decode.js 192行(程式碼+空行+註釋)
  3. 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 的頻率訊號,相當於441008000多了 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混音。


最後來個動圖收尾吧:

= 完 =

相關文章