1use sva_formula::filter::Shape;
4
5use crate::biquad::{Coeffs, clamp_cutoff, clamp_q, design, response};
6
7pub const TRACE_HZ: f64 = 1000.0;
8
9#[derive(Clone, Copy, Debug, PartialEq)]
10pub struct AutomationFrame {
11 pub t_secs: f64,
12 pub cutoff: f64,
13 pub q: f64,
14 pub gain_db: f64,
15}
16
17#[derive(Clone, Debug, PartialEq)]
18pub struct FilterTrace {
19 pub node: String,
20 pub site: usize,
21 pub channel: Option<usize>,
22 pub shape: &'static str,
23 pub clamped: bool,
24 pub trace_secs: f64,
25 pub frames: Vec<AutomationFrame>,
26}
27
28#[derive(Clone, Debug, PartialEq)]
29pub struct Automation {
30 pub node: String,
31 pub site: usize,
32 pub channel: Option<usize>,
33 pub shape: &'static str,
34 pub clamped: bool,
35 pub frames: Vec<AutomationFrame>,
36 pub coefficients: Coeffs,
37 pub response_db: Vec<(f64, f64)>,
38}
39
40impl FilterTrace {
41 pub fn over(&self, start_secs: f64, end_secs: f64, sample_rate: f64) -> Automation {
42 let frames: Vec<AutomationFrame> = self
43 .frames
44 .iter()
45 .copied()
46 .filter(|f| f.t_secs >= start_secs && f.t_secs < end_secs)
47 .collect();
48 let midpoint = (start_secs + end_secs) / 2.0;
49 let mid = nearest(&self.frames, midpoint).unwrap_or(AutomationFrame {
50 t_secs: midpoint,
51 cutoff: 0.0,
52 q: 1.0,
53 gain_db: 0.0,
54 });
55 let shape = Shape::from_name(self.shape).expect("shape came from Shape::name");
56 let cutoff = clamp_cutoff(mid.cutoff, sample_rate).0;
57 let coefficients = design(shape, cutoff, clamp_q(mid.q).0, mid.gain_db, sample_rate);
58 Automation {
59 node: self.node.clone(),
60 site: self.site,
61 channel: self.channel,
62 shape: self.shape,
63 clamped: self.clamped,
64 response_db: response(&coefficients, cutoff, sample_rate),
65 coefficients,
66 frames,
67 }
68 }
69}
70
71#[inline]
73fn part<T: Copy>(v: &[T], c: usize) -> T {
74 v[c.min(v.len() - 1)]
75}
76
77#[inline]
78#[allow(clippy::too_many_arguments)]
79fn step(
80 lane: &mut Lane,
81 shape: Shape,
82 stride: usize,
83 clamped: &mut bool,
84 x: f64,
85 cutoff: f64,
86 q: f64,
87 gain_db: f64,
88 sr: f64,
89 i: usize,
90) -> f64 {
91 if (cutoff, q, gain_db) != lane.designed_for {
92 let (cc, c_clamped) = clamp_cutoff(cutoff, sr);
93 let (qq, q_clamped) = clamp_q(q);
94 lane.coeffs = design(shape, cc, qq, gain_db, sr);
95 lane.designed_for = (cutoff, q, gain_db);
96 *clamped |= c_clamped || q_clamped;
97 }
98 if i.is_multiple_of(stride) {
99 lane.frames.push(AutomationFrame {
100 t_secs: i as f64 / sr,
101 cutoff,
102 q,
103 gain_db,
104 });
105 }
106
107 lane.state.step(&lane.coeffs, x)
108}
109
110fn nearest(frames: &[AutomationFrame], t_secs: f64) -> Option<AutomationFrame> {
113 frames
114 .iter()
115 .min_by(|a, b| {
116 (a.t_secs - t_secs)
117 .abs()
118 .total_cmp(&(b.t_secs - t_secs).abs())
119 })
120 .copied()
121}
122
123#[derive(Clone)]
124struct Lane {
125 state: crate::biquad::State,
126 coeffs: Coeffs,
127 designed_for: (f64, f64, f64),
128 frames: Vec<AutomationFrame>,
129}
130
131impl Lane {
132 fn new(shape: Shape, cutoff: f64, q: f64, gain_db: f64, sr: f64) -> (Lane, bool) {
133 let (c, c_clamped) = clamp_cutoff(cutoff, sr);
134 let (qq, q_clamped) = clamp_q(q);
135 let lane = Lane {
136 state: crate::biquad::State::default(),
137 coeffs: design(shape, c, qq, gain_db, sr),
138 designed_for: (cutoff, q, gain_db),
139 frames: Vec::new(),
140 };
141 (lane, c_clamped || q_clamped)
142 }
143}
144
145#[derive(Clone)]
148pub struct FilterSite {
149 shape: Shape,
150 lanes: Vec<Lane>,
151 clamped: bool,
152 stride: usize,
153}
154
155impl FilterSite {
156 pub fn new(
157 shape: Shape,
158 width: usize,
159 cutoff: &[f64],
160 q: &[f64],
161 gain_db: &[f64],
162 sr: f64,
163 ) -> FilterSite {
164 let mut lanes = Vec::with_capacity(width);
165 let mut clamped = false;
166 for c in 0..width.max(1) {
167 let (lane, hit) = Lane::new(shape, part(cutoff, c), part(q, c), part(gain_db, c), sr);
168 lanes.push(lane);
169 clamped |= hit;
170 }
171 FilterSite {
172 shape,
173 lanes,
174 clamped,
175 stride: ((sr / TRACE_HZ).round() as usize).max(1),
176 }
177 }
178
179 #[allow(clippy::too_many_arguments)]
182 pub fn process(
183 &mut self,
184 x: &[f64],
185 cutoff: &[f64],
186 q: &[f64],
187 gain_db: &[f64],
188 out: &mut [f64],
189 sr: f64,
190 i: usize,
191 ) {
192 let (shape, stride) = (self.shape, self.stride);
193 let mut clamped = self.clamped;
194 for (c, lane) in self.lanes.iter_mut().enumerate() {
195 out[c] = step(
196 lane,
197 shape,
198 stride,
199 &mut clamped,
200 part(x, c),
201 part(cutoff, c),
202 part(q, c),
203 part(gain_db, c),
204 sr,
205 i,
206 );
207 }
208 self.clamped = clamped;
209 }
210
211 pub fn lanes(&self) -> impl Iterator<Item = (&Coeffs, [f64; 4])> {
213 self.lanes
214 .iter()
215 .map(|lane| (&lane.coeffs, lane.state.held()))
216 }
217
218 pub fn forget_frames(&mut self) {
220 for lane in &mut self.lanes {
221 lane.frames.clear();
222 }
223 }
224
225 pub fn trace(&self, node: &str, site: usize, sr: f64) -> Vec<FilterTrace> {
226 let wide = self.lanes.len() > 1;
227 self.lanes
228 .iter()
229 .enumerate()
230 .map(|(c, lane)| FilterTrace {
231 node: node.to_string(),
232 site,
233 channel: wide.then_some(c),
234 shape: self.shape.name(),
235 clamped: self.clamped,
236 trace_secs: self.stride as f64 / sr,
237 frames: lane.frames.clone(),
238 })
239 .collect()
240 }
241}