プログラミング
ZX Spectrum システムツアー:サウンド
ZX Spectrum System Tour: Sound (bumbershootsoft.wordpress.com)
要約
ZX Spectrumのサウンド機能について解説する記事。このシステムは、ソフトウェア制御の1ビットビーパーと、128Kモデル以降に搭載されるGeneral Instrument AY-3-8910シンセサイザーチップの2つのサウンドオプションを提供します。記事では、ビーパーをBIOS経由で、または直接Z80の16ビット演算機能を用いて制御する方法を、Cメジャースケールを再生するプログラム例と共に詳細に説明しています。
全文翻訳
シューティングギャラリーの作業が完了したので、当初のZX Spectrumツアーでまだ触れていない部分、つまりサウンドに焦点を移す時が来ました。他のプラットフォームで以前に説明した内容と比べて、新しいことはあまりありませんが、それは逆に、サウンド問題への解決策の核となる原則がすでに確立されていることを意味します。私たちはスペクトラム固有の装飾を加えるだけで済みます。メインラインのスペクトラムシステムは2つのサウンドオプションを提供します:ソフトウェア制御の1ビットビーパー。これはすべてのシステムに搭載されており、基盤となるハードウェアはApple IIやPCスピーカーで見たものと似ています。General Instrument AY-3-8910シンセサイザーチップ。これは128K Spectrum以降のすべてのモデルに搭載されており、16Kおよび48Kバリアント用の周辺機器としても利用可能です。これらは基本的にAtari STのものと同じですが、クロックが異なり、通信用の組み込みBIOSコールがありません。これは、テキストモードやビットマップモードの調査と同様に、一般的な調査の一部であるため、目標は控えめなものになります。この記事の最後には、Cメジャースケールを再生する3つのプログラムを掲載します。BIOS経由でのビーパーの使用1ビットビーパーはApple IIのものと非常によく似ています。I/Oポートの$10ビットをセットまたはクリアすることで、スピーカーの状態を設定できます。特定のトーンは、そのビットをトグルする間隔を慎重にタイミングを取ることによって生成されます。後でそのためのコードにアクセスしますが、それを延期できます。なぜなら、私たち自身で行う必要はないからです。BASICのBEEPコマンドはすでにこれを管理しており、それは私たちが自分で利用できる汎用ルーチンに転送することによって行われます。BEEPERルーチンはメモリ位置$03B5にあり、正直なところ、私が6502システム用に書いたバージョンよりも巧妙です。HLレジスタは4サイクルの単位(no-opの長さ)での遅延時間を指定し、プログラム可能なサイクルエキザクトな6502遅延に使用した手法に似たテクニックを使用してその精度を取得します。DEレジスタはノートの持続時間を指定しますが、これは実際にはノートが持つべき波長の数を指定するため、少し不便です。同じ長さだが異なるピッチのノートは、異なる持続時間コードを必要とします。BEEPコマンド自体は、中央のCからの半音階でノートを伝え、秒単位で測定された持続時間を伝える必要があります。その実装($03F8)を参照して、必要な値を取得する方法を確認できます。これはシステムの浮動小数点エンジンを介して行われるため、コメントから両方の正確な数式が得られます。周波数f Hzの周波数コードHLは、437500/f – 30.125の式によって提供されます。周波数f Hzと持続時間t秒のノートの持続時間コードDEは、単純にf×tです。Pythonの数行で、これをCメジャースケールの周波数に適用し、ルーチンに渡す周波数と持続時間コードを取得できます。再生プログラムは次のとおりです。org $7000 ld b,8 ; 8つのノートを再生 ld hl,scale ; ポインタからロード loop: push bc ld c,(hl) ; 周波数をBCにロード inc hl ld b,(hl) inc hl ld e,(hl) ; 持続時間をHLにロード inc hl ld d,(hl) inc hl push hl ; ポインタをスタッシュ... ld h,b ; ...BCをHLにコピー... ld l,c call $03b5 ; ...そしてROMに任せる pop hl pop bc djnz loop ret scale: dw $066a,$0082,$05b3,$0092,$0511,$00a4,$04c6,$00ae dw $043d,$00c3,$03c4,$00dc,$0357,$00f6,$0325,$0105これは私の最初のApple IIサウンド再生ルーチンに似た「半波長」システムですが、より細かいタイミング制御により、ノート全体の精度が大幅に向上しています。正直なところ、私の最初の記事で直面したトレードオフはここではまったく現れず、専用PSGに期待するような制御が得られます。正直に言って、これは本当に素晴らしいです。唯一の本当の欠点は、両側で独自の除算を実行する必要があることです。もし任意の周波数と持続時間のノートを管理する汎用ルーチンを作成したいのであれば、独自の除算器を持参する必要があります。しかし、すぐにわかるように、16KBのSpectrumでは他に選択肢がなく、このルーチンに頼るしかありません。しかし、私たちは先走っています。16Kシステムで遭遇するであろう問題に実際に直面する前に、手動でどのように行うかを見てみましょう。ビーパーを直接使用するBIOSルーチンは十分に優れているため、独自のオシレーター関数を書きたいのであれば、まったく異なる原則で実行されることを望みます。幸いなことに、私にはそのような原則があります。それは、半波長ではなく周波数に基づいた16ビットシステムであり、SIDとEnsoniqチップの内部構造に触発されて作成したものです。Z80の16ビット演算の組み込みサポートにより、6502コードよりもはるかに簡単になるはずです。基本的な考え方は、周波数コードを実行中のカウンターに繰り返し追加し、カウンターがオーバーフローするたびにスピーカーをトグルすることです。難しい部分は、他のものを台無しにしないようにすることです。なぜなら、I/Oポート$FEは3つの役割を果たすからです:下位3ビットはボーダーカラーを設定します。ここではBORDCRシステム変数の値を保持したいと考えています。$08ビットは、ラウドスピーカーを制御するのではなく、テープデッキを制御する際のオーディオ出力を制御します。これらは電圧を何らかの方法でいじります。Korthのドキュメント(私のプラットフォームガイドにリンクされています)に詳細が記載されていますが、システムROMが行っていることを行い、このビットを常に強制的にオンにします。$10ビットは、私たちが実際に扱いたいビットであり、ラウドスピーカーオーディオを制御します。まず、これらのすべてを直接実装し、可能な限り少ない分岐で実装します。sound: ld hl,0 ; カウンターは0から開始 ld a,($5c48) ; BORDCRと$38 ; ボーダーカラーを抽出 rrca ; 低位3ビットにシフト rrca rrca or $08 ; テープオーディオを無効化 di ; IRQによる割り込みを受けない push af .loop: pop af add hl,de ; 周波数をカウンターに追加 jr nc,1F ; オーバーフローした場合... xor $10 ; ...スピーカーを出力にトグル out ($fe),a 1 dec bc ; タイマーをデクリメント push af ; 0をチェック。AFをポップできない ld a,b ; ブランチの後でなければ or c ; Zフラグを失う! jr nz,.loop pop af ; クリーンアップ ei ret このコードでは、各イテレーションの各パスがまったく同じ時間で実行されるようにコードを再構成する必要があります。ここでの敵はJR命令です。分岐が発生する場合は12サイクル、発生しない場合は7サイクルかかります。つまり、出力をトグルしないパスでは、7(JR)+ 7(XOR)+ 11(OUT)– 12(分岐が発生する場合のJR)= 13サイクルの遅延が必要です。無条件のJRバックも12サイクルかかるため、各命令の最小時間が4サイクルであるアーキテクチャで1サイクルの遅延が必要です。これはひどいです。面白いことに、解決策はJR命令を使用しないことです。絶対アドレス指定のJPバージョンはバイトが1つ長くなりますが、条件付きでも完全に一貫したタイミング(それぞれ10サイクル)を持ちます。これにより、これははるかに簡単になります。遅延分岐は2つのNOPとメインラインへのJPになります。次のドラフトは次のとおりです。sound: ld hl,0 ; カウンターは0から開始 ld a,($5c48) ; BORDCRと$38 ; ボーダーカラーを抽出 rrca ; 低位3ビットにシフト rrca rrca or $08 ; テープオーディオを無効化 di ; IRQによる割り込みを受けない push af .loop: pop af add hl,de ; 周波数をカウンターに追加 jp nc,2F ; オーバーフローした場合... xor $10 ; ...スピーカーを出力にトグル out ($fe),a 1 dec bc ; タイマーをデクリメント push af ; 0をチェック。AFをポップできない ld a,b ; ブランチの後でなければ or c ; Zフラグを失う! jr nz,.loop pop af ; クリーンアップ ei ret 2 nop ; オーバーフローしない場合、18サイクル待機 nop jp 1B 合計すると、このイテレーションは1イテレーションあたり84サイクルになります。これにより、約3Hzからほぼ100kHzまでの範囲で線形な周波数制御が可能になり、これは十分すぎるほどです。ただし、持続時間は約1.5秒までしか行けず、これはあまり良くありません。しかし、さらに大きな問題があります。このルーチンによって生成されるトーンはひどく、ピッチが大きく外れており、タイミングも一貫性がなく、ガタガタとウォブルします。問題は、Spectrumのビデオ回路がRAMからピクセルとカラーデータをフェッチする必要があり、それができることです。