Soloing
This commit is contained in:
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user