use super::*;
use crate::midi::{MidiMessage, MidiSink, tuned_note};
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];
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub(crate) enum LeadPattern {
Off,
Play,
}
pub(crate) const LEAD_PATTERNS: [LeadPattern; 2] = [LeadPattern::Off, LeadPattern::Play];
impl LeadPattern {
pub(crate) fn from_value(value: f32) -> Self {
LEAD_PATTERNS[(value.round() as i64).clamp(0, LEAD_PATTERNS.len() as i64 - 1) as usize]
}
pub(crate) const fn value(self) -> f32 {
match self {
Self::Off => 0.0,
Self::Play => 1.0,
}
}
pub(crate) fn toggled(self) -> Self {
match self {
Self::Off => Self::Play,
Self::Play => Self::Off,
}
}
pub(crate) fn label(self) -> &'static str {
match self {
Self::Off => "Off",
Self::Play => "Play",
}
}
}
#[derive(Clone, Copy, Debug, PartialEq)]
pub(crate) struct LeadPress {
pub(crate) beat: f64,
pub(crate) tone: usize,
}
#[derive(Clone, Debug, Default, PartialEq)]
pub(crate) struct LeadPhraseBuffer {
presses: Vec<LeadPress>,
}
const LEAD_PHRASE_PRESSES: usize = 128;
impl LeadPhraseBuffer {
pub(crate) fn push(&mut self, press: LeadPress) {
if self.presses.len() == LEAD_PHRASE_PRESSES {
self.presses.remove(0);
}
self.presses.push(press);
}
pub(crate) fn presses(&self) -> &[LeadPress] {
&self.presses
}
}
#[derive(Clone, Copy, Debug, PartialEq)]
pub(crate) struct LeadCapture {
pub(crate) count: usize,
pub(crate) steps: [f32; LEAD_STEP_COUNT],
}
const LEAD_CAPTURE_LENGTHS: [usize; 3] = [4, 8, 16];
const LEAD_PHRASE_GAP_BEATS: f64 = 4.0;
pub(crate) fn lead_capture(
presses: &[LeadPress],
now: f64,
rate_beats: f32,
offset_beats: f32,
) -> Option<LeadCapture> {
let rate = f64::from(rate_beats.clamp(LEAD_RATE_BEATS_MIN, LEAD_RATE_BEATS_MAX));
let horizon = now - rate * LEAD_STEP_COUNT as f64;
let recent: Vec<LeadPress> = presses
.iter()
.copied()
.filter(|press| press.beat >= horizon && press.beat <= now)
.collect();
let start = (1..recent.len())
.rev()
.find(|&i| recent[i].beat - recent[i - 1].beat >= LEAD_PHRASE_GAP_BEATS)
.unwrap_or(0);
let phrase = &recent[start..];
let step_of =
|press: &LeadPress| ((press.beat - f64::from(offset_beats)) / rate).round() as i64;
let first = step_of(phrase.first()?);
let last = step_of(phrase.last()?);
let span = (last - first + 1).max(1) as usize;
let count = LEAD_CAPTURE_LENGTHS
.into_iter()
.find(|&length| length >= span)
.unwrap_or(LEAD_STEP_COUNT);
let mut steps = [0.0; LEAD_STEP_COUNT];
for press in phrase {
let step = step_of(press).rem_euclid(count as i64) as usize;
steps[step] = press.tone as f32;
}
Some(LeadCapture { count, steps })
}
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,
slot: usize,
) -> LeadReach {
match follow {
LeadFollow::Chord => {
let chord = pad_chord_tones(pad, progression, slot);
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 slot in ChordWindow::requested(pad).slots() {
for note in pad_chord_tones(pad, progression, slot) {
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 {
let window = ChordWindow::requested(pad);
lead_reach(
LeadFollow::from_value(lead.follow),
pad,
window.progression,
window.slot(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,
midi: Option<MidiSink>,
midi_active: bool,
midi_held: bool,
midi_gate_remaining_beats: Option<f64>,
input_note: Option<u8>,
transport: Transport,
}
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,
midi: None,
midi_active: false,
midi_held: false,
midi_gate_remaining_beats: None,
input_note: None,
transport: Transport::Playing,
}
}
pub(crate) fn set_midi(&mut self, sink: MidiSink) {
self.midi = Some(sink);
}
pub(crate) fn midi_note_on(&mut self, note: u8, c: &LeadControls, tune: f32) {
self.input_note = Some(note);
self.play(i32::from(note), tune, c, true);
}
pub(crate) fn midi_note_off(&mut self, note: u8) {
if self.input_note == Some(note) {
self.input_note = None;
self.release();
}
}
pub(crate) fn release_input(&mut self) {
if self.input_note.take().is_some() {
self.release();
}
}
fn release_midi(&mut self) {
if self.midi_active {
if let Some(sink) = &self.midi {
sink.send(MidiMessage::LeadOff);
}
self.midi_active = false;
self.midi_held = false;
self.midi_gate_remaining_beats = None;
}
}
fn play_midi(&mut self, note: i32, tune: f32, gate_beats: f32, hold: bool) {
if let Some(sink) = &self.midi {
sink.send(MidiMessage::LeadNote(tuned_note(note, tune)));
self.midi_active = true;
self.midi_held = hold;
self.midi_gate_remaining_beats = (!hold).then_some(f64::from(gate_beats));
}
}
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, slot) = self.progression.follow(pad, timing);
let midi_enabled = c.midi_out >= 0.5 && self.midi.is_some();
if !midi_enabled || timing.transport != self.transport {
self.release_midi();
}
self.transport = timing.transport;
if let Some(remaining) = &mut self.midi_gate_remaining_beats {
*remaining -= timing.bpm / (60.0 * timing.sample_rate);
if *remaining <= 0.0 {
self.release_midi();
}
}
let released = std::mem::take(&mut self.pending_release);
if released && self.midi_held {
self.release_midi();
}
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 || midi_enabled)
&& LeadPattern::from_value(c.pattern) != LeadPattern::Off
&& !self.held_seen
&& self.input_note.is_none()
&& let Some(tone) = lead_step_tone(c.steps[lane_step])
{
let reach = lead_reach(LeadFollow::from_value(c.follow), pad, progression, slot);
let note = reach.note(tone, c.octave);
if c.level != 0.0 {
self.play(note, tune, c, false);
}
if midi_enabled {
self.play_midi(note, tune, c.midi_gate_beats, false);
}
}
}
if let Some((pressed, hold)) = self.pending_press.take()
&& (c.level != 0.0 || midi_enabled)
&& let Some(tone) = lead_step_tone(pressed as f32)
{
let reach = lead_reach(LeadFollow::from_value(c.follow), pad, progression, slot);
let note = reach.note(tone, c.octave);
if c.level != 0.0 {
self.play(note, tune, c, hold);
}
if midi_enabled {
self.play_midi(note, tune, c.midi_gate_beats, hold);
}
}
if released && self.input_note.is_none() {
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)
}
}