"""Export auditions from completed assets; this command never composes music.""" from __future__ import annotations import argparse import array import copy import dataclasses import html import json from pathlib import Path from fractions import Fraction as F from generator import model, musicxml_writer, playback, score from generator.solo import notation, placement, types, similarity from generator.solo.audition import excerpt, mix def region(document, part): measures=document['staves'][0]['measures'] first=next(i for i,m in enumerate(measures) if m.get('section_label')==part.capitalize()+' Solo') finish=next((i for i in range(first+1,len(measures)) if measures[i].get('section_label')),len(measures)) return first,finish-first def staged(document, details, stage, tempo): notes=[] for phrase in details['phrases']: for n in phrase['lead_stages'].get(stage,{}).get('notes',[]): p=n['pitch'] notes.append(types.Note(F(n['start']),F(n['end']),types.Pitch(p['letter'],F(p['alter']),p['octave']), tuple(n['sources']),n['role'],continuation=n['continuation'],event_id=n['event_id'])) notes=types.sounding_notes(tuple(notes)) placed=placement.place(notes,tempo) if notes and placed is None:raise ValueError(stage+': no legal audition placement') result=copy.deepcopy(document) notation.replace_region(result,0,notation.measures(result['staves'][0]['measures'],placed[1] if placed else (),stage)) model.validate_document(result) return result def overlap(lead, backing): a=playback.compile_timeline(lead)['events'];b=playback.compile_timeline(backing)['events'] total=covered=above=0. for n in a: start=model.fraction(n['onset']);end=start+model.fraction(n['duration']);pitch=model.fraction(n['pitch']) cuts={start,end} for other in b: s=model.fraction(other['onset']);e=s+model.fraction(other['duration']) if sstart:cuts.update((max(s,start),min(e,end))) points=sorted(cuts) for x,y in zip(points,points[1:]): simultaneous=[o for o in b if model.fraction(o['onset'])<=xpitch for o in simultaneous):above+=float(y-x) return {'sounding_beats':total,'backing_overlap_fraction':covered/max(.001,total),'backing_above_lead_fraction':above/max(.001,total), 'interpretation':'score overlap only; not a perceptual masking measurement; no feedback to composition'} def export(assets,baseline,output): output.mkdir(parents=True,exist_ok=True) manifest={'composition_inputs':False,'gain':.65,'backing_gain':.55,'playback':'unchanged neutral constant-level synthesis', 'listening_status':'not auditioned by the agent; score validation and rendered signal checks only','solos':{}} tone_cache={};clips=[];notes_report=[] for roles_path in sorted((assets/'analysis').glob('*.roles')): song=roles_path.stem;roles=json.loads(roles_path.read_text()) for part in ('acoustic','electric'): print('Audition: '+song+'/'+part,flush=True) other='electric' if part=='acoustic' else 'acoustic' assembled=score.load_json(assets/'songs'/f'{song}_{part}.json') backing=score.load_json(assets/'songs'/f'{song}_{other}.json') first,count=region(assembled,part) lead=excerpt(assembled,first,count);backing=excerpt(backing,first,count) detail=roles[part+'_solo']['details'] tempo=next((model.fraction(t['bpm'])*model.written_length(t['written']) for m in assembled['staves'][0]['measures'] for t in m.get('tempos',[]) if 'bpm' in t),F(120)) versions={'final':lead, 'plain':staged(lead,detail,'refined',tempo), 'strict':staged(lead,detail,'before_enclosure',tempo), 'source':staged(lead,detail,'initial',tempo)} baseline_backing=None old=baseline/'songs'/f'{song}_{part}.json' if baseline else None if old and old.is_file(): original=score.load_json(old);a,b=region(original,part) versions['baseline']=excerpt(original,a,b) baseline_backing=excerpt(score.load_json(baseline/'songs'/f'{song}_{other}.json'),a,b) key=song+'_'+part manifest['solos'][key]={'tempo':str(tempo),'source_form_measures':count,'details':detail, 'input_trace':roles[part+'_solo']['trace'],'ensemble':overlap(lead,backing)} notes_report.append(key+' โ€” solo-relative rational quarter-note time; sounding pitches\n') for phrase in detail['phrases']: notes_report.append(f"{phrase['id']}: {phrase['start']}โ€“{phrase['end']}; {phrase['ending']}; completed={phrase['ending_completed']}\n") for n in phrase['draft']['notes']: p=n['pitch'];tone=types.Pitch(p['letter'],F(p['alter']),p['octave']) pos=n['position'];notes_report.append(f" {n['start']}โ€“{n['end']} {tone.label()} {n['role']} string {pos[0]} fret {pos[1]}\n") back_audio=mix([backing],tempo,output,tone_cache) for name,document in versions.items(): label=key+'_'+name score.write_json(output/(label+'.json'),document) musicxml_writer.write_musicxml(output/(label+'.xml'),document) solo_audio=mix([document],tempo,output,tone_cache) combined=array.array('f',(v+.55*(back_audio[i] if i=1:raise ValueError('Audition gain would clip; choose a lower shared gain') path=label+'_'+context+'.wav' playback.write_pcm(output/path,(v*manifest['gain'] for v in values)) clips.append({'file':path,'song':song,'part':part,'version':name,'context':context,'peak':peak}) if context=='fixed_backing': # Exact slices retain the same backing and gain. Counts come from # the completed source form, not a presumed 32-bar chorus. measures=document['staves'][0]['measures'];durations=[model.fraction(m['duration']) for m in measures] for excerpt_name,start,end in (('opening',F(),sum(durations[:8],F())),('ending',sum(durations[:-4],F()),sum(durations,F()))): factor=F(60*playback.SAMPLE_RATE)/tempo selection=values[round(start*factor):round(end*factor)] clip=label+'_'+excerpt_name+'.wav' playback.write_pcm(output/clip,(v*manifest['gain'] for v in selection)) clips.append({'file':clip,'song':song,'part':part,'version':name,'context':excerpt_name,'peak':peak}) manifest['pair_comparisons']={} for song in sorted({c['song'] for c in clips}): def events(part): detail=manifest['solos'][song+'_'+part]['details'] return tuple((float(F(n['start'])),float(F(n['end'])),types.Pitch(n['pitch']['letter'],F(n['pitch']['alter']),n['pitch']['octave']).number()) for p in detail['phrases'] for n in p['draft']['notes']) manifest['pair_comparisons'][song]=similarity.compare_events(events('electric'),events('acoustic')) manifest['clips']=clips (output/'report.json').write_text(json.dumps(manifest,indent=2)) (output/'notes.txt').write_text(''.join(notes_report)) rows=['Independent generation auditions', '

Independent generation auditions

Baseline and new music, same neutral rendering and fixed comparison backing. All files share gain 0.65; backing gain 0.55. Final arrangements use the new independent canonical backing. No subjective listening verdict has been made.

Full roles, contracts and diagnostics ยท Sounding note report

'] for key,detail in manifest['solos'].items(): rows.append('

'+html.escape(key)+'

') for c in clips: if c['song']+'_'+c['part']!=key:continue label=c['version']+' / '+c['context'] rows.append('') rows.append('
'+html.escape(label)+'WAV
') (output/'index.html').write_text('\n'.join(rows)) def main(): p=argparse.ArgumentParser(description=__doc__) p.add_argument('--assets',type=Path,required=True);p.add_argument('--baseline',type=Path);p.add_argument('--output',type=Path,required=True) args=p.parse_args();export(args.assets,args.baseline,args.output) if __name__=='__main__':main()