This commit is contained in:
khodowany
2026-10-03 12:44:53 -05:00
parent 59afd78205
commit 04c6ea2e00
25 changed files with 673 additions and 185 deletions
+16 -61
View File
@@ -4,14 +4,11 @@ import copy
import fractions
import generator.accompaniment
import generator.acoustic
import generator.acoustic_voicing
import generator.arrange.accompaniment
import generator.arrange.form
import generator.electric
import generator.fretboard
import generator.guitar
import generator.model
import generator.parts
import generator.score
import generator.solo.analysis
import generator.solo.composer
@@ -41,57 +38,11 @@ def annotate(document, solo, offset):
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, solos=None, revise=True, contexts=None, references=(), stage='elaborated', enclosures_enabled=True):
"""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 = []
requests = {}
for section_index, part in ((3, 'acoustic'), (4, 'electric')):
@@ -115,17 +66,21 @@ def apply(source, parts, *, rhythms, scales, sequences, solos=None, revise=True,
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)
# 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)