Skip to content

Commit

Permalink
Fix stuff
Browse files Browse the repository at this point in the history
- fix mmap is null error (do not emulate frame without rom present)
- use device native samplerate for correct speed and eliminate resampler lag
- use static 60fps since NTSC (60Hz) cpu freq is already statically declared in papu

embed example:
- change default keybinds to the commonly used B=Z / A=X / START=ENTER / SELECT=SHIFT setup
- make canvas pixel perfect (2x size)
- add example functions for changing master volume and loading and saving nvram to window.localStorage
  • Loading branch information
anzz1 committed Mar 31, 2024
1 parent d8021d0 commit 7d05725
Show file tree
Hide file tree
Showing 4 changed files with 87 additions and 70 deletions.
37 changes: 19 additions & 18 deletions example/nes-embed.html
Original file line number Diff line number Diff line change
@@ -1,18 +1,19 @@
<!DOCTYPE html>

<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
<title>Embedding Example</title>

<script type="text/javascript" src="https://unpkg.com/jsnes/dist/jsnes.min.js"></script>
<script type="text/javascript" src="nes-embed.js"></script>
<script>window.onload = function(){nes_load_url("nes-canvas", "InterglacticTransmissing.nes");}</script>
</head>
<body>
<div style="margin: auto; width: 75%;">
<canvas id="nes-canvas" width="256" height="240" style="width: 100%"/>
</div>
<p>DPad: Arrow keys<br/>Start: Return, Select: Tab<br/>A Button: A, B Button: S</p>
</body>
</html>
<!DOCTYPE html>

<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1,maximum-scale=1,user-scalable=0"/>
<title>Embedding Example</title>

<script type="text/javascript" src="https://unpkg.com/jsnes/dist/jsnes.min.js"></script>
<script type="text/javascript" src="nes-embed.js"></script>
<script>window.onload = function(){nes_load_url("nes-canvas", "InterglacticTransmissing.nes");}</script>
</head>
<body>
<center>
<canvas id="nes-canvas" width="256" height="240" style="width: 512px; image-rendering: crisp-edges; image-rendering: pixelated; image-rendering: optimizespeed;"></canvas>
</center>
<p>DPad: Arrow keys<br/>Start: Return, Select: Shift<br/>A Button: X, B Button: Z</p>
</body>
</html>
89 changes: 58 additions & 31 deletions example/nes-embed.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand All @@ -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);
}
Expand All @@ -37,18 +27,18 @@ 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++){
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;
}

Expand All @@ -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){
Expand All @@ -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);
}
}
16 changes: 3 additions & 13 deletions src/nes.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -26,8 +23,6 @@ var NES = function (opts) {
}
}

this.frameTime = 1000 / this.opts.preferredFrameRate;

this.ui = {
writeFrame: this.opts.onFrame,
updateStatus: this.opts.onStatusUpdate,
Expand Down Expand Up @@ -78,6 +73,7 @@ NES.prototype = {
},

frame: function () {
if (!this.mmap) return;
this.ppu.startFrame();
var cycles = 0;
var emulateSound = this.opts.emulateSound;
Expand Down Expand Up @@ -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);
}
Expand Down Expand Up @@ -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,
Expand Down
15 changes: 7 additions & 8 deletions src/papu.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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;
Expand Down Expand Up @@ -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;

Expand Down Expand Up @@ -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();
}
Expand Down Expand Up @@ -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.
Expand Down

0 comments on commit 7d05725

Please sign in to comment.