sound_to_patch/estimate/pitch/
line.rs1use crate::audio::SampleRate;
10use crate::cast;
11use crate::dsp::{AnalyticSignal, FilterError, band_pass_zero_phase};
12use crate::units::{Hz, Seconds};
13
14pub(crate) const LINE_LO: Hz = Hz::from_const(20.0);
16pub(crate) const LINE_LO_FAST: Hz = Hz::from_const(10.0);
22pub(crate) const LINE_HI: Hz = Hz::from_const(200.0);
24pub(crate) const LINE_MEDIAN: Seconds = Seconds::from_const(0.0014);
27
28pub const LINE_CONVENTION: &str = "fourth-order Butterworth high-pass at 20 Hz and low-pass at \
30200 Hz, each forward and backward; the frequency of the analytic signal between consecutive \
31samples, arg(z[n+1] conj(z[n])) fs / 2 pi; a centred median of round(1.4 ms fs) samples, made \
32odd, shortened at the ends; the refit of a drop uses a high-pass at 10 Hz";
33
34#[derive(Clone, Debug, PartialEq)]
37pub struct SubBandLine {
38 start: Seconds,
39 rate: SampleRate,
40 hz: Vec<f64>,
41 envelope: Vec<f64>,
42}
43
44impl SubBandLine {
45 pub fn of(segment: &[f64], rate: SampleRate, start: Seconds) -> Result<Self, FilterError> {
57 Self::with_low_edge(segment, rate, start, LINE_LO)
58 }
59
60 pub fn with_low_edge(
67 segment: &[f64],
68 rate: SampleRate,
69 start: Seconds,
70 lo: Hz,
71 ) -> Result<Self, FilterError> {
72 Self::with_edges(segment, rate, start, lo, LINE_HI)
73 }
74
75 pub fn with_edges(
83 segment: &[f64],
84 rate: SampleRate,
85 start: Seconds,
86 lo: Hz,
87 hi: Hz,
88 ) -> Result<Self, FilterError> {
89 let band = band_pass_zero_phase(segment, rate, lo, hi)?;
90 let analytic = AnalyticSignal::new(&band, rate, band.len());
93 let hz = median_filter(&analytic.instantaneous_frequency(), median_len(rate));
94 Ok(Self {
95 start,
96 rate,
97 hz,
98 envelope: analytic.envelope(),
99 })
100 }
101
102 #[must_use]
105 pub fn hz(&self) -> &[f64] {
106 &self.hz
107 }
108
109 #[must_use]
111 pub fn sample_rate(&self) -> SampleRate {
112 self.rate
113 }
114
115 #[must_use]
117 pub fn envelope(&self) -> &[f64] {
118 &self.envelope
119 }
120
121 #[must_use]
124 pub fn time_of(&self, n: usize) -> Seconds {
125 Seconds::clamped(self.start.get() + (cast::usize_to_f64(n) + 0.5) / self.rate.hz().get())
126 }
127
128 #[must_use]
130 pub fn sample_time(&self, n: usize) -> Seconds {
131 Seconds::clamped(self.start.get() + cast::usize_to_f64(n) / self.rate.hz().get())
132 }
133
134 #[must_use]
136 pub fn index_at(&self, time: Seconds) -> usize {
137 let offset = (time.get() - self.start.get()) * self.rate.hz().get();
138 cast::ceil_to_usize(offset.max(0.0)).unwrap_or(0)
139 }
140}
141
142#[must_use]
144pub(crate) fn median_len(rate: SampleRate) -> usize {
145 let len = cast::round_to_usize(LINE_MEDIAN.get() * rate.hz().get()).unwrap_or(1);
146 len.max(1) | 1
147}
148
149#[must_use]
153pub(crate) fn median_filter(x: &[f64], len: usize) -> Vec<f64> {
154 let half = len / 2;
155 let mut window = Vec::with_capacity(len);
156 (0..x.len())
157 .map(|n| {
158 window.clear();
159 window.extend_from_slice(&x[n.saturating_sub(half)..(n + half + 1).min(x.len())]);
160 window.sort_by(f64::total_cmp);
161 let mid = window.len() / 2;
162 if window.len() % 2 == 1 {
163 window[mid]
164 } else {
165 f64::midpoint(window[mid - 1], window[mid])
166 }
167 })
168 .collect()
169}
170
171#[cfg(test)]
172mod tests {
173 use super::*;
174
175 #[test]
176 fn the_convention_states_the_constants() {
177 use super::super::{num, says};
178 says(
179 LINE_CONVENTION,
180 &[
181 format!(
182 "high-pass at {} Hz and low-pass at {} Hz",
183 num(LINE_LO.get()),
184 num(LINE_HI.get())
185 ),
186 format!("round({} ms fs)", num(1000.0 * LINE_MEDIAN.get())),
187 format!("uses a high-pass at {} Hz", num(LINE_LO_FAST.get())),
188 ],
189 );
190 }
191
192 #[test]
193 fn the_median_length_is_odd_and_near_1_4_ms() {
194 for (rate, len) in [(22_050, 31), (44_100, 63), (48_000, 67), (8_000, 11)] {
195 assert_eq!(median_len(SampleRate::new(rate).unwrap()), len, "{rate} Hz");
196 }
197 }
198
199 #[test]
200 fn the_median_removes_a_spike_and_keeps_the_ends() {
201 let x = [1.0, 1.0, 9.0, 1.0, 1.0, 2.0, 3.0];
202 assert_eq!(median_filter(&x, 3), [1.0, 1.0, 1.0, 1.0, 1.0, 2.0, 2.5]);
203 assert_eq!(median_filter(&[], 31), Vec::<f64>::new());
204 assert_eq!(median_filter(&[4.0], 31), [4.0]);
205 }
206
207 #[test]
208 fn a_steady_low_sine_reads_its_frequency() {
209 let rate = SampleRate::new(22_050).unwrap();
210 let x: Vec<f64> = (0..22_050)
211 .map(|n| (std::f64::consts::TAU * 41.2 * cast::usize_to_f64(n) / 22_050.0).sin())
212 .collect();
213 let line = SubBandLine::of(&x, rate, Seconds::new(1.0).unwrap()).unwrap();
214 assert_eq!(line.hz().len(), x.len() - 1);
215 let middle = &line.hz()[5_000..17_000];
218 let mean = middle.iter().sum::<f64>() / cast::usize_to_f64(middle.len());
219 assert!((mean - 41.2).abs() < 0.01, "{mean}");
220 assert!(middle.iter().all(|f| (f - 41.2).abs() < 0.5));
221 assert!((line.time_of(0).get() - (1.0 + 0.5 / 22_050.0)).abs() < 1e-12);
222 assert_eq!(line.index_at(Seconds::new(1.5).unwrap()), 11_025);
223 assert!((line.envelope()[11_025] - 1.0).abs() < 0.01);
224 }
225}