diff --git a/runtimes/native/src/apu.c b/runtimes/native/src/apu.c index 28ea5f1e..3998c1ce 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 { @@ -32,8 +32,8 @@ typedef struct { /** Time the tone should end. */ unsigned long long releaseTime; - /** The tick the tone should end. */ - unsigned long long endTick; + /** Tick at the end of the sustain period where the tone switch over to release. */ + unsigned long long sustainTick; /** Sustain volume level. */ int16_t sustainVolume; @@ -100,7 +100,7 @@ static float getCurrentFrequency (const Channel* channel) { } 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 +136,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 +171,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) { @@ -175,7 +186,7 @@ void w4_apuTone (int frequency, int duration, int volume, int flags) { 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->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; @@ -208,7 +219,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..82bd490c 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 { @@ -30,8 +30,8 @@ class Channel { /** Time the tone should end. */ releaseTime = 0; - /** The tick the tone should end. */ - endTick = 0; + /** Tick at the end of the sustain period where the tone switch over to release. */ + sustainTick = 0; /** Sustain volume level. */ sustainVolume = 0; @@ -117,7 +117,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 +133,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 +166,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) { @@ -170,7 +181,7 @@ class APUProcessor extends AudioWorkletProcessor { 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.sustainTick = this.ticks + attack + decay + sustain; channel.pan = pan; const maxVolume = (channelIdx == 2) ? MAX_VOLUME_TRIANGLE : MAX_VOLUME; @@ -204,7 +215,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;