from __future__ import annotations import dataclasses import json import fractions import generator.harmony import generator.model import generator.scales.report import generator.scales.selection import generator.scales.timeline import generator.solo.types import generator.solo.harmonic import generator.solo.drafts def describe(solo: generator.solo.types.Solo, scales, measure_offset=0, time_offset=0) -> str: region = generator.scales.timeline.Solo(time_offset, time_offset + solo.end, solo.part.capitalize() + ' Solo') positioned = tuple(dataclasses.replace(c, span=dataclasses.replace(c.span, start=c.span.start + time_offset, end=c.span.end + time_offset, measure=c.span.measure + measure_offset)) for c in solo.choices) lines = [generator.scales.report.passage(solo.part, region, positioned, scales), f'Phrase composition: {solo.part}', f'Placement reference tempo: {solo.tempo} quarter notes/minute. Runtime tempo changes are not a comfort guarantee.', 'Times below are quarter-note offsets from the start of this solo; pitches are sounding.'] if solo.diagnostics: lines.append('Composition diagnostics: ' + json.dumps({k: v for k, v in solo.diagnostics.items() if not k.endswith('_seconds')}, sort_keys=True)) detail = structure(solo) for passage in detail['passages']: lines.append(f"Melodic passage {passage['id']}: {passage['start']}-{passage['end']}; phrases {', '.join(passage['phrases'])}") lines.append(f"Harmonic exposure: {detail['harmonic_exposure']:.4f}; unresolved treatments: {len(detail['unresolved_treatments'])}") for phrase in solo.phrases: plan = phrase.plan lines.extend(('', f'{plan.id}: {plan.start}-{plan.end}; {plan.relationship}; motif {plan.motif.id}', f' Origin: {plan.motif.origin}; source events: ' + ', '.join(s for n in plan.motif.notes for s in n.sources))) lines.append(f' Contract: {plan.treatment}; closure: {plan.closure}; ending: {plan.ending}; score: {phrase.score:.4f}; identity: {plan.seed}') if phrase.draft: draft = phrase.draft lines.append(f' Complete draft {draft.id}: {draft.relationship}; home {draft.home.id if draft.home else "none"}; {len(draft.notes)} attacks; rests {draft.rests()}') for transform in draft.transformations: lines.append(f' Development {transform.id}: {transform.operation}; {transform.start}-{transform.end}; parent {transform.parent}; home {transform.home}; preserves {transform.preserves}') for edit in phrase.edits: lines.append(f' Revision {edit.kind}: {edit.start}-{edit.end}; {edit.source}; replaced {edit.removed}; realized {edit.added}') for passage in plan.passages: lines.append(f' Activity {passage.start}-{passage.end}: {passage.intent}; attacks/beat {float(passage.activity):.3f}; occupancy {float(passage.occupancy):.2f}') for landmark in plan.landmarks: lines.append(f' Landmark {landmark.name}: {landmark.earliest}-{landmark.latest}; sounding MIDI {landmark.low}-{landmark.high}; direction {landmark.direction}') if plan.target: lines.append(f' Destination: {plan.target.chord}; {plan.target.reason}; arrival {plan.target.earliest}-{plan.target.latest}') for anchor in phrase.anchors: actual = generator.solo.harmonic.anchor_note(anchor, phrase.state.notes if phrase.state else solo.notes()) lines.append(f' Anchor {anchor.id}: {anchor.role}; {anchor.chord}; {anchor.pitch.label()}; window {anchor.earliest}-{anchor.latest}; hold {anchor.duration}; hold until {anchor.hold_until}; completed {actual is not None}') if phrase.state: valid, reason = generator.solo.harmonic.anchors_valid(phrase.anchors, phrase.state.notes, phrase.plan.end, complete=phrase.plan.end == phrase.plan.passage_end, closure=phrase.plan.closure) lines.append(f' Ending contract fulfilled: {valid}; {reason}') for motif in phrase.state.motifs: if phrase.plan.start <= motif.source_start < phrase.plan.end: lines.append(f' Realized motif {motif.id}: parent {motif.parent}; {motif.relationship}; {motif.treatment}; pitches ' + ' '.join(n.pitch.label() for n in motif.notes) + f'; rhythm ' + ' '.join(f'{n.start}:{n.end}' for n in motif.notes)) for decision in phrase.decisions: lines.append(f' {decision.start}-{decision.end}: {decision.source}; {decision.treatment}') if not decision.notes: lines.append(' Silence') for note in decision.notes: choice = next(c for c in solo.choices if c.span.start <= note.start < c.span.end) root = generator.scales.selection.root_number(choice.span.chord) degrees = [str(d) for d, a in generator.harmony.degrees(choice.span.chord).items() if (root + generator.harmony.degree_semitones(d, a)) % 12 == note.pitch.number() % 12] function = 'chord degree ' + '/'.join(degrees) if degrees else 'in-collection connection' lines.append(f' {note.start}-{note.end}: {note.pitch.label()}; {note.role}; {generator.harmony.symbol(choice.span.chord)}, {function}; {choice.scale.label()}; string {note.position.string_number}, fret {note.position.fret}') lines.append('\nSounding harmonic spans (ties merged before classification):') for event in detail['harmonic_roles']: if float(fractions.Fraction(event['sounding_duration'])) >= 1 or event['protected']: lines.append(' ' + json.dumps(event, sort_keys=True)) return '\n'.join(lines) + '\n' def structure(solo): harmony = generator.solo.harmonic.Harmony(solo.choices) contracts = tuple(dict.fromkeys(t for p in solo.phrases if p.state for t in p.state.treatments)) checked = harmony.evaluate(solo.notes(), contracts, until=solo.end, complete=True) def rational(value): if isinstance(value, dict): return {key: rational(item) for key, item in value.items()} if isinstance(value, (tuple, list, set, frozenset)): return [rational(item) for item in value] if isinstance(value, fractions.Fraction): return str(value) return value phrases = [] for phrase in solo.phrases: notes = phrase.state.notes if phrase.state else solo.notes() complete = phrase.plan.end == (phrase.plan.passage_end or phrase.plan.end) valid, reason = generator.solo.harmonic.anchors_valid(phrase.anchors, notes, phrase.plan.end, complete=complete, closure=phrase.plan.closure) anchors = [] for anchor in phrase.anchors: event = generator.solo.harmonic.anchor_note(anchor, notes) anchors.append({**rational(dataclasses.asdict(anchor)), 'pitch': anchor.pitch.label(), 'completed': event is not None, 'actual_start': str(event.start) if event else None, 'actual_end': str(event.end) if event else None}) motifs = tuple(m for m in phrase.state.motifs if phrase.plan.start <= m.source_start < phrase.plan.end) if phrase.state else () entry = {'id': phrase.plan.id, 'passage': phrase.plan.passage_id, 'start': str(phrase.plan.start), 'end': str(phrase.plan.end), 'ending': phrase.plan.ending, 'closure': phrase.plan.closure, 'ending_completed': valid, 'ending_error': reason, 'anchors': anchors, 'motif_lineage': rational([dataclasses.asdict(m) for m in motifs])} if phrase.draft: entry.update({'draft': rational(dataclasses.asdict(phrase.draft)), 'draft_rests': rational(phrase.draft.rests()), 'edits': rational([dataclasses.asdict(e) for e in phrase.edits]), 'relationships': generator.solo.drafts.relationship_features(phrase.draft, tuple(n for d in phrase.decisions for n in d.notes)), 'home': phrase.state.home.id if phrase.state and phrase.state.home else None, 'current_development': phrase.state.development.id if phrase.state and phrase.state.development else None}) phrases.append(entry) return {'passages': [{'id': p.id, 'start': str(p.start), 'end': str(p.end), 'phrases': list(p.phrase_ids), 'seed': p.seed} for p in solo.passages], 'phrases': phrases, 'harmonic_roles': harmony.describe(solo.notes(), contracts), 'harmonic_exposure': checked.exposure, 'harmonic_valid': checked.valid, 'harmonic_error': checked.error, 'unresolved_treatments': rational([dataclasses.asdict(t) for t in checked.pending]), 'treatments': rational([dataclasses.asdict(t) for t in contracts])}