183 lines
6.9 KiB
Python
183 lines
6.9 KiB
Python
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: ") |