Browse and listen to thousands of radio stations across the globe right from your terminal ๐ ๐ป ๐ตโจ
radio
rust
tokio
web-radio
command-line-tool
tui
1use crossterm::event::{Event, KeyCode, KeyModifiers};
2use ratatui::{
3 style::Style,
4 text::Span,
5 widgets::{Axis, GraphType},
6};
7
8use crate::{
9 app::{update_value_f, update_value_i},
10 input::Matrix,
11};
12
13use super::{DataSet, Dimension, DisplayMode, GraphConfig};
14
15#[derive(Default)]
16pub struct Oscilloscope {
17 pub triggering: bool,
18 pub falling_edge: bool,
19 pub threshold: f64,
20 pub depth: u32,
21 pub peaks: bool,
22}
23
24impl DisplayMode for Oscilloscope {
25 fn from_args(_opts: &crate::cfg::SourceOptions) -> Self {
26 Oscilloscope::default()
27 }
28
29 fn mode_str(&self) -> &'static str {
30 "oscillo"
31 }
32
33 fn channel_name(&self, index: usize) -> String {
34 match index {
35 0 => "L".into(),
36 1 => "R".into(),
37 _ => format!("{}", index),
38 }
39 }
40
41 fn header(&self, _: &GraphConfig) -> String {
42 if self.triggering {
43 format!(
44 "{} {:.0}{} trigger",
45 if self.falling_edge { "v" } else { "^" },
46 self.threshold,
47 if self.depth > 1 {
48 format!(":{}", self.depth)
49 } else {
50 "".into()
51 },
52 )
53 } else {
54 "live".into()
55 }
56 }
57
58 fn axis(&self, cfg: &GraphConfig, dimension: Dimension) -> Axis<'_> {
59 let (name, bounds) = match dimension {
60 Dimension::X => ("time -", [0.0, cfg.samples as f64]),
61 Dimension::Y => ("| amplitude", [-cfg.scale, cfg.scale]),
62 };
63 let mut a = Axis::default();
64 if cfg.show_ui {
65 // TODO don't make it necessary to check show_ui inside here
66 a = a.title(Span::styled(name, Style::default().fg(cfg.labels_color)));
67 }
68 a.style(Style::default().fg(cfg.axis_color)).bounds(bounds)
69 }
70
71 fn references(&self, cfg: &GraphConfig) -> Vec<DataSet> {
72 vec![DataSet::new(
73 None,
74 vec![(0.0, 0.0), (cfg.samples as f64, 0.0)],
75 cfg.marker_type,
76 GraphType::Line,
77 cfg.axis_color,
78 )]
79 }
80
81 fn process(&mut self, cfg: &GraphConfig, data: &Matrix<f64>) -> Vec<DataSet> {
82 let mut out = Vec::new();
83
84 let mut trigger_offset = 0;
85 if self.depth == 0 {
86 self.depth = 1
87 }
88 if self.triggering {
89 for i in 0..data[0].len() {
90 if triggered(&data[0], i, self.threshold, self.depth, self.falling_edge) {
91 // triggered
92 break;
93 }
94 trigger_offset += 1;
95 }
96 }
97
98 if self.triggering {
99 out.push(DataSet::new(
100 Some("T".into()),
101 vec![(0.0, self.threshold)],
102 cfg.marker_type,
103 GraphType::Scatter,
104 cfg.labels_color,
105 ));
106 }
107
108 for (n, channel) in data.iter().enumerate().rev() {
109 let (mut min, mut max) = (0.0, 0.0);
110 let mut tmp = Vec::new();
111 for (i, sample) in channel.iter().enumerate() {
112 if *sample < min {
113 min = *sample
114 };
115 if *sample > max {
116 max = *sample
117 };
118 if i >= trigger_offset {
119 tmp.push(((i - trigger_offset) as f64, *sample));
120 }
121 }
122
123 if self.peaks {
124 out.push(DataSet::new(
125 None,
126 vec![(0.0, min), (0.0, max)],
127 cfg.marker_type,
128 GraphType::Scatter,
129 cfg.palette(n),
130 ))
131 }
132
133 out.push(DataSet::new(
134 Some(self.channel_name(n)),
135 tmp,
136 cfg.marker_type,
137 if cfg.scatter {
138 GraphType::Scatter
139 } else {
140 GraphType::Line
141 },
142 cfg.palette(n),
143 ));
144 }
145
146 out
147 }
148
149 fn handle(&mut self, event: Event) {
150 if let Event::Key(key) = event {
151 let magnitude = match key.modifiers {
152 KeyModifiers::SHIFT => 10.0,
153 KeyModifiers::CONTROL => 5.0,
154 KeyModifiers::ALT => 0.2,
155 _ => 1.0,
156 };
157 match key.code {
158 KeyCode::PageUp => {
159 update_value_f(&mut self.threshold, 250.0, magnitude, 0.0..32768.0)
160 }
161 KeyCode::PageDown => {
162 update_value_f(&mut self.threshold, -250.0, magnitude, 0.0..32768.0)
163 }
164 KeyCode::Char('t') => self.triggering = !self.triggering,
165 KeyCode::Char('e') => self.falling_edge = !self.falling_edge,
166 KeyCode::Char('p') => self.peaks = !self.peaks,
167 KeyCode::Char('=') => update_value_i(&mut self.depth, true, 1, 1.0, 1..65535),
168 KeyCode::Char('-') => update_value_i(&mut self.depth, false, 1, 1.0, 1..65535),
169 KeyCode::Char('+') => update_value_i(&mut self.depth, true, 10, 1.0, 1..65535),
170 KeyCode::Char('_') => update_value_i(&mut self.depth, false, 10, 1.0, 1..65535),
171 KeyCode::Esc => {
172 self.triggering = false;
173 }
174 _ => {}
175 }
176 }
177 }
178}
179
180#[allow(clippy::collapsible_else_if)] // TODO can this be made nicer?
181fn triggered(data: &[f64], index: usize, threshold: f64, depth: u32, falling_edge: bool) -> bool {
182 if data.len() < index + (1 + depth as usize) {
183 return false;
184 }
185 if falling_edge {
186 if data[index] >= threshold {
187 for i in 1..=depth as usize {
188 if data[index + i] >= threshold {
189 return false;
190 }
191 }
192 true
193 } else {
194 false
195 }
196 } else {
197 if data[index] <= threshold {
198 for i in 1..=depth as usize {
199 if data[index + i] <= threshold {
200 return false;
201 }
202 }
203 true
204 } else {
205 false
206 }
207 }
208}