This commit is contained in:
Trance-0
2025-11-18 15:11:12 -06:00
commit f169b422af
7 changed files with 572 additions and 0 deletions

0
__init__.py Normal file
View File

Binary file not shown.

51
app.py Normal file
View 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
View 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
View 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
View 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
View 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()