From 008f39a5f4070332d547ad554088fc23d8936dd2 Mon Sep 17 00:00:00 2001 From: Dylan Muller Date: Tue, 5 Sep 2023 23:37:23 +0200 Subject: emu8910: core: Add demo files Add demo files. --- README.md | 12 +- emu8910.js | 574 ++++++++++++++++++++++++++ emu8910.ts | 917 ----------------------------------------- fym.js | 66 +++ index.html | 7 + music/01_scalesmannmisfire.fym | Bin 0 -> 2092 bytes pako_inflate.min.js | 1 + parser.js | 79 ++++ src/emu8910.ts | 774 ++++++++++++++++++++++++++++++++++ 9 files changed, 1511 insertions(+), 919 deletions(-) create mode 100644 emu8910.js delete mode 100644 emu8910.ts create mode 100644 fym.js create mode 100644 index.html create mode 100644 music/01_scalesmannmisfire.fym create mode 100644 pako_inflate.min.js create mode 100644 parser.js create mode 100644 src/emu8910.ts diff --git a/README.md b/README.md index adee73c..ff89f40 100644 --- a/README.md +++ b/README.md @@ -6,8 +6,16 @@ It implements most of the PSG's original registers. A datasheet can be found [he Sound output is achieved in the browser through an AudioContext() hook.
This emulator also adds interrupt support (with variable frequency) for updating the PSG's registers. -This repository currently lacks a working example of the emulator which I plan to add in the future. +FIR filter data generated using: +[https://www.arc.id.au/FilterDesign.html](https://www.arc.id.au/FilterDesign.html) Files: -* emu8910.ts - Core emulator implementation +* src/emu8910.ts - Core emulator implementation +* fym.js - FYM (Fast YM) format parser +* update.js - Register parser +* index.html - HTML boilerplate + +To run demo start web server: `python -m http.server 8000` and navigate to `index.html`. +Then click anywhere on the page to start audio output. + diff --git a/emu8910.js b/emu8910.js new file mode 100644 index 0000000..d05fb78 --- /dev/null +++ b/emu8910.js @@ -0,0 +1,574 @@ +// Copyright (C) +// Author: Dylan Muller +var YM_CLOCK_ZX = 1750000; +var FIR = [-0.011368, + 0.004512, + 0.008657, + -0.011763, + -0.000000, + 0.012786, + -0.010231, + -0.005801, + 0.015915, + -0.006411, + -0.012504, + 0.017299, + -0.000000, + -0.019605, + 0.016077, + 0.009370, + -0.026526, + 0.011074, + 0.022508, + -0.032676, + 0.000000, + 0.042011, + -0.037513, + -0.024362, + 0.079577, + -0.040604, + -0.112540, + 0.294080, + 0.625000, + 0.294080, + -0.112540, + -0.040604, + 0.079577, + -0.024362, + -0.037513, + 0.042011, + 0.000000, + -0.032676, + 0.022508, + 0.011074, + -0.026526, + 0.009370, + 0.016077, + -0.019605, + -0.000000, + 0.017299, + -0.012504, + -0.006411, + 0.015915, + -0.005801, + -0.010231, + 0.012786, + -0.000000, + -0.011763, + 0.008657, + 0.004512, + -0.011368]; +var Interpolator = /** @class */ (function () { + function Interpolator() { + this.buffer = []; + for (var i = 0; i < 4; i++) { + this.buffer[i] = 0x0; + } + } + Interpolator.prototype.step = function (x) { + var b = this.buffer; + b[0] = b[1]; + b[1] = b[2]; + b[2] = b[3]; + b[3] = x; + }; + Interpolator.prototype.cubic = function (mu) { + var b = this.buffer; + var a0, a1, a2, a3, mu2 = 0; + mu2 = mu * mu2; + a0 = b[3] - b[2] - b[0] + b[1]; + a1 = b[0] - b[1] - a0; + a2 = b[2] - b[0]; + a3 = b[1]; + return (a0 * mu * mu2 + a1 * mu2 + a2 * mu + a3); + }; + return Interpolator; +}()); +// DC filter +var BiasFilter = /** @class */ (function () { + function BiasFilter(length, attenuate) { + this.samples = []; + this.index = 0x0; + this.length = 0x0; + this.sum = 0x0; + this.attenuate = 0x0; + this.length = length; + this.sum = 0x0; + for (var i = 0; i < this.length; i++) { + this.samples[i] = 0x0; + } + this.attenuate = attenuate; + } + BiasFilter.prototype.step = function (x) { + var index = this.index; + var delta = x - this.samples[index]; + var attenuate = this.attenuate; + var avg = 0x0; + this.sum += delta; + this.samples[index] = x; + if (++this.index > (this.length - 1)) { + this.index = 0x0; + } + avg = this.sum / this.length; + return (x - avg) * (1 / attenuate); + }; + return BiasFilter; +}()); +var FirFilter = /** @class */ (function () { + function FirFilter(h, m) { + this.buffer = []; + this.index = 0x0; + this.offset = 0x0; + this.length = 0x0; + this.m = 0x0; + this.h = []; + this.length = h.length * m; + this.index = 0; + this.m = m; + this.h = h; + var buffer = this.buffer; + for (var i = 0; i < this.length * 2; i++) { + buffer[i] = 0x0; + } + } + FirFilter.prototype.step = function (samples) { + var index = this.index; + var buffer = this.buffer; + var length = this.length; + var m = this.m; + var h = this.h; + var y = 0x0; + var i = 0x0; + this.offset = length - (index * m); + var sub = buffer.slice(this.offset); + this.index = (index + 1) % (length / m - 1); + for (i = m - 1; i >= 0; i--) { + buffer[this.offset + i] = samples[i]; + } + for (i = 0; i < h.length; i++) { + y += h[i] * sub[i]; + } + for (i = 0; i < m; i++) { + buffer[this.offset + length - m + i] = buffer[this.offset + i]; + } + return y; + }; + return FirFilter; +}()); +var AudioDriver = /** @class */ (function () { + function AudioDriver(host) { + this.frequency = 0x0; + this.update = function (ev) { + var ch0 = ev.outputBuffer.getChannelData(0); + var ch1 = ev.outputBuffer.getChannelData(1); + var host = this.host; + var filter = this.filter; + var bias = this.bias; + var output = [0, 0]; + var port = [0, 0]; + for (var i = 0; i < ch0.length; i++) { + output = host.step(); + port[0] = filter[0].step(output[0]); + port[1] = filter[1].step(output[1]); + ch0[i] = bias + port[0]; + ch1[i] = bias + port[1]; + } + }.bind(this); + this.device = new AudioContext(); + var device = this.device; + this.filter = [ + new BiasFilter(1024, 1.25), + new BiasFilter(1024, 1.25), + device.createBiquadFilter(), + device.createBiquadFilter() + ]; + var filter = this.filter; + filter[2].type = "lowshelf"; + filter[2].frequency.value = 10000; + filter[2].gain.value = 2; + filter[3].type = "lowpass"; + filter[3].frequency.value = 10000; + filter[3].Q.value = 1; + this.frequency = device.sampleRate; + this.context = device.createScriptProcessor(4096, 0, 2); + this.context.onaudioprocess = this.update; + this.context.connect(filter[2]); + filter[2].connect(filter[3]); + filter[3].connect(device.destination); + this.host = host; + this.bias = 1 / 100; + } + return AudioDriver; +}()); +var PSG49_LUT; +(function (PSG49_LUT) { + PSG49_LUT[PSG49_LUT["A_FINE"] = 0] = "A_FINE"; + PSG49_LUT[PSG49_LUT["A_COARSE"] = 1] = "A_COARSE"; + PSG49_LUT[PSG49_LUT["B_FINE"] = 2] = "B_FINE"; + PSG49_LUT[PSG49_LUT["B_COARSE"] = 3] = "B_COARSE"; + PSG49_LUT[PSG49_LUT["C_FINE"] = 4] = "C_FINE"; + PSG49_LUT[PSG49_LUT["C_COARSE"] = 5] = "C_COARSE"; + PSG49_LUT[PSG49_LUT["NOISE_PERIOD"] = 6] = "NOISE_PERIOD"; + PSG49_LUT[PSG49_LUT["MIXER"] = 7] = "MIXER"; + PSG49_LUT[PSG49_LUT["A_VOL"] = 8] = "A_VOL"; + PSG49_LUT[PSG49_LUT["B_VOL"] = 9] = "B_VOL"; + PSG49_LUT[PSG49_LUT["C_VOL"] = 10] = "C_VOL"; + PSG49_LUT[PSG49_LUT["ENV_FINE"] = 11] = "ENV_FINE"; + PSG49_LUT[PSG49_LUT["ENV_COARSE"] = 12] = "ENV_COARSE"; + PSG49_LUT[PSG49_LUT["ENV_SHAPE"] = 13] = "ENV_SHAPE"; +})(PSG49_LUT || (PSG49_LUT = {})); +var PSG49 = /** @class */ (function () { + function PSG49(clockRate, intRate) { + // main register file + this.register = { + A_FINE: 0x0, A_COARSE: 0x0, + B_FINE: 0x0, B_COARSE: 0x0, + C_FINE: 0x0, C_COARSE: 0x0, + NOISE_PERIOD: 0x0, + // bit position + // 5 4 3 2 1 0 + // NC NB NA TC TB TA + // T = Tone, N = Noise + MIXER: 0x0, + A_VOL: 0x0, + B_VOL: 0x0, + C_VOL: 0x0, + ENV_FINE: 0x0, ENV_COARSE: 0x0, + ENV_SHAPE: 0x0, + PORT_A: 0x0, + PORT_B: 0x0 + }; + this.driver = new AudioDriver(this); + this.interpolate = [ + new Interpolator(), + new Interpolator() + ]; + var m = 8; + this.fir = [ + new FirFilter(FIR, m), + new FirFilter(FIR, m) + ]; + this.oversample = m; + this.clock = { + frequency: clockRate, + scale: 1 / 16 * 2, + cycle: 0, + step: 0 + }; + this.interrupt = { + frequency: intRate, + cycle: 0, + routine: function () { } + }; + this.envelope = { + strobe: 0, + transient: 0, + step: 0, + shape: 0, + offset: 0, + stub: [] + }; + this.channels = [ + { + counter: 0x0, + pan: 0.5, + }, + { + counter: 0x0, + pan: 0.5 + }, + { + counter: 0x0, + pan: 0.5 + }, + { counter: 0x0 } + ]; + // seed noise generator + this.channels[3].port = 0x1; + this.dac = []; + this.build_dac(1.3, 40); + this.build_adsr(); + } + PSG49.prototype.build_dac = function (decay, shift) { + var dac = this.dac; + var y = Math.sqrt(decay); + var z = shift / 31; + dac[0] = 0; + dac[1] = 0; + for (var i = 2; i <= 31; i++) { + dac[i] = 1.0 / Math.pow(y, shift - (z * i)); + } + }; + PSG49.prototype.init_test = function () { + var r = this.register; + r.MIXER = 56; + r.A_VOL = 15; + //r.A_VOL |= 0x10; + r.A_FINE = 200; + //r.ENV_COARSE = 200; + }; + PSG49.prototype.build_adsr = function () { + var envelope = this.envelope; + var stub = envelope.stub; + stub.reset = function (ev) { + var strobe = ev.strobe; + var transient = ev.transient; + switch (ev.offset) { + case 0x4: + transient = 0; + case 0x0: + ev.step = strobe ? transient : 31; + break; + case 0x5: + transient = 31; + case 0x1: + ev.step = strobe ? transient : 0; + break; + case 0x2: + ev.step = 31; + break; + case 0x3: + ev.step = 0; + break; + } + }; + stub.grow = function (ev) { + if (++ev.step > 31) { + ev.strobe ^= 1; + ev.stub.reset(ev); + } + }; + stub.decay = function (ev) { + if (--ev.step < 0) { + ev.strobe ^= 1; + ev.stub.reset(ev); + } + }; + stub.hold = function (ev) { }; + envelope.matrix = [ + [stub.decay, stub.hold], + [stub.grow, stub.hold], + [stub.decay, stub.decay], + [stub.grow, stub.grow], + [stub.decay, stub.grow], + [stub.grow, stub.decay], + ]; + }; + PSG49.prototype.clamp = function () { + var r = this.register; + r.A_FINE &= 0xff; + r.B_FINE &= 0xff; + r.C_FINE &= 0xff; + r.ENV_FINE &= 0xff; + r.A_COARSE &= 0xf; + r.B_COARSE &= 0xf; + r.C_COARSE &= 0xf; + r.ENV_COARSE &= 0xff; + r.A_VOL &= 0x1f; + r.B_VOL &= 0x1f; + r.C_VOL &= 0x1f; + r.NOISE_PERIOD &= 0x1f; + r.MIXER &= 0x3f; + r.ENV_SHAPE &= 0xff; + }; + PSG49.prototype.map = function () { + var r = this.register; + var channel = this.channels; + var ev = this.envelope; + var toneMask = [0x1, 0x2, 0x4]; + var noiseMask = [0x8, 0x10, 0x20]; + this.clamp(); + // update tone channel period + channel[0].period = r.A_FINE | r.A_COARSE << 8; + channel[1].period = r.B_FINE | r.B_COARSE << 8; + channel[2].period = r.C_FINE | r.C_COARSE << 8; + channel[0].volume = r.A_VOL & 0xf; + channel[1].volume = r.B_VOL & 0xf; + channel[2].volume = r.C_VOL & 0xf; + for (var i = 0; i < 3; i++) { + var bit = r.MIXER & toneMask[i]; + channel[i].tone = bit ? 1 : 0; + } + for (var i = 0; i < 3; i++) { + var bit = r.MIXER & noiseMask[i]; + channel[i].noise = bit ? 1 : 0; + } + channel[0].envelope = (r.A_VOL & 0x10) ? 0 : 1; + channel[1].envelope = (r.B_VOL & 0x10) ? 0 : 1; + channel[2].envelope = (r.C_VOL & 0x10) ? 0 : 1; + // update channel noise period + channel[3].period = r.NOISE_PERIOD << 1; + ev.period = r.ENV_FINE | r.ENV_COARSE << 8; + ev.shape = r.ENV_SHAPE; + switch (ev.shape) { + case 0x0: + case 0x1: + case 0x2: + case 0x3: + case 0x9: + ev.transient = 0; + ev.offset = 0; + r.ENV_SHAPE = 0xff; + break; + case 0xb: + ev.transient = 31; + ev.offset = 0; + r.ENV_SHAPE = 0xff; + break; + case 0x4: + case 0x5: + case 0x6: + case 0x7: + case 0xf: + ev.transient = 0; + ev.offset = 1; + r.ENV_SHAPE = 0xff; + case 0xd: + ev.transient = 31; + ev.offset = 1; + r.ENV_SHAPE = 0xff; + break; + case 0x8: + ev.offset = 2; + break; + case 0xc: + ev.offset = 3; + break; + case 0xa: + ev.offset = 4; + break; + case 0xe: + ev.offset = 5; + break; + } + if (ev.shape != ev.store) { + ev.strobe = 0x0; + ev.counter = 0x0; + ev.stub.reset(ev); + } + ev.store = r.ENV_SHAPE; + }; + PSG49.prototype.step_tone = function (index) { + var ch = this.channels[index % 3]; + var step = this.clock.step; + var port = ch.port; + var period = (ch.period == 0x0) ? 0x1 : ch.period; + ch.counter += step; + if (ch.counter >= period) { + // 50% duty cycle + port ^= 0x1; + ch.port = port; + ch.counter = 0x0; + } + return ch.port; + }; + PSG49.prototype.step_envelope = function () { + var step = this.clock.step; + var ev = this.envelope; + ev.counter += step; + if (ev.counter >= ev.period) { + ev.matrix[ev.offset][ev.strobe](ev); + ev.counter = 0x0; + } + return (ev.step); + }; + PSG49.prototype.step_noise = function () { + var ch = this.channels[3]; + var step = this.clock.step; + var port = ch.port; + var period = (ch.period == 0) ? 1 : ch.period; + ch.counter += step; + if (ch.counter >= period) { + port ^= (((port & 1) ^ ((port >> 3) & 1)) << 17); + port >>= 1; + ch.port = port; + ch.counter = 0x0; + } + return ch.port & 1; + }; + PSG49.prototype.step_mixer = function () { + var port = 0x0; + var output = [0.0, 0.0]; + var index = 0x0; + var ch = this.channels; + var noise = this.step_noise(); + var step = this.step_envelope(); + for (var i = 0; i < 3; i++) { + var volume = ch[i].volume; + var pan = ch[i].pan; + port = this.step_tone(i) | ch[i].tone; + port &= noise | ch[i].noise; + // todo: add dac volume table + //bit*=toneChannel[i].volume; + // mix each channel + if (!ch[i].envelope) { + index = step; + } + else { + index = volume * 2 + 1; + } + port *= this.dac[index]; + // clamp pan levels + // distortion over +1 ? + if (pan > 0.9) { + pan = 0.9; + } + else if (pan < 0.1) { + pan = 0.1; + } + output[0] += port * (1 - pan); + output[1] += port * (pan); + } + return output; + }; + PSG49.prototype.step = function () { + var output = []; + var clockStep = 0; + var intStep = 0; + var i = 0x0; + var clock = this.clock; + var driver = this.driver; + var fir = this.fir; + var oversample = this.oversample; + var interpolate = this.interpolate; + var interrupt = this.interrupt; + var x = clock.scale; + var fc = clock.frequency; + var fd = driver.frequency; + var fi = interrupt.frequency; + clockStep = (fc * x) / fd; + clock.step = clockStep / oversample; + intStep = fi / fd; + // add number of clock cycle + interrupt.cycle += intStep; + // do we have clock cycles to process? + // if so process single clock cycle + var sample_left = []; + var sample_right = []; + for (i = 0; i < oversample; i++) { + sample_left[i] = 0x0; + sample_right[i] = 0x0; + } + if (interrupt.cycle > 1) { + interrupt.cycle--; + interrupt.routine(); + interrupt.cycle = 0; + } + for (var i_1 = 0; i_1 < oversample; i_1++) { + clock.cycle += clockStep; + if (clock.cycle > 1) { + clock.cycle--; + this.map(); + output = this.step_mixer(); + interpolate[0].step(output[0]); + interpolate[1].step(output[1]); + } + sample_left[i_1] = interpolate[0].cubic(0.5); + sample_right[i_1] = interpolate[1].cubic(0.5); + } + output[0] = fir[0].step(sample_left); + output[1] = fir[1].step(sample_right); + return output; + }; + return PSG49; +}()); diff --git a/emu8910.ts b/emu8910.ts deleted file mode 100644 index a057c2d..0000000 --- a/emu8910.ts +++ /dev/null @@ -1,917 +0,0 @@ -// Copyright (C) -// Author: Dylan Muller - -const YM_CLOCK_ZX = 1750000; -const FIR = - -[-0.000058, - 0.000024, - 0.000088, - -0.000003, - -0.000116, - -0.000035, - 0.000137, - 0.000089, - -0.000143, - -0.000156, - 0.000126, - 0.000231, - -0.000079, - -0.000304, - -0.000002, - 0.000362, - 0.000117, - -0.000390, - -0.000261, - 0.000375, - 0.000423, - -0.000304, - -0.000585, - 0.000168, - 0.000726, - 0.000033, - -0.000821, - -0.000294, - 0.000845, - 0.000596, - -0.000775, - -0.000915, - 0.000593, - 0.001215, - -0.000292, - -0.001456, - -0.000121, - 0.001594, - 0.000626, - -0.001589, - -0.001186, - 0.001405, - 0.001751, - -0.001025, - -0.002258, - 0.000444, - 0.002638, - 0.000316, - -0.002820, - -0.001211, - 0.002743, - 0.002175, - -0.002360, - -0.003117, - 0.001647, - 0.003933, - -0.000611, - -0.004512, - -0.000705, - 0.004744, - 0.002222, - -0.004535, - -0.003825, - 0.003817, - 0.005369, - -0.002558, - -0.006686, - 0.000773, - 0.007598, - 0.001470, - -0.007929, - -0.004048, - 0.007523, - 0.006783, - -0.006258, - -0.009449, - 0.004059, - 0.011782, - -0.000908, - -0.013485, - -0.003144, - 0.014246, - 0.007982, - -0.013741, - -0.013425, - 0.011632, - 0.019237, - -0.007546, - -0.025141, - 0.000999, - 0.030835, - 0.008787, - -0.036013, - -0.023415, - 0.040387, - 0.047128, - -0.043707, - -0.095861, - 0.045780, - 0.314841, - 0.453515, - 0.314841, - 0.045780, - -0.095861, - -0.043707, - 0.047128, - 0.040387, - -0.023415, - -0.036013, - 0.008787, - 0.030835, - 0.000999, - -0.025141, - -0.007546, - 0.019237, - 0.011632, - -0.013425, - -0.013741, - 0.007982, - 0.014246, - -0.003144, - -0.013485, - -0.000908, - 0.011782, - 0.004059, - -0.009449, - -0.006258, - 0.006783, - 0.007523, - -0.004048, - -0.007929, - 0.001470, - 0.007598, - 0.000773, - -0.006686, - -0.002558, - 0.005369, - 0.003817, - -0.003825, - -0.004535, - 0.002222, - 0.004744, - -0.000705, - -0.004512, - -0.000611, - 0.003933, - 0.001647, - -0.003117, - -0.002360, - 0.002175, - 0.002743, - -0.001211, - -0.002820, - 0.000316, - 0.002638, - 0.000444, - -0.002258, - -0.001025, - 0.001751, - 0.001405, - -0.001186, - -0.001589, - 0.000626, - 0.001594, - -0.000121, - -0.001456, - -0.000292, - 0.001215, - 0.000593, - -0.000915, - -0.000775, - 0.000596, - 0.000845, - -0.000294, - -0.000821, - 0.000033, - 0.000726, - 0.000168, - -0.000585, - -0.000304, - 0.000423, - 0.000375, - -0.000261, - -0.000390, - 0.000117, - 0.000362, - -0.000002, - -0.000304, - -0.000079, - 0.000231, - 0.000126, - -0.000156, - -0.000143, - 0.000089, - 0.000137, - -0.000035, - -0.000116, - -0.000003, - 0.000088, - 0.000024, - -0.000058] ; - - - -interface Channel{ - - port : number, - counter : number, - period : number, - volume : number, - pan : number, - tone : number, - noise : number, - envelope : number - -} - -interface Envelope{ - - counter : number, - period : number, - shape : number, - stub : any, - matrix : any, - strobe : number, - offset : number, - transient : number, - store : number, - step : number - -} - -interface Oscillator{ - - frequency: number, - scale : number, - cycle : number, - step : number - -} - -interface Interrupt{ - frequency : number, - routine : any, - cycle : number, -} - -class Interpolator{ - buffer : number[] = []; - - constructor(){ - for(let i = 0; i < 4; i++){ - this.buffer[i] = 0x0; - } - } - - step(x : number){ - let b = this.buffer; - b[0] = b[1]; - b[1] = b[2]; - b[2] = b[3]; - - b[3] = x; - } - - cubic(mu : number){ - - let b = this.buffer; - let a0,a1,a2,a3,mu2 = 0; - mu2 = mu * mu2; - a0 = b[3] - b[2] - b[0] + b[1]; - a1 = b[0] - b[1] - a0; - a2 = b[2] - b[0]; - a3 = b[1]; - - return (a0*mu*mu2 + a1*mu2 + a2*mu + a3); - } - -} - -// DC filter -class BiasFilter { - - samples : number[] =[]; - index : number = 0x0; - length : number = 0x0; - sum: number = 0x0; - attenuate : number = 0x0; - - constructor(length : number, attenuate : number){ - - this.length = length; - this.sum = 0x0; - - for(let i = 0; i < this.length; i++){ - this.samples[i] = 0x0; - } - this.attenuate = attenuate; - } - - step(x : number){ - let index = this.index; - let delta = x - this.samples[index]; - let attenuate = this.attenuate; - let avg = 0x0; - - this.sum += delta; - this.samples[index] = x; - - if(++this.index > (this.length - 1)){ - this.index = 0x0; - } - - avg = this.sum / this.length; - - return (x - avg) * (1/attenuate); - } -} - -class FirFilter { - buffer : number[] = []; - index : number = 0x0; - offset : number = 0x0; - length : number = 0x0; - m : number = 0x0; - h : number[] = []; - - constructor(h : number[], m : number){ - - this.length = h.length * m; - this.index = 0; - this.m = m; - this.h = h; - - let buffer = this.buffer; - for(let i = 0; i < this.length * 2; i++){ - buffer[i] = 0x0; - } - } - - step(samples : number []){ - - let index = this.index; - let buffer = this.buffer; - let length = this.length; - let m = this.m; - let h = this.h; - let y = 0x0; - let i = 0x0; - - this.offset = length - (index * m); - let sub = buffer.slice(this.offset); - this.index = (index + 1) % (length / m - 1); - for( i = m -1; i >=0; i--){ - buffer[this.offset + i] = samples[i]; - } - for( i = 0; i < h.length; i++){ - y += h[i] * sub[i]; - } - for( i = 0; i < m; i++){ - buffer[this.offset + length - m + i] = buffer[this.offset + i]; - } - - - return y; - - } - -} - -class AudioDriver { - - host : PSG49; - device : AudioContext; - context: ScriptProcessorNode; - frequency : number = 0x0; - filter : (BiasFilter | any)[]; - bias : number; - - constructor(host : PSG49){ - - this.device = new AudioContext(); - let device = this.device; - - this.filter = [ - - new BiasFilter(1024, 1.25), - new BiasFilter(1024, 1.25), - - device.createBiquadFilter(), - device.createBiquadFilter() - ]; - - let filter = this.filter; - - filter[2].type = "lowshelf"; - filter[2].frequency.value = 1500; - filter[2].gain.value = 3.35; - - filter[3].type = "lowpass"; - filter[3].frequency.value = 10000; - filter[3].Q.value = 1; - - this.frequency = device.sampleRate; - this.context = device.createScriptProcessor(4096,0,2); - this.context.onaudioprocess = this.update; - this.context.connect(filter[2]); - - filter[2].connect(filter[3]); - filter[3].connect(device.destination); - - this.host = host; - this.bias = 1/100; - - } - - update = function(ev : AudioProcessingEvent){ - - let ch0 = ev.outputBuffer.getChannelData(0); - let ch1 = ev.outputBuffer.getChannelData(1); - - let host = this.host; - let filter = this.filter; - let bias = this.bias; - let output = [0, 0]; - let port = [0, 0]; - - for(let i = 0; i < ch0.length; i++){ - - output = host.step(); - - port[0] = filter[0].step(output[0]); - port[1] = filter[1].step(output[1]); - - ch0[i] = bias + port[0]; - ch1[i] = bias + port[1]; - } - - }.bind(this); -} - -enum PSG49_LUT{ - - A_FINE, A_COARSE, - B_FINE, B_COARSE, - C_FINE, C_COARSE, - NOISE_PERIOD, - MIXER, - A_VOL, - B_VOL, - C_VOL, - ENV_FINE, - ENV_COARSE, - ENV_SHAPE - -} -class PSG49 { - - clock : Oscillator; - driver : AudioDriver; - interrupt : Interrupt; - channels: Channel[]; - envelope : Envelope; - fir : FirFilter[]; - oversample : number; - interpolate : Interpolator[]; - dac : number[]; - - // main register file - register = { - - A_FINE: 0x0, A_COARSE: 0x0, - B_FINE: 0x0, B_COARSE: 0x0, - C_FINE: 0x0, C_COARSE: 0x0, - - NOISE_PERIOD: 0x0, - - MIXER: 0x0, - - A_VOL: 0x0, - B_VOL: 0x0, - C_VOL: 0x0, - - ENV_FINE: 0x0, ENV_COARSE: 0x0, - ENV_SHAPE: 0x0, - - PORT_A: 0x0, - PORT_B: 0x0 - } - - constructor(clockRate : number, intRate : number){ - - this.driver = new AudioDriver(this); - this.interpolate = [ - new Interpolator(), - new Interpolator() - ]; - - let m = 8; - this.fir = [ - new FirFilter(FIR, m), - new FirFilter(FIR, m) - ]; - this.oversample = m; - - this.clock = { - frequency : clockRate, - scale : 1/16 * 2, - cycle : 0, - step : 0 - }; - - this.interrupt = { - frequency : intRate, - cycle : 0, - routine : ()=>{} - } - - this.envelope = { - strobe : 0, - transient : 0, - step : 0, - shape : 0, - offset : 0, - stub : [] - - } as Envelope; - - this.channels = [ - { - counter : 0x0, - pan : 0.5, - } as Channel, - { - counter : 0x0, - pan : 0.5 - } as Channel, - { - counter : 0x0, - pan : 0.5 - } as Channel, - - {counter : 0x0} as Channel - ] - - // seed noise generator - this.channels[3].port = 0x1; - - this.dac = []; - - this.build_dac(1.3, 40); - this.build_adsr(); - - } - - build_dac(decay : number, shift : number){ - let dac = this.dac; - let y = Math.sqrt(decay); - let z = shift/31; - - dac[0] = 0; - dac[1] = 0; - - for(let i = 2; i <= 31; i++){ - dac[i] = 1.0 / Math.pow(y, shift - (z*i) ); - } - } - - init_test(){ - let r = this.register; - - r.MIXER = 0b00111000; - r.A_VOL = 15; - //r.A_VOL |= 0x10; - r.A_FINE = 200; - //r.ENV_COARSE = 200; - } - - - build_adsr(){ - let envelope = this.envelope; - let stub = envelope.stub; - - stub.reset = (ev : Envelope)=>{ - let strobe = ev.strobe; - let transient = ev.transient; - - switch(ev.offset){ - - case 0x4: - transient = 0; - case 0x0: - ev.step = strobe ? transient : 31; - break; - case 0x5: - transient = 31; - case 0x1: - ev.step = strobe ? transient : 0; - break; - case 0x2: ev.step = 31; - break; - case 0x3: ev.step = 0; - break; - } - } - - stub.grow = (ev: Envelope)=>{ - - if(++ ev.step > 31 ){ - ev.strobe ^= 1; - ev.stub.reset(ev); - } - - }; - - stub.decay = (ev : Envelope)=>{ - if(-- ev.step < 0){ - ev.strobe ^= 1; - ev.stub.reset(ev); - } - - }; - - stub.hold = (ev : Envelope)=>{ } - - envelope.matrix = [ - - [stub.decay, stub.hold], - [stub.grow, stub.hold], - [stub.decay, stub.decay], - [stub.grow, stub.grow], - [stub.decay, stub.grow], - [stub.grow, stub.decay], - - ]; - } - - clamp(){ - let r = this.register; - - r.A_FINE &= 0xff; r.B_FINE &= 0xff; - r.C_FINE &= 0xff; r.ENV_FINE &= 0xff; - - r.A_COARSE &= 0xf; r.B_COARSE &=0xf; - r.C_COARSE &= 0xf; r.ENV_COARSE &= 0xff; - - r.A_VOL &= 0x1f; r.B_VOL &= 0x1f; - r.C_VOL &= 0x1f; - - r.NOISE_PERIOD &= 0x1f; r.MIXER &= 0x3f; - r.ENV_SHAPE &= 0xff; - - } - - map(){ - - let r = this.register; - let channel = this.channels; - let ev = this.envelope; - - let toneMask = [0x1,0x2,0x4]; - let noiseMask = [0x8,0x10,0x20]; - - this.clamp(); - // update tone channel period - channel[0].period = r.A_FINE | r.A_COARSE << 8; - channel[1].period = r.B_FINE | r.B_COARSE << 8; - channel[2].period = r.C_FINE | r.C_COARSE << 8; - - channel[0].volume = r.A_VOL & 0xf; - channel[1].volume = r.B_VOL & 0xf; - channel[2].volume = r.C_VOL & 0xf; - - for(let i = 0; i < 3; i++){ - let bit = r.MIXER & toneMask[i]; - channel[i].tone = bit ? 1 : 0; - } - - for(let i = 0; i < 3; i++){ - let bit = r.MIXER & noiseMask[i]; - channel[i].noise = bit ? 1 : 0; - } - - channel[0].envelope = (r.A_VOL & 0x10) ? 0 : 1; - channel[1].envelope = (r.B_VOL & 0x10) ? 0 : 1; - channel[2].envelope = (r.C_VOL & 0x10) ? 0 : 1; - - // update channel noise period - channel[3].period = r.NOISE_PERIOD << 1; - - ev.period = r.ENV_FINE | r.ENV_COARSE << 8; - ev.shape = r.ENV_SHAPE; - - - switch(ev.shape){ - - case 0x0: case 0x1: - case 0x2: case 0x3: - case 0x9: - ev.transient = 0; - ev.offset = 0; - r.ENV_SHAPE = 0xff; - break; - case 0xb: - ev.transient = 31; - ev.offset = 0; - r.ENV_SHAPE = 0xff; - break; - case 0x4: case 0x5: - case 0x6: case 0x7: - case 0xf: - ev.transient = 0; - ev.offset = 1; - r.ENV_SHAPE = 0xff; - case 0xd: - ev.transient = 31; - ev.offset = 1; - r.ENV_SHAPE = 0xff; - break; - case 0x8: - ev.offset = 2; - break; - case 0xc: - ev.offset = 3; - break; - case 0xa: - ev.offset = 4; - break; - case 0xe: - ev.offset = 5; - break; - - } - if(ev.shape != ev.store){ - ev.strobe = 0x0; - ev.counter = 0x0; - ev.stub.reset(ev); - - } - ev.store = r.ENV_SHAPE; - } - - step_tone(index : number){ - - let ch = this.channels[index % 3]; - let step = this.clock.step; - let port = ch.port; - - let period = (ch.period == 0x0) ? 0x1 : ch.period; - ch.counter += step; - - if(ch.counter >= period){ - // 50% duty cycle - port ^= 0x1; - ch.port = port; - ch.counter = 0x0; - } - - return ch.port; - - } - - step_envelope(){ - - let step = this.clock.step; - let ev = this.envelope; - - ev.counter += step; - - if(ev.counter >= ev.period){ - ev.matrix[ev.offset][ev.strobe](ev); - ev.counter = 0x0; - } - - return (ev.step); - } - - step_noise(){ - - let ch = this.channels[3]; - let step = this.clock.step; - let port = ch.port; - let period = (ch.period == 0) ? 1 : ch.period; - - ch.counter += step; - - if(ch.counter >= period){ - port ^= (((port & 1) ^ ((port >> 3) & 1)) << 17); - port >>= 1; - ch.port = port; - ch.counter = 0x0; - } - return ch.port & 1; - } - - step_mixer(){ - - let port = 0x0; - let output = [0.0, 0.0]; - let index = 0x0; - let ch = this.channels; - let noise = this.step_noise(); - let step = this.step_envelope(); - - for(let i = 0; i < 3; i++){ - - let volume = ch[i].volume; - let pan = ch[i].pan; - - port = this.step_tone(i) | ch[i].tone; - port &= noise | ch[i].noise; - - // todo: add dac volume table - //bit*=toneChannel[i].volume; - // mix each channel - - if(!ch[i].envelope){ - index = step; - }else{ - - index = volume * 2 + 1; - } - - port *= this.dac[index]; - - // clamp pan levels - // distortion over +1 ? - - if(pan > 0.9){ - pan = 0.9; - } - else if (pan < 0.1){ - pan = 0.1; - } - - output[0] += port * (1- pan) ; - output[1] += port * (pan) ; - - } - - return output; - } - - step(){ - - let output = []; - let clockStep = 0; - let intStep = 0; - let i = 0x0; - - let clock = this.clock; - let driver = this.driver; - let fir = this.fir; - let oversample = this.oversample; - let interpolate = this.interpolate; - let interrupt = this.interrupt; - - let x = clock.scale; - let fc = clock.frequency; - let fd = driver.frequency; - let fi = interrupt.frequency; - - clockStep = (fc * x) / fd; - clock.step = clockStep / oversample; - - intStep = fi/ fd; - - // add number of clock cycle - - interrupt.cycle += intStep; - // do we have clock cycles to process? - // if so process single clock cycle - - let sample_left = []; - let sample_right = []; - - for(i = 0; i < oversample; i++){ - sample_left[i] = 0x0; - sample_right[i] = 0x0; - } - - if(interrupt.cycle > 1){ - interrupt.cycle--; - interrupt.routine(); - interrupt.cycle = 0; - } - for(let i = 0; i < oversample; i++){ - clock.cycle += clockStep; - - if(clock.cycle > 1){ - clock.cycle--; - - this.map(); - output = this.step_mixer(); - - interpolate[0].step(output[0]); - interpolate[1].step(output[1]); - - } - sample_left[i] = interpolate[0].cubic(0.5); - sample_right[i] = interpolate[1].cubic(0.5); - - - } - output[0] = fir[0].step(sample_left); - output[1] = fir[1].step(sample_right); - - return output; - } - -} diff --git a/fym.js b/fym.js new file mode 100644 index 0000000..8ee4cc3 --- /dev/null +++ b/fym.js @@ -0,0 +1,66 @@ +FYMReader = function(buffer) { + var psgDump = pako.inflate(new Uint8Array(buffer)); + var ptr = 0; + var frame = 0; + + function getInt() { + var r = 0; + for(var i = 0; i < 4; i++) r += psgDump[ptr++] << (8*i); + return r; + } + + function getStr() { + var c, r = ''; + while(c = psgDump[ptr++]) r += String.fromCharCode(c); + return r; + } + + var offset = getInt(); + + var frameCount = getInt(); + this.getFrameCount = function() { + return frameCount; + } + + var loopFrame = getInt(); + this.getLoopFrame = function() { + return loopFrame; + } + + var clockRate = getInt(); + this.getClockRate = function() { + return clockRate; + } + + var frameRate = getInt(); + this.getFrameRate = function() { + return frameRate; + } + + var trackName = getStr(); + this.getTrackName = function() { + return trackName; + } + + var authorName = getStr(); + this.getAuthorName = function() { + return authorName; + } + + var loopCount = 0; + this.getLoopCount = function() { + return loopCount; + } + + this.getNextFrame = function() { + var regs = []; + for(var r = 0; r < 14; r++) { + regs[r] = psgDump[r * frameCount + frame + offset]; + } + if(++frame >= frameCount) { + loopCount++; + frame = loopFrame; + } + return regs; + } +} diff --git a/index.html b/index.html new file mode 100644 index 0000000..3b469da --- /dev/null +++ b/index.html @@ -0,0 +1,7 @@ + +Click anywhere to play. + + + + + \ No newline at end of file diff --git a/music/01_scalesmannmisfire.fym b/music/01_scalesmannmisfire.fym new file mode 100644 index 0000000..497e8a1 Binary files /dev/null and b/music/01_scalesmannmisfire.fym differ diff --git a/pako_inflate.min.js b/pako_inflate.min.js new file mode 100644 index 0000000..0bc5de0 --- /dev/null +++ b/pako_inflate.min.js @@ -0,0 +1 @@ +!function(e){if("object"==typeof exports&&"undefined"!=typeof module)module.exports=e();else if("function"==typeof define&&define.amd)define([],e);else{("undefined"!=typeof window?window:"undefined"!=typeof global?global:"undefined"!=typeof self?self:this).pako=e()}}(function(){return function r(o,s,f){function l(t,e){if(!s[t]){if(!o[t]){var i="function"==typeof require&&require;if(!e&&i)return i(t,!0);if(d)return d(t,!0);var n=new Error("Cannot find module '"+t+"'");throw n.code="MODULE_NOT_FOUND",n}var a=s[t]={exports:{}};o[t][0].call(a.exports,function(e){return l(o[t][1][e]||e)},a,a.exports,r,o,s,f)}return s[t].exports}for(var d="function"==typeof require&&require,e=0;e>>6:(i<65536?t[r++]=224|i>>>12:(t[r++]=240|i>>>18,t[r++]=128|i>>>12&63),t[r++]=128|i>>>6&63),t[r++]=128|63&i);return t},i.buf2binstring=function(e){return d(e,e.length)},i.binstring2buf=function(e){for(var t=new f.Buf8(e.length),i=0,n=t.length;i>10&1023,s[n++]=56320|1023&a)}return d(s,n)},i.utf8border=function(e,t){var i;for((t=t||e.length)>e.length&&(t=e.length),i=t-1;0<=i&&128==(192&e[i]);)i--;return i<0?t:0===i?t:i+l[e[i]]>t?i:t}},{"./common":1}],3:[function(e,t,i){"use strict";t.exports=function(e,t,i,n){for(var a=65535&e|0,r=e>>>16&65535|0,o=0;0!==i;){for(i-=o=2e3>>1:e>>>1;t[i]=e}return t}();t.exports=function(e,t,i,n){var a=s,r=n+i;e^=-1;for(var o=n;o>>8^a[255&(e^t[o])];return-1^e}},{}],6:[function(e,t,i){"use strict";t.exports=function(){this.text=0,this.time=0,this.xflags=0,this.os=0,this.extra=null,this.extra_len=0,this.name="",this.comment="",this.hcrc=0,this.done=!1}},{}],7:[function(e,t,i){"use strict";t.exports=function(e,t){var i,n,a,r,o,s,f,l,d,c,u,h,b,m,w,k,_,g,v,p,x,y,S,E,Z;i=e.state,n=e.next_in,E=e.input,a=n+(e.avail_in-5),r=e.next_out,Z=e.output,o=r-(t-e.avail_out),s=r+(e.avail_out-257),f=i.dmax,l=i.wsize,d=i.whave,c=i.wnext,u=i.window,h=i.hold,b=i.bits,m=i.lencode,w=i.distcode,k=(1<>>=v=g>>>24,b-=v,0===(v=g>>>16&255))Z[r++]=65535&g;else{if(!(16&v)){if(0==(64&v)){g=m[(65535&g)+(h&(1<>>=v,b-=v),b<15&&(h+=E[n++]<>>=v=g>>>24,b-=v,!(16&(v=g>>>16&255))){if(0==(64&v)){g=w[(65535&g)+(h&(1<>>=v,b-=v,(v=r-o)>3,h&=(1<<(b-=p<<3))-1,e.next_in=n,e.next_out=r,e.avail_in=n>>24&255)+(e>>>8&65280)+((65280&e)<<8)+((255&e)<<24)}function r(){this.mode=0,this.last=!1,this.wrap=0,this.havedict=!1,this.flags=0,this.dmax=0,this.check=0,this.total=0,this.head=null,this.wbits=0,this.wsize=0,this.whave=0,this.wnext=0,this.window=null,this.hold=0,this.bits=0,this.length=0,this.offset=0,this.extra=0,this.lencode=null,this.distcode=null,this.lenbits=0,this.distbits=0,this.ncode=0,this.nlen=0,this.ndist=0,this.have=0,this.next=null,this.lens=new z.Buf16(320),this.work=new z.Buf16(288),this.lendyn=null,this.distdyn=null,this.sane=0,this.back=0,this.was=0}function o(e){var t;return e&&e.state?(t=e.state,e.total_in=e.total_out=t.total=0,e.msg="",t.wrap&&(e.adler=1&t.wrap),t.mode=F,t.last=0,t.havedict=0,t.dmax=32768,t.head=null,t.hold=0,t.bits=0,t.lencode=t.lendyn=new z.Buf32(n),t.distcode=t.distdyn=new z.Buf32(a),t.sane=1,t.back=-1,T):U}function s(e){var t;return e&&e.state?((t=e.state).wsize=0,t.whave=0,t.wnext=0,o(e)):U}function f(e,t){var i,n;return e&&e.state?(n=e.state,t<0?(i=0,t=-t):(i=1+(t>>4),t<48&&(t&=15)),t&&(t<8||15=r.wsize?(z.arraySet(r.window,t,i-r.wsize,r.wsize,0),r.wnext=0,r.whave=r.wsize):(n<(a=r.wsize-r.wnext)&&(a=n),z.arraySet(r.window,t,i-n,a,r.wnext),(n-=a)?(z.arraySet(r.window,t,i-n,n,0),r.wnext=n,r.whave=r.wsize):(r.wnext+=a,r.wnext===r.wsize&&(r.wnext=0),r.whave>>8&255,i.check=N(i.check,B,2,0),d=l=0,i.mode=2;break}if(i.flags=0,i.head&&(i.head.done=!1),!(1&i.wrap)||(((255&l)<<8)+(l>>8))%31){e.msg="incorrect header check",i.mode=30;break}if(8!=(15&l)){e.msg="unknown compression method",i.mode=30;break}if(d-=4,x=8+(15&(l>>>=4)),0===i.wbits)i.wbits=x;else if(x>i.wbits){e.msg="invalid window size",i.mode=30;break}i.dmax=1<>8&1),512&i.flags&&(B[0]=255&l,B[1]=l>>>8&255,i.check=N(i.check,B,2,0)),d=l=0,i.mode=3;case 3:for(;d<32;){if(0===s)break e;s--,l+=n[r++]<>>8&255,B[2]=l>>>16&255,B[3]=l>>>24&255,i.check=N(i.check,B,4,0)),d=l=0,i.mode=4;case 4:for(;d<16;){if(0===s)break e;s--,l+=n[r++]<>8),512&i.flags&&(B[0]=255&l,B[1]=l>>>8&255,i.check=N(i.check,B,2,0)),d=l=0,i.mode=5;case 5:if(1024&i.flags){for(;d<16;){if(0===s)break e;s--,l+=n[r++]<>>8&255,i.check=N(i.check,B,2,0)),d=l=0}else i.head&&(i.head.extra=null);i.mode=6;case 6:if(1024&i.flags&&(s<(h=i.length)&&(h=s),h&&(i.head&&(x=i.head.extra_len-i.length,i.head.extra||(i.head.extra=new Array(i.head.extra_len)),z.arraySet(i.head.extra,n,r,h,x)),512&i.flags&&(i.check=N(i.check,n,h,r)),s-=h,r+=h,i.length-=h),i.length))break e;i.length=0,i.mode=7;case 7:if(2048&i.flags){if(0===s)break e;for(h=0;x=n[r+h++],i.head&&x&&i.length<65536&&(i.head.name+=String.fromCharCode(x)),x&&h>9&1,i.head.done=!0),e.adler=i.check=0,i.mode=12;break;case 10:for(;d<32;){if(0===s)break e;s--,l+=n[r++]<>>=7&d,d-=7&d,i.mode=27;break}for(;d<3;){if(0===s)break e;s--,l+=n[r++]<>>=1)){case 0:i.mode=14;break;case 1:if(H(i),i.mode=20,6!==t)break;l>>>=2,d-=2;break e;case 2:i.mode=17;break;case 3:e.msg="invalid block type",i.mode=30}l>>>=2,d-=2;break;case 14:for(l>>>=7&d,d-=7&d;d<32;){if(0===s)break e;s--,l+=n[r++]<>>16^65535)){e.msg="invalid stored block lengths",i.mode=30;break}if(i.length=65535&l,d=l=0,i.mode=15,6===t)break e;case 15:i.mode=16;case 16:if(h=i.length){if(s>>=5,d-=5,i.ndist=1+(31&l),l>>>=5,d-=5,i.ncode=4+(15&l),l>>>=4,d-=4,286>>=3,d-=3}for(;i.have<19;)i.lens[A[i.have++]]=0;if(i.lencode=i.lendyn,i.lenbits=7,S={bits:i.lenbits},y=C(0,i.lens,0,19,i.lencode,0,i.work,S),i.lenbits=S.bits,y){e.msg="invalid code lengths set",i.mode=30;break}i.have=0,i.mode=19;case 19:for(;i.have>>16&255,_=65535&Z,!((w=Z>>>24)<=d);){if(0===s)break e;s--,l+=n[r++]<>>=w,d-=w,i.lens[i.have++]=_;else{if(16===_){for(E=w+2;d>>=w,d-=w,0===i.have){e.msg="invalid bit length repeat",i.mode=30;break}x=i.lens[i.have-1],h=3+(3&l),l>>>=2,d-=2}else if(17===_){for(E=w+3;d>>=w)),l>>>=3,d-=3}else{for(E=w+7;d>>=w)),l>>>=7,d-=7}if(i.have+h>i.nlen+i.ndist){e.msg="invalid bit length repeat",i.mode=30;break}for(;h--;)i.lens[i.have++]=x}}if(30===i.mode)break;if(0===i.lens[256]){e.msg="invalid code -- missing end-of-block",i.mode=30;break}if(i.lenbits=9,S={bits:i.lenbits},y=C(I,i.lens,0,i.nlen,i.lencode,0,i.work,S),i.lenbits=S.bits,y){e.msg="invalid literal/lengths set",i.mode=30;break}if(i.distbits=6,i.distcode=i.distdyn,S={bits:i.distbits},y=C(D,i.lens,i.nlen,i.ndist,i.distcode,0,i.work,S),i.distbits=S.bits,y){e.msg="invalid distances set",i.mode=30;break}if(i.mode=20,6===t)break e;case 20:i.mode=21;case 21:if(6<=s&&258<=f){e.next_out=o,e.avail_out=f,e.next_in=r,e.avail_in=s,i.hold=l,i.bits=d,O(e,u),o=e.next_out,a=e.output,f=e.avail_out,r=e.next_in,n=e.input,s=e.avail_in,l=i.hold,d=i.bits,12===i.mode&&(i.back=-1);break}for(i.back=0;k=(Z=i.lencode[l&(1<>>16&255,_=65535&Z,!((w=Z>>>24)<=d);){if(0===s)break e;s--,l+=n[r++]<>g)])>>>16&255,_=65535&Z,!(g+(w=Z>>>24)<=d);){if(0===s)break e;s--,l+=n[r++]<>>=g,d-=g,i.back+=g}if(l>>>=w,d-=w,i.back+=w,i.length=_,0===k){i.mode=26;break}if(32&k){i.back=-1,i.mode=12;break}if(64&k){e.msg="invalid literal/length code",i.mode=30;break}i.extra=15&k,i.mode=22;case 22:if(i.extra){for(E=i.extra;d>>=i.extra,d-=i.extra,i.back+=i.extra}i.was=i.length,i.mode=23;case 23:for(;k=(Z=i.distcode[l&(1<>>16&255,_=65535&Z,!((w=Z>>>24)<=d);){if(0===s)break e;s--,l+=n[r++]<>g)])>>>16&255,_=65535&Z,!(g+(w=Z>>>24)<=d);){if(0===s)break e;s--,l+=n[r++]<>>=g,d-=g,i.back+=g}if(l>>>=w,d-=w,i.back+=w,64&k){e.msg="invalid distance code",i.mode=30;break}i.offset=_,i.extra=15&k,i.mode=24;case 24:if(i.extra){for(E=i.extra;d>>=i.extra,d-=i.extra,i.back+=i.extra}if(i.offset>i.dmax){e.msg="invalid distance too far back",i.mode=30;break}i.mode=25;case 25:if(0===f)break e;if(h=u-f,i.offset>h){if((h=i.offset-h)>i.whave&&i.sane){e.msg="invalid distance too far back",i.mode=30;break}h>i.wnext?(h-=i.wnext,b=i.wsize-h):b=i.wnext-h,h>i.length&&(h=i.length),m=i.window}else m=a,b=o-i.offset,h=i.length;for(fh?(m=O[C+o[g]],w=A[z+o[g]]):(m=96,w=0),f=1<<_-S,v=l=1<>S)+(l-=f)]=b<<24|m<<16|w|0,0!==l;);for(f=1<<_-1;B&f;)f>>=1;if(0!==f?(B&=f-1,B+=f):B=0,g++,0==--R[_]){if(_===p)break;_=t[i+o[g]]}if(x<_&&(B&c)!==d){for(0===S&&(S=x),u+=v,E=1<<(y=_-S);y+S> 1) & 1) << 1; + var mix_tone_C = ((r[7] >> 2) & 1) << 2; + + var mix_noise_A = ((r[7] >> 3) & 1) << 3; + var mix_noise_B = ((r[7] >> 4) & 1) << 4; + var mix_noise_C = ((r[7] >> 5) & 1) << 5; + + var MIXER = (mix_tone_A | mix_tone_B | mix_tone_C + | mix_noise_A | mix_noise_B | mix_noise_C); + + emu8910.register.MIXER = MIXER; + + emu8910.register.A_VOL = r[8]; + emu8910.register.B_VOL = r[9]; + emu8910.register.C_VOL = r[10]; + + emu8910.register.ENV_FINE = r[11]; + emu8910.register.ENV_COURSE = r[12]; + + if (r[13] != 0xff) { + emu8910.register.ENV_SHAPE = r[13]; + } + } + + play = function(fym) { + song = new FYMReader(fym); + emu8910.interrupt.routine = updateState; + emu8910.clock.frequency = song.getClockRate() + emu8910.interrupt.frequency = song.getFrameRate() + } + + document.addEventListener('click', loadAndPlay, false); + document.addEventListener('touchend', loadAndPlay, false); diff --git a/src/emu8910.ts b/src/emu8910.ts new file mode 100644 index 0000000..180399b --- /dev/null +++ b/src/emu8910.ts @@ -0,0 +1,774 @@ +// Copyright (C) +// AY8910 emulator. +// Author: Dylan Muller. + +const YM_CLOCK_ZX = 1750000; +const FIR = [-0.011368, + 0.004512, + 0.008657, + -0.011763, + -0.000000, + 0.012786, + -0.010231, + -0.005801, + 0.015915, + -0.006411, + -0.012504, + 0.017299, + -0.000000, + -0.019605, + 0.016077, + 0.009370, + -0.026526, + 0.011074, + 0.022508, + -0.032676, + 0.000000, + 0.042011, + -0.037513, + -0.024362, + 0.079577, + -0.040604, + -0.112540, + 0.294080, + 0.625000, + 0.294080, + -0.112540, + -0.040604, + 0.079577, + -0.024362, + -0.037513, + 0.042011, + 0.000000, + -0.032676, + 0.022508, + 0.011074, + -0.026526, + 0.009370, + 0.016077, + -0.019605, + -0.000000, + 0.017299, + -0.012504, + -0.006411, + 0.015915, + -0.005801, + -0.010231, + 0.012786, + -0.000000, + -0.011763, + 0.008657, + 0.004512, + -0.011368] + +interface Channel{ + + port : number, + counter : number, + period : number, + volume : number, + pan : number, + tone : number, + noise : number, + envelope : number + +} + +interface Envelope{ + + counter : number, + period : number, + shape : number, + stub : any, + matrix : any, + strobe : number, + offset : number, + transient : number, + store : number, + step : number + +} + +interface Oscillator{ + + frequency: number, + scale : number, + cycle : number, + step : number + +} + +interface Interrupt{ + frequency : number, + routine : any, + cycle : number, +} + +class Interpolator{ + buffer : number[] = []; + + constructor(){ + for(let i = 0; i < 4; i++){ + this.buffer[i] = 0x0; + } + } + + step(x : number){ + let b = this.buffer; + b[0] = b[1]; + b[1] = b[2]; + b[2] = b[3]; + + b[3] = x; + } + + cubic(mu : number){ + + let b = this.buffer; + let a0,a1,a2,a3,mu2 = 0; + mu2 = mu * mu2; + a0 = b[3] - b[2] - b[0] + b[1]; + a1 = b[0] - b[1] - a0; + a2 = b[2] - b[0]; + a3 = b[1]; + + return (a0*mu*mu2 + a1*mu2 + a2*mu + a3); + } + +} + +// DC filter +class BiasFilter { + + samples : number[] =[]; + index : number = 0x0; + length : number = 0x0; + sum: number = 0x0; + attenuate : number = 0x0; + + constructor(length : number, attenuate : number){ + + this.length = length; + this.sum = 0x0; + + for(let i = 0; i < this.length; i++){ + this.samples[i] = 0x0; + } + this.attenuate = attenuate; + } + + step(x : number){ + let index = this.index; + let delta = x - this.samples[index]; + let attenuate = this.attenuate; + let avg = 0x0; + + this.sum += delta; + this.samples[index] = x; + + if(++this.index > (this.length - 1)){ + this.index = 0x0; + } + + avg = this.sum / this.length; + + return (x - avg) * (1/attenuate); + } +} + +class FirFilter { + buffer : number[] = []; + index : number = 0x0; + offset : number = 0x0; + length : number = 0x0; + m : number = 0x0; + h : number[] = []; + + constructor(h : number[], m : number){ + + this.length = h.length * m; + this.index = 0; + this.m = m; + this.h = h; + + let buffer = this.buffer; + for(let i = 0; i < this.length * 2; i++){ + buffer[i] = 0x0; + } + } + + step(samples : number []){ + + let index = this.index; + let buffer = this.buffer; + let length = this.length; + let m = this.m; + let h = this.h; + let y = 0x0; + let i = 0x0; + + this.offset = length - (index * m); + let sub = buffer.slice(this.offset); + this.index = (index + 1) % (length / m - 1); + for( i = m -1; i >=0; i--){ + buffer[this.offset + i] = samples[i]; + } + for( i = 0; i < h.length; i++){ + y += h[i] * sub[i]; + } + for( i = 0; i < m; i++){ + buffer[this.offset + length - m + i] = buffer[this.offset + i]; + } + + + return y; + + } + +} + +class AudioDriver { + + host : PSG49; + device : AudioContext; + context: ScriptProcessorNode; + frequency : number = 0x0; + filter : (BiasFilter | any)[]; + bias : number; + + constructor(host : PSG49){ + + this.device = new AudioContext(); + let device = this.device; + + this.filter = [ + + new BiasFilter(1024, 1.25), + new BiasFilter(1024, 1.25), + + device.createBiquadFilter(), + device.createBiquadFilter() + ]; + + let filter = this.filter; + + filter[2].type = "lowshelf"; + filter[2].frequency.value = 10000; + filter[2].gain.value = 2; + + filter[3].type = "lowpass"; + filter[3].frequency.value = 10000; + filter[3].Q.value = 1; + + this.frequency = device.sampleRate; + this.context = device.createScriptProcessor(4096,0,2); + this.context.onaudioprocess = this.update; + this.context.connect(filter[2]); + + filter[2].connect(filter[3]); + filter[3].connect(device.destination); + + this.host = host; + this.bias = 1/100; + + } + + update = function(ev : AudioProcessingEvent){ + + let ch0 = ev.outputBuffer.getChannelData(0); + let ch1 = ev.outputBuffer.getChannelData(1); + + let host = this.host; + let filter = this.filter; + let bias = this.bias; + let output = [0, 0]; + let port = [0, 0]; + + for(let i = 0; i < ch0.length; i++){ + + output = host.step(); + + port[0] = filter[0].step(output[0]); + port[1] = filter[1].step(output[1]); + + ch0[i] = bias + port[0]; + ch1[i] = bias + port[1]; + } + + }.bind(this); +} + +enum PSG49_LUT{ + + A_FINE, A_COARSE, + B_FINE, B_COARSE, + C_FINE, C_COARSE, + NOISE_PERIOD, + MIXER, + A_VOL, + B_VOL, + C_VOL, + ENV_FINE, + ENV_COARSE, + ENV_SHAPE + +} +class PSG49 { + + clock : Oscillator; + driver : AudioDriver; + interrupt : Interrupt; + channels: Channel[]; + envelope : Envelope; + fir : FirFilter[]; + oversample : number; + interpolate : Interpolator[]; + dac : number[]; + + // main register file + register = { + + A_FINE: 0x0, A_COARSE: 0x0, + B_FINE: 0x0, B_COARSE: 0x0, + C_FINE: 0x0, C_COARSE: 0x0, + + NOISE_PERIOD: 0x0, + + // bit position + // 5 4 3 2 1 0 + // NC NB NA TC TB TA + // T = Tone, N = Noise + MIXER: 0x0, + + A_VOL: 0x0, + B_VOL: 0x0, + C_VOL: 0x0, + + ENV_FINE: 0x0, ENV_COARSE: 0x0, + ENV_SHAPE: 0x0, + + PORT_A: 0x0, + PORT_B: 0x0 + } + + constructor(clockRate : number, intRate : number){ + + this.driver = new AudioDriver(this); + this.interpolate = [ + new Interpolator(), + new Interpolator() + ]; + + let m = 8; + this.fir = [ + new FirFilter(FIR, m), + new FirFilter(FIR, m) + ]; + this.oversample = m; + + this.clock = { + frequency : clockRate, + scale : 1/16 * 2, + cycle : 0, + step : 0 + }; + + this.interrupt = { + frequency : intRate, + cycle : 0, + routine : ()=>{} + } + + this.envelope = { + strobe : 0, + transient : 0, + step : 0, + shape : 0, + offset : 0, + stub : [] + + } as Envelope; + + this.channels = [ + { + counter : 0x0, + pan : 0.5, + } as Channel, + { + counter : 0x0, + pan : 0.5 + } as Channel, + { + counter : 0x0, + pan : 0.5 + } as Channel, + + {counter : 0x0} as Channel + ] + + // seed noise generator + this.channels[3].port = 0x1; + + this.dac = []; + + this.build_dac(1.3, 40); + this.build_adsr(); + + } + + build_dac(decay : number, shift : number){ + let dac = this.dac; + let y = Math.sqrt(decay); + let z = shift/31; + + dac[0] = 0; + dac[1] = 0; + + for(let i = 2; i <= 31; i++){ + dac[i] = 1.0 / Math.pow(y, shift - (z*i) ); + } + } + + init_test(){ + let r = this.register; + + r.MIXER = 0b00111000; + r.A_VOL = 15; + //r.A_VOL |= 0x10; + r.A_FINE = 200; + //r.ENV_COARSE = 200; + } + + + build_adsr(){ + let envelope = this.envelope; + let stub = envelope.stub; + + stub.reset = (ev : Envelope)=>{ + let strobe = ev.strobe; + let transient = ev.transient; + + switch(ev.offset){ + + case 0x4: + transient = 0; + case 0x0: + ev.step = strobe ? transient : 31; + break; + case 0x5: + transient = 31; + case 0x1: + ev.step = strobe ? transient : 0; + break; + case 0x2: ev.step = 31; + break; + case 0x3: ev.step = 0; + break; + } + } + + stub.grow = (ev: Envelope)=>{ + + if(++ ev.step > 31 ){ + ev.strobe ^= 1; + ev.stub.reset(ev); + } + + }; + + stub.decay = (ev : Envelope)=>{ + if(-- ev.step < 0){ + ev.strobe ^= 1; + ev.stub.reset(ev); + } + + }; + + stub.hold = (ev : Envelope)=>{ } + + envelope.matrix = [ + + [stub.decay, stub.hold], + [stub.grow, stub.hold], + [stub.decay, stub.decay], + [stub.grow, stub.grow], + [stub.decay, stub.grow], + [stub.grow, stub.decay], + + ]; + } + + clamp(){ + let r = this.register; + + r.A_FINE &= 0xff; r.B_FINE &= 0xff; + r.C_FINE &= 0xff; r.ENV_FINE &= 0xff; + + r.A_COARSE &= 0xf; r.B_COARSE &=0xf; + r.C_COARSE &= 0xf; r.ENV_COARSE &= 0xff; + + r.A_VOL &= 0x1f; r.B_VOL &= 0x1f; + r.C_VOL &= 0x1f; + + r.NOISE_PERIOD &= 0x1f; r.MIXER &= 0x3f; + r.ENV_SHAPE &= 0xff; + + } + + map(){ + + let r = this.register; + let channel = this.channels; + let ev = this.envelope; + + let toneMask = [0x1,0x2,0x4]; + let noiseMask = [0x8,0x10,0x20]; + + this.clamp(); + // update tone channel period + channel[0].period = r.A_FINE | r.A_COARSE << 8; + channel[1].period = r.B_FINE | r.B_COARSE << 8; + channel[2].period = r.C_FINE | r.C_COARSE << 8; + + channel[0].volume = r.A_VOL & 0xf; + channel[1].volume = r.B_VOL & 0xf; + channel[2].volume = r.C_VOL & 0xf; + + for(let i = 0; i < 3; i++){ + let bit = r.MIXER & toneMask[i]; + channel[i].tone = bit ? 1 : 0; + } + + for(let i = 0; i < 3; i++){ + let bit = r.MIXER & noiseMask[i]; + channel[i].noise = bit ? 1 : 0; + } + + channel[0].envelope = (r.A_VOL & 0x10) ? 0 : 1; + channel[1].envelope = (r.B_VOL & 0x10) ? 0 : 1; + channel[2].envelope = (r.C_VOL & 0x10) ? 0 : 1; + + // update channel noise period + channel[3].period = r.NOISE_PERIOD << 1; + + ev.period = r.ENV_FINE | r.ENV_COARSE << 8; + ev.shape = r.ENV_SHAPE; + + + switch(ev.shape){ + + case 0x0: case 0x1: + case 0x2: case 0x3: + case 0x9: + ev.transient = 0; + ev.offset = 0; + r.ENV_SHAPE = 0xff; + break; + case 0xb: + ev.transient = 31; + ev.offset = 0; + r.ENV_SHAPE = 0xff; + break; + case 0x4: case 0x5: + case 0x6: case 0x7: + case 0xf: + ev.transient = 0; + ev.offset = 1; + r.ENV_SHAPE = 0xff; + case 0xd: + ev.transient = 31; + ev.offset = 1; + r.ENV_SHAPE = 0xff; + break; + case 0x8: + ev.offset = 2; + break; + case 0xc: + ev.offset = 3; + break; + case 0xa: + ev.offset = 4; + break; + case 0xe: + ev.offset = 5; + break; + + } + if(ev.shape != ev.store){ + ev.strobe = 0x0; + ev.counter = 0x0; + ev.stub.reset(ev); + + } + ev.store = r.ENV_SHAPE; + } + + step_tone(index : number){ + + let ch = this.channels[index % 3]; + let step = this.clock.step; + let port = ch.port; + + let period = (ch.period == 0x0) ? 0x1 : ch.period; + ch.counter += step; + + if(ch.counter >= period){ + // 50% duty cycle + port ^= 0x1; + ch.port = port; + ch.counter = 0x0; + } + + return ch.port; + + } + + step_envelope(){ + + let step = this.clock.step; + let ev = this.envelope; + + ev.counter += step; + + if(ev.counter >= ev.period){ + ev.matrix[ev.offset][ev.strobe](ev); + ev.counter = 0x0; + } + + return (ev.step); + } + + step_noise(){ + + let ch = this.channels[3]; + let step = this.clock.step; + let port = ch.port; + let period = (ch.period == 0) ? 1 : ch.period; + + ch.counter += step; + + if(ch.counter >= period){ + port ^= (((port & 1) ^ ((port >> 3) & 1)) << 17); + port >>= 1; + ch.port = port; + ch.counter = 0x0; + } + return ch.port & 1; + } + + step_mixer(){ + + let port = 0x0; + let output = [0.0, 0.0]; + let index = 0x0; + let ch = this.channels; + let noise = this.step_noise(); + let step = this.step_envelope(); + + for(let i = 0; i < 3; i++){ + + let volume = ch[i].volume; + let pan = ch[i].pan; + + port = this.step_tone(i) | ch[i].tone; + port &= noise | ch[i].noise; + + // todo: add dac volume table + //bit*=toneChannel[i].volume; + // mix each channel + + if(!ch[i].envelope){ + index = step; + }else{ + + index = volume * 2 + 1; + } + + port *= this.dac[index]; + + // clamp pan levels + // distortion over +1 ? + + if(pan > 0.9){ + pan = 0.9; + } + else if (pan < 0.1){ + pan = 0.1; + } + + output[0] += port * (1- pan) ; + output[1] += port * (pan) ; + + } + + return output; + } + + step(){ + + let output = []; + let clockStep = 0; + let intStep = 0; + let i = 0x0; + + let clock = this.clock; + let driver = this.driver; + let fir = this.fir; + let oversample = this.oversample; + let interpolate = this.interpolate; + let interrupt = this.interrupt; + + let x = clock.scale; + let fc = clock.frequency; + let fd = driver.frequency; + let fi = interrupt.frequency; + + clockStep = (fc * x) / fd; + clock.step = clockStep / oversample; + + intStep = fi/ fd; + + // add number of clock cycle + + interrupt.cycle += intStep; + // do we have clock cycles to process? + // if so process single clock cycle + + let sample_left = []; + let sample_right = []; + + for(i = 0; i < oversample; i++){ + sample_left[i] = 0x0; + sample_right[i] = 0x0; + } + + if(interrupt.cycle > 1){ + interrupt.cycle--; + interrupt.routine(); + interrupt.cycle = 0; + } + for(let i = 0; i < oversample; i++){ + clock.cycle += clockStep; + + if(clock.cycle > 1){ + clock.cycle--; + + this.map(); + output = this.step_mixer(); + + interpolate[0].step(output[0]); + interpolate[1].step(output[1]); + + } + sample_left[i] = interpolate[0].cubic(0.5); + sample_right[i] = interpolate[1].cubic(0.5); + + + } + output[0] = fir[0].step(sample_left); + output[1] = fir[1].step(sample_right); + + return output; + } + +} -- cgit v1.2.3-70-g09d2