meowio/jsnes/src/papu.js

1605 lines
42 KiB
JavaScript

var utils = require("./utils");
var CPU_FREQ_NTSC = 1789772.5; //1789772.72727272d;
// var CPU_FREQ_PAL = 1773447.4;
var PAPU = function (nes) {
this.nes = nes;
this.square1 = new ChannelSquare(this, true);
this.square2 = new ChannelSquare(this, false);
this.triangle = new ChannelTriangle(this);
this.noise = new ChannelNoise(this);
this.dmc = new ChannelDM(this);
this.frameIrqCounter = null;
this.frameIrqCounterMax = 4;
this.initCounter = 2048;
this.channelEnableValue = null;
this.sampleRate = 44100;
this.lengthLookup = null;
this.dmcFreqLookup = null;
this.noiseWavelengthLookup = null;
this.square_table = null;
this.tnd_table = null;
this.frameIrqEnabled = false;
this.frameIrqActive = null;
this.frameClockNow = null;
this.startedPlaying = false;
this.recordOutput = false;
this.initingHardware = false;
this.masterFrameCounter = null;
this.derivedFrameCounter = null;
this.countSequence = null;
this.sampleTimer = null;
this.frameTime = null;
this.sampleTimerMax = null;
this.sampleCount = null;
this.triValue = 0;
this.smpSquare1 = null;
this.smpSquare2 = null;
this.smpTriangle = null;
this.smpDmc = null;
this.accCount = null;
// DC removal vars:
this.prevSampleL = 0;
this.prevSampleR = 0;
this.smpAccumL = 0;
this.smpAccumR = 0;
// DAC range:
this.dacRange = 0;
this.dcValue = 0;
// Master volume:
this.masterVolume = 256;
// Stereo positioning:
this.stereoPosLSquare1 = null;
this.stereoPosLSquare2 = null;
this.stereoPosLTriangle = null;
this.stereoPosLNoise = null;
this.stereoPosLDMC = null;
this.stereoPosRSquare1 = null;
this.stereoPosRSquare2 = null;
this.stereoPosRTriangle = null;
this.stereoPosRNoise = null;
this.stereoPosRDMC = null;
this.extraCycles = null;
this.maxSample = null;
this.minSample = null;
// Panning:
this.panning = [80, 170, 100, 150, 128];
this.setPanning(this.panning);
// Initialize lookup tables:
this.initLengthLookup();
this.initDmcFrequencyLookup();
this.initNoiseWavelengthLookup();
this.initDACtables();
// Init sound registers:
for (var i = 0; i < 0x14; i++) {
if (i === 0x10) {
this.writeReg(0x4010, 0x10);
} else {
this.writeReg(0x4000 + i, 0);
}
}
this.reset();
};
PAPU.prototype = {
reset: function () {
this.sampleRate = this.nes.opts.sampleRate;
this.sampleTimerMax = Math.floor(
(1024.0 * CPU_FREQ_NTSC * this.nes.opts.preferredFrameRate) /
(this.sampleRate * 60.0)
);
this.frameTime = Math.floor(
(14915.0 * this.nes.opts.preferredFrameRate) / 60.0
);
this.sampleTimer = 0;
this.updateChannelEnable(0);
this.masterFrameCounter = 0;
this.derivedFrameCounter = 0;
this.countSequence = 0;
this.sampleCount = 0;
this.initCounter = 2048;
this.frameIrqEnabled = false;
this.initingHardware = false;
this.resetCounter();
this.square1.reset();
this.square2.reset();
this.triangle.reset();
this.noise.reset();
this.dmc.reset();
this.accCount = 0;
this.smpSquare1 = 0;
this.smpSquare2 = 0;
this.smpTriangle = 0;
this.smpDmc = 0;
this.frameIrqEnabled = false;
this.frameIrqCounterMax = 4;
this.channelEnableValue = 0xff;
this.startedPlaying = false;
this.prevSampleL = 0;
this.prevSampleR = 0;
this.smpAccumL = 0;
this.smpAccumR = 0;
this.maxSample = -500000;
this.minSample = 500000;
},
// eslint-disable-next-line no-unused-vars
readReg: function (address) {
// Read 0x4015:
var tmp = 0;
tmp |= this.square1.getLengthStatus();
tmp |= this.square2.getLengthStatus() << 1;
tmp |= this.triangle.getLengthStatus() << 2;
tmp |= this.noise.getLengthStatus() << 3;
tmp |= this.dmc.getLengthStatus() << 4;
tmp |= (this.frameIrqActive && this.frameIrqEnabled ? 1 : 0) << 6;
tmp |= this.dmc.getIrqStatus() << 7;
this.frameIrqActive = false;
this.dmc.irqGenerated = false;
return tmp & 0xffff;
},
writeReg: function (address, value) {
if (address >= 0x4000 && address < 0x4004) {
// Square Wave 1 Control
this.square1.writeReg(address, value);
// console.log("Square Write");
} else if (address >= 0x4004 && address < 0x4008) {
// Square 2 Control
this.square2.writeReg(address, value);
} else if (address >= 0x4008 && address < 0x400c) {
// Triangle Control
this.triangle.writeReg(address, value);
} else if (address >= 0x400c && address <= 0x400f) {
// Noise Control
this.noise.writeReg(address, value);
} else if (address === 0x4010) {
// DMC Play mode & DMA frequency
this.dmc.writeReg(address, value);
} else if (address === 0x4011) {
// DMC Delta Counter
this.dmc.writeReg(address, value);
} else if (address === 0x4012) {
// DMC Play code starting address
this.dmc.writeReg(address, value);
} else if (address === 0x4013) {
// DMC Play code length
this.dmc.writeReg(address, value);
} else if (address === 0x4015) {
// Channel enable
this.updateChannelEnable(value);
if (value !== 0 && this.initCounter > 0) {
// Start hardware initialization
this.initingHardware = true;
}
// DMC/IRQ Status
this.dmc.writeReg(address, value);
} else if (address === 0x4017) {
// Frame counter control
this.countSequence = (value >> 7) & 1;
this.masterFrameCounter = 0;
this.frameIrqActive = false;
if (((value >> 6) & 0x1) === 0) {
this.frameIrqEnabled = true;
} else {
this.frameIrqEnabled = false;
}
if (this.countSequence === 0) {
// NTSC:
this.frameIrqCounterMax = 4;
this.derivedFrameCounter = 4;
} else {
// PAL:
this.frameIrqCounterMax = 5;
this.derivedFrameCounter = 0;
this.frameCounterTick();
}
}
},
resetCounter: function () {
if (this.countSequence === 0) {
this.derivedFrameCounter = 4;
} else {
this.derivedFrameCounter = 0;
}
},
// Updates channel enable status.
// This is done on writes to the
// channel enable register (0x4015),
// and when the user enables/disables channels
// in the GUI.
updateChannelEnable: function (value) {
this.channelEnableValue = value & 0xffff;
this.square1.setEnabled((value & 1) !== 0);
this.square2.setEnabled((value & 2) !== 0);
this.triangle.setEnabled((value & 4) !== 0);
this.noise.setEnabled((value & 8) !== 0);
this.dmc.setEnabled((value & 16) !== 0);
},
// Clocks the frame counter. It should be clocked at
// twice the cpu speed, so the cycles will be
// divided by 2 for those counters that are
// clocked at cpu speed.
clockFrameCounter: function (nCycles) {
if (this.initCounter > 0) {
if (this.initingHardware) {
this.initCounter -= nCycles;
if (this.initCounter <= 0) {
this.initingHardware = false;
}
return;
}
}
// Don't process ticks beyond next sampling:
nCycles += this.extraCycles;
var maxCycles = this.sampleTimerMax - this.sampleTimer;
if (nCycles << 10 > maxCycles) {
this.extraCycles = ((nCycles << 10) - maxCycles) >> 10;
nCycles -= this.extraCycles;
} else {
this.extraCycles = 0;
}
var dmc = this.dmc;
var triangle = this.triangle;
var square1 = this.square1;
var square2 = this.square2;
var noise = this.noise;
// Clock DMC:
if (dmc.isEnabled) {
dmc.shiftCounter -= nCycles << 3;
while (dmc.shiftCounter <= 0 && dmc.dmaFrequency > 0) {
dmc.shiftCounter += dmc.dmaFrequency;
dmc.clockDmc();
}
}
// Clock Triangle channel Prog timer:
var trianglesample = this.nes.opts.sampleTriangle;
if (triangle.progTimerMax > 0) {
triangle.progTimerCount -= nCycles;
while (triangle.progTimerCount <= 0) {
if (trianglesample) { trianglesample.advance(8); }
triangle.progTimerCount += triangle.progTimerMax + 1;
if (triangle.linearCounter > 0 && triangle.lengthCounter > 0) {
triangle.triangleCounter++;
triangle.triangleCounter &= 0x1f;
if (triangle.isEnabled) {
if (triangle.triangleCounter >= 0x10) {
// Normal value.
triangle.sampleValue = triangle.triangleCounter & 0xf;
} else {
// Inverted value.
triangle.sampleValue = 0xf - (triangle.triangleCounter & 0xf);
}
triangle.sampleValue <<= 4;
}
}
}
}
if (trianglesample) {
if (triangle.isEnabled && triangle.progTimerMax > 0) {
triangle.sampleValue = trianglesample.sample(triangle.progTimerCount, triangle.progTimerMax, 0.3);
} else {
trianglesample.reset();
triangle.sampleValue = 0;
}
}
// Clock Square channel 1 Prog timer:
var square1sample = this.nes.opts[square1.sampleName];
square1.progTimerCount -= nCycles;
if (square1.progTimerCount <= 0) {
square1.progTimerCount += (square1.progTimerMax + 1) << 1;
if (square1sample) { square1sample.advance(4); }
square1.squareCounter++;
square1.squareCounter &= 0x7;
square1.updateSampleValue();
} else if (square1sample) {
square1.updateSampleValue();
}
// Clock Square channel 2 Prog timer:
var square2sample = this.nes.opts[square2.sampleName];
square2.progTimerCount -= nCycles;
if (square2.progTimerCount <= 0) {
square2.progTimerCount += (square2.progTimerMax + 1) << 1;
if (square2sample) { square2sample.advance(4); }
square2.squareCounter++;
square2.squareCounter &= 0x7;
square2.updateSampleValue();
} else if (square2sample) {
square2.updateSampleValue();
}
// Clock noise channel Prog timer:
var acc_c = nCycles;
if (noise.progTimerCount - acc_c > 0) {
// Do all cycles at once:
noise.progTimerCount -= acc_c;
noise.accCount += acc_c;
noise.accValue += acc_c * noise.sampleValue;
} else {
// Slow-step:
while (acc_c-- > 0) {
if (--noise.progTimerCount <= 0 && noise.progTimerMax > 0) {
// Update noise shift register:
noise.shiftReg <<= 1;
noise.tmp =
((noise.shiftReg << (noise.randomMode === 0 ? 1 : 6)) ^
noise.shiftReg) &
0x8000;
if (noise.tmp !== 0) {
// Sample value must be 0.
noise.shiftReg |= 0x01;
noise.randomBit = 0;
noise.sampleValue = 0;
} else {
// Find sample value:
noise.randomBit = 1;
if (noise.isEnabled && noise.lengthCounter > 0) {
noise.sampleValue = noise.masterVolume;
} else {
noise.sampleValue = 0;
}
}
noise.progTimerCount += noise.progTimerMax;
}
noise.accValue += noise.sampleValue;
noise.accCount++;
}
}
// Frame IRQ handling:
if (this.frameIrqEnabled && this.frameIrqActive) {
this.nes.cpu.requestIrq(this.nes.cpu.IRQ_NORMAL);
}
// Clock frame counter at double CPU speed:
this.masterFrameCounter += nCycles << 1;
if (this.masterFrameCounter >= this.frameTime) {
// 240Hz tick:
this.masterFrameCounter -= this.frameTime;
this.frameCounterTick();
}
// Accumulate sample value:
this.accSample(nCycles);
// Clock sample timer:
this.sampleTimer += nCycles << 10;
if (this.sampleTimer >= this.sampleTimerMax) {
// Sample channels:
this.sample();
this.sampleTimer -= this.sampleTimerMax;
}
},
accSample: function (cycles) {
// Special treatment for triangle channel - need to interpolate.
if (this.triangle.sampleCondition) {
this.triValue = Math.floor(
(this.triangle.progTimerCount << 4) / (this.triangle.progTimerMax + 1)
);
if (this.triValue > 16) {
this.triValue = 16;
}
if (this.triangle.triangleCounter >= 16) {
this.triValue = 16 - this.triValue;
}
// Add non-interpolated sample value:
this.triValue += this.triangle.sampleValue;
}
// Now sample normally:
if (cycles === 2) {
this.smpTriangle += this.triValue << 1;
this.smpDmc += this.dmc.sample << 1;
this.smpSquare1 += this.square1.sampleValue << 1;
this.smpSquare2 += this.square2.sampleValue << 1;
this.accCount += 2;
} else if (cycles === 4) {
this.smpTriangle += this.triValue << 2;
this.smpDmc += this.dmc.sample << 2;
this.smpSquare1 += this.square1.sampleValue << 2;
this.smpSquare2 += this.square2.sampleValue << 2;
this.accCount += 4;
} else {
this.smpTriangle += cycles * this.triValue;
this.smpDmc += cycles * this.dmc.sample;
this.smpSquare1 += cycles * this.square1.sampleValue;
this.smpSquare2 += cycles * this.square2.sampleValue;
this.accCount += cycles;
}
},
frameCounterTick: function () {
this.derivedFrameCounter++;
if (this.derivedFrameCounter >= this.frameIrqCounterMax) {
this.derivedFrameCounter = 0;
}
if (this.derivedFrameCounter === 1 || this.derivedFrameCounter === 3) {
// Clock length & sweep:
this.triangle.clockLengthCounter();
this.square1.clockLengthCounter();
this.square2.clockLengthCounter();
this.noise.clockLengthCounter();
this.square1.clockSweep();
this.square2.clockSweep();
}
if (this.derivedFrameCounter >= 0 && this.derivedFrameCounter < 4) {
// Clock linear & decay:
this.square1.clockEnvDecay();
this.square2.clockEnvDecay();
this.noise.clockEnvDecay();
this.triangle.clockLinearCounter();
}
if (this.derivedFrameCounter === 3 && this.countSequence === 0) {
// Enable IRQ:
this.frameIrqActive = true;
}
// End of 240Hz tick
},
// Samples the channels, mixes the output together, then writes to buffer.
sample: function () {
var sq_index, tnd_index;
if (this.accCount > 0) {
this.smpSquare1 <<= 4;
this.smpSquare1 = Math.floor(this.smpSquare1 / this.accCount);
this.smpSquare2 <<= 4;
this.smpSquare2 = Math.floor(this.smpSquare2 / this.accCount);
this.smpTriangle = Math.floor(this.smpTriangle / this.accCount);
this.smpDmc <<= 4;
this.smpDmc = Math.floor(this.smpDmc / this.accCount);
this.accCount = 0;
} else {
this.smpSquare1 = this.square1.sampleValue << 4;
this.smpSquare2 = this.square2.sampleValue << 4;
this.smpTriangle = this.triangle.sampleValue;
this.smpDmc = this.dmc.sample << 4;
}
var smpNoise = Math.floor((this.noise.accValue << 4) / this.noise.accCount);
this.noise.accValue = smpNoise >> 4;
this.noise.accCount = 1;
var extra = 0;
if (this.nes.opts.sampleSquare1) { this.smpSquare1 = 0; extra += this.square1.sampleValue; }
if (this.nes.opts.sampleSquare2) { this.smpSquare2 = 0; extra += this.square2.sampleValue; }
if (this.nes.opts.sampleTriangle) { this.smpTriangle = 0; extra += this.triangle.sampleValue; }
// Stereo sound.
// Left channel:
sq_index =
(this.smpSquare1 * this.stereoPosLSquare1 +
this.smpSquare2 * this.stereoPosLSquare2) >>
8;
tnd_index =
(3 * this.smpTriangle * this.stereoPosLTriangle +
(smpNoise << 1) * this.stereoPosLNoise +
this.smpDmc * this.stereoPosLDMC) >>
8;
if (sq_index >= this.square_table.length) {
sq_index = this.square_table.length - 1;
}
if (tnd_index >= this.tnd_table.length) {
tnd_index = this.tnd_table.length - 1;
}
var sampleValueL =
this.square_table[sq_index] + this.tnd_table[tnd_index] - this.dcValue;
// Right channel:
sq_index =
(this.smpSquare1 * this.stereoPosRSquare1 +
this.smpSquare2 * this.stereoPosRSquare2) >>
8;
tnd_index =
(3 * this.smpTriangle * this.stereoPosRTriangle +
(smpNoise << 1) * this.stereoPosRNoise +
this.smpDmc * this.stereoPosRDMC) >>
8;
if (sq_index >= this.square_table.length) {
sq_index = this.square_table.length - 1;
}
if (tnd_index >= this.tnd_table.length) {
tnd_index = this.tnd_table.length - 1;
}
var sampleValueR =
this.square_table[sq_index] + this.tnd_table[tnd_index] - this.dcValue;
if (Number.isNaN(sampleValueL) || Number.isNaN(sampleValueR) ||
Number.isNaN(this.prevSampleL) || Number.isNaN(this.prevSampleR) ||
Number.isNaN(this.smpAccumL) || Number.isNaN(this.smpAccumR)) {
// this happens sometimes when turning off samples. doesn't seem to make any
// sense. reset everything back to 0.
sampleValueL = sampleValueR = this.prevSampleL = this.prevSampleR = this.smpAccumL = this.smpAccumR = 0;
}
// Remove DC from left channel:
var smpDiffL = sampleValueL - this.prevSampleL;
this.prevSampleL += smpDiffL;
this.smpAccumL += smpDiffL - (this.smpAccumL >> 10);
sampleValueL = this.smpAccumL;
// Remove DC from right channel:
var smpDiffR = sampleValueR - this.prevSampleR;
this.prevSampleR += smpDiffR;
this.smpAccumR += smpDiffR - (this.smpAccumR >> 10);
sampleValueR = this.smpAccumR;
// Write:
if (sampleValueL > this.maxSample) {
this.maxSample = sampleValueL;
}
if (sampleValueL < this.minSample) {
this.minSample = sampleValueL;
}
if (this.nes.opts.onAudioSample) {
this.nes.opts.onAudioSample((sampleValueL / 32768) + extra, (sampleValueR / 32768) + extra);
}
// Reset sampled values:
this.smpSquare1 = 0;
this.smpSquare2 = 0;
this.smpTriangle = 0;
this.smpDmc = 0;
},
getLengthMax: function (value) {
return this.lengthLookup[value >> 3];
},
getDmcFrequency: function (value) {
if (value >= 0 && value < 0x10) {
return this.dmcFreqLookup[value];
}
return 0;
},
getNoiseWaveLength: function (value) {
if (value >= 0 && value < 0x10) {
return this.noiseWavelengthLookup[value];
}
return 0;
},
setPanning: function (pos) {
for (var i = 0; i < 5; i++) {
this.panning[i] = pos[i];
}
this.updateStereoPos();
},
setMasterVolume: function (value) {
if (value < 0) {
value = 0;
}
if (value > 256) {
value = 256;
}
this.masterVolume = value;
this.updateStereoPos();
},
updateStereoPos: function () {
this.stereoPosLSquare1 = (this.panning[0] * this.masterVolume) >> 8;
this.stereoPosLSquare2 = (this.panning[1] * this.masterVolume) >> 8;
this.stereoPosLTriangle = (this.panning[2] * this.masterVolume) >> 8;
this.stereoPosLNoise = (this.panning[3] * this.masterVolume) >> 8;
this.stereoPosLDMC = (this.panning[4] * this.masterVolume) >> 8;
this.stereoPosRSquare1 = this.masterVolume - this.stereoPosLSquare1;
this.stereoPosRSquare2 = this.masterVolume - this.stereoPosLSquare2;
this.stereoPosRTriangle = this.masterVolume - this.stereoPosLTriangle;
this.stereoPosRNoise = this.masterVolume - this.stereoPosLNoise;
this.stereoPosRDMC = this.masterVolume - this.stereoPosLDMC;
},
initLengthLookup: function () {
// prettier-ignore
this.lengthLookup = [
0x0A, 0xFE,
0x14, 0x02,
0x28, 0x04,
0x50, 0x06,
0xA0, 0x08,
0x3C, 0x0A,
0x0E, 0x0C,
0x1A, 0x0E,
0x0C, 0x10,
0x18, 0x12,
0x30, 0x14,
0x60, 0x16,
0xC0, 0x18,
0x48, 0x1A,
0x10, 0x1C,
0x20, 0x1E
];
},
initDmcFrequencyLookup: function () {
this.dmcFreqLookup = new Array(16);
this.dmcFreqLookup[0x0] = 0xd60;
this.dmcFreqLookup[0x1] = 0xbe0;
this.dmcFreqLookup[0x2] = 0xaa0;
this.dmcFreqLookup[0x3] = 0xa00;
this.dmcFreqLookup[0x4] = 0x8f0;
this.dmcFreqLookup[0x5] = 0x7f0;
this.dmcFreqLookup[0x6] = 0x710;
this.dmcFreqLookup[0x7] = 0x6b0;
this.dmcFreqLookup[0x8] = 0x5f0;
this.dmcFreqLookup[0x9] = 0x500;
this.dmcFreqLookup[0xa] = 0x470;
this.dmcFreqLookup[0xb] = 0x400;
this.dmcFreqLookup[0xc] = 0x350;
this.dmcFreqLookup[0xd] = 0x2a0;
this.dmcFreqLookup[0xe] = 0x240;
this.dmcFreqLookup[0xf] = 0x1b0;
//for(int i=0;i<16;i++)dmcFreqLookup[i]/=8;
},
initNoiseWavelengthLookup: function () {
this.noiseWavelengthLookup = new Array(16);
this.noiseWavelengthLookup[0x0] = 0x004;
this.noiseWavelengthLookup[0x1] = 0x008;
this.noiseWavelengthLookup[0x2] = 0x010;
this.noiseWavelengthLookup[0x3] = 0x020;
this.noiseWavelengthLookup[0x4] = 0x040;
this.noiseWavelengthLookup[0x5] = 0x060;
this.noiseWavelengthLookup[0x6] = 0x080;
this.noiseWavelengthLookup[0x7] = 0x0a0;
this.noiseWavelengthLookup[0x8] = 0x0ca;
this.noiseWavelengthLookup[0x9] = 0x0fe;
this.noiseWavelengthLookup[0xa] = 0x17c;
this.noiseWavelengthLookup[0xb] = 0x1fc;
this.noiseWavelengthLookup[0xc] = 0x2fa;
this.noiseWavelengthLookup[0xd] = 0x3f8;
this.noiseWavelengthLookup[0xe] = 0x7f2;
this.noiseWavelengthLookup[0xf] = 0xfe4;
},
initDACtables: function () {
var value, ival, i;
var max_sqr = 0;
var max_tnd = 0;
this.square_table = new Array(32 * 16);
this.tnd_table = new Array(204 * 16);
for (i = 0; i < 32 * 16; i++) {
value = 95.52 / (8128.0 / (i / 16.0) + 100.0);
value *= 0.98411;
value *= 50000.0;
ival = Math.floor(value);
this.square_table[i] = ival;
if (ival > max_sqr) {
max_sqr = ival;
}
}
for (i = 0; i < 204 * 16; i++) {
value = 163.67 / (24329.0 / (i / 16.0) + 100.0);
value *= 0.98411;
value *= 50000.0;
ival = Math.floor(value);
this.tnd_table[i] = ival;
if (ival > max_tnd) {
max_tnd = ival;
}
}
this.dacRange = max_sqr + max_tnd;
this.dcValue = this.dacRange / 2;
},
JSON_PROPERTIES: [
"frameIrqCounter",
"frameIrqCounterMax",
"initCounter",
"channelEnableValue",
"sampleRate",
"frameIrqEnabled",
"frameIrqActive",
"frameClockNow",
"startedPlaying",
"recordOutput",
"initingHardware",
"masterFrameCounter",
"derivedFrameCounter",
"countSequence",
"sampleTimer",
"frameTime",
"sampleTimerMax",
"sampleCount",
"triValue",
"smpSquare1",
"smpSquare2",
"smpTriangle",
"smpDmc",
"accCount",
"prevSampleL",
"prevSampleR",
"smpAccumL",
"smpAccumR",
"masterVolume",
"stereoPosLSquare1",
"stereoPosLSquare2",
"stereoPosLTriangle",
"stereoPosLNoise",
"stereoPosLDMC",
"stereoPosRSquare1",
"stereoPosRSquare2",
"stereoPosRTriangle",
"stereoPosRNoise",
"stereoPosRDMC",
"extraCycles",
"maxSample",
"minSample",
"panning",
],
toJSON: function () {
let obj = utils.toJSON(this);
obj.dmc = this.dmc.toJSON();
obj.noise = this.noise.toJSON();
obj.square1 = this.square1.toJSON();
obj.square2 = this.square2.toJSON();
obj.triangle = this.triangle.toJSON();
return obj;
},
fromJSON: function (s) {
utils.fromJSON(this, s);
this.dmc.fromJSON(s.dmc);
this.noise.fromJSON(s.noise);
this.square1.fromJSON(s.square1);
this.square2.fromJSON(s.square2);
this.triangle.fromJSON(s.triangle);
},
};
var ChannelDM = function (papu) {
this.papu = papu;
this.MODE_NORMAL = 0;
this.MODE_LOOP = 1;
this.MODE_IRQ = 2;
this.isEnabled = null;
this.hasSample = null;
this.irqGenerated = false;
this.playMode = null;
this.dmaFrequency = null;
this.dmaCounter = null;
this.deltaCounter = null;
this.playStartAddress = null;
this.playAddress = null;
this.playLength = null;
this.playLengthCounter = null;
this.shiftCounter = null;
this.reg4012 = null;
this.reg4013 = null;
this.sample = null;
this.dacLsb = null;
this.data = null;
this.reset();
};
ChannelDM.prototype = {
clockDmc: function () {
// Only alter DAC value if the sample buffer has data:
if (this.hasSample) {
if ((this.data & 1) === 0) {
// Decrement delta:
if (this.deltaCounter > 0) {
this.deltaCounter--;
}
} else {
// Increment delta:
if (this.deltaCounter < 63) {
this.deltaCounter++;
}
}
// Update sample value:
this.sample = this.isEnabled ? (this.deltaCounter << 1) + this.dacLsb : 0;
// Update shift register:
this.data >>= 1;
}
this.dmaCounter--;
if (this.dmaCounter <= 0) {
// No more sample bits.
this.hasSample = false;
this.endOfSample();
this.dmaCounter = 8;
}
if (this.irqGenerated) {
this.papu.nes.cpu.requestIrq(this.papu.nes.cpu.IRQ_NORMAL);
}
},
endOfSample: function () {
if (this.playLengthCounter === 0 && this.playMode === this.MODE_LOOP) {
// Start from beginning of sample:
this.playAddress = this.playStartAddress;
this.playLengthCounter = this.playLength;
}
if (this.playLengthCounter > 0) {
// Fetch next sample:
this.nextSample();
if (this.playLengthCounter === 0) {
// Last byte of sample fetched, generate IRQ:
if (this.playMode === this.MODE_IRQ) {
// Generate IRQ:
this.irqGenerated = true;
}
}
}
},
nextSample: function () {
// Fetch byte:
this.data = this.papu.nes.mmap.load(this.playAddress);
this.papu.nes.cpu.haltCycles(4);
this.playLengthCounter--;
this.playAddress++;
if (this.playAddress > 0xffff) {
this.playAddress = 0x8000;
}
this.hasSample = true;
},
writeReg: function (address, value) {
if (address === 0x4010) {
// Play mode, DMA Frequency
if (value >> 6 === 0) {
this.playMode = this.MODE_NORMAL;
} else if (((value >> 6) & 1) === 1) {
this.playMode = this.MODE_LOOP;
} else if (value >> 6 === 2) {
this.playMode = this.MODE_IRQ;
}
if ((value & 0x80) === 0) {
this.irqGenerated = false;
}
this.dmaFrequency = this.papu.getDmcFrequency(value & 0xf);
} else if (address === 0x4011) {
// Delta counter load register:
this.deltaCounter = (value >> 1) & 63;
this.dacLsb = value & 1;
this.sample = (this.deltaCounter << 1) + this.dacLsb; // update sample value
} else if (address === 0x4012) {
// DMA address load register
this.playStartAddress = (value << 6) | 0x0c000;
this.playAddress = this.playStartAddress;
this.reg4012 = value;
} else if (address === 0x4013) {
// Length of play code
this.playLength = (value << 4) + 1;
this.playLengthCounter = this.playLength;
this.reg4013 = value;
} else if (address === 0x4015) {
// DMC/IRQ Status
if (((value >> 4) & 1) === 0) {
// Disable:
this.playLengthCounter = 0;
} else {
// Restart:
this.playAddress = this.playStartAddress;
this.playLengthCounter = this.playLength;
}
this.irqGenerated = false;
}
},
setEnabled: function (value) {
if (!this.isEnabled && value) {
this.playLengthCounter = this.playLength;
}
this.isEnabled = value;
},
getLengthStatus: function () {
return this.playLengthCounter === 0 || !this.isEnabled ? 0 : 1;
},
getIrqStatus: function () {
return this.irqGenerated ? 1 : 0;
},
reset: function () {
this.isEnabled = false;
this.irqGenerated = false;
this.playMode = this.MODE_NORMAL;
this.dmaFrequency = 0;
this.dmaCounter = 0;
this.deltaCounter = 0;
this.playStartAddress = 0;
this.playAddress = 0;
this.playLength = 0;
this.playLengthCounter = 0;
this.sample = 0;
this.dacLsb = 0;
this.shiftCounter = 0;
this.reg4012 = 0;
this.reg4013 = 0;
this.data = 0;
},
JSON_PROPERTIES: [
"MODE_NORMAL",
"MODE_LOOP",
"MODE_IRQ",
"isEnabled",
"hasSample",
"irqGenerated",
"playMode",
"dmaFrequency",
"dmaCounter",
"deltaCounter",
"playStartAddress",
"playAddress",
"playLength",
"playLengthCounter",
"shiftCounter",
"reg4012",
"reg4013",
"sample",
"dacLsb",
"data",
],
toJSON: function () {
return utils.toJSON(this);
},
fromJSON: function (s) {
utils.fromJSON(this, s);
},
};
var ChannelNoise = function (papu) {
this.papu = papu;
this.isEnabled = null;
this.envDecayDisable = null;
this.envDecayLoopEnable = null;
this.lengthCounterEnable = null;
this.envReset = null;
this.shiftNow = null;
this.lengthCounter = null;
this.progTimerCount = null;
this.progTimerMax = null;
this.envDecayRate = null;
this.envDecayCounter = null;
this.envVolume = null;
this.masterVolume = null;
this.shiftReg = 1 << 14;
this.randomBit = null;
this.randomMode = null;
this.sampleValue = null;
this.accValue = 0;
this.accCount = 1;
this.tmp = null;
this.reset();
};
ChannelNoise.prototype = {
reset: function () {
this.progTimerCount = 0;
this.progTimerMax = 0;
this.isEnabled = false;
this.lengthCounter = 0;
this.lengthCounterEnable = false;
this.envDecayDisable = false;
this.envDecayLoopEnable = false;
this.shiftNow = false;
this.envDecayRate = 0;
this.envDecayCounter = 0;
this.envVolume = 0;
this.masterVolume = 0;
this.shiftReg = 1;
this.randomBit = 0;
this.randomMode = 0;
this.sampleValue = 0;
this.tmp = 0;
},
clockLengthCounter: function () {
if (this.lengthCounterEnable && this.lengthCounter > 0) {
this.lengthCounter--;
if (this.lengthCounter === 0) {
this.updateSampleValue();
}
}
},
clockEnvDecay: function () {
if (this.envReset) {
// Reset envelope:
this.envReset = false;
this.envDecayCounter = this.envDecayRate + 1;
this.envVolume = 0xf;
} else if (--this.envDecayCounter <= 0) {
// Normal handling:
this.envDecayCounter = this.envDecayRate + 1;
if (this.envVolume > 0) {
this.envVolume--;
} else {
this.envVolume = this.envDecayLoopEnable ? 0xf : 0;
}
}
if (this.envDecayDisable) {
this.masterVolume = this.envDecayRate;
} else {
this.masterVolume = this.envVolume;
}
this.updateSampleValue();
},
updateSampleValue: function () {
if (this.isEnabled && this.lengthCounter > 0) {
this.sampleValue = this.randomBit * this.masterVolume;
}
},
writeReg: function (address, value) {
if (address === 0x400c) {
// Volume/Envelope decay:
this.envDecayDisable = (value & 0x10) !== 0;
this.envDecayRate = value & 0xf;
this.envDecayLoopEnable = (value & 0x20) !== 0;
this.lengthCounterEnable = (value & 0x20) === 0;
if (this.envDecayDisable) {
this.masterVolume = this.envDecayRate;
} else {
this.masterVolume = this.envVolume;
}
} else if (address === 0x400e) {
// Programmable timer:
this.progTimerMax = this.papu.getNoiseWaveLength(value & 0xf);
this.randomMode = value >> 7;
} else if (address === 0x400f) {
// Length counter
this.lengthCounter = this.papu.getLengthMax(value & 248);
this.envReset = true;
}
// Update:
//updateSampleValue();
},
setEnabled: function (value) {
this.isEnabled = value;
if (!value) {
this.lengthCounter = 0;
}
this.updateSampleValue();
},
getLengthStatus: function () {
return this.lengthCounter === 0 || !this.isEnabled ? 0 : 1;
},
JSON_PROPERTIES: [
"isEnabled",
"envDecayDisable",
"envDecayLoopEnable",
"lengthCounterEnable",
"envReset",
"shiftNow",
"lengthCounter",
"progTimerCount",
"progTimerMax",
"envDecayRate",
"envDecayCounter",
"envVolume",
"masterVolume",
"shiftReg",
"randomBit",
"randomMode",
"sampleValue",
"accValue",
"accCount",
"tmp",
],
toJSON: function () {
return utils.toJSON(this);
},
fromJSON: function (s) {
utils.fromJSON(this, s);
},
};
var ChannelSquare = function (papu, square1) {
this.papu = papu;
this.sampleName = `sampleSquare${square1 ? 1 : 2}`;
// prettier-ignore
this.dutyLookup = [
0, 1, 0, 0, 0, 0, 0, 0,
0, 1, 1, 0, 0, 0, 0, 0,
0, 1, 1, 1, 1, 0, 0, 0,
1, 0, 0, 1, 1, 1, 1, 1
];
// prettier-ignore
this.impLookup = [
1,-1, 0, 0, 0, 0, 0, 0,
1, 0,-1, 0, 0, 0, 0, 0,
1, 0, 0, 0,-1, 0, 0, 0,
-1, 0, 1, 0, 0, 0, 0, 0
];
this.sqr1 = square1;
this.isEnabled = null;
this.lengthCounterEnable = null;
this.sweepActive = null;
this.envDecayDisable = null;
this.envDecayLoopEnable = null;
this.envReset = null;
this.sweepCarry = null;
this.updateSweepPeriod = null;
this.progTimerCount = null;
this.progTimerMax = null;
this.lengthCounter = null;
this.squareCounter = null;
this.sweepCounter = null;
this.sweepCounterMax = null;
this.sweepMode = null;
this.sweepShiftAmount = null;
this.envDecayRate = null;
this.envDecayCounter = null;
this.envVolume = null;
this.masterVolume = null;
this.dutyMode = null;
this.sweepResult = null;
this.sampleValue = null;
this.vol = null;
this.reset();
};
ChannelSquare.prototype = {
reset: function () {
this.progTimerCount = 0;
this.progTimerMax = 0;
this.lengthCounter = 0;
this.squareCounter = 0;
this.sweepCounter = 0;
this.sweepCounterMax = 0;
this.sweepMode = 0;
this.sweepShiftAmount = 0;
this.envDecayRate = 0;
this.envDecayCounter = 0;
this.envVolume = 0;
this.masterVolume = 0;
this.dutyMode = 0;
this.vol = 0;
this.isEnabled = false;
this.lengthCounterEnable = false;
this.sweepActive = false;
this.sweepCarry = false;
this.envDecayDisable = false;
this.envDecayLoopEnable = false;
},
clockLengthCounter: function () {
if (this.lengthCounterEnable && this.lengthCounter > 0) {
this.lengthCounter--;
if (this.lengthCounter === 0) {
this.updateSampleValue();
}
}
},
clockEnvDecay: function () {
if (this.envReset) {
// Reset envelope:
this.envReset = false;
this.envDecayCounter = this.envDecayRate + 1;
this.envVolume = 0xf;
} else if (--this.envDecayCounter <= 0) {
// Normal handling:
this.envDecayCounter = this.envDecayRate + 1;
if (this.envVolume > 0) {
this.envVolume--;
} else {
this.envVolume = this.envDecayLoopEnable ? 0xf : 0;
}
}
if (this.envDecayDisable) {
this.masterVolume = this.envDecayRate;
} else {
this.masterVolume = this.envVolume;
}
this.updateSampleValue();
},
clockSweep: function () {
if (--this.sweepCounter <= 0) {
this.sweepCounter = this.sweepCounterMax + 1;
if (
this.sweepActive &&
this.sweepShiftAmount > 0 &&
this.progTimerMax > 7
) {
// Calculate result from shifter:
this.sweepCarry = false;
if (this.sweepMode === 0) {
this.progTimerMax += this.progTimerMax >> this.sweepShiftAmount;
if (this.progTimerMax > 4095) {
this.progTimerMax = 4095;
this.sweepCarry = true;
}
} else {
this.progTimerMax =
this.progTimerMax -
((this.progTimerMax >> this.sweepShiftAmount) -
(this.sqr1 ? 1 : 0));
}
}
}
if (this.updateSweepPeriod) {
this.updateSweepPeriod = false;
this.sweepCounter = this.sweepCounterMax + 1;
}
},
updateSampleValue: function () {
var sample = this.papu.nes.opts[this.sampleName];
if (this.isEnabled && this.lengthCounter > 0 && this.progTimerMax > 7) {
if (
this.sweepMode === 0 &&
this.progTimerMax + (this.progTimerMax >> this.sweepShiftAmount) > 4095
) {
//if (this.sweepCarry) {
this.sampleValue = 0;
if (sample) { sample.reset(); }
} else {
if (sample) {
if (this.envVolume === 0 || this.masterVolume === 0) {
this.sampleValue = 0;
sample.reset();
} else {
this.sampleValue = sample.sample(this.progTimerCount, (this.progTimerMax + 1) << 1, this.masterVolume / 15);
// console.log("sampled value:", this.sampleValue);
}
} else {
this.sampleValue =
this.masterVolume *
this.dutyLookup[(this.dutyMode << 3) + this.squareCounter];
}
}
} else {
this.sampleValue = 0;
if (sample) { sample.reset(); }
}
},
writeReg: function (address, value) {
var addrAdd = this.sqr1 ? 0 : 4;
if (address === 0x4000 + addrAdd) {
// Volume/Envelope decay:
this.envDecayDisable = (value & 0x10) !== 0;
this.envDecayRate = value & 0xf;
this.envDecayLoopEnable = (value & 0x20) !== 0;
this.dutyMode = (value >> 6) & 0x3;
this.lengthCounterEnable = (value & 0x20) === 0;
if (this.envDecayDisable) {
this.masterVolume = this.envDecayRate;
} else {
this.masterVolume = this.envVolume;
}
this.updateSampleValue();
} else if (address === 0x4001 + addrAdd) {
// Sweep:
this.sweepActive = (value & 0x80) !== 0;
this.sweepCounterMax = (value >> 4) & 7;
this.sweepMode = (value >> 3) & 1;
this.sweepShiftAmount = value & 7;
this.updateSweepPeriod = true;
} else if (address === 0x4002 + addrAdd) {
// Programmable timer:
this.progTimerMax &= 0x700;
this.progTimerMax |= value;
} else if (address === 0x4003 + addrAdd) {
// Programmable timer, length counter
this.progTimerMax &= 0xff;
this.progTimerMax |= (value & 0x7) << 8;
if (this.isEnabled) {
this.lengthCounter = this.papu.getLengthMax(value & 0xf8);
}
this.envReset = true;
}
},
setEnabled: function (value) {
this.isEnabled = value;
if (!value) {
this.lengthCounter = 0;
}
this.updateSampleValue();
},
getLengthStatus: function () {
return this.lengthCounter === 0 || !this.isEnabled ? 0 : 1;
},
JSON_PROPERTIES: [
"isEnabled",
"lengthCounterEnable",
"sweepActive",
"envDecayDisable",
"envDecayLoopEnable",
"envReset",
"sweepCarry",
"updateSweepPeriod",
"progTimerCount",
"progTimerMax",
"lengthCounter",
"squareCounter",
"sweepCounter",
"sweepCounterMax",
"sweepMode",
"sweepShiftAmount",
"envDecayRate",
"envDecayCounter",
"envVolume",
"masterVolume",
"dutyMode",
"sweepResult",
"sampleValue",
"vol",
],
toJSON: function () {
return utils.toJSON(this);
},
fromJSON: function (s) {
utils.fromJSON(this, s);
},
};
var ChannelTriangle = function (papu) {
this.papu = papu;
this.isEnabled = null;
this.sampleCondition = null;
this.lengthCounterEnable = null;
this.lcHalt = null;
this.lcControl = null;
this.progTimerCount = null;
this.progTimerMax = null;
this.triangleCounter = null;
this.lengthCounter = null;
this.linearCounter = null;
this.lcLoadValue = null;
this.sampleValue = null;
this.tmp = null;
this.reset();
};
ChannelTriangle.prototype = {
reset: function () {
this.progTimerCount = 0;
this.progTimerMax = 0;
this.triangleCounter = 0;
this.isEnabled = false;
this.sampleCondition = false;
this.lengthCounter = 0;
this.lengthCounterEnable = false;
this.linearCounter = 0;
this.lcLoadValue = 0;
this.lcHalt = true;
this.lcControl = false;
this.tmp = 0;
this.sampleValue = 0xf;
},
clockLengthCounter: function () {
if (this.lengthCounterEnable && this.lengthCounter > 0) {
this.lengthCounter--;
if (this.lengthCounter === 0) {
this.updateSampleCondition();
}
}
},
clockLinearCounter: function () {
if (this.lcHalt) {
// Load:
this.linearCounter = this.lcLoadValue;
this.updateSampleCondition();
} else if (this.linearCounter > 0) {
// Decrement:
this.linearCounter--;
this.updateSampleCondition();
}
if (!this.lcControl) {
// Clear halt flag:
this.lcHalt = false;
}
},
getLengthStatus: function () {
return this.lengthCounter === 0 || !this.isEnabled ? 0 : 1;
},
// eslint-disable-next-line no-unused-vars
readReg: function (address) {
return 0;
},
writeReg: function (address, value) {
if (address === 0x4008) {
// New values for linear counter:
this.lcControl = (value & 0x80) !== 0;
this.lcLoadValue = value & 0x7f;
// Length counter enable:
this.lengthCounterEnable = !this.lcControl;
} else if (address === 0x400a) {
// Programmable timer:
this.progTimerMax &= 0x700;
this.progTimerMax |= value;
} else if (address === 0x400b) {
// Programmable timer, length counter
this.progTimerMax &= 0xff;
this.progTimerMax |= (value & 0x07) << 8;
this.lengthCounter = this.papu.getLengthMax(value & 0xf8);
this.lcHalt = true;
}
this.updateSampleCondition();
},
clockProgrammableTimer: function (nCycles) {
if (this.progTimerMax > 0) {
this.progTimerCount += nCycles;
while (
this.progTimerMax > 0 &&
this.progTimerCount >= this.progTimerMax
) {
this.progTimerCount -= this.progTimerMax;
if (
this.isEnabled &&
this.lengthCounter > 0 &&
this.linearCounter > 0
) {
this.clockTriangleGenerator();
}
}
}
},
clockTriangleGenerator: function () {
this.triangleCounter++;
this.triangleCounter &= 0x1f;
},
setEnabled: function (value) {
this.isEnabled = value;
if (!value) {
this.lengthCounter = 0;
}
this.updateSampleCondition();
},
updateSampleCondition: function () {
this.sampleCondition =
this.isEnabled &&
this.progTimerMax > 7 &&
this.linearCounter > 0 &&
this.lengthCounter > 0;
},
JSON_PROPERTIES: [
"isEnabled",
"sampleCondition",
"lengthCounterEnable",
"lcHalt",
"lcControl",
"progTimerCount",
"progTimerMax",
"triangleCounter",
"lengthCounter",
"linearCounter",
"lcLoadValue",
"sampleValue",
"tmp",
],
toJSON: function () {
return utils.toJSON(this);
},
fromJSON: function (s) {
utils.fromJSON(this, s);
},
};
module.exports = PAPU;