From 3155caf3ccf42b532a49bc17c3034682ae3c8a33 Mon Sep 17 00:00:00 2001 From: Reactorcoremeltdown Date: Sat, 18 Jul 2026 02:04:14 +0200 Subject: [PATCH] Fix stuck synth voices: release scaled by sustain level (v0.4.5) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- app/build.gradle.kts | 4 ++-- .../java/space/rcmd/android/sizzle/audio/SynthVoice.kt | 9 ++++++++- 2 files changed, 10 insertions(+), 3 deletions(-) diff --git a/app/build.gradle.kts b/app/build.gradle.kts index f621420..96d23e1 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -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" diff --git a/app/src/main/java/space/rcmd/android/sizzle/audio/SynthVoice.kt b/app/src/main/java/space/rcmd/android/sizzle/audio/SynthVoice.kt index 53080d8..d2b79e5 100644 --- a/app/src/main/java/space/rcmd/android/sizzle/audio/SynthVoice.kt +++ b/app/src/main/java/space/rcmd/android/sizzle/audio/SynthVoice.kt @@ -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 }