diff --git a/build.zig b/build.zig index 214a659..5fbde48 100644 --- a/build.zig +++ b/build.zig @@ -196,7 +196,7 @@ pub fn add_cart( wasm.initial_memory = 64 * 65536; wasm.max_memory = 64 * 65536; wasm.stack_size = 14752; - wasm.global_base = 160 * 128 * 2 + 0x1e; + wasm.global_base = 2 * 512 * 2 + 0xa01e; wasm.rdynamic = true; wasm.root_module.addImport("cart-api", d.module("cart-api")); 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) { diff --git a/src/badge/cart.zig b/src/badge/cart.zig index d740951..c354a83 100644 --- a/src/badge/cart.zig +++ b/src/badge/cart.zig @@ -47,7 +47,6 @@ pub fn svcall_handler() callconv(.Naked) void { \\ .byte (4f - 0b) / 2 \\ .byte (5f - 0b) / 2 \\ .byte (6f - 0b) / 2 - \\ .byte (7f - 0b) / 2 \\ .byte (8f - 0b) / 2 \\ .byte (9f - 0b) / 2 \\ .byte (10f - 0b) / 2 @@ -77,9 +76,6 @@ pub fn svcall_handler() callconv(.Naked) void { \\6: \\ ldm r1, {r0-r3} \\ b %[vline:P] - \\7: - \\ ldm r1, {r0-r3} - \\ b %[tone:P] \\8: \\ ldm r1, {r0-r2} \\ b %[read_flash:P] @@ -115,7 +111,6 @@ pub fn svcall_handler() callconv(.Naked) void { [text] "X" (&text), [hline] "X" (&hline), [vline] "X" (&vline), - [tone] "X" (&tone), [read_flash] "X" (&read_flash), [write_flash_page] "X" (&write_flash_page), [rand] "X" (&rand), @@ -546,92 +541,6 @@ fn vline( @memset(api.framebuffer[@intCast(x)][@max(y, 0)..@intCast(@min(end_y, api.screen_height))], pixel); } -fn tone( - frequency: u32, - duration: u32, - volume: u32, - flags: api.ToneOptions.Flags, -) callconv(.C) void { - const start_frequency: u16 = @truncate(frequency >> 0); - const end_frequency = switch (@as(u16, @truncate(frequency >> 16))) { - 0 => start_frequency, - else => |end_frequency| end_frequency, - }; - const sustain_time: u8 = @truncate(duration >> 0); - const release_time: u8 = @truncate(duration >> 8); - const decay_time: u8 = @truncate(duration >> 16); - const attack_time: u8 = @truncate(duration >> 24); - const total_time = @as(u10, attack_time) + decay_time + sustain_time + release_time; - const sustain_volume: u8 = @truncate(volume >> 0); - const peak_volume = switch (@as(u8, @truncate(volume >> 8))) { - 0 => 100, - else => |attack_volume| attack_volume, - }; - - var state: audio.Channel = .{ - .duty = 0, - .phase = 0, - .phase_step = 0, - .phase_step_step = 0, - - .duration = 0, - .attack_duration = 0, - .decay_duration = 0, - .sustain_duration = 0, - .release_duration = 0, - - .volume = 0, - .volume_step = 0, - .peak_volume = 0, - .sustain_volume = 0, - .attack_volume_step = 0, - .decay_volume_step = 0, - .release_volume_step = 0, - }; - - const start_phase_step = @mulWithOverflow((1 << 32) / 44100, @as(u31, start_frequency)); - const end_phase_step = @mulWithOverflow((1 << 32) / 44100, @as(u31, end_frequency)); - if (start_phase_step[1] != 0 or end_phase_step[1] != 0) return; - state.phase_step = start_phase_step[0]; - state.phase_step_step = @divTrunc(@as(i32, end_phase_step[0]) - start_phase_step[0], @as(u20, total_time) * @divExact(44100, 60)); - - state.attack_duration = @as(u18, attack_time) * @divExact(44100, 60); - state.decay_duration = @as(u18, decay_time) * @divExact(44100, 60); - state.sustain_duration = @as(u18, sustain_time) * @divExact(44100, 60); - state.release_duration = @as(u18, release_time) * @divExact(44100, 60); - - state.peak_volume = @as(u29, peak_volume) << 21; - state.sustain_volume = @as(u29, sustain_volume) << 21; - if (state.attack_duration > 0) { - state.attack_volume_step = @divTrunc(@as(i32, state.peak_volume) - 0, state.attack_duration); - } - if (state.decay_duration > 0) { - state.decay_volume_step = @divTrunc(@as(i32, state.sustain_volume) - state.peak_volume, state.decay_duration); - } - if (state.release_duration > 0) { - state.release_volume_step = @divTrunc(@as(i32, 0) - state.sustain_volume, state.release_duration); - } - - switch (flags.channel) { - .pulse1, .pulse2 => { - state.duty = switch (flags.duty_cycle) { - .@"1/8" => (1 << 32) / 8, - .@"1/4" => (1 << 32) / 4, - .@"1/2" => (1 << 32) / 2, - .@"3/4" => (3 << 32) / 4, - }; - }, - .triangle => { - state.duty = (1 << 32) / 2; - }, - .noise => { - state.duty = (1 << 32) / 2; - }, - } - - audio.set_channel(@intFromEnum(flags.channel), state); -} - fn read_flash( offset: u32, dst_ptr: [*]User(u8), diff --git a/src/badge/demos/song.zig b/src/badge/demos/song.zig index 33240cd..1bdbd8e 100644 --- a/src/badge/demos/song.zig +++ b/src/badge/demos/song.zig @@ -187,6 +187,7 @@ export fn update() void { &channels_note_start, &song, ) |channel_index, *note_index, *note_start, notes| { + _ = channel_index; // autofix if (note_index.* > notes.len) continue; const next_note_time = note_start.* + if (note_index.* > 0) notes[note_index.* - 1].duration else 0.0; @@ -194,15 +195,16 @@ export fn update() void { note_index.* += 1; if (note_index.* > notes.len) continue; const note = notes[note_index.* - 1]; + _ = note; // autofix note_start.* = next_note_time; - cart.tone(.{ - .frequency = @intFromFloat(note.frequency + 0.5), - .duration = @intFromFloat(@max(note.duration - 0.04, 0.0) * 60), - .volume = 100, - .flags = .{ - .channel = @enumFromInt(channel_index), - }, - }); + // cart.tone(.{ + // .frequency = @intFromFloat(note.frequency + 0.5), + // .duration = @intFromFloat(@max(note.duration - 0.04, 0.0) * 60), + // .volume = 100, + // .flags = .{ + // .channel = @enumFromInt(channel_index), + // }, + // }); } } } diff --git a/src/badge/feature_test.zig b/src/badge/feature_test.zig index d221bb0..c0d0c07 100644 --- a/src/badge/feature_test.zig +++ b/src/badge/feature_test.zig @@ -23,16 +23,16 @@ fn write_stored_number(number: u64) void { export fn update() void { if (offset % (60 * 2) == 0) { - cart.tone(.{ - .frequency = 440, - .duration = 20, - .volume = 10, - .flags = .{ - .channel = .pulse1, - .duty_cycle = .@"1/8", - .panning = .left, - }, - }); + // cart.tone(.{ + // .frequency = 440, + // .duration = 20, + // .volume = 10, + // .flags = .{ + // .channel = .pulse1, + // .duty_cycle = .@"1/8", + // .panning = .left, + // }, + // }); } offset +%= 1; @@ -154,3 +154,15 @@ export fn update() void { .background_color = .{ .r = 31, .g = 63, .b = 31 }, }); } + +var off: f32 = 0; +export fn audio(buffer: *volatile [2][512]u16) bool { + for (&buffer[0], &buffer[1]) |*l, *r| { + l.* = @intFromFloat(@sin(off) * std.math.maxInt(u16)); + r.* = @intFromFloat(@sin(off) * std.math.maxInt(u16)); + + off += 0.1; + } + + return false; +} diff --git a/src/cart/api.zig b/src/cart/api.zig index 4e56e4f..2c1644f 100644 --- a/src/cart/api.zig +++ b/src/cart/api.zig @@ -1,24 +1,19 @@ +//! API for interacting with the badge. +//! +//! To get started, define the following functions: +//! - `export fn start() void {}` +//! - `export fn update() void {}` +//! - `export fn audio(buffer: *volatile [2][512]u16) bool {}` + const std = @import("std"); const builtin = @import("builtin"); -// ┌───────────────────────────────────────────────────────────────────────────┐ -// │ │ -// │ Platform Constants │ -// │ │ -// └───────────────────────────────────────────────────────────────────────────┘ - pub const screen_width: u32 = 160; pub const screen_height: u32 = 128; pub const font_width: u32 = 8; pub const font_height: u32 = 8; -// ┌───────────────────────────────────────────────────────────────────────────┐ -// │ │ -// │ Memory Addresses │ -// │ │ -// └───────────────────────────────────────────────────────────────────────────┘ - /// RGB888, true color pub const NeopixelColor = extern struct { g: u8, r: u8, b: u8 }; @@ -92,12 +87,6 @@ pub const red_led: *bool = @ptrFromInt(base + 0x1c); pub const battery_level: *u12 = @ptrFromInt(base + 0x1e); pub const framebuffer: *volatile [screen_width][screen_height]Pixel = @ptrFromInt(base + 0x20); -// ┌───────────────────────────────────────────────────────────────────────────┐ -// │ │ -// │ Drawing Functions │ -// │ │ -// └───────────────────────────────────────────────────────────────────────────┘ - pub const BlitOptions = struct { pub const Flags = packed struct(u32) { flip_x: bool = false, @@ -426,82 +415,6 @@ pub inline fn vline(options: StraightLineOptions) void { } } -// ┌───────────────────────────────────────────────────────────────────────────┐ -// │ │ -// │ Sound Functions │ -// │ │ -// └───────────────────────────────────────────────────────────────────────────┘ - -pub const ToneOptions = struct { - pub const Flags = packed struct(u32) { - pub const Channel = enum(u2) { - pulse1, - pulse2, - triangle, - noise, - }; - - pub const DutyCycle = enum(u2) { - @"1/8", - @"1/4", - @"1/2", - @"3/4", - }; - - pub const Panning = enum(u2) { - stereo, - left, - right, - }; - - channel: Channel, - /// `duty_cycle` is only used when `channel` is set to `pulse1` or `pulse2` - duty_cycle: DutyCycle = .@"1/8", - panning: Panning = .stereo, - padding: u26 = undefined, - }; - - frequency: u32, - duration: u32, - volume: u32, - flags: Flags, -}; - -/// Plays a sound tone. -pub inline fn tone(options: ToneOptions) void { - if (builtin.target.isWasm()) { - struct { - extern fn tone(frequency: u32, duration: u32, volume: u32, flags: ToneOptions.Flags) void; - }.tone( - options.frequency, - options.duration, - options.volume, - options.flags, - ); - } else { - var clobber_r0: usize = undefined; - var clobber_r1: usize = undefined; - var clobber_r2: usize = undefined; - var clobber_r3: usize = undefined; - asm volatile (" svc #7" - : [clobber_r0] "={r0}" (clobber_r0), - [clobber_r1] "={r1}" (clobber_r1), - [clobber_r2] "={r2}" (clobber_r2), - [clobber_r3] "={r3}" (clobber_r3), - : [frequency] "{r0}" (options.frequency), - [duration] "{r1}" (options.duration), - [volume] "{r2}" (options.volume), - [flags] "{r3}" (options.flags), - ); - } -} - -// ┌───────────────────────────────────────────────────────────────────────────┐ -// │ │ -// │ Storage Functions │ -// │ │ -// └───────────────────────────────────────────────────────────────────────────┘ - pub const flash_page_size = 256; pub const flash_page_count = 8000; @@ -544,12 +457,6 @@ pub inline fn write_flash_page(page: u16, src: [flash_page_size]u8) void { } } -// ┌───────────────────────────────────────────────────────────────────────────┐ -// │ │ -// │ Other Functions │ -// │ │ -// └───────────────────────────────────────────────────────────────────────────┘ - /// Returns a random number, useful for seeding a faster prng. pub inline fn rand() u32 { if (builtin.target.isWasm()) {