mirror of
https://github.com/alemidev/scope-tui.git
synced 2024-11-14 18:59:19 +01:00
feat: restructured project, initial broken fft
This commit is contained in:
parent
925659639a
commit
37af6d9fc4
9 changed files with 555 additions and 457 deletions
|
@ -1,6 +1,6 @@
|
||||||
[package]
|
[package]
|
||||||
name = "scope-tui"
|
name = "scope-tui"
|
||||||
version = "0.2.1"
|
version = "0.2.2"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
authors = [ "alemi <me@alemi.dev>" ]
|
authors = [ "alemi <me@alemi.dev>" ]
|
||||||
description = "A simple oscilloscope/vectorscope for the terminal, inspired by cava"
|
description = "A simple oscilloscope/vectorscope for the terminal, inspired by cava"
|
||||||
|
@ -19,3 +19,4 @@ libpulse-simple-binding = "2.25"
|
||||||
clap = { version = "4.0.32", features = ["derive"] }
|
clap = { version = "4.0.32", features = ["derive"] }
|
||||||
derive_more = "0.99.17"
|
derive_more = "0.99.17"
|
||||||
thiserror = "1.0.48"
|
thiserror = "1.0.48"
|
||||||
|
easyfft = "0.4.0"
|
||||||
|
|
568
src/app.rs
568
src/app.rs
|
@ -1,444 +1,232 @@
|
||||||
|
|
||||||
use std::{io::{self, ErrorKind}, time::{Duration, Instant}};
|
use std::{io, time::{Duration, Instant}, ops::Range};
|
||||||
use tui::{
|
use tui::{
|
||||||
style::Color, widgets::{GraphType, Table, Row, Cell}, symbols,
|
style::Color, widgets::{Table, Row, Cell}, symbols::Marker,
|
||||||
backend::Backend,
|
backend::Backend,
|
||||||
widgets::{Chart, Axis, Dataset},
|
widgets::Chart,
|
||||||
Terminal, text::Span, style::{Style, Modifier}, layout::{Rect, Constraint}
|
Terminal, style::{Style, Modifier}, layout::{Rect, Constraint}
|
||||||
};
|
};
|
||||||
use crossterm::event::{self, Event, KeyCode, KeyModifiers};
|
use crossterm::event::{self, Event, KeyCode, KeyModifiers};
|
||||||
|
|
||||||
use libpulse_simple_binding::Simple;
|
use crate::{source::DataSource, display::{GraphConfig, oscilloscope::Oscilloscope, DisplayMode, Dimension, vectorscope::Vectorscope, spectroscope::Spectroscope}};
|
||||||
use libpulse_binding::{stream::Direction, def::BufferAttr};
|
|
||||||
use libpulse_binding::sample::{Spec, Format};
|
|
||||||
|
|
||||||
use crate::{Args, source::{PulseAudioSimple, DataSource}};
|
|
||||||
use crate::config::{ChartNames, ChartBounds, ChartReferences, AppConfig, Dimension};
|
|
||||||
use crate::parser::{SampleParser, Signed16PCM};
|
use crate::parser::{SampleParser, Signed16PCM};
|
||||||
|
|
||||||
pub fn run_app<T : Backend>(args: Args, terminal: &mut Terminal<T>) -> Result<(), io::Error> {
|
pub enum CurrentDisplayMode {
|
||||||
// prepare globals
|
Oscilloscope,
|
||||||
let mut app = App::from(&args);
|
Vectorscope,
|
||||||
let fmt = Signed16PCM{}; // TODO some way to choose this?
|
Spectroscope,
|
||||||
let mut source = PulseAudioSimple::new(
|
|
||||||
args.device.as_deref(),
|
|
||||||
args.channels,
|
|
||||||
args.sample_rate,
|
|
||||||
args.buffer,
|
|
||||||
args.server_buffer
|
|
||||||
).unwrap();
|
|
||||||
|
|
||||||
let mut fps = 0;
|
|
||||||
let mut framerate = 0;
|
|
||||||
let mut last_poll = Instant::now();
|
|
||||||
let mut channels = vec![];
|
|
||||||
|
|
||||||
loop {
|
|
||||||
let data = source.recv().unwrap();
|
|
||||||
|
|
||||||
if !app.cfg.pause {
|
|
||||||
channels = fmt.oscilloscope(data, args.channels);
|
|
||||||
}
|
|
||||||
|
|
||||||
let mut trigger_offset = 0;
|
|
||||||
|
|
||||||
if app.cfg.triggering {
|
|
||||||
// TODO allow to customize channel to use for triggering
|
|
||||||
if let Some(ch) = channels.get(0) {
|
|
||||||
for i in 0..ch.len() { // seek to first sample rising through threshold
|
|
||||||
if triggered(ch, i, app.cfg.threshold, app.cfg.depth, app.cfg.falling_edge) { // triggered
|
|
||||||
break;
|
|
||||||
} else {
|
|
||||||
trigger_offset += 1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// for ch in channels.iter_mut() {
|
|
||||||
// let limit = if ch.len() < discard { ch.len() } else { discard };
|
|
||||||
// *ch = ch[limit..].to_vec();
|
|
||||||
// }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let mut measures : Vec<(String, Vec<(f64, f64)>)> = vec![];
|
|
||||||
let mut peaks : Vec<Vec<(f64, f64)>> = vec![];
|
|
||||||
|
|
||||||
// This third buffer is kinda weird because of lifetimes on Datasets, TODO
|
|
||||||
// would be nice to make it more straight forward instead of this deep tuple magic
|
|
||||||
if app.cfg.vectorscope {
|
|
||||||
for (i, chunk) in channels.chunks(2).enumerate() {
|
|
||||||
let mut tmp = vec![];
|
|
||||||
match chunk.len() {
|
|
||||||
2 => {
|
|
||||||
for i in 0..std::cmp::min(chunk[0].len(), chunk[0].len()) {
|
|
||||||
tmp.push((chunk[0][i] as f64, chunk[1][i] as f64));
|
|
||||||
}
|
|
||||||
},
|
|
||||||
1 => {
|
|
||||||
for i in 0..chunk[0].len() {
|
|
||||||
tmp.push((chunk[0][i] as f64, i as f64));
|
|
||||||
}
|
|
||||||
},
|
|
||||||
_ => continue,
|
|
||||||
}
|
|
||||||
// split it in two so the math downwards still works the same
|
|
||||||
let pivot = tmp.len() / 2;
|
|
||||||
measures.push((channel_name(i * 2, true), tmp[pivot..].to_vec())); // put more recent first
|
|
||||||
measures.push((channel_name((i * 2) + 1, true), tmp[..pivot].to_vec()));
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
for (i, channel) in channels.iter().enumerate() {
|
|
||||||
let mut tmp = vec![];
|
|
||||||
let mut peak_up = 0.0;
|
|
||||||
let mut peak_down = 0.0;
|
|
||||||
for i in 0..channel.len() {
|
|
||||||
if i >= trigger_offset {
|
|
||||||
tmp.push(((i - trigger_offset) as f64, channel[i]));
|
|
||||||
}
|
|
||||||
if channel[i] > peak_up {
|
|
||||||
peak_up = channel[i];
|
|
||||||
}
|
|
||||||
if channel[i] < peak_down {
|
|
||||||
peak_down = channel[i];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
measures.push((channel_name(i, false), tmp));
|
|
||||||
peaks.push(vec![(0.0, peak_down), (0.0, peak_up)]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let samples = measures.iter().map(|(_,x)| x.len()).max().unwrap_or(0);
|
|
||||||
|
|
||||||
let mut datasets = vec![];
|
|
||||||
|
|
||||||
if app.cfg.references {
|
|
||||||
datasets.push(data_set("", &app.references.x, app.marker_type(), GraphType::Line, app.cfg.axis_color));
|
|
||||||
datasets.push(data_set("", &app.references.y, app.marker_type(), GraphType::Line, app.cfg.axis_color));
|
|
||||||
}
|
|
||||||
|
|
||||||
let trigger_pt;
|
|
||||||
if app.cfg.triggering {
|
|
||||||
trigger_pt = [(0.0, app.cfg.threshold)];
|
|
||||||
datasets.push(data_set("T", &trigger_pt, app.marker_type(), GraphType::Scatter, Color::Cyan));
|
|
||||||
}
|
|
||||||
|
|
||||||
let m_len = measures.len() - 1;
|
|
||||||
|
|
||||||
if !app.cfg.vectorscope && app.cfg.peaks {
|
|
||||||
for (i, pt) in peaks.iter().rev().enumerate() {
|
|
||||||
datasets.push(data_set("", pt, app.marker_type(), GraphType::Scatter, app.palette(m_len - i)));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
for (i, (name, ds)) in measures.iter().rev().enumerate() {
|
|
||||||
datasets.push(data_set(&name, ds, app.marker_type(), app.graph_type(), app.palette(m_len - i)));
|
|
||||||
}
|
|
||||||
|
|
||||||
fps += 1;
|
|
||||||
|
|
||||||
if last_poll.elapsed().as_secs() >= 1 {
|
|
||||||
framerate = fps;
|
|
||||||
fps = 0;
|
|
||||||
last_poll = Instant::now();
|
|
||||||
}
|
|
||||||
|
|
||||||
terminal.draw(|f| {
|
|
||||||
let mut size = f.size();
|
|
||||||
if app.cfg.show_ui {
|
|
||||||
let heading = header(&app, samples as u32, framerate);
|
|
||||||
f.render_widget(heading, Rect { x: size.x, y: size.y, width: size.width, height:1 });
|
|
||||||
size.height -= 1;
|
|
||||||
size.y += 1;
|
|
||||||
}
|
|
||||||
let chart = Chart::new(datasets)
|
|
||||||
.x_axis(axis(&app, Dimension::X)) // TODO allow to have axis sometimes?
|
|
||||||
.y_axis(axis(&app, Dimension::Y));
|
|
||||||
f.render_widget(chart, size)
|
|
||||||
})?;
|
|
||||||
|
|
||||||
if process_events(&mut app, &args)? {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct App {
|
pub struct App {
|
||||||
pub cfg: AppConfig,
|
pause: bool,
|
||||||
pub references: ChartReferences,
|
channels: u8,
|
||||||
pub bounds: ChartBounds,
|
graph: GraphConfig,
|
||||||
pub names: ChartNames,
|
oscilloscope: Oscilloscope,
|
||||||
|
vectorscope: Vectorscope,
|
||||||
|
spectroscope: Spectroscope,
|
||||||
|
mode: CurrentDisplayMode,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl App {
|
impl App {
|
||||||
pub fn update_values(&mut self) {
|
pub fn run<T : Backend>(&mut self, mut source: impl DataSource, terminal: &mut Terminal<T>) -> Result<(), io::Error> {
|
||||||
if self.cfg.scale > 32770 { // sample max value is 32768 (32 bits), but we leave 2 pixels for
|
// prepare globals
|
||||||
self.cfg.scale = 32770; // padding (and to not "disaling" range when reaching limit)
|
let fmt = Signed16PCM{}; // TODO some way to choose this?
|
||||||
}
|
|
||||||
if self.cfg.scale < 0 {
|
let mut fps = 0;
|
||||||
self.cfg.scale = 0;
|
let mut framerate = 0;
|
||||||
}
|
let mut last_poll = Instant::now();
|
||||||
if self.cfg.depth < 1 {
|
let mut channels = vec![];
|
||||||
self.cfg.depth = 1;
|
|
||||||
}
|
loop {
|
||||||
if self.cfg.vectorscope {
|
let data = source.recv().unwrap();
|
||||||
self.names.x = "left -".into();
|
|
||||||
self.names.y = "| right".into();
|
if !self.pause {
|
||||||
self.bounds.x = [-(self.cfg.scale as f64), self.cfg.scale as f64];
|
channels = fmt.oscilloscope(data, self.channels);
|
||||||
self.bounds.y = [-(self.cfg.scale as f64), self.cfg.scale as f64];
|
}
|
||||||
self.references.x = vec![(-(self.cfg.scale as f64), 0.0), (self.cfg.scale as f64, 0.0)];
|
|
||||||
self.references.y = vec![(0.0, -(self.cfg.scale as f64)), (0.0, self.cfg.scale as f64)];
|
fps += 1;
|
||||||
} else {
|
|
||||||
self.names.x = "time -".into();
|
if last_poll.elapsed().as_secs() >= 1 {
|
||||||
self.names.y = "| amplitude".into();
|
framerate = fps;
|
||||||
self.bounds.x = [0.0, self.cfg.width as f64];
|
fps = 0;
|
||||||
self.bounds.y = [-(self.cfg.scale as f64), self.cfg.scale as f64];
|
last_poll = Instant::now();
|
||||||
self.references.x = vec![(0.0, 0.0), (self.cfg.width as f64, 0.0)];
|
}
|
||||||
let half_width = self.cfg.width as f64 / 2.0;
|
|
||||||
self.references.y = vec![(half_width, -(self.cfg.scale as f64)), (half_width, self.cfg.scale as f64)];
|
{
|
||||||
|
let display = match self.mode {
|
||||||
|
CurrentDisplayMode::Oscilloscope => &self.oscilloscope as &dyn DisplayMode,
|
||||||
|
CurrentDisplayMode::Vectorscope => &self.vectorscope as &dyn DisplayMode,
|
||||||
|
CurrentDisplayMode::Spectroscope => &self.spectroscope as &dyn DisplayMode,
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut datasets = Vec::new();
|
||||||
|
if self.graph.references {
|
||||||
|
datasets.append(&mut display.references(&self.graph));
|
||||||
|
}
|
||||||
|
datasets.append(&mut display.process(&self.graph, &channels));
|
||||||
|
terminal.draw(|f| {
|
||||||
|
let mut size = f.size();
|
||||||
|
if self.graph.show_ui {
|
||||||
|
f.render_widget(
|
||||||
|
make_header(&self.graph, &display.header(&self.graph), framerate, self.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;
|
||||||
|
}
|
||||||
|
let chart = Chart::new(datasets.iter().map(|x| x.into()).collect())
|
||||||
|
.x_axis(display.axis(&self.graph, Dimension::X)) // TODO allow to have axis sometimes?
|
||||||
|
.y_axis(display.axis(&self.graph, Dimension::Y));
|
||||||
|
f.render_widget(chart, size)
|
||||||
|
}).unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
while event::poll(Duration::from_millis(0))? { // process all enqueued events
|
||||||
|
let event = event::read()?;
|
||||||
|
|
||||||
|
if self.process_events(event.clone())? { return Ok(()); }
|
||||||
|
self.current_display().handle(event);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn marker_type(&self) -> symbols::Marker {
|
fn current_display(&mut self) -> &mut dyn DisplayMode {
|
||||||
if self.cfg.braille {
|
match self.mode {
|
||||||
symbols::Marker::Braille
|
CurrentDisplayMode::Oscilloscope => &mut self.oscilloscope as &mut dyn DisplayMode,
|
||||||
} else {
|
CurrentDisplayMode::Vectorscope => &mut self.vectorscope as &mut dyn DisplayMode,
|
||||||
symbols::Marker::Dot
|
CurrentDisplayMode::Spectroscope => &mut self.spectroscope as &mut dyn DisplayMode,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn graph_type(&self) -> GraphType {
|
fn process_events(&mut self, event: Event) -> Result<bool, io::Error> {
|
||||||
if self.cfg.scatter {
|
let mut quit = false;
|
||||||
GraphType::Scatter
|
if let Event::Key(key) = event {
|
||||||
} else {
|
if let KeyModifiers::CONTROL = key.modifiers {
|
||||||
GraphType::Line
|
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 {
|
||||||
|
KeyModifiers::SHIFT => 10.0,
|
||||||
|
KeyModifiers::CONTROL => 5.0,
|
||||||
|
KeyModifiers::ALT => 0.2,
|
||||||
|
_ => 1.0,
|
||||||
|
};
|
||||||
|
match key.code {
|
||||||
|
KeyCode::Up => update_value_i(&mut self.graph.scale, true, 250, magnitude, 0..32768), // inverted to act as zoom
|
||||||
|
KeyCode::Down => update_value_i(&mut self.graph.scale, false, 250, magnitude, 0..32768), // inverted to act as zoom
|
||||||
|
KeyCode::Right => update_value_i(&mut self.graph.samples, true, 25, magnitude, 0..self.graph.width),
|
||||||
|
KeyCode::Left => update_value_i(&mut self.graph.samples, false, 25, magnitude, 0..self.graph.width),
|
||||||
|
KeyCode::Char('q') => quit = true,
|
||||||
|
KeyCode::Char(' ') => self.pause = !self.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,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
_ => {},
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
pub fn palette(&self, index: usize) -> Color {
|
Ok(quit)
|
||||||
*self.cfg.palette.get(index % self.cfg.palette.len()).unwrap_or(&Color::White)
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn update_value_f(val: &mut f64, base: f64, magnitude: f64, range: Range<f64>) {
|
||||||
|
let delta = base * magnitude;
|
||||||
|
if *val + delta > range.end {
|
||||||
|
*val = range.end
|
||||||
|
} else if *val + delta < range.start {
|
||||||
|
*val = range.start
|
||||||
|
} else {
|
||||||
|
*val += delta;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn update_value_i(val: &mut u32, inc: bool, base: u32, magnitude: f64, range: Range<u32>) {
|
||||||
|
let delta = (base as f64 * magnitude) as u32;
|
||||||
|
if inc {
|
||||||
|
if range.end - delta < *val {
|
||||||
|
*val = range.end
|
||||||
|
} else {
|
||||||
|
*val += delta
|
||||||
|
}
|
||||||
|
} else if range.start + delta > *val {
|
||||||
|
*val = range.start
|
||||||
|
} else {
|
||||||
|
*val -= delta
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl From::<&crate::Args> for App {
|
impl From::<&crate::Args> for App {
|
||||||
fn from(args: &crate::Args) -> Self {
|
fn from(args: &crate::Args) -> Self {
|
||||||
let cfg = AppConfig {
|
let graph = GraphConfig {
|
||||||
axis_color: Color::DarkGray,
|
axis_color: Color::DarkGray,
|
||||||
|
labels_color: Color::Cyan,
|
||||||
palette: vec![Color::Red, Color::Yellow, Color::Green, Color::Magenta],
|
palette: vec![Color::Red, Color::Yellow, Color::Green, Color::Magenta],
|
||||||
scale: args.range,
|
scale: args.range,
|
||||||
width: args.buffer / (2 * args.channels as u32), // TODO also make bit depth customizable
|
width: args.buffer / (2 * args.channels as u32), // TODO also make bit depth customizable
|
||||||
|
samples: args.buffer / (2 * args.channels as u32),
|
||||||
|
references: !args.no_reference,
|
||||||
|
show_ui: !args.no_ui,
|
||||||
|
scatter: args.scatter,
|
||||||
|
marker_type: if args.no_braille {
|
||||||
|
Marker::Dot
|
||||||
|
} else {
|
||||||
|
Marker::Braille
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
let oscilloscope = Oscilloscope {
|
||||||
triggering: args.triggering,
|
triggering: args.triggering,
|
||||||
depth: args.check_depth,
|
depth: args.check_depth,
|
||||||
threshold: args.threshold,
|
threshold: args.threshold,
|
||||||
vectorscope: args.vectorscope,
|
|
||||||
references: !args.no_reference,
|
|
||||||
show_ui: !args.no_ui,
|
|
||||||
braille: !args.no_braille,
|
|
||||||
scatter: args.scatter,
|
|
||||||
falling_edge: args.falling_edge,
|
falling_edge: args.falling_edge,
|
||||||
peaks: args.show_peaks,
|
peaks: args.show_peaks,
|
||||||
|
};
|
||||||
|
|
||||||
|
let vectorscope = Vectorscope {};
|
||||||
|
let spectroscope = Spectroscope {};
|
||||||
|
|
||||||
|
App {
|
||||||
|
graph, oscilloscope, vectorscope, spectroscope,
|
||||||
|
mode: CurrentDisplayMode::Oscilloscope,
|
||||||
|
channels: args.channels,
|
||||||
pause: false,
|
pause: false,
|
||||||
};
|
}
|
||||||
|
|
||||||
let mut app = App {
|
|
||||||
cfg,
|
|
||||||
references: ChartReferences::default(),
|
|
||||||
bounds: ChartBounds::default(),
|
|
||||||
names: ChartNames::default(),
|
|
||||||
};
|
|
||||||
|
|
||||||
app.update_values();
|
|
||||||
|
|
||||||
app
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// TODO these functions probably shouldn't be here
|
|
||||||
|
|
||||||
fn header(app: &App, samples: u32, framerate: u32) -> Table<'static> {
|
fn make_header<'a>(cfg: &GraphConfig, module_header: &'a str, fps: usize, pause: bool) -> Table<'a> {
|
||||||
Table::new(
|
Table::new(
|
||||||
vec![
|
vec![
|
||||||
Row::new(
|
Row::new(
|
||||||
vec![
|
vec![
|
||||||
Cell::from(format!("TUI {}", if app.cfg.vectorscope { "Vectorscope" } else { "Oscilloscope" })).style(Style::default().fg(Color::Yellow).add_modifier(Modifier::BOLD)),
|
Cell::from("tui **scope").style(Style::default().fg(*cfg.palette.get(0).expect("empty palette?")).add_modifier(Modifier::BOLD)),
|
||||||
Cell::from(format!("{}",
|
Cell::from(module_header),
|
||||||
if app.cfg.triggering {
|
Cell::from(format!("-{}+", cfg.scale)),
|
||||||
format!(
|
Cell::from(format!("{}/{} spf", cfg.samples, cfg.width)),
|
||||||
"{} {:.0}{} trigger",
|
Cell::from(format!("{}fps", fps)),
|
||||||
if app.cfg.falling_edge { "v" } else { "^" },
|
Cell::from(if cfg.scatter { "***" } else { "---" }),
|
||||||
app.cfg.threshold,
|
Cell::from(if pause { "||" } else { "|>" }),
|
||||||
if app.cfg.depth > 1 { format!(":{}", app.cfg.depth) } else { "".into() },
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
"live".into()
|
|
||||||
}
|
|
||||||
)),
|
|
||||||
Cell::from(format!("-{}+ range", app.cfg.scale)),
|
|
||||||
Cell::from(format!("{}/{} sample", app.cfg.width, samples as u32)),
|
|
||||||
Cell::from(format!("{}fps", framerate)),
|
|
||||||
Cell::from(format!("{}{}", if app.cfg.peaks { "|" } else { " " }, if app.cfg.scatter { "***" } else { "---" })),
|
|
||||||
Cell::from(format!("{}", if app.cfg.pause { "||" } else { "|>" } )),
|
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
.style(Style::default().fg(Color::Cyan))
|
.style(Style::default().fg(cfg.labels_color))
|
||||||
.widths(&[
|
.widths(&[
|
||||||
Constraint::Percentage(35),
|
Constraint::Percentage(35),
|
||||||
Constraint::Percentage(15),
|
Constraint::Percentage(25),
|
||||||
Constraint::Percentage(15),
|
Constraint::Percentage(7),
|
||||||
Constraint::Percentage(15),
|
Constraint::Percentage(13),
|
||||||
Constraint::Percentage(6),
|
Constraint::Percentage(6),
|
||||||
Constraint::Percentage(6),
|
Constraint::Percentage(6),
|
||||||
Constraint::Percentage(6)
|
Constraint::Percentage(6)
|
||||||
])
|
])
|
||||||
}
|
}
|
||||||
|
|
||||||
fn process_events(app: &mut App, args: &Args) -> Result<bool, io::Error> {
|
|
||||||
let mut quit = false;
|
|
||||||
|
|
||||||
if event::poll(Duration::from_millis(0))? { // process all enqueued events
|
|
||||||
let event = event::read()?;
|
|
||||||
|
|
||||||
match event {
|
|
||||||
Event::Key(key) => {
|
|
||||||
match key.modifiers {
|
|
||||||
KeyModifiers::SHIFT => {
|
|
||||||
match key.code {
|
|
||||||
KeyCode::Up => app.cfg.scale -= 1000, // inverted to act as zoom
|
|
||||||
KeyCode::Down => app.cfg.scale += 1000, // inverted to act as zoom
|
|
||||||
KeyCode::Right => app.cfg.width += 100,
|
|
||||||
KeyCode::Left => app.cfg.width -= 100,
|
|
||||||
KeyCode::PageUp => app.cfg.threshold += 1000.0,
|
|
||||||
KeyCode::PageDown => app.cfg.threshold -= 1000.0,
|
|
||||||
_ => {},
|
|
||||||
}
|
|
||||||
},
|
|
||||||
KeyModifiers::CONTROL => {
|
|
||||||
match key.code { // mimic other programs shortcuts to quit, for user friendlyness
|
|
||||||
KeyCode::Char('c') | KeyCode::Char('q') | KeyCode::Char('w') => quit = true,
|
|
||||||
KeyCode::Up => app.cfg.scale -= 50, // inverted to act as zoom
|
|
||||||
KeyCode::Down => app.cfg.scale += 50, // inverted to act as zoom
|
|
||||||
KeyCode::Right => app.cfg.width += 5,
|
|
||||||
KeyCode::Left => app.cfg.width -= 5,
|
|
||||||
KeyCode::PageUp => app.cfg.threshold += 50.0,
|
|
||||||
KeyCode::PageDown => app.cfg.threshold -= 50.0,
|
|
||||||
KeyCode::Char('r') => { // reset settings
|
|
||||||
app.cfg.references = !args.no_reference;
|
|
||||||
app.cfg.show_ui = !args.no_ui;
|
|
||||||
app.cfg.braille = !args.no_braille;
|
|
||||||
app.cfg.threshold = args.threshold;
|
|
||||||
app.cfg.width = args.buffer / (args.channels as u32 * 2); // TODO ...
|
|
||||||
app.cfg.scale = args.range;
|
|
||||||
app.cfg.vectorscope = args.vectorscope;
|
|
||||||
app.cfg.triggering = args.triggering;
|
|
||||||
},
|
|
||||||
_ => {},
|
|
||||||
}
|
|
||||||
},
|
|
||||||
_ => {
|
|
||||||
match key.code {
|
|
||||||
KeyCode::Char('q') => quit = true,
|
|
||||||
KeyCode::Char(' ') => app.cfg.pause = !app.cfg.pause,
|
|
||||||
KeyCode::Char('v') => app.cfg.vectorscope = !app.cfg.vectorscope,
|
|
||||||
KeyCode::Char('s') => app.cfg.scatter = !app.cfg.scatter,
|
|
||||||
KeyCode::Char('b') => app.cfg.braille = !app.cfg.braille,
|
|
||||||
KeyCode::Char('h') => app.cfg.show_ui = !app.cfg.show_ui,
|
|
||||||
KeyCode::Char('r') => app.cfg.references = !app.cfg.references,
|
|
||||||
KeyCode::Char('e') => app.cfg.falling_edge = !app.cfg.falling_edge,
|
|
||||||
KeyCode::Char('t') => app.cfg.triggering = !app.cfg.triggering,
|
|
||||||
KeyCode::Char('p') => app.cfg.peaks = !app.cfg.peaks,
|
|
||||||
KeyCode::Char('=') => app.cfg.depth += 1,
|
|
||||||
KeyCode::Char('-') => app.cfg.depth -= 1,
|
|
||||||
KeyCode::Char('+') => app.cfg.depth += 10,
|
|
||||||
KeyCode::Char('_') => app.cfg.depth -= 10,
|
|
||||||
KeyCode::Up => app.cfg.scale -= 250, // inverted to act as zoom
|
|
||||||
KeyCode::Down => app.cfg.scale += 250, // inverted to act as zoom
|
|
||||||
KeyCode::Right => app.cfg.width += 25,
|
|
||||||
KeyCode::Left => app.cfg.width -= 25,
|
|
||||||
KeyCode::PageUp => app.cfg.threshold += 250.0,
|
|
||||||
KeyCode::PageDown => app.cfg.threshold -= 250.0,
|
|
||||||
KeyCode::Tab => { // only reset "zoom"
|
|
||||||
app.cfg.width = args.buffer / (args.channels as u32 * 2); // TODO ...
|
|
||||||
app.cfg.scale = args.range;
|
|
||||||
},
|
|
||||||
KeyCode::Esc => { // back to oscilloscope
|
|
||||||
app.cfg.references = !args.no_reference;
|
|
||||||
app.cfg.show_ui = !args.no_ui;
|
|
||||||
app.cfg.vectorscope = args.vectorscope;
|
|
||||||
},
|
|
||||||
_ => {},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
app.update_values();
|
|
||||||
},
|
|
||||||
_ => {},
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(quit)
|
|
||||||
}
|
|
||||||
|
|
||||||
// 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; }
|
|
||||||
if falling_edge {
|
|
||||||
if data[index] >= threshold {
|
|
||||||
for i in 1..=depth as usize {
|
|
||||||
if data[index+i] >= threshold {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
} else {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
if data[index] <= threshold {
|
|
||||||
for i in 1..=depth as usize {
|
|
||||||
if data[index+i] <= threshold {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
} else {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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 (name, bounds) = match dim {
|
|
||||||
Dimension::X => (&app.names.x, &app.bounds.x),
|
|
||||||
Dimension::Y => (&app.names.y, &app.bounds.y),
|
|
||||||
};
|
|
||||||
let mut a = Axis::default();
|
|
||||||
if app.cfg.show_ui {
|
|
||||||
a = a.title(Span::styled(name, Style::default().fg(Color::Cyan)));
|
|
||||||
}
|
|
||||||
a.style(Style::default().fg(app.cfg.axis_color)).bounds(*bounds)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn channel_name(index: usize, vectorscope: bool) -> String {
|
|
||||||
if vectorscope { return format!("{}", index); }
|
|
||||||
match index {
|
|
||||||
0 => "L".into(),
|
|
||||||
1 => "R".into(),
|
|
||||||
_ => format!("{}", index),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
|
@ -1,61 +0,0 @@
|
||||||
use tui::style::Color;
|
|
||||||
|
|
||||||
// use crate::parser::SampleParser;
|
|
||||||
|
|
||||||
pub enum Dimension {
|
|
||||||
X, Y
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Default)]
|
|
||||||
pub struct ChartNames {
|
|
||||||
pub x: String,
|
|
||||||
pub y: String,
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
pub struct ChartBounds {
|
|
||||||
pub x: [f64;2],
|
|
||||||
pub y: [f64;2],
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Default for ChartBounds {
|
|
||||||
fn default() -> Self {
|
|
||||||
ChartBounds { x: [0.0, 0.0], y: [0.0, 0.0] }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub struct ChartReferences {
|
|
||||||
pub x: Vec<(f64, f64)>,
|
|
||||||
pub y: Vec<(f64, f64)>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Default for ChartReferences {
|
|
||||||
fn default() -> Self {
|
|
||||||
ChartReferences {
|
|
||||||
x: vec![(0.0, 0.0), (0.0, 1.0)],
|
|
||||||
y: vec![(0.5, 1.0), (0.5, -1.0)]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub struct AppConfig {
|
|
||||||
pub axis_color: Color,
|
|
||||||
pub palette: Vec<Color>,
|
|
||||||
|
|
||||||
pub scale: i32,
|
|
||||||
pub width: u32,
|
|
||||||
pub vectorscope: bool,
|
|
||||||
pub references: bool,
|
|
||||||
pub show_ui: bool,
|
|
||||||
pub peaks: bool,
|
|
||||||
|
|
||||||
pub triggering: bool,
|
|
||||||
pub threshold: f64,
|
|
||||||
pub depth: u32,
|
|
||||||
pub falling_edge: bool,
|
|
||||||
|
|
||||||
pub scatter: bool,
|
|
||||||
pub braille: bool,
|
|
||||||
|
|
||||||
pub pause: bool,
|
|
||||||
}
|
|
81
src/display/mod.rs
Normal file
81
src/display/mod.rs
Normal file
|
@ -0,0 +1,81 @@
|
||||||
|
pub mod oscilloscope;
|
||||||
|
pub mod vectorscope;
|
||||||
|
pub mod spectroscope;
|
||||||
|
|
||||||
|
use crossterm::event::Event;
|
||||||
|
use tui::{widgets::{Dataset, Axis, GraphType}, style::{Style, Color}, symbols::Marker};
|
||||||
|
|
||||||
|
pub enum Dimension {
|
||||||
|
X, Y
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct GraphConfig {
|
||||||
|
pub samples: u32,
|
||||||
|
pub scale: u32,
|
||||||
|
pub width: u32,
|
||||||
|
pub scatter: bool,
|
||||||
|
pub references: bool,
|
||||||
|
pub show_ui: bool,
|
||||||
|
pub marker_type: Marker,
|
||||||
|
pub palette: Vec<Color>,
|
||||||
|
pub labels_color: Color,
|
||||||
|
pub axis_color: Color,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl GraphConfig {
|
||||||
|
pub fn palette(&self, index: usize) -> Color {
|
||||||
|
*self.palette.get(index % self.palette.len()).unwrap_or(&Color::White)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[allow(clippy::ptr_arg)] // TODO temporarily! it's a shitty solution
|
||||||
|
pub trait DisplayMode {
|
||||||
|
// MUST define
|
||||||
|
fn axis(&self, cfg: &GraphConfig, dimension: Dimension) -> Axis; // TODO simplify this
|
||||||
|
fn process(&self, cfg: &GraphConfig, data: &Vec<Vec<f64>>) -> Vec<DataSet>;
|
||||||
|
|
||||||
|
// SHOULD override
|
||||||
|
fn handle(&mut self, _event: Event) {}
|
||||||
|
fn channel_name(&self, index: usize) -> String { format!("{}", index) }
|
||||||
|
fn header(&self, _cfg: &GraphConfig) -> String { "".into() }
|
||||||
|
fn references(&self, cfg: &GraphConfig) -> Vec<DataSet> {
|
||||||
|
let half_width = cfg.samples as f64 / 2.0;
|
||||||
|
vec![
|
||||||
|
DataSet::new("".into(), vec![(0.0, 0.0), (cfg.width as f64, 0.0)], cfg.marker_type, GraphType::Line, cfg.axis_color),
|
||||||
|
DataSet::new("".into(), vec![(half_width, -(cfg.scale as f64)), (half_width, cfg.scale as f64)], cfg.marker_type, GraphType::Line, cfg.axis_color),
|
||||||
|
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct DataSet {
|
||||||
|
name: String,
|
||||||
|
data: Vec<(f64, f64)>,
|
||||||
|
marker_type: Marker,
|
||||||
|
graph_type: GraphType,
|
||||||
|
color: Color,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a> From::<&'a DataSet> for Dataset<'a> {
|
||||||
|
fn from(ds: &'a DataSet) -> Dataset<'a> {
|
||||||
|
Dataset::default()
|
||||||
|
.name(ds.name.clone())
|
||||||
|
.marker(ds.marker_type)
|
||||||
|
.graph_type(ds.graph_type)
|
||||||
|
.style(Style::default().fg(ds.color))
|
||||||
|
.data(&ds.data)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl DataSet {
|
||||||
|
pub fn new(
|
||||||
|
name: String,
|
||||||
|
data: Vec<(f64, f64)>,
|
||||||
|
marker_type: Marker,
|
||||||
|
graph_type: GraphType,
|
||||||
|
color: Color
|
||||||
|
) -> Self {
|
||||||
|
DataSet { name, data, marker_type, graph_type, color }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
149
src/display/oscilloscope.rs
Normal file
149
src/display/oscilloscope.rs
Normal file
|
@ -0,0 +1,149 @@
|
||||||
|
use crossterm::event::{Event, KeyModifiers, KeyCode};
|
||||||
|
use tui::{widgets::{Axis, GraphType}, style::Style, text::Span};
|
||||||
|
|
||||||
|
use crate::app::update_value_f;
|
||||||
|
|
||||||
|
use super::{DisplayMode, GraphConfig, DataSet, Dimension};
|
||||||
|
|
||||||
|
pub struct Oscilloscope {
|
||||||
|
pub triggering: bool,
|
||||||
|
pub falling_edge: bool,
|
||||||
|
pub threshold: f64,
|
||||||
|
pub depth: u32,
|
||||||
|
pub peaks: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl DisplayMode for Oscilloscope {
|
||||||
|
fn channel_name(&self, index: usize) -> String {
|
||||||
|
match index {
|
||||||
|
0 => "L".into(),
|
||||||
|
1 => "R".into(),
|
||||||
|
_ => format!("{}", index),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn header(&self, _: &GraphConfig) -> String {
|
||||||
|
if self.triggering {
|
||||||
|
format!(
|
||||||
|
"{} {:.0}{} trigger",
|
||||||
|
if self.falling_edge { "v" } else { "^" },
|
||||||
|
self.threshold,
|
||||||
|
if self.depth > 1 { format!(":{}", self.depth) } else { "".into() },
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
"live".into()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn axis(&self, cfg: &GraphConfig, dimension: Dimension) -> Axis {
|
||||||
|
let (name, bounds) = match dimension {
|
||||||
|
Dimension::X => ("time -", [0.0, cfg.samples as f64]),
|
||||||
|
Dimension::Y => ("| amplitude", [-(cfg.scale as f64), cfg.scale as f64]),
|
||||||
|
};
|
||||||
|
let mut a = Axis::default();
|
||||||
|
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(&self, cfg: &GraphConfig, data: &Vec<Vec<f64>>) -> Vec<DataSet> {
|
||||||
|
let mut out = Vec::new();
|
||||||
|
|
||||||
|
let mut trigger_offset = 0;
|
||||||
|
if self.triggering {
|
||||||
|
for i in 0..data[0].len() {
|
||||||
|
if triggered(&data[0], i, self.threshold, self.depth, self.falling_edge) { // triggered
|
||||||
|
break;
|
||||||
|
} else {
|
||||||
|
trigger_offset += 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if self.triggering {
|
||||||
|
out.push(DataSet::new("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 i >= trigger_offset {
|
||||||
|
tmp.push(((i - trigger_offset) as f64, *sample));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if self.peaks {
|
||||||
|
out.push(DataSet::new(
|
||||||
|
"".into(),
|
||||||
|
vec![(0.0, min), (0.0, max)],
|
||||||
|
cfg.marker_type,
|
||||||
|
GraphType::Scatter,
|
||||||
|
cfg.palette(n)
|
||||||
|
))
|
||||||
|
}
|
||||||
|
|
||||||
|
out.push(DataSet::new(
|
||||||
|
self.channel_name(n),
|
||||||
|
tmp,
|
||||||
|
cfg.marker_type,
|
||||||
|
if cfg.scatter { GraphType::Scatter } else { GraphType::Line },
|
||||||
|
cfg.palette(n),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
out
|
||||||
|
}
|
||||||
|
|
||||||
|
fn handle(&mut self, event: Event) {
|
||||||
|
if let Event::Key(key) = event {
|
||||||
|
let magnitude = match key.modifiers {
|
||||||
|
KeyModifiers::SHIFT => 10.0,
|
||||||
|
KeyModifiers::CONTROL => 5.0,
|
||||||
|
KeyModifiers::ALT => 0.2,
|
||||||
|
_ => 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::Char('e') => self.falling_edge = !self.falling_edge,
|
||||||
|
KeyCode::Char('p') => self.peaks = !self.peaks,
|
||||||
|
KeyCode::Char('=') => self.depth += 1,
|
||||||
|
KeyCode::Char('-') => self.depth -= 1,
|
||||||
|
KeyCode::Char('+') => self.depth += 10,
|
||||||
|
KeyCode::Char('_') => self.depth -= 10,
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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; }
|
||||||
|
if falling_edge {
|
||||||
|
if data[index] >= threshold {
|
||||||
|
for i in 1..=depth as usize {
|
||||||
|
if data[index+i] >= threshold {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
true
|
||||||
|
} else {
|
||||||
|
false
|
||||||
|
}
|
||||||
|
} else if data[index] <= threshold {
|
||||||
|
for i in 1..=depth as usize {
|
||||||
|
if data[index+i] <= threshold {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
true
|
||||||
|
} else {
|
||||||
|
false
|
||||||
|
}
|
||||||
|
}
|
53
src/display/spectroscope.rs
Normal file
53
src/display/spectroscope.rs
Normal file
|
@ -0,0 +1,53 @@
|
||||||
|
use tui::{widgets::{Axis, GraphType}, style::Style, text::Span};
|
||||||
|
|
||||||
|
use super::{DisplayMode, GraphConfig, DataSet, Dimension};
|
||||||
|
|
||||||
|
use easyfft::prelude::*;
|
||||||
|
|
||||||
|
pub struct Spectroscope {}
|
||||||
|
|
||||||
|
impl DisplayMode for Spectroscope {
|
||||||
|
fn channel_name(&self, index: usize) -> String {
|
||||||
|
format!("{}", index)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn header(&self, _: &GraphConfig) -> String {
|
||||||
|
"live".into()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn axis(&self, cfg: &GraphConfig, dimension: Dimension) -> Axis {
|
||||||
|
let (name, bounds) = match dimension {
|
||||||
|
Dimension::X => ("frequency -", [-(cfg.scale as f64), cfg.scale as f64]),
|
||||||
|
Dimension::Y => ("| level", [-(cfg.scale as f64), cfg.scale as f64]),
|
||||||
|
};
|
||||||
|
let mut a = Axis::default();
|
||||||
|
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<DataSet> {
|
||||||
|
vec![
|
||||||
|
DataSet::new("".into(), vec![(0.0, 0.0), (20000.0, 0.0)], cfg.marker_type, GraphType::Line, cfg.axis_color),
|
||||||
|
DataSet::new("".into(), vec![(0.0, 0.0), (0.0, cfg.scale as f64)], cfg.marker_type, GraphType::Line, cfg.axis_color),
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
fn process(&self, cfg: &GraphConfig, data: &Vec<Vec<f64>>) -> Vec<DataSet> {
|
||||||
|
let mut out = Vec::new();
|
||||||
|
|
||||||
|
for (n, chunk) in data.iter().enumerate() {
|
||||||
|
let tmp = chunk.real_fft().iter().map(|x| (x.re, x.im)).collect();
|
||||||
|
out.push(DataSet::new(
|
||||||
|
self.channel_name(n),
|
||||||
|
tmp,
|
||||||
|
cfg.marker_type,
|
||||||
|
if cfg.scatter { GraphType::Scatter } else { GraphType::Line },
|
||||||
|
cfg.palette(n),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
out
|
||||||
|
}
|
||||||
|
}
|
76
src/display/vectorscope.rs
Normal file
76
src/display/vectorscope.rs
Normal file
|
@ -0,0 +1,76 @@
|
||||||
|
use tui::{widgets::{Axis, GraphType}, style::Style, text::Span};
|
||||||
|
|
||||||
|
use super::{DisplayMode, GraphConfig, DataSet, Dimension};
|
||||||
|
|
||||||
|
pub struct Vectorscope {}
|
||||||
|
|
||||||
|
impl DisplayMode for Vectorscope {
|
||||||
|
fn channel_name(&self, index: usize) -> String {
|
||||||
|
format!("{}", index)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn header(&self, _: &GraphConfig) -> String {
|
||||||
|
"live".into()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn axis(&self, cfg: &GraphConfig, dimension: Dimension) -> Axis {
|
||||||
|
let (name, bounds) = match dimension {
|
||||||
|
Dimension::X => ("left -", [-(cfg.scale as f64), cfg.scale as f64]),
|
||||||
|
Dimension::Y => ("| right", [-(cfg.scale as f64), cfg.scale as f64]),
|
||||||
|
};
|
||||||
|
let mut a = Axis::default();
|
||||||
|
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<DataSet> {
|
||||||
|
vec![
|
||||||
|
DataSet::new("".into(), vec![(-(cfg.scale as f64), 0.0), (cfg.scale as f64, 0.0)], cfg.marker_type, GraphType::Line, cfg.axis_color),
|
||||||
|
DataSet::new("".into(), vec![(0.0, -(cfg.scale as f64)), (0.0, cfg.scale as f64)], cfg.marker_type, GraphType::Line, cfg.axis_color),
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
fn process(&self, cfg: &GraphConfig, data: &Vec<Vec<f64>>) -> Vec<DataSet> {
|
||||||
|
let mut out = Vec::new();
|
||||||
|
|
||||||
|
for (n, chunk) in data.chunks(2).enumerate() {
|
||||||
|
let mut tmp = vec![];
|
||||||
|
match chunk.len() {
|
||||||
|
2 => {
|
||||||
|
for i in 0..std::cmp::min(chunk[0].len(), chunk[1].len()) {
|
||||||
|
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 }
|
||||||
|
tmp.push((chunk[0][i], i as f64));
|
||||||
|
}
|
||||||
|
},
|
||||||
|
_ => continue,
|
||||||
|
}
|
||||||
|
// split it in two for easier coloring
|
||||||
|
// TODO configure splitting in multiple parts?
|
||||||
|
let pivot = tmp.len() / 2;
|
||||||
|
out.push(DataSet::new(
|
||||||
|
self.channel_name((n * 2) + 1),
|
||||||
|
tmp[pivot..].to_vec(),
|
||||||
|
cfg.marker_type,
|
||||||
|
if cfg.scatter { GraphType::Scatter } else { GraphType::Line },
|
||||||
|
cfg.palette((n * 2) + 1),
|
||||||
|
));
|
||||||
|
out.push(DataSet::new(
|
||||||
|
self.channel_name(n * 2),
|
||||||
|
tmp[..pivot].to_vec(),
|
||||||
|
cfg.marker_type,
|
||||||
|
if cfg.scatter { GraphType::Scatter } else { GraphType::Line },
|
||||||
|
cfg.palette(n * 2),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
out
|
||||||
|
}
|
||||||
|
}
|
21
src/main.rs
21
src/main.rs
|
@ -1,9 +1,11 @@
|
||||||
mod parser;
|
mod parser;
|
||||||
mod app;
|
mod app;
|
||||||
mod config;
|
|
||||||
mod music;
|
mod music;
|
||||||
mod source;
|
mod source;
|
||||||
|
mod display;
|
||||||
|
|
||||||
|
use app::App;
|
||||||
|
use source::PulseAudioSimple;
|
||||||
use tui::{
|
use tui::{
|
||||||
backend::CrosstermBackend,
|
backend::CrosstermBackend,
|
||||||
Terminal,
|
Terminal,
|
||||||
|
@ -17,7 +19,6 @@ use crossterm::{
|
||||||
use clap::Parser;
|
use clap::Parser;
|
||||||
|
|
||||||
use crate::music::Note;
|
use crate::music::Note;
|
||||||
use crate::app::run_app;
|
|
||||||
|
|
||||||
const HELP_TEMPLATE : &str = "{before-help}\
|
const HELP_TEMPLATE : &str = "{before-help}\
|
||||||
{name} {version} -- by {author}
|
{name} {version} -- by {author}
|
||||||
|
@ -41,7 +42,7 @@ pub struct Args {
|
||||||
|
|
||||||
/// Max value, positive and negative, on amplitude scale
|
/// Max value, positive and negative, on amplitude scale
|
||||||
#[arg(short, long, value_name = "SIZE", default_value_t = 20000)]
|
#[arg(short, long, value_name = "SIZE", default_value_t = 20000)]
|
||||||
range: i32, // TODO counterintuitive, improve this
|
range: u32, // TODO counterintuitive, improve this
|
||||||
|
|
||||||
/// Use vintage looking scatter mode instead of line mode
|
/// Use vintage looking scatter mode instead of line mode
|
||||||
#[arg(long, default_value_t = false)]
|
#[arg(long, default_value_t = false)]
|
||||||
|
@ -52,7 +53,7 @@ pub struct Args {
|
||||||
vectorscope: bool,
|
vectorscope: bool,
|
||||||
|
|
||||||
/// Show peaks for each channel as dots
|
/// Show peaks for each channel as dots
|
||||||
#[arg(long, default_value_t = false)]
|
#[arg(long, default_value_t = true)]
|
||||||
show_peaks: bool,
|
show_peaks: bool,
|
||||||
|
|
||||||
/// Tune buffer size to be in tune with given note (overrides buffer option)
|
/// Tune buffer size to be in tune with given note (overrides buffer option)
|
||||||
|
@ -114,6 +115,16 @@ fn main() -> Result<(), std::io::Error> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let source = PulseAudioSimple::new(
|
||||||
|
args.device.as_deref(),
|
||||||
|
args.channels,
|
||||||
|
args.sample_rate,
|
||||||
|
args.buffer,
|
||||||
|
args.server_buffer
|
||||||
|
).unwrap();
|
||||||
|
|
||||||
|
let mut app = App::from(&args);
|
||||||
|
|
||||||
// setup terminal
|
// setup terminal
|
||||||
enable_raw_mode()?;
|
enable_raw_mode()?;
|
||||||
let mut stdout = std::io::stdout();
|
let mut stdout = std::io::stdout();
|
||||||
|
@ -122,7 +133,7 @@ fn main() -> Result<(), std::io::Error> {
|
||||||
let mut terminal = Terminal::new(backend)?;
|
let mut terminal = Terminal::new(backend)?;
|
||||||
terminal.hide_cursor()?;
|
terminal.hide_cursor()?;
|
||||||
|
|
||||||
let res = run_app(args, &mut terminal);
|
let res = app.run(source, &mut terminal);
|
||||||
|
|
||||||
// restore terminal
|
// restore terminal
|
||||||
disable_raw_mode()?;
|
disable_raw_mode()?;
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
use libpulse_binding::{stream::Direction, sample::{Spec, Format}, def::BufferAttr, error::{PAErr, Code}};
|
use libpulse_binding::{sample::{Spec, Format}, def::BufferAttr, error::PAErr, stream::Direction};
|
||||||
use libpulse_simple_binding::Simple;
|
use libpulse_simple_binding::Simple;
|
||||||
|
|
||||||
pub trait DataSource {
|
pub trait DataSource {
|
||||||
|
|
Loading…
Reference in a new issue