Browse and listen to thousands of radio stations across the globe right from your terminal ๐ŸŒŽ ๐Ÿ“ป ๐ŸŽตโœจ
radio rust tokio web-radio command-line-tool tui
at main 30 kB view raw
1use crossterm::event::{self, Event, KeyCode, KeyModifiers, MediaKeyCode}; 2use ratatui::{ 3 prelude::*, 4 widgets::{block::*, *}, 5}; 6use souvlaki::MediaControlEvent; 7use std::{ 8 io, 9 ops::Range, 10 sync::{mpsc::Receiver, Arc, Mutex}, 11 thread, 12 time::{Duration, Instant}, 13}; 14use tokio::sync::mpsc::{UnboundedReceiver, UnboundedSender}; 15use tunein_cli::os_media_controls::{self, OsMediaControls}; 16 17use crate::{ 18 extract::get_currently_playing, 19 input::stream_to_matrix, 20 play::SinkCommand, 21 tui, 22 visualization::{ 23 oscilloscope::Oscilloscope, spectroscope::Spectroscope, vectorscope::Vectorscope, 24 Dimension, DisplayMode, GraphConfig, 25 }, 26}; 27 28#[derive(Debug, Default, Clone)] 29pub struct State { 30 pub name: String, 31 pub now_playing: String, 32 pub genre: String, 33 pub description: String, 34 pub br: String, 35 /// [`Volume`]. 36 pub volume: Volume, 37} 38 39/// Volume of the player. 40#[derive(Debug, Clone, PartialEq)] 41pub struct Volume { 42 /// Raw volume stored as percentage. 43 raw_volume_percent: f32, 44 /// Is muted? 45 is_muted: bool, 46} 47 48impl Volume { 49 /// Create a new [`Volume`]. 50 pub const fn new(raw_volume_percent: f32, is_muted: bool) -> Self { 51 Self { 52 raw_volume_percent, 53 is_muted, 54 } 55 } 56 57 /// Get the current volume ratio. Returns `0.0` if muted. 58 pub const fn volume_ratio(&self) -> f32 { 59 if self.is_muted { 60 0.0 61 } else { 62 self.raw_volume_percent / 100.0 63 } 64 } 65 66 /// Get the raw volume percent. 67 pub const fn raw_volume_percent(&self) -> f32 { 68 self.raw_volume_percent 69 } 70 71 /// Is volume muted? 72 pub const fn is_muted(&self) -> bool { 73 self.is_muted 74 } 75 76 /// Toggle mute. 77 pub const fn toggle_mute(&mut self) { 78 self.is_muted = !self.is_muted; 79 } 80 81 /// Set the volume to the given volume ratio. 82 /// 83 /// `1.0` is 100% volume. 84 pub const fn set_volume_ratio(&mut self, volume: f32) { 85 self.raw_volume_percent = volume * 100.0; 86 self.raw_volume_percent = self.raw_volume_percent.max(0.0); 87 } 88 89 /// Change the volume by the given step percent. 90 /// 91 /// To increase the volume, use a positive step. To decrease the 92 /// volume, use a negative step. 93 pub const fn change_volume(&mut self, step_percent: f32) { 94 self.raw_volume_percent += step_percent; 95 // limit to 0 volume, no upper bound 96 self.raw_volume_percent = self.raw_volume_percent.max(0.0); 97 } 98} 99 100impl Default for Volume { 101 fn default() -> Self { 102 Self::new(100.0, false) 103 } 104} 105 106pub enum CurrentDisplayMode { 107 Oscilloscope, 108 Vectorscope, 109 Spectroscope, 110 None, 111} 112 113impl std::str::FromStr for CurrentDisplayMode { 114 type Err = InvalidDisplayModeError; 115 116 fn from_str(s: &str) -> Result<Self, Self::Err> { 117 match s { 118 "Oscilloscope" => Ok(Self::Oscilloscope), 119 "Vectorscope" => Ok(Self::Vectorscope), 120 "Spectroscope" => Ok(Self::Spectroscope), 121 "None" => Ok(Self::None), 122 _ => Err(InvalidDisplayModeError), 123 } 124 } 125} 126 127/// Invalid display mode error. 128#[derive(Debug)] 129pub struct InvalidDisplayModeError; 130 131impl std::fmt::Display for InvalidDisplayModeError { 132 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 133 write!(f, "invalid display mode") 134 } 135} 136 137impl std::error::Error for InvalidDisplayModeError {} 138 139pub struct App { 140 #[allow(unused)] 141 channels: u8, 142 graph: GraphConfig, 143 oscilloscope: Oscilloscope, 144 vectorscope: Vectorscope, 145 spectroscope: Spectroscope, 146 mode: CurrentDisplayMode, 147 frame_rx: Receiver<minimp3::Frame>, 148 /// [`OsMediaControls`]. 149 os_media_controls: Option<OsMediaControls>, 150 /// Poll for events every specified [`Duration`]. 151 /// 152 /// Allows user to decide the trade off between computational 153 /// resource comsumption, animation smoothness and how responsive 154 /// the application. Smaller durations lead to more resource 155 /// consumption but smoother animations and better responsiveness. 156 poll_events_every: Duration, 157 /// [`Self::poll_events_every`] but when player is paused. 158 /// 159 /// This should generally be larger than 160 /// [`Self::poll_events_every`]. 161 poll_events_every_while_paused: Duration, 162} 163 164impl App { 165 pub fn new( 166 ui: &crate::cfg::UiOptions, 167 source: &crate::cfg::SourceOptions, 168 frame_rx: Receiver<minimp3::Frame>, 169 mode: CurrentDisplayMode, 170 os_media_controls: Option<OsMediaControls>, 171 poll_events_every: Duration, 172 poll_events_every_while_paused: Duration, 173 ) -> Self { 174 let graph = GraphConfig { 175 axis_color: Color::DarkGray, 176 labels_color: Color::Cyan, 177 palette: vec![Color::Red, Color::Yellow, Color::Green, Color::Magenta], 178 scale: ui.scale as f64, 179 width: source.buffer, // TODO also make bit depth customizable 180 samples: source.buffer, 181 sampling_rate: source.sample_rate, 182 references: !ui.no_reference, 183 show_ui: !ui.no_ui, 184 scatter: ui.scatter, 185 pause: false, 186 marker_type: if ui.no_braille { 187 Marker::Dot 188 } else { 189 Marker::Braille 190 }, 191 }; 192 193 let oscilloscope = Oscilloscope::from_args(source); 194 let vectorscope = Vectorscope::from_args(source); 195 let spectroscope = Spectroscope::from_args(source); 196 197 Self { 198 graph, 199 oscilloscope, 200 vectorscope, 201 spectroscope, 202 mode, 203 channels: source.channels as u8, 204 frame_rx, 205 os_media_controls, 206 poll_events_every, 207 poll_events_every_while_paused, 208 } 209 } 210} 211 212fn render_frame(state: Arc<Mutex<State>>, frame: &mut Frame) { 213 let state = state.lock().unwrap(); 214 let size = frame.size(); 215 216 frame.render_widget( 217 Block::new() 218 .borders(Borders::TOP) 219 .title(" TuneIn CLI ") 220 .title_alignment(Alignment::Center), 221 Rect { 222 x: size.x, 223 y: size.y, 224 width: size.width, 225 height: 1, 226 }, 227 ); 228 229 render_line( 230 "Station ", 231 &state.name, 232 Rect { 233 x: size.x, 234 y: size.y + 1, 235 width: size.width, 236 height: 1, 237 }, 238 frame, 239 ); 240 241 if !state.now_playing.is_empty() { 242 render_line( 243 "Now Playing ", 244 &state.now_playing, 245 Rect { 246 x: size.x, 247 y: size.y + 2, 248 width: size.width, 249 height: 1, 250 }, 251 frame, 252 ); 253 } 254 255 render_line( 256 "Genre ", 257 &state.genre, 258 Rect { 259 x: size.x, 260 y: match state.now_playing.is_empty() { 261 true => size.y + 2, 262 false => size.y + 3, 263 }, 264 width: size.width, 265 height: 1, 266 }, 267 frame, 268 ); 269 render_line( 270 "Description ", 271 &state.description, 272 Rect { 273 x: size.x, 274 y: match state.now_playing.is_empty() { 275 true => size.y + 3, 276 false => size.y + 4, 277 }, 278 width: size.width, 279 height: 1, 280 }, 281 frame, 282 ); 283 render_line( 284 "Bitrate ", 285 &match state.br.is_empty() { 286 true => "Unknown".to_string(), 287 false => format!("{} kbps", &state.br), 288 }, 289 Rect { 290 x: size.x, 291 y: match state.now_playing.is_empty() { 292 true => size.y + 4, 293 false => size.y + 5, 294 }, 295 width: size.width, 296 height: 1, 297 }, 298 frame, 299 ); 300 render_line( 301 "Volume ", 302 &if state.volume.is_muted() { 303 format!("{}% muted", state.volume.raw_volume_percent()) 304 } else { 305 format!("{}%", state.volume.raw_volume_percent()) 306 }, 307 Rect { 308 x: size.x, 309 y: match state.now_playing.is_empty() { 310 true => size.y + 5, 311 false => size.y + 6, 312 }, 313 width: size.width, 314 height: 1, 315 }, 316 frame, 317 ) 318} 319 320fn render_line(label: &str, value: &str, area: Rect, frame: &mut Frame) { 321 let span1 = Span::styled(label, Style::new().fg(Color::LightBlue)); 322 let span2 = Span::raw(value); 323 324 let line = Line::from(vec![span1, span2]); 325 let text: Text = Text::from(vec![line]); 326 327 frame.render_widget(Paragraph::new(text), area); 328} 329 330impl App { 331 /// runs the application's main loop until the user quits 332 pub async fn run( 333 &mut self, 334 terminal: &mut tui::Tui, 335 mut cmd_rx: UnboundedReceiver<State>, 336 mut sink_cmd_tx: UnboundedSender<SinkCommand>, 337 id: &str, 338 ) { 339 let new_state = cmd_rx.recv().await.unwrap(); 340 341 let now_playing = new_state.now_playing.clone(); 342 let name = new_state.name.clone(); 343 // Report initial metadata to OS 344 send_os_media_controls_command( 345 self.os_media_controls.as_mut(), 346 os_media_controls::Command::SetMetadata(souvlaki::MediaMetadata { 347 title: (!now_playing.is_empty()).then(|| now_playing.as_str()), 348 album: (!name.is_empty()).then(|| name.as_str()), 349 artist: None, 350 cover_url: None, 351 duration: None, 352 }), 353 ); 354 // Report started playing to OS 355 send_os_media_controls_command( 356 self.os_media_controls.as_mut(), 357 os_media_controls::Command::Play, 358 ); 359 // Report volume to OS 360 send_os_media_controls_command( 361 self.os_media_controls.as_mut(), 362 os_media_controls::Command::SetVolume(new_state.volume.volume_ratio() as f64), 363 ); 364 365 let new_state = Arc::new(Mutex::new(new_state)); 366 let id = id.to_string(); 367 let new_state_clone = new_state.clone(); 368 369 // Background thread to update now_playing 370 thread::spawn(move || { 371 let rt = tokio::runtime::Runtime::new().unwrap(); 372 rt.block_on(async { 373 loop { 374 let mut new_state = new_state_clone.lock().unwrap(); 375 // Get current playing if available, otherwise use default 376 let now_playing = get_currently_playing(&id).await.unwrap_or_default(); 377 if new_state.now_playing != now_playing { 378 new_state.now_playing = now_playing; 379 } 380 drop(new_state); 381 std::thread::sleep(Duration::from_millis(10000)); 382 } 383 }); 384 }); 385 386 let mut fps = 0; 387 let mut framerate = 0; 388 let mut last_poll = Instant::now(); 389 let mut last_metadata_update = Instant::now(); 390 let mut last_now_playing = String::new(); 391 const METADATA_UPDATE_INTERVAL: Duration = Duration::from_secs(1); // Check every second 392 393 loop { 394 let channels = if self.graph.pause { 395 None 396 } else { 397 let Ok(audio_frame) = self.frame_rx.recv() else { 398 // other thread has closed so application has closed 399 return; 400 }; 401 Some(stream_to_matrix( 402 audio_frame.data.iter().cloned(), 403 audio_frame.channels, 404 1., 405 )) 406 }; 407 408 fps += 1; 409 410 if last_poll.elapsed().as_secs() >= 1 { 411 framerate = fps; 412 fps = 0; 413 last_poll = Instant::now(); 414 } 415 416 { 417 let mut datasets = Vec::new(); 418 let graph = self.graph.clone(); // TODO cheap fix... 419 if self.graph.references { 420 if let Some(current_display) = self.current_display() { 421 datasets.append(&mut current_display.references(&graph)); 422 } 423 } 424 if let Some((current_display, channels)) = self.current_display_mut().zip(channels) 425 { 426 datasets.append(&mut current_display.process(&graph, &channels)); 427 } 428 terminal 429 .draw(|f| { 430 let mut size = f.size(); 431 render_frame(new_state.clone(), f); 432 if let Some(current_display) = self.current_display() { 433 if self.graph.show_ui { 434 f.render_widget( 435 make_header( 436 &self.graph, 437 &current_display.header(&self.graph), 438 current_display.mode_str(), 439 framerate, 440 self.graph.pause, 441 ), 442 Rect { 443 x: size.x, 444 y: size.y + 7, 445 width: size.width, 446 height: 1, 447 }, 448 ); 449 size.height -= 8; 450 size.y += 8; 451 } 452 let chart = Chart::new(datasets.iter().map(|x| x.into()).collect()) 453 .x_axis(current_display.axis(&self.graph, Dimension::X)) 454 .y_axis(current_display.axis(&self.graph, Dimension::Y)); 455 f.render_widget(chart, size) 456 } 457 }) 458 .unwrap(); 459 460 // Update metadata only if needed and at a controlled interval 461 if last_metadata_update.elapsed() >= METADATA_UPDATE_INTERVAL { 462 let state = new_state.lock().unwrap(); 463 if state.now_playing != last_now_playing { 464 let now_playing = state.now_playing.clone(); 465 let name = state.name.clone(); 466 send_os_media_controls_command( 467 self.os_media_controls.as_mut(), 468 os_media_controls::Command::SetMetadata(souvlaki::MediaMetadata { 469 title: (!now_playing.is_empty()).then_some(now_playing.as_str()), 470 album: (!name.is_empty()).then_some(name.as_str()), 471 artist: None, 472 cover_url: None, 473 duration: None, 474 }), 475 ); 476 last_now_playing = state.now_playing.clone(); 477 } 478 last_metadata_update = Instant::now(); 479 } 480 } 481 482 while let Some(event) = self 483 .os_media_controls 484 .as_mut() 485 .and_then(|os_media_controls| os_media_controls.try_recv_os_event()) 486 { 487 if self.process_os_media_control_event(event, &new_state, &mut sink_cmd_tx) { 488 return; 489 } 490 } 491 492 let timeout_duration = if self.graph.pause { 493 self.poll_events_every_while_paused 494 } else { 495 self.poll_events_every 496 }; 497 498 while event::poll(timeout_duration).unwrap() { 499 // process all enqueued events 500 let event = event::read().unwrap(); 501 502 if self 503 .process_events(event.clone(), new_state.clone(), &mut sink_cmd_tx) 504 .unwrap() 505 { 506 return; 507 } 508 if let Some(current_display) = self.current_display_mut() { 509 current_display.handle(event); 510 } 511 } 512 } 513 } 514 fn current_display_mut(&mut self) -> Option<&mut dyn DisplayMode> { 515 match self.mode { 516 CurrentDisplayMode::Oscilloscope => { 517 Some(&mut self.oscilloscope as &mut dyn DisplayMode) 518 } 519 CurrentDisplayMode::Vectorscope => Some(&mut self.vectorscope as &mut dyn DisplayMode), 520 CurrentDisplayMode::Spectroscope => { 521 Some(&mut self.spectroscope as &mut dyn DisplayMode) 522 } 523 CurrentDisplayMode::None => None, 524 } 525 } 526 527 fn current_display(&self) -> Option<&dyn DisplayMode> { 528 match self.mode { 529 CurrentDisplayMode::Oscilloscope => Some(&self.oscilloscope as &dyn DisplayMode), 530 CurrentDisplayMode::Vectorscope => Some(&self.vectorscope as &dyn DisplayMode), 531 CurrentDisplayMode::Spectroscope => Some(&self.spectroscope as &dyn DisplayMode), 532 CurrentDisplayMode::None => None, 533 } 534 } 535 536 fn process_events( 537 &mut self, 538 event: Event, 539 state: Arc<Mutex<State>>, 540 sink_cmd_tx: &mut UnboundedSender<SinkCommand>, 541 ) -> Result<bool, io::Error> { 542 let mut quit = false; 543 544 if let Event::Key(key) = event { 545 if let KeyModifiers::CONTROL = key.modifiers { 546 match key.code { 547 // mimic other programs shortcuts to quit, for user friendlyness 548 KeyCode::Char('c') | KeyCode::Char('q') | KeyCode::Char('w') => quit = true, 549 _ => {} 550 } 551 } 552 let magnitude = match key.modifiers { 553 KeyModifiers::SHIFT => 10.0, 554 KeyModifiers::CONTROL => 5.0, 555 KeyModifiers::ALT => 0.2, 556 _ => 1.0, 557 }; 558 match key.code { 559 KeyCode::Up => { 560 // inverted to act as zoom 561 update_value_f(&mut self.graph.scale, 0.01, magnitude, 0.0..10.0); 562 raise_volume(&state, self.os_media_controls.as_mut(), sink_cmd_tx); 563 } 564 KeyCode::Down => { 565 // inverted to act as zoom 566 update_value_f(&mut self.graph.scale, -0.01, magnitude, 0.0..10.0); 567 lower_volume(&state, self.os_media_controls.as_mut(), sink_cmd_tx); 568 } 569 KeyCode::Right => update_value_i( 570 &mut self.graph.samples, 571 true, 572 25, 573 magnitude, 574 0..self.graph.width * 2, 575 ), 576 KeyCode::Left => update_value_i( 577 &mut self.graph.samples, 578 false, 579 25, 580 magnitude, 581 0..self.graph.width * 2, 582 ), 583 KeyCode::Char('q') => quit = true, 584 KeyCode::Char(' ') => toggle_play_pause( 585 &mut self.graph, 586 self.os_media_controls.as_mut(), 587 sink_cmd_tx, 588 ), 589 KeyCode::Char('s') => self.graph.scatter = !self.graph.scatter, 590 KeyCode::Char('h') => self.graph.show_ui = !self.graph.show_ui, 591 KeyCode::Char('r') => self.graph.references = !self.graph.references, 592 KeyCode::Char('m') => { 593 mute_volume(&state, self.os_media_controls.as_mut(), sink_cmd_tx) 594 } 595 KeyCode::Esc => { 596 self.graph.samples = self.graph.width; 597 self.graph.scale = 1.; 598 } 599 KeyCode::Char('c') if key.modifiers.contains(KeyModifiers::CONTROL) => { 600 quit = true; 601 } 602 KeyCode::Tab => { 603 // switch modes 604 match self.mode { 605 CurrentDisplayMode::Oscilloscope => { 606 self.mode = CurrentDisplayMode::Vectorscope; 607 } 608 CurrentDisplayMode::Vectorscope => { 609 self.mode = CurrentDisplayMode::Spectroscope; 610 } 611 CurrentDisplayMode::Spectroscope => { 612 self.mode = CurrentDisplayMode::None; 613 } 614 CurrentDisplayMode::None => { 615 self.mode = CurrentDisplayMode::Oscilloscope; 616 } 617 } 618 } 619 KeyCode::Media(media_key_code) => match media_key_code { 620 MediaKeyCode::Play => play( 621 &mut self.graph, 622 self.os_media_controls.as_mut(), 623 sink_cmd_tx, 624 ), 625 MediaKeyCode::Pause => pause( 626 &mut self.graph, 627 self.os_media_controls.as_mut(), 628 sink_cmd_tx, 629 ), 630 MediaKeyCode::PlayPause => toggle_play_pause( 631 &mut self.graph, 632 self.os_media_controls.as_mut(), 633 sink_cmd_tx, 634 ), 635 MediaKeyCode::Stop => { 636 quit = true; 637 } 638 MediaKeyCode::LowerVolume => { 639 lower_volume(&state, self.os_media_controls.as_mut(), sink_cmd_tx) 640 } 641 MediaKeyCode::RaiseVolume => { 642 raise_volume(&state, self.os_media_controls.as_mut(), sink_cmd_tx) 643 } 644 MediaKeyCode::MuteVolume => { 645 mute_volume(&state, self.os_media_controls.as_mut(), sink_cmd_tx) 646 } 647 MediaKeyCode::TrackNext 648 | MediaKeyCode::TrackPrevious 649 | MediaKeyCode::Reverse 650 | MediaKeyCode::FastForward 651 | MediaKeyCode::Rewind 652 | MediaKeyCode::Record => {} 653 }, 654 _ => {} 655 } 656 }; 657 658 Ok(quit) 659 } 660 661 /// Process OS media control event. 662 /// 663 /// Returns [`true`] if application should quit. 664 fn process_os_media_control_event( 665 &mut self, 666 event: MediaControlEvent, 667 state: &Mutex<State>, 668 sink_cmd_tx: &mut UnboundedSender<SinkCommand>, 669 ) -> bool { 670 let mut quit = false; 671 672 match event { 673 MediaControlEvent::Play => { 674 play( 675 &mut self.graph, 676 self.os_media_controls.as_mut(), 677 sink_cmd_tx, 678 ); 679 } 680 MediaControlEvent::Pause => { 681 pause( 682 &mut self.graph, 683 self.os_media_controls.as_mut(), 684 sink_cmd_tx, 685 ); 686 } 687 MediaControlEvent::Toggle => { 688 toggle_play_pause( 689 &mut self.graph, 690 self.os_media_controls.as_mut(), 691 sink_cmd_tx, 692 ); 693 } 694 MediaControlEvent::Stop | MediaControlEvent::Quit => { 695 quit = true; 696 } 697 MediaControlEvent::SetVolume(volume) => { 698 set_volume_ratio( 699 volume as f32, 700 state, 701 self.os_media_controls.as_mut(), 702 sink_cmd_tx, 703 ); 704 } 705 MediaControlEvent::Next 706 | MediaControlEvent::Previous 707 | MediaControlEvent::Seek(_) 708 | MediaControlEvent::SeekBy(_, _) 709 | MediaControlEvent::SetPosition(_) 710 | MediaControlEvent::OpenUri(_) 711 | MediaControlEvent::Raise => {} 712 } 713 714 quit 715 } 716} 717 718pub fn update_value_f(val: &mut f64, base: f64, magnitude: f64, range: Range<f64>) { 719 let delta = base * magnitude; 720 if *val + delta > range.end { 721 *val = range.end 722 } else if *val + delta < range.start { 723 *val = range.start 724 } else { 725 *val += delta; 726 } 727} 728 729pub fn update_value_i(val: &mut u32, inc: bool, base: u32, magnitude: f64, range: Range<u32>) { 730 let delta = (base as f64 * magnitude) as u32; 731 if inc { 732 if range.end - delta < *val { 733 *val = range.end 734 } else { 735 *val += delta 736 } 737 } else if range.start + delta > *val { 738 *val = range.start 739 } else { 740 *val -= delta 741 } 742} 743 744fn make_header<'a>( 745 cfg: &GraphConfig, 746 module_header: &'a str, 747 kind_o_scope: &'static str, 748 fps: usize, 749 pause: bool, 750) -> Table<'a> { 751 Table::new( 752 vec![Row::new(vec![ 753 Cell::from(format!("{}::scope-tui", kind_o_scope)).style( 754 Style::default() 755 .fg(*cfg.palette.first().expect("empty palette?")) 756 .add_modifier(Modifier::BOLD), 757 ), 758 Cell::from(module_header), 759 Cell::from(format!("-{:.2}x+", cfg.scale)), 760 Cell::from(format!("{}/{} spf", cfg.samples, cfg.width)), 761 Cell::from(format!("{}fps", fps)), 762 Cell::from(if cfg.scatter { "***" } else { "---" }), 763 Cell::from(if pause { "||" } else { "|>" }), 764 ])], 765 vec![ 766 Constraint::Percentage(35), 767 Constraint::Percentage(25), 768 Constraint::Percentage(7), 769 Constraint::Percentage(13), 770 Constraint::Percentage(6), 771 Constraint::Percentage(6), 772 Constraint::Percentage(6), 773 ], 774 ) 775 .style(Style::default().fg(cfg.labels_color)) 776} 777 778/// Play music. 779fn play( 780 graph: &mut GraphConfig, 781 os_media_controls: Option<&mut OsMediaControls>, 782 sink_cmd_tx: &UnboundedSender<SinkCommand>, 783) { 784 graph.pause = false; 785 send_os_media_controls_command(os_media_controls, os_media_controls::Command::Play); 786 sink_cmd_tx 787 .send(SinkCommand::Play) 788 .expect("receiver never dropped"); 789} 790 791/// Pause music. 792fn pause( 793 graph: &mut GraphConfig, 794 os_media_controls: Option<&mut OsMediaControls>, 795 sink_cmd_tx: &UnboundedSender<SinkCommand>, 796) { 797 graph.pause = true; 798 send_os_media_controls_command(os_media_controls, os_media_controls::Command::Pause); 799 sink_cmd_tx 800 .send(SinkCommand::Pause) 801 .expect("receiver never dropped"); 802} 803 804/// Toggle between play and pause. 805fn toggle_play_pause( 806 graph: &mut GraphConfig, 807 os_media_controls: Option<&mut OsMediaControls>, 808 sink_cmd_tx: &UnboundedSender<SinkCommand>, 809) { 810 graph.pause = !graph.pause; 811 let (sink_cmd, os_media_controls_command) = if graph.pause { 812 (SinkCommand::Pause, os_media_controls::Command::Pause) 813 } else { 814 (SinkCommand::Play, os_media_controls::Command::Play) 815 }; 816 send_os_media_controls_command(os_media_controls, os_media_controls_command); 817 sink_cmd_tx.send(sink_cmd).expect("receiver never dropped"); 818} 819 820/// Lower the volume. 821fn lower_volume( 822 state: &Mutex<State>, 823 os_media_controls: Option<&mut OsMediaControls>, 824 sink_cmd_tx: &UnboundedSender<SinkCommand>, 825) { 826 let mut state = state.lock().unwrap(); 827 state.volume.change_volume(-1.0); 828 send_os_media_controls_command( 829 os_media_controls, 830 os_media_controls::Command::SetVolume(state.volume.volume_ratio() as f64), 831 ); 832 sink_cmd_tx 833 .send(SinkCommand::SetVolume(state.volume.volume_ratio())) 834 .expect("receiver never dropped"); 835} 836 837/// Raise the volume. 838fn raise_volume( 839 state: &Mutex<State>, 840 os_media_controls: Option<&mut OsMediaControls>, 841 sink_cmd_tx: &UnboundedSender<SinkCommand>, 842) { 843 let mut state = state.lock().unwrap(); 844 state.volume.change_volume(1.0); 845 send_os_media_controls_command( 846 os_media_controls, 847 os_media_controls::Command::SetVolume(state.volume.volume_ratio() as f64), 848 ); 849 sink_cmd_tx 850 .send(SinkCommand::SetVolume(state.volume.volume_ratio())) 851 .expect("receiver never dropped"); 852} 853 854/// Mute the volume. 855fn mute_volume( 856 state: &Mutex<State>, 857 os_media_controls: Option<&mut OsMediaControls>, 858 sink_cmd_tx: &UnboundedSender<SinkCommand>, 859) { 860 let mut state = state.lock().unwrap(); 861 state.volume.toggle_mute(); 862 send_os_media_controls_command( 863 os_media_controls, 864 os_media_controls::Command::SetVolume(state.volume.volume_ratio() as f64), 865 ); 866 sink_cmd_tx 867 .send(SinkCommand::SetVolume(state.volume.volume_ratio())) 868 .expect("receiver never dropped"); 869} 870 871/// Set the volume to the given volume ratio. 872fn set_volume_ratio( 873 volume_ratio: f32, 874 state: &Mutex<State>, 875 os_media_controls: Option<&mut OsMediaControls>, 876 sink_cmd_tx: &UnboundedSender<SinkCommand>, 877) { 878 let mut state = state.lock().unwrap(); 879 state.volume.set_volume_ratio(volume_ratio); 880 send_os_media_controls_command( 881 os_media_controls, 882 os_media_controls::Command::SetVolume(state.volume.volume_ratio() as f64), 883 ); 884 sink_cmd_tx 885 .send(SinkCommand::SetVolume(state.volume.volume_ratio())) 886 .expect("receiver never dropped"); 887} 888 889/// Send [`os_media_controls::Command`]. 890pub fn send_os_media_controls_command( 891 os_media_controls: Option<&mut OsMediaControls>, 892 command: os_media_controls::Command<'_>, 893) { 894 if let Some(os_media_controls) = os_media_controls { 895 let _ = os_media_controls.send_to_os(command).inspect_err(|err| { 896 eprintln!( 897 "error: failed to send command to OS media controls due to `{}`", 898 err 899 ); 900 }); 901 } 902}