PHP實(shí)現(xiàn)基于文本的莫斯電碼生成器
介紹
我最近遇到一個(gè)基于輸入文本生成摩斯代碼音頻文件的需求。幾番搜索無(wú)果之后,我決定自己編寫一個(gè)生成器。
因?yàn)槲蚁Mㄟ^(guò)web的方式訪問(wèn)我的摩斯代碼音頻文件,所以我決定采用PHP作為我主要的編程語(yǔ)言。上面的截圖顯示了一個(gè)開(kāi)始生成莫斯代碼的網(wǎng)頁(yè)。 在下載的zip文件中,包含了用于提交文本的網(wǎng)頁(yè)以及用于生成和展現(xiàn)音頻文件的PHP源文件。如果你想測(cè)試PHP代碼,你需要將網(wǎng)頁(yè)和相關(guān)的PHP文件復(fù) 制到啟用了PHP的服務(wù)器上。
對(duì)于許多人來(lái)說(shuō),莫斯代碼就像一些老電影中表現(xiàn)的那樣,就是一些“點(diǎn)”和“橫線”的序列,或者一連串的嗶嗶聲。顯然,如果你想用計(jì)算機(jī)代碼來(lái)生成莫 斯代碼,這樣的了解是遠(yuǎn)遠(yuǎn)不夠的。這篇文章將會(huì)介紹生成莫斯代碼的要素,如何生成WAVE 格式的音頻文件,以及如何用PHP將莫斯代碼轉(zhuǎn)化成音頻文件。
莫斯代碼
莫斯代碼是一種文本編碼方式。它的優(yōu)點(diǎn)是編碼方便,而且用人耳就能夠方便的解碼。本質(zhì)上,是通過(guò)音頻(或者無(wú)線電頻)的開(kāi)和關(guān),從而形成或短或長(zhǎng)的 音頻脈沖,一般稱作點(diǎn)(dot)和線(dash),或者用無(wú)線電術(shù)語(yǔ)稱作“嘀”和“嗒”。用現(xiàn)代數(shù)字通信術(shù)語(yǔ),莫斯代碼是一種振幅鍵控 (amplitude shift keying ,ASK)。
在莫斯代碼中,字符(字母,數(shù)字,標(biāo)點(diǎn)符號(hào)和特殊符號(hào))被編碼成一個(gè)“嘀”和“嗒”的序列。所以為了把文本轉(zhuǎn)化成莫斯代碼,我們首先要確定如何來(lái)表 示“嘀”和“嗒”。一個(gè)很顯然的選擇就是,用0表示“嘀”,用1表示“嗒”,或者反過(guò)來(lái)。不幸的是,莫斯代碼采用的是可變長(zhǎng)編碼方案。所以我們也必須要使 用一種可變長(zhǎng)序列,或者采取一種方式,把數(shù)據(jù)打包成一種計(jì)算機(jī)內(nèi)存通用的固定位寬(fixed bit-size)的格式。另外,需要特別注意的是,莫斯代碼并不區(qū)分字母大小寫,而且對(duì)一些特殊符號(hào)無(wú)法編碼。在我們這個(gè)實(shí)現(xiàn)中,未定義的字符和符號(hào)將 會(huì)被忽略。
在這個(gè)項(xiàng)目中,內(nèi)存占用并不是一個(gè)需要特別考慮的問(wèn)題。所以,我們提出一個(gè)簡(jiǎn)單的編碼方案,即用“0”來(lái)表示每個(gè)“嘀”,用“1”來(lái)表示每個(gè)“嗒”,并且把他們放在一個(gè)字符串關(guān)聯(lián)數(shù)組中。定義莫斯代碼編碼表的PHP代碼就像下面這樣:
- $CWCODE = array ('A'=>'01','B'=>'1000','C'=>'1010','D'=>'100','E'=>'0',
- 'F'=>'0010','G'=>'110','H'=>'0000','I'=>'00','J'=>'0111',
- 'K'=>'101','L'=>'0100','M'=>'11','N'=>'10', 'O'=>'111',
- 'P'=>'0110','Q'=>'1101','R'=>'010','S'=>'000','T'=>'1',
- 'U'=>'001','V'=>'0001','W'=>'011','X'=>'1001','Y'=>'1011',
- 'Z'=>'1100', '0'=>'11111','1'=>'01111','2'=>'00111',
- '3'=>'00011','4'=>'00001','5'=>'00000','6'=>'10000',
- '7'=>'11000','8'=>'11100','9'=>'11110','.'=>'010101',
- ','=>'110011','/'=>'10010','-'=>'10001','~'=>'01010',
- '?'=>'001100','@'=>'00101');
需要注意的是,如果你特別在意內(nèi)存占用的話,上面的代碼可以解釋為位(bit)。給每個(gè)代碼增加一個(gè)開(kāi)始位,就可以形成一個(gè)位的模式,每個(gè)字符就可以用一個(gè)字節(jié)來(lái)儲(chǔ)存。同時(shí),當(dāng)解析最終編碼的時(shí)候,要?jiǎng)h除開(kāi)始位左邊的位(bit),從而獲得真正的變長(zhǎng)編碼。
盡管許多人沒(méi)有意識(shí)到,事實(shí)上“時(shí)間間隔”是定義莫斯代碼的主要因素,所以理解這一點(diǎn)是生成莫斯代碼的關(guān)鍵。所以,我們要做的第一件事,就是定義莫 斯代碼的內(nèi)部碼(即“嘀”和“嗒”)的時(shí)間間隔。為了方便起見(jiàn),我們定義一個(gè)“嘀”的聲音長(zhǎng)度為一個(gè)時(shí)間單位dt,“嘀”和“嗒”之間的間隔也是一個(gè)時(shí)間 單位dt;定義一個(gè)“嗒”的長(zhǎng)度為3個(gè)dt,字符(letters)之間的間隔也是3個(gè)dt;定義單詞(words)之間的間隔是7個(gè)dt。所以,總結(jié)起 來(lái),我們的時(shí)間間隔表就像下面這樣:
項(xiàng)目 |
時(shí)間長(zhǎng)度 |
嘀 |
dt |
“嘀”/“嗒”之間的間隔 |
dt |
“嗒” |
3*dt |
字符之間的間隔 |
3*dt |
單詞之間的間隔 |
7*dt |
在莫斯代碼中,編碼聲音的“播放速度”通常用 單詞數(shù)/分鐘(WPM) 來(lái)表示。由于英文單詞有不同的長(zhǎng)度,而且字符也有不同數(shù)量的“嘀”和“嗒”,所以,從WPM轉(zhuǎn)化成(音頻)數(shù)字采樣并不是看上去那樣簡(jiǎn)單。在一份被國(guó)際組 織采用的方案中,采用5個(gè)字符作為單詞的平均長(zhǎng)度,同時(shí),一個(gè)數(shù)字或標(biāo)點(diǎn)符號(hào)被當(dāng)做2個(gè)字符。這樣,平均一個(gè)單詞就是50個(gè)時(shí)間單位dt。這樣,如果你指 定了WPM,那么我們總的播放時(shí)間就是 50 * WPM的時(shí)間單位/分鐘,每個(gè)“嘀”(即一個(gè)時(shí)間單位dt)的長(zhǎng)度等于1.2/WPM秒。這樣,給出一個(gè)“嘀”的時(shí)間長(zhǎng)度,其他元素的時(shí)間長(zhǎng)度很容易就能 夠計(jì)算出來(lái)。
你可能已經(jīng)注意到,在上面顯示的網(wǎng)頁(yè)中,對(duì)于低于15WPM的選項(xiàng),我們使用了“Farnsworth spacing”。那么這個(gè)“Farnsworth spacing”又是個(gè)什么鬼?
當(dāng)報(bào)務(wù)員學(xué)習(xí)用耳朵來(lái)解碼莫斯代碼的時(shí)候,他就會(huì)意識(shí)到,當(dāng)播放速度變化的時(shí)候,字符出現(xiàn)的節(jié)奏也會(huì)跟著變化。當(dāng)播放速度低于10WPM的時(shí)候,他 能夠從容的識(shí)別“嘀”和“嗒”,并且知道發(fā)送的哪個(gè)字符。但是當(dāng)播放速度超過(guò)10WPM的時(shí)候,報(bào)務(wù)員的識(shí)別就會(huì)出錯(cuò),他識(shí)別出來(lái)的字符會(huì)多于實(shí)際的 “嘀”和“嗒”。當(dāng)一個(gè)學(xué)習(xí)的時(shí)候習(xí)慣低速莫斯代碼的人,在處理高速播放代碼的時(shí)候,就會(huì)出現(xiàn)問(wèn)題。因?yàn)楣?jié)奏變了,他潛意識(shí)的識(shí)別就會(huì)出錯(cuò)。
為了解決這個(gè)問(wèn)題,“Farnsworth spacing”就被發(fā)明出來(lái)了。本質(zhì)上來(lái)講,字母和符號(hào)的播放速度依然采取高于15WPM的速度,同時(shí),通過(guò)在字符之間插入更多的空格,來(lái)使整體的播放 速度降低。這樣,報(bào)務(wù)員就能夠以一個(gè)合理的速度和節(jié)奏來(lái)識(shí)別每個(gè)字符,一旦所有的字符都學(xué)習(xí)完畢,就可以增加速度,而接收員只需要加快識(shí)別字符的速度就可 以了。本質(zhì)上來(lái)說(shuō),“Farnsworth spacing”這個(gè)技巧解決了節(jié)奏變化這個(gè)問(wèn)題,使接收員能夠快速學(xué)習(xí)。
所以,在整個(gè)系統(tǒng)中,對(duì)于更低的播放速度,都統(tǒng)一成15WPM。相對(duì)應(yīng)的,一個(gè)“嘀”的長(zhǎng)度是0.08秒,但是字符之間和單詞之間的間隔就不再是3個(gè)dit或者7個(gè)dit,而是進(jìn)行的調(diào)整以適應(yīng)整體速度。
生成聲音
在PHP代碼中,一個(gè)字符(即前面數(shù)組的索引)代表一組由“嘀”、“嗒”和空白間隔組成的莫斯聲音。我們用數(shù)字采樣來(lái)組成音頻序列,并且將其寫入到文件中,同時(shí)加上適當(dāng)?shù)念^信息來(lái)將其定義成WAVE格式。
生成聲音的代碼其實(shí)相當(dāng)簡(jiǎn)單,你可以在項(xiàng)目中PHP文件中找到它們。我發(fā)現(xiàn)定義一個(gè)“數(shù)字振蕩器”相當(dāng)方便。每調(diào)用一次osc(),它就會(huì)返回一個(gè) 從正玄波產(chǎn)生的定時(shí)采樣。運(yùn)用聲音采樣和聲頻規(guī)范,生成WAVE格式的音頻已經(jīng)足夠了。在產(chǎn)生的正玄波中的-1到+1之間是被移動(dòng)和調(diào)整過(guò)的,這樣聲音的 字節(jié)數(shù)據(jù)可以用0到255來(lái)表示,同時(shí)128表示零振幅。
同時(shí),在生成聲音方面我們還要考慮另外一個(gè)問(wèn)題。一般來(lái)講,我們是通過(guò)正玄波的開(kāi)關(guān)來(lái)生成莫斯代碼。但是你直接這樣來(lái)做的話,就會(huì)發(fā)現(xiàn)你生成的信號(hào)會(huì)占用非常大的帶寬。所以,通常無(wú)線電設(shè)備會(huì)對(duì)其加以修正,以減少帶寬占用。
在我們的項(xiàng)目中,也會(huì)做這樣的修正,只不過(guò)是用數(shù)字的方式。既然我們已經(jīng)知道了一個(gè)最小聲音樣本“嘀”的時(shí)間長(zhǎng)度,那么,可以證明,最小帶寬的聲幅 發(fā)生在長(zhǎng)度等于“嘀”的正玄波半周期。事實(shí)上,我們使用低通濾波器(low pass filter)來(lái)過(guò)濾音頻信號(hào)也能達(dá)到同樣的效果。不過(guò),既然我們已經(jīng)知道所有的信號(hào)字符,我們直接簡(jiǎn)單的過(guò)濾一下每一個(gè)字符信號(hào)就可以了。
生成“嘀”、“嗒”和空白信號(hào)的PHP代碼就像下面這樣:
- while ($dt < $DitTime) {
- $x = Osc();
- if ($dt < (0.5*$DitTime)) {
- // Generate the rising part of a dit and dah up to half the dit-time
- $x = $x*sin((M_PI/2.0)*$dt/(0.5*$DitTime));
- $ditstr .= chr(floor(120*$x+128));
- $dahstr .= chr(floor(120*$x+128));
- }
- else if ($dt > (0.5*$DitTime)) {
- // For a dah, the second part of the dit-time is constant amplitude
- $dahstr .= chr(floor(120*$x+128));
- // For a dit, the second half decays with a sine shape
- $x = $x*sin((M_PI/2.0)*($DitTime-$dt)/(0.5*$DitTime));
- $ditstr .= chr(floor(120*$x+128));
- }
- else {
- $ditstr .= chr(floor(120*$x+128));
- $dahstr .= chr(floor(120*$x+128));
- }
- // a space has an amplitude of 0 shifted to 128
- $spcstr .= chr(128);
- $dt += $sampleDT;
- }
- // At this point the dit sound has been generated
- // For another dit-time unit the dah sound has a constant amplitude
- $dt = 0;
- while ($dt < $DitTime) {
- $x = Osc();
- $dahstr .= chr(floor(120*$x+128));
- $dt += $sampleDT;
- }
- // Finally during the 3rd dit-time, the dah sound must be completed
- // and decay during the final half dit-time
- $dt = 0;
- while ($dt < $DitTime) {
- $x = Osc();
- if ($dt > (0.5*$DitTime)) {
- $x = $x*sin((M_PI/2.0)*($DitTime-$dt)/(0.5*$DitTime));
- $dahstr .= chr(floor(120*$x+128));
- }
- else {
- $dahstr .= chr(floor(120*$x+128));
- }
- $dt += $sampleDT;
- }
WAVE格式的文件
WAVE是一種通用的音頻格式。從最簡(jiǎn)單的形式來(lái)看,WAVE文件通過(guò)在頭部包含一個(gè)整數(shù)序列來(lái)表示指定采樣率的音頻振幅。關(guān)于WAVE文件的詳細(xì)信息請(qǐng)查看這里Audio File Format Specifications website。 對(duì)于產(chǎn)生莫斯代碼,我們并不需要用到WAVE格式的所有參數(shù)選項(xiàng),僅僅需要一個(gè)8位的單聲道就可以了,所以,so easy。需要注意的是,多字節(jié)數(shù)據(jù)需要采用低位優(yōu)先(little-endian)的字節(jié)順序。WAVE文件使用一種由叫做“塊(chunks)”的記 錄組成的RIFF格式。
WAVE文件由一個(gè)ASCII標(biāo)識(shí)符RIFF開(kāi)始,緊跟著一個(gè)4字節(jié)的“塊”,然后是一個(gè)包含ASCII字符WAVE的頭信息,最后是定義格式的數(shù)據(jù)和聲音數(shù)據(jù)。
在我們的程序中,第一個(gè)“塊”包含了一個(gè)格式說(shuō)明符,它由ASCII字符fmt和一個(gè)4倍字節(jié)的“塊”。在這里,由于我使用的是普通脈沖編碼調(diào)制 (plain vanilla PCM)格式,所以每個(gè)“塊”都是16字節(jié)。然后,我們還需要這些數(shù)據(jù):聲道數(shù)、聲音采樣/秒、平均字節(jié)/秒、一個(gè)區(qū)塊(block)對(duì)齊指示器、位 (bit)/聲音采樣。另外,由于我們不需要高質(zhì)量立體聲,我們只采用單聲道,我們使用 11050采樣/秒(標(biāo)準(zhǔn)的CD質(zhì)量音頻的采樣率是 44200 采樣/秒)的采樣率來(lái)生成聲音,并且用8位(bit)保存。
最后,真實(shí)的音頻數(shù)據(jù)儲(chǔ)存在接下來(lái)的“塊”中。其中包含ASCII字符data,一個(gè)4字節(jié)的“塊”,最后是由字節(jié)序列(因?yàn)槲覀儾捎玫氖?位(bit)/采樣)組成的真實(shí)音頻數(shù)據(jù)。
在程序中,由8位音頻振幅序列組成的聲音保存在變量$soundstr中。一旦音頻數(shù)據(jù)生成完畢,就可以計(jì)算出所有的“塊”大小,然后就可以把它們 合并在一起寫入磁盤文件中。下面的代碼展示了如何生成頭信息和音頻“塊”。需要注意的是,$riffstr表示RIFF頭,$fmtstr表示“塊”格 式,$soundstr表示音頻數(shù)據(jù)“塊”。
- $riffstr = 'RIFF'.$NSizeStr.'WAVE';
- $x = SAMPLERATE;
- $SampRateStr = '';
- for ($i=0; $i<4; $i++) {
- $SampRateStr .= chr($x % 256);
- $x = floor($x/256);
- }
- $fmtstr = 'fmt '.chr(16).chr(0).chr(0).chr(0).chr(1).chr(0).chr(1).chr(0)
- .$SampRateStr.$SampRateStr.chr(1).chr(0).chr(8).chr(0);
- $x = $n;
- $NSampStr = '';
- for ($i=0; $i<4; $i++) {
- $NSampStr .= chr($x % 256);
- $x = floor($x/256);
- }
- $soundstr = 'data'.$NSampStr.$soundstr;
總結(jié)和評(píng)論
我們的文本莫斯代碼生成器目前看起來(lái)還不錯(cuò)。當(dāng)然,我們還可以對(duì)它做很多的修改和完善,比如使用其他字符集、直接從文件中讀取文本、生成壓縮音頻等等。因?yàn)槲覀冞@個(gè)項(xiàng)目的目的是使其能夠在網(wǎng)絡(luò)上方便的使用,所以我們這個(gè)簡(jiǎn)單的方案,已經(jīng)達(dá)到我們的目的了。
當(dāng)然,一如既往的,希望大家對(duì)這些簡(jiǎn)單粗暴的代碼提出建議。這些年來(lái)雖然一直有人在教我,但我還是缺乏莫斯代碼相關(guān)背景知識(shí),所以,如果出現(xiàn)任何的錯(cuò)誤或遺漏都算是我的錯(cuò)。
譯文鏈接:http://www.codeceo.com/article/php-morse-code-generation.html
英文原文:Morse Code Generation from Text