From c29f364957599a112132690bd46a8f3a2c50ffff Mon Sep 17 00:00:00 2001 From: Jeremy Penner Date: Tue, 29 Oct 2024 21:20:35 -0400 Subject: [PATCH] adapt simple embedding example (bring your own smb.nes) --- .gitignore | 3 +- index.html | 18 +++++++ nes-embed.js | 132 +++++++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 152 insertions(+), 1 deletion(-) create mode 100644 index.html create mode 100644 nes-embed.js diff --git a/.gitignore b/.gitignore index ff51edf..279311c 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,2 @@ -.direnv \ No newline at end of file +.direnv +smb.nes \ No newline at end of file diff --git a/index.html b/index.html new file mode 100644 index 0000000..5ff7fd6 --- /dev/null +++ b/index.html @@ -0,0 +1,18 @@ + + + + + + Embedding Example + + + + + +
+ +
+ +

DPad: Arrow keys
B button: Z, A button: X, Select: Tab / C, Start: Return / V

+ + diff --git a/nes-embed.js b/nes-embed.js new file mode 100644 index 0000000..dfd60f1 --- /dev/null +++ b/nes-embed.js @@ -0,0 +1,132 @@ +var SCREEN_WIDTH = 256; +var SCREEN_HEIGHT = 240; +var FRAMEBUFFER_SIZE = SCREEN_WIDTH*SCREEN_HEIGHT; + +var canvas_ctx, image; +var framebuffer_u8, framebuffer_u32; + +var AUDIO_BUFFERING = 512; +var SAMPLE_COUNT = 4*1024; +var SAMPLE_MASK = SAMPLE_COUNT - 1; +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); +} + +function audio_remain(){ + return (audio_write_cursor - audio_read_cursor) & SAMPLE_MASK; +} + +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++){ + var src_idx = (audio_read_cursor + i) & SAMPLE_MASK; + 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; +} + +const KEY_MAPPINGS = [{ + "ArrowUp": "BUTTON_UP", + "ArrowDown": "BUTTON_DOWN", + "ArrowLeft": "BUTTON_LEFT", + "ArrowRight": "BUTTON_RIGHT", + "KeyZ": "BUTTON_B", + "KeyX": "BUTTON_A", + "KeyC": "BUTTON_SELECT", + "KeyV": "BUTTON_START", + "Enter": "BUTTON_START", + "Tab": "BUTTON_SELECT" +}] + +function keyboard(callback, event){ + var player = 1; + for (const mapping of KEY_MAPPINGS) { + const button = mapping[event.code]; + if (button) { + callback(player, jsnes.Controller[button]); + event.preventDefault(); + } + player ++; + } +} + +function nes_init(canvas_id){ + 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); +} + +function nes_boot(rom_data){ + nes.loadROM(rom_data); + window.requestAnimationFrame(onAnimationFrame); +} + +function nes_load_data(canvas_id, rom_data){ + nes_init(canvas_id); + nes_boot(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); + } 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)});