"""The sole production solo composer: frozen learned model plus music validity.""" import dataclasses import time from fractions import Fraction as F from pathlib import Path from generator import harmonic_time, vocabulary from generator.solo import types, profiles, harmonic, placement, learned, decoder, groups def compose(analysis,measures,part,end,*,learned_model=None,initial=None,expressions_enabled=True,start=F()): began=time.monotonic() spans=tuple(dataclasses.replace(s,start=s.start+start,end=s.end+start) for s in harmonic_time.read({'measures':measures},initial=initial)) profile=profiles.for_part(part,analysis);harmony=harmonic.Harmony(spans,profile) model=learned_model or learned.Model.load(Path(__file__).resolve().parents[2]/'data/models/solo.json') settings=vocabulary.role(part+'_solo') builder=decoder.Decoder(analysis,harmony,model,part,start,end,expressions_enabled=expressions_enabled, attempts=settings.get('attempts',4),seed=settings.get('choice_seed','')) selected=builder.compose();decoded=time.monotonic() placed=placement.place(selected.notes,analysis.tempo,profile=profile) if placed is None:raise decoder.SearchExhausted('learned solo has no complete legal guitar path') # Phrase labels describe heard rests/arrivals. They do not impose a new # endpoint or reset the learned history at barlines or arbitrary windows. notes=placed[1];ends=[n.end for i,n in enumerate(notes[:-1]) if notes[i+1].start-n.end>=1] boundaries=[start,*[t for t in ends if startsolo.end or n.end<=n.start or previous and previous.end>n.start:raise ValueError('overlapping/out-of-bounds sounding event') if n.position not in placement.positions(n.pitch.number(),solo.profile):raise ValueError('invalid guitar placement') previous=n checked=harmonic.Harmony(solo.harmony).evaluate(solo.notes(),until=solo.end,complete=True, expressions=tuple(c for p in solo.phrases for c in p.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 if cursor!=solo.end:raise ValueError('incomplete solo duration')