SL9821

はじめに

ダウンロード

使い方

FAQ

実行画面

技術的なはなし

プログラム構成

CPU

テキスト

グラフィック

PEGC

サウンド

サウンド関係はあまり知見がないので再現性はそれほど高くありませんが、一応現状の実装として以下に説明します。

ブロック構成

サウンド機能は、86音源相当の再現の他ビープ音の再生処理も行っています。CD-DAの再生についてはSCSIの機能ブロック、MIDIはMIDIのブロックで行っているためここには出てきません。

上記がサウンド機能に関する機能ブロックの概要です。FM音源とPCM音源、ミュート制御用のIOポート制御を行うほか、タイマ1の設定関係のポート制御(ポート73h、77h、35h、37h)を経由して、ビープのON/OFF制御が呼ばれます。
ポートへの出力、およびビープ制御が発生したら、そのときのタイムスタンプと共に設定された内容をキューに積みサウンドスレッドを起動します。以降の実際に鳴動に関わる処理はサウンドスレッドによって行われます。

●関連ソース
sound.c
sound_win.cpp(sound_osx.m)
sysmodule.c

サウンドスレッド

サウンドスレッドはイベントキューにサウンドイベントが積まれ、スレッドが起こされると起動します。起動後はキューを引き取りキューの内容に従って対応する処理関数を呼び鳴動処理を実施します。鳴動処理はXAudio2のストリーミング再生の機能を使用しています。
一度鳴動処理が発生した以降は、すべての音源の鳴動処理がOFFになるまでは、イベントが発生しなくても定期的に起きて鳴動処理を行います。サウンドは一旦音が鳴る設定を行ったらその後設定の変更がなくても鳴らし続けなければならないのですが、バッファがアンダーフローしないように常にバッファを更新し続ける必要があるためです。
ここでストリーミングの機能を用いた音声データのバッファリングについて簡単に説明します。
以下の図は、FM音源/SSG音源の鳴動用のバッファの使い方について図示したものです。実際はかなり頻繁に音データは更新されるためこんなに間隔が空くことはまれですが、説明の都合上間隔が空いた状態を例示します。

XAudio2に再生を指示するバッファの最小単位は10ms(PCMだけは13.33ms)で、これをUIで指定された数+3用意しリングバッファとして使用します。
初期状態はすべてのバッファは初期化された状態で鳴動処理も行われていません。
最初の設定が行われると、まず無音データで(UIで指定された)バッファリングサイズを埋め、無音で埋めた期間を再生開始します。これから以降はこの無音で先行した時間(図では50ms)に再生が追いつく前に常にバッファを更新し続ける必要があります。
最初の設定から5ms経過後に次の設定が来た場合、最初の設定によって鳴る音の波形が確定するため、最初の設定から今回の設定までの期間の音データを設定します。このように音データは常に一つ前の設定に基づいて決まる波形をその時点まで設定するという形をとります。
次にさらに10ms経過後に次の設定が来た場合を想定します。直前の10ms分の音データが確定し更新されるのは先のケースと同じですが、音データが10ms分更新されるので、その期間のバッファの再生指示を行います。
最後にバッファの更新がしばらく(実装では12msに設定)行われない場合、最後に設定された設定に基づいて経過した時間分バッファを更新します。このときもバッファは10ms分更新されるので再生指示が行われます。
再生はFM音源/SSG音源、リズム音源、ビープ、PCM音源の4つ分かれて行っています。
これはFM音源/SSG音源が、ステレオでサンプリングレートが44.1KHz/48KHzいずれか(UIで選択)なのに対し、リズム音源は44.1KHz固定、ビープはモノラル、PCMは再生バッファの最小単位が13.33msでサンプリングレートが44.1KHz/33.075KHzのいずれか(エミュレータ上のプログラムにより変化)バッファの数も固定(6つ)、のようにそれぞれ再生条件が異なるからです。

サイン波と矩形波

リズム音源とPCM音源はもともとPCMデータなので、リズム音源はあらかじめ用意したファイルのデータ、PCMはエミュレータ上で渡されるデータをそのまま鳴らすようになっていますが、FM音源はサイン波をベースに指定されたアルゴリズムに基づいて波形を変化させ音データとする必要があります。また、SSG音源とビープ音は矩形波に基づいて音データを作成する必要があります。
方法はいろいろあるのでしょうが、私はサイン波のテーブルと矩形波のテーブルをあらかじめ用意し、サンプリングのタイミングに合わせてテーブルの値を参照するという方法をとっています。

サイン波は0~π/2のサイン値をサンプリングレートの1536/325倍の配列として用意しています。これは1サンプル毎に配列を一つずつ読み出していった場合に周波数は0.052897Hzになります。一つおきに読み出すと倍の0.105794Hzになります。このようにこのテーブルを使うと、いくつおきの読み出すかを変えることで分解能0.052897Hzの任意のサイン波が取得できます。
一方、FM音源が基準とするサイン波の周波数はF-NumberとBlockの設定によって決められますが、そのときの周波数freqとF-Number,Blockの関係は、
freq = (MasterClock * F-Number * 2Block - 1) / (72 * 220)
で表すことが出来ます。ここでMasterClockは3993600Hzなので、最小の周波数はBlock=0、F-Number=1のときの0.026448Hzになります。またF-Numberが増えることによる増分も0.026448Hzなので、分解能が0.026448Hzであるといえます。
このことより上記のサイン波のテーブルを使うとBlockが0のときは1/2の精度、Blockの値が上がる毎に周波数の分解能は倍になるため、Blockが1以上は設定と同じ精度でサイン波を再現することが出来ることになります(Block=0のときの精度が1/2になるのはおそらく検討ミスだと思います。実装を確認したらBlock=0で、Fnumber=0または1のときサインテーブルのピックアップのインクリメント数が0になるため音が鳴らないことになります。もっとも実用上は問題にはならないと思いますが...)。

