from __future__ import annotations import dataclasses import fractions import typing 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 = 1 MAX_FRET = 20 MAX_FRET_SPAN = 3 WRITTEN_GUITAR_OCTAVE_OFFSET = 12 class TabPosition(typing.NamedTuple): string_number: int fret: int midi: int def within_fret_span(positions) -> bool: frets = [position.fret for position in positions] return not frets or max(frets) - min(frets) <= MAX_FRET_SPAN 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) def validate_tuning(state: dict, location: str) -> None: details = state['staff_details'] if details.get('capo', 0): 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']: generator.model.pitch_number(t) for t in tunings} != expected): raise ValueError(f'{location}: guitar generation requires standard six-string tuning') @dataclasses.dataclass(frozen=True) class ChordRequirements: required: tuple[int, ...] = () allowed: tuple[int, ...] = () foundation: tuple[int, ...] = () basses: tuple[int, ...] = () def covers(self, classes: set[int]) -> bool: return set(self.required) <= classes and bool(set(self.foundation) & classes) def chord_requirements(definition: dict, *, respect_bass: bool = True) -> ChordRequirements: degrees = generator.harmony.degrees(definition) root = definition['root'] 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 in (9, 11) and d < max(degrees)} 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 and (d not in (1, 5) or degrees[d])} allowed = set(classes.values()) foundation = {value for d, value in classes.items() if d in (1, 5)} if not foundation: raise ValueError('generated guitar chords require a root or fifth in the harmony') bass = definition.get('bass') 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'] if not 0 <= inversion < len(ordered): raise ValueError('harmony inversion exceeds its chord degrees') bass_pc = classes[ordered[inversion]] if not respect_bass: bass_pc = None if bass_pc is not None: allowed.add(bass_pc) required.add(bass_pc) basses = {bass_pc} if bass_pc is not None else set() return ChordRequirements(tuple(sorted(required)), tuple(sorted(allowed)), tuple(sorted(foundation)), tuple(sorted(basses))) def chord_pitches(definition: dict, positions: tuple[TabPosition, ...]) -> list[dict]: spellings = {} 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[(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 - 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