diff --git a/tests/test_parser.py b/tests/test_parser.py index 19d716b..0c7538a 100644 --- a/tests/test_parser.py +++ b/tests/test_parser.py @@ -72,7 +72,8 @@ def test_pitch_octaves(pattern: str, expected: list): [ ("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]) + ("0.5 (0 0.25 3)+1", [0.5, 0.25]), + ("[0 2 <2 8>:2 4] 0", [0.05, 0.05, 0.05, 0.05, 0.05, 0.25]) ] ) def test_subdivisions(pattern: str, expected: list): @@ -82,13 +83,14 @@ def test_subdivisions(pattern: str, expected: list): "pattern,expected", [ ("[: 1 [: 2 :] 3 :]", [62, 64, 64, 65, 62, 64, 64, 65]), - ("(: 1 (: 2 :) 3 :)", [62, 64, 64, 65, 62, 64, 64, 65]) + ("(: 1 (: 2 :) 3 :)", [62, 64, 64, 65, 62, 64, 64, 65]), + ("(1 2:2 3):2", [62, 64, 64, 65, 62, 64, 64, 65]), + ("1:4",[62,62,62,62]) ] ) def test_repeats(pattern: str, expected: list): assert collect(zparse(pattern),len(expected)*2,"note") == expected*2 - @pytest.mark.parametrize( "pattern,expected", [ @@ -106,3 +108,20 @@ def test_looping_durations(pattern: str, expected: list): durations.append(parsed[i].duration) assert durations == expected +@pytest.mark.parametrize( + "pattern,expected", + [ + ("e 1 | 3 | h 3 | e3 | 4", [0.125,0.25,0.5,0.125,0.25]) + ] +) +def test_measure_durations(pattern: str, expected: list): + assert collect(zparse(pattern),len(expected)*2,"duration") == expected*2 + +@pytest.mark.parametrize( + "pattern,expected", + [ + ("^ 1 | _ 3 | ^3 | 3 | _4", [1,-1,1,0,-1]) + ] +) +def test_measure_octaves(pattern: str, expected: list): + assert collect(zparse(pattern),len(expected)*2,"octave") == expected*2 diff --git a/ziffers/classes.py b/ziffers/classes.py index 595e5ba..8e13216 100644 --- a/ziffers/classes.py +++ b/ziffers/classes.py @@ -58,6 +58,7 @@ class Item(Meta): """Class for all Ziffers text based items""" text: str = field(default=None) + measure: int = field(default=0, init=False) def get_updated_item(self, options: dict): """Get updated item with replaced options @@ -135,6 +136,20 @@ class Event(Item): class Rest(Event): """Class for rests""" +@dataclass +class Measure(Item): + """ Class for measures/bars. Used to reset default options. """ + text: str = field(default="|", init=False) + + def reset_options(self, options: dict): + """Reset options when measure changes""" + next_measure = options.get("measure", 0)+1 + start_options = options["start_options"].copy() + options.clear() + options.update(start_options) + options["measure"] = next_measure + options["start_options"] = start_options.copy() + self.measure = next_measure @dataclass(kw_only=True) class Pitch(Event): @@ -211,25 +226,6 @@ class Pitch(Event): """ return self.pitch_class - def get_notes(self) -> int: - """Return notes""" - return self.note - - def get_octaves(self) -> int: - """Return octave""" - return self.octave - - def get_beats(self) -> float: - """Return beats""" - return self.beat - - def get_durations(self) -> float: - """Return duration""" - return self.duration - - def get_freqs(self) -> float: - """Return frequencies""" - return self.freq @dataclass(kw_only=True) @@ -261,13 +257,13 @@ class Chord(Event): """Class for chords""" pitch_classes: list[Pitch] = field(default=None) - notes: list[int] = field(default=None) + note: list[int] = field(default=None) inversions: int = field(default=None) - pitches: list[int] = field(default=None, init=False) - freqs: list[float] = field(default=None, init=False) - octaves: list[int] = field(default=None, init=False) - durations: list[float] = field(default=None, init=False) - beats: list[float] = field(default=None, init=False) + pitch_class: list[int] = field(default=None, init=False) + freq: list[float] = field(default=None, init=False) + octave: list[int] = field(default=None, init=False) + duration: list[float] = field(default=None, init=False) + beat: list[float] = field(default=None, init=False) def __post_init__(self): if self.inversions is not None: @@ -275,7 +271,7 @@ class Chord(Event): def set_notes(self, notes: list[int]): """Set notes to the class""" - self.notes = notes + self.note = notes def invert(self, value: int): """Chord inversion""" @@ -311,36 +307,12 @@ class Chord(Event): durations.append(pitch.duration) beats.append(pitch.beat) - self.pitches = pitches - self.notes = notes - self.freqs = freqs - self.octaves = octaves + self.pitch = pitches + self.note = notes + self.freq = freqs + self.octave = octaves self.duration = durations - self.beats = beats - - def get_pitches(self) -> list: - """Return pitch classes""" - return self.pitches - - def get_notes(self) -> list: - """Return notes""" - return self.notes - - def get_octaves(self) -> list: - """Return octave""" - return self.octaves - - def get_beats(self) -> float: - """Return beats""" - return self.beats - - def get_durations(self) -> float: - """Return duration""" - return self.durations - - def get_freqs(self) -> float: - """Return frequencies""" - return self.freqs + self.beat = beats @dataclass(kw_only=True) @@ -452,11 +424,13 @@ class Sequence(Meta): elif isinstance(item, RepeatedSequence): item.evaluate_values(options) repeats = item.repeats.get_value(options) - repeats = _resolve_repeat_value(repeats) + if not isinstance(repeats, int): + repeats = _resolve_repeat_value(repeats) yield from _normal_repeat(item.evaluated_values, repeats, options) elif isinstance(item, RepeatedListSequence): repeats = item.repeats.get_value(options) - repeats = _resolve_repeat_value(repeats) + if not isinstance(repeats, int): + repeats = _resolve_repeat_value(repeats) yield from _generative_repeat(item, repeats, options) elif isinstance(item, Subdivision): item.evaluate_values(options) @@ -484,6 +458,8 @@ class Sequence(Meta): yield from _euclidean_items(item, options) elif isinstance(item, Modification): options = _parse_options(item, options) + elif isinstance(item, Measure): + item.reset_options(options) elif isinstance(item, Meta): # Filters whitespace yield _update_item(item, options) @@ -492,8 +468,8 @@ class Sequence(Meta): item = item.get_value(options) if isinstance(item, Pitch): return item.get_value(options) - if not isinstance(item, Integer): - return 2 + if isinstance(item, Integer): + return item.get_value(options) return item def _update_item(item, options): @@ -548,9 +524,6 @@ class Sequence(Meta): options[current.key] = current.value return options - def _create_pitch_without_note(current: Item, options: dict) -> Pitch: - return Pitch(pitch_class=current.get_value(options)) - def _create_pitch(current: Item, options: dict) -> Pitch: """Create pitch based on values and options""" @@ -623,7 +596,7 @@ class Sequence(Meta): chord = Chord( text=pitch_text, pitch_classes=pitch_classes, - notes=chord_notes, + note=chord_notes, kwargs=options, inversions=current.inversions, ) @@ -691,11 +664,13 @@ class Ziffers(Sequence): self.options = DEFAULT_OPTIONS.copy() self.start_options = self.options.copy() + self.options["start_options"] = self.start_options self.init_tree(self.options) def re_eval(self): """Re-evaluate the iterator""" self.options = self.start_options.copy() + self.options["start_options"] = self.start_options self.init_tree(self.options) def init_tree(self, options): @@ -743,7 +718,7 @@ class Ziffers(Sequence): def pitch_classes(self) -> list[int]: """Return list of pitch classes as ints""" return [ - val.get_pitches() + val.pitch_class for val in self.evaluated_values if isinstance(val, (Pitch, Chord)) ] @@ -751,7 +726,7 @@ class Ziffers(Sequence): def notes(self) -> list[int]: """Return list of midi notes""" return [ - val.get_notes() + val.note for val in self.evaluated_values if isinstance(val, (Pitch, Chord)) ] @@ -759,7 +734,7 @@ class Ziffers(Sequence): def durations(self) -> list[float]: """Return list of pitch durations as floats""" return [ - val.get_durations() + val.duration for val in self.evaluated_values if isinstance(val, Event) ] @@ -767,7 +742,7 @@ class Ziffers(Sequence): def beats(self) -> list[float]: """Return list of pitch durations as floats""" return [ - val.get_beats() for val in self.evaluated_values if isinstance(val, Event) + val.beat for val in self.evaluated_values if isinstance(val, Event) ] def pairs(self) -> list[tuple]: @@ -781,7 +756,7 @@ class Ziffers(Sequence): def octaves(self) -> list[int]: """Return list of octaves""" return [ - val.get_octaves() + val.octave for val in self.evaluated_values if isinstance(val, (Pitch, Chord)) ] @@ -789,7 +764,7 @@ class Ziffers(Sequence): def freqs(self) -> list[int]: """Return list of octaves""" return [ - val.get_freqs() + val.freq for val in self.evaluated_values if isinstance(val, (Pitch, Chord)) ] diff --git a/ziffers/converters.py b/ziffers/converters.py index e9902c1..708ce05 100644 --- a/ziffers/converters.py +++ b/ziffers/converters.py @@ -65,7 +65,7 @@ class ZiffersMusic21(converter.subConverters.SubConverter): elif isinstance(item, Rest): m_item = note.Rest(item.duration * 4) elif isinstance(item, Chord): - m_item = chord.Chord(item.notes) + m_item = chord.Chord(item.note) m_item.duration.quarterLength = item.duration * 4 note_stream.append(m_item) self.stream = note_stream.makeMeasures() diff --git a/ziffers/defaults.py b/ziffers/defaults.py index e9ca580..6aae6e5 100644 --- a/ziffers/defaults.py +++ b/ziffers/defaults.py @@ -46,7 +46,8 @@ DEFAULT_OPTIONS = MappingProxyType({ "octave": 0, "duration": 0.25, "key": "C4", - "scale": "IONIAN" + "scale": "IONIAN", + "measure": 0 }) OPERATORS = MappingProxyType({ diff --git a/ziffers/mapper.py b/ziffers/mapper.py index b05242a..a554068 100644 --- a/ziffers/mapper.py +++ b/ziffers/mapper.py @@ -29,6 +29,7 @@ from .classes import ( RepeatedSequence, VariableAssignment, Variable, + Measure ) from .common import flatten, sum_dict from .defaults import DEFAULT_DURS, OPERATORS @@ -56,6 +57,10 @@ class ZiffersTransformer(Transformer): return Rest(text=text_prefix + "r", local_options=prefixes) return Rest(text="r") + def measure(self, items): + """Return new measure""" + return Measure() + def random_integer(self, items) -> RandomInteger: """Parses random integer syntax""" if len(items) > 1: diff --git a/ziffers/spec/ziffers.lark b/ziffers/spec/ziffers.lark index 8715bfe..acf2f30 100644 --- a/ziffers/spec/ziffers.lark +++ b/ziffers/spec/ziffers.lark @@ -1,6 +1,6 @@ // Root for the rules ?root: sequence -> start - sequence: (pitch_class | repeat_item | assignment | variable | rest | dur_change | oct_mod | oct_change | WS | chord | named_roman | cycle | random_integer | random_pitch | random_percent | range | list | repeated_list | lisp_operation | list_op | subdivision | eval | euclid | repeat)* + sequence: (pitch_class | repeat_item | assignment | variable | rest | dur_change | oct_mod | oct_change | WS | measure | chord | named_roman | cycle | random_integer | random_pitch | random_percent | range | list | repeated_list | lisp_operation | list_op | subdivision | eval | euclid | repeat)* // Pitch classes pitch_class: prefix* pitch @@ -11,6 +11,8 @@ octave: /[_^]+/ modifier: /[#b]/ + measure: "|" + // Variable assignment assignment: variable ass_op (list | pitch_class | random_integer | random_pitch | cycle | list_op | repeat_item) ass_op: /[=~]/