diff options
Diffstat (limited to 'src')
| -rw-r--r-- | src/emu8910.ts | 774 | 
1 files changed, 774 insertions, 0 deletions
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; +    } + +}  | 
