summaryrefslogtreecommitdiffstats
path: root/js.2/papu.js
diff options
context:
space:
mode:
Diffstat (limited to 'js.2/papu.js')
-rw-r--r--js.2/papu.js1422
1 files changed, 1422 insertions, 0 deletions
diff --git a/js.2/papu.js b/js.2/papu.js
new file mode 100644
index 0000000..f30864f
--- /dev/null
+++ b/js.2/papu.js
@@ -0,0 +1,1422 @@
+
+NES.PAPU = function(nes) {
+ this.nes = nes;
+
+ this.square1 = new NES.PAPU.ChannelSquare(this, true);
+ this.square2 = new NES.PAPU.ChannelSquare(this, false);
+ this.triangle = new NES.PAPU.ChannelTriangle(this);
+ this.noise = new NES.PAPU.ChannelNoise(this);
+ this.dmc = new NES.PAPU.ChannelDM(this);
+
+ this.frameIrqCounter = null;
+ this.frameIrqCounterMax = 4;
+ this.initCounter = 2048;
+ this.channelEnableValue = null;
+
+ this.bufferSize = 8192;
+ this.bufferIndex = 0;
+ this.sampleRate = 44100;
+
+ this.lengthLookup = null;
+ this.dmcFreqLookup = null;
+ this.noiseWavelengthLookup = null;
+ this.square_table = null;
+ this.tnd_table = null;
+ this.sampleBuffer = new Array(this.bufferSize*2);
+
+ this.frameIrqEnabled = false;
+ this.frameIrqActive;
+ this.frameClockNow;
+ 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.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 = new Array(
+ 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.dynamicaudio = new DynamicAudio();
+
+ this.reset();
+}
+
+NES.PAPU.prototype = {
+ reset: function() {
+ this.sampleRate = this.nes.opts.sampleRate;
+ this.sampleTimerMax = parseInt(
+ (1024.0 * this.nes.opts.CPU_FREQ_NTSC
+ * this.nes.opts.preferredFrameRate) /
+ (this.sampleRate * 60.0)
+ );
+
+ this.frameTime = parseInt(
+ (14915.0 * this.nes.opts.preferredFrameRate) / 60.0
+ );
+
+ this.sampleTimer = 0;
+ this.bufferIndex = 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.bufferIndex = 0;
+ 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;
+ },
+
+ 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);
+ ////System.out.println("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:
+ if(triangle.progTimerMax>0){
+
+ triangle.progTimerCount -= nCycles;
+ while(triangle.progTimerCount <= 0){
+
+ 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;
+ }
+ }
+ }
+ }
+
+ // Clock Square channel 1 Prog timer:
+ square1.progTimerCount -= nCycles;
+ if(square1.progTimerCount <= 0){
+
+ square1.progTimerCount += (square1.progTimerMax+1)<<1;
+
+ square1.squareCounter++;
+ square1.squareCounter&=0x7;
+ square1.updateSampleValue();
+
+ }
+
+ // Clock Square channel 2 Prog timer:
+ square2.progTimerCount -= nCycles;
+ if(square2.progTimerCount <= 0){
+
+ square2.progTimerCount += (square2.progTimerMax+1)<<1;
+
+ square2.squareCounter++;
+ square2.squareCounter&=0x7;
+ 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){
+ var triangle = this.triangle;
+
+ // Special treatment for triangle channel - need to interpolate.
+ if(triangle.sampleCondition){
+
+ var triValue = parseInt((triangle.progTimerCount<<4) / (triangle.progTimerMax+1));
+ if(triValue>16) triValue = 16;
+ if(triangle.triangleCounter >= 16){
+ triValue = 16-triValue;
+ }
+
+ // Add non-interpolated sample value:
+ triValue += triangle.sampleValue;
+ }
+
+ // Now sample normally:
+ if(cycles == 2){
+
+ this.smpTriangle += 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 += 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 * 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,
+ // writes to buffer and (if enabled) file.
+ sample: function(){
+
+ if(this.accCount>0){
+
+ this.smpSquare1 <<= 4;
+ this.smpSquare1 = parseInt(this.smpSquare1/this.accCount);
+
+ this.smpSquare2 <<= 4;
+ this.smpSquare2 = parseInt(this.smpSquare2/this.accCount);
+
+ this.smpTriangle = parseInt(this.smpTriangle/this.accCount);
+
+ this.smpDmc <<= 4;
+ this.smpDmc = parseInt(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 = parseInt((this.noise.accValue<<4)/this.noise.accCount);
+ this.noise.accValue = smpNoise>>4;
+ this.noise.accCount = 1;
+
+ // Stereo sound.
+
+ // Left channel:
+ var sq_index = ( this.smpSquare1 * this.stereoPosLSquare1 + this.smpSquare2 * this.stereoPosLSquare2 )>>8;
+ var 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:
+ var sq_index = ( this.smpSquare1 * this.stereoPosRSquare1 + this.smpSquare2 * this.stereoPosRSquare2 )>>8;
+ var 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;
+
+ // 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;
+ this.sampleBuffer[this.bufferIndex++] = (sampleValueL );
+ this.sampleBuffer[this.bufferIndex++] = (sampleValueR );
+
+ // Write full buffer
+ if (this.bufferIndex == this.sampleBuffer.length) {
+ this.dynamicaudio.writeInt(this.sampleBuffer);
+ this.sampleBuffer = new Array(this.bufferSize*2);
+ this.bufferIndex = 0;
+ }
+
+ // 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(){
+
+ this.lengthLookup = new Array(
+ 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(){
+
+ this.square_table = new Array(32*16);
+ this.tnd_table = new Array(204*16);
+ var value;
+
+ var ival;
+ var max_sqr = 0;
+ var max_tnd = 0;
+
+ for(var i=0;i<32*16;i++){
+
+
+ value = 95.52 / (8128.0 / (i/16.0) + 100.0);
+ value *= 0.98411;
+ value *= 50000.0;
+ ival = parseInt(value);
+
+ this.square_table[i] = ival;
+ if(ival > max_sqr){
+ max_sqr = ival;
+ }
+
+ }
+
+ for(var i=0;i<204*16;i++){
+
+ value = 163.67 / (24329.0 / (i/16.0) + 100.0);
+ value *= 0.98411;
+ value *= 50000.0;
+ ival = parseInt(value);
+
+ this.tnd_table[i] = ival;
+ if(ival > max_tnd){
+ max_tnd = ival;
+ }
+
+ }
+
+ this.dacRange = max_sqr+max_tnd;
+ this.dcValue = this.dacRange/2;
+
+ }
+}
+
+
+NES.PAPU.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();
+}
+
+NES.PAPU.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;
+ }
+}
+
+
+NES.PAPU.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();
+}
+
+NES.PAPU.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;
+ }
+ }
+ this.masterVolume = this.envDecayDisable ? this.envDecayRate : 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);
+ this.masterVolume = this.envDecayDisable?this.envDecayRate: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);
+ }
+}
+
+
+NES.PAPU.ChannelSquare = function(papu, square1) {
+ this.papu = papu;
+
+ 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
+ ];
+ 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();
+}
+
+NES.PAPU.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;
+ }
+ }
+
+ this.masterVolume = this.envDecayDisable ? this.envDecayRate : 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() {
+ 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;
+ }else{
+ this.sampleValue = this.masterVolume*this.dutyLookup[(this.dutyMode<<3)+this.squareCounter];
+ }
+ }else{
+ this.sampleValue = 0;
+ }
+ },
+
+ 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);
+ this.masterVolume = this.envDecayDisable?this.envDecayRate: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);
+ }
+}
+
+
+NES.PAPU.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();
+}
+
+NES.PAPU.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);
+ },
+
+ 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;
+ }
+}
+