ZX Spectrum 向けのデモシーンイントロを書く
レトロ風の crack intro を作るためのツール、テクニック、課題。
日本語
コピー

今すぐダウンロード · pouet.net で見る · Hacker News で議論する
はじめに

80年代末、私は ZX Spectrum の前に(とても口に出せないほど長い時間)張りついて、雑誌を見ながら Sinclair BASIC のプログラムや初歩的なアセンブリのルーチンを打ち込み、友人の家で見た Amiga の crack intro を再現しようとしていた。当然、出来上がったものは足元にも及ばなかった。それから数十年、ついにやってのけた。手書きのレトロな demoscene intro で、純正の 128Kb Spectrum 上で動く。80年代末のあの手口は全部入っている。滑らかに流れるスクロールテキスト(「leet」なカスタムフォント付き)、3チャンネルの AY サウンド、跳ねる VU メーター、そしてお約束の視差スクロールする星空。現在のコードは10287バイト、完全なテープイメージは19552バイト。昨年12月13日から今日まで作業してきたので、平均すると週に約455バイトしか書いていない計算になる。情けない話だ。
この記事では、どうやって作ったか、途中で拾ったいくつかの便利なテクニック、そして Speccy と Z80 アセンブリについて学んだ(そして学び直した)すべてを書く。自分の中でも特に好きな副業プロジェクトのひとつであり、同時にモニターに足を突っ込みたくなる回数が最も多いプロジェクトでもある。その辺の感想は最後に回す。今すぐ完成品を見たいなら、ダウンロードボタンは記事の冒頭にある。同梱の .tap テープイメージはどのエミュレータでも読み込めるはずだ。YouTube の録画、pouet.netのリリースページもあり、ブラウザで直接動かすこともできる。私のTNFS サイトのポータルにアクセスして、最近アップロードされたものはオプション4を見てほしい。
巻き戻そう
時間を80年代末に戻す。私は8ビットのテープロード式コンピュータの世界から来ており、Amiga を初めて見たときは異星の造物に出くわしたような気分だった。当時それを見た誰もがそうだったように、映像と音に度肝を抜かれた。画期的なゲームの数々は言うまでもない。しかし本当に私を夢中にさせたのは、海賊版ゲームの冒頭にあった謎めいた(そして最高にクールな)「intro」だった。
これらのオープニング画面はソフトウェア海賊グループが作ったもので、彼らこそがゲームのコピープロテクトを破り、フロッピーにコピーして配布していた連中だ。作品そのものが小さな芸術だった。音楽、グラフィック、そしてたいていはグループの実力を誇示するための、いかにもクールなエフェクトがひとつ。YouTube にはこうしたオープニング(「cracktro」とも呼ばれる)のかなり網羅的なアーカイブがある。
いくつか眺めれば、ほとんどすべての cracktro に共通する「デザイン」の定型がすぐに分かる。画面上部にクールでノリのいいロゴ(血が滴っていたり、「メタル」な雰囲気だったりすればなお良い)、その下に何らかのエフェクト(たいていは視差スクロールする星空か回転する 3D オブジェクト)の上に重ねられた自画自賛のテキスト、そしてさらに下に、挨拶、同じハンドルネームのクラッカーたちへの呼びかけ、そして堂々たる違法 BBS の宣伝が詰まった長いスクロールテキスト。言うまでもなく、80年代から90年代初頭の子供にとって、これはおそらく史上最高にクールなものだった。

初めてこれを見たとき、完璧だと思った。正弦波スクロール、海賊風の BBS 広告、剣をあしらったクールなロゴ、そしてもちろん星空の背景。誰もが好きなものを見つけられる。

この曲はもう頭から離れない。90年代初頭、『Lemmings』はあれほど流行っていたので、このオープニングを知らないクラスメイトはほとんどいなかった。Skid Row は今も活動を続けており、これはおそらく当時を代表する Amiga の cracktro だろう。

Endless Piracy。前衛的。そしてまたしてもクールなロゴ、サイン波のように流れるスクロールテキスト、星空の背景、そして自画自賛のコピー。定番は色あせない。

またひとつ、要素が揃い踏みの佳作だ。かっこいいロゴ、さらにかっこいいフォント、流れる星空、そして下端にはスクロールする一行。レトロ感満載。

Logo。文字。スクロールバー。だが星空の背景はない。それ以外の部分は、Anthrox はかなりしっかり作っている :)
demoscene は、まさにこうした intro から生まれた。クラッキングは次第に表舞台から退き、プログラマー、"graphician"、ミュージシャンがこぞって参加して、尺のある作品を作り、それぞれの腕前を見せ、他のグループと競い合い、マシンを限界まで追い込んだ。驚くべき創意工夫と、純粋な「コードは芸術」という精神によって、この界隈は今も生き続けている。各グループはこれやこれのような息をのむ作品を次々と発表している。どちらも 7MHz の家庭用コンピューター、フロッピードライブと 1Mb のメモリ、そして1985年のサウンドチップで動いているのを忘れてはいけない。
だが、その背後にある純粋な技術と打ち込みには敬意を抱きつつも、私を惹きつけてやまないのは、初期の「シングルスクリーン」なスクロールテキストと星空の cracktro だ。それらは私にとって一種の神秘性を帯びていて、外からしか体験できない界隈と文化の存在をほのめかしていた。だからこそ私は何時間も Sinclair BASIC に浸かって、無駄だと知りながら再現しようとし、いつか自分の Amiga を持つことを夢見たのだ。
過去へ戻る
その後、私は実際に何度もアップグレードを重ね、何台も Amiga を乗り継ぎ、いくつかの「scene」グループにも加わって、さまざまな活動に没頭した。やがて試験と少年時代の終わりが私を呼び戻すまで。その頃は Z80 のコードにも触れ、一時は 68k アセンブリでごまかせる程度には習熟していた。半分は Amiga の黄金期のおかげ、半分は大学の「組み込みシステム」の授業のおかげで、そこでは信号機やエレベーターの制御をプログラムさせられた。だが私の $DAYJOB では、こうしたものの出番はほとんどなく、記憶から薄れていった。

