Files
product-vamp/generator/solo/arrangement.py
T
khodowany eae432aa83
ci / checks (push) Successful in 50m39s
Soloing
2026-10-01 06:31:14 -05:00

124 lines
6.7 KiB
Python

from __future__ import annotations
import copy
import fractions
import generator.accompaniment
import generator.acoustic
import generator.acoustic_voicing
import generator.arrange.form
import generator.electric
import generator.guitar
import generator.model
import generator.parts
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 reuse_head_grips(document, references, choices):
previous = generator.accompaniment.carried_chords(references)
target_previous = generator.accompaniment.carried_chords(document['staves'][0]['measures'])
links, absolute = [], fractions.Fraction()
for index, measure in enumerate(document['staves'][0]['measures']):
for note in generator.score.measure_notes(measure):
onset = generator.model.fraction(note['onset'])
chord = generator.accompaniment.active_chord(measure, onset, target_previous[index])
if not note['pitches'] or chord is None:
continue
allowed = generator.solo.types.permitted(choices, absolute + onset, absolute + onset + generator.model.fraction(note['duration']))
matches = []
for candidate in generator.score.measure_notes(references[index]):
if len(candidate['pitches']) < 3:
continue
time = generator.model.fraction(candidate['onset'])
head_chord = generator.accompaniment.active_chord(references[index], time, previous[index])
if generator.accompaniment.same_chord(chord, head_chord) and {int(generator.model.pitch_number(p)) % 12 for p in candidate['pitches']} <= allowed:
matches.append((abs(time - onset), candidate['id'], candidate))
links.append(generator.electric.VoicingLink(note, tuple(item[2] for item in sorted(matches))))
absolute += generator.model.fraction(measure['duration'])
generator.electric.reuse_voicings(links)
def backing(baseline, foreground, part, rhythms, references, choices, initial=None):
plan = generator.accompaniment.plan_section(foreground, rhythms, initial=initial, electric=part == 'electric', acoustic_companions=False)
previous = generator.accompaniment.carried_chords(foreground, initial)
measures = []
for index, measure in enumerate(foreground):
rhythm = (plan.acoustic_bass[index], plan.acoustic_upper[index]) if part == 'acoustic' else plan.electric[index]
result = generator.parts.build_measure(measure, measure['id'], 'accompaniment', part, previous[index], measure.get('section_label', ''), rhythm)
measures.append(result)
generator.score.reidentify(measures, [])
staff = baseline['staves'][0]
document = generator.score.make_arranged_document(baseline, staff, staff_id=staff['id'], staff_name=staff['name'], role='guitar', arrangement_part_type=part, measures=measures)
if part == 'acoustic':
generator.acoustic.prepare(document)
generator.acoustic_voicing.place(document)
generator.acoustic.combine(document)
else:
generator.guitar.place_guitar(document)
reuse_head_grips(document, references, choices)
generator.model.validate_document(document)
return document['staves'][0]['measures']
def apply(source, parts, *, rhythms, scales, sequences):
"""Compose once per song, replacing only already-placed solo section windows."""
analysis = generator.solo.analysis.analyze(source)
sections = regions(source)
source_measures = generator.score.source_staff(source)['measures']
head_first, head_count, _, _ = sections[2]
head = parts['electric']['staves'][0]['measures'][head_first:head_first + head_count]
reports = []
for section_index, part, other in ((3, 'acoustic', 'electric'), (4, 'electric', 'acoustic')):
first, count, offset, length = sections[section_index]
initial = generator.accompaniment.carried_chords(parts[part]['staves'][0]['measures'])[first]
template = copy.deepcopy(parts[part]['staves'][0]['measures'][first:first + 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)
solo = generator.solo.composer.compose(analysis, template, part, cutoff, scales=scales, sequences=sequences, rhythms=rhythms, initial=initial)
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 guitarist responds to the actual foreground, including vocal return.
if cutoff < length:
vocal = parts['vocal']['staves'][0]['measures'][first:first + count]
time = fractions.Fraction()
for measure, voice_measure in zip(foreground, vocal):
returning = [copy.deepcopy(event) | {'voice': 'vocal_return'} for event in generator.score.measure_notes(voice_measure) if event['pitches'] and time + generator.model.fraction(event['onset']) >= cutoff]
if returning:
measure['voices'].append({'id': 'vocal_return', 'notes': returning})
time += generator.model.fraction(measure['duration'])
accompaniment = backing(parts[other], foreground, other, rhythms, head, solo.choices, initial)
generator.solo.notation.replace_region(parts[other], first, accompaniment)
reports.append(generator.solo.report.describe(solo, scales, first, offset))
for document in parts.values():
generator.model.validate_document(document)
return '\n'.join(reports)