FreeBSDでもモールス練習したいぞ

昼時に女房が居ないと、しょうがなしに自炊を強いられる。面倒だけど、腹が減ると 性格が豹変するらしいので、重い腰を上げる事になる。

近頃のマイブームは、Pasco社製の「麦のめぐみ・イングリッシュ・マフィン」だ。 4個入りのやつで、168円。一回で、2個食べるから、一食84円か。

これに、有り合わせのハムかチーズ。大体、一食で50円ぐらいだろうか。後は、 トマトかレタスか、無ければ、私の冷蔵庫と呼んでいるコンビニから、100円相当の サラダを仕入れてくる。

これだけだと、喉が詰まるので、理研のワカメスープ(一食30円)という献立だ。 えっと、これで幾らになるんだ? 264円か。外に食べに行くより安いし、死ぬほど 暑い車(orママチャリ)に乗る事も無いので、体にも家計にも優しいな。

マフィンをトースターで軽く焼くのと、お湯を沸かすのは、パラレルに行なう。 トースターは3分のタイマー、お湯は湧き上がると笛で教えてくれるので、Done Flag が上がってくるまでの間に、ハムを用意したり、皿を用意したりしてればいい。

3回ぐらい、自炊してやっとこの手順を会得したおいらは、コンカレント・プログラミングに向いていないのだろうか? まあ、一度手順が確立してしまえば、終了時間が 予測出来るんで、いいけど。(sakaeの3分間クッキング)

FreeBSDでもモールス可能?

と思って、Windowsで動いてた Haskellのコードを、GHC 6.8.3の環境へ持ってきました。まずは、Ehのモジュールをコンパイルしておくんだな。それが終ったら、morse.hsの コンパイルか。異なる環境でも動く事を確認しておかないと、たまたま動いたのが 正解だったと誤認しちゃいますので。

[sakae@fb ~/mo]$ ghc --make eh.hs
[1 of 1] Compiling Eh               ( eh.hs, eh.o )
[sakae@fb ~/mo]$ ghc --make morse.hs

morse.hs:9:7:
    Could not find module `Eh':
      Use -v to see a list of the files searched for.

あぁー、言わんこっちゃない。何で? ちゃんと、eh.o と eh.hi は、出来上がってるよ 。 しゃーないので、ghcの解説をみてみました。そしたら、ふふふ、-i オプションで、 モジュールのあるdirを指定するんだと。デフォは、今居るdirだとさ。おかしくないかい?

まー、-i の威力を確認してみっか。

[sakae@fb ~/mo]$ ghc -i /home/sakae/mo --make morse.hs

<command line>:
    Could not find module `/home/sakae/mo':
      it is not a module in the current program, or in any known package.

少し、挙動が変わりましたね。で、ヒントっぽいのが出てる。しょうがないので、ヒントに従ってみるか。

[sakae@fb ~/mo]$ ghc -i /home/sakae/mo/eh --make morse.hs
[2 of 2] Compiling Main             ( morse.hs, morse.o )

