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

Fix triangle release by switching period by tick #714

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
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
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
Loading