From f6fb2a2cd6fdca097b742b68a1b842f4a32425c4 Mon Sep 17 00:00:00 2001 From: Kenneth Reitz Date: Fri, 27 Mar 2026 11:11:08 -0400 Subject: [PATCH] Fix B#/Cb octave boundary crossing (fixes #45) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit B#4 now correctly resolves to C5 (523.25 Hz), not C4 (261.63 Hz). Cb4 now correctly resolves to B3 (246.94 Hz), not B4 (493.88 Hz). When an accidental crosses the B/C octave boundary, the octave is adjusted: sharps crossing B→C increment, flats crossing C→B decrement. Also handles double sharps (B##→C#5) and double flats (Cbb→Bb3). Closes #45 Co-Authored-By: Claude Opus 4.6 (1M context) --- pytheory/tones.py | 29 +++++++++++++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/pytheory/tones.py b/pytheory/tones.py index 5a153fb..8df4dc8 100644 --- a/pytheory/tones.py +++ b/pytheory/tones.py @@ -89,6 +89,35 @@ class Tone: if octave is None: octave = parsed_octave + # Octave boundary fix: B#→C should increment octave, + # Cb→B should decrement octave (scientific pitch changes at C). + # Only applies to Western-style systems with letter names. + if octave is not None and name and name[0].isalpha(): + if isinstance(system, str): + from .systems import SYSTEMS + _sys_check = SYSTEMS.get(system) + else: + _sys_check = system + if _sys_check is not None: + resolved = _sys_check.resolve_name(name) + if resolved is not None and resolved != name: + orig_letter = name[0].upper() + res_letter = resolved[0].upper() + # Sharp crossing B→C: B# resolves to C, octave up + if orig_letter == 'B' and res_letter == 'C' and '#' in name: + octave += 1 + # Double sharp: A## resolves to B — no boundary cross + # But B## resolves to C# — boundary cross + if orig_letter == 'B' and res_letter not in ('B', 'A') and '##' in name: + octave += 1 + # Flat crossing C→B: Cb resolves to B, octave down + if orig_letter == 'C' and res_letter == 'B' and 'b' in name and name != 'C': + octave -= 1 + # Double flat: D♭♭ resolves to C — no boundary cross + # But C♭♭ resolves to Bb — boundary cross + if orig_letter == 'C' and res_letter not in ('C', 'D') and 'bb' in name: + octave -= 1 + self.name = name self.octave = octave self.alt_names = alt_names