diff --git a/examples/music21/music21_example.py b/examples/music21/music21_example.py new file mode 100644 index 0000000..c53f80d --- /dev/null +++ b/examples/music21/music21_example.py @@ -0,0 +1,8 @@ +from music21 import * +from sardine import * +from ziffers import * + + +s = to_music21('(i v vi vii^dim)@(q0 e2 s1 012)',time="4/4") + +s.show('midi') \ No newline at end of file diff --git a/examples/music21_example.py b/examples/music21/music21_example_2.py similarity index 69% rename from examples/music21_example.py rename to examples/music21/music21_example_2.py index a4a8f61..6b8321b 100644 --- a/examples/music21_example.py +++ b/examples/music21/music21_example_2.py @@ -2,12 +2,6 @@ from music21 import * from sardine import * from ziffers import * - -s = to_music21('i v vi vii^dim',time="4/4") - -s.show() -s.show('midi') - parsed = zparse('1 2 qr e 124') s2 = to_music21(parsed,time="4/4") diff --git a/ziffers/classes.py b/ziffers/classes.py index 938d172..d71b5d2 100644 --- a/ziffers/classes.py +++ b/ziffers/classes.py @@ -6,7 +6,7 @@ import operator import random from copy import deepcopy from .defaults import DEFAULT_OPTIONS -from .common import repeat_text +from .common import repeat_text, cyclic_zip from .scale import ( note_from_pc, midi_to_pitch_class, @@ -179,13 +179,13 @@ class Pitch(Event): if self.text is None: self.text = str(self.pitch_class) self.update_note() - #self._update_text() + # self._update_text() def _update_text(self): if self.octave is not None: - self.text = repeat_text("^","_",self.octave) + self.text + self.text = repeat_text("^", "_", self.octave) + self.text if self.modifier is not None: - self.text = repeat_text("#","b",self.modifier) + self.text + self.text = repeat_text("#", "b", self.modifier) + self.text def get_note(self): """Getter for note""" @@ -382,7 +382,9 @@ class Chord(Event): self.freqs = freqs self.octaves = octaves self.durations = durations + self.duration = durations[0] self.beats = beats + self.text = "".join([val.text for val in self.pitch_classes]) @dataclass(kw_only=True) @@ -394,6 +396,7 @@ class RomanNumeral(Event): notes: list[int] = field(default=None, init=False) pitch_classes: list = field(default=None, init=False) inversions: int = field(default=None) + evaluated_chord: Chord = None def set_notes(self, chord_notes: list[int]): """Set notes to roman numeral @@ -414,6 +417,42 @@ class RomanNumeral(Event): for pitch in pitches: self.pitch_classes.append(Pitch(**pitch)) + def evaluate_chord(self, options: dict) -> Chord: + """Create chord fom roman numeral""" + key = options["key"] + scale = options["scale"] + pitch_text = "" + pitch_classes = [] + self.notes = chord_from_degree( + self.value, self.chord_type, options["scale"], options["key"] + ) + for note in self.notes: + pitch_dict = midi_to_pitch_class(note, key, scale) + pitch_classes.append( + Pitch( + pitch_class=pitch_dict["pitch_class"], + note=note, + freq=midi_to_freq(note), + kwargs=(options | pitch_dict), + ) + ) + pitch_text += pitch_dict["text"] + + chord = Chord( + text=pitch_text, + pitch_classes=pitch_classes, + duration=options["duration"], + notes=self.notes, + kwargs=options, + inversions=self.inversions, + ) + + chord.update_notes(options) + + self.evaluated_chord = chord + + return chord + @dataclass(kw_only=True) class Function(Event): @@ -457,6 +496,9 @@ class Sequence(Meta): def __getitem__(self, index): return self.values[index] + def __len__(self): + return len(self.values) + def update_local_options(self): """Update value attributes from dict""" if self.local_options: @@ -558,7 +600,7 @@ class Sequence(Meta): item.update_options(options) item.update_notes(options) elif isinstance(item, RomanNumeral): - item = _create_chord_from_roman(item, options) + item = item.evaluate_chord(options) return item def _generative_repeat(tree: list, times: int, options: dict): @@ -637,40 +679,6 @@ class Sequence(Meta): ) return new_pitch - def _create_chord_from_roman(current: RomanNumeral, options: dict) -> Chord: - """Create chord fom roman numeral""" - key = options["key"] - scale = options["scale"] - pitch_text = "" - pitch_classes = [] - current.notes = chord_from_degree( - current.value, current.chord_type, options["scale"], options["key"] - ) - for note in current.notes: - pitch_dict = midi_to_pitch_class(note, key, scale) - pitch_classes.append( - Pitch( - pitch_class=pitch_dict["pitch_class"], - note=note, - freq=midi_to_freq(note), - kwargs=(options | pitch_dict), - ) - ) - pitch_text += pitch_dict["text"] - - chord = Chord( - text=pitch_text, - pitch_classes=pitch_classes, - duration=options["duration"], - notes=current.notes, - kwargs=options, - inversions=current.inversions, - ) - - chord.update_notes(options) - - return chord - # Start of the main function: Evaluate and flatten the Ziffers object tree values = self.evaluated_values if eval_tree else self.values for item in values: @@ -978,23 +986,31 @@ class ListOperation(Sequence): def evaluate(self, options=DEFAULT_OPTIONS.copy()): """Evaluates the operation""" - def filter_operation(input_list, options): - flattened_list = [] + def _filter_whitespace(input_list): + for item in input_list: + if isinstance(item, Meta): + yield item + def _filter_operation(input_list, options): + """Filter and evaluate values""" + flattened_list = [] for item in input_list: if isinstance(item, (list, Sequence)): if isinstance(item, ListOperation): flattened_list.extend(item.evaluated_values) else: - flattened_list.append(filter_operation(item, options)) + flattened_list.append(_filter_operation(item, options)) elif isinstance(item, Cyclic): value = item.get_value() if isinstance(value, Sequence): - flattened_list.extend(filter_operation(value, options)) + flattened_list.extend(_filter_operation(value, options)) elif isinstance(value, (Event, RandomInteger, Integer)): flattened_list.append(value) elif isinstance(item, Modification): options = options | item.as_options() + elif isinstance(item, RomanNumeral): + item = item.evaluate_chord(options) + flattened_list.append(item) elif isinstance(item, Range): flattened_list.extend(list(item.evaluate(options))) elif isinstance(item, (Event, RandomInteger, Integer)): @@ -1006,9 +1022,123 @@ class ListOperation(Sequence): return flattened_list + def _vertical_arpeggio(left, right, options): + """Vertical arpeggio operation, eg. (135)@(q 1 2 021)""" + left = _filter_operation(left, options) + right = _filter_operation(right, options) + left = list(left.evaluate_tree(options)) + right = list(right.evaluate_tree(options)) + arp_items = [] + + for item in left: + for index in right: + pcs = item.pitch_classes + if isinstance(index, Pitch): + new_pitch = deepcopy(pcs[index.get_value(options) % len(pcs)]) + new_pitch.duration = index.duration + arp_items.append(new_pitch) + else: # Should be a chord + new_pitches = [] + for pitch in index.pitch_classes: + new_pitch = deepcopy( + pcs[pitch.get_value(options) % len(pcs)] + ) + new_pitch.duration = pitch.duration + new_pitches.append(new_pitch) + new_chord = Chord(pitch_classes=new_pitches, kwargs=options) + new_chord.update_notes() + new_chord.text = "".join( + [val.text for val in new_chord.pitch_classes] + ) + arp_items.append(new_chord) + + return Sequence(values=arp_items) + + def _horizontal_arpeggio(left, right, options): + """Horizontal arpeggio operation, eg. (1 2 3 4)#(0 3 2 1)""" + left = _filter_operation(left, options) + right = _filter_operation(right, options) + left = list(left.evaluate_tree(options)) + right = list(right.evaluate_tree(options)) + arp_items = [] + for index in right: + new_item = deepcopy(left[index.get_value(options) % len(left)]) + arp_items.append(new_item) + return Sequence(values=arp_items) + + def _cyclic_zip(left, right, options): + """Cyclic zip operaiton, eg. (q e)<>(1 2 3)""" + left = list(_filter_whitespace(left)) + right = list(_filter_whitespace(right)) + result = Sequence(values=cyclic_zip(left, right)) + return _filter_operation(result, options) + + def _python_operations(left, right, options): + """Python math operations""" + + def __chord_operation(chord, pitch_y, yass, options): + """Operation for single chords""" + new_pitches = [] + pitch_y = pitch_y.get_value(options) + for pitch_x in chord.pitch_classes: + pitch_x = pitch_x.pitch_class + new_pitch = Pitch( + pitch_class=operation( + pitch_y if yass else pitch_x, pitch_x if yass else pitch_y + ), + kwargs=options, + ) + new_pitches.append(new_pitch) + new_chord = Chord(pitch_classes=new_pitches, kwargs=options) + new_chord.update_notes() + return new_chord + + # _python_operation starts. Filter & evaluate items. + + left = _filter_operation(left, options) + if isinstance(right, (Sequence, Cyclic)): + right = _filter_operation(right, options) + + # Create product of items. + pairs = product( + (right.values if isinstance(right, Sequence) else [right]), left + ) + + results = [] + for first, second in pairs: + if isinstance(first, Chord) and isinstance(second, Chord): + new_pitches = [] + for pitch_x in first.pitch_classes: + for pitch_y in second.pitch_classes: + new_pitch = Pitch( + pitch_class=operation( + pitch_x.pitch_class, pitch_y.pitch_class + ), + kwargs=options, + ) + new_pitches.append(new_pitch) + new_chord = Chord(pitch_classes=new_pitches, kwargs=options) + new_chord.update_notes() + outcome = new_chord + elif isinstance(first, Chord): + outcome = __chord_operation(first, second, False, options) + elif isinstance(second, Chord): + outcome = __chord_operation(second, first, True, options) + else: + outcome = Pitch( + pitch_class=operation( + first.get_value(options), second.get_value(options) + ), + kwargs=second.get_options(), + ) + results.append(outcome) + return results + + # Start of the evaluate() function + operators = self.values[1::2] # Fetch every second operator element values = self.values[::2] # Fetch every second list element - values = filter_operation(values, options) # Filter out + # values = _filter_operation(values, options) # Filter out if len(values) == 1: return values[0] # If right hand doesnt contain anything sensible left = values[0] # Start results with the first array @@ -1016,16 +1146,15 @@ class ListOperation(Sequence): for i, operand in enumerate(operators): operation = operand.value right = values[i + 1] - pairs = product( - (right.values if isinstance(right, Sequence) else [right]), left - ) - left = [ - Pitch( - pitch_class=operation(x.get_value(options), y.get_value(options)), - kwargs=y.get_options(), - ) - for (x, y) in pairs - ] + if isinstance(operation, str): + if operation == "vertical": + left = _vertical_arpeggio(left, right, options) + elif operation == "horizontal": + left = _horizontal_arpeggio(left, right, options) + if operation == "zip": + left = _cyclic_zip(left, right, options) + else: + left = _python_operations(left, right, options) return left diff --git a/ziffers/common.py b/ziffers/common.py index 83b026d..f9354c5 100644 --- a/ziffers/common.py +++ b/ziffers/common.py @@ -1,6 +1,6 @@ """ Common methods used in parsing """ import re - +from copy import deepcopy def flatten(arr: list) -> list: """Flattens array""" @@ -93,3 +93,20 @@ def euclidian_rhythm(pulses: int, length: int, rot: int = 0): bool_list = [_starts_descent(res_list, index) for index in range(length)] return rotation(bool_list, rot) + + +def cyclic_zip(first: list, second: list) -> list: + """Cyclic zip method + + Args: + first (list): First list + second (list): Second list + + Returns: + list: Cyclicly zipped list + """ + max_length = max(len(first), len(second)) + result = [] + for i in range(max_length): + result.append([first[i % len(first)], second[i % len(second)]]) + return [deepcopy(item) for sublist in result for item in sublist] diff --git a/ziffers/converters.py b/ziffers/converters.py index 23d3035..e9902c1 100644 --- a/ziffers/converters.py +++ b/ziffers/converters.py @@ -66,6 +66,6 @@ class ZiffersMusic21(converter.subConverters.SubConverter): m_item = note.Rest(item.duration * 4) elif isinstance(item, Chord): m_item = chord.Chord(item.notes) - m_item.duration.quarterLength = item.durations * 4 + 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 eb34e56..bd84abe 100644 --- a/ziffers/defaults.py +++ b/ziffers/defaults.py @@ -59,7 +59,10 @@ OPERATORS = MappingProxyType({ "|": operator.or_, "&": operator.and_, "<<": operator.ilshift, - ">>": operator.irshift + ">>": operator.irshift, + "@": "vertical", + "#": "horizontal", + "<>": "zip" }) diff --git a/ziffers/scale.py b/ziffers/scale.py index 693132f..660770d 100644 --- a/ziffers/scale.py +++ b/ziffers/scale.py @@ -301,7 +301,19 @@ def midi_to_pitch_class(note: int, key: str | int, scale: str) -> dict: def chord_from_degree( degree: int, name: str, scale: str, root: str | int, num_octaves: int = 1 -): +) -> list[int]: + """Generate chord from scale + + Args: + degree (int): Chord degree + name (str): Chord type + scale (str): Scale name + root (str | int): Root for the chord + num_octaves (int, optional): Number of octaves. Defaults to 1. + + Returns: + list[int]: Created chord as list of midi notes + """ root = note_name_to_midi(root) if isinstance(root, str) else root if name is None and scale.lower().capitalize() == "Chromatic": diff --git a/ziffers/spec/ziffers.lark b/ziffers/spec/ziffers.lark index 28ee5e2..048a8a2 100644 --- a/ziffers/spec/ziffers.lark +++ b/ziffers/spec/ziffers.lark @@ -52,7 +52,8 @@ // Right recursive list operation list_op: list (operator right_op)+ right_op: list | number - operator: /([\+\-\*\/%\|\&]|<<|>>)/ + //operator: "+" | "-" | "*" | "%" | "&" | "|" | "<<" | ">>" | "@" | "#" + operator: /([\+\-\*\/%\|\&]|<<|>>|@|#|<>)/ // Euclidean cycles // TODO: Support randomization etc. @@ -72,7 +73,7 @@ oct_change: escaped_octave WS dur_change: (decimal | char_change) char_change: dchar_not_prefix+ - dchar_not_prefix: /([mklpdcwyhnqaefsxtgujzo](\.)*)(?=[ >])/ + dchar_not_prefix: /([mklpdcwyhnqaefsxtgujzo](\.)*)(?=[ >)])/ // Generative rules random_integer: prefix* random_integer_re