@@ -1,104 +1,133 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import copy
|
||||
from fractions import Fraction
|
||||
from pathlib import Path
|
||||
import fractions
|
||||
import pathlib
|
||||
|
||||
from generator.arrange.form import full_arrangement_sections
|
||||
from generator.guitar import place_guitar
|
||||
from generator.parts import PART_TYPES, build_measure, prepare_document, propagated_chords, source_paths
|
||||
from generator.model import fraction, key_label, make_event, meter_label, rational
|
||||
from generator.musicxml import load_musicxml
|
||||
from generator.score import copy_source_voices, make_arranged_document, reidentify, slugify, source_staff, write_json
|
||||
import generator.accompaniment
|
||||
import generator.arrange.form
|
||||
import generator.guitar
|
||||
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) -> dict:
|
||||
return _arrange_document(prepare_document(document), part_type)
|
||||
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 _arrange_document(document: dict, part_type: str) -> dict:
|
||||
if part_type not in PART_TYPES:
|
||||
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 = source_staff(document)
|
||||
staff = generator.score.source_staff(document)
|
||||
source = staff['measures']
|
||||
sections = full_arrangement_sections(source, part_type)
|
||||
sections = generator.arrange.form.full_arrangement_sections(source, part_type)
|
||||
output, tuplets, octave_shifts = [], [], []
|
||||
absolute = Fraction()
|
||||
absolute = fractions.Fraction()
|
||||
source_starts = []
|
||||
source_time = Fraction()
|
||||
source_time = fractions.Fraction()
|
||||
for m in source:
|
||||
source_starts.append(source_time)
|
||||
source_time += fraction(m['duration'])
|
||||
source_time += generator.model.fraction(m['duration'])
|
||||
for section in sections:
|
||||
previous = propagated_chords(source, include_optional=section.section_index != FINAL_SECTION_INDEX)
|
||||
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 = copy.deepcopy(source[si])
|
||||
m['harmonies'] = [h for h in m['harmonies'] if section.section_index != FINAL_SECTION_INDEX or not h.get('chord_optional')]
|
||||
for h in m['harmonies']:
|
||||
h.pop('chord_optional', None)
|
||||
result = build_measure(m, f'measure-{len(output) + len(segment) + 1}', section.role, part_type, previous[si], section.label if not segment else '')
|
||||
m = section_measures[si]
|
||||
rhythm = None
|
||||
if section.role == 'accompaniment':
|
||||
plan = plans[section.section_index]
|
||||
rhythm = (plan.acoustic 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)
|
||||
segment.append(result)
|
||||
identity_segment = segment
|
||||
segment_length = sum((fraction(m['duration']) for m in segment), Fraction())
|
||||
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, fraction(shift['start'])), min(start + segment_length, fraction(shift['end']))
|
||||
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': rational(absolute + low - start), 'end': rational(absolute + high - start)})
|
||||
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 = build_measure(source[0], source[0]['id'], 'vocal', part_type, None)
|
||||
pickup = generator.parts.build_measure(source[0], source[0]['id'], 'vocal', part_type, None)
|
||||
destination = output[-1]
|
||||
pad = fraction(destination['duration']) - fraction(pickup['duration'])
|
||||
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 = copy_source_voices(pickup, strip_lyrics=False)
|
||||
voices = generator.score.copy_source_voices(pickup, strip_lyrics=False)
|
||||
for voice in voices:
|
||||
for n in voice['notes']:
|
||||
n['onset'] = rational(fraction(n['onset']) + pad)
|
||||
n['onset'] = generator.model.rational(generator.model.fraction(n['onset']) + pad)
|
||||
if pad:
|
||||
voice['notes'].insert(0, make_event(f'pickup-rest-{voice["id"]}', Fraction(), pad, 'rest') | {'voice': voice['id']})
|
||||
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 fraction(h['onset']) < pad]
|
||||
destination['harmonies'] = [h for h in destination['harmonies'] if generator.model.fraction(h['onset']) < pad]
|
||||
for h in pickup['harmonies']:
|
||||
h['onset'] = rational(fraction(h['onset']) + pad)
|
||||
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': rational(pad), **state})
|
||||
destination['attributes'].append({'onset': generator.model.rational(pad), **state})
|
||||
else:
|
||||
destination.update(state)
|
||||
destination['key_signature'] = key_label(state['key'])
|
||||
destination['time_signature'] = meter_label(state['meter'])
|
||||
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': rational(fraction(change['onset']) + pad)})
|
||||
destination['attributes'].append({**change, 'onset': generator.model.rational(generator.model.fraction(change['onset']) + pad)})
|
||||
for shift in staff.get('octave_shifts', []):
|
||||
low, high = fraction(shift['start']), min(fraction(pickup['duration']), fraction(shift['end']))
|
||||
low, high = generator.model.fraction(shift['start']), min(generator.model.fraction(pickup['duration']), generator.model.fraction(shift['end']))
|
||||
if low < high:
|
||||
offset = absolute - fraction(pickup['duration'])
|
||||
octave_shifts.append({**shift, 'id': f'pickup-{section.section_index}-{shift["id"]}', 'start': rational(offset + low), 'end': rational(offset + high)})
|
||||
tuplets.extend(reidentify(identity_segment, staff.get('tuplets', [])))
|
||||
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 = 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 = 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':
|
||||
place_guitar(result)
|
||||
generator.guitar.place_guitar(result)
|
||||
if generator.accompaniment.connect_continuations(result):
|
||||
generator.guitar.place_guitar(result)
|
||||
return result
|
||||
|
||||
def generate_performance_scores(source_dir: Path, output_dir: Path) -> int:
|
||||
def generate_performance_scores(source_dir: pathlib.Path, output_dir: pathlib.Path, *, rhythms: generator.rhythm.Library) -> int:
|
||||
count = 0
|
||||
for path in source_paths(source_dir):
|
||||
for path in generator.parts.source_paths(source_dir):
|
||||
try:
|
||||
document = prepare_document(load_musicxml(path))
|
||||
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 PART_TYPES:
|
||||
for part in generator.parts.PART_TYPES:
|
||||
try:
|
||||
write_json(output_dir / f'{slugify(path.stem)}-{part}.json', _arrange_document(document, part))
|
||||
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
|
||||
|
||||
Reference in New Issue
Block a user