プログラミング
ZX Spectrum: 1ビットサウンドの実験
ZX Spectrum: Experimenting with 1-Bit Sound (bumbershootsoft.wordpress.com)
要約
この記事では、ZX Spectrumの1ビットブザーを使用してマルチチャンネルサウンド(和音)を再生する実験について解説しています。筆者は、IBM PCの1ビットスピーカーで得られた成果に触発され、PCスピーカーの高度な技術をZX Spectrumで再現しようと試みました。その過程で、PCM(パルス符号変調)とPWM(パルス幅変調)の概念を説明し、特に1ビットPCMの再生ルーチンと、Z80 CPUでの正確なサイクルカウントによる遅延処理について詳細に述べています。PWMの実装は満足いく結果を得られなかったものの、将来的な可能性を示唆しています。
全文翻訳
ZX Spectrumツアーの一環として、1ビットブザーから単純なトーンを再生できるルーチンを実装しました。これらは、Apple IIでの以前の作業に大いに触発されたものです。しかし、それが限界ではないことを知っています。IBM PCの1ビットスピーカーからも、ありえないほど良い結果を得ることができました。今週の当初の計画は、PCスピーカーの高度な技術の一部をZX Spectrumで再現することでした。残念ながら、それは期待ほどうまくいきませんでしたが、それは問題ありません。なぜなら、他の多くのことを機能させることができたからです。今週達成した主なことは、1ビットスピーカーを介したマルチチャンネルサウンド、つまり和音の再生に関連しています。また、PCスピーカーの記事からの単純な1ビットPCM再生を再現し、より高度なPWM技術についても簡単に実験しました。これら両方とも、和音再生の作業に役立ちました。これは包括的なガイドではありません。このブログでは、私の失敗と成功の両方を記録しており、今週は失敗がかなりの割合を占めました。
パルス符号変調とパルス幅変調
パルス符号変調(PCM)は非常に単純な概念です。波形は0から最大値までの値のシリーズとしてサンプリングされ、それらの値はハードウェアに送信されてスピーカー電圧に変換されます。最大値が大きいほど、振幅の制御はより細かくなり、サンプルをより速く送信するほど、周波数の制御はより細かくなります。(信号処理の基本的なルールの一つは、サンプルレートの半分よりも高い周波数の波形を正確にサンプリングできないということです。)PCM波は通常、各サンプルを表現するために使用されるビット数とサンプルレートによって記述されます。高品質なサンプルは16ビット44kHzかもしれませんが、ほとんどのユースケースは8kHzという低いサンプリングレートの8ビットサンプルで対応できます。SpectrumはCPUで約1MHzでメモリにアクセスできるため、CPU制御下でも16kHzの再生に到達できます。しかし、スピーカーは常にオンかオフのどちらかです。これは1ビットPCMであり、かなり音が悪くなるはずです。スピーカーを過度の増幅で破損させたときのような歪みが生じますが、常に、そして低音量でも発生します。SpectrumのモデルによってCPU速度はわずかに異なりました。私はサイクルカウントのために、フラット3.5MHzで動作した48Kのクロックを使用します。これを毎秒16,000サンプルで割ると、書き込みの間隔は219サイクルになることがわかります。これはかなり余裕のある時間です。1バイトあたり8サンプルをパックした1ビットPCM録音を簡単に消費できます。サンプルは毎秒約2KBを消費します。これは48Kシステムではタイトですが、壊滅的ではありません。主な問題は、後述するように、音の歪みになります。この技術の実装には特別な課題はありません。
パルス幅変調(PWM)は少しトリッキーですが、はるかに高品質なオーディオが期待できます。電気信号レベルでは、PWMデータはサンプルごとに1ビットのパルスを送信し、パルスの長さはサンプルポイントでのオーディオ信号の意図された強度を示します。(PCMと比較してください。PCMは、これを達成するために事実上マルチビットデジタル値をワイヤに送信します。PWMは、より積極的に1ビットであるにもかかわらず、PCMよりもアナログ的です。)物理レベルでは、これは電気信号がナノ秒で1から0に変化し、元に戻ることができるという事実の結果として現れますが、デバイスに接続された物理スピーカーは、その「オン」と「オフ」の状態の間を実際に移動するために数十マイクロ秒を必要とします。信号がその移動のさまざまな時点でオフになるようにすることで、スピーカーの総強度は、1ビットPCMが提供するものよりもはるかに正確に制御可能な方法で変化します。IBM PCでは、1ビットスピーカーはハードウェアタイマーに接続されており、7ビットPCMデータを供給することで高品質なオーディオを生成できます。Spectrumはそれほど親切ではなく、サイクルカウントで管理する必要があります。私はSpectrumで満足のいくPWMシステムを機能させることはできませんでしたが、いくつかの手がかりがあり、技術全体は健全であると確信しています。
Z80でのサイクル正確な遅延
コードの詳細に入る前に、正確なタイミング遅延を行うために必要なことを明確にする必要があります。Z80の「Tステート」カウントは、6502のCPUサイクルよりもはるかに大きく、はるかに滑りやすいです。6502で任意のサイクル数を簡単に待つための命令をまとめることができましたが、私のZ80のガイダンスは必然的に少し依存的です。この作業では、経験則に頼ることが多く、遅延シーケンスをより有機的な方法で組み立てました。NOP自体は4サイクルであり、基本的にすべての1バイト8ビット操作(多数あります)も同様です。残りの遅延が短く、4サイクルの倍数であれば、基本的に完了です。INC HLは6サイクルであり、同様に構造化された1バイト16ビット命令も同様です。これらは2サイクルのずれを丸めることができます。または、3バイトのコードではなく2バイトのコードで12サイクル待つためにペアにすることができます。LD A,nは、他の8ビット即値命令とともに7サイクルです。合計待機時間が十分に長い限り、これらはサイクル精度まで下げることができます。JP命令は、条件付きであっても10サイクルであり、正確なタイミングのループで決定を行う必要があるコードでは、JRやDJNZよりもはるかに好ましいです。それらはまだその場所を持っていますが、なぜなら… LD B,n; LBL: DJNZ LBLは、あなたが書くことができる最も短いループであり、13n+2サイクルを消費します。長い待機では、このループがその大部分を占め、上記の他の命令が不便を丸くします。これは体系的ではありませんが、私たちの目的には十分です。
PCM再生ルーチン
全体的な計画は、各バイトに8サンプルをパックし、高ビットを最初に再生することです。16ビットサンプルでは、各出力の間に219サイクルが必要です。入力はHLにデータへのポインタを、DEにサンプルのバイト数を配置します。(合計サンプル数は8の倍数であると仮定します。エンコーダーは末尾をパディングするだけでよく、それはエンコーダーの問題であり、私たちの問題ではありません。)通常、BCをカウンターに使用しますが、内部時間遅延カウンターにBを特に必要とするため、DEがその負担を引き受ける必要があります。ルーチンは、面白いことに、ほとんど終わりに始まります。メイン再生ループに入るとき、私たちは219サイクルのシーケンスの途中にいることになり、タイミング制約を知るために、ループの終わりをそれに合わせて書く必要があります。また、全体的な関数プロローグとエピローグは非常に短いため、すぐに処分できることも判明しました。それらがしなければならないすべては、割り込みを無効にし、再有効化することです。
pcmout:
di
.lp:
????????????? ; バイトから8サンプル再生、...で終了
out ($fe),a ; + 11 (219) ...最終ビット出力
;; バイトカウンターを調整してループバック
dec de ; + 6 ( 6)
ld a,d ; + 4 ( 10)
or e ; + 4 ( 14)
jp nz,.lp ; + 10 ( 24)
;; 完了しました。割り込みを再有効化して戻ります。
ei
ret
通常、バイトを一度に1ビットずつ消費する場合、一方の端または他方の端からシフトし続け、キャリービットを参照して何をするかを決定します。ここでは少し工夫できます。消費しているビットを、出力バイトの$10ビットにコピーする必要があります。この事実を考慮すると、最初のバイトを取得し、それを右に3ビット回転させ、その後、$10ビットを直接操作する方が効果的です。8ビットのRRCおよびRLC命令を使用する必要があり、9ビットのRRおよびRL命令は使用しません。上記から、ループをサイクル24で開始することがわかっています。バイトを読み込み、準備し、適切なタイミングで最初のビットを出力すると、次のようになります。
.lp:
ld a,(hl) ; + 7 ( 31) バイトをロード
inc hl ; + 6 ( 37) ポインタを進める
rrca ; + 4 ( 41) $80ビットを$10に回転
rrca ; + 4 ( 45) 8ビット回転で
rrca ; + 4 ( 49) ld c,a ; + 4 ( 53) バイトをCに、$10をスタッシュ
; + 7 ( 60) サウンドビットを分離
or $01 ; + 7 ( 67) 青い境界線、なぜなら?