morse.hs:30:26: Not in scope: `rawSystem'

一歩前進一歩後退で、足踏みしてます。rawSystem って、GHC 6.8.3には無いの?

Prelude> :mo System.Process
Prelude System.Process> :bro

してみましたが、取り扱っていなかったよ。全く、油断も隙もあったもんじゃないな。

こうなったら、方針転換で、同じ関数言語科に属する、別のものにスイッチしちゃうか。この所、出番が無いmatzLispなんてのは、どうだろう。同じ科目だから、悩む事は無いよね。

こうやって、あちこちにほいほいと浮気してると、Haskellのあの人やSchemeのあの方や rubyのあの方に、愛想つかされるよ。いやいや、「汝の妻を平等に愛せよ」の博愛精神が 発露してるだけですから、浮気と違いまっせ。

考えてみたら、何を使うかなんて些細な問題よりも、何の方が問題なんだ。何何何?

それはね、Windows特有なAPIを使いまくりで作っちゃったモールス発信器(mosc)相当 を、FreeBSDへ移植出来るかって事。突き詰めれば、FreeBSDに、Beepとsleepが 有るか、なんだ。

FreeBSDでの調査

調査と言ったら、そりゃ man ですわな。

man beep
名称
       beep, flash - ncurses ベルおよび画面フラッシュルーチン

書式
       #include <ncurses.h>

       int beep(void);
       int flash(void);

はい、却下。sleepの方はどうだ。

man -k sleep
sleep(1)      - suspend execution for an interval of time
sleep(3)      - suspend process execution for an interval measured in seconds
usleep(3)     - suspend process execution for an interval measured in microseconds

こちらの方は、何とか見込みありそうだ。

だけど、beep相当が無いんじゃ、絶望的だあ。こういう時は、殻に籠っていないで、情報の海へと航海してみる事にします。

えっと、検索ワードは、"FreeBSD 音を鳴らす" ぐらいかな?

そしたら、色々な雑音に紛れて、かすかに信号らしきものが飛び込んできました。

echo CDE > /dev/speaker

これで、ドレミ と speaker が、鳴ってくれるそうです。俄には信じられません。

/dev/speaker

でも信じるしかないので、早速、man speaker します。

名称
     speaker, spkr - コンソールスピーカのデバイスドライバ

書式
     pseudo-device speaker
     #include <machine/speaker.h>

解説
     スピーカデバイスドライバは、 FreeBSD が走っている IBM-PC 互換 PC 上で、ア
     プリケーションがコンソールスピーカを制御できるようにします。

     いかなるときでも、このデバイスをオープンできるのはただ 1 つのプロセスだけ
     です。このため、このデバイスのロックと解放には、 open(2) と close(2) を使
     用します。他のプロセスがデバイスを独占している時にオープンしようとすると
     、 EBUSY エラーを示して -1 を返します。デバイスへの書き込みは、 ASCII 文
     字で単純にメロディを表記した演奏文字列 (`play string') として解釈されます
     。 ioctl(2) リクエストによる任意の周波数の発音もサポートされています。

     アプリケーションは、スピーカのファイル記述子に対して ioctl(2) 呼び出しを
     することにより、スピーカドライバを直接制御可能です。 ioctl(2) インタフェ
     ースについての定義は /usr/include/machine/speaker.h にあります。これらの
     呼び出しに使われる tone_t 構造体には、周波数 (ヘルツ) と持続時間 (1/100
     秒単位で) を指定する 2 つのフィールドがあります。周波数 0 は、休符と解釈
     されます。

     演奏文字列の語法は IBM Advanced BASIC 2.0 の PLAY 文の習慣を模倣していま
     す。 PLAY 文の MB, MF, X 要素は時分割環境では役に立たないため除かれます。
     `オクターブ追従' 機能とスラー記号は新しく追加されたものです。

へぇー、面白い機能があるんだ。これを手がかりに調べてみると、機能自身は、カーネルモジュールになっているので、rootになって、kldload speaker して、おく事。/etc/devfs.conf を編集して、一般ユーザーにも使えるようする事、が判明。(実際は、コメントを外すだけ)

スピーカーの試験としては、spkrtest(8) が、提供されてた。実行してみると、プレイリストが出てきて、どれかを選ぶと、曲を演奏してくれた。

って、事は、休符もあるわけで、そこらへんをどうやってるか、spkrtest のソースを 見れば分かるんじゃないかと、期待した。そしたら、shellスクリプトだったよ。 ちょっと悔しいけど、一部を載せておく。

/usr/bin/dialog --title "Speaker test" --checklist \
        "Please select the melodies you wish to play (space for select)" \
        -1 -1 10 \
        reveille "Reveille" OFF \
        contact "Contact theme from Close Encounters" OFF \
        dance "Lord of the Dance (aka Simple Gifts)" OFF \
        loony "Loony Toons theme" OFF \
        sinister "Standard villain's entrance music" OFF \
        rightstuff "A trope from 'The Right Stuff' score by Bill Conti" OFF \
        toccata "Opening bars of Bach's Toccata and Fugue in D Minor" OFF \
        startrek "Opening bars of the theme from Star Trek Classic" OFF \
                2> ${choices} || cleanExit 0
    ...
        toccata)
                title="opening bars of Bach's Toccata and Fugue in D Minor"
                music="msl16oldcd4mll8pcb-agf+4.g4p4<msl16dcd4mll8pa.a+f+4p16g4"
                ;;
        startrek)
                title="opening bars of the theme from Star Trek Classic"
                music="l2b.f+.p16a.c+.p l4mn<b.>e8a2mspg+e8c+f+8b2"
                ;;
        esac
        echo "Title: ${title}"
        echo ${music} > ${speaker}

しょうがないので、dev/speakerのソースを見てみる。

more spkr.c
tone(thz, centisecs)
        unsigned int thz, centisecs;
{

    divisor = timer_freq / thz;

    spkr_set_pitch(divisor);

    /* turn the speaker on */
    ppi_spkr_on();

    /*
     * Set timeout to endtone function, then give up the timeslice.
     * This is so other processes can execute while the tone is being
     * emitted.
     */
    timo = centisecs * hz / 100;
    if (timo > 0)
        tsleep(&endtone, SPKRPRI | PCATCH, "spkrtn", timo);
    ppi_spkr_off();

.....
/**************** PLAY STRING INTERPRETER BEGINS HERE **********************
 *
 * Play string interpretation is modelled on IBM BASIC 2.0's PLAY statement;
 * M[LNS] are missing; the ~ synonym and the _ slur mark and the octave-
 * tracking facility are added.
 * Requires tone(), rest(), and endtone(). String play is not interruptible
 * except possibly at physical block boundaries.
 */

内部発振機の周波数をセットしてから、スピーカーをONし、後は時間になるまで お休み、時間が来たら、スピーカーをOFFしてるだけだ。後、時間待ちのrestなる 関数も定義されてた。 後半部分と言うか、このコードの大部分は、BASICからのMML移植版か。

上のコード中に出てくる hz は、FreeBSDのRTC割り込み頻度だったな。1秒間に100回だった はずだから、周期に直すと 10msになる。ここから、設定出来る時間分解能は、10ms になるんだ。

文房具用OSである、Windowsで、Beepの時間分解能は、1msとなっているけど、CPU負荷に よって、大いにずれる事が観測されてる。メッセージキューに入った仕事の依頼を順番に処理する方式では、前の仕事が長びけば、後のやつは遅れてしまうのはしょうがない。 その点、FreeBSDの方が、ずっと正確っぽいぞ。

いよいよ移植

デバドラのソースまで見てしまったけど、正直 ioctlの使い方の参考になりそうなのは、ありませんでした。しょうがないので、また、情報の海をさ迷ってみると、かすかに、 FreeBSDにも、/usr/ports/audio/beep が、あるよと言う信号を受信できました。 で、カンニングさせて頂きました。答案用紙は、こちらです。

何点頂けますか? エラーチェックサボっているから、ー20点は、免れません。でも、よかれと思って APIを追加したので、+5点。以上が、事故評価です。どうでっしゃろ、裁判員の皆様。

// mosc (Morse generator) for FreeBSD
// ex. mosc  '.- .- | -... ' ( morse code = "aa b" )
// . : dot, - : dash, SPACE : char separator, | : word separator

#include <stdio.h>
#include <stdlib.h>
#include <fcntl.h>
#include <sys/ioctl.h>
#include <dev/speaker/speaker.h>

tone_t tone_description; // freq and duration
int freq = 1000;         // tone freq in  [Hz]
int t    = 10;           // dot length in x10 [MS]
int t3   = 30;           // dash length in x10 [MS]
int t2   = 20;           // char,word separator length in x10 [MS]

set_tn(int t){
  t3 = t * 3;
  t2 = t * 2;
}

dev_con(){
  int fd;
  fd = open("/dev/speaker", O_WRONLY);
  ioctl(fd, SPKRTONE, &tone_description);
  close(fd);
}

tone(int freq, int t){
   tone_description.frequency = freq;
   tone_description.duration  = t;
   dev_con();
}

osc(char *str){
  int c;
  while ( (c = *str++) ){
    switch (c) {
    case '.':             // dot
      tone(freq, t);
      tone(0, t);
      break;
    case '-':             // dash
      tone(freq, t3);
      tone(0, t);
      break;
    case '\'': case ' ':  // char separator
      tone(0, t2);
      break;
    case '!': case '|':   // word separator
      tone(0, t2);
      break;
    default:
      exit(2);            // Bad char, soon exit
      break;
    }
  }
}

main(int argc, char *argv[]){
    if (argc > 3){ freq = atoi(argv[3]); }
    if (argc > 2){ t    = atoi(argv[2]) / 10; set_tn(t); }
    if (argc > 1){ osc(argv[1]); }
    if (argc == 1){
        printf("Usage: mosc '.- |' [speed freq]\n");
        exit(1);
    }
    exit(0);
}