use crate::voice::Voice;
use alloc::vec::Vec;
use core::num::NonZeroU32;
#[derive(Copy, Clone, PartialEq, Eq, Hash, Debug)]
pub struct VoiceId(NonZeroU32);
impl VoiceId {
pub const MAX_SLOTS: usize = 0xFFFF;
#[inline(always)]
fn new(slot: usize, generation: u32) -> Self {
debug_assert!(slot < Self::MAX_SLOTS, "slot index overflow");
let packed = ((generation & 0xFFFF) << 16) | ((slot as u32 + 1) & 0xFFFF);
Self(NonZeroU32::new(packed).expect("encoded VoiceId is non-zero by construction"))
}
#[inline(always)]
fn slot(self) -> usize {
((self.0.get() & 0xFFFF) - 1) as usize
}
#[inline(always)]
fn generation(self) -> u32 {
(self.0.get() >> 16) & 0xFFFF
}
}
#[allow(clippy::large_enum_variant)]
enum SlotState<'a> {
Vacant { next_free: usize },
Occupied(Voice<'a>),
}
struct Slot<'a> {
generation: u32,
state: SlotState<'a>,
}
pub(crate) struct VoicePool<'a> {
slots: Vec<Slot<'a>>,
free_head: usize,
allocated: usize,
}
const NIL: usize = usize::MAX;
impl<'a> VoicePool<'a> {
pub fn new(capacity: usize) -> Self {
assert!(capacity > 0, "VoicePool capacity must be positive");
assert!(
capacity < VoiceId::MAX_SLOTS,
"VoicePool capacity {} exceeds VoiceId::MAX_SLOTS ({})",
capacity,
VoiceId::MAX_SLOTS
);
let mut slots = Vec::with_capacity(capacity);
for i in 0..capacity {
let next_free = if i + 1 < capacity { i + 1 } else { NIL };
slots.push(Slot {
generation: 1,
state: SlotState::Vacant { next_free },
});
}
Self {
slots,
free_head: 0,
allocated: 0,
}
}
#[cfg(test)]
pub fn capacity(&self) -> usize {
self.slots.len()
}
#[cfg(test)]
pub fn allocated_count(&self) -> usize {
self.allocated
}
#[cfg(test)]
pub fn is_full(&self) -> bool {
self.free_head == NIL
}
pub fn allocate_live(&mut self, voice: Voice<'a>) -> VoiceId {
if self.free_head == NIL {
let victim_slot = self
.find_eviction_victim()
.expect("allocate_live: pool full ⇒ at least one slot is Occupied");
let gen = self.slots[victim_slot].generation;
self.release(VoiceId::new(victim_slot, gen));
}
self.place_in_free_slot(voice)
}
pub fn allocate_ghost(&mut self, voice: Voice<'a>) -> Option<VoiceId> {
if self.free_head == NIL {
let victim_slot = self.find_evictable_victim()?;
let gen = self.slots[victim_slot].generation;
self.release(VoiceId::new(victim_slot, gen));
}
Some(self.place_in_free_slot(voice))
}
fn place_in_free_slot(&mut self, voice: Voice<'a>) -> VoiceId {
let slot_idx = self.free_head;
let slot = &mut self.slots[slot_idx];
let next = match slot.state {
SlotState::Vacant { next_free } => next_free,
SlotState::Occupied(_) => unreachable!(
"VoicePool free-list invariant violated: free_head ({}) points to an Occupied slot. \
This indicates a bug in release() or a double-free; the free-list and SlotState \
must agree by construction.",
slot_idx
),
};
slot.state = SlotState::Occupied(voice);
self.free_head = next;
self.allocated += 1;
VoiceId::new(slot_idx, slot.generation)
}
fn find_evictable_victim(&self) -> Option<usize> {
let mut best: Option<(usize, i32)> = None;
for (i, slot) in self.slots.iter().enumerate() {
let SlotState::Occupied(v) = &slot.state else {
continue;
};
if v.instr.sustained {
continue;
}
let score = self.eviction_score(v);
best = match best {
None => Some((i, score)),
Some((_, bs)) if score < bs => Some((i, score)),
Some(p) => Some(p),
};
}
best.map(|(i, _)| i)
}
fn find_eviction_victim(&self) -> Option<usize> {
if let Some(slot) = self.find_evictable_victim() {
return Some(slot);
}
let mut best: Option<(usize, i32)> = None;
for (i, slot) in self.slots.iter().enumerate() {
let SlotState::Occupied(v) = &slot.state else {
continue;
};
let score = self.eviction_score(v);
best = match best {
None => Some((i, score)),
Some((_, bs)) if score < bs => Some((i, score)),
Some(p) => Some(p),
};
}
best.map(|(i, _)| i)
}
fn eviction_score(&self, v: &Voice<'a>) -> i32 {
let v_q15 = v.instr.volume.as_q15_i32();
let mut score: i32 = if v.instr.sustained {
v_q15 << 10
} else {
let f_q15 = v.instr.volume_fadeout.as_q15_i32();
(v_q15 * f_q15) >> 15
};
if v.instr
.state_sample
.as_ref()
.is_some_and(|s| s.is_looping())
{
score >>= 1;
}
score
}
pub fn release(&mut self, id: VoiceId) {
let slot_idx = id.slot();
let Some(slot) = self.slots.get_mut(slot_idx) else {
return;
};
if slot.generation != id.generation() {
return;
}
if matches!(slot.state, SlotState::Vacant { .. }) {
return;
}
slot.generation = slot.generation.wrapping_add(1);
if slot.generation == 0 {
slot.generation = 1;
}
slot.state = SlotState::Vacant {
next_free: self.free_head,
};
self.free_head = slot_idx;
self.allocated -= 1;
}
#[inline(always)]
pub fn get(&self, id: VoiceId) -> Option<&Voice<'a>> {
let slot = self.slots.get(id.slot())?;
if slot.generation != id.generation() {
return None;
}
match &slot.state {
SlotState::Occupied(v) => Some(v),
SlotState::Vacant { .. } => None,
}
}
#[inline(always)]
pub fn get_mut(&mut self, id: VoiceId) -> Option<&mut Voice<'a>> {
let slot = self.slots.get_mut(id.slot())?;
if slot.generation != id.generation() {
return None;
}
match &mut slot.state {
SlotState::Occupied(v) => Some(v),
SlotState::Vacant { .. } => None,
}
}
#[cfg(test)]
pub fn iter(&self) -> impl Iterator<Item = (VoiceId, &Voice<'a>)> {
self.slots.iter().enumerate().filter_map(|(i, slot)| {
if let SlotState::Occupied(v) = &slot.state {
Some((VoiceId::new(i, slot.generation), v))
} else {
None
}
})
}
#[cfg(test)]
pub fn iter_mut(&mut self) -> impl Iterator<Item = (VoiceId, &mut Voice<'a>)> {
self.slots.iter_mut().enumerate().filter_map(|(i, slot)| {
let gen = slot.generation;
if let SlotState::Occupied(v) = &mut slot.state {
Some((VoiceId::new(i, gen), v))
} else {
None
}
})
}
#[cfg(test)]
pub fn retain<F>(&mut self, mut f: F)
where
F: FnMut(VoiceId, &mut Voice<'a>) -> bool,
{
let mut to_drop: Vec<VoiceId> = Vec::new();
for (id, voice) in self.iter_mut() {
if !f(id, voice) {
to_drop.push(id);
}
}
for id in to_drop {
self.release(id);
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::state_instr_default::StateInstrDefault;
use alloc::boxed::Box;
use alloc::vec::Vec;
use xmrs::fixed::units::{ChannelVolume, EnvValue, Panning, Volume};
use xmrs::prelude::*;
fn dummy_voice() -> Voice<'static> {
let instr: &'static InstrDefault = Box::leak(Box::new(InstrDefault::default()));
let ph = PeriodHelper::new(FrequencyType::LinearFrequencies, false);
let state = StateInstrDefault::new(instr, 0, ph, SampleRate::from_hz(44100), true);
Voice::new_ghost(
state,
0,
Volume::FULL,
ChannelVolume::FULL,
Panning::CENTER,
None,
None,
Period::ZERO,
)
}
#[test]
fn empty_pool_reports_correct_state() {
let pool: VoicePool<'_> = VoicePool::new(4);
assert_eq!(pool.capacity(), 4);
assert_eq!(pool.allocated_count(), 0);
assert!(!pool.is_full());
}
#[test]
#[should_panic]
fn zero_capacity_panics() {
let _pool: VoicePool<'_> = VoicePool::new(0);
}
#[test]
fn allocate_then_release_returns_to_free_list() {
let mut pool = VoicePool::new(2);
let id1 = pool.allocate_live(dummy_voice());
assert_eq!(pool.allocated_count(), 1);
pool.release(id1);
assert_eq!(pool.allocated_count(), 0);
assert!(!pool.is_full());
let id2 = pool.allocate_live(dummy_voice());
assert_eq!(pool.allocated_count(), 1);
assert_eq!(id1.slot(), id2.slot());
assert_ne!(id1, id2);
}
#[test]
fn full_pool_evicts_lowest_volume() {
let make_voice = |vol: Volume| -> Voice<'static> {
let instr: &'static InstrDefault = Box::leak(Box::new(InstrDefault::default()));
let ph = PeriodHelper::new(FrequencyType::LinearFrequencies, false);
let mut state = StateInstrDefault::new(instr, 0, ph, SampleRate::from_hz(44100), true);
state.volume = vol;
Voice::new_ghost(
state,
0,
Volume::FULL,
ChannelVolume::FULL,
Panning::CENTER,
None,
None,
Period::ZERO,
)
};
let mut pool = VoicePool::new(2);
let _loud = pool.allocate_live(make_voice(Volume::FULL));
let quiet = pool.allocate_live(make_voice(Volume::from_ratio(5, 100)));
assert!(pool.is_full());
let _new = pool.allocate_live(make_voice(Volume::from_ratio(50, 100)));
assert_eq!(pool.allocated_count(), 2);
assert!(
pool.get(quiet).is_none(),
"quietest voice should have been evicted"
);
}
#[test]
fn stale_handle_after_release_returns_none() {
let mut pool = VoicePool::new(2);
let id = pool.allocate_live(dummy_voice());
pool.release(id);
assert!(pool.get(id).is_none());
assert!(pool.get_mut(id).is_none());
}
#[test]
fn stale_handle_after_slot_reuse_returns_none() {
let mut pool = VoicePool::new(1);
let id1 = pool.allocate_live(dummy_voice());
pool.release(id1);
let _id2 = pool.allocate_live(dummy_voice());
assert!(pool.get(id1).is_none());
}
#[test]
fn double_release_is_a_noop() {
let mut pool = VoicePool::new(1);
let id = pool.allocate_live(dummy_voice());
pool.release(id);
pool.release(id); assert_eq!(pool.allocated_count(), 0);
let id3 = pool.allocate_live(dummy_voice());
assert_eq!(pool.allocated_count(), 1);
assert_ne!(id, id3);
}
#[test]
fn iter_visits_every_live_voice() {
let mut pool = VoicePool::new(4);
let id_a = pool.allocate_live(dummy_voice());
let id_b = pool.allocate_live(dummy_voice());
let _id_c = pool.allocate_live(dummy_voice());
pool.release(id_b);
let live: Vec<VoiceId> = pool.iter().map(|(id, _)| id).collect();
assert_eq!(live.len(), 2);
assert!(live.contains(&id_a));
assert!(!live.contains(&id_b));
}
#[test]
fn retain_drops_voices_when_predicate_returns_false() {
let mut pool = VoicePool::new(4);
let _id1 = pool.allocate_live(dummy_voice());
let _id2 = pool.allocate_live(dummy_voice());
let _id3 = pool.allocate_live(dummy_voice());
assert_eq!(pool.allocated_count(), 3);
let mut dropped = false;
pool.retain(|_id, _voice| {
if !dropped {
dropped = true;
false
} else {
true
}
});
assert_eq!(pool.allocated_count(), 2);
}
#[test]
fn voice_id_packing_round_trips() {
for slot in [0usize, 1, 5, 100, 0xFFFE] {
for generation in [1u32, 2, 100, 0xFFFE, 0xFFFF] {
let id = VoiceId::new(slot, generation);
assert_eq!(id.slot(), slot);
assert_eq!(id.generation(), generation);
}
}
}
fn sustained_voice(vol: Volume) -> Voice<'static> {
let instr: &'static InstrDefault = Box::leak(Box::new(InstrDefault::default()));
let ph = PeriodHelper::new(FrequencyType::LinearFrequencies, false);
let mut state = StateInstrDefault::new(instr, 0, ph, SampleRate::from_hz(44100), true);
state.volume = vol;
Voice::new_ghost(
state,
0,
Volume::FULL,
ChannelVolume::FULL,
Panning::CENTER,
None,
None,
Period::ZERO,
)
}
fn fading_voice(vol: Volume) -> Voice<'static> {
let mut v = sustained_voice(vol);
v.instr.sustained = false;
v
}
#[test]
fn allocate_ghost_refuses_when_all_voices_sustained() {
let mut pool = VoicePool::new(2);
let _a = pool.allocate_live(sustained_voice(Volume::FULL));
let _b = pool.allocate_live(sustained_voice(Volume::FULL));
assert!(pool.is_full());
let result = pool.allocate_ghost(sustained_voice(Volume::FULL));
assert!(result.is_none(), "ghost allocation must be refused");
assert_eq!(pool.allocated_count(), 2);
}
#[test]
fn allocate_ghost_evicts_when_a_fading_voice_exists() {
let mut pool = VoicePool::new(2);
let sustained = pool.allocate_live(sustained_voice(Volume::FULL));
let fading = pool.allocate_live(fading_voice(Volume::from_ratio(50, 100)));
let new_id = pool
.allocate_ghost(sustained_voice(Volume::FULL))
.expect("an evictable voice was available");
assert!(pool.get(fading).is_none(), "fading voice should be evicted");
assert!(
pool.get(sustained).is_some(),
"sustained voice must survive"
);
assert!(pool.get(new_id).is_some(), "new ghost must be installed");
}
#[test]
fn allocate_live_evicts_even_sustained_voices() {
let mut pool = VoicePool::new(2);
let a = pool.allocate_live(sustained_voice(Volume::from_ratio(50, 100)));
let b = pool.allocate_live(sustained_voice(Volume::FULL));
assert!(pool.is_full());
let new_id = pool.allocate_live(sustained_voice(Volume::FULL));
assert!(pool.get(a).is_none(), "lower-vol sustained voice evicted");
assert!(pool.get(b).is_some(), "louder sustained voice survives");
assert!(pool.get(new_id).is_some(), "new live voice installed");
assert_eq!(pool.allocated_count(), 2);
}
#[test]
fn sustained_voice_at_zero_envelope_is_not_evictable() {
let mut pool = VoicePool::new(2);
let full = pool.allocate_live(sustained_voice(Volume::FULL));
let mut silent = sustained_voice(Volume::FULL);
silent.instr.envelope_volume.value = EnvValue::default();
let silent_id = pool.allocate_live(silent);
assert!(pool.is_full());
let result = pool.allocate_ghost(sustained_voice(Volume::FULL));
assert!(
result.is_none(),
"ghost must be refused — both sustained voices are protected, \
regardless of envelope value"
);
assert!(pool.get(full).is_some(), "full-envelope voice survives");
assert!(
pool.get(silent_id).is_some(),
"zero-envelope sustained voice must not be evicted"
);
}
}