Skip to content

Note chaining#947

Merged
dpwe merged 2 commits into
mainfrom
note_chaining
Jul 20, 2026
Merged

Note chaining#947
dpwe merged 2 commits into
mainfrom
note_chaining

Conversation

@dpwe

@dpwe dpwe commented Jul 20, 2026

Copy link
Copy Markdown
Collaborator

When getting rid of voices in tests, I had a problem with portamento (which works best if you can change the note of a single voice without retriggering it - see #946). So I was tracking exactly when an osc receives a note, and I saw that sending a note to synth voice caused certain oscs to get the note written to them many, many times. Here's what happened:

  • amy.send(synth=1, note=60, vel=1) lands up in the "osc-directed with no explicit osc" part of patches_event_has_voices (around patches.c:1045).
  • The event (which is a note-on, but the code doesn't care) is sent sequentially to every osc within the voice.
  • Within event_to_deltas_queue, each of these generates a MIDI_NOTE delta to the corresponding osc.
  • Subsequently, these deltas are passed to play_delta, which is, for MIDI_NOTE deltas, specially configured to walk down chained_osc chains setting the midi_note field of all of them. (MOD_SOURCE and ALGO_SOURCE oscs are excluded, needed for the "osc broadcast" feature to work).
  • If the voice contains an osc chain (i.e., a Juno patch), the first osc causes the note to be written to all the subsequent chained oscs.
  • But then the MIDI_NOTE deltas for all the subsequent oscs in the chain arrive, and their tails of the chain are similarly walked.

My solution was to introduce a SYNTH_IS_CHAINED state, similar to the SYNTH_IS_MOD_SOURCE and SYNTH_IS_ALGO_SOURCE. Now, efforts to explicitly set the midi_note of a CHAINED osc are ignored, since that will have been handled by the head of the chain.

This revealed some confusion about the purpose of synth[osc]->status - it was holding both the audible status of active oscs (SYNTH_AUDIBLE/INAUDIBLE) as well is SYNTH_IS_*. These are really different things.

So I retained synth[osc]->status for AUDIBLE/INAUDIBLE and added a new field, synth[osc]->role to hold SYNTH_IS_* fields. I defined SYNTH_IS_NORMAL for oscs that don't have a special role.

(n.b.: the confusion about "synth" and "osc" is probably something we should clean up. There have always been oscs, and their parameter structure has always been called synth[osc]. When we introduced voice management structures, it was originally called instrument to avoid the confusion, but for the Python interface, it had to be synth, and now this is the general term throughout AMYland. We should rename the venerable struct synthinfo **synth to be struct oscinfo **oscs or something.)

(note 2: In the process, I was studying the use of the similar-sounding status field in amy_event, which it turns out is unrelated to synth[osc]->status. In fact, as far as I could tell the various things it had been used for - marking events as sequencer-related, or transfer-related - had been made redundant. So I removed it entirely, and everything still appears to work.)

(note 3: You might be tempted to check the role of oscs in the patches_event_has_voices osc-broadcast loop, rather than going to all the bother of issuing the deltas, only to have them ignored. The problem with this is that when the event is being processed, we don't actually know what the osc status will be when the delta is finally fired - the event can be defined for some time in the future, and we don't know what osc param changes will occur in the interim. So the only strictly correct thing is to issue the deltas, then see what state the osc is in when the deltas land. This is a bit pedantic, though. However, it's revealed by the test.py tests, where all of the events are issued before any deltas are processed, so this ontological delay is made very very obvious.)

@dpwe
dpwe merged commit 31111e5 into main Jul 20, 2026
12 checks passed
@github-actions

Copy link
Copy Markdown
Contributor

🎛️ AMY HW CI (AMYboard bench)

Flashed this PR's AMY (LoadTestChord: 6-voice Juno patch=1, one held note every 2 s) onto the physical AMYboard and measured the smoothed render load as the chord grows — back-to-back with the same sketch built at the PR's merge base, so Δ is this PR's own cost.

PASS — the bench ran the test to completion.

notes held main @ aa48a5a this PR Δ
1 1469 1472 +3
2 1659 1663 +4
3 2325 2331 +6
4 2542 2548 +6
5 3269 3277 +8
6 3492 3503 +11

Full chord settled render μs: 3502 (was 3495, Δ +0.2%) (peak 3509, 39 samples)

⬇️ Artifacts: serial log · load trace · report

Self-hosted bench (amyboardci). FAIL means only that the test could not run — the load values are informational, with no threshold and no audio compare. See tools/arduino_loadsweep/.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant