+132
-127
@@ -1,163 +1,168 @@
|
||||
"""Contextual musical validity needs notes and chords, not author annotations."""
|
||||
import dataclasses
|
||||
import json
|
||||
from fractions import Fraction as F
|
||||
from pathlib import Path
|
||||
import fractions
|
||||
import pathlib
|
||||
import unittest
|
||||
|
||||
from generator import train,training_io,vocabulary,harmonic_time,harmony as chords
|
||||
from generator.solo import harmonic,types,decoder,learned,analysis,expressions
|
||||
from tests.solo_fixtures import choices,note,song
|
||||
|
||||
ROOT=Path(__file__).resolve().parents[1]
|
||||
import generator.train
|
||||
import generator.training_io
|
||||
import generator.vocabulary
|
||||
import generator.harmonic_time
|
||||
import generator.harmony
|
||||
import generator.solo.harmonic
|
||||
import generator.solo.types
|
||||
import generator.solo.decoder
|
||||
import generator.solo.learned
|
||||
import generator.solo.analysis
|
||||
import generator.solo.expressions
|
||||
import tests.solo_fixtures
|
||||
import generator.musicxml
|
||||
import generator.parts
|
||||
import generator.solo.authored
|
||||
|
||||
ROOT = pathlib.Path(__file__).resolve().parents[1]
|
||||
|
||||
class ContextualHarmony(unittest.TestCase):
|
||||
def test_approved_corpus_passes_without_annotations_or_authored_privileges(self):
|
||||
examples,_=train.load_examples(ROOT/'data/training/manifest.json')
|
||||
manifest=json.loads((ROOT/'data/training/manifest.json').read_text())
|
||||
from generator import musicxml,parts
|
||||
from generator.solo.authored import sounding
|
||||
examples, _ = generator.train.load_examples(ROOT / 'data/training/manifest.json')
|
||||
manifest = json.loads((ROOT / 'data/training/manifest.json').read_text())
|
||||
for entry in manifest['examples']:
|
||||
raw=training_io.read(ROOT/'data/training'/entry['solo'],'',pitch_convention=entry['pitch_convention'])
|
||||
self.assertEqual(raw['annotations'],[])
|
||||
doc=parts.prepare_document(musicxml.load_musicxml(ROOT/'data/training'/entry['source']))
|
||||
h=harmonic.Harmony(harmonic_time.solo_source(doc['staves'][0]))
|
||||
ns=tuple(dataclasses.replace(n,role='ordinary',sources=(),event_id='') for n in sounding(raw))
|
||||
checked=h.evaluate(ns,complete=True)
|
||||
self.assertTrue(checked.valid,(entry['id'],checked.error))
|
||||
raw = generator.training_io.read(ROOT / 'data/training' / entry['solo'], '', pitch_convention=entry['pitch_convention'])
|
||||
self.assertEqual(raw['annotations'], [])
|
||||
doc = generator.parts.prepare_document(generator.musicxml.load_musicxml(ROOT / 'data/training' / entry['source']))
|
||||
h = generator.solo.harmonic.Harmony(generator.harmonic_time.solo_source(doc['staves'][0]))
|
||||
ns = tuple(dataclasses.replace(n, role='ordinary', sources=(), event_id='') for n in generator.solo.authored.sounding(raw))
|
||||
checked = h.evaluate(ns, complete=True)
|
||||
self.assertTrue(checked.valid, (entry['id'], checked.error))
|
||||
self.assertTrue(checked.contracts)
|
||||
self.assertEqual(len(examples),4)
|
||||
|
||||
self.assertEqual(len(examples), 4)
|
||||
def test_passing_to_nonchord_scale_note(self):
|
||||
h=harmonic.Harmony(choices(('G6',)))
|
||||
notes=(note(0,F(1,2),'g'),note(F(1,2),1,'g',alter=1),note(1,2,'a'))
|
||||
result=h.evaluate(notes)
|
||||
self.assertTrue(result.valid,result.error)
|
||||
self.assertEqual(result.contracts[0].kind,'passing')
|
||||
self.assertFalse(h.evaluate(notes,infer=False).valid)
|
||||
h = generator.solo.harmonic.Harmony(tests.solo_fixtures.choices(('G6',)))
|
||||
notes = (tests.solo_fixtures.note(0, fractions.Fraction(1, 2), 'g'), tests.solo_fixtures.note(fractions.Fraction(1, 2), 1, 'g', alter=1), tests.solo_fixtures.note(1, 2, 'a'))
|
||||
result = h.evaluate(notes)
|
||||
self.assertTrue(result.valid, result.error)
|
||||
self.assertEqual(result.contracts[0].kind, 'passing')
|
||||
self.assertFalse(h.evaluate(notes, infer=False).valid)
|
||||
self.assertFalse(h.evaluate(notes[:-1]).valid)
|
||||
leap=(*notes[:2],dataclasses.replace(notes[2],pitch=types.Pitch('b',F(),4)))
|
||||
leap = (*notes[:2], dataclasses.replace(notes[2], pitch=generator.solo.types.Pitch('b', fractions.Fraction(), 4)))
|
||||
self.assertFalse(h.evaluate(leap).valid)
|
||||
long=(notes[0],dataclasses.replace(notes[1],end=F(3,2)),dataclasses.replace(notes[2],start=F(3,2)))
|
||||
long = (notes[0], dataclasses.replace(notes[1], end=fractions.Fraction(3, 2)), dataclasses.replace(notes[2], start=fractions.Fraction(3, 2)))
|
||||
self.assertFalse(h.evaluate(long).valid)
|
||||
|
||||
def test_passing_contract_requires_its_actual_preparation(self):
|
||||
h=harmonic.Harmony(choices(('G6',)))
|
||||
a=note(0,F(1,2),'g',event_id='a');b=note(F(1,2),1,'g',alter=1,event_id='b');c=note(1,2,'a',event_id='c')
|
||||
contract=types.ExpressionContract('p','passing','chromatic_passing',(b,),c,'GM6',preparation=a)
|
||||
self.assertTrue(h.evaluate((a,b,c),expressions=(contract,)).valid)
|
||||
changed=dataclasses.replace(a,end=F(1,4))
|
||||
wrong=dataclasses.replace(contract,preparation=changed)
|
||||
self.assertFalse(h.evaluate((changed,b,c),expressions=(wrong,)).valid)
|
||||
high=dataclasses.replace(c,pitch=dataclasses.replace(c.pitch,octave=5))
|
||||
self.assertFalse(h.evaluate((a,b,high),expressions=(dataclasses.replace(contract,target=high),)).valid)
|
||||
|
||||
h = generator.solo.harmonic.Harmony(tests.solo_fixtures.choices(('G6',)))
|
||||
a = tests.solo_fixtures.note(0, fractions.Fraction(1, 2), 'g', event_id='a')
|
||||
b = tests.solo_fixtures.note(fractions.Fraction(1, 2), 1, 'g', alter=1, event_id='b')
|
||||
c = tests.solo_fixtures.note(1, 2, 'a', event_id='c')
|
||||
contract = generator.solo.types.ExpressionContract('p', 'passing', 'chromatic_passing', (b,), c, 'GM6', preparation=a)
|
||||
self.assertTrue(h.evaluate((a, b, c), expressions=(contract,)).valid)
|
||||
changed = dataclasses.replace(a, end=fractions.Fraction(1, 4))
|
||||
wrong = dataclasses.replace(contract, preparation=changed)
|
||||
self.assertFalse(h.evaluate((changed, b, c), expressions=(wrong,)).valid)
|
||||
high = dataclasses.replace(c, pitch=dataclasses.replace(c.pitch, octave=5))
|
||||
self.assertFalse(h.evaluate((a, b, high), expressions=(dataclasses.replace(contract, target=high),)).valid)
|
||||
def test_color_resolution_can_have_intervening_valid_motion(self):
|
||||
h=harmonic.Harmony(choices(('C7',)))
|
||||
notes=(note(0,F(3,2),'f',alter=1),note(F(3,2),2,'a'),note(2,4,'g'))
|
||||
checked=h.evaluate(notes)
|
||||
self.assertTrue(checked.valid,checked.error)
|
||||
self.assertEqual(checked.contracts[0].rule,'lydian_color')
|
||||
h = generator.solo.harmonic.Harmony(tests.solo_fixtures.choices(('C7',)))
|
||||
notes = (tests.solo_fixtures.note(0, fractions.Fraction(3, 2), 'f', alter=1), tests.solo_fixtures.note(fractions.Fraction(3, 2), 2, 'a'), tests.solo_fixtures.note(2, 4, 'g'))
|
||||
checked = h.evaluate(notes)
|
||||
self.assertTrue(checked.valid, checked.error)
|
||||
self.assertEqual(checked.contracts[0].rule, 'lydian_color')
|
||||
self.assertFalse(h.evaluate(notes[:2]).valid)
|
||||
bad_mid=dataclasses.replace(notes[1],pitch=types.Pitch('g',F(1),5))
|
||||
self.assertFalse(h.evaluate((notes[0],bad_mid,notes[2])).valid)
|
||||
|
||||
bad_mid = dataclasses.replace(notes[1], pitch=generator.solo.types.Pitch('g', fractions.Fraction(1), 5))
|
||||
self.assertFalse(h.evaluate((notes[0], bad_mid, notes[2])).valid)
|
||||
def test_anticipated_stable_and_resolving_colors(self):
|
||||
h=harmonic.Harmony(choices(('Bbdim7','Am7')))
|
||||
n=note(F(7,2),6,'d')
|
||||
result=h.evaluate((n,))
|
||||
self.assertTrue(result.valid,result.error)
|
||||
self.assertEqual(result.contracts[0].kind,'anticipated_color')
|
||||
self.assertFalse(h.evaluate((dataclasses.replace(n,start=F(2)),)).valid)
|
||||
h=harmonic.Harmony(choices(('Gm7','C7')))
|
||||
color=note(F(7,2),F(11,2),'f',alter=1)
|
||||
target=note(F(11,2),7,'g')
|
||||
self.assertTrue(h.evaluate((color,target)).valid)
|
||||
h = generator.solo.harmonic.Harmony(tests.solo_fixtures.choices(('Bbdim7', 'Am7')))
|
||||
n = tests.solo_fixtures.note(fractions.Fraction(7, 2), 6, 'd')
|
||||
result = h.evaluate((n,))
|
||||
self.assertTrue(result.valid, result.error)
|
||||
self.assertEqual(result.contracts[0].kind, 'anticipated_color')
|
||||
self.assertFalse(h.evaluate((dataclasses.replace(n, start=fractions.Fraction(2)),)).valid)
|
||||
h = generator.solo.harmonic.Harmony(tests.solo_fixtures.choices(('Gm7', 'C7')))
|
||||
color = tests.solo_fixtures.note(fractions.Fraction(7, 2), fractions.Fraction(11, 2), 'f', alter=1)
|
||||
target = tests.solo_fixtures.note(fractions.Fraction(11, 2), 7, 'g')
|
||||
self.assertTrue(h.evaluate((color, target)).valid)
|
||||
self.assertFalse(h.evaluate((color,)).valid)
|
||||
flat=dataclasses.replace(color,pitch=types.Pitch('g',F(-1),4))
|
||||
self.assertTrue(h.evaluate((flat,target)).valid)
|
||||
|
||||
flat = dataclasses.replace(color, pitch=generator.solo.types.Pitch('g', fractions.Fraction(-1), 4))
|
||||
self.assertTrue(h.evaluate((flat, target)).valid)
|
||||
def test_repeated_harmony_cannot_reset_color_duration(self):
|
||||
h=harmonic.Harmony(choices(('C7','C7')))
|
||||
color=note(3,6,'f',alter=1);target=note(6,7,'g')
|
||||
self.assertFalse(h.evaluate((color,target)).valid)
|
||||
|
||||
h = generator.solo.harmonic.Harmony(tests.solo_fixtures.choices(('C7', 'C7')))
|
||||
color = tests.solo_fixtures.note(3, 6, 'f', alter=1)
|
||||
target = tests.solo_fixtures.note(6, 7, 'g')
|
||||
self.assertFalse(h.evaluate((color, target)).valid)
|
||||
def test_held_color_is_independent_of_notation_partition(self):
|
||||
h=harmonic.Harmony(choices(('Bbdim7','Am7')))
|
||||
whole=note(F(7,2),6,'d')
|
||||
split=(dataclasses.replace(whole,end=F(4)),dataclasses.replace(whole,start=F(4),continuation=True))
|
||||
self.assertEqual(h.evaluate((whole,)),h.evaluate(split))
|
||||
self.assertEqual(h.describe((whole,)),h.describe(split))
|
||||
|
||||
h = generator.solo.harmonic.Harmony(tests.solo_fixtures.choices(('Bbdim7', 'Am7')))
|
||||
whole = tests.solo_fixtures.note(fractions.Fraction(7, 2), 6, 'd')
|
||||
split = (dataclasses.replace(whole, end=fractions.Fraction(4)), dataclasses.replace(whole, start=fractions.Fraction(4), continuation=True))
|
||||
self.assertEqual(h.evaluate((whole,)), h.evaluate(split))
|
||||
self.assertEqual(h.describe((whole,)), h.describe(split))
|
||||
def test_unsupported_notes_are_not_authorized_by_labels(self):
|
||||
h=harmonic.Harmony(choices(('C7',)))
|
||||
for role in ('ordinary','authored','quote','color','Lead in'):
|
||||
self.assertFalse(h.evaluate((note(0,3,'c',alter=1,role=role),)).valid)
|
||||
missing=harmonic.Harmony(())
|
||||
self.assertFalse(missing.evaluate((note(0,1,'c'),)).valid)
|
||||
|
||||
h = generator.solo.harmonic.Harmony(tests.solo_fixtures.choices(('C7',)))
|
||||
for role in ('ordinary', 'authored', 'quote', 'color', 'Lead in'):
|
||||
self.assertFalse(h.evaluate((tests.solo_fixtures.note(0, 3, 'c', alter=1, role=role),)).valid)
|
||||
missing = generator.solo.harmonic.Harmony(())
|
||||
self.assertFalse(missing.evaluate((tests.solo_fixtures.note(0, 1, 'c'),)).valid)
|
||||
def test_fully_expired_partial_treatments_remain_invalid(self):
|
||||
h=harmonic.Harmony(choices(('C7',)))
|
||||
notes=(note(0,F(1,2),'d',alter=1,event_id='a'),note(F(1,2),2,'e',event_id='b'))
|
||||
contract=types.ExpressionContract('a','approach','below',(notes[0],),notes[1],'C7')
|
||||
partial=h.evaluate(notes[:1],until=F(1,2),expressions=(contract,))
|
||||
h = generator.solo.harmonic.Harmony(tests.solo_fixtures.choices(('C7',)))
|
||||
notes = (tests.solo_fixtures.note(0, fractions.Fraction(1, 2), 'd', alter=1, event_id='a'), tests.solo_fixtures.note(fractions.Fraction(1, 2), 2, 'e', event_id='b'))
|
||||
contract = generator.solo.types.ExpressionContract('a', 'approach', 'below', (notes[0],), notes[1], 'C7')
|
||||
partial = h.evaluate(notes[:1], until=fractions.Fraction(1, 2), expressions=(contract,))
|
||||
self.assertTrue(partial.valid)
|
||||
self.assertEqual(len(partial.pending),1)
|
||||
self.assertFalse(h.evaluate(notes[:1],until=F(2),expressions=(contract,)).valid)
|
||||
|
||||
self.assertEqual(len(partial.pending), 1)
|
||||
self.assertFalse(h.evaluate(notes[:1], until=fractions.Fraction(2), expressions=(contract,)).valid)
|
||||
def test_pickup_uses_source_form_context_without_overriding_written_harmony(self):
|
||||
doc=song(count=3,chords=('C','Dm','G7'));staff=doc['staves'][0]
|
||||
pickup=staff['measures'][0];pickup['implicit']=True;pickup['harmonies']=[]
|
||||
self.assertEqual(chords.symbol(harmonic_time.solo_source(staff)[0].chord),'G7')
|
||||
self.assertIsNone(harmonic_time.read(staff)[0].chord)
|
||||
pickup['harmonies']=[dict(chords.parse_symbol('F'),onset={'numerator':0,'denominator':1})]
|
||||
self.assertEqual(chords.symbol(harmonic_time.solo_source(staff)[0].chord),'FM')
|
||||
pickup['harmonies']=[{'kind':'none','onset':{'numerator':0,'denominator':1}}]
|
||||
self.assertIsNone(harmonic_time.solo_source(staff)[0].chord)
|
||||
pickup['harmonies']=[];pickup['implicit']=False
|
||||
self.assertIsNone(harmonic_time.solo_source(staff)[0].chord)
|
||||
|
||||
doc = tests.solo_fixtures.song(count=3, chords=('C', 'Dm', 'G7'))
|
||||
staff = doc['staves'][0]
|
||||
pickup = staff['measures'][0]
|
||||
pickup['implicit'] = True
|
||||
pickup['harmonies'] = []
|
||||
self.assertEqual(generator.harmony.symbol(generator.harmonic_time.solo_source(staff)[0].chord), 'G7')
|
||||
self.assertIsNone(generator.harmonic_time.read(staff)[0].chord)
|
||||
pickup['harmonies'] = [dict(generator.harmony.parse_symbol('F'), onset={'numerator': 0, 'denominator': 1})]
|
||||
self.assertEqual(generator.harmony.symbol(generator.harmonic_time.solo_source(staff)[0].chord), 'FM')
|
||||
pickup['harmonies'] = [{'kind': 'none', 'onset': {'numerator': 0, 'denominator': 1}}]
|
||||
self.assertIsNone(generator.harmonic_time.solo_source(staff)[0].chord)
|
||||
pickup['harmonies'] = []
|
||||
pickup['implicit'] = False
|
||||
self.assertIsNone(generator.harmonic_time.solo_source(staff)[0].chord)
|
||||
|
||||
class PredictionReachability(unittest.TestCase):
|
||||
def engine(self,chords):
|
||||
doc=song(count=2,chords=chords)
|
||||
return decoder.Decoder(analysis.analyze(doc),harmonic.Harmony(choices(chords)),
|
||||
learned.Model.load(ROOT/'data/models/solo.json'),'acoustic',F(),F(8))
|
||||
|
||||
def engine(self, chords):
|
||||
doc = tests.solo_fixtures.song(count=2, chords=chords)
|
||||
return generator.solo.decoder.Decoder(generator.solo.analysis.analyze(doc), generator.solo.harmonic.Harmony(tests.solo_fixtures.choices(chords)), generator.solo.learned.Model.load(ROOT / 'data/models/solo.json'), 'acoustic', fractions.Fraction(), fractions.Fraction(8))
|
||||
def test_joint_resolved_color_reaches_the_real_action_builder(self):
|
||||
d=self.engine(('C7','FM7'))
|
||||
n=d.note(F(),F(3,2),66)
|
||||
actions=list(d.resolutions(n))
|
||||
self.assertTrue(any([p for _,_,p in events]==[66,69,67] for events,_,_ in actions))
|
||||
for _,notes,contracts in actions:
|
||||
self.assertTrue(d.harmony.evaluate(notes,expressions=contracts,infer=False).valid)
|
||||
|
||||
d = self.engine(('C7', 'FM7'))
|
||||
n = d.note(fractions.Fraction(), fractions.Fraction(3, 2), 66)
|
||||
actions = list(d.resolutions(n))
|
||||
self.assertTrue(any([p for _, _, p in events] == [66, 69, 67] for events, _, _ in actions))
|
||||
for _, notes, contracts in actions:
|
||||
self.assertTrue(d.harmony.evaluate(notes, expressions=contracts, infer=False).valid)
|
||||
def test_anticipated_color_is_a_normal_single_event_option(self):
|
||||
d=self.engine(('Bbdim7','Am7'));n=d.note(F(7,2),F(6),62)
|
||||
support=d.permission(n)
|
||||
d = self.engine(('Bbdim7', 'Am7'))
|
||||
n = d.note(fractions.Fraction(7, 2), fractions.Fraction(6), 62)
|
||||
support = d.permission(n)
|
||||
self.assertTrue(support)
|
||||
self.assertEqual(support[0].kind,'anticipated_color')
|
||||
d.expressions_enabled=False;d._permissions.clear()
|
||||
self.assertEqual(support[0].kind, 'anticipated_color')
|
||||
d.expressions_enabled = False
|
||||
d._permissions.clear()
|
||||
self.assertIsNone(d.permission(n))
|
||||
|
||||
def test_delayed_action_retains_space_and_intervening_motion(self):
|
||||
d=self.engine(('C7','FM7'));n=d.note(F(),F(1,2),63)
|
||||
actions=list(d.resolutions(n))
|
||||
self.assertTrue(any(c[0].kind=='delayed_approach' and any(p is None for _,_,p in events) for events,_,c in actions))
|
||||
self.assertTrue(any(c[0].kind=='delayed_approach' and len(notes)==3 for _,notes,c in actions))
|
||||
|
||||
d = self.engine(('C7', 'FM7'))
|
||||
n = d.note(fractions.Fraction(), fractions.Fraction(1, 2), 63)
|
||||
actions = list(d.resolutions(n))
|
||||
self.assertTrue(any(c[0].kind == 'delayed_approach' and any(p is None for _, _, p in events) for events, _, c in actions))
|
||||
self.assertTrue(any(c[0].kind == 'delayed_approach' and len(notes) == 3 for _, notes, c in actions))
|
||||
def test_passing_action_uses_incoming_state(self):
|
||||
d=self.engine(('G6','G6'));first=d.note(F(),F(1,2),67)
|
||||
state=d.extend(decoder.State(),(((first.start,first.end,67),),(first,),()))
|
||||
actions=[a for a in d.actions(F(1,2)) if a[2] and a[2][0].kind=='passing'
|
||||
and a[0]==((F(1,2),F(1),68),(F(1),F(2),69))]
|
||||
d = self.engine(('G6', 'G6'))
|
||||
first = d.note(fractions.Fraction(), fractions.Fraction(1, 2), 67)
|
||||
state = d.extend(generator.solo.decoder.State(), (((first.start, first.end, 67),), (first,), ()))
|
||||
actions = [a for a in d.actions(fractions.Fraction(1, 2)) if a[2] and a[2][0].kind == 'passing'
|
||||
and a[0] == ((fractions.Fraction(1, 2), fractions.Fraction(1), 68), (fractions.Fraction(1), fractions.Fraction(2), 69))]
|
||||
self.assertTrue(actions)
|
||||
self.assertIsNotNone(d.extend(state,actions[0]))
|
||||
self.assertIsNone(d.extend(decoder.State(),actions[0]))
|
||||
wrong=d.note(F(),F(1,2),64)
|
||||
prior=d.extend(decoder.State(),(((wrong.start,wrong.end,64),),(wrong,),()))
|
||||
self.assertIsNone(d.extend(prior,actions[0]))
|
||||
self.assertIsNotNone(d.extend(state, actions[0]))
|
||||
self.assertIsNone(d.extend(generator.solo.decoder.State(), actions[0]))
|
||||
wrong = d.note(fractions.Fraction(), fractions.Fraction(1, 2), 64)
|
||||
prior = d.extend(generator.solo.decoder.State(), (((wrong.start, wrong.end, 64),), (wrong,), ()))
|
||||
self.assertIsNone(d.extend(prior, actions[0]))
|
||||
|
||||
|
||||
if __name__=='__main__':unittest.main()
|
||||
if __name__ == '__main__':
|
||||
unittest.main()
|
||||
|
||||
Reference in New Issue
Block a user