Chord Families
This commit is contained in:
+27
-20
@@ -76,27 +76,34 @@ def prepare(document: dict) -> None:
|
||||
generator.accompaniment.normalize_beams(notes)
|
||||
|
||||
def combine(document: dict) -> None:
|
||||
"""Write prepared bass/upper attacks as one voice with shared stems and rests."""
|
||||
"""One rhythmic voice; split held pitches into ties rather than release them."""
|
||||
originals = {p['id']: p for m in document['staves'][0]['measures'] for v in m['voices'] for n in v['notes'] for p in n['pitches']}
|
||||
edges = [(p['id'], p['tie_to']) for p in originals.values() if p.get('tie_to')]
|
||||
fragments = {}
|
||||
for measure in measures(document):
|
||||
length = generator.model.fraction(measure['duration'])
|
||||
attacks = sorted((n for v in measure['voices'] for n in v['notes'] if n['pitches']), key=lambda n: generator.model.fraction(n['onset']))
|
||||
notes, cursor = [], fractions.Fraction()
|
||||
|
||||
def rest(end):
|
||||
nonlocal cursor
|
||||
for duration, written in generator.accompaniment.written_durations(end - cursor):
|
||||
notes.append(generator.model.make_event(f'{measure["id"]}_acoustic_{len(notes) + 1}', cursor, duration, 'rest', written=written) | {'voice': 'voice_arranged'})
|
||||
cursor += duration
|
||||
|
||||
for start, group in itertools.groupby(attacks, key=lambda n: generator.model.fraction(n['onset'])):
|
||||
rest(start)
|
||||
group = list(group)
|
||||
pitches = sorted((p for n in group for p in n['pitches']), key=lambda p: (generator.model.pitch_number(p), p['tablature']['string']))
|
||||
pitches = [{k: v for k, v in p.items() if k != 'stem'} for p in pitches]
|
||||
duration = generator.model.fraction(group[0]['duration'])
|
||||
notes.append(generator.model.make_event(f'{measure["id"]}_acoustic_{len(notes) + 1}', start, duration, 'note', pitches, group[0]['written']) | {'voice': 'voice_arranged'})
|
||||
cursor = start + duration
|
||||
rest(length)
|
||||
if not attacks and length == generator.model.meter_length(measure['meter']):
|
||||
sounding = [(generator.model.fraction(n['onset']), generator.model.fraction(n['onset']) + generator.model.fraction(p['duration']), p)
|
||||
for v in measure['voices'] for n in v['notes'] for p in n['pitches']]
|
||||
boundaries = sorted({fractions.Fraction(), length, *(t for a, b, _ in sounding for t in (a, b))})
|
||||
notes = []
|
||||
for a, b in zip(boundaries, boundaries[1:]):
|
||||
active = sorted((p for start, end, p in sounding if start <= a < end), key=lambda p: (generator.model.pitch_number(p), p['tablature']['string']))
|
||||
for duration, written in generator.accompaniment.written_durations(b - a):
|
||||
pitches = [{k: v for k, v in p.items() if k not in ('tie_from', 'tie_to', 'stem')} for p in active]
|
||||
note = generator.model.make_event(f'{measure["id"]}_acoustic_{len(notes) + 1}', a, duration, 'note' if pitches else 'rest', pitches, written)
|
||||
note['voice'] = 'voice_arranged'
|
||||
for old, new in zip(active, note['pitches']):
|
||||
fragments.setdefault(old['id'], []).append(new)
|
||||
notes.append(note)
|
||||
a += duration
|
||||
if not sounding and length == generator.model.meter_length(measure['meter']):
|
||||
notes = [generator.model.make_event(f'{measure["id"]}_acoustic_1', fractions.Fraction(), length, 'rest', written={'type': 'whole', 'dots': 0}) | {'voice': 'voice_arranged', 'measure_rest': True}]
|
||||
measure['voices'] = [{'id': 'voice_arranged', 'notes': notes}]
|
||||
for pieces in fragments.values():
|
||||
for a, b in zip(pieces, pieces[1:]):
|
||||
a['tie_to'], b['tie_from'] = b['id'], a['id']
|
||||
for start, end in edges:
|
||||
if start in fragments or end in fragments:
|
||||
a = fragments[start][-1] if start in fragments else originals[start]
|
||||
b = fragments[end][0] if end in fragments else originals[end]
|
||||
a['tie_to'], b['tie_from'] = b['id'], a['id']
|
||||
|
||||
@@ -100,9 +100,10 @@ def _arrange_document(document: dict, part_type: str, plans=None, *, owned=None,
|
||||
tuplets = generator.arrange.pickup.retain_links(output, tuplets)
|
||||
result = generator.score.make_arranged_document(document, staff, staff_id='staff_arranged_' + part_type, staff_name=part_type.capitalize(), role='vocal' if part_type == 'vocal' else 'guitar', arrangement_part_type=part_type, measures=output, tuplets=tuplets, octave_shifts=octave_shifts)
|
||||
result['metadata'].setdefault('tempo', 124)
|
||||
if part_type == 'acoustic' and accompaniment is None and (owned is None or 'accompaniment' in owned):
|
||||
generator.acoustic.prepare(result)
|
||||
generator.acoustic_voicing.place(result)
|
||||
if part_type == 'acoustic':
|
||||
if accompaniment is None and (owned is None or 'accompaniment' in owned):
|
||||
generator.acoustic.prepare(result)
|
||||
generator.acoustic_voicing.place(result)
|
||||
generator.acoustic.combine(result)
|
||||
elif part_type == 'electric' and (owned is None or 'melody' in owned):
|
||||
generator.melody.harmonize(result)
|
||||
|
||||
@@ -0,0 +1,49 @@
|
||||
"""Transposable guitar families, derived from the written chord and legal grips."""
|
||||
import generator.harmony
|
||||
import generator.model
|
||||
import generator.vocabulary
|
||||
|
||||
def acoustic(chord, grip):
|
||||
degrees = generator.harmony.degrees(chord)
|
||||
root = int(generator.model.NATURAL[chord['root']['letter']] + generator.model.alteration(chord['root']['alter']))
|
||||
classes = {d: (root + generator.harmony.degree_semitones(d, a)) % 12 for d, a in degrees.items()}
|
||||
guide = next((d for d in (3, 4, 2) if d in degrees), None)
|
||||
terminal = next((d for d in (7, 6) if d in degrees), None)
|
||||
color = max((d for d in degrees if d > 7), default=None)
|
||||
kind = 'extended' if terminal and color else 'tetrad' if terminal else 'triad'
|
||||
bass = next((d for d in (1, 5, 3) if classes.get(d) == grip[0].midi % 12), None)
|
||||
slots = {'root': 1, 'fifth': 5, 'guide': guide, 'terminal': terminal, 'color': color}
|
||||
policy = generator.vocabulary.accompaniment('acoustic')['voicing_families']
|
||||
orders = policy[kind].get(f'{grip[0].string_number}/{bass}', ())
|
||||
upper = tuple(p.midi % 12 for p in grip[1:])
|
||||
preferred = any(all(slots[s] in classes for s in order) and tuple(classes[slots[s]] for s in order) == upper for order in orders)
|
||||
# The physical/harmonic enumerator remains authoritative: an explicit bass
|
||||
# or an unrepresented extension can use another valid compact voicing.
|
||||
return {'id': f'{kind}/s{grip[0].string_number}/bass{bass or "written"}/' + '-'.join(str(next((d for d in sorted(classes) if pc == classes[d]), 'bass')) for pc in upper),
|
||||
'preferred': preferred, 'bass_degree': bass, 'bass_string': grip[0].string_number,
|
||||
'relative_frets': [p.fret - grip[0].fret for p in grip]}
|
||||
|
||||
def electric(grip):
|
||||
"""Classify actual voicing geometry, without inventing chord permissions."""
|
||||
pitches = sorted(p.midi for p in grip)
|
||||
if len(grip) == 3:
|
||||
return 'three-note-shell'
|
||||
if len(set(p % 12 for p in pitches)) < len(pitches):
|
||||
return 'compact-doubled'
|
||||
if len(pitches) == 4:
|
||||
lifted = sorted([pitches[0] + 12, *pitches[1:]])
|
||||
if lifted[-1] - lifted[0] < 12 and lifted[2] == pitches[0] + 12:
|
||||
return 'drop-two'
|
||||
return 'close-position' if pitches and pitches[-1] - pitches[0] < 12 else 'compact-open'
|
||||
|
||||
def coverage(chord, options, part):
|
||||
if part == 'electric':
|
||||
return {name: sum(electric(v) == name for v in options) for name in generator.vocabulary.accompaniment('electric')['voicing_families']['rank']}
|
||||
families = [acoustic(chord, v) for v in options]
|
||||
result = []
|
||||
for string in (5, 6):
|
||||
for degree in (1, 5):
|
||||
found = [f for f in families if f['bass_degree'] == degree and f['bass_string'] == string]
|
||||
result.append({'bass_string': string, 'bass_degree': degree, 'candidates': len(found), 'preferred': sum(f['preferred'] for f in found),
|
||||
'reason': '' if found else 'no legal grip for this bass under written harmony and current fret/span limits'})
|
||||
return result
|
||||
@@ -8,6 +8,7 @@ import fractions
|
||||
|
||||
import generator.accompaniment
|
||||
import generator.comping_rhythm
|
||||
import generator.chord_families
|
||||
import generator.acoustic_voicing
|
||||
import generator.arrange.form
|
||||
import generator.fretboard
|
||||
@@ -67,9 +68,10 @@ def local_cost(part, key, grip, center):
|
||||
register = abs(fractions.Fraction(sum(p.midi for p in grip), len(grip)) - center)
|
||||
if part == 'electric':
|
||||
value = generator.guitar.voicing_cost(grip, 'electric', required.allowed)
|
||||
return (value.string_shortfall, value.effort, value.missing_tones, register)
|
||||
# As in acoustic harmony-span placement: travel first, then completeness.
|
||||
return (0, 0, len(set(required.allowed) - {p.midi % 12 for p in grip}), register)
|
||||
return (3 * value.string_shortfall + value.family_penalty, value.effort, value.missing_tones, register)
|
||||
# Familiar degree orders on either bass string, then whole-form movement.
|
||||
family = generator.chord_families.acoustic(json.loads(key), grip)
|
||||
return (int(not family['preferred']), 0, len(set(required.allowed) - {p.midi % 12 for p in grip}), register)
|
||||
|
||||
def select_grips(layers, costs, part, seed, *, left=(), right=(), close=False):
|
||||
"""Exact small-state dynamic programming; never prune by hand center.
|
||||
@@ -146,7 +148,8 @@ class Canonical:
|
||||
self.cache[self.key(source, prior, index)] = self.render(source, prior, index, spans, selected, previous, False)
|
||||
if selected:
|
||||
previous = selected[-1]
|
||||
self.connect_holds(inputs)
|
||||
if generator.vocabulary.accompaniment(part)['connect_holds']:
|
||||
self.connect_holds(inputs)
|
||||
@property
|
||||
def report(self):
|
||||
return [row for key in sorted(self._reports, key=lambda k: (k[0], repr(k))) for row in self._reports[key]]
|
||||
@@ -207,6 +210,8 @@ class Canonical:
|
||||
root = int(generator.model.NATURAL[span.chord['root']['letter']] + generator.model.alteration(span.chord['root']['alter']))
|
||||
rows.append({'source_index': index, 'onset': str(span.start), 'chord': generator.harmony.symbol(span.chord),
|
||||
'figure': span.figure, 'tones': len(grip), 'bass': bool(bass), 'variant': variant,
|
||||
'family': (generator.chord_families.acoustic(span.chord, grip) if self.part == 'acoustic' else generator.chord_families.electric(grip)) if grip else None,
|
||||
'family_coverage': generator.chord_families.coverage(span.chord, span.options, self.part),
|
||||
'positions': [{'string': p.string_number, 'fret': p.fret, 'sounding_midi': p.midi} for p in grip],
|
||||
'bass_degree': next((d for d, a in degrees.items() if bass and (root + generator.harmony.degree_semitones(d, a)) % 12 == bass.midi % 12), None),
|
||||
'bass_string': bass.string_number if bass else None,
|
||||
|
||||
@@ -137,7 +137,7 @@ def lane_candidates(measure, prior, lane, melody, policy, seed):
|
||||
continue
|
||||
if any(not any(a <= t < b for t in onsets) for a, b in required):
|
||||
continue
|
||||
if any(b - a < 1 for a, b in zip(onsets, onsets[1:])):
|
||||
if any(b - a < fractions.Fraction(policy['attack_spacing']) for a, b in zip(onsets, onsets[1:])):
|
||||
continue
|
||||
for family in policy['layers'][lane]['families']:
|
||||
durations = policy['families'][family]
|
||||
@@ -147,12 +147,14 @@ def lane_candidates(measure, prior, lane, melody, policy, seed):
|
||||
following = onsets[i + 1] if i + 1 < len(onsets) else length
|
||||
duration = fractions.Fraction(durations[i % len(durations)])
|
||||
events.append(Attack(lane, a, min(a + duration, following, end)))
|
||||
if policy.get('allowed_durations') and any(e.end - e.start not in tuple(map(fractions.Fraction, policy['allowed_durations'])) for e in events):
|
||||
continue
|
||||
exceptions = ((lane, 'extra attack for harmonic changes'),) if count > high else ()
|
||||
contributions = (*lane_score(events, lane, melody, harmony, length), ('extra_attacks', .5 * bool(exceptions)))
|
||||
result.append(Rhythm(tuple(events), ((lane, family if count else 'space'),), contributions, exceptions))
|
||||
if not result:
|
||||
# A short partial bar can have fewer gestures, not invented subdivisions.
|
||||
if length < 1:
|
||||
if length < 1 and not policy.get('allowed_durations'):
|
||||
a, b, _ = active[0]
|
||||
return Rhythm((Attack(lane, a, b),), ((lane, 'partial'),), (('activity', 0.),)),
|
||||
raise ValueError(f'measure {measure["number"]}: no {lane} rhythm within the bounded attack/spacing policy')
|
||||
@@ -166,7 +168,7 @@ def crowding(events):
|
||||
return cost + .5 * sum(z - x <= 1 for x, y, z in zip(times, times[1:], times[2:]))
|
||||
|
||||
def candidates(measure, prior, part, melody, seed):
|
||||
policy = generator.vocabulary.data('accompaniment')
|
||||
policy = generator.vocabulary.accompaniment(part)
|
||||
if part == 'electric':
|
||||
return lane_candidates(measure, prior, 'electric', melody, policy, seed)
|
||||
bass = lane_candidates(measure, prior, 'bass', melody, policy, seed)
|
||||
|
||||
+21
-5
@@ -10,14 +10,18 @@ import typing
|
||||
import generator.fretboard
|
||||
import generator.model
|
||||
import generator.pitch
|
||||
import generator.chord_families
|
||||
import generator.vocabulary
|
||||
|
||||
PREFERRED_MAX_FRET = 12
|
||||
ELECTRIC_STRINGS = ((3, 2, 1), (4, 3, 2, 1))
|
||||
|
||||
class VoicingCost(typing.NamedTuple):
|
||||
string_shortfall: int = 0
|
||||
melody_string: int = 0
|
||||
covered_melody: int = 0
|
||||
covering_tones: int = 0
|
||||
string_shortfall: int = 0
|
||||
family_penalty: int = 0
|
||||
effort: int = 0
|
||||
missing_tones: int = 0
|
||||
def plus(self, other: VoicingCost) -> VoicingCost:
|
||||
@@ -26,8 +30,12 @@ class VoicingCost(typing.NamedTuple):
|
||||
def moved(self, distance: int) -> VoicingCost:
|
||||
return self._replace(effort=self.effort + distance)
|
||||
|
||||
@functools.lru_cache(maxsize=8192)
|
||||
def voicing_cost(positions: tuple[generator.fretboard.TabPosition, ...], profile: str = '', allowed: tuple[int, ...] = (), melody: tuple[int, ...] = ()) -> VoicingCost:
|
||||
families = tuple(generator.vocabulary.accompaniment('electric')['voicing_families']['rank'].items()) if profile == 'electric' else ()
|
||||
return _voicing_cost(positions, profile, allowed, melody, families)
|
||||
|
||||
@functools.lru_cache(maxsize=8192)
|
||||
def _voicing_cost(positions, profile, allowed, melody, families):
|
||||
frets = {p.fret for p in positions}
|
||||
classes = {p.midi % 12 for p in positions}
|
||||
above = sum(p.midi > max(melody) for p in positions) if melody else 0
|
||||
@@ -36,10 +44,16 @@ def voicing_cost(positions: tuple[generator.fretboard.TabPosition, ...], profile
|
||||
effort = 2 * max(0, len(frets) - 1) + 3 * span
|
||||
else:
|
||||
effort = 2 * max(0, max(frets, default=0) - PREFERRED_MAX_FRET) + span
|
||||
return VoicingCost(max(0, 4 - len(positions)) if profile == 'electric' else 0, int(bool(above)), above, effort, len(set(allowed) - classes))
|
||||
family_penalty = dict(families).get(generator.chord_families.electric(positions), 0) if positions and families else 0
|
||||
melody_string = min((p.string_number - 1 for p in positions if melody and p.midi == max(melody)), default=0)
|
||||
return VoicingCost(melody_string=melody_string, string_shortfall=max(0, 4 - len(positions)) if profile == 'electric' else 0, covered_melody=int(bool(above)), covering_tones=above, family_penalty=family_penalty, effort=effort, missing_tones=len(set(allowed) - classes))
|
||||
|
||||
def chord_positions(requirements: generator.fretboard.ChordRequirements, prune: bool = True, profile: str = '', melody: tuple[int, ...] = ()) -> tuple[tuple[generator.fretboard.TabPosition, ...], ...]:
|
||||
families = tuple(generator.vocabulary.accompaniment('electric')['voicing_families']['rank'].items()) if profile == 'electric' else ()
|
||||
return _chord_positions(requirements, prune, profile, melody, families)
|
||||
|
||||
@functools.lru_cache(maxsize=256)
|
||||
def chord_positions(requirements: generator.fretboard.ChordRequirements, prune: bool = True, profile: str = '', melody: tuple[int, ...] = ()) -> tuple[tuple[generator.fretboard.TabPosition, ...], ...]:
|
||||
def _chord_positions(requirements, prune, profile, melody, families):
|
||||
result = []
|
||||
options = [tuple(generator.fretboard.TabPosition(string, fret, open_midi + fret) for fret in range(generator.fretboard.MIN_FRET, generator.fretboard.MAX_FRET + 1) if (open_midi + fret) % 12 in requirements.allowed or open_midi + fret in melody) for string, open_midi in generator.fretboard.STANDARD_TUNING]
|
||||
|
||||
@@ -57,7 +71,9 @@ def chord_positions(requirements: generator.fretboard.ChordRequirements, prune:
|
||||
|
||||
if profile:
|
||||
by_string = {string: options[i] for i, (string, _) in enumerate(generator.fretboard.STANDARD_TUNING)}
|
||||
for strings in ELECTRIC_STRINGS:
|
||||
# A lower source melody can be the top of a three-note adaptation on
|
||||
# strings 2-4. It need not be buried beneath an added first-string tone.
|
||||
for strings in (*ELECTRIC_STRINGS, (4, 3, 2)) if melody else ELECTRIC_STRINGS:
|
||||
for selected in itertools.product(*(by_string[s] for s in strings)):
|
||||
if not generator.fretboard.within_fret_span(selected) or not generator.fretboard.has_fret_anchor(selected) or not requirements.covers({p.midi % 12 for p in selected}) or not set(melody) <= {p.midi for p in selected}:
|
||||
continue
|
||||
|
||||
@@ -97,8 +97,10 @@ def windows(document, part):
|
||||
return tuple(result)
|
||||
|
||||
def dependencies(role, resources):
|
||||
names = ['harmony', 'lead_ins'] if role.endswith('_solo') else ['accompaniment'] if role.endswith('_accompaniment') else []
|
||||
names = ['harmony', 'lead_ins'] if role.endswith('_solo') else []
|
||||
return {'roles': {'role': resources['roles']['roles'][role], 'difficulty': resources['roles']['difficulty']},
|
||||
**({'voicing_families': generator.vocabulary.accompaniment('electric', resources)['voicing_families']} if role == 'electric_melody' else {}),
|
||||
**({'accompaniment': generator.vocabulary.accompaniment(role.split('_')[0], resources)} if role.endswith('_accompaniment') else {}),
|
||||
**{name: resources[name] for name in names}}
|
||||
|
||||
def _code_key(role):
|
||||
@@ -109,11 +111,11 @@ def _code_key(role):
|
||||
paths += list((root / 'solo').glob('*.py'))
|
||||
paths += [root / 'harmonic_time.py', root / 'musicxml.py', root / 'training_io.py']
|
||||
elif role.endswith('_accompaniment'):
|
||||
paths += [root / n for n in ('comping.py', 'comping_rhythm.py', 'accompaniment.py', 'guitar.py')]
|
||||
paths += [root / n for n in ('comping.py', 'comping_rhythm.py', 'chord_families.py', 'accompaniment.py', 'guitar.py')]
|
||||
if role == 'acoustic_accompaniment':
|
||||
paths += [root / 'acoustic_voicing.py', root / 'acoustic.py']
|
||||
elif role == 'electric_melody':
|
||||
paths += [root / 'guitar.py', root / 'melody.py']
|
||||
paths += [root / 'guitar.py', root / 'melody.py', root / 'chord_families.py']
|
||||
return generator.vocabulary.fingerprint([(p.relative_to(root).as_posix(), hashlib.sha256(p.read_bytes()).hexdigest()) for p in sorted(set(paths))])
|
||||
|
||||
def _resource_key(directory, role):
|
||||
|
||||
+34
-8
@@ -23,18 +23,40 @@ def _load(contents):
|
||||
result = {}
|
||||
for name, text in contents:
|
||||
value = json.loads(text)
|
||||
if value.get('version') != (2 if name == 'accompaniment' else 1) or not value.get('provenance') or not value.get('license'):
|
||||
if value.get('version') != (3 if name == 'accompaniment' else 1) or not value.get('provenance') or not value.get('license'):
|
||||
raise ValueError(f'{name}: unsupported or unprovenanced production vocabulary')
|
||||
if name == 'accompaniment':
|
||||
if fractions.Fraction(value['grid']) != fractions.Fraction(1, 2):
|
||||
raise ValueError('accompaniment requires an eighth-note placement grid')
|
||||
for lane in ('electric', 'bass', 'upper'):
|
||||
layer = value['layers'][lane]
|
||||
if not 0 <= layer['attacks'][0] <= layer['attacks'][1] <= 3 or not layer['families']:
|
||||
raise ValueError('invalid accompaniment attack range or duration families')
|
||||
for family in layer['families']:
|
||||
if not value['families'][family] or any(fractions.Fraction(d) <= 0 for d in value['families'][family]):
|
||||
raise ValueError('accompaniment durations must be positive')
|
||||
for part, lanes in (('electric', ('electric',)), ('acoustic', ('bass', 'upper'))):
|
||||
policy = value['instruments'][part]
|
||||
if fractions.Fraction(policy['attack_spacing']) < fractions.Fraction(1, 2):
|
||||
raise ValueError('accompaniment attacks must be at least an eighth apart')
|
||||
for lane in lanes:
|
||||
layer = policy['layers'][lane]
|
||||
if not 0 <= layer['attacks'][0] <= layer['attacks'][1] <= 3 or not layer['families']:
|
||||
raise ValueError('invalid accompaniment attack range or duration families')
|
||||
for family in layer['families']:
|
||||
durations = policy['families'][family]
|
||||
if not durations or any(fractions.Fraction(d) <= 0 for d in durations):
|
||||
raise ValueError('accompaniment durations must be positive')
|
||||
if part == 'acoustic' and any(fractions.Fraction(d) not in (fractions.Fraction(1, 2), 1) for d in durations):
|
||||
raise ValueError('acoustic rhythm uses quarter and eighth notes')
|
||||
families = policy['voicing_families']
|
||||
if not families.get('provenance'):
|
||||
raise ValueError('guitar voicing families require provenance')
|
||||
if part == 'electric':
|
||||
if set(families['rank']) != {'drop-two', 'close-position', 'compact-open', 'compact-doubled', 'three-note-shell'} or any(type(v) is not int or not 0 <= v <= 2 for v in families['rank'].values()):
|
||||
raise ValueError('invalid electric family ranking')
|
||||
else:
|
||||
if set(map(fractions.Fraction, policy['allowed_durations'])) != {fractions.Fraction(1, 2), fractions.Fraction(1)} or policy['connect_holds']:
|
||||
raise ValueError('acoustic rhythm must retain short quarter/eighth attacks')
|
||||
for kind in ('triad', 'tetrad', 'extended'):
|
||||
if set(families[kind]) != {'5/1', '5/5', '6/1', '6/5'}:
|
||||
raise ValueError('acoustic families require root/fifth bass on both bass strings')
|
||||
for orders in families[kind].values():
|
||||
if not orders or any(len(o) != 3 or set(o) - {'root', 'fifth', 'guide', 'terminal', 'color'} for o in orders):
|
||||
raise ValueError('invalid acoustic upper chord order')
|
||||
if not 1 <= value['lane_candidates'] <= 64 or not 1 <= value['measure_candidates'] <= 64:
|
||||
raise ValueError('accompaniment candidate budgets must be 1–64')
|
||||
entries = value.get('cells', value.get('patterns', value.get('colors', value.get('transformations', []))))
|
||||
@@ -53,6 +75,10 @@ def current():
|
||||
def data(name):
|
||||
return current()['data'][name]
|
||||
|
||||
def accompaniment(part, resources=None):
|
||||
value = (resources or current()['data'])['accompaniment']
|
||||
return {**{k: v for k, v in value.items() if k != 'instruments'}, **value['instruments'][part]}
|
||||
|
||||
def role(name=None):
|
||||
active = current()
|
||||
name = name or active['role']
|
||||
|
||||
Reference in New Issue
Block a user