Moving things around

This commit is contained in:
2023-03-04 12:36:09 +02:00
parent 5fd03fac6b
commit 13f68f7ee7
6 changed files with 864 additions and 785 deletions

View File

@ -0,0 +1,3 @@
from .items import *
from .sequences import *
from .root import *

592
ziffers/classes/items.py Normal file
View File

@ -0,0 +1,592 @@
""" Ziffers item classes """
from dataclasses import dataclass, field, asdict
import operator
import random
from ..scale import (
note_from_pc,
midi_to_pitch_class,
midi_to_freq,
get_scale_length,
chord_from_degree,
)
from ..common import repeat_text
@dataclass(kw_only=True)
class Meta:
"""Abstract class for all Ziffers items"""
kwargs: dict = field(default=None, repr=False)
local_options: dict = field(default_factory=dict)
def __post_init__(self):
if self.kwargs:
self.update_options(self.kwargs)
def replace_options(self, new_values):
"""Replaces attribute values from dict"""
for key, value in new_values.items():
if hasattr(self, key):
setattr(self, key, value)
def update_options(self, options):
"""Updates attribute values only if value is None"""
merged_options = self.local_options | options
for key, value in merged_options.items():
if hasattr(self, key):
if key == "octave":
local_value = self.local_options.get("octave", False)
oct_change = self.local_options.get("octave_change", False)
if oct_change:
setattr(self, key, oct_change)
elif local_value:
setattr(self, key, value + local_value)
elif getattr(self, key) is None:
setattr(self, key, value)
elif getattr(self, key) is None:
local_value = self.local_options.get(key, False)
if local_value:
setattr(self, key, local_value)
else:
setattr(self, key, value)
def dict(self):
"""Returns safe dict from the dataclass"""
return {k: str(v) for k, v in asdict(self).items()}
@dataclass(kw_only=True)
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
Args:
options (dict): Options as a dict
Returns:
Item: Returns updated item
"""
self.replace_options(options)
return self
def get_options(self) -> dict:
"""Return local options from item
Returns:
dict: Options as a dict
"""
keys = ["octave", "modifier", "key", "scale", "duration"]
return {key: getattr(self, key) for key in keys if hasattr(self, key)}
@dataclass(kw_only=True)
class Whitespace:
"""Class for whitespace"""
text: str
@dataclass(kw_only=True)
class Modification(Item):
"""Superclass for pitch modifications"""
key: str
value: ...
def as_options(self):
"""Return modification as a dict"""
return {self.key: self.value}
@dataclass(kw_only=True)
class DurationChange(Modification):
"""Class for changing duration"""
value: float
key: str = field(default="duration", repr=False, init=False)
@dataclass
class OctaveChange(Modification):
"""Class for changing octave"""
value: int
key: str = field(default="octave", repr=False, init=False)
@dataclass(kw_only=True)
class OctaveAdd(Modification):
"""Class for modifying octave"""
value: int
key: str = field(default="octave", repr=False, init=False)
@dataclass(kw_only=True)
class Event(Item):
"""Abstract class for events with duration"""
duration: float = field(default=None)
@dataclass
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):
"""Class for pitch in time"""
pitch_class: int
octave: int = field(default=None)
modifier: int = field(default=None)
note: int = field(default=None)
key: str = field(default=None)
scale: str | list = field(default=None)
freq: float = field(default=None)
beat: float = field(default=None)
def __post_init__(self):
super().__post_init__()
if self.text is None:
self.text = str(self.pitch_class)
self.update_note()
# self._update_text()
def _update_text(self):
if self.octave is not None:
self.text = repeat_text("^", "_", self.octave) + self.text
if self.modifier is not None:
self.text = repeat_text("#", "b", self.modifier) + self.text
def get_note(self):
"""Getter for note"""
return self.note
def get_freq(self):
"""Getter for freq"""
return self.freq
def get_octave(self):
"""Getter for octave"""
return self.octave
def get_beat(self):
"""Getter for beat"""
return self.beat
def get_pitch_class(self):
"""Getter for pitche"""
return self.pitch_class
def get_duration(self):
"""Getter for duration"""
return self.duration
def update_note(self, force: bool = False):
"""Update note if Key, Scale and Pitch-class are present"""
if (
(self.key is not None)
and (self.scale is not None)
and (self.pitch_class is not None)
and (self.note is None or force)
):
note = note_from_pc(
root=self.key,
pitch_class=self.pitch_class,
intervals=self.scale,
modifier=self.modifier if self.modifier is not None else 0,
octave=self.octave if self.octave is not None else 0,
)
self.freq = midi_to_freq(note)
self.note = note
if self.duration is not None:
self.beat = self.duration * 4
def check_note(self, options: dict):
"""Check for note modification"""
if "key" in options and self.key is not options["key"]:
self.key = options["key"]
edit = True
if "scale" in options and self.scale is not options["scale"]:
self.scale = options["scale"]
edit = True
if edit:
self.update_note(True)
def set_note(self, note: int) -> int:
"""Sets a note for the pitch and returns the note.
Args:
note (int): Midi note
Returns:
int: Returns the saved note
"""
self.note = note
return note
def set_freq(self, freq: float):
"""Set frequency for the pitch object"""
self.freq = freq
# pylint: disable=locally-disabled, unused-argument
def get_value(self, options) -> int:
"""Returns the pitch class
Returns:
int: Integer value for the pitch
"""
return self.pitch_class
@dataclass(kw_only=True)
class RandomPitch(Event):
"""Class for random pitch"""
pitch_class: int = field(default=None)
def get_value(self, options: dict) -> int:
"""Return random value
Returns:
int: Returns random pitch
"""
return random.randint(
0, get_scale_length(options.get("scale", "Major")) if options else 9
)
@dataclass(kw_only=True)
class RandomPercent(Item):
"""Class for random percent"""
percent: float = field(default=None)
@dataclass(kw_only=True)
class Chord(Event):
"""Class for chords"""
pitch_classes: list[Pitch] = field(default=None)
notes: 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)
def __post_init__(self):
if self.inversions is not None:
self.invert(self.inversions)
@property
def note(self):
"""Synonym for notes"""
return self.notes
def set_notes(self, notes: list[int]):
"""Set notes to the class"""
self.notes = notes
def get_note(self):
"""Getter for notes"""
return self.notes
def get_freq(self):
"""Getter for freqs"""
return self.freqs
def get_octave(self):
"""Getter for octaves"""
return self.octaves
def get_beat(self):
"""Getter for beats"""
return self.beats
def get_pitch_class(self):
"""Getter for pitches"""
return self.pitches
def get_duration(self):
"""Getter for durations"""
return self.durations
def invert(self, value: int):
"""Chord inversion"""
new_pitches = (
list(reversed(self.pitch_classes)) if value < 0 else self.pitch_classes
)
for _ in range(abs(value)):
new_pitch = new_pitches[_ % len(new_pitches)]
if not new_pitch.local_options.get("octave"):
new_pitch.local_options["octave"] = 0
new_pitch.local_options["octave"] += -1 if value <= 0 else 1
self.pitch_classes = new_pitches
def update_notes(self, options=None):
"""Update notes"""
pitches, notes, freqs, octaves, durations, beats = ([] for _ in range(6))
# Update notes
for pitch in self.pitch_classes:
if options is not None:
pitch.update_options(options)
pitch.update_note(True)
# Sort by generated notes
self.pitch_classes = sorted(self.pitch_classes, key=lambda x: x.note)
# Create helper lists
for pitch in self.pitch_classes:
pitches.append(pitch.pitch_class)
notes.append(pitch.note)
freqs.append(pitch.freq)
octaves.append(pitch.octave)
durations.append(pitch.duration)
beats.append(pitch.beat)
self.pitches = pitches
self.notes = notes
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)
class RomanNumeral(Event):
"""Class for roman numbers"""
value: str = field(default=None)
chord_type: str = field(default=None)
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
Args:
chord_notes (list[int]): List of notes in midi to be added
"""
self.notes = chord_notes
def set_pitch_classes(self, pitches: list[tuple]):
"""Set pitch classes to roman numeral
Args:
pitches (list[tuple]): Pitch classes to be added
"""
if self.pitch_classes is None:
self.pitch_classes = []
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):
"""Class for functions"""
run: ... = field(default=None)
@dataclass(kw_only=True)
class FunctionList(Event):
"""Class for functions"""
values: list
@dataclass(kw_only=True)
class VariableAssignment(Item):
"""Class for defining variables"""
variable: str
value: Item
pre_eval: bool
@dataclass(kw_only=True)
class Variable(Event):
"""Class for using variables"""
name: str
@dataclass(kw_only=True)
class VariableList(Item):
"""Class for using variables"""
values: list
@dataclass(kw_only=True)
class Integer(Item):
"""Class for integers"""
value: int
# pylint: disable=locally-disabled, unused-argument
def get_value(self, options):
"""Return value of the integer"""
return self.value
@dataclass(kw_only=True)
class RandomInteger(Item):
"""Class for random integer"""
min: int
max: int
def __post_init__(self):
super().__post_init__()
if self.min > self.max:
new_max = self.min
self.min = self.max
self.max = new_max
# pylint: disable=locally-disabled, unused-argument
def get_value(self, options: dict = None):
"""Evaluate the random value for the generator"""
return random.randint(self.min, self.max)
@dataclass(kw_only=True)
class Cyclic(Item):
"""Class for cyclic sequences"""
values: list
cycle: int = 0
wrap_start: str = field(default="<", repr=False)
wrap_end: str = field(default=">", repr=False)
def __post_init__(self):
super().__post_init__()
self.text = self.__collect_text()
self.values = [val for val in self.values if not isinstance(val, Whitespace)]
def __collect_text(self) -> str:
"""Collect text value from values"""
text = "".join([val.text for val in self.values])
if self.wrap_start is not None:
text = self.wrap_start + text
if self.wrap_end is not None:
text = text + self.wrap_end
return text
def get_value(self, options=None):
"""Get the value for the current cycle"""
value = self.values[self.cycle % len(self.values)]
self.cycle += 1
return value
@dataclass(kw_only=True)
class Range(Item):
"""Class for range"""
start: int = field(default=None)
end: int = field(default=None)
def evaluate(self, options):
"""Evaluates range and generates a generator of Pitches"""
merged_options = options | self.local_options
if options["octave"]:
merged_options["octave"] += options["octave"]
if self.start < self.end:
for i in range(self.start, self.end + 1):
yield Pitch(pitch_class=i, kwargs=merged_options)
elif self.start > self.end:
for i in reversed(range(self.end, self.start + 1)):
yield Pitch(pitch_class=i, kwargs=merged_options)
else:
yield Pitch(pitch_class=self.start, kwargs=merged_options)
@dataclass(kw_only=True)
class Operator(Item):
"""Class for math operators"""
value: ...
@dataclass(kw_only=True)
class Operation(Item):
"""Class for lisp-like operations: (+ 1 2 3) etc."""
values: list
operator: operator
@dataclass(kw_only=True)
class Atom(Item):
"""Class for evaluable atoms"""
value: ...

