Files
songbird/src/tracks/handle.rs
Kyle Simpson 8cc7a22b0b Driver/Input: Migrate audio backend to Symphonia (#89)
This extensive PR rewrites the internal mixing logic of the driver to use symphonia for parsing and decoding audio data, and rubato to resample audio. Existing logic to decode DCA and Opus formats/data have been reworked as plugins for symphonia. The main benefit is that we no longer need to keep yt-dlp and ffmpeg processes alive, saving a lot of memory and CPU: all decoding can be done in Rust! In exchange, we now need to do a lot of the HTTP handling and resumption ourselves, but this is still a huge net positive.

`Input`s have been completely reworked such that all default (non-cached) sources are lazy by default, and are no longer covered by a special-case `Restartable`. These now span a gamut from a `Compose` (lazy), to a live source, to a fully `Parsed` source. As mixing is still sync, this includes adapters for `AsyncRead`/`AsyncSeek`, and HTTP streams.

`Track`s have been reworked so that they only contain initialisation state for each track. `TrackHandles` are only created once a `Track`/`Input` has been handed over to the driver, replacing `create_player` and related functions. `TrackHandle::action` now acts on a `View` of (im)mutable state, and can request seeks/readying via `Action`.

Per-track event handling has also been improved -- we can now determine and propagate the reason behind individual track errors due to the new backend. Some `TrackHandle` commands (seek etc.) benefit from this, and now use internal callbacks to signal completion.

Due to associated PRs on felixmcfelix/songbird from avid testers, this includes general clippy tweaks, API additions, and other repo-wide cleanup. Thanks go out to the below co-authors.

Co-authored-by: Gnome! <45660393+GnomedDev@users.noreply.github.com>
Co-authored-by: Alakh <36898190+alakhpc@users.noreply.github.com>
2023-11-19 23:58:34 +00:00

311 lines
9.8 KiB
Rust

use super::*;
use crate::events::{Event, EventData, EventHandler};
use flume::{Receiver, Sender};
use std::{fmt, sync::Arc, time::Duration};
use tokio::sync::RwLock;
use typemap_rev::TypeMap;
use uuid::Uuid;
#[derive(Clone, Debug)]
/// Handle for safe control of a [`Track`] from other threads, outside
/// of the audio mixing and voice handling context.
///
/// These are cheap to clone, using `Arc<...>` internally.
///
/// Many method calls here are fallible; in most cases, this will be because
/// the underlying [`Track`] object has been discarded. Those which aren't refer
/// to shared data not used by the driver.
///
/// [`Track`]: Track
pub struct TrackHandle {
inner: Arc<InnerHandle>,
}
struct InnerHandle {
command_channel: Sender<TrackCommand>,
uuid: Uuid,
typemap: RwLock<TypeMap>,
}
impl fmt::Debug for InnerHandle {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("InnerHandle")
.field("command_channel", &self.command_channel)
.field("uuid", &self.uuid)
.field("typemap", &"<LOCK>")
.finish()
}
}
impl TrackHandle {
/// Creates a new handle, using the given command sink.
///
/// [`Input`]: crate::input::Input
#[must_use]
pub(crate) fn new(command_channel: Sender<TrackCommand>, uuid: Uuid) -> Self {
let inner = Arc::new(InnerHandle {
command_channel,
uuid,
typemap: RwLock::new(TypeMap::new()),
});
Self { inner }
}
/// Unpauses an audio track.
pub fn play(&self) -> TrackResult<()> {
self.send(TrackCommand::Play)
}
/// Pauses an audio track.
pub fn pause(&self) -> TrackResult<()> {
self.send(TrackCommand::Pause)
}
/// Stops an audio track.
///
/// This is *final*, and will cause the audio context to fire
/// a [`TrackEvent::End`] event.
///
/// [`TrackEvent::End`]: crate::events::TrackEvent::End
pub fn stop(&self) -> TrackResult<()> {
self.send(TrackCommand::Stop)
}
/// Sets the volume of an audio track.
pub fn set_volume(&self, volume: f32) -> TrackResult<()> {
self.send(TrackCommand::Volume(volume))
}
#[must_use]
/// Ready a track for playing if it is lazily initialised.
///
/// If a track is already playable, the callback will instantly succeed.
pub fn make_playable(&self) -> TrackCallback<()> {
let (tx, rx) = flume::bounded(1);
let fail = self.send(TrackCommand::MakePlayable(tx)).is_err();
TrackCallback { fail, rx }
}
/// Ready a track for playing if it is lazily initialised.
///
/// This folds [`Self::make_playable`] into a single `async` result, but must
/// be awaited for the command to be sent.
pub async fn make_playable_async(&self) -> TrackResult<()> {
self.make_playable().result_async().await
}
#[must_use]
/// Seeks along the track to the specified position.
///
/// If the underlying [`Input`] does not support seeking,
/// forward seeks will succeed. Backward seeks will recreate the
/// track using the lazy [`Compose`] if present. The returned callback
/// will indicate whether the seek succeeded.
///
/// [`Input`]: crate::input::Input
/// [`Compose`]: crate::input::Compose
pub fn seek(&self, position: Duration) -> TrackCallback<Duration> {
let (tx, rx) = flume::bounded(1);
let fail = self
.send(TrackCommand::Seek(SeekRequest {
time: position,
callback: tx,
}))
.is_err();
TrackCallback { fail, rx }
}
/// Seeks along the track to the specified position.
///
/// This folds [`Self::seek`] into a single `async` result, but must
/// be awaited for the command to be sent.
pub async fn seek_async(&self, position: Duration) -> TrackResult<Duration> {
self.seek(position).result_async().await
}
/// Attach an event handler to an audio track. These will receive [`EventContext::Track`].
///
/// Events which can only be fired by the global context return [`ControlError::InvalidTrackEvent`]
///
/// [`EventContext::Track`]: crate::events::EventContext::Track
pub fn add_event<F: EventHandler + 'static>(&self, event: Event, action: F) -> TrackResult<()> {
let cmd = TrackCommand::AddEvent(EventData::new(event, action));
if event.is_global_only() {
Err(ControlError::InvalidTrackEvent)
} else {
self.send(cmd)
}
}
/// Perform an arbitrary synchronous action on a raw [`Track`] object.
///
/// This will give access to a [`View`] of the current track state and [`Metadata`],
/// which can be used to take an [`Action`].
///
/// Users **must** ensure that no costly work or blocking occurs
/// within the supplied function or closure. *Taking excess time could prevent
/// timely sending of packets, causing audio glitches and delays*.
///
/// [`Metadata`]: crate::input::Metadata
pub fn action<F>(&self, action: F) -> TrackResult<()>
where
F: FnOnce(View) -> Option<Action> + Send + Sync + 'static,
{
self.send(TrackCommand::Do(Box::new(action)))
}
/// Request playback information and state from the audio context.
pub async fn get_info(&self) -> TrackResult<TrackState> {
let (tx, rx) = flume::bounded(1);
self.send(TrackCommand::Request(tx))?;
rx.recv_async().await.map_err(|_| ControlError::Finished)
}
/// Set an audio track to loop indefinitely.
///
/// This requires either a [`Compose`] to be present or for the
/// input stream to be seekable.
///
/// [`Input`]: crate::input::Input
/// [`Compose`]: crate::input::Compose
pub fn enable_loop(&self) -> TrackResult<()> {
self.send(TrackCommand::Loop(LoopState::Infinite))
}
/// Set an audio track to no longer loop.
///
/// This follows the same rules as [`enable_loop`].
///
/// [`enable_loop`]: Self::enable_loop
pub fn disable_loop(&self) -> TrackResult<()> {
self.send(TrackCommand::Loop(LoopState::Finite(0)))
}
/// Set an audio track to loop a set number of times.
///
/// This follows the same rules as [`enable_loop`].
///
/// [`enable_loop`]: Self::enable_loop
pub fn loop_for(&self, count: usize) -> TrackResult<()> {
self.send(TrackCommand::Loop(LoopState::Finite(count)))
}
/// Returns this handle's (and track's) unique identifier.
#[must_use]
pub fn uuid(&self) -> Uuid {
self.inner.uuid
}
/// Allows access to this track's attached [`TypeMap`].
///
/// [`TypeMap`]s allow additional, user-defined data shared by all handles
/// to be attached to any track.
///
/// Driver code will never attempt to lock access to this map,
/// preventing deadlock/stalling.
#[must_use]
pub fn typemap(&self) -> &RwLock<TypeMap> {
&self.inner.typemap
}
#[inline]
/// Send a raw command to the [`Track`] object.
///
/// [`Track`]: Track
pub(crate) fn send(&self, cmd: TrackCommand) -> TrackResult<()> {
// As the send channels are unbounded, we can be reasonably certain
// that send failure == cancellation.
self.inner
.command_channel
.send(cmd)
.map_err(|_e| ControlError::Finished)
}
}
/// Asynchronous reply for an operation applied to a [`TrackHandle`].
///
/// This object does not need to be `.await`ed for the driver to perform an action.
/// Async threads can then call, e.g., [`TrackHandle::make_playable`], and safely drop
/// this callback if the result isn't needed.
pub struct TrackCallback<T> {
fail: bool,
rx: Receiver<Result<T, PlayError>>,
}
impl<T> TrackCallback<T> {
/// Consumes this handle to await a reply from the driver, blocking the current thread.
pub fn result(self) -> TrackResult<T> {
if self.fail {
Err(ControlError::Finished)
} else {
self.rx.recv()?.map_err(ControlError::Play)
}
}
/// Consumes this handle to await a reply from the driver asynchronously.
pub async fn result_async(self) -> TrackResult<T> {
if self.fail {
Err(ControlError::Finished)
} else {
self.rx.recv_async().await?.map_err(ControlError::Play)
}
}
#[must_use]
/// Returns `true` if the operation instantly failed due to the target track being
/// removed.
pub fn is_hung_up(&self) -> bool {
self.fail
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::{
constants::test_data::FILE_WAV_TARGET,
driver::Driver,
input::File,
tracks::Track,
Config,
};
#[tokio::test]
#[ntest::timeout(10_000)]
async fn make_playable_callback_fires() {
let (t_handle, config) = Config::test_cfg(true);
let mut driver = Driver::new(config.clone());
let file = File::new(FILE_WAV_TARGET);
let handle = driver.play(Track::from(file).pause());
let callback = handle.make_playable();
t_handle.spawn_ticker().await;
assert!(callback.result_async().await.is_ok());
}
#[tokio::test]
#[ntest::timeout(10_000)]
async fn seek_callback_fires() {
let (t_handle, config) = Config::test_cfg(true);
let mut driver = Driver::new(config.clone());
let file = File::new(FILE_WAV_TARGET);
let handle = driver.play(Track::from(file).pause());
let target = Duration::from_millis(500);
let callback = handle.seek(target);
t_handle.spawn_ticker().await;
let answer = callback.result_async().await;
assert!(answer.is_ok());
let answer = answer.unwrap();
let delta = Duration::from_millis(100);
assert!(answer > target - delta && answer < target + delta);
}
}