"""Recognize bounded harmonic relationships in actual sounded music. No filenames, annotations, learned examples or role exemptions enter here. Existing expression validators prove each candidate; deterministic set-cover search chooses a mutually consistent explanation, never edits the notes. """ from fractions import Fraction as F from generator import vocabulary from generator.solo import expressions, types def signature(c): return (c.kind,c.rule,tuple(n.event_id for n in c.events),c.target.event_id, str(c.boundary),c.preparation.event_id if c.preparation else '') def merge(*groups): result={} for group in groups: for c in group:result.setdefault(signature(c),c) return tuple(result.values()) def recognize(notes,harmony,provided=(),*,budget=4096): required={n.event_id for n in notes if not harmony.ordinary(n)} fixed={n.event_id for c in provided for n in c.events} if required<=fixed:return tuple(provided),'' candidates={};single={};by_id={n.event_id:n for n in notes} if len(by_id)!=len(notes):return (), 'Harmonic analysis requires unique sounding-event IDs' def contract(kind,rule,events,target,boundary=None,preparation=None): span=harmony.at(boundary if boundary is not None else target.start) if not span or span.chord=='N.C.':return None identifier=f'inferred:{kind}:{rule}:{events[0].event_id}:{target.event_id}:{boundary}' return types.ExpressionContract(identifier,kind,rule,tuple(events),target,span.chord,boundary,preparation) def add(c,extras=()): if c is None:return False bundle=merge(extras,(c,));covered=frozenset(n.event_id for a in bundle for n in a.events) if not (covered & required):return False combined=merge(provided,bundle) _,error=expressions.validate(notes,combined,harmony) if error:return False candidates[tuple(signature(a) for a in bundle)]=(bundle,covered) return True # First discover independently supported colors and harmonic anticipations. # These can also support a later approach's destination or passing endpoint. for i,n in enumerate(notes): if n.event_id not in required:continue future=[t for t in notes[i+1:] if n.end<=t.start<=n.end+F(vocabulary.data('harmony')['resolution_gap']) and 0size and rule['kind']=='connection' else None for boundary,extras in targets: add(contract(rule['kind'],rule['id'],ns,target,boundary,preparation),extras) # Delayed approaches may include real space or intervening valid music. # They still have an exact semitone target and a finite deadline. rule=vocabulary.data('lead_ins')['delayed_approach'] for n in reversed(notes[:i]): if target.start-n.start>F(rule['deadline']):break if n.event_id in required and target.pitch.number()-n.pitch.number() in rule['steps']: add(contract('delayed_approach',rule['id'],(n,),target)) if i>=2: before,n=notes[i-2:i] if n.event_id in required: for left in [(),*single.get(before.event_id,())]: for right in [(),*single.get(target.event_id,())]: add(contract('passing',vocabulary.data('lead_ins')['passing']['id'],(n,),target,preparation=before),merge(left,right)) for rule in vocabulary.data('lead_ins')['blues']: add(contract('blues',rule['id'],(n,),target,preparation=before)) options={identifier:[] for identifier in sorted(required-fixed,key=lambda identifier:(by_id[identifier].start,identifier))} for bundle,covered in candidates.values(): for identifier in covered & options.keys():options[identifier].append((bundle,covered)) for identifier,choices in options.items(): if not choices: n=by_id[identifier] return (),f'{identifier} at {n.start}: unsupported harmonic relationship for {n.pitch.label()}' choices.sort(key=lambda x:(sum((n.end-n.start for c in x[0] for n in c.events),F()), sum((max(F(),c.target.start-c.events[-1].end) for c in x[0]),F()),tuple(signature(c) for c in x[0]))) visits=0;failed=set() def solve(chosen,covered): nonlocal visits remaining=required-covered if not remaining:return chosen key=tuple(sorted(signature(c) for c in chosen)) if key in failed or visits>=budget:return None visits+=1 identifier=min(remaining,key=lambda x:(len(options[x]),by_id[x].start,x)) for bundle,added in options[identifier]: combined=merge(chosen,bundle) _,error=expressions.validate(notes,combined,harmony) if error:continue result=solve(combined,covered|added) if result is not None:return result failed.add(key) return None result=solve(tuple(provided),fixed) if result is None:return (), 'Harmonic treatment search budget exhausted' if visits>=budget else 'No mutually consistent harmonic treatment coverage' return result,''