Plugin presets: factory default and user presets (save/recall), mix.insert_params
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
718dccdad7
commit
f534ca4e15
|
|
@ -175,6 +175,7 @@ fn main() -> eframe::Result {
|
|||
let player = if no_audio { None } else { Some(soundcraft_playback::Player::new(Arc::new(engine.session().clone()))) };
|
||||
let mut app = SoundApp::new(engine, player, services());
|
||||
app.autosave_dir = prefs_path().and_then(|p| p.parent().map(|d| d.join("Autosave")));
|
||||
app.preset_dir = prefs_path().and_then(|p| p.parent().map(|d| d.join("Presets")));
|
||||
if let Some(ui) = load_prefs() {
|
||||
app.ui = ui;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -95,6 +95,15 @@ pub fn specs() -> Vec<CommandSpec> {
|
|||
cmd!("mix.insert_remove", "Remove Insert", [], None, "{track?, slot}", has_selection, insert_remove),
|
||||
cmd!("mix.insert_bypass", "Bypass Insert", [], None, "{track?, slot, value?: bool}", has_selection, insert_bypass),
|
||||
cmd!("mix.insert_param", "Set Plugin Parameter", [], None, "{track?, slot, param, value}", has_selection, insert_param),
|
||||
cmd!(
|
||||
"mix.insert_params",
|
||||
"Set Plugin Parameters",
|
||||
[],
|
||||
None,
|
||||
"{track?, slot, params: {id: value}, preset?: name}",
|
||||
has_selection,
|
||||
insert_params
|
||||
),
|
||||
cmd!("mix.insert_move", "Move Insert", [], None, "{track?, from, to}", has_selection, insert_move),
|
||||
cmd!("mix.send", "Assign Send", [], None, "{track?, slot?: 0..9|a..j, bus: name, level_db?: 0, pre_fader?: false}", has_selection, send),
|
||||
cmd!("mix.send_remove", "Remove Send", [], None, "{track?, slot}", has_selection, |e, p| {
|
||||
|
|
@ -343,6 +352,30 @@ fn insert_param(e: &mut Engine, p: &Value) -> Result<Value> {
|
|||
Ok(json!({"value": v}))
|
||||
}
|
||||
|
||||
/// Apply many parameters at once (preset recall); unknown ids are ignored, values clamped.
|
||||
fn insert_params(e: &mut Engine, p: &Value) -> Result<Value> {
|
||||
let (t, slot) = track_slot(e, p, "mix.insert_params", INSERT_SLOTS)?;
|
||||
let obj = p.get("params").and_then(Value::as_object).cloned().ok_or_else(|| bad("mix.insert_params", "`params` must be an object"))?;
|
||||
let preset = str_param(p, "preset").map(str::to_string);
|
||||
let s = e.session_mut();
|
||||
let ins = s
|
||||
.track_mut(t)
|
||||
.and_then(|tr| tr.mixer.inserts.get_mut(slot))
|
||||
.and_then(Option::as_mut)
|
||||
.ok_or_else(|| bad("mix.insert_params", "empty slot"))?;
|
||||
let info = super::clap::plugin_info(&ins.plugin).ok_or_else(|| bad("mix.insert_params", "unknown plugin"))?;
|
||||
let mut n = 0;
|
||||
for (k, v) in obj {
|
||||
let (Some(pi), Some(f)) = (info.params.iter().find(|x| x.id == k), v.as_f64().filter(|f| f.is_finite())) else { continue };
|
||||
ins.params.insert(k, (f as f32).clamp(pi.min, pi.max));
|
||||
n += 1;
|
||||
}
|
||||
if let Some(name) = preset {
|
||||
ins.preset = name;
|
||||
}
|
||||
Ok(json!({"set": n}))
|
||||
}
|
||||
|
||||
fn insert_move(e: &mut Engine, p: &Value) -> Result<Value> {
|
||||
let t = tracks_required(e, "mix.insert_move", p)?.first().copied().ok_or_else(|| bad("mix.insert_move", "no track"))?;
|
||||
let from = slot_of(p, "from", INSERT_SLOTS).ok_or_else(|| bad("mix.insert_move", "`from` slot"))?;
|
||||
|
|
|
|||
|
|
@ -136,6 +136,11 @@ pub struct Services {
|
|||
pub pick_save: Option<Box<dyn Fn(&str, &str) -> Option<String>>>,
|
||||
}
|
||||
|
||||
/// File-system-safe folder name for a plugin id (`clap:com.x.y` → `clap_com.x.y`).
|
||||
pub fn preset_folder_name(plugin: &str) -> String {
|
||||
plugin.chars().map(|c| if c.is_ascii_alphanumeric() || c == '.' || c == '-' || c == '_' { c } else { '_' }).collect()
|
||||
}
|
||||
|
||||
/// Ballistic meter display state per strip.
|
||||
#[derive(Debug, Clone, Copy, Default)]
|
||||
pub struct MeterDisplay {
|
||||
|
|
@ -173,6 +178,8 @@ pub struct SoundApp {
|
|||
pub services: Services,
|
||||
/// Folder for crash-recovery autosaves (native apps set it).
|
||||
pub autosave_dir: Option<std::path::PathBuf>,
|
||||
/// Folder for user plugin presets (native apps set it).
|
||||
pub preset_dir: Option<std::path::PathBuf>,
|
||||
last_autosave: f64,
|
||||
pub dialogs: dialogs::Dialogs,
|
||||
pub meters: HashMap<TrackId, MeterDisplay>,
|
||||
|
|
@ -212,6 +219,7 @@ impl SoundApp {
|
|||
ui: UiState::default(),
|
||||
services,
|
||||
autosave_dir: None,
|
||||
preset_dir: None,
|
||||
last_autosave: 0.0,
|
||||
dialogs: dialogs::Dialogs::default(),
|
||||
meters: HashMap::new(),
|
||||
|
|
|
|||
|
|
@ -319,6 +319,7 @@ fn plugin_windows(app: &mut SoundApp, ctx: &egui::Context) {
|
|||
if ui.button(label).clicked() {
|
||||
let _ = app.run("mix.insert_bypass", json!({"track": tid.0, "slot": slot}));
|
||||
}
|
||||
ui.menu_button("Presets ▾", |ui| preset_menu(app, ui, tid, slot, info, &ins));
|
||||
});
|
||||
if info.id == "eq_7band" || info.id == "eq_1band" {
|
||||
eq_curve(ui, info.id, &ins);
|
||||
|
|
@ -419,3 +420,60 @@ fn session_info(app: &mut SoundApp, ctx: &egui::Context) {
|
|||
});
|
||||
app.ui.show_session_info = open;
|
||||
}
|
||||
|
||||
/// Folder for a plugin's user presets.
|
||||
fn preset_dir(app: &SoundApp, plugin: &str) -> Option<std::path::PathBuf> {
|
||||
app.preset_dir.as_ref().map(|d| d.join(crate::preset_folder_name(plugin)))
|
||||
}
|
||||
|
||||
fn preset_menu(
|
||||
app: &mut SoundApp,
|
||||
ui: &mut Ui,
|
||||
tid: soundcraft_model::TrackId,
|
||||
slot: usize,
|
||||
info: &soundcraft_dsp::PluginInfo,
|
||||
ins: &soundcraft_model::Insert,
|
||||
) {
|
||||
if ui.button("<factory default>").clicked() {
|
||||
let defaults: serde_json::Map<String, serde_json::Value> = info.params.iter().map(|p| (p.id.to_string(), json!(p.default))).collect();
|
||||
let _ = app.run("mix.insert_params", json!({"track": tid.0, "slot": slot, "params": defaults, "preset": "<factory default>"}));
|
||||
ui.close();
|
||||
}
|
||||
let Some(dir) = preset_dir(app, info.id) else {
|
||||
ui.label("User presets need the desktop app.");
|
||||
return;
|
||||
};
|
||||
let mut names: Vec<String> = std::fs::read_dir(&dir)
|
||||
.map(|rd| rd.flatten().filter_map(|e| e.path().file_stem().and_then(|x| x.to_str()).map(str::to_string)).collect())
|
||||
.unwrap_or_default();
|
||||
names.sort();
|
||||
for n in &names {
|
||||
if ui.button(n).clicked() {
|
||||
if let Ok(text) = std::fs::read_to_string(dir.join(format!("{n}.json")))
|
||||
&& let Ok(v) = serde_json::from_str::<serde_json::Value>(&text)
|
||||
{
|
||||
let _ = app.run(
|
||||
"mix.insert_params",
|
||||
json!({"track": tid.0, "slot": slot, "params": v.get("params").cloned().unwrap_or_default(), "preset": n}),
|
||||
);
|
||||
}
|
||||
ui.close();
|
||||
}
|
||||
}
|
||||
ui.separator();
|
||||
let key = egui::Id::new(("preset_name", tid.0, slot));
|
||||
let mut name: String = ui.ctx().memory(|m| m.data.get_temp(key)).unwrap_or_else(|| format!("{} preset", info.name));
|
||||
ui.horizontal(|ui| {
|
||||
ui.add(egui::TextEdit::singleline(&mut name).desired_width(140.0));
|
||||
if ui.button("Save").clicked() {
|
||||
let clean = soundcraft_engine::io::sanitize_name(&name);
|
||||
let body = json!({"format": "soundcraft-plugin-preset", "plugin": info.id, "params": ins.params});
|
||||
let ok = std::fs::create_dir_all(&dir).is_ok() && std::fs::write(dir.join(format!("{clean}.json")), body.to_string()).is_ok();
|
||||
app.ui.status = if ok { format!("Saved preset {clean}") } else { "Could not save the preset".into() };
|
||||
if ok {
|
||||
let _ = app.run("mix.insert_params", json!({"track": tid.0, "slot": slot, "params": {}, "preset": clean}));
|
||||
}
|
||||
}
|
||||
});
|
||||
ui.ctx().memory_mut(|m| m.data.insert_temp(key, name));
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue