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:
@@ -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"
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user