diff options
Diffstat (limited to 'channels.js')
-rw-r--r-- | channels.js | 685 |
1 files changed, 685 insertions, 0 deletions
diff --git a/channels.js b/channels.js new file mode 100644 index 0000000..afcb3d7 --- /dev/null +++ b/channels.js @@ -0,0 +1,685 @@ +function ChannelDM(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); + } + +}; + +ChannelDM.prototype.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; + + } + + } + + } + +}; + +ChannelDM.prototype.nextSample = function(){ + + // Fetch byte: + this.data = this.papu.nes.memMapper.load(this.playAddress); + this.papu.nes.cpu.haltCycles(4); + + this.playLengthCounter--; + this.playAddress++; + if(this.playAddress>0xFFFF){ + this.playAddress = 0x8000; + } + + this.hasSample = true; + +}; + +ChannelDM.prototype.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; + } + +}; + +ChannelDM.prototype.setEnabled = function(value){ + + if((!this.isEnabled) && value){ + this.playLengthCounter = this.playLength; + } + this.isEnabled = value; + +}; + +ChannelDM.prototype.getLengthStatus = function(){ + return ((this.playLengthCounter==0 || !this.isEnabled)?0:1); +}; + +ChannelDM.prototype.getIrqStatus = function(){ + return (this.irqGenerated?1:0); +}; + +ChannelDM.prototype.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; + +}; + + +function ChannelNoise(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; +}; + +ChannelNoise.prototype.clockLengthCounter = function(){ + if(this.lengthCounterEnable && this.lengthCounter>0){ + this.lengthCounter--; + if(this.lengthCounter == 0) this.updateSampleValue(); + } +}; + +ChannelNoise.prototype.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(); +}; + +ChannelNoise.prototype.updateSampleValue = function(){ + if(this.isEnabled && this.lengthCounter>0){ + this.sampleValue = this.randomBit * this.masterVolume; + } +}; + +ChannelNoise.prototype.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(); + +}; + +ChannelNoise.prototype.setEnabled = function(value){ + this.isEnabled = value; + if(!value) this.lengthCounter = 0; + this.updateSampleValue(); +}; + +ChannelNoise.prototype.getLengthStatus = function(){ + return ((this.lengthCounter==0 || !this.isEnabled)?0:1); +}; + + +function ChannelSquare(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(); +} + + +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; +}; + +ChannelSquare.prototype.clockLengthCounter = function(){ + + if(this.lengthCounterEnable && this.lengthCounter>0){ + this.lengthCounter--; + if(this.lengthCounter==0) this.updateSampleValue(); + } + +}; + +ChannelSquare.prototype.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(); + +}; + +ChannelSquare.prototype.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; + } + +}; + +ChannelSquare.prototype.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; + } + +}; + +ChannelSquare.prototype.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; + + } + +}; + +ChannelSquare.prototype.setEnabled = function(value){ + this.isEnabled = value; + if(!value) this.lengthCounter = 0; + this.updateSampleValue(); +}; + +ChannelSquare.prototype.getLengthStatus = function() { + return ((this.lengthCounter==0 || !this.isEnabled)?0:1); +}; + + +function ChannelTriangle(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; +}; + +ChannelTriangle.prototype.clockLengthCounter = function(){ + if(this.lengthCounterEnable && this.lengthCounter>0){ + this.lengthCounter--; + if(this.lengthCounter==0){ + this.updateSampleCondition(); + } + } +}; + +ChannelTriangle.prototype.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; + } +}; + +ChannelTriangle.prototype.getLengthStatus = function(){ + return ((this.lengthCounter === 0 || !this.isEnabled)?0:1); +}; + +ChannelTriangle.prototype.readReg = function(address){ + return 0; +}; + +ChannelTriangle.prototype.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(); + +}; + +ChannelTriangle.prototype.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(); + } + } + } + +}; + +ChannelTriangle.prototype.clockTriangleGenerator = function(){ + this.triangleCounter++; + this.triangleCounter &= 0x1F; +}; + +ChannelTriangle.prototype.setEnabled = function(value){ + this.isEnabled = value; + if(!value) this.lengthCounter = 0; + this.updateSampleCondition(); +}; + +ChannelTriangle.prototype.updateSampleCondition = function(){ + this.sampleCondition = + this.isEnabled && + this.progTimerMax>7 && + this.linearCounter>0 && + this.lengthCounter>0 + ; +}; + + |