以下に書くことは、単にボクが楽しいから備忘するものであって、21世紀の今日においては何の役にも立たないばかりか害があるかも知れない知識であり、かつ、車輪の再発明にすら届かないほどの、わかっている人からすれば当たり前の話で逆にわからない人にとっては永遠にわからないしわかる必要もない話である。昔......と言っても高校生の頃の話なので正確な記憶ではないのだが......音楽畑の友人がこんなことを言っていた。「音楽という芸術は、聞き手を時間的に拘束するという点において特殊である」とか何とか。この言明が正しいか、と問われるとどうかな?と思わないでもないが、印象に残ったのは確かだ。今、こうして思い出すくらいだから。音楽というのは、正確な音程や美しい音色もさることながら、そのそれぞれが時間の流れの中の意図された時点で発せられ、同じく意図した時点で消える、そのようなものである。その順序を変えたり一部のみをつまみ食いしても音楽を聴いたことにならない。音楽の弾き手は、聴き手を時間的に拘束することによってのみ自身の音楽を伝え得るのであり、よって弾き手たる者は、聴き手の人生の時間を一部なりとも奪っていることに自覚的であるべきだ、というのが友人の言わんとするところだったのだろう、と思っているのだが、まぁ、そういう理念的なことはどうでもよくて、意図したタイミングで音を鳴らし、そして止める、これをプログラムによってどのように実現するか、という話がしたい。<開発中の1コマ/手製ツールでワークエリアを覗き見ている>クドいが繰り返し言っておこうと思うが、本稿を書いている理由は、それを説明するため、ではなく、ある意図とそれを実現すること、の間にある数々の論理をプログラムとして組み上げていく思考過程の楽しさ、を書き残したいと思ったからである。このような行為を通すことによってのみ、普段何気なく無意識のうちにやってしまっている多くのことが、意図と実現をつなぐ何段階もの緻密な論理によって成り立っていることを実感できる。実感したくない人はしなくてもいいのだが。意外に思われるかも知れないが、一般的にコンピュータの動作、特にミクロレベルにおいていくつかのCPU命令を実行完了するのに何マイクロ秒を要するか、というのを正確に見積もったり把握するのは、実は難しいか、あるいは不可能な場合がある。コンピュータに音楽を演奏させるに際し、その実現に至る途上の処理所要時間が不定である、というのは、前述したように音楽と言うものが「意図したタイミングで音を鳴らし、そして止める」ことの繰り返しから成る以上、都合の悪い話である。実際にはどうするのかというと、これも一般論であるがCPUから見て外部由来の“割り込み”というものを使う。割り込みというのは、通常、一直線に並んだ命令を順に実行しているCPUに対し、横から強制的に「ちょっと今やってる処理は置いてこっちをやれ」と命じることを言う。復帰を意味する命令があって、ここに至ってCPUは先程やりかけていた仕事へと戻る。MSX(というか、80〜90年代のホビーパソコンの多く)の場合、垂直同期割り込みという機能があって、これは実用上はほぼ正確に1/60秒毎に発生する割り込みであり、元来は画面表示のリフレッシュレートに由来する。余談だが、ゆえに、NTSC方式ではなくPAL方式を採用した国のMSXは、この割り込みが1/60秒毎ではなく、1/50秒毎で発生するので、本稿で述べる方法で再生されるMSX音楽は、PAL国のMSXで何の配慮もなく実行すると間延びする。WebMSXを含む大抵のMSXエミュレータはNTSC/PALを選択できるので、試したい人は試してみるといい。こちらの拙作ゲームも同じ原理でリズムを刻んでいるので、PALを選択すると演奏速度が8割くらいになって難易度が若干下がる。いうか、そもそもアレはスペイン人に供すべく書いたものなので、今般公開しているものとは別に、PAL用に調整したバージョンが別途存在している。WebMSXは実機再現を無視してエミュレータ速度を加減できるので、そっちの方が難易度調整の方法としては素直だけども。他に方法がないワケでもないが、この垂直同期割り込み、別名タイマー割り込みを使って音楽を演奏するのがもっともオーソドックスな方法である。そもそも、今作ろうとしているのは単なる音楽演奏プログラムではなく、ゲームのBGMを演奏するドライバなのであり、ゲームの主処理に並行して一定速度でBGMを再生できるという点で、目的にかなっている。具体的にどうすれば良いだろうか。あるパートのみを取り出して考えた場合、音楽の演奏というのはつきつめれば、ある音階を鳴らし始めて音符分の時間が経過したら音を止める、次は休符の時間だけ何もせずに待って、また次の音符へ進む......といったことの繰り返しである。ここで必要な時間経過の計測を、タイマー割り込みを使っておこなう。一般的な音楽において、音符1つが実際にどれだけの長さの時間に相当するかは、音符の種類と曲のテンポによって決まる。Minute)1分間に四分音符を150個含むという意味であるから、1秒当たり2.5個になり、これを1/60秒毎に発生するタイマー割り込みで計ろうとすると、割り込みが24回発生すると四分音符の長さの時間が経過したことになる。この考え方に従って、いろんなテンポによって各種音符が割り込み何回分の長さに相当するかを表にすると以下のようになる。表中、赤い×印で示しているのは、値が整数にならない部分である。単位がタイマー割り込みの発生回数だから、小数の回数を数える、ということはできない。この方法では実現できない、ということになる。120BPMにおける十六分音符を演奏するに際し、これが二つ並んでいるとして一方に8/60秒、もう一方に7/60秒を用いれば、普通の人の耳には十六分二連符に聴こえる。これを自動的にプログラムで処理するのはかなり骨が折れるし、ちょっと耳の良い人であれば、前の十六分音符を長くするかあるいは後ろの方が自然か、は、そのときの前後のフレーズや音色に依存することに気づくのであって、これはすなわち機械的に自動化すると不自然になることを含意している。今回は、汎用性が高い=あらゆる音楽の再生に使える、ものを目指しているワケではないので、上掲表における整数値で表現可能な音符のみを使った楽曲が再生できればOK、と考えることにした。この考え方に基づいてBGMドライバを作るとどんな実装になるだろうか。前回同様に、まず楽譜、つまり演奏用データの記述の側から考えてみよう。本物の楽譜は1つの記号の形状(音符の種類)が音の長さを、五線譜上の位置が音階を表現している。これに倣って、1音毎に音階と長さを指定する......つまり最初の音は四分音符の4Cです、次は四分音符の4Eです、と連記していく......のが、もっともシンプルなその実現方法だ。これを実際にデータにすると、1音毎に疑似十六進数で表す音階(4Ch、4Ehなど)と音の長さ(仮に150BPMとすると24)の2バイトを使うことになる。これが16小節続けば計128バイトだ。考えてみると、一般的な楽譜を想起した場合、1つ進むごとに様々な異なる種類の音符が並ぶ、ということはまずない。8ビートのベースであれば、文字通り八分音符がずーっと並んでいるワケだが、これを、4Ch,12,4Eh,12,4Ch,12......と128バイト続けるのは、ちょっとメモリ空間の浪費というべきだろう。BGMは欠かすべからざる要素ではあるが、ゲームプログラム全体から考えればオマケに過ぎないのであって、なるべく少ないメモリで済むのであれば済ませたい。そこで「一旦音の長さを宣言したら次に別の長さを宣言するまで有効」という仕様を選ぶことにする。宣言された音の長さは演奏中のデータアドレス同様、チャネル毎にワークエリアに保存される。この方法であれば、例に挙げた四分音符が16小節続くケースだと、最初に音長を宣言する1バイトと実際の音階を示す64バイトの計65バイトで、ベタ書きする128バイトとまったく同じことが実現できる。要するにこれは、いわゆる“圧縮”と総称される処理の最もシンプルなケースである。音長の宣言は、音階同様にデータ作成時の直感性も担保したいので、上掲表に「データ」として示したやはり疑似十六進数を使うことにした。上位4ビットがAであれば、下位4ビットが音符の種類を示す。一部恣意的な部分もあるが、たとえば4が四分音符、8が八分音符というように、直感的な対応関係を確保した。これを、150BPMのケースで実装したのが以下のコードになる。上の行に並んでいる数値が前掲表の150BPM列と対応している。テンポを変更したい場合はこの16バイトを書き換えればよい。ちなみに、黄色で示した部分は前稿で扱った「音階とPSG値の対応表」の先頭である。音階を示す疑似十六進数は“10h”から始まるので、計算上の先頭との間にはこれを2倍した32バイトの隙間ができる。この隙間を、ここで述べた「音符と音長の対応表」等を置くのに活用している(オクターブ間の隙間とは違い、こちらは周期性がないのでワークエリアには使いにくいのだ)。ボク自身は生来の横着者なので「1ビットたりとも無駄にしない」までのこだわりこそないが、まぁ、これは80〜90年代のパソコンで電脳リテラシを培ってしまった者特有の病、というべきかも知れない。以上を踏まえて、改めてBGM演奏ドライバがどのような動作をおこなうべきか、を考えてみよう。そのプログラムは繰り返し述べているように、1/60秒に1回実行され、かつ、速やかにやるべきことを終わらせて主処理へとCPUを返してやらないといけない。箇条書きにすると、概ね以下のような感じになる。割り込み処理の冒頭で、1つ前に鳴らした音の途中かどうかを判断し、途中であれば主処理へ返す。演奏データから1バイト読む。演奏データが音長宣言であれば、音符音長対応表を調べて音長をワークエリアに保存し、演奏データを1バイト進める。演奏データが音階であれば、音階PSG値対応表を調べてPSGに適切な値を設定し、ワークエリアに保存していた値を新たな待ち時間として設定する。演奏データが0であれば、休符としてPSGの音量を0にし、ワークエリアに保存していた値を新たな待ち時間として設定する。これを1/60秒毎に繰り返していけば「意図したタイミングで音を鳴らし、そして止める」が実現されることになる。以上のうち(1)〜(3)の部分をニーモニックで書いたのが以下のものになる。256バイトの範囲を指定するアドレッシングモードがある。これが何を意味するかというと、前後128バイトの範囲にデータが収まっていれば、ixレジスタの値を更新することで、同型の複数のデータ構造に同じプログラムで処理ができる、ということであり、具体的には、PSGの2つのチャネルの処理は、ixの値を変更した上で同じサブルーチンをcallすることによりおこなわれる。*と、つらつら書いてみた。繰り返しになるが、何故敢えて本稿を書いているか、もちろん以上のことを理解して欲しい、とか、誰かに伝えたい、といった考えは毛頭ない。普段のボクは以上のような思考過程を、メモすら取らずにいきなりニーモニックコードとして書くという物臭者なのであり(これはボクの有能さを示すものでは決してなく、特に職業プログラマとしては致命的な悪癖である)言うなれば、これは自身の二足歩行の詳細な過程を言語で論述することは困難かつ億劫であるが、あえてそれを試みること自体を楽しむ、という文学的な行為である。ボク自身はその行為を楽しんだし自分で読み返してみて面白いが、おそらく共感してくれる人はいないだろう。それを言い出したらボクの書き物すべてがそうなのであるが、そこがまた楽しいのである。投稿者メールコメントURL画像内の文字列(認証コード)を入力してください。認証コードを正しく入力するとコメントが反映されます。※大文字/小文字の区別はありません。コメントは新しいものから表示されます。コメント本文中とURL欄にURLを記入すると、自動的にリンクされます。“AutoPage”AutoPage最新お知らせ。