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
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.
- Run it with
uv run python song.py when 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 / 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 Chords
(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: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".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):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
Pattern from Hits and pass it to drums():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:
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:
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:
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
lyric is vocal-only — only vocal_synth / choir_synth accept it.
detune is in cents, not semitones — 8–15 is a gentle widening.
lfo rate 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 a Pattern object.
- 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.
1---2name: composing-with-pytheory3description: 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`).4license: MIT5---67# Composing with PyTheory89PyTheory turns music theory into runnable Python. You build a `Score`, add parts10(chords, melodies, basslines) and drums, then **render it to audio or MIDI** —11every sound is synthesized from math, so there are no samples, plugins, or DAW to12install.1314## How to work15161. **Write a short Python script** that builds a `Score` and either plays it or17 saves it. Don't compose by chaining shell one-liners — a script is clearer,18 reproducible, and easy to iterate on.192. **Run it** with `uv run python song.py` when uv is available, otherwise20 `python3 song.py`.213. To let the user *hear* it, call `play_score(score)` (speakers) or render to a22 WAV they can open. To hand off to a DAW, save MIDI.2324If PyTheory isn't installed, prefer uv when available: `uv pip install pytheory`25(or `uv add pytheory` in a uv project); otherwise `pip install pytheory`. For live26MIDI input add the extra: `uv pip install "pytheory[live]"` (or `pip install27"pytheory[live]"`). NumPy/SciPy ship as PyTheory dependencies, so the28`scipy.io.wavfile` WAV export below needs no extra install.2930## Core building blocks3132```python33from pytheory import Score, Key, Chord, Duration3435score = Score("4/4", bpm=120) # time signature + tempo3637# A part is a voice with its own synth/instrument and effects.38piano = score.part("piano", instrument="piano", reverb=0.3)39lead = score.part("lead", synth="saw", envelope="pluck", lowpass=4000)40bass = score.part("bass", synth="triangle", lowpass=900)4142# Chords from a Key's progression (Roman numerals) or Chord.from_symbol.43for chord in Key("G", "major").progression("I", "V", "vi", "IV"):44 piano.add(chord, Duration.WHOLE)4546# Melodies are note names + durations (a float = beats, or Duration.*).47lead.add("D5", 1).add("B4", 0.5).add("D5", 0.5).add("G5", 2)48lead.rest(2)4950for n in ["G2", "D2", "E2", "C2"]:51 bass.add(n, Duration.WHOLE)52```5354- `part.add(note_or_chord, duration)` chains and returns the part.55 `part.rest(duration)` inserts silence.56- `Duration.WHOLE / HALF / QUARTER / EIGHTH / SIXTEENTH` plus dotted variants57 (`DOTTED_HALF`, `DOTTED_QUARTER`, …), or pass a float in beats (`1`, `0.5`,58 `0.25`, `0.125`).59- `Key(tonic, mode).progression("I", "V", "vi", "IV")` returns `Chord`s60 (lowercase numeral = minor, e.g. `"i"`, `"VI"`). `Key(...).chords` lists the61 diatonic chords; `Key(...).scale` gives the scale tones. `Chord.from_symbol("F#m7b5")`62 parses any symbol.63- Note names are scientific pitch (`C4` = middle C). `Tone.from_string("A1")`64 builds an explicit pitch; `tone.add(12)`/`tone.add(-12)` shift octaves.6566## Expression & dynamics6768`add()` takes keyword controls that make lines feel human and musical:6970```python71part.add("A4", Duration.HALF, velocity=90, bend=-0.5, articulation="accent")72```7374- **`velocity`** (1–127) — loudness per note. Vary it; flat velocities sound75 robotic. Fades are just descending velocities:76 ```python77 for v in [100, 85, 70, 55, 40, 25]:78 part.add("E5", Duration.QUARTER, velocity=v)79 ```80- **`bend`** (semitones, float) — pitch bend over the note's duration. `bend=2`81 bends up a whole step, `bend=-0.5` slides down a quartertone. Great for sitar82 meends, guitar bends, theremin. `bend_type` = `"smooth"` (default), `"linear"`,83 or `"late"`.84- **`articulation`** — `"accent"`, `"staccato"`, `"legato"`, `"marcato"`,85 `"tenuto"`, `"fermata"` (or `""`).86- **`lyric`** — a syllable for vocal synths only (`synth="vocal_synth"` /87 `"choir_synth"`). Passing `lyric` to a non-vocal synth raises an error.88- **`humanize`** (part kwarg, ~0.02–0.2) — subtle timing drift per note.8990## Sound design palette9192Each part is either an **instrument** preset (realistic) or a raw **synth**93waveform (shapeable), plus an **envelope** and an effects chain.9495- **Synths (56)** — `sine`, `saw`, `triangle`, `square`, `pulse`, `fm`, `noise`,96 `supersaw`, `pwm_slow/fast`, `hard_sync`, `ring_mod`, `wavefold`, `drift`, and97 many modeled instruments as `*_synth` (`rhodes_synth`, `sitar_synth`,98 `vocal_synth`, `mellotron_synth`, `singing_bowl_ring_synth`, …).99 `from pytheory.play import Synth; [s.value for s in Synth]` lists them.100- **Instruments (83)** — `piano`, `electric_piano`, `acoustic_guitar`, `cello`,101 `violin`, `harp`, `flute`, `choir`, `vocal`, `sitar`, `koto`, `kalimba`,102 `timpani`, `pipe_organ`, `808_bass`, `acid_bass`, … `from pytheory import103 INSTRUMENTS; sorted(INSTRUMENTS)` lists them.104- **Envelopes (10)** — `none`, `piano`, `organ`, `pluck`, `pad`, `strings`,105 `bowed`, `bell`, `mallet`, `staccato`.106107Effects are part kwargs (set once at creation, or change later with `.set()`):108109| Group | kwargs |110| --- | --- |111| Level / space | `volume`, `pan` (−1..1), `reverb` (0..1), `reverb_decay`, `reverb_type` |112| Time | `delay`, `delay_time` (beats, e.g. `0.375` dotted-8th, `0.333` triplet), `delay_feedback` |113| Filter | `lowpass`, `lowpass_q`, `highpass`, `highpass_q` (Hz / resonance) |114| Drive | `distortion`, `distortion_drive`, `saturation` |115| Width / pitch | `chorus`, `chorus_rate`, `chorus_depth`, `detune` (**cents**, keep 8–15), `spread` |116| Synth body | `sub_osc` (0..1 sub-oscillator), `tremolo_depth`, `tremolo_rate`, `legato`, `glide` (portamento secs) |117| Mix glue | `sidechain` (0..0.5 duck by the kick), `sidechain_release`, `humanize` |118119**`reverb_type`** picks a convolution space: `"algorithmic"` (default),120`"taj_mahal"`, `"cathedral"`, `"hall"`, `"plate"`, `"spring"`, `"cave"`,121`"parking_garage"`, `"canyon"`.122123**`score.ring_out()`** appends trailing silence so reverb/delay tails decay124naturally instead of being clipped at the last beat (most audible on a final125drum hit with a long reverb). Call it once before playing or exporting; the126length auto-sizes to the longest effect tail, or pass `ring_out(seconds)`.127Skip it for seamless loops, where you *want* the hard cut.128129```python130pad = score.part("pad", synth="supersaw", envelope="pad", reverb=0.5,131 reverb_type="cathedral", sidechain=0.2, sub_osc=0.3)132acid = score.part("acid", synth="saw", envelope="pluck", legato=True, glide=0.05,133 lowpass=1500, lowpass_q=8, distortion=0.3, distortion_drive=2.0)134```135136## Movement: LFOs and mid-track changes137138- **`part.lfo(param, rate=, min=, max=, bars=, shape=)`** sweeps a parameter over139 time — the classic filter sweep / auto-wah / tremolo. `rate` is **cycles per140 bar** (`0.5` = one sweep every 2 bars, `1` = once per bar). `shape` =141 `"sine"`, `"triangle"`, `"saw"`, `"square"`.142 ```python143 pad.lfo("lowpass", rate=0.25, min=600, max=5000, bars=8, shape="triangle")144 ```145- **`part.set(**params)`** changes a part's settings partway through, for146 arrangement dynamics (drop the pad back, open the filter for a chorus):147 ```python148 pad.set(volume=0.5, reverb=0.4)149 ```150151## Drums152153```python154score.drums("hip hop", repeats=4) # by preset name155score.drums("rock", repeats=8, fill="rock", fill_every=4)156```157158- `score.drums(preset_or_pattern, repeats, fill=, fill_every=, split=, layer=)`.159 Presets include `rock`, `funk`, `jazz`, `hip hop`, `bossa nova`, `salsa`,160 `samba`, `reggae`, `waltz`, `tresillo`, `tabla solo`, … (`Pattern.list_presets()`).161- **Custom beats** — build a `Pattern` from `Hit`s and pass it to `drums()`:162 ```python163 from pytheory import Pattern, Hit, DrumSound164 K, S, CH = DrumSound.KICK, DrumSound.SNARE, DrumSound.CLOSED_HAT165 beat = Pattern("my beat", [166 Hit(K, 0.0), Hit(CH, 0.0), Hit(CH, 0.5),167 Hit(S, 1.0), Hit(CH, 1.0), Hit(CH, 1.5),168 Hit(K, 2.0), Hit(CH, 2.0), Hit(K, 2.5), Hit(CH, 2.5),169 Hit(S, 3.0), Hit(CH, 3.0), Hit(S, 1.75, velocity=35), # ghost170 ], beats=4.0)171 score.drums(beat, repeats=4)172 ```173 `Hit(sound, position_in_beats, velocity=100)`; 74 `DrumSound` members174 (`print([d.name for d in DrumSound])`) including world kits (`TABLA_*`,175 `CAJON_*`, `DOUMBEK_*`, `DJEMBE_*`).176- **Hand-programmed hits** — `part.hit(DrumSound.KICK, Duration.QUARTER,177 velocity=110, articulation="accent")` places a hit in any part's stream, with178 full per-hit control (good for tabla/cajon fills).179- **Layering / polyrhythm** — repeated `drums()` calls play *in sequence*; pass180 `layer=True` to overlay (clave over a backbeat). For a true polyrhythm, put181 both voices in one `Pattern` at fractional positions.182- **`split=True`** splits a kit into separate `kick`/`snare`/`hats`/`toms`/183 `cymbals`/`percussion` parts so each takes its own effects184 (`score.parts["snare"].set(reverb=0.3)`).185- **`score.set_drum_effects(reverb=, volume=, humanize=, …)`** applies to the186 whole kit at once.187188## Arrangement & structure189190Real songs are built from repetition and dynamics. Useful idioms:191192- **Octave / voicing spread** — layer the same progression across registers:193 ```python194 for c in prog:195 low.add(c.transpose(-12), Duration.WHOLE)196 high.add(c.transpose(12), Duration.WHOLE)197 ```198 `Chord.transpose(semitones)` and `Tone.transpose(semitones)` move pitches.199- **Reusable phrases** — capture a motif as data and replay it with variation:200 ```python201 RIFF = [("A4", 0.5, 90), ("C5", 0.5, 80), ("E5", 1, 100)]202 def play_riff(part, vshift=0):203 for note, dur, vel in RIFF:204 part.add(note, dur, velocity=max(1, min(127, vel + vshift)))205 ```206- **Section boundaries** — rest parts in/out to create intros, breakdowns, drops:207 ```python208 def rest_bars(part, n):209 for _ in range(n):210 part.rest(Duration.WHOLE)211 ```212- **`score.section(name)` / `score.repeat(name, times)`** capture and repeat213 spans for structured arrangements.214215## Alternate tunings216217PyTheory isn't limited to 12-tone equal temperament:218219```python220score = Score("4/4", bpm=75, system="shruti", temperament="just")221```222223`system` selects a tuning system (16 available, e.g. `"western"`, `"shruti"`);224`temperament` ∈ `"equal"`, `"just"`, `"meantone"`, etc. Use for raga, microtonal,225and historical-tuning work.226227## Hearing it & exporting228229```python230from pytheory.play import play_score231232play_score(score) # play through the speakers233234buf = score.render() # float32 (N, 2) buffer, no audio device needed235score.to_wav("song.wav") # render + save a 16-bit stereo WAV236237score.save_midi("song.mid") # MIDI (drums on channel 10)238open("song.abc", "w").write(score.to_abc(title="Song", key="G"))239open("song.xml", "w").write(score.to_musicxml(title="Song"))240open("song.ly", "w").write(score.to_lilypond(title="Song", key="G"))241print(score.to_tab("guitar_part")) # ASCII tab for a part242```243244In a headless/CI context (no speakers), prefer `score.render()` /245`score.to_wav()` over `play_score`.246247## Metronome, practice click & tempo trainer248249A real-time click that also plays a progression to practise over, and ramps the250tempo like the phone trainer apps. It makes sound and blocks, so the **user** runs251the CLI (suggest the `!` prefix); from Python it's `pytheory.metronome.Metronome`.252253```254$ pytheory metronome 120255$ pytheory metronome 90 --chords Am F C G # click + soft chords, cycling per bar256$ pytheory metronome 100 --subdivide 2 # eighth-note clicks257$ pytheory metronome 80 --to 120 --step 5 --every 8 # tempo trainer (start→end BPM)258```259260```python261from pytheory.metronome import Metronome262Metronome(bpm=90, progression=["Am", "F", "C", "G"]).start()263Metronome(bpm=80, end_bpm=120, step=5, every=8).start() # trainer264```265266## House style (apply unless the user asks otherwise)267268- **Detune**: subtle, **8–15 cents**. Never above ~25 — it smears.269- **Humanize**: **0.2** for melodic parts; **0.15** for drums270 (`Score(..., drum_humanize=0.15)`).271- **No swing** unless asked.272- **Sine and triangle are underrated** — reach for them, not just saw/square.273- **Marching music is always 120 BPM.**274- Avoid `strings`-style synths *with detune* for solo classical lines — use a275 cleaner voice.276- If a mix clips, don't crush the synth peaks — add/rebalance parts instead.277278## Complete example279280```python281from pytheory import Score, Key, Duration282from pytheory.play import play_score283284score = Score("4/4", bpm=80, drum_humanize=0.15)285score.drums("hip hop", repeats=4)286287piano = score.part("piano", instrument="piano", reverb=0.4, volume=0.4, humanize=0.2)288lead = score.part("lead", synth="sine", envelope="pluck", detune=10, humanize=0.2,289 lowpass=2600, delay=0.25, reverb=0.25, volume=0.32)290pad = score.part("pad", synth="supersaw", envelope="pad", reverb=0.5,291 reverb_type="cathedral", sidechain=0.2, volume=0.3)292bass = score.part("bass", synth="triangle", lowpass=800, humanize=0.2, volume=0.5)293294prog = Key("A", "minor").progression("i", "VI", "III", "VII")295for c in prog:296 piano.add(c, Duration.WHOLE)297 pad.add(c, Duration.WHOLE, velocity=55)298 bass.add(c.transpose(-24), Duration.WHOLE)299300lead.add("E5", 1, velocity=90).add("D5", 1).add("C5", 1).rest(1)301lead.add("A4", 1).add("C5", 0.5).add("D5", 0.5).add("E5", 1).rest(1)302pad.lfo("lowpass", rate=0.25, min=700, max=4000, bars=4, shape="triangle")303304play_score(score) # or render to WAV (see "Hearing it & exporting")305```306307## Tips & gotchas308309- **`lyric` is vocal-only** — only `vocal_synth` / `choir_synth` accept it.310- **`detune` is in cents**, not semitones — 8–15 is a gentle widening.311- **`lfo` rate is cycles per bar** — for a slow sweep over a whole 8-bar section312 use a small `rate` (e.g. `0.125`) with `bars=8`.313- **Effects are part-level**, not per-note — to vary an effect over time use314 `lfo()` or `set()`, and for per-note expression use `velocity`/`bend`/315 `articulation`.316- `score.drums()` takes a preset **name** or a `Pattern` object.317- Durations are in beats; a whole note fills one 4/4 bar.318- Octave matters — keep bass low (`C2`) and leads up top (`C5`).319- PyTheory can also *identify* chords, build fretboard fingerings, and transcribe320 recordings (`Chord.identify()`, `Fretboard`, `Score.from_wav(...)`). See321 https://pytheory.org.