+32
-32
@@ -1,56 +1,56 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import copy
|
||||
from fractions import Fraction
|
||||
from typing import Any
|
||||
import fractions
|
||||
import typing
|
||||
|
||||
MAX_INTEGER = (1 << 63) - 1
|
||||
NOTE_TYPES = ('maxima', 'long', 'breve', 'whole', 'half', 'quarter', 'eighth', '16th', '32nd', '64th', '128th', '256th', '512th', '1024th')
|
||||
TYPE_LENGTHS = {name: Fraction(32, 2 ** index) for index, name in enumerate(NOTE_TYPES)}
|
||||
TYPE_LENGTHS = {name: fractions.Fraction(32, 2 ** index) for index, name in enumerate(NOTE_TYPES)}
|
||||
NATURAL = {'c': 0, 'd': 2, 'e': 4, 'f': 5, 'g': 7, 'a': 9, 'b': 11}
|
||||
LETTERS = 'cdefgab'
|
||||
MODE_STEPS = {'major': 0, 'ionian': 0, 'dorian': 1, 'phrygian': 2, 'lydian': 3, 'mixolydian': 4, 'minor': 5, 'aeolian': 5, 'locrian': 6}
|
||||
ACCIDENTALS = {'natural': Fraction(0), 'sharp': Fraction(1), 'flat': Fraction(-1), 'double-sharp': Fraction(2), 'sharp-sharp': Fraction(2), 'flat-flat': Fraction(-2), 'triple-sharp': Fraction(3), 'triple-flat': Fraction(-3), 'natural-sharp': Fraction(1), 'natural-flat': Fraction(-1), 'quarter-sharp': Fraction(1, 2), 'quarter-flat': Fraction(-1, 2), 'three-quarters-sharp': Fraction(3, 2), 'three-quarters-flat': Fraction(-3, 2)}
|
||||
ACCIDENTALS = {'natural': fractions.Fraction(0), 'sharp': fractions.Fraction(1), 'flat': fractions.Fraction(-1), 'double-sharp': fractions.Fraction(2), 'sharp-sharp': fractions.Fraction(2), 'flat-flat': fractions.Fraction(-2), 'triple-sharp': fractions.Fraction(3), 'triple-flat': fractions.Fraction(-3), 'natural-sharp': fractions.Fraction(1), 'natural-flat': fractions.Fraction(-1), 'quarter-sharp': fractions.Fraction(1, 2), 'quarter-flat': fractions.Fraction(-1, 2), 'three-quarters-sharp': fractions.Fraction(3, 2), 'three-quarters-flat': fractions.Fraction(-3, 2)}
|
||||
|
||||
def accidental_name(value: Fraction) -> str:
|
||||
def accidental_name(value: fractions.Fraction) -> str:
|
||||
for name, amount in ACCIDENTALS.items():
|
||||
if amount == value:
|
||||
return name
|
||||
return 'other'
|
||||
|
||||
def rational(value: Fraction | int) -> dict[str, int]:
|
||||
value = Fraction(value)
|
||||
def rational(value: fractions.Fraction | int) -> dict[str, int]:
|
||||
value = fractions.Fraction(value)
|
||||
if abs(value.numerator) > MAX_INTEGER or value.denominator > MAX_INTEGER:
|
||||
raise ValueError('musical time exceeds the supported 64-bit rational range')
|
||||
return {'numerator': value.numerator, 'denominator': value.denominator}
|
||||
|
||||
def fraction(value: Any) -> Fraction:
|
||||
def fraction(value: typing.Any) -> fractions.Fraction:
|
||||
if not isinstance(value, dict) or set(value) != {'numerator', 'denominator'}:
|
||||
raise ValueError('musical time must be a numerator/denominator object')
|
||||
n, d = value['numerator'], value['denominator']
|
||||
if type(n) is not int or type(d) is not int or d <= 0 or abs(n) > MAX_INTEGER or d > MAX_INTEGER:
|
||||
raise ValueError('invalid musical time')
|
||||
result = Fraction(n, d)
|
||||
result = fractions.Fraction(n, d)
|
||||
if rational(result) != value:
|
||||
raise ValueError('musical time must be reduced')
|
||||
return result
|
||||
|
||||
def written_length(written: dict) -> Fraction:
|
||||
def written_length(written: dict) -> fractions.Fraction:
|
||||
kind, dots = written['type'], written.get('dots', 0)
|
||||
if kind not in TYPE_LENGTHS or type(dots) is not int or not 0 <= dots <= 16:
|
||||
raise ValueError('invalid written duration or augmentation-dot count (maximum 16)')
|
||||
return TYPE_LENGTHS[kind] * (2 - Fraction(1, 2 ** dots))
|
||||
return TYPE_LENGTHS[kind] * (2 - fractions.Fraction(1, 2 ** dots))
|
||||
|
||||
def written_for(length: Fraction) -> dict:
|
||||
def written_for(length: fractions.Fraction) -> dict:
|
||||
for name, base in TYPE_LENGTHS.items():
|
||||
for dots in range(17):
|
||||
if length == base * (2 - Fraction(1, 2 ** dots)):
|
||||
if length == base * (2 - fractions.Fraction(1, 2 ** dots)):
|
||||
return {'type': name, 'dots': dots}
|
||||
# Generated rational spans need an explicit time modification, not rounded ticks.
|
||||
base = next((name for name, size in TYPE_LENGTHS.items() if size <= length), '1024th')
|
||||
return {'type': base, 'dots': 0, 'ratio': rational(length / TYPE_LENGTHS[base])}
|
||||
|
||||
def metric_length(written: dict) -> Fraction:
|
||||
def metric_length(written: dict) -> fractions.Fraction:
|
||||
ratio = fraction(written.get('ratio', rational(1)))
|
||||
if ratio <= 0:
|
||||
raise ValueError('written duration ratio must be positive')
|
||||
@@ -58,10 +58,10 @@ def metric_length(written: dict) -> Fraction:
|
||||
written_length(written['normal'])
|
||||
return written_length(written) * ratio
|
||||
|
||||
def alteration(value: int | dict) -> Fraction:
|
||||
return Fraction(value) if type(value) is int else fraction(value)
|
||||
def alteration(value: int | dict) -> fractions.Fraction:
|
||||
return fractions.Fraction(value) if type(value) is int else fraction(value)
|
||||
|
||||
def pitch_number(pitch: dict) -> Fraction:
|
||||
def pitch_number(pitch: dict) -> fractions.Fraction:
|
||||
letter = pitch['letter'].lower()
|
||||
alter, octave = alteration(pitch.get('alter', 0)), pitch['octave']
|
||||
if letter not in NATURAL or type(octave) is not int or not -4 <= octave <= 16:
|
||||
@@ -98,24 +98,24 @@ def key_label(key: dict) -> str:
|
||||
label = pitch_name(key['tonic'])
|
||||
return label if key['mode'] in ('major', 'ionian') else label + ' ' + key['mode']
|
||||
|
||||
def meter_length(meter: dict) -> Fraction:
|
||||
return sum((Fraction(sum(group['beats']) * 4, group['unit']) for group in meter.get('groups', [])), Fraction())
|
||||
def meter_length(meter: dict) -> fractions.Fraction:
|
||||
return sum((fractions.Fraction(sum(group['beats']) * 4, group['unit']) for group in meter.get('groups', [])), fractions.Fraction())
|
||||
|
||||
def meter_label(meter: dict) -> str:
|
||||
if meter.get('unmetered'):
|
||||
return 'senza misura'
|
||||
return '+'.join('+'.join(map(str, group['beats'])) + '/' + str(group['unit']) for group in meter['groups'])
|
||||
|
||||
def beat_lengths(meter: dict) -> list[Fraction]:
|
||||
def beat_lengths(meter: dict) -> list[fractions.Fraction]:
|
||||
result = []
|
||||
for group in meter.get('groups', []):
|
||||
beats, unit = group['beats'], group['unit']
|
||||
if len(beats) > 1:
|
||||
result.extend(Fraction(b * 4, unit) for b in beats)
|
||||
result.extend(fractions.Fraction(b * 4, unit) for b in beats)
|
||||
elif beats[0] > 3 and beats[0] % 3 == 0 and unit >= 8:
|
||||
result.extend([Fraction(12, unit)] * (beats[0] // 3))
|
||||
result.extend([fractions.Fraction(12, unit)] * (beats[0] // 3))
|
||||
else:
|
||||
result.extend([Fraction(4, unit)] * beats[0])
|
||||
result.extend([fractions.Fraction(4, unit)] * beats[0])
|
||||
return result
|
||||
|
||||
def validate_meter(meter: dict) -> None:
|
||||
@@ -128,7 +128,7 @@ def validate_meter(meter: dict) -> None:
|
||||
if any(type(b) is not int or not 0 < b <= 1024 for b in group['beats']):
|
||||
raise ValueError('meter count must be between 1 and 1024')
|
||||
|
||||
def make_event(identifier: str, onset: Fraction, duration: Fraction, kind: str, pitches: list[dict] | None = None, written: dict | None = None) -> dict:
|
||||
def make_event(identifier: str, onset: fractions.Fraction, duration: fractions.Fraction, kind: str, pitches: list[dict] | None = None, written: dict | None = None) -> dict:
|
||||
if duration <= 0:
|
||||
raise ValueError('generated event duration must be positive')
|
||||
notation = written or written_for(duration)
|
||||
@@ -155,9 +155,9 @@ def validate_document(document: dict) -> None:
|
||||
raise ValueError('score instruments require unique nonempty IDs')
|
||||
ids: set[str] = set()
|
||||
events: dict[str, dict] = {}
|
||||
tones: dict[str, tuple[dict, Fraction, Fraction, str]] = {}
|
||||
tones: dict[str, tuple[dict, fractions.Fraction, fractions.Fraction, str]] = {}
|
||||
ownership: dict[str, str] = {}
|
||||
absolute = Fraction()
|
||||
absolute = fractions.Fraction()
|
||||
for measure in staves[0]['measures']:
|
||||
if {'repeats', 'endings', 'initial_attributes'} & measure.keys():
|
||||
raise ValueError('repeat notation is unsupported; regenerate scores from music written out in reading order')
|
||||
@@ -167,7 +167,7 @@ def validate_document(document: dict) -> None:
|
||||
voices = measure.get('voices', [])
|
||||
if not voices or len({v['id'] for v in voices}) != len(voices) or any(not v['id'] for v in voices):
|
||||
raise ValueError('measure requires uniquely identified voice lanes')
|
||||
previous_ends = {v['id']: Fraction() for v in voices}
|
||||
previous_ends = {v['id']: fractions.Fraction() for v in voices}
|
||||
for voice, note in ((v, n) for v in voices for n in v['notes']):
|
||||
if any({'sound_ties', 'printed_ties'} & pitch.keys() for pitch in note['pitches']):
|
||||
raise ValueError('pass-specific tie metadata is unsupported; regenerate scores')
|
||||
@@ -206,17 +206,17 @@ def validate_document(document: dict) -> None:
|
||||
else:
|
||||
pitch_number(p)
|
||||
if staves[0].get('tablature'):
|
||||
from generator.guitar import MAX_FRET, MIN_FRET, STANDARD_TUNING
|
||||
from generator.pitch import sounding_pitch
|
||||
import generator.guitar
|
||||
import generator.pitch
|
||||
tab = p.get('tablature', {})
|
||||
string, fret = tab.get('string'), tab.get('fret')
|
||||
if type(string) is not int or type(fret) is not int or string not in dict(STANDARD_TUNING) or not MIN_FRET <= fret <= MAX_FRET:
|
||||
if type(string) is not int or type(fret) is not int or string not in dict(generator.guitar.STANDARD_TUNING) or not generator.guitar.MIN_FRET <= fret <= generator.guitar.MAX_FRET:
|
||||
raise ValueError(f'{p["id"]}: invalid generated tablature position')
|
||||
state = dict(measure)
|
||||
for change in measure.get('attributes', []):
|
||||
if fraction(change['onset']) <= onset:
|
||||
state.update(change)
|
||||
if dict(STANDARD_TUNING)[string] + fret != sounding_pitch(p, state):
|
||||
if dict(generator.guitar.STANDARD_TUNING)[string] + fret != generator.pitch.sounding_pitch(p, state):
|
||||
raise ValueError(f'{p["id"]}: tablature disagrees with sounding pitch')
|
||||
pd = fraction(p['duration'])
|
||||
if pd < 0 or (pd == 0 and not note.get('grace')) or pd > duration or not p['id'] or p['id'] in tones:
|
||||
@@ -269,7 +269,7 @@ def validate_document(document: dict) -> None:
|
||||
span_ids.add(span['id'])
|
||||
for note in events.values():
|
||||
for verse in note.get('lyrics', []):
|
||||
start = tones[note['pitches'][0]['id']][1] if note['pitches'] else Fraction()
|
||||
start = tones[note['pitches'][0]['id']][1] if note['pitches'] else fractions.Fraction()
|
||||
if 'extend_to' in verse and not start < fraction(verse['extend_to']) <= absolute:
|
||||
raise ValueError('lyric extension endpoint is outside the score')
|
||||
if any(t not in span_ids for t in note.get('tuplet_ids', [])):
|
||||
|
||||
Reference in New Issue
Block a user