154
ziffers/classes/root.py Normal file
View File

@ -0,0 +1,154 @@
"""Root class for Ziffers object"""
from dataclasses import dataclass, field
from itertools import islice, cycle
from ..defaults import DEFAULT_OPTIONS
from .items import Item, Pitch, Chord, Event
from .sequences import Sequence, Subdivision
@dataclass(kw_only=True)
class Ziffers(Sequence):
"""Main class for holding options and the current state"""
options: dict = field(default_factory=DEFAULT_OPTIONS.copy())
start_options: dict = None
loop_i: int = field(default=0, init=False)
cycle_i: int = field(default=0, init=False)
iterator = None
current: Item = field(default=None)
cycle_length: int = field(default=0, init=False)
def __getitem__(self, index):
self.loop_i = index % self.cycle_length
new_cycle = index // self.cycle_length
# Re-evaluate if the prior loop has ended
if new_cycle > self.cycle_i or new_cycle < self.cycle_i:
self.re_eval()
self.cycle_i = new_cycle
self.cycle_length = len(self.evaluated_values)
self.loop_i = index % self.cycle_length
return self.evaluated_values[self.loop_i]
def __iter__(self):
return self
def __next__(self):
self.current = next(self.iterator)
self.loop_i += 1
return self.current
# pylint: disable=locally-disabled, dangerous-default-value
def init_opts(self, options=None):
"""Evaluate the Ziffers tree using the options"""
self.options.update(DEFAULT_OPTIONS.copy())
if options:
self.options.update(options)
else:
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):
"""Initialize evaluated values and perform post-evaluation"""
self.evaluated_values = list(self.evaluate_tree(options))
self.evaluated_values = list(self.post_evaluation())
self.iterator = iter(self.evaluated_values)
self.cycle_length = len(self.evaluated_values)
def post_evaluation(self):
"""Post-evaluation performs evaluation that can only be done after initial evaluation"""
for item in self.evaluated_values:
if isinstance(item, Subdivision):
yield from item.evaluate_durations()
else:
yield item
def get_list(self):
"""Return list"""
return list(self)
def take(self, num: int) -> list[Pitch]:
"""Take number of pitch classes from the parsed sequence. Cycles from the beginning.
Args:
num (int): Number of pitch classes to take from the sequence
Returns:
list: List of pitch class items
"""
return list(islice(cycle(self), num))
def loop(self) -> iter:
"""Return cyclic loop"""
return cycle(iter(self))
def set_defaults(self, options: dict):
"""Sets options for the parser
Args:
options (dict): Options as a dict
"""
self.options = DEFAULT_OPTIONS.copy() | options
def pitch_classes(self) -> list[int]:
"""Return list of pitch classes as ints"""
return [
val.get_pitch_class()
for val in self.evaluated_values
if isinstance(val, (Pitch, Chord))
]
def notes(self) -> list[int]:
"""Return list of midi notes"""
return [
val.get_note()
for val in self.evaluated_values
if isinstance(val, (Pitch, Chord))
]
def durations(self) -> list[float]:
"""Return list of pitch durations as floats"""
return [
val.get_duration()
for val in self.evaluated_values
if isinstance(val, Event)
]
def beats(self) -> list[float]:
"""Return list of pitch durations as floats"""
return [
val.get_beat() for val in self.evaluated_values if isinstance(val, Event)
]
def pairs(self) -> list[tuple]:
"""Return list of pitches and durations"""
return [
(val.get_pitch_class(), val.get_duration())
for val in self.evaluated_values
if isinstance(val, Pitch)
]
def octaves(self) -> list[int]:
"""Return list of octaves"""
return [
val.get_octave()
for val in self.evaluated_values
if isinstance(val, (Pitch, Chord))
]
def freqs(self) -> list[int]:
"""Return list of octaves"""
return [
val.get_freq()
for val in self.evaluated_values
if isinstance(val, (Pitch, Chord))
]

