57 lines
3.0 KiB
Python
57 lines
3.0 KiB
Python
"""Pure musical contracts; no reference files or filesystem access."""
|
|
from __future__ import annotations
|
|
import dataclasses
|
|
import fractions
|
|
import generator.solo.types as types
|
|
F = fractions.Fraction
|
|
|
|
def identifying_features(notes, source):
|
|
"""Select a characteristic leap followed by return, wherever it occurs."""
|
|
notes = tuple(notes)
|
|
candidates = []
|
|
for i in range(len(notes) - 2):
|
|
a, b, c = notes[i:i + 3]
|
|
leap, reply = b.pitch.number() - a.pitch.number(), c.pitch.number() - b.pitch.number()
|
|
if 3 <= abs(leap) <= 9 and leap * reply < 0 and c.start - a.start <= 4:
|
|
group = notes[i:min(len(notes), i + 4)]
|
|
rhythm = tuple((n.start - a.start, n.end - n.start) for n in group)
|
|
intervals = tuple(y.pitch.number() - x.pitch.number() for x, y in zip(group, group[1:]))
|
|
candidates.append(types.IdentifyingFeature(a.event_id + '_identity', tuple(n.event_id for n in group), rhythm, intervals, source))
|
|
return tuple(candidates[:1])
|
|
|
|
def feature_error(feature, notes):
|
|
by_id = {n.event_id: n for n in notes}
|
|
actual = [by_id.get(identity) for identity in feature.events]
|
|
if not actual or not all(actual):
|
|
return 1.0
|
|
first = actual[0].start
|
|
rhythm = tuple((n.start - first, n.end - n.start) for n in actual)
|
|
movement = tuple(b.pitch.number() - a.pitch.number() for a, b in zip(actual, actual[1:]))
|
|
errors = []
|
|
if 'rhythm' in feature.preserves:
|
|
errors.append(1.0 if len(rhythm) != len(feature.rhythm) else sum(a != b for a,b in zip(rhythm,feature.rhythm))/max(1,len(rhythm)))
|
|
if 'intervals' in feature.preserves or 'contour' in feature.preserves:
|
|
if len(movement) != len(feature.intervals) or not movement and feature.intervals:
|
|
errors.append(1.0)
|
|
else:
|
|
errors.append(sum(((a>0)-(a<0)) != ((b>0)-(b<0)) or 'intervals' in feature.preserves and abs(a-b)>2 for a,b in zip(movement,feature.intervals))/max(1,len(movement)))
|
|
if 'exact_intervals' in feature.preserves:
|
|
errors.append(float(movement != feature.intervals))
|
|
if 'pitches' in feature.preserves:
|
|
errors.append(float(tuple(n.pitch.number() for n in actual) != feature.pitches))
|
|
return max(errors,default=0.0)
|
|
|
|
def relationship(notes, reference):
|
|
"""One joint relationship cost: timed movement, not absolute-pitch copying."""
|
|
notes = tuple(notes)
|
|
if len(notes) < 2 or len(reference.notes) < 2:
|
|
return 0.0 if len(notes) == len(reference.notes) == 1 else 1.0
|
|
actual = tuple(b.pitch.number() - a.pitch.number() for a, b in zip(notes, notes[1:]))
|
|
expected = reference.contour()
|
|
count = min(len(actual), len(expected))
|
|
contour = sum(0.75 * (((a > 0) - (a < 0)) != ((b > 0) - (b < 0))) + 0.25 * min(1, max(0, abs(a - b) - 2) / 7) for a, b in zip(actual, expected)) / max(1, count)
|
|
start, ref_start = notes[0].start, reference.notes[0].start
|
|
rhythm = sum(min(1, float(abs((n.start - start) - (r.start - ref_start))) / 2) for n, r in zip(notes, reference.notes)) / min(len(notes), len(reference.notes))
|
|
evidence = max(0, min(3, len(reference.notes)) - len(notes)) / 3
|
|
return max(evidence, (contour + rhythm) / 2)
|