use super::*;
pub(crate) struct KickEngine {
pub(crate) sample_rate: f32,
pub(crate) trigger: GridTrigger,
pub(crate) voices: Vec<KickVoice>,
pub(crate) rng: StdRng,
pub(crate) telemetry: Arc<FluidTelemetry>,
}
impl KickEngine {
pub(crate) fn new(sample_rate: f32, telemetry: Arc<FluidTelemetry>) -> Self {
Self {
sample_rate,
trigger: GridTrigger::new(),
voices: Vec::with_capacity(4),
rng: StdRng::from_entropy(),
telemetry,
}
}
pub(crate) fn next(&mut self, c: &KickControls, timing: TimingContext) -> (f32, f32) {
if self
.trigger
.pop_swung(timing, c.interval_beats, c.offset_beats, c.swing)
{
self.voices.push(KickVoice::new(
kick_type_index(c.voice_type),
c,
self.sample_rate,
&mut self.rng,
));
self.telemetry.kick_pulse.fetch_add(1, Ordering::Relaxed);
}
let rng = &mut self.rng;
mix_and_retain(&mut self.voices, |v| v.next(rng), KickVoice::is_done)
}
}
pub(crate) struct KickVoiceCore {
pub(crate) amp: f32,
pub(crate) amp_decay: f32,
pub(crate) click_remaining: u64,
pub(crate) click_level: f32,
pub(crate) attack_remaining: u64,
pub(crate) attack_gain: f32,
pub(crate) attack_inc: f32,
pub(crate) pan_gains: (f32, f32),
}
impl KickVoiceCore {
pub(crate) fn new(
c: &KickControls,
sample_rate: f32,
rng: &mut StdRng,
attack_ms: f32,
click_scale: f32,
) -> Self {
let amp_tau = (c.amp_decay_ms * 0.001 * sample_rate / 3.0).max(1.0);
let attack_samples = (attack_ms * 0.001 * sample_rate).round().max(0.0) as u64;
Self {
amp: c.level,
amp_decay: (-1.0 / amp_tau).exp(),
click_remaining: (c.amp_decay_ms * 0.001 * sample_rate * 0.04).round() as u64,
click_level: c.click * click_scale,
attack_remaining: attack_samples,
attack_gain: 0.0,
attack_inc: if attack_samples == 0 {
0.0
} else {
1.0 / attack_samples as f32
},
pan_gains: StereoPanner::gains(rng.gen_range(-0.15f32..0.15)),
}
}
#[inline]
pub(crate) fn shape<R: Rng>(&mut self, body: f32, rng: &mut R) -> f32 {
let mut s = body * self.amp;
if self.click_remaining > 0 {
s += rng.gen_range(-1.0f32..1.0) * self.click_level * self.amp;
self.click_remaining -= 1;
}
if self.attack_remaining > 0 {
s *= self.attack_gain;
self.attack_gain = (self.attack_gain + self.attack_inc).min(1.0);
self.attack_remaining -= 1;
}
self.amp *= self.amp_decay;
s
}
pub(crate) fn is_done(&self) -> bool {
self.amp < 0.0001
}
}
pub(crate) enum KickVoice {
A(SubKickVoice),
B(WarmKickVoice),
C(WoodKickVoice),
D(FeltKickVoice),
}
impl KickVoice {
pub(crate) fn new(
voice_type: usize,
c: &KickControls,
sample_rate: f32,
rng: &mut StdRng,
) -> Self {
match voice_type {
0 => Self::A(SubKickVoice::new(c, sample_rate, rng)),
1 => Self::B(WarmKickVoice::new(c, sample_rate, rng)),
2 => Self::C(WoodKickVoice::new(c, sample_rate, rng)),
_ => Self::D(FeltKickVoice::new(c, sample_rate, rng)),
}
}
pub(crate) fn next<R: Rng>(&mut self, rng: &mut R) -> (f32, f32) {
match self {
Self::A(voice) => voice.next(rng),
Self::B(voice) => voice.next(rng),
Self::C(voice) => voice.next(rng),
Self::D(voice) => voice.next(rng),
}
}
pub(crate) fn is_done(&self) -> bool {
match self {
Self::A(voice) => voice.is_done(),
Self::B(voice) => voice.is_done(),
Self::C(voice) => voice.is_done(),
Self::D(voice) => voice.is_done(),
}
}
}
pub(crate) struct KickFmBody {
pub(crate) phase: f32,
pub(crate) mod_phase: f32,
pub(crate) freq: f32,
pub(crate) target_freq: f32,
pub(crate) freq_glide: f32,
pub(crate) mod_ratio: f32,
pub(crate) fm_depth: f32,
pub(crate) fm_depth_decay: f32,
pub(crate) sample_rate: f32,
}
impl KickFmBody {
pub(crate) fn new(
c: &KickControls,
sample_rate: f32,
pitch_drop_ratio: f32,
mod_ratio: f32,
fm_depth: f32,
) -> Self {
let tau = (c.pitch_decay_ms * 0.001 * sample_rate / 3.0).max(1.0);
let fm_tau = (c.pitch_decay_ms * 0.001 * sample_rate / 9.0).max(1.0);
Self {
phase: 0.0,
mod_phase: 0.0,
freq: c.start_freq,
target_freq: c.start_freq * pitch_drop_ratio,
freq_glide: 1.0 / tau,
mod_ratio,
fm_depth,
fm_depth_decay: (-1.0 / fm_tau).exp(),
sample_rate,
}
}
#[inline]
pub(crate) fn next_phase(&mut self) -> f32 {
self.freq += (self.target_freq - self.freq) * self.freq_glide;
let mod_freq = self.freq * self.mod_ratio;
self.mod_phase += TAU * mod_freq / self.sample_rate;
if self.mod_phase >= TAU {
self.mod_phase -= TAU;
}
let fm = self.mod_phase.sin() * self.fm_depth * self.freq;
self.fm_depth *= self.fm_depth_decay;
self.phase += TAU * (self.freq + fm) / self.sample_rate;
if self.phase >= TAU {
self.phase -= TAU;
}
self.phase
}
}
pub(crate) struct KickLowPass {
pub(crate) state: f32,
pub(crate) coeff: f32,
}
impl KickLowPass {
pub(crate) fn new(filter: f32, bias: f32) -> Self {
Self {
state: 0.0,
coeff: 10_f32.powf(filter * 3.0 + bias).clamp(0.01, 0.99),
}
}
#[inline]
pub(crate) fn process(&mut self, s: f32) -> f32 {
self.state += self.coeff * (s - self.state);
self.state
}
}
const KICK_SUB_PITCH_DROP_RATIO: f32 = 0.28;
const KICK_SUB_FM_MOD_RATIO: f32 = 2.0;
const KICK_SUB_FM_DEPTH: f32 = 3.5;
const KICK_SUB_FILTER_BIAS: f32 = -2.5;
pub(crate) struct SubKickVoice {
pub(crate) core: KickVoiceCore,
pub(crate) body: KickFmBody,
pub(crate) lowpass: KickLowPass,
}
impl SubKickVoice {
pub(crate) fn new(c: &KickControls, sample_rate: f32, rng: &mut StdRng) -> Self {
Self {
core: KickVoiceCore::new(c, sample_rate, rng, 0.0, 1.0),
body: KickFmBody::new(
c,
sample_rate,
KICK_SUB_PITCH_DROP_RATIO,
KICK_SUB_FM_MOD_RATIO,
KICK_SUB_FM_DEPTH,
),
lowpass: KickLowPass::new(c.filter, KICK_SUB_FILTER_BIAS),
}
}
pub(crate) fn next<R: Rng>(&mut self, rng: &mut R) -> (f32, f32) {
if self.core.is_done() {
return (0.0, 0.0);
}
let body = self.body.next_phase().sin();
let s = self.lowpass.process(self.core.shape(body, rng));
(s * self.core.pan_gains.0, s * self.core.pan_gains.1)
}
pub(crate) fn is_done(&self) -> bool {
self.core.is_done()
}
}
const KICK_WARM_PITCH_DROP_RATIO: f32 = 0.42;
const KICK_WARM_FM_MOD_RATIO: f32 = 1.5;
const KICK_WARM_FM_DEPTH: f32 = 1.2;
const KICK_WARM_FILTER_BIAS: f32 = -2.35;
const KICK_WARM_ATTACK_MS: f32 = 6.0;
const KICK_WARM_CLICK_SCALE: f32 = 0.45;
const KICK_WARM_OUTPUT_GAIN: f32 = 0.9;
pub(crate) struct WarmKickVoice {
pub(crate) core: KickVoiceCore,
pub(crate) body: KickFmBody,
pub(crate) lowpass: KickLowPass,
}
impl WarmKickVoice {
pub(crate) fn new(c: &KickControls, sample_rate: f32, rng: &mut StdRng) -> Self {
Self {
core: KickVoiceCore::new(
c,
sample_rate,
rng,
KICK_WARM_ATTACK_MS,
KICK_WARM_CLICK_SCALE,
),
body: KickFmBody::new(
c,
sample_rate,
KICK_WARM_PITCH_DROP_RATIO,
KICK_WARM_FM_MOD_RATIO,
KICK_WARM_FM_DEPTH,
),
lowpass: KickLowPass::new(c.filter, KICK_WARM_FILTER_BIAS),
}
}
pub(crate) fn next<R: Rng>(&mut self, rng: &mut R) -> (f32, f32) {
if self.core.is_done() {
return (0.0, 0.0);
}
let body = self.body.next_phase().sin();
let s = self
.lowpass
.process(self.core.shape(body, rng) * KICK_WARM_OUTPUT_GAIN);
(s * self.core.pan_gains.0, s * self.core.pan_gains.1)
}
pub(crate) fn is_done(&self) -> bool {
self.core.is_done()
}
}
const KICK_WOOD_CENTER_MIN_HZ: f32 = 110.0;
const KICK_WOOD_CENTER_MAX_HZ: f32 = 400.0;
const KICK_WOOD_DAMP: f32 = 0.9;
const KICK_WOOD_PITCH_DROP_RATIO: f32 = 0.35;
const KICK_WOOD_BANDPASS_MIX: f32 = 0.6;
const KICK_WOOD_ATTACK_MS: f32 = 5.0;
const KICK_WOOD_CLICK_SCALE: f32 = 0.35;
const KICK_WOOD_OUTPUT_GAIN: f32 = 1.7;
pub(crate) struct WoodKickVoice {
pub(crate) core: KickVoiceCore,
pub(crate) body: KickFmBody,
pub(crate) svf_low: f32,
pub(crate) svf_band: f32,
pub(crate) svf_f: f32,
}
impl WoodKickVoice {
pub(crate) fn new(c: &KickControls, sample_rate: f32, rng: &mut StdRng) -> Self {
let filter = c.filter.clamp(0.0, 1.0);
let center_hz = KICK_WOOD_CENTER_MIN_HZ
* (KICK_WOOD_CENTER_MAX_HZ / KICK_WOOD_CENTER_MIN_HZ).powf(filter);
let svf_f = (2.0 * (std::f32::consts::PI * center_hz / sample_rate).sin()).clamp(0.0, 1.9);
Self {
core: KickVoiceCore::new(
c,
sample_rate,
rng,
KICK_WOOD_ATTACK_MS,
KICK_WOOD_CLICK_SCALE,
),
body: KickFmBody::new(
c,
sample_rate,
KICK_WOOD_PITCH_DROP_RATIO,
KICK_SUB_FM_MOD_RATIO,
KICK_SUB_FM_DEPTH,
),
svf_low: 0.0,
svf_band: 0.0,
svf_f,
}
}
pub(crate) fn next<R: Rng>(&mut self, rng: &mut R) -> (f32, f32) {
if self.core.is_done() {
return (0.0, 0.0);
}
let body = self.body.next_phase().sin();
let dry = self.core.shape(body, rng);
let high = dry - self.svf_low - KICK_WOOD_DAMP * self.svf_band;
self.svf_band += self.svf_f * high;
self.svf_low += self.svf_f * self.svf_band;
let s = (self.svf_band * KICK_WOOD_BANDPASS_MIX + dry * (1.0 - KICK_WOOD_BANDPASS_MIX))
* KICK_WOOD_OUTPUT_GAIN;
(s * self.core.pan_gains.0, s * self.core.pan_gains.1)
}
pub(crate) fn is_done(&self) -> bool {
self.core.is_done()
}
}
const KICK_FELT_PITCH_DROP_RATIO: f32 = 0.28;
const KICK_FELT_FM_DEPTH: f32 = 1.8;
const KICK_FELT_FILTER_BIAS: f32 = -2.9;
const KICK_FELT_ATTACK_MS: f32 = 8.0;
const KICK_FELT_CLICK_SCALE: f32 = 0.25;
const KICK_FELT_OUTPUT_GAIN: f32 = 0.95;
pub(crate) struct FeltKickVoice {
pub(crate) core: KickVoiceCore,
pub(crate) body: KickFmBody,
pub(crate) lowpass: KickLowPass,
}
impl FeltKickVoice {
pub(crate) fn new(c: &KickControls, sample_rate: f32, rng: &mut StdRng) -> Self {
Self {
core: KickVoiceCore::new(
c,
sample_rate,
rng,
KICK_FELT_ATTACK_MS,
KICK_FELT_CLICK_SCALE,
),
body: KickFmBody::new(
c,
sample_rate,
KICK_FELT_PITCH_DROP_RATIO,
KICK_SUB_FM_MOD_RATIO,
KICK_FELT_FM_DEPTH,
),
lowpass: KickLowPass::new(c.filter, KICK_FELT_FILTER_BIAS),
}
}
pub(crate) fn next<R: Rng>(&mut self, rng: &mut R) -> (f32, f32) {
if self.core.is_done() {
return (0.0, 0.0);
}
let t = self.body.next_phase() / TAU;
let body = 4.0 * (t - (t + 0.5).floor()).abs() - 1.0;
let s = self
.lowpass
.process(self.core.shape(body, rng) * KICK_FELT_OUTPUT_GAIN);
(s * self.core.pan_gains.0, s * self.core.pan_gains.1)
}
pub(crate) fn is_done(&self) -> bool {
self.core.is_done()
}
}
#[cfg(test)]
mod tests {
use std::sync::Arc;
use super::*;
#[test]
fn swing_delays_the_kicks_odd_subdivision() {
let telemetry = Arc::new(FluidTelemetry::default());
let mut kick = KickEngine::new(48_000.0, Arc::clone(&telemetry));
let controls = KickControls {
level: 1.0,
interval_beats: 0.5,
swing: 1.0,
..KickControls::default()
};
kick.next(&controls, TimingContext::new(48_000.0, 120.0, 0.0));
kick.next(&controls, TimingContext::new(48_000.0, 120.0, 0.5));
assert_eq!(telemetry.kick_pulse.load(Ordering::Relaxed), 1);
kick.next(&controls, TimingContext::new(48_000.0, 120.0, 0.75));
assert_eq!(telemetry.kick_pulse.load(Ordering::Relaxed), 2);
}
}