WebアプリケーションでPSG(Programmable Sound Generator)を再現するドライバ(ソフトウェアPSGエミュレータ)「PSG for Web」を作成しました。
関連ページを集めたポータルとして「PSGで遊ぼう!」を開設しました。
PSGとは
PSG(Programmable Sound Generator)とは、狭義にはゼネラル・インスツルメンツ社(GI)の音源LSI「AY-3-8910」を指しますが、広義には同様の原理による音源チップや音源を指します。
【参考】Programmable Sound Generator – Wikipedia
PSGは、デューティ比1:1(高と低の時間が均等)の矩形波を基本波形とし、エンベロープ(時間に対して音量を変化させること)を組み合わせることで様々な音色を出すことができます。
PSG for Webの目的
このドライバの目的は、昔のパソコンソフト(主にゲーム)をWebアプリケーションとして復刻する際に、当時の雰囲気を再現するとともに、近年ひとつの表現手法として認識されている「チップチューン」の制作に資することを目的としています。
PSG for Webの特徴
「PSG for Web」は、AY-3-8910の動作を再現します。AY-3-8910では14個のパラメータ(レジスタ)を指定することにより様々な音を出すことができます。AY-3-8910は外部から供給されるクロック周波数とレジスタの値により出力される周波数(音の高さ)が決まりますが、「PSG for Web」では、デフォルトではMSXの規格に合わせてクロック周波数を「1.7897725MHz」(CPUのクロック周波数の半分)として処理しています。
また、「PSG for Web」は、MSX BASICのPLAY命令(MMLによる音楽演奏)とSOUND命令(レジスタ値を直接設定)に相当する機能を提供します。
使い方
初期化
var psg = new PSGWEB();
psg.init(callback);
まず、PSGWEBをnewしてオブジェクトを生成します。続いて、生成したオブジェクトのinitメソッドを呼び出して初期化します。initメソッドは非同期処理であるため、処理が終わり次第callbackで指定された関数を呼び出します。Web Audio APIの仕様により、ユーザーのアクション(ボタンのクリック等)を起点にしないと音が出ないため、initメソッドはユーザーアクションのイベントハンドラから呼び出す必要があります。
シンプルなコーディング例
例として、MSX BASICで下記のプログラムを実行した場合に相当するプログラムのコーディング例を示します。
10 PLAY "T220 S0 M3000 O3 L8 D D R8 D R8 D D R8 O4G4 R4 O3G4 R4","T220 S0 M3000 O4 L8 F# F# R8 F# R8 F# F# R8 B4 R4 R2","T220 S0 M3000 O5 L8 E E R8 E R8 C E R8 G4 R4 O4G4 R4"
<!doctype html>
<html>
<head>
<meta charset="UTF-8">
<script src="https://www.minagi.jp/apps/util/psgweb-1.3.2.js"></script>
<script src="psgforwebsample.js"></script>
<title>PSG for Web sample</title>
</head>
<body>
<button id="play" onClick="play();"><!-- ボタンがクリックされたら、JavaScriptのplayメソッドを呼び出す -->
<h1>PLAY</h1>
</button>
</body>
</html>
var psg;
window.onload = function () {
psg = new PSGWEB();
}
function play() {
psg.init(main);
}
function main() {
psg.play(
'T220 S0 M3000 O3 L8 D D R8 D R8 D D R8 O4G4 R4 O3G4 R4',
'T220 S0 M3000 O4 L8 F# F# R8 F# R8 F# F# R8 B4 R4 R2',
'T220 S0 M3000 O5 L8 E E R8 E R8 C E R8 G4 R4 O4G4 R4',
1
);
}
実行例(「PLAY」ボタンを押すと音が出ます。♪「スーパーマリオブラザーズ」より)
「new PSGWEB();」の箇所で、パラメータとしてクロック周波数の値(単位はHz)を与えることにより、レジスタ値に対する音の高さを変えることができます。省略した場合は、1789772.5を指定したものとみなされます。
initメソッドで指定したコールバック関数が呼び出された場合、playメソッドやsoundメソッドで音を出すことができます。ブラウザがWeb Audio APIに対応していない場合、コールバック関数は呼び出されませんので、コールバック関数が呼ばれなくても全体の処理が止まらないようにプログラミングする必要があります。
演奏
play(mml1[,mml2[,mml3[,repeat]]]);
MMLで記述される音楽を演奏します。MSX BASICのPLAY命令に相当します。MMLもMSX BASICのものに準拠しています。戻り値はありません。MMLに文法違反があった場合、MSX BASICでは、Illegal function callエラーが発生しますが、PSG for Webでは例外がスローされることはなく、無視されます。
バージョン1.2.0で第4引数repeatが追加されました。正の整数で繰り返し演奏回数を指定します。0を指定した場合、回数無制限で演奏し続けます。省略した場合は、1を指定したものとみなされます。
pause(boolean);
バージョン1.2.0で追加されたメソッドです。引数にtrueを指定すると演奏を一時停止します。falseを指定すると一時停止したところから再開します。
stop();
バージョン1.2.0で追加されたメソッドです。演奏を終了します。演奏中のスケジュールもクリアされます。pause(false);を実行しても再開されません。
sound(register,value);
PSGのレジスタ値を直接設定します。MSX BASICのSOUND命令に相当します。registerがレジスタ番号(0〜13)、valueがレジスタに設定する値です。戻り値はありません。
各レジスタの意味はこちらをご参照ください。
setOnEnded(callback);
playメソッドによるMMLの演奏が終わったときに呼び出されるイベントハンドラを指定します。このイベントハンドラで次の演奏を行うことでリピート演奏を実現できます。
バージョン1.2.0では標準でリピート演奏がサポートされましたので、このイベントハンドラでリピート演奏を行う必要はなくなりました。なお、リピート演奏中はcallbackで指定したイベントハンドラは呼び出されません。
PSG for Webの構成
PSG for Webは、Web Audio APIのAudioWorkletによりPSGの動作を再現しています。しかし、2020年11月時点ではiOSの各ブラウザ、macOS版SafariはAudioWorkletに対応していないため、前世代の仕様であるScriptProcessorでも同じ処理を実装しており、AudioWorkletを使用できない場合はScriptProcessorにより動作します。
【2021年5月21日追記】Safari も14.1でAudioWorkletに対応したようです。
PSG for Webは次のような構成になっています。
ファイル名 | 説明 |
psgweb-x.y.z.js | PSG for Web本体です。MSX BASICのPLAY命令に相当するMMLのパース機能を提供します。また、ScriptProcessorによるPSGエミュレーション機能も提供します。 |
psgawp-x.y.z.js | AudioWorkletによるPSGエミュレーションルーチンです。Workletの仕様により、HTMLのscriptタグではなく、psgweb-x.y.z.jsからHTTP(S)により呼び出されます。 |
サンプルプログラム
PSGピアノ
ソースコード
<!doctype html>
<html>
<head>
<meta charset="UTF-8">
<title>Piano</title>
<script src="https://www.minagi.jp/apps/util/psgweb-1.3.2.js"></script>
<script>
window.onload=function(){
document.getElementById('o4c+').addEventListener('click',clickFunc);
document.getElementById('o4d+').addEventListener('click',clickFunc);
document.getElementById('o4f+').addEventListener('click',clickFunc);
document.getElementById('o4g+').addEventListener('click',clickFunc);
document.getElementById('o4a+').addEventListener('click',clickFunc);
document.getElementById('o4c').addEventListener('click',clickFunc);
document.getElementById('o4d').addEventListener('click',clickFunc);
document.getElementById('o4e').addEventListener('click',clickFunc);
document.getElementById('o4f').addEventListener('click',clickFunc);
document.getElementById('o4g').addEventListener('click',clickFunc);
document.getElementById('o4a').addEventListener('click',clickFunc);
document.getElementById('o4b').addEventListener('click',clickFunc);
document.getElementById('o5c').addEventListener('click',clickFunc);
}
function clickFunc(event){
tone=event.target.id;
if(!psg.enable){
psg.init(()=>{psg.play('V15T120L4'+tone);});
}else{
psg.play('V15T120L4'+tone);
}
}
var psg=new PSGWEB();
</script>
<style>
/* https://www.musicca.com/jp/piano */
.piano {
list-style: none;
text-align: center;
margin-top: .5rem;
display: flex;
width: 100%;
overflow: hidden;
height: 200px;
padding: 0;
}
.key {
display: inline-flex;
flex-grow: 1;
position: relative;
justify-content: center;
cursor: pointer;
}
.key>span {
display: flex;
justify-content: flex-end;
align-items: center;
flex-direction: column;
}
.whitekey {
margin: 0;
width: 100%;
height: 190px;
border: 1px solid black;
}
.blackkey {
height: 110px;
position: absolute;
top: 0;
right: -40%;
width: 80%;
z-index: 10;
color: white;
background: black;
border: 1px solid black;
}
</style>
</head>
<body>
<div>
<ul class="piano">
<li class="key"> <span class="whitekey" id="o4c">C</span> <span class="blackkey" id="o4c+">C#</span> </li>
<li class="key"> <span class="whitekey" id="o4d">D</span> <span class="blackkey" id="o4d+">D#</span> </li>
<li class="key"> <span class="whitekey" id="o4e">E</span> </li>
<li class="key"> <span class="whitekey" id="o4f">F</span> <span class="blackkey" id="o4f+">F#</span> </li>
<li class="key"> <span class="whitekey" id="o4g">G</span> <span class="blackkey" id="o4g+">G#</span> </li>
<li class="key"> <span class="whitekey" id="o4a">A</span> <span class="blackkey" id="o4a+">A#</span> </li>
<li class="key"> <span class="whitekey" id="o4b">B</span> </li>
<li class="key"> <span class="whitekey" id="o5c">C</span> </li>
</ul>
</div>
</body>
</html>
MMLによる演奏
MMLの文法は、こちらをご参照ください。
ソースコード
<!doctype html>
<html>
<head>
<meta charset="UTF-8">
<title>PSG for Web demo</title>
<script src="https://www.minagi.jp/apps/util/psgweb-1.3.2.js"></script>
<script>
window.onload=function(){
document.getElementById('play').addEventListener('click',clickFunc);
}
function clickFunc(event){
if(!psg.enable){
psg.init(()=>{playFunc();});
}else{
playFunc();
}
}
function playFunc(){
psg.play(
document.getElementById('mml1').value,
document.getElementById('mml2').value,
document.getElementById('mml3').value
);
}
var psg=new PSGWEB();
</script>
<style>
.mml {
width: 100%;
height: 120px;
border-radius: 10px;
}
</style>
</head>
<body>
<button id="play" style="width: 100%;">♪ 演奏する</button>
<br>
<label for="mml1">MML 1:</label>
<textarea id="mml1" class="mml">t100v15o5l8
a16.r32a16.r32a8.r16a16.r32a16.r32a8.r16ao6co5f.g16a2
b-16.r32b-16.r32b-r16 b-32.r64b-a16.r32a16.r32 a16.r32ag16.r32gfgo6c4.
o5a16.r32a16.r32a8.r16a16.r32a16.r32a8.r16ao6co5f.g16a2
b-16.r32b-16.r32b-r16b-32.r64b-a16.r32a16.r32ao6c16.r32co5b-gf2</textarea>
<br>
<label for="mml2">MML 2:</label>
<textarea id="mml2" class="mml">t100v15o5l8
f16.r32f16.r32f8.r16f16.r32f16.r32f8.r16fr8r4f2
f16.r32fgr16g32.r64gf16.r32f16.r32f16.r32fr8r8r8r8e4.
f16.r32f16.r32f8.r16f16.r32f16.r32f8.r16fr8r8.r16f2
f16.r32f16.r32fr16f32.r64fr8r8r8e16.r32er8r8r2</textarea>
<br>
<label for="mml3">MML 3:</label>
<textarea id="mml3" class="mml">t100v15o4l4
fedcfedcdef8.r16fg8.r16gl8cdec
l4fedcfedcf8.r16f8.r16f8.r16fg8.r16gf2</textarea>
<br>
</body>
</html>
その他
本作と目的が似ている「FONTX for Web」はネット上を探してみても同じことをしている例が見つからなかったのですが、こちらはすでに多くの方が同様の取り組みをされているようです。「チップチューン」の隆盛がうかがわれます。
参考文献
自作のWASMプリプロセッサを使ってPSGエミュレータを作った。 – sfpgmr氏
emu2149 – Digital Sound Antiques氏
ツール【動画あり】
PSG for Webの機能を試すことができる「PSG Pad」を公開しました。サンプル動画もあわせてご覧ください。
改訂履歴
日付 | バージョン | 内容 |
2024/2/8 | 1.3.2 | AudioNode.connectメソッドがAudioNodeオブジェクトではなく、undefinedを返す古いブラウザ(Safari 11など)に対応。 |
2024/2/7 | 1.3.1 | Chromeで実行時にエラーになる不具合を修正しました。 新しいChromeのWeb Audio APIの実装では、AudioWorkletProcessor派生クラスをregisterProcessorメソッドで登録してから、AudioContextをdistinationにconnectするまでの間に、AudioWorkletProcessor派生クラスのprocessメソッドが、引数outputs(出力器の配列)が初期化されていない状態で呼び出されてしまうようで、outputsの要素数を取得する処理でエラーになっていました。 |
2021/10/30 | 1.3.0 | MSXマガジン(アスキー)1987年3月号・4月号に掲載された「究極の音楽演奏システム」(いわゆる「究極のPSG」)の拡張MMLに対応しました。 I … ノイズの制御。チャンネルごとに、0(ノイズON・周波数変更なし)、1〜31(ノイズON・周波数変更<SOUND 6,nに相当>)、32(ノイズOFF・周波数変更なし) Z … 周波数の制御。チャンネルごとに、音を指定した値÷128オクターブ下げます。 |
2021/7/28 | 1.2.1 | ・playメソッドのMMLでM,N,S,T,Vに0を指定したときに正しく設定されない不具合を修正。 |
2021/7/21 | 1.2.0 | ・playメソッドに第4引数として繰り返し回数を追加。指定した回数演奏を繰り返します。0を指定して場合は回数無制限のループ演奏になります。 ・pauseメソッドを追加。引数はbooleanで、trueで一時停止、falseで一時停止を解除します。 ・stopメソッドを追加。引数はなし。演奏中のスケジュールもクリアされます。 ・開発過程の不要なコードを削除。 |
2021/3/13 | 1.1.1 | ・playメソッドにおいて、あるチャンネルでS(エンベロープパターンの指定)を使用したとき、他のチャンネルもエンベロープモードになってしまう不具合を修正。 |
2021/2/8 | 1.1.0 | ・音量レベルを調整し、WebMSXと同等になるようにしました。1.0.0より音が大きくになります。 ・getCurrentSchedulesメソッドを追加。その時点で設定されている演奏スケジュールを返します。形式は{R:レジスタ番号,value:値,time:演奏開始からの秒数}の配列です。 ・getAnalyserメソッドを追加。Web Audio APIのAnalyserノードを返します。 ・レジスタ7の各チャンネルのトーンに対応するビットが1(発声オフ)になっていてもマイナス側(下半分)の波形が出力される不具合を修正。 |
2020/12/12 | 1.0.0 | 初回リリース |