140 lines
7.6 KiB
Python
140 lines
7.6 KiB
Python
from __future__ import annotations
|
|
|
|
import copy
|
|
import fractions
|
|
import pathlib
|
|
|
|
import generator.accompaniment
|
|
import generator.acoustic
|
|
import generator.arrange.form
|
|
import generator.guitar
|
|
import generator.melody
|
|
import generator.model
|
|
import generator.musicxml
|
|
import generator.parts
|
|
import generator.rhythm
|
|
import generator.score
|
|
|
|
FINAL_SECTION_INDEX = 6
|
|
|
|
def arrange_document(document: dict, part_type: str, *, rhythms: generator.rhythm.Library) -> dict:
|
|
document = generator.parts.prepare_document(document)
|
|
plans = plan_arrangement(document, rhythms) if part_type != 'vocal' else {}
|
|
return _arrange_document(document, part_type, plans)
|
|
|
|
def section_source(source: list[dict], final: bool) -> list[dict]:
|
|
result = copy.deepcopy(source)
|
|
for measure in result:
|
|
measure['harmonies'] = [h for h in measure['harmonies'] if not final or not h.get('chord_optional')]
|
|
for chord in measure['harmonies']:
|
|
chord.pop('chord_optional', None)
|
|
return result
|
|
|
|
def plan_arrangement(document: dict, rhythms: generator.rhythm.Library) -> dict[int, generator.accompaniment.Plan]:
|
|
source = generator.score.source_staff(document)['measures']
|
|
result = {}
|
|
for section in generator.arrange.form.full_arrangement_sections(source, 'acoustic'):
|
|
if section.role == 'slash':
|
|
continue
|
|
measures = section_source(source, section.section_index == FINAL_SECTION_INDEX)
|
|
previous = generator.accompaniment.carried_chords(measures)
|
|
start = section.source_measure_index
|
|
result[section.section_index] = generator.accompaniment.plan_section(measures[start:start + section.measure_count], rhythms, initial=previous[start], electric=section.section_index in (1, 5, 6))
|
|
return result
|
|
|
|
def _arrange_document(document: dict, part_type: str, plans: dict[int, generator.accompaniment.Plan]) -> dict:
|
|
if part_type not in generator.parts.PART_TYPES:
|
|
raise ValueError(f'unknown arrangement part: {part_type}')
|
|
staff = generator.score.source_staff(document)
|
|
source = staff['measures']
|
|
sections = generator.arrange.form.full_arrangement_sections(source, part_type)
|
|
output, tuplets, octave_shifts = [], [], []
|
|
absolute = fractions.Fraction()
|
|
source_starts = []
|
|
source_time = fractions.Fraction()
|
|
for m in source:
|
|
source_starts.append(source_time)
|
|
source_time += generator.model.fraction(m['duration'])
|
|
for section in sections:
|
|
section_measures = section_source(source, section.section_index == FINAL_SECTION_INDEX)
|
|
previous = generator.accompaniment.carried_chords(section_measures)
|
|
segment = []
|
|
for si in range(section.source_measure_index, section.source_measure_index + section.measure_count):
|
|
m = section_measures[si]
|
|
rhythm = None
|
|
if section.role == 'accompaniment':
|
|
plan = plans[section.section_index]
|
|
rhythm = (plan.acoustic_bass[len(segment)], plan.acoustic_upper[len(segment)]) if part_type == 'acoustic' else plan.electric[len(segment)]
|
|
result = generator.parts.build_measure(m, f'measure-{len(output) + len(segment) + 1}', section.role, part_type, previous[si], section.label if not segment else '', rhythm, melody_octave=True)
|
|
segment.append(result)
|
|
identity_segment = segment
|
|
segment_length = sum((generator.model.fraction(m['duration']) for m in segment), fractions.Fraction())
|
|
if section.role in ('vocal', 'melody'):
|
|
start = source_starts[section.source_measure_index]
|
|
for shift in staff.get('octave_shifts', []):
|
|
low, high = max(start, generator.model.fraction(shift['start'])), min(start + segment_length, generator.model.fraction(shift['end']))
|
|
if low < high:
|
|
octave_shifts.append({**shift, 'id': f'section-{section.section_index}-{shift["id"]}', 'start': generator.model.rational(absolute + low - start), 'end': generator.model.rational(absolute + high - start)})
|
|
if part_type == 'vocal' and section.section_index in (1, 5) and section.source_measure_index == 1 and source[0].get('implicit') and output:
|
|
pickup = generator.parts.build_measure(source[0], source[0]['id'], 'vocal', part_type, None)
|
|
destination = output[-1]
|
|
pad = generator.model.fraction(destination['duration']) - generator.model.fraction(pickup['duration'])
|
|
if pad < 0:
|
|
raise ValueError('pickup is longer than the preceding arrangement measure')
|
|
voices = generator.score.copy_source_voices(pickup, strip_lyrics=False)
|
|
for voice in voices:
|
|
for n in voice['notes']:
|
|
n['onset'] = generator.model.rational(generator.model.fraction(n['onset']) + pad)
|
|
if pad:
|
|
voice['notes'].insert(0, generator.model.make_event(f'pickup-rest-{voice["id"]}', fractions.Fraction(), pad, 'rest') | {'voice': voice['id']})
|
|
destination['voices'] = voices
|
|
identity_segment = [destination, *segment]
|
|
if pickup['harmonies']:
|
|
destination['harmonies'] = [h for h in destination['harmonies'] if generator.model.fraction(h['onset']) < pad]
|
|
for h in pickup['harmonies']:
|
|
h['onset'] = generator.model.rational(generator.model.fraction(h['onset']) + pad)
|
|
destination['harmonies'].append(h)
|
|
state = {k: copy.deepcopy(pickup[k]) for k in ('key', 'clef', 'meter', 'transpose')}
|
|
if pad:
|
|
destination['attributes'].append({'onset': generator.model.rational(pad), **state})
|
|
else:
|
|
destination.update(state)
|
|
destination['key_signature'] = generator.model.key_label(state['key'])
|
|
destination['time_signature'] = generator.model.meter_label(state['meter'])
|
|
for change in pickup.get('attributes', []):
|
|
destination['attributes'].append({**change, 'onset': generator.model.rational(generator.model.fraction(change['onset']) + pad)})
|
|
for shift in staff.get('octave_shifts', []):
|
|
low, high = generator.model.fraction(shift['start']), min(generator.model.fraction(pickup['duration']), generator.model.fraction(shift['end']))
|
|
if low < high:
|
|
offset = absolute - generator.model.fraction(pickup['duration'])
|
|
octave_shifts.append({**shift, 'id': f'pickup-{section.section_index}-{shift["id"]}', 'start': generator.model.rational(offset + low), 'end': generator.model.rational(offset + high)})
|
|
tuplets.extend(generator.score.reidentify(identity_segment, staff.get('tuplets', [])))
|
|
output.extend(segment)
|
|
absolute += segment_length
|
|
result = generator.score.make_arranged_document(document, staff, staff_id='staff-arranged-' + part_type, staff_name=part_type.capitalize(), role='vocal' if part_type == 'vocal' else 'guitar', arrangement_part_type=part_type, measures=output, tuplets=tuplets, octave_shifts=octave_shifts)
|
|
result['metadata'].setdefault('tempo', 124)
|
|
if part_type != 'vocal':
|
|
generator.acoustic.prepare(result)
|
|
generator.melody.harmonize(result)
|
|
generator.guitar.place_guitar(result)
|
|
if generator.accompaniment.connect_continuations(result):
|
|
generator.guitar.place_guitar(result)
|
|
generator.acoustic.combine(result)
|
|
return result
|
|
|
|
def generate_performance_scores(source_dir: pathlib.Path, output_dir: pathlib.Path, *, rhythms: generator.rhythm.Library) -> int:
|
|
count = 0
|
|
for path in generator.parts.source_paths(source_dir):
|
|
try:
|
|
document = generator.parts.prepare_document(generator.musicxml.load_musicxml(path))
|
|
plans = plan_arrangement(document, rhythms)
|
|
except ValueError as error:
|
|
raise ValueError(f'{path}: {error}') from error
|
|
for part in generator.parts.PART_TYPES:
|
|
try:
|
|
generator.score.write_json(output_dir / f'{generator.score.slugify(path.stem)}-{part}.json', _arrange_document(document, part, plans))
|
|
except ValueError as error:
|
|
raise ValueError(f'{path}, part {part}: {error}') from error
|
|
count += 1
|
|
return count
|