UNPKG

@malvineous/opl

Version:

Yamaha OPL2/3 FM synth chip emulator

164 lines (145 loc) 5.49 kB
/* * Example program for using the OPL synth from within a web browser. * * You will probably need to run serve-demo-web.js from within Node to set up * a small web server hosting this file, as some browsers have difficulty * loading WebAssembly files from file:// URLs. * * Copyright (C) 2018 Adam Nielsen <malvineous@shikadi.net> * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see <http://www.gnu.org/licenses/>. */ let AudioContext = window.AudioContext || window.webkitAudioContext; let audioCtx = new AudioContext(); let source = audioCtx.createBufferSource(); let scriptNode = audioCtx.createScriptProcessor(8192, 2, 2); // When the buffer source stops playing, disconnect everything source.onended = () => { console.log('source.onended()'); source.disconnect(scriptNode); scriptNode.disconnect(audioCtx.destination); scriptNode = null; source = null; } console.log('Sample rate', audioCtx.sampleRate); let mute = [], muteperc = 0; const imfdata = atob(` AAAAAAAACwC4AAAAsQAAALMAAAC0AAAAtQAAALYAAAAyAgAAUiIAAHLyAACSEwAA8gAAADUCAABV AQAAdfUAAJVDAAD1AAAAyA4AAKggAAC4LgAAIQIAAEEiAABh8gAAgRMAAOEAAAAkAgAARAEAAGT1 AACEQwAA5AAAAMEOAAChIAAAsSoAACgRAABIigAAaPEAAIgRAADoAAAAKwEAAEtBAABr8QAAi7MA AOsAAADDAQAAoyAAALMuAAApEQAASYoAAGnxAACJEQAA6QAAACwBAABMQQAAbPEAAIyzAADsAAAA xAEAAKQgAAC0KgAAKgUAAEpOAABq2gAAiiUAAOoAAAAtAQAATQEAAG35AACNFQAA7QAAAMUKAACl MAAAtTcAADAyAABQRAAAcPgAAJD/AADwAAAAMxEAAFMBAABz9QAAk38AAPMAAADGDgAApjAAALYz cACyAAAAthMAACIFAABCTgAAYtoAAIIlAADiAAAAJQEAAEUBAABl+QAAhRUAAOUAAADCCgAAojAA ALIzAACmmAAAtjFwALUXAAC2EQAApSAAALU2AAC2MXAAshMAALYRAACiYwAAsjYAAKYwAAC2M3AA tRYAAKWYAAC1NXAAshYAALYTAACiIAAAsjYAAKaYAAC2MXAAtRUAALU1cACyFgAAthEAAKIwAACy NwAApjAAALYzcAC1FQAAtTVwALIXAAC2EwAAoiAAALI2AACmmAAAtjFwALUVAAC1NXAAshYAALYR AACiYwAAsjYAALYxcAC1FQAAthEAALU1AACmMAAAtjNwALIWAACiIAAAsjZwALUVAAC2EwAAtTUA AKaYAAC2MeAAuA4AALEKAACyFgAAsw4AALQKAAC2EQAAqGMAALguAAChYwAAsSoAAKLWAACyNgAA o2MAALMuAACkYwAAtCoAAKbWAAC2MnAAtRUAALYSAAC1NQAApmsAALYxcACyFgAAthEAAKIgAACy NgAAtjFwALUVAAC2EQAApWMAALU2AACm1gAAtjJwALIWAACimAAAsjVwALUWAAC2EgAApSAAALU2 AACmawAAtjFwALIVAACyNXAAtRYAALYRAACl1gAAtTYAAKbWAAC2MnAAshUAALI1cAC1FgAAthIA AKUgAAC1NgAApmsAALYxcACyFQAAsjVwALUWAAC2EQAApWMAALU2AAC2MXAAshUAALYRAACyNQAA ptYAALYycAC1FgAApSAAALU2cACyFQAAthIAALI1AACmawAAtjHUAA==`); const imf = new Uint8Array(imfdata.length); for (let i = 0; i < imfdata.length; i++) { imf[i] = imfdata.charCodeAt(i); } const samplesPerTick = Math.round(audioCtx.sampleRate / 560); console.log('Init WASM'); OPL.create(audioCtx.sampleRate).then(opl => { console.log('WASM init done'); let p = 0; let lenGen = 0; scriptNode.onaudioprocess = audioProcessingEvent => { var b = audioProcessingEvent.outputBuffer; var c0 = b.getChannelData(0); var c1 = b.getChannelData(1); let lenFill = b.length; let posFill = 0; while (posFill < lenFill) { // Fill any leftover delay from the last buffer-fill event first while (lenGen > 0) { if (lenFill - posFill < 2) { // No more space in buffer return; } let lenNow = Math.max(2, Math.min(512, lenGen, lenFill - posFill)); const samples = opl.generate(lenNow, Int16Array); //const samples = new Int16Array(s); for (let i = 0; i < lenNow; i++) { c0[posFill] = samples[i * 2 + 0] / 32768.0; c1[posFill] = samples[i * 2 + 1] / 32768.0; posFill++; } lenGen -= lenNow; } let delay; do { // Read the song event const reg = imf[p + 0]; let val = imf[p + 1]; delay = imf[p + 2] | (imf[p + 3] << 8); // Force the 'note-on' bit off, if the channel is muted if ((reg & 0xF0) == 0xB0) { if (reg == 0xBD) { val &= ~muteperc; } else if (mute[reg & 0x0F]) { val &= ~0x20; } }// else console.log(reg.toString(16), (reg & 0xF0).toString(16), (reg & 0xF0); opl.write(reg, val); // Advance to the next event in the song p += 4; if (p >= imf.length) { console.log('Looping'); p = 0; // loop } } while (!delay); document.getElementById('progress').firstChild.nodeValue = Math.round(p / imf.length * 100) + '%'; lenGen += delay * samplesPerTick; } }; scriptNode.connect(audioCtx.destination); source.connect(scriptNode); source.start(); audioCtx.suspend(); console.log('Ready'); }); document.getElementById('play').onclick = () => { audioCtx.resume(); console.log('Play'); }; document.getElementById('pause').onclick = () => { audioCtx.suspend(); console.log('Pause'); }; for (let i = 0; i < 9; i++) { mute[i] = false; const ct = document.getElementById('ch' + i); ct.className = 'play'; ct.onclick = ev => { mute[i] = !mute[i]; ev.target.className = mute[i] ? 'mute' : 'play'; }; } for (let i = 0; i < 5; i++) { const ct = document.getElementById('p' + i); ct.className = 'play'; ct.onclick = ev => { muteperc ^= 1 << i; const muted = !!(muteperc & (1 << i)); ev.target.className = muted ? 'mute' : 'play'; }; }