たくさんのプロジェクトを趣味で細々と進めているが、その中でも特にお気に入りなのが TNFS サイトだ。Spectrum 向けの旧式な BBS、いわばインタラクティブな「ウェブサイト」のようなものだ。Sinclair BASIC をどこまで使い倒せるか試す遊びの実験から始まった。Spectrum に挿す現代的なネットワークカードに依存していて、それがきっかけで TNFS プロトコルに詳しいもう一人、Desertkun と知り合った。彼はいま Speccy 向けに優れた WiFi カードを作っていて、テスト用にとプロトタイプを何枚か気前よく送ってくれた。
彼のハードウェアの腕と低レイヤのコーディング能力には度肝を抜かれた。あるときビデオ通話でお互いの開発環境を見せ合っていて、彼はなぜ自分で Z80 アセンブリを書かないのかと聞いてきた。何気ない一言だったが、そこで挑戦が成立した。サイト全体をアセンブリで書き直す気にはとてもなれないが、それなら自己完結したオープニング画面を作って宣伝すればいい。初期の「BBStros」を模したスタイルで、ついにクールな奴らの仲間入りをして、自分の scene 作品を持てるわけだ :)
どれほど難しいというのか?(ああ、これは伏線だ!)
開発環境
作業を通じて開発環境はいたってシンプルで、TNFS サイトを作るときと同じものを使った。ビルドは Makefile で回し、コードは Pasmo アセンブラに渡し、BASIC ファイルはフォークした zmakebas で生成し、テストは FUSE エミュレータで行う。最終確認は実機で、.tap ファイルをローカルの TNFS サーバーにアップロードすれば +3で動かせる。

いくつか個別に触れておきたいツールがある。結果的にこれらが大きな差を生んだからだ。ひとつは Z80 の命令を調べていて見つけたウェブ版の「asm-editor」で、これが非常に使いやすい。Z80 の命令リファレンスが一通りそろっているうえにエディタが付いていて、サンプルコードを実行しながらメモリやレジスタがどう変化するかを観察できる。djnz 命令なら、アセンブルしてループ構造を1命令ずつステップ実行すればいい。あるいはスクリーンショットにある add 命令の例で、ビットがレジスタからあふれたときにキャリーフラグがどう変わるかを見るのもいい。文法が Pasmo と少し違うので、自分のコードはそのまま持ち込めなかった。それでも手動でステップ実行しながらのデバッグではずいぶん助けられた。

最後に、このプロジェクトでおそらく最も重要なデバッグツールとなったもの、ノートとペン、そして2匹の犬(Biscuit と Pebble、よろしく……)。これについては締めくくりでもう少し詳しく書くが、何十年もはるかに抽象度の高い言語と構文を使ってきた自分にとって、これは本当に異例のプロジェクトだった。Z80 のコードの大半は、バイトとビットマスクを紙に描き、レジスタフラグの表を管理し、スクリーンメモリを手作業でブロックごとに描いて、ようやく読めるようになった。夕方に犬を2匹連れて散歩するのは、ラバーダックの練習にちょうどいいと気づいた。もっとも、2匹が延々と続く hl レジスタペアの話をどう思っているかはわからないが。ともあれ、散歩デバッグは強くおすすめする。
出発点
さて、始めよう。まずfuturice.comにある優れた「Hello, world!」の例でビルド環境を確認した。それ以外で最初にやりたかったのは、ロゴと背景画像を読み込んで、のちのオープニング全体の「舞台」にすることだった。この時点では具体的に何を作るか決めていなかった。ロゴを上部に置き、画面最下行をスクロールテキストが流れる、ということだけは決めていた。自分の TNFS サイトのヘッダーはもともと少し demoscene 風だったので、それを素早く作り直し、青い参照用の帯を2本入れて、以降の内容はその間に収めることにした。

メモリマップは breakintoprogram.co.uk より。
Spectrum の SCREEN$ ファイル(たいていはロード画面)は、16384バイト目から始まる6912バイトの画面メモリをそのままダンプしたものにすぎない。この奇妙なレイアウトは後に重要になってくるが、表示するだけならファイルをそのアドレスにロードするだけで済む。Speccy の内部資料を何時間も漁り、あちこちのガイドを突き合わせた末に、私のデモには、その後に続くすべての土台となるごく単純な出発点ができた。
org $8000
screen EQU $4000 ; address of the first byte of screen memory, 16384 in dec
scrlen EQU 6912 ; total length of the screen memory inc. attributes
ula EQU $FE ; I/O port for the ULA to control the border
black EQU 0 ; Spectrum colour number, the exact same ones BASIC uses
start:
ld a,black ; Set the border colour to black using the ULA
out (ula),a
; Copy my logo header picture to screen memory
ld hl,header ; HL = location of my screen file in this code
ld de,screen ; DE = start of screen memory
ld bc,scrlen ; BC = 6912 bytes to copy
ldir ; block copy, see https://jnz.dk/z80/ldir.html
; busy loop - rest of intro goes here :P
hold:
halt ; sleep until the next interrupt (1/50 second)
jr hold ; then jump back and sleep again, for ever
header:
incbin "assets/header.scr" ; my logo header/background
end start

