diff options
author | Ben Firshman <ben@firshman.co.uk> | 2009-10-17 14:29:44 +0100 |
---|---|---|
committer | Ben Firshman <ben@firshman.co.uk> | 2009-10-17 14:29:44 +0100 |
commit | 4c2e24801f70dd5eab4a798c0d44cd1a4aa6082e (patch) | |
tree | 4544b823cf626302984b6e5a99b40c2aafb1c2a6 | |
parent | 6a81ba4b4eb2b3a5324c6be4a392d2e4ffa69c67 (diff) | |
download | jsnes-4c2e24801f70dd5eab4a798c0d44cd1a4aa6082e.zip jsnes-4c2e24801f70dd5eab4a798c0d44cd1a4aa6082e.tar.gz jsnes-4c2e24801f70dd5eab4a798c0d44cd1a4aa6082e.tar.bz2 |
Simple sound support
-rw-r--r-- | globals.js | 8 | ||||
-rw-r--r-- | index.html | 10 | ||||
-rw-r--r-- | jssound.swf | bin | 0 -> 1416 bytes | |||
-rw-r--r-- | mappers.js | 14 | ||||
-rw-r--r-- | nes.js | 35 | ||||
-rw-r--r-- | papu.js | 1471 |
6 files changed, 1526 insertions, 12 deletions
@@ -1,8 +1,14 @@ var Globals = { + CPU_FREQ_NTSC: 1789772.5,//1789772.72727272d; + CPU_FREQ_PAL: 1773447.4, + preferredFrameRate: 60, frameTime: null, // Microsecs per frame memoryFlushValue: 0xFF, // What value to flush memory with on power-up nes: null, - fpsInterval: 500 // Time between updating FPS in ms + fpsInterval: 500, // Time between updating FPS in ms + + emulateSound: true, + sampleRate: 44100, // Sound sample rate in hz } Globals.frameTime = 1000/Globals.preferredFrameRate; @@ -20,6 +20,7 @@ charset="utf-8"> <script src="nametable.js" type="text/javascript" charset="utf-8"></script> <script src="nes.js" type="text/javascript" charset="utf-8"></script> <script src="palettetable.js" type="text/javascript" charset="utf-8"></script> + <script src="papu.js" type="text/javascript" charset="utf-8"></script> <script src="ppu.js" type="text/javascript" charset="utf-8"></script> <script src="rom.js" type="text/javascript" charset="utf-8"></script> <script src="roms.js" type="text/javascript" charset="utf-8"></script> @@ -117,6 +118,11 @@ digg_url = 'http://digg.com/playable_web_games/JSNES_A_NES_emulator_written_enti <p>I got underway shamelessly porting <a href="http://www.virtualnes.com/">vNES</a> into Javascript. Although not the most efficient, it didn't have any of the pointer memory mapping magic associated with emulators written in lower level languages. As such, it was more or less a direct port, bar a few tweaks to compensate for the lack of static typing, and obviously a rewrite of all the I/O.</p> <p>JSNES runs at full speed on <a href="http://www.google.com/chrome">Google Chrome</a> with a modern computer, so it is highly recommended you use that to play. <a href="http://build.chromium.org/buildbot/snapshots/chromium-rel-mac/">Mac builds</a> are also available. Otherwise, it just about runs on <a href="http://getfirefox.com/">Firefox 3.5</a> or <a href="http://www.apple.com/safari/">Safari 4</a>, but it's hardly playable.</p> <p>The source is available on <a href="http://github.com/bfirsh/jsnes/">Github</a>, contributions welcome!</p> + <object classid="clsid:d27cdb6e-ae6d-11cf-96b8-444553540000" codebase="http://download.macromedia.com/pub/shockwave/cabs/flash/swflash.cab#version=10,0,0,0" width="1" height="1" id="jssound" align="middle"> + <param name="allowScriptAccess" value="sameDomain" /> + <param name="allowFullScreen" value="false" /> + <param name="movie" value="jssound.swf?callback=readJSSoundBuffer" /><param name="quality" value="high" /><param name="bgcolor" value="#ffffff" /> <embed src="jssound.swf?callback=readJSSoundBuffer" quality="high" bgcolor="#ffffff" width="1" height="1" name="jssound" align="middle" allowScriptAccess="always" allowFullScreen="false" type="application/x-shockwave-flash" pluginspage="http://www.adobe.com/go/getflashplayer" /> + </object> <script type="text/javascript" charset="utf-8"> // Mouse events $("#screen").mousedown(function(e){ @@ -168,6 +174,10 @@ digg_url = 'http://digg.com/playable_web_games/JSNES_A_NES_emulator_written_enti nes.start(); }); + function readJSSoundBuffer() { + return nes.papu.readBuffer(); + } + var gaJsHost = (("https:" == document.location.protocol) ? "https://ssl." : "http://www."); document.write(unescape("%3Cscript src='" + gaJsHost + "google-analytics.com/ga.js' type='text/javascript'%3E%3C/script%3E")); </script> diff --git a/jssound.swf b/jssound.swf Binary files differnew file mode 100644 index 0000000..cde545f --- /dev/null +++ b/jssound.swf @@ -174,9 +174,7 @@ MapperDefault.prototype.regLoad = function(address){ // 0x4015: // Sound channel enable, DMC Status - //return nes.getPapu().readReg(address); - //alert("Accessed sound register") - return 0; + return this.nes.papu.readReg(address); }case 1:{ @@ -285,7 +283,7 @@ MapperDefault.prototype.regWrite = function(address, value){ }case 0x4015:{ // Sound Channel Switch, DMC Status - //this.nes.getPapu().writeReg(address,value); + this.nes.papu.writeReg(address,value); break; }case 0x4016:{ @@ -304,16 +302,16 @@ MapperDefault.prototype.regWrite = function(address, value){ }case 0x4017:{ // Sound channel frame sequencer: - //nes.papu.writeReg(address,value); + this.nes.papu.writeReg(address,value); break; }default:{ // Sound registers ////System.out.println("write to sound reg"); - /*if(address >= 0x4000 && address <= 0x4017){ - nes.getPapu().writeReg(address,value); - }*/ + if(address >= 0x4000 && address <= 0x4017){ + this.nes.papu.writeReg(address,value); + } break; } @@ -7,6 +7,7 @@ function NES() { this.cpu = new CPU(this); this.ppu = new PPU(this); + this.papu = new PAPU(this); this.memMapper = null; this.palTable = new PaletteTable(); this.rom = null; @@ -16,6 +17,7 @@ function NES() { this.lastFpsTime = null; this.fpsFrameCount = 0; this.crashMessage = null; + this.limitFrames = true; this.palTable.loadNTSCPalette(); //this.palTable.loadDefaultPalette(); @@ -31,13 +33,25 @@ function NES() { this.imageData.data[i] = 0xFF; } + // Init sound registers: + for(var i=0;i<0x14;i++){ + if(i==0x10){ + this.papu.writeReg(0x4010, 0x10); + }else{ + this.papu.writeReg(0x4000+i, 0); + } + } this.start = function() { if(this.rom != null && this.rom.valid) { if (!this.isRunning) { //$("#status").text("Running "+this.romFile) this.isRunning = true; - this.frameInterval = setInterval(runFrame, Globals.frameTime); + var frameTime = 0; + if (this.limitFrames) { + frameTime = Globals.frameTime; + } + this.frameInterval = setInterval(runFrame, frameTime); this.resetFps(); this.printFps(); this.fpsInterval = setInterval(runPrintFps, Globals.fpsInterval); @@ -51,17 +65,29 @@ function NES() { this.frame = function() { this.ppu.startFrame(); var cycles = 0; + var emulateSound = Globals.emulateSound; FRAMELOOP: for (;;) { - if (this.cpu.cyclesToHalt == 0) + if (this.cpu.cyclesToHalt == 0) { // Execute a CPU instruction - cycles = this.cpu.emulate()*3; + cycles = this.cpu.emulate(); + if(emulateSound) { + this.papu.clockFrameCounter(cycles); + } + cycles *= 3; + } else { if (this.cpu.cyclesToHalt > 8) { cycles = 24; + if (emulateSound) { + this.papu.clockFrameCounter(8); + } this.cpu.cyclesToHalt -= 8; } else { cycles = this.cpu.cyclesToHalt * 3; + if (emulateSound) { + this.papu.clockFrameCounter(this.cpu.cyclesToHalt); + } this.cpu.cyclesToHalt = 0; } } @@ -183,6 +209,8 @@ function NES() { this.cpu.init(); this.ppu.reset(); this.palTable.reset(); + + this.papu.reset(); } this.resetFps = function() { @@ -193,6 +221,7 @@ function NES() { this.setFramerate = function(rate){ Globals.preferredFrameRate = rate; Globals.frameTime = 1000/rate; + papu.setSampleRate(Globals.sampleRate, false); } this.cpu.init(); @@ -0,0 +1,1471 @@ + +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,reg4013 = null; + this.status = 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.status = 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 = new Array( + 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 = new Array( + 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 + ; +} + + + +function PAPU(nes) { + this.nes = nes; + + this.buffer = 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.bufferSize = 4096; + 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*4); + + 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.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.maxCycles = 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(); + + this.reset(); +} + +PAPU.prototype.reset = function(){ + this.sampleRate = Globals.sampleRate; + this.sampleTimerMax = parseInt((1024.0*Globals.CPU_FREQ_NTSC*Globals.preferredFrameRate) / + (this.sampleRate*60.0)); + + this.frameTime = parseInt((14915.0*Globals.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; +} + +PAPU.prototype.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; +} + +PAPU.prototype.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(); + + } + + } +} + +PAPU.prototype.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. +PAPU.prototype.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. +PAPU.prototype.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; + this.maxCycles = this.sampleTimerMax-this.sampleTimer; + if((nCycles<<10) > this.maxCycles){ + + this.extraCycles = ((nCycles<<10) - this.maxCycles)>>10; + nCycles -= this.extraCycles; + + }else{ + + this.extraCycles = 0; + + } + + // Clock DMC: + if(this.dmc.isEnabled){ + + this.dmc.shiftCounter-=(nCycles<<3); + while(this.dmc.shiftCounter<=0 && this.dmc.dmaFrequency>0){ + this.dmc.shiftCounter += this.dmc.dmaFrequency; + this.dmc.clockDmc(); + } + + } + + // Clock Triangle channel Prog timer: + if(this.triangle.progTimerMax>0){ + + this.triangle.progTimerCount -= nCycles; + while(this.triangle.progTimerCount <= 0){ + + this.triangle.progTimerCount += this.triangle.progTimerMax+1; + if(this.triangle.linearCounter>0 && this.triangle.lengthCounter>0){ + + this.triangle.triangleCounter++; + this.triangle.triangleCounter &= 0x1F; + + if(this.triangle.isEnabled){ + if(this.triangle.triangleCounter>=0x10){ + // Normal value. + this.triangle.sampleValue = (this.triangle.triangleCounter&0xF); + }else{ + // Inverted value. + this.triangle.sampleValue = (0xF - (this.triangle.triangleCounter&0xF)); + } + this.triangle.sampleValue <<= 4; + } + + } + } + + } + + // Clock Square channel 1 Prog timer: + this.square1.progTimerCount -= nCycles; + if(this.square1.progTimerCount <= 0){ + + this.square1.progTimerCount += (this.square1.progTimerMax+1)<<1; + + this.square1.squareCounter++; + this.square1.squareCounter&=0x7; + this.square1.updateSampleValue(); + + } + + // Clock Square channel 2 Prog timer: + this.square2.progTimerCount -= nCycles; + if(this.square2.progTimerCount <= 0){ + + this.square2.progTimerCount += (this.square2.progTimerMax+1)<<1; + + this.square2.squareCounter++; + this.square2.squareCounter&=0x7; + this.square2.updateSampleValue(); + + } + + // Clock noise channel Prog timer: + var acc_c = nCycles; + if(this.noise.progTimerCount-acc_c > 0){ + + // Do all cycles at once: + this.noise.progTimerCount -= acc_c; + this.noise.accCount += acc_c; + this.noise.accValue += acc_c * this.noise.sampleValue; + + }else{ + + // Slow-step: + while((acc_c--) > 0){ + + if(--this.noise.progTimerCount <= 0 && this.noise.progTimerMax>0){ + + // Update noise shift register: + this.noise.shiftReg <<= 1; + this.noise.tmp = (((this.noise.shiftReg << (this.noise.randomMode==0?1:6)) ^ this.noise.shiftReg) & 0x8000 ); + if(this.noise.tmp!=0){ + + // Sample value must be 0. + this.noise.shiftReg |= 0x01; + this.noise.randomBit = 0; + this.noise.sampleValue = 0; + + }else{ + + // Find sample value: + this.noise.randomBit = 1; + if(this.noise.isEnabled && this.noise.lengthCounter>0){ + this.noise.sampleValue = this.noise.masterVolume; + }else{ + this.noise.sampleValue = 0; + } + + } + + this.noise.progTimerCount += this.noise.progTimerMax; + + } + + this.noise.accValue += this.noise.sampleValue; + this.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; + + } + +} + +PAPU.prototype.accSample = function(cycles){ + + // Special treatment for triangle channel - need to interpolate. + if(this.triangle.sampleCondition){ + + this.triValue = parseInt((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; + + } + +} + +PAPU.prototype.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. +PAPU.prototype.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(this.bufferIndex+4 < this.sampleBuffer.length){ + if (sampleValueL > this.maxSample) this.maxSample = sampleValueL; + if (sampleValueL < this.minSample) this.minSample = sampleValueL; + this.sampleBuffer[this.bufferIndex++] = (sampleValueL ); + this.sampleBuffer[this.bufferIndex++] = (sampleValueR ); + } + + // Reset sampled values: + this.smpSquare1 = 0; + this.smpSquare2 = 0; + this.smpTriangle = 0; + this.smpDmc = 0; + +} + +PAPU.prototype.readBuffer = function() { + //console.debug(this.bufferIndex); + if (this.bufferIndex+4 >= this.sampleBuffer.length) { + var b = this.sampleBuffer; + this.sampleBuffer = new Array(this.bufferSize*4); + this.bufferIndex = 0; + return b + } +} + +PAPU.prototype.getLengthMax = function(value){ + return this.lengthLookup[value>>3]; +} + +PAPU.prototype.getDmcFrequency = function(value){ + if(value>=0 && value<0x10){ + return this.dmcFreqLookup[value]; + } + return 0; +} + +PAPU.prototype.getNoiseWaveLength = function(value){ + if(value>=0 && value<0x10){ + return this.noiseWavelengthLookup[value]; + } + return 0; +} + +PAPU.prototype.setPanning = function(pos){ + for(var i=0;i<5;i++){ + this.panning[i] = pos[i]; + } + this.updateStereoPos(); +} + +PAPU.prototype.setMasterVolume = function(value){ + if(value<0)value=0; + if(value>256)value=256; + this.masterVolume = value; + this.updateStereoPos(); +} + +PAPU.prototype.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; +} + +PAPU.prototype.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 + ); + +} + +PAPU.prototype.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; + +} + +PAPU.prototype.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; + +} + +PAPU.prototype.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; + +} + |