@@ -0,0 +1,111 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from generator.model import NATURAL, parse_pitch_name, pitch_name
|
||||
|
||||
# Semantic degrees are independent of guitar voicing omissions.
|
||||
KINDS = {
|
||||
'major': {1:0, 3:0, 5:0}, 'minor': {1:0, 3:-1, 5:0},
|
||||
'augmented': {1:0, 3:0, 5:1}, 'diminished': {1:0, 3:-1, 5:-1},
|
||||
'dominant': {1:0, 3:0, 5:0, 7:-1}, 'major-seventh': {1:0, 3:0, 5:0, 7:0},
|
||||
'minor-seventh': {1:0, 3:-1, 5:0, 7:-1}, 'diminished-seventh': {1:0, 3:-1, 5:-1, 7:-2},
|
||||
'augmented-seventh': {1:0, 3:0, 5:1, 7:-1}, 'augmented-major-seventh': {1:0, 3:0, 5:1, 7:0},
|
||||
'half-diminished': {1:0, 3:-1, 5:-1, 7:-1}, 'major-minor': {1:0, 3:-1, 5:0, 7:0},
|
||||
'major-sixth': {1:0, 3:0, 5:0, 6:0}, 'minor-sixth': {1:0, 3:-1, 5:0, 6:0},
|
||||
'suspended-second': {1:0, 2:0, 5:0}, 'suspended-fourth': {1:0, 4:0, 5:0},
|
||||
'power': {1:0, 5:0}, 'pedal': {1:0}, 'other': {}, 'none': {},
|
||||
'Neapolitan': {1:0, 3:0, 5:0}, 'Italian': {1:0, 3:0, 6:1},
|
||||
'French': {1:0, 3:0, 4:1, 6:1}, 'German': {1:0, 3:0, 5:0, 6:1},
|
||||
}
|
||||
for family, base in (('dominant', 'dominant'), ('major', 'major-seventh'), ('minor', 'minor-seventh')):
|
||||
for suffix, highest in (('ninth',9), ('11th',11), ('13th',13)):
|
||||
KINDS[f'{family}-{suffix}'] = {**KINDS[base], **{i:0 for i in range(9, highest+1, 2)}}
|
||||
|
||||
SUFFIXES = {'major':'', 'minor':'-', 'augmented':'+', 'diminished':'dim', 'dominant':'7', 'major-seventh':'maj7', 'minor-seventh':'-7', 'diminished-seventh':'dim7', 'augmented-seventh':'+7', 'augmented-major-seventh':'+maj7', 'half-diminished':'-7b5', 'major-minor':'-maj7', 'major-sixth':'6', 'minor-sixth':'-6', 'suspended-second':'sus2', 'suspended-fourth':'sus4', 'power':'5', 'pedal':'', 'other':''}
|
||||
for family, prefix in (('dominant',''), ('major','maj'), ('minor','-')):
|
||||
for suffix, number in (('ninth','9'), ('11th','11'), ('13th','13')):
|
||||
SUFFIXES[f'{family}-{suffix}'] = prefix + number
|
||||
|
||||
|
||||
def degrees(harmony: dict) -> dict[int, int]:
|
||||
kind = harmony['kind']
|
||||
if kind not in KINDS:
|
||||
raise ValueError(f'unsupported harmony kind: {kind}')
|
||||
result = KINDS[kind].copy()
|
||||
for change in harmony.get('degrees', []):
|
||||
degree, alter, operation = change['value'], change['alter'], change['type']
|
||||
if type(degree) is not int or degree < 1 or type(alter) is not int:
|
||||
raise ValueError('invalid harmony degree')
|
||||
if operation == 'subtract':
|
||||
if degree not in result:
|
||||
raise ValueError(f'cannot subtract absent chord degree {degree}')
|
||||
del result[degree]
|
||||
elif operation == 'alter':
|
||||
if degree not in result:
|
||||
raise ValueError(f'cannot alter absent chord degree {degree}; use add')
|
||||
result[degree] = alter
|
||||
elif operation == 'add':
|
||||
if degree in result:
|
||||
raise ValueError(f'duplicate added chord degree {degree}')
|
||||
result[degree] = alter
|
||||
else:
|
||||
raise ValueError(f'unsupported degree operation: {operation}')
|
||||
if kind != 'none' and not result:
|
||||
raise ValueError('harmony contains no pitch information')
|
||||
return result
|
||||
|
||||
|
||||
def degree_semitones(degree: int, alter: int) -> int:
|
||||
return 12 * ((degree-1)//7) + (0,2,4,5,7,9,11)[(degree-1)%7] + alter
|
||||
|
||||
|
||||
def pitch_classes(harmony: dict) -> tuple[int, ...]:
|
||||
if harmony['kind'] == 'none':
|
||||
return ()
|
||||
root = harmony['root']
|
||||
root_pc = NATURAL[root['letter']] + root['alter']
|
||||
values = [(root_pc + degree_semitones(d, a)) % 12 for d, a in degrees(harmony).items()]
|
||||
if bass := harmony.get('bass'):
|
||||
values.append((NATURAL[bass['letter']] + bass['alter']) % 12)
|
||||
return tuple(dict.fromkeys(values))
|
||||
|
||||
|
||||
def symbol(harmony: dict) -> str:
|
||||
if harmony['kind'] == 'none':
|
||||
return 'N.C.'
|
||||
suffix = harmony.get('kind_text', SUFFIXES.get(harmony['kind'], harmony['kind']))
|
||||
if 'kind_text' not in harmony:
|
||||
for d in harmony.get('degrees', []):
|
||||
sign = '#' * max(d['alter'], 0) + 'b' * max(-d['alter'], 0)
|
||||
suffix += ('no' if d['type']=='subtract' else 'add' if d['type']=='add' and not sign else '') + sign + str(d['value'])
|
||||
result = pitch_name(harmony['root']) + suffix
|
||||
if bass := harmony.get('bass'):
|
||||
result += '/' + pitch_name(bass)
|
||||
return result
|
||||
|
||||
|
||||
def parse_symbol(text: str) -> dict:
|
||||
import re
|
||||
match = re.fullmatch(r'([A-Ga-g][#b]*)([^/]*)(?:/([A-Ga-g][#b]*))?', text.strip())
|
||||
if not match:
|
||||
raise ValueError(f'invalid chord symbol: {text!r}')
|
||||
root, suffix, bass = match.groups()
|
||||
aliases = {'m':'minor', 'min':'minor', 'maj':'major', 'M':'major', 'm7':'minor-seventh', 'm6':'minor-sixth', 'm7b5':'half-diminished', 'o':'diminished', 'o7':'diminished-seventh', 'aug':'augmented', 'sus':'suspended-fourth'}
|
||||
aliases.update({v:k for k,v in SUFFIXES.items() if k not in ('other','pedal')})
|
||||
core = suffix
|
||||
changes = []
|
||||
while core not in aliases:
|
||||
m = re.search(r'(add|no)?([b#]?)(\d+)$', core)
|
||||
if not m:
|
||||
raise ValueError(f'unsupported chord spelling: {text!r}')
|
||||
op, accidental, value = m.groups()
|
||||
changes.insert(0, {'value':int(value), 'alter':1 if accidental=='#' else -1 if accidental=='b' else 0, 'type':'subtract' if op=='no' else 'add'})
|
||||
core = core[:m.start()]
|
||||
kind = aliases[core]
|
||||
for d in changes:
|
||||
if d['type']=='add' and d['value'] in KINDS[kind]:
|
||||
d['type'] = 'alter'
|
||||
result = {'root':parse_pitch_name(root), 'kind':kind, 'degrees':changes, 'kind_text':suffix}
|
||||
if bass:
|
||||
result['bass'] = parse_pitch_name(bass)
|
||||
degrees(result)
|
||||
return result
|
||||
Reference in New Issue
Block a user