今はまだ地味に見えるが、この単純な出発点からいくつか重要な概念に触れることになった。out 命令で Spectrum のハードウェアを扱うこと(最初は ULA チップ、後に AY サウンドチップ)、Pasmo の INCBIN 命令でバイナリファイルを取り込むこと、Z80 のブロック転送命令 ldir の使い方、レジスタペア による16ビットアドレッシング、そしてラベルへジャンプする単純な分岐だ。
Hello World

次は画面に何かを表示し、Speccy の ROM に用意されたルーチンをいくつか触ってみる。現代の OS でいうシステムコールにほぼ相当するものだ。Spectrum の ROM を逆アセンブルしたものや解説はネット上に大量にあり、コメントとリンクが最も充実しているのは https://skoolkid.github.io/rom/ だ。これを参考に、各種の「Hello, world!」チュートリアルと突き合わせながら、こうした処理がどう組み上がっているのかを理解していった。
at EQU 22 ; "at" control character used to position the cursor
row EQU 11 ; coordinates where to print the message
col EQU 9
prstring EQU $203C ; where the pr_string routine lives in ROM
ld de,msg ; DE = address of the first character
ld bc,msglen ; BC = how many characters to print
call prstring ; print the lot
; busy loop is the same...
msg:
db at,row,col ; equivalent of "PRINT AT 11,9" in BASIC
db "Hello, world!"
msglen EQU $-msg ; 16 bytes in all, pasmo shortcut, $=program counter
結局のところ、コード内でこれらの ROM ルーチンはほとんど使っていない。使わないものを大量に引き連れてくることになるからだ。それでも、どのように動くのかを把握しておいたのは大きな収穫だった。よく使われる書き方をより多く知るきっかけになったからだ。たとえばここで定義されている PR_STRING ルーチンは、文字ごとにループを組む方法を示している。初心者なら、紙とペンを用意してあの8行のルーチンを1行ずつ追い、各レジスタの中身を推測しながらループ構造をたどってみるといい。

asm-editor でも同じものを作ってみたので、ぜひ試してほしい。リンクを開くと、コードはすでに Web エディタに読み込まれている。“Build” を押し、“Step” ボタンで1ステップずつ実行できる。画面に実際に文字が表示されるわけではない(Spectrum ROM の “restart” ベクタの呼び出しは省いてある。文字表示はこれに依存している)が、各レジスタの状態は確認できる。たとえば BC レジスタが、文字列の各バイトを処理するたびに減っていくのが見えるはずだ。
少しスタイルを加える
問題は、Speccy 内蔵のフォントに demoscene らしさのかけらもないことだ。スタイルのあるフォントを使いたいと思って調べてみたら、これが驚くほど簡単だった。検索してすぐに見つかったのが Jim Blimey のフォント集で、すぐに使えるアセンブリファイルと使い方が付属している。説明に従って、あっという間にコードに以下を追加できた:
; CHARS is the address the ROM's printing routine reads to find the font.
; Picking our own font is simply a matter of over-writing this
chars EQU $5C36 ; from Jim's docs - POKE 23607=5C36 in hex
ld hl,myfont-256 ; HL = font address - 256 as per docs
ld (chars),hl ; from now on the ROM draws with it!
; rest of code is the same
myfont:
include "textfont.asm" ; "Cartoon" font from jimblimey.com

来た!これでだいぶ様になってきた。失礼、31337 のことだ。心の準備をしてくれ、そろそろ初心者街を出る……ここまでで1か月ほど試行錯誤して、よくある書き方についてはそれなりに信頼できる理解ができたし、このプロジェクトは案外ちゃんとしたものになるかもしれないと思い始めていた。問題は本物のデモ効果を実装しようとした段階で起きた。この仕組みがどう動いているのか、自分がどれだけ分かっていないかを思い知らされた。ましてこのレベルになると、ハードウェアの細部に直接触れることになる。1980年代初頭、ハードウェア設計者が極端に限られた仕様の中で強いられた設計上のトレードオフも含めて。
厄介な画面レイアウト

