use std::f32::consts::TAU;
use crate::fluid::widget::DialScale;
use crate::fluid::{Entry, beats2, pct, smoothstep, splitmix64_mix};
use super::{
FieldSpec, Stepping, clamped_index, index_at_ratio, morph_scalar_route, stepped_index,
};
pub(crate) const DEFAULT_LFO_CYCLE_BEATS: f32 = 1.0;
pub(crate) const DEFAULT_LFO_DEPTH_RATIO: f32 = 0.0;
pub(crate) const MIN_LFO_CYCLE_BEATS: f32 = 0.125;
pub(crate) const MAX_LFO_CYCLE_BEATS: f32 = 64.0;
pub(crate) const MAX_LFO_OFFSET_BEATS: f32 = 4.0;
pub(crate) const MAX_LFO_STEPS: usize = 16;
pub(crate) const DEFAULT_LFO_STEP_COUNT: u8 = 4;
pub(crate) const DEFAULT_LFO_STEP_GLIDE: f32 = 0.15;
const DEFAULT_LFO_STEPS: [f32; MAX_LFO_STEPS] = {
let mut steps = [0.0f32; MAX_LFO_STEPS];
steps[3] = 1.0;
steps
};
const AMOUNT_STEP: f32 = 0.01;
const INTERVAL_STEP: f32 = 0.125;
const OFFSET_STEP: f32 = 0.125;
const STEP_VALUE_STEP: f32 = 0.05;
const STEP_GLIDE_STEP: f32 = 0.05;
const SQUARE_SMOOTH: f32 = 6.0;
const RAMP_WRAP_EASE: f32 = 0.02;
const PICKUP_SCAN_STEP_BEATS: f64 = 1.0 / 256.0;
const PICKUP_CROSSING_EPSILON: f32 = 1e-4;
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub(crate) enum LfoShape {
Sine,
Triangle,
RampUp,
RampDown,
Square,
RandomDrift,
SampleHold,
Steps,
}
impl LfoShape {
pub(crate) const ALL: [LfoShape; 8] = [
Self::Sine,
Self::Triangle,
Self::RampUp,
Self::RampDown,
Self::Square,
Self::RandomDrift,
Self::SampleHold,
Self::Steps,
];
pub(crate) fn label(self) -> &'static str {
match self {
Self::Sine => "sine",
Self::Triangle => "triangle",
Self::RampUp => "ramp up",
Self::RampDown => "ramp down",
Self::Square => "square",
Self::RandomDrift => "random drift",
Self::SampleHold => "sample & hold",
Self::Steps => "steps",
}
}
pub(crate) fn is_random(self) -> bool {
matches!(self, Self::RandomDrift | Self::SampleHold)
}
fn index(self) -> usize {
Self::ALL.iter().position(|&s| s == self).unwrap_or(0)
}
fn cycled(self, dir: f32) -> Self {
Self::ALL[stepped_index(self.index(), dir, Self::ALL.len())]
}
fn from_index(index: f32) -> Self {
Self::ALL[clamped_index(index, Self::ALL.len())]
}
}
fn seeded_unit(seed: u32, index: i64) -> f32 {
let z = splitmix64_mix(
(index as u64)
.wrapping_mul(0x9E37_79B9_7F4A_7C15)
.wrapping_add(u64::from(seed))
.wrapping_add(0x9E37_79B9_7F4A_7C15),
);
let unit = (z >> 40) as f32 / f32::from(1u16 << 8) / f32::from(1u16 << 8) / 256.0;
unit * 2.0 - 1.0
}
fn ease_ramp_wrap(phase: f32, raw: f32, next_cycle_start: f32) -> f32 {
let window_start = 1.0 - RAMP_WRAP_EASE;
if phase < window_start {
return raw;
}
let t = smoothstep((phase - window_start) / RAMP_WRAP_EASE);
raw + (next_cycle_start - raw) * t
}
fn periodic_shape_value(shape: LfoShape, phase: f32) -> f32 {
match shape {
LfoShape::Sine => (TAU * phase).sin(),
LfoShape::Triangle => {
if phase < 0.25 {
4.0 * phase
} else if phase < 0.75 {
1.0 - 4.0 * (phase - 0.25)
} else {
-1.0 + 4.0 * (phase - 0.75)
}
}
LfoShape::RampUp => ease_ramp_wrap(phase, 2.0 * phase - 1.0, -1.0),
LfoShape::RampDown => ease_ramp_wrap(phase, 1.0 - 2.0 * phase, 1.0),
LfoShape::Square => (SQUARE_SMOOTH * (TAU * phase).sin()).tanh(),
LfoShape::RandomDrift | LfoShape::SampleHold | LfoShape::Steps => 0.0,
}
}
pub(super) fn seed_for_id(id: &str) -> u32 {
let mut hash = 0x811C_9DC5u32;
for byte in id.bytes() {
hash ^= u32::from(byte);
hash = hash.wrapping_mul(0x0100_0193);
}
hash
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub(crate) enum LfoField {
Amount,
Interval,
Offset,
Shape,
}
impl LfoField {
pub(crate) const ALL: [LfoField; 4] = [Self::Amount, Self::Interval, Self::Offset, Self::Shape];
pub(crate) const fn time_key(self) -> Option<&'static str> {
match self {
Self::Interval => Some("lfo.interval"),
Self::Offset => Some("lfo.offset"),
Self::Amount | Self::Shape => None,
}
}
pub(crate) fn label(self) -> &'static str {
match self {
Self::Shape => "shape",
_ => self.spec().label,
}
}
pub(crate) fn scale(self) -> DialScale {
match self {
Self::Shape => DialScale::enumerated(LfoShape::ALL.len()),
_ => self.spec().scale,
}
}
fn spec(self) -> &'static FieldSpec<LfoField> {
LFO_FIELD_SPECS
.iter()
.find(|spec| spec.field == self)
.expect("every continuous LFO field has a spec")
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub(crate) enum StepTarget {
Count,
Glide,
Value(usize),
}
const LFO_FIELD_SPECS: &[FieldSpec<LfoField>] = &[
FieldSpec {
field: LfoField::Amount,
label: "amount",
min: 0.0,
max: 1.0,
step: AMOUNT_STEP,
scale: DialScale::linear(0.0, 1.0),
stepping: Stepping::Snapped,
entry: Entry::Percent,
reset: 0.0,
},
FieldSpec {
field: LfoField::Interval,
label: "rate",
min: MIN_LFO_CYCLE_BEATS,
max: MAX_LFO_CYCLE_BEATS,
step: INTERVAL_STEP,
scale: DialScale::Rungs(LFO_RATE_ARROW_STEPS),
stepping: Stepping::Ladder(LFO_RATE_ARROW_STEPS),
entry: Entry::Free,
reset: MIN_LFO_CYCLE_BEATS,
},
FieldSpec {
field: LfoField::Offset,
label: "offset",
min: 0.0,
max: MAX_LFO_OFFSET_BEATS,
step: OFFSET_STEP,
scale: DialScale::BeatGrid {
min: 0.0,
max: MAX_LFO_OFFSET_BEATS,
},
stepping: Stepping::BeatGrid,
entry: Entry::Snap,
reset: 0.0,
},
];
pub(crate) const LFO_RATE_ARROW_STEPS: &[f32] = &[
0.125, 0.25, 0.5, 0.75, 1.0, 1.25, 1.5, 1.75, 2.0, 2.25, 2.5, 2.75, 3.0, 3.25, 3.5, 3.75, 4.0,
8.0, 12.0, 16.0, 32.0, 64.0,
];
const STEP_FIELD_SPECS: &[FieldSpec<StepTarget>] = &[
FieldSpec {
field: StepTarget::Count,
label: "steps",
min: 1.0,
max: MAX_LFO_STEPS as f32,
step: 1.0,
scale: DialScale::linear(1.0, MAX_LFO_STEPS as f32),
stepping: Stepping::Linear,
entry: Entry::Round,
reset: DEFAULT_LFO_STEP_COUNT as f32,
},
FieldSpec {
field: StepTarget::Glide,
label: "glide",
min: 0.0,
max: 1.0,
step: STEP_GLIDE_STEP,
scale: DialScale::linear(0.0, 1.0),
stepping: Stepping::Linear,
entry: Entry::Percent,
reset: DEFAULT_LFO_STEP_GLIDE,
},
FieldSpec {
field: StepTarget::Value(0),
label: "step",
min: 0.0,
max: 1.0,
step: STEP_VALUE_STEP,
scale: DialScale::linear(0.0, 1.0),
stepping: Stepping::Linear,
entry: Entry::Percent,
reset: 0.0,
},
];
impl StepTarget {
fn spec(self) -> &'static FieldSpec<StepTarget> {
let key = match self {
Self::Value(_) => Self::Value(0),
other => other,
};
STEP_FIELD_SPECS
.iter()
.find(|spec| spec.field == key)
.expect("every step target has a spec")
}
}
#[derive(Clone, Copy, Debug, PartialEq)]
pub(crate) struct LfoPickup {
pub(crate) from_cycle_beats: f32,
pub(crate) at_beat: f64,
}
#[derive(Clone, Copy, Debug, PartialEq)]
pub(crate) struct LfoRoute {
pub(crate) enabled: bool,
pub(crate) depth_ratio: f32,
pub(crate) cycle_beats: f32,
pub(crate) phase_offset_beats: f32,
pub(crate) shape: LfoShape,
pub(crate) seed: u32,
pub(crate) steps: [f32; MAX_LFO_STEPS],
pub(crate) step_count: u8,
pub(crate) step_glide: f32,
pub(crate) pickup: Option<LfoPickup>,
}
impl Default for LfoRoute {
fn default() -> Self {
Self {
enabled: true,
depth_ratio: DEFAULT_LFO_DEPTH_RATIO,
cycle_beats: DEFAULT_LFO_CYCLE_BEATS,
phase_offset_beats: 0.0,
shape: LfoShape::Sine,
seed: 0,
steps: DEFAULT_LFO_STEPS,
step_count: DEFAULT_LFO_STEP_COUNT,
step_glide: DEFAULT_LFO_STEP_GLIDE,
pickup: None,
}
}
}
impl LfoRoute {
pub(crate) fn with_seed(seed: u32) -> Self {
Self {
seed,
..Self::default()
}
}
pub(crate) fn phase_at(&self, beat: f64) -> f64 {
self.phase_at_cycle(beat, self.active_cycle_at(beat))
}
fn phase_at_cycle(&self, beat: f64, cycle_beats: f32) -> f64 {
global_lfo_position(beat, cycle_beats, self.phase_offset_beats).1
}
fn active_cycle_at(&self, beat: f64) -> f32 {
self.pickup
.filter(|pickup| beat < pickup.at_beat)
.map_or(self.cycle_beats, |pickup| pickup.from_cycle_beats)
}
fn cycle_index_and_phase_for(&self, beat: f64, cycle_beats: f32) -> (i64, f32) {
let (index, phase) = global_lfo_position(beat, cycle_beats, self.phase_offset_beats);
(index, phase as f32)
}
pub(crate) fn wave_at(&self, beat: f64) -> f32 {
self.wave_at_cycle(beat, self.active_cycle_at(beat))
}
fn wave_at_cycle(&self, beat: f64, cycle_beats: f32) -> f32 {
let (index, phase) = self.cycle_index_and_phase_for(beat, cycle_beats);
match self.shape {
LfoShape::SampleHold => seeded_unit(self.seed, index),
LfoShape::RandomDrift => {
let a = seeded_unit(self.seed, index);
let b = seeded_unit(self.seed, index + 1);
a + (b - a) * smoothstep(phase)
}
LfoShape::Steps => {
let count = self.active_step_count();
self.step_value_at(index.rem_euclid(count as i64) as usize, phase)
}
shape => periodic_shape_value(shape, phase),
}
}
pub(crate) fn active_step_count(&self) -> usize {
(self.step_count as usize).clamp(1, MAX_LFO_STEPS)
}
pub(crate) fn active_step_at(&self, beat: f64) -> Option<usize> {
(self.shape == LfoShape::Steps).then(|| {
let (index, _) = self.cycle_index_and_phase_for(beat, self.active_cycle_at(beat));
index.rem_euclid(self.active_step_count() as i64) as usize
})
}
fn step_value_at(&self, step_index: usize, frac: f32) -> f32 {
let count = self.active_step_count();
let step_index = step_index.min(count - 1);
let cur = self.steps[step_index];
let glide = self.step_glide.clamp(0.0, 1.0);
if glide <= f32::EPSILON || frac >= glide {
return cur;
}
let prev = self.steps[(step_index + count - 1) % count];
prev + (cur - prev) * smoothstep(frac / glide)
}
pub(crate) fn shape_value_at_phase(&self, phase: f32) -> f32 {
match self.shape {
LfoShape::Steps => {
let count = self.active_step_count();
let scaled = phase.rem_euclid(1.0) * count as f32;
let step_index = (scaled.floor() as usize).min(count - 1);
self.step_value_at(step_index, scaled - step_index as f32)
}
_ => periodic_shape_value(self.shape, phase),
}
}
pub(crate) fn pattern_phase_at(&self, beat: f64) -> f64 {
match self.shape {
LfoShape::Steps => {
let cycles =
global_lfo_cycles(beat, self.active_cycle_at(beat), self.phase_offset_beats);
(cycles / self.active_step_count() as f64).rem_euclid(1.0)
}
_ => self.phase_at(beat),
}
}
pub(crate) fn adjust_step(&mut self, target: StepTarget, dir: f32) {
let next = target.spec().adjust(self.step_value(target), dir);
self.write_step(target, next);
}
pub(crate) fn set_step(&mut self, target: StepTarget, value: f32) {
self.write_step(target, target.spec().parse_value(value));
}
pub(crate) fn reset_step(&mut self, target: StepTarget) {
self.write_step(target, target.spec().reset);
}
pub(crate) fn max_step(&mut self, target: StepTarget) {
self.write_step(target, Self::step_scale(target).max_value());
}
fn write_step(&mut self, target: StepTarget, value: f32) {
self.pickup = None;
match target {
StepTarget::Count => self.step_count = value.round() as u8,
StepTarget::Glide => self.step_glide = value,
StepTarget::Value(i) => {
if let Some(step) = self.steps.get_mut(i) {
*step = value;
}
}
}
}
pub(crate) fn step_scale(target: StepTarget) -> DialScale {
target.spec().scale
}
pub(crate) fn step_value(&self, target: StepTarget) -> f32 {
match target {
StepTarget::Count => self.active_step_count() as f32,
StepTarget::Glide => self.step_glide,
StepTarget::Value(i) => self.steps.get(i).copied().unwrap_or(0.0),
}
}
pub(crate) fn step_display(&self, target: StepTarget) -> String {
match target {
StepTarget::Count => format!("{}", self.active_step_count()),
StepTarget::Glide => pct(self.step_glide),
StepTarget::Value(i) => pct(self.steps.get(i).copied().unwrap_or(0.0)),
}
}
pub(crate) fn step_label(&self, target: StepTarget) -> String {
match target {
StepTarget::Count => "steps".to_string(),
StepTarget::Glide => "glide".to_string(),
StepTarget::Value(i) => format!("· step {}", i + 1),
}
}
pub(crate) fn reseed(&mut self) {
self.seed = self
.seed
.wrapping_mul(1_664_525)
.wrapping_add(1_013_904_223)
^ 0x5DEE_CE66;
}
pub(crate) fn randomize(&mut self, rng: &mut impl rand::Rng, beat: f64) {
for field in LfoField::ALL {
self.randomize_field_at(field, rng.r#gen(), beat);
}
self.seed = rng.r#gen();
self.randomize_step(StepTarget::Count, rng.r#gen());
self.randomize_step(StepTarget::Glide, rng.r#gen());
self.randomize_step_values(rng);
}
pub(crate) fn randomize_step_values(&mut self, rng: &mut impl rand::Rng) {
for step in 0..self.active_step_count() {
self.randomize_step(StepTarget::Value(step), rng.r#gen());
}
}
pub(crate) fn randomize_field_at(&mut self, field: LfoField, ratio: f32, beat: f64) {
match field {
LfoField::Shape => {
self.write_shape(LfoShape::ALL[index_at_ratio(ratio, LfoShape::ALL.len())]);
}
_ => self.write_field_at(field, field.spec().value_at_ratio(ratio), beat),
}
}
pub(crate) fn randomize_step(&mut self, target: StepTarget, ratio: f32) {
self.write_step(target, target.spec().value_at_ratio(ratio));
}
pub(crate) fn adjust_field_at(&mut self, field: LfoField, dir: f32, beat: f64) {
match field {
LfoField::Shape => self.write_shape(self.shape.cycled(dir)),
_ => {
let next = field.spec().adjust(self.field_value(field), dir);
self.write_field_at(field, next, beat);
}
}
}
pub(crate) fn set_field_at(&mut self, field: LfoField, value: f32, beat: f64) {
match field {
LfoField::Shape => self.write_shape(LfoShape::from_index(value)),
_ => self.write_field_at(field, field.spec().parse_value(value), beat),
}
}
pub(crate) fn set_field_raw_at(&mut self, field: LfoField, value: f32, beat: f64) {
match field {
LfoField::Interval | LfoField::Offset => {
let spec = field.spec();
self.write_field_at(field, value.clamp(spec.min, spec.max), beat);
}
_ => self.set_field_at(field, value, beat),
}
}
pub(crate) fn reset_field_at(&mut self, field: LfoField, beat: f64) {
match field {
LfoField::Shape => self.write_shape(LfoShape::Sine),
_ => self.write_field_at(field, field.spec().reset, beat),
}
}
pub(crate) fn max_field_at(&mut self, field: LfoField, beat: f64) {
match field {
LfoField::Shape => self.write_shape(LfoShape::from_index(field.scale().max_value())),
_ => self.write_field_at(field, field.scale().max_value(), beat),
}
}
fn write_field_at(&mut self, field: LfoField, value: f32, beat: f64) {
match field {
LfoField::Amount => self.depth_ratio = value,
LfoField::Interval => self.set_cycle_with_pickup(value, beat),
LfoField::Offset => {
self.phase_offset_beats = value;
self.pickup = None;
}
LfoField::Shape => {}
}
}
fn write_shape(&mut self, shape: LfoShape) {
self.shape = shape;
self.pickup = None;
}
pub(crate) fn field_value(&self, field: LfoField) -> f32 {
match field {
LfoField::Shape => self.shape.index() as f32,
LfoField::Amount => self.depth_ratio,
LfoField::Interval => self.cycle_beats,
LfoField::Offset => self.phase_offset_beats,
}
}
pub(crate) fn field_display(&self, field: LfoField) -> String {
match field {
LfoField::Shape => self.shape.label().to_string(),
LfoField::Amount => pct(self.depth_ratio),
LfoField::Interval => beats2(self.cycle_beats),
LfoField::Offset => beats2(self.phase_offset_beats),
}
}
fn set_cycle_with_pickup(&mut self, cycle_beats: f32, beat: f64) {
let old_cycle = self.active_cycle_at(beat);
if (old_cycle - cycle_beats).abs() <= f32::EPSILON {
self.cycle_beats = cycle_beats;
self.pickup = None;
return;
}
self.cycle_beats = cycle_beats;
self.pickup = next_wave_crossing(self, old_cycle, cycle_beats, beat).and_then(|at_beat| {
(at_beat > beat + f64::EPSILON).then_some(LfoPickup {
from_cycle_beats: old_cycle,
at_beat,
})
});
}
pub(super) fn morph(
from: Option<&LfoRoute>,
to: Option<&LfoRoute>,
tt: f32,
use_to: bool,
) -> Option<LfoRoute> {
morph_scalar_route(
from,
to,
tt,
use_to,
|r| r.depth_ratio,
|r, v| r.depth_ratio = v,
)
}
}
fn global_lfo_position(beat: f64, cycle_beats: f32, offset_beats: f32) -> (i64, f64) {
let position = global_lfo_cycles(beat, cycle_beats, offset_beats);
let index = position.floor();
(index as i64, position - index)
}
fn global_lfo_cycles(beat: f64, cycle_beats: f32, offset_beats: f32) -> f64 {
let cycle = f64::from(cycle_beats.max(MIN_LFO_CYCLE_BEATS));
(beat + f64::from(offset_beats)) / cycle
}
fn next_wave_crossing(
route: &LfoRoute,
old_cycle_beats: f32,
new_cycle_beats: f32,
beat: f64,
) -> Option<f64> {
let delta_at =
|at| route.wave_at_cycle(at, old_cycle_beats) - route.wave_at_cycle(at, new_cycle_beats);
let mut previous_beat = beat;
let mut previous_delta = delta_at(beat);
if previous_delta.abs() <= PICKUP_CROSSING_EPSILON {
return Some(beat);
}
let horizon = f64::from(old_cycle_beats.max(new_cycle_beats)) * 2.0;
let steps = (horizon / PICKUP_SCAN_STEP_BEATS).ceil() as usize;
let mut closest = (previous_delta.abs(), beat);
for step in 1..=steps {
let next_beat = beat + step as f64 * PICKUP_SCAN_STEP_BEATS;
let next_delta = delta_at(next_beat);
if next_delta.abs() < closest.0 {
closest = (next_delta.abs(), next_beat);
}
if next_delta.abs() <= PICKUP_CROSSING_EPSILON {
return Some(next_beat);
}
if previous_delta.signum() != next_delta.signum() {
let mut lo = previous_beat;
let mut hi = next_beat;
let lo_sign = previous_delta.signum();
for _ in 0..20 {
let mid = (lo + hi) * 0.5;
if delta_at(mid).signum() == lo_sign {
lo = mid;
} else {
hi = mid;
}
}
return Some((lo + hi) * 0.5);
}
previous_beat = next_beat;
previous_delta = next_delta;
}
Some(closest.1)
}