Voice Rework -- Events, Track Queues (#806)
This implements a proof-of-concept for an improved audio frontend. The largest change is the introduction of events and event handling: both by time elapsed and by track events, such as ending or looping. Following on from this, the library now includes a basic, event-driven track queue system (which people seem to ask for unusually often). A new sample, `examples/13_voice_events`, demonstrates both the `TrackQueue` system and some basic events via the `~queue` and `~play_fade` commands. Locks are removed from around the control of `Audio` objects, which should allow the backend to be moved to a more granular futures-based backend solution in a cleaner way.
This commit is contained in:
53
src/tracks/command.rs
Normal file
53
src/tracks/command.rs
Normal file
@@ -0,0 +1,53 @@
|
||||
use super::*;
|
||||
use crate::events::EventData;
|
||||
use std::time::Duration;
|
||||
use tokio::sync::oneshot::Sender as OneshotSender;
|
||||
|
||||
/// A request from external code using a [`TrackHandle`] to modify
|
||||
/// or act upon an [`Track`] object.
|
||||
///
|
||||
/// [`Track`]: struct.Track.html
|
||||
/// [`TrackHandle`]: struct.TrackHandle.html
|
||||
pub enum TrackCommand {
|
||||
/// Set the track's play_mode to play/resume.
|
||||
Play,
|
||||
/// Set the track's play_mode to pause.
|
||||
Pause,
|
||||
/// Stop the target track. This cannot be undone.
|
||||
Stop,
|
||||
/// Set the track's volume.
|
||||
Volume(f32),
|
||||
/// Seek to the given duration.
|
||||
///
|
||||
/// On unsupported input types, this can be fatal.
|
||||
Seek(Duration),
|
||||
/// Register an event on this track.
|
||||
AddEvent(EventData),
|
||||
/// Run some closure on this track, with direct access to the core object.
|
||||
Do(Box<dyn FnOnce(&mut Track) + Send + Sync + 'static>),
|
||||
/// Request a read-only view of this track's state.
|
||||
Request(OneshotSender<Box<TrackState>>),
|
||||
/// Change the loop count/strategy of this track.
|
||||
Loop(LoopState),
|
||||
}
|
||||
|
||||
impl std::fmt::Debug for TrackCommand {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> Result<(), std::fmt::Error> {
|
||||
use TrackCommand::*;
|
||||
write!(
|
||||
f,
|
||||
"TrackCommand::{}",
|
||||
match self {
|
||||
Play => "Play".to_string(),
|
||||
Pause => "Pause".to_string(),
|
||||
Stop => "Stop".to_string(),
|
||||
Volume(vol) => format!("Volume({})", vol),
|
||||
Seek(d) => format!("Seek({:?})", d),
|
||||
AddEvent(evt) => format!("AddEvent({:?})", evt),
|
||||
Do(_f) => "Do([function])".to_string(),
|
||||
Request(tx) => format!("Request({:?})", tx),
|
||||
Loop(loops) => format!("Loop({:?})", loops),
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
159
src/tracks/handle.rs
Normal file
159
src/tracks/handle.rs
Normal file
@@ -0,0 +1,159 @@
|
||||
use super::*;
|
||||
use crate::events::{Event, EventData, EventHandler};
|
||||
use std::time::Duration;
|
||||
use tokio::sync::{
|
||||
mpsc::{error::SendError, UnboundedSender},
|
||||
oneshot,
|
||||
};
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
/// Handle for safe control of a [`Track`] track from other threads, outside
|
||||
/// of the audio mixing and voice handling context.
|
||||
///
|
||||
/// Almost all 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 immutable properties of the underlying stream.
|
||||
///
|
||||
/// [`Track`]: struct.Track.html
|
||||
pub struct TrackHandle {
|
||||
command_channel: UnboundedSender<TrackCommand>,
|
||||
seekable: bool,
|
||||
}
|
||||
|
||||
impl TrackHandle {
|
||||
/// Creates a new handle, using the given command sink and hint as to whether
|
||||
/// the underlying [`Input`] supports seek operations.
|
||||
///
|
||||
/// [`Input`]: ../input/struct.Input.html
|
||||
pub fn new(command_channel: UnboundedSender<TrackCommand>, seekable: bool) -> Self {
|
||||
Self {
|
||||
command_channel,
|
||||
seekable,
|
||||
}
|
||||
}
|
||||
|
||||
/// 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`]: ../events/enum.TrackEvent.html#variant.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))
|
||||
}
|
||||
|
||||
/// Denotes whether the underlying [`Input`] stream is compatible with arbitrary seeking.
|
||||
///
|
||||
/// If this returns `false`, all calls to [`seek`] will fail, and the track is
|
||||
/// incapable of looping.
|
||||
///
|
||||
/// [`seek`]: #method.seek
|
||||
/// [`Input`]: ../input/struct.Input.html
|
||||
pub fn is_seekable(&self) -> bool {
|
||||
self.seekable
|
||||
}
|
||||
|
||||
/// Seeks along the track to the specified position.
|
||||
///
|
||||
/// If the underlying [`Input`] does not support this behaviour,
|
||||
/// then all calls will fail.
|
||||
///
|
||||
/// [`Input`]: ../input/struct.Input.html
|
||||
pub fn seek_time(&self, position: Duration) -> TrackResult {
|
||||
if self.seekable {
|
||||
self.send(TrackCommand::Seek(position))
|
||||
} else {
|
||||
Err(SendError(TrackCommand::Seek(position)))
|
||||
}
|
||||
}
|
||||
|
||||
/// Attach an event handler to an audio track. These will receive [`EventContext::Track`].
|
||||
///
|
||||
/// 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*.
|
||||
///
|
||||
/// [`Track`]: struct.Track.html
|
||||
/// [`EventContext::Track`]: ../events/enum.EventContext.html#variant.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(SendError(cmd))
|
||||
} else {
|
||||
self.send(cmd)
|
||||
}
|
||||
}
|
||||
|
||||
/// Perform an arbitrary action on a raw [`Track`] object.
|
||||
///
|
||||
/// 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*.
|
||||
///
|
||||
/// [`Track`]: struct.Track.html
|
||||
pub fn action<F>(&self, action: F) -> TrackResult
|
||||
where
|
||||
F: FnOnce(&mut Track) + Send + Sync + 'static,
|
||||
{
|
||||
self.send(TrackCommand::Do(Box::new(action)))
|
||||
}
|
||||
|
||||
/// Request playback information and state from the audio context.
|
||||
///
|
||||
/// Crucially, the audio thread will respond *at a later time*:
|
||||
/// It is up to the user when or how this should be read from the returned channel.
|
||||
pub fn get_info(&self) -> TrackQueryResult {
|
||||
let (tx, rx) = oneshot::channel();
|
||||
self.send(TrackCommand::Request(tx)).map(move |_| rx)
|
||||
}
|
||||
|
||||
/// Set an audio track to loop indefinitely.
|
||||
pub fn enable_loop(&self) -> TrackResult {
|
||||
if self.seekable {
|
||||
self.send(TrackCommand::Loop(LoopState::Infinite))
|
||||
} else {
|
||||
Err(SendError(TrackCommand::Loop(LoopState::Infinite)))
|
||||
}
|
||||
}
|
||||
|
||||
/// Set an audio track to no longer loop.
|
||||
pub fn disable_loop(&self) -> TrackResult {
|
||||
if self.seekable {
|
||||
self.send(TrackCommand::Loop(LoopState::Finite(0)))
|
||||
} else {
|
||||
Err(SendError(TrackCommand::Loop(LoopState::Finite(0))))
|
||||
}
|
||||
}
|
||||
|
||||
/// Set an audio track to loop a set number of times.
|
||||
pub fn loop_for(&self, count: usize) -> TrackResult {
|
||||
if self.seekable {
|
||||
self.send(TrackCommand::Loop(LoopState::Finite(count)))
|
||||
} else {
|
||||
Err(SendError(TrackCommand::Loop(LoopState::Finite(count))))
|
||||
}
|
||||
}
|
||||
|
||||
#[inline]
|
||||
/// Send a raw command to the [`Track`] object.
|
||||
///
|
||||
/// [`Track`]: struct.Track.html
|
||||
pub fn send(&self, cmd: TrackCommand) -> TrackResult {
|
||||
self.command_channel.send(cmd)
|
||||
}
|
||||
}
|
||||
22
src/tracks/looping.rs
Normal file
22
src/tracks/looping.rs
Normal file
@@ -0,0 +1,22 @@
|
||||
#[derive(Copy, Clone, Debug, Eq, PartialEq)]
|
||||
/// Looping behaviour for a [`Track`].
|
||||
///
|
||||
/// [`Track`]: struct.Track.html
|
||||
pub enum LoopState {
|
||||
/// Track will loop endlessly until loop state is changed or
|
||||
/// manually stopped.
|
||||
Infinite,
|
||||
|
||||
/// Track will loop `n` more times.
|
||||
///
|
||||
/// `Finite(0)` is the `Default`, stopping the track once its [`Input`] ends.
|
||||
///
|
||||
/// [`Input`]: ../input/struct.Input.html
|
||||
Finite(usize),
|
||||
}
|
||||
|
||||
impl Default for LoopState {
|
||||
fn default() -> Self {
|
||||
Self::Finite(0)
|
||||
}
|
||||
}
|
||||
379
src/tracks/mod.rs
Normal file
379
src/tracks/mod.rs
Normal file
@@ -0,0 +1,379 @@
|
||||
//! Live, controllable audio instances.
|
||||
//!
|
||||
//! Tracks add control and event data around the bytestreams offered by [`Input`],
|
||||
//! where each represents a live audio source inside of the driver's mixer.
|
||||
//!
|
||||
//! To prevent locking and stalling of the driver, tracks are controlled from your bot using a
|
||||
//! [`TrackHandle`]. These handles remotely send commands from your bot's (a)sync
|
||||
//! context to control playback, register events, and execute synchronous closures.
|
||||
//!
|
||||
//! If you want a new track from an [`Input`], i.e., for direct control before
|
||||
//! playing your source on the driver, use [`create_player`].
|
||||
//!
|
||||
//! [`Input`]: ../input/struct.Input.html
|
||||
//! [`TrackHandle`]: struct.TrackHandle.html
|
||||
//! [`create_player`]: fn.create_player.html
|
||||
|
||||
mod command;
|
||||
mod handle;
|
||||
mod looping;
|
||||
mod mode;
|
||||
mod queue;
|
||||
mod state;
|
||||
|
||||
pub use self::{command::*, handle::*, looping::*, mode::*, queue::*, state::*};
|
||||
|
||||
use crate::{constants::*, driver::tasks::message::*, events::EventStore, input::Input};
|
||||
use std::time::Duration;
|
||||
use tokio::sync::{
|
||||
mpsc::{
|
||||
self,
|
||||
error::{SendError, TryRecvError},
|
||||
UnboundedReceiver,
|
||||
},
|
||||
oneshot::Receiver as OneshotReceiver,
|
||||
};
|
||||
|
||||
/// Control object for audio playback.
|
||||
///
|
||||
/// Accessed by both commands and the playback code -- as such, access from user code is
|
||||
/// almost always guarded via a [`TrackHandle`]. You should expect to receive
|
||||
/// access to a raw object of this type via [`create_player`], for use in
|
||||
/// [`Driver::play`] or [`Driver::play_only`].
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```rust,no_run
|
||||
/// use songbird::{driver::Driver, ffmpeg, tracks::create_player};
|
||||
///
|
||||
/// # async {
|
||||
/// // A Call is also valid here!
|
||||
/// let mut handler: Driver = Default::default();
|
||||
/// let source = ffmpeg("../audio/my-favourite-song.mp3")
|
||||
/// .await
|
||||
/// .expect("This might fail: handle this error!");
|
||||
/// let (mut audio, audio_handle) = create_player(source);
|
||||
///
|
||||
/// audio.set_volume(0.5);
|
||||
///
|
||||
/// handler.play_only(audio);
|
||||
///
|
||||
/// // Future access occurs via audio_handle.
|
||||
/// # };
|
||||
/// ```
|
||||
///
|
||||
/// [`Driver::play_only`]: ../struct.Driver.html#method.play_only
|
||||
/// [`Driver::play`]: ../struct.Driver.html#method.play
|
||||
/// [`TrackHandle`]: struct.TrackHandle.html
|
||||
/// [`create_player`]: fn.create_player.html
|
||||
#[derive(Debug)]
|
||||
pub struct Track {
|
||||
/// Whether or not this sound is currently playing.
|
||||
///
|
||||
/// Can be controlled with [`play`] or [`pause`] if chaining is desired.
|
||||
///
|
||||
/// [`play`]: #method.play
|
||||
/// [`pause`]: #method.pause
|
||||
pub(crate) playing: PlayMode,
|
||||
|
||||
/// The desired volume for playback.
|
||||
///
|
||||
/// Sensible values fall between `0.0` and `1.0`.
|
||||
///
|
||||
/// Can be controlled with [`volume`] if chaining is desired.
|
||||
///
|
||||
/// [`volume`]: #method.volume
|
||||
pub(crate) volume: f32,
|
||||
|
||||
/// Underlying data access object.
|
||||
///
|
||||
/// *Calling code is not expected to use this.*
|
||||
pub(crate) source: Input,
|
||||
|
||||
/// The current playback position in the track.
|
||||
pub(crate) position: Duration,
|
||||
|
||||
/// The total length of time this track has been active.
|
||||
pub(crate) play_time: Duration,
|
||||
|
||||
/// List of events attached to this audio track.
|
||||
///
|
||||
/// This may be used to add additional events to a track
|
||||
/// before it is sent to the audio context for playing.
|
||||
pub events: Option<EventStore>,
|
||||
|
||||
/// Channel from which commands are received.
|
||||
///
|
||||
/// Track commands are sent in this manner to ensure that access
|
||||
/// occurs in a thread-safe manner, without allowing any external
|
||||
/// code to lock access to audio objects and block packet generation.
|
||||
pub(crate) commands: UnboundedReceiver<TrackCommand>,
|
||||
|
||||
/// Handle for safe control of this audio track from other threads.
|
||||
///
|
||||
/// Typically, this is used by internal code to supply context information
|
||||
/// to event handlers, though more may be cloned from this handle.
|
||||
pub handle: TrackHandle,
|
||||
|
||||
/// Count of remaining loops.
|
||||
pub loops: LoopState,
|
||||
}
|
||||
|
||||
impl Track {
|
||||
/// Create a new track directly from an input, command source,
|
||||
/// and handle.
|
||||
///
|
||||
/// In general, you should probably use [`create_player`].
|
||||
///
|
||||
/// [`create_player`]: fn.create_player.html
|
||||
pub fn new_raw(
|
||||
source: Input,
|
||||
commands: UnboundedReceiver<TrackCommand>,
|
||||
handle: TrackHandle,
|
||||
) -> Self {
|
||||
Self {
|
||||
playing: Default::default(),
|
||||
volume: 1.0,
|
||||
source,
|
||||
position: Default::default(),
|
||||
play_time: Default::default(),
|
||||
events: Some(EventStore::new_local()),
|
||||
commands,
|
||||
handle,
|
||||
loops: LoopState::Finite(0),
|
||||
}
|
||||
}
|
||||
|
||||
/// Sets a track to playing if it is paused.
|
||||
pub fn play(&mut self) -> &mut Self {
|
||||
self.set_playing(PlayMode::Play)
|
||||
}
|
||||
|
||||
/// Pauses a track if it is playing.
|
||||
pub fn pause(&mut self) -> &mut Self {
|
||||
self.set_playing(PlayMode::Pause)
|
||||
}
|
||||
|
||||
/// Manually stops a track.
|
||||
///
|
||||
/// This will cause the audio track to be removed, with any relevant events triggered.
|
||||
/// Stopped/ended tracks cannot be restarted.
|
||||
pub fn stop(&mut self) -> &mut Self {
|
||||
self.set_playing(PlayMode::Stop)
|
||||
}
|
||||
|
||||
pub(crate) fn end(&mut self) -> &mut Self {
|
||||
self.set_playing(PlayMode::End)
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn set_playing(&mut self, new_state: PlayMode) -> &mut Self {
|
||||
self.playing = self.playing.change_to(new_state);
|
||||
|
||||
self
|
||||
}
|
||||
|
||||
/// Returns the current play status of this track.
|
||||
pub fn playing(&self) -> PlayMode {
|
||||
self.playing
|
||||
}
|
||||
|
||||
/// Sets [`volume`] in a manner that allows method chaining.
|
||||
///
|
||||
/// [`volume`]: #structfield.volume
|
||||
pub fn set_volume(&mut self, volume: f32) -> &mut Self {
|
||||
self.volume = volume;
|
||||
|
||||
self
|
||||
}
|
||||
|
||||
/// Returns the current playback position.
|
||||
pub fn volume(&self) -> f32 {
|
||||
self.volume
|
||||
}
|
||||
|
||||
/// Returns the current playback position.
|
||||
pub fn position(&self) -> Duration {
|
||||
self.position
|
||||
}
|
||||
|
||||
/// Returns the total length of time this track has been active.
|
||||
pub fn play_time(&self) -> Duration {
|
||||
self.play_time
|
||||
}
|
||||
|
||||
/// Sets [`loops`] in a manner that allows method chaining.
|
||||
///
|
||||
/// [`loops`]: #structfield.loops
|
||||
pub fn set_loops(&mut self, loops: LoopState) -> &mut Self {
|
||||
self.loops = loops;
|
||||
self
|
||||
}
|
||||
|
||||
pub(crate) fn do_loop(&mut self) -> bool {
|
||||
match self.loops {
|
||||
LoopState::Infinite => true,
|
||||
LoopState::Finite(0) => false,
|
||||
LoopState::Finite(ref mut n) => {
|
||||
*n -= 1;
|
||||
true
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
/// Steps playback location forward by one frame.
|
||||
pub(crate) fn step_frame(&mut self) {
|
||||
self.position += TIMESTEP_LENGTH;
|
||||
self.play_time += TIMESTEP_LENGTH;
|
||||
}
|
||||
|
||||
/// Receives and acts upon any commands forwarded by [`TrackHandle`]s.
|
||||
///
|
||||
/// *Used internally*, this should not be exposed to users.
|
||||
///
|
||||
/// [`TrackHandle`]: struct.TrackHandle.html
|
||||
pub(crate) fn process_commands(&mut self, index: usize, ic: &Interconnect) {
|
||||
// Note: disconnection and an empty channel are both valid,
|
||||
// and should allow the audio object to keep running as intended.
|
||||
|
||||
// Note that interconnect failures are not currently errors.
|
||||
// In correct operation, the event thread should never panic,
|
||||
// but it receiving status updates is secondary do actually
|
||||
// doing the work.
|
||||
loop {
|
||||
match self.commands.try_recv() {
|
||||
Ok(cmd) => {
|
||||
use TrackCommand::*;
|
||||
match cmd {
|
||||
Play => {
|
||||
self.play();
|
||||
let _ = ic.events.send(EventMessage::ChangeState(
|
||||
index,
|
||||
TrackStateChange::Mode(self.playing),
|
||||
));
|
||||
},
|
||||
Pause => {
|
||||
self.pause();
|
||||
let _ = ic.events.send(EventMessage::ChangeState(
|
||||
index,
|
||||
TrackStateChange::Mode(self.playing),
|
||||
));
|
||||
},
|
||||
Stop => {
|
||||
self.stop();
|
||||
let _ = ic.events.send(EventMessage::ChangeState(
|
||||
index,
|
||||
TrackStateChange::Mode(self.playing),
|
||||
));
|
||||
},
|
||||
Volume(vol) => {
|
||||
self.set_volume(vol);
|
||||
let _ = ic.events.send(EventMessage::ChangeState(
|
||||
index,
|
||||
TrackStateChange::Volume(self.volume),
|
||||
));
|
||||
},
|
||||
Seek(time) => {
|
||||
self.seek_time(time);
|
||||
let _ = ic.events.send(EventMessage::ChangeState(
|
||||
index,
|
||||
TrackStateChange::Position(self.position),
|
||||
));
|
||||
},
|
||||
AddEvent(evt) => {
|
||||
let _ = ic.events.send(EventMessage::AddTrackEvent(index, evt));
|
||||
},
|
||||
Do(action) => {
|
||||
action(self);
|
||||
let _ = ic.events.send(EventMessage::ChangeState(
|
||||
index,
|
||||
TrackStateChange::Total(self.state()),
|
||||
));
|
||||
},
|
||||
Request(tx) => {
|
||||
let _ = tx.send(Box::new(self.state()));
|
||||
},
|
||||
Loop(loops) => {
|
||||
self.set_loops(loops);
|
||||
let _ = ic.events.send(EventMessage::ChangeState(
|
||||
index,
|
||||
TrackStateChange::Loops(self.loops, true),
|
||||
));
|
||||
},
|
||||
}
|
||||
},
|
||||
Err(TryRecvError::Closed) => {
|
||||
// this branch will never be visited.
|
||||
break;
|
||||
},
|
||||
Err(TryRecvError::Empty) => {
|
||||
break;
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Creates a read-only copy of the audio track's state.
|
||||
///
|
||||
/// The primary use-case of this is sending information across
|
||||
/// threads in response to a [`TrackHandle`].
|
||||
///
|
||||
/// [`TrackHandle`]: struct.TrackHandle.html
|
||||
pub fn state(&self) -> TrackState {
|
||||
TrackState {
|
||||
playing: self.playing,
|
||||
volume: self.volume,
|
||||
position: self.position,
|
||||
play_time: self.play_time,
|
||||
loops: self.loops,
|
||||
}
|
||||
}
|
||||
|
||||
/// Seek to a specific point in the track.
|
||||
///
|
||||
/// Returns `None` if unsupported.
|
||||
pub fn seek_time(&mut self, pos: Duration) -> Option<Duration> {
|
||||
let out = self.source.seek_time(pos);
|
||||
|
||||
if let Some(t) = out {
|
||||
self.position = t;
|
||||
}
|
||||
|
||||
out
|
||||
}
|
||||
}
|
||||
|
||||
/// Creates a [`Track`] object to pass into the audio context, and a [`TrackHandle`]
|
||||
/// for safe, lock-free access in external code.
|
||||
///
|
||||
/// Typically, this would be used if you wished to directly work on or configure
|
||||
/// the [`Track`] object before it is passed over to the driver.
|
||||
///
|
||||
/// [`Track`]: struct.Track.html
|
||||
/// [`TrackHandle`]: struct.TrackHandle.html
|
||||
pub fn create_player(source: Input) -> (Track, TrackHandle) {
|
||||
let (tx, rx) = mpsc::unbounded_channel();
|
||||
let can_seek = source.is_seekable();
|
||||
let player = Track::new_raw(source, rx, TrackHandle::new(tx.clone(), can_seek));
|
||||
|
||||
(player, TrackHandle::new(tx, can_seek))
|
||||
}
|
||||
|
||||
/// Alias for most result-free calls to a [`TrackHandle`].
|
||||
///
|
||||
/// Failure indicates that the accessed audio object has been
|
||||
/// removed or deleted by the audio context.
|
||||
///
|
||||
/// [`TrackHandle`]: struct.TrackHandle.html
|
||||
pub type TrackResult = Result<(), SendError<TrackCommand>>;
|
||||
|
||||
/// Alias for return value from calls to [`TrackHandle::get_info`].
|
||||
///
|
||||
/// Crucially, the audio thread will respond *at a later time*:
|
||||
/// It is up to the user when or how this should be read from the returned channel.
|
||||
///
|
||||
/// Failure indicates that the accessed audio object has been
|
||||
/// removed or deleted by the audio context.
|
||||
///
|
||||
/// [`TrackHandle::get_info`]: struct.TrackHandle.html#method.get_info
|
||||
pub type TrackQueryResult = Result<OneshotReceiver<Box<TrackState>>, SendError<TrackCommand>>;
|
||||
37
src/tracks/mode.rs
Normal file
37
src/tracks/mode.rs
Normal file
@@ -0,0 +1,37 @@
|
||||
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
||||
/// Playback status of a track.
|
||||
pub enum PlayMode {
|
||||
/// The track is currently playing.
|
||||
Play,
|
||||
/// The track is currently paused, and may be resumed.
|
||||
Pause,
|
||||
/// The track has been manually stopped, and cannot be restarted.
|
||||
Stop,
|
||||
/// The track has naturally ended, and cannot be restarted.
|
||||
End,
|
||||
}
|
||||
|
||||
impl PlayMode {
|
||||
/// Returns whether the track has irreversibly stopped.
|
||||
pub fn is_done(self) -> bool {
|
||||
matches!(self, PlayMode::Stop | PlayMode::End)
|
||||
}
|
||||
|
||||
pub(crate) fn change_to(self, other: Self) -> PlayMode {
|
||||
use PlayMode::*;
|
||||
|
||||
// Idea: a finished track cannot be restarted -- this action is final.
|
||||
// We may want to change this in future so that seekable tracks can uncancel
|
||||
// themselves, perhaps, but this requires a bit more machinery to readd...
|
||||
match self {
|
||||
Play | Pause => other,
|
||||
state => state,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for PlayMode {
|
||||
fn default() -> Self {
|
||||
PlayMode::Play
|
||||
}
|
||||
}
|
||||
213
src/tracks/queue.rs
Normal file
213
src/tracks/queue.rs
Normal file
@@ -0,0 +1,213 @@
|
||||
use crate::{
|
||||
driver::Driver,
|
||||
events::{Event, EventContext, EventData, EventHandler, TrackEvent},
|
||||
input::Input,
|
||||
tracks::{self, Track, TrackHandle, TrackResult},
|
||||
};
|
||||
use async_trait::async_trait;
|
||||
use parking_lot::Mutex;
|
||||
use std::{collections::VecDeque, sync::Arc};
|
||||
use tracing::{info, warn};
|
||||
|
||||
#[derive(Default)]
|
||||
/// A simple queue for several audio sources, designed to
|
||||
/// play in sequence.
|
||||
///
|
||||
/// This makes use of [`TrackEvent`]s to determine when the current
|
||||
/// song or audio file has finished before playing the next entry.
|
||||
///
|
||||
/// `examples/e16_voice_events` demonstrates how a user might manage,
|
||||
/// track and use this to run a song queue in many guilds in parallel.
|
||||
/// This code is trivial to extend if extra functionality is needed.
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```rust,no_run
|
||||
/// use songbird::{
|
||||
/// driver::Driver,
|
||||
/// id::GuildId,
|
||||
/// ffmpeg,
|
||||
/// tracks::{create_player, TrackQueue},
|
||||
/// };
|
||||
/// use std::collections::HashMap;
|
||||
///
|
||||
/// # async {
|
||||
/// let guild = GuildId(0);
|
||||
/// // A Call is also valid here!
|
||||
/// let mut driver: Driver = Default::default();
|
||||
///
|
||||
/// let mut queues: HashMap<GuildId, TrackQueue> = Default::default();
|
||||
///
|
||||
/// let source = ffmpeg("../audio/my-favourite-song.mp3")
|
||||
/// .await
|
||||
/// .expect("This might fail: handle this error!");
|
||||
///
|
||||
/// // We need to ensure that this guild has a TrackQueue created for it.
|
||||
/// let queue = queues.entry(guild)
|
||||
/// .or_default();
|
||||
///
|
||||
/// // Queueing a track is this easy!
|
||||
/// queue.add_source(source, &mut driver);
|
||||
/// # };
|
||||
/// ```
|
||||
|
||||
///
|
||||
/// [`TrackEvent`]: ../events/enum.TrackEvent.html
|
||||
pub struct TrackQueue {
|
||||
// NOTE: the choice of a parking lot mutex is quite deliberate
|
||||
inner: Arc<Mutex<TrackQueueCore>>,
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
/// Inner portion of a [`TrackQueue`].
|
||||
///
|
||||
/// This abstracts away thread-safety from the user,
|
||||
/// and offers a convenient location to store further state if required.
|
||||
///
|
||||
/// [`TrackQueue`]: struct.TrackQueue.html
|
||||
struct TrackQueueCore {
|
||||
tracks: VecDeque<TrackHandle>,
|
||||
}
|
||||
|
||||
struct QueueHandler {
|
||||
remote_lock: Arc<Mutex<TrackQueueCore>>,
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl EventHandler for QueueHandler {
|
||||
async fn act(&self, ctx: &EventContext<'_>) -> Option<Event> {
|
||||
let mut inner = self.remote_lock.lock();
|
||||
let _old = inner.tracks.pop_front();
|
||||
|
||||
info!("Queued track ended: {:?}.", ctx);
|
||||
info!("{} tracks remain.", inner.tracks.len());
|
||||
|
||||
// If any audio files die unexpectedly, then keep going until we
|
||||
// find one which works, or we run out.
|
||||
let mut keep_looking = true;
|
||||
while keep_looking && !inner.tracks.is_empty() {
|
||||
if let Some(new) = inner.tracks.front() {
|
||||
keep_looking = new.play().is_err();
|
||||
|
||||
// Discard files which cannot be used for whatever reason.
|
||||
if keep_looking {
|
||||
warn!("Track in Queue couldn't be played...");
|
||||
let _ = inner.tracks.pop_front();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
impl TrackQueue {
|
||||
/// Create a new, empty, track queue.
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
inner: Arc::new(Mutex::new(TrackQueueCore {
|
||||
tracks: VecDeque::new(),
|
||||
})),
|
||||
}
|
||||
}
|
||||
|
||||
/// Adds an audio source to the queue, to be played in the channel managed by `handler`.
|
||||
pub fn add_source(&self, source: Input, handler: &mut Driver) {
|
||||
let (audio, audio_handle) = tracks::create_player(source);
|
||||
self.add(audio, audio_handle, handler);
|
||||
}
|
||||
|
||||
/// Adds a [`Track`] object to the queue, to be played in the channel managed by `handler`.
|
||||
///
|
||||
/// This is used with [`voice::create_player`] if additional configuration or event handlers
|
||||
/// are required before enqueueing the audio track.
|
||||
///
|
||||
/// [`Track`]: struct.Track.html
|
||||
/// [`voice::create_player`]: fn.create_player.html
|
||||
pub fn add(&self, mut track: Track, track_handle: TrackHandle, handler: &mut Driver) {
|
||||
info!("Track added to queue.");
|
||||
let remote_lock = self.inner.clone();
|
||||
let mut inner = self.inner.lock();
|
||||
|
||||
if !inner.tracks.is_empty() {
|
||||
track.pause();
|
||||
}
|
||||
|
||||
track
|
||||
.events
|
||||
.as_mut()
|
||||
.expect("Queue inspecting EventStore on new Track: did not exist.")
|
||||
.add_event(
|
||||
EventData::new(Event::Track(TrackEvent::End), QueueHandler { remote_lock }),
|
||||
track.position,
|
||||
);
|
||||
|
||||
handler.play(track);
|
||||
inner.tracks.push_back(track_handle);
|
||||
}
|
||||
|
||||
/// Returns the number of tracks currently in the queue.
|
||||
pub fn len(&self) -> usize {
|
||||
let inner = self.inner.lock();
|
||||
|
||||
inner.tracks.len()
|
||||
}
|
||||
|
||||
/// Returns whether there are no tracks currently in the queue.
|
||||
pub fn is_empty(&self) -> bool {
|
||||
let inner = self.inner.lock();
|
||||
|
||||
inner.tracks.is_empty()
|
||||
}
|
||||
|
||||
/// Pause the track at the head of the queue.
|
||||
pub fn pause(&self) -> TrackResult {
|
||||
let inner = self.inner.lock();
|
||||
|
||||
if let Some(handle) = inner.tracks.front() {
|
||||
handle.pause()
|
||||
} else {
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
/// Resume the track at the head of the queue.
|
||||
pub fn resume(&self) -> TrackResult {
|
||||
let inner = self.inner.lock();
|
||||
|
||||
if let Some(handle) = inner.tracks.front() {
|
||||
handle.play()
|
||||
} else {
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
/// Stop the currently playing track, and clears the queue.
|
||||
pub fn stop(&self) -> TrackResult {
|
||||
let mut inner = self.inner.lock();
|
||||
|
||||
let out = inner.stop_current();
|
||||
|
||||
inner.tracks.clear();
|
||||
|
||||
out
|
||||
}
|
||||
|
||||
/// Skip to the next track in the queue, if it exists.
|
||||
pub fn skip(&self) -> TrackResult {
|
||||
let inner = self.inner.lock();
|
||||
|
||||
inner.stop_current()
|
||||
}
|
||||
}
|
||||
|
||||
impl TrackQueueCore {
|
||||
/// Skip to the next track in the queue, if it exists.
|
||||
fn stop_current(&self) -> TrackResult {
|
||||
if let Some(handle) = self.tracks.front() {
|
||||
handle.stop()
|
||||
} else {
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
}
|
||||
31
src/tracks/state.rs
Normal file
31
src/tracks/state.rs
Normal file
@@ -0,0 +1,31 @@
|
||||
use super::*;
|
||||
|
||||
/// State of an [`Track`] object, designed to be passed to event handlers
|
||||
/// and retrieved remotely via [`TrackHandle::get_info`] or
|
||||
/// [`TrackHandle::get_info_blocking`].
|
||||
///
|
||||
/// [`Track`]: struct.Track.html
|
||||
/// [`TrackHandle::get_info`]: struct.TrackHandle.html#method.get_info
|
||||
/// [`TrackHandle::get_info_blocking`]: struct.TrackHandle.html#method.get_info_blocking
|
||||
#[derive(Copy, Clone, Debug, Default, PartialEq)]
|
||||
pub struct TrackState {
|
||||
/// Play status (e.g., active, paused, stopped) of this track.
|
||||
pub playing: PlayMode,
|
||||
/// Current volume of this track.
|
||||
pub volume: f32,
|
||||
/// Current playback position in the source.
|
||||
///
|
||||
/// This is altered by loops and seeks
|
||||
pub position: Duration,
|
||||
/// Total playback time, increasing monotonically.
|
||||
pub play_time: Duration,
|
||||
/// Remaining loops on this track.
|
||||
pub loops: LoopState,
|
||||
}
|
||||
|
||||
impl TrackState {
|
||||
pub(crate) fn step_frame(&mut self) {
|
||||
self.position += TIMESTEP_LENGTH;
|
||||
self.play_time += TIMESTEP_LENGTH;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user