Files
product-vamp/generator/arrange/generate.py
T
khodowany 9fd5abd075
ci / checks (push) Successful in 1h16m10s
Rhythm Selection & Cleanup
2026-09-26 02:50:22 -05:00

135 lines
7.4 KiB
Python

from __future__ import annotations
import copy
import fractions
import pathlib
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, *, 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 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((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.guitar.place_guitar(result)
if generator.accompaniment.connect_continuations(result):
generator.guitar.place_guitar(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