init
This commit is contained in:
0
__init__.py
Normal file
0
__init__.py
Normal file
BIN
__pycache__/major.cpython-310.pyc
Normal file
BIN
__pycache__/major.cpython-310.pyc
Normal file
Binary file not shown.
51
app.py
Normal file
51
app.py
Normal file
@@ -0,0 +1,51 @@
|
|||||||
|
import numpy as np
|
||||||
|
import gradio as gr
|
||||||
|
from major import MajorScale,chromatic,chromatic_notes_set,Note
|
||||||
|
|
||||||
|
# sample rate, default is 48000
|
||||||
|
sr = 48000
|
||||||
|
|
||||||
|
def generate_tone(note, octave, duration):
|
||||||
|
note_in_scale = Note(chromatic[note][0], octave)
|
||||||
|
frequency = note_in_scale.get_frequency()
|
||||||
|
# debug print
|
||||||
|
# print(note_in_scale,frequency)
|
||||||
|
duration = int(duration)
|
||||||
|
audio = np.linspace(0, duration, duration * sr)
|
||||||
|
audio = (20000 * np.sin(audio * (2 * np.pi * frequency))).astype(np.int16)
|
||||||
|
return sr, audio
|
||||||
|
|
||||||
|
def generate_rhythm(rhythm, duration_per_note):
|
||||||
|
rhythms = rhythm.split(",")
|
||||||
|
audio = np.zeros(int(duration_per_note * len(rhythms) * sr))
|
||||||
|
for i, rhythm_note in enumerate(rhythms):
|
||||||
|
if rhythm != "x":
|
||||||
|
if rhythm_note[:-1] not in chromatic_notes_set:
|
||||||
|
raise ValueError(f"Invalid note: {rhythm_note}")
|
||||||
|
note = Note(rhythm_note[:-1], int(rhythm_note[-1]))
|
||||||
|
# debug print
|
||||||
|
print(note, note.get_frequency(), note.code)
|
||||||
|
segment_audio = np.linspace(0, duration_per_note, duration_per_note * sr)
|
||||||
|
segment_audio = (20000 * np.sin(segment_audio * (2 * np.pi * note.get_frequency()))).astype(np.int16)
|
||||||
|
audio[int(i * duration_per_note * sr):int((i + 1) * duration_per_note * sr)] = segment_audio
|
||||||
|
else:
|
||||||
|
audio[int(i * duration_per_note * sr):int((i + 1) * duration_per_note * sr)] = 0
|
||||||
|
return sr, audio.astype(np.int16)
|
||||||
|
|
||||||
|
demo = gr.Interface(
|
||||||
|
# generate_tone,
|
||||||
|
# [
|
||||||
|
# gr.Dropdown(chromatic, type="index"),
|
||||||
|
# gr.Slider(0, 10, step=1),
|
||||||
|
# gr.Textbox(value="1", label="Duration in seconds"),
|
||||||
|
# ],
|
||||||
|
# "audio",
|
||||||
|
generate_rhythm,
|
||||||
|
[
|
||||||
|
gr.Textbox(value="C4,D4,E4,F4,G4,A4,B4,C5", label="Rhythm"),
|
||||||
|
gr.Slider(0, 10, value=1, step=1, label="Duration per note"),
|
||||||
|
],
|
||||||
|
"audio",
|
||||||
|
)
|
||||||
|
if __name__ == "__main__":
|
||||||
|
demo.launch()
|
||||||
262
chord.py
Normal file
262
chord.py
Normal file
@@ -0,0 +1,262 @@
|
|||||||
|
from major import MajorScale,Note
|
||||||
|
|
||||||
|
intervals_triad = {
|
||||||
|
(4,3): "major",
|
||||||
|
(3,4): "minor",
|
||||||
|
(3,3): "diminished",
|
||||||
|
(4,4): "augmented"
|
||||||
|
}
|
||||||
|
|
||||||
|
triad_intervals = {
|
||||||
|
"major": [4,3],
|
||||||
|
"minor": [3,4],
|
||||||
|
"diminished": [3,3],
|
||||||
|
"augmented": [4,4]
|
||||||
|
}
|
||||||
|
|
||||||
|
seven_chord_intervals = {
|
||||||
|
"dominant": [4,3,3],
|
||||||
|
"diminished": [3,4,3],
|
||||||
|
"half-diminished": [3,3,4],
|
||||||
|
"augmented major": [4,4,3],
|
||||||
|
"minor-major": [3,4,4],
|
||||||
|
"major": [4,3,4],
|
||||||
|
"full-diminished": [3,3,3]
|
||||||
|
}
|
||||||
|
|
||||||
|
intervals_seven_chord = {
|
||||||
|
(4,3,3): ("major-minor 7","dominant 7","7"),
|
||||||
|
(3,4,3): ("minor-minor 7","diminished 7","m7"),
|
||||||
|
(3,3,4): ("diminished-minor 7","half-diminished 7","m7b5"),
|
||||||
|
(4,4,3): ("augmented major 7","augmented major 7","M7#5"),
|
||||||
|
(3,4,4): ("minor-major 7","minor-major 7","mM7"),
|
||||||
|
(4,3,4): ("major-major 7","major 7","M7"),
|
||||||
|
(3,3,3): ("diminished-diminished 7","full-diminished 7","dim7"),
|
||||||
|
}
|
||||||
|
|
||||||
|
Roman_numerals = ["I","II","III","IV","V","VI","VII"]
|
||||||
|
|
||||||
|
class Chord:
|
||||||
|
def __init__(self, root:Note, keys:list[Note]=None, zero_indexed:bool=False):
|
||||||
|
"""
|
||||||
|
Build a chord object.
|
||||||
|
Args:
|
||||||
|
root: Note, the root note of the chord
|
||||||
|
keys: list[str], the notes of the chord, default is None, if want to build with initial triad, use build_triad method
|
||||||
|
zero_indexed: bool, if the keys are zero indexed, default is False
|
||||||
|
"""
|
||||||
|
self.root = root
|
||||||
|
self.major = MajorScale(root)
|
||||||
|
self.keys = keys
|
||||||
|
self.zero_indexed = zero_indexed
|
||||||
|
|
||||||
|
def get_intervals(self) -> list[int]:
|
||||||
|
"""
|
||||||
|
Get the intervals of the chord.
|
||||||
|
Returns:
|
||||||
|
list[int], the quality of the chord, empty list if not built yet
|
||||||
|
"""
|
||||||
|
if self.keys is None:
|
||||||
|
return []
|
||||||
|
difference = [self.keys[i].code - self.keys[i-1].code for i in range(1, len(self.keys))]
|
||||||
|
return difference
|
||||||
|
|
||||||
|
def __str__(self) -> str:
|
||||||
|
return f"{self.get_quality()} chord under major scale {self.root.get_name()}"
|
||||||
|
|
||||||
|
class Triad(Chord):
|
||||||
|
def __init__(self, root:Note, keys:list[Note]=None, zero_indexed:bool=False):
|
||||||
|
if len(keys) != 3:
|
||||||
|
raise ValueError("Triad must have 3 keys")
|
||||||
|
super().__init__(root, keys, zero_indexed)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def build_triad(self, root:Note, starting_index:int=1, quality:str=None) -> 'Triad':
|
||||||
|
"""
|
||||||
|
Build a triad chord object.
|
||||||
|
Args:
|
||||||
|
root: Note, the root note of the chord
|
||||||
|
starting_index: int, the starting index of the chord, default is 1
|
||||||
|
quality: str, the quality of the chord, default is None
|
||||||
|
"""
|
||||||
|
keys = self.get_triad(starting_index, quality)
|
||||||
|
return Triad(root, keys, self.zero_indexed)
|
||||||
|
|
||||||
|
def get_triad(self, starting_index:int, quality:str=None) -> list[Note]:
|
||||||
|
"""
|
||||||
|
Get a triad chord from the major scale.
|
||||||
|
Args:
|
||||||
|
starting_index: int, the starting index of the chord, default is 1, assume is positive indexed
|
||||||
|
quality: str, the quality of the chord ["major", "minor", "diminished", "augmented"], default is None
|
||||||
|
Returns:
|
||||||
|
list[Note], the triad chord
|
||||||
|
"""
|
||||||
|
if not self.zero_indexed:
|
||||||
|
starting_index = starting_index - 1
|
||||||
|
triad = [self.major.root + starting_index]
|
||||||
|
major_notes=self.major.get_major_scale(starting_index+6)
|
||||||
|
if quality is None:
|
||||||
|
# major triad from selected note
|
||||||
|
for i in range(starting_index, starting_index + 5,2):
|
||||||
|
triad.append(major_notes[i])
|
||||||
|
else:
|
||||||
|
# major triad from quality, note may not be in major scale
|
||||||
|
if quality not in triad_intervals:
|
||||||
|
raise ValueError(f"Invalid quality: {quality}")
|
||||||
|
current_note_code = major_notes[starting_index].code
|
||||||
|
for interval in triad_intervals[quality]:
|
||||||
|
current_note_code += interval
|
||||||
|
triad.append(Note.from_int(current_note_code, self.root.is_sharp))
|
||||||
|
return triad
|
||||||
|
|
||||||
|
def get_quality(self) -> str:
|
||||||
|
"""
|
||||||
|
Get the quality of the chord.
|
||||||
|
Returns:
|
||||||
|
str, the quality of the chord, empty string if not built yet
|
||||||
|
"""
|
||||||
|
intervals=tuple(self.get_intervals())
|
||||||
|
if intervals not in triad_intervals.keys():
|
||||||
|
return "Unknown chord"
|
||||||
|
return triad_intervals[intervals]
|
||||||
|
|
||||||
|
def __str__(self) -> str:
|
||||||
|
return f"{self.keys[0].get_name()}{self.get_quality()} triad under major scale {self.root.get_name()}"
|
||||||
|
|
||||||
|
|
||||||
|
class SevenChord(Chord):
|
||||||
|
def __init__(self, root:Note, keys:list[Note]=None, zero_indexed:bool=False):
|
||||||
|
if len(keys) != 4:
|
||||||
|
raise ValueError("Seven chord must have 4 keys")
|
||||||
|
super().__init__(root, keys, zero_indexed)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def build_seven_chord(self, root:Note, starting_index:int=1, quality:str=None) -> 'SevenChord':
|
||||||
|
"""
|
||||||
|
Build a seven chord object.
|
||||||
|
Args:
|
||||||
|
root: str, the root note of the chord
|
||||||
|
starting_index: int, the starting note index of the chord, default is 1
|
||||||
|
quality: str, the quality of the chord, default is None
|
||||||
|
"""
|
||||||
|
keys = self.get_seven_chord(self, starting_index, quality)
|
||||||
|
return SevenChord(root, keys, self.zero_indexed)
|
||||||
|
|
||||||
|
def get_seven_chord(self, starting_index:int, quality:str=None) -> list[Note]:
|
||||||
|
"""
|
||||||
|
Get a seven chord from the major scale.
|
||||||
|
Args:
|
||||||
|
starting_index: int, the starting index of the chord, default is 1, assume is positive indexed
|
||||||
|
quality: str, the quality of the chord ["dominant", "diminished", "half-diminished", "augmented major", "minor-major", "major", "full-diminished"], default is None
|
||||||
|
Returns:
|
||||||
|
list[Note], the seven chord
|
||||||
|
"""
|
||||||
|
print(type(self))
|
||||||
|
if not self.zero_indexed:
|
||||||
|
starting_index = starting_index - 1
|
||||||
|
major_notes = self.major.get_major_scale(starting_index+8)
|
||||||
|
seven_chord = []
|
||||||
|
if quality is None:
|
||||||
|
# major seven chord from selected note
|
||||||
|
for i in range(starting_index, starting_index + 8,2):
|
||||||
|
seven_chord.append(major_notes[i])
|
||||||
|
else:
|
||||||
|
# major seven chord from quality, note may not be in major scale
|
||||||
|
if quality not in seven_chord_intervals:
|
||||||
|
raise ValueError(f"Invalid quality: {quality}")
|
||||||
|
current_note_code = major_notes[starting_index].code
|
||||||
|
for interval in seven_chord_intervals[quality]:
|
||||||
|
current_note_code += interval
|
||||||
|
seven_chord.append(Note.from_int(current_note_code, self.root.is_sharp))
|
||||||
|
return seven_chord
|
||||||
|
|
||||||
|
def get_quality(self, name_type:str="shortest") -> str:
|
||||||
|
"""
|
||||||
|
Get the quality of the chord.
|
||||||
|
Args:
|
||||||
|
name_type: str, the type of the name, default is "shortest", can be "short" or "full"
|
||||||
|
Returns:
|
||||||
|
str, the quality of the chord, empty string if not built yet
|
||||||
|
"""
|
||||||
|
intervals=tuple(self.get_intervals())
|
||||||
|
if intervals not in intervals_seven_chord.keys():
|
||||||
|
return "Unknown chord"
|
||||||
|
if name_type == "shortest":
|
||||||
|
return f'{self.keys[0].get_name()}{intervals_seven_chord[intervals][0]} '
|
||||||
|
elif name_type == "short":
|
||||||
|
return f'{self.keys[0].get_name()}{intervals_seven_chord[intervals][1]} '
|
||||||
|
elif name_type == "full":
|
||||||
|
return f'{self.keys[0].get_name()}{intervals_seven_chord[intervals][2]} '
|
||||||
|
else:
|
||||||
|
raise ValueError(f"Invalid name type: {name_type}")
|
||||||
|
|
||||||
|
def is_primary_dominant(self) -> bool:
|
||||||
|
"""
|
||||||
|
Check if the chord is a primary dominant chord.
|
||||||
|
Returns:
|
||||||
|
bool, True if the chord is a primary dominant chord (I, IV, V), False otherwise
|
||||||
|
"""
|
||||||
|
intervals=tuple(self.get_intervals())
|
||||||
|
if intervals not in intervals_seven_chord.keys():
|
||||||
|
return False
|
||||||
|
if self.keys[0]-self.root==0 and intervals==seven_chord_intervals["major"]:
|
||||||
|
return True
|
||||||
|
elif self.keys[0]-self.root==5 and intervals==seven_chord_intervals["major"]:
|
||||||
|
return True
|
||||||
|
elif self.keys[0]-self.root==7 and intervals==seven_chord_intervals["dominant"]:
|
||||||
|
return True
|
||||||
|
else:
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
def is_secondary_dominant(self) -> bool:
|
||||||
|
"""
|
||||||
|
Check if the chord is a secondary dominant chord.
|
||||||
|
Returns:
|
||||||
|
bool, True if the chord is a secondary chord (ii, iii, vi, vii*), False otherwise
|
||||||
|
"""
|
||||||
|
intervals=tuple(self.get_intervals())
|
||||||
|
if intervals not in intervals_seven_chord.keys():
|
||||||
|
return False
|
||||||
|
if self.keys[0]-self.root==2 and intervals==seven_chord_intervals["minor"]:
|
||||||
|
return True
|
||||||
|
elif self.keys[0]-self.root==4 and intervals==seven_chord_intervals["minor"]:
|
||||||
|
return True
|
||||||
|
elif self.keys[0]-self.root==7 and intervals==seven_chord_intervals["minor"]:
|
||||||
|
return True
|
||||||
|
elif self.keys[0]-self.root==9 and intervals==seven_chord_intervals["diminished"]:
|
||||||
|
return True
|
||||||
|
else:
|
||||||
|
return False
|
||||||
|
|
||||||
|
def get_secondary_dominant(self) -> 'SevenChord':
|
||||||
|
"""
|
||||||
|
Get the secondary dominant chord.
|
||||||
|
Returns:
|
||||||
|
Chord, the secondary dominant chord
|
||||||
|
"""
|
||||||
|
if not((self.is_secondary_dominant() or self.is_primary_dominant()) and self.get_intervals()!=seven_chord_intervals["diminished"]):
|
||||||
|
raise ValueError("Chord is not primary or secondary dominant")
|
||||||
|
root_note = self.keys[0]-2
|
||||||
|
return SevenChord.build_seven_chord(root_note, starting_index=8, quality='dominant')
|
||||||
|
|
||||||
|
def tritone_substitution(self) -> 'SevenChord':
|
||||||
|
"""
|
||||||
|
Get the tritone substitution of the chord.
|
||||||
|
Returns:
|
||||||
|
Chord, the tritone substitution chord
|
||||||
|
"""
|
||||||
|
if self.get_quality()!="dominant":
|
||||||
|
raise ValueError("Chord is not dominant")
|
||||||
|
root_note = self.keys[2]-4
|
||||||
|
return SevenChord.build_seven_chord(root_note, starting_index=8, quality='dominant')
|
||||||
|
|
||||||
|
def __str__(self) -> str:
|
||||||
|
return f"{self.keys[0].get_name()}{self.get_quality()} seven chord under major scale {self.root.get_name()}"
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
words=""
|
||||||
|
while words != "quit":
|
||||||
|
words = input("Enter a root note for chord: ")
|
||||||
|
chord = SevenChord.build_seven_chord(Note(words), quality='dominant')
|
||||||
|
print([note.get_name() for note in chord.keys])
|
||||||
54
main.py
Normal file
54
main.py
Normal file
@@ -0,0 +1,54 @@
|
|||||||
|
# minimal counting function used for review
|
||||||
|
|
||||||
|
chromatic_scale = ['A', 'A#', 'B', 'C', 'C#', 'D', 'D#', 'E', 'F', 'F#', 'G', 'G#']
|
||||||
|
note_value = {e:i for i, e in enumerate(chromatic_scale)}
|
||||||
|
seven_chord_interval_name = {
|
||||||
|
'M7#5':(4,4,3),
|
||||||
|
'M7':(4,3,4),
|
||||||
|
'7':(4,3,3),
|
||||||
|
'mM7':(3,4,4),
|
||||||
|
'm7':(3,4,3),
|
||||||
|
'm7b5':(3,3,4),
|
||||||
|
'dim7':(3,3,3),
|
||||||
|
}
|
||||||
|
major_scale_intervals = [2,2,1,2,2,2,1]
|
||||||
|
|
||||||
|
def get_major_scale(root):
|
||||||
|
scale = []
|
||||||
|
scale.append(root)
|
||||||
|
root_index = note_value[root]
|
||||||
|
for interval in major_scale_intervals:
|
||||||
|
root_index = (root_index + interval) % 12
|
||||||
|
scale.append(chromatic_scale[root_index])
|
||||||
|
return scale
|
||||||
|
|
||||||
|
def get_number_of_half_steps(note1, note2):
|
||||||
|
return min(abs(note_value[note1] - note_value[note2]), 12 - abs(note_value[note1] - note_value[note2]))
|
||||||
|
|
||||||
|
def _construct_chord(root, interval_type=(4,3,3)):
|
||||||
|
chord = []
|
||||||
|
chord.append(root)
|
||||||
|
root_index = note_value[root]
|
||||||
|
for interval in interval_type:
|
||||||
|
root_index = (root_index + interval) % 12
|
||||||
|
chord.append(chromatic_scale[root_index])
|
||||||
|
return chord
|
||||||
|
|
||||||
|
words=""
|
||||||
|
|
||||||
|
while words != "quit":
|
||||||
|
words = input("Enter a note or chord: ")
|
||||||
|
notes = words.split(" ")
|
||||||
|
if notes[0][1:] in seven_chord_interval_name.keys():
|
||||||
|
# construct dominant 7 chord
|
||||||
|
chord = _construct_chord(notes[0][0],seven_chord_interval_name[notes[0][1:]])
|
||||||
|
print(chord,get_major_scale(notes[0][0]))
|
||||||
|
else:
|
||||||
|
# calculate the interval between notes
|
||||||
|
res=[]
|
||||||
|
for i in range(len(notes)-1):
|
||||||
|
if notes[i] not in note_value or notes[i+1] not in note_value:
|
||||||
|
print("Invalid note")
|
||||||
|
break
|
||||||
|
res.append(get_number_of_half_steps(notes[i], notes[i+1]))
|
||||||
|
print(res)
|
||||||
183
major.py
Normal file
183
major.py
Normal file
@@ -0,0 +1,183 @@
|
|||||||
|
chromatic=[('A','A'),
|
||||||
|
('A#','Bb'),
|
||||||
|
('B','B'),
|
||||||
|
('C','C'),
|
||||||
|
('C#','Db'),
|
||||||
|
('D','D'),
|
||||||
|
('D#','Eb'),
|
||||||
|
('E','E'),
|
||||||
|
('F','F'),
|
||||||
|
('F#','Gb'),
|
||||||
|
('G','G'),
|
||||||
|
('G#','Ab')]
|
||||||
|
chromatic_notes_set=set(note for pair in chromatic for note in pair)
|
||||||
|
major_scale_intervals = [2,2,1,2,2,2,1]
|
||||||
|
natural_minor_scale_intervals = [2,1,2,2,1,2,2]
|
||||||
|
harmonic_minor_scale_intervals = [2,1,2,2,1,3,1]
|
||||||
|
melodic_minor_scale_intervals = [2,1,2,2,2,2,1]
|
||||||
|
# use A4 as 0th note, note octave number change from B to C.
|
||||||
|
a4_freq = 440
|
||||||
|
|
||||||
|
class Note:
|
||||||
|
def __init__(self, note:str, octave:int=4):
|
||||||
|
if note not in chromatic_notes_set:
|
||||||
|
raise ValueError(f"Invalid note: {note}, must be in {chromatic_notes_set}")
|
||||||
|
self.is_sharp = 'b' not in note
|
||||||
|
self.position = next(i for i, v in enumerate(chromatic) if note in v)
|
||||||
|
self.octave_position = self.position
|
||||||
|
# if octave_position is greater than or equal to 3 (C), subtract 12
|
||||||
|
if self.octave_position >= 3:
|
||||||
|
self.octave_position -= 12
|
||||||
|
self.code = 12 * (octave - 3) + self.octave_position
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def from_int(self, code:int, is_sharp:bool=True) -> 'Note':
|
||||||
|
note = chromatic[code % len(chromatic)][0] if is_sharp else chromatic[code % len(chromatic)][1]
|
||||||
|
return Note(note, code // 12 + 4)
|
||||||
|
|
||||||
|
def whole_step(self, invert:bool=False) -> 'Note':
|
||||||
|
return self.from_int(self.code + 2) if not invert else self.from_int(self.code - 2)
|
||||||
|
|
||||||
|
def half_step(self, invert:bool=False) -> 'Note':
|
||||||
|
return self.from_int(self.code + 1) if not invert else self.from_int(self.code - 1)
|
||||||
|
|
||||||
|
def __add__(self, other) -> 'Note' or int:
|
||||||
|
"""
|
||||||
|
Add a note or an integer to a note.
|
||||||
|
Args:
|
||||||
|
other: Note or int, the note or integer to add
|
||||||
|
Returns:
|
||||||
|
Note, the result of the addition
|
||||||
|
"""
|
||||||
|
if isinstance(other, Note):
|
||||||
|
return self.from_int(self.code + other.code)
|
||||||
|
elif isinstance(other, int):
|
||||||
|
return self.from_int(self.code + other)
|
||||||
|
else:
|
||||||
|
raise ValueError(f"Instance of {type(other)} is not supported")
|
||||||
|
|
||||||
|
def __sub__(self, other) -> 'Note' or int:
|
||||||
|
"""
|
||||||
|
Subtract a note or an integer from a note.
|
||||||
|
Args:
|
||||||
|
other: Note or int, the note or integer to subtract
|
||||||
|
Returns:
|
||||||
|
Note, the result of the subtraction
|
||||||
|
int, the result of the subtraction if other is an integer
|
||||||
|
"""
|
||||||
|
if isinstance(other, Note):
|
||||||
|
return self.from_int(self.code - other.code)
|
||||||
|
elif isinstance(other, int):
|
||||||
|
return self.code - other
|
||||||
|
else:
|
||||||
|
raise ValueError(f"Instance of {type(other)} is not supported")
|
||||||
|
|
||||||
|
def __eq__(self, other: 'Note') -> bool:
|
||||||
|
return self.code == other.code
|
||||||
|
|
||||||
|
def enharmonic_equivalent(self) -> 'Note':
|
||||||
|
return chromatic[self.position%12][1 if self.is_sharp else 0]
|
||||||
|
|
||||||
|
def get_frequency(self) -> float:
|
||||||
|
"""
|
||||||
|
Get the frequency of a note.
|
||||||
|
Returns:
|
||||||
|
float, the frequency of the note
|
||||||
|
"""
|
||||||
|
return a4_freq * 2 ** ((self.code) / 12)
|
||||||
|
|
||||||
|
def get_name(self) -> str:
|
||||||
|
return chromatic[self.position][0] if self.is_sharp else chromatic[self.position][1]
|
||||||
|
|
||||||
|
def get_octave(self) -> int:
|
||||||
|
"""
|
||||||
|
Get the octave of a note.
|
||||||
|
A4 is 0, A#4 is 1, B4 is 2, C5 is 3 (change octave), etc.
|
||||||
|
Returns:
|
||||||
|
int, the octave of the note
|
||||||
|
"""
|
||||||
|
return (self.code - self.octave_position) // 12 + 4
|
||||||
|
|
||||||
|
def __str__(self) -> str:
|
||||||
|
return f"{self.get_name()}{self.get_octave()}"
|
||||||
|
|
||||||
|
class MajorScale:
|
||||||
|
def __init__(self, root:Note):
|
||||||
|
if not isinstance(root, Note):
|
||||||
|
raise ValueError(f"Root must be a Note object, got {type(root)}")
|
||||||
|
self.root = root
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def from_note_int(self, root_code:int, is_sharp:bool=True) -> 'MajorScale':
|
||||||
|
return MajorScale(Note.from_int(root_code, is_sharp))
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def from_note_name(self, root_name:str, octave:int=4) -> 'MajorScale':
|
||||||
|
return MajorScale(Note(root_name, octave))
|
||||||
|
|
||||||
|
def is_in_scale(self, list_of_notes:list[Note]) -> bool:
|
||||||
|
"""
|
||||||
|
Check if a list of notes is in the major scale.
|
||||||
|
Args:
|
||||||
|
list_of_notes: list[Note], the list of notes to check
|
||||||
|
Returns:
|
||||||
|
bool, True if the list of notes is in the major scale, False otherwise
|
||||||
|
"""
|
||||||
|
for note in list_of_notes:
|
||||||
|
if note.code not in [note.code for note in self.get_major_scale()]:
|
||||||
|
return False
|
||||||
|
return True
|
||||||
|
|
||||||
|
def get_major_scale(self, length:int=7) -> list[Note]:
|
||||||
|
scale = []
|
||||||
|
scale.append(self.root)
|
||||||
|
cur_note_pos = self.root.code
|
||||||
|
interval_index = 0
|
||||||
|
for _ in range(length):
|
||||||
|
cur_note_pos += major_scale_intervals[interval_index]
|
||||||
|
# keep sharp notation
|
||||||
|
scale.append(Note.from_int(cur_note_pos, self.root.is_sharp))
|
||||||
|
interval_index = (interval_index + 1) % len(major_scale_intervals)
|
||||||
|
return scale
|
||||||
|
|
||||||
|
def get_minor_scale(self, type:str='natural', length:int=7) -> list[Note]:
|
||||||
|
scale = []
|
||||||
|
scale.append(self.root)
|
||||||
|
cur_note_pos = self.root.code
|
||||||
|
minor_scale_intervals=natural_minor_scale_intervals
|
||||||
|
if type!='natural':
|
||||||
|
if type=='harmonic':
|
||||||
|
minor_scale_intervals=harmonic_minor_scale_intervals
|
||||||
|
elif type=='melodic':
|
||||||
|
minor_scale_intervals=melodic_minor_scale_intervals
|
||||||
|
else:
|
||||||
|
raise ValueError("Invalid minor scale type. Choose 'natural', 'harmonic', or 'melodic'.")
|
||||||
|
interval_index = 0
|
||||||
|
for _ in range(length):
|
||||||
|
cur_note_pos += minor_scale_intervals[interval_index]
|
||||||
|
# keep sharp notation
|
||||||
|
scale.append(Note.from_int(cur_note_pos, self.root.is_sharp))
|
||||||
|
interval_index = (interval_index + 1) % len(minor_scale_intervals)
|
||||||
|
return scale
|
||||||
|
|
||||||
|
def get_parallel_minor(self) -> 'MajorScale':
|
||||||
|
"""
|
||||||
|
Get the parallel minor scale.
|
||||||
|
Returns:
|
||||||
|
MajorScale, the parallel minor scale
|
||||||
|
"""
|
||||||
|
return MajorScale(self.root+5)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
a=input("Enter a root note for major scale: ")
|
||||||
|
while a != "quit":
|
||||||
|
if a not in chromatic_notes_set:
|
||||||
|
a = input("Invalid note. Enter a root note for major scale: ")
|
||||||
|
continue
|
||||||
|
major_scale = MajorScale.from_note_name(a)
|
||||||
|
print(f"Major scale for {a}: {[note.get_name() for note in major_scale.get_major_scale()]}")
|
||||||
|
print(f"Natural minor scale for {a}: {[note.get_name() for note in major_scale.get_minor_scale('natural')]}")
|
||||||
|
print(f"Harmonic minor scale for {a}: {[note.get_name() for note in major_scale.get_minor_scale('harmonic')]}")
|
||||||
|
print(f"Melodic minor scale for {a}: {[note.get_name() for note in major_scale.get_minor_scale('melodic')]}")
|
||||||
|
a=input("Enter a root note for major scale: ")
|
||||||
22
test/major_test.py
Normal file
22
test/major_test.py
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
import major
|
||||||
|
|
||||||
|
import unittest
|
||||||
|
|
||||||
|
class MajorScaleTest(unittest.TestCase):
|
||||||
|
def test_from_note_int(self):
|
||||||
|
scale = major.MajorScale.from_note_int(0)
|
||||||
|
self.assertEqual(scale.root, "A")
|
||||||
|
self.assertEqual(scale.octave, 4)
|
||||||
|
self.assertEqual(scale.scale, ["A", "B", "C#", "D", "E", "F#", "G#"])
|
||||||
|
|
||||||
|
def test_get_major_scale(self):
|
||||||
|
scale = major.MajorScale.from_note_int(0)
|
||||||
|
self.assertEqual(scale.get_major_scale(), ["A", "B", "C#", "D", "E", "F#", "G#"])
|
||||||
|
|
||||||
|
def test_get_minor_scale(self):
|
||||||
|
scale = major.MajorScale.from_note_int(0)
|
||||||
|
self.assertEqual(scale.get_minor_scale(), ["A", "B", "C#", "D", "E", "F#", "G#"])
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
unittest.main()
|
||||||
Reference in New Issue
Block a user