diff --git a/Tetris.nes b/Tetris.nes new file mode 100644 index 0000000..8336a66 Binary files /dev/null and b/Tetris.nes differ diff --git a/jsnes/src/index.js b/jsnes/src/index.js index b69b1ff..c8ef3a9 100644 --- a/jsnes/src/index.js +++ b/jsnes/src/index.js @@ -1,4 +1,5 @@ module.exports = { Controller: require("./controller"), NES: require("./nes"), + Sample: require("./sample"), }; diff --git a/jsnes/src/papu.js b/jsnes/src/papu.js index 2579eb8..318a717 100644 --- a/jsnes/src/papu.js +++ b/jsnes/src/papu.js @@ -316,23 +316,31 @@ PAPU.prototype = { } // Clock Square channel 1 Prog timer: + var square1sample = this.nes.opts[square1.sampleName]; square1.progTimerCount -= nCycles; if (square1.progTimerCount <= 0) { square1.progTimerCount += (square1.progTimerMax + 1) << 1; + if (square1sample) { square1sample.advance(4); } square1.squareCounter++; square1.squareCounter &= 0x7; square1.updateSampleValue(); + } else if (square1sample) { + square1.updateSampleValue(); } // Clock Square channel 2 Prog timer: + var square2sample = this.nes.opts[square2.sampleName]; square2.progTimerCount -= nCycles; if (square2.progTimerCount <= 0) { square2.progTimerCount += (square2.progTimerMax + 1) << 1; + if (square2sample) { square2sample.advance(4); } square2.squareCounter++; square2.squareCounter &= 0x7; square2.updateSampleValue(); + } else if (square2sample) { + square2.updateSampleValue(); } // Clock noise channel Prog timer: @@ -499,6 +507,9 @@ PAPU.prototype = { this.noise.accValue = smpNoise >> 4; this.noise.accCount = 1; + var extra = 0; + if (this.nes.opts.sampleSquare1) { this.smpSquare1 = 0; extra += this.square1.sampleValue; } + if (this.nes.opts.sampleSquare2) { this.smpSquare2 = 0; extra += this.square2.sampleValue; } // Stereo sound. // Left channel: @@ -560,7 +571,7 @@ PAPU.prototype = { } if (this.nes.opts.onAudioSample) { - this.nes.opts.onAudioSample(sampleValueL / 32768, sampleValueR / 32768); + this.nes.opts.onAudioSample((sampleValueL / 32768) + extra, (sampleValueR / 32768) + extra); } // Reset sampled values: @@ -1156,6 +1167,7 @@ ChannelNoise.prototype = { var ChannelSquare = function (papu, square1) { this.papu = papu; + this.sampleName = `sampleSquare${square1 ? 1 : 2}`; // prettier-ignore this.dutyLookup = [ @@ -1292,6 +1304,7 @@ ChannelSquare.prototype = { }, updateSampleValue: function () { + var sample = this.papu.nes.opts[this.sampleName]; if (this.isEnabled && this.lengthCounter > 0 && this.progTimerMax > 7) { if ( this.sweepMode === 0 && @@ -1299,13 +1312,25 @@ ChannelSquare.prototype = { ) { //if (this.sweepCarry) { this.sampleValue = 0; + if (sample) { sample.reset(); } } else { - this.sampleValue = - this.masterVolume * - this.dutyLookup[(this.dutyMode << 3) + this.squareCounter]; + if (sample) { + if (this.envVolume === 0 || this.masterVolume === 0) { + this.sampleValue = 0; + sample.reset(); + } else { + this.sampleValue = sample.sample(this.progTimerCount, (this.progTimerMax + 1) << 1, this.masterVolume); + // console.log("sampled value:", this.sampleValue); + } + } else { + this.sampleValue = + this.masterVolume * + this.dutyLookup[(this.dutyMode << 3) + this.squareCounter]; + } } } else { this.sampleValue = 0; + if (sample) { sample.reset(); } } }, diff --git a/jsnes/src/sample.js b/jsnes/src/sample.js new file mode 100644 index 0000000..a881eaf --- /dev/null +++ b/jsnes/src/sample.js @@ -0,0 +1,52 @@ + +var Sample = function (audiobuffer, toneHz, gain) { + var samples = new Float32Array(audiobuffer.getChannelData(0)); + for (var chan = 1; chan < audiobuffer.numberOfChannels; chan ++) { + var channel_samples = audiobuffer.getChannelData(chan); + for (var i = 0; i < samples.length; i ++) { + samples[i] += channel_samples[i] + 1; + } + } + gain = (gain ? gain : 1) / (audiobuffer.numberOfChannels * 2); + for (i = 0; i < samples.length; i ++) { + samples[i] = samples[i] * gain; + } + + this.samples = samples; + this.index = 0; + this.increment = audiobuffer.sampleRate / toneHz; + this.prevMaxTimer = null; + this.clocksPerPeriod = 1; +} + +Sample.prototype = { + clone: function() { + var copy = Object.create(this); + copy.index = 0; + return copy; + }, + advance: function(clocksPerPeriod) { + this.clocksPerPeriod = clocksPerPeriod; + this.index += this.increment / clocksPerPeriod; + }, + reset: function() { this.index = 0; }, + sample: function(timer, maxTimer, volume) { + if (maxTimer !== this.prevMaxTimer) { + if (this.prevMaxTimer) { + var semitoneInterval = 1.059463; + const semitoneUp = this.prevMaxTimer * semitoneInterval; + const semitoneDown = this.prevMaxTimer / semitoneInterval; + if (maxTimer <= semitoneDown || maxTimer >= semitoneUp) { + this.index = 0; + } + } + this.prevMaxTimer = maxTimer; + } + var index = (this.index + (((this.increment / this.clocksPerPeriod) * (maxTimer - timer)) / maxTimer)) >> 0; + if (index < 0) index = 0; + if (index >= this.samples.length) index = this.samples.length - 1; + return (this.samples[index] * volume); + } +} + +module.exports = Sample; \ No newline at end of file diff --git a/package.json b/package.json index 9ab88c0..97a54cc 100644 --- a/package.json +++ b/package.json @@ -6,10 +6,11 @@ "scripts": { "dev": "vite", "build": "vite build", - "preview": "vite preview" + "preview": "vite preview", + "rebuild": "cd jsnes && yarn && cd .. && yarn upgrade jsnes && vite --force" }, "dependencies": { - "jsnes": "file:../jsnes" + "jsnes": "file:./jsnes" }, "devDependencies": { "@sveltejs/vite-plugin-svelte": "^4.0.0", diff --git a/samples/YEARGH.WAV b/samples/YEARGH.WAV new file mode 100644 index 0000000..7e6417f Binary files /dev/null and b/samples/YEARGH.WAV differ diff --git a/samples/meow1.wav b/samples/meow1.wav new file mode 100644 index 0000000..2e25a4a Binary files /dev/null and b/samples/meow1.wav differ diff --git a/src/App.svelte b/src/App.svelte index 1cb6c92..4ce6e78 100644 --- a/src/App.svelte +++ b/src/App.svelte @@ -1,7 +1,9 @@
- - + + + + + +

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

