Files
JazzTheory/major.py
Trance-0 f169b422af init
2025-11-18 15:11:12 -06:00

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: ")