diff --git a/simulator/src/apu-worklet.ts b/simulator/src/apu-worklet.ts index adff32b..0b66b1f 100644 --- a/simulator/src/apu-worklet.ts +++ b/simulator/src/apu-worklet.ts @@ -1,274 +1,35 @@ "use strict"; -// Audio worklet file: do not export anything directly. -const SAMPLE_RATE = 44100; -const MAX_VOLUME = 0.15; -// The triangle channel sounds a bit quieter than the others, so give it higher amplitude -const MAX_VOLUME_TRIANGLE = 0.25; -// Also for the triangle channel, prevent popping on hard stops by adding a 1 ms release -const RELEASE_TIME_TRIANGLE = Math.floor(SAMPLE_RATE / 1000); - -class Channel { - /** Starting frequency. */ - freq1 = 0; - - /** Ending frequency, or zero for no frequency transition. */ - freq2 = 0; - - /** Time the tone was started. */ - startTime = 0; - - /** Time at the end of the attack period. */ - attackTime = 0; - - /** Time at the end of the decay period. */ - decayTime = 0; - - /** Time at the end of the sustain period. */ - sustainTime = 0; - - /** Time the tone should end. */ - releaseTime = 0; - - /** The tick the tone should end. */ - endTick = 0; - - /** Sustain volume level. */ - sustainVolume = 0; - - /** Peak volume level at the end of the attack phase. */ - peakVolume = 0; - - /** Used for time tracking. */ - phase = 0; - - /** Tone panning. 0 = center, 1 = only left, 2 = only right. */ - pan = 0; - - /** Duty cycle for pulse channels. */ - pulseDutyCycle = 0; - - /** Noise generation state. */ - noiseSeed = 0x0001; - - /** The last generated random number, either -1 or 1. */ - noiseLastRandom = 0; -} - -function lerp (value1: number, value2: number, t: number) { - return value1 + t * (value2 - value1); -} - -function polyblep (phase: number, phaseInc: number) { - if (phase < phaseInc) { - const t = phase / phaseInc; - return t+t - t*t; - } else if (phase > 1 - phaseInc) { - const t = (phase - (1 - phaseInc)) / phaseInc; - return 1 - (t+t - t*t); - } else { - return 1; - } -} - -function midiFreq (note: number, bend: number) { - return Math.pow(2, (note - 69 + bend / 256) / 12) * 440; -} - class APUProcessor extends AudioWorkletProcessor { - time: number; - ticks: number; - channels: Channel[]; + // multiple of 512 + samplesLeft: number[] = [] + samplesRight: number[] = [] constructor () { super(); - this.time = 0; - this.ticks = 0; - this.channels = new Array(4); - for (let ii = 0; ii < 4; ++ii) { - this.channels[ii] = new Channel(); - } - if (this.port != null) { - this.port.onmessage = (event: MessageEvent<"tick" | [number, number, number, number]>) => { - if (event.data == "tick") { - this.tick(); - } else { - this.tone(...event.data); - } + this.port.onmessage = (event: MessageEvent<{left: number[], right: number[]}>) => { + this.samplesLeft = this.samplesLeft.concat(event.data.left); + this.samplesRight = this.samplesRight.concat(event.data.right); }; } } - ramp (value1: number, value2: number, time1: number, time2: number) { - if (this.time >= time2) return value2; - const t = (this.time - time1) / (time2 - time1); - return lerp(value1, value2, t); - } - - getCurrentFrequency (channel: Channel) { - if (channel.freq2 > 0) { - return this.ramp(channel.freq1, channel.freq2, channel.startTime, channel.releaseTime); - } else { - return channel.freq1; - } - } - - getCurrentVolume (channel: Channel) { - const time = this.time; - if (time >= channel.sustainTime && (channel.releaseTime - channel.sustainTime) > RELEASE_TIME_TRIANGLE) { - // Release - return this.ramp(channel.sustainVolume, 0, channel.sustainTime, channel.releaseTime); - } else if (time >= channel.decayTime) { - // Sustain - return channel.sustainVolume; - } else if (time >= channel.attackTime) { - // Decay - return this.ramp(channel.peakVolume, channel.sustainVolume, channel.attackTime, channel.decayTime); - } else { - // Attack - return this.ramp(0, channel.peakVolume, channel.startTime, channel.attackTime); - } - } - - tick () { - this.ticks++; - } - - tone (frequency: number, duration: number, volume: number, flags: number) { - const freq1 = frequency & 0xffff; - const freq2 = (frequency >> 16) & 0xffff; - - const sustain = (duration & 0xff); - const release = ((duration >> 8) & 0xff); - const decay = ((duration >> 16) & 0xff); - const attack = ((duration >> 24) & 0xff); + /** + * Web standards only support [2][128]f32 but hardware (and thus the wasm code) runs with [2][512]u16 (but I think it's signed in reality?) + */ + process (_inputs: Float32Array[][], [[ outputLeft, outputRight ]]: Float32Array[][], _parameters: Record): boolean { + const pcmLeft = this.samplesLeft.splice(0, 128); + const pcmRight = this.samplesRight.splice(0, 128); - const sustainVolume = Math.min(volume & 0xff, 100); - const peakVolume = Math.min((volume >> 8) & 0xff, 100); - - const channelIdx = flags & 0x3; - const mode = (flags >> 2) & 0x3; - const pan = (flags >> 4) & 0x3; - const noteMode = flags & 0x40; - - const channel = this.channels[channelIdx]; - - // Restart the phase if this channel wasn't already playing - if (this.time > channel.releaseTime && this.ticks != channel.endTick) { - channel.phase = (channelIdx == 2) ? 0.25 : 0; + for (let index = 0; index < pcmLeft.length; index += 1) { + pcmLeft[index] = pcmLeft[index] / 32767; + pcmRight[index] = pcmRight[index] / 32767; } - if (noteMode) { - channel.freq1 = midiFreq(freq1 & 0xff, freq1 >> 8); - channel.freq2 = (freq2 == 0) ? 0 : midiFreq(freq2 & 0xff, freq2 >> 8); - } else { - channel.freq1 = freq1; - channel.freq2 = freq2; - } - channel.startTime = this.time; - channel.attackTime = channel.startTime + ((SAMPLE_RATE*attack/60) >>> 0); - channel.decayTime = channel.attackTime + ((SAMPLE_RATE*decay/60) >>> 0); - channel.sustainTime = channel.decayTime + ((SAMPLE_RATE*sustain/60) >>> 0); - channel.releaseTime = channel.sustainTime + ((SAMPLE_RATE*release/60) >>> 0); - channel.endTick = this.ticks + attack + decay + sustain + release; - channel.pan = pan; - - const maxVolume = (channelIdx == 2) ? MAX_VOLUME_TRIANGLE : MAX_VOLUME; - channel.sustainVolume = maxVolume * sustainVolume/100; - channel.peakVolume = peakVolume ? maxVolume * peakVolume/100 : maxVolume; - - if (channelIdx == 0 || channelIdx == 1) { - switch (mode) { - case 0: - channel.pulseDutyCycle = 0.125; - break; - case 1: case 3: default: - channel.pulseDutyCycle = 0.25; - break; - case 2: - channel.pulseDutyCycle = 0.5; - break; - } - - } else if (channelIdx == 2) { - if (release == 0) { - channel.releaseTime += RELEASE_TIME_TRIANGLE; - } - } - } - - process (_inputs: Float32Array[][], [[ outputLeft, outputRight ]]: Float32Array[][], _parameters: Record) { - for (let ii = 0, frames = outputLeft.length; ii < frames; ++ii, ++this.time) { - let mixLeft = 0, mixRight = 0; - - for (let channelIdx = 0; channelIdx < 4; ++channelIdx) { - const channel = this.channels[channelIdx]; - - if (this.time < channel.releaseTime || this.ticks == channel.endTick) { - const freq = this.getCurrentFrequency(channel); - const volume = this.getCurrentVolume(channel); - let sample; - - if (channelIdx == 3) { - // Noise channel - channel.phase += freq * freq / (1000000/44100 * SAMPLE_RATE); - while (channel.phase > 0) { - channel.phase--; - let noiseSeed = channel.noiseSeed; - noiseSeed ^= noiseSeed >> 7; - noiseSeed ^= noiseSeed << 9; - noiseSeed ^= noiseSeed >> 13; - channel.noiseSeed = noiseSeed; - channel.noiseLastRandom = ((noiseSeed & 0x1) << 1) - 1; - } - sample = volume * channel.noiseLastRandom; - - } else { - const phaseInc = freq / SAMPLE_RATE; - let phase = channel.phase + phaseInc; - - if (phase >= 1) { - phase--; - } - channel.phase = phase; - - if (channelIdx == 2) { - // Triangle channel - sample = volume * (2*Math.abs(2*channel.phase - 1) - 1); - - } else { - // Pulse channel - let dutyPhase, dutyPhaseInc, multiplier; - - // Map duty to 0->1 - const pulseDutyCycle = channel.pulseDutyCycle; - if (phase < pulseDutyCycle) { - dutyPhase = phase / pulseDutyCycle; - dutyPhaseInc = phaseInc / pulseDutyCycle; - multiplier = volume; - } else { - dutyPhase = (phase - pulseDutyCycle) / (1 - pulseDutyCycle); - dutyPhaseInc = phaseInc / (1 - pulseDutyCycle); - multiplier = -volume; - } - sample = multiplier * polyblep(dutyPhase, dutyPhaseInc); - } - } - - if (channel.pan != 1) { - mixRight += sample; - } - if (channel.pan != 2) { - mixLeft += sample; - } - } - } - - outputLeft[ii] = mixLeft; - outputRight[ii] = mixRight; - } + outputLeft.set(new Float32Array(pcmLeft)); + outputRight.set(new Float32Array(pcmRight)); return true; } diff --git a/simulator/src/apu.ts b/simulator/src/apu.ts index 1832394..9eb7320 100644 --- a/simulator/src/apu.ts +++ b/simulator/src/apu.ts @@ -23,12 +23,8 @@ export class APU { workletNode.connect(audioCtx.destination); } - tick() { - this.processorPort!.postMessage("tick"); - } - - tone(frequency: number, duration: number, volume: number, flags: number) { - this.processorPort!.postMessage([frequency, duration, volume, flags]); + send(left: number[], right: number[]) { + this.processorPort!.postMessage({left, right}); } unlockAudio() { diff --git a/simulator/src/constants.ts b/simulator/src/constants.ts index c5947e5..18fa4b1 100644 --- a/simulator/src/constants.ts +++ b/simulator/src/constants.ts @@ -12,6 +12,7 @@ export const ADDR_LIGHT_LEVEL = 0x06; export const ADDR_NEOPIXELS = 0x08; export const ADDR_RED_LED = 0x1c; export const ADDR_FRAMEBUFFER = 0x1e; +export const ADDR_AUDIO_BUFFER = 0xa01e; export const CONTROLS_START = 1; export const CONTROLS_SELECT = 2; diff --git a/simulator/src/runtime.ts b/simulator/src/runtime.ts index 7b24118..4343338 100644 --- a/simulator/src/runtime.ts +++ b/simulator/src/runtime.ts @@ -124,8 +124,6 @@ export class Runtime { blit: this.blit.bind(this), - tone: this.apu.tone.bind(this.apu), - read_flash: this.read_flash.bind(this), write_flash_page: this.write_flash_page.bind(this), @@ -224,6 +222,8 @@ export class Runtime { if (typeof start_function === "function") { this.bluescreenOnError(start_function); } + + new Uint16Array(this.memory.buffer).slice(constants.ADDR_AUDIO_BUFFER, constants.ADDR_AUDIO_BUFFER + 2 * 512).fill(0); } update () { @@ -235,7 +235,19 @@ export class Runtime { if (typeof update_function === "function") { this.bluescreenOnError(update_function); } - this.apu.tick(); + + // TODO: should this be called via a message from the worklet maybe? + let audio_function = this.wasm!.exports["audio"] as any; + // if (typeof audio_function === "function") { + // this.bluescreenOnError(audio_function); + // } + + if (audio_function(constants.ADDR_AUDIO_BUFFER, constants.ADDR_AUDIO_BUFFER + 512 * 2)) { + this.apu.send( + [...new Uint16Array(this.memory.buffer).slice(constants.ADDR_AUDIO_BUFFER, constants.ADDR_AUDIO_BUFFER + 512)], + [...new Uint16Array(this.memory.buffer).slice(constants.ADDR_AUDIO_BUFFER + 512, constants.ADDR_AUDIO_BUFFER + 512 * 2)], + ); + } } blueScreen (text: string) {