モールス信号decoderの製作
暫く避暑を兼ねて田舎に行っていた。もう田舎は秋。いい加減な格好でいると、寒いぐらい だったよ。
帰りに土産で、ララベリージャムと言うのを買ってきた。桑の実のジャムだ。子供の頃、 学校の行き帰りに道草をして、桑の実を食べたなあと、懐かしくなったからだ。桑の 葉っぱはお蚕さまの食べ物、実は子供達のよきオヤツ。
親からは、腹を壊すから、食べちゃいけないと、きつく言い渡されていたものだけど、 よく食べた。家に帰ってきて母に、食べただろうと言われ、食べて無いと言い張っても 歴然とした証拠が残っていて、すぐにバレる。口の中や舌が真っ赤に染色されてしまうのだ。
子供ごころに、バレないように、うがいをしたり、奥歯でそっと咬んでそのまま飲み干す等を したけど、だめだった。そのうち、母は諦めて何も言わなくなった。 あれ? 染色されちゃうやつは、桑の実じゃなくて、ヘビ苺だったかなあ。
肝心のジャムであるが、やたらに甘くて砂糖みたいだったよ。うつろな記憶では、少し 酸っぱくて、ワイルドな味だったような。。。
モールス信号のdecoder
あれから、随分と日が経過しちゃったけど、取りあえず版を書いてみた。 プログラムは、FreeBSD用で、cwd.c と lookup.c の2本1組になる。
まずは、cwd.c の方
// Morse code decoder #include <stdio.h> #include <stdlib.h> #include <math.h> #include <fcntl.h> #include <sys/types.h> #include <sys/uio.h> #include <unistd.h> //#define SOURCE "/dev/dspW" // Morse tone from audio device #define SOURCE "sound.au" // Morse tone from raw file #define DATAN 128 // FFT point # #define MAXELE 10 // max dot dash element (par char) #define DUMP(fmt,val) do{ if (argc == 2 ){ printf(fmt, val); } } while(0) double WaveL[DATAN]; // tW input wave double SpecL[DATAN]; // fA output spectoram abs double cmpXL[DATAN]; // fR output spectoram real part double cmpYL[DATAN]; // fI output spectoram image part void fft(double *tW, double *fA, double *fR, double *fI) { int N = DATAN; int N_2 = N/2; int i,j,k,kp,m,h,P; double mdd; double w1, w2; double t1,t2,s1,s2; double tri[DATAN]; double fReal[DATAN], fImag[DATAN]; // Get P, where N = 2 ** P i = N; P = 0; while (i != 1) { i = i / 2; P++; } // copy real wave into buffer for (i = 0; i < N; i++) { fReal[i] = tW[i]; fImag[i] = 0.0; } // make sin,cos table for ( i = 0; i < N_2; i++ ) { tri[i] = cos( 2 * i * M_PI / N ); tri[i + N_2] = (-1.0) * sin( 2 * i * M_PI / N ); } // sort j = 0; for ( i = 0; i <= N - 2; i++ ) { if (i < j) { t1 = fReal[j]; fReal[j] = fReal[i]; fReal[i] = t1; t2 = fImag[j]; fImag[j] = fImag[i]; fImag[i] = t2; } k = N_2; while (k <= j) { j = j - k; k = k/2; } j = j + k; } // do butterfly for ( i = 1; i <= P; i++ ) { mdd = pow(2.0, (double)i); m = (int)mdd; h = m/2; for ( j = 0; j < h; j++ ) { w1 = tri[j*(N/m)]; w2 = tri[j*(N/m) + N_2]; for( k = j; k < N; k+=m ) { kp = k + h; s1 = fReal[kp] * w1 - fImag[kp] * w2; s2 = fReal[kp] * w2 + fImag[kp] * w1; t1 = fReal[k] + s1; fReal[kp] = fReal[k] - s1; fReal[k] = t1; t2 = fImag[k] + s2; fImag[kp] = fImag[k] - s2; fImag[k] = t2; } } } // fft result copy for output for ( i = 0; i < N; i++ ) { fA[i] = sqrt(fReal[i] * fReal[i] + fImag[i] * fImag[i])/DATAN; fR[i] = fReal[i]; fI[i] = fImag[i]; } } load_wave(double *tW, int fd){ int i; signed short buf[DATAN]; read(fd, &buf, sizeof(signed short) * DATAN); for (i = 0; i < DATAN; i++){ tW[i] = (double)buf[i]; } } main(int argc, char* argv[]){ int count = 491520 / DATAN; // 491520 = samples @ 8000/sec * 60sec int fd; double th = 800.0; // thereshold for 1/0 int dot = 5; // dot length int onoff; // Signal on(1) or off(0) int ms = 0; // mark(1) or space(0) int mcnt = 0; // mark counter int scnt = 0; // space counter char val[MAXELE]; // dot(1),dash(3) int x = 0; // index for val int ws= 1; // word-space print enable fd = open(SOURCE, O_RDONLY); while(count--){ load_wave(WaveL, fd); fft(WaveL, SpecL, cmpXL, cmpYL); onoff = SpecL[10] > th ? 1 : 0; // SpecL[10] is amplitude of 625Hz DUMP("%d",onoff); if (onoff != ms ){ // mark->space or space->mark ms = onoff; if (ms) { mcnt = scnt = 0; ws = 1; } else { scnt = 0; } } if (ms) { mcnt++; } else { scnt++; if ( mcnt && (scnt > dot) ){ // in char val[x++] = mcnt > dot * 2 ? '3' : '1'; mcnt = 0; if (x == MAXELE){ exit(1); } // fatal, broken morce code. } if ( x && (scnt > dot * 2)){ // detect char val[x] = '\0'; printf("%s", lookup(atol(val))); DUMP("%s", "\n"); x = 0; } if ( ws && (scnt > dot * 5) ){ // detect word printf("%s", "_\n"); ws = 0; } } } close(fd); printf("%s", "\n"); exit(0); }
次は、lookup.cだ。
// lookup morse code to char #include <stdio.h> struct kv { long key; char val[8]; }; struct kv tbl[] = { { 13, "A" }, { 3111, "B" }, { 3131, "C" }, { 311, "D" }, { 1, "E" }, { 1131, "F" }, { 331, "G" }, { 1111, "H" }, { 11, "I" }, { 1333, "J" }, { 313, "K" }, { 1311, "L" }, { 33, "M" }, { 31, "N" }, { 333, "O" }, { 1331, "P" }, { 3313, "Q" }, { 131, "R" }, { 111, "S" }, { 3, "T" }, { 113, "U" }, { 1113, "V" }, { 133, "W" }, { 3113, "X" }, { 3133, "Y" }, { 3311, "Z" }, { 33333, "0" }, { 13333, "1" }, { 11333, "2" }, { 11133, "3" }, { 11113, "4" }, { 11111, "5" }, { 31111, "6" }, { 33111, "7" }, { 33311, "8" }, { 33331, "9" }, { 131313, "." }, { 331133, "," }, { 113311, "\?"}, { 31113, "=" }, { 311113, "-" }, { 333111, ":" }, { 133331, "'" }, { 31131, "/" }, { 133131, "@" }, {11111111, "<HH>"}, { 13131, "<AR>" }, { 0, "<\?\?>" } // case not found. (don't delete this line) }; char* lookup(long k){ int i = 0; while(1){ if (tbl[i].key == 0){ return tbl[i].val; } if (tbl[i].key == k){ return tbl[i].val; } i++; } } /******* for test ****************************************************** main(){ printf("%s\n", lookup(atol("3111"))); // "B" printf("%s\n", lookup(atol("33331113"))); // "<??>" } *************************************************************************/
おまけで、Makefile
a.out: cwd.o lookup.o cc -lm lookup.o cwd.o -o a.out cwd.o: cwd.c cc -g -c cwd.c lookup.o: lookup.c cc -g -c lookup.c
設計方針
前回書いたグラフで、モールス信号のある所(以後、マークと呼称)、無い所(スペースと 呼称)の時間を計り、ドット、ダッシュの検出を行う。それを元に、文字の検出、ワードの 抽出を行う事にする。
上で、時間を計ると表現したが、どのくらいの時間分解能が望ましいのだろう? 細かければ 細かいほど、良いに決まっているが、FFTとの兼ね合いもあるので、好きに設定する事は 出来ない。(細かさと、周波数分解能は、反比例する)
FFTをするためには、ある程度の測定データが揃わなければならない。通常は、データの個数を 2のべき乗にする。
たとえば、128個のデータを揃えるには、8000Hzでサンプリングしてるとすると、 128 * (1/8000) となり、これを計算すると、16msとなる。すなわち、16ms待っていると、 1回FFTが出来るデータが揃うのだ。
一方、FFTした時の周波数分解能は、サンプリング周波数/FFTポイント数となる。上の例を 当てはめると、8000/128 = 62.5Hzだ。 今回は、この設定で手を打つ事にした。
なお、8000サンプリング/秒は、FreeBSDの/dev/dspWのデフォルトに引きずられているだけで 他意は無い。(単なる、手抜きです)
上のうらはらの関係は、周波数分解能を上げようとすると、長時間に渡ってデータを採取する 必要があると言う、「ハイゼンベルグの不確定性原理」によるものだ
cwdのデフォルト値について
本当は、プログラム起動時にでも(理想的には、DSCWのように、信号に自動追従)変更 出来るようにすべきだが、面倒になるのでやめた。変更が必要ならソース上で修正して 再コンパイルしてください。
まずは、抽出周波数を固定とした。以前は440Hzを使っていたけど、マイク感度が悪かった ので、気持ち周波数を上げる事にした。600Hzぐらいを中心に検出して欲しいので、600/62.5 = 10 。 これが、 SpecL[10]; の根拠だ。
次は、ドット周期。100msに設定したかったので、100/16 = 6。なのだけど、調整の結果、 int dot = 5; とした。(A1A breakerの出力が正常にdecode出来るように調整したって のが、舞台裏な訳ですよ。)まあ、タイミングの関係で、プラス・マイナス1カウントは 避けられない誤差となりますから、細かい事は、気にしない。
マーク/スペースの判定信号レベル(double th = 800.0;)は、データを見ながら適当に決めた。 他意はありません。
プログラム上重要な定数、FFTのポイント数は、#define DATAN 128 で、決め打。信号を 何処から取り出すかも、#define SOURCE で決め打。今は、ファイルから取っているけど、 リアルにサンプリングしたかったら、/dev/dspW を選んでおいて、main内の while(count--) を、while(1) にすればOK。エラーが無い限り、回り続けるでしょう。
#define MAXELE 10 は、1文字を構成するドットとダッシュの最大個数。訂正符号の "HH" が、ドット8個で一番多いと思うんだけど(和文知らないから嘘かも?)、余裕を 見て、10にしておいた。(実際はnull文字が必要なので9個まで)それを超える場合は、 諦めて、プログラムを強制終了させるようにした。
プログラムの動き
load_wave()で、128個のデータを取ってくる。それをFFTして、該当周波数の信号を抽出。 その信号をスレッショルドで判定して、マーク/スペースを決定。
前のマーク/スペース状態から変化したかを判定、(変更)。マークかスペースどちらかの カウンターを増加。
スペースのカウンターが、ドット分を超えていたら、ドットかダッシュを決定して、配列に 記録。
スペースが、3ドット分なら(プログラム上は、2ドット分以上と言う、緩やかな設定にしてますが)、 1文字を構成するドット・ダッシュが揃った事になる。 ドットは、"1" ダッシュは、"3"として記録してるので、ツー・ト・ト・ト なら、"3111" という文字列になる。
この文字列(符号)を数字に変換してから、lookup を使って検索し文字に変換する。モールスのテーブルは、{3111,"B"} と言うような、構造体をずらずら並べた配列になっている。 検索したい数字と、構造体内の数字が一致したら、文字を返せば良い。
配列の最後には、番兵として数字の0が入っているので、そこまで行っても、一致しなかったら それは、不明な符号なので、 <??> を返す。 後は、それを表示。
ワードを検出したら、スペースを表示するのが正当だろうけど、今回は、分かり易いように、 ("_"後)改行するようにしてみた。
DUMPと言うマクロは、debug用に仕込んだもので、信号のon/off状態を表示します。
結果発表
モールス練習ソフト A1A Breaker を、Tone 600Hz 60Char/分に設定して、初画面のテキストを キーイングさせ、それを1分間録音した。その生録データを、オフラインで解析しました。 まずは、実行時間です。(on VMWARE)
[sakae@nil ~/CW]$ time ./a.out > /dev/null real 0m0.388s user 0m0.046s sys 0m0.323s
0.388/60 = 0.6% っつう事で、まあこんなものかと思いますね。次は、実際の実行結果です。
[sakae@nil ~/CW]$ ./a.out RY_ TIRED_ OF_ SITTING_ BY_ HER_ SISTER_ ON_ THE_ BANK,_ AND_ OF_ HAVING_ NOTHING_ TO_ DO:_
途中からの録音なので、出だしの部分が単語の途中からになっちゃってますが、正しくdecodingさ れている事を確認出来ました。
次は、内部状態も含めて、表示してみます。(debug mode)
[sakae@nil ~/CW]$ ./a.out debug 0011111100000011111111111111111100000011111100000000000R 0000000111111111111111111000000011111100000011111111111111111100000011111111111111111100000000000Y 000000000000000_ 000000000000000011111111111111111100000000000T 000000011111100000011111100000000000I 0000000111111100000011111111111111111100000011111100000000000R 00000011111100000000000E 000000011111111111111111100000011111100000011111100000000000D 000000000000000_ 0000000000000000111111111111111111000000111111111111111111000000111111111111111111100000000000O 000000011111100000011111100000011111111111111111100000011111100000000000F 000000000000000_ 000000000000000011111100000011111100000011111100000000000S 0000000111111100000011111100000000000I 000000011111111111111111100000000000T 000000011111111111111111100000000000T 000000011111100000011111100000000000I 0000000111111111111111111000000111111100000000000N 000000011111111111111111100000011111111111111111100000011111100000000000G 000000000000000_ 000000000000000011111111111111111100000011111100000011111100000011111100000000000B 0000000011111111111111111100000011111100000011111111111111111100000011111111111111111100000000000Y 000000000000000_ 0000000000000000111111000000111111000000111111000000011111100000000000H 000000011111100000000000E 000000011111100000011111111111111111100000011111100000000000R 000000000000000_ .....
ノイズも無い、スピードも一定と言う限定条件ながら、ちゃんと動いているようです。良かった、 良かった。
嗚呼、今気づいたんだけど、このままじゃ、表示文字列がバファリングされちゃって、 リアルタイムに文字が表示されないな。後で何とかしよう。
(9/6 追記)リアルタイム解析をやってみた。心配してたオーバーヘッドも無く、無事に decodeしてくれたよ。上のソースのままだと、やはりバファリングされちゃって、単語が 確定された時(改行が入るので)、づらっと単語が表示された。これは通な人みたいで面白い んだけど、新人にはやはり、ちときつい。fflush(stdout); を、printfの次の行に入れて あげたら新人向けとなった。