"""Smoothed joint musical-event model. No examples or song lookup at inference. Counts are sufficient statistics of short, relative contexts, not stored scores. The hierarchical Dirichlet estimator uses eight parent pseudo-observations. All paths through the decoder score the same sounding event stream identically. """ from __future__ import annotations import collections import json import math import functools from fractions import Fraction as F from pathlib import Path from generator import model from generator.solo import expressions VERSION = 'pooled-events-1' PRIOR = 8 DURATIONS = ('1/4','1/3','1/2','2/3','3/4','1','3/2','2','5/2','3','4') def key(value): return json.dumps(value,separators=(',',':')) def chord_context(harmony,time): span=harmony.at(time) current=expressions.parsed_chord(span.chord) if span and span.chord!='N.C.' else None future=next((s for s in harmony.spans if s.start>time and s.chord!='N.C.' and (not span or s.chord!=span.chord)),None) following=expressions.parsed_chord(future.chord) if future else None def root(c):return int(model.NATURAL[c['root']['letter']]+model.alteration(c['root']['alter']))%12 if c else 0 return (expressions.quality(current) if current else 'unknown', expressions.quality(following) if following else 'end', (root(following)-root(current))%12 if current and following else -1),root(current),future def token(duration,pitch,previous,root): return ('rest',str(duration),0,0) if pitch is None else ('note',str(duration), pitch-previous if previous is not None else 'start',(pitch-root)%12) def source_feature(analysis,time,root=0): time+=analysis.body_offset before=[n for n in analysis.melody if n.start<=time][-2:] following=next((n for n in analysis.melody if n.start>time),None) direction=0 if len(before)<2 else (before[-1].pitch.number()>before[-2].pitch.number())-(before[-1].pitch.number()time) upcoming=0 if not before or not following else (following.pitch.number()>before[-1].pitch.number())-(following.pitch.number()1 else 0) def phase_at(analysis,time): absolute=time+analysis.body_offset start=next((a for a,b in zip(analysis.boundaries,analysis.boundaries[1:]) if a<=absolute100000:self._prob.clear() self._prob[cache]=probability return probability def cost(self,action,context):return -math.log(self.probability(action,context)) def register_cost(self,number): counts=self.value['registers'] return -math.log((counts.get(str(number//12),0)+PRIOR)/(sum(counts.values())+PRIOR*4)) def fit(examples,provenance): tables=collections.defaultdict(collections.Counter) rhythms=collections.Counter();steps=collections.Counter();degrees=collections.Counter() registers=collections.Counter();durations=set(DURATIONS) for example in examples: history=[];previous=None for start,end,pitch in example['events']: harmonic,root,_=chord_context(example['harmony'],start) action=token(end-start,pitch,previous,root) context=contexts(harmonic,phase_at(example['analysis'],start-example['analysis'].body_offset), source_feature(example['analysis'],start-example['analysis'].body_offset,root),tuple(history[-4:])) for c in context:tables[c][key(action)]+=1 durations.add(str(end-start));rhythms[action[0]+':'+action[1]]+=1 if pitch is not None: steps[str(action[2])]+=1;degrees[str(action[3])]+=1 registers[str(pitch//12)]+=1;previous=pitch history.append(action) return {'version':VERSION,'provenance':provenance,'prior':PRIOR, 'durations':sorted(durations,key=F),'rhythms':dict(rhythms),'steps':dict(steps),'degrees':dict(degrees), 'registers':dict(registers), 'contexts':{k:{'total':sum(v.values()),'counts':dict(sorted(v.items()))} for k,v in sorted(tables.items())}}