diff --git a/ziffers/classes.py b/ziffers/classes.py index 31c0d0e..595e5ba 100644 --- a/ziffers/classes.py +++ b/ziffers/classes.py @@ -262,43 +262,71 @@ class Chord(Event): 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) + def set_notes(self, notes: list[int]): """Set notes to the class""" self.notes = notes + 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): """Update notes""" - notes = [] - freqs = [] - octaves = [] - durations = [] - beats = [] + pitches, notes, freqs, octaves, durations, beats = ([] for _ in range(6)) + # Update notes for pitch in self.pitch_classes: pitch.update_options(options) pitch.update_note() + + # 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.duration = durations self.beats = beats - def get_notes(self) -> int: + 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) -> int: + def get_octaves(self) -> list: """Return octave""" return self.octaves @@ -314,6 +342,7 @@ class Chord(Event): """Return frequencies""" return self.freqs + @dataclass(kw_only=True) class RomanNumeral(Event): """Class for roman numbers""" @@ -322,6 +351,7 @@ class RomanNumeral(Event): chord_type: str = field(default=None) notes: list[int] = field(default_factory=[]) pitch_classes: list = None + inversions: int = None def set_notes(self, chord_notes: list[int]): """Set notes to roman numeral @@ -485,17 +515,15 @@ class Sequence(Meta): item = _create_chord_from_roman(item, options) return item - # pylint: disable=locally-disabled, unused-variable def _generative_repeat(tree: list, times: int, options: dict): """Repeats items and generates new random values""" - for i in range(times): + for _ in range(times): for item in tree.evaluate_tree(options): yield from _resolve_item(item, options) - # pylint: disable=locally-disabled, unused-variable def _normal_repeat(tree: list, times: int, options: dict): """Repeats items with the same random values""" - for i in range(times): + for _ in range(times): for item in tree: yield from _resolve_item(item, options) @@ -597,7 +625,11 @@ class Sequence(Meta): pitch_classes=pitch_classes, notes=chord_notes, kwargs=options, + inversions=current.inversions, ) + + chord.update_notes(options) + return chord # Start of the main function: Evaluate and flatten the Ziffers object tree diff --git a/ziffers/common.py b/ziffers/common.py index 3aa9dfc..b5d3578 100644 --- a/ziffers/common.py +++ b/ziffers/common.py @@ -10,6 +10,18 @@ def flatten(arr: list) -> list: else [arr] ) +def rotate(arr, k): + """Rotates array""" + # Calculate the effective rotation amount (mod the array length) + k = k % len(arr) + # Rotate the array to the right + if k > 0: + arr = arr[-k:] + arr[:-k] + # Rotate the array to the left + elif k < 0: + arr = arr[-k:] + arr[:-k] + return arr + def sum_dict(arr: list[dict]) -> dict: """Sums a list of dicts: [{a:3,b:3},{b:1}] -> {a:3,b:4}""" @@ -73,3 +85,4 @@ def euclidian_rhythm(pulses: int, length: int, rotate: int = 0): bool_list = [_starts_descent(res_list, index) for index in range(length)] return rotation(bool_list, rotate) + diff --git a/ziffers/defaults.py b/ziffers/defaults.py index 82b7327..e9ca580 100644 --- a/ziffers/defaults.py +++ b/ziffers/defaults.py @@ -1648,6 +1648,9 @@ def __build_chords(): dim = [0, 3, 6] dim7 = [0, 3, 6, 9] halfdim = [0, 3, 6, 10] + aug7 = [0, 4, 8, 10] + aug9 = [0, 4, 10, 14] + six = [0, 4, 7, 9] all_chords = { "1": [0], "5": [0, 7], @@ -1655,12 +1658,12 @@ def __build_chords(): "m+5": [0, 3, 8], "sus2": [0, 2, 7], "sus4": [0, 5, 7], - "6": [0, 4, 7, 9], + "6": six, "m6": [0, 3, 7, 9], "7sus2": [0, 2, 7, 10], "7sus4": [0, 5, 7, 10], "7-5": [0, 4, 6, 10], - "7+5": [0, 4, 8, 10], + "7+5": aug7, "m7+5": [0, 3, 8, 10], "9": [0, 4, 7, 10, 14], "m9": [0, 3, 7, 10, 14], @@ -1695,6 +1698,21 @@ def __build_chords(): "madd9": [0, 3, 7, 14], "madd11": [0, 3, 7, 17], "madd13": [0, 3, 7, 21], + "dim9": [0, 3, 6, 9, 14], + "hdim7": halfdim, + "hdim9": [0, 3, 6, 10, 14], + "hdimb9": [0, 3, 6, 10, 13], + "augMaj7": [0, 4, 8, 11], + "minmaj7": [0, 3, 7, 11], + "five": [0, 7, 12], + "seven": dom7, + "nine": aug9, + "b9": [0, 4, 10, 13], + "mM9": [0, 3, 11, 14], + "min7": minor7, + "min9": [0, 3, 10, 14], + "b5": [0, 4, 6, 12], + "mb5": [0, 3, 6, 12], "major": major, "maj": major, "M": major, @@ -1705,7 +1723,6 @@ def __build_chords(): "dom7": dom7, "7": dom7, "M7": major7, - "minor7": minor7, "m7": minor7, "augmented": aug, "a": aug, diff --git a/ziffers/mapper.py b/ziffers/mapper.py index f3ec2c5..b05242a 100644 --- a/ziffers/mapper.py +++ b/ziffers/mapper.py @@ -1,5 +1,5 @@ """ Lark transformer for mapping Lark tokens to Ziffers objects """ -from lark import Transformer +from lark import Transformer, Token from .classes import ( Ziffers, Whitespace, @@ -161,23 +161,52 @@ class ZiffersTransformer(Transformer): def chord(self, items): """Parses chord""" + if isinstance(items[-1], Token): + return Chord( + pitch_classes=items[0:-1], + text="".join([val.text for val in items[0:-1]]), + inversions=int(items[-1].value[1:]), + ) return Chord(pitch_classes=items, text="".join([val.text for val in items])) + def invert(self, items): + return items[0] + def named_roman(self, items) -> RomanNumeral: """Parse chord from roman numeral""" numeral = items[0].value - if len(items) > 1: - name = items[1] - chord_notes = chord_from_roman_numeral(numeral, name) - parsed_number = parse_roman(numeral) + if len(items) == 1: return RomanNumeral( - text=numeral, value=parsed_number, chord_type=name, notes=chord_notes + value=parse_roman(numeral), + text=numeral, + notes=chord_from_roman_numeral(numeral), ) - return RomanNumeral( - value=parse_roman(numeral), - text=numeral, - notes=chord_from_roman_numeral(numeral), - ) + if len(items) > 2: + name = items[1] + inversions = int(items[-1].value[1:]) + return RomanNumeral( + text=numeral, + value=parse_roman(numeral), + chord_type=name, + notes=chord_from_roman_numeral(numeral, name), + inversions=inversions, + ) + elif len(items) == 2: + if isinstance(items[-1], Token): + inversions = int(items[-1].value[1:]) + return RomanNumeral( + value=parse_roman(numeral), + text=numeral, + notes=chord_from_roman_numeral(numeral), + inversions=inversions, + ) + else: + return RomanNumeral( + value=parse_roman(numeral), + text=numeral, + chord_type=items[1], + notes=chord_from_roman_numeral(numeral), + ) def chord_name(self, item): """Return name for chord""" diff --git a/ziffers/spec/ziffers.lark b/ziffers/spec/ziffers.lark index 9dbc3e8..8715bfe 100644 --- a/ziffers/spec/ziffers.lark +++ b/ziffers/spec/ziffers.lark @@ -26,10 +26,12 @@ rest: prefix* "r" // Chords - chord: pitch_class pitch_class+ - named_roman: roman_number (("^" chord_name))? // TODO: Add | ("+" number) + chord: pitch_class pitch_class+ invert? + named_roman: roman_number (("^" chord_name))? invert? // TODO: Add | ("+" number) chord_name: /[a-zA-Z0-9]+/ ?roman_number: /iv|v|v?i{1,3}/ + + invert: /%-?[0-9][0-9]*/ // Valid as integer number: NUMBER | random_integer | cycle