Created
December 16, 2021 22:07
-
-
Save atsukoba/7b39febb1dc07e7336c5f53d475ace6a to your computer and use it in GitHub Desktop.
Python utility for scales using MIDI note numbers and NoteSequence by Magenta
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
from copy import copy | |
from typing import Any, Dict, List, Tuple, Callable | |
import note_seq | |
from note_seq.protobuf.music_pb2 import NoteSequence as ns | |
class Scales: | |
def __init__(self, key: str): | |
""" | |
author: Atsuya Kobayashi | |
reference: https://www.feelyoursound.com/scale-chords/ | |
""" | |
self.MIDI_RANGE: Tuple[int, int] = (0, 127) | |
self.BASE_NOTE_NAMES: Dict[int, str] = { | |
# general midi | |
0: "C", | |
1: "C#", | |
2: "D", | |
3: "D#", | |
4: "E", | |
5: "F", | |
6: "F#", | |
7: "G", | |
8: "G#", | |
9: "A", | |
10: "A#", | |
11: "B" | |
} | |
assert key in self.BASE_NOTE_NAMES.values(), \ | |
f"`key` should be one of {self.BASE_NOTE_NAMES.values()}" | |
self.KEY = key | |
self.BASE_NOTE_NUMS: Dict[str, int] = { | |
v: k for k, v in self.BASE_NOTE_NAMES.items() | |
} | |
# for type hints | |
self.Major: List[int] | |
self.fit_to_Major: Callable[[int], int] | |
self.fit_notes_to_Major: Callable[[List[int]], List[int]] | |
self.fit_note_sequence_to_Major: Callable[[ns], ns] | |
self.Major_Names: List[str] | |
self.Major_Names_All: List[str] | |
self.Minor: List[int] | |
self.fit_to_Minor: Callable[[int], int] | |
self.fit_notes_to_Minor: Callable[[List[int]], List[int]] | |
self.fit_note_sequence_to_Minor: Callable[[ns], ns] | |
self.Minor_Names: List[str] | |
self.Minor_Names_All: List[str] | |
self.Harmonic_Minor: List[int] | |
self.fit_to_Harmonic_Minor: Callable[[int], int] | |
self.fit_notes_to_Harmonic_Minor: Callable[[List[int]], List[int]] | |
self.fit_note_sequence_to_Harmonic_Minor: Callable[[ns], ns] | |
self.Harmonic_Minor_Names: List[str] | |
self.Harmonic_Minor_Names_All: List[str] | |
self.Melodic_Minor: List[int] | |
self.fit_to_Melodic_Minor: Callable[[int], int] | |
self.fit_notes_to_Melodic_Minor: Callable[[List[int]], List[int]] | |
self.fit_note_sequence_to_Melodic_Minor: Callable[[ns], ns] | |
self.Melodic_Minor_Names: List[str] | |
self.Melodic_Minor_Names_All: List[str] | |
self.Ionian: List[int] | |
self.fit_to_Ionian: Callable[[int], int] | |
self.fit_notes_to_Ionian: Callable[[List[int]], List[int]] | |
self.fit_note_sequence_to_Ionian: Callable[[ns], ns] | |
self.Ionian_Names: List[str] | |
self.Ionian_Names_All: List[str] | |
self.Dorian: List[int] | |
self.fit_to_Dorian: Callable[[int], int] | |
self.fit_notes_to_Dorian: Callable[[List[int]], List[int]] | |
self.fit_note_sequence_to_Dorian: Callable[[ns], ns] | |
self.Dorian_Names: List[str] | |
self.Dorian_Names_All: List[str] | |
self.Phrygian: List[int] | |
self.fit_to_Phrygian: Callable[[int], int] | |
self.fit_notes_to_Phrygian: Callable[[List[int]], List[int]] | |
self.fit_note_sequence_to_Phrygian: Callable[[ns], ns] | |
self.Phrygian_Names: List[str] | |
self.Phrygian_Names_All: List[str] | |
self.Lydian: List[int] | |
self.fit_to_Lydian: Callable[[int], int] | |
self.fit_notes_to_Lydian: Callable[[List[int]], List[int]] | |
self.fit_note_sequence_to_Lydian: Callable[[ns], ns] | |
self.Lydian_Names: List[str] | |
self.Lydian_Names_All: List[str] | |
self.Mixolydian: List[int] | |
self.fit_to_Mixolydian: Callable[[int], int] | |
self.fit_notes_to_Mixolydian: Callable[[List[int]], List[int]] | |
self.fit_note_sequence_to_Mixolydian: Callable[[ns], ns] | |
self.Mixolydian_Names: List[str] | |
self.Mixolydian_Names_All: List[str] | |
self.Aeolian: List[int] | |
self.fit_to_Aeolian: Callable[[int], int] | |
self.fit_notes_to_Aeolian: Callable[[List[int]], List[int]] | |
self.fit_note_sequence_to_Aeolian: Callable[[ns], ns] | |
self.Aeolian_Names: List[str] | |
self.Aeolian_Names_All: List[str] | |
self.Locrian: List[int] | |
self.fit_to_Locrian: Callable[[int], int] | |
self.fit_notes_to_Locrian: Callable[[List[int]], List[int]] | |
self.fit_note_sequence_to_Locrian: Callable[[ns], ns] | |
self.Locrian_Names: List[str] | |
self.Locrian_Names_All: List[str] | |
self.Minor_Pentatonic: List[int] | |
self.fit_to_Minor_Pentatonic: Callable[[int], int] | |
self.fit_notes_to_Minor_Pentatonic: Callable[[List[int]], List[int]] | |
self.fit_note_sequence_to_Minor_Pentatonic: Callable[[ns], ns] | |
self.Minor_Pentatonic_Names: List[str] | |
self.Minor_Pentatonic_Names_All: List[str] | |
self.Major_Pentatonic: List[int] | |
self.fit_to_Major_Pentatonic: Callable[[int], int] | |
self.fit_notes_to_Major_Pentatonic: Callable[[List[int]], List[int]] | |
self.fit_note_sequence_to_Major_Pentatonic: Callable[[ns], ns] | |
self.Major_Pentatonic_Names: List[str] | |
self.Major_Pentatonic_Names_All: List[str] | |
C, C_SHARP, D, D_SHARP, E, F, F_SHARP, G, G_SHARP, A, A_SHARP, B = range( | |
12) | |
self.BASE_SCALES: Dict[str, List[int]] = { # key=C | |
"Major": [C, D, E, F, G, A, B], | |
"Minor": [C, D, D_SHARP, F, G, G_SHARP, A_SHARP], | |
"Harmonic_Minor": [C, D, D_SHARP, F, G, G_SHARP, B], | |
"Melodic_Minor": [C, D, D_SHARP, F, G, A, B], | |
"Ionian": [C, D, E, F, G, A, B], | |
"Dorian": [C, D, D_SHARP, F, G, A, A_SHARP], | |
"Phrygian": [C, C_SHARP, D_SHARP, F, G, G_SHARP, A_SHARP], | |
"Lydian": [C, D, E, F_SHARP, G, A, B], | |
"Mixolydian": [C, D, E, F, G, A, A_SHARP], | |
"Aeolian": [C, D, D_SHARP, F, G, G_SHARP, A_SHARP], | |
"Locrian": [C, C_SHARP, D_SHARP, F, F_SHARP, G_SHARP, A_SHARP], | |
"Minor_Pentatonic": [C, D_SHARP, F, G, A_SHARP], | |
"Major_Pentatonic": [C, D, E, G, A] | |
} | |
self.__pitch_gap: int = self.BASE_NOTE_NUMS[key] - C % 12 | |
for scale_name, notes in self.BASE_SCALES.items(): | |
setattr(self, scale_name, self._get_all_midi_notes_range(notes)) | |
setattr(self, | |
f"{scale_name}_Names_All", self._get_notes_as_name( | |
self._get_all_midi_notes_range(notes))) | |
setattr(self, | |
f"{scale_name}_Names", | |
self._sort_note_names_based_on_key( | |
list(set([self.BASE_NOTE_NAMES[n % 12] | |
for n in self._get_all_midi_notes_range(notes)])))) | |
setattr(self, f"fit_note_to_{scale_name}", | |
lambda note: self._fit_to_scale( | |
note, self._get_all_midi_notes_range(notes))) | |
setattr(self, f"fit_notes_to_{scale_name}", | |
lambda _notes: self._fit_notes_to_scale( | |
_notes, self._get_all_midi_notes_range(notes))) | |
setattr(self, f"fit_note_sequence_to_{scale_name}", | |
lambda ns: self._fit_note_sequence_to_scale( | |
ns, self._get_all_midi_notes_range(notes))) | |
def _get_all_midi_notes_range(self, notes: List[int]) -> List[int]: | |
l = [sorted(list(range(note, 127, 12)) + list(range(note, 0, 12))) | |
for note in notes] | |
return [_note + self.__pitch_gap for _notes in l for _note in _notes] | |
def _fit_to_scale(self, note: int, scale_notes: List[int]) -> int: | |
min_gap = 127 | |
# this takes O(n) | |
for idx, scale_note in enumerate(scale_notes): | |
if note == scale_notes: | |
return note | |
gap = scale_note - note | |
if abs(gap) < abs(min_gap): | |
min_gap = gap | |
return note + min_gap | |
def _sort_note_names_based_on_key(self, notes: List[str]) -> List[str]: | |
notes = sorted(notes) | |
s_idx = notes.index(self.KEY) | |
return notes[s_idx:] + notes[:s_idx] | |
def _fit_notes_to_scale(self, notes: List[int], | |
scale_notes: List[int]) -> List[int]: | |
return [self._fit_to_scale(n, scale_notes) for n in notes] | |
def _fit_note_sequence_to_scale(self, notes: ns, | |
scale_notes: List[int]) -> ns: | |
new_ns = copy(notes) | |
for note in new_ns.notes: | |
note.pitch = self._fit_to_scale(note.pitch, scale_notes) | |
return new_ns | |
def _get_notes_as_name(self, notes: List[int]) -> List[str]: | |
notes = sorted(notes) # input notes are midi numbers | |
return [self._get_note_as_name(note) for note in notes] | |
def _get_note_as_name(self, note: int) -> str: | |
return f"{self.BASE_NOTE_NAMES[note % 12]}_{str(note // 12)}" | |
## examples | |
scales_A = Scales("A") | |
scales_C = Scales("C") | |
scales_D_SHARP = Scales("D#") | |
# You can check notes on each scale | |
print(scales_A.Major_Names) # get ['A', 'B', 'C#', 'D', 'E', 'F#', 'G#'] | |
print(scales_A.Minor_Names) # get ['A', 'B', 'C', 'D', 'E', 'F', 'G'] | |
print(scales_C.Major_Names) # get ['C', 'D', 'E', 'F', 'G', 'A', 'B'] | |
print(scales_C.Minor_Names) # get ['C', 'D', 'D#', 'F', 'G', 'G#', 'A#'] | |
print(scales_D_SHARP.Phrygian_Names) # get ['D#', 'E', 'F#', 'G#', 'A#', 'B', 'C#'] | |
print(scales_D_SHARP.Minor_Pentatonic_Names_All[:10]) # get ['D#_0', 'F#_0', 'G#_0', 'A#_0', 'C#_1', 'D#_1', 'F#_1', 'G#_1', 'A#_1', 'C#_2'] | |
# scale | |
scales_A.fit_notes_to_Dorian([64, 48, 60, 50]) # get [64, 47, 59, 49] |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment