├── .gitignore ├── Makefile ├── README.md ├── apu ├── apu.js ├── envelope.js ├── lengthcounter.js ├── noise.js ├── pulse.js ├── resampler.js └── triangle.js ├── audio ├── bufferedaudionode.js ├── kernel.js ├── stepbufferwriter.js └── stepgeneratornode.js ├── bankswitcher.js ├── clock.js ├── cpu.js ├── deps.pl ├── index.html ├── mem.js ├── nsf.js ├── nsfplayer.js ├── scraps ├── async.js ├── bitmask.js ├── cpu-scraps.js ├── divider.js ├── index.html ├── nes.js ├── test1.js ├── test2.js ├── test3.html ├── test3.js ├── test4.html └── test4.js ├── tabble.js ├── test4.html ├── test4.js └── util ├── math.js └── util.js /.gitignore: -------------------------------------------------------------------------------- 1 | deps.d 2 | *.compiled.* 3 | *.srcmap 4 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | .PHONY: all 2 | 3 | all: nes.compiled.js 4 | 5 | JSCOMP=java -jar ~/Downloads/compiler.jar 6 | 7 | deps.d: deps.pl Makefile 8 | ./deps.pl '!' -path './scraps/\*' 9 | 10 | -include deps.d 11 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # JsNsf 2 | 3 | JsNsf is a JavaScript NSF player. It emulates the 4 | 8-bit Nintendo's 6502 processor and APU sound chip 5 | and can play chiptunes written for this system. 6 | 7 | -------------------------------------------------------------------------------- /apu/apu.js: -------------------------------------------------------------------------------- 1 | import Clock from '../clock'; 2 | import Memory from '../mem'; 3 | import Noise from './noise'; 4 | import Pulse from './pulse'; 5 | import Triangle from './triangle'; 6 | 7 | // APU has a clock speed of ~900 kHz, and it potentially samples 8 | // a point at every time (though most of the time nothing changes). 9 | // 10 | // We output at 44.1 kHz, which is quite different and may involve 11 | // arbitrary fractional delays for the steps. We therefore need to 12 | // resample. Ideally we could precompute heaviside functions at 13 | // arbitrary fractional delays and then pick the closest...? 14 | // 15 | // cf. Lanczos sampling with sinc(x)=sin(pi*x)/(pi*x) functions. 16 | 17 | 18 | // NOTE: 32-sample triangle waves are muddy below about 1380 Hz. 19 | // (and likewise, 64-sample below 690 Hz, 128-sample below 345 Hz, 20 | // and 256-sample below 170 Hz). As such, we may consider adding 21 | // additional samples for longer periods, or else possibly even 22 | // using a fixed period and repurposing the period register as a 23 | // delta register (though we'd need to be careful about what happens 24 | // when it changes!) Then again, the muddier versions are actually 25 | // more accurate - so let's try both and compare. 26 | 27 | 28 | // NOTE: Frame counter should always be assumed to operate in 29 | // 4-step mode. We never need to worry about IRQ. 30 | 31 | 32 | // TODO - log changes to APU state 33 | // - but we should bundle them such that if several registers 34 | // change within a short period of time, then only a single 35 | // line is logged (provided the same one doesn't change 36 | // multiple times) 37 | // - consider allowing registers to be named and then having 38 | // the MMU do this? 39 | 40 | 41 | class Enabler { 42 | constructor(id) { 43 | const element = document.getElementById(id); 44 | this.enabled = true; 45 | if (element) { 46 | element.addEventListener('click', () => { 47 | this.enabled = element.checked; 48 | console.log(id + ' => ' + this.enabled); 49 | }); 50 | } 51 | } 52 | } 53 | 54 | export default class Apu { 55 | /** 56 | * @param {!Memory} mem 57 | * @param {!Clock} clock 58 | */ 59 | constructor(mem, clock) { 60 | this.mem_ = mem; 61 | this.clock_ = clock; 62 | this.pulse1_ = new Pulse(mem, 0x4000); 63 | this.pulse2_ = new Pulse(mem, 0x4004); 64 | this.triangle_ = new Triangle(mem); 65 | this.noise_ = new Noise(mem); 66 | this.steps_ = []; 67 | this.last_ = 0; 68 | this.wait_ = 2; 69 | 70 | this.frameCounter_ = 0; 71 | 72 | this.pulse1Enabled = new Enabler('pulse1_enabled'); 73 | this.pulse2Enabled = new Enabler('pulse2_enabled'); 74 | this.triangleEnabled = new Enabler('triangle_enabled'); 75 | this.noiseEnabled = new Enabler('noise_enabled'); 76 | 77 | 78 | // TODO - add a callback when volume changes, so we don't 79 | // need to keep recomputing the mixer every single time! 80 | 81 | // mem.register(0x4015, { 82 | // get: this.getStatus.bind(this), 83 | // set: this.setStatus.bind(this), 84 | // }); 85 | 86 | // this.status_ = 0; 87 | } 88 | 89 | // getStatus() { 90 | // // console.log('get status'); 91 | // return this.status_; 92 | // } 93 | 94 | // setStatus(value) { 95 | // // console.log('set status: ' + value); 96 | // this.status_ = value; 97 | // } 98 | 99 | clock() { 100 | if (++this.frameCounter_ == FRAME_LIMIT) this.frameCounter_ = 0; 101 | const quarter = FRAME_CYCLES[this.frameCounter_]; 102 | if (quarter != null) { 103 | // TODO - distinguish half from quarter frames. 104 | this.pulse1_.clockFrame(); 105 | this.pulse2_.clockFrame(); 106 | this.triangle_.clockFrame(); 107 | this.noise_.clockFrame(); 108 | } 109 | 110 | this.triangle_.clockSequencer(); // clocks every cycle 111 | if (!--this.wait_) { 112 | this.pulse1_.clockSequencer(); 113 | this.pulse2_.clockSequencer(); 114 | this.noise_.clockSequencer(); 115 | this.wait_ = 2; 116 | } 117 | 118 | const volume = this.volume(); 119 | if (volume != this.last_) { 120 | this.steps_.push([this.clock_.time, volume]); 121 | this.last_ = volume; 122 | } 123 | } 124 | 125 | // clockFrame() { 126 | // this.pulse1_.clockFrame(); 127 | // this.pulse2_.clockFrame(); 128 | // this.noise_.clockFrame(); 129 | // } 130 | 131 | steps() { 132 | const steps = this.steps_; 133 | const volume = this.volume(); 134 | steps.push([this.clock_.time, volume]); // always return at least one. 135 | this.steps_ = [[this.clock_.time, volume]]; 136 | return steps; 137 | } 138 | 139 | volume() { 140 | const pulse1 = this.pulse1Enabled.enabled ? this.pulse1_.volume() : 0; 141 | const pulse2 = this.pulse2Enabled.enabled ? this.pulse2_.volume() : 0; 142 | const pulseOut = (pulse1 || pulse2) && 143 | 95.88 / (8128 / (pulse1 + pulse2) + 100); 144 | 145 | const triangle = this.triangleEnabled.enabled ? this.triangle_.volume() : 0; 146 | const noise = this.noiseEnabled.enabled ? this.noise_.volume() : 0; 147 | const dmc = 0; // this.dmc_.volume(); 148 | const tndOut = (triangle || noise || dmc) && 149 | 159.79 / (1 / (triangle / 8227 + noise / 12241 + dmc / 22638) + 100); 150 | 151 | //console.log('volume=' + (pulseOut + tndOut)); 152 | return pulseOut + tndOut; 153 | 154 | // TODO(sdh): consider using the linear approximation and adjusting 155 | // all the APU units to output waves centered at zero. 156 | 157 | 158 | // TODO - consider giving each unit a property for 159 | // "# of cycles until volume changes" (provided 160 | // memory doesn't change). Then fast-forward without 161 | // recalculating anything - might decrease the 162 | // number of reads. 163 | 164 | } 165 | } 166 | 167 | const FRAME_CYCLES = {[3728.5 * 2]: 0, 168 | [7456.5 * 2]: 1, 169 | [11185.5 * 2]: 2, 170 | [14914.5 * 2]: 3}; 171 | const FRAME_LIMIT = 14915 * 2; 172 | -------------------------------------------------------------------------------- /apu/envelope.js: -------------------------------------------------------------------------------- 1 | import Memory from '../mem'; 2 | import LengthCounter from './lengthcounter'; 3 | 4 | /** Envelope generator (TODO - inherit from LengthCounter?). */ 5 | export default class Envelope { 6 | /** 7 | * @param {!Memory} mem Memory unit. 8 | * @param {number} base Base address for the envelope. 9 | */ 10 | constructor(mem, base) { 11 | /** @private @const {!Memory.Register} */ 12 | this.volumeEnvelope_ = mem.int(base, 0, 4, 'env'); 13 | /** @private @const {!Memory.Register} */ 14 | this.constantVolume_ = mem.bool(base, 4, '-c'); 15 | /** @private @const {!Memory.Register} */ 16 | this.loopFlag_ = mem.bool(base, 5, '-l'); // TODO(sdh): also: length counter halt? 17 | 18 | mem.listen(base + 0, () => { 19 | this.volume_ = this.computeVolume_(); 20 | }); 21 | 22 | mem.listen(base + 3, () => { 23 | // console.log('envelope start: ' + mem.get(base + 3)); 24 | // window.msg = true; 25 | this.start_ = true; 26 | if (!this.loopFlag_.get()) this.lengthCounter_.start(); 27 | }); 28 | 29 | /** @private {boolean} */ 30 | this.start_ = false; 31 | /** @private {number} */ 32 | this.divider_ = 0; 33 | /** @private {number} */ 34 | this.counter_ = 0; 35 | 36 | /** @private {number} */ 37 | this.volume_ = 0; 38 | 39 | /** @private {!LengthCounter} */ 40 | this.lengthCounter_ = new LengthCounter(mem, base); 41 | } 42 | 43 | print() { 44 | return ` 45 | volumeEnvelope=${this.volumeEnvelope_.get()} 46 | constantVolume=${this.constantVolume_.get()} 47 | loopFlag=${this.loopFlag_.get()}`; 48 | // TODO -include length counter 49 | } 50 | 51 | /** 52 | * Clocked by the frame counter. 53 | * @param {number} half Whether this is a half frame. 54 | */ 55 | clock(half) { 56 | if (!this.start_) { 57 | this.clockDivider_(); 58 | } else { 59 | this.start_ = false; 60 | this.counter_ = 15; 61 | this.reloadDivider_(); 62 | } 63 | if (half && !this.loopFlag_.get()) { 64 | this.lengthCounter_.clock(); 65 | this.volume_ = this.computeVolume_(); 66 | } 67 | } 68 | 69 | clockDivider_() { 70 | if (this.divider_ == 0) { 71 | // When the divider finishes, the counter is clocked 72 | if (this.counter_ == 0) { 73 | if (this.loopFlag_.get()) this.counter_ = 15; 74 | } else { 75 | this.counter_--; 76 | } 77 | this.reloadDivider_(); 78 | } else { 79 | this.divider_--; 80 | } 81 | } 82 | 83 | reloadDivider_() { 84 | this.divider_ = this.volumeEnvelope_.get(); 85 | this.volume_ = this.computeVolume_(); 86 | } 87 | 88 | /** Returns the volume. */ 89 | computeVolume_() { 90 | // First check the length counter 91 | if (!this.loopFlag_.get() && !this.lengthCounter_.enabled()) return 0; 92 | if (this.constantVolume_.get()) { 93 | //console.log('constant volume: ' + this.volumeEnvelope_.get()); 94 | return this.volumeEnvelope_.get(); 95 | } else { 96 | //console.log('counter: ' + this.counter_); 97 | return this.counter_; 98 | } 99 | } 100 | 101 | volume() { 102 | return this.volume_; 103 | } 104 | } 105 | -------------------------------------------------------------------------------- /apu/lengthcounter.js: -------------------------------------------------------------------------------- 1 | export default class LengthCounter { 2 | constructor(mem, base) { 3 | /** @private @const {!Memory.Register} */ 4 | this.enabled_ = mem.bool(0x4015, (base >>> 2) & 7); 5 | /** @private @const {!Memory.Register} */ 6 | this.reload_ = mem.int(base + 3, 3, 5, 'lcr'); 7 | /** @private {number} */ 8 | this.counter_ = 1; // TODO - so that it's nonzero if it's never clocked...? 9 | 10 | /** @private {function()} */ 11 | this.disableCallback_ = function() {}; 12 | 13 | mem.listen(0x4015, () => { if (!this.enabled_.get()) this.disable(); }); 14 | } 15 | 16 | /** @param {function()} callback */ 17 | onDisable(callback) { 18 | this.disableCallback_ = callback; 19 | } 20 | 21 | clock() { 22 | if (this.counter_ > 0) this.counter_--; 23 | if (!this.counter_ && this.enabled_.get()) this.enabled_.set(false); 24 | } 25 | 26 | start() { 27 | if (this.enabled_.get()) { 28 | this.counter_ = LengthCounter.LENGTHS[this.reload_.get()]; 29 | } 30 | } 31 | 32 | disable() { 33 | this.counter_ = 0; 34 | this.disableCallback_(); 35 | } 36 | 37 | /** @return {boolean} */ 38 | enabled() { 39 | return !!this.counter_; 40 | } 41 | } 42 | 43 | 44 | /** 45 | * List of lengths. 46 | * @const {!Array} 47 | */ 48 | LengthCounter.LENGTHS = [ 49 | 10,254, 20, 2, 40, 4, 80, 6, 160, 8, 60, 10, 14, 12, 26, 14, 50 | 12, 16, 24, 18, 48, 20, 96, 22, 192, 24, 72, 26, 16, 28, 32, 30]; 51 | -------------------------------------------------------------------------------- /apu/noise.js: -------------------------------------------------------------------------------- 1 | import Memory from '../mem'; 2 | import Envelope from './envelope'; 3 | 4 | /** Noise generator for NES APU. */ 5 | export default class Noise { 6 | /** 7 | * @param {!Memory} mem 8 | */ 9 | constructor(mem) { 10 | const base = 0x400C; 11 | 12 | // FOR LOGGING... 13 | mem.bool(0x4015, 3, 'N'); 14 | 15 | /** @private @const {!Memory.Register} */ 16 | this.rate_ = mem.int(base + 2, 0, 4, 'r'); 17 | /** @private @const {!Memory.Register} */ 18 | this.mode_ = mem.int(base + 2, 7, 'm'); 19 | 20 | /** @private @const {!Envelope} */ 21 | this.envelope_ = new Envelope(mem, base); 22 | 23 | /** @private {number} */ 24 | this.shiftDivider_ = 0; 25 | /** @private {number} */ 26 | this.shiftRegister_ = 1; 27 | 28 | /** @private {boolean} */ 29 | this.duty_ = false; 30 | } 31 | 32 | // print() { 33 | // return; 34 | // console.log(` 35 | // pulse ${this.base_ - 0x4000}: silenced=${this.silenced_}, duty=${DUTY_CYCLE_LIST[this.dutyCycle_.get()][this.sequence_]} 36 | // dutyCycle=${this.dutyCycle_.get()} 37 | // sweepShift=${this.sweepShift_.get()} 38 | // sweepNegate=${this.sweepNegate_.get()} 39 | // sweepPeriod=${this.sweepPeriod_.get()} 40 | // sweepEnabled=${this.sweepEnabled_.get()} 41 | // wavePeriod=${this.wavePeriod_.get()}` + this.envelope_.print()); 42 | // } 43 | 44 | /** 45 | * @return {number} The value of the waveform, from 0 to 15 (?) 46 | */ 47 | volume() { 48 | //console.log('pulse ' + (this.base_ - 0x4000) + ': silenced=' + this.silenced_ + ', length=' + this.lengthCounter_.get() + ', period=' + this.wavePeriod_.get() + ', duty=' + DUTY_CYCLE_LIST[this.dutyCycle_.get()][this.sequence_]); 49 | 50 | return this.duty_ ? this.envelope_.volume() : 0; 51 | } 52 | 53 | /** 54 | * Clocks the frame counter. 55 | * @param {number} quarter An integer from 0 to 3, indicating the quarter. 56 | */ 57 | clockFrame(quarter) { 58 | this.envelope_.clock(quarter % 2); 59 | } 60 | 61 | /** Clocks the sequencer. */ 62 | clockSequencer() { 63 | if (this.shiftDivider_ == 0) { 64 | const feedback = 65 | (this.shiftRegister_ & 1) ^ 66 | ((this.shiftRegister_ >>> (this.mode_.get() ? 6 : 1)) & 1); 67 | this.shiftRegister_ = (this.shiftRegister_ >>> 1) | (feedback << 14); 68 | this.duty_ = !!(this.shiftRegister_ & 1); 69 | this.shiftDivider_ = TIMER_PERIOD_NTSC[this.rate_.get()]; 70 | } else { 71 | this.shiftDivider_--; 72 | } 73 | } 74 | }; 75 | 76 | 77 | const TIMER_PERIOD_NTSC = [ 78 | 4, 8, 16, 32, 64, 96, 128, 160, 202, 254, 380, 508, 762, 1016, 2034, 4068]; 79 | const TIMER_PERIOD_PAL = [ 80 | 4, 8, 14, 30, 60, 88, 118, 148, 188, 236, 354, 472, 708, 944, 1890, 3778]; 81 | -------------------------------------------------------------------------------- /apu/pulse.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @fileoverview NES APU Emulator. 3 | */ 4 | 5 | import Memory from '../mem'; 6 | import Envelope from './envelope'; 7 | 8 | export default class Pulse { 9 | /** 10 | * @param {!Memory} mem 11 | * @param {number} base Base address, either $4000 or $4004. 12 | */ 13 | constructor(mem, base) { 14 | /** @private @const {number} */ 15 | this.base_ = base; 16 | 17 | // FOR LOGGING... 18 | var index = base > 0x4000 ? 2 : 1; 19 | mem.bool(0x4015, index - 1, 'P' + index); 20 | 21 | /** @private @const {!Memory.Register} */ 22 | this.dutyCycle_ = mem.int(base, 6, 2, 'd'); 23 | /** @private @const {!Memory.Register} */ 24 | this.sweepEnabled_ = mem.bool(base, 15, 'sw'); 25 | /** @private @const {!Memory.Register} */ 26 | this.sweepShift_ = mem.int(base, 8, 3, '-sh'); 27 | /** @private @const {!Memory.Register} */ 28 | this.sweepNegate_ = mem.bool(base, 11, '-n'); 29 | /** @private @const {!Memory.Register} */ 30 | this.sweepPeriod_ = mem.int(base, 12, 3, '-p'); 31 | /** @private @const {!Memory.Register} */ 32 | this.wavePeriod_ = mem.int(base, 16, 11, 'p'); 33 | 34 | /** @private @const {!Envelope} */ 35 | this.envelope_ = new Envelope(mem, base); 36 | 37 | /** @private {number} */ 38 | this.sweepDivider_ = 0; // TODO(sdh): use a Divider? 39 | /** @private {boolean} */ 40 | this.sweepReload_ = false; 41 | 42 | /** @private {boolean} Whether we're silenced due to period overflow. */ 43 | this.silenced_ = false; 44 | 45 | /** @private {number} */ 46 | this.sequence_ = 0; 47 | /** @private {number} */ 48 | this.sequenceDivider_ = 0; 49 | 50 | /** @private {boolean} Whether the duty is on or not. */ 51 | this.duty_ = false; 52 | 53 | // for (let i = 0; i < 4; i++) { 54 | // mem.listen(base + i, () => this.print()); 55 | // } 56 | mem.listen(base + 1, () => { 57 | this.sweepReload_ = true; 58 | }); 59 | mem.listen(base + 2, () => { 60 | this.duty_ = this.computeDuty_(); 61 | }); 62 | mem.listen(base + 3, () => { 63 | this.sequence_ = 0; 64 | this.duty_ = this.computeDuty_(); 65 | // NOTE: envelope also restarted... (elsewhere) 66 | }); 67 | } 68 | 69 | print() { 70 | return; 71 | console.log(` 72 | pulse ${this.base_ - 0x4000}: silenced=${this.silenced_}, duty=${DUTY_CYCLE_LIST[this.dutyCycle_.get()][this.sequence_]} 73 | dutyCycle=${this.dutyCycle_.get()} 74 | sweepShift=${this.sweepShift_.get()} 75 | sweepNegate=${this.sweepNegate_.get()} 76 | sweepPeriod=${this.sweepPeriod_.get()} 77 | sweepEnabled=${this.sweepEnabled_.get()} 78 | wavePeriod=${this.wavePeriod_.get()}` + this.envelope_.print()); 79 | } 80 | 81 | /** 82 | * @return {boolean} Whether the pulse is currently high. 83 | */ 84 | computeDuty_() { 85 | //console.log('pulse ' + (this.base_ - 0x4000) + ': silenced=' + this.silenced_ + ', length=' + this.lengthCounter_.get() + ', period=' + this.wavePeriod_.get() + ', duty=' + DUTY_CYCLE_LIST[this.dutyCycle_.get()][this.sequence_]); 86 | return !( 87 | this.silenced_ || 88 | this.wavePeriod_.get() < 8 || 89 | !DUTY_CYCLE_LIST[this.dutyCycle_.get()][this.sequence_] == 0); 90 | } 91 | 92 | /** 93 | * @return {number} The value of the waveform, from 0 to 15 (?) 94 | */ 95 | volume() { 96 | return this.duty_ ? this.envelope_.volume() : 0; 97 | } 98 | 99 | /** 100 | * Clocks the frame counter. 101 | * @param {number} quarter An integer from 0 to 3, indicating the quarter. 102 | */ 103 | clockFrame(quarter) { 104 | if (this.sweepDivider_ == 0 && this.sweepEnabled_.get()) { 105 | const target = this.sweepTarget_(); 106 | if (target > 0x7ff || target < 8) { 107 | this.silenced_ = true; 108 | this.duty_ = false; 109 | } else { 110 | this.wavePeriod_.set(target); 111 | } 112 | } else if (this.sweepDivider_ != 0) { 113 | this.sweepDivider_--; 114 | } 115 | if (this.sweepReload_) { 116 | this.sweepDivider_ = this.sweepPeriod_.get(); 117 | this.sweepReload_ = false; 118 | this.silenced_ = false; 119 | } 120 | this.envelope_.clock(quarter % 2); 121 | this.duty_ = this.computeDuty_(); 122 | } 123 | 124 | /** Clocks the sequencer. */ 125 | clockSequencer() { 126 | if (this.sequenceDivider_ == 0) { 127 | this.sequenceDivider_ = this.wavePeriod_.get(); 128 | this.sequence_ = (this.sequence_ + 1) % 8; 129 | this.duty_ = this.computeDuty_(); 130 | } else { 131 | this.sequenceDivider_--; 132 | } 133 | } 134 | 135 | sweepTarget_() { 136 | const period = this.wavePeriod_.get(); 137 | let delta = period >>> this.sweepShift_.get(); 138 | if (this.sweepNegate_.get()) { 139 | delta = this.base_ == 0x4000 ? ~delta : -delta; 140 | } 141 | return period + delta; 142 | } 143 | }; 144 | 145 | 146 | /** 147 | * The various duty cycles. 148 | * @enum {!Array} 149 | */ 150 | const Duty = { 151 | /** 12.5% duty */ 152 | EIGHTH: [0, 1, 0, 0, 0, 0, 0, 0], 153 | /** 25% duty */ 154 | QUARTER: [0, 1, 1, 0, 0, 0, 0, 0], 155 | /** 50% duty */ 156 | HALF: [0, 1, 1, 1, 1, 0, 0, 0], 157 | /** 25% duty negated */ 158 | THREE_QUARTERS: [1, 0, 0, 1, 1, 1, 1, 1], 159 | }; 160 | 161 | const DUTY_CYCLE_LIST = [ 162 | Duty.EIGHTH, Duty.QUARTER, Duty.HALF, Duty.THREE_QUARTERS]; 163 | -------------------------------------------------------------------------------- /apu/resampler.js: -------------------------------------------------------------------------------- 1 | /** 2 | * A resampler takes input at one frequency and 3 | * converts it to output at a different frequency. 4 | */ 5 | export default class Resampler { 6 | /** 7 | * @param {{write: function(!Float32Array)}} output 8 | * @param {number} factor The quotient 'outputRate / inputRate'. 9 | */ 10 | constructor(output, factor) { 11 | this.output_ = output; 12 | // Note: this should be less than 1 for NES-style downsampling. 13 | this.factor_ = factor; 14 | 15 | this.offset_ = 0; 16 | } 17 | 18 | 19 | /** 20 | * @param {!Float32Array} data 21 | */ 22 | write(data) { 23 | const delta = this.factor_ * data.length; 24 | const startTime = Math.ceil(this.offset_); 25 | const endTime = Math.floor(this.offset_ + delta); 26 | const result = new Float32Array(endTime - startTime); 27 | for (let time = startTime; i < endTime; i++) { 28 | const index = Math.floor((time - this.offset_) / this.factor_); 29 | if (index >= data.length) { 30 | throw new Error('bad index: ' + index + ', ' + data.length); 31 | } 32 | // TODO(sdh): implement Lanczos resampling for fractional 33 | // delays using some sort of lookup table. 34 | result[time - startTime] = data[index]; 35 | } 36 | this.offset_ = this.offset_ + delta - endTime; 37 | write 38 | } 39 | 40 | } 41 | -------------------------------------------------------------------------------- /apu/triangle.js: -------------------------------------------------------------------------------- 1 | import Memory from '../mem'; 2 | import LengthCounter from './lengthcounter'; 3 | 4 | // TODO - this pops a lot - can we smooth it out?!? 5 | 6 | export default class Triangle { 7 | /** 8 | * @param {!Memory} mem 9 | */ 10 | constructor(mem) { 11 | const base = 0x4008; 12 | 13 | // FOR LOGGING... 14 | mem.bool(0x4015, 2, 'T'); 15 | 16 | /** @private @const {!Memory.Register} */ 17 | this.linearCounterReloadValue_ = mem.int(0x4008, 0, 7, 'r'); 18 | /** @private @const {!Memory.Register} */ 19 | this.control_ = mem.bool(0x4008, 7, 'c'); 20 | /** @private @const {!Memory.Register} */ 21 | this.sequenceTimerPeriod_ = mem.int(0x400A, 0, 11, 'p'); 22 | 23 | /** @private {number} */ 24 | this.volume_ = 0; 25 | 26 | /** @private {!LengthCounter} */ 27 | this.lengthCounter_ = new LengthCounter(mem, base); 28 | 29 | /** @private {boolean} */ 30 | this.linearCounterReloadFlag_ = false; 31 | /** @private {number} */ 32 | this.linearCounter_ = 0; 33 | 34 | /** @private {number} */ 35 | this.sequenceTimer_ = 0; 36 | /** @private {number} */ 37 | this.sequence_ = 0; 38 | 39 | 40 | for (let i = 0x4008; i < 0x400C; i++) mem.listen(i, () => this.print()); 41 | 42 | mem.listen(0x400B, () => this.linearCounterReloadFlag_ = true); 43 | this.lengthCounter_.onDisable(() => this.volume_ = 0); 44 | } 45 | 46 | print() { 47 | return; 48 | console.log(`TRIANGLE 49 | linear ${this.linearCounter_} of ${this.linearCounterReloadValue_.get()} 50 | control ${this.control_.get()} 51 | sequence ${this.sequenceTimer_} of ${this.sequenceTimerPeriod_.get()} => ${this.sequence_} 52 | length ${this.lengthCounter_.enabled_.get()} ${this.lengthCounter_.counter_} ${this.lengthCounter_.reload_.get()} 53 | volume ${this.volume_}`); 54 | } 55 | 56 | /** 57 | * @return {number} The value of the waveform, from 0 to 15 (?) 58 | */ 59 | volume() { 60 | return this.volume_; 61 | } 62 | 63 | /** 64 | * @return {number} Computes the volume. 65 | */ 66 | computeVolume_() { 67 | const v = this.lengthCounter_.enabled() && this.linearCounter_ > 0 ? 68 | WAVEFORM[this.sequence_] : 0; 69 | //if (v == 15) console.log('TRIANGLE 15'); 70 | return v; 71 | } 72 | 73 | /** 74 | * Clocks the frame counter. 75 | * @param {number} quarter An integer from 0 to 3, indicating the quarter. 76 | */ 77 | clockFrame(quarter) { 78 | if (this.linearCounterReloadFlag_) { 79 | this.linearCounter_ = this.linearCounterReloadValue_.get(); 80 | } else if (this.linearCounter_ > 0) { 81 | this.linearCounter_--; 82 | if (!this.linearCounter_) this.volume_ = 0; 83 | } 84 | 85 | if (this.control_.get()) { 86 | this.linearCounterReloadFlag_ = false; 87 | } 88 | 89 | if (quarter % 2 && !this.control_.get()) { 90 | this.lengthCounter_.clock(); 91 | } 92 | } 93 | 94 | /** Clocks the sequencer. */ 95 | clockSequencer() { 96 | if (this.sequenceTimer_ == 0) { 97 | this.sequence_ = (this.sequence_ + 1) % 32; 98 | this.sequenceTimer_ = this.sequenceTimerPeriod_.get(); 99 | this.volume_ = this.computeVolume_(); 100 | } else { 101 | this.sequenceTimer_--; 102 | } 103 | } 104 | }; 105 | 106 | 107 | const WAVEFORM = [15, 14, 13, 12, 11, 10, 9, 8, 7, 6, 5, 4, 3, 2, 1, 0, 108 | 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15]; 109 | -------------------------------------------------------------------------------- /audio/bufferedaudionode.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /** An audio node that allows writing samples. */ 4 | export default class BufferedAudioNode { 5 | /** 6 | * @param {!AudioContext} ac 7 | * @param {number=} buffer Length of buffer, in seconds (default 2). 8 | */ 9 | constructor(ac, buffer) { 10 | buffer = buffer != null ? buffer : 2; 11 | /** @private @const */ 12 | this.fc_ = buffer * ac.sampleRate; 13 | /** @private @const */ 14 | this.ac_ = ac; 15 | /** @private @const */ 16 | this.ab_ = ac.createBuffer(2, this.fc_, ac.sampleRate); 17 | /** @private @const */ 18 | this.c0_ = this.ab_.getChannelData(0); 19 | /** @private @const */ 20 | this.c1_ = this.ab_.getChannelData(1); 21 | /** @private {!AudioBufferSourceNode} */ 22 | this.s_ = ac.createBufferSource(); 23 | /** @private {number} */ 24 | this.written_ = 0; 25 | /** @private {number} */ 26 | this.started_ = -1; 27 | /** @private {number} */ 28 | this.underrun_ = -1; 29 | /** @private {?AudioNode} */ 30 | this.destination_ = null; 31 | 32 | this.s_.buffer = this.ab_; 33 | this.s_.loop = true; 34 | 35 | this.maxBuffer_ = 0.9; 36 | this.minBuffer_ = 0.5; 37 | this.writePromise_ = null; 38 | this.writeResolver_ = null; 39 | 40 | Object.seal(this); 41 | } 42 | 43 | get context() { return this.ac_; } 44 | get numberOfInputs() { return 0; } 45 | get numberOfOutputs() { return 1; } 46 | get channelCount() { return 2; } 47 | get channelCountMode() { return 'explicit'; } 48 | get channelInterpretation() { return 'speakers'; } 49 | 50 | 51 | /** Connects to an output. */ 52 | connect(dest) { 53 | this.destination_ = dest; 54 | this.s_.connect(dest); 55 | } 56 | 57 | 58 | /** Disconnects the output. */ 59 | disconnect() { 60 | this.destination_ = null; 61 | this.s_.disconnect(); 62 | } 63 | 64 | 65 | /** Resets everything to an empty buffer. */ 66 | reset() { 67 | if (this.writeResolver_) this.writeResolver_(); 68 | if (this.started_ >= 0) this.s_.stop(0); 69 | this.s_.disconnect(); 70 | 71 | this.s_ = this.ac_.createBufferSource(); 72 | this.written_ = 0; 73 | this.started_ = -1; 74 | this.writePromise_ = this.writeResolver_ = null; 75 | 76 | this.s_.buffer = this.ab_; 77 | this.s_.loop = true; 78 | this.s_.connect(this.ac_.destination); 79 | } 80 | 81 | 82 | /** 83 | * @return {number} The current fraction of the buffer filled. 84 | */ 85 | buffer() { 86 | if (this.started_ < 0) return 0; 87 | const frames = this.written_ + this.started_ - 88 | this.ac_.currentTime * this.ac_.sampleRate; 89 | // TODO(sdh): if frames < 0 then there's an underrun. 90 | // - if started is zero then this shouldn't be a problem 91 | // (though it would be nice to rearrange things so this 92 | // doesn't happen). 93 | // - otherwise we probably want to advance written? 94 | return Math.max(0, frames) / this.fc_; 95 | } 96 | 97 | 98 | /** 99 | * @return {number} Time (in seconds) until the buffer underruns. 100 | */ 101 | bufferTime() { 102 | if (this.started_ < 0) return Infinity; 103 | const time = (this.written_ + this.started_) / this.ac_.sampleRate - 104 | this.ac_.currentTime; 105 | return Math.max(0, time); 106 | } 107 | 108 | 109 | /** 110 | * @return {number} Max samples that can be added without an overrun. 111 | */ 112 | bufferLimit() { 113 | const limit = (this.maxBuffer_ - this.buffer()) * this.fc_; 114 | return Math.max(0, Math.floor(limit)); 115 | } 116 | 117 | 118 | /** 119 | * @param {!Array|!Float32Array} left 120 | * @param {!Array|!Float32Array=} right 121 | * @param {number=} offset 122 | * @param {!Promise=} promise 123 | * @return {!Promise} Promise that completes when buffer written. 124 | */ 125 | write(left, right, offset, promise) { 126 | right = right || left; 127 | offset = offset || 0; 128 | if (this.writePromise_ != promise) { // n.b. single-threaded 129 | return this.writePromise_ = this.writePromise_.then( 130 | () => this.write(left, right, offset, promise)); 131 | } 132 | const max = Math.max(left.length, right.length); 133 | const remainingBuffer = 134 | Math.max(0, this.maxBuffer_ - Math.max(0, this.buffer())); 135 | const end = Math.floor(Math.min(max, offset + remainingBuffer * this.fc_)); 136 | let pos = this.written_ % this.fc_; 137 | if (offset > end) throw new Error('impossible ' + offset + ' > ' + end); 138 | if (this.written_ == 0 && offset < end) { 139 | this.s_.start(this.ac_.currentTime); 140 | this.started_ = this.ac_.currentTime * this.ac_.sampleRate; 141 | } 142 | for (let i = offset; i < end; i++) { 143 | this.c0_[pos] = i < left.length ? left[i] : right[i]; 144 | this.c1_[pos] = i < right.length ? right[i] : left[i]; 145 | if (++pos >= this.fc_) pos = 0; 146 | } 147 | this.written_ += (end - offset); 148 | if (end < max) { 149 | if (!this.writePromise_) { 150 | this.writePromise_ = 151 | new Promise(resolve => this.writeResolver_ = resolve); 152 | } 153 | const delta = Math.max(this.maxBuffer_ - this.minBuffer_, 0); 154 | setTimeout( 155 | () => { this.write(left, right, end, this.writePromise_); }, 156 | this.fc_ / this.ac_.sampleRate * 1000 * delta); 157 | return this.writePromise_; 158 | } else if (this.writeResolver_) { // we're done, so resolve 159 | this.writeResolver_(); // n.b. then() methods happen *after frame* 160 | this.writeResolver_ = this.writePromise_ = null; 161 | } 162 | // No pending operation, so zero out the buffer after the end (underrun?) 163 | const buf = this.buffer() * this.fc_; // frames of buffer left 164 | const empty = this.fc_ - buf; 165 | for (let i = 0; i < empty; i++) { 166 | this.c0_[pos] = this.c1_[pos] = 0; 167 | if (++pos >= this.fc_) pos = 0; 168 | } 169 | const written = this.written_; 170 | // TODO(sdh): cancel previous checks? 171 | clearTimeout(this.underrun_); 172 | this.underrun_ = setTimeout(() => { 173 | if (this.written_ == written) this.reset(); 174 | }, (empty * 0.9 + buf) / this.ac_.sampleRate * 1000); 175 | return Promise.resolve(); 176 | } 177 | } 178 | -------------------------------------------------------------------------------- /audio/kernel.js: -------------------------------------------------------------------------------- 1 | import {Complex,cisi} from '../util/math'; 2 | import {nub} from '../util/util'; 3 | 4 | /** A convolution kernel. */ 5 | export class StepKernel { 6 | /** 7 | * @param {!Array>} data Array of convolution kernels, 8 | * expressed as differences. The outer array contains evenly-spaced 9 | * delays, and the inner array is a list of differences, one for 10 | * each following sample. The opposite array will be used for the 11 | * samples preceding the step. 12 | */ 13 | constructor(data) { 14 | /** @private @const {!Array>} */ 15 | this.data_ = data; 16 | const sizes = nub(data.map(x => x.length)); 17 | if (sizes.length != 1) throw new Error('non-rectangular array'); 18 | /** @private @const {number} */ 19 | this.radius_ = sizes[0] / 2; 20 | /** @private @const {number} */ 21 | this.phases_ = 2 * (data.length - 1); 22 | } 23 | 24 | /** @return {number} Required distance from a sample. */ 25 | get radius() { 26 | return this.radius_; 27 | } 28 | 29 | /** 30 | * Note: caller should keep track of radius and when to stop passing. 31 | * @param {number} startTime Integer sample index. 32 | * @param {number} endTime Integer sample index. 33 | * @param {!Array>} steps List of [time, delta] pairs, 34 | * where times may be fractional sample indices. 35 | * @return {!Float32Array} Deltas. 36 | */ 37 | convolve(startTime, endTime, steps) { 38 | const out = new Float32Array(endTime - startTime); 39 | for (let step of steps) { 40 | const delta = step[1]; 41 | const center = Math.round(step[0]); 42 | const phase = Math.round((step[0] - center) * this.phases_); 43 | let kernel = this.data_[Math.abs(phase)]; 44 | if (phase < 0) { 45 | kernel = kernel.slice().reverse(); 46 | } 47 | // ex: center=5, startTime=2, kernel.length=4 (3-4, 4-5, 5-6, 6-7) 48 | // --> startIndex = 5 - 2 - (4/2) = 1 = second diff 49 | const startIndex = center - startTime - kernel.length / 2; 50 | for (let i = Math.max(0, -startIndex); 51 | i < Math.min(kernel.length, out.length - startIndex); i++) { 52 | out[startIndex + i] += kernel[i] * delta; 53 | } 54 | } 55 | return out; 56 | } 57 | } 58 | 59 | 60 | /** 61 | * @param {number} radius Integer radius of kernel. Given a step at 62 | * time t, all points between [t-r, t+r] are affected. 63 | * @param {number} phases Number of different phases to compute. 64 | * @return {!StepKernel} 65 | */ 66 | export function lanczosKernel(radius, phases) { 67 | radius = radius >>> 0; // unsigned int 68 | phases -= phases % 2; // must be even 69 | 70 | const a = 2 * radius - 1; // Note: guarantees a is always odd. 71 | const PI = Math.PI; 72 | function f(x) { 73 | if (x == 0) return 0; 74 | if (x >= a) return 0.5; 75 | if (x <= -a) return -0.5; 76 | const m1 = (a - 1) * PI * x; 77 | const p1 = (a + 1) * PI * x; 78 | return (-m1 * cisi(m1 / a).im + p1 * cisi(p1 / a).im - 79 | 2 * a * Math.sin(PI * x) * Math.sin(PI * x / a)) / 80 | (2 * PI * PI * x); 81 | } 82 | const norm = f(a) - f(-a); 83 | function diff(xs) { 84 | const diffs = new Array(xs.length - 1); 85 | for (let i = 1; i < xs.length; i++) { 86 | diffs[i - 1] = xs[i] - xs[i - 1]; 87 | } 88 | return diffs; 89 | } 90 | 91 | const fracs = []; // [0, 2/phases, ..., 1], negative fracs inferred 92 | for (let i = 0; i <= phases; i += 2) fracs.push(i / phases /* - 1*/); 93 | const samples = []; // [-a - 1, -a + 1, ..., a + 1] 94 | for (let i = -a - 1; i <= a + 1; i += 2) samples.push(i); 95 | // 2d array of times [[-5, -3, ..., 5], [-4.75, -2.75, ..., 5.25], ... 96 | const times = fracs.map(frac => samples.map(base => base - frac)); 97 | 98 | // console.log('times([' + times.map(ts=>'['+ts.join(',')+']').join(', ') + '])'); 99 | 100 | const values = times.map(ts => ts.map(t => f(t) / norm + 0.5)); 101 | return new StepKernel(values.map(diff)); 102 | 103 | // TODO(sdh): There are a few cases where floating-point roundoff 104 | // causes the sum to be 1-ε instead of 1. These *should* cancel 105 | // out in the long run, but we could also take one of two other 106 | // approaches if it causes a problem: 107 | // 1. add a very low frequency drift correction in the direction 108 | // opposite the 1-2s moving average 109 | // 2. discretize the fractions, rounding off at (say) 2^-26 both 110 | // the step heights and the lanczos coefficients 111 | // Note that the latter option requires care: we can't simply round 112 | // the coefficients because they won't necessarily add to 1 - we'd 113 | // need to add/subtract a constant (very small) offset first until 114 | // they did add to exactly 1. This same problem could possibly 115 | // also show up in adding rounded steps, so the drift may be a 116 | // better option. 117 | } 118 | -------------------------------------------------------------------------------- /audio/stepbufferwriter.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | import BufferedAudioNode from './bufferedaudionode'; 4 | import {StepKernel,lanczosKernel} from './kernel'; 5 | 6 | /** A class to write steps to a buffer. */ 7 | export default class StepBufferWriter { 8 | /** 9 | * @param {!BufferedAudioNode} buffer 10 | */ 11 | constructor(buffer) { 12 | /** @private @const {!BufferedAudioNode} */ 13 | this.buffer_ = buffer; 14 | /** @private @const {number} */ 15 | this.sampleRate_ = buffer.context.sampleRate; 16 | 17 | /** @private {number} Current sample index. */ 18 | this.sampleIndex_ = 0; 19 | /** @private {number} Last sample value read from steps. */ 20 | this.lastStep_ = 0; 21 | /** @private {number} Last sample value written to buffer. */ 22 | this.lastSample_ = 0; 23 | /** @private {!Array>} Steps we currently care about. */ 24 | this.steps_ = []; 25 | 26 | // this.kernel_ = new StepKernel([[.5, .5], [.5, .5]]); 27 | //this.kernel_ = new StepKernel([[1, 0], [0, 1]]); 28 | 29 | /** @private @const {!StepKernel} */ 30 | this.kernel_ = lanczosKernel(5, 32); 31 | } 32 | 33 | 34 | /** 35 | * @param {!Array>} steps Array of [time (s), sample] 36 | * indicating transition times between steps. 37 | * @return {!Promise} 38 | */ 39 | write(steps) { 40 | 41 | // TODO(sdh): consider having the input always start at zero? 42 | 43 | // console.log('WRITE: [' + steps.map(s=>`[${s[0]},${s[1]}]`) + ']'); 44 | if (!steps.length) return new Promise(resolve => setTimeout(resolve, 50)); 45 | 46 | const samples = []; 47 | 48 | for (let step of steps) { 49 | // console.log('step: ' + step[0] + ', ' + step[1]); 50 | const s = step[0] * this.sampleRate_; 51 | const v = step[1] - this.lastStep_; 52 | this.lastStep_ = step[1]; 53 | //console.log('step: [' + s + ', ' + v + ']'); 54 | 55 | // console.log(`step: ${step} s=${s} v=${v} sampleIndex=${this.sampleIndex_} endSample=${Math.floor(s - this.kernel_.radius)}`); 56 | 57 | if (s <= this.sampleIndex_) { 58 | this.lastStep_ = this.lastSample_ = step[1]; 59 | this.steps_ = []; 60 | console.log('past value: resetting'); 61 | continue; 62 | } 63 | //this.lastSample_ += v; 64 | 65 | // Compute samples until s - kernel.radius 66 | const endSample = Math.floor(s - this.kernel_.radius); 67 | if (endSample > this.sampleIndex_) { 68 | const deltas = 69 | this.kernel_.convolve(this.sampleIndex_, endSample, this.steps_) 70 | for (let delta of /** @type {!Iterable} */ (deltas)) { 71 | // TODO(sdh): can we remove the floating-point error drift?!? 72 | //this.lastSample_ *= 0.9999995; 73 | samples.push(this.lastSample_ += delta); 74 | } 75 | this.sampleIndex_ = endSample; 76 | 77 | const done = Math.floor(this.sampleIndex_ - this.kernel_.radius); 78 | let i = 0; 79 | while (i < this.steps_.length && this.steps_[i][0] < done) i++; 80 | if (i > 0) this.steps_.splice(0, i); 81 | } 82 | // console.log('step push: ' + s + ', ' + v); 83 | this.steps_.push([s, v]); 84 | } 85 | // now write the buffer. 86 | // console.log(`Writing ${samples.length} samples`, samples); 87 | return this.buffer_.write(samples); 88 | } 89 | } 90 | 91 | 92 | -------------------------------------------------------------------------------- /audio/stepgeneratornode.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | import BufferedAudioNode from './bufferedaudionode'; 4 | import {StepKernel,lanczosKernel} from './kernel'; 5 | 6 | /** An audio node that takes a generator function as input. */ 7 | export default class StepGeneratorNode { 8 | /** 9 | * @param {!AudioContext} ac 10 | * @param {number=} bufferLength 11 | */ 12 | constructor(ac, bufferLength) { 13 | /** @private @const {!AudioContext} */ 14 | this.ac_ = ac; 15 | /** @private @const {number} */ 16 | this.bufferLength_ = bufferLength != null ? bufferLength : 2; 17 | 18 | /** @private @const {!BufferedAudioNode} */ 19 | this.buffer_ = new BufferedAudioNode(ac, this.bufferLength_); 20 | 21 | /** @private {?function()} Generator function. */ 22 | this.generator_ = null; 23 | /** @private {number} Current sample index. */ 24 | this.sample_ = 0; 25 | /** @private {number} Last sample index. */ 26 | this.lastSample_ = 0; 27 | /** @private {number} Last sample value. */ 28 | this.lastValue_ = 0; 29 | /** @private {!Array>} Steps we currently care about. */ 30 | this.steps_ = []; 31 | 32 | // this.kernel_ = new StepKernel([[.5, .5], [.5, .5]]); 33 | // this.kernel_ = new StepKernel([[1, 0], [0, 1]]); 34 | 35 | /** @private @const {!StepKernel} */ 36 | this.kernel_ = lanczosKernel(5, 32); 37 | } 38 | 39 | 40 | /** @param {!AudioNode} destination */ 41 | connect(destination) { 42 | this.buffer_.connect(destination); 43 | } 44 | 45 | 46 | /** @param {?function()} generator */ 47 | set generator(generator) { 48 | // TODO - do we need to cancel the previous one? 49 | generator = generator instanceof Function ? generator() : generator; 50 | this.generator_ = generator; 51 | this.sample_ = 0; 52 | this.lastSample_ = 0; 53 | this.lastValue_ = 0; 54 | this.steps_ = []; 55 | 56 | if (generator) this.generate_(generator); 57 | } 58 | 59 | /** 60 | * Generates more waveform. 61 | * @param {?function()} generator The current generator; quits on mismatch. 62 | * @private 63 | */ 64 | generate_(generator) { 65 | // If generator has changed, then do nothing and don't queue another 66 | if (generator != this.generator_) return; 67 | 68 | const timeLimit = new Date().getTime() + this.buffer_.bufferTime() * 1000; 69 | const bufferLimit = this.buffer_.bufferLimit(); 70 | 71 | // Loop until we either have (1) a full buffer, (2) 10000 steps, 72 | // (3) exhausted generator, or (4) risk of buffer underrun. 73 | let stepCount = 0; 74 | let next; 75 | const samples = []; 76 | while (!(next = generator.next()).done) { 77 | for (let step of next.value) { 78 | // console.log('step: ' + step[0] + ', ' + step[1]); 79 | const s = step[0] * this.ac_.sampleRate; 80 | const v = step[1] - this.lastValue_; 81 | //console.log('step: [' + s + ', ' + v + ']'); 82 | 83 | if (s <= this.sample_) { 84 | this.lastSample_ = step[1]; 85 | this.lastValue_ = step[1]; 86 | this.steps_ = []; 87 | console.log('past value: resetting'); 88 | continue; 89 | } 90 | this.lastValue_ += v; 91 | 92 | // Compute samples until s - kernel.radius 93 | const endSample = Math.floor(s - this.kernel_.radius); 94 | if (endSample > this.sample_) { 95 | const deltas = 96 | this.kernel_.convolve(this.sample_, endSample, this.steps_) 97 | for (let delta of /** @type {!Iterable} */ (deltas)) { 98 | // TODO(sdh): can we remove the floating-point error drift?!? 99 | //this.lastSample_ *= 0.9999995; 100 | samples.push(this.lastSample_ += delta); 101 | } 102 | this.sample_ = endSample; 103 | 104 | const done = Math.floor(this.sample_ - this.kernel_.radius); 105 | let i = 0; 106 | while (i < this.steps_.length && this.steps_[i][0] < done) i++; 107 | if (i > 0) this.steps_.splice(0, i); 108 | } 109 | // console.log('step push: ' + s + ', ' + v); 110 | this.steps_.push([s, v]); 111 | } 112 | 113 | // Check the various ending conditions. 114 | if (new Date().getTime() >= timeLimit) { 115 | console.log('time limit exceeded'); 116 | break; 117 | } 118 | if (samples.length >= bufferLimit) { 119 | console.log('buffer limit exceeded'); 120 | break; 121 | } 122 | if (++stepCount >= 100000) { 123 | console.log('step count exceeded'); 124 | break; 125 | } 126 | if (next.done) console.log('done!?'); 127 | } 128 | if (!samples.length) { 129 | //this.generate_(generator); 130 | console.log('no samples!'); 131 | return; 132 | } 133 | // now write the buffer. 134 | // console.log('write()', samples); 135 | this.buffer_.write(samples).then(() => this.generate_(generator)); 136 | } 137 | } 138 | 139 | 140 | -------------------------------------------------------------------------------- /bankswitcher.js: -------------------------------------------------------------------------------- 1 | 2 | /** Sets up bank switching for the upper half of the RAM. */ 3 | export default class BankSwitcher { 4 | /** 5 | * @param {!Memory} mem 6 | * @param {boolean} fds Whether to set up FDS bank switching. 7 | */ 8 | constructor(mem, fds) { 9 | /** @private @const {!Memory} */ 10 | this.mem_ = mem; 11 | /** @private @const {!Array} */ 12 | this.pages_ = []; 13 | for (let i = fds ? 6 : 8; i < 16; i++) { 14 | mem.listen(0x5FF0 + i, page => this.swap_(i, page)); 15 | } 16 | } 17 | 18 | 19 | get pages() { 20 | return this.pages_.length; 21 | } 22 | 23 | 24 | /** 25 | * Loads the banks. 26 | * @param {!Uint8Array} data 27 | * @param {number} padding Padding, only the low 12 bits are used. 28 | */ 29 | load(data, padding) { 30 | padding &= 0xfff; 31 | let bufferSize = padding + data.length; 32 | if (bufferSize & 0xfff) bufferSize = ((bufferSize & ~0xfff) + 0x1000); 33 | let buffer; 34 | if (data.length < bufferSize) { 35 | buffer = new Uint8Array(bufferSize); 36 | buffer.set(data, padding); 37 | } else { 38 | buffer = data; 39 | } 40 | for (let i = 0; i < (bufferSize >>> 12); i++) { 41 | this.pages_[i] = new Uint8Array(buffer.buffer, i << 12, 0x1000); 42 | } 43 | console.log('Loaded ' + this.pages_.length + ' pages from ' + data.length); 44 | } 45 | 46 | 47 | /** 48 | * @param {number} bank The bank index to fill. 49 | * @param {number} page The page number to load from. 50 | * @private 51 | */ 52 | swap_(bank, page) { 53 | console.log('BANK SWITCH: ' + bank + ' <= ' + page); 54 | if (!this.pages_.length) return; // bank switching not loaded/enabled 55 | if (page >= this.pages_.length) throw new Error('invalid bank index'); 56 | this.mem_.load(this.pages_[page], (bank & 0xf) << 12); 57 | console.log(' ==> done'); 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /clock.js: -------------------------------------------------------------------------------- 1 | /** 2 | * The master clock. 3 | * @constructor 4 | * @struct 5 | */ 6 | export default function Clock(speed, ntsc) { 7 | let tick = 1 / speed; 8 | this.time = 0; 9 | this.ntsc = ntsc; 10 | this.cycleLength = tick; 11 | this.tick = function() { this.time += tick; } 12 | }; 13 | 14 | 15 | /** Makes a new NTSC clock. */ 16 | Clock.ntsc = function() { return new Clock(1789773, true); }; 17 | 18 | /** Makes a new PAL clock. */ 19 | Clock.pal = function() { return new Clock(1662607, false); }; 20 | 21 | 22 | // export default class Clock { 23 | // /** @param {number} speed Clock speed, in Hz. 24 | // * @param {boolean} ntsc */ 25 | // constructor(speed, ntsc) { 26 | // /** @private {number} */ 27 | // this.tick_ = 1 / speed; 28 | // /** @private {number} */ 29 | // this.time_ = 0; 30 | // // /** @private {!Array<{u: (function(): number), d: number}>} */ 31 | // // this.units_ = []; 32 | // this.ntsc_ = ntsc; 33 | // } 34 | 35 | // get ntsc() { 36 | // return this.ntsc_; 37 | // } 38 | 39 | // get cycleLength() { 40 | // return this.tick_; 41 | // } 42 | 43 | // get time() { 44 | // return this.time_; 45 | // } 46 | 47 | // // /** 48 | // // * Adds a unit, which is a tick function that returns a delay. 49 | // // * @param {function(): number} unit 50 | // // */ 51 | // // add(unit) { 52 | // // this.units_.push({u: unit, d: 1}); 53 | // // } 54 | 55 | // /** Ticks the clock. */ 56 | // tick() { 57 | // // for (let u of this.units_) { 58 | // // if (--u.d == 0) { 59 | // // u.d = u.u() || 1; 60 | // // } 61 | // // } 62 | // this.time_ += this.tick_; 63 | // } 64 | 65 | // /** Makes a new NTSC clock. */ 66 | // static ntsc() { return new Clock(1789773, true); } 67 | 68 | // /** Makes a new PAL clock. */ 69 | // static pal() { return new Clock(1662607, false); } 70 | // } 71 | -------------------------------------------------------------------------------- /cpu.js: -------------------------------------------------------------------------------- 1 | import Memory from './mem'; 2 | 3 | 4 | /** Models the CPU. */ 5 | export default class Cpu { 6 | /** 7 | * @param {!Memory} mem 8 | */ 9 | constructor(mem) { 10 | /** @private @const {!Memory} */ 11 | this.mem_ = mem; 12 | 13 | /** @type {boolean} Carry flag. */ 14 | this.C = false; 15 | /** @type {boolean} Zero flag. */ 16 | this.Z = false; 17 | /** @type {boolean} IRQ disable. */ 18 | this.I = false; 19 | /** @type {boolean} Decimal mode. */ 20 | this.D = false; 21 | /** @type {boolean} Break flag. */ 22 | this.B = false; 23 | /** @type {boolean} Overflow flag. */ 24 | this.V = false; 25 | /** @type {boolean} Sign flag. */ 26 | this.S = false; 27 | /** @type {number} Accumulator */ 28 | this.A = 0; 29 | /** @type {number} Index X */ 30 | this.X = 0; 31 | /** @type {number} Index Y */ 32 | this.Y = 0; 33 | /** @type {number} Stack pointer */ 34 | this.SP = 0x1ff; 35 | /** @type {number} Program counter */ 36 | this.PC = 0; 37 | 38 | this.opcode = null; 39 | this.operand = null; 40 | this.wait = 1; 41 | 42 | this.opcodes_ = instructionTable(); 43 | 44 | const logdiv = document.getElementById('log'); 45 | let msgs = []; 46 | let msgslen = 0; 47 | //const tabble = new Tabble(logdiv); 48 | // this.log = msg => tabble.write(msg); 49 | 50 | const VOID = () => {}; 51 | this.log = logdiv ? msg => { 52 | msgs.push(msg); 53 | msgslen += msg.length; 54 | if (msgslen > 1000) { 55 | logdiv.textContent += msgs.join(''); 56 | msgslen = 0; 57 | msgs = []; 58 | } 59 | } : VOID; // msg => console.log(msg); 60 | 61 | this.line = this.log != VOID ? rest => this.log('\t' + rest()) : VOID; 62 | 63 | // this.log = () => {}; 64 | // this.line = () => {}; 65 | } 66 | 67 | init() { 68 | this.C = this.Z = this.I = this.D = this.B = this.V = this.S = false; 69 | this.A = this.X = this.Y = this.PC = 0; 70 | this.SP = 0x1ff; 71 | this.opcode = this.operand = null; 72 | this.wait = 1; 73 | } 74 | 75 | clock() { 76 | // Tick the wait until we're down to zero. Once we hit zero, 77 | // then run the opcode that was set before wait started. 78 | if (!this.opcode) this.loadOp(); 79 | if (--this.wait > 1) return; 80 | if (this.opcode) { 81 | // Actually execute the opcode. 82 | //try { 83 | this.opcode.op.call(this); 84 | //} finally { 85 | // //if (window.msg) { 86 | //this.log(this.message); 87 | //this.message = ''; 88 | // //window.msg = false; } 89 | //} 90 | 91 | if (!this.opcode.extraCycles) this.wait = 0; 92 | this.opcode = null; 93 | } 94 | } 95 | 96 | 97 | loadOp() { 98 | this.opcode = this.opcodes_[this.mem_.get(++this.PC)]; 99 | this.operand = 0; 100 | let shift = 0; 101 | for (let i = this.opcode.mode.bytes; i > 0; i--) { 102 | this.operand |= this.mem_.get(++this.PC) << ((shift += 8) - 8); 103 | } 104 | if (this.opcode.mode.signed && this.operand > 0x7F) { 105 | this.operand -= 0x100; 106 | } 107 | this.wait += this.opcode.cycles; 108 | const lastPc = hex(this.PC - this.opcode.mode.bytes, 2); 109 | //this.message = `${lastPc}: ${this.opcode.format(this.operand)}`; 110 | // TODO - skip? 111 | this.log(`\n${lastPc}: ${this.opcode.format(this.operand)}`); 112 | } 113 | 114 | 115 | disassemble(addr, count) { 116 | const result = []; 117 | this.PC = --addr; 118 | while (count-- > 0) { 119 | this.loadOp(); 120 | let bytes = '\t'; 121 | while (addr < this.PC) { 122 | bytes += hex(this.mem_.get(++addr)).substring(1) + ' '; 123 | } 124 | // TODO(sdh): this.message no longer used... 125 | //result.push(this.message + bytes); 126 | this.log(bytes); 127 | } 128 | //console.log(result.join('\n')); 129 | } 130 | 131 | 132 | get SR() { 133 | let sr = 0; 134 | sr |= this.S ? 0x80 : 0; 135 | sr |= this.V ? 0x40 : 0; 136 | sr |= this.B ? 0x10 : 0; 137 | sr |= this.D ? 0x08 : 0; 138 | sr |= this.I ? 0x04 : 0; 139 | sr |= this.Z ? 0x02 : 0; 140 | sr |= this.C ? 0x01 : 0; 141 | return sr; 142 | } 143 | 144 | set SR(sr) { 145 | this.S = !!(sr & 0x80); 146 | this.V = !!(sr & 0x40); 147 | this.B = !!(sr & 0x10); 148 | this.D = !!(sr & 0x08); 149 | this.I = !!(sr & 0x04); 150 | this.Z = !!(sr & 0x02); 151 | this.C = !!(sr & 0x01); 152 | } 153 | 154 | accumulator() { return null; } 155 | absolute() { return this.operand; } 156 | absoluteX() { return this.checkCross_(this.operand, this.X); } 157 | absoluteY() { return this.checkCross_(this.operand, this.Y); } 158 | immediate() { return ~this.operand; } 159 | implied() { throw new Error('Implied opcode has no operand.'); } 160 | indirect() { return this.mem_.getWord(this.operand, 0xfff); } 161 | indirectX() { return this.mem_.getWord(0xff & (this.operand + this.X), 0xff); } 162 | indirectY() { return this.checkCross_(this.mem_.getWord(this.operand), this.Y); } 163 | relative() { return this.PC + this.operand; } 164 | zeroPage() { return this.operand & 0xff; } 165 | zeroPageX() { return (this.operand + this.X) & 0xff } 166 | zeroPageY() { return (this.operand + this.Y) & 0xff } 167 | 168 | // ILLEGAL OPCODES 169 | // Kill 170 | KIL() { throw new Error('Illegal opcode: ' + this.opcode.name); } 171 | 172 | // LOAD AND STORE 173 | // Load Accumulator with Memory: M -> A 174 | LDA() { this.A = this.eqFlags_(this.M); } 175 | // Load Index X with Memory: M -> X 176 | LDX() { this.X = this.eqFlags_(this.M); } 177 | // Load Index Y with Memory: M -> Y 178 | LDY() { this.Y = this.eqFlags_(this.M); } 179 | // Store Accumulator in Memory: A -> M 180 | STA() { this.M = this.A; } 181 | // Store Index X in Memory: X -> M 182 | STX() { this.M = this.X; } 183 | // Store Index Y in Memory: Y -> M 184 | STY() { this.M = this.Y; } 185 | 186 | // ARITHMETIC 187 | // Add Memory to Accumulator with Carry: A + M + C -> A 188 | ADC() { 189 | if (this.D) throw new Error('BCD not supported!'); 190 | const x = this.M; 191 | const sum = this.A + x + this.C; 192 | const as = this.A & 0x80; 193 | const xs = x & 0x80; 194 | const ss = sum & 0x80; 195 | this.V = (as == xs && as != ss) || (this.C && sum == 0x80); 196 | this.C = sum > 0xff; 197 | this.A = this.eqFlags_(sum & 0xff); 198 | } 199 | // Subtract Memory from Accumulator with Borrow: A - M - ~C -> A 200 | SBC() { 201 | if (this.D) throw new Error('BCD not supported!'); 202 | const x = this.M; 203 | const diff = this.A - x - !this.C; 204 | const as = this.A & 0x80; 205 | const xs = x & 0x80; 206 | const ds = diff & 0x80; 207 | this.V = (as != xs && as != ds) || (!this.C && diff == -129); 208 | this.C = diff < 0; 209 | this.A = this.eqFlags_(diff & 0xff); 210 | } 211 | 212 | // INCREMENT AND DECREMENT 213 | // Increment Memory by One: M + 1 -> M 214 | INC() { this.eqFlags_(++this.M); } 215 | // Increment Index X by One: X + 1 -> X 216 | INX() { this.eqFlags_(++this.X); } 217 | // Increment Index Y by One: Y + 1 -> Y 218 | INY() { this.eqFlags_(++this.Y); } 219 | // Decrement Memory by One: M - 1 -> M 220 | DEC() { this.eqFlags_(--this.M); } 221 | // Decrement Index X by One: X - 1 -> X 222 | DEX() { this.eqFlags_(--this.X); } 223 | // Decrement Index Y by One: Y - 1 -> Y 224 | DEY() { this.eqFlags_(--this.Y); } 225 | 226 | // SHIFT AND ROTATE 227 | // Arithmetic Shift Left One Bit: C <- 76543210 <- 0 228 | ASL() { 229 | const shift = this.M << 1; 230 | this.C = shift > 0xff; 231 | this.M = this.eqFlags_(shift & 0xff); 232 | } 233 | // Logical Shift Right One Bit: 0 -> 76543210 -> C 234 | LSR() { 235 | const value = this.M; 236 | this.C = value & 1; 237 | this.M = this.eqFlags_(value >>> 1); 238 | } 239 | // Rotate Left One Bit: C <- 76543210 <- C 240 | ROL() { 241 | const shift = (this.M << 1) | this.C 242 | this.C = shift > 0xff; 243 | this.M = this.eqFlags_(shift & 0xff); 244 | } 245 | // Rotate Right One Bit: C -> 76543210 -> C 246 | ROR() { 247 | const value = this.M | (this.C ? 0x100 : 0); 248 | this.C = value & 1; 249 | this.M = this.eqFlags_(value >>> 1); 250 | } 251 | 252 | // LOGIC 253 | // AND Memory with Accumulator: A & M -> A 254 | AND() { this.A = this.eqFlags_(this.M & this.A); } 255 | // OR Memory with Accumulator: A | M -> A 256 | ORA() { this.A = this.eqFlags_(this.M | this.A); } 257 | // Exclusive-OR Memory with Accumulator: A ^ M -> A 258 | EOR() { this.A = this.eqFlags_(this.M ^ this.A); } 259 | 260 | // COMPARE AND TEST BIT 261 | // Compare Memory and Accumulator: A - M 262 | CMP() { this.cmpFlags_(this.A, this.M); } 263 | // Compare Memory and Index X: X - M 264 | CPX() { this.cmpFlags_(this.X, this.M); } 265 | // Compare Memory and Index Y: Y - M 266 | CPY() { this.cmpFlags_(this.Y, this.M); } 267 | // Test Bits in Memory with Accumulator: A & M 268 | BIT() { 269 | const value = this.M; 270 | this.S = value & 0x80; 271 | this.V = value & 0x40; 272 | this.Z = !(this.A & value); // TODO(sdh): is the ! correct here?!? 273 | } 274 | 275 | // BRANCH -- TODO(sdh): how to not add 1 cycle if no branch? 276 | // Branch on Carry Clear 277 | BCC() { if (!this.C) this.PC = this.checkBranch_(this.MP); } 278 | // Branch on Carry Set 279 | BCS() { if (this.C) this.PC = this.checkBranch_(this.MP); } 280 | // Branch on Result Zero 281 | BEQ() { if (this.Z) this.PC = this.checkBranch_(this.MP); } 282 | // Branch on Result Not Zero 283 | BNE() { if (!this.Z) this.PC = this.checkBranch_(this.MP); } 284 | // Branch on Result Minus 285 | BMI() { if (this.S) this.PC = this.checkBranch_(this.MP); } 286 | // Branch on Result Plus 287 | BPL() { if (!this.S) this.PC = this.checkBranch_(this.MP); } 288 | // Branch on Overflow Clear 289 | BVC() { if (!this.V) this.PC = this.checkBranch_(this.MP); } 290 | // Branch on Overflow Set 291 | BVS() { if (this.V) this.PC = this.checkBranch_(this.MP); } 292 | 293 | // TRANSFER 294 | // Transfer Accumulator to Index X: A -> X 295 | TAX() { this.X = this.eqFlags_(this.A); } 296 | // Transfer Index X to Accumulator: X -> A 297 | TXA() { this.A = this.eqFlags_(this.X); } 298 | // Transfer Accumulator to Index Y: A -> Y 299 | TAY() { this.Y = this.eqFlags_(this.A); } 300 | // Transfer Index Y to Accumulator: Y -> A 301 | TYA() { this.A = this.eqFlags_(this.Y); } 302 | // Transfer Stack Pointer to Index X: SP -> X 303 | TSX() { this.X = this.eqFlags_(this.SP); } 304 | // Transfer Index X to Stack Pointer: X -> SP 305 | TXS() { this.SP = this.eqFlags_(this.X); } 306 | 307 | // STACK 308 | // Push Accumulator on Stack: A -> (SP) 309 | PHA() { this.pushByte(this.A); } 310 | // Pull Accumulator from Stack: (SP) -> A 311 | PLA() { this.A = this.pullByte(); } 312 | // Push Processor Status on Stack: SR -> (SP) 313 | PHP() { this.pushByte(this.SR); } 314 | // Pull Processor Status from Stack: (SP) -> SR 315 | PLP() { this.SR = this.pullByte(); } 316 | 317 | // SUBROUTINES AND JUMP 318 | // Jump to New Location 319 | JMP() { this.PC = this.MP - 1; } 320 | // Jump to New Location Saving Return Address 321 | JSR() { 322 | // TODO(!!!): nail down the pc++ nuances 323 | // - specifically, during execution, PC is 1 before the next instr to run 324 | // - so jump addresses are actually instruction minus 1 325 | this.pushWord(this.PC); 326 | this.PC = this.MP - 1; 327 | } 328 | // Return from Subroutine 329 | RTS() { this.PC = this.pullWord(); 330 | this.line(() => 'PC=' + hex(this.PC)); 331 | } 332 | // Return from Interrupt 333 | RTI() { 334 | this.SR = this.pullByte(); 335 | // NOTE: INTERRUPTS GO AFTER ++PC, so we need to un-add 336 | this.PC = this.pullWord() - 1; 337 | } 338 | 339 | // SET AND CLEAR 340 | // Set Carry Flag: 1 -> C 341 | SEC() { this.C = 1; } 342 | // Set Decimal Mode: 1 -> D 343 | SED() { this.D = 1; } 344 | // Set Interrupt Disable Status: 1 -> I 345 | SEI() { this.I = 1; } 346 | // Clear Carry Flag: 0 -> C 347 | CLC() { this.C = 0; } 348 | // Clear Decimal Mode: 0 -> D 349 | CLD() { this.D = 0; } 350 | // Clear Interrupt Disable Status: 0 -> I 351 | CLI() { this.I = 0; } 352 | // Clear Overflow Flag: 0 -> V 353 | CLV() { this.V = 0; } 354 | 355 | // MISC 356 | // No Operation 357 | NOP() {} 358 | // Break: 1 -> B, 1 -> I 359 | BRK() { 360 | this.B = this.I = 1; 361 | 362 | if (!this.mem_.getWord(this.PC) && !this.mem_.getWord(this.PC + 2)) { 363 | throw new Error('Executing zeros!'); 364 | } 365 | 366 | } 367 | 368 | 369 | get MP() { 370 | const addr = this.opcode.mode.func.call(this); 371 | if (addr == null || addr < 0) throw new Error('Jump to non-address.'); 372 | return addr; 373 | } 374 | 375 | // get MM() { 376 | // return this.mem_.getWord(this.MP); 377 | // } 378 | 379 | get M() { 380 | const addr = this.opcode.mode.func.call(this); 381 | if (addr == null) return this.A; 382 | if (addr < 0) return ~addr; 383 | return this.mem_.get(addr); 384 | } 385 | 386 | set M(value) { 387 | const addr = this.opcode.mode.func.call(this); 388 | if (addr < 0) throw new Error('Cannot write to immediate value.'); 389 | if (addr == null) { 390 | this.A = value; 391 | this.line(() => 'A=' + hex(value)); 392 | } else { 393 | this.mem_.set(addr, value); 394 | this.line(() => '(' + hex(addr, 2) + ')=' + hex(value)); 395 | } 396 | } 397 | 398 | /** @param {number} value A one-byte integer. */ 399 | pushByte(value) { 400 | this.mem_.set(this.SP--, value); 401 | this.line(() => `(SP)=${hex(value)}, SP=${hex(this.SP,2)}`); 402 | } 403 | 404 | /** @param {number} value A two-byte integer. */ 405 | pushWord(value) { 406 | this.mem_.setWord(this.SP - 1, value); 407 | this.SP -= 2; 408 | this.line(() => `(SP)=${hex(value, 2)}, SP=${hex(this.SP,2)}`); 409 | } 410 | 411 | /** @return {number} */ 412 | pullByte(value) { 413 | const result = this.mem_.get(++this.SP); 414 | this.line(() => `${hex(result)}<-(SP), SP=${hex(this.SP,2)}`); 415 | return result; 416 | } 417 | 418 | /** @return {number} */ 419 | pullWord(value) { 420 | const result = this.mem_.getWord((this.SP += 2) - 1); 421 | this.line(() => `${hex(result, 2)}<-(SP), SP=${hex(this.SP,2)}`); 422 | return result; 423 | } 424 | 425 | /** 426 | * Sets the sign and zero flags based on the number. 427 | * @param {number} arg 428 | * @return {number} The argument, for chaining. 429 | * @private 430 | */ 431 | eqFlags_(arg) { 432 | this.S = arg & 0x80; 433 | this.Z = !arg; 434 | return arg; 435 | } 436 | 437 | /** 438 | * Compare register to memory. 439 | * @param {number} reg 440 | * @param {number} mem 441 | * @private 442 | */ 443 | cmpFlags_(reg, mem) { 444 | this.C = !(this.S = reg < mem); 445 | this.Z = reg == mem; 446 | this.line(() => `R=${hex(reg)}, M=${hex(mem)}, C=${this.C?1:0}, S=${this.S?1:0}, Z=${this.Z?1:0}`); 447 | } 448 | 449 | /** 450 | * Check if a jump occured on the same or different page, 451 | * and set extra cycles accordingly. 452 | * @param {number} addr 453 | * @return {number} The input address. 454 | * @private 455 | */ 456 | checkBranch_(addr) { 457 | this.line(() => `PC=${hex(this.PC, 2)}->${hex(addr, 2)}`); 458 | this.wait = ((this.PC & 0xf000) == (addr & 0xf000)) ? 1 : 2; 459 | return addr; 460 | } 461 | 462 | /** 463 | * Adds addresses and sets extra wait if there's a page crossing. 464 | * @param {number} addr A 16-bit address. 465 | * @param {number} index An 8-bit index register. 466 | * @return {number} The sum 467 | */ 468 | checkCross_(addr, index) { 469 | const sum = addr + index; 470 | if (sum & 0xff00 != addr & 0xff00) { 471 | this.wait += 1; 472 | } 473 | return sum; 474 | } 475 | } 476 | 477 | 478 | /** An addressing mode. */ 479 | class AddressingMode { 480 | /** 481 | * @param {string} fmt 482 | * @param {function(!Cpu): ?number} func 483 | */ 484 | constructor(fmt, func) { 485 | /** @const {string} */ 486 | this.fmt = fmt; 487 | /** @const {number} */ 488 | this.bytes = /\$\$/.test(fmt) ? 2 : /\$/.test(fmt) ? 1 : 0; 489 | /** @const {function(!Cpu): ?number} */ 490 | this.func = func; 491 | /** @const {boolean} */ 492 | this.signed = /\+\$/.test(fmt); 493 | const before = fmt.replace(/\+?\$\$?.*/, ''); 494 | const after = fmt.replace(/.*\+?\$\$?/, ''); 495 | this.format = 496 | !this.bytes ? 497 | arg => fmt : 498 | arg => { 499 | return `${before}${hex(arg, this.bytes, this.signed)}${after}` 500 | }; 501 | } 502 | } 503 | 504 | 505 | /** An opcode, with an addressing mode. */ 506 | class Opcode { 507 | /** 508 | * @param {string} name 509 | * @param {!AddressingMode} mode 510 | * @param {function(this: Cpu)} op 511 | * @param {number} cycles 512 | * @param {boolean} extraCycles 513 | */ 514 | constructor(name, mode, op, cycles, extraCycles) { 515 | /** @const {string} */ 516 | this.name = name; 517 | /** @const {!AddressingMode} */ 518 | this.mode = mode; 519 | /** @const {function(this: Cpu)} */ 520 | this.op = op; 521 | /** @const {number} */ 522 | this.cycles = cycles; 523 | /** @const {boolean} */ 524 | this.extraCycles = extraCycles; 525 | } 526 | 527 | /** 528 | * @param {number} arg 529 | * @return {string} 530 | */ 531 | format(arg) { 532 | return `${this.name} ${this.mode.format(arg)}`; 533 | } 534 | 535 | /** @return {string} */ 536 | toString() { 537 | return `${this.name} ${this.mode.fmt}`; 538 | } 539 | } 540 | 541 | /** 542 | * Builds the instruction table. 543 | * @return {!Array} 544 | */ 545 | function instructionTable() { 546 | const modes = {}; 547 | const ops = {}; 548 | 549 | function op(name, func) { 550 | ops[name] = func; 551 | } 552 | function mode(fmt, func) { 553 | modes[fmt] = new AddressingMode(fmt, func); 554 | } 555 | 556 | // Standard Opcodes 557 | op('LDA', Cpu.prototype.LDA); 558 | op('LDX', Cpu.prototype.LDX); 559 | op('LDY', Cpu.prototype.LDY); 560 | op('STA', Cpu.prototype.STA); 561 | op('STX', Cpu.prototype.STX); 562 | op('STY', Cpu.prototype.STY); 563 | op('ADC', Cpu.prototype.ADC); 564 | op('SBC', Cpu.prototype.SBC); 565 | op('INC', Cpu.prototype.INC); 566 | op('INX', Cpu.prototype.INX); 567 | op('INY', Cpu.prototype.INY); 568 | op('DEC', Cpu.prototype.DEC); 569 | op('DEX', Cpu.prototype.DEX); 570 | op('DEY', Cpu.prototype.DEY); 571 | op('ASL', Cpu.prototype.ASL); 572 | op('LSR', Cpu.prototype.LSR); 573 | op('ROL', Cpu.prototype.ROL); 574 | op('ROR', Cpu.prototype.ROR); 575 | op('AND', Cpu.prototype.AND); 576 | op('ORA', Cpu.prototype.ORA); 577 | op('EOR', Cpu.prototype.EOR); 578 | op('CMP', Cpu.prototype.CMP); 579 | op('CPX', Cpu.prototype.CPX); 580 | op('CPY', Cpu.prototype.CPY); 581 | op('BIT', Cpu.prototype.BIT); 582 | op('BCC', Cpu.prototype.BCC); 583 | op('BCS', Cpu.prototype.BCS); 584 | op('BEQ', Cpu.prototype.BEQ); 585 | op('BMI', Cpu.prototype.BMI); 586 | op('BNE', Cpu.prototype.BNE); 587 | op('BPL', Cpu.prototype.BPL); 588 | op('BVC', Cpu.prototype.BVC); 589 | op('BVS', Cpu.prototype.BVS); 590 | op('TAX', Cpu.prototype.TAX); 591 | op('TXA', Cpu.prototype.TXA); 592 | op('TAY', Cpu.prototype.TAY); 593 | op('TYA', Cpu.prototype.TYA); 594 | op('TSX', Cpu.prototype.TSX); 595 | op('TXS', Cpu.prototype.TXS); 596 | op('PHA', Cpu.prototype.PHA); 597 | op('PLA', Cpu.prototype.PLA); 598 | op('PHP', Cpu.prototype.PHP); 599 | op('PLP', Cpu.prototype.PLP); 600 | op('JMP', Cpu.prototype.JMP); 601 | op('JSR', Cpu.prototype.JSR); 602 | op('RTS', Cpu.prototype.RTS); 603 | op('RTI', Cpu.prototype.RTI); 604 | op('SEC', Cpu.prototype.SEC); 605 | op('SED', Cpu.prototype.SED); 606 | op('SEI', Cpu.prototype.SEI); 607 | op('CLC', Cpu.prototype.CLC); 608 | op('CLD', Cpu.prototype.CLD); 609 | op('CLI', Cpu.prototype.CLI); 610 | op('CLV', Cpu.prototype.CLV); 611 | op('NOP', Cpu.prototype.NOP); 612 | op('BRK', Cpu.prototype.BRK); 613 | // Illegal Opcodes 614 | function combo(a, b) { 615 | if (!a || !b) throw new Error('bad reference'); 616 | return function() { a.call(this); b.call(this); }; 617 | } 618 | op('KIL', Cpu.prototype.KIL); 619 | op('SLO', combo(Cpu.prototype.ASL, Cpu.prototype.ORA)); 620 | op('XAA', combo(Cpu.prototype.TXA, Cpu.prototype.AND)); 621 | op('RLA', combo(Cpu.prototype.ROL, Cpu.prototype.AND)); 622 | 623 | // Addressing Modes 624 | mode('A', Cpu.prototype.accumulator); 625 | mode('$$', Cpu.prototype.absolute); 626 | mode('$$,X', Cpu.prototype.absoluteX); 627 | mode('$$,Y', Cpu.prototype.absoluteY); 628 | mode('#$', Cpu.prototype.immediate); 629 | mode('', Cpu.prototype.implied); 630 | mode('($$)', Cpu.prototype.indirect); 631 | mode('($,X)', Cpu.prototype.indirectX); 632 | mode('($),Y', Cpu.prototype.indirectY); 633 | mode('+$', Cpu.prototype.relative); 634 | mode('$', Cpu.prototype.zeroPage); 635 | mode('$,X', Cpu.prototype.zeroPageX); 636 | mode('$,Y', Cpu.prototype.zeroPageY); 637 | 638 | const data = ` 639 | 00: BRK 7 | ORA ($,X) 6 | KIL! | SLO! ($,X) 8 640 | 04: NOP! $ 3 | ORA $ 3 | ASL $ 5 | SLO! $ 5 641 | 08: PHP 3 | ORA #$ 2 | ASL A 2 | ANC! #$ 2 642 | 0C: NOP! $$ 4 | ORA $$ 4 | ASL $$ 6 | SLO! $$ 6 643 | 644 | 10: BPL +$ 2+ | ORA ($),Y 5+ | KIL! | SLO! ($),Y 8 645 | 14: NOP! $,X 4 | ORA $,X 4 | ASL $,X 6 | SLO! $,X 6 646 | 18: CLC 2 | ORA $$,Y 4+ | NOP! A 2 | SLO! $$,Y 7 647 | 1C: NOP! $$,X 4+ | ORA $$,X 4+ | ASL $$,X 7 | SLO! $$,X 7 648 | 649 | 20: JSR $$ 6 | AND ($,X) 6 | KIL! | RLA! ($,X) 8 650 | 24: BIT $ 3 | AND $ 3 | ROL $ 5 | RLA! $ 5 651 | 28: PLP 4 | AND #$ 2 | ROL A 2 | ANC! #$ 2 652 | 2C: BIT $$ 4 | AND $$ 4 | ROL $$ 6 | RLA! $$ 6 653 | 654 | 30: BMI +$ 2+ | AND ($),Y 5+ | KIL! | RLA! ($),Y 8 655 | 34: NOP! $,X 4 | AND $,X 4 | ROL $,X 6 | RLA! $,X 6 656 | 38: SEC 2 | AND $$,Y 4+ | NOP! A 2 | RLA! $$,Y 7 657 | 3C: NOP $$,X 4+ | AND $$,X 4+ | ROL $$,X 7 | RLA! $$,X 7 658 | 659 | 40: RTI 6 | EOR ($,X) 6 | KIL! | SRE! ($,X) 8 660 | 44: NOP! $ 3 | EOR $ 3 | LSR $ 5 | SRE! $ 5 661 | 48: PHA 3 | EOR #$ 2 | LSR A 2 | ALR! #$ 2 662 | 4C: JMP $$ 3 | EOR $$ 4 | LSR $$ 6 | SRE! $$ 6 663 | 664 | 50: BVC +$ 2+ | EOR ($),Y 5+ | KIL! | SRE! ($),Y 8 665 | 54: NOP! $,X 4 | EOR $,X 4 | LSR $,X 6 | SRE! $,X 6 666 | 58: CLI 2 | EOR $$,Y 4+ | NOP! A 2 | SRE! $$,Y 7 667 | 5C: NOP! $$,X 4+ | EOR $$,X 4+ | LSR $$,X 7 | SRE! $$,X 7 668 | 669 | 60: RTS 6 | ADC ($,X) 6 | KIL! | RRA! ($,X) 8 670 | 64: NOP! $ 3 | ADC $ 3 | ROR $ 5 | RRA! $ 5 671 | 68: PLA 4 | ADC #$ 2 | ROR A 2 | ARR! #$ 2 672 | 6C: JMP ($$) 5 | ADC $$ 4 | ROR $$ 6 | RRA! $$ 6 673 | 674 | 70: BVS +$ 2+ | ADC ($),Y 5+ | KIL! | RRA! ($),Y 8 675 | 74: NOP! $,X 4 | ADC $,X 4 | ROR $,X 6 | RRA! $,X 6 676 | 78: SEI 2 | ADC $$,Y 4+ | NOP! A 2 | RRA! $$,Y 7 677 | 7C: NOP! $$,X 4+ | ADC $$,X 4+ | ROR $$,X 7 | RRA! $$,X 7 678 | 679 | 80: NOP! #$ 2 | STA ($,X) 6 | NOP! #$ 2 | SAX! ($,X) 6 680 | 84: STY $ 3 | STA $ 3 | STX $ 3 | SAX! $ 3 681 | 88: DEY 2 | NOP! #$ 2 | TXA 2 | XAA!! #$ 2 682 | 8C: STY $$ 4 | STA $$ 4 | STX $$ 4 | SAX! $$ 4 683 | 684 | 90: BCC +$ 2+ | STA ($),Y 6 | KIL! | AHX!! ($),Y 6 685 | 94: STY $,X 4 | STA $,X 4 | STX $,Y 4 | SAX! $,Y 4 686 | 98: TYA 2 | STA $$,Y 5 | TXS 2 | TAS!! $$,Y 5 687 | 9C: SHY!! $$,X 5 | STA $$,X 5 | SHX!! $$,Y 5 | AHX!! $$,Y 5 688 | 689 | A0: LDY #$ 2 | LDA ($,X) 6 | LDX #$ 2 | LAX! ($,X) 6 690 | A4: LDY $ 3 | LDA $ 3 | LDX $ 3 | LAX! $ 3 691 | A8: TAY 2 | LDA #$ 2 | TAX 2 | LAX!! #$ 2 692 | AC: LDY $$ 4 | LDA $$ 4 | LDX $$ 4 | LAX! $$ 4 693 | 694 | B0: BCS +$ 2+ | LDA ($),Y 5+ | KIL! | LAX! ($),Y 5+ 695 | B4: LDY $,X 4 | LDA $,X 4 | LDX $,Y 4 | LAX! $,Y 4 696 | B8: CLV 2 | LDA $$,Y 4+ | TSX 2 | LAS! $$,Y 4+ 697 | BC: LDY $$,X 4+ | LDA $$,X 4+ | LDX $$,Y 4+ | LAX! $$,Y 4+ 698 | 699 | C0: CPY #$ 2 | CMP ($,X) 6 | NOP! #$ 2 | DCP! ($,X) 8 700 | C4: CPY $ 3 | CMP $ 3 | DEC $ 5 | DCP! $ 5 701 | C8: INY 2 | CMP #$ 2 | DEX 2 | AXS! #$ 2 702 | CC: CPY $$ 4 | CMP $$ 4 | DEC $$ 6 | DCP! $$ 6 703 | 704 | D0: BNE +$ 2+ | CMP ($),Y 5+ | KIL! | DCP! ($),Y 8 705 | D4: NOP! $,X 4 | CMP $,X 4 | DEC $,X 6 | DCP! $,X 6 706 | D8: CLD 2 | CMP $$,Y 4+ | NOP! 2 | DCP! $$,Y 7 707 | DC: NOP! $$,X 4+ | CMP $$,X 4+ | DEC $$,X 7 | DCP! $$,X 7 708 | 709 | E0: CPX #$ 2 | SBC ($,X) 6 | NOP! #$ 2 | ISC! ($,X) 8 710 | E4: CPX $ 3 | SBC $ 3 | INC $ 5 | ISC! $ 5 711 | E8: INX 2 | SBC #$ 2 | NOP 2 | SBC! #$ 2 712 | EC: CPX $$ 4 | SBC $$ 4 | INC $$ 6 | ISC! $$ 6 713 | 714 | F0: BEQ +$ 2+ | SBC ($),Y 5+ | KIL! | ISC! ($),Y 8 715 | F4: NOP! $,X 4 | SBC $,X 4 | INC $,X 6 | ISC! $,X 6 716 | F8: SED 2 | SBC $$,Y 4+ | NOP! 2 | ISC! $$,Y 7 717 | FC: NOP! $$,X 4+ | SBC $$,X 4+ | INC $$,X 7 | ISC! $$,X 7`; 718 | 719 | function buildOpcode(str) { 720 | //console.log('opcode: [' + str + ']'); 721 | const split = str.split(' '); 722 | const mode = modes[split.length > 2 ? split[1] : '']; 723 | if (!mode) throw new Error('Unknown mode: ' + str); 724 | const cycles = split.length > 1 ? split[split.length - 1] : '1'; 725 | let illegal = false; 726 | if (split[0][3] == '!') { 727 | split[0] = split[0].replace('!', ''); 728 | split[0] = split[0].replace('!', ''); // some have two 729 | illegal = true; 730 | } 731 | let op = ops[split[0]]; 732 | if (!op) { 733 | if (illegal) { 734 | op = ops['KIL']; 735 | } else { 736 | throw new Error('Unknown op: ' + str); 737 | } 738 | } 739 | // check for + in cycles, then check for relative addressing 740 | return new Opcode(split[0], mode, op, 741 | Number.parseInt(cycles.replace('+', ''), 10), 742 | /\+/.test(cycles)); 743 | } 744 | 745 | const opcodes = data.split('\n') 746 | .map(line => line.replace(/^\s*[0-9A-F]{2}:\s*/, '').split('|')) 747 | .reduce((x, y) => x.concat(y), []) 748 | .map(s => s.trim()) 749 | .filter(s => s) 750 | .map(buildOpcode); 751 | if (opcodes.length != 256) { 752 | throw new Exception('wrong number of opcodes: ' + opcodes.join('|')); 753 | } 754 | return opcodes; 755 | } 756 | 757 | function hex(num, bytes = 1, signed = false) { 758 | if (num == null) { 759 | window.msg = true; 760 | setTimeout(() => { throw 'NULL number'; }, 0); 761 | return 'NUL'; 762 | } 763 | let sign = ''; 764 | if (signed) { 765 | sign = '+'; 766 | if (num < 0) { 767 | sign = '-'; 768 | num = -num; 769 | } 770 | } 771 | let str = num.toString(16).toUpperCase(); 772 | while (str.length < bytes * 2) { 773 | str = '0000'.substring(0, bytes * 2 - str.length) + str; 774 | } 775 | return sign + '$' + str; 776 | } 777 | -------------------------------------------------------------------------------- /deps.pl: -------------------------------------------------------------------------------- 1 | #!/usr/bin/perl 2 | 3 | # Dependency and makefile management. 4 | # Usage: ./deps.pl FIND_ARGS... 5 | # Result: Generates a makefile based on in-file markup. 6 | 7 | # Input files: 8 | # foo.js: 9 | # //@ foo.compiled.js --checkTypes=warning 10 | # import Whatever from './bar'; 11 | # bar.js: 12 | # import Something from './baz'; 13 | # 14 | # Performs a toposort, collecting all deps, and generates deps.d: 15 | # 16 | # foo.compiled.js: foo.js bar.js deps.d 17 | # $(JSCOMP) ... --js=foo.js --js=bar.js --js_output_file=foo.compiled.js --checkTypes=warning 18 | # deps.d: foo.js bar.js 19 | # 20 | # This should be included in the top-level Makefile as: 21 | # 22 | # .PHONY: all 23 | # all: foo.compiled.js 24 | # 25 | # deps.d: 26 | # ./deps.pl 27 | 28 | use strict; 29 | use warnings; 30 | 31 | use File::Basename qw/dirname/; 32 | 33 | # Find all files recursively from current dir 34 | open FIND, "find . -name '*.js' @ARGV | sed 's+^./++' |"; 35 | my @files = ; 36 | close FIND; 37 | map {chomp $_} @files; 38 | 39 | # Scan all the files 40 | my %generated = (); 41 | my %deps = (); 42 | foreach my $file (@files) { 43 | $deps{$file} = ''; 44 | open JS, $file; 45 | while () { 46 | if (m|//@\s*(\S+)\s+(.*)|) { 47 | $generated{$1} = "$file $2"; 48 | } elsif (m|import .* from '(\.\.?/[^']+)'|) { 49 | my $dep = $1; 50 | my $dir = dirname $file; 51 | while ($dep =~ s|^\./|| or $dep =~ s|^\.\./||) { 52 | $dir = dirname $dir if $& eq '../'; 53 | } 54 | $dep = "$dir/$dep.js"; 55 | $dep =~ s|^(?:\./)+||; 56 | $deps{$file} .= " $dep"; 57 | } 58 | } 59 | close JS; 60 | } 61 | 62 | my $makefile = ''; 63 | 64 | # Now build up the makefile. 65 | foreach my $out (sort(keys(%generated))) { 66 | delete $deps{$out}; 67 | # Find all the deps 68 | my ($file, $flags) = split / /, $generated{$out}, 2; 69 | my %d = ($file => 1); 70 | my @q = ($file); 71 | while (@q) { 72 | my $cur = shift @q; 73 | foreach (split / /, ($deps{$cur} or '')) { 74 | next unless $_; 75 | next if defined $d{$_}; 76 | $d{$_} = 1; 77 | push @q, $_; 78 | } 79 | } 80 | # Generate the makefile line 81 | my $srcmap = $out; 82 | $srcmap =~ s/\.js$//; 83 | $srcmap .= ".srcmap"; 84 | my @deps = sort(keys(%d)); 85 | my $header = "$out: @deps deps.d"; 86 | my $cmd = "{ \$(JSCOMP) $flags"; 87 | foreach (@deps) { $cmd .= " --js=$_"; } 88 | $cmd .= " --create_source_map=$srcmap; echo '//# sourceMappingURL=$srcmap'; } >| $out"; 89 | $makefile .= "$header\n\t$cmd\n\n"; 90 | } 91 | 92 | my @srcs = sort(keys(%deps)); 93 | $makefile .= "deps.d: @srcs\n"; 94 | 95 | # Now read the existing file if it's there and only update if changed. 96 | if (open DEPS, "deps.d") { 97 | $/ = undef; 98 | my $prev = ; 99 | close DEPS; 100 | exit 0 if $prev eq $makefile; 101 | } 102 | 103 | open DEPS, ">deps.d"; 104 | print DEPS $makefile; 105 | close DEPS; 106 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 |
10 | Pulse 1: 11 |
12 | Pulse 2: 13 |
14 | Triangle: 15 |
16 | Noise: 17 |
18 | 19 |
20 |
21 | -------------------------------------------------------------------------------- /mem.js: -------------------------------------------------------------------------------- 1 | import Clock from './clock'; 2 | 3 | /** Manages memory, including various views and listeners. */ 4 | export default class Memory { 5 | /** @param {!Clock=} opt_clock */ 6 | constructor(opt_clock) { 7 | /** @private @const {!Uint8Array} */ 8 | this.data8_ = new Uint8Array(0x10000); 9 | /** @private @const {!Object>} */ 10 | this.callbacks_ = {}; 11 | /** @private @const {!Object} */ 12 | this.registers_ = {}; 13 | 14 | this.log_ = opt_clock ? new MemLogger(opt_clock) : null; 15 | } 16 | 17 | 18 | zero() { 19 | this.data8_.fill(0); 20 | // TODO - clear callbacks? 21 | } 22 | 23 | 24 | /** 25 | * @param {number} addr 26 | * @return {number} 27 | */ 28 | get(addr) { 29 | return this.data8_[addr]; 30 | const reg = this.registers_[addr]; 31 | return reg ? reg.get() : this.data8_[addr]; 32 | } 33 | 34 | 35 | /** 36 | * @param {number} addr 37 | * @param {number=} opt_wrap Page size for wrapping. 38 | * @return {number} 39 | */ 40 | getWord(addr, opt_wrap) { 41 | let next = (addr + 1) & 0xffff; 42 | if (opt_wrap) { 43 | next = (next & opt_wrap) | (addr & ~opt_wrap); 44 | } 45 | return this.data8_[addr] | (this.data8_[next] << 8); 46 | return this.get(addr) | (this.get(next) << 8); 47 | } 48 | 49 | 50 | /** 51 | * @param {number} addr 52 | * @param {number} value 53 | */ 54 | set(addr, value) { 55 | if (addr & 0xfff0 == 0x4000) { 56 | console.log(`($${addr.toString(16)}) <- $${value.toString(16)}`); 57 | } 58 | // const reg = this.registers_[addr]; 59 | // if (reg) reg.set(value); 60 | this.data8_[addr] = value; 61 | this.call_(addr, value); 62 | } 63 | 64 | 65 | /** 66 | * @param {number} addr 67 | * @param {number} value 68 | */ 69 | setWord(addr, value) { 70 | this.set(addr, value & 0xff); 71 | this.set(addr + 1, value >>> 8); 72 | } 73 | 74 | 75 | /** 76 | * Loads a 4k chunk. 77 | * @param {!Uint8Array} data A 4k buffer. 78 | * @param {number} offset The offset to start loading. 79 | */ 80 | load(data, offset) { 81 | this.data8_.set(data, offset); 82 | } 83 | 84 | 85 | /** 86 | * @param {number} addr 87 | * @param {number} value 88 | * @private 89 | */ 90 | call_(addr, value) { 91 | const cbs = this.callbacks_[addr]; 92 | if (cbs) { 93 | for (let cb of cbs) { 94 | cb(value); 95 | } 96 | } 97 | } 98 | 99 | 100 | /** 101 | * @param {number} addr 102 | * @param {function(number)} callback 103 | */ 104 | listen(addr, callback) { 105 | (this.callbacks_[addr] = this.callbacks_[addr] || []).push(callback); 106 | } 107 | 108 | 109 | /** 110 | * @param {number} addr 111 | * @param {!Memory.Register} register 112 | */ 113 | register(addr, register) { 114 | this.registers_[addr] = register; 115 | } 116 | 117 | 118 | /** 119 | * @param {number} addr 120 | * @param {number} shift 121 | * @param {number} length 122 | * @param {string=} opt_name Name for logging. 123 | * @return {!Memory.Register} 124 | */ 125 | int(addr, shift, length, opt_name) { 126 | 127 | // TODO - have these listen for writes and keep a local copy?!? 128 | 129 | if (shift > 8) { 130 | addr += shift >>> 3; 131 | shift = shift & 3; 132 | } 133 | if (shift + length > 16) throw new Error('two bytes max'); 134 | const self = this; 135 | const mask = makeMask(length) << shift; 136 | const getWord = mask > 0xff ? this.getWord : this.get; 137 | const setWord = mask > 0xff ? this.setWord : this.set; 138 | const get = () => (getWord.call(self, addr) & mask) >>> shift; 139 | let value = get(); 140 | const reg = { 141 | get() { 142 | return value; 143 | }, 144 | set(value) { 145 | const word = getWord.call(self, addr); 146 | setWord.call((word & ~mask) | ((value << shift) & mask)); 147 | }, 148 | }; 149 | const cb = opt_name && this.log_ ? 150 | this.log_.add(reg, opt_name) : function() {}; 151 | this.listen(addr, () => { value = get(); cb(value); }); 152 | if (mask > 0xff) this.listen(addr + 1, () => { value = get(); cb(value); }); 153 | return reg; 154 | } 155 | 156 | 157 | /** 158 | * @param {number} addr 159 | * @param {number} bit 160 | * @param {string=} opt_name Name for logging. 161 | * @return {!Memory.Register} 162 | */ 163 | bool(addr, bit, opt_name) { 164 | const mask = 1 << bit; 165 | const self = this; 166 | const get = () => !!(self.get(addr) & mask); 167 | let value = get(); 168 | const reg = { 169 | get() { 170 | return value; 171 | }, 172 | set(value) { 173 | if (value) self.set(addr, self.get(addr) | mask); 174 | else self.set(addr, self.get(addr) & ~mask); 175 | }, 176 | }; 177 | const cb = opt_name && this.log_ ? 178 | this.log_.add(reg, opt_name) : function() {}; 179 | this.listen(addr, () => { value = get(); cb(value); }); 180 | return reg; 181 | } 182 | } 183 | 184 | 185 | /** 186 | * @param {number} length 187 | * @return {number} 188 | */ 189 | function makeMask(length) { 190 | let mask = 0; 191 | while (length--) mask = (mask << 1) | 1; 192 | return mask; 193 | } 194 | 195 | 196 | /** 197 | * @record 198 | * @template T 199 | */ 200 | Memory.Register = class { 201 | /** @return {T} */ 202 | get() {} 203 | /** @param {T} value */ 204 | set(value) {} 205 | }; 206 | 207 | 208 | class MemLogger { 209 | constructor(clock) { 210 | this.clock = clock; 211 | this.names = []; 212 | this.regs = []; 213 | this.changed = []; 214 | this.lastTime = Infinity; 215 | this.log = document.getElementById('memlog'); 216 | } 217 | 218 | /** 219 | * @param {!Memory.Register} reg 220 | * @param {string} name 221 | * @return {function(number)} Callback 222 | */ 223 | add(reg, name) { 224 | if (!this.log) return function() {}; 225 | this.regs.push(reg); 226 | this.names.push(name); 227 | return this.callback.bind(this, this.regs.length - 1); 228 | } 229 | 230 | callback(i, val) { 231 | var time = this.clock.time; 232 | if ((i in this.changed && this.changed[i] != val) || 233 | time - this.lastTime > 0.01) { 234 | this.emit(); 235 | } 236 | this.changed[i] = val; 237 | this.lastTime = time; 238 | } 239 | 240 | emit() { 241 | const pieces = [String(this.lastTime).substring(0, 6) + 's']; 242 | for (var i = 0; i < this.regs.length; i++) { 243 | pieces.push(this.names[i] + ': ' + Number(this.regs[i].get())); 244 | } 245 | this.log.textContent += pieces.join('\t') + '\n'; 246 | this.changed = []; 247 | } 248 | } 249 | -------------------------------------------------------------------------------- /nsf.js: -------------------------------------------------------------------------------- 1 | import BankSwitcher from './bankswitcher'; 2 | import Clock from './clock'; 3 | import Cpu from './cpu'; 4 | import Memory from './mem'; 5 | 6 | function getString(view, start, length) { 7 | const bytes = []; 8 | for (let i = 0; i < length; i++) { 9 | const byte = view.getUint8(start + i); 10 | if (!byte) break; 11 | bytes.push(byte); 12 | } 13 | return String.fromCharCode(...bytes); 14 | } 15 | 16 | function readBits(value, ...names) { 17 | const out = []; 18 | while (value > 0) { 19 | if (value & 1) out.push(names[0] || 'ILLEGAL'); 20 | names.shift(); 21 | value <<= 1; 22 | } 23 | return out; 24 | } 25 | 26 | export default class Nsf { 27 | /** @param {!ArrayBuffer} buf */ 28 | constructor(buf) { 29 | const header = new DataView(buf, 0, 0x80); 30 | // Check magic constant in header 31 | if (getString(header, 0, 5) != 'NESM\x1a') { 32 | throw new Error('Invalid NSF file.'); 33 | } 34 | 35 | this.version_ = header.getUint8(0x5); 36 | this.songCount_ = header.getUint8(0x6); 37 | this.startSong_ = header.getUint8(0x7); 38 | this.loadAddress_ = header.getUint16(0x8, true); 39 | this.initAddress_ = header.getUint16(0xA, true); 40 | this.playAddress_ = header.getUint16(0xC, true); 41 | this.songName_ = getString(header, 0xE, 32); 42 | this.artistName_ = getString(header, 0x2E, 32); 43 | this.copyrightName_ = getString(header, 0x4E, 32); 44 | this.playSpeedNtsc_ = header.getUint16(0x6E, true); 45 | this.bankInits_ = new Uint8Array(buf, 0x70, 0x8); 46 | this.playSpeedPal_ = header.getUint16(0x78, true); 47 | const palNtscBits = header.getUint8(0x7A); 48 | this.palNtsc_ = palNtscBits & 2 ? 'dual' : palNtscBits & 1 ? 'pal' : 'ntsc'; 49 | this.extraSupport_ = readBits(header.getUint8(0x7B), 50 | 'VRC6', 'VRC7', 'FDS', 'MMC5', 51 | 'Namco 163', 'Sunsoft 5B'); 52 | 53 | /** @private @const {!Uint8Array} */ 54 | this.data_ = new Uint8Array(buf, 0x80); 55 | } 56 | 57 | /** @param {!Clock} clock */ 58 | cyclesPerFrame(clock) { 59 | const speed = clock.ntsc ? this.playSpeedNtsc_ : this.playSpeedPal_; 60 | return Math.floor(speed / 1e6 / clock.cycleLength); 61 | } 62 | 63 | /** 64 | * @param {!Cpu} cpu 65 | * @param {!Memory} mem 66 | * @param {?number=} song 67 | * @param {?BankSwitcher=} banks 68 | */ 69 | init(cpu, mem, song = null, banks = null) { 70 | // Load the data 71 | mem.zero(); 72 | cpu.init(); 73 | 74 | if (this.bankInits_.find(i => i)) { 75 | // Bank switching is enabled. 76 | if (!banks) throw new Error('Bank switcher required for this ROM'); 77 | banks.load(this.data_, this.loadAddress_); 78 | const fds = this.extraSupport_.indexOf('FDS') >= 0; 79 | for (let i = 0; i < 8; i++) { 80 | const addr = (i > 5 && fds) ? 0x5ff0 + i : 0x5ff8 + i; 81 | mem.set(addr, this.bankInits_[i]); 82 | } 83 | } else { 84 | // No bank switching, so load directly. 85 | mem.load(this.data_, this.loadAddress_); 86 | } 87 | cpu.pushWord(0xffff); // special signal that we're done... 88 | cpu.pushWord(this.playAddress_ - 1); 89 | cpu.PC = this.initAddress_ - 1; 90 | mem.set(0x4015, 0xf); 91 | mem.set(0x4017, 0x40); 92 | cpu.A = song != null ? song : this.startSong_; 93 | // really, we need the clock... this is getting horribly tangled! 94 | cpu.X = this.palNtsc_ != 'pal' ? 0 : 1; // default to NTSC 95 | } 96 | 97 | frame(cpu) { 98 | cpu.pushWord(0xffff); // special signal that we're done... 99 | cpu.PC = this.playAddress_ - 1; 100 | } 101 | 102 | toString() { 103 | const speed = []; 104 | if (this.palNtsc_ == 'ntsc' || this.palNtsc_ == 'dual') { 105 | speed.push((1e6 / this.playSpeedNtsc_) + ' Hz NTSC'); 106 | } 107 | if (this.palNtsc_ == 'pal' || this.palNtsc_ == 'dual') { 108 | speed.push((1e6 / this.playSpeedPal_) + ' Hz PAL'); 109 | } 110 | return [ 111 | `NSF v${this.version_}`, 112 | `${this.songCount_} songs, starting at ${this.startSong_}`, 113 | `Load: $${this.loadAddress_.toString(16)}`, 114 | `Init: $${this.initAddress_.toString(16)}`, 115 | `Play: $${this.playAddress_.toString(16)}`, 116 | `Song name: ${this.songName_}`, 117 | `Artist: ${this.artistName_}`, 118 | `Copyright: ${this.copyrightName_}`, 119 | `Speed: ${speed.join(' / ')}`, 120 | `Bank Init: ${this.bankInits_.join(' ')}`, 121 | `Extras: ${this.extraSupport_.join(' ')}`, 122 | ].join('\n'); 123 | } 124 | } 125 | -------------------------------------------------------------------------------- /nsfplayer.js: -------------------------------------------------------------------------------- 1 | //@ nes.compiled.js --language_in=ES6_STRICT --language_out=ES5_STRICT --create_source_map=nes.srcmap --jscomp_warning=checkTypes --jscomp_warning=checkVars 2 | 3 | // -O ADVANCED_OPTIMIZATIONS 4 | 5 | import Apu from './apu/apu'; 6 | import BankSwitcher from './bankswitcher'; 7 | import Clock from './clock'; 8 | import Cpu from './cpu'; 9 | import Memory from './mem'; 10 | import Nsf from './nsf'; 11 | import StepBufferWriter from './audio/stepbufferwriter'; 12 | import BufferedAudioNode from './audio/bufferedaudionode'; 13 | 14 | export default class NsfPlayer { 15 | 16 | constructor(ac, nsf) { 17 | this.nsf = nsf; 18 | this.clock = Clock.ntsc(); 19 | this.cyclesPerFrame = nsf.cyclesPerFrame(this.clock); 20 | this.mem = new Memory(this.clock); 21 | this.apu = new Apu(this.mem, this.clock); 22 | this.cpu = new Cpu(this.mem); 23 | this.banks = new BankSwitcher(this.mem); 24 | this.node = new BufferedAudioNode(ac, 2); 25 | this.node.connect(ac.destination); 26 | this.writer = new StepBufferWriter(this.node); 27 | this.promise = null; 28 | 29 | 30 | const log = document.getElementById('log'); 31 | for (let a = 0x4000; a < 0x4018; a++) { 32 | this.mem.listen(a, (function(addr) { 33 | let frame = 'Frame ' + (this.clock.time * 60) + ':'; 34 | if (frame.length < 20) frame = frame + ' '.repeat(20 - frame.length); 35 | if (log) { 36 | log.textContent += frame + ' (' + addr.toString(16) + ') <= ' + 37 | this.mem.get(addr).toString(16) + '\n'; 38 | } 39 | }).bind(this, a)); 40 | } 41 | 42 | } 43 | 44 | start(song = 0) { 45 | const log = document.getElementById('memlog'); 46 | if (log) log.textContent = ''; 47 | this.nsf.init(this.cpu, this.mem, song, this.banks); 48 | this.node.reset(); 49 | this.promise = null; 50 | this.play(null); 51 | } 52 | 53 | play(promise) { 54 | 55 | // TODO - add a check - store assembler logs for first ten (60?) 56 | // frames - if volume is never non-zero after these frames, 57 | // dump the whole log...? 58 | 59 | if (this.promise != promise) return; 60 | if (this.node.bufferTime() == 0) console.log('buffer underrun!'); 61 | // TODO - use i < 100 and requestAnimationFrame to be smoother?!? 62 | for (let i = 0; i < 10; i++) { 63 | for (let frameCycle = this.cyclesPerFrame; frameCycle >= 0; frameCycle--) { 64 | if (frameCycle != frameCycle) throw new Error('NaN'); 65 | if (this.cpu.PC != 0xFFFF) this.cpu.clock(); 66 | this.apu.clock(); 67 | this.clock.tick(); 68 | } 69 | if (this.cpu.PC == 0xFFFF) { 70 | //this.cpu.log('START FRAME') 71 | // console.log('New frame'); 72 | this.nsf.frame(this.cpu); 73 | } else { 74 | this.cpu.log('LONG FRAME'); 75 | } 76 | } 77 | // Yield a single frame worth of steps 78 | promise = this.promise = 79 | this.writer.write(this.apu.steps(), this.clock.time) 80 | //.then(() => this.play(promise)); 81 | // .then(() => { setTimeout(() => this.play(promise), 0); }); 82 | // TODO(sdh): for some reason, requestAnimationFrame (and/or i<100) 83 | // causes a weird glitch on the 2nd and later song...?!? 84 | //.then(() => { requestAnimationFrame(() => this.play(promise)); }); 85 | .then(() => { 86 | setTimeout(() => this.play(promise), 1000 * (this.node.bufferTime()) - 60); 87 | }); 88 | // console.log('Yield data', data); 89 | } 90 | 91 | stop() { 92 | this.promise = null; 93 | } 94 | } 95 | 96 | function readLocalFiles() { 97 | const file = document.getElementById('files').files[0]; 98 | const reader = new FileReader(); 99 | reader.onload = (e => startEmu(e.target.result)); 100 | reader.readAsArrayBuffer(file); 101 | } 102 | 103 | function startEmu(buf) { 104 | const nsf = new Nsf(buf); 105 | new Cpu(new Memory()).log(nsf + ''); 106 | const ac = new AudioContext(); 107 | const player = new NsfPlayer(ac, nsf); 108 | window.PLAYER = player; 109 | 110 | let track = 2; 111 | 112 | player.start(track); 113 | 114 | const stop = document.getElementById('stop'); 115 | const next = document.getElementById('next'); 116 | 117 | stop.style.display = ''; 118 | next.style.display = ''; 119 | 120 | stop.addEventListener('click', () => player.stop()); 121 | next.addEventListener('click', () => player.start(++track)); 122 | 123 | } 124 | 125 | document.getElementById('fetch').addEventListener('click', readLocalFiles); 126 | -------------------------------------------------------------------------------- /scraps/async.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Runs a coroutine. 3 | * @param {function(): (function(): T)} gen A generator function. 4 | * @return {!Promise} The final returned result. 5 | * @template T 6 | */ 7 | 8 | export function run(gen) { 9 | return run_(gen(), void 0); 10 | } 11 | 12 | function run_(iter, last) { 13 | const result = iter.next(last); 14 | if (result.done) return result.value; 15 | return result.value.then(x => run_(iter, x)); 16 | } 17 | -------------------------------------------------------------------------------- /scraps/bitmask.js: -------------------------------------------------------------------------------- 1 | /** 2 | * A bit mask, enables getting and setting. 3 | */ 4 | export default class BitMask { 5 | /** 6 | * @param {number} start 7 | * @param {number} length 8 | */ 9 | constructor(start, length) { 10 | let mask = 0; 11 | while (length--) mask = (mask | 1) << 1; 12 | this.shift = start; 13 | this.mask = mask << start; 14 | } 15 | 16 | 17 | /** 18 | * @param {number} full 19 | * @return {number} 20 | */ 21 | get(full) { 22 | return (full & this.mask) >>> this.shift; 23 | } 24 | 25 | 26 | /** 27 | * @param {number} full 28 | * @return {boolean} 29 | */ 30 | check(full) { 31 | return !!(full & this.mask); 32 | } 33 | 34 | 35 | /** 36 | * @param {number} full 37 | * @param {number} word 38 | * @return {number} 39 | */ 40 | set(full, word) { 41 | word = (word << this.shift) & this.mask; 42 | return (full & ~this.mask) | word; 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /scraps/cpu-scraps.js: -------------------------------------------------------------------------------- 1 | class AddressingMode { 2 | 3 | addr(arg, mem, cpu) { 4 | const addr = this.func_(arg, mem, cpu); 5 | if (addr != null && addr >= 0) return addr; 6 | throw new Error('Immediate valuew have no address.'); 7 | } 8 | 9 | get(arg, mem, cpu) { 10 | const addr = this.func_(arg, mem, cpu); 11 | if (addr == null) return cpu.A; 12 | if (addr < 0) return ~addr; 13 | return mem.get(addr); 14 | } 15 | 16 | set(arg, mem, cpu, value) { 17 | const addr = this.func_(arg, mem, cpu); 18 | if (addr == null) { 19 | cpu.A = value; 20 | } else if (addr < 0) { 21 | throw new Error('Cannot set an immediate value.'); 22 | } 23 | mem.set(addr, value); 24 | } 25 | 26 | /** 27 | * @param {number} arg 28 | * @param {!Memory} mem 29 | * @param {!Registers} reg 30 | * @return {number} The 8-bit result. The 8th bit (0x100) is 31 | * set if there was a branch crossing. 32 | */ 33 | // get(arg, mem, reg) { 34 | // let arg = 0; 35 | // let size = this.size_; 36 | // let addr = reg.pc + 1; 37 | // while (size--) { 38 | // arg = (arg << 8) | mem.get(addr++); 39 | // } 40 | // const result = this.func_(arg, mem, reg); 41 | // reg.pc += this.size + 1; 42 | // return result; 43 | // } 44 | } 45 | 46 | 47 | 48 | 49 | /** An addressing mode. */ 50 | class AddressingMode { 51 | /** 52 | * @param {number} size 53 | * @param {function(number, !Memory, !Cpu): ?number} func 54 | */ 55 | constructor(name, fmt, func) { 56 | this.bytes_ = /\$\$/.test(fmt) ? 2 : /\$/.test(fmt) ? 1 : 0; 57 | this.signed_ = /\+\$/.test(fmt); 58 | this.name_ = name; 59 | /** @private @const {function(!Cpu): number} */ 60 | this.func_ = func; 61 | 62 | const before = fmt.replace(/\+?\$\$?.*/, ''); 63 | const after = fmt.replace(/.*\+?\$\$?/, ''); 64 | const pad = 65 | this.bytes_ == 2 ? 66 | x => '0000'.substring(x.length, 4) + x.toUpperCase() : 67 | x => '00'.substring(x.length, 2) + x.toUpperCase(); 68 | const sgn = this.signed_ ? (x => x < 0 ? '-' : x > 0 ? '+' : '') : () => ''; 69 | /** @const {function(number): string} */ 70 | this.format = 71 | this.bytes_ == 0 ? 72 | arg => fmt : 73 | arg => before + sgn(arg) + '$' + pad(arg.toString(16)) + after; 74 | } 75 | 76 | addr(arg, mem, cpu) { 77 | const addr = this.func_(arg, mem, cpu); 78 | if (addr != null && addr >= 0) return addr; 79 | throw new Error('Immediate valuew have no address.'); 80 | } 81 | 82 | get(arg, mem, cpu) { 83 | const addr = this.func_(arg, mem, cpu); 84 | if (addr == null) return cpu.A; 85 | if (addr < 0) return ~addr; 86 | return mem.get(addr); 87 | } 88 | 89 | set(arg, mem, cpu, value) { 90 | const addr = this.func_(arg, mem, cpu); 91 | if (addr == null) { 92 | cpu.A = value; 93 | } else if (addr < 0) { 94 | throw new Error('Cannot set an immediate value.'); 95 | } 96 | mem.set(addr, value); 97 | } 98 | 99 | /** 100 | * @param {number} arg 101 | * @param {!Memory} mem 102 | * @param {!Registers} reg 103 | * @return {number} The 8-bit result. The 8th bit (0x100) is 104 | * set if there was a branch crossing. 105 | */ 106 | // get(arg, mem, reg) { 107 | // let arg = 0; 108 | // let size = this.size_; 109 | // let addr = reg.pc + 1; 110 | // while (size--) { 111 | // arg = (arg << 8) | mem.get(addr++); 112 | // } 113 | // const result = this.func_(arg, mem, reg); 114 | // reg.pc += this.size + 1; 115 | // return result; 116 | // } 117 | } 118 | 119 | 120 | AddressingMode.MODES = [ 121 | new AddressingMode('A', () => null), 122 | new AddressingMode('', () => { throw new Error('Operand is implied.'); }), 123 | new AddressingMode('#$', arg => ~arg), 124 | new AddressingMode('$$', arg => arg), 125 | new AddressingMode('($$)', (arg, mem) => mem.getWord(arg)), 126 | new AddressingMode('$$,x', (arg, mem, cpu) => mem.getWord(arg + cpu.X)), 127 | new AddressingMode('$$,y', (arg, mem, cpu) => mem.getWord(arg + cpu.Y)), 128 | new AddressingMode('$', arg => arg), 129 | new AddressingMode('$,x', (arg, mem, cpu) => arg), 130 | new AddressingMode('$,y', (arg, mem, cpu) => arg), 131 | new AddressingMode('($,x)', (arg, mem, cpu) => arg), 132 | new AddressingMode('($),y', (arg, mem, cpu) => arg), 133 | AddressingMode.IMPLIED = new AddressingMode( 134 | 135 | 'A': mode(0, (arg, mem, reg) => accum(reg)), 136 | 'i': mode(0, () => { throw new Error('Operand is implied.'); }), 137 | '#': mode(1, arg => immediate(arg)), 138 | '##': mode(2, arg => immediate(arg)), // NOTE: fake mode for JMP 139 | 'a': mode(2, (arg, mem) => mem.cell(arg)), 140 | 'aa': mode(2, (arg, mem) => immediate(mem.getWord(arg, true))), // FAKE 141 | 'zp': mode(1, (arg, mem) => mem.cell(arg)), 142 | 'r': mode(1, (arg, mem, reg) => mem.cell((arg << 24 >> 24) + reg.pc)), 143 | 'a,x': mode(2, (arg, mem, reg) => absWith(mem, arg, reg.x)), 144 | 'a,y': mode(2, (arg, mem, reg) => absWith(mem, arg, reg.y)), 145 | 'zp,x': mode(1, (arg, mem, reg) => mem.cell(0xff & (arg + reg.x))), 146 | 'zp,y': mode(1, (arg, mem, reg) => mem.cell(0xff & (arg + reg.y))), 147 | '(zp,x)': mode(1, (arg, mem, reg) => 148 | mem.cell(mem.get(0xff & (arg + reg.x)) + 149 | (mem.get(0xff & (arg + reg.x + 1)) << 8))), 150 | '(zp),y': mode(1, (arg, mem, reg) => absWith(mem, mem.getWord(arg)), reg.y), 151 | 152 | /** @const {!Object} */ 153 | const ADDRESSING_MODES = (function() { 154 | /** 155 | * @param {number} size 156 | * @param {function(number, !Memory, !Registers): !Memory.Cell} func 157 | * @return {!AddressingMode} 158 | */ 159 | function mode(size, func) { 160 | return new AddressingMode(size, func); 161 | } 162 | /** 163 | * Adds in the page-crossing bit. 164 | * @param {!Memory} mem 165 | * @param {number} arg 166 | * @param {number} offset 167 | * @return {!Memory.Cell} 168 | */ 169 | function absWith(mem, arg, offset) { 170 | const addr = arg + offset; 171 | return mem.cell(addr, addr & 0xff00 != arg && 0xff00); 172 | } 173 | /** 174 | * Accumulator register as a cell. 175 | * @param {!Registers} reg 176 | * @return {!Memory.Cell} 177 | */ 178 | function accum(reg) { 179 | return { 180 | get() { return reg.a; }, 181 | set(value) { reg.a = value; }, 182 | cross: false, 183 | }; 184 | } 185 | /** 186 | * Immediate value as a cell. 187 | * @param {number} value 188 | * @return {!Memory.Cell} 189 | */ 190 | function immediate(reg) { 191 | return { 192 | get() { return value; }, 193 | set(value) { throw new Error('Cannot set immediate value'); }, 194 | cross: false, 195 | }; 196 | } 197 | return { 198 | 'A': mode(0, (arg, mem, reg) => accum(reg)), 199 | 'i': mode(0, () => { throw new Error('Operand is implied.'); }), 200 | '#': mode(1, arg => immediate(arg)), 201 | '##': mode(2, arg => immediate(arg)), // NOTE: fake mode for JMP 202 | 'a': mode(2, (arg, mem) => mem.cell(arg)), 203 | 'aa': mode(2, (arg, mem) => immediate(mem.getWord(arg, true))), // FAKE 204 | 'zp': mode(1, (arg, mem) => mem.cell(arg)), 205 | 'r': mode(1, (arg, mem, reg) => mem.cell((arg << 24 >> 24) + reg.pc)), 206 | 'a,x': mode(2, (arg, mem, reg) => absWith(mem, arg, reg.x)), 207 | 'a,y': mode(2, (arg, mem, reg) => absWith(mem, arg, reg.y)), 208 | 'zp,x': mode(1, (arg, mem, reg) => mem.cell(0xff & (arg + reg.x))), 209 | 'zp,y': mode(1, (arg, mem, reg) => mem.cell(0xff & (arg + reg.y))), 210 | '(zp,x)': mode(1, (arg, mem, reg) => 211 | mem.cell(mem.get(0xff & (arg + reg.x)) + 212 | (mem.get(0xff & (arg + reg.x + 1)) << 8))), 213 | '(zp),y': mode(1, (arg, mem, reg) => absWith(mem, mem.getWord(arg)), reg.y), 214 | }; 215 | })(); 216 | 217 | 218 | /** 219 | * @const {!Object} 220 | */ 221 | const OPCODES = { 222 | } 223 | 224 | 225 | // TODO - potential optimization 226 | // - class Opcode { 227 | // constructor(addrmode, cycles) { 228 | // this.addrmode = ... 229 | // this.cycles = ... // how to indicate +1/+2? 230 | // this.cross = false; // set each time 231 | // } 232 | // get(operand) { from memory/reg via addrmode } 233 | // set(operand, value) { into memory/reg via addrmode } 234 | // run(operand, mem, reg) { abstract } 235 | 236 | // basic idea is that we can return extra information (e.g. cycles, etc) 237 | // as properties on the Opcode instance, since we're single-threaded... 238 | // - this should eliminate most allocations for CPU cycles... 239 | // - can we also eliminate allocations for APU cycles? 240 | 241 | 242 | // TODO - 243 | // - rework opcodes as simple methods on CPU - no need to pass args anymore 244 | // - addressing modes are a separate class since they need to format 245 | // themselves, and we may need to compare for e.g. JMP? 246 | // - alernatively, make them methods, too? 247 | // - separate parallel map for formatting? 248 | -------------------------------------------------------------------------------- /scraps/divider.js: -------------------------------------------------------------------------------- 1 | /** A simple divider. */ 2 | export default class Divider { 3 | /** 4 | * @param {function(this: THIS)} output Function to call at zero. 5 | * @param {THIS=} opt_thisArg 6 | * @template THIS 7 | */ 8 | constructor(output, opt_thisArg) { 9 | /** @private @const {function(this: THIS)} */ 10 | this.output_ = output; 11 | /** @private @const {THIS|undefined} */ 12 | this.this_ = opt_thisArg; 13 | /** @private {number} */ 14 | this.counter_ = 0; 15 | 16 | /** @type {number} */ 17 | this.period = 0; // TODO(sdh): make this a property? 18 | } 19 | 20 | /** @return {number} */ 21 | get counter() { 22 | return this.counter_; 23 | } 24 | 25 | /** Reloads the divider. */ 26 | reload() { 27 | this.counter_ = this.period; 28 | } 29 | 30 | /** Clocks the counter. */ 31 | clock() { 32 | if (this.counter_ == 0) { 33 | this.counter_ = this.period; 34 | this.output_.call(this.this_); 35 | } else { 36 | this.counter_--; 37 | } 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /scraps/index.html: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /scraps/nes.js: -------------------------------------------------------------------------------- 1 | import Clock from './clock'; 2 | import Cpu from './cpu'; 3 | import Memory from './mem'; 4 | import Nsf from './nsf'; 5 | import Apu from './apu/apu'; 6 | 7 | const clock = CLock.ntsc(); 8 | const mem = new Memory(); 9 | const apu = new Apu(mem, clock); 10 | const cpu = new Cpu(mem); 11 | 12 | function readLocalFiles() { 13 | const file = document.getElementById('files').files[0]; 14 | const reader = new FileReader(); 15 | reader.onload = (e => startEmu(e.target.result)); 16 | reader.readAsArrayBuffer(file); 17 | } 18 | 19 | function startEmu(buf) { 20 | const nsf = new Nsf(buf); 21 | console.log(nsf + ''); 22 | nsf.init(cpu, mem); 23 | clockCpu(nsf, 5, nsg.cyclesPerFrame()) 24 | } 25 | 26 | function clockCpu(nsf, frames, cyclesLeft) { 27 | if (cpu.PC == 0) { 28 | console.log('returned'); 29 | return; // done? 30 | } 31 | 32 | const start = new Date().getTime(); 33 | for (let i = 0; i < 17890; i++) { 34 | 35 | cpu.clock(); 36 | if (cpu.PC == 0xffff) { 37 | if (--frames == 0) { 38 | console.log('last frame'); 39 | return; 40 | } 41 | console.log('next frame'); 42 | nsf.frame(cpu, mem); 43 | } 44 | } 45 | setTimeout(() => clockCpu(nsf, frames), 10 + start - new Date().getTime()); 46 | } 47 | 48 | document.getElementById('fetch').addEventListener('click', readLocalFiles); 49 | -------------------------------------------------------------------------------- /scraps/test1.js: -------------------------------------------------------------------------------- 1 | var ac = new AudioContext(); 2 | 3 | var g = ac.createGain(); 4 | g.gain.value = 0.1; 5 | g.connect(ac.destination); 6 | 7 | // Square wave 8 | var o = ac.createOscillator(); 9 | o.type = 'square'; 10 | o.frequency.value = 440; 11 | o.connect(g); 12 | o.start(); 13 | 14 | // White noise 15 | var fc = ac.sampleRate*2; // count frames for 2 seconds 16 | var ab = ac.createBuffer(2, fc, ac.sampleRate); 17 | for (var c = 0; c < 2; c++) { 18 | // note: reverse order of for loop, otherwise 19 | // get weird superimposition when one is done and other is not 20 | var b = ab.getChannelData(c); 21 | for (var i = 0; i < fc; i++) { 22 | b[i] = Math.random() * 2 - 1; 23 | } 24 | } 25 | var s = ac.createBufferSource(); 26 | s.buffer = ab; 27 | s.loop = true; 28 | s.connect(g); 29 | s.start(); 30 | 31 | // TODO - separate gains.. 32 | // TODO - noise period?!? 33 | 34 | 35 | // ... or use