Skip to content

Commit

Permalink
WIP raw pcm
Browse files Browse the repository at this point in the history
  • Loading branch information
SuperAuguste committed May 19, 2024
1 parent 58a59a8 commit 06c94ad
Show file tree
Hide file tree
Showing 4 changed files with 35 additions and 265 deletions.
273 changes: 17 additions & 256 deletions simulator/src/apu-worklet.ts
Original file line number Diff line number Diff line change
@@ -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<string, Float32Array>): 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<string, Float32Array>) {
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;
}
Expand Down
8 changes: 2 additions & 6 deletions simulator/src/apu.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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() {
Expand Down
1 change: 1 addition & 0 deletions simulator/src/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
18 changes: 15 additions & 3 deletions simulator/src/runtime.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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),

Expand Down Expand Up @@ -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 () {
Expand All @@ -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) {
Expand Down

0 comments on commit 06c94ad

Please sign in to comment.