mirror of
https://github.com/kennethreitz/pytheory.git
synced 2026-06-05 23:00:20 +00:00
Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 72aa097552 | |||
| 5ebf0bdd97 |
@@ -2,6 +2,18 @@
|
||||
|
||||
All notable changes to PyTheory are documented here.
|
||||
|
||||
## 0.41.4
|
||||
|
||||
- **Fix** — `to_abc()` now ties long notes across barlines instead of emitting
|
||||
oversized durations that abcjs can't render (e.g. 16-beat notes become four
|
||||
tied whole notes).
|
||||
|
||||
## 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)
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
[project]
|
||||
name = "pytheory"
|
||||
version = "0.41.2"
|
||||
version = "0.41.4"
|
||||
description = "Music Theory for Humans"
|
||||
readme = "README.md"
|
||||
license = "MIT"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
"""PyTheory: Music Theory for Humans."""
|
||||
|
||||
__version__ = "0.41.2"
|
||||
__version__ = "0.41.4"
|
||||
|
||||
from .tones import Tone, Interval
|
||||
from .systems import System, SYSTEMS, TET
|
||||
|
||||
+56
-34
@@ -4396,15 +4396,25 @@ 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
|
||||
|
||||
@@ -4489,6 +4499,24 @@ class Score:
|
||||
|
||||
return f"{abc_acc}{note_char}{oct_str}"
|
||||
|
||||
@staticmethod
|
||||
def _format_dur(multiplier):
|
||||
"""Format an ABC duration multiplier string."""
|
||||
if abs(multiplier - 1) < 0.001:
|
||||
return ""
|
||||
elif abs(multiplier - int(multiplier)) < 0.001:
|
||||
return str(int(multiplier))
|
||||
elif abs(multiplier - 0.5) < 0.001:
|
||||
return "/2"
|
||||
elif abs(multiplier - 0.25) < 0.001:
|
||||
return "/4"
|
||||
elif abs(multiplier - 1.5) < 0.001:
|
||||
return "3/2"
|
||||
else:
|
||||
from fractions import Fraction
|
||||
frac = Fraction(multiplier).limit_denominator(16)
|
||||
return f"{frac.numerator}/{frac.denominator}"
|
||||
|
||||
def _notes_to_abc(self, notes, default_unit, ts,
|
||||
bars_per_line=4):
|
||||
"""Convert a list of Note objects to an ABC body string."""
|
||||
@@ -4498,17 +4526,12 @@ class Score:
|
||||
measure_count = 0
|
||||
|
||||
for note in notes:
|
||||
beats = note.duration.value
|
||||
|
||||
# ABC length multiplier relative to L:1/default_unit
|
||||
# L:1/8 means 1 unit = 0.5 beats (an eighth note)
|
||||
total_beats = note.duration.value
|
||||
unit_beats = 4.0 / default_unit # beats per L unit
|
||||
multiplier = beats / unit_beats
|
||||
|
||||
if note.tone is None:
|
||||
abc_note = "z"
|
||||
elif hasattr(note.tone, "tones"):
|
||||
# Chord: [CEG]
|
||||
chord_notes = [
|
||||
self._tone_to_abc(t, default_unit)
|
||||
for t in note.tone.tones
|
||||
@@ -4517,33 +4540,32 @@ class Score:
|
||||
else:
|
||||
abc_note = self._tone_to_abc(note.tone, default_unit)
|
||||
|
||||
# Format duration multiplier
|
||||
if multiplier == 1:
|
||||
dur_str = ""
|
||||
elif multiplier == int(multiplier):
|
||||
dur_str = str(int(multiplier))
|
||||
elif multiplier == 0.5:
|
||||
dur_str = "/2"
|
||||
elif multiplier == 0.25:
|
||||
dur_str = "/4"
|
||||
elif multiplier == 1.5:
|
||||
dur_str = "3/2"
|
||||
else:
|
||||
# General fraction
|
||||
from fractions import Fraction
|
||||
frac = Fraction(multiplier).limit_denominator(16)
|
||||
dur_str = f"{frac.numerator}/{frac.denominator}"
|
||||
# Split notes longer than one measure into tied pieces
|
||||
remaining = total_beats
|
||||
first_chunk = True
|
||||
while remaining > 0.001:
|
||||
# How much room left in this measure?
|
||||
room = beats_per_measure - beat_in_measure
|
||||
chunk = min(remaining, room) if remaining > room + 0.001 else remaining
|
||||
needs_tie = remaining - chunk > 0.001
|
||||
|
||||
tokens.append(f"{abc_note}{dur_str}")
|
||||
multiplier = chunk / unit_beats
|
||||
dur_str = self._format_dur(multiplier)
|
||||
|
||||
beat_in_measure += beats
|
||||
if beat_in_measure >= beats_per_measure - 0.001:
|
||||
measure_count += 1
|
||||
if measure_count % bars_per_line == 0:
|
||||
tokens.append("|\n")
|
||||
else:
|
||||
tokens.append("|")
|
||||
beat_in_measure -= beats_per_measure
|
||||
tie_str = "-" if needs_tie and abc_note != "z" else ""
|
||||
tokens.append(f"{abc_note}{dur_str}{tie_str}")
|
||||
|
||||
remaining -= chunk
|
||||
beat_in_measure += chunk
|
||||
first_chunk = False
|
||||
|
||||
if beat_in_measure >= beats_per_measure - 0.001:
|
||||
measure_count += 1
|
||||
if measure_count % bars_per_line == 0:
|
||||
tokens.append("|\n")
|
||||
else:
|
||||
tokens.append("|")
|
||||
beat_in_measure -= beats_per_measure
|
||||
|
||||
body = " ".join(tokens)
|
||||
# Clean up trailing/double barlines
|
||||
|
||||
Reference in New Issue
Block a user