use super::*;
fn flip_entry(base: TimeBase, value: f32, bpm: f32) -> f32 {
match base {
TimeBase::Beats => ms_to_beats(value, bpm),
TimeBase::Ms => beats_to_ms(value, bpm),
TimeBase::None => value,
}
}
const FLIP_MS_STEP: f32 = 10.0;
const FLIP_BEAT_STEP: f32 = 0.125;
fn flipped_step(native: TimeBase, value: f32, dir: f32, bpm: f32) -> f32 {
match native {
TimeBase::Beats => ms_to_beats(
snap_step(beats_to_ms(value, bpm) + dir * FLIP_MS_STEP, FLIP_MS_STEP),
bpm,
),
TimeBase::Ms => beats_to_ms(
snap_step(
ms_to_beats(value, bpm) + dir * FLIP_BEAT_STEP,
FLIP_BEAT_STEP,
),
bpm,
),
TimeBase::None => value,
}
}
pub(crate) fn snap_after_unit_flip(
effects: &mut EffectExecutor,
automation: &AutomationState,
lfo_selected: usize,
tab: Tab,
selected: usize,
now_flipped: bool,
beat: f64,
) {
let active = active_field(automation, lfo_selected);
let recent_id = recent_id(automation, tab, selected);
effects.edit_session(recent_id, |snapshot| match active {
ActiveField::Lfo(address, field) if !now_flipped => {
if let Some(route) = snapshot.automation.route_mut(address)
&& field == LfoField::Offset
{
route.set_field_at(field, route.phase_offset_beats, beat);
}
}
ActiveField::Envelope(address, field) if !now_flipped => {
if let Some(route) = snapshot.automation.envelope_mut(address) {
match field {
EnvField::Attack => route.set_field(field, route.attack_beats),
EnvField::Decay => route.set_field(field, route.decay_beats),
EnvField::Amount | EnvField::Trigger => {}
}
}
}
ActiveField::Control => {
let Some(spec) = tab_specs(tab).get(selected) else {
return;
};
let bpm = snapshot.controls.master.bpm;
let current = (spec.get)(&snapshot.controls);
match (spec.time_base, now_flipped) {
(TimeBase::Beats, false) => {
spec.apply_quantized_value(current, &mut snapshot.controls)
}
(TimeBase::Ms, true) => {
let beats =
snap_step(ms_to_beats(current, bpm), FLIP_BEAT_STEP).max(FLIP_BEAT_STEP);
spec.apply_raw(beats_to_ms(beats, bpm), &mut snapshot.controls);
}
_ => {}
}
}
_ => {}
});
}
fn recent_id(automation: &AutomationState, tab: Tab, selected: usize) -> Option<&'static str> {
automation
.active_address()
.map(ControlAddress::id)
.or_else(|| tab_specs(tab).get(selected).map(|spec| spec.id))
}
#[derive(Clone, Copy, PartialEq)]
pub(crate) enum LfoSubRow {
Field(LfoField),
Step(StepTarget),
}
pub(crate) fn lfo_submenu_rows(
automation: &AutomationState,
address: ControlAddress,
) -> Vec<LfoSubRow> {
let mut rows = Vec::with_capacity(LfoField::ALL.len());
for field in LfoField::ALL {
rows.push(LfoSubRow::Field(field));
}
if let Some(route) = automation.route(address)
&& route.shape == LfoShape::Steps
{
rows.push(LfoSubRow::Step(StepTarget::Count));
rows.push(LfoSubRow::Step(StepTarget::Glide));
for i in 0..route.active_step_count() {
rows.push(LfoSubRow::Step(StepTarget::Value(i)));
}
}
rows
}
pub(crate) fn lfo_steps_selected(
automation: &AutomationState,
address: ControlAddress,
selected: usize,
) -> bool {
selected
.checked_sub(1)
.and_then(|row| lfo_submenu_rows(automation, address).get(row).copied())
.is_some_and(|row| matches!(row, LfoSubRow::Step(_)))
}
pub(crate) fn env_field_at(index: usize) -> Option<EnvField> {
EnvField::ALL.get(index.checked_sub(1)?).copied()
}
#[cfg(test)]
pub(crate) fn clamp_lfo_selection(current: usize, direction: isize, row_count: usize) -> usize {
if row_count == 0 {
return 0;
}
current.saturating_add_signed(direction).clamp(1, row_count)
}
pub(crate) fn open_modulator_effect_for_id(
effects: &mut EffectExecutor,
id: &'static str,
kind: ModKind,
sub_selected: &mut usize,
) {
let address = ControlAddress::new(id);
effects.edit_session(Some(id), |snapshot| {
let state = &mut snapshot.automation;
let already = state.active_address() == Some(address) && state.active_kind() == Some(kind);
if already {
state.cycle_open(address, kind);
} else {
state.close_editor();
match kind {
ModKind::Lfo => {
state.open_or_create(address);
}
ModKind::Envelope => {
state.open_or_create_envelope(address);
}
}
}
});
*sub_selected = 1;
}
pub(crate) fn apply_recipe_effect(
effects: &mut EffectExecutor,
recipe: &recipe::Recipe,
target: recipe::RecipeTarget,
) -> Result<Arc<LiveSessionSnapshot>, EffectFailure> {
effects.edit_session_checked(
target.id,
|snapshot| recipe.check(snapshot, target),
|snapshot| recipe.apply(snapshot, target),
)
}
pub(crate) fn add_modulator_effect_for_id(
effects: &mut EffectExecutor,
id: &'static str,
kind: ModKind,
) -> bool {
let address = ControlAddress::new(id);
let mut added = false;
effects.edit_session(Some(id), |snapshot| {
let state = &mut snapshot.automation;
if state.active_address() != Some(address) || state.active_kind() != Some(kind) {
state.close_editor();
}
added = state.add_and_open(address, kind);
});
added
}
#[derive(Clone, Copy)]
enum ActiveField {
Lfo(ControlAddress, LfoField),
LfoStep(ControlAddress, StepTarget),
Envelope(ControlAddress, EnvField),
Control,
}
fn active_field(automation: &AutomationState, lfo_selected: usize) -> ActiveField {
let Some(address) = automation.active_address() else {
return ActiveField::Control;
};
if lfo_selected == 0 {
return ActiveField::Control;
}
match automation.active_kind() {
Some(ModKind::Lfo) => match lfo_submenu_rows(automation, address).get(lfo_selected - 1) {
Some(LfoSubRow::Field(field)) => ActiveField::Lfo(address, *field),
Some(LfoSubRow::Step(target)) => ActiveField::LfoStep(address, *target),
None => ActiveField::Control,
},
Some(ModKind::Envelope) => match env_field_at(lfo_selected) {
Some(field) => ActiveField::Envelope(address, field),
None => ActiveField::Control,
},
None => ActiveField::Control,
}
}
pub(crate) fn automation_kind_is_supported(
_selected_control: Option<&str>,
kind: interaction::AutomationKind,
) -> bool {
matches!(
kind,
interaction::AutomationKind::Lfo | interaction::AutomationKind::Envelope
)
}
#[derive(Clone, Copy)]
enum FieldOp<'a> {
Adjust {
dir: f32,
flipped: &'a FlippedUnits,
},
Reset,
Max,
Set {
value: f32,
flipped: &'a FlippedUnits,
},
Randomize {
ratio: f32,
},
}
impl<'a> FieldOp<'a> {
fn flipped(self) -> Option<&'a FlippedUnits> {
match self {
FieldOp::Adjust { flipped, .. } | FieldOp::Set { flipped, .. } => Some(flipped),
FieldOp::Reset | FieldOp::Max | FieldOp::Randomize { .. } => None,
}
}
}
fn with_active_field(
effects: &mut EffectExecutor,
automation: &AutomationState,
lfo_selected: usize,
tab: Tab,
selected: usize,
beat: f64,
op: FieldOp<'_>,
) {
let active = active_field(automation, lfo_selected);
let recent_id = recent_id(automation, tab, selected);
effects.edit_session(recent_id, |snapshot| {
apply_field_op(snapshot, active, tab, selected, beat, op);
});
}
fn apply_field_op(
snapshot: &mut LiveSessionSnapshot,
active: ActiveField,
tab: Tab,
selected: usize,
beat: f64,
op: FieldOp<'_>,
) {
let bpm = snapshot.controls.master.bpm;
match active {
ActiveField::Lfo(address, field) => {
let is_flipped = op.flipped().is_some_and(|flipped| {
field
.time_key()
.is_some_and(|key| flipped.contains(&unit_key(address.id(), Some(key))))
});
let Some(route) = snapshot.automation.route_mut(address) else {
return;
};
match op {
FieldOp::Adjust { dir, .. } if is_flipped => {
let next = flipped_step(TimeBase::Beats, route.field_value(field), dir, bpm);
route.set_field_raw_at(field, next, beat);
}
FieldOp::Adjust { dir, .. } => route.adjust_field_at(field, dir, beat),
FieldOp::Reset => route.reset_field_at(field, beat),
FieldOp::Max => route.max_field_at(field, beat),
FieldOp::Set { value, .. } if is_flipped => {
route.set_field_raw_at(field, flip_entry(TimeBase::Beats, value, bpm), beat);
}
FieldOp::Set { value, .. } => route.set_field_at(field, value, beat),
FieldOp::Randomize { .. }
if field == LfoField::Shape && route.shape.is_random() =>
{
route.reseed();
}
FieldOp::Randomize { ratio } => route.randomize_field_at(field, ratio, beat),
}
}
ActiveField::Envelope(address, field) => {
let is_flipped = op.flipped().is_some_and(|flipped| {
field
.time_key()
.is_some_and(|key| flipped.contains(&unit_key(address.id(), Some(key))))
});
let Some(route) = snapshot.automation.envelope_mut(address) else {
return;
};
match op {
FieldOp::Adjust { dir, .. } if is_flipped => {
let next = flipped_step(TimeBase::Beats, route.field_value(field), dir, bpm);
route.set_field_raw(field, next);
}
FieldOp::Adjust { dir, .. } => route.adjust_field(field, dir),
FieldOp::Reset => route.reset_field(field),
FieldOp::Max => route.max_field(field),
FieldOp::Set { value, .. } if is_flipped => {
route.set_field_raw(field, flip_entry(TimeBase::Beats, value, bpm));
}
FieldOp::Set { value, .. } => route.set_field(field, value),
FieldOp::Randomize { ratio } => route.randomize_field(field, ratio),
}
}
ActiveField::LfoStep(address, target) => {
if let Some(route) = snapshot.automation.route_mut(address) {
match op {
FieldOp::Adjust { dir, .. } => route.adjust_step(target, dir),
FieldOp::Reset => route.reset_step(target),
FieldOp::Max => route.max_step(target),
FieldOp::Set { value, .. } => route.set_step(target, value),
FieldOp::Randomize { ratio } => route.randomize_step(target, ratio),
}
}
}
ActiveField::Control => {
let filter_type =
tab_specs(tab).get(selected).and_then(|spec| {
match module_slot_field(tab, spec, &snapshot.controls, Family::Filter)? {
(slot, ModuleSlotField::Feedback) => Some(slot),
_ => None,
}
});
if let Some(spec) = tab_specs(tab).get(selected) {
suspend_capture(snapshot, spec.id);
}
let before =
filter_type.and_then(|slot| filter_slot(snapshot, tab, slot).map(|m| m.feedback));
match op {
FieldOp::Reset => apply_reset(tab, selected, &mut snapshot.controls),
FieldOp::Max => apply_max(tab, selected, &mut snapshot.controls),
FieldOp::Randomize { ratio } => {
if let Some(spec) = tab_specs(tab).get(selected) {
spec.apply_ratio(ratio, &mut snapshot.controls);
}
}
FieldOp::Adjust { .. } | FieldOp::Set { .. } => {
apply_control_value_op(snapshot, tab, selected, op, bpm)
}
}
if let (Some(slot), Some(before)) = (filter_type, before)
&& !matches!(op, FieldOp::Randomize { .. })
&& let Some(module) = filter_slot(snapshot, tab, slot)
{
let next = module.feedback;
module.feedback = before;
switch_filter_type(module, next);
}
}
}
}
fn filter_slot(
snapshot: &mut LiveSessionSnapshot,
tab: Tab,
slot: usize,
) -> Option<&mut ModuleSlot> {
snapshot.controls.modules.for_tab_mut(tab)?.get_mut(slot)
}
fn apply_control_value_op(
snapshot: &mut LiveSessionSnapshot,
tab: Tab,
selected: usize,
op: FieldOp<'_>,
bpm: f32,
) {
let spec = tab_specs(tab).get(selected);
if let Some(spec) = spec
&& apply_delay_row(snapshot, tab, spec, op, bpm)
{
return;
}
let flipped = op
.flipped()
.expect("only Reset and Randomize carry no flipped set");
let flipped_spec = spec.filter(|spec| {
spec.time_base != TimeBase::None && flipped.contains(&unit_key(spec.id, None))
});
match (flipped_spec, op) {
(Some(spec), FieldOp::Adjust { dir, .. }) => {
let current = (spec.get)(&snapshot.controls);
spec.apply_raw(
flipped_step(spec.time_base, current, dir, bpm),
&mut snapshot.controls,
);
}
(Some(spec), FieldOp::Set { value, .. }) => {
spec.apply_raw(
flip_entry(spec.time_base, value, bpm),
&mut snapshot.controls,
);
}
(None, FieldOp::Adjust { dir, .. }) => {
apply_delta(tab, selected, dir, &mut snapshot.controls)
}
(None, FieldOp::Set { value, .. }) => {
apply_value(tab, selected, value, &mut snapshot.controls)
}
(_, FieldOp::Reset | FieldOp::Max | FieldOp::Randomize { .. }) => {
unreachable!("a reset, max, or roll never reaches the value path")
}
}
}
fn module_slot_field(
tab: Tab,
spec: &ControlSpec,
controls: &FluidControls,
family: Family,
) -> Option<(usize, ModuleSlotField)> {
let (_, slot, field) = parse_module_slot_id(spec.id)?;
let module = controls.modules.for_tab(tab)?.get(slot)?;
module
.kind()
.is_some_and(|kind| kind.family == family)
.then_some((slot, field))
}
fn apply_delay_row(
snapshot: &mut LiveSessionSnapshot,
tab: Tab,
spec: &ControlSpec,
op: FieldOp<'_>,
bpm: f32,
) -> bool {
let Some((slot, ModuleSlotField::Clock)) =
module_slot_field(tab, spec, &snapshot.controls, Family::Delay)
else {
return false;
};
if !matches!(op, FieldOp::Adjust { .. }) {
return false;
}
if let Some(slots) = snapshot.controls.modules.for_tab_mut(tab)
&& let Some(module) = slots.get_mut(slot)
{
switch_delay_clock(module, false, bpm);
}
true
}
#[allow(clippy::too_many_arguments)]
pub(crate) fn adjust_lfo_or_control(
effects: &mut EffectExecutor,
automation: &AutomationState,
lfo_selected: usize,
tab: Tab,
selected: usize,
dir: f32,
beat: f64,
flipped: &FlippedUnits,
) {
with_active_field(
effects,
automation,
lfo_selected,
tab,
selected,
beat,
FieldOp::Adjust { dir, flipped },
);
}
pub(crate) fn reset_lfo_or_control(
effects: &mut EffectExecutor,
automation: &AutomationState,
lfo_selected: usize,
tab: Tab,
selected: usize,
beat: f64,
) {
with_active_field(
effects,
automation,
lfo_selected,
tab,
selected,
beat,
FieldOp::Reset,
);
}
pub(crate) fn max_lfo_or_control(
effects: &mut EffectExecutor,
automation: &AutomationState,
lfo_selected: usize,
tab: Tab,
selected: usize,
beat: f64,
) {
with_active_field(
effects,
automation,
lfo_selected,
tab,
selected,
beat,
FieldOp::Max,
);
}
#[allow(clippy::too_many_arguments)]
pub(crate) fn set_modulator_or_control(
effects: &mut EffectExecutor,
automation: &AutomationState,
lfo_selected: usize,
tab: Tab,
selected: usize,
value: f32,
beat: f64,
flipped: &FlippedUnits,
) {
with_active_field(
effects,
automation,
lfo_selected,
tab,
selected,
beat,
FieldOp::Set { value, flipped },
);
}
pub(crate) fn toggle_units_effect(
effects: &mut EffectExecutor,
automation: &AutomationState,
flipped: &mut FlippedUnits,
lfo_selected: usize,
tab: Tab,
selected: usize,
beat: f64,
) {
if matches!(active_field(automation, lfo_selected), ActiveField::Control)
&& let Some(spec) = tab_specs(tab).get(selected)
&& let Some((slot, field @ (ModuleSlotField::Time | ModuleSlotField::RightTime))) =
module_slot_field(tab, spec, &effects.session().load().controls, Family::Delay)
{
effects.edit_session(Some(spec.id), |snapshot| {
let bpm = snapshot.controls.master.bpm;
if let Some(slots) = snapshot.controls.modules.for_tab_mut(tab)
&& let Some(module) = slots.get_mut(slot)
{
switch_delay_clock(module, field == ModuleSlotField::RightTime, bpm);
}
});
return;
}
let key = match active_field(automation, lfo_selected) {
ActiveField::Lfo(address, field) => field
.time_key()
.map(|key| unit_key(address.id(), Some(key))),
ActiveField::Envelope(address, field) => field
.time_key()
.map(|key| unit_key(address.id(), Some(key))),
ActiveField::LfoStep(..) => None,
ActiveField::Control => tab_specs(tab)
.get(selected)
.filter(|spec| spec.time_base != TimeBase::None)
.map(|spec| unit_key(spec.id, None)),
};
let Some(key) = key else { return };
let now_flipped = !flipped.remove(&key);
if now_flipped {
flipped.insert(key);
}
snap_after_unit_flip(
effects,
automation,
lfo_selected,
tab,
selected,
now_flipped,
beat,
);
}
pub(crate) fn remove_automation_effect(
effects: &mut EffectExecutor,
automation: &AutomationState,
selected_control: Option<&'static str>,
lfo_selected: usize,
) {
match active_field(automation, lfo_selected) {
_ if automation.is_editor_open() => {
let id = automation
.active_address()
.expect("open editor has an address")
.id();
effects.edit_session(Some(id), |snapshot| {
snapshot.automation.remove_open_route();
});
}
_ => {
if let Some(id) = selected_control {
let address = ControlAddress::new(id);
effects.edit_session(Some(id), |snapshot| {
snapshot.automation.clear_control(address);
});
}
}
}
}
pub(crate) fn randomize_lfo_or_control(
effects: &mut EffectExecutor,
automation: &AutomationState,
lfo_selected: usize,
tab: Tab,
selected: usize,
ratio: f32,
beat: f64,
) {
with_active_field(
effects,
automation,
lfo_selected,
tab,
selected,
beat,
FieldOp::Randomize { ratio },
);
}