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