"""Constrained, deterministically seeded autoregressive musical sampling. The pooled model proposes joint note/rest, duration and pitch events. Complete expression actions include their real resolution. Guitar and chord/function validity precede sampling. No target access, hand-authored melody scorer or legacy fallback. A bounded restart handles an exhausted/entirely silent sample. """ from __future__ import annotations import collections import dataclasses import hashlib import random import math from fractions import Fraction as F from generator import model as music, vocabulary from generator.solo import expressions, groups, learned, types class SearchExhausted(ValueError):pass @dataclasses.dataclass(frozen=True) class State: events:tuple=() notes:tuple=() contracts:tuple=() history:tuple=() previous:int|None=None cost:float=0. def event_key(events):return tuple((a,b,-1 if p is None else p) for a,b,p in events) class Decoder: def __init__(self,analysis,harmony,model,role,start,end,*,expressions_enabled=True,attempts=4,seed=''): if type(attempts) is not int or not 1<=attempts<=16:raise ValueError('attempts must be between 1 and 16') self.analysis,self.harmony,self.model,self.role=analysis,harmony,model,role self.start,self.end=start,end self.attempts=attempts source=tuple((str(n.start),str(n.end),n.pitch.number()) for n in analysis.melody) self.seed=seed or hashlib.sha256(repr((source,role,learned.VERSION)).encode()).hexdigest() self.expressions_enabled=expressions_enabled self.low,self.high=harmony.profile.register limits=vocabulary.data('roles')['difficulty'] self.durations=tuple(d for d in model.durations if 0F(rule['max_beats']):continue # A held chord member can become a color only after a chord change. # Test the regions needing permission, not just the attack's harmony. if any(region.chord=='N.C.' or not expressions.eligible_color(rule,region,self.harmony) or expressions.degree_pitch(expressions.parsed_chord(region.chord),rule['degree'],rule['alter'],n.pitch.number()).number()!=n.pitch.number() for region in exposed):continue c=types.ExpressionContract(n.event_id+':color','color',rule['id'],(n,),n,span.chord) if self.harmony.evaluate((n,),complete=True,expressions=(c,)).valid:return (c,) for arriving in self.harmony.spans: if n.startself.end:continue add(((time,end,None),),()) for number in range(self.low,self.high+1): n=self.note(time,end,number);support=self.permission(n) if support is not None:add(((time,end,number),),(n,),support) if self.expressions_enabled: limits=vocabulary.data('roles')['difficulty'] for rule in vocabulary.data('lead_ins')['patterns']: difficulty=rule.get('difficulty') if difficulty and self.analysis.tempo>limits[difficulty+'_max_bpm']:continue # Connections require a concrete preceding note, checked at expansion. durations=tuple(map(F,rule['durations']));gaps=tuple(map(F,rule.get('internal_gaps',['0']*(len(durations)-1)))) target_time=time+sum(durations,F())+sum(gaps,F())+F(rule['gap']) span=self.harmony.at(target_time) if not span or span.chord=='N.C.':continue holds=(F(1,3),) if difficulty=='triplet' else (F(1,2),F(1),F(2)) for duration in holds: end=target_time+duration if end>self.end:continue for number in range(self.low,self.high+1): if number%12 not in self.harmony.members(target_time,end):continue target=self.note(target_time,end,number,'destination');cursor=time;notes=[];events=[] for i,(delta,d) in enumerate(zip(rule['semitones'],durations)): n=self.note(cursor,cursor+d,expressions.chromatic(target.pitch,delta),rule['kind']) notes.append(n);events.append((n.start,n.end,n.pitch.number()));cursor=n.end gap=gaps[i] if i12:return None if contracts and contracts[0].kind=='connection': if not state.notes:return None c=dataclasses.replace(contracts[0],preparation=state.notes[-1]);contracts=(c,) _,error=expressions.validate((state.notes[-1],*notes),contracts,self.harmony) if error:return None history=state.history;previous=state.previous;cost=state.cost for a,b,p in events: harmonic,root,phase,source,_=self.context(a) action=learned.token(b-a,p,previous,root) context=learned.contexts(harmonic,phase,source,history) cost+=self.model.cost(action,context) if p is not None: if previous is None:cost+=self.model.register_cost(p) previous=p history=(*history,action)[-4:] return State((*state.events,*events),(*state.notes,*notes),(*state.contracts,*contracts),history,previous,cost) def compose(self): for attempt in range(self.attempts): rng=random.Random(self.seed+':'+str(attempt));state=State();time=self.start trace=[] while time