nes-emu
Version:
A NES emulator
137 lines (126 loc) • 4.69 kB
JavaScript
"use strict";
Object.defineProperty(exports, "__esModule", {
value: true
});
exports.default = void 0;
var _config = _interopRequireDefault(require("../../config"));
var _helpers = require("../../helpers");
function _interopRequireDefault(e) { return e && e.__esModule ? e : { default: e }; }
/**
* The delta modulation channel (DMC) can output 1-bit delta-encoded samples (DPCM) or can
* have its 7-bit counter directly loaded (PCM). This allows flexible manual sample playback.
*
* DPCM samples are stored as a stream of 1-bit deltas that control the 7-bit PCM counter that
* the channel outputs. A bit of 1 will increment the counter, 0 will decrement, and it will clamp
* rather than overflow if the 7-bit range is exceeded.
*/
class DMCChannel {
constructor() {
_helpers.WithContext.apply(this);
this.outputSample = 0;
// DPCM:
this.startFlag = false;
this.isUsingDPCM = false;
this.buffer = null;
this.cursorByte = 0;
this.cursorBit = 0;
this.dividerCount = 0;
this.samplePeriod = 0;
this.sampleAddress = 0;
this.sampleLength = 0;
}
/** When a context is loaded. */
onLoad(context) {
this.registers = context.apu.registers.dmc;
}
/** Generates a new sample. It calls `onIRQ` when a DPCM sample finishes (if IRQ flag is on). */
sample(onIRQ) {
// (the output level is sent to the mixer whether the channel is enabled or not)
if (this.startFlag) {
const samplePeriod = this.registers.control.dpcmPeriod;
this.startFlag = false;
this.isUsingDPCM = true;
this.cursorByte = -1;
this.cursorBit = 0;
this.dividerCount = samplePeriod - 1;
this.samplePeriod = samplePeriod;
this.sampleAddress = this.registers.sampleAddress.absoluteAddress;
this.sampleLength = this.registers.sampleLength.lengthInBytes;
}
return (this.isUsingDPCM ? this._processDPCM(onIRQ) : this.outputSample) * _config.default.DMC_CHANNEL_VOLUME;
}
/** Starts auto-playing a sample from `DMCSampleAddress` in DPCM mode. */
startDPCM() {
this.startFlag = true;
}
/** Stops DPCM, finishing the 1-byte buffer first. */
stopDPCM() {
this.cursorByte = this.sampleLength;
}
/** Returns the remaining bytes of the current DPCM sample. */
get remainingBytes() {
if (!this.isUsingDPCM) return 0;
return this.sampleLength - this.cursorByte;
}
/** Returns a snapshot of the current state. */
getSaveState() {
return {
outputSample: this.outputSample,
startFlag: this.startFlag,
isUsingDPCM: this.isUsingDPCM,
buffer: this.buffer,
cursorByte: this.cursorByte,
cursorBit: this.cursorBit,
dividerCount: this.dividerCount,
samplePeriod: this.samplePeriod,
sampleAddress: this.sampleAddress,
sampleLength: this.sampleLength
};
}
/** Restores state from a snapshot. */
setSaveState(saveState) {
this.outputSample = saveState.outputSample;
this.startFlag = saveState.startFlag;
this.isUsingDPCM = saveState.isUsingDPCM;
this.buffer = saveState.buffer;
this.cursorByte = saveState.cursorByte;
this.cursorBit = saveState.cursorBit;
this.dividerCount = saveState.dividerCount;
this.samplePeriod = saveState.samplePeriod;
this.sampleAddress = saveState.sampleAddress;
this.sampleLength = saveState.sampleLength;
}
/** Returns whether the channel is enabled or not. */
get isEnabled() {
return this.context.apu.registers.apuControl.enableDMC;
}
_processDPCM(onIRQ) {
this.dividerCount++;
if (this.dividerCount >= this.samplePeriod) this.dividerCount = 0;else return this.outputSample;
const needFetch = this.buffer === null || this.cursorBit === 8;
if (needFetch) {
const nextByte = this.cursorByte + 1;
if (nextByte >= this.sampleLength) {
this.isActive = false;
this.buffer = null;
if (this.registers.control.irqEnable) onIRQ("dmc");
if (this.registers.control.loop) this.startDPCM();
return this.outputSample;
}
this.cursorByte = nextByte;
this.cursorBit = 0;
let address = this.sampleAddress + this.cursorByte;
if (address > 0xffff) {
// (if it exceeds $FFFF, it is wrapped around to $8000)
address = 0x8000 + address % 0x10000;
}
this.buffer = this.context.cpu.memory.readAt(address);
}
const variation = _helpers.Byte.getBit(this.buffer, this.cursorBit) ? 2 : -2;
const newSample = this.outputSample + variation;
if (newSample >= 0 && newSample <= 127) this.outputSample = newSample;
this.cursorBit++;
return this.outputSample;
}
}
exports.default = DMCChannel;