+118
-48
@@ -1,10 +1,12 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import dataclasses
|
||||
import fractions
|
||||
|
||||
import generator.harmony
|
||||
import generator.model
|
||||
import generator.scales.selection
|
||||
import generator.solo.harmonic
|
||||
import generator.solo.analysis
|
||||
import generator.solo.policy
|
||||
import generator.solo.sequences
|
||||
@@ -91,67 +93,135 @@ def activity_passages(start, end, release, activity, shape=0):
|
||||
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))))
|
||||
result.append(generator.solo.types.ActivitySpan(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()))
|
||||
result.append(generator.solo.types.ActivitySpan(music_end, end, 'space', fractions.Fraction(), fractions.Fraction()))
|
||||
return tuple(result)
|
||||
|
||||
def decision(seed, name):
|
||||
return int(generator.solo.policy.stable_rank(seed, name)[:8], 16)
|
||||
|
||||
def same_harmony(left, right):
|
||||
return bool(left and right and left.span.chord and right.span.chord and generator.harmony.symbol(left.span.chord) == generator.harmony.symbol(right.span.chord))
|
||||
|
||||
def resolutions(choices):
|
||||
distinct = []
|
||||
for choice in choices:
|
||||
if not distinct or not same_harmony(distinct[-1], choice):
|
||||
distinct.append(choice)
|
||||
return tuple(right for left, right in zip(distinct, distinct[1:]) if left.span.chord and right.span.chord and generator.scales.selection.dominant(left.span.chord) and (generator.scales.selection.root_number(left.span.chord) - generator.scales.selection.root_number(right.span.chord)) % 12 == 7)
|
||||
|
||||
def anchor_backbones(identifier, start, end, release, register, choices, bar, seed):
|
||||
active = [c for c in choices if c.scale and c.span.start < end - release and c.span.end > start]
|
||||
if not active:
|
||||
return tuple([()])
|
||||
finish = min(end - release, active[-1].span.end)
|
||||
terminal_duration = min(fractions.Fraction(2), (finish - start) / 2)
|
||||
terminal_start = max(start, active[-1].span.start, finish - terminal_duration)
|
||||
terminal_duration = finish - terminal_start
|
||||
opening = max(start, active[0].span.start)
|
||||
moments = [(opening, 'opening')]
|
||||
meaningful = list(resolutions(active)) + [b for a, b in zip(active, active[1:]) if a.scale.pitches != b.scale.pitches]
|
||||
for choice in sorted(meaningful, key=lambda c: c.span.start):
|
||||
time = choice.span.start
|
||||
if time >= moments[-1][0] + bar * fractions.Fraction(3, 2) and time + bar < terminal_start and len(moments) < 4:
|
||||
moments.append((time, 'intermediate'))
|
||||
if terminal_start <= opening + fractions.Fraction(1, 2):
|
||||
moments = []
|
||||
moments.append((terminal_start, 'terminal'))
|
||||
states = [(0.0, ())]
|
||||
for index, (time, role) in enumerate(moments):
|
||||
choice = next(c for c in active if c.span.start <= time < c.span.end)
|
||||
chord_roles = generator.solo.harmonic.roles(choice.span.chord)
|
||||
eligible = (chord_roles.basic | chord_roles.specified) & choice.scale.pitches
|
||||
duration = terminal_duration if role == 'terminal' else fractions.Fraction(1, 2)
|
||||
latest = time if role == 'terminal' else min(time + fractions.Fraction(3, 2), choice.span.end - duration, terminal_start - duration)
|
||||
pitches = [generator.solo.types.scale_pitch(choice.scale, degree) for degree in range(-21, 28)]
|
||||
pitches = [p for p in pitches if register[0] <= p.number() <= register[1] and p.number() % 12 in eligible]
|
||||
desired = register[0] + (5 if index == 0 else 8 if role == 'terminal' else 11)
|
||||
following = []
|
||||
for cost, anchors in states:
|
||||
for pitch in pitches:
|
||||
jump = abs(pitch.number() - anchors[-1].pitch.number()) if anchors else 0
|
||||
direction = 1 if role != 'terminal' else -1
|
||||
wrong_way = bool(anchors and (pitch.number() - anchors[-1].pitch.number()) * direction < 0)
|
||||
preferred = target_pitches(choice.span.chord, role == 'terminal')
|
||||
score = cost + jump / 12 + 0.3 * wrong_way + abs(pitch.number() - desired) / 18 + 0.3 * (pitch.number() % 12 not in chord_roles.basic) + 0.2 * (role == 'terminal' and pitch.number() % 12 not in preferred)
|
||||
anchor = generator.solo.types.Anchor(f'{identifier}_anchor_{index + 1}', time, latest, pitch, frozenset(eligible), register, duration, role, generator.harmony.symbol(choice.span.chord), False, finish if role == 'terminal' else None)
|
||||
following.append((score, (*anchors, anchor)))
|
||||
states = list(generator.solo.sequences.diverse(following, lambda item: (item[0], generator.solo.policy.stable_rank(seed, tuple(a.pitch for a in item[1]))), lambda item: (item[1][0].pitch.number(), item[1][-1].pitch.number()), generator.solo.policy.DEFAULT.alternatives))
|
||||
return tuple(anchors for _, anchors in states)
|
||||
|
||||
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
|
||||
head, contrast, _ = choose_motifs(analysis, sequences, boundaries[1])
|
||||
seed = generator.solo.policy.stable_rank(generator.solo.analysis.identity(analysis, measures, part), generator.solo.policy.SEED_NAMESPACE)
|
||||
activity = fractions.Fraction(str(generator.solo.policy.DEFAULT.acoustic_activity if part == 'acoustic' else generator.solo.policy.DEFAULT.electric_activity))
|
||||
result, index, passage_index, inventions = [], 0, 0, {}
|
||||
cadences = resolutions(choices)
|
||||
while index < len(measures) and boundaries[index] < end:
|
||||
start = boundaries[index]
|
||||
possible = list(range(min(index + 2, len(measures)), min(index + 4, len(measures)) + 1))
|
||||
stops = sorted({min(index + size, len(measures)) for size in (4, 6, 8)})
|
||||
|
||||
def boundary_rank(stop):
|
||||
time = boundaries[stop]
|
||||
before = next((c for c in reversed(choices) if c.span.start < time), None)
|
||||
after = next((c for c in choices if c.span.start >= time and c.span.chord), None)
|
||||
resolution = before and after and before.span.chord and generator.scales.selection.dominant(before.span.chord) and (generator.scales.selection.root_number(before.span.chord) - generator.scales.selection.root_number(after.span.chord)) % 12 == 7
|
||||
source_index = min(stop + int(len(analysis.boundaries) > len(measures) + 1), len(analysis.boundaries) - 1)
|
||||
source_time = analysis.boundaries[source_index]
|
||||
preceding = [n for n in analysis.melody if n.start < source_time]
|
||||
breathing = bool(preceding) and preceding[-1].end <= source_time and (preceding[-1].end < source_time or preceding[-1].end - preceding[-1].start >= 2)
|
||||
return not resolution, not breathing, abs(stop - index - 3), stop
|
||||
def destination_rank(stop):
|
||||
finish = boundaries[stop]
|
||||
last = next((c for c in reversed(choices) if c.scale and c.span.start < finish and c.span.end > start), None)
|
||||
resolved = bool(last and any(start < c.span.start < finish and same_harmony(c, last) for c in cadences))
|
||||
dominant = bool(last and generator.scales.selection.dominant(last.span.chord))
|
||||
return (0 if resolved else 2 if dominant else 1), abs(stop - index - 4), finish
|
||||
|
||||
stop = min(possible, key=boundary_rank)
|
||||
stop = min(stops, key=destination_rank)
|
||||
finish = boundaries[stop]
|
||||
final = finish == end
|
||||
phase = (start + finish) / (2 * end)
|
||||
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]
|
||||
if finish <= start:
|
||||
break
|
||||
passage_index += 1
|
||||
passage_id = f'{part}_passage_{passage_index}'
|
||||
passage_seed = generator.solo.policy.stable_rank(seed, (str(start), str(finish)))
|
||||
low, high = (55, 74) if part == 'acoustic' else (59, 79)
|
||||
# Reserve the upper register for a prepared peak, not every active passage.
|
||||
phase = (start + finish) / (2 * end)
|
||||
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(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))
|
||||
bar = boundaries[index + 1] - start
|
||||
release = fractions.Fraction(1, 2) if decision(passage_seed, 'breathing') % 3 == 0 else fractions.Fraction()
|
||||
release = min(release, (finish - start) / 8)
|
||||
envelope = activity_passages(start, finish, release, activity, decision(passage_seed, 'activity') % 3)
|
||||
anchors = anchor_backbones(passage_id, start, finish, release, register, choices, bar, passage_seed)
|
||||
# Prefer four-bar phrasing; extend when a sustained harmony crosses that seam.
|
||||
middle = min(index + 4, stop)
|
||||
before = next((c for c in reversed(choices) if c.span.start < boundaries[middle]), None)
|
||||
after = next((c for c in choices if c.span.start <= boundaries[middle] < c.span.end), None)
|
||||
extended = stop - index > 4 and same_harmony(before, after)
|
||||
phrase_stops = [stop] if extended or middle == stop else [middle, stop]
|
||||
local = tuple(c for c in choices if c.span.start < boundaries[phrase_stops[0]] and c.span.end > start)
|
||||
slot = decision(passage_seed, 'motif') % 3
|
||||
if part == 'acoustic':
|
||||
slot = 0 if passage_index <= 2 else slot
|
||||
if slot == 2 and 0 not in inventions:
|
||||
inventions[0] = new_motif(sequences, bar, choices=local, register=register, seed=passage_seed)
|
||||
motif = head if slot == 0 else contrast if slot == 1 else inventions[0]
|
||||
else:
|
||||
slot = 0 if passage_index == 1 else slot
|
||||
if slot < 2 and slot not in inventions:
|
||||
inventions[slot] = new_motif(sequences, bar, choices=local, register=register, seed=passage_seed, variant=slot)
|
||||
motif = head if slot == 2 else inventions[slot]
|
||||
gestures, cursor = [], start
|
||||
while cursor < finish - release:
|
||||
gesture_end = min(cursor + bar, finish - release)
|
||||
closing = gesture_end == finish - release
|
||||
changed = any(c.span.start == cursor and c.scale and prior.scale and c.scale.pitches != prior.scale.pitches for prior, c in zip(choices, choices[1:]))
|
||||
relationship = 'statement' if not gestures else 'close' if closing else 'repeat_with_harmonic_adaptation' if changed else 'answer' if cursor >= start + (finish - start) / 2 else 'extend'
|
||||
treatment = 'opening_reference' if not gestures and motif.origin == 'melody' else 'invented' if not gestures else 'rhythmic_reference' if relationship == 'repeat_with_harmonic_adaptation' else 'answer' if relationship in ('answer', 'close') else 'contour_reference'
|
||||
gestures.append(generator.solo.types.Gesture(f'{passage_id}_gesture_{len(gestures) + 1}', cursor, gesture_end, relationship, treatment))
|
||||
cursor = gesture_end
|
||||
phrase_start = start
|
||||
for phrase_stop in phrase_stops:
|
||||
phrase_end = boundaries[phrase_stop]
|
||||
terminal = phrase_end == finish
|
||||
treatment = 'opening_reference' if motif.origin == 'melody' else 'invented'
|
||||
local_gestures = tuple(g for g in gestures if g.start < phrase_end and g.end > phrase_start)
|
||||
landmarks = tuple(generator.solo.types.Landmark(a.earliest, a.latest, min(v[i].pitch.number() for v in anchors), max(v[i].pitch.number() for v in anchors), 0, a.role) for i, a in enumerate(anchors[0])) if anchors and anchors[0] else ()
|
||||
result.append(generator.solo.types.PhrasePlan(f'{part}_phrase_{len(result) + 1}', phrase_start, phrase_end, motif, 'statement' if phrase_start == start else 'continue', treatment, None, register, activity, release if terminal else fractions.Fraction(), envelope, landmarks, 'breath' if terminal and release else 'sustain' if terminal else 'connected', 'closed' if terminal else 'open', passage_seed, passage_id, start, finish, anchors, local_gestures))
|
||||
phrase_start = phrase_end
|
||||
index = stop
|
||||
return tuple(result)
|
||||
|
||||
Reference in New Issue
Block a user