1605 lines
42 KiB
JavaScript
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;
|