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 = {
|
||||
Controller: require("./controller"),
|
||||
NES: require("./nes"),
|
||||
Sample: require("./sample"),
|
||||
};
|
||||
|
|
|
@ -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 {
|
||||
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(); }
|
||||
}
|
||||
},
|
||||
|
||||
|
|
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": {
|
||||
"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",
|
||||
|
|
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>
|
||||
import { loadSample } from './lib/sampload.js'
|
||||
import NES from './lib/NES.svelte'
|
||||
|
||||
let rom = $state(null);
|
||||
let opts = $state({});
|
||||
|
||||
function nes_load_url(url) {
|
||||
// todo: port to fetch
|
||||
|
@ -21,12 +23,23 @@
|
|||
};
|
||||
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>
|
||||
|
||||
<main>
|
||||
<NES {rom}/>
|
||||
<button onclick={() => nes_load_url("smb.nes")}>Start!</button>
|
||||
<NES {rom} {opts}/>
|
||||
<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>
|
||||
</main>
|
||||
|
||||
|
|
|
@ -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 };
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<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