Fix stuck synth voices: release scaled by sustain level (v0.4.5)

The subtractive synth's amplitude-envelope release decremented at
`(dt / r) * s` — the rate was multiplied by the sustain level, floored
at 0.001. For any percussive patch (sustain≈0: Square Bass, Pulse Pluck,
Noise Perc), release ran ~1000× too slow, so a 0.2 s tail took minutes.
Since a voice's isActive follows its amp envelope, the voice never freed
and kept rendering (3 osc + filter) indefinitely.

That one bug produced all three reported symptoms:
 * missed note-offs / stuck notes needing panic — the note-off fires but
   the voice takes minutes to go silent, reading as ignored;
 * fast-tapping dropouts on every instrument — released-mid-envelope
   voices pile up (whole pool + 8 live audition voices) and CPU climbs
   until the audio callback overruns → xruns;
 * audition-then-play dropouts — audition notes are already stuck on the
   live pool before the sequencer stacks track voices on top.

Release now ramps to zero at a constant 1/r rate, reaching silence
within r seconds from any level (matching SampleVoice's behaviour). The
Sampler/SoundFont path already captured its release rate at note-off, so
it was less affected — but the shared synth voice pinned the CPU budget,
which is why the stutter showed across all instruments.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Reactorcoremeltdown
2026-07-18 02:04:14 +02:00
parent 00f52c3434
commit 3155caf3cc
2 changed files with 10 additions and 3 deletions

View File

@@ -22,8 +22,8 @@ android {
// (android.media.midi) and AAudio low-latency audio we rely on.
minSdk = 26
targetSdk = 34
versionCode = 11
versionName = "0.4.4"
versionCode = 12
versionName = "0.4.5"
// We provide our own instrumentation runner if/when tests are added.
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"

View File

@@ -268,7 +268,14 @@ class SynthVoice(private val sampleRate: Int) {
1 -> { level += dt / a; if (level >= 1f) { level = 1f; stage = 2 } }
2 -> { level -= dt / d.coerceAtLeast(0.001f) * (1f - s); if (level <= s) { level = s; stage = 3 } }
3 -> {}
4 -> { level -= dt / r.coerceAtLeast(0.001f) * s.coerceAtLeast(0.001f); if (level <= 0f) { level = 0f; stage = 0 } }
// Release ramps to zero at a rate set by the release TIME alone. The
// decrement was previously scaled by the sustain level (`* s`), so a
// percussive patch (sustain≈0) released ~1000× too slowly — the voice
// effectively never freed, piling up stuck notes and CPU until the
// audio callback missed its deadline (dropouts) and note-offs looked
// ignored (panic needed). A constant 1/r rate guarantees the voice
// reaches silence within r seconds from any level.
4 -> { level -= dt / r.coerceAtLeast(0.001f); if (level <= 0f) { level = 0f; stage = 0 } }
}
return level
}