Spaces:
Sleeping
Sleeping
| import numpy as np | |
| from .basic import PitchClass, PitchInterval | |
| _LILY_NAME_TO_SCALE_DEGREES = { | |
| "major": (2, 2, 1, 2, 2, 2), | |
| "dorian": (2, 1, 2, 2, 2, 1), | |
| "phrygian": (1, 2, 2, 2, 1, 2), | |
| "lydian": (2, 2, 2, 1, 2, 2), | |
| "mixolydian": (2, 2, 1, 2, 2, 1), | |
| "minor": (2, 1, 2, 2, 1, 2), | |
| "locrian": (1, 2, 2, 1, 2, 2), | |
| } | |
| _SCALE_DEGREES_TO_LILY_NAME = {v: k for k, v in _LILY_NAME_TO_SCALE_DEGREES.items()} | |
| _KEY_TO_ENHARMONICS = {} | |
| for o, ln in zip( | |
| [0, 2, 4, 5, 7, 9, 11], | |
| ["major", "dorian", "phrygian", "lydian", "mixolydian", "minor", "locrian"], | |
| ): | |
| for i, pc in enumerate(range(12)): | |
| pc = (o + (pc * 7)) % 12 | |
| ks = (pc, _LILY_NAME_TO_SCALE_DEGREES[ln]) | |
| enharmonics = "#" if i <= 6 else "b" | |
| _KEY_TO_ENHARMONICS[ks] = enharmonics | |
| _CHORD_DEGREES_TO_LILY_NAME = { | |
| (4, 3): "", # Major | |
| (3, 4): "m", # Minor | |
| (3, 4, 3): "m7", # Minor 7 | |
| (4, 3, 3): "7", # 7 | |
| (4, 3, 4): "maj7", # Major 7 | |
| (5, 2): "sus4", # Sus 4 | |
| (4, 3, 7): "9^7", # Add 9 | |
| (3, 3): "dim", # Dim | |
| (3, 3, 4): "m7.5-", # Dim minor 7 | |
| (2, 5): "sus2", # Sus 2 | |
| (5, 2, 3): "7sus4", # 7 Sus 4 | |
| (3, 4, 3, 4): "m9", # Minor 9 | |
| (4, 3, 4, 3): "maj9", # Major 9 | |
| (2, 5, 3): "7sus2", # 7 Sus 2 | |
| (3, 4, 7): "m9^7", # Minor Add 9 | |
| (3, 3, 3): "dim7", # Dim 7 | |
| (2, 5, 4): "maj7sus2", # Major 7 Sus 2 | |
| (4, 3, 3, 4): "9", # 9 | |
| (2, 3, 2): "11.9^7", # Sus 2 Sus 4 | |
| (4, 4): "aug", # Aug | |
| } | |
| _LILY_ABSOLUTE_OCTAVE = 3 | |
| class _ImmutableIterable(tuple): | |
| def __new__(cls, *args, **kwargs): | |
| return super().__new__(cls, *args, **kwargs) | |
| class Meter(_ImmutableIterable): | |
| # Semantics: https://en.wikipedia.org/wiki/Metre_(music)#Metric_structure | |
| # Meter is defined by "pulse groups": collections of repeating patterns of accented pulses, i.e., measures | |
| # A "pulse group" has a "primary" and "secondary" subdivision | |
| # "Primary subdivision" is synonymous with "pulse" | |
| # Primary subdivision names: "duple" = 2, "triple" = 3, "quadruple" = 4, ... | |
| # Secondary subdivision names: "simple" = 2, "compound" = 3 | |
| # Here we add a tertiary subdivision which refers to the quantization | |
| # Time signatures are abstractions of meter which also take readability into account | |
| # E.g., 3/4 with sixteenth note subdivisions is (3, 2, 2), because we have a simple triple meter w/ each secondary subdivision divided twice | |
| def __new__(cls, primary, secondary, tertiary): | |
| meter = (primary, secondary, tertiary) | |
| if meter not in [(3, 2, 2), (4, 2, 2)]: | |
| raise NotImplementedError() | |
| return super().__new__(cls, meter) | |
| def as_lily(self): | |
| assert self in [(3, 2, 2), (4, 2, 2)] | |
| if self == (3, 2, 2): | |
| result = ("3", "4") | |
| else: | |
| result = ("4", "4") | |
| return result | |
| class Tempo(_ImmutableIterable): | |
| def __new__(cls, pulses_per_minute): | |
| if not isinstance(pulses_per_minute, int): | |
| raise TypeError() | |
| if pulses_per_minute <= 0: | |
| raise ValueError() | |
| return super().__new__(cls, (pulses_per_minute,)) | |
| def as_lily(self, meter): | |
| if not isinstance(meter, Meter): | |
| raise TypeError() | |
| assert meter in [(3, 2, 2), (4, 2, 2)] | |
| return ("4", str(self[0])) | |
| class Key(_ImmutableIterable): | |
| def __new__(cls, root_pc, scale_degree_pis): | |
| if not isinstance(root_pc, int): | |
| raise TypeError() | |
| if not isinstance(scale_degree_pis, tuple): | |
| raise TypeError() | |
| for pi in scale_degree_pis: | |
| if pi <= 0: | |
| raise ValueError() | |
| if sum(scale_degree_pis) >= 12: | |
| raise ValueError() | |
| return super().__new__( | |
| cls, | |
| (PitchClass(root_pc), tuple(PitchInterval(pi) for pi in scale_degree_pis)), | |
| ) | |
| def as_lily(self): | |
| enharmonics = _KEY_TO_ENHARMONICS.get(self) | |
| if enharmonics is None: | |
| raise ValueError("Unknown scale") | |
| root = self[0].as_human_pitch_name(enharmonics=enharmonics).as_lily_pitch_name() | |
| scale_name = _SCALE_DEGREES_TO_LILY_NAME.get(self[1]) | |
| assert scale_name is not None | |
| return (root, scale_name) | |
| class Note(_ImmutableIterable): | |
| def __new__(cls, pc, octave): | |
| if not isinstance(pc, int): | |
| raise TypeError() | |
| if not isinstance(octave, int): | |
| raise TypeError() | |
| return super().__new__(cls, (PitchClass(pc), int(octave))) | |
| def as_lily(self, key, octave_0=4): | |
| if not isinstance(key, Key): | |
| raise TypeError() | |
| enharmonics = _KEY_TO_ENHARMONICS.get(key) | |
| if enharmonics is None: | |
| raise ValueError() | |
| pitch_class_str = ( | |
| self[0].as_human_pitch_name(enharmonics=enharmonics).as_lily_pitch_name() | |
| ) | |
| octave = (octave_0 - _LILY_ABSOLUTE_OCTAVE) + self[1] | |
| octave_str = ("'" if octave > 0 else ",") * abs(octave) | |
| return (pitch_class_str, octave_str) | |
| def as_midi_pitch(self, octave_0=4): | |
| return 12 + (12 * (octave_0 + self[1])) + self[0] | |
| class Chord(_ImmutableIterable): | |
| def __new__(cls, root_pc, chord_degree_pis): | |
| if root_pc is None: | |
| if chord_degree_pis is not None: | |
| raise ValueError() | |
| t = (None, None) | |
| else: | |
| if not isinstance(root_pc, int): | |
| raise TypeError() | |
| if not isinstance(chord_degree_pis, tuple): | |
| raise TypeError() | |
| for pi in chord_degree_pis: | |
| if pi <= 0: | |
| raise ValueError() | |
| t = ( | |
| PitchClass(root_pc), | |
| tuple(PitchInterval(pi) for pi in chord_degree_pis), | |
| ) | |
| return super().__new__(cls, t) | |
| def as_lily(self, key): | |
| if not isinstance(key, Key): | |
| raise TypeError() | |
| if self[0] is None: | |
| assert self[1] is None | |
| result = ("r", "") | |
| else: | |
| enharmonics = _KEY_TO_ENHARMONICS.get(key) | |
| if enharmonics is None: | |
| raise ValueError() | |
| root_str = ( | |
| self[0] | |
| .as_human_pitch_name(enharmonics=enharmonics) | |
| .as_lily_pitch_name() | |
| ) | |
| result = (root_str, _CHORD_DEGREES_TO_LILY_NAME.get(self[1], self[1])) | |
| return result | |
| def as_midi_pitches(self, octave_0=3): | |
| if self[0] is None: | |
| assert self[1] is None | |
| result = [] | |
| else: | |
| chord = [self[0]] + (self[0] + np.cumsum(self[1])).tolist() | |
| result = (12 + (12 * octave_0) + np.array(chord)).tolist() | |
| return result | |
| class _InstantEvent(_ImmutableIterable): | |
| def __new__(cls, event_cls, onset, event): | |
| if not isinstance(onset, int): | |
| raise TypeError() | |
| if onset < 0: | |
| raise ValueError() | |
| event = event_cls(*event) | |
| assert isinstance(onset, int) | |
| assert isinstance(event, event_cls) | |
| assert isinstance(event, _ImmutableIterable) | |
| return super().__new__(cls, (onset, event)) | |
| class _SustainedEvent(_ImmutableIterable): | |
| def __new__(cls, event_cls, onset, duration, event): | |
| onset, event = _InstantEvent(event_cls, onset, event) | |
| if not isinstance(duration, int): | |
| raise TypeError() | |
| if duration < 0: | |
| raise ValueError() | |
| assert isinstance(onset, int) | |
| assert isinstance(duration, int) | |
| assert isinstance(event, event_cls) | |
| assert isinstance(event, _ImmutableIterable) | |
| return super().__new__(cls, (onset, duration, event)) | |
| class _InstantEventList(_ImmutableIterable): | |
| def __new__(cls, event_cls, *args): | |
| # Instantiate and sort | |
| instant_events = [_InstantEvent(event_cls, *ie) for ie in args] | |
| instant_events = sorted(instant_events, key=lambda ie: ie[0]) | |
| # Check that no two start on same offset | |
| offsets = set(ie[0] for ie in instant_events) | |
| if len(offsets) != len(instant_events): | |
| raise ValueError() | |
| # Check that all events constitute a change | |
| last_ie = None | |
| for ie in instant_events: | |
| if last_ie is not None and ie[1] == last_ie[1]: | |
| raise ValueError() | |
| last_ie = ie | |
| return super().__new__(cls, tuple(instant_events)) | |
| class _SustainedEventList(_ImmutableIterable): | |
| def __new__(cls, event_cls, *args): | |
| # Instantiate and sort | |
| sustained_events = [_SustainedEvent(event_cls, *se) for se in args] | |
| sustained_events = sorted(sustained_events, key=lambda se: se[0]) | |
| # Check that no two start on same offset | |
| offsets = set(se[0] for se in sustained_events) | |
| if len(offsets) != len(sustained_events): | |
| raise ValueError() | |
| # Check that all durations are nonzero | |
| if any(d == 0 for _, d, _ in sustained_events): | |
| raise ValueError() | |
| # Ensure monophonic | |
| for i, se in enumerate(sustained_events): | |
| if i + 1 < len(sustained_events): | |
| max_duration = sustained_events[i + 1][0] - se[0] | |
| assert max_duration > 0 | |
| if se[1] > max_duration: | |
| raise ValueError() | |
| return super().__new__(cls, tuple(sustained_events)) | |
| class _DefinedAtStartInstantEventList(_InstantEventList): | |
| def __new__(cls, event_cls, *args): | |
| instant_events = super().__new__(cls, event_cls, *args) | |
| if len(instant_events) == 0 or instant_events[0][0] != 0: | |
| raise ValueError() | |
| return instant_events | |
| class MeterChanges(_DefinedAtStartInstantEventList): | |
| def __new__(cls, *args): | |
| return super().__new__(cls, Meter, *args) | |
| class TempoChanges(_DefinedAtStartInstantEventList): | |
| def __new__(cls, *args): | |
| return super().__new__(cls, Tempo, *args) | |
| class KeyChanges(_DefinedAtStartInstantEventList): | |
| def __new__(cls, *args): | |
| return super().__new__(cls, Key, *args) | |
| class Harmony(_InstantEventList): | |
| def __new__(cls, *args): | |
| return super().__new__(cls, Chord, *args) | |
| class Melody(_SustainedEventList): | |
| def __new__(cls, *args): | |
| return super().__new__(cls, Note, *args) | |