Browse and listen to thousands of radio stations across the globe right from your terminal ๐ŸŒŽ ๐Ÿ“ป ๐ŸŽตโœจ
radio rust tokio web-radio command-line-tool tui
at main 13 kB view raw
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}