from __future__ import annotations import copy import fractions import generator.accompaniment import generator.arrange.accompaniment import generator.arrange.form import generator.electric import generator.fretboard import generator.model import generator.score import generator.solo.analysis import generator.solo.composer import generator.solo.notation import generator.solo.report import generator.solo.types def regions(document): result, first, start = {}, 0, fractions.Fraction() source = generator.score.source_staff(document)['measures'] for section in generator.arrange.form.full_arrangement_sections(source, 'acoustic'): length = sum((generator.model.fraction(m['duration']) for m in source[section.source_measure_index:section.source_measure_index + section.measure_count]), fractions.Fraction()) result[section.section_index] = (first, section.measure_count, start, length) first += section.measure_count start += length return result def annotate(document, solo, offset): spans = [] for choice in solo.choices: if choice.scale is None: continue label = choice.scale.label() if spans and spans[-1]['label'] == label and generator.model.fraction(spans[-1]['end']) == offset + choice.span.start: spans[-1]['end'] = generator.model.rational(offset + choice.span.end) else: spans.append({'id': f'{solo.part}_scale_{len(spans) + 1}', 'start': generator.model.rational(offset + choice.span.start), 'end': generator.model.rational(offset + choice.span.end), 'label': label, 'matched': True}) document['staves'][0]['scale_spans'] = spans def apply(source, parts, *, rhythms, scales, sequences, solos=None, revise=True, contexts=None, references=(), stage='elaborated', enclosures_enabled=True): """Compose once per song, replacing only already-placed solo section windows.""" if references: raise ValueError('Audition references are not production composition inputs') from generator.arrange import generate analysis = generator.solo.analysis.analyze(source) sections = regions(source) source_measures = generator.score.source_staff(source)['measures'] reports = [] requests = {} for section_index, part in ((3, 'acoustic'), (4, 'electric')): first, count, offset, length = sections[section_index] form = generator.arrange.form.full_arrangement_sections(source_measures, part) section = form[section_index] effective = generate.section_source(source_measures, section, form[section_index+1]) initial = generator.accompaniment.carried_chords(effective)[section.source_measure_index] template = copy.deepcopy(effective[section.source_measure_index:section.source_measure_index+count]) cutoff = length if part == 'electric' and generator.score.body_start_measure_index(source_measures): pickup = source_measures[0] sounding = [generator.model.fraction(n['onset']) for n in generator.score.measure_notes(pickup) if n['pitches']] if sounding: cutoff -= generator.model.fraction(pickup['duration']) - min(sounding) requests[part] = (template, cutoff, initial) selected = generator.solo.composer.compose_pair(analysis, requests, scales=scales, sequences=sequences, rhythms=rhythms, contexts=contexts, stage=stage,enclosures_enabled=enclosures_enabled) for solo, section_index, other in zip(selected, (3, 4), ('electric', 'acoustic')): part = solo.part first, count, offset, length = sections[section_index] template, cutoff, initial = requests[part] if solos is not None: solos.append(solo) foreground = generator.solo.notation.measures(template, solo.notes(), 'solo_' + part) generator.solo.notation.replace_region(parts[part], first, foreground) annotate(parts[part], solo, offset) # The other instrument already contains its canonical accompaniment. # Installing a solo must never rewrite the learned supporting part. # Canonical supporting notes must remain compatible throughout every # overlapped mapped span, independent of the foreground realization. elapsed = fractions.Fraction() for measure in parts[other]['staves'][0]['measures'][first:first+count]: for event in generator.score.measure_notes(measure): start = elapsed + generator.model.fraction(event['onset']) end = min(cutoff, start + generator.model.fraction(event['duration'])) if start >= end: continue allowed = generator.solo.types.permitted(solo.choices,start,end) if any(int(generator.model.pitch_number(p)) % 12 not in allowed for p in event['pitches']): raise ValueError(f'{other} accompaniment, measure {measure["number"]}: canonical voicing outside solo map') elapsed += generator.model.fraction(measure['duration']) solo.diagnostics['canonical_accompaniment'] = generator.arrange.accompaniment.audit(source, parts) reports.append(generator.solo.report.describe(solo, scales, first, offset)) for document in parts.values(): generator.model.validate_document(document) generator.fretboard.validate_document(document) return '\n'.join(reports)