mirror of
https://github.com/alemidev/scope-tui.git
synced 2024-11-23 14:14:48 +01:00
chore: refactored a little
This commit is contained in:
parent
155a5df67d
commit
9d3e73f640
11 changed files with 183 additions and 181 deletions
|
@ -16,7 +16,8 @@ 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"
|
||||||
rustfft = "6.1.0"
|
rustfft = "6.1.0"
|
||||||
cpal = "0.15.3"
|
# cross platform audio library backend
|
||||||
|
cpal = { version = "0.15.3", optional = true }
|
||||||
# for TUI backend
|
# for TUI backend
|
||||||
ratatui = { version = "0.26", features = ["all-widgets"], optional = true }
|
ratatui = { version = "0.26", features = ["all-widgets"], optional = true }
|
||||||
crossterm = { version = "0.27", optional = true }
|
crossterm = { version = "0.27", optional = true }
|
||||||
|
@ -25,8 +26,10 @@ libpulse-binding = { version = "2.0", optional = true }
|
||||||
libpulse-simple-binding = { version = "2.25", optional = true }
|
libpulse-simple-binding = { version = "2.25", optional = true }
|
||||||
|
|
||||||
[features]
|
[features]
|
||||||
default = ["tui"]
|
default = ["tui", "file", "cpal", "pulseaudio"]
|
||||||
|
file = []
|
||||||
tui = ["dep:ratatui", "dep:crossterm"]
|
tui = ["dep:ratatui", "dep:crossterm"]
|
||||||
|
cpal = ["dep:cpal"]
|
||||||
pulseaudio = ["dep:libpulse-binding", "dep:libpulse-simple-binding"]
|
pulseaudio = ["dep:libpulse-binding", "dep:libpulse-simple-binding"]
|
||||||
|
|
||||||
[profile.release] # make small binaries! will take quite longer, from https://github.com/johnthagen/min-sized-rust
|
[profile.release] # make small binaries! will take quite longer, from https://github.com/johnthagen/min-sized-rust
|
||||||
|
|
34
src/app.rs
34
src/app.rs
|
@ -26,40 +26,38 @@ pub struct App {
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO another way to build this that doesn't require getting cli args directly!!!
|
// TODO another way to build this that doesn't require getting cli args directly!!!
|
||||||
impl From::<&crate::ScopeArgs> for App {
|
impl App {
|
||||||
fn from(args: &crate::ScopeArgs) -> Self {
|
pub fn new(ui: &crate::cfg::UiOptions, source: &crate::cfg::SourceOptions) -> Self {
|
||||||
let graph = GraphConfig {
|
let graph = GraphConfig {
|
||||||
axis_color: Color::DarkGray,
|
axis_color: Color::DarkGray,
|
||||||
labels_color: Color::Cyan,
|
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.scale as f64,
|
scale: ui.scale as f64,
|
||||||
width: args.buffer, // TODO also make bit depth customizable
|
width: source.buffer, // TODO also make bit depth customizable
|
||||||
samples: args.buffer,
|
samples: source.buffer,
|
||||||
sampling_rate: args.sample_rate,
|
sampling_rate: source.sample_rate,
|
||||||
references: !args.no_reference,
|
references: !ui.no_reference,
|
||||||
show_ui: !args.no_ui,
|
show_ui: !ui.no_ui,
|
||||||
scatter: args.scatter,
|
scatter: ui.scatter,
|
||||||
pause: false,
|
pause: false,
|
||||||
marker_type: if args.no_braille {
|
marker_type: if ui.no_braille {
|
||||||
Marker::Dot
|
Marker::Dot
|
||||||
} else {
|
} else {
|
||||||
Marker::Braille
|
Marker::Braille
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
let oscilloscope = Oscilloscope::from_args(args);
|
let oscilloscope = Oscilloscope::from_args(source);
|
||||||
let vectorscope = Vectorscope::from_args(args);
|
let vectorscope = Vectorscope::from_args(source);
|
||||||
let spectroscope = Spectroscope::from_args(args);
|
let spectroscope = Spectroscope::from_args(source);
|
||||||
|
|
||||||
App {
|
App {
|
||||||
graph, oscilloscope, vectorscope, spectroscope,
|
graph, oscilloscope, vectorscope, spectroscope,
|
||||||
mode: CurrentDisplayMode::Oscilloscope,
|
mode: CurrentDisplayMode::Oscilloscope,
|
||||||
channels: args.channels as u8,
|
channels: source.channels as u8,
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl App {
|
|
||||||
pub fn run<T : Backend>(&mut self, mut source: Box<dyn DataSource<f64>>, terminal: &mut Terminal<T>) -> Result<(), io::Error> {
|
pub fn run<T : Backend>(&mut self, mut source: Box<dyn DataSource<f64>>, terminal: &mut Terminal<T>) -> Result<(), io::Error> {
|
||||||
let mut fps = 0;
|
let mut fps = 0;
|
||||||
let mut framerate = 0;
|
let mut framerate = 0;
|
||||||
|
@ -147,8 +145,8 @@ impl App {
|
||||||
_ => 1.0,
|
_ => 1.0,
|
||||||
};
|
};
|
||||||
match key.code {
|
match key.code {
|
||||||
KeyCode::Up => update_value_f(&mut self.graph.scale, 0.01, magnitude, 0.0..1.5), // inverted to act as zoom
|
KeyCode::Up => update_value_f(&mut self.graph.scale, 0.01, magnitude, 0.0..10.0), // inverted to act as zoom
|
||||||
KeyCode::Down => update_value_f(&mut self.graph.scale, -0.01, magnitude, 0.0..1.5), // inverted to act as zoom
|
KeyCode::Down => update_value_f(&mut self.graph.scale, -0.01, magnitude, 0.0..10.0), // inverted to act as zoom
|
||||||
KeyCode::Right => update_value_i(&mut self.graph.samples, true, 25, magnitude, 0..self.graph.width*2),
|
KeyCode::Right => update_value_i(&mut self.graph.samples, true, 25, magnitude, 0..self.graph.width*2),
|
||||||
KeyCode::Left => update_value_i(&mut self.graph.samples, false, 25, magnitude, 0..self.graph.width*2),
|
KeyCode::Left => update_value_i(&mut self.graph.samples, false, 25, magnitude, 0..self.graph.width*2),
|
||||||
KeyCode::Char('q') => quit = true,
|
KeyCode::Char('q') => quit = true,
|
||||||
|
|
119
src/cfg.rs
Normal file
119
src/cfg.rs
Normal file
|
@ -0,0 +1,119 @@
|
||||||
|
use clap::{Parser, Subcommand};
|
||||||
|
|
||||||
|
use crate::music::Note;
|
||||||
|
|
||||||
|
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
|
||||||
|
#[derive(Parser, Debug)]
|
||||||
|
#[command(author, version, about, long_about = None, help_template = HELP_TEMPLATE)]
|
||||||
|
pub struct ScopeArgs {
|
||||||
|
#[clap(subcommand)]
|
||||||
|
pub source: ScopeSource,
|
||||||
|
|
||||||
|
#[command(flatten)]
|
||||||
|
pub opts: SourceOptions,
|
||||||
|
|
||||||
|
#[command(flatten)]
|
||||||
|
pub ui: UiOptions,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Parser)]
|
||||||
|
pub struct UiOptions {
|
||||||
|
/// floating point vertical scale, from 0 to 1
|
||||||
|
#[arg(short, long, value_name = "x", default_value_t = 1.0)]
|
||||||
|
pub scale: f32,
|
||||||
|
|
||||||
|
/// use vintage looking scatter mode instead of line mode
|
||||||
|
#[arg(long, default_value_t = false)]
|
||||||
|
pub scatter: bool,
|
||||||
|
|
||||||
|
/// don't draw reference line
|
||||||
|
#[arg(long, default_value_t = false)]
|
||||||
|
pub no_reference: bool,
|
||||||
|
|
||||||
|
/// hide UI and only draw waveforms
|
||||||
|
#[arg(long, default_value_t = false)]
|
||||||
|
pub no_ui: bool,
|
||||||
|
|
||||||
|
/// don't use braille dots for drawing lines
|
||||||
|
#[arg(long, default_value_t = false)]
|
||||||
|
pub no_braille: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Subcommand)]
|
||||||
|
pub enum ScopeSource {
|
||||||
|
|
||||||
|
#[cfg(feature = "pulseaudio")]
|
||||||
|
/// use PulseAudio Simple api to read data from an audio sink
|
||||||
|
Pulse {
|
||||||
|
/// source device to attach to
|
||||||
|
device: Option<String>,
|
||||||
|
|
||||||
|
/// PulseAudio server buffer size, in block number
|
||||||
|
#[arg(long, value_name = "N", default_value_t = 32)]
|
||||||
|
server_buffer: u32,
|
||||||
|
},
|
||||||
|
|
||||||
|
/// use a file from filesystem and read its content
|
||||||
|
File {
|
||||||
|
/// path on filesystem of file or pipe
|
||||||
|
path: String,
|
||||||
|
|
||||||
|
/// limit data flow to match requested sample rate (UNIMPLEMENTED)
|
||||||
|
#[arg(short, long, default_value_t = false)]
|
||||||
|
limit_rate: bool,
|
||||||
|
},
|
||||||
|
|
||||||
|
/// use new experimental CPAL backend
|
||||||
|
Audio {
|
||||||
|
/// source device to attach to
|
||||||
|
device: Option<String>,
|
||||||
|
|
||||||
|
/// timeout (in seconds) waiting for audio stream
|
||||||
|
#[arg(long, default_value_t = 60)]
|
||||||
|
timeout: u64,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Parser)]
|
||||||
|
pub struct SourceOptions {
|
||||||
|
/// number of channels to open
|
||||||
|
#[arg(long, value_name = "N", default_value_t = 2)]
|
||||||
|
pub channels: usize,
|
||||||
|
|
||||||
|
/// size of audio buffer, and width of scope
|
||||||
|
#[arg(short, long, value_name = "SIZE", default_value_t = 2048)]
|
||||||
|
pub buffer: u32,
|
||||||
|
|
||||||
|
/// sample rate to use
|
||||||
|
#[arg(long, value_name = "HZ", default_value_t = 48000)]
|
||||||
|
pub sample_rate: u32,
|
||||||
|
|
||||||
|
/// tune buffer size to be in tune with given note (overrides buffer option)
|
||||||
|
#[arg(long, value_name = "NOTE")]
|
||||||
|
pub tune: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO its convenient to keep this here but it's not really the best place...
|
||||||
|
impl SourceOptions {
|
||||||
|
pub fn tune(&mut self) {
|
||||||
|
if let Some(txt) = &self.tune { // TODO make it less jank
|
||||||
|
if let Ok(note) = txt.parse::<Note>() {
|
||||||
|
self.buffer = note.tune_buffer_size(self.sample_rate);
|
||||||
|
while self.buffer % (self.channels as u32 * 2) != 0 { // TODO customizable bit depth
|
||||||
|
self.buffer += 1; // TODO jank but otherwise it doesn't align
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
eprintln!("[!] Unrecognized note '{}', ignoring option", txt);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -36,7 +36,7 @@ impl GraphConfig {
|
||||||
#[allow(clippy::ptr_arg)] // TODO temporarily! it's a shitty solution
|
#[allow(clippy::ptr_arg)] // TODO temporarily! it's a shitty solution
|
||||||
pub trait DisplayMode {
|
pub trait DisplayMode {
|
||||||
// MUST define
|
// MUST define
|
||||||
fn from_args(args: &crate::ScopeArgs) -> Self where Self : Sized;
|
fn from_args(args: &crate::cfg::SourceOptions) -> Self where Self : Sized;
|
||||||
fn axis(&self, cfg: &GraphConfig, dimension: Dimension) -> Axis; // TODO simplify this
|
fn axis(&self, cfg: &GraphConfig, dimension: Dimension) -> Axis; // TODO simplify this
|
||||||
fn process(&mut self, cfg: &GraphConfig, data: &Matrix<f64>) -> Vec<DataSet>;
|
fn process(&mut self, cfg: &GraphConfig, data: &Matrix<f64>) -> Vec<DataSet>;
|
||||||
fn mode_str(&self) -> &'static str;
|
fn mode_str(&self) -> &'static str;
|
||||||
|
|
|
@ -15,7 +15,7 @@ pub struct Oscilloscope {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl DisplayMode for Oscilloscope {
|
impl DisplayMode for Oscilloscope {
|
||||||
fn from_args(_args: &crate::ScopeArgs) -> Self {
|
fn from_args(_opts: &crate::cfg::SourceOptions) -> Self {
|
||||||
Oscilloscope::default()
|
Oscilloscope::default()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -38,10 +38,10 @@ pub fn hann_window(samples: &[f64]) -> Vec<f64> {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl DisplayMode for Spectroscope {
|
impl DisplayMode for Spectroscope {
|
||||||
fn from_args(args: &crate::ScopeArgs) -> Self {
|
fn from_args(opts: &crate::cfg::SourceOptions) -> Self {
|
||||||
Spectroscope {
|
Spectroscope {
|
||||||
sampling_rate: args.sample_rate,
|
sampling_rate: opts.sample_rate,
|
||||||
buffer_size: args.buffer / (2 * args.channels as u32),
|
buffer_size: opts.buffer,
|
||||||
average: 1, buf: Vec::new(),
|
average: 1, buf: Vec::new(),
|
||||||
window: false,
|
window: false,
|
||||||
log_y: true,
|
log_y: true,
|
||||||
|
|
|
@ -8,7 +8,7 @@ use super::{DisplayMode, GraphConfig, DataSet, Dimension};
|
||||||
pub struct Vectorscope {}
|
pub struct Vectorscope {}
|
||||||
|
|
||||||
impl DisplayMode for Vectorscope {
|
impl DisplayMode for Vectorscope {
|
||||||
fn from_args(_args: &crate::ScopeArgs) -> Self {
|
fn from_args(_opts: &crate::cfg::SourceOptions) -> Self {
|
||||||
Vectorscope::default()
|
Vectorscope::default()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -26,8 +26,8 @@ impl DisplayMode for Vectorscope {
|
||||||
|
|
||||||
fn axis(&self, cfg: &GraphConfig, dimension: Dimension) -> Axis {
|
fn axis(&self, cfg: &GraphConfig, dimension: Dimension) -> Axis {
|
||||||
let (name, bounds) = match dimension {
|
let (name, bounds) = match dimension {
|
||||||
Dimension::X => ("left -", [-(cfg.scale as f64), cfg.scale as f64]),
|
Dimension::X => ("left -", [-cfg.scale, cfg.scale]),
|
||||||
Dimension::Y => ("| right", [-(cfg.scale as f64), cfg.scale as f64]),
|
Dimension::Y => ("| right", [-cfg.scale, cfg.scale]),
|
||||||
};
|
};
|
||||||
let mut a = Axis::default();
|
let mut a = Axis::default();
|
||||||
if cfg.show_ui { // TODO don't make it necessary to check show_ui inside here
|
if cfg.show_ui { // TODO don't make it necessary to check show_ui inside here
|
||||||
|
@ -38,8 +38,8 @@ impl DisplayMode for Vectorscope {
|
||||||
|
|
||||||
fn references(&self, cfg: &GraphConfig) -> Vec<DataSet> {
|
fn references(&self, cfg: &GraphConfig) -> Vec<DataSet> {
|
||||||
vec![
|
vec![
|
||||||
DataSet::new(None, vec![(-(cfg.scale as f64), 0.0), (cfg.scale as f64, 0.0)], cfg.marker_type, GraphType::Line, cfg.axis_color),
|
DataSet::new(None, vec![(-cfg.scale, 0.0), (cfg.scale, 0.0)], cfg.marker_type, GraphType::Line, cfg.axis_color),
|
||||||
DataSet::new(None, vec![(0.0, -(cfg.scale as f64)), (0.0, cfg.scale as f64)], cfg.marker_type, GraphType::Line, cfg.axis_color),
|
DataSet::new(None, vec![(0.0, -cfg.scale), (0.0, cfg.scale)], cfg.marker_type, GraphType::Line, cfg.axis_color),
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -25,7 +25,7 @@ pub enum AudioDeviceErrors {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl DefaultAudioDeviceWithCPAL {
|
impl DefaultAudioDeviceWithCPAL {
|
||||||
pub fn new(device: Option<&str>, channels: u32, sample_rate: u32, buffer: u32, timeout_secs: u64) -> Result<Box<impl super::DataSource<f64>>, AudioDeviceErrors> {
|
pub fn new(device: Option<&str>, opts: &crate::cfg::SourceOptions, timeout_secs: u64) -> Result<Box<impl super::DataSource<f64>>, AudioDeviceErrors> {
|
||||||
let host = cpal::default_host();
|
let host = cpal::default_host();
|
||||||
let device = match device {
|
let device = match device {
|
||||||
Some(name) => host
|
Some(name) => host
|
||||||
|
@ -37,14 +37,15 @@ impl DefaultAudioDeviceWithCPAL {
|
||||||
.ok_or(AudioDeviceErrors::NotFound)?,
|
.ok_or(AudioDeviceErrors::NotFound)?,
|
||||||
};
|
};
|
||||||
let cfg = cpal::StreamConfig {
|
let cfg = cpal::StreamConfig {
|
||||||
channels: channels as u16,
|
channels: opts.channels as u16,
|
||||||
buffer_size: cpal::BufferSize::Fixed(buffer * channels * 2),
|
buffer_size: cpal::BufferSize::Fixed(opts.buffer * opts.channels as u32 * 2),
|
||||||
sample_rate: cpal::SampleRate(sample_rate),
|
sample_rate: cpal::SampleRate(opts.sample_rate),
|
||||||
};
|
};
|
||||||
let (tx, rx) = mpsc::channel();
|
let (tx, rx) = mpsc::channel();
|
||||||
|
let channels = opts.channels;
|
||||||
let stream = device.build_input_stream(
|
let stream = device.build_input_stream(
|
||||||
&cfg,
|
&cfg,
|
||||||
move |data:&[f32], _info| tx.send(stream_to_matrix(data.iter().cloned(), channels as usize, 1.)).unwrap_or(()),
|
move |data:&[f32], _info| tx.send(stream_to_matrix(data.iter().cloned(), channels, 1.)).unwrap_or(()),
|
||||||
|e| eprintln!("error in input stream: {e}"),
|
|e| eprintln!("error in input stream: {e}"),
|
||||||
Some(std::time::Duration::from_secs(timeout_secs)),
|
Some(std::time::Duration::from_secs(timeout_secs)),
|
||||||
)?;
|
)?;
|
||||||
|
|
|
@ -15,12 +15,14 @@ pub struct FileSource {
|
||||||
|
|
||||||
impl FileSource {
|
impl FileSource {
|
||||||
#[allow(clippy::new_ret_no_self)]
|
#[allow(clippy::new_ret_no_self)]
|
||||||
pub fn new(path: &str, channels: usize, sample_rate: usize, buffer: usize, limit_rate: bool) -> Result<Box<dyn super::DataSource<f64>>, std::io::Error> {
|
pub fn new(path: &str, opts: &crate::cfg::SourceOptions, limit_rate: bool) -> Result<Box<dyn super::DataSource<f64>>, std::io::Error> {
|
||||||
Ok(Box::new(
|
Ok(Box::new(
|
||||||
FileSource {
|
FileSource {
|
||||||
channels, sample_rate, limit_rate,
|
channels: opts.channels,
|
||||||
|
sample_rate: opts.sample_rate as usize,
|
||||||
|
limit_rate,
|
||||||
file: File::open(path)?,
|
file: File::open(path)?,
|
||||||
buffer: vec![0u8; buffer * channels],
|
buffer: vec![0u8; opts.buffer as usize * opts.channels],
|
||||||
}
|
}
|
||||||
))
|
))
|
||||||
}
|
}
|
||||||
|
|
|
@ -11,19 +11,18 @@ pub struct PulseAudioSimpleDataSource {
|
||||||
|
|
||||||
impl PulseAudioSimpleDataSource {
|
impl PulseAudioSimpleDataSource {
|
||||||
#[allow(clippy::new_ret_no_self)]
|
#[allow(clippy::new_ret_no_self)]
|
||||||
pub fn new(
|
pub fn new(device: Option<&str>, opts: &crate::cfg::SourceOptions, server_buffer: u32) -> Result<Box<dyn super::DataSource<f64>>, PAErr> {
|
||||||
device: Option<&str>, channels: u8, rate: u32, buffer: u32, server_buffer: u32
|
|
||||||
) -> Result<Box<dyn super::DataSource<f64>>, PAErr> {
|
|
||||||
let spec = Spec {
|
let spec = Spec {
|
||||||
format: Format::S16NE, // TODO allow more formats?
|
format: Format::S16NE, // TODO allow more formats?
|
||||||
channels, rate,
|
channels: opts.channels as u8,
|
||||||
|
rate: opts.sample_rate,
|
||||||
};
|
};
|
||||||
if !spec.is_valid() {
|
if !spec.is_valid() {
|
||||||
return Err(PAErr(0)); // TODO what error number should we throw?
|
return Err(PAErr(0)); // TODO what error number should we throw?
|
||||||
}
|
}
|
||||||
let attrs = BufferAttr {
|
let attrs = BufferAttr {
|
||||||
maxlength: server_buffer * buffer * channels as u32 * 2,
|
maxlength: server_buffer * opts.buffer * opts.channels as u32 * 2,
|
||||||
fragsize: buffer,
|
fragsize: opts.buffer,
|
||||||
..Default::default()
|
..Default::default()
|
||||||
};
|
};
|
||||||
let simple = Simple::new(
|
let simple = Simple::new(
|
||||||
|
@ -38,8 +37,8 @@ impl PulseAudioSimpleDataSource {
|
||||||
)?;
|
)?;
|
||||||
Ok(Box::new(Self {
|
Ok(Box::new(Self {
|
||||||
simple,
|
simple,
|
||||||
buffer: vec![0; buffer as usize * channels as usize * 2],
|
buffer: vec![0; opts.buffer as usize * opts.channels * 2],
|
||||||
channels: channels as usize
|
channels: opts.channels
|
||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
148
src/main.rs
148
src/main.rs
|
@ -1,160 +1,40 @@
|
||||||
mod app;
|
mod app;
|
||||||
|
mod cfg;
|
||||||
mod music;
|
mod music;
|
||||||
mod input;
|
mod input;
|
||||||
mod display;
|
mod display;
|
||||||
|
|
||||||
use app::App;
|
use app::App;
|
||||||
use ratatui::{
|
use cfg::{ScopeArgs, ScopeSource};
|
||||||
backend::CrosstermBackend,
|
use clap::Parser;
|
||||||
Terminal,
|
use ratatui::{backend::CrosstermBackend, Terminal};
|
||||||
};
|
use crossterm::{execute, terminal::{
|
||||||
use crossterm::{
|
|
||||||
execute, terminal::{
|
|
||||||
disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen
|
disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen
|
||||||
},
|
}};
|
||||||
};
|
|
||||||
|
|
||||||
use clap::{Parser, Subcommand};
|
|
||||||
|
|
||||||
use crate::music::Note;
|
|
||||||
|
|
||||||
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
|
|
||||||
#[derive(Parser, Debug)]
|
|
||||||
#[command(author, version, about, long_about = None, help_template = HELP_TEMPLATE)]
|
|
||||||
pub struct ScopeArgs {
|
|
||||||
#[clap(subcommand)]
|
|
||||||
source: ScopeSource,
|
|
||||||
|
|
||||||
/// number of channels to open
|
|
||||||
#[arg(long, value_name = "N", default_value_t = 2)]
|
|
||||||
channels: usize,
|
|
||||||
|
|
||||||
/// tune buffer size to be in tune with given note (overrides buffer option)
|
|
||||||
#[arg(long, value_name = "NOTE")]
|
|
||||||
tune: Option<String>,
|
|
||||||
|
|
||||||
/// size of audio buffer, and width of scope
|
|
||||||
#[arg(short, long, value_name = "SIZE", default_value_t = 2048)]
|
|
||||||
buffer: u32,
|
|
||||||
|
|
||||||
/// sample rate to use
|
|
||||||
#[arg(long, value_name = "HZ", default_value_t = 48000)]
|
|
||||||
sample_rate: u32,
|
|
||||||
|
|
||||||
/// floating point vertical scale, from 0 to 1
|
|
||||||
#[arg(short, long, value_name = "x", default_value_t = 1.0)]
|
|
||||||
scale: f32,
|
|
||||||
|
|
||||||
/// use vintage looking scatter mode instead of line mode
|
|
||||||
#[arg(long, default_value_t = false)]
|
|
||||||
scatter: bool,
|
|
||||||
|
|
||||||
/// don't draw reference line
|
|
||||||
#[arg(long, default_value_t = false)]
|
|
||||||
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
|
|
||||||
#[arg(long, default_value_t = false)]
|
|
||||||
no_braille: bool,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, Subcommand)]
|
|
||||||
pub enum ScopeSource {
|
|
||||||
|
|
||||||
#[cfg(feature = "pulseaudio")]
|
|
||||||
/// use PulseAudio Simple api to read data from an audio sink
|
|
||||||
Pulse {
|
|
||||||
/// source device to attach to
|
|
||||||
device: Option<String>,
|
|
||||||
|
|
||||||
/// PulseAudio server buffer size, in block number
|
|
||||||
#[arg(long, value_name = "N", default_value_t = 32)]
|
|
||||||
server_buffer: u32,
|
|
||||||
},
|
|
||||||
|
|
||||||
/// use a file from filesystem and read its content
|
|
||||||
File {
|
|
||||||
/// path on filesystem of file or pipe
|
|
||||||
path: String,
|
|
||||||
|
|
||||||
/// limit data flow to match requested sample rate (UNIMPLEMENTED)
|
|
||||||
#[arg(short, long, default_value_t = false)]
|
|
||||||
limit_rate: bool,
|
|
||||||
},
|
|
||||||
|
|
||||||
/// use new experimental CPAL backend
|
|
||||||
Audio {
|
|
||||||
/// source device to attach to
|
|
||||||
device: Option<String>,
|
|
||||||
|
|
||||||
/// timeout (in seconds) waiting for audio stream
|
|
||||||
#[arg(long, default_value_t = 60)]
|
|
||||||
timeout: u64,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn main() -> Result<(), Box<dyn std::error::Error>> {
|
fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||||
let mut args = ScopeArgs::parse();
|
let mut args = ScopeArgs::parse();
|
||||||
|
args.opts.tune();
|
||||||
|
|
||||||
if let Some(txt) = &args.tune { // TODO make it less jank
|
let source = match args.source {
|
||||||
if let Ok(note) = txt.parse::<Note>() {
|
|
||||||
args.buffer = note.tune_buffer_size(args.sample_rate);
|
|
||||||
while args.buffer % (args.channels as u32 * 2) != 0 { // TODO customizable bit depth
|
|
||||||
args.buffer += 1; // TODO jank but otherwise it doesn't align
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
eprintln!("[!] Unrecognized note '{}', ignoring option", txt);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let source = match &args.source {
|
|
||||||
|
|
||||||
#[cfg(feature = "pulseaudio")]
|
#[cfg(feature = "pulseaudio")]
|
||||||
ScopeSource::Pulse { device, server_buffer } => {
|
ScopeSource::Pulse { device, server_buffer } => {
|
||||||
input::pulse::PulseAudioSimpleDataSource::new(
|
input::pulse::PulseAudioSimpleDataSource::new(device.as_deref(), &args.opts, server_buffer)?
|
||||||
device.as_deref(),
|
|
||||||
args.channels as u8,
|
|
||||||
args.sample_rate,
|
|
||||||
args.buffer,
|
|
||||||
*server_buffer,
|
|
||||||
)?
|
|
||||||
},
|
},
|
||||||
|
|
||||||
|
#[cfg(feature = "file")]
|
||||||
ScopeSource::File { path, limit_rate } => {
|
ScopeSource::File { path, limit_rate } => {
|
||||||
input::file::FileSource::new(
|
input::file::FileSource::new(&path, &args.opts, limit_rate)?
|
||||||
path,
|
|
||||||
args.channels,
|
|
||||||
args.sample_rate as usize,
|
|
||||||
args.buffer as usize,
|
|
||||||
*limit_rate
|
|
||||||
)?
|
|
||||||
},
|
},
|
||||||
|
|
||||||
|
#[cfg(feature = "cpal")]
|
||||||
ScopeSource::Audio { device, timeout } => {
|
ScopeSource::Audio { device, timeout } => {
|
||||||
input::cpal::DefaultAudioDeviceWithCPAL::new(
|
input::cpal::DefaultAudioDeviceWithCPAL::new(device.as_deref(), &args.opts, timeout)?
|
||||||
device.as_deref(),
|
|
||||||
args.channels as u32,
|
|
||||||
args.sample_rate,
|
|
||||||
args.buffer,
|
|
||||||
*timeout,
|
|
||||||
)?
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
let mut app = App::from(&args);
|
let mut app = App::new(&args.ui, &args.opts);
|
||||||
|
|
||||||
// setup terminal
|
// setup terminal
|
||||||
enable_raw_mode()?;
|
enable_raw_mode()?;
|
||||||
|
|
Loading…
Reference in a new issue