mirror of
https://github.com/kennethreitz/pytheory.git
synced 2026-06-05 06:46:14 +00:00
Support Cb, Fb, E#, B#, double sharps/flats, unicode symbols
- Cb, Fb, E#, B# resolve to their enharmonic equivalents (fixes #40) - C##, Dbb, etc. resolve via semitone arithmetic (fixes #41) - Unicode symbols accepted: ♯ ♭ 𝄪 𝄫 - 'x'/'X' accepted as double sharp (Bach notation): Fx = F## - resolve_name handles all accidentals dynamically Closes #40, closes #41 Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
+44
-1
@@ -25,13 +25,56 @@ class System:
|
||||
return tuple([Tone.from_tuple(tone) for tone in self.tone_names])
|
||||
|
||||
def resolve_name(self, name: str) -> str | None:
|
||||
"""Resolve a note name (including flats) to the canonical name.
|
||||
"""Resolve a note name (including flats, double sharps/flats) to the canonical name.
|
||||
|
||||
Handles enharmonic equivalents:
|
||||
- Standard names and their alternates (e.g. Bb, C#)
|
||||
- Double sharps (C## = D, F## = G)
|
||||
- Double flats (Dbb = C, Ebb = D)
|
||||
|
||||
Returns the primary name if found, or None if not recognized.
|
||||
"""
|
||||
# Direct lookup first
|
||||
for names in self.tone_names:
|
||||
if name in names:
|
||||
return names[0]
|
||||
|
||||
# Handle double sharps (e.g. C## → D, F## → G)
|
||||
if name.endswith('##') and len(name) >= 3:
|
||||
base = name[:-2]
|
||||
base_idx = self._name_to_index(base)
|
||||
if base_idx is not None:
|
||||
resolved_idx = (base_idx + 2) % len(self.tone_names)
|
||||
return self.tone_names[resolved_idx][0]
|
||||
|
||||
# Handle double flats (e.g. Dbb → C, Ebb → D)
|
||||
if name.endswith('bb') and len(name) >= 3 and name[0] != 'b':
|
||||
base = name[:-2]
|
||||
base_idx = self._name_to_index(base)
|
||||
if base_idx is not None:
|
||||
resolved_idx = (base_idx - 2) % len(self.tone_names)
|
||||
return self.tone_names[resolved_idx][0]
|
||||
|
||||
# Handle single sharps/flats on natural notes (e.g. Cb → B, E# → F)
|
||||
if len(name) == 2:
|
||||
base = name[0]
|
||||
modifier = name[1]
|
||||
base_idx = self._name_to_index(base)
|
||||
if base_idx is not None:
|
||||
if modifier == '#':
|
||||
resolved_idx = (base_idx + 1) % len(self.tone_names)
|
||||
return self.tone_names[resolved_idx][0]
|
||||
elif modifier == 'b':
|
||||
resolved_idx = (base_idx - 1) % len(self.tone_names)
|
||||
return self.tone_names[resolved_idx][0]
|
||||
|
||||
return None
|
||||
|
||||
def _name_to_index(self, name: str) -> int | None:
|
||||
"""Return the index of a tone name, or None if not found."""
|
||||
for i, names in enumerate(self.tone_names):
|
||||
if name in names:
|
||||
return i
|
||||
return None
|
||||
|
||||
|
||||
|
||||
+18
-2
@@ -47,6 +47,17 @@ class Tone:
|
||||
alt_names = []
|
||||
|
||||
if isinstance(name, str):
|
||||
# Normalize unicode music symbols to ASCII equivalents
|
||||
name = (name
|
||||
.replace('\u266f', '#') # ♯ → #
|
||||
.replace('\u266d', 'b') # ♭ → b
|
||||
.replace('\U0001d12a', '##') # 𝄪 → ##
|
||||
.replace('\U0001d12b', 'bb') # 𝄫 → bb
|
||||
)
|
||||
# Normalize 'x' / 'X' as double sharp (only after letter name)
|
||||
if len(name) >= 2 and name[1] in ('x', 'X') and name[0].isalpha():
|
||||
name = name[0] + '##' + name[2:]
|
||||
|
||||
try:
|
||||
parsed_octave = int("".join([c for c in filter(str.isdigit, name)]))
|
||||
except ValueError:
|
||||
@@ -442,9 +453,14 @@ class Tone:
|
||||
"""
|
||||
tone_names = system.tone_names[i]
|
||||
if prefer_flats and len(tone_names) > 1:
|
||||
tone = tone_names[1] # flat spelling (e.g. "Bb")
|
||||
# Find the first flat spelling (contains 'b' but isn't just 'B')
|
||||
tone = tone_names[0] # fallback to primary
|
||||
for tn in tone_names[1:]:
|
||||
if 'b' in tn and tn != 'B':
|
||||
tone = tn
|
||||
break
|
||||
else:
|
||||
tone = tone_names[0] # sharp spelling (e.g. "A#")
|
||||
tone = tone_names[0] # primary spelling
|
||||
return klass(name=tone, octave=octave, system=system)
|
||||
|
||||
@property
|
||||
|
||||
Reference in New Issue
Block a user