use crate::fluid::widget::DialScale;
use crate::fluid::{Entry, TIME_TAPER, Taper, beats2, signed_pct};
use super::{FieldSpec, ModContext, Stepping, clamped_index, morph_scalar_route, stepped_index};
pub(crate) const MAX_ENV_ATTACK_BEATS: f32 = 512.0;
pub(crate) const MAX_ENV_DECAY_BEATS: f32 = 512.0;
const ENV_BEATS_STEP: f32 = 0.5;
const ENV_AMOUNT_STEP: f32 = 0.01;
const DEFAULT_ENV_ATTACK_BEATS: f32 = 1.0;
const DEFAULT_ENV_DECAY_BEATS: f32 = 4.0;
#[derive(Clone, Copy, Debug, PartialEq)]
pub(crate) enum EnvTrigger {
EveryBeats(f32),
OnKick,
Once,
}
impl EnvTrigger {
const CYCLE: [EnvTrigger; 8] = [
Self::EveryBeats(1.0),
Self::EveryBeats(2.0),
Self::EveryBeats(4.0),
Self::EveryBeats(8.0),
Self::EveryBeats(16.0),
Self::EveryBeats(32.0),
Self::OnKick,
Self::Once,
];
fn index(self) -> usize {
Self::CYCLE.iter().position(|&t| t == self).unwrap_or(2) }
fn cycled(self, dir: f32) -> Self {
Self::CYCLE[stepped_index(self.index(), dir, Self::CYCLE.len())]
}
fn from_index(index: f32) -> Self {
Self::CYCLE[clamped_index(index, Self::CYCLE.len())]
}
fn label(self) -> String {
match self {
Self::EveryBeats(n) => format!("every {n:.0} beats"),
Self::OnKick => "on kick".to_string(),
Self::Once => "once (macro)".to_string(),
}
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub(crate) enum EnvField {
Amount,
Attack,
Decay,
Trigger,
}
impl EnvField {
pub(crate) const ALL: [EnvField; 4] = [Self::Amount, Self::Attack, Self::Decay, Self::Trigger];
pub(crate) const fn time_key(self) -> Option<&'static str> {
match self {
Self::Attack => Some("env.attack"),
Self::Decay => Some("env.decay"),
Self::Amount | Self::Trigger => None,
}
}
pub(crate) fn scale(self) -> DialScale {
match self {
Self::Trigger => DialScale::enumerated(EnvTrigger::CYCLE.len()),
_ => self.spec().scale,
}
}
pub(crate) fn label(self) -> &'static str {
match self {
Self::Trigger => "trigger",
_ => self.spec().label,
}
}
fn spec(self) -> &'static FieldSpec<EnvField> {
ENV_FIELD_SPECS
.iter()
.find(|spec| spec.field == self)
.expect("every continuous envelope field has a spec")
}
}
const ENV_FIELD_SPECS: &[FieldSpec<EnvField>] = &[
FieldSpec {
field: EnvField::Amount,
label: "amount",
min: -1.0,
max: 1.0,
step: ENV_AMOUNT_STEP,
scale: DialScale::bipolar(),
stepping: Stepping::Linear,
entry: Entry::Percent,
reset: 0.0,
},
FieldSpec {
field: EnvField::Attack,
label: "attack",
min: 0.0,
max: MAX_ENV_ATTACK_BEATS,
step: ENV_BEATS_STEP,
scale: DialScale::tapered(0.0, MAX_ENV_ATTACK_BEATS, Taper::Exp(TIME_TAPER)),
stepping: Stepping::Position,
entry: Entry::Snap,
reset: DEFAULT_ENV_ATTACK_BEATS,
},
FieldSpec {
field: EnvField::Decay,
label: "decay",
min: 0.0,
max: MAX_ENV_DECAY_BEATS,
step: ENV_BEATS_STEP,
scale: DialScale::tapered(0.0, MAX_ENV_DECAY_BEATS, Taper::Exp(TIME_TAPER)),
stepping: Stepping::Position,
entry: Entry::Snap,
reset: DEFAULT_ENV_DECAY_BEATS,
},
];
#[derive(Clone, Copy, Debug, PartialEq)]
pub(crate) struct EnvelopeRoute {
pub(crate) amount: f32,
pub(crate) attack_beats: f32,
pub(crate) decay_beats: f32,
pub(crate) trigger: EnvTrigger,
}
impl Default for EnvelopeRoute {
fn default() -> Self {
Self {
amount: 0.0,
attack_beats: DEFAULT_ENV_ATTACK_BEATS,
decay_beats: DEFAULT_ENV_DECAY_BEATS,
trigger: EnvTrigger::EveryBeats(4.0),
}
}
}
impl EnvelopeRoute {
pub(crate) fn randomize(&mut self, rng: &mut impl rand::Rng) {
for field in EnvField::ALL {
let spec = match field {
EnvField::Trigger => {
self.set_field(
field,
rng.r#gen::<f32>() * (EnvTrigger::CYCLE.len() - 1) as f32,
);
continue;
}
_ => field.spec(),
};
let ratio = rng.r#gen::<f32>();
let value = spec.scale.value_at(ratio).map_or_else(
|| spec.min + ratio * (spec.max - spec.min),
|value| spec.quantize(value),
);
self.set_field(field, value);
}
}
fn beats_since_trigger(&self, ctx: ModContext) -> Option<f32> {
match self.trigger {
EnvTrigger::EveryBeats(n) => {
let n = f64::from(n.max(ENV_BEATS_STEP));
if ctx.beat < 0.0 {
return None;
}
Some(ctx.beat.rem_euclid(n) as f32)
}
EnvTrigger::Once => {
if ctx.beat < 0.0 {
None
} else {
Some(ctx.beat as f32)
}
}
EnvTrigger::OnKick => {
let interval = f64::from(ctx.kick_interval_beats.max(1.0 / 64.0));
let offset = f64::from(ctx.kick_offset_beats).rem_euclid(interval);
let slot = ((ctx.beat - offset) / interval).floor();
let last = offset + slot * interval;
if last < -1e-9 {
None
} else {
Some((ctx.beat - last) as f32)
}
}
}
}
pub(crate) fn level_at(&self, ctx: ModContext) -> f32 {
let Some(since) = self.beats_since_trigger(ctx) else {
return 0.0;
};
self.level_for_elapsed(since)
}
fn level_for_elapsed(&self, since: f32) -> f32 {
if since < 0.0 {
0.0
} else if self.attack_beats > 0.0 && since < self.attack_beats {
since / self.attack_beats
} else if self.decay_beats <= 0.0 {
1.0
} else if since < self.attack_beats + self.decay_beats {
1.0 - (since - self.attack_beats) / self.decay_beats
} else {
0.0
}
}
pub(crate) fn window_beats(&self) -> f32 {
match self.trigger {
EnvTrigger::EveryBeats(n) => n.max(ENV_BEATS_STEP),
EnvTrigger::OnKick => self.attack_beats + self.decay_beats.max(ENV_BEATS_STEP),
EnvTrigger::Once => (self.attack_beats + self.decay_beats).max(ENV_BEATS_STEP),
}
}
pub(crate) fn level_for_lane(&self, since: f32) -> f32 {
self.level_for_elapsed(since)
}
pub(crate) fn lane_head_phase(&self, ctx: ModContext) -> f32 {
match self.beats_since_trigger(ctx) {
Some(since) => (since / self.window_beats().max(ENV_BEATS_STEP)).clamp(0.0, 1.0),
None => 0.0,
}
}
pub(crate) fn adjust_field(&mut self, field: EnvField, dir: f32) {
match field {
EnvField::Trigger => self.trigger = self.trigger.cycled(dir),
_ => {
let next = field.spec().adjust(self.field_value(field), dir);
self.write_field(field, next);
}
}
}
pub(crate) fn set_field(&mut self, field: EnvField, value: f32) {
match field {
EnvField::Trigger => self.trigger = EnvTrigger::from_index(value),
_ => self.write_field(field, field.spec().parse_value(value)),
}
}
pub(crate) fn set_field_raw(&mut self, field: EnvField, value: f32) {
match field {
EnvField::Attack | EnvField::Decay => {
let spec = field.spec();
self.write_field(field, value.clamp(spec.min, spec.max));
}
EnvField::Amount | EnvField::Trigger => self.set_field(field, value),
}
}
pub(crate) fn reset_field(&mut self, field: EnvField) {
match field {
EnvField::Trigger => self.trigger = EnvelopeRoute::default().trigger,
_ => self.write_field(field, field.spec().reset),
}
}
fn write_field(&mut self, field: EnvField, value: f32) {
match field {
EnvField::Amount => self.amount = value,
EnvField::Attack => self.attack_beats = value,
EnvField::Decay => self.decay_beats = value,
EnvField::Trigger => {}
}
}
pub(crate) fn field_value(&self, field: EnvField) -> f32 {
match field {
EnvField::Amount => self.amount,
EnvField::Attack => self.attack_beats,
EnvField::Decay => self.decay_beats,
EnvField::Trigger => self.trigger.index() as f32,
}
}
pub(crate) fn field_display(&self, field: EnvField) -> String {
match field {
EnvField::Amount => signed_pct(self.amount),
EnvField::Attack => beats2(self.attack_beats),
EnvField::Decay => {
if self.decay_beats <= 0.0 {
"hold".to_string()
} else {
beats2(self.decay_beats)
}
}
EnvField::Trigger => self.trigger.label(),
}
}
pub(super) fn morph(
from: Option<&EnvelopeRoute>,
to: Option<&EnvelopeRoute>,
tt: f32,
use_to: bool,
) -> Option<EnvelopeRoute> {
morph_scalar_route(from, to, tt, use_to, |r| r.amount, |r, v| r.amount = v)
}
}