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:
137
src/events/context.rs
Normal file
137
src/events/context.rs
Normal file
@@ -0,0 +1,137 @@
|
||||
use super::*;
|
||||
use crate::{
|
||||
model::payload::{ClientConnect, ClientDisconnect, Speaking},
|
||||
tracks::{TrackHandle, TrackState},
|
||||
};
|
||||
use discortp::{rtcp::Rtcp, rtp::Rtp};
|
||||
|
||||
/// Information about which tracks or data fired an event.
|
||||
///
|
||||
/// [`Track`] events may be local or global, and have no tracks
|
||||
/// if fired on the global context via [`Handler::add_global_event`].
|
||||
///
|
||||
/// [`Track`]: ../tracks/struct.Track.html
|
||||
/// [`Handler::add_global_event`]: ../struct.Handler.html#method.add_global_event
|
||||
#[derive(Clone, Debug)]
|
||||
pub enum EventContext<'a> {
|
||||
/// Track event context, passed to events created via [`TrackHandle::add_event`],
|
||||
/// [`EventStore::add_event`], or relevant global events.
|
||||
///
|
||||
/// [`EventStore::add_event`]: struct.EventStore.html#method.add_event
|
||||
/// [`TrackHandle::add_event`]: ../tracks/struct.TrackHandle.html#method.add_event
|
||||
Track(&'a [(&'a TrackState, &'a TrackHandle)]),
|
||||
/// Speaking state update, typically describing how another voice
|
||||
/// user is transmitting audio data. Clients must send at least one such
|
||||
/// packet to allow SSRC/UserID matching.
|
||||
SpeakingStateUpdate(Speaking),
|
||||
/// Speaking state transition, describing whether a given source has started/stopped
|
||||
/// transmitting. This fires in response to a silent burst, or the first packet
|
||||
/// breaking such a burst.
|
||||
SpeakingUpdate {
|
||||
/// Synchronisation Source of the user who has begun speaking.
|
||||
///
|
||||
/// This must be combined with another event class to map this back to
|
||||
/// its original UserId.
|
||||
ssrc: u32,
|
||||
/// Whether this user is currently speaking.
|
||||
speaking: bool,
|
||||
},
|
||||
/// Opus audio packet, received from another stream (detailed in `packet`).
|
||||
/// `payload_offset` contains the true payload location within the raw packet's `payload()`,
|
||||
/// if extensions or raw packet data are required.
|
||||
/// if `audio.len() == 0`, then this packet arrived out-of-order.
|
||||
VoicePacket {
|
||||
/// Decoded audio from this packet.
|
||||
audio: &'a Vec<i16>,
|
||||
/// Raw RTP packet data.
|
||||
///
|
||||
/// Includes the SSRC (i.e., sender) of this packet.
|
||||
packet: &'a Rtp,
|
||||
/// Byte index into the packet for where the payload begins.
|
||||
payload_offset: usize,
|
||||
},
|
||||
/// Telemetry/statistics packet, received from another stream (detailed in `packet`).
|
||||
/// `payload_offset` contains the true payload location within the raw packet's `payload()`,
|
||||
/// to allow manual decoding of `Rtcp` packet bodies.
|
||||
RtcpPacket {
|
||||
/// Raw RTCP packet data.
|
||||
packet: &'a Rtcp,
|
||||
/// Byte index into the packet for where the payload begins.
|
||||
payload_offset: usize,
|
||||
},
|
||||
/// Fired whenever a client connects to a call for the first time, allowing SSRC/UserID
|
||||
/// matching.
|
||||
ClientConnect(ClientConnect),
|
||||
/// Fired whenever a client disconnects.
|
||||
ClientDisconnect(ClientDisconnect),
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub(crate) enum CoreContext {
|
||||
SpeakingStateUpdate(Speaking),
|
||||
SpeakingUpdate {
|
||||
ssrc: u32,
|
||||
speaking: bool,
|
||||
},
|
||||
VoicePacket {
|
||||
audio: Vec<i16>,
|
||||
packet: Rtp,
|
||||
payload_offset: usize,
|
||||
},
|
||||
RtcpPacket {
|
||||
packet: Rtcp,
|
||||
payload_offset: usize,
|
||||
},
|
||||
ClientConnect(ClientConnect),
|
||||
ClientDisconnect(ClientDisconnect),
|
||||
}
|
||||
|
||||
impl<'a> CoreContext {
|
||||
pub(crate) fn to_user_context(&'a self) -> EventContext<'a> {
|
||||
use CoreContext::*;
|
||||
|
||||
match self {
|
||||
SpeakingStateUpdate(evt) => EventContext::SpeakingStateUpdate(*evt),
|
||||
SpeakingUpdate { ssrc, speaking } => EventContext::SpeakingUpdate {
|
||||
ssrc: *ssrc,
|
||||
speaking: *speaking,
|
||||
},
|
||||
VoicePacket {
|
||||
audio,
|
||||
packet,
|
||||
payload_offset,
|
||||
} => EventContext::VoicePacket {
|
||||
audio,
|
||||
packet,
|
||||
payload_offset: *payload_offset,
|
||||
},
|
||||
RtcpPacket {
|
||||
packet,
|
||||
payload_offset,
|
||||
} => EventContext::RtcpPacket {
|
||||
packet,
|
||||
payload_offset: *payload_offset,
|
||||
},
|
||||
ClientConnect(evt) => EventContext::ClientConnect(*evt),
|
||||
ClientDisconnect(evt) => EventContext::ClientDisconnect(*evt),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl EventContext<'_> {
|
||||
/// Retreive the event class for an event (i.e., when matching)
|
||||
/// an event against the registered listeners.
|
||||
pub fn to_core_event(&self) -> Option<CoreEvent> {
|
||||
use EventContext::*;
|
||||
|
||||
match self {
|
||||
SpeakingStateUpdate { .. } => Some(CoreEvent::SpeakingStateUpdate),
|
||||
SpeakingUpdate { .. } => Some(CoreEvent::SpeakingUpdate),
|
||||
VoicePacket { .. } => Some(CoreEvent::VoicePacket),
|
||||
RtcpPacket { .. } => Some(CoreEvent::RtcpPacket),
|
||||
ClientConnect { .. } => Some(CoreEvent::ClientConnect),
|
||||
ClientDisconnect { .. } => Some(CoreEvent::ClientDisconnect),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
31
src/events/core.rs
Normal file
31
src/events/core.rs
Normal file
@@ -0,0 +1,31 @@
|
||||
#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
|
||||
/// Voice core events occur on receipt of
|
||||
/// voice packets and telemetry.
|
||||
///
|
||||
/// Core events persist while the `action` in [`EventData`]
|
||||
/// returns `None`.
|
||||
///
|
||||
/// [`EventData`]: struct.EventData.html
|
||||
pub enum CoreEvent {
|
||||
/// Fired on receipt of a speaking state update from another host.
|
||||
///
|
||||
/// Note: this will fire when a user starts speaking for the first time,
|
||||
/// or changes their capabilities.
|
||||
SpeakingStateUpdate,
|
||||
/// Fires when a source starts speaking, or stops speaking
|
||||
/// (*i.e.*, 5 consecutive silent frames).
|
||||
SpeakingUpdate,
|
||||
/// Fires on receipt of a voice packet from another stream in the voice call.
|
||||
///
|
||||
/// As RTP packets do not map to Discord's notion of users, SSRCs must be mapped
|
||||
/// back using the user IDs seen through client connection, disconnection,
|
||||
/// or speaking state update.
|
||||
VoicePacket,
|
||||
/// Fires on receipt of an RTCP packet, containing various call stats
|
||||
/// such as latency reports.
|
||||
RtcpPacket,
|
||||
/// Fires whenever a user connects to the same stream as the bot.
|
||||
ClientConnect,
|
||||
/// Fires whenever a user disconnects from the same stream as the bot.
|
||||
ClientDisconnect,
|
||||
}
|
||||
88
src/events/data.rs
Normal file
88
src/events/data.rs
Normal file
@@ -0,0 +1,88 @@
|
||||
use super::*;
|
||||
use std::{cmp::Ordering, time::Duration};
|
||||
|
||||
/// Internal representation of an event, as handled by the audio context.
|
||||
pub struct EventData {
|
||||
pub(crate) event: Event,
|
||||
pub(crate) fire_time: Option<Duration>,
|
||||
pub(crate) action: Box<dyn EventHandler>,
|
||||
}
|
||||
|
||||
impl EventData {
|
||||
/// Create a representation of an event and its associated handler.
|
||||
///
|
||||
/// An event handler, `action`, receives an [`EventContext`] and optionally
|
||||
/// produces a new [`Event`] type for itself. Returning `None` will
|
||||
/// maintain the same event type, while removing any [`Delayed`] entries.
|
||||
/// Event handlers will be re-added with their new trigger condition,
|
||||
/// or removed if [`Cancel`]led
|
||||
///
|
||||
/// [`EventContext`]: enum.EventContext.html
|
||||
/// [`Event`]: enum.Event.html
|
||||
/// [`Delayed`]: enum.Event.html#variant.Delayed
|
||||
/// [`Cancel`]: enum.Event.html#variant.Cancel
|
||||
pub fn new<F: EventHandler + 'static>(event: Event, action: F) -> Self {
|
||||
Self {
|
||||
event,
|
||||
fire_time: None,
|
||||
action: Box::new(action),
|
||||
}
|
||||
}
|
||||
|
||||
/// Computes the next firing time for a timer event.
|
||||
pub fn compute_activation(&mut self, now: Duration) {
|
||||
match self.event {
|
||||
Event::Periodic(period, phase) => {
|
||||
self.fire_time = Some(now + phase.unwrap_or(period));
|
||||
},
|
||||
Event::Delayed(offset) => {
|
||||
self.fire_time = Some(now + offset);
|
||||
},
|
||||
_ => {},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl std::fmt::Debug for EventData {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> Result<(), std::fmt::Error> {
|
||||
write!(
|
||||
f,
|
||||
"Event {{ event: {:?}, fire_time: {:?}, action: <fn> }}",
|
||||
self.event, self.fire_time
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/// Events are ordered/compared based on their firing time.
|
||||
impl Ord for EventData {
|
||||
fn cmp(&self, other: &Self) -> Ordering {
|
||||
if self.fire_time.is_some() && other.fire_time.is_some() {
|
||||
let t1 = self
|
||||
.fire_time
|
||||
.as_ref()
|
||||
.expect("T1 known to be well-defined by above.");
|
||||
let t2 = other
|
||||
.fire_time
|
||||
.as_ref()
|
||||
.expect("T2 known to be well-defined by above.");
|
||||
|
||||
t1.cmp(&t2)
|
||||
} else {
|
||||
Ordering::Equal
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl PartialOrd for EventData {
|
||||
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
|
||||
Some(self.cmp(other))
|
||||
}
|
||||
}
|
||||
|
||||
impl PartialEq for EventData {
|
||||
fn eq(&self, other: &Self) -> bool {
|
||||
self.fire_time == other.fire_time
|
||||
}
|
||||
}
|
||||
|
||||
impl Eq for EventData {}
|
||||
91
src/events/mod.rs
Normal file
91
src/events/mod.rs
Normal file
@@ -0,0 +1,91 @@
|
||||
//! Events relating to tracks, timing, and other callers.
|
||||
|
||||
mod context;
|
||||
mod core;
|
||||
mod data;
|
||||
mod store;
|
||||
mod track;
|
||||
mod untimed;
|
||||
|
||||
pub use self::{context::*, core::*, data::*, store::*, track::*, untimed::*};
|
||||
|
||||
use async_trait::async_trait;
|
||||
use std::time::Duration;
|
||||
|
||||
#[async_trait]
|
||||
/// Trait to handle an event which can be fired per-track, or globally.
|
||||
///
|
||||
/// These may be feasibly reused between several event sources.
|
||||
pub trait EventHandler: Send + Sync {
|
||||
/// Respond to one received event.
|
||||
async fn act(&self, ctx: &EventContext<'_>) -> Option<Event>;
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
|
||||
/// Classes of event which may occur, triggering a handler
|
||||
/// at the local (track-specific) or global level.
|
||||
///
|
||||
/// Local time-based events rely upon the current playback
|
||||
/// time of a track, and so will not fire if a track becomes paused
|
||||
/// or stops. In case this is required, global events are a better
|
||||
/// fit.
|
||||
///
|
||||
/// Event handlers themselves are described in [`EventData::action`].
|
||||
///
|
||||
/// [`EventData::action`]: struct.EventData.html#method.action
|
||||
pub enum Event {
|
||||
/// Periodic events rely upon two parameters: a *period*
|
||||
/// and an optional *phase*.
|
||||
///
|
||||
/// If the *phase* is `None`, then the event will first fire
|
||||
/// in one *period*. Periodic events repeat automatically
|
||||
/// so long as the `action` in [`EventData`] returns `None`.
|
||||
///
|
||||
/// [`EventData`]: struct.EventData.html
|
||||
Periodic(Duration, Option<Duration>),
|
||||
/// Delayed events rely upon a *delay* parameter, and
|
||||
/// fire one *delay* after the audio context processes them.
|
||||
///
|
||||
/// Delayed events are automatically removed once fired,
|
||||
/// so long as the `action` in [`EventData`] returns `None`.
|
||||
///
|
||||
/// [`EventData`]: struct.EventData.html
|
||||
Delayed(Duration),
|
||||
/// Track events correspond to certain actions or changes
|
||||
/// of state, such as a track finishing, looping, or being
|
||||
/// manually stopped.
|
||||
///
|
||||
/// Track events persist while the `action` in [`EventData`]
|
||||
/// returns `None`.
|
||||
///
|
||||
/// [`EventData`]: struct.EventData.html
|
||||
Track(TrackEvent),
|
||||
/// Core events
|
||||
///
|
||||
/// Track events persist while the `action` in [`EventData`]
|
||||
/// returns `None`. Core events **must** be applied globally,
|
||||
/// as attaching them to a track is a no-op.
|
||||
///
|
||||
/// [`EventData`]: struct.EventData.html
|
||||
Core(CoreEvent),
|
||||
/// Cancels the event, if it was intended to persist.
|
||||
Cancel,
|
||||
}
|
||||
|
||||
impl Event {
|
||||
pub(crate) fn is_global_only(&self) -> bool {
|
||||
matches!(self, Self::Core(_))
|
||||
}
|
||||
}
|
||||
|
||||
impl From<TrackEvent> for Event {
|
||||
fn from(evt: TrackEvent) -> Self {
|
||||
Event::Track(evt)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<CoreEvent> for Event {
|
||||
fn from(evt: CoreEvent) -> Self {
|
||||
Event::Core(evt)
|
||||
}
|
||||
}
|
||||
252
src/events/store.rs
Normal file
252
src/events/store.rs
Normal file
@@ -0,0 +1,252 @@
|
||||
use super::*;
|
||||
use crate::{
|
||||
constants::*,
|
||||
tracks::{PlayMode, TrackHandle, TrackState},
|
||||
};
|
||||
use std::{
|
||||
collections::{BinaryHeap, HashMap},
|
||||
time::Duration,
|
||||
};
|
||||
use tracing::info;
|
||||
|
||||
#[derive(Debug, Default)]
|
||||
/// Storage for [`EventData`], designed to be used for both local and global contexts.
|
||||
///
|
||||
/// Timed events are stored in a binary heap for fast selection, and have custom `Eq`,
|
||||
/// `Ord`, etc. implementations to support (only) this.
|
||||
///
|
||||
/// [`EventData`]: struct.EventData.html
|
||||
pub struct EventStore {
|
||||
timed: BinaryHeap<EventData>,
|
||||
untimed: HashMap<UntimedEvent, Vec<EventData>>,
|
||||
local_only: bool,
|
||||
}
|
||||
|
||||
impl EventStore {
|
||||
/// Creates a new event store to be used globally.
|
||||
pub fn new() -> Self {
|
||||
Default::default()
|
||||
}
|
||||
|
||||
/// Creates a new event store to be used within a [`Track`].
|
||||
///
|
||||
/// This is usually automatically installed by the driver once
|
||||
/// a track has been registered.
|
||||
///
|
||||
/// [`Track`]: ../tracks/struct.Track.html
|
||||
pub fn new_local() -> Self {
|
||||
EventStore {
|
||||
local_only: true,
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
|
||||
/// Add an event to this store.
|
||||
///
|
||||
/// Updates `evt` according to [`EventData::compute_activation`].
|
||||
///
|
||||
/// [`EventData::compute_activation`]: struct.EventData.html#method.compute_activation
|
||||
pub fn add_event(&mut self, mut evt: EventData, now: Duration) {
|
||||
evt.compute_activation(now);
|
||||
|
||||
if self.local_only && evt.event.is_global_only() {
|
||||
return;
|
||||
}
|
||||
|
||||
use Event::*;
|
||||
match evt.event {
|
||||
Core(c) => {
|
||||
self.untimed
|
||||
.entry(c.into())
|
||||
.or_insert_with(Vec::new)
|
||||
.push(evt);
|
||||
},
|
||||
Track(t) => {
|
||||
self.untimed
|
||||
.entry(t.into())
|
||||
.or_insert_with(Vec::new)
|
||||
.push(evt);
|
||||
},
|
||||
Delayed(_) | Periodic(_, _) => {
|
||||
self.timed.push(evt);
|
||||
},
|
||||
_ => {
|
||||
// Event cancelled.
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
/// Processes all events due up to and including `now`.
|
||||
pub(crate) async fn process_timed(&mut self, now: Duration, ctx: EventContext<'_>) {
|
||||
while let Some(evt) = self.timed.peek() {
|
||||
if evt
|
||||
.fire_time
|
||||
.as_ref()
|
||||
.expect("Timed event must have a fire_time.")
|
||||
> &now
|
||||
{
|
||||
break;
|
||||
}
|
||||
let mut evt = self
|
||||
.timed
|
||||
.pop()
|
||||
.expect("Can only succeed due to peek = Some(...).");
|
||||
|
||||
let old_evt_type = evt.event;
|
||||
if let Some(new_evt_type) = evt.action.act(&ctx).await {
|
||||
evt.event = new_evt_type;
|
||||
self.add_event(evt, now);
|
||||
} else if let Event::Periodic(d, _) = old_evt_type {
|
||||
evt.event = Event::Periodic(d, None);
|
||||
self.add_event(evt, now);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Processes all events attached to the given track event.
|
||||
pub(crate) async fn process_untimed(
|
||||
&mut self,
|
||||
now: Duration,
|
||||
untimed_event: UntimedEvent,
|
||||
ctx: EventContext<'_>,
|
||||
) {
|
||||
// move a Vec in and out: not too expensive, but could be better.
|
||||
// Although it's obvious that moving an event out of one vec and into
|
||||
// another necessitates that they be different event types, thus entries,
|
||||
// convincing the compiler of this is non-trivial without making them dedicated
|
||||
// fields.
|
||||
let events = self.untimed.remove(&untimed_event);
|
||||
if let Some(mut events) = events {
|
||||
// TODO: Possibly use tombstones to prevent realloc/memcpys?
|
||||
// i.e., never shrink array, replace ended tracks with <DEAD>,
|
||||
// maintain a "first-track" stack and freelist alongside.
|
||||
let mut i = 0;
|
||||
while i < events.len() {
|
||||
let evt = &mut events[i];
|
||||
// Only remove/readd if the event type changes (i.e., Some AND new != old)
|
||||
if let Some(new_evt_type) = evt.action.act(&ctx).await {
|
||||
if evt.event == new_evt_type {
|
||||
let mut evt = events.remove(i);
|
||||
|
||||
evt.event = new_evt_type;
|
||||
self.add_event(evt, now);
|
||||
} else {
|
||||
i += 1;
|
||||
}
|
||||
} else {
|
||||
i += 1;
|
||||
};
|
||||
}
|
||||
self.untimed.insert(untimed_event, events);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Default)]
|
||||
pub(crate) struct GlobalEvents {
|
||||
pub(crate) store: EventStore,
|
||||
pub(crate) time: Duration,
|
||||
pub(crate) awaiting_tick: HashMap<TrackEvent, Vec<usize>>,
|
||||
}
|
||||
|
||||
impl GlobalEvents {
|
||||
pub(crate) fn add_event(&mut self, evt: EventData) {
|
||||
self.store.add_event(evt, self.time);
|
||||
}
|
||||
|
||||
pub(crate) async fn fire_core_event(&mut self, evt: CoreEvent, ctx: EventContext<'_>) {
|
||||
self.store.process_untimed(self.time, evt.into(), ctx).await;
|
||||
}
|
||||
|
||||
pub(crate) fn fire_track_event(&mut self, evt: TrackEvent, index: usize) {
|
||||
let holder = self.awaiting_tick.entry(evt).or_insert_with(Vec::new);
|
||||
|
||||
holder.push(index);
|
||||
}
|
||||
|
||||
pub(crate) async fn tick(
|
||||
&mut self,
|
||||
events: &mut Vec<EventStore>,
|
||||
states: &mut Vec<TrackState>,
|
||||
handles: &mut Vec<TrackHandle>,
|
||||
) {
|
||||
// Global timed events
|
||||
self.time += TIMESTEP_LENGTH;
|
||||
self.store
|
||||
.process_timed(self.time, EventContext::Track(&[]))
|
||||
.await;
|
||||
|
||||
// Local timed events
|
||||
for (i, state) in states.iter_mut().enumerate() {
|
||||
if state.playing == PlayMode::Play {
|
||||
state.step_frame();
|
||||
|
||||
let event_store = events
|
||||
.get_mut(i)
|
||||
.expect("Missing store index for Tick (local timed).");
|
||||
let handle = handles
|
||||
.get_mut(i)
|
||||
.expect("Missing handle index for Tick (local timed).");
|
||||
|
||||
event_store
|
||||
.process_timed(state.play_time, EventContext::Track(&[(&state, &handle)]))
|
||||
.await;
|
||||
}
|
||||
}
|
||||
|
||||
for (evt, indices) in self.awaiting_tick.iter() {
|
||||
let untimed = (*evt).into();
|
||||
|
||||
if !indices.is_empty() {
|
||||
info!("Firing {:?} for {:?}", evt, indices);
|
||||
}
|
||||
|
||||
// Local untimed track events.
|
||||
for &i in indices.iter() {
|
||||
let event_store = events
|
||||
.get_mut(i)
|
||||
.expect("Missing store index for Tick (local untimed).");
|
||||
let handle = handles
|
||||
.get_mut(i)
|
||||
.expect("Missing handle index for Tick (local untimed).");
|
||||
let state = states
|
||||
.get_mut(i)
|
||||
.expect("Missing state index for Tick (local untimed).");
|
||||
|
||||
event_store
|
||||
.process_untimed(
|
||||
state.position,
|
||||
untimed,
|
||||
EventContext::Track(&[(&state, &handle)]),
|
||||
)
|
||||
.await;
|
||||
}
|
||||
|
||||
// Global untimed track events.
|
||||
if self.store.untimed.contains_key(&untimed) && !indices.is_empty() {
|
||||
let global_ctx: Vec<(&TrackState, &TrackHandle)> = indices
|
||||
.iter()
|
||||
.map(|i| {
|
||||
(
|
||||
states
|
||||
.get(*i)
|
||||
.expect("Missing state index for Tick (global untimed)"),
|
||||
handles
|
||||
.get(*i)
|
||||
.expect("Missing handle index for Tick (global untimed)"),
|
||||
)
|
||||
})
|
||||
.collect();
|
||||
|
||||
self.store
|
||||
.process_untimed(self.time, untimed, EventContext::Track(&global_ctx[..]))
|
||||
.await
|
||||
}
|
||||
}
|
||||
|
||||
// Now drain vecs.
|
||||
for (_evt, indices) in self.awaiting_tick.iter_mut() {
|
||||
indices.clear();
|
||||
}
|
||||
}
|
||||
}
|
||||
16
src/events/track.rs
Normal file
16
src/events/track.rs
Normal file
@@ -0,0 +1,16 @@
|
||||
#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
|
||||
/// Track events correspond to certain actions or changes
|
||||
/// of state, such as a track finishing, looping, or being
|
||||
/// manually stopped. Voice core events occur on receipt of
|
||||
/// voice packets and telemetry.
|
||||
///
|
||||
/// Track events persist while the `action` in [`EventData`]
|
||||
/// returns `None`.
|
||||
///
|
||||
/// [`EventData`]: struct.EventData.html
|
||||
pub enum TrackEvent {
|
||||
/// The attached track has ended.
|
||||
End,
|
||||
/// The attached track has looped.
|
||||
Loop,
|
||||
}
|
||||
28
src/events/untimed.rs
Normal file
28
src/events/untimed.rs
Normal file
@@ -0,0 +1,28 @@
|
||||
use super::*;
|
||||
|
||||
#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
|
||||
/// Track and voice core events.
|
||||
///
|
||||
/// Untimed events persist while the `action` in [`EventData`]
|
||||
/// returns `None`.
|
||||
///
|
||||
/// [`EventData`]: struct.EventData.html
|
||||
pub enum UntimedEvent {
|
||||
/// Untimed events belonging to a track, such as state changes, end, or loops.
|
||||
Track(TrackEvent),
|
||||
/// Untimed events belonging to the global context, such as finished tracks,
|
||||
/// client speaking updates, or RT(C)P voice and telemetry data.
|
||||
Core(CoreEvent),
|
||||
}
|
||||
|
||||
impl From<TrackEvent> for UntimedEvent {
|
||||
fn from(evt: TrackEvent) -> Self {
|
||||
UntimedEvent::Track(evt)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<CoreEvent> for UntimedEvent {
|
||||
fn from(evt: CoreEvent) -> Self {
|
||||
UntimedEvent::Core(evt)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user