Add sampling capability to square wave channels
This commit is contained in:
parent
8772714135
commit
82591aa870
BIN
Tetris.nes
Normal file
BIN
Tetris.nes
Normal file
Binary file not shown.
|
@ -1,4 +1,5 @@
|
||||||
module.exports = {
|
module.exports = {
|
||||||
Controller: require("./controller"),
|
Controller: require("./controller"),
|
||||||
NES: require("./nes"),
|
NES: require("./nes"),
|
||||||
|
Sample: require("./sample"),
|
||||||
};
|
};
|
||||||
|
|
|
@ -316,23 +316,31 @@ PAPU.prototype = {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Clock Square channel 1 Prog timer:
|
// Clock Square channel 1 Prog timer:
|
||||||
|
var square1sample = this.nes.opts[square1.sampleName];
|
||||||
square1.progTimerCount -= nCycles;
|
square1.progTimerCount -= nCycles;
|
||||||
if (square1.progTimerCount <= 0) {
|
if (square1.progTimerCount <= 0) {
|
||||||
square1.progTimerCount += (square1.progTimerMax + 1) << 1;
|
square1.progTimerCount += (square1.progTimerMax + 1) << 1;
|
||||||
|
if (square1sample) { square1sample.advance(4); }
|
||||||
|
|
||||||
square1.squareCounter++;
|
square1.squareCounter++;
|
||||||
square1.squareCounter &= 0x7;
|
square1.squareCounter &= 0x7;
|
||||||
square1.updateSampleValue();
|
square1.updateSampleValue();
|
||||||
|
} else if (square1sample) {
|
||||||
|
square1.updateSampleValue();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Clock Square channel 2 Prog timer:
|
// Clock Square channel 2 Prog timer:
|
||||||
|
var square2sample = this.nes.opts[square2.sampleName];
|
||||||
square2.progTimerCount -= nCycles;
|
square2.progTimerCount -= nCycles;
|
||||||
if (square2.progTimerCount <= 0) {
|
if (square2.progTimerCount <= 0) {
|
||||||
square2.progTimerCount += (square2.progTimerMax + 1) << 1;
|
square2.progTimerCount += (square2.progTimerMax + 1) << 1;
|
||||||
|
if (square2sample) { square2sample.advance(4); }
|
||||||
|
|
||||||
square2.squareCounter++;
|
square2.squareCounter++;
|
||||||
square2.squareCounter &= 0x7;
|
square2.squareCounter &= 0x7;
|
||||||
square2.updateSampleValue();
|
square2.updateSampleValue();
|
||||||
|
} else if (square2sample) {
|
||||||
|
square2.updateSampleValue();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Clock noise channel Prog timer:
|
// Clock noise channel Prog timer:
|
||||||
|
@ -499,6 +507,9 @@ PAPU.prototype = {
|
||||||
this.noise.accValue = smpNoise >> 4;
|
this.noise.accValue = smpNoise >> 4;
|
||||||
this.noise.accCount = 1;
|
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.
|
// Stereo sound.
|
||||||
|
|
||||||
// Left channel:
|
// Left channel:
|
||||||
|
@ -560,7 +571,7 @@ PAPU.prototype = {
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.nes.opts.onAudioSample) {
|
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:
|
// Reset sampled values:
|
||||||
|
@ -1156,6 +1167,7 @@ ChannelNoise.prototype = {
|
||||||
|
|
||||||
var ChannelSquare = function (papu, square1) {
|
var ChannelSquare = function (papu, square1) {
|
||||||
this.papu = papu;
|
this.papu = papu;
|
||||||
|
this.sampleName = `sampleSquare${square1 ? 1 : 2}`;
|
||||||
|
|
||||||
// prettier-ignore
|
// prettier-ignore
|
||||||
this.dutyLookup = [
|
this.dutyLookup = [
|
||||||
|
@ -1292,6 +1304,7 @@ ChannelSquare.prototype = {
|
||||||
},
|
},
|
||||||
|
|
||||||
updateSampleValue: function () {
|
updateSampleValue: function () {
|
||||||
|
var sample = this.papu.nes.opts[this.sampleName];
|
||||||
if (this.isEnabled && this.lengthCounter > 0 && this.progTimerMax > 7) {
|
if (this.isEnabled && this.lengthCounter > 0 && this.progTimerMax > 7) {
|
||||||
if (
|
if (
|
||||||
this.sweepMode === 0 &&
|
this.sweepMode === 0 &&
|
||||||
|
@ -1299,13 +1312,25 @@ ChannelSquare.prototype = {
|
||||||
) {
|
) {
|
||||||
//if (this.sweepCarry) {
|
//if (this.sweepCarry) {
|
||||||
this.sampleValue = 0;
|
this.sampleValue = 0;
|
||||||
|
if (sample) { sample.reset(); }
|
||||||
} else {
|
} else {
|
||||||
this.sampleValue =
|
if (sample) {
|
||||||
this.masterVolume *
|
if (this.envVolume === 0 || this.masterVolume === 0) {
|
||||||
this.dutyLookup[(this.dutyMode << 3) + this.squareCounter];
|
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 {
|
} else {
|
||||||
this.sampleValue = 0;
|
this.sampleValue = 0;
|
||||||
|
if (sample) { sample.reset(); }
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|
52
jsnes/src/sample.js
Normal file
52
jsnes/src/sample.js
Normal file
|
@ -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;
|
|
@ -6,10 +6,11 @@
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "vite",
|
"dev": "vite",
|
||||||
"build": "vite build",
|
"build": "vite build",
|
||||||
"preview": "vite preview"
|
"preview": "vite preview",
|
||||||
|
"rebuild": "cd jsnes && yarn && cd .. && yarn upgrade jsnes && vite --force"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"jsnes": "file:../jsnes"
|
"jsnes": "file:./jsnes"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@sveltejs/vite-plugin-svelte": "^4.0.0",
|
"@sveltejs/vite-plugin-svelte": "^4.0.0",
|
||||||
|
|
BIN
samples/YEARGH.WAV
Normal file
BIN
samples/YEARGH.WAV
Normal file
Binary file not shown.
BIN
samples/meow1.wav
Normal file
BIN
samples/meow1.wav
Normal file
Binary file not shown.
|
@ -1,7 +1,9 @@
|
||||||
<script>
|
<script>
|
||||||
|
import { loadSample } from './lib/sampload.js'
|
||||||
import NES from './lib/NES.svelte'
|
import NES from './lib/NES.svelte'
|
||||||
|
|
||||||
let rom = $state(null);
|
let rom = $state(null);
|
||||||
|
let opts = $state({});
|
||||||
|
|
||||||
function nes_load_url(url) {
|
function nes_load_url(url) {
|
||||||
// todo: port to fetch
|
// todo: port to fetch
|
||||||
|
@ -21,12 +23,23 @@
|
||||||
};
|
};
|
||||||
req.send();
|
req.send();
|
||||||
}
|
}
|
||||||
|
async function meow() {
|
||||||
|
const sample = await loadSample("samples/meow1.wav", 1400, 5);
|
||||||
|
opts = { sampleSquare1: sample, sampleSquare2: sample.clone() };
|
||||||
|
}
|
||||||
|
async function yeargh() {
|
||||||
|
const sample = await loadSample("samples/YEARGH.WAV", 1400, 1);
|
||||||
|
opts = { sampleSquare1: sample, sampleSquare2: sample.clone() };
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<main>
|
<main>
|
||||||
<NES {rom}/>
|
<NES {rom} {opts}/>
|
||||||
<button onclick={() => nes_load_url("smb.nes")}>Start!</button>
|
<button onclick={() => nes_load_url("smb.nes")}>Mario</button>
|
||||||
|
<button onclick={() => nes_load_url("Tetris.nes")}>Tetris</button>
|
||||||
|
<button onclick={() => { opts = {} }}>Normal</button>
|
||||||
|
<button onclick={meow}>Meow</button>
|
||||||
|
<button onclick={yeargh}>Yeargh</button>
|
||||||
<p>DPad: Arrow keys<br/>B button: Z, A button: X, Select: Tab / C, Start: Return / V</p>
|
<p>DPad: Arrow keys<br/>B button: Z, A button: X, Select: Tab / C, Start: Return / V</p>
|
||||||
</main>
|
</main>
|
||||||
|
|
||||||
|
|
|
@ -3,7 +3,7 @@
|
||||||
import { onMount } from 'svelte';
|
import { onMount } from 'svelte';
|
||||||
|
|
||||||
let nes = $state();
|
let nes = $state();
|
||||||
let { rom } = $props();
|
let { rom, opts } = $props();
|
||||||
|
|
||||||
const SCREEN_WIDTH = 256;
|
const SCREEN_WIDTH = 256;
|
||||||
const SCREEN_HEIGHT = 240;
|
const SCREEN_HEIGHT = 240;
|
||||||
|
@ -81,10 +81,10 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
let emu = new jsnes.NES({
|
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];
|
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_L[audio_write_cursor] = l;
|
||||||
audio_samples_R[audio_write_cursor] = r;
|
audio_samples_R[audio_write_cursor] = r;
|
||||||
audio_write_cursor = (audio_write_cursor + 1) & SAMPLE_MASK;
|
audio_write_cursor = (audio_write_cursor + 1) & SAMPLE_MASK;
|
||||||
|
@ -105,6 +105,7 @@
|
||||||
canvas_ctx.putImageData(image, 0, 0);
|
canvas_ctx.putImageData(image, 0, 0);
|
||||||
}
|
}
|
||||||
nes = {
|
nes = {
|
||||||
|
baseOpts: emu.opts,
|
||||||
emu,
|
emu,
|
||||||
activate() {
|
activate() {
|
||||||
this.stopAnimating();
|
this.stopAnimating();
|
||||||
|
@ -140,6 +141,11 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
$effect(() => {
|
||||||
|
if (nes) {
|
||||||
|
nes.emu.opts = { ...opts, ...nes.baseOpts };
|
||||||
|
}
|
||||||
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<main>
|
<main>
|
||||||
|
|
8
src/lib/sampload.js
Normal file
8
src/lib/sampload.js
Normal file
|
@ -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);
|
||||||
|
}
|
Loading…
Reference in a new issue