diff --git a/ziffers/classes.py b/ziffers/classes.py index 2f859d0..859e7c2 100644 --- a/ziffers/classes.py +++ b/ziffers/classes.py @@ -88,6 +88,9 @@ class Event(Item): duration: float = field(default=None) +@dataclass +class Rest(Event): + """Class for rests""" @dataclass(kw_only=True) class Pitch(Event): @@ -107,7 +110,7 @@ class Pitch(Event): self.text = str(self.pitch_class) self.update_note() - def update_note(self, force: bool=False): + def update_note(self, force: bool = False): """Update note if Key, Scale and Pitch-class are present""" if ( (self.key is not None) @@ -136,7 +139,6 @@ class Pitch(Event): if edit: self.update_note(True) - def set_note(self, note: int) -> int: """Sets a note for the pitch and returns the note. @@ -175,7 +177,9 @@ class RandomPitch(Event): Returns: int: Returns random pitch """ - return random.randint(0, get_scale_length(options.get("scale","Major")) if options else 9) + return random.randint( + 0, get_scale_length(options.get("scale", "Major")) if options else 9 + ) @dataclass(kw_only=True) @@ -195,7 +199,7 @@ class Chord(Event): def set_notes(self, notes: list[int]): """Set notes to the class""" self.notes = notes - + def update_notes(self, options): """Update notes""" notes = [] @@ -205,6 +209,7 @@ class Chord(Event): notes.append(pitch.note) self.notes = notes + @dataclass(kw_only=True) class RomanNumeral(Event): """Class for roman numbers""" @@ -257,7 +262,7 @@ class Sequence(Meta): self.text = self.__collect_text() def __getitem__(self, index): - return self.evaluated_values[index % len(self.evaluated_values)] + return self.values[index] def update_values(self, new_values): """Update value attributes from dict""" @@ -317,6 +322,7 @@ class Sequence(Meta): for item in tree: yield from _resolve_item(item, options) + # TODO: Refactor types to isinstance? def _update_options(current: Item, options: dict) -> dict: """Update options based on current item""" if current.item_type == "change": # Change options @@ -369,30 +375,43 @@ class Sequence(Meta): """Create chord fom roman numeral""" key = options["key"] scale = options["scale"] - pitch_text="" + pitch_text = "" pitch_classes = [] chord_notes = [] for note in current.notes: pitch_dict = midi_to_pitch_class(note, key, scale) - pitch_classes.append(Pitch(pitch_class=pitch_dict["pitch_class"],kwargs=(pitch_dict | options))) - pitch_text+=pitch_dict["text"] - chord_notes.append(note_from_pc( - root=key, - pitch_class=pitch_dict["pitch_class"], - intervals=scale, - modifier=pitch_dict.get("modifier",0), - octave=pitch_dict.get("octave",0) - )) + pitch_classes.append( + Pitch( + pitch_class=pitch_dict["pitch_class"], + kwargs=(pitch_dict | options), + ) + ) + pitch_text += pitch_dict["text"] + chord_notes.append( + note_from_pc( + root=key, + pitch_class=pitch_dict["pitch_class"], + intervals=scale, + modifier=pitch_dict.get("modifier", 0), + octave=pitch_dict.get("octave", 0), + ) + ) chord = Chord( - text=pitch_text, pitch_classes=pitch_classes, notes=chord_notes, kwargs=options + text=pitch_text, + pitch_classes=pitch_classes, + notes=chord_notes, + kwargs=options, ) return chord def _update_item(item, options): """Update or create new pitch""" if set(("key", "scale")) <= options.keys(): - if isinstance(item,Pitch): + if isinstance(item, Pitch): + item.update_options(options) + item.update_note() + if isinstance(item,Rest): item.update_options(options) elif isinstance(item, (RandomPitch, RandomInteger)): item = _create_pitch(item, options) @@ -420,7 +439,6 @@ class Sequence(Meta): self, values=[item for item in self.values if isinstance(item, keep)] ) - @dataclass(kw_only=True) class Ziffers(Sequence): """Main class for holding options and the current state""" @@ -430,6 +448,9 @@ class Ziffers(Sequence): iterator = None current: Item = field(default=None) + def __getitem__(self, index): + return self.evaluated_values[index % len(self.evaluated_values)] + def __iter__(self): return self @@ -547,7 +568,7 @@ class RandomInteger(Item): self.max = new_max # pylint: disable=locally-disabled, unused-argument - def get_value(self, options: dict=None): + def get_value(self, options: dict = None): """Evaluate the random value for the generator""" return random.randint(self.min, self.max) diff --git a/ziffers/mapper.py b/ziffers/mapper.py index 41d1c13..eb7b875 100644 --- a/ziffers/mapper.py +++ b/ziffers/mapper.py @@ -8,6 +8,7 @@ from .classes import ( OctaveChange, OctaveAdd, Pitch, + Rest, RandomPitch, RandomPercent, Chord, @@ -37,19 +38,31 @@ from .scale import parse_roman, chord_from_roman_numeral class ZiffersTransformer(Transformer): """Rules for transforming Ziffers expressions into tree.""" - def __init__(self, options: Optional[dict] = None): - super().__init__() - self.options = options - def start(self, items) -> Ziffers: """Root for the rules""" - # seq = Sequence(values=items[0]) return Ziffers(values=items[0], options={}) def sequence(self, items): """Flatten sequence""" return flatten(items) + def rest(self, items): + """Return duration event""" + if len(items)>0: + chars = items[0] + val = DEFAULT_DURS[chars[0]] + # TODO: Add support for dots + #if len(chars)>1: + # dots = len(chars)-1 + # val = val * (2.0 - (1.0 / (2 * dots))) + return Rest(text=chars+"r", duration=val) + return Rest(text="r") + + return Rest(text=chars+"r", duration=val) + + def rest_duration(self,items): + return items[0].value + def random_integer(self, item) -> RandomInteger: """Parses random integer syntax""" val = item[0][1:-1].split(",") diff --git a/ziffers/ziffers.lark b/ziffers/ziffers.lark index bccf0e1..7a52d1a 100644 --- a/ziffers/ziffers.lark +++ b/ziffers/ziffers.lark @@ -1,6 +1,6 @@ // Root for the rules ?root: sequence -> start - sequence: (pitch_class | 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 | 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)* // Pitch classes pitch_class: prefix* pitch @@ -10,7 +10,19 @@ escaped_octave: /<-?[0-9]>/ octave: /[_^]+/ modifier: /[#b]/ + + // Durations + // TODO: Refactor dchar as: /([mklpdcwyhnqaefsxtgujzo](\.)*)(?=\d)/ + duration_chars: dotted_dur+ + dotted_dur: dchar dot* + decimal: /-?[0-9]+\.[0-9]+/ + dchar: /[mklpdcwyhnqaefsxtgujzo]/ + dot: "." + rest: rest_duration? "r" + // TODO: Refactor (\.)* when other durchars uses lookaheads + rest_duration: /([mklpdcwyhnqaefsxtgujzo])(?=r)/ + // Chords chord: pitch_class pitch_class+ named_roman: roman_number (("^" chord_name))? // TODO: Add | ("+" number) @@ -34,21 +46,13 @@ operator: /([\+\-\*\/%]|<<|>>)/ // Euclidean cycles + // TODO: Support randomization etc. + //euclid_operator: (">" | "<") number "," number ["," number] (">" | "<") euclid: list euclid_operator list? ?euclid_operator: /<[0-9]+,[0-9]+(,[0-9])?>/ - // TODO: Support randomization etc. - //euclid_operator: (">" | "<") number "," number ["," number] (">" | "<") - // Lisp like list operation lisp_operation: "(" operator WS sequence ")" - - // Durations - duration_chars: dotted_dur+ - dotted_dur: dchar dot* - decimal: /-?[0-9]+\.[0-9]+/ - dchar: /[mklpdcwyhnqaefsxtgujzo]/ - dot: "." // Subdivision subdivision: "[" subitems "]" @@ -59,7 +63,7 @@ oct_change: escaped_octave WS dur_change: (decimal | char_change) char_change: dchar_not_prefix+ - dchar_not_prefix: /([mklpdcwyhnqaefsxtgujzo](\.)*)(?!\d)/ + dchar_not_prefix: /([mklpdcwyhnqaefsxtgujzo](\.)*)(?![\dr])/ // Generative rules random_integer: /\(-?[0-9]+,-?[0-9]+\)/