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
123struct Lane {
124 state: crate::biquad::State,
125 coeffs: Coeffs,
126 designed_for: (f64, f64, f64),
127 frames: Vec<AutomationFrame>,
128}
129
130impl Lane {
131 fn new(shape: Shape, cutoff: f64, q: f64, gain_db: f64, sr: f64) -> (Lane, bool) {
132 let (c, c_clamped) = clamp_cutoff(cutoff, sr);
133 let (qq, q_clamped) = clamp_q(q);
134 let lane = Lane {
135 state: crate::biquad::State::default(),
136 coeffs: design(shape, c, qq, gain_db, sr),
137 designed_for: (cutoff, q, gain_db),
138 frames: Vec::new(),
139 };
140 (lane, c_clamped || q_clamped)
141 }
142}
143
144pub struct FilterSite {
147 shape: Shape,
148 lanes: Vec<Lane>,
149 clamped: bool,
150 stride: usize,
151}
152
153impl FilterSite {
154 pub fn new(
155 shape: Shape,
156 width: usize,
157 cutoff: &[f64],
158 q: &[f64],
159 gain_db: &[f64],
160 sr: f64,
161 ) -> FilterSite {
162 let mut lanes = Vec::with_capacity(width);
163 let mut clamped = false;
164 for c in 0..width.max(1) {
165 let (lane, hit) = Lane::new(shape, part(cutoff, c), part(q, c), part(gain_db, c), sr);
166 lanes.push(lane);
167 clamped |= hit;
168 }
169 FilterSite {
170 shape,
171 lanes,
172 clamped,
173 stride: ((sr / TRACE_HZ).round() as usize).max(1),
174 }
175 }
176
177 #[allow(clippy::too_many_arguments)]
180 pub fn process(
181 &mut self,
182 x: &[f64],
183 cutoff: &[f64],
184 q: &[f64],
185 gain_db: &[f64],
186 out: &mut [f64],
187 sr: f64,
188 i: usize,
189 ) {
190 let (shape, stride) = (self.shape, self.stride);
191 let mut clamped = self.clamped;
192 for (c, lane) in self.lanes.iter_mut().enumerate() {
193 out[c] = step(
194 lane,
195 shape,
196 stride,
197 &mut clamped,
198 part(x, c),
199 part(cutoff, c),
200 part(q, c),
201 part(gain_db, c),
202 sr,
203 i,
204 );
205 }
206 self.clamped = clamped;
207 }
208
209 pub fn trace(&self, node: &str, site: usize, sr: f64) -> Vec<FilterTrace> {
210 let wide = self.lanes.len() > 1;
211 self.lanes
212 .iter()
213 .enumerate()
214 .map(|(c, lane)| FilterTrace {
215 node: node.to_string(),
216 site,
217 channel: wide.then_some(c),
218 shape: self.shape.name(),
219 clamped: self.clamped,
220 trace_secs: self.stride as f64 / sr,
221 frames: lane.frames.clone(),
222 })
223 .collect()
224 }
225}