Improve Catalog
ci / checks (push) Successful in 46m42s

This commit is contained in:
khodowany
2026-09-27 11:03:43 -05:00
parent a416a80dd9
commit 142d843fac
418 changed files with 440 additions and 324488 deletions
+16 -20
View File
@@ -12,28 +12,25 @@ import generator.score
PART_TYPES = ('acoustic', 'electric', 'vocal')
def source_paths(directory: pathlib.Path, *, practice: bool = False) -> list[pathlib.Path]:
def source_paths(directory: pathlib.Path) -> list[pathlib.Path]:
if not directory.is_dir():
raise ValueError(f'{"practice" if practice else "performance"} source directory not found: {directory}')
raise ValueError(f'performance source directory not found: {directory}')
paths = sorted(directory.rglob('*.xml'))
identities = {}
for path in paths:
if practice:
if path.parent != directory or len(path.stem) != 4 or not path.stem.isascii() or not path.stem.isdecimal():
raise ValueError(f'{path}: practice sources require a four-digit filename directly under data/practice')
elif path.parent != directory:
if path.parent != directory:
raise ValueError(f'{path}: performance sources belong directly under data/songs')
identity = path.stem if practice else generator.score.slugify(path.stem)
identity = generator.score.slugify(path.stem)
if identity in identities:
raise ValueError(f'{identities[identity]} and {path}: conflicting output path {identity}-acoustic.json')
identities[identity] = path
return paths
def prepare_document(document: dict, *, practice: bool = False) -> dict:
validate_source(document, practice=practice)
def prepare_document(document: dict) -> dict:
validate_source(document)
return generator.pitch.concert_document(document, octave_transposing=True)
def generated_voice(measure: dict, role: str, previous: dict | None) -> list[dict]:
def generated_voice(measure: dict, role: str) -> list[dict]:
length = generator.model.fraction(measure['duration'])
if role == 'rest':
return [generator.model.make_event('rest', fractions.Fraction(), length, 'rest', written={'type': 'whole', 'dots': 0}) | {'measure_rest': True}]
@@ -55,8 +52,7 @@ def generated_voice(measure: dict, role: str, previous: dict | None) -> list[dic
beat_index += 1
return events
def validate_source(document: dict, *, practice: bool = False) -> None:
voices = set()
def validate_source(document: dict) -> None:
for measure in generator.score.source_staff(document)['measures']:
for state in [measure, *measure.get('attributes', [])]:
if state.get('clef', {}).get('sign') in ('TAB', 'percussion'):
@@ -64,26 +60,26 @@ def validate_source(document: dict, *, practice: bool = False) -> None:
if 'staff_details' in state:
generator.guitar.validate_tuning(state, measure['id'])
for voice in measure['voices']:
voices.add(voice['id'])
for note in voice['notes']:
if practice and len(note['pitches']) > 1:
raise ValueError(f'{measure["id"]}: practice requires a monophonic melody, not chord stacks')
if any('tablature' in p or 'unpitched' in p for p in note['pitches']):
raise ValueError(f'{measure["id"]}: authored TAB and percussion are unsupported')
if practice and len(voices) > 1:
raise ValueError('practice requires one monophonic voice')
def build_measure(source: dict, identifier: str, role: str, part: str, previous: dict | None, label: str = '', rhythm: generator.rhythm.Bar | tuple[generator.rhythm.Bar, generator.rhythm.Bar] | None = None, *, melody_octave: bool = False) -> dict:
def build_measure(source: dict, identifier: str, role: str, part: str, previous: dict | None, label: str = '', rhythm: generator.rhythm.Bar | tuple[generator.rhythm.Bar, generator.rhythm.Bar] | None = None) -> dict:
voices = generator.score.copy_source_voices(source, strip_lyrics=role == 'melody') if role in ('vocal', 'melody') else None
if role == 'accompaniment' and part == 'acoustic':
voices = [{'id': lane, 'notes': generator.accompaniment.realize(source, bar, previous, lane=lane)} for lane, bar in zip(('upper', 'bass'), (rhythm[1], rhythm[0]))]
for voice in voices:
for note in voice['notes']:
note.update(voice=voice['id'], stem='down' if voice['id'] == 'bass' else 'up')
notes = [n for v in voices for n in v['notes']] if voices else generator.accompaniment.realize(source, rhythm, previous, lane='electric') if rhythm is not None else generated_voice(source, role, previous)
if voices:
notes = [note for voice in voices for note in voice['notes']]
elif rhythm is not None:
notes = generator.accompaniment.realize(source, rhythm, previous, lane='electric')
else:
notes = generated_voice(source, role)
if role in ('melody', 'vocal'):
for note in notes:
octaves = 2 if melody_octave and role == 'melody' else 1
octaves = 2 if role == 'melody' else 1
note['pitches'] = [generator.pitch.shift_pitch(p, 7 * octaves, 12 * octaves) for p in note['pitches']]
if role == 'melody':
note['_melody'] = True