1use sva_formula::filter::Shape;
4
5use crate::biquad::{Coeffs, clamp_cutoff, clamp_gain, 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: 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: 1.0,
54 });
55 let shape = self.shape;
56 let cutoff = clamp_cutoff(mid.cutoff, sample_rate).0;
57 let gain = clamp_gain(mid.gain).0;
58 let q = clamp_q(shape, cutoff, mid.q, gain, sample_rate).0;
59 let coefficients = design(shape, cutoff, q, gain, sample_rate);
60 Automation {
61 node: self.node.clone(),
62 site: self.site,
63 channel: self.channel,
64 shape: self.shape,
65 clamped: self.clamped,
66 response_db: response(&coefficients, cutoff, sample_rate),
67 coefficients,
68 frames,
69 }
70 }
71}
72
73#[inline]
75fn part<T: Copy>(v: &[T], c: usize) -> T {
76 v[c.min(v.len() - 1)]
77}
78
79#[inline]
80#[allow(clippy::too_many_arguments)]
81fn step(
82 lane: &mut Lane,
83 shape: Shape,
84 stride: usize,
85 clamped: &mut bool,
86 x: f64,
87 cutoff: f64,
88 q: f64,
89 gain: f64,
90 sr: f64,
91 i: i64,
92) -> f64 {
93 if (cutoff, q, gain) != lane.designed_for {
94 let (coeffs, hit) = coefficients(shape, cutoff, q, gain, sr);
95 lane.coeffs = coeffs;
96 lane.designed_for = (cutoff, q, gain);
97 *clamped |= hit;
98 }
99 if i.rem_euclid(stride as i64) == 0 {
100 lane.frames.push(AutomationFrame {
101 t_secs: i as f64 / sr,
102 cutoff,
103 q,
104 gain,
105 });
106 }
107
108 lane.state.step(&lane.coeffs, x)
109}
110
111fn nearest(frames: &[AutomationFrame], t_secs: f64) -> Option<AutomationFrame> {
114 frames
115 .iter()
116 .min_by(|a, b| {
117 (a.t_secs - t_secs)
118 .abs()
119 .total_cmp(&(b.t_secs - t_secs).abs())
120 })
121 .copied()
122}
123
124#[derive(Clone)]
125struct Lane {
126 state: crate::biquad::State,
127 coeffs: Coeffs,
128 designed_for: (f64, f64, f64),
129 frames: Vec<AutomationFrame>,
130}
131
132pub fn coefficients(shape: Shape, cutoff: f64, q: f64, gain: f64, sr: f64) -> (Coeffs, bool) {
133 let (c, c_clamped) = clamp_cutoff(cutoff, sr);
134 let (g, g_clamped) = match shape.takes_gain() {
135 true => clamp_gain(gain),
136 false => (1.0, false),
137 };
138 let (qq, q_clamped) = clamp_q(shape, c, q, g, sr);
139 (
140 design(shape, c, qq, g, sr),
141 c_clamped || q_clamped || g_clamped,
142 )
143}
144
145impl Lane {
146 fn new(shape: Shape, cutoff: f64, q: f64, gain: f64, sr: f64) -> (Lane, bool) {
147 let (coeffs, clamped) = coefficients(shape, cutoff, q, gain, sr);
148 let lane = Lane {
149 state: crate::biquad::State::default(),
150 coeffs,
151 designed_for: (cutoff, q, gain),
152 frames: Vec::new(),
153 };
154 (lane, clamped)
155 }
156}
157
158#[derive(Clone)]
161pub struct FilterSite {
162 shape: Shape,
163 lanes: Vec<Lane>,
164 clamped: bool,
165 stride: usize,
166}
167
168impl FilterSite {
169 pub fn new(
170 shape: Shape,
171 width: usize,
172 cutoff: &[f64],
173 q: &[f64],
174 gain: &[f64],
175 sr: f64,
176 ) -> FilterSite {
177 let mut lanes = Vec::with_capacity(width);
178 let mut clamped = false;
179 for c in 0..width.max(1) {
180 let (lane, hit) = Lane::new(shape, part(cutoff, c), part(q, c), part(gain, c), sr);
181 lanes.push(lane);
182 clamped |= hit;
183 }
184 FilterSite {
185 shape,
186 lanes,
187 clamped,
188 stride: ((sr / TRACE_HZ).round() as usize).max(1),
189 }
190 }
191
192 #[allow(clippy::too_many_arguments)]
195 pub fn process(
196 &mut self,
197 x: &[f64],
198 cutoff: &[f64],
199 q: &[f64],
200 gain: &[f64],
201 out: &mut [f64],
202 sr: f64,
203 i: i64,
204 ) {
205 let (shape, stride) = (self.shape, self.stride);
206 let mut clamped = self.clamped;
207 for (c, lane) in self.lanes.iter_mut().enumerate() {
208 out[c] = step(
209 lane,
210 shape,
211 stride,
212 &mut clamped,
213 part(x, c),
214 part(cutoff, c),
215 part(q, c),
216 part(gain, c),
217 sr,
218 i,
219 );
220 }
221 self.clamped = clamped;
222 }
223
224 pub fn lanes(&self) -> impl Iterator<Item = (&Coeffs, [f64; 4])> {
226 self.lanes
227 .iter()
228 .map(|lane| (&lane.coeffs, lane.state.held()))
229 }
230
231 pub fn bytes(&self) -> usize {
234 let frames: usize = self
235 .lanes
236 .iter()
237 .map(|l| std::mem::size_of_val(l.frames.as_slice()))
238 .sum();
239 size_of::<Self>() + std::mem::size_of_val(self.lanes.as_slice()) + frames
240 }
241
242 pub fn forget_frames(&mut self) {
243 for lane in &mut self.lanes {
244 lane.frames.clear();
245 }
246 }
247
248 pub fn trace(&self, node: &str, site: usize, sr: f64) -> Vec<FilterTrace> {
249 let wide = self.lanes.len() > 1;
250 self.lanes
251 .iter()
252 .enumerate()
253 .map(|(c, lane)| FilterTrace {
254 node: node.to_string(),
255 site,
256 channel: wide.then_some(c),
257 shape: self.shape,
258 clamped: self.clamped,
259 trace_secs: self.stride as f64 / sr,
260 frames: lane.frames.clone(),
261 })
262 .collect()
263 }
264}