diff --git a/src/lib/NES.svelte b/src/lib/NES.svelte index 87fbf50..8be1c1e 100644 --- a/src/lib/NES.svelte +++ b/src/lib/NES.svelte @@ -3,7 +3,7 @@ import { onMount } from 'svelte'; let nes = $state(); - let { rom } = $props(); + let { rom, opts } = $props(); const SCREEN_WIDTH = 256; const SCREEN_HEIGHT = 240; @@ -81,10 +81,10 @@ } let emu = new jsnes.NES({ - onFrame: function(framebuffer_24){ + onFrame: function(framebuffer_24) { for(var i = 0; i < FRAMEBUFFER_SIZE; i++) framebuffer_u32[i] = 0xFF000000 | framebuffer_24[i]; }, - onAudioSample: function(l, r){ + 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; @@ -105,6 +105,7 @@ canvas_ctx.putImageData(image, 0, 0); } nes = { + baseOpts: emu.opts, emu, activate() { this.stopAnimating(); @@ -140,6 +141,11 @@ } } }) + $effect(() => { + if (nes) { + nes.emu.opts = { ...opts, ...nes.baseOpts }; + } + })
diff --git a/src/lib/sampload.js b/src/lib/sampload.js new file mode 100644 index 0000000..db3e223 --- /dev/null +++ b/src/lib/sampload.js @@ -0,0 +1,8 @@ +import { Sample } from 'jsnes'; + +export async function loadSample(url, toneHz, gain) { + const audioCtx = new AudioContext(); + const response = await fetch(url); + const buffer = await audioCtx.decodeAudioData(await response.arrayBuffer()); + return new Sample(buffer, toneHz, gain); +} \ No newline at end of file diff --git a/yarn.lock b/yarn.lock index 9529f00..7ba2cc7 100644 --- a/yarn.lock +++ b/yarn.lock @@ -357,7 +357,7 @@ is-reference@^3.0.3: dependencies: "@types/estree" "^1.0.6" -"jsnes@file:../jsnes": +"jsnes@file:./jsnes": version "1.2.1" kleur@^4.1.5: