Added arpeggios and cyclic zip operation

This commit is contained in:
2023-03-02 20:45:32 +02:00
parent 6167c4be33
commit bc779b0c81
8 changed files with 230 additions and 66 deletions

View File

@ -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')

View File

@ -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")

View File

@ -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,
@ -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

View File

@ -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]

View File

@ -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()

View File

@ -59,7 +59,10 @@ OPERATORS = MappingProxyType({
"|": operator.or_,
"&": operator.and_,
"<<": operator.ilshift,
">>": operator.irshift
">>": operator.irshift,
"@": "vertical",
"#": "horizontal",
"<>": "zip"
})

View File

@ -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":

View File

@ -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