deLyrium/src/player.rs

293 lines
8.9 KiB
Rust

use std::fs::File;
use std::time::Instant;
use core::time::Duration;
use rodio::Decoder;
use rodio::source::Buffered;
use rodio::OutputStream;
use rodio::Sink;
use rodio::Source;
use std::fmt;
pub type Song = Buffered<Decoder<File>>;
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
///
/// IMPORTANT: Because computing the duration of a song is currently a very expensive
/// task, this DOES NOT COMPUTE THE DURATION. All calculations that require a
/// duration assume a duration of 2:30s until a duration is manually calculated with
/// [`compute_duration()`] AND passed back into [`set_duration()`].
pub fn new(song: File) -> Result<Self, PlayerError> {
let song = Decoder::new(song)
.map_err(PlayerError::Decoder)?
.buffered();
let duration = Duration::from_secs(150);
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::Stream)?
.map(|(stream, handle)| Sink::try_new(&handle).map(|sink| (sink, stream)))
.transpose()
.map_err(PlayerError::Play)?;
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.
#[allow(dead_code)]
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()
}
/// Computes the exact duration of the current song.
///
/// This is an EXPENSIVE, BLOCKING long-running function that should be run in a
/// seperate thread. Hopefully this will be optimized in the future, but this is what
/// we have for now.
///
/// This does not set the duration of this song, so be sure to call [`set_duration()`]
/// with the result of this call after it finishes executing.
///
/// Note: This doesn't actually preform the calculation, but instead returns a
/// `'static FnOnce` that can be used to do so, in order to simplify running the
/// transaction in another thread.
///
/// Technical Details: Currently, this involves decoding and inefficiently counting
/// the number of samples in the song. Hopefully, we'll be able to do this more
/// efficiently in the future, pending mostly on
/// https://github.com/RustAudio/rodio/issues/405
pub fn compute_duration(&self) -> impl FnOnce() -> Duration {
let sample_rate = self.song.sample_rate() as u64;
let n_channels = self.song.channels() as u64;
let song_clone = self.song.clone();
move|| {
let n_samples = song_clone.count() as u64; // expensive!
let n_whole_seconds = n_samples / n_channels / sample_rate;
let remaining_samples = n_samples % sample_rate;
let n_nanos = remaining_samples * 1_000_000_000 / sample_rate;
Duration::new(n_whole_seconds, n_nanos as u32)
}
}
/// Set the duration of the song
///
/// This struct does not automatically compute its own duration, so until this method
/// is called, we just assume the duration is 2:30s for any calculations that require
/// it.
///
/// In order to use an exact duration, use another thread to run
/// [`compute_duration()`] and pass the result into this method.
pub fn set_duration(&mut self, duration: Duration) {
self.duration = duration;
}
}
#[derive(Debug)]
pub enum PlayerError {
Decoder(rodio::decoder::DecoderError),
Play(rodio::PlayError),
Stream(rodio::StreamError),
}
impl fmt::Display for PlayerError {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
match self {
Self::Decoder(e) => write!(f, "Could not decode the provided song: {}", e),
Self::Play(e) => write!(f, "Problem playing to the audio output: {}", e),
Self::Stream(e) => write!(f, "Problem connecting to the audio output: {}", e),
}
}
}
impl std::error::Error for PlayerError { }