Skip to content

Fix some euclidean issues & Use euclidean in cycle #15

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 4 commits into from
May 22, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
131 changes: 92 additions & 39 deletions src/pattern/euclidean.rs
Original file line number Diff line number Diff line change
@@ -1,63 +1,116 @@
use crate::pattern::fixed::FixedPattern;

// -------------------------------------------------------------------------------------------------

/// Generates an euclidean rhythm pattern with the given pulse, step count and rotation offset.
pub fn euclidean(pulses: u32, steps: u32, offset: i32) -> Vec<bool> {
type Group = Vec<bool>;
type Groups = Vec<Group>;
/// Generates a Euclidean rhythm pattern with the given number of steps, pulses, and rotation offset.
pub fn euclidean(steps: u32, pulses: u32, offset: i32) -> Vec<bool> {
type Pattern = Vec<bool>;
type Patterns = Vec<Pattern>;

#[allow(clippy::similar_names)]
fn generate(mut fgs: Groups, mut lgs: Groups) -> Groups {
if lgs.len() < 2 {
fgs.append(&mut lgs.clone());
/// Recursively combines front and last groups to generate the Euclidean rhythm pattern.
fn combine_groups(mut front_groups: Patterns, mut last_groups: Patterns) -> Patterns {
if last_groups.len() < 2 {
front_groups.append(&mut last_groups.clone());
} else {
let mut nfgs: Groups = Vec::new();
while !fgs.is_empty() && !lgs.is_empty() {
let mut flg = fgs.last().unwrap().clone();
let mut llg = lgs.last().unwrap().clone();
flg.append(&mut llg);

nfgs.push(flg);
fgs.pop();
lgs.pop();
let mut new_front_groups: Patterns =
Vec::with_capacity(front_groups.len() + last_groups.len());
while !front_groups.is_empty() && !last_groups.is_empty() {
let mut front_last_group = front_groups.pop().unwrap();
let mut last_last_group = last_groups.pop().unwrap();
front_last_group.append(&mut last_last_group);

debug_assert!(new_front_groups.capacity() > new_front_groups.len());
new_front_groups.push(front_last_group);
}
fgs.append(&mut lgs);
return generate(nfgs, fgs);
front_groups.append(&mut last_groups);
return combine_groups(new_front_groups, front_groups);
}
fgs
front_groups
}

if pulses < steps {
let mut rhythm: Group = Vec::with_capacity(steps as usize);
if steps == 0 {
vec![false; pulses as usize]
} else if steps >= pulses {
vec![true; pulses as usize]
} else {
let mut rhythm: Pattern = Vec::with_capacity(pulses as usize);

let front = vec![vec![true]; pulses as usize];
let last = vec![vec![false]; (steps - pulses) as usize];
let front_groups = vec![vec![true]; steps as usize];
let last_groups = vec![vec![false]; (pulses - steps) as usize];

let rhythms = generate(front, last);
for g in rhythms {
for i in g {
rhythm.push(i);
let combined_patterns = combine_groups(front_groups, last_groups);
for group in combined_patterns {
for value in group {
rhythm.push(value);
}
}

match offset {
n if n > 0 => rhythm.rotate_right((n as usize) % (steps as usize)),
n if n < 0 => rhythm.rotate_left((-n as usize) % (steps as usize)),
n if n > 0 => rhythm.rotate_left((n as usize) % (pulses as usize)),
n if n < 0 => rhythm.rotate_right((-n as usize) % (pulses as usize)),
_ => (),
}

rhythm
} else {
vec![true; steps as usize]
}
}

// -------------------------------------------------------------------------------------------------
// --------------------------------------------------------------------------------------------------

#[cfg(test)]
mod test {
use super::euclidean;

impl FixedPattern {
/// Create a pattern from an euclidan rhythm.
pub fn from_euclidean(pulses: u32, steps: u32, offset: i32) -> Self {
Self::from_pulses(euclidean(pulses, steps, offset))
#[test]
fn patterns() {
// patterns from Toussaint
let check_pattern = |(steps, pulses), result: &str| {
let result = result.split(' ').map(|c| c == "x").collect::<Vec<bool>>();
assert_eq!(euclidean(steps, pulses, 0), result);
};
check_pattern((1, 2), "x ~");
check_pattern((1, 3), "x ~ ~");
check_pattern((1, 4), "x ~ ~ ~");
check_pattern((4, 12), "x ~ ~ x ~ ~ x ~ ~ x ~ ~");
check_pattern((2, 5), "x ~ x ~ ~");
check_pattern((3, 4), "x x x ~");
check_pattern((3, 5), "x ~ x ~ x");
check_pattern((3, 7), "x ~ x ~ x ~ ~");
check_pattern((3, 8), "x ~ ~ x ~ ~ x ~");
check_pattern((4, 7), "x ~ x ~ x ~ x");
check_pattern((4, 9), "x ~ x ~ x ~ x ~ ~");
check_pattern((4, 11), "x ~ ~ x ~ ~ x ~ ~ x ~");
check_pattern((5, 6), "x x x x x ~");
check_pattern((5, 7), "x ~ x x ~ x x");
check_pattern((5, 8), "x ~ x x ~ x x ~");
check_pattern((5, 9), "x ~ x ~ x ~ x ~ x");
check_pattern((5, 11), "x ~ x ~ x ~ x ~ x ~ ~");
check_pattern((5, 12), "x ~ ~ x ~ x ~ ~ x ~ x ~");
check_pattern((5, 16), "x ~ ~ x ~ ~ x ~ ~ x ~ ~ x ~ ~ ~");
check_pattern((7, 8), "x x x x x x x ~");
check_pattern((7, 12), "x ~ x x ~ x ~ x x ~ x ~");
check_pattern((7, 16), "x ~ ~ x ~ x ~ x ~ ~ x ~ x ~ x ~");
check_pattern((9, 16), "x ~ x x ~ x ~ x ~ x x ~ x ~ x ~");
check_pattern((11, 24), "x ~ ~ x ~ x ~ x ~ x ~ x ~ ~ x ~ x ~ x ~ x ~ x ~");
check_pattern((13, 24), "x ~ x x ~ x ~ x ~ x ~ x ~ x x ~ x ~ x ~ x ~ x ~");
// steps > pulses
assert_eq!(
euclidean(9, 8, 0),
[true, true, true, true, true, true, true, true]
);
// empty steps
assert_eq!(euclidean(0, 8, 0), vec![false; 8]);
// empty pulses
assert_eq!(euclidean(8, 0, 0), Vec::<bool>::new());
// rotate
assert_eq!(
euclidean(3, 8, 3),
[true, false, false, true, false, true, false, false]
);
assert_eq!(
euclidean(3, 8, -3),
[false, true, false, true, false, false, true, false]
);
// rotate and wrap
assert_eq!(euclidean(3, 8, 5), euclidean(3, 8, 5 + 8));
assert_eq!(euclidean(3, 8, -3), euclidean(3, 8, -3 - 8));
}
}
6 changes: 6 additions & 0 deletions src/pattern/fixed.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
use std::borrow::Cow;

use super::euclidean::euclidean;
use crate::{BeatTimeBase, Pattern, Pulse, PulseIter, PulseIterItem};

// -------------------------------------------------------------------------------------------------
Expand Down Expand Up @@ -43,6 +44,11 @@ impl FixedPattern {
repeat_count,
}
}

/// Create a pattern from an euclidan rhythm.
pub fn from_euclidean(steps: u32, pulses: u32, offset: i32) -> Self {
Self::from_pulses(euclidean(steps, pulses, offset))
}
}

impl Pattern for FixedPattern {
Expand Down
77 changes: 28 additions & 49 deletions src/tidal/cycle.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ use rand_xoshiro::Xoshiro256PlusPlus;

use fraction::{Fraction, One, Zero};

use crate::pattern::euclidean::euclidean;

// -------------------------------------------------------------------------------------------------

#[derive(Clone, Debug)]
Expand Down Expand Up @@ -553,7 +555,7 @@ impl Events {
}
}
}

fn flatten(&self, channels: &mut Vec<Vec<Event>>, channel: usize) {
if channels.len() <= channel {
channels.push(vec![])
Expand Down Expand Up @@ -711,31 +713,6 @@ impl Cycle {
Err(format!("invalid polymeter\n{:?}", pair))
}

fn rotate(pattern: &mut [bool], r: i32) {
let steps = pattern.len();
if steps == 0 { return }
match r {
r if r > 0 => pattern.rotate_left((r as usize) % steps),
r if r < 0 => pattern.rotate_right((r.unsigned_abs() as usize) % steps),
_ => (),
}
}

fn bjorklund_pattern(pulses: i32, steps: i32, rotation: Option<i32>) -> Vec<bool> {
let slope = (pulses as f64) / (steps as f64);
let mut pattern = vec![];
let mut prev = -1.0;
for i in 0..steps {
let curr = ((i as f64) * slope).floor();
pattern.push(curr != prev);
prev = curr;
}
if let Some(r) = rotation {
Cycle::rotate(&mut pattern, r);
}
pattern
}

// helper to convert a section rule to a vector of Steps
fn parse_section(pair: Pair<Rule>) -> Result<Vec<Step>, String> {
let mut steps = vec![];
Expand Down Expand Up @@ -1052,9 +1029,9 @@ impl Cycle {
#[allow(clippy::single_match)]
// TODO support something other than Step::Single as the right hand side
match b.pulses.as_ref() {
Step::Single(pulses_single) => {
Step::Single(steps_single) => {
match b.steps.as_ref() {
Step::Single(steps_single) => {
Step::Single(pulses_single) => {
let rotation = match &b.rotation {
Some(r) => match r.as_ref() {
Step::Single(rotation_single) => {
Expand All @@ -1064,12 +1041,14 @@ impl Cycle {
},
None => None,
};
if let Some(pulses) = pulses_single.to_integer() {
if let Some(steps) = steps_single.to_integer() {
if let Some(steps) = steps_single.to_integer() {
if let Some(pulses) = pulses_single.to_integer() {
let out = Cycle::output(&mut b.left, rng);
for pulse in
Cycle::bjorklund_pattern(pulses, steps, rotation)
{
for pulse in euclidean(
steps.max(0) as u32,
pulses.max(0) as u32,
rotation.unwrap_or(0),
) {
if pulse {
events.push(out.clone())
} else {
Expand Down Expand Up @@ -1524,11 +1503,11 @@ mod test {
.with_int(2)
.with_target(Target::Name("a".to_string())),
Event::at(F::new(1u8, 5u8), F::new(1u8, 5u8)),
Event::at(F::new(2u8, 5u8), F::new(1u8, 5u8)),
Event::at(F::new(3u8, 5u8), F::new(1u8, 10u8)).with_int(1),
Event::at(F::new(7u8, 10u8), F::new(1u8, 10u8))
Event::at(F::new(2u8, 5u8), F::new(1u8, 10u8)).with_int(1),
Event::at(F::new(5u8, 10u8), F::new(1u8, 10u8))
.with_int(2)
.with_target(Target::Name("a".to_string())),
Event::at(F::new(3u8, 5u8), F::new(1u8, 5u8)),
Event::at(F::new(4u8, 5u8), F::new(1u8, 5u8)),
]],
vec![vec![
Expand All @@ -1537,11 +1516,11 @@ mod test {
.with_int(20)
.with_target(Target::Name("a".to_string())),
Event::at(F::new(1u8, 5u8), F::new(1u8, 5u8)),
Event::at(F::new(2u8, 5u8), F::new(1u8, 5u8)),
Event::at(F::new(3u8, 5u8), F::new(1u8, 10u8)).with_int(10),
Event::at(F::new(7u8, 10u8), F::new(1u8, 10u8))
Event::at(F::new(2u8, 5u8), F::new(1u8, 10u8)).with_int(10),
Event::at(F::new(5u8, 10u8), F::new(1u8, 10u8))
.with_int(20)
.with_target(Target::Name("a".to_string())),
Event::at(F::new(3u8, 5u8), F::new(1u8, 5u8)),
Event::at(F::new(4u8, 5u8), F::new(1u8, 5u8)),
]],
],
Expand Down Expand Up @@ -1582,16 +1561,17 @@ mod test {
],
)?;

assert_eq!(Cycle::from("c(3,8,9)", None)?.generate(),
assert_eq!(
Cycle::from("c(3,8,9)", None)?.generate(),
[[
Event::at(F::from(0), F::new(1u8,8u8)),
Event::at(F::new(1u8,8u8), F::new(1u8,8u8)),
Event::at(F::new(2u8,8u8), F::new(1u8,8u8)).with_note(0, 4),
Event::at(F::new(3u8,8u8), F::new(1u8,8u8)),
Event::at(F::new(4u8,8u8), F::new(1u8,8u8)),
Event::at(F::new(5u8,8u8), F::new(1u8,8u8)).with_note(0, 4),
Event::at(F::new(6u8,8u8), F::new(1u8,8u8)),
Event::at(F::new(7u8,8u8), F::new(1u8,8u8)).with_note(0, 4),
Event::at(F::from(0), F::new(1u8, 8u8)),
Event::at(F::new(1u8, 8u8), F::new(1u8, 8u8)),
Event::at(F::new(2u8, 8u8), F::new(1u8, 8u8)).with_note(0, 4),
Event::at(F::new(3u8, 8u8), F::new(1u8, 8u8)),
Event::at(F::new(4u8, 8u8), F::new(1u8, 8u8)),
Event::at(F::new(5u8, 8u8), F::new(1u8, 8u8)).with_note(0, 4),
Event::at(F::new(6u8, 8u8), F::new(1u8, 8u8)),
Event::at(F::new(7u8, 8u8), F::new(1u8, 8u8)).with_note(0, 4),
]]
);

Expand All @@ -1605,7 +1585,6 @@ mod test {
Cycle::from("[a b c](3,8,-1)", None)?.generate()
);


// TODO test random outputs // parse_with_debug("[a b c d]?0.5");

assert!(Cycle::from("a b c [d", None).is_err());
Expand Down
52 changes: 32 additions & 20 deletions types/nerdo/library/extras/pattern.lua
Original file line number Diff line number Diff line change
Expand Up @@ -144,35 +144,47 @@ function pattern.euclidean(steps, length, offset, empty_value)
table.insert(front, { v })
end
else
assert(type(steps) == "number" and steps > 0,
assert(type(steps) == "number" and steps >= 0,
"invalid steps argument (must be a table or an integer > 0)")
for _ = 1, steps do
table.insert(front, { 1 })
end
end
assert(type(length) == "number" and length > 0,
assert(type(length) == "number" and length > 0,
"invalid length argument (expecting an integer > 0)")
assert(length >= #front,
"invalid length or step (length must be >= #pulses)")
assert(type(offset) == "number" or offset == nil,
assert(type(offset) == "number" or offset == nil,
"invalid offset argument (must be an integer or nil)")
empty_value = empty_value == nil and empty_pulse_value(steps) or 0
local back = {}
for _ = 1, length - #front do
table.insert(back, { empty_value })
end
-- spread
local rhythms = euclidean_impl(front, back);
-- convert to pattern and flatten
local result = pattern.new();
for _, g in ipairs(rhythms) do
result:push_back(g);
end
-- rotate
if offset then
result:rotate(offset)
if #front == 0 then
local result = pattern.new();
for _ = 1, length do
result:push_back(empty_value)
end
return result
elseif #front >= length then
local result = pattern.new();
for _ = 1, length do
result:push_back(1)
end
return result
else
local back = {}
for _ = 1, length - #front do
table.insert(back, { empty_value })
end
-- spread
local rhythms = euclidean_impl(front, back);
-- convert to pattern and flatten
local result = pattern.new();
for _, g in ipairs(rhythms) do
result:push_back(g);
end
-- rotate
if offset then
result:rotate(-offset)
end
return result
end
return result
end

----------------------------------------------------------------------------------------------------
Expand Down