Skip to main content

sound_to_patch/preset/
apply.rs

1//! Applies resolved edits to a model through the format traits. The
2//! applier is synth-neutral: the registry supplies keys, ranges and
3//! defaults, and the format supplies the route rules and the final scan.
4
5use std::collections::HashMap;
6
7use crate::preset::edits::{EditError, EditErrors, Resolved};
8use crate::preset::format::{FormatError, PresetFormat, PresetModel, Route, RoutePolarity, Stored};
9use crate::preset::registry::ParamRegistry;
10
11/// What one edit did.
12#[derive(Clone, Debug, PartialEq)]
13pub struct Change {
14    /// The key, or `slot N` for a route.
15    pub target: String,
16    /// The value written, as text.
17    pub value: String,
18    /// The law of the control is interim.
19    pub uncalibrated: bool,
20    /// The key is outside the registry.
21    pub raw: bool,
22    /// Why.
23    pub reason: Option<String>,
24}
25
26/// An edit that changed nothing, and why.
27#[derive(Clone, Debug, PartialEq, Eq)]
28pub struct Skipped {
29    /// The key.
30    pub target: String,
31    /// Why it was skipped.
32    pub why: String,
33}
34
35/// What an edit list did to a model.
36#[derive(Clone, Debug, Default, PartialEq)]
37pub struct ApplyReport {
38    /// The changes, in edit order.
39    pub changes: Vec<Change>,
40    /// The edits that changed nothing.
41    pub skipped: Vec<Skipped>,
42    /// Warnings of the format scan.
43    pub warnings: Vec<String>,
44}
45
46fn as_number(value: &Stored) -> Option<f64> {
47    match value {
48        Stored::Float(number) => Some(*number),
49        Stored::Int(number) => Some(crate::cast::i64_to_f64(*number)),
50        Stored::Flag(_) | Stored::Text(_) => None,
51    }
52}
53
54/// Puts a route in the slot that already joins the same source and
55/// destination, or in the first free slot.
56///
57/// # Errors
58///
59/// A [`FormatError`] that names the slot limit when every slot is taken,
60/// or the error of [`PresetModel::set_route`].
61pub fn place_route(model: &mut dyn PresetModel, route: Route) -> Result<usize, FormatError> {
62    let routes = model.routes();
63    let same = routes.iter().position(|slot| {
64        slot.as_ref().is_some_and(|have| {
65            have.source == route.source && have.destination == route.destination
66        })
67    });
68    let slot = same
69        .or_else(|| routes.iter().position(Option::is_none))
70        .ok_or_else(|| {
71            FormatError::new(
72                "cannot add the route",
73                format!("all {} modulation slots are taken", routes.len()),
74                "Remove a route, or reuse a slot with the same source and destination",
75            )
76        })?;
77    model.set_route(slot, Some(route))?;
78    Ok(slot)
79}
80
81/// Applies resolved edits in order, then runs the format scan.
82///
83/// A parameter or route equal to what the model already holds changes
84/// nothing. Its skip says `equals the template` when the model held it
85/// before the run, and `equals an earlier edit` when an edit of the same
86/// run set it, such as a route that wrote the amount of its slot. A
87/// parameter equal to its default is skipped when the key is absent and
88/// the format reads an absent key as the default.
89///
90/// # Errors
91///
92/// [`EditErrors`] with one entry per edit the format refuses, or with the
93/// scan error. The model then holds every edit that succeeded, before and
94/// after a failing one.
95pub fn apply(
96    format: &dyn PresetFormat,
97    registry: &dyn ParamRegistry,
98    model: &mut dyn PresetModel,
99    resolved: &[Resolved],
100) -> Result<ApplyReport, EditErrors> {
101    let mut report = ApplyReport::default();
102    let mut errors = Vec::new();
103    // What the model held before the run, so a skip can tell the template
104    // from an earlier edit.
105    let held: HashMap<&str, Option<Stored>> = resolved
106        .iter()
107        .filter_map(|edit| match edit {
108            Resolved::Param { key, .. } => Some((key.as_str(), model.get(key))),
109            Resolved::Route { .. } | Resolved::Raw { .. } => None,
110        })
111        .collect();
112    let routes_held = model.routes();
113    for (index, edit) in resolved.iter().enumerate() {
114        let result = match edit {
115            Resolved::Param {
116                key,
117                stored,
118                uncalibrated,
119                reason,
120            } => apply_param(
121                format,
122                registry,
123                model,
124                key,
125                *stored,
126                held.get(key.as_str()),
127                &mut report,
128            )
129            .map(|changed| {
130                if changed {
131                    let physical = registry.get(key).map_or_else(String::new, |spec| {
132                        shown_value(spec, *stored)
133                            .map_or_else(String::new, |text| format!("{text}, "))
134                    });
135                    report.changes.push(Change {
136                        target: key.clone(),
137                        value: format!("{physical}stored {stored:?}"),
138                        uncalibrated: *uncalibrated,
139                        raw: false,
140                        reason: reason.clone(),
141                    });
142                }
143            }),
144            Resolved::Route {
145                source,
146                destination,
147                amount,
148                polarity,
149                reason,
150            } => apply_route(format, model, source, destination, *amount, *polarity).map(|slot| {
151                match slot {
152                    Some(slot) => report.changes.push(Change {
153                        target: format!("slot {}", slot + 1),
154                        value: format!("{source} -> {destination}, amount {amount:?}"),
155                        uncalibrated: false,
156                        raw: false,
157                        reason: reason.clone(),
158                    }),
159                    None => report.skipped.push(Skipped {
160                        target: format!("{source} -> {destination}"),
161                        why: route_skip(&routes_held, &model.routes(), source, destination),
162                    }),
163                }
164            }),
165            Resolved::Raw { key, value, reason } => model.set(key, value.clone()).map(|()| {
166                report.changes.push(Change {
167                    target: key.clone(),
168                    value: value.to_string(),
169                    uncalibrated: false,
170                    raw: true,
171                    reason: reason.clone(),
172                });
173            }),
174        };
175        if let Err(error) = result {
176            errors.push(EditError {
177                place: format!("edit {}", index + 1),
178                cause: format!("{}: {}", error.what, error.cause),
179                fix: error.fix,
180            });
181        }
182    }
183    if !errors.is_empty() {
184        return Err(EditErrors(errors));
185    }
186    let edited: Vec<String> = resolved
187        .iter()
188        .filter_map(|edit| match edit {
189            Resolved::Param { key, .. } | Resolved::Raw { key, .. } => Some(key.clone()),
190            Resolved::Route { .. } => None,
191        })
192        .collect();
193    let edited: Vec<&str> = edited.iter().map(String::as_str).collect();
194    report.warnings = format.scan(model, &edited).map_err(|error| {
195        EditErrors(vec![EditError {
196            place: "the edited preset".to_owned(),
197            cause: format!("{}: {}", error.what, error.cause),
198            fix: error.fix,
199        }])
200    })?;
201    Ok(report)
202}
203
204/// A stored value as people read it: the physical value, the menu label
205/// or on and off.
206fn shown_value(spec: &crate::preset::registry::ParamSpec, stored: f64) -> Option<String> {
207    use crate::preset::registry::ParamKind;
208    match spec.kind {
209        ParamKind::Flag => Some(if stored == 0.0 { "off" } else { "on" }.to_owned()),
210        ParamKind::Choice => {
211            let index = (stored - spec.stored_min).max(0.0);
212            spec.choices
213                .iter()
214                .zip(0u32..)
215                .find(|(_, i)| (f64::from(*i) - index).abs() < 0.5)
216                .map(|(label, _)| label.clone())
217        }
218        ParamKind::Float | ParamKind::Count => spec.physical(stored).ok().map(|value| {
219            // The law round trip leaves noise in the last digits, such as
220            // 400.00000000000017 Hz; 12 significant digits drop it.
221            let tidy = format!("{:.11e}", value.get())
222                .parse()
223                .unwrap_or(value.get());
224            crate::preset::registry::Physical::new(value.kind(), tidy)
225                .map_or_else(|_| value.to_string(), |tidy| tidy.to_string())
226        }),
227    }
228}
229
230/// The skip text of an edit equal to the model: the model held the value
231/// before the run, or an earlier edit of the run set it.
232fn skip_why(held_before: bool) -> String {
233    if held_before {
234        "equals the template"
235    } else {
236        "equals an earlier edit"
237    }
238    .to_owned()
239}
240
241/// The skip text of a route equal to the model's: the same route, amount
242/// and polarity stood in the model before the run, or an earlier edit
243/// set its amount or polarity.
244fn route_skip(
245    before: &[Option<Route>],
246    now: &[Option<Route>],
247    source: &str,
248    destination: &str,
249) -> String {
250    let find = |routes: &[Option<Route>]| {
251        routes
252            .iter()
253            .flatten()
254            .find(|have| have.source == source && have.destination == destination)
255            .map(|have| (have.amount.to_bits(), have.polarity))
256    };
257    skip_why(find(before).is_some() && find(before) == find(now))
258}
259
260fn apply_param(
261    format: &dyn PresetFormat,
262    registry: &dyn ParamRegistry,
263    model: &mut dyn PresetModel,
264    key: &str,
265    stored: f64,
266    held: Option<&Option<Stored>>,
267    report: &mut ApplyReport,
268) -> Result<bool, FormatError> {
269    format.check_value(key, stored)?;
270    let current = model.get(key);
271    if current
272        .as_ref()
273        .and_then(as_number)
274        .is_some_and(|value| value.to_bits() == stored.to_bits())
275    {
276        report.skipped.push(Skipped {
277            target: key.to_owned(),
278            why: skip_why(held.is_none_or(|held| *held == current)),
279        });
280        return Ok(false);
281    }
282    let is_default = registry
283        .get(key)
284        .is_some_and(|spec| spec.default.to_bits() == stored.to_bits());
285    if current.is_none() && is_default && format.absent_means_default(key) {
286        report.skipped.push(Skipped {
287            target: key.to_owned(),
288            why: "equals the default".to_owned(),
289        });
290        return Ok(false);
291    }
292    model.set(key, Stored::Float(stored))?;
293    Ok(true)
294}
295
296fn apply_route(
297    format: &dyn PresetFormat,
298    model: &mut dyn PresetModel,
299    source: &str,
300    destination: &str,
301    amount: f64,
302    polarity: Option<RoutePolarity>,
303) -> Result<Option<usize>, FormatError> {
304    if !format.route_sources().contains(&source) {
305        return Err(FormatError::new(
306            "cannot add the route",
307            format!("{source:?} is not a modulation source"),
308            format!("Use one of: {}", format.route_sources().join(", ")),
309        ));
310    }
311    // An existing route keeps its polarity and extra controls unless the
312    // edit sets them, and an identical one changes nothing.
313    let existing = model
314        .routes()
315        .into_iter()
316        .flatten()
317        .find(|have| have.source == source && have.destination == destination);
318    let polarity = polarity
319        .or(existing.as_ref().map(|have| have.polarity))
320        .unwrap_or_else(|| format.default_polarity(source));
321    if existing
322        .as_ref()
323        .is_some_and(|have| have.amount.to_bits() == amount.to_bits() && have.polarity == polarity)
324    {
325        return Ok(None);
326    }
327    place_route(
328        model,
329        Route {
330            source: source.to_owned(),
331            destination: destination.to_owned(),
332            amount,
333            polarity,
334            extras: existing.map(|have| have.extras).unwrap_or_default(),
335        },
336    )
337    .map(Some)
338}