pachet's picture
Deploy Theme Lab Docker app
187bf9a
Raw
History Blame Contribute Delete
3 kB
"""Common symbols and musical constants for generation engines."""
from __future__ import annotations
from dataclasses import dataclass
PITCH_CLASS = {
"C": 0,
"B#": 0,
"C#": 1,
"Db": 1,
"D": 2,
"D#": 3,
"Eb": 3,
"E": 4,
"Fb": 4,
"E#": 5,
"F": 5,
"F#": 6,
"Gb": 6,
"G": 7,
"G#": 8,
"Ab": 8,
"A": 9,
"A#": 10,
"Bb": 10,
"B": 11,
"Cb": 11,
}
PC_TO_SHARP_NAME = ["C", "C#", "D", "D#", "E", "F", "F#", "G", "G#", "A", "A#", "B"]
COMMON_DURATION_BEATS = {
"64th": 0.0625,
"32nd": 0.125,
"16th triplet": 1 / 6,
"16th": 0.25,
"eighth triplet": 1 / 3,
"dotted 16th": 0.375,
"eighth": 0.5,
"quarter triplet": 2 / 3,
"dotted eighth": 0.75,
"quarter": 1.0,
"dotted quarter": 1.5,
"half": 2.0,
"dotted half": 3.0,
"whole": 4.0,
}
ABC_DURATION = {
"64th": "/4",
"32nd": "/2",
"16th triplet": "/3",
"16th": "",
"eighth triplet": "4/3",
"dotted 16th": "3/2",
"eighth": "2",
"quarter triplet": "8/3",
"dotted eighth": "3",
"quarter": "4",
"dotted quarter": "6",
"half": "8",
"dotted half": "12",
"whole": "16",
}
DEFAULT_DURATIONS = {
"16th",
"eighth",
"quarter",
"half",
"dotted eighth",
"dotted quarter",
"eighth triplet",
"16th triplet",
}
START_SYMBOL = "__THEME_GENERATION_START__"
@dataclass(frozen=True)
class Symbol:
"""A key-relative pitch class plus notated duration."""
rpc: int
duration: str
def __str__(self) -> str:
return f"{self.rpc}:{self.duration}"
def duration_options(min_duration: str, duration_grid: str, allow_triplets: bool) -> set[str]:
if min_duration not in COMMON_DURATION_BEATS:
labels = ", ".join(sorted(COMMON_DURATION_BEATS))
raise ValueError(f"Unknown duration {min_duration!r}; choose one of: {labels}")
if duration_grid not in COMMON_DURATION_BEATS:
labels = ", ".join(sorted(COMMON_DURATION_BEATS))
raise ValueError(f"Unknown duration grid {duration_grid!r}; choose one of: {labels}")
min_beats = COMMON_DURATION_BEATS[min_duration]
grid_beats = COMMON_DURATION_BEATS[duration_grid]
allowed = set()
for duration in DEFAULT_DURATIONS:
duration_beats = COMMON_DURATION_BEATS[duration]
on_grid = abs((duration_beats / grid_beats) - round(duration_beats / grid_beats)) < 1e-9
regular_triplet = allow_triplets and "triplet" in duration
if duration_beats >= min_beats and (on_grid or regular_triplet):
allowed.add(duration)
return allowed
def duration_units(duration: str) -> int:
return round(COMMON_DURATION_BEATS[duration] * 12)
def is_triplet_duration(duration: str) -> bool:
return "triplet" in duration
def parse_key_root(raw_key: str | None) -> int | None:
if not raw_key:
return None
root = raw_key.split("%", 1)[0].strip().replace("m", "")
return PITCH_CLASS.get(root)