今更PSG用BGM/SEドライバ(前編)

2016/12/24 | 投稿者: ghost

以下に書くことは、単にボクが楽しいから備忘するものであって、21世紀の今日においては何の役にも立たないばかりか害があるかも知れない知識であり、かつ、車輪の再発明にすら届かないほどの、わかっている人からすれば当たり前の話で逆にわからない人にとっては永遠にわからないしわかる必要もない話である。

*     *     *

コレの続き。

高速化を要する部分だけをアセンブラに置き換えるつもりでやり始めた開発だったのだが、書いているうちにいろいろ欲が出てきて、半ば発作的にBGM/SEドライバを新製してしまった。ここでいう“BGM/SEドライバ”というのは、ゲームなどの主処理と並行し、かつ、主処理に影響を与えることなくBGMや効果音を再生することのできるプログラム(サブルーチン)のことである。

最初に言っておくと、これはまったく無駄な行為なのであって、MSXの世界ではPSG、FM音源に加え、コナミ製SCC音源までサポートする汎用かつ高性能のBGMドライバが既に存在している(MuSICA,MGSDRV等)。真っ当な人間であればこれを利用して然りである。が、そもそも真っ当な人間は2016年にもなってMSXでプログラミングなんかしないのであり、そしてZ80ニーモニックを書くこと、それ自体が開発の目的なのであるから、既存資源を活用して楽をしよう(必ずしも楽が出来るワケでもないのだが)などという発想はそもそもないのだ。

それはさておき。

今回作ったのは、MSXがサポートする音源の中ではもっともプリミティブなものとなるPSGのみを使って音楽と効果音を演奏するサブルーチンである。PSGというのは、大雑把に言うと以下のようなものだ。もう少し他にも機能があるのだが、話を単純化するために本稿では割愛する。

・同時に3声まで音を出せる。
・音はピーといった平板な正弦波である。
・3声それぞれについて、4096段階の音程を出すことができる。これは8オクターブに相当する。
・3声それぞれについて、15段階の音量を出すことができる。

これらの機能は、14個あるPSGレジスタそれぞれに1バイト(0〜255)の数値を与えることで発現する。MSXの場合、これを代行するBIOSと呼ばれるサブルーチンが用意されていて、Z80のAレジスタにPSGレジスタ番号、Eレジスタに与えたい数値をセットした上で、MSX-BASICが載っているROMの0093h番地をcallすれば良いのであって、PSGと呼ばれる回路が実際にはどのようなものであり、どのように音を出すのか、CPUとはどのように配線されているか、等については把握する必要がない。

従って、BGM/SEドライバを書く、という行為は、意図した演奏するために、適切なタイミングに、AレジスタとEレジスタに適切な値をセットしてBIOSをcallするプログラムを書く、と言い換えることが出来る。

では「意図した演奏をする」とは、具体的にはどのようなことになるだろうか?

本稿を書いている理由は、それを説明するため、ではなく、ある意図とそれを実現すること、の間にある数々の論理をプログラムとして組み上げていく思考過程の楽しさ、を書き残したいと思ったからである。このような行為を通すことによってのみ、普段何気なく無意識のうちにやってしまっている多くのことが、意図と実現をつなぐ何段階もの緻密な論理によって成り立っていることを実感できる。まぁ、実感したくない人はしなくてもいいのだが。


さて、音楽を演奏する、とは、より正確に言えばどういうことだろうか?

直感的に思い浮かぶのは「楽器を弾く」という行為であり、本稿におけるPSGの制御がこれに相当する。もう少し細かく考えてみよう。楽器を弾く、とはどういうことか。まず、演奏しようとする曲を知っている必要がある。平たく言えば楽譜だ。BGMドライバは、楽譜に相当する数字の列(これも0〜255の数字の組み合わせになる)を演奏に変換する装置だ、ということになる。従って、BGMドライバは以下の機能を有していなければならない。

(1) 自分が楽譜のどこを演奏しているか、次はどこへ進むかがわかっている。
(2) 今、弾くべき音階と、その弾き方がわかっている。

(1)は、BGMドライバが演奏中のデータが記録されているメモリ上のアドレスを管理しなければならないことを意味している。PSGは3声同時発音可能であり、これは楽譜上に3つのパートがあることに相当する。パートが異なれば、必ずしも楽譜上の同じ縦の列を見ていれば良いことにはならないから、このアドレス管理は3声それぞれに独立していなければならない。

(2)は、楽譜上のドレミと前述した「4096段階の音程」の間に対応関係がある、ということである。本稿は、ここに深入りしてみる。

普通の曲を演奏するのに必要な音階は、1オクターブあたり、ドレミファソラシの7音階と間を埋める半音を加えた12音階になる。これが8オクターブ積み重なるから96音階。原理の説明は省くが、この96音階と前述の4096段階の対応関係は、単純な関数で表現することが出来ない。従って、この対応関係を示す表を用意しておいて、楽譜から音階を1つ拾う都度この表から4096段階のどれを使うべきか調べる、という操作が必要になる。

さて、最もこれを単純に実現すると、1〜96の数字を音階と対応させ、対応表に順番に4096段階のどれかの数値を記録しておいて、対応表の先頭アドレスに1〜96を加えた(厳密には4096段階=12ビットのデータを収めるには1.5バイト必要なので1.5倍、ないしは0.5ビットを無駄にして2倍する必要があるが)アドレスを調べれば、めでたく目的音階の音を出すための数値を得ることが出来る。

余談になるが、DTMをやる人は知っているかと思うが、DTMソフトでも譜面上の音符の音階について、オクターブ+音名と対応する「ノート番号」と一般に呼ばれる0〜127の数字が割り当てられているが、これがここで言っているものに相当し、いわゆるMIDI規格も同じ発想の上に成り立っていることがわかる。

