mirror of
https://github.com/kennethreitz/pytheory.git
synced 2026-06-05 23:00:20 +00:00
Compare commits
4 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 4113aad5d0 | |||
| 6ecef688e1 | |||
| fcc5db8e3d | |||
| 9de113b6e7 |
@@ -2,6 +2,17 @@
|
||||
|
||||
All notable changes to PyTheory are documented here.
|
||||
|
||||
## 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
|
||||
strings. Supports multi-voice scores (via `V:` directives), chords, rests,
|
||||
accidentals, and all standard durations. Pass `html=True` to get a
|
||||
self-contained HTML page that renders sheet music in the browser via abcjs.
|
||||
|
||||
## 0.40.9
|
||||
|
||||
- **Mellotron synth** — tape-replay keyboard with wow/flutter, tape saturation,
|
||||
|
||||
Vendored
BIN
Binary file not shown.
BIN
Binary file not shown.
Vendored
BIN
Binary file not shown.
Vendored
BIN
Binary file not shown.
Vendored
BIN
Binary file not shown.
BIN
Binary file not shown.
Vendored
BIN
Binary file not shown.
Vendored
BIN
Binary file not shown.
Vendored
BIN
Binary file not shown.
Vendored
BIN
Binary file not shown.
Vendored
BIN
Binary file not shown.
Vendored
BIN
Binary file not shown.
Vendored
BIN
Binary file not shown.
Vendored
BIN
Binary file not shown.
Vendored
BIN
Binary file not shown.
Vendored
BIN
Binary file not shown.
Vendored
BIN
Binary file not shown.
Vendored
BIN
Binary file not shown.
Vendored
BIN
Binary file not shown.
Vendored
BIN
Binary file not shown.
Vendored
BIN
Binary file not shown.
Vendored
BIN
Binary file not shown.
BIN
Binary file not shown.
Vendored
BIN
Binary file not shown.
Vendored
BIN
Binary file not shown.
Vendored
BIN
Binary file not shown.
Vendored
BIN
Binary file not shown.
Vendored
BIN
Binary file not shown.
Vendored
BIN
Binary file not shown.
Vendored
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
Vendored
BIN
Binary file not shown.
Vendored
BIN
Binary file not shown.
BIN
Binary file not shown.
Vendored
BIN
Binary file not shown.
Vendored
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
Vendored
BIN
Binary file not shown.
BIN
Binary file not shown.
Vendored
BIN
Binary file not shown.
Vendored
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
Vendored
BIN
Binary file not shown.
BIN
Binary file not shown.
Vendored
BIN
Binary file not shown.
Vendored
BIN
Binary file not shown.
BIN
Binary file not shown.
Vendored
BIN
Binary file not shown.
BIN
Binary file not shown.
Vendored
BIN
Binary file not shown.
Vendored
BIN
Binary file not shown.
Vendored
BIN
Binary file not shown.
BIN
Binary file not shown.
Vendored
BIN
Binary file not shown.
BIN
Binary file not shown.
Vendored
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
Vendored
BIN
Binary file not shown.
BIN
Binary file not shown.
Vendored
BIN
Binary file not shown.
BIN
Binary file not shown.
Vendored
BIN
Binary file not shown.
BIN
Binary file not shown.
Binary file not shown.
Vendored
BIN
Binary file not shown.
Vendored
BIN
Binary file not shown.
BIN
Binary file not shown.
Vendored
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
Vendored
BIN
Binary file not shown.
BIN
Binary file not shown.
Vendored
BIN
Binary file not shown.
Vendored
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
Vendored
BIN
Binary file not shown.
BIN
Binary file not shown.
Vendored
BIN
Binary file not shown.
Vendored
BIN
Binary file not shown.
@@ -850,6 +850,56 @@ def gen_synth_ukulele():
|
||||
p.strum(ch, Duration.WHOLE, velocity=72)
|
||||
render("synth_ukulele", score)
|
||||
|
||||
def gen_synth_hard_sync():
|
||||
score = Score("4/4", bpm=120)
|
||||
p = score.part("demo", instrument="sync_lead_bright", volume=0.5)
|
||||
for n in ["C4", "E4", "G4", "C5", "G4", "E4", "C4", "E4"]:
|
||||
p.add(n, Duration.QUARTER, velocity=90)
|
||||
render("synth_hard_sync", score)
|
||||
|
||||
|
||||
def gen_synth_ring_mod():
|
||||
score = Score("4/4", bpm=90)
|
||||
p = score.part("demo", instrument="ring_mod_bell", volume=0.5)
|
||||
for n in ["C5", "E5", "G5", "C6", "G5", "E5", "C5", "E5"]:
|
||||
p.add(n, Duration.QUARTER, velocity=80)
|
||||
render("synth_ring_mod", score)
|
||||
|
||||
|
||||
def gen_synth_wavefold():
|
||||
score = Score("4/4", bpm=110)
|
||||
p = score.part("demo", instrument="wavefold_warm", volume=0.5)
|
||||
for n in ["C4", "E4", "G4", "C5", "G4", "E4", "C4", "E4"]:
|
||||
p.add(n, Duration.QUARTER, velocity=85)
|
||||
render("synth_wavefold", score)
|
||||
|
||||
|
||||
def gen_synth_drift():
|
||||
score = Score("4/4", bpm=90)
|
||||
p = score.part("demo", instrument="drift_saw", volume=0.5, reverb=0.35,
|
||||
reverb_type="taj_mahal")
|
||||
for n in ["C4", "E4", "G4", "C5", "G4", "E4", "C4", "E4"]:
|
||||
p.add(n, Duration.HALF, velocity=75)
|
||||
render("synth_drift", score)
|
||||
|
||||
|
||||
def gen_synth_karplus():
|
||||
score = Score("4/4", bpm=100)
|
||||
p = score.part("demo", synth="pluck_synth", envelope="none",
|
||||
volume=0.5, reverb=0.2)
|
||||
for n in ["C4", "E4", "G4", "C5", "G4", "E4", "C4", "E4"]:
|
||||
p.add(n, Duration.QUARTER, velocity=85)
|
||||
render("synth_karplus", score)
|
||||
|
||||
|
||||
def gen_synth_mellotron():
|
||||
score = Score("4/4", bpm=80)
|
||||
p = score.part("demo", instrument="mellotron_flute", volume=0.5)
|
||||
for n in ["C4", "E4", "G4", "C5"]:
|
||||
p.add(n, Duration.WHOLE, velocity=75)
|
||||
render("synth_mellotron", score)
|
||||
|
||||
|
||||
def gen_synth_granular():
|
||||
score = Score("4/4", bpm=80)
|
||||
p = score.part("demo", instrument="granular_pad", volume=0.5, reverb=0.4)
|
||||
@@ -1139,6 +1189,12 @@ GENERATORS = [
|
||||
gen_synth_banjo,
|
||||
gen_synth_mandolin,
|
||||
gen_synth_ukulele,
|
||||
gen_synth_hard_sync,
|
||||
gen_synth_ring_mod,
|
||||
gen_synth_wavefold,
|
||||
gen_synth_drift,
|
||||
gen_synth_karplus,
|
||||
gen_synth_mellotron,
|
||||
gen_synth_granular,
|
||||
gen_synth_crotales,
|
||||
gen_synth_tingsha,
|
||||
|
||||
+25
-1
@@ -277,6 +277,10 @@ of the Prophet-5, Moog Prodigy, and every screaming analog lead since
|
||||
from pytheory import play, Synth, Tone
|
||||
play(Tone.from_string("C4"), synth=Synth.HARD_SYNC, slave_ratio=2.5)
|
||||
|
||||
.. raw:: html
|
||||
|
||||
<audio controls style="width:100%;margin:0.3em 0 0.5em"><source src="../_static/audio/synth_hard_sync.wav" type="audio/wav"></audio>
|
||||
|
||||
Ring Modulation
|
||||
~~~~~~~~~~~~~~~
|
||||
|
||||
@@ -296,6 +300,10 @@ soundtrack.
|
||||
# Non-integer ratios = more inharmonic
|
||||
play(Tone.from_string("C4"), synth=Synth.RING_MOD, mod_ratio=2.1)
|
||||
|
||||
.. raw:: html
|
||||
|
||||
<audio controls style="width:100%;margin:0.3em 0 0.5em"><source src="../_static/audio/synth_ring_mod.wav" type="audio/wav"></audio>
|
||||
|
||||
Wavefolding
|
||||
~~~~~~~~~~~
|
||||
|
||||
@@ -322,6 +330,10 @@ the wave. Pairs beautifully with a lowpass filter after the fold.
|
||||
# Direct control over fold amount
|
||||
play(Tone.from_string("C4"), synth=Synth.WAVEFOLD, folds=3.0)
|
||||
|
||||
.. raw:: html
|
||||
|
||||
<audio controls style="width:100%;margin:0.3em 0 0.5em"><source src="../_static/audio/synth_wavefold.wav" type="audio/wav"></audio>
|
||||
|
||||
Drift Oscillator
|
||||
~~~~~~~~~~~~~~~~
|
||||
|
||||
@@ -351,6 +363,10 @@ that needs to feel "alive."
|
||||
play(Tone.from_string("C4"), synth=Synth.DRIFT,
|
||||
shape="triangle", drift_amount=0.25)
|
||||
|
||||
.. raw:: html
|
||||
|
||||
<audio controls style="width:100%;margin:0.3em 0 0.5em"><source src="../_static/audio/synth_drift.wav" type="audio/wav"></audio>
|
||||
|
||||
Drift amount controls how unstable the oscillator is:
|
||||
|
||||
- **0.05** = studio-grade (Sequential, Oberheim)
|
||||
@@ -514,6 +530,10 @@ It sounds genuinely like a real guitar, harp, or koto.
|
||||
guitar = score.part("guitar", synth="pluck_synth")
|
||||
harp = score.part("harp", instrument="harp") # uses pluck_synth
|
||||
|
||||
.. raw:: html
|
||||
|
||||
<audio controls style="width:100%;margin:0.3em 0 0.5em"><source src="../_static/audio/synth_karplus.wav" type="audio/wav"></audio>
|
||||
|
||||
Hammond Organ
|
||||
~~~~~~~~~~~~~
|
||||
|
||||
@@ -633,7 +653,11 @@ Three tape banks are available via the ``tape`` parameter:
|
||||
|
||||
# Or select the tape directly
|
||||
from pytheory import play, Synth, Tone
|
||||
play(Tone.from_string("C4"), synth=Synth.MELLOTRON, tape="choir", t=3000)
|
||||
play(Tone.from_string("C4"), synth=Synth.MELLOTRON, tape="flute", t=3000)
|
||||
|
||||
.. raw:: html
|
||||
|
||||
<audio controls style="width:100%;margin:0.3em 0 0.5em"><source src="../_static/audio/synth_mellotron.wav" type="audio/wav"></audio>
|
||||
|
||||
Vibraphone Synth
|
||||
~~~~~~~~~~~~~~~~
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
[project]
|
||||
name = "pytheory"
|
||||
version = "0.40.9"
|
||||
version = "0.41.1"
|
||||
description = "Music Theory for Humans"
|
||||
readme = "README.md"
|
||||
license = "MIT"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
"""PyTheory: Music Theory for Humans."""
|
||||
|
||||
__version__ = "0.40.9"
|
||||
__version__ = "0.41.1"
|
||||
|
||||
from .tones import Tone, Interval
|
||||
from .systems import System, SYSTEMS, TET
|
||||
|
||||
@@ -4369,6 +4369,167 @@ class Score:
|
||||
f"{part_info} {self.measures:.1f} measures>"
|
||||
)
|
||||
|
||||
# ── ABC notation export ────────────────────────────────────────────
|
||||
|
||||
def to_abc(self, *, title="Untitled", key="C", html=False):
|
||||
"""Export the score as ABC notation.
|
||||
|
||||
Args:
|
||||
title: Tune title for the ``T:`` field.
|
||||
key: Key signature (e.g. ``"C"``, ``"Gm"``, ``"D"``) for the
|
||||
``K:`` field.
|
||||
html: If *True*, wrap the ABC string in a self-contained HTML
|
||||
page that renders sheet music via abcjs.
|
||||
|
||||
Returns:
|
||||
An ABC notation string, or a full HTML document string when
|
||||
*html* is True.
|
||||
"""
|
||||
ts = self.time_signature
|
||||
default_unit = 8 # L:1/8
|
||||
|
||||
lines = [
|
||||
"X:1",
|
||||
f"T:{title}",
|
||||
f"M:{ts.beats}/{ts.unit}",
|
||||
f"Q:1/4={self.bpm}",
|
||||
f"L:1/{default_unit}",
|
||||
]
|
||||
|
||||
# Collect voices: default notes first, then named parts (skip drums)
|
||||
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))
|
||||
|
||||
multi = len(voices) > 1
|
||||
|
||||
if multi:
|
||||
for i, (vname, _) in enumerate(voices, 1):
|
||||
lines.append(f"V:{i} name=\"{vname}\"")
|
||||
lines.append(f"K:{key}")
|
||||
for i, (_, notes) in enumerate(voices, 1):
|
||||
lines.append(f"V:{i}")
|
||||
lines.append(self._notes_to_abc(notes, default_unit, ts))
|
||||
else:
|
||||
lines.append(f"K:{key}")
|
||||
if voices:
|
||||
lines.append(self._notes_to_abc(voices[0][1], default_unit, ts))
|
||||
|
||||
abc = "\n".join(lines) + "\n"
|
||||
|
||||
if not html:
|
||||
return abc
|
||||
|
||||
return (
|
||||
"<!DOCTYPE html>\n<html><head><meta charset=\"utf-8\">\n"
|
||||
"<title>" + title + "</title>\n"
|
||||
"<script src=\"https://cdn.jsdelivr.net/npm/abcjs@6/dist"
|
||||
"/abcjs-basic-min.js\"></script>\n"
|
||||
"</head><body>\n<div id=\"score\"></div>\n<script>\n"
|
||||
"ABCJS.renderAbc(\"score\", "
|
||||
+ repr(abc)
|
||||
+ ");\n</script>\n</body></html>\n"
|
||||
)
|
||||
|
||||
@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
|
||||
|
||||
# ABC accidentals: ^ = sharp, _ = flat, ^^ = double sharp, __ = double flat
|
||||
letter = name[0].upper()
|
||||
acc = name[1:] if len(name) > 1 else ""
|
||||
abc_acc = acc.replace("##", "^^").replace("#", "^").replace("bb", "__").replace("b", "_")
|
||||
|
||||
# ABC octave: C-B = octave 4, c-b = octave 5,
|
||||
# c' = 6, c'' = 7, C, = 3, C,, = 2
|
||||
if octave >= 5:
|
||||
note_char = letter.lower()
|
||||
ticks = octave - 5
|
||||
oct_str = "'" * ticks
|
||||
else:
|
||||
note_char = letter.upper()
|
||||
commas = 4 - octave
|
||||
oct_str = "," * commas
|
||||
|
||||
return f"{abc_acc}{note_char}{oct_str}"
|
||||
|
||||
def _notes_to_abc(self, notes, default_unit, ts,
|
||||
bars_per_line=4):
|
||||
"""Convert a list of Note objects to an ABC body string."""
|
||||
beats_per_measure = ts.beats_per_measure
|
||||
tokens = []
|
||||
beat_in_measure = 0.0
|
||||
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)
|
||||
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
|
||||
]
|
||||
abc_note = "[" + "".join(chord_notes) + "]"
|
||||
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}"
|
||||
|
||||
tokens.append(f"{abc_note}{dur_str}")
|
||||
|
||||
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
|
||||
|
||||
body = " ".join(tokens)
|
||||
# Clean up trailing/double barlines
|
||||
body = body.replace("| |", "|").rstrip("| \n").rstrip()
|
||||
if not body.endswith("|"):
|
||||
body += " |"
|
||||
return body
|
||||
|
||||
def save_midi(self, path, velocity=100):
|
||||
"""Export to Standard MIDI File, measure-aware."""
|
||||
ticks_per_beat = 480
|
||||
|
||||
Reference in New Issue
Block a user