use super::*;
const OUTPUT_TRIM: f32 = 0.42;
pub(crate) struct ClapEngine {
pub(crate) sample_rate: f32,
pub(crate) trigger: GridTrigger,
pub(crate) voices: Vec<ClapVoice>,
pub(crate) rng: StdRng,
}
impl ClapEngine {
pub(crate) fn new(sample_rate: f32) -> Self {
Self {
sample_rate,
trigger: GridTrigger::new(),
voices: Vec::with_capacity(4),
rng: StdRng::from_entropy(),
}
}
pub(crate) fn next(&mut self, c: &ClapControls, timing: TimingContext) -> (f32, f32) {
if self
.trigger
.pop_swung(timing, c.interval_beats, c.offset_beats, c.swing)
{
self.voices
.push(ClapVoice::new(c, self.sample_rate, &mut self.rng));
}
let rng = &mut self.rng;
let dry = mix_and_retain_mono(&mut self.voices, |v| v.next(rng), ClapVoice::is_done);
(dry, dry)
}
}
pub(crate) struct ClapVoice {
pub(crate) noise: WhiteNoise,
pub(crate) scheduled: Vec<u64>,
pub(crate) bursts: Vec<ClapBurst>,
pub(crate) current: u64,
pub(crate) decay_samples: u64,
pub(crate) filter_smoothing: f32,
pub(crate) body_coeff: f32,
pub(crate) body_state: f32,
pub(crate) level: f32,
}
impl ClapVoice {
pub(crate) fn new(c: &ClapControls, sample_rate: f32, rng: &mut StdRng) -> Self {
let count = c.slap_count.round().max(1.0) as usize;
let spread = (c.slap_spread_ms * 0.001 * sample_rate) as u64;
let mut scheduled: Vec<u64> = (0..count)
.map(|i| {
if i == 0 {
0
} else {
rng.gen_range(0..=spread.max(1))
}
})
.collect();
scheduled.sort_unstable();
Self {
noise: WhiteNoise::new(),
scheduled,
bursts: Vec::new(),
current: 0,
decay_samples: (c.decay_ms * 0.001 * sample_rate).round() as u64,
filter_smoothing: noise_filter_smoothing(c.filter),
body_coeff: c.body * 0.08,
body_state: 0.0,
level: c.level,
}
}
pub(crate) fn next<R: Rng>(&mut self, rng: &mut R) -> f32 {
self.scheduled.retain(|&t| {
if self.current >= t {
self.bursts.push(ClapBurst {
remaining: self.decay_samples,
total: self.decay_samples,
});
false
} else {
true
}
});
if self.bursts.is_empty() && self.scheduled.is_empty() {
return 0.0;
}
let mut out = 0.0f32;
for burst in &mut self.bursts {
if burst.remaining > 0 {
let env = (burst.remaining as f32 / burst.total as f32).sqrt();
burst.remaining -= 1;
let raw = self.noise.next_filtered(rng, self.filter_smoothing);
self.body_state += self.body_coeff * (raw - self.body_state);
out += (raw + self.body_state) * env;
}
}
self.bursts.retain(|b| b.remaining > 0);
self.current += 1;
out * self.level * OUTPUT_TRIM
}
pub(crate) fn is_done(&self) -> bool {
self.scheduled.is_empty() && self.bursts.is_empty()
}
}
pub(crate) struct ClapBurst {
pub(crate) remaining: u64,
pub(crate) total: u64,
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn swing_delays_the_claps_odd_subdivision() {
let mut clap = ClapEngine::new(48_000.0);
let controls = ClapControls {
level: 1.0,
interval_beats: 0.5,
swing: 1.0,
..ClapControls::default()
};
clap.next(&controls, TimingContext::new(48_000.0, 120.0, 0.0));
clap.next(&controls, TimingContext::new(48_000.0, 120.0, 0.5));
assert_eq!(clap.voices.len(), 1);
clap.next(&controls, TimingContext::new(48_000.0, 120.0, 0.75));
assert_eq!(clap.voices.len(), 2);
}
}