最初の悩み(おそらく苦しみの9割はこれ)は、Speccy のきわめて変則的な画面レイアウトだ。より現代的なハードで期待するような標準的なビットマップ表示ではなく、3つの区画に分かれ、各区画はインターリーブされた行の集まりでできている(だから画面アドレスに1を足し続けても、次の行には行き着かず、8行下に飛び、それを繰り返して区画の底に達すると、今度は開始位置の1行下に戻ってくる!)。そのあとに「属性」バイトの塊が続き、8×8ブロックごとの色を決める。属性ブロックがあるせいで、8×8の文字セル1つにつき2色(PAPERとINKの属性)しか使えない。これがあの Spectrum ゲームの目に刺さる「カラー衝突」と独特の見た目を生んでいる。アーティストはグラフィックを8×8ブロックに収めようと腐心する。だが本当に頭を悩ませるのは、ビットマップ部分のレイアウトだ。
ゲームがテープからロード画面を読み込むとき、その過程は画面で見える。各行が交互に積み上がっていき、色やその他の属性(点滅など)は最後にまとめて付く。Break Into Program にはかなり詳しく書かれている。設計者のために言っておくと、当時扱えたハードウェアは極端に限られていたし、Speccy はそもそもゲーム機として設計されたわけではないと思う。バイトを動かし始めると、この方式がテキスト表示に最適化されていることがわかる。テキストは8×8の文字境界にきれいに収まる。たとえばこの「いじった」ビット順のおかげで、画面アドレスを hl レジスタペアに入れておけば、「上位」バイトに1を足すだけで、あるアドレスの次のピクセル行へ素早く飛べる。ほかにも、数ビット動かすだけで済む「楽な操作」がいろいろある:
| 操作 | 命令 |
|---|---|
| 右へ8ピクセル | inc l |
| 左へ8ピクセル | dec l |
| 1ピクセル行下へ | inc h |
| 1ピクセル行上へ | dec h |
| 1文字行下へ | l に32を足す |
大半の時間を BASIC のプログラムリスト表示に費やすであろうマシンとしては、妥当な選択だ。本当に扱いにくいと感じるのは、デモやスプライトのルーチンを書き始めてからだ。つまり、X、Y 座標を対応するメモリアドレスに変換するルーチンがすぐに必要になる。Break Into Program にもそのまま使えるルーチンがあるし、Spectrum のゲームやデモをどれでも逆アセンブルすれば、どこかでこの処理の変種が見つかる。そう、これらは後々すべてとてつもなく厄介になっていく……
スクロールテキスト
派手なロゴと「Hello World」の文字はできた。だが、これでは面白いイントロとは言えない。最初に欲しかったのはスクロールするテキストだ。これがなくては本物のイントロとは呼べない。Amiga にはハードウェアスプライトや blitter 演算、カスタムチップがある。Spectrum には何もない。だから自分でやるしかない。ハードウェアを直接叩く、つまり画面メモリ上でデータを手作業でビット単位に動かし、CPU にすべてを任せるのだ。
Speccy の奇妙な画面とキャラクタセルのレイアウトを理解することが重要になるのは、まさにこのためだ。1つのキャラクタ行にとどまっている限り、巧妙な技がある。RL 命令だ。1バイトを受け取り、左へ「循環シフト」する。キャリーフラグの中身が最右ビットに挿入され、はみ出したビットがキャリーフラグに入る。つまり、この命令を連ねれば、ピクセルの並びを画面上で動かせる。あるキャラクタセルから「こぼれ落ちた」ピクセルはキャリーフラグに入り、次の rl 命令がそれを左隣のキャラクタセルの先頭に挿入する。これを32回繰り返せば、1ピクセル(またはピクセルの並び)が画面の1行分を移動する:
linestart EQU $4880 ; leftmost byte of the line 12 rows down
lineend EQU linestart+31 ; rightmost byte column ($489F)
linelen EQU 32 ; one whole line is 32 bytes (character cells) across
dot EQU %00000001 ; the dot "shape", just one pixel lit at the far right end
newdot:
ld a,dot ; load the dot shape (only rightmost pixel lit) into a
ld (lineend),a ; store it in the rightmost byte of the line, pixel is now lit
frame:
halt ; wait for the next frame, otherwise it whizzes across too fast to see
ld hl,lineend ; hl = last (rightmost) byte of the line
ld b,linelen ; loop counter, 32 bytes to work through
or a ; trick to clear the carry flag, or against itself leaves a register
; unchanged but clears various bits including the carry flag
; see https://jnz.dk/z80/or_r.html
shift:
rl (hl) ; rotate this byte left (00000001 -> 00000010)
dec hl ; point hl at the next byte to the left
djnz shift ; loop construct: b=b-1 and then jump back to shift label until b=0
jr c,newdot ; now we've shifted the whole line, so if the carry flag is set now it means
; the dot has just left the screen, so we jump back to the start
jr frame ; otherwise it is still somewhere on the screen, so keep shifting!
このロジックをもう一段ループで包み、繰り返すたびに h を1ずつ増やす(これで走査線を1本ずつ下へたどれる)。すると文字セル全体が動き出す。ここでは、この仕組みが「Hello world」のメッセージを1行ずつ動かしているのがわかる。ただし、まともなスクロールテキストにして各文字を滑らかに出現させるには、はるかに多くの作業が要る。最初は画面の右端に1文字まるごとを一度に表示していただけだった。動きはするが、見栄えはひどい。新しい文字が画面の右側にいきなり現れ、8ピクセルスクロールしたところで次の文字がまた突然出てくる。欲しいのは各文字が1ピクセルずつ現れること。そのためにはビット操作をもう少しいじる必要がある。最終的に書いたルーチンは、ROM の文字表示ルーチンを呼ばずに、フォントデータを1行ずつ、1ピクセルずつ直接流し込む。
やり方はこうだ。ある文字の ASCII コードを調べ、対応するグリフを8バイトのバッファに読み込む。1行につき1バイトだ。フォントデータはスペース文字(文字コード32)から始まるので、表を引くときは font code base address + (ascii code - 32) * 8 だけでその文字のビット位置にたどり着ける。

フォントデータの文字 H は、8×8のグリッドにビットが並んでいて、こんなふうに見える。行ごとのビットに対して、私はちょっとした小技でその行を左シフトしている。まずバイトを a レジスタに読み込み、それを自分自身に足す(add a,a)と値が左シフトされる(2進数で2を掛けるのは、全体を1桁ずらすことと同じ)。すると描きたいピクセルがちょうどキャリーフラグに収まる。これは Z80 コードでは定番の書き方で、sla 命令より速く、サイズも小さく済む。しかも毎フレーム256バイトを描き直すのだから、節約したサイクルはすぐにばかにならない量になる!

このとき文字バッファはこうなっていて、最上行はすでにずれている。hl レジスタ対が画面上の正しい位置を指しているので、先ほど説明した rl の手法が使える。画面最下行の文字の走査線を左へ1ピクセル分ずらし、キャリーフラグからシフトインする形で右端に自分のピクセルを描き込む。これを32回繰り返せば、行全体が1ピクセル分動き、新しいピクセルが右側に挿入される。

