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 = automation
.active_address()
.map(ControlAddress::id)
.or_else(|| tab_specs(tab).get(selected).map(|spec| spec.id));
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 lfo_time_key(field: LfoField) -> Option<&'static str> {
match field {
LfoField::Interval => Some("lfo.interval"),
LfoField::Offset => Some("lfo.offset"),
_ => None,
}
}
fn env_time_key(field: EnvField) -> Option<&'static str> {
match field {
EnvField::Attack => Some("env.attack"),
EnvField::Decay => Some("env.decay"),
EnvField::Amount | EnvField::Trigger => None,
}
}
#[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 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 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,
Set {
value: f32,
flipped: &'a FlippedUnits,
},
}
impl<'a> FieldOp<'a> {
fn flipped(self) -> Option<&'a FlippedUnits> {
match self {
FieldOp::Adjust { flipped, .. } | FieldOp::Set { flipped, .. } => Some(flipped),
FieldOp::Reset => 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 = automation
.active_address()
.map(ControlAddress::id)
.or_else(|| tab_specs(tab).get(selected).map(|spec| spec.id));
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| {
lfo_time_key(field)
.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 current = match field {
LfoField::Offset => route.phase_offset_beats,
_ => route.cycle_beats,
};
let next = flipped_step(TimeBase::Beats, current, 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::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),
}
}
ActiveField::Envelope(address, field) => {
let is_flipped = op.flipped().is_some_and(|flipped| {
env_time_key(field)
.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 current = match field {
EnvField::Decay => route.decay_beats,
_ => route.attack_beats,
};
let next = flipped_step(TimeBase::Beats, current, dir, bpm);
route.set_field_raw(field, next);
}
FieldOp::Adjust { dir, .. } => route.adjust_field(field, dir),
FieldOp::Reset => route.reset_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),
}
}
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::Set { value, .. } => route.set_step(target, value),
}
}
}
ActiveField::Control => match op {
FieldOp::Reset => apply_reset(tab, selected, &mut snapshot.controls),
FieldOp::Adjust { .. } | FieldOp::Set { .. } => {
apply_control_value_op(snapshot, tab, selected, op, bpm)
}
},
}
}
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 carries 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) => unreachable!("a reset never reaches the value path"),
}
}
fn apply_delay_row(
snapshot: &mut LiveSessionSnapshot,
tab: Tab,
spec: &ControlSpec,
op: FieldOp<'_>,
bpm: f32,
) -> bool {
let Some((slot, module)) = module_slot_at_id(tab, spec.id, &snapshot.controls) else {
return false;
};
if !module
.kind()
.is_some_and(|kind| kind.family == Family::Delay)
{
return false;
}
let field = spec.id.rsplit('.').next();
if field == Some("clock") {
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);
}
return true;
}
let right = match field {
Some("time") => false,
Some("right_time") => true,
_ => return false,
};
if let Some(slots) = snapshot.controls.modules.for_tab_mut(tab)
&& let Some(module) = slots.get_mut(slot)
{
let clock = DelayClock::from_value(if right {
module.right_clock
} else {
module.clock
});
let current = if right {
&mut module.right_time
} else {
&mut module.time
};
*current = match (clock, op) {
(DelayClock::Sync, FieldOp::Adjust { dir, .. }) => {
beat_grid_adjust(*current, dir, DELAY_SYNC_MIN_BEATS, DELAY_SYNC_MAX_BEATS)
}
(DelayClock::Free, FieldOp::Adjust { dir, .. }) => {
(*current + dir * 10.0).clamp(DELAY_FREE_MIN_MS, DELAY_FREE_MAX_MS)
}
(DelayClock::Sync, FieldOp::Set { value, .. }) => {
value.clamp(DELAY_SYNC_MIN_BEATS, DELAY_SYNC_MAX_BEATS)
}
(DelayClock::Free, FieldOp::Set { value, .. }) => {
value.clamp(DELAY_FREE_MIN_MS, DELAY_FREE_MAX_MS)
}
(_, FieldOp::Reset) => unreachable!("a reset never reaches the delay rows"),
};
}
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,
);
}
#[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)
&& matches!(spec.id.rsplit('.').next(), Some("time" | "right_time"))
&& let snapshot = effects.session().load()
&& let Some((slot, module)) = module_slot_at_id(tab, spec.id, &snapshot.controls)
&& module
.kind()
.is_some_and(|kind| kind.family == 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, spec.id.ends_with(".right_time"), bpm);
}
});
return;
}
let key = match active_field(automation, lfo_selected) {
ActiveField::Lfo(address, field) => {
lfo_time_key(field).map(|key| unit_key(address.id(), Some(key)))
}
ActiveField::Envelope(address, field) => {
env_time_key(field).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 reseed_automation_effect(effects: &mut EffectExecutor, automation: &AutomationState) {
if let Some(address) = automation.active_address()
&& automation.active_kind() == Some(ModKind::Lfo)
{
effects.edit_session(Some(address.id()), |snapshot| {
let state = &mut snapshot.automation;
if let Some(route) = state.route_mut(address)
&& route.shape.is_random()
{
route.reseed();
}
});
}
}