use alloc::string::String;
use alloc::vec;
use alloc::vec::Vec;
use core::fmt::Write;
use crate::cell_note::CellNote;
use crate::daw::track::Track;
use crate::fixed::fixed::Q15;
use crate::instr_ekn::InstrEkn;
use crate::instrument::{Instrument, InstrumentType};
use crate::module::Module;
use crate::pitch::Pitch;
use crate::track_unit::TrackUnit;
#[derive(Debug, Clone)]
pub struct EuclideanParams {
pub events: u8,
pub steps: u8,
pub rotation: u8,
pub prototype: TrackUnit,
}
pub fn auto_convert_strict_euclidean_tracks(module: &mut Module) {
let n_tracks = module.tracks.len();
for track_idx in 0..n_tracks {
let cur_instr_idx = module.tracks[track_idx].instrument;
let Some(instr) = module.instrument.get(cur_instr_idx) else {
continue;
};
if matches!(instr.instr_type, InstrumentType::Euclidean(_)) {
continue;
}
if let Some(params) = detect_strict_euclidean(&module.tracks[track_idx]) {
convert_track_to_euclidean_in_place(module, track_idx, params);
}
}
}
pub fn detect_strict_euclidean(track: &Track) -> Option<EuclideanParams> {
let k = track.rows.len();
if !(4..=u8::MAX as usize).contains(&k) {
return None;
}
let mut positions: Vec<usize> = Vec::new();
let mut first_pitch: Option<Pitch> = None;
let mut first_velocity = None;
let mut first_instrument: Option<usize> = None;
for (i, cell) in track.rows.iter().enumerate() {
if !cell.effects.is_empty() || !cell.global_effects.is_empty() {
return None;
}
match cell.note {
CellNote::Empty => continue,
CellNote::Play(p) => {
if let Some(fp) = first_pitch {
if fp != p {
return None;
}
} else {
first_pitch = Some(p);
}
if let Some(fv) = first_velocity {
if fv != cell.velocity {
return None;
}
} else {
first_velocity = Some(cell.velocity);
}
match (first_instrument, cell.instrument) {
(None, Some(i)) => first_instrument = Some(i),
(Some(prev), Some(i)) if prev != i => return None,
_ => {}
}
positions.push(i);
}
CellNote::KeyOff | CellNote::NoteCut | CellNote::NoteFade => return None,
}
}
let n = positions.len();
if n < 2 {
return None;
}
let n_u8 = n as u8;
let k_u8 = k as u8;
let pitch = first_pitch.expect("first_pitch set when n >= 2");
let velocity = first_velocity.expect("first_velocity set when n >= 2");
let instrument = first_instrument.unwrap_or(track.instrument);
let canonical = euclidean_pattern(n_u8, k_u8);
for rotation in 0..k {
let all_match = positions.iter().all(|&p| canonical[(p + k - rotation) % k]);
if all_match {
let prototype = TrackUnit {
note: CellNote::Play(pitch),
velocity,
instrument: Some(instrument),
effects: Vec::new(),
global_effects: Vec::new(),
};
return Some(EuclideanParams {
events: n_u8,
steps: k_u8,
rotation: rotation as u8,
prototype,
});
}
}
None
}
pub fn euclidean_pattern(events: u8, steps: u8) -> Vec<bool> {
let s = steps as i32;
let mut result: Vec<bool> = Vec::with_capacity(steps as usize);
if events == 0 {
result.resize(steps as usize, false);
return result;
}
let e = events as i32;
let mut error: i32 = 0;
for _ in 0..steps {
if error < 0 {
result.push(false);
error += e;
} else {
result.push(true);
error += e - s;
}
}
result
}
fn convert_track_to_euclidean_in_place(
module: &mut Module,
track_idx: usize,
params: EuclideanParams,
) {
let original_instr_idx = module.tracks[track_idx].instrument;
let ekn = InstrEkn {
events: params.events,
steps: params.steps,
rotation: params.rotation,
instr: Some(original_instr_idx),
humanize_advance_max_ticks: 0,
humanize_probability: Q15::ZERO,
};
let mut name = String::new();
let _ = write!(
&mut name,
"Euclidean({},{}) of {}",
params.events, params.steps, module.instrument[original_instr_idx].name
);
let empty_slot = module
.instrument
.iter()
.position(|i| i.name.is_empty() && matches!(i.instr_type, InstrumentType::Empty));
let new_instr_idx = match empty_slot {
Some(idx) => {
module.instrument[idx] = Instrument {
name,
instr_type: InstrumentType::Euclidean(ekn),
muted: false,
};
idx
}
None => {
module.instrument.push(Instrument {
name,
instr_type: InstrumentType::Euclidean(ekn),
muted: false,
});
module.instrument.len() - 1
}
};
let mut prototype = params.prototype;
prototype.instrument = Some(new_instr_idx);
let track = &mut module.tracks[track_idx];
track.instrument = new_instr_idx;
track.rows = vec![prototype];
}
#[cfg(test)]
mod tests {
use super::*;
use crate::prelude::*;
use alloc::vec;
#[test]
fn bjorklund_3_8_is_canonical() {
let p = euclidean_pattern(3, 8);
assert_eq!(p, vec![true, false, false, true, false, false, true, false]);
}
#[test]
fn bjorklund_5_8() {
let p = euclidean_pattern(5, 8);
assert_eq!(p.iter().filter(|&&b| b).count(), 5);
assert!(p[0]);
}
fn play_cell(pitch: Pitch, instr: usize) -> TrackUnit {
TrackUnit {
note: CellNote::Play(pitch),
instrument: Some(instr),
..TrackUnit::default()
}
}
fn track_with_pulses(positions: &[usize], length: usize) -> Track {
let mut rows: Vec<TrackUnit> = (0..length).map(|_| TrackUnit::default()).collect();
for &p in positions {
rows[p] = play_cell(Pitch::C4, 0);
}
Track {
name: "t".into(),
instrument: 0,
rows,
muted: false,
}
}
#[test]
fn detect_canonical_3_8_rotation_zero() {
let track = track_with_pulses(&[0, 3, 6], 8);
let params = detect_strict_euclidean(&track).expect("should match");
assert_eq!(params.events, 3);
assert_eq!(params.steps, 8);
assert_eq!(params.rotation, 0);
}
#[test]
fn detect_canonical_3_8_rotation_one() {
let track = track_with_pulses(&[1, 4, 7], 8);
let params = detect_strict_euclidean(&track).expect("should match");
assert_eq!(params.events, 3);
assert_eq!(params.steps, 8);
assert_eq!(params.rotation, 1);
}
#[test]
fn reject_non_euclidean_pattern() {
let track = track_with_pulses(&[0, 1, 2], 8);
assert!(detect_strict_euclidean(&track).is_none());
}
#[test]
fn reject_short_track() {
let track = track_with_pulses(&[0, 2], 3);
assert!(detect_strict_euclidean(&track).is_none());
}
#[test]
fn reject_single_pulse() {
let track = track_with_pulses(&[0], 8);
assert!(detect_strict_euclidean(&track).is_none());
}
#[test]
fn reject_non_uniform_pitch() {
let mut track = track_with_pulses(&[0, 3, 6], 8);
track.rows[3].note = CellNote::Play(Pitch::D4); assert!(detect_strict_euclidean(&track).is_none());
}
#[test]
fn reject_per_cell_effect() {
let mut track = track_with_pulses(&[0, 3, 6], 8);
track.rows[0]
.effects
.push(TrackEffect::Arpeggio { half1: 4, half2: 7 });
assert!(detect_strict_euclidean(&track).is_none());
}
#[test]
fn reject_keyoff_cell() {
let mut track = track_with_pulses(&[0, 3, 6], 8);
track.rows[1].note = CellNote::KeyOff;
assert!(detect_strict_euclidean(&track).is_none());
}
#[test]
fn auto_convert_rewrites_track_and_cells() {
let mut module = Module::default();
module.instrument.push(Instrument {
name: "snare".into(),
instr_type: InstrumentType::Default(InstrDefault::default()),
muted: false,
});
let track = track_with_pulses(&[0, 3, 6], 8);
module.tracks.push(track);
auto_convert_strict_euclidean_tracks(&mut module);
assert_eq!(module.instrument.len(), 2);
assert!(matches!(
module.instrument[1].instr_type,
InstrumentType::Euclidean(_)
));
assert_eq!(module.tracks[0].instrument, 1);
for cell in &module.tracks[0].rows {
if matches!(cell.note, CellNote::Play(_)) {
assert_eq!(cell.instrument, Some(1));
}
}
if let InstrumentType::Euclidean(ekn) = &module.instrument[1].instr_type {
assert_eq!(ekn.events, 3);
assert_eq!(ekn.steps, 8);
assert_eq!(ekn.rotation, 0);
assert_eq!(ekn.instr, Some(0)); }
}
#[test]
fn auto_convert_is_idempotent() {
let mut module = Module::default();
module.instrument.push(Instrument {
name: "snare".into(),
instr_type: InstrumentType::Default(InstrDefault::default()),
muted: false,
});
module.tracks.push(track_with_pulses(&[0, 3, 6], 8));
auto_convert_strict_euclidean_tracks(&mut module);
let after_first = module.instrument.len();
let track_instr_first = module.tracks[0].instrument;
auto_convert_strict_euclidean_tracks(&mut module);
assert_eq!(module.instrument.len(), after_first, "no duplicate wrapper");
assert_eq!(
module.tracks[0].instrument, track_instr_first,
"track unchanged"
);
}
}