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.