from __future__ import annotations import copy import fractions import pathlib import generator.accompaniment import generator.arrange.accompaniment import generator.acoustic import generator.acoustic_voicing import generator.arrange.form import generator.arrange.pickup import generator.electric import generator.fretboard import generator.guitar import generator.melody import generator.model import generator.musicxml import generator.parts import generator.score import generator.solo.arrangement import generator.comping import generator.vocabulary import generator.production FINAL_SECTION_INDEX = 5 def arrange_document(document: dict, part_type: str) -> dict: """Canonical source-only template; solos are inserted by production roles.""" document = generator.parts.prepare_document(document) with generator.vocabulary.using(generator.vocabulary.load(), part_type + '_accompaniment'): backing = generator.comping.Canonical(document, part_type, 'canonical') if part_type != 'vocal' else None return _arrange_document(document, part_type, accompaniment=backing) def section_source(source: list[dict], section, next_section=None) -> list[dict]: result = copy.deepcopy(source) for measure in result: measure['harmonies'] = [h for h in measure['harmonies'] if section.section_index != FINAL_SECTION_INDEX or not h.get('chord_optional')] for chord in measure['harmonies']: chord.pop('chord_optional', None) if generator.arrange.pickup.enters(next_section, source): destination = result[section.source_measure_index + section.measure_count - 1] generator.arrange.pickup.harmonies(destination, result[0]) return result def _arrange_document(document: dict, part_type: str, plans=None, *, owned=None, accompaniment=None) -> 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, voicing_origins = [], [], [], [] 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 index, section in enumerate(sections): role = section.role if owned is None or section.role in owned else 'rest' section_measures = section_source(source, section, sections[index + 1] if index + 1 < len(sections) else None) 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] if role == 'accompaniment' and accompaniment is not None: result = accompaniment(m, previous[si], si) result['id'] = f'measure_{len(output) + len(segment) + 1}' result['section_label'] = section.label if not segment else '' else: result = generator.parts.build_measure(m, f'measure_{len(output) + len(segment) + 1}', role, part_type, previous[si], section.label if not segment else '') segment.append(result) if part_type == 'electric' and section.role in ('melody', 'accompaniment'): voicing_origins.append(generator.electric.MeasureOrigin(source_starts[si], section.role, result)) segment_length = sum((generator.model.fraction(m['duration']) for m in segment), fractions.Fraction()) if 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)}) pickup = None if role in ('vocal', 'melody') and generator.arrange.pickup.enters(section, source) and output: pickup = generator.parts.build_measure(source[0], f'pickup_{section.section_index}', section.role, part_type, None) tuplets.extend(generator.score.reidentify(([pickup] if pickup else []) + segment, staff.get('tuplets', []))) if pickup: destination = output[-1] pad = generator.arrange.pickup.offset(destination, pickup) onset = generator.arrange.pickup.splice(destination, pickup) if part_type == 'electric': voicing_origins = [generator.electric.MeasureOrigin(o.onset, o.role, o.measure, o.start, onset if o.measure is destination else o.end) for o in voicing_origins] voicing_origins.append(generator.electric.MeasureOrigin(-pad, 'melody', destination, onset)) for shift in staff.get('octave_shifts', []): low, high = max(onset - pad, 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)}) output.extend(segment) absolute += segment_length tuplets = generator.arrange.pickup.retain_links(output, tuplets) 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 == 'acoustic' and accompaniment is None and (owned is None or 'accompaniment' in owned): generator.acoustic.prepare(result) generator.acoustic_voicing.place(result) generator.acoustic.combine(result) elif part_type == 'electric' and (owned is None or 'melody' in owned): generator.melody.harmonize(result) generator.guitar.place_guitar(result) if generator.accompaniment.connect_continuations(result): generator.guitar.place_guitar(result) # Source labels identify the head while arranging; published labels identify # the complete performance, including its lead-in and repeated choruses. for number, measure in enumerate(output, 1): measure['number'] = str(number) generator.fretboard.validate_document(result) return result def generate_performance_scores(source_dir: pathlib.Path, output_dir: pathlib.Path, *, composition_jobs=2, cache_dir=None, progress=False, timings=None) -> int: return generator.production.generate_tree(source_dir, output_dir, jobs=composition_jobs, cache_dir=cache_dir, progress=progress, timings=timings)