"""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(a),'pitch':a.pitch.label(),'completed':harmonic.anchor_note(a,p.notes) is not None} for a in p.arrivals or ((anchor,) if anchor else ())], 'identifying_events':plain(p.identity), '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() duration=solo.end-solo.start 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),'allocation_beats':str(duration), 'attacks_per_four_beats':4*len(ns)/float(duration) if duration else 0, 'occupancy':sum(float(n.end-n.start) for n in ns)/float(duration) if duration 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 melodic-phrase solo: {solo.part}', 'Times are exact quarter beats from the chorus start; negative times are allocated lead-ins. 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)}