@@ -0,0 +1,90 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import fractions
|
||||
|
||||
import generator.harmony
|
||||
import generator.model
|
||||
import generator.scales.selection
|
||||
import generator.solo.analysis
|
||||
import generator.solo.sequences
|
||||
import generator.solo.types
|
||||
|
||||
def target_pitches(chord: dict, final: bool) -> frozenset[int]:
|
||||
degrees = generator.harmony.degrees(chord)
|
||||
explicit = {d['value'] for d in chord.get('degrees', []) if d['type'] != 'subtract'}
|
||||
preferred = {1, 3} if final else {3, 7}
|
||||
preferred |= explicit | {d for d, a in degrees.items() if d == 5 and a}
|
||||
if 3 not in degrees:
|
||||
preferred |= {2, 4}
|
||||
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)):
|
||||
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')
|
||||
|
||||
def new_motif(sequences: generator.solo.sequences.Library, length: fractions.Fraction) -> generator.solo.types.Motif:
|
||||
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')
|
||||
path = min(paths, key=lambda s: (sum(a * b >= 0 for a, b in zip(s.steps, s.steps[1:])), s.rank()))
|
||||
count = len(path.steps) + 1
|
||||
unit = min(fractions.Fraction(1, 2), length / (count + 1))
|
||||
notes = []
|
||||
for i, offset in enumerate(path.offsets()):
|
||||
octave, degree = divmod(28 + offset, 7)
|
||||
pitch = generator.solo.types.Pitch(generator.model.LETTERS[degree], fractions.Fraction(), octave)
|
||||
notes.append(generator.solo.types.Note(unit * i, unit * (i + 1), pitch))
|
||||
return generator.solo.types.Motif('new_' + path.id.replace('/', '_').removesuffix('.xml'), tuple(notes), length, (), 'sequence ' + path.id)
|
||||
|
||||
def choose_motifs(analysis, sequences, length):
|
||||
eligible = [m for m in analysis.motifs if m.duration <= 2 * length]
|
||||
head = next((m for m in eligible if len(m.notes) >= 2), eligible[0] if eligible else new_motif(sequences, length))
|
||||
others = [m for m in eligible if m.id != head.id]
|
||||
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 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])
|
||||
result, index = [], 0
|
||||
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))
|
||||
|
||||
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
|
||||
|
||||
stop = min(possible, key=boundary_rank)
|
||||
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'
|
||||
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))
|
||||
index = stop
|
||||
return tuple(result)
|
||||
Reference in New Issue
Block a user