• サウンドドライバを作成する
サウンドドライバの構造と、PCG8100 からの発音の仕組みが分かりましたので、今回は実際にフローチャートに則ってサウンドドライバを作成してみました。
これがサンプルサウンドドライバのワークエリアです。

MML:
.ADDRES equ 0 ; MMLデータアドレス
.REPEAT equ 0 ; MMLデータ初期アドレス
.FREQ equ 2 ; 発音周波数
.LENGTH equ 4 ; 現在の音長カウンタ
.LENDATA equ 5 ; 音長初期値
.SIZE equ 6 ; チャンネルデータサイズ

SND:
.Counter ds 1 ; 汎用カウンタ
.Repeat ds 1 ; BGMリピート再生フラグ
.CH1 ds MML.SIZE ; 1ch
.CH2 ds MML.SIZE ; 2ch
.CH3 ds MML.SIZE ; 3ch
.CH4 ds MML.SIZE ; SE
上記のワークエリアをメモリのどこかに配置します。アライメントによる高速化を利用すると、下記で記載の MMLコマンド解析の inc hl を inc l に出来ます。

SNDTimer:
ld hl, SND.Counter
inc (hl)
全体的な処理部の SNDTimer は、割り込み等の呼び出し先となります。最初に汎用カウンタを +1 します。今回はこの汎用カウンタを直接的に使用する事はありません。

; チャンネル別更新
ld ix, SND.CH1
call SNDPlayer
ld ix, SND.CH2
call SNDPlayer
ld ix, SND.CH3
call SNDPlayer
ld ix, SND.SE
call SNDPlayer
続いて全てのチャンネルに対して更新を行います。サウンド再生が3チャンネルとして、その3チャンネル分全てに対して再生更新処理を行います。加えて効果音発音専用チャンネルも更新します。効果音は発音時に音楽再生の 3ch を置き換える事で再生させますが、音が鳴っていないときでも全てのチャンネルは等しく進行しなければならないため、このような処理としています。

; 発音する
ld hl, (SND.CH1 + MML.FREQ)
ld bc, SNDWRT.@1 * 256 + SNDCH.@1
call SNDOutput
ld hl, (SND.CH2 + MML.FREQ)
ld bc, SNDWRT.@2 * 256 + SNDCH.@2
call SNDOutput
ld hl, (SND.SE + MML.FREQ)
ld bc, SNDWRT.@3 * 256 + SNDCH.@3
ld a, h
or l
jr nz, .outpt
ld hl, (SND.CH3 + MML.FREQ)
.outpt call SNDOutput
ld a, KEYON.All
out (0x02), a ; キーオンフラグを全解放する
ret
実際の発音処理で、各チャンネルの再生更新の結果得られた周波数データを、発音ポートに対して出力します。効果音チャンネルにデータが無い場合は、1ch,2ch, 3ch の周波数を、効果音チャンネルが存在する場合は、1ch, 2ch, SE の周波数を出力します。

BEEP 単音で1チャンネルしかない場合でも、呼び出しのたびに更新される汎用カウンタの LSB =  0 なら 1ch、LSB = 1 なら 2ch を再生するようにする事で、なんちゃって2チャンネル再生が出来ます。この場合でも当然、SE に出力データがあれば、SE を発音します。

SNDOutput の処理部は機種依存します。それ以外の殆どは汎用的に使えると思います。このサンプルでは PCG8100 をモデルにしていますが、そこは別機種の場合は各自調整してください。おそらく休符の実現のために、周波数以外にキーオン情報が必要だと思われます。本サンプルでは説明用に簡素化するためキーオン処理を省いています。その点はご注意ください。
;-----------------------------------------------------------------------
; チャンネル別更新処理(ドライバ内部呼び出し専用)
; IX = ワーク先頭アドレス
;
SNDPlayer:
; アドレスがなければ終了
ld l, (ix + MML.ADDRESS)
ld h, (ix + MML.ADDRESS + 1)
ld a, h
or l
ret z

; 発音継続確認
dec (ix + MML.LENGTH)
jp m, .cmd ; マイナスならMML解析に
ret nz ; プラスなら今のまま継続
ld (ix + MML.FREQ), a ; ゲームタイムで音を消す
ld (ix + MML.FREQ + 1), a
ret

; MMLコマンド解析
.cmd ld a, (hl)
inc hl
cp 0x80
jr z, .stop
jp nc, .length

; 音階
ld (ix + MML.ADDRESS), l ; MMLポインタを保存
ld (ix + MML.ADDRESS + 1), h
add a, a
ld l, a
ld h, 0
ld bc, FRQTBL
add hl, bc
ld a, (hl)
ld (ix + MML.FREQ), a
inc hl
ld a, (hl)
ld (ix + MML.FREQ + 1), a ; 発音カウンタ値を保存
ld a, (ix + MML.LENDATA)
ld (ix + MML.LENGTH), a ; 音長カウンタ初期化
ret

; 音長
.length neg
ld (ix + MML.LENDATA), a
jr .cmd

; 終了
.stop xor a
ld (ix + MML.ADDRESS), a
ld (ix + MML.ADDRESS + 1), a
ld (ix + MML.FREQ), a
ld (ix + MML.FREQ + 1), a
ret
音階処理の FRQTBL も 0xNN00 とキリの良い位置に配置すれば、16ビット計算を8ビットで済ます事が出来ます。

