Skip to main content

audiobox/mixer/
virtualization.rs

1//! Opt-in bounded physical-voice selection; legacy admission stays the default.
2use super::{Mixer, TAILS};
3
4/// Fixed global output-frame cadence for physical selection, plus command frames.
5pub const VIRTUAL_VOICE_QUANTUM: u64 = 32;
6/// Maximum retained logical voices, excluding the existing bounded fade-out slots.
7pub const MAX_LOGICAL_VOICES: usize = 4096;
8
9/// Opt-in logical voice pool. `MixerConfig::max_voices` remains the physical budget.
10#[derive(Clone, Copy, Debug, PartialEq)]
11pub struct VirtualVoices {
12    /// Total logical live voices, max_voices..=4096. Includes virtual/paused voices.
13    pub capacity: usize,
14    /// Estimated peak-amplitude floor in linear units, 0..=1. Memory uses its
15    /// prepared sample peak; streams use a unit reference. Combined with a
16    /// gain/fade/distance/pause bound through the quantum, not RMS/perceived
17    /// loudness or bus gain (pre-fader sends may bypass the latter).
18    pub threshold: f32,
19}
20impl Default for VirtualVoices {
21    fn default() -> Self {
22        Self {
23            capacity: 256,
24            threshold: 0.0001,
25        }
26    }
27}
28
29struct Candidate {
30    slot: usize,
31    priority: u8,
32    level: f64,
33    order: u64,
34}
35pub(super) struct Scheduler {
36    pub settings: VirtualVoices,
37    pub dirty: bool,
38    quantum: Option<u64>,
39    candidates: Vec<Candidate>,
40}
41impl Scheduler {
42    pub fn new(settings: VirtualVoices, slots: usize) -> Self {
43        Self {
44            settings,
45            dirty: true,
46            quantum: None,
47            candidates: Vec::with_capacity(slots),
48        }
49    }
50}
51impl Mixer {
52    /// Latest snapshot of selected, unpaused physical voices, including audible tails.
53    pub fn physical_voices(&self) -> usize {
54        if self.buses[0].paused {
55            return 0;
56        }
57        self.voices
58            .iter()
59            .flatten()
60            .filter(|v| !v.virtualized && !v.paused() && !self.buses[v.bus].paused)
61            .count()
62    }
63    /// Latest snapshot of advancing virtual voices; paused voices are separate.
64    pub fn virtual_voice_count(&self) -> usize {
65        if self.buses[0].paused {
66            return 0;
67        }
68        self.voices
69            .iter()
70            .flatten()
71            .filter(|v| v.virtualized && !v.paused() && !self.buses[v.bus].paused)
72            .count()
73    }
74    pub(super) fn select_physical(&mut self, frame: u64) {
75        let quantum = frame / VIRTUAL_VOICE_QUANTUM;
76        let Some(scheduler) = &self.virtualization else {
77            return;
78        };
79        if !scheduler.dirty && scheduler.quantum == Some(quantum) {
80            return;
81        }
82        // Identical eligibility at a fixed decision frame regardless of caller
83        // chunking: finish a source that reached EOF at the preceding sample.
84        for voice in self.voices.iter_mut().flatten() {
85            voice.settle();
86        }
87        self.retire_done();
88        let scheduler = self.virtualization.as_mut().unwrap();
89        scheduler.dirty = false;
90        scheduler.quantum = Some(quantum);
91        scheduler.candidates.clear();
92        let end = (frame - frame % VIRTUAL_VOICE_QUANTUM) + VIRTUAL_VOICE_QUANTUM - 1;
93        for (slot, voice) in self.voices.iter_mut().enumerate() {
94            let Some(voice) = voice else { continue };
95            if !voice.live() {
96                continue;
97            }
98            voice.virtualized = true;
99            if self.buses[0].paused || self.buses[voice.bus].paused || voice.paused() {
100                continue;
101            }
102            let level = voice.source_peak * voice.level_bound(frame, end);
103            if level > scheduler.settings.threshold as f64 {
104                scheduler.candidates.push(Candidate {
105                    slot,
106                    priority: voice.priority,
107                    level,
108                    order: voice.order,
109                });
110            }
111        }
112        scheduler.candidates.sort_unstable_by(|a, b| {
113            b.priority
114                .cmp(&a.priority)
115                .then(b.level.total_cmp(&a.level))
116                .then(a.order.cmp(&b.order))
117        });
118        let physical = scheduler.candidates.len().min(self.max_voices);
119        for candidate in &scheduler.candidates[..physical] {
120            self.voices[candidate.slot].as_mut().unwrap().virtualized = false;
121        }
122        // Preserve already physical stop fades while bounding their additional
123        // cost to the legacy physical budget + TAILS. A culled tail never returns.
124        scheduler.candidates.clear();
125        for (slot, voice) in self.voices.iter().enumerate() {
126            if let Some(voice) = voice.as_deref().filter(|v| !v.live() && !v.virtualized) {
127                scheduler.candidates.push(Candidate {
128                    slot,
129                    priority: voice.priority,
130                    level: 0.0,
131                    order: voice.order,
132                });
133            }
134        }
135        scheduler
136            .candidates
137            .sort_unstable_by_key(|v| std::cmp::Reverse(v.order));
138        for candidate in scheduler
139            .candidates
140            .iter()
141            .skip(self.max_voices + TAILS - physical)
142        {
143            self.voices[candidate.slot].as_mut().unwrap().virtualized = true;
144        }
145    }
146}
147
148pub(super) fn placement(
149    spatial: &super::SpatialConfig,
150    listener: &super::Listener,
151    emitter: &super::Emitter,
152    layout: super::OutputLayout,
153    cull: bool,
154) -> super::Placement {
155    let mut p = spatial.place_validated(listener, emitter, layout);
156    if cull {
157        let squared = (0..3)
158            .map(|i| {
159                let d = emitter.position[i] as f64 - listener.position[i] as f64;
160                d * d
161            })
162            .sum::<f64>();
163        let max = emitter.attenuation.max_distance as f64;
164        if squared >= max * max {
165            p.distance_gain = 0.0;
166        }
167    }
168    p
169}