Install
$ agentstack add skill-kennethreitz-pytheory-skill-composing-with-pytheory ✓ scanned · ✓ verified, works with Claude Code, Cursor, and more.
Security review
✓ PassedNo issues found. Passed automated security review. · v0.1.0 How review works →
- ✓ Prompt-injection patterns
- ✓ Secret / credential exfiltration
- ✓ Dangerous shell & filesystem operations
- ✓ Untrusted network calls
- ✓ Known-malicious package signatures
What it can access
- ✓ Network access No
- ✓ Filesystem access No
- ✓ Shell / process execution No
- ✓ Environment & secrets No
- ✓ Dynamic code execution No
From automated source analysis of v0.1.0. “Used” means the capability is present in the source — more access means more to trust, not that it’s unsafe.
Verified badge
Passed review? Show it. Paste this badge into your README, it links to the public security report.
Reliability & compatibility
Declared compatibility
Compatibility is declared by the source manifest. End-to-end runtime verification is coming, see below.
We're building live execution health for every listing: tool-call success rate, median latency, uptime, and last-checked timestamps, measured, not self-reported. It isn't live yet, so we don't show numbers we can't stand behind.
How agent discovery & health will work →About
Composing with PyTheory
PyTheory turns music theory into runnable Python. You build a Score, add parts (chords, melodies, basslines) and drums, then render it to audio or MIDI — every sound is synthesized from math, so there are no samples, plugins, or DAW to install.
How to work
- Write a short Python script that builds a
Scoreand either plays it or
saves it. Don't compose by chaining shell one-liners — a script is clearer, reproducible, and easy to iterate on.
- Run it with
uv run python song.pywhen uv is available, otherwise
python3 song.py.
- To let the user hear it, call
play_score(score)(speakers) or render to a
WAV they can open. To hand off to a DAW, save MIDI.
If PyTheory isn't installed, prefer uv when available: uv pip install pytheory (or uv add pytheory in a uv project); otherwise pip install pytheory. For live MIDI input add the extra: uv pip install "pytheory[live]" (or pip install "pytheory[live]"). NumPy/SciPy ship as PyTheory dependencies, so the scipy.io.wavfile WAV export below needs no extra install.
Core building blocks
from pytheory import Score, Key, Chord, Duration
score = Score("4/4", bpm=120) # time signature + tempo
# A part is a voice with its own synth/instrument and effects.
piano = score.part("piano", instrument="piano", reverb=0.3)
lead = score.part("lead", synth="saw", envelope="pluck", lowpass=4000)
bass = score.part("bass", synth="triangle", lowpass=900)
# Chords from a Key's progression (Roman numerals) or Chord.from_symbol.
for chord in Key("G", "major").progression("I", "V", "vi", "IV"):
piano.add(chord, Duration.WHOLE)
# Melodies are note names + durations (a float = beats, or Duration.*).
lead.add("D5", 1).add("B4", 0.5).add("D5", 0.5).add("G5", 2)
lead.rest(2)
for n in ["G2", "D2", "E2", "C2"]:
bass.add(n, Duration.WHOLE)
part.add(note_or_chord, duration)chains and returns the part.
part.rest(duration) inserts silence.
Duration.WHOLE / HALF / QUARTER / EIGHTH / SIXTEENTHplus dotted variants
(DOTTED_HALF, DOTTED_QUARTER, …), or pass a float in beats (1, 0.5, 0.25, 0.125).
Key(tonic, mode).progression("I", "V", "vi", "IV")returnsChords
(lowercase numeral = minor, e.g. "i", "VI"). Key(...).chords lists the diatonic chords; Key(...).scale gives the scale tones. Chord.from_symbol("F#m7b5") parses any symbol.
- Note names are scientific pitch (
C4= middle C).Tone.from_string("A1")
builds an explicit pitch; tone.add(12)/tone.add(-12) shift octaves.
Expression & dynamics
add() takes keyword controls that make lines feel human and musical:
part.add("A4", Duration.HALF, velocity=90, bend=-0.5, articulation="accent")
velocity(1–127) — loudness per note. Vary it; flat velocities sound
robotic. Fades are just descending velocities: ``python for v in [100, 85, 70, 55, 40, 25]: part.add("E5", Duration.QUARTER, velocity=v) ``
bend(semitones, float) — pitch bend over the note's duration.bend=2
bends up a whole step, bend=-0.5 slides down a quartertone. Great for sitar meends, guitar bends, theremin. bend_type = "smooth" (default), "linear", or "late".
articulation—"accent","staccato","legato","marcato",
"tenuto", "fermata" (or "").
lyric— a syllable for vocal synths only (synth="vocal_synth"/
"choir_synth"). Passing lyric to a non-vocal synth raises an error.
humanize(part kwarg, ~0.02–0.2) — subtle timing drift per note.
Sound design palette
Each part is either an instrument preset (realistic) or a raw synth waveform (shapeable), plus an envelope and an effects chain.
- Synths (56) —
sine,saw,triangle,square,pulse,fm,noise,
supersaw, pwm_slow/fast, hard_sync, ring_mod, wavefold, drift, and many modeled instruments as *_synth (rhodes_synth, sitar_synth, vocal_synth, mellotron_synth, singing_bowl_ring_synth, …). from pytheory.play import Synth; [s.value for s in Synth] lists them.
- Instruments (83) —
piano,electric_piano,acoustic_guitar,cello,
violin, harp, flute, choir, vocal, sitar, koto, kalimba, timpani, pipe_organ, 808_bass, acid_bass, … from pytheory import INSTRUMENTS; sorted(INSTRUMENTS) lists them.
- Envelopes (10) —
none,piano,organ,pluck,pad,strings,
bowed, bell, mallet, staccato.
Effects are part kwargs (set once at creation, or change later with .set()):
| Group | kwargs | | --- | --- | | Level / space | volume, pan (−1..1), reverb (0..1), reverb_decay, reverb_type | | Time | delay, delay_time (beats, e.g. 0.375 dotted-8th, 0.333 triplet), delay_feedback | | Filter | lowpass, lowpass_q, highpass, highpass_q (Hz / resonance) | | Drive | distortion, distortion_drive, saturation | | Width / pitch | chorus, chorus_rate, chorus_depth, detune (cents, keep 8–15), spread | | Synth body | sub_osc (0..1 sub-oscillator), tremolo_depth, tremolo_rate, legato, glide (portamento secs) | | Mix glue | sidechain (0..0.5 duck by the kick), sidechain_release, humanize |
reverb_type picks a convolution space: "algorithmic" (default), "taj_mahal", "cathedral", "hall", "plate", "spring", "cave", "parking_garage", "canyon".
score.ring_out() appends trailing silence so reverb/delay tails decay naturally instead of being clipped at the last beat (most audible on a final drum hit with a long reverb). Call it once before playing or exporting; the length auto-sizes to the longest effect tail, or pass ring_out(seconds). Skip it for seamless loops, where you want the hard cut.
pad = score.part("pad", synth="supersaw", envelope="pad", reverb=0.5,
reverb_type="cathedral", sidechain=0.2, sub_osc=0.3)
acid = score.part("acid", synth="saw", envelope="pluck", legato=True, glide=0.05,
lowpass=1500, lowpass_q=8, distortion=0.3, distortion_drive=2.0)
Movement: LFOs and mid-track changes
part.lfo(param, rate=, min=, max=, bars=, shape=)sweeps a parameter over
time — the classic filter sweep / auto-wah / tremolo. rate is cycles per bar (0.5 = one sweep every 2 bars, 1 = once per bar). shape = "sine", "triangle", "saw", "square". ``python pad.lfo("lowpass", rate=0.25, min=600, max=5000, bars=8, shape="triangle") ``
part.set(**params)changes a part's settings partway through, for
arrangement dynamics (drop the pad back, open the filter for a chorus): ``python pad.set(volume=0.5, reverb=0.4) ``
Drums
score.drums("hip hop", repeats=4) # by preset name
score.drums("rock", repeats=8, fill="rock", fill_every=4)
score.drums(preset_or_pattern, repeats, fill=, fill_every=, split=, layer=).
Presets include rock, funk, jazz, hip hop, bossa nova, salsa, samba, reggae, waltz, tresillo, tabla solo, … (Pattern.list_presets()).
- Custom beats — build a
PatternfromHits and pass it todrums():
``python from pytheory import Pattern, Hit, DrumSound K, S, CH = DrumSound.KICK, DrumSound.SNARE, DrumSound.CLOSED_HAT beat = Pattern("my beat", [ Hit(K, 0.0), Hit(CH, 0.0), Hit(CH, 0.5), Hit(S, 1.0), Hit(CH, 1.0), Hit(CH, 1.5), Hit(K, 2.0), Hit(CH, 2.0), Hit(K, 2.5), Hit(CH, 2.5), Hit(S, 3.0), Hit(CH, 3.0), Hit(S, 1.75, velocity=35), # ghost ], beats=4.0) score.drums(beat, repeats=4) ` Hit(sound, positioninbeats, velocity=100); 74 DrumSound members (print([d.name for d in DrumSound])) including world kits (TABLA, CAJON, DOUMBEK, DJEMBE`).
- Hand-programmed hits — `part.hit(DrumSound.KICK, Duration.QUARTER,
velocity=110, articulation="accent")` places a hit in any part's stream, with full per-hit control (good for tabla/cajon fills).
- Layering / polyrhythm — repeated
drums()calls play in sequence; pass
layer=True to overlay (clave over a backbeat). For a true polyrhythm, put both voices in one Pattern at fractional positions.
split=Truesplits a kit into separatekick/snare/hats/toms/
cymbals/percussion parts so each takes its own effects (score.parts["snare"].set(reverb=0.3)).
score.set_drum_effects(reverb=, volume=, humanize=, …)applies to the
whole kit at once.
Arrangement & structure
Real songs are built from repetition and dynamics. Useful idioms:
- Octave / voicing spread — layer the same progression across registers:
``python for c in prog: low.add(c.transpose(-12), Duration.WHOLE) high.add(c.transpose(12), Duration.WHOLE) ` Chord.transpose(semitones) and Tone.transpose(semitones)` move pitches.
- Reusable phrases — capture a motif as data and replay it with variation:
``python RIFF = [("A4", 0.5, 90), ("C5", 0.5, 80), ("E5", 1, 100)] def play_riff(part, vshift=0): for note, dur, vel in RIFF: part.add(note, dur, velocity=max(1, min(127, vel + vshift))) ``
- Section boundaries — rest parts in/out to create intros, breakdowns, drops:
``python def rest_bars(part, n): for _ in range(n): part.rest(Duration.WHOLE) ``
score.section(name)/score.repeat(name, times)capture and repeat
spans for structured arrangements.
Alternate tunings
PyTheory isn't limited to 12-tone equal temperament:
score = Score("4/4", bpm=75, system="shruti", temperament="just")
system selects a tuning system (16 available, e.g. "western", "shruti"); temperament ∈ "equal", "just", "meantone", etc. Use for raga, microtonal, and historical-tuning work.
Hearing it & exporting
from pytheory.play import play_score
play_score(score) # play through the speakers
buf = score.render() # float32 (N, 2) buffer, no audio device needed
score.to_wav("song.wav") # render + save a 16-bit stereo WAV
score.save_midi("song.mid") # MIDI (drums on channel 10)
open("song.abc", "w").write(score.to_abc(title="Song", key="G"))
open("song.xml", "w").write(score.to_musicxml(title="Song"))
open("song.ly", "w").write(score.to_lilypond(title="Song", key="G"))
print(score.to_tab("guitar_part")) # ASCII tab for a part
In a headless/CI context (no speakers), prefer score.render() / score.to_wav() over play_score.
Metronome, practice click & tempo trainer
A real-time click that also plays a progression to practise over, and ramps the tempo like the phone trainer apps. It makes sound and blocks, so the user runs the CLI (suggest the ! prefix); from Python it's pytheory.metronome.Metronome.
$ pytheory metronome 120
$ pytheory metronome 90 --chords Am F C G # click + soft chords, cycling per bar
$ pytheory metronome 100 --subdivide 2 # eighth-note clicks
$ pytheory metronome 80 --to 120 --step 5 --every 8 # tempo trainer (start→end BPM)
from pytheory.metronome import Metronome
Metronome(bpm=90, progression=["Am", "F", "C", "G"]).start()
Metronome(bpm=80, end_bpm=120, step=5, every=8).start() # trainer
House style (apply unless the user asks otherwise)
- Detune: subtle, 8–15 cents. Never above ~25 — it smears.
- Humanize: 0.2 for melodic parts; 0.15 for drums
(Score(..., drum_humanize=0.15)).
- No swing unless asked.
- Sine and triangle are underrated — reach for them, not just saw/square.
- Marching music is always 120 BPM.
- Avoid
strings-style synths with detune for solo classical lines — use a
cleaner voice.
- If a mix clips, don't crush the synth peaks — add/rebalance parts instead.
Complete example
from pytheory import Score, Key, Duration
from pytheory.play import play_score
score = Score("4/4", bpm=80, drum_humanize=0.15)
score.drums("hip hop", repeats=4)
piano = score.part("piano", instrument="piano", reverb=0.4, volume=0.4, humanize=0.2)
lead = score.part("lead", synth="sine", envelope="pluck", detune=10, humanize=0.2,
lowpass=2600, delay=0.25, reverb=0.25, volume=0.32)
pad = score.part("pad", synth="supersaw", envelope="pad", reverb=0.5,
reverb_type="cathedral", sidechain=0.2, volume=0.3)
bass = score.part("bass", synth="triangle", lowpass=800, humanize=0.2, volume=0.5)
prog = Key("A", "minor").progression("i", "VI", "III", "VII")
for c in prog:
piano.add(c, Duration.WHOLE)
pad.add(c, Duration.WHOLE, velocity=55)
bass.add(c.transpose(-24), Duration.WHOLE)
lead.add("E5", 1, velocity=90).add("D5", 1).add("C5", 1).rest(1)
lead.add("A4", 1).add("C5", 0.5).add("D5", 0.5).add("E5", 1).rest(1)
pad.lfo("lowpass", rate=0.25, min=700, max=4000, bars=4, shape="triangle")
play_score(score) # or render to WAV (see "Hearing it & exporting")
Tips & gotchas
lyricis vocal-only — onlyvocal_synth/choir_synthaccept it.detuneis in cents, not semitones — 8–15 is a gentle widening.lforate is cycles per bar — for a slow sweep over a whole 8-bar section
use a small rate (e.g. 0.125) with bars=8.
- Effects are part-level, not per-note — to vary an effect over time use
lfo() or set(), and for per-note expression use velocity/bend/ articulation.
score.drums()takes a preset name or aPatternobject.- Durations are in beats; a whole note fills one 4/4 bar.
- Octave matters — keep bass low (
C2) and leads up top (C5). - PyTheory can also identify chords, build fretboard fingerings, and transcribe
recordings (Chord.identify(), Fretboard, Score.from_wav(...)). See https://pytheory.org.
Source & license
This open-source skill is cataloged on AgentStack and links to its original source — we do not rehost the code.
- Author: kennethreitz
- Source: kennethreitz/pytheory-skill
- License: MIT
Install and usage instructions live in the source repository linked above.
Reviews
No reviews yet, be the first.
Write a review
Versions
- v0.1.0 Imported from the upstream source.