use rustc_hash::FxHashMap;
use crate::input::UiEvent;
use crate::key::Key;
use crate::resources::SoundId;
use crate::tree::OriginId;
use crate::value::Value;
use crate::window::WindowId;
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, PartialOrd, Ord)]
pub struct PlaybackId(pub u64);
#[derive(Clone, Debug, PartialEq)]
pub struct PlayOptions {
pub volume: f32,
pub looped: bool,
pub fade_in_ms: f32,
pub tag: Option<Value>,
}
impl Default for PlayOptions {
fn default() -> Self {
Self {
volume: 1.0,
looped: false,
fade_in_ms: 0.0,
tag: None,
}
}
}
impl PlayOptions {
pub fn volume(mut self, v: f32) -> Self {
self.volume = v;
self
}
pub fn looped(mut self) -> Self {
self.looped = true;
self
}
pub fn fade_in(mut self, ms: f32) -> Self {
self.fade_in_ms = ms;
self
}
pub fn tag(mut self, tag: impl Into<Value>) -> Self {
self.tag = Some(tag.into());
self
}
}
#[derive(Clone, Debug, PartialEq)]
pub struct AudioSpec {
pub src: SoundId,
pub volume: f32,
pub looped: bool,
pub paused: bool,
pub finish: bool,
pub tag: Option<Value>,
}
impl AudioSpec {
pub fn new(src: SoundId) -> Self {
Self {
src,
volume: 1.0,
looped: false,
paused: false,
finish: false,
tag: None,
}
}
pub fn volume(mut self, v: f32) -> Self {
self.volume = v;
self
}
pub fn looped(mut self) -> Self {
self.looped = true;
self
}
pub fn paused(mut self, paused: bool) -> Self {
self.paused = paused;
self
}
pub fn finish(mut self) -> Self {
self.finish = true;
self
}
pub fn tag(mut self, tag: impl Into<Value>) -> Self {
self.tag = Some(tag.into());
self
}
}
#[derive(Clone, Debug, PartialEq)]
pub enum AudioCommand {
Play {
playback: PlaybackId,
sound: SoundId,
volume: f32,
looped: bool,
fade_in_ms: f32,
},
Stop {
playback: PlaybackId,
fade_ms: f32,
},
SetVolume {
playback: PlaybackId,
volume: f32,
tween_ms: f32,
},
Pause {
playback: PlaybackId,
fade_ms: f32,
},
Resume {
playback: PlaybackId,
fade_ms: f32,
},
MasterVolume {
volume: f32,
tween_ms: f32,
},
Unload {
sound: SoundId,
},
}
impl AudioCommand {
pub fn kind_name(&self) -> &'static str {
match self {
AudioCommand::Play { .. } => "play",
AudioCommand::Stop { .. } => "stop",
AudioCommand::SetVolume { .. } => "setVolume",
AudioCommand::Pause { .. } => "pause",
AudioCommand::Resume { .. } => "resume",
AudioCommand::MasterVolume { .. } => "masterVolume",
AudioCommand::Unload { .. } => "unload",
}
}
pub fn to_value(&self, h: crate::value::Handles) -> Value {
let pb = |p: PlaybackId| Value::Int(p.0 as i64);
let mut out = vec![("kind".to_string(), Value::str(self.kind_name()))];
let mut push = |k: &str, v: Value| out.push((k.to_string(), v));
match *self {
AudioCommand::Play {
playback,
sound,
volume,
looped,
fade_in_ms,
} => {
push("playback", pb(playback));
push("sound", (h.id)(sound.to_ffi()));
push("volume", Value::float(volume));
push("loop", Value::Bool(looped));
push("fade_in", Value::float(fade_in_ms));
}
AudioCommand::Stop { playback, fade_ms }
| AudioCommand::Pause { playback, fade_ms }
| AudioCommand::Resume { playback, fade_ms } => {
push("playback", pb(playback));
push("fade", Value::float(fade_ms));
}
AudioCommand::SetVolume {
playback,
volume,
tween_ms,
} => {
push("playback", pb(playback));
push("volume", Value::float(volume));
push("tween", Value::float(tween_ms));
}
AudioCommand::MasterVolume { volume, tween_ms } => {
push("volume", Value::float(volume));
push("tween", Value::float(tween_ms));
}
AudioCommand::Unload { sound } => push("sound", (h.id)(sound.to_ffi())),
}
Value::Map(out)
}
}
struct Tagged {
origin: OriginId,
window: WindowId,
key: Key,
tag: Value,
}
struct Mounted {
playback: PlaybackId,
spec: AudioSpec,
}
type Mount = (WindowId, Key);
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum Why {
Removed,
Restarted,
}
impl Why {
pub(crate) fn verb(self) -> &'static str {
match self {
Why::Removed => "removed",
Why::Restarted => "restarted",
}
}
}
const MAX_STOPPED: usize = 256;
#[derive(Default)]
pub struct AudioStore {
next: u64,
commands: Vec<AudioCommand>,
tagged: FxHashMap<PlaybackId, Tagged>,
mounted: FxHashMap<Mount, Mounted>,
declared: Vec<(Mount, OriginId, AudioSpec)>,
stopped: FxHashMap<PlaybackId, (Key, Why)>,
}
impl AudioStore {
fn alloc(&mut self) -> PlaybackId {
self.next += 1;
PlaybackId(self.next)
}
pub(crate) fn play(
&mut self,
origin: OriginId,
window: WindowId,
key: Key,
sound: SoundId,
opts: PlayOptions,
) -> PlaybackId {
let playback = self.alloc();
self.commands.push(AudioCommand::Play {
playback,
sound,
volume: opts.volume,
looped: opts.looped,
fade_in_ms: opts.fade_in_ms,
});
if let Some(tag) = opts.tag {
self.tagged.insert(
playback,
Tagged {
origin,
window,
key,
tag,
},
);
}
playback
}
pub(crate) fn stop(&mut self, playback: PlaybackId, fade_ms: f32) {
self.tagged.remove(&playback);
self.commands.push(AudioCommand::Stop { playback, fade_ms });
}
fn record_stop(&mut self, playback: PlaybackId, key: Key, why: Why) {
if self.stopped.len() >= MAX_STOPPED
&& let Some(oldest) = self.stopped.keys().min().copied()
{
self.stopped.remove(&oldest);
}
self.stopped.insert(playback, (key, why));
}
pub(crate) fn truncated(&mut self, playback: PlaybackId) -> Option<(Key, Why)> {
self.stopped.remove(&playback)
}
pub(crate) fn set_volume(&mut self, playback: PlaybackId, volume: f32, tween_ms: f32) {
self.commands.push(AudioCommand::SetVolume {
playback,
volume,
tween_ms,
});
}
pub(crate) fn pause(&mut self, playback: PlaybackId, fade_ms: f32) {
self.commands
.push(AudioCommand::Pause { playback, fade_ms });
}
pub(crate) fn resume(&mut self, playback: PlaybackId, fade_ms: f32) {
self.commands
.push(AudioCommand::Resume { playback, fade_ms });
}
pub(crate) fn master_volume(&mut self, volume: f32, tween_ms: f32) {
self.commands
.push(AudioCommand::MasterVolume { volume, tween_ms });
}
pub(crate) fn unload(&mut self, sound: SoundId) {
self.commands.push(AudioCommand::Unload { sound });
}
pub fn take_commands(&mut self) -> Vec<AudioCommand> {
std::mem::take(&mut self.commands)
}
pub fn pending(&self) -> &[AudioCommand] {
&self.commands
}
pub fn playback_of(&self, window: WindowId, key: Key) -> Option<PlaybackId> {
self.mounted.get(&(window, key)).map(|m| m.playback)
}
#[cfg(feature = "conformance")]
pub(crate) fn any_mounted(&self, window: WindowId) -> bool {
self.mounted.keys().any(|(w, _)| *w == window)
}
pub(crate) fn declare(
&mut self,
window: WindowId,
key: Key,
origin: OriginId,
spec: AudioSpec,
) {
self.declared.push(((window, key), origin, spec));
}
pub(crate) fn ended(&mut self, playback: PlaybackId) -> Option<UiEvent> {
self.stopped.remove(&playback);
let t = self.tagged.remove(&playback)?;
Some(UiEvent {
origin: t.origin,
window: t.window,
key: t.key,
payload: Value::map([
("kind", Value::str("sound")),
("phase", Value::str("ended")),
("playback", Value::Int(playback.0 as i64)),
("tag", t.tag),
]),
slot: None,
})
}
pub(crate) fn refused(
&mut self,
playback: PlaybackId,
) -> (Option<UiEvent>, crate::diag::Warning) {
let warning = crate::diag::playback_refused(self.key_of(playback), playback);
let event = self.tagged.remove(&playback).map(|t| UiEvent {
origin: t.origin,
window: t.window,
key: t.key,
payload: Value::map([
("kind", Value::str("sound")),
("phase", Value::str("refused")),
("playback", Value::Int(playback.0 as i64)),
("tag", t.tag),
]),
slot: None,
});
(event, warning)
}
fn key_of(&self, playback: PlaybackId) -> Key {
if let Some(t) = self.tagged.get(&playback) {
return t.key;
}
self.mounted
.iter()
.find(|(_, m)| m.playback == playback)
.map(|((_, k), _)| *k)
.unwrap_or(Key::ROOT)
}
pub(crate) fn reconcile(&mut self, window: WindowId) {
let (declared, others): (Vec<_>, Vec<_>) = std::mem::take(&mut self.declared)
.into_iter()
.partition(|(mount, _, _)| mount.0 == window);
self.declared = others;
let mut seen: Vec<Key> = Vec::with_capacity(declared.len());
for (mount, origin, spec) in declared {
let key = mount.1;
if seen.contains(&key) {
continue;
}
seen.push(key);
let restart = match self.mounted.get(&mount) {
None => true,
Some(m) => m.spec.src != spec.src || m.spec.looped != spec.looped,
};
if restart {
if let Some(old) = self.mounted.remove(&mount) {
self.stop(old.playback, 0.0);
if !old.spec.looped && !old.spec.finish {
self.record_stop(old.playback, key, Why::Restarted);
}
}
let opts = PlayOptions {
volume: spec.volume,
looped: spec.looped,
fade_in_ms: 0.0,
tag: spec.tag.clone(),
};
let playback = self.play(origin, window, key, spec.src, opts);
if spec.paused {
self.pause(playback, 0.0);
}
self.mounted.insert(mount, Mounted { playback, spec });
continue;
}
let m = self.mounted.get_mut(&mount).expect("mounted");
let playback = m.playback;
if m.spec.volume != spec.volume {
self.commands.push(AudioCommand::SetVolume {
playback,
volume: spec.volume,
tween_ms: 0.0,
});
}
if m.spec.paused != spec.paused {
self.commands.push(if spec.paused {
AudioCommand::Pause {
playback,
fade_ms: 0.0,
}
} else {
AudioCommand::Resume {
playback,
fade_ms: 0.0,
}
});
}
if m.spec.tag != spec.tag {
match (&spec.tag, self.tagged.get_mut(&playback)) {
(Some(tag), Some(t)) => t.tag = tag.clone(),
(Some(tag), None) => {
self.tagged.insert(
playback,
Tagged {
origin,
window,
key,
tag: tag.clone(),
},
);
}
(None, _) => {
self.tagged.remove(&playback);
}
}
}
m.spec = spec;
}
let gone: Vec<(Key, PlaybackId, bool, bool)> = self
.mounted
.iter()
.filter(|((w, k), _)| *w == window && !seen.contains(k))
.map(|((_, k), m)| (*k, m.playback, m.spec.finish, m.spec.looped))
.collect();
for (key, playback, finish, looped) in gone {
self.mounted.remove(&(window, key));
if finish && !looped {
continue;
}
self.stop(playback, 0.0);
if !looped && !finish {
self.record_stop(playback, key, Why::Removed);
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::resources::{Resources, SessionId};
fn sound() -> SoundId {
Resources::new(SessionId::next()).add_sound(vec![0; 4])
}
#[test]
fn play_allocates_ids_and_only_tagged_playbacks_report_ended() {
let mut a = AudioStore::default();
let s = sound();
let quiet = a.play(
OriginId::HOST,
WindowId::MAIN,
Key::ROOT,
s,
PlayOptions::default(),
);
let loud = a.play(
OriginId::HOST,
WindowId::MAIN,
Key::ROOT,
s,
PlayOptions::default().tag(Value::str("t")),
);
assert_ne!(quiet, loud);
assert_eq!(a.take_commands().len(), 2);
assert!(a.ended(quiet).is_none());
let ev = a.ended(loud).expect("tagged playback reports ended");
assert_eq!(ev.kind(), Some("sound"));
assert_eq!(ev.payload.get_str("tag"), Some("t"));
assert_eq!(ev.payload.get_int("playback"), Some(loud.0 as i64));
assert!(a.ended(loud).is_none());
}
#[test]
fn stop_cancels_the_ended_event() {
let mut a = AudioStore::default();
let s = sound();
let p = a.play(
OriginId::HOST,
WindowId::MAIN,
Key::ROOT,
s,
PlayOptions::default().tag(Value::Null),
);
a.stop(p, 0.0);
assert!(a.ended(p).is_none());
}
#[test]
fn a_removed_node_that_asked_to_finish_is_not_stopped() {
let mut a = AudioStore::default();
let s = sound();
let k = Key::ROOT.str("chime");
a.declare(
WindowId::MAIN,
k,
OriginId::HOST,
AudioSpec::new(s).finish(),
);
a.reconcile(WindowId::MAIN);
assert!(matches!(
a.take_commands().as_slice(),
[AudioCommand::Play { .. }]
));
a.reconcile(WindowId::MAIN);
assert_eq!(a.take_commands(), vec![]);
assert!(a.playback_of(WindowId::MAIN, k).is_none());
}
#[test]
fn a_removed_loop_stops_even_when_it_asked_to_finish() {
let mut a = AudioStore::default();
let s = sound();
let k = Key::ROOT.str("bed");
a.declare(
WindowId::MAIN,
k,
OriginId::HOST,
AudioSpec::new(s).looped().finish(),
);
a.reconcile(WindowId::MAIN);
let p = a.playback_of(WindowId::MAIN, k).unwrap();
a.take_commands();
a.reconcile(WindowId::MAIN);
assert_eq!(
a.take_commands(),
vec![AudioCommand::Stop {
playback: p,
fade_ms: 0.0
}]
);
}
#[test]
fn a_released_playback_still_reports_ended() {
let mut a = AudioStore::default();
let s = sound();
let k = Key::ROOT.str("chime");
let spec = {
let mut spec = AudioSpec::new(s).finish();
spec.tag = Some(Value::str("chime"));
spec
};
a.declare(WindowId::MAIN, k, OriginId::HOST, spec);
a.reconcile(WindowId::MAIN);
let p = a.playback_of(WindowId::MAIN, k).unwrap();
a.take_commands();
a.reconcile(WindowId::MAIN);
assert_eq!(a.take_commands(), vec![]);
let ev = a.ended(p).expect("a released playback still reports ended");
assert_eq!(ev.key, k);
assert_eq!(ev.payload.get_str("tag"), Some("chime"));
}
#[test]
fn a_refused_tagged_playback_reports_refused_and_warns() {
let mut a = AudioStore::default();
let s = sound();
let k = Key::ROOT.str("chime");
let p = a.play(
OriginId::HOST,
WindowId::MAIN,
k,
s,
PlayOptions::default().tag(Value::str("chime")),
);
let (event, warning) = a.refused(p);
let ev = event.expect("a tagged playback hears the refusal");
assert_eq!(ev.key, k);
assert_eq!(ev.kind(), Some("sound"));
assert_eq!(
ev.payload.get_str("phase"),
Some("refused"),
"told apart from the `ended` that will never come"
);
assert_eq!(ev.payload.get_str("tag"), Some("chime"));
assert_eq!(ev.payload.get_int("playback"), Some(p.0 as i64));
assert_eq!(warning.code, crate::diag::PLAYBACK_REFUSED);
assert_eq!(warning.key, k);
assert!(a.refused(p).0.is_none());
assert!(a.ended(p).is_none(), "and it can never end afterwards");
}
#[test]
fn a_refused_untagged_playback_is_the_warning_alone() {
let mut a = AudioStore::default();
let s = sound();
let p = a.play(
OriginId::HOST,
WindowId::MAIN,
Key::ROOT,
s,
PlayOptions::default(),
);
let (event, warning) = a.refused(p);
assert!(event.is_none(), "nothing asked to hear about this one");
assert_eq!(warning.code, crate::diag::PLAYBACK_REFUSED);
assert_eq!(warning.key, Key::ROOT);
}
#[test]
fn a_refused_released_playback_still_reports() {
let mut a = AudioStore::default();
let s = sound();
let k = Key::ROOT.str("chime");
let spec = {
let mut spec = AudioSpec::new(s).finish();
spec.tag = Some(Value::str("chime"));
spec
};
a.declare(WindowId::MAIN, k, OriginId::HOST, spec);
a.reconcile(WindowId::MAIN);
let p = a.playback_of(WindowId::MAIN, k).unwrap();
a.take_commands();
a.reconcile(WindowId::MAIN);
assert!(a.playback_of(WindowId::MAIN, k).is_none());
let (event, warning) = a.refused(p);
let ev = event.expect("a released playback still hears the refusal");
assert_eq!(ev.key, k);
assert_eq!(ev.payload.get_str("phase"), Some("refused"));
assert_eq!(warning.key, k);
}
#[test]
fn an_untagged_audio_node_warns_on_its_own_key() {
let mut a = AudioStore::default();
let s = sound();
let k = Key::ROOT.str("bed");
a.declare(
WindowId::MAIN,
k,
OriginId::HOST,
AudioSpec::new(s).looped(),
);
a.reconcile(WindowId::MAIN);
let p = a.playback_of(WindowId::MAIN, k).unwrap();
let (event, warning) = a.refused(p);
assert!(event.is_none());
assert_eq!(warning.key, k);
}
#[test]
fn audio_nodes_reconcile_by_key() {
let mut a = AudioStore::default();
let s = sound();
let k = Key::ROOT.str("music");
a.declare(
WindowId::MAIN,
k,
OriginId::HOST,
AudioSpec::new(s).looped(),
);
a.reconcile(WindowId::MAIN);
let cmds = a.take_commands();
assert!(matches!(
cmds.as_slice(),
[AudioCommand::Play { looped: true, .. }]
));
let p = a.playback_of(WindowId::MAIN, k).unwrap();
a.declare(
WindowId::MAIN,
k,
OriginId::HOST,
AudioSpec::new(s).looped(),
);
a.reconcile(WindowId::MAIN);
assert!(a.take_commands().is_empty());
a.declare(
WindowId::MAIN,
k,
OriginId::HOST,
AudioSpec::new(s).looped().volume(0.5).paused(true),
);
a.reconcile(WindowId::MAIN);
let cmds = a.take_commands();
assert_eq!(
cmds,
vec![
AudioCommand::SetVolume {
playback: p,
volume: 0.5,
tween_ms: 0.0
},
AudioCommand::Pause {
playback: p,
fade_ms: 0.0
}
]
);
a.reconcile(WindowId::MAIN);
assert_eq!(
a.take_commands(),
vec![AudioCommand::Stop {
playback: p,
fade_ms: 0.0
}]
);
assert!(a.playback_of(WindowId::MAIN, k).is_none());
}
#[test]
fn changing_src_restarts() {
let mut a = AudioStore::default();
let mut r = Resources::new(SessionId::next());
let (s1, s2) = (r.add_sound(vec![0; 4]), r.add_sound(vec![1; 4]));
let k = Key::ROOT.str("fx");
a.declare(WindowId::MAIN, k, OriginId::HOST, AudioSpec::new(s1));
a.reconcile(WindowId::MAIN);
let p1 = a.playback_of(WindowId::MAIN, k).unwrap();
a.take_commands();
a.declare(WindowId::MAIN, k, OriginId::HOST, AudioSpec::new(s2));
a.reconcile(WindowId::MAIN);
let cmds = a.take_commands();
assert!(matches!(
cmds.as_slice(),
[
AudioCommand::Stop { playback, .. },
AudioCommand::Play { sound, .. }
] if *playback == p1 && *sound == s2
));
}
}