Refactored roman numeral chords
This commit is contained in:
@ -157,4 +157,24 @@ def test_rest(pattern: str, expected: list):
|
||||
]
|
||||
)
|
||||
def test_ranges(pattern: str, expected: list):
|
||||
assert collect(zparse(pattern),len(expected)*2,"note") == expected*2
|
||||
assert collect(zparse(pattern),len(expected)*2,"note") == expected*2
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"pattern,expected",
|
||||
[
|
||||
("i ii iii iv v vi vii", [[60, 64, 67], [62, 65, 69], [64, 67, 71], [65, 69, 72], [67, 71, 74], [69, 72, 76], [71, 74, 77]])
|
||||
]
|
||||
)
|
||||
def test_romans(pattern: str, expected: list):
|
||||
assert collect(zparse(pattern),len(expected)*2,"note") == expected*2
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"pattern,expected",
|
||||
[
|
||||
("[: i vi v :]", [[0, 2, 4], [5, 0, 2], [4, 6, 1], [0, 2, 4], [5, 0, 2], [4, 6, 1]]),
|
||||
("i ii iii iv v vi vii", [[0, 2, 4], [1, 3, 5], [2, 4, 6], [3, 5, 0], [4, 6, 1], [5, 0, 2], [6, 1, 3]]),
|
||||
("i^7 i^min iv^6", [[0, 2, 4, 6], [0, 2, 4], [3, 5, 0, 1]])
|
||||
]
|
||||
)
|
||||
def test_romans_pcs(pattern: str, expected: list):
|
||||
assert collect(zparse(pattern),len(expected)*2,"pitches") == expected*2
|
||||
|
||||
@ -6,7 +6,14 @@ import operator
|
||||
import random
|
||||
from copy import deepcopy
|
||||
from .defaults import DEFAULT_OPTIONS
|
||||
from .scale import note_from_pc, midi_to_pitch_class, midi_to_freq, get_scale_length
|
||||
from .common import repeat_text
|
||||
from .scale import (
|
||||
note_from_pc,
|
||||
midi_to_pitch_class,
|
||||
midi_to_freq,
|
||||
get_scale_length,
|
||||
chord_from_degree,
|
||||
)
|
||||
from .common import euclidian_rhythm
|
||||
|
||||
|
||||
@ -39,7 +46,7 @@ class Meta:
|
||||
setattr(self, key, oct_change)
|
||||
elif local_value:
|
||||
setattr(self, key, value + local_value)
|
||||
else:
|
||||
elif getattr(self, key) is None:
|
||||
setattr(self, key, value)
|
||||
elif getattr(self, key) is None:
|
||||
local_value = self.local_options.get(key, False)
|
||||
@ -172,6 +179,13 @@ class Pitch(Event):
|
||||
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"""
|
||||
@ -295,6 +309,11 @@ class Chord(Event):
|
||||
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
|
||||
@ -336,7 +355,7 @@ class Chord(Event):
|
||||
|
||||
self.pitch_classes = new_pitches
|
||||
|
||||
def update_notes(self, options):
|
||||
def update_notes(self, options, force=False):
|
||||
"""Update notes"""
|
||||
pitches, notes, freqs, octaves, durations, beats = ([] for _ in range(6))
|
||||
|
||||
@ -371,9 +390,9 @@ class RomanNumeral(Event):
|
||||
|
||||
value: str = field(default=None)
|
||||
chord_type: str = field(default=None)
|
||||
notes: list[int] = field(default_factory=[])
|
||||
pitch_classes: list = None
|
||||
inversions: int = None
|
||||
notes: list[int] = field(default=None, init=False)
|
||||
pitch_classes: list = field(default=None, init=False)
|
||||
inversions: int = field(default=None)
|
||||
|
||||
def set_notes(self, chord_notes: list[int]):
|
||||
"""Set notes to roman numeral
|
||||
@ -623,30 +642,24 @@ class Sequence(Meta):
|
||||
scale = options["scale"]
|
||||
pitch_text = ""
|
||||
pitch_classes = []
|
||||
chord_notes = []
|
||||
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"],
|
||||
kwargs=(pitch_dict | options),
|
||||
note=note,
|
||||
kwargs=(options | pitch_dict),
|
||||
)
|
||||
)
|
||||
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,
|
||||
notes=current.notes,
|
||||
kwargs=options,
|
||||
inversions=current.inversions,
|
||||
)
|
||||
@ -776,16 +789,24 @@ class Ziffers(Sequence):
|
||||
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))
|
||||
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)]
|
||||
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)]
|
||||
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"""
|
||||
@ -806,7 +827,9 @@ class Ziffers(Sequence):
|
||||
def freqs(self) -> list[int]:
|
||||
"""Return list of octaves"""
|
||||
return [
|
||||
val.get_freq() for val in self.evaluated_values if isinstance(val, (Pitch, Chord))
|
||||
val.get_freq()
|
||||
for val in self.evaluated_values
|
||||
if isinstance(val, (Pitch, Chord))
|
||||
]
|
||||
|
||||
|
||||
@ -1110,6 +1133,8 @@ class RepeatedSequence(Sequence):
|
||||
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),
|
||||
|
||||
@ -10,6 +10,7 @@ def flatten(arr: list) -> list:
|
||||
else [arr]
|
||||
)
|
||||
|
||||
|
||||
def rotate(arr, k):
|
||||
"""Rotates array"""
|
||||
# Calculate the effective rotation amount (mod the array length)
|
||||
@ -22,6 +23,13 @@ def rotate(arr, k):
|
||||
arr = arr[-k:] + arr[:-k]
|
||||
return arr
|
||||
|
||||
def repeat_text(pos,neg,times):
|
||||
"""Helper to repeat text"""
|
||||
if times>0:
|
||||
return pos*times
|
||||
if times<0:
|
||||
return neg*abs(times)
|
||||
return ""
|
||||
|
||||
def sum_dict(arr: list[dict]) -> dict:
|
||||
"""Sums a list of dicts: [{a:3,b:3},{b:1}] -> {a:3,b:4}"""
|
||||
@ -67,7 +75,7 @@ def string_rewrite(axiom: str, rules: dict):
|
||||
return pattern.sub(lambda m: next(_apply_rules(m)), axiom)
|
||||
|
||||
|
||||
def euclidian_rhythm(pulses: int, length: int, rotate: int = 0):
|
||||
def euclidian_rhythm(pulses: int, length: int, rot: int = 0):
|
||||
"""Calculate Euclidean rhythms. Original algorithm by Thomas Morrill."""
|
||||
|
||||
def _starts_descent(arr, index):
|
||||
@ -84,5 +92,4 @@ def euclidian_rhythm(pulses: int, length: int, rotate: int = 0):
|
||||
res_list = [pulses * t % length for t in range(-1, length - 1)]
|
||||
bool_list = [_starts_descent(res_list, index) for index in range(length)]
|
||||
|
||||
return rotation(bool_list, rotate)
|
||||
|
||||
return rotation(bool_list, rot)
|
||||
|
||||
@ -1729,7 +1729,6 @@ def __build_chords():
|
||||
"a": aug,
|
||||
"aug": aug,
|
||||
"diminished": dim,
|
||||
"dim": dim,
|
||||
"i": dim,
|
||||
"diminished7": dim7,
|
||||
"dim7": dim7,
|
||||
|
||||
@ -29,11 +29,11 @@ from .classes import (
|
||||
RepeatedSequence,
|
||||
VariableAssignment,
|
||||
Variable,
|
||||
Measure
|
||||
Measure,
|
||||
)
|
||||
from .common import flatten, sum_dict
|
||||
from .defaults import DEFAULT_DURS, OPERATORS
|
||||
from .scale import parse_roman, chord_from_roman_numeral
|
||||
from .scale import parse_roman
|
||||
|
||||
|
||||
# pylint: disable=locally-disabled, unused-argument, too-many-public-methods, invalid-name
|
||||
@ -60,7 +60,7 @@ class ZiffersTransformer(Transformer):
|
||||
def measure(self, items):
|
||||
"""Return new measure"""
|
||||
return Measure()
|
||||
|
||||
|
||||
def random_integer(self, items) -> RandomInteger:
|
||||
"""Parses random integer syntax"""
|
||||
if len(items) > 1:
|
||||
@ -175,17 +175,15 @@ class ZiffersTransformer(Transformer):
|
||||
return Chord(pitch_classes=items, text="".join([val.text for val in items]))
|
||||
|
||||
def invert(self, items):
|
||||
"""Return chord inversion"""
|
||||
return items[0]
|
||||
|
||||
def named_roman(self, items) -> RomanNumeral:
|
||||
"""Parse chord from roman numeral"""
|
||||
numeral = items[0].value
|
||||
# TODO: Refactor this and the rule
|
||||
if len(items) == 1:
|
||||
return RomanNumeral(
|
||||
value=parse_roman(numeral),
|
||||
text=numeral,
|
||||
notes=chord_from_roman_numeral(numeral),
|
||||
)
|
||||
return RomanNumeral(value=parse_roman(numeral), text=numeral)
|
||||
if len(items) > 2:
|
||||
name = items[1]
|
||||
inversions = int(items[-1].value[1:])
|
||||
@ -193,7 +191,6 @@ class ZiffersTransformer(Transformer):
|
||||
text=numeral,
|
||||
value=parse_roman(numeral),
|
||||
chord_type=name,
|
||||
notes=chord_from_roman_numeral(numeral, name),
|
||||
inversions=inversions,
|
||||
)
|
||||
elif len(items) == 2:
|
||||
@ -202,15 +199,11 @@ class ZiffersTransformer(Transformer):
|
||||
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),
|
||||
value=parse_roman(numeral), text=numeral, chord_type=items[1]
|
||||
)
|
||||
|
||||
def chord_name(self, item):
|
||||
@ -219,7 +212,7 @@ class ZiffersTransformer(Transformer):
|
||||
|
||||
def roman_number(self, item):
|
||||
"""Return roman numeral"""
|
||||
return item.value
|
||||
return item[0]
|
||||
|
||||
def dur_change(self, items):
|
||||
"""Parses duration change"""
|
||||
|
||||
@ -3,8 +3,8 @@
|
||||
# pylint: disable=locally-disabled, no-name-in-module
|
||||
import re
|
||||
from math import floor
|
||||
from .common import repeat_text
|
||||
from .defaults import (
|
||||
DEFAULT_OCTAVE,
|
||||
SCALES,
|
||||
MODIFIERS,
|
||||
NOTES_TO_INTERVALS,
|
||||
@ -44,11 +44,13 @@ def note_name_to_interval(name: str) -> int:
|
||||
interval = NOTES_TO_INTERVALS[values[0].capitalize()]
|
||||
return interval + modifier
|
||||
|
||||
|
||||
def midi_to_freq(note: int) -> float:
|
||||
"""Transform midi to frequency"""
|
||||
freq = 440 # Frequency of A
|
||||
"""Transform midi to frequency"""
|
||||
freq = 440 # Frequency of A
|
||||
return (freq / 32) * (2 ** ((note - 9) / 12))
|
||||
|
||||
|
||||
def note_name_to_midi(name: str) -> int:
|
||||
"""Parse note name to midi
|
||||
|
||||
@ -80,6 +82,45 @@ def get_scale(name: str) -> list[int]:
|
||||
scale = SCALES.get(name.lower().capitalize(), SCALES["Ionian"])
|
||||
return scale
|
||||
|
||||
|
||||
def get_scale_notes(name: str, root: int = 60, num_octaves: int = 1) -> list[int]:
|
||||
"""Return notes for the scale
|
||||
|
||||
Args:
|
||||
name (str): Name of the scale
|
||||
root (int, optional): Root note. Defaults to 60.
|
||||
num_octaves (int, optional): Number of octaves. Defaults to 1.
|
||||
|
||||
Returns:
|
||||
list[int]: List of notes
|
||||
"""
|
||||
scale = get_scale(name)
|
||||
scale_notes = [root]
|
||||
for _ in range(num_octaves):
|
||||
scale_notes = scale_notes + [root := root + semitone for semitone in scale]
|
||||
return scale_notes
|
||||
|
||||
|
||||
def get_chord_from_scale(
|
||||
degree: int, root: int = 60, scale: str = "Major", num_notes: int = 3, skip: int = 2
|
||||
) -> list[int]:
|
||||
"""Generate chord from the scale by skipping notes
|
||||
|
||||
Args:
|
||||
degree (int): Degree of scale to start on
|
||||
root (int, optional): Root for the scale. Defaults to 60.
|
||||
scale (str, optional): Name of the scale. Defaults to "Major".
|
||||
num_notes (int, optional): Number of notes. Defaults to 3.
|
||||
skip (int, optional): Takes every n from the scale. Defaults to 2.
|
||||
|
||||
Returns:
|
||||
list[int]: List of midi notes
|
||||
"""
|
||||
num_of_octaves = ((num_notes * skip + degree) // get_scale_length(scale)) + 1
|
||||
scale_notes = get_scale_notes(scale, root, num_of_octaves)
|
||||
return scale_notes[degree - 1 :: skip][:num_notes]
|
||||
|
||||
|
||||
def get_scale_length(name: str) -> int:
|
||||
"""Get length of the scale
|
||||
|
||||
@ -92,6 +133,7 @@ def get_scale_length(name: str) -> int:
|
||||
scale = SCALES.get(name.lower().capitalize(), SCALES["Ionian"])
|
||||
return len(scale)
|
||||
|
||||
|
||||
# pylint: disable=locally-disabled, too-many-arguments
|
||||
def note_from_pc(
|
||||
root: int | str,
|
||||
@ -242,18 +284,33 @@ def midi_to_pitch_class(note: int, key: str | int, scale: str) -> dict:
|
||||
npc = sharps[pitch_class]
|
||||
|
||||
if len(npc) > 1:
|
||||
modifier = 1 if (npc[0] == "#") else -1
|
||||
return {
|
||||
"text": npc,
|
||||
"text": repeat_text("^", "_", octave)+npc,
|
||||
"pitch_class": int(npc[1]),
|
||||
"octave": octave,
|
||||
"modifier": 1 if (npc[0] == "#") else -1,
|
||||
"modifier": modifier,
|
||||
}
|
||||
|
||||
return {"text": npc, "pitch_class": int(npc), "octave": octave}
|
||||
return {
|
||||
"text": repeat_text("^", "_", octave)+npc,
|
||||
"pitch_class": int(npc),
|
||||
"octave": octave,
|
||||
}
|
||||
|
||||
|
||||
def chord_from_roman_numeral(
|
||||
roman: str, name: str = "major", num_octaves: int = 1
|
||||
def chord_from_degree(
|
||||
degree: int, name: str, scale: str, root: str | int, num_octaves: int = 1
|
||||
):
|
||||
root = note_name_to_midi(root) if isinstance(root, str) else root
|
||||
if name:
|
||||
return named_chord_from_degree(degree, name, root, scale, num_octaves)
|
||||
else:
|
||||
return get_chord_from_scale(degree, root, scale)
|
||||
|
||||
|
||||
def named_chord_from_degree(
|
||||
degree: int, name: str = "major", root: int = 60, scale: str="Major", num_octaves: int = 1
|
||||
) -> list[int]:
|
||||
"""Generates chord from given roman numeral and chord name
|
||||
|
||||
@ -265,11 +322,10 @@ def chord_from_roman_numeral(
|
||||
Returns:
|
||||
list[int]: _description_
|
||||
"""
|
||||
root = parse_roman(roman) - 1
|
||||
tonic = (DEFAULT_OCTAVE * 12) + root + 12
|
||||
intervals = CHORDS.get(name, CHORDS["major"])
|
||||
scale_degree = get_scale_notes(scale, root)[degree-1]
|
||||
notes = []
|
||||
for cur_oct in range(num_octaves):
|
||||
for iterval in intervals:
|
||||
notes.append(tonic + iterval + (cur_oct * 12))
|
||||
for interval in intervals:
|
||||
notes.append(scale_degree + interval + (cur_oct * 12))
|
||||
return notes
|
||||
|
||||
@ -31,7 +31,7 @@
|
||||
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}/
|
||||
!roman_number: "i" | "ii" | "iii" | "iv" | "v" | "vi" | "vii"
|
||||
|
||||
invert: /%-?[0-9][0-9]*/
|
||||
|
||||
|
||||
Reference in New Issue
Block a user