summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--README.md13
-rw-r--r--emu8910.ts917
2 files changed, 930 insertions, 0 deletions
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..adee73c
--- /dev/null
+++ b/README.md
@@ -0,0 +1,13 @@
+### emu8910
+
+This repository contains a Typescript implementation of General Instrument's [AY-3-8910](https://en.wikipedia.org/wiki/General_Instrument_AY-3-8910) PSG (programmable sound generator).
+It implements most of the PSG's original registers. A datasheet can be found [here](http://map.grauw.nl/resources/sound/generalinstrument_ay-3-8910.pdf).
+
+Sound output is achieved in the browser through an AudioContext() hook. <br>
+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.
+
+Files:
+
+* emu8910.ts - Core emulator implementation
diff --git a/emu8910.ts b/emu8910.ts
new file mode 100644
index 0000000..a057c2d
--- /dev/null
+++ b/emu8910.ts
@@ -0,0 +1,917 @@
+// 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;
+ }
+
+}