103 lines
5.8 KiB
Python
103 lines
5.8 KiB
Python
"""One complete-event evaluator, including the declared musical relationships.
|
|
|
|
The inputs are realized notes and independently planned obligations. Action
|
|
counts and constructor-family labels never award musical credit.
|
|
"""
|
|
import collections
|
|
import fractions
|
|
import generator.solo.harmonic
|
|
import generator.solo.scoring
|
|
import generator.solo.types
|
|
|
|
VERSION = 'directed-phrases-2'
|
|
WEIGHTS = {'continuity': 2, 'repetition': 3, 'interruption': 2, 'activity': 2,
|
|
'harmony': 2, 'register': 1, 'identity': 3, 'arrival': 2}
|
|
|
|
|
|
def signature(notes):
|
|
notes = generator.solo.types.sounding_notes(notes)
|
|
if not notes:
|
|
return ()
|
|
return tuple((n.start-notes[0].start, n.end-n.start, n.pitch.number()-notes[0].pitch.number()) for n in notes)
|
|
|
|
|
|
def theme_distance(home, sample, purpose):
|
|
if not home or purpose not in ('return','develop','answer','extend'):
|
|
return fractions.Fraction()
|
|
# Returns preserve the whole introduced signature; development preserves
|
|
# its selected opening intervals, allowing the rhythm and conclusion to change.
|
|
count = len(home) if purpose=='return' else min(3,len(home))
|
|
if count<3 or len(sample)<count:
|
|
return fractions.Fraction(2)
|
|
a,b = signature(home[:count]),signature(sample[:count])
|
|
intervals = [(x[i][2]-x[i-1][2]) for x in (a,b) for i in range(1,count)]
|
|
left,right = intervals[:count-1],intervals[count-1:]
|
|
pitch = sum(min(fractions.Fraction(1),fractions.Fraction(abs(x-y),4)) for x,y in zip(left,right))/(count-1)
|
|
rhythm = sum((x[0],x[1])!=(y[0],y[1]) for x,y in zip(a,b))/fractions.Fraction(count) if purpose=='return' else 0
|
|
return pitch+rhythm
|
|
|
|
|
|
def figures(notes):
|
|
return tuple(signature(notes[i:i+4]) for i in range(max(0,len(notes)-3)))
|
|
|
|
|
|
def evaluate(notes, start, end, harmony, *, incoming=(), home=(), purpose='develop', contracts=(), center=60,
|
|
destinations=(), history=(), identity=()):
|
|
notes = generator.solo.types.sounding_notes(notes)
|
|
duration = end-start
|
|
if duration <= 0:
|
|
raise ValueError('Evaluation requires a positive span')
|
|
checked = harmony.evaluate(notes, expressions=contracts, until=end, complete=True)
|
|
anchors_valid,_ = generator.solo.harmonic.anchors_valid(destinations,notes,end,complete=True,closure='closed')
|
|
sounding = sum((n.end-n.start for n in notes), fractions.Fraction())
|
|
pitches = [n.pitch.number() for n in notes]
|
|
joined = [n.pitch.number() for n in incoming[-1:]]+pitches
|
|
intervals = [b-a for a,b in zip(joined, joined[1:])]
|
|
continuity = fractions.Fraction(sum(max(0, abs(d)-7) for d in intervals),max(1,len(intervals)))
|
|
oscillation = sum(a == -b == c and a != 0 for a,b,c in zip(intervals,intervals[1:],intervals[2:]))
|
|
patterns = collections.Counter(figures(notes))
|
|
recent = collections.Counter(f for p in history[-3:] for f in figures(generator.solo.types.sounding_notes(p.notes)))
|
|
# A single clear return is allowed. Repeating the same figure several times
|
|
# still costs something; a "return" label is not unlimited immunity.
|
|
overuse = sum(max(0,n+recent[f]-2) for f,n in patterns.items())
|
|
repetition = fractions.Fraction(oscillation+sum(a==b for a,b in zip(pitches,pitches[1:]))+overuse,max(1,len(notes)))
|
|
arrival_ids = {n.event_id for a in destinations if (n:=generator.solo.harmonic.anchor_note(a,notes))}
|
|
interruption = fractions.Fraction(sum(a.end<b.start and a.end-a.start<1 and b.end-b.start<1
|
|
and a.event_id not in arrival_ids for a,b in zip(notes,notes[1:])),max(1,len(notes)))
|
|
density = fractions.Fraction(len(notes)*4,1)/duration
|
|
active = any(s.scale and s.start<end and s.end>start for s in harmony.spans)
|
|
activity = max(0,density-7)+(2 if active and not notes else 0)
|
|
if notes and sounding<duration/3:
|
|
activity += (duration/3-sounding)/duration
|
|
# Unplanned broad notes in the moving portion have a different function from
|
|
# a fulfilled arrival. This does not penalize the required held destination.
|
|
motion = [n for n in notes if n.event_id not in arrival_ids and n.role!='quote']
|
|
activity += sum((max(0,n.end-n.start-2) for n in motion),fractions.Fraction())/duration
|
|
identity_cost = theme_distance(home,identity or notes,purpose)
|
|
register = sum(( (n.end-n.start)*max(0,abs(n.pitch.number()-center)-10) for n in notes),fractions.Fraction())/max(1,sounding)
|
|
arrival_cost = fractions.Fraction()
|
|
for anchor in destinations:
|
|
target = generator.solo.harmonic.anchor_note(anchor,notes)
|
|
if target is None:
|
|
arrival_cost += 2
|
|
continue
|
|
before = [n for n in notes if n.end<=target.start][-3:]
|
|
if before:
|
|
distances = [abs(n.pitch.number()-target.pitch.number()) for n in before]+[0]
|
|
# A contour may turn. What matters here is whether the final approach
|
|
# prepares its already selected destination, rather than jumping to it.
|
|
arrival_cost += fractions.Fraction(max(0,distances[-2]-2),6)
|
|
arrival_cost += fractions.Fraction(sum(max(0,b-a) for a,b in zip(distances,distances[1:])),12)
|
|
if before[-1].end<target.start:
|
|
supported_gap = any(c.target.event_id==target.event_id and c.events[-1].end==before[-1].end for c in contracts)
|
|
if not supported_gap:
|
|
arrival_cost += min(1,target.start-before[-1].end)
|
|
arrival_cost /= max(1,len(destinations))
|
|
values = dict(continuity=continuity,repetition=repetition,interruption=interruption,
|
|
activity=activity,harmony=checked.exposure,register=register,identity=identity_cost,arrival=arrival_cost)
|
|
raw = {k:fractions.Fraction(str(v)).limit_denominator(1000000) for k,v in values.items()}
|
|
contributions = {k:v*WEIGHTS[k] for k,v in raw.items()}
|
|
return generator.solo.scoring.Evaluation(VERSION,checked.valid and anchors_valid,raw,dict(WEIGHTS),contributions,
|
|
sum(contributions.values(),fractions.Fraction()),{'span':str(duration),'attacks':len(notes),'sounding':str(sounding),
|
|
'destinations':len(destinations),'identity_events':len(identity or notes[:len(home)]),'history_phrases':min(3,len(history))})
|