+40
-39
@@ -1,13 +1,14 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from fractions import Fraction
|
||||
from dataclasses import dataclass
|
||||
from functools import lru_cache
|
||||
from itertools import groupby
|
||||
from typing import NamedTuple
|
||||
import dataclasses
|
||||
import fractions
|
||||
import functools
|
||||
import itertools
|
||||
import typing
|
||||
|
||||
from generator import harmony, pitch
|
||||
from generator.model import NATURAL, alteration, fraction, make_event, pitch_number
|
||||
import generator.harmony
|
||||
import generator.model
|
||||
import generator.pitch
|
||||
|
||||
STANDARD_TUNING = ((6, 40), (5, 45), (4, 50), (3, 55), (2, 59), (1, 64))
|
||||
MIN_FRET = 0
|
||||
@@ -15,7 +16,7 @@ MAX_FRET = 20
|
||||
MAX_FRET_SPAN = 4
|
||||
WRITTEN_GUITAR_OCTAVE_OFFSET = 12
|
||||
|
||||
class TabPosition(NamedTuple):
|
||||
class TabPosition(typing.NamedTuple):
|
||||
string_number: int
|
||||
fret: int
|
||||
midi: int
|
||||
@@ -24,7 +25,7 @@ def within_fret_span(positions) -> bool:
|
||||
fretted = [position.fret for position in positions if position.fret > 0]
|
||||
return not fretted or max(fretted) - min(fretted) <= MAX_FRET_SPAN
|
||||
|
||||
def positions_for_pitch(midi: Fraction) -> tuple[TabPosition, ...]:
|
||||
def positions_for_pitch(midi: fractions.Fraction) -> tuple[TabPosition, ...]:
|
||||
if midi.denominator != 1:
|
||||
return ()
|
||||
return tuple(TabPosition(string, int(midi - open_midi), int(midi)) for string, open_midi in STANDARD_TUNING if MIN_FRET <= midi - open_midi <= MAX_FRET)
|
||||
@@ -35,20 +36,20 @@ def validate_tuning(state: dict, location: str) -> None:
|
||||
raise ValueError(f'{location}: guitar generation does not support capo configurations')
|
||||
tunings = details.get('tunings', [])
|
||||
expected = {7 - string: midi for string, midi in STANDARD_TUNING}
|
||||
if tunings and (len(tunings) != 6 or {t['line']: pitch_number(t) for t in tunings} != expected):
|
||||
if tunings and (len(tunings) != 6 or {t['line']: generator.model.pitch_number(t) for t in tunings} != expected):
|
||||
raise ValueError(f'{location}: guitar generation requires standard six-string tuning')
|
||||
|
||||
def chord_requirements(definition: dict) -> tuple[tuple[int, ...], tuple[int, ...], int | None]:
|
||||
degrees = harmony.degrees(definition)
|
||||
degrees = generator.harmony.degrees(definition)
|
||||
root = definition['root']
|
||||
root_pc = NATURAL[root['letter']] + alteration(root['alter'])
|
||||
root_pc = generator.model.NATURAL[root['letter']] + generator.model.alteration(root['alter'])
|
||||
explicit = {d['value'] for d in definition.get('degrees', []) if d['type'] != 'subtract'}
|
||||
optional = {d for d, a in degrees.items() if not a and d not in explicit and (d == 5 and len(degrees) > 3 or d in (9, 11) and d < max(degrees))}
|
||||
classes = {d: (root_pc + harmony.degree_semitones(d, a)) % 12 for d, a in degrees.items()}
|
||||
classes = {d: (root_pc + generator.harmony.degree_semitones(d, a)) % 12 for d, a in degrees.items()}
|
||||
required = {value for d, value in classes.items() if d not in optional}
|
||||
allowed = set(classes.values())
|
||||
bass = definition.get('bass')
|
||||
bass_pc = (NATURAL[bass['letter']] + alteration(bass['alter'])) % 12 if bass else None
|
||||
bass_pc = (generator.model.NATURAL[bass['letter']] + generator.model.alteration(bass['alter'])) % 12 if bass else None
|
||||
if bass_pc is None and 'inversion' in definition:
|
||||
ordered = sorted(degrees)
|
||||
inversion = definition['inversion']
|
||||
@@ -60,7 +61,7 @@ def chord_requirements(definition: dict) -> tuple[tuple[int, ...], tuple[int, ..
|
||||
required.add(bass_pc)
|
||||
return tuple(sorted(required)), tuple(sorted(allowed)), bass_pc
|
||||
|
||||
@lru_cache(maxsize=256)
|
||||
@functools.lru_cache(maxsize=256)
|
||||
def chord_positions(required: tuple[int, ...], allowed: tuple[int, ...], bass: int | None, prune: bool = True) -> tuple[tuple[TabPosition, ...], ...]:
|
||||
result = []
|
||||
options = [tuple(TabPosition(string, fret, open_midi + fret) for fret in range(MIN_FRET, MAX_FRET + 1) if (open_midi + fret) % 12 in allowed) for string, open_midi in STANDARD_TUNING]
|
||||
@@ -92,26 +93,26 @@ def chord_positions(required: tuple[int, ...], allowed: tuple[int, ...], bass: i
|
||||
|
||||
def chord_pitches(definition: dict, positions: tuple[TabPosition, ...]) -> list[dict]:
|
||||
spellings = {}
|
||||
for degree, alter in harmony.degrees(definition).items():
|
||||
value = pitch.shift_pitch(definition['root'], degree - 1, harmony.degree_semitones(degree, alter))
|
||||
spellings[(NATURAL[value['letter']] + alteration(value['alter'])) % 12] = value
|
||||
for degree, alter in generator.harmony.degrees(definition).items():
|
||||
value = generator.pitch.shift_pitch(definition['root'], degree - 1, generator.harmony.degree_semitones(degree, alter))
|
||||
spellings[(generator.model.NATURAL[value['letter']] + generator.model.alteration(value['alter'])) % 12] = value
|
||||
if bass := definition.get('bass'):
|
||||
spellings[(NATURAL[bass['letter']] + alteration(bass['alter'])) % 12] = bass
|
||||
spellings[(generator.model.NATURAL[bass['letter']] + generator.model.alteration(bass['alter'])) % 12] = bass
|
||||
result = []
|
||||
for position in sorted(positions, key=lambda p: p.midi):
|
||||
value = spellings[position.midi % 12].copy()
|
||||
value['octave'] = int((position.midi + WRITTEN_GUITAR_OCTAVE_OFFSET - NATURAL[value['letter']] - alteration(value['alter'])) // 12 - 1)
|
||||
value['octave'] = int((position.midi + WRITTEN_GUITAR_OCTAVE_OFFSET - generator.model.NATURAL[value['letter']] - generator.model.alteration(value['alter'])) // 12 - 1)
|
||||
value['tablature'] = {'string': position.string_number, 'fret': position.fret}
|
||||
result.append(value)
|
||||
return result
|
||||
|
||||
@lru_cache(maxsize=16384)
|
||||
@functools.lru_cache(maxsize=16384)
|
||||
def movement(previous: tuple[TabPosition, ...], current: tuple[TabPosition, ...]) -> int:
|
||||
if not previous or not current:
|
||||
return 0
|
||||
return sum(abs(a - b) for a, b in zip(position_center(previous), position_center(current)))
|
||||
|
||||
@lru_cache(maxsize=4096)
|
||||
@functools.lru_cache(maxsize=4096)
|
||||
def position_center(positions: tuple[TabPosition, ...]) -> tuple[int, int]:
|
||||
frets = [p.fret for p in positions if p.fret] or [0]
|
||||
strings = [p.string_number for p in positions]
|
||||
@@ -131,7 +132,7 @@ def extend_positions(held, pending):
|
||||
if position.string_number not in occupied and within_fret_span((*held.values(), position)):
|
||||
yield from extend_positions({**held, identifier: position}, pending[1:])
|
||||
|
||||
@dataclass
|
||||
@dataclasses.dataclass
|
||||
class Placement:
|
||||
cost: tuple[int, int, int]
|
||||
active: tuple
|
||||
@@ -169,13 +170,13 @@ def nearest_positions(states: dict) -> list[list[tuple | None]]:
|
||||
relax(x, y, x, y + 1)
|
||||
return grid
|
||||
|
||||
@dataclass(frozen=True)
|
||||
@dataclasses.dataclass(frozen=True)
|
||||
class GuitarTone:
|
||||
pitch: dict
|
||||
end: Fraction
|
||||
end: fractions.Fraction
|
||||
positions: tuple[TabPosition, ...]
|
||||
|
||||
@dataclass(frozen=True)
|
||||
@dataclasses.dataclass(frozen=True)
|
||||
class AttackCandidate:
|
||||
active: tuple
|
||||
choices: tuple
|
||||
@@ -208,38 +209,38 @@ def attack_candidates(notes: list[dict], tones: dict[str, GuitarTone], active: d
|
||||
|
||||
def guitar_events(document: dict) -> tuple[list, dict[str, GuitarTone]]:
|
||||
events, tones = [], {}
|
||||
absolute = Fraction()
|
||||
absolute = fractions.Fraction()
|
||||
for measure in document['staves'][0]['measures']:
|
||||
changes = sorted(measure.get('attributes', []), key=lambda a: fraction(a['onset']))
|
||||
changes = sorted(measure.get('attributes', []), key=lambda a: generator.model.fraction(a['onset']))
|
||||
state = {k: measure[k] for k in ('transpose', 'staff_details')}
|
||||
for change in [state, *changes]:
|
||||
state.update({k: change[k] for k in state if k in change})
|
||||
validate_tuning(state, measure['id'])
|
||||
for voice in measure['voices']:
|
||||
for order, note in enumerate(voice['notes']):
|
||||
onset = fraction(note['onset'])
|
||||
onset = generator.model.fraction(note['onset'])
|
||||
state = {k: measure[k] for k in ('transpose', 'staff_details')}
|
||||
for change in changes:
|
||||
if fraction(change['onset']) <= onset:
|
||||
if generator.model.fraction(change['onset']) <= onset:
|
||||
state.update({k: change[k] for k in state if k in change})
|
||||
for tone in note['pitches']:
|
||||
sounding = pitch.sounding_pitch(tone, state)
|
||||
sounding = generator.pitch.sounding_pitch(tone, state)
|
||||
options = positions_for_pitch(sounding)
|
||||
if not options:
|
||||
raise ValueError(f'{measure["id"]}, event {note["id"]}: sounding pitch {sounding} has no string/fret in {MIN_FRET}-{MAX_FRET}')
|
||||
tones[tone['id']] = GuitarTone(tone, absolute + onset + fraction(tone['duration']), options)
|
||||
tones[tone['id']] = GuitarTone(tone, absolute + onset + generator.model.fraction(tone['duration']), options)
|
||||
if '_harmony' in note:
|
||||
for string, _ in STANDARD_TUNING:
|
||||
tones[f'{note["id"]}-s{string}'] = GuitarTone({}, absolute + onset + fraction(note['duration']), ())
|
||||
tones[f'{note["id"]}-s{string}'] = GuitarTone({}, absolute + onset + generator.model.fraction(note['duration']), ())
|
||||
if note['pitches'] or '_harmony' in note:
|
||||
events.append((absolute + onset, not bool(note.get('grace')), order if note.get('grace') else -1, note))
|
||||
absolute += fraction(measure['duration'])
|
||||
absolute += generator.model.fraction(measure['duration'])
|
||||
return sorted(events, key=lambda e: e[:3]), tones
|
||||
|
||||
def retain_tone(tone: GuitarTone, onset: Fraction) -> bool:
|
||||
def retain_tone(tone: GuitarTone, onset: fractions.Fraction) -> bool:
|
||||
return tone.end > onset or tone.end == onset and bool(tone.pitch.get('tie_to'))
|
||||
|
||||
def advance_positions(notes: list[dict], onset: Fraction, tones: dict[str, GuitarTone], states: dict, preserve_strings: bool) -> dict:
|
||||
def advance_positions(notes: list[dict], onset: fractions.Fraction, tones: dict[str, GuitarTone], states: dict, preserve_strings: bool) -> dict:
|
||||
continued = {p['tie_from'] for n in notes for p in n['pitches'] if 'tie_from' in p}
|
||||
held_ids = {i for previous in states.values() for i, _ in previous.active if retain_tone(tones[i], onset)}
|
||||
result = {}
|
||||
@@ -269,17 +270,17 @@ def advance_positions(notes: list[dict], onset: Fraction, tones: dict[str, Guita
|
||||
def place_guitar(document: dict) -> None:
|
||||
"""Choose a minimum-cost path, retaining held strings in each search state."""
|
||||
events, tones = guitar_events(document)
|
||||
groups = [(key[0], [item[3] for item in values]) for key, values in groupby(events, key=lambda e: e[:3])]
|
||||
groups = [(key[0], [item[3] for item in values]) for key, values in itertools.groupby(events, key=lambda e: e[:3])]
|
||||
states = {(): Placement((0, 0, 0), (), None, ())}
|
||||
for index, (onset, notes) in enumerate(groups):
|
||||
next_onset = groups[index + 1][0] if index + 1 < len(groups) else None
|
||||
preserve_strings = next_onset is not None and any('_harmony' in n and onset + fraction(n['duration']) > next_onset for n in notes)
|
||||
preserve_strings = next_onset is not None and any('_harmony' in n and onset + generator.model.fraction(n['duration']) > next_onset for n in notes)
|
||||
states = advance_positions(notes, onset, tones, states, preserve_strings)
|
||||
selected = min(states.values(), key=lambda s: (s.cost, s.active))
|
||||
while selected.previous is not None:
|
||||
for note, choices in selected.choices:
|
||||
if definition := note.pop('_harmony', None):
|
||||
note['pitches'] = make_event(note['id'], fraction(note['onset']), fraction(note['duration']), 'note', chord_pitches(definition, choices))['pitches']
|
||||
note['pitches'] = generator.model.make_event(note['id'], generator.model.fraction(note['onset']), generator.model.fraction(note['duration']), 'note', chord_pitches(definition, choices))['pitches']
|
||||
else:
|
||||
for tone, position in choices:
|
||||
tone['tablature'] = {'string': position.string_number, 'fret': position.fret}
|
||||
|
||||
Reference in New Issue
Block a user