morseplayer

Table of Contents

IBM BASIC 2.0's PLAY statement

Q: 演奏データの意味を解説して。

A1: IBM BASIC 2.0の`PLAY`文は、音楽を演奏するためのコマンドで、文字列内の音符やコマンドによって音階やリズムを指定できます。あなたが示した音楽データは、バッハの「トッカータとフーガ ニ短調」を模したメロディが`PLAY`文を使って表現されています。

### 各部分の解説:

  1. `msl16`:
    • 「M」コマンドは音符の長さを指定します。
    • `msl16`はスラー(連続する音符を滑らかに演奏)を適用しつつ、16分音符の長さで音を演奏することを意味します。
  2. `old`:
    • 「O」コマンドはオクターブを指定します。
    • `o`はオクターブ、`l`は音の長さの指定で、`old`は低めのオクターブで演奏されていることを示しています。
  3. `cd4`:
    • `C`と`D`は音符の音階を表します。Cはド、Dはレです。`d4`はDの音を4分音符として演奏することを意味します。
  4. `mll8pcb-agf+4`:
    • `M`コマンドは音符の長さを変更します。`mll8`は8分音符の長さに切り替える指示です。
    • `p`は休符(音を鳴らさない)。
    • `cb-ag`はC、B、A、G音を連続して演奏することを示します。
    • `f+4`はF#(嬰ヘ音)を4分音符で演奏します。
  5. `.g4p4`:
    • `.`は音符の長さを1.5倍にします(付点音符)。
    • `g4`はG音を4分音符で演奏する指示で、`p4`は4分休符を意味します。
  6. `<msl16dcd4mll8pa.a+f+4p16g4`:
    • `<`はオクターブを1つ下げる指示です。
    • その後は、先ほどと同様にD、C、Dの音を演奏し、さらにA、F#を含む音符が続いています。

### 全体のまとめ: このデータは、`PLAY`文によってバッハの「トッカータとフーガ ニ短調」の 一部を再現するためのものであり、オクターブ、音符の長さ、休符などの指示 を含む音楽データです。

A2: このIBM BASIC 2.0の`PLAY`文による演奏データは、Bachの「トッカータとフーガ ニ短調」を再現しようとするもので、`PLAY`文を使用して音楽を再生します。`PLAY`文は、文字列内に音の高さ、音の長さ、オクターブの変更、テンポなどを指定し、コンピュータの内蔵音源で音楽を演奏させるものです。この例では、`msl16oldcd4mll8pcb-agf+4.g4p4<msl16dcd4mll8pa.a+f+4p16g4`という文字列に基づいて音楽が生成されます。それぞれのコマンドが何を意味しているかを説明します。

### コマンドの詳細:

  1. `msl16`: 音符の長さを16分音符に設定します。
  2. `o`: オクターブを指定します。`o`に続く数字がオクターブの番号です。`o`の後の数字(例: `l`, `d`)が音符の高さやオクターブの変更を指示します。
  3. `l`: 長さを指定します。`l8`は8分音符、`l16`は16分音符です。
  4. `c`: 音階の「ド」を意味します。アルファベット(a〜g)はそれぞれ音階(ラからソ)に対応しています。
  5. `d`: 音階の「レ」を表しています。
  6. `p`: 休符を表します。`p4`は4分休符、`p16`は16分休符の意味です。
  7. `b-`: 音階の「シ♭」(Bフラット)を示します。`-`は音程が半音低いことを示しています。
  8. `f+`: 音階の「ファ#」(Fシャープ)を示します。`+`は音程が半音高いことを示しています。
  9. `<`: オクターブを下げる指示です。
  10. `>`: オクターブを上げる指示です。

### 解釈例:

  • `msl16` で16分音符がデフォルトの音符の長さに設定され、`o`でオクターブが指定されています。
  • `d4` は4分音符の「レ」を演奏します。
  • `p4` は4分休符です。
  • `f+4` は4分音符の「ファ#」を演奏します。
  • `<` はオクターブを一つ下げる指示です。

これらのコマンドが組み合わさって、音の高さ、長さ、休符、オクターブの移動などが指示されており、Bachの「トッカータとフーガ ニ短調」の一部が再現されることになります。

