diff --git a/Cargo.lock b/Cargo.lock index 79900c3..ceb5179 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -790,6 +790,7 @@ dependencies = [ name = "delyrium" version = "0.1.0" dependencies = [ + "blocking", "exoquant", "iced", "iced_futures", diff --git a/Cargo.toml b/Cargo.toml index daf2ef3..a252c9e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -19,6 +19,10 @@ iced_native = "0.4.0" # Native file dialogs rfd = "0.6.3" +# Run long-running blocking tasks in another thread +# Also used by iced, so we share a threadpool +blocking = "1.1.0" + [dependencies.symphonia] # Music decoding and metadata parsing features = ["isomp4", "aac", "mp3"] diff --git a/src/app.rs b/src/app.rs index f4f79a0..35639d5 100644 --- a/src/app.rs +++ b/src/app.rs @@ -91,7 +91,9 @@ impl Application for DelyriumApp { Message::FileOpened(path) => { println!("File opened! {}", path.display()); let song = load_song(&path).unwrap().format; - self.lyrics_component = Some(Editor::new(song, self.size)); + let (editor, cmd) = Editor::new(song, self.size); + self.lyrics_component = Some(editor); + command = Some(cmd); }, Message::PromptForFile => { let task = async { diff --git a/src/controls.rs b/src/controls.rs index 2c7781f..8fc4e95 100644 --- a/src/controls.rs +++ b/src/controls.rs @@ -1,3 +1,5 @@ +use iced::Command; +use core::time::Duration; use iced::Length; use iced::Color; use symphonia::core::formats::FormatReader; @@ -14,11 +16,13 @@ use iced::canvas::Program; use iced::Canvas; use iced::mouse::{self, Button}; use crate::player::Player; +use blocking::unblock; #[derive(Debug, Clone, Copy)] pub enum ControlsEvent { SeekPosition(f32), TogglePlay, + DurationDiscovered(Duration), } enum ErrorState { @@ -36,20 +40,26 @@ pub struct Controls { } impl Controls { - pub fn new(song: Box) -> Self { + pub fn new(song: Box) -> (Self, Command) { match Player::new(song) { Ok(player) => { - Controls { + let duration_task = unblock(player.compute_duration()); + let duration_cmd = Command::perform( + duration_task, + |d| Message::ControlsEvent(ControlsEvent::DurationDiscovered(d)), + ); + + (Controls { error_state: NoError { has_device: player.has_output_device(), player, } - } + }, duration_cmd) }, Err(e) => { - Controls { + (Controls { error_state: Error(e) - } + }, Command::none()) } } } @@ -65,6 +75,11 @@ impl Controls { let result = match event { ControlsEvent::SeekPosition(pos) => player.seek_percentage(pos), ControlsEvent::TogglePlay => player.toggle_play(), + ControlsEvent::DurationDiscovered(d) => { + player.set_duration(d); + println!("Found duration! {:?}", d); + Ok(player.has_output_device()) + }, }; match result { diff --git a/src/editor.rs b/src/editor.rs index 0ffff18..35c01e8 100644 --- a/src/editor.rs +++ b/src/editor.rs @@ -1,3 +1,4 @@ +use iced::Command; use iced::Image; use iced::image::Handle; use image::DynamicImage; @@ -28,7 +29,7 @@ pub struct Editor { } impl Editor { - pub fn new(mut song: Box, size: (u32, u32)) -> Self { + pub fn new(mut song: Box, size: (u32, u32)) -> (Self, Command) { let cover = extract_cover(song.as_mut()); let theme = cover.as_ref() @@ -43,14 +44,14 @@ impl Editor { cover.blur((cover.width() / 50) as f32).into_bgra8() ); - let controls = Controls::new(song); + let (controls, cmd) = Controls::new(song); - Self { + (Self { lyrics: Lyrics::new(), dimensions: size, cached_resized_bg: None, controls, theme, bg_img, - } + }, cmd) } // TODO: work on untangling this mess diff --git a/src/player.rs b/src/player.rs index a40e8cd..b138d0d 100644 --- a/src/player.rs +++ b/src/player.rs @@ -29,13 +29,18 @@ pub struct Player { impl Player { /// Create a new player from a song + /// + /// IMPORTANT: Because computing the duration of a song is currently a very expensive + /// task, this DOES NOT COMPUTE THE DURATION. All calculations that require a + /// duration assume a duration of 2:30s until a duration is manually calculated with + /// [`compute_duration()`] AND passed back into [`set_duration()`]. pub fn new(song: Box) -> Result { let song = Decoder::new_from_format_reader(song) .map_err(PlayerError::DecoderError)? .buffered(); - let duration = get_duration_of_song(song.clone()); + let duration = Duration::from_secs(150); let mut player = Player { sink: None, @@ -221,20 +226,49 @@ impl Player { // nightly: self.position().div_duration_f32(self.duration) self.position().as_secs_f32() / self.duration.as_secs_f32() } -} -/// Manually calculate the exact length of a given song -/// -/// This is really expensive, and involves decoding and inefficiently counting the number -/// of samples in the song, but produces an exact output. Use sparingly. -fn get_duration_of_song(song: Song) -> Duration { - let sample_rate = song.sample_rate() as u64; - let n_channels = song.channels() as u64; - let n_samples = song.count() as u64; // expensive! - let n_whole_seconds = n_samples / n_channels / sample_rate; - let remaining_samples = n_samples % sample_rate; - let n_nanos = remaining_samples * 1_000_000_000 / sample_rate; - Duration::new(n_whole_seconds, n_nanos as u32) + /// Computes the exact duration of the current song. + /// + /// This is an EXPENSIVE, BLOCKING long-running function that should be run in a + /// seperate thread. Hopefully this will be optimized in the future, but this is what + /// we have for now. + /// + /// This does not set the duration of this song, so be sure to call [`set_duration()`] + /// with the result of this call after it finishes executing. + /// + /// Note: This doesn't actually preform the calculation, but instead returns a + /// `'static FnOnce` that can be used to do so, in order to simplify running the + /// transaction in another thread. + /// + /// Technical Details: Currently, this involves decoding and inefficiently counting + /// the number of samples in the song. Hopefully, we'll be able to do this more + /// efficiently in the future, pending mostly on + /// https://github.com/RustAudio/rodio/issues/405 + pub fn compute_duration(&self) -> impl FnOnce() -> Duration { + let sample_rate = self.song.sample_rate() as u64; + let n_channels = self.song.channels() as u64; + let song_clone = self.song.clone(); + + move|| { + let n_samples = song_clone.count() as u64; // expensive! + let n_whole_seconds = n_samples / n_channels / sample_rate; + let remaining_samples = n_samples % sample_rate; + let n_nanos = remaining_samples * 1_000_000_000 / sample_rate; + Duration::new(n_whole_seconds, n_nanos as u32) + } + } + + /// Set the duration of the song + /// + /// This struct does not automatically compute its own duration, so until this method + /// is called, we just assume the duration is 2:30s for any calculations that require + /// it. + /// + /// In order to use an exact duration, use another thread to run + /// [`compute_duration()`] and pass the result into this method. + pub fn set_duration(&mut self, duration: Duration) { + self.duration = duration; + } } #[derive(Debug)]