が。

ここに一つ問題が生じる。それは、ノート番号と音階の対応関係が直感的ではない、という点だ。これはプログラミングではなく、実際の演奏データを作る際に問題になる。たとえば、チューニングに使う音叉の音(440Hz)、すなわち第4オクターブのAはノート番号69になるが、データ上の69という数字から第4オクターブのAを想起するのは、不可能ではないが容易ではない。ましてや、楽譜はたくさんの音符からなるのだから、データの作成はもちろん、修正に際して1音毎にこのノート番号を確認する、というのはストレスフルな作業になる。

このような場合、一般的には人間から見てわかりやすい表記法、たとえば第4オクターブのAであれば“4A”を、69へと自動的に変換するプログラムを用意し、これを使って演奏データを作る。これは本質的にニーモニック言語を機械語へ変換するアセンブラ・コンパイラと同じものである。前述のMuSICAやMGSDRVも、演奏ドライバとコンパイラがセットになっている。

つまり、ボクもまた自分の書いているドライバ用のコンパイラを開発しなければならない、ということになるのだが、コードを書くこと自体が目的だ、と言っておいてアレゲだが、単なる集合変換に過ぎないプログラムを書くほど退屈な作業もないのであって(あくまでもボクにとっての話であって、世の中にはコンパイラを作ること自体が趣味の人だっている)今回はこれとは異なるアプローチを採ることにした。

先に第4オクターブのAを“4A”と書いたが、これは十六進数として読むことが出来る。同様に、第1オクターブのFを“1F”、第6オクターブのCを“6C”と書けば、コンパイラがなくてもそのまま記述できそうな気がする。少なくとも、それぞれノート番号69、29、84で記述するよりは直感的に扱えそうである。

もちろん、一工夫が必要である。十六進数の一桁は0〜FだからGが扱えない。これは見た目が似ている0で代用しよう。半音はどうするか。C#,D#,F#,G#,A#をそれぞれ1,2,3,4,5に対応させれば、まぁなんとかなるだろう。すると、たとえば第3オクターブから第4オクターブにかけての音階、PSGに与える値、拙作ドライバでの表記、の対応関係は以下のようになる。

クリックすると元のサイズで表示します

まぁ、言ってしまえば程度の問題であるが、ノート番号よりは使えそうではないか。というか、実際にこの表記で直に演奏データを入力してみたのだが、意外にイケる(あくまでも個人の感想であり、商品の効能を保証するものではありません)。さらに、この方式にもう一つのメリットがある。と言うか、こちらが本当は主目的なのであるが。

クリックすると元のサイズで表示します

上に示したのは、この表構造を記述しているアセンブラ用のプログラムコード(データ宣言部)である。

これは上掲の表を、拙式表記の“疑似”十六進数表記順に並べ替えたものである。ただ並べるだけだと音階に紐付かない36h〜39h、46h〜49hが欠けてしまうので、同じ数の0を補って埋めてある(黄色着色部)。これで何が実現されるか、と言うと、一番右に書いてある十六進数は、音階を現す意味を有すると同時に、このデータ構造の先頭からの距離でもあるのだ。十六進数1つにつき2つ(12ビットを収めるための2バイト)のデータに対応しているから、楽譜データから1バイト拙式表記十六進数を読み出して2倍し、これをデータ構造の先頭アドレスに加えてやれば、音階に対応する12ビット値のアドレスを得ることが出来る。

つまり、

・96音階と4096段階の対応関係が単純な関数で表現される。
・楽譜上の音階が人間視点から見て直感的である。

の、一見して矛盾する二要件が同時に満たされたことになる。

強いて問題点を挙げれば、黄色で示したデータ構造の隙間がメモリの無駄遣いだ、ということはある。が、実はこれは折り込み済みなのであって、オレンジ色で示したラベルにWKBCHとあるが、これは WorK area for B CHannel の意味でこうしているのだが、前述した(1)3声(Channel)それぞれが自身の現在の状態を管理しなければならない、のために使うワークエリアとして活用するのである。

この隙間は、当然のことながらデータ構造中正しく32バイト毎に8バイトずつ現れるのであるが、こういう周期性のある隙間と、Z80のインデックスレジスタを使ったアドレッシングは相性がいい。厳密に言うと、データがリニアに並んでいるのに対して処理速度を犠牲にしてしまうのだが、3.5MHzで駆動するZ80と言えども、この程度の仕事をするには十二分に高速なので大した問題にはならない(ある程度以上の込み入った処理の続くゲームプログラムと並走させる場合には深刻になってくるが、ボク如きの作るゲームであればそこまでには至らない)。

12ビットに対して2バイト使っている=0.5バイト無駄にしている、というのも問題と言えば問題である。連続する12ビット値の収納については、隣接するデータから4ビットずつ切り出して、一方を16倍して加え計3バイトに詰め込む、というベストプラクティスがあって、CP/M、その流れを汲む我らがMSX-DOS、そして当然のことながらMS-DOSのFAT12(12ビットだからFAT12なのだ)がまさにこの方法を採用した。が、ディスク媒体上のデータのような、オンメモリに比べて膨大なデータを少しでも多く詰め込みたい際にこれは有用ではあるが、たかだか192バイトのデータを144バイトに圧縮したとして、その読み出しプログラムを書くのに48バイト以上使うと返って損をしてしまう(48バイトもあればいけそうな気もするが……)ので、この0.5バイトずつの無駄は必要経費と割り切ることにした。っつーか、面倒臭いし。

予想はしていたが、音階の話だけでいっぱいいっぱいになってしまったので、実際の演奏の実現については稿を改めることにする。

つづく>>



コメントを書く

名前
メールアドレス
コメント本文(1000文字まで)
URL





AutoPage最新お知らせ