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_CHORD_TONES: usize = 4;
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub(crate) struct LeadTone {
pub(crate) label: &'static str,
pub(crate) chord_tone: usize,
pub(crate) octave: i32,
}
pub(crate) const LEAD_STEP_TONES: [Option<LeadTone>; 10] = [
None,
Some(LeadTone {
label: "1",
chord_tone: 0,
octave: 0,
}),
Some(LeadTone {
label: "2",
chord_tone: 1,
octave: 0,
}),
Some(LeadTone {
label: "3",
chord_tone: 2,
octave: 0,
}),
Some(LeadTone {
label: "4",
chord_tone: 3,
octave: 0,
}),
Some(LeadTone {
label: "1'",
chord_tone: 0,
octave: 1,
}),
Some(LeadTone {
label: "2'",
chord_tone: 1,
octave: 1,
}),
Some(LeadTone {
label: "3'",
chord_tone: 2,
octave: 1,
}),
Some(LeadTone {
label: "4'",
chord_tone: 3,
octave: 1,
}),
Some(LeadTone {
label: "1''",
chord_tone: 0,
octave: 2,
}),
];
pub(crate) fn lead_step_tone(value: f32) -> Option<LeadTone> {
LEAD_STEP_TONES[(value.round() as i64).clamp(0, LEAD_STEP_TONES.len() as i64 - 1) as usize]
}
pub(crate) fn lead_step_label(value: f32) -> &'static str {
lead_step_tone(value).map_or("Rest", |tone| tone.label)
}
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_note(tone: LeadTone, chord: [i32; LEAD_CHORD_TONES], octave: f32) -> i32 {
chord[tone.chord_tone] + 12 * (tone.octave + octave.round() as i32)
}
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,
}
impl LeadVoice {
pub(crate) fn new(hz: f32, attack: f32, decay: f32, sample_rate: f32) -> Self {
Self {
phase: 0.0,
hz,
target_hz: hz,
envelope: Adsr::new(attack, decay, 0.0, decay, sample_rate),
sample_rate,
}
}
pub(crate) fn retrigger(&mut self, hz: f32, attack: f32, decay: f32) {
self.target_hz = hz;
self.envelope = Adsr::new(attack, decay, 0.0, decay, self.sample_rate);
}
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) struct LeadEngine {
pub(crate) sample_rate: f32,
pub(crate) progression: ProgressionFollower,
pub(crate) step_trigger: GridTrigger,
pub(crate) voice: Option<LeadVoice>,
presses_seen: u64,
pending_press: Option<usize>,
}
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,
pending_press: None,
}
}
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);
}
}
pub(crate) fn play(&mut self, note: i32, tune: f32, c: &LeadControls) {
let hz = note_hz(note, tune);
match &mut self.voice {
Some(voice) if !voice.is_done() => voice.retrigger(hz, c.attack, c.decay),
_ => self.voice = Some(LeadVoice::new(hz, c.attack, c.decay, self.sample_rate)),
}
}
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
&& let Some(tone) = lead_step_tone(c.steps[lane_step])
{
let chord = pad_chord_tones(pad, progression, step);
self.play(lead_note(tone, chord, c.octave), tune, c);
}
}
if let Some(pressed) = self.pending_press.take()
&& c.level != 0.0
&& let Some(tone) = lead_step_tone(pressed as f32)
{
let chord = pad_chord_tones(pad, progression, step);
self.play(lead_note(tone, chord, c.octave), tune, c);
}
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)
}
}