+103
-8
Cargo.lock
+103
-8
Cargo.lock
···
605
605
checksum = "fbdcdcb6d86f71c5e97409ad45898af11cbc995b4ee8112d59095a28d376c935"
606
606
607
607
[[package]]
608
+
name = "convert_case"
609
+
version = "0.4.0"
610
+
source = "registry+https://github.com/rust-lang/crates.io-index"
611
+
checksum = "6245d59a3e82a7fc217c5828a6692dbc6dfb63a0c8c90495621f7b9d79704a0e"
612
+
613
+
[[package]]
608
614
name = "cookie"
609
615
version = "0.14.4"
610
616
source = "registry+https://github.com/rust-lang/crates.io-index"
···
789
795
"num_cpus",
790
796
"serde",
791
797
"tokio",
798
+
]
799
+
800
+
[[package]]
801
+
name = "derive_more"
802
+
version = "0.99.17"
803
+
source = "registry+https://github.com/rust-lang/crates.io-index"
804
+
checksum = "4fb810d30a7c1953f91334de7244731fc3f3c10d7fe163338a35b9f640960321"
805
+
dependencies = [
806
+
"convert_case",
807
+
"proc-macro2",
808
+
"quote",
809
+
"rustc_version 0.4.0",
810
+
"syn 1.0.107",
792
811
]
793
812
794
813
[[package]]
···
1646
1665
]
1647
1666
1648
1667
[[package]]
1668
+
name = "num-complex"
1669
+
version = "0.4.5"
1670
+
source = "registry+https://github.com/rust-lang/crates.io-index"
1671
+
checksum = "23c6602fda94a57c990fe0df199a035d83576b496aa29f4e634a8ac6004e68a6"
1672
+
dependencies = [
1673
+
"num-traits",
1674
+
]
1675
+
1676
+
[[package]]
1649
1677
name = "num-derive"
1650
1678
version = "0.3.3"
1651
1679
source = "registry+https://github.com/rust-lang/crates.io-index"
···
1654
1682
"proc-macro2",
1655
1683
"quote",
1656
1684
"syn 1.0.107",
1685
+
]
1686
+
1687
+
[[package]]
1688
+
name = "num-integer"
1689
+
version = "0.1.46"
1690
+
source = "registry+https://github.com/rust-lang/crates.io-index"
1691
+
checksum = "7969661fd2958a5cb096e56c8e1ad0444ac2bbcd0061bd28660485a44879858f"
1692
+
dependencies = [
1693
+
"num-traits",
1657
1694
]
1658
1695
1659
1696
[[package]]
···
1907
1944
dependencies = [
1908
1945
"proc-macro2",
1909
1946
"syn 1.0.107",
1947
+
]
1948
+
1949
+
[[package]]
1950
+
name = "primal-check"
1951
+
version = "0.3.3"
1952
+
source = "registry+https://github.com/rust-lang/crates.io-index"
1953
+
checksum = "9df7f93fd637f083201473dab4fee2db4c429d32e55e3299980ab3957ab916a0"
1954
+
dependencies = [
1955
+
"num-integer",
1910
1956
]
1911
1957
1912
1958
[[package]]
···
2238
2284
source = "registry+https://github.com/rust-lang/crates.io-index"
2239
2285
checksum = "138e3e0acb6c9fb258b19b67cb8abd63c00679d2851805ea151465464fe9030a"
2240
2286
dependencies = [
2241
-
"semver",
2287
+
"semver 0.9.0",
2288
+
]
2289
+
2290
+
[[package]]
2291
+
name = "rustc_version"
2292
+
version = "0.4.0"
2293
+
source = "registry+https://github.com/rust-lang/crates.io-index"
2294
+
checksum = "bfa0f585226d2e68097d4f95d113b15b83a82e819ab25717ec0590d9584ef366"
2295
+
dependencies = [
2296
+
"semver 1.0.22",
2297
+
]
2298
+
2299
+
[[package]]
2300
+
name = "rustfft"
2301
+
version = "6.2.0"
2302
+
source = "registry+https://github.com/rust-lang/crates.io-index"
2303
+
checksum = "43806561bc506d0c5d160643ad742e3161049ac01027b5e6d7524091fd401d86"
2304
+
dependencies = [
2305
+
"num-complex",
2306
+
"num-integer",
2307
+
"num-traits",
2308
+
"primal-check",
2309
+
"strength_reduce",
2310
+
"transpose",
2311
+
"version_check",
2242
2312
]
2243
2313
2244
2314
[[package]]
···
2330
2400
dependencies = [
2331
2401
"semver-parser",
2332
2402
]
2403
+
2404
+
[[package]]
2405
+
name = "semver"
2406
+
version = "1.0.22"
2407
+
source = "registry+https://github.com/rust-lang/crates.io-index"
2408
+
checksum = "92d43fe69e652f3df9bdc2b85b2854a0825b86e4fb76bc44d945137d053639ca"
2333
2409
2334
2410
[[package]]
2335
2411
name = "semver-parser"
···
2544
2620
checksum = "d022496b16281348b52d0e30ae99e01a73d737b2f45d38fed4edf79f9325a1d5"
2545
2621
dependencies = [
2546
2622
"discard",
2547
-
"rustc_version",
2623
+
"rustc_version 0.2.3",
2548
2624
"stdweb-derive",
2549
2625
"stdweb-internal-macros",
2550
2626
"stdweb-internal-runtime",
···
2587
2663
checksum = "213701ba3370744dcd1a12960caa4843b3d68b4d1c0a5d575e0d65b2ee9d16c0"
2588
2664
2589
2665
[[package]]
2666
+
name = "strength_reduce"
2667
+
version = "0.2.4"
2668
+
source = "registry+https://github.com/rust-lang/crates.io-index"
2669
+
checksum = "fe895eb47f22e2ddd4dabc02bce419d2e643c8e3b585c78158b349195bc24d82"
2670
+
2671
+
[[package]]
2590
2672
name = "strsim"
2591
2673
version = "0.10.0"
2592
2674
source = "registry+https://github.com/rust-lang/crates.io-index"
···
2896
2978
2897
2979
[[package]]
2898
2980
name = "thiserror"
2899
-
version = "1.0.38"
2981
+
version = "1.0.58"
2900
2982
source = "registry+https://github.com/rust-lang/crates.io-index"
2901
-
checksum = "6a9cd18aa97d5c45c6603caea1da6628790b37f7a34b6ca89522331c5180fed0"
2983
+
checksum = "03468839009160513471e86a034bb2c5c0e4baae3b43f79ffc55c4a5427b3297"
2902
2984
dependencies = [
2903
2985
"thiserror-impl",
2904
2986
]
2905
2987
2906
2988
[[package]]
2907
2989
name = "thiserror-impl"
2908
-
version = "1.0.38"
2990
+
version = "1.0.58"
2909
2991
source = "registry+https://github.com/rust-lang/crates.io-index"
2910
-
checksum = "1fb327af4685e4d03fa8cbcf1716380da910eeb2bb8be417e7f9fd3fb164f36f"
2992
+
checksum = "c61f3ba182994efc43764a46c018c347bc492c79f024e705f46567b418f6d4f7"
2911
2993
dependencies = [
2912
2994
"proc-macro2",
2913
2995
"quote",
2914
-
"syn 1.0.107",
2996
+
"syn 2.0.51",
2915
2997
]
2916
2998
2917
2999
[[package]]
···
3217
3299
]
3218
3300
3219
3301
[[package]]
3302
+
name = "transpose"
3303
+
version = "0.2.3"
3304
+
source = "registry+https://github.com/rust-lang/crates.io-index"
3305
+
checksum = "1ad61aed86bc3faea4300c7aee358b4c6d0c8d6ccc36524c96e4c92ccf26e77e"
3306
+
dependencies = [
3307
+
"num-integer",
3308
+
"strength_reduce",
3309
+
]
3310
+
3311
+
[[package]]
3220
3312
name = "try-lock"
3221
3313
version = "0.2.4"
3222
3314
source = "registry+https://github.com/rust-lang/crates.io-index"
···
3239
3331
3240
3332
[[package]]
3241
3333
name = "tunein-cli"
3242
-
version = "0.2.5"
3334
+
version = "0.2.6"
3243
3335
dependencies = [
3244
3336
"anyhow",
3245
3337
"clap",
3246
3338
"cpal",
3247
3339
"crossterm",
3340
+
"derive_more",
3248
3341
"futures",
3249
3342
"futures-util",
3250
3343
"hyper",
···
3256
3349
"ratatui",
3257
3350
"reqwest",
3258
3351
"rodio",
3352
+
"rustfft",
3259
3353
"serde",
3260
3354
"surf",
3261
3355
"symphonia",
3262
3356
"termion",
3357
+
"thiserror",
3263
3358
"tokio",
3264
3359
"tonic",
3265
3360
"tonic-build",
+6
-3
Cargo.toml
+6
-3
Cargo.toml
···
8
8
name = "tunein-cli"
9
9
readme = "README.md"
10
10
repository = "https://github.com/tsirysndr/tunein-cli"
11
-
version = "0.2.5"
11
+
version = "0.2.6"
12
12
13
13
[[bin]]
14
14
name = "tunein"
···
16
16
17
17
[workspace.metadata.cross.target.aarch64-unknown-linux-gnu]
18
18
pre-build = [
19
-
"dpkg --add-architecture $CROSS_DEB_ARCH",
20
-
"apt-get update && apt-get --assume-yes install libasound2-dev libasound2-dev:$CROSS_DEB_ARCH protobuf-compiler"
19
+
"dpkg --add-architecture $CROSS_DEB_ARCH",
20
+
"apt-get update && apt-get --assume-yes install libasound2-dev libasound2-dev:$CROSS_DEB_ARCH protobuf-compiler",
21
21
]
22
22
23
23
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
···
27
27
clap = "3.2.20"
28
28
cpal = "0.14.0"
29
29
crossterm = "0.27.0"
30
+
derive_more = "0.99.17"
30
31
futures = "0.3.26"
31
32
futures-util = "0.3.26"
32
33
hyper = {version = "0.14.23", features = ["client", "stream", "tcp", "http1", "http2"]}
···
38
39
ratatui = "0.26.1"
39
40
reqwest = {version = "0.11.14", features = ["blocking", "rustls-tls"], default-features = false}
40
41
rodio = {version = "0.16"}
42
+
rustfft = "6.2.0"
41
43
serde = "1.0.197"
42
44
surf = {version = "2.3.2", features = ["h1-client-rustls"], default-features = false}
43
45
symphonia = {version = "0.5.1", features = ["aac", "alac", "mp3", "isomp4", "flac"]}
44
46
termion = "2.0.1"
47
+
thiserror = "1.0.58"
45
48
tokio = {version = "1.24.2", features = ["tokio-macros", "macros", "rt", "rt-multi-thread"]}
46
49
tonic = "0.8.3"
47
50
tonic-web = "0.4.0"
+2
-2
README.md
+2
-2
README.md
···
72
72
Or download the latest release for your platform [here](https://github.com/tsirysndr/tunein-cli/releases).
73
73
74
74
## ๐ฆ Downloads
75
-
- `Mac`: arm64: [tunein_v0.2.5_aarch64-apple-darwin.tar.gz](https://github.com/tsirysndr/tunein-cli/releases/download/v0.2.5/tunein_v0.2.5_aarch64-apple-darwin.tar.gz) intel: [tunein_v0.2.5_x86_64-apple-darwin.tar.gz](https://github.com/tsirysndr/tunein-cli/releases/download/v0.2.5/tunein_v0.2.5_x86_64-apple-darwin.tar.gz)
76
-
- `Linux`: [tunein_v0.2.5_x86_64-unknown-linux-gnu.tar.gz](https://github.com/tsirysndr/tunein-cli/releases/download/v0.2.5/tunein_v0.2.5_x86_64-unknown-linux-gnu.tar.gz)
75
+
- `Mac`: arm64: [tunein_v0.2.6_aarch64-apple-darwin.tar.gz](https://github.com/tsirysndr/tunein-cli/releases/download/v0.2.6/tunein_v0.2.6_aarch64-apple-darwin.tar.gz) intel: [tunein_v0.2.6_x86_64-apple-darwin.tar.gz](https://github.com/tsirysndr/tunein-cli/releases/download/v0.2.6/tunein_v0.2.6_x86_64-apple-darwin.tar.gz)
76
+
- `Linux`: [tunein_v0.2.6_x86_64-unknown-linux-gnu.tar.gz](https://github.com/tsirysndr/tunein-cli/releases/download/v0.2.6/tunein_v0.2.6_x86_64-unknown-linux-gnu.tar.gz)
77
77
## ๐ Usage
78
78
```
79
79
USAGE:
+1
-1
flake.nix
+1
-1
flake.nix
+357
-111
src/app.rs
+357
-111
src/app.rs
···
1
-
use crossterm::event::{self, Event, KeyCode, KeyEvent, KeyEventKind, KeyModifiers};
1
+
use crossterm::event::{self, Event, KeyCode, KeyModifiers};
2
2
use ratatui::{
3
3
prelude::*,
4
4
widgets::{block::*, *},
5
5
};
6
-
use std::{io, process, thread, time::Duration};
6
+
use std::{
7
+
io,
8
+
ops::Range,
9
+
process,
10
+
sync::{mpsc::Receiver, Arc, Mutex},
11
+
thread,
12
+
time::{Duration, Instant},
13
+
};
7
14
use tokio::sync::mpsc::UnboundedReceiver;
8
15
9
-
use crate::{extract::get_currently_playing, tui};
16
+
use crate::{
17
+
extract::get_currently_playing,
18
+
input::stream_to_matrix,
19
+
tui,
20
+
visualization::{
21
+
oscilloscope::Oscilloscope, spectroscope::Spectroscope, vectorscope::Vectorscope,
22
+
Dimension, DisplayMode, GraphConfig,
23
+
},
24
+
};
10
25
11
-
#[derive(Debug, Default)]
26
+
#[derive(Debug, Default, Clone)]
12
27
pub struct State {
13
28
pub name: String,
14
29
pub now_playing: String,
···
17
32
pub br: String,
18
33
}
19
34
20
-
#[derive(Debug, Default)]
35
+
pub enum CurrentDisplayMode {
36
+
Oscilloscope,
37
+
Vectorscope,
38
+
Spectroscope,
39
+
}
40
+
21
41
pub struct App {
22
-
name: String,
23
-
now_playing: String,
24
-
genre: String,
25
-
description: String,
26
-
br: String,
27
-
exit: bool,
42
+
#[allow(unused)]
43
+
channels: u8,
44
+
graph: GraphConfig,
45
+
oscilloscope: Oscilloscope,
46
+
vectorscope: Vectorscope,
47
+
spectroscope: Spectroscope,
48
+
mode: CurrentDisplayMode,
49
+
frame_rx: Receiver<minimp3::Frame>,
28
50
}
29
51
30
52
impl App {
31
-
pub fn new() -> Self {
53
+
pub fn new(
54
+
ui: &crate::cfg::UiOptions,
55
+
source: &crate::cfg::SourceOptions,
56
+
frame_rx: Receiver<minimp3::Frame>,
57
+
) -> Self {
58
+
let graph = GraphConfig {
59
+
axis_color: Color::DarkGray,
60
+
labels_color: Color::Cyan,
61
+
palette: vec![Color::Red, Color::Yellow, Color::Green, Color::Magenta],
62
+
scale: ui.scale as f64,
63
+
width: source.buffer, // TODO also make bit depth customizable
64
+
samples: source.buffer,
65
+
sampling_rate: source.sample_rate,
66
+
references: !ui.no_reference,
67
+
show_ui: !ui.no_ui,
68
+
scatter: ui.scatter,
69
+
pause: false,
70
+
marker_type: if ui.no_braille {
71
+
Marker::Dot
72
+
} else {
73
+
Marker::Braille
74
+
},
75
+
};
76
+
77
+
let oscilloscope = Oscilloscope::from_args(source);
78
+
let vectorscope = Vectorscope::from_args(source);
79
+
let spectroscope = Spectroscope::from_args(source);
80
+
32
81
Self {
33
-
name: "".to_string(),
34
-
now_playing: "".to_string(),
35
-
genre: "".to_string(),
36
-
description: "".to_string(),
37
-
br: "".to_string(),
38
-
exit: false,
82
+
graph,
83
+
oscilloscope,
84
+
vectorscope,
85
+
spectroscope,
86
+
mode: CurrentDisplayMode::Spectroscope,
87
+
channels: source.channels as u8,
88
+
frame_rx,
39
89
}
40
90
}
41
91
}
42
92
93
+
fn render_frame(state: Arc<Mutex<State>>, frame: &mut Frame) {
94
+
let state = state.lock().unwrap();
95
+
let size = frame.size();
96
+
97
+
frame.render_widget(
98
+
Block::new()
99
+
.borders(Borders::TOP)
100
+
.title(" TuneIn CLI ")
101
+
.title_alignment(Alignment::Center),
102
+
Rect {
103
+
x: size.x,
104
+
y: size.y,
105
+
width: size.width,
106
+
height: 1,
107
+
},
108
+
);
109
+
110
+
render_line(
111
+
"Station ",
112
+
&state.name,
113
+
Rect {
114
+
x: size.x,
115
+
y: size.y + 1,
116
+
width: size.width,
117
+
height: 1,
118
+
},
119
+
frame,
120
+
);
121
+
render_line(
122
+
"Now Playing ",
123
+
&state.now_playing,
124
+
Rect {
125
+
x: size.x,
126
+
y: size.y + 2,
127
+
width: size.width,
128
+
height: 1,
129
+
},
130
+
frame,
131
+
);
132
+
render_line(
133
+
"Genre ",
134
+
&state.genre,
135
+
Rect {
136
+
x: size.x,
137
+
y: size.y + 3,
138
+
width: size.width,
139
+
height: 1,
140
+
},
141
+
frame,
142
+
);
143
+
render_line(
144
+
"Description ",
145
+
&state.description,
146
+
Rect {
147
+
x: size.x,
148
+
y: size.y + 4,
149
+
width: size.width,
150
+
height: 1,
151
+
},
152
+
frame,
153
+
);
154
+
render_line(
155
+
"Bitrate ",
156
+
&format!("{} kbps", &state.br),
157
+
Rect {
158
+
x: size.x,
159
+
y: size.y + 5,
160
+
width: size.width,
161
+
height: 1,
162
+
},
163
+
frame,
164
+
);
165
+
}
166
+
167
+
fn render_line(label: &str, value: &str, area: Rect, frame: &mut Frame) {
168
+
let span1 = Span::styled(label, Style::new().fg(Color::LightBlue));
169
+
let span2 = Span::raw(value);
170
+
171
+
let line = Line::from(vec![span1, span2]);
172
+
let text: Text = Text::from(vec![line]);
173
+
174
+
frame.render_widget(Paragraph::new(text), area);
175
+
}
176
+
43
177
impl App {
44
178
/// runs the application's main loop until the user quits
45
179
pub async fn run(
···
49
183
id: &str,
50
184
) {
51
185
let new_state = cmd_rx.recv().await.unwrap();
52
-
self.name = new_state.name;
53
-
self.genre = new_state.genre;
54
-
self.description = new_state.description;
55
-
self.br = new_state.br;
186
+
let new_state = Arc::new(Mutex::new(new_state));
187
+
188
+
let id = id.to_string();
189
+
let new_state_clone = new_state.clone();
56
190
57
-
thread::spawn(|| loop {
58
-
match event::read().unwrap() {
59
-
// it's important to check that the event is a key press event as
60
-
// crossterm also emits key release and repeat events on Windows.
61
-
Event::Key(key_event) if key_event.kind == KeyEventKind::Press => {
62
-
match key_event.code {
63
-
KeyCode::Char('q') => {
64
-
let _ = tui::restore();
65
-
process::exit(0);
66
-
},
67
-
KeyCode::Char('d')
68
-
if key_event.modifiers.contains(KeyModifiers::CONTROL) =>
69
-
{
70
-
let _ = tui::restore();
71
-
process::exit(0);
72
-
}
73
-
KeyCode::Char('c')
74
-
if key_event.modifiers.contains(KeyModifiers::CONTROL) =>
75
-
{
76
-
let _ = tui::restore();
77
-
process::exit(0);
78
-
}
79
-
_ => {}
80
-
}
81
-
}
82
-
_ => {}
83
-
};
191
+
thread::spawn(move || loop {
192
+
let rt = tokio::runtime::Runtime::new().unwrap();
193
+
rt.block_on(async {
194
+
let mut new_state = new_state_clone.lock().unwrap();
195
+
// Get current playing if available, otherwise use state's value
196
+
new_state.now_playing = get_currently_playing(&id).await.unwrap_or_default();
197
+
drop(new_state);
198
+
std::thread::sleep(Duration::from_millis(10000));
199
+
});
84
200
});
85
201
202
+
let mut fps = 0;
203
+
let mut framerate = 0;
204
+
let mut last_poll = Instant::now();
205
+
86
206
loop {
87
-
// Get current playing if available, otherwise use state's value
88
-
let now_playing = get_currently_playing(id).await.unwrap_or_default();
207
+
let audio_frame = self.frame_rx.recv().unwrap();
208
+
let channels =
209
+
stream_to_matrix(audio_frame.data.iter().cloned(), audio_frame.channels, 1.);
89
210
90
-
// Update state with current playing
91
-
self.now_playing = now_playing;
92
-
terminal.draw(|frame| self.render_frame(frame)).unwrap();
93
-
std::thread::sleep(Duration::from_millis(10000));
94
-
}
95
-
}
211
+
fps += 1;
96
212
97
-
fn render_frame(&self, frame: &mut Frame) {
98
-
let areas = Layout::new(
99
-
Direction::Vertical,
100
-
[
101
-
Constraint::Length(1),
102
-
Constraint::Length(1),
103
-
Constraint::Length(1),
104
-
Constraint::Length(1),
105
-
Constraint::Length(1),
106
-
Constraint::Min(0),
107
-
],
108
-
)
109
-
.split(frame.size());
213
+
if last_poll.elapsed().as_secs() >= 1 {
214
+
framerate = fps;
215
+
fps = 0;
216
+
last_poll = Instant::now();
217
+
}
218
+
219
+
{
220
+
let mut datasets = Vec::new();
221
+
let graph = self.graph.clone(); // TODO cheap fix...
222
+
if self.graph.references {
223
+
datasets.append(&mut self.current_display_mut().references(&graph));
224
+
}
225
+
datasets.append(&mut self.current_display_mut().process(&graph, &channels));
226
+
terminal
227
+
.draw(|f| {
228
+
let mut size = f.size();
229
+
render_frame(new_state.clone(), f);
230
+
if self.graph.show_ui {
231
+
f.render_widget(
232
+
make_header(
233
+
&self.graph,
234
+
&self.current_display().header(&self.graph),
235
+
self.current_display().mode_str(),
236
+
framerate,
237
+
self.graph.pause,
238
+
),
239
+
Rect {
240
+
x: size.x,
241
+
y: size.y + 6,
242
+
width: size.width,
243
+
height: 1,
244
+
},
245
+
);
246
+
size.height -= 7;
247
+
size.y += 7;
248
+
}
249
+
let chart = Chart::new(datasets.iter().map(|x| x.into()).collect())
250
+
.x_axis(self.current_display().axis(&self.graph, Dimension::X)) // TODO allow to have axis sometimes?
251
+
.y_axis(self.current_display().axis(&self.graph, Dimension::Y));
252
+
f.render_widget(chart, size)
253
+
})
254
+
.unwrap();
255
+
}
110
256
111
-
frame.render_widget(
112
-
Block::new()
113
-
.borders(Borders::TOP)
114
-
.title(" TuneIn CLI ")
115
-
.title_alignment(Alignment::Center),
116
-
areas[0],
117
-
);
257
+
while event::poll(Duration::from_millis(0)).unwrap() {
258
+
// process all enqueued events
259
+
let event = event::read().unwrap();
118
260
119
-
self.render_line("Station ", &self.name, areas[1], frame);
120
-
self.render_line("Now Playing ", &self.now_playing, areas[2], frame);
121
-
self.render_line("Genre ", &self.genre, areas[3], frame);
122
-
self.render_line("Description ", &self.description, areas[4], frame);
123
-
self.render_line("Bitrate ", &format!("{} kbps", &self.br), areas[5], frame);
261
+
if self.process_events(event.clone()).unwrap() {
262
+
return;
263
+
}
264
+
self.current_display_mut().handle(event);
265
+
}
266
+
}
124
267
}
125
268
126
-
fn render_line(&self, label: &str, value: &str, area: Rect, frame: &mut Frame) {
127
-
let span1 = Span::styled(label, Style::new().fg(Color::LightBlue));
128
-
let span2 = Span::raw(value);
129
-
130
-
let line = Line::from(vec![span1, span2]);
131
-
let text: Text = Text::from(vec![line]);
269
+
fn current_display_mut(&mut self) -> &mut dyn DisplayMode {
270
+
match self.mode {
271
+
CurrentDisplayMode::Oscilloscope => &mut self.oscilloscope as &mut dyn DisplayMode,
272
+
CurrentDisplayMode::Vectorscope => &mut self.vectorscope as &mut dyn DisplayMode,
273
+
CurrentDisplayMode::Spectroscope => &mut self.spectroscope as &mut dyn DisplayMode,
274
+
}
275
+
}
132
276
133
-
frame.render_widget(Paragraph::new(text), area);
277
+
fn current_display(&self) -> &dyn DisplayMode {
278
+
match self.mode {
279
+
CurrentDisplayMode::Oscilloscope => &self.oscilloscope as &dyn DisplayMode,
280
+
CurrentDisplayMode::Vectorscope => &self.vectorscope as &dyn DisplayMode,
281
+
CurrentDisplayMode::Spectroscope => &self.spectroscope as &dyn DisplayMode,
282
+
}
134
283
}
135
284
136
-
/// updates the application's state based on user input
137
-
fn handle_events(&mut self) -> io::Result<()> {
138
-
match event::read()? {
139
-
// it's important to check that the event is a key press event as
140
-
// crossterm also emits key release and repeat events on Windows.
141
-
Event::Key(key_event) if key_event.kind == KeyEventKind::Press => {
142
-
self.handle_key_event(key_event)
285
+
fn process_events(&mut self, event: Event) -> Result<bool, io::Error> {
286
+
let mut quit = false;
287
+
if let Event::Key(key) = event {
288
+
if let KeyModifiers::CONTROL = key.modifiers {
289
+
match key.code {
290
+
// mimic other programs shortcuts to quit, for user friendlyness
291
+
KeyCode::Char('c') | KeyCode::Char('q') | KeyCode::Char('w') => quit = true,
292
+
_ => {}
293
+
}
143
294
}
144
-
_ => {}
295
+
let magnitude = match key.modifiers {
296
+
KeyModifiers::SHIFT => 10.0,
297
+
KeyModifiers::CONTROL => 5.0,
298
+
KeyModifiers::ALT => 0.2,
299
+
_ => 1.0,
300
+
};
301
+
match key.code {
302
+
KeyCode::Up => update_value_f(&mut self.graph.scale, 0.01, magnitude, 0.0..10.0), // inverted to act as zoom
303
+
KeyCode::Down => update_value_f(&mut self.graph.scale, -0.01, magnitude, 0.0..10.0), // inverted to act as zoom
304
+
KeyCode::Right => update_value_i(
305
+
&mut self.graph.samples,
306
+
true,
307
+
25,
308
+
magnitude,
309
+
0..self.graph.width * 2,
310
+
),
311
+
KeyCode::Left => update_value_i(
312
+
&mut self.graph.samples,
313
+
false,
314
+
25,
315
+
magnitude,
316
+
0..self.graph.width * 2,
317
+
),
318
+
KeyCode::Char('q') => quit = true,
319
+
KeyCode::Char(' ') => self.graph.pause = !self.graph.pause,
320
+
KeyCode::Char('s') => self.graph.scatter = !self.graph.scatter,
321
+
KeyCode::Char('h') => self.graph.show_ui = !self.graph.show_ui,
322
+
KeyCode::Char('r') => self.graph.references = !self.graph.references,
323
+
KeyCode::Esc => {
324
+
self.graph.samples = self.graph.width;
325
+
self.graph.scale = 1.;
326
+
}
327
+
KeyCode::Char('c') if key.modifiers.contains(KeyModifiers::CONTROL) => {
328
+
let _ = tui::restore();
329
+
process::exit(0);
330
+
}
331
+
KeyCode::Tab => {
332
+
// switch modes
333
+
match self.mode {
334
+
CurrentDisplayMode::Oscilloscope => {
335
+
self.mode = CurrentDisplayMode::Vectorscope
336
+
}
337
+
CurrentDisplayMode::Vectorscope => {
338
+
self.mode = CurrentDisplayMode::Spectroscope
339
+
}
340
+
CurrentDisplayMode::Spectroscope => {
341
+
self.mode = CurrentDisplayMode::Oscilloscope
342
+
}
343
+
}
344
+
}
345
+
_ => {}
346
+
}
145
347
};
146
-
Ok(())
348
+
349
+
Ok(quit)
147
350
}
351
+
}
148
352
149
-
fn handle_key_event(&mut self, key_event: KeyEvent) {
150
-
match key_event.code {
151
-
KeyCode::Char('q') => self.exit(),
152
-
KeyCode::Char('d') if key_event.modifiers.contains(KeyModifiers::CONTROL) => {
153
-
self.exit()
154
-
}
155
-
KeyCode::Char('c') if key_event.modifiers.contains(KeyModifiers::CONTROL) => {
156
-
self.exit()
157
-
}
158
-
_ => {}
353
+
pub fn update_value_f(val: &mut f64, base: f64, magnitude: f64, range: Range<f64>) {
354
+
let delta = base * magnitude;
355
+
if *val + delta > range.end {
356
+
*val = range.end
357
+
} else if *val + delta < range.start {
358
+
*val = range.start
359
+
} else {
360
+
*val += delta;
361
+
}
362
+
}
363
+
364
+
pub fn update_value_i(val: &mut u32, inc: bool, base: u32, magnitude: f64, range: Range<u32>) {
365
+
let delta = (base as f64 * magnitude) as u32;
366
+
if inc {
367
+
if range.end - delta < *val {
368
+
*val = range.end
369
+
} else {
370
+
*val += delta
159
371
}
372
+
} else if range.start + delta > *val {
373
+
*val = range.start
374
+
} else {
375
+
*val -= delta
160
376
}
377
+
}
161
378
162
-
fn exit(&mut self) {
163
-
self.exit = true;
164
-
}
379
+
fn make_header<'a>(
380
+
cfg: &GraphConfig,
381
+
module_header: &'a str,
382
+
kind_o_scope: &'static str,
383
+
fps: usize,
384
+
pause: bool,
385
+
) -> Table<'a> {
386
+
Table::new(
387
+
vec![Row::new(vec![
388
+
Cell::from(format!("{}::scope-tui", kind_o_scope)).style(
389
+
Style::default()
390
+
.fg(*cfg.palette.first().expect("empty palette?"))
391
+
.add_modifier(Modifier::BOLD),
392
+
),
393
+
Cell::from(module_header),
394
+
Cell::from(format!("-{:.2}x+", cfg.scale)),
395
+
Cell::from(format!("{}/{} spf", cfg.samples, cfg.width)),
396
+
Cell::from(format!("{}fps", fps)),
397
+
Cell::from(if cfg.scatter { "***" } else { "---" }),
398
+
Cell::from(if pause { "||" } else { "|>" }),
399
+
])],
400
+
vec![
401
+
Constraint::Percentage(35),
402
+
Constraint::Percentage(25),
403
+
Constraint::Percentage(7),
404
+
Constraint::Percentage(13),
405
+
Constraint::Percentage(6),
406
+
Constraint::Percentage(6),
407
+
Constraint::Percentage(6),
408
+
],
409
+
)
410
+
.style(Style::default().fg(cfg.labels_color))
165
411
}
+59
src/cfg.rs
+59
src/cfg.rs
···
1
+
use crate::music::Note;
2
+
3
+
/// a simple oscilloscope/vectorscope for your terminal
4
+
#[derive(Debug)]
5
+
pub struct ScopeArgs {
6
+
pub opts: SourceOptions,
7
+
pub ui: UiOptions,
8
+
}
9
+
10
+
#[derive(Debug, Clone)]
11
+
pub struct UiOptions {
12
+
/// floating point vertical scale, from 0 to 1
13
+
pub scale: f32,
14
+
15
+
/// use vintage looking scatter mode instead of line mode
16
+
pub scatter: bool,
17
+
18
+
/// don't draw reference line
19
+
pub no_reference: bool,
20
+
21
+
/// hide UI and only draw waveforms
22
+
pub no_ui: bool,
23
+
24
+
/// don't use braille dots for drawing lines
25
+
pub no_braille: bool,
26
+
}
27
+
28
+
#[derive(Debug, Clone)]
29
+
pub struct SourceOptions {
30
+
/// number of channels to open
31
+
pub channels: usize,
32
+
33
+
/// size of audio buffer, and width of scope
34
+
pub buffer: u32,
35
+
36
+
/// sample rate to use
37
+
pub sample_rate: u32,
38
+
39
+
/// tune buffer size to be in tune with given note (overrides buffer option)
40
+
pub tune: Option<String>,
41
+
}
42
+
43
+
// TODO its convenient to keep this here but it's not really the best place...
44
+
impl SourceOptions {
45
+
pub fn tune(&mut self) {
46
+
if let Some(txt) = &self.tune {
47
+
// TODO make it less jank
48
+
if let Ok(note) = txt.parse::<Note>() {
49
+
self.buffer = note.tune_buffer_size(self.sample_rate);
50
+
while self.buffer % (self.channels as u32 * 2) != 0 {
51
+
// TODO customizable bit depth
52
+
self.buffer += 1; // TODO jank but otherwise it doesn't align
53
+
}
54
+
} else {
55
+
eprintln!("[!] Unrecognized note '{}', ignoring option", txt);
56
+
}
57
+
}
58
+
}
59
+
}
+11
-3
src/decoder.rs
+11
-3
src/decoder.rs
···
1
-
use std::io::Read;
2
1
use std::time::Duration;
2
+
use std::{io::Read, sync::mpsc::Sender};
3
3
4
4
use minimp3::{Decoder, Frame};
5
5
use rodio::Source;
···
11
11
decoder: Decoder<R>,
12
12
current_frame: Frame,
13
13
current_frame_offset: usize,
14
+
tx: Sender<Frame>,
14
15
}
15
16
16
17
impl<R> Mp3Decoder<R>
17
18
where
18
19
R: Read,
19
20
{
20
-
pub fn new(mut data: R) -> Result<Self, R> {
21
+
pub fn new(mut data: R, tx: Sender<Frame>) -> Result<Self, R> {
21
22
if !is_mp3(data.by_ref()) {
22
23
return Err(data);
23
24
}
···
28
29
decoder,
29
30
current_frame,
30
31
current_frame_offset: 0,
32
+
tx,
31
33
})
32
34
}
33
35
}
···
67
69
fn next(&mut self) -> Option<i16> {
68
70
if self.current_frame_offset == self.current_frame.data.len() {
69
71
match self.decoder.next_frame() {
70
-
Ok(frame) => self.current_frame = frame,
72
+
Ok(frame) => {
73
+
match self.tx.send(frame.clone()) {
74
+
Ok(_) => {}
75
+
Err(_) => return None,
76
+
}
77
+
self.current_frame = frame
78
+
}
71
79
_ => return None,
72
80
}
73
81
self.current_frame_offset = 0;
+10
src/format.rs
+10
src/format.rs
+24
src/input.rs
+24
src/input.rs
···
1
+
pub type Matrix<T> = Vec<Vec<T>>;
2
+
3
+
/// separate a stream of alternating channels into a matrix of channel streams:
4
+
/// L R L R L R L R L R
5
+
/// becomes
6
+
/// L L L L L
7
+
/// R R R R R
8
+
pub fn stream_to_matrix<I, O>(
9
+
stream: impl Iterator<Item = I>,
10
+
channels: usize,
11
+
norm: O,
12
+
) -> Matrix<O>
13
+
where
14
+
I: Copy + Into<O>,
15
+
O: Copy + std::ops::Div<Output = O>,
16
+
{
17
+
let mut out = vec![vec![]; channels];
18
+
let mut channel = 0;
19
+
for sample in stream {
20
+
out[channel].push(sample.into() / norm);
21
+
channel = (channel + 1) % channels;
22
+
}
23
+
out
24
+
}
+4
src/main.rs
+4
src/main.rs
···
3
3
4
4
mod app;
5
5
mod browse;
6
+
mod cfg;
6
7
mod decoder;
7
8
mod extract;
9
+
mod input;
10
+
mod music;
8
11
mod play;
9
12
mod player;
10
13
mod search;
11
14
mod server;
12
15
mod tui;
16
+
mod visualization;
13
17
14
18
fn cli() -> Command<'static> {
15
19
const VESRION: &str = env!("CARGO_PKG_VERSION");
+110
src/music.rs
+110
src/music.rs
···
1
+
use std::{num::ParseIntError, str::FromStr};
2
+
3
+
#[derive(Debug, PartialEq, Clone)]
4
+
pub enum Tone {
5
+
C,
6
+
Db,
7
+
D,
8
+
Eb,
9
+
E,
10
+
F,
11
+
Gb,
12
+
G,
13
+
Ab,
14
+
A,
15
+
Bb,
16
+
B,
17
+
}
18
+
19
+
#[derive(Debug, thiserror::Error, derive_more::Display)]
20
+
pub struct ToneError();
21
+
22
+
#[derive(Debug, PartialEq, Clone)]
23
+
pub struct Note {
24
+
tone: Tone,
25
+
octave: u32,
26
+
}
27
+
28
+
#[derive(Debug, thiserror::Error, derive_more::From, derive_more::Display)]
29
+
pub enum NoteError {
30
+
InvalidOctave(ParseIntError),
31
+
InalidNote(ToneError),
32
+
}
33
+
34
+
impl FromStr for Note {
35
+
type Err = NoteError;
36
+
37
+
fn from_str(txt: &str) -> Result<Self, Self::Err> {
38
+
let trimmed = txt.trim();
39
+
let mut split = 0;
40
+
for c in trimmed.chars() {
41
+
if !c.is_ascii_digit() {
42
+
split += 1;
43
+
} else {
44
+
break;
45
+
}
46
+
}
47
+
Ok(Note {
48
+
tone: trimmed[..split].parse::<Tone>()?,
49
+
octave: trimmed[split..].parse::<u32>().unwrap_or(0),
50
+
})
51
+
}
52
+
}
53
+
54
+
impl FromStr for Tone {
55
+
type Err = ToneError;
56
+
57
+
fn from_str(txt: &str) -> Result<Self, Self::Err> {
58
+
match txt {
59
+
"C" => Ok(Tone::C),
60
+
"C#" | "Db" => Ok(Tone::Db),
61
+
"D" => Ok(Tone::D),
62
+
"D#" | "Eb" => Ok(Tone::Eb),
63
+
"E" => Ok(Tone::E),
64
+
"F" => Ok(Tone::F),
65
+
"F#" | "Gb" => Ok(Tone::Gb),
66
+
"G" => Ok(Tone::G),
67
+
"G#" | "Ab" => Ok(Tone::Ab),
68
+
"A" => Ok(Tone::A),
69
+
"A#" | "Bb" => Ok(Tone::Bb),
70
+
"B" => Ok(Tone::B),
71
+
_ => Err(ToneError()),
72
+
}
73
+
}
74
+
}
75
+
76
+
impl Note {
77
+
pub fn tune_buffer_size(&self, sample_rate: u32) -> u32 {
78
+
let t = 1.0 / self.tone.freq(self.octave); // periodo ?
79
+
let buf = (sample_rate as f32) * t;
80
+
buf.round() as u32
81
+
}
82
+
}
83
+
84
+
impl Tone {
85
+
pub fn freq(&self, octave: u32) -> f32 {
86
+
match octave {
87
+
0 => match self {
88
+
Tone::C => 16.35,
89
+
Tone::Db => 17.32,
90
+
Tone::D => 18.35,
91
+
Tone::Eb => 19.45,
92
+
Tone::E => 20.60,
93
+
Tone::F => 21.83,
94
+
Tone::Gb => 23.12,
95
+
Tone::G => 24.50,
96
+
Tone::Ab => 25.96,
97
+
Tone::A => 27.50,
98
+
Tone::Bb => 29.14,
99
+
Tone::B => 30.87,
100
+
},
101
+
_ => {
102
+
let mut freq = self.freq(0);
103
+
for _ in 0..octave {
104
+
freq *= 2.0;
105
+
}
106
+
freq
107
+
}
108
+
}
109
+
}
110
+
}
+19
-2
src/play.rs
+19
-2
src/play.rs
···
5
5
6
6
use crate::{
7
7
app::{App, State},
8
+
cfg::{SourceOptions, UiOptions},
8
9
decoder::Mp3Decoder,
9
10
extract::{extract_stream_url, get_currently_playing},
10
11
tui,
···
58
59
println!("{}", stream_url);
59
60
60
61
let (cmd_tx, cmd_rx) = tokio::sync::mpsc::unbounded_channel::<State>();
62
+
let (frame_tx, frame_rx) = std::sync::mpsc::channel::<minimp3::Frame>();
61
63
62
-
let mut app = App::new();
64
+
let ui = UiOptions {
65
+
scale: 1.0,
66
+
scatter: false,
67
+
no_reference: true,
68
+
no_ui: true,
69
+
no_braille: false,
70
+
};
71
+
72
+
let opts = SourceOptions {
73
+
channels: 2,
74
+
buffer: 1152,
75
+
sample_rate: 44100,
76
+
tune: None,
77
+
};
78
+
79
+
let mut app = App::new(&ui, &opts, frame_rx);
63
80
64
81
thread::spawn(move || {
65
82
let client = reqwest::blocking::Client::new();
···
107
124
108
125
let (_stream, handle) = rodio::OutputStream::try_default().unwrap();
109
126
let sink = rodio::Sink::try_new(&handle).unwrap();
110
-
let decoder = Mp3Decoder::new(response).unwrap();
127
+
let decoder = Mp3Decoder::new(response, frame_tx).unwrap();
111
128
sink.append(decoder);
112
129
113
130
loop {
+2
-1
src/player.rs
+2
-1
src/player.rs
···
51
51
}
52
52
53
53
fn handle_play(&mut self, url: String) -> Result<(), Error> {
54
+
let (frame_tx, frame_rx) = std::sync::mpsc::channel::<minimp3::Frame>();
54
55
let client = reqwest::blocking::Client::new();
55
56
56
57
let response = client.get(url).send().unwrap();
···
74
75
self.stream = stream;
75
76
self.sink = rodio::Sink::try_new(&handle).unwrap();
76
77
self.handle = handle;
77
-
let decoder = Mp3Decoder::new(response).unwrap();
78
+
let decoder = Mp3Decoder::new(response, frame_tx).unwrap();
78
79
self.sink.append(decoder);
79
80
self.sink.play();
80
81
Ok(())
+105
src/visualization/mod.rs
+105
src/visualization/mod.rs
···
1
+
pub mod oscilloscope;
2
+
pub mod spectroscope;
3
+
pub mod vectorscope;
4
+
5
+
use crossterm::event::Event;
6
+
use ratatui::{
7
+
style::{Color, Style},
8
+
symbols::Marker,
9
+
widgets::{Axis, Dataset, GraphType},
10
+
};
11
+
12
+
use crate::input::Matrix;
13
+
14
+
pub enum Dimension {
15
+
X,
16
+
Y,
17
+
}
18
+
19
+
#[derive(Debug, Clone)]
20
+
pub struct GraphConfig {
21
+
pub pause: bool,
22
+
pub samples: u32,
23
+
pub sampling_rate: u32,
24
+
pub scale: f64,
25
+
pub width: u32,
26
+
pub scatter: bool,
27
+
pub references: bool,
28
+
pub show_ui: bool,
29
+
pub marker_type: Marker,
30
+
pub palette: Vec<Color>,
31
+
pub labels_color: Color,
32
+
pub axis_color: Color,
33
+
}
34
+
35
+
impl GraphConfig {
36
+
pub fn palette(&self, index: usize) -> Color {
37
+
*self
38
+
.palette
39
+
.get(index % self.palette.len())
40
+
.unwrap_or(&Color::White)
41
+
}
42
+
}
43
+
44
+
#[allow(clippy::ptr_arg)]
45
+
pub trait DisplayMode {
46
+
// MUST define
47
+
fn from_args(args: &crate::cfg::SourceOptions) -> Self
48
+
where
49
+
Self: Sized;
50
+
fn axis(&self, cfg: &GraphConfig, dimension: Dimension) -> Axis; // TODO simplify this
51
+
fn process(&mut self, cfg: &GraphConfig, data: &Matrix<f64>) -> Vec<DataSet>;
52
+
fn mode_str(&self) -> &'static str;
53
+
54
+
// SHOULD override
55
+
fn channel_name(&self, index: usize) -> String {
56
+
format!("{}", index)
57
+
}
58
+
fn header(&self, _cfg: &GraphConfig) -> String {
59
+
"".into()
60
+
}
61
+
fn references(&self, _cfg: &GraphConfig) -> Vec<DataSet> {
62
+
vec![]
63
+
}
64
+
fn handle(&mut self, _event: Event) {}
65
+
}
66
+
67
+
pub struct DataSet {
68
+
name: Option<String>,
69
+
data: Vec<(f64, f64)>,
70
+
marker_type: Marker,
71
+
graph_type: GraphType,
72
+
color: Color,
73
+
}
74
+
75
+
impl<'a> From<&'a DataSet> for Dataset<'a> {
76
+
fn from(ds: &'a DataSet) -> Dataset<'a> {
77
+
let mut out = Dataset::default(); // TODO creating a binding is kinda ugly, is it avoidable?
78
+
if let Some(name) = &ds.name {
79
+
out = out.name(name.clone());
80
+
}
81
+
out.marker(ds.marker_type)
82
+
.graph_type(ds.graph_type)
83
+
.style(Style::default().fg(ds.color))
84
+
.data(&ds.data)
85
+
}
86
+
}
87
+
88
+
// TODO this is pretty ugly but I need datasets which own the data
89
+
impl DataSet {
90
+
pub fn new(
91
+
name: Option<String>,
92
+
data: Vec<(f64, f64)>,
93
+
marker_type: Marker,
94
+
graph_type: GraphType,
95
+
color: Color,
96
+
) -> Self {
97
+
DataSet {
98
+
name,
99
+
data,
100
+
marker_type,
101
+
graph_type,
102
+
color,
103
+
}
104
+
}
105
+
}
+208
src/visualization/oscilloscope.rs
+208
src/visualization/oscilloscope.rs
···
1
+
use crossterm::event::{Event, KeyCode, KeyModifiers};
2
+
use ratatui::{
3
+
style::Style,
4
+
text::Span,
5
+
widgets::{Axis, GraphType},
6
+
};
7
+
8
+
use crate::{
9
+
app::{update_value_f, update_value_i},
10
+
input::Matrix,
11
+
};
12
+
13
+
use super::{DataSet, Dimension, DisplayMode, GraphConfig};
14
+
15
+
#[derive(Default)]
16
+
pub 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
+
24
+
impl 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?
181
+
fn 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
+
}
+403
src/visualization/spectroscope.rs
+403
src/visualization/spectroscope.rs
···
1
+
use std::collections::VecDeque;
2
+
3
+
use crossterm::event::{Event, KeyCode};
4
+
use ratatui::{
5
+
style::Style,
6
+
text::Span,
7
+
widgets::{Axis, GraphType},
8
+
};
9
+
10
+
use crate::{app::update_value_i, input::Matrix};
11
+
12
+
use super::{DataSet, Dimension, DisplayMode, GraphConfig};
13
+
14
+
use rustfft::{num_complex::Complex, FftPlanner};
15
+
16
+
#[derive(Default)]
17
+
pub struct Spectroscope {
18
+
pub sampling_rate: u32,
19
+
pub buffer_size: u32,
20
+
pub average: u32,
21
+
pub buf: Vec<VecDeque<Vec<f64>>>,
22
+
pub window: bool,
23
+
pub log_y: bool,
24
+
}
25
+
26
+
fn magnitude(c: Complex<f64>) -> f64 {
27
+
let squared = (c.re * c.re) + (c.im * c.im);
28
+
squared.sqrt()
29
+
}
30
+
31
+
// got this from https://github.com/phip1611/spectrum-analyzer/blob/3c079ec2785b031d304bb381ff5f5fe04e6bcf71/src/windows.rs#L40
32
+
pub fn hann_window(samples: &[f64]) -> Vec<f64> {
33
+
let mut windowed_samples = Vec::with_capacity(samples.len());
34
+
let samples_len = samples.len() as f64;
35
+
for (i, sample) in samples.iter().enumerate() {
36
+
let two_pi_i = 2.0 * std::f64::consts::PI * i as f64;
37
+
let idontknowthename = (two_pi_i / samples_len).cos();
38
+
let multiplier = 0.5 * (1.0 - idontknowthename);
39
+
windowed_samples.push(sample * multiplier)
40
+
}
41
+
windowed_samples
42
+
}
43
+
44
+
impl DisplayMode for Spectroscope {
45
+
fn from_args(opts: &crate::cfg::SourceOptions) -> Self {
46
+
Spectroscope {
47
+
sampling_rate: opts.sample_rate,
48
+
buffer_size: opts.buffer,
49
+
average: 1,
50
+
buf: Vec::new(),
51
+
window: false,
52
+
log_y: true,
53
+
}
54
+
}
55
+
56
+
fn mode_str(&self) -> &'static str {
57
+
"spectro"
58
+
}
59
+
60
+
fn channel_name(&self, index: usize) -> String {
61
+
match index {
62
+
0 => "L".into(),
63
+
1 => "R".into(),
64
+
_ => format!("{}", index),
65
+
}
66
+
}
67
+
68
+
fn header(&self, _: &GraphConfig) -> String {
69
+
let window_marker = if self.window { "-|-" } else { "---" };
70
+
if self.average <= 1 {
71
+
format!(
72
+
"live {} {:.3}Hz bins",
73
+
window_marker,
74
+
self.sampling_rate as f64 / self.buffer_size as f64
75
+
)
76
+
} else {
77
+
format!(
78
+
"{}x avg ({:.1}s) {} {:.3}Hz bins",
79
+
self.average,
80
+
(self.average * self.buffer_size) as f64 / self.sampling_rate as f64,
81
+
window_marker,
82
+
self.sampling_rate as f64 / (self.buffer_size * self.average) as f64,
83
+
)
84
+
}
85
+
}
86
+
87
+
fn axis(&self, cfg: &GraphConfig, dimension: Dimension) -> Axis {
88
+
let (name, bounds) = match dimension {
89
+
Dimension::X => (
90
+
"frequency -",
91
+
[
92
+
20.0f64.ln(),
93
+
((cfg.samples as f64 / cfg.width as f64) * 20000.0).ln(),
94
+
],
95
+
),
96
+
Dimension::Y => (
97
+
if self.log_y { "| level" } else { "| amplitude" },
98
+
[if self.log_y { 0. } else { 0.0 }, cfg.scale * 7.5], // very arbitrary but good default
99
+
),
100
+
// TODO super arbitraty! wtf! also ugly inline ifs, get this thing together!
101
+
};
102
+
let mut a = Axis::default();
103
+
if cfg.show_ui {
104
+
// TODO don't make it necessary to check show_ui inside here
105
+
a = a.title(Span::styled(name, Style::default().fg(cfg.labels_color)));
106
+
}
107
+
a.style(Style::default().fg(cfg.axis_color)).bounds(bounds)
108
+
}
109
+
110
+
fn process(&mut self, cfg: &GraphConfig, data: &Matrix<f64>) -> Vec<DataSet> {
111
+
if self.average == 0 {
112
+
self.average = 1
113
+
} // otherwise fft breaks
114
+
if !cfg.pause {
115
+
for (i, chan) in data.iter().enumerate() {
116
+
if self.buf.len() <= i {
117
+
self.buf.push(VecDeque::new());
118
+
}
119
+
self.buf[i].push_back(chan.clone());
120
+
while self.buf[i].len() > self.average as usize {
121
+
self.buf[i].pop_front();
122
+
}
123
+
}
124
+
}
125
+
126
+
let mut out = Vec::new();
127
+
let mut planner: FftPlanner<f64> = FftPlanner::new();
128
+
let sample_len = self.buffer_size * self.average;
129
+
let resolution = self.sampling_rate as f64 / sample_len as f64;
130
+
let fft = planner.plan_fft_forward(sample_len as usize);
131
+
132
+
for (n, chan_queue) in self.buf.iter().enumerate().rev() {
133
+
let mut chunk = chan_queue.iter().flatten().copied().collect::<Vec<f64>>();
134
+
if self.window {
135
+
chunk = hann_window(chunk.as_slice());
136
+
}
137
+
let mut max_val = *chunk
138
+
.iter()
139
+
.max_by(|a, b| a.total_cmp(b))
140
+
.expect("empty dataset?");
141
+
if max_val < 1. {
142
+
max_val = 1.;
143
+
}
144
+
let mut tmp: Vec<Complex<f64>> = chunk
145
+
.iter()
146
+
.map(|x| Complex {
147
+
re: *x / max_val,
148
+
im: 0.0,
149
+
})
150
+
.collect();
151
+
fft.process(tmp.as_mut_slice());
152
+
out.push(DataSet::new(
153
+
Some(self.channel_name(n)),
154
+
tmp[..=tmp.len() / 2]
155
+
.iter()
156
+
.enumerate()
157
+
.map(|(i, x)| {
158
+
(
159
+
(i as f64 * resolution).ln(),
160
+
if self.log_y {
161
+
magnitude(*x).ln()
162
+
} else {
163
+
magnitude(*x)
164
+
},
165
+
)
166
+
})
167
+
.collect(),
168
+
cfg.marker_type,
169
+
if cfg.scatter {
170
+
GraphType::Scatter
171
+
} else {
172
+
GraphType::Line
173
+
},
174
+
cfg.palette(n),
175
+
));
176
+
}
177
+
178
+
out
179
+
}
180
+
181
+
fn handle(&mut self, event: Event) {
182
+
if let Event::Key(key) = event {
183
+
match key.code {
184
+
KeyCode::PageUp => update_value_i(&mut self.average, true, 1, 1., 1..65535),
185
+
KeyCode::PageDown => update_value_i(&mut self.average, false, 1, 1., 1..65535),
186
+
KeyCode::Char('w') => self.window = !self.window,
187
+
KeyCode::Char('l') => self.log_y = !self.log_y,
188
+
_ => {}
189
+
}
190
+
}
191
+
}
192
+
193
+
fn references(&self, cfg: &GraphConfig) -> Vec<DataSet> {
194
+
let lower = 0.; // if self.log_y { -(cfg.scale * 5.) } else { 0. };
195
+
let upper = cfg.scale * 7.5;
196
+
vec![
197
+
DataSet::new(
198
+
None,
199
+
vec![(0.0, 0.0), ((cfg.samples as f64).ln(), 0.0)],
200
+
cfg.marker_type,
201
+
GraphType::Line,
202
+
cfg.axis_color,
203
+
),
204
+
// TODO can we auto generate these? lol...
205
+
DataSet::new(
206
+
None,
207
+
vec![(20.0f64.ln(), lower), (20.0f64.ln(), upper)],
208
+
cfg.marker_type,
209
+
GraphType::Line,
210
+
cfg.axis_color,
211
+
),
212
+
DataSet::new(
213
+
None,
214
+
vec![(30.0f64.ln(), lower), (30.0f64.ln(), upper)],
215
+
cfg.marker_type,
216
+
GraphType::Line,
217
+
cfg.axis_color,
218
+
),
219
+
DataSet::new(
220
+
None,
221
+
vec![(40.0f64.ln(), lower), (40.0f64.ln(), upper)],
222
+
cfg.marker_type,
223
+
GraphType::Line,
224
+
cfg.axis_color,
225
+
),
226
+
DataSet::new(
227
+
None,
228
+
vec![(50.0f64.ln(), lower), (50.0f64.ln(), upper)],
229
+
cfg.marker_type,
230
+
GraphType::Line,
231
+
cfg.axis_color,
232
+
),
233
+
DataSet::new(
234
+
None,
235
+
vec![(60.0f64.ln(), lower), (60.0f64.ln(), upper)],
236
+
cfg.marker_type,
237
+
GraphType::Line,
238
+
cfg.axis_color,
239
+
),
240
+
DataSet::new(
241
+
None,
242
+
vec![(70.0f64.ln(), lower), (70.0f64.ln(), upper)],
243
+
cfg.marker_type,
244
+
GraphType::Line,
245
+
cfg.axis_color,
246
+
),
247
+
DataSet::new(
248
+
None,
249
+
vec![(80.0f64.ln(), lower), (80.0f64.ln(), upper)],
250
+
cfg.marker_type,
251
+
GraphType::Line,
252
+
cfg.axis_color,
253
+
),
254
+
DataSet::new(
255
+
None,
256
+
vec![(90.0f64.ln(), lower), (90.0f64.ln(), upper)],
257
+
cfg.marker_type,
258
+
GraphType::Line,
259
+
cfg.axis_color,
260
+
),
261
+
DataSet::new(
262
+
None,
263
+
vec![(100.0f64.ln(), lower), (100.0f64.ln(), upper)],
264
+
cfg.marker_type,
265
+
GraphType::Line,
266
+
cfg.axis_color,
267
+
),
268
+
DataSet::new(
269
+
None,
270
+
vec![(200.0f64.ln(), lower), (200.0f64.ln(), upper)],
271
+
cfg.marker_type,
272
+
GraphType::Line,
273
+
cfg.axis_color,
274
+
),
275
+
DataSet::new(
276
+
None,
277
+
vec![(300.0f64.ln(), lower), (300.0f64.ln(), upper)],
278
+
cfg.marker_type,
279
+
GraphType::Line,
280
+
cfg.axis_color,
281
+
),
282
+
DataSet::new(
283
+
None,
284
+
vec![(400.0f64.ln(), lower), (400.0f64.ln(), upper)],
285
+
cfg.marker_type,
286
+
GraphType::Line,
287
+
cfg.axis_color,
288
+
),
289
+
DataSet::new(
290
+
None,
291
+
vec![(500.0f64.ln(), lower), (500.0f64.ln(), upper)],
292
+
cfg.marker_type,
293
+
GraphType::Line,
294
+
cfg.axis_color,
295
+
),
296
+
DataSet::new(
297
+
None,
298
+
vec![(600.0f64.ln(), lower), (600.0f64.ln(), upper)],
299
+
cfg.marker_type,
300
+
GraphType::Line,
301
+
cfg.axis_color,
302
+
),
303
+
DataSet::new(
304
+
None,
305
+
vec![(700.0f64.ln(), lower), (700.0f64.ln(), upper)],
306
+
cfg.marker_type,
307
+
GraphType::Line,
308
+
cfg.axis_color,
309
+
),
310
+
DataSet::new(
311
+
None,
312
+
vec![(800.0f64.ln(), lower), (800.0f64.ln(), upper)],
313
+
cfg.marker_type,
314
+
GraphType::Line,
315
+
cfg.axis_color,
316
+
),
317
+
DataSet::new(
318
+
None,
319
+
vec![(900.0f64.ln(), lower), (900.0f64.ln(), upper)],
320
+
cfg.marker_type,
321
+
GraphType::Line,
322
+
cfg.axis_color,
323
+
),
324
+
DataSet::new(
325
+
None,
326
+
vec![(1000.0f64.ln(), lower), (1000.0f64.ln(), upper)],
327
+
cfg.marker_type,
328
+
GraphType::Line,
329
+
cfg.axis_color,
330
+
),
331
+
DataSet::new(
332
+
None,
333
+
vec![(2000.0f64.ln(), lower), (2000.0f64.ln(), upper)],
334
+
cfg.marker_type,
335
+
GraphType::Line,
336
+
cfg.axis_color,
337
+
),
338
+
DataSet::new(
339
+
None,
340
+
vec![(3000.0f64.ln(), lower), (3000.0f64.ln(), upper)],
341
+
cfg.marker_type,
342
+
GraphType::Line,
343
+
cfg.axis_color,
344
+
),
345
+
DataSet::new(
346
+
None,
347
+
vec![(4000.0f64.ln(), lower), (4000.0f64.ln(), upper)],
348
+
cfg.marker_type,
349
+
GraphType::Line,
350
+
cfg.axis_color,
351
+
),
352
+
DataSet::new(
353
+
None,
354
+
vec![(5000.0f64.ln(), lower), (5000.0f64.ln(), upper)],
355
+
cfg.marker_type,
356
+
GraphType::Line,
357
+
cfg.axis_color,
358
+
),
359
+
DataSet::new(
360
+
None,
361
+
vec![(6000.0f64.ln(), lower), (6000.0f64.ln(), upper)],
362
+
cfg.marker_type,
363
+
GraphType::Line,
364
+
cfg.axis_color,
365
+
),
366
+
DataSet::new(
367
+
None,
368
+
vec![(7000.0f64.ln(), lower), (7000.0f64.ln(), upper)],
369
+
cfg.marker_type,
370
+
GraphType::Line,
371
+
cfg.axis_color,
372
+
),
373
+
DataSet::new(
374
+
None,
375
+
vec![(8000.0f64.ln(), lower), (8000.0f64.ln(), upper)],
376
+
cfg.marker_type,
377
+
GraphType::Line,
378
+
cfg.axis_color,
379
+
),
380
+
DataSet::new(
381
+
None,
382
+
vec![(9000.0f64.ln(), lower), (9000.0f64.ln(), upper)],
383
+
cfg.marker_type,
384
+
GraphType::Line,
385
+
cfg.axis_color,
386
+
),
387
+
DataSet::new(
388
+
None,
389
+
vec![(10000.0f64.ln(), lower), (10000.0f64.ln(), upper)],
390
+
cfg.marker_type,
391
+
GraphType::Line,
392
+
cfg.axis_color,
393
+
),
394
+
DataSet::new(
395
+
None,
396
+
vec![(20000.0f64.ln(), lower), (20000.0f64.ln(), upper)],
397
+
cfg.marker_type,
398
+
GraphType::Line,
399
+
cfg.axis_color,
400
+
),
401
+
]
402
+
}
403
+
}
+116
src/visualization/vectorscope.rs
+116
src/visualization/vectorscope.rs
···
1
+
use ratatui::{
2
+
style::Style,
3
+
text::Span,
4
+
widgets::{Axis, GraphType},
5
+
};
6
+
7
+
use crate::input::Matrix;
8
+
9
+
use super::{DataSet, Dimension, DisplayMode, GraphConfig};
10
+
11
+
#[derive(Default)]
12
+
pub struct Vectorscope {}
13
+
14
+
impl DisplayMode for Vectorscope {
15
+
fn from_args(_opts: &crate::cfg::SourceOptions) -> Self {
16
+
Vectorscope::default()
17
+
}
18
+
19
+
fn mode_str(&self) -> &'static str {
20
+
"vector"
21
+
}
22
+
23
+
fn channel_name(&self, index: usize) -> String {
24
+
format!("{}", index)
25
+
}
26
+
27
+
fn header(&self, _: &GraphConfig) -> String {
28
+
"live".into()
29
+
}
30
+
31
+
fn axis(&self, cfg: &GraphConfig, dimension: Dimension) -> Axis {
32
+
let (name, bounds) = match dimension {
33
+
Dimension::X => ("left -", [-cfg.scale, cfg.scale]),
34
+
Dimension::Y => ("| right", [-cfg.scale, cfg.scale]),
35
+
};
36
+
let mut a = Axis::default();
37
+
if cfg.show_ui {
38
+
// TODO don't make it necessary to check show_ui inside here
39
+
a = a.title(Span::styled(name, Style::default().fg(cfg.labels_color)));
40
+
}
41
+
a.style(Style::default().fg(cfg.axis_color)).bounds(bounds)
42
+
}
43
+
44
+
fn references(&self, cfg: &GraphConfig) -> Vec<DataSet> {
45
+
vec![
46
+
DataSet::new(
47
+
None,
48
+
vec![(-cfg.scale, 0.0), (cfg.scale, 0.0)],
49
+
cfg.marker_type,
50
+
GraphType::Line,
51
+
cfg.axis_color,
52
+
),
53
+
DataSet::new(
54
+
None,
55
+
vec![(0.0, -cfg.scale), (0.0, cfg.scale)],
56
+
cfg.marker_type,
57
+
GraphType::Line,
58
+
cfg.axis_color,
59
+
),
60
+
]
61
+
}
62
+
63
+
fn process(&mut self, cfg: &GraphConfig, data: &Matrix<f64>) -> Vec<DataSet> {
64
+
let mut out = Vec::new();
65
+
66
+
for (n, chunk) in data.chunks(2).enumerate() {
67
+
let mut tmp = vec![];
68
+
match chunk.len() {
69
+
2 => {
70
+
for i in 0..std::cmp::min(chunk[0].len(), chunk[1].len()) {
71
+
if i > cfg.samples as usize {
72
+
break;
73
+
}
74
+
tmp.push((chunk[0][i], chunk[1][i]));
75
+
}
76
+
}
77
+
1 => {
78
+
for i in 0..chunk[0].len() {
79
+
if i > cfg.samples as usize {
80
+
break;
81
+
}
82
+
tmp.push((chunk[0][i], i as f64));
83
+
}
84
+
}
85
+
_ => continue,
86
+
}
87
+
// split it in two for easier coloring
88
+
// TODO configure splitting in multiple parts?
89
+
let pivot = tmp.len() / 2;
90
+
out.push(DataSet::new(
91
+
Some(self.channel_name((n * 2) + 1)),
92
+
tmp[pivot..].to_vec(),
93
+
cfg.marker_type,
94
+
if cfg.scatter {
95
+
GraphType::Scatter
96
+
} else {
97
+
GraphType::Line
98
+
},
99
+
cfg.palette((n * 2) + 1),
100
+
));
101
+
out.push(DataSet::new(
102
+
Some(self.channel_name(n * 2)),
103
+
tmp[..pivot].to_vec(),
104
+
cfg.marker_type,
105
+
if cfg.scatter {
106
+
GraphType::Scatter
107
+
} else {
108
+
GraphType::Line
109
+
},
110
+
cfg.palette(n * 2),
111
+
));
112
+
}
113
+
114
+
out
115
+
}
116
+
}