まとめると、このデータはBASIC言語を使用してコンピュータで音楽を生成す るための具体的な命令が書かれており、Bachの「トッカータとフーガ」の一部 をシミュレートしているものです。

ChatGPTに質問したら、2つの回答を並列表示してくれたぞ。もったいないので、 そのまま載せてみた。

これが共通化されると MIDI になるんだな。昔、iMacを使っていた時、ローラン ドのキーボードをMIDI接続して、生演奏させたな。懐しい。

どうでもいいけど、 < ってスラーの事、> ってゴーヘィ ってのは、建築重機のクレーンのオペレー タの用語なんだね。NHKの解体キングダムによく出てきていた。

Interpret and play an item from a notation string

/sys/dev/speaker/spkr.c に解釈ルーチンが内蔵されてた。

static int pitchtab[] = {
/*        C     C#    D     D#    E     F     F#    G     G#    A     A#    B*/
/* 0 */   65,   69,   73,   78,   82,   87,   93,   98,  103,  110,  117,  123,
/* 1 */  131,  139,  147,  156,  165,  175,  185,  196,  208,  220,  233,  247,
/* 2 */  262,  277,  294,  311,  330,  349,  370,  392,  415,  440,  466,  494,
/* 3 */  523,  554,  587,  622,  659,  698,  740,  784,  831,  880,  932,  988,
/* 4 */ 1047, 1109, 1175, 1245, 1319, 1397, 1480, 1568, 1661, 1760, 1865, 1975,
/* 5 */ 2093, 2217, 2349, 2489, 2637, 2794, 2960, 3136, 3322, 3520, 3729, 3951,
/* 6 */ 4186, 4435, 4698, 4978, 5274, 5588, 5920, 6272, 6644, 7040, 7459, 7902,
};

