+1
-1
Cargo.toml
+1
-1
Cargo.toml
+260
-50
src/app.rs
+260
-50
src/app.rs
···
1
-
use crossterm::event::{self, Event, KeyCode, KeyModifiers};
1
+
use crossterm::event::{self, Event, KeyCode, KeyModifiers, MediaKeyCode};
2
2
use ratatui::{
3
3
prelude::*,
4
4
widgets::{block::*, *},
···
6
6
use std::{
7
7
io,
8
8
ops::Range,
9
-
process,
10
9
sync::{mpsc::Receiver, Arc, Mutex},
11
10
thread,
12
11
time::{Duration, Instant},
13
12
};
14
-
use tokio::sync::mpsc::UnboundedReceiver;
13
+
use tokio::sync::mpsc::{UnboundedReceiver, UnboundedSender};
15
14
16
15
use crate::{
17
16
extract::get_currently_playing,
18
17
input::stream_to_matrix,
18
+
play::SinkCommand,
19
19
tui,
20
20
visualization::{
21
21
oscilloscope::Oscilloscope, spectroscope::Spectroscope, vectorscope::Vectorscope,
···
30
30
pub genre: String,
31
31
pub description: String,
32
32
pub br: String,
33
+
/// [`Volume`].
34
+
pub volume: Volume,
35
+
}
36
+
37
+
/// Volume of the player.
38
+
#[derive(Debug, Clone, PartialEq)]
39
+
pub struct Volume {
40
+
/// Raw volume stored as percentage.
41
+
raw_volume_percent: f32,
42
+
/// Is muted?
43
+
is_muted: bool,
44
+
}
45
+
46
+
impl Volume {
47
+
/// Create a new [`Volume`].
48
+
pub const fn new(raw_volume_percent: f32, is_muted: bool) -> Self {
49
+
Self {
50
+
raw_volume_percent,
51
+
is_muted,
52
+
}
53
+
}
54
+
55
+
/// Get the current volume ratio. Returns `0.0` if muted.
56
+
pub const fn volume_ratio(&self) -> f32 {
57
+
if self.is_muted {
58
+
0.0
59
+
} else {
60
+
self.raw_volume_percent / 100.0
61
+
}
62
+
}
63
+
64
+
/// Get the raw volume percent.
65
+
pub const fn raw_volume_percent(&self) -> f32 {
66
+
self.raw_volume_percent
67
+
}
68
+
69
+
/// Is volume muted?
70
+
pub const fn is_muted(&self) -> bool {
71
+
self.is_muted
72
+
}
73
+
74
+
/// Toggle mute.
75
+
pub const fn toggle_mute(&mut self) {
76
+
self.is_muted = !self.is_muted;
77
+
}
78
+
79
+
/// Change the volume by the given step percent.
80
+
///
81
+
/// To increase the volume, use a positive step. To decrease the
82
+
/// volume, use a negative step.
83
+
pub const fn change_volume(&mut self, step_percent: f32) {
84
+
self.raw_volume_percent += step_percent;
85
+
// limit to 0 volume, no upper bound
86
+
self.raw_volume_percent = self.raw_volume_percent.max(0.0);
87
+
}
88
+
}
89
+
90
+
impl Default for Volume {
91
+
fn default() -> Self {
92
+
Self::new(100.0, false)
93
+
}
33
94
}
34
95
35
96
pub enum CurrentDisplayMode {
36
97
Oscilloscope,
37
98
Vectorscope,
38
99
Spectroscope,
100
+
None,
39
101
}
40
102
103
+
impl std::str::FromStr for CurrentDisplayMode {
104
+
type Err = InvalidDisplayModeError;
105
+
106
+
fn from_str(s: &str) -> Result<Self, Self::Err> {
107
+
match s {
108
+
"Oscilloscope" => Ok(Self::Oscilloscope),
109
+
"Vectorscope" => Ok(Self::Vectorscope),
110
+
"Spectroscope" => Ok(Self::Spectroscope),
111
+
"None" => Ok(Self::None),
112
+
_ => Err(InvalidDisplayModeError),
113
+
}
114
+
}
115
+
}
116
+
117
+
/// Invalid display mode error.
118
+
#[derive(Debug)]
119
+
pub struct InvalidDisplayModeError;
120
+
121
+
impl std::fmt::Display for InvalidDisplayModeError {
122
+
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
123
+
write!(f, "invalid display mode")
124
+
}
125
+
}
126
+
127
+
impl std::error::Error for InvalidDisplayModeError {}
128
+
41
129
pub struct App {
42
130
#[allow(unused)]
43
131
channels: u8,
···
54
142
ui: &crate::cfg::UiOptions,
55
143
source: &crate::cfg::SourceOptions,
56
144
frame_rx: Receiver<minimp3::Frame>,
145
+
mode: CurrentDisplayMode,
57
146
) -> Self {
58
147
let graph = GraphConfig {
59
148
axis_color: Color::DarkGray,
···
83
172
oscilloscope,
84
173
vectorscope,
85
174
spectroscope,
86
-
mode: CurrentDisplayMode::Spectroscope,
175
+
mode,
87
176
channels: source.channels as u8,
88
177
frame_rx,
89
178
}
···
178
267
},
179
268
frame,
180
269
);
270
+
render_line(
271
+
"Volume ",
272
+
&if state.volume.is_muted() {
273
+
format!("{}% muted", state.volume.raw_volume_percent())
274
+
} else {
275
+
format!("{}%", state.volume.raw_volume_percent())
276
+
},
277
+
Rect {
278
+
x: size.x,
279
+
y: match state.now_playing.is_empty() {
280
+
true => size.y + 5,
281
+
false => size.y + 6,
282
+
},
283
+
width: size.width,
284
+
height: 1,
285
+
},
286
+
frame,
287
+
)
181
288
}
182
289
183
290
fn render_line(label: &str, value: &str, area: Rect, frame: &mut Frame) {
···
196
303
&mut self,
197
304
terminal: &mut tui::Tui,
198
305
mut cmd_rx: UnboundedReceiver<State>,
306
+
mut sink_cmd_tx: UnboundedSender<SinkCommand>,
199
307
id: &str,
200
308
) {
201
309
let new_state = cmd_rx.recv().await.unwrap();
···
220
328
let mut last_poll = Instant::now();
221
329
222
330
loop {
223
-
let audio_frame = self.frame_rx.recv().unwrap();
224
-
let channels =
225
-
stream_to_matrix(audio_frame.data.iter().cloned(), audio_frame.channels, 1.);
331
+
let channels = (!self.graph.pause)
332
+
.then(|| self.frame_rx.recv().unwrap())
333
+
.map(|audio_frame| {
334
+
stream_to_matrix(audio_frame.data.iter().cloned(), audio_frame.channels, 1.)
335
+
});
226
336
227
337
fps += 1;
228
338
···
236
346
let mut datasets = Vec::new();
237
347
let graph = self.graph.clone(); // TODO cheap fix...
238
348
if self.graph.references {
239
-
datasets.append(&mut self.current_display_mut().references(&graph));
349
+
if let Some(current_display) = self.current_display() {
350
+
datasets.append(&mut current_display.references(&graph));
351
+
}
352
+
}
353
+
if let Some((current_display, channels)) = self.current_display_mut().zip(channels)
354
+
{
355
+
datasets.append(&mut current_display.process(&graph, &channels));
240
356
}
241
-
datasets.append(&mut self.current_display_mut().process(&graph, &channels));
242
357
terminal
243
358
.draw(|f| {
244
359
let mut size = f.size();
245
360
render_frame(new_state.clone(), f);
246
-
if self.graph.show_ui {
247
-
f.render_widget(
248
-
make_header(
249
-
&self.graph,
250
-
&self.current_display().header(&self.graph),
251
-
self.current_display().mode_str(),
252
-
framerate,
253
-
self.graph.pause,
254
-
),
255
-
Rect {
256
-
x: size.x,
257
-
y: size.y + 6,
258
-
width: size.width,
259
-
height: 1,
260
-
},
261
-
);
262
-
size.height -= 7;
263
-
size.y += 7;
361
+
if let Some(current_display) = self.current_display() {
362
+
if self.graph.show_ui {
363
+
f.render_widget(
364
+
make_header(
365
+
&self.graph,
366
+
¤t_display.header(&self.graph),
367
+
current_display.mode_str(),
368
+
framerate,
369
+
self.graph.pause,
370
+
),
371
+
Rect {
372
+
x: size.x,
373
+
y: size.y + 7,
374
+
width: size.width,
375
+
height: 1,
376
+
},
377
+
);
378
+
size.height -= 8;
379
+
size.y += 8;
380
+
}
381
+
let chart = Chart::new(datasets.iter().map(|x| x.into()).collect())
382
+
.x_axis(current_display.axis(&self.graph, Dimension::X)) // TODO allow to have axis sometimes?
383
+
.y_axis(current_display.axis(&self.graph, Dimension::Y));
384
+
f.render_widget(chart, size)
264
385
}
265
-
let chart = Chart::new(datasets.iter().map(|x| x.into()).collect())
266
-
.x_axis(self.current_display().axis(&self.graph, Dimension::X)) // TODO allow to have axis sometimes?
267
-
.y_axis(self.current_display().axis(&self.graph, Dimension::Y));
268
-
f.render_widget(chart, size)
269
386
})
270
387
.unwrap();
271
388
}
···
274
391
// process all enqueued events
275
392
let event = event::read().unwrap();
276
393
277
-
if self.process_events(event.clone()).unwrap() {
394
+
if self
395
+
.process_events(event.clone(), new_state.clone(), &mut sink_cmd_tx)
396
+
.unwrap()
397
+
{
278
398
return;
279
399
}
280
-
self.current_display_mut().handle(event);
400
+
if let Some(current_display) = self.current_display_mut() {
401
+
current_display.handle(event);
402
+
}
281
403
}
282
404
}
283
405
}
284
406
285
-
fn current_display_mut(&mut self) -> &mut dyn DisplayMode {
407
+
fn current_display_mut(&mut self) -> Option<&mut dyn DisplayMode> {
286
408
match self.mode {
287
-
CurrentDisplayMode::Oscilloscope => &mut self.oscilloscope as &mut dyn DisplayMode,
288
-
CurrentDisplayMode::Vectorscope => &mut self.vectorscope as &mut dyn DisplayMode,
289
-
CurrentDisplayMode::Spectroscope => &mut self.spectroscope as &mut dyn DisplayMode,
409
+
CurrentDisplayMode::Oscilloscope => {
410
+
Some(&mut self.oscilloscope as &mut dyn DisplayMode)
411
+
}
412
+
CurrentDisplayMode::Vectorscope => Some(&mut self.vectorscope as &mut dyn DisplayMode),
413
+
CurrentDisplayMode::Spectroscope => {
414
+
Some(&mut self.spectroscope as &mut dyn DisplayMode)
415
+
}
416
+
CurrentDisplayMode::None => None,
290
417
}
291
418
}
292
419
293
-
fn current_display(&self) -> &dyn DisplayMode {
420
+
fn current_display(&self) -> Option<&dyn DisplayMode> {
294
421
match self.mode {
295
-
CurrentDisplayMode::Oscilloscope => &self.oscilloscope as &dyn DisplayMode,
296
-
CurrentDisplayMode::Vectorscope => &self.vectorscope as &dyn DisplayMode,
297
-
CurrentDisplayMode::Spectroscope => &self.spectroscope as &dyn DisplayMode,
422
+
CurrentDisplayMode::Oscilloscope => Some(&self.oscilloscope as &dyn DisplayMode),
423
+
CurrentDisplayMode::Vectorscope => Some(&self.vectorscope as &dyn DisplayMode),
424
+
CurrentDisplayMode::Spectroscope => Some(&self.spectroscope as &dyn DisplayMode),
425
+
CurrentDisplayMode::None => None,
298
426
}
299
427
}
300
428
301
-
fn process_events(&mut self, event: Event) -> Result<bool, io::Error> {
429
+
fn process_events(
430
+
&mut self,
431
+
event: Event,
432
+
state: Arc<Mutex<State>>,
433
+
sink_cmd_tx: &mut UnboundedSender<SinkCommand>,
434
+
) -> Result<bool, io::Error> {
302
435
let mut quit = false;
436
+
437
+
let play = |graph: &mut GraphConfig| {
438
+
graph.pause = false;
439
+
sink_cmd_tx
440
+
.send(SinkCommand::Play)
441
+
.expect("receiver never dropped");
442
+
};
443
+
444
+
let pause = |graph: &mut GraphConfig| {
445
+
graph.pause = true;
446
+
sink_cmd_tx
447
+
.send(SinkCommand::Pause)
448
+
.expect("receiver never dropped");
449
+
};
450
+
451
+
let toggle_play_pause = |graph: &mut GraphConfig| {
452
+
graph.pause = !graph.pause;
453
+
let sink_cmd = if graph.pause {
454
+
SinkCommand::Pause
455
+
} else {
456
+
SinkCommand::Play
457
+
};
458
+
sink_cmd_tx.send(sink_cmd).expect("receiver never dropped");
459
+
};
460
+
461
+
let lower_volume = || {
462
+
let mut state = state.lock().unwrap();
463
+
state.volume.change_volume(-1.0);
464
+
sink_cmd_tx
465
+
.send(SinkCommand::SetVolume(state.volume.volume_ratio()))
466
+
.expect("receiver never dropped");
467
+
};
468
+
469
+
let raise_volume = || {
470
+
let mut state = state.lock().unwrap();
471
+
state.volume.change_volume(1.0);
472
+
sink_cmd_tx
473
+
.send(SinkCommand::SetVolume(state.volume.volume_ratio()))
474
+
.expect("receiver never dropped");
475
+
};
476
+
477
+
let mute_volume = || {
478
+
let mut state = state.lock().unwrap();
479
+
state.volume.toggle_mute();
480
+
sink_cmd_tx
481
+
.send(SinkCommand::SetVolume(state.volume.volume_ratio()))
482
+
.expect("receiver never dropped");
483
+
};
484
+
303
485
if let Event::Key(key) = event {
304
486
if let KeyModifiers::CONTROL = key.modifiers {
305
487
match key.code {
···
315
497
_ => 1.0,
316
498
};
317
499
match key.code {
318
-
KeyCode::Up => update_value_f(&mut self.graph.scale, 0.01, magnitude, 0.0..10.0), // inverted to act as zoom
319
-
KeyCode::Down => update_value_f(&mut self.graph.scale, -0.01, magnitude, 0.0..10.0), // inverted to act as zoom
500
+
KeyCode::Up => {
501
+
// inverted to act as zoom
502
+
update_value_f(&mut self.graph.scale, 0.01, magnitude, 0.0..10.0);
503
+
raise_volume();
504
+
}
505
+
KeyCode::Down => {
506
+
// inverted to act as zoom
507
+
update_value_f(&mut self.graph.scale, -0.01, magnitude, 0.0..10.0);
508
+
lower_volume();
509
+
}
320
510
KeyCode::Right => update_value_i(
321
511
&mut self.graph.samples,
322
512
true,
···
332
522
0..self.graph.width * 2,
333
523
),
334
524
KeyCode::Char('q') => quit = true,
335
-
KeyCode::Char(' ') => self.graph.pause = !self.graph.pause,
525
+
KeyCode::Char(' ') => toggle_play_pause(&mut self.graph),
336
526
KeyCode::Char('s') => self.graph.scatter = !self.graph.scatter,
337
527
KeyCode::Char('h') => self.graph.show_ui = !self.graph.show_ui,
338
528
KeyCode::Char('r') => self.graph.references = !self.graph.references,
529
+
KeyCode::Char('m') => mute_volume(),
339
530
KeyCode::Esc => {
340
531
self.graph.samples = self.graph.width;
341
532
self.graph.scale = 1.;
342
533
}
343
534
KeyCode::Char('c') if key.modifiers.contains(KeyModifiers::CONTROL) => {
344
-
let _ = tui::restore();
345
-
process::exit(0);
535
+
quit = true;
346
536
}
347
537
KeyCode::Tab => {
348
538
// switch modes
349
539
match self.mode {
350
540
CurrentDisplayMode::Oscilloscope => {
351
-
self.mode = CurrentDisplayMode::Vectorscope
541
+
self.mode = CurrentDisplayMode::Vectorscope;
352
542
}
353
543
CurrentDisplayMode::Vectorscope => {
354
-
self.mode = CurrentDisplayMode::Spectroscope
544
+
self.mode = CurrentDisplayMode::Spectroscope;
355
545
}
356
546
CurrentDisplayMode::Spectroscope => {
357
-
self.mode = CurrentDisplayMode::Oscilloscope
547
+
self.mode = CurrentDisplayMode::None;
548
+
}
549
+
CurrentDisplayMode::None => {
550
+
self.mode = CurrentDisplayMode::Oscilloscope;
358
551
}
359
552
}
360
553
}
554
+
KeyCode::Media(media_key_code) => match media_key_code {
555
+
MediaKeyCode::Play => play(&mut self.graph),
556
+
MediaKeyCode::Pause => pause(&mut self.graph),
557
+
MediaKeyCode::PlayPause => toggle_play_pause(&mut self.graph),
558
+
MediaKeyCode::Stop => {
559
+
quit = true;
560
+
}
561
+
MediaKeyCode::LowerVolume => lower_volume(),
562
+
MediaKeyCode::RaiseVolume => raise_volume(),
563
+
MediaKeyCode::MuteVolume => mute_volume(),
564
+
MediaKeyCode::TrackNext
565
+
| MediaKeyCode::TrackPrevious
566
+
| MediaKeyCode::Reverse
567
+
| MediaKeyCode::FastForward
568
+
| MediaKeyCode::Rewind
569
+
| MediaKeyCode::Record => {}
570
+
},
361
571
_ => {}
362
572
}
363
573
};
+11
-2
src/main.rs
+11
-2
src/main.rs
···
1
1
use anyhow::Error;
2
+
use app::CurrentDisplayMode;
2
3
use clap::{arg, Command};
3
4
4
5
mod app;
···
45
46
.subcommand(
46
47
Command::new("play")
47
48
.about("Play a radio station")
48
-
.arg(arg!(<station> "The station to play")),
49
+
.arg(arg!(<station> "The station to play"))
50
+
.arg(arg!(--volume "Set the initial volume (as a percent)").default_value("100"))
51
+
.arg(clap::Arg::new("display-mode").long("display-mode").help("Set the display mode to start with").default_value("Spectroscope")),
49
52
)
50
53
.subcommand(
51
54
Command::new("browse")
···
90
93
Some(("play", args)) => {
91
94
let station = args.value_of("station").unwrap();
92
95
let provider = matches.value_of("provider").unwrap();
93
-
play::exec(station, provider).await?;
96
+
let volume = args.value_of("volume").unwrap().parse::<f32>().unwrap();
97
+
let display_mode = args
98
+
.value_of("display-mode")
99
+
.unwrap()
100
+
.parse::<CurrentDisplayMode>()
101
+
.unwrap();
102
+
play::exec(station, provider, volume, display_mode).await?;
94
103
}
95
104
Some(("browse", args)) => {
96
105
let category = args.value_of("category");
+38
-4
src/play.rs
+38
-4
src/play.rs
···
4
4
use hyper::header::HeaderValue;
5
5
6
6
use crate::{
7
-
app::{App, State},
7
+
app::{App, CurrentDisplayMode, State, Volume},
8
8
cfg::{SourceOptions, UiOptions},
9
9
decoder::Mp3Decoder,
10
10
provider::{radiobrowser::Radiobrowser, tunein::Tunein, Provider},
11
11
tui,
12
12
};
13
13
14
-
pub async fn exec(name_or_id: &str, provider: &str) -> Result<(), Error> {
14
+
pub async fn exec(
15
+
name_or_id: &str,
16
+
provider: &str,
17
+
volume: f32,
18
+
display_mode: CurrentDisplayMode,
19
+
) -> Result<(), Error> {
15
20
let _provider = provider;
16
21
let provider: Box<dyn Provider> = match provider {
17
22
"tunein" => Box::new(Tunein::new()),
···
34
39
let now_playing = station.playing.clone().unwrap_or_default();
35
40
36
41
let (cmd_tx, cmd_rx) = tokio::sync::mpsc::unbounded_channel::<State>();
42
+
let (sink_cmd_tx, mut sink_cmd_rx) = tokio::sync::mpsc::unbounded_channel::<SinkCommand>();
37
43
let (frame_tx, frame_rx) = std::sync::mpsc::channel::<minimp3::Frame>();
38
44
39
45
let ui = UiOptions {
···
51
57
tune: None,
52
58
};
53
59
54
-
let mut app = App::new(&ui, &opts, frame_rx);
60
+
let mut app = App::new(&ui, &opts, frame_rx, display_mode);
55
61
let station_name = station.name.clone();
56
62
57
63
thread::spawn(move || {
···
60
66
let response = client.get(stream_url).send().unwrap();
61
67
62
68
let headers = response.headers();
69
+
let volume = Volume::new(volume, false);
70
+
63
71
cmd_tx
64
72
.send(State {
65
73
name: match headers
···
90
98
.to_str()
91
99
.unwrap()
92
100
.to_string(),
101
+
volume: volume.clone(),
93
102
})
94
103
.unwrap();
95
104
let location = response.headers().get("location");
···
108
117
109
118
let (_stream, handle) = rodio::OutputStream::try_default().unwrap();
110
119
let sink = rodio::Sink::try_new(&handle).unwrap();
120
+
sink.set_volume(volume.volume_ratio());
111
121
let decoder = Mp3Decoder::new(response, frame_tx).unwrap();
112
122
sink.append(decoder);
113
123
114
124
loop {
125
+
while let Ok(sink_cmd) = sink_cmd_rx.try_recv() {
126
+
match sink_cmd {
127
+
SinkCommand::Play => {
128
+
sink.play();
129
+
}
130
+
SinkCommand::Pause => {
131
+
sink.pause();
132
+
}
133
+
SinkCommand::SetVolume(volume) => {
134
+
sink.set_volume(volume);
135
+
}
136
+
}
137
+
}
115
138
std::thread::sleep(Duration::from_millis(10));
116
139
}
117
140
});
118
141
119
142
let mut terminal = tui::init()?;
120
-
app.run(&mut terminal, cmd_rx, &id).await;
143
+
app.run(&mut terminal, cmd_rx, sink_cmd_tx, &id).await;
121
144
tui::restore()?;
122
145
Ok(())
123
146
}
147
+
148
+
/// Command for a sink.
149
+
#[derive(Debug, Clone, PartialEq)]
150
+
pub enum SinkCommand {
151
+
/// Play.
152
+
Play,
153
+
/// Pause.
154
+
Pause,
155
+
/// Set the volume.
156
+
SetVolume(f32),
157
+
}
+18
-1
src/tui.rs
+18
-1
src/tui.rs
···
1
1
use std::io::{self, stderr, stdout, Stdout};
2
2
3
-
use crossterm::{execute, terminal::*};
3
+
use crossterm::{
4
+
event::{KeyboardEnhancementFlags, PopKeyboardEnhancementFlags, PushKeyboardEnhancementFlags},
5
+
execute,
6
+
terminal::*,
7
+
};
4
8
use ratatui::prelude::*;
5
9
6
10
/// A type alias for the terminal type used in this application
···
10
14
pub fn init() -> io::Result<Tui> {
11
15
execute!(stdout(), EnterAlternateScreen)?;
12
16
execute!(stderr(), EnterAlternateScreen)?;
17
+
if let Ok(true) = crossterm::terminal::supports_keyboard_enhancement() {
18
+
execute!(
19
+
stdout(),
20
+
PushKeyboardEnhancementFlags(KeyboardEnhancementFlags::DISAMBIGUATE_ESCAPE_CODES)
21
+
)?;
22
+
}
23
+
13
24
enable_raw_mode()?;
25
+
14
26
Terminal::new(CrosstermBackend::new(stdout()))
15
27
}
16
28
···
18
30
pub fn restore() -> io::Result<()> {
19
31
execute!(stdout(), LeaveAlternateScreen)?;
20
32
execute!(stderr(), LeaveAlternateScreen)?;
33
+
if let Ok(true) = crossterm::terminal::supports_keyboard_enhancement() {
34
+
execute!(stdout(), PopKeyboardEnhancementFlags)?;
35
+
}
36
+
21
37
disable_raw_mode()?;
38
+
22
39
Ok(())
23
40
}