use super::*;
pub(crate) const ARP_RATE_BEATS_MIN: f32 = 0.125;
pub(crate) const ARP_RATE_BEATS_MAX: f32 = 4.0;
pub(crate) const ARP_OCTAVES_MIN: f32 = 1.0;
pub(crate) const ARP_OCTAVES_MAX: f32 = 3.0;
pub(crate) const ARP_CHORD_TONES: usize = 4;
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub(crate) enum ArpPattern {
Up,
Down,
UpDown,
Random,
}
const ARP_PATTERNS: [ArpPattern; 4] = [
ArpPattern::Up,
ArpPattern::Down,
ArpPattern::UpDown,
ArpPattern::Random,
];
pub(crate) fn arp_pattern_from_control(value: f32) -> ArpPattern {
ARP_PATTERNS[wrapped_index(value, ARP_PATTERNS.len())]
}
pub(crate) fn arp_pattern_label(value: f32) -> &'static str {
match arp_pattern_from_control(value) {
ArpPattern::Up => "Up",
ArpPattern::Down => "Down",
ArpPattern::UpDown => "Up-Down",
ArpPattern::Random => "Random",
}
}
pub(crate) fn arp_octave_span(value: f32) -> usize {
(value.round() as i32).clamp(ARP_OCTAVES_MIN as i32, ARP_OCTAVES_MAX as i32) as usize
}
pub(crate) fn arp_cycle_notes(chord: [i32; ARP_CHORD_TONES], octaves: usize) -> Vec<i32> {
let mut notes = Vec::with_capacity(ARP_CHORD_TONES * octaves.max(1));
for octave in 0..octaves.max(1) {
for tone in chord {
notes.push(tone + 12 * octave as i32);
}
}
notes.sort_unstable();
notes
}
pub(crate) fn arp_ping_pong_advance(pos: usize, dir: i32, len: usize) -> (usize, i32) {
if len <= 1 {
return (0, dir);
}
let proposed = pos as i32 + dir;
if proposed < 0 {
(1, 1)
} else if proposed >= len as i32 {
(len - 2, -1)
} else {
(proposed as usize, dir)
}
}
pub(crate) fn arp_advance(
pos: usize,
pattern: ArpPattern,
len: usize,
dir: i32,
rng: &mut StdRng,
) -> (usize, i32) {
match pattern {
ArpPattern::Up => ((pos + 1) % len, dir),
ArpPattern::Down => ((pos + len - 1) % len, dir),
ArpPattern::UpDown => arp_ping_pong_advance(pos, dir, len),
ArpPattern::Random => (rng.gen_range(0..len), dir),
}
}
pub(crate) struct ArpEngine {
pub(crate) sample_rate: f32,
pub(crate) progression: ProgressionFollower,
pub(crate) note_trigger: GridTrigger,
pub(crate) cycle_pos: usize,
pub(crate) ping_pong_dir: i32,
pub(crate) voices: Vec<TonalVoice>,
pub(crate) rng: StdRng,
}
impl ArpEngine {
pub(crate) fn new(sample_rate: f32) -> Self {
Self {
sample_rate,
progression: ProgressionFollower::new(),
note_trigger: GridTrigger::new(),
cycle_pos: 0,
ping_pong_dir: 1,
voices: Vec::with_capacity(8),
rng: StdRng::from_entropy(),
}
}
pub(crate) fn next(
&mut self,
c: &ArpControls,
pad: &PadControls,
tune: f32,
timing: TimingContext,
) -> (f32, f32) {
let (progression, step) = self.progression.follow(pad, timing);
let rate_beats = c.rate_beats.clamp(ARP_RATE_BEATS_MIN, ARP_RATE_BEATS_MAX);
if self
.note_trigger
.pop_swung(timing, rate_beats, c.offset_beats, c.swing)
{
let chord = pad_chord_tones(pad, progression, step);
let octaves = arp_octave_span(c.octaves);
let notes = arp_cycle_notes(chord, octaves);
let len = notes.len().max(1);
self.cycle_pos = self.cycle_pos.min(len - 1);
let pattern = arp_pattern_from_control(c.pattern);
let note = notes[self.cycle_pos];
let (next_pos, next_dir) = arp_advance(
self.cycle_pos,
pattern,
len,
self.ping_pong_dir,
&mut self.rng,
);
self.cycle_pos = next_pos;
self.ping_pong_dir = next_dir;
let hz = note_hz(note, tune);
let pan = self.rng.gen_range(-0.4f32..0.4);
if c.gain != 0.0 {
self.voices.push(TonalVoice::new(
wrapped_index(c.voice_type, TONAL_SYNTH_TYPES.len()),
TonalNote {
midi: note,
hz,
pan,
sample_rate: self.sample_rate,
attack_time: c.attack,
decay_time: c.decay,
},
));
}
}
let (dry_l, dry_r) =
mix_and_retain(&mut self.voices, TonalVoice::next, TonalVoice::is_done);
(dry_l * c.gain, dry_r * c.gain)
}
}