From 4b50fff7d42887d7a453215af93a60af492056f6 Mon Sep 17 00:00:00 2001 From: Marcus Ramse Date: Sun, 28 Apr 2024 12:45:25 +0000 Subject: [PATCH] Fix triangle release by switching period by tick --- runtimes/native/src/apu.c | 40 +++++++++++++++++++++++---------- runtimes/web/src/apu-worklet.ts | 40 +++++++++++++++++++++++---------- 2 files changed, 56 insertions(+), 24 deletions(-) diff --git a/runtimes/native/src/apu.c b/runtimes/native/src/apu.c index 28ea5f1e..0eb8cb5c 100644 --- a/runtimes/native/src/apu.c +++ b/runtimes/native/src/apu.c @@ -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 { @@ -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; @@ -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) { @@ -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++; } @@ -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) { @@ -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; @@ -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) { @@ -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; diff --git a/runtimes/web/src/apu-worklet.ts b/runtimes/web/src/apu-worklet.ts index 9011f1b7..054782ab 100644 --- a/runtimes/web/src/apu-worklet.ts +++ b/runtimes/web/src/apu-worklet.ts @@ -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 { @@ -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; @@ -109,7 +112,7 @@ 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; } @@ -117,7 +120,7 @@ class APUProcessor extends AudioWorkletProcessor { 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) { @@ -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++; } @@ -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) { @@ -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; @@ -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 | null) { @@ -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;