Skip to content

Instantly share code, notes, and snippets.

@atsukoba
Created December 16, 2021 22:07
Show Gist options
  • Save atsukoba/7b39febb1dc07e7336c5f53d475ace6a to your computer and use it in GitHub Desktop.
Save atsukoba/7b39febb1dc07e7336c5f53d475ace6a to your computer and use it in GitHub Desktop.
Python utility for scales using MIDI note numbers and NoteSequence by Magenta
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