Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

APU Note Mode (alternative version of #334) #706

Merged
merged 2 commits into from
Apr 18, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions cli/assets/templates/assemblyscript/src/wasm4.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;

// ┌───────────────────────────────────────────────────────────────────────────┐
// │ │
Expand Down
1 change: 1 addition & 0 deletions cli/assets/templates/c/src/wasm4.h
Original file line number Diff line number Diff line change
Expand Up @@ -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

// ┌───────────────────────────────────────────────────────────────────────────┐
// │ │
Expand Down
1 change: 1 addition & 0 deletions cli/assets/templates/c3/cart/src/wasm4.c3
Original file line number Diff line number Diff line change
Expand Up @@ -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;

// ┌───────────────────────────────────────────────────────────────────────────┐
// │ │
Expand Down
1 change: 1 addition & 0 deletions cli/assets/templates/d/source/wasm4.d
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,7 @@ enum toneMode3 = 8;
enum toneMode4 = 12;
enum tonePanLeft = 16;
enum tonePanRight = 32;
enum toneNoteMode = 64;

// ┌───────────────────────────────────────────────────────────────────────────┐
// │ │
Expand Down
1 change: 1 addition & 0 deletions cli/assets/templates/go/w4/wasm4.go
Original file line number Diff line number Diff line change
Expand Up @@ -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

// ┌───────────────────────────────────────────────────────────────────────────┐
// │ │
Expand Down
1 change: 1 addition & 0 deletions cli/assets/templates/nelua/src/wasm4.nelua
Original file line number Diff line number Diff line change
Expand Up @@ -122,6 +122,7 @@ global TONE_MODE3 <comptime> = 8
global TONE_MODE4 <comptime> = 12
global TONE_PAN_LEFT <comptime> = 16
global TONE_PAN_RIGHT <comptime> = 32
global TONE_NOTE_MODE <comptime> = 64

-- ┌───────────────────────────────────────────────────────────────────────────┐
-- │ │
Expand Down
1 change: 1 addition & 0 deletions cli/assets/templates/nim/src/cart/wasm4.nim
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
12 changes: 8 additions & 4 deletions cli/assets/templates/odin/src/w4/wasm4_wasm32.odin
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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)

Expand Down
1 change: 1 addition & 0 deletions cli/assets/templates/penne/src/wasm4.pn
Original file line number Diff line number Diff line change
Expand Up @@ -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;

// ┌───────────────────────────────────────────────────────────────────────────┐
// │ │
Expand Down
1 change: 1 addition & 0 deletions cli/assets/templates/rust/src/wasm4.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;

// ┌───────────────────────────────────────────────────────────────────────────┐
// │ │
Expand Down
1 change: 1 addition & 0 deletions cli/assets/templates/wat/main.wat
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
1 change: 1 addition & 0 deletions cli/assets/templates/zig/src/wasm4.zig
Original file line number Diff line number Diff line change
Expand Up @@ -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;

// ┌───────────────────────────────────────────────────────────────────────────┐
// │ │
Expand Down
34 changes: 25 additions & 9 deletions runtimes/native/src/apu.c
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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;
}
Expand Down Expand Up @@ -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;
}
Expand All @@ -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];
Expand All @@ -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;
Expand Down Expand Up @@ -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;

Expand All @@ -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) {
Expand Down
16 changes: 12 additions & 4 deletions runtimes/web/src/apu-worklet.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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);
Expand All @@ -144,16 +147,21 @@ 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];

// 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;
}

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);
Expand Down
75 changes: 75 additions & 0 deletions site/docs/guides/audio.md
Original file line number Diff line number Diff line change
Expand Up @@ -409,6 +409,81 @@ w4.tone(262, 60, 100, w4.TONE_PULSE1 | w4.TONE_PAN_LEFT);

</MultiLanguageCode>

## 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:

<MultiLanguageCode>

```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);
```

</MultiLanguageCode>

## Calculating Flags

Setting ADSR flags require the use of various bitwise and bitshift operations. This can be a little confusing to understand.
Expand Down
8 changes: 8 additions & 0 deletions site/docs/reference/functions.md
Original file line number Diff line number Diff line change
Expand Up @@ -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:

Expand All @@ -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 |
Expand Down
Loading