mirror of
https://github.com/kennethreitz/pytheory.git
synced 2026-06-05 23:00:20 +00:00
Compare commits
3 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 5ebf0bdd97 | |||
| 1d897c6609 | |||
| 4113aad5d0 |
@@ -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
@@ -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,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
@@ -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
|
||||
|
||||
|
||||
Reference in New Issue
Block a user