"""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 if any(s.chord=='N.C.' or not expressions.eligible_color(rule,s,self.harmony) or expressions.degree_pitch(expressions.parsed_chord(s.chord),rule['degree'],rule['alter'],n.pitch.number()).number()!=n.pitch.number() for s in exposed):continue bases.append((rule,boundary)) self._colors[key]=tuple(bases) return self._colors[key] def color_contract(self,n,target,rule,boundary): span=self.harmony.at(boundary if boundary is not None else n.start) if not span:return None kind='anticipated_color' if boundary is not None else 'color' return types.ExpressionContract(n.event_id+':'+rule['id']+':'+str(boundary),kind,rule['id'],(n,),target,span.chord,boundary) def permission(self,n): key=(n.start,n.end,n.pitch.number()) if key not in self._permissions:self._permissions[key]=self._permission(n) return self._permissions[key] def _permission(self,n): """Find justified color/anticipation support, never authorize by label.""" # Source spans are contiguous. These are precisely the ordinary-event # branches of Harmony.evaluate, without rebuilding an empty contract map. if self.harmony.at(n.start) and n.end<=self.harmony.spans[-1].end: if n.pitch.number()%12 in self.harmony.members(n.start,n.end):return () if n.end-n.start<2 and n.pitch.number()%12 in self.harmony.permitted(n.start,n.end):return () if not self.expressions_enabled:return None span=self.harmony.at(n.start) if not span or span.chord=='N.C.':return None for rule,boundary in self.color_bases(n): if rule.get('resolve'):continue c=self.color_contract(n,n,rule,boundary) if self.harmony.evaluate((n,),complete=True,expressions=(c,),infer=False).valid:return (c,) for arriving in self.harmony.spans: if n.start=1:windows|={(n.end,n.end+F(1,2)),(target.start-F(1,2),target.start)} for a,b in sorted(windows): for number in range(max(self.low,n.pitch.number()-3),min(self.high,n.pitch.number()+3)+1): mid=self.note(a,b,number) if number in (n.pitch.number(),target.pitch.number()) or not self.harmony.ordinary(mid):continue events=[(n.start,n.end,n.pitch.number())] if a>n.end:events.append((n.end,a,None)) events.append((a,b,number)) if bself.end or number%12 not in self.harmony.members(arrival,arrival+hold):continue target=self.note(arrival,arrival+hold,number,'destination') c=self.color_contract(n,target,rule,boundary) if kind=='color' else types.ExpressionContract(n.event_id+':delayed','delayed_approach',rule['id'],(n,),target,self.harmony.at(arrival).chord) # Color and delayed-approach contracts permit this intervening # window. Validate the invariant endpoints once; tails() checks # every inserted note's ordinary full-duration permission itself. if not self.harmony.evaluate((n,target),complete=True,expressions=(c,),infer=False).valid:continue for events,ns in self.tails(n,target): if not groups.valid(tuple((a,b) for a,b,_ in events)):continue yield events,ns,(c,) def actions(self,time): if time in self._actions:return self._actions[time] result=[] def add(events,notes,contracts=()):result.append((events,notes,contracts)) for duration in self.durations: end=time+duration if end>self.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 and not self.harmony.ordinary(n): for events,ns,contracts in self.resolutions(n):add(events,ns,contracts) rule=vocabulary.data('lead_ins')['passing'] if duration<=F(rule['max_note']): for step in rule['steps']: p=number+step if not self.low<=p<=self.high:continue for hold in (F(1,2),F(1),F(2)): if end+hold>self.end:continue target=self.note(end,end+hold,p);extra=self.permission(target) if extra is None or any(c.kind not in ('color','anticipated_color') for c in extra):continue c=types.ExpressionContract(n.event_id+':passing','passing',rule['id'],(n,),target,self.harmony.at(end).chord) add(((time,end,number),(end,end+hold,p)),(n,target),(c,*extra)) 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 any(c.kind in ('connection','passing') and c.preparation is None for c in contracts): if not state.notes:return None contracts=tuple(dataclasses.replace(c,preparation=state.notes[-1]) if c.kind in ('connection','passing') and c.preparation is None else c for c in contracts) if self.harmony.ordinary(state.notes[-1]): _,error=expressions.validate((state.notes[-1],*notes),contracts,self.harmony) else: _,error=expressions.validate((*state.notes,*notes),(*state.contracts,*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