Files
product-vamp/generator/solo/arrangement.py
T
2026-10-04 09:05:25 -05:00

95 lines
5.0 KiB
Python

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)