Files
product-vamp/generator/solo/report.py
T
khodowany 0e68f5364b Cleanup
2026-10-04 18:48:02 -05:00

62 lines
4.0 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""Compact reports of actual selected phrases, not duplicated draft ancestry."""
import dataclasses, fractions, json
import generator.solo.placement
from generator.solo import harmonic
def plain(value):
if dataclasses.is_dataclass(value):return plain(dataclasses.asdict(value))
if isinstance(value,dict):return {k:plain(v) for k,v in value.items()}
if isinstance(value,(tuple,list,set,frozenset)):return [plain(v) for v in value]
if isinstance(value,fractions.Fraction):return str(value)
return value
def structure(solo):
harmony=harmonic.Harmony(solo.choices)
contracts=tuple(c for p in solo.phrases for c in p.expressions)
checked=harmony.evaluate(solo.notes(),until=solo.end,complete=True,expressions=contracts)
phrases=[];endings=[]
for p in solo.phrases:
anchor=p.destination
completed=anchor is None or harmonic.anchor_note(anchor,p.notes) is not None
entry={'id':p.id,'start':str(p.start),'end':str(p.end),'purpose':p.purpose,'direction':p.direction,'ending':p.ending,
'theme':p.theme,'notes':plain(p.notes),'rhythm_groups':plain(p.groups),'sources':p.sources,
'anchors':[{**plain(anchor),'pitch':anchor.pitch.label(),'completed':completed}] if anchor else [],
'ending_completed':completed,'evaluation':plain(p.evaluation),'score':p.score,'expressions':plain(p.expressions)}
phrases.append(entry)
if anchor:endings.append({'phrase':p.id,'effect':p.ending,'arrival':str(anchor.earliest),'pitch':anchor.pitch.label(),'required_beats':str(anchor.duration),'completed':completed})
ns=solo.notes()
return {'schema':'composer-report-3','selection_trace':plain({k:v for k,v in solo.diagnostics.items() if not k.endswith('_seconds')}),
'instrument_profile':plain(solo.profile),'phrases':phrases,'expressions':plain(contracts),'fretboard':fretboard_report(solo),
'phrasing':{'endings':endings,'attacks':len(ns),'attacks_per_four_beats':4*len(ns)/float(solo.end) if solo.end else 0,
'occupancy':sum(float(n.end-n.start) for n in ns)/float(solo.end) if solo.end else 0},
'harmonic_roles':harmony.describe(ns,expressions=contracts),'harmonic_valid':checked.valid,'harmonic_error':checked.error,
'harmonic_exposure':checked.exposure,'unresolved_treatments':plain(checked.pending)}
def describe(solo,scales,measure_offset=0,time_offset=0,*,detail=None):
detail=detail or structure(solo)
lines=[f'Independent complete-phrase solo: {solo.part}', 'Times are exact quarter beats from the solo start; pitches sound one octave below guitar notation.',
'Approximate fingering costs are not a biomechanical guarantee.',json.dumps(detail['phrasing'])]
for p in solo.phrases:
lines.append(f'{p.id}: {p.start}–{p.end}; {p.purpose}, {p.direction}, {p.ending}; theme {p.theme}; score {p.score:.4f}')
for n in p.notes:lines.append(f' {n.start}–{n.end}: {n.pitch.label()}, {n.role}; string {n.position.string_number}, fret {n.position.fret}')
for c in p.expressions:lines.append(f' {c.kind}/{c.rule} → {c.target.pitch.label()} at {c.target.start}, held to {c.target.end}')
return '\n'.join(lines)+'\n'
def fretboard_report(solo):
import collections
import generator.fretboard
notes=solo.notes()
attacks=collections.Counter(); durations=collections.Counter(); forced=optional=0
for note in notes:
string=note.position.string_number
attacks[string]+=1;durations[string]+=float(note.end-note.start)
if string==1:
if len(generator.solo.placement.positions(note.pitch.number(),solo.profile))==1:forced+=1
else:optional+=1
total=sum(durations.values())
return {'attacks_by_string':dict(sorted(attacks.items())),
'duration_fraction_by_string':{s:d/max(.001,total) for s,d in sorted(durations.items())},
'first_string_forced':forced,'first_string_optional':optional,
'range_midi': [min(n.pitch.number() for n in notes),max(n.pitch.number() for n in notes)] if notes else [],
'cross_bar_sounding_events':sum(any(n.start < t < n.end for t in {c.span.start for c in solo.choices if c.span.beat == 1}) for n in notes)}