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.accompaniment.prepare_basses(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