use crate::fx::crossfade::{Outgoing, mix};
use crate::midi::{MidiMessage, MidiSink, pad_notes_with_count};
use super::*;
pub(crate) const MAX_PAD_LAYERS: usize = 4;
const MAX_MIDI_PAD_VOICES: usize = 16;
const PAD_TYPE_CROSSFADE_SECONDS: f32 = 0.03;
pub(crate) struct PadEngine {
pub(crate) sample_rate: f32,
pub(crate) layers: Vec<PadLayer>,
input_voices: Vec<PadInputVoice>,
pub(crate) cursor: ProgressionCursor,
pub(crate) active_character: usize,
pub(crate) last_chord_notes: [i32; 4],
active_note_count: usize,
transport: Transport,
pub(crate) width_lfo: DriftingLfo,
pub(crate) air: WhiteNoise,
pub(crate) rng: StdRng,
pub(crate) telemetry: Arc<FluidTelemetry>,
midi: Option<MidiSink>,
midi_input_attached: bool,
internal_audio: EasedRamp,
midi_suppressed: bool,
midi_initial_pending: bool,
stab_trigger: GridTrigger,
active_stab_until_beat: Option<f64>,
active_stab_until_sample: Option<u64>,
sample_clock: u64,
last_midi_trigger_sample: Option<u64>,
stabbing: bool,
midi_triggering: bool,
}
struct PadInputVoice {
note: u8,
held: bool,
tone: PadTone,
}
impl PadEngine {
pub(crate) fn new(
sample_rate: f32,
c: &PadControls,
tune: f32,
telemetry: Arc<FluidTelemetry>,
) -> Self {
let cursor = ProgressionCursor::new(c);
let active_character = wrapped_index(c.voice_type, PAD_TYPES.len());
let initial_notes = pad_chord_tones(c, cursor.window.progression, cursor.slot());
let note_count = pad_note_count(c.chord_notes);
telemetry
.chord_slot
.store(cursor.slot() as u64, Ordering::Relaxed);
Self {
sample_rate,
layers: vec![PadLayer::new(
active_character,
initial_notes,
note_count,
tune,
sample_rate,
c.attack_time,
c.release_time,
)],
input_voices: Vec::with_capacity(MAX_MIDI_PAD_VOICES),
cursor,
active_character,
last_chord_notes: initial_notes,
active_note_count: note_count,
transport: Transport::Playing,
width_lfo: DriftingLfo::new(1.0 / 54.0, sample_rate),
air: WhiteNoise::new(),
rng: StdRng::from_entropy(),
telemetry,
midi: None,
midi_input_attached: false,
internal_audio: EasedRamp::settled(1.0),
midi_suppressed: false,
midi_initial_pending: false,
stab_trigger: GridTrigger::new(),
active_stab_until_beat: None,
active_stab_until_sample: None,
sample_clock: 0,
last_midi_trigger_sample: None,
stabbing: false,
midi_triggering: false,
}
}
pub(crate) fn set_midi(&mut self, sink: MidiSink) {
self.midi = Some(sink);
self.midi_initial_pending = true;
}
pub(crate) fn set_midi_input_attached(&mut self) {
self.midi_input_attached = true;
}
pub(crate) fn set_midi_suppressed(&mut self, suppressed: bool) {
if self.midi_suppressed != suppressed {
if suppressed {
if let Some(sink) = &self.midi {
sink.send(MidiMessage::PadOff);
}
self.midi_initial_pending = false;
} else {
self.midi_initial_pending = true;
}
self.midi_suppressed = suppressed;
}
}
fn send_midi(&self, message: MidiMessage) {
if !self.midi_suppressed
&& let Some(sink) = &self.midi
{
sink.send(message);
}
}
fn release_stab(&mut self) {
let beat_active = self.active_stab_until_beat.take().is_some();
let sample_active = self.active_stab_until_sample.take().is_some();
if beat_active || sample_active {
for layer in &mut self.layers {
layer.release();
}
self.send_midi(MidiMessage::PadOff);
}
}
pub(crate) fn release_input(&mut self) {
for voice in &mut self.input_voices {
if voice.held {
voice.held = false;
voice.tone.release();
}
}
if self.midi_triggering {
self.release_stab();
}
}
pub(crate) fn midi_note_off(&mut self, note: u8) {
for voice in &mut self.input_voices {
if voice.note == note && voice.held {
voice.held = false;
voice.tone.release();
}
}
}
pub(crate) fn midi_note_on(
&mut self,
note: u8,
c: &PadControls,
tune: f32,
timing: TimingContext,
) {
if self.stabbing && self.midi_triggering {
let coalesce_samples = (self.sample_rate * 0.01).round() as u64;
if self.last_midi_trigger_sample.is_some_and(|previous| {
self.sample_clock.saturating_sub(previous) < coalesce_samples
}) {
return;
}
self.last_midi_trigger_sample = Some(self.sample_clock);
self.trigger_stab(c, tune, timing);
return;
}
self.midi_note_off(note);
if self.input_voices.len() >= MAX_MIDI_PAD_VOICES {
self.input_voices.remove(0);
}
self.input_voices.push(PadInputVoice {
note,
held: true,
tone: PadTone::new(
self.active_character,
note_hz(i32::from(note), tune),
0.0,
0.17,
c.attack_time,
c.release_time,
self.sample_rate,
),
});
}
fn trigger_stab(&mut self, c: &PadControls, tune: f32, timing: TimingContext) {
self.release_stab();
let chord_notes = self.last_chord_notes;
self.telemetry
.publish_hit(MusicalHit::Pad, c.level, pitch_class(chord_notes[0]));
self.send_midi(MidiMessage::PadChord(pad_notes_with_count(
chord_notes,
self.active_note_count,
tune,
)));
if self.layers.len() >= MAX_PAD_LAYERS {
let remove_count = self.layers.len() + 1 - MAX_PAD_LAYERS;
self.layers.drain(0..remove_count);
}
self.layers.push(PadLayer::new(
self.active_character,
chord_notes,
self.active_note_count,
tune,
self.sample_rate,
c.attack_time,
c.release_time,
));
if timing.transport == Transport::Stopped {
let samples = (f64::from(c.gate_beats) * 60.0 / timing.bpm
* f64::from(self.sample_rate))
.round() as u64;
self.active_stab_until_sample = Some(self.sample_clock + samples.max(1));
} else {
self.active_stab_until_beat = Some(timing.beat + f64::from(c.gate_beats));
}
}
pub(crate) fn restart_sequence(&mut self, c: &PadControls) {
self.release_stab();
for layer in &mut self.layers {
layer.release();
}
self.cursor = ProgressionCursor::new(c);
self.stab_trigger = GridTrigger::new();
self.transport = Transport::Stopped;
}
pub(crate) fn next(&mut self, c: &PadControls, tune: f32, timing: TimingContext) -> (f32, f32) {
let advance = self.cursor.tick(c, timing);
let chord_notes = pad_chord_tones(c, self.cursor.window.progression, self.cursor.slot());
let note_count = pad_note_count(c.chord_notes);
let chord_edited =
chord_notes != self.last_chord_notes || note_count != self.active_note_count;
let character = wrapped_index(c.voice_type, PAD_TYPES.len());
let character_changed = character != self.active_character;
self.last_chord_notes = chord_notes;
self.active_note_count = note_count;
self.active_character = character;
if character_changed {
for layer in &mut self.layers {
layer.set_character(character, self.sample_rate);
}
for voice in &mut self.input_voices {
voice.tone.set_character(character, self.sample_rate);
}
}
let transport_changed = timing.transport != self.transport;
self.transport = timing.transport;
let playing = timing.transport == Transport::Playing;
let stabbing = c.trigger >= 0.5;
let midi_triggering = stabbing && c.midi_trigger >= 0.5;
let mode_changed = stabbing != self.stabbing;
let source_changed = midi_triggering != self.midi_triggering;
self.stabbing = stabbing;
self.midi_triggering = midi_triggering;
if mode_changed {
if stabbing {
self.layers.clear();
self.stab_trigger = GridTrigger::new();
} else {
for layer in &mut self.layers {
layer.release();
}
}
self.active_stab_until_beat = None;
self.active_stab_until_sample = None;
self.send_midi(MidiMessage::PadOff);
}
if source_changed {
self.release_input();
self.release_stab();
}
if mode_changed || source_changed {
self.last_midi_trigger_sample = None;
}
if transport_changed && !playing {
self.release_stab();
for layer in &mut self.layers {
layer.release();
}
}
if stabbing {
self.midi_initial_pending = false;
if playing && (advance || chord_edited) {
self.telemetry.publish_chord(
self.cursor.slot() as u64,
pitch_class(chord_notes[0]),
0.01,
0.08,
);
}
if chord_edited
&& (self.active_stab_until_beat.is_some()
|| self.active_stab_until_sample.is_some())
{
self.release_stab();
}
if self
.active_stab_until_beat
.is_some_and(|end| timing.beat >= end)
|| self
.active_stab_until_sample
.is_some_and(|end| self.sample_clock >= end)
{
self.release_stab();
}
if self.stab_trigger.pop_swung(timing, 0.25, 0.0, c.swing) && !midi_triggering {
let step = ((timing.beat / 0.25).floor() as usize) % 16;
if c.steps[step] >= 0.5 {
self.trigger_stab(c, tune, timing);
}
}
}
if !stabbing && playing && (advance || chord_edited || transport_changed || mode_changed) {
for layer in &mut self.layers {
layer.release();
}
self.telemetry.publish_chord(
self.cursor.slot() as u64,
pitch_class(chord_notes[0]),
c.attack_time,
c.release_time,
);
self.telemetry
.publish_hit(MusicalHit::Pad, c.level, pitch_class(chord_notes[0]));
self.send_midi(MidiMessage::PadChord(pad_notes_with_count(
chord_notes,
note_count,
tune,
)));
self.midi_initial_pending = false;
if self.layers.len() >= MAX_PAD_LAYERS {
let remove_count = self.layers.len() + 1 - MAX_PAD_LAYERS;
self.layers.drain(0..remove_count);
}
self.layers.push(PadLayer::new(
character,
chord_notes,
note_count,
tune,
self.sample_rate,
c.attack_time,
c.release_time,
));
}
if !stabbing && playing && self.midi_initial_pending {
self.send_midi(MidiMessage::PadChord(pad_notes_with_count(
chord_notes,
note_count,
tune,
)));
self.midi_initial_pending = false;
}
let width = c.stereo_width
* (0.58
+ normalized_lfo(self.width_lfo.next(&mut self.rng, 1.0 / 86.0, 1.0 / 38.0))
* 0.16);
let detune_mix = c.detune * 0.84;
let octave_mix = c.octave_mix * 0.32;
let (dry_l, dry_r) = mix_and_retain(
&mut self.layers,
|layer| layer.next_stereo(width, detune_mix, octave_mix),
PadLayer::is_done,
);
let (input_l, input_r) = mix_and_retain(
&mut self.input_voices,
|voice| voice.tone.next_stereo(width, detune_mix, octave_mix),
|voice| voice.tone.is_done(),
);
let air = self.air.next_filtered(&mut self.rng, 0.0002) * 0.00025;
const OUTPUT_TRIM: f32 = 0.95;
let internal_target = if self.midi_input_attached && c.midi_in >= 0.5 && !stabbing {
0.0
} else {
1.0
};
if self.internal_audio.target != internal_target {
let samples = (LEVEL_RAMP_MS * 0.001 * self.sample_rate).round() as u32;
self.internal_audio.retarget(internal_target, samples);
}
let internal_gain = self.internal_audio.next();
self.sample_clock += 1;
(
((dry_l * internal_gain + input_l) * OUTPUT_TRIM + air) * c.level,
((dry_r * internal_gain + input_r) * OUTPUT_TRIM + air) * c.level,
)
}
}
pub(crate) struct PadLayer {
pub(crate) tones: Vec<PadTone>,
}
impl PadLayer {
pub(crate) fn new(
character: usize,
notes: [i32; 4],
note_count: usize,
tune: f32,
sample_rate: f32,
attack_time: f32,
release_time: f32,
) -> Self {
Self {
tones: pad_tones(
character,
notes,
note_count,
tune,
sample_rate,
attack_time,
release_time,
),
}
}
pub(crate) fn next_stereo(
&mut self,
width: f32,
detune_mix: f32,
octave_mix: f32,
) -> (f32, f32) {
let (mut l, mut r) = (0.0f32, 0.0f32);
for t in &mut self.tones {
let (tl, tr) = t.next_stereo(width, detune_mix, octave_mix);
l += tl;
r += tr;
}
(l, r)
}
pub(crate) fn release(&mut self) {
for t in &mut self.tones {
t.release();
}
}
pub(crate) fn set_character(&mut self, character: usize, sample_rate: f32) {
for t in &mut self.tones {
t.set_character(character, sample_rate);
}
}
pub(crate) fn is_done(&self) -> bool {
self.tones.iter().all(PadTone::is_done)
}
}
pub(crate) struct PadOscStack {
pub(crate) primary: SineOscillator,
pub(crate) detuned: SineOscillator,
pub(crate) octave: SineOscillator,
pub(crate) envelope: Adsr,
pub(crate) pan: f32,
pub(crate) gain: f32,
}
impl PadOscStack {
pub(crate) fn new(
hz: f32,
pan: f32,
gain: f32,
attack_time: f32,
release_time: f32,
sample_rate: f32,
) -> Self {
Self {
primary: SineOscillator::new(hz, sample_rate),
detuned: SineOscillator::new(hz * 1.003, sample_rate),
octave: SineOscillator::new(hz * 2.0, sample_rate),
envelope: Adsr::new(attack_time, 12.0, 0.86, release_time, sample_rate),
pan,
gain,
}
}
#[inline]
pub(crate) fn stack_sum(&mut self, detune_mix: f32, octave_mix: f32) -> f32 {
self.primary.next() + self.detuned.next() * detune_mix + self.octave.next() * octave_mix
}
pub(crate) fn release(&mut self) {
self.envelope.note_off();
}
pub(crate) fn is_done(&self) -> bool {
self.envelope.is_done()
}
}
const PAD_DARK_LOWPASS_COEFF: f32 = 0.18;
const PAD_DARK_OUTPUT_GAIN: f32 = 1.22;
const PAD_GLASS_SHIMMER_MIX: f32 = 0.09;
const PAD_GLASS_OUTPUT_GAIN: f32 = 0.93;
const PAD_CHOIR_PARTIAL_MIX_MIN: f32 = 0.04;
const PAD_CHOIR_PARTIAL_MIX_RANGE: f32 = 0.07;
const PAD_CHOIR_OUTPUT_GAIN: f32 = 0.94;
const PAD_HOLLOW_STACK_MIX: f32 = 0.82;
const PAD_HOLLOW_SUB_MIX: f32 = 0.18;
const PAD_HOLLOW_OUTPUT_GAIN: f32 = 1.1;
const PAD_TAPE_LOWPASS_COEFF: f32 = 0.32;
const PAD_TAPE_OUTPUT_GAIN: f32 = 1.16;
pub(crate) enum PadStage {
None,
Lowpass { state: f32 },
Shimmer { oscillator: SineOscillator },
Choir {
partial: SineOscillator,
movement: SineOscillator,
},
Hollow { sub: SineOscillator },
Tape {
state: f32,
movement: SineOscillator,
},
}
impl PadStage {
#[inline]
fn apply(&mut self, s: f32) -> f32 {
match self {
Self::None => s,
Self::Lowpass { state } => {
*state += PAD_DARK_LOWPASS_COEFF * (s - *state);
*state
}
Self::Shimmer { oscillator } => s + oscillator.next() * PAD_GLASS_SHIMMER_MIX,
Self::Choir { partial, movement } => {
let partial_mix = PAD_CHOIR_PARTIAL_MIX_MIN
+ normalized_lfo(movement.next()) * PAD_CHOIR_PARTIAL_MIX_RANGE;
s + partial.next() * partial_mix
}
Self::Hollow { sub } => s * PAD_HOLLOW_STACK_MIX + sub.next() * PAD_HOLLOW_SUB_MIX,
Self::Tape { state, movement } => {
*state += PAD_TAPE_LOWPASS_COEFF * (s - *state);
*state * (0.94 + normalized_lfo(movement.next()) * 0.06)
}
}
}
}
struct OutgoingStage {
stage: PadStage,
output_gain: f32,
}
pub(crate) struct PadTone {
pub(crate) stack: PadOscStack,
pub(crate) stage: PadStage,
pub(crate) output_gain: f32,
hz: f32,
outgoing: Option<Outgoing<OutgoingStage>>,
}
fn pad_stage(character: usize, hz: f32, sample_rate: f32) -> (PadStage, f32) {
match character {
0 => (PadStage::None, 1.0),
1 => (PadStage::Lowpass { state: 0.0 }, PAD_DARK_OUTPUT_GAIN),
2 => (
PadStage::Shimmer {
oscillator: SineOscillator::new(hz * 4.0, sample_rate),
},
PAD_GLASS_OUTPUT_GAIN,
),
3 => (
PadStage::Choir {
partial: SineOscillator::new(hz * 3.0, sample_rate),
movement: SineOscillator::new(0.17, sample_rate),
},
PAD_CHOIR_OUTPUT_GAIN,
),
4 => (
PadStage::Hollow {
sub: SineOscillator::new(hz * 0.5, sample_rate),
},
PAD_HOLLOW_OUTPUT_GAIN,
),
5 => (
PadStage::Tape {
state: 0.0,
movement: SineOscillator::new(0.11, sample_rate),
},
PAD_TAPE_OUTPUT_GAIN,
),
_ => (PadStage::None, 1.0),
}
}
impl PadTone {
pub(crate) fn new(
character: usize,
hz: f32,
pan: f32,
gain: f32,
attack_time: f32,
release_time: f32,
sample_rate: f32,
) -> Self {
let (stage, output_gain) = pad_stage(character, hz, sample_rate);
Self {
stack: PadOscStack::new(hz, pan, gain, attack_time, release_time, sample_rate),
stage,
output_gain,
hz,
outgoing: None,
}
}
pub(crate) fn set_character(&mut self, character: usize, sample_rate: f32) {
let (stage, output_gain) = pad_stage(character, self.hz, sample_rate);
self.outgoing = Some(Outgoing::start(
OutgoingStage {
stage: std::mem::replace(&mut self.stage, stage),
output_gain: std::mem::replace(&mut self.output_gain, output_gain),
},
PAD_TYPE_CROSSFADE_SECONDS * sample_rate,
));
}
pub(crate) fn next_stereo(
&mut self,
width: f32,
detune_mix: f32,
octave_mix: f32,
) -> (f32, f32) {
let raw = self.stack.stack_sum(detune_mix, octave_mix);
let envelope = self.stack.envelope.next();
let mut shaped =
soft_clip(self.stage.apply(raw) * 0.55) * envelope * self.stack.gain * self.output_gain;
if let Some(outgoing) = &mut self.outgoing {
let previous = soft_clip(outgoing.inner.stage.apply(raw) * 0.55)
* envelope
* self.stack.gain
* outgoing.inner.output_gain;
shaped = mix(shaped, previous, outgoing.advance());
if outgoing.is_done() {
self.outgoing = None;
}
}
StereoPanner::equal_power(shaped, self.stack.pan * width)
}
pub(crate) fn release(&mut self) {
self.stack.release();
}
pub(crate) fn is_done(&self) -> bool {
self.stack.is_done()
}
}
pub(crate) fn pad_tones(
character: usize,
notes: [i32; 4],
note_count: usize,
tune: f32,
sample_rate: f32,
attack_time: f32,
release_time: f32,
) -> Vec<PadTone> {
let (voicing, count) = pad_voicing(notes, note_count);
let (pans, gains) = match count {
2 => ([-0.3, 0.3, 0.0, 0.0, 0.0], [0.17, 0.126, 0.0, 0.0, 0.0]),
3 => ([-0.45, 0.0, 0.45, 0.0, 0.0], [0.17, 0.132, 0.126, 0.0, 0.0]),
4 => (
[-0.52, -0.18, 0.16, 0.46, 0.0],
[0.17, 0.132, 0.126, 0.098, 0.0],
),
_ => (
[-0.52, -0.25, 0.0, 0.25, 0.52],
[0.17, 0.132, 0.126, 0.098, 0.07],
),
};
voicing[..count]
.iter()
.zip(pans)
.zip(gains)
.map(|((hz, pan), gain)| {
PadTone::new(
character,
note_hz(*hz, tune),
pan,
gain,
attack_time,
release_time,
sample_rate,
)
})
.collect()
}
pub(crate) fn pad_note_count(value: f32) -> usize {
(value.round() as i64).clamp(2, 5) as usize
}
pub(crate) fn pad_voicing(notes: [i32; 4], count: usize) -> ([i32; 5], usize) {
let count = count.clamp(2, 5);
let mut voiced = [notes[0], notes[1], notes[2], notes[3], notes[0] + 24];
if count == 2 {
voiced[1] = notes[2];
}
(voiced, count)
}
pub(crate) struct Chord {
pub(crate) name: &'static str,
pub(crate) notes: [i32; 4],
}
const fn chord(name: &'static str, notes: [i32; 4]) -> Chord {
Chord { name, notes }
}
pub(crate) struct Progression {
pub(crate) song_value: i8,
pub(crate) mood: &'static str,
pub(crate) chords: [Chord; CHORD_SLOT_COUNT],
pub(crate) bass: [i32; CHORD_SLOT_COUNT],
}
pub(crate) const PROGRESSIONS: [Progression; 14] = [
Progression {
song_value: 0,
mood: "Drift",
chords: [
chord("Am11", [45, 50, 55, 60]),
chord("Gsus", [43, 50, 57, 60]),
chord("Am", [45, 52, 57, 60]),
chord("Em7/B", [47, 52, 55, 62]),
chord("A5", [45, 52, 57, 64]),
chord("G5", [43, 50, 55, 62]),
chord("C", [48, 55, 60, 64]),
chord("Em/G", [55, 59, 64, 67]),
],
bass: [45, 47, 45, 43, 52, 53, 45, 45],
},
Progression {
song_value: 1,
mood: "Tide",
chords: [
chord("Am11", [45, 50, 57, 60]),
chord("Dm", [50, 53, 57, 62]),
chord("C", [48, 55, 60, 64]),
chord("G", [43, 50, 55, 59]),
chord("F", [41, 48, 53, 57]),
chord("Em", [52, 59, 64, 67]),
chord("Am", [45, 52, 57, 60]),
chord("G", [43, 50, 55, 59]),
],
bass: [45, 50, 48, 43, 41, 52, 45, 43],
},
Progression {
song_value: 2,
mood: "Velvet",
chords: [
chord("Am7", [45, 48, 52, 55]),
chord("Fmaj7", [41, 45, 48, 52]),
chord("Cmaj7", [48, 52, 55, 59]),
chord("G7", [43, 47, 50, 53]),
chord("Dm7", [50, 53, 57, 60]),
chord("Em7", [52, 55, 59, 62]),
chord("Bm7b5", [47, 50, 53, 57]),
chord("G", [43, 50, 55, 59]),
],
bass: [45, 41, 48, 43, 50, 52, 47, 43],
},
Progression {
song_value: 3,
mood: "Ache",
chords: [
chord("Am", [45, 52, 57, 60]),
chord("Fadd9", [41, 45, 48, 55]),
chord("G/C", [48, 55, 59, 62]),
chord("Dm/G", [43, 50, 53, 57]),
chord("Am/D", [50, 57, 60, 64]),
chord("Em", [52, 55, 59, 64]),
chord("Fmaj7/B", [47, 53, 57, 64]),
chord("Em7/G", [43, 50, 55, 64]),
],
bass: [45, 41, 48, 43, 50, 52, 47, 43],
},
Progression {
song_value: 4,
mood: "Shadow",
chords: [
chord("Am", [45, 48, 52, 57]),
chord("Bbmaj7", [46, 50, 53, 57]),
chord("Csus4", [48, 53, 55, 60]),
chord("Dm7", [50, 53, 60, 62]),
chord("E7sus", [52, 59, 62, 64]),
chord("G6", [55, 59, 62, 64]),
chord("Gm7", [55, 58, 62, 65]),
chord("Bbmaj7#11/A", [45, 52, 58, 62]),
],
bass: [45, 46, 48, 50, 52, 43, 43, 45],
},
Progression {
song_value: 5,
mood: "Drone",
chords: [
chord("Em", [52, 55, 59, 64]),
chord("E5/B", [47, 52, 59, 64]),
chord("G/D", [50, 59, 62, 67]),
chord("Dm/A", [45, 57, 62, 65]),
chord("A5", [45, 52, 57, 64]),
chord("C/E", [52, 60, 64, 67]),
chord("G7sus", [55, 60, 62, 65]),
chord("Em7", [52, 55, 59, 62]),
],
bass: [52, 47, 50, 45, 45, 52, 43, 52],
},
Progression {
song_value: 6,
mood: "Sunny",
chords: [
chord("C", [48, 52, 55, 60]),
chord("Gsus4", [55, 60, 62, 67]),
chord("Am", [45, 57, 60, 64]),
chord("F", [53, 57, 60, 65]),
chord("Cadd4", [48, 52, 60, 65]),
chord("Gsus4", [55, 60, 62, 67]),
chord("F", [53, 57, 60, 65]),
chord("C", [48, 55, 60, 64]),
],
bass: [48, 55, 45, 53, 48, 55, 53, 48],
},
Progression {
song_value: 7,
mood: "Lift",
chords: [
chord("G", [55, 59, 62, 67]),
chord("D", [50, 54, 57, 62]),
chord("Em7", [52, 55, 59, 62]),
chord("C", [48, 52, 55, 60]),
chord("G5", [43, 55, 62, 67]),
chord("D", [50, 57, 62, 66]),
chord("E7sus", [52, 59, 62, 64]),
chord("C", [48, 55, 60, 64]),
],
bass: [43, 50, 52, 48, 43, 50, 52, 48],
},
Progression {
song_value: 9,
mood: "Dawn",
chords: [
chord("Dmaj7", [50, 57, 61, 66]),
chord("E/D", [50, 56, 59, 64]),
chord("Bm7", [47, 57, 59, 62]),
chord("A/C#", [49, 57, 61, 64]),
chord("Gmaj9", [43, 57, 59, 66]),
chord("A6", [45, 57, 61, 66]),
chord("F#m7", [42, 57, 61, 64]),
chord("Bm7", [47, 57, 62, 66]),
],
bass: [50, 50, 47, 49, 43, 45, 42, 47],
},
Progression {
song_value: 10,
mood: "Rain",
chords: [
chord("Dm9", [50, 53, 60, 64]),
chord("G6", [43, 55, 59, 64]),
chord("Am7", [45, 55, 60, 64]),
chord("Cmaj7", [48, 55, 59, 64]),
chord("Fmaj7", [41, 57, 60, 64]),
chord("Em", [52, 55, 59, 64]),
chord("G", [43, 55, 59, 62]),
chord("Asus", [45, 57, 62, 64]),
],
bass: [50, 43, 45, 48, 41, 52, 43, 45],
},
Progression {
song_value: 11,
mood: "Night",
chords: [
chord("F#m7", [54, 57, 61, 64]),
chord("Dmaj7", [50, 57, 61, 66]),
chord("A", [45, 57, 61, 64]),
chord("Esus4", [52, 57, 59, 64]),
chord("Bm7", [47, 57, 59, 62]),
chord("Dadd9", [50, 54, 57, 64]),
chord("A/C#", [49, 57, 61, 64]),
chord("C#m7", [49, 56, 59, 64]),
],
bass: [42, 50, 45, 52, 47, 50, 49, 49],
},
Progression {
song_value: 12,
mood: "Glow",
chords: [
chord("Ebmaj7", [51, 55, 58, 62]),
chord("Abmaj7", [44, 55, 60, 63]),
chord("Cm7", [48, 55, 58, 63]),
chord("Bbsus4", [46, 53, 58, 63]),
chord("Fm7", [41, 56, 60, 63]),
chord("Abm", [44, 56, 59, 63]),
chord("Eb/G", [43, 55, 58, 63]),
chord("Bb7sus", [46, 56, 58, 63]),
],
bass: [51, 44, 48, 46, 41, 44, 43, 46],
},
Progression {
song_value: 13,
mood: "Float",
chords: [
chord("Cadd9", [48, 55, 62, 64]),
chord("Bb", [46, 58, 62, 65]),
chord("F/A", [45, 57, 60, 65]),
chord("Gm7", [43, 58, 62, 65]),
chord("Dm7", [50, 57, 60, 65]),
chord("Bbmaj7", [46, 57, 62, 65]),
chord("F", [41, 57, 60, 65]),
chord("Csus4", [48, 55, 60, 65]),
],
bass: [48, 46, 45, 43, 50, 46, 41, 48],
},
Progression {
song_value: 14,
mood: "Deep",
chords: [
chord("Cm", [48, 55, 60, 63]),
chord("Fm/C", [48, 56, 60, 65]),
chord("Ab/C", [48, 56, 60, 63]),
chord("Bb/C", [48, 58, 62, 65]),
chord("Gm7", [43, 58, 62, 65]),
chord("Ebmaj7", [51, 55, 58, 62]),
chord("Abmaj7", [44, 55, 60, 63]),
chord("Gsus4", [43, 55, 60, 62]),
],
bass: [48, 48, 48, 48, 43, 51, 44, 43],
},
];
const CUSTOM_SONG_VALUE: i8 = 8;
pub(crate) const PROGRESSION_SONG_VALUES: [i8; CUSTOM_PROGRESSION_INDEX + 1] = {
let mut values = [CUSTOM_SONG_VALUE; CUSTOM_PROGRESSION_INDEX + 1];
let mut index = 0;
while index < PROGRESSIONS.len() {
values[index] = PROGRESSIONS[index].song_value;
index += 1;
}
values
};
pub(crate) fn pad_chord_midi(progression: usize, slot: usize) -> [i32; 4] {
PROGRESSIONS[progression % PROGRESSIONS.len()].chords[slot % CHORD_SLOT_COUNT].notes
}
pub(crate) fn progression_key(progression: &Progression) -> &'static str {
let name = progression.chords[0].name;
let root_len = match name.as_bytes().get(1) {
Some(b'b' | b'#') => 2,
_ => 1,
};
let rest = &name[root_len..];
if rest.starts_with('m') && !rest.starts_with("maj") {
&name[..root_len + 1]
} else {
&name[..root_len]
}
}
pub(crate) fn progression_label(progression: usize) -> String {
match PROGRESSIONS.get(progression) {
Some(built_in) => format!("{} · {}", progression_key(built_in), built_in.mood),
None => "Custom".to_string(),
}
}
pub(crate) const CUSTOM_PROGRESSION_INDEX: usize = PROGRESSIONS.len();
pub(crate) fn progression_index(value: f32) -> usize {
wrapped_index(value, CUSTOM_PROGRESSION_INDEX + 1)
}
pub(crate) fn is_custom_progression(progression: usize) -> bool {
progression == CUSTOM_PROGRESSION_INDEX
}
#[derive(Clone, Copy, Debug, PartialEq)]
pub(crate) struct ChordWindow {
pub(crate) progression: usize,
pub(crate) count: usize,
pub(crate) offset: usize,
}
impl ChordWindow {
pub(crate) fn requested(c: &PadControls) -> Self {
Self {
progression: progression_index(c.progression),
count: c.chord_count.round().clamp(1.0, CHORD_SLOT_COUNT as f32) as usize,
offset: wrapped_index(c.chord_offset, CHORD_SLOT_COUNT),
}
}
pub(crate) fn slot(self, step: usize) -> usize {
(self.offset + step) % CHORD_SLOT_COUNT
}
pub(crate) fn slots(self) -> impl Iterator<Item = usize> {
(0..self.count).map(move |step| self.slot(step))
}
}
#[derive(Clone, Copy, Debug, PartialEq)]
pub(crate) struct ProgressionCursor {
pub(crate) window: ChordWindow,
pub(crate) chord_beats: f32,
pub(crate) step: usize,
next_chord_beat: Option<f64>,
}
impl ProgressionCursor {
pub(crate) fn new(c: &PadControls) -> Self {
Self {
window: ChordWindow::requested(c),
chord_beats: c.chord_bars * 4.0,
step: 0,
next_chord_beat: None,
}
}
pub(crate) fn slot(&self) -> usize {
self.window.slot(self.step)
}
pub(crate) fn tick(&mut self, c: &PadControls, timing: TimingContext) -> bool {
let next = *self.next_chord_beat.get_or_insert_with(|| {
GridSpec::new(self.chord_beats, 0.0, 0.0)
.hit_after(timing.beat)
.beat
});
if timing.transport == Transport::Stopped || timing.beat + GRID_BEAT_EPSILON < next {
return false;
}
let window = ChordWindow::requested(c);
let chord_beats = c.chord_bars * 4.0;
let old_next_slot = self.window.slot((self.step + 1) % self.window.count);
self.step = if window.progression != self.window.progression {
0
} else if let Some(current_step) = window.slots().position(|slot| slot == self.slot()) {
(current_step + 1) % window.count
} else {
window
.slots()
.position(|slot| slot == old_next_slot)
.unwrap_or(0)
};
self.window = window;
self.chord_beats = chord_beats;
self.next_chord_beat =
Some(next + GridSpec::new(self.chord_beats, 0.0, 0.0).interval_beats);
true
}
}
pub(crate) struct ProgressionFollower {
pub(crate) cursor: Option<ProgressionCursor>,
}
impl ProgressionFollower {
pub(crate) fn new() -> Self {
Self { cursor: None }
}
pub(crate) fn follow(&mut self, pad: &PadControls, timing: TimingContext) -> (usize, usize) {
let cursor = self
.cursor
.get_or_insert_with(|| ProgressionCursor::new(pad));
cursor.tick(pad, timing);
(cursor.window.progression, cursor.slot())
}
}
pub(crate) fn pad_chord_tones(c: &PadControls, progression: usize, slot: usize) -> [i32; 4] {
if is_custom_progression(progression) {
pad_chord_notes_with_slot(&c.chord_slots[slot])
} else {
pad_chord_midi(progression, slot)
}
}
pub(crate) fn pad_chord_name(c: &PadControls, progression: usize, slot: usize) -> String {
match PROGRESSIONS.get(progression) {
Some(built_in) => built_in.chords[slot % CHORD_SLOT_COUNT].name.to_string(),
None => custom_chord_name(&c.chord_slots[slot]),
}
}
pub(crate) fn pad_chord_notes_with_slot(slot: &ChordSlotControls) -> [i32; 4] {
let root = slot_root(slot);
let accidental = slot.accidental.round().clamp(-1.0, 1.0) as i32;
let extension = slot.extension.round().clamp(0.0, 3.0) as i32;
let inversion = slot.inversion.round().clamp(0.0, 3.0) as i32;
let third = slot_third(slot, root);
let top = match extension {
1 => shift_diatonic(root, 6),
2 => shift_diatonic(root, 8),
3 => shift_diatonic(root, 10),
_ => root + 12,
};
let mut notes = [root, third, shift_diatonic(root, 4), top];
apply_inversion(&mut notes, inversion);
if accidental != 0 {
notes = notes.map(|note| note + accidental);
}
dedupe_upwards(&mut notes);
notes
}
pub(crate) fn pad_chord_root_note(slot: &ChordSlotControls) -> i32 {
slot_root(slot) + slot.accidental.round().clamp(-1.0, 1.0) as i32
}
pub(crate) fn pad_chord_slot_is_minor(slot: &ChordSlotControls) -> bool {
let root = slot_root(slot);
slot_third(slot, root) - root == 3
}
const CUSTOM_TONIC: i32 = 45;
fn slot_root(slot: &ChordSlotControls) -> i32 {
shift_diatonic(CUSTOM_TONIC, slot.degree.round().clamp(-7.0, 7.0) as i32)
}
fn slot_third(slot: &ChordSlotControls, root: i32) -> i32 {
match slot.quality.round().clamp(-1.0, 1.0) as i32 {
-1 => root + 3,
1 => root + 4,
_ => shift_diatonic(root, 2),
}
}
fn shift_diatonic(note: i32, steps: i32) -> i32 {
const SCALE: [i32; 7] = [0, 2, 4, 5, 7, 9, 11];
let octave = note.div_euclid(12);
let pitch = note.rem_euclid(12);
let degree = SCALE
.iter()
.position(|&pc| pc == pitch)
.map(|index| octave * 7 + index as i32)
.unwrap_or_else(|| octave * 7);
let shifted = degree + steps;
let shifted_octave = shifted.div_euclid(7);
let shifted_degree = shifted.rem_euclid(7) as usize;
shifted_octave * 12 + SCALE[shifted_degree]
}
fn apply_inversion(notes: &mut [i32; 4], inversion: i32) {
notes.sort_unstable();
for _ in 0..inversion {
notes[0] += 12;
notes.sort_unstable();
}
}
fn dedupe_upwards(notes: &mut [i32; 4]) {
notes.sort_unstable();
for i in 1..notes.len() {
while notes[i] <= notes[i - 1] {
notes[i] = shift_diatonic(notes[i], 1);
}
}
}
pub(crate) fn custom_chord_name(slot: &ChordSlotControls) -> String {
const NATURALS: [&str; 12] = ["C", "", "D", "", "E", "F", "", "G", "", "A", "", "B"];
let natural = slot_root(slot);
let accidental = slot.accidental.round().clamp(-1.0, 1.0) as i32;
let minor = slot_third(slot, natural) - natural == 3;
let flat_five = shift_diatonic(natural, 4) - natural == 6;
let minor_seventh = shift_diatonic(natural, 6) - natural == 10;
let body = match (slot.extension.round().clamp(0.0, 3.0) as i32, minor) {
(1, true) => "m6",
(1, false) => "6",
(2, true) if minor_seventh && flat_five => "m7b5",
(2, true) if minor_seventh => "m7",
(2, true) => "mmaj7",
(2, false) if minor_seventh => "7",
(2, false) => "maj7",
(3, true) => "madd9",
(3, false) => "add9",
(_, true) if flat_five => "dim",
(_, true) => "m",
(_, false) => "",
};
let flat_five_suffix = if flat_five && !matches!(body, "dim" | "m7b5") {
"b5"
} else {
""
};
let root_name = format!(
"{}{}",
NATURALS[natural.rem_euclid(12) as usize],
match accidental {
-1 => "b",
1 => "#",
_ => "",
}
);
let bass = pad_chord_notes_with_slot(slot)[0];
let slash = if (bass - natural - accidental).rem_euclid(12) == 0 {
String::new()
} else {
format!("/{}", pitch_class_name(bass, accidental > 0))
};
format!("{root_name}{body}{flat_five_suffix}{slash}")
}
fn pitch_class_name(note: i32, sharps: bool) -> &'static str {
const FLATS: [&str; 12] = [
"C", "Db", "D", "Eb", "E", "F", "Gb", "G", "Ab", "A", "Bb", "B",
];
const SHARPS: [&str; 12] = [
"C", "C#", "D", "D#", "E", "F", "F#", "G", "G#", "A", "A#", "B",
];
let pitch = note.rem_euclid(12) as usize;
if sharps { SHARPS[pitch] } else { FLATS[pitch] }
}