"""In this text, a number can mean one of
three things:
1. A note (2 for "Re", or D).
2. An interval (2 for "two notes from the previous note").
3. An offset ("two notes from the first note").
To differentiate these, use type "aliases"
defined in the following cell instead of `int`.
"""
from typing import overload, Sequence, Optional
import collections
import re
"""An interval. The interval from
the :math:`a`\\ :sup:`th` note and the
:math:`b`\\ :sup:`th` is :math:`b-a`.
"""
Interval = int
Pitch = int
"""A pitch, representing
a note by its position in :attr:`NOTES`.
"""
Note = int
[docs]
def note_m2s(note: int) -> str:
""" """
assert note >= 0 and note <= 127, \
"Note outside of MIDI range"
return NOTE_NAMES[note % len(NOTE_NAMES)]\
+ str(note // len(NOTE_NAMES))
[docs]
def note_s2m(note: str) -> int:
assert re.match("[0-9]", note[-1]), \
"Specification does not include octave"
assert note[:-1] in NOTE_NAMES, \
f"{note[:-1]} is not in `NOTES`"
return 12 * int(note[-1]) + NOTE_NAMES.index(note[:-1])
@overload
def note_i2s(note: Optional[Note]) -> str:
pass
@overload
def note_i2s(note: Sequence[Optional[Note]]) -> list[str]:
pass
[docs]
def note_i2s(note: Optional[Note]
| Sequence[Optional[Note]]) -> str | list[str]:
"""Return either a string or a list
of strings that represents note(s) in
:arg:`note`.
"""
def _base(key_int: Optional[Note]) -> str:
if key_int is None:
return " "
else:
return NOTES[key_int % len(NOTES)]
if note is None:
return " "
if isinstance(note, Note):
return _base(note)
else:
return [_base(kn) for kn in note]
@overload
def note_s2i(name: str,
solfege: bool = False) -> Note:
pass
@overload
def note_s2i(name: list[str],
solfege: bool = False) -> list[Note]:
pass
[docs]
def note_s2i(name: str | list[str],
solfege: bool = False) -> Note | list[Note]:
"""Return either an integer or a list
of notes that represents note(s) in
:arg:`name`.
"""
def _base(name: str) -> Note:
sofege_offset: int = 1 if solfege else 0
if name in NOTES:
return NOTES.index(name) + sofege_offset
else:
return NOTE_NAMES.index(name) + sofege_offset
if isinstance(name, str):
return _base(name)
else:
return [_base(kn) for kn in name]
"""Names of notes.
"""
NOTE_NAMES: list[str] = [
"C",
"C#",
"D",
"D#",
"E",
"F",
"F#",
"G",
"G#",
"A",
"A#",
"B",
]
NOTES_MIDI: list[str] = [note_m2s(i) for i in range(0, 127 + 1)]
NOTES_INTEGER: list[str] = NOTE_NAMES
NOTES = NOTES_MIDI
"""Map of each apartness to a name. The entry
`apartness`: (`major/minor`, `number`) means
apartness `apartness` is the `major/minor` `number`
:sup:`th`.
"""
interval_seeds: dict[int, tuple[str, int]] = {
2: ("major", 2),
4: ("major", 3),
5: ("perfect", 4),
7: ("perfect", 5),
9: ("major", 6),
11: ("major", 7),
12: ("perfect", 8),
}
# from each perfect, produce: augmented +1, diminished -2
INTERVALS: dict[str, int] = dict()
INTERVALS["prime 1"] = 0
INTERVALS["augmented 1"] = 1
INTERVALS["diminished 1"] = 11
for interval_semitone, (name, order) in interval_seeds.items():
if name == "major":
# from each major, produce: augmented +1, minor -1, diminished -2
INTERVALS[f"augmented {order}"] =\
(interval_semitone + 1) % len(NOTE_NAMES)
INTERVALS[f"major {order}"] =\
interval_semitone
INTERVALS[f"minor {order}"] =\
(interval_semitone - 1) % len(NOTE_NAMES)
# Diminishing a minor gives -2 from major
INTERVALS[f"diminished {order}"] =\
(interval_semitone - 2) % len(NOTE_NAMES)
elif name == "perfect":
INTERVALS[f"augmented {order}"] =\
(interval_semitone + 1) % len(NOTE_NAMES)
INTERVALS[f"perfect {order}"] =\
(interval_semitone) % len(NOTE_NAMES)
INTERVALS[f"diminished {order}"] =\
(interval_semitone - 1) % len(NOTE_NAMES)
else:
raise KeyError()
[docs]
def interval_s2i(name: str) -> int:
return INTERVALS[name]
[docs]
def interval_i2s(key_num: int) -> list[str]:
"""Map an apartness to the appropriate name
according to :attr:`INTERVALS`.
"""
return [
name
for name, interval_semitone in INTERVALS.items()
if interval_semitone == key_num
]
[docs]
def name_interval(note_from: str, note_to: str) -> str:
"""Map an apartness to the appropriate name
according to :attr:`INTERVALS`.
"""
interval_semitone = (note_s2i(note_to) - note_s2i(note_from)) \
% len(NOTE_NAMES)
interval_major = str(
(ord(note_to[0]) - ord(note_from[0]))
% (ord("G") - ord("A") + 1) + 1
)
possible_intervals = interval_i2s(interval_semitone)
matching_names: list[str] = [
x for x in possible_intervals if x[-1] == interval_major
]
assert len(matching_names) < 2
return matching_names[0]
[docs]
def interval_from_scale(scale: list[str],
poses: tuple[int, int]) -> list[str]:
print(note_s2i(scale[poses[1]]) - note_s2i(scale[poses[0]]))
return interval_i2s(abs(note_s2i(scale[poses[1]])
- note_s2i(scale[poses[0]])))
@overload
def invert(arg: str) -> str:
pass
@overload
def invert(arg: Note) -> Note:
pass
[docs]
def invert(arg: int | str) -> Note | Interval | str:
"""Invert an interval. Or a note.
Works with both because taking the
modular complement happens to have the
same effect on both.
"""
if isinstance(arg, Note) or isinstance(arg, Interval):
return (len(NOTE_NAMES) - arg) % len(NOTES)
else:
return note_i2s((len(NOTE_NAMES) - note_s2i(arg))
% len(NOTES))
interval_i2s(invert(INTERVALS["major 7"]))
[docs]
def reach(base_key: str, interval: str | int) -> str:
""" "Reach up" from :arg:`base_key` by
:arg:`interval`. :arg:`interval` can be
either a number (e.g. 1) or a string
(e.g. augmented 8).
In the latter case, :arg:`interval` should be a
key in :attr:`INTERVALS`.
"""
apartness: int
if isinstance(interval, str):
apartness = INTERVALS[interval]
else:
apartness = interval
return note_i2s((note_s2i(base_key) + apartness) % len(NOTES))
[docs]
def match_out_numbers(expr: str) -> str:
manah: Optional[re.Match] = re.match("((?![0-9]).)*", expr)
if manah:
return manah.group(0)
else:
raise Exception("owo")
@overload
def same_class(a: list[str], b: list[str]) -> bool:
pass
@overload
def same_class(a: str, b: str) -> bool:
pass
[docs]
def same_class(a: list[str] | str,
b: list[str] | str) -> bool:
if isinstance(a, str) and isinstance(b, str):
return match_out_numbers(a) == match_out_numbers(b)
else:
a = [match_out_numbers(x) for x in a]
b = [match_out_numbers(x) for x in b]
return collections.Counter(a) == collections.Counter(b)
# return [invert(triad[0]), *triad[1:]]