use super::*;
pub(crate) const MAX_PAD_LAYERS: usize = 4;
const PAD_TYPE_CROSSFADE_SECONDS: f32 = 0.03;
pub(crate) struct PadEngine {
pub(crate) sample_rate: f32,
pub(crate) layers: Vec<PadLayer>,
pub(crate) chord_trigger: GridTrigger,
pub(crate) step_index: usize,
pub(crate) active_progression: usize,
pub(crate) active_chord_count: usize,
pub(crate) active_character: usize,
pub(crate) last_chord_notes: [i32; 4],
pub(crate) width_lfo: DriftingLfo,
pub(crate) air: WhiteNoise,
pub(crate) rng: StdRng,
pub(crate) telemetry: Arc<FluidTelemetry>,
}
impl PadEngine {
pub(crate) fn new(
sample_rate: f32,
c: &PadControls,
tune: f32,
telemetry: Arc<FluidTelemetry>,
) -> Self {
let active_progression = progression_index(c.progression);
let active_character = pad_type_index(c.voice_type);
let initial_notes = pad_chord_tones(c, active_progression, 0);
Self {
sample_rate,
layers: vec![PadLayer::new(
active_character,
initial_notes,
tune,
sample_rate,
c.attack_time,
c.release_time,
)],
chord_trigger: GridTrigger::after_start(),
step_index: 0,
active_progression,
active_chord_count: pad_chord_count(c),
active_character,
last_chord_notes: initial_notes,
width_lfo: DriftingLfo::new(1.0 / 54.0, sample_rate),
air: WhiteNoise::new(),
rng: StdRng::from_entropy(),
telemetry,
}
}
pub(crate) fn next(&mut self, c: &PadControls, tune: f32, timing: TimingContext) -> (f32, f32) {
let progression = progression_index(c.progression);
let chord_count = pad_chord_count(c);
let advance = self.chord_trigger.pop(timing, c.chord_bars * 4.0, 0.0);
advance_pad_progression(
&mut self.step_index,
&mut self.active_chord_count,
&mut self.active_progression,
chord_count,
progression,
advance,
);
let chord_notes = pad_chord_tones(c, self.active_progression, self.step_index);
let chord_edited = chord_notes != self.last_chord_notes;
let character = pad_type_index(c.voice_type);
let character_changed = character != self.active_character;
self.last_chord_notes = chord_notes;
self.active_character = character;
if character_changed {
for layer in &mut self.layers {
layer.set_character(character, self.sample_rate);
}
}
if advance || chord_edited {
for layer in &mut self.layers {
layer.release();
}
self.telemetry
.chord_index
.store(self.step_index as u64, Ordering::Relaxed);
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,
tune,
self.sample_rate,
c.attack_time,
c.release_time,
));
}
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 air = self.air.next_filtered(&mut self.rng, 0.0002) * 0.00025;
const OUTPUT_TRIM: f32 = 0.95;
(
(dry_l * OUTPUT_TRIM + air) * c.level,
(dry_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],
tune: f32,
sample_rate: f32,
attack_time: f32,
release_time: f32,
) -> Self {
Self {
tones: pad_tones(
character,
notes,
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,
weight: f32,
step: f32,
}
pub(crate) struct PadTone {
pub(crate) stack: PadOscStack,
pub(crate) stage: PadStage,
pub(crate) output_gain: f32,
hz: f32,
outgoing: Option<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(OutgoingStage {
stage: std::mem::replace(&mut self.stage, stage),
output_gain: std::mem::replace(&mut self.output_gain, output_gain),
weight: 1.0,
step: 1.0 / (PAD_TYPE_CROSSFADE_SECONDS * sample_rate).max(1.0),
});
}
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.stage.apply(raw) * 0.55)
* envelope
* self.stack.gain
* outgoing.output_gain;
shaped += (previous - shaped) * outgoing.weight;
outgoing.weight -= outgoing.step;
if outgoing.weight <= 0.0 {
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],
tune: f32,
sample_rate: f32,
attack_time: f32,
release_time: f32,
) -> Vec<PadTone> {
let freqs = notes.map(|note| midi_to_hz(note) * tune_ratio(tune));
let pans = [-0.52_f32, -0.18, 0.16, 0.46];
let gains = [0.17_f32, 0.132, 0.126, 0.098];
freqs
.iter()
.zip(pans)
.zip(gains)
.map(|((hz, pan), gain)| {
PadTone::new(
character,
*hz,
pan,
gain,
attack_time,
release_time,
sample_rate,
)
})
.collect()
}
pub(crate) const PROGRESSIONS: [[[i32; 4]; 8]; 8] = [
[
[45, 50, 55, 60], [43, 50, 57, 60], [45, 52, 57, 60], [47, 52, 55, 62], [45, 52, 57, 64], [43, 50, 55, 62], [48, 55, 60, 64], [55, 59, 64, 67], ],
[
[45, 50, 57, 60], [50, 53, 57, 62], [48, 55, 60, 64], [43, 50, 55, 59], [41, 48, 53, 57], [52, 59, 64, 67], [45, 52, 57, 60], [43, 50, 55, 59], ],
[
[45, 48, 52, 55], [41, 45, 48, 52], [48, 52, 55, 59], [43, 47, 50, 53], [50, 53, 57, 60], [52, 55, 59, 62], [47, 50, 53, 57], [43, 50, 55, 59], ],
[
[45, 52, 57, 60], [41, 45, 48, 55], [48, 55, 59, 62], [43, 50, 53, 57], [50, 57, 60, 64], [52, 55, 59, 64], [47, 53, 57, 64], [43, 50, 55, 64], ],
[
[45, 48, 52, 57], [46, 50, 53, 57], [48, 53, 55, 60], [50, 53, 60, 62], [52, 59, 62, 64], [55, 59, 62, 64], [55, 58, 62, 65], [45, 52, 58, 62], ],
[
[52, 55, 59, 64], [47, 52, 59, 64], [50, 59, 62, 67], [45, 57, 62, 65], [45, 52, 57, 64], [52, 60, 64, 67], [55, 60, 62, 65], [52, 55, 59, 62], ],
[
[48, 52, 55, 60], [55, 60, 62, 67], [45, 57, 60, 64], [53, 57, 60, 65], [48, 52, 60, 65], [55, 60, 62, 67], [53, 57, 60, 65], [48, 55, 60, 64], ],
[
[55, 59, 62, 67], [50, 54, 57, 62], [52, 55, 59, 62], [48, 52, 55, 60], [43, 55, 62, 67], [50, 57, 62, 66], [52, 59, 62, 64], [48, 55, 60, 64], ],
];
pub(crate) fn pad_chord_midi(progression: usize, step: usize) -> [i32; 4] {
PROGRESSIONS[progression % PROGRESSIONS.len()][step % 8]
}
pub(crate) const CUSTOM_PROGRESSION_INDEX: usize = PROGRESSIONS.len();
pub(crate) fn progression_index(value: f32) -> usize {
(value.round() as i64).rem_euclid((PROGRESSIONS.len() + 1) as i64) as usize
}
pub(crate) fn is_custom_progression(progression: usize) -> bool {
progression == CUSTOM_PROGRESSION_INDEX
}
pub(crate) fn pad_chord_count(c: &PadControls) -> usize {
c.chord_count.round().clamp(1.0, CHORD_SLOT_COUNT as f32) as usize
}
pub(crate) fn advance_pad_progression(
step_index: &mut usize,
active_chord_count: &mut usize,
active_progression: &mut usize,
requested_chord_count: usize,
requested_progression: usize,
advance: bool,
) {
if !advance {
return;
}
*step_index += 1;
if *step_index == *active_chord_count {
*step_index = 0;
*active_chord_count = requested_chord_count;
*active_progression = requested_progression;
}
}
pub(crate) fn pad_chord_tones(c: &PadControls, progression: usize, step: usize) -> [i32; 4] {
if is_custom_progression(progression) {
pad_chord_notes_with_slot(&c.chord_slots[step])
} else {
pad_chord_midi(progression, step)
}
}
pub(crate) fn pad_chord_notes_with_slot(slot: &ChordSlotControls) -> [i32; 4] {
const TONIC: i32 = 45; let root = shift_diatonic(TONIC, slot.degree.round().clamp(-7.0, 7.0) as i32);
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 = match slot.quality.round().clamp(-1.0, 1.0) as i32 {
-1 => root + 3,
1 => root + 4,
_ => shift_diatonic(root, 2),
};
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 {
const TONIC: i32 = 45;
let root = shift_diatonic(TONIC, slot.degree.round().clamp(-7.0, 7.0) as i32);
root + slot.accidental.round().clamp(-1.0, 1.0) as i32
}
pub(crate) fn pad_chord_slot_is_minor(slot: &ChordSlotControls) -> bool {
match slot.quality.round().clamp(-1.0, 1.0) as i32 {
-1 => true,
1 => false,
_ => {
const TONIC: i32 = 45;
let root = shift_diatonic(TONIC, slot.degree.round().clamp(-7.0, 7.0) as i32);
shift_diatonic(root, 2) - root == 3
}
}
}
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);
}
}
}