; 音階
ld (ix + MML.ADDRESS), l ; MMLポインタを保存
ld (ix + MML.ADDRESS + 1), h
add a, a
ld l, a
ld h, FRQTBL / 256
ld a, (hl)
ld (ix + MML.FREQ), a
inc l
ld a, (hl)
ld (ix + MML.FREQ + 1), a ; 発音カウンタ値を保存
ld a, (ix + MML.LENDATA)
ld (ix + MML.LENGTH), a ; 音長カウンタ初期化
ret

音楽再生処理部である SNDPlayer では、最初に MMLデータアドレスの確認をしています。もしアドレスが 0x0000 であれば、処理は停止していますので、そのまま終了します。続いて現在の音長を確認します。0x00 でなければ、音長を減算して終了します。

音長が 0 ならば、次の音を出す確認となります。先に取得したMMLデータアドレスから、MMLコマンドを取得します。そのMMLコマンドの内容によって、処理を振り替えます。この例では説明のため機能を絞り込んでいますが、ビブラート等の機能拡張を行う場合は、音長処理の部分を拡張すれば実現出来ます。新しい音階指定があれば、発音周波数データを更新して終了します。MML終了であれば、データアドレスと発音データを 0x0000 クリアして終了しています。

MMLコマンドは 0x80 を終了として、プラスなら音階、マイナスなら音長という非常にシンプルな構造にしました。おかげでおそらくかなり高速化出来ていますが、メモリ効率はさほど良いとは言えないと思います。また、音楽再生処理に必須の休符指定等がないため、満足いく楽曲再生にはもう少し改良が必要となるでしょうが、単純な曲や効果音を鳴らすだけなら必要にして十分だと思います。
;-----------------------------------------------------------------------
; 音楽を鳴らす
; HL = BGMデータアドレス
;
SNDMusicStart:
ld de, 0x0800 ; 音長初期値 8
ld c, (hl)
inc hl
ld b, (hl)
inc hl
ld (SND.CH1 + MML.ADDRESS), bc
ld (SND.CH1 + MML.REPEAT), bc
ld (SND.CH1 + MML.LENGTH), de

ld c, (hl)
inc hl
ld b, (hl)
inc hl
ld (SND.CH2 + MML.ADDRESS), bc
ld (SND.CH2 + MML.REPEAT), bc
ld (SND.CH2 + MML.LENGTH), de

ld a, (hl)
inc hl
ld h, (hl)
ld l, a
ld (SND.CH3 + MML.ADDRESS), hl
ld (SND.CH3 + MML.REPEAT), hl
ld (SND.CH3 + MML.LENGTH), de
ret
;-----------------------------------------------------------------------
; 効果音を鳴らす
; HL = 効果音データアドレス
;
SNDEffectStart:
ld (SND.SE + MML.ADDRESS), hl
ld (SND.SE + MML.REPEAT), hl
ld hl, 0x0100 ; 音長初期値 1
ld (SND.SE + MML.LENGTH), hl
ret

音楽を鳴らしたり、効果音を鳴らすエントリです。楽曲データは先頭に各チャンネルアドレスの先頭アドレスを入れていますので、そのアドレスを取り出しながら演奏ワークにセットしています。ここで大事なのは音長カウントの初期化で、ここをゼロにしないと各チャンネルの再スタートが揃わなくなります。効果音は、殆どの場合は再生最高速で立て続けて発音周波数を変えるので、音長のデフォルトを1にしています。
;-----------------------------------------------------------------------
; 音を止める
;
SNDInitialize:
SNDStop:
xor a
out (0x02), a ; キーオンフラグを全停止する
ld ix, SND.CH1
call SNDPlayer.stop
ld ix, SND.CH2
call SNDPlayer.stop
ld ix, SND.CH3
call SNDPlayer.stop
ld ix, SND.SE
jp SNDPlayer.stop
音を止めるには、ワーク先頭をゼロクリアします。音楽再生時の終了コマンドを呼び出せば簡単に実装出来ます。なお、音楽は途中で止めても、効果音は自然に止まるまで鳴らした方が自然に聞こえますが、このサンプルでは同時に停止するようにしています。この辺りはゲームや実装方式によって臨機応変に改変して使います。

定期的に呼び出しを行うには垂直帰線を参照します。下記の SNDDriverを各処理の至る箇所から呼び出します。レジスタやフラグも全て保存されていますので、どこからでも呼び出し可能です。さほど重くない処理なので、なるべくあちこちに書きます。ハードウェアが本来行うべき割り込みを手動で発生させるイメージです。
;-----------------------------------------------------------------------
; 垂直帰線が裏に入ったら音を鳴らす
;
SNDDiver:
push af
ld a, (SND.BLANKFLG)
or a
jr nz, .chkF

; 垂直帰線が裏にいる
in a, (0x40)
and %00100000
ld (SND.BLANKFLG), a
pop af
ret

; 垂直帰線が表にいる
.chkF in a, (0x40)
and %00100000
ld (SND.BLANKFLG), a
call z, SNDTimer ; 裏から表に変わったので呼び出す
pop af
ret
ということで、上記で説明したサウンドドライバのサンプルプログラムを、実行ファイル cmt とともに格納して SoundTest.zip として配布いたします。テキストは Shift-JISとなっているのでご注意ください。このサンプルは tools80AILZ80ASM でアセンブルを行いバイナリか出来る事を確認しています。実行は mon<RET> L<ret> として読み込み、GC000<ret>としてください。音楽演奏中に [RETURN]を押すと、効果音が鳴ります。終了は[Q]キーとなります。

 第1回:ASM サウンドドライバの構造
番外編ですが、こちらにさらにチューンしたサウンドドライバを公開しています。