Added measures and fixed some bugs

This commit is contained in:
2023-02-26 20:42:20 +02:00
parent 443d4e6639
commit 78295da323
6 changed files with 77 additions and 75 deletions

View File

@ -72,7 +72,8 @@ def test_pitch_octaves(pattern: str, expected: list):
[ [
("w [1 [2 3]]", [0.5, 0.25, 0.25]), ("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]), ("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): def test_subdivisions(pattern: str, expected: list):
@ -82,13 +83,14 @@ def test_subdivisions(pattern: str, expected: list):
"pattern,expected", "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 :) 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): def test_repeats(pattern: str, expected: list):
assert collect(zparse(pattern),len(expected)*2,"note") == expected*2 assert collect(zparse(pattern),len(expected)*2,"note") == expected*2
@pytest.mark.parametrize( @pytest.mark.parametrize(
"pattern,expected", "pattern,expected",
[ [
@ -106,3 +108,20 @@ def test_looping_durations(pattern: str, expected: list):
durations.append(parsed[i].duration) durations.append(parsed[i].duration)
assert durations == expected 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

View File

@ -58,6 +58,7 @@ class Item(Meta):
"""Class for all Ziffers text based items""" """Class for all Ziffers text based items"""
text: str = field(default=None) text: str = field(default=None)
measure: int = field(default=0, init=False)
def get_updated_item(self, options: dict): def get_updated_item(self, options: dict):
"""Get updated item with replaced options """Get updated item with replaced options
@ -135,6 +136,20 @@ class Event(Item):
class Rest(Event): class Rest(Event):
"""Class for rests""" """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) @dataclass(kw_only=True)
class Pitch(Event): class Pitch(Event):
@ -211,25 +226,6 @@ class Pitch(Event):
""" """
return self.pitch_class 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) @dataclass(kw_only=True)
@ -261,13 +257,13 @@ class Chord(Event):
"""Class for chords""" """Class for chords"""
pitch_classes: list[Pitch] = field(default=None) pitch_classes: list[Pitch] = field(default=None)
notes: list[int] = field(default=None) note: list[int] = field(default=None)
inversions: int = field(default=None) inversions: int = field(default=None)
pitches: list[int] = field(default=None, init=False) pitch_class: list[int] = field(default=None, init=False)
freqs: list[float] = field(default=None, init=False) freq: list[float] = field(default=None, init=False)
octaves: list[int] = field(default=None, init=False) octave: list[int] = field(default=None, init=False)
durations: list[float] = field(default=None, init=False) duration: list[float] = field(default=None, init=False)
beats: list[float] = field(default=None, init=False) beat: list[float] = field(default=None, init=False)
def __post_init__(self): def __post_init__(self):
if self.inversions is not None: if self.inversions is not None:
@ -275,7 +271,7 @@ class Chord(Event):
def set_notes(self, notes: list[int]): def set_notes(self, notes: list[int]):
"""Set notes to the class""" """Set notes to the class"""
self.notes = notes self.note = notes
def invert(self, value: int): def invert(self, value: int):
"""Chord inversion""" """Chord inversion"""
@ -311,36 +307,12 @@ class Chord(Event):
durations.append(pitch.duration) durations.append(pitch.duration)
beats.append(pitch.beat) beats.append(pitch.beat)
self.pitches = pitches self.pitch = pitches
self.notes = notes self.note = notes
self.freqs = freqs self.freq = freqs
self.octaves = octaves self.octave = octaves
self.duration = durations self.duration = durations
self.beats = beats self.beat = 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
@dataclass(kw_only=True) @dataclass(kw_only=True)
@ -452,11 +424,13 @@ class Sequence(Meta):
elif isinstance(item, RepeatedSequence): elif isinstance(item, RepeatedSequence):
item.evaluate_values(options) item.evaluate_values(options)
repeats = item.repeats.get_value(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) yield from _normal_repeat(item.evaluated_values, repeats, options)
elif isinstance(item, RepeatedListSequence): elif isinstance(item, RepeatedListSequence):
repeats = item.repeats.get_value(options) 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) yield from _generative_repeat(item, repeats, options)
elif isinstance(item, Subdivision): elif isinstance(item, Subdivision):
item.evaluate_values(options) item.evaluate_values(options)
@ -484,6 +458,8 @@ class Sequence(Meta):
yield from _euclidean_items(item, options) yield from _euclidean_items(item, options)
elif isinstance(item, Modification): elif isinstance(item, Modification):
options = _parse_options(item, options) options = _parse_options(item, options)
elif isinstance(item, Measure):
item.reset_options(options)
elif isinstance(item, Meta): # Filters whitespace elif isinstance(item, Meta): # Filters whitespace
yield _update_item(item, options) yield _update_item(item, options)
@ -492,8 +468,8 @@ class Sequence(Meta):
item = item.get_value(options) item = item.get_value(options)
if isinstance(item, Pitch): if isinstance(item, Pitch):
return item.get_value(options) return item.get_value(options)
if not isinstance(item, Integer): if isinstance(item, Integer):
return 2 return item.get_value(options)
return item return item
def _update_item(item, options): def _update_item(item, options):
@ -548,9 +524,6 @@ class Sequence(Meta):
options[current.key] = current.value options[current.key] = current.value
return options 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: def _create_pitch(current: Item, options: dict) -> Pitch:
"""Create pitch based on values and options""" """Create pitch based on values and options"""
@ -623,7 +596,7 @@ class Sequence(Meta):
chord = Chord( chord = Chord(
text=pitch_text, text=pitch_text,
pitch_classes=pitch_classes, pitch_classes=pitch_classes,
notes=chord_notes, note=chord_notes,
kwargs=options, kwargs=options,
inversions=current.inversions, inversions=current.inversions,
) )
@ -691,11 +664,13 @@ class Ziffers(Sequence):
self.options = DEFAULT_OPTIONS.copy() self.options = DEFAULT_OPTIONS.copy()
self.start_options = self.options.copy() self.start_options = self.options.copy()
self.options["start_options"] = self.start_options
self.init_tree(self.options) self.init_tree(self.options)
def re_eval(self): def re_eval(self):
"""Re-evaluate the iterator""" """Re-evaluate the iterator"""
self.options = self.start_options.copy() self.options = self.start_options.copy()
self.options["start_options"] = self.start_options
self.init_tree(self.options) self.init_tree(self.options)
def init_tree(self, options): def init_tree(self, options):
@ -743,7 +718,7 @@ class Ziffers(Sequence):
def pitch_classes(self) -> list[int]: def pitch_classes(self) -> list[int]:
"""Return list of pitch classes as ints""" """Return list of pitch classes as ints"""
return [ return [
val.get_pitches() val.pitch_class
for val in self.evaluated_values for val in self.evaluated_values
if isinstance(val, (Pitch, Chord)) if isinstance(val, (Pitch, Chord))
] ]
@ -751,7 +726,7 @@ class Ziffers(Sequence):
def notes(self) -> list[int]: def notes(self) -> list[int]:
"""Return list of midi notes""" """Return list of midi notes"""
return [ return [
val.get_notes() val.note
for val in self.evaluated_values for val in self.evaluated_values
if isinstance(val, (Pitch, Chord)) if isinstance(val, (Pitch, Chord))
] ]
@ -759,7 +734,7 @@ class Ziffers(Sequence):
def durations(self) -> list[float]: def durations(self) -> list[float]:
"""Return list of pitch durations as floats""" """Return list of pitch durations as floats"""
return [ return [
val.get_durations() val.duration
for val in self.evaluated_values for val in self.evaluated_values
if isinstance(val, Event) if isinstance(val, Event)
] ]
@ -767,7 +742,7 @@ class Ziffers(Sequence):
def beats(self) -> list[float]: def beats(self) -> list[float]:
"""Return list of pitch durations as floats""" """Return list of pitch durations as floats"""
return [ 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]: def pairs(self) -> list[tuple]:
@ -781,7 +756,7 @@ class Ziffers(Sequence):
def octaves(self) -> list[int]: def octaves(self) -> list[int]:
"""Return list of octaves""" """Return list of octaves"""
return [ return [
val.get_octaves() val.octave
for val in self.evaluated_values for val in self.evaluated_values
if isinstance(val, (Pitch, Chord)) if isinstance(val, (Pitch, Chord))
] ]
@ -789,7 +764,7 @@ class Ziffers(Sequence):
def freqs(self) -> list[int]: def freqs(self) -> list[int]:
"""Return list of octaves""" """Return list of octaves"""
return [ return [
val.get_freqs() val.freq
for val in self.evaluated_values for val in self.evaluated_values
if isinstance(val, (Pitch, Chord)) if isinstance(val, (Pitch, Chord))
] ]

View File

@ -65,7 +65,7 @@ class ZiffersMusic21(converter.subConverters.SubConverter):
elif isinstance(item, Rest): elif isinstance(item, Rest):
m_item = note.Rest(item.duration * 4) m_item = note.Rest(item.duration * 4)
elif isinstance(item, Chord): elif isinstance(item, Chord):
m_item = chord.Chord(item.notes) m_item = chord.Chord(item.note)
m_item.duration.quarterLength = item.duration * 4 m_item.duration.quarterLength = item.duration * 4
note_stream.append(m_item) note_stream.append(m_item)
self.stream = note_stream.makeMeasures() self.stream = note_stream.makeMeasures()

View File

@ -46,7 +46,8 @@ DEFAULT_OPTIONS = MappingProxyType({
"octave": 0, "octave": 0,
"duration": 0.25, "duration": 0.25,
"key": "C4", "key": "C4",
"scale": "IONIAN" "scale": "IONIAN",
"measure": 0
}) })
OPERATORS = MappingProxyType({ OPERATORS = MappingProxyType({

View File

@ -29,6 +29,7 @@ from .classes import (
RepeatedSequence, RepeatedSequence,
VariableAssignment, VariableAssignment,
Variable, Variable,
Measure
) )
from .common import flatten, sum_dict from .common import flatten, sum_dict
from .defaults import DEFAULT_DURS, OPERATORS 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=text_prefix + "r", local_options=prefixes)
return Rest(text="r") return Rest(text="r")
def measure(self, items):
"""Return new measure"""
return Measure()
def random_integer(self, items) -> RandomInteger: def random_integer(self, items) -> RandomInteger:
"""Parses random integer syntax""" """Parses random integer syntax"""
if len(items) > 1: if len(items) > 1:

View File

@ -1,6 +1,6 @@
// Root for the rules // Root for the rules
?root: sequence -> start ?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 classes
pitch_class: prefix* pitch pitch_class: prefix* pitch
@ -11,6 +11,8 @@
octave: /[_^]+/ octave: /[_^]+/
modifier: /[#b]/ modifier: /[#b]/
measure: "|"
// Variable assignment // Variable assignment
assignment: variable ass_op (list | pitch_class | random_integer | random_pitch | cycle | list_op | repeat_item) assignment: variable ass_op (list | pitch_class | random_integer | random_pitch | cycle | list_op | repeat_item)
ass_op: /[=~]/ ass_op: /[=~]/