diff --git a/src/app.rs b/src/app.rs index 3d2558a..7b9fe2d 100644 --- a/src/app.rs +++ b/src/app.rs @@ -124,3 +124,222 @@ impl From::<&crate::Args> for App { app } } + +pub fn run_app(args: Args, terminal: &mut Terminal) -> Result<(), io::Error> { + // prepare globals + let mut buffer : Vec = vec![0; args.buffer as usize]; + let mut app = App::from(&args); + let fmt = Signed16PCM{}; // TODO some way to choose this? + + let mut pause = false; + + // setup audio capture + let spec = Spec { + format: Format::S16NE, + channels: 2, + rate: args.sample_rate, + }; + assert!(spec.is_valid()); + + let dev = match &args.device { + Some(d) => Some(d.as_str()), + None => None, + }; + + let s = match Simple::new( + None, // Use the default server + "ScopeTUI", // Our application’s name + Direction::Record, // We want a record stream + dev, // Use requested device, or default + "Music", // Description of our stream + &spec, // Our sample format + None, // Use default channel map + Some(&BufferAttr { + maxlength: args.server_buffer * args.buffer, + fragsize: args.buffer, + ..Default::default() + }), + ) { + Ok(s) => s, + Err(e) => { + println!("[!] Could not connect to pulseaudio : {:?}", e); + return Err(io::Error::new(ErrorKind::Other, "could not connect to pulseaudio")); + }, + }; + + let mut fps = 0; + let mut framerate = 0; + let mut last_poll = Instant::now(); + let mut channels = vec![]; + + loop { + match s.read(&mut buffer) { + Ok(()) => {}, + Err(e) => { + println!("[!] Could not read data from pulseaudio : {:?}", e); + return Err(io::Error::new(ErrorKind::Other, "could not read from pulseaudio")); + }, + } + + if !pause { + channels = fmt.oscilloscope(&mut buffer, 2); + } + + if app.cfg.triggering { + // TODO allow to customize channel to use for triggering and threshold + if let Some(ch) = channels.get(0) { + let mut discard = 0; + for i in 0..ch.len() { // seek to first sample rising through threshold + if i + 1 < ch.len() && ch[i] <= 0.0 && ch[i+1] > 0.0 { // triggered + break; + } else { + discard += 1; + } + } + for ch in channels.iter_mut() { + *ch = ch[discard..].to_vec(); + } + } + } + + let mut measures; + + if app.cfg.vectorscope { + measures = vec![]; + for chunk in channels.chunks(2) { + let mut tmp = vec![]; + for i in 0..chunk[0].len() { + tmp.push((chunk[0][i] as f64, chunk[1][i] as f64)); + } + // split it in two so the math downwards still works the same + let pivot = tmp.len() / 2; + measures.push(tmp[pivot..].to_vec()); // put more recent first + measures.push(tmp[..pivot].to_vec()); + } + } else { + measures = vec![vec![]; channels.len()]; + for i in 0..channels[0].len() { + for j in 0..channels.len() { + measures[j].push((i as f64, channels[j][i])); + } + } + } + + let mut datasets = vec![]; + + if app.cfg.references { + datasets.push(data_set("", &app.references.x, app.cfg.marker_type, GraphType::Line, app.cfg.axis_color)); + datasets.push(data_set("", &app.references.y, app.cfg.marker_type, GraphType::Line, app.cfg.axis_color)); + } + + let ds_names = if app.cfg.vectorscope { vec!["1", "2"] } else { vec!["R", "L"] }; + let palette : Vec = app.cfg.palette.iter().rev().map(|x| x.clone()).collect(); + + for (i, ds) in measures.iter().rev().enumerate() { + datasets.push(data_set(ds_names[i], ds, app.cfg.marker_type, app.cfg.graph_type, palette[i % palette.len()])); + } + + fps += 1; + + if last_poll.elapsed().as_secs() >= 1 { + framerate = fps; + fps = 0; + last_poll = Instant::now(); + } + + terminal.draw(|f| { + let size = f.size(); + let chart = Chart::new(datasets) + .block(block(&app, args.sample_rate as f32, framerate)) + .x_axis(axis(&app, Dimension::X)) // TODO allow to have axis sometimes? + .y_axis(axis(&app, Dimension::Y)); + f.render_widget(chart, size) + })?; + + if let Some(Event::Key(key)) = poll_event()? { + match key.modifiers { + KeyModifiers::CONTROL => { + match key.code { + KeyCode::Char('c') | KeyCode::Char('q') | KeyCode::Char('w') => break, + _ => {}, + } + }, + _ => { + match key.code { + KeyCode::Char('q') => break, + KeyCode::Char(' ') => pause = !pause, + KeyCode::Char('=') => app.update_scale(-1000), + KeyCode::Char('-') => app.update_scale(1000), + KeyCode::Char('+') => app.update_scale(-100), + KeyCode::Char('_') => app.update_scale(100), + KeyCode::Char('v') => app.cfg.vectorscope = !app.cfg.vectorscope, + KeyCode::Char('s') => app.set_scatter(!app.scatter()), // TODO no funcs + KeyCode::Char('h') => app.cfg.references = !app.cfg.references, + KeyCode::Char('t') => app.cfg.triggering = !app.cfg.triggering, + KeyCode::Up => {}, + KeyCode::Down => {}, + _ => {}, + } + } + } + app.update_values(); + } + } + + Ok(()) +} + + +// TODO these functions probably shouldn't be here + +fn poll_event() -> Result, std::io::Error> { + if event::poll(Duration::from_millis(0))? { + Ok(Some(event::read()?)) + } else { + Ok(None) + } +} + +fn data_set<'a>( + name: &'a str, + data: &'a [(f64, f64)], + marker_type: symbols::Marker, + graph_type: GraphType, + axis_color: Color +) -> Dataset<'a> { + Dataset::default() + .name(name) + .marker(marker_type) + .graph_type(graph_type) + .style(Style::default().fg(axis_color)) + .data(&data) +} + +fn axis(app: &App, dim: Dimension) -> Axis { + let mut a = Axis::default(); + if app.cfg.references { + a = a.title(Span::styled(app.name(&dim), Style::default().fg(Color::Cyan))); + } + a.style(Style::default().fg(app.cfg.axis_color)) + .bounds(app.bounds(&dim)) +} + +fn block(app: &App, sample_rate: f32, framerate: u32) -> Block { + let mut b = Block::default(); + + if app.cfg.references { + b = b.title( + Span::styled( + format!( + "TUI {} -- {}{} mode -- range {} -- {} samples -- {:.1} kHz -- {} fps", + if app.cfg.vectorscope { "Vectorscope" } else { "Oscilloscope" }, + if app.cfg.triggering { "triggered " } else { "" }, + if app.scatter() { "scatter" } else { "line" }, + app.cfg.scale, app.cfg.width, sample_rate / 1000.0, framerate, + ), + Style::default().add_modifier(Modifier::BOLD).fg(Color::Yellow)) + ).title_alignment(Alignment::Center); + } + + b +} diff --git a/src/main.rs b/src/main.rs index 0fc7916..1e5d7c6 100644 --- a/src/main.rs +++ b/src/main.rs @@ -3,35 +3,24 @@ mod app; mod config; mod music; -use std::{io::{self, ErrorKind}, time::{Duration, Instant}}; use tui::{ - backend::{CrosstermBackend, Backend}, - widgets::{Block, Chart, Axis, Dataset, GraphType}, - // layout::{Layout, Constraint, Direction}, - Terminal, text::Span, style::{Style, Color, Modifier}, symbols, layout::Alignment + backend::CrosstermBackend, + Terminal, }; use crossterm::{ - event::{self, DisableMouseCapture, Event, KeyCode, KeyModifiers}, - execute, + event::DisableMouseCapture, execute, terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen}, }; -use libpulse_simple_binding::Simple; -use libpulse_binding::{stream::Direction, def::BufferAttr}; -use libpulse_binding::sample::{Spec, Format}; - use clap::Parser; -use parser::{SampleParser, Signed16PCM}; - -use crate::app::App; use crate::music::Note; -use crate::config::Dimension; +use crate::app::run_app; /// A simple oscilloscope/vectorscope for your terminal #[derive(Parser, Debug)] #[command(author, version, about, long_about = None)] -struct Args { +pub struct Args { /// Audio device to attach to device: Option, @@ -76,7 +65,7 @@ struct Args { no_braille: bool, } -fn main() -> Result<(), io::Error> { +fn main() -> Result<(), std::io::Error> { let mut args = Args::parse(); if let Some(txt) = &args.tune { // TODO make it less jank @@ -92,7 +81,7 @@ fn main() -> Result<(), io::Error> { // setup terminal enable_raw_mode()?; - let mut stdout = io::stdout(); + let mut stdout = std::io::stdout(); execute!(stdout, EnterAlternateScreen)?; let backend = CrosstermBackend::new(stdout); let mut terminal = Terminal::new(backend)?; @@ -116,222 +105,3 @@ fn main() -> Result<(), io::Error> { Ok(()) } - -fn run_app(args: Args, terminal: &mut Terminal) -> Result<(), io::Error> { - // prepare globals - let mut buffer : Vec = vec![0; args.buffer as usize]; - let mut app = App::from(&args); - let fmt = Signed16PCM{}; // TODO some way to choose this? - - let mut pause = false; - - // setup audio capture - let spec = Spec { - format: Format::S16NE, - channels: 2, - rate: args.sample_rate, - }; - assert!(spec.is_valid()); - - let dev = match &args.device { - Some(d) => Some(d.as_str()), - None => None, - }; - - let s = match Simple::new( - None, // Use the default server - "ScopeTUI", // Our application’s name - Direction::Record, // We want a record stream - dev, // Use requested device, or default - "Music", // Description of our stream - &spec, // Our sample format - None, // Use default channel map - Some(&BufferAttr { - maxlength: args.server_buffer * args.buffer, - fragsize: args.buffer, - ..Default::default() - }), - ) { - Ok(s) => s, - Err(e) => { - println!("[!] Could not connect to pulseaudio : {:?}", e); - return Err(io::Error::new(ErrorKind::Other, "could not connect to pulseaudio")); - }, - }; - - let mut fps = 0; - let mut framerate = 0; - let mut last_poll = Instant::now(); - let mut channels = vec![]; - - loop { - match s.read(&mut buffer) { - Ok(()) => {}, - Err(e) => { - println!("[!] Could not read data from pulseaudio : {:?}", e); - return Err(io::Error::new(ErrorKind::Other, "could not read from pulseaudio")); - }, - } - - if !pause { - channels = fmt.oscilloscope(&mut buffer, 2); - } - - if app.cfg.triggering { - // TODO allow to customize channel to use for triggering and threshold - if let Some(ch) = channels.get(0) { - let mut discard = 0; - for i in 0..ch.len() { // seek to first sample rising through threshold - if i + 1 < ch.len() && ch[i] <= 0.0 && ch[i+1] > 0.0 { // triggered - break; - } else { - discard += 1; - } - } - for ch in channels.iter_mut() { - *ch = ch[discard..].to_vec(); - } - } - } - - let mut measures; - - if app.cfg.vectorscope { - measures = vec![]; - for chunk in channels.chunks(2) { - let mut tmp = vec![]; - for i in 0..chunk[0].len() { - tmp.push((chunk[0][i] as f64, chunk[1][i] as f64)); - } - // split it in two so the math downwards still works the same - let pivot = tmp.len() / 2; - measures.push(tmp[pivot..].to_vec()); // put more recent first - measures.push(tmp[..pivot].to_vec()); - } - } else { - measures = vec![vec![]; channels.len()]; - for i in 0..channels[0].len() { - for j in 0..channels.len() { - measures[j].push((i as f64, channels[j][i])); - } - } - } - - let mut datasets = vec![]; - - if app.cfg.references { - datasets.push(data_set("", &app.references.x, app.cfg.marker_type, GraphType::Line, app.cfg.axis_color)); - datasets.push(data_set("", &app.references.y, app.cfg.marker_type, GraphType::Line, app.cfg.axis_color)); - } - - let ds_names = if app.cfg.vectorscope { vec!["1", "2"] } else { vec!["R", "L"] }; - let palette : Vec = app.cfg.palette.iter().rev().map(|x| x.clone()).collect(); - - for (i, ds) in measures.iter().rev().enumerate() { - datasets.push(data_set(ds_names[i], ds, app.cfg.marker_type, app.cfg.graph_type, palette[i % palette.len()])); - } - - fps += 1; - - if last_poll.elapsed().as_secs() >= 1 { - framerate = fps; - fps = 0; - last_poll = Instant::now(); - } - - terminal.draw(|f| { - let size = f.size(); - let chart = Chart::new(datasets) - .block(block(&app, args.sample_rate as f32, framerate)) - .x_axis(axis(&app, Dimension::X)) // TODO allow to have axis sometimes? - .y_axis(axis(&app, Dimension::Y)); - f.render_widget(chart, size) - })?; - - if let Some(Event::Key(key)) = poll_event()? { - match key.modifiers { - KeyModifiers::CONTROL => { - match key.code { - KeyCode::Char('c') | KeyCode::Char('q') | KeyCode::Char('w') => break, - _ => {}, - } - }, - _ => { - match key.code { - KeyCode::Char('q') => break, - KeyCode::Char(' ') => pause = !pause, - KeyCode::Char('=') => app.update_scale(-1000), - KeyCode::Char('-') => app.update_scale(1000), - KeyCode::Char('+') => app.update_scale(-100), - KeyCode::Char('_') => app.update_scale(100), - KeyCode::Char('v') => app.cfg.vectorscope = !app.cfg.vectorscope, - KeyCode::Char('s') => app.set_scatter(!app.scatter()), // TODO no funcs - KeyCode::Char('h') => app.cfg.references = !app.cfg.references, - KeyCode::Char('t') => app.cfg.triggering = !app.cfg.triggering, - KeyCode::Up => {}, - KeyCode::Down => {}, - _ => {}, - } - } - } - app.update_values(); - } - } - - Ok(()) -} - - -// TODO these functions probably shouldn't be here - -fn poll_event() -> Result, std::io::Error> { - if event::poll(Duration::from_millis(0))? { - Ok(Some(event::read()?)) - } else { - Ok(None) - } -} - -fn data_set<'a>( - name: &'a str, - data: &'a [(f64, f64)], - marker_type: symbols::Marker, - graph_type: GraphType, - axis_color: Color -) -> Dataset<'a> { - Dataset::default() - .name(name) - .marker(marker_type) - .graph_type(graph_type) - .style(Style::default().fg(axis_color)) - .data(&data) -} - -fn axis(app: &App, dim: Dimension) -> Axis { - let mut a = Axis::default(); - if app.cfg.references { - a = a.title(Span::styled(app.name(&dim), Style::default().fg(Color::Cyan))); - } - a.style(Style::default().fg(app.cfg.axis_color)) - .bounds(app.bounds(&dim)) -} - -fn block(app: &App, sample_rate: f32, framerate: u32) -> Block { - let mut b = Block::default(); - - if app.cfg.references { - b = b.title( - Span::styled( - format!( - "TUI {} -- {}{} mode -- range {} -- {} samples -- {:.1} kHz -- {} fps", - if app.cfg.vectorscope { "Vectorscope" } else { "Oscilloscope" }, - if app.cfg.triggering { "triggered " } else { "" }, - if app.scatter() { "scatter" } else { "line" }, - app.cfg.scale, app.cfg.width, sample_rate / 1000.0, framerate, - ), - Style::default().add_modifier(Modifier::BOLD).fg(Color::Yellow)) - ).title_alignment(Alignment::Center); - } - - b -}