From e9c0731d7e3230d638563896d3f9b9e223422a90 Mon Sep 17 00:00:00 2001 From: Miika Alonen Date: Tue, 21 Feb 2023 03:16:00 +0200 Subject: [PATCH] Refactored evaluation for subdivisions --- tests/test_parser.py | 41 +++++++++++++++++++++++++-- ziffers/classes.py | 66 ++++++++++++++++++++------------------------ ziffers/mapper.py | 3 +- 3 files changed, 70 insertions(+), 40 deletions(-) diff --git a/tests/test_parser.py b/tests/test_parser.py index 66b809a..86271e1 100644 --- a/tests/test_parser.py +++ b/tests/test_parser.py @@ -2,9 +2,12 @@ import pytest from ziffers import zparse +# pylint: disable=missing-function-docstring, line-too-long, invalid-name def test_can_parse(): expressions = [ + "0 1 2 3 4 5 6 7 8 9 T E", + "023 i iv iv^min", "[1 [2 3]]", "(1 (1,3) 1..3)", "_^ q _qe^3 qww_4 _123 <1 2>", @@ -12,6 +15,8 @@ def test_can_parse(): "2 qe2 e4", "q 2 <3 343>", "q (2 <3 343 (3 4)>)", + "? 1 2", + "(? 2 ? 4)+(1,4)" ] results = [] for expression in expressions: @@ -49,6 +54,36 @@ def test_parsing_text(pattern: str): def test_pitch_classes(pattern: str, expected: list): assert zparse(pattern).pitch_classes() == expected -# TODO: Add tests for octaves -# ("__6 _0 _1 _2 _3 _4 _5 _6 0 1 2 3 4 5 6 ^0 ^1 ^2 ^3 ^4 ^5 ^6 ^^0", [-2, -1, -1, -1, -1, -1, -1, -1, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 2]), -# ("_ 1 _2 <3>3 ^^4", [-1,-2,3,-1]), + +@pytest.mark.parametrize( + "pattern,expected", + [ + ("__6 _0 _1 _2 _3 _4 _5 _6 0 1 2 3 4 5 6 ^0 ^1 ^2 ^3 ^4 ^5 ^6 ^^0", [-2, -1, -1, -1, -1, -1, -1, -1, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 2]), + ("_ 1 _2 <3>3 ^^4", [-1, -1, 3, 2]), + ] +) +def test_pitch_octaves(pattern: str, expected: list): + assert zparse(pattern).octaves() == expected + + +@pytest.mark.parametrize( + "pattern,expected", + [ + ("w [1 [2 3]]", [0.5, 0.25, 0.25]), + ("1.0 [1 [2 3]] 4 [3 [4 5]]", [0.5, 0.25, 0.25, 1.0, 0.5, 0.25, 0.25]), + ("0.5 (0 0.25 3)+1", [0.5, 0.25]) + ] +) +def test_subdivisions(pattern: str, expected: list): + assert zparse(pattern).durations() == expected + +@pytest.mark.parametrize( + "pattern,expected", + [ + ("[: 1 [: 2 :] 3 :]", [62, 64, 64, 65, 62, 64, 64, 65]), + ("(: 1 (: 2 :) 3 :)", [62, 64, 64, 65, 62, 64, 64, 65]) + ] +) +def test_repeats(pattern: str, expected: list): + assert zparse(pattern).notes() == expected + diff --git a/ziffers/classes.py b/ziffers/classes.py index 74f43ba..ed06d3d 100644 --- a/ziffers/classes.py +++ b/ziffers/classes.py @@ -174,7 +174,7 @@ class Pitch(Event): self.freq = freq # pylint: disable=locally-disabled, unused-argument - def get_value(self) -> int: + def get_value(self, options) -> int: """Returns the pitch class Returns: @@ -318,17 +318,14 @@ class Sequence(Meta): if isinstance(item, ListOperation): yield from item.evaluate(options) elif isinstance(item, RepeatedSequence): - repeats = item.repeats.get_value() + repeats = item.repeats.get_value(options) yield from _normal_repeat(item.evaluated_values, repeats, options) elif isinstance(item, RepeatedListSequence): - repeats = item.repeats.get_value() + repeats = item.repeats.get_value(options) yield from _generative_repeat(item, repeats, options) elif isinstance(item, Subdivision): - items = item.evaluate(options) - if item.has_children: - yield from items.evaluated_values - else: - yield from _loop_items(item, options) + item.evaluated_values = list(item.evaluate_tree(options)) + yield item else: yield from item.evaluate_tree(options) elif isinstance(item, Cyclic): @@ -513,17 +510,27 @@ class Ziffers(Sequence): self.options = DEFAULT_OPTIONS self.start_options = self.options.copy() - self.evaluated_values = list(self.evaluate_tree(self.options)) - self.iterator = iter(self.evaluated_values) + self.init_tree(self.options) def re_eval(self, options=None): """Re-evaluate the iterator""" self.options = self.start_options.copy() if options: self.options.update(options) - self.evaluated_values = list(self.evaluate_tree(self.options)) + self.init_tree(self.options) + + def init_tree(self, options): + self.evaluated_values = list(self.evaluate_tree(options)) + self.evaluated_values = list(self.post_check()) self.iterator = iter(self.evaluated_values) + def post_check(self): + for item in self.evaluated_values: + if isinstance(item, Subdivision): + yield from item.evaluate_durations() + else: + yield item + def get_list(self): """Return list""" return list(self) @@ -593,7 +600,7 @@ class Integer(Item): value: int # pylint: disable=locally-disabled, unused-argument - def get_value(self): + def get_value(self, options): """Return value of the integer""" return self.value @@ -631,31 +638,17 @@ class RepeatedListSequence(Sequence): class Subdivision(Sequence): """Class for subdivisions""" - subdiv_length: float = field(default=None, init=False) - local_options: dict = None - has_children: bool = field(default=False, init=False) - - def evaluate(self, options): - """Evaluate tree and then calculate lengths using subdivision""" - self.evaluated_values = list(self.evaluate_tree(options.copy())) - self.evaluated_values = list(self.evaluate_subdivisions(options)) - return self - - def evaluate_subdivisions(self, options): + def evaluate_durations(self, duration=None): """Calculate new durations by dividing with the number of items in the sequence""" - self.subdiv_length = len(self.evaluated_values) - self.local_options = options.copy() - self.local_options["duration"] = options["duration"] / self.subdiv_length + if duration is None: + duration = self.evaluated_values[0].duration + new_d = duration / len(self.evaluated_values) for item in self.evaluated_values: if isinstance(item, Subdivision): - self.has_children = True - yield from item.evaluate_subdivisions(self.local_options) - elif isinstance(item, Cyclic): - yield item # Return the cycle - elif isinstance(item, Rest): - yield item.get_updated_item(self.local_options) - elif isinstance(item, Pitch): - item.duration = self.local_options["duration"] + yield from item.evaluate_durations(new_d) + if isinstance(item, Event): + if duration is not None: + item.duration = new_d yield item @@ -748,7 +741,8 @@ class ListOperation(Sequence): ) left = [ Pitch( - pitch_class=operation(x.get_value(), y.get_value()), kwargs=options + pitch_class=operation(x.get_value(options), y.get_value(options)), + kwargs=options, ) for (x, y) in pairs ] @@ -856,4 +850,4 @@ class RepeatedSequence(Sequence): elif isinstance(item, Rest): yield item.get_updated_item(self.local_options) elif isinstance(item, (Event, RandomInteger)): - yield Pitch(pitch_class=item.get_value(), kwargs=self.local_options) + yield Pitch(pitch_class=item.get_value(self.local_options), kwargs=self.local_options) diff --git a/ziffers/mapper.py b/ziffers/mapper.py index 931842d..a309c39 100644 --- a/ziffers/mapper.py +++ b/ziffers/mapper.py @@ -92,7 +92,8 @@ class ZiffersTransformer(Transformer): def pitch(self, items): """Return pitch class info""" - return {"pitch_class": int(items[0].value), "text": items[0].value} + text_value = items[0].value.replace("T","10").replace("E","11") + return {"pitch_class": int(text_value), "text": items[0].value} def prefix(self, items): """Return prefix"""