Skip to content

Commit

Permalink
Merge pull request #706 from JerwuQu/jwq_apu-midi-mode
Browse files Browse the repository at this point in the history
APU Note Mode (alternative version of #334)
  • Loading branch information
aduros authored Apr 18, 2024
2 parents 71f5a70 + 537cda5 commit 6227e90
Show file tree
Hide file tree
Showing 16 changed files with 139 additions and 17 deletions.
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

0 comments on commit 6227e90

Please sign in to comment.