diff --git a/example/nes-embed.html b/example/nes-embed.html index 7f2ef1fa..4f356cc3 100644 --- a/example/nes-embed.html +++ b/example/nes-embed.html @@ -1,18 +1,19 @@ - - - - - - Embedding Example - - - - - - -
- -
-

DPad: Arrow keys
Start: Return, Select: Tab
A Button: A, B Button: S

- - + + + + + + + Embedding Example + + + + + + +
+ +
+

DPad: Arrow keys
Start: Return, Select: Shift
A Button: X, B Button: Z

+ + diff --git a/example/nes-embed.js b/example/nes-embed.js index 27aabd52..9263dc56 100644 --- a/example/nes-embed.js +++ b/example/nes-embed.js @@ -2,6 +2,7 @@ var SCREEN_WIDTH = 256; var SCREEN_HEIGHT = 240; var FRAMEBUFFER_SIZE = SCREEN_WIDTH*SCREEN_HEIGHT; +var nes; var canvas_ctx, image; var framebuffer_u8, framebuffer_u32; @@ -12,20 +13,9 @@ var audio_samples_L = new Float32Array(SAMPLE_COUNT); var audio_samples_R = new Float32Array(SAMPLE_COUNT); var audio_write_cursor = 0, audio_read_cursor = 0; -var nes = new jsnes.NES({ - onFrame: function(framebuffer_24){ - for(var i = 0; i < FRAMEBUFFER_SIZE; i++) framebuffer_u32[i] = 0xFF000000 | framebuffer_24[i]; - }, - onAudioSample: function(l, r){ - audio_samples_L[audio_write_cursor] = l; - audio_samples_R[audio_write_cursor] = r; - audio_write_cursor = (audio_write_cursor + 1) & SAMPLE_MASK; - }, -}); - function onAnimationFrame(){ window.requestAnimationFrame(onAnimationFrame); - + image.data.set(framebuffer_u8); canvas_ctx.putImageData(image, 0, 0); } @@ -37,10 +27,10 @@ function audio_remain(){ function audio_callback(event){ var dst = event.outputBuffer; var len = dst.length; - + // Attempt to avoid buffer underruns. if(audio_remain() < AUDIO_BUFFERING) nes.frame(); - + var dst_l = dst.getChannelData(0); var dst_r = dst.getChannelData(1); for(var i = 0; i < len; i++){ @@ -48,7 +38,7 @@ function audio_callback(event){ dst_l[i] = audio_samples_L[src_idx]; dst_r[i] = audio_samples_R[src_idx]; } - + audio_read_cursor = (audio_read_cursor + len) & SAMPLE_MASK; } @@ -63,38 +53,54 @@ function keyboard(callback, event){ callback(player, jsnes.Controller.BUTTON_LEFT); break; case 39: // Right callback(player, jsnes.Controller.BUTTON_RIGHT); break; - case 65: // 'a' - qwerty, dvorak - case 81: // 'q' - azerty + case 88: // A = X callback(player, jsnes.Controller.BUTTON_A); break; - case 83: // 's' - qwerty, azerty - case 79: // 'o' - dvorak + case 90: // B = Z callback(player, jsnes.Controller.BUTTON_B); break; - case 9: // Tab + case 16: // SELECT = Shift callback(player, jsnes.Controller.BUTTON_SELECT); break; - case 13: // Return + case 13: // START = Return callback(player, jsnes.Controller.BUTTON_START); break; default: break; } } function nes_init(canvas_id){ + var audio_ctx = new window.AudioContext({ + latencyHint: "interactive", + }); + + nes = new jsnes.NES({ + onFrame: function(framebuffer_24){ + for(var i = 0; i < FRAMEBUFFER_SIZE; i++) framebuffer_u32[i] = 0xFF000000 | framebuffer_24[i]; + }, + onAudioSample: function(l, r){ + audio_samples_L[audio_write_cursor] = l; + audio_samples_R[audio_write_cursor] = r; + audio_write_cursor = (audio_write_cursor + 1) & SAMPLE_MASK; + }, + sampleRate: audio_ctx.sampleRate, + }); + var canvas = document.getElementById(canvas_id); canvas_ctx = canvas.getContext("2d"); image = canvas_ctx.getImageData(0, 0, SCREEN_WIDTH, SCREEN_HEIGHT); - + canvas_ctx.fillStyle = "black"; canvas_ctx.fillRect(0, 0, SCREEN_WIDTH, SCREEN_HEIGHT); - + // Allocate framebuffer array. var buffer = new ArrayBuffer(image.data.length); framebuffer_u8 = new Uint8ClampedArray(buffer); framebuffer_u32 = new Uint32Array(buffer); - + // Setup audio. - var audio_ctx = new window.AudioContext(); var script_processor = audio_ctx.createScriptProcessor(AUDIO_BUFFERING, 0, 2); script_processor.onaudioprocess = audio_callback; script_processor.connect(audio_ctx.destination); + + document.addEventListener('keydown', (event) => {keyboard(nes.buttonDown, event)}); + document.addEventListener('keyup', (event) => {keyboard(nes.buttonUp, event)}); } function nes_boot(rom_data){ @@ -109,24 +115,45 @@ function nes_load_data(canvas_id, rom_data){ function nes_load_url(canvas_id, path){ nes_init(canvas_id); - + var req = new XMLHttpRequest(); req.open("GET", path); req.overrideMimeType("text/plain; charset=x-user-defined"); req.onerror = () => console.log(`Error loading ${path}: ${req.statusText}`); - + req.onload = function() { if (this.status === 200) { - nes_boot(this.responseText); + nes_boot(this.responseText); } else if (this.status === 0) { // Aborted, so ignore error } else { req.onerror(); } }; - + req.send(); } -document.addEventListener('keydown', (event) => {keyboard(nes.buttonDown, event)}); -document.addEventListener('keyup', (event) => {keyboard(nes.buttonUp, event)}); +function nes_load_nvram(item){ + if (nes != null && nes.cpu != null && nes.cpu.mem != null && window.localStorage != null) { + let data = window.localStorage.getItem("jsnes-nvram-" + item); + if (data != null){ + data = JSON.parse(data); + for (let i = 0; i < 8192; i++){ + nes.cpu.mem[24576 + i] = data.mem[i]; //$6000-$7FFF (2K) + } + } + } +} + +function nes_save_nvram(item){ + if (nes != null && nes.cpu != null && nes.cpu.mem != null && window.localStorage != null) { + window.localStorage.setItem("jsnes-nvram-" + item, JSON.stringify({ mem: nes.cpu.mem.slice(24576, 32768)})); //$6000-$7FFF (2K) + } +} + +function nes_volume(value){ + if (nes != null && nes.papu != null) { + nes.papu.setMasterVolume(value); + } +} diff --git a/src/nes.js b/src/nes.js index 1cdd30cc..fee1ac63 100644 --- a/src/nes.js +++ b/src/nes.js @@ -11,11 +11,8 @@ var NES = function (opts) { onStatusUpdate: function () {}, onBatteryRamWrite: function () {}, - // FIXME: not actually used except for in PAPU - preferredFrameRate: 60, - emulateSound: true, - sampleRate: 48000, // Sound sample rate in hz + sampleRate: 48000, }; if (typeof opts !== "undefined") { var key; @@ -26,8 +23,6 @@ var NES = function (opts) { } } - this.frameTime = 1000 / this.opts.preferredFrameRate; - this.ui = { writeFrame: this.opts.onFrame, updateStatus: this.opts.onStatusUpdate, @@ -78,6 +73,7 @@ NES.prototype = { }, frame: function () { + if (!this.mmap) return; this.ppu.startFrame(); var cycles = 0; var emulateSound = this.opts.emulateSound; @@ -164,7 +160,7 @@ NES.prototype = { getFPS: function () { var now = +new Date(); - var fps = null; + var fps = 0; if (this.lastFpsTime) { fps = this.fpsFrameCount / ((now - this.lastFpsTime) / 1000); } @@ -193,12 +189,6 @@ NES.prototype = { this.romData = data; }, - setFramerate: function (rate) { - this.opts.preferredFrameRate = rate; - this.frameTime = 1000 / rate; - this.papu.setSampleRate(this.opts.sampleRate, false); - }, - toJSON: function () { return { // romData: this.romData, diff --git a/src/papu.js b/src/papu.js index 2579eb88..59ca4132 100644 --- a/src/papu.js +++ b/src/papu.js @@ -2,6 +2,8 @@ var utils = require("./utils"); var CPU_FREQ_NTSC = 1789772.5; //1789772.72727272d; // var CPU_FREQ_PAL = 1773447.4; +var APU_TO_CPU_CYCLE_NTSC = 14915; +// var APU_TO_CPU_CYCLE_PAL = 16627; var PAPU = function (nes) { this.nes = nes; @@ -17,7 +19,7 @@ var PAPU = function (nes) { this.initCounter = 2048; this.channelEnableValue = null; - this.sampleRate = 44100; + this.sampleRate = 48000; this.lengthLookup = null; this.dmcFreqLookup = null; @@ -103,13 +105,10 @@ PAPU.prototype = { reset: function () { this.sampleRate = this.nes.opts.sampleRate; this.sampleTimerMax = Math.floor( - (1024.0 * CPU_FREQ_NTSC * this.nes.opts.preferredFrameRate) / - (this.sampleRate * 60.0) + (1024.0 * CPU_FREQ_NTSC) / this.sampleRate ); - this.frameTime = Math.floor( - (14915.0 * this.nes.opts.preferredFrameRate) / 60.0 - ); + this.frameTime = APU_TO_CPU_CYCLE_NTSC; this.sampleTimer = 0; @@ -383,7 +382,7 @@ PAPU.prototype = { // Clock frame counter at double CPU speed: this.masterFrameCounter += nCycles << 1; if (this.masterFrameCounter >= this.frameTime) { - // 240Hz tick: + // 240Hz (NTSC) tick: this.masterFrameCounter -= this.frameTime; this.frameCounterTick(); } @@ -468,7 +467,7 @@ PAPU.prototype = { this.frameIrqActive = true; } - // End of 240Hz tick + // End of 240Hz (NSTC) tick }, // Samples the channels, mixes the output together, then writes to buffer.