diff --git a/.DS_Store b/.DS_Store
new file mode 100644
index 0000000..4dabf11
Binary files /dev/null and b/.DS_Store differ
diff --git a/.gitignore b/.gitignore
index 7307d87..edf6070 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1 +1,2 @@
roms
+.vercel
diff --git a/index.htm b/index.html
similarity index 98%
rename from index.htm
rename to index.html
index 0121ff5..5a9a372 100755
--- a/index.htm
+++ b/index.html
@@ -24,6 +24,7 @@
+
diff --git a/jsgb.audio.js b/jsgb.audio.js
new file mode 100644
index 0000000..b0666c1
--- /dev/null
+++ b/jsgb.audio.js
@@ -0,0 +1,324 @@
+/*
+ * JSGB DMG audio processing unit.
+ * The APU is clocked with CPU T-cycles and produces stereo PCM for Web Audio.
+ */
+
+var gbAudioClock = 4194304;
+var gbAudioContext = null;
+var gbAudioNode = null;
+var gbAudioSampleRate = 44100;
+var gbAudioSampleTicks = gbAudioClock / gbAudioSampleRate;
+var gbAudioSampleCounter = 0;
+var gbAudioFrameCounter = 0;
+var gbAudioFrameStep = 0;
+var gbAudioQueueL = [];
+var gbAudioQueueR = [];
+var gbAudioQueuePos = 0;
+var gbAudioStarted = false;
+var gbAudioPowered = false;
+
+var gbAudioDuty = [
+ [0,0,0,0,0,0,0,1],
+ [1,0,0,0,0,0,0,1],
+ [1,0,0,0,0,1,1,1],
+ [0,1,1,1,1,1,1,0]
+];
+
+function gb_Audio_New_Square(hasSweep) {
+ return {enabled:false,dac:false,length:0,lengthOn:false,duty:0,phase:0,
+ frequency:0,timer:4,volume:0,envelopePeriod:0,envelopeTimer:0,
+ envelopeAdd:false,sweep:hasSweep,sweepPeriod:0,sweepTimer:0,
+ sweepNegate:false,sweepShift:0,sweepShadow:0,sweepEnabled:false};
+}
+
+function gb_Audio_New_Wave() {
+ return {enabled:false,dac:false,length:0,lengthOn:false,frequency:0,
+ timer:2,position:0,volumeCode:0};
+}
+
+function gb_Audio_New_Noise() {
+ return {enabled:false,dac:false,length:0,lengthOn:false,timer:8,lfsr:0x7FFF,
+ clockShift:0,width7:false,divisor:0,volume:0,envelopePeriod:0,
+ envelopeTimer:0,envelopeAdd:false};
+}
+
+var gbAudioSquare1 = gb_Audio_New_Square(true);
+var gbAudioSquare2 = gb_Audio_New_Square(false);
+var gbAudioWave = gb_Audio_New_Wave();
+var gbAudioNoise = gb_Audio_New_Noise();
+
+function gb_Audio_Square_Period(c) { return (2048-c.frequency)*4; }
+function gb_Audio_Wave_Period() { return (2048-gbAudioWave.frequency)*2; }
+function gb_Audio_Noise_Period() {
+ return (gbAudioNoise.divisor ? gbAudioNoise.divisor*16 : 8) << gbAudioNoise.clockShift;
+}
+
+function gb_Audio_Set_Status() {
+ gbMemory[0xFF26]=(gbAudioPowered?0x80:0) |
+ (gbAudioSquare1.enabled?1:0) | (gbAudioSquare2.enabled?2:0) |
+ (gbAudioWave.enabled?4:0) | (gbAudioNoise.enabled?8:0);
+}
+
+function gb_Audio_Trigger_Square(c) {
+ c.enabled=c.dac;
+ if (!c.length) c.length=64;
+ c.timer=gb_Audio_Square_Period(c);
+ c.phase=0;
+ c.volume=(gbMemory[c===gbAudioSquare1?0xFF12:0xFF17]>>4)&15;
+ c.envelopeTimer=c.envelopePeriod || 8;
+ if (c.sweep) {
+ c.sweepShadow=c.frequency;
+ c.sweepTimer=c.sweepPeriod || 8;
+ c.sweepEnabled=!!(c.sweepPeriod || c.sweepShift);
+ if (c.sweepShift) gb_Audio_Sweep_Calculate(c, false);
+ }
+ gb_Audio_Set_Status();
+}
+
+function gb_Audio_Trigger_Wave() {
+ var c=gbAudioWave;
+ c.enabled=c.dac;
+ if (!c.length) c.length=256;
+ c.timer=gb_Audio_Wave_Period();
+ c.position=0;
+ gb_Audio_Set_Status();
+}
+
+function gb_Audio_Trigger_Noise() {
+ var c=gbAudioNoise;
+ c.enabled=c.dac;
+ if (!c.length) c.length=64;
+ c.timer=gb_Audio_Noise_Period();
+ c.lfsr=0x7FFF;
+ c.volume=(gbMemory[0xFF21]>>4)&15;
+ c.envelopeTimer=c.envelopePeriod || 8;
+ gb_Audio_Set_Status();
+}
+
+function gb_Audio_Clock_Length(c) {
+ if (c.lengthOn && c.length && !--c.length) c.enabled=false;
+}
+
+function gb_Audio_Clock_Envelope(c) {
+ if (!c.envelopePeriod || !c.enabled) return;
+ if (!--c.envelopeTimer) {
+ c.envelopeTimer=c.envelopePeriod;
+ if (c.envelopeAdd && c.volume<15) c.volume++;
+ else if (!c.envelopeAdd && c.volume>0) c.volume--;
+ }
+}
+
+function gb_Audio_Sweep_Calculate(c, update) {
+ var delta=c.sweepShadow>>c.sweepShift;
+ var frequency=c.sweepNegate ? c.sweepShadow-delta : c.sweepShadow+delta;
+ if (frequency>2047 || frequency<0) {
+ c.enabled=false;
+ return false;
+ }
+ if (update && c.sweepShift) {
+ c.sweepShadow=c.frequency=frequency;
+ c.timer=gb_Audio_Square_Period(c);
+ }
+ return true;
+}
+
+function gb_Audio_Clock_Sweep() {
+ var c=gbAudioSquare1;
+ if (!c.sweepEnabled || --c.sweepTimer) return;
+ c.sweepTimer=c.sweepPeriod || 8;
+ if (c.sweepPeriod && c.sweepShift && gb_Audio_Sweep_Calculate(c,true)) gb_Audio_Sweep_Calculate(c,false);
+}
+
+function gb_Audio_Clock_Frame() {
+ if (!(gbAudioFrameStep&1)) {
+ gb_Audio_Clock_Length(gbAudioSquare1);
+ gb_Audio_Clock_Length(gbAudioSquare2);
+ gb_Audio_Clock_Length(gbAudioWave);
+ gb_Audio_Clock_Length(gbAudioNoise);
+ }
+ if (gbAudioFrameStep===2 || gbAudioFrameStep===6) gb_Audio_Clock_Sweep();
+ if (gbAudioFrameStep===7) {
+ gb_Audio_Clock_Envelope(gbAudioSquare1);
+ gb_Audio_Clock_Envelope(gbAudioSquare2);
+ gb_Audio_Clock_Envelope(gbAudioNoise);
+ }
+ gbAudioFrameStep=(gbAudioFrameStep+1)&7;
+ gb_Audio_Set_Status();
+}
+
+function gb_Audio_Advance_Square(c,ticks) {
+ c.timer-=ticks;
+ while (c.timer<=0) { c.timer+=gb_Audio_Square_Period(c); c.phase=(c.phase+1)&7; }
+}
+
+function gb_Audio_Advance_Wave(ticks) {
+ var c=gbAudioWave;
+ c.timer-=ticks;
+ while (c.timer<=0) { c.timer+=gb_Audio_Wave_Period(); c.position=(c.position+1)&31; }
+}
+
+function gb_Audio_Advance_Noise(ticks) {
+ var c=gbAudioNoise;
+ c.timer-=ticks;
+ while (c.timer<=0) {
+ c.timer+=gb_Audio_Noise_Period();
+ var bit=(c.lfsr^(c.lfsr>>1))&1;
+ c.lfsr=(c.lfsr>>1)|(bit<<14);
+ if (c.width7) c.lfsr=(c.lfsr&~0x40)|(bit<<6);
+ }
+}
+
+function gb_Audio_Channel_Output(channel) {
+ if (channel===0) return gbAudioSquare1.enabled && gbAudioSquare1.dac ?
+ gbAudioDuty[gbAudioSquare1.duty][gbAudioSquare1.phase]*gbAudioSquare1.volume : null;
+ if (channel===1) return gbAudioSquare2.enabled && gbAudioSquare2.dac ?
+ gbAudioDuty[gbAudioSquare2.duty][gbAudioSquare2.phase]*gbAudioSquare2.volume : null;
+ if (channel===2) {
+ if (!gbAudioWave.enabled || !gbAudioWave.dac || !gbAudioWave.volumeCode) return null;
+ var b=gbMemory[0xFF30+(gbAudioWave.position>>1)];
+ var sample=(gbAudioWave.position&1)?b&15:b>>4;
+ return sample>>(gbAudioWave.volumeCode-1);
+ }
+ return gbAudioNoise.enabled && gbAudioNoise.dac ?
+ ((~gbAudioNoise.lfsr)&1)*gbAudioNoise.volume : null;
+}
+
+function gb_Audio_Mix_Sample() {
+ var route=gbMemory[0xFF25], volume=gbMemory[0xFF24], left=0, right=0;
+ for (var i=0;i<4;i++) {
+ var output=gb_Audio_Channel_Output(i);
+ if (output===null) continue;
+ var sample=output/7.5-1;
+ if (route&(1<>4)&7)+1;
+ right*=(volume&7)+1;
+ // There is no reason to retain PCM if the browser has no audio output.
+ if (!gbAudioNode) return;
+ gbAudioQueueL.push(left/32);
+ gbAudioQueueR.push(right/32);
+ if (gbAudioQueuePos>8192) {
+ gbAudioQueueL=gbAudioQueueL.slice(gbAudioQueuePos);
+ gbAudioQueueR=gbAudioQueueR.slice(gbAudioQueuePos);
+ gbAudioQueuePos=0;
+ }
+}
+
+function gb_Audio_Step(ticks) {
+ if (!gbAudioPowered) return;
+ gb_Audio_Advance_Square(gbAudioSquare1,ticks);
+ gb_Audio_Advance_Square(gbAudioSquare2,ticks);
+ gb_Audio_Advance_Wave(ticks);
+ gb_Audio_Advance_Noise(ticks);
+ gbAudioFrameCounter+=ticks;
+ while (gbAudioFrameCounter>=8192) { gbAudioFrameCounter-=8192; gb_Audio_Clock_Frame(); }
+ gbAudioSampleCounter+=ticks;
+ while (gbAudioSampleCounter>=gbAudioSampleTicks) {
+ gbAudioSampleCounter-=gbAudioSampleTicks;
+ gb_Audio_Mix_Sample();
+ }
+}
+
+function gb_Audio_Create_Output() {
+ if (gbAudioContext) return;
+ var AudioContext=window.AudioContext||window.webkitAudioContext;
+ if (!AudioContext) return;
+ gbAudioContext=new AudioContext();
+ gbAudioSampleRate=gbAudioContext.sampleRate;
+ gbAudioSampleTicks=gbAudioClock/gbAudioSampleRate;
+ gbAudioNode=gbAudioContext.createScriptProcessor(2048,0,2);
+ gbAudioNode.onaudioprocess=function(e) {
+ var left=e.outputBuffer.getChannelData(0), right=e.outputBuffer.getChannelData(1);
+ var available=gbAudioQueueL.length-gbAudioQueuePos;
+ // Start (or restart) only with a complete block ready. Keeping partial
+ // audio queued avoids repeatedly alternating between signal and silence.
+ if (available>4)&7; c.sweepNegate=!!(v&8); c.sweepShift=v&7; break;
+ case 0xFF11: c=gbAudioSquare1; c.duty=v>>6; c.length=64-(v&63); break;
+ case 0xFF12: c=gbAudioSquare1; c.dac=!!(v&0xF8); c.envelopePeriod=v&7; c.envelopeAdd=!!(v&8); if (!c.dac)c.enabled=false; break;
+ case 0xFF13: c=gbAudioSquare1; c.frequency=(c.frequency&0x700)|v; break;
+ case 0xFF14: c=gbAudioSquare1; c.frequency=(c.frequency&0xFF)|((v&7)<<8); c.lengthOn=!!(v&0x40); if(v&0x80)gb_Audio_Trigger_Square(c); break;
+ case 0xFF16: c=gbAudioSquare2; c.duty=v>>6; c.length=64-(v&63); break;
+ case 0xFF17: c=gbAudioSquare2; c.dac=!!(v&0xF8); c.envelopePeriod=v&7; c.envelopeAdd=!!(v&8); if (!c.dac)c.enabled=false; break;
+ case 0xFF18: c=gbAudioSquare2; c.frequency=(c.frequency&0x700)|v; break;
+ case 0xFF19: c=gbAudioSquare2; c.frequency=(c.frequency&0xFF)|((v&7)<<8); c.lengthOn=!!(v&0x40); if(v&0x80)gb_Audio_Trigger_Square(c); break;
+ case 0xFF1A: gbAudioWave.dac=!!(v&0x80); if(!gbAudioWave.dac)gbAudioWave.enabled=false; break;
+ case 0xFF1B: gbAudioWave.length=256-v; break;
+ case 0xFF1C: gbAudioWave.volumeCode=(v>>5)&3; break;
+ case 0xFF1D: gbAudioWave.frequency=(gbAudioWave.frequency&0x700)|v; break;
+ case 0xFF1E: gbAudioWave.frequency=(gbAudioWave.frequency&0xFF)|((v&7)<<8); gbAudioWave.lengthOn=!!(v&0x40); if(v&0x80)gb_Audio_Trigger_Wave(); break;
+ case 0xFF20: gbAudioNoise.length=64-(v&63); break;
+ case 0xFF21: c=gbAudioNoise; c.dac=!!(v&0xF8); c.envelopePeriod=v&7; c.envelopeAdd=!!(v&8); if(!c.dac)c.enabled=false; break;
+ case 0xFF22: c=gbAudioNoise; c.clockShift=v>>4; c.width7=!!(v&8); c.divisor=v&7; break;
+ case 0xFF23: gbAudioNoise.lengthOn=!!(v&0x40); if(v&0x80)gb_Audio_Trigger_Noise(); break;
+ }
+ gb_Audio_Set_Status();
+}
+
+function gb_Audio_Read(a) {
+ if (a>=0xFF30) return gbMemory[a];
+ var masks=[0x80,0x3F,0x00,0xFF,0xBF,0xFF,0x3F,0x00,0xFF,0xBF,
+ 0x7F,0xFF,0x9F,0xFF,0xBF,0xFF,0xFF,0x00,0x00,0xBF,0x00,0x00,0x70,0x00,0x00,0x00,0x70];
+ if (a===0xFF26) { gb_Audio_Set_Status(); return gbMemory[a]|0x70; }
+ return gbMemory[a]|masks[a-0xFF10];
+}
diff --git a/jsgb.gameboy.js b/jsgb.gameboy.js
index 4fb9419..50fd203 100755
--- a/jsgb.gameboy.js
+++ b/jsgb.gameboy.js
@@ -1,6 +1,8 @@
var gbRunInterval;
var gbFpsInterval;
+var gbFrameDuration=70224/4194304*1000;
+var gbNextFrameTime=0;
function gb_Frame() {
gbEndFrame=false;
@@ -22,23 +24,39 @@ function gb_Step(){
gb_Dump_All();
}
+function gb_Schedule_Frame() {
+ if (gbPause) return;
+ gb_Frame();
+ gbNextFrameTime+=gbFrameDuration;
+ var now=performance.now();
+ // Do not execute a large burst after the tab or the main thread was blocked.
+ if (gbNextFrameTime=0xFF10 && a<=0xFF3F) return gb_Audio_Read(a);
return gbMemory[a];
}
function gb_Memory_Read_MBC1_ROM(a) {
+ if (a>=0xFF10 && a<=0xFF3F) return gb_Audio_Read(a);
switch (a>>12) {
case 0:
case 1:
@@ -100,6 +102,10 @@ var MEMR = gb_Memory_Read_ROM_Only;
function MEMW(a,v) {
// Special registers+HRAM
if (a>=0xFF00) {
+ if (a>=0xFF10 && a<=0xFF3F) {
+ gb_Audio_Write(a,v);
+ return;
+ }
switch(a&0xFF) {
case 0x00: // FF00 P1 Joypad
//if(v==3)gbMemory[a]=0xF0; else // Fx->GB/GBP; 3x->SGB
@@ -321,4 +327,3 @@ function gb_Init_Memory() {
MEMW(0xFF4B,0x00); // WX
MEMW(0xFFFF,0x00); // IE
}
-
diff --git a/jsgb.timers.js b/jsgb.timers.js
index 759c6fd..768f0a8 100755
--- a/jsgb.timers.js
+++ b/jsgb.timers.js
@@ -74,6 +74,8 @@ function gb_LY_LYC_compare() { // LY - LYC Compare
function gb_TIMER_Control() {
+ gb_Audio_Step(gbCPUTicks);
+
// DIV control
if ((gbDIVTicks+=gbCPUTicks)>=256) {
gbDIVTicks-=256;