Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion src/amy.h
Original file line number Diff line number Diff line change
Expand Up @@ -1362,7 +1362,9 @@ extern void cv_trigger_clear_mappings(int gate_cv);
// note_output.c -- cv_trigger's mirror: a synth's note events out to
// CV/gate or MIDI instead of to its oscillators.
extern uint8_t note_output_mode_for(uint8_t synth);
extern bool note_output_handle_event(amy_event *e);
// live: the event is being played (headed for amy_global.delta_queue), not
// stored into a patch. Only a live event sends anything.
extern bool note_output_handle_event(amy_event *e, bool live);
extern void note_output_config(uint8_t synth, int mode, float *args, int num_args);
extern void note_output_all_off(uint8_t synth);
extern void note_output_all_gates_off(void);
Expand Down
13 changes: 11 additions & 2 deletions src/note_output.c
Original file line number Diff line number Diff line change
Expand Up @@ -183,7 +183,15 @@ void note_output_all_off(uint8_t synth) {

// Called from patches_event_has_voices. Returns true if this event was a
// note for a note-output synth and has been dealt with.
bool note_output_handle_event(amy_event *e) {
//
// `live` is false when the event is being STORED -- parsed into a patch's
// delta list by patches_store_patch -- rather than played. A note in a
// patch string is not a note being played, and before this a stored
// "i1n60l1" sent a real note-on out of the port (or raised a gate) at the
// moment the patch was saved. Such a note is still claimed, so a voiceless
// note-output synth skips the voice path exactly as when it plays, but
// nothing is sent and no held-note state changes.
bool note_output_handle_event(amy_event *e, bool live) {
if (AMY_IS_UNSET(e->synth)) return false;
note_output_t *n = note_output_find(e->synth);
if (n == NULL || n->mode == NOTE_OUTPUT_OFF) return false;
Expand All @@ -197,14 +205,15 @@ bool note_output_handle_event(amy_event *e) {
// the API could clear. Panic has to reach here or it is not panic.
if (AMY_IS_UNSET(e->midi_note) && AMY_IS_SET(e->velocity)
&& e->velocity == 0) {
note_output_all_off(e->synth);
if (live) note_output_all_off(e->synth);
return false; // ...and the synth's own voices still get it
}
// ONLY NOTE EVENTS ARE CLAIMED. Anything else addressed to this synth
// -- a level, a bus, a patch that redefines it as an ordinary synth --
// carries on down the normal path, because swallowing it here would
// make a note-output synth a synth you cannot change.
if (AMY_IS_UNSET(e->midi_note)) return false;
if (!live) return true;
// A note that arrived over MIDI is not sent back out over MIDI by
// default: without this a thru-patched port is a feedback loop. The
// wave-type implementation guarded on the same thing. A CV/gate
Expand Down
2 changes: 1 addition & 1 deletion src/patches.c
Original file line number Diff line number Diff line change
Expand Up @@ -1268,7 +1268,7 @@ void patches_event_has_voices(amy_event *e, struct delta **queue) {
// here -- and why the existence check passes a NULL tag, since
// "synth N not defined" three times per note would be noise about a
// deliberate arrangement.
if (note_output_handle_event(e) && !instrument_number_exists(e->synth, NULL))
if (note_output_handle_event(e, queue == &amy_global.delta_queue) && !instrument_number_exists(e->synth, NULL))
return;

uint8_t synth = e->synth;
Expand Down
35 changes: 35 additions & 0 deletions tests/test_note_output.c
Original file line number Diff line number Diff line change
Expand Up @@ -301,6 +301,40 @@ static void test_repeated_note_on_retriggers(void) {
CHECK(fabsf(last_on(1)) < 1e-4, "one note-off is enough to drop the gate");
}

// Storing a patch parses its string into the patch's delta list, and a
// segment like "i1n60l1" names a synth and a note. That is a note being
// SAVED, not played: it used to go out of the port (or raise the gate) the
// moment the patch was stored. Pinned for both modes, and for a stored
// panic, which must not drop a gate that a real note is holding up.
static void test_storing_a_patch_sends_nothing(void) {
printf("storing a patch that contains a note sends nothing\n");
restart();
wire("i1iG2,1");
clear_log();
wire("K1024ui1n60l1");
CHECK(midi_writes == 0, "MIDI: nothing sent while storing (%d)", midi_writes);
wire("i1n62l1");
CHECK(midi_writes == 1 && midi_log[0][0] == 0x90 && midi_log[0][1] == 62,
"MIDI: a played note still goes out (%d sent)", midi_writes);

restart();
wire("i1iG1,0,1");
clear_log();
wire("K1025ui1n60l1");
CHECK(cv_writes == 0, "CV: no voltage written while storing (%d writes)", cv_writes);
// Had the stored note been pushed onto the held stack, this note-on
// would be legato over it and raise no gate edge at all.
wire("i1n48l1");
CHECK(fabsf(last_on(1) - 5.0f) < 1e-4 && fabsf(last_on(0) - 2.0f) < 1e-4,
"CV: the next played note raises the gate at its own pitch (gate %.2f, pitch %.3f)",
last_on(1), last_on(0));
clear_log();
wire("K1026ui1l0"); // a stored ALL NOTES OFF
CHECK(writes_on(1) == 0, "CV: a stored panic leaves the held gate alone (%d gate writes)", writes_on(1));
wire("i1l0"); // the real one
CHECK(fabsf(last_on(1)) < 1e-4, "CV: a played panic still drops it (%.2f)", last_on(1));
}

int main(void) {
test_cv_gate_voltages();
test_loopback_identity();
Expand All @@ -313,6 +347,7 @@ int main(void) {
test_echo_not_diversion();
test_lost_note_off_is_recoverable();
test_repeated_note_on_retriggers();
test_storing_a_patch_sends_nothing();
printf("%s: %d failure%s\n", failures ? "FAILED" : "PASSED",
failures, failures == 1 ? "" : "s");
return failures ? 1 : 0;
Expand Down
Loading