nes-emu
Version:
A NES emulator
143 lines (134 loc) • 5.17 kB
JavaScript
"use strict";
Object.defineProperty(exports, "__esModule", {
value: true
});
exports.default = void 0;
var _registers = require("./registers");
var _channels = require("./channels");
var _frameClock = _interopRequireDefault(require("./frameClock"));
var _constants = require("../cpu/constants");
var _config = _interopRequireDefault(require("../config"));
var _constants2 = _interopRequireDefault(require("../constants"));
var _helpers = require("../helpers");
function _interopRequireDefault(e) { return e && e.__esModule ? e : { default: e }; }
const STEPS_PER_SAMPLE = Math.floor(_constants2.default.FREQ_APU_HZ / _constants2.default.APU_SAMPLE_RATE);
/** The Audio Processing Unit. It generates audio samples. */
class APU {
constructor() {
_helpers.WithContext.apply(this);
this.sampleCounter = 0;
this.frameClockCounter = 0;
this.sample = 0;
this.frameIRQFlag = false;
this.registers = null;
this.channels = {
pulses: [new _channels.PulseChannel(0, "enablePulse1"), new _channels.PulseChannel(1, "enablePulse2")],
triangle: new _channels.TriangleChannel(),
noise: new _channels.NoiseChannel(),
dmc: new _channels.DMCChannel()
};
this._onQuarter = this._onQuarter.bind(this);
this._onHalf = this._onHalf.bind(this);
this._onEnd = this._onEnd.bind(this);
}
/** When a context is loaded. */
onLoad(context) {
this.registers = new _registers.APURegisterSegment(context);
this.channels.pulses[0].loadContext(context);
this.channels.pulses[1].loadContext(context);
this.channels.triangle.loadContext(context);
this.channels.noise.loadContext(context);
this.channels.dmc.loadContext(context);
this._reset();
}
/**
* Executes the next step (1 step = 1 APU cycle).
* Returns an interrupt or null.
* It calls `onSample` when it generates a new sample.
*/
step(onSample) {
let irq = null;
const onIRQ = type => {
if (type === "frame") {
if (!this.registers.apuFrameCounter.interruptInhibitFlag) {
this.frameIRQFlag = true;
irq = _constants.interrupts.IRQ;
}
} else irq = _constants.interrupts.IRQ;
};
this._onNewCycle(onIRQ);
this._incrementCounters();
if (this.sampleCounter === 0) onSample(this.sample);
return irq;
}
/** Returns a snapshot of the current state. */
getSaveState() {
return {
sampleCounter: this.sampleCounter,
frameClockCounter: this.frameClockCounter,
sample: this.sample,
frameIRQFlag: this.frameIRQFlag,
pulse1: this.channels.pulses[0].getSaveState(),
pulse2: this.channels.pulses[1].getSaveState(),
triangle: this.channels.triangle.getSaveState(),
noise: this.channels.noise.getSaveState(),
dmc: this.channels.dmc.getSaveState()
};
}
/** Restores state from a snapshot. */
setSaveState(saveState) {
this.sampleCounter = saveState.sampleCounter;
this.frameClockCounter = saveState.frameClockCounter;
this.sample = saveState.sample;
this.frameIRQFlag = saveState.frameIRQFlag;
this.channels.pulses[0].setSaveState(saveState.pulse1);
this.channels.pulses[1].setSaveState(saveState.pulse2);
this.channels.triangle.setSaveState(saveState.triangle);
this.channels.noise.setSaveState(saveState.noise);
this.channels.dmc.setSaveState(saveState.dmc);
}
_onNewCycle(onIRQ) {
this.channels.pulses[0].cycle();
this.channels.pulses[1].cycle();
_frameClock.default.measure(this.frameClockCounter, this.registers.apuFrameCounter.use5StepSequencer, onIRQ, this._onQuarter, this._onHalf, this._onEnd);
const isNewSample = this.sampleCounter + 1 === STEPS_PER_SAMPLE;
const pulse1 = this.channels.pulses[0].sample(isNewSample); // 0 ~ 15
const pulse2 = this.channels.pulses[1].sample(isNewSample); // 0 ~ 15
const triangle = this.channels.triangle.sample(isNewSample); // 0 ~ 15
const noise = this.channels.noise.sample(); // 0 ~ 15
const dmc = this.channels.dmc.sample(onIRQ); // 0 ~ 127
// (APU mixer from nesdev)
const pulseOut = 0.00752 * (pulse1 + pulse2);
const tndOut = 0.00851 * triangle + 0.00494 * noise + 0.00335 * dmc;
this.sample = _config.default.BASE_VOLUME * (pulseOut + tndOut);
}
_onQuarter() {
// (quarter frame "beats" adjust the volume envelope and triangle's linear length counter)
this.channels.pulses[0].quarterBeat();
this.channels.pulses[1].quarterBeat();
this.channels.triangle.quarterBeat();
this.channels.noise.quarterBeat();
}
_onHalf() {
// (half frame "beats" adjust the note length and frequency sweepers)
this.channels.pulses[0].halfBeat();
this.channels.pulses[1].halfBeat();
this.channels.triangle.halfBeat();
this.channels.noise.halfBeat();
}
_onEnd() {
this.frameClockCounter = 0;
}
_incrementCounters() {
this.sampleCounter++;
this.frameClockCounter++;
if (this.sampleCounter === STEPS_PER_SAMPLE) this.sampleCounter = 0;
}
_reset() {
this.sampleCounter = 0;
this.frameClockCounter = 0;
this.sample = 0;
this.frameIRQFlag = false;
}
}
exports.default = APU;