From 96130d22c7e31bbda0f5f875f75eaaa2fee506ed Mon Sep 17 00:00:00 2001 From: NetterSilver <133535268+Silvertree2010@users.noreply.github.com> Date: Tue, 20 Oct 2026 20:30:04 +0000 Subject: [PATCH] Reject mix.send to an unknown bus name instead of creating a bus Fixes #816 Co-Authored-By: Claude Haiku 5.5 Co-Authored-By: Claude Sonnet 5.5 --- crates/engine/src/cmd/mix.rs | 18 +++++++++++++++++- crates/engine/src/cmd/surround.rs | 1 + crates/ui-egui/src/mix_window.rs | 8 ++++++-- crates/ui-egui/tests/menu_search.rs | 1 + crates/ui-egui/tests/send_window.rs | 1 + 5 files changed, 26 insertions(+), 3 deletions(-) diff --git a/crates/engine/src/cmd/mix.rs b/crates/engine/src/cmd/mix.rs index 3e5d75cc..7502b4f2 100644 --- a/crates/engine/src/cmd/mix.rs +++ b/crates/engine/src/cmd/mix.rs @@ -444,7 +444,7 @@ fn send(e: &mut Engine, p: &Value) -> Result { let t = tracks_required(e, "mix.send", p)?.first().copied().ok_or_else(|| bad("mix.send", "no track"))?; let explicit = optional_slot(p, "mix.send", SEND_SLOTS)?; let s = e.session_mut(); - let bid = s.add_bus(&bus, soundcraft_model::ChannelFormat::Stereo); + let bid = s.bus_by_name(&bus).map(|b| b.id).ok_or_else(|| bad("mix.send", format!("no bus named \"{bus}\"")))?; let tr = s.track_mut(t).ok_or_else(|| bad("mix.send", "no track"))?; let slot = explicit.or_else(|| tr.mixer.sends.iter().position(Option::is_none)).ok_or_else(|| bad("mix.send", "all send slots are in use"))?; // Re-targeting an existing send keeps its level, pan, mute and pre/post setting. @@ -930,8 +930,10 @@ mod tests { fn retargeting_a_send_keeps_its_settings() { let mut e = Engine::default(); e.execute("track.new", &json!({"count": 1, "name": "Gtr", "format": "mono"})).unwrap(); + e.execute("mix.new_bus", &json!({"name": "Verb"})).unwrap(); e.execute("mix.send", &json!({"track": "Gtr", "slot": 0, "bus": "Verb", "level_db": -9.0, "pre_fader": true})).unwrap(); e.execute("mix.send_level", &json!({"track": "Gtr", "slot": 0, "pan": 0.5, "mute": true})).unwrap(); + e.execute("mix.new_bus", &json!({"name": "Delay"})).unwrap(); e.execute("mix.send", &json!({"track": "Gtr", "slot": 0, "bus": "Delay"})).unwrap(); let sl = e.session().tracks[0].mixer.sends[0].clone().unwrap(); let delay = e.session().busses.iter().find(|b| b.name == "Delay").unwrap().id; @@ -985,10 +987,24 @@ mod tests { assert_eq!(count(&e), 0, "absent param must still clear every lane"); } + #[test] + fn send_rejects_unknown_bus() { + let mut e = Engine::default(); + e.execute("track.new", &json!({"count": 1, "name": "Gtr", "format": "mono"})).unwrap(); + e.execute("mix.new_bus", &json!({"name": "Verb"})).unwrap(); + let err = e.execute("mix.send", &json!({"track": "Gtr", "bus": "Nowhere"})).unwrap_err(); + assert!(err.to_string().contains("no bus named \"Nowhere\""), "{err}"); + assert_eq!(e.session().busses.len(), 1); + assert!(e.session().tracks[0].mixer.sends.iter().all(Option::is_none)); + e.execute("mix.send", &json!({"track": "Gtr", "bus": "verb"})).unwrap(); + assert_eq!(e.session().busses.len(), 1); + } + #[test] fn insert_and_send_reject_invalid_slot() { let mut e = Engine::default(); e.execute("track.new", &json!({"count": 1, "name": "Gtr", "format": "mono"})).unwrap(); + e.execute("mix.new_bus", &json!({"name": "Verb"})).unwrap(); for slot in [json!(12), json!("k"), json!("x1")] { let r = e.execute("mix.insert", &json!({"track": "Gtr", "slot": slot, "plugin": "compressor"})); assert!(r.is_err(), "insert slot {slot} accepted: {r:?}"); diff --git a/crates/engine/src/cmd/surround.rs b/crates/engine/src/cmd/surround.rs index 5a6630d3..bd23be54 100644 --- a/crates/engine/src/cmd/surround.rs +++ b/crates/engine/src/cmd/surround.rs @@ -184,6 +184,7 @@ mod tests { assert!(e.session().track(t).unwrap().mixer.surround.is_none()); // Sends. assert!(e.execute("mix.send_surround_pan", &json!({"track": t.0, "slot": "a", "x": 1.0})).is_err(), "empty slot"); + e.execute("mix.new_bus", &json!({"name": "FX"})).unwrap(); e.execute("mix.send", &json!({"track": t.0, "slot": 0, "bus": "FX"})).unwrap(); e.execute("mix.send_surround_pan", &json!({"track": t.0, "slot": "a", "x": 1.0, "y": -1.0})).unwrap(); let snd = e.session().track(t).unwrap().mixer.sends[0].clone().unwrap(); diff --git a/crates/ui-egui/src/mix_window.rs b/crates/ui-egui/src/mix_window.rs index 35b5f5cb..4ee3fb83 100644 --- a/crates/ui-egui/src/mix_window.rs +++ b/crates/ui-egui/src/mix_window.rs @@ -1247,10 +1247,14 @@ fn send_route_menu(app: &mut SoundApp, ui: &mut Ui, id: TrackId, slot: usize, cu ui.separator(); if ui.button("new bus...").clicked() { let mut n = busses.len() + 1; - while busses.iter().any(|bus| bus.name == format!("Bus {n}")) || app.engine.session().track_by_name(&format!("Bus {n}")).is_some() { + while busses.iter().any(|bus| bus.name.eq_ignore_ascii_case(&format!("Bus {n}"))) + || app.engine.session().track_by_name(&format!("Bus {n}")).is_some() + { n += 1; } - let _ = app.run("mix.send", json!({"track": id.0, "slot": slot, "bus": format!("Bus {n}")})); + if app.run("mix.new_bus", json!({"name": format!("Bus {n}")})).is_ok() { + let _ = app.run("mix.send", json!({"track": id.0, "slot": slot, "bus": format!("Bus {n}")})); + } ui.close(); } } diff --git a/crates/ui-egui/tests/menu_search.rs b/crates/ui-egui/tests/menu_search.rs index 973e6f5d..21b99a6c 100644 --- a/crates/ui-egui/tests/menu_search.rs +++ b/crates/ui-egui/tests/menu_search.rs @@ -143,6 +143,7 @@ fn send_and_output_menus_search_buses_as_soon_as_they_open() { let mut engine = vox(); let vox = engine.session().tracks[0].id; for (slot, bus) in [(0, "Verb"), (1, "Delay")] { + engine.execute("mix.new_bus", &json!({"name": bus})).unwrap(); engine.execute("mix.send", &json!({"track": vox.0, "slot": slot, "bus": bus})).unwrap(); } let mut h = harness(engine); diff --git a/crates/ui-egui/tests/send_window.rs b/crates/ui-egui/tests/send_window.rs index 908885d3..dcdef662 100644 --- a/crates/ui-egui/tests/send_window.rs +++ b/crates/ui-egui/tests/send_window.rs @@ -12,6 +12,7 @@ fn harness() -> (Harness<'static, SoundApp>, soundcraft_model::TrackId) { engine.execute("track.new", &json!({"name": "Vox"})).unwrap(); let vox = engine.session().tracks[0].id; for (slot, bus) in [(0, "Verb"), (1, "Delay")] { + engine.execute("mix.new_bus", &json!({"name": bus})).unwrap(); engine.execute("mix.send", &json!({"track": vox.0, "slot": slot, "bus": bus, "level_db": -10.0})).unwrap(); } let mut app = SoundApp::new(engine, None, Services::default());