From f169b422af8eeb85d3151c2a5719d800c55e26b8 Mon Sep 17 00:00:00 2001 From: Trance-0 <60459821+Trance-0@users.noreply.github.com> Date: Tue, 18 Nov 2025 15:11:12 -0600 Subject: [PATCH] init --- __init__.py | 0 __pycache__/major.cpython-310.pyc | Bin 0 -> 8215 bytes app.py | 51 ++++++ chord.py | 262 ++++++++++++++++++++++++++++++ main.py | 54 ++++++ major.py | 183 +++++++++++++++++++++ test/major_test.py | 22 +++ 7 files changed, 572 insertions(+) create mode 100644 __init__.py create mode 100644 __pycache__/major.cpython-310.pyc create mode 100644 app.py create mode 100644 chord.py create mode 100644 main.py create mode 100644 major.py create mode 100644 test/major_test.py diff --git a/__init__.py b/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/__pycache__/major.cpython-310.pyc b/__pycache__/major.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..58b7f63a84139e67c53fa4c4f8e447fac8aa151c GIT binary patch literal 8215 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 literal 0 HcmV?d00001 diff --git a/app.py b/app.py new file mode 100644 index 0000000..2ff19df --- /dev/null +++ b/app.py @@ -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() \ No newline at end of file diff --git a/chord.py b/chord.py new file mode 100644 index 0000000..7dd0913 --- /dev/null +++ b/chord.py @@ -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]) \ No newline at end of file diff --git a/main.py b/main.py new file mode 100644 index 0000000..59e2ccb --- /dev/null +++ b/main.py @@ -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) \ No newline at end of file diff --git a/major.py b/major.py new file mode 100644 index 0000000..18b43d1 --- /dev/null +++ b/major.py @@ -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: ") \ No newline at end of file diff --git a/test/major_test.py b/test/major_test.py new file mode 100644 index 0000000..a12aac9 --- /dev/null +++ b/test/major_test.py @@ -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() \ No newline at end of file