use super::widget::DialScale;
use super::*;
use crate::fx::filter::FilterType;
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub(crate) enum Tab {
Chords = 0,
Perc = 1,
Bass = 2,
Kick = 3,
Tonal = 4,
Clap = 5,
Arp = 6,
Lead = 7,
Master = 8,
}
pub(crate) const TAB_COUNT: usize = 9;
const TAB_META: [(Tab, &str, Option<&str>, &[ControlSpec]); TAB_COUNT] = [
(Tab::Chords, "Pads", Some("pad.level"), CHORDS_CONTROLS),
(Tab::Perc, "Perc", Some("perc.level"), PERC_CONTROLS),
(Tab::Bass, "Bass", Some("bass.level"), BASS_CONTROLS),
(Tab::Kick, "Kick", Some("kick.level"), KICK_CONTROLS),
(Tab::Tonal, "Tonal", Some("tonal.level"), TONAL_CONTROLS),
(Tab::Clap, "Clap", Some("clap.level"), CLAP_CONTROLS),
(Tab::Arp, "Arp", Some("arp.gain"), ARP_CONTROLS),
(Tab::Lead, "Lead", Some("lead.level"), LEAD_CONTROLS),
(Tab::Master, "Master", Some("master.level"), MASTER_CONTROLS),
];
impl Tab {
pub(crate) fn all() -> [Tab; TAB_COUNT] {
TAB_META.map(|(tab, _, _, _)| tab)
}
pub(crate) fn name(self) -> &'static str {
TAB_META[self as usize].1
}
#[cfg(test)]
pub(crate) fn previous(self) -> Self {
let all = Self::all();
all[(self as usize + all.len() - 1) % all.len()]
}
pub(crate) fn level_id(self) -> Option<&'static str> {
TAB_META[self as usize].2
}
pub(crate) const fn mute_bit(self) -> usize {
match self {
Tab::Chords => 0,
Tab::Perc => 1,
Tab::Bass => 2,
Tab::Kick => 3,
Tab::Tonal => 4,
Tab::Clap => 5,
Tab::Arp => 6,
Tab::Master => 7,
Tab::Lead => 8,
}
}
}
pub(crate) const MUTE_BYTES: usize = {
let mut highest = 0;
let mut index = 0;
while index < TAB_COUNT {
let bit = TAB_META[index].0.mute_bit();
if bit > highest {
highest = bit;
}
index += 1;
}
highest / 8 + 1
};
pub(crate) struct ControlItem {
pub(crate) id: &'static str,
pub(crate) label: String,
pub(crate) kind: ControlKind,
pub(crate) value: f32,
pub(crate) min: f32,
pub(crate) max: f32,
pub(crate) step: Step,
pub(crate) taper: Taper,
pub(crate) display: String,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub(crate) enum ControlKind {
Gain,
Continuous,
Timing,
Discrete,
}
impl ControlKind {
pub(crate) fn smooths_audio(self) -> bool {
matches!(self, Self::Gain)
}
}
pub(crate) type GetFn = fn(&FluidControls) -> f32;
pub(crate) type SetFn = fn(&mut FluidControls, f32);
pub(crate) type DisplayFn = fn(&FluidControls) -> String;
#[derive(Clone, Copy, Debug, PartialEq)]
pub(crate) enum Step {
Linear(f32),
PowerOfTwo,
BeatGrid,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub(crate) enum Entry {
Percent,
BeatsAsBars,
Round,
Snap,
Free,
}
pub(crate) const TAPER_STEPS_PER_SWEEP: f32 = 48.0;
pub(crate) const TIME_TAPER: f32 = 3.0;
#[derive(Clone, Copy, Debug, PartialEq)]
pub(crate) enum Taper {
Linear,
Log2,
Exp(f32),
}
impl Taper {
pub(crate) fn forward(self, v: f32) -> f32 {
match self {
Self::Linear => v,
Self::Log2 => v.log2(),
Self::Exp(n) => v.max(0.0).powf(1.0 / n),
}
}
pub(crate) fn inverse(self, t: f32) -> f32 {
match self {
Self::Linear => t,
Self::Log2 => t.exp2(),
Self::Exp(n) => t.max(0.0).powf(n),
}
}
pub(crate) fn ratio(self, value: f32, min: f32, max: f32) -> f32 {
let (lo, hi) = (self.forward(min), self.forward(max));
let span = hi - lo;
if span.abs() <= f32::EPSILON {
0.0
} else {
((self.forward(value) - lo) / span).clamp(0.0, 1.0)
}
}
pub(crate) fn value_at(self, ratio: f32, min: f32, max: f32) -> f32 {
let (lo, hi) = (self.forward(min), self.forward(max));
self.inverse(lo + ratio.clamp(0.0, 1.0) * (hi - lo))
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub(crate) enum TimeBase {
None,
Beats,
Ms,
}
pub(crate) fn beats_to_ms(beats: f32, bpm: f32) -> f32 {
beats * 60_000.0 / bpm.max(1.0)
}
pub(crate) fn ms_to_beats(ms: f32, bpm: f32) -> f32 {
ms * bpm.max(1.0) / 60_000.0
}
pub(crate) fn convert_time_base(value: f32, from: TimeBase, to: TimeBase, bpm: f32) -> f32 {
match (from, to) {
(TimeBase::Beats, TimeBase::Ms) => beats_to_ms(value, bpm),
(TimeBase::Ms, TimeBase::Beats) => ms_to_beats(value, bpm),
_ => value,
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub(crate) enum LfoSnap {
None,
PowerOfTwo,
Step,
}
#[derive(Clone, Copy)]
pub(crate) struct ControlSpec {
pub(crate) id: &'static str,
pub(crate) label: &'static str,
pub(crate) kind: ControlKind,
pub(crate) min: f32,
pub(crate) max: f32,
pub(crate) step: Step,
pub(crate) entry: Entry,
pub(crate) reset: f32,
pub(crate) taper: Taper,
pub(crate) lfo_snap: LfoSnap,
pub(crate) time_base: TimeBase,
pub(crate) exact_in_song: bool,
pub(crate) get: GetFn,
pub(crate) set: SetFn,
pub(crate) display: DisplayFn,
pub(crate) label_of: Option<DisplayFn>,
}
impl ControlSpec {
#[allow(clippy::too_many_arguments)]
pub(crate) const fn new(
id: &'static str,
label: &'static str,
kind: ControlKind,
min: f32,
max: f32,
step: Step,
entry: Entry,
get: GetFn,
set: SetFn,
display: DisplayFn,
) -> Self {
Self {
id,
label,
kind,
min,
max,
step,
entry,
reset: min,
taper: Taper::Linear,
lfo_snap: LfoSnap::None,
time_base: TimeBase::None,
exact_in_song: false,
get,
set,
display,
label_of: None,
}
}
pub(crate) const fn gain(
id: &'static str,
label: &'static str,
min: f32,
max: f32,
get: GetFn,
set: SetFn,
display: DisplayFn,
) -> Self {
Self::new(
id,
label,
ControlKind::Gain,
min,
max,
Step::Linear(0.02),
Entry::Percent,
get,
set,
display,
)
}
pub(crate) const fn labeled_by(mut self, label_of: DisplayFn) -> Self {
self.label_of = Some(label_of);
self
}
pub(crate) const fn with_step(mut self, step: f32) -> Self {
self.step = Step::Linear(step);
self
}
pub(crate) const fn reset_at(mut self, reset: f32) -> Self {
self.reset = reset;
self
}
pub(crate) const fn taper(mut self, taper: Taper) -> Self {
self.taper = taper;
self
}
pub(crate) const fn lfo_snap(mut self, snap: LfoSnap) -> Self {
self.lfo_snap = snap;
self
}
pub(crate) const fn in_beats(mut self) -> Self {
self.time_base = TimeBase::Beats;
self
}
pub(crate) const fn in_ms(mut self) -> Self {
self.time_base = TimeBase::Ms;
self
}
pub(crate) const fn exact_in_song(mut self) -> Self {
self.exact_in_song = true;
self
}
pub(crate) fn contextual(&self, c: &FluidControls) -> Self {
let Some((slot, field)) = module_slot_row(self.id, c) else {
return *self;
};
let Some(kind) = slot.kind() else {
return *self;
};
let mut spec = *self;
match (kind.family, field) {
(Family::Delay, ModuleSlotField::Time | ModuleSlotField::RightTime) => {
let clock = if field == ModuleSlotField::RightTime {
DelayClock::from_value(slot.right_clock)
} else {
DelayClock::from_value(slot.clock)
};
spec.kind = ControlKind::Timing;
spec.entry = Entry::Free;
spec.taper = Taper::Linear;
match clock {
DelayClock::Sync => {
spec.min = DELAY_SYNC_MIN_BEATS;
spec.max = DELAY_SYNC_MAX_BEATS;
spec.step = Step::BeatGrid;
}
DelayClock::Free => {
spec.min = DELAY_FREE_MIN_MS;
spec.max = DELAY_FREE_MAX_MS;
spec.step = Step::Linear(10.0);
}
}
spec.reset = spec.min;
}
(Family::Delay, ModuleSlotField::Feedback) => {
spec.max = 0.95;
}
(Family::Reverb, ModuleSlotField::Time | ModuleSlotField::Feedback) => {
spec.kind = ControlKind::Continuous;
spec.min = 0.0;
spec.max = 1.0;
spec.step = Step::Linear(0.01);
spec.entry = Entry::Percent;
spec.reset = if field == ModuleSlotField::Time {
0.72
} else {
0.45
};
}
(Family::Compression, ModuleSlotField::Time) => {
spec.kind = ControlKind::Continuous;
spec.min = -40.0;
spec.max = 0.0;
spec.step = Step::Linear(1.0);
spec.entry = Entry::Round;
spec.reset = -8.0;
}
(Family::Compression, ModuleSlotField::RightTime) => {
spec.kind = ControlKind::Continuous;
spec.min = 1.0;
spec.max = 8.0;
spec.step = Step::Linear(0.25);
spec.entry = Entry::Snap;
spec.reset = 2.0;
}
(Family::Compression, ModuleSlotField::Feedback) => {
spec.kind = ControlKind::Timing;
spec.min = 10.0;
spec.max = 500.0;
spec.step = Step::Linear(1.0);
spec.entry = Entry::Round;
spec.reset = 100.0;
}
(Family::Compression, ModuleSlotField::Vintage) => {
spec.min = 0.0;
spec.max = 12.0;
spec.step = Step::Linear(0.5);
spec.entry = Entry::Free;
spec.reset = 2.0;
}
(Family::Filter, ModuleSlotField::Time) => {
spec.kind = ControlKind::Continuous;
spec.min = 80.0;
spec.max = 8_000.0;
spec.step = Step::Linear(1.0);
spec.entry = Entry::Round;
spec.taper = Taper::Log2;
spec.reset = 8_000.0;
}
(Family::Filter, ModuleSlotField::RightTime) => {
spec.kind = ControlKind::Continuous;
spec.min = 0.0;
spec.max = 1.0;
spec.step = Step::Linear(0.01);
spec.entry = Entry::Percent;
spec.reset = 0.0;
}
(Family::Filter, ModuleSlotField::Feedback) => {
spec.kind = ControlKind::Discrete;
spec.min = 0.0;
spec.max = 2.0;
spec.step = Step::Linear(1.0);
spec.entry = Entry::Round;
spec.reset = 0.0;
}
_ => {}
}
spec
}
pub(crate) fn item(&self, c: &FluidControls) -> ControlItem {
let spec = self.contextual(c);
ControlItem {
id: spec.id,
label: spec
.label_of
.map_or_else(|| spec.label.to_string(), |resolve| resolve(c)),
kind: spec.kind,
value: (spec.get)(c),
min: spec.min,
max: spec.max,
step: spec.step,
taper: spec.taper,
display: (spec.display)(c),
}
}
pub(crate) fn apply_delta(&self, dir: f32, c: &mut FluidControls) {
let spec = self.contextual(c);
let value = (spec.get)(c);
let next = if spec.is_continuous_tapered() {
spec.scale()
.step_in_position(value, dir, TAPER_STEPS_PER_SWEEP)
.expect("a Linear step maps to a Tapered scale, which has an inverse")
.clamp(spec.min, spec.max)
} else {
match spec.step {
Step::Linear(step) => (value + dir * step).clamp(spec.min, spec.max),
Step::PowerOfTwo => {
if dir > 0.0 {
(value * 2.0).min(spec.max)
} else {
(value / 2.0).max(spec.min)
}
}
Step::BeatGrid => beat_grid_adjust(value, dir, spec.min, spec.max),
}
};
(spec.set)(c, next);
}
pub(crate) fn ratio(&self, value: f32, c: &FluidControls) -> f32 {
self.contextual(c).scale().ratio(value)
}
pub(crate) fn scale(&self) -> DialScale {
DialScale::from_step(self.min, self.max, self.step, self.taper)
}
fn is_continuous_tapered(&self) -> bool {
!matches!(self.taper, Taper::Linear) && matches!(self.step, Step::Linear(_))
}
pub(crate) fn apply_reset(&self, c: &mut FluidControls) {
let spec = self.contextual(c);
(spec.set)(c, spec.reset);
}
pub(crate) fn apply_value(&self, value: f32, c: &mut FluidControls) {
let spec = self.contextual(c);
let next = match spec.entry {
Entry::Percent if parse_module_slot_id(spec.id).is_some() => {
normalize_unit_input(value)
}
Entry::Percent => normalize_unit_input(value) * spec.max,
Entry::BeatsAsBars => nearest_power_of_two(value / 4.0, spec.min, spec.max),
Entry::Round => value.round(),
Entry::Snap => spec.snap_on_grid(value),
Entry::Free => value,
};
(spec.set)(c, next.clamp(spec.min, spec.max));
}
pub(crate) fn apply_ratio(&self, ratio: f32, c: &mut FluidControls) {
let spec = self.contextual(c);
let ratio = ratio.clamp(0.0, 1.0);
let value = match spec.scale().value_at(ratio) {
Some(value) => spec.quantize(value),
None => {
let rungs = spec.rungs(c);
let index = ((ratio * rungs.len() as f32) as usize).min(rungs.len() - 1);
rungs[index]
}
};
(spec.set)(c, value);
}
fn rungs(&self, c: &FluidControls) -> Vec<f32> {
let mut scratch = c.clone();
(self.set)(&mut scratch, self.min);
let mut rungs = vec![self.quantize(self.min)];
loop {
self.apply_delta(1.0, &mut scratch);
let next = (self.get)(&scratch);
if rungs.last().is_some_and(|last| next <= *last) {
return rungs;
}
rungs.push(next);
}
}
pub(crate) fn quantized_value(&self, c: &FluidControls) -> f32 {
let spec = self.contextual(c);
spec.quantize((spec.get)(c))
}
pub(crate) fn apply_quantized_value(&self, value: f32, c: &mut FluidControls) {
let spec = self.contextual(c);
(spec.set)(c, spec.quantize(value));
}
pub(crate) fn apply_raw(&self, value: f32, c: &mut FluidControls) {
let spec = self.contextual(c);
(spec.set)(c, value.clamp(spec.min, spec.max));
}
pub(crate) fn quantize(&self, value: f32) -> f32 {
let clamped = value.clamp(self.min, self.max);
if self.is_continuous_tapered() {
return clamped;
}
self.snap_on_grid(clamped)
}
fn snap_on_grid(&self, v: f32) -> f32 {
let clamped = v.clamp(self.min, self.max);
match self.step {
Step::Linear(step) => snap_step(clamped, step).clamp(self.min, self.max),
Step::PowerOfTwo => nearest_power_of_two(clamped, self.min, self.max),
Step::BeatGrid => beat_grid_snap(clamped, self.min, self.max),
}
}
}
pub(crate) fn pct(v: f32) -> String {
format!("{:.0}%", v * 100.0)
}
pub(crate) fn signed_pct(v: f32) -> String {
format!("{:+.0}%", v * 100.0)
}
pub(crate) fn beats2(v: f32) -> String {
format!("{v:.2} beats")
}
pub(crate) fn secs(seconds: f32) -> String {
if seconds < 1.0 {
format!("{:.0} ms", seconds * 1000.0)
} else {
format!("{seconds:.2} s")
}
}
macro_rules! gain_pct {
($id:literal, $label:literal, $min:literal, $max:literal, $($f:tt)+) => {
ControlSpec::gain(
$id,
$label,
$min,
$max,
|c| c.$($f)+,
|c, v| c.$($f)+ = v,
|c| pct(c.$($f)+),
)
};
($id:literal, $label:literal, $($f:tt)+) => {
ControlSpec::gain(
$id,
$label,
0.0,
1.0,
|c| c.$($f)+,
|c, v| c.$($f)+ = v,
|c| pct(c.$($f)+),
)
};
}
macro_rules! time_secs {
($id:literal, $label:literal, $min:expr, $max:expr, $step:expr, $($f:tt)+) => {
ControlSpec::new(
$id,
$label,
ControlKind::Timing,
$min,
$max,
Step::Linear($step),
Entry::Free,
|c| c.$($f)+,
|c, v| c.$($f)+ = v,
|c| secs(c.$($f)+),
)
.taper(Taper::Exp(TIME_TAPER))
};
}
macro_rules! time_ms {
($id:literal, $label:literal, $min:expr, $max:expr, $step:expr, $($f:tt)+) => {
ControlSpec::new(
$id,
$label,
ControlKind::Timing,
$min,
$max,
Step::Linear($step),
Entry::Free,
|c| c.$($f)+,
|c, v| c.$($f)+ = v,
|c| secs(c.$($f)+ / 1000.0),
)
.taper(Taper::Exp(TIME_TAPER))
.in_ms()
};
}
macro_rules! beat_interval {
($id:literal, $label:literal, $min:expr, $max:expr, $($f:tt)+) => {
ControlSpec::new(
$id,
$label,
ControlKind::Timing,
$min,
$max,
Step::BeatGrid,
Entry::Snap,
|c| c.$($f)+,
|c, v| c.$($f)+ = v,
|c| beats2(c.$($f)+),
)
.lfo_snap(LfoSnap::PowerOfTwo)
.in_beats()
};
}
macro_rules! beat_offset {
($id:literal, $label:literal, $max:expr, $($f:tt)+) => {
ControlSpec::new(
$id,
$label,
ControlKind::Timing,
0.0,
$max,
Step::BeatGrid,
Entry::Snap,
|c| c.$($f)+,
|c, v| c.$($f)+ = v,
|c| beats2(c.$($f)+),
)
.lfo_snap(LfoSnap::Step)
.in_beats()
};
}
fn time_row_label(kind_value: f32) -> String {
let Some(kind) = module_kind_at(kind_value) else {
return "Time".to_string();
};
if kind.collapsed_field() == ModuleSlotField::Time {
return module_kind_label(kind_value);
}
kind.parameters()
.iter()
.find(|p| p.field == ModuleSlotField::Time)
.map_or_else(|| "Time".to_string(), |p| p.label.to_string())
}
macro_rules! module_slot_rows {
($layer:ident, $prefix:literal, $slot:literal) => {
[
ControlSpec::new(
concat!($prefix, ".slot", $slot, ".kind"),
concat!("Slot ", $slot),
ControlKind::Discrete,
MODULE_EMPTY,
module_kind_max(),
Step::Linear(1.0),
Entry::Round,
|c| c.modules.$layer[$slot - 1].kind,
|c, v| c.modules.$layer[$slot - 1].kind = v,
|c| module_kind_label(c.modules.$layer[$slot - 1].kind),
)
.reset_at(MODULE_EMPTY),
ControlSpec::new(
concat!($prefix, ".slot", $slot, ".amount"),
concat!("Slot ", $slot, " Amount"),
ControlKind::Gain,
0.0,
1.0,
Step::Linear(0.01),
Entry::Percent,
|c| c.modules.$layer[$slot - 1].amount,
|c, v| c.modules.$layer[$slot - 1].amount = v,
|c| pct(c.modules.$layer[$slot - 1].amount),
)
.labeled_by(|c| module_kind_label(c.modules.$layer[$slot - 1].kind))
.reset_at(0.0),
ControlSpec::new(
concat!($prefix, ".slot", $slot, ".time"),
concat!("Slot ", $slot, " Time"),
ControlKind::Continuous,
0.0,
2_000.0,
Step::Linear(0.01),
Entry::Free,
|c| c.modules.$layer[$slot - 1].time,
|c, v| c.modules.$layer[$slot - 1].time = v,
|c| format!("{:.0}", c.modules.$layer[$slot - 1].time),
)
.labeled_by(|c| time_row_label(c.modules.$layer[$slot - 1].kind))
.reset_at(0.0)
.exact_in_song(),
ControlSpec::new(
concat!($prefix, ".slot", $slot, ".right_time"),
concat!("Slot ", $slot, " Right Time"),
ControlKind::Continuous,
0.0,
2_000.0,
Step::Linear(0.01),
Entry::Free,
|c| c.modules.$layer[$slot - 1].right_time,
|c, v| c.modules.$layer[$slot - 1].right_time = v,
|c| format!("{:.0}", c.modules.$layer[$slot - 1].right_time),
)
.reset_at(0.0)
.exact_in_song(),
ControlSpec::new(
concat!($prefix, ".slot", $slot, ".clock"),
concat!("Slot ", $slot, " Clock"),
ControlKind::Discrete,
0.0,
1.0,
Step::Linear(1.0),
Entry::Round,
|c| c.modules.$layer[$slot - 1].clock,
|c, v| c.modules.$layer[$slot - 1].clock = v,
|c| {
DelayClock::from_value(c.modules.$layer[$slot - 1].clock)
.label()
.to_string()
},
)
.reset_at(DelayClock::Sync.value()),
ControlSpec::new(
concat!($prefix, ".slot", $slot, ".feedback"),
concat!("Slot ", $slot, " Feedback"),
ControlKind::Gain,
0.0,
1.0,
Step::Linear(0.01),
Entry::Percent,
|c| c.modules.$layer[$slot - 1].feedback,
|c, v| {
let slot = &mut c.modules.$layer[$slot - 1];
slot.feedback = if slot.kind().is_some_and(|kind| kind.family == Family::Delay)
{
v.min(0.95)
} else {
v
};
},
|c| pct(c.modules.$layer[$slot - 1].feedback),
)
.reset_at(0.0)
.exact_in_song(),
ControlSpec::new(
concat!($prefix, ".slot", $slot, ".vintage"),
concat!("Slot ", $slot, " Vintage"),
ControlKind::Gain,
0.0,
1.0,
Step::Linear(0.01),
Entry::Percent,
|c| c.modules.$layer[$slot - 1].vintage,
|c, v| c.modules.$layer[$slot - 1].vintage = v,
|c| pct(c.modules.$layer[$slot - 1].vintage),
)
.reset_at(0.0)
.exact_in_song(),
ControlSpec::new(
concat!($prefix, ".slot", $slot, ".right_clock"),
concat!("Slot ", $slot, " Right Clock"),
ControlKind::Discrete,
0.0,
1.0,
Step::Linear(1.0),
Entry::Round,
|c| c.modules.$layer[$slot - 1].right_clock,
|c, v| c.modules.$layer[$slot - 1].right_clock = v,
|c| {
DelayClock::from_value(c.modules.$layer[$slot - 1].right_clock)
.label()
.to_string()
},
)
.reset_at(DelayClock::Sync.value()),
]
};
}
macro_rules! chord_slot_rows {
($slot:literal) => {
[
ControlSpec::new(
concat!("pad.chord", $slot, "_degree"),
concat!("Chord ", $slot, " Root"),
ControlKind::Discrete,
-7.0,
7.0,
Step::Linear(1.0),
Entry::Round,
|c| c.pad.chord_slots[$slot - 1].degree,
|c, v| c.pad.chord_slots[$slot - 1].degree = v,
|c| format!("{:+.0}", c.pad.chord_slots[$slot - 1].degree),
)
.reset_at(0.0),
ControlSpec::new(
concat!("pad.chord", $slot, "_accidental"),
concat!("Chord ", $slot, " Accidental"),
ControlKind::Discrete,
-1.0,
1.0,
Step::Linear(1.0),
Entry::Round,
|c| c.pad.chord_slots[$slot - 1].accidental,
|c, v| c.pad.chord_slots[$slot - 1].accidental = v,
|c| match c.pad.chord_slots[$slot - 1].accidental.round() as i32 {
-1 => "b".to_string(),
1 => "#".to_string(),
_ => "natural".to_string(),
},
)
.reset_at(0.0),
ControlSpec::new(
concat!("pad.chord", $slot, "_quality"),
concat!("Chord ", $slot, " Quality"),
ControlKind::Discrete,
-1.0,
1.0,
Step::Linear(1.0),
Entry::Round,
|c| c.pad.chord_slots[$slot - 1].quality,
|c, v| c.pad.chord_slots[$slot - 1].quality = v,
|c| {
let slot = &c.pad.chord_slots[$slot - 1];
let sound = if pad_chord_slot_is_minor(slot) {
"min"
} else {
"maj"
};
match slot.quality.round() as i32 {
0 => format!("scale ({sound})"),
_ => sound.to_string(),
}
},
)
.reset_at(0.0),
ControlSpec::new(
concat!("pad.chord", $slot, "_extension"),
concat!("Chord ", $slot, " Extension"),
ControlKind::Discrete,
0.0,
3.0,
Step::Linear(1.0),
Entry::Round,
|c| c.pad.chord_slots[$slot - 1].extension,
|c, v| c.pad.chord_slots[$slot - 1].extension = v,
|c| format!("{:.0}", c.pad.chord_slots[$slot - 1].extension),
),
ControlSpec::new(
concat!("pad.chord", $slot, "_inversion"),
concat!("Chord ", $slot, " Inversion"),
ControlKind::Discrete,
0.0,
3.0,
Step::Linear(1.0),
Entry::Round,
|c| c.pad.chord_slots[$slot - 1].inversion,
|c, v| c.pad.chord_slots[$slot - 1].inversion = v,
|c| format!("{:.0}", c.pad.chord_slots[$slot - 1].inversion),
)
.reset_at(0.0),
]
};
}
macro_rules! layer_controls {
($layer:ident, $prefix:literal, [$($base:expr),* $(,)?]) => {
layer_controls!(@rows $layer, $prefix, [$($base),*], [], [1, 2, 3, 4, 5, 6, 7, 8])
};
(chords $layer:ident, $prefix:literal, [$($base:expr),* $(,)?]) => {
layer_controls!(
@rows $layer,
$prefix,
[$($base),*],
[1, 2, 3, 4, 5, 6, 7, 8],
[1, 2, 3, 4, 5, 6, 7, 8]
)
};
(@rows $layer:ident, $prefix:literal, [$($base:expr),*], [$($chord:literal),*], [$($slot:literal),*]) => {
[
$($base,)*
$(
chord_slot_rows!($chord)[0],
chord_slot_rows!($chord)[1],
chord_slot_rows!($chord)[2],
chord_slot_rows!($chord)[3],
chord_slot_rows!($chord)[4],
)*
$(
module_slot_rows!($layer, $prefix, $slot)[0],
module_slot_rows!($layer, $prefix, $slot)[1],
module_slot_rows!($layer, $prefix, $slot)[2],
module_slot_rows!($layer, $prefix, $slot)[3],
module_slot_rows!($layer, $prefix, $slot)[4],
module_slot_rows!($layer, $prefix, $slot)[5],
module_slot_rows!($layer, $prefix, $slot)[6],
module_slot_rows!($layer, $prefix, $slot)[7],
)*
]
};
}
const _: () = assert!(MODULE_SLOTS == 8);
const _: () = assert!(CHORD_SLOT_COUNT == 8);
pub(crate) const MASTER_CONTROLS: &[ControlSpec] = &layer_controls!(
master,
"master",
[
gain_pct!("pad.level", "Pads Vol", pad.level),
gain_pct!("perc.level", "Perc Vol", perc.level),
gain_pct!("kick.level", "Kick Vol", kick.level),
gain_pct!("tonal.level", "Tonal Vol", tonal.level),
gain_pct!("clap.level", "Clap Vol", clap.level),
gain_pct!("bass.level", "Bass Vol", bass.level),
gain_pct!("arp.gain", "Arp Vol", arp.gain),
gain_pct!("lead.level", "Lead Vol", lead.level),
ControlSpec::new(
"master.bpm",
"BPM",
ControlKind::Timing,
MASTER_BPM_MIN,
MASTER_BPM_MAX,
Step::Linear(1.0),
Entry::Round,
|c| c.master.bpm,
|c, v| c.master.bpm = v,
|c| format!("{:.0} bpm", c.master.bpm),
),
gain_pct!("master.level", "Master Level", master.level),
ControlSpec::new(
"master.tone",
"Tone",
ControlKind::Continuous,
-1.0,
1.0,
Step::Linear(0.05),
Entry::Free,
|c| c.master.tone,
|c, v| c.master.tone = v,
|c| {
if c.master.tone < -0.05 {
format!("bass {:.0}%", -c.master.tone * 100.0)
} else if c.master.tone > 0.05 {
format!("treble {:.0}%", c.master.tone * 100.0)
} else {
"flat".to_string()
}
},
),
ControlSpec::new(
"master.tune",
"Tune",
ControlKind::Discrete,
-12.0,
12.0,
Step::Linear(1.0),
Entry::Round,
|c| c.master.tune,
|c, v| c.master.tune = v,
|c| {
if c.master.tune.abs() < 0.05 {
"0 st".to_string()
} else {
format!("{:+.0} st", c.master.tune)
}
},
)
.reset_at(0.0),
]
);
pub(crate) const PERC_CONTROLS: &[ControlSpec] = &layer_controls!(
perc,
"perc",
[
gain_pct!("perc.level", "Level", perc.level),
time_ms!("perc.decay_ms", "Decay", 20.0, 2000.0, 1.0, perc.decay_ms),
ControlSpec::new(
"perc.interval_beats",
"Interval",
ControlKind::Timing,
0.125,
4.25,
Step::BeatGrid,
Entry::Snap,
|c| c.perc.interval_beats,
|c, v| c.perc.interval_beats = v,
|c| {
if c.perc.interval_beats >= 4.25 {
"Continuous".to_string()
} else {
beats2(c.perc.interval_beats)
}
},
)
.lfo_snap(LfoSnap::PowerOfTwo)
.in_beats(),
beat_offset!("perc.offset_beats", "Offset", 4.0, perc.offset_beats),
]
);
const CHORD_BASE_CONTROL_COUNT: usize = 10;
pub(crate) const CHORDS_CONTROLS: &[ControlSpec] = &layer_controls!(chords pad, "pad", [
gain_pct!("pad.level", "Level", pad.level),
time_secs!(
"pad.attack_time",
"Attack",
0.05,
30.0,
0.001,
pad.attack_time
),
time_secs!(
"pad.release_time",
"Release",
0.05,
20.0,
0.001,
pad.release_time
),
ControlSpec::new(
"pad.type",
"Type",
ControlKind::Discrete,
0.0,
last_index_of(PAD_TYPES),
Step::Linear(1.0),
Entry::Round,
|c| c.pad.voice_type,
|c, v| c.pad.voice_type = v,
|c| type_label(c.pad.voice_type, PAD_TYPES).to_string(),
),
ControlSpec::new(
"pad.chord_bars",
"Chord Length",
ControlKind::Timing,
1.0,
64.0,
Step::PowerOfTwo,
Entry::BeatsAsBars,
|c| c.pad.chord_bars,
|c, v| c.pad.chord_bars = v,
|c| format!("{:.0} beats", c.pad.chord_bars * 4.0),
)
.taper(Taper::Log2)
.lfo_snap(LfoSnap::Step),
ControlSpec::new(
"pad.chord_count",
"Chord Count",
ControlKind::Discrete,
1.0,
CHORD_SLOT_COUNT as f32,
Step::Linear(1.0),
Entry::Round,
|c| c.pad.chord_count,
|c, v| c.pad.chord_count = v,
|c| format!("{:.0}", c.pad.chord_count),
)
.reset_at(CHORD_SLOT_COUNT as f32),
ControlSpec::new(
"pad.progression",
"Progression",
ControlKind::Discrete,
0.0,
CUSTOM_PROGRESSION_INDEX as f32,
Step::Linear(1.0),
Entry::Round,
|c| c.pad.progression,
|c, v| c.pad.progression = v,
|c| {
let index = progression_index(c.pad.progression);
if is_custom_progression(index) {
"Custom".to_string()
} else {
letter_label(index)
}
},
),
gain_pct!("pad.stereo_width", "Stereo Width", pad.stereo_width),
gain_pct!("pad.detune", "Detune", pad.detune),
gain_pct!("pad.octave_mix", "Octave Mix", pad.octave_mix),
]);
pub(crate) const BASS_CONTROLS: &[ControlSpec] = &layer_controls!(
bass,
"bass",
[
gain_pct!("bass.level", "Level", bass.level),
time_secs!(
"bass.attack_time",
"Attack",
0.005,
1.0,
0.001,
bass.attack_time
),
time_secs!(
"bass.decay_time",
"Decay",
0.005,
2.0,
0.001,
bass.decay_time
),
ControlSpec::new(
"bass.type",
"Type",
ControlKind::Discrete,
0.0,
last_index_of(BASS_TYPES),
Step::Linear(1.0),
Entry::Round,
|c| c.bass.voice_type,
|c, v| c.bass.voice_type = v,
|c| type_label(c.bass.voice_type, BASS_TYPES).to_string(),
),
beat_interval!(
"bass.interval_beats",
"Interval",
0.125,
8.0,
bass.interval_beats
),
beat_offset!("bass.offset_beats", "Offset", 4.0, bass.offset_beats),
ControlSpec::new(
"bass.rhythm",
"Rhythm",
ControlKind::Discrete,
0.0,
last_index_of(&BASS_RHYTHMS),
Step::Linear(1.0),
Entry::Round,
|c| c.bass.rhythm,
|c, v| c.bass.rhythm = v,
|c| letter_label(wrapped_index(c.bass.rhythm, BASS_RHYTHMS.len())),
),
ControlSpec::new(
"bass.octave",
"Octave",
ControlKind::Discrete,
-3.0,
0.0,
Step::Linear(1.0),
Entry::Round,
|c| c.bass.octave,
|c, v| c.bass.octave = v,
|c| format!("{:.0}", c.bass.octave),
),
]
);
pub(crate) const BASS_TYPES: &[&str] = &["Sub", "Saw", "Pluck"];
pub(crate) const PAD_TYPES: &[&str] = &["Warm", "Dark", "Glass", "Choir", "Hollow", "Tape"];
pub(crate) const KICK_TYPES: &[&str] = &["Sub", "Warm", "Wood", "Felt"];
pub(crate) const TONAL_SYNTH_TYPES: &[&str] = &[
"Sine",
"Rhodes",
"Wurli",
"Felt",
"Marimba",
"Kalimba",
"Pluck",
"Dulcet",
"Cloud Keys",
"Haze",
];
pub(crate) fn wrapped_index(value: f32, len: usize) -> usize {
(value.round() as i64).rem_euclid(len as i64) as usize
}
pub(crate) fn type_label(value: f32, types: &'static [&'static str]) -> &'static str {
types[wrapped_index(value, types.len())]
}
const fn last_index_of<T>(table: &[T]) -> f32 {
(table.len() - 1) as f32
}
fn letter_label(index: usize) -> String {
char::from(b'A' + index as u8).to_string()
}
pub(crate) const KICK_CONTROLS: &[ControlSpec] = &layer_controls!(
kick,
"kick",
[
gain_pct!("kick.level", "Level", kick.level),
time_ms!(
"kick.pitch_decay_ms",
"Pitch Decay",
10.0,
300.0,
1.0,
kick.pitch_decay_ms
),
time_ms!(
"kick.amp_decay_ms",
"Amp Decay",
50.0,
1000.0,
1.0,
kick.amp_decay_ms
),
ControlSpec::new(
"kick.type",
"Type",
ControlKind::Discrete,
0.0,
last_index_of(KICK_TYPES),
Step::Linear(1.0),
Entry::Round,
|c| c.kick.voice_type,
|c, v| c.kick.voice_type = v,
|c| type_label(c.kick.voice_type, KICK_TYPES).to_string(),
),
beat_interval!(
"kick.interval_beats",
"Interval",
0.125,
4.0,
kick.interval_beats
),
beat_offset!("kick.offset_beats", "Offset", 4.0, kick.offset_beats),
ControlSpec::new(
"kick.start_freq",
"Start Freq",
ControlKind::Continuous,
40.0,
200.0,
Step::Linear(5.0),
Entry::Snap,
|c| c.kick.start_freq,
|c, v| c.kick.start_freq = v,
|c| format!("{:.0} Hz", c.kick.start_freq),
),
ControlSpec::gain(
"kick.click",
"Click",
0.0,
0.2,
|c| c.kick.click,
|c, v| c.kick.click = v,
|c| pct(c.kick.click / 0.2),
)
.with_step(0.01),
]
);
pub(crate) const TONAL_CONTROLS: &[ControlSpec] = &layer_controls!(
tonal,
"tonal",
[
gain_pct!("tonal.level", "Level", tonal.level),
time_secs!("tonal.attack", "Attack", 0.0, 1.0, 0.001, tonal.attack),
time_secs!(
"tonal.decay",
"Decay",
TONAL_DECAY_MIN,
6.0,
0.001,
tonal.decay
),
ControlSpec::new(
"tonal.synth_type",
"Type",
ControlKind::Discrete,
0.0,
last_index_of(TONAL_SYNTH_TYPES),
Step::Linear(1.0),
Entry::Round,
|c| c.tonal.synth_type,
|c, v| c.tonal.synth_type = v,
|c| type_label(c.tonal.synth_type, TONAL_SYNTH_TYPES).to_string(),
),
ControlSpec::new(
"tonal.octave",
"Octave",
ControlKind::Discrete,
-2.0,
2.0,
Step::Linear(1.0),
Entry::Round,
|c| c.tonal.octave,
|c, v| c.tonal.octave = v,
|c| format!("{:.0}", c.tonal.octave),
),
ControlSpec::new(
"tonal.phrase",
"Phrase",
ControlKind::Discrete,
0.0,
last_index_of(&TONAL_PHRASES),
Step::Linear(1.0),
Entry::Round,
|c| c.tonal.phrase,
|c, v| c.tonal.phrase = v,
|c| letter_label(wrapped_index(c.tonal.phrase, TONAL_PHRASES.len())),
),
beat_interval!(
"tonal.rate_beats",
"Rate",
TONAL_RATE_BEATS_MIN,
TONAL_RATE_BEATS_MAX,
tonal.rate_beats
),
beat_interval!(
"tonal.step_interval_beats",
"Cycle",
TONAL_CYCLE_BEATS_MIN,
TONAL_CYCLE_BEATS_MAX,
tonal.step_interval_beats
),
beat_offset!("tonal.offset_beats", "Offset", 4.0, tonal.offset_beats),
gain_pct!("tonal.randomness", "Randomness", tonal.randomness),
ControlSpec::new(
"tonal.evolve_rate",
"Evolve",
ControlKind::Continuous,
0.0,
1.0,
Step::Linear(0.05),
Entry::Percent,
|c| c.tonal.evolve_rate,
|c, v| c.tonal.evolve_rate = v,
|c| pct(c.tonal.evolve_rate),
),
]
);
pub(crate) const CLAP_CONTROLS: &[ControlSpec] = &layer_controls!(
clap,
"clap",
[
gain_pct!("clap.level", "Level", clap.level),
gain_pct!("clap.filter", "Filter", 0.5, 1.0, clap.filter),
time_ms!("clap.decay_ms", "Decay", 10.0, 200.0, 1.0, clap.decay_ms),
beat_interval!(
"clap.interval_beats",
"Interval",
0.5,
8.0,
clap.interval_beats
),
beat_offset!("clap.offset_beats", "Offset", 8.0, clap.offset_beats),
ControlSpec::new(
"clap.slap_count",
"Slap Count",
ControlKind::Discrete,
1.0,
8.0,
Step::Linear(1.0),
Entry::Round,
|c| c.clap.slap_count,
|c, v| c.clap.slap_count = v,
|c| format!("{:.0}", c.clap.slap_count),
),
time_ms!(
"clap.slap_spread_ms",
"Slap Spread",
0.0,
100.0,
1.0,
clap.slap_spread_ms
),
gain_pct!("clap.body", "Body", clap.body),
]
);
pub(crate) const ARP_CONTROLS: &[ControlSpec] = &layer_controls!(
arp,
"arp",
[
gain_pct!("arp.gain", "Level", arp.gain),
time_secs!("arp.attack", "Attack", 0.0, 1.0, 0.001, arp.attack),
time_secs!("arp.decay", "Decay", TONAL_DECAY_MIN, 6.0, 0.001, arp.decay),
ControlSpec::new(
"arp.type",
"Type",
ControlKind::Discrete,
0.0,
last_index_of(TONAL_SYNTH_TYPES),
Step::Linear(1.0),
Entry::Round,
|c| c.arp.voice_type,
|c, v| c.arp.voice_type = v,
|c| type_label(c.arp.voice_type, TONAL_SYNTH_TYPES).to_string(),
),
beat_interval!(
"arp.rate_beats",
"Rate",
ARP_RATE_BEATS_MIN,
ARP_RATE_BEATS_MAX,
arp.rate_beats
),
beat_offset!("arp.offset_beats", "Offset", 4.0, arp.offset_beats),
ControlSpec::new(
"arp.pattern",
"Pattern",
ControlKind::Discrete,
0.0,
3.0,
Step::Linear(1.0),
Entry::Round,
|c| c.arp.pattern,
|c, v| c.arp.pattern = v,
|c| arp_pattern_label(c.arp.pattern).to_string(),
),
ControlSpec::new(
"arp.octaves",
"Octaves",
ControlKind::Discrete,
ARP_OCTAVES_MIN,
ARP_OCTAVES_MAX,
Step::Linear(1.0),
Entry::Round,
|c| c.arp.octaves,
|c, v| c.arp.octaves = v,
|c| format!("{:.0}", c.arp.octaves),
),
]
);
macro_rules! lead_step_row {
($step:literal) => {
ControlSpec::new(
concat!("lead.step", $step),
concat!("Step ", $step),
ControlKind::Discrete,
0.0,
LEAD_TONE_COUNT as f32,
Step::Linear(1.0),
Entry::Round,
|c| c.lead.steps[$step - 1],
|c, v| c.lead.steps[$step - 1] = v,
|c| lead_step_label(c.lead.steps[$step - 1], &lead_page_reach(&c.lead, &c.pad)),
)
};
}
const _: () = assert!(LEAD_STEP_COUNT == 16);
pub(crate) const LEAD_CONTROLS: &[ControlSpec] = &layer_controls!(
lead,
"lead",
[
gain_pct!("lead.level", "Level", lead.level),
time_secs!("lead.attack", "Attack", 0.001, 1.0, 0.001, lead.attack),
time_secs!(
"lead.decay",
"Decay",
TONAL_DECAY_MIN,
6.0,
0.001,
lead.decay
),
time_secs!("lead.glide", "Glide", 0.0, 1.0, 0.001, lead.glide),
ControlSpec::new(
"lead.type",
"Type",
ControlKind::Discrete,
0.0,
last_index_of(&LEAD_TYPES),
Step::Linear(1.0),
Entry::Round,
|c| c.lead.voice_type,
|c, v| c.lead.voice_type = v,
|c| lead_type_label(c.lead.voice_type).to_string(),
),
ControlSpec::new(
"lead.octave",
"Octave",
ControlKind::Discrete,
LEAD_OCTAVE_MIN,
LEAD_OCTAVE_MAX,
Step::Linear(1.0),
Entry::Round,
|c| c.lead.octave,
|c, v| c.lead.octave = v,
|c| format!("{:+.0}", c.lead.octave),
)
.reset_at(0.0),
ControlSpec::new(
"lead.follow",
"Follow",
ControlKind::Discrete,
0.0,
last_index_of(&LEAD_FOLLOWS),
Step::Linear(1.0),
Entry::Round,
|c| c.lead.follow,
|c, v| c.lead.follow = v,
|c| LeadFollow::from_value(c.lead.follow).label().to_string(),
),
beat_interval!(
"lead.rate_beats",
"Rate",
LEAD_RATE_BEATS_MIN,
LEAD_RATE_BEATS_MAX,
lead.rate_beats
),
beat_offset!("lead.offset_beats", "Offset", 4.0, lead.offset_beats),
ControlSpec::new(
LEAD_STEPS_ID,
"Steps",
ControlKind::Discrete,
1.0,
LEAD_STEP_COUNT as f32,
Step::Linear(1.0),
Entry::Round,
|c| c.lead.step_count,
|c, v| c.lead.step_count = v,
|c| format!("{:.0}", c.lead.step_count),
)
.reset_at(8.0),
lead_step_row!(1),
lead_step_row!(2),
lead_step_row!(3),
lead_step_row!(4),
lead_step_row!(5),
lead_step_row!(6),
lead_step_row!(7),
lead_step_row!(8),
lead_step_row!(9),
lead_step_row!(10),
lead_step_row!(11),
lead_step_row!(12),
lead_step_row!(13),
lead_step_row!(14),
lead_step_row!(15),
lead_step_row!(16),
]
);
pub(crate) fn tab_owning_control(id: &str) -> Option<Tab> {
let owner = Tab::all()
.into_iter()
.filter(|tab| *tab != Tab::Master)
.find(|tab| tab_specs(*tab).iter().any(|spec| spec.id == id));
owner.or_else(|| {
MASTER_CONTROLS
.iter()
.any(|spec| spec.id == id)
.then_some(Tab::Master)
})
}
pub(crate) fn tab_specs(tab: Tab) -> &'static [ControlSpec] {
TAB_META[tab as usize].3
}
pub(crate) fn spec_index(tab: Tab, id: &str) -> Option<usize> {
tab_specs(tab).iter().position(|spec| spec.id == id)
}
pub(crate) fn all_specs() -> impl Iterator<Item = &'static ControlSpec> {
Tab::all().into_iter().flat_map(tab_specs)
}
pub(crate) fn spec_by_id(id: &str) -> Option<&'static ControlSpec> {
all_specs().find(|spec| spec.id == id)
}
pub(crate) fn performance_target(
instrument: interaction::PerformanceInstrument,
action: interaction::PerformanceAction,
) -> Option<(Tab, usize, &'static ControlSpec, f32)> {
let tab = instrument.tab();
let interaction::InstrumentRow { shape, density, .. } = *instrument.row();
let level = tab.level_id()?;
let (id, direction) = match action {
interaction::PerformanceAction::Shorter => (shape, -1.0),
interaction::PerformanceAction::Longer => (shape, 1.0),
interaction::PerformanceAction::Quieter => (level, -1.0),
interaction::PerformanceAction::Louder => (level, 1.0),
interaction::PerformanceAction::Sparser => (density, 1.0),
interaction::PerformanceAction::Denser => (density, -1.0),
};
let index = spec_index(tab, id)?;
Some((tab, index, &tab_specs(tab)[index], direction))
}
pub(crate) fn tab_controls(tab: Tab, c: &FluidControls) -> Vec<ControlItem> {
tab_specs(tab)
.iter()
.filter(|spec| module_slot_row_visible(spec.id, c) && lead_step_index(spec.id).is_none())
.map(|spec| spec.item(c))
.collect()
}
pub(crate) fn lead_tab_controls(
c: &FluidControls,
drill: interaction::LeadDrill,
) -> Vec<ControlItem> {
match drill {
interaction::LeadDrill::None => tab_controls(Tab::Lead, c),
interaction::LeadDrill::Pattern { .. } => LEAD_CONTROLS
.iter()
.filter(|spec| {
lead_step_index(spec.id)
.is_some_and(|step| step < lead_live_step_count(c.lead.step_count))
})
.map(|spec| spec.item(c))
.collect(),
}
}
pub(crate) fn lead_drill_for_index(
flat: usize,
c: &FluidControls,
) -> (interaction::LeadDrill, usize) {
let Some(spec) = LEAD_CONTROLS.get(flat) else {
return (interaction::LeadDrill::None, 0);
};
let root = lead_tab_controls(c, interaction::LeadDrill::None);
let root_row = |id: &str| root.iter().position(|item| item.id == id).unwrap_or(0);
match lead_step_index(spec.id) {
Some(step) => (
interaction::LeadDrill::Pattern {
return_to: root_row(LEAD_STEPS_ID),
},
step,
),
None => (interaction::LeadDrill::None, root_row(spec.id)),
}
}
pub(crate) fn module_slot_collapsed_id(
tab: Tab,
slot: usize,
controls: &FluidControls,
) -> Option<&'static str> {
let kind = controls.modules.for_tab(tab)?.get(slot)?.kind()?;
module_slot_spec(tab, slot, kind.collapsed_field()).map(|spec| spec.id)
}
pub(crate) fn module_slot_at_collapsed_id<'a>(
tab: Tab,
id: &str,
controls: &'a FluidControls,
) -> Option<(usize, &'a ModuleSlot)> {
let slots = controls.modules.for_tab(tab)?;
slots.iter().enumerate().find(|(slot, _)| {
module_slot_collapsed_id(tab, *slot, controls)
.is_some_and(|collapsed_id| collapsed_id == id)
})
}
pub(crate) fn module_detail_controls(
tab: Tab,
slot: usize,
controls: &FluidControls,
) -> Vec<ControlItem> {
let Some(module_slot) = controls
.modules
.for_tab(tab)
.and_then(|slots| slots.get(slot))
else {
return Vec::new();
};
let Some(kind) = module_slot.kind() else {
return Vec::new();
};
let mut items = kind
.parameters()
.iter()
.filter_map(|parameter| {
module_slot_spec(tab, slot, parameter.field).map(|spec| {
let mut item = spec.item(controls);
item.label = parameter.label.to_string();
item
})
})
.collect::<Vec<_>>();
let field_of = |id: &str| parse_module_slot_id(id).map(|(_, _, field)| field);
if kind.family == Family::Delay {
for item in &mut items {
let field = field_of(item.id);
if field == Some(ModuleSlotField::Feedback) {
item.max = 0.95;
}
if matches!(
field,
Some(ModuleSlotField::Time | ModuleSlotField::RightTime)
) {
let clock = if field == Some(ModuleSlotField::RightTime) {
DelayClock::from_value(module_slot.right_clock)
} else {
DelayClock::from_value(module_slot.clock)
};
match clock {
DelayClock::Sync => {
item.kind = ControlKind::Timing;
item.min = DELAY_SYNC_MIN_BEATS;
item.max = DELAY_SYNC_MAX_BEATS;
item.step = Step::BeatGrid;
item.display = beats2(item.value);
}
DelayClock::Free => {
item.kind = ControlKind::Timing;
item.min = DELAY_FREE_MIN_MS;
item.max = DELAY_FREE_MAX_MS;
item.step = Step::Linear(10.0);
item.display = secs(item.value / 1000.0);
}
}
}
}
}
if kind.family == Family::Reverb {
for item in &mut items {
if matches!(
field_of(item.id),
Some(ModuleSlotField::Time | ModuleSlotField::Feedback)
) {
item.display = pct(item.value);
}
}
}
if kind.family == Family::Compression {
for item in &mut items {
item.display = match field_of(item.id) {
Some(ModuleSlotField::Amount) => pct(item.value),
Some(ModuleSlotField::Time) => format!("{:.0} dB", item.value),
Some(ModuleSlotField::RightTime) => format!("{:.1}:1", item.value),
Some(ModuleSlotField::Feedback) => format!("{:.0} ms", item.value),
Some(ModuleSlotField::Vintage) => format!("{:.1} dB", item.value),
_ => item.display.clone(),
};
}
}
if kind.family == Family::Filter {
for item in &mut items {
item.display = match field_of(item.id) {
Some(ModuleSlotField::Amount | ModuleSlotField::RightTime) => pct(item.value),
Some(ModuleSlotField::Time) => format!("{:.0} Hz", item.value),
Some(ModuleSlotField::Feedback) => {
FilterType::from_value(item.value).label().to_string()
}
_ => item.display.clone(),
};
}
}
items
}
pub(crate) const LEAD_STEPS_ID: &str = "lead.steps";
pub(crate) fn lead_step_index(id: &str) -> Option<usize> {
let number = id.strip_prefix("lead.step")?;
let step: usize = number.parse().ok()?;
(1..=LEAD_STEP_COUNT).contains(&step).then(|| step - 1)
}
pub(crate) fn module_slot_row_visible(id: &str, c: &FluidControls) -> bool {
let Some((slot, field)) = module_slot_row(id, c) else {
return true;
};
if slot.is_empty() {
return false;
}
let collapsed = slot
.kind()
.expect("non-empty slot has a kind")
.collapsed_field();
match field {
ModuleSlotField::Kind => false,
ModuleSlotField::Amount => collapsed == ModuleSlotField::Amount,
ModuleSlotField::Time => {
slot.kind()
.is_some_and(|kind| kind.family == Family::TwoKnob)
|| collapsed == ModuleSlotField::Time
}
ModuleSlotField::RightTime
| ModuleSlotField::Clock
| ModuleSlotField::RightClock
| ModuleSlotField::Feedback
| ModuleSlotField::Vintage => false,
}
}
const MODULE_SLOT_FIELD_IDS: [(ModuleSlotField, &str); 8] = [
(ModuleSlotField::Kind, "kind"),
(ModuleSlotField::Amount, "amount"),
(ModuleSlotField::Time, "time"),
(ModuleSlotField::RightTime, "right_time"),
(ModuleSlotField::Clock, "clock"),
(ModuleSlotField::RightClock, "right_clock"),
(ModuleSlotField::Feedback, "feedback"),
(ModuleSlotField::Vintage, "vintage"),
];
impl ModuleSlotField {
pub(crate) fn from_id(id: &str) -> Option<Self> {
MODULE_SLOT_FIELD_IDS
.iter()
.find(|(_, field_id)| *field_id == id)
.map(|(field, _)| *field)
}
}
pub(crate) fn parse_module_slot_id(id: &str) -> Option<(&str, usize, ModuleSlotField)> {
let (layer, rest) = id.split_once(".slot")?;
let (index, field) = rest.split_once('.')?;
let field = ModuleSlotField::from_id(field)?;
let index = index.parse::<usize>().ok()?.checked_sub(1)?;
Some((layer, index, field))
}
pub(crate) fn module_slot_spec(
tab: Tab,
slot: usize,
field: ModuleSlotField,
) -> Option<&'static ControlSpec> {
tab_specs(tab).iter().find(|spec| {
parse_module_slot_id(spec.id)
.is_some_and(|(_, spec_slot, spec_field)| spec_slot == slot && spec_field == field)
})
}
fn parse_chord_slot_id(id: &str) -> Option<(usize, usize)> {
let rest = id.strip_prefix("pad.chord")?;
let (num, field) = rest.split_once('_')?;
let field = match field {
"degree" => 0,
"accidental" => 1,
"quality" => 2,
"extension" => 3,
"inversion" => 4,
_ => return None,
};
let slot = num.parse::<usize>().ok()?.checked_sub(1)?;
Some((slot, field))
}
fn module_slot_row<'a>(
id: &str,
c: &'a FluidControls,
) -> Option<(&'a ModuleSlot, ModuleSlotField)> {
let (layer, index, field) = parse_module_slot_id(id)?;
let slots: &[ModuleSlot; MODULE_SLOTS] = match layer {
"pad" => &c.modules.pad,
"perc" => &c.modules.perc,
"bass" => &c.modules.bass,
"kick" => &c.modules.kick,
"tonal" => &c.modules.tonal,
"clap" => &c.modules.clap,
"arp" => &c.modules.arp,
"lead" => &c.modules.lead,
"master" => &c.modules.master,
_ => return None,
};
slots.get(index).map(|slot| (slot, field))
}
pub(crate) fn chords_tab_controls(
c: &FluidControls,
drill: interaction::ChordDrill,
) -> Vec<ControlItem> {
match drill {
interaction::ChordDrill::None => CHORDS_CONTROLS[..CHORD_BASE_CONTROL_COUNT]
.iter()
.chain(CHORDS_CONTROLS[CHORD_BASE_CONTROL_COUNT + CHORD_SLOT_COUNT * 5..].iter())
.filter(|spec| module_slot_row_visible(spec.id, c))
.map(|spec| spec.item(c))
.collect(),
interaction::ChordDrill::Progression { .. } => {
let count = (c.pad.chord_count.round() as usize).clamp(1, CHORD_SLOT_COUNT);
(0..count)
.map(|slot| CHORDS_CONTROLS[CHORD_BASE_CONTROL_COUNT + 5 * slot].item(c))
.collect()
}
interaction::ChordDrill::Slot { slot, .. } => {
let base = CHORD_BASE_CONTROL_COUNT + 5 * slot;
[base + 1, base + 2, base + 3, base + 4]
.iter()
.map(|&i| CHORDS_CONTROLS[i].item(c))
.collect()
}
}
}
#[cfg(test)]
pub(crate) fn chords_flat_index(drill: interaction::ChordDrill, visible_row: usize) -> usize {
match drill {
interaction::ChordDrill::None => visible_row,
interaction::ChordDrill::Progression { .. } => CHORD_BASE_CONTROL_COUNT + 5 * visible_row,
interaction::ChordDrill::Slot { slot, .. } => {
CHORD_BASE_CONTROL_COUNT + 5 * slot + 1 + visible_row
}
}
}
pub(crate) fn chords_drill_for_index(
flat: usize,
c: &FluidControls,
) -> (interaction::ChordDrill, usize) {
let Some(spec) = CHORDS_CONTROLS.get(flat) else {
return (interaction::ChordDrill::None, 0);
};
if let Some((slot, field)) = parse_chord_slot_id(spec.id) {
return if field == 0 {
(interaction::ChordDrill::Progression { return_to: 4 }, slot)
} else {
(
interaction::ChordDrill::Slot { slot, return_to: 4 },
field - 1,
)
};
}
let selected = chords_tab_controls(c, interaction::ChordDrill::None)
.iter()
.position(|item| item.id == spec.id)
.unwrap_or(0);
(interaction::ChordDrill::None, selected)
}
pub(crate) fn apply_delta(tab: Tab, selected: usize, dir: f32, c: &mut FluidControls) {
if let Some(spec) = tab_specs(tab).get(selected) {
spec.apply_delta(dir, c);
}
}
pub(crate) fn apply_reset(tab: Tab, selected: usize, c: &mut FluidControls) {
if let Some(spec) = tab_specs(tab).get(selected) {
spec.apply_reset(c);
}
}
pub(crate) fn apply_value(tab: Tab, selected: usize, value: f32, c: &mut FluidControls) {
if let Some(spec) = tab_specs(tab).get(selected) {
spec.apply_value(value, c);
}
}
pub(crate) fn normalize_unit_input(value: f32) -> f32 {
(value / 100.0).clamp(0.0, 1.0)
}
pub(crate) fn snap_step(value: f32, step: f32) -> f32 {
(value / step).round() * step
}
pub(crate) fn ordered_step_ratio(value: f32, steps: &[f32]) -> f32 {
let Some((&first, rest)) = steps.split_first() else {
return 0.0;
};
if rest.is_empty() || value <= first {
return 0.0;
}
let last = *rest.last().unwrap_or(&first);
if value >= last {
return 1.0;
}
let upper = steps.partition_point(|step| *step <= value);
let lower = upper - 1;
let local = (value - steps[lower]) / (steps[upper] - steps[lower]);
(lower as f32 + local) / (steps.len() - 1) as f32
}
pub(crate) const BEAT_GRID_FLOOR: f32 = 0.125;
pub(crate) const BEAT_GRID_STEP: f32 = 0.25;
pub(crate) fn beat_grid_snap(value: f32, min: f32, max: f32) -> f32 {
let clamped = value.clamp(min, max);
let low = if min < BEAT_GRID_FLOOR {
min
} else {
BEAT_GRID_FLOOR
};
if low < BEAT_GRID_FLOOR && clamped <= (low + BEAT_GRID_FLOOR) / 2.0 {
return low.clamp(min, max);
}
if clamped < (BEAT_GRID_FLOOR + BEAT_GRID_STEP) / 2.0 {
return BEAT_GRID_FLOOR.clamp(min, max);
}
snap_step(clamped, BEAT_GRID_STEP).clamp(min, max)
}
pub(crate) fn beat_grid_adjust(value: f32, dir: f32, min: f32, max: f32) -> f32 {
let current = beat_grid_snap(value, min, max);
let low = if min < BEAT_GRID_FLOOR {
min
} else {
BEAT_GRID_FLOOR
};
let next = if dir > 0.0 {
if current < BEAT_GRID_FLOOR {
BEAT_GRID_FLOOR
} else if current <= BEAT_GRID_FLOOR {
BEAT_GRID_STEP
} else {
current + BEAT_GRID_STEP
}
} else if current > BEAT_GRID_STEP {
current - BEAT_GRID_STEP
} else if current > BEAT_GRID_FLOOR {
BEAT_GRID_FLOOR
} else {
low
};
beat_grid_snap(next, min, max)
}
pub(crate) fn beat_grid_ratio(value: f32, min: f32, max: f32) -> f32 {
if max <= min {
return 0.0;
}
let value = value.clamp(min, max);
let mut rung = min;
let mut rung_index = 0usize;
let mut value_position = 0.0;
let mut found = value <= min;
while rung < max {
let next = beat_grid_adjust(rung, 1.0, min, max);
if next <= rung {
break;
}
if !found && value <= next {
let local = (value - rung) / (next - rung);
value_position = rung_index as f32 + local;
found = true;
}
rung = next;
rung_index += 1;
}
if rung_index == 0 {
0.0
} else if found {
value_position / rung_index as f32
} else {
1.0
}
}
pub(crate) fn nearest_power_of_two(value: f32, min: f32, max: f32) -> f32 {
let clamped = value.clamp(min, max);
let exponent = clamped.log2().round();
2.0f32.powf(exponent).clamp(min, max)
}
#[cfg(test)]
mod performance_tests {
use super::*;
use crate::fluid::interaction::{PerformanceAction, PerformanceInstrument};
#[test]
fn performance_vocabulary_resolves_every_instrument_action_through_registry() {
let actions = [
PerformanceAction::Shorter,
PerformanceAction::Longer,
PerformanceAction::Quieter,
PerformanceAction::Louder,
PerformanceAction::Sparser,
PerformanceAction::Denser,
];
let expected = [
(
PerformanceInstrument::Pads,
[
("pad.release_time", -1.0),
("pad.release_time", 1.0),
("pad.level", -1.0),
("pad.level", 1.0),
("pad.chord_bars", 1.0),
("pad.chord_bars", -1.0),
],
),
(
PerformanceInstrument::Bass,
[
("bass.decay_time", -1.0),
("bass.decay_time", 1.0),
("bass.level", -1.0),
("bass.level", 1.0),
("bass.interval_beats", 1.0),
("bass.interval_beats", -1.0),
],
),
(
PerformanceInstrument::Kick,
[
("kick.amp_decay_ms", -1.0),
("kick.amp_decay_ms", 1.0),
("kick.level", -1.0),
("kick.level", 1.0),
("kick.interval_beats", 1.0),
("kick.interval_beats", -1.0),
],
),
(
PerformanceInstrument::Perc,
[
("perc.decay_ms", -1.0),
("perc.decay_ms", 1.0),
("perc.level", -1.0),
("perc.level", 1.0),
("perc.interval_beats", 1.0),
("perc.interval_beats", -1.0),
],
),
];
for (instrument, targets) in expected {
for (action, (expected_id, expected_direction)) in actions.into_iter().zip(targets) {
let (tab, index, spec, direction) =
performance_target(instrument, action).expect("closed grammar has a target");
assert_eq!(spec.id, expected_id);
assert_eq!(direction, expected_direction);
assert_eq!(tab, tab_owning_control(spec.id).expect("target has owner"));
assert_eq!(tab_specs(tab)[index].id, spec.id);
let mut controls = FluidControls::default();
(spec.set)(&mut controls, (spec.min + spec.max) / 2.0);
let before = (spec.get)(&controls);
spec.apply_delta(direction, &mut controls);
let after = (spec.get)(&controls);
let expected_ordering = if direction > 0.0 {
std::cmp::Ordering::Greater
} else {
std::cmp::Ordering::Less
};
assert_eq!(
after.partial_cmp(&before),
Some(expected_ordering),
"{instrument:?}/{action:?} moved {spec_id} with {direction}",
spec_id = spec.id,
);
}
}
}
}
#[cfg(test)]
mod scale_tests {
use super::*;
#[test]
fn continuous_tapered_specs_step_in_position() {
let controls = FluidControls::default();
let mut checked = 0;
for spec in all_specs().map(|spec| spec.contextual(&controls)) {
if !spec.is_continuous_tapered() {
continue;
}
checked += 1;
assert!(
matches!(spec.scale(), DialScale::Tapered { .. }),
"{}: continuous tapered row without a tapered scale",
spec.id
);
assert!(
spec.scale()
.step_in_position(spec.min, 1.0, TAPER_STEPS_PER_SWEEP)
.is_some()
);
}
assert!(checked > 0, "no continuous tapered rows in the registry");
}
}
#[cfg(test)]
mod module_slot_id_tests {
use super::*;
#[test]
fn module_slot_field_ids_follow_discriminant_order() {
for (i, (field, id)) in MODULE_SLOT_FIELD_IDS.iter().enumerate() {
assert_eq!(
*field as usize, i,
"row {i} ({id}) out of discriminant order"
);
assert_eq!(ModuleSlotField::from_id(id), Some(*field));
}
assert_eq!(ModuleSlotField::from_id("slot"), None);
}
#[test]
fn module_slot_spec_finds_every_slot_row_of_every_tab() {
for tab in Tab::all() {
for spec in tab_specs(tab) {
let Some((_, slot, field)) = parse_module_slot_id(spec.id) else {
continue;
};
assert!(
std::ptr::eq(module_slot_spec(tab, slot, field).expect(spec.id), spec),
"{}: {} resolves to a different row",
tab.name(),
spec.id
);
}
}
assert!(module_slot_spec(Tab::Bass, MODULE_SLOTS, ModuleSlotField::Kind).is_none());
}
}