use std::collections::HashMap;
use std::sync::atomic::{AtomicBool, AtomicU64, Ordering};
use std::sync::Arc;
use std::time::Instant;
use tracing::{debug, warn};
use crate::config::samples::{ReleaseBehavior, RetriggerBehavior};
use crate::playsync::CancelHandle;
static NEXT_VOICE_ID: AtomicU64 = AtomicU64::new(1);
pub(super) struct Voice {
id: u64,
sample_name: String,
release_group: Option<String>,
release_behavior: ReleaseBehavior,
start_time: Instant,
#[allow(dead_code)]
mixer_source_id: u64,
cancel_handle: CancelHandle,
cancel_at_sample: Arc<AtomicU64>,
is_finished: Arc<AtomicBool>,
}
impl Voice {
pub(super) fn new(
sample_name: String,
release_group: Option<String>,
release_behavior: ReleaseBehavior,
mixer_source_id: u64,
cancel_handle: CancelHandle,
cancel_at_sample: Arc<AtomicU64>,
is_finished: Arc<AtomicBool>,
) -> Self {
Self {
id: NEXT_VOICE_ID.fetch_add(1, Ordering::Relaxed),
sample_name,
release_group,
release_behavior,
start_time: Instant::now(),
mixer_source_id,
cancel_handle,
cancel_at_sample,
is_finished,
}
}
fn matches_release(&self, group: &str) -> bool {
self.release_group.as_deref() == Some(group)
}
fn cancel_handle(&self) -> CancelHandle {
self.cancel_handle.clone()
}
fn cancel_at_sample(&self) -> Arc<AtomicU64> {
self.cancel_at_sample.clone()
}
}
pub(super) struct VoiceManager {
voices: Vec<Voice>,
max_voices: u32,
sample_limits: HashMap<String, u32>,
}
impl VoiceManager {
pub(super) fn new(max_voices: u32) -> Self {
Self {
voices: Vec::new(),
max_voices,
sample_limits: HashMap::new(),
}
}
pub(super) fn set_sample_limit(&mut self, sample_name: &str, limit: u32) {
self.sample_limits.insert(sample_name.to_string(), limit);
}
fn sweep_finished(&mut self) {
self.voices
.retain(|v| !v.is_finished.load(Ordering::Relaxed));
}
pub(super) fn add_voice(
&mut self,
voice: Voice,
retrigger: RetriggerBehavior,
) -> Vec<Arc<AtomicU64>> {
self.sweep_finished();
let mut voices_to_stop = Vec::new();
match retrigger {
RetriggerBehavior::Cut => {
for v in self.voices.iter() {
if v.sample_name == voice.sample_name {
voices_to_stop.push(v.cancel_at_sample());
}
}
self.voices.retain(|v| v.sample_name != voice.sample_name);
}
RetriggerBehavior::Polyphonic => {
if let Some(&limit) = self.sample_limits.get(&voice.sample_name) {
let count = self
.voices
.iter()
.filter(|v| v.sample_name == voice.sample_name)
.count();
if count >= limit as usize {
if let Some(oldest) = self
.voices
.iter()
.filter(|v| v.sample_name == voice.sample_name)
.min_by_key(|v| v.start_time)
{
voices_to_stop.push(oldest.cancel_at_sample());
let oldest_id = oldest.id;
self.voices.retain(|v| v.id != oldest_id);
debug!(
sample = voice.sample_name,
limit, "Per-sample voice limit reached, stealing oldest"
);
}
}
}
}
}
if self.voices.len() >= self.max_voices as usize {
if let Some(oldest) = self.voices.iter().min_by_key(|v| v.start_time) {
voices_to_stop.push(oldest.cancel_at_sample());
let oldest_id = oldest.id;
self.voices.retain(|v| v.id != oldest_id);
warn!(
max_voices = self.max_voices,
"Global voice limit reached, stealing oldest"
);
}
}
self.voices.push(voice);
voices_to_stop
}
pub(super) fn release(&mut self, group: &str) -> Vec<CancelHandle> {
let mut to_stop = Vec::new();
for v in self.voices.iter() {
if v.matches_release(group) {
match v.release_behavior {
ReleaseBehavior::PlayToCompletion => {
}
ReleaseBehavior::Stop | ReleaseBehavior::Fade => {
to_stop.push(v.cancel_handle());
}
}
}
}
self.voices.retain(|v| {
!v.matches_release(group) || v.release_behavior == ReleaseBehavior::PlayToCompletion
});
to_stop
}
pub(super) fn active_count(&self) -> usize {
self.voices.len()
}
pub(super) fn clear(&mut self) -> Vec<CancelHandle> {
let handles: Vec<CancelHandle> = self.voices.iter().map(|v| v.cancel_handle()).collect();
self.voices.clear();
handles
}
}
impl std::fmt::Debug for VoiceManager {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("VoiceManager")
.field("active_voices", &self.voices.len())
.field("max_voices", &self.max_voices)
.finish()
}
}
#[cfg(test)]
mod tests {
use super::*;
fn make_voice(sample: &str, release_group: Option<&str>, id: u64) -> Voice {
make_voice_with_behavior(sample, release_group, id, ReleaseBehavior::Stop)
}
fn make_voice_with_behavior(
sample: &str,
release_group: Option<&str>,
id: u64,
release_behavior: ReleaseBehavior,
) -> Voice {
Voice::new(
sample.to_string(),
release_group.map(|s| s.to_string()),
release_behavior,
id,
CancelHandle::new(),
Arc::new(AtomicU64::new(0)),
Arc::new(AtomicBool::new(false)),
)
}
#[test]
fn test_voice_release_matching() {
let voice = make_voice("test", Some("midi:10:60"), 1);
assert!(voice.matches_release("midi:10:60"));
assert!(!voice.matches_release("midi:10:61"));
assert!(!voice.matches_release("midi:11:60"));
let voice2 = make_voice("test", None, 2);
assert!(!voice2.matches_release("midi:10:60"));
assert!(!voice2.matches_release("anything"));
}
#[test]
fn test_voice_manager_cut_retrigger() {
let mut manager = VoiceManager::new(32);
let voice1 = make_voice("kick", Some("midi:10:36"), 1);
let stopped = manager.add_voice(voice1, RetriggerBehavior::Cut);
assert!(stopped.is_empty());
assert_eq!(manager.active_count(), 1);
let voice2 = make_voice("kick", Some("midi:10:36"), 2);
let stopped = manager.add_voice(voice2, RetriggerBehavior::Cut);
assert_eq!(stopped.len(), 1); assert_eq!(manager.active_count(), 1);
}
#[test]
fn test_voice_manager_polyphonic() {
let mut manager = VoiceManager::new(32);
manager.set_sample_limit("snare", 4);
for i in 1..=4 {
let voice = make_voice("snare", Some("midi:10:38"), i);
let stopped = manager.add_voice(voice, RetriggerBehavior::Polyphonic);
assert!(stopped.is_empty());
}
assert_eq!(manager.active_count(), 4);
let voice5 = make_voice("snare", Some("midi:10:38"), 5);
let stopped = manager.add_voice(voice5, RetriggerBehavior::Polyphonic);
assert_eq!(stopped.len(), 1); assert_eq!(manager.active_count(), 4);
}
#[test]
fn test_voice_manager_global_limit() {
let mut manager = VoiceManager::new(3);
for i in 1..=3 {
let voice = make_voice(&format!("sample{}", i), Some("midi:10:36"), i);
let stopped = manager.add_voice(voice, RetriggerBehavior::Polyphonic);
assert!(stopped.is_empty());
}
let voice4 = make_voice("sample4", Some("midi:10:36"), 4);
let stopped = manager.add_voice(voice4, RetriggerBehavior::Polyphonic);
assert_eq!(stopped.len(), 1);
assert_eq!(manager.active_count(), 3);
}
#[test]
fn test_release_stop() {
let mut manager = VoiceManager::new(32);
let voice1 = make_voice("kick", Some("midi:10:36"), 1);
let voice2 = make_voice("snare", Some("midi:10:38"), 2);
manager.add_voice(voice1, RetriggerBehavior::Polyphonic);
manager.add_voice(voice2, RetriggerBehavior::Polyphonic);
let stopped = manager.release("midi:10:36");
assert_eq!(stopped.len(), 1);
assert_eq!(manager.active_count(), 1);
}
#[test]
fn test_release_play_to_completion() {
let mut manager = VoiceManager::new(32);
let voice = make_voice_with_behavior(
"kick",
Some("midi:10:36"),
1,
ReleaseBehavior::PlayToCompletion,
);
manager.add_voice(voice, RetriggerBehavior::Polyphonic);
let stopped = manager.release("midi:10:36");
assert!(stopped.is_empty());
assert_eq!(manager.active_count(), 1);
}
#[test]
fn test_release_custom_group() {
let mut manager = VoiceManager::new(32);
let voice1 = make_voice("cymbal", Some("cymbal"), 1);
let voice2 = make_voice("cymbal", Some("cymbal"), 2);
let voice3 = make_voice("kick", Some("kick"), 3);
manager.add_voice(voice1, RetriggerBehavior::Polyphonic);
manager.add_voice(voice2, RetriggerBehavior::Polyphonic);
manager.add_voice(voice3, RetriggerBehavior::Polyphonic);
let stopped = manager.release("cymbal");
assert_eq!(stopped.len(), 2);
assert_eq!(manager.active_count(), 1);
}
#[test]
fn test_voice_manager_clear() {
let mut manager = VoiceManager::new(32);
let voice1 = make_voice("kick", Some("midi:10:36"), 1);
let voice2 = make_voice("snare", Some("midi:10:38"), 2);
manager.add_voice(voice1, RetriggerBehavior::Polyphonic);
manager.add_voice(voice2, RetriggerBehavior::Polyphonic);
assert_eq!(manager.active_count(), 2);
let handles = manager.clear();
assert_eq!(handles.len(), 2);
assert_eq!(manager.active_count(), 0);
}
#[test]
fn test_voice_manager_debug_format() {
let manager = VoiceManager::new(16);
let debug_str = format!("{:?}", manager);
assert!(debug_str.contains("VoiceManager"));
assert!(debug_str.contains("active_voices"));
assert!(debug_str.contains("max_voices"));
assert!(debug_str.contains("16"));
}
#[test]
fn test_voice_manager_sweep_finished() {
let mut manager = VoiceManager::new(32);
let is_finished = Arc::new(AtomicBool::new(false));
let voice = Voice::new(
"test".to_string(),
None,
ReleaseBehavior::Stop,
1,
CancelHandle::new(),
Arc::new(AtomicU64::new(0)),
is_finished.clone(),
);
manager.add_voice(voice, RetriggerBehavior::Polyphonic);
assert_eq!(manager.active_count(), 1);
is_finished.store(true, Ordering::Relaxed);
let voice2 = make_voice("other", None, 2);
manager.add_voice(voice2, RetriggerBehavior::Polyphonic);
assert_eq!(manager.active_count(), 1);
}
#[test]
fn test_release_nonexistent_group() {
let mut manager = VoiceManager::new(32);
let voice = make_voice("kick", Some("midi:10:36"), 1);
manager.add_voice(voice, RetriggerBehavior::Polyphonic);
let stopped = manager.release("nonexistent");
assert!(stopped.is_empty());
assert_eq!(manager.active_count(), 1);
}
#[test]
fn test_voice_without_release_group() {
let mut manager = VoiceManager::new(32);
let voice = make_voice("ambient", None, 1);
manager.add_voice(voice, RetriggerBehavior::Polyphonic);
let stopped = manager.release("midi:10:36");
assert!(stopped.is_empty());
assert_eq!(manager.active_count(), 1);
}
#[test]
fn test_release_fade_behavior_stops_voice() {
let mut manager = VoiceManager::new(32);
let voice = make_voice_with_behavior("pad", Some("pad"), 1, ReleaseBehavior::Fade);
manager.add_voice(voice, RetriggerBehavior::Polyphonic);
let stopped = manager.release("pad");
assert_eq!(stopped.len(), 1);
assert_eq!(manager.active_count(), 0);
}
#[test]
fn test_set_sample_limit_directly() {
let mut manager = VoiceManager::new(32);
manager.set_sample_limit("snare", 2);
let voice1 = make_voice("snare", None, 1);
let voice2 = make_voice("snare", None, 2);
assert!(manager
.add_voice(voice1, RetriggerBehavior::Polyphonic)
.is_empty());
assert!(manager
.add_voice(voice2, RetriggerBehavior::Polyphonic)
.is_empty());
let voice3 = make_voice("snare", None, 3);
let stopped = manager.add_voice(voice3, RetriggerBehavior::Polyphonic);
assert_eq!(stopped.len(), 1);
assert_eq!(manager.active_count(), 2);
}
#[test]
fn test_polyphonic_no_sample_limit_respects_global() {
let mut manager = VoiceManager::new(2);
let voice1 = make_voice("a", None, 1);
let voice2 = make_voice("b", None, 2);
assert!(manager
.add_voice(voice1, RetriggerBehavior::Polyphonic)
.is_empty());
assert!(manager
.add_voice(voice2, RetriggerBehavior::Polyphonic)
.is_empty());
let voice3 = make_voice("c", None, 3);
let stopped = manager.add_voice(voice3, RetriggerBehavior::Polyphonic);
assert_eq!(stopped.len(), 1);
assert_eq!(manager.active_count(), 2);
}
#[test]
fn test_cut_retrigger_different_sample_no_cut() {
let mut manager = VoiceManager::new(32);
let voice1 = make_voice("kick", None, 1);
manager.add_voice(voice1, RetriggerBehavior::Cut);
let voice2 = make_voice("snare", None, 2);
let stopped = manager.add_voice(voice2, RetriggerBehavior::Cut);
assert!(stopped.is_empty());
assert_eq!(manager.active_count(), 2);
}
#[test]
fn test_sweep_finished_multiple() {
let mut manager = VoiceManager::new(32);
let finished1 = Arc::new(AtomicBool::new(false));
let finished2 = Arc::new(AtomicBool::new(false));
let voice1 = Voice::new(
"a".to_string(),
None,
ReleaseBehavior::Stop,
1,
CancelHandle::new(),
Arc::new(AtomicU64::new(0)),
finished1.clone(),
);
let voice2 = Voice::new(
"b".to_string(),
None,
ReleaseBehavior::Stop,
2,
CancelHandle::new(),
Arc::new(AtomicU64::new(0)),
finished2.clone(),
);
let voice3 = make_voice("c", None, 3);
manager.add_voice(voice1, RetriggerBehavior::Polyphonic);
manager.add_voice(voice2, RetriggerBehavior::Polyphonic);
manager.add_voice(voice3, RetriggerBehavior::Polyphonic);
assert_eq!(manager.active_count(), 3);
finished1.store(true, Ordering::Relaxed);
finished2.store(true, Ordering::Relaxed);
let voice4 = make_voice("d", None, 4);
manager.add_voice(voice4, RetriggerBehavior::Polyphonic);
assert_eq!(manager.active_count(), 2);
}
#[test]
fn test_release_mixed_behaviors_in_same_group() {
let mut manager = VoiceManager::new(32);
let voice1 =
make_voice_with_behavior("ride", Some("cymbal"), 1, ReleaseBehavior::PlayToCompletion);
let voice2 = make_voice_with_behavior("hi-hat", Some("cymbal"), 2, ReleaseBehavior::Stop);
manager.add_voice(voice1, RetriggerBehavior::Polyphonic);
manager.add_voice(voice2, RetriggerBehavior::Polyphonic);
let stopped = manager.release("cymbal");
assert_eq!(stopped.len(), 1);
assert_eq!(manager.active_count(), 1); }
}