+55
-44
@@ -1,60 +1,71 @@
|
||||
"""Compact reports of actual selected phrases, not duplicated draft ancestry."""
|
||||
import dataclasses, fractions, json
|
||||
import generator.solo.placement
|
||||
from generator.solo import harmonic
|
||||
import generator.solo.harmonic
|
||||
import collections
|
||||
import generator.fretboard
|
||||
|
||||
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)
|
||||
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.harmony)
|
||||
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=[]
|
||||
harmony = generator.solo.harmonic.Harmony(solo.harmony)
|
||||
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 = []
|
||||
for p in solo.phrases:
|
||||
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,
|
||||
'identifying_events':plain(p.identity),
|
||||
'evaluation':plain(p.evaluation),'score':p.score,'expressions':plain(p.expressions)}
|
||||
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,
|
||||
'identifying_events': plain(p.identity),
|
||||
'evaluation': plain(p.evaluation), 'score': p.score, 'expressions': plain(p.expressions)}
|
||||
phrases.append(entry)
|
||||
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':{'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_policy':'contextual (shared by authored and predicted music)', 'harmonic_valid':checked.valid,'harmonic_error':checked.error,
|
||||
'harmonic_exposure':checked.exposure,'unresolved_treatments':plain(checked.pending)}
|
||||
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': {'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_policy': 'contextual (shared by authored and predicted music)', 'harmonic_valid': checked.valid, 'harmonic_error': checked.error,
|
||||
'harmonic_exposure': checked.exposure, 'unresolved_treatments': plain(checked.pending)}
|
||||
|
||||
def describe(solo,measure_offset=0,time_offset=0,*,detail=None):
|
||||
detail=detail or structure(solo)
|
||||
lines=[f'{solo.diagnostics.get("construction","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'])]
|
||||
def describe(solo, measure_offset=0, time_offset=0, *, detail=None):
|
||||
detail = detail or structure(solo)
|
||||
lines = [f'{solo.diagnostics.get("construction", "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'
|
||||
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
|
||||
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.start for c in solo.harmony if c.beat == 1}) for n 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.start for c in solo.harmony if c.beat == 1}) for n in notes)}
|
||||
|
||||
Reference in New Issue
Block a user