"""The single production entry point for independent phrase composition.""" from fractions import Fraction as F import time import dataclasses from generator import harmony as chords, vocabulary from generator.scales import timeline, selection from generator.solo import types, profiles, harmonic, construction, placement, policy def compose(analysis,measures,part,end,*,scales,sequences,rhythms,initial=None,expressions_enabled=True,start=F()): began=time.monotonic() choices=selection.select(timeline.read({'measures':measures},initial=initial),timeline.Solo(F(),end-start,part.capitalize()+' Solo'),scales) choices=tuple(dataclasses.replace(c,span=dataclasses.replace(c.span,start=c.span.start+start,end=c.span.end+start)) for c in choices) for c in choices: if c.span.chord and c.scale is None:raise ValueError(f'{part} solo, source measure {c.span.source_measure}, beat {c.span.beat}, {chords.symbol(c.span.chord)}: No exact match within the permitted catalog') profile=profiles.for_part(part,analysis);harmony=harmonic.Harmony(choices,profile) source=tuple((n.start,n.end,n.pitch) for n in analysis.melody) seed=vocabulary.role().get('choice_seed') or policy.stable_rank(part,(source,tuple((c.span.start,c.span.end,c.span.chord) for c in choices),vocabulary.role(part+'_solo'),policy.DEFAULT.composition_version)) builder=construction.Builder(analysis,measures,harmony,sequences,rhythms,end,seed,expressions_enabled,start=start) phrases=builder.compose() # Resolve hand-position transitions over the selected whole melody. This # cannot reselect pitches, rhythm or another independently generated part. raw=tuple(n for p in phrases for n in p.notes) placed=placement.place(raw,analysis.tempo,profile=profile) if raw else (0,()) if placed is None:raise construction.SearchExhausted('selected melody has no complete legal guitar path') by_id={n.event_id:n for n in placed[1]} phrases=tuple(dataclasses.replace(p,notes=tuple(by_id[n.event_id] for n in p.notes)) for p in phrases) result=types.Solo(part,choices,phrases,analysis.tempo,end,{'construction':'source_melodic_phrases','version':policy.DEFAULT.composition_version, 'construction_seconds':time.monotonic()-began,**builder.timings,'operation_coverage':{k:dict(v) for k,v in builder.coverage.items()}, 'whole_stream_diagnostic':dataclasses.asdict(builder.winner_evaluation), 'frontiers':builder.frontiers,'completed_candidates':[{'rank':i+1,'evaluation':{'selection_total': str(s.total / (end-start)), 'phrases': [dataclasses.asdict(p.evaluation) for p in s.phrases]},'events':[(str(n.start),str(n.end),n.pitch.number()) for n in s.notes]} for i,s in enumerate(builder.completed)],'rejections':dict(builder.failures),'decisions':builder.decisions,'expressions_enabled':expressions_enabled},profile,start) validate(result) return result def validate(solo): previous=None for note in solo.notes(): if note.startsolo.end or note.end<=note.start or previous and previous.end>note.start:raise ValueError('overlapping/out-of-bounds sounding event') if note.position not in placement.positions(note.pitch.number(),solo.profile):raise ValueError('invalid guitar placement') previous=note expressions=tuple(c for p in solo.phrases for c in p.expressions) checked=harmonic.Harmony(solo.choices).evaluate(solo.notes(),until=solo.end,complete=True,expressions=expressions) if not checked.valid:raise ValueError(checked.error) cursor=solo.start for p in solo.phrases: if p.start!=cursor or p.end<=p.start:raise ValueError('phrase coverage gap or overlap') cursor=p.end valid,error=harmonic.anchors_valid((p.destination,) if p.destination else (),p.notes,p.end,complete=True,closure='closed' if p.ending in ('complete','handoff') else 'open') if not valid:raise ValueError(error) if cursor!=solo.end:raise ValueError('incomplete solo duration')