+34
-75
@@ -1,16 +1,18 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from fractions import Fraction
|
||||
from pathlib import Path
|
||||
import fractions
|
||||
import pathlib
|
||||
|
||||
from generator.guitar import validate_tuning
|
||||
from generator.model import beat_lengths, fraction, make_event
|
||||
from generator.pitch import concert_document, shift_pitch
|
||||
from generator.score import copy_source_voices, make_measure, slugify, source_staff
|
||||
import generator.accompaniment
|
||||
import generator.guitar
|
||||
import generator.model
|
||||
import generator.pitch
|
||||
import generator.rhythm
|
||||
import generator.score
|
||||
|
||||
PART_TYPES = ('acoustic', 'electric', 'vocal')
|
||||
|
||||
def source_paths(directory: Path, *, practice: bool = False) -> list[Path]:
|
||||
def source_paths(directory: pathlib.Path, *, practice: bool = False) -> list[pathlib.Path]:
|
||||
if not directory.is_dir():
|
||||
raise ValueError(f'{"practice" if practice else "performance"} source directory not found: {directory}')
|
||||
paths = sorted(directory.rglob('*.xml'))
|
||||
@@ -21,7 +23,7 @@ def source_paths(directory: Path, *, practice: bool = False) -> list[Path]:
|
||||
raise ValueError(f'{path}: practice sources require a four-digit filename directly under data/practice')
|
||||
elif path.parent != directory:
|
||||
raise ValueError(f'{path}: performance sources belong directly under data/songs')
|
||||
identity = path.stem if practice else slugify(path.stem)
|
||||
identity = path.stem if practice else 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
|
||||
@@ -29,81 +31,38 @@ def source_paths(directory: Path, *, practice: bool = False) -> list[Path]:
|
||||
|
||||
def prepare_document(document: dict, *, practice: bool = False) -> dict:
|
||||
validate_source(document, practice=practice)
|
||||
return concert_document(document, octave_transposing=True)
|
||||
|
||||
def propagated_chords(measures: list[dict], *, include_optional: bool = True) -> list[dict | None]:
|
||||
result = []
|
||||
current = None
|
||||
for measure in measures:
|
||||
result.append(current)
|
||||
for h in measure.get('harmonies', []):
|
||||
if include_optional or not h.get('chord_optional'):
|
||||
current = h
|
||||
return result
|
||||
|
||||
def active_chord(measure: dict, onset: Fraction, previous: dict | None) -> dict | None:
|
||||
active = previous
|
||||
for h in measure['harmonies']:
|
||||
if fraction(h['onset']) > onset:
|
||||
break
|
||||
active = h
|
||||
return active if active and active['kind'] != 'none' else None
|
||||
return generator.pitch.concert_document(document, octave_transposing=True)
|
||||
|
||||
def generated_voice(measure: dict, role: str, previous: dict | None) -> list[dict]:
|
||||
length = fraction(measure['duration'])
|
||||
length = generator.model.fraction(measure['duration'])
|
||||
if role == 'rest':
|
||||
return [make_event('rest', Fraction(), length, 'rest', written={'type': 'whole', 'dots': 0}) | {'measure_rest': True}]
|
||||
common_time = measure['time_signature'] == '4/4' and not any('meter' in a for a in measure.get('attributes', []))
|
||||
if role == 'rhythm' and common_time:
|
||||
attacks = [(Fraction(n, 2), Fraction(1, 2)) for n in (0, 3, 4, 5, 7)]
|
||||
elif role == 'accompaniment':
|
||||
boundaries = sorted({Fraction(), length} | {fraction(h['onset']) for h in measure['harmonies'] if fraction(h['onset']) < length})
|
||||
attacks = [(start, end - start) for start, end in zip(boundaries, boundaries[1:])]
|
||||
else:
|
||||
attacks = []
|
||||
states = [(Fraction(), measure['meter'])]
|
||||
states.extend((fraction(a['onset']), a['meter']) for a in measure.get('attributes', []) if 'meter' in a)
|
||||
for index, (onset, meter) in enumerate(states):
|
||||
end = states[index + 1][0] if index + 1 < len(states) else length
|
||||
beats = beat_lengths(meter)
|
||||
beat_index = 0
|
||||
while onset < end:
|
||||
beat = min(beats[beat_index % len(beats)] if beats else end - onset, end - onset)
|
||||
duration = beat if role == 'slash' or not beats else min(beat / 2, Fraction(1, 2))
|
||||
attacks.append((onset, duration))
|
||||
onset += beat
|
||||
beat_index += 1
|
||||
return [generator.model.make_event('rest', fractions.Fraction(), length, 'rest', written={'type': 'whole', 'dots': 0}) | {'measure_rest': True}]
|
||||
if role != 'slash':
|
||||
raise ValueError(f'{role} requires a preplanned rhythm')
|
||||
events = []
|
||||
cursor = Fraction()
|
||||
for onset, duration in attacks:
|
||||
if onset >= length:
|
||||
break
|
||||
if onset > cursor:
|
||||
events.append(make_event(f'rest-{len(events)}', cursor, onset - cursor, 'rest'))
|
||||
duration = min(duration, length - onset)
|
||||
chord = active_chord(measure, onset, previous)
|
||||
if role == 'slash':
|
||||
event = make_event(f'slash-{len(events)}', onset, duration, 'note')
|
||||
states = [(fractions.Fraction(), measure['meter'])]
|
||||
states.extend((generator.model.fraction(a['onset']), a['meter']) for a in measure.get('attributes', []) if 'meter' in a)
|
||||
for index, (onset, meter) in enumerate(states):
|
||||
end = states[index + 1][0] if index + 1 < len(states) else length
|
||||
beats = generator.model.beat_lengths(meter)
|
||||
beat_index = 0
|
||||
while onset < end:
|
||||
duration = min(beats[beat_index % len(beats)] if beats else end - onset, end - onset)
|
||||
event = generator.model.make_event(f'slash-{len(events)}', onset, duration, 'note')
|
||||
event['notehead'] = 'slash'
|
||||
elif not chord:
|
||||
event = make_event(f'rest-{len(events)}', onset, duration, 'rest')
|
||||
else:
|
||||
event = make_event(f'chord-{len(events)}', onset, duration, 'note')
|
||||
event['_harmony'] = chord
|
||||
events.append(event)
|
||||
cursor = onset + duration
|
||||
if cursor < length:
|
||||
events.append(make_event(f'rest-{len(events)}', cursor, length - cursor, 'rest'))
|
||||
events.append(event)
|
||||
onset += duration
|
||||
beat_index += 1
|
||||
return events
|
||||
|
||||
def validate_source(document: dict, *, practice: bool = False) -> None:
|
||||
voices = set()
|
||||
for measure in source_staff(document)['measures']:
|
||||
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'):
|
||||
raise ValueError(f'{measure["id"]}: parts require a pitched, non-TAB source')
|
||||
if 'staff_details' in state:
|
||||
validate_tuning(state, measure['id'])
|
||||
generator.guitar.validate_tuning(state, measure['id'])
|
||||
for voice in measure['voices']:
|
||||
voices.add(voice['id'])
|
||||
for note in voice['notes']:
|
||||
@@ -114,13 +73,13 @@ def validate_source(document: dict, *, practice: bool = False) -> None:
|
||||
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 = '') -> dict:
|
||||
voices = copy_source_voices(source, strip_lyrics=role == 'melody') if role in ('vocal', 'melody') else None
|
||||
notes = [n for v in voices for n in v['notes']] if voices else generated_voice(source, role, previous)
|
||||
def build_measure(source: dict, identifier: str, role: str, part: str, previous: dict | None, label: str = '', rhythm: generator.rhythm.Bar | None = None) -> dict:
|
||||
voices = generator.score.copy_source_voices(source, strip_lyrics=role == 'melody') if role in ('vocal', 'melody') else None
|
||||
notes = [n for v in voices for n in v['notes']] if voices else generator.accompaniment.realize(source, rhythm, previous) if rhythm is not None else generated_voice(source, role, previous)
|
||||
if role in ('melody', 'vocal'):
|
||||
for note in notes:
|
||||
note['pitches'] = [shift_pitch(p, 7, 12) for p in note['pitches']]
|
||||
result = make_measure(identifier, source, [] if voices else notes, section_label=label)
|
||||
note['pitches'] = [generator.pitch.shift_pitch(p, 7, 12) for p in note['pitches']]
|
||||
result = generator.score.make_measure(identifier, source, [] if voices else notes, section_label=label)
|
||||
if voices:
|
||||
result['voices'] = voices
|
||||
result['transpose'] = {'diatonic': 0, 'chromatic': 0, 'octave_change': -1, 'explicit': True}
|
||||
|
||||
Reference in New Issue
Block a user