amy_parse_transfer_layer_message() in src/parse.c calls the full amy_execute_deltas() when a sample load starts (z<preset>,<length>,... with a non-zero length, around line 591), just before pcm_load():
} else {
amy_execute_deltas();
int16_t * ram = pcm_load(sm[0], sm[1], sm[2], 1, midinote, sm[4], sm[5]);
start_receiving_transfer(sm[1]*2, (uint8_t*)ram);
}
This runs on whichever thread sent the message, and amy_execute_deltas() does more than flush due deltas:
sequencer_check_and_fill() advances the sequencer clock. That's a read-modify-write of next_amy_tick_us with no lock, which races the render thread's own per-block call, so a tick can be processed twice. It can also run the external sequencer hook on a thread the application doesn't expect.
update_external_cv_in() polls CV inputs off the render thread.
2b2876b ("Keep the sequencer tick service out of event ingest") fixed exactly this on the patch-load path by splitting the flush out as flush_due_deltas() and calling only that from ingest. The sample-transfer start was left calling the full function.
Since #1205, the flush part of that call is safe from any thread: amy_execute_deltas() takes the render lock around its flush. The sequencer tick and CV poll still run unlocked on the sending thread.
Suggested fix: do what 2b2876b did for patch loads. Have this path settle pending deltas with only the flush, under the render lock (expose flush_due_deltas(), or add a small wrapper that takes the render lock and flushes), and leave the sequencer and CV to the render thread. It's also worth checking why the flush is needed here at all. Presumably it's so pending deltas that reference the preset being replaced are applied before pcm_load() overwrites it. If so, a comment saying so would help.
Impact: low. It only fires when a sample transfer starts, which is rare, and the worst likely outcome is a doubled sequencer tick at that moment. Noted while working on #1205.
Generated by Claude Code
amy_parse_transfer_layer_message()insrc/parse.ccalls the fullamy_execute_deltas()when a sample load starts (z<preset>,<length>,...with a non-zero length, around line 591), just beforepcm_load():This runs on whichever thread sent the message, and
amy_execute_deltas()does more than flush due deltas:sequencer_check_and_fill()advances the sequencer clock. That's a read-modify-write ofnext_amy_tick_uswith no lock, which races the render thread's own per-block call, so a tick can be processed twice. It can also run the external sequencer hook on a thread the application doesn't expect.update_external_cv_in()polls CV inputs off the render thread.2b2876b ("Keep the sequencer tick service out of event ingest") fixed exactly this on the patch-load path by splitting the flush out as
flush_due_deltas()and calling only that from ingest. The sample-transfer start was left calling the full function.Since #1205, the flush part of that call is safe from any thread:
amy_execute_deltas()takes the render lock around its flush. The sequencer tick and CV poll still run unlocked on the sending thread.Suggested fix: do what 2b2876b did for patch loads. Have this path settle pending deltas with only the flush, under the render lock (expose
flush_due_deltas(), or add a small wrapper that takes the render lock and flushes), and leave the sequencer and CV to the render thread. It's also worth checking why the flush is needed here at all. Presumably it's so pending deltas that reference the preset being replaced are applied beforepcm_load()overwrites it. If so, a comment saying so would help.Impact: low. It only fires when a sample transfer starts, which is rare, and the worst likely outcome is a doubled sequencer tick at that moment. Noted while working on #1205.
Generated by Claude Code