• カウンタ値
PC-8001 の周辺機器である PCG8100 に搭載されている発音チップ 8253 は、発音したい周波数をそのまま設定はできません。カウンタ値という値を設定する事になります。このカウンタ値は分周比から計算して、各音程毎にテーブル化しておきます。音程は有名なところで「ラ」の音が 440Hzです。この音が基本となります。

実行速度優先のため、同一オクターブの12音は全て周波数決め打ちのテーブルで考えます。分周比は動作クロックを周波数で割る事で得られます。PCG8100 の動作クロックは水晶発振の 15.9744MHzを4分周しますので、3.9936MHzとなります。Hzに換算すると 3993440Hzですから、この値で周波数を割るとカウント値が得られます。

例えば、ドの音 261.626Hz を出力する場合は、3993440÷261.626=15263.926≓15264=0x3BA0 となります。

音階MML周波数カウント値
C 261.6260x3BA0
ド# C+277.1830x3847
D 293.6650x351F
レ# D+311.1270x3223
E 329.6280x2F53
ファ F 349.2280x2CAB
ファ#F+369.9940x2A29
G 391.9950x27CB
ソ# G+415.3050x2590
A 440 0x2374
ラ# A+466.1640x2177
B 493.8830x1F96

このカウント値を 8253に設定すると目的の音が鳴るというわけです。オクターブが一つ変わると、このカウント値は2倍または1/2になります。計算し続けていくと、どこかで 16ビットより大きくなったり、ゼロに近づきすぎて音階の差が無くなったりと、破綻する瞬間が来ます。そこがこの 8253というチップで出せる音の限界という事になります。

上記の値を仮にオクターブ O3 とします。計算上、O1 でほぼギリギリの低音が出ます。

音階MML周波数カウント値
C 65.4060xEE80
ド# C+ 69.2960xE11D
D 73.4160xD47B
レ# D+ 77.7820xC88D
E 82.4070xBD4C
ファ F 87.3070xB2AC
ファ#F+ 92.4990xA8A5
G 97.9990x9F2E
ソ# G+103.8260x963F
A 110 0x8DD0
ラ# A+116.5410x85DA
B 123.4710x7E57

高音は O6 がようやく音が出せます。このように 8253 の出力音域は少し狭いんです。このため、作曲する人にはこの音域の狭さが足かせになったりします。

音階MML周波数カウント値
C 2093.0050x0774
ド# C+2217.4610x0709
D 2349.3180x06A4
レ# D+2489.0160x0644
E 2637.02 0x05EA
ファ F 2793.8260x0595
ファ#F+2959.9550x0545
G 3135.9630x04F9
ソ# G+3322.4380x04B2
A 3520 0x046F
ラ# A+3729.31 0x042F
B 3951.0660x03F3

2倍や1/2の計算は Z80では比較的演算しやすいため、440Hzを基準とした値をテーブルで持って、オクターブの違いでシフトによりカウント値を作り出すのも良いとは思います。Newシティヒーローでは、音楽で使用している音程全てをテーブル化して、その中から使用頻度の上位10数個だけ1バイト命令にしてと、なんちゃってハフマン符号圧縮のような作りにして、データの圧縮と実行速度の両立を目指してたりしました。

さて、オクターブ1(O1)からオクターブ6(O6)までの、全域の周波数カウント値テーブルは以下のようになります。このテーブルから、発音したい音階のカウント値を取り出して、8253 に設定すると、目的の音が出るようになります。

FRQTBL: dw      0xEE80, 0xE11D, 0xD47B, 0xC88D, 0xBD4C, 0xB2AC  ; O1
        dw      0xA8A5, 0x9F2E, 0x963F, 0x8DD0, 0x85DA, 0x7E57
        dw      0x7740, 0x708F, 0x6A3D, 0x6447, 0x5EA6, 0x5956  ; O2
        dw      0x5453, 0x4F97, 0x4B1F, 0x46E8, 0x42ED, 0x3F2C
        dw      0x3BA0, 0x3847, 0x351F, 0x3223, 0x2F53, 0x2CAB  ; O3
        dw      0x2A29, 0x27CB, 0x2590, 0x2374, 0x2177, 0x1F96
        dw      0x1DD0, 0x1C24, 0x1A8F, 0x1912, 0x17AA, 0x1656  ; O4
        dw      0x1515, 0x13E6, 0x12C8, 0x11BA, 0x10BB, 0x0FCB
        dw      0x0EE8, 0x0E12, 0x0D48, 0x0C89, 0x0BD5, 0x0B2B  ; O5
        dw      0x0A8A, 0x09F3, 0x0964, 0x08DD, 0x085E, 0x07E5
        dw      0x0774, 0x0709, 0x06A4, 0x0644, 0x05EA, 0x0595  ; O6
        dw      0x0545, 0x04F9, 0x04B2, 0x046F, 0x042F, 0x03F3



  • 8253 から発音するために必要なポート
8253 の音に関係するポートは以下の通りです。

 * 0x02 キーオンフラグ
 * 0x0C ch1 カウント値設定
 * 0x0D ch2 カウント値設定
 * 0x0E ch3 カウント値設定
 * 0x0F カウント値設定開始指定

1ch から O4C(ド)の音を出すのは下記のようになります。

    ld      a, SNDWRT.@1    ; ch1 下位/上位
    out     (0x0F), a       ; カウント値設定開始宣言
    ld      c, 0x0C         ; ch1 カウンタ値周力ポート
    ld      hl, 0x1DD0      ; HL O4C 音階カウント値
    out     (c), l          ; 下位カウント値設定
    nop                     ; 4サイクル時間待ち
    out     (c), h          ; 上位カウント値設定
    ld      a, KEYON.@1     ; ch1 キーオンフラグを
    out     (0x02), a       ; 設定して音を出す
N-BASIC からでもテストできます。

    out(2),0
    out(15),&H36
    out(12),&HD0
    out(12),&H1D
    out(2),8



  • 0x02 キーオンフラグ
0x02 のキーオンは各チャンネル毎に音を出すか出さないかを設定します。初代 PCG8100 はシングルチャンネルでしたので、1ch のフラグと 2ch,3ch のフラグに連続性がありません。そのため、計算でフラグを設定する事は出来ず、決め打ちでコーディングする事になります。0x02 ポートに出力するフラグは下記の通りです。

KEYON:  ; キーオンフラグ out(0x02) に出力する
.@1     equ     %00001000   ; ch.1
.@2     equ     %01000000   ; ch.2
.@3     equ     %10000000   ; ch.3
.All    equ     (.@1 | .@2 | .@3)
例えば out (0x02), KEYON.@1 (実際には Acc または C レジスタを介して設定します)とすると、ch1 のみ音が出て、ch2,ch3 は音が止まります。全ての音を止めたければ out (0x02), 0 とします。逆に全てのチャンネルから音を出したければ、out (0x02), KEYON.@1 | KEYON.@2 | KEYON.@3 とします。面倒なので out (0x02), KEYON.All としています、

キーオンフラグは音楽再生では休符処理で使用します。発音カウント値に 0 を入れても音は消えますが正しい処理ではありません。キーオンフラグ制御を心がけてください(めちゃめちゃ面倒ですけどね…)

さて、実はこの 0x02 にはもう一つ大事な機能があります。4ビット目 %00010000 の部分が、PCG 定義開始ビットになっています。そのため、ゲーム中に PCGを書き換えたい場合は、このフラグの操作時にサウンドのキーオン情報が狂わないように配慮する必要があります。通常は最初に定義を終えてしまうので問題はありませんが、拙作の Newシティヒーローというゲームでは、1/60毎に高速に PCG再定義を行っていますので、この 0x02 のフラグ管理がかなり大変でした。PCG 書き換えを音楽を鳴らしながら行いたい場合は、かなり注意が必要です。※ IN してもキーボードの状態がとれるたけですしね…。


  • 0x0F カウント値設定開始指定
上位2ビットのフラグの状態によって、これからどのチャンネルのカウント値を変更するかの指定となります。00 = ch1, 01 = ch2, 10 = ch3 となります。そして、続く2ビットで設定するカウント値の下位上位 の指定となります。少しややこしいので、これも定数定義してしまえば良いと思います。

SNDWRT: ; カウンタ値出力開始フラグ 下位/上位
.@1     equ %00110110   ; ch.1
.@2     equ %01110110   ; ch.2
.@3     equ %10110110   ; ch.3
ch1 のカウント値を設定するためには、下記のように設定します。

    ld      a, SNDWRT.@1    ; ch1 下位/上位
    out     (0x0F), a       ; カウント値設定開始宣言


  • 0x0C, 0x0D, 0x0E カウント値設定
カウント値を設定する専用ポートです。後述する 0x0F ら出力する設定値によって、下位のみとか上位のみという設定ができますが、ほぼ意味が無いので通常は下位上位と2回ポート出力する設定のみを使います。例えば、O4C の音を ch1 から出したければ、先のカウント値テーブルから得られた値 0x1DD0 を 0xD0, 0x1D と順番に out (0x0C), a と設定してきます。

    ld      c, 0x0C;        ; ch1 カウンタ値周力ポート
    ld      hl, 0x1DD0      ; HL O4C 音階カウント値
    out     (c), l          ; 下位カウント値設定
    nop                     ; 4サイクル時間待ち
    out     (c), h          ; 上位カウント値設定
    ld      a, KEYON.@1     ; ch1 キーオンフラグを
    out     (0x02), a       ; 設定して音を出す
この例で注意が一つ。下位カウント値設定と上位カウント値設定の間に nop を挟んでいます。OBF等の高速互換ボードだと nop は不要ですが、初期型 PCG8100 では間髪入れず連続してポートに設定値を送り込むと、正しく設定されない事があります。そのため、最低4サイクル、待ち時間を入れてください。この間に次の処理のためのレジスタ代入とかしておくと無駄がないです。

これで自分で好きな音が出せるようになります。

 第1回:ASM サウンドドライバの構造
 第2回PCG8100 から発音させる
あなたの知らないN-BASICの真実: PC-8001から98DO+まで

川俣 晶
株式会社ピーデー
2018-08-12