View File

@ -0,0 +1,601 @@
""" Sequence classes for Ziffers """
from dataclasses import dataclass, field, replace
from itertools import product
from types import LambdaType
from copy import deepcopy
from ..defaults import DEFAULT_OPTIONS
from ..common import cyclic_zip, euclidian_rhythm
from ..scale import note_from_pc, midi_to_freq
from .items import (
Meta,
Item,
Event,
DurationChange,
OctaveChange,
OctaveAdd,
Pitch,
Rest,
RandomPitch,
Chord,
RomanNumeral,
Cyclic,
RandomInteger,
Range,
Integer,
VariableAssignment,
Variable,
VariableList,
Measure,
Function,
Modification,
Whitespace,
)
@dataclass(kw_only=True)
class Sequence(Meta):
"""Class for sequences of items"""
values: list
text: str = field(default=None)
wrap_start: str = field(default=None, repr=False)
wrap_end: str = field(default=None, repr=False)
local_index: int = field(default=0, init=False)
evaluated_values: list = field(default=None)
def __post_init__(self):
super().__post_init__()
self.text = self.__collect_text()
self.update_local_options()
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:
for obj in self.values:
if isinstance(obj, Event):
if obj.local_options:
obj.local_options = (
obj.local_options | self.local_options.copy()
)
else:
obj.local_options = self.local_options.copy()
def __collect_text(self) -> str:
"""Collect text value from values"""
text = "".join([val.text for val in self.values])
if self.wrap_start is not None:
text = self.wrap_start + text
if self.wrap_end is not None:
text = text + self.wrap_end
return text
def evaluate_tree(self, options: dict = None, eval_tree: bool = False):
"""Evaluate the tree and return array of resolved pitches
Args:
options (dict, optional): Options for the pitches. Defaults to None.
eval_tree (bool, optional): Flag for using the evaluated subtree. Defaults to False.
"""
def _resolve_item(item: Meta, options: dict):
"""Resolve cyclic value"""
if isinstance(item, Sequence):
if isinstance(item, ListOperation):
yield from item.evaluate(options)
elif isinstance(item, RepeatedSequence):
item.evaluate_values(options)
repeats = item.repeats.get_value(options)
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)
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)
yield item
else:
yield from item.evaluate_tree(options)
elif isinstance(item, VariableAssignment):
if item.pre_eval:
pre_options = options.copy()
pre_options["pre_eval"] = True
options[item.variable.name] = Sequence(
values=list(_resolve_item(item.value, pre_options))
)
else:
options[item.variable.name] = item.value
elif isinstance(item, Variable):
if options[item.name]:
if item.name in options:
opt_item = options[item.name]
if isinstance(opt_item, LambdaType):
yield Function(run=opt_item, text=item.text, kwargs=options)
variable = deepcopy(opt_item)
yield from _resolve_item(variable, options)
elif isinstance(item, VariableList):
seqlist = []
for var in item.values:
if var.name in options:
opt_item = options[var.name]
if isinstance(opt_item, LambdaType):
seqlist.append(
Function(run=opt_item, text=var.text, kwargs=options)
)
elif isinstance(opt_item, Sequence):
seqlist.append(opt_item)
if len(seqlist) > 0:
yield PolyphonicSequence(values=seqlist)
elif isinstance(item, Range):
yield from item.evaluate(options)
elif isinstance(item, Cyclic):
yield from _resolve_item(item.get_value(), options)
elif isinstance(item, Euclid):
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)
def _resolve_repeat_value(item):
while isinstance(item, Cyclic):
item = item.get_value(options)
if isinstance(item, Pitch):
return item.get_value(options)
if isinstance(item, Integer):
return item.get_value(options)
return item
def _update_item(item, options):
"""Update or create new pitch"""
if set(("key", "scale")) <= options.keys():
if isinstance(item, Pitch):
item.update_options(options)
item.update_note()
if options.get("pre_eval", False):
item.duration = options["duration"]
if isinstance(item, Rest):
item.update_options(options)
elif isinstance(item, (RandomPitch, RandomInteger)):
item = _create_pitch(item, options)
elif isinstance(item, Chord):
item.update_options(options)
item.update_notes(options)
elif isinstance(item, RomanNumeral):
item = item.evaluate_chord(options)
return item
def _generative_repeat(tree: list, times: int, options: dict):
"""Repeats items and generates new random values"""
for _ in range(times):
for item in tree.evaluate_tree(options):
yield from _resolve_item(item, options)
def _normal_repeat(tree: list, times: int, options: dict):
"""Repeats items with the same random values"""
for _ in range(times):
for item in tree:
yield from _resolve_item(item, options)
def _euclidean_items(euclid: Item, options: dict):
"""Loops values from generated euclidean sequence"""
euclid.evaluate(options)
for item in euclid.evaluated_values:
yield from _resolve_item(item, options)
def _loop_items(items, options):
for item in items:
yield from _resolve_item(item, options)
def _parse_options(current: Item, options: dict) -> dict:
"""Update options based on current item"""
if isinstance(current, (OctaveChange, DurationChange)):
options[current.key] = current.value
elif isinstance(current, OctaveAdd):
if current.key in options: # Add to existing value
options[current.key] += current.value
else: # Create value if not existing
options[current.key] = current.value
return options
def _create_pitch(current: Item, options: dict) -> Pitch:
"""Create pitch based on values and options"""
merged_options = options | self.local_options
if "modifier" in merged_options:
c_modifier = merged_options["modifier"]
else:
c_modifier = 0
if hasattr(current, "modifier") and current.modifier is not None:
c_modifier += current.modifier
if "octave" in merged_options:
c_octave = merged_options["octave"]
if "octave" in options:
c_octave = options["octave"] + c_octave
else:
c_octave = 0
if hasattr(current, "octave") and current.octave is not None:
c_octave += current.octave
current_value = current.get_value(merged_options)
note = note_from_pc(
root=merged_options["key"],
pitch_class=current_value,
intervals=merged_options["scale"],
modifier=c_modifier,
octave=c_octave,
)
new_pitch = Pitch(
pitch_class=current_value,
text=str(current_value),
note=note,
freq=midi_to_freq(note),
octave=c_octave,
modifier=c_modifier,
kwargs=merged_options,
)
return new_pitch
# 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:
yield from _resolve_item(item, options)
def filter(self, keep: tuple):
"""Filter out items from sequence.
Args:
keep (tuple): Tuple describing classes to keep
Returns:
Sequence: Copy of the sequence with filtered values.
"""
return replace(
self, values=[item for item in self.values if isinstance(item, keep)]
)
@dataclass(kw_only=True)
class PolyphonicSequence:
values: list
@dataclass(kw_only=True)
class ListSequence(Sequence):
"""Class for Ziffers list sequences"""
wrap_start: str = field(default="(", repr=False)
wrap_end: str = field(default=")", repr=False)
@dataclass(kw_only=True)
class RepeatedListSequence(Sequence):
"""Class for Ziffers list sequences"""
repeats: RandomInteger | Integer = field(default_factory=Integer(value=1, text="1"))
wrap_start: str = field(default="(:", repr=False)
wrap_end: str = field(default=":)", repr=False)
@dataclass(kw_only=True)
class Subdivision(Sequence):
"""Class for subdivisions"""
full_duration: float = field(default=None, init=False)
def evaluate_values(self, options):
"""Evaluate values and store to evaluated_values"""
self.full_duration = options["duration"]
self.evaluated_values = list(self.evaluate_tree(options))
def evaluate_durations(self, duration=None):
"""Calculate new durations by dividing with the number of items in the sequence"""
if duration is None:
duration = self.full_duration
new_d = duration / len(self.evaluated_values)
for item in self.evaluated_values:
if isinstance(item, Subdivision):
yield from item.evaluate_durations(new_d)
if isinstance(item, Event):
if duration is not None:
item.duration = new_d
yield item
@dataclass(kw_only=True)
class ListOperation(Sequence):
"""Class for list operations"""
evaluated_values: list = None
def evaluate(self, options=DEFAULT_OPTIONS.copy()):
"""Evaluates the operation"""
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))
elif isinstance(item, Cyclic):
value = item.get_value()
if isinstance(value, Sequence):
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)):
item.update_options(options)
flattened_list.append(item)
if isinstance(input_list, Sequence):
return replace(input_list, values=flattened_list)
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):
right = _filter_operation(right, options)
elif isinstance(right, Cyclic):
right = right.get_value(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
if len(values) == 1:
return values[0] # If right hand doesnt contain anything sensible
left = values[0] # Start results with the first array
for i, operand in enumerate(operators):
operation = operand.value
right = values[i + 1]
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
@dataclass(kw_only=True)
class Eval(Sequence):
"""Class for evaluation notation"""
result: ... = None
wrap_start: str = field(default="{", repr=False)
wrap_end: str = field(default="}", repr=False)
def __post_init__(self):
super().__post_init__()
self.result = eval(self.text)
@dataclass(kw_only=True)
class RepeatedSequence(Sequence):
"""Class for repeats"""
repeats: RandomInteger | Integer = field(default_factory=Integer(value=1, text="1"))
wrap_start: str = field(default="[:", repr=False)
wrap_end: str = field(default=":]", repr=False)
evaluated_values: list = None
def evaluate_values(self, options):
"""Evaluate values and store to evaluated_values"""
self.evaluated_values = list(self.evaluate(options))
def evaluate(self, options: dict):
"""Evaluate repeated sequence partially. Leaves Cycles intact."""
self.local_options = options.copy()
for item in self.values:
if isinstance(item, Sequence):
if isinstance(item, ListOperation):
yield from item.evaluate_tree(self.local_options, True)
elif isinstance(item, RepeatedSequence):
yield item
elif isinstance(item, Subdivision):
item.evaluate_values(options)
yield item
else:
yield from item
elif isinstance(item, Cyclic):
yield item # Return the cycle instead of values
elif isinstance(item, Modification):
self.local_options = self.local_options | item.as_options()
elif isinstance(item, Rest):
yield item.get_updated_item(self.local_options)
elif isinstance(item, Range):
yield from item.evaluate(self.local_options)
elif isinstance(item, (Pitch, Chord, RomanNumeral)):
yield item
elif isinstance(item, (Event, RandomInteger)):
yield Pitch(
pitch_class=item.get_value(self.local_options),
kwargs=self.local_options,
)
@dataclass(kw_only=True)
class Euclid(Item):
"""Class for euclidean cycles"""
pulses: int
length: int
onset: ListSequence
offset: ListSequence = field(default=None)
rotate: int = field(default=0)
evaluated_values: list = field(default=None)
def evaluate(self, options):
"""Evaluate values using euclidean spread"""
onset_values = [
val for val in self.onset.values if not isinstance(val, Whitespace)
]
onset_length = len(onset_values)
booleans = euclidian_rhythm(self.pulses, self.length, self.rotate)
self.evaluated_values = []
if self.offset is not None:
offset_values = [
val for val in self.offset.values if not isinstance(val, Whitespace)
]
offset_length = len(offset_values)
on_i = 0
off_i = 0
for i in range(self.length):
if booleans[i]:
value = onset_values[on_i % onset_length]
on_i += 1
else:
if self.offset is None:
value = Rest(duration=options["duration"])
else:
value = offset_values[off_i % offset_length]
off_i += 1
self.evaluated_values.append(value)