From 9a4c970b9558d11bb47c131fe31d11466bbab1b8 Mon Sep 17 00:00:00 2001 From: Miika Alonen Date: Sun, 19 Feb 2023 22:56:11 +0200 Subject: [PATCH] Added euclidean evaluation --- ziffers/classes.py | 51 ++++++++++++++++++++++++++++++++++++++++++---- ziffers/common.py | 20 ++++++++++++++++++ ziffers/mapper.py | 4 ++-- 3 files changed, 69 insertions(+), 6 deletions(-) diff --git a/ziffers/classes.py b/ziffers/classes.py index b9a2845..fefc8e7 100644 --- a/ziffers/classes.py +++ b/ziffers/classes.py @@ -5,6 +5,7 @@ import operator import random from .defaults import DEFAULT_OPTIONS from .scale import note_from_pc, midi_to_pitch_class, midi_to_freq, get_scale_length +from .common import euclidian_rhythm @dataclass(kw_only=True) @@ -326,6 +327,8 @@ class Sequence(Meta): yield from item.evaluate_tree(options) elif isinstance(item, Cyclic): yield from _resolve_item(item.get_value(), options) + elif isinstance(item, Euclid): + yield from _euclidean_items(item, options) elif isinstance(item, Modification): options = _update_options(item, options) elif isinstance(item, Meta): # Filters whitespace @@ -362,6 +365,13 @@ class Sequence(Meta): for item in tree: yield from _resolve_item(item, options) + def _euclidean_items(euclid: Item, options: dict): + """Repeats items with the same random values""" + euclid.evaluate(options) + for item in euclid.evaluated_values: + yield from _resolve_item(item, options) + + def _update_options(current: Item, options: dict) -> dict: """Update options based on current item""" if isinstance(current, (OctaveChange, DurationChange)): @@ -727,7 +737,9 @@ class ListOperation(Sequence): (right.values if isinstance(right, Sequence) else [right]), left ) left = [ - Pitch(pitch_class=operation(x.get_value(), y.get_value()), kwargs=options) + Pitch( + pitch_class=operation(x.get_value(), y.get_value()), kwargs=options + ) for (x, y) in pairs ] return left @@ -767,9 +779,40 @@ class Euclid(Item): pulses: int length: int - onset: list - offset: list = field(default=None) - rotate: int = field(default=None) + onset: ListSequence + offset: ListSequence = field(default=None) + rotate: int = field(default=0) + evaluated_values: list = field(default=None) + + def evaluate(self, options): + onset_values = [ + val for val in self.onset.values if not isinstance(val, Whitespace) + ] + onset_length = len(onset_values) + booleans = euclidian_rhythm(self.pulses, self.length, self.rotate) + self.evaluated_values = [] + + if self.offset is not None: + offset_values = [ + val for val in self.offset.values if not isinstance(val, Whitespace) + ] + offset_length = len(offset_values) + + on_i = 0 + off_i = 0 + + for i in range(self.length): + if booleans[i]: + value = onset_values[on_i % onset_length] + on_i+=1 + else: + if self.offset is None: + value = Rest(duration=options["duration"]) + else: + value = offset_values[off_i % offset_length] + off_i+=1 + + self.evaluated_values.append(value) @dataclass(kw_only=True) diff --git a/ziffers/common.py b/ziffers/common.py index 93bc955..d74529a 100644 --- a/ziffers/common.py +++ b/ziffers/common.py @@ -53,3 +53,23 @@ def string_rewrite(axiom: str, rules: dict): pattern = re.compile("|".join(rules.keys())) return pattern.sub(lambda m: next(_apply_rules(m)), axiom) + + +def euclidian_rhythm(pulses: int, length: int, rotate: int = 0): + """Calculate Euclidean rhythms. Original algorithm by Thomas Morrill.""" + + def _starts_descent(list, index): + length = len(list) + next_index = (index + 1) % length + return list[index] > list[next_index] + + def rotation(l, n): + return l[-n:] + l[:-n] + + if pulses >= length: + return [True] + + res_list = [pulses * t % length for t in range(-1, length - 1)] + bool_list = [_starts_descent(res_list, index) for index in range(length)] + + return rotation(bool_list, rotate) diff --git a/ziffers/mapper.py b/ziffers/mapper.py index eb7b875..931842d 100644 --- a/ziffers/mapper.py +++ b/ziffers/mapper.py @@ -340,10 +340,10 @@ class ZiffersTransformer(Transformer): def euclid(self, items): """Parse euclid notation""" params = items[1][1:-1].split(",") - init = {"onset": items[0], "pulses": params[0], "length": params[1]} + init = {"onset": items[0], "pulses": int(params[0]), "length": int(params[1])} text = items[0].text + items[1] if len(params) > 2: - init["rotate"] = params[2] + init["rotate"] = int(params[2]) if len(items) > 2: init["offset"] = items[2] text = text + items[2].text