Skip to main content

pedalkernel/compiler/
split.rs

1//! FX loop split: partition a PedalDef at fx_send/fx_return boundaries.
2
3use hashbrown::HashMap;
4
5use crate::dsl::*;
6use crate::PedalProcessor;
7
8use super::compile::compile_pedal;
9use super::compiled::CompiledPedal;
10
11/// Extract the primary node name from a Pin for graph connectivity purposes.
12fn pin_node_name(pin: &Pin) -> String {
13    match pin {
14        Pin::Reserved(n) => n.clone(),
15        Pin::ComponentPin { component, .. } => component.clone(),
16        // For Fork, we use the switch component as the node
17        Pin::Fork { switch, .. } => switch.clone(),
18        // SubcircuitPort references are resolved before split
19        Pin::SubcircuitPort { subcircuit, .. } => subcircuit.clone(),
20    }
21}
22
23// ═══════════════════════════════════════════════════════════════════════════
24// FX loop split — partition a PedalDef at fx_send/fx_return
25// ═══════════════════════════════════════════════════════════════════════════
26
27/// Split a pedal definition at its `fx_send`/`fx_return` boundary into
28/// pre-section and post-section `PedalDef`s.
29///
30/// Returns `None` if the pedal has no FX loop (no `fx_send`/`fx_return`).
31pub fn split_pedal_def(pedal: &PedalDef) -> Option<(PedalDef, PedalDef)> {
32    if !pedal.has_fx_loop() {
33        return None;
34    }
35
36    // Collect all node names that each component touches.
37    let mut component_nodes: HashMap<String, Vec<String>> = HashMap::new();
38    for net in &pedal.nets {
39        let node_name = pin_node_name(&net.from);
40        for pin in &net.to {
41            let to_name = pin_node_name(pin);
42            component_nodes
43                .entry(node_name.clone())
44                .or_default()
45                .push(to_name.clone());
46            component_nodes
47                .entry(to_name)
48                .or_default()
49                .push(node_name.clone());
50        }
51    }
52
53    // BFS from "in" — collect all component IDs reachable WITHOUT crossing fx_send→fx_return.
54    // Stop at fx_send (include it as a boundary but don't cross to fx_return).
55    let pre_components = bfs_collect_components(&component_nodes, "in", "fx_send");
56    let post_components = bfs_collect_components(&component_nodes, "fx_return", "out");
57
58    // Build pre PedalDef: filter to pre components, replace fx_send→out
59    let pre_def = build_half(pedal, &pre_components, "fx_send", "out", "pre");
60    // Build post PedalDef: filter to post components, replace fx_return→in
61    let post_def = build_half(pedal, &post_components, "fx_return", "in", "post");
62
63    Some((pre_def, post_def))
64}
65
66/// BFS from `start_node` through component adjacency, collecting component IDs.
67/// Stops at `stop_node` (does not cross it to reach further components).
68fn bfs_collect_components(
69    adj: &HashMap<String, Vec<String>>,
70    start_node: &str,
71    stop_node: &str,
72) -> std::collections::HashSet<String> {
73    use std::collections::{HashSet, VecDeque};
74    let mut visited = HashSet::new();
75    let mut queue = VecDeque::new();
76    queue.push_back(start_node.to_string());
77    visited.insert(start_node.to_string());
78    // Always include gnd and vcc — they're shared by all stages
79    visited.insert("gnd".to_string());
80    visited.insert("vcc".to_string());
81
82    while let Some(node) = queue.pop_front() {
83        if node == stop_node {
84            // Include the stop node itself but don't traverse its neighbors
85            continue;
86        }
87        if let Some(neighbors) = adj.get(&node) {
88            for nb in neighbors {
89                if !visited.contains(nb) {
90                    visited.insert(nb.clone());
91                    queue.push_back(nb.clone());
92                }
93            }
94        }
95    }
96    visited
97}
98
99/// Build one half of a split pedal definition.
100fn build_half(
101    pedal: &PedalDef,
102    component_ids: &std::collections::HashSet<String>,
103    old_node: &str,
104    new_node: &str,
105    suffix: &str,
106) -> PedalDef {
107    // Filter components to those in this half
108    let components: Vec<ComponentDef> = pedal
109        .components
110        .iter()
111        .filter(|c| component_ids.contains(&c.id))
112        .cloned()
113        .collect();
114
115    // Rename the boundary node and filter nets
116    let rename = |pin: &Pin| -> Pin {
117        match pin {
118            Pin::Reserved(n) if n == old_node => Pin::Reserved(new_node.to_string()),
119            // Drop the other fx node — it doesn't exist in this half
120            Pin::Reserved(n) if (n == "fx_send" || n == "fx_return") && n != old_node => {
121                pin.clone() // will be filtered out
122            }
123            _ => pin.clone(),
124        }
125    };
126
127    let nets: Vec<NetDef> = pedal
128        .nets
129        .iter()
130        .filter_map(|net| {
131            let from = rename(&net.from);
132            let to: Vec<Pin> = net.to.iter().map(&rename).collect();
133
134            // A pin is "relevant" if it references a component in this half
135            // or is a standard reserved node (in, out, gnd, vcc, or a supply rail).
136            let relevant = |p: &Pin| match p {
137                Pin::Reserved(n) => {
138                    n == "in" || n == "out" || n == "gnd" || n == "vcc" || n == new_node
139                        || pedal.is_supply_rail(n)
140                }
141                Pin::ComponentPin { component, .. } => component_ids.contains(component),
142                Pin::Fork { switch, destinations } => {
143                    component_ids.contains(switch)
144                        || destinations.iter().any(|d| match d {
145                            Pin::ComponentPin { component, .. } => component_ids.contains(component),
146                            _ => false,
147                        })
148                }
149                Pin::SubcircuitPort { .. } => false,
150            };
151
152            // A pin "belongs" to this half — only component pins in our set.
153            let belongs = |p: &Pin| match p {
154                Pin::Reserved(_) => true,
155                Pin::ComponentPin { component, .. } => component_ids.contains(component),
156                Pin::Fork { switch, .. } => component_ids.contains(switch),
157                Pin::SubcircuitPort { .. } => false,
158            };
159
160            // Keep this net only if at least one component pin belongs to this half.
161            // This avoids orphan nets like `C6.b -> out` when C6 is in the other half.
162            let all_pins = std::iter::once(&from).chain(to.iter());
163            let has_local_component = all_pins.clone().any(|p| {
164                matches!(p, Pin::ComponentPin { component, .. } if component_ids.contains(component))
165            });
166
167            if has_local_component {
168                let filtered_to: Vec<Pin> = to.into_iter().filter(|p| belongs(p)).collect();
169                if !filtered_to.is_empty() && relevant(&from) {
170                    Some(NetDef {
171                        from,
172                        to: filtered_to,
173                    })
174                } else {
175                    None
176                }
177            } else {
178                None
179            }
180        })
181        .collect();
182
183    // Filter controls to those referencing components in this half
184    let controls: Vec<ControlDef> = pedal
185        .controls
186        .iter()
187        .filter(|c| component_ids.contains(&c.component))
188        .cloned()
189        .collect();
190
191    // Filter trims to those referencing components in this half
192    let trims: Vec<ControlDef> = pedal
193        .trims
194        .iter()
195        .filter(|c| component_ids.contains(&c.component))
196        .cloned()
197        .collect();
198
199    let monitors: Vec<MonitorDef> = pedal
200        .monitors
201        .iter()
202        .filter(|m| {
203            component_ids.contains(&m.component)
204                || m.component == "input"
205                || m.component == "output"
206        })
207        .cloned()
208        .collect();
209
210    // Filter mirrors to only include components in this half
211    let mirrors = pedal
212        .mirrors
213        .iter()
214        .filter(|(k, v)| component_ids.contains(*k) && component_ids.contains(*v))
215        .map(|(k, v)| (k.clone(), v.clone()))
216        .collect();
217
218    PedalDef {
219        name: format!("{} ({})", pedal.name, suffix),
220        subtitle: None,
221        supplies: pedal.supplies.clone(),
222        components,
223        nets,
224        controls,
225        trims,
226        monitors,
227        sidechains: vec![],
228        mirrors,
229        calibrate: false,
230        subcircuits: vec![],
231        ports: vec![],
232        init_hints: vec![],
233        uses: vec![],
234    }
235}
236
237/// A split compiled pedal — pre-section and post-section with an FX loop between them.
238pub struct SplitCompiledPedal {
239    pub pre: CompiledPedal,
240    pub post: CompiledPedal,
241}
242
243impl SplitCompiledPedal {
244    /// Process with no FX loop — direct connection from pre to post.
245    pub fn process(&mut self, input: f64) -> f64 {
246        let send = self.pre.process(input as crate::Wave);
247        self.post.process(send) as f64
248    }
249
250    /// Process the pre-section only, returning the FX send signal.
251    pub fn process_pre(&mut self, input: f64) -> f64 {
252        self.pre.process(input as crate::Wave) as f64
253    }
254
255    /// Process the post-section only, taking the FX return signal.
256    pub fn process_post(&mut self, fx_return: f64) -> f64 {
257        self.post.process(fx_return as crate::Wave) as f64
258    }
259
260    pub fn set_sample_rate(&mut self, rate: f64) {
261        self.pre.set_sample_rate(rate as crate::Wave);
262        self.post.set_sample_rate(rate as crate::Wave);
263    }
264
265    pub fn reset(&mut self) {
266        self.pre.reset();
267        self.post.reset();
268    }
269
270    pub fn set_control(&mut self, label: &str, value: f64) {
271        // Try both halves — controls are partitioned but the user shouldn't
272        // need to know which half a control belongs to.
273        self.pre.set_control(label, value as crate::Wave);
274        self.post.set_control(label, value as crate::Wave);
275    }
276
277    /// Get the input impedance of the split pedal (Ω).
278    ///
279    /// Returns the input impedance of the pre-section.
280    pub fn input_impedance(&self) -> f64 {
281        self.pre.input_impedance() as f64
282    }
283
284    /// Get the output impedance of the split pedal (Ω).
285    ///
286    /// Returns the output impedance of the post-section.
287    pub fn output_impedance(&self) -> f64 {
288        self.post.output_impedance() as f64
289    }
290}
291
292/// Compile a pedal with an FX loop, splitting at `fx_send`/`fx_return`.
293///
294/// Returns `Ok(SplitCompiledPedal)` if the pedal has send/return nodes.
295/// Returns `Err` if the pedal has no FX loop or compilation fails.
296pub fn compile_split_pedal(
297    pedal: &PedalDef,
298    sample_rate: f64,
299) -> Result<SplitCompiledPedal, String> {
300    let (pre_def, post_def) =
301        split_pedal_def(pedal).ok_or_else(|| "Pedal has no fx_send/fx_return nodes".to_string())?;
302
303    let pre = compile_pedal(&pre_def, sample_rate)
304        .map_err(|e| format!("Failed to compile pre-section: {e}"))?;
305    let post = compile_pedal(&post_def, sample_rate)
306        .map_err(|e| format!("Failed to compile post-section: {e}"))?;
307
308    Ok(SplitCompiledPedal { pre, post })
309}