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:
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"
|
||||
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
|
||||
|
|
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!!!
|
||||
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
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
|
||||
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;
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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),
|
||||
]
|
||||
}
|
||||
|
||||
|
|
|
@ -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)),
|
||||
)?;
|
||||
|
|
|
@ -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],
|
||||
}
|
||||
))
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}))
|
||||
}
|
||||
}
|
||||
|
|
148
src/main.rs
148
src/main.rs
|
@ -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()?;
|
||||
|
|
Loading…
Reference in a new issue