mirui 0.46.3

A lightweight, no_std ECS-driven UI framework for embedded, mobile, desktop, and WebAssembly
Documentation
use super::{AudioTone, CueId};

#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum AudioCommand {
    Play { cue: CueId, gain: u8 },
    Tone(AudioTone),
    Stop { cue: CueId },
    StopAll,
    SetMasterGain(u8),
    SetMuted(bool),
}

impl AudioCommand {
    const fn is_disposable(self) -> bool {
        matches!(self, Self::Play { .. } | Self::Tone(_))
    }

    const fn state_kind(self) -> u8 {
        match self {
            Self::SetMasterGain(_) => 1,
            Self::SetMuted(_) => 2,
            _ => 0,
        }
    }

    const fn is_priority(self) -> bool {
        !matches!(self, Self::Play { .. } | Self::Tone(_))
    }
}

#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
pub enum AudioOutputState {
    #[default]
    Starting,
    Locked,
    Ready,
    Suspended,
    Failed,
    Stopped,
}

pub struct AudioBus<const N: usize = 32> {
    queue: [Option<AudioCommand>; N],
    head: usize,
    len: usize,
    muted: bool,
    master_gain: u8,
    saturation_count: u32,
    failure_count: u32,
    state: AudioOutputState,
}

impl<const N: usize> Default for AudioBus<N> {
    fn default() -> Self {
        Self::new()
    }
}

impl<const N: usize> AudioBus<N> {
    pub const fn new() -> Self {
        Self {
            queue: [None; N],
            head: 0,
            len: 0,
            muted: false,
            master_gain: 220,
            saturation_count: 0,
            failure_count: 0,
            state: AudioOutputState::Starting,
        }
    }

    pub fn play(&mut self, cue: CueId) -> bool {
        self.play_with_gain(cue, u8::MAX)
    }

    pub fn play_with_gain(&mut self, cue: CueId, gain: u8) -> bool {
        self.enqueue(AudioCommand::Play { cue, gain })
    }

    pub fn tone(&mut self, tone: AudioTone) -> bool {
        self.enqueue(AudioCommand::Tone(tone))
    }

    pub fn stop(&mut self, cue: CueId) -> bool {
        self.enqueue(AudioCommand::Stop { cue })
    }

    pub fn stop_all(&mut self) -> bool {
        self.enqueue(AudioCommand::StopAll)
    }

    pub fn set_muted(&mut self, muted: bool) -> bool {
        self.muted = muted;
        self.enqueue(AudioCommand::SetMuted(muted))
    }

    pub fn toggle_muted(&mut self) -> bool {
        self.set_muted(!self.muted)
    }

    pub fn set_master_gain(&mut self, gain: u8) -> bool {
        self.master_gain = gain;
        self.enqueue(AudioCommand::SetMasterGain(gain))
    }

    pub const fn is_muted(&self) -> bool {
        self.muted
    }

    pub const fn master_gain(&self) -> u8 {
        self.master_gain
    }

    pub const fn pending(&self) -> usize {
        self.len
    }

    pub const fn saturation_count(&self) -> u32 {
        self.saturation_count
    }

    pub const fn failure_count(&self) -> u32 {
        self.failure_count
    }

    pub const fn state(&self) -> AudioOutputState {
        self.state
    }

    pub(crate) fn set_state(&mut self, state: AudioOutputState) {
        self.state = state;
    }

    pub(crate) fn record_failure(&mut self) {
        self.failure_count = self.failure_count.saturating_add(1);
        self.state = AudioOutputState::Failed;
    }

    pub(crate) fn pop(&mut self) -> Option<AudioCommand> {
        if self.len == 0 || N == 0 {
            return None;
        }
        let command = self.queue[self.head].take();
        self.head = (self.head + 1) % N;
        self.len -= 1;
        command
    }

    fn enqueue(&mut self, command: AudioCommand) -> bool {
        if N == 0 {
            self.saturation_count = self.saturation_count.saturating_add(1);
            return false;
        }
        let state_kind = command.state_kind();
        if state_kind != 0 {
            for offset in (0..self.len).rev() {
                let index = (self.head + offset) % N;
                if self.queue[index].is_some_and(|queued| queued.state_kind() == state_kind) {
                    self.queue[index] = Some(command);
                    return true;
                }
            }
        }
        if self.len == N {
            if command.is_priority()
                && let Some(offset) = (0..self.len).find(|offset| {
                    let index = (self.head + *offset) % N;
                    self.queue[index].is_some_and(AudioCommand::is_disposable)
                })
            {
                self.remove(offset);
            } else if command.is_priority() {
                let _ = self.pop();
            } else {
                self.saturation_count = self.saturation_count.saturating_add(1);
                return false;
            }
            self.saturation_count = self.saturation_count.saturating_add(1);
        }
        let tail = (self.head + self.len) % N;
        self.queue[tail] = Some(command);
        self.len += 1;
        true
    }

    fn remove(&mut self, offset: usize) {
        for current in offset..self.len - 1 {
            let from = (self.head + current + 1) % N;
            let to = (self.head + current) % N;
            self.queue[to] = self.queue[from].take();
        }
        let tail = (self.head + self.len - 1) % N;
        self.queue[tail] = None;
        self.len -= 1;
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    const A: CueId = CueId::new(1);
    const B: CueId = CueId::new(2);

    #[test]
    fn queue_preserves_command_order() {
        let mut bus = AudioBus::<4>::new();
        assert!(bus.play(A));
        assert!(bus.play(B));
        assert_eq!(bus.pop(), Some(AudioCommand::Play { cue: A, gain: 255 }));
        assert_eq!(bus.pop(), Some(AudioCommand::Play { cue: B, gain: 255 }));
        assert_eq!(bus.pop(), None);
    }

    #[test]
    fn priority_command_replaces_disposable_play_when_full() {
        let mut bus = AudioBus::<2>::new();
        bus.play(A);
        bus.play(B);
        assert!(bus.set_muted(true));
        assert_eq!(bus.saturation_count(), 1);
        assert_eq!(bus.pop(), Some(AudioCommand::Play { cue: B, gain: 255 }));
        assert_eq!(bus.pop(), Some(AudioCommand::SetMuted(true)));
    }

    #[test]
    fn state_updates_coalesce_before_dispatch() {
        let mut bus = AudioBus::<2>::new();
        bus.set_master_gain(10);
        bus.set_master_gain(90);
        bus.set_muted(true);
        assert_eq!(bus.pending(), 2);
        assert_eq!(bus.pop(), Some(AudioCommand::SetMasterGain(90)));
        assert_eq!(bus.pop(), Some(AudioCommand::SetMuted(true)));
    }

    #[test]
    fn zero_capacity_bus_fails_without_panicking() {
        let mut bus = AudioBus::<0>::new();
        assert!(!bus.play(A));
        assert_eq!(bus.saturation_count(), 1);
    }

    #[test]
    fn direct_tones_share_the_bounded_command_queue() {
        let mut bus = AudioBus::<1>::new();
        let tone = AudioTone::new(69, super::super::Waveform::Sine, 120, 180);
        assert!(bus.tone(tone));
        assert!(!bus.tone(tone));
        assert_eq!(bus.saturation_count(), 1);
        assert_eq!(bus.pop(), Some(AudioCommand::Tone(tone)));
    }
}