From fd90471264e67f47745f2ee585a76f351305cd70 Mon Sep 17 00:00:00 2001 From: zX3no <dr_xeno@hotmail.com.au> Date: Tue, 14 Jun 2022 14:02:38 +0930 Subject: [PATCH 01/40] completely reworked player --- gonk-player/Cargo.toml | 4 +- gonk-player/src/buffer.rs | 105 ------ gonk-player/src/conversions/channels.rs | 94 ----- gonk-player/src/conversions/mod.rs | 15 - gonk-player/src/conversions/sample.rs | 155 -------- gonk-player/src/decoder.rs | 262 ------------- gonk-player/src/dynamic_mixer.rs | 188 ---------- gonk-player/src/index.rs | 3 - gonk-player/src/lib.rs | 352 +++++++++--------- gonk-player/src/queue.rs | 239 ------------ gonk-player/src/sample_processor.rs | 108 ++++++ .../src/{conversions => }/sample_rate.rs | 158 ++------ gonk-player/src/sink.rs | 193 ---------- gonk-player/src/source/amplify.rs | 105 ------ gonk-player/src/source/done.rs | 98 ----- gonk-player/src/source/empty.rs | 64 ---- gonk-player/src/source/fadein.rs | 116 ------ gonk-player/src/source/mod.rs | 203 ---------- gonk-player/src/source/pausable.rs | 125 ------- gonk-player/src/source/periodic.rs | 127 ------- gonk-player/src/source/samples_converter.rs | 97 ----- gonk-player/src/source/stoppable.rs | 100 ----- gonk-player/src/source/take.rs | 186 --------- gonk-player/src/source/uniform.rs | 203 ---------- gonk-player/src/source/zero.rs | 67 ---- gonk-player/src/stream.rs | 256 ------------- gonk/src/app.rs | 14 +- gonk/src/app/queue.rs | 48 ++- gonk/src/app/search.rs | 2 +- gonk/src/app/status_bar.rs | 10 +- 30 files changed, 359 insertions(+), 3338 deletions(-) delete mode 100644 gonk-player/src/buffer.rs delete mode 100644 gonk-player/src/conversions/channels.rs delete mode 100644 gonk-player/src/conversions/mod.rs delete mode 100644 gonk-player/src/conversions/sample.rs delete mode 100644 gonk-player/src/decoder.rs delete mode 100644 gonk-player/src/dynamic_mixer.rs delete mode 100644 gonk-player/src/queue.rs create mode 100644 gonk-player/src/sample_processor.rs rename gonk-player/src/{conversions => }/sample_rate.rs (53%) delete mode 100644 gonk-player/src/sink.rs delete mode 100644 gonk-player/src/source/amplify.rs delete mode 100644 gonk-player/src/source/done.rs delete mode 100644 gonk-player/src/source/empty.rs delete mode 100644 gonk-player/src/source/fadein.rs delete mode 100644 gonk-player/src/source/mod.rs delete mode 100644 gonk-player/src/source/pausable.rs delete mode 100644 gonk-player/src/source/periodic.rs delete mode 100644 gonk-player/src/source/samples_converter.rs delete mode 100644 gonk-player/src/source/stoppable.rs delete mode 100644 gonk-player/src/source/take.rs delete mode 100644 gonk-player/src/source/uniform.rs delete mode 100644 gonk-player/src/source/zero.rs delete mode 100644 gonk-player/src/stream.rs diff --git a/gonk-player/Cargo.toml b/gonk-player/Cargo.toml index ca5fb661..6d494c7f 100644 --- a/gonk-player/Cargo.toml +++ b/gonk-player/Cargo.toml @@ -11,5 +11,5 @@ license = "MIT" [dependencies] cpal = "0.13.5" -rand = "0.8.5" -symphonia = { version = "0.5.0", features = ["aac", "mp3", "isomp4"] } +crossbeam-channel = "0.5.4" +symphonia = "0.5.0" diff --git a/gonk-player/src/buffer.rs b/gonk-player/src/buffer.rs deleted file mode 100644 index d1755d3a..00000000 --- a/gonk-player/src/buffer.rs +++ /dev/null @@ -1,105 +0,0 @@ -use std::time::Duration; -use std::vec::IntoIter as VecIntoIter; - -use crate::{conversions::Sample, source::Source}; - -/// A buffer of samples treated as a source. -pub struct SamplesBuffer<S> { - data: VecIntoIter<S>, - channels: u16, - sample_rate: u32, - duration: Duration, -} - -impl<S> SamplesBuffer<S> -where - S: Sample, -{ - /// Builds a new `SamplesBuffer`. - /// - /// # Panic - /// - /// - Panics if the number of channels is zero. - /// - Panics if the samples rate is zero. - /// - Panics if the length of the buffer is larger than approximately 16 billion elements. - /// This is because the calculation of the duration would overflow. - /// - pub fn new<D>(channels: u16, sample_rate: u32, data: D) -> SamplesBuffer<S> - where - D: Into<Vec<S>>, - { - assert!(channels != 0); - assert!(sample_rate != 0); - - let data = data.into(); - let duration_ns = 1_000_000_000u64.checked_mul(data.len() as u64).unwrap() - / sample_rate as u64 - / channels as u64; - let duration = Duration::new( - duration_ns / 1_000_000_000, - (duration_ns % 1_000_000_000) as u32, - ); - - SamplesBuffer { - data: data.into_iter(), - channels, - sample_rate, - duration, - } - } -} - -impl<S> Source for SamplesBuffer<S> -where - S: Sample, -{ - #[inline] - fn current_frame_len(&self) -> Option<usize> { - None - } - - #[inline] - fn channels(&self) -> u16 { - self.channels - } - - #[inline] - fn sample_rate(&self) -> u32 { - self.sample_rate - } - - #[inline] - fn total_duration(&self) -> Option<Duration> { - Some(self.duration) - } - - #[inline] - fn elapsed(&mut self) -> Duration { - Duration::from_secs(0) - } - - fn seek(&mut self, seek_time: Duration) -> Option<Duration> { - let iters = (self.sample_rate as f32 / 1000. * seek_time.as_millis() as f32).round() as u32; - for i in 0..iters { - self.data.next().ok_or(i).unwrap(); - } - Some(seek_time) - } -} - -impl<S> Iterator for SamplesBuffer<S> -where - S: Sample, -{ - type Item = S; - - #[inline] - fn next(&mut self) -> Option<S> { - self.data.next() - } - - #[inline] - fn size_hint(&self) -> (usize, Option<usize>) { - self.data.size_hint() - } -} diff --git a/gonk-player/src/conversions/channels.rs b/gonk-player/src/conversions/channels.rs deleted file mode 100644 index a9d94d3d..00000000 --- a/gonk-player/src/conversions/channels.rs +++ /dev/null @@ -1,94 +0,0 @@ -/// Iterator that converts from a certain channel count to another. -#[derive(Clone, Debug)] -pub struct ChannelCountConverter<I> -where - I: Iterator, -{ - input: I, - from: cpal::ChannelCount, - to: cpal::ChannelCount, - sample_repeat: Option<I::Item>, - next_output_sample_pos: cpal::ChannelCount, -} - -impl<I> ChannelCountConverter<I> -where - I: Iterator, -{ - /// Initializes the iterator. - /// - /// # Panic - /// - /// Panicks if `from` or `to` are equal to 0. - /// - #[inline] - pub fn new( - input: I, - from: cpal::ChannelCount, - to: cpal::ChannelCount, - ) -> ChannelCountConverter<I> { - assert!(from >= 1); - assert!(to >= 1); - - ChannelCountConverter { - input, - from, - to, - sample_repeat: None, - next_output_sample_pos: 0, - } - } - - /// Destroys this iterator and returns the underlying iterator. - #[inline] - pub fn into_inner(self) -> I { - self.input - } -} - -impl<I> Iterator for ChannelCountConverter<I> -where - I: Iterator, - I::Item: Clone, -{ - type Item = I::Item; - - fn next(&mut self) -> Option<I::Item> { - let result = if self.next_output_sample_pos == self.from - 1 { - let value = self.input.next(); - self.sample_repeat = value.clone(); - value - } else if self.next_output_sample_pos < self.from { - self.input.next() - } else { - self.sample_repeat.clone() - }; - - self.next_output_sample_pos += 1; - - if self.next_output_sample_pos == self.to { - self.next_output_sample_pos -= self.to; - - if self.from > self.to { - for _ in self.to..self.from { - self.input.next(); // discarding extra input - } - } - } - - result - } - - #[inline] - fn size_hint(&self) -> (usize, Option<usize>) { - let (min, max) = self.input.size_hint(); - - let min = - (min / self.from as usize) * self.to as usize + self.next_output_sample_pos as usize; - let max = max.map(|max| { - (max / self.from as usize) * self.to as usize + self.next_output_sample_pos as usize - }); - - (min, max) - } -} diff --git a/gonk-player/src/conversions/mod.rs b/gonk-player/src/conversions/mod.rs deleted file mode 100644 index a842ccd0..00000000 --- a/gonk-player/src/conversions/mod.rs +++ /dev/null @@ -1,15 +0,0 @@ -/*! -This module contains function that will convert from one PCM format to another. - -This includes conversion between sample formats, channels or sample rates. - -*/ - -pub use self::channels::ChannelCountConverter; -pub use self::sample::DataConverter; -pub use self::sample::Sample; -pub use self::sample_rate::SampleRateConverter; - -mod channels; -mod sample; -mod sample_rate; diff --git a/gonk-player/src/conversions/sample.rs b/gonk-player/src/conversions/sample.rs deleted file mode 100644 index b242a19d..00000000 --- a/gonk-player/src/conversions/sample.rs +++ /dev/null @@ -1,155 +0,0 @@ -use cpal::Sample as CpalSample; -use std::marker::PhantomData; - -/// Converts the samples data type to `O`. -#[derive(Clone, Debug)] -pub struct DataConverter<I, O> { - input: I, - marker: PhantomData<O>, -} - -impl<I, O> DataConverter<I, O> { - /// Builds a new converter. - #[inline] - pub fn new(input: I) -> DataConverter<I, O> { - DataConverter { - input, - marker: PhantomData, - } - } - - /// Destroys this iterator and returns the underlying iterator. - #[inline] - pub fn into_inner(self) -> I { - self.input - } -} - -impl<I, O> Iterator for DataConverter<I, O> -where - I: Iterator, - I::Item: Sample, - O: Sample, -{ - type Item = O; - - #[inline] - fn next(&mut self) -> Option<O> { - self.input.next().map(|s| CpalSample::from(&s)) - } - - #[inline] - fn size_hint(&self) -> (usize, Option<usize>) { - self.input.size_hint() - } -} - -impl<I, O> ExactSizeIterator for DataConverter<I, O> -where - I: ExactSizeIterator, - I::Item: Sample, - O: Sample, -{ -} - -/// Represents a value of a single sample. -/// -/// This trait is implemented by default on three types: `i16`, `u16` and `f32`. -/// -/// - For `i16`, silence corresponds to the value `0`. The minimum and maximum amplitudes are -/// represented by `i16::min_value()` and `i16::max_value()` respectively. -/// - For `u16`, silence corresponds to the value `u16::max_value() / 2`. The minimum and maximum -/// amplitudes are represented by `0` and `u16::max_value()` respectively. -/// - For `f32`, silence corresponds to the value `0.0`. The minimum and maximum amplitudes are -/// represented by `-1.0` and `1.0` respectively. -/// -/// You can implement this trait on your own type as well if you wish so. -/// -pub trait Sample: CpalSample { - /// Linear interpolation between two samples. - /// - /// The result should be equal to - /// `first * numerator / denominator + second * (1 - numerator / denominator)`. - fn lerp(first: Self, second: Self, numerator: u32, denominator: u32) -> Self; - /// Multiplies the value of this sample by the given amount. - #[must_use] - fn amplify(self, value: f32) -> Self; - - /// Calls `saturating_add` on the sample. - #[must_use] - fn saturating_add(self, other: Self) -> Self; - - /// Returns the value corresponding to the absence of sound. - fn zero_value() -> Self; -} - -impl Sample for u16 { - #[inline] - fn lerp(first: u16, second: u16, numerator: u32, denominator: u32) -> u16 { - let a = first as i32; - let b = second as i32; - let n = numerator as i32; - let d = denominator as i32; - (a + (b - a) * n / d) as u16 - } - - #[inline] - fn amplify(self, value: f32) -> u16 { - self.to_i16().amplify(value).to_u16() - } - - #[inline] - fn saturating_add(self, other: u16) -> u16 { - self.saturating_add(other) - } - - #[inline] - fn zero_value() -> u16 { - 32768 - } -} - -impl Sample for i16 { - #[inline] - fn lerp(first: i16, second: i16, numerator: u32, denominator: u32) -> i16 { - (first as i32 + (second as i32 - first as i32) * numerator as i32 / denominator as i32) - as i16 - } - - #[inline] - fn amplify(self, value: f32) -> i16 { - ((self as f32) * value) as i16 - } - - #[inline] - fn saturating_add(self, other: i16) -> i16 { - self.saturating_add(other) - } - - #[inline] - fn zero_value() -> i16 { - 0 - } -} - -impl Sample for f32 { - #[inline] - fn lerp(first: f32, second: f32, numerator: u32, denominator: u32) -> f32 { - first + (second - first) * numerator as f32 / denominator as f32 - } - - #[inline] - fn amplify(self, value: f32) -> f32 { - self * value - } - - #[inline] - fn saturating_add(self, other: f32) -> f32 { - self + other - } - - #[inline] - fn zero_value() -> f32 { - 0.0 - } -} diff --git a/gonk-player/src/decoder.rs b/gonk-player/src/decoder.rs deleted file mode 100644 index 26e2fc34..00000000 --- a/gonk-player/src/decoder.rs +++ /dev/null @@ -1,262 +0,0 @@ -use crate::source::Source; -use std::{fmt, fs::File, time::Duration}; -use symphonia::{ - core::{ - audio::{AudioBufferRef, SampleBuffer, SignalSpec}, - codecs::{self, CodecParameters}, - errors::Error, - formats::{FormatOptions, FormatReader, SeekMode, SeekTo}, - io::MediaSourceStream, - meta::MetadataOptions, - probe::Hint, - units::{Time, TimeBase}, - }, - default::get_probe, -}; -// Decoder errors are not considered fatal. -// The correct action is to just get a new packet and try again. -// But a decode error in more than 3 consecutive packets is fatal. -const MAX_DECODE_ERRORS: usize = 3; - -pub struct Decoder { - decoder: Box<dyn codecs::Decoder>, - current_frame_offset: usize, - format: Box<dyn FormatReader>, - buffer: SampleBuffer<i16>, - spec: SignalSpec, - duration: Duration, - elapsed: Duration, -} - -impl Decoder { - pub fn new(file: File) -> Result<Self, DecoderError> { - let source = Box::new(file); - - let mss = MediaSourceStream::new(source, Default::default()); - match Decoder::init(mss) { - Err(e) => match e { - Error::IoError(e) => Err(DecoderError::IoError(e.to_string())), - Error::DecodeError(e) => Err(DecoderError::DecodeError(e)), - Error::SeekError(_) => { - unreachable!("Seek errors should not occur during initialization") - } - Error::Unsupported(_) => Err(DecoderError::UnrecognizedFormat), - Error::LimitError(e) => Err(DecoderError::LimitError(e)), - Error::ResetRequired => Err(DecoderError::ResetRequired), - }, - Ok(Some(decoder)) => Ok(decoder), - Ok(None) => Err(DecoderError::NoStreams), - } - } - fn init(mss: MediaSourceStream) -> symphonia::core::errors::Result<Option<Decoder>> { - let mut probed = get_probe().format( - &Hint::default(), - mss, - &FormatOptions { - prebuild_seek_index: true, - seek_index_fill_rate: 10, - enable_gapless: false, - }, - &MetadataOptions::default(), - )?; - - let track = match probed.format.default_track() { - Some(stream) => stream, - None => return Ok(None), - }; - - let mut decoder = symphonia::default::get_codecs().make( - &track.codec_params, - &codecs::DecoderOptions { verify: true }, - )?; - - let duration = Decoder::get_duration(&track.codec_params); - - let mut decode_errors: usize = 0; - let decoded = loop { - let current_frame = probed.format.next_packet()?; - match decoder.decode(¤t_frame) { - Ok(decoded) => break decoded, - Err(e) => match e { - Error::DecodeError(_) => { - decode_errors += 1; - if decode_errors > MAX_DECODE_ERRORS { - return Err(e); - } else { - continue; - } - } - _ => return Err(e), - }, - } - }; - let spec = decoded.spec().to_owned(); - let buffer = Decoder::get_buffer(decoded, &spec); - - Ok(Some(Decoder { - decoder, - current_frame_offset: 0, - format: probed.format, - buffer, - spec, - duration, - elapsed: Duration::from_secs(0), - })) - } - - fn get_duration(params: &CodecParameters) -> Duration { - if let Some(n_frames) = params.n_frames { - if let Some(tb) = params.time_base { - let time = tb.calc_time(n_frames); - Duration::from_secs(time.seconds) + Duration::from_secs_f64(time.frac) - } else { - panic!("no time base?"); - } - } else { - panic!("no n_frames"); - } - } - - #[inline] - fn get_buffer(decoded: AudioBufferRef, spec: &SignalSpec) -> SampleBuffer<i16> { - let duration = decoded.capacity() as u64; - let mut buffer = SampleBuffer::<i16>::new(duration, *spec); - buffer.copy_interleaved_ref(decoded); - buffer - } -} - -impl Source for Decoder { - #[inline] - fn current_frame_len(&self) -> Option<usize> { - Some(self.buffer.samples().len()) - } - - #[inline] - fn channels(&self) -> u16 { - self.spec.channels.count() as u16 - } - - #[inline] - fn sample_rate(&self) -> u32 { - self.spec.rate - } - - #[inline] - fn total_duration(&self) -> Option<Duration> { - Some(self.duration) - } - - #[inline] - fn elapsed(&mut self) -> Duration { - self.elapsed - } - - #[inline] - fn seek(&mut self, time: Duration) -> Option<Duration> { - let nanos_per_sec = 1_000_000_000.0; - match self.format.seek( - SeekMode::Coarse, - SeekTo::Time { - time: Time::new(time.as_secs(), time.subsec_nanos() as f64 / nanos_per_sec), - track_id: None, - }, - ) { - Ok(seeked_to) => { - let base = TimeBase::new(1, self.sample_rate()); - let time = base.calc_time(seeked_to.actual_ts); - - Some(Duration::from_millis( - time.seconds * 1000 + ((time.frac * 60. * 1000.).round() as u64), - )) - } - Err(_) => None, - } - } -} - -impl Iterator for Decoder { - type Item = i16; - - #[inline] - fn next(&mut self) -> Option<i16> { - if self.current_frame_offset == self.buffer.len() { - let mut decode_errors: usize = 0; - let decoded = loop { - match self.format.next_packet() { - Ok(packet) => match self.decoder.decode(&packet) { - Ok(decoded) => { - let ts = packet.ts(); - if let Some(track) = self.format.default_track() { - if let Some(tb) = track.codec_params.time_base { - let t = tb.calc_time(ts); - self.elapsed = Duration::from_secs(t.seconds) - + Duration::from_secs_f64(t.frac); - } - } - break decoded; - } - Err(e) => match e { - Error::DecodeError(_) => { - decode_errors += 1; - if decode_errors > MAX_DECODE_ERRORS { - return None; - } else { - continue; - } - } - _ => return None, - }, - }, - Err(_) => return None, - } - }; - self.spec = decoded.spec().to_owned(); - self.buffer = Decoder::get_buffer(decoded, &self.spec); - self.current_frame_offset = 0; - } - - let sample = self.buffer.samples()[self.current_frame_offset]; - self.current_frame_offset += 1; - - Some(sample) - } -} - -/// Error that can happen when creating a decoder. -#[derive(Debug, Clone)] -pub enum DecoderError { - /// The format of the data has not been recognized. - UnrecognizedFormat, - - /// An IO error occured while reading, writing, or seeking the stream. - IoError(String), - - /// The stream contained malformed data and could not be decoded or demuxed. - DecodeError(&'static str), - - /// A default or user-defined limit was reached while decoding or demuxing the stream. Limits - /// are used to prevent denial-of-service attacks from malicious streams. - LimitError(&'static str), - - /// The demuxer or decoder needs to be reset before continuing. - ResetRequired, - - /// No streams were found by the decoder - NoStreams, -} - -impl fmt::Display for DecoderError { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - let text = match self { - DecoderError::UnrecognizedFormat => "Unrecognized format", - DecoderError::IoError(msg) => &msg[..], - DecoderError::DecodeError(msg) => msg, - DecoderError::LimitError(msg) => msg, - DecoderError::ResetRequired => "Reset required", - DecoderError::NoStreams => "No streams", - }; - write!(f, "{}", text) - } -} -impl std::error::Error for DecoderError {} diff --git a/gonk-player/src/dynamic_mixer.rs b/gonk-player/src/dynamic_mixer.rs deleted file mode 100644 index dd99a79d..00000000 --- a/gonk-player/src/dynamic_mixer.rs +++ /dev/null @@ -1,188 +0,0 @@ -//! Mixer that plays multiple sounds at the same time. - -use std::sync::atomic::{AtomicBool, Ordering}; -use std::sync::{Arc, Mutex}; -use std::time::Duration; - -use crate::conversions::Sample; -use crate::source::{Source, UniformSourceIterator}; - -/// Builds a new mixer. -/// -/// You can choose the characteristics of the output thanks to this constructor. All the sounds -/// added to the mixer will be converted to these values. -/// -/// After creating a mixer, you can add new sounds with the controller. -pub fn mixer<S>( - channels: u16, - sample_rate: u32, -) -> (Arc<DynamicMixerController<S>>, DynamicMixer<S>) -where - S: Sample + Send + 'static, -{ - let input = Arc::new(DynamicMixerController { - has_pending: AtomicBool::new(false), - pending_sources: Mutex::new(Vec::new()), - channels, - sample_rate, - }); - - let output = DynamicMixer { - current_sources: Vec::with_capacity(16), - input: input.clone(), - sample_count: 0, - still_pending: vec![], - still_current: vec![], - }; - - (input, output) -} - -/// The input of the mixer. -pub struct DynamicMixerController<S> { - has_pending: AtomicBool, - pending_sources: Mutex<Vec<Box<dyn Source<Item = S> + Send>>>, - channels: u16, - sample_rate: u32, -} - -impl<S> DynamicMixerController<S> -where - S: Sample + Send + 'static, -{ - /// Adds a new source to mix to the existing ones. - #[inline] - pub fn add<T>(&self, source: T) - where - T: Source<Item = S> + Send + 'static, - { - let uniform_source = UniformSourceIterator::new(source, self.channels, self.sample_rate); - self.pending_sources - .lock() - .unwrap() - .push(Box::new(uniform_source) as Box<_>); - self.has_pending.store(true, Ordering::SeqCst); // TODO: can we relax this ordering? - } -} - -/// The output of the mixer. Implements `Source`. -pub struct DynamicMixer<S> { - // The current iterator that produces samples. - current_sources: Vec<Box<dyn Source<Item = S> + Send>>, - - // The pending sounds. - input: Arc<DynamicMixerController<S>>, - - // The number of samples produced so far. - sample_count: usize, - - // A temporary vec used in start_pending_sources. - still_pending: Vec<Box<dyn Source<Item = S> + Send>>, - - // A temporary vec used in sum_current_sources. - still_current: Vec<Box<dyn Source<Item = S> + Send>>, -} - -impl<S> Source for DynamicMixer<S> -where - S: Sample + Send + 'static, -{ - #[inline] - fn current_frame_len(&self) -> Option<usize> { - None - } - - #[inline] - fn channels(&self) -> u16 { - self.input.channels - } - - #[inline] - fn sample_rate(&self) -> u32 { - self.input.sample_rate - } - - #[inline] - fn total_duration(&self) -> Option<Duration> { - None - } - - #[inline] - fn elapsed(&mut self) -> Duration { - Duration::from_secs(0) - } - - fn seek(&mut self, time: Duration) -> Option<Duration> { - self.current_sources[0].seek(time) - } -} - -impl<S> Iterator for DynamicMixer<S> -where - S: Sample + Send + 'static, -{ - type Item = S; - - #[inline] - fn next(&mut self) -> Option<S> { - if self.input.has_pending.load(Ordering::SeqCst) { - self.start_pending_sources(); - } - - self.sample_count += 1; - - let sum = self.sum_current_sources(); - - if self.current_sources.is_empty() { - None - } else { - Some(sum) - } - } - - #[inline] - fn size_hint(&self) -> (usize, Option<usize>) { - (0, None) - } -} - -impl<S> DynamicMixer<S> -where - S: Sample + Send + 'static, -{ - // Samples from the #next() function are interlaced for each of the channels. - // We need to ensure we start playing sources so that their samples are - // in-step with the modulo of the samples produced so far. Otherwise, the - // sound will play on the wrong channels, e.g. left / right will be reversed. - fn start_pending_sources(&mut self) { - let mut pending = self.input.pending_sources.lock().unwrap(); // TODO: relax ordering? - - for source in pending.drain(..) { - let in_step = self.sample_count % source.channels() as usize == 0; - - if in_step { - self.current_sources.push(source); - } else { - self.still_pending.push(source); - } - } - std::mem::swap(&mut self.still_pending, &mut pending); - - let has_pending = !pending.is_empty(); - self.input.has_pending.store(has_pending, Ordering::SeqCst); // TODO: relax ordering? - } - - fn sum_current_sources(&mut self) -> S { - let mut sum = S::zero_value(); - - for mut source in self.current_sources.drain(..) { - if let Some(value) = source.next() { - sum = sum.saturating_add(value); - self.still_current.push(source); - } - } - std::mem::swap(&mut self.still_current, &mut self.current_sources); - - sum - } -} diff --git a/gonk-player/src/index.rs b/gonk-player/src/index.rs index 595a5244..e779c135 100644 --- a/gonk-player/src/index.rs +++ b/gonk-player/src/index.rs @@ -69,9 +69,6 @@ impl<T> Index<T> { pub fn select(&mut self, i: Option<usize>) { self.index = i; } - pub fn is_none(&self) -> bool { - self.index.is_none() - } pub fn is_empty(&self) -> bool { self.data.is_empty() } diff --git a/gonk-player/src/lib.rs b/gonk-player/src/lib.rs index 87fd968a..a24cb50a 100644 --- a/gonk-player/src/lib.rs +++ b/gonk-player/src/lib.rs @@ -1,150 +1,90 @@ -#![allow(dead_code)] -use cpal::traits::HostTrait; -pub use cpal::{ - self, traits::DeviceTrait, Device, Devices, DevicesError, InputDevices, OutputDevices, - SupportedStreamConfig, +use cpal::{ + traits::{HostTrait, StreamTrait}, + StreamError, +}; +use crossbeam_channel::{unbounded, Receiver, Sender}; +use sample_processor::SampleProcessor; +use std::{ + path::PathBuf, + sync::{Arc, RwLock}, + thread, }; -use decoder::Decoder; -use rand::prelude::SliceRandom; -use rand::thread_rng; -use sink::Sink; -use source::Source; -use std::fs::File; -use std::time::Duration; -use stream::{OutputStream, OutputStreamHandle}; - -mod buffer; -mod conversions; -mod decoder; -mod dynamic_mixer; -mod queue; -mod sink; -mod source; -mod stream; -pub mod index; -pub mod song; +mod index; +mod sample_processor; +mod sample_rate; +mod song; +pub use cpal::traits::DeviceTrait; +pub use cpal::Device; pub use index::Index; pub use song::Song; -const VOLUME_STEP: u16 = 5; -const VOLUME_REDUCTION: f32 = 600.0; +#[derive(Debug)] +pub enum Event { + Play, + Pause, + Stop, + SeekBy(f32), + SeekTo(f32), +} pub struct Player { - stream: OutputStream, - handle: OutputStreamHandle, - sink: Sink, - pub duration: f64, - pub volume: u16, - pub songs: Index<Song>, + s: Sender<Event>, + r: Receiver<Event>, + playing: bool, + volume: u16, + songs: Index<Song>, } impl Player { pub fn new(volume: u16) -> Self { - let (stream, handle) = - OutputStream::try_default().expect("Could not create output stream."); - let sink = Sink::try_new(&handle).unwrap(); - sink.set_volume(volume as f32 / VOLUME_REDUCTION); - + let (s, r) = unbounded(); Self { - stream, - handle, - sink, - duration: 0.0, + s, + r, + playing: true, volume, songs: Index::default(), } } + pub fn update(&mut self) { + // if self.elapsed() > self.duration { + // self.next_song(); + // } + } + pub fn duration(&self) -> f32 { + 0.0 + } pub fn is_empty(&self) -> bool { self.songs.is_empty() } - pub fn add_songs(&mut self, song: &[Song]) { - self.songs.data.extend(song.to_vec()); - if self.songs.is_none() && !self.songs.is_empty() { + pub fn add_songs(&mut self, songs: &[Song]) { + self.songs.data.extend(songs.to_vec()); + if self.songs.selected().is_none() { self.songs.select(Some(0)); self.play_selected(); } } - pub fn play_song(&mut self, i: usize) { - if self.songs.data.get(i).is_some() { - self.songs.select(Some(i)); - self.play_selected(); - }; + pub fn get_volume(&self) -> u16 { + self.volume } - pub fn clear(&mut self) { - self.songs = Index::default(); - self.stop(); - } - //TODO: might remove this? - pub fn clear_except_playing(&mut self) { - let selected = self.songs.selected().cloned(); - let mut i = 0; - while i < self.songs.len() { - if Some(&self.songs.data[i]) != selected.as_ref() { - self.songs.data.remove(i); - } else { - i += 1; + pub fn play_selected(&mut self) { + if let Some(song) = self.songs.selected() { + if self.playing { + self.stop(); } + self.playing = true; + let r2 = self.r.clone(); + Player::run(r2, song.path.clone()); } - self.songs.select(Some(0)); - } - pub fn prev_song(&mut self) { - self.songs.up(); - self.play_selected(); } - pub fn next_song(&mut self) { - self.songs.down(); + pub fn play_index(&mut self, i: usize) { + self.songs.select(Some(i)); self.play_selected(); } - pub fn volume_up(&mut self) { - self.volume += VOLUME_STEP; - - if self.volume > 100 { - self.volume = 100; - } - - self.update_volume(); - } - pub fn volume_down(&mut self) { - if self.volume != 0 { - self.volume -= VOLUME_STEP; - } - - self.update_volume(); - } - fn update_volume(&self) { - if let Some(song) = self.songs.selected() { - let volume = self.volume as f32 / VOLUME_REDUCTION; - - //Calculate the volume with gain - let volume = if song.track_gain == 0.0 { - //Reduce the volume a little to match - //songs with replay gain information. - volume * 0.75 - } else { - volume * song.track_gain as f32 - }; - - self.sink.set_volume(volume); - } else { - self.sink.set_volume(self.volume as f32 / VOLUME_REDUCTION); - } - } - pub fn play_selected(&mut self) { - if let Some(song) = self.songs.selected().cloned() { - self.stop(); - let file = File::open(&song.path).expect("Could not open song."); - let decoder = Decoder::new(file).unwrap(); - - //FIXME: The duration is slightly off for some reason. - self.duration = decoder.total_duration().unwrap().as_secs_f64() - 0.29; - self.sink.append(decoder); - self.update_volume(); - } - } - pub fn delete_song(&mut self, selected: usize) { - self.songs.data.remove(selected); + pub fn delete_index(&mut self, i: usize) { + self.songs.data.remove(i); if let Some(playing) = self.songs.index() { let len = self.songs.len(); @@ -153,63 +93,120 @@ impl Player { return self.clear(); } - if selected == playing && selected == 0 { - if selected == 0 { + if i == playing && i == 0 { + if i == 0 { self.songs.select(Some(0)); } self.play_selected(); - } else if selected == playing && selected == len { + } else if i == playing && i == len { self.songs.select(Some(len - 1)); - } else if selected < playing { + } else if i < playing { self.songs.select(Some(playing - 1)); } }; } - pub fn randomize(&mut self) { - if let Some(song) = &self.songs.selected().cloned() { - self.songs.data.shuffle(&mut thread_rng()); - - let mut index = 0; - for (i, s) in self.songs.data.iter().enumerate() { - if s == song { - index = i; - } - } - self.songs.select(Some(index)); - } - } - pub fn stop(&mut self) { - self.sink = Sink::try_new(&self.handle).expect("Could not create new sink."); - self.update_volume(); - } - pub fn elapsed(&self) -> f64 { - self.sink.elapsed().as_secs_f64() - } - pub fn toggle_playback(&self) { - self.sink.toggle_playback(); - } - pub fn is_paused(&self) -> bool { - self.sink.is_paused() + pub fn clear(&mut self) { + self.songs = Index::default(); + self.stop(); } - pub fn seek_by(&mut self, amount: f64) { - let mut seek = self.elapsed() + amount; - if seek > self.duration { - return self.next_song(); - } else if seek < 0.0 { - seek = 0.0; + pub fn clear_except_playing(&mut self) { + let selected = self.songs.selected().cloned(); + let mut i = 0; + while i < self.songs.len() { + if Some(&self.songs.data[i]) != selected.as_ref() { + self.songs.data.remove(i); + } else { + i += 1; + } } - self.sink.seek(Duration::from_secs_f64(seek)); + self.songs.select(Some(0)); } - pub fn seek_to(&self, time: f64) { - self.sink.seek(Duration::from_secs_f64(time)); - if self.is_paused() { - self.toggle_playback(); + pub fn randomize(&self) {} + pub fn toggle_playback(&mut self) { + if self.playing { + self.pause(); + } else { + self.play(); } } - pub fn update(&mut self) { - if self.elapsed() > self.duration { - self.next_song(); - } + pub fn previous(&self) {} + pub fn next(&self) {} + pub fn volume_up(&self) {} + pub fn volume_down(&self) {} + pub fn is_playing(&self) -> bool { + self.playing + } + pub fn total_songs(&self) -> usize { + self.songs.len() + } + fn play(&mut self) { + self.s.send(Event::Play).unwrap(); + self.playing = true; + } + pub fn elapsed(&self) -> f32 { + 0.0 + } + pub fn get_index(&self) -> &Index<Song> { + &self.songs + } + fn pause(&mut self) { + self.s.send(Event::Pause).unwrap(); + self.playing = false; + } + pub fn selected_song(&self) -> Option<&Song> { + self.songs.selected() + } + pub fn seek_by(&self, duration: f32) { + self.s.send(Event::SeekBy(duration)).unwrap(); + } + pub fn seek_to(&self, duration: f32) { + self.s.send(Event::SeekTo(duration)).unwrap(); + } + pub fn stop(&self) { + self.s.send(Event::Stop).unwrap(); + } + fn run(r: Receiver<Event>, path: PathBuf) { + thread::spawn(move || { + let device = cpal::default_host().default_output_device().unwrap(); + let config = device.default_output_config().unwrap(); + + let processor = Arc::new(RwLock::new(SampleProcessor::new( + Some(config.sample_rate().0), + path, + ))); + + let p = processor.clone(); + + let stream = device + .build_output_stream( + &config.config(), + move |data: &mut [f32], _: &cpal::OutputCallbackInfo| { + for frame in data.chunks_mut(2) { + for sample in frame.iter_mut() { + *sample = p.write().unwrap().next_sample(); + } + } + }, + |err| panic!("{}", err), + ) + .unwrap(); + + stream.play().unwrap(); + + loop { + if let Ok(event) = r.recv() { + dbg!(&event); + match event { + Event::Play => stream.play().unwrap(), + Event::Pause => stream.pause().unwrap(), + Event::SeekBy(_) => (), + Event::SeekTo(_) => (), + // Event::Seek(duration) => processor.write().unwrap().seek_to(duration), + Event::Stop => break, + } + } + } + }); } pub fn audio_devices() -> Vec<Device> { let host_id = cpal::default_host().id(); @@ -222,21 +219,22 @@ impl Player { pub fn default_device() -> Device { cpal::default_host().default_output_device().unwrap() } - pub fn change_output_device(&mut self, device: &Device) -> Result<(), stream::StreamError> { - match OutputStream::try_from_device(device) { - Ok((stream, handle)) => { - let pos = self.elapsed(); - self.stop(); - self.stream = stream; - self.handle = handle; - self.play_selected(); - self.seek_to(pos); - Ok(()) - } - Err(e) => match e { - stream::StreamError::DefaultStreamConfigError(_) => Ok(()), - _ => Err(e), - }, - } + pub fn change_output_device(&mut self, _device: &Device) -> Result<(), StreamError> { + Ok(()) + // match OutputStream::try_from_device(device) { + // Ok((stream, handle)) => { + // let pos = self.elapsed(); + // self.stop(); + // self.stream = stream; + // self.handle = handle; + // self.play_selected(); + // self.seek_to(pos); + // Ok(()) + // } + // Err(e) => match e { + // stream::StreamError::DefaultStreamConfigError(_) => Ok(()), + // _ => Err(e), + // }, + // } } } diff --git a/gonk-player/src/queue.rs b/gonk-player/src/queue.rs deleted file mode 100644 index 11383c27..00000000 --- a/gonk-player/src/queue.rs +++ /dev/null @@ -1,239 +0,0 @@ -//! Queue that plays sounds one after the other. - -use std::sync::mpsc::{Receiver, Sender}; -use std::sync::{mpsc, Arc, Mutex}; -use std::time::Duration; -use std::{ - collections::VecDeque, - sync::atomic::{AtomicBool, Ordering}, -}; - -use crate::conversions::Sample; -use crate::source::{Empty, Source, Zero}; - -/// Builds a new queue. It consists of an input and an output. -/// -/// The input can be used to add sounds to the end of the queue, while the output implements -/// `Source` and plays the sounds. -/// -/// The parameter indicates how the queue should behave if the queue becomes empty: -/// -/// - If you pass `true`, then the queue is infinite and will play a silence instead until you add -/// a new sound. -/// - If you pass `false`, then the queue will report that it has finished playing. -/// -pub fn queue<S>(keep_alive_if_empty: bool) -> (Arc<SourcesQueueInput<S>>, SourcesQueueOutput<S>) -where - S: Sample + Send + 'static, -{ - let input = Arc::new(SourcesQueueInput { - next_sounds: Mutex::new(Vec::new()), - keep_alive_if_empty: AtomicBool::new(keep_alive_if_empty), - }); - - let output = SourcesQueueOutput { - current: Box::new(Empty::<S>::new()) as Box<_>, - signal_after_end: None, - input: input.clone(), - sample_cache: VecDeque::new(), - }; - - (input, output) -} - -type BoxSource<S> = Box<dyn Source<Item = S> + Send>; -type OptionSender = Option<Sender<()>>; - -// TODO: consider reimplementing this with `from_factory` -/// The input of the queue. -pub struct SourcesQueueInput<S> { - next_sounds: Mutex<Vec<(BoxSource<S>, OptionSender)>>, - - // See constructor. - keep_alive_if_empty: AtomicBool, -} - -impl<S> SourcesQueueInput<S> -where - S: Sample + Send + 'static, -{ - /// Adds a new source to the end of the queue. - #[inline] - pub fn append<T>(&self, source: T) - where - T: Source<Item = S> + Send + 'static, - { - self.next_sounds - .lock() - .unwrap() - .push((Box::new(source) as Box<_>, None)); - } - - /// Adds a new source to the end of the queue. - /// - /// The `Receiver` will be signalled when the sound has finished playing. - #[inline] - pub fn append_with_signal<T>(&self, source: T) -> Receiver<()> - where - T: Source<Item = S> + Send + 'static, - { - let (tx, rx) = mpsc::channel(); - self.next_sounds - .lock() - .unwrap() - .push((Box::new(source) as Box<_>, Some(tx))); - rx - } - - /// Sets whether the queue stays alive if there's no more sound to play. - /// - /// See also the constructor. - pub fn set_keep_alive_if_empty(&self, keep_alive_if_empty: bool) { - self.keep_alive_if_empty - .store(keep_alive_if_empty, Ordering::Release); - } -} - -/// The output of the queue. Implements `Source`. -pub struct SourcesQueueOutput<S> { - // The current iterator that produces samples. - current: Box<dyn Source<Item = S> + Send>, - - // Signal this sender before picking from `next`. - signal_after_end: Option<Sender<()>>, - - // The next sounds. - input: Arc<SourcesQueueInput<S>>, - sample_cache: VecDeque<Option<S>>, -} - -impl<S> Source for SourcesQueueOutput<S> -where - S: Sample + Send + 'static, -{ - #[inline] - fn current_frame_len(&self) -> Option<usize> { - // This function is non-trivial because the boundary between two sounds in the queue should - // be a frame boundary as well. - // - // The current sound is free to return `None` for `current_frame_len()`, in which case - // we *should* return the number of samples remaining the current sound. - // This can be estimated with `size_hint()`. - // - // If the `size_hint` is `None` as well, we are in the worst case scenario. To handle this - // situation we force a frame to have a maximum number of samples indicate by this - // constant. - const THRESHOLD: usize = 512; - - // Try the current `current_frame_len`. - if let Some(val) = self.current.current_frame_len() { - if val != 0 { - return Some(val); - } - } - - // Try the size hint. - let (lower_bound, _) = self.current.size_hint(); - // The iterator default implementation just returns 0. - // That's a problematic value, so skip it. - if lower_bound > 0 { - return Some(lower_bound); - } - - // Otherwise we use the constant value. - Some(THRESHOLD) - } - - #[inline] - fn channels(&self) -> u16 { - self.current.channels() - } - - #[inline] - fn sample_rate(&self) -> u32 { - self.current.sample_rate() - } - - #[inline] - fn total_duration(&self) -> Option<Duration> { - None - } - - fn seek(&mut self, time: Duration) -> Option<Duration> { - self.current.seek(time) - } - - fn elapsed(&mut self) -> Duration { - Duration::from_secs(0) - } -} - -impl<S> Iterator for SourcesQueueOutput<S> -where - S: Sample + Send + 'static, -{ - type Item = S; - - #[inline] - fn next(&mut self) -> Option<S> { - loop { - if !self.sample_cache.is_empty() { - return self.sample_cache.pop_front().unwrap(); - } - // Basic situation that will happen most of the time. - if let Some(sample) = self.current.next() { - return Some(sample); - } - - // Since `self.current` has finished, we need to pick the next sound. - // In order to avoid inlining this expensive operation, the code is in another function. - if self.go_next().is_err() { - return None; - }; - } - } - - #[inline] - fn size_hint(&self) -> (usize, Option<usize>) { - (self.current.size_hint().0, None) - } -} - -impl<S> SourcesQueueOutput<S> -where - S: Sample + Send + 'static, -{ - // Called when `current` is empty and we must jump to the next element. - // Returns `Ok` if the sound should continue playing, or an error if it should stop. - // - // This method is separate so that it is not inlined. - fn go_next(&mut self) -> Result<(), ()> { - if let Some(signal_after_end) = self.signal_after_end.take() { - let _ = signal_after_end.send(()); - } - - let (next, signal_after_end) = { - let mut next = self.input.next_sounds.lock().unwrap(); - - if next.len() == 0 { - if self.input.keep_alive_if_empty.load(Ordering::Acquire) { - // Play a short silence in order to avoid spinlocking. - let silence = Zero::<S>::new(1, 44100); // TODO: meh - ( - Box::new(silence.take_duration(Duration::from_millis(10))) as Box<_>, - None, - ) - } else { - return Err(()); - } - } else { - next.remove(0) - } - }; - - self.current = next; - - self.signal_after_end = signal_after_end; - Ok(()) - } -} diff --git a/gonk-player/src/sample_processor.rs b/gonk-player/src/sample_processor.rs new file mode 100644 index 00000000..10049508 --- /dev/null +++ b/gonk-player/src/sample_processor.rs @@ -0,0 +1,108 @@ +use crate::sample_rate::SampleRateConverter; +use std::{fs::File, path::Path, time::Duration}; +use symphonia::{ + core::{ + audio::{SampleBuffer, SignalSpec}, + codecs::{Decoder, DecoderOptions}, + formats::{FormatOptions, FormatReader, SeekMode, SeekTo}, + io::MediaSourceStream, + meta::MetadataOptions, + probe::Hint, + units::Time, + }, + default::get_probe, +}; + +pub struct SampleProcessor { + pub decoder: Box<dyn Decoder>, + pub format: Box<dyn FormatReader>, + pub spec: SignalSpec, + pub duration: u64, + pub converter: SampleRateConverter, + pub finished: bool, + pub left: bool, +} + +impl SampleProcessor { + pub fn next_sample(&mut self) -> f32 { + loop { + if let Some(sample) = self.converter.next() { + return sample * 0.1; + } else { + self.update(); + } + } + } + pub fn update(&mut self) { + match self.format.next_packet() { + Ok(packet) => { + let decoded = self.decoder.decode(&packet).unwrap(); + let mut buffer = SampleBuffer::<f32>::new(self.duration, self.spec); + buffer.copy_interleaved_ref(decoded); + + self.converter.update(buffer.samples().to_vec().into_iter()); + } + Err(e) => match e { + symphonia::core::errors::Error::IoError(_) => self.finished = true, + _ => panic!("{:?}", e), + }, + } + } + pub fn seek_to(&mut self, time: Duration) { + let nanos_per_sec = 1_000_000_000.0; + self.format + .seek( + SeekMode::Coarse, + SeekTo::Time { + time: Time::new(time.as_secs(), time.subsec_nanos() as f64 / nanos_per_sec), + track_id: None, + }, + ) + .unwrap(); + } + pub fn new(sample_rate: Option<u32>, path: impl AsRef<Path>) -> Self { + let source = Box::new(File::open(path).unwrap()); + + let mss = MediaSourceStream::new(source, Default::default()); + + let mut probed = get_probe() + .format( + &Hint::default(), + mss, + &FormatOptions { + prebuild_seek_index: true, + ..Default::default() + }, + &MetadataOptions::default(), + ) + .unwrap(); + + let track = probed.format.default_track().unwrap(); + let mut decoder = symphonia::default::get_codecs() + .make(&track.codec_params, &DecoderOptions::default()) + .unwrap(); + + let current_frame = probed.format.next_packet().unwrap(); + let decoded = decoder.decode(¤t_frame).unwrap(); + + let spec = decoded.spec().to_owned(); + let duration = decoded.capacity() as u64; + + let mut sample_buffer = SampleBuffer::<f32>::new(duration, spec); + sample_buffer.copy_interleaved_ref(decoded); + + Self { + format: probed.format, + decoder, + spec, + duration, + converter: SampleRateConverter::new( + sample_buffer.samples().to_vec().into_iter(), + spec.rate, + sample_rate.unwrap_or(44100), + ), + finished: false, + left: true, + } + } +} diff --git a/gonk-player/src/conversions/sample_rate.rs b/gonk-player/src/sample_rate.rs similarity index 53% rename from gonk-player/src/conversions/sample_rate.rs rename to gonk-player/src/sample_rate.rs index fe4ebea5..de907ff8 100644 --- a/gonk-player/src/conversions/sample_rate.rs +++ b/gonk-player/src/sample_rate.rs @@ -1,113 +1,72 @@ -use crate::conversions::Sample; - -use std::mem; +//https://github.com/RustAudio/rodio/blob/master/src/conversions/sample_rate.rs +use std::{mem, vec::IntoIter}; + +#[inline] +const fn gcd(a: u32, b: u32) -> u32 { + if b == 0 { + a + } else { + gcd(b, a % b) + } +} /// Iterator that converts from a certain sample rate to another. -#[derive(Clone, Debug)] -pub struct SampleRateConverter<I> -where - I: Iterator, -{ +pub struct SampleRateConverter { /// The iterator that gives us samples. - input: I, + input: IntoIter<f32>, /// We convert chunks of `from` samples into chunks of `to` samples. from: u32, /// We convert chunks of `from` samples into chunks of `to` samples. to: u32, - /// Number of channels in the stream - channels: cpal::ChannelCount, /// One sample per channel, extracted from `input`. - current_frame: Vec<I::Item>, + current_frame: Vec<f32>, /// Position of `current_sample` modulo `from`. current_frame_pos_in_chunk: u32, /// The samples right after `current_sample` (one per channel), extracted from `input`. - next_frame: Vec<I::Item>, + next_frame: Vec<f32>, /// The position of the next sample that the iterator should return, modulo `to`. /// This counter is incremented (modulo `to`) every time the iterator is called. next_output_frame_pos_in_chunk: u32, /// The buffer containing the samples waiting to be output. - output_buffer: Vec<I::Item>, + output_buffer: Vec<f32>, } -impl<I> SampleRateConverter<I> -where - I: Iterator, - I::Item: Sample, -{ - /// - /// - /// # Panic - /// - /// Panics if `from` or `to` are equal to 0. - /// - #[inline] - pub fn new( - mut input: I, - from: cpal::SampleRate, - to: cpal::SampleRate, - num_channels: cpal::ChannelCount, - ) -> SampleRateConverter<I> { - let from = from.0; - let to = to.0; - - assert!(from >= 1); - assert!(to >= 1); +impl SampleRateConverter { + pub fn new(mut input: IntoIter<f32>, from_rate: u32, to_rate: u32) -> SampleRateConverter { + assert!(from_rate >= 1); + assert!(to_rate >= 1); // finding greatest common divisor - let gcd = { - #[inline] - fn gcd(a: u32, b: u32) -> u32 { - if b == 0 { - a - } else { - gcd(b, a % b) - } - } + let gcd = gcd(from_rate, to_rate); - gcd(from, to) - }; - - let (first_samples, next_samples) = if from == to { + let (first_samples, next_samples) = if from_rate == to_rate { // if `from` == `to` == 1, then we just pass through - debug_assert_eq!(from, gcd); + debug_assert_eq!(from_rate, gcd); (Vec::new(), Vec::new()) } else { - let first = input - .by_ref() - .take(num_channels as usize) - .collect::<Vec<_>>(); - let next = input - .by_ref() - .take(num_channels as usize) - .collect::<Vec<_>>(); + let first = vec![input.next().unwrap(), input.next().unwrap()]; + let next = vec![input.next().unwrap(), input.next().unwrap()]; (first, next) }; SampleRateConverter { input, - from: from / gcd, - to: to / gcd, - channels: num_channels, + from: from_rate / gcd, + to: to_rate / gcd, current_frame_pos_in_chunk: 0, next_output_frame_pos_in_chunk: 0, current_frame: first_samples, next_frame: next_samples, - output_buffer: Vec::with_capacity(num_channels as usize - 1), + output_buffer: Vec::with_capacity(1), } } - /// Destroys this iterator and returns the underlying iterator. - #[inline] - pub fn into_inner(self) -> I { - self.input - } - fn next_input_frame(&mut self) { self.current_frame_pos_in_chunk += 1; mem::swap(&mut self.current_frame, &mut self.next_frame); self.next_frame.clear(); - for _ in 0..self.channels { + for _ in 0..2 { if let Some(i) = self.input.next() { self.next_frame.push(i); } else { @@ -115,19 +74,20 @@ where } } } -} -impl<I> Iterator for SampleRateConverter<I> -where - I: Iterator, - I::Item: Sample + Clone, -{ - type Item = I::Item; + pub fn update(&mut self, mut input: IntoIter<f32>) { + let current_frame = vec![input.next().unwrap(), input.next().unwrap()]; + let next_frame = vec![input.next().unwrap(), input.next().unwrap()]; + self.input = input; + self.current_frame = current_frame; + self.next_frame = next_frame; + self.current_frame_pos_in_chunk = 0; + self.next_output_frame_pos_in_chunk = 0; + } - fn next(&mut self) -> Option<I::Item> { + pub fn next(&mut self) -> Option<f32> { // the algorithm below doesn't work if `self.from == self.to` if self.from == self.to { - debug_assert_eq!(self.from, 1); return self.input.next(); } @@ -173,7 +133,7 @@ where .zip(self.next_frame.iter()) .enumerate() { - let sample = Sample::lerp(*cur, *next, numerator, self.to); + let sample = cur + (next - cur) * numerator as f32 / self.to as f32; if off == 0 { result = Some(sample); @@ -201,42 +161,4 @@ where } } } - - #[inline] - fn size_hint(&self) -> (usize, Option<usize>) { - let apply = |samples: usize| { - // `samples_after_chunk` will contain the number of samples remaining after the chunk - // currently being processed - let samples_after_chunk = samples; - // adding the samples of the next chunk that may have already been read - let samples_after_chunk = if self.current_frame_pos_in_chunk == self.from - 1 { - samples_after_chunk + self.next_frame.len() - } else { - samples_after_chunk - }; - // removing the samples of the current chunk that have not yet been read - let samples_after_chunk = samples_after_chunk.saturating_sub( - self.from - .saturating_sub(self.current_frame_pos_in_chunk + 2) as usize - * usize::from(self.channels), - ); - // calculating the number of samples after the transformation - // TODO: this is wrong here \|/ - let samples_after_chunk = samples_after_chunk * self.to as usize / self.from as usize; - - // `samples_current_chunk` will contain the number of samples remaining to be output - // for the chunk currently being processed - let samples_current_chunk = (self.to - self.next_output_frame_pos_in_chunk) as usize - * usize::from(self.channels); - - samples_current_chunk + samples_after_chunk + self.output_buffer.len() - }; - - if self.from == self.to { - self.input.size_hint() - } else { - let (min, max) = self.input.size_hint(); - (apply(min), max.map(apply)) - } - } } diff --git a/gonk-player/src/sink.rs b/gonk-player/src/sink.rs deleted file mode 100644 index 07b911e5..00000000 --- a/gonk-player/src/sink.rs +++ /dev/null @@ -1,193 +0,0 @@ -use std::sync::atomic::{AtomicBool, AtomicUsize, Ordering}; -use std::sync::mpsc::Receiver; -use std::sync::{Arc, Mutex, RwLock}; -use std::time::Duration; - -use crate::stream::{OutputStreamHandle, PlayError}; -use crate::{conversions::Sample, queue, source::Done, source::Source}; - -/// Handle to an device that outputs sounds. -/// -/// Dropping the `Sink` stops all sounds. You can use `detach` if you want the sounds to continue -/// playing. -pub struct Sink { - queue_tx: Arc<queue::SourcesQueueInput<f32>>, - sleep_until_end: Mutex<Option<Receiver<()>>>, - - controls: Arc<Controls>, - sound_count: Arc<AtomicUsize>, - - detached: bool, - - elapsed: Arc<RwLock<Duration>>, -} - -struct Controls { - pause: AtomicBool, - volume: Mutex<f32>, - seek: Mutex<Option<Duration>>, - stopped: AtomicBool, -} - -impl Sink { - /// Builds a new `Sink`, beginning playback on a stream. - #[inline] - pub fn try_new(stream: &OutputStreamHandle) -> Result<Sink, PlayError> { - let (sink, queue_rx) = Sink::new_idle(); - stream.play_raw(queue_rx)?; - Ok(sink) - } - - /// Builds a new `Sink`. - #[inline] - pub fn new_idle() -> (Sink, queue::SourcesQueueOutput<f32>) { - let (queue_tx, queue_rx) = queue::queue(true); - - let sink = Sink { - queue_tx, - sleep_until_end: Mutex::new(None), - controls: Arc::new(Controls { - pause: AtomicBool::new(false), - volume: Mutex::new(1.0), - stopped: AtomicBool::new(false), - seek: Mutex::new(None), - }), - sound_count: Arc::new(AtomicUsize::new(0)), - detached: false, - elapsed: Arc::new(RwLock::new(Duration::from_secs(0))), - }; - (sink, queue_rx) - } - - /// Appends a sound to the queue of sounds to play. - #[inline] - pub fn append<S>(&self, source: S) - where - S: Source + Send + 'static, - S::Item: Sample, - S::Item: Send, - { - let controls = self.controls.clone(); - - let elapsed = self.elapsed.clone(); - let source = source - .pausable(false) - .amplify(1.0) - .stoppable() - .periodic_access(Duration::from_millis(5), move |src| { - if controls.stopped.load(Ordering::SeqCst) { - src.stop(); - } else { - if let Some(seek_time) = controls.seek.lock().unwrap().take() { - src.seek(seek_time).unwrap(); - } - *elapsed.write().unwrap() = src.elapsed(); - src.inner_mut().set_factor(*controls.volume.lock().unwrap()); - src.inner_mut() - .inner_mut() - .set_paused(controls.pause.load(Ordering::SeqCst)); - } - }) - .convert_samples(); - self.sound_count.fetch_add(1, Ordering::Relaxed); - let source = Done::new(source, self.sound_count.clone()); - *self.sleep_until_end.lock().unwrap() = Some(self.queue_tx.append_with_signal(source)); - } - - /// Gets the volume of the sound. - /// - /// The value `1.0` is the "normal" volume (unfiltered input). Any value other than 1.0 will - /// multiply each sample by this value. - #[inline] - pub fn volume(&self) -> f32 { - *self.controls.volume.lock().unwrap() - } - - /// Changes the volume of the sound. - /// - /// The value `1.0` is the "normal" volume (unfiltered input). Any value other than `1.0` will - /// multiply each sample by this value. - #[inline] - pub fn set_volume(&self, value: f32) { - *self.controls.volume.lock().unwrap() = value; - } - - /// Resumes playback of a paused sink. - /// - /// No effect if not paused. - #[inline] - pub fn play(&self) { - self.controls.pause.store(false, Ordering::SeqCst); - } - - /// Pauses playback of this sink. - /// - /// No effect if already paused. - /// - /// A paused sink can be resumed with `play()`. - pub fn pause(&self) { - self.controls.pause.store(true, Ordering::SeqCst); - } - - /// Toggles playback of the sink - pub fn toggle_playback(&self) { - if self.is_paused() { - self.play(); - } else { - self.pause(); - } - } - - pub fn seek(&self, seek_time: Duration) { - *self.controls.seek.lock().unwrap() = Some(seek_time); - } - - /// Gets if a sink is paused - /// - /// Sinks can be paused and resumed using `pause()` and `play()`. This returns `true` if the - /// sink is paused. - pub fn is_paused(&self) -> bool { - self.controls.pause.load(Ordering::SeqCst) - } - - /// Destroys the sink without stopping the sounds that are still playing. - #[inline] - pub fn detach(mut self) { - self.detached = true; - } - - /// Sleeps the current thread until the sound ends. - #[inline] - pub fn sleep_until_end(&self) { - if let Some(sleep_until_end) = self.sleep_until_end.lock().unwrap().take() { - let _ = sleep_until_end.recv(); - } - } - /// Returns true if this sink has no more sounds to play. - #[inline] - pub fn is_empty(&self) -> bool { - self.len() == 0 - } - - /// Returns the number of sounds currently in the queue. - #[inline] - pub fn len(&self) -> usize { - self.sound_count.load(Ordering::Relaxed) - } - - #[inline] - pub fn elapsed(&self) -> Duration { - *self.elapsed.read().unwrap() - } -} - -impl Drop for Sink { - #[inline] - fn drop(&mut self) { - self.queue_tx.set_keep_alive_if_empty(false); - - if !self.detached { - self.controls.stopped.store(true, Ordering::Relaxed); - } - } -} diff --git a/gonk-player/src/source/amplify.rs b/gonk-player/src/source/amplify.rs deleted file mode 100644 index 48919ad9..00000000 --- a/gonk-player/src/source/amplify.rs +++ /dev/null @@ -1,105 +0,0 @@ -use std::time::Duration; - -use crate::{conversions::Sample, source::Source}; - -/// Internal function that builds a `Amplify` object. -pub fn amplify<I>(input: I, factor: f32) -> Amplify<I> -where - I: Source, - I::Item: Sample, -{ - Amplify { input, factor } -} - -/// Filter that modifies each sample by a given value. -#[derive(Clone, Debug)] -pub struct Amplify<I> { - input: I, - factor: f32, -} - -impl<I> Amplify<I> { - /// Modifies the amplification factor. - #[inline] - pub fn set_factor(&mut self, factor: f32) { - self.factor = factor; - } - - /// Returns a reference to the inner source. - #[inline] - pub fn inner(&self) -> &I { - &self.input - } - - /// Returns a mutable reference to the inner source. - #[inline] - pub fn inner_mut(&mut self) -> &mut I { - &mut self.input - } - - /// Returns the inner source. - #[inline] - pub fn into_inner(self) -> I { - self.input - } -} - -impl<I> Iterator for Amplify<I> -where - I: Source, - I::Item: Sample, -{ - type Item = I::Item; - - #[inline] - fn next(&mut self) -> Option<I::Item> { - self.input.next().map(|value| value.amplify(self.factor)) - } - - #[inline] - fn size_hint(&self) -> (usize, Option<usize>) { - self.input.size_hint() - } -} - -impl<I> ExactSizeIterator for Amplify<I> -where - I: Source + ExactSizeIterator, - I::Item: Sample, -{ -} - -impl<I> Source for Amplify<I> -where - I: Source, - I::Item: Sample, -{ - #[inline] - fn current_frame_len(&self) -> Option<usize> { - self.input.current_frame_len() - } - - #[inline] - fn channels(&self) -> u16 { - self.input.channels() - } - - #[inline] - fn sample_rate(&self) -> u32 { - self.input.sample_rate() - } - - #[inline] - fn total_duration(&self) -> Option<Duration> { - self.input.total_duration() - } - - #[inline] - fn elapsed(&mut self) -> Duration { - self.input.elapsed() - } - - fn seek(&mut self, time: Duration) -> Option<Duration> { - self.input.seek(time) - } -} diff --git a/gonk-player/src/source/done.rs b/gonk-player/src/source/done.rs deleted file mode 100644 index aafcf638..00000000 --- a/gonk-player/src/source/done.rs +++ /dev/null @@ -1,98 +0,0 @@ -use std::sync::atomic::{AtomicUsize, Ordering}; -use std::sync::Arc; -use std::time::Duration; - -use crate::{conversions::Sample, source::Source}; - -/// When the inner source is empty this decrements an `AtomicUsize`. -#[derive(Debug, Clone)] -pub struct Done<I> { - input: I, - signal: Arc<AtomicUsize>, - signal_sent: bool, -} - -impl<I> Done<I> { - #[inline] - pub fn new(input: I, signal: Arc<AtomicUsize>) -> Done<I> { - Done { - input, - signal, - signal_sent: false, - } - } - - /// Returns a reference to the inner source. - #[inline] - pub fn inner(&self) -> &I { - &self.input - } - - /// Returns a mutable reference to the inner source. - #[inline] - pub fn inner_mut(&mut self) -> &mut I { - &mut self.input - } - - /// Returns the inner source. - #[inline] - pub fn into_inner(self) -> I { - self.input - } -} - -impl<I: Source> Iterator for Done<I> -where - I: Source, - I::Item: Sample, -{ - type Item = I::Item; - - #[inline] - fn next(&mut self) -> Option<I::Item> { - let next = self.input.next(); - if !self.signal_sent && next.is_none() { - self.signal.fetch_sub(1, Ordering::Relaxed); - self.signal_sent = true; - } - next - } - - fn size_hint(&self) -> (usize, Option<usize>) { - self.input.size_hint() - } -} - -impl<I> Source for Done<I> -where - I: Source, - I::Item: Sample, -{ - #[inline] - fn current_frame_len(&self) -> Option<usize> { - self.input.current_frame_len() - } - - #[inline] - fn channels(&self) -> u16 { - self.input.channels() - } - - #[inline] - fn sample_rate(&self) -> u32 { - self.input.sample_rate() - } - - #[inline] - fn total_duration(&self) -> Option<Duration> { - self.input.total_duration() - } - - #[inline] - fn elapsed(&mut self) -> Duration { - self.input.elapsed() - } - fn seek(&mut self, time: Duration) -> Option<Duration> { - self.input.seek(time) - } -} diff --git a/gonk-player/src/source/empty.rs b/gonk-player/src/source/empty.rs deleted file mode 100644 index 0dcdfbf5..00000000 --- a/gonk-player/src/source/empty.rs +++ /dev/null @@ -1,64 +0,0 @@ -use std::marker::PhantomData; -use std::time::Duration; - -use crate::{conversions::Sample, source::Source}; - -/// An empty source. -#[derive(Debug, Copy, Clone)] -pub struct Empty<S>(PhantomData<S>); - -impl<S> Default for Empty<S> { - #[inline] - fn default() -> Self { - Self::new() - } -} - -impl<S> Empty<S> { - #[inline] - pub fn new() -> Empty<S> { - Empty(PhantomData) - } -} - -impl<S> Iterator for Empty<S> { - type Item = S; - - #[inline] - fn next(&mut self) -> Option<S> { - None - } -} - -impl<S> Source for Empty<S> -where - S: Sample, -{ - #[inline] - fn current_frame_len(&self) -> Option<usize> { - None - } - - #[inline] - fn channels(&self) -> u16 { - 1 - } - - #[inline] - fn sample_rate(&self) -> u32 { - 48000 - } - - #[inline] - fn total_duration(&self) -> Option<Duration> { - Some(Duration::new(0, 0)) - } - - #[inline] - fn elapsed(&mut self) -> Duration { - Duration::from_secs(0) - } - fn seek(&mut self, time: Duration) -> Option<Duration> { - Some(time) - } -} diff --git a/gonk-player/src/source/fadein.rs b/gonk-player/src/source/fadein.rs deleted file mode 100644 index b354b8e5..00000000 --- a/gonk-player/src/source/fadein.rs +++ /dev/null @@ -1,116 +0,0 @@ -use crate::{conversions::Sample, source::Source}; -use std::time::Duration; - -/// Internal function that builds a `FadeIn` object. -pub fn fadein<I>(input: I, duration: Duration) -> FadeIn<I> -where - I: Source, - I::Item: Sample, -{ - let duration = duration.as_secs() * 1000000000 + duration.subsec_nanos() as u64; - - FadeIn { - input, - remaining_ns: duration as f32, - total_ns: duration as f32, - } -} - -/// Filter that modifies raises the volume from silence over a time period. -#[derive(Clone, Debug)] -pub struct FadeIn<I> { - input: I, - remaining_ns: f32, - total_ns: f32, -} - -impl<I> FadeIn<I> -where - I: Source, - I::Item: Sample, -{ - /// Returns a reference to the inner source. - #[inline] - pub fn inner(&self) -> &I { - &self.input - } - - /// Returns a mutable reference to the inner source. - #[inline] - pub fn inner_mut(&mut self) -> &mut I { - &mut self.input - } - - /// Returns the inner source. - #[inline] - pub fn into_inner(self) -> I { - self.input - } -} - -impl<I> Iterator for FadeIn<I> -where - I: Source, - I::Item: Sample, -{ - type Item = I::Item; - - #[inline] - fn next(&mut self) -> Option<I::Item> { - if self.remaining_ns <= 0.0 { - return self.input.next(); - } - - let factor = 1.0 - self.remaining_ns / self.total_ns; - self.remaining_ns -= - 1000000000.0 / (self.input.sample_rate() as f32 * self.channels() as f32); - self.input.next().map(|value| value.amplify(factor)) - } - - #[inline] - fn size_hint(&self) -> (usize, Option<usize>) { - self.input.size_hint() - } -} - -impl<I> ExactSizeIterator for FadeIn<I> -where - I: Source + ExactSizeIterator, - I::Item: Sample, -{ -} - -impl<I> Source for FadeIn<I> -where - I: Source, - I::Item: Sample, -{ - #[inline] - fn current_frame_len(&self) -> Option<usize> { - self.input.current_frame_len() - } - - #[inline] - fn channels(&self) -> u16 { - self.input.channels() - } - - #[inline] - fn sample_rate(&self) -> u32 { - self.input.sample_rate() - } - - #[inline] - fn total_duration(&self) -> Option<Duration> { - self.input.total_duration() - } - - #[inline] - fn elapsed(&mut self) -> Duration { - self.input.elapsed() - } - - fn seek(&mut self, time: Duration) -> Option<Duration> { - self.input.seek(time) - } -} diff --git a/gonk-player/src/source/mod.rs b/gonk-player/src/source/mod.rs deleted file mode 100644 index 0ade1096..00000000 --- a/gonk-player/src/source/mod.rs +++ /dev/null @@ -1,203 +0,0 @@ -//! Sources of sound and various filters. - -use std::time::Duration; - -use crate::conversions::Sample; - -pub use self::amplify::Amplify; -pub use self::done::Done; -pub use self::empty::Empty; -pub use self::fadein::FadeIn; -pub use self::pausable::Pausable; -pub use self::periodic::PeriodicAccess; -pub use self::samples_converter::SamplesConverter; -pub use self::stoppable::Stoppable; -pub use self::take::TakeDuration; -pub use self::uniform::UniformSourceIterator; -pub use self::zero::Zero; - -mod amplify; -mod done; -mod empty; -mod fadein; -mod pausable; -mod periodic; -mod samples_converter; -mod stoppable; -mod take; -mod uniform; -mod zero; - -/// A source of samples. -/// -/// # A quick lesson about sounds -/// -/// ## Sampling -/// -/// A sound is a vibration that propagates through air and reaches your ears. This vibration can -/// be represented as an analog signal. -/// -/// In order to store this signal in the computer's memory or on the disk, we perform what is -/// called *sampling*. The consists in choosing an interval of time (for example 20µs) and reading -/// the amplitude of the signal at each interval (for example, if the interval is 20µs we read the -/// amplitude every 20µs). By doing so we obtain a list of numerical values, each value being -/// called a *sample*. -/// -/// Therefore a sound can be represented in memory by a frequency and a list of samples. The -/// frequency is expressed in hertz and corresponds to the number of samples that have been -/// read per second. For example if we read one sample every 20µs, the frequency would be -/// 50000 Hz. In reality, common values for the frequency are 44100, 48000 and 96000. -/// -/// ## Channels -/// -/// But a frequency and a list of values only represent one signal. When you listen to a sound, -/// your left and right ears don't receive exactly the same signal. In order to handle this, -/// we usually record not one but two different signals: one for the left ear and one for the right -/// ear. We say that such a sound has two *channels*. -/// -/// Sometimes sounds even have five or six channels, each corresponding to a location around the -/// head of the listener. -/// -/// The standard in audio manipulation is to *interleave* the multiple channels. In other words, -/// in a sound with two channels the list of samples contains the first sample of the first -/// channel, then the first sample of the second channel, then the second sample of the first -/// channel, then the second sample of the second channel, and so on. The same applies if you have -/// more than two channels. The rodio library only supports this schema. -/// -/// Therefore in order to represent a sound in memory in fact we need three characteristics: the -/// frequency, the number of channels, and the list of samples. -/// -/// ## The `Source` trait -/// -/// A Rust object that represents a sound should implement the `Source` trait. -/// -/// The three characteristics that describe a sound are provided through this trait: -/// -/// - The number of channels can be retrieved with `channels`. -/// - The frequency can be retrieved with `sample_rate`. -/// - The list of values can be retrieved by iterating on the source. The `Source` trait requires -/// that the `Iterator` trait be implemented as well. -/// -/// # Frames -/// -/// The samples rate and number of channels of some sound sources can change by itself from time -/// to time. -/// -/// > **Note**: As a basic example, if you play two audio files one after the other and treat the -/// > whole as a single source, then the channels and samples rate of that source may change at the -/// > transition between the two files. -/// -/// However, for optimization purposes rodio supposes that the number of channels and the frequency -/// stay the same for long periods of time and avoids calling `channels()` and -/// `sample_rate` too frequently. -/// -/// In order to properly handle this situation, the `current_frame_len()` method should return -/// the number of samples that remain in the iterator before the samples rate and number of -/// channels can potentially change. -/// -pub trait Source: Iterator -where - Self::Item: Sample, -{ - /// Returns the number of samples before the current frame ends. `None` means "infinite" or - /// "until the sound ends". - /// Should never return 0 unless there's no more data. - /// - /// After the engine has finished reading the specified number of samples, it will check - /// whether the value of `channels()` and/or `sample_rate()` have changed. - fn current_frame_len(&self) -> Option<usize>; - - /// Returns the number of channels. Channels are always interleaved. - fn channels(&self) -> u16; - - /// Returns the rate at which the source should be played. In number of samples per second. - fn sample_rate(&self) -> u32; - - /// Returns the total duration of this source, if known. - /// - /// `None` indicates at the same time "infinite" or "unknown". - fn total_duration(&self) -> Option<Duration>; - - fn seek(&mut self, time: Duration) -> Option<Duration>; - - fn elapsed(&mut self) -> Duration; - - /// Takes a certain duration of this source and then stops. - #[inline] - fn take_duration(self, duration: Duration) -> TakeDuration<Self> - where - Self: Sized, - { - take::take_duration(self, duration) - } - - /// Immediately skips a certain duration of this source. - /// - /// If the specified duration is longer than the source itself, `skip_duration` will skip to the end of the source. - - /// Amplifies the sound by the given value. - #[inline] - fn amplify(self, value: f32) -> Amplify<Self> - where - Self: Sized, - { - amplify::amplify(self, value) - } - - /// Fades in the sound. - #[inline] - fn fade_in(self, duration: Duration) -> FadeIn<Self> - where - Self: Sized, - { - fadein::fadein(self, duration) - } - - /// Calls the `access` closure on `Self` the first time the source is iterated and every - /// time `period` elapses. - /// - /// Later changes in either `sample_rate()` or `channels_count()` won't be reflected in - /// the rate of access. - /// - /// The rate is based on playback speed, so both the following will call `access` when the - /// same samples are reached: - /// `periodic_access(Duration::from_secs(1), ...).speed(2.0)` - /// `speed(2.0).periodic_access(Duration::from_secs(2), ...)` - #[inline] - fn periodic_access<F>(self, period: Duration, access: F) -> PeriodicAccess<Self, F> - where - Self: Sized, - F: FnMut(&mut Self), - { - periodic::periodic(self, period, access) - } - - /// Converts the samples of this source to another type. - #[inline] - fn convert_samples<D>(self) -> SamplesConverter<Self, D> - where - Self: Sized, - D: Sample, - { - SamplesConverter::new(self) - } - - /// Makes the sound pausable. - // TODO: add example - #[inline] - fn pausable(self, initially_paused: bool) -> Pausable<Self> - where - Self: Sized, - { - pausable::pausable(self, initially_paused) - } - - /// Makes the sound stoppable. - #[inline] - fn stoppable(self) -> Stoppable<Self> - where - Self: Sized, - { - stoppable::stoppable(self) - } -} diff --git a/gonk-player/src/source/pausable.rs b/gonk-player/src/source/pausable.rs deleted file mode 100644 index 6b134408..00000000 --- a/gonk-player/src/source/pausable.rs +++ /dev/null @@ -1,125 +0,0 @@ -use std::time::Duration; - -use crate::{conversions::Sample, source::Source}; - -/// Internal function that builds a `Pausable` object. -pub fn pausable<I>(source: I, paused: bool) -> Pausable<I> -where - I: Source, - I::Item: Sample, -{ - let paused_channels = if paused { - Some(source.channels()) - } else { - None - }; - Pausable { - input: source, - paused_channels, - remaining_paused_samples: 0, - } -} - -#[derive(Clone, Debug)] -pub struct Pausable<I> { - input: I, - paused_channels: Option<u16>, - remaining_paused_samples: u16, -} - -impl<I> Pausable<I> -where - I: Source, - I::Item: Sample, -{ - /// Sets whether the filter applies. - /// - /// If set to true, the inner sound stops playing and no samples are processed from it. - #[inline] - pub fn set_paused(&mut self, paused: bool) { - match (self.paused_channels, paused) { - (None, true) => self.paused_channels = Some(self.input.channels()), - (Some(_), false) => self.paused_channels = None, - _ => (), - } - } - - /// Returns a reference to the inner source. - #[inline] - pub fn inner(&self) -> &I { - &self.input - } - - /// Returns a mutable reference to the inner source. - #[inline] - pub fn inner_mut(&mut self) -> &mut I { - &mut self.input - } - - /// Returns the inner source. - #[inline] - pub fn into_inner(self) -> I { - self.input - } -} - -impl<I> Iterator for Pausable<I> -where - I: Source, - I::Item: Sample, -{ - type Item = I::Item; - - #[inline] - fn next(&mut self) -> Option<I::Item> { - if self.remaining_paused_samples > 0 { - self.remaining_paused_samples -= 1; - return Some(I::Item::zero_value()); - } - - if let Some(paused_channels) = self.paused_channels { - self.remaining_paused_samples = paused_channels - 1; - return Some(I::Item::zero_value()); - } - - self.input.next() - } - - #[inline] - fn size_hint(&self) -> (usize, Option<usize>) { - self.input.size_hint() - } -} - -impl<I> Source for Pausable<I> -where - I: Source, - I::Item: Sample, -{ - #[inline] - fn current_frame_len(&self) -> Option<usize> { - self.input.current_frame_len() - } - - #[inline] - fn channels(&self) -> u16 { - self.input.channels() - } - - #[inline] - fn sample_rate(&self) -> u32 { - self.input.sample_rate() - } - - #[inline] - fn total_duration(&self) -> Option<Duration> { - self.input.total_duration() - } - #[inline] - fn elapsed(&mut self) -> Duration { - self.input.elapsed() - } - fn seek(&mut self, time: Duration) -> Option<Duration> { - self.input.seek(time) - } -} diff --git a/gonk-player/src/source/periodic.rs b/gonk-player/src/source/periodic.rs deleted file mode 100644 index 41cbf269..00000000 --- a/gonk-player/src/source/periodic.rs +++ /dev/null @@ -1,127 +0,0 @@ -use std::time::Duration; - -use crate::{conversions::Sample, source::Source}; - -/// Internal function that builds a `PeriodicAccess` object. -pub fn periodic<I, F>(source: I, period: Duration, modifier: F) -> PeriodicAccess<I, F> -where - I: Source, - I::Item: Sample, -{ - // TODO: handle the fact that the samples rate can change - // TODO: generally, just wrong - let update_ms = period.as_secs() as u32 * 1_000 + period.subsec_millis(); - let update_frequency = (update_ms * source.sample_rate()) / 1000 * source.channels() as u32; - - PeriodicAccess { - input: source, - modifier, - // Can overflow when subtracting if this is 0 - update_frequency: if update_frequency == 0 { - 1 - } else { - update_frequency - }, - samples_until_update: 1, - } -} - -/// Calls a function on a source every time a period elapsed. -#[derive(Clone, Debug)] -pub struct PeriodicAccess<I, F> { - // The inner source. - input: I, - - // Closure that gets access to `inner`. - modifier: F, - - // The frequency with which local_volume should be updated by remote_volume - update_frequency: u32, - - // How many samples remain until it is time to update local_volume with remote_volume. - samples_until_update: u32, -} - -impl<I, F> PeriodicAccess<I, F> -where - I: Source, - I::Item: Sample, - F: FnMut(&mut I), -{ - /// Returns a reference to the inner source. - #[inline] - pub fn inner(&self) -> &I { - &self.input - } - - /// Returns a mutable reference to the inner source. - #[inline] - pub fn inner_mut(&mut self) -> &mut I { - &mut self.input - } - - /// Returns the inner source. - #[inline] - pub fn into_inner(self) -> I { - self.input - } -} - -impl<I, F> Iterator for PeriodicAccess<I, F> -where - I: Source, - I::Item: Sample, - F: FnMut(&mut I), -{ - type Item = I::Item; - - #[inline] - fn next(&mut self) -> Option<I::Item> { - self.samples_until_update -= 1; - if self.samples_until_update == 0 { - (self.modifier)(&mut self.input); - self.samples_until_update = self.update_frequency; - } - - self.input.next() - } - - #[inline] - fn size_hint(&self) -> (usize, Option<usize>) { - self.input.size_hint() - } -} - -impl<I, F> Source for PeriodicAccess<I, F> -where - I: Source, - I::Item: Sample, - F: FnMut(&mut I), -{ - #[inline] - fn current_frame_len(&self) -> Option<usize> { - self.input.current_frame_len() - } - - #[inline] - fn channels(&self) -> u16 { - self.input.channels() - } - - #[inline] - fn sample_rate(&self) -> u32 { - self.input.sample_rate() - } - - #[inline] - fn total_duration(&self) -> Option<Duration> { - self.input.total_duration() - } - #[inline] - fn elapsed(&mut self) -> Duration { - self.input.elapsed() - } - fn seek(&mut self, seek_time: Duration) -> Option<Duration> { - self.input.seek(seek_time) - } -} diff --git a/gonk-player/src/source/samples_converter.rs b/gonk-player/src/source/samples_converter.rs deleted file mode 100644 index 314e71e6..00000000 --- a/gonk-player/src/source/samples_converter.rs +++ /dev/null @@ -1,97 +0,0 @@ -use std::marker::PhantomData; -use std::time::Duration; - -use crate::{conversions::Sample, source::Source}; -use cpal::Sample as CpalSample; - -/// An iterator that reads from a `Source` and converts the samples to a specific rate and -/// channels count. -/// -/// It implements `Source` as well, but all the data is guaranteed to be in a single frame whose -/// channels and samples rate have been passed to `new`. -#[derive(Clone)] -pub struct SamplesConverter<I, D> { - inner: I, - dest: PhantomData<D>, -} - -impl<I, D> SamplesConverter<I, D> { - #[inline] - pub fn new(input: I) -> SamplesConverter<I, D> { - SamplesConverter { - inner: input, - dest: PhantomData, - } - } - - /// Returns a reference to the inner source. - #[inline] - pub fn inner(&self) -> &I { - &self.inner - } - - /// Returns a mutable reference to the inner source. - #[inline] - pub fn inner_mut(&mut self) -> &mut I { - &mut self.inner - } - - /// Returns the inner source. - #[inline] - pub fn into_inner(self) -> I { - self.inner - } -} - -impl<I, D> Iterator for SamplesConverter<I, D> -where - I: Source, - I::Item: Sample, - D: Sample, -{ - type Item = D; - - #[inline] - fn next(&mut self) -> Option<D> { - self.inner.next().map(|s| CpalSample::from(&s)) - } - - #[inline] - fn size_hint(&self) -> (usize, Option<usize>) { - self.inner.size_hint() - } -} - -impl<I, D> Source for SamplesConverter<I, D> -where - I: Source, - I::Item: Sample, - D: Sample, -{ - #[inline] - fn current_frame_len(&self) -> Option<usize> { - self.inner.current_frame_len() - } - - #[inline] - fn channels(&self) -> u16 { - self.inner.channels() - } - - #[inline] - fn sample_rate(&self) -> u32 { - self.inner.sample_rate() - } - - #[inline] - fn total_duration(&self) -> Option<Duration> { - self.inner.total_duration() - } - #[inline] - fn elapsed(&mut self) -> Duration { - Duration::from_secs(0) - } - fn seek(&mut self, time: Duration) -> Option<Duration> { - self.inner.seek(time) - } -} diff --git a/gonk-player/src/source/stoppable.rs b/gonk-player/src/source/stoppable.rs deleted file mode 100644 index 8d9f75b8..00000000 --- a/gonk-player/src/source/stoppable.rs +++ /dev/null @@ -1,100 +0,0 @@ -use crate::{conversions::Sample, source::Source}; -use std::time::Duration; - -/// Internal function that builds a `Stoppable` object. -pub fn stoppable<I>(source: I) -> Stoppable<I> { - Stoppable { - input: source, - stopped: false, - } -} - -#[derive(Clone, Debug)] -pub struct Stoppable<I> { - input: I, - stopped: bool, -} - -impl<I> Stoppable<I> { - /// Stops the sound. - #[inline] - pub fn stop(&mut self) { - self.stopped = true; - } - - /// Returns a reference to the inner source. - #[inline] - pub fn inner(&self) -> &I { - &self.input - } - - /// Returns a mutable reference to the inner source. - #[inline] - pub fn inner_mut(&mut self) -> &mut I { - &mut self.input - } - - /// Returns the inner source. - #[inline] - pub fn into_inner(self) -> I { - self.input - } -} - -impl<I> Iterator for Stoppable<I> -where - I: Source, - I::Item: Sample, -{ - type Item = I::Item; - - #[inline] - fn next(&mut self) -> Option<I::Item> { - if self.stopped { - None - } else { - self.input.next() - } - } - - #[inline] - fn size_hint(&self) -> (usize, Option<usize>) { - self.input.size_hint() - } -} - -impl<I> Source for Stoppable<I> -where - I: Source, - I::Item: Sample, -{ - #[inline] - fn current_frame_len(&self) -> Option<usize> { - self.input.current_frame_len() - } - - #[inline] - fn channels(&self) -> u16 { - self.input.channels() - } - - #[inline] - fn sample_rate(&self) -> u32 { - self.input.sample_rate() - } - - #[inline] - fn total_duration(&self) -> Option<Duration> { - self.input.total_duration() - } - - #[inline] - fn seek(&mut self, time: Duration) -> Option<Duration> { - self.input.seek(time) - } - - #[inline] - fn elapsed(&mut self) -> Duration { - self.input.elapsed() - } -} diff --git a/gonk-player/src/source/take.rs b/gonk-player/src/source/take.rs deleted file mode 100644 index 87a80b63..00000000 --- a/gonk-player/src/source/take.rs +++ /dev/null @@ -1,186 +0,0 @@ -use std::time::Duration; - -use crate::{conversions::Sample, source::Source}; - -/// Internal function that builds a `TakeDuration` object. -pub fn take_duration<I>(input: I, duration: Duration) -> TakeDuration<I> -where - I: Source, - I::Item: Sample, -{ - TakeDuration { - current_frame_len: input.current_frame_len(), - duration_per_sample: TakeDuration::get_duration_per_sample(&input), - input, - remaining_duration: duration, - requested_duration: duration, - filter: None, - } -} - -/// A filter that can be applied to a `TakeDuration`. -#[derive(Clone, Debug)] -enum DurationFilter { - FadeOut, -} -impl DurationFilter { - fn apply<I: Iterator>( - &self, - sample: <I as Iterator>::Item, - parent: &TakeDuration<I>, - ) -> <I as Iterator>::Item - where - I::Item: Sample, - { - use self::DurationFilter::*; - match self { - FadeOut => { - let remaining = parent.remaining_duration.as_millis() as f32; - let total = parent.requested_duration.as_millis() as f32; - sample.amplify(remaining / total) - } - } - } -} - -const NANOS_PER_SEC: u64 = 1_000_000_000; - -/// A source that truncates the given source to a certain duration. -#[derive(Clone, Debug)] -pub struct TakeDuration<I> { - input: I, - remaining_duration: Duration, - requested_duration: Duration, - filter: Option<DurationFilter>, - // Remaining samples in current frame. - current_frame_len: Option<usize>, - // Only updated when the current frame len is exhausted. - duration_per_sample: Duration, -} - -impl<I> TakeDuration<I> -where - I: Source, - I::Item: Sample, -{ - /// Returns the duration elapsed for each sample extracted. - #[inline] - fn get_duration_per_sample(input: &I) -> Duration { - let ns = NANOS_PER_SEC / (input.sample_rate() as u64 * input.channels() as u64); - // \|/ the maximum value of `ns` is one billion, so this can't fail - Duration::new(0, ns as u32) - } - - /// Returns a reference to the inner source. - #[inline] - pub fn inner(&self) -> &I { - &self.input - } - - /// Returns a mutable reference to the inner source. - #[inline] - pub fn inner_mut(&mut self) -> &mut I { - &mut self.input - } - - /// Returns the inner source. - #[inline] - pub fn into_inner(self) -> I { - self.input - } - - pub fn set_filter_fadeout(&mut self) { - self.filter = Some(DurationFilter::FadeOut); - } - - pub fn clear_filter(&mut self) { - self.filter = None; - } -} - -impl<I> Iterator for TakeDuration<I> -where - I: Source, - I::Item: Sample, -{ - type Item = <I as Iterator>::Item; - - fn next(&mut self) -> Option<<I as Iterator>::Item> { - if let Some(frame_len) = self.current_frame_len.take() { - if frame_len > 0 { - self.current_frame_len = Some(frame_len - 1); - } else { - self.current_frame_len = self.input.current_frame_len(); - // Sample rate might have changed - self.duration_per_sample = Self::get_duration_per_sample(&self.input); - } - } - - if self.remaining_duration <= self.duration_per_sample { - None - } else if let Some(sample) = self.input.next() { - let sample = match &self.filter { - Some(filter) => filter.apply(sample, self), - None => sample, - }; - - self.remaining_duration -= self.duration_per_sample; - - Some(sample) - } else { - None - } - } - - // TODO: size_hint -} - -impl<I> Source for TakeDuration<I> -where - I: Iterator + Source, - I::Item: Sample, -{ - #[inline] - fn current_frame_len(&self) -> Option<usize> { - let remaining_nanos = self.remaining_duration.as_secs() * NANOS_PER_SEC - + self.remaining_duration.subsec_nanos() as u64; - let nanos_per_sample = self.duration_per_sample.as_secs() * NANOS_PER_SEC - + self.duration_per_sample.subsec_nanos() as u64; - let remaining_samples = (remaining_nanos / nanos_per_sample) as usize; - - self.input - .current_frame_len() - .filter(|value| *value < remaining_samples) - .or(Some(remaining_samples)) - } - - #[inline] - fn channels(&self) -> u16 { - self.input.channels() - } - - #[inline] - fn sample_rate(&self) -> u32 { - self.input.sample_rate() - } - - #[inline] - fn total_duration(&self) -> Option<Duration> { - if let Some(duration) = self.input.total_duration() { - if duration < self.requested_duration { - Some(duration) - } else { - Some(self.requested_duration) - } - } else { - None - } - } - #[inline] - fn elapsed(&mut self) -> Duration { - self.input.elapsed() - } - fn seek(&mut self, time: Duration) -> Option<Duration> { - self.input.seek(time) - } -} diff --git a/gonk-player/src/source/uniform.rs b/gonk-player/src/source/uniform.rs deleted file mode 100644 index 0e9edcd0..00000000 --- a/gonk-player/src/source/uniform.rs +++ /dev/null @@ -1,203 +0,0 @@ -use std::cmp; -use std::time::Duration; - -use crate::conversions::{ChannelCountConverter, DataConverter, SampleRateConverter}; -use crate::{conversions::Sample, source::Source}; - -/// An iterator that reads from a `Source` and converts the samples to a specific rate and -/// channels count. -/// -/// It implements `Source` as well, but all the data is guaranteed to be in a single frame whose -/// channels and samples rate have been passed to `new`. -#[derive(Clone)] -pub struct UniformSourceIterator<I, D> -where - I: Source, - I::Item: Sample, - D: Sample, -{ - inner: Option<DataConverter<ChannelCountConverter<SampleRateConverter<Take<I>>>, D>>, - target_channels: u16, - target_sample_rate: u32, - total_duration: Option<Duration>, -} - -impl<I, D> UniformSourceIterator<I, D> -where - I: Source, - I::Item: Sample, - D: Sample, -{ - #[inline] - pub fn new( - input: I, - target_channels: u16, - target_sample_rate: u32, - ) -> UniformSourceIterator<I, D> { - let total_duration = input.total_duration(); - let input = UniformSourceIterator::bootstrap(input, target_channels, target_sample_rate); - - UniformSourceIterator { - inner: Some(input), - target_channels, - target_sample_rate, - total_duration, - } - } - - #[inline] - fn bootstrap( - input: I, - target_channels: u16, - target_sample_rate: u32, - ) -> DataConverter<ChannelCountConverter<SampleRateConverter<Take<I>>>, D> { - // Limit the frame length to something reasonable - let frame_len = input.current_frame_len().map(|x| x.min(32768)); - - let from_channels = input.channels(); - let from_sample_rate = input.sample_rate(); - - let input = Take { - iter: input, - n: frame_len, - }; - let input = SampleRateConverter::new( - input, - cpal::SampleRate(from_sample_rate), - cpal::SampleRate(target_sample_rate), - from_channels, - ); - let input = ChannelCountConverter::new(input, from_channels, target_channels); - - DataConverter::new(input) - } -} - -impl<I, D> Iterator for UniformSourceIterator<I, D> -where - I: Source, - I::Item: Sample, - D: Sample, -{ - type Item = D; - - #[inline] - fn next(&mut self) -> Option<D> { - if let Some(value) = self.inner.as_mut().unwrap().next() { - return Some(value); - } - - let input = self - .inner - .take() - .unwrap() - .into_inner() - .into_inner() - .into_inner() - .iter; - - let mut input = - UniformSourceIterator::bootstrap(input, self.target_channels, self.target_sample_rate); - - let value = input.next(); - self.inner = Some(input); - value - } - - #[inline] - fn size_hint(&self) -> (usize, Option<usize>) { - (self.inner.as_ref().unwrap().size_hint().0, None) - } -} - -impl<I, D> Source for UniformSourceIterator<I, D> -where - I: Iterator + Source, - I::Item: Sample, - D: Sample, -{ - #[inline] - fn current_frame_len(&self) -> Option<usize> { - None - } - - #[inline] - fn channels(&self) -> u16 { - self.target_channels - } - - #[inline] - fn sample_rate(&self) -> u32 { - self.target_sample_rate - } - - #[inline] - fn total_duration(&self) -> Option<Duration> { - self.total_duration - } - #[inline] - fn elapsed(&mut self) -> Duration { - Duration::from_secs(0) - } - fn seek(&mut self, time: Duration) -> Option<Duration> { - let mut input = self - .inner - .take() - .unwrap() - .into_inner() - .into_inner() - .into_inner() - .iter; - let ret = input.seek(time); - let input = - UniformSourceIterator::bootstrap(input, self.target_channels, self.target_sample_rate); - - self.inner = Some(input); - ret - } -} - -#[derive(Clone, Debug)] -struct Take<I> { - iter: I, - n: Option<usize>, -} - -impl<I> Iterator for Take<I> -where - I: Iterator, -{ - type Item = <I as Iterator>::Item; - - #[inline] - fn next(&mut self) -> Option<<I as Iterator>::Item> { - if let Some(n) = &mut self.n { - if *n != 0 { - *n -= 1; - self.iter.next() - } else { - None - } - } else { - self.iter.next() - } - } - - #[inline] - fn size_hint(&self) -> (usize, Option<usize>) { - if let Some(n) = self.n { - let (lower, upper) = self.iter.size_hint(); - - let lower = cmp::min(lower, n); - - let upper = match upper { - Some(x) if x < n => Some(x), - _ => Some(n), - }; - - (lower, upper) - } else { - self.iter.size_hint() - } - } -} diff --git a/gonk-player/src/source/zero.rs b/gonk-player/src/source/zero.rs deleted file mode 100644 index af4e61f0..00000000 --- a/gonk-player/src/source/zero.rs +++ /dev/null @@ -1,67 +0,0 @@ -use std::marker::PhantomData; -use std::time::Duration; - -use crate::{conversions::Sample, source::Source}; - -/// An infinite source that produces zero. -#[derive(Clone, Debug)] -pub struct Zero<S> { - channels: u16, - sample_rate: u32, - marker: PhantomData<S>, -} - -impl<S> Zero<S> { - #[inline] - pub fn new(channels: u16, sample_rate: u32) -> Zero<S> { - Zero { - channels, - sample_rate, - marker: PhantomData, - } - } -} - -impl<S> Iterator for Zero<S> -where - S: Sample, -{ - type Item = S; - - #[inline] - fn next(&mut self) -> Option<S> { - Some(S::zero_value()) - } -} - -impl<S> Source for Zero<S> -where - S: Sample, -{ - #[inline] - fn current_frame_len(&self) -> Option<usize> { - None - } - - #[inline] - fn channels(&self) -> u16 { - self.channels - } - - #[inline] - fn sample_rate(&self) -> u32 { - self.sample_rate - } - - #[inline] - fn total_duration(&self) -> Option<Duration> { - None - } - #[inline] - fn elapsed(&mut self) -> Duration { - Duration::from_secs(0) - } - fn seek(&mut self, time: Duration) -> Option<Duration> { - Some(time) - } -} diff --git a/gonk-player/src/stream.rs b/gonk-player/src/stream.rs deleted file mode 100644 index fc54dabc..00000000 --- a/gonk-player/src/stream.rs +++ /dev/null @@ -1,256 +0,0 @@ -use std::sync::{Arc, Weak}; -use std::{error, fmt}; - -use crate::decoder; -use crate::dynamic_mixer::{self, DynamicMixerController}; -use crate::source::Source; -use cpal::traits::{DeviceTrait, HostTrait, StreamTrait}; -use cpal::Sample; - -/// `cpal::Stream` container. Also see the more useful `OutputStreamHandle`. -/// -/// If this is dropped playback will end & attached `OutputStreamHandle`s will no longer work. -pub struct OutputStream { - mixer: Arc<DynamicMixerController<f32>>, - _stream: cpal::Stream, -} - -/// More flexible handle to a `OutputStream` that provides playback. -#[derive(Clone)] -pub struct OutputStreamHandle { - mixer: Weak<DynamicMixerController<f32>>, -} - -impl OutputStream { - /// Returns a new stream & handle using the given output device. - pub fn try_from_device( - device: &cpal::Device, - ) -> Result<(Self, OutputStreamHandle), StreamError> { - let (mixer, _stream) = device.try_new_output_stream()?; - _stream.play()?; - let out = Self { mixer, _stream }; - let handle = OutputStreamHandle { - mixer: Arc::downgrade(&out.mixer), - }; - Ok((out, handle)) - } - - /// Return a new stream & handle using the default output device. - /// - /// On failure will fallback to trying any non-default output devices. - pub fn try_default() -> Result<(Self, OutputStreamHandle), StreamError> { - let default_device = cpal::default_host() - .default_output_device() - .ok_or(StreamError::NoDevice)?; - - let default_stream = Self::try_from_device(&default_device); - - default_stream.or_else(|original_err| { - // default device didn't work, try other ones - let mut devices = match cpal::default_host().output_devices() { - Ok(d) => d, - Err(_) => return Err(original_err), - }; - - devices - .find_map(|d| Self::try_from_device(&d).ok()) - .ok_or(original_err) - }) - } -} - -impl OutputStreamHandle { - /// Plays a source with a device until it ends. - pub fn play_raw<S>(&self, source: S) -> Result<(), PlayError> - where - S: Source<Item = f32> + Send + 'static, - { - let mixer = self.mixer.upgrade().ok_or(PlayError::NoDevice)?; - mixer.add(source); - Ok(()) - } -} - -/// An error occurred while attemping to play a sound. -#[derive(Debug)] -pub enum PlayError { - /// Attempting to decode the audio failed. - DecoderError(decoder::DecoderError), - /// The output device was lost. - NoDevice, -} - -impl From<decoder::DecoderError> for PlayError { - fn from(err: decoder::DecoderError) -> Self { - Self::DecoderError(err) - } -} - -impl fmt::Display for PlayError { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - match self { - Self::DecoderError(e) => e.fmt(f), - Self::NoDevice => write!(f, "NoDevice"), - } - } -} - -impl error::Error for PlayError { - fn source(&self) -> Option<&(dyn error::Error + 'static)> { - match self { - Self::DecoderError(e) => Some(e), - Self::NoDevice => None, - } - } -} - -#[derive(Debug)] -pub enum StreamError { - PlayStreamError(cpal::PlayStreamError), - DefaultStreamConfigError(cpal::DefaultStreamConfigError), - BuildStreamError(cpal::BuildStreamError), - SupportedStreamConfigsError(cpal::SupportedStreamConfigsError), - NoDevice, -} - -impl From<cpal::DefaultStreamConfigError> for StreamError { - fn from(err: cpal::DefaultStreamConfigError) -> Self { - Self::DefaultStreamConfigError(err) - } -} - -impl From<cpal::SupportedStreamConfigsError> for StreamError { - fn from(err: cpal::SupportedStreamConfigsError) -> Self { - Self::SupportedStreamConfigsError(err) - } -} - -impl From<cpal::BuildStreamError> for StreamError { - fn from(err: cpal::BuildStreamError) -> Self { - Self::BuildStreamError(err) - } -} - -impl From<cpal::PlayStreamError> for StreamError { - fn from(err: cpal::PlayStreamError) -> Self { - Self::PlayStreamError(err) - } -} - -impl fmt::Display for StreamError { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - match self { - Self::PlayStreamError(e) => e.fmt(f), - Self::BuildStreamError(e) => e.fmt(f), - Self::DefaultStreamConfigError(e) => e.fmt(f), - Self::SupportedStreamConfigsError(e) => e.fmt(f), - Self::NoDevice => write!(f, "NoDevice"), - } - } -} - -impl error::Error for StreamError { - fn source(&self) -> Option<&(dyn error::Error + 'static)> { - match self { - Self::PlayStreamError(e) => Some(e), - Self::BuildStreamError(e) => Some(e), - Self::DefaultStreamConfigError(e) => Some(e), - Self::SupportedStreamConfigsError(e) => Some(e), - Self::NoDevice => None, - } - } -} - -/// Extensions to `cpal::Device` -pub(crate) trait CpalDeviceExt { - fn new_output_stream_with_format( - &self, - format: cpal::SupportedStreamConfig, - ) -> Result<(Arc<DynamicMixerController<f32>>, cpal::Stream), cpal::BuildStreamError>; - - fn try_new_output_stream( - &self, - ) -> Result<(Arc<DynamicMixerController<f32>>, cpal::Stream), StreamError>; -} - -impl CpalDeviceExt for cpal::Device { - fn new_output_stream_with_format( - &self, - format: cpal::SupportedStreamConfig, - ) -> Result<(Arc<DynamicMixerController<f32>>, cpal::Stream), cpal::BuildStreamError> { - let (mixer_tx, mut mixer_rx) = - dynamic_mixer::mixer::<f32>(format.channels(), format.sample_rate().0); - - let error_callback = |err| panic!("an error occurred on output stream: {}", err); - - match format.sample_format() { - cpal::SampleFormat::F32 => self.build_output_stream::<f32, _, _>( - &format.config(), - move |data, _| { - data.iter_mut() - .for_each(|d| *d = mixer_rx.next().unwrap_or(0f32)) - }, - error_callback, - ), - cpal::SampleFormat::I16 => self.build_output_stream::<i16, _, _>( - &format.config(), - move |data, _| { - data.iter_mut() - .for_each(|d| *d = mixer_rx.next().map(|s| s.to_i16()).unwrap_or(0i16)) - }, - error_callback, - ), - cpal::SampleFormat::U16 => self.build_output_stream::<u16, _, _>( - &format.config(), - move |data, _| { - data.iter_mut().for_each(|d| { - *d = mixer_rx - .next() - .map(|s| s.to_u16()) - .unwrap_or(u16::max_value() / 2) - }) - }, - error_callback, - ), - } - .map(|stream| (mixer_tx, stream)) - } - - fn try_new_output_stream( - &self, - ) -> Result<(Arc<DynamicMixerController<f32>>, cpal::Stream), StreamError> { - // Determine the format to use for the new stream. - let default_format = self.default_output_config()?; - - self.new_output_stream_with_format(default_format) - .or_else(|err| { - // look through all supported formats to see if another works - supported_output_formats(self)? - .filter_map(|format| self.new_output_stream_with_format(format).ok()) - .next() - // return original error if nothing works - .ok_or(StreamError::BuildStreamError(err)) - }) - } -} - -/// All the supported output formats with sample rates -fn supported_output_formats( - device: &cpal::Device, -) -> Result<impl Iterator<Item = cpal::SupportedStreamConfig>, StreamError> { - const HZ_44100: cpal::SampleRate = cpal::SampleRate(44_100); - - let mut supported: Vec<_> = device.supported_output_configs()?.collect(); - supported.sort_by(|a, b| b.cmp_default_heuristics(a)); - - Ok(supported.into_iter().flat_map(|sf| { - let max_rate = sf.max_sample_rate(); - let min_rate = sf.min_sample_rate(); - let mut formats = vec![sf.clone().with_max_sample_rate()]; - if HZ_44100 < max_rate && HZ_44100 > min_rate { - formats.push(sf.clone().with_sample_rate(HZ_44100)) - } - formats.push(sf.with_sample_rate(min_rate)); - formats - })) -} diff --git a/gonk/src/app.rs b/gonk/src/app.rs index 7a75a783..c9005bd2 100644 --- a/gonk/src/app.rs +++ b/gonk/src/app.rs @@ -40,7 +40,7 @@ pub enum Mode { const TICK_RATE: Duration = Duration::from_millis(200); const POLL_RATE: Duration = Duration::from_millis(4); -const SEEK_TIME: f64 = 10.0; +const SEEK_TIME: f32 = 10.0; pub struct App { terminal: Terminal<CrosstermBackend<Stdout>>, @@ -230,7 +230,7 @@ impl App { self.mode = Mode::Playlist; } Mode::Queue => { - if let Some(song) = self.queue.selected() { + if let Some(song) = self.queue.player.selected_song() { self.playlist.add_to_playlist(&[song.clone()]); self.mode = Mode::Playlist; } @@ -244,7 +244,7 @@ impl App { } Mode::Queue => { if let Some(i) = self.queue.ui.index() { - self.queue.player.play_song(i); + self.queue.player.play_index(i); } } Mode::Search => self.search.on_enter(&mut self.queue.player), @@ -294,18 +294,18 @@ impl App { self.queue.player.seek_by(SEEK_TIME) } _ if hotkey.previous == bind && self.mode != Mode::Search => { - self.queue.player.prev_song() + self.queue.player.previous() } _ if hotkey.next == bind && self.mode != Mode::Search => { - self.queue.player.next_song() + self.queue.player.next() } _ if hotkey.volume_up == bind => { self.queue.player.volume_up(); - self.toml.set_volume(self.queue.player.volume); + self.toml.set_volume(self.queue.player.get_volume()); } _ if hotkey.volume_down == bind => { self.queue.player.volume_down(); - self.toml.set_volume(self.queue.player.volume); + self.toml.set_volume(self.queue.player.get_volume()); } _ if hotkey.delete == bind => match self.mode { Mode::Queue => self.queue.delete(), diff --git a/gonk/src/app/queue.rs b/gonk/src/app/queue.rs index 3f18aba7..8e128c13 100644 --- a/gonk/src/app/queue.rs +++ b/gonk/src/app/queue.rs @@ -2,7 +2,7 @@ use crate::toml::Colors; use crate::widgets::{Cell, Gauge, Row, Table, TableState}; use crate::Frame; use crossterm::event::KeyModifiers; -use gonk_player::{Index, Player, Song}; +use gonk_player::{Index, Player}; use tui::layout::{Alignment, Constraint, Direction, Layout, Rect}; use tui::style::{Color, Modifier, Style}; use tui::text::{Span, Spans}; @@ -28,7 +28,7 @@ impl Queue { } } pub fn update(&mut self) { - if self.ui.is_none() && !self.player.songs.is_empty() { + if self.ui.selected().is_none() && !self.player.is_empty() { self.ui.select(Some(0)); } self.player.update(); @@ -51,10 +51,10 @@ impl Queue { ); } pub fn up(&mut self) { - self.ui.up_with_len(self.player.songs.len()); + self.ui.up_with_len(self.player.total_songs()); } pub fn down(&mut self) { - self.ui.down_with_len(self.player.songs.len()); + self.ui.down_with_len(self.player.total_songs()); } pub fn clear(&mut self) { self.player.clear(); @@ -66,17 +66,14 @@ impl Queue { } pub fn delete(&mut self) { if let Some(i) = self.ui.index() { - self.player.delete_song(i); + self.player.delete_index(i); //make sure the ui index is in sync - let len = self.player.songs.len().saturating_sub(1); + let len = self.player.total_songs().saturating_sub(1); if i > len { self.ui.select(Some(len)); } } } - pub fn selected(&self) -> Option<&Song> { - self.player.songs.selected() - } } impl Queue { @@ -102,8 +99,8 @@ impl Queue { //Mouse support for the seek bar. if (size.height - 2 == y || size.height - 1 == y) && size.height > 15 { - let ratio = f64::from(x) / f64::from(size.width); - let duration = self.player.duration; + let ratio = x as f32 / size.width as f32; + let duration = self.player.duration(); let new_time = duration * ratio; self.player.seek_to(new_time); self.clicked_pos = None; @@ -117,7 +114,7 @@ impl Queue { //Make sure you didn't click on the seek bar //and that the song index exists. - if index < self.player.songs.len() + if index < self.player.total_songs() && ((size.height < 15 && y < size.height.saturating_sub(1)) || y < size.height.saturating_sub(3)) { @@ -138,9 +135,9 @@ impl Queue { area, ); - let state = if self.player.songs.is_empty() { + let state = if self.player.is_empty() { String::from("╭─Stopped") - } else if !self.player.is_paused() { + } else if self.player.is_playing() { String::from("╭─Playing") } else { String::from("╭─Paused") @@ -148,15 +145,15 @@ impl Queue { f.render_widget(Paragraph::new(state).alignment(Alignment::Left), area); - if !self.player.songs.is_empty() { + if !self.player.is_empty() { self.draw_title(f, area); } - let volume = Spans::from(format!("Vol: {}%─╮", self.player.volume)); + let volume = Spans::from(format!("Vol: {}%─╮", self.player.get_volume())); f.render_widget(Paragraph::new(volume).alignment(Alignment::Right), area); } fn draw_title(&mut self, f: &mut Frame, area: Rect) { - let title = if let Some(song) = self.player.songs.selected() { + let title = if let Some(song) = self.player.selected_song() { let mut name = song.name.trim_end().to_string(); let mut album = song.album.trim_end().to_string(); let mut artist = song.artist.trim_end().to_string(); @@ -198,7 +195,7 @@ impl Queue { f.render_widget(Paragraph::new(title).alignment(Alignment::Center), area); } fn draw_body(&mut self, f: &mut Frame, area: Rect) -> Option<(usize, usize)> { - if self.player.songs.is_empty() { + if self.player.is_empty() { if self.clicked_pos.is_some() { self.clicked_pos = None; } @@ -212,11 +209,8 @@ impl Queue { return None; } - let (songs, player_index, ui_index) = ( - &self.player.songs.data, - self.player.songs.index(), - self.ui.index(), - ); + let songs = self.player.get_index(); + let (songs, player_index, ui_index) = (&songs.data, songs.index(), self.ui.index()); let mut items: Vec<Row> = songs .iter() @@ -329,7 +323,7 @@ impl Queue { Some(row_bounds) } fn draw_seeker(&mut self, f: &mut Frame, area: Rect) { - if self.player.songs.is_empty() { + if self.player.is_empty() { return f.render_widget( Block::default() .border_type(BorderType::Rounded) @@ -339,7 +333,7 @@ impl Queue { } let elapsed = self.player.elapsed(); - let duration = self.player.duration; + let duration = self.player.duration(); let seeker = format!( "{:02}:{:02}/{:02}:{:02}", @@ -349,7 +343,7 @@ impl Queue { duration.trunc() as u32 % 60, ); - let ratio = self.player.elapsed() / self.player.duration; + let ratio = self.player.elapsed() / self.player.duration(); let ratio = if ratio.is_nan() { 0.0 } else { @@ -364,7 +358,7 @@ impl Queue { .border_type(BorderType::Rounded), ) .gauge_style(Style::default().fg(self.colors.seeker)) - .ratio(ratio) + .ratio(ratio as f64) .label(seeker), area, ); diff --git a/gonk/src/app/search.rs b/gonk/src/app/search.rs index e2c573b1..3003a743 100644 --- a/gonk/src/app/search.rs +++ b/gonk/src/app/search.rs @@ -465,7 +465,7 @@ impl Search { let area = f.size(); //Move the cursor position when typing if let Mode::Search = self.mode { - if self.results.is_none() && self.query.is_empty() { + if self.results.selected().is_none() && self.query.is_empty() { f.set_cursor(1, 1); } else { let len = self.query.len() as u16; diff --git a/gonk/src/app/status_bar.rs b/gonk/src/app/status_bar.rs index 6cdc5cc5..00f94cd1 100644 --- a/gonk/src/app/status_bar.rs +++ b/gonk/src/app/status_bar.rs @@ -1,6 +1,6 @@ use super::queue::Queue; use crate::Frame; -use crate::{toml::Colors, sqlite}; +use crate::{sqlite, toml::Colors}; use std::time::{Duration, Instant}; use tui::{ layout::{Alignment, Constraint, Direction, Layout, Rect}, @@ -106,7 +106,7 @@ impl StatusBar { } else if self.wait_timer.is_some() { Spans::from(self.scan_message.as_str()) } else { - if let Some(song) = queue.selected() { + if let Some(song) = queue.player.selected_song() { Spans::from(vec![ Span::raw(" "), Span::styled( @@ -144,10 +144,10 @@ impl StatusBar { //TODO: Draw mini progress bar here. - let text = if queue.player.is_paused() { - String::from("Paused ") + let text = if queue.player.is_playing() { + format!("Vol: {}% ", queue.player.get_volume()) } else { - format!("Vol: {}% ", queue.player.volume) + String::from("Paused ") }; f.render_widget( From eaf3e43aee10534f89531c2dd5b2346e8165f7f5 Mon Sep 17 00:00:00 2001 From: zX3no <dr_xeno@hotmail.com.au> Date: Tue, 14 Jun 2022 14:47:47 +0930 Subject: [PATCH 02/40] duration --- gonk-player/src/lib.rs | 17 ++++-- gonk-player/src/sample_processor.rs | 88 ++++++++++++++++------------- gonk/src/app/queue.rs | 6 +- 3 files changed, 64 insertions(+), 47 deletions(-) diff --git a/gonk-player/src/lib.rs b/gonk-player/src/lib.rs index a24cb50a..5539149a 100644 --- a/gonk-player/src/lib.rs +++ b/gonk-player/src/lib.rs @@ -8,6 +8,7 @@ use std::{ path::PathBuf, sync::{Arc, RwLock}, thread, + time::Duration, }; mod index; @@ -35,6 +36,7 @@ pub struct Player { playing: bool, volume: u16, songs: Index<Song>, + duration: Arc<RwLock<Duration>>, } impl Player { @@ -45,6 +47,7 @@ impl Player { r, playing: true, volume, + duration: Arc::new(RwLock::new(Duration::default())), songs: Index::default(), } } @@ -53,8 +56,8 @@ impl Player { // self.next_song(); // } } - pub fn duration(&self) -> f32 { - 0.0 + pub fn duration(&self) -> Duration { + *self.duration.read().unwrap() } pub fn is_empty(&self) -> bool { self.songs.is_empty() @@ -75,8 +78,7 @@ impl Player { self.stop(); } self.playing = true; - let r2 = self.r.clone(); - Player::run(r2, song.path.clone()); + self.run(song.path.clone()); } } pub fn play_index(&mut self, i: usize) { @@ -165,7 +167,9 @@ impl Player { pub fn stop(&self) { self.s.send(Event::Stop).unwrap(); } - fn run(r: Receiver<Event>, path: PathBuf) { + fn run(&self, path: PathBuf) { + let r = self.r.clone(); + let duration = self.duration.clone(); thread::spawn(move || { let device = cpal::default_host().default_output_device().unwrap(); let config = device.default_output_config().unwrap(); @@ -175,6 +179,9 @@ impl Player { path, ))); + //Update the duration; + *duration.write().unwrap() = processor.read().unwrap().duration; + let p = processor.clone(); let stream = device diff --git a/gonk-player/src/sample_processor.rs b/gonk-player/src/sample_processor.rs index 10049508..34feb508 100644 --- a/gonk-player/src/sample_processor.rs +++ b/gonk-player/src/sample_processor.rs @@ -17,49 +17,14 @@ pub struct SampleProcessor { pub decoder: Box<dyn Decoder>, pub format: Box<dyn FormatReader>, pub spec: SignalSpec, - pub duration: u64, + pub capacity: u64, + pub duration: Duration, pub converter: SampleRateConverter, pub finished: bool, pub left: bool, } impl SampleProcessor { - pub fn next_sample(&mut self) -> f32 { - loop { - if let Some(sample) = self.converter.next() { - return sample * 0.1; - } else { - self.update(); - } - } - } - pub fn update(&mut self) { - match self.format.next_packet() { - Ok(packet) => { - let decoded = self.decoder.decode(&packet).unwrap(); - let mut buffer = SampleBuffer::<f32>::new(self.duration, self.spec); - buffer.copy_interleaved_ref(decoded); - - self.converter.update(buffer.samples().to_vec().into_iter()); - } - Err(e) => match e { - symphonia::core::errors::Error::IoError(_) => self.finished = true, - _ => panic!("{:?}", e), - }, - } - } - pub fn seek_to(&mut self, time: Duration) { - let nanos_per_sec = 1_000_000_000.0; - self.format - .seek( - SeekMode::Coarse, - SeekTo::Time { - time: Time::new(time.as_secs(), time.subsec_nanos() as f64 / nanos_per_sec), - track_id: None, - }, - ) - .unwrap(); - } pub fn new(sample_rate: Option<u32>, path: impl AsRef<Path>) -> Self { let source = Box::new(File::open(path).unwrap()); @@ -78,6 +43,14 @@ impl SampleProcessor { .unwrap(); let track = probed.format.default_track().unwrap(); + + let duration = if let Some(tb) = track.codec_params.time_base { + let n_frames = track.codec_params.n_frames.unwrap(); + let time = tb.calc_time(n_frames); + Duration::from_secs(time.seconds) + Duration::from_secs_f64(time.frac) + } else { + panic!("Could not decode track duration."); + }; let mut decoder = symphonia::default::get_codecs() .make(&track.codec_params, &DecoderOptions::default()) .unwrap(); @@ -86,15 +59,16 @@ impl SampleProcessor { let decoded = decoder.decode(¤t_frame).unwrap(); let spec = decoded.spec().to_owned(); - let duration = decoded.capacity() as u64; + let capacity = decoded.capacity() as u64; - let mut sample_buffer = SampleBuffer::<f32>::new(duration, spec); + let mut sample_buffer = SampleBuffer::<f32>::new(capacity, spec); sample_buffer.copy_interleaved_ref(decoded); Self { format: probed.format, decoder, spec, + capacity, duration, converter: SampleRateConverter::new( sample_buffer.samples().to_vec().into_iter(), @@ -105,4 +79,40 @@ impl SampleProcessor { left: true, } } + pub fn next_sample(&mut self) -> f32 { + loop { + if let Some(sample) = self.converter.next() { + return sample * 0.1; + } else { + self.update(); + } + } + } + pub fn update(&mut self) { + match self.format.next_packet() { + Ok(packet) => { + let decoded = self.decoder.decode(&packet).unwrap(); + let mut buffer = SampleBuffer::<f32>::new(self.capacity, self.spec); + buffer.copy_interleaved_ref(decoded); + + self.converter.update(buffer.samples().to_vec().into_iter()); + } + Err(e) => match e { + symphonia::core::errors::Error::IoError(_) => self.finished = true, + _ => panic!("{:?}", e), + }, + } + } + pub fn seek_to(&mut self, time: Duration) { + let nanos_per_sec = 1_000_000_000.0; + self.format + .seek( + SeekMode::Coarse, + SeekTo::Time { + time: Time::new(time.as_secs(), time.subsec_nanos() as f64 / nanos_per_sec), + track_id: None, + }, + ) + .unwrap(); + } } diff --git a/gonk/src/app/queue.rs b/gonk/src/app/queue.rs index 8e128c13..4dde6bad 100644 --- a/gonk/src/app/queue.rs +++ b/gonk/src/app/queue.rs @@ -100,7 +100,7 @@ impl Queue { //Mouse support for the seek bar. if (size.height - 2 == y || size.height - 1 == y) && size.height > 15 { let ratio = x as f32 / size.width as f32; - let duration = self.player.duration(); + let duration = self.player.duration().as_secs_f32(); let new_time = duration * ratio; self.player.seek_to(new_time); self.clicked_pos = None; @@ -333,7 +333,7 @@ impl Queue { } let elapsed = self.player.elapsed(); - let duration = self.player.duration(); + let duration = self.player.duration().as_secs_f32(); let seeker = format!( "{:02}:{:02}/{:02}:{:02}", @@ -343,7 +343,7 @@ impl Queue { duration.trunc() as u32 % 60, ); - let ratio = self.player.elapsed() / self.player.duration(); + let ratio = elapsed / duration; let ratio = if ratio.is_nan() { 0.0 } else { From 5da02abce21f75e1d8440f1230eea6063908b7ea Mon Sep 17 00:00:00 2001 From: zX3no <dr_xeno@hotmail.com.au> Date: Tue, 14 Jun 2022 14:58:17 +0930 Subject: [PATCH 03/40] elapsed --- gonk-player/src/lib.rs | 17 ++++++++++++----- gonk-player/src/sample_processor.rs | 9 +++++++++ gonk/src/app/queue.rs | 2 +- 3 files changed, 22 insertions(+), 6 deletions(-) diff --git a/gonk-player/src/lib.rs b/gonk-player/src/lib.rs index 5539149a..c3035e8f 100644 --- a/gonk-player/src/lib.rs +++ b/gonk-player/src/lib.rs @@ -37,6 +37,7 @@ pub struct Player { volume: u16, songs: Index<Song>, duration: Arc<RwLock<Duration>>, + elapsed: Arc<RwLock<Duration>>, } impl Player { @@ -45,9 +46,10 @@ impl Player { Self { s, r, - playing: true, + playing: false, volume, duration: Arc::new(RwLock::new(Duration::default())), + elapsed: Arc::new(RwLock::new(Duration::default())), songs: Index::default(), } } @@ -59,6 +61,9 @@ impl Player { pub fn duration(&self) -> Duration { *self.duration.read().unwrap() } + pub fn elapsed(&self) -> Duration { + *self.elapsed.read().unwrap() + } pub fn is_empty(&self) -> bool { self.songs.is_empty() } @@ -145,9 +150,7 @@ impl Player { self.s.send(Event::Play).unwrap(); self.playing = true; } - pub fn elapsed(&self) -> f32 { - 0.0 - } + pub fn get_index(&self) -> &Index<Song> { &self.songs } @@ -170,6 +173,8 @@ impl Player { fn run(&self, path: PathBuf) { let r = self.r.clone(); let duration = self.duration.clone(); + let elapsed = self.elapsed.clone(); + thread::spawn(move || { let device = cpal::default_host().default_output_device().unwrap(); let config = device.default_output_config().unwrap(); @@ -201,7 +206,9 @@ impl Player { stream.play().unwrap(); loop { - if let Ok(event) = r.recv() { + *elapsed.write().unwrap() = processor.read().unwrap().elapsed; + + if let Ok(event) = r.recv_timeout(Duration::from_millis(16)) { dbg!(&event); match event { Event::Play => stream.play().unwrap(), diff --git a/gonk-player/src/sample_processor.rs b/gonk-player/src/sample_processor.rs index 34feb508..499a41aa 100644 --- a/gonk-player/src/sample_processor.rs +++ b/gonk-player/src/sample_processor.rs @@ -19,6 +19,7 @@ pub struct SampleProcessor { pub spec: SignalSpec, pub capacity: u64, pub duration: Duration, + pub elapsed: Duration, pub converter: SampleRateConverter, pub finished: bool, pub left: bool, @@ -70,6 +71,7 @@ impl SampleProcessor { spec, capacity, duration, + elapsed: Duration::default(), converter: SampleRateConverter::new( sample_buffer.samples().to_vec().into_iter(), spec.rate, @@ -96,6 +98,13 @@ impl SampleProcessor { buffer.copy_interleaved_ref(decoded); self.converter.update(buffer.samples().to_vec().into_iter()); + + //Update elapsed + let ts = packet.ts(); + let track = self.format.default_track().unwrap(); + let tb = track.codec_params.time_base.unwrap(); + let t = tb.calc_time(ts); + self.elapsed = Duration::from_secs(t.seconds) + Duration::from_secs_f64(t.frac); } Err(e) => match e { symphonia::core::errors::Error::IoError(_) => self.finished = true, diff --git a/gonk/src/app/queue.rs b/gonk/src/app/queue.rs index 4dde6bad..dbe4312f 100644 --- a/gonk/src/app/queue.rs +++ b/gonk/src/app/queue.rs @@ -332,7 +332,7 @@ impl Queue { ); } - let elapsed = self.player.elapsed(); + let elapsed = self.player.elapsed().as_secs_f32(); let duration = self.player.duration().as_secs_f32(); let seeker = format!( From cd05abbeb059cfd172645a89a27892fdda9c46ab Mon Sep 17 00:00:00 2001 From: zX3no <dr_xeno@hotmail.com.au> Date: Tue, 14 Jun 2022 15:08:27 +0930 Subject: [PATCH 04/40] prev + next --- gonk-player/src/lib.rs | 16 ++++++++++------ gonk-player/src/sample_processor.rs | 10 +++++++--- gonk/src/app/queue.rs | 2 +- gonk/src/app/search.rs | 2 +- 4 files changed, 19 insertions(+), 11 deletions(-) diff --git a/gonk-player/src/lib.rs b/gonk-player/src/lib.rs index c3035e8f..ecd752a5 100644 --- a/gonk-player/src/lib.rs +++ b/gonk-player/src/lib.rs @@ -136,8 +136,14 @@ impl Player { self.play(); } } - pub fn previous(&self) {} - pub fn next(&self) {} + pub fn previous(&mut self) { + self.songs.up(); + self.play_selected(); + } + pub fn next(&mut self) { + self.songs.down(); + self.play_selected(); + } pub fn volume_up(&self) {} pub fn volume_down(&self) {} pub fn is_playing(&self) -> bool { @@ -209,13 +215,11 @@ impl Player { *elapsed.write().unwrap() = processor.read().unwrap().elapsed; if let Ok(event) = r.recv_timeout(Duration::from_millis(16)) { - dbg!(&event); match event { Event::Play => stream.play().unwrap(), Event::Pause => stream.pause().unwrap(), - Event::SeekBy(_) => (), - Event::SeekTo(_) => (), - // Event::Seek(duration) => processor.write().unwrap().seek_to(duration), + Event::SeekBy(duration) => processor.write().unwrap().seek_by(duration), + Event::SeekTo(duration) => processor.write().unwrap().seek_to(duration), Event::Stop => break, } } diff --git a/gonk-player/src/sample_processor.rs b/gonk-player/src/sample_processor.rs index 499a41aa..0972afcf 100644 --- a/gonk-player/src/sample_processor.rs +++ b/gonk-player/src/sample_processor.rs @@ -112,13 +112,17 @@ impl SampleProcessor { }, } } - pub fn seek_to(&mut self, time: Duration) { - let nanos_per_sec = 1_000_000_000.0; + pub fn seek_by(&mut self, time: f32) { + let time = self.elapsed.as_secs_f32() + time; + self.seek_to(time); + } + pub fn seek_to(&mut self, time: f32) { + let time = Duration::from_secs_f32(time); self.format .seek( SeekMode::Coarse, SeekTo::Time { - time: Time::new(time.as_secs(), time.subsec_nanos() as f64 / nanos_per_sec), + time: Time::new(time.as_secs(), time.subsec_nanos() as f64 / 1_000_000_000.0), track_id: None, }, ) diff --git a/gonk/src/app/queue.rs b/gonk/src/app/queue.rs index dbe4312f..aa29cab4 100644 --- a/gonk/src/app/queue.rs +++ b/gonk/src/app/queue.rs @@ -28,7 +28,7 @@ impl Queue { } } pub fn update(&mut self) { - if self.ui.selected().is_none() && !self.player.is_empty() { + if self.ui.index().is_none() && !self.player.is_empty() { self.ui.select(Some(0)); } self.player.update(); diff --git a/gonk/src/app/search.rs b/gonk/src/app/search.rs index 3003a743..5299e846 100644 --- a/gonk/src/app/search.rs +++ b/gonk/src/app/search.rs @@ -465,7 +465,7 @@ impl Search { let area = f.size(); //Move the cursor position when typing if let Mode::Search = self.mode { - if self.results.selected().is_none() && self.query.is_empty() { + if self.query.is_empty() { f.set_cursor(1, 1); } else { let len = self.query.len() as u16; From cd02fefd4c47a944119324449a7eb882b9c84228 Mon Sep 17 00:00:00 2001 From: zX3no <dr_xeno@hotmail.com.au> Date: Tue, 14 Jun 2022 15:16:08 +0930 Subject: [PATCH 05/40] volume --- gonk-player/src/lib.rs | 43 +++++++++++++++++++++++++++-- gonk-player/src/sample_processor.rs | 6 ++-- 2 files changed, 45 insertions(+), 4 deletions(-) diff --git a/gonk-player/src/lib.rs b/gonk-player/src/lib.rs index ecd752a5..3df0f51b 100644 --- a/gonk-player/src/lib.rs +++ b/gonk-player/src/lib.rs @@ -21,6 +21,9 @@ pub use cpal::Device; pub use index::Index; pub use song::Song; +const VOLUME_STEP: u16 = 5; +const VOLUME_REDUCTION: f32 = 600.0; + #[derive(Debug)] pub enum Event { Play, @@ -28,6 +31,7 @@ pub enum Event { Stop, SeekBy(f32), SeekTo(f32), + Volume(f32), } pub struct Player { @@ -144,8 +148,40 @@ impl Player { self.songs.down(); self.play_selected(); } - pub fn volume_up(&self) {} - pub fn volume_down(&self) {} + pub fn volume_up(&mut self) { + self.volume += VOLUME_STEP; + + if self.volume > 100 { + self.volume = 100; + } + + self.update_volume(); + } + pub fn volume_down(&mut self) { + if self.volume != 0 { + self.volume -= VOLUME_STEP; + } + + self.update_volume(); + } + fn update_volume(&self) { + self.s.send(Event::Volume(self.real_volume())).unwrap(); + } + fn real_volume(&self) -> f32 { + if let Some(song) = self.songs.selected() { + let volume = self.volume as f32 / VOLUME_REDUCTION; + //Calculate the volume with gain + if song.track_gain == 0.0 { + //Reduce the volume a little to match + //songs with replay gain information. + volume * 0.75 + } else { + volume * song.track_gain as f32 + } + } else { + self.volume as f32 / VOLUME_REDUCTION + } + } pub fn is_playing(&self) -> bool { self.playing } @@ -180,6 +216,7 @@ impl Player { let r = self.r.clone(); let duration = self.duration.clone(); let elapsed = self.elapsed.clone(); + let volume = self.real_volume(); thread::spawn(move || { let device = cpal::default_host().default_output_device().unwrap(); @@ -188,6 +225,7 @@ impl Player { let processor = Arc::new(RwLock::new(SampleProcessor::new( Some(config.sample_rate().0), path, + volume, ))); //Update the duration; @@ -220,6 +258,7 @@ impl Player { Event::Pause => stream.pause().unwrap(), Event::SeekBy(duration) => processor.write().unwrap().seek_by(duration), Event::SeekTo(duration) => processor.write().unwrap().seek_to(duration), + Event::Volume(volume) => processor.write().unwrap().volume = volume, Event::Stop => break, } } diff --git a/gonk-player/src/sample_processor.rs b/gonk-player/src/sample_processor.rs index 0972afcf..8b85a92c 100644 --- a/gonk-player/src/sample_processor.rs +++ b/gonk-player/src/sample_processor.rs @@ -23,10 +23,11 @@ pub struct SampleProcessor { pub converter: SampleRateConverter, pub finished: bool, pub left: bool, + pub volume: f32, } impl SampleProcessor { - pub fn new(sample_rate: Option<u32>, path: impl AsRef<Path>) -> Self { + pub fn new(sample_rate: Option<u32>, path: impl AsRef<Path>, volume: f32) -> Self { let source = Box::new(File::open(path).unwrap()); let mss = MediaSourceStream::new(source, Default::default()); @@ -79,12 +80,13 @@ impl SampleProcessor { ), finished: false, left: true, + volume, } } pub fn next_sample(&mut self) -> f32 { loop { if let Some(sample) = self.converter.next() { - return sample * 0.1; + return sample * self.volume; } else { self.update(); } From 990d7b5d94fabb4ae186d843643f1401ff77e919 Mon Sep 17 00:00:00 2001 From: zX3no <dr_xeno@hotmail.com.au> Date: Tue, 14 Jun 2022 15:21:23 +0930 Subject: [PATCH 06/40] handle decode errors --- gonk-player/src/sample_processor.rs | 44 ++++++++++++++++++----------- 1 file changed, 27 insertions(+), 17 deletions(-) diff --git a/gonk-player/src/sample_processor.rs b/gonk-player/src/sample_processor.rs index 8b85a92c..cf43ef49 100644 --- a/gonk-player/src/sample_processor.rs +++ b/gonk-player/src/sample_processor.rs @@ -93,25 +93,35 @@ impl SampleProcessor { } } pub fn update(&mut self) { - match self.format.next_packet() { - Ok(packet) => { - let decoded = self.decoder.decode(&packet).unwrap(); - let mut buffer = SampleBuffer::<f32>::new(self.capacity, self.spec); - buffer.copy_interleaved_ref(decoded); + let mut decode_errors: usize = 0; + const MAX_DECODE_ERRORS: usize = 3; + loop { + match self.format.next_packet() { + Ok(packet) => { + let decoded = self.decoder.decode(&packet).unwrap(); + let mut buffer = SampleBuffer::<f32>::new(self.capacity, self.spec); + buffer.copy_interleaved_ref(decoded); - self.converter.update(buffer.samples().to_vec().into_iter()); + self.converter.update(buffer.samples().to_vec().into_iter()); - //Update elapsed - let ts = packet.ts(); - let track = self.format.default_track().unwrap(); - let tb = track.codec_params.time_base.unwrap(); - let t = tb.calc_time(ts); - self.elapsed = Duration::from_secs(t.seconds) + Duration::from_secs_f64(t.frac); - } - Err(e) => match e { - symphonia::core::errors::Error::IoError(_) => self.finished = true, - _ => panic!("{:?}", e), - }, + //Update elapsed + let ts = packet.ts(); + let track = self.format.default_track().unwrap(); + let tb = track.codec_params.time_base.unwrap(); + let t = tb.calc_time(ts); + self.elapsed = Duration::from_secs(t.seconds) + Duration::from_secs_f64(t.frac); + break; + } + Err(e) => match e { + symphonia::core::errors::Error::DecodeError(_) => { + decode_errors += 1; + if decode_errors > MAX_DECODE_ERRORS { + panic!("{:?}", e); + } + } + _ => (), + }, + }; } } pub fn seek_by(&mut self, time: f32) { From 2d23d70517d1a4dbc80dd7d124ec70603cf29104 Mon Sep 17 00:00:00 2001 From: zX3no <dr_xeno@hotmail.com.au> Date: Tue, 14 Jun 2022 16:38:47 +0930 Subject: [PATCH 07/40] error cleanup --- gonk-player/src/lib.rs | 5 ++--- gonk-player/src/sample_processor.rs | 10 +++++++--- 2 files changed, 9 insertions(+), 6 deletions(-) diff --git a/gonk-player/src/lib.rs b/gonk-player/src/lib.rs index 3df0f51b..6c534a59 100644 --- a/gonk-player/src/lib.rs +++ b/gonk-player/src/lib.rs @@ -58,10 +58,9 @@ impl Player { } } pub fn update(&mut self) { - // if self.elapsed() > self.duration { - // self.next_song(); - // } + //TODO: Check if the song is finished playing and skip to next one. } + pub fn duration(&self) -> Duration { *self.duration.read().unwrap() } diff --git a/gonk-player/src/sample_processor.rs b/gonk-player/src/sample_processor.rs index cf43ef49..137a1494 100644 --- a/gonk-player/src/sample_processor.rs +++ b/gonk-player/src/sample_processor.rs @@ -1,9 +1,10 @@ use crate::sample_rate::SampleRateConverter; -use std::{fs::File, path::Path, time::Duration}; +use std::{fs::File, io::ErrorKind, path::Path, time::Duration}; use symphonia::{ core::{ audio::{SampleBuffer, SignalSpec}, codecs::{Decoder, DecoderOptions}, + errors::Error, formats::{FormatOptions, FormatReader, SeekMode, SeekTo}, io::MediaSourceStream, meta::MetadataOptions, @@ -113,13 +114,16 @@ impl SampleProcessor { break; } Err(e) => match e { - symphonia::core::errors::Error::DecodeError(_) => { + Error::DecodeError(e) => { decode_errors += 1; if decode_errors > MAX_DECODE_ERRORS { panic!("{:?}", e); } } - _ => (), + Error::IoError(e) if e.kind() == ErrorKind::UnexpectedEof => { + self.finished = true; + } + _ => panic!("{:?}", e), }, }; } From aeafa5ce89e340359dfc23f2cdee9d86c9186bf1 Mon Sep 17 00:00:00 2001 From: zX3no <dr_xeno@hotmail.com.au> Date: Tue, 14 Jun 2022 21:53:35 +0930 Subject: [PATCH 08/40] fixed mp3 and alac not playing --- gonk-player/Cargo.toml | 2 +- gonk-player/src/sample_processor.rs | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/gonk-player/Cargo.toml b/gonk-player/Cargo.toml index 6d494c7f..82536013 100644 --- a/gonk-player/Cargo.toml +++ b/gonk-player/Cargo.toml @@ -12,4 +12,4 @@ license = "MIT" [dependencies] cpal = "0.13.5" crossbeam-channel = "0.5.4" -symphonia = "0.5.0" +symphonia = { version = "0.5.0", features = ["mp3", "isomp4", "alac", "aac"] } diff --git a/gonk-player/src/sample_processor.rs b/gonk-player/src/sample_processor.rs index 137a1494..75ec1f0a 100644 --- a/gonk-player/src/sample_processor.rs +++ b/gonk-player/src/sample_processor.rs @@ -39,6 +39,7 @@ impl SampleProcessor { mss, &FormatOptions { prebuild_seek_index: true, + seek_index_fill_rate: 1, ..Default::default() }, &MetadataOptions::default(), From 6aa3f258b410e022775ceb3f59f78c69d51a41ca Mon Sep 17 00:00:00 2001 From: zX3no <dr_xeno@hotmail.com.au> Date: Wed, 15 Jun 2022 14:21:44 +0930 Subject: [PATCH 09/40] various fixes --- gonk-player/src/lib.rs | 155 +++++++++++++--------------- gonk-player/src/sample_processor.rs | 123 +++++++++++++++++++--- 2 files changed, 179 insertions(+), 99 deletions(-) diff --git a/gonk-player/src/lib.rs b/gonk-player/src/lib.rs index 6c534a59..937d6eb2 100644 --- a/gonk-player/src/lib.rs +++ b/gonk-player/src/lib.rs @@ -2,10 +2,9 @@ use cpal::{ traits::{HostTrait, StreamTrait}, StreamError, }; -use crossbeam_channel::{unbounded, Receiver, Sender}; -use sample_processor::SampleProcessor; +use crossbeam_channel::{unbounded, Sender}; +use sample_processor::Generator; use std::{ - path::PathBuf, sync::{Arc, RwLock}, thread, time::Duration, @@ -28,7 +27,6 @@ const VOLUME_REDUCTION: f32 = 600.0; pub enum Event { Play, Pause, - Stop, SeekBy(f32), SeekTo(f32), Volume(f32), @@ -36,33 +34,81 @@ pub enum Event { pub struct Player { s: Sender<Event>, - r: Receiver<Event>, playing: bool, volume: u16, songs: Index<Song>, - duration: Arc<RwLock<Duration>>, elapsed: Arc<RwLock<Duration>>, + generator: Arc<RwLock<Generator>>, + duration: Duration, } impl Player { pub fn new(volume: u16) -> Self { let (s, r) = unbounded(); + + let device = cpal::default_host().default_output_device().unwrap(); + let config = device.default_output_config().unwrap(); + let rate = config.sample_rate().0; + + let generator = Arc::new(RwLock::new(Generator::new( + rate, + volume as f32 / VOLUME_REDUCTION, + ))); + let gen = generator.clone(); + let g = generator.clone(); + + let elapsed = Arc::new(RwLock::new(Duration::default())); + let e = elapsed.clone(); + + thread::spawn(move || { + let stream = device + .build_output_stream( + &config.config(), + move |data: &mut [f32], _: &cpal::OutputCallbackInfo| { + for frame in data.chunks_mut(2) { + for sample in frame.iter_mut() { + *sample = g.write().unwrap().next(); + } + } + }, + |err| panic!("{}", err), + ) + .unwrap(); + + stream.play().unwrap(); + + loop { + *e.write().unwrap() = gen.read().unwrap().elapsed(); + + if let Ok(event) = r.recv_timeout(Duration::from_millis(8)) { + match event { + Event::Play => stream.play().unwrap(), + Event::Pause => stream.pause().unwrap(), + Event::SeekBy(duration) => gen.write().unwrap().seek_by(duration).unwrap(), + Event::SeekTo(duration) => gen.write().unwrap().seek_to(duration).unwrap(), + Event::Volume(volume) => gen.write().unwrap().set_volume(volume), + } + } + } + }); + Self { s, - r, playing: false, volume, - duration: Arc::new(RwLock::new(Duration::default())), - elapsed: Arc::new(RwLock::new(Duration::default())), + elapsed, + duration: Duration::default(), songs: Index::default(), + generator, } } pub fn update(&mut self) { - //TODO: Check if the song is finished playing and skip to next one. + if self.generator.read().unwrap().is_done() { + self.next(); + } } - pub fn duration(&self) -> Duration { - *self.duration.read().unwrap() + self.duration } pub fn elapsed(&self) -> Duration { *self.elapsed.read().unwrap() @@ -82,11 +128,9 @@ impl Player { } pub fn play_selected(&mut self) { if let Some(song) = self.songs.selected() { - if self.playing { - self.stop(); - } self.playing = true; - self.run(song.path.clone()); + self.generator.write().unwrap().update(&song.path.clone()); + self.duration = self.generator.read().unwrap().duration(); } } pub fn play_index(&mut self, i: usize) { @@ -117,7 +161,7 @@ impl Player { } pub fn clear(&mut self) { self.songs = Index::default(); - self.stop(); + self.generator.write().unwrap().stop(); } pub fn clear_except_playing(&mut self) { let selected = self.songs.selected().cloned(); @@ -131,7 +175,6 @@ impl Player { } self.songs.select(Some(0)); } - pub fn randomize(&self) {} pub fn toggle_playback(&mut self) { if self.playing { self.pause(); @@ -139,6 +182,14 @@ impl Player { self.play(); } } + fn play(&mut self) { + self.s.send(Event::Play).unwrap(); + self.playing = true; + } + fn pause(&mut self) { + self.s.send(Event::Pause).unwrap(); + self.playing = false; + } pub fn previous(&mut self) { self.songs.up(); self.play_selected(); @@ -163,6 +214,7 @@ impl Player { self.update_volume(); } + pub fn randomize(&self) {} fn update_volume(&self) { self.s.send(Event::Volume(self.real_volume())).unwrap(); } @@ -187,18 +239,10 @@ impl Player { pub fn total_songs(&self) -> usize { self.songs.len() } - fn play(&mut self) { - self.s.send(Event::Play).unwrap(); - self.playing = true; - } - pub fn get_index(&self) -> &Index<Song> { &self.songs } - fn pause(&mut self) { - self.s.send(Event::Pause).unwrap(); - self.playing = false; - } + pub fn selected_song(&self) -> Option<&Song> { self.songs.selected() } @@ -208,62 +252,6 @@ impl Player { pub fn seek_to(&self, duration: f32) { self.s.send(Event::SeekTo(duration)).unwrap(); } - pub fn stop(&self) { - self.s.send(Event::Stop).unwrap(); - } - fn run(&self, path: PathBuf) { - let r = self.r.clone(); - let duration = self.duration.clone(); - let elapsed = self.elapsed.clone(); - let volume = self.real_volume(); - - thread::spawn(move || { - let device = cpal::default_host().default_output_device().unwrap(); - let config = device.default_output_config().unwrap(); - - let processor = Arc::new(RwLock::new(SampleProcessor::new( - Some(config.sample_rate().0), - path, - volume, - ))); - - //Update the duration; - *duration.write().unwrap() = processor.read().unwrap().duration; - - let p = processor.clone(); - - let stream = device - .build_output_stream( - &config.config(), - move |data: &mut [f32], _: &cpal::OutputCallbackInfo| { - for frame in data.chunks_mut(2) { - for sample in frame.iter_mut() { - *sample = p.write().unwrap().next_sample(); - } - } - }, - |err| panic!("{}", err), - ) - .unwrap(); - - stream.play().unwrap(); - - loop { - *elapsed.write().unwrap() = processor.read().unwrap().elapsed; - - if let Ok(event) = r.recv_timeout(Duration::from_millis(16)) { - match event { - Event::Play => stream.play().unwrap(), - Event::Pause => stream.pause().unwrap(), - Event::SeekBy(duration) => processor.write().unwrap().seek_by(duration), - Event::SeekTo(duration) => processor.write().unwrap().seek_to(duration), - Event::Volume(volume) => processor.write().unwrap().volume = volume, - Event::Stop => break, - } - } - } - }); - } pub fn audio_devices() -> Vec<Device> { let host_id = cpal::default_host().id(); let host = cpal::host_from_id(host_id).unwrap(); @@ -276,6 +264,7 @@ impl Player { cpal::default_host().default_output_device().unwrap() } pub fn change_output_device(&mut self, _device: &Device) -> Result<(), StreamError> { + //TODO Ok(()) // match OutputStream::try_from_device(device) { // Ok((stream, handle)) => { diff --git a/gonk-player/src/sample_processor.rs b/gonk-player/src/sample_processor.rs index 75ec1f0a..1db1b7d4 100644 --- a/gonk-player/src/sample_processor.rs +++ b/gonk-player/src/sample_processor.rs @@ -4,7 +4,7 @@ use symphonia::{ core::{ audio::{SampleBuffer, SignalSpec}, codecs::{Decoder, DecoderOptions}, - errors::Error, + errors::{Error, SeekErrorKind}, formats::{FormatOptions, FormatReader, SeekMode, SeekTo}, io::MediaSourceStream, meta::MetadataOptions, @@ -14,21 +14,97 @@ use symphonia::{ default::get_probe, }; -pub struct SampleProcessor { +pub struct Generator { + processor: Option<Processor>, + sample_rate: u32, + volume: f32, +} + +impl Generator { + pub fn new(sample_rate: u32, volume: f32) -> Self { + Self { + processor: None, + sample_rate, + volume, + } + } + pub fn next(&mut self) -> f32 { + if let Some(processor) = &mut self.processor { + if processor.finished { + 0.0 + } else { + processor.next_sample() + } + } else { + 0.0 + } + } + pub fn seek_to(&mut self, time: f32) -> Result<(), ()> { + if let Some(processor) = &mut self.processor { + processor.seek_to(time); + Ok(()) + } else { + Err(()) + } + } + pub fn elapsed(&self) -> Duration { + if let Some(processor) = &self.processor { + processor.elapsed + } else { + Duration::default() + } + } + pub fn duration(&self) -> Duration { + if let Some(processor) = &self.processor { + processor.duration + } else { + Duration::default() + } + } + pub fn seek_by(&mut self, time: f32) -> Result<(), ()> { + if let Some(processor) = &mut self.processor { + processor.seek_by(time); + Ok(()) + } else { + Err(()) + } + } + pub fn set_volume(&mut self, volume: f32) { + self.volume = volume; + if let Some(processor) = &mut self.processor { + processor.volume = volume; + } + } + pub fn update(&mut self, path: &Path) { + self.processor = Some(Processor::new(self.sample_rate, path, self.volume)); + } + pub fn is_done(&self) -> bool { + if let Some(processor) = &self.processor { + processor.finished + } else { + false + } + } + pub fn stop(&mut self) { + self.processor = None; + } +} + +pub struct Processor { pub decoder: Box<dyn Decoder>, pub format: Box<dyn FormatReader>, pub spec: SignalSpec, pub capacity: u64, - pub duration: Duration, - pub elapsed: Duration, pub converter: SampleRateConverter, pub finished: bool, pub left: bool, + pub duration: Duration, + pub elapsed: Duration, pub volume: f32, } -impl SampleProcessor { - pub fn new(sample_rate: Option<u32>, path: impl AsRef<Path>, volume: f32) -> Self { +impl Processor { + pub fn new(sample_rate: u32, path: &Path, volume: f32) -> Self { let source = Box::new(File::open(path).unwrap()); let mss = MediaSourceStream::new(source, Default::default()); @@ -55,6 +131,7 @@ impl SampleProcessor { } else { panic!("Could not decode track duration."); }; + let mut decoder = symphonia::default::get_codecs() .make(&track.codec_params, &DecoderOptions::default()) .unwrap(); @@ -78,7 +155,7 @@ impl SampleProcessor { converter: SampleRateConverter::new( sample_buffer.samples().to_vec().into_iter(), spec.rate, - sample_rate.unwrap_or(44100), + sample_rate, ), finished: false, left: true, @@ -87,7 +164,9 @@ impl SampleProcessor { } pub fn next_sample(&mut self) -> f32 { loop { - if let Some(sample) = self.converter.next() { + if self.finished { + return 0.0; + } else if let Some(sample) = self.converter.next() { return sample * self.volume; } else { self.update(); @@ -95,6 +174,10 @@ impl SampleProcessor { } } pub fn update(&mut self) { + if self.finished { + return; + } + let mut decode_errors: usize = 0; const MAX_DECODE_ERRORS: usize = 3; loop { @@ -123,6 +206,7 @@ impl SampleProcessor { } Error::IoError(e) if e.kind() == ErrorKind::UnexpectedEof => { self.finished = true; + return; } _ => panic!("{:?}", e), }, @@ -135,14 +219,21 @@ impl SampleProcessor { } pub fn seek_to(&mut self, time: f32) { let time = Duration::from_secs_f32(time); - self.format - .seek( - SeekMode::Coarse, - SeekTo::Time { - time: Time::new(time.as_secs(), time.subsec_nanos() as f64 / 1_000_000_000.0), - track_id: None, + match self.format.seek( + SeekMode::Coarse, + SeekTo::Time { + time: Time::new(time.as_secs(), time.subsec_nanos() as f64 / 1_000_000_000.0), + track_id: None, + }, + ) { + Ok(_) => (), + Err(e) => match e { + Error::SeekError(e) => match e { + SeekErrorKind::OutOfRange => self.finished = true, + _ => panic!("{:?}", e), }, - ) - .unwrap(); + _ => panic!("{}", e), + }, + } } } From bc13b1f172efc9b2450599459faef2b8e3ed3977 Mon Sep 17 00:00:00 2001 From: zX3no <dr_xeno@hotmail.com.au> Date: Wed, 15 Jun 2022 14:33:05 +0930 Subject: [PATCH 10/40] fixed broken volume when playing new songs --- gonk-player/src/lib.rs | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/gonk-player/src/lib.rs b/gonk-player/src/lib.rs index 937d6eb2..b657bb89 100644 --- a/gonk-player/src/lib.rs +++ b/gonk-player/src/lib.rs @@ -129,8 +129,10 @@ impl Player { pub fn play_selected(&mut self) { if let Some(song) = self.songs.selected() { self.playing = true; - self.generator.write().unwrap().update(&song.path.clone()); - self.duration = self.generator.read().unwrap().duration(); + let mut gen = self.generator.write().unwrap(); + gen.update(&song.path.clone()); + gen.set_volume(self.real_volume()); + self.duration = gen.duration(); } } pub fn play_index(&mut self, i: usize) { From 20534431936ad7e381c0d4b403929fd73b7ecd83 Mon Sep 17 00:00:00 2001 From: zX3no <dr_xeno@hotmail.com.au> Date: Wed, 15 Jun 2022 14:57:06 +0930 Subject: [PATCH 11/40] cleanup --- gonk-player/src/lib.rs | 17 +++++++++++++++-- gonk/src/app.rs | 6 +++++- gonk/src/app/options.rs | 26 ++++++++++++++------------ gonk/src/app/queue.rs | 4 ++-- 4 files changed, 36 insertions(+), 17 deletions(-) diff --git a/gonk-player/src/lib.rs b/gonk-player/src/lib.rs index b657bb89..0b5cf9b9 100644 --- a/gonk-player/src/lib.rs +++ b/gonk-player/src/lib.rs @@ -43,10 +43,20 @@ pub struct Player { } impl Player { - pub fn new(volume: u16) -> Self { - let (s, r) = unbounded(); + //TODO: get device from toml file + pub fn new(_device: String, volume: u16) -> Self { + // let host_id = cpal::default_host().id(); + // let host = cpal::host_from_id(host_id).unwrap(); + // let mut devices: Vec<Device> = host.devices().unwrap().collect(); + // devices.retain(|host| host.name().unwrap() == device); + // let device = if devices.is_empty() { + // cpal::default_host().default_output_device().unwrap() + // } else { + // devices.remove(0) + // }; let device = cpal::default_host().default_output_device().unwrap(); + let config = device.default_output_config().unwrap(); let rate = config.sample_rate().0; @@ -60,6 +70,8 @@ impl Player { let elapsed = Arc::new(RwLock::new(Duration::default())); let e = elapsed.clone(); + let (s, r) = unbounded(); + thread::spawn(move || { let stream = device .build_output_stream( @@ -254,6 +266,7 @@ impl Player { pub fn seek_to(&self, duration: f32) { self.s.send(Event::SeekTo(duration)).unwrap(); } + //TODO: Remove? pub fn audio_devices() -> Vec<Device> { let host_id = cpal::default_host().id(); let host = cpal::host_from_id(host_id).unwrap(); diff --git a/gonk/src/app.rs b/gonk/src/app.rs index c9005bd2..03fd0aee 100644 --- a/gonk/src/app.rs +++ b/gonk/src/app.rs @@ -118,7 +118,11 @@ impl App { Ok(Self { terminal, mode: Mode::Browser, - queue: Queue::new(toml.config.volume, toml.colors), + queue: Queue::new( + toml.config.volume, + toml.colors, + toml.config.output_device.clone(), + ), browser: Browser::new(), options: Options::new(&mut toml), search: Search::new(toml.colors).init(), diff --git a/gonk/src/app/options.rs b/gonk/src/app/options.rs index de8b36f1..855a427f 100644 --- a/gonk/src/app/options.rs +++ b/gonk/src/app/options.rs @@ -38,19 +38,21 @@ impl Options { pub fn down(&mut self) { self.devices.down(); } + #[allow(unused)] pub fn on_enter(&mut self, player: &mut Player, toml: &mut Toml) { - if let Some(device) = self.devices.selected() { - //don't update the device if there is an error - match player.change_output_device(device) { - Ok(_) => { - let name = device.name().unwrap(); - self.current_device = name.clone(); - toml.set_output_device(name); - } - //TODO: Print error in status bar - Err(e) => panic!("{:?}", e), - } - } + //TODO: Update playback device. + // if let Some(device) = self.devices.selected() { + // //don't update the device if there is an error + // match player.change_output_device(device) { + // Ok(_) => { + // let name = device.name().unwrap(); + // self.current_device = name.clone(); + // toml.set_output_device(name); + // } + // //TODO: Print error in status bar + // Err(e) => panic!("{:?}", e), + // } + // } } } diff --git a/gonk/src/app/queue.rs b/gonk/src/app/queue.rs index aa29cab4..163333d0 100644 --- a/gonk/src/app/queue.rs +++ b/gonk/src/app/queue.rs @@ -18,12 +18,12 @@ pub struct Queue { } impl Queue { - pub fn new(vol: u16, colors: Colors) -> Self { + pub fn new(vol: u16, colors: Colors, device: String) -> Self { Self { ui: Index::default(), constraint: [8, 42, 24, 26], clicked_pos: None, - player: Player::new(vol), + player: Player::new(device, vol), colors, } } From 842b024e178af09da51c7f3d72ec86fec17bbebb Mon Sep 17 00:00:00 2001 From: zX3no <dr_xeno@hotmail.com.au> Date: Sat, 18 Jun 2022 14:06:46 +0930 Subject: [PATCH 12/40] ci: build action --- .github/workflows/build.yml | 22 ++++++++++++++++++++++ gonk-player/src/sample_processor.rs | 2 +- 2 files changed, 23 insertions(+), 1 deletion(-) create mode 100644 .github/workflows/build.yml diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml new file mode 100644 index 00000000..45bae997 --- /dev/null +++ b/.github/workflows/build.yml @@ -0,0 +1,22 @@ +name: Build + +on: + push: + branches: [ main ] + pull_request: + branches: [ main ] + +env: + CARGO_TERM_COLOR: always + +jobs: + build: + + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v2 + - name: Install libasound2-dev + run: sudo apt install -y libasound2-dev + - name: Build + run: cargo build --verbose \ No newline at end of file diff --git a/gonk-player/src/sample_processor.rs b/gonk-player/src/sample_processor.rs index 1db1b7d4..29ce38c7 100644 --- a/gonk-player/src/sample_processor.rs +++ b/gonk-player/src/sample_processor.rs @@ -195,7 +195,7 @@ impl Processor { let tb = track.codec_params.time_base.unwrap(); let t = tb.calc_time(ts); self.elapsed = Duration::from_secs(t.seconds) + Duration::from_secs_f64(t.frac); - break; + return; } Err(e) => match e { Error::DecodeError(e) => { From 02c55eacf80b23faaf20163e995ae12f881ca3ca Mon Sep 17 00:00:00 2001 From: zX3no <dr_xeno@hotmail.com.au> Date: Wed, 22 Jun 2022 13:57:41 +0930 Subject: [PATCH 13/40] Clean failed merge from 'main' --- gonk/src/app.rs | 6 +----- gonk/src/app/queue.rs | 11 ++++------- gonk/src/app/status_bar.rs | 3 ++- 3 files changed, 7 insertions(+), 13 deletions(-) diff --git a/gonk/src/app.rs b/gonk/src/app.rs index b95afd66..d96bd2fb 100644 --- a/gonk/src/app.rs +++ b/gonk/src/app.rs @@ -118,11 +118,7 @@ impl App { Ok(Self { terminal, mode: Mode::Browser, - queue: Queue::new( - toml.config.volume, - toml.colors, - toml.config.output_device.clone(), - ), + queue: Queue::new(toml.config.volume, toml.config.output_device.clone()), browser: Browser::new(), options: Options::new(&mut toml), search: Search::new().init(), diff --git a/gonk/src/app/queue.rs b/gonk/src/app/queue.rs index 4fa308e7..9b20050f 100644 --- a/gonk/src/app/queue.rs +++ b/gonk/src/app/queue.rs @@ -1,6 +1,5 @@ -use crate::toml::Colors; use crate::widgets::{Cell, Gauge, Row, Table, TableState}; -use crate::Frame; +use crate::{Frame, COLORS}; use crossterm::event::{KeyModifiers, MouseEvent}; use gonk_player::{Index, Player}; use tui::layout::{Alignment, Constraint, Direction, Layout, Rect}; @@ -16,13 +15,11 @@ pub struct Queue { } impl Queue { - pub fn new(vol: u16, colors: Colors, device: String) -> Self { + pub fn new(vol: u16, device: String) -> Self { Self { ui: Index::default(), constraint: [8, 42, 24, 26], - clicked_pos: None, player: Player::new(device, vol), - colors, } } pub fn update(&mut self) { @@ -197,7 +194,7 @@ impl Queue { f.render_widget(Paragraph::new(title).alignment(Alignment::Center), area); } fn draw_body(&mut self, f: &mut Frame, area: Rect) -> Option<(usize, usize)> { - if self.player.songs.is_empty() { + if self.player.is_empty() { f.render_widget( Block::default() .border_type(BorderType::Rounded) @@ -353,7 +350,7 @@ impl Queue { .borders(Borders::ALL) .border_type(BorderType::Rounded), ) - .gauge_style(Style::default().fg(self.colors.seeker)) + .gauge_style(Style::default().fg(COLORS.seeker)) .ratio(ratio as f64) .label(seeker), area, diff --git a/gonk/src/app/status_bar.rs b/gonk/src/app/status_bar.rs index 245c1eb5..f24f0776 100644 --- a/gonk/src/app/status_bar.rs +++ b/gonk/src/app/status_bar.rs @@ -1,6 +1,7 @@ use super::queue::Queue; +use crate::sqlite; use crate::Frame; -use crate::{sqlite, toml::Colors}; +use crate::COLORS; use std::time::{Duration, Instant}; use tui::{ layout::{Alignment, Constraint, Direction, Layout, Rect}, From 7a508eb7462eaff954fb312537fe4b62e1c9bc65 Mon Sep 17 00:00:00 2001 From: zX3no <dr_xeno@hotmail.com.au> Date: Wed, 22 Jun 2022 14:06:27 +0930 Subject: [PATCH 14/40] Clean failed merge into 'main' --- gonk/src/app/queue.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/gonk/src/app/queue.rs b/gonk/src/app/queue.rs index fe0aabf0..275ed68c 100644 --- a/gonk/src/app/queue.rs +++ b/gonk/src/app/queue.rs @@ -104,8 +104,8 @@ impl Queue { if (size.height - 3 == y || size.height - 2 == y || size.height - 1 == y) && size.height > 15 { - let ratio = f64::from(x) / f64::from(size.width); - let duration = self.player.duration; + let ratio = x as f32 / size.width as f32; + let duration = self.player.duration().as_secs_f32(); let new_time = duration * ratio; self.player.seek_to(new_time); } From 2f52d16569e23ac2761e7e207d0f40d4cc69436c Mon Sep 17 00:00:00 2001 From: zX3no <dr_xeno@hotmail.com.au> Date: Sat, 25 Jun 2022 15:33:08 +0930 Subject: [PATCH 15/40] Merged 'main into 'new_player' --- .gitignore | 7 +- Cargo.toml | 2 +- gonk-database/Cargo.toml | 13 + gonk-database/src/database.rs | 61 +++++ gonk-database/src/lib.rs | 277 +++++++++++++++++++ gonk-database/src/playlist.rs | 90 +++++++ gonk-database/src/query.rs | 182 +++++++++++++ gonk-player/src/lib.rs | 22 +- gonk-player/src/song.rs | 27 +- gonk/Cargo.toml | 29 +- gonk/src/app.rs | 384 --------------------------- gonk/src/app/browser.rs | 214 --------------- gonk/src/app/options.rs | 91 ------- gonk/src/app/playlist.rs | 386 --------------------------- gonk/src/app/queue.rs | 361 ------------------------- gonk/src/app/search.rs | 478 --------------------------------- gonk/src/app/status_bar.rs | 155 ----------- gonk/src/browser.rs | 220 +++++++++++++++ gonk/src/main.rs | 391 +++++++++++++++++++++++++-- gonk/src/playlist.rs | 371 ++++++++++++++++++++++++++ gonk/src/queue.rs | 360 +++++++++++++++++++++++++ gonk/src/search.rs | 485 ++++++++++++++++++++++++++++++++++ gonk/src/settings.rs | 98 +++++++ gonk/src/sqlite.rs | 307 --------------------- gonk/src/status_bar.rs | 143 ++++++++++ gonk/src/toml.rs | 262 ------------------ gonk/src/widgets/list.rs | 21 +- gonk/src/widgets/table.rs | 8 +- 28 files changed, 2701 insertions(+), 2744 deletions(-) create mode 100644 gonk-database/Cargo.toml create mode 100644 gonk-database/src/database.rs create mode 100644 gonk-database/src/lib.rs create mode 100644 gonk-database/src/playlist.rs create mode 100644 gonk-database/src/query.rs delete mode 100644 gonk/src/app.rs delete mode 100644 gonk/src/app/browser.rs delete mode 100644 gonk/src/app/options.rs delete mode 100644 gonk/src/app/playlist.rs delete mode 100644 gonk/src/app/queue.rs delete mode 100644 gonk/src/app/search.rs delete mode 100644 gonk/src/app/status_bar.rs create mode 100644 gonk/src/browser.rs create mode 100644 gonk/src/playlist.rs create mode 100644 gonk/src/queue.rs create mode 100644 gonk/src/search.rs create mode 100644 gonk/src/settings.rs delete mode 100644 gonk/src/sqlite.rs create mode 100644 gonk/src/status_bar.rs delete mode 100644 gonk/src/toml.rs diff --git a/.gitignore b/.gitignore index b456e766..a9719ed7 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,7 @@ /target -/gonk/target -Cargo.lock +/.vscode + *.log *.opt -/.vscode \ No newline at end of file +*.db +*.lock \ No newline at end of file diff --git a/Cargo.toml b/Cargo.toml index a4927824..d8cb9068 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,2 +1,2 @@ [workspace] -members = ["gonk", "gonk-player"] \ No newline at end of file +members = ["gonk", "gonk-player", "gonk-database"] \ No newline at end of file diff --git a/gonk-database/Cargo.toml b/gonk-database/Cargo.toml new file mode 100644 index 00000000..49f856a5 --- /dev/null +++ b/gonk-database/Cargo.toml @@ -0,0 +1,13 @@ +[package] +name = "gonk-database" +version = "0.1.0" +edition = "2021" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +jwalk = "0.6.0" +rayon = "1.5.3" +rusqlite = { version = "0.27.0", features = ["bundled"] } +gonk-player = {path = "../gonk-player"} +lazy_static = "1.4.0" diff --git a/gonk-database/src/database.rs b/gonk-database/src/database.rs new file mode 100644 index 00000000..f8234dec --- /dev/null +++ b/gonk-database/src/database.rs @@ -0,0 +1,61 @@ +use crate::*; +use std::thread::{self, JoinHandle}; + +#[derive(Debug, Eq, PartialEq)] +pub enum State { + Busy, + Idle, + NeedsUpdate, +} + +#[derive(Default)] +pub struct Database { + handle: Option<JoinHandle<()>>, +} + +impl Database { + pub fn add_path(&mut self, path: &str) { + if let Some(handle) = &self.handle { + if !handle.is_finished() { + return; + } + } + + let path = path.to_string(); + self.handle = Some(thread::spawn(move || { + add_folder(&path); + })); + } + + pub fn refresh(&mut self) { + if let Some(handle) = &self.handle { + if !handle.is_finished() { + return; + } + } + + self.handle = Some(thread::spawn(move || { + for path in query::paths() { + let songs = collect_songs(&path); + insert_parents(&songs); + let query = create_batch_query("song", &path, &songs); + conn().execute_batch(&query).unwrap(); + } + })); + } + + pub fn state(&mut self) -> State { + match self.handle { + Some(ref handle) => { + let finished = handle.is_finished(); + if finished { + self.handle = None; + State::NeedsUpdate + } else { + State::Busy + } + } + None => State::Idle, + } + } +} diff --git a/gonk-database/src/lib.rs b/gonk-database/src/lib.rs new file mode 100644 index 00000000..b56c643a --- /dev/null +++ b/gonk-database/src/lib.rs @@ -0,0 +1,277 @@ +#[macro_use] +extern crate lazy_static; + +use gonk_player::Song; +use jwalk::WalkDir; +use rayon::{ + iter::{IntoParallelRefIterator, ParallelBridge, ParallelIterator}, + slice::ParallelSliceMut, +}; +use rusqlite::*; +use std::{ + path::{Path, PathBuf}, + sync::{Mutex, MutexGuard}, +}; + +mod database; +pub mod playlist; +pub mod query; + +pub use crate::database::*; + +lazy_static! { + pub static ref GONK_DIR: PathBuf = { + let gonk = if cfg!(windows) { + PathBuf::from(&std::env::var("APPDATA").unwrap()) + } else { + PathBuf::from(&std::env::var("HOME").unwrap()).join(".config") + } + .join("gonk"); + + if !gonk.exists() { + std::fs::create_dir_all(&gonk).unwrap(); + } + gonk + }; + pub static ref DB_DIR: PathBuf = GONK_DIR.join("gonk.db"); + pub static ref CONN: Mutex<Connection> = { + let exists = PathBuf::from(DB_DIR.as_path()).exists(); + let conn = Connection::open(DB_DIR.as_path()).unwrap(); + + if !exists { + conn.execute( + "CREATE TABLE settings ( + volume INTEGER UNIQUE, + device TEXT UNIQUE)", + [], + ) + .unwrap(); + + conn.execute("INSERT INTO settings (volume, device) VALUES (15, '')", []) + .unwrap(); + + conn.execute( + "CREATE TABLE folder ( + path TEXT PRIMARY KEY)", + [], + ) + .unwrap(); + + conn.execute( + "CREATE TABLE artist ( + name TEXT PRIMARY KEY)", + [], + ) + .unwrap(); + + conn.execute("CREATE TABLE persist(song_id INTEGER)", []) + .unwrap(); + conn.execute( + "CREATE TABLE album ( + name TEXT PRIMARY KEY, + artist_id TEXT NOT NULL, + FOREIGN KEY (artist_id) REFERENCES artist (name) )", + [], + ) + .unwrap(); + + conn.execute( + "CREATE TABLE song ( + name TEXT NOT NULL, + disc INTEGER NOT NULL, + number INTEGER NOT NULL, + path TEXT NOT NULL, + gain DOUBLE NOT NULL, + album_id TEXT NOT NULL, + artist_id TEXT NOT NULL, + folder_id TEXT NOT NULL, + FOREIGN KEY (album_id) REFERENCES album (name), + FOREIGN KEY (artist_id) REFERENCES artist (name), + FOREIGN KEY (folder_id) REFERENCES folder (path), + UNIQUE(name, disc, number, path, album_id, artist_id, folder_id) ON CONFLICT REPLACE)", + [], + ) + .unwrap(); + + conn.execute( + "CREATE TABLE playlist ( + name TEXT PRIMARY KEY)", + [], + ) + .unwrap(); + + //Used for intersects + //https://www.sqlitetutorial.net/sqlite-intersect/ + conn.execute( + "CREATE TABLE temp_song ( + name TEXT NOT NULL, + disc INTEGER NOT NULL, + number INTEGER NOT NULL, + path TEXT NOT NULL, + gain DOUBLE NOT NULL, + album_id TEXT NOT NULL, + artist_id TEXT NOT NULL, + folder_id TEXT NOT NULL, + FOREIGN KEY (album_id) REFERENCES album (name), + FOREIGN KEY (artist_id) REFERENCES artist (name), + FOREIGN KEY (folder_id) REFERENCES folder (path) + )", + [], + ) + .unwrap(); + + conn.execute( + "CREATE TABLE playlist_item ( + path TEXT NOT NULL, + name TEXT NOT NULL, + album_id TEXT NOT NULL, + artist_id TEXT NOT NULL, + playlist_id TEXT NOT NULL, + FOREIGN KEY (album_id) REFERENCES album (name), + FOREIGN KEY (artist_id) REFERENCES artist (name), + FOREIGN KEY (playlist_id) REFERENCES playlist (name))", + [], + ) + .unwrap(); + } + + Mutex::new(conn) + }; +} + +pub fn reset() -> Result<(), &'static str> { + *CONN.lock().unwrap() = Connection::open_in_memory().unwrap(); + + if std::fs::remove_file(DB_DIR.as_path()).is_err() { + Err("Could not remove database while it's in use.") + } else { + Ok(()) + } +} + +pub fn conn() -> MutexGuard<'static, Connection> { + CONN.lock().unwrap() +} + +pub fn collect_songs(path: impl AsRef<Path>) -> Vec<Song> { + WalkDir::new(path) + .into_iter() + .flatten() + .map(|dir| dir.path()) + .filter(|path| match path.extension() { + Some(ex) => { + matches!(ex.to_str(), Some("flac" | "mp3" | "ogg" | "wav" | "m4a")) + } + None => false, + }) + .par_bridge() + .flat_map(|path| Song::from(&path)) + .collect() +} + +pub fn insert_parents(songs: &[Song]) { + let mut albums: Vec<(&str, &str)> = songs + .par_iter() + .map(|song| (song.album.as_str(), song.artist.as_str())) + .collect(); + + albums.par_sort(); + albums.dedup(); + + let mut artists: Vec<&str> = songs.par_iter().map(|song| song.artist.as_str()).collect(); + + artists.par_sort(); + artists.dedup(); + + let query: String = artists + .par_iter() + .map(|artist| { + let artist = artist.replace('\'', r"''"); + format!("INSERT OR IGNORE INTO artist (name) VALUES ('{}');", artist) + }) + .collect::<Vec<String>>() + .join("\n"); + + let query = format!("BEGIN;\n{}\nCOMMIT;", query); + conn().execute_batch(&query).unwrap(); + + let query: Vec<String> = albums + .par_iter() + .map(|(album, artist)| { + let artist = artist.replace('\'', r"''"); + let album = album.replace('\'', r"''"); + format!( + "INSERT OR IGNORE INTO album (name, artist_id) VALUES ('{}', '{}');", + album, artist + ) + }) + .collect(); + + let query = format!("BEGIN;\n{}\nCOMMIT;", query.join("\n")); + conn().execute_batch(&query).unwrap(); +} + +pub fn create_batch_query(table: &str, folder: &str, songs: &[Song]) -> String { + let queries: Vec<String> = songs + .iter() + .map(|song| { + let name = song.name.replace('\'', r"''"); + let artist = song.artist.replace('\'', r"''"); + let album = song.album.replace('\'', r"''"); + let path = song.path.to_string_lossy().replace('\'', r"''"); + let folder = folder.replace('\'', r"''"); + + format!( + "INSERT OR REPLACE INTO {} (name, disc, number, path, gain, album_id, artist_id, folder_id) + VALUES ('{}', '{}', '{}', '{}', '{}', '{}', '{}', '{}');", + table, name, song.disc, song.number, path, song.gain, album, artist, folder, + ) + }) + .collect(); + + format!("BEGIN;\n{}\nCOMMIT;", queries.join("\n")) +} + +pub fn rescan_folder(folder: &str) { + //Make sure folder exists. + if conn() + .execute("INSERT INTO folder (path) VALUES (?1)", [folder]) + .is_err() + { + //Collect the songs. + let songs = collect_songs(folder); + insert_parents(&songs); + + //Create query. + let query = create_batch_query("temp_song", folder, &songs); + + let conn = conn(); + + //Clean the temp table and add songs. + conn.execute("DELETE FROM temp_song", []).unwrap(); + conn.execute_batch(&query).unwrap(); + + //Insert songs into default table. + let query = create_batch_query("song", folder, &songs); + conn.execute_batch(&query).unwrap(); + + //Drop the difference. + conn.execute( + "DELETE FROM song WHERE rowid IN (SELECT rowid FROM song EXCEPT SELECT rowid FROM temp_song)", + [], + ).unwrap(); + } +} + +pub fn add_folder(folder: &str) { + if conn() + .execute("INSERT INTO folder (path) VALUES (?1)", [folder]) + .is_ok() + { + let songs = collect_songs(folder); + insert_parents(&songs); + + let query = create_batch_query("song", folder, &songs); + conn().execute_batch(&query).unwrap(); + } +} diff --git a/gonk-database/src/playlist.rs b/gonk-database/src/playlist.rs new file mode 100644 index 00000000..b8167e96 --- /dev/null +++ b/gonk-database/src/playlist.rs @@ -0,0 +1,90 @@ +#[allow(unused)] +use crate::query::*; +use crate::*; + +#[derive(Debug)] +pub struct PlaylistSong { + pub path: PathBuf, + pub name: String, + pub album: String, + pub artist: String, + pub id: usize, +} + +pub fn add(playlist: &str, ids: &[usize]) { + let songs = songs_from_ids(ids); + + if songs.is_empty() { + panic!("Failed to add song ids: {:?}", ids); + } + + let conn = conn(); + conn.execute( + "INSERT OR IGNORE INTO playlist (name) VALUES (?1)", + [playlist], + ) + .unwrap(); + + let query: Vec<String> = songs.iter().map(|song|{ + let name = song.name.replace('\'', r"''"); + let artist = song.artist.replace('\'', r"''"); + let album = song.album.replace('\'', r"''"); + let path = song.path.to_string_lossy().replace('\'', r"''"); + let playlist = playlist.replace('\'', r"''"); + format!("INSERT OR IGNORE INTO playlist_item (path, name, album_id, artist_id, playlist_id) VALUES ('{}', '{}', '{}', '{}', '{}');", + path, name, album, artist, playlist) + }).collect(); + + let query = format!("BEGIN;\n{}\nCOMMIT;", query.join("\n")); + conn.execute_batch(&query).unwrap(); +} + +//Only select playlists with songs in them +pub fn playlists() -> Vec<String> { + let conn = conn(); + let mut stmt = conn + .prepare("SELECT DISTINCT playlist_id FROM playlist_item") + .unwrap(); + + stmt.query_map([], |row| row.get(0)) + .unwrap() + .flatten() + .collect() +} + +pub fn get(playlist_name: &str) -> Vec<PlaylistSong> { + let conn = conn(); + let mut stmt = conn + .prepare("SELECT path, name, album_id, artist_id, rowid FROM playlist_item WHERE playlist_id = ?") + .unwrap(); + + stmt.query_map([playlist_name], |row| { + let path: String = row.get(0).unwrap(); + + Ok(PlaylistSong { + path: PathBuf::from(path), + name: row.get(1).unwrap(), + album: row.get(2).unwrap(), + artist: row.get(3).unwrap(), + id: row.get(4).unwrap(), + }) + }) + .unwrap() + .flatten() + .collect() +} + +pub fn remove_id(id: usize) { + conn() + .execute("DELETE FROM playlist_item WHERE rowid = ?", [id]) + .unwrap(); +} + +pub fn remove(name: &str) { + let conn = conn(); + conn.execute("DELETE FROM playlist_item WHERE playlist_id = ?", [name]) + .unwrap(); + + conn.execute("DELETE FROM playlist WHERE name = ?", [name]) + .unwrap(); +} diff --git a/gonk-database/src/query.rs b/gonk-database/src/query.rs new file mode 100644 index 00000000..82c2d893 --- /dev/null +++ b/gonk-database/src/query.rs @@ -0,0 +1,182 @@ +use crate::conn; +use gonk_player::Song; +use rusqlite::*; +use std::path::PathBuf; + +pub fn cache(ids: &[usize]) { + let conn = conn(); + + conn.execute("DELETE FROM persist", []).unwrap(); + + for id in ids { + conn.execute("INSERT INTO persist (song_id) VALUES (?)", [id]) + .unwrap(); + } +} + +pub fn get_cache() -> Vec<Song> { + let ids: Vec<usize> = { + let conn = conn(); + let mut stmt = conn.prepare("SELECT song_id FROM persist").unwrap(); + + stmt.query_map([], |row| row.get(0)) + .unwrap() + .flatten() + .collect() + }; + + songs_from_ids(&ids) +} + +pub fn volume() -> u16 { + let conn = conn(); + let mut stmt = conn.prepare("SELECT volume FROM settings").unwrap(); + stmt.query_row([], |row| row.get(0)).unwrap() +} + +pub fn set_volume(vol: u16) { + conn() + .execute("UPDATE settings SET volume = ?", [vol]) + .unwrap(); +} + +pub fn paths() -> Vec<String> { + let conn = conn(); + let mut stmt = conn.prepare("SELECT path FROM folder").unwrap(); + + stmt.query_map([], |row| row.get(0)) + .unwrap() + .flatten() + .collect() +} + +pub fn remove_path(path: &str) -> Result<(), &str> { + let conn = conn(); + let result = conn + .execute("DELETE FROM folder WHERE path = ?", [path]) + .unwrap(); + if result == 0 { + Err("Invalid path.") + } else { + Ok(()) + } +} + +pub fn total_songs() -> usize { + let conn = conn(); + let mut stmt = conn.prepare("SELECT COUNT(*) FROM song").unwrap(); + stmt.query_row([], |row| row.get(0)).unwrap() +} + +pub fn songs() -> Vec<Song> { + collect_songs("SELECT *, rowid FROM song", params![]) +} + +pub fn artists() -> Vec<String> { + let conn = conn(); + let mut stmt = conn + .prepare("SELECT name FROM artist ORDER BY name COLLATE NOCASE") + .unwrap(); + + stmt.query_map([], |row| { + let artist: String = row.get(0).unwrap(); + Ok(artist) + }) + .unwrap() + .flatten() + .collect() +} + +pub fn albums() -> Vec<(String, String)> { + let conn = conn(); + let mut stmt = conn + .prepare("SELECT name, artist_id FROM album ORDER BY artist_id COLLATE NOCASE") + .unwrap(); + + stmt.query_map([], |row| { + let album: String = row.get(0).unwrap(); + let artist: String = row.get(1).unwrap(); + Ok((album, artist)) + }) + .unwrap() + .flatten() + .collect() +} + +pub fn albums_by_artist(artist: &str) -> Vec<String> { + let conn = conn(); + let mut stmt = conn + .prepare("SELECT name FROM album WHERE artist_id = ? ORDER BY name COLLATE NOCASE") + .unwrap(); + + stmt.query_map([artist], |row| row.get(0)) + .unwrap() + .flatten() + .collect() +} + +pub fn songs_from_album(album: &str, artist: &str) -> Vec<Song> { + collect_songs( + "SELECT *, rowid FROM song WHERE artist_id = (?1) AND album_id = (?2) ORDER BY disc, number", + params![artist, album], + ) +} + +pub fn songs_by_artist(artist: &str) -> Vec<Song> { + collect_songs( + "SELECT *, rowid FROM song WHERE artist_id = ? ORDER BY album_id, disc, number", + params![artist], + ) +} + +pub fn songs_from_ids(ids: &[usize]) -> Vec<Song> { + let conn = conn(); + let mut stmt = conn + .prepare("SELECT *, rowid FROM song WHERE rowid = ?") + .unwrap(); + + //TODO: Maybe batch this? + ids.iter() + .flat_map(|id| stmt.query_row([id], |row| Ok(song(row)))) + .collect() +} + +fn collect_songs<P>(query: &str, params: P) -> Vec<Song> +where + P: Params, +{ + let conn = conn(); + let mut stmt = conn.prepare(query).expect(query); + + stmt.query_map(params, |row| Ok(song(row))) + .unwrap() + .flatten() + .collect() +} + +fn song(row: &Row) -> Song { + let path: String = row.get(3).unwrap(); + Song { + name: row.get(0).unwrap(), + disc: row.get(1).unwrap(), + number: row.get(2).unwrap(), + path: PathBuf::from(path), + gain: row.get(4).unwrap(), + album: row.get(5).unwrap(), + artist: row.get(6).unwrap(), + // folder: row.get(7).unwrap(), + id: row.get(8).unwrap(), + } +} + +pub fn playback_device() -> String { + let conn = conn(); + let mut stmt = conn.prepare("SELECT device FROM settings").unwrap(); + stmt.query_row([], |row| row.get(0)).unwrap() +} + +pub fn set_playback_device(name: &str) { + conn() + .execute("UPDATE settings SET device = ? ", [name]) + .unwrap(); +} diff --git a/gonk-player/src/lib.rs b/gonk-player/src/lib.rs index 0b5cf9b9..17ce6c91 100644 --- a/gonk-player/src/lib.rs +++ b/gonk-player/src/lib.rs @@ -33,18 +33,18 @@ pub enum Event { } pub struct Player { - s: Sender<Event>, - playing: bool, - volume: u16, - songs: Index<Song>, - elapsed: Arc<RwLock<Duration>>, - generator: Arc<RwLock<Generator>>, - duration: Duration, + pub s: Sender<Event>, + pub playing: bool, + pub volume: u16, + pub songs: Index<Song>, + pub elapsed: Arc<RwLock<Duration>>, + pub generator: Arc<RwLock<Generator>>, + pub duration: Duration, } impl Player { //TODO: get device from toml file - pub fn new(_device: String, volume: u16) -> Self { + pub fn new(_device: String, volume: u16, _songs: &[Song]) -> Self { // let host_id = cpal::default_host().id(); // let host = cpal::host_from_id(host_id).unwrap(); // let mut devices: Vec<Device> = host.devices().unwrap().collect(); @@ -236,12 +236,12 @@ impl Player { if let Some(song) = self.songs.selected() { let volume = self.volume as f32 / VOLUME_REDUCTION; //Calculate the volume with gain - if song.track_gain == 0.0 { + if song.gain == 0.0 { //Reduce the volume a little to match //songs with replay gain information. volume * 0.75 } else { - volume * song.track_gain as f32 + volume * song.gain as f32 } } else { self.volume as f32 / VOLUME_REDUCTION @@ -298,3 +298,5 @@ impl Player { // } } } + +unsafe impl Sync for Player {} diff --git a/gonk-player/src/song.rs b/gonk-player/src/song.rs index 5ea3084e..45659ba6 100644 --- a/gonk-player/src/song.rs +++ b/gonk-player/src/song.rs @@ -1,7 +1,6 @@ use std::{ fs::File, path::{Path, PathBuf}, - time::Duration, }; use symphonia::{ core::{ @@ -25,8 +24,7 @@ pub struct Song { pub album: String, pub artist: String, pub path: PathBuf, - pub duration: Duration, - pub track_gain: f64, + pub gain: f64, pub id: Option<usize>, } @@ -89,7 +87,7 @@ impl Song { .unwrap() .parse() .unwrap_or(0.0); - song.track_gain = db_to_amplitude(db); + song.gain = db_to_amplitude(db); } _ => (), } @@ -114,27 +112,6 @@ impl Song { song.album = String::from("Unknown Album"); } - //Calculate duration - let track = probe.format.default_track().unwrap(); - if let Some(tb) = track.codec_params.time_base { - let ts = track.codec_params.start_ts; - - let dur = track - .codec_params - .n_frames - .map(|frames| track.codec_params.start_ts + frames); - - if let Some(dur) = dur { - let d = tb.calc_time(dur.saturating_sub(ts)); - let duration = Duration::from_secs(d.seconds) + Duration::from_secs_f64(d.frac); - song.duration = duration; - } else { - song.duration = Duration::from_secs(0); - } - } else { - song.duration = Duration::from_secs(0); - } - Some(song) } } diff --git a/gonk/Cargo.toml b/gonk/Cargo.toml index 76712105..77bec6df 100644 --- a/gonk/Cargo.toml +++ b/gonk/Cargo.toml @@ -2,31 +2,16 @@ name = "gonk" version = "0.0.10" edition = "2021" - -authors = ["Bay"] -description = "A terminal music player" -repository = "https://github.com/zX3no/gonk" -readme = "../README.md" -license = "MIT" +default-run = "gonk" [dependencies] -# Music -gonk-player = {version = "0.0.10", path = "../gonk-player"} - -# UI crossterm = "0.23.2" -tui = { version = "0.18.0", features = ["serde"] } -unicode-width = "0.1.9" +tui = "0.18.0" -# Config -static_init = "1.0.2" -serde = { version = "1.0.137", features = ["derive"] } -toml = { version = "0.5.9", features = ["preserve_order"] } - -# Database -rusqlite = { version = "0.27.0", features = ["bundled"] } -jwalk = "0.6.0" rayon = "1.5.3" -# Search -strsim = "0.10.0" \ No newline at end of file +unicode-width = "0.1.9" +strsim = "0.10.0" + +gonk-player = {path = "../gonk-player"} +gonk-database = {path = "../gonk-database"} diff --git a/gonk/src/app.rs b/gonk/src/app.rs deleted file mode 100644 index d96bd2fb..00000000 --- a/gonk/src/app.rs +++ /dev/null @@ -1,384 +0,0 @@ -use crate::sqlite::{Database, State}; -use crate::{sqlite, toml::*}; -use crossterm::{ - event::{ - self, DisableMouseCapture, EnableMouseCapture, Event, KeyCode, KeyModifiers, MouseEventKind, - }, - execute, - terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen}, -}; -use std::time::Duration; -use std::time::Instant; -use std::{ - io::{stdout, Stdout}, - path::Path, -}; -use tui::Terminal; -use tui::{ - backend::CrosstermBackend, - layout::{Constraint, Direction, Layout}, -}; - -use self::status_bar::StatusBar; -use {browser::Browser, options::Options, playlist::Playlist, queue::Queue, search::Search}; - -mod browser; -mod options; -mod playlist; -mod queue; -mod search; -mod status_bar; - -#[derive(PartialEq, Eq, Debug, Clone)] -pub enum Mode { - Browser, - Queue, - Search, - Options, - Playlist, -} - -const TICK_RATE: Duration = Duration::from_millis(200); -const POLL_RATE: Duration = Duration::from_millis(4); -const SEEK_TIME: f32 = 10.0; - -pub struct App { - terminal: Terminal<CrosstermBackend<Stdout>>, - pub mode: Mode, - queue: Queue, - browser: Browser, - options: Options, - search: Search, - playlist: Playlist, - status_bar: StatusBar, - toml: Toml, - db: Database, - busy: bool, -} - -impl App { - pub fn new() -> Result<Self, String> { - match Toml::new().check_paths() { - Ok(mut toml) => { - let args: Vec<String> = std::env::args().skip(1).collect(); - let mut db = Database::default(); - - if let Some(first) = args.first() { - match first as &str { - "add" => { - if let Some(dir) = args.get(1..) { - let dir = dir.join(" "); - let path = Path::new(&dir); - if path.exists() { - toml.add_path(dir.clone()); - db.add_paths(&[dir]); - } else { - return Err(format!("{} is not a valid path.", dir)); - } - } - } - "reset" => { - sqlite::reset(); - toml.reset(); - return Err(String::from("Files reset!")); - } - "help" | "--help" => { - println!("Usage"); - println!(" gonk [<command> <args>]"); - println!(); - println!("Options"); - println!(" add <path> Add music to the library"); - println!(" reset Reset the database"); - return Err(String::new()); - } - _ => return Err(String::from("Invalid command.")), - } - } - - //make sure the terminal recovers after a panic - let orig_hook = std::panic::take_hook(); - std::panic::set_hook(Box::new(move |panic_info| { - disable_raw_mode().unwrap(); - execute!(stdout(), LeaveAlternateScreen, DisableMouseCapture).unwrap(); - orig_hook(panic_info); - std::process::exit(1); - })); - - //Initialize the terminal and clear the screen - let mut terminal = Terminal::new(CrosstermBackend::new(stdout())).unwrap(); - execute!( - terminal.backend_mut(), - EnterAlternateScreen, - EnableMouseCapture, - ) - .unwrap(); - enable_raw_mode().unwrap(); - terminal.clear().unwrap(); - - Ok(Self { - terminal, - mode: Mode::Browser, - queue: Queue::new(toml.config.volume, toml.config.output_device.clone()), - browser: Browser::new(), - options: Options::new(&mut toml), - search: Search::new().init(), - playlist: Playlist::new(), - status_bar: StatusBar::new(), - busy: false, - db, - toml, - }) - } - Err(err) => Err(err), - } - } - pub fn run(&mut self) -> std::io::Result<()> { - let mut last_tick = Instant::now(); - - loop { - if last_tick.elapsed() >= TICK_RATE { - //Update the status_bar at a constant rate. - self.status_bar.update(self.busy, &self.queue); - last_tick = Instant::now(); - } - - match self.db.state() { - State::Busy => self.busy = true, - State::Idle => self.busy = false, - State::NeedsUpdate => { - self.browser.refresh(); - self.search.update(); - } - } - - self.queue.update(); - - self.terminal.draw(|f| { - let area = Layout::default() - .direction(Direction::Vertical) - .constraints([Constraint::Min(2), Constraint::Length(3)]) - .split(f.size()); - - let top = if self.status_bar.is_hidden() { - f.size() - } else { - area[0] - }; - - match self.mode { - Mode::Browser => self.browser.draw(top, f), - Mode::Queue => self.queue.draw(f, None), - Mode::Options => self.options.draw(top, f), - Mode::Search => self.search.draw(top, f), - Mode::Playlist => self.playlist.draw(top, f), - }; - - if self.mode != Mode::Queue { - self.status_bar.draw(area[1], f, self.busy, &self.queue); - } - })?; - - if crossterm::event::poll(POLL_RATE)? { - match event::read()? { - Event::Key(event) => { - let hotkey = &self.toml.hotkey; - let shift = event.modifiers == KeyModifiers::SHIFT; - let bind = Bind { - key: Key::from(event.code), - modifiers: Modifier::from_bitflags(event.modifiers), - }; - - //Check if the user wants to exit. - if event.code == KeyCode::Char('C') && shift { - break; - } else if hotkey.quit == bind { - break; - }; - - match event.code { - KeyCode::Char(c) - if self.search.input_mode() && self.mode == Mode::Search => - { - self.search.on_key(c) - } - KeyCode::Char(c) - if self.playlist.input_mode() && self.mode == Mode::Playlist => - { - self.playlist.on_key(c) - } - KeyCode::Up => self.up(), - KeyCode::Down => self.down(), - KeyCode::Left => self.left(), - KeyCode::Right => self.right(), - KeyCode::Tab => { - self.mode = match self.mode { - Mode::Browser | Mode::Options => Mode::Queue, - Mode::Queue => Mode::Browser, - Mode::Search => Mode::Queue, - Mode::Playlist => Mode::Browser, - }; - } - KeyCode::Backspace => match self.mode { - Mode::Search => self.search.on_backspace(event.modifiers), - Mode::Playlist => self.playlist.on_backspace(event.modifiers), - _ => (), - }, - KeyCode::Enter if shift => match self.mode { - Mode::Browser => { - let songs = self.browser.on_enter(); - self.playlist.add_to_playlist(&songs); - self.mode = Mode::Playlist; - } - Mode::Queue => { - if let Some(song) = self.queue.player.selected_song() { - self.playlist.add_to_playlist(&[song.clone()]); - self.mode = Mode::Playlist; - } - } - _ => (), - }, - KeyCode::Enter => match self.mode { - Mode::Browser => { - let songs = self.browser.on_enter(); - self.queue.player.add_songs(&songs); - } - Mode::Queue => { - if let Some(i) = self.queue.ui.index() { - self.queue.player.play_index(i); - } - } - Mode::Search => self.search.on_enter(&mut self.queue.player), - Mode::Options => self - .options - .on_enter(&mut self.queue.player, &mut self.toml), - Mode::Playlist => self.playlist.on_enter(&mut self.queue.player), - }, - KeyCode::Esc => match self.mode { - Mode::Search => self.search.on_escape(&mut self.mode), - Mode::Options => self.mode = Mode::Queue, - Mode::Playlist => self.playlist.on_escape(&mut self.mode), - _ => (), - }, - //TODO: Rework mode changing buttons - KeyCode::Char('`') => { - self.status_bar.toggle_hidden(); - } - KeyCode::Char(',') => self.mode = Mode::Playlist, - KeyCode::Char('.') => self.mode = Mode::Options, - KeyCode::Char('/') => self.mode = Mode::Search, - KeyCode::Char('1' | '!') => { - self.queue.move_constraint(0, event.modifiers); - } - KeyCode::Char('2' | '@') => { - self.queue.move_constraint(1, event.modifiers); - } - KeyCode::Char('3' | '#') => { - self.queue.move_constraint(2, event.modifiers); - } - _ if hotkey.up == bind => self.up(), - _ if hotkey.down == bind => self.down(), - _ if hotkey.left == bind => self.left(), - _ if hotkey.right == bind => self.right(), - _ if hotkey.play_pause == bind => self.queue.player.toggle_playback(), - _ if hotkey.clear == bind => self.queue.clear(), - _ if hotkey.clear_except_playing == bind => { - self.queue.clear_except_playing(); - } - _ if hotkey.refresh_database == bind && self.mode == Mode::Browser => { - self.db.add_paths(&self.toml.config.paths); - } - _ if hotkey.seek_backward == bind && self.mode != Mode::Search => { - self.queue.player.seek_by(-SEEK_TIME) - } - _ if hotkey.seek_forward == bind && self.mode != Mode::Search => { - self.queue.player.seek_by(SEEK_TIME) - } - _ if hotkey.previous == bind && self.mode != Mode::Search => { - self.queue.player.previous() - } - _ if hotkey.next == bind && self.mode != Mode::Search => { - self.queue.player.next() - } - _ if hotkey.volume_up == bind => { - self.queue.player.volume_up(); - self.toml.set_volume(self.queue.player.get_volume()); - } - _ if hotkey.volume_down == bind => { - self.queue.player.volume_down(); - self.toml.set_volume(self.queue.player.get_volume()); - } - _ if hotkey.delete == bind => match self.mode { - Mode::Queue => self.queue.delete(), - Mode::Playlist => self.playlist.delete(), - _ => (), - }, - _ if hotkey.random == bind => self.queue.player.randomize(), - _ => (), - } - } - Event::Mouse(event) => match event.kind { - MouseEventKind::ScrollUp => self.up(), - MouseEventKind::ScrollDown => self.down(), - MouseEventKind::Down(_) => { - if let Mode::Queue = self.mode { - self.terminal.draw(|f| self.queue.draw(f, Some(event)))?; - } - } - _ => (), - }, - _ => (), - } - } - } - - Ok(()) - } - - fn left(&mut self) { - match self.mode { - Mode::Browser => self.browser.left(), - Mode::Playlist => self.playlist.left(), - _ => (), - } - } - - fn right(&mut self) { - match self.mode { - Mode::Browser => self.browser.right(), - Mode::Playlist => self.playlist.right(), - _ => (), - } - } - - fn up(&mut self) { - match self.mode { - Mode::Browser => self.browser.up(), - Mode::Queue => self.queue.up(), - Mode::Search => self.search.up(), - Mode::Options => self.options.up(), - Mode::Playlist => self.playlist.up(), - } - } - - fn down(&mut self) { - match self.mode { - Mode::Browser => self.browser.down(), - Mode::Queue => self.queue.down(), - Mode::Search => self.search.down(), - Mode::Options => self.options.down(), - Mode::Playlist => self.playlist.down(), - } - } -} - -impl Drop for App { - fn drop(&mut self) { - disable_raw_mode().unwrap(); - execute!( - self.terminal.backend_mut(), - LeaveAlternateScreen, - DisableMouseCapture - ) - .unwrap(); - } -} diff --git a/gonk/src/app/browser.rs b/gonk/src/app/browser.rs deleted file mode 100644 index 54879c25..00000000 --- a/gonk/src/app/browser.rs +++ /dev/null @@ -1,214 +0,0 @@ -use crate::widgets::{List, ListItem, ListState}; -use crate::{sqlite, Frame}; -use gonk_player::{Index, Song}; -use tui::{ - layout::{Constraint, Direction, Layout, Rect}, - style::{Color, Style}, - widgets::{Block, BorderType, Borders}, -}; - -#[derive(PartialEq, Eq)] -pub enum Mode { - Artist, - Album, - Song, -} - -impl Mode { - pub fn right(&mut self) { - match self { - Mode::Artist => *self = Mode::Album, - Mode::Album => *self = Mode::Song, - Mode::Song => (), - } - } - pub fn left(&mut self) { - match self { - Mode::Artist => (), - Mode::Album => *self = Mode::Artist, - Mode::Song => *self = Mode::Album, - } - } -} - -pub struct BrowserSong { - name: String, - id: usize, -} - -pub struct Browser { - artists: Index<String>, - albums: Index<String>, - songs: Index<BrowserSong>, - pub mode: Mode, -} - -impl Browser { - pub fn new() -> Self { - let artists = Index::new(sqlite::get_all_artists(), Some(0)); - - let (albums, songs) = if let Some(first_artist) = artists.selected() { - let albums = Index::new(sqlite::get_all_albums_by_artist(first_artist), Some(0)); - - if let Some(first_album) = albums.selected() { - let songs = sqlite::get_all_songs_from_album(first_album, first_artist) - .into_iter() - .map(|song| BrowserSong { - name: format!("{}. {}", song.number, song.name), - id: song.id.unwrap(), - }) - .collect(); - (albums, Index::new(songs, Some(0))) - } else { - (albums, Index::default()) - } - } else { - (Index::default(), Index::default()) - }; - - Self { - artists, - albums, - songs, - mode: Mode::Artist, - } - } - pub fn up(&mut self) { - match self.mode { - Mode::Artist => self.artists.up(), - Mode::Album => self.albums.up(), - Mode::Song => self.songs.up(), - } - self.update_browser(); - } - pub fn down(&mut self) { - match self.mode { - Mode::Artist => self.artists.down(), - Mode::Album => self.albums.down(), - Mode::Song => self.songs.down(), - } - self.update_browser(); - } - pub fn update_browser(&mut self) { - match self.mode { - Mode::Artist => self.update_albums(), - Mode::Album => self.update_songs(), - Mode::Song => (), - } - } - pub fn update_albums(&mut self) { - //Update the album based on artist selection - if let Some(artist) = self.artists.selected() { - self.albums = Index::new(sqlite::get_all_albums_by_artist(artist), Some(0)); - self.update_songs(); - } - } - pub fn update_songs(&mut self) { - if let Some(artist) = self.artists.selected() { - if let Some(album) = self.albums.selected() { - let songs = sqlite::get_all_songs_from_album(album, artist) - .into_iter() - .map(|song| BrowserSong { - name: format!("{}. {}", song.number, song.name), - id: song.id.unwrap(), - }) - .collect(); - self.songs = Index::new(songs, Some(0)); - } - } - } - pub fn right(&mut self) { - self.mode.right(); - } - pub fn left(&mut self) { - self.mode.left(); - } - pub fn on_enter(&self) -> Vec<Song> { - if let Some(artist) = self.artists.selected() { - if let Some(album) = self.albums.selected() { - if let Some(song) = self.songs.selected() { - return match self.mode { - Mode::Artist => sqlite::get_songs_by_artist(artist), - Mode::Album => sqlite::get_all_songs_from_album(album, artist), - Mode::Song => sqlite::get_songs(&[song.id]), - }; - } - } - } - Vec::new() - } - pub fn refresh(&mut self) { - self.mode = Mode::Artist; - - self.artists = Index::new(sqlite::get_all_artists(), Some(0)); - self.albums = Index::default(); - self.songs = Index::default(); - - self.update_albums(); - } -} - -impl Browser { - fn list<'a>(title: &'static str, content: &'a [ListItem], use_symbol: bool) -> List<'a> { - let list = List::new(content.to_vec()) - .block( - Block::default() - .title(title) - .borders(Borders::ALL) - .border_type(BorderType::Rounded), - ) - .style(Style::default().fg(Color::White)); - - if use_symbol { - list.highlight_symbol(">") - } else { - list.highlight_symbol("") - } - } - pub fn draw(&self, area: Rect, f: &mut Frame) { - let size = area.width / 3; - let rem = area.width % 3; - - let chunks = Layout::default() - .direction(Direction::Horizontal) - .constraints([ - Constraint::Length(size), - Constraint::Length(size), - Constraint::Length(size + rem), - ]) - .split(area); - - let a: Vec<ListItem> = self - .artists - .data - .iter() - .map(|name| ListItem::new(name.as_str())) - .collect(); - - let b: Vec<ListItem> = self - .albums - .data - .iter() - .map(|name| ListItem::new(name.as_str())) - .collect(); - - let c: Vec<ListItem> = self - .songs - .data - .iter() - .map(|song| ListItem::new(song.name.as_str())) - .collect(); - - let artists = Browser::list("─Aritst", &a, self.mode == Mode::Artist); - let albums = Browser::list("─Album", &b, self.mode == Mode::Album); - let songs = Browser::list("─Song", &c, self.mode == Mode::Song); - - f.render_stateful_widget( - artists, - chunks[0], - &mut ListState::new(self.artists.index()), - ); - f.render_stateful_widget(albums, chunks[1], &mut ListState::new(self.albums.index())); - f.render_stateful_widget(songs, chunks[2], &mut ListState::new(self.songs.index())); - } -} diff --git a/gonk/src/app/options.rs b/gonk/src/app/options.rs deleted file mode 100644 index 855a427f..00000000 --- a/gonk/src/app/options.rs +++ /dev/null @@ -1,91 +0,0 @@ -use crate::toml::Toml; -use crate::widgets::{List, ListItem, ListState}; -use crate::Frame; -use gonk_player::{Device, DeviceTrait, Index, Player}; -use tui::{ - layout::Rect, - style::{Color, Modifier, Style}, - widgets::{Block, BorderType, Borders}, -}; - -pub struct Options { - pub devices: Index<Device>, - current_device: String, -} - -impl Options { - pub fn new(toml: &mut Toml) -> Self { - let default_device = Player::default_device(); - let mut config_device = toml.config.output_device.clone(); - - let devices = Player::audio_devices(); - let device_names: Vec<String> = devices.iter().flat_map(DeviceTrait::name).collect(); - - if !device_names.contains(&config_device) { - let name = default_device.name().unwrap(); - config_device = name.clone(); - toml.set_output_device(name); - } - - Self { - devices: Index::new(devices, Some(0)), - current_device: config_device, - } - } - pub fn up(&mut self) { - self.devices.up(); - } - pub fn down(&mut self) { - self.devices.down(); - } - #[allow(unused)] - pub fn on_enter(&mut self, player: &mut Player, toml: &mut Toml) { - //TODO: Update playback device. - // if let Some(device) = self.devices.selected() { - // //don't update the device if there is an error - // match player.change_output_device(device) { - // Ok(_) => { - // let name = device.name().unwrap(); - // self.current_device = name.clone(); - // toml.set_output_device(name); - // } - // //TODO: Print error in status bar - // Err(e) => panic!("{:?}", e), - // } - // } - } -} - -impl Options { - pub fn draw(&self, area: Rect, f: &mut Frame) { - let items: Vec<_> = self - .devices - .data - .iter() - .map(|device| { - let name = device.name().unwrap(); - if name == self.current_device { - ListItem::new(name) - } else { - ListItem::new(name).style(Style::default().add_modifier(Modifier::DIM)) - } - }) - .collect(); - - let list = List::new(items) - .block( - Block::default() - .title("─Output Device") - .borders(Borders::ALL) - .border_type(BorderType::Rounded), - ) - .style(Style::default().fg(Color::White)) - .highlight_style(Style::default()) - .highlight_symbol("> "); - - let mut state = ListState::default(); - state.select(self.devices.index()); - - f.render_stateful_widget(list, area, &mut state); - } -} diff --git a/gonk/src/app/playlist.rs b/gonk/src/app/playlist.rs deleted file mode 100644 index 3c83ca9f..00000000 --- a/gonk/src/app/playlist.rs +++ /dev/null @@ -1,386 +0,0 @@ -use crate::widgets::*; -use crate::*; -use crossterm::event::KeyModifiers; -use gonk_player::{Index, Player, Song}; -use tui::style::Style; -use tui::text::Span; -use tui::{ - layout::{Constraint, Direction, Layout, Margin, Rect}, - widgets::{Block, BorderType, Borders, Clear, Paragraph}, -}; - -#[derive(PartialEq, Eq)] -enum Mode { - Playlist, - Song, - Popup, -} - -pub struct Item { - row: usize, - song: Song, -} - -pub struct Playlist { - mode: Mode, - playlist: Index<String>, - songs: Index<Item>, - songs_to_add: Vec<Song>, - search: String, - search_result: String, - changed: bool, -} - -impl Playlist { - pub fn new() -> Self { - let playlists = sqlite::playlist::get_names(); - let songs = Playlist::get_songs(playlists.first()); - - Self { - mode: Mode::Playlist, - playlist: Index::new(playlists, Some(0)), - songs: Index::new(songs, Some(0)), - songs_to_add: Vec::new(), - changed: false, - search: String::new(), - search_result: String::from("Enter a playlist name..."), - } - } - fn get_songs(playlist: Option<&String>) -> Vec<Item> { - if let Some(playlist) = playlist { - let (row_ids, song_ids) = sqlite::playlist::get(playlist); - let songs = sqlite::get_songs(&song_ids); - songs - .into_iter() - .zip(row_ids) - .map(|(song, row)| Item { row, song }) - .collect() - } else { - Vec::new() - } - } - pub fn up(&mut self) { - match self.mode { - Mode::Playlist => { - self.playlist.up(); - self.update_songs(); - } - Mode::Song => self.songs.up(), - Mode::Popup => (), - } - } - pub fn down(&mut self) { - match self.mode { - Mode::Playlist => { - self.playlist.down(); - self.update_songs(); - } - Mode::Song => self.songs.down(), - Mode::Popup => (), - } - } - fn update_songs(&mut self) { - //Update the list of songs. - let songs = Playlist::get_songs(self.playlist.selected()); - self.songs = if !songs.is_empty() { - Index::new(songs, self.songs.index()) - } else { - self.mode = Mode::Playlist; - Index::default() - }; - } - fn update_playlists(&mut self) { - self.playlist = Index::new(sqlite::playlist::get_names(), self.playlist.index()); - } - pub fn on_enter(&mut self, player: &mut Player) { - match self.mode { - Mode::Playlist => { - let songs: Vec<Song> = self - .songs - .data - .iter() - .map(|item| item.song.clone()) - .collect(); - - player.add_songs(&songs); - } - Mode::Song => { - if let Some(item) = self.songs.selected() { - player.add_songs(&[item.song.clone()]); - } - } - Mode::Popup if !self.songs_to_add.is_empty() => { - //Select an existing playlist or create a new one. - let name = self.search.trim().to_string(); - - let ids: Vec<usize> = self - .songs_to_add - .iter() - .map(|song| song.id.unwrap()) - .collect(); - - sqlite::add_playlist(&name, &ids); - - self.update_playlists(); - - let mut i = Some(0); - for (j, playlist) in self.playlist.data.iter().enumerate() { - if playlist == &name { - i = Some(j); - break; - } - } - //Select the playlist was just modified and update the songs. - self.playlist.select(i); - self.update_songs(); - - //Reset everything. - self.search = String::new(); - self.mode = Mode::Song; - } - _ => (), - } - } - pub fn on_backspace(&mut self, modifiers: KeyModifiers) { - match self.mode { - Mode::Popup => { - self.changed = true; - if modifiers == KeyModifiers::CONTROL { - self.search.clear(); - } else { - self.search.pop(); - } - } - _ => self.left(), - } - } - pub fn left(&mut self) { - match self.mode { - Mode::Song => { - self.mode = Mode::Playlist; - } - Mode::Popup => (), - _ => (), - } - } - pub fn right(&mut self) { - match self.mode { - Mode::Playlist if !self.songs.is_empty() => { - self.mode = Mode::Song; - } - Mode::Popup => (), - _ => (), - } - } - pub fn add_to_playlist(&mut self, songs: &[Song]) { - self.songs_to_add = songs.to_vec(); - self.mode = Mode::Popup; - } - pub fn delete(&mut self) { - match self.mode { - Mode::Playlist => { - if let Some(playlist) = self.playlist.selected() { - //TODO: Prompt the user with yes or no. - sqlite::playlist::remove(&playlist); - - let index = self.playlist.index().unwrap(); - self.playlist.remove(index); - self.update_songs(); - } - } - Mode::Song => { - if let Some(song) = self.songs.selected() { - sqlite::playlist::remove_id(song.row); - let index = self.songs.index().unwrap(); - self.songs.remove(index); - if self.songs.is_empty() { - self.update_playlists(); - } - } - } - Mode::Popup => return, - } - } - pub fn on_key(&mut self, c: char) { - self.changed = true; - self.search.push(c); - } - pub fn input_mode(&self) -> bool { - self.mode == Mode::Popup - } - pub fn on_escape(&mut self, mode: &mut super::Mode) { - match self.mode { - Mode::Popup => { - self.mode = Mode::Playlist; - self.search = String::new(); - self.changed = true; - } - _ => *mode = super::Mode::Browser, - }; - } -} - -impl Playlist { - //TODO: I think I want a different popup. - //It should be a small side bar in the browser. - //There should be a list of existing playlists. - //The first playlist will be the one you just added to - //so it's fast to keep adding things - //The last item will be add a new playlist. - //If there are no playlists it will prompt you to create on. - //This should be similar to foobar on android. - - //TODO: Renaming - //Move items around in lists - //There should be a hotkey to add to most recent playlist - //And a message should show up in the bottom bar saying - //"[name] has been has been added to [playlist name]" - //or - //"25 songs have been added to [playlist name]" - - //TODO: Add keybindings to readme - pub fn draw_popup(&mut self, f: &mut Frame) { - if let Some(area) = centered_rect(45, 6, f.size()) { - let v = Layout::default() - .direction(Direction::Vertical) - .constraints([Constraint::Length(3), Constraint::Percentage(50)]) - .margin(1) - .split(area); - - f.render_widget(Clear, area); - f.render_widget( - Block::default() - .title("─Add to playlist") - .borders(Borders::ALL) - .border_type(BorderType::Rounded), - area, - ); - - //Scroll the playlist name. - let len = self.search.len() as u16; - let width = v[0].width.saturating_sub(1); - let offset_x = if len < width { 0 } else { len - width + 1 }; - - f.render_widget( - Paragraph::new(self.search.as_str()) - .block( - Block::default() - .borders(Borders::ALL) - .border_type(BorderType::Rounded), - ) - .scroll((0, offset_x)), - v[0], - ); - - if self.changed { - self.changed = false; - let eq = self.playlist.data.iter().any(|e| e == &self.search); - self.search_result = if eq { - format!("Add to existing playlist: {}", self.search) - } else if self.search.is_empty() { - String::from("Enter a playlist name...") - } else { - format!("Add to new playlist: {}", self.search) - } - } - - f.render_widget( - Paragraph::new(self.search_result.as_str()), - v[1].inner(&Margin { - horizontal: 1, - vertical: 0, - }), - ); - - //Draw the cursor. - let (x, y) = (v[0].x + 1, v[0].y + 1); - if self.search.is_empty() { - f.set_cursor(x, y); - } else { - let width = v[0].width.saturating_sub(3); - if len < width { - f.set_cursor(x + len, y) - } else { - f.set_cursor(x + width, y) - } - } - } - } - pub fn draw(&mut self, area: Rect, f: &mut Frame) { - let horizontal = Layout::default() - .direction(Direction::Horizontal) - .constraints([Constraint::Percentage(30), Constraint::Percentage(70)]) - .split(area); - - let items: Vec<ListItem> = self - .playlist - .clone() - .into_iter() - .map(|str| ListItem::new(str)) - .collect(); - - let list = List::new(items) - .block( - Block::default() - .title("─Playlist") - .borders(Borders::ALL) - .border_type(BorderType::Rounded), - ) - .highlight_symbol(">"); - - let list = if let Mode::Playlist = self.mode { - list.highlight_symbol(">") - } else { - list.highlight_symbol("") - }; - - f.render_stateful_widget( - list, - horizontal[0], - &mut ListState::new(self.playlist.index()), - ); - - let content = self - .songs - .data - .iter() - .map(|item| { - let song = item.song.clone(); - Row::new(vec![ - Span::styled(song.name, Style::default().fg(COLORS.name)), - Span::styled(song.album, Style::default().fg(COLORS.album)), - Span::styled(song.artist, Style::default().fg(COLORS.artist)), - ]) - }) - .collect(); - - let table = Table::new(content) - .widths(&[ - Constraint::Percentage(42), - Constraint::Percentage(30), - Constraint::Percentage(28), - ]) - .block( - Block::default() - .title("─Songs") - .borders(Borders::ALL) - .border_type(BorderType::Rounded), - ); - - let table = if let Mode::Song = self.mode { - table.highlight_symbol(">") - } else { - table.highlight_symbol("") - }; - - f.render_stateful_widget( - table, - horizontal[1], - &mut TableState::new(self.songs.index()), - ); - - if let Mode::Popup = self.mode { - self.draw_popup(f); - } - } -} diff --git a/gonk/src/app/queue.rs b/gonk/src/app/queue.rs deleted file mode 100644 index 275ed68c..00000000 --- a/gonk/src/app/queue.rs +++ /dev/null @@ -1,361 +0,0 @@ -use crate::widgets::{Cell, Gauge, Row, Table, TableState}; -use crate::{Frame, COLORS}; -use crossterm::event::{KeyModifiers, MouseEvent}; -use gonk_player::{Index, Player}; -use tui::layout::{Alignment, Constraint, Direction, Layout, Rect}; -use tui::style::{Color, Modifier, Style}; -use tui::text::{Span, Spans}; -use tui::widgets::{Block, BorderType, Borders, Paragraph}; -use unicode_width::UnicodeWidthStr; - -pub struct Queue { - pub ui: Index<()>, - pub constraint: [u16; 4], - pub player: Player, -} - -impl Queue { - pub fn new(vol: u16, device: String) -> Self { - Self { - ui: Index::default(), - constraint: [8, 42, 24, 26], - player: Player::new(device, vol), - } - } - pub fn update(&mut self) { - if self.ui.index().is_none() && !self.player.is_empty() { - self.ui.select(Some(0)); - } - self.player.update(); - } - pub fn move_constraint(&mut self, row: usize, modifier: KeyModifiers) { - if modifier == KeyModifiers::SHIFT && self.constraint[row] != 0 { - //Move row back. - self.constraint[row + 1] += 1; - self.constraint[row] = self.constraint[row].saturating_sub(1); - } else if self.constraint[row + 1] != 0 { - //Move row forward. - self.constraint[row] += 1; - self.constraint[row + 1] = self.constraint[row + 1].saturating_sub(1); - } - - debug_assert!( - self.constraint.iter().sum::<u16>() == 100, - "Constraint went out of bounds: {:?}", - self.constraint - ); - } - pub fn up(&mut self) { - self.ui.up_with_len(self.player.total_songs()); - } - pub fn down(&mut self) { - self.ui.down_with_len(self.player.total_songs()); - } - pub fn clear(&mut self) { - self.player.clear(); - self.ui.select(Some(0)); - } - pub fn clear_except_playing(&mut self) { - self.player.clear_except_playing(); - self.ui.select(Some(0)); - } - pub fn delete(&mut self) { - if let Some(i) = self.ui.index() { - self.player.delete_index(i); - //make sure the ui index is in sync - let len = self.player.total_songs().saturating_sub(1); - if i > len { - self.ui.select(Some(len)); - } - } - } -} - -impl Queue { - pub fn draw(&mut self, f: &mut Frame, event: Option<MouseEvent>) { - let chunks = Layout::default() - .direction(Direction::Vertical) - .constraints([ - Constraint::Length(3), - Constraint::Min(10), - Constraint::Length(3), - ]) - .split(f.size()); - - self.draw_header(f, chunks[0]); - - let row_bounds = self.draw_body(f, chunks[1]); - - self.draw_seeker(f, chunks[2]); - - //Don't handle mouse input when the queue is empty. - if self.player.is_empty() { - return; - } - - //Handle mouse input. - if let Some(event) = event { - let (x, y) = (event.column, event.row); - const HEADER_HEIGHT: u16 = 5; - - let size = f.size(); - - //Mouse support for the seek bar. - if (size.height - 3 == y || size.height - 2 == y || size.height - 1 == y) - && size.height > 15 - { - let ratio = x as f32 / size.width as f32; - let duration = self.player.duration().as_secs_f32(); - let new_time = duration * ratio; - self.player.seek_to(new_time); - } - - //Mouse support for the queue. - if let Some((start, _)) = row_bounds { - //Check if you clicked on the header. - if y >= HEADER_HEIGHT { - let index = (y - HEADER_HEIGHT) as usize + start; - - //Make sure you didn't click on the seek bar - //and that the song index exists. - if index < self.player.total_songs() - && ((size.height < 15 && y < size.height.saturating_sub(1)) - || y < size.height.saturating_sub(3)) - { - self.ui.select(Some(index)); - } - } - } - } - } - fn draw_header(&mut self, f: &mut Frame, area: Rect) { - f.render_widget( - Block::default() - .borders(Borders::TOP | Borders::LEFT | Borders::RIGHT) - .border_type(BorderType::Rounded), - area, - ); - - let state = if self.player.is_empty() { - String::from("╭─Stopped") - } else if self.player.is_playing() { - String::from("╭─Playing") - } else { - String::from("╭─Paused") - }; - - f.render_widget(Paragraph::new(state).alignment(Alignment::Left), area); - - if !self.player.is_empty() { - self.draw_title(f, area); - } - - let volume = Spans::from(format!("Vol: {}%─╮", self.player.get_volume())); - f.render_widget(Paragraph::new(volume).alignment(Alignment::Right), area); - } - fn draw_title(&mut self, f: &mut Frame, area: Rect) { - let title = if let Some(song) = self.player.selected_song() { - let mut name = song.name.trim_end().to_string(); - let mut album = song.album.trim_end().to_string(); - let mut artist = song.artist.trim_end().to_string(); - let max_width = area.width.saturating_sub(30) as usize; - - while artist.width() + name.width() + "-| - |-".width() > max_width { - if artist.width() > name.width() { - artist.pop(); - } else { - name.pop(); - } - } - - while album.width() > max_width { - album.pop(); - } - - let n = album - .width() - .saturating_sub(artist.width() + name.width() + 3); - let rem = n % 2; - let pad_front = " ".repeat(n / 2); - let pad_back = " ".repeat(n / 2 + rem); - - vec![ - Spans::from(vec![ - Span::raw(format!("─│ {}", pad_front)), - Span::styled(artist, Style::default().fg(COLORS.artist)), - Span::raw(" ─ "), - Span::styled(name, Style::default().fg(COLORS.name)), - Span::raw(format!("{} │─", pad_back)), - ]), - Spans::from(Span::styled(album, Style::default().fg(COLORS.album))), - ] - } else { - Vec::new() - }; - - f.render_widget(Paragraph::new(title).alignment(Alignment::Center), area); - } - fn draw_body(&mut self, f: &mut Frame, area: Rect) -> Option<(usize, usize)> { - if self.player.is_empty() { - f.render_widget( - Block::default() - .border_type(BorderType::Rounded) - .borders(Borders::LEFT | Borders::RIGHT), - area, - ); - return None; - } - - let songs = self.player.get_index(); - let (songs, player_index, ui_index) = (&songs.data, songs.index(), self.ui.index()); - - let mut items: Vec<Row> = songs - .iter() - .map(|song| { - Row::new(vec![ - Cell::from(""), - Cell::from(song.number.to_string()).style(Style::default().fg(COLORS.number)), - Cell::from(song.name.as_str()).style(Style::default().fg(COLORS.name)), - Cell::from(song.album.as_str()).style(Style::default().fg(COLORS.album)), - Cell::from(song.artist.as_str()).style(Style::default().fg(COLORS.artist)), - ]) - }) - .collect(); - - if let Some(player_index) = player_index { - if let Some(song) = songs.get(player_index) { - if let Some(ui_index) = ui_index { - //Currently playing song - let row = if ui_index == player_index { - Row::new(vec![ - Cell::from(">>").style( - Style::default() - .fg(Color::White) - .add_modifier(Modifier::DIM | Modifier::BOLD), - ), - Cell::from(song.number.to_string()) - .style(Style::default().bg(COLORS.number).fg(Color::Black)), - Cell::from(song.name.as_str()) - .style(Style::default().bg(COLORS.name).fg(Color::Black)), - Cell::from(song.album.as_str()) - .style(Style::default().bg(COLORS.album).fg(Color::Black)), - Cell::from(song.artist.as_str()) - .style(Style::default().bg(COLORS.artist).fg(Color::Black)), - ]) - } else { - Row::new(vec![ - Cell::from(">>").style( - Style::default() - .fg(Color::White) - .add_modifier(Modifier::DIM | Modifier::BOLD), - ), - Cell::from(song.number.to_string()) - .style(Style::default().fg(COLORS.number)), - Cell::from(song.name.as_str()).style(Style::default().fg(COLORS.name)), - Cell::from(song.album.as_str()) - .style(Style::default().fg(COLORS.album)), - Cell::from(song.artist.as_str()) - .style(Style::default().fg(COLORS.artist)), - ]) - }; - - items.remove(player_index); - items.insert(player_index, row); - - //Current selection - if ui_index != player_index { - if let Some(song) = songs.get(ui_index) { - let row = Row::new(vec![ - Cell::default(), - Cell::from(song.number.to_string()) - .style(Style::default().bg(COLORS.number)), - Cell::from(song.name.as_str()) - .style(Style::default().bg(COLORS.name)), - Cell::from(song.album.as_str()) - .style(Style::default().bg(COLORS.album)), - Cell::from(song.artist.as_str()) - .style(Style::default().bg(COLORS.artist)), - ]) - .style(Style::default().fg(Color::Black)); - items.remove(ui_index); - items.insert(ui_index, row); - } - } - } - } - } - - let con = [ - Constraint::Length(2), - Constraint::Percentage(self.constraint[0]), - Constraint::Percentage(self.constraint[1]), - Constraint::Percentage(self.constraint[2]), - Constraint::Percentage(self.constraint[3]), - ]; - - let t = Table::new(items) - .header( - Row::new(["", "Track", "Title", "Album", "Artist"]) - .style( - Style::default() - .fg(Color::White) - .add_modifier(Modifier::BOLD), - ) - .bottom_margin(1), - ) - .block( - Block::default() - .borders(Borders::LEFT | Borders::RIGHT | Borders::BOTTOM) - .border_type(BorderType::Rounded), - ) - // .separator() - .widths(&con); - - let row_bounds = t.get_row_bounds(ui_index, t.get_row_height(area)); - - f.render_stateful_widget(t, area, &mut TableState::new(ui_index)); - - Some(row_bounds) - } - fn draw_seeker(&mut self, f: &mut Frame, area: Rect) { - if self.player.is_empty() { - return f.render_widget( - Block::default() - .border_type(BorderType::Rounded) - .borders(Borders::BOTTOM | Borders::LEFT | Borders::RIGHT), - area, - ); - } - - let elapsed = self.player.elapsed().as_secs_f32(); - let duration = self.player.duration().as_secs_f32(); - - let seeker = format!( - "{:02}:{:02}/{:02}:{:02}", - (elapsed / 60.0).floor(), - elapsed.trunc() as u32 % 60, - (duration / 60.0).floor(), - duration.trunc() as u32 % 60, - ); - - let ratio = elapsed / duration; - let ratio = if ratio.is_nan() { - 0.0 - } else { - ratio.clamp(0.0, 1.0) - }; - - f.render_widget( - Gauge::default() - .block( - Block::default() - .borders(Borders::ALL) - .border_type(BorderType::Rounded), - ) - .gauge_style(Style::default().fg(COLORS.seeker)) - .ratio(ratio as f64) - .label(seeker), - area, - ); - } -} diff --git a/gonk/src/app/search.rs b/gonk/src/app/search.rs deleted file mode 100644 index 81f4a88b..00000000 --- a/gonk/src/app/search.rs +++ /dev/null @@ -1,478 +0,0 @@ -use super::Mode as AppMode; -use crate::widgets::*; -use crate::*; -use crossterm::event::KeyModifiers; -use gonk_player::{Index, Player}; -use rayon::iter::{IntoParallelRefIterator, ParallelIterator}; -use std::cmp::Ordering; -use tui::{ - layout::{Alignment, Constraint, Direction, Layout, Rect}, - style::{Modifier, Style}, - text::{Span, Spans}, - widgets::{Block, BorderType, Borders, Paragraph}, -}; - -#[derive(Clone)] -pub enum Item { - Song(Song), - Album(Album), - Artist(Artist), -} - -#[derive(Clone, Default)] -pub struct Song { - pub id: usize, - pub name: String, - pub album: String, - pub artist: String, -} - -#[derive(Clone, Default)] -pub struct Album { - pub name: String, - pub artist: String, -} - -#[derive(Clone, Default)] -pub struct Artist { - pub name: String, -} - -#[derive(PartialEq, Eq)] -pub enum Mode { - Search, - Select, -} - -pub struct Search { - query: String, - query_changed: bool, - mode: Mode, - results: Index<Item>, - cache: Vec<Item>, -} - -impl Search { - pub fn new() -> Self { - Self { - cache: Vec::new(), - query: String::new(), - query_changed: false, - mode: Mode::Search, - results: Index::default(), - } - } - pub fn init(mut self) -> Self { - self.update(); - self - } - pub fn update(&mut self) { - self.update_cache(); - self.update_search(); - } - fn update_cache(&mut self) { - self.cache = Vec::new(); - - for song in sqlite::get_all_songs() { - self.cache.push(Item::Song(Song { - name: song.name, - album: song.album, - artist: song.artist, - id: song.id.unwrap(), - })); - } - - for (name, artist) in sqlite::get_all_albums() { - self.cache.push(Item::Album(Album { name, artist })); - } - - for name in sqlite::get_all_artists() { - self.cache.push(Item::Artist(Artist { name })); - } - } - fn update_search(&mut self) { - let query = &self.query.to_lowercase(); - - let mut results: Vec<_> = if query.is_empty() { - //If there user has not asked to search anything - //populate the list with 40 results. - self.cache - .iter() - .take(40) - .rev() - .map(|item| { - let acc = match item { - Item::Song(song) => strsim::jaro_winkler(query, &song.name.to_lowercase()), - Item::Album(album) => { - strsim::jaro_winkler(query, &album.name.to_lowercase()) - } - Item::Artist(artist) => { - strsim::jaro_winkler(query, &artist.name.to_lowercase()) - } - }; - - (item, acc) - }) - .collect() - } else { - self.cache - .par_iter() - .filter_map(|item| { - //I don't know if 'to_lowercase' has any overhead. - let acc = match item { - Item::Song(song) => strsim::jaro_winkler(query, &song.name.to_lowercase()), - Item::Album(album) => { - strsim::jaro_winkler(query, &album.name.to_lowercase()) - } - Item::Artist(artist) => { - strsim::jaro_winkler(query, &artist.name.to_lowercase()) - } - }; - - //Filter out results that are poor matches. 0.75 is an arbitrary value. - if acc > 0.75 { - Some((item, acc)) - } else { - None - } - }) - .collect() - }; - - //Sort results by score. - results.sort_by(|(_, a), (_, b)| b.partial_cmp(a).unwrap()); - - //Sort artists above self-titled albums. - results.sort_by(|(item, a), (_, b)| { - //If the score is the same - if a == b { - //And the item is an album - if let Item::Album(_) = item { - //Move item lower in the list. - Ordering::Greater - } else { - //Move item higher in the list. - Ordering::Less - } - } else { - //Keep the same order. - Ordering::Equal - } - }); - - self.results.data = results.into_iter().map(|(item, _)| item.clone()).collect(); - } - pub fn on_key(&mut self, c: char) { - self.query_changed = true; - self.query.push(c); - } - pub fn up(&mut self) { - self.results.up(); - } - pub fn down(&mut self) { - self.results.down(); - } - pub fn on_backspace(&mut self, modifiers: KeyModifiers) { - match self.mode { - Mode::Search => { - if modifiers == KeyModifiers::CONTROL { - self.query.clear(); - } else { - self.query.pop(); - } - } - Mode::Select => { - self.results.select(None); - self.mode = Mode::Search; - } - } - } - pub fn on_escape(&mut self, mode: &mut AppMode) { - match self.mode { - Mode::Search => { - if let Mode::Search = self.mode { - self.query.clear(); - *mode = AppMode::Queue; - } - } - Mode::Select => { - self.mode = Mode::Search; - self.results.select(None); - } - } - } - pub fn on_enter(&mut self, player: &mut Player) { - match self.mode { - Mode::Search => { - if !self.results.is_empty() { - self.mode = Mode::Select; - self.results.select(Some(0)); - } - } - Mode::Select => { - if let Some(item) = self.results.selected() { - let songs = match item { - Item::Song(song) => sqlite::get_songs(&[song.id]), - Item::Album(album) => { - sqlite::get_all_songs_from_album(&album.name, &album.artist) - } - Item::Artist(artist) => sqlite::get_songs_by_artist(&artist.name), - }; - - player.add_songs(&songs); - } - } - } - } - pub fn input_mode(&self) -> bool { - self.mode == Mode::Search - } -} - -impl Search { - pub fn draw(&mut self, area: Rect, f: &mut Frame) { - if self.query_changed { - self.update_search(); - } - - let v = Layout::default() - .direction(Direction::Vertical) - .constraints([ - Constraint::Length(3), - Constraint::Percentage(30), - Constraint::Percentage(60), - ]) - .split(area); - - let h = Layout::default() - .direction(Direction::Horizontal) - .constraints([Constraint::Percentage(35), Constraint::Percentage(65)]) - .split(v[1]); - - self.draw_textbox(f, v[0]); - - let item = if self.results.selected().is_some() { - self.results.selected() - } else { - self.results.data.first() - }; - - if let Some(item) = item { - match item { - Item::Song(song) => { - Search::song(f, &song.name, &song.album, &song.artist, h[0]); - self.album(f, &song.album, &song.artist, h[1]); - } - Item::Album(album) => { - self.album(f, &album.name, &album.artist, h[0]); - self.artist(f, &album.artist, h[1]); - } - Item::Artist(artist) => { - let albums = sqlite::get_all_albums_by_artist(&artist.name); - - self.artist(f, &artist.name, h[0]); - - let h_split = Layout::default() - .direction(Direction::Horizontal) - .constraints([Constraint::Percentage(50), Constraint::Percentage(50)]) - .split(h[1]); - - //draw the first two albums - for (i, area) in h_split.iter().enumerate() { - if let Some(album) = albums.get(i) { - self.album(f, album, &artist.name, *area); - } - } - } - } - self.draw_results(f, v[2]); - } else { - self.draw_results(f, v[1].union(v[2])); - } - - self.update_cursor(f); - } - fn song(f: &mut Frame, name: &str, album: &str, artist: &str, area: Rect) { - let song_table = Table::new(vec![ - Row::new(vec![Spans::from(Span::raw(album))]), - Row::new(vec![Spans::from(Span::raw(artist))]), - ]) - .header( - Row::new(vec![Span::styled( - format!("{} ", name), - Style::default().add_modifier(Modifier::ITALIC), - )]) - .bottom_margin(1), - ) - .block( - Block::default() - .borders(Borders::ALL) - .border_type(BorderType::Rounded) - .title("Song"), - ) - .widths(&[Constraint::Percentage(100)]); - - f.render_widget(song_table, area); - } - fn album(&self, f: &mut Frame, album: &str, artist: &str, area: Rect) { - let cells: Vec<_> = sqlite::get_all_songs_from_album(album, artist) - .iter() - .map(|song| Row::new(vec![Cell::from(format!("{}. {}", song.number, song.name))])) - .collect(); - - let table = Table::new(cells) - .header( - Row::new(vec![Cell::from(Span::styled( - format!("{} ", album), - Style::default().add_modifier(Modifier::ITALIC), - ))]) - .bottom_margin(1), - ) - .block( - Block::default() - .borders(Borders::ALL) - .border_type(BorderType::Rounded) - .title("Album"), - ) - .widths(&[Constraint::Percentage(100)]); - - f.render_widget(table, area); - } - fn artist(&self, f: &mut Frame, artist: &str, area: Rect) { - let albums = sqlite::get_all_albums_by_artist(artist); - let cells: Vec<_> = albums - .iter() - .map(|album| Row::new(vec![Cell::from(Span::raw(album))])) - .collect(); - - let table = Table::new(cells) - .header( - Row::new(vec![Cell::from(Span::styled( - format!("{} ", artist), - Style::default().add_modifier(Modifier::ITALIC), - ))]) - .bottom_margin(1), - ) - .block( - Block::default() - .borders(Borders::ALL) - .border_type(BorderType::Rounded) - .title("Artist"), - ) - .widths(&[Constraint::Percentage(100)]); - - f.render_widget(table, area); - } - fn draw_results(&self, f: &mut Frame, area: Rect) { - let get_cell = |item: &Item, selected: bool| -> Row { - let selected_cell = if selected { - Cell::from(">") - } else { - Cell::default() - }; - - match item { - Item::Song(song) => { - let song = sqlite::get_songs(&[song.id])[0].clone(); - Row::new(vec![ - selected_cell, - Cell::from(song.name).style(Style::default().fg(COLORS.name)), - Cell::from(song.album).style(Style::default().fg(COLORS.album)), - Cell::from(song.artist).style(Style::default().fg(COLORS.artist)), - ]) - } - Item::Album(album) => Row::new(vec![ - selected_cell, - Cell::from(format!("{} - Album", album.name)) - .style(Style::default().fg(COLORS.name)), - Cell::from("").style(Style::default().fg(COLORS.album)), - Cell::from(album.artist.clone()).style(Style::default().fg(COLORS.artist)), - ]), - Item::Artist(artist) => Row::new(vec![ - selected_cell, - Cell::from(format!("{} - Artist", artist.name)) - .style(Style::default().fg(COLORS.name)), - Cell::from("").style(Style::default().fg(COLORS.album)), - Cell::from("").style(Style::default().fg(COLORS.artist)), - ]), - } - }; - - let rows: Vec<_> = self - .results - .data - .iter() - .enumerate() - .map(|(i, item)| { - if let Some(s) = self.results.index() { - if s == i { - return get_cell(item, true); - } - } else if i == 0 { - return get_cell(item, false); - } - get_cell(item, false) - }) - .collect(); - - let italic = Style::default().add_modifier(Modifier::ITALIC); - let table = Table::new(rows) - .header( - Row::new(vec![ - Cell::default(), - Cell::from("Name").style(italic), - Cell::from("Album").style(italic), - Cell::from("Artist").style(italic), - ]) - .bottom_margin(1), - ) - .block( - Block::default() - .borders(Borders::ALL) - .border_type(BorderType::Rounded), - ) - .widths(&[ - Constraint::Length(1), - Constraint::Percentage(40), - Constraint::Percentage(40), - Constraint::Percentage(20), - ]); - - f.render_stateful_widget(table, area, &mut TableState::new(self.results.index())); - } - fn draw_textbox(&self, f: &mut Frame, area: Rect) { - let len = self.query.len() as u16; - //Search box is a little smaller than the max width - let width = area.width.saturating_sub(1); - let offset_x = if len < width { 0 } else { len - width + 1 }; - - f.render_widget( - Paragraph::new(self.query.as_str()) - .block( - Block::default() - .borders(Borders::ALL) - .border_type(BorderType::Rounded), - ) - .alignment(Alignment::Left) - .scroll((0, offset_x)), - area, - ); - } - fn update_cursor(&self, f: &mut Frame) { - let area = f.size(); - //Move the cursor position when typing - if let Mode::Search = self.mode { - if self.query.is_empty() { - f.set_cursor(1, 1); - } else { - let len = self.query.len() as u16; - let max_width = area.width.saturating_sub(2); - if len >= max_width { - f.set_cursor(max_width, 1); - } else { - f.set_cursor(len + 1, 1); - } - } - } - } -} diff --git a/gonk/src/app/status_bar.rs b/gonk/src/app/status_bar.rs deleted file mode 100644 index f24f0776..00000000 --- a/gonk/src/app/status_bar.rs +++ /dev/null @@ -1,155 +0,0 @@ -use super::queue::Queue; -use crate::sqlite; -use crate::Frame; -use crate::COLORS; -use std::time::{Duration, Instant}; -use tui::{ - layout::{Alignment, Constraint, Direction, Layout, Rect}, - style::Style, - text::{Span, Spans}, - widgets::{Block, BorderType, Borders, Paragraph}, -}; - -const WAIT_TIME: Duration = Duration::from_secs(2); - -pub struct StatusBar { - dots: usize, - busy: bool, - scan_message: String, - wait_timer: Option<Instant>, - scan_timer: Option<Instant>, - hidden: bool, -} - -impl StatusBar { - pub fn new() -> Self { - Self { - dots: 1, - busy: false, - scan_message: String::new(), - wait_timer: None, - scan_timer: None, - hidden: true, - } - } - - //Updates the dots in "Scanning for files .." - pub fn update(&mut self, db_busy: bool, queue: &Queue) { - if db_busy { - if self.dots < 3 { - self.dots += 1; - } else { - self.dots = 1; - } - } else { - self.dots = 1; - } - - if let Some(timer) = self.wait_timer { - if timer.elapsed() >= WAIT_TIME { - self.wait_timer = None; - self.busy = false; - - //FIXME: If the queue was not empty - //and the status bar was hidden - //before triggering an update - //the status bar will stay open - //without the users permission. - if queue.player.is_empty() { - self.hidden = true; - } - } - } - } - - pub fn toggle_hidden(&mut self) { - self.hidden = !self.hidden; - } - - pub fn is_hidden(&self) -> bool { - self.hidden - } -} -impl StatusBar { - pub fn draw(&mut self, area: Rect, f: &mut Frame, busy: bool, queue: &Queue) { - if busy { - //If database is busy but status_bar is not - //set the status bar to busy - if !self.busy { - self.busy = true; - self.hidden = false; - self.scan_timer = Some(Instant::now()); - } - } else if self.busy { - //If database is no-longer busy - //but status bar is. Print the duration - //and start the wait timer. - if let Some(scan_time) = self.scan_timer { - self.busy = false; - self.wait_timer = Some(Instant::now()); - self.scan_timer = None; - self.scan_message = format!( - "Finished adding {} files in {:.2} seconds.", - sqlite::total_songs(), - scan_time.elapsed().as_secs_f32(), - ); - } - } - - if self.hidden { - return; - } - - let text = if busy { - Spans::from(format!("Scannig for files{}", ".".repeat(self.dots))) - } else if self.wait_timer.is_some() { - Spans::from(self.scan_message.as_str()) - } else { - if let Some(song) = queue.player.selected_song() { - Spans::from(vec![ - Span::raw(" "), - Span::styled(song.number.to_string(), Style::default().fg(COLORS.number)), - Span::raw(" | "), - Span::styled(song.name.as_str(), Style::default().fg(COLORS.name)), - Span::raw(" | "), - Span::styled(song.album.as_str(), Style::default().fg(COLORS.album)), - Span::raw(" | "), - Span::styled(song.artist.as_str(), Style::default().fg(COLORS.artist)), - ]) - } else { - Spans::default() - } - }; - - let area = Layout::default() - .direction(Direction::Horizontal) - .constraints([Constraint::Percentage(85), Constraint::Percentage(15)]) - .split(area); - - f.render_widget( - Paragraph::new(text).alignment(Alignment::Left).block( - Block::default() - .borders(Borders::TOP | Borders::LEFT | Borders::BOTTOM) - .border_type(BorderType::Rounded), - ), - area[0], - ); - - //TODO: Draw mini progress bar here. - - let text = if queue.player.is_playing() { - format!("Vol: {}% ", queue.player.get_volume()) - } else { - String::from("Paused ") - }; - - f.render_widget( - Paragraph::new(text).alignment(Alignment::Right).block( - Block::default() - .borders(Borders::TOP | Borders::RIGHT | Borders::BOTTOM) - .border_type(BorderType::Rounded), - ), - area[1], - ); - } -} diff --git a/gonk/src/browser.rs b/gonk/src/browser.rs new file mode 100644 index 00000000..a3180245 --- /dev/null +++ b/gonk/src/browser.rs @@ -0,0 +1,220 @@ +use crate::widgets::{List, ListItem, ListState}; +use crate::{Frame, Input}; +use gonk_database::query; +use gonk_player::{Index, Song}; +use tui::{ + layout::{Constraint, Direction, Layout, Rect}, + style::{Color, Style}, + widgets::{Block, BorderType, Borders}, +}; + +#[derive(PartialEq, Eq)] +pub enum Mode { + Artist, + Album, + Song, +} + +pub struct BrowserSong { + name: String, + id: usize, +} + +pub struct Browser { + artists: Index<String>, + albums: Index<String>, + songs: Index<BrowserSong>, + pub mode: Mode, +} + +impl Browser { + pub fn new() -> Self { + let artists = Index::new(query::artists(), Some(0)); + + let (albums, songs) = if let Some(first_artist) = artists.selected() { + let albums = Index::new(query::albums_by_artist(first_artist), Some(0)); + + if let Some(first_album) = albums.selected() { + let songs = query::songs_from_album(first_album, first_artist) + .into_iter() + .map(|song| BrowserSong { + name: format!("{}. {}", song.number, song.name), + id: song.id.unwrap(), + }) + .collect(); + (albums, Index::new(songs, Some(0))) + } else { + (albums, Index::default()) + } + } else { + (Index::default(), Index::default()) + }; + + Self { + artists, + albums, + songs, + mode: Mode::Artist, + } + } +} + +impl Input for Browser { + fn up(&mut self) { + match self.mode { + Mode::Artist => self.artists.up(), + Mode::Album => self.albums.up(), + Mode::Song => self.songs.up(), + } + update_browser(self); + } + + fn down(&mut self) { + match self.mode { + Mode::Artist => self.artists.down(), + Mode::Album => self.albums.down(), + Mode::Song => self.songs.down(), + } + update_browser(self); + } + + fn left(&mut self) { + match self.mode { + Mode::Artist => (), + Mode::Album => self.mode = Mode::Artist, + Mode::Song => self.mode = Mode::Album, + } + } + + fn right(&mut self) { + match self.mode { + Mode::Artist => self.mode = Mode::Album, + Mode::Album => self.mode = Mode::Song, + Mode::Song => (), + } + } +} + +pub fn refresh(browser: &mut Browser) { + browser.mode = Mode::Artist; + + browser.artists = Index::new(query::artists(), Some(0)); + browser.albums = Index::default(); + browser.songs = Index::default(); + + update_albums(browser); +} + +pub fn update_browser(browser: &mut Browser) { + match browser.mode { + Mode::Artist => update_albums(browser), + Mode::Album => update_songs(browser), + Mode::Song => (), + } +} + +pub fn update_albums(browser: &mut Browser) { + //Update the album based on artist selection + if let Some(artist) = browser.artists.selected() { + browser.albums = Index::new(query::albums_by_artist(artist), Some(0)); + update_songs(browser); + } +} + +pub fn update_songs(browser: &mut Browser) { + if let Some(artist) = browser.artists.selected() { + if let Some(album) = browser.albums.selected() { + let songs = query::songs_from_album(album, artist) + .into_iter() + .map(|song| BrowserSong { + name: format!("{}. {}", song.number, song.name), + id: song.id.unwrap(), + }) + .collect(); + browser.songs = Index::new(songs, Some(0)); + } + } +} + +pub fn get_selected(browser: &Browser) -> Vec<Song> { + if let Some(artist) = browser.artists.selected() { + if let Some(album) = browser.albums.selected() { + if let Some(song) = browser.songs.selected() { + return match browser.mode { + Mode::Artist => query::songs_by_artist(artist), + Mode::Album => query::songs_from_album(album, artist), + Mode::Song => query::songs_from_ids(&[song.id]), + }; + } + } + } + Vec::new() +} + +pub fn draw(browser: &Browser, area: Rect, f: &mut Frame) { + let size = area.width / 3; + let rem = area.width % 3; + + let chunks = Layout::default() + .direction(Direction::Horizontal) + .constraints([ + Constraint::Length(size), + Constraint::Length(size), + Constraint::Length(size + rem), + ]) + .split(area); + + let a: Vec<ListItem> = browser + .artists + .data + .iter() + .map(|name| ListItem::new(name.as_str())) + .collect(); + + let b: Vec<ListItem> = browser + .albums + .data + .iter() + .map(|name| ListItem::new(name.as_str())) + .collect(); + + let c: Vec<ListItem> = browser + .songs + .data + .iter() + .map(|song| ListItem::new(song.name.as_str())) + .collect(); + + let artists = list("─Aritst", &a, browser.mode == Mode::Artist); + let albums = list("─Album", &b, browser.mode == Mode::Album); + let songs = list("─Song", &c, browser.mode == Mode::Song); + + f.render_stateful_widget( + artists, + chunks[0], + &mut ListState::new(browser.artists.index()), + ); + f.render_stateful_widget( + albums, + chunks[1], + &mut ListState::new(browser.albums.index()), + ); + f.render_stateful_widget(songs, chunks[2], &mut ListState::new(browser.songs.index())); +} + +fn list<'a>(title: &'static str, content: &'a [ListItem], use_symbol: bool) -> List<'a> { + let list = List::new(content) + .block( + Block::default() + .title(title) + .borders(Borders::ALL) + .border_type(BorderType::Rounded), + ) + .style(Style::default().fg(Color::White)); + + if use_symbol { + list.highlight_symbol(">") + } else { + list.highlight_symbol("") + } +} diff --git a/gonk/src/main.rs b/gonk/src/main.rs index b540d637..fc5ebd6a 100644 --- a/gonk/src/main.rs +++ b/gonk/src/main.rs @@ -1,42 +1,375 @@ -use crate::toml::{Colors, Toml}; -use app::App; -use static_init::dynamic; +use browser::Browser; +use crossterm::{event::*, terminal::*, *}; +use gonk_database::{query, Database, State}; +use gonk_player::Player; +use playlist::{Mode as PlaylistMode, Playlist}; +use queue::Queue; +use search::{Mode as SearchMode, Search}; +use settings::Settings; +use status_bar::StatusBar; use std::{ - io::{Result, Stdout}, - path::PathBuf, + io::{stdout, Stdout}, + path::Path, + sync::mpsc, + time::{Duration, Instant}, }; -use tui::backend::CrosstermBackend; +use tui::{backend::CrosstermBackend, layout::*, style::Color, Terminal}; -mod app; -mod sqlite; -mod toml; +mod browser; +mod playlist; +mod queue; +mod search; +mod settings; +mod status_bar; mod widgets; -#[dynamic] -static GONK_DIR: PathBuf = { - let gonk = if cfg!(windows) { - PathBuf::from(&std::env::var("APPDATA").unwrap()) - } else { - PathBuf::from(&std::env::var("HOME").unwrap()).join(".config") - } - .join("gonk"); +type Frame<'a> = tui::Frame<'a, CrosstermBackend<Stdout>>; - if !gonk.exists() { - std::fs::create_dir_all(&gonk).unwrap(); - } - gonk +//TODO: Cleanup colors +pub struct Colors { + pub number: Color, + pub name: Color, + pub album: Color, + pub artist: Color, + pub seeker: Color, +} + +const COLORS: Colors = Colors { + number: Color::Green, + name: Color::Cyan, + album: Color::Magenta, + artist: Color::Blue, + seeker: Color::White, }; -#[dynamic] -static COLORS: Colors = Toml::new().colors; +#[derive(PartialEq, Eq)] +pub enum Mode { + Browser, + Queue, + Search, + Playlist, + Settings, +} -type Frame<'a> = tui::Frame<'a, CrosstermBackend<Stdout>>; +pub trait Input { + fn up(&mut self); + fn down(&mut self); + fn left(&mut self); + fn right(&mut self); +} + +fn init() -> Terminal<CrosstermBackend<Stdout>> { + //Panic handler + let orig_hook = std::panic::take_hook(); + std::panic::set_hook(Box::new(move |panic_info| { + disable_raw_mode().unwrap(); + execute!(stdout(), LeaveAlternateScreen, DisableMouseCapture).unwrap(); + orig_hook(panic_info); + std::process::exit(1); + })); -fn main() -> Result<()> { - sqlite::initialize_database(); + //Terminal + let mut terminal = Terminal::new(CrosstermBackend::new(stdout())).unwrap(); + execute!( + terminal.backend_mut(), + EnterAlternateScreen, + EnableMouseCapture, + ) + .unwrap(); + enable_raw_mode().unwrap(); + terminal.clear().unwrap(); - match App::new() { - Ok(mut app) => app.run(), - Err(err) => Ok(println!("{}", err)), + terminal +} + +fn main() { + let mut db = Database::default(); + let args: Vec<String> = std::env::args().skip(1).collect(); + + if !args.is_empty() { + match args[0].as_str() { + "add" if args.len() > 1 => { + let path = args[1..].join(" "); + //TODO: This might silently scan a directory but not add anything. + //Might be confusing. + if Path::new(&path).exists() { + db.add_path(&path); + } else { + return println!("Invalid path."); + } + } + "rm" if args.len() > 1 => { + let path = args[1..].join(" "); + match query::remove_path(&path) { + Ok(_) => return, + Err(e) => return println!("{e}"), + }; + } + "list" => { + return for path in query::paths() { + println!("{path}"); + }; + } + "reset" => { + return match gonk_database::reset() { + Ok(_) => println!("Files reset!"), + Err(e) => println!("{}", e), + } + } + "help" | "--help" => { + println!("Usage"); + println!(" gonk [<command> <args>]"); + println!(); + println!("Options"); + println!(" add <path> Add music to the library"); + println!(" reset Reset the database"); + return; + } + _ if !args.is_empty() => return println!("Invalid command."), + _ if args.len() > 1 => return println!("Invalid argument."), + _ => (), + } } + + //Player takes a while so off-load it to another thread. + let (s, r) = mpsc::channel(); + + //TODO: figure out why database is crashing + let songs = query::get_cache(); + let volume = query::volume(); + + std::thread::spawn(move || { + let player = Player::new(String::from("device"), volume, &songs); + s.send(player).unwrap(); + }); + + let mut terminal = init(); + + //13ms + let mut search = Search::new(); + let mut settings = Settings::new(); + let mut browser = Browser::new(); + let mut queue = Queue::new(); + let mut status_bar = StatusBar::new(); + let mut playlist = Playlist::new(); + + let mut mode = Mode::Browser; + + let mut busy = false; + let mut last_tick = Instant::now(); + + let mut player = r.recv().unwrap(); + + loop { + if last_tick.elapsed() >= Duration::from_millis(200) { + //Update the status_bar at a constant rate. + status_bar::update(&mut status_bar, busy, &player); + last_tick = Instant::now(); + } + + queue.len = player.songs.len(); + player.update(); + + match db.state() { + State::Busy => busy = true, + State::Idle => busy = false, + State::NeedsUpdate => { + browser::refresh(&mut browser); + search::refresh_cache(&mut search); + search::refresh_results(&mut search); + } + } + + //Draw + terminal + .draw(|f| { + let area = Layout::default() + .direction(Direction::Vertical) + .constraints([Constraint::Min(2), Constraint::Length(3)]) + .split(f.size()); + + let (top, bottom) = if status_bar.hidden { + (f.size(), area[1]) + } else { + (area[0], area[1]) + }; + + match mode { + Mode::Browser => browser::draw(&browser, top, f), + Mode::Queue => queue::draw(&mut queue, &mut player, f, None), + Mode::Search => search::draw(&mut search, top, f), + Mode::Playlist => playlist::draw(&mut playlist, top, f), + Mode::Settings => settings::draw(&mut settings, top, f), + }; + + if mode != Mode::Queue { + status_bar::draw(&mut status_bar, bottom, f, busy, &player); + } + }) + .unwrap(); + + let input_search = search.mode == SearchMode::Search && mode == Mode::Search; + let input_playlist = playlist.mode == PlaylistMode::Popup && mode == Mode::Playlist; + + let input = match mode { + Mode::Browser => &mut browser as &mut dyn Input, + Mode::Queue => &mut queue as &mut dyn Input, + Mode::Search => &mut search as &mut dyn Input, + Mode::Playlist => &mut playlist as &mut dyn Input, + Mode::Settings => &mut settings as &mut dyn Input, + }; + + if event::poll(Duration::from_millis(2)).unwrap() { + match event::read().unwrap() { + Event::Key(event) => { + let shift = event.modifiers == KeyModifiers::SHIFT; + let control = event.modifiers == KeyModifiers::CONTROL; + + match event.code { + KeyCode::Char('c') if control => break, + KeyCode::Char(c) if input_search => { + //Handle ^W as control backspace. + if control && c == 'w' { + search::on_backspace(&mut search, true); + } else { + search.query_changed = true; + search.query.push(c); + } + } + KeyCode::Char(c) if input_playlist => { + if control && c == 'w' { + playlist::on_backspace(&mut playlist, true); + } else { + playlist.changed = true; + playlist.search.push(c); + } + } + KeyCode::Char(' ') => player.toggle_playback(), + KeyCode::Char('c') if shift => { + player.clear_except_playing(); + queue.ui.select(Some(0)); + } + KeyCode::Char('c') => { + player.clear(); + queue.ui.select(Some(0)); + } + KeyCode::Char('x') => match mode { + Mode::Queue => queue::delete(&mut queue, &mut player), + Mode::Playlist => playlist::delete(&mut playlist), + _ => (), + }, + KeyCode::Char('u') if mode == Mode::Browser => db.refresh(), + KeyCode::Char('q') => player.seek_by(-10.0), + KeyCode::Char('e') => player.seek_by(10.0), + KeyCode::Char('a') => player.previous(), + KeyCode::Char('d') => player.next(), + KeyCode::Char('w') => player.volume_up(), + KeyCode::Char('s') => player.volume_down(), + KeyCode::Char('r') => player.randomize(), + //TODO: Rework mode changing buttons + KeyCode::Char('`') => { + status_bar.hidden = !status_bar.hidden; + } + KeyCode::Char(',') => mode = Mode::Playlist, + KeyCode::Char('.') => mode = Mode::Settings, + KeyCode::Char('/') => mode = Mode::Search, + KeyCode::Tab => { + mode = match mode { + Mode::Browser | Mode::Settings | Mode::Search => Mode::Queue, + Mode::Queue | Mode::Playlist => Mode::Browser, + }; + } + KeyCode::Esc => match mode { + Mode::Search => { + search::on_escape(&mut search, &mut mode); + } + Mode::Settings => mode = Mode::Queue, + Mode::Playlist => playlist::on_escape(&mut playlist, &mut mode), + _ => (), + }, + KeyCode::Enter if shift => match mode { + Mode::Browser => { + let songs = browser::get_selected(&browser); + playlist::add_to_playlist(&mut playlist, &songs); + mode = Mode::Playlist; + } + Mode::Queue => { + if let Some(song) = player.songs.selected() { + playlist::add_to_playlist(&mut playlist, &[song.clone()]); + mode = Mode::Playlist; + } + } + _ => (), + }, + KeyCode::Enter => match mode { + Mode::Browser => { + let songs = browser::get_selected(&browser); + player.add_songs(&songs); + } + Mode::Queue => { + if let Some(i) = queue.ui.index() { + player.play_index(i); + } + } + Mode::Search => search::on_enter(&mut search, &mut player), + Mode::Settings => settings::on_enter(&mut settings, &mut player), + Mode::Playlist => playlist::on_enter(&mut playlist, &mut player), + }, + KeyCode::Backspace => match mode { + Mode::Search => search::on_backspace(&mut search, control), + Mode::Playlist => playlist::on_backspace(&mut playlist, control), + _ => (), + }, + KeyCode::Up => input.up(), + KeyCode::Down => input.down(), + KeyCode::Left => input.left(), + KeyCode::Right => input.right(), + KeyCode::Char('1' | '!') => { + queue::constraint(&mut queue, 0, shift); + } + KeyCode::Char('2' | '@') => { + queue::constraint(&mut queue, 1, shift); + } + KeyCode::Char('3' | '#') => { + queue::constraint(&mut queue, 2, shift); + } + KeyCode::Char(c) => match c { + 'h' => input.left(), + 'j' => input.down(), + 'k' => input.up(), + 'l' => input.right(), + _ => (), + }, + _ => (), + } + } + Event::Mouse(event) => match event.kind { + MouseEventKind::ScrollUp => input.up(), + MouseEventKind::ScrollDown => input.down(), + MouseEventKind::Down(_) => { + if let Mode::Queue = mode { + terminal + .draw(|f| queue::draw(&mut queue, &mut player, f, Some(event))) + .unwrap(); + } + } + _ => (), + }, + Event::Resize(..) => (), + } + } + } + + query::set_volume(player.volume); + + let ids: Vec<usize> = player.songs.data.iter().flat_map(|song| song.id).collect(); + query::cache(&ids); + + disable_raw_mode().unwrap(); + execute!( + terminal.backend_mut(), + LeaveAlternateScreen, + DisableMouseCapture + ) + .unwrap(); } diff --git a/gonk/src/playlist.rs b/gonk/src/playlist.rs new file mode 100644 index 00000000..bbebcb07 --- /dev/null +++ b/gonk/src/playlist.rs @@ -0,0 +1,371 @@ +use crate::{widgets::*, Frame, Input, COLORS}; +use gonk_database::playlist::PlaylistSong; +use gonk_database::{playlist, query}; +use gonk_player::{Index, Player, Song}; +use tui::style::Style; +use tui::text::Span; +use tui::{ + layout::{Constraint, Direction, Layout, Margin, Rect}, + widgets::{Block, BorderType, Borders, Clear, Paragraph}, +}; + +#[derive(PartialEq, Eq)] +pub enum Mode { + Playlist, + Song, + Popup, +} + +pub struct Playlist { + pub mode: Mode, + pub playlists: Index<String>, + pub songs: Index<PlaylistSong>, + pub song_buffer: Vec<Song>, + pub search: String, + pub search_result: String, + pub changed: bool, +} + +impl Playlist { + pub fn new() -> Self { + let playlists = playlist::playlists(); + + let songs = if let Some(playlist) = playlists.first() { + Index::new(playlist::get(playlist), Some(0)) + } else { + Index::new(Vec::new(), Some(0)) + }; + + Self { + mode: Mode::Playlist, + playlists: Index::new(playlists, Some(0)), + songs, + song_buffer: Vec::new(), + changed: false, + search: String::new(), + search_result: String::from("Enter a playlist name..."), + } + } +} + +impl Input for Playlist { + fn up(&mut self) { + match self.mode { + Mode::Playlist => { + self.playlists.up(); + let songs = playlist::get(&self.playlists.selected().unwrap()); + self.songs = Index::new(songs, Some(0)); + } + Mode::Song => self.songs.up(), + Mode::Popup => (), + } + } + + fn down(&mut self) { + match self.mode { + Mode::Playlist => { + self.playlists.down(); + let songs = playlist::get(&self.playlists.selected().unwrap()); + self.songs = Index::new(songs, Some(0)); + } + Mode::Song => self.songs.down(), + Mode::Popup => (), + } + } + + fn left(&mut self) { + if self.mode == Mode::Song { + self.mode = Mode::Playlist; + } + } + + fn right(&mut self) { + match self.mode { + Mode::Playlist if !self.songs.is_empty() => { + self.mode = Mode::Song; + } + _ => (), + } + } +} + +pub fn on_enter(playlist: &mut Playlist, player: &mut Player) { + match playlist.mode { + Mode::Playlist => { + let ids: Vec<usize> = playlist.songs.data.iter().map(|song| song.id).collect(); + let songs = query::songs_from_ids(&ids); + player.add_songs(&songs); + } + Mode::Song => { + if let Some(item) = playlist.songs.selected() { + let song = query::songs_from_ids(&[item.id]).remove(0); + player.add_songs(&[song]); + } + } + Mode::Popup if !playlist.song_buffer.is_empty() => { + //Select an existing playlist or create a new one. + let name = playlist.search.trim().to_string(); + + let ids: Vec<usize> = playlist + .song_buffer + .iter() + .map(|song| song.id.unwrap()) + .collect(); + + playlist::add(&name, &ids); + + playlist.playlists = Index::new(playlist::playlists(), playlist.playlists.index()); + + let mut i = Some(0); + for (j, playlist) in playlist.playlists.data.iter().enumerate() { + if playlist == &name { + i = Some(j); + break; + } + } + + //Select the playlist that was just modified and update the songs. + playlist.playlists.select(i); + let songs = playlist::get(playlist.playlists.selected().unwrap()); + playlist.songs = Index::new(songs, Some(0)); + + //Reset everything. + playlist.search = String::new(); + playlist.mode = Mode::Playlist; + } + Mode::Popup => (), + } +} + +pub fn on_backspace(playlist: &mut Playlist, control: bool) { + match playlist.mode { + Mode::Popup => { + playlist.changed = true; + if control { + playlist.search.clear(); + } else { + playlist.search.pop(); + } + } + _ => playlist.left(), + } +} + +pub fn add_to_playlist(playlist: &mut Playlist, songs: &[Song]) { + playlist.song_buffer = songs.to_vec(); + playlist.mode = Mode::Popup; +} + +pub fn delete(playlist: &mut Playlist) { + match playlist.mode { + Mode::Playlist => { + if let Some(index) = playlist.playlists.index() { + //TODO: Prompt the user with yes or no. + playlist::remove(&playlist.playlists.data[index]); + playlist.playlists.remove(index); + + if playlist.playlists.is_empty() { + //No more playlists mean no more songs. + playlist.songs = Index::default(); + } else { + //After removing a playlist the next songs will need to be loaded. + let songs = playlist::get(playlist.playlists.selected().unwrap()); + playlist.songs = Index::new(songs, Some(0)); + } + } + } + Mode::Song => { + if let Some(song) = playlist.songs.selected() { + playlist::remove_id(song.id); + let index = playlist.songs.index().unwrap(); + playlist.songs.remove(index); + + //If there are no songs left delete the playlist. + if playlist.songs.is_empty() { + let index = playlist.playlists.index().unwrap(); + playlist.playlists.remove(index); + } + } + } + Mode::Popup => (), + } +} + +pub fn on_escape(playlist: &mut Playlist, mode: &mut super::Mode) { + match playlist.mode { + Mode::Popup => { + playlist.mode = Mode::Playlist; + playlist.search = String::new(); + playlist.changed = true; + } + _ => *mode = super::Mode::Browser, + }; +} + +//TODO: I think I want a different popup. +//It should be a small side bar in the browser. +//There should be a list of existing playlists. +//The first playlist will be the one you just added to +//so it's fast to keep adding things +//The last item will be add a new playlist. +//If there are no playlists it will prompt you to create on. +//This should be similar to foobar on android. + +//TODO: Renaming +//Move items around in lists +//There should be a hotkey to add to most recent playlist +//And a message should show up in the bottom bar saying +//"[name] has been has been added to [playlist name]" +//or +//"25 songs have been added to [playlist name]" + +//TODO: Prompt the user with yes or no on deletes. +//TODO: Clear playlist with confirmation. +pub fn draw_popup(playlist: &mut Playlist, f: &mut Frame) { + if let Some(area) = centered_rect(45, 6, f.size()) { + let v = Layout::default() + .direction(Direction::Vertical) + .constraints([Constraint::Length(3), Constraint::Percentage(50)]) + .margin(1) + .split(area); + + f.render_widget(Clear, area); + f.render_widget( + Block::default() + .title("─Add to playlist") + .borders(Borders::ALL) + .border_type(BorderType::Rounded), + area, + ); + + //Scroll the playlist name. + let len = playlist.search.len() as u16; + let width = v[0].width.saturating_sub(1); + let offset_x = if len < width { 0 } else { len - width + 1 }; + + f.render_widget( + Paragraph::new(playlist.search.as_str()) + .block( + Block::default() + .borders(Borders::ALL) + .border_type(BorderType::Rounded), + ) + .scroll((0, offset_x)), + v[0], + ); + + if playlist.changed { + playlist.changed = false; + let eq = playlist + .playlists + .data + .iter() + .any(|e| e == &playlist.search); + playlist.search_result = if eq { + format!("Add to existing playlist: {}", playlist.search) + } else if playlist.search.is_empty() { + String::from("Enter a playlist name...") + } else { + format!("Add to new playlist: {}", playlist.search) + } + } + + f.render_widget( + Paragraph::new(playlist.search_result.as_str()), + v[1].inner(&Margin { + horizontal: 1, + vertical: 0, + }), + ); + + //Draw the cursor. + let (x, y) = (v[0].x + 1, v[0].y + 1); + if playlist.search.is_empty() { + f.set_cursor(x, y); + } else { + let width = v[0].width.saturating_sub(3); + if len < width { + f.set_cursor(x + len, y) + } else { + f.set_cursor(x + width, y) + } + } + } +} + +pub fn draw(playlist: &mut Playlist, area: Rect, f: &mut Frame) { + let horizontal = Layout::default() + .direction(Direction::Horizontal) + .constraints([Constraint::Percentage(30), Constraint::Percentage(70)]) + .split(area); + + let items: Vec<ListItem> = playlist + .playlists + .clone() + .into_iter() + .map(ListItem::new) + .collect(); + + let list = List::new(&items) + .block( + Block::default() + .title("─Playlist") + .borders(Borders::ALL) + .border_type(BorderType::Rounded), + ) + .highlight_symbol(">"); + + let list = if let Mode::Playlist = playlist.mode { + list.highlight_symbol(">") + } else { + list.highlight_symbol("") + }; + + f.render_stateful_widget( + list, + horizontal[0], + &mut ListState::new(playlist.playlists.index()), + ); + + let content = playlist + .songs + .data + .iter() + .map(|song| { + Row::new(vec![ + Span::styled(song.name.as_str(), Style::default().fg(COLORS.name)), + Span::styled(song.album.as_str(), Style::default().fg(COLORS.album)), + Span::styled(song.artist.as_str(), Style::default().fg(COLORS.artist)), + ]) + }) + .collect(); + + let table = Table::new(content) + .widths(&[ + Constraint::Percentage(42), + Constraint::Percentage(30), + Constraint::Percentage(28), + ]) + .block( + Block::default() + .title("─Songs") + .borders(Borders::ALL) + .border_type(BorderType::Rounded), + ); + + let table = if let Mode::Song = playlist.mode { + table.highlight_symbol(">") + } else { + table.highlight_symbol("") + }; + + f.render_stateful_widget( + table, + horizontal[1], + &mut TableState::new(playlist.songs.index()), + ); + + if let Mode::Popup = playlist.mode { + draw_popup(playlist, f); + } +} diff --git a/gonk/src/queue.rs b/gonk/src/queue.rs new file mode 100644 index 00000000..7b071147 --- /dev/null +++ b/gonk/src/queue.rs @@ -0,0 +1,360 @@ +use crate::widgets::*; +use crate::*; +use crossterm::event::MouseEvent; +use gonk_player::{Index, Player}; +use tui::layout::{Alignment, Constraint, Direction, Layout, Rect}; +use tui::style::{Color, Modifier, Style}; +use tui::text::{Span, Spans}; +use tui::widgets::{Block, BorderType, Borders, Paragraph}; +use unicode_width::UnicodeWidthStr; + +pub struct Queue { + pub ui: Index<()>, + pub constraint: [u16; 4], + pub len: usize, +} + +impl Queue { + pub fn new() -> Self { + Self { + ui: Index::new(Vec::new(), Some(0)), + constraint: [8, 42, 24, 26], + len: 0, + } + } +} + +impl Input for Queue { + fn up(&mut self) { + self.ui.up_with_len(self.len); + } + + fn down(&mut self) { + self.ui.down_with_len(self.len); + } + + fn left(&mut self) {} + + fn right(&mut self) {} +} + +pub fn constraint(queue: &mut Queue, row: usize, shift: bool) { + if shift && queue.constraint[row] != 0 { + //Move row back. + queue.constraint[row + 1] += 1; + queue.constraint[row] = queue.constraint[row].saturating_sub(1); + } else if queue.constraint[row + 1] != 0 { + //Move row forward. + queue.constraint[row] += 1; + queue.constraint[row + 1] = queue.constraint[row + 1].saturating_sub(1); + } + + debug_assert!( + queue.constraint.iter().sum::<u16>() == 100, + "Constraint went out of bounds: {:?}", + queue.constraint + ); +} + +pub fn delete(queue: &mut Queue, player: &mut Player) { + if let Some(i) = queue.ui.index() { + player.delete_index(i); + //make sure the ui index is in sync + let len = player.songs.len().saturating_sub(1); + if i > len { + queue.ui.select(Some(len)); + } + } +} + +pub fn draw(queue: &mut Queue, player: &mut Player, f: &mut Frame, event: Option<MouseEvent>) { + let chunks = Layout::default() + .direction(Direction::Vertical) + .constraints([ + Constraint::Length(3), + Constraint::Min(10), + Constraint::Length(3), + ]) + .split(f.size()); + + draw_header(player, f, chunks[0]); + + let row_bounds = draw_body(queue, player, f, chunks[1]); + + draw_seeker(player, f, chunks[2]); + + //Don't handle mouse input when the queue is empty. + if player.is_empty() { + return; + } + + //Handle mouse input. + if let Some(event) = event { + let (x, y) = (event.column, event.row); + const HEADER_HEIGHT: u16 = 5; + + let size = f.size(); + + //Mouse support for the seek bar. + if (size.height - 3 == y || size.height - 2 == y || size.height - 1 == y) + && size.height > 15 + { + let ratio = x as f32 / size.width as f32; + let duration = player.duration.as_secs_f32(); + player.seek_to(duration * ratio); + } + + //Mouse support for the queue. + if let Some((start, _)) = row_bounds { + //Check if you clicked on the header. + if y >= HEADER_HEIGHT { + let index = (y - HEADER_HEIGHT) as usize + start; + + //Make sure you didn't click on the seek bar + //and that the song index exists. + if index < player.songs.len() + && ((size.height < 15 && y < size.height.saturating_sub(1)) + || y < size.height.saturating_sub(3)) + { + queue.ui.select(Some(index)); + } + } + } + } +} + +fn draw_header(player: &mut Player, f: &mut Frame, area: Rect) { + f.render_widget( + Block::default() + .borders(Borders::TOP | Borders::LEFT | Borders::RIGHT) + .border_type(BorderType::Rounded), + area, + ); + + let state = if player.songs.is_empty() { + String::from("╭─Stopped") + } else if player.is_playing() { + String::from("╭─Playing") + } else { + String::from("╭─Paused") + }; + + f.render_widget(Paragraph::new(state).alignment(Alignment::Left), area); + + if !player.songs.is_empty() { + draw_title(player, f, area); + } + + let volume = Spans::from(format!("Vol: {}%─╮", player.volume)); + f.render_widget(Paragraph::new(volume).alignment(Alignment::Right), area); +} + +fn draw_title(player: &mut Player, f: &mut Frame, area: Rect) { + let title = if let Some(song) = player.songs.selected() { + let mut name = song.name.trim_end().to_string(); + let mut album = song.album.trim_end().to_string(); + let mut artist = song.artist.trim_end().to_string(); + let max_width = area.width.saturating_sub(30) as usize; + + while artist.width() + name.width() + "-| - |-".width() > max_width { + if artist.width() > name.width() { + artist.pop(); + } else { + name.pop(); + } + } + + while album.width() > max_width { + album.pop(); + } + + let n = album + .width() + .saturating_sub(artist.width() + name.width() + 3); + let rem = n % 2; + let pad_front = " ".repeat(n / 2); + let pad_back = " ".repeat(n / 2 + rem); + + vec![ + Spans::from(vec![ + Span::raw(format!("─│ {}", pad_front)), + Span::styled(artist, Style::default().fg(COLORS.artist)), + Span::raw(" ─ "), + Span::styled(name, Style::default().fg(COLORS.name)), + Span::raw(format!("{} │─", pad_back)), + ]), + Spans::from(Span::styled(album, Style::default().fg(COLORS.album))), + ] + } else { + Vec::new() + }; + + f.render_widget(Paragraph::new(title).alignment(Alignment::Center), area); +} + +fn draw_body( + queue: &mut Queue, + player: &mut Player, + f: &mut Frame, + area: Rect, +) -> Option<(usize, usize)> { + if player.songs.is_empty() { + f.render_widget( + Block::default() + .border_type(BorderType::Rounded) + .borders(Borders::LEFT | Borders::RIGHT), + area, + ); + return None; + } + + let (songs, player_index, ui_index) = + (&player.songs.data, player.songs.index(), queue.ui.index()); + + let mut items: Vec<Row> = songs + .iter() + .map(|song| { + Row::new(vec![ + Cell::from(""), + Cell::from(song.number.to_string()).style(Style::default().fg(COLORS.number)), + Cell::from(song.name.as_str()).style(Style::default().fg(COLORS.name)), + Cell::from(song.album.as_str()).style(Style::default().fg(COLORS.album)), + Cell::from(song.artist.as_str()).style(Style::default().fg(COLORS.artist)), + ]) + }) + .collect(); + + if let Some(player_index) = player_index { + if let Some(song) = songs.get(player_index) { + if let Some(ui_index) = ui_index { + //Currently playing song + let row = if ui_index == player_index { + Row::new(vec![ + Cell::from(">>").style( + Style::default() + .fg(Color::White) + .add_modifier(Modifier::DIM | Modifier::BOLD), + ), + Cell::from(song.number.to_string()) + .style(Style::default().bg(COLORS.number).fg(Color::Black)), + Cell::from(song.name.as_str()) + .style(Style::default().bg(COLORS.name).fg(Color::Black)), + Cell::from(song.album.as_str()) + .style(Style::default().bg(COLORS.album).fg(Color::Black)), + Cell::from(song.artist.as_str()) + .style(Style::default().bg(COLORS.artist).fg(Color::Black)), + ]) + } else { + Row::new(vec![ + Cell::from(">>").style( + Style::default() + .fg(Color::White) + .add_modifier(Modifier::DIM | Modifier::BOLD), + ), + Cell::from(song.number.to_string()) + .style(Style::default().fg(COLORS.number)), + Cell::from(song.name.as_str()).style(Style::default().fg(COLORS.name)), + Cell::from(song.album.as_str()).style(Style::default().fg(COLORS.album)), + Cell::from(song.artist.as_str()).style(Style::default().fg(COLORS.artist)), + ]) + }; + + items.remove(player_index); + items.insert(player_index, row); + + //Current selection + if ui_index != player_index { + if let Some(song) = songs.get(ui_index) { + let row = Row::new(vec![ + Cell::default(), + Cell::from(song.number.to_string()) + .style(Style::default().bg(COLORS.number)), + Cell::from(song.name.as_str()).style(Style::default().bg(COLORS.name)), + Cell::from(song.album.as_str()) + .style(Style::default().bg(COLORS.album)), + Cell::from(song.artist.as_str()) + .style(Style::default().bg(COLORS.artist)), + ]) + .style(Style::default().fg(Color::Black)); + items.remove(ui_index); + items.insert(ui_index, row); + } + } + } + } + } + + let con = [ + Constraint::Length(2), + Constraint::Percentage(queue.constraint[0]), + Constraint::Percentage(queue.constraint[1]), + Constraint::Percentage(queue.constraint[2]), + Constraint::Percentage(queue.constraint[3]), + ]; + + let t = Table::new(items) + .header( + Row::new(["", "Track", "Title", "Album", "Artist"]) + .style( + Style::default() + .fg(Color::White) + .add_modifier(Modifier::BOLD), + ) + .bottom_margin(1), + ) + .block( + Block::default() + .borders(Borders::LEFT | Borders::RIGHT | Borders::BOTTOM) + .border_type(BorderType::Rounded), + ) + // .separator() + .widths(&con); + + let row_bounds = t.get_row_bounds(ui_index, t.get_row_height(area)); + + f.render_stateful_widget(t, area, &mut TableState::new(ui_index)); + + Some(row_bounds) +} + +fn draw_seeker(player: &mut Player, f: &mut Frame, area: Rect) { + if player.songs.is_empty() { + return f.render_widget( + Block::default() + .border_type(BorderType::Rounded) + .borders(Borders::BOTTOM | Borders::LEFT | Borders::RIGHT), + area, + ); + } + + let elapsed = player.elapsed().as_secs_f64(); + let duration = player.duration.as_secs_f64(); + + let seeker = format!( + "{:02}:{:02}/{:02}:{:02}", + (elapsed / 60.0).floor(), + elapsed.trunc() as u32 % 60, + (duration / 60.0).floor(), + duration.trunc() as u32 % 60, + ); + + let ratio = elapsed / duration; + let ratio = if ratio.is_nan() { + 0.0 + } else { + ratio.clamp(0.0, 1.0) + }; + + f.render_widget( + Gauge::default() + .block( + Block::default() + .borders(Borders::ALL) + .border_type(BorderType::Rounded), + ) + .gauge_style(Style::default().fg(COLORS.seeker)) + .ratio(ratio) + .label(seeker), + area, + ); +} diff --git a/gonk/src/search.rs b/gonk/src/search.rs new file mode 100644 index 00000000..9556053a --- /dev/null +++ b/gonk/src/search.rs @@ -0,0 +1,485 @@ +use super::Mode as AppMode; +use crate::widgets::*; +use crate::*; +use gonk_database::query; +use gonk_player::{Index, Player}; +use rayon::iter::{IntoParallelRefIterator, ParallelIterator}; +use std::cmp::Ordering; +use tui::{ + layout::{Alignment, Constraint, Direction, Layout, Rect}, + style::{Modifier, Style}, + text::{Span, Spans}, + widgets::{Block, BorderType, Borders, Paragraph}, +}; + +#[derive(Clone)] +pub enum Item { + Song(Song), + Album(Album), + Artist(Artist), +} + +#[derive(Clone, Default)] +pub struct Song { + pub id: usize, + pub name: String, + pub album: String, + pub artist: String, +} + +#[derive(Clone, Default)] +pub struct Album { + pub name: String, + pub artist: String, +} + +#[derive(Clone, Default)] +pub struct Artist { + pub name: String, +} + +#[derive(PartialEq, Eq)] +pub enum Mode { + Search, + Select, +} + +pub struct Search { + pub query: String, + pub query_changed: bool, + pub mode: Mode, + pub results: Index<Item>, + pub cache: Vec<Item>, +} + +impl Search { + pub fn new() -> Self { + let mut search = Self { + cache: Vec::new(), + query: String::new(), + query_changed: false, + mode: Mode::Search, + results: Index::default(), + }; + refresh_cache(&mut search); + refresh_results(&mut search); + search + } +} + +impl Input for Search { + fn up(&mut self) { + self.results.up(); + } + + fn down(&mut self) { + self.results.down(); + } + + fn left(&mut self) {} + + fn right(&mut self) {} +} + +pub fn on_backspace(search: &mut Search, shift: bool) { + match search.mode { + Mode::Search => { + if shift { + search.query.clear(); + } else { + search.query.pop(); + } + } + Mode::Select => { + search.results.select(None); + search.mode = Mode::Search; + } + } +} + +pub fn on_escape(search: &mut Search, mode: &mut AppMode) { + match search.mode { + Mode::Search => { + if let Mode::Search = search.mode { + search.query.clear(); + *mode = AppMode::Queue; + } + } + Mode::Select => { + search.mode = Mode::Search; + search.results.select(None); + } + } +} + +pub fn on_enter(search: &mut Search, player: &mut Player) { + match search.mode { + Mode::Search => { + if !search.results.is_empty() { + search.mode = Mode::Select; + search.results.select(Some(0)); + } + } + Mode::Select => { + if let Some(item) = search.results.selected() { + let songs = match item { + Item::Song(song) => query::songs_from_ids(&[song.id]), + Item::Album(album) => query::songs_from_album(&album.name, &album.artist), + Item::Artist(artist) => query::songs_by_artist(&artist.name), + }; + + player.add_songs(&songs); + } + } + } +} + +pub fn refresh_cache(search: &mut Search) { + search.cache = Vec::new(); + + for song in query::songs() { + search.cache.push(Item::Song(Song { + name: song.name, + album: song.album, + artist: song.artist, + id: song.id.unwrap(), + })); + } + + for (name, artist) in query::albums() { + search.cache.push(Item::Album(Album { name, artist })); + } + + for name in query::artists() { + search.cache.push(Item::Artist(Artist { name })); + } +} + +pub fn refresh_results(search: &mut Search) { + let query = &search.query.to_lowercase(); + + let get_accuary = |item: &Item| -> f64 { + match item { + Item::Song(song) => strsim::jaro_winkler(query, &song.name.to_lowercase()), + Item::Album(album) => strsim::jaro_winkler(query, &album.name.to_lowercase()), + Item::Artist(artist) => strsim::jaro_winkler(query, &artist.name.to_lowercase()), + } + }; + + let mut results: Vec<_> = if query.is_empty() { + //If there user has not asked to search anything + //populate the list with 40 results. + search + .cache + .iter() + .take(40) + .rev() + .map(|item| (item, get_accuary(item))) + .collect() + } else { + search + .cache + .par_iter() + .filter_map(|item| { + let acc = get_accuary(item); + + //Filter out results that are poor matches. 0.75 is a magic number. + if acc > 0.75 { + Some((item, acc)) + } else { + None + } + }) + .collect() + }; + + //Sort results by score. + results.sort_by(|(_, a), (_, b)| b.partial_cmp(a).unwrap()); + + //Sort artists above search-titled albums. + results.sort_by(|(item, a), (_, b)| { + //This is just x == y but for floats. + if (a - b).abs() < f64::EPSILON { + //And the item is an album + if let Item::Album(_) = item { + //Move item lower in the list. + Ordering::Greater + } else { + //Move item higher in the list. + Ordering::Less + } + } else { + //Keep the same order. + Ordering::Equal + } + }); + + search.results.data = results.into_iter().map(|(item, _)| item.clone()).collect(); +} + +pub fn draw(search: &mut Search, area: Rect, f: &mut Frame) { + if search.query_changed { + refresh_results(search); + } + + let v = Layout::default() + .direction(Direction::Vertical) + .constraints([ + Constraint::Length(3), + Constraint::Percentage(30), + Constraint::Percentage(60), + ]) + .split(area); + + let h = Layout::default() + .direction(Direction::Horizontal) + .constraints([Constraint::Percentage(35), Constraint::Percentage(65)]) + .split(v[1]); + + draw_textbox(search, f, v[0]); + + let item = if search.results.selected().is_some() { + search.results.selected() + } else { + search.results.data.first() + }; + + if let Some(item) = item { + match item { + Item::Song(song) => { + search::song(f, &song.name, &song.album, &song.artist, h[0]); + album(f, &song.album, &song.artist, h[1]); + } + Item::Album(album) => { + search::album(f, &album.name, &album.artist, h[0]); + artist(f, &album.artist, h[1]); + } + Item::Artist(artist) => { + let albums = query::albums_by_artist(&artist.name); + + search::artist(f, &artist.name, h[0]); + + let h_split = Layout::default() + .direction(Direction::Horizontal) + .constraints([Constraint::Percentage(50), Constraint::Percentage(50)]) + .split(h[1]); + + //draw the first two albums + for (i, area) in h_split.iter().enumerate() { + if let Some(album) = albums.get(i) { + search::album(f, album, &artist.name, *area); + } + } + } + } + draw_results(search, f, v[2]); + } else { + draw_results(search, f, v[1].union(v[2])); + } + + //Move the cursor position when typing + if let Mode::Search = search.mode { + if search.results.index().is_none() && search.query.is_empty() { + f.set_cursor(1, 1); + } else { + let len = search.query.len() as u16; + let max_width = area.width.saturating_sub(2); + if len >= max_width { + f.set_cursor(max_width, 1); + } else { + f.set_cursor(len + 1, 1); + } + } + } +} + +fn song(f: &mut Frame, name: &str, album: &str, artist: &str, area: Rect) { + let song_table = Table::new(vec![ + Row::new(vec![Spans::from(Span::raw(album))]), + Row::new(vec![Spans::from(Span::raw(artist))]), + ]) + .header( + Row::new(vec![Span::styled( + format!("{} ", name), + Style::default().add_modifier(Modifier::ITALIC), + )]) + .bottom_margin(1), + ) + .block( + Block::default() + .borders(Borders::ALL) + .border_type(BorderType::Rounded) + .title("Song"), + ) + .widths(&[Constraint::Percentage(100)]); + + f.render_widget(song_table, area); +} + +fn album(f: &mut Frame, album: &str, artist: &str, area: Rect) { + let cells: Vec<_> = query::songs_from_album(album, artist) + .iter() + .map(|song| Row::new(vec![Cell::from(format!("{}. {}", song.number, song.name))])) + .collect(); + + let table = Table::new(cells) + .header( + Row::new(vec![Cell::from(Span::styled( + format!("{} ", album), + Style::default().add_modifier(Modifier::ITALIC), + ))]) + .bottom_margin(1), + ) + .block( + Block::default() + .borders(Borders::ALL) + .border_type(BorderType::Rounded) + .title("Album"), + ) + .widths(&[Constraint::Percentage(100)]); + + f.render_widget(table, area); +} + +fn artist(f: &mut Frame, artist: &str, area: Rect) { + let albums = query::albums_by_artist(artist); + let cells: Vec<_> = albums + .iter() + .map(|album| Row::new(vec![Cell::from(Span::raw(album))])) + .collect(); + + let table = Table::new(cells) + .header( + Row::new(vec![Cell::from(Span::styled( + format!("{} ", artist), + Style::default().add_modifier(Modifier::ITALIC), + ))]) + .bottom_margin(1), + ) + .block( + Block::default() + .borders(Borders::ALL) + .border_type(BorderType::Rounded) + .title("Artist"), + ) + .widths(&[Constraint::Percentage(100)]); + + f.render_widget(table, area); +} + +fn draw_results(search: &Search, f: &mut Frame, area: Rect) { + let get_cell = |item: &Item, selected: bool| -> Row { + let selected_cell = if selected { + Cell::from(">") + } else { + Cell::default() + }; + + match item { + Item::Song(song) => { + let song = query::songs_from_ids(&[song.id])[0].clone(); + Row::new(vec![ + selected_cell, + Cell::from(song.name).style(Style::default().fg(COLORS.name)), + Cell::from(song.album).style(Style::default().fg(COLORS.album)), + Cell::from(song.artist).style(Style::default().fg(COLORS.artist)), + ]) + } + Item::Album(album) => Row::new(vec![ + selected_cell, + Cell::from(Spans::from(vec![ + Span::styled( + format!("{} - ", album.name), + Style::default().fg(COLORS.name), + ), + Span::styled( + "Album", + Style::default() + .fg(COLORS.name) + .add_modifier(Modifier::ITALIC), + ), + ])), + Cell::from("").style(Style::default().fg(COLORS.album)), + Cell::from(album.artist.clone()).style(Style::default().fg(COLORS.artist)), + ]), + Item::Artist(artist) => Row::new(vec![ + selected_cell, + Cell::from(Spans::from(vec![ + Span::styled( + format!("{} - ", artist.name), + Style::default().fg(COLORS.name), + ), + Span::styled( + "Artist", + Style::default() + .fg(COLORS.name) + .add_modifier(Modifier::ITALIC), + ), + ])), + Cell::from("").style(Style::default().fg(COLORS.album)), + Cell::from("").style(Style::default().fg(COLORS.artist)), + ]), + } + }; + + let rows: Vec<_> = search + .results + .data + .iter() + .enumerate() + .map(|(i, item)| { + if let Some(s) = search.results.index() { + if s == i { + return get_cell(item, true); + } + } else if i == 0 { + return get_cell(item, false); + } + get_cell(item, false) + }) + .collect(); + + let italic = Style::default().add_modifier(Modifier::ITALIC); + let table = Table::new(rows) + .header( + Row::new(vec![ + Cell::default(), + Cell::from("Name").style(italic), + Cell::from("Album").style(italic), + Cell::from("Artist").style(italic), + ]) + .bottom_margin(1), + ) + .block( + Block::default() + .borders(Borders::ALL) + .border_type(BorderType::Rounded), + ) + .widths(&[ + Constraint::Length(1), + Constraint::Percentage(40), + Constraint::Percentage(40), + Constraint::Percentage(20), + ]); + + f.render_stateful_widget(table, area, &mut TableState::new(search.results.index())); +} + +fn draw_textbox(search: &Search, f: &mut Frame, area: Rect) { + let len = search.query.len() as u16; + //Search box is a little smaller than the max width + let width = area.width.saturating_sub(1); + let offset_x = if len < width { 0 } else { len - width + 1 }; + + f.render_widget( + Paragraph::new(search.query.as_str()) + .block( + Block::default() + .borders(Borders::ALL) + .border_type(BorderType::Rounded), + ) + .alignment(Alignment::Left) + .scroll((0, offset_x)), + area, + ); +} diff --git a/gonk/src/settings.rs b/gonk/src/settings.rs new file mode 100644 index 00000000..2b943856 --- /dev/null +++ b/gonk/src/settings.rs @@ -0,0 +1,98 @@ +use crate::{widgets::*, Frame, Input}; +use gonk_database::query; +use gonk_player::{Device, DeviceTrait, Index, Player}; +use tui::{ + layout::Rect, + style::{Color, Modifier, Style}, + widgets::{Block, BorderType, Borders}, +}; + +pub struct Settings { + pub devices: Index<Device>, + pub current_device: String, +} + +impl Settings { + pub fn new() -> Self { + let default_device = Player::default_device(); + let wanted_device = query::playback_device(); + + let devices = Player::audio_devices(); + let device_names: Vec<String> = devices.iter().flat_map(DeviceTrait::name).collect(); + + let current_device = if !device_names.contains(&wanted_device) { + let name = default_device.name().unwrap(); + query::set_playback_device(&name); + name + } else { + wanted_device + }; + + Self { + devices: Index::new(devices, Some(0)), + current_device, + } + } +} + +impl Input for Settings { + fn up(&mut self) { + self.devices.up(); + } + + fn down(&mut self) { + self.devices.down() + } + + fn left(&mut self) {} + + fn right(&mut self) {} +} + +pub fn on_enter(settings: &mut Settings, player: &mut Player) { + if let Some(device) = settings.devices.selected() { + match player.change_output_device(device) { + Ok(_) => { + let name = device.name().unwrap(); + dbg!(&name); + query::set_playback_device(&name); + settings.current_device = name; + } + //TODO: Print error in status bar + Err(e) => panic!("{:?}", e), + } + } +} + +#[allow(unused)] +pub fn draw(settings: &mut Settings, area: Rect, f: &mut Frame) { + let items: Vec<ListItem> = settings + .devices + .data + .iter() + .map(|device| { + let name = device.name().unwrap(); + if name == settings.current_device { + ListItem::new(name) + } else { + ListItem::new(name).style(Style::default().add_modifier(Modifier::DIM)) + } + }) + .collect(); + + let list = List::new(&items) + .block( + Block::default() + .title("─Output Device") + .borders(Borders::ALL) + .border_type(BorderType::Rounded), + ) + .style(Style::default().fg(Color::White)) + .highlight_style(Style::default()) + .highlight_symbol("> "); + + let mut state = ListState::default(); + state.select(settings.devices.index()); + + f.render_stateful_widget(list, area, &mut state); +} diff --git a/gonk/src/sqlite.rs b/gonk/src/sqlite.rs deleted file mode 100644 index 4474661d..00000000 --- a/gonk/src/sqlite.rs +++ /dev/null @@ -1,307 +0,0 @@ -use crate::GONK_DIR; -use gonk_player::Song; -use jwalk::WalkDir; -use rayon::iter::{IntoParallelRefIterator, ParallelIterator}; -use rusqlite::{params, Connection, Params, Row}; -use static_init::dynamic; -use std::{ - path::PathBuf, - sync::{Mutex, MutexGuard}, - thread::{self, JoinHandle}, - time::Duration, -}; - -#[dynamic] -static DB_DIR: PathBuf = GONK_DIR.join("gonk.db"); - -pub fn total_songs() -> usize { - let conn = conn(); - let mut stmt = conn.prepare("SELECT COUNT(*) FROM song").unwrap(); - stmt.query_row([], |row| row.get(0)).unwrap() -} -pub fn get_all_songs() -> Vec<Song> { - collect_songs("SELECT *, rowid FROM song", params![]) -} -pub fn get_all_artists() -> Vec<String> { - let conn = conn(); - let mut stmt = conn - .prepare("SELECT DISTINCT artist FROM song ORDER BY artist COLLATE NOCASE") - .unwrap(); - - stmt.query_map([], |row| { - let artist: String = row.get(0).unwrap(); - Ok(artist) - }) - .unwrap() - .flatten() - .collect() -} -pub fn get_all_albums() -> Vec<(String, String)> { - let conn = conn(); - let mut stmt = conn - .prepare("SELECT DISTINCT album, artist FROM song ORDER BY artist COLLATE NOCASE") - .unwrap(); - - stmt.query_map([], |row| { - let album: String = row.get(0).unwrap(); - let artist: String = row.get(1).unwrap(); - Ok((album, artist)) - }) - .unwrap() - .flatten() - .collect() -} -pub fn get_all_albums_by_artist(artist: &str) -> Vec<String> { - let conn = conn(); - let mut stmt = conn - .prepare("SELECT DISTINCT album FROM song WHERE artist = ? ORDER BY album COLLATE NOCASE") - .unwrap(); - - stmt.query_map([artist], |row| row.get(0)) - .unwrap() - .flatten() - .collect() -} -pub fn get_all_songs_from_album(album: &str, artist: &str) -> Vec<Song> { - collect_songs( - "SELECT *, rowid FROM song WHERE artist=(?1) AND album=(?2) ORDER BY disc, number", - params![artist, album], - ) -} -pub fn get_songs_by_artist(artist: &str) -> Vec<Song> { - collect_songs( - "SELECT *, rowid FROM song WHERE artist = ? ORDER BY album, disc, number", - params![artist], - ) -} -pub fn get_songs(ids: &[usize]) -> Vec<Song> { - let conn = conn(); - let mut stmt = conn - .prepare("SELECT *, rowid FROM song WHERE rowid = ?") - .unwrap(); - - ids.iter() - .map(|id| stmt.query_row([id], |row| Ok(song(row)))) - .flatten() - .collect() -} -fn collect_songs<P>(query: &str, params: P) -> Vec<Song> -where - P: Params, -{ - let conn = conn(); - let mut stmt = conn.prepare(query).expect(query); - - stmt.query_map(params, |row| Ok(song(row))) - .unwrap() - .flatten() - .collect() -} -fn song(row: &Row) -> Song { - let path: String = row.get(5).unwrap(); - let dur: f64 = row.get(6).unwrap(); - let _parent: String = row.get(8).unwrap(); - Song { - number: row.get(0).unwrap(), - disc: row.get(1).unwrap(), - name: row.get(2).unwrap(), - album: row.get(3).unwrap(), - artist: row.get(4).unwrap(), - duration: Duration::from_secs_f64(dur), - path: PathBuf::from(path), - track_gain: row.get(7).unwrap(), - id: row.get(9).unwrap(), - } -} - -pub static mut CONN: Option<Mutex<rusqlite::Connection>> = None; - -pub fn initialize_database() { - let exists = DB_DIR.exists(); - if let Ok(conn) = Connection::open(DB_DIR.as_path()) { - if !exists { - conn.execute( - "CREATE TABLE song ( - number INTEGER NOT NULL, - disc INTEGER NOT NULL, - name TEXT NOT NULL, - album TEXT NOT NULL, - artist TEXT NOT NULL, - path TEXT NOT NULL UNIQUE, - duration DOUBLE NOT NULL, - track_gain DOUBLE NOT NULL, - parent TEXT NOT NULL - )", - [], - ) - .unwrap(); - - conn.execute( - "CREATE TABLE playlist ( - song_id INTEGER NOT NULL, - name TEXT NOT NULL - )", - [], - ) - .unwrap(); - } - - unsafe { - CONN = Some(Mutex::new(conn)); - } - } else { - panic!("Could not open database!") - } -} - -pub fn reset() { - unsafe { - CONN = None; - } - let _ = std::fs::remove_file(DB_DIR.as_path()); -} - -pub fn conn() -> MutexGuard<'static, Connection> { - unsafe { CONN.as_ref().unwrap().lock().unwrap() } -} - -pub fn add_playlist(name: &str, ids: &[usize]) { - let conn = conn(); - - //TODO: batch this - for id in ids { - conn.execute( - "INSERT INTO playlist (song_id, name) VALUES (?1, ?2)", - params![id, name], - ) - .unwrap(); - } -} - -pub mod playlist { - use super::conn; - - pub fn get_names() -> Vec<String> { - let conn = conn(); - let mut stmt = conn.prepare("SELECT DISTINCT name FROM playlist").unwrap(); - - stmt.query_map([], |row| row.get(0)) - .unwrap() - .flatten() - .collect() - } - - pub fn get(playlist_name: &str) -> (Vec<usize>, Vec<usize>) { - let conn = conn(); - let mut stmt = conn - .prepare("SELECT rowid, song_id FROM playlist WHERE name = ?") - .unwrap(); - - let ids: Vec<_> = stmt - .query_map([playlist_name], |row| { - Ok((row.get(0).unwrap(), row.get(1).unwrap())) - }) - .unwrap() - .flatten() - .collect(); - - let row_ids: Vec<_> = ids.iter().map(|id| id.0).collect(); - let song_ids: Vec<_> = ids.iter().map(|id| id.1).collect(); - (row_ids, song_ids) - } - - pub fn remove_id(id: usize) { - conn() - .execute("DELETE FROM playlist WHERE rowid = ?", [id]) - .unwrap(); - } - pub fn remove(name: &str) { - conn() - .execute("DELETE FROM playlist WHERE name = ?", [name]) - .unwrap(); - } -} - -pub enum State { - Busy, - Idle, - NeedsUpdate, -} - -#[derive(Default)] -pub struct Database { - handle: Option<JoinHandle<()>>, -} - -impl Database { - pub fn add_paths(&mut self, paths: &[String]) { - if let Some(handle) = &self.handle { - if !handle.is_finished() { - return; - } - } - - let paths = paths.to_vec(); - - self.handle = Some(thread::spawn(move || { - let queries: Vec<String> = paths - .iter() - .map(|path| { - let paths: Vec<PathBuf> = WalkDir::new(path) - .into_iter() - .flatten() - .map(|dir| dir.path()) - .filter(|path| match path.extension() { - Some(ex) => { - matches!(ex.to_str(), Some("flac" | "mp3" | "ogg" | "wav" | "m4a")) - } - None => false, - }) - .collect(); - - let songs: Vec<Song> = paths - .par_iter() - .map(|dir| Song::from(dir)) - .flatten() - .collect(); - - if songs.is_empty() { - String::new() - } else { - songs - .iter() - .map(|song| { - let artist = song.artist.replace('\'', r"''"); - let album = song.album.replace('\'', r"''"); - let name = song.name.replace('\'', r"''"); - let song_path= song.path.to_string_lossy().replace('\'', r"''"); - let parent = path.replace('\'', r"''"); - - format!("INSERT OR IGNORE INTO song (number, disc, name, album, artist, path, duration, track_gain, parent) VALUES ('{}', '{}', '{}', '{}', '{}', '{}', '{}', '{}', '{}');", - song.number, song.disc, name, album, artist, song_path, song.duration.as_secs_f64(), song.track_gain, parent) - }) - .collect::<Vec<String>>() - .join("\n") - } - }) - .collect(); - - let stmt = format!("BEGIN;\nDELETE FROM song;\n{}COMMIT;\n", queries.join("\n")); - conn().execute_batch(&stmt).unwrap(); - })); - } - pub fn state(&mut self) -> State { - match self.handle { - Some(ref handle) => { - let finished = handle.is_finished(); - if finished { - self.handle = None; - State::NeedsUpdate - } else { - State::Busy - } - } - None => State::Idle, - } - } -} diff --git a/gonk/src/status_bar.rs b/gonk/src/status_bar.rs new file mode 100644 index 00000000..dd39291d --- /dev/null +++ b/gonk/src/status_bar.rs @@ -0,0 +1,143 @@ +use crate::{Frame, COLORS}; +use gonk_database::query; +use gonk_player::Player; +use std::time::{Duration, Instant}; +use tui::{ + layout::{Alignment, Constraint, Direction, Layout, Rect}, + style::Style, + text::{Span, Spans}, + widgets::{Block, BorderType, Borders, Paragraph}, +}; + +const WAIT_TIME: Duration = Duration::from_secs(2); + +pub struct StatusBar { + pub dots: usize, + pub busy: bool, + pub scan_message: String, + pub wait_timer: Option<Instant>, + pub scan_timer: Option<Instant>, + pub hidden: bool, +} + +impl StatusBar { + pub fn new() -> Self { + Self { + dots: 1, + busy: false, + scan_message: String::new(), + wait_timer: None, + scan_timer: None, + hidden: true, + } + } +} + +//Updates the dots in "Scanning for files .." +pub fn update(status_bar: &mut StatusBar, db_busy: bool, player: &Player) { + if db_busy { + if status_bar.dots < 3 { + status_bar.dots += 1; + } else { + status_bar.dots = 1; + } + } else { + status_bar.dots = 1; + } + + if let Some(timer) = status_bar.wait_timer { + if timer.elapsed() >= WAIT_TIME { + status_bar.wait_timer = None; + status_bar.busy = false; + + //FIXME: If the queue was not empty + //and the status bar was hidden + //before triggering an update + //the status bar will stay open + //without the users permission. + if player.is_empty() { + status_bar.hidden = true; + } + } + } +} + +pub fn draw(status_bar: &mut StatusBar, area: Rect, f: &mut Frame, busy: bool, player: &Player) { + if busy { + //If database is busy but status_bar is not + //set the status bar to busy + if !status_bar.busy { + status_bar.busy = true; + status_bar.hidden = false; + status_bar.scan_timer = Some(Instant::now()); + } + } else if status_bar.busy { + //If database is no-longer busy + //but status bar is. Print the duration + //and start the wait timer. + if let Some(scan_time) = status_bar.scan_timer { + status_bar.busy = false; + status_bar.wait_timer = Some(Instant::now()); + status_bar.scan_timer = None; + status_bar.scan_message = format!( + "Finished adding {} files in {:.2} seconds.", + query::total_songs(), + scan_time.elapsed().as_secs_f32(), + ); + } + } + + if status_bar.hidden { + return; + } + + let text = if busy { + Spans::from(format!("Scannig for files{}", ".".repeat(status_bar.dots))) + } else if status_bar.wait_timer.is_some() { + Spans::from(status_bar.scan_message.as_str()) + } else if let Some(song) = player.songs.selected() { + Spans::from(vec![ + Span::raw(" "), + Span::styled(song.number.to_string(), Style::default().fg(COLORS.number)), + Span::raw(" | "), + Span::styled(song.name.as_str(), Style::default().fg(COLORS.name)), + Span::raw(" | "), + Span::styled(song.album.as_str(), Style::default().fg(COLORS.album)), + Span::raw(" | "), + Span::styled(song.artist.as_str(), Style::default().fg(COLORS.artist)), + ]) + } else { + Spans::default() + }; + + let area = Layout::default() + .direction(Direction::Horizontal) + .constraints([Constraint::Percentage(85), Constraint::Percentage(15)]) + .split(area); + + f.render_widget( + Paragraph::new(text).alignment(Alignment::Left).block( + Block::default() + .borders(Borders::TOP | Borders::LEFT | Borders::BOTTOM) + .border_type(BorderType::Rounded), + ), + area[0], + ); + + //TODO: Draw mini progress bar here. + + let text = if !player.is_playing() { + String::from("Paused ") + } else { + format!("Vol: {}% ", player.volume) + }; + + f.render_widget( + Paragraph::new(text).alignment(Alignment::Right).block( + Block::default() + .borders(Borders::TOP | Borders::RIGHT | Borders::BOTTOM) + .border_type(BorderType::Rounded), + ), + area[1], + ); +} diff --git a/gonk/src/toml.rs b/gonk/src/toml.rs deleted file mode 100644 index 0c2be45e..00000000 --- a/gonk/src/toml.rs +++ /dev/null @@ -1,262 +0,0 @@ -use crate::GONK_DIR; -use crossterm::event::{KeyCode, KeyModifiers}; -use serde::{Deserialize, Serialize}; -use static_init::dynamic; -use std::{ - fs, - path::{Path, PathBuf}, -}; -use tui::style::Color; - -#[dynamic] -static TOML_DIR: PathBuf = GONK_DIR.join("gonk.toml"); - -#[derive(Serialize, Deserialize, Clone, PartialEq, Debug, Eq)] -pub enum Modifier { - Control, - Shift, - Alt, -} - -impl Modifier { - pub fn from_bitflags(m: KeyModifiers) -> Option<Vec<Self>> { - match m.bits() { - 0b0000_0001 => Some(vec![Modifier::Shift]), - 0b0000_0100 => Some(vec![Modifier::Alt]), - 0b0000_0010 => Some(vec![Modifier::Control]), - 3 => Some(vec![Modifier::Control, Modifier::Shift]), - 5 => Some(vec![Modifier::Alt, Modifier::Shift]), - 6 => Some(vec![Modifier::Control, Modifier::Alt]), - 7 => Some(vec![Modifier::Control, Modifier::Alt, Modifier::Shift]), - _ => None, - } - } -} - -#[derive(Serialize, Deserialize, Clone, PartialEq, Debug, Eq)] -pub struct Key(pub String); - -impl From<&str> for Key { - fn from(key: &str) -> Self { - Self(key.to_string()) - } -} - -impl From<KeyCode> for Key { - fn from(item: KeyCode) -> Self { - match item { - KeyCode::Char(' ') => Key::from("SPACE"), - KeyCode::Char(c) => Key(c.to_string().to_ascii_uppercase()), - KeyCode::Backspace => Key::from("BACKSPACE"), - KeyCode::Enter => Key::from("ENTER"), - KeyCode::Left => Key::from("LEFT"), - KeyCode::Right => Key::from("RIGHT"), - KeyCode::Up => Key::from("UP"), - KeyCode::Down => Key::from("DOWN"), - KeyCode::Home => Key::from("HOME"), - KeyCode::End => Key::from("END"), - KeyCode::PageUp => Key::from("PAGEUP"), - KeyCode::PageDown => Key::from("PAGEDOWN"), - KeyCode::Tab => Key::from("TAB"), - KeyCode::BackTab => Key::from("BACKTAB"), - KeyCode::Delete => Key::from("DELETE"), - KeyCode::Insert => Key::from("INSERT"), - KeyCode::F(num) => Key(format!("F{num}")), - KeyCode::Null => Key::from("NULL"), - KeyCode::Esc => Key::from("ESCAPE"), - } - } -} - -#[derive(Serialize, Deserialize, Clone, PartialEq, Debug, Eq)] -pub struct Bind { - pub key: Key, - pub modifiers: Option<Vec<Modifier>>, -} - -#[derive(Serialize, Deserialize, Clone)] -pub struct Hotkey { - pub up: Bind, - pub down: Bind, - pub left: Bind, - pub right: Bind, - pub play_pause: Bind, - pub volume_up: Bind, - pub volume_down: Bind, - pub next: Bind, - pub previous: Bind, - pub seek_forward: Bind, - pub seek_backward: Bind, - pub clear: Bind, - pub clear_except_playing: Bind, - pub delete: Bind, - pub random: Bind, - pub refresh_database: Bind, - pub quit: Bind, -} - -#[derive(Serialize, Deserialize, Clone)] -pub struct Config { - pub paths: Vec<String>, - pub output_device: String, - pub volume: u16, -} - -#[derive(Serialize, Deserialize, Clone, Copy)] -pub struct Colors { - pub number: Color, - pub name: Color, - pub album: Color, - pub artist: Color, - pub seeker: Color, -} - -#[derive(Serialize, Deserialize, Clone)] -pub struct Toml { - pub config: Config, - pub colors: Colors, - pub hotkey: Hotkey, -} - -impl Toml { - pub fn new() -> Self { - let file = if TOML_DIR.exists() { - fs::read_to_string(TOML_DIR.as_path()).unwrap() - } else { - let toml = Toml { - config: Config { - paths: Vec::new(), - output_device: String::new(), - volume: 15, - }, - colors: Colors { - number: Color::Green, - name: Color::Cyan, - album: Color::Magenta, - artist: Color::Blue, - seeker: Color::White, - }, - hotkey: Hotkey { - up: Bind { - key: Key::from("K"), - modifiers: None, - }, - down: Bind { - key: Key::from("J"), - modifiers: None, - }, - left: Bind { - key: Key::from("H"), - modifiers: None, - }, - right: Bind { - key: Key::from("L"), - modifiers: None, - }, - play_pause: Bind { - key: Key::from("SPACE"), - modifiers: None, - }, - volume_up: Bind { - key: Key::from("W"), - modifiers: None, - }, - volume_down: Bind { - key: Key::from("S"), - modifiers: None, - }, - seek_forward: Bind { - key: Key::from("E"), - modifiers: None, - }, - seek_backward: Bind { - key: Key::from("Q"), - modifiers: None, - }, - next: Bind { - key: Key::from("D"), - modifiers: None, - }, - previous: Bind { - key: Key::from("A"), - modifiers: None, - }, - clear: Bind { - key: Key::from("C"), - modifiers: None, - }, - clear_except_playing: Bind { - key: Key::from("C"), - modifiers: Some(vec![Modifier::Shift]), - }, - delete: Bind { - key: Key::from("X"), - modifiers: None, - }, - random: Bind { - key: Key::from("R"), - modifiers: None, - }, - refresh_database: Bind { - key: Key::from("U"), - modifiers: None, - }, - quit: Bind { - key: Key::from("C"), - modifiers: Some(vec![Modifier::Control]), - }, - }, - }; - - match toml::to_string_pretty(&toml) { - Ok(toml) => toml, - Err(err) => panic!("{}", &err), - } - }; - - match toml::from_str(&file) { - Ok(toml) => toml, - Err(err) => { - //TODO: parse and describe error to user? - panic!("{:#?}", &err); - } - } - } - pub fn check_paths(self) -> Result<Self, String> { - for path in &self.config.paths { - let path = Path::new(&path); - if !path.exists() { - return Err(format!("{} is not a valid path.", path.to_string_lossy())); - } - } - Ok(self) - } - pub fn add_path(&mut self, path: String) { - if !self.config.paths.contains(&path) { - self.config.paths.push(path); - self.write(); - } - } - pub fn set_volume(&mut self, vol: u16) { - self.config.volume = vol; - self.write(); - } - pub fn set_output_device(&mut self, device: String) { - self.config.output_device = device; - self.write(); - } - pub fn write(&self) { - let toml = toml::to_string(&self).expect("Failed to write toml file."); - fs::write(TOML_DIR.as_path(), toml).expect("Could not write toml flie."); - } - pub fn reset(&mut self) { - *self = Self::default(); - self.write(); - } -} - -impl Default for Toml { - fn default() -> Self { - Toml::new() - } -} diff --git a/gonk/src/widgets/list.rs b/gonk/src/widgets/list.rs index 0407393f..b0ce3b93 100644 --- a/gonk/src/widgets/list.rs +++ b/gonk/src/widgets/list.rs @@ -79,7 +79,7 @@ pub struct List<'a> { } impl<'a> List<'a> { - pub fn new(items: Vec<ListItem<'a>>) -> List<'a> { + pub fn new(items: &[ListItem<'a>]) -> List<'a> { List { block: None, style: Style::default(), @@ -114,7 +114,7 @@ impl<'a> List<'a> { fn get_items_bounds(&self, selection: usize, terminal_height: usize) -> (usize, usize) { let mut real_end = 0; let mut height = 0; - for item in self.items.iter() { + for item in &self.items { if height + item.height() > terminal_height { break; } @@ -180,16 +180,13 @@ impl<'a> StatefulWidget for List<'a> { .skip(start) .take(end - start) { - let (x, y) = match self.start_corner { - Corner::BottomLeft => { - current_height += item.height() as u16; - (list_area.left(), list_area.bottom() - current_height) - } - _ => { - let pos = (list_area.left(), list_area.top() + current_height); - current_height += item.height() as u16; - pos - } + let (x, y) = if self.start_corner == Corner::BottomLeft { + current_height += item.height() as u16; + (list_area.left(), list_area.bottom() - current_height) + } else { + let pos = (list_area.left(), list_area.top() + current_height); + current_height += item.height() as u16; + pos }; let area = Rect { diff --git a/gonk/src/widgets/table.rs b/gonk/src/widgets/table.rs index 77608112..9e081801 100644 --- a/gonk/src/widgets/table.rs +++ b/gonk/src/widgets/table.rs @@ -48,7 +48,7 @@ impl<'a> Row<'a> { { Self { height: 1, - cells: cells.into_iter().map(|c| c.into()).collect(), + cells: cells.into_iter().map(Into::into).collect(), style: Style::default(), bottom_margin: 0, } @@ -143,7 +143,7 @@ impl<'a> Table<'a> { fn get_columns_widths(&self, max_width: u16, has_selection: bool) -> Vec<u16> { let mut constraints = Vec::with_capacity(self.widths.len() * 2 + 1); if has_selection { - let highlight_symbol_width = self.highlight_symbol.map(|s| s.len() as u16).unwrap_or(0); + let highlight_symbol_width = self.highlight_symbol.map_or(0, |s| s.len() as u16); constraints.push(Constraint::Length(highlight_symbol_width)); } for constraint in self.widths { @@ -174,7 +174,7 @@ impl<'a> Table<'a> { let len = self.rows.len(); let selection = selected.unwrap_or(0).min(len.saturating_sub(1)); - for item in self.rows.iter() { + for item in &self.rows { if height + item.height as usize > terminal_height as usize { break; } @@ -327,7 +327,7 @@ impl<'a> StatefulWidget for Table<'a> { height: table_row.height, }; buf.set_style(table_row_area, table_row.style); - let is_selected = state.selected.map(|s| s == i).unwrap_or(false); + let is_selected = state.selected.map_or(false, |s| s == i); let table_row_start_col = if has_selection { let symbol = if is_selected { highlight_symbol From 7ddee68bee45d00a34def36591b79c47542ddcb8 Mon Sep 17 00:00:00 2001 From: zX3no <dr_xeno@hotmail.com.au> Date: Sun, 3 Jul 2022 10:59:08 +0930 Subject: [PATCH 16/40] fix: update now works when from == to --- gonk-player/src/lib.rs | 11 ----------- gonk-player/src/sample_rate.rs | 11 ++++++++--- 2 files changed, 8 insertions(+), 14 deletions(-) diff --git a/gonk-player/src/lib.rs b/gonk-player/src/lib.rs index 17ce6c91..cc924ccd 100644 --- a/gonk-player/src/lib.rs +++ b/gonk-player/src/lib.rs @@ -43,18 +43,7 @@ pub struct Player { } impl Player { - //TODO: get device from toml file pub fn new(_device: String, volume: u16, _songs: &[Song]) -> Self { - // let host_id = cpal::default_host().id(); - // let host = cpal::host_from_id(host_id).unwrap(); - // let mut devices: Vec<Device> = host.devices().unwrap().collect(); - // devices.retain(|host| host.name().unwrap() == device); - - // let device = if devices.is_empty() { - // cpal::default_host().default_output_device().unwrap() - // } else { - // devices.remove(0) - // }; let device = cpal::default_host().default_output_device().unwrap(); let config = device.default_output_config().unwrap(); diff --git a/gonk-player/src/sample_rate.rs b/gonk-player/src/sample_rate.rs index de907ff8..0fe1545f 100644 --- a/gonk-player/src/sample_rate.rs +++ b/gonk-player/src/sample_rate.rs @@ -76,11 +76,16 @@ impl SampleRateConverter { } pub fn update(&mut self, mut input: IntoIter<f32>) { - let current_frame = vec![input.next().unwrap(), input.next().unwrap()]; - let next_frame = vec![input.next().unwrap(), input.next().unwrap()]; - self.input = input; + let (current_frame, next_frame) = if self.from == self.to { + (Vec::new(), Vec::new()) + } else { + let current = vec![input.next().unwrap(), input.next().unwrap()]; + let next = vec![input.next().unwrap(), input.next().unwrap()]; + (current, next) + }; self.current_frame = current_frame; self.next_frame = next_frame; + self.input = input; self.current_frame_pos_in_chunk = 0; self.next_output_frame_pos_in_chunk = 0; } From d72f7ea1885b3a470e3c828b925e728d8c2debdc Mon Sep 17 00:00:00 2001 From: zX3no <dr_xeno@hotmail.com.au> Date: Sun, 3 Jul 2022 14:56:16 +0930 Subject: [PATCH 17/40] cleanup --- gonk-player/src/lib.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/gonk-player/src/lib.rs b/gonk-player/src/lib.rs index cc924ccd..f09cc77d 100644 --- a/gonk-player/src/lib.rs +++ b/gonk-player/src/lib.rs @@ -103,6 +103,7 @@ impl Player { generator, } } + //TODO: Run update loop in the player not in the client. pub fn update(&mut self) { if self.generator.read().unwrap().is_done() { self.next(); From 8aea6f806a38742ccd7565cf960041ee3b343e2a Mon Sep 17 00:00:00 2001 From: zX3no <dr_xeno@hotmail.com.au> Date: Wed, 6 Jul 2022 14:34:17 +0930 Subject: [PATCH 18/40] simplified resampler --- gonk-player/src/lib.rs | 1 + gonk-player/src/sample_rate.rs | 61 ++++++++++++++-------------------- 2 files changed, 26 insertions(+), 36 deletions(-) diff --git a/gonk-player/src/lib.rs b/gonk-player/src/lib.rs index f09cc77d..72b61c26 100644 --- a/gonk-player/src/lib.rs +++ b/gonk-player/src/lib.rs @@ -1,3 +1,4 @@ +#![feature(const_fn_floating_point_arithmetic)] use cpal::{ traits::{HostTrait, StreamTrait}, StreamError, diff --git a/gonk-player/src/sample_rate.rs b/gonk-player/src/sample_rate.rs index 0fe1545f..305b5d8c 100644 --- a/gonk-player/src/sample_rate.rs +++ b/gonk-player/src/sample_rate.rs @@ -10,6 +10,11 @@ const fn gcd(a: u32, b: u32) -> u32 { } } +#[inline] +const fn lerp(a: f32, b: f32, t: f32) -> f32 { + return a + t * (b - a); +} + /// Iterator that converts from a certain sample rate to another. pub struct SampleRateConverter { /// The iterator that gives us samples. @@ -28,7 +33,7 @@ pub struct SampleRateConverter { /// This counter is incremented (modulo `to`) every time the iterator is called. next_output_frame_pos_in_chunk: u32, /// The buffer containing the samples waiting to be output. - output_buffer: Vec<f32>, + output_buffer: Option<f32>, } impl SampleRateConverter { @@ -57,7 +62,7 @@ impl SampleRateConverter { next_output_frame_pos_in_chunk: 0, current_frame: first_samples, next_frame: next_samples, - output_buffer: Vec::with_capacity(1), + output_buffer: None, } } @@ -66,12 +71,11 @@ impl SampleRateConverter { mem::swap(&mut self.current_frame, &mut self.next_frame); self.next_frame.clear(); - for _ in 0..2 { - if let Some(i) = self.input.next() { - self.next_frame.push(i); - } else { - break; - } + if let Some(i) = self.input.next() { + self.next_frame.push(i); + } + if let Some(i) = self.input.next() { + self.next_frame.push(i); } } @@ -97,8 +101,8 @@ impl SampleRateConverter { } // Short circuit if there are some samples waiting. - if !self.output_buffer.is_empty() { - return Some(self.output_buffer.remove(0)); + if let Some(output) = self.output_buffer.take() { + return Some(output); } // The frame we are going to return from this function will be a linear interpolation @@ -130,40 +134,25 @@ impl SampleRateConverter { // Merging `self.current_frame` and `self.next_frame` into `self.output_buffer`. // Note that `self.output_buffer` can be truncated if there is not enough data in // `self.next_frame`. - let mut result = None; let numerator = (self.from * self.next_output_frame_pos_in_chunk) % self.to; - for (off, (cur, next)) in self - .current_frame - .iter() - .zip(self.next_frame.iter()) - .enumerate() - { - let sample = cur + (next - cur) * numerator as f32 / self.to as f32; - - if off == 0 { - result = Some(sample); - } else { - self.output_buffer.push(sample); - } - } // Incrementing the counter for the next iteration. self.next_output_frame_pos_in_chunk += 1; - if result.is_some() { - result - } else { - debug_assert!(self.next_frame.is_empty()); - - // draining `self.current_frame` - if !self.current_frame.is_empty() { + if self.next_frame.is_empty() { + if self.current_frame.is_empty() { + None + } else { let r = Some(self.current_frame.remove(0)); - mem::swap(&mut self.output_buffer, &mut self.current_frame); - self.current_frame.clear(); + let current_frame = self.current_frame[0]; + self.output_buffer = Some(current_frame); r - } else { - None } + } else { + let ratio = numerator as f32 / self.to as f32; + let sample = lerp(self.current_frame[1], self.next_frame[1], ratio); + self.output_buffer = Some(sample); + Some(lerp(self.current_frame[0], self.next_frame[0], ratio)) } } } From 5dfddc0b8d27db7a465723c62c12ab87e08930f7 Mon Sep 17 00:00:00 2001 From: zX3no <dr_xeno@hotmail.com.au> Date: Wed, 6 Jul 2022 14:56:19 +0930 Subject: [PATCH 19/40] cleanup --- gonk-player/src/sample_processor.rs | 2 +- gonk-player/src/sample_rate.rs | 80 +++++++++++++++-------------- 2 files changed, 43 insertions(+), 39 deletions(-) diff --git a/gonk-player/src/sample_processor.rs b/gonk-player/src/sample_processor.rs index 29ce38c7..1694a08a 100644 --- a/gonk-player/src/sample_processor.rs +++ b/gonk-player/src/sample_processor.rs @@ -187,7 +187,7 @@ impl Processor { let mut buffer = SampleBuffer::<f32>::new(self.capacity, self.spec); buffer.copy_interleaved_ref(decoded); - self.converter.update(buffer.samples().to_vec().into_iter()); + self.converter.update(buffer.samples().to_vec()); //Update elapsed let ts = packet.ts(); diff --git a/gonk-player/src/sample_rate.rs b/gonk-player/src/sample_rate.rs index 305b5d8c..1bdac854 100644 --- a/gonk-player/src/sample_rate.rs +++ b/gonk-player/src/sample_rate.rs @@ -18,11 +18,11 @@ const fn lerp(a: f32, b: f32, t: f32) -> f32 { /// Iterator that converts from a certain sample rate to another. pub struct SampleRateConverter { /// The iterator that gives us samples. - input: IntoIter<f32>, - /// We convert chunks of `from` samples into chunks of `to` samples. - from: u32, - /// We convert chunks of `from` samples into chunks of `to` samples. - to: u32, + sample_buffer: IntoIter<f32>, + ///Input sample rate - interpolation factor + input: u32, + ///Output sample rate - decimation factor + output: u32, /// One sample per channel, extracted from `input`. current_frame: Vec<f32>, /// Position of `current_sample` modulo `from`. @@ -55,9 +55,9 @@ impl SampleRateConverter { }; SampleRateConverter { - input, - from: from_rate / gcd, - to: to_rate / gcd, + sample_buffer: input, + input: from_rate / gcd, + output: to_rate / gcd, current_frame_pos_in_chunk: 0, next_output_frame_pos_in_chunk: 0, current_frame: first_samples, @@ -71,33 +71,39 @@ impl SampleRateConverter { mem::swap(&mut self.current_frame, &mut self.next_frame); self.next_frame.clear(); - if let Some(i) = self.input.next() { + if let Some(i) = self.sample_buffer.next() { self.next_frame.push(i); } - if let Some(i) = self.input.next() { + if let Some(i) = self.sample_buffer.next() { self.next_frame.push(i); } } - pub fn update(&mut self, mut input: IntoIter<f32>) { - let (current_frame, next_frame) = if self.from == self.to { - (Vec::new(), Vec::new()) + pub fn update(&mut self, input: Vec<f32>) { + self.sample_buffer = input.into_iter(); + + if self.input == self.output { + self.current_frame = Vec::new(); + self.next_frame = Vec::new(); } else { - let current = vec![input.next().unwrap(), input.next().unwrap()]; - let next = vec![input.next().unwrap(), input.next().unwrap()]; - (current, next) + self.current_frame = vec![ + self.sample_buffer.next().unwrap(), + self.sample_buffer.next().unwrap(), + ]; + self.next_frame = vec![ + self.sample_buffer.next().unwrap(), + self.sample_buffer.next().unwrap(), + ]; }; - self.current_frame = current_frame; - self.next_frame = next_frame; - self.input = input; + self.current_frame_pos_in_chunk = 0; self.next_output_frame_pos_in_chunk = 0; } pub fn next(&mut self) -> Option<f32> { // the algorithm below doesn't work if `self.from == self.to` - if self.from == self.to { - return self.input.next(); + if self.input == self.output { + return self.sample_buffer.next(); } // Short circuit if there are some samples waiting. @@ -107,51 +113,49 @@ impl SampleRateConverter { // The frame we are going to return from this function will be a linear interpolation // between `self.current_frame` and `self.next_frame`. - - if self.next_output_frame_pos_in_chunk == self.to { + if self.next_output_frame_pos_in_chunk == self.output { // If we jump to the next frame, we reset the whole state. self.next_output_frame_pos_in_chunk = 0; self.next_input_frame(); - while self.current_frame_pos_in_chunk != self.from { + while self.current_frame_pos_in_chunk != self.input { self.next_input_frame(); } self.current_frame_pos_in_chunk = 0; } else { // Finding the position of the first sample of the linear interpolation. let req_left_sample = - (self.from * self.next_output_frame_pos_in_chunk / self.to) % self.from; + (self.input * self.next_output_frame_pos_in_chunk / self.output) % self.input; // Advancing `self.current_frame`, `self.next_frame` and // `self.current_frame_pos_in_chunk` until the latter variable // matches `req_left_sample`. while self.current_frame_pos_in_chunk != req_left_sample { self.next_input_frame(); - debug_assert!(self.current_frame_pos_in_chunk < self.from); + debug_assert!(self.current_frame_pos_in_chunk < self.input); } } // Merging `self.current_frame` and `self.next_frame` into `self.output_buffer`. // Note that `self.output_buffer` can be truncated if there is not enough data in // `self.next_frame`. - let numerator = (self.from * self.next_output_frame_pos_in_chunk) % self.to; + let numerator = (self.input * self.next_output_frame_pos_in_chunk) % self.output; // Incrementing the counter for the next iteration. self.next_output_frame_pos_in_chunk += 1; + if self.current_frame.is_empty() && self.next_frame.is_empty() { + return None; + } + if self.next_frame.is_empty() { - if self.current_frame.is_empty() { - None - } else { - let r = Some(self.current_frame.remove(0)); - let current_frame = self.current_frame[0]; - self.output_buffer = Some(current_frame); - r - } + let r = self.current_frame.remove(0); + self.output_buffer = self.current_frame.get(0).cloned(); + self.current_frame.clear(); + Some(r) } else { - let ratio = numerator as f32 / self.to as f32; - let sample = lerp(self.current_frame[1], self.next_frame[1], ratio); - self.output_buffer = Some(sample); + let ratio = numerator as f32 / self.output as f32; + self.output_buffer = Some(lerp(self.current_frame[1], self.next_frame[1], ratio)); Some(lerp(self.current_frame[0], self.next_frame[0], ratio)) } } From c680699feb2367b536ba9827190e45c8b6481cdc Mon Sep 17 00:00:00 2001 From: zX3no <dr_xeno@hotmail.com.au> Date: Wed, 6 Jul 2022 15:09:28 +0930 Subject: [PATCH 20/40] cleanup --- gonk-player/src/sample_rate.rs | 92 ++++++++++++++++------------------ 1 file changed, 43 insertions(+), 49 deletions(-) diff --git a/gonk-player/src/sample_rate.rs b/gonk-player/src/sample_rate.rs index 1bdac854..bd0b7ce4 100644 --- a/gonk-player/src/sample_rate.rs +++ b/gonk-player/src/sample_rate.rs @@ -1,4 +1,3 @@ -//https://github.com/RustAudio/rodio/blob/master/src/conversions/sample_rate.rs use std::{mem, vec::IntoIter}; #[inline] @@ -15,49 +14,51 @@ const fn lerp(a: f32, b: f32, t: f32) -> f32 { return a + t * (b - a); } -/// Iterator that converts from a certain sample rate to another. pub struct SampleRateConverter { /// The iterator that gives us samples. - sample_buffer: IntoIter<f32>, - ///Input sample rate - interpolation factor + buffer: IntoIter<f32>, + + ///Input sample rate - interpolation factor. input: u32, - ///Output sample rate - decimation factor + ///Output sample rate - decimation factor. output: u32, + /// One sample per channel, extracted from `input`. current_frame: Vec<f32>, /// Position of `current_sample` modulo `from`. + /// + /// `0..input / gcd` current_frame_pos_in_chunk: u32, + /// The samples right after `current_sample` (one per channel), extracted from `input`. next_frame: Vec<f32>, /// The position of the next sample that the iterator should return, modulo `to`. /// This counter is incremented (modulo `to`) every time the iterator is called. + /// + /// `0..output / gcd` next_output_frame_pos_in_chunk: u32, - /// The buffer containing the samples waiting to be output. + output_buffer: Option<f32>, } impl SampleRateConverter { - pub fn new(mut input: IntoIter<f32>, from_rate: u32, to_rate: u32) -> SampleRateConverter { - assert!(from_rate >= 1); - assert!(to_rate >= 1); + pub fn new(mut buffer: IntoIter<f32>, input: u32, output: u32) -> SampleRateConverter { + assert!(input >= 1); + assert!(output >= 1); - // finding greatest common divisor - let gcd = gcd(from_rate, to_rate); - - let (first_samples, next_samples) = if from_rate == to_rate { - // if `from` == `to` == 1, then we just pass through - debug_assert_eq!(from_rate, gcd); + let gcd = gcd(input, output); + let (first_samples, next_samples) = if input == output { (Vec::new(), Vec::new()) } else { - let first = vec![input.next().unwrap(), input.next().unwrap()]; - let next = vec![input.next().unwrap(), input.next().unwrap()]; + let first = vec![buffer.next().unwrap(), buffer.next().unwrap()]; + let next = vec![buffer.next().unwrap(), buffer.next().unwrap()]; (first, next) }; SampleRateConverter { - sample_buffer: input, - input: from_rate / gcd, - output: to_rate / gcd, + buffer, + input: input / gcd, + output: output / gcd, current_frame_pos_in_chunk: 0, next_output_frame_pos_in_chunk: 0, current_frame: first_samples, @@ -66,49 +67,42 @@ impl SampleRateConverter { } } - fn next_input_frame(&mut self) { - self.current_frame_pos_in_chunk += 1; - - mem::swap(&mut self.current_frame, &mut self.next_frame); - self.next_frame.clear(); - if let Some(i) = self.sample_buffer.next() { - self.next_frame.push(i); - } - if let Some(i) = self.sample_buffer.next() { - self.next_frame.push(i); - } - } - pub fn update(&mut self, input: Vec<f32>) { - self.sample_buffer = input.into_iter(); + self.buffer = input.into_iter(); if self.input == self.output { self.current_frame = Vec::new(); self.next_frame = Vec::new(); } else { - self.current_frame = vec![ - self.sample_buffer.next().unwrap(), - self.sample_buffer.next().unwrap(), - ]; - self.next_frame = vec![ - self.sample_buffer.next().unwrap(), - self.sample_buffer.next().unwrap(), - ]; + self.current_frame = vec![self.buffer.next().unwrap(), self.buffer.next().unwrap()]; + self.next_frame = vec![self.buffer.next().unwrap(), self.buffer.next().unwrap()]; }; self.current_frame_pos_in_chunk = 0; self.next_output_frame_pos_in_chunk = 0; } - pub fn next(&mut self) -> Option<f32> { - // the algorithm below doesn't work if `self.from == self.to` - if self.input == self.output { - return self.sample_buffer.next(); + fn next_input_frame(&mut self) { + mem::swap(&mut self.current_frame, &mut self.next_frame); + + self.next_frame.clear(); + + if let Some(sample) = self.buffer.next() { + self.next_frame.push(sample); } - // Short circuit if there are some samples waiting. - if let Some(output) = self.output_buffer.take() { - return Some(output); + if let Some(sample) = self.buffer.next() { + self.next_frame.push(sample); + } + + self.current_frame_pos_in_chunk += 1; + } + + pub fn next(&mut self) -> Option<f32> { + if self.input == self.output { + return self.buffer.next(); + } else if let Some(sample) = self.output_buffer.take() { + return Some(sample); } // The frame we are going to return from this function will be a linear interpolation From 8af6c70f65cf5fcf7f2b03e3ca1fb5853f93b9d8 Mon Sep 17 00:00:00 2001 From: zX3no <dr_xeno@hotmail.com.au> Date: Wed, 6 Jul 2022 15:16:31 +0930 Subject: [PATCH 21/40] fix: left over error from merge --- gonk/src/main.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gonk/src/main.rs b/gonk/src/main.rs index 14744086..8187e489 100644 --- a/gonk/src/main.rs +++ b/gonk/src/main.rs @@ -137,7 +137,7 @@ fn main() { let volume = query::volume(); //40ms - let player = thread::spawn(move || Player::new(volume, &cache, &Vec::new())); + let player = thread::spawn(move || Player::new(String::new(), volume, &cache)); //3ms let mut browser = Browser::new(); From 6653b2d678b191688970c8b342cf0405c5e8632c Mon Sep 17 00:00:00 2001 From: zX3no <dr_xeno@hotmail.com.au> Date: Thu, 7 Jul 2022 10:51:15 +0930 Subject: [PATCH 22/40] removed update function --- gonk-player/src/sample_processor.rs | 15 +++++++++++--- gonk-player/src/sample_rate.rs | 32 ++++++++--------------------- 2 files changed, 21 insertions(+), 26 deletions(-) diff --git a/gonk-player/src/sample_processor.rs b/gonk-player/src/sample_processor.rs index 1694a08a..1e53135f 100644 --- a/gonk-player/src/sample_processor.rs +++ b/gonk-player/src/sample_processor.rs @@ -101,10 +101,13 @@ pub struct Processor { pub duration: Duration, pub elapsed: Duration, pub volume: f32, + + pub input: u32, + pub output: u32, } impl Processor { - pub fn new(sample_rate: u32, path: &Path, volume: f32) -> Self { + pub fn new(output_rate: u32, path: &Path, volume: f32) -> Self { let source = Box::new(File::open(path).unwrap()); let mss = MediaSourceStream::new(source, Default::default()); @@ -155,11 +158,13 @@ impl Processor { converter: SampleRateConverter::new( sample_buffer.samples().to_vec().into_iter(), spec.rate, - sample_rate, + output_rate, ), finished: false, left: true, volume, + input: spec.rate, + output: output_rate, } } pub fn next_sample(&mut self) -> f32 { @@ -187,7 +192,11 @@ impl Processor { let mut buffer = SampleBuffer::<f32>::new(self.capacity, self.spec); buffer.copy_interleaved_ref(decoded); - self.converter.update(buffer.samples().to_vec()); + self.converter = SampleRateConverter::new( + buffer.samples().to_vec().into_iter(), + self.input, + self.output, + ); //Update elapsed let ts = packet.ts(); diff --git a/gonk-player/src/sample_rate.rs b/gonk-player/src/sample_rate.rs index bd0b7ce4..414cf5f7 100644 --- a/gonk-player/src/sample_rate.rs +++ b/gonk-player/src/sample_rate.rs @@ -43,16 +43,17 @@ pub struct SampleRateConverter { impl SampleRateConverter { pub fn new(mut buffer: IntoIter<f32>, input: u32, output: u32) -> SampleRateConverter { - assert!(input >= 1); - assert!(output >= 1); + debug_assert!(input >= 1); + debug_assert!(output >= 1); let gcd = gcd(input, output); - let (first_samples, next_samples) = if input == output { + let (current_frame, next_frame) = if input == output { (Vec::new(), Vec::new()) } else { - let first = vec![buffer.next().unwrap(), buffer.next().unwrap()]; - let next = vec![buffer.next().unwrap(), buffer.next().unwrap()]; - (first, next) + ( + vec![buffer.next().unwrap(), buffer.next().unwrap()], + vec![buffer.next().unwrap(), buffer.next().unwrap()], + ) }; SampleRateConverter { @@ -61,27 +62,12 @@ impl SampleRateConverter { output: output / gcd, current_frame_pos_in_chunk: 0, next_output_frame_pos_in_chunk: 0, - current_frame: first_samples, - next_frame: next_samples, + current_frame, + next_frame, output_buffer: None, } } - pub fn update(&mut self, input: Vec<f32>) { - self.buffer = input.into_iter(); - - if self.input == self.output { - self.current_frame = Vec::new(); - self.next_frame = Vec::new(); - } else { - self.current_frame = vec![self.buffer.next().unwrap(), self.buffer.next().unwrap()]; - self.next_frame = vec![self.buffer.next().unwrap(), self.buffer.next().unwrap()]; - }; - - self.current_frame_pos_in_chunk = 0; - self.next_output_frame_pos_in_chunk = 0; - } - fn next_input_frame(&mut self) { mem::swap(&mut self.current_frame, &mut self.next_frame); From d36cb2081b82d9088e66f602ae11246eacf2fbc4 Mon Sep 17 00:00:00 2001 From: zX3no <dr_xeno@hotmail.com.au> Date: Thu, 7 Jul 2022 10:58:45 +0930 Subject: [PATCH 23/40] changed mem::swap to mem::take --- gonk-player/src/sample_rate.rs | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/gonk-player/src/sample_rate.rs b/gonk-player/src/sample_rate.rs index 414cf5f7..86dca38e 100644 --- a/gonk-player/src/sample_rate.rs +++ b/gonk-player/src/sample_rate.rs @@ -69,9 +69,7 @@ impl SampleRateConverter { } fn next_input_frame(&mut self) { - mem::swap(&mut self.current_frame, &mut self.next_frame); - - self.next_frame.clear(); + self.current_frame = mem::take(&mut self.next_frame); if let Some(sample) = self.buffer.next() { self.next_frame.push(sample); From 521d38d4474a944a03a638d3fa75bffaf32ef277 Mon Sep 17 00:00:00 2001 From: zX3no <dr_xeno@hotmail.com.au> Date: Thu, 7 Jul 2022 14:34:47 +0930 Subject: [PATCH 24/40] fix build failing because of nightly feature --- gonk-player/src/lib.rs | 1 - gonk-player/src/sample_rate.rs | 2 +- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/gonk-player/src/lib.rs b/gonk-player/src/lib.rs index 312e461c..ae6d6a33 100644 --- a/gonk-player/src/lib.rs +++ b/gonk-player/src/lib.rs @@ -1,4 +1,3 @@ -#![feature(const_fn_floating_point_arithmetic)] use cpal::{ traits::{HostTrait, StreamTrait}, StreamError, diff --git a/gonk-player/src/sample_rate.rs b/gonk-player/src/sample_rate.rs index 86dca38e..6fc0a269 100644 --- a/gonk-player/src/sample_rate.rs +++ b/gonk-player/src/sample_rate.rs @@ -10,7 +10,7 @@ const fn gcd(a: u32, b: u32) -> u32 { } #[inline] -const fn lerp(a: f32, b: f32, t: f32) -> f32 { +fn lerp(a: f32, b: f32, t: f32) -> f32 { return a + t * (b - a); } From 334718a49a46e342558f7facb5feb65459bac9d3 Mon Sep 17 00:00:00 2001 From: zX3no <dr_xeno@hotmail.com.au> Date: Thu, 7 Jul 2022 14:54:25 +0930 Subject: [PATCH 25/40] clippy --- gonk-database/src/database.rs | 2 +- gonk-database/src/lib.rs | 7 ++----- gonk-database/src/query.rs | 4 ++-- gonk-player/src/sample_rate.rs | 4 ++-- gonk-player/src/song.rs | 2 +- gonk/src/main.rs | 8 +++++++- gonk/src/playlist.rs | 8 ++++---- gonk/src/queue.rs | 6 +++--- gonk/src/search.rs | 16 +++++----------- gonk/src/settings.rs | 13 ++++++++----- gonk/src/status_bar.rs | 7 +++---- 11 files changed, 38 insertions(+), 39 deletions(-) diff --git a/gonk-database/src/database.rs b/gonk-database/src/database.rs index 7c4f0bec..68997060 100644 --- a/gonk-database/src/database.rs +++ b/gonk-database/src/database.rs @@ -34,7 +34,7 @@ impl Database { } } - self.handle = Some(thread::spawn(|| rescan_folders())); + self.handle = Some(thread::spawn(rescan_folders)); } pub fn state(&mut self) -> State { diff --git a/gonk-database/src/lib.rs b/gonk-database/src/lib.rs index 6b36739d..aaf91424 100644 --- a/gonk-database/src/lib.rs +++ b/gonk-database/src/lib.rs @@ -127,10 +127,7 @@ pub fn collect_songs(path: &str) -> Vec<Song> { }) .collect(); - paths - .par_iter() - .flat_map(|path| Song::from(&path)) - .collect() + paths.par_iter().flat_map(|path| Song::from(path)).collect() } pub fn rescan_folders() { @@ -145,7 +142,7 @@ pub fn rescan_folders() { } pub fn add_folder(folder: &str) { - let folder = folder.replace("\\", "/"); + let folder = folder.replace('\\', "/"); conn() .execute( diff --git a/gonk-database/src/query.rs b/gonk-database/src/query.rs index a6244361..249459aa 100644 --- a/gonk-database/src/query.rs +++ b/gonk-database/src/query.rs @@ -50,7 +50,7 @@ pub fn folders() -> Vec<String> { } pub fn remove_folder(path: &str) -> Result<(), &str> { - let path = path.replace("\\", "/"); + let path = path.replace('\\', "/"); let conn = conn(); conn.execute("DELETE FROM song WHERE folder = ?", [&path]) @@ -146,7 +146,7 @@ pub fn songs_from_ids(ids: &[usize]) -> Vec<Song> { //Remove the last 'UNION ALL' let sql = &sql[..sql.len() - 10]; - let mut stmt = conn.prepare(&sql).unwrap(); + let mut stmt = conn.prepare(sql).unwrap(); stmt.query_map([], |row| Ok(song(row))) .unwrap() diff --git a/gonk-player/src/sample_rate.rs b/gonk-player/src/sample_rate.rs index 6fc0a269..df7738c6 100644 --- a/gonk-player/src/sample_rate.rs +++ b/gonk-player/src/sample_rate.rs @@ -11,7 +11,7 @@ const fn gcd(a: u32, b: u32) -> u32 { #[inline] fn lerp(a: f32, b: f32, t: f32) -> f32 { - return a + t * (b - a); + a + t * (b - a) } pub struct SampleRateConverter { @@ -128,7 +128,7 @@ impl SampleRateConverter { if self.next_frame.is_empty() { let r = self.current_frame.remove(0); - self.output_buffer = self.current_frame.get(0).cloned(); + self.output_buffer = self.current_frame.first().cloned(); self.current_frame.clear(); Some(r) } else { diff --git a/gonk-player/src/song.rs b/gonk-player/src/song.rs index 4ce6ab0b..b4d1d764 100644 --- a/gonk-player/src/song.rs +++ b/gonk-player/src/song.rs @@ -110,7 +110,7 @@ impl Song { update_metadata(metadata); } - if song.artist == String::from("Unknown Artist") { + if song.artist == *"Unknown Artist" { song.artist = backup_artist; } diff --git a/gonk/src/main.rs b/gonk/src/main.rs index 8187e489..02674abd 100644 --- a/gonk/src/main.rs +++ b/gonk/src/main.rs @@ -383,7 +383,13 @@ fn main() { query::set_volume(player.volume); - let ids: Vec<usize> = player.songs.data.iter().flat_map(|song| song.id).collect(); + let ids: Vec<usize> = player + .songs + .data + .iter() + .filter_map(|song| song.id) + .collect(); + query::cache(&ids); disable_raw_mode().unwrap(); diff --git a/gonk/src/playlist.rs b/gonk/src/playlist.rs index 154414bc..3df81fc9 100644 --- a/gonk/src/playlist.rs +++ b/gonk/src/playlist.rs @@ -53,7 +53,7 @@ impl Input for Playlist { match self.mode { Mode::Playlist => { self.playlists.up(); - let songs = playlist::get(&self.playlists.selected().unwrap()); + let songs = playlist::get(self.playlists.selected().unwrap()); self.songs = Index::new(songs, Some(0)); } Mode::Song => self.songs.up(), @@ -65,7 +65,7 @@ impl Input for Playlist { match self.mode { Mode::Playlist => { self.playlists.down(); - let songs = playlist::get(&self.playlists.selected().unwrap()); + let songs = playlist::get(self.playlists.selected().unwrap()); self.songs = Index::new(songs, Some(0)); } Mode::Song => self.songs.down(), @@ -285,9 +285,9 @@ pub fn draw_popup(playlist: &mut Playlist, f: &mut Frame) { } else { let width = v[0].width.saturating_sub(3); if len < width { - f.set_cursor(x + len, y) + f.set_cursor(x + len, y); } else { - f.set_cursor(x + width, y) + f.set_cursor(x + width, y); } } } diff --git a/gonk/src/queue.rs b/gonk/src/queue.rs index 527fb5c4..4f9b9700 100644 --- a/gonk/src/queue.rs +++ b/gonk/src/queue.rs @@ -91,7 +91,7 @@ pub fn draw(queue: &mut Queue, player: &mut Player, f: &mut Frame, event: Option //Handle mouse input. if let Some(event) = event { let (x, y) = (event.column, event.row); - const HEADER_HEIGHT: u16 = 5; + let header_height = 5; let size = f.size(); @@ -107,8 +107,8 @@ pub fn draw(queue: &mut Queue, player: &mut Player, f: &mut Frame, event: Option //Mouse support for the queue. if let Some((start, _)) = row_bounds { //Check if you clicked on the header. - if y >= HEADER_HEIGHT { - let index = (y - HEADER_HEIGHT) as usize + start; + if y >= header_height { + let index = (y - header_height) as usize + start; //Make sure you didn't click on the seek bar //and that the song index exists. diff --git a/gonk/src/search.rs b/gonk/src/search.rs index 4531c0b1..6c55060c 100644 --- a/gonk/src/search.rs +++ b/gonk/src/search.rs @@ -121,17 +121,11 @@ pub fn on_enter(search: &mut Search) -> Option<Vec<Song>> { } None } - Mode::Select => { - if let Some(item) = search.results.selected() { - Some(match item { - Item::Song(song) => query::songs_from_ids(&[song.id]), - Item::Album(album) => query::songs_from_album(&album.name, &album.artist), - Item::Artist(artist) => query::songs_by_artist(&artist.name), - }) - } else { - None - } - } + Mode::Select => search.results.selected().map(|item| match item { + Item::Song(song) => query::songs_from_ids(&[song.id]), + Item::Album(album) => query::songs_from_album(&album.name, &album.artist), + Item::Artist(artist) => query::songs_by_artist(&artist.name), + }), } } diff --git a/gonk/src/settings.rs b/gonk/src/settings.rs index 57ee2f0d..b9c2f0a4 100644 --- a/gonk/src/settings.rs +++ b/gonk/src/settings.rs @@ -18,14 +18,17 @@ impl Settings { let wanted_device = query::playback_device(); let devices = Player::audio_devices(); - let device_names: Vec<String> = devices.iter().flat_map(DeviceTrait::name).collect(); - let current_device = if !device_names.contains(&wanted_device) { + let current_device = if devices + .iter() + .flat_map(DeviceTrait::name) + .any(|x| x == wanted_device) + { + wanted_device + } else { let name = default_device.name().unwrap(); query::set_playback_device(&name); name - } else { - wanted_device }; Self { @@ -41,7 +44,7 @@ impl Input for Settings { } fn down(&mut self) { - self.devices.down() + self.devices.down(); } fn left(&mut self) {} diff --git a/gonk/src/status_bar.rs b/gonk/src/status_bar.rs index dd39291d..28bf08b8 100644 --- a/gonk/src/status_bar.rs +++ b/gonk/src/status_bar.rs @@ -125,11 +125,10 @@ pub fn draw(status_bar: &mut StatusBar, area: Rect, f: &mut Frame, busy: bool, p ); //TODO: Draw mini progress bar here. - - let text = if !player.is_playing() { - String::from("Paused ") - } else { + let text = if player.is_playing() { format!("Vol: {}% ", player.volume) + } else { + String::from("Paused ") }; f.render_widget( From b978fe9c43f23693f0a17418e15a9e664a95c8d9 Mon Sep 17 00:00:00 2001 From: zX3no <dr_xeno@hotmail.com.au> Date: Mon, 11 Jul 2022 12:57:28 +0930 Subject: [PATCH 26/40] cleanup --- gonk-player/src/lib.rs | 18 +----------------- gonk-player/src/sample_processor.rs | 2 -- 2 files changed, 1 insertion(+), 19 deletions(-) diff --git a/gonk-player/src/lib.rs b/gonk-player/src/lib.rs index ae6d6a33..1e04487f 100644 --- a/gonk-player/src/lib.rs +++ b/gonk-player/src/lib.rs @@ -268,23 +268,7 @@ impl Player { cpal::default_host().default_output_device().unwrap() } pub fn change_output_device(&mut self, _device: &Device) -> Result<(), StreamError> { - //TODO - Ok(()) - // match OutputStream::try_from_device(device) { - // Ok((stream, handle)) => { - // let pos = self.elapsed(); - // self.stop(); - // self.stream = stream; - // self.handle = handle; - // self.play_selected(); - // self.seek_to(pos); - // Ok(()) - // } - // Err(e) => match e { - // stream::StreamError::DefaultStreamConfigError(_) => Ok(()), - // _ => Err(e), - // }, - // } + todo!() } } diff --git a/gonk-player/src/sample_processor.rs b/gonk-player/src/sample_processor.rs index 1e53135f..9c5284c1 100644 --- a/gonk-player/src/sample_processor.rs +++ b/gonk-player/src/sample_processor.rs @@ -97,7 +97,6 @@ pub struct Processor { pub capacity: u64, pub converter: SampleRateConverter, pub finished: bool, - pub left: bool, pub duration: Duration, pub elapsed: Duration, pub volume: f32, @@ -161,7 +160,6 @@ impl Processor { output_rate, ), finished: false, - left: true, volume, input: spec.rate, output: output_rate, From 05d7bdb291426899f43b37387eeafdd405f3652b Mon Sep 17 00:00:00 2001 From: zX3no <dr_xeno@hotmail.com.au> Date: Mon, 11 Jul 2022 14:16:43 +0930 Subject: [PATCH 27/40] reworked player again --- gonk-player/Cargo.toml | 1 - gonk-player/src/lib.rs | 564 +++++++++++++++++++--------- gonk-player/src/sample_processor.rs | 246 ------------ gonk-player/src/sample_rate.rs | 140 ------- gonk-player/src/song.rs | 6 +- gonk/src/main.rs | 2 +- gonk/src/queue.rs | 12 +- gonk/src/settings.rs | 57 +-- gonk/src/status_bar.rs | 4 +- 9 files changed, 428 insertions(+), 604 deletions(-) delete mode 100644 gonk-player/src/sample_processor.rs delete mode 100644 gonk-player/src/sample_rate.rs diff --git a/gonk-player/Cargo.toml b/gonk-player/Cargo.toml index 6c60104c..b8381996 100644 --- a/gonk-player/Cargo.toml +++ b/gonk-player/Cargo.toml @@ -11,5 +11,4 @@ license = "MIT" [dependencies] cpal = "0.13.5" -crossbeam-channel = "0.5.4" symphonia = { version = "0.5.0", features = ["mp3", "isomp4", "alac", "aac"] } diff --git a/gonk-player/src/lib.rs b/gonk-player/src/lib.rs index 1e04487f..9892b424 100644 --- a/gonk-player/src/lib.rs +++ b/gonk-player/src/lib.rs @@ -1,123 +1,311 @@ +#![feature(const_fn_floating_point_arithmetic)] use cpal::{ traits::{HostTrait, StreamTrait}, - StreamError, + Stream, }; -use crossbeam_channel::{unbounded, Sender}; -use sample_processor::Generator; -use std::{ - sync::{Arc, RwLock}, - thread, - time::Duration, +use std::{fs::File, time::Duration, vec::IntoIter}; +use symphonia::{ + core::{ + audio::SampleBuffer, + codecs::{Decoder, DecoderOptions}, + errors::{Error, SeekErrorKind}, + formats::{FormatOptions, SeekMode, SeekTo}, + io::MediaSourceStream, + meta::MetadataOptions, + probe::{Hint, ProbeResult}, + units::{Time, TimeBase}, + }, + default::get_probe, }; +pub use cpal::{traits::DeviceTrait, Device}; +pub use index::Index; +pub use song::Song; + mod index; -mod sample_processor; -mod sample_rate; mod song; -pub use cpal::traits::DeviceTrait; -pub use cpal::Device; -pub use index::Index; -pub use song::Song; +#[inline] +const fn gcd(a: usize, b: usize) -> usize { + if b == 0 { + a + } else { + gcd(b, a % b) + } +} + +#[inline] +const fn lerp(a: f32, b: f32, t: f32) -> f32 { + a + t * (b - a) +} + +static mut RESAMPLER: Option<Resampler> = None; const VOLUME_STEP: u16 = 5; const VOLUME_REDUCTION: f32 = 600.0; +const MAX_VOLUME: f32 = 100.0 / VOLUME_REDUCTION; +const MIN_VOLUME: f32 = 0.0 / VOLUME_REDUCTION; -#[derive(Debug)] -pub enum Event { - Play, - Pause, - SeekBy(f32), - SeekTo(f32), - Volume(f32), -} +pub struct Resampler { + probed: ProbeResult, + decoder: Box<dyn Decoder>, -pub struct Player { - pub s: Sender<Event>, - pub playing: bool, - pub volume: u16, - pub songs: Index<Song>, - pub elapsed: Arc<RwLock<Duration>>, - pub generator: Arc<RwLock<Generator>>, + input: usize, + output: usize, + + buffer: IntoIter<f32>, + + current_frame: Vec<f32>, + current_frame_pos_in_chunk: usize, + + next_frame: Vec<f32>, + next_output_frame_pos_in_chunk: usize, + + output_buffer: Option<f32>, + + time_base: TimeBase, + + gain: f32, + + pub volume: f32, pub duration: Duration, + pub finished: bool, + pub elapsed: Duration, } -impl Player { - pub fn new(_device: String, volume: u16, _songs: &[Song]) -> Self { - let device = cpal::default_host().default_output_device().unwrap(); +impl Resampler { + pub fn new(output: usize, path: &str, gain: f32) -> Self { + let source = Box::new(File::open(path).unwrap()); + let mss = MediaSourceStream::new(source, Default::default()); + + let mut probed = get_probe() + .format( + &Hint::default(), + mss, + &FormatOptions::default(), + &MetadataOptions::default(), + ) + .unwrap(); + + let track = probed.format.default_track().unwrap(); + let input = track.codec_params.sample_rate.unwrap() as usize; + let time_base = track.codec_params.time_base.unwrap(); + + let n_frames = track.codec_params.n_frames.unwrap(); + let time = track.codec_params.time_base.unwrap().calc_time(n_frames); + let duration = Duration::from_secs(time.seconds) + Duration::from_secs_f64(time.frac); + + let mut decoder = symphonia::default::get_codecs() + .make(&track.codec_params, &DecoderOptions::default()) + .unwrap(); + + let next_packet = probed.format.next_packet().unwrap(); + let decoded = decoder.decode(&next_packet).unwrap(); + let mut buffer = SampleBuffer::<f32>::new(decoded.capacity() as u64, *decoded.spec()); + buffer.copy_interleaved_ref(decoded); + let mut buffer = buffer.samples().to_vec().into_iter(); + + let ts = next_packet.ts(); + let t = time_base.calc_time(ts); + let elapsed = Duration::from_secs(t.seconds) + Duration::from_secs_f64(t.frac); + + let gcd = gcd(input, output); + + let (current_frame, next_frame) = if input == output { + (Vec::new(), Vec::new()) + } else { + ( + vec![buffer.next().unwrap(), buffer.next().unwrap()], + vec![buffer.next().unwrap(), buffer.next().unwrap()], + ) + }; + + Self { + probed, + decoder, + buffer, + input: input / gcd, + output: output / gcd, + current_frame_pos_in_chunk: 0, + next_output_frame_pos_in_chunk: 0, + current_frame, + next_frame, + output_buffer: None, + volume: 15.0 / VOLUME_REDUCTION, + duration, + elapsed, + time_base, + finished: false, + gain, + } + } + + pub fn next(&mut self) -> f32 { + if let Some(smp) = self.next_sample() { + if self.gain == 0.0 { + //Reduce the volume a little to match + //songs with replay gain information. + smp * self.volume * 0.75 + } else { + smp * self.volume * self.gain + } + } else { + let next_packet = self.probed.format.next_packet().unwrap(); + let decoded = self.decoder.decode(&next_packet).unwrap(); + let mut buffer = SampleBuffer::<f32>::new(decoded.capacity() as u64, *decoded.spec()); + buffer.copy_interleaved_ref(decoded); + self.buffer = buffer.samples().to_vec().into_iter(); + + let ts = next_packet.ts(); + let t = self.time_base.calc_time(ts); + self.elapsed = Duration::from_secs(t.seconds) + Duration::from_secs_f64(t.frac); - let config = device.default_output_config().unwrap(); - let rate = config.sample_rate().0; + if self.input == self.output { + self.current_frame = Vec::new(); + self.next_frame = Vec::new(); + } else { + self.current_frame = vec![self.buffer.next().unwrap(), self.buffer.next().unwrap()]; + self.next_frame = vec![self.buffer.next().unwrap(), self.buffer.next().unwrap()]; + } + + self.current_frame_pos_in_chunk = 0; + self.next_output_frame_pos_in_chunk = 0; + + debug_assert!(self.output_buffer.is_none()); + + self.next() + } + } + + fn next_input_frame(&mut self) { + self.current_frame = std::mem::take(&mut self.next_frame); + + if let Some(sample) = self.buffer.next() { + self.next_frame.push(sample); + } + + if let Some(sample) = self.buffer.next() { + self.next_frame.push(sample); + } + + self.current_frame_pos_in_chunk += 1; + } + + fn next_sample(&mut self) -> Option<f32> { + if self.input == self.output { + return self.buffer.next(); + } else if let Some(sample) = self.output_buffer.take() { + return Some(sample); + } + + if self.next_output_frame_pos_in_chunk == self.output { + self.next_output_frame_pos_in_chunk = 0; + + self.next_input_frame(); + while self.current_frame_pos_in_chunk != self.input { + self.next_input_frame(); + } + self.current_frame_pos_in_chunk = 0; + } else { + let req_left_sample = + (self.input * self.next_output_frame_pos_in_chunk / self.output) % self.input; + + while self.current_frame_pos_in_chunk != req_left_sample { + self.next_input_frame(); + debug_assert!(self.current_frame_pos_in_chunk < self.input); + } + } + + let numerator = (self.input * self.next_output_frame_pos_in_chunk) % self.output; + + self.next_output_frame_pos_in_chunk += 1; + + if self.current_frame.is_empty() && self.next_frame.is_empty() { + return None; + } + + if self.next_frame.is_empty() { + let r = self.current_frame.remove(0); + self.output_buffer = self.current_frame.first().cloned(); + self.current_frame.clear(); + Some(r) + } else { + let ratio = numerator as f32 / self.output as f32; + self.output_buffer = Some(lerp(self.current_frame[1], self.next_frame[1], ratio)); + Some(lerp(self.current_frame[0], self.next_frame[0], ratio)) + } + } + + pub fn volume_up(&mut self) { + self.volume += VOLUME_STEP as f32 / VOLUME_REDUCTION; - let generator = Arc::new(RwLock::new(Generator::new( - rate, - volume as f32 / VOLUME_REDUCTION, - ))); - let gen = generator.clone(); - let g = generator.clone(); + if self.volume > MAX_VOLUME { + self.volume = MAX_VOLUME; + } + } + + pub fn volume_down(&mut self) { + self.volume -= VOLUME_STEP as f32 / VOLUME_REDUCTION; - let elapsed = Arc::new(RwLock::new(Duration::default())); - let e = elapsed.clone(); + if self.volume < MIN_VOLUME { + self.volume = MIN_VOLUME; + } + } +} + +#[derive(Debug, PartialEq)] +pub enum State { + Playing, + Paused, + Stopped, +} + +pub struct Player { + pub stream: Stream, + pub sample_rate: usize, + pub state: State, + pub songs: Index<Song>, +} - let (s, r) = unbounded(); +impl Player { + pub fn new(_device: String, _volume: u16, _cache: &[Song]) -> Self { + let device = cpal::default_host().default_output_device().unwrap(); + let config = device.default_output_config().unwrap().config(); - thread::spawn(move || { - let stream = device - .build_output_stream( - &config.config(), - move |data: &mut [f32], _: &cpal::OutputCallbackInfo| { + let stream = device + .build_output_stream( + &config, + move |data: &mut [f32], _: &cpal::OutputCallbackInfo| { + if let Some(resampler) = unsafe { &mut RESAMPLER } { for frame in data.chunks_mut(2) { for sample in frame.iter_mut() { - *sample = g.write().unwrap().next(); + *sample = resampler.next(); } } - }, - |err| panic!("{}", err), - ) - .unwrap(); - - stream.play().unwrap(); - - loop { - *e.write().unwrap() = gen.read().unwrap().elapsed(); - - if let Ok(event) = r.recv_timeout(Duration::from_millis(8)) { - match event { - Event::Play => stream.play().unwrap(), - Event::Pause => stream.pause().unwrap(), - Event::SeekBy(duration) => gen.write().unwrap().seek_by(duration).unwrap(), - Event::SeekTo(duration) => gen.write().unwrap().seek_to(duration).unwrap(), - Event::Volume(volume) => gen.write().unwrap().set_volume(volume), } - } - } - }); + }, + |err| panic!("{}", err), + ) + .unwrap(); + + stream.play().unwrap(); Self { - s, - playing: false, - volume, - elapsed, - duration: Duration::default(), + sample_rate: config.sample_rate.0 as usize, + stream, + state: State::Stopped, songs: Index::default(), - generator, } } - //TODO: Run update loop in the player not in the client. + pub fn update(&mut self) { - if self.generator.read().unwrap().is_done() { - self.next(); + if let Some(resampler) = unsafe { RESAMPLER.as_ref() } { + if resampler.finished { + self.next(); + } } } - pub fn duration(&self) -> Duration { - self.duration - } - pub fn elapsed(&self) -> Duration { - *self.elapsed.read().unwrap() - } - pub fn is_empty(&self) -> bool { - self.songs.is_empty() - } + pub fn add_songs(&mut self, songs: &[Song]) { self.songs.data.extend(songs.to_vec()); if self.songs.selected().is_none() { @@ -125,22 +313,42 @@ impl Player { self.play_selected(); } } - pub fn get_volume(&self) -> u16 { - self.volume + + pub fn previous(&mut self) { + self.songs.up(); + self.play_selected(); + } + + pub fn next(&mut self) { + self.songs.down(); + self.play_selected(); } + + pub fn play(&mut self, path: &str) { + unsafe { + RESAMPLER = Some(Resampler::new(self.sample_rate, path, 0.0)); + } + self.state = State::Playing; + } + pub fn play_selected(&mut self) { if let Some(song) = self.songs.selected() { - self.playing = true; - let mut gen = self.generator.write().unwrap(); - gen.update(&song.path.clone()); - gen.set_volume(self.real_volume()); - self.duration = gen.duration(); + unsafe { + RESAMPLER = Some(Resampler::new( + self.sample_rate, + song.path.to_str().unwrap(), + song.gain as f32, + )); + } + self.state = State::Playing; } } + pub fn play_index(&mut self, i: usize) { self.songs.select(Some(i)); self.play_selected(); } + pub fn delete_index(&mut self, i: usize) { self.songs.data.remove(i); @@ -163,113 +371,113 @@ impl Player { } }; } + pub fn clear(&mut self) { self.songs = Index::default(); - self.generator.write().unwrap().stop(); + self.state = State::Stopped; } + pub fn clear_except_playing(&mut self) { - let selected = self.songs.selected().cloned(); - let mut i = 0; - while i < self.songs.len() { - if Some(&self.songs.data[i]) != selected.as_ref() { - self.songs.data.remove(i); - } else { - i += 1; - } + if let Some(index) = self.songs.index() { + let playing = self.songs.data.remove(index); + self.songs = Index::new(vec![playing], Some(0)); } - self.songs.select(Some(0)); } - pub fn toggle_playback(&mut self) { - if self.playing { - self.pause(); - } else { - self.play(); + + pub fn volume_up(&self) { + unsafe { + if let Some(resampler) = RESAMPLER.as_mut() { + resampler.volume_up(); + } } } - fn play(&mut self) { - self.s.send(Event::Play).unwrap(); - self.playing = true; + + pub fn volume_down(&self) { + unsafe { + if let Some(resampler) = RESAMPLER.as_mut() { + resampler.volume_down(); + } + } } - fn pause(&mut self) { - self.s.send(Event::Pause).unwrap(); - self.playing = false; + + pub fn volume(&self) -> u16 { + //TODO: Volume needs to be stored in the player + //otherwise it will go away when the next song is played. + unsafe { + let volume = RESAMPLER.as_ref().unwrap().volume; + (volume * VOLUME_REDUCTION).round() as u16 + } } - pub fn previous(&mut self) { - self.songs.up(); - self.play_selected(); + + pub fn duration(&self) -> Duration { + unsafe { + match RESAMPLER.as_ref() { + Some(resampler) => resampler.duration, + None => Duration::default(), + } + } } - pub fn next(&mut self) { - self.songs.down(); - self.play_selected(); + + pub fn elapsed(&self) -> Duration { + unsafe { + match RESAMPLER.as_ref() { + Some(resampler) => resampler.elapsed, + None => Duration::default(), + } + } } - pub fn volume_up(&mut self) { - self.volume += VOLUME_STEP; - if self.volume > 100 { - self.volume = 100; + pub fn toggle_playback(&mut self) { + match self.state { + State::Playing => self.stream.pause().unwrap(), + State::Paused => self.stream.play().unwrap(), + State::Stopped => (), } + } - self.update_volume(); + pub fn seek_by(&mut self, time: f32) { + unsafe { + if RESAMPLER.is_none() { + return; + } + + self.seek_to(RESAMPLER.as_ref().unwrap().elapsed.as_secs_f32() + time); + } } - pub fn volume_down(&mut self) { - if self.volume != 0 { - self.volume -= VOLUME_STEP; + + pub fn seek_to(&mut self, time: f32) { + if unsafe { RESAMPLER.is_none() } { + return; } - self.update_volume(); - } - fn update_volume(&self) { - self.s.send(Event::Volume(self.real_volume())).unwrap(); - } - fn real_volume(&self) -> f32 { - if let Some(song) = self.songs.selected() { - let volume = self.volume as f32 / VOLUME_REDUCTION; - //Calculate the volume with gain - if song.gain == 0.0 { - //Reduce the volume a little to match - //songs with replay gain information. - volume * 0.75 - } else { - volume * song.gain as f32 + let time = if time.is_sign_negative() { 0.0 } else { time }; + + let time = Duration::from_secs_f32(time); + unsafe { + match RESAMPLER.as_mut().unwrap().probed.format.seek( + SeekMode::Coarse, + SeekTo::Time { + time: Time::new(time.as_secs(), time.subsec_nanos() as f64 / 1_000_000_000.0), + track_id: None, + }, + ) { + Ok(_) => (), + Err(e) => match e { + Error::SeekError(e) => match e { + SeekErrorKind::OutOfRange => { + self.next(); + } + _ => panic!("{:?}", e), + }, + _ => panic!("{}", e), + }, } - } else { - self.volume as f32 / VOLUME_REDUCTION } } - pub fn is_playing(&self) -> bool { - self.playing - } - pub fn total_songs(&self) -> usize { - self.songs.len() - } - pub fn get_index(&self) -> &Index<Song> { - &self.songs - } - pub fn selected_song(&self) -> Option<&Song> { - self.songs.selected() - } - pub fn seek_by(&self, duration: f32) { - self.s.send(Event::SeekBy(duration)).unwrap(); - } - pub fn seek_to(&self, duration: f32) { - self.s.send(Event::SeekTo(duration)).unwrap(); - } - //TODO: Remove? - pub fn audio_devices() -> Vec<Device> { - let host_id = cpal::default_host().id(); - let host = cpal::host_from_id(host_id).unwrap(); - - //FIXME: Getting just the output devies was too slow(150ms). - //Collecting every device is still slow but it's not as bad. - host.devices().unwrap().collect() - } - pub fn default_device() -> Device { - cpal::default_host().default_output_device().unwrap() - } - pub fn change_output_device(&mut self, _device: &Device) -> Result<(), StreamError> { - todo!() + pub fn is_playing(&self) -> bool { + State::Playing == self.state } } -unsafe impl Sync for Player {} +unsafe impl Send for Player {} diff --git a/gonk-player/src/sample_processor.rs b/gonk-player/src/sample_processor.rs deleted file mode 100644 index 9c5284c1..00000000 --- a/gonk-player/src/sample_processor.rs +++ /dev/null @@ -1,246 +0,0 @@ -use crate::sample_rate::SampleRateConverter; -use std::{fs::File, io::ErrorKind, path::Path, time::Duration}; -use symphonia::{ - core::{ - audio::{SampleBuffer, SignalSpec}, - codecs::{Decoder, DecoderOptions}, - errors::{Error, SeekErrorKind}, - formats::{FormatOptions, FormatReader, SeekMode, SeekTo}, - io::MediaSourceStream, - meta::MetadataOptions, - probe::Hint, - units::Time, - }, - default::get_probe, -}; - -pub struct Generator { - processor: Option<Processor>, - sample_rate: u32, - volume: f32, -} - -impl Generator { - pub fn new(sample_rate: u32, volume: f32) -> Self { - Self { - processor: None, - sample_rate, - volume, - } - } - pub fn next(&mut self) -> f32 { - if let Some(processor) = &mut self.processor { - if processor.finished { - 0.0 - } else { - processor.next_sample() - } - } else { - 0.0 - } - } - pub fn seek_to(&mut self, time: f32) -> Result<(), ()> { - if let Some(processor) = &mut self.processor { - processor.seek_to(time); - Ok(()) - } else { - Err(()) - } - } - pub fn elapsed(&self) -> Duration { - if let Some(processor) = &self.processor { - processor.elapsed - } else { - Duration::default() - } - } - pub fn duration(&self) -> Duration { - if let Some(processor) = &self.processor { - processor.duration - } else { - Duration::default() - } - } - pub fn seek_by(&mut self, time: f32) -> Result<(), ()> { - if let Some(processor) = &mut self.processor { - processor.seek_by(time); - Ok(()) - } else { - Err(()) - } - } - pub fn set_volume(&mut self, volume: f32) { - self.volume = volume; - if let Some(processor) = &mut self.processor { - processor.volume = volume; - } - } - pub fn update(&mut self, path: &Path) { - self.processor = Some(Processor::new(self.sample_rate, path, self.volume)); - } - pub fn is_done(&self) -> bool { - if let Some(processor) = &self.processor { - processor.finished - } else { - false - } - } - pub fn stop(&mut self) { - self.processor = None; - } -} - -pub struct Processor { - pub decoder: Box<dyn Decoder>, - pub format: Box<dyn FormatReader>, - pub spec: SignalSpec, - pub capacity: u64, - pub converter: SampleRateConverter, - pub finished: bool, - pub duration: Duration, - pub elapsed: Duration, - pub volume: f32, - - pub input: u32, - pub output: u32, -} - -impl Processor { - pub fn new(output_rate: u32, path: &Path, volume: f32) -> Self { - let source = Box::new(File::open(path).unwrap()); - - let mss = MediaSourceStream::new(source, Default::default()); - - let mut probed = get_probe() - .format( - &Hint::default(), - mss, - &FormatOptions { - prebuild_seek_index: true, - seek_index_fill_rate: 1, - ..Default::default() - }, - &MetadataOptions::default(), - ) - .unwrap(); - - let track = probed.format.default_track().unwrap(); - - let duration = if let Some(tb) = track.codec_params.time_base { - let n_frames = track.codec_params.n_frames.unwrap(); - let time = tb.calc_time(n_frames); - Duration::from_secs(time.seconds) + Duration::from_secs_f64(time.frac) - } else { - panic!("Could not decode track duration."); - }; - - let mut decoder = symphonia::default::get_codecs() - .make(&track.codec_params, &DecoderOptions::default()) - .unwrap(); - - let current_frame = probed.format.next_packet().unwrap(); - let decoded = decoder.decode(¤t_frame).unwrap(); - - let spec = decoded.spec().to_owned(); - let capacity = decoded.capacity() as u64; - - let mut sample_buffer = SampleBuffer::<f32>::new(capacity, spec); - sample_buffer.copy_interleaved_ref(decoded); - - Self { - format: probed.format, - decoder, - spec, - capacity, - duration, - elapsed: Duration::default(), - converter: SampleRateConverter::new( - sample_buffer.samples().to_vec().into_iter(), - spec.rate, - output_rate, - ), - finished: false, - volume, - input: spec.rate, - output: output_rate, - } - } - pub fn next_sample(&mut self) -> f32 { - loop { - if self.finished { - return 0.0; - } else if let Some(sample) = self.converter.next() { - return sample * self.volume; - } else { - self.update(); - } - } - } - pub fn update(&mut self) { - if self.finished { - return; - } - - let mut decode_errors: usize = 0; - const MAX_DECODE_ERRORS: usize = 3; - loop { - match self.format.next_packet() { - Ok(packet) => { - let decoded = self.decoder.decode(&packet).unwrap(); - let mut buffer = SampleBuffer::<f32>::new(self.capacity, self.spec); - buffer.copy_interleaved_ref(decoded); - - self.converter = SampleRateConverter::new( - buffer.samples().to_vec().into_iter(), - self.input, - self.output, - ); - - //Update elapsed - let ts = packet.ts(); - let track = self.format.default_track().unwrap(); - let tb = track.codec_params.time_base.unwrap(); - let t = tb.calc_time(ts); - self.elapsed = Duration::from_secs(t.seconds) + Duration::from_secs_f64(t.frac); - return; - } - Err(e) => match e { - Error::DecodeError(e) => { - decode_errors += 1; - if decode_errors > MAX_DECODE_ERRORS { - panic!("{:?}", e); - } - } - Error::IoError(e) if e.kind() == ErrorKind::UnexpectedEof => { - self.finished = true; - return; - } - _ => panic!("{:?}", e), - }, - }; - } - } - pub fn seek_by(&mut self, time: f32) { - let time = self.elapsed.as_secs_f32() + time; - self.seek_to(time); - } - pub fn seek_to(&mut self, time: f32) { - let time = Duration::from_secs_f32(time); - match self.format.seek( - SeekMode::Coarse, - SeekTo::Time { - time: Time::new(time.as_secs(), time.subsec_nanos() as f64 / 1_000_000_000.0), - track_id: None, - }, - ) { - Ok(_) => (), - Err(e) => match e { - Error::SeekError(e) => match e { - SeekErrorKind::OutOfRange => self.finished = true, - _ => panic!("{:?}", e), - }, - _ => panic!("{}", e), - }, - } - } -} diff --git a/gonk-player/src/sample_rate.rs b/gonk-player/src/sample_rate.rs deleted file mode 100644 index df7738c6..00000000 --- a/gonk-player/src/sample_rate.rs +++ /dev/null @@ -1,140 +0,0 @@ -use std::{mem, vec::IntoIter}; - -#[inline] -const fn gcd(a: u32, b: u32) -> u32 { - if b == 0 { - a - } else { - gcd(b, a % b) - } -} - -#[inline] -fn lerp(a: f32, b: f32, t: f32) -> f32 { - a + t * (b - a) -} - -pub struct SampleRateConverter { - /// The iterator that gives us samples. - buffer: IntoIter<f32>, - - ///Input sample rate - interpolation factor. - input: u32, - ///Output sample rate - decimation factor. - output: u32, - - /// One sample per channel, extracted from `input`. - current_frame: Vec<f32>, - /// Position of `current_sample` modulo `from`. - /// - /// `0..input / gcd` - current_frame_pos_in_chunk: u32, - - /// The samples right after `current_sample` (one per channel), extracted from `input`. - next_frame: Vec<f32>, - /// The position of the next sample that the iterator should return, modulo `to`. - /// This counter is incremented (modulo `to`) every time the iterator is called. - /// - /// `0..output / gcd` - next_output_frame_pos_in_chunk: u32, - - output_buffer: Option<f32>, -} - -impl SampleRateConverter { - pub fn new(mut buffer: IntoIter<f32>, input: u32, output: u32) -> SampleRateConverter { - debug_assert!(input >= 1); - debug_assert!(output >= 1); - - let gcd = gcd(input, output); - let (current_frame, next_frame) = if input == output { - (Vec::new(), Vec::new()) - } else { - ( - vec![buffer.next().unwrap(), buffer.next().unwrap()], - vec![buffer.next().unwrap(), buffer.next().unwrap()], - ) - }; - - SampleRateConverter { - buffer, - input: input / gcd, - output: output / gcd, - current_frame_pos_in_chunk: 0, - next_output_frame_pos_in_chunk: 0, - current_frame, - next_frame, - output_buffer: None, - } - } - - fn next_input_frame(&mut self) { - self.current_frame = mem::take(&mut self.next_frame); - - if let Some(sample) = self.buffer.next() { - self.next_frame.push(sample); - } - - if let Some(sample) = self.buffer.next() { - self.next_frame.push(sample); - } - - self.current_frame_pos_in_chunk += 1; - } - - pub fn next(&mut self) -> Option<f32> { - if self.input == self.output { - return self.buffer.next(); - } else if let Some(sample) = self.output_buffer.take() { - return Some(sample); - } - - // The frame we are going to return from this function will be a linear interpolation - // between `self.current_frame` and `self.next_frame`. - if self.next_output_frame_pos_in_chunk == self.output { - // If we jump to the next frame, we reset the whole state. - self.next_output_frame_pos_in_chunk = 0; - - self.next_input_frame(); - while self.current_frame_pos_in_chunk != self.input { - self.next_input_frame(); - } - self.current_frame_pos_in_chunk = 0; - } else { - // Finding the position of the first sample of the linear interpolation. - let req_left_sample = - (self.input * self.next_output_frame_pos_in_chunk / self.output) % self.input; - - // Advancing `self.current_frame`, `self.next_frame` and - // `self.current_frame_pos_in_chunk` until the latter variable - // matches `req_left_sample`. - while self.current_frame_pos_in_chunk != req_left_sample { - self.next_input_frame(); - debug_assert!(self.current_frame_pos_in_chunk < self.input); - } - } - - // Merging `self.current_frame` and `self.next_frame` into `self.output_buffer`. - // Note that `self.output_buffer` can be truncated if there is not enough data in - // `self.next_frame`. - let numerator = (self.input * self.next_output_frame_pos_in_chunk) % self.output; - - // Incrementing the counter for the next iteration. - self.next_output_frame_pos_in_chunk += 1; - - if self.current_frame.is_empty() && self.next_frame.is_empty() { - return None; - } - - if self.next_frame.is_empty() { - let r = self.current_frame.remove(0); - self.output_buffer = self.current_frame.first().cloned(); - self.current_frame.clear(); - Some(r) - } else { - let ratio = numerator as f32 / self.output as f32; - self.output_buffer = Some(lerp(self.current_frame[1], self.next_frame[1], ratio)); - Some(lerp(self.current_frame[0], self.next_frame[0], ratio)) - } - } -} diff --git a/gonk-player/src/song.rs b/gonk-player/src/song.rs index b4d1d764..78fd6eb2 100644 --- a/gonk-player/src/song.rs +++ b/gonk-player/src/song.rs @@ -12,8 +12,8 @@ use symphonia::{ default::get_probe, }; -fn db_to_amplitude(db: f64) -> f64 { - 10.0_f64.powf(db / 20.0_f64) +fn db_to_amplitude(db: f32) -> f32 { + 10.0_f32.powf(db / 20.0) } #[derive(Debug, Clone, Default)] @@ -22,7 +22,7 @@ pub struct Song { pub disc: u64, pub number: u64, pub path: PathBuf, - pub gain: f64, + pub gain: f32, pub album: String, pub artist: String, pub id: Option<usize>, diff --git a/gonk/src/main.rs b/gonk/src/main.rs index 02674abd..4a71aae4 100644 --- a/gonk/src/main.rs +++ b/gonk/src/main.rs @@ -381,7 +381,7 @@ fn main() { } } - query::set_volume(player.volume); + query::set_volume(player.volume()); let ids: Vec<usize> = player .songs diff --git a/gonk/src/queue.rs b/gonk/src/queue.rs index 4f9b9700..efdfb58f 100644 --- a/gonk/src/queue.rs +++ b/gonk/src/queue.rs @@ -84,7 +84,7 @@ pub fn draw(queue: &mut Queue, player: &mut Player, f: &mut Frame, event: Option draw_seeker(player, f, chunks[2]); //Don't handle mouse input when the queue is empty. - if player.is_empty() { + if player.songs.is_empty() { return; } @@ -100,7 +100,7 @@ pub fn draw(queue: &mut Queue, player: &mut Player, f: &mut Frame, event: Option && size.height > 15 { let ratio = x as f32 / size.width as f32; - let duration = player.duration.as_secs_f32(); + let duration = player.duration().as_secs_f32(); player.seek_to(duration * ratio); } @@ -145,7 +145,7 @@ fn draw_header(player: &mut Player, f: &mut Frame, area: Rect) { draw_title(player, f, area); } - let volume = Spans::from(format!("Vol: {}%─╮", player.volume)); + let volume = Spans::from(format!("Vol: {}%─╮", player.volume())); f.render_widget(Paragraph::new(volume).alignment(Alignment::Right), area); } @@ -326,8 +326,8 @@ fn draw_seeker(player: &mut Player, f: &mut Frame, area: Rect) { ); } - let elapsed = player.elapsed().as_secs_f64(); - let duration = player.duration.as_secs_f64(); + let elapsed = player.elapsed().as_secs_f32(); + let duration = player.duration().as_secs_f32(); let seeker = format!( "{:02}:{:02}/{:02}:{:02}", @@ -352,7 +352,7 @@ fn draw_seeker(player: &mut Player, f: &mut Frame, area: Rect) { .border_type(BorderType::Rounded), ) .gauge_style(Style::default().fg(COLORS.seeker)) - .ratio(ratio) + .ratio(ratio as f64) .label(seeker), area, ); diff --git a/gonk/src/settings.rs b/gonk/src/settings.rs index b9c2f0a4..16d9b862 100644 --- a/gonk/src/settings.rs +++ b/gonk/src/settings.rs @@ -1,3 +1,4 @@ +#![allow(unused)] use crate::{widgets::*, Frame, Input}; use gonk_database::query; use gonk_player::{Device, DeviceTrait, Index, Player}; @@ -14,26 +15,28 @@ pub struct Settings { impl Settings { pub fn new() -> Self { - let default_device = Player::default_device(); - let wanted_device = query::playback_device(); + // let default_device = Player::default_device(); + // let wanted_device = query::playback_device(); - let devices = Player::audio_devices(); + // let devices = Player::audio_devices(); - let current_device = if devices - .iter() - .flat_map(DeviceTrait::name) - .any(|x| x == wanted_device) - { - wanted_device - } else { - let name = default_device.name().unwrap(); - query::set_playback_device(&name); - name - }; + // let current_device = if devices + // .iter() + // .flat_map(DeviceTrait::name) + // .any(|x| x == wanted_device) + // { + // wanted_device + // } else { + // let name = default_device.name().unwrap(); + // query::set_playback_device(&name); + // name + // }; Self { - devices: Index::new(devices, Some(0)), - current_device, + // devices: Index::new(devices, Some(0)), + // current_device, + devices: Index::default(), + current_device: String::new(), } } } @@ -53,17 +56,17 @@ impl Input for Settings { } pub fn on_enter(settings: &mut Settings, player: &mut Player) { - if let Some(device) = settings.devices.selected() { - match player.change_output_device(device) { - Ok(_) => { - let name = device.name().unwrap(); - query::set_playback_device(&name); - settings.current_device = name; - } - //TODO: Print error in status bar - Err(e) => panic!("{:?}", e), - } - } + // if let Some(device) = settings.devices.selected() { + // match player.change_output_device(device) { + // Ok(_) => { + // let name = device.name().unwrap(); + // query::set_playback_device(&name); + // settings.current_device = name; + // } + // //TODO: Print error in status bar + // Err(e) => panic!("{:?}", e), + // } + // } } #[allow(unused)] diff --git a/gonk/src/status_bar.rs b/gonk/src/status_bar.rs index 28bf08b8..2d54bb6c 100644 --- a/gonk/src/status_bar.rs +++ b/gonk/src/status_bar.rs @@ -55,7 +55,7 @@ pub fn update(status_bar: &mut StatusBar, db_busy: bool, player: &Player) { //before triggering an update //the status bar will stay open //without the users permission. - if player.is_empty() { + if player.songs.is_empty() { status_bar.hidden = true; } } @@ -126,7 +126,7 @@ pub fn draw(status_bar: &mut StatusBar, area: Rect, f: &mut Frame, busy: bool, p //TODO: Draw mini progress bar here. let text = if player.is_playing() { - format!("Vol: {}% ", player.volume) + format!("Vol: {}% ", player.volume()) } else { String::from("Paused ") }; From 1815fa211b9a107e791cdab11c90b60351303ce5 Mon Sep 17 00:00:00 2001 From: zX3no <dr_xeno@hotmail.com.au> Date: Mon, 11 Jul 2022 14:23:01 +0930 Subject: [PATCH 28/40] fix: volume was not saved when changing tracks --- gonk-player/src/lib.rs | 62 ++++++++++++++++-------------------------- gonk/src/main.rs | 2 +- gonk/src/queue.rs | 2 +- gonk/src/status_bar.rs | 2 +- 4 files changed, 26 insertions(+), 42 deletions(-) diff --git a/gonk-player/src/lib.rs b/gonk-player/src/lib.rs index 9892b424..072d3832 100644 --- a/gonk-player/src/lib.rs +++ b/gonk-player/src/lib.rs @@ -1,4 +1,3 @@ -#![feature(const_fn_floating_point_arithmetic)] use cpal::{ traits::{HostTrait, StreamTrait}, Stream, @@ -35,7 +34,7 @@ const fn gcd(a: usize, b: usize) -> usize { } #[inline] -const fn lerp(a: f32, b: f32, t: f32) -> f32 { +fn lerp(a: f32, b: f32, t: f32) -> f32 { a + t * (b - a) } @@ -43,8 +42,6 @@ static mut RESAMPLER: Option<Resampler> = None; const VOLUME_STEP: u16 = 5; const VOLUME_REDUCTION: f32 = 600.0; -const MAX_VOLUME: f32 = 100.0 / VOLUME_REDUCTION; -const MIN_VOLUME: f32 = 0.0 / VOLUME_REDUCTION; pub struct Resampler { probed: ProbeResult, @@ -74,7 +71,7 @@ pub struct Resampler { } impl Resampler { - pub fn new(output: usize, path: &str, gain: f32) -> Self { + pub fn new(output: usize, path: &str, volume: u16, gain: f32) -> Self { let source = Box::new(File::open(path).unwrap()); let mss = MediaSourceStream::new(source, Default::default()); @@ -131,7 +128,7 @@ impl Resampler { current_frame, next_frame, output_buffer: None, - volume: 15.0 / VOLUME_REDUCTION, + volume: volume as f32 / VOLUME_REDUCTION, duration, elapsed, time_base, @@ -236,20 +233,8 @@ impl Resampler { } } - pub fn volume_up(&mut self) { - self.volume += VOLUME_STEP as f32 / VOLUME_REDUCTION; - - if self.volume > MAX_VOLUME { - self.volume = MAX_VOLUME; - } - } - - pub fn volume_down(&mut self) { - self.volume -= VOLUME_STEP as f32 / VOLUME_REDUCTION; - - if self.volume < MIN_VOLUME { - self.volume = MIN_VOLUME; - } + pub fn set_volume(&mut self, volume: u16) { + self.volume = volume as f32 / VOLUME_REDUCTION; } } @@ -265,10 +250,11 @@ pub struct Player { pub sample_rate: usize, pub state: State, pub songs: Index<Song>, + pub volume: u16, } impl Player { - pub fn new(_device: String, _volume: u16, _cache: &[Song]) -> Self { + pub fn new(_device: String, volume: u16, _cache: &[Song]) -> Self { let device = cpal::default_host().default_output_device().unwrap(); let config = device.default_output_config().unwrap().config(); @@ -293,6 +279,7 @@ impl Player { Self { sample_rate: config.sample_rate.0 as usize, stream, + volume, state: State::Stopped, songs: Index::default(), } @@ -326,7 +313,7 @@ impl Player { pub fn play(&mut self, path: &str) { unsafe { - RESAMPLER = Some(Resampler::new(self.sample_rate, path, 0.0)); + RESAMPLER = Some(Resampler::new(self.sample_rate, path, self.volume, 0.0)); } self.state = State::Playing; } @@ -337,6 +324,7 @@ impl Player { RESAMPLER = Some(Resampler::new( self.sample_rate, song.path.to_str().unwrap(), + self.volume, song.gain as f32, )); } @@ -384,28 +372,24 @@ impl Player { } } - pub fn volume_up(&self) { - unsafe { - if let Some(resampler) = RESAMPLER.as_mut() { - resampler.volume_up(); - } + pub fn volume_up(&mut self) { + self.volume += VOLUME_STEP; + if self.volume > 100 { + self.volume = 100; } - } - pub fn volume_down(&self) { - unsafe { - if let Some(resampler) = RESAMPLER.as_mut() { - resampler.volume_down(); - } + if let Some(resampler) = unsafe { RESAMPLER.as_mut() } { + resampler.set_volume(self.volume); } } - pub fn volume(&self) -> u16 { - //TODO: Volume needs to be stored in the player - //otherwise it will go away when the next song is played. - unsafe { - let volume = RESAMPLER.as_ref().unwrap().volume; - (volume * VOLUME_REDUCTION).round() as u16 + pub fn volume_down(&mut self) { + if self.volume != 0 { + self.volume -= VOLUME_STEP; + } + + if let Some(resampler) = unsafe { RESAMPLER.as_mut() } { + resampler.set_volume(self.volume); } } diff --git a/gonk/src/main.rs b/gonk/src/main.rs index 4a71aae4..02674abd 100644 --- a/gonk/src/main.rs +++ b/gonk/src/main.rs @@ -381,7 +381,7 @@ fn main() { } } - query::set_volume(player.volume()); + query::set_volume(player.volume); let ids: Vec<usize> = player .songs diff --git a/gonk/src/queue.rs b/gonk/src/queue.rs index efdfb58f..0f612ac5 100644 --- a/gonk/src/queue.rs +++ b/gonk/src/queue.rs @@ -145,7 +145,7 @@ fn draw_header(player: &mut Player, f: &mut Frame, area: Rect) { draw_title(player, f, area); } - let volume = Spans::from(format!("Vol: {}%─╮", player.volume())); + let volume = Spans::from(format!("Vol: {}%─╮", player.volume)); f.render_widget(Paragraph::new(volume).alignment(Alignment::Right), area); } diff --git a/gonk/src/status_bar.rs b/gonk/src/status_bar.rs index 2d54bb6c..b1c723ed 100644 --- a/gonk/src/status_bar.rs +++ b/gonk/src/status_bar.rs @@ -126,7 +126,7 @@ pub fn draw(status_bar: &mut StatusBar, area: Rect, f: &mut Frame, busy: bool, p //TODO: Draw mini progress bar here. let text = if player.is_playing() { - format!("Vol: {}% ", player.volume()) + format!("Vol: {}% ", player.volume) } else { String::from("Paused ") }; From bbf47cf11ad7786a8b38a70cd05583f73d1cb437 Mon Sep 17 00:00:00 2001 From: zX3no <dr_xeno@hotmail.com.au> Date: Mon, 11 Jul 2022 14:44:26 +0930 Subject: [PATCH 29/40] fixed play and pause not working --- gonk-player/src/lib.rs | 105 +++++++++++++++++++++++++++-------------- gonk/src/main.rs | 2 +- 2 files changed, 70 insertions(+), 37 deletions(-) diff --git a/gonk-player/src/lib.rs b/gonk-player/src/lib.rs index 072d3832..9065e879 100644 --- a/gonk-player/src/lib.rs +++ b/gonk-player/src/lib.rs @@ -2,7 +2,7 @@ use cpal::{ traits::{HostTrait, StreamTrait}, Stream, }; -use std::{fs::File, time::Duration, vec::IntoIter}; +use std::{fs::File, io::ErrorKind, time::Duration, vec::IntoIter}; use symphonia::{ core::{ audio::SampleBuffer, @@ -42,6 +42,7 @@ static mut RESAMPLER: Option<Resampler> = None; const VOLUME_STEP: u16 = 5; const VOLUME_REDUCTION: f32 = 600.0; +const MAX_DECODE_ERRORS: usize = 3; pub struct Resampler { probed: ProbeResult, @@ -138,7 +139,9 @@ impl Resampler { } pub fn next(&mut self) -> f32 { - if let Some(smp) = self.next_sample() { + if self.finished { + 0.0 + } else if let Some(smp) = self.next_sample() { if self.gain == 0.0 { //Reduce the volume a little to match //songs with replay gain information. @@ -147,30 +150,53 @@ impl Resampler { smp * self.volume * self.gain } } else { - let next_packet = self.probed.format.next_packet().unwrap(); - let decoded = self.decoder.decode(&next_packet).unwrap(); - let mut buffer = SampleBuffer::<f32>::new(decoded.capacity() as u64, *decoded.spec()); - buffer.copy_interleaved_ref(decoded); - self.buffer = buffer.samples().to_vec().into_iter(); - - let ts = next_packet.ts(); - let t = self.time_base.calc_time(ts); - self.elapsed = Duration::from_secs(t.seconds) + Duration::from_secs_f64(t.frac); - - if self.input == self.output { - self.current_frame = Vec::new(); - self.next_frame = Vec::new(); - } else { - self.current_frame = vec![self.buffer.next().unwrap(), self.buffer.next().unwrap()]; - self.next_frame = vec![self.buffer.next().unwrap(), self.buffer.next().unwrap()]; - } + loop { + let mut decode_errors: usize = 0; + match self.probed.format.next_packet() { + Ok(next_packet) => { + let decoded = self.decoder.decode(&next_packet).unwrap(); + let mut buffer = + SampleBuffer::<f32>::new(decoded.capacity() as u64, *decoded.spec()); + buffer.copy_interleaved_ref(decoded); + self.buffer = buffer.samples().to_vec().into_iter(); + + let ts = next_packet.ts(); + let t = self.time_base.calc_time(ts); + self.elapsed = + Duration::from_secs(t.seconds) + Duration::from_secs_f64(t.frac); + + if self.input == self.output { + self.current_frame = Vec::new(); + self.next_frame = Vec::new(); + } else { + self.current_frame = + vec![self.buffer.next().unwrap(), self.buffer.next().unwrap()]; + self.next_frame = + vec![self.buffer.next().unwrap(), self.buffer.next().unwrap()]; + } - self.current_frame_pos_in_chunk = 0; - self.next_output_frame_pos_in_chunk = 0; + self.current_frame_pos_in_chunk = 0; + self.next_output_frame_pos_in_chunk = 0; - debug_assert!(self.output_buffer.is_none()); + debug_assert!(self.output_buffer.is_none()); - self.next() + return self.next(); + } + Err(e) => match e { + Error::DecodeError(e) => { + decode_errors += 1; + if decode_errors > MAX_DECODE_ERRORS { + panic!("{:?}", e); + } + } + Error::IoError(e) if e.kind() == ErrorKind::UnexpectedEof => { + self.finished = true; + return 0.0; + } + _ => panic!("{:?}", e), + }, + } + } } } @@ -254,7 +280,7 @@ pub struct Player { } impl Player { - pub fn new(_device: String, volume: u16, _cache: &[Song]) -> Self { + pub fn new(_device: String, volume: u16, cache: &[Song]) -> Self { let device = cpal::default_host().default_output_device().unwrap(); let config = device.default_output_config().unwrap().config(); @@ -276,13 +302,16 @@ impl Player { stream.play().unwrap(); - Self { + let mut player = Self { sample_rate: config.sample_rate.0 as usize, stream, volume, state: State::Stopped, songs: Index::default(), - } + }; + player.add_songs(cache); + player.pause(); + player } pub fn update(&mut self) { @@ -299,6 +328,7 @@ impl Player { self.songs.select(Some(0)); self.play_selected(); } + // self.play(); } pub fn previous(&mut self) { @@ -311,13 +341,6 @@ impl Player { self.play_selected(); } - pub fn play(&mut self, path: &str) { - unsafe { - RESAMPLER = Some(Resampler::new(self.sample_rate, path, self.volume, 0.0)); - } - self.state = State::Playing; - } - pub fn play_selected(&mut self) { if let Some(song) = self.songs.selected() { unsafe { @@ -328,7 +351,7 @@ impl Player { song.gain as f32, )); } - self.state = State::Playing; + self.play(); } } @@ -413,12 +436,22 @@ impl Player { pub fn toggle_playback(&mut self) { match self.state { - State::Playing => self.stream.pause().unwrap(), - State::Paused => self.stream.play().unwrap(), + State::Playing => self.pause(), + State::Paused => self.play(), State::Stopped => (), } } + pub fn play(&mut self) { + self.stream.play().unwrap(); + self.state = State::Playing; + } + + pub fn pause(&mut self) { + self.stream.pause().unwrap(); + self.state = State::Paused; + } + pub fn seek_by(&mut self, time: f32) { unsafe { if RESAMPLER.is_none() { diff --git a/gonk/src/main.rs b/gonk/src/main.rs index 02674abd..ea4db85c 100644 --- a/gonk/src/main.rs +++ b/gonk/src/main.rs @@ -161,7 +161,7 @@ fn main() { let mut last_tick = Instant::now(); let mut busy = false; - //Using the a thread here is roughly 7ms faster. + //Using the another thread here is roughly 7ms faster. let mut player = player.join().unwrap(); loop { From 41076f15bfce4d78ce805d3fc6ac42a2edce55cc Mon Sep 17 00:00:00 2001 From: zX3no <dr_xeno@hotmail.com.au> Date: Mon, 11 Jul 2022 14:49:31 +0930 Subject: [PATCH 30/40] fixed player being unable to stop --- gonk-player/src/lib.rs | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/gonk-player/src/lib.rs b/gonk-player/src/lib.rs index 9065e879..971858d8 100644 --- a/gonk-player/src/lib.rs +++ b/gonk-player/src/lib.rs @@ -288,11 +288,14 @@ impl Player { .build_output_stream( &config, move |data: &mut [f32], _: &cpal::OutputCallbackInfo| { - if let Some(resampler) = unsafe { &mut RESAMPLER } { - for frame in data.chunks_mut(2) { - for sample in frame.iter_mut() { - *sample = resampler.next(); - } + for frame in data.chunks_mut(2) { + for sample in frame.iter_mut() { + let smp = if let Some(resampler) = unsafe { &mut RESAMPLER } { + resampler.next() + } else { + 0.0 + }; + *sample = smp; } } }, @@ -386,6 +389,9 @@ impl Player { pub fn clear(&mut self) { self.songs = Index::default(); self.state = State::Stopped; + unsafe { + RESAMPLER = None; + } } pub fn clear_except_playing(&mut self) { From f774172d589b0b6e765849354f628026e163fef0 Mon Sep 17 00:00:00 2001 From: zX3no <dr_xeno@hotmail.com.au> Date: Mon, 11 Jul 2022 14:59:25 +0930 Subject: [PATCH 31/40] fixed click at start up --- gonk-player/src/lib.rs | 23 +++++++++++++---------- gonk/src/main.rs | 5 +++++ 2 files changed, 18 insertions(+), 10 deletions(-) diff --git a/gonk-player/src/lib.rs b/gonk-player/src/lib.rs index 971858d8..78ca3f32 100644 --- a/gonk-player/src/lib.rs +++ b/gonk-player/src/lib.rs @@ -305,16 +305,15 @@ impl Player { stream.play().unwrap(); - let mut player = Self { + let index = if cache.is_empty() { None } else { Some(0) }; + + Self { sample_rate: config.sample_rate.0 as usize, stream, volume, state: State::Stopped, - songs: Index::default(), - }; - player.add_songs(cache); - player.pause(); - player + songs: Index::new(cache.to_vec(), index), + } } pub fn update(&mut self) { @@ -441,10 +440,14 @@ impl Player { } pub fn toggle_playback(&mut self) { - match self.state { - State::Playing => self.pause(), - State::Paused => self.play(), - State::Stopped => (), + if unsafe { RESAMPLER.is_none() } { + self.play_selected() + } else { + match self.state { + State::Playing => self.pause(), + State::Paused => self.play(), + State::Stopped => (), + } } } diff --git a/gonk/src/main.rs b/gonk/src/main.rs index ea4db85c..f89dfe10 100644 --- a/gonk/src/main.rs +++ b/gonk/src/main.rs @@ -164,6 +164,11 @@ fn main() { //Using the another thread here is roughly 7ms faster. let mut player = player.join().unwrap(); + //If there are songs in the queue, display the queue. + if !player.songs.is_empty() { + mode = Mode::Queue; + } + loop { match db.state() { State::Busy => busy = true, From 90ff212e0f2781fe3edb01fe7fc5a916dbf40558 Mon Sep 17 00:00:00 2001 From: zX3no <dr_xeno@hotmail.com.au> Date: Mon, 11 Jul 2022 18:33:47 +0930 Subject: [PATCH 32/40] fixed heap overrun --- gonk-player/src/index.rs | 6 ------ gonk-player/src/lib.rs | 36 ++++++++++++++++++++++++------------ gonk/src/playlist.rs | 1 + 3 files changed, 25 insertions(+), 18 deletions(-) diff --git a/gonk-player/src/index.rs b/gonk-player/src/index.rs index e779c135..c08d905c 100644 --- a/gonk-player/src/index.rs +++ b/gonk-player/src/index.rs @@ -90,12 +90,6 @@ impl<T> Index<T> { } } -impl<T: Clone> Index<T> { - pub fn clone(&self) -> Vec<T> { - self.data.to_owned() - } -} - impl<T> Default for Index<T> { fn default() -> Self { Self { diff --git a/gonk-player/src/lib.rs b/gonk-player/src/lib.rs index 78ca3f32..b1f5e4f2 100644 --- a/gonk-player/src/lib.rs +++ b/gonk-player/src/lib.rs @@ -72,9 +72,8 @@ pub struct Resampler { } impl Resampler { - pub fn new(output: usize, path: &str, volume: u16, gain: f32) -> Self { - let source = Box::new(File::open(path).unwrap()); - let mss = MediaSourceStream::new(source, Default::default()); + pub fn new(output: usize, file: File, volume: u16, gain: f32) -> Self { + let mss = MediaSourceStream::new(Box::new(file), Default::default()); let mut probed = get_probe() .format( @@ -291,7 +290,14 @@ impl Player { for frame in data.chunks_mut(2) { for sample in frame.iter_mut() { let smp = if let Some(resampler) = unsafe { &mut RESAMPLER } { - resampler.next() + //Makes sure that the next sample isn't + //read in the middle of changing songs. + //idk reliable this is. + if resampler.finished { + 0.0 + } else { + resampler.next() + } } else { 0.0 }; @@ -330,7 +336,6 @@ impl Player { self.songs.select(Some(0)); self.play_selected(); } - // self.play(); } pub fn previous(&mut self) { @@ -345,10 +350,18 @@ impl Player { pub fn play_selected(&mut self) { if let Some(song) = self.songs.selected() { + let file = match File::open(&song.path) { + Ok(file) => file, + //TODO: Print to status-bar + Err(_) => panic!("Could not open path: {:?}", song.path), + }; unsafe { + if let Some(resampler) = &mut RESAMPLER { + resampler.finished = true; + } RESAMPLER = Some(Resampler::new( self.sample_rate, - song.path.to_str().unwrap(), + file, self.volume, song.gain as f32, )); @@ -406,7 +419,7 @@ impl Player { self.volume = 100; } - if let Some(resampler) = unsafe { RESAMPLER.as_mut() } { + if let Some(resampler) = unsafe { &mut RESAMPLER } { resampler.set_volume(self.volume); } } @@ -416,7 +429,7 @@ impl Player { self.volume -= VOLUME_STEP; } - if let Some(resampler) = unsafe { RESAMPLER.as_mut() } { + if let Some(resampler) = unsafe { &mut RESAMPLER } { resampler.set_volume(self.volume); } } @@ -463,7 +476,7 @@ impl Player { pub fn seek_by(&mut self, time: f32) { unsafe { - if RESAMPLER.is_none() { + if RESAMPLER.is_none() || self.state != State::Playing { return; } @@ -472,13 +485,12 @@ impl Player { } pub fn seek_to(&mut self, time: f32) { - if unsafe { RESAMPLER.is_none() } { + if unsafe { RESAMPLER.is_none() } || self.state != State::Playing { return; } - let time = if time.is_sign_negative() { 0.0 } else { time }; + let time = Duration::from_secs_f32(time.clamp(0.0, f32::MAX)); - let time = Duration::from_secs_f32(time); unsafe { match RESAMPLER.as_mut().unwrap().probed.format.seek( SeekMode::Coarse, diff --git a/gonk/src/playlist.rs b/gonk/src/playlist.rs index 3df81fc9..09035b8a 100644 --- a/gonk/src/playlist.rs +++ b/gonk/src/playlist.rs @@ -301,6 +301,7 @@ pub fn draw(playlist: &mut Playlist, area: Rect, f: &mut Frame) { let items: Vec<ListItem> = playlist .playlists + .data .clone() .into_iter() .map(ListItem::new) From 934d1bb174a0f53e26a28a9d7727313273971090 Mon Sep 17 00:00:00 2001 From: zX3no <dr_xeno@hotmail.com.au> Date: Mon, 11 Jul 2022 18:42:18 +0930 Subject: [PATCH 33/40] fixed an issue where seeking would cause EOF --- gonk-player/src/lib.rs | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/gonk-player/src/lib.rs b/gonk-player/src/lib.rs index b1f5e4f2..ccee1e21 100644 --- a/gonk-player/src/lib.rs +++ b/gonk-player/src/lib.rs @@ -149,8 +149,8 @@ impl Resampler { smp * self.volume * self.gain } } else { + let mut decode_errors: usize = 0; loop { - let mut decode_errors: usize = 0; match self.probed.format.next_packet() { Ok(next_packet) => { let decoded = self.decoder.decode(&next_packet).unwrap(); @@ -489,7 +489,9 @@ impl Player { return; } - let time = Duration::from_secs_f32(time.clamp(0.0, f32::MAX)); + //Seeking at under 0.5 seconds causes an unexpected EOF. + //Could be because of the coarse seek. + let time = Duration::from_secs_f32(time.clamp(0.5, f32::MAX)); unsafe { match RESAMPLER.as_mut().unwrap().probed.format.seek( From 93eb0e4caf7c6a27532f2a583bec2ced8f71bbe3 Mon Sep 17 00:00:00 2001 From: zX3no <dr_xeno@hotmail.com.au> Date: Mon, 11 Jul 2022 18:52:14 +0930 Subject: [PATCH 34/40] prebuild the seek index --- gonk-player/src/lib.rs | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/gonk-player/src/lib.rs b/gonk-player/src/lib.rs index ccee1e21..4a220a81 100644 --- a/gonk-player/src/lib.rs +++ b/gonk-player/src/lib.rs @@ -79,7 +79,11 @@ impl Resampler { .format( &Hint::default(), mss, - &FormatOptions::default(), + &FormatOptions { + prebuild_seek_index: true, + seek_index_fill_rate: 10, + enable_gapless: false, + }, &MetadataOptions::default(), ) .unwrap(); From 4e7b1df5709584256dc71a3867738782c5f388a1 Mon Sep 17 00:00:00 2001 From: zX3no <dr_xeno@hotmail.com.au> Date: Mon, 11 Jul 2022 18:54:42 +0930 Subject: [PATCH 35/40] store replay gain as a float instead of a double --- gonk-database/src/lib.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gonk-database/src/lib.rs b/gonk-database/src/lib.rs index aaf91424..fcfb446f 100644 --- a/gonk-database/src/lib.rs +++ b/gonk-database/src/lib.rs @@ -70,7 +70,7 @@ pub fn create_tables(conn: &Connection) { disc INTEGER NOT NULL, number INTEGER NOT NULL, path TEXT NOT NULL, - gain DOUBLE NOT NULL, + gain FLOAT NOT NULL, album TEXT NOT NULL, artist TEXT NOT NULL, folder TEXT NOT NULL, From 04f41199dd1228098fc259d17e0d4f2f8fbd2a11 Mon Sep 17 00:00:00 2001 From: zX3no <dr_xeno@hotmail.com.au> Date: Tue, 12 Jul 2022 11:11:38 +0930 Subject: [PATCH 36/40] fixed: output devices can now be selected in the settings --- gonk-player/src/lib.rs | 103 +++++++++++++++++++++++++++++------------ gonk/src/main.rs | 4 +- gonk/src/settings.rs | 53 +++++++++------------ 3 files changed, 99 insertions(+), 61 deletions(-) diff --git a/gonk-player/src/lib.rs b/gonk-player/src/lib.rs index 4a220a81..d6549a2c 100644 --- a/gonk-player/src/lib.rs +++ b/gonk-player/src/lib.rs @@ -1,6 +1,6 @@ use cpal::{ traits::{HostTrait, StreamTrait}, - Stream, + BuildStreamError, Stream, StreamConfig, }; use std::{fs::File, io::ErrorKind, time::Duration, vec::IntoIter}; use symphonia::{ @@ -283,36 +283,23 @@ pub struct Player { } impl Player { - pub fn new(_device: String, volume: u16, cache: &[Song]) -> Self { - let device = cpal::default_host().default_output_device().unwrap(); - let config = device.default_output_config().unwrap().config(); + pub fn new(wanted_device: String, volume: u16, cache: &[Song]) -> Self { + let mut device = None; - let stream = device - .build_output_stream( - &config, - move |data: &mut [f32], _: &cpal::OutputCallbackInfo| { - for frame in data.chunks_mut(2) { - for sample in frame.iter_mut() { - let smp = if let Some(resampler) = unsafe { &mut RESAMPLER } { - //Makes sure that the next sample isn't - //read in the middle of changing songs. - //idk reliable this is. - if resampler.finished { - 0.0 - } else { - resampler.next() - } - } else { - 0.0 - }; - *sample = smp; - } - } - }, - |err| panic!("{}", err), - ) - .unwrap(); + for d in audio_devices() { + if d.name().unwrap() == wanted_device { + device = Some(d); + } + } + let device = if let Some(device) = device { + device + } else { + default_device() + }; + + let config = device.default_output_config().unwrap().config(); + let stream = create_output_stream(&device, &config).unwrap(); stream.play().unwrap(); let index = if cache.is_empty() { None } else { Some(0) }; @@ -326,6 +313,22 @@ impl Player { } } + pub fn set_output_device(&mut self, device: &Device) -> Result<(), String> { + match device.default_output_config() { + Ok(supported_stream) => { + match create_output_stream(device, &supported_stream.config()) { + Ok(stream) => { + self.stream = stream; + self.stream.play().unwrap(); + Ok(()) + } + Err(e) => Err(format!("{}", e)), + } + } + Err(e) => Err(format!("{}", e)), + } + } + pub fn update(&mut self) { if let Some(resampler) = unsafe { RESAMPLER.as_ref() } { if resampler.finished { @@ -525,3 +528,45 @@ impl Player { } unsafe impl Send for Player {} + +fn create_output_stream( + device: &Device, + config: &StreamConfig, +) -> Result<Stream, BuildStreamError> { + device.build_output_stream( + &config, + move |data: &mut [f32], _: &cpal::OutputCallbackInfo| { + for frame in data.chunks_mut(2) { + for sample in frame.iter_mut() { + let smp = if let Some(resampler) = unsafe { &mut RESAMPLER } { + //Makes sure that the next sample isn't + //read in the middle of changing songs. + //idk reliable this is. + if resampler.finished { + 0.0 + } else { + resampler.next() + } + } else { + 0.0 + }; + *sample = smp; + } + } + }, + |err| panic!("{}", err), + ) +} + +pub fn audio_devices() -> Vec<Device> { + let host_id = cpal::default_host().id(); + let host = cpal::host_from_id(host_id).unwrap(); + + //FIXME: Getting just the output devies was too slow(150ms). + //Collecting every device is still slow but it's not as bad. + host.devices().unwrap().collect() +} + +pub fn default_device() -> Device { + cpal::default_host().default_output_device().unwrap() +} diff --git a/gonk/src/main.rs b/gonk/src/main.rs index f89dfe10..b72796ee 100644 --- a/gonk/src/main.rs +++ b/gonk/src/main.rs @@ -136,8 +136,10 @@ fn main() { let cache = query::get_cache(); let volume = query::volume(); + let device = query::playback_device(); + //40ms - let player = thread::spawn(move || Player::new(String::new(), volume, &cache)); + let player = thread::spawn(move || Player::new(device, volume, &cache)); //3ms let mut browser = Browser::new(); diff --git a/gonk/src/settings.rs b/gonk/src/settings.rs index 16d9b862..900893cb 100644 --- a/gonk/src/settings.rs +++ b/gonk/src/settings.rs @@ -1,4 +1,3 @@ -#![allow(unused)] use crate::{widgets::*, Frame, Input}; use gonk_database::query; use gonk_player::{Device, DeviceTrait, Index, Player}; @@ -15,28 +14,26 @@ pub struct Settings { impl Settings { pub fn new() -> Self { - // let default_device = Player::default_device(); - // let wanted_device = query::playback_device(); + let default_device = gonk_player::default_device(); + let wanted_device = query::playback_device(); - // let devices = Player::audio_devices(); + let devices = gonk_player::audio_devices(); - // let current_device = if devices - // .iter() - // .flat_map(DeviceTrait::name) - // .any(|x| x == wanted_device) - // { - // wanted_device - // } else { - // let name = default_device.name().unwrap(); - // query::set_playback_device(&name); - // name - // }; + let current_device = if devices + .iter() + .flat_map(DeviceTrait::name) + .any(|x| x == wanted_device) + { + wanted_device + } else { + let name = default_device.name().unwrap(); + query::set_playback_device(&name); + name + }; Self { - // devices: Index::new(devices, Some(0)), - // current_device, - devices: Index::default(), - current_device: String::new(), + devices: Index::new(devices, Some(0)), + current_device, } } } @@ -56,20 +53,14 @@ impl Input for Settings { } pub fn on_enter(settings: &mut Settings, player: &mut Player) { - // if let Some(device) = settings.devices.selected() { - // match player.change_output_device(device) { - // Ok(_) => { - // let name = device.name().unwrap(); - // query::set_playback_device(&name); - // settings.current_device = name; - // } - // //TODO: Print error in status bar - // Err(e) => panic!("{:?}", e), - // } - // } + if let Some(device) = settings.devices.selected() { + match player.set_output_device(device) { + Ok(_) => (), + Err(e) => println!("{}", e), + } + } } -#[allow(unused)] pub fn draw(settings: &mut Settings, area: Rect, f: &mut Frame) { let items: Vec<ListItem> = settings .devices From bbd69367f03d9be313d39410dc1176296a9ddc8e Mon Sep 17 00:00:00 2001 From: zX3no <dr_xeno@hotmail.com.au> Date: Tue, 12 Jul 2022 11:57:14 +0930 Subject: [PATCH 37/40] scuffed error handling in the status bar --- gonk-player/src/lib.rs | 56 +++++++++++++++++----------------- gonk/src/error_bar.rs | 44 +++++++++++++++++++++++++++ gonk/src/main.rs | 69 +++++++++++++++++++++++++++++++++++------- gonk/src/playlist.rs | 12 ++++++-- gonk/src/settings.rs | 4 +-- 5 files changed, 142 insertions(+), 43 deletions(-) create mode 100644 gonk/src/error_bar.rs diff --git a/gonk-player/src/lib.rs b/gonk-player/src/lib.rs index d6549a2c..cd4fb7d2 100644 --- a/gonk-player/src/lib.rs +++ b/gonk-player/src/lib.rs @@ -329,38 +329,40 @@ impl Player { } } - pub fn update(&mut self) { + pub fn update(&mut self) -> Result<(), String> { if let Some(resampler) = unsafe { RESAMPLER.as_ref() } { if resampler.finished { - self.next(); + return self.next(); } } + Ok(()) } - pub fn add_songs(&mut self, songs: &[Song]) { + pub fn add_songs(&mut self, songs: &[Song]) -> Result<(), String> { self.songs.data.extend(songs.to_vec()); if self.songs.selected().is_none() { self.songs.select(Some(0)); - self.play_selected(); + self.play_selected() + } else { + Ok(()) } } - pub fn previous(&mut self) { + pub fn previous(&mut self) -> Result<(), String> { self.songs.up(); - self.play_selected(); + self.play_selected() } - pub fn next(&mut self) { + pub fn next(&mut self) -> Result<(), String> { self.songs.down(); - self.play_selected(); + self.play_selected() } - pub fn play_selected(&mut self) { + fn play_selected(&mut self) -> Result<(), String> { if let Some(song) = self.songs.selected() { let file = match File::open(&song.path) { Ok(file) => file, - //TODO: Print to status-bar - Err(_) => panic!("Could not open path: {:?}", song.path), + Err(_) => return Err(format!("Could not open file: {:?}", song.path)), }; unsafe { if let Some(resampler) = &mut RESAMPLER { @@ -375,34 +377,34 @@ impl Player { } self.play(); } + Ok(()) } - pub fn play_index(&mut self, i: usize) { + pub fn play_index(&mut self, i: usize) -> Result<(), String> { self.songs.select(Some(i)); - self.play_selected(); + self.play_selected() } - pub fn delete_index(&mut self, i: usize) { + pub fn delete_index(&mut self, i: usize) -> Result<(), String> { self.songs.data.remove(i); if let Some(playing) = self.songs.index() { let len = self.songs.len(); if len == 0 { - return self.clear(); - } - - if i == playing && i == 0 { + self.clear(); + } else if i == playing && i == 0 { if i == 0 { self.songs.select(Some(0)); } - self.play_selected(); + return self.play_selected(); } else if i == playing && i == len { self.songs.select(Some(len - 1)); } else if i < playing { self.songs.select(Some(playing - 1)); } }; + Ok(()) } pub fn clear(&mut self) { @@ -461,7 +463,7 @@ impl Player { pub fn toggle_playback(&mut self) { if unsafe { RESAMPLER.is_none() } { - self.play_selected() + self.play_selected().unwrap() } else { match self.state { State::Playing => self.pause(), @@ -481,19 +483,19 @@ impl Player { self.state = State::Paused; } - pub fn seek_by(&mut self, time: f32) { + pub fn seek_by(&mut self, time: f32) -> Result<(), String> { unsafe { if RESAMPLER.is_none() || self.state != State::Playing { - return; + return Ok(()); } - self.seek_to(RESAMPLER.as_ref().unwrap().elapsed.as_secs_f32() + time); + self.seek_to(RESAMPLER.as_ref().unwrap().elapsed.as_secs_f32() + time) } } - pub fn seek_to(&mut self, time: f32) { + pub fn seek_to(&mut self, time: f32) -> Result<(), String> { if unsafe { RESAMPLER.is_none() } || self.state != State::Playing { - return; + return Ok(()); } //Seeking at under 0.5 seconds causes an unexpected EOF. @@ -508,11 +510,11 @@ impl Player { track_id: None, }, ) { - Ok(_) => (), + Ok(_) => Ok(()), Err(e) => match e { Error::SeekError(e) => match e { SeekErrorKind::OutOfRange => { - self.next(); + return self.next(); } _ => panic!("{:?}", e), }, diff --git a/gonk/src/error_bar.rs b/gonk/src/error_bar.rs new file mode 100644 index 00000000..e5f84aa4 --- /dev/null +++ b/gonk/src/error_bar.rs @@ -0,0 +1,44 @@ +use crate::{Frame, ERROR, SHOW_ERROR}; +use std::time::{Duration, Instant}; +use tui::{ + layout::{Alignment, Rect}, + widgets::{Block, BorderType, Borders, Paragraph}, +}; + +const WAIT_TIME: Duration = Duration::from_secs(2); + +pub struct ErrorBar { + pub timer: Option<Instant>, +} + +impl ErrorBar { + pub fn new() -> Self { + Self { timer: None } + } + pub fn start(&mut self) { + self.timer = Some(Instant::now()); + } +} + +pub fn draw(error_bar: &mut ErrorBar, area: Rect, f: &mut Frame) { + if let Some(timer) = error_bar.timer { + if timer.elapsed() >= WAIT_TIME { + error_bar.timer = None; + unsafe { + SHOW_ERROR = false; + ERROR = String::new(); + } + } + } + + let message = unsafe { ERROR.as_str() }; + + f.render_widget( + Paragraph::new(message).alignment(Alignment::Left).block( + Block::default() + .borders(Borders::ALL) + .border_type(BorderType::Rounded), + ), + area, + ); +} diff --git a/gonk/src/main.rs b/gonk/src/main.rs index b72796ee..c4fd208a 100644 --- a/gonk/src/main.rs +++ b/gonk/src/main.rs @@ -1,5 +1,6 @@ use browser::Browser; use crossterm::{event::*, terminal::*, *}; +use error_bar::ErrorBar; use gonk_database::{query, Database, State}; use gonk_player::Player; use playlist::{Mode as PlaylistMode, Playlist}; @@ -16,6 +17,7 @@ use std::{ use tui::{backend::CrosstermBackend, layout::*, style::Color, Terminal}; mod browser; +mod error_bar; mod playlist; mod queue; mod search; @@ -25,6 +27,15 @@ mod widgets; type Frame<'a> = tui::Frame<'a, CrosstermBackend<Stdout>>; +static mut SHOW_ERROR: bool = false; +static mut ERROR: String = String::new(); + +pub fn set_error(message: String) { + unsafe { + ERROR = message; + } +} + pub struct Colors { pub number: Color, pub name: Color, @@ -150,6 +161,8 @@ fn main() { //200 ns let mut status_bar = StatusBar::new(); + let mut error_bar = ErrorBar::new(); + //68 us let mut playlist = Playlist::new(); @@ -189,7 +202,18 @@ fn main() { } queue.len = player.songs.len(); - player.update(); + + match player.update() { + Ok(_) => (), + Err(e) => set_error(e), + }; + + unsafe { + if !ERROR.is_empty() && SHOW_ERROR == false { + error_bar.start(); + SHOW_ERROR = true; + } + } terminal .draw(|f| { @@ -198,8 +222,8 @@ fn main() { .constraints([Constraint::Min(2), Constraint::Length(3)]) .split(f.size()); - let (top, bottom) = if status_bar.hidden { - (f.size(), area[1]) + let (top, bottom) = if status_bar.hidden && unsafe { !SHOW_ERROR } { + (f.size(), Rect::default()) } else { (area[0], area[1]) }; @@ -212,7 +236,9 @@ fn main() { Mode::Settings => settings::draw(&mut settings, top, f), }; - if mode != Mode::Queue { + if unsafe { SHOW_ERROR } { + error_bar::draw(&mut error_bar, bottom, f); + } else if mode != Mode::Queue { status_bar::draw(&mut status_bar, bottom, f, busy, &player); } }) @@ -269,10 +295,22 @@ fn main() { _ => (), }, KeyCode::Char('u') if mode == Mode::Browser => db.refresh(), - KeyCode::Char('q') => player.seek_by(-10.0), - KeyCode::Char('e') => player.seek_by(10.0), - KeyCode::Char('a') => player.previous(), - KeyCode::Char('d') => player.next(), + KeyCode::Char('q') => match player.seek_by(-10.0) { + Ok(_) => (), + Err(e) => set_error(e), + }, + KeyCode::Char('e') => match player.seek_by(10.0) { + Ok(_) => (), + Err(e) => set_error(e), + }, + KeyCode::Char('a') => match player.previous() { + Ok(_) => (), + Err(e) => set_error(e), + }, + KeyCode::Char('d') => match player.next() { + Ok(_) => (), + Err(e) => set_error(e), + }, KeyCode::Char('w') => player.volume_up(), KeyCode::Char('s') => player.volume_down(), //TODO: Rework mode changing buttons @@ -328,16 +366,25 @@ fn main() { KeyCode::Enter => match mode { Mode::Browser => { let songs = browser::get_selected(&browser); - player.add_songs(&songs); + match player.add_songs(&songs) { + Ok(_) => (), + Err(e) => set_error(e), + } } Mode::Queue => { if let Some(i) = queue.ui.index() { - player.play_index(i); + match player.play_index(i) { + Ok(_) => (), + Err(e) => set_error(e), + } } } Mode::Search => { if let Some(songs) = search::on_enter(&mut search) { - player.add_songs(&songs); + match player.add_songs(&songs) { + Ok(_) => (), + Err(e) => set_error(e), + } } } Mode::Settings => settings::on_enter(&mut settings, &mut player), diff --git a/gonk/src/playlist.rs b/gonk/src/playlist.rs index 09035b8a..333992c3 100644 --- a/gonk/src/playlist.rs +++ b/gonk/src/playlist.rs @@ -1,4 +1,4 @@ -use crate::{widgets::*, Frame, Input, COLORS}; +use crate::{set_error, widgets::*, Frame, Input, COLORS}; use gonk_database::playlist::PlaylistSong; use gonk_database::{playlist, query}; use gonk_player::{Index, Player, Song}; @@ -94,12 +94,18 @@ pub fn on_enter(playlist: &mut Playlist, player: &mut Player) { Mode::Playlist => { let ids: Vec<usize> = playlist.songs.data.iter().map(|song| song.id).collect(); let songs = query::songs_from_ids(&ids); - player.add_songs(&songs); + match player.add_songs(&songs) { + Ok(_) => (), + Err(e) => set_error(e), + } } Mode::Song => { if let Some(item) = playlist.songs.selected() { let song = query::songs_from_ids(&[item.id]).remove(0); - player.add_songs(&[song]); + match player.add_songs(&[song]) { + Ok(_) => (), + Err(e) => set_error(e), + } } } Mode::Popup if !playlist.song_buffer.is_empty() => { diff --git a/gonk/src/settings.rs b/gonk/src/settings.rs index 900893cb..851d6829 100644 --- a/gonk/src/settings.rs +++ b/gonk/src/settings.rs @@ -1,4 +1,4 @@ -use crate::{widgets::*, Frame, Input}; +use crate::{set_error, widgets::*, Frame, Input}; use gonk_database::query; use gonk_player::{Device, DeviceTrait, Index, Player}; use tui::{ @@ -56,7 +56,7 @@ pub fn on_enter(settings: &mut Settings, player: &mut Player) { if let Some(device) = settings.devices.selected() { match player.set_output_device(device) { Ok(_) => (), - Err(e) => println!("{}", e), + Err(e) => set_error(e), } } } From dfaeac3531b3fffdf7e09151d75db8a3b9753426 Mon Sep 17 00:00:00 2001 From: zX3no <dr_xeno@hotmail.com.au> Date: Tue, 12 Jul 2022 11:58:11 +0930 Subject: [PATCH 38/40] cleanup error handling --- gonk/src/queue.rs | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/gonk/src/queue.rs b/gonk/src/queue.rs index 0f612ac5..5a3c651f 100644 --- a/gonk/src/queue.rs +++ b/gonk/src/queue.rs @@ -58,7 +58,10 @@ pub fn constraint(queue: &mut Queue, row: usize, shift: bool) { pub fn delete(queue: &mut Queue, player: &mut Player) { if let Some(i) = queue.ui.index() { - player.delete_index(i); + match player.delete_index(i) { + Ok(_) => (), + Err(e) => set_error(e), + }; //make sure the ui index is in sync let len = player.songs.len().saturating_sub(1); if i > len { @@ -101,7 +104,10 @@ pub fn draw(queue: &mut Queue, player: &mut Player, f: &mut Frame, event: Option { let ratio = x as f32 / size.width as f32; let duration = player.duration().as_secs_f32(); - player.seek_to(duration * ratio); + match player.seek_to(duration * ratio) { + Ok(_) => (), + Err(e) => set_error(e), + }; } //Mouse support for the queue. From 14d6c3fbd21003b7776bde17afb8e8b93ed4f98a Mon Sep 17 00:00:00 2001 From: zX3no <dr_xeno@hotmail.com.au> Date: Tue, 12 Jul 2022 12:05:31 +0930 Subject: [PATCH 39/40] more error handling --- gonk-database/src/lib.rs | 2 +- gonk-database/src/query.rs | 2 +- gonk-player/src/lib.rs | 7 ++++--- gonk/src/main.rs | 13 ++++++++++++- 4 files changed, 18 insertions(+), 6 deletions(-) diff --git a/gonk-database/src/lib.rs b/gonk-database/src/lib.rs index fcfb446f..99208f45 100644 --- a/gonk-database/src/lib.rs +++ b/gonk-database/src/lib.rs @@ -142,7 +142,7 @@ pub fn rescan_folders() { } pub fn add_folder(folder: &str) { - let folder = folder.replace('\\', "/"); + let folder = folder.replace('/', "\\"); conn() .execute( diff --git a/gonk-database/src/query.rs b/gonk-database/src/query.rs index 249459aa..ebf818b0 100644 --- a/gonk-database/src/query.rs +++ b/gonk-database/src/query.rs @@ -50,7 +50,7 @@ pub fn folders() -> Vec<String> { } pub fn remove_folder(path: &str) -> Result<(), &str> { - let path = path.replace('\\', "/"); + let path = path.replace('/', "\\"); let conn = conn(); conn.execute("DELETE FROM song WHERE folder = ?", [&path]) diff --git a/gonk-player/src/lib.rs b/gonk-player/src/lib.rs index cd4fb7d2..c925d4f9 100644 --- a/gonk-player/src/lib.rs +++ b/gonk-player/src/lib.rs @@ -461,15 +461,16 @@ impl Player { } } - pub fn toggle_playback(&mut self) { + pub fn toggle_playback(&mut self) -> Result<(), String> { if unsafe { RESAMPLER.is_none() } { - self.play_selected().unwrap() + self.play_selected() } else { match self.state { State::Playing => self.pause(), State::Paused => self.play(), State::Stopped => (), - } + }; + Ok(()) } } diff --git a/gonk/src/main.rs b/gonk/src/main.rs index c4fd208a..ae7402ce 100644 --- a/gonk/src/main.rs +++ b/gonk/src/main.rs @@ -28,10 +28,12 @@ mod widgets; type Frame<'a> = tui::Frame<'a, CrosstermBackend<Stdout>>; static mut SHOW_ERROR: bool = false; +static mut OLD_ERROR: String = String::new(); static mut ERROR: String = String::new(); pub fn set_error(message: String) { unsafe { + OLD_ERROR = ERROR.clone(); ERROR = message; } } @@ -209,6 +211,12 @@ fn main() { }; unsafe { + if OLD_ERROR != ERROR { + error_bar.start(); + SHOW_ERROR = true; + OLD_ERROR = ERROR.clone(); + } + if !ERROR.is_empty() && SHOW_ERROR == false { error_bar.start(); SHOW_ERROR = true; @@ -280,7 +288,10 @@ fn main() { playlist.search.push(c); } } - KeyCode::Char(' ') => player.toggle_playback(), + KeyCode::Char(' ') => match player.toggle_playback() { + Ok(_) => (), + Err(e) => set_error(e), + }, KeyCode::Char('C') if shift => { player.clear_except_playing(); queue.ui.select(Some(0)); From defb38701934b490e4a390e349b922c38b8f0401 Mon Sep 17 00:00:00 2001 From: zX3no <dr_xeno@hotmail.com.au> Date: Tue, 12 Jul 2022 12:17:42 +0930 Subject: [PATCH 40/40] handle some edge cases for errors --- gonk/src/error_bar.rs | 3 ++- gonk/src/queue.rs | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/gonk/src/error_bar.rs b/gonk/src/error_bar.rs index e5f84aa4..e70d97a9 100644 --- a/gonk/src/error_bar.rs +++ b/gonk/src/error_bar.rs @@ -1,4 +1,4 @@ -use crate::{Frame, ERROR, SHOW_ERROR}; +use crate::{Frame, ERROR, OLD_ERROR, SHOW_ERROR}; use std::time::{Duration, Instant}; use tui::{ layout::{Alignment, Rect}, @@ -27,6 +27,7 @@ pub fn draw(error_bar: &mut ErrorBar, area: Rect, f: &mut Frame) { unsafe { SHOW_ERROR = false; ERROR = String::new(); + OLD_ERROR = String::new(); } } } diff --git a/gonk/src/queue.rs b/gonk/src/queue.rs index 5a3c651f..8a5ba971 100644 --- a/gonk/src/queue.rs +++ b/gonk/src/queue.rs @@ -204,7 +204,7 @@ fn draw_body( f: &mut Frame, area: Rect, ) -> Option<(usize, usize)> { - if player.songs.is_empty() { + if player.songs.is_empty() && unsafe { ERROR.is_empty() } { f.render_widget( Block::default() .border_type(BorderType::Rounded)