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 |