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: Shape,
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: Shape,
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 = self.shape;
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: i64,
90) -> f64 {
91 if (cutoff, q, gain_db) != lane.designed_for {
92 let (coeffs, hit) = coefficients(shape, cutoff, q, gain_db, sr);
93 lane.coeffs = coeffs;
94 lane.designed_for = (cutoff, q, gain_db);
95 *clamped |= hit;
96 }
97 if i.rem_euclid(stride as i64) == 0 {
98 lane.frames.push(AutomationFrame {
99 t_secs: i as f64 / sr,
100 cutoff,
101 q,
102 gain_db,
103 });
104 }
105
106 lane.state.step(&lane.coeffs, x)
107}
108
109fn nearest(frames: &[AutomationFrame], t_secs: f64) -> Option<AutomationFrame> {
112 frames
113 .iter()
114 .min_by(|a, b| {
115 (a.t_secs - t_secs)
116 .abs()
117 .total_cmp(&(b.t_secs - t_secs).abs())
118 })
119 .copied()
120}
121
122#[derive(Clone)]
123struct Lane {
124 state: crate::biquad::State,
125 coeffs: Coeffs,
126 designed_for: (f64, f64, f64),
127 frames: Vec<AutomationFrame>,
128}
129
130pub fn coefficients(shape: Shape, cutoff: f64, q: f64, gain_db: f64, sr: f64) -> (Coeffs, bool) {
131 let (c, c_clamped) = clamp_cutoff(cutoff, sr);
132 let (qq, q_clamped) = clamp_q(q);
133 (design(shape, c, qq, gain_db, sr), c_clamped || q_clamped)
134}
135
136impl Lane {
137 fn new(shape: Shape, cutoff: f64, q: f64, gain_db: f64, sr: f64) -> (Lane, bool) {
138 let (coeffs, clamped) = coefficients(shape, cutoff, q, gain_db, sr);
139 let lane = Lane {
140 state: crate::biquad::State::default(),
141 coeffs,
142 designed_for: (cutoff, q, gain_db),
143 frames: Vec::new(),
144 };
145 (lane, clamped)
146 }
147}
148
149#[derive(Clone)]
152pub struct FilterSite {
153 shape: Shape,
154 lanes: Vec<Lane>,
155 clamped: bool,
156 stride: usize,
157}
158
159impl FilterSite {
160 pub fn new(
161 shape: Shape,
162 width: usize,
163 cutoff: &[f64],
164 q: &[f64],
165 gain_db: &[f64],
166 sr: f64,
167 ) -> FilterSite {
168 let mut lanes = Vec::with_capacity(width);
169 let mut clamped = false;
170 for c in 0..width.max(1) {
171 let (lane, hit) = Lane::new(shape, part(cutoff, c), part(q, c), part(gain_db, c), sr);
172 lanes.push(lane);
173 clamped |= hit;
174 }
175 FilterSite {
176 shape,
177 lanes,
178 clamped,
179 stride: ((sr / TRACE_HZ).round() as usize).max(1),
180 }
181 }
182
183 #[allow(clippy::too_many_arguments)]
186 pub fn process(
187 &mut self,
188 x: &[f64],
189 cutoff: &[f64],
190 q: &[f64],
191 gain_db: &[f64],
192 out: &mut [f64],
193 sr: f64,
194 i: i64,
195 ) {
196 let (shape, stride) = (self.shape, self.stride);
197 let mut clamped = self.clamped;
198 for (c, lane) in self.lanes.iter_mut().enumerate() {
199 out[c] = step(
200 lane,
201 shape,
202 stride,
203 &mut clamped,
204 part(x, c),
205 part(cutoff, c),
206 part(q, c),
207 part(gain_db, c),
208 sr,
209 i,
210 );
211 }
212 self.clamped = clamped;
213 }
214
215 pub fn lanes(&self) -> impl Iterator<Item = (&Coeffs, [f64; 4])> {
217 self.lanes
218 .iter()
219 .map(|lane| (&lane.coeffs, lane.state.held()))
220 }
221
222 pub fn bytes(&self) -> usize {
225 let frames: usize = self
226 .lanes
227 .iter()
228 .map(|l| std::mem::size_of_val(l.frames.as_slice()))
229 .sum();
230 size_of::<Self>() + std::mem::size_of_val(self.lanes.as_slice()) + frames
231 }
232
233 pub fn forget_frames(&mut self) {
234 for lane in &mut self.lanes {
235 lane.frames.clear();
236 }
237 }
238
239 pub fn trace(&self, node: &str, site: usize, sr: f64) -> Vec<FilterTrace> {
240 let wide = self.lanes.len() > 1;
241 self.lanes
242 .iter()
243 .enumerate()
244 .map(|(c, lane)| FilterTrace {
245 node: node.to_string(),
246 site,
247 channel: wide.then_some(c),
248 shape: self.shape,
249 clamped: self.clamped,
250 trace_secs: self.stride as f64 / sr,
251 frames: lane.frames.clone(),
252 })
253 .collect()
254 }
255}