@@ -0,0 +1,58 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import dataclasses
|
||||
import fractions
|
||||
|
||||
import generator.fretboard
|
||||
import generator.harmony
|
||||
import generator.model
|
||||
import generator.scales.selection
|
||||
import generator.scales.timeline
|
||||
import generator.solo.analysis
|
||||
import generator.solo.planning
|
||||
import generator.solo.realization
|
||||
import generator.solo.types
|
||||
|
||||
def compose(analysis, measures, part, end, *, scales, sequences, rhythms, initial=None):
|
||||
timeline = generator.scales.timeline.read({'measures': measures}, initial=initial)
|
||||
region = generator.scales.timeline.Solo(fractions.Fraction(), end, part.capitalize() + ' Solo')
|
||||
choices = generator.scales.selection.select(timeline, region, scales)
|
||||
for choice in choices:
|
||||
if choice.span.chord and choice.scale is None:
|
||||
raise ValueError(f'{part} solo, source measure {choice.span.source_measure}, beat {choice.span.beat}, {generator.harmony.symbol(choice.span.chord)}: No exact match within the permitted catalog')
|
||||
plans = generator.solo.planning.plans(analysis, measures, choices, end, part, sequences)
|
||||
# A protected quote may change the local collection, never its individual pitches.
|
||||
for plan in plans:
|
||||
if plan.treatment != 'quote' or plan.motif.duration > plan.end - plan.start:
|
||||
continue
|
||||
retained = tuple((plan.start + n.start, plan.start + n.end, {n.pitch.number() % 12}) for n in plan.motif.notes)
|
||||
if all(pc <= generator.solo.types.permitted(choices, start, finish) for start, finish, pc in retained):
|
||||
continue
|
||||
alternative = generator.scales.selection.select(timeline, region, scales, retained=retained)
|
||||
if all(pc <= generator.solo.types.permitted(alternative, start, finish) for start, finish, pc in retained) and all(c.scale or c.span.chord is None for c in alternative):
|
||||
choices = tuple(dataclasses.replace(c, reason=c.reason + '; retained quotation') if any(a < c.span.end and b > c.span.start for a, b, _ in retained) else c for c in alternative)
|
||||
phrases, previous = [], None
|
||||
for plan in plans:
|
||||
phrase = generator.solo.realization.compose(plan, measures, choices, sequences, rhythms, analysis.tempo, previous)
|
||||
phrases.append(phrase)
|
||||
notes = tuple(n for d in phrase.decisions for n in d.notes)
|
||||
if notes:
|
||||
previous = notes[-1]
|
||||
result = generator.solo.types.Solo(part, choices, tuple(phrases), analysis.tempo, end)
|
||||
validate(result)
|
||||
return result
|
||||
|
||||
def validate(solo):
|
||||
previous = None
|
||||
for note in solo.notes():
|
||||
if note.start < 0 or note.end > solo.end or note.end <= note.start or previous and previous.end > note.start:
|
||||
raise ValueError(f'{solo.part} solo: overlapping or out-of-bounds note at {note.start}')
|
||||
if note.pitch.number() % 12 not in generator.solo.types.permitted(solo.choices, note.start, note.end):
|
||||
raise ValueError(f'{solo.part} solo: held pitch {note.pitch.label()} violates a scale span at {note.start}-{note.end}')
|
||||
if note.position not in generator.fretboard.positions_for_pitch(fractions.Fraction(note.pitch.number())):
|
||||
raise ValueError(f'{solo.part} solo: invalid guitar placement at {note.start}')
|
||||
previous = note
|
||||
for phrase in solo.phrases:
|
||||
notes = tuple(n for d in phrase.decisions for n in d.notes)
|
||||
if not generator.solo.realization.target_ok(notes, phrase.plan.target):
|
||||
raise ValueError(f'{phrase.plan.id}: planned harmonic arrival was not realized')
|
||||
Reference in New Issue
Block a user