続いて inc h で同じ手順をもう一度繰り返し、走査線を1本下へずらす。これを文字列全体を描き終えるまで続ける。この時点で文字バッファはこうなっていて、画面最下部の行の8本の走査線すべてが、32文字列分のループを回した結果、まとめて1文字分左へシフトしている。このあと全体をもう7回繰り返すと、文字が滑らかに画面へスクロールインしていく。
ここで改めて強調しておきたい。画面下端をメッセージが1本スクロールするだけで、1フレームごとに256ピクセルをループで描き直しているのだ。これは、アセンブリが Spectrum ROM 内蔵の BASIC インタプリタよりどれだけ速いかを物語ってもいる。自分のコードがようやく動き出し、ビットが飛び交い、レジスタが右へ左へと駆け回る様子を想像するのは、私にとって驚きだった。当時遊んだゲームの裏にどれだけの心血が注がれていたのか、身をもって実感した。
音楽
もちろん、良いオープニングには音楽が欠かせない。Amiga の標準フォーマットは「tracker」モジュール形式で、長く曲がりくねった歴史を持ち、数多くの派生やバージョン、改変を生んできた。曲は一連の pattern を組み合わせて作られ、各 pattern は複数行の命令で構成される。命令には通常、音符や音程の情報、再生するサンプル、そして音量やエフェクトなどの各種パラメータが含まれる。なかには曲中のどの位置へジャンプするかを指定する命令もある。多くの意味で、これは音楽のためのプログラミング言語と言っていい。ProTracker を例に取ると、典型的な音符セルは4バイトで、画面上では C-2 01 C20 のような見た目になる。これは三つのフィールドに分かれる。音符とオクターブ(C-2)、サンプル番号(01)、そしてエフェクトとそのパラメータ(C20 はここでは「音量を $20 に設定」、つまり最大値 $40 の半分)だ。

Protracker で曲を再生している画面のクローズアップショット。VU メーターに注目。これも後にさりげなく借用することになる、定番のデモエフェクトだ!
トラッカーでどこまで驚くべきデモが作れるのか、あるいは音符データが画面上でどう見えるのかを知りたければ、h0ffman の作品を見るといい。特にRevision 2014で受賞した「Professional Tracker」がおすすめだ。トラッキングという科学(あるいは芸術!)において彼は認められた達人であり、「Secrets of Protracker」と題した講演では、その可能性がよくまとめられている。
Amiga のサウンドハードウェアは当時の市場の他の何よりもはるかに優れており、それに比べれば ZX Spectrum はずっと貧弱だった。初期の 48K モデルには粗末な「ビーパー」があるだけだったが、後の 128K モデルで3チャンネルの AY-3-8912 サウンドチップが加わった。このチップは当時の8ビットマイクロコンピュータで非常によく使われ、Atari ST もほぼ同一のバリエーションを搭載していた。基本的な波形とサウンド処理しか提供しないが、マルチチャンネルの「チップチューン」を実現するには十分で、ZX Spectrum のデモシーンの中心的存在となった。Spectrum 本体向けにも SoundTracker のクローンがいくつか生まれたが、現在のミュージシャンの多くは Windows でネイティブに動く Vortex Tracker II のようなモダンなツールを好んで使っている。
変換
この intro では、Amiga のモジュール音楽を移植したかった。しかも、ぴったりの曲がもう頭にある。お気に入りの Amiga 音楽ディスクのひとつが、スウェーデンのデュオ Mahoney & Kaktus が1989年に出した『Sounds Of Gnome』だ。Mahoney は NoiseTracker の作者でもある。Amiga で最初に本格的に普及した tracker のひとつで、10代のころ、自分でコンピュータ音楽を作り始めるのに使ったソフトでもある。2018年には、同じディスクに入っていた別の曲をロック風にアレンジして、そのことを Mahoney(本名は「Pex」)に手紙で伝えた。すると彼は demo パーティーで私の「ミュージックビデオ」を流してくれた。今でも本当に感謝している。というわけで、同じコンピレーションからもう1曲持ってきて Spectrum 版にするのは、筋が通っているはずだ……二人とも、すまない! 選んだのは「Glue Master」の『Milkman』。Mahoney の知り合いで、このコンピレーションには何曲か提供している。
この曲は変換に向いている。とにかく短く(pattern は全部で7つだけ)、ほとんどの pattern が同じコード進行を共有していて、元の4チャンネルを Spectrum の3チャンネルに詰めるのも楽そうに見えた。おまけにメロディーが抜群に耳に残るうえ、私はこの曲をよく知っている。35年以上聴いてきたし、ギターやベースで合わせて弾いたりもよくする。変換は最初、かなり順調に進んだ。やることは Amiga mod の pattern を音符ごとに写していくだけで、当時 Windows で使っていた tracker、OpenMPT に mod を開いて作業した。下の画像は、両方の tracker で同じ pattern を開いたセッションの様子だ。左が元の Amiga mod、右が私が作った3チャンネルの Spectrum 版になる。

Spectrum 版の3チャンネル目、11.7 に注目してほしい。Vortex Tracker の構文で「ピッチスライドダウン」を意味する効果で、ピッチをおよそ4分の1音下げる。次の行でもう一度繰り返されるため、全体としては A# から滑り落ちて、続く低い音につながる形になる。元の Amiga mod も独自の構文で同じことをしている。トラック4の下部、同じ音符のところに並んでいる 202 がそれだ。
すべてのパートを同じ「ビープ」音ひとつで鳴らしても、正常に動いていることは十分確認できた。その後数週間、夜の時間を使ってあれこれいじり、編曲し直し、たたき台として作った基本バージョンは、初稿にしては驚くほどよく聴こえた:
特にこの フォーラム投稿には助けられた。Vortex Tracker の入門資料として、おそらくこれが一番いい。ここまで丁寧に書き切ってくれた原作者に感謝したい。

