95 lines
5.0 KiB
Python
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)
|