use crate::params::{ArpMode, ArpParams, MidiNote};
#[cfg(feature = "arp")]
#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)]
pub struct ArpEvents {
pub off: Option<MidiNote>,
pub on: Option<MidiNote>,
}
#[cfg(feature = "arp")]
#[derive(Debug, Clone)]
pub struct Arpeggiator {
phase: f32,
pub(crate) step: u8,
pub(crate) sounding: Option<MidiNote>,
gate_fired: bool,
direction: u8,
lfsr: u32,
}
#[cfg(feature = "arp")]
impl Default for Arpeggiator {
fn default() -> Self {
Self {
phase: 0.0,
step: 0,
sounding: None,
gate_fired: false,
direction: 0,
lfsr: 0xACE1_FEED,
}
}
}
#[cfg(feature = "arp")]
impl Arpeggiator {
#[must_use]
pub fn new() -> Self {
Self::default()
}
pub fn tick(&mut self, sample_rate: f32, params: &ArpParams) -> ArpEvents {
let count = params.count.min(4);
if count == 0 {
return ArpEvents::default();
}
let gate = params.gate.clamp(0.0, 1.0);
let mut result = ArpEvents::default();
if self.sounding.is_some() && !self.gate_fired && self.phase >= gate {
result.off = self.sounding;
self.gate_fired = true;
}
self.phase += (params.rate / sample_rate).clamp(0.0, 1.0);
if self.phase >= 1.0 {
self.phase = (self.phase - 1.0).max(0.0);
let pre_gate_fired = self.gate_fired;
self.gate_fired = false;
if let Some(note) = self.sounding.take() {
if !pre_gate_fired {
result.off = Some(note);
}
self.advance_step(count, params);
}
}
if self.sounding.is_none() {
let note = params.notes[self.step as usize % 4];
self.sounding = Some(note);
result.on = Some(note);
}
result
}
fn advance_step(&mut self, count: u8, params: &ArpParams) {
debug_assert!(count > 0 && count <= 4);
match params.mode {
ArpMode::Up => self.step = (self.step + 1) % count,
ArpMode::Down => self.step = (self.step + count - 1) % count,
ArpMode::UpDown => self.advance_step_updown(count),
ArpMode::Random => {
self.tick_lfsr();
#[allow(clippy::cast_possible_truncation)]
{
self.step = (self.lfsr % u32::from(count)) as u8;
}
}
}
}
fn advance_step_updown(&mut self, count: u8) {
if count <= 1 {
self.step = 0;
return;
}
if self.direction == 0 {
self.step += 1;
if self.step >= count {
self.step = count.saturating_sub(2);
self.direction = 1;
}
} else if self.step == 0 {
self.step = 1.min(count - 1);
self.direction = 0;
} else {
self.step -= 1;
}
}
fn tick_lfsr(&mut self) {
let bit = self.lfsr & 1;
self.lfsr >>= 1;
if bit != 0 {
self.lfsr ^= 0xB4BC_D35C;
}
}
pub fn add_note(&mut self, params: &mut ArpParams, note: MidiNote) {
if params.count >= 4 {
return;
}
if params.notes[..params.count as usize].contains(¬e) {
return;
}
params.notes[params.count as usize] = note;
params.count += 1;
}
pub fn remove_note(&mut self, params: &mut ArpParams, note: MidiNote) {
let count = params.count as usize;
if let Some(pos) = params.notes[..count].iter().position(|&n| n == note) {
for i in pos..count - 1 {
params.notes[i] = params.notes[i + 1];
}
params.count -= 1;
if params.count == 0 {
self.step = 0;
self.direction = 0;
self.phase = 0.0;
self.gate_fired = false;
} else if self.step >= params.count {
self.step = params.count - 1;
}
}
}
pub fn set_notes(&mut self, params: &mut ArpParams, notes: &[MidiNote]) {
let n = notes.len().min(4);
params.notes[..n].copy_from_slice(¬es[..n]);
#[allow(clippy::cast_possible_truncation)]
{
params.count = n as u8;
}
self.step = 0;
self.direction = 0;
self.phase = 0.0;
self.sounding = None;
self.gate_fired = false;
}
pub fn panic(&mut self, params: &mut ArpParams) {
params.count = 0;
self.step = 0;
self.direction = 0;
self.phase = 0.0;
self.sounding = None;
self.gate_fired = false;
}
}
#[cfg(all(test, feature = "arp"))]
mod tests {
use super::*;
use crate::params::{ArpMode, ArpParams, MidiNote};
const SR: f32 = 44100.0;
fn up_params(notes: &[u8]) -> ArpParams {
#[allow(clippy::cast_possible_truncation)]
let count = notes.len().min(4) as u8;
let mut note_arr = ArpParams::default().notes;
for (i, &n) in notes.iter().enumerate().take(4) {
note_arr[i] = MidiNote(n);
}
ArpParams {
enabled: true,
rate: SR,
mode: ArpMode::Up,
notes: note_arr,
count,
..ArpParams::default()
}
}
#[test]
fn count_zero_returns_no_events() {
let mut arp = Arpeggiator::new();
let params = ArpParams {
count: 0,
..ArpParams::default()
};
let e = arp.tick(SR, ¶ms);
assert_eq!(e.on, None);
assert_eq!(e.off, None);
}
#[test]
fn first_tick_fires_note_on_immediately() {
let mut arp = Arpeggiator::new();
let params = up_params(&[60, 64, 67]);
let e = arp.tick(SR, ¶ms);
assert_eq!(e.on, Some(MidiNote(60)));
assert_eq!(e.off, None);
}
#[test]
fn up_mode_cycles_three_notes_in_order() {
let mut arp = Arpeggiator::new();
let params = up_params(&[60, 64, 67]);
let e = arp.tick(SR, ¶ms);
assert_eq!(e.on, Some(MidiNote(60)), "tick 1 NoteOn");
assert_eq!(e.off, None, "tick 1 no NoteOff");
let e = arp.tick(SR, ¶ms);
assert_eq!(e.off, Some(MidiNote(60)), "tick 2 NoteOff 60");
assert_eq!(e.on, Some(MidiNote(64)), "tick 2 NoteOn 64");
let e = arp.tick(SR, ¶ms);
assert_eq!(e.off, Some(MidiNote(64)), "tick 3 NoteOff 64");
assert_eq!(e.on, Some(MidiNote(67)), "tick 3 NoteOn 67");
let e = arp.tick(SR, ¶ms);
assert_eq!(e.off, Some(MidiNote(67)), "tick 4 NoteOff 67");
assert_eq!(e.on, Some(MidiNote(60)), "tick 4 NoteOn 60 (wrap)");
}
#[test]
fn gate_fires_note_off_mid_step() {
let mut arp = Arpeggiator::new();
let mut params = up_params(&[60, 64]);
params.rate = SR / 2.0;
params.gate = 0.5;
let e = arp.tick(SR, ¶ms);
assert_eq!(e.on, Some(MidiNote(60)), "s1 NoteOn");
assert_eq!(e.off, None, "s1 no NoteOff");
let e = arp.tick(SR, ¶ms);
assert_eq!(e.off, Some(MidiNote(60)), "s2 NoteOff");
assert_eq!(e.on, Some(MidiNote(64)), "s2 NoteOn");
}
#[test]
fn gate_1_fires_note_off_at_step_boundary() {
let mut arp = Arpeggiator::new();
let mut params = up_params(&[60, 64]);
params.rate = SR / 2.0;
params.gate = 1.0;
let e = arp.tick(SR, ¶ms);
assert_eq!(e.on, Some(MidiNote(60)));
assert_eq!(e.off, None);
let e = arp.tick(SR, ¶ms);
assert_eq!(e.off, Some(MidiNote(60)), "gate=1.0 NoteOff at boundary");
assert_eq!(e.on, Some(MidiNote(64)));
}
#[test]
fn down_mode_cycles_descending() {
let mut arp = Arpeggiator::new();
let mut params = up_params(&[60, 64, 67]);
params.mode = ArpMode::Down;
let e = arp.tick(SR, ¶ms);
assert_eq!(e.on, Some(MidiNote(60)));
let e = arp.tick(SR, ¶ms);
assert_eq!(e.off, Some(MidiNote(60)));
assert_eq!(e.on, Some(MidiNote(67)));
let e = arp.tick(SR, ¶ms);
assert_eq!(e.off, Some(MidiNote(67)));
assert_eq!(e.on, Some(MidiNote(64)));
let e = arp.tick(SR, ¶ms);
assert_eq!(e.off, Some(MidiNote(64)));
assert_eq!(e.on, Some(MidiNote(60)));
}
#[test]
fn updown_mode_bounces_at_ends() {
let mut arp = Arpeggiator::new();
let mut params = up_params(&[60, 64, 67]);
params.mode = ArpMode::UpDown;
let expected_notes = [60u8, 64, 67, 64, 60, 64, 67, 64, 60];
let mut prev_on: Option<MidiNote> = None;
for (i, &n) in expected_notes.iter().enumerate() {
let e = arp.tick(SR, ¶ms);
assert_eq!(
e.on,
Some(MidiNote(n)),
"tick {} expected NoteOn({})",
i + 1,
n
);
if i > 0 {
assert_eq!(
e.off,
prev_on,
"tick {} expected NoteOff({:?})",
i + 1,
prev_on
);
}
prev_on = e.on;
}
}
#[test]
fn random_mode_stays_in_bounds() {
let mut arp = Arpeggiator::new();
let mut params = up_params(&[60, 64, 67, 69]); params.mode = ArpMode::Random;
for _ in 0..1000 {
let e = arp.tick(SR, ¶ms);
if let Some(note) = e.on {
assert!(
params.notes[..params.count as usize].contains(¬e),
"random arp emitted note {note:?} not in list"
);
}
}
}
#[test]
fn random_mode_single_note_always_returns_same_note() {
let mut arp = Arpeggiator::new();
let mut params = up_params(&[60]);
params.mode = ArpMode::Random;
for _ in 0..20 {
let e = arp.tick(SR, ¶ms);
if let Some(note) = e.on {
assert_eq!(note, MidiNote(60));
}
}
}
#[test]
fn add_note_appends_to_list() {
let mut arp = Arpeggiator::new();
let mut params = ArpParams::default();
arp.add_note(&mut params, MidiNote(60));
assert_eq!(params.count, 1);
assert_eq!(params.notes[0], MidiNote(60));
}
#[test]
fn add_note_ignores_duplicate() {
let mut arp = Arpeggiator::new();
let mut params = ArpParams::default();
arp.add_note(&mut params, MidiNote(60));
arp.add_note(&mut params, MidiNote(60));
assert_eq!(params.count, 1);
}
#[test]
fn add_note_caps_at_four() {
let mut arp = Arpeggiator::new();
let mut params = ArpParams::default();
for n in [60u8, 62, 64, 65, 67] {
arp.add_note(&mut params, MidiNote(n));
}
assert_eq!(params.count, 4, "list must not exceed 4");
assert_eq!(params.notes[3], MidiNote(65), "5th note must be rejected");
}
#[test]
fn remove_note_shifts_remaining_down() {
let mut arp = Arpeggiator::new();
let mut params = ArpParams::default();
for n in [60u8, 64, 67] {
arp.add_note(&mut params, MidiNote(n));
}
arp.remove_note(&mut params, MidiNote(64));
assert_eq!(params.count, 2);
assert_eq!(params.notes[0], MidiNote(60));
assert_eq!(params.notes[1], MidiNote(67));
}
#[test]
fn remove_note_missing_note_is_no_op() {
let mut arp = Arpeggiator::new();
let mut params = ArpParams::default();
arp.add_note(&mut params, MidiNote(60));
arp.remove_note(&mut params, MidiNote(99));
assert_eq!(params.count, 1);
}
#[test]
fn remove_note_clamps_step_when_list_shrinks() {
let mut arp = Arpeggiator::new();
let mut params = ArpParams::default();
for n in [60u8, 64, 67] {
arp.add_note(&mut params, MidiNote(n));
}
params.enabled = true;
params.rate = SR;
arp.tick(SR, ¶ms); arp.tick(SR, ¶ms); arp.tick(SR, ¶ms); arp.remove_note(&mut params, MidiNote(67));
assert!(arp.step < params.count, "step must be < new count");
}
#[test]
fn set_notes_replaces_list_and_resets_state() {
let mut arp = Arpeggiator::new();
let mut params = up_params(&[60, 64, 67]);
params.rate = SR;
arp.tick(SR, ¶ms);
arp.tick(SR, ¶ms);
let new_notes = [MidiNote(72), MidiNote(76)];
arp.set_notes(&mut params, &new_notes);
assert_eq!(params.count, 2);
assert_eq!(params.notes[0], MidiNote(72));
assert_eq!(params.notes[1], MidiNote(76));
assert_eq!(arp.step, 0);
assert_eq!(arp.sounding, None);
params.enabled = true;
params.rate = SR;
let e = arp.tick(SR, ¶ms);
assert_eq!(e.on, Some(MidiNote(72)));
}
#[test]
fn panic_clears_all_state() {
let mut arp = Arpeggiator::new();
let mut params = up_params(&[60, 64, 67]);
params.rate = SR;
arp.tick(SR, ¶ms);
arp.tick(SR, ¶ms);
arp.panic(&mut params);
assert_eq!(params.count, 0);
assert_eq!(arp.sounding, None);
assert_eq!(arp.step, 0);
let e = arp.tick(SR, ¶ms);
assert_eq!(e.on, None);
assert_eq!(e.off, None);
}
}