2022-01-03 23:19:10 +00:00
|
|
|
use std::time::Instant;
|
|
|
|
use core::time::Duration;
|
|
|
|
use rodio::Decoder;
|
|
|
|
use symphonia::core::formats::FormatReader;
|
|
|
|
use rodio::source::Buffered;
|
|
|
|
use rodio::OutputStream;
|
|
|
|
use rodio::Sink;
|
|
|
|
use rodio::Source;
|
|
|
|
|
|
|
|
use std::fmt;
|
|
|
|
|
|
|
|
pub type Song = Buffered<Decoder<std::io::Empty>>;
|
|
|
|
|
|
|
|
pub struct Player {
|
|
|
|
// [Buffered] is a pointer to a linked-list representation of the song, and so cloning
|
|
|
|
// it is as cheap as cloning an [Arc]. This should always point to the start of the
|
|
|
|
// song though, and so should not be changed after being initialized.
|
|
|
|
song: Song,
|
|
|
|
sink: Option<(Sink, OutputStream)>,
|
|
|
|
duration: Duration,
|
|
|
|
|
|
|
|
/// The position of the playhead when playback started or was last stopped
|
|
|
|
start_position: Duration,
|
|
|
|
|
|
|
|
/// The [`Instant`] playback started
|
|
|
|
playback_started: Option<Instant>,
|
|
|
|
}
|
|
|
|
|
|
|
|
impl Player {
|
|
|
|
|
|
|
|
/// Create a new player from a song
|
2022-01-05 20:42:12 +00:00
|
|
|
///
|
|
|
|
/// IMPORTANT: Because computing the duration of a song is currently a very expensive
|
|
|
|
/// task, this DOES NOT COMPUTE THE DURATION. All calculations that require a
|
|
|
|
/// duration assume a duration of 2:30s until a duration is manually calculated with
|
|
|
|
/// [`compute_duration()`] AND passed back into [`set_duration()`].
|
2022-01-03 23:19:10 +00:00
|
|
|
pub fn new(song: Box<dyn FormatReader>) -> Result<Self, PlayerError> {
|
|
|
|
|
|
|
|
let song = Decoder::new_from_format_reader(song)
|
|
|
|
.map_err(PlayerError::DecoderError)?
|
|
|
|
.buffered();
|
|
|
|
|
2022-01-05 20:42:12 +00:00
|
|
|
let duration = Duration::from_secs(150);
|
2022-01-03 23:19:10 +00:00
|
|
|
|
|
|
|
let mut player = Player {
|
|
|
|
sink: None,
|
|
|
|
start_position: Duration::ZERO,
|
|
|
|
playback_started: None,
|
|
|
|
song, duration, };
|
|
|
|
player.get_or_set_sink()?;
|
|
|
|
|
|
|
|
Ok(player)
|
|
|
|
}
|
|
|
|
|
|
|
|
/// Check if an audio sink is available
|
|
|
|
///
|
|
|
|
/// Returns `true` if an output device is currently loaded, or `false` if a device was
|
|
|
|
/// not availble last time we tried to access one.
|
|
|
|
pub fn has_output_device(&self) -> bool {
|
|
|
|
self.sink.is_some()
|
|
|
|
}
|
|
|
|
|
|
|
|
/// Attempt to re-request access to the output sink
|
|
|
|
///
|
|
|
|
/// Returns `&self.sink`
|
|
|
|
///
|
|
|
|
/// Can produce a [PlayerError::PlayError]
|
|
|
|
fn try_set_sink(&mut self) -> Result<Option<&Sink>, PlayerError> {
|
|
|
|
self.sink = Some(OutputStream::try_default())
|
|
|
|
.and_then(|result|
|
|
|
|
if let Err(&rodio::StreamError::NoDevice) = result.as_ref() {
|
|
|
|
None // This is okay and doesn't need to raise an error
|
|
|
|
} else {
|
|
|
|
Some(result) // We'll report this error
|
|
|
|
})
|
|
|
|
.transpose()
|
|
|
|
.map_err(PlayerError::StreamError)?
|
|
|
|
.map(|(stream, handle)| Sink::try_new(&handle).map(|sink| (sink, stream)))
|
|
|
|
.transpose()
|
|
|
|
.map_err(PlayerError::PlayError)?;
|
|
|
|
|
|
|
|
Ok(self.sink.as_ref().map(|(s, _)| s))
|
|
|
|
}
|
|
|
|
|
|
|
|
/// Return the current active sink, or attempt to request a new one
|
|
|
|
///
|
|
|
|
/// Equivilent to calling [`try_set_sink()`] if the sink is missing
|
|
|
|
fn get_or_set_sink(&mut self) -> Result<Option<&Sink>, PlayerError> {
|
|
|
|
if self.sink.is_some() {
|
|
|
|
Ok(self.sink.as_ref().map(|(s, _)| s))
|
|
|
|
} else {
|
|
|
|
let song = self.song.clone();
|
|
|
|
let out = self.try_set_sink();
|
|
|
|
|
|
|
|
if let Ok(Some(sink)) = out.as_ref() {
|
|
|
|
sink.pause();
|
|
|
|
sink.append(song);
|
|
|
|
}
|
|
|
|
|
|
|
|
out
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/// Attempt to resume playback, or start fresh
|
|
|
|
///
|
|
|
|
/// Returns `true` if playback was resumed, `false` if there is no audio sink
|
|
|
|
/// available to start play, and [PlayerError] if there was a problem requesting
|
|
|
|
/// access to the audio sink.
|
|
|
|
pub fn play(&mut self) -> Result<bool, PlayerError> {
|
|
|
|
if let Some(sink) = self.get_or_set_sink()? {
|
|
|
|
sink.play();
|
|
|
|
self.playback_started = Some(Instant::now());
|
|
|
|
Ok(true)
|
|
|
|
} else {
|
|
|
|
Ok(false)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/// Pause playback if playing, do nothing otherwise
|
|
|
|
///
|
|
|
|
/// May be resumed later with [`play()`]
|
|
|
|
pub fn pause(&mut self) {
|
|
|
|
if let Some((sink, _)) = &self.sink {
|
|
|
|
sink.pause();
|
|
|
|
self.start_position = self.position();
|
|
|
|
self.playback_started = None;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/// Returns true if the song is currently playing
|
|
|
|
///
|
|
|
|
/// That is, if playback has started, has not been paused, and has not ended naturally
|
|
|
|
/// yet.
|
|
|
|
pub fn is_playing(&self) -> bool {
|
|
|
|
self.playback_started.is_some() && self.position() < self.duration
|
|
|
|
}
|
|
|
|
|
|
|
|
/// Toggle playing
|
|
|
|
///
|
|
|
|
/// This calls [`play()`] is the track is currently paused or stopped, and calls
|
|
|
|
/// [`pause()`] is the track is currently playing. If the track was already playing,
|
|
|
|
/// this will always return `Ok(true)`.
|
|
|
|
pub fn toggle_play(&mut self) -> Result<bool, PlayerError> {
|
|
|
|
if self.position() == self.duration {
|
|
|
|
self.seek(Duration::ZERO)
|
|
|
|
.and_then(|device| if device { self.play() } else { Ok(false) } )
|
|
|
|
} else if self.playback_started.is_none() {
|
|
|
|
self.play()
|
|
|
|
} else {
|
|
|
|
self.pause();
|
|
|
|
Ok(true)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/// Return the duration of this track
|
|
|
|
///
|
|
|
|
/// This is cheap and exact. All processing is done before hand.
|
2022-01-08 14:14:11 +00:00
|
|
|
#[allow(dead_code)]
|
2022-01-03 23:19:10 +00:00
|
|
|
pub fn get_duration(&self) -> Duration {
|
|
|
|
self.duration
|
|
|
|
}
|
|
|
|
|
|
|
|
/// Attempt to seek to a given duration
|
|
|
|
///
|
|
|
|
/// This is pretty expensive, since due to technical limitations, this means resetting
|
|
|
|
/// the player to the beginning before skipping the given duration.
|
|
|
|
///
|
|
|
|
/// This can fail if there is no current output sink, and the attempt to access a
|
|
|
|
/// new one fails. If this is the case, false will be returned, unless the reason the
|
|
|
|
/// access failed was due to an error.
|
|
|
|
pub fn seek(&mut self, duration: Duration) -> Result<bool, PlayerError> {
|
|
|
|
|
|
|
|
let was_stopped = self.sink
|
|
|
|
.as_mut()
|
|
|
|
.map(|(s, _)| s.is_paused() || s.empty())
|
|
|
|
.unwrap_or(false);
|
|
|
|
|
|
|
|
let song = self.song.clone();
|
|
|
|
if let Some(sink) = self.try_set_sink()? {
|
|
|
|
sink.pause();
|
|
|
|
sink.append(
|
|
|
|
song.skip_duration(duration)
|
|
|
|
);
|
|
|
|
if was_stopped {
|
|
|
|
self.playback_started = None;
|
|
|
|
} else {
|
|
|
|
sink.play();
|
|
|
|
self.playback_started = Some(Instant::now());
|
|
|
|
}
|
|
|
|
self.start_position = duration;
|
|
|
|
Ok(true)
|
|
|
|
} else {
|
|
|
|
Ok(false)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/// Seek to a specific percentage (out of 1.0)
|
|
|
|
///
|
|
|
|
/// Performs a [`seek()`] operation, seeking to a given percentage of the song's full
|
|
|
|
/// length. See [`seek()`] for more details
|
|
|
|
pub fn seek_percentage(&mut self, percent: f32) -> Result<bool, PlayerError> {
|
|
|
|
self.seek(
|
|
|
|
self.duration.mul_f32(percent)
|
|
|
|
)
|
|
|
|
}
|
|
|
|
|
|
|
|
/// How far into the song the playback head currently is
|
|
|
|
///
|
|
|
|
/// Computes the duration of the song that is before the playback head. This is
|
|
|
|
/// really an approximation based on how much time has elapsed since playback started,
|
|
|
|
/// but it should be close enough for almost all purposes.
|
|
|
|
pub fn position(&self) -> Duration {
|
|
|
|
self.duration.min(
|
|
|
|
self.start_position +
|
|
|
|
self.playback_started.map_or(
|
|
|
|
Duration::ZERO,
|
|
|
|
|ts| Instant::now() - ts,
|
|
|
|
)
|
|
|
|
)
|
|
|
|
}
|
|
|
|
|
|
|
|
/// Computes the position as a fraction of the song duration
|
|
|
|
///
|
|
|
|
/// 0.0 represents the beginning of the song, while 1.0 represents the end. See
|
|
|
|
/// [`position()`] for more information.
|
|
|
|
pub fn position_percent(&self) -> f32 {
|
|
|
|
// nightly: self.position().div_duration_f32(self.duration)
|
|
|
|
self.position().as_secs_f32() / self.duration.as_secs_f32()
|
|
|
|
}
|
|
|
|
|
2022-01-05 20:42:12 +00:00
|
|
|
/// Computes the exact duration of the current song.
|
|
|
|
///
|
|
|
|
/// This is an EXPENSIVE, BLOCKING long-running function that should be run in a
|
|
|
|
/// seperate thread. Hopefully this will be optimized in the future, but this is what
|
|
|
|
/// we have for now.
|
|
|
|
///
|
|
|
|
/// This does not set the duration of this song, so be sure to call [`set_duration()`]
|
|
|
|
/// with the result of this call after it finishes executing.
|
|
|
|
///
|
|
|
|
/// Note: This doesn't actually preform the calculation, but instead returns a
|
|
|
|
/// `'static FnOnce` that can be used to do so, in order to simplify running the
|
|
|
|
/// transaction in another thread.
|
|
|
|
///
|
|
|
|
/// Technical Details: Currently, this involves decoding and inefficiently counting
|
|
|
|
/// the number of samples in the song. Hopefully, we'll be able to do this more
|
|
|
|
/// efficiently in the future, pending mostly on
|
|
|
|
/// https://github.com/RustAudio/rodio/issues/405
|
|
|
|
pub fn compute_duration(&self) -> impl FnOnce() -> Duration {
|
|
|
|
let sample_rate = self.song.sample_rate() as u64;
|
|
|
|
let n_channels = self.song.channels() as u64;
|
|
|
|
let song_clone = self.song.clone();
|
|
|
|
|
|
|
|
move|| {
|
|
|
|
let n_samples = song_clone.count() as u64; // expensive!
|
|
|
|
let n_whole_seconds = n_samples / n_channels / sample_rate;
|
|
|
|
let remaining_samples = n_samples % sample_rate;
|
|
|
|
let n_nanos = remaining_samples * 1_000_000_000 / sample_rate;
|
|
|
|
Duration::new(n_whole_seconds, n_nanos as u32)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/// Set the duration of the song
|
|
|
|
///
|
|
|
|
/// This struct does not automatically compute its own duration, so until this method
|
|
|
|
/// is called, we just assume the duration is 2:30s for any calculations that require
|
|
|
|
/// it.
|
|
|
|
///
|
|
|
|
/// In order to use an exact duration, use another thread to run
|
|
|
|
/// [`compute_duration()`] and pass the result into this method.
|
|
|
|
pub fn set_duration(&mut self, duration: Duration) {
|
|
|
|
self.duration = duration;
|
|
|
|
}
|
2022-01-03 23:19:10 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
#[derive(Debug)]
|
|
|
|
pub enum PlayerError {
|
|
|
|
DecoderError(rodio::decoder::DecoderError),
|
|
|
|
PlayError(rodio::PlayError),
|
|
|
|
StreamError(rodio::StreamError),
|
|
|
|
}
|
|
|
|
|
|
|
|
impl fmt::Display for PlayerError {
|
|
|
|
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
|
|
|
match self {
|
|
|
|
Self::DecoderError(e) => write!(f, "Could not decode the provided song: {}", e),
|
|
|
|
Self::PlayError(e) => write!(f, "Problem playing to the audio output: {}", e),
|
|
|
|
Self::StreamError(e) => write!(f, "Problem connecting to the audio output: {}", e),
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
impl std::error::Error for PlayerError { }
|