1
0
Fork 0
mirror of https://github.com/alemidev/scope-tui.git synced 2024-11-14 10:49:20 +01:00

chore: refactored a little

This commit is contained in:
əlemi 2024-03-18 06:12:34 +01:00
parent 155a5df67d
commit 9d3e73f640
Signed by: alemi
GPG key ID: A4895B84D311642C
11 changed files with 183 additions and 181 deletions

View file

@ -16,7 +16,8 @@ clap = { version = "4.0.32", features = ["derive"] }
derive_more = "0.99.17"
thiserror = "1.0.48"
rustfft = "6.1.0"
cpal = "0.15.3"
# cross platform audio library backend
cpal = { version = "0.15.3", optional = true }
# for TUI backend
ratatui = { version = "0.26", features = ["all-widgets"], 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 }
[features]
default = ["tui"]
default = ["tui", "file", "cpal", "pulseaudio"]
file = []
tui = ["dep:ratatui", "dep:crossterm"]
cpal = ["dep:cpal"]
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

View file

@ -26,40 +26,38 @@ pub struct App {
}
// TODO another way to build this that doesn't require getting cli args directly!!!
impl From::<&crate::ScopeArgs> for App {
fn from(args: &crate::ScopeArgs) -> Self {
impl App {
pub fn new(ui: &crate::cfg::UiOptions, source: &crate::cfg::SourceOptions) -> Self {
let graph = GraphConfig {
axis_color: Color::DarkGray,
labels_color: Color::Cyan,
palette: vec![Color::Red, Color::Yellow, Color::Green, Color::Magenta],
scale: args.scale as f64,
width: args.buffer, // TODO also make bit depth customizable
samples: args.buffer,
sampling_rate: args.sample_rate,
references: !args.no_reference,
show_ui: !args.no_ui,
scatter: args.scatter,
scale: ui.scale as f64,
width: source.buffer, // TODO also make bit depth customizable
samples: source.buffer,
sampling_rate: source.sample_rate,
references: !ui.no_reference,
show_ui: !ui.no_ui,
scatter: ui.scatter,
pause: false,
marker_type: if args.no_braille {
marker_type: if ui.no_braille {
Marker::Dot
} else {
Marker::Braille
},
};
let oscilloscope = Oscilloscope::from_args(args);
let vectorscope = Vectorscope::from_args(args);
let spectroscope = Spectroscope::from_args(args);
let oscilloscope = Oscilloscope::from_args(source);
let vectorscope = Vectorscope::from_args(source);
let spectroscope = Spectroscope::from_args(source);
App {
graph, oscilloscope, vectorscope, spectroscope,
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> {
let mut fps = 0;
let mut framerate = 0;
@ -147,8 +145,8 @@ impl App {
_ => 1.0,
};
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::Down => 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..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::Left => update_value_i(&mut self.graph.samples, false, 25, magnitude, 0..self.graph.width*2),
KeyCode::Char('q') => quit = true,

119
src/cfg.rs Normal file
View 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);
}
}
}
}

View file

@ -36,7 +36,7 @@ impl GraphConfig {
#[allow(clippy::ptr_arg)] // TODO temporarily! it's a shitty solution
pub trait DisplayMode {
// 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 process(&mut self, cfg: &GraphConfig, data: &Matrix<f64>) -> Vec<DataSet>;
fn mode_str(&self) -> &'static str;

View file

@ -15,7 +15,7 @@ pub struct Oscilloscope {
}
impl DisplayMode for Oscilloscope {
fn from_args(_args: &crate::ScopeArgs) -> Self {
fn from_args(_opts: &crate::cfg::SourceOptions) -> Self {
Oscilloscope::default()
}

View file

@ -38,10 +38,10 @@ pub fn hann_window(samples: &[f64]) -> Vec<f64> {
}
impl DisplayMode for Spectroscope {
fn from_args(args: &crate::ScopeArgs) -> Self {
fn from_args(opts: &crate::cfg::SourceOptions) -> Self {
Spectroscope {
sampling_rate: args.sample_rate,
buffer_size: args.buffer / (2 * args.channels as u32),
sampling_rate: opts.sample_rate,
buffer_size: opts.buffer,
average: 1, buf: Vec::new(),
window: false,
log_y: true,

View file

@ -8,7 +8,7 @@ use super::{DisplayMode, GraphConfig, DataSet, Dimension};
pub struct Vectorscope {}
impl DisplayMode for Vectorscope {
fn from_args(_args: &crate::ScopeArgs) -> Self {
fn from_args(_opts: &crate::cfg::SourceOptions) -> Self {
Vectorscope::default()
}
@ -26,8 +26,8 @@ impl DisplayMode for Vectorscope {
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]),
Dimension::X => ("left -", [-cfg.scale, cfg.scale]),
Dimension::Y => ("| right", [-cfg.scale, cfg.scale]),
};
let mut a = Axis::default();
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> {
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![(0.0, -(cfg.scale as f64)), (0.0, cfg.scale as f64)], 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), (0.0, cfg.scale)], cfg.marker_type, GraphType::Line, cfg.axis_color),
]
}

View file

@ -25,7 +25,7 @@ pub enum AudioDeviceErrors {
}
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 device = match device {
Some(name) => host
@ -37,14 +37,15 @@ impl DefaultAudioDeviceWithCPAL {
.ok_or(AudioDeviceErrors::NotFound)?,
};
let cfg = cpal::StreamConfig {
channels: channels as u16,
buffer_size: cpal::BufferSize::Fixed(buffer * channels * 2),
sample_rate: cpal::SampleRate(sample_rate),
channels: opts.channels as u16,
buffer_size: cpal::BufferSize::Fixed(opts.buffer * opts.channels as u32 * 2),
sample_rate: cpal::SampleRate(opts.sample_rate),
};
let (tx, rx) = mpsc::channel();
let channels = opts.channels;
let stream = device.build_input_stream(
&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}"),
Some(std::time::Duration::from_secs(timeout_secs)),
)?;

View file

@ -15,12 +15,14 @@ pub struct FileSource {
impl FileSource {
#[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(
FileSource {
channels, sample_rate, limit_rate,
channels: opts.channels,
sample_rate: opts.sample_rate as usize,
limit_rate,
file: File::open(path)?,
buffer: vec![0u8; buffer * channels],
buffer: vec![0u8; opts.buffer as usize * opts.channels],
}
))
}

View file

@ -11,19 +11,18 @@ pub struct PulseAudioSimpleDataSource {
impl PulseAudioSimpleDataSource {
#[allow(clippy::new_ret_no_self)]
pub fn new(
device: Option<&str>, channels: u8, rate: u32, buffer: u32, server_buffer: u32
) -> Result<Box<dyn super::DataSource<f64>>, PAErr> {
pub fn new(device: Option<&str>, opts: &crate::cfg::SourceOptions, server_buffer: u32) -> Result<Box<dyn super::DataSource<f64>>, PAErr> {
let spec = Spec {
format: Format::S16NE, // TODO allow more formats?
channels, rate,
channels: opts.channels as u8,
rate: opts.sample_rate,
};
if !spec.is_valid() {
return Err(PAErr(0)); // TODO what error number should we throw?
}
let attrs = BufferAttr {
maxlength: server_buffer * buffer * channels as u32 * 2,
fragsize: buffer,
maxlength: server_buffer * opts.buffer * opts.channels as u32 * 2,
fragsize: opts.buffer,
..Default::default()
};
let simple = Simple::new(
@ -38,8 +37,8 @@ impl PulseAudioSimpleDataSource {
)?;
Ok(Box::new(Self {
simple,
buffer: vec![0; buffer as usize * channels as usize * 2],
channels: channels as usize
buffer: vec![0; opts.buffer as usize * opts.channels * 2],
channels: opts.channels
}))
}
}

View file

@ -1,160 +1,40 @@
mod app;
mod cfg;
mod music;
mod input;
mod display;
use app::App;
use ratatui::{
backend::CrosstermBackend,
Terminal,
};
use crossterm::{
execute, terminal::{
use cfg::{ScopeArgs, ScopeSource};
use clap::Parser;
use ratatui::{backend::CrosstermBackend, Terminal};
use crossterm::{execute, terminal::{
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>> {
let mut args = ScopeArgs::parse();
args.opts.tune();
if let Some(txt) = &args.tune { // TODO make it less jank
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 {
let source = match args.source {
#[cfg(feature = "pulseaudio")]
ScopeSource::Pulse { device, server_buffer } => {
input::pulse::PulseAudioSimpleDataSource::new(
device.as_deref(),
args.channels as u8,
args.sample_rate,
args.buffer,
*server_buffer,
)?
input::pulse::PulseAudioSimpleDataSource::new(device.as_deref(), &args.opts, server_buffer)?
},
#[cfg(feature = "file")]
ScopeSource::File { path, limit_rate } => {
input::file::FileSource::new(
path,
args.channels,
args.sample_rate as usize,
args.buffer as usize,
*limit_rate
)?
input::file::FileSource::new(&path, &args.opts, limit_rate)?
},
#[cfg(feature = "cpal")]
ScopeSource::Audio { device, timeout } => {
input::cpal::DefaultAudioDeviceWithCPAL::new(
device.as_deref(),
args.channels as u32,
args.sample_rate,
args.buffer,
*timeout,
)?
input::cpal::DefaultAudioDeviceWithCPAL::new(device.as_deref(), &args.opts, timeout)?
}
};
let mut app = App::from(&args);
let mut app = App::new(&args.ui, &args.opts);
// setup terminal
enable_raw_mode()?;