53 lines
2.7 KiB
Python
53 lines
2.7 KiB
Python
"""Bounded timing revisions of complete realized gestures, with fixed pitches."""
|
|
from __future__ import annotations
|
|
|
|
import dataclasses
|
|
from fractions import Fraction as F
|
|
|
|
from generator.solo import drafts, harmonic, types
|
|
|
|
|
|
def fragmentation(notes):
|
|
isolated = sum(n.end-n.start <= F(1,2) and a.end < n.start and n.end < b.start
|
|
for a,n,b in zip(notes, notes[1:], notes[2:]))
|
|
gaps = sum(0 < b.start-a.end <= 1 for a,b in zip(notes, notes[1:]))
|
|
return isolated + .1*gaps
|
|
|
|
|
|
def revise(draft, harmony, treatments=()):
|
|
notes=draft.notes; edits=[]
|
|
if len(notes)<3: return draft, ()
|
|
protected=set(draft.protected)|set(draft.structural)|{n.event_id for n in notes if n.role=='quote'}
|
|
# An identifying rhythm also protects its internal rests.
|
|
protected.update(e for feature in draft.features if 'rhythm' in feature.preserves for e in feature.events)
|
|
for _ in range(2):
|
|
proposals=[]
|
|
for i,(a,n,b) in enumerate(zip(notes,notes[1:],notes[2:]),1):
|
|
if n.event_id in protected or n.end-n.start>1: continue
|
|
if not (a.end<n.start or n.end<b.start): continue
|
|
if a.event_id not in protected and 0<n.start-a.end<=1:
|
|
proposals.append((*notes[:i-1],dataclasses.replace(a,end=n.start),*notes[i:]))
|
|
if 0<b.start-n.end<=1:
|
|
proposals.append((*notes[:i],dataclasses.replace(n,end=b.start),*notes[i+1:]))
|
|
for delta in (F(-1,2),F(1,2)):
|
|
if a.end<=n.start+delta and n.end+delta<=b.start:
|
|
proposals.append((*notes[:i],dataclasses.replace(n,start=n.start+delta,end=n.end+delta),*notes[i+1:]))
|
|
if len(proposals)>=8: break
|
|
valid=[]
|
|
for actual in proposals[:8]:
|
|
if fragmentation(actual)>=fragmentation(notes): continue
|
|
if not drafts.contract_valid(draft,actual)[0]: continue
|
|
if not harmony.evaluate(actual,treatments,until=draft.end,complete=True).valid: continue
|
|
if not harmonic.anchors_valid(draft.anchors,actual,draft.end,complete=True,closure='closed')[0]: continue
|
|
valid.append(actual)
|
|
if not valid: break
|
|
actual=min(valid,key=lambda ns:(fragmentation(ns),tuple((n.start,n.end,n.pitch.number()) for n in ns)))
|
|
changed=[(a,b) for a,b in zip(notes,actual) if (a.start,a.end)!=(b.start,b.end)]
|
|
edits.append(types.DraftEdit('connected_rhythm',min(min(a.start,b.start) for a,b in changed),
|
|
max(max(a.end,b.end) for a,b in changed),tuple(a.event_id for a,b in changed),
|
|
tuple(b.event_id for a,b in changed),'same pitch sequence and attack count; connect an editable gesture'))
|
|
notes=actual
|
|
transforms=tuple(dataclasses.replace(t,rhythm=tuple((n.start-t.start,n.end-t.start) for n in notes if n.event_id in t.events))
|
|
if not t.preserves else t for t in draft.transformations)
|
|
return dataclasses.replace(draft,notes=notes,transformations=transforms),tuple(edits)
|