summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorBen Firshman <ben@firshman.co.uk>2009-10-17 14:29:44 +0100
committerBen Firshman <ben@firshman.co.uk>2009-10-17 14:29:44 +0100
commit4c2e24801f70dd5eab4a798c0d44cd1a4aa6082e (patch)
tree4544b823cf626302984b6e5a99b40c2aafb1c2a6
parent6a81ba4b4eb2b3a5324c6be4a392d2e4ffa69c67 (diff)
downloadjsnes-4c2e24801f70dd5eab4a798c0d44cd1a4aa6082e.zip
jsnes-4c2e24801f70dd5eab4a798c0d44cd1a4aa6082e.tar.gz
jsnes-4c2e24801f70dd5eab4a798c0d44cd1a4aa6082e.tar.bz2
Simple sound support
-rw-r--r--globals.js8
-rw-r--r--index.html10
-rw-r--r--jssound.swfbin0 -> 1416 bytes
-rw-r--r--mappers.js14
-rw-r--r--nes.js35
-rw-r--r--papu.js1471
6 files changed, 1526 insertions, 12 deletions
diff --git a/globals.js b/globals.js
index c0570d5..5d786de 100644
--- a/globals.js
+++ b/globals.js
@@ -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;
diff --git a/index.html b/index.html
index 24ed73f..b824be9 100644
--- a/index.html
+++ b/index.html
@@ -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
new file mode 100644
index 0000000..cde545f
--- /dev/null
+++ b/jssound.swf
Binary files differ
diff --git a/mappers.js b/mappers.js
index 81c8f15..10b9664 100644
--- a/mappers.js
+++ b/mappers.js
@@ -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;
}
diff --git a/nes.js b/nes.js
index 47c0fdb..c606368 100644
--- a/nes.js
+++ b/nes.js
@@ -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();
diff --git a/papu.js b/papu.js
new file mode 100644
index 0000000..a897b7c
--- /dev/null
+++ b/papu.js
@@ -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;
+
+}
+