Skip to main content

audiobox/mixer/
status.rs

1//! Voice states published by the audio side for the game to read without blocking.
2//!
3//! Each voice slot has a sequence counter (a seqlock): the mixer makes it odd, writes
4//! the slot, and makes it even again; a reader retries if the counter was odd or
5//! changed while it read. Only atomics are used, so neither side ever waits on the
6//! other for long, and nothing is allocated.
7use std::sync::atomic::{AtomicU8, AtomicU64, Ordering, fence};
8
9use super::VoiceId;
10
11/// What a voice is doing, as of the mixer's last render.
12#[derive(Clone, Copy, Debug, Eq, PartialEq)]
13pub enum VoiceState {
14    Playing,
15    /// Paused by itself or by its bus; its position does not move.
16    Paused,
17    /// Fading out after a stop or a steal.
18    Stopping,
19    Finished,
20    /// Retained and advancing without interpolation/mixing; can become physical.
21    Virtual,
22}
23
24/// A voice's state and its position in its sound, in source frames. A looping voice's
25/// position wraps back with the loop.
26#[derive(Clone, Copy, Debug, Eq, PartialEq)]
27pub struct VoiceStatus {
28    pub state: VoiceState,
29    pub position: u64,
30}
31
32const EMPTY: u64 = u64::MAX;
33
34struct Slot {
35    sequence: AtomicU64,
36    id: AtomicU64,
37    state: AtomicU8,
38    position: AtomicU64,
39}
40
41pub(super) struct Table {
42    slots: Box<[Slot]>,
43}
44
45impl Table {
46    pub fn new(slots: usize) -> Self {
47        Self {
48            slots: (0..slots)
49                .map(|_| Slot {
50                    sequence: AtomicU64::new(0),
51                    id: AtomicU64::new(EMPTY),
52                    state: AtomicU8::new(0),
53                    position: AtomicU64::new(0),
54                })
55                .collect(),
56        }
57    }
58    /// Writes a slot. Only the mixer writes.
59    pub fn publish(&self, slot: usize, id: VoiceId, status: VoiceStatus) {
60        let s = &self.slots[slot];
61        let sequence = s.sequence.load(Ordering::Relaxed);
62        s.sequence
63            .store(sequence.wrapping_add(1), Ordering::Relaxed);
64        fence(Ordering::Release);
65        s.id.store(id.0, Ordering::Relaxed);
66        s.state.store(status.state as u8, Ordering::Relaxed);
67        s.position.store(status.position, Ordering::Relaxed);
68        s.sequence
69            .store(sequence.wrapping_add(2), Ordering::Release);
70    }
71    /// The last status published for `id`, if a slot still holds it.
72    pub fn read(&self, id: VoiceId) -> Option<VoiceStatus> {
73        for s in self.slots.iter() {
74            loop {
75                let before = s.sequence.load(Ordering::Acquire);
76                if before % 2 == 1 {
77                    std::hint::spin_loop();
78                    continue;
79                }
80                let slot_id = s.id.load(Ordering::Relaxed);
81                let state = s.state.load(Ordering::Relaxed);
82                let position = s.position.load(Ordering::Relaxed);
83                fence(Ordering::Acquire);
84                if s.sequence.load(Ordering::Relaxed) != before {
85                    continue;
86                }
87                if slot_id == id.0 {
88                    let state = match state {
89                        0 => VoiceState::Playing,
90                        1 => VoiceState::Paused,
91                        2 => VoiceState::Stopping,
92                        4 => VoiceState::Virtual,
93                        _ => VoiceState::Finished,
94                    };
95                    return Some(VoiceStatus { state, position });
96                }
97                break;
98            }
99        }
100        None
101    }
102}