75 lines
4.2 KiB
Python
75 lines
4.2 KiB
Python
"""Compact reports of actual selected phrases, not duplicated draft ancestry."""
|
||
import collections
|
||
import dataclasses, fractions, json
|
||
|
||
import generator.solo.harmonic
|
||
import generator.solo.placement
|
||
import generator.vocabulary
|
||
|
||
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, *, composition=None):
|
||
composition = composition or generator.vocabulary.current()
|
||
harmony = generator.solo.harmonic.Harmony(solo.harmony, composition=composition)
|
||
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)}
|
||
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), 'guitar_placement': generator.solo.placement.describe(solo.notes(), solo.tempo),
|
||
'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, composition=None):
|
||
composition = composition or generator.vocabulary.current()
|
||
detail = detail or structure(solo, composition=composition)
|
||
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'
|
||
|
||
def fretboard_report(solo):
|
||
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)}
|