Skip to main content

scope/
cfg.rs

1use clap::{Parser, Subcommand};
2use ratatui::style::Color;
3
4use crate::music::Note;
5
6// TODO is this still necessary?
7const HELP_TEMPLATE: &str = "{before-help}\
8{name} {version} -- by {author}
9{about}
10
11{usage-heading} {usage}
12
13{all-args}{after-help}
14";
15
16/// a simple oscilloscope/vectorscope for your terminal
17#[derive(Parser, Debug)]
18#[command(author, version, about, long_about = None, help_template = HELP_TEMPLATE)]
19pub struct ScopeArgs {
20	#[clap(subcommand)]
21	pub source: ScopeSource,
22
23	#[command(flatten)]
24	pub opts: SourceOptions,
25
26	#[command(flatten)]
27	pub ui: UiOptions,
28}
29
30#[derive(Debug, Clone, Parser)]
31pub struct UiOptions {
32	/// floating point vertical scale, from 0 to 1
33	#[arg(short, long, value_name = "x", default_value_t = 1.0)]
34	pub scale: f32,
35
36	/// use vintage looking scatter mode instead of line mode
37	#[arg(long, default_value_t = false)]
38	pub scatter: bool,
39
40	/// don't draw reference line
41	#[arg(long, default_value_t = false)]
42	pub no_reference: bool,
43
44	/// hide UI and only draw waveforms
45	#[arg(long, default_value_t = false)]
46	pub no_ui: bool,
47
48	/// don't use braille dots for drawing lines
49	#[arg(long, default_value_t = false)]
50	pub no_braille: bool,
51
52	/// palette to use for scope signal lines
53	#[arg(long, value_name = "color1,color2", value_delimiter = ',', default_value = "red,yellow,cyan,magenta")]
54	pub palette_color: Vec<Color>,
55
56	/// color to use for axis labels
57	#[arg(long, value_name = "color", default_value_t = Color::Cyan)]
58	pub labels_color: Color,
59
60	/// color to use for axis lines
61	#[arg(long, value_name = "color", default_value_t = Color::DarkGray)]
62	pub axis_color: Color,
63}
64
65#[derive(Debug, Clone, Subcommand)]
66pub enum ScopeSource {
67	#[cfg(feature = "pulseaudio")]
68	/// use PulseAudio Simple api to read data from an audio sink
69	Pulse {
70		/// source device to attach to
71		device: Option<String>,
72
73		/// PulseAudio server buffer size, in block number
74		#[arg(long, value_name = "N", default_value_t = 32)]
75		server_buffer: u32,
76	},
77
78	#[cfg(feature = "file")]
79	/// use a file from filesystem and read its content
80	File {
81		/// path on filesystem of file or pipe
82		path: String,
83
84		/// limit data flow to match requested sample rate
85		#[arg(short, long, default_value_t = false)]
86		limit_rate: bool,
87	},
88
89	#[cfg(feature = "cpal")]
90	/// use new experimental CPAL backend
91	Audio {
92		/// source device to attach to
93		device: Option<String>,
94
95		/// timeout (in seconds) waiting for audio stream
96		#[arg(long, default_value_t = 5)]
97		timeout: u64,
98
99		/// just list available devices and quit
100		#[arg(long, default_value_t = false)]
101		list: bool,
102	},
103}
104
105#[derive(Debug, Clone, Parser)]
106pub struct SourceOptions {
107	/// number of channels to open
108	#[arg(short, long, value_name = "N", default_value_t = 2)]
109	pub channels: usize,
110
111	/// size of audio buffer, and width of scope
112	#[arg(short, long, value_name = "SIZE", default_value_t = 2048)]
113	pub buffer: u32,
114
115	/// sample rate to use
116	#[arg(short = 'r', long, value_name = "HZ", default_value_t = 48000)]
117	pub sample_rate: u32,
118
119	/// tune buffer size to be in tune with given note (overrides buffer option)
120	#[arg(short, long, value_name = "NOTE")]
121	pub tune: Option<String>,
122}
123
124// TODO its convenient to keep this here but it's not really the best place...
125impl SourceOptions {
126	pub fn tune(&mut self) {
127		if let Some(txt) = &self.tune {
128			// TODO make it less jank
129			if let Ok(note) = txt.parse::<Note>() {
130				self.buffer = note.tune_buffer_size(self.sample_rate);
131				while self.buffer.is_multiple_of(self.channels as u32 * 2) {
132					// TODO customizable bit depth
133					self.buffer += 1; // TODO jank but otherwise it doesn't align
134				}
135			} else {
136				eprintln!("[!] Unrecognized note '{}', ignoring option", txt);
137			}
138		}
139	}
140}