playstring(char *cp, size_t slen)
{
                :
                switch (c) {
                case 'A':
                case 'B':
                case 'C':
                case 'D':
                case 'E':
                case 'F':
                case 'G':
                        /* compute pitch */
                        pitch = notetab[c - 'A'] + octave * OCTAVE_NOTES;

                        /* this may be followed by an accidental sign */
                        if (cp[1] == '#' || cp[1] == '+') {
                                ++pitch;
                                ++cp;
                                slen--;
                        } else if (cp[1] == '-') {
                                --pitch;
                                ++cp;
                                slen--;
                        }
			:

morseplayer

前回はFreeBSDのBEEPでモールスした。でも、予告されているように、BEEPは 消えゆく運命にある。ならば、オルターネィティブとしてOpenBSD界では、ど うなっているか調べておきたい。

趣味と実益を兼ねて、BEEPに拘らず、morseを探してみる。デフォでは入いいっ ていないので、portsをガサ入れ

ob$ cd /usr/ports/
ob$ make search key=morse
Port:   morseplayer-1.4p1
Path:   audio/morseplayer
Info:   morse player
Maint:  The OpenBSD ports mailing-list <ports@openbsd.org>
Index:  audio
L-deps:
B-deps:
R-deps:
Archs:  any

HOMEPAGE=    https://github.com/wrigjl/morseplayer    (from Makefile)

幸いな事に、単独なアプリだった。安心して、 pkg_add したよ。manすると、 ちょっとデフォがオイラーの感性と合致してない事が判明。

-d device    use device for audio output instead of the default

-f freq      use freq for the wave frequency instead of the default:
             720.0Hz

-w wpm       use wpm words per minute output; default is 5.

パッチが当ててあるmainの中で、pitch=500 wpm=15 に変更。

ob$ cc morseplayer.c -lm -lsndio
ob$ echo hello CW world | ./a.out

sndio

OpenBSDの音制御は独特なので、簡単に利用できる様にsndioと言うライブラリィ が用意されている。morseplayerのソースもそれを利用しているので、パッチ が提供されてた。以下は、その一部。

/usr/ports/audio/morseplayer/patch-morseplayerc

-                       r = write(pars->sp_fd, e->pl_ptr, res);
-                       if (r == -1)
+                       r = sio_write(pars->hdl, e->pl_ptr, res);
+                       if (r == 0)

-       if (afname == NULL)
-               afname = "/dev/audio";
+       pars.hdl = sio_open(afname, SIO_PLAY, 0);
+       if (pars.hdl == NULL)
+               errx(1, "Could not open sndio device");

-       f = open(afname, O_WRONLY, 0);
-       if (f == -1)
-               err(1, "open %s", afname);
+       sio_initpar(&par);
+       par.rate = 22050;
+       par.sig = 0;
+       par.bits = 8;
+       par.pchan = 1;
+       if (!sio_setpar(pars.hdl, &par))
+               errx(1, "sio_setpar failed");
+       if (!sio_getpar(pars.hdl, &par))
+               errx(1, "sio_getpar failed");

afnameはコマンドラインからの引数。一般のOSに実装されている音システムと の違いが結構有るな。

それから、もっと大事な事は、/usr/bin/sndiod (と、その子分 sndiod: helper (sndiod)) すなわち、サウンドサーバー が裏でrunしてる事だ。走っていないと、

ob$ doas rcctl stop sndiod
sndiod(ok)
ob$ cat AB | ./a.out
a.out: Could not open sndio device
ob$ doas rcctl start sndiod
sndiod(ok)
ob$ cat AB | ./a.out
ob$

sndiod

下記の様に4つのデバイスをサポートしてるみたい。同時使用は可能なのかな?

ob$ ls -l /dev/audio*
crw-rw----  1 root  _sndiop  42,   0 Sep 26 08:46 /dev/audio0
crw-rw----  1 root  _sndiop  42,   1 Apr 13 06:26 /dev/audio1
crw-rw----  1 root  _sndiop  42,   2 Apr 13 06:26 /dev/audio2
crw-rw----  1 root  _sndiop  42,   3 Apr 13 06:26 /dev/audio3
crw-rw----  1 root  _sndiop  42, 192 Apr 13 06:26 /dev/audioctl0
crw-rw----  1 root  _sndiop  42, 193 Apr 13 06:26 /dev/audioctl1
crw-rw----  1 root  _sndiop  42, 194 Apr 13 06:26 /dev/audioctl2
crw-rw----  1 root  _sndiop  42, 195 Apr 13 06:26 /dev/audioctl3

これの元ネタ。

ob$ dmesg | grep az
azalia0 at pci0 dev 27 function 0 "Intel 82801I HD Audio" rev 0x03: msi
azalia0: codecs: Realtek ALC269, Intel/0x2802, using Realtek ALC269
audio0 at azalia0

sample

何はサンプルが欲しい。そこで思い出したのは、音の万能薬soxである。GUI屋 さんなら迷わずに、audadityなんだろうけど。。

ob$ sox -r 8000 -n output.wav synth 3 sine 300-3300
ob$ play output.wav

output.wav:

 File Size: 96.1k     Bit Rate: 256k
  Encoding: Signed PCM
  Channels: 1 @ 32-bit
Samplerate: 8000Hz
Replaygain: off
  Duration: 00:00:03.00

In:100%  00:00:03.00 [00:00:00.00] Out:24.0k [      |      ] Hd:0.0 Clip:0
ob$ aucat -n -i output.wav -h raw -o z.raw

300ー3300Hzまで3秒かけてスウィープする正弦波を発生。それをplayしてみた。 この音源って、ピンクレディーのUFOに挿入されてた未来的な音だな、って 昭和におじさんが、懐かしがってます。 次にそれをaucatでヘッダー無しに変換してみた。

debug

aucatにはデバッグ・フラグが用意されてる。-dを重ねると、より詳細に挙動 を報告してくれる。

ob$ aucat -d -d -d -i output.wav
output.wav: skipped unknown chunk
output.wav,pst=cfg: play, chan 0:0, 8000Hz, s32le, bytes 80..96080, vol 8388608
default: 8000Hz, play 0:0, 33 blocks of 80 frames
output.wav,pst=cfg: allocated 2640 frame buffer
cmap: nch = 1, ostart = 0, onext = 0, istart = 0, inext = 0
dec: s32le, 1 channels
output.wav,pst=ini: chain initialized
output.wav,pst=run: started
started
default: cycle, prime = 33
dec: copying 80 frames
cmap: adding 80 frames
default: cycle, prime = 33
  :
default: cycle, prime = 33
output.wav,pst=stp: drained, done
output.wav,pst=ini: stopped
stopped
output.wav,pst=ini: closed

上でrawに変換した。そしたら、あろう事かファイルが肥大しちゃった。どん な風に変換された?

ob$ aucat -dd -i z.raw
z.raw,pst=cfg: play, chan 0:1, 48000Hz, s24le4msb, vol 8388608
default: 48000Hz, play 0:1, 36 blocks of 480 frames
z.raw,pst=cfg: allocated 17280 frame buffer
cmap: nch = 2, ostart = 0, onext = 0, istart = 0, inext = 0
dec: s24le4msb, 2 channels
z.raw,pst=ini: chain initialized
z.raw,pst=run: started
started
z.raw,pst=run: eof reached, stopping
z.raw,pst=stp: drained, done
z.raw,pst=ini: stopped
stopped
z.raw,pst=ini: closed

ヘッダーが除去されてしまうと、ただのデータだ。これでは演奏も何も有った ものじゃない。そこでaucatはローカルルールを適用するんだな。48Kサンプリ ング、ステレオ、サイン付きの24Bitリトルエンディアン。これ世の中一般の フォーマットなんでしょうか?

ちょいとgdbしてみると、このあたりでグルグルしてる。 playrec_cycle () at aucat.c:1118

sndio hints on writing & porting audio code

こんな資料を発見した。ならば、、、

sample

#include <fcntl.h>
#include <unistd.h>
#include <sndio.h>

#define SZ 9600   // from par.appbufsz
int buf[SZ];

int main(){
  struct sio_hdl *my;
  struct sio_par par;
  char *afname = "default";
  char *sndf   = "z.raw";
  int fd;

        my = sio_open(afname, SIO_PLAY, 0);
        fd = open(sndf, O_RDONLY);

        sio_initpar(&par);
        par.rate = 48000;
        par.sig = 1;
        par.le = 1;
        par.msb = 0;
        par.bits = 24;
        par.pchan = 2;
        par.xrun = 0;
        sio_setpar(my, &par);
        sio_getpar(my, &par);

        sio_start(my);
        while(read(fd, buf, SZ)) {
            sio_write(my, buf, SZ);
        }
        sio_stop(my);
	close(fd);
        sio_close(my);
}

例によって、全くエラーチェックをしていないと言う恐ろしいプログラム。コ ンパイルは、cc -g -O0 sample.c -lsndio

特徴的なのは、パラメータの設定部分。まず、initparして、初期値を準備(全 てが最大値になる)。変更したい部分を設定してから、ネゴシエーションされ た値を再確認(してないけど)。

それからは、スタートして、音ファイルから読んで、それをハードに転送、を、 EOFになるまで繰り返し。stopを呼ぶと、ハードに転送されてるデータが残っ ていたら音出しする。それが完了したら、デバイスを閉じて終了。

aucatのソースを見ると、 sio_pollfd() sio_revents() を駆使して、 bufferへ上手にデータを送り込んでいるようだ。オイラーの奴は、呆れるぐら い手抜きバージョンです。まあ、それがハックですと自慢しておこう。

エラー祭り

どんな挙動をするか、ktrace/kdumpしてみた。

93592 a.out    CALL  read(4,0x3b1d2138,0x2580)
93592 a.out    RET   read 9600/0x2580
93592 a.out    CALL  write(3,0x7360dbf8,0x28)
93592 a.out    RET   write 40/0x28
93592 a.out    CALL  write(3,0x3b1d2138,0x780)
93592 a.out    RET   write 1920/0x780
93592 a.out    CALL  poll(0xcf7f2080,1,INFTIM)
93592 a.out    RET   poll 1
93592 a.out    CALL  read(3,0x7360dbd0,0x28)
93592 a.out    RET   read 40/0x28
93592 a.out    CALL  read(3,0x7360dbd0,0x28)
93592 a.out    RET   read 40/0x28
93592 a.out    CALL  read(3,0x7360dbd0,0x28)
93592 a.out    RET   read -1 errno 35 Resource temporarily unavailable

音ファイルから指示された9600バイトを読み出し、それをハードに転送してる んだけど、事情が有って、全部を転送できない。何度がやってるうちに、poll を使った交通整理が出てきた(sndioのボランティアだな)。そのうちに、整理ができなくなって、OSが文句 を言い出した。

音を聞いてる分には、分からないのだけどね。。。

で、errno 35 なんて言ってるので、sys/errno.h で、#define EAGAIN 35 を取得。そして、read(2) すると、

[EAGAIN]           The file was marked for non-blocking I/O, and no data
                   were ready to be read.

sys_read の中で表われるかと思ったら、出てきていない。もっと下部かな?

ob$ grep EAGAIN -l *.c
kern_fork.c
kern_rwlock.c
kern_sig.c
subr_extent.c
subr_suspend.c
sys_futex.c
sys_pipe.c
sys_process.c
sysv_msg.c
sysv_sem.c
sysv_shm.c
uipc_syscalls.c
vfs_lockf.c

さあ、何処でしょう? それより、open(2)に助け船は無いか?

      O_RDONLY     Open for reading only.
      O_WRONLY     Open for writing only.
      O_RDWR       Open for reading and writing.

Any combination of the following flags may additionally be used:

      O_NONBLOCK   Do not block on open or for data to become available.
      :

わざわざ O_NONBLOCK を付けていないので、ちゃんとブロックするはずだけ どなあ。ググったら、OpenBSD 7.5 で、これがでるようになったって報告が散 見された。何か変更でも有ったのだろうか?

エラー祭 (2)

ob$ hexdump z.raw
 :
01193e0    5a00    5f99    0000    0000    9800    58a8    0000    0000
01193f0    5c00    512a    0000    0000    ad00    492a    0000    0000

触れ込みでは、24Bitって事だけど、実際は32Bitじゃん。それからステレオっ て事だけど、右チャンネルには、音が入いっていない。そんな物なのか。

my = sio_open(afname, SIO_PLAY, 1);
fd = open(sndf, O_RDONLY | O_NONBLOCK);

両ポートをノンブロックングしてみた。

ob$ time ./a.out
    0m03.01s real     0m00.00s user     0m00.02s system
ob$ time ./a.out
    0m00.01s real     0m00.00s user     0m00.00s system

上の結果は、ブロッキングした時としない時のもの。ノンブロッキングだと、 ほとんど直ぐに終了。実際に音システムへの書き込みは、ほとんど実行されて いないので、音はしない結果になった。

次は、32Bitにしてみた。音がより忠実に再生されたぞ。まろやかである。 にわかオーディオ評論家じゃん。プラグは金メッキされた物を使用すると、音 が良くなりますと一緒だな。世の中に32BitのA/Dコンバーターなんて存在する んか? ハルネーション 幻覚である。 ハイレゾオーディオ

今度は、ステレオを止めてモノラルにしてみた。(par.pchan = 1)

ob$ time ./a.out
    0m06.01s real     0m00.00s user     0m00.03s system

当然の事ながら、実行時間は2倍になった。それから、音も低くなった。再生 スピードが遅くなるんで、低温の方へシフトするのは、当然か。

par.le = 0 として、無理にビッグエンディアンとして解釈させると、もはや 雑音ですよ。ピンクレディーの華々しさは消え失せてしまいました。嗚呼、昭 和は遠くなりにけり。

だから、エンディアンにも気を使ってください、とAC JAPAN からのお願いで した。

beep

OpenBSD方面でbeepはどうなっているか? dev/pckbc/pckbd.c

/*
 * Keyboard can't beep directly; we have an
 * externally-provided global hook to do this.
 */
pckbd_bell(d->pitch, d->period, d->volume, 0);

一応スピーカーは存在してる。

ob$ ls -l /dev/sp*
crw-------  1 root  wheel  27, 0 Apr 13 06:26 /dev/speaker
SPKR(4)                      Device Drivers Manual                     SPKR(4)

NAME
     speaker, spkr - console speaker device driver

SYNOPSIS
     spkr0     at pcppi?

DESCRIPTION
     The speaker device driver allows applications to control the built-in
     speaker on machines providing a PCPPI speaker interface.

     Only one process may have this device open at any given time; open(2) and
     close(2) are used to lock and relinquish it.  An attempt to open() when
     another process has the device locked will return -1 with an EBUSY error
     indication.  Writes to the device are interpreted as "play strings" in a
     simple ASCII melody notation.  An ioctl() for tone generation at
     arbitrary frequencies is also supported.

etc