mirror of
https://github.com/alemidev/scope-tui.git
synced 2025-01-09 02:43:55 +01:00
feat: better UI and keybinds, more CLI opts
Added CLI option to specify channel number, but I'm not sure it's really working. Refactored AppConfig a little. Added threshold indication on status bar. References and UI are separate settings and can be toggled individually. At least 1 sample is always shown when discarding due to triggering. Range and threshold are configured with arrow keys. Added more colors to palette. Added ability to toggle braille mode. Fix: fully write "fps" instead of cutting "s" out.
This commit is contained in:
parent
df273dd011
commit
d72e471dbd
4 changed files with 192 additions and 127 deletions
277
src/app.rs
277
src/app.rs
|
@ -26,6 +26,12 @@ pub struct App {
|
||||||
|
|
||||||
impl App {
|
impl App {
|
||||||
pub fn update_values(&mut self) {
|
pub fn update_values(&mut self) {
|
||||||
|
if self.cfg.scale > 32768 {
|
||||||
|
self.cfg.scale = 32768;
|
||||||
|
}
|
||||||
|
if self.cfg.scale < 0 {
|
||||||
|
self.cfg.scale = 0;
|
||||||
|
}
|
||||||
if self.cfg.vectorscope {
|
if self.cfg.vectorscope {
|
||||||
self.names.x = "left -".into();
|
self.names.x = "left -".into();
|
||||||
self.names.y = "| right".into();
|
self.names.y = "| right".into();
|
||||||
|
@ -44,72 +50,41 @@ impl App {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn bounds(&self, axis: &Dimension) -> [f64;2] {
|
pub fn marker_type(&self) -> symbols::Marker {
|
||||||
match axis {
|
if self.cfg.braille {
|
||||||
Dimension::X => self.bounds.x,
|
symbols::Marker::Braille
|
||||||
Dimension::Y => self.bounds.y,
|
} else {
|
||||||
|
symbols::Marker::Dot
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn name(&self, axis: &Dimension) -> &str {
|
pub fn graph_type(&self) -> GraphType {
|
||||||
match axis {
|
if self.cfg.scatter {
|
||||||
Dimension::X => self.names.x.as_str(),
|
GraphType::Scatter
|
||||||
Dimension::Y => self.names.y.as_str(),
|
} else {
|
||||||
|
GraphType::Line
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn scatter(&self) -> bool {
|
pub fn palette(&self, index: usize) -> Color {
|
||||||
match self.cfg.graph_type {
|
*self.cfg.palette.get(index % self.cfg.palette.len()).unwrap_or(&Color::White)
|
||||||
GraphType::Scatter => true,
|
|
||||||
_ => false,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// pub fn references(&self) -> Vec<Dataset> {
|
|
||||||
// vec![
|
|
||||||
// Dataset::default()
|
|
||||||
// .name("")
|
|
||||||
// .marker(self.cfg.marker_type)
|
|
||||||
// .graph_type(GraphType::Line)
|
|
||||||
// .style(Style::default().fg(self.cfg.axis_color))
|
|
||||||
// .data(&self.references.x),
|
|
||||||
// Dataset::default()
|
|
||||||
// .name("")
|
|
||||||
// .marker(self.cfg.marker_type)
|
|
||||||
// .graph_type(GraphType::Line)
|
|
||||||
// .style(Style::default().fg(self.cfg.axis_color))
|
|
||||||
// .data(&self.references.y),
|
|
||||||
// ]
|
|
||||||
// }
|
|
||||||
|
|
||||||
pub fn update_scale(&mut self, increment: i32) {
|
|
||||||
if increment > 0 || increment.abs() < self.cfg.scale as i32 {
|
|
||||||
self.cfg.scale = ((self.cfg.scale as i32) + increment) as u32;
|
|
||||||
self.update_values();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn set_scatter(&mut self, scatter: bool) {
|
|
||||||
self.cfg.graph_type = if scatter { GraphType::Scatter } else { GraphType::Line };
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
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 marker_type = if args.no_braille { symbols::Marker::Dot } else { symbols::Marker::Braille };
|
|
||||||
let graph_type = if args.scatter { GraphType::Scatter } else { GraphType::Line };
|
|
||||||
|
|
||||||
let cfg = AppConfig {
|
let cfg = AppConfig {
|
||||||
title: "TUI Oscilloscope -- <me@alemi.dev>".into(),
|
|
||||||
axis_color: Color::DarkGray,
|
axis_color: Color::DarkGray,
|
||||||
palette: vec![Color::Red, Color::Yellow],
|
palette: vec![Color::Red, Color::Yellow, Color::Green, Color::Magenta],
|
||||||
scale: args.range,
|
scale: args.range,
|
||||||
width: args.buffer / 4, // TODO It's 4 because 2 channels and 2 bytes per sample!
|
width: args.buffer / (2 * args.channels as u32), // TODO also make bit depth customizable
|
||||||
triggering: args.triggering,
|
triggering: args.triggering,
|
||||||
threshold: args.threshold,
|
threshold: args.threshold,
|
||||||
vectorscope: args.vectorscope,
|
vectorscope: args.vectorscope,
|
||||||
references: !args.no_reference,
|
references: !args.no_reference,
|
||||||
marker_type, graph_type,
|
show_ui: !args.no_ui,
|
||||||
|
braille: !args.no_braille,
|
||||||
|
scatter: args.scatter,
|
||||||
};
|
};
|
||||||
|
|
||||||
let mut app = App {
|
let mut app = App {
|
||||||
|
@ -136,7 +111,7 @@ pub fn run_app<T : Backend>(args: Args, terminal: &mut Terminal<T>) -> Result<()
|
||||||
// setup audio capture
|
// setup audio capture
|
||||||
let spec = Spec {
|
let spec = Spec {
|
||||||
format: Format::S16NE,
|
format: Format::S16NE,
|
||||||
channels: 2,
|
channels: args.channels,
|
||||||
rate: args.sample_rate,
|
rate: args.sample_rate,
|
||||||
};
|
};
|
||||||
assert!(spec.is_valid());
|
assert!(spec.is_valid());
|
||||||
|
@ -151,7 +126,7 @@ pub fn run_app<T : Backend>(args: Args, terminal: &mut Terminal<T>) -> Result<()
|
||||||
"ScopeTUI", // Our application’s name
|
"ScopeTUI", // Our application’s name
|
||||||
Direction::Record, // We want a record stream
|
Direction::Record, // We want a record stream
|
||||||
dev, // Use requested device, or default
|
dev, // Use requested device, or default
|
||||||
"Music", // Description of our stream
|
"data", // Description of our stream
|
||||||
&spec, // Our sample format
|
&spec, // Our sample format
|
||||||
None, // Use default channel map
|
None, // Use default channel map
|
||||||
Some(&BufferAttr {
|
Some(&BufferAttr {
|
||||||
|
@ -182,66 +157,79 @@ pub fn run_app<T : Backend>(args: Args, terminal: &mut Terminal<T>) -> Result<()
|
||||||
}
|
}
|
||||||
|
|
||||||
if !pause {
|
if !pause {
|
||||||
channels = fmt.oscilloscope(&mut buffer, 2);
|
channels = fmt.oscilloscope(&mut buffer, args.channels);
|
||||||
}
|
|
||||||
|
|
||||||
if app.cfg.triggering {
|
if app.cfg.triggering {
|
||||||
// TODO allow to customize channel to use for triggering and threshold
|
// TODO allow to customize channel to use for triggering
|
||||||
if let Some(ch) = channels.get(0) {
|
if let Some(ch) = channels.get(0) {
|
||||||
let mut discard = 0;
|
let mut discard = 0;
|
||||||
for i in 0..ch.len() { // seek to first sample rising through threshold
|
for i in 0..ch.len()-1 { // seek to first sample rising through threshold
|
||||||
if i + 1 < ch.len() && ch[i] <= app.cfg.threshold && ch[i+1] > app.cfg.threshold { // triggered
|
if ch[i] <= app.cfg.threshold && ch[i+1] > app.cfg.threshold { // triggered
|
||||||
break;
|
break;
|
||||||
} else {
|
} else {
|
||||||
discard += 1;
|
discard += 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for ch in channels.iter_mut() {
|
||||||
|
let limit = if ch.len() < discard { ch.len() } else { discard };
|
||||||
|
*ch = ch[limit..].to_vec();
|
||||||
}
|
}
|
||||||
}
|
|
||||||
for ch in channels.iter_mut() {
|
|
||||||
*ch = ch[discard..].to_vec();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let samples = channels.iter().map(|x| x.len()).max().unwrap_or(0);
|
let samples = channels.iter().map(|x| x.len()).max().unwrap_or(0);
|
||||||
|
|
||||||
let mut measures;
|
let mut measures : Vec<(String, Vec<(f64, f64)>)>;
|
||||||
|
|
||||||
|
// 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 {
|
if app.cfg.vectorscope {
|
||||||
measures = vec![];
|
measures = vec![];
|
||||||
for chunk in channels.chunks(2) {
|
for (i, chunk) in channels.chunks(2).enumerate() {
|
||||||
let mut tmp = vec![];
|
let mut tmp = vec![];
|
||||||
for i in 0..chunk[0].len() {
|
match chunk.len() {
|
||||||
tmp.push((chunk[0][i] as f64, chunk[1][i] as f64));
|
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
|
// split it in two so the math downwards still works the same
|
||||||
let pivot = tmp.len() / 2;
|
let pivot = tmp.len() / 2;
|
||||||
measures.push(tmp[pivot..].to_vec()); // put more recent first
|
measures.push((channel_name(i * 2, true), tmp[pivot..].to_vec())); // put more recent first
|
||||||
measures.push(tmp[..pivot].to_vec());
|
measures.push((channel_name((i * 2) + 1, true), tmp[..pivot].to_vec()));
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
measures = vec![vec![]; channels.len()];
|
measures = vec![];
|
||||||
for i in 0..channels[0].len() {
|
for (i, channel) in channels.iter().enumerate() {
|
||||||
for j in 0..channels.len() {
|
let mut tmp = vec![];
|
||||||
measures[j].push((i as f64, channels[j][i]));
|
for i in 0..channel.len() {
|
||||||
|
tmp.push((i as f64, channel[i]));
|
||||||
}
|
}
|
||||||
|
measures.push((channel_name(i, false), tmp));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let mut datasets = vec![];
|
let mut datasets = vec![];
|
||||||
let trigger_pt;
|
|
||||||
|
|
||||||
if app.cfg.references {
|
if app.cfg.references {
|
||||||
trigger_pt = [(0.0, app.cfg.threshold)];
|
datasets.push(data_set("", &app.references.x, app.marker_type(), GraphType::Line, app.cfg.axis_color));
|
||||||
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.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));
|
|
||||||
datasets.push(data_set("T", &trigger_pt, app.cfg.marker_type, GraphType::Scatter, Color::Cyan));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
let ds_names = if app.cfg.vectorscope { vec!["1", "2"] } else { vec!["R", "L"] };
|
let trigger_pt = [(0.0, app.cfg.threshold)];
|
||||||
let palette : Vec<Color> = app.cfg.palette.iter().rev().map(|x| x.clone()).collect();
|
datasets.push(data_set("T", &trigger_pt, app.marker_type(), GraphType::Scatter, Color::Cyan));
|
||||||
|
|
||||||
for (i, ds) in measures.iter().rev().enumerate() {
|
let m_len = measures.len() - 1;
|
||||||
datasets.push(data_set(ds_names[i], ds, app.cfg.marker_type, app.cfg.graph_type, palette[i % palette.len()]));
|
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;
|
fps += 1;
|
||||||
|
@ -254,23 +242,8 @@ pub fn run_app<T : Backend>(args: Args, terminal: &mut Terminal<T>) -> Result<()
|
||||||
|
|
||||||
terminal.draw(|f| {
|
terminal.draw(|f| {
|
||||||
let mut size = f.size();
|
let mut size = f.size();
|
||||||
if app.cfg.references {
|
if app.cfg.show_ui {
|
||||||
let heading = Table::new(
|
let heading = header(&args, &app, samples as u32, framerate);
|
||||||
vec![
|
|
||||||
Row::new(
|
|
||||||
vec![
|
|
||||||
Cell::from(format!("TUI {}", if app.cfg.vectorscope { "Vectorscope" } else { "Oscilloscope" })).style(Style::default().fg(Color::Yellow).add_modifier(Modifier::BOLD)),
|
|
||||||
Cell::from(format!("{}{} mode", if app.cfg.triggering { "triggered " } else { "" }, if app.scatter() { "scatter" } else { "line" })),
|
|
||||||
Cell::from(format!("range +-{}", app.cfg.scale)),
|
|
||||||
Cell::from(format!("{}smpl", samples as u32)),
|
|
||||||
Cell::from(format!("{:.1}kHz", args.sample_rate as f32 / 1000.0)),
|
|
||||||
Cell::from(format!("{}fps", framerate)),
|
|
||||||
]
|
|
||||||
)
|
|
||||||
]
|
|
||||||
)
|
|
||||||
.style(Style::default().fg(Color::Cyan))
|
|
||||||
.widths(&[Constraint::Percentage(40), Constraint::Percentage(20), Constraint::Percentage(20), Constraint::Percentage(7), Constraint::Percentage(7), Constraint::Percentage(6)]);
|
|
||||||
f.render_widget(heading, Rect { x: size.x, y: size.y, width: size.width, height:1 });
|
f.render_widget(heading, Rect { x: size.x, y: size.y, width: size.width, height:1 });
|
||||||
size.height -= 1;
|
size.height -= 1;
|
||||||
size.y += 1;
|
size.y += 1;
|
||||||
|
@ -283,9 +256,31 @@ pub fn run_app<T : Backend>(args: Args, terminal: &mut Terminal<T>) -> Result<()
|
||||||
|
|
||||||
if let Some(Event::Key(key)) = poll_event()? {
|
if let Some(Event::Key(key)) = poll_event()? {
|
||||||
match key.modifiers {
|
match key.modifiers {
|
||||||
KeyModifiers::CONTROL => {
|
KeyModifiers::SHIFT => {
|
||||||
match key.code {
|
match key.code {
|
||||||
|
KeyCode::Up => app.cfg.threshold += 1000.0,
|
||||||
|
KeyCode::Down => app.cfg.threshold -= 1000.0,
|
||||||
|
KeyCode::Right => app.cfg.scale += 1000,
|
||||||
|
KeyCode::Left => app.cfg.scale -= 1000,
|
||||||
|
_ => {},
|
||||||
|
}
|
||||||
|
},
|
||||||
|
KeyModifiers::CONTROL => {
|
||||||
|
match key.code { // mimic other programs shortcuts to quit, for user friendlyness
|
||||||
KeyCode::Char('c') | KeyCode::Char('q') | KeyCode::Char('w') => break,
|
KeyCode::Char('c') | KeyCode::Char('q') | KeyCode::Char('w') => break,
|
||||||
|
KeyCode::Up => app.cfg.threshold += 10.0,
|
||||||
|
KeyCode::Down => app.cfg.threshold -= 10.0,
|
||||||
|
KeyCode::Right => app.cfg.scale += 10,
|
||||||
|
KeyCode::Left => app.cfg.scale -= 10,
|
||||||
|
_ => {},
|
||||||
|
}
|
||||||
|
},
|
||||||
|
KeyModifiers::ALT => {
|
||||||
|
match key.code {
|
||||||
|
KeyCode::Up => app.cfg.threshold += 1.0,
|
||||||
|
KeyCode::Down => app.cfg.threshold -= 1.0,
|
||||||
|
KeyCode::Right => app.cfg.scale += 1,
|
||||||
|
KeyCode::Left => app.cfg.scale -= 1,
|
||||||
_ => {},
|
_ => {},
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
@ -293,18 +288,25 @@ pub fn run_app<T : Backend>(args: Args, terminal: &mut Terminal<T>) -> Result<()
|
||||||
match key.code {
|
match key.code {
|
||||||
KeyCode::Char('q') => break,
|
KeyCode::Char('q') => break,
|
||||||
KeyCode::Char(' ') => pause = !pause,
|
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('v') => app.cfg.vectorscope = !app.cfg.vectorscope,
|
||||||
KeyCode::Char('s') => app.set_scatter(!app.scatter()), // TODO no funcs
|
KeyCode::Char('s') => app.cfg.scatter = !app.cfg.scatter,
|
||||||
KeyCode::Char('h') => app.cfg.references = !app.cfg.references,
|
KeyCode::Char('b') => app.cfg.braille = !app.cfg.braille,
|
||||||
KeyCode::Char('t') => app.cfg.triggering = !app.cfg.triggering,
|
KeyCode::Char('h') => app.cfg.show_ui = !app.cfg.show_ui,
|
||||||
KeyCode::Up => app.cfg.threshold += 100.0,
|
KeyCode::Char('r') => app.cfg.references = !app.cfg.references,
|
||||||
KeyCode::Down => app.cfg.threshold -= 100.0,
|
KeyCode::Char('t') => app.cfg.triggering = !app.cfg.triggering,
|
||||||
KeyCode::PageUp => app.cfg.threshold += 1000.0,
|
KeyCode::Up => app.cfg.threshold += 100.0,
|
||||||
KeyCode::PageDown => app.cfg.threshold -= 1000.0,
|
KeyCode::Down => app.cfg.threshold -= 100.0,
|
||||||
|
KeyCode::Right => app.cfg.scale += 100,
|
||||||
|
KeyCode::Left => app.cfg.scale -= 100,
|
||||||
|
KeyCode::Esc => { // reset settings
|
||||||
|
app.cfg.references = !args.no_reference;
|
||||||
|
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;
|
||||||
|
},
|
||||||
_ => {},
|
_ => {},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -316,9 +318,38 @@ pub fn run_app<T : Backend>(args: Args, terminal: &mut Terminal<T>) -> Result<()
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
// TODO these functions probably shouldn't be here
|
// TODO these functions probably shouldn't be here
|
||||||
|
|
||||||
|
fn header(args: &Args, app: &App, samples: u32, framerate: u32) -> Table<'static> {
|
||||||
|
Table::new(
|
||||||
|
vec![
|
||||||
|
Row::new(
|
||||||
|
vec![
|
||||||
|
Cell::from(format!("TUI {}", if app.cfg.vectorscope { "Vectorscope" } else { "Oscilloscope" })).style(Style::default().fg(Color::Yellow).add_modifier(Modifier::BOLD)),
|
||||||
|
Cell::from(format!("{} plot", if app.cfg.scatter { "scatter" } else { "line" })),
|
||||||
|
Cell::from(format!("{}", if app.cfg.triggering { "triggered" } else { "live" } )),
|
||||||
|
Cell::from(format!("threshold {:.0}", app.cfg.threshold)),
|
||||||
|
Cell::from(format!("range +-{}", app.cfg.scale)),
|
||||||
|
Cell::from(format!("{}smpl", samples as u32)),
|
||||||
|
Cell::from(format!("{:.1}kHz", args.sample_rate as f32 / 1000.0)),
|
||||||
|
Cell::from(format!("{}fps", framerate)),
|
||||||
|
]
|
||||||
|
)
|
||||||
|
]
|
||||||
|
)
|
||||||
|
.style(Style::default().fg(Color::Cyan))
|
||||||
|
.widths(&[
|
||||||
|
Constraint::Percentage(32),
|
||||||
|
Constraint::Percentage(12),
|
||||||
|
Constraint::Percentage(12),
|
||||||
|
Constraint::Percentage(12),
|
||||||
|
Constraint::Percentage(12),
|
||||||
|
Constraint::Percentage(6),
|
||||||
|
Constraint::Percentage(6),
|
||||||
|
Constraint::Percentage(6)
|
||||||
|
])
|
||||||
|
}
|
||||||
|
|
||||||
fn poll_event() -> Result<Option<Event>, std::io::Error> {
|
fn poll_event() -> Result<Option<Event>, std::io::Error> {
|
||||||
if event::poll(Duration::from_millis(0))? {
|
if event::poll(Duration::from_millis(0))? {
|
||||||
Ok(Some(event::read()?))
|
Ok(Some(event::read()?))
|
||||||
|
@ -343,10 +374,22 @@ fn data_set<'a>(
|
||||||
}
|
}
|
||||||
|
|
||||||
fn axis(app: &App, dim: Dimension) -> Axis {
|
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();
|
let mut a = Axis::default();
|
||||||
if app.cfg.references {
|
if app.cfg.show_ui {
|
||||||
a = a.title(Span::styled(app.name(&dim), Style::default().fg(Color::Cyan)));
|
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),
|
||||||
}
|
}
|
||||||
a.style(Style::default().fg(app.cfg.axis_color))
|
|
||||||
.bounds(app.bounds(&dim))
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
use tui::{style::Color, widgets::GraphType, symbols};
|
use tui::style::Color;
|
||||||
|
|
||||||
// use crate::parser::SampleParser;
|
// use crate::parser::SampleParser;
|
||||||
|
|
||||||
|
@ -39,18 +39,18 @@ impl Default for ChartReferences {
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct AppConfig {
|
pub struct AppConfig {
|
||||||
pub title: String,
|
|
||||||
pub axis_color: Color,
|
pub axis_color: Color,
|
||||||
pub palette: Vec<Color>,
|
pub palette: Vec<Color>,
|
||||||
|
|
||||||
pub scale: u32,
|
pub scale: i32,
|
||||||
pub width: u32,
|
pub width: u32,
|
||||||
pub vectorscope: bool,
|
pub vectorscope: bool,
|
||||||
pub references: bool,
|
pub references: bool,
|
||||||
|
pub show_ui: bool,
|
||||||
|
|
||||||
pub triggering: bool,
|
pub triggering: bool,
|
||||||
pub threshold: f64,
|
pub threshold: f64,
|
||||||
|
|
||||||
pub marker_type: symbols::Marker,
|
pub scatter: bool,
|
||||||
pub graph_type: GraphType,
|
pub braille: bool,
|
||||||
}
|
}
|
||||||
|
|
23
src/main.rs
23
src/main.rs
|
@ -17,9 +17,18 @@ use clap::Parser;
|
||||||
use crate::music::Note;
|
use crate::music::Note;
|
||||||
use crate::app::run_app;
|
use crate::app::run_app;
|
||||||
|
|
||||||
|
const HELP_TEMPLATE : &str = "{before-help}\
|
||||||
|
{name} {version} -- by {author}
|
||||||
|
{about}
|
||||||
|
|
||||||
|
{usage-heading} {usage}
|
||||||
|
|
||||||
|
{all-args}{after-help}
|
||||||
|
";
|
||||||
|
|
||||||
/// A simple oscilloscope/vectorscope for your terminal
|
/// A simple oscilloscope/vectorscope for your terminal
|
||||||
#[derive(Parser, Debug)]
|
#[derive(Parser, Debug)]
|
||||||
#[command(author, version, about, long_about = None)]
|
#[command(author, version, about, long_about = None, help_template = HELP_TEMPLATE)]
|
||||||
pub struct Args {
|
pub struct Args {
|
||||||
/// Audio device to attach to
|
/// Audio device to attach to
|
||||||
device: Option<String>,
|
device: Option<String>,
|
||||||
|
@ -30,7 +39,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: u32, // TODO counterintuitive, improve this
|
range: i32, // 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)]
|
||||||
|
@ -44,6 +53,10 @@ pub struct Args {
|
||||||
#[arg(long, value_name = "NOTE")]
|
#[arg(long, value_name = "NOTE")]
|
||||||
tune: Option<String>,
|
tune: Option<String>,
|
||||||
|
|
||||||
|
/// Number of channels to open
|
||||||
|
#[arg(long, value_name = "N", default_value_t = 2)]
|
||||||
|
channels: u8,
|
||||||
|
|
||||||
/// Sample rate to use
|
/// Sample rate to use
|
||||||
#[arg(long, value_name = "HZ", default_value_t = 44100)]
|
#[arg(long, value_name = "HZ", default_value_t = 44100)]
|
||||||
sample_rate: u32,
|
sample_rate: u32,
|
||||||
|
@ -64,6 +77,10 @@ pub struct Args {
|
||||||
#[arg(long, default_value_t = false)]
|
#[arg(long, default_value_t = false)]
|
||||||
no_reference: bool,
|
no_reference: bool,
|
||||||
|
|
||||||
|
/// Hide UI and only draw waveforms
|
||||||
|
#[arg(long, default_value_t = false)]
|
||||||
|
no_ui: bool,
|
||||||
|
|
||||||
/// Don't use braille dots for drawing lines
|
/// Don't use braille dots for drawing lines
|
||||||
#[arg(long, default_value_t = false)]
|
#[arg(long, default_value_t = false)]
|
||||||
no_braille: bool,
|
no_braille: bool,
|
||||||
|
@ -75,7 +92,7 @@ fn main() -> Result<(), std::io::Error> {
|
||||||
if let Some(txt) = &args.tune { // TODO make it less jank
|
if let Some(txt) = &args.tune { // TODO make it less jank
|
||||||
if let Ok(note) = txt.parse::<Note>() {
|
if let Ok(note) = txt.parse::<Note>() {
|
||||||
args.buffer = note.tune_buffer_size(args.sample_rate);
|
args.buffer = note.tune_buffer_size(args.sample_rate);
|
||||||
while args.buffer % 4 != 0 {
|
while args.buffer % (args.channels as u32 * 2) != 0 { // TODO customizable bit depth
|
||||||
args.buffer += 1; // TODO jank but otherwise it doesn't align
|
args.buffer += 1; // TODO jank but otherwise it doesn't align
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
|
|
@ -8,14 +8,15 @@
|
||||||
// }
|
// }
|
||||||
|
|
||||||
pub trait SampleParser {
|
pub trait SampleParser {
|
||||||
fn oscilloscope(&self, data: &mut [u8], channels: u32) -> Vec<Vec<f64>>;
|
fn oscilloscope(&self, data: &mut [u8], channels: u8) -> Vec<Vec<f64>>;
|
||||||
|
fn sample_size(&self) -> usize;
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct Signed16PCM {}
|
pub struct Signed16PCM {}
|
||||||
|
|
||||||
/// TODO these are kinda inefficient, can they be faster?
|
/// TODO these are kinda inefficient, can they be faster?
|
||||||
impl SampleParser for Signed16PCM {
|
impl SampleParser for Signed16PCM {
|
||||||
fn oscilloscope(&self, data: &mut [u8], channels: u32) -> Vec<Vec<f64>> {
|
fn oscilloscope(&self, data: &mut [u8], channels: u8) -> Vec<Vec<f64>> {
|
||||||
let mut out = vec![vec![]; channels as usize];
|
let mut out = vec![vec![]; channels as usize];
|
||||||
let mut channel = 0;
|
let mut channel = 0;
|
||||||
for chunk in data.chunks(2) {
|
for chunk in data.chunks(2) {
|
||||||
|
@ -25,4 +26,8 @@ impl SampleParser for Signed16PCM {
|
||||||
}
|
}
|
||||||
out
|
out
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn sample_size(&self) -> usize {
|
||||||
|
return 2; // 16 bit, thus 2 bytes
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue