- name
- composing-with-pytheory
- description
- Compose music with PyTheory — chord progressions, melodies, basslines, drum grooves, and full multi-part arrangements written in pure Python and rendered to audio or MIDI. Use whenever the user wants to write, sketch, generate, or arrange music — "write me a bossa nova in G minor", "make a four-chord pop loop", "lay down a funk beat", "turn this progression into a song" — or export the result to WAV, MIDI, MusicXML, LilyPond, ABC, or guitar tab. Also covers the metronome / chord-practice click / tempo trainer (`pytheory metronome`).
- license
- MIT
- allowed-tools
- Write, Read, Bash(python3:*), Bash(uv run:*)
# 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
1. **Write a short Python script** that builds a `Score` and either plays it or
saves it. Don't compose by chaining shell one-liners — a script is clearer,
reproducible, and easy to iterate on.
2. **Run it** with `uv run python song.py` when uv is available, otherwise
`python3 song.py`.
3. 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
```python
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 / SIXTEENTH` plus 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")` returns `Chord`s
(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:
```python
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.
```python
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
```python
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 `Pattern` from `Hit`s and pass it to `drums()`:
```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, position_in_beats, 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=True`** splits a kit into separate `kick`/`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:
```python
Voir sur GitHub