矩形波は0~π/2の区間サンプリングレートの3/2倍の配列として用意しています。これは分解能0.167Hzになります。
矩形波はSSG音源とビープ音で使用しますが、いずれもFM音源のサイン波と違って、指定された周波数とぴったりあう周波数がテーブルから作成できるわけではないので、テーブルで作成可能な近い周波数で再生することになります。
矩形波は算術的に、以下の式のように異なる周波数のサイン波の合成で表すことが出来ます。

f(t) = Σ(-1)l*sin(2π*t*(2l+1)/T)/(2l+1)
lはΣの係数で初期値0
Tは1周期にかかる時間
そこで、この計算式を元にテーブルを作成するのですが、Σの係数lは上限を大きくすれば正確な矩形波に近づく一方で大きくすることによって問題も出てきます。
例えばlの上限値を6とした場合、基準の周波数のサイン波の他に、3倍、5倍、7倍、9倍、11倍、13倍のサイン波の成分が含まれます。
この条件で矩形波テーブルを作成し、テーブルから2KHzの矩形波を作成した場合、テーブルからピックアップして作られた矩形波には2Khz、6KHz、10KHz、14KHz、18KHz、22KHz、26KHzのサイン波成分が含まれていることになります。ここでこの矩形波を、サンプリングレート44.1KHzで再生しようとした場合再現可能な周波数の上限はサンプリングレートの1/2である22.05KHzなので、矩形波の成分に含まれる26KHzは再生可能な周波数を超えているため正しく再現できず違う周波数のノイズとなって聞こえる場合があります。
そのため、特に高周波の音を再現するためにはあまり多くのサイン波要素を含めることが出来ません。一方でサイン波要素が少ないと波形の立ち上がり、立ち下がりが緩くなり、低周波の音を鳴らそうとすると音がひずんで聞こえます。
このため、現在の実装では含まれるサイン波の要素が異なる(lの上限が異なる)矩形波テーブルを4つ用意し、その中から最適なテーブルを選択するようにしています。ただ、高めの周波数の再現性を優先させいているため、低い周波数はやはりひずんで聞こえてしまいます。

ビープ音

ビープ音の矩形波のソースはインターバルタイマ(μPD8253)になります。このインターバルタイマは6つのモードを持っていて、モードと設定したクロックカウンタに応じてそれぞれ信号がHighになる期間とLowになる期間が異なるようになっています。簡単にモード別の違いを書くと、

モード0、カウンタをデクリメントし0に達するまではLow、0に達するとHigh、ここで終了し繰り返しはなし、カウントの開始はカウンタ設定直後
モード1、カウンタをデクリメントし0に達するまではLow、0に達するとHigh、ここで終了し繰り返しはなし、カウントの開始はGATE信号の立ち上がりエッジ検出
モード2、カウンタをデクリメントし0に達するまではHigh、0に達すると1クロックLowで次からはまたHighになり再度カウントを行う
モード3、カウンタを2ずつ減らし、0に達するまではHigh、カウンタを元に戻し再度2ずつ減らし0に達するまでLow、Lowの期間が終わるとHighになり再度カウントを行う。
モード4、カウンタをデクリメントし0に達するまではHigh、0に達すると1クロックLowで次からはまたHighになり終了、繰り返しはなし、カウントの開始はカウンタ設定直後
モード5、カウンタをデクリメントし0に達するまではHigh、0に達すると1クロックLowで次からはまたHighになり終了、繰り返しはなし、カウントの開始はGATE信号の立ち上がりエッジ検出
通常ビープ音は、モード3のHighとLowが等間隔に交互に繰り返し発生するモードを使用します。カウンタの値を変えることで周期を変えることが出来るので音程も変えることが出来ます。
しかし、まれにモード0を使用してビープ音をならすケースがあります。このケースが見られるのはビープ音でサンプリングデータを再生するときです。
ビープ音でサンプリング再生する原理は(正確に理解できているわけではありませんが)、まず単位時間を決め、その単位時間の中でLowの期間とHighの期間の時間量を出力レベルに見立て、擬似的にサンプリングの波形を再現させているように見受けられます。つまり、単位時間中すべてHighならば最大出力、すべてLowなら最小出力、HighとLowの期間が同じ場合は中間の出力レベルという感じです。
具体的な処理としては、タイマのモードをカウントを一回で終えるモード0を指定し、これを単位時間毎に定期的にタイマを開始します。タイマを開始するにあたりカウンタの値は、その単位期間中どの位Lowの期間にするか(音量をどの程度にするか)で都度値を変えることで、単位時間あたりの音量に見立てたLowとHighの比を変化させます。
そこで、そうであるならば逆にモード0でビープの再生する場合は、単位時間あたりのLowとHighの割合を音量に変えて出力すれば本来再生させたかった音が再現できる(はず)と考え、本エミュレータではモード0のビープ再生のときは矩形波によるHigh、Lowの再現ではなく単位時間中のHighとLowの期間を音量に変換して出力するようにしています。ですので、ビープ音を使用したサンプリング再生は実機よりもクリアに聞こえるかもしれません。