Acoustic Comping
This commit is contained in:
@@ -2,7 +2,9 @@
|
||||
import copy
|
||||
import fractions
|
||||
import itertools
|
||||
import json
|
||||
import pathlib
|
||||
import shutil
|
||||
import tempfile
|
||||
import unittest
|
||||
|
||||
@@ -16,6 +18,7 @@ import generator.production
|
||||
import generator.score
|
||||
import generator.vocabulary
|
||||
import tests.solo_fixtures
|
||||
import tests.test_acoustic_voicing
|
||||
|
||||
def options(symbol, part):
|
||||
return generator.comping.voicings(generator.comping.semantic_key(generator.harmony.parse_symbol(symbol)), part)
|
||||
@@ -27,6 +30,86 @@ def path_cost(path, costs, part, left=(), right=(), close=False):
|
||||
return tuple(total)
|
||||
|
||||
class CompingVoicings(unittest.TestCase):
|
||||
def test_lower_progression_can_outweigh_extra_boundary_travel(self):
|
||||
symbols = ('Em7b5', 'A7b9', 'Dm7', 'G7', 'Cm7', 'F7')
|
||||
low = tuple(tests.test_acoustic_voicing.voicing(f) for f in ((7, 8, 7, 8), (7, 8, 6, 8), (5, 7, 5, 6), (5, 5, 4, 6), (3, 5, 3, 4), (3, 3, 2, 4)))
|
||||
high = tuple(tests.test_acoustic_voicing.voicing(f, 6) for f in ((12, 12, 12, 11), (12, 11, 12, 11), (10, 10, 10, 10), (10, 9, 10, 8), (8, 8, 8, 8), (8, 7, 8, 6)))
|
||||
left, right = tests.test_acoustic_voicing.voicing((8, 10, 9, 10)), low[0]
|
||||
layers = tuple(zip(low, high))
|
||||
costs = []
|
||||
for symbol, layer in zip(symbols, layers):
|
||||
self.assertTrue(all(v in options(symbol, 'acoustic') for v in layer))
|
||||
key = generator.comping.semantic_key(generator.harmony.parse_symbol(symbol))
|
||||
costs.append({v: generator.comping.local_cost('acoustic', key, v, 53) for v in layer})
|
||||
def travel(path):
|
||||
return sum(generator.comping.movement('acoustic', a, b) for a, b in zip((left, *path), (*path, right)))
|
||||
self.assertEqual((travel(low), travel(high)), (43, 35))
|
||||
self.assertLess(path_cost(low, costs, 'acoustic', left, right), path_cost(high, costs, 'acoustic', left, right))
|
||||
path, cost = generator.comping.select_grips(layers, costs, 'acoustic', 'lower-voicings', left=left, right=right)
|
||||
self.assertEqual(path, low)
|
||||
self.assertEqual(cost, min(path_cost(p, costs, 'acoustic', left, right) for p in itertools.product(*layers)))
|
||||
def test_large_round_trip_can_retain_a_high_position(self):
|
||||
low = tests.test_acoustic_voicing.voicing((7, 8, 7, 8))
|
||||
high = tests.test_acoustic_voicing.voicing((12, 12, 12, 11), 6)
|
||||
key = generator.comping.semantic_key(generator.harmony.parse_symbol('Em7b5'))
|
||||
costs = {v: generator.comping.local_cost('acoustic', key, v, 53) for v in (low, high)}
|
||||
self.assertLess(costs[low], costs[high])
|
||||
path, _ = generator.comping.select_grips(((low, high),), (costs,), 'acoustic', 'travel', left=high, right=high)
|
||||
self.assertEqual(path, (high,))
|
||||
def test_acoustic_preferences_are_exact_and_weights_are_operational(self):
|
||||
high = tests.test_acoustic_voicing.voicing((12, 12, 12, 11), 6)
|
||||
low = tests.test_acoustic_voicing.voicing((7, 8, 7, 8))
|
||||
resources = generator.vocabulary.load()
|
||||
key = generator.comping.semantic_key(generator.harmony.parse_symbol('Em7b5'))
|
||||
self.assertEqual(generator.comping.acoustic_preferences(high, 53), (fractions.Fraction(39, 4), fractions.Fraction(11, 4)))
|
||||
self.assertEqual(generator.comping.acoustic_preferences(low, 53)[1], 0)
|
||||
baseline = generator.comping.local_cost('acoustic', key, high, 53)
|
||||
for field, expected in (('register_weight', fractions.Fraction(39, 4)), ('high_position_weight', fractions.Fraction(11, 4)), ('comfortable_max_fret', -1)):
|
||||
policy = dict(resources['roles']['roles']['acoustic_accompaniment']['voicing'])
|
||||
policy[field] += 1
|
||||
with generator.vocabulary.using(resources, 'acoustic_accompaniment', {'voicing': policy}):
|
||||
cost = generator.comping.local_cost('acoustic', key, high, 53)
|
||||
self.assertEqual(cost.effort - baseline.effort, expected, field)
|
||||
self.assertEqual(cost.extended_grips, baseline.extended_grips)
|
||||
self.assertEqual(cost.family, baseline.family)
|
||||
with generator.vocabulary.using(resources, 'acoustic_accompaniment', {'voicing': {'register_weight': -1}}), self.assertRaises(ValueError):
|
||||
generator.comping.local_cost('acoustic', key, high, 53)
|
||||
def test_production_voicing_policy_changes_cache_not_rhythm_or_electric(self):
|
||||
source = generator.production.SourceAnalysis.read(tests.solo_fixtures.song(count=6, chords=('Em7b5', 'A7b9', 'Dm7', 'G7', 'Cm7', 'F7')))
|
||||
def rhythm(result):
|
||||
return [[(n['onset'], n['duration'], n['written'], n['kind'], len(n['pitches']), tuple((bool(p.get('tie_from')), bool(p.get('tie_to'))) for p in n['pitches'])) for n in generator.score.measure_notes(m)] for m in result['document']['staves'][0]['measures']]
|
||||
with tempfile.TemporaryDirectory() as directory:
|
||||
root = pathlib.Path(directory) / 'data'
|
||||
shutil.copytree(pathlib.Path('data/composition'), root / 'composition')
|
||||
role_path = root / 'composition/roles.json'
|
||||
data = json.loads(role_path.read_text())
|
||||
policy = data['roles']['acoustic_accompaniment'].pop('voicing')
|
||||
role_path.write_text(json.dumps(data))
|
||||
legacy = generator.production.generate_role(source, 'acoustic_accompaniment', root)
|
||||
electric = generator.production.generate_role(source, 'electric_accompaniment', root)
|
||||
data['roles']['acoustic_accompaniment']['voicing'] = policy
|
||||
role_path.write_text(json.dumps(data))
|
||||
current = generator.production.generate_role(source, 'acoustic_accompaniment', root)
|
||||
settings = {'voicing': {'register_weight': 0, 'high_position_weight': 0, 'comfortable_max_fret': 9}}
|
||||
old = generator.production.generate_role(source, 'acoustic_accompaniment', root, settings, pathlib.Path(directory) / 'cache')
|
||||
warm = generator.production.generate_role(source, 'acoustic_accompaniment', root, settings, pathlib.Path(directory) / 'cache')
|
||||
self.assertTrue(warm['cache_hit'])
|
||||
self.assertEqual(old['document'], warm['document'])
|
||||
self.assertNotEqual(current['document'], old['document'])
|
||||
self.assertNotEqual(current['trace']['cache_key'], old['trace']['cache_key'])
|
||||
self.assertNotEqual(legacy['trace']['cache_key'], current['trace']['cache_key'])
|
||||
self.assertEqual(legacy['document'], current['document'])
|
||||
self.assertEqual(rhythm(current), rhythm(old))
|
||||
self.assertEqual(current['details']['accompaniment_rhythm'], old['details']['accompaniment_rhythm'])
|
||||
self.assertEqual(electric['document'], generator.production.generate_role(source, 'electric_accompaniment', root)['document'])
|
||||
def test_report_balanced_cost_matches_selected_path(self):
|
||||
canonical = generator.comping.Canonical(tests.solo_fixtures.song(count=4, chords=('Em7b5', 'A7b9', 'Dm7', 'G7')), 'acoustic', 'report')
|
||||
rows = [r for r in canonical.report if r['tones']]
|
||||
for row in rows:
|
||||
cost = row['voicing_cost']
|
||||
self.assertEqual(cost['movement'], row['movement'])
|
||||
self.assertEqual(fractions.Fraction(cost['combined']), sum(fractions.Fraction(cost[k]) for k in ('movement', 'register', 'position')))
|
||||
self.assertEqual(canonical.path_cost.effort, sum(fractions.Fraction(r['voicing_cost']['combined']) for r in rows))
|
||||
def test_acoustic_geometry_complete_triads_and_component_fret_limits(self):
|
||||
for symbol in ('C', 'Cm', 'F#', 'Bbm', 'C7', 'Cm6', 'Em7b5', 'A7b9', 'C7/E', 'C/D'):
|
||||
with self.subTest(chord=symbol):
|
||||
@@ -76,7 +159,7 @@ class CompingVoicings(unittest.TestCase):
|
||||
def test_complete_path_beats_greedy_and_matches_exhaustive_search(self):
|
||||
for part in ('acoustic', 'electric'):
|
||||
with self.subTest(part=part):
|
||||
chords = ('C', 'C', 'Dm7')
|
||||
chords = ('C', 'Dm7', 'A7b9') if part == 'acoustic' else ('C', 'C', 'Dm7')
|
||||
layers = tuple(options(s, part) for s in chords)
|
||||
costs = tuple({v: generator.comping.local_cost(part, generator.comping.semantic_key(generator.harmony.parse_symbol(s)), v, 55) for v in layer} for s, layer in zip(chords, layers))
|
||||
path, cost = generator.comping.select_grips(layers, costs, part, 'test')
|
||||
|
||||
Reference in New Issue
Block a user