"""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('audition/controls')) 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 / 'paraphrase_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': 'Source is the original head, close paraphrase is the preserved previous plain theme, refined is the newly recomposed theme, and elaborated includes optional in-scale runs. The selected production branch supplies both new stages.'} body = ['Lead melody comparisons

Lead melody comparisons

Compare the original head, the previous close paraphrase, the new solo theme, and its optional in-scale runs. Source controls may contain original harmonic tensions that production explicitly adapts. 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('

' + html.escape(name.replace('_', ' ').title() + ' — ' + part) + '

') for window, offset, count in (('opening8', 0, 8), ('closing4', solo['count'] - 4, 4), ('full', 0, solo['count'])): first = solo['first'] + offset backing = tuple(audition.excerpt(old['parts'][p], first, count) for p in (other, 'vocal')) body.append('') for stage in ('initial', 'close_paraphrase', 'refined', 'elaborated'): foreground = audition.excerpt(song['parts'][part], first, count) if stage=='elaborated' else audition.excerpt(old['solos'][part]['lead_stages']['refined']['document'] if stage=='close_paraphrase' else 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'') 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('') body.append('
ExcerptSong sourcePrevious close paraphraseNew themeTheme with runs
' + window + 'Solo only · MusicXML
') body.append(f'

Previous complete production solo

') prefix = f'elaborated/{name}_{part}' (root / (prefix + '.txt')).write_text(solo['report']) (root / (prefix + '.json')).write_text(json.dumps(solo['structure'], indent=2) + '\n') body.append(f'

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('

Accepted reference controls

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'

{control["id"]}: Solo only · MusicXML

') (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_melody_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()