+79
-12
@@ -6,6 +6,7 @@ import generator.harmony
|
||||
import generator.model
|
||||
import generator.scales.selection
|
||||
import generator.solo.analysis
|
||||
import generator.solo.policy
|
||||
import generator.solo.sequences
|
||||
import generator.solo.types
|
||||
|
||||
@@ -19,16 +20,44 @@ def target_pitches(chord: dict, final: bool) -> frozenset[int]:
|
||||
root = generator.scales.selection.root_number(chord)
|
||||
return frozenset(int(root + generator.harmony.degree_semitones(d, a)) % 12 for d, a in degrees.items() if d in preferred) or frozenset(generator.harmony.pitch_classes(chord))
|
||||
|
||||
def destination(choices, start, end, final=False, release=fractions.Fraction(1, 2)):
|
||||
def destination(choices, start, end, final=False, release=fractions.Fraction(1, 2), closure='closed'):
|
||||
last = next((c for c in reversed(choices) if c.span.start < end and c.span.end > start), None)
|
||||
if not last or not last.span.chord:
|
||||
return None
|
||||
latest = end - release - fractions.Fraction(1, 4)
|
||||
if last.span.start > latest:
|
||||
return None
|
||||
return generator.solo.types.TargetEvent(max(start, last.span.start, end - 1 - release), latest, target_pitches(last.span.chord, final), generator.harmony.symbol(last.span.chord), 'phrase resolution' if final else 'defining chord-tone arrival')
|
||||
pitches = target_pitches(last.span.chord, final)
|
||||
if closure == 'open' and last.scale:
|
||||
root = generator.scales.selection.root_number(last.span.chord)
|
||||
degrees = generator.harmony.degrees(last.span.chord)
|
||||
additions = {int(root + generator.harmony.degree_semitones(d, degrees.get(d, 0))) % 12 for d in (5, 9, 11, 13) if d not in {v['value'] for v in last.span.chord.get('degrees', []) if v['type'] == 'subtract'}}
|
||||
pitches = frozenset(additions & last.scale.pitches) or pitches
|
||||
return generator.solo.types.TargetEvent(max(start, last.span.start, end - 2 - release), latest, pitches, generator.harmony.symbol(last.span.chord), 'open continuation' if closure == 'open' else 'phrase resolution' if final else 'defining chord-tone arrival')
|
||||
|
||||
def new_motif(sequences: generator.solo.sequences.Library, length: fractions.Fraction) -> generator.solo.types.Motif:
|
||||
def new_motif(sequences: generator.solo.sequences.Library, length: fractions.Fraction, *, choices=(), register=(55, 74), seed='', variant=0) -> generator.solo.types.Motif:
|
||||
if choices and any(c.scale for c in choices):
|
||||
scale = next(c.scale for c in choices if c.scale)
|
||||
arrival = next(c for c in reversed(choices) if c.scale)
|
||||
count = min(6, max(3, int(length * fractions.Fraction(3, 2))))
|
||||
shapes = (('rising', 4), ('falling', -4), ('arch', 1), ('valley', -1), ('leap_return', 2), ('neighbor', 0))
|
||||
shape, displacement = shapes[(int(generator.solo.policy.stable_rank(seed, 'invention')[:8], 16) + variant) % len(shapes)]
|
||||
paths = sequences.retrieve(count, displacement, (displacement > 0) - (displacement < 0))
|
||||
candidates = []
|
||||
target = target_pitches(arrival.span.chord, False)
|
||||
for path in paths:
|
||||
for start in range(-14, 15):
|
||||
pitches = tuple(generator.solo.types.scale_pitch(scale, start + offset) for offset in path.offsets())
|
||||
if not all(register[0] <= pitch.number() <= register[1] for pitch in pitches):
|
||||
continue
|
||||
score = (generator.solo.sequences.family(path.steps) != shape, pitches[-1].number() % 12 not in target, abs(path.offsets()[-1] - displacement), abs(pitches[0].number() - (register[0] + 5)), generator.solo.policy.stable_rank(seed, (variant, path.steps, start)))
|
||||
candidates.append((score, pitches, path))
|
||||
if candidates:
|
||||
_, pitches, path = min(candidates, key=lambda c: c[0])
|
||||
unit = min(fractions.Fraction(1, 2), length / count)
|
||||
notes = tuple(generator.solo.types.Note(i * unit, (i + 1) * unit, pitch) for i, pitch in enumerate(pitches))
|
||||
identifier = generator.solo.policy.stable_rank(seed, (variant, path.steps))[:12]
|
||||
return generator.solo.types.Motif('invented_' + identifier, notes, length, (), 'sequences ' + ' + '.join(path.sources))
|
||||
paths = sequences.connecting(3) or sequences.connecting(2) or sequences.connecting(4)
|
||||
if not paths:
|
||||
raise ValueError('solo composition requires a two-, three-, or four-note sequence')
|
||||
@@ -49,11 +78,33 @@ def choose_motifs(analysis, sequences, length):
|
||||
contrast = min(others, key=lambda m: (m.contour() == head.contour(), abs(len(m.notes) - 3), m.duration, m.id)) if others else head
|
||||
return head, contrast, new_motif(sequences, length)
|
||||
|
||||
def activity_passages(start, end, release, activity, shape=0):
|
||||
music_end = end - release
|
||||
boundaries = sorted({start, music_end} | {start + fractions.Fraction(round(float((end - start) * i / 4) * 2), 2) for i in (1, 2, 3) if start < start + fractions.Fraction(round(float((end - start) * i / 4) * 2), 2) < music_end})
|
||||
weights = ((1, 1.6, 1.1), (0.8, 1.7, 1.2), (1.5, 1.4, 0.8))[shape % 3]
|
||||
spans = list(zip(boundaries, boundaries[1:]))
|
||||
arrival_rate = fractions.Fraction(3, 8)
|
||||
desired = max(0, activity * (end - start) - arrival_rate * (spans[-1][1] - spans[-1][0]))
|
||||
denominator = sum(float(b - a) * weights[min(i, 2)] for i, (a, b) in enumerate(spans[:-1]))
|
||||
result = []
|
||||
for i, (a, b) in enumerate(spans):
|
||||
arrival = i == len(spans) - 1
|
||||
rate = arrival_rate if arrival else fractions.Fraction(str(min(2, float(desired) * weights[min(i, 2)] / max(denominator, 0.001)))).limit_denominator(1000)
|
||||
occupancy = generator.solo.policy.DEFAULT.arrival_occupancy if arrival else generator.solo.policy.DEFAULT.active_occupancy
|
||||
result.append(generator.solo.types.Passage(a, b, 'sustained_arrival' if arrival else 'active', rate, fractions.Fraction(str(occupancy))))
|
||||
if release:
|
||||
result.append(generator.solo.types.Passage(music_end, end, 'space', fractions.Fraction(), fractions.Fraction()))
|
||||
return tuple(result)
|
||||
|
||||
def plans(analysis: generator.solo.analysis.Analysis, measures: list[dict], choices, end: fractions.Fraction, part: str, sequences: generator.solo.sequences.Library) -> tuple[generator.solo.types.PhrasePlan, ...]:
|
||||
boundaries = [fractions.Fraction()]
|
||||
for measure in measures:
|
||||
boundaries.append(min(end, boundaries[-1] + generator.model.fraction(measure['duration'])))
|
||||
head, contrast, invented = choose_motifs(analysis, sequences, boundaries[1])
|
||||
seed = generator.solo.policy.stable_rank(generator.solo.analysis.identity(analysis, measures, part), generator.solo.policy.describe()['fingerprint'])
|
||||
corridor = (55, 74) if part == 'acoustic' else (59, 79)
|
||||
invented = new_motif(sequences, boundaries[1], choices=choices, register=corridor, seed=seed)
|
||||
alternate = new_motif(sequences, boundaries[1], choices=choices, register=corridor, seed=seed, variant=1)
|
||||
result, index = [], 0
|
||||
while index < len(measures) and boundaries[index] < end:
|
||||
start = boundaries[index]
|
||||
@@ -74,17 +125,33 @@ def plans(analysis: generator.solo.analysis.Analysis, measures: list[dict], choi
|
||||
finish = boundaries[stop]
|
||||
final = finish == end
|
||||
phase = (start + finish) / (2 * end)
|
||||
motif = head if part == 'acoustic' or final else invented
|
||||
if len(result) > 1 and not final and len(result) % 3 == 2:
|
||||
motif = contrast if part == 'acoustic' else invented
|
||||
relationship = 'statement' if not result else 'callback' if final else 'repeat' if len(result) == 1 else 'answer' if len(result) % 2 else 'variation'
|
||||
treatment = 'quote' if (part == 'acoustic' and not result) or (part == 'electric' and final and motif.origin == 'melody') else 'thematic'
|
||||
phrase_seed = generator.solo.policy.stable_rank(seed, (str(start), str(finish)))
|
||||
choice = int(phrase_seed[:8], 16)
|
||||
relationship = 'statement' if not result else 'repeat' if len(result) == 1 else 'answer' if len(result) % 2 else 'variation'
|
||||
if part == 'acoustic':
|
||||
motif = head if len(result) < 2 else (head, contrast, invented)[choice % 3]
|
||||
else:
|
||||
motif = invented if len(result) < 2 else (invented, alternate, head)[choice % 3]
|
||||
treatment = 'opening_reference' if not result and motif.origin == 'melody' else 'invented' if motif.origin != 'melody' else 'answer' if relationship == 'answer' else ('rhythmic_reference', 'contour_reference')[choice % 2]
|
||||
low, high = (55, 74) if part == 'acoustic' else (59, 79)
|
||||
# Reserve the upper register for a prepared peak, not every active passage.
|
||||
register = (low + (5 if fractions.Fraction(1, 2) <= phase < fractions.Fraction(3, 4) else 0), high if phase >= fractions.Fraction(1, 2) else high - 4)
|
||||
activity = fractions.Fraction(3, 4) if part == 'acoustic' else fractions.Fraction(1)
|
||||
if final:
|
||||
activity *= fractions.Fraction(2, 3)
|
||||
result.append(generator.solo.types.PhrasePlan(f'{part}_phrase_{len(result) + 1}', start, finish, motif, relationship, treatment, destination(choices, start, finish, final), register, activity))
|
||||
activity = fractions.Fraction(str(generator.solo.policy.DEFAULT.acoustic_activity if part == 'acoustic' else generator.solo.policy.DEFAULT.electric_activity))
|
||||
ending = ('connected', 'sustain', 'breath')[choice % 3]
|
||||
release = fractions.Fraction(1, 2) if choice % 2 else fractions.Fraction(1)
|
||||
release = min(release, (finish - start) / 4) if ending == 'breath' else fractions.Fraction()
|
||||
if treatment == 'rhythmic_reference' and motif.duration > finish - start - release:
|
||||
treatment = 'contour_reference'
|
||||
closure = 'closed' if final or relationship == 'answer' else 'open'
|
||||
passages = activity_passages(start, finish, release, activity, choice % 3)
|
||||
sounding = [p for p in passages if p.intent != 'space']
|
||||
peak = sounding[min(1, len(sounding) - 1)]
|
||||
ascending = choice % 2 == 0
|
||||
landmarks = (
|
||||
generator.solo.types.Landmark(start, min(start + 1, peak.start), register[0] + (3 if ascending else 8), register[0] + (7 if ascending else 12), 0, 'entrance'),
|
||||
generator.solo.types.Landmark(peak.start, peak.end, register[1] - 6 if ascending else register[0] + 2, register[1] - 2 if ascending else register[0] + 6, 1 if ascending else -1, 'peak' if ascending else 'low point'),
|
||||
generator.solo.types.Landmark(sounding[-1].start, sounding[-1].end, register[0] + (4 if ascending else 8), register[0] + (9 if ascending else 13), -1 if ascending else 1, 'arrival'),
|
||||
)
|
||||
result.append(generator.solo.types.PhrasePlan(f'{part}_phrase_{len(result) + 1}', start, finish, motif, relationship, treatment, destination(choices, start, finish, final, release, closure), register, activity, release, passages, landmarks, ending, closure, phrase_seed))
|
||||
index = stop
|
||||
return tuple(result)
|
||||
|
||||
Reference in New Issue
Block a user