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;