本当に難しかったのは、いかに原曲に近づけるかだった。そのためには各パート向けに音色のよいサンプルライブラリを揃え、Vortex Tracker の「ornaments」を多用する必要がある。Vortex Tracker にはなかなかよい音色が最初から入っている。幸いなことに私は変わり者で、仕事中にバックグラウンドで chiptunes を聴くのが好きだ。だから耳に残るフレーズに出会うと、あとでアイデアを盗むためにメモしておく。Vortex Tracker の sample は Amiga でいう録音済みの音ではない。1フレーム1行で、ある音符がどう鳴るかを定義したものだ。各行にはトーン发生器かホワイトノイズ发生器(スネアなどの効果に便利)を混ぜられ、ほかにもさまざまなパラメータがある。
ornaments を使うと、音符のピッチを追加で制御できる。これが「アルペジオ和音」のような本当に役立つテクニックを可能にする。たとえば 0,4 の ornament は、鳴っている音符とその4半音上の音の間でチャンネルを素早く切り替える(毎秒25回)。切り替えが速すぎて2つの音が同時に鳴っているように聞こえるので、2チャンネルだけで厚みのある三和音を組み立てられる。完全版の冒頭の Cm 和音を例に取ろう。第1チャンネルが C、第2チャンネルが短三度(D#)を鳴らし、そこに第2チャンネル用の ornament を加えて4半音上げ、五度(G)にする。これで和音の3音が揃い、第3チャンネルは他のパートのために空けておける。
限られた3チャンネルを効率よく使えるだけでなく、Spectrum 音楽の象徴的な「ビリビリした」感じも生まれる。レトロな目で見るとかなり魅力的だ。こうしたテクニックに加え、トーンとホワイトノイズを混ぜたサンプル(「ベース+スネア」の合成音色など)を使い、曲全体を3チャンネルに収めることができた。何か月も経った今、最終成果にはとても満足している。
再生
Vortex Tracker の元のサイトは、ドキュメントやその他のリソースがあった場所だが、今は落ちているようだ。必要なものの完全なコピーは archive.org から取得した。必要なのは PTxTools.7z ファイルで、この記事で扱う再生ルーチンが入っている。
更新。Hacker News のユーザーが役に立つ情報を寄せてくれた。「Sergey Bulba のサイトはまだ生きていて、該当する内容は今は https://ay.strangled.net/progr_e.htm で見つかる」。JdeBP に感謝!
再生の仕組みは単純だ。この概要によれば、プレイヤールーチンをインクルードし、自分のモジュールファイルを組み込み、メインループで毎フレーム1回プレイヤールーチンを呼び出すだけでよい。問題は、アーカイブに入っているどのソース版も、私が使っている Pasmo アセンブラでは動かなかったことだ。幸いソースの隣にプリコンパイル済みのバイナリオブジェクトファイルが付属していた(PTxTools/PT3Play ディレクトリの VTII10bG)。これをアドレス $C000 にロードするだけでよい。このアドレスは変更できない。大量の絶対アドレスが埋め込まれているからだ。
モジュール再生を扱う私のコードは現在おおよそ次のとおりで、バイナリモジュールは所定の位置にロード済みだ:
; Define the start address of the player routine and functions we need
player EQU $C000 ; hard-coded org address of binary player
plinit EQU player+3 ; init function in docs, module address in hl
plplay EQU player+5 ; play and move on one frame
; from the docs - this "analyzes PT3 module header and prepares
; corresponding note and volume tables"
ld hl,moduleimg
call plinit
; main loop
hold:
halt
call plplay
call scroll
; other effects / routines go here...
jr hold
moduleimg:
incbin "assets/music.pt3"
星空
次は本題だ。星空のない cracktro は物足りない!
古典的な星空エフェクトに対する私の最初の素朴な実装は、画面にランダムに星を散らし、走査線ごとに下へ移動させ、スクロールテキストで使ったのと同じ rl のテクニックを当てはめるというものだった。行ごとにオフセット量を変えて視差を作るつもりだった。1行目は1ピクセル、2行目は2ピクセル、3行目は3ピクセル、そしてまた1に戻る。これを画面全体にわたって繰り返せば、いくつかの「星野」がそれぞれ違う速度で動いているように見えるはずだ。
ところが、このやり方には大きな問題が2つあった。まず、行全体をシフトすると、その行にある星以外のものまで一緒に引きずられて崩れる。これは予想していたことで、未来の自分への宿題としてメモしておいた。もっと深刻だったのは速度だ。最初の3行を処理し終えるまでに数百回のシフト演算が走り、1フレームに収めるにはまったく足りなかった。画面上のすべてが這うように動き、音楽はひどく遅れて再生され、アルペジオの装飾音が1音ずつ震えながら出てくるのが聞き取れるほどだった。
明らかに、星を1つずつ個別に追跡する必要があった。私はこのルーチンを見つけた。おおむねやりたいことと一致していて、星ごとの状態を構造体で持つというアイデアもここから得た。このルーチンでは画面アドレスの参照テーブルも使われていて、「Break Into Program」のものとよく似ている。
だいぶ進んだ。ただし座標の検索と追跡のコストが大きい(星ごと、フレームごとに走らせなければならず、消すために古い位置も覚えておく必要がある)。しかも、もっと大きな問題からはずっと目を背けていた。星空の上にどうやってものを表示するか、だ。最初に考えたのは単純で、毎フレームまず星を描き、その上に文字をかぶせる。結果は最初の試みと同じで、フレーム予算を完全に吹き飛ばした。
空っぽの星空以上を望むなら、どのセルが文字に占有されているかを記録し、最初からそこには星を描かないようにするしかない。上書きして描き直すのではない。ノートに落書きし、頭の中でビット行を何度も何度もシミュレートした末、かなり図々しく「ズル」をすることで、両方の問題を片付けた。
というわけで今は、ランダムな星空を生成していない。代わりに、あらかじめ計算した Y、X 座標のペアの表を使う:
stdata:
defb 109, 25, 88,182, 42, 96, 155,226
defb 59,254, 137,169, 160, 84, 125,193
; ... and so on, for 32 stars ...
これで RNG と格闘せずに済むし、レイアウトが一度気に入れば、それがずっと気に入ったままでいられることも保証される。さらに「細工」をして、2つの星が同じ y 座標を共有しないようにできる。これで、星が同じ走査線上に落ちて互いを追い越す処理をする、といった問題の一群をまるごと回避できる。
この「ランダム」なレイアウトは、簡単な Ruby スクリプトで生成している:
#!/usr/bin/env ruby
PAIRS = 32
PER_LINE = 4 # pairs per line
Y_RANGE = 40..175 # exclude rows taken up by my logo & bars
X_RANGE = 0..255
raise "y range too small" if Y_RANGE.size < PAIRS
# no duplicate y values
ys = Y_RANGE.to_a.sample(PAIRS)
pairs = ys.map { |y| [y, rand(X_RANGE)] }
pairs.each_slice(PER_LINE) do |row|
body = row.map { |y, x| format('%3d,%3d,', y, x) }.join(' ')
puts "defb #{body.chomp(',')}"
end
ルーチンの初期化時に、これらの座標ペアを各星の状態を記録する表に変換する。毎フレーム位置を保存してスクリーンアドレスに変換するのではなく、各星のレコードは今こんな形になった(基底アドレスからのオフセット):
STSCR EQU 0 ; screen address of the byte the star is in, e.g. $4123
STOCC EQU 2 ; a pointer to the "occupancy check" table cell
STMASK EQU 4 ; the star's bit pattern, e.g. %00010000
STSTEP EQU 5 ; "speed" - num. of pixels this star travels per frame
肝心なのは、X/Y 座標が一切入っていないことだ。星ごとに保存するのは、それが載っているスクリーンバイトと、そのバイト内のビットマスク。つまり、スクリーンへの書き込みに必要な形式そのものになっている。各フレームでアドレス変換をする必要はなく、単純なシフトとループだけで済む。
星を動かすのも極めて簡単になった。rlca でマスクを左にシフトすると、ビット7からあふれたビットがキャリーフラグとともにビット0に戻ってくる。これがまさに、星を1文字セル分左へ動かすための信号であり、前述の「安価な移動」のひとつだ。
各星がどのセルにあるかを記録しているので、衝突判定はほとんどコストがかからない。2つ目のポインタはそのためにある:STOCC(Star Occupancy)は STSCR と同期して動き、各文字セルにつき1バイトの表をたどる。「タイプライター」ルーチンは、テキスト文字かカーソルを含むセルに対応するバイトを1にセットする。だから、ある星が文字に隠れているかどうかは、or 命令1本で判定できる。セルが占有済みなら、その星の描画は飛ばすが、回転と前進は続け、表示できるセルに届くまで進める:
ld l,(ix+STOCC)
ld h,(ix+STOCC+1)
ld a,(hl) ; A = the occupancy check for this cell
or a ; test A against zero
jr nz,stmove ; it's occupied, so leave the screen alone
視差は相変わらず、星ごとに1フレームあたりの移動ピクセル数を変えて実現している。ただし今回のコストははるかに低い。各行のすべてのバイトではなく、星が実際に占めているセルだけを書き換えればいいからだ。
雑記
小ネタがいくつか。単独で節を立てるほどでもないもの……
VU メーター
音に合わせて何かの図形が動くというのは、初期デモでよく見られたもう一つの定番だ。最初はリアルタイムのスペクトラムアナライザを作ることも考えたが、複雑すぎる。1フレームあたりの残りサイクル数で考えてもまず無理だろうし、今の限られた脳みそで考えれば絶対に無理だ! というわけで、簡単な動くレベルバーを並べるだけに落ち着いた。
これに触れる価値があるのは、主にchibiakumasという最高のサイトにリンクする口実ができたからだ! 90年代末のウェブデザインの匂いがたまらない。可読性やアクセシビリティが犠牲になっていると言えなくもないが、今でもこんな見た目のサイトに出くわすのはめちゃくちゃ面白い。明らかに心血を注いだ成果物で、内容も極めて豊富だ。目が耐えられるなら、の話だが。
彼らのMaking Sound with the AY-3-8910というガイドのおかげで、バーを描くのに必要なデータの取り方が分かった。サンプルでは$FFFD にレジスタ番号を書き込んでレジスタを選択していたが、一度レジスタを選んでしまえば、新しい値を書き込まなくてもin命令でその値を読み戻せることに気づいた。あのページにもある通り、R8、R9、R10 には各チャンネルの振幅が入っていて、バーを描くときの高さパラメータにそのまま渡せる。つまりチャンネル A の振幅を取るのはこんなに簡単だ:
ld a,8 ; Channel A register
call ayread
; ... rest of code here ...
ayread:
ld bc,$FFFD ; Register select port
out (c),a
in a,(c)
ret
きらきら星
ページヘッダーのロゴを少しだけ面白くする、単純な小効果だ。元のアイデアがどこから来たのかはよく覚えていない。ただ、80年代末から90年代初頭のデモで似たようなものを見たことは間違いない。ロゴの背景に星の点をランダムにいくつか置いて、それを動かしたかっただけだ。

これは ZXPaintbrush に読み込んだヘッダー画像で、文字セルグリッドがすでに表示されている。幸運なことに、4つの星がそれぞれちょうど1つの文字セル内に収まり、周囲にも余裕があった。そこで中心点から十字を描いて戻すことで、またたくアニメーションを作れる。
これらの点は下の座標に対応しており、狙いは各々が完全に成長した星の一番上の走査線(中心より2ピクセル上)だ:
twkdata:
defb 1,5 ; char row 0, col 5, centre y=3 ink 5
defb 1,20 ; char row 0, col 20, centre y=3 ink 1 bright
defb 26,7 ; char row 3, col 7, centre y=4 ink 4
defb 25,22 ; char row 3, col 22, centre y=3 ink 6 bright
「またたき」ルーチン自体は単純で、2軸それぞれに1ピクセル加えて星を大きくし、数フレーム待ってから元に戻す。面白いのは、またたきの順序をランダムに見せる方法だ。私の星空は Ruby スクリプトが生成したデータテーブルで完全に事前計算されているので、アセンブリでは乱数生成器を一度も使っていない。Jonathan Cauldwell の電子書籍“How To Write Spectrum Games”で、もうひとつ便利な「ハック」を見つけた。Spectrum の ROM の先頭から順にたどり、ランダムに見えるバイトを拾っていくというものだ。彼によれば、ほとんどのゲームでこの手を使っているそうで、やり方はきわめて単純で、ROM の 8Kb 地点にある構造化された繰り返しデータを越えなければいいだけだ。
twkrand:
push hl
ld hl,(twkseed)
ld a,h
and 31 ; keep it within the first 8k of ROM
ld h,a
ld a,(hl) ; the "random" byte
inc hl
ld (twkseed),hl
pop hl
ret
スペースが足りない
オープニングアニメーションを書いている途中で、かなり予想通りの問題にぶつかった。メモリが尽きたのだ。すべてを1つのコードに自己完結させるため、あらゆるリソース(画面背景、player バイナリ、音楽ファイルなど)をプログラム自体に詰め込み、ldir ブロックのコピー命令でそれぞれをあるべき場所へ移していた。つまり各ブロックが二重に保存され、しかも player ルーチンの開始アドレスは $C000 にハードコードされている。あっという間に空きが底をつき、コードが再生ルーチンに食い込み始めた。一番簡単なのは、いくつかの独立したコードブロックに分割し、各ブロックをそれが置かれるべきアドレスへ直接ロードし、それを多段 BASIC テープローダーでまとめることだった:
10 CLEAR 32767
20 BORDER 0: PAPER 0: INK 0: CLS
30 LOAD "" CODE
40 LOAD "" CODE
50 LOAD "" CODE
60 RANDOMIZE USR 32768
このコードはまずメインプログラムをロードし、次にプレイヤールーチンを $C000 へ直接ロードし、最後に背景画面を画面メモリへ直接ロードする。つまり demo が完全にロードされる前に背景が表示され始めるわけだが、ほとんどの人はエミュレータか私の TNFS サイトで動かすだろうから、問題はない。それにこの時点で、さらに深みにはまる危険があった(それでも多色ボーダー効果の作り方は どうしても 知りたい!)。これ以上いじっていたら、これは永遠に公開できなかった。
Z80 の禅

このプロジェクトを振り返ると、なんとも不思議な気分になる。ひとつには、子どもの頃の夢を叶え、自分でも誇りに思えるものを作り上げたということ。今では、バイナリファイルのほぼ1バイト1バイトが何をしているのか、どうやって今の形になったのかを説明できる人はほとんどいない。だがもうひとつは、アセンブリを書かない理由が今ようやく説明できることだ。自分(に残された)正気を大切にしているからだ!問題は、こういうコードを書くには、機械そのもののように考え始めなければならないことにある。レジスタとステータスフラグが次々と切り替わる様子を頭に思い描き、グラフィックはビットが奇妙な順序で詰め込まれ、ループや条件分岐は二進ロジック、といった具合だ。ハードウェアにこれほど密着すると、子どもの頃に触れたハードウェアへの愛着は確かに深まるし、アセンブリプログラマやデモシーナーがこれに夢中になる理由もよくわかる。とはいえ、自分の作品を全部アセンブリで書き直したいとは、まったく思わない!
本当に好きなプロジェクトは、何かを純粋に楽しむために学ぶ口実を与えてくれるもの、そして現代の職場で加速し続けるペースや要求から少しだけ立ち止まらせてくれるものだ。Z80 のコードに戻るのは、ハードウェアの改造に手を出さずに到達できる、おそらく最も遠い地点だった。魅力的なパズルだと思うこともあれば、苛立たしい練習問題だと感じることも多く、いくつかの段階では諦めかけた。このプロジェクトは間違いなく、ある機能の「アイデア」段階から意味のある成果に至るまで、最も時間がかかったものだ。
もちろん、この1年毎日取り組んでいたわけではない。実際にコードを書いていたのは、週に数回、数時間ずつ程度だった。それでもほぼ毎日、このことを考え、あれこれ思案していた(うちの犬が証人だ)。大きな突破口や「あっ!」という瞬間は、たいていキーボードの前ではなく、ノートへの落書きや、別のことをしながらプロセッサの状態をあれこれ考える中から生まれた。
ただ、これを軽々とやってのける開発者たちには、大きな敬意を抱くようになった。80年代の子どもたちやベッドルームプログラマーたちは、なおさらだ。当時はすべて、自分たちの試行錯誤だけで少しずつ解き明かしていったのだから。ここまで来られたのは、大量の書籍、オンラインチュートリアル、サンプルコード付きの GitHub リポジトリ、YouTube の動画……のおかげだ。もし当時、手元に Spectrum とカセットレコーダーとノート1冊しかなかったら、どれだけ時間がかかったか想像もつかない。
というわけで、このプロジェクトに(あくまで一時的に!)区切りをつけるにあたり、h0ffman の言葉を借りて乾杯しよう。「ハッカーとグラフィックアーティスト、そして Protracker に敬意を」
