Comp
This commit is contained in:
@@ -0,0 +1,115 @@
|
||||
"""Accepted, paired pitch/rhythm examples; production uses contextual profiles."""
|
||||
from __future__ import annotations
|
||||
|
||||
import dataclasses
|
||||
import fractions
|
||||
import hashlib
|
||||
import json
|
||||
import pathlib
|
||||
|
||||
import generator.harmony
|
||||
import generator.model
|
||||
import generator.solo.types as types
|
||||
|
||||
F = fractions.Fraction
|
||||
|
||||
@dataclasses.dataclass(frozen=True)
|
||||
class ReferencePhrase:
|
||||
id: str
|
||||
version: str
|
||||
notes: tuple[types.Note, ...]
|
||||
decorated: tuple[types.Note, ...]
|
||||
duration: F
|
||||
tonic: int
|
||||
harmony: tuple[tuple[F, str], ...]
|
||||
features: tuple[tuple[str, F, F], ...]
|
||||
runs: tuple[tuple[F, F], ...]
|
||||
acceptance: str
|
||||
def windows(self):
|
||||
# Windows retain their parent and context; they are not separate approvals.
|
||||
result = []
|
||||
for length in (F(8), F(16)):
|
||||
for start in range(0, int(self.duration), int(length)):
|
||||
end = min(self.duration, start + length)
|
||||
selected = tuple(dataclasses.replace(n, start=n.start - start, end=n.end - start) for n in self.notes if start <= n.start and n.end <= end)
|
||||
if len(selected) >= 2:
|
||||
chords = tuple(chord for time, chord in self.harmony if start <= time < end)
|
||||
result.append(types.Motif(f'{self.id}:{start}:{end}', selected, end - start, chords, 'accepted reference profile'))
|
||||
return tuple(result)
|
||||
|
||||
def read(path):
|
||||
raw = path.read_bytes()
|
||||
data = json.loads(raw)
|
||||
if data['pitch_convention'] != 'sounding' or not data.get('acceptance'):
|
||||
raise ValueError(f'{path}: reference requires sounding pitches and explicit acceptance provenance')
|
||||
|
||||
def notes(variant):
|
||||
result, cursor = [], F()
|
||||
for i, event in enumerate(data['variants'][variant]['events']):
|
||||
start, duration = F(event['onset_beats']), F(event['duration_beats'])
|
||||
if start != cursor or duration <= 0:
|
||||
raise ValueError(f'{path}: reference events must fill exact sequential time')
|
||||
cursor = start + duration
|
||||
if event['pitch'] is None:
|
||||
continue
|
||||
name = event['pitch']
|
||||
pitch = types.Pitch(name[0].lower(), F(1 if '#' in name else -1 if 'b' in name else 0), int(name[-1]))
|
||||
if pitch.number() != event['midi_pitch']:
|
||||
raise ValueError(f'{path}: inconsistent sounding pitch {name}')
|
||||
result.append(types.Note(start, cursor, pitch, (data['id'],), event_id=f'{variant}_{i}'))
|
||||
return tuple(result), cursor
|
||||
|
||||
plain, duration = notes('lead_theme')
|
||||
decorated, other_duration = notes('lead_with_runs')
|
||||
if duration != other_duration or len(plain) != data['variants']['lead_theme']['attacks'] or len(decorated) != data['variants']['lead_with_runs']['attacks']:
|
||||
raise ValueError(f'{path}: inconsistent reference variants')
|
||||
tonic = generator.model.parse_pitch_name(data['tonic'])
|
||||
number = int(generator.model.NATURAL[tonic['letter']] + generator.model.alteration(tonic['alter']))
|
||||
harmony = tuple((F(4 * (h['bar'] - 1)), h['chord']) for h in data['harmony'])
|
||||
for _, chord in harmony:
|
||||
generator.harmony.parse_symbol(chord)
|
||||
return ReferencePhrase(data['id'], hashlib.sha256(raw).hexdigest(), plain, decorated, duration, number, harmony,
|
||||
tuple((f['id'], F(f['start']), F(f['end'])) for f in data['features']), tuple((F(w['start']), F(w['end'])) for w in data['run_windows']), data['acceptance'])
|
||||
|
||||
def load_library(root: pathlib.Path):
|
||||
return tuple(read(path) for path in sorted(root.glob('*.json')))
|
||||
|
||||
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 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:]))
|
||||
timing = sum(a != b for a, b in zip(rhythm, feature.rhythm)) / max(1, len(rhythm))
|
||||
contour = sum(((a > 0) - (a < 0)) != ((b > 0) - (b < 0)) or abs(a - b) > 2 for a, b in zip(movement, feature.intervals)) / max(1, len(movement))
|
||||
return (timing + contour) / 2
|
||||
|
||||
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)
|
||||
Reference in New Issue
Block a user