mirror of
https://github.com/alemidev/scope-tui.git
synced 2024-11-23 14:14:48 +01:00
fix: updated readme, catch errors
This commit is contained in:
parent
e2269ef6d6
commit
00f27b7f83
5 changed files with 84 additions and 57 deletions
|
@ -3,9 +3,7 @@ name = "scope-tui"
|
||||||
version = "0.1.1"
|
version = "0.1.1"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
authors = [ "alemi <me@alemi.dev>" ]
|
authors = [ "alemi <me@alemi.dev>" ]
|
||||||
description = """
|
description = "A simple audio visualization tool for the terminal built with tui-rs and libpulse-simple-binding, inspired from cava"
|
||||||
A simple audio visualization tool for the terminal built with tui-rs and libpulse-simple-binding, inspired from cava
|
|
||||||
"""
|
|
||||||
keywords = ["tui", "terminal", "audio", "visualization", "scope", "dashboard"]
|
keywords = ["tui", "terminal", "audio", "visualization", "scope", "dashboard"]
|
||||||
repository = "https://github.com/alemidev/scope-tui"
|
repository = "https://github.com/alemidev/scope-tui"
|
||||||
readme = "README.md"
|
readme = "README.md"
|
||||||
|
|
15
README.md
15
README.md
|
@ -13,17 +13,17 @@ the first version of `scope-tui` was developed, with very minimal settings given
|
||||||
|
|
||||||
# Usage
|
# Usage
|
||||||
```
|
```
|
||||||
$ scope-tui [OPTIONS] <WIDTH>
|
$ scope-tui [OPTIONS] [DEVICE]
|
||||||
|
|
||||||
Arguments:
|
Arguments:
|
||||||
<WIDTH> Size of audio buffer, and width of scope
|
[DEVICE] Audio device to attach to
|
||||||
|
|
||||||
Options:
|
Options:
|
||||||
-d, --device <DEVICE> Audio device to attach to
|
-b, --buffer <BUFFER> Size of audio buffer, and width of scope [default: 8192]
|
||||||
-s, --scale <SCALE> Max value on Amplitude scale [default: 20000]
|
-r, --range <RANGE> Max value, positive and negative, on amplitude scale [default: 20000]
|
||||||
--no-reference Don't draw reference line
|
--no-reference Don't draw reference line
|
||||||
--no-braille Don't use braille dots for drawing lines
|
--no-braille Don't use braille dots for drawing lines
|
||||||
--scatter Use vintage looking scatter mode
|
--scatter Use vintage looking scatter mode instead of line mode
|
||||||
--vectorscope Combine left and right channels into vectorscope view
|
--vectorscope Combine left and right channels into vectorscope view
|
||||||
-h, --help Print help information
|
-h, --help Print help information
|
||||||
-V, --version Print version information
|
-V, --version Print version information
|
||||||
|
@ -32,6 +32,9 @@ Options:
|
||||||
The audio buffer size directly impacts resource usage, latency and refresh rate and its limits are given by the audio refresh rate. Larger buffers are slower but less resource intensive. A good starting value might be `8192`
|
The audio buffer size directly impacts resource usage, latency and refresh rate and its limits are given by the audio refresh rate. Larger buffers are slower but less resource intensive. A good starting value might be `8192`
|
||||||
|
|
||||||
## Controls
|
## Controls
|
||||||
* Use `q` or `CTRL+C` to exit. Not all keypresses are caught, so keep trying... (wip!)
|
* Use `q` or `CTRL+C` to exit
|
||||||
* Use `<SPACE>` to pause and resume display
|
* Use `<SPACE>` to pause and resume display
|
||||||
|
* Use `-` and `=` to decrease or increase range (`_` and `+` for smaller steps)
|
||||||
|
* Use `v` to toggle vectorscope mode
|
||||||
|
* Use `s` to toggle scatter mode
|
||||||
* Decrease/increase terminal font size to increase/decrease scope resolution.
|
* Decrease/increase terminal font size to increase/decrease scope resolution.
|
||||||
|
|
10
src/app.rs
10
src/app.rs
|
@ -101,9 +101,11 @@ impl AppConfig {
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn update_scale(&mut self, increment: i32) {
|
pub fn update_scale(&mut self, increment: i32) {
|
||||||
|
if increment > 0 || increment.abs() < self.scale as i32 {
|
||||||
self.scale = ((self.scale as i32) + increment) as u32;
|
self.scale = ((self.scale as i32) + increment) as u32;
|
||||||
self.update_values();
|
self.update_values();
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub fn set_scatter(&mut self, scatter: bool) {
|
pub fn set_scatter(&mut self, scatter: bool) {
|
||||||
self.graph_type = if scatter { GraphType::Scatter } else { GraphType::Line };
|
self.graph_type = if scatter { GraphType::Scatter } else { GraphType::Line };
|
||||||
|
@ -111,8 +113,8 @@ impl AppConfig {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
impl From::<crate::Args> for AppConfig {
|
impl From::<&crate::Args> for AppConfig {
|
||||||
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 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 graph_type = if args.scatter { GraphType::Scatter } else { GraphType::Line };
|
||||||
|
|
||||||
|
@ -121,8 +123,8 @@ impl From::<crate::Args> for AppConfig {
|
||||||
primary_color: Color::Red,
|
primary_color: Color::Red,
|
||||||
secondary_color: Color::Yellow,
|
secondary_color: Color::Yellow,
|
||||||
axis_color: Color::DarkGray,
|
axis_color: Color::DarkGray,
|
||||||
scale: args.scale,
|
scale: args.range,
|
||||||
width: args.width / 4, // TODO It's 4 because 2 channels and 2 bytes per sample!
|
width: args.buffer / 4, // TODO It's 4 because 2 channels and 2 bytes per sample!
|
||||||
vectorscope: args.vectorscope,
|
vectorscope: args.vectorscope,
|
||||||
references: !args.no_reference,
|
references: !args.no_reference,
|
||||||
bounds: ChartBounds::default(),
|
bounds: ChartBounds::default(),
|
||||||
|
|
107
src/main.rs
107
src/main.rs
|
@ -1,9 +1,9 @@
|
||||||
mod parser;
|
mod parser;
|
||||||
mod app;
|
mod app;
|
||||||
|
|
||||||
use std::{io, time::Duration};
|
use std::{io::{self, ErrorKind}, time::Duration};
|
||||||
use tui::{
|
use tui::{
|
||||||
backend::CrosstermBackend,
|
backend::{CrosstermBackend, Backend},
|
||||||
widgets::{Block, Chart, Axis, Dataset, GraphType},
|
widgets::{Block, Chart, Axis, Dataset, GraphType},
|
||||||
// layout::{Layout, Constraint, Direction},
|
// layout::{Layout, Constraint, Direction},
|
||||||
Terminal, text::Span, style::{Style, Color, Modifier}, symbols
|
Terminal, text::Span, style::{Style, Color, Modifier}, symbols
|
||||||
|
@ -32,11 +32,11 @@ struct Args {
|
||||||
|
|
||||||
/// Size of audio buffer, and width of scope
|
/// Size of audio buffer, and width of scope
|
||||||
#[arg(short, long, default_value_t = 8192)]
|
#[arg(short, long, default_value_t = 8192)]
|
||||||
width: u32,
|
buffer: u32,
|
||||||
|
|
||||||
/// Max value on Amplitude scale
|
/// Max value, positive and negative, on amplitude scale
|
||||||
#[arg(short, long, default_value_t = 20000)]
|
#[arg(short, long, default_value_t = 20000)]
|
||||||
scale: u32,
|
range: u32,
|
||||||
|
|
||||||
/// Don't draw reference line
|
/// Don't draw reference line
|
||||||
#[arg(long, default_value_t = false)]
|
#[arg(long, default_value_t = false)]
|
||||||
|
@ -46,7 +46,7 @@ struct Args {
|
||||||
#[arg(long, default_value_t = false)]
|
#[arg(long, default_value_t = false)]
|
||||||
no_braille: bool,
|
no_braille: bool,
|
||||||
|
|
||||||
/// Use vintage looking scatter mode
|
/// Use vintage looking scatter mode instead of line mode
|
||||||
#[arg(long, default_value_t = false)]
|
#[arg(long, default_value_t = false)]
|
||||||
scatter: bool,
|
scatter: bool,
|
||||||
|
|
||||||
|
@ -78,10 +78,45 @@ fn data_set<'a>(
|
||||||
.data(&data)
|
.data(&data)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
fn main() -> Result<(), io::Error> {
|
fn main() -> Result<(), io::Error> {
|
||||||
let args = Args::parse();
|
let args = Args::parse();
|
||||||
|
|
||||||
|
// setup terminal
|
||||||
|
enable_raw_mode()?;
|
||||||
|
let mut stdout = io::stdout();
|
||||||
|
execute!(stdout, EnterAlternateScreen)?;
|
||||||
|
let backend = CrosstermBackend::new(stdout);
|
||||||
|
let mut terminal = Terminal::new(backend)?;
|
||||||
|
terminal.hide_cursor()?;
|
||||||
|
|
||||||
|
match run_app(args, &mut terminal) {
|
||||||
|
Ok(()) => {},
|
||||||
|
Err(e) => {
|
||||||
|
println!("[!] Error executing app: {:?}", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// restore terminal
|
||||||
|
disable_raw_mode()?;
|
||||||
|
execute!(
|
||||||
|
terminal.backend_mut(),
|
||||||
|
LeaveAlternateScreen,
|
||||||
|
DisableMouseCapture
|
||||||
|
)?;
|
||||||
|
terminal.show_cursor()?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
fn run_app<T : Backend>(args: Args, terminal: &mut Terminal<T>) -> Result<(), io::Error> {
|
||||||
|
// prepare globals
|
||||||
|
let mut buffer : Vec<u8> = vec![0; args.buffer as usize];
|
||||||
|
let mut cfg = AppConfig::from(&args);
|
||||||
|
let fmt = Signed16PCM{}; // TODO some way to choose this?
|
||||||
|
|
||||||
|
let mut pause = false;
|
||||||
|
|
||||||
// setup audio capture
|
// setup audio capture
|
||||||
let spec = Spec {
|
let spec = Spec {
|
||||||
format: Format::S16NE,
|
format: Format::S16NE,
|
||||||
|
@ -95,8 +130,7 @@ fn main() -> Result<(), io::Error> {
|
||||||
None => None,
|
None => None,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
let s = match Simple::new(
|
||||||
let s = Simple::new(
|
|
||||||
None, // Use the default server
|
None, // Use the default server
|
||||||
"ScopeTUI", // Our application’s name
|
"ScopeTUI", // Our application’s name
|
||||||
Direction::Record, // We want a record stream
|
Direction::Record, // We want a record stream
|
||||||
|
@ -105,29 +139,27 @@ fn main() -> Result<(), io::Error> {
|
||||||
&spec, // Our sample format
|
&spec, // Our sample format
|
||||||
None, // Use default channel map
|
None, // Use default channel map
|
||||||
Some(&BufferAttr {
|
Some(&BufferAttr {
|
||||||
maxlength: 32 * args.width,
|
maxlength: 32 * args.buffer,
|
||||||
fragsize: args.width,
|
fragsize: args.buffer,
|
||||||
..Default::default()
|
..Default::default()
|
||||||
}),
|
}),
|
||||||
).unwrap();
|
) {
|
||||||
|
Ok(s) => s,
|
||||||
|
Err(e) => {
|
||||||
|
println!("[!] Could not connect to pulseaudio : {:?}", e);
|
||||||
|
return Err(io::Error::new(ErrorKind::Other, "could not connect to pulseaudio"));
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
// setup terminal
|
|
||||||
enable_raw_mode()?;
|
|
||||||
let mut stdout = io::stdout();
|
|
||||||
execute!(stdout, EnterAlternateScreen)?;
|
|
||||||
let backend = CrosstermBackend::new(stdout);
|
|
||||||
let mut terminal = Terminal::new(backend)?;
|
|
||||||
terminal.hide_cursor().unwrap();
|
|
||||||
|
|
||||||
// prepare globals
|
|
||||||
let mut buffer : Vec<u8> = vec![0; args.width as usize];
|
|
||||||
let mut cfg = AppConfig::from(args);
|
|
||||||
let fmt = Signed16PCM{}; // TODO some way to choose this?
|
|
||||||
|
|
||||||
let mut pause = false;
|
|
||||||
|
|
||||||
loop {
|
loop {
|
||||||
s.read(&mut buffer).unwrap();
|
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 {
|
if !pause {
|
||||||
let mut datasets = vec![];
|
let mut datasets = vec![];
|
||||||
|
@ -138,7 +170,7 @@ fn main() -> Result<(), io::Error> {
|
||||||
let mut ref_data_y = Vec::new();
|
let mut ref_data_y = Vec::new();
|
||||||
|
|
||||||
if cfg.references {
|
if cfg.references {
|
||||||
|
// TODO find a proper way to put these references...
|
||||||
if cfg.vectorscope() {
|
if cfg.vectorscope() {
|
||||||
for x in -(cfg.scale() as i64)..(cfg.scale() as i64) {
|
for x in -(cfg.scale() as i64)..(cfg.scale() as i64) {
|
||||||
ref_data_x.push((x as f64, 0 as f64));
|
ref_data_x.push((x as f64, 0 as f64));
|
||||||
|
@ -182,7 +214,7 @@ fn main() -> Result<(), io::Error> {
|
||||||
.x_axis(Axis::default()
|
.x_axis(Axis::default()
|
||||||
.title(Span::styled(cfg.name(app::Axis::X), Style::default().fg(Color::Cyan)))
|
.title(Span::styled(cfg.name(app::Axis::X), Style::default().fg(Color::Cyan)))
|
||||||
.style(Style::default().fg(cfg.axis_color))
|
.style(Style::default().fg(cfg.axis_color))
|
||||||
.bounds(cfg.bounds(app::Axis::X)))
|
.bounds(cfg.bounds(app::Axis::X))) // TODO allow to have axis sometimes?
|
||||||
.y_axis(Axis::default()
|
.y_axis(Axis::default()
|
||||||
.title(Span::styled(cfg.name(app::Axis::Y), Style::default().fg(Color::Cyan)))
|
.title(Span::styled(cfg.name(app::Axis::Y), Style::default().fg(Color::Cyan)))
|
||||||
.style(Style::default().fg(cfg.axis_color))
|
.style(Style::default().fg(cfg.axis_color))
|
||||||
|
@ -196,8 +228,6 @@ fn main() -> Result<(), io::Error> {
|
||||||
KeyModifiers::CONTROL => {
|
KeyModifiers::CONTROL => {
|
||||||
match key.code {
|
match key.code {
|
||||||
KeyCode::Char('c') => break,
|
KeyCode::Char('c') => break,
|
||||||
KeyCode::Char('+') => cfg.update_scale(100),
|
|
||||||
KeyCode::Char('-') => cfg.update_scale(-100),
|
|
||||||
_ => {},
|
_ => {},
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
@ -205,8 +235,10 @@ fn main() -> Result<(), io::Error> {
|
||||||
match key.code {
|
match key.code {
|
||||||
KeyCode::Char('q') => break,
|
KeyCode::Char('q') => break,
|
||||||
KeyCode::Char(' ') => pause = !pause,
|
KeyCode::Char(' ') => pause = !pause,
|
||||||
KeyCode::Char('+') => cfg.update_scale(1000),
|
KeyCode::Char('=') => cfg.update_scale(-1000),
|
||||||
KeyCode::Char('-') => cfg.update_scale(-1000),
|
KeyCode::Char('-') => cfg.update_scale(1000),
|
||||||
|
KeyCode::Char('+') => cfg.update_scale(-100),
|
||||||
|
KeyCode::Char('_') => cfg.update_scale(100),
|
||||||
KeyCode::Char('v') => cfg.set_vectorscope(!cfg.vectorscope()),
|
KeyCode::Char('v') => cfg.set_vectorscope(!cfg.vectorscope()),
|
||||||
KeyCode::Char('s') => cfg.set_scatter(!cfg.scatter()),
|
KeyCode::Char('s') => cfg.set_scatter(!cfg.scatter()),
|
||||||
_ => {},
|
_ => {},
|
||||||
|
@ -216,14 +248,5 @@ fn main() -> Result<(), io::Error> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// restore terminal
|
|
||||||
disable_raw_mode()?;
|
|
||||||
execute!(
|
|
||||||
terminal.backend_mut(),
|
|
||||||
LeaveAlternateScreen,
|
|
||||||
DisableMouseCapture
|
|
||||||
)?;
|
|
||||||
terminal.show_cursor()?;
|
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
|
@ -14,6 +14,7 @@ pub trait SampleParser {
|
||||||
|
|
||||||
pub struct Signed16PCM {}
|
pub struct Signed16PCM {}
|
||||||
|
|
||||||
|
/// TODO these are kinda inefficient, can they be faster?
|
||||||
impl SampleParser for Signed16PCM {
|
impl SampleParser for Signed16PCM {
|
||||||
fn oscilloscope(&self, data: &mut [u8]) -> (Vec<(f64, f64)>, Vec<(f64, f64)>) {
|
fn oscilloscope(&self, data: &mut [u8]) -> (Vec<(f64, f64)>, Vec<(f64, f64)>) {
|
||||||
let mut left = Vec::new(); // TODO does left really come first?
|
let mut left = Vec::new(); // TODO does left really come first?
|
||||||
|
|
Loading…
Reference in a new issue