"""Sounding-register references and hard solo instrument domains.""" from __future__ import annotations import dataclasses from fractions import Fraction as F import generator.fretboard import generator.vocabulary @dataclasses.dataclass(frozen=True) class Instrument: part: str = '' strings: tuple[int, ...] = (1, 2, 3, 4, 5, 6) reference: int | None = None melody_range: tuple[int, int] | None = None @property def register(self): tuning = [p for s, p in generator.fretboard.STANDARD_TUNING if s in self.strings] return min(tuning) + 1, max(tuning) + 12 def permits(self, number): return any(p.string_number in self.strings for p in generator.fretboard.single_positions_for_pitch(F(number))) def cost(self, number): return .5 * min(1., max(0, abs(number - self.reference) - 7) / 12) if self.reference is not None else 0. UNRESTRICTED = Instrument() def weighted_center(notes): values = sorted((n.pitch.number(), n.end - n.start) for n in notes) total = sum((d for _, d in values), F()) elapsed = F() for number, duration in values: elapsed += duration if elapsed >= total / 2: return number return 60 def for_part(part, analysis=None): if part not in ('acoustic', 'electric'): raise ValueError('unknown solo instrument: ' + part) strings = (1, 2, 3, 4, 5, 6) if analysis is None or not analysis.melody: return Instrument(part, strings) # Analysis contains the prepared concert source. Electric melody is written # two octaves up and sounds one down (parts.build_measure), hence +12. # Use the source voice, never the highest added harmonization voice. shift = generator.vocabulary.role(part + '_solo').get('register_offset', 12 if part == 'electric' else 0) notes = tuple(n for n in analysis.melody if n.end > analysis.body_offset) return Instrument(part, strings, weighted_center(notes) + shift, (min(n.pitch.number() for n in notes) + shift, max(n.pitch.number() for n in notes) + shift))