Browse and listen to thousands of radio stations across the globe right from your terminal ๐ ๐ป ๐ตโจ
radio
rust
tokio
web-radio
command-line-tool
tui
1use std::collections::VecDeque;
2
3use crossterm::event::{Event, KeyCode};
4use ratatui::{
5 style::Style,
6 text::Span,
7 widgets::{Axis, GraphType},
8};
9
10use crate::{app::update_value_i, input::Matrix};
11
12use super::{DataSet, Dimension, DisplayMode, GraphConfig};
13
14use rustfft::{num_complex::Complex, FftPlanner};
15
16#[derive(Default)]
17pub struct Spectroscope {
18 pub sampling_rate: u32,
19 pub buffer_size: u32,
20 pub average: u32,
21 pub buf: Vec<VecDeque<Vec<f64>>>,
22 pub window: bool,
23 pub log_y: bool,
24}
25
26fn magnitude(c: Complex<f64>) -> f64 {
27 let squared = (c.re * c.re) + (c.im * c.im);
28 squared.sqrt()
29}
30
31// got this from https://github.com/phip1611/spectrum-analyzer/blob/3c079ec2785b031d304bb381ff5f5fe04e6bcf71/src/windows.rs#L40
32pub fn hann_window(samples: &[f64]) -> Vec<f64> {
33 let mut windowed_samples = Vec::with_capacity(samples.len());
34 let samples_len = samples.len() as f64;
35 for (i, sample) in samples.iter().enumerate() {
36 let two_pi_i = 2.0 * std::f64::consts::PI * i as f64;
37 let idontknowthename = (two_pi_i / samples_len).cos();
38 let multiplier = 0.5 * (1.0 - idontknowthename);
39 windowed_samples.push(sample * multiplier)
40 }
41 windowed_samples
42}
43
44impl DisplayMode for Spectroscope {
45 fn from_args(opts: &crate::cfg::SourceOptions) -> Self {
46 Spectroscope {
47 sampling_rate: opts.sample_rate,
48 buffer_size: opts.buffer,
49 average: 1,
50 buf: Vec::new(),
51 window: false,
52 log_y: true,
53 }
54 }
55
56 fn mode_str(&self) -> &'static str {
57 "spectro"
58 }
59
60 fn channel_name(&self, index: usize) -> String {
61 match index {
62 0 => "L".into(),
63 1 => "R".into(),
64 _ => format!("{}", index),
65 }
66 }
67
68 fn header(&self, _: &GraphConfig) -> String {
69 let window_marker = if self.window { "-|-" } else { "---" };
70 if self.average <= 1 {
71 format!(
72 "live {} {:.3}Hz bins",
73 window_marker,
74 self.sampling_rate as f64 / self.buffer_size as f64
75 )
76 } else {
77 format!(
78 "{}x avg ({:.1}s) {} {:.3}Hz bins",
79 self.average,
80 (self.average * self.buffer_size) as f64 / self.sampling_rate as f64,
81 window_marker,
82 self.sampling_rate as f64 / (self.buffer_size * self.average) as f64,
83 )
84 }
85 }
86
87 fn axis(&self, cfg: &GraphConfig, dimension: Dimension) -> Axis<'_> {
88 let (name, bounds) = match dimension {
89 Dimension::X => (
90 "frequency -",
91 [
92 20.0f64.ln(),
93 ((cfg.samples as f64 / cfg.width as f64) * 20000.0).ln(),
94 ],
95 ),
96 Dimension::Y => (
97 if self.log_y { "| level" } else { "| amplitude" },
98 [if self.log_y { 0. } else { 0.0 }, cfg.scale * 7.5], // very arbitrary but good default
99 ),
100 // TODO super arbitraty! wtf! also ugly inline ifs, get this thing together!
101 };
102 let mut a = Axis::default();
103 if cfg.show_ui {
104 // TODO don't make it necessary to check show_ui inside here
105 a = a.title(Span::styled(name, Style::default().fg(cfg.labels_color)));
106 }
107 a.style(Style::default().fg(cfg.axis_color)).bounds(bounds)
108 }
109
110 fn process(&mut self, cfg: &GraphConfig, data: &Matrix<f64>) -> Vec<DataSet> {
111 if self.average == 0 {
112 self.average = 1
113 } // otherwise fft breaks
114 if !cfg.pause {
115 for (i, chan) in data.iter().enumerate() {
116 if self.buf.len() <= i {
117 self.buf.push(VecDeque::new());
118 }
119 self.buf[i].push_back(chan.clone());
120 while self.buf[i].len() > self.average as usize {
121 self.buf[i].pop_front();
122 }
123 }
124 }
125
126 let mut out = Vec::new();
127 let mut planner: FftPlanner<f64> = FftPlanner::new();
128 let sample_len = self.buffer_size * self.average;
129 let resolution = self.sampling_rate as f64 / sample_len as f64;
130 let fft = planner.plan_fft_forward(sample_len as usize);
131
132 for (n, chan_queue) in self.buf.iter().enumerate().rev() {
133 let mut chunk = chan_queue.iter().flatten().copied().collect::<Vec<f64>>();
134 if self.window {
135 chunk = hann_window(chunk.as_slice());
136 }
137 let mut max_val = *chunk
138 .iter()
139 .max_by(|a, b| a.total_cmp(b))
140 .expect("empty dataset?");
141 if max_val < 1. {
142 max_val = 1.;
143 }
144 let mut tmp: Vec<Complex<f64>> = chunk
145 .iter()
146 .map(|x| Complex {
147 re: *x / max_val,
148 im: 0.0,
149 })
150 .collect();
151 fft.process(tmp.as_mut_slice());
152 out.push(DataSet::new(
153 Some(self.channel_name(n)),
154 tmp[..=tmp.len() / 2]
155 .iter()
156 .enumerate()
157 .map(|(i, x)| {
158 (
159 (i as f64 * resolution).ln(),
160 if self.log_y {
161 magnitude(*x).ln()
162 } else {
163 magnitude(*x)
164 },
165 )
166 })
167 .collect(),
168 cfg.marker_type,
169 if cfg.scatter {
170 GraphType::Scatter
171 } else {
172 GraphType::Line
173 },
174 cfg.palette(n),
175 ));
176 }
177
178 out
179 }
180
181 fn handle(&mut self, event: Event) {
182 if let Event::Key(key) = event {
183 match key.code {
184 KeyCode::PageUp => update_value_i(&mut self.average, true, 1, 1., 1..65535),
185 KeyCode::PageDown => update_value_i(&mut self.average, false, 1, 1., 1..65535),
186 KeyCode::Char('w') => self.window = !self.window,
187 KeyCode::Char('l') => self.log_y = !self.log_y,
188 _ => {}
189 }
190 }
191 }
192
193 fn references(&self, cfg: &GraphConfig) -> Vec<DataSet> {
194 let lower = 0.; // if self.log_y { -(cfg.scale * 5.) } else { 0. };
195 let upper = cfg.scale * 7.5;
196 vec![
197 DataSet::new(
198 None,
199 vec![(0.0, 0.0), ((cfg.samples as f64).ln(), 0.0)],
200 cfg.marker_type,
201 GraphType::Line,
202 cfg.axis_color,
203 ),
204 // TODO can we auto generate these? lol...
205 DataSet::new(
206 None,
207 vec![(20.0f64.ln(), lower), (20.0f64.ln(), upper)],
208 cfg.marker_type,
209 GraphType::Line,
210 cfg.axis_color,
211 ),
212 DataSet::new(
213 None,
214 vec![(30.0f64.ln(), lower), (30.0f64.ln(), upper)],
215 cfg.marker_type,
216 GraphType::Line,
217 cfg.axis_color,
218 ),
219 DataSet::new(
220 None,
221 vec![(40.0f64.ln(), lower), (40.0f64.ln(), upper)],
222 cfg.marker_type,
223 GraphType::Line,
224 cfg.axis_color,
225 ),
226 DataSet::new(
227 None,
228 vec![(50.0f64.ln(), lower), (50.0f64.ln(), upper)],
229 cfg.marker_type,
230 GraphType::Line,
231 cfg.axis_color,
232 ),
233 DataSet::new(
234 None,
235 vec![(60.0f64.ln(), lower), (60.0f64.ln(), upper)],
236 cfg.marker_type,
237 GraphType::Line,
238 cfg.axis_color,
239 ),
240 DataSet::new(
241 None,
242 vec![(70.0f64.ln(), lower), (70.0f64.ln(), upper)],
243 cfg.marker_type,
244 GraphType::Line,
245 cfg.axis_color,
246 ),
247 DataSet::new(
248 None,
249 vec![(80.0f64.ln(), lower), (80.0f64.ln(), upper)],
250 cfg.marker_type,
251 GraphType::Line,
252 cfg.axis_color,
253 ),
254 DataSet::new(
255 None,
256 vec![(90.0f64.ln(), lower), (90.0f64.ln(), upper)],
257 cfg.marker_type,
258 GraphType::Line,
259 cfg.axis_color,
260 ),
261 DataSet::new(
262 None,
263 vec![(100.0f64.ln(), lower), (100.0f64.ln(), upper)],
264 cfg.marker_type,
265 GraphType::Line,
266 cfg.axis_color,
267 ),
268 DataSet::new(
269 None,
270 vec![(200.0f64.ln(), lower), (200.0f64.ln(), upper)],
271 cfg.marker_type,
272 GraphType::Line,
273 cfg.axis_color,
274 ),
275 DataSet::new(
276 None,
277 vec![(300.0f64.ln(), lower), (300.0f64.ln(), upper)],
278 cfg.marker_type,
279 GraphType::Line,
280 cfg.axis_color,
281 ),
282 DataSet::new(
283 None,
284 vec![(400.0f64.ln(), lower), (400.0f64.ln(), upper)],
285 cfg.marker_type,
286 GraphType::Line,
287 cfg.axis_color,
288 ),
289 DataSet::new(
290 None,
291 vec![(500.0f64.ln(), lower), (500.0f64.ln(), upper)],
292 cfg.marker_type,
293 GraphType::Line,
294 cfg.axis_color,
295 ),
296 DataSet::new(
297 None,
298 vec![(600.0f64.ln(), lower), (600.0f64.ln(), upper)],
299 cfg.marker_type,
300 GraphType::Line,
301 cfg.axis_color,
302 ),
303 DataSet::new(
304 None,
305 vec![(700.0f64.ln(), lower), (700.0f64.ln(), upper)],
306 cfg.marker_type,
307 GraphType::Line,
308 cfg.axis_color,
309 ),
310 DataSet::new(
311 None,
312 vec![(800.0f64.ln(), lower), (800.0f64.ln(), upper)],
313 cfg.marker_type,
314 GraphType::Line,
315 cfg.axis_color,
316 ),
317 DataSet::new(
318 None,
319 vec![(900.0f64.ln(), lower), (900.0f64.ln(), upper)],
320 cfg.marker_type,
321 GraphType::Line,
322 cfg.axis_color,
323 ),
324 DataSet::new(
325 None,
326 vec![(1000.0f64.ln(), lower), (1000.0f64.ln(), upper)],
327 cfg.marker_type,
328 GraphType::Line,
329 cfg.axis_color,
330 ),
331 DataSet::new(
332 None,
333 vec![(2000.0f64.ln(), lower), (2000.0f64.ln(), upper)],
334 cfg.marker_type,
335 GraphType::Line,
336 cfg.axis_color,
337 ),
338 DataSet::new(
339 None,
340 vec![(3000.0f64.ln(), lower), (3000.0f64.ln(), upper)],
341 cfg.marker_type,
342 GraphType::Line,
343 cfg.axis_color,
344 ),
345 DataSet::new(
346 None,
347 vec![(4000.0f64.ln(), lower), (4000.0f64.ln(), upper)],
348 cfg.marker_type,
349 GraphType::Line,
350 cfg.axis_color,
351 ),
352 DataSet::new(
353 None,
354 vec![(5000.0f64.ln(), lower), (5000.0f64.ln(), upper)],
355 cfg.marker_type,
356 GraphType::Line,
357 cfg.axis_color,
358 ),
359 DataSet::new(
360 None,
361 vec![(6000.0f64.ln(), lower), (6000.0f64.ln(), upper)],
362 cfg.marker_type,
363 GraphType::Line,
364 cfg.axis_color,
365 ),
366 DataSet::new(
367 None,
368 vec![(7000.0f64.ln(), lower), (7000.0f64.ln(), upper)],
369 cfg.marker_type,
370 GraphType::Line,
371 cfg.axis_color,
372 ),
373 DataSet::new(
374 None,
375 vec![(8000.0f64.ln(), lower), (8000.0f64.ln(), upper)],
376 cfg.marker_type,
377 GraphType::Line,
378 cfg.axis_color,
379 ),
380 DataSet::new(
381 None,
382 vec![(9000.0f64.ln(), lower), (9000.0f64.ln(), upper)],
383 cfg.marker_type,
384 GraphType::Line,
385 cfg.axis_color,
386 ),
387 DataSet::new(
388 None,
389 vec![(10000.0f64.ln(), lower), (10000.0f64.ln(), upper)],
390 cfg.marker_type,
391 GraphType::Line,
392 cfg.axis_color,
393 ),
394 DataSet::new(
395 None,
396 vec![(20000.0f64.ln(), lower), (20000.0f64.ln(), upper)],
397 cfg.marker_type,
398 GraphType::Line,
399 cfg.axis_color,
400 ),
401 ]
402 }
403}