"""Audition selected lead stages and accepted controls through the normal output path.""" from __future__ import annotations import argparse import copy import dataclasses import fractions import html import json import pathlib import generator.model import generator.musicxml import generator.musicxml_writer import generator.parts import generator.playback import generator.scales.timeline import generator.scales.selection import generator.solo.audition as audition import generator.solo.drafts as drafts import generator.solo.harmonic as harmonic import generator.solo.notation as notation import generator.solo.placement as placement import generator.solo.realization as realization import generator.solo.references as references import generator.solo.types as types F = fractions.Fraction def document_for(template, notes, tempo, identifier): placed = placement.place(notes, tempo) if placed is None: raise ValueError(identifier + ': no valid guitar placement') document = copy.deepcopy(template) notation.replace_region(document, 0, notation.measures(document['staves'][0]['measures'], placed[1], identifier)) generator.model.validate_document(document) generator.fretboard.validate_document(document) timeline = generator.playback.compile_timeline(document) expected = [(n.start, n.end - n.start, n.pitch.number()) for n in types.sounding_notes(placed[1])] actual = [(generator.model.fraction(e['onset']), generator.model.fraction(e['duration']), generator.model.fraction(e['pitch'])) for e in timeline['events']] if actual != expected: raise ValueError(identifier + ': notation/playback changed the sounding melody') return document def controls(song, libraries): """Literal reference injection is restricted to this diagnostic, never production.""" _, scales, sequences = libraries reference, = references.load_library(pathlib.Path('data/lead_phrases')) solo = song['solos']['acoustic'] measures = song['parts']['acoustic']['staves'][0]['measures'][solo['first']:solo['first'] + solo['count']] choices = generator.scales.selection.select(generator.scales.timeline.read({'measures': measures}), generator.scales.timeline.Solo(F(), F(solo['length']), 'control'), scales) harmony = harmonic.Harmony(choices) template = audition.excerpt(song['parts']['acoustic'], solo['first'], 8) result = [] for label, source in (('plain', reference.notes), ('runs', reference.decorated)): checked = harmony.evaluate(source, until=reference.duration, complete=True) if not checked.valid: raise ValueError('accepted control: ' + checked.error) theme = types.Motif(reference.id, source, reference.duration, tuple(c for _, c in reference.harmony), 'accepted control') groups = tuple(types.Transformation(f'control_{i}', reference.id, 'control_0', 'statement' if i == 0 else 'answer', F(i * 16), F((i + 1) * 16), tuple(n.event_id for n in source if i * 16 <= n.start < (i + 1) * 16), tuple((n.start - i * 16, n.end - i * 16) for n in source if i * 16 <= n.start < (i + 1) * 16), preserves=(), source=reference.id) for i in range(2)) final = source[-1] terminal = types.Anchor('accepted_terminal', final.start, final.start, final.pitch, frozenset({final.pitch.number() % 12}), (55, 79), final.end - final.start, 'terminal', 'Dm', hold_until=final.end) home = realization.make_identity('control_0', reference.id, source[:len(groups[0].events)], F(), F(16), 'statement', harmony) draft = types.MelodicDraft('accepted_' + label, F(), reference.duration, source, groups, (final.event_id,), (), (terminal,), home, 'statement', (F(16),), references.identifying_features(source, reference.id), reference.id, theme) plan = types.PhrasePlan('accepted_' + label, F(), reference.duration, theme, 'statement', 'opening_reference', None, (55, 79), F(9, 8), F(1), passage_id='control', passage_start=F(), passage_end=reference.duration) incoming = types.ContinuationState('') phrase = realization.checked_phrase(plan, draft, source, (), incoming, harmony, F(solo['tempo'])) if phrase is None: raise ValueError('accepted control rejected by phrase validation') context = realization.SearchContext() refined = realization.refine(phrase, (phrase,), incoming, harmony, sequences, F(solo['tempo']), context) elaborated = realization.elaborate(refined, incoming, harmony, sequences, F(solo['tempo']), context) for stage, notes in (('injected', source), ('processed', realization.emitted_notes(elaborated))): identifier = 'accepted_' + label + '_' + stage checked = harmony.evaluate(notes, until=reference.duration, complete=True) if not checked.valid: raise ValueError(identifier + ': ' + checked.error) result.append({'id': identifier, 'document': document_for(template, notes, F(solo['tempo']), identifier), 'attacks': len(notes), 'harmonic_exposure': checked.exposure, 'changed_events': sum((a.start, a.end, a.pitch) != (b.start, b.end, b.pitch) for a, b in zip(source, notes)) + abs(len(source) - len(notes)), 'edits': [e.kind for e in elaborated.edits] if stage == 'processed' else [], 'acceptance': reference.acceptance, 'reference_version': reference.version}) return result def render(root): baseline = json.loads((root / 'baseline/snapshot.json').read_text()) current = json.loads((root / 'elaborated/snapshot.json').read_text()) cache, clips = {}, [] gain = 0.4 # Shared audition gain; every mixed clip is checked for clipping. manifest = {'implementation': current['implementation'], 'baseline_implementation': baseline['implementation'], 'policy': current['policy'], 'shared_gain': gain, 'clips': clips, 'stage_semantics': 'Initial and refined are intermediate versions of the selected production branches, with the same incoming context. They are not independently rerun compositions.'} body = ['
Compare the tune before and after refinement, then hear whether the runs preserve it. All fixed mixes use the original backing, identical tempo and one shared gain. Solo-only clips remove the backing. Initial/refined exports follow the selected production branches; they are not independent searches.
These metrics verify behavior, not musical quality. Listen for a recognizable tune, prepared destinations, and runs that belong to the tune. Diagnostics manifest.
'] def clip(prefix, foreground, tempo, backing=()): path = root / (prefix + '.wav') path.parent.mkdir(parents=True, exist_ok=True) samples = audition.mix((foreground, *backing), tempo, root, cache) peak = max(map(abs, samples), default=0) * gain if peak >= 1: raise ValueError(prefix + ': shared audition gain would clip') generator.playback.write_pcm(path, (value * gain for value in samples)) clips.append({'path': path.relative_to(root).as_posix(), 'tempo': str(tempo), 'peak': peak, 'attacks': len(generator.playback.compile_timeline(foreground)['events'])}) return path.relative_to(root).as_posix() for name, song in current['songs'].items(): old = baseline['songs'][name] if old['source_sha256'] != song['source_sha256']: raise ValueError(name + ': source changed during comparison') for part, solo in song['solos'].items(): other = 'electric' if part == 'acoustic' else 'acoustic' body.append('| Excerpt | Baseline | Initial tune | Refined tune | With runs |
|---|---|---|---|---|
| ' + window + ' | ') for stage in ('baseline', 'initial', 'refined', 'elaborated'): foreground = audition.excerpt((old if stage == 'baseline' else song)['parts'][part], first, count) if stage in ('baseline', 'elaborated') else audition.excerpt(solo['lead_stages'][stage]['document'], offset, count) prefix = f'{stage}/{name}_{part}_{window}' path = clip(prefix + '_fixed', foreground, F(solo['tempo']), backing) solo_path = clip(prefix + '_solo', foreground, F(solo['tempo'])) generator.musicxml_writer.write_musicxml(root / (prefix + '.musicxml'), foreground) body.append(f'Solo only · MusicXML | ') if stage == 'elaborated': clip(prefix + '_responsive', foreground, F(solo['tempo']), tuple(audition.excerpt(song['parts'][p], first, count) for p in (other, 'vocal'))) body.append('
Passage report · Anchors, motif lineage, stages and routes · Full solo with responsive backing
') print('Rendered', name, part, flush=True) song = current['songs']['autumn_leaves'] solo = song['solos']['acoustic'] backing = tuple(audition.excerpt(baseline['songs']['autumn_leaves']['parts'][p], solo['first'], 8) for p in ('electric', 'vocal')) body.append('The exact accepted notes enter the ordinary validation, guitar placement, notation and playback path. Processed controls additionally run refinement and elaboration. Literal injection is confined to these controls.
') results = controls(song, audition.resources()) manifest['controls'] = [{k: v for k, v in r.items() if k != 'document'} for r in results] for control in results: prefix = 'controls/' + control['id'] path = clip(prefix + '_fixed', control['document'], F(solo['tempo']), backing) single = clip(prefix + '_solo', control['document'], F(solo['tempo'])) generator.musicxml_writer.write_musicxml(root / (prefix + '.musicxml'), control['document']) body.append(f'') (root / 'manifest.json').write_text(json.dumps(manifest, indent=2) + '\n') (root / 'index.html').write_text(''.join(body) + '\n') def main(): parser = argparse.ArgumentParser(description=__doc__) parser.add_argument('--output', type=pathlib.Path, default=pathlib.Path('build/solo_lead_auditions')) action = parser.add_mutually_exclusive_group(required=True) action.add_argument('--capture', action='store_true') action.add_argument('--render', action='store_true') args = parser.parse_args() if args.capture: audition.capture(args.output, 'elaborated') else: render(args.output) if __name__ == '__main__': main()