use crate::midi::{MidiClockFollower, MidiSink};
use std::collections::BTreeSet;
use crate::fx::compression::{CompressorParams, StereoCompressor};
use crate::fx::crossfade::{Outgoing, mix_stereo};
use crate::fx::delay::{DelayParams, StereoDelay};
use crate::fx::drive;
use crate::fx::filter::{FilterParams, FilterType, StereoFilter};
use crate::fx::reverb::{Freeverb, ReverbParams};
use super::*;
mod gesture_audio;
use gesture_audio::GestureAudioBank;
const STARTUP_FADE_SECONDS: f32 = 2.0;
enum SlotFx {
Delay(StereoDelay),
Reverb(Freeverb),
Compression(StereoCompressor),
Filter(StereoFilter),
}
impl SlotFx {
fn family(&self) -> Family {
match self {
Self::Delay(_) => Family::Delay,
Self::Reverb(_) => Family::Reverb,
Self::Compression(_) => Family::Compression,
Self::Filter(_) => Family::Filter,
}
}
}
struct RetiredFx {
fx: SlotFx,
slot: ModuleSlot,
}
struct ModuleFxBank {
slots: [Option<SlotFx>; MODULE_LAYERS * MODULE_SLOTS],
retiring: [Option<Outgoing<RetiredFx>>; MODULE_LAYERS * MODULE_SLOTS],
last_loaded: [ModuleSlot; MODULE_LAYERS * MODULE_SLOTS],
max_delay_samples: usize,
sample_rate: f32,
retire_fade_samples: f32,
}
impl ModuleFxBank {
fn new(sample_rate: f32) -> Self {
Self {
slots: std::array::from_fn(|_| None),
retiring: std::array::from_fn(|_| None),
last_loaded: [ModuleSlot::default(); MODULE_LAYERS * MODULE_SLOTS],
max_delay_samples: (sample_rate * (DELAY_FREE_MAX_MS / 1_000.0)).ceil() as usize,
sample_rate,
retire_fade_samples: LEVEL_RAMP_MS * 0.001 * sample_rate,
}
}
fn process_slot_fx(
fx: &mut SlotFx,
slot: &ModuleSlot,
sample: (f32, f32),
timing: TimingContext,
max_delay_samples: usize,
sample_rate: f32,
) -> (f32, f32) {
match fx {
SlotFx::Delay(line) => {
let left = delay_time_ms(
slot.time,
DelayClock::from_value(slot.clock),
timing.bpm as f32,
);
let right = delay_time_ms(
slot.right_time,
DelayClock::from_value(slot.right_clock),
timing.bpm as f32,
);
let samples = |ms: f32| {
((ms * timing.sample_rate as f32 / 1_000.0).round() as usize)
.clamp(1, max_delay_samples)
};
line.process(
sample,
DelayParams {
left_delay_samples: samples(left),
right_delay_samples: samples(right),
feedback: slot.feedback,
amount: slot.amount,
vintage: slot.vintage,
sample_rate: timing.sample_rate as f32,
},
)
}
SlotFx::Reverb(reverb) => {
let input = if slot.amount > 0.0 {
sample
} else {
(0.0, 0.0)
};
let wet = reverb.process(
input.0,
input.1,
ReverbParams {
room_size: slot.time,
damp: slot.feedback,
},
);
(
sample.0 + wet.0 * slot.amount,
sample.1 + wet.1 * slot.amount,
)
}
SlotFx::Compression(compressor) => compressor.process(
sample,
CompressorParams {
sample_rate,
threshold_db: slot.time,
ratio: slot.right_time,
release_ms: slot.feedback,
makeup_db: slot.vintage,
amount: slot.amount,
},
),
SlotFx::Filter(filter) => filter.process(
sample,
FilterParams {
sample_rate,
cutoff_hz: slot.time,
resonance: slot.right_time,
filter_type: FilterType::from_value(slot.feedback),
amount: slot.amount,
},
),
}
}
fn retire_if_replaced(&mut self, index: usize, slot: &ModuleSlot) {
let wanted = slot.kind().map(|kind| kind.family);
let loaded = self.slots[index].as_ref().map(SlotFx::family);
if loaded.is_none() || loaded == wanted {
return;
}
let Some(fx) = self.slots[index].take() else {
return;
};
self.retiring[index] = Some(Outgoing::start(
RetiredFx {
fx,
slot: self.last_loaded[index],
},
self.retire_fade_samples,
));
}
fn run_retiring(
&mut self,
index: usize,
sample: (f32, f32),
timing: TimingContext,
) -> (f32, f32) {
let (max_delay_samples, sample_rate) = (self.max_delay_samples, self.sample_rate);
let Some(retiring) = &mut self.retiring[index] else {
return sample;
};
let wet = Self::process_slot_fx(
&mut retiring.inner.fx,
&retiring.inner.slot,
sample,
timing,
max_delay_samples,
sample_rate,
);
let weight = retiring.advance();
if retiring.is_done() {
self.retiring[index] = None;
}
mix_stereo(sample, wet, weight)
}
fn process(
&mut self,
tab: Tab,
slots: &[ModuleSlot; MODULE_SLOTS],
mut sample: (f32, f32),
timing: TimingContext,
) -> (f32, f32) {
let Some(layer) = module_layer_index(tab) else {
return sample;
};
for (slot_index, slot) in slots.iter().enumerate() {
let index = layer * MODULE_SLOTS + slot_index;
self.retire_if_replaced(index, slot);
sample = self.run_retiring(index, sample, timing);
let Some(kind) = slot.kind() else {
continue;
};
self.last_loaded[index] = *slot;
let (max_delay_samples, sample_rate) = (self.max_delay_samples, self.sample_rate);
let processor = &mut self.slots[index];
if slot.amount <= 0.0 && processor.is_none() {
continue;
}
match kind.family {
Family::Delay | Family::Reverb | Family::Compression | Family::Filter => {
if processor.is_none() {
*processor = Some(match kind.family {
Family::Delay => SlotFx::Delay(StereoDelay::new(max_delay_samples)),
Family::Reverb => SlotFx::Reverb(Freeverb::new(sample_rate)),
Family::Filter => SlotFx::Filter(StereoFilter::default()),
_ => SlotFx::Compression(StereoCompressor::new(0.0)),
});
}
let Some(fx) = processor else {
continue;
};
sample = Self::process_slot_fx(
fx,
slot,
sample,
timing,
max_delay_samples,
sample_rate,
);
}
Family::SingleAmount => {
if kind.id == "drive" {
sample = drive::process(sample, slot.amount);
}
}
Family::TwoKnob => {}
}
}
sample
}
}
#[cfg(test)]
mod module_fx_tests {
use super::*;
const TEST_SAMPLE_RATE: f32 = 48_000.0;
fn timing() -> TimingContext {
TimingContext::new(TEST_SAMPLE_RATE as f64, 120.0, 0.0)
}
fn first_nonzero_after_silence(
bank: &mut ModuleFxBank,
slots: &[ModuleSlot; MODULE_SLOTS],
tab: Tab,
timing: TimingContext,
frames: usize,
) -> Option<(f32, f32)> {
(0..frames)
.map(|_| bank.process(tab, slots, (0.0, 0.0), timing))
.find(|output| *output != (0.0, 0.0))
}
fn run(
bank: &mut ModuleFxBank,
slots: &[ModuleSlot; MODULE_SLOTS],
input: (f32, f32),
samples: usize,
) -> Vec<f32> {
(0..samples)
.map(|_| {
let (l, r) = bank.process(Tab::Chords, slots, input, timing());
(l * l + r * r).sqrt()
})
.collect()
}
fn bank_with_a_live_tail() -> (ModuleFxBank, [ModuleSlot; MODULE_SLOTS], f32) {
let mut bank = ModuleFxBank::new(TEST_SAMPLE_RATE);
let mut slots: [ModuleSlot; MODULE_SLOTS] = std::array::from_fn(|_| ModuleSlot::default());
slots[0] = preset_slot("room", 1.0);
run(&mut bank, &slots, (0.6, 0.6), 12_000);
let tail = run(&mut bank, &slots, (0.0, 0.0), 64);
let level = tail.iter().sum::<f32>() / tail.len() as f32;
assert!(level > 0.001, "no reverb tail to test against: {level}");
(bank, slots, level)
}
#[test]
fn removing_a_module_fades_its_tail_instead_of_cutting_it() {
let (mut bank, mut slots, level) = bank_with_a_live_tail();
slots[0] = ModuleSlot::default();
assert!(slots[0].is_empty(), "the slot under test must be empty");
let first = run(&mut bank, &slots, (0.0, 0.0), 1)[0];
assert!(
first > level * 0.5,
"tail was cut to {first} from a {level} tail"
);
let ramp = (LEVEL_RAMP_MS * 0.001 * TEST_SAMPLE_RATE) as usize;
run(&mut bank, &slots, (0.0, 0.0), ramp);
let settled = run(&mut bank, &slots, (0.0, 0.0), 64);
assert!(
settled.iter().all(|s| *s <= level * 0.02),
"removed module still audible after its fade"
);
}
#[test]
fn re_adding_a_module_does_not_resurrect_the_previous_tail() {
let (mut bank, mut slots, level) = bank_with_a_live_tail();
slots[0] = ModuleSlot::default();
let ramp = (LEVEL_RAMP_MS * 0.001 * TEST_SAMPLE_RATE) as usize;
run(&mut bank, &slots, (0.0, 0.0), ramp + 64);
slots[0] = preset_slot("room", 1.0);
let revived = run(&mut bank, &slots, (0.0, 0.0), 256);
let loudest = revived.iter().fold(0.0f32, |acc, s| acc.max(*s));
assert!(
loudest <= level * 0.02,
"re-adding the module replayed a stale tail at {loudest}"
);
}
#[test]
fn replacing_a_module_fades_the_outgoing_one() {
let (mut bank, mut slots, level) = bank_with_a_live_tail();
slots[0] = preset_slot("drive", 0.0);
let first = run(&mut bank, &slots, (0.0, 0.0), 1)[0];
assert!(
first > level * 0.5,
"outgoing module was cut to {first} from a {level} tail"
);
}
#[test]
fn compression_slot_specs_bound_every_field_the_dsp_reads() {
let mut controls = FluidControls::default();
controls.modules.master[1] = preset_slot("compression", 1.0);
for (field, min, max) in [
("time", -40.0, 0.0),
("right_time", 1.0, 8.0),
("feedback", 10.0, 500.0),
("vintage", 0.0, 12.0),
] {
let id = format!("master.slot2.{field}");
let spec = spec_by_id(&id)
.unwrap_or_else(|| panic!("{id} is a registry control"))
.contextual(&controls);
assert_eq!(
(spec.min, spec.max),
(min, max),
"{id} no longer carries the range the compressor DSP assumes"
);
}
}
#[test]
fn zero_delay_amount_preserves_the_dry_track() {
let timing = TimingContext::new(44_100.0, 120.0, 0.0);
let mut bank = ModuleFxBank::new(44_100.0);
let mut slots = [ModuleSlot::default(); MODULE_SLOTS];
slots[0] = preset_slot("delay", 0.0);
let output = bank.process(Tab::Clap, &slots, (0.4, -0.2), timing);
assert_eq!(output, (0.4, -0.2));
}
#[test]
fn every_post_effect_executes_on_every_layer_chain() {
let timing = TimingContext::new(44_100.0, 120.0, 0.0);
for tab in Tab::all() {
let mut slots = [ModuleSlot::default(); MODULE_SLOTS];
slots[0] = preset_slot("drive", 0.7);
let mut bank = ModuleFxBank::new(44_100.0);
assert_ne!(
bank.process(tab, &slots, (0.4, -0.2), timing),
(0.4, -0.2),
"Drive is inert on {}",
tab.name()
);
slots[0] = preset_slot("compression", 1.0);
slots[0].time = -20.0;
slots[0].right_time = 4.0;
slots[0].vintage = 0.0;
let mut bank = ModuleFxBank::new(44_100.0);
let mut compressed = (1.0, 1.0);
for _ in 0..256 {
compressed = bank.process(tab, &slots, (1.0, 1.0), timing);
}
assert!(compressed.0 < 1.0, "Compression is inert on {}", tab.name());
slots[0] = preset_slot("room", 1.0);
let mut bank = ModuleFxBank::new(44_100.0);
bank.process(tab, &slots, (1.0, 1.0), timing);
assert!(
first_nonzero_after_silence(&mut bank, &slots, tab, timing, 2_000).is_some(),
"Reverb is inert on {}",
tab.name()
);
slots[0] = preset_slot("delay", 1.0);
slots[0].clock = DelayClock::Free.value();
slots[0].right_clock = DelayClock::Free.value();
slots[0].time = 10.0;
slots[0].right_time = 10.0;
slots[0].feedback = 0.0;
let mut bank = ModuleFxBank::new(44_100.0);
bank.process(tab, &slots, (1.0, -1.0), timing);
assert!(
first_nonzero_after_silence(&mut bank, &slots, tab, timing, 500).is_some(),
"Delay is inert on {}",
tab.name()
);
}
}
#[test]
fn zero_amount_is_an_exact_dry_bypass_for_every_post_family() {
let timing = TimingContext::new(44_100.0, 120.0, 0.0);
for kind in ["drive", "room", "delay", "compression"] {
let mut bank = ModuleFxBank::new(44_100.0);
let mut slots = [ModuleSlot::default(); MODULE_SLOTS];
slots[0] = preset_slot(kind, 0.0);
assert_eq!(
bank.process(Tab::Clap, &slots, (0.4, -0.2), timing),
(0.4, -0.2),
"{kind} amount zero changed the dry signal"
);
}
}
}
pub(crate) struct FluidEngine {
pub(crate) current_sample: u64,
pub(crate) sample_rate: f32,
pub(crate) tempo: TempoClock,
beat_trigger: GridTrigger,
midi_clock: Option<MidiClockFollower>,
midi_input: Option<crate::midi::MidiInputSource>,
midi_input_enabled: [bool; 3],
pub(crate) gain_smoothers: GainSmoothers,
mute_gates: OutputGates,
muted: MuteState,
pub(crate) pad: PadEngine,
pub(crate) perc: PercEngine,
pub(crate) kick: KickEngine,
pub(crate) tonal: TonalEngine,
pub(crate) clap: ClapEngine,
pub(crate) bass: BassEngine,
pub(crate) arp: ArpEngine,
pub(crate) lead: LeadEngine,
module_fx: ModuleFxBank,
gesture_audio: GestureAudioBank,
pub(crate) master_bus: MasterBus,
pub(crate) session: LiveSession,
pub(crate) morph: Arc<ArcSwap<Option<MorphState>>>,
morph_writer: MorphWriter,
pub(crate) telemetry: Arc<FluidTelemetry>,
level_acc: [f32; TAB_COUNT],
pub(crate) snapshot: FluidControls,
gesture_snapshot: GestureState,
transport: Transport,
transport_restart: u64,
plan: AutomationPlan,
plan_source: Arc<AutomationState>,
}
impl FluidEngine {
pub(crate) fn new(
sample_rate: f32,
session: LiveSession,
morph: Arc<ArcSwap<Option<MorphState>>>,
telemetry: Arc<FluidTelemetry>,
) -> Self {
Self::new_with_tonal_session_state(sample_rate, session, morph, telemetry, false)
}
pub(crate) fn new_with_tonal_session_state(
sample_rate: f32,
session: LiveSession,
morph: Arc<ArcSwap<Option<MorphState>>>,
telemetry: Arc<FluidTelemetry>,
publish_tonal_session_state: bool,
) -> Self {
let live = session.load();
let snapshot = live.controls.clone();
let plan_source = Arc::new(live.automation.clone());
let mut plan = AutomationPlan::default();
plan.rebuild(&plan_source);
Self {
current_sample: 0,
sample_rate,
tempo: TempoClock::new(sample_rate, snapshot.master.bpm),
beat_trigger: GridTrigger::new(),
midi_clock: None,
midi_input: None,
midi_input_enabled: [false; 3],
gain_smoothers: GainSmoothers::new(&snapshot),
mute_gates: OutputGates::new(&live.muted),
muted: live.muted,
pad: PadEngine::new(
sample_rate,
&snapshot.pad,
snapshot.master.tune,
Arc::clone(&telemetry),
),
perc: PercEngine::with_telemetry(sample_rate, Arc::clone(&telemetry)),
kick: KickEngine::new(sample_rate, Arc::clone(&telemetry)),
tonal: TonalEngine::new_with_live_state(
sample_rate,
publish_tonal_session_state.then(|| session.clone()),
Arc::clone(&telemetry),
),
clap: ClapEngine::with_telemetry(sample_rate, Arc::clone(&telemetry)),
bass: BassEngine::new(sample_rate),
arp: ArpEngine::with_telemetry(sample_rate, Arc::clone(&telemetry)),
lead: LeadEngine::with_play_state(sample_rate, live.lead_play),
module_fx: ModuleFxBank::new(sample_rate),
gesture_audio: GestureAudioBank::new(sample_rate),
master_bus: MasterBus::new(&snapshot.master, sample_rate),
session,
morph,
morph_writer: MorphWriter::default(),
telemetry,
level_acc: [0.0; TAB_COUNT],
snapshot,
gesture_snapshot: live.gestures.clone(),
transport: live.transport,
transport_restart: live.transport_restart,
plan,
plan_source,
}
}
fn restart_sequence(&mut self) {
self.tempo.beat = 0.0;
self.morph_writer = MorphWriter::default();
self.morph
.rcu(|current| current.as_ref().as_ref().map(MorphState::restarted));
self.beat_trigger = GridTrigger::new();
self.bass.progression = ProgressionFollower::new();
self.bass.step_trigger = GridTrigger::new();
self.perc.trigger = GridTrigger::new();
self.kick.trigger = GridTrigger::new();
self.clap.trigger = GridTrigger::new();
self.tonal.step_trigger = GridTrigger::new();
self.tonal.step_index = 0;
self.tonal.last_cycle = None;
self.arp.progression = ProgressionFollower::new();
self.arp.note_trigger = GridTrigger::new();
self.arp.cycle_pos = 0;
self.arp.ping_pong_dir = 1;
self.lead.progression = ProgressionFollower::new();
self.lead.step_trigger = GridTrigger::new();
if let Some(clock) = &mut self.midi_clock {
clock.restart();
}
}
pub(crate) fn with_midi(mut self, sink: MidiSink) -> Self {
self.pad.set_midi(sink.clone());
self.arp.set_midi(sink.clone());
self.lead.set_midi(sink.clone());
self.midi_clock = Some(MidiClockFollower::new(sink));
self
}
pub(crate) fn with_midi_input(mut self, source: crate::midi::MidiInputSource) -> Self {
self.pad.set_midi_input_attached();
self.midi_input = Some(source);
self
}
fn route_midi_input(
&mut self,
event: crate::midi::MidiInputEvent,
controls: &FluidControls,
timing: TimingContext,
) {
use crate::midi::MidiInputEvent::{NoteOff, NoteOn};
match event {
NoteOn(note) => {
if controls.pad.midi_in >= 0.5 {
self.pad
.midi_note_on(note, &controls.pad, controls.master.tune, timing);
}
if controls.arp.midi_in >= 0.5 {
self.arp.midi_note_on(note);
}
if controls.lead.midi_in >= 0.5 {
self.lead
.midi_note_on(note, &controls.lead, controls.master.tune);
}
}
NoteOff(note) => {
self.pad.midi_note_off(note);
self.arp.midi_note_off(note);
self.lead.midi_note_off(note);
}
crate::midi::MidiInputEvent::AllNotesOff => {
self.pad.release_input();
self.arp.release_input();
self.lead.release_input();
}
}
}
fn sync_midi_input_switches(&mut self, controls: &FluidControls) {
let enabled = [
controls.pad.midi_in >= 0.5,
controls.arp.midi_in >= 0.5,
controls.lead.midi_in >= 0.5,
];
if self.midi_input_enabled[0] && !enabled[0] {
self.pad.release_input();
}
if self.midi_input_enabled[1] && !enabled[1] {
self.arp.release_input();
}
if self.midi_input_enabled[2] && !enabled[2] {
self.lead.release_input();
}
self.midi_input_enabled = enabled;
}
}
impl FluidEngine {
pub(crate) fn reseed(&mut self, seed: u64) {
self.pad.rng = StdRng::seed_from_u64(seed);
self.perc.rng = StdRng::seed_from_u64(seed.wrapping_add(1));
self.kick.rng = StdRng::seed_from_u64(seed.wrapping_add(2));
self.tonal.rng = StdRng::seed_from_u64(seed.wrapping_add(3));
self.clap.rng = StdRng::seed_from_u64(seed.wrapping_add(4));
self.arp.rng = StdRng::seed_from_u64(seed.wrapping_add(5));
}
}
impl StereoEngine for FluidEngine {
fn next_stereo(&mut self) -> (f32, f32) {
if self.current_sample.is_multiple_of(128) {
let live = self.session.load();
let restarting = self.transport_restart != live.transport_restart;
if restarting {
self.transport_restart = live.transport_restart;
self.restart_sequence();
}
self.session.publish_audio_beat(self.tempo.beat);
self.session
.publish_audio_seconds(self.current_sample as f64 / self.sample_rate as f64);
let morph_source = self.morph.load_full();
if let Some(morph) = morph_source.as_ref()
&& let Some((next_controls, next_automation)) =
self.morph_writer.tick(morph, self.tempo.beat)
{
let _ = self.session.transact(|snapshot| {
if !Arc::ptr_eq(&self.morph.load_full(), &morph_source) {
return Err(());
}
snapshot.controls = next_controls.clone();
snapshot.automation = next_automation.clone();
Ok(())
});
}
let session = self.session.load();
self.snapshot = session.controls.clone();
if restarting {
self.pad.restart_sequence(&self.snapshot.pad);
}
self.gesture_snapshot = session.gestures.clone();
self.transport = session.transport;
self.gain_smoothers
.set_targets(&self.snapshot, self.sample_rate);
self.mute_gates
.set_targets(&session.muted, self.sample_rate);
self.muted = session.muted;
self.lead.observe(session.lead_play);
self.master_bus
.set_controls(&self.snapshot.master, self.sample_rate);
if session.automation != *self.plan_source {
self.plan.rebuild(&session.automation);
self.plan_source = Arc::new(session.automation.clone());
}
}
let fade = startup_fade(self.current_sample, self.sample_rate);
let mut effective = self.gain_smoothers.next_controls(&self.snapshot);
let timing = self.tempo.tick(effective.master.bpm, self.transport);
if let Some(clock) = &mut self.midi_clock {
clock.tick(timing);
}
if self.beat_trigger.pop_swung(timing, 1.0, 0.0, 0.0) {
self.telemetry
.publish_hit(MusicalHit::Beat, 1.0, NO_PITCH_CLASS);
}
if self.current_sample.is_multiple_of(256) {
self.telemetry.publish_beat(timing.beat);
}
self.plan.apply(&mut effective, timing);
resolve_module_chain(&mut effective);
effective.keep_midi_directions_exclusive();
self.sync_midi_input_switches(&effective);
let master_muted = self.muted[Tab::Master as usize];
if master_muted || self.muted[Tab::Chords as usize] {
effective.pad.midi_out = 0.0;
}
if master_muted || self.muted[Tab::Arp as usize] {
effective.arp.midi_out = 0.0;
}
if master_muted || self.muted[Tab::Lead as usize] {
effective.lead.midi_out = 0.0;
}
let mute_gains = self.mute_gates.next();
let now_seconds = self.current_sample as f64 / self.sample_rate as f64;
let tune = effective.master.tune;
self.pad.set_midi_suppressed(effective.pad.midi_out < 0.5);
let pad = self.module_fx.process(
Tab::Chords,
&effective.modules.pad,
self.pad.next(&effective.pad, tune, timing),
timing,
);
let (pad_l, pad_r) = gate_stereo(pad, mute_gains[Tab::Chords as usize]);
let perc = self.module_fx.process(
Tab::Perc,
&effective.modules.perc,
{
let perc = self.perc.next(&effective.perc, timing);
(perc, perc)
},
timing,
);
let (perc_l, perc_r) = gate_stereo(perc, mute_gains[Tab::Perc as usize]);
let kick = self.module_fx.process(
Tab::Kick,
&effective.modules.kick,
self.kick.next(&effective.kick, timing),
timing,
);
let (kick_l, kick_r) = gate_stereo(kick, mute_gains[Tab::Kick as usize]);
let tonal = self.module_fx.process(
Tab::Tonal,
&effective.modules.tonal,
self.tonal.next(&effective.tonal, tune, timing),
timing,
);
let (ton_l, ton_r) = gate_stereo(tonal, mute_gains[Tab::Tonal as usize]);
let clap = self.module_fx.process(
Tab::Clap,
&effective.modules.clap,
self.clap.next(&effective.clap, timing),
timing,
);
let (clap_l, clap_r) = gate_stereo(clap, mute_gains[Tab::Clap as usize]);
let bass = self.module_fx.process(
Tab::Bass,
&effective.modules.bass,
self.bass
.next(&effective.bass, &effective.pad, tune, timing),
timing,
);
let (bass_l, bass_r) = gate_stereo(bass, mute_gains[Tab::Bass as usize]);
let arp = self.module_fx.process(
Tab::Arp,
&effective.modules.arp,
self.arp.next(&effective.arp, &effective.pad, tune, timing),
timing,
);
let (arp_l, arp_r) = gate_stereo(arp, mute_gains[Tab::Arp as usize]);
let lead = self.module_fx.process(
Tab::Lead,
&effective.modules.lead,
self.lead
.next(&effective.lead, &effective.pad, tune, timing),
timing,
);
let (lead_l, lead_r) = gate_stereo(lead, mute_gains[Tab::Lead as usize]);
if self.current_sample.is_multiple_of(128)
&& let Some(source) = self.midi_input.clone()
{
let overflowed = source.drain(|event| self.route_midi_input(event, &effective, timing));
if overflowed {
self.pad.release_input();
self.arp.release_input();
self.lead.release_input();
}
}
self.current_sample += 1;
let voices_l = VoiceMix {
pad: pad_l,
perc: perc_l,
kick: kick_l,
tonal: ton_l,
clap: clap_l,
bass: bass_l,
arp: arp_l,
lead: lead_l,
};
let voices_r = VoiceMix {
pad: pad_r,
perc: perc_r,
kick: kick_r,
tonal: ton_r,
clap: clap_r,
bass: bass_r,
arp: arp_r,
lead: lead_r,
};
let heard_l = voices_l.weighted(fade);
let heard_r = voices_r.weighted(fade);
for (acc, (l, r)) in self.level_acc.iter_mut().zip(heard_l.iter().zip(&heard_r)) {
*acc += l * l + r * r;
}
let raw_l = voices_l.sum(fade);
let raw_r = voices_r.sum(fade);
let master = self.module_fx.process(
Tab::Master,
&effective.modules.master,
(raw_l, raw_r),
timing,
);
let master =
self.gesture_audio
.process(master, self.gesture_snapshot.amounts(now_seconds), timing);
let out = gate_stereo(
self.master_bus
.process(master.0, master.1, &effective.master),
mute_gains[Tab::Master as usize],
);
self.level_acc[Tab::Master as usize] += out.0 * out.0 + out.1 * out.1;
if self.current_sample.is_multiple_of(LEVEL_BLOCK) {
for (tab, acc) in Tab::all().into_iter().zip(&mut self.level_acc) {
self.telemetry
.publish_level(tab, (*acc / (2 * LEVEL_BLOCK) as f32).sqrt());
*acc = 0.0;
}
for (kind, amount) in GestureKind::ALL
.into_iter()
.zip(self.gesture_snapshot.amounts(now_seconds))
{
self.telemetry.publish_gesture(kind, amount);
}
}
out
}
}
#[inline]
fn gate_stereo(sample: (f32, f32), gain: f32) -> (f32, f32) {
(sample.0 * gain, sample.1 * gain)
}
pub(crate) fn startup_fade(current_sample: u64, sample_rate: f32) -> f32 {
(current_sample as f32 / (sample_rate * STARTUP_FADE_SECONDS)).min(1.0)
}
struct VoiceMix {
pad: f32,
perc: f32,
kick: f32,
tonal: f32,
clap: f32,
bass: f32,
arp: f32,
lead: f32,
}
impl VoiceMix {
#[inline]
fn sum(&self, fade: f32) -> f32 {
(self.pad
+ self.perc * 0.6
+ self.kick * 0.7
+ self.tonal
+ self.clap * 0.65
+ self.bass * 0.75
+ self.arp
+ self.lead)
* fade
}
#[inline]
fn weighted(&self, fade: f32) -> [f32; TAB_COUNT] {
let mut out = [0.0; TAB_COUNT];
out[Tab::Chords as usize] = self.pad * fade;
out[Tab::Perc as usize] = self.perc * 0.6 * fade;
out[Tab::Bass as usize] = self.bass * 0.75 * fade;
out[Tab::Kick as usize] = self.kick * 0.7 * fade;
out[Tab::Tonal as usize] = self.tonal * fade;
out[Tab::Clap as usize] = self.clap * 0.65 * fade;
out[Tab::Arp as usize] = self.arp * fade;
out[Tab::Lead as usize] = self.lead * fade;
out
}
}
#[derive(Clone, Copy)]
pub(crate) struct EasedRamp {
pub(crate) start: f32,
pub(crate) current: f32,
pub(crate) target: f32,
pub(crate) samples_total: u32,
pub(crate) samples_remaining: u32,
}
impl EasedRamp {
pub(crate) fn settled(value: f32) -> Self {
Self {
start: value,
current: value,
target: value,
samples_total: 0,
samples_remaining: 0,
}
}
pub(crate) fn retarget(&mut self, target: f32, ramp_samples: u32) {
self.start = self.current;
self.target = target;
self.samples_total = ramp_samples.max(1);
self.samples_remaining = self.samples_total;
}
pub(crate) fn next(&mut self) -> f32 {
if self.samples_remaining == 0 {
self.current = self.target;
return self.current;
}
let elapsed = self.samples_total - self.samples_remaining + 1;
let eased = smoothstep(elapsed as f32 / self.samples_total as f32);
self.current = self.start + (self.target - self.start) * eased;
self.samples_remaining -= 1;
if self.samples_remaining == 0 {
self.current = self.target;
}
self.current
}
}
struct OutputGates {
gates: [EasedRamp; TAB_COUNT],
}
impl OutputGates {
fn gain(muted: bool) -> f32 {
if muted { 0.0 } else { 1.0 }
}
fn new(muted: &MuteState) -> Self {
Self {
gates: std::array::from_fn(|index| EasedRamp::settled(Self::gain(muted[index]))),
}
}
fn set_targets(&mut self, muted: &MuteState, sample_rate: f32) {
let ramp_samples = (LEVEL_RAMP_MS * 0.001 * sample_rate).round() as u32;
for (gate, muted) in self.gates.iter_mut().zip(muted) {
let target = Self::gain(*muted);
if target != gate.target {
gate.retarget(target, ramp_samples);
}
}
}
fn next(&mut self) -> [f32; TAB_COUNT] {
std::array::from_fn(|index| self.gates[index].next())
}
}
pub(crate) struct GainSmoother {
pub(crate) spec: &'static ControlSpec,
pub(crate) ramp: EasedRamp,
pub(crate) idle: bool,
}
impl GainSmoother {
#[cfg(test)]
pub(crate) fn new(value: f32) -> Self {
let spec = all_specs()
.find(|spec| spec.kind.smooths_audio())
.expect("the registry declares at least one gain control");
Self::for_spec(spec, value)
}
pub(crate) fn for_spec(spec: &'static ControlSpec, value: f32) -> Self {
Self {
spec,
ramp: EasedRamp::settled(value),
idle: false,
}
}
pub(crate) fn set_target(&mut self, target: f32, ramp_samples: u32) {
if (target - self.ramp.target).abs() <= f32::EPSILON {
return;
}
self.ramp.retarget(target, ramp_samples);
}
pub(crate) fn next(&mut self) -> f32 {
self.ramp.next()
}
}
pub(crate) struct GainSmoothers {
pub(crate) smoothers: Vec<GainSmoother>,
}
impl GainSmoothers {
pub(crate) fn new(c: &FluidControls) -> Self {
let mut seen = BTreeSet::new();
let smoothers = all_specs()
.filter(|spec| spec.kind.smooths_audio())
.filter(|spec| seen.insert(spec.id))
.map(|spec| GainSmoother::for_spec(spec, (spec.get)(c)))
.collect();
Self { smoothers }
}
pub(crate) fn set_targets(&mut self, c: &FluidControls, sample_rate: f32) {
let ramp_samples = (LEVEL_RAMP_MS * 0.001 * sample_rate).round() as u32;
for smoother in &mut self.smoothers {
let snapshot_value = (smoother.spec.get)(c);
smoother.set_target(snapshot_value, ramp_samples);
smoother.idle =
smoother.ramp.samples_remaining == 0 && smoother.ramp.target == snapshot_value;
}
}
pub(crate) fn next_controls(&mut self, c: &FluidControls) -> FluidControls {
let mut next = c.clone();
for smoother in &mut self.smoothers {
if smoother.idle {
continue;
}
(smoother.spec.set)(&mut next, smoother.next());
}
next
}
}
pub(crate) const TEMPO_SMOOTH_MS: f64 = 180.0;
pub(crate) struct TempoClock {
pub(crate) beat: f64,
pub(crate) bpm: f64,
pub(crate) sample_rate: f64,
pub(crate) smoothing_coeff: f64,
}
impl TempoClock {
pub(crate) fn new(sample_rate: f32, bpm: f32) -> Self {
let sample_rate = f64::from(sample_rate.max(1.0));
let smoothing_samples = (TEMPO_SMOOTH_MS * 0.001 * sample_rate).max(1.0);
Self {
beat: 0.0,
bpm: f64::from(bpm.clamp(MASTER_BPM_MIN, MASTER_BPM_MAX)),
sample_rate,
smoothing_coeff: 1.0 - (-1.0 / smoothing_samples).exp(),
}
}
pub(crate) fn tick(&mut self, target_bpm: f32, transport: Transport) -> TimingContext {
let target_bpm = f64::from(target_bpm.clamp(MASTER_BPM_MIN, MASTER_BPM_MAX));
self.bpm += (target_bpm - self.bpm) * self.smoothing_coeff;
let timing = TimingContext {
transport,
..TimingContext::new(self.sample_rate, self.bpm, self.beat)
};
if transport == Transport::Playing {
self.beat += self.bpm / (60.0 * self.sample_rate);
}
timing
}
}
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub(crate) enum Transport {
#[default]
Playing,
Stopped,
}
impl Transport {
pub(crate) fn toggled(self) -> Self {
match self {
Self::Playing => Self::Stopped,
Self::Stopped => Self::Playing,
}
}
}
#[derive(Clone, Copy)]
pub(crate) struct TimingContext {
pub(crate) sample_rate: f64,
pub(crate) bpm: f64,
pub(crate) beat: f64,
pub(crate) transport: Transport,
}
impl TimingContext {
pub(crate) fn new(sample_rate: f64, bpm: f64, beat: f64) -> Self {
Self {
sample_rate: sample_rate.max(1.0),
bpm: bpm.max(1.0),
beat,
transport: Transport::Playing,
}
}
#[cfg(test)]
pub(crate) fn samples_per_beat(self) -> f64 {
self.sample_rate * 60.0 / self.bpm
}
#[cfg(test)]
pub(crate) fn beats_to_samples(self, beats: f32) -> u64 {
(f64::from(beats.max(0.0)) * self.samples_per_beat())
.round()
.max(1.0) as u64
}
}
const SWING_MAX_INTERVAL_BEATS: f64 = 1.0;
const SWING_MAX_FRACTION: f64 = 0.5;
#[derive(Clone, Copy, Debug, PartialEq)]
pub(crate) struct GridSpec {
pub(crate) interval_beats: f64,
pub(crate) offset_beats: f64,
swing_delay_beats: f64,
}
impl GridSpec {
pub(crate) fn new(interval_beats: f32, offset_beats: f32, swing: f32) -> Self {
let interval_beats = f64::from(interval_beats).max(1.0 / 64.0);
let swing_fraction = if interval_beats <= SWING_MAX_INTERVAL_BEATS {
f64::from(swing.clamp(0.0, 1.0)) * SWING_MAX_FRACTION
} else {
0.0
};
Self {
interval_beats,
offset_beats: f64::from(offset_beats).rem_euclid(interval_beats),
swing_delay_beats: swing_fraction * interval_beats,
}
}
fn swung_beat(self, slot: u64) -> f64 {
let base = self.offset_beats + slot as f64 * self.interval_beats;
if slot % 2 == 1 {
base + self.swing_delay_beats
} else {
base
}
}
pub(crate) fn hit_at_or_after(self, beat: f64) -> GridHit {
if beat <= self.offset_beats {
return GridHit {
beat: self.offset_beats,
};
}
let est = ((beat - self.offset_beats) / self.interval_beats)
.floor()
.max(0.0) as u64;
let mut slot = est.saturating_sub(1);
loop {
let hit = self.swung_beat(slot);
if hit >= beat {
return GridHit { beat: hit };
}
slot += 1;
}
}
pub(crate) fn hit_after(self, beat: f64) -> GridHit {
self.hit_at_or_after(beat + GRID_BEAT_EPSILON)
}
}
#[cfg(test)]
mod grid_swing_tests {
use super::*;
#[test]
fn straight_grid_hits_land_on_even_subdivisions() {
let grid = GridSpec::new(0.5, 0.0, 0.0);
assert_eq!(grid.hit_at_or_after(0.0).beat, 0.0);
assert_eq!(grid.hit_at_or_after(0.1).beat, 0.5);
assert_eq!(grid.hit_at_or_after(0.5).beat, 0.5);
assert_eq!(grid.hit_at_or_after(0.6).beat, 1.0);
}
#[test]
fn swing_delays_odd_slots_only_and_stays_ordered() {
let grid = GridSpec::new(0.5, 0.0, 1.0);
assert_eq!(grid.hit_at_or_after(0.0).beat, 0.0); assert!((grid.hit_at_or_after(0.1).beat - 0.75).abs() < 1e-9); assert_eq!(grid.hit_at_or_after(0.8).beat, 1.0); assert!(grid.hit_at_or_after(0.0).beat < grid.hit_at_or_after(0.1).beat);
assert!(grid.hit_at_or_after(0.1).beat < grid.hit_at_or_after(0.8).beat);
}
#[test]
fn chord_rate_grids_never_swing() {
let straight = GridSpec::new(4.0, 0.0, 0.0);
let asked_to_swing = GridSpec::new(4.0, 0.0, 1.0);
assert_eq!(straight, asked_to_swing);
assert_eq!(asked_to_swing.hit_at_or_after(4.1).beat, 8.0);
}
}
pub(crate) const GRID_BEAT_EPSILON: f64 = 1e-9;
#[derive(Clone, Copy, Debug)]
pub(crate) struct GridHit {
pub(crate) beat: f64,
}
pub(crate) struct GridTrigger {
pub(crate) spec: Option<GridSpec>,
pub(crate) next_hit: Option<GridHit>,
last_hit_beat: Option<f64>,
}
impl GridTrigger {
pub(crate) fn new() -> Self {
Self {
spec: None,
next_hit: None,
last_hit_beat: None,
}
}
#[cfg(test)]
pub(crate) fn pop(
&mut self,
timing: TimingContext,
interval_beats: f32,
offset_beats: f32,
) -> bool {
self.pop_swung(timing, interval_beats, offset_beats, 0.0)
}
fn earliest_hit(&self, spec: GridSpec, beat: f64) -> f64 {
let floor = self
.last_hit_beat
.map_or(f64::NEG_INFINITY, |b| b + spec.interval_beats * 0.5);
(beat + GRID_BEAT_EPSILON).max(floor)
}
pub(crate) fn pop_swung(
&mut self,
timing: TimingContext,
interval_beats: f32,
offset_beats: f32,
swing: f32,
) -> bool {
if timing.transport == Transport::Stopped {
return false;
}
let spec = GridSpec::new(interval_beats, offset_beats, swing);
if self.spec != Some(spec) {
self.spec = Some(spec);
match self.next_hit {
None => self.next_hit = Some(spec.hit_at_or_after(timing.beat)),
Some(hit) => {
let candidate = spec.hit_at_or_after(self.earliest_hit(spec, timing.beat));
if candidate.beat < hit.beat {
self.next_hit = Some(candidate);
}
}
}
}
let Some(next_hit) = self.next_hit else {
return false;
};
if timing.beat + GRID_BEAT_EPSILON >= next_hit.beat {
self.last_hit_beat = Some(next_hit.beat);
self.next_hit = Some(spec.hit_at_or_after(self.earliest_hit(spec, timing.beat)));
true
} else {
false
}
}
}
pub(crate) struct MasterBus {
pub(crate) tone_l: f32,
pub(crate) tone_r: f32,
}
impl MasterBus {
pub(crate) fn new(_c: &MasterControls, _sample_rate: f32) -> Self {
Self {
tone_l: 0.0,
tone_r: 0.0,
}
}
pub(crate) fn set_controls(&mut self, _c: &MasterControls, _sample_rate: f32) {}
pub(crate) fn process(&mut self, mut l: f32, mut r: f32, c: &MasterControls) -> (f32, f32) {
if c.tone.abs() > 0.01 {
let coeff = (0.05 + c.tone.abs() * 0.7).min(0.99);
self.tone_l += coeff * (l - self.tone_l);
self.tone_r += coeff * (r - self.tone_r);
if c.tone > 0.0 {
l += (l - self.tone_l) * c.tone * 0.6;
r += (r - self.tone_r) * c.tone * 0.6;
} else {
l += self.tone_l * (-c.tone) * 0.6;
r += self.tone_r * (-c.tone) * 0.6;
}
}
(
(l * c.level).clamp(-0.95, 0.95),
(r * c.level).clamp(-0.95, 0.95),
)
}
}