use super::*;
pub(crate) const LEAD_RATE_BEATS_MIN: f32 = 0.125;
pub(crate) const LEAD_RATE_BEATS_MAX: f32 = 4.0;
pub(crate) const LEAD_OCTAVE_MIN: f32 = -2.0;
pub(crate) const LEAD_OCTAVE_MAX: f32 = 2.0;
pub(crate) const LEAD_TONE_COUNT: usize = 9;
const LEAD_REACH_MAX: usize = 12;
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub(crate) enum LeadFollow {
Chord,
Scale,
}
pub(crate) const LEAD_FOLLOWS: [LeadFollow; 2] = [LeadFollow::Chord, LeadFollow::Scale];
impl LeadFollow {
pub(crate) fn from_value(value: f32) -> Self {
LEAD_FOLLOWS[(value.round() as i64).clamp(0, LEAD_FOLLOWS.len() as i64 - 1) as usize]
}
pub(crate) fn label(self) -> &'static str {
match self {
Self::Chord => "Chord",
Self::Scale => "Scale",
}
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub(crate) struct LeadReach {
notes: [i32; LEAD_REACH_MAX],
len: usize,
}
impl LeadReach {
pub(crate) fn len(&self) -> usize {
self.len
}
pub(crate) fn note(&self, tone: usize, octave: f32) -> i32 {
let index = tone.saturating_sub(1);
self.notes[index % self.len] + 12 * ((index / self.len) as i32 + octave.round() as i32)
}
pub(crate) fn label(&self, tone: usize) -> String {
lead_tone_label(tone, self.len)
}
}
pub(crate) fn lead_tone_label(tone: usize, reach_len: usize) -> String {
let index = tone.saturating_sub(1);
let len = reach_len.max(1);
format!("{}{}", index % len + 1, "'".repeat(index / len))
}
pub(crate) fn lead_reach(
follow: LeadFollow,
pad: &PadControls,
progression: usize,
step: usize,
) -> LeadReach {
match follow {
LeadFollow::Chord => {
let chord = pad_chord_tones(pad, progression, step);
let mut notes = [0; LEAD_REACH_MAX];
notes[..chord.len()].copy_from_slice(&chord);
LeadReach {
notes,
len: chord.len(),
}
}
LeadFollow::Scale => progression_scale(pad, progression),
}
}
pub(crate) fn progression_scale(pad: &PadControls, progression: usize) -> LeadReach {
let tonic = pad_chord_tones(pad, progression, 0)[0];
let mut present = [false; 12];
for step in 0..pad_chord_count(pad) {
for note in pad_chord_tones(pad, progression, step) {
present[(note - tonic).rem_euclid(12) as usize] = true;
}
}
let mut notes = [0; LEAD_REACH_MAX];
let mut len = 0;
for (interval, _) in present.iter().enumerate().filter(|(_, hit)| **hit) {
notes[len] = tonic + interval as i32;
len += 1;
}
LeadReach { notes, len }
}
pub(crate) fn lead_step_tone(value: f32) -> Option<usize> {
match (value.round() as i64).clamp(0, LEAD_TONE_COUNT as i64) as usize {
0 => None,
tone => Some(tone),
}
}
pub(crate) fn lead_step_label(value: f32, reach: &LeadReach) -> String {
lead_step_tone(value).map_or_else(|| "Rest".to_string(), |tone| reach.label(tone))
}
pub(crate) fn lead_page_reach(lead: &LeadControls, pad: &PadControls) -> LeadReach {
lead_reach(
LeadFollow::from_value(lead.follow),
pad,
progression_index(pad.progression),
0,
)
}
pub(crate) fn lead_live_step_count(step_count: f32) -> usize {
(step_count.round() as i64).clamp(1, LEAD_STEP_COUNT as i64) as usize
}
pub(crate) fn lead_step_at(beat: f64, rate_beats: f32, offset_beats: f32, count: usize) -> usize {
let position = (beat - offset_beats as f64) / rate_beats as f64 + 0.25;
(position.floor() as i64).rem_euclid(count.max(1) as i64) as usize
}
pub(crate) const LEAD_HARMONIC_COUNT: usize = 8;
const LEAD_NYQUIST_FRACTION: f32 = 0.45;
#[derive(Clone, Copy, Debug, PartialEq)]
pub(crate) struct LeadRecipe {
pub(crate) label: &'static str,
pub(crate) harmonics: [f32; LEAD_HARMONIC_COUNT],
pub(crate) trim: f32,
}
pub(crate) const LEAD_TYPES: [LeadRecipe; 5] = [
LeadRecipe {
label: "Saw",
harmonics: [1.0, 0.5, 0.333, 0.25, 0.2, 0.167, 0.143, 0.125],
trim: 0.165,
},
LeadRecipe {
label: "Square",
harmonics: [1.0, 0.0, 0.333, 0.0, 0.2, 0.0, 0.143, 0.0],
trim: 0.188,
},
LeadRecipe {
label: "Soft",
harmonics: [1.0, 0.0, -0.111, 0.0, 0.04, 0.0, -0.02, 0.0],
trim: 0.2025,
},
LeadRecipe {
label: "Pure",
harmonics: [1.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0],
trim: 0.204,
},
LeadRecipe {
label: "Reed",
harmonics: [1.0, 0.7, 0.9, 0.4, 0.6, 0.2, 0.3, 0.1],
trim: 0.1185,
},
];
pub(crate) fn lead_recipe(value: f32) -> LeadRecipe {
LEAD_TYPES[wrapped_index(value, LEAD_TYPES.len())]
}
pub(crate) fn lead_type_label(value: f32) -> &'static str {
lead_recipe(value).label
}
pub(crate) struct LeadVoice {
phase: f32,
hz: f32,
target_hz: f32,
envelope: Adsr,
sample_rate: f32,
}
#[derive(Clone, Copy, Debug, PartialEq)]
pub(crate) struct LeadShape {
pub(crate) attack: f32,
pub(crate) decay: f32,
pub(crate) hold: bool,
}
impl LeadShape {
fn envelope(self, sample_rate: f32) -> Adsr {
let sustain = if self.hold { 1.0 } else { 0.0 };
Adsr::new(self.attack, self.decay, sustain, self.decay, sample_rate)
}
}
impl LeadVoice {
pub(crate) fn new(hz: f32, shape: LeadShape, sample_rate: f32) -> Self {
Self {
phase: 0.0,
hz,
target_hz: hz,
envelope: shape.envelope(sample_rate),
sample_rate,
}
}
pub(crate) fn retrigger(&mut self, hz: f32, shape: LeadShape) {
self.target_hz = hz;
let mut envelope = shape.envelope(self.sample_rate);
envelope.start_at(self.envelope.level());
self.envelope = envelope;
}
pub(crate) fn release(&mut self) {
self.envelope.note_off();
}
pub(crate) fn next(&mut self, glide: f32, recipe: &LeadRecipe) -> f32 {
self.hz += (self.target_hz - self.hz) * glide_coefficient(glide, self.sample_rate);
let mut raw = 0.0f32;
let ceiling = self.sample_rate * LEAD_NYQUIST_FRACTION;
for (index, gain) in recipe.harmonics.iter().enumerate() {
let n = (index + 1) as f32;
if self.hz * n > ceiling {
break;
}
if *gain != 0.0 {
raw += (self.phase * n).sin() * gain;
}
}
self.phase += std::f32::consts::TAU * self.hz / self.sample_rate;
if self.phase >= std::f32::consts::TAU {
self.phase -= std::f32::consts::TAU;
}
raw * recipe.trim * self.envelope.next()
}
pub(crate) fn is_done(&self) -> bool {
self.envelope.is_done()
}
#[cfg(test)]
pub(crate) fn hz(&self) -> f32 {
self.hz
}
}
pub(crate) fn glide_coefficient(glide: f32, sample_rate: f32) -> f32 {
if glide <= 0.0 {
return 1.0;
}
1.0 - (-3.0 / (glide * sample_rate)).exp()
}
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub(crate) struct LeadPlayState {
pub(crate) presses: u64,
pub(crate) tone: usize,
pub(crate) held: bool,
}
pub(crate) struct LeadEngine {
pub(crate) sample_rate: f32,
pub(crate) progression: ProgressionFollower,
pub(crate) step_trigger: GridTrigger,
pub(crate) voice: Option<LeadVoice>,
presses_seen: u64,
held_seen: bool,
pending_press: Option<(usize, bool)>,
pending_release: bool,
}
impl LeadEngine {
#[cfg(test)]
pub(crate) fn new(sample_rate: f32) -> Self {
Self::with_play_state(sample_rate, LeadPlayState::default())
}
pub(crate) fn with_play_state(sample_rate: f32, play: LeadPlayState) -> Self {
Self {
sample_rate,
progression: ProgressionFollower::new(),
step_trigger: GridTrigger::new(),
voice: None,
presses_seen: play.presses,
held_seen: play.held,
pending_press: None,
pending_release: false,
}
}
pub(crate) fn observe(&mut self, play: LeadPlayState) {
if play.presses != self.presses_seen {
self.presses_seen = play.presses;
self.pending_press = Some((play.tone, play.held));
} else if self.held_seen && !play.held {
self.pending_release = true;
}
self.held_seen = play.held;
}
pub(crate) fn play(&mut self, note: i32, tune: f32, c: &LeadControls, hold: bool) {
let hz = note_hz(note, tune);
let shape = LeadShape {
attack: c.attack,
decay: c.decay,
hold,
};
match &mut self.voice {
Some(voice) if !voice.is_done() => voice.retrigger(hz, shape),
_ => self.voice = Some(LeadVoice::new(hz, shape, self.sample_rate)),
}
}
pub(crate) fn release(&mut self) {
if let Some(voice) = &mut self.voice {
voice.release();
}
}
pub(crate) fn next(
&mut self,
c: &LeadControls,
pad: &PadControls,
tune: f32,
timing: TimingContext,
) -> (f32, f32) {
let (progression, step) = self.progression.follow(pad, timing);
let rate_beats = c.rate_beats.clamp(LEAD_RATE_BEATS_MIN, LEAD_RATE_BEATS_MAX);
if self
.step_trigger
.pop_swung(timing, rate_beats, c.offset_beats, c.swing)
{
let count = lead_live_step_count(c.step_count);
let lane_step = lead_step_at(timing.beat, rate_beats, c.offset_beats, count);
if c.level != 0.0
&& !self.held_seen
&& let Some(tone) = lead_step_tone(c.steps[lane_step])
{
let reach = lead_reach(LeadFollow::from_value(c.follow), pad, progression, step);
self.play(reach.note(tone, c.octave), tune, c, false);
}
}
if let Some((pressed, hold)) = self.pending_press.take()
&& c.level != 0.0
&& let Some(tone) = lead_step_tone(pressed as f32)
{
let reach = lead_reach(LeadFollow::from_value(c.follow), pad, progression, step);
self.play(reach.note(tone, c.octave), tune, c, hold);
}
if std::mem::take(&mut self.pending_release) {
self.release();
}
let Some(voice) = &mut self.voice else {
return (0.0, 0.0);
};
let sample = voice.next(c.glide, &lead_recipe(c.voice_type));
if voice.is_done() {
self.voice = None;
}
let out = sample * c.level;
(out, out)
}
}