+7
-23
@@ -12,14 +12,12 @@ 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}
|
||||
|
||||
|
||||
def rational(value: Fraction | int) -> dict[str, int]:
|
||||
value = 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:
|
||||
if not isinstance(value, dict) or set(value) != {'numerator', 'denominator'}:
|
||||
raise ValueError('musical time must be a numerator/denominator object')
|
||||
@@ -31,14 +29,12 @@ def fraction(value: Any) -> Fraction:
|
||||
raise ValueError('musical time must be reduced')
|
||||
return result
|
||||
|
||||
|
||||
def written_length(written: dict) -> 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))
|
||||
|
||||
|
||||
def written_for(length: Fraction) -> dict:
|
||||
for name, base in TYPE_LENGTHS.items():
|
||||
for dots in range(17):
|
||||
@@ -48,7 +44,6 @@ def written_for(length: Fraction) -> dict:
|
||||
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:
|
||||
ratio = fraction(written.get('ratio', rational(1)))
|
||||
if ratio <= 0:
|
||||
@@ -57,7 +52,6 @@ def metric_length(written: dict) -> Fraction:
|
||||
written_length(written['normal'])
|
||||
return written_length(written) * ratio
|
||||
|
||||
|
||||
def pitch_number(pitch: dict) -> int:
|
||||
letter = pitch['letter'].lower()
|
||||
alter, octave = pitch.get('alter', 0), pitch['octave']
|
||||
@@ -65,18 +59,15 @@ def pitch_number(pitch: dict) -> int:
|
||||
raise ValueError('invalid spelled pitch')
|
||||
return 12 * (octave + 1) + NATURAL[letter] + alter
|
||||
|
||||
|
||||
def pitch_name(pitch: dict) -> str:
|
||||
alter = pitch.get('alter', 0)
|
||||
return pitch['letter'].upper() + ('#' * alter if alter > 0 else 'b' * -alter)
|
||||
|
||||
|
||||
def parse_pitch_name(name: str) -> dict:
|
||||
if not name or name[0].lower() not in NATURAL or any(c not in '#b' for c in name[1:]):
|
||||
raise ValueError(f'invalid pitch name: {name!r}')
|
||||
return {'letter': name[0].lower(), 'alter': name[1:].count('#') - name[1:].count('b')}
|
||||
|
||||
|
||||
def key_from_fifths(fifths: int, mode: str = 'major') -> dict:
|
||||
if mode not in MODE_STEPS and mode != 'none':
|
||||
raise ValueError(f'unsupported mode: {mode}')
|
||||
@@ -91,24 +82,20 @@ def key_from_fifths(fifths: int, mode: str = 'major') -> dict:
|
||||
tonic = {'letter': letter, 'alter': signature[LETTERS.index(letter)]}
|
||||
return {'fifths': fifths, 'mode': mode, 'tonic': tonic if mode != 'none' else None, 'alterations': signature}
|
||||
|
||||
|
||||
def key_label(key: dict) -> str:
|
||||
if not key.get('tonic'):
|
||||
return 'No key'
|
||||
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_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]:
|
||||
result = []
|
||||
for group in meter.get('groups', []):
|
||||
@@ -121,7 +108,6 @@ def beat_lengths(meter: dict) -> list[Fraction]:
|
||||
result.extend([Fraction(4, unit)] * beats[0])
|
||||
return result
|
||||
|
||||
|
||||
def validate_meter(meter: dict) -> None:
|
||||
groups = meter.get('groups', [])
|
||||
if not groups and not meter.get('unmetered'):
|
||||
@@ -132,17 +118,15 @@ 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:
|
||||
if duration <= 0:
|
||||
raise ValueError('generated event duration must be positive')
|
||||
notation = written or written_for(duration)
|
||||
event = {'id': identifier, 'kind': kind, 'onset': rational(onset), 'duration': rational(duration), 'written': notation, 'hidden': False, 'pitches': []}
|
||||
for i, p in enumerate(pitches or []):
|
||||
event['pitches'].append({**copy.deepcopy(p), 'id': f'{identifier}-p{i+1}', 'duration': rational(duration), 'written': copy.deepcopy(notation)})
|
||||
event['pitches'].append({**copy.deepcopy(p), 'id': f'{identifier}-p{i + 1}', 'duration': rational(duration), 'written': copy.deepcopy(notation)})
|
||||
return event
|
||||
|
||||
|
||||
def validate_document(document: dict) -> None:
|
||||
if document.get('schema_version') != SCHEMA_VERSION:
|
||||
raise ValueError('unsupported score schema; regenerate score assets with the current generator')
|
||||
@@ -203,7 +187,7 @@ def validate_document(document: dict) -> None:
|
||||
validate_meter(state['meter'])
|
||||
if 'key' in state and (len(state['key']['alterations']) != 7 or any(type(a) is not int or not -3 <= a <= 3 for a in state['key']['alterations'])):
|
||||
raise ValueError('key alterations must contain seven integers from -3 through 3')
|
||||
if 'clef' in state and (state['clef']['sign'] not in ('G','F','C') or not 1 <= state['clef']['line'] <= 5 or not -3 <= state['clef']['octave_change'] <= 3):
|
||||
if 'clef' in state and (state['clef']['sign'] not in ('G', 'F', 'C') or not 1 <= state['clef']['line'] <= 5 or not -3 <= state['clef']['octave_change'] <= 3):
|
||||
raise ValueError('invalid pitched clef')
|
||||
absolute += length
|
||||
for identifier, (p, start, duration) in tones.items():
|
||||
@@ -217,26 +201,26 @@ def validate_document(document: dict) -> None:
|
||||
if source not in tones or tones[source][0].get('tie_to') != identifier:
|
||||
raise ValueError(f'dangling incoming tie: {identifier}')
|
||||
span_ids = set()
|
||||
event_order = {identifier:index for index, identifier in enumerate(events)}
|
||||
event_order = {identifier: index for index, identifier in enumerate(events)}
|
||||
for span in staves[0].get('tuplets', []):
|
||||
if not span['id'] or span['id'] in span_ids or span['start'] not in ids or span['end'] not in ids or not 0 < span['note_count'] <= 1000000 or not 0 < span['space_count'] <= 1000000 or not 0 <= span['depth'] <= 16:
|
||||
raise ValueError('invalid tuplet span')
|
||||
members = [n['id'] for n in events.values() if span['id'] in n.get('tuplet_ids', [])]
|
||||
if not members or members[0] != span['start'] or members[-1] != span['end'] or event_order[span['start']] > event_order[span['end']]:
|
||||
raise ValueError('tuplet endpoints disagree with member events')
|
||||
if span.get('bracket', 'auto') not in ('yes','no','auto') or span.get('show_number','actual') not in ('none','actual','both'):
|
||||
if span.get('bracket', 'auto') not in ('yes', 'no', 'auto') or span.get('show_number', 'actual') not in ('none', 'actual', 'both'):
|
||||
raise ValueError('invalid tuplet appearance')
|
||||
span_ids.add(span['id'])
|
||||
for note in events.values():
|
||||
if any(t not in span_ids for t in note.get('tuplet_ids', [])):
|
||||
raise ValueError('event refers to an unknown tuplet')
|
||||
if tremolo := note.get('tremolo'):
|
||||
if tremolo['type'] not in ('single','start','stop') or type(tremolo['marks']) is not int or not 0 <= tremolo['marks'] <= 8:
|
||||
if tremolo['type'] not in ('single', 'start', 'stop') or type(tremolo['marks']) is not int or not 0 <= tremolo['marks'] <= 8:
|
||||
raise ValueError('invalid measured tremolo')
|
||||
if tremolo['type'] in ('start','stop'):
|
||||
if tremolo['type'] in ('start', 'stop'):
|
||||
other = events.get(tremolo.get('partner'))
|
||||
expected_type = 'stop' if tremolo['type'] == 'start' else 'start'
|
||||
if not other or other.get('tremolo',{}).get('partner') != note['id'] or other['tremolo']['type'] != expected_type or other['tremolo']['marks'] != tremolo['marks']:
|
||||
if not other or other.get('tremolo', {}).get('partner') != note['id'] or other['tremolo']['type'] != expected_type or other['tremolo']['marks'] != tremolo['marks']:
|
||||
raise ValueError('invalid paired tremolo')
|
||||
for span in staves[0].get('octave_shifts', []):
|
||||
if not 0 <= fraction(span['start']) < fraction(span['end']) <= absolute or not -3 <= span['octaves'] <= 3:
|
||||
|
||||
Reference in New Issue
Block a user