diff --git a/src/app.rs b/src/app.rs index 3c6e746..4fe4138 100644 --- a/src/app.rs +++ b/src/app.rs @@ -1,14 +1,26 @@ - -use std::{io, time::{Duration, Instant}}; +use crossterm::event::{self, Event, KeyCode, KeyModifiers}; use ratatui::{ - style::Color, widgets::{Table, Row, Cell}, symbols::Marker, backend::Backend, + layout::{Constraint, Rect}, + style::Color, + style::{Modifier, Style}, + symbols::Marker, widgets::Chart, - Terminal, style::{Style, Modifier}, layout::{Rect, Constraint} + widgets::{Cell, Row, Table}, + Terminal, +}; +use std::{ + io, + time::{Duration, Instant}, }; -use crossterm::event::{self, Event, KeyCode, KeyModifiers}; -use crate::{display::{oscilloscope::Oscilloscope, spectroscope::Spectroscope, update_value_f, update_value_i, vectorscope::Vectorscope, Dimension, DisplayMode, GraphConfig}, input::{DataSource, Matrix}}; +use crate::{ + display::{ + oscilloscope::Oscilloscope, spectroscope::Spectroscope, update_value_f, update_value_i, + vectorscope::Vectorscope, Dimension, DisplayMode, GraphConfig, + }, + input::{DataSource, Matrix}, +}; pub enum CurrentDisplayMode { Oscilloscope, @@ -17,7 +29,8 @@ pub enum CurrentDisplayMode { } pub struct App { - #[allow(unused)] channels: u8, + #[allow(unused)] + channels: u8, graph: GraphConfig, oscilloscope: Oscilloscope, vectorscope: Vectorscope, @@ -51,35 +64,44 @@ impl App { let vectorscope = Vectorscope::default(); let spectroscope = Spectroscope::from(source); - App { - graph, oscilloscope, vectorscope, spectroscope, + App { + graph, + oscilloscope, + vectorscope, + spectroscope, mode: CurrentDisplayMode::Oscilloscope, channels: source.channels as u8, } } - pub fn run(&mut self, mut source: Box>, terminal: &mut Terminal) -> Result<(), io::Error> { + pub fn run( + &mut self, + mut source: Box>, + terminal: &mut Terminal, + ) -> Result<(), io::Error> { let mut fps = 0; let mut framerate = 0; let mut last_poll = Instant::now(); let mut channels = Matrix::default(); - + loop { - let data = source.recv() - .ok_or(io::Error::new(io::ErrorKind::BrokenPipe, "data source returned null"))?; - + let data = source.recv().ok_or(io::Error::new( + io::ErrorKind::BrokenPipe, + "data source returned null", + ))?; + if !self.graph.pause { channels = data; } - + fps += 1; - + if last_poll.elapsed().as_secs() >= 1 { framerate = fps; fps = 0; last_poll = Instant::now(); } - + { let mut datasets = Vec::new(); let graph = self.graph.clone(); // TODO cheap fix... @@ -91,8 +113,19 @@ impl App { let mut size = f.area(); if self.graph.show_ui { f.render_widget( - make_header(&self.graph, &self.current_display().header(&self.graph), self.current_display().mode_str(), framerate, self.graph.pause), - Rect { x: size.x, y: size.y, width: size.width, height:1 } // a 1px line at the top + make_header( + &self.graph, + &self.current_display().header(&self.graph), + self.current_display().mode_str(), + framerate, + self.graph.pause, + ), + Rect { + x: size.x, + y: size.y, + width: size.width, + height: 1, + }, // a 1px line at the top ); size.height -= 1; size.y += 1; @@ -104,10 +137,13 @@ impl App { })?; } - while event::poll(Duration::from_millis(0))? { // process all enqueued events + while event::poll(Duration::from_millis(0))? { + // process all enqueued events let event = event::read()?; - if self.process_events(event.clone())? { return Ok(()); } + if self.process_events(event.clone())? { + return Ok(()); + } self.current_display_mut().handle(event); } } @@ -133,9 +169,10 @@ impl App { let mut quit = false; if let Event::Key(key) = event { if let KeyModifiers::CONTROL = key.modifiers { - match key.code { // mimic other programs shortcuts to quit, for user friendlyness + match key.code { + // mimic other programs shortcuts to quit, for user friendlyness KeyCode::Char('c') | KeyCode::Char('q') | KeyCode::Char('w') => quit = true, - _ => {}, + _ => {} } } let magnitude = match key.modifiers { @@ -145,51 +182,76 @@ impl App { _ => 1.0, }; match key.code { - KeyCode::Up => update_value_f(&mut self.graph.scale, 0.01, magnitude, 0.0..10.0), // inverted to act as zoom - KeyCode::Down => update_value_f(&mut self.graph.scale, -0.01, magnitude, 0.0..10.0), // inverted to act as zoom - KeyCode::Right => update_value_i(&mut self.graph.samples, true, 25, magnitude, 0..self.graph.width*2), - KeyCode::Left => update_value_i(&mut self.graph.samples, false, 25, magnitude, 0..self.graph.width*2), + KeyCode::Up => update_value_f(&mut self.graph.scale, 0.01, magnitude, 0.0..10.0), // inverted to act as zoom + KeyCode::Down => update_value_f(&mut self.graph.scale, -0.01, magnitude, 0.0..10.0), // inverted to act as zoom + KeyCode::Right => update_value_i( + &mut self.graph.samples, + true, + 25, + magnitude, + 0..self.graph.width * 2, + ), + KeyCode::Left => update_value_i( + &mut self.graph.samples, + false, + 25, + magnitude, + 0..self.graph.width * 2, + ), KeyCode::Char('q') => quit = true, - KeyCode::Char(' ') => self.graph.pause = !self.graph.pause, - KeyCode::Char('s') => self.graph.scatter = !self.graph.scatter, - KeyCode::Char('h') => self.graph.show_ui = !self.graph.show_ui, - KeyCode::Char('r') => self.graph.references = !self.graph.references, - KeyCode::Tab => { // switch modes + KeyCode::Char(' ') => self.graph.pause = !self.graph.pause, + KeyCode::Char('s') => self.graph.scatter = !self.graph.scatter, + KeyCode::Char('h') => self.graph.show_ui = !self.graph.show_ui, + KeyCode::Char('r') => self.graph.references = !self.graph.references, + KeyCode::Tab => { + // switch modes match self.mode { - CurrentDisplayMode::Oscilloscope => self.mode = CurrentDisplayMode::Vectorscope, - CurrentDisplayMode::Vectorscope => self.mode = CurrentDisplayMode::Spectroscope, - CurrentDisplayMode::Spectroscope => self.mode = CurrentDisplayMode::Oscilloscope, + CurrentDisplayMode::Oscilloscope => { + self.mode = CurrentDisplayMode::Vectorscope + } + CurrentDisplayMode::Vectorscope => { + self.mode = CurrentDisplayMode::Spectroscope + } + CurrentDisplayMode::Spectroscope => { + self.mode = CurrentDisplayMode::Oscilloscope + } } - }, + } KeyCode::Esc => { self.graph.samples = self.graph.width; self.graph.scale = 1.; - }, - _ => {}, + } + _ => {} } }; - + Ok(quit) } } // TODO can these be removed or merged somewhere else? -fn make_header<'a>(cfg: &GraphConfig, module_header: &'a str, kind_o_scope: &'static str, fps: usize, pause: bool) -> Table<'a> { +fn make_header<'a>( + cfg: &GraphConfig, + module_header: &'a str, + kind_o_scope: &'static str, + fps: usize, + pause: bool, +) -> Table<'a> { Table::new( - vec![ - Row::new( - vec![ - Cell::from(format!("{}::scope-tui", kind_o_scope)).style(Style::default().fg(*cfg.palette.first().expect("empty palette?")).add_modifier(Modifier::BOLD)), - Cell::from(module_header), - Cell::from(format!("-{:.2}x+", cfg.scale)), - Cell::from(format!("{}/{} spf", cfg.samples, cfg.width)), - Cell::from(format!("{}fps", fps)), - Cell::from(if cfg.scatter { "***" } else { "---" }), - Cell::from(if pause { "||" } else { "|>" }), - ] - ) - ], + vec![Row::new(vec![ + Cell::from(format!("{}::scope-tui", kind_o_scope)).style( + Style::default() + .fg(*cfg.palette.first().expect("empty palette?")) + .add_modifier(Modifier::BOLD), + ), + Cell::from(module_header), + Cell::from(format!("-{:.2}x+", cfg.scale)), + Cell::from(format!("{}/{} spf", cfg.samples, cfg.width)), + Cell::from(format!("{}fps", fps)), + Cell::from(if cfg.scatter { "***" } else { "---" }), + Cell::from(if pause { "||" } else { "|>" }), + ])], vec![ Constraint::Percentage(35), Constraint::Percentage(25), @@ -197,8 +259,8 @@ fn make_header<'a>(cfg: &GraphConfig, module_header: &'a str, kind_o_scope: &'st Constraint::Percentage(13), Constraint::Percentage(6), Constraint::Percentage(6), - Constraint::Percentage(6) - ] + Constraint::Percentage(6), + ], ) .style(Style::default().fg(cfg.labels_color)) } diff --git a/src/cfg.rs b/src/cfg.rs index d2afea2..ff2c2ea 100644 --- a/src/cfg.rs +++ b/src/cfg.rs @@ -2,7 +2,7 @@ use clap::{Parser, Subcommand}; use crate::music::Note; -const HELP_TEMPLATE : &str = "{before-help}\ +const HELP_TEMPLATE: &str = "{before-help}\ {name} {version} -- by {author} {about} @@ -17,7 +17,7 @@ const HELP_TEMPLATE : &str = "{before-help}\ pub struct ScopeArgs { #[clap(subcommand)] pub source: ScopeSource, - + #[command(flatten)] pub opts: SourceOptions, @@ -50,7 +50,6 @@ pub struct UiOptions { #[derive(Debug, Clone, Subcommand)] pub enum ScopeSource { - #[cfg(feature = "pulseaudio")] /// use PulseAudio Simple api to read data from an audio sink Pulse { @@ -86,7 +85,7 @@ pub enum ScopeSource { /// just list available devices and quit #[arg(long, default_value_t = false)] list: bool, - } + }, } #[derive(Debug, Clone, Parser)] @@ -111,10 +110,12 @@ pub struct SourceOptions { // TODO its convenient to keep this here but it's not really the best place... impl SourceOptions { pub fn tune(&mut self) { - if let Some(txt) = &self.tune { // TODO make it less jank + if let Some(txt) = &self.tune { + // TODO make it less jank if let Ok(note) = txt.parse::() { self.buffer = note.tune_buffer_size(self.sample_rate); - while self.buffer % (self.channels as u32 * 2) != 0 { // TODO customizable bit depth + while self.buffer % (self.channels as u32 * 2) != 0 { + // TODO customizable bit depth self.buffer += 1; // TODO jank but otherwise it doesn't align } } else { diff --git a/src/display/mod.rs b/src/display/mod.rs index abff4be..54dd16f 100644 --- a/src/display/mod.rs +++ b/src/display/mod.rs @@ -1,14 +1,19 @@ pub mod oscilloscope; -pub mod vectorscope; pub mod spectroscope; +pub mod vectorscope; use crossterm::event::Event; -use ratatui::{widgets::{Dataset, Axis, GraphType}, style::{Style, Color}, symbols::Marker}; +use ratatui::{ + style::{Color, Style}, + symbols::Marker, + widgets::{Axis, Dataset, GraphType}, +}; use crate::input::Matrix; pub enum Dimension { - X, Y + X, + Y, } #[derive(Debug, Clone, Default)] @@ -29,7 +34,10 @@ pub struct GraphConfig { impl GraphConfig { pub fn palette(&self, index: usize) -> Color { - *self.palette.get(index % self.palette.len()).unwrap_or(&Color::White) + *self + .palette + .get(index % self.palette.len()) + .unwrap_or(&Color::White) } } @@ -41,9 +49,15 @@ pub trait DisplayMode { fn mode_str(&self) -> &'static str; // SHOULD override - fn channel_name(&self, index: usize) -> String { format!("{}", index) } - fn header(&self, _cfg: &GraphConfig) -> String { "".into() } - fn references(&self, _cfg: &GraphConfig) -> Vec { vec![] } + fn channel_name(&self, index: usize) -> String { + format!("{}", index) + } + fn header(&self, _cfg: &GraphConfig) -> String { + "".into() + } + fn references(&self, _cfg: &GraphConfig) -> Vec { + vec![] + } fn handle(&mut self, _event: Event) {} } @@ -55,18 +69,17 @@ pub struct DataSet { color: Color, } -impl<'a> From::<&'a DataSet> for Dataset<'a> { +impl<'a> From<&'a DataSet> for Dataset<'a> { fn from(ds: &'a DataSet) -> Dataset<'a> { let mut out = Dataset::default(); // TODO creating a binding is kinda ugly, is it avoidable? if let Some(name) = &ds.name { out = out.name(name.clone()); } - out - .marker(ds.marker_type) + out.marker(ds.marker_type) .graph_type(ds.graph_type) .style(Style::default().fg(ds.color)) .data(&ds.data) - } + } } // TODO this is pretty ugly but I need datasets which own the data @@ -76,14 +89,24 @@ impl DataSet { data: Vec<(f64, f64)>, marker_type: Marker, graph_type: GraphType, - color: Color + color: Color, ) -> Self { - DataSet { name, data, marker_type, graph_type, color } + DataSet { + name, + data, + marker_type, + graph_type, + color, + } } } - -pub(crate) fn update_value_f(val: &mut f64, base: f64, magnitude: f64, range: std::ops::Range) { +pub(crate) fn update_value_f( + val: &mut f64, + base: f64, + magnitude: f64, + range: std::ops::Range, +) { let delta = base * magnitude; if *val + delta > range.end { *val = range.end @@ -94,7 +117,13 @@ pub(crate) fn update_value_f(val: &mut f64, base: f64, magnitude: f64, range: st } } -pub(crate) fn update_value_i(val: &mut u32, inc: bool, base: u32, magnitude: f64, range: std::ops::Range) { +pub(crate) fn update_value_i( + val: &mut u32, + inc: bool, + base: u32, + magnitude: f64, + range: std::ops::Range, +) { let delta = (base as f64 * magnitude) as u32; if inc { if range.end - delta < *val { diff --git a/src/display/oscilloscope.rs b/src/display/oscilloscope.rs index 232c594..347e3e4 100644 --- a/src/display/oscilloscope.rs +++ b/src/display/oscilloscope.rs @@ -1,5 +1,9 @@ -use crossterm::event::{Event, KeyModifiers, KeyCode}; -use ratatui::{widgets::{Axis, GraphType}, style::Style, text::Span}; +use crossterm::event::{Event, KeyCode, KeyModifiers}; +use ratatui::{ + style::Style, + text::Span, + widgets::{Axis, GraphType}, +}; use crate::input::Matrix; @@ -44,7 +48,11 @@ impl DisplayMode for Oscilloscope { "{} {:.0}{} trigger", if self.falling_edge { "v" } else { "^" }, self.threshold, - if self.depth > 1 { format!(":{}", self.depth) } else { "".into() }, + if self.depth > 1 { + format!(":{}", self.depth) + } else { + "".into() + }, ) } else { "live".into() @@ -57,26 +65,34 @@ impl DisplayMode for Oscilloscope { Dimension::Y => ("| amplitude", [-cfg.scale, cfg.scale]), }; let mut a = Axis::default(); - if cfg.show_ui { // TODO don't make it necessary to check show_ui inside here + if cfg.show_ui { + // TODO don't make it necessary to check show_ui inside here a = a.title(Span::styled(name, Style::default().fg(cfg.labels_color))); } a.style(Style::default().fg(cfg.axis_color)).bounds(bounds) } fn references(&self, cfg: &GraphConfig) -> Vec { - vec![ - DataSet::new(None, vec![(0.0, 0.0), (cfg.samples as f64, 0.0)], cfg.marker_type, GraphType::Line, cfg.axis_color), - ] + vec![DataSet::new( + None, + vec![(0.0, 0.0), (cfg.samples as f64, 0.0)], + cfg.marker_type, + GraphType::Line, + cfg.axis_color, + )] } fn process(&mut self, cfg: &GraphConfig, data: &Matrix) -> Vec { let mut out = Vec::new(); let mut trigger_offset = 0; - if self.depth == 0 { self.depth = 1 } + if self.depth == 0 { + self.depth = 1 + } if self.triggering { for i in 0..data[0].len() { - if triggered(&data[0], i, self.threshold, self.depth, self.falling_edge) { // triggered + if triggered(&data[0], i, self.threshold, self.depth, self.falling_edge) { + // triggered break; } trigger_offset += 1; @@ -84,15 +100,25 @@ impl DisplayMode for Oscilloscope { } if self.triggering { - out.push(DataSet::new(Some("T".into()), vec![(0.0, self.threshold)], cfg.marker_type, GraphType::Scatter, cfg.labels_color)); + out.push(DataSet::new( + Some("T".into()), + vec![(0.0, self.threshold)], + cfg.marker_type, + GraphType::Scatter, + cfg.labels_color, + )); } for (n, channel) in data.iter().enumerate().rev() { let (mut min, mut max) = (0.0, 0.0); let mut tmp = Vec::new(); for (i, sample) in channel.iter().enumerate() { - if *sample < min { min = *sample }; - if *sample > max { max = *sample }; + if *sample < min { + min = *sample + }; + if *sample > max { + max = *sample + }; if i >= trigger_offset { tmp.push(((i - trigger_offset) as f64, *sample)); } @@ -104,7 +130,7 @@ impl DisplayMode for Oscilloscope { vec![(0.0, min), (0.0, max)], cfg.marker_type, GraphType::Scatter, - cfg.palette(n) + cfg.palette(n), )) } @@ -112,7 +138,11 @@ impl DisplayMode for Oscilloscope { Some(self.channel_name(n)), tmp, cfg.marker_type, - if cfg.scatter { GraphType::Scatter } else { GraphType::Line }, + if cfg.scatter { + GraphType::Scatter + } else { + GraphType::Line + }, cfg.palette(n), )); } @@ -129,18 +159,22 @@ impl DisplayMode for Oscilloscope { _ => 1.0, }; match key.code { - KeyCode::PageUp => update_value_f(&mut self.threshold, 250.0, magnitude, 0.0..32768.0), - KeyCode::PageDown => update_value_f(&mut self.threshold, -250.0, magnitude, 0.0..32768.0), - KeyCode::Char('t') => self.triggering = !self.triggering, + KeyCode::PageUp => { + update_value_f(&mut self.threshold, 250.0, magnitude, 0.0..32768.0) + } + KeyCode::PageDown => { + update_value_f(&mut self.threshold, -250.0, magnitude, 0.0..32768.0) + } + KeyCode::Char('t') => self.triggering = !self.triggering, KeyCode::Char('e') => self.falling_edge = !self.falling_edge, - KeyCode::Char('p') => self.peaks = !self.peaks, + KeyCode::Char('p') => self.peaks = !self.peaks, KeyCode::Char('=') => update_value_i(&mut self.depth, true, 1, 1.0, 1..65535), KeyCode::Char('-') => update_value_i(&mut self.depth, false, 1, 1.0, 1..65535), KeyCode::Char('+') => update_value_i(&mut self.depth, true, 10, 1.0, 1..65535), KeyCode::Char('_') => update_value_i(&mut self.depth, false, 10, 1.0, 1..65535), KeyCode::Esc => { self.triggering = false; - }, + } _ => {} } } @@ -148,12 +182,14 @@ impl DisplayMode for Oscilloscope { } #[allow(clippy::collapsible_else_if)] // TODO can this be made nicer? -fn triggered(data: &[f64], index: usize, threshold: f64, depth: u32, falling_edge:bool) -> bool { - if data.len() < index + (1+depth as usize) { return false; } +fn triggered(data: &[f64], index: usize, threshold: f64, depth: u32, falling_edge: bool) -> bool { + if data.len() < index + (1 + depth as usize) { + return false; + } if falling_edge { if data[index] >= threshold { for i in 1..=depth as usize { - if data[index+i] >= threshold { + if data[index + i] >= threshold { return false; } } @@ -164,7 +200,7 @@ fn triggered(data: &[f64], index: usize, threshold: f64, depth: u32, falling_edg } else { if data[index] <= threshold { for i in 1..=depth as usize { - if data[index+i] <= threshold { + if data[index + i] <= threshold { return false; } } diff --git a/src/display/spectroscope.rs b/src/display/spectroscope.rs index 0630301..c3e05fc 100644 --- a/src/display/spectroscope.rs +++ b/src/display/spectroscope.rs @@ -1,13 +1,17 @@ use std::collections::VecDeque; use crossterm::event::{Event, KeyCode}; -use ratatui::{widgets::{Axis, GraphType}, style::Style, text::Span}; +use ratatui::{ + style::Style, + text::Span, + widgets::{Axis, GraphType}, +}; use crate::input::Matrix; use super::{update_value_i, DataSet, Dimension, DisplayMode, GraphConfig}; -use rustfft::{FftPlanner, num_complex::Complex}; +use rustfft::{num_complex::Complex, FftPlanner}; pub struct Spectroscope { pub sampling_rate: u32, @@ -42,7 +46,7 @@ impl From<&crate::cfg::SourceOptions> for Spectroscope { Spectroscope { sampling_rate: value.sample_rate, buffer_size: value.buffer, - average: 5, + average: 1, buf: Vec::new(), window: false, log_y: true, @@ -66,7 +70,11 @@ impl DisplayMode for Spectroscope { fn header(&self, _: &GraphConfig) -> String { let window_marker = if self.window { "-|-" } else { "---" }; if self.average <= 1 { - format!("live {} {:.3}Hz bins", window_marker, self.sampling_rate as f64 / self.buffer_size as f64) + format!( + "live {} {:.3}Hz bins", + window_marker, + self.sampling_rate as f64 / self.buffer_size as f64 + ) } else { format!( "{}x avg ({:.1}s) {} {:.3}Hz bins", @@ -80,22 +88,31 @@ impl DisplayMode for Spectroscope { fn axis(&self, cfg: &GraphConfig, dimension: Dimension) -> Axis { let (name, bounds) = match dimension { - Dimension::X => ("frequency -", [20.0f64.ln(), ((cfg.samples as f64 / cfg.width as f64) * 20000.0).ln()]), + Dimension::X => ( + "frequency -", + [ + 20.0f64.ln(), + ((cfg.samples as f64 / cfg.width as f64) * 20000.0).ln(), + ], + ), Dimension::Y => ( if self.log_y { "| level" } else { "| amplitude" }, - [0.0, cfg.scale * 7.5] // very arbitrary but good default + [0.0, cfg.scale * 7.5], // very arbitrary but good default ), // TODO super arbitraty! wtf! also ugly inline ifs, get this thing together! }; let mut a = Axis::default(); - if cfg.show_ui { // TODO don't make it necessary to check show_ui inside here + if cfg.show_ui { + // TODO don't make it necessary to check show_ui inside here a = a.title(Span::styled(name, Style::default().fg(cfg.labels_color))); } a.style(Style::default().fg(cfg.axis_color)).bounds(bounds) } fn process(&mut self, cfg: &GraphConfig, data: &Matrix) -> Vec { - if self.average == 0 { self.average = 1 } // otherwise fft breaks + if self.average == 0 { + self.average = 1 + } // otherwise fft breaks if !cfg.pause { for (i, chan) in data.iter().enumerate() { if self.buf.len() <= i { @@ -119,19 +136,43 @@ impl DisplayMode for Spectroscope { if self.window { chunk = hann_window(chunk.as_slice()); } - let mut max_val = *chunk.iter().max_by(|a, b| a.total_cmp(b)).expect("empty dataset?"); - if max_val < 1. { max_val = 1.; } - let mut tmp : Vec> = chunk.iter().map(|x| Complex { re: *x / max_val, im: 0.0 }).collect(); + let mut max_val = *chunk + .iter() + .max_by(|a, b| a.total_cmp(b)) + .expect("empty dataset?"); + if max_val < 1. { + max_val = 1.; + } + let mut tmp: Vec> = chunk + .iter() + .map(|x| Complex { + re: *x / max_val, + im: 0.0, + }) + .collect(); fft.process(tmp.as_mut_slice()); out.push(DataSet::new( Some(self.channel_name(n)), tmp[..=tmp.len() / 2] .iter() .enumerate() - .map(|(i,x)| ((i as f64 * resolution).ln(), if self.log_y { magnitude(*x).ln() } else { magnitude(*x) })) + .map(|(i, x)| { + ( + (i as f64 * resolution).ln(), + if self.log_y { + magnitude(*x).ln() + } else { + magnitude(*x) + }, + ) + }) .collect(), cfg.marker_type, - if cfg.scatter { GraphType::Scatter } else { GraphType::Line }, + if cfg.scatter { + GraphType::Scatter + } else { + GraphType::Line + }, cfg.palette(n), )); } @@ -142,7 +183,7 @@ impl DisplayMode for Spectroscope { fn handle(&mut self, event: Event) { if let Event::Key(key) = event { match key.code { - KeyCode::PageUp => update_value_i(&mut self.average, true, 1, 1., 1..65535), + KeyCode::PageUp => update_value_i(&mut self.average, true, 1, 1., 1..65535), KeyCode::PageDown => update_value_i(&mut self.average, false, 1, 1., 1..65535), KeyCode::Char('w') => self.window = !self.window, KeyCode::Char('l') => self.log_y = !self.log_y, @@ -151,9 +192,11 @@ impl DisplayMode for Spectroscope { } } + #[rustfmt::skip] fn references(&self, cfg: &GraphConfig) -> Vec { let lower = 0.; // if self.log_y { -(cfg.scale * 5.) } else { 0. }; let upper = cfg.scale * 7.5; + vec![ DataSet::new(None, vec![(0.0, 0.0), ((cfg.samples as f64).ln(), 0.0)], cfg.marker_type, GraphType::Line, cfg.axis_color), diff --git a/src/display/vectorscope.rs b/src/display/vectorscope.rs index 2b703a2..076e9ae 100644 --- a/src/display/vectorscope.rs +++ b/src/display/vectorscope.rs @@ -1,8 +1,12 @@ -use ratatui::{widgets::{Axis, GraphType}, style::Style, text::Span}; +use ratatui::{ + style::Style, + text::Span, + widgets::{Axis, GraphType}, +}; use crate::input::Matrix; -use super::{DisplayMode, GraphConfig, DataSet, Dimension}; +use super::{DataSet, Dimension, DisplayMode, GraphConfig}; #[derive(Default)] pub struct Vectorscope {} @@ -26,7 +30,8 @@ impl DisplayMode for Vectorscope { Dimension::Y => ("| right", [-cfg.scale, cfg.scale]), }; let mut a = Axis::default(); - if cfg.show_ui { // TODO don't make it necessary to check show_ui inside here + if cfg.show_ui { + // TODO don't make it necessary to check show_ui inside here a = a.title(Span::styled(name, Style::default().fg(cfg.labels_color))); } a.style(Style::default().fg(cfg.axis_color)).bounds(bounds) @@ -34,8 +39,20 @@ impl DisplayMode for Vectorscope { fn references(&self, cfg: &GraphConfig) -> Vec { vec![ - DataSet::new(None, vec![(-cfg.scale, 0.0), (cfg.scale, 0.0)], cfg.marker_type, GraphType::Line, cfg.axis_color), - DataSet::new(None, vec![(0.0, -cfg.scale), (0.0, cfg.scale)], cfg.marker_type, GraphType::Line, cfg.axis_color), + DataSet::new( + None, + vec![(-cfg.scale, 0.0), (cfg.scale, 0.0)], + cfg.marker_type, + GraphType::Line, + cfg.axis_color, + ), + DataSet::new( + None, + vec![(0.0, -cfg.scale), (0.0, cfg.scale)], + cfg.marker_type, + GraphType::Line, + cfg.axis_color, + ), ] } @@ -47,16 +64,20 @@ impl DisplayMode for Vectorscope { match chunk.len() { 2 => { for i in 0..std::cmp::min(chunk[0].len(), chunk[1].len()) { - if i > cfg.samples as usize { break } + if i > cfg.samples as usize { + break; + } tmp.push((chunk[0][i], chunk[1][i])); } - }, + } 1 => { for i in 0..chunk[0].len() { - if i > cfg.samples as usize { break } + if i > cfg.samples as usize { + break; + } tmp.push((chunk[0][i], i as f64)); } - }, + } _ => continue, } // split it in two for easier coloring @@ -66,14 +87,22 @@ impl DisplayMode for Vectorscope { Some(self.channel_name((n * 2) + 1)), tmp[pivot..].to_vec(), cfg.marker_type, - if cfg.scatter { GraphType::Scatter } else { GraphType::Line }, + if cfg.scatter { + GraphType::Scatter + } else { + GraphType::Line + }, cfg.palette((n * 2) + 1), )); out.push(DataSet::new( Some(self.channel_name(n * 2)), tmp[..pivot].to_vec(), cfg.marker_type, - if cfg.scatter { GraphType::Scatter } else { GraphType::Line }, + if cfg.scatter { + GraphType::Scatter + } else { + GraphType::Line + }, cfg.palette(n * 2), )); } diff --git a/src/input/cpal.rs b/src/input/cpal.rs index 7ae4e09..168c1d3 100644 --- a/src/input/cpal.rs +++ b/src/input/cpal.rs @@ -1,5 +1,5 @@ -use std::sync::mpsc; use cpal::traits::{DeviceTrait, HostTrait, StreamTrait}; +use std::sync::mpsc; use super::{stream_to_matrix, Matrix}; @@ -28,7 +28,11 @@ pub enum AudioDeviceErrors { } impl DefaultAudioDeviceWithCPAL { - pub fn instantiate(device: Option<&str>, opts: &crate::cfg::SourceOptions, timeout_secs: u64) -> Result>, AudioDeviceErrors> { + pub fn instantiate( + device: Option<&str>, + opts: &crate::cfg::SourceOptions, + timeout_secs: u64, + ) -> Result>, AudioDeviceErrors> { let host = cpal::default_host(); let device = match device { Some(name) => host @@ -56,11 +60,13 @@ impl DefaultAudioDeviceWithCPAL { let (tx, rx) = mpsc::channel(); let stream = device.build_input_stream( &cfg, - move |data:&[f32], _info| { - tx.send( - stream_to_matrix(data.iter().cloned(), actual_channels as usize, 1.) - ) - .unwrap_or(()) + move |data: &[f32], _info| { + tx.send(stream_to_matrix( + data.iter().cloned(), + actual_channels as usize, + 1., + )) + .unwrap_or(()) }, |e| eprintln!("error in input stream: {e}"), Some(std::time::Duration::from_secs(timeout_secs)), @@ -78,7 +84,7 @@ impl super::DataSource for DefaultAudioDeviceWithCPAL { Err(e) => { println!("error receiving from source? {e}"); None - }, + } } } } diff --git a/src/input/file.rs b/src/input/file.rs index 1a979d3..6598808 100644 --- a/src/input/file.rs +++ b/src/input/file.rs @@ -1,6 +1,9 @@ use std::{fs::File, io::Read}; -use super::{format::{SampleParser, Signed16PCM}, stream_to_matrix, Matrix}; +use super::{ + format::{SampleParser, Signed16PCM}, + stream_to_matrix, Matrix, +}; pub struct FileSource { file: File, @@ -15,29 +18,29 @@ pub struct FileSource { impl FileSource { #[allow(clippy::new_ret_no_self)] - pub fn new(path: &str, opts: &crate::cfg::SourceOptions, limit_rate: bool) -> Result>, std::io::Error> { - Ok(Box::new( - FileSource { - channels: opts.channels, - _sample_rate: opts.sample_rate as usize, - _limit_rate: limit_rate, - file: File::open(path)?, - buffer: vec![0u8; opts.buffer as usize * opts.channels], - } - )) + pub fn new( + path: &str, + opts: &crate::cfg::SourceOptions, + limit_rate: bool, + ) -> Result>, std::io::Error> { + Ok(Box::new(FileSource { + channels: opts.channels, + _sample_rate: opts.sample_rate as usize, + _limit_rate: limit_rate, + file: File::open(path)?, + buffer: vec![0u8; opts.buffer as usize * opts.channels], + })) } } impl super::DataSource for FileSource { fn recv(&mut self) -> Option> { match self.file.read_exact(&mut self.buffer) { - Ok(()) => Some( - stream_to_matrix( - self.buffer.chunks(2).map(Signed16PCM::parse), - self.channels, - 32768.0, - ) - ), + Ok(()) => Some(stream_to_matrix( + self.buffer.chunks(2).map(Signed16PCM::parse), + self.channels, + 32768.0, + )), Err(_e) => None, // TODO log it } } diff --git a/src/input/format/mod.rs b/src/input/format/mod.rs index 73d20ce..8eb8ba2 100644 --- a/src/input/format/mod.rs +++ b/src/input/format/mod.rs @@ -1,4 +1,3 @@ - pub trait SampleParser { fn parse(data: &[u8]) -> T; } diff --git a/src/input/mod.rs b/src/input/mod.rs index 776f8f9..2d1f8cd 100644 --- a/src/input/mod.rs +++ b/src/input/mod.rs @@ -20,8 +20,14 @@ pub trait DataSource { /// becomes /// L L L L L /// R R R R R -pub fn stream_to_matrix(stream: impl Iterator, channels: usize, norm: O) -> Matrix -where I : Copy + Into, O : Copy + std::ops::Div +pub fn stream_to_matrix( + stream: impl Iterator, + channels: usize, + norm: O, +) -> Matrix +where + I: Copy + Into, + O: Copy + std::ops::Div, { let mut out = vec![vec![]; channels]; let mut channel = 0; diff --git a/src/input/pulse.rs b/src/input/pulse.rs index f7a7ca8..1cff15e 100644 --- a/src/input/pulse.rs +++ b/src/input/pulse.rs @@ -1,7 +1,15 @@ -use libpulse_binding::{sample::{Spec, Format}, def::BufferAttr, error::PAErr, stream::Direction}; +use libpulse_binding::{ + def::BufferAttr, + error::PAErr, + sample::{Format, Spec}, + stream::Direction, +}; use libpulse_simple_binding::Simple; -use super::{format::{SampleParser, Signed16PCM}, stream_to_matrix}; +use super::{ + format::{SampleParser, Signed16PCM}, + stream_to_matrix, +}; pub struct PulseAudioSimpleDataSource { simple: Simple, @@ -11,7 +19,11 @@ pub struct PulseAudioSimpleDataSource { impl PulseAudioSimpleDataSource { #[allow(clippy::new_ret_no_self)] - pub fn new(device: Option<&str>, opts: &crate::cfg::SourceOptions, server_buffer: u32) -> Result>, PAErr> { + pub fn new( + device: Option<&str>, + opts: &crate::cfg::SourceOptions, + server_buffer: u32, + ) -> Result>, PAErr> { let spec = Spec { format: Format::S16NE, // TODO allow more formats? channels: opts.channels as u8, @@ -26,19 +38,19 @@ impl PulseAudioSimpleDataSource { ..Default::default() }; let simple = Simple::new( - None, // Use the default server - "scope-tui", // Our application’s name - Direction::Record, // We want a record stream - device, // Use requested device, or default - "data", // Description of our stream - &spec, // Our sample format - None, // Use default channel map - Some(&attrs), // Our hints on how to handle client/server buffers + None, // Use the default server + "scope-tui", // Our application’s name + Direction::Record, // We want a record stream + device, // Use requested device, or default + "data", // Description of our stream + &spec, // Our sample format + None, // Use default channel map + Some(&attrs), // Our hints on how to handle client/server buffers )?; Ok(Box::new(Self { simple, buffer: vec![0; opts.buffer as usize * opts.channels * 2], - channels: opts.channels + channels: opts.channels, })) } } diff --git a/src/lib.rs b/src/lib.rs index 8908bdf..0dcd248 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,6 +1,6 @@ -pub mod music; -pub mod input; pub mod display; +pub mod input; +pub mod music; #[cfg(feature = "app")] pub mod cfg; diff --git a/src/main.rs b/src/main.rs index ab513dc..76c82eb 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,11 +1,11 @@ -use scope::app::App; -use scope::cfg::{ScopeArgs, ScopeSource}; use clap::Parser; +use crossterm::{ + execute, + terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen}, +}; use ratatui::{backend::CrosstermBackend, Terminal}; -use crossterm::{execute, terminal::{ - disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen -}}; - +use scope::app::App; +use scope::cfg::{ScopeArgs, ScopeSource}; fn main() -> Result<(), Box> { let mut args = ScopeArgs::parse(); @@ -13,24 +13,30 @@ fn main() -> Result<(), Box> { let source = match args.source { #[cfg(feature = "pulseaudio")] - ScopeSource::Pulse { device, server_buffer } => { - scope::input::pulse::PulseAudioSimpleDataSource::new(device.as_deref(), &args.opts, server_buffer)? - }, + ScopeSource::Pulse { + device, + server_buffer, + } => scope::input::pulse::PulseAudioSimpleDataSource::new( + device.as_deref(), + &args.opts, + server_buffer, + )?, #[cfg(feature = "file")] ScopeSource::File { path, limit_rate } => { scope::input::file::FileSource::new(&path, &args.opts, limit_rate)? - }, + } #[cfg(feature = "cpal")] - ScopeSource::Audio { device, timeout, list } => { + ScopeSource::Audio { + device, + timeout, + list, + } => { if list { use cpal::traits::{DeviceTrait, HostTrait}; let host = cpal::default_host(); - for dev in host - .input_devices() - .unwrap() - { + for dev in host.input_devices().unwrap() { println!("> {}", dev.name().unwrap()); for config in dev.supported_input_configs().unwrap() { let bufsize = match config.buffer_size() { @@ -39,13 +45,22 @@ fn main() -> Result<(), Box> { }; println!( " + {}ch {}-{}hz {}-{}buf ({})", - config.channels(), config.min_sample_rate().0, config.max_sample_rate().0, bufsize.0, bufsize.1, config.sample_format() + config.channels(), + config.min_sample_rate().0, + config.max_sample_rate().0, + bufsize.0, + bufsize.1, + config.sample_format() ); } } return Ok(()); } - scope::input::cpal::DefaultAudioDeviceWithCPAL::instantiate(device.as_deref(), &args.opts, timeout)? + scope::input::cpal::DefaultAudioDeviceWithCPAL::instantiate( + device.as_deref(), + &args.opts, + timeout, + )? } }; @@ -63,10 +78,7 @@ fn main() -> Result<(), Box> { // restore terminal disable_raw_mode()?; - execute!( - terminal.backend_mut(), - LeaveAlternateScreen, - )?; + execute!(terminal.backend_mut(), LeaveAlternateScreen,)?; terminal.show_cursor()?; if let Err(e) = res { diff --git a/src/music.rs b/src/music.rs index e46938a..74cde7d 100644 --- a/src/music.rs +++ b/src/music.rs @@ -1,8 +1,9 @@ -use std::{str::FromStr, num::ParseIntError}; +use std::{num::ParseIntError, str::FromStr}; +#[rustfmt::skip] #[derive(Debug, PartialEq, Clone)] pub enum Tone { - C, Db, D, Eb, E, F, Gb, G, Ab, A, Bb, B + C, Db, D, Eb, E, F, Gb, G, Ab, A, Bb, B, } #[derive(Debug, thiserror::Error, derive_more::Display)] @@ -33,12 +34,10 @@ impl FromStr for Note { break; } } - Ok( - Note { - tone: trimmed[..split].parse::()?, - octave: trimmed[split..].parse::().unwrap_or(0), - } - ) + Ok(Note { + tone: trimmed[..split].parse::()?, + octave: trimmed[split..].parse::().unwrap_or(0), + }) } } @@ -47,19 +46,19 @@ impl FromStr for Tone { fn from_str(txt: &str) -> Result { match txt { - "C" => Ok(Tone::C ), + "C" => Ok(Tone::C), "C#" | "Db" => Ok(Tone::Db), - "D" => Ok(Tone::D ), + "D" => Ok(Tone::D), "D#" | "Eb" => Ok(Tone::Eb), - "E" => Ok(Tone::E ), - "F" => Ok(Tone::F ), + "E" => Ok(Tone::E), + "F" => Ok(Tone::F), "F#" | "Gb" => Ok(Tone::Gb), - "G" => Ok(Tone::G ), + "G" => Ok(Tone::G), "G#" | "Ab" => Ok(Tone::Ab), - "A" => Ok(Tone::A ), + "A" => Ok(Tone::A), "A#" | "Bb" => Ok(Tone::Bb), - "B" => Ok(Tone::B ), - _ => Err(ToneError()) + "B" => Ok(Tone::B), + _ => Err(ToneError()), } } } @@ -76,18 +75,18 @@ impl Tone { pub fn freq(&self, octave: u32) -> f32 { match octave { 0 => match self { - Tone::C => 16.35, + Tone::C => 16.35, Tone::Db => 17.32, - Tone::D => 18.35, + Tone::D => 18.35, Tone::Eb => 19.45, - Tone::E => 20.60, - Tone::F => 21.83, + Tone::E => 20.60, + Tone::F => 21.83, Tone::Gb => 23.12, - Tone::G => 24.50, + Tone::G => 24.50, Tone::Ab => 25.96, - Tone::A => 27.50, + Tone::A => 27.50, Tone::Bb => 29.14, - Tone::B => 30.87, + Tone::B => 30.87, }, _ => { let mut freq = self.freq(0); @@ -99,4 +98,3 @@ impl Tone { } } } -