synthio: Fix note dropped when re-pressed after decay - #11289
Conversation
A note released and then re-pressed after its envelope has run down to 0 is silently dropped. synth.pressed reports it as pressed, and then the next render loses it. synthio_span_change_note()'s fast path for a note still on its channel re-enters ATTACK without touching level. At level 0 the render loop's "note is truly finished" check reaps the channel before the envelope is stepped, so the re-press never sounds. Route that case through the same envelope init a fresh press uses.
|
The logic makes sense, but what if we instead changed We can change the return type to flag if the envelope is done: static bool synthio_envelope_state_step(synthio_envelope_state_t *state, synthio_envelope_definition_t *def, size_t n_steps) {
state->substep += n_steps;
while (state->substep >= SYNTHIO_MAX_DUR) {
// max n_steps should be SYNTHIO_MAX_DUR so this loop executes at most
// once
state->substep -= SYNTHIO_MAX_DUR;
switch (state->state) {
case SYNTHIO_ENVELOPE_STATE_SUSTAIN:
break;
case SYNTHIO_ENVELOPE_STATE_ATTACK:
state->level = MIN(state->level + def->attack_step, def->attack_level);
if (state->level == def->attack_level) {
state->state = SYNTHIO_ENVELOPE_STATE_DECAY;
}
break;
case SYNTHIO_ENVELOPE_STATE_DECAY:
state->level = MAX(state->level + def->decay_step, def->sustain_level);
if (state->level == def->sustain_level) {
state->state = SYNTHIO_ENVELOPE_STATE_SUSTAIN;
}
break;
case SYNTHIO_ENVELOPE_STATE_RELEASE:
state->level = MAX(state->level + def->release_step, 0);
if (state->level == 0) {
return false; // Indicate that note envelope has completed
}
}
}
return true;
}And then go into // advance envelope states
for (int chan = 0; chan < CIRCUITPY_SYNTHIO_MAX_CHANNELS; chan++) {
mp_obj_t note_obj = synth->span.note_obj[chan];
if (note_obj == SYNTHIO_SILENCE) {
continue;
}
if (!synthio_envelope_state_step(&synth->envelope_state[chan], synthio_synth_get_note_envelope(synth, note_obj), dur)) {
// Envelope has completed, go ahead and reset channel
synth->span.note_obj[chan] = SYNTHIO_SILENCE;
}
} |
|
Agreed on the direction — One catch. It only fixes the re-press if the Measured on a unix build, drum-shaped envelope (attack 1 ms, decay 20 ms, With the patch as written those 14 stay pressed, and on a 14-channel build a So: return false whenever the level reaches 0 and cannot rise again without a case SYNTHIO_ENVELOPE_STATE_DECAY:
state->level = MAX(state->level + def->decay_step, def->sustain_level);
if (state->level == def->sustain_level) {
if (state->level == 0) {
return false;
}
state->state = SYNTHIO_ENVELOPE_STATE_SUSTAIN;
}
break;Then the ambiguous check can go and both cases are handled at the Happy to redo the PR that way if you prefer it to the current fix. |
|
Just adding another |
tannewt
left a comment
There was a problem hiding this comment.
Let's merge and we can always change the internals later.
A note released and then re-pressed after its envelope has run down to 0
is silently dropped.
synth.pressedreports it as pressed, and then thenext render loses it.
synthio_span_change_note()'s fast path for a note still on its channelre-enters ATTACK without touching
level. At level 0 the render loop's"note is truly finished" check reaps the channel before the envelope is
stepped, so the re-press never sounds.
Test included; fails on main, passes with the fix.