From 88eb6cdc3cb51195ffdb391b4488441ff1ca0443 Mon Sep 17 00:00:00 2001 From: Marcus Ramse Date: Wed, 17 Apr 2024 16:11:51 +0000 Subject: [PATCH 1/2] Implement Note Mode for APU --- runtimes/native/src/apu.c | 34 +++++++++++---- runtimes/web/src/apu-worklet.ts | 16 +++++-- site/docs/guides/audio.md | 75 ++++++++++++++++++++++++++++++++ site/docs/reference/functions.md | 8 ++++ 4 files changed, 120 insertions(+), 13 deletions(-) diff --git a/runtimes/native/src/apu.c b/runtimes/native/src/apu.c index 3662b37f..d92590b8 100644 --- a/runtimes/native/src/apu.c +++ b/runtimes/native/src/apu.c @@ -9,10 +9,10 @@ typedef struct { /** Starting frequency. */ - uint16_t freq1; + float freq1; /** Ending frequency, or zero for no frequency transition. */ - uint16_t freq2; + float freq2; /** Time the tone was started. */ unsigned long long startTime; @@ -73,16 +73,23 @@ static int w4_min (int a, int b) { static int lerp (int value1, int value2, float t) { return value1 + t * (value2 - value1); } +static float lerpf (float value1, float value2, float t) { + return value1 + t * (value2 - value1); +} static int ramp (int value1, int value2, unsigned long long time1, unsigned long long time2) { if (time >= time2) return value2; float t = (float)(time - time1) / (time2 - time1); return lerp(value1, value2, t); } +static float rampf (float value1, float value2, unsigned long long time1, unsigned long long time2) { + float t = (float)(time - time1) / (time2 - time1); + return lerpf(value1, value2, t); +} -static uint16_t getCurrentFrequency (const Channel* channel) { +static float getCurrentFrequency (const Channel* channel) { if (channel->freq2 > 0) { - return ramp(channel->freq1, channel->freq2, channel->startTime, channel->releaseTime); + return rampf(channel->freq1, channel->freq2, channel->startTime, channel->releaseTime); } else { return channel->freq1; } @@ -116,6 +123,10 @@ static float polyblep (float phase, float phaseInc) { } } +static float midiFreq (uint8_t note, uint8_t bend) { + return powf(2.0f, ((float)note - 69.0f + (float)bend / 256.0f) / 12.0f) * 440.0f; +} + void w4_apuInit () { channels[3].noise.seed = 0x0001; } @@ -139,6 +150,7 @@ void w4_apuTone (int frequency, int duration, int volume, int flags) { int channelIdx = flags & 0x03; int mode = (flags >> 2) & 0x3; int pan = (flags >> 4) & 0x3; + int noteMode = flags & 0x40; // TODO(2022-01-08): Thread safety Channel* channel = &channels[channelIdx]; @@ -147,9 +159,13 @@ void w4_apuTone (int frequency, int duration, int volume, int flags) { if (time > channel->releaseTime && ticks != channel->endTick) { channel->phase = (channelIdx == 2) ? 0.25 : 0; } - - channel->freq1 = freq1; - channel->freq2 = freq2; + 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 = time; channel->attackTime = channel->startTime + SAMPLE_RATE*attack/60; channel->decayTime = channel->attackTime + SAMPLE_RATE*decay/60; @@ -190,7 +206,7 @@ void w4_apuWriteSamples (int16_t* output, unsigned long frames) { Channel* channel = &channels[channelIdx]; if (time < channel->releaseTime || ticks == channel->endTick) { - uint16_t freq = getCurrentFrequency(channel); + float freq = getCurrentFrequency(channel); int16_t volume = getCurrentVolume(channel); int16_t sample; @@ -207,7 +223,7 @@ void w4_apuWriteSamples (int16_t* output, unsigned long frames) { sample = volume * channel->noise.lastRandom; } else { - float phaseInc = (float)freq / SAMPLE_RATE; + float phaseInc = freq / SAMPLE_RATE; channel->phase += phaseInc; if (channel->phase >= 1) { diff --git a/runtimes/web/src/apu-worklet.ts b/runtimes/web/src/apu-worklet.ts index f201dbc0..803a9797 100644 --- a/runtimes/web/src/apu-worklet.ts +++ b/runtimes/web/src/apu-worklet.ts @@ -68,6 +68,10 @@ function polyblep (phase: number, phaseInc: number) { } } +function midiFreq (note: number, bend: number) { + return Math.pow(2, (note - 69 + bend / 256) / 12) * 440; +} + class APUProcessor extends AudioWorkletProcessor { time: number; ticks: number; @@ -132,7 +136,6 @@ class APUProcessor extends AudioWorkletProcessor { 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); @@ -144,6 +147,7 @@ class APUProcessor extends AudioWorkletProcessor { const channelIdx = flags & 0x3; const mode = (flags >> 2) & 0x3; const pan = (flags >> 4) & 0x3; + const noteMode = flags & 0x40; const channel = this.channels[channelIdx]; @@ -151,9 +155,13 @@ class APUProcessor extends AudioWorkletProcessor { if (this.time > channel.releaseTime && this.ticks != channel.endTick) { channel.phase = (channelIdx == 2) ? 0.25 : 0; } - - channel.freq1 = freq1; - channel.freq2 = freq2; + 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); diff --git a/site/docs/guides/audio.md b/site/docs/guides/audio.md index 9146706a..188a8188 100644 --- a/site/docs/guides/audio.md +++ b/site/docs/guides/audio.md @@ -409,6 +409,81 @@ w4.tone(262, 60, 100, w4.TONE_PULSE1 | w4.TONE_PAN_LEFT); +## Note Mode + +By enabling Note Mode with the `TONE_NOTE_MODE` flag, `tone` will use MIDI note numbers rather than frequencies. +This results in more accurate pitches when playing musical notes. + +You can read more about how this works in the [`tone(...)` documentation](../reference/functions#tone-frequency-duration-volume-flags). + +Here's the same example as before, now playing middle-C using the MIDI note number 60: + + + +```typescript +w4.tone(60, 60, 100, w4.TONE_PULSE1 | w4.TONE_NOTE_MODE); +``` + +```c +tone(60, 60, 100, TONE_PULSE1 | TONE_NOTE_MODE); +``` + +```c3 +w4::tone(60, 60, 100, w4::TONE_PULSE1 | w4::TONE_NOTE_MODE); +``` + +```d +w4.tone(60, 60, 100, w4.tonePulse1 | w4.toneNoteMode); +``` + +```go +w4.Tone(60, 60, 100, w4.TONE_PULSE1 | w4.TONE_NOTE_MODE) +``` + +```lua +tone(60, 60, 100, TONE_PULSE1 | TONE_NOTE_MODE) +``` + +```nim +tone(60, 60, 100, TONE_PULSE1 or TONE_NOTE_MODE) +``` + +```odin +w4.tone(60, 60, 100, .Pulse1, .Half, .Left, .Note) +``` + +```penne +tone(60, 60, 100, TONE_PULSE1 | TONE_NOTE_MODE); +``` + +```porth +$TONE_NOTE_MODE $TONE_PULSE1 or 100 60 60 tone +``` + +```roland +tone(60, 60, 100, TONE_PULSE1 | TONE_NOTE_MODE); +``` + +```rust +tone(60, 60, 100, TONE_PULSE1 | TONE_NOTE_MODE); +``` + +```wasm +(call $tone + (i32.const 60) + (i32.const 60) + (i32.const 100) + (i32.or + (global.get $TONE_PULSE1) + (global.get $TONE_NOTE_MODE))) +``` + +```zig +w4.tone(60, 60, 100, w4.TONE_PULSE1 | w4.TONE_NOTE_MODE); +``` + + + ## Calculating Flags Setting ADSR flags require the use of various bitwise and bitshift operations. This can be a little confusing to understand. diff --git a/site/docs/reference/functions.md b/site/docs/reference/functions.md index d81c4f39..fe04b0ce 100644 --- a/site/docs/reference/functions.md +++ b/site/docs/reference/functions.md @@ -113,6 +113,7 @@ Plays a sound tone. | 0 - 1 | Channel (0-3): 0 = Pulse1, 1 = Pulse2, 2 = Triangle, 3 = Noise | | 2 - 3 | Mode (0-3): For pulse channels, the pulse wave duty cycle. 0 = 1/8, 1 = 1/4, 2 = 1/2, 3 = 3/4 | | 4 - 5 | Pan (0-2): 0 = Center, 1 = Left, 2 = Right | +| 6 | Use *Note Mode* for frequencies: See below. | The high bits of `frequency` can optionally describe a pitch slide effect: @@ -123,6 +124,13 @@ The high bits of `frequency` can optionally describe a pitch slide effect: If the end frequency is non-zero, then the frequency is ramped linearly over the total duration of the tone. +If *Note Mode* is enabled, both the Start and End frequency values are instead interpreted as notes with pitch bend rather than frequencies: + +| Frequency bits | Description | +| --- | --- | +| 0 - 7 | Note (0-255): Note number according to the MIDI specification, e.g. 60 = C4, 69 = A4 (440 Hz) | +| 8 - 15 | Note bend (0-255): Bend note upwards. 0 = Nothing, 255 = One 256th away from the next note above | + The high bits of `duration` can optionally describe an ADSR volume envelope: | Duration bits | Description | From 537cda5335b068a14078a4d9d5fe1b16dac4c47e Mon Sep 17 00:00:00 2001 From: Marcus Ramse Date: Wed, 17 Apr 2024 16:12:21 +0000 Subject: [PATCH 2/2] Add Note Mode to templates --- cli/assets/templates/assemblyscript/src/wasm4.ts | 1 + cli/assets/templates/c/src/wasm4.h | 1 + cli/assets/templates/c3/cart/src/wasm4.c3 | 1 + cli/assets/templates/d/source/wasm4.d | 1 + cli/assets/templates/go/w4/wasm4.go | 1 + cli/assets/templates/nelua/src/wasm4.nelua | 1 + cli/assets/templates/nim/src/cart/wasm4.nim | 1 + cli/assets/templates/odin/src/w4/wasm4_wasm32.odin | 12 ++++++++---- cli/assets/templates/penne/src/wasm4.pn | 1 + cli/assets/templates/rust/src/wasm4.rs | 1 + cli/assets/templates/wat/main.wat | 1 + cli/assets/templates/zig/src/wasm4.zig | 1 + 12 files changed, 19 insertions(+), 4 deletions(-) diff --git a/cli/assets/templates/assemblyscript/src/wasm4.ts b/cli/assets/templates/assemblyscript/src/wasm4.ts index e29ed870..83b62ac0 100644 --- a/cli/assets/templates/assemblyscript/src/wasm4.ts +++ b/cli/assets/templates/assemblyscript/src/wasm4.ts @@ -121,6 +121,7 @@ export const TONE_MODE3: u32 = 8; export const TONE_MODE4: u32 = 12; export const TONE_PAN_LEFT: u32 = 16; export const TONE_PAN_RIGHT: u32 = 32; +export const TONE_NOTE_MODE: u32 = 64; // ┌───────────────────────────────────────────────────────────────────────────┐ // │ │ diff --git a/cli/assets/templates/c/src/wasm4.h b/cli/assets/templates/c/src/wasm4.h index abfb43ab..0c8796d3 100644 --- a/cli/assets/templates/c/src/wasm4.h +++ b/cli/assets/templates/c/src/wasm4.h @@ -117,6 +117,7 @@ void tone (uint32_t frequency, uint32_t duration, uint32_t volume, uint32_t flag #define TONE_MODE4 12 #define TONE_PAN_LEFT 16 #define TONE_PAN_RIGHT 32 +#define TONE_NOTE_MODE 64 // ┌───────────────────────────────────────────────────────────────────────────┐ // │ │ diff --git a/cli/assets/templates/c3/cart/src/wasm4.c3 b/cli/assets/templates/c3/cart/src/wasm4.c3 index 60c55c40..c5627f35 100644 --- a/cli/assets/templates/c3/cart/src/wasm4.c3 +++ b/cli/assets/templates/c3/cart/src/wasm4.c3 @@ -97,6 +97,7 @@ const TONE_MODE3 = 8; const TONE_MODE4 = 12; const TONE_PAN_LEFT = 16; const TONE_PAN_RIGHT = 32; +const TONE_NOTE_MODE = 64; // ┌───────────────────────────────────────────────────────────────────────────┐ // │ │ diff --git a/cli/assets/templates/d/source/wasm4.d b/cli/assets/templates/d/source/wasm4.d index 0414dfcd..866ccb74 100644 --- a/cli/assets/templates/d/source/wasm4.d +++ b/cli/assets/templates/d/source/wasm4.d @@ -101,6 +101,7 @@ enum toneMode3 = 8; enum toneMode4 = 12; enum tonePanLeft = 16; enum tonePanRight = 32; +enum toneNoteMode = 64; // ┌───────────────────────────────────────────────────────────────────────────┐ // │ │ diff --git a/cli/assets/templates/go/w4/wasm4.go b/cli/assets/templates/go/w4/wasm4.go index 877c35ec..f6962e58 100644 --- a/cli/assets/templates/go/w4/wasm4.go +++ b/cli/assets/templates/go/w4/wasm4.go @@ -111,6 +111,7 @@ const TONE_MODE3 = 8 const TONE_MODE4 = 12 const TONE_PAN_LEFT = 16 const TONE_PAN_RIGHT = 32 +const TONE_NOTE_MODE = 64 // ┌───────────────────────────────────────────────────────────────────────────┐ // │ │ diff --git a/cli/assets/templates/nelua/src/wasm4.nelua b/cli/assets/templates/nelua/src/wasm4.nelua index d8240bac..2bbd29ba 100644 --- a/cli/assets/templates/nelua/src/wasm4.nelua +++ b/cli/assets/templates/nelua/src/wasm4.nelua @@ -122,6 +122,7 @@ global TONE_MODE3 = 8 global TONE_MODE4 = 12 global TONE_PAN_LEFT = 16 global TONE_PAN_RIGHT = 32 +global TONE_NOTE_MODE = 64 -- ┌───────────────────────────────────────────────────────────────────────────┐ -- │ │ diff --git a/cli/assets/templates/nim/src/cart/wasm4.nim b/cli/assets/templates/nim/src/cart/wasm4.nim index 4a89492a..49c438b4 100644 --- a/cli/assets/templates/nim/src/cart/wasm4.nim +++ b/cli/assets/templates/nim/src/cart/wasm4.nim @@ -48,6 +48,7 @@ const TONE_MODE4* = 12 TONE_PAN_LEFT* = 16 TONE_PAN_RIGHT* = 32 + TONE_NOTE_MODE* = 64 {.push importc, codegenDecl: "__attribute__((import_name(\"$2\"))) $1 $2$3".} proc blit*(data: ptr uint8; x: int32; y: int32; width: uint32; height: uint32; diff --git a/cli/assets/templates/odin/src/w4/wasm4_wasm32.odin b/cli/assets/templates/odin/src/w4/wasm4_wasm32.odin index 7f947d0a..3184f66e 100644 --- a/cli/assets/templates/odin/src/w4/wasm4_wasm32.odin +++ b/cli/assets/templates/odin/src/w4/wasm4_wasm32.odin @@ -118,6 +118,10 @@ Tone_Pan :: enum u32 { Left = 16, Right = 32, } +Tone_Mode :: enum u32 { + Frequency = 0, + Note = 64, +} Tone_Duration :: struct { attack: u8, // in frames @@ -135,13 +139,13 @@ foreign wasm4 { } // Plays a sound tone. -tone :: proc "c" (frequency: u32, duration: u32, volume_percent: u32, channel: Tone_Channel, duty_cycle := Tone_Duty_Cycle.Eigth, pan := Tone_Pan.Center) { - flags := u32(channel) | u32(duty_cycle) | u32(pan) +tone :: proc "c" (frequency: u32, duration: u32, volume_percent: u32, channel: Tone_Channel, duty_cycle := Tone_Duty_Cycle.Eigth, pan := Tone_Pan.Center, tone_mode := Tone_Mode.Frequency) { + flags := u32(channel) | u32(duty_cycle) | u32(pan) | u32(tone_mode) internal_tone(frequency, duration, volume_percent, flags) } -tone_complex :: proc "c" (start_frequency, end_frequency: u16, duration: Tone_Duration, volume_percent: u32, channel: Tone_Channel, duty_cycle := Tone_Duty_Cycle.Eigth, pan := Tone_Pan.Center) { - flags := u32(channel) | u32(duty_cycle) | u32(pan) +tone_complex :: proc "c" (start_frequency, end_frequency: u16, duration: Tone_Duration, volume_percent: u32, channel: Tone_Channel, duty_cycle := Tone_Duty_Cycle.Eigth, pan := Tone_Pan.Center, tone_mode := Tone_Mode.Frequency) { + flags := u32(channel) | u32(duty_cycle) | u32(pan) | u32(tone_mode) frequency := u32(start_frequency) | u32(end_frequency)<<16 duration_in_frames := u32(duration.attack)<<24 | u32(duration.delay)<<16 | u32(duration.release)<<8 | u32(duration.sustain) diff --git a/cli/assets/templates/penne/src/wasm4.pn b/cli/assets/templates/penne/src/wasm4.pn index b096bf44..cfeb3ccb 100644 --- a/cli/assets/templates/penne/src/wasm4.pn +++ b/cli/assets/templates/penne/src/wasm4.pn @@ -103,6 +103,7 @@ pub const TONE_MODE3: u32 = 8; pub const TONE_MODE4: u32 = 12; pub const TONE_PAN_LEFT: u32 = 16; pub const TONE_PAN_RIGHT: u32 = 32; +pub const TONE_NOTE_MODE: u32 = 64; // ┌───────────────────────────────────────────────────────────────────────────┐ // │ │ diff --git a/cli/assets/templates/rust/src/wasm4.rs b/cli/assets/templates/rust/src/wasm4.rs index eb378192..1342cbdd 100644 --- a/cli/assets/templates/rust/src/wasm4.rs +++ b/cli/assets/templates/rust/src/wasm4.rs @@ -193,6 +193,7 @@ pub const TONE_MODE3: u32 = 8; pub const TONE_MODE4: u32 = 12; pub const TONE_PAN_LEFT: u32 = 16; pub const TONE_PAN_RIGHT: u32 = 32; +pub const TONE_NOTE_MODE: u32 = 64; // ┌───────────────────────────────────────────────────────────────────────────┐ // │ │ diff --git a/cli/assets/templates/wat/main.wat b/cli/assets/templates/wat/main.wat index 59ef7eab..d418a67d 100644 --- a/cli/assets/templates/wat/main.wat +++ b/cli/assets/templates/wat/main.wat @@ -109,6 +109,7 @@ (global $TONE_MODE4 i32 (i32.const 12)) (global $TONE_PAN_LEFT i32 (i32.const 16)) (global $TONE_PAN_RIGHT i32 (i32.const 32)) +(global $TONE_NOTE_MODE i32 (i32.const 64)) ;; smiley diff --git a/cli/assets/templates/zig/src/wasm4.zig b/cli/assets/templates/zig/src/wasm4.zig index e5c40c5e..25f836dd 100644 --- a/cli/assets/templates/zig/src/wasm4.zig +++ b/cli/assets/templates/zig/src/wasm4.zig @@ -100,6 +100,7 @@ pub const TONE_MODE3: u32 = 8; pub const TONE_MODE4: u32 = 12; pub const TONE_PAN_LEFT: u32 = 16; pub const TONE_PAN_RIGHT: u32 = 32; +pub const TONE_NOTE_MODE: u32 = 64; // ┌───────────────────────────────────────────────────────────────────────────┐ // │ │