Skip to content

Commit

Permalink
Fix triangle release by switching period by tick
Browse files Browse the repository at this point in the history
  • Loading branch information
JerwuQu committed Apr 28, 2024
1 parent 0dff7ad commit 4b50fff
Show file tree
Hide file tree
Showing 2 changed files with 56 additions and 24 deletions.
40 changes: 28 additions & 12 deletions runtimes/native/src/apu.c
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
#define MAX_VOLUME 0x1333 // ~15% of INT16_MAX
// The triangle channel sounds a bit quieter than the others, so give it higher amplitude
#define MAX_VOLUME_TRIANGLE 0x2000 // ~25% of INT16_MAX
// Also the triangle channel prevent popping on hard stops by adding a 1 ms release
// Also for the triangle channel, prevent popping on hard stops by adding a 1 ms release
#define RELEASE_TIME_TRIANGLE (SAMPLE_RATE / 1000)

typedef struct {
Expand All @@ -26,14 +26,17 @@ typedef struct {
/** Time at the end of the decay period. */
unsigned long long decayTime;

/** Time at the end of the sustain period. */
/** Time at the end of the sustain period, with adjustments due to tick-sample drift. */
unsigned long long sustainTime;

/** Time the tone should end. */
/** Time at the end of the release period, with adjustments due to tick-sample drift. */
unsigned long long releaseTime;

/** The tick the tone should end. */
unsigned long long endTick;
/** Time at the end of the release period, without adjustments due to tick-sample drift. */
unsigned long long estReleaseTime;

/** Tick at the end of the sustain period where the tone switches over to release. */
unsigned long long sustainTick;

/** Sustain volume level. */
int16_t sustainVolume;
Expand Down Expand Up @@ -93,14 +96,14 @@ static float rampf (float value1, float value2, unsigned long long time1, unsign

static float getCurrentFrequency (const Channel* channel) {
if (channel->freq2 > 0) {
return rampf(channel->freq1, channel->freq2, channel->startTime, channel->releaseTime);
return rampf(channel->freq1, channel->freq2, channel->startTime, channel->estReleaseTime);
} else {
return channel->freq1;
}
}

static int16_t getCurrentVolume (const Channel* channel) {
if (time >= channel->sustainTime && (channel->releaseTime - channel->sustainTime) > RELEASE_TIME_TRIANGLE) {
if (ticks > channel->sustainTick) {
// Release
return ramp(channel->sustainVolume, 0, channel->sustainTime, channel->releaseTime);
} else if (time >= channel->decayTime) {
Expand Down Expand Up @@ -136,6 +139,17 @@ void w4_apuInit () {
}

void w4_apuTick () {
// Update releaseTime for channels that should begin their release period this tick.
// This fixes drift drift between ticks and samples.
for (int channelIdx = 0; channelIdx < 4; ++channelIdx) {
Channel* channel = &channels[channelIdx];
if (ticks == channel->sustainTick) {
const delta = time - channel->sustainTime;
channel->sustainTime = time;
channel->releaseTime += delta;
}
}

ticks++;
}

Expand All @@ -160,7 +174,7 @@ void w4_apuTone (int frequency, int duration, int volume, int flags) {
Channel* channel = &channels[channelIdx];

// Restart the phase if this channel wasn't already playing
if (time > channel->releaseTime && ticks != channel->endTick) {
if (time > channel->releaseTime && ticks > channel->sustainTick) {
channel->phase = (channelIdx == 2) ? 0.25 : 0;
}
if (noteMode) {
Expand All @@ -174,8 +188,8 @@ void w4_apuTone (int frequency, int duration, int volume, int flags) {
channel->attackTime = channel->startTime + SAMPLE_RATE*attack/60;
channel->decayTime = channel->attackTime + SAMPLE_RATE*decay/60;
channel->sustainTime = channel->decayTime + SAMPLE_RATE*sustain/60;
channel->releaseTime = channel->sustainTime + SAMPLE_RATE*release/60;
channel->endTick = ticks + attack + decay + sustain + release;
channel->estReleaseTime = channel->sustainTime + SAMPLE_RATE*release/60;
channel->sustainTick = ticks + attack + decay + sustain;
int16_t maxVolume = (channelIdx == 2) ? MAX_VOLUME_TRIANGLE : MAX_VOLUME;
channel->sustainVolume = maxVolume * sustainVolume/100;
channel->peakVolume = peakVolume ? maxVolume * peakVolume/100 : maxVolume;
Expand All @@ -196,9 +210,11 @@ void w4_apuTone (int frequency, int duration, int volume, int flags) {

} else if (channelIdx == 2) {
if (release == 0) {
channel->releaseTime += RELEASE_TIME_TRIANGLE;
channel->estReleaseTime += RELEASE_TIME_TRIANGLE;
}
}

channel->releaseTime = channel->estReleaseTime;
}

void w4_apuWriteSamples (int16_t* output, unsigned long frames) {
Expand All @@ -208,7 +224,7 @@ void w4_apuWriteSamples (int16_t* output, unsigned long frames) {
for (int channelIdx = 0; channelIdx < 4; ++channelIdx) {
Channel* channel = &channels[channelIdx];

if (time < channel->releaseTime || ticks == channel->endTick) {
if (time < channel->releaseTime || ticks <= channel->sustainTick) {
float freq = getCurrentFrequency(channel);
int16_t volume = getCurrentVolume(channel);
int16_t sample;
Expand Down
40 changes: 28 additions & 12 deletions runtimes/web/src/apu-worklet.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ 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 the triangle channel prevent popping on hard stops by adding a 1 ms release
// 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 {
Expand All @@ -24,14 +24,17 @@ class Channel {
/** Time at the end of the decay period. */
decayTime = 0;

/** Time at the end of the sustain period. */
/** Time at the end of the sustain period, with adjustments due to tick-sample drift. */
sustainTime = 0;

/** Time the tone should end. */
/** Time at the end of the release period, with adjustments due to tick-sample drift. */
releaseTime = 0;

/** The tick the tone should end. */
endTick = 0;
/** Time at the end of the release period, without adjustments due to tick-sample drift. */
estReleaseTime = 0;

/** Tick at the end of the sustain period where the tone switches over to release. */
sustainTick = 0;

/** Sustain volume level. */
sustainVolume = 0;
Expand Down Expand Up @@ -109,15 +112,15 @@ class APUProcessor extends AudioWorkletProcessor {

getCurrentFrequency (channel: Channel) {
if (channel.freq2 > 0) {
return this.ramp(channel.freq1, channel.freq2, channel.startTime, channel.releaseTime);
return this.ramp(channel.freq1, channel.freq2, channel.startTime, channel.estReleaseTime);
} else {
return channel.freq1;
}
}

getCurrentVolume (channel: Channel) {
const time = this.time;
if (time >= channel.sustainTime && (channel.releaseTime - channel.sustainTime) > RELEASE_TIME_TRIANGLE) {
if (this.ticks > channel.sustainTick) {
// Release
return this.ramp(channel.sustainVolume, 0, channel.sustainTime, channel.releaseTime);
} else if (time >= channel.decayTime) {
Expand All @@ -133,6 +136,17 @@ class APUProcessor extends AudioWorkletProcessor {
}

tick () {
// Update releaseTime for channels that should begin their release period this tick.
// This fixes drift drift between ticks and samples.
for (let channelIdx = 0; channelIdx < 4; ++channelIdx) {
const channel = this.channels[channelIdx];
if (this.ticks == channel.sustainTick) {
const delta = this.time - channel.sustainTime;
channel.sustainTime = this.time;
channel.releaseTime += delta;
}
}

this.ticks++;
}

Expand All @@ -155,7 +169,7 @@ class APUProcessor extends AudioWorkletProcessor {
const channel = this.channels[channelIdx];

// Restart the phase if this channel wasn't already playing
if (this.time > channel.releaseTime && this.ticks != channel.endTick) {
if (this.time > channel.releaseTime && this.ticks > channel.sustainTick) {
channel.phase = (channelIdx == 2) ? 0.25 : 0;
}
if (noteMode) {
Expand All @@ -169,8 +183,8 @@ class APUProcessor extends AudioWorkletProcessor {
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.estReleaseTime = channel.sustainTime + ((SAMPLE_RATE*release/60) >>> 0);
channel.sustainTick = this.ticks + attack + decay + sustain;
channel.pan = pan;

const maxVolume = (channelIdx == 2) ? MAX_VOLUME_TRIANGLE : MAX_VOLUME;
Expand All @@ -192,9 +206,11 @@ class APUProcessor extends AudioWorkletProcessor {

} else if (channelIdx == 2) {
if (release == 0) {
channel.releaseTime += RELEASE_TIME_TRIANGLE;
channel.estReleaseTime += RELEASE_TIME_TRIANGLE;
}
}

channel.releaseTime = channel.estReleaseTime;
}

process (_inputs: Float32Array[][] | null, [[ outputLeft, outputRight ]]: Float32Array[][], _parameters: Record<string, Float32Array> | null) {
Expand All @@ -204,7 +220,7 @@ class APUProcessor extends AudioWorkletProcessor {
for (let channelIdx = 0; channelIdx < 4; ++channelIdx) {
const channel = this.channels[channelIdx];

if (this.time < channel.releaseTime || this.ticks == channel.endTick) {
if (this.time < channel.releaseTime || this.ticks <= channel.sustainTick) {
const freq = this.getCurrentFrequency(channel);
const volume = this.getCurrentVolume(channel);
let sample;
Expand Down

0 comments on commit 4b50fff

Please sign in to comment.