def test_scale_get_chord_with_alteration(start_degree_name, alteration, chord_basic, chord_seventh): scale = Scale("A", "major") assert scale.get_chord(start_degree_name, "1,3,5", alteration=alteration) == chord_basic assert scale.get_chord(start_degree_name, "1,3,5,7", alteration=alteration) == chord_seventh
def test_scale_notation_on_initialization(name, alt_name): root_note = "A" notation1 = Scale(root_note, name) notation2 = Scale(root_note, alt_name) assert notation1 == notation2 assert repr(notation1) == repr(notation2)
def test_grid_part_repr(): grid_part = GridPart( Scale("C", "ionian"), Chord("C", "maj7"), Whole, TimeSignature(4, 4), Tempo(60) ) assert repr(grid_part) == "<GridPart : C ionian / Cmaj7 / Whole / 4/4 / 60>"
def _get(self, degree, inversion=None, base_note=None, base_degree=None, default_degrees=None, seventh=True, strict=True, extensions=None): if not default_degrees: default_degrees = self._DEFAULT_DEGREES base_degree_interval = None note, alteration, index, chord_name = self._parse_degree( degree, seventh=seventh, strict=strict) if base_degree: base_degree_interval = self.get_base_degree_interval(base_degree) note += base_degree_interval if chord_name: chord = Chord(note, chord_name, inversion=inversion, base_note=base_note, extensions=extensions) else: scale = self.scale if base_degree_interval: scale = Scale(scale.tonic + base_degree_interval, str(scale.name)) chord = scale.get_chord(index, default_degrees, alteration=alteration, inversion=inversion, base_note=base_note, extensions=extensions) return chord
def test_create_grid_with_parts(): parts = [ {"scale": Scale("A", "major"), "chord": Chord("A", "maj")}, {"chord": Chord("C", "min"), "tempo": Tempo(120)}, {"chord": Chord("E", "maj"), "duration": Half}, {"chord": Chord("G#", "dim"), "time_signature": TimeSignature(3, 2)} ] time_signature = TimeSignature(4, 4) tempo = Tempo(90) grid = Grid( time_signature=time_signature, tempo=tempo, duration=Whole ) grid.set_parts(*parts) assert repr(grid) == "<Grid : 4 parts>" assert len(grid.parts) == 4 assert grid.parts[0].tempo == tempo assert grid.parts[0].time_signature == time_signature
def test_scale_instanciation_without_attributes(): with raises(ValueError, match="Scale name and tonic note must be set"): Scale()
def test_scale(root_note, scale_name, result_notes): scale = Scale(root_note, scale_name) assert scale.notes == Note.to_list(result_notes)
root_note = "A" notation1 = Scale(root_note, name) notation2 = Scale(root_note, alt_name) assert notation1 == notation2 assert repr(notation1) == repr(notation2) def test_scale_instanciation_through_to_dict(): scale = Scale("A", "major") assert scale == Scale(**scale.to_dict()) @mark.parametrize("base_scale,mode_switch,target_scale", [ (Scale("A", "major"), 2, Scale("A", "phrygian")), (Scale("A", "major"), 4, Scale("A", "mixolydian")), (Scale("A", "major"), 6, Scale("A", "locrian")), (Scale("A", "melodic minor"), 3, Scale("A", "lydian dominant")), (Scale("A", "harmonic minor"), 6, Scale("A", "altered diminished")), (Scale("A", "double harmonic minor"), 1, Scale("A", "lydian #2 #6")), ]) def test_scale_switch_mode(base_scale, mode_switch, target_scale): assert base_scale.switch_mode(mode_switch) == target_scale def test_scale_raise_on_switch_mode(): with raises(ValueError, match="Scale doesnt have modes"): Scale("A", "pentatonic").switch_mode(2)
def validate_chord_item(string, loc, expr): if not (Chord.get_from_fullname(expr[0]) or Harmony(Scale("A", "major")).get(expr[0])): raise Exception("Couldn't parse chord item")
def validate_note_or_degree(string, loc, expr): try: Note(expr[0]) except Exception: Harmony(Scale("A", "major")).get(expr[0])
def test_harmony_parse_degree_non_strictly(degree_name, expected_chord): harmony = Harmony(Scale("A", "major")) assert harmony.get(degree_name, strict=False) == Chord.get_from_fullname(expected_chord)
def test_harmony_instanciation_through_to_dict(): harmony = Harmony(Scale("A", "major")) assert harmony == Harmony(**harmony.to_dict())
def test_scale_instanciation_through_to_dict(): scale = Scale("A", "major") assert scale == Scale(**scale.to_dict())
def test_harmony_instanciation_on_non_diatonic_scale(scale_name): with raises(ValueError, match="Scale given to harmony must be diatonic"): Harmony(Scale("A", scale_name))
scale_name = None tonic_note = None if current_scale: scale_name = str(current_scale.name) tonic_note = current_scale.tonic if scale_name_value := parsed_config.get("scale"): scale_name = scale_name_value scale_updated = True if tonic_name := parsed_config.get("note"): tonic_note = Note(tonic_name) scale_updated = True if scale_updated and scale_name and tonic_note: parsed["scale"] = Scale(tonic_note, scale_name) elif not current_scale: return parsed if progression := parsed_config.get("progression"): parsed["progression"] = progression if time_signature := parsed_config.get("time_signature"): parsed["time_signature"] = TimeSignature(*time_signature) if tempo := parsed_config.get("tempo"): parsed["tempo"] = Tempo(tempo) return parsed
from beethoven.theory.note import Note from beethoven.theory.scale import Scale @mark.parametrize( "tuning,fretboard_kwargs,call_kwargs,expected_ascii", [ # Test without any scale/chord (Tuning(Note("A")), {}, {}, (" ║ | | | | | | | | | | | |\n" "\n" " 0 ║ | | 3 | | 5 | | 7 | | 9 | | | 12 |" )), # Test with a scale (Tuning(Note("A")), {}, { "scale": Scale("A", "major") }, (" A ║ | B | | C# | D | | E | | F# | | G# | A |\n" "\n" " 0 ║ | | 3 | | 5 | | 7 | | 9 | | | 12 |" )), # Test with a chord (Tuning(Note("A")), {}, { "chord": Chord("A", "maj7") }, (" A ║ | | | C# | | | E | | | | G# | A |\n" "\n" " 0 ║ | | 3 | | 5 | | 7 | | 9 | | | 12 |" )), # Test shifting first and last frets on initialization (Tuning(Note("A")), {
def test_harmony_degrees_and_noteslmao(): harmony = Harmony(Scale("A", "ionian")) assert harmony.get("i", seventh=False) == Chord.get_from_fullname("Amin") assert harmony.get("II", seventh=False) == Chord.get_from_fullname("Bmaj")
def test_harmony_degrees_and_notes(scale_name, expected_degrees): harmony = Harmony(Scale("A", scale_name)) assert harmony.degrees == expected_degrees.split(",")
def test_scale_with_wrong_scale_name(scale_name): with raises(ValueError, match="Scale name does not exists"): Scale("A", scale_name)
class Harmony(metaclass=HarmonySingletonMeta): _DIRECTORY = {} _MAJOR_INTERVALS = { i: interval for i, interval in enumerate(Scale("A", "major").intervals, start=1) } _DEGREES = ("I", "II", "III", "IV", "V", "VI", "VII") _DEFAULT_DEGREES = "1,3,5,7" def __init__(self, scale): self._load_attributes(scale) def __repr__(self): return f"<Harmony {str(self)}>" def __str__(self): return str(self.scale) def _load_attributes(self, scale): if not len(scale.intervals) == 7: raise ValueError("Scale given to harmony must be diatonic") self.scale = scale self.degrees = [] default_degrees = [1, 3, 5, 7] scale_notes = self.scale.notes for i, degree_name in enumerate(self._DEGREES): # TODO refactor with intervals for scale object instead of notes notes = list( map(lambda x: scale_notes[(x + i - 1) % 7], default_degrees)) intervals = [notes[0] // note for note in notes] chord_name = Chord.get_chord_name_from_intervals(intervals).short if "min" in chord_name or "dim" in chord_name: degree_name = degree_name.lower() self.degrees.append(degree_name) def get(self, *args, **kwargs): try: return self._get(*args, **kwargs) except ValueError: return def get_base_degree_interval(self, degree): note = self._parse_degree(degree)[0] return self.scale.notes[0] // note def _get(self, degree, inversion=None, base_note=None, base_degree=None, default_degrees=None, seventh=True, strict=True, extensions=None): if not default_degrees: default_degrees = self._DEFAULT_DEGREES base_degree_interval = None note, alteration, index, chord_name = self._parse_degree( degree, seventh=seventh, strict=strict) if base_degree: base_degree_interval = self.get_base_degree_interval(base_degree) note += base_degree_interval if chord_name: chord = Chord(note, chord_name, inversion=inversion, base_note=base_note, extensions=extensions) else: scale = self.scale if base_degree_interval: scale = Scale(scale.tonic + base_degree_interval, str(scale.name)) chord = scale.get_chord(index, default_degrees, alteration=alteration, inversion=inversion, base_note=base_note, extensions=extensions) return chord def _parse_degree(self, degree, seventh=True, strict=True): matched = HARMONY_PARSER.match(degree) if not matched: raise ValueError("Degree could not be parsed") parsed = matched.groupdict() final_alteration = 0 degree_name = parsed.get("degree_name") alteration = parsed.get("alteration") chord_name = parsed.get("chord_name") index = self._DEGREES.index(degree_name.upper()) note = self.scale.notes[index] if degree_name not in self.degrees and not chord_name: if strict and degree_name.isupper(): chord_name = "maj" elif degree_name.islower(): chord_name = "min" if chord_name and seventh: chord_name += "7" flats = alteration.count("b") sharps = alteration.count("#") if flats: final_alteration -= flats for _ in range(flats): note += DIMINISHED elif sharps: final_alteration += sharps for _ in range(sharps): note += AUGMENTED return note, final_alteration, index, chord_name def to_dict(self): return {"scale": self.scale}
def test_scale_get_chord(start_degree_name, chord_basic, chord_seventh): scale = Scale("A", "major") assert scale.get_chord(start_degree_name, "1,3,5") == chord_basic assert scale.get_chord(start_degree_name, "1,3,5,7") == chord_seventh
def test_harmony_instanciation_on_diatonic_scale(scale_name): Harmony(Scale("A", scale_name))
from beethoven.sequencer.chord import Chord from beethoven.sequencer.note import Note from beethoven.sequencer.utils import adapt_chord_to_sequencer, get_all_notes from beethoven.theory.scale import Scale @mark.parametrize("chord,note_range,expected", [ (Chord("C1", "maj"), "C3,C6", "C3,E3,G3"), (Chord("C2", "maj"), "C3,C6", "C3,E3,G3"), (Chord("C3", "maj"), "C4,C6", "C4,E4,G4"), (Chord("C4", "maj"), "C4,C6", "C4,E4,G4"), (Chord("C5", "maj"), "C4,C6", "C5,E5,G5"), (Chord("C6", "maj"), "C5,F6", "C6,E6"), (Chord("A4", "maj7"), "C4,C6", "A4,C#5,E5,G#5"), (Chord("A4", "maj7"), "C4,E5", "A4,C#5,E5"), ]) def test_adapt_chord_to_sequencer(chord, note_range, expected): assert (adapt_chord_to_sequencer( chord, Note.to_list(note_range)) == Note.to_list(expected)) @mark.parametrize("scale,note_range,expected", [ (Scale("C3", "major"), "C3,C4", "C3,D3,E3,F3,G3,A3,B3,C4"), (Scale("C3", "major"), "E3,E4", "E3,F3,G3,A3,B3,C4,D4,E4"), (Scale("C3", "major"), "C3,C5", "C3,D3,E3,F3,G3,A3,B3,C4,D4,E4,F4,G4,A4,B4,C5"), ]) def test_get_all_notes(scale, note_range, expected): assert (get_all_notes(scale, Note.to_list(note_range)) == Note.to_list(expected))
def test_harmony_repr(): assert repr(Harmony(Scale("A", "ionian"))) == "<Harmony A ionian>"
def test_scale_raise_on_switch_mode(): with raises(ValueError, match="Scale doesnt have modes"): Scale("A", "pentatonic").switch_mode(2)
from os import environ from beethoven.common.tuning import Tuning from beethoven.models import GridPart from beethoven.players.drum import Drum from beethoven.players.piano import Piano from beethoven.sequencer.jam_room import JamRoom from beethoven.sequencer.note_duration import Whole from beethoven.sequencer.tempo import Tempo from beethoven.sequencer.time_signature import TimeSignature from beethoven.theory.scale import Scale DEFAULT_CONFIG = { "scale": Scale("C", "major"), "duration": Whole, "tempo": Tempo(60), "time_signature": TimeSignature(4, 4) } DEFAULT_PROMPT_CONFIG = {"strict": True} class State: def __init__(self, prompt_config=None): self.jam_room = JamRoom() self.grid_parts = {gp.name: gp for gp in GridPart.list()} self.jam_room.players.add(Piano()) self.jam_room.players.add(Drum())
def validate_scale(string, loc, expr): Scale("A", expr[0])
from pytest import mark from beethoven.prompt.parser import prompt_harmony_list_parser from beethoven.sequencer.note_duration import Half from beethoven.sequencer.tempo import Tempo from beethoven.sequencer.time_signature import TimeSignature from beethoven.theory.chord import Chord from beethoven.theory.scale import Scale @mark.parametrize( "input_string,expected", [("", []), ("n=A sc=major p=", []), ("n=A p=I", []), ("n=A sc=major p=I,i", [{ "chord": Chord("A", "maj7"), "scale": Scale("A", "ionian"), }, { "chord": Chord("A", "min7") }]), ("n=A sc=major p=Amin7,Cmin7", [{ "chord": Chord("A", "min7"), "scale": Scale("A", "ionian"), }, { "chord": Chord("C", "min7") }]), ("n=A sc=major p=I;n=B sc=minor p=I", [{ "chord": Chord("A", "maj7"), "scale": Scale("A", "ionian"), }, { "chord": Chord("B", "maj7"), "scale": Scale("B", "aeolian")