Compare commits

...

3 Commits

Author SHA1 Message Date
kennethreitz 5ebf0bdd97 Skip unpitched parts in to_abc(), fix 'pitch is undefined' — v0.41.3
Parts with only drum tones or rests are excluded from ABC output.
Chords correctly recognized as pitched content.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-07 07:47:44 -04:00
kennethreitz 1d897c6609 Auto bass clef detection in to_abc() — v0.41.2
Parts with average note octave below C4 get clef=bass automatically.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-07 07:43:51 -04:00
kennethreitz 4113aad5d0 Fix to_abc() crash on parts with drum tones — v0.41.1
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-07 07:40:07 -04:00
5 changed files with 59 additions and 8 deletions
+15
View File
@@ -2,6 +2,21 @@
All notable changes to PyTheory are documented here.
## 0.41.3
- **Fix** — `to_abc()` now skips parts with only drum tones or rests (no pitched
notes), fixing "pitch is undefined" errors in abcjs. Chords are correctly
recognized as pitched content.
## 0.41.2
- **Auto bass clef** — `to_abc()` detects low-register parts (808, bass, timpani)
and assigns `clef=bass` automatically based on average note octave.
## 0.41.1
- **Fix** — `to_abc()` no longer crashes on parts containing drum tones.
## 0.41.0
- **ABC notation export** — `Score.to_abc()` converts scores to ABC notation
+1 -1
View File
@@ -1,6 +1,6 @@
[project]
name = "pytheory"
version = "0.41.0"
version = "0.41.3"
description = "Music Theory for Humans"
readme = "README.md"
license = "MIT"
+1 -1
View File
@@ -1,6 +1,6 @@
"""PyTheory: Music Theory for Humans."""
__version__ = "0.41.0"
__version__ = "0.41.3"
from .tones import Tone, Interval
from .systems import System, SYSTEMS, TET
+41 -5
View File
@@ -4396,21 +4396,33 @@ class Score:
f"L:1/{default_unit}",
]
# Collect voices: default notes first, then named parts (skip drums)
# Collect voices: default notes first, then named parts
# Skip drum parts and parts with no pitched notes
voices: list[tuple[str, list]] = []
if self.notes:
voices.append(("default", self.notes))
for name, part in self.parts.items():
if part.is_drums:
continue
if part.notes:
voices.append((name, part.notes))
if not part.notes:
continue
# Skip parts that have no pitched tones (only drum tones / rests)
has_pitched = any(
n.tone is not None
and (hasattr(n.tone, "name") or hasattr(n.tone, "tones"))
for n in part.notes
)
if not has_pitched:
continue
voices.append((name, part.notes))
multi = len(voices) > 1
if multi:
for i, (vname, _) in enumerate(voices, 1):
lines.append(f"V:{i} name=\"{vname}\"")
for i, (vname, notes) in enumerate(voices, 1):
clef = self._guess_clef(notes)
clef_str = f" clef={clef}" if clef != "treble" else ""
lines.append(f"V:{i} name=\"{vname}\"{clef_str}")
lines.append(f"K:{key}")
for i, (_, notes) in enumerate(voices, 1):
lines.append(f"V:{i}")
@@ -4436,12 +4448,36 @@ class Score:
+ ");\n</script>\n</body></html>\n"
)
@staticmethod
def _guess_clef(notes):
"""Return 'bass' if most pitched notes are below C4, else 'treble'."""
octaves = []
for note in notes:
tone = note.tone
if tone is None or not hasattr(tone, "octave"):
continue
if hasattr(tone, "tones"):
# Chord — use average of chord tones
for t in tone.tones:
if hasattr(t, "octave") and t.octave is not None:
octaves.append(t.octave)
elif tone.octave is not None:
octaves.append(tone.octave)
if not octaves:
return "treble"
avg = sum(octaves) / len(octaves)
return "bass" if avg < 4 else "treble"
@staticmethod
def _tone_to_abc(tone, default_unit):
"""Convert a single Tone to an ABC note string."""
if tone is None:
return "z"
# Skip drum tones — they don't have pitched names
if not hasattr(tone, "name") or not hasattr(tone, "octave"):
return "z"
name = tone.name # e.g. "C", "C#", "Bb"
octave = tone.octave if tone.octave is not None else 4
Generated
+1 -1
View File
@@ -690,7 +690,7 @@ wheels = [
[[package]]
name = "pytheory"
version = "0.41.0"
version = "0.41.3"
source = { editable = "." }
dependencies = [
{ name = "rich" },