Compare commits

..

345 Commits

Author SHA1 Message Date
kennethreitz 7a6942c8e4 Add singing bowl synth (strike + ring) — v0.40.1
Two variants modeling Himalayan singing bowl acoustics:
- Strike: mallet hit with chirp from inharmonic partials, long ring
- Ring: rim-rubbed sustained tone with slow build and beating modes

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-30 04:32:16 -04:00
kennethreitz db7fabf985 Fully restore original ge_bend synth — only keep dispatch override
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-30 02:10:59 -04:00
kennethreitz a07b7e7cea Restore original ge_bend synth, keep only the envelope fix
Reverted all frequency/harmonic changes — original thump, metal, sub,
click all restored. Only change from original: envelope uses exp(-0.8*t)
instead of _exp_decay(n_samples, 6) so the sweep sustains long enough
to be audible. Dispatch override for sound_value 108 kept to bypass
stale closure.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-30 02:04:40 -04:00
kennethreitz 7245cd0e51 Fix tabla ge_bend: bypass stale dispatch closure, improved sweep synth
The dispatch dict lambda was holding a stale function reference that
survived module reloads. Added explicit override for sound_value 108
to always call the current _synth_tabla_ge_bend directly.

Synth improvements:
- Sweep: 50→450Hz with slow exp(-1.5t) so ear tracks the bend
- Sustain: exp(-0.8t) envelope gives ~1.2s of audible signal
- Removed static sub/metal that masked the sweep
- Added 2nd harmonic for richness

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-30 02:01:01 -04:00
kennethreitz 9e85a48d0e Fix ge_bend decay — envelope was killing the sweep before it was audible
Replaced _exp_decay (which uses sample-count-based decay and was too fast)
with a direct time-based exponential: exp(-0.8*t) giving ~1.2s of signal.
The sweep from 50→450Hz is now actually hearable.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-30 01:50:49 -04:00
kennethreitz 95b7bd830c Fix tabla ge_bend synth — wider sweep, longer sustain, actually audible
- Sweep range: 50→450Hz (was 60→240Hz)
- Slower sweep rate: exp(-1.5t) so ear can track it (was -4t)
- Longer sustain: decay rate 2.5 (was 6) — bend lives long enough to hear
- Removed static sub and metal that masked the sweep
- Added 2nd harmonic for richness
- Louder body (1.2 gain)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-30 01:48:20 -04:00
kennethreitz 150c57ed3d Remove live extras from pytheory — split to pytheory-live repo
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-30 01:00:30 -04:00
kennethreitz d35d2b12f3 Add pytheory-live CLI entry point
pytheory-live is now a proper command after pip install pytheory[live].
TUI moved to pytheory/live_tui.py, registered as console script.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-29 21:18:58 -04:00
kennethreitz 2084473788 CLI args: --channels, --port, --drums, --buffer
python test_live.py --channels 4 --drums trap --port OP-XY
python test_live.py 4217 -c 16 -d house -b 256

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-29 21:16:29 -04:00
kennethreitz 970c730012 Hold-to-sustain keyboard: ignore repeats, release on key-up timeout
Tracks held keys. OS keyboard repeat is ignored (same key refreshes
timer). Note releases 150ms after last repeat stops — approximates
key-up detection. All notes released on Esc.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-29 20:31:29 -04:00
kennethreitz 5f94e1939b Full keyboard mapping: all letters, numbers, punctuation
Piano-style layout across the full QWERTY keyboard:
- Bottom rows (ZXCVBNM + ASDFGHJKL): lower octave, white+black keys
- Top rows (QWERTYUIOP + 1234567890): upper octave, white+black keys
- Every letter mapped, ~3.5 octaves total

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-29 20:28:20 -04:00
kennethreitz b649b2e659 Extended keyboard mapping: , . / ; ' [ ] and more keys
Lower row extends past M: comma=C5 L=C#5 .=D5 ;=D#5 /=E5 '=F5
Upper row extends past U: I=C6 9=C#6 O=D6 0=D#6 P=E6 [=F6 ]=G6

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-29 20:26:54 -04:00
kennethreitz ed6ba2ab9f Audio stream starts without MIDI — keyboard mode works standalone
MIDI port is now optional. If no device found or port fails,
the audio stream still starts so keyboard mode works. Cleanup
handles missing MIDI gracefully.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-29 20:25:09 -04:00
kennethreitz fd317f9cfd Fix: capture engine output, track stream ready state
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-29 20:23:03 -04:00
kennethreitz c57e29fe28 Debug: check stream active state
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-29 20:21:43 -04:00
kennethreitz 938024bfa2 More debug: vol, level, wavetable peak
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-29 20:19:59 -04:00
kennethreitz acc92f9a60 Debug keyboard: cache check + voice count logging
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-29 20:18:43 -04:00
kennethreitz 0d340dad30 Debug keyboard mode: log key presses
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-29 20:17:12 -04:00
kennethreitz 1762500108 Fix self.self.kbd_active typo
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-29 20:12:18 -04:00
kennethreitz ac2801d07d Keyboard modal, VU meter fix, play_recording.py
- Keyboard mode is now a proper modal: kbd enters, Esc exits
- All keys go to MIDI while in keyboard mode, Up/Down change octave
- Header shows KBD and REC indicators
- VU meters use ASCII-safe characters
- play_recording.py: render MIDI through full engine

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-29 20:06:57 -04:00
kennethreitz c49ec27b1b Next level: stereo pan, VU meters, keyboard MIDI, record, save/load
Live engine:
- Stereo panning per channel (constant power)
- VU meter level tracking per channel
- Computer keyboard as MIDI controller (QWERTY layout)
- Record mode: capture MIDI events with timestamps
- Export recording to MIDI file via pytheory Score
- Save/load channel config to JSON
- All effect params supported (volume, pan, lowpass, reverb,
  chorus, detune, spread, analog, distortion, delay, tremolo,
  saturation, phaser, sub_osc, noise_mix)

TUI:
- Live VU meters in config panel
- REC indicator, keyboard mode indicator
- Commands: kbd, rec, stop, export, save, load, pan, octave
- Tab completion for all new commands

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-29 20:03:24 -04:00
kennethreitz 5f4070c4a7 TUI: tab completion, cursor movement, fx command
- Tab completes commands, instruments, patterns, fx params
- Left/Right arrows move cursor, insert mid-line
- Home/Ctrl-A, End/Ctrl-E for jump to start/end
- fx <ch> <param> <val> for live effect tweaking
- fx alone lists all params, fx <ch> shows current values

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-29 19:59:14 -04:00
kennethreitz 8735393aaa Effects support in live engine + fx command in TUI
_Channel now stores and applies: chorus, detune, distortion,
saturation, tremolo, analog, delay, phaser, sub_osc, noise_mix.
TUI fx command: fx <ch> <param> <val> to tweak any effect live.
fx alone lists all available params. fx <ch> shows current values.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-29 19:51:03 -04:00
kennethreitz 12f15d5138 Fix BPM: average up to 240 ticks, round to integer, update every beat
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-29 19:36:37 -04:00
kennethreitz 20fc5e40b8 More accurate BPM: average over 96 ticks (4 quarter notes)
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-29 19:34:12 -04:00
kennethreitz 91d16595b7 Fix BPM calculation: 24 ticks = 1 quarter note, not per-tick
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-29 19:29:01 -04:00
kennethreitz 54659d39b1 Make python-rtmidi optional (pip install pytheory[live])
Fixes CI failure — rtmidi needs ALSA headers on Linux which
aren't available in the test runner. Now optional: import is
lazy with clear error message if missing.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-29 19:17:15 -04:00
kennethreitz 7cb2c166f9 Address all CodeRabbit review issues
- Channel validation: ch must be int 1-16, raises ValueError
- Port validation: string port raises ValueError if not found
- Exception-safe MIDI: open_port wrapped in try/except, cleanup on failure
- Reverb CC clears cache (was missing)
- stop() uses _stop_event to unblock start()
- _all_notes_off clears drum channel too
- Sorted __slots__
- Fixed en-dash in docstring
- Documented 3-second wavetable limitation
- Unused loop var fixed

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-29 19:06:10 -04:00
kennethreitz ba2038d7ff Pitch bend support in live engine
MIDI pitch bend (0xE0) adjusts playback rate of active voices
via linear interpolation through the wavetable. ±2 semitone range.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-29 18:58:46 -04:00
kennethreitz 198fded20e Enable MIDI clock/transport reception (ignore_types timing=False)
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-29 18:56:27 -04:00
kennethreitz 51159e309a MIDI clock sync, drum patterns, wavetable pre-rendering
- MIDI clock (0xF8) tracking for BPM detection
- Start/Stop/Continue transport handling
- engine.drums("rock") plays pattern synced to MIDI clock
- Pre-render all wavetables (MIDI 36-96) on startup for zero-glitch playback

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-29 18:55:33 -04:00
kennethreitz 54df949089 Handle MIDI start/stop/continue, all-notes-off on stop
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-29 18:53:27 -04:00
kennethreitz 30c70da468 Add reverb to live engine (baked into wavetable)
Simple feedback delay network reverb applied during wavetable
rendering. 3 early reflection taps + 6-pass feedback loop for tail.
Extended wavetable to 3 seconds for reverb room.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-29 18:51:29 -04:00
kennethreitz c633bd6f61 Add CC mapping to live engine
engine.cc(0, "lowpass", min_val=300, max_val=8000) maps any MIDI CC
to any channel parameter. Supports per-channel or global mapping.
Invalidates synth cache when filter params change.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-29 18:46:30 -04:00
kennethreitz bc652c37d0 Live engine: real-time MIDI-to-audio synthesis
LiveEngine listens for MIDI input and synthesizes audio in real-time.
Each MIDI channel maps to a pytheory instrument with its own synth,
envelope, and effects. Supports polyphony, voice stealing, and
GM drum channel (10).

Adds python-rtmidi as a dependency.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-29 18:29:51 -04:00
kennethreitz 417d9a6908 10x faster ensemble: render once + duplicate with time shifts
Ensemble rendering no longer re-synthesizes every note N times.
Renders the part once, then creates N copies with per-player
time shifts and velocity variation (cheap buffer ops).

Benchmarks:
- Heavy (ens=10+effects): 12.7s → 3.0s (4.2x faster)
- Ensemble=20: 4.3s → 0.43s (10x faster)

Also: vectorized strings_wave body_response, synth output cache.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-29 18:27:03 -04:00
kennethreitz f7d8f08446 Add Wurlitzer, vibraphone, pipe organ, choir synths
- Wurlitzer: reed-based, nasal, biting — bark on hard hits
- Vibraphone: aluminum bars with motor tremolo (spinning disc)
- Pipe organ: multi-rank (8'+4'+2'), constant air, wind chiff
- Choir: formant-filtered glottal source, vowel control via lyric=,
  no vibrato (ensemble handles voice variation)
- All four with instrument presets, audio demos, and docs

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-29 18:21:12 -04:00
kennethreitz b8d1fe5e81 v0.40.0: Rhodes synth, 74 audio demos, improved percussion
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-29 17:22:43 -04:00
kennethreitz 2612444146 Louder snares in marching demo (volume 1.5 to compensate ensemble/8)
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-29 16:59:11 -04:00
kennethreitz 48a954d063 Cajón slap: pure wood by default, snare wires optional
CAJON_SLAP is now dry wood crack (no wires). New CAJON_SLAP_SNARE
has the buzzy version for cajóns with snare wires engaged.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-29 16:55:09 -04:00
kennethreitz 8a07be23e6 Cajón bass: fleshy palm impact + thuddy box resonance
Added hand-on-wood transient (lowpassed noise for soft palm feel)
before the box resonance kicks in. Deeper sub, longer air cavity
thump. You hear the hand, then the box.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-29 16:53:00 -04:00
kennethreitz d771117d5c Full drumline marching demo: 8 snares, 4 quads, 5 basses with ensemble
Click count-off → snare groove with quad sweeps and bass splits →
flams and diddles → buzz roll into big unison hit.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-29 16:51:41 -04:00
kennethreitz 1ae9404f07 Add room reverb to cajón demo
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-29 16:50:53 -04:00
kennethreitz 2e5b18de2e Boxy cajón: box resonance modes, plywood character, hollow mids
Bass has 180/320Hz box resonance modes. Slap has wood panel
resonance under the wire buzz. Tap has hollow finger-on-plywood.
Everything sounds like hitting a wooden box now.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-29 16:48:55 -04:00
kennethreitz 248594fb21 Extend metal gallop+triplet section to 4 bars
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-29 16:47:22 -04:00
kennethreitz 3ce890c54c Hall reverb on all tabla demos to match keherwa
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-29 16:46:01 -04:00
kennethreitz 499c49b6eb Metal demo: groove → gallop → blast → double kick with fills
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-29 16:44:53 -04:00
kennethreitz f85504b456 Tabla demos: add bayan pitch bends (ge_bend) to all three examples
Teental ends with bayan fill. Keherwa has ge_bend accents throughout
plus a bend-heavy final bar. Chakradar preceded by bayan showcase.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-29 16:42:04 -04:00
kennethreitz d0624f8b78 Thunderous dhol dagga: deeper sub, longer sustain, more body
Dagga now booms at 30Hz with sustained sub, wider thump band,
and heavier stick impact. The kind of hit you feel in your chest.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-29 16:40:18 -04:00
kennethreitz fb37a7c27b Regenerate all 74 audio samples with choke-free renderer
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-29 16:38:44 -04:00
kennethreitz fb36e75a42 Remove melodic note choke, fix banjo to 16th notes
The melodic renderer was choking all signal at each new note,
killing strum/hold resonance. Removed — notes now naturally
overlap and ring out. Banjo lick now uses 16th notes.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-29 16:35:06 -04:00
kennethreitz 81b54d2394 Mandolin demo: tremolo rolls + melody
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-29 16:31:39 -04:00
kennethreitz fa6a3090cb Banjo demo: strum + bluegrass picking lick
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-29 16:31:22 -04:00
kennethreitz 58286ddb69 Bagpipe demo: drone + chanter melody like Highland pipes
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-29 16:30:44 -04:00
kennethreitz 6d137be9f5 Accordion demo: waltz chords with held bass
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-29 16:29:08 -04:00
kennethreitz 383802a1e1 Timpani: even longer fundamental sustain (0.35/s decay)
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-29 16:27:59 -04:00
kennethreitz 7f2aeb2395 Timpani: long fundamental sustain for resonance buildup in rolls
Fundamental now decays at 0.6/s instead of 1.5/s — rings long
enough that rapid roll hits stack into a singing 'oooh' resonance.
Upper modes still die fast for the initial thump character.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-29 16:27:08 -04:00
kennethreitz 16b4c7d1fa Faster timpani roll: 140bpm + 32nd notes
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-29 16:24:57 -04:00
kennethreitz b9ee5c9cde Timpani demo: crescendo roll + accent hit, matches doc example
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-29 16:23:21 -04:00
kennethreitz 7375d58209 Lower timpani demo to octave 2-3
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-29 16:21:57 -04:00
kennethreitz 6316a6c910 Hybrid harp: KS pluck transient + pure additive sustain
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-29 16:20:49 -04:00
kennethreitz 237cfe171c Rewrite harp synth: pure additive tone instead of Karplus-Strong
Clean harmonics with dominant fundamental, gentle upper partials,
and soft finger pluck transient. Much purer, singing tone.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-29 16:18:50 -04:00
kennethreitz 1751f97617 Update changelog for v0.39.4
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-29 16:08:32 -04:00
kennethreitz 943a12b3bb Add dedicated Rhodes electric piano synth
Tine + tonebar + electromagnetic pickup model with bell-like
harmonics, metallic attack transient, and pickup nonlinearity.
electric_piano instrument preset now uses rhodes_synth instead of FM.
FM section updated to show bells. Audio demos for both.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-29 16:08:10 -04:00
kennethreitz 04d2de3e70 Add pulse, noise, PWM slow, PWM fast audio demos
Every waveform on the synths page now has an audio player.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-29 16:04:13 -04:00
kennethreitz ead42751ef Trim trailing silence from all audio exports
Detects silence below -60dB threshold and trims with 0.2s tail
for natural decay. 69 audio files regenerated.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-29 15:59:34 -04:00
kennethreitz 8dee0d00d8 Raw waveform demos (no envelope), open up bass lowpass to 1200
Basic waveforms now play without envelope shaping so you hear
the raw timbre. Complete Example bass at 1200 instead of 600.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-29 15:57:39 -04:00
kennethreitz de112e0d9f Idiomatic synth demos: strum, hold, drone, ensemble per instrument
Harp: arpeggiated hold. Harpsichord: baroque runs. Guitars: strum
chords. Piano: hold + melody. Cello: bowed ensemble=3. Sitar: drone
under melody. Banjo/uke: strum. Mandolin: tremolo. Strings: ensemble=8.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-29 11:36:30 -04:00
kennethreitz f469ad90f8 Add granular synth audio demo
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-29 11:33:05 -04:00
kennethreitz bab7f39304 Fix bass guitar demo: play in octave 2-3
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-29 11:29:21 -04:00
kennethreitz 62557ba534 Fix upright bass demo: play in octave 2-3 not 4-5
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-29 11:28:50 -04:00
kennethreitz 8b50a9c325 Audio demos for every synth: 34 waveform/instrument players
Every synth section in the docs now has an audio player.
Classic waveforms, FM, supersaw, and all 31 instrument synths
each play a C major arpeggio for easy comparison.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-29 11:27:56 -04:00
kennethreitz 7e9caac70b Update homepage + quickstart to rock example, match code to audio
Both pages now show the same rock beat example in G major with
piano, saw lead, triangle bass. Code and audio are in sync.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-29 11:15:27 -04:00
kennethreitz a0756b3172 v0.39.3: Audio samples in docs, numpy vectorization
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-29 11:11:23 -04:00
kennethreitz e3dd706032 Remove stale sequencing_bossa.wav (replaced by complete_rock.wav)
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-29 11:05:19 -04:00
kennethreitz 9b412906bc Fix acid example, add basic chords audio, regenerate all 34 samples
All audio files: stereo, normalized, no issues.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-29 11:04:51 -04:00
kennethreitz 54e0421997 Fix acid legato example: drop pad envelope, add filter + distortion
The pad envelope has slow attack — wrong for fast acid lines.
Updated both the docs code and the audio generator.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-29 10:57:47 -04:00
kennethreitz 109343ad30 Replace bossa nova with rock in Complete Example, add arpeggio audio
Complete Example now uses rock beat with piano/saw/bass in G major.
Added audio player for the arpeggiator code example.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-29 10:53:06 -04:00
kennethreitz 28e84de566 Add legato/glide audio example to sequencing docs
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-29 10:50:55 -04:00
kennethreitz d353d64298 Add Polyphonic Hold section with audio example to sequencing docs
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-29 10:49:49 -04:00
kennethreitz 7ee02e7ed2 Fix audio samples to stereo WAV
save_wav was writing mono — now properly writes stereo from
render_score's (n_samples, 2) output. All 31 files regenerated.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-29 10:48:51 -04:00
kennethreitz a5c9a46eb2 Add ensemble= to strings, cellos, choir, pads in songs.py
String ensembles: 6-10 players. Cellos: 3 players.
Choir: 6 voices. Cathedral siren pad: 4 voices.
Makes everything sound fuller and more alive.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-29 10:39:51 -04:00
kennethreitz f9c63ec360 Add audio player to homepage, remove save_midi from example
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-29 10:37:18 -04:00
kennethreitz b9e88b77d8 Add audio for all world percussion, metal, cajón sections
28 audio samples total. Tabla (teental, keherwa, chakradar at fast
tempos), dhol, dholak, mridangam, metal blast, cajón. No labels
on stacked players.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-29 06:04:30 -04:00
kennethreitz 1910b09132 Add individual audio samples for all 4 Playing Patterns examples
Rock, bossa nova, salsa, and afrobeat each get their own audio player.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-29 05:55:11 -04:00
kennethreitz 0c5287450b Merge pull request #48 from kennethreitz/docs-audio
Audio samples in documentation
2026-03-29 05:51:38 -04:00
kennethreitz 5ac1873d83 Audio samples for all play_score() examples in docs
20 WAV files covering quickstart, sequencing, drums, playback,
and cookbook examples. Audio players embedded after every code
block that calls play_score().

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-29 05:51:25 -04:00
kennethreitz 9fafca2b08 Add audio samples for documentation
- docs/generate_audio.py renders 12 code examples as WAV files
- Audio players embedded in sequencing and drums docs via raw HTML
- Covers: piano hold, articulations, dynamics, filter ramp, rock,
  bossa nova, djembe, tabla, marching snare, ensemble, strum, swell
- WAV files gitignored — generated at build time

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-29 05:42:34 -04:00
kennethreitz af044f68ca Numpy vectorization: cached helpers, vectorized piano harmonics
- Cache time arrays and exp_decay envelopes (avoid reallocation)
- Cache drum hit renders (same sound at same length = same output)
- Vectorize piano_wave harmonic synthesis: 30 sin() calls in a
  Python loop → one 2D numpy.sin() operation

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-29 05:25:52 -04:00
kennethreitz 60f697f846 Add song #33: Ensemble Showcase
Acid bass (ensemble=3), 16-player strings, tabla with solo and tihai,
8-player snare line, 6-player lead synth. Showcases ensemble, ramp,
LFO, sidechain, articulations, and dynamic curves.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-28 23:07:23 -04:00
kennethreitz 7d678e364e v0.39.2: Marching drumline, ensemble rendering, rudiments
Full marching percussion: snare, quads, pitched bass drums.
Part.flam(), Part.diddle(), Part.cheese() rudiment methods.
Part ensemble= for multi-player rendering with timing tendencies.
Sympathetic snare resonance. Updated docs.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-28 23:01:18 -04:00
kennethreitz 3a8d829010 Quads, pitched bass drums, full drumline cadence
- 5 quad/tenor drum sounds (4 pitched + spock rim)
- 5 pitched marching bass drums (high to low, more beater sound)
- 6 patterns: quad sweep, quad groove, bass split, bass unison, drumline
- Louder snares, more beater on bass drums
- Song #32 rewritten as full drumline cadence with ensemble
  (8 snares, 4 quads, 5 basses)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-28 22:59:08 -04:00
kennethreitz 2a67906937 Update changelog for v0.40.0
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-28 22:49:57 -04:00
kennethreitz b9dcad0454 Marching snare, ensemble, flam/diddle/cheese, resonance buildup
- 3 marching percussion sounds: snare, rimshot, stick click
- 4 marching patterns: march, cadence, paradiddle, roll
- Part.flam(), Part.diddle(), Part.cheese() rudiment methods
- Part ensemble= parameter: duplicate voices with per-player timing
  tendencies and micro pitch drift (works on any Part)
- Sympathetic resonance: marching snare buzz builds up with repeated hits
- Song #32: Snare Cadence (16 bars with triplets, 32nds, flams, cheese)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-28 22:48:37 -04:00
kennethreitz db9726168a Add chakradar tabla pattern, simplify sprunki.py
16-beat chakradar: tihai of tihais with 3 escalating phrases
and crescendo triplet finale landing on sam.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-28 21:22:34 -04:00
kennethreitz 26af923789 Simplify sprunki.py to clean melody reference
Just the notes transcribed from MIDI, no arrangement.
Base for future iterations.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-28 21:18:40 -04:00
kennethreitz 72e18a9bec Add Sprunki Simon Phase 1 arrangement (examples/sprunki.py)
Full arrangement with three-octave square wave lead, drums, bass,
supersaw pads, and a saw solo section. Melody transcribed from MIDI.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-28 18:39:46 -04:00
kennethreitz 7d56ed7a2c Add render_score to __all__, 19 tests for new features
Tests for articulations, dynamic curves, Part.hit(), Part.ramp(),
djembe/cajón/metal patterns and fills. 882 tests total.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-28 18:08:03 -04:00
kennethreitz 6efa4f18ce v0.39.0: Articulations, ramp(), drop numeral, djembe expansion
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-28 17:51:29 -04:00
kennethreitz 06fc4cabb7 Drop numeral dependency, inline Roman numeral helpers
Replace the numeral package with ~30 lines of int2roman()/roman2int()
in _statics.py. Reduces supply chain surface. Fixes #47.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-28 17:48:22 -04:00
kennethreitz d3a93c18b3 Add song #31: Acid Tabla (303 + tabla fusion)
Showcases ramp(), articulations, Part.hit(), filter automation,
and cross-genre fusion. 303 acid bass with tabla entering at the
peak and riding through the outro.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-28 17:26:20 -04:00
kennethreitz 0e10359236 v0.38.2: Part.ramp() for smooth parameter automation
Smoothly sweep any parameter (lowpass, reverb, distortion, etc.)
from current value to target with linear, ease_in, ease_out, or
ease_in_out interpolation curves.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-28 17:12:32 -04:00
kennethreitz df00c3436d Docs: articulations, dynamic curves, Part.hit(), Duration arithmetic
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-28 15:24:21 -04:00
kennethreitz 2f02df15b8 v0.38.1: Dynamic curves (crescendo, decrescendo, swell, dynamics)
Part.crescendo(), Part.decrescendo(), Part.swell(), and Part.dynamics()
for velocity ramps and custom curves across note sequences.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-28 15:23:16 -04:00
kennethreitz a2740b8d57 v0.38.0: Articulations, Part.hit(), djembe expansion, cross-choke
Articulations (staccato, legato, marcato, tenuto, accent, fermata)
on Part.add() and Part.hold(). Part.hit() for placing individual
drum sounds with articulation support. 5 new djembe patterns,
3 djembe fills, cross-choke damping, improved djembe slap.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-28 15:14:21 -04:00
kennethreitz 840bfcc36c v0.37.0: Djembe expansion and cross-choke drum damping
5 new djembe patterns (dununba, tiriba, yankadi, djansa, mendiani),
3 djembe fills, cross-choke damping across drum families, and
improved djembe slap synthesis.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-28 12:32:14 -04:00
kennethreitz 938c1cc132 v0.36.6: Cajón and metal drum fills
Add 6 new drum fills: cajon flam, cajon rumble, cajon breakdown,
metal triplet, metal blast, metal cascade. 27 fills total.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-28 12:25:52 -04:00
kennethreitz 9dc22db4b2 v0.36.5: Duration arithmetic support
Duration enum now supports multiplication, division, and addition
so expressions like `Duration.WHOLE * 2` work instead of raising TypeError.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-28 12:19:28 -04:00
kennethreitz f570e226cd v0.36.4: Harmonium, doumbek, tabla fills, Part.hold()
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-28 12:11:06 -04:00
kennethreitz 0c5c3abedc Harmonium synth, doumbek drums (3 sounds, 4 patterns, 2 fills)
- Harmonium: single free reed, nasal midrange, bellows swell.
  The sound of kirtan and qawwali.
- Doumbek (darbuka): dum (center bass), tek (edge sharp), ka (muted).
  4 patterns: maqsoum, baladi, saidi, ayoub.
  2 fills: doumbek roll, doumbek accent.
- 42 synth waveforms total

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-28 12:05:46 -04:00
kennethreitz 35d07b984b Docs: add tabla fills to drums.rst
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-28 11:57:08 -04:00
kennethreitz aec7723ee6 5 tabla fills: tihai, chakkardar, tiri kita, bayan, tabla call
Tihai (3x crescendo landing on sam), chakkardar (32nd triplet
cascade), tiri kita (rapid 16th dayan burst), bayan (bass bends),
tabla call (dayan/bayan call-and-response).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-28 11:56:19 -04:00
kennethreitz b98a40297b v0.36.3: Part.hold() polyphony, strum fix, 30 songs
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-28 11:46:45 -04:00
kennethreitz 9117568b74 Strum uses hold() — leading string plays simultaneously with chord
No more timing gaps. The leading string is held at 15% velocity
at the same beat position as the full chord via hold(), adding
strum texture without stealing time.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-28 11:42:36 -04:00
kennethreitz 11e4417c62 Part.hold() — polyphonic overlap on a single part
hold() adds a note without advancing the beat position, so the
next note starts at the same time. Enables: piano sustain (bass
rings while melody plays), drone notes under melody, held chords
with moving lines.

Two lines in the renderer: skip beat_pos advance when _hold is set.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-28 11:38:41 -04:00
kennethreitz 4edf1d983d Remove strum grace notes — clean chord hit only
Grace notes created audible gap before chord and sounded like
separate plucks. Pure chord hit sounds better.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-28 11:30:18 -04:00
kennethreitz 74b07b1a8a Song #29: Pop Rock (I-V-vi-IV) — the progression that launched 1000 hits
G-D-Em-C at 120 BPM. Picked intro, strummed verse, electric lead
melody, strings swell, rock drums. The most popular chord progression
in pop history.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-27 23:22:08 -04:00
kennethreitz c9437209a7 Song #28: Descent (generative — different every time)
Random key, tempo, reverb space, instruments, and melodies.
Melodies walk the scale stepwise (not random jumps), arpeggios
follow chord tones in order, piano walks up/down. Tabla solo
always closes with random strokes. No seed — truly unique each play.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-27 23:18:19 -04:00
kennethreitz 92cb855a49 Song #27: Ascent (Deep → Sky → Theremin Solo → Tabla Solo)
Didgeridoo drone throughout, granular abyss, kalimba light,
cello surfacing, piano + quiet uke, pedal steel + theremin solo
(searching → building → soaring peak), strings/flute/harp/timpani
at the peak, 4-part tabla solo finale (whisper → ghosts → 9-tuplet
call-response → 32nd triplet cascade + grand tihai).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-27 23:09:34 -04:00
kennethreitz f06c6f77d1 Comprehensive docs sweep: all 9 guide pages updated
- index.rst: 16 systems, 60+ presets, 41 waveforms, full feature list
- synths.rst: 31 dedicated synths, 60+ presets, complete instrument list
- drums.rst: 51 drum sounds, cajón section, bayan pitch bend
- effects.rst: cabinet/analog_drift in automatable params
- playback.rst: temperament, reference_pitch, KeyboardInterrupt
- systems.rst: 16 systems, full microtonal section (shruti JI,
  maqam Zalzalian, slendro, pelog, thai, makam, carnatic, 19/31-TET,
  Bohlen-Pierce), TET factory, int tone names, System.tone()
- sequencing.rst: Score tuning params documented
- tones.rst: enharmonics (Cb/Fb/E#/B#, double sharps/flats, unicode),
  B#/Cb octave fix, tone validation
- chords.rst: enharmonic support cross-reference

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-27 20:41:08 -04:00
kennethreitz 51bd63658f Docs: update synths.rst — 41 waveforms, all 24 dedicated synths
Added: pedal steel, theremin, kalimba, steel drum, accordion,
didgeridoo, bagpipe, banjo, mandolin, ukulele. Updated counts.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-27 20:31:31 -04:00
kennethreitz 92ade3ee3d v0.36.2: REPL updates, 862 tests, improved songs, Ctrl-C handling
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-27 20:28:49 -04:00
kennethreitz 833867329e REPL: new commands, all instruments, updated autocomplete
New commands: strum, roll, bend, temperament, reference, instruments
Updated autocomplete: 41 synths, 50+ instruments, bowed/mallet
envelopes, all drum patterns (tabla, dhol, djembe, cajón, metal)
Part command supports instrument= keyword
Status shows temperament and reference pitch

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-27 20:27:41 -04:00
kennethreitz 93b9fe9ced 25 new tests: all new synths, vocal, cajón, bends, rolls, int tones
862 tests total. Covers: 11 new synth waveforms, vocal synth with
lyrics, all instrument presets, cajón drums/patterns, pitch bend
rendering (3 types), roll velocity ramp, int tone names + wrapping,
B#/Cb octave fix, note choking, Score system/temperament/ref_pitch,
synth enum count (41).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-27 20:27:06 -04:00
kennethreitz 88a1171bbe Fix Theremin Noir: granular pad → strings pad (less noisy)
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-27 20:20:01 -04:00
kennethreitz 3ca0842b7a Improve songs 1-16, 19: humanize, reverb, velocity dynamics
- humanize=0.2 added to all melodic parts (leads, basses, bells)
- Subtle reverb (0.1-0.2) on bass parts that had none
- Per-note velocity dynamics on all leads (was static)
- Blues lead changed from trumpet to saxophone (more fitting)
- Songs 17-18, 20-26 left untouched (already well-crafted)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-27 20:19:22 -04:00
kennethreitz 00de5eb354 Catch KeyboardInterrupt in all playback functions
play(), play_score(), _play_for() now catch Ctrl-C and stop
cleanly instead of crashing with a traceback. CLI demo also
wrapped.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-27 20:18:43 -04:00
kennethreitz d2b0c6f329 v0.36.1: 7 new synths, 9 new demo moods
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-27 20:15:22 -04:00
kennethreitz 76612682f1 9 new demo moods: theremin noir, caribbean, accordion waltz, kalimba
dreams, outback drone, highland, nashville tears, tabla fusion

All new synths represented in pytheory demo random rotation.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-27 20:13:11 -04:00
kennethreitz ce480858e9 7 new synths: pedal steel, theremin, kalimba, steel drum, accordion, didgeridoo, bagpipe
- Pedal steel: singing harmonics, slow vibrato, spring reverb
- Theremin: pure sine with hand wobble, legato+glide preset
- Kalimba: inharmonic metal tine modes, wooden body, bell-like
- Steel drum: hammered metal partials, bright Caribbean ring
- Accordion: musette-tuned doubled reeds, bellows pressure swell
- Didgeridoo: deep cylindrical drone, shifting formant overtones
- Bagpipe: bright chanter reed, constant bag pressure
- 41 synth waveforms total

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-27 20:11:32 -04:00
kennethreitz 70efb0ad40 v0.36.0: Banjo, mandolin, ukulele, cajón, vocal synth, granular
34 synth waveforms, 26 songs, vocal/formant synthesis with choir
preset, granular engine, banjo/mandolin/ukulele physical models,
cajón drum with 3 patterns, strum sweep on fretboard instruments.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-27 19:54:08 -04:00
kennethreitz bf6deaab64 Mandolin synth, cajón drums, Song #26 Acoustic Ensemble
- Mandolin: paired steel strings (natural chorus from doubled
  courses), bright body resonance (500/1000/2000Hz)
- Cajón: bass (woody box thump), slap (snare wire buzz), tap
  (ghost note). 3 patterns: cajon, cajon rumba, cajon folk
- Song #26: guitar + uke + mandolin + cajón — humanized strumming,
  stereo panned, plate reverb
- Mandola preset (mandolin with lowpass for darker tone)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-27 17:46:15 -04:00
kennethreitz 7c792c0a2a Ukulele synth + strum sweep on all fretboard instruments
- Ukulele: nylon string KS with small body resonance (350/700/1200Hz),
  faster decay than guitar, mid-heavy character
- Strum sweep: 2 quiet grace notes (25% vel) before the chord hit,
  gives audible strum feel without choppiness
- Default strum_time 0.08 → 0.05 for tighter feel

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-27 17:36:29 -04:00
kennethreitz bf8d4b9a77 Epic Bhairav: musical polyrhythm section, fix reverb levels
Polyrhythm section uses musical phrases (ti-ra-ki-ta patterns)
in 5-groups, 7-groups, and 9-groups rather than mechanical grid
overlays. Reverb pulled back to 0.4 across the song.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-27 17:12:31 -04:00
kennethreitz d2d5115c8a Song #25: Epic Bhairav + vocal synth merged to master
Orchestral piece in 22-shruti JI with choir vowel pads, timpani
rolls, bansuri, cello, sitar, strings, harp, djembe→tabla→extended
tabla solo finale (whisper→ghosts→call/response→9-tuplets→32nd
triplet cascades→grand tihai→slam).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-27 12:25:05 -04:00
kennethreitz 3cdd98b158 Merge vocal/formant synth: LF glottal model, 5 formants, choir
Formant synthesis with LF glottal pulse, 5 Peterson & Barney
formant peaks, jitter/shimmer, consonant onsets, click-free
transitions. Presets: vocal, choir.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-27 12:22:36 -04:00
kennethreitz 751d5a49b8 Cleaner vocal synth: less static, click-free note transitions
- Jitter reduced (0.3% → 0.1%), shimmer reduced (2% → 0.8%)
- Breath noise halved (0.08 → 0.04), mix 85/15 → 92/8
- 10ms fade in/out on every vocal note prevents clicks
- Smoother syllable transitions

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-27 12:17:12 -04:00
kennethreitz 6a836dd891 Overhaul vocal synth: LF glottal model, 5 formants, jitter/shimmer
- LF glottal pulse: asymmetric open/close phase (not sines)
- 5 parallel formant filters per vowel (Peterson & Barney data)
- Jitter (0.3% pitch irregularity) + shimmer (2% amplitude)
- Much more voice-like than previous version
- Consonant onsets preserved

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-27 12:13:50 -04:00
kennethreitz 1f888e2b21 Vocal/formant synth with choir preset
Formant synthesis: glottal buzz source through parallel bandpass
filters at vowel resonance frequencies. Supports 5 vowels (A E I O U)
with consonant onsets (plosives, sibilants, nasals, fricatives,
liquids, aspirates, glides). Per-note lyrics via Part.add(lyric=).

Best for choir pads — vowel sounds with cathedral reverb and detune.
Consonant synthesis is rudimentary (noise bursts, not real speech).

Presets: vocal (solo), choir (detuned ensemble).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-27 12:10:54 -04:00
kennethreitz fb923f6c76 v0.35.1: Granular synthesis engine
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-27 11:50:32 -04:00
kennethreitz 59e3338892 Granular synthesis engine with presets
Grain cloud synthesis: source waveform chopped into tiny overlapping
grains (40ms, 50/sec) with Hanning windows, random scatter, and
per-grain pitch variation. Creates textures impossible with other
synthesis. Two presets: granular_pad, granular_texture.
30 synth waveforms total.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-27 11:47:52 -04:00
kennethreitz 8cf4145c15 Docs: timpani, saxophone, Part.roll(), update waveform counts
- Add timpani and saxophone synth sections to synths.rst
- Add rolls section to sequencing.rst with examples
- Update waveform count: 27 → 29

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-27 11:38:46 -04:00
kennethreitz b3885b2c15 v0.35.0: JI ratios, 8.5x faster import, timpani, saxophone, rolls
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-27 11:34:34 -04:00
kennethreitz ae04fa60cc Reduce vibrato across all instruments to 0.001
Strings, cello, trumpet, clarinet, oboe all cut to 0.001 depth.
Much subtler in ensemble context.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-27 11:29:48 -04:00
kennethreitz 6c411e43f8 Part.roll() for crescendo/decrescendo rolls, reedier sax, timpani reverb
- roll(tone, duration, velocity_start, velocity_end, speed) — rapid
  repeated notes with velocity ramp. Works on any instrument.
- Saxophone reed noise boosted and bandpass filtered for more bite
- Timpani preset: cathedral reverb at 0.4

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-27 11:26:28 -04:00
kennethreitz e0427af3cc Timpani and saxophone synths, 4 sax presets
- Timpani: inharmonic membrane modes (1.0, 1.5, 1.99, 2.44),
  felt mallet attack, copper kettle resonance, two-stage decay
- Saxophone: conical bore (all harmonics), strong mids, reed buzz,
  brass body warmth. 4 presets: saxophone, alto_sax, tenor_sax, bari_sax
- 29 synth waveforms total

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-27 11:22:47 -04:00
kennethreitz 552836ae5b Drop pytuning/sympy, lazy-load scipy: import 0.48s → 0.05s (fixes #44)
- Replace pytuning with 30-line native implementations of EDO,
  Pythagorean, and quarter-comma meantone scale generators
- Lazy-load scipy.signal (337ms) — only imported when audio rendering
  is actually used, not on theory-only imports
- Removes pytuning and sympy from dependencies entirely

Import time: 0.479s → 0.056s (8.5x faster)

Closes #44

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-27 11:17:52 -04:00
kennethreitz 0fe53fcdeb Merge pull request #46 from kennethreitz/fix/accidental-octave-wrap
Fix B#/Cb octave boundary crossing
2026-03-27 11:11:43 -04:00
kennethreitz f6fb2a2cd6 Fix B#/Cb octave boundary crossing (fixes #45)
B#4 now correctly resolves to C5 (523.25 Hz), not C4 (261.63 Hz).
Cb4 now correctly resolves to B3 (246.94 Hz), not B4 (493.88 Hz).

When an accidental crosses the B/C octave boundary, the octave is
adjusted: sharps crossing B→C increment, flats crossing C→B decrement.
Also handles double sharps (B##→C#5) and double flats (Cbb→Bb3).

Closes #45

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-27 11:11:08 -04:00
kennethreitz 70d6e6b8ce Reduce flute vibrato further (0.0015 → 0.0008)
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-27 11:05:55 -04:00
kennethreitz aec9a999cb Arabic maqam JI ratios: Zalzalian neutral third (27/22)
Maqam system now uses just intonation ratios instead of 24-TET:
- Quarter-tone positions use Zalzalian (11-limit) ratios
- Mi↓ (the defining Rast note) is exactly 27/22 from Do
- Standard JI intervals for chromatic positions
- Septimal ratios (7-limit) for other quarter-tone positions

Research confirmed: Turkish 53-TET and Thai 7-TET are already
correct as equal temperaments. Gamelan has no universal ratios
(each ensemble is unique), so TET remains the best default.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-27 11:00:16 -04:00
kennethreitz 3acde86028 Int tone names, wrapping, System.tone(), proper shruti JI ratios
- Tone(0, system=edo22) works alongside Tone("0", ...)
- Tone(22, system=edo22) wraps to tone 0, octave+1
- Tone(-1) wraps to last tone, octave-1
- System.tone(name, octave) convenience method
- Shruti system now uses 5-limit just intonation ratios instead
  of 22-TET approximation. Based on Pythagorean/harmonic ratios
  from traditional Indian musicology. Pa is a pure 3/2, Ga is a
  pure 5/4.
- System.ratios attribute overrides equal temperament when set

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-27 10:54:28 -04:00
kennethreitz aa405702a9 Fix Journey: EDM parts start after tabla solo ends
Calculated edm_start from actual section lengths so pad/sub/sitar2
don't bleed into the tabla solo.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-27 10:44:42 -04:00
kennethreitz b7c018fb94 Expanded tabla solo: 4 sections, 9-tuplets, polyrhythm, grand tihai
Solo now has 4 distinct parts:
1. Whisper — single hits with space, breath
2. Ghosts emerge — 16th note ghost fills between accents
3. Call and response — dayan vs bayan, 9-tuplet break
4. Blazing — 32nd triplet cascades, rapid alternating hands,
   9-against-4 polyrhythm, grand tihai (3x, each louder), slam

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-27 03:06:59 -04:00
kennethreitz 07a52a3a25 Add tabla solo section to Journey, louder sitar in EDM drop
Tabla solo with ghost notes, 32nd triplet cascade, tihai, then
slams into house beat. Sitar volume 0.22 → 0.4 in EDM section.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-27 03:03:57 -04:00
kennethreitz e12cb9003b Song #24: Journey (Piano → World → Sitar EDM)
Single score, one reverb space (Taj Mahal), tanpura drone throughout.
Piano arpeggios alone → cello joins → harp/oboe/flute with djembe →
sitar over tabla → EDM section with sitar, synth pad, 808 sub, house
drums. 28 bars, 5 movements.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-27 03:01:36 -04:00
kennethreitz 28968a1b5c Docs: strumming, pitch bends, tuning systems, fix instrument count
- Add guitar strumming section to sequencing.rst
- Add pitch bends section with three bend types
- Add tuning systems section (temperament, reference_pitch, TET)
- Fix index.rst: 25 → 49 instrument presets

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-27 02:49:24 -04:00
kennethreitz 8a4a2df1aa Song #23: Tabla Solo in Raga Yaman (22-shruti)
Tanpura drone intro, quiet sitar Yaman phrases, tabla solo building
from gentle theka through ghost notes to blazing tiri kita with
bayan pitch bends, tihai, dramatic silence, slam finish. Taj Mahal
reverb throughout.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-27 02:37:34 -04:00
kennethreitz f4a90637db Note choking: new hits fade out previous tails on same sound
Drums and melodic notes now choke previous resonance with a quick
fade when a new hit/note starts. Prevents muddy buildup at fast
tempos. Added bayan pitch bend drum sound (TABLA_GE_BEND).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-27 02:32:34 -04:00
kennethreitz 90a1a31049 Fix pitch bends: resampling preserves instrument timbre
Render at base pitch using the actual synth, then variable-rate
resample to shift pitch over time. No more sine wave fallback or
retriggering artifacts. Three bend types: smooth (log), linear, late.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-27 02:22:03 -04:00
kennethreitz 33b2e82594 v0.34.1: Pitch bends, updated docs and songs
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-27 02:15:32 -04:00
kennethreitz 9f8dd0006d Pitch bends, updated docs, songs with new instruments
- Pitch bend: part.add("C4", bend=2, bend_type="smooth") bends up
  a whole step. Three types: smooth (log/perceptual), linear, late
  (hold then bend — blues style).
- Updated songs.py: use dedicated instrument synths (piano_synth,
  flute_synth, trumpet_synth, etc.) instead of generic waveforms
- Updated docs: synths.rst (27 waveforms, instrument synths section),
  effects.rst (cabinet sim, analog drift, updated signal chain),
  drums.rst (world percussion: tabla, dhol, dholak, mridangam,
  djembe, metal kit), index.rst (feature counts)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-27 02:14:45 -04:00
kennethreitz 417f7f74a3 19 new tests: instrument synths, cabinet, analog, strumming, world drums
Tests for: all 14 dedicated synth waveforms, piano brightness scaling,
cabinet sim frequency reduction, analog drift rendering, strum with/
without fretboard, strum direction, all 6 tabla sounds, dhol/mridangam/
djembe/metal kit sounds, 20 world drum pattern presets, guitar preset
cabinet sim. 838 tests total.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-27 02:11:48 -04:00
kennethreitz cd6f814049 Update changelog with vibrato tuning
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-27 02:08:37 -04:00
kennethreitz 83fcdb0a09 Reduce vibrato depth on flute, oboe, trumpet, cello
All cut from 0.003-0.004 to 0.0015-0.002 — less wobbly in
ensemble context, more natural.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-27 02:08:17 -04:00
kennethreitz aa21bf0f2a v0.34.0: 27 synth waveforms, world drums, guitar strumming
16 dedicated instrument synths, speaker cab sim, analog drift,
strumming with fretboard lookup, dhol/dholak/mridangam/djembe/
metal kit with 22 patterns, 5 new demo moods.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-27 02:00:57 -04:00
kennethreitz e7e35ad4e4 5 more dedicated synths: oboe, harpsichord, cello, harp, upright bass
- Oboe: double reed buzz + conical bore (all harmonics, peaked 3-5)
- Harpsichord: KS with quill chiff, bright metallic pluck
- Cello: deep bowed string with 250/500Hz body resonance
- Harp: soft KS pluck with large soundboard bloom
- Upright bass: thick string pizzicato with wooden body resonance
- 27 synth waveforms total

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-27 01:59:20 -04:00
kennethreitz 503dbce937 6 dedicated instrument synths: piano, bass, flute, trumpet, clarinet, marimba
- Piano: hammer strike + detuned strings + inharmonicity + soundboard
- Bass guitar: heavy KS with thick string damping + low-mid pickup
- Flute: breath noise + tube resonance + developing vibrato
- Trumpet: lip buzz harmonics + brass bell resonance + vibrato
- Clarinet: odd harmonics (cylindrical bore) + reed noise
- Marimba: inharmonic bar modes (1x, 4x, 9.2x) + resonator tube
- 22 synth waveforms total

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-27 01:44:00 -04:00
kennethreitz c6bbfae7e6 Acoustic guitar synth with body resonance, fix strum
- New acoustic_guitar_synth: Karplus-Strong with wooden body
  resonance (3 formant peaks at 110/250/500 Hz), warmer initial
  noise, gentle rolloff. Sounds woody, not harsh.
- Strum renders as a single chord hit — no more exposed grace
  notes that sounded digital. Clean, full chord sound.
- 16 synth waveforms total

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-27 01:38:35 -04:00
kennethreitz 64ef7f0803 Add analog oscillator drift for synth warmth
Per-note random pitch wobble (gaussian, ±cents scaled by analog param)
simulates analog oscillator instability. Applied to synth_lead (0.3),
synth_pad (0.4), synth_bass (0.2), acid_bass (0.3), electric_piano
(0.2), organ (0.15). Subtle enough to add life without sounding
out of tune.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-27 01:32:03 -04:00
kennethreitz 406e5d7e54 Electric guitar synth, cab sim, strumming, world drums, metal kit
- Electric guitar: Karplus-Strong + magnetic pickup comb filter
- Cabinet simulation: speaker rolloff + presence bump (tames fizz)
- 6 guitar presets: clean, crunch, distorted, orange, metal
- Part.strum(): fretboard fingering lookup with down/up strumming
- Sitar synth: jawari buzz + chikari sympathetic strings
- Dhol, dholak, mridangam, djembe synthesis (membrane noise)
- Metal drum kit (kick click, bright snare, tight hats)
- 11 world patterns + 4 metal patterns + 7 tabla patterns

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-27 01:25:53 -04:00
kennethreitz 267b7284ba Add dhol, dholak, mridangam, djembe drums + 11 world patterns
Drum synthesis:
- Dhol: dagga (heavy bass), tilli (treble crack), both
- Dholak: ge (bass palm), na (treble fingers), tit (light tap)
- Mridangam: tham (clay body bass), nam (rich overtone ring),
  din (both heads), tha (muted)
- Djembe: bass (center palm), tone (edge ring), slap (sharp crack)
All with bandpass-filtered membrane noise for drum head character.

Patterns:
- Dhol: bhangra, dhol chaal
- Dholak: qawwali, dholak folk
- Mridangam: adi talam, mridangam korvai
- Djembe: djembe (standard), kuku, soli

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-27 00:59:27 -04:00
kennethreitz 9b62b56120 Sitar synth, tabla drums with wood/metal shells, 7 tabla patterns
- Sitar synth: Karplus-Strong with gentle jawari bridge buzz,
  variable damping (bright attack fades to warm sustain), chikari
  sympathetic string shimmer
- Tabla: 6 synthesized strokes (Na, Tin, Ge, Dha, Tit, Ke) with
  goatskin membrane noise (bandpass filtered), wooden shell resonance
  on dayan, copper/metal shell resonance on bayan
- 7 tabla patterns: teental (16 beats), jhaptaal (10), rupak (7),
  dadra (6), keherwa (8), tabla solo, tiri kita (fast 16th-note)
- Sitar instrument preset with proper lowpass

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-27 00:54:19 -04:00
kennethreitz 4fe7771d83 v0.33.0: Microtonal systems, historical tuning, Bohlen-Pierce
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-27 00:34:59 -04:00
kennethreitz 57079a43ac Merge feature/non-12-tet: microtonal systems, historical tuning
11 microtonal systems, Bohlen-Pierce tritave, just intonation,
reference pitch, Score(system=, temperament=, reference_pitch=).
TET(n) factory for any equal temperament. 819 tests passing.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-27 00:34:12 -04:00
kennethreitz 1d07b06968 Add Greensleeves (Renaissance lute, meantone A=415) to songs.py
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-27 00:33:50 -04:00
kennethreitz 9887b59cfb Add reference_pitch to Score and playback pipeline
Score(reference_pitch=415.0, temperament="meantone") renders an
entire piece at Baroque pitch with historical tuning. Flows through
to all .pitch() calls in both normal and legato renderers.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-27 00:32:11 -04:00
kennethreitz 9850a8016e Bohlen-Pierce, just intonation, temperament in Score/playback
- Bohlen-Pierce (13-TET tritave): period=3.0 support in pitch(),
  System, and TET factory. 13 equal divisions of the 3:1 ratio.
- Just intonation temperament: 5-limit JI ratios (pure 3/2 fifths,
  5/4 thirds). Use temperament="just" anywhere.
- Score(temperament="just") flows through to playback — all .pitch()
  calls in the render pipeline use the Score's temperament.
- Carnatic 72-TET system with 10 melakartas.
- Fix c_index for Indian, Arabic, and Gamelan 12-TET systems.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-27 00:24:03 -04:00
kennethreitz 35f5f35dc5 Carnatic 72-TET, Score system param, 22 microtonal tests
- Carnatic (72-TET): 10 melakartas including shankarabharanam,
  kalyani, mayamalavagowla, kharaharapriya, etc.
- Score(system=) param passes tuning system to all parts, so
  Part.add("Sa") resolves through the correct system
- 22 new tests covering all microtonal systems: TET factory,
  19/31-TET, shruti, maqam, slendro, pelog, thai, makam,
  carnatic, circle of fifths, from_frequency, Score integration

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-27 00:18:09 -04:00
kennethreitz 47ca94111f Add gamelan slendro/pelog, Thai 7-TET, Turkish 53-TET makam
- Slendro (5-TET): true equal 5-tone gamelan tuning, 240 cents/step
- Pelog (9-TET): 7-of-9 gamelan tuning with pathet nem/lima/barang
- Thai classical (7-TET): 7 equal divisions (~171 cents each)
- Turkish makam (53-TET): Arel-Ezgi-Uzdilek system with 9 makams
  (rast, hicaz, ussak, nihavend, huseyni, kurdi, segah, saba, huzzam)
- Fix octave parser to only match trailing digits (not "Mib+3")
- Fix _index to use _name_to_index (avoid creating Tone objects)
- Fix _math to use per-system c_index

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-27 00:11:19 -04:00
kennethreitz 62cfbb2591 Add 22-shruti Indian and 24-TET Arabic maqam systems
- "shruti" system: 22 named shrutis with proper microtonal intervals
  for all 10 thaats (bilawal, bhairav, todi, etc.) and pentatonic
  scales (bhupali, malkauns, durga). Captures the 2-shruti vs 3-shruti
  distinctions that 12-TET approximations lose.

- "maqam" system: 24-TET with quarter-tone positions (↑/↓ notation).
  True maqam Rast with quarter-flat E and B. Bayati, Saba, Sikah,
  Hijaz, and 6 more maqamat with exact quarter-tone intervals.

- 12-TET "indian" and "arabic" systems preserved for backwards compat.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-27 00:04:27 -04:00
kennethreitz de855a3fe6 Add non-12-TET support: TET() factory, 19-TET, 31-TET
- TET(n) factory creates N-tone equal temperament systems
- Built-in named systems: "19-tet" and "31-tet" with proper note
  names and scale definitions (major, minor, harmonic minor, pentatonic)
- Per-system c_index replaces global C_INDEX constant
- Fix 6 hardcoded '12's in tones.py: from_frequency, from_midi,
  interval_to, midi property, circle_of_fifths/fourths
- Numbered pitch classes for custom EDOs: TET(17) uses "0"-"16"
- Octave parser skips numeric-only names (fixes "0" being eaten)

Refs #38

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-26 23:58:33 -04:00
kennethreitz dc9f7b3342 Update changelog for 0.32.1
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-26 23:50:52 -04:00
kennethreitz 60fdff6d36 Merge pull request #43 from kennethreitz/fix/enharmonics-and-double-accidentals
Support enharmonic spellings and double accidentals
2026-03-26 23:50:33 -04:00
kennethreitz f42d38d1fd Support Cb, Fb, E#, B#, double sharps/flats, unicode symbols
- Cb, Fb, E#, B# resolve to their enharmonic equivalents (fixes #40)
- C##, Dbb, etc. resolve via semitone arithmetic (fixes #41)
- Unicode symbols accepted: ♯ ♭ 𝄪 𝄫
- 'x'/'X' accepted as double sharp (Bach notation): Fx = F##
- resolve_name handles all accidentals dynamically

Closes #40, closes #41

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-26 23:48:42 -04:00
kennethreitz 5a4122d61f Merge pull request #42 from kennethreitz/fix/tone-validate-early
Validate tone name at construction time
2026-03-26 23:43:41 -04:00
kennethreitz 3e4ba54a32 Validate tone name at construction time (fixes #39)
Tone("X") now raises ValueError immediately instead of silently
storing an invalid name and only failing on .frequency access.

Closes #39

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-26 23:43:00 -04:00
kennethreitz 5dd1c5e15d v0.32.0: 8 new synth features, highpass filter, preset overhaul
Filter envelope, velocity→brightness, sub-oscillator, tremolo,
saturation, noise layer, phaser, configurable FM. Highpass filter.
Bowed and mallet envelopes. Improved strings_synth with additive
synthesis. All 38 instrument presets sanity-checked and enhanced.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-26 22:00:49 -04:00
kennethreitz e46732fb5a Improved strings_synth, highpass filter, bowed envelope
- Rewrite strings_wave with additive synthesis: natural 1/n harmonic
  rolloff shaped by body resonance curve, per-harmonic phase
  randomization, delayed vibrato onset, bow pressure variation
- Add highpass filter (12dB/oct biquad) to signal chain and Part API
- Add BOWED envelope (40ms attack with bite) for string instruments
- Update string presets to use strings_synth + bowed envelope

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-26 21:38:15 -04:00
kennethreitz 833ab56857 Fix solo string instruments: clean triangle, no detune
Solo violin/viola/cello/contrabass now use triangle + strings envelope
(clean, clear). String ensemble keeps strings_synth + detune for
thick ensemble textures. Solo instruments need clarity, not width.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-26 21:26:35 -04:00
kennethreitz 6b2b1e201e Update index.rst with 13 synths, 38 instrument presets
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-26 21:20:46 -04:00
kennethreitz f9c81fe05f v0.31.0: 3 new synths, 38 instrument presets
- Karplus-Strong pluck (physical modeling for guitar/harp/koto)
- Hammond organ (additive drawbar synthesis)
- String ensemble (filtered saw with body resonance formants)
- 38 instrument presets: score.part("lead", instrument="violin")
- Demo updated with pluck_synth, organ_synth, strings_synth

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-26 21:18:27 -04:00
kennethreitz 931ec905c3 Add 3 new synths + 38 instrument presets
New synths:
- pluck_synth: Karplus-Strong physical modeling (guitar, harp, koto)
- organ_synth: Hammond-style additive drawbar synthesis
- strings_synth: Filtered saw with body resonance formants

38 instrument presets across 7 categories: keys, strings, woodwinds,
brass, plucked, synth, percussion/mallet. Each preset combines synth,
envelope, and effects to approximate real instruments.

score.part("lead", instrument="violin")
Score.list_instruments()

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-26 21:15:56 -04:00
kennethreitz 799ffbdac9 Add MIT LICENSE file
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-26 21:02:44 -04:00
kennethreitz b29b33524f v0.30.0: Drums as Parts, split drums, kick-only sidechain, MIDI import
- Drums are real Parts with full effects pipeline
- split=True creates kick/snare/hats/toms/cymbals/percussion Parts
- Sidechain triggers on kick only
- Score.from_midi() imports Standard MIDI Files
- Document split drums workflow

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-26 20:27:10 -04:00
kennethreitz 25f25c1f23 Split drums into separate Parts: kick, snare, hats, toms, cymbals, percussion
score.drums("rock", split=True) creates independent Parts per group.
Each gets its own effects chain. set_drum_effects() applies to all.
Sidechain triggers on kick only. Render loop handles multiple drum Parts.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-26 20:26:08 -04:00
kennethreitz 3f1d632285 Sidechain triggers on kick only, not all drum hits
Hi-hats and snares no longer duck the pad — only the kick does.
This is how sidechain compression works in real mixes.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-26 20:22:20 -04:00
kennethreitz 1938037458 Update changelog: drums as Part
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-26 20:20:05 -04:00
kennethreitz f7c05e1b31 Drums are now a real Part — same effects pipeline, zero duplication
_drum_hits and _drum_pattern_beats proxy through score.parts['drums'].
Drum Part goes through _apply_part_effects like any other Part.
set_drum_effects() is now sugar over the Part's attributes.
All 789 tests pass with no API changes.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-26 20:17:05 -04:00
kennethreitz c375785bb9 Update changelog for drum bus effects
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-26 20:12:06 -04:00
kennethreitz 9ebd54b7fc Add drum bus effects — same engine as parts, zero duplication
score.set_drum_effects(reverb=0.2, reverb_type="plate", lowpass=8000)
Uses _apply_effects_with_params on each stereo channel.
Supports all effects: reverb, delay, lowpass, distortion, chorus.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-26 20:11:40 -04:00
kennethreitz ce68ad8f19 Update changelog for v0.29.1
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-26 20:09:00 -04:00
kennethreitz f402e76480 Rename song.py → songs.py, polish all 20 songs with effects
Every song now has: stereo panning, convolution reverb (plate/cathedral),
humanize (0.2), detune (8-12) on pads, sidechain on electronic tracks,
lowpass on bass, delay on leads. No melodies changed — just better sound.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-26 20:03:15 -04:00
kennethreitz 4d3c7e0d6c v0.29.0: MIDI import — Score.from_midi()
Load any Standard MIDI File into a Score. Zero-dependency parser
handles Type 0 and Type 1 files. Each channel becomes a Part,
channel 10 becomes drum hits. Roundtrip with save_midi works.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-26 14:25:19 -04:00
kennethreitz 5a74a6f715 v0.28.3: Better demo songs
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-26 11:39:14 -04:00
kennethreitz 5416674858 Rewrite demo songs: stereo, effects, humanize, 8 moods
Each demo now uses pan, detune, spread, convolution reverb,
sidechain, humanize, velocity accents, genre-matched fills.
Added Dub and Temple moods.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-26 11:38:19 -04:00
kennethreitz 9a5f305ac6 Add CLAUDE.md with release process and music preferences
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-26 11:05:24 -04:00
kennethreitz bc38ce73f0 Update changelog for v0.28.2
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-26 11:00:17 -04:00
kennethreitz 081b924d29 v0.28.2: Tighter drum humanize default (0.15)
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-26 10:59:41 -04:00
kennethreitz 427ff44ce9 Lower drum_humanize default to 0.15 — tighter feel
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-26 10:54:13 -04:00
kennethreitz 360a908464 v0.28.1: Humanized drum hits
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-26 10:53:45 -04:00
kennethreitz a8dd4d6542 Humanize drum hits — random timing jitter and velocity variation
Drums now have micro-timing and velocity imperfections like a real
drummer. Default 0.3 (subtle). Control via Score(drum_humanize=0.5).
Kick stays tightest, hats and ghost notes drift naturally.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-26 10:51:18 -04:00
kennethreitz 866b110afa Sync all summary pages with current feature set
index.rst: add figured bass, pitch class sets, scale recommendation,
stereo, detune, pan/spread, master compressor, REPL
quickstart.rst: same updates to "What's in the Box"
README.md: add stereo, sidechain, compressor, repl, forte numbers
drums.rst: document stereo drum panning
playback.rst: document stereo output and master compressor
cli.rst: add REPL section with cross-reference

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-26 07:29:12 -04:00
kennethreitz 0fc0b87017 Move closing line below toctree
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-26 07:22:53 -04:00
kennethreitz 1a724a94b0 Add closing line to homepage
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-26 07:18:42 -04:00
kennethreitz b239e9a997 Add warm closing paragraphs to all 11 guide pages
Every page now ends on prose instead of a code block.
Chords, tones, scales, effects, drums, CLI, cookbook,
fretboard, playback, systems, theory — each with a
sentence that ties the page together.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-26 07:17:36 -04:00
kennethreitz a766737707 v0.28.0: Figured bass, pitch class sets, scale recommendation
- Chord.figured_bass: classical inversion notation (6, 6/4, 7, 6/5, 4/3, 2)
- Chord.analyze_figured(): Roman numerals with figured bass (V6/5, ii6)
- Chord.pitch_classes, normal_form, prime_form, forte_number: set theory
- Scale.recommend(): ranked scale suggestions from note sets
- Forte catalog: all trichords and tetrachords
- Documented in chords and scales guides

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-26 07:13:26 -04:00
kennethreitz 0843c21884 v0.27.2: Temple Bell song
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-25 22:59:03 -04:00
kennethreitz eb7a2bf27d Add Temple Bell (Japanese) to song player — #20
Sparse triangle koto over E hirajoshi scale, Taj Mahal reverb,
sine drone, FM bells. Silence as instrument.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-25 22:58:07 -04:00
kennethreitz c78530611d v0.27.1: Tab completion in REPL
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-25 22:52:50 -04:00
kennethreitz 7267d25fb5 Add tab completion to REPL
Context-aware: commands on first word, drum presets after 'drums',
synth names after 'part', chord symbols after 'arp'/'chord',
note names after 'add', systems after 'system', LFO params after
'set'/'lfo', envelope names as third arg for 'part'.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-25 22:52:08 -04:00
kennethreitz 53db299b5f v0.27.0: High-quality drum sounds, Dance Party song
All 15 drum sounds rewritten with inharmonic partials, proper
transients, multi-mode resonance, and saturation. Song #19:
Dance Party at the Reitz House — for Sarah and Malachi.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-25 22:41:57 -04:00
kennethreitz a4fa233edf Rewrite all drum sounds for higher quality
Every percussion voice improved with proper transients, inharmonic
partials, multi-mode resonance, and saturation:
- Clap: 808-style layered bursts
- Rimshot: dual resonance (rim+head)
- Toms: pitch sweep + shell resonance + stick attack
- Crash: 5 inharmonic metallic partials
- Ride: 4 partials + stick click
- Cowbell: 808 square-ish tones
- Clave: dual wood resonance
- Conga: pitch drop + shell mode + slap
- Shaker: shaped envelope + sparkle
- Tambourine: 4 jingle frequencies
- Timbale: metal shell overtones
- Agogo: 3 bell modes
- Guiro: rhythmic ridge scrapes

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-25 22:39:27 -04:00
kennethreitz e7e90382c5 v0.26.3: Stereo drums, stereo convolution reverb, 2 new songs
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-25 22:37:10 -04:00
kennethreitz 1ef32ecc92 Stereo drum panning — each sound placed in the stereo field
Kick/snare center, hat right, crash left, toms spread L-to-R,
congas/timbales/agogos across the field. Sounds like a real kit.
Mono sidechain trigger preserved for compression.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-25 22:36:22 -04:00
kennethreitz f9af708f0a Add Glass and Silk (sine+triangle waltz) to song player — #18
Pure sine and triangle only. Ab major waltz at 72bpm. Cathedral
and Taj Mahal reverb. Triangle legato melody with glide.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-25 22:32:50 -04:00
kennethreitz 0e93e45853 v0.26.2: Stereo convolution reverb for all 7 IR presets
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-25 22:29:08 -04:00
kennethreitz f0802ae614 Add stereo convolution reverb — different IR per L/R channel
Two independently generated impulse responses create natural stereo
width in the convolution reverb tail. Works with all 7 presets
(Taj Mahal, cathedral, plate, spring, cave, parking garage, canyon).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-25 22:24:02 -04:00
kennethreitz 4a992eba2b Add Neon Grid (stereo acid) to song player — #17
Dual 303s arping in opposite directions on opposite sides,
supersaw pad with full spread, ping-pong FM bells, sidechain sub.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-25 22:17:24 -04:00
kennethreitz 398cc68166 Document pan, spread, and stereo reverb in synths guide
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-25 22:12:57 -04:00
kennethreitz 3b0a63d57c v0.26.1: Stereo reverb with different L/R early reflections
Different comb filter delay times per channel create natural stereo
width in the reverb tail. Effects chain skips mono reverb in favor
of stereo reverb applied in the mixer after panning.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-25 22:12:09 -04:00
kennethreitz c86ae7b118 v0.26.0: Stereo output with pan and spread
- render_score() outputs stereo (N, 2) arrays
- pan: -1.0 (left) to 1.0 (right), constant-power panning
- spread: detuned oscillators go to opposite L/R channels
- Master compressor runs per-channel
- Drums center, parts panned independently

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-25 22:08:38 -04:00
kennethreitz d2044f1f53 v0.25.7: Detune, drum swing, improved drum sounds
- detune parameter: ±cents oscillator spread on any synth
- Drum swing: offbeats shift with score groove
- Snare: 220Hz + transient click + saturation
- Hi-hats: metallic harmonics (6k+8.5k+12k), crisper
- Detune documented in synths guide

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-25 22:04:08 -04:00
kennethreitz 7991516c3e Add detune parameter — ±cents oscillator spread on any synth
Renders three oscillators per note: center + up + down by the
specified cents. Creates analog-style beating/width on any waveform.
detune=15 is classic Juno drift, detune=25 is trance supersaw territory.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-25 22:02:41 -04:00
kennethreitz a0e0dbc807 v0.25.6: Drum swing + improved snare/hat sounds
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-25 21:50:08 -04:00
kennethreitz a77db557f3 Apply swing to drum hits — offbeats shift with the groove
Drum hits on fractional beat positions now get pushed later by
the score's swing amount. Everything locks into the same pocket.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-25 21:48:31 -04:00
kennethreitz f4c3b2dd88 v0.25.5: Improved snare and hi-hat sounds
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-25 21:46:05 -04:00
kennethreitz 32387e2d23 Improve snare and hi-hat drum sounds
Snare: 220Hz body (was 180), faster decay, transient click, tanh saturation.
Closed hat: 30ms (was 50), metallic harmonics (6k+8.5k+12k), decay=100.
Open hat: 150ms (was 250), same metallic harmonics, decay=18.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-25 21:45:29 -04:00
kennethreitz 2ecb1e5ce8 Use CHANGELOG.md directly in docs via myst-parser
No more maintaining a separate RST changelog. The docs now include
the markdown file directly.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-25 21:37:28 -04:00
kennethreitz 30dacc4fbf Sync docs changelog with CHANGELOG.md (0.15.1 through 0.25.4)
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-25 21:36:22 -04:00
kennethreitz 2a5ffcf78a v0.25.4: Master bus compressor/limiter
Feed-forward compression (threshold=0.5, ratio=4:1) with envelope
following, makeup gain, and brick-wall limiter at 0.95. Replaces
simple normalization. Everything sounds louder and punchier.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-25 21:34:49 -04:00
kennethreitz fe00644e3f v0.25.3: Interactive REPL — pytheory repl
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-25 21:28:46 -04:00
kennethreitz 89df5c4201 Merge pull request #37 from kennethreitz/repl
Add interactive REPL — music theory scratchpad
2026-03-25 21:26:20 -04:00
kennethreitz b396f42f84 Add REPL guide: theory scratchpad, composition, effects, complete example
Covers the prompt, theory commands, composition flow, effects,
automation, LFOs, playback, export, and a full start-to-finish session.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-25 21:26:11 -04:00
kennethreitz 45789f7af0 Fix all 6 PR review issues
1. readline: try/except for Windows compatibility
2. drums: only persist preset after successful load
3. chords: use analyze() for correct Roman numerals in minor keys
4. clear: full reset to initial state (key, bpm, drums, parts)
5. progression: add as alias for prog
6. lint: split one-line if statements (ruff E701)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-25 21:24:23 -04:00
kennethreitz e0bf637de5 Add theory commands, guitar, systems, intervals, 22 REPL tests
Theory: circle, interval, identify, system (with correct per-system tonics)
Guitar: fingering, diagram (scale on fretboard)
22 new tests covering all REPL commands.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-25 21:20:25 -04:00
kennethreitz e5f258bc21 Add guided welcome: 5 commands that teach the flow
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-25 21:15:11 -04:00
kennethreitz a294887215 Multiline prompt: compact when short, stacks when context grows
Single line: pytheory[key=Am | bpm=140]>
Multiline when >60 chars:
  key=Am | bpm=140 | drums=bossa nova | →lead(saw) rev=0.3 lp=2000
♫>

Shows active part synth and effects in the prompt.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-25 21:14:00 -04:00
kennethreitz 20932f48ab Context-aware prompt: pytheory[key=Am bpm=140 drums=bossa nova →lead]>
Shows key, bpm, drums preset, and active part in the prompt.
Fix: Part with 0 notes was falsy due to __len__, use 'is not None'.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-25 21:12:54 -04:00
kennethreitz 9e0faf840b Add interactive REPL — music theory scratchpad and composition tool
pytheory repl — commands mirror the Python API:
  key Am, chords, prog I V vi IV, modes, scales (theory)
  drums bossa nova, part lead saw pluck, add C5 1 (composition)
  arp Am updown 2 2, reverb 0.4, lfo lowpass... (effects)
  play_score, save_midi, render (output)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-25 21:10:36 -04:00
kennethreitz c7c733044c Remove Claude Code links from homepage, keep text
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-25 21:03:08 -04:00
kennethreitz 866065d7d7 Add connective prose to homepage — warm but concise
Brief intro paragraph, theory/composition section intros, pytheory demo
context. Enough personality without the wall of text.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-25 20:53:20 -04:00
kennethreitz aa9d4282c9 Trim homepage: two examples, compact feature list, get out of the way
197 → 85 lines. Theory example, composition example, pytheory demo,
one-line feature summary per category. No more walls of text
before the toctree.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-25 20:09:57 -04:00
kennethreitz 3593735243 Polish systems, fretboard, and theory docs
Systems: fix "four" to "six", add cultural context for each system
(Indian ragas, Arabic maqam, Japanese koto, Blues Delta origins,
Gamelan's influence on Debussy). Each system feels alive now.
Fretboard: add scale_diagram chord highlighting, non-string instruments note.
Theory: warmer opening, cross-reference to composition guide.
Tones, scales, chords, cli: verified complete — no changes needed.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-25 20:09:01 -04:00
kennethreitz 54fd4c2241 Make theory-only path first-class in quickstart
Two clear paths: theory (no audio needed) and composition.
Theory section expanded with tones, intervals, keys, chords,
analysis, modulation, 6 systems, guitar — all pure Python.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-25 20:05:25 -04:00
kennethreitz a4b11e6f35 Rewrite quickstart, update CLI docs, add composition recipes to cookbook
Quickstart: zero to arrangement in 5 minutes, pytheory demo, MIDI export.
CLI: add demo command docs at the top.
Cookbook: acid house, dub reggae, jazz ballad, song sections, MIDI export recipes.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-25 20:04:46 -04:00
kennethreitz 044e9a7eac Fix CI: lazy-import sounddevice so MIDI/render work without PortAudio
sounddevice is now only imported when actually playing audio through
speakers. All other functions (save_midi, render_score, save) work
without PortAudio installed. Fixes 9 test failures in CI.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-25 19:43:39 -04:00
kennethreitz 094887c849 v0.25.1: pytheory demo command, README rewrite
- `pytheory demo` plays a randomly generated track (6 moods, different every time)
- README rewritten to showcase full feature set: composition, effects, drums,
  sidechain, automation, MIDI export, AI collaboration

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-25 15:28:42 -04:00
kennethreitz 2fc5aae678 Document sidechain compression and song structure sections
Effects: sidechain pump with parameters, practical examples, tip
about not sidechaining everything.
Sequencing: song sections with verse/chorus/repeat workflow,
custom names, real songwriting analogy.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-25 15:22:51 -04:00
kennethreitz ac0cc0b6ce v0.25.0: Sidechain compression, song sections, punchier kick
- Sidechain: kick ducks pad/bass for EDM pump (sidechain=0.85)
- Song structure: score.section("verse"), score.repeat("chorus")
- Kick: 808-style 200→45Hz sweep, sub thump, soft saturation
- Section repeat copies notes, drums, automation with offset

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-25 15:21:33 -04:00
kennethreitz ce5f3e7626 Document humanize in sequencing guide
Values, use cases, combo with swing for realistic feel.
Convolution reverb was already documented in effects guide.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-25 15:06:24 -04:00
kennethreitz 0c4ba83b0c v0.24.1: Humanize — random micro-timing and velocity variation
Makes programmed parts feel like a real player. Random timing jitter
and velocity wobble applied at render time, score data stays clean.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-25 15:04:07 -04:00
kennethreitz f81b1e882d Document velocity, swing, tempo changes, and fades in sequencing guide
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-25 14:50:24 -04:00
kennethreitz ebf26cfbfa v0.24.0: Velocity, swing, tempo changes, fade in/out
- Per-note velocity for dynamics and accents
- Swing/groove parameter on Score and per-Part override
- score.set_tempo() for mid-song tempo changes with tempo map engine
- Part.fade_in() and Part.fade_out() volume envelopes
- Arpeggiator velocity support

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-25 14:44:50 -04:00
kennethreitz f9654fcdea Clean up code formatting: black-style, remove >>> from config snippets
Multi-line part creation uses vertical layout with trailing commas.
Configuration snippets use code-block:: python (clean, no >>>).
Interactive exploration keeps pycon format.
Mixed blocks split into setup + exploration.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-25 14:36:58 -04:00
kennethreitz f3f4174783 Mention AI collaboration (Claude Code) on homepage
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-25 14:14:38 -04:00
kennethreitz 119dd2d921 Add conversational prose throughout all 5 guide pages
Sequencing: Score concept, time signatures for non-musicians, Parts as
DAW tracks, arpeggiator/legato/glide context with TB-303 and acid history.
Synths: synthesis philosophy, DX7/Juno/JP-8000 history, practical combos.
Effects: real-music context (The Edge, DJ knobs, shower reverb), why signal
chain order matters, automation as breathing, LFO as repeating automation.
Drums: drums-as-genre foundation, genre group cultural context, fills as
transition signals, drum synthesis as real drum machine techniques.
Playback: three output options context, MIDI as the working musician's path.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-25 14:13:44 -04:00
kennethreitz 80698ccc3a Restructure docs: 5 focused pages + why-compose-in-Python intro
Break rhythm.rst and playback.rst into:
- sequencing.rst: Score, Parts, Duration, arpeggiator, legato
- synths.rst: 10 waveforms, 8 envelopes, combo recommendations
- effects.rst: signal chain, 5 effects, automation, LFOs
- drums.rst: DrumSound, 58 patterns, 21 fills, synthesis
- playback.rst: simplified output functions only

Rewrite index.rst with "Why compose in Python?" section explaining
the sketch→hear→export→DAW workflow, plus comprehensive highlights.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-25 14:03:27 -04:00
kennethreitz a41f20e805 v0.22.0: LFO automation for parameter modulation
- Part.lfo() generates automation points from oscillator shapes
- 4 shapes: sine, triangle, saw, square
- Rate, range, duration, and resolution all configurable
- Stack LFOs on different params for complex modulation

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-25 13:52:40 -04:00
kennethreitz 2de263c814 v0.21.0: Effect automation via Part.set(), chorus effect
- Part.set() inserts automation markers for mid-song parameter changes
- Chorus effect (LFO-modulated delay, Juno-style thickening)
- Renderer segments at automation points for per-section processing
- Chain: distortion → chorus → lowpass → delay → reverb
- Docs: automation, chorus, updated signal chain

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-25 13:49:12 -04:00
kennethreitz a7ad8a374b v0.20.0: Arpeggiator, flat numeral parser, generative showoff
- Part.arpeggio() with 5 patterns, octave spanning, division control
- Roman numeral parser handles bVI, bVII, bIII, #IV prefixes
- song_showoff.py: generative composition using every feature,
  different every time (4 moods, matched keys/drums/effects)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-25 13:41:03 -04:00
kennethreitz e75c35a099 v0.19.1: Arpeggiator, legato/glide docs, sequencing page rename
- Part.arpeggio() with up/down/updown/downup/random patterns
- Octave spanning and division control for arps
- Document legato, glide, and arpeggiator in rhythm guide
- Rename docs page to "Sequencing: Rhythm and Scores"

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-25 13:33:19 -04:00
kennethreitz b9f0a3870a v0.19.0: Legato mode with pitch glide/portamento
- Part(legato=True) renders continuous waveform without per-note retriggering
- Part(glide=0.04) adds 303-style pitch slides between notes
- Phase-accumulating oscillator for smooth frequency changes
- Exponential pitch interpolation for perceptually linear slides

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-25 13:28:50 -04:00
kennethreitz d425d6b624 Document per-part effects chain: distortion → lowpass → delay → reverb
Comprehensive effects docs with signal chain diagram, per-effect
reference, and combination examples (dub, acid, Drake-style 808).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-25 13:22:56 -04:00
kennethreitz 8c61cb146b Bump version to 0.18.1
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-25 13:06:29 -04:00
kennethreitz a11523e889 v0.18.1: Distortion effect, 3 new songs (dub delay, DnB, Drake)
- Soft-clip distortion (tanh waveshaping) with drive and mix controls
- Dub Delay Madness: separate snare track with massive delay/reverb
- Liquid DnB: 174bpm rollers with flowing lead
- Late Night Texts: Drake-style trap with 808 bass + distortion
- 16 total example songs

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-25 13:05:56 -04:00
kennethreitz cf061e3783 Rewrite all 13 songs with per-part effects (reverb, delay, lowpass)
Every song now uses the full effects chain. Added 3 new songs:
Kingston After Dark (dub), Minimal Techno, Gospel Shuffle.
Each demonstrates different effect combinations.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-25 13:01:56 -04:00
kennethreitz 5d746ed0b1 v0.18.0: Per-part audio effects — reverb, delay, lowpass filter
- Schroeder reverb (4 comb + 2 allpass filters) with mix/decay
- Tempo-synced delay with feedback
- 12 dB/oct biquad lowpass with resonance (Q) control
- Effects set at part creation, applied per-part before mixing

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-25 12:56:48 -04:00
kennethreitz 89323c0eb3 v0.17.0: 10 new grooves + 10 new fills (58 grooves, 21 fills total)
Grooves: country, ska, dub, jungle, techno, gospel, swing, bolero, tango, flamenco
Fills: reggae, afrobeat, bossa nova, house, trap, hip hop, disco, cumbia, highlife, second line

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-25 12:49:14 -04:00
kennethreitz 5a44d619d0 v0.16.0: Drum fills — 11 genre presets with auto-fill support
- Pattern.fill() with 11 presets: rock, jazz, salsa, samba, funk, metal, blast, buildup, breakdown
- Score.fill() inserts a fill at the current position
- Score.drums() auto-fill support: fill_every=4 replaces every 4th bar

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-25 12:43:46 -04:00
kennethreitz f5bf7ce505 v0.15.1: PWM synths, score.drums() shorthand, docs update
- Synth.PWM_SLOW: pulse width modulation with 0.3 Hz LFO (Juno pads)
- Synth.PWM_FAST: pulse width modulation with 3 Hz LFO (chorus/vibrato)
- Score.drums("preset", repeats=N) shorthand
- All docs updated with score.drums() syntax and 10-synth reference table

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-25 12:37:36 -04:00
kennethreitz 4f3b706336 v0.15.0: Add square, pulse, FM, noise, and supersaw synth waveforms
- Square wave: chiptune / 8-bit (odd harmonics at 1/n)
- Pulse wave: variable duty cycle NES-style timbres
- FM synthesis: DX7-style carrier/modulator for bells, e-piano, brass
- Noise: white noise for percussion and texture
- Supersaw: 7 detuned saws for trance/EDM pads
- Refactor Synth enum to string-valued with callable dispatch
- All 8 waveforms available via API, Part strings, and CLI

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-25 11:37:21 -04:00
kennethreitz 83a988d085 Update rhythm and playback docs with Part class, multi-part examples
- Rhythm guide: full Part API docs with synths, envelopes, chaining,
  raw float beats, headless rendering, complete bossa nova example
- Playback guide: rewrite intro with quick-start showing both simple
  and expressive usage, update all Score examples to use named parts

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-25 11:30:09 -04:00
kennethreitz d0e8e43b56 v0.14.0: Multi-part Score API with Part class
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-25 11:24:27 -04:00
kennethreitz c67a08a34e Rewrite songs with multi-part API, accept raw float durations
- All 10 songs now use score.part() for lead, bass, and chords
- Part.add() and .rest() accept raw float beats alongside Duration enums
- _RawDuration duck-type wrapper for arbitrary beat values

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-25 11:23:13 -04:00
kennethreitz e72ef4a6a7 Add Part class for multi-voice Score arrangements
- Part: named voice with synth, envelope, and volume settings
- Score.part() creates and registers parts
- Score.add_pattern() for cleaner drum pattern attachment
- render_score() renders all parts + drums into one buffer
- play_score() updated to use the new multi-part renderer
- Backwards compatible: Score.add() still works for simple cases

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-25 11:20:26 -04:00
kennethreitz 994c4e244a Rewrite song.py with 10 full arrangements using drums, chords, and synth leads
Bossa nova, bebop, salsa, afrobeat, reggae, funk, 12/8 blues, samba,
jazz waltz, and house — each with drum patterns, chord progressions,
and hand-written melody lines rendered through the synthesizer.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-25 11:15:06 -04:00
kennethreitz adfbc3079a v0.13.1: Fix drum pattern repeat sync
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-25 11:10:35 -04:00
kennethreitz 772fa84b4f Fix drum pattern repeats: offset hit positions for each cycle
Hits were piling up at the same positions instead of being spread
across repeats. Now each repeat offsets by pattern.beats.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-25 11:10:07 -04:00
kennethreitz b97378c836 Add Playing a Score section to playback docs
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-25 11:04:29 -04:00
kennethreitz f00cf10c41 v0.13.0: Drum synthesizer with play_pattern() and play_score()
- 27 synthesized drum voices (kick, snare, hat, conga, timbale, etc.)
- play_pattern() renders and plays drum patterns through speakers
- play_score() mixes drum patterns + chord progressions together
- Comprehensive drum synthesis docs with sound descriptions

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-25 11:01:02 -04:00
kennethreitz d57e780f6f v0.12.0: Rhythm module with 48 drum pattern presets
- Duration, TimeSignature, Score for note-level rhythm
- DrumSound enum (27 GM percussion sounds)
- Pattern class with 48 presets: rock, jazz, bebop, salsa, bossa nova,
  samba, afrobeat, funk, reggae, house, trap, metal, Afro-Cuban claves,
  cumbia, merengue, breakbeat, and many more
- Pattern.to_score() + save_midi() for drum MIDI export
- Comprehensive rhythm guide documentation

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-25 05:02:01 -04:00
kennethreitz 4f03bb6616 v0.12.0: Rhythm module with Duration, TimeSignature, Score, MIDI export
- Duration enum (whole through sixteenth, dotted, triplet)
- TimeSignature with string parsing (4/4, 3/4, 6/8, 12/8)
- Score class with fluent .add()/.rest() chaining
- Measure-aware MIDI export with time signature meta events
- Rhythm guide documentation

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-25 04:56:46 -04:00
kennethreitz 4aafd8d0b0 v0.11.0: Drop voicings, modulation, degree names, extensions, solfege, CLI identify/midi, docs
- Chord.close_voicing(), open_voicing(), drop2(), drop3()
- Key.modulation_path() for pivot-chord modulation paths
- Scale.degree_name() for traditional function names
- Chord.extensions() for available 9th/11th/13th suggestions
- Tone.solfege for fixed-Do solfege syllables
- CLI identify and midi commands
- Comprehensive docs update covering all v0.9.0–v0.11.0 features

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-25 04:49:17 -04:00
kennethreitz c74600d42f v0.10.0: Scale fitness, chord suggestions, Helmholtz, slash chords, MIDI export
- Scale.fitness() scores note-to-scale fit
- Key.suggest_next() for functional harmony chord suggestions
- Tone.helmholtz/scientific notation properties
- Chord.slash() and slash_name for slash chord notation
- save_midi() for Standard MIDI File export
- Fretboard.scale_diagram() highlights chord tones vs passing tones
- Chord.analyze() recognizes borrowed chords (bVI, bVII, etc.)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-25 04:39:27 -04:00
kennethreitz b3ef0ddc58 Add tests for all new features (fitness, suggest_next, helmholtz, slash, MIDI)
Fix Helmholtz octave 2 notation and update borrowed chord test expectation.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-25 04:38:43 -04:00
kennethreitz dd3b7bd03e Add MIDI export via save_midi()
Zero-dependency Standard MIDI File writer for tones, chords, and progressions.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-25 04:36:41 -04:00
kennethreitz 62111de2da Add Helmholtz notation, cents_difference, Scale.fitness, Key.suggest_next
- Tone.helmholtz and Tone.scientific properties for pitch notation
- Tone.cents_difference() for fine pitch measurement
- Scale.fitness() scores how well notes fit a scale
- Key.suggest_next() suggests likely next chords from functional harmony

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-25 04:35:49 -04:00
kennethreitz c3ae02ec4f Add slash chords, borrowed chord analysis, scale_diagram chord highlighting
- Chord.slash(bass) and slash_name property for slash chord notation
- Chord.analyze() now returns bVI, bVII etc. for borrowed/chromatic chords
- Fretboard.scale_diagram() highlights chord tones in uppercase when chord given

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-25 04:34:25 -04:00
kennethreitz f4d2cca663 v0.9.0: ADSR envelopes, chord symbol parser, modulation, parallel modes, cents
- Add Envelope enum with 8 ADSR presets (piano, organ, pluck, pad, strings, bell, staccato, none)
- Add Chord.from_symbol() to parse any standard chord symbol without lookup tables
- Add Key.pivot_chords() for finding modulation pivot chords between keys
- Add Scale.parallel_modes() to show all modes sharing the same notes
- Add Tone.cents_difference() for fine pitch comparison in cents
- Add --envelope flag to CLI play command
- Extract C_INDEX constant, removing hardcoded magic number

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-25 04:30:46 -04:00
kennethreitz 3b5a07dfce Add changelog with retroactive history, include in docs
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-25 04:20:50 -04:00
kennethreitz aa454ea7e9 v0.8.3: Chord symbols, common progressions, CLI modes/circle/progressions
- Add Chord.symbol property for standard shorthand notation (Cmaj7, Dm, G7)
- Add Key.common_progressions() to realize all named progressions in a key
- Add CLI commands: modes, circle, progressions

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-25 04:19:29 -04:00
kennethreitz 5aed586187 v0.8.2: Flat spellings in CHARTS acceptable_tone_names
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-23 09:52:50 -04:00
kennethreitz 09d90b3425 Use flat spellings in CHARTS acceptable_tone_names
NamedChord.acceptable_tones now uses prefer_flats based on circle-of-fifths
conventions. Cm7 shows (C, Eb, G, Bb) instead of (C, D#, G, A#).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-23 09:43:55 -04:00
kennethreitz 96131da59c v0.8.1: Musically correct flat spellings in flat keys
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-23 09:24:06 -04:00
kennethreitz d2058668a6 Use musically correct flat spellings in flat keys
Flat keys now display flats (Bb, Eb, Ab) instead of sharps (A#, D#, G#).
Uses the "no duplicate letter names" rule: if building a scale with
sharps produces two notes with the same letter (e.g. C and C# in C minor),
the scale is rebuilt with flat spellings instead.

- Tone.add() and Tone.from_index() accept prefer_flats parameter
- TonedScale detects flat vs sharp per-scale automatically
- F major: Bb (not A#), Eb major: Ab Bb (not G# A#), etc.
- All tests and docs updated to match

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-23 09:22:39 -04:00
kennethreitz a5ffdc6104 Expand cookbook, fix scale_diagram alignment, add play_progression
- 11 new cookbook recipes: circle of fifths, voice leading, tension
  analysis, tritone substitution, key signatures/detection, relative
  and parallel keys, borrowed chords, secondary dominants, overtones,
  enharmonics, world scales, guitar scale visualization
- Fix scale_diagram header alignment for 2-digit fret numbers
- play_progression() for sequencing chord playback

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-23 09:05:41 -04:00
kennethreitz 724a0df7b5 Extract pentatonic variable on homepage for readability
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-23 08:57:18 -04:00
kennethreitz 4750061b87 v0.8.0: Scale diagrams, cookbook, progression playback
- scale_diagram() showcased on homepage and quickstart
- New cookbook page: analyze a song, 12-bar blues, find chords in a key,
  compare scales, guitar chord chart, explore intervals
- play_progression() for sequencing chord playback with gaps
- Scale and Note aliases exported
- Version bump to 0.8.0

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-23 08:56:43 -04:00
kennethreitz d53d8b60dd API ergonomics, curated fingerings, bounded caching, slow test markers
- Fretboard["G"] shorthand via __getitem__
- Chord repr now shows <Chord C major> format
- Scale = TonedScale and Note = Tone aliases
- GUITAR_OVERRIDES dict with 15 curated standard chord shapes
  (F barre 133211, B barre x24442, etc.)
- Bounded caches (max 1024 entries) for fingerings and possible_fingerings
- @pytest.mark.slow on 4 chart-generation tests; fast suite runs in 2s
- Highlights section moved above CLI examples on docs homepage

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-23 08:52:17 -04:00
kennethreitz de1db0aa8d Add fingering memoization, barre detection, and 4-fret span constraint
- Cache fingering results by chord name + tuning for instant repeat lookups
- chart() goes from ~53s to <1ms on second call
- Barre chord detection: when multiple strings share the lowest fret,
  count them as 1 finger instead of N
- Hard 4-fret span constraint rejects unplayable voicings
- Penalize shapes requiring more than 4 fingers

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-23 08:42:45 -04:00
kennethreitz b22b3c063f Improve fingering algorithm, add convenience APIs, convert all docs to REPL style
- Fretboard.chord(), .tab(), .chart() convenience methods
- Fingering.tab() for rendering ASCII tablature
- Fingering algorithm now considers muting, fret span, root-in-bass,
  and contiguous bass-side muting for idiomatic voicings
- All docs converted from code-block:: python to pycon with >>> prompts
- All doc outputs verified against actual library output
- Tests for new methods; version test no longer checks exact string

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-23 08:36:31 -04:00
kennethreitz 7e1d9e76bd v0.7.0: Add Fretboard.chord() method for named chord lookups
New `fb.chord("G")` API lets you look up fingerings by chord name
instead of knowing fret positions upfront. Updates all docs to use
REPL-style examples with verified output.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-23 08:12:42 -04:00
kennethreitz 447d03a2d2 Update homepage code example to REPL style with verified output
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-23 08:05:30 -04:00
kennethreitz 7b82d70ad6 Document save() in playback guide and tritone_sub() in chords guide
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-22 21:20:35 -04:00
kennethreitz 44f8b902e2 Document capo support in fretboard guitars section
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-22 21:10:30 -04:00
kennethreitz 03eb61cd5d Rewrite docs landing page with richer examples
Show Key class, chord progressions, chord identification, interval
naming, and labeled fingerings in the hero code block. Add pip install
line, CLI examples, and a Highlights section summarizing all features.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-22 21:07:12 -04:00
kennethreitz eba299d406 Rewrite quickstart with sections for each feature area
Breaks the single code block into focused sections: Tones, Scales,
Keys and Chords, Guitar Fingerings, Audio Playback, and Command Line.
Adds installation notes for PortAudio, shows from_frequency/from_midi,
enharmonics, Key class, Chord convenience constructors, tab output,
WAV export, and CLI commands.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-22 20:59:55 -04:00
kennethreitz d11c930308 Fix key_explorer.py: borrowed_chords returns strings not Chords
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-22 20:56:44 -04:00
kennethreitz 19663ed6c5 Fix world_scales.py: correct gamelan tonic and scale names
Gamelan uses its own tone names (nem, pi, barang, etc.), not Western
note names. Fixed tonic from C4 to nem4 and added pelog nem/barang
modes. Replaced miyako-bushi with iwato and kumoi (actual scale names
in the system). Added ValueError to exception handling.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-22 20:55:52 -04:00
kennethreitz f949ca5b45 Show version number in docs sidebar via extra_nav_links
Links to PyPI page for the current version.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-22 20:45:28 -04:00
kennethreitz d9f847603a Show version in docs sidebar and switch to GitHub star button
Version is now pulled from pytheory.__version__ instead of hardcoded.
GitHub button changed from watch to star with count.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-22 20:43:25 -04:00
kennethreitz ee41691728 Skip play module tests when PortAudio is not available
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-22 20:41:39 -04:00
kennethreitz 02df87af09 v0.6.1
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-22 20:39:48 -04:00
kennethreitz b3110c6e0e Fix TODOs, add WAV export, CLI tests, and play module tests
Play module:
- Add save() for WAV file export
- Add _render() to separate rendering from playback
- Expand play() docstring with args and examples
- Add comments to SAMPLE_RATE/SAMPLE_PEAK constants
- Remove orphaned comment at EOF

Code cleanup:
- Remove 3 TODO comments (_statics.py, systems.py, scales.py)
- Remove commented-out incomplete scale definitions
- Fix silent return in TonedScale.get() to explicit return None

Tests (15 new):
- CLI: tone, scale, chord, key, fingering, progression, detect commands
- Play: _render for tones and chords, all synth engines, WAV save

README:
- Add CLI section with usage examples
- Mention WAV export in audio and features sections

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-22 20:39:12 -04:00
kennethreitz fd82dccbfd Fix sawtooth and triangle wave generation
Both were using incorrect frequency scaling (magic numbers instead of
deriving cycle length from sample rate / hz). Now they match the sine
wave approach: compute one cycle at the correct frequency, then resize.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-22 20:31:36 -04:00
kennethreitz 6f7f9008b0 Use sine wave for chord progression playback
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-22 20:30:55 -04:00
kennethreitz acb92171a1 Speed up songs (BPM 120→180) and catch KeyboardInterrupt
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-22 20:30:04 -04:00
kennethreitz c006f5b3da Rewrite song.py with 7 playable songs and chord progressions
Melodies: Twinkle Twinkle, Ode to Joy, Happy Birthday, Fur Elise
Progressions: Pop I-V-vi-IV, 12-bar blues in A, Jazz ii-V-I turnaround
Interactive menu for picking songs. Clean helper functions for
melody and chord progression playback.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-22 20:28:31 -04:00
kennethreitz 9da3ac8b28 Add 12 example scripts showcasing pytheory features
- circle_of_fifths.py — visualize keys around the circle
- chord_identifier.py — identify chords from notes and fingerings
- key_explorer.py — explore keys, signatures, progressions, borrowed chords
- temperament_comparison.py — compare equal, Pythagorean, and meantone
- chord_tension.py — analyze tension, consonance, and voice leading
- world_scales.py — scales from 6 musical traditions
- fretboard_explorer.py — instruments, tunings, capo transposition
- midi_converter.py — MIDI ↔ note ↔ frequency reference
- progression_writer.py — famous progressions, Nashville numbers, random generation
- interval_trainer.py — interval names, songs, and consonance ranking
- overtone_series.py — harmonics and why chords sound good
- key_detection.py — detect keys from melodies and chord progressions

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-22 20:27:18 -04:00
kennethreitz e94ef5dcfd Expand documentation with undocumented features and CLI guide
Tones: add from_frequency, from_midi, letter, midi, exists properties;
  interval naming with interval_to(); transpose(); MIDI section
Scales: add Key.signature, relative/parallel keys, borrowed chords,
  secondary dominants, random progressions, all_keys, scale transpose
Chords: add transpose, add_tone/remove_tone, root/quality properties;
  simplify identification examples with from_tones()
CLI: new guide covering all 8 commands (tone, scale, chord, key,
  fingering, progression, play, detect)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-22 20:11:01 -04:00
kennethreitz a5e47c37cd v0.6.0
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-22 20:04:00 -04:00
kennethreitz 8a9651f989 Add tests for flat note name support
14 tests covering: flat tone creation, frequency matching with sharp
equivalents, all enharmonic pairs, arithmetic, intervals, exists
property, index resolution, chords built from flats, and
System.resolve_name().

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-22 20:02:26 -04:00
kennethreitz cc4a25e70d Support flat note names (Db, Bb, Eb, etc.) throughout the system
Flat names are now resolved to their canonical sharp equivalents when
looking up tones in a system. This means Tone.from_string("Db4") now
works for frequency, arithmetic, intervals, and chord building —
previously it raised a ValueError.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-22 20:01:11 -04:00
kennethreitz 904c61b2d6 Show enharmonic property in tones docs instead of from_tuple
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-22 19:59:30 -04:00
kennethreitz d23de92713 Update docs to use newer APIs (Key, Fingering, convenience constructors)
- Circle of fifths: use tone.circle_of_fifths() instead of manual loop
- Fingerings: show labeled Fingering class with string names, identify()
- Chords: document from_tones(), from_name(), from_intervals(), from_midi_message()
- Scales: add Key class, Key.detect(), Key.progression(), nashville()
- Playback: simplify examples with Chord.from_name()
- README: add Keys section, update fingering output format
- Quickstart: add chord identification from fret positions

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-22 19:57:06 -04:00
kennethreitz e8bfeb884a Add Fingering class for labeled chord fingerings (#25)
Replace plain tuples from fingering() methods with a Fingering object
that labels each fret position with its string name, supporting both
named (f['A']) and index (f[1]) access while remaining backward
compatible with tuple equality.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-22 16:11:37 -04:00
kennethreitz 6aad427fb8 Fix 'pytheory play' chord name parsing for names containing digits
Chord names like Cmaj7 and G7 were incorrectly treated as tone names
because they contain digits. Now tries chord name lookup first. v0.5.1.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-22 14:53:12 -04:00
kennethreitz e9c630705e Add 'pytheory play' CLI command for playing notes and chords
Supports single tones and chords, with --synth (sine/saw/triangle),
--duration, and --temperament flags. Bumps version to v0.5.0.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-22 14:47:20 -04:00
kennethreitz e78ba203d9 Add Symbolic Pitch section to tones docs
Dedicated section explaining symbolic=True with examples across
all three temperaments, showing exact SymPy expressions, arbitrary
precision evaluation, and why the math reveals temperament differences.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-22 13:40:20 -04:00
kennethreitz c307c1e41f v0.4.1
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-22 13:34:04 -04:00
kennethreitz b1f6996cd7 Add --temperament flag to CLI tone command
pytheory tone C5 -t pythagorean
pytheory tone A4 -t meantone

Shows frequency in chosen temperament and difference in cents
from equal temperament. Supports equal, pythagorean, meantone.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-22 13:08:24 -04:00
kennethreitz 51ca98779d Add CLI tool and Jupyter notebook tutorial
CLI (pytheory command):
  pytheory tone C4          — frequency, MIDI, overtones
  pytheory scale C major    — notes and intervals
  pytheory chord C E G      — identify, harmony, tension
  pytheory key C major      — full key analysis with diatonic chords
  pytheory fingering Am     — ASCII guitar tab
  pytheory progression C major I V vi IV — build from Roman numerals
  pytheory detect C D E G   — detect the key

Jupyter notebook (examples/tutorial.ipynb):
  46-cell interactive tutorial covering tones, scales, modes, keys,
  chord analysis, progressions, world music systems, guitar fingerings,
  and building a song from scratch.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-22 13:05:33 -04:00
kennethreitz 37b41e1bbf Improve test coverage from 93% to 97% (476 tests)
Added 33 targeted tests covering:
- Tone: NotImplemented returns on comparison operators, negative
  frequency error, compound intervals, circle methods, octave
  parsing, enharmonic edge cases
- Chord: unidentified chord repr/str, __add__ NotImplemented,
  voice leading with different sizes, analyze with Tone key,
  diminished/augmented/9th analysis
- Scale: system object constructor, mode name degree lookup,
  KeyError on bad degree
- Key: string system param, flat key signatures, borrowed chords
  for minor, parallel/relative None returns
- Fretboard: fingering method returns Chord
- Charts: fix_fingering muted string

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-22 12:53:06 -04:00
kennethreitz da40189845 v0.4.0: key signatures, scale diagrams, chord building, progression analysis
New features:
- Key.signature — sharps/flats count and accidental names
- Key.borrowed_chords — modal interchange from parallel key
- Key.random_progression(n) — weighted random diatonic progressions
- Chord.from_intervals("C", 4, 7) — build from root + semitones
- Chord.from_midi_message(60, 64, 67) — build from MIDI note numbers
- Chord.add_tone(tone) / remove_tone("B") — modify chords immutably
- Tone.letter — "C" from "C#" (letter without accidental)
- Fretboard.scale_diagram(scale) — ASCII neck diagram
- analyze_progression([chords], key="C") → ["I", "vi", "IV", "V"]

443 tests.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-22 12:48:04 -04:00
139 changed files with 34792 additions and 1063 deletions
+693
View File
@@ -0,0 +1,693 @@
# Changelog
All notable changes to PyTheory are documented here.
## 0.40.1
- **Singing bowl synth** — two variants: strike (mallet hit with chirp
and long decay) and ring (rim-rubbed sustained tone with slow build).
Inharmonic partials beat against near-degenerate mode pairs for
authentic Himalayan bowl shimmer.
- `singing_bowl` and `singing_bowl_ring` instrument presets
- Audio demos in docs for both variants
## 0.40.0
- **Rhodes electric piano synth** — tine + tonebar + electromagnetic
pickup model. `electric_piano` preset now uses dedicated `rhodes_synth`
instead of FM
- **73 audio demos in docs** — every synth, every drum pattern, every
code example with `play_score()` now has an embedded audio player
- Idiomatic demos: harp arpeggiates, guitars strum, cello bows, sitar
drones, strings use ensemble
- Trailing silence trimming on all audio exports
- Raw waveform demos (no envelope) for classic waveforms
## 0.39.3
- **33 audio samples in documentation** — every `play_score()` example
now has an embedded stereo audio player. Covers quickstart, sequencing,
drums (all world percussion), playback, and cookbook.
- **`docs/generate_audio.py`** — renders all doc examples to WAV
- Numpy vectorization: cached time arrays, decay envelopes, drum hits;
vectorized piano harmonic synthesis
- Fixed acid legato example (removed pad envelope, added proper 303 recipe)
## 0.39.2
- **Marching percussion** — snare, rimshot, and stick click sounds with
high-tension kevlar synthesis and woody-metallic rimshot crack
- **`Part.flam()`**, **`Part.diddle()`**, **`Part.cheese()`** — marching
rudiment methods for any drum sound
- **`Part ensemble=`** — duplicate voices with per-player timing tendencies
and micro pitch drift. Works on any Part (drumline, string section, choir).
`ensemble=20` for a full snare line, `ensemble=4` for a string quartet.
- **Sympathetic resonance** — marching snare buzz builds up with repeated
hits, decays during rests (like real snare wire response)
- **4 marching patterns** — march, cadence, paradiddle, roll
- **Chakradar tabla pattern** — 16-beat tihai of tihais composition
- Song #32: Snare Cadence (flams, diddles, cheese, triplets, 32nds)
## 0.39.1
- **Chakradar tabla pattern** — 16-beat tihai of tihais composition with
3 escalating phrases and a crescendo triplet finale
## 0.39.0
- **Dropped `numeral` dependency** — Roman numeral helpers inlined,
reducing supply chain surface (#47)
- **`Part.ramp()`** — smooth parameter automation with 4 interpolation
curves (linear, ease_in, ease_out, ease_in_out)
- **Articulations** — staccato, legato, marcato, tenuto, accent, fermata
- **Dynamic curves** — crescendo(), decrescendo(), swell(), dynamics()
- **`Part.hit()`** — individual drum sounds with articulation support
- **Cross-choke drum damping** — djembe, hi-hats, cajón, doumbek
- **5 new djembe patterns** + 3 djembe fills (30 fills total)
- **6 new drum fills** — 3 cajón, 3 metal
- **Duration arithmetic** — multiply, divide, add
- **Improved djembe slap** synthesis
- Song #31: Acid Tabla
## 0.38.2
- **`Part.ramp()`** — smooth parameter automation from current value to
target over a duration. Works for lowpass, reverb, distortion, chorus,
delay, volume, and any `.set()` parameter. Four interpolation curves:
linear, ease_in, ease_out, ease_in_out.
## 0.38.1
- **Dynamic curves** — `Part.crescendo()`, `Part.decrescendo()`,
`Part.swell()`, and `Part.dynamics()` for velocity ramps and custom
curves across a sequence of notes
## 0.38.0
- **Articulations** — `staccato`, `legato`, `marcato`, `tenuto`, `accent`,
`fermata` via `articulation=` on `Part.add()` and `Part.hold()`
- **`Part.hit()`** — place individual drum sounds in a Part's note stream
with articulation, velocity, and effects support
- **5 new djembe patterns** — dununba, tiriba, yankadi, djansa, mendiani
- **3 new djembe fills** — djembe call, djembe roll, djembe break (30 fills total)
- **Cross-choke drum damping** — striking one sound fades out related sounds
(djembe, hi-hats, cajón, doumbek)
- **Improved djembe slap** — dry goatskin pop instead of snare-like noise
## 0.37.0
- **5 new djembe patterns** — dununba, tiriba, yankadi, djansa, mendiani
- **3 new djembe fills** — djembe call, djembe roll, djembe break (30 fills total)
- **Cross-choke drum damping** — striking one sound on a hand drum fades
out the ring of related sounds (djembe slap kills bass resonance, closed
hat chokes open hat, cajón slap dampens bass, doumbek tek dampens dum)
- **Improved djembe slap** — dry, high-pitched goatskin pop instead of
snare-like noise rattle
## 0.36.6
- **6 new drum fills** — 3 cajón (flam, rumble, breakdown) and 3 metal
(triplet, blast, cascade). 27 fills total.
- Updated drums documentation with fill lists and examples
## 0.36.5
- **Duration arithmetic** — `Duration.WHOLE * 2`, `Duration.HALF + Duration.QUARTER`,
division, and reverse multiply all work now (previously raised TypeError)
## 0.36.3
- **`Part.hold()`** — polyphonic overlap on a single part. Add notes
without advancing the beat position so they play simultaneously.
Enables: piano sustain, sitar drone under melody, guitar strum texture.
- **Strum uses hold()** — leading string plays simultaneously with chord,
no more timing gaps or choppiness
- **Improved songs** 1-16: humanize, velocity dynamics, reverb, saxophone
for blues
- **Ctrl-C handling** — clean stop on all playback functions
- **REPL updates** — strum, roll, bend, temperament, reference commands
- Song #28 Descent (generative), #29 Pop Rock, #30 Sitar Drone
- 862 tests
## 0.36.1
- **7 new instrument synths:** pedal steel guitar, theremin, kalimba/thumb
piano, steel drum/pan, accordion (musette reeds), didgeridoo (drone +
shifting formants), bagpipes (chanter reed)
- **9 new demo moods** in ``pytheory demo``: Theremin Noir, Caribbean,
Accordion Waltz, Kalimba Dreams, Outback Drone, Highland, Nashville
Tears, Tabla Fusion
- Improved existing songs with dedicated instrument synths
- 41 synth waveforms, 26+ songs, 21 demo moods
## 0.36.0
- **Banjo synth** — steel strings on drum-head body, nasal twang,
fast decay with membrane resonance
- **Mandolin synth** — paired steel strings (natural chorus from
doubled courses), bright body resonance
- **Ukulele synth** — nylon strings, small mid-heavy body, shorter
sustain than guitar
- **Cajón drums** — bass (woody box thump), slap (snare wire buzz),
tap (ghost note). 3 patterns: cajon, cajon rumba, cajon folk
- **Vocal/formant synth** — LF glottal model, 5 Peterson & Barney
formant peaks, jitter/shimmer, consonant onsets, per-note lyrics.
Presets: vocal, choir
- **Granular synthesis** — grain cloud engine with scatter, pitch
variation, Hanning windows. Presets: granular_pad, granular_texture
- **Strum sweep** — subtle grace notes before chord hit for natural
strum feel on all fretboard instruments
- Mandola preset, 34 synth waveforms, 26 songs
## 0.35.0
- **8.5x faster import** — dropped pytuning/sympy, lazy-load scipy.
`import pytheory` now takes ~50ms instead of ~480ms (#44)
- **Proper shruti JI ratios** — 22 positions with 5-limit just intonation
(pure 3/2 fifths, 5/4 thirds), not 22-TET approximation
- **Arabic maqam JI ratios** — Zalzalian 11-limit ratios.
Mi↓ (the Rast third) is exactly 27/22 from Do
- **B#/Cb octave boundary fix** — B#4 = C5, Cb4 = B3 (#45)
- **Int tone names** — `Tone(0, system=TET(22))` works alongside strings.
Wrapping: `Tone(22)` → tone 0, octave+1. `System.tone()` convenience.
- **Timpani synth** — inharmonic membrane modes, felt mallet, copper kettle
resonance, cathedral reverb
- **Saxophone synth** — conical bore, reed buzz, brass body warmth.
4 presets: saxophone, alto_sax, tenor_sax, bari_sax
- **Part.roll()** — rapid repeated notes with velocity ramp for crescendo/
decrescendo rolls on any instrument
- **Vibrato tuning** — all instruments reduced to 0.001 depth for cleaner
ensemble sound
- **Granular synthesis** — grain cloud engine with scatter, pitch
variation, and Hanning-windowed grains. Two presets: granular_pad,
granular_texture.
- 30 synth waveforms, 838 tests
## 0.34.0
- **16 dedicated instrument synths** — physical modeling and specialized
synthesis for: piano (hammer + steel strings + soundboard), bass guitar
(thick KS + pickup), flute (breath + tube resonance), trumpet (lip buzz
+ bell), clarinet (odd harmonics + reed), oboe (double reed + conical
bore), marimba (inharmonic bar modes), harpsichord (quill pluck),
cello (deep bowed + body), harp (soft pluck + soundboard bloom),
upright bass (pizzicato + wooden body), acoustic guitar (KS + body
resonance), electric guitar (KS + pickup comb filter), sitar (jawari
+ chikari), plus organ and bowed strings
- **Speaker cabinet simulation** — tames distorted guitar fizz
- **Guitar strumming** — `Part.strum("Am")` with fretboard lookup
- **Analog oscillator drift** — subtle per-note pitch wobble on synth presets
- **World percussion:** dhol, dholak, mridangam, djembe, metal kit
with 22 new drum patterns
- **Piano improvements:** brightness scales with pitch, two-stage decay,
hammer impact with felt character
- **Vibrato tuning:** reduced across flute, oboe, trumpet, cello for
smoother ensemble sound
- 27 synth waveforms, 10 envelopes, 40+ instrument presets, 80+ drum patterns
## 0.33.1
- **Electric guitar synth** — Karplus-Strong with magnetic pickup comb filter
simulation (single-coil honk, proper sustain)
- **Speaker cabinet simulation** — steep rolloff above 4-5kHz with presence
bump. Makes distorted guitar sound warm instead of fizzy.
- **6 guitar presets:** electric_guitar, clean_guitar, crunch_guitar,
distorted_guitar, orange_crunch, metal_guitar — all with proper cab sim
- **Sitar synth** — Karplus-Strong with jawari bridge buzz, chikari
sympathetic strings, variable damping
- **Guitar strumming** — `Part.strum("Am", Duration.HALF)` with
fretboard fingering lookup, down/up direction, adjustable strum speed
- **World drums:** dhol (bhangra, chaal), dholak (qawwali, folk),
mridangam (adi talam, korvai), djembe (standard, kuku, soli)
— all with bandpass-filtered membrane noise for realistic drum head sound
- **Metal drum kit** — clicky kick, bright snare, tight hats
with 4 patterns (double kick, metal blast, metal groove, metal gallop)
- 15 synth waveforms, 10 envelopes, 40+ instrument presets
## 0.33.0
- **Non-12-TET support** — `TET(n)` factory creates any equal temperament
- **11 microtonal systems:**
- `"shruti"` (22-TET Indian, 10 thaats with proper shruti intervals)
- `"maqam"` (24-TET Arabic, quarter-tone Rast/Bayati/Hijaz + 7 more)
- `"slendro"` (5-TET gamelan), `"pelog"` (9-TET gamelan with 3 pathet)
- `"thai"` (7-TET, 171 cents/step)
- `"makam"` (53-TET Turkish Arel-Ezgi-Uzdilek, 9 makams)
- `"carnatic"` (72-TET, 10 melakartas)
- `"19-tet"`, `"31-tet"` (historical Western)
- `"bohlen-pierce"` (13 divisions of the tritave 3:1 — non-octave!)
- **Just intonation** — `temperament="just"` for pure 5-limit ratios
- **Historical pitch** — `Score(reference_pitch=415.0)` for Baroque A=415
- **`Score(system=, temperament=, reference_pitch=)`** flows through to all playback
- Per-system `c_index` and `period` replace hardcoded constants
- Fixed all hardcoded `12`s in tone arithmetic
- Song #22: Greensleeves (Renaissance lute, meantone, A=415)
- 22 new microtonal tests (819 total)
## 0.32.1
- `Tone("X")` now raises `ValueError` immediately instead of silently accepting invalid names (#39)
- Support enharmonic spellings: `Cb`, `Fb`, `E#`, `B#` resolve correctly (#40)
- Support double sharps (`C##`, `Fx`) and double flats (`Dbb`) via semitone arithmetic (#41)
- Accept unicode music symbols: `♯` `♭` `𝄪` `𝄫`
## 0.32.0
- **8 new synth engine features:**
- Filter envelope: per-note lowpass sweep (`filter_amount`, `filter_attack`, `filter_decay`, `filter_sustain`)
- Velocity → brightness: harder notes = brighter filter (`vel_to_filter`)
- Sub-oscillator: octave-below sine for bass weight (`sub_osc`)
- Tremolo: amplitude LFO modulation (`tremolo_depth`, `tremolo_rate`)
- Saturation: even-harmonic tape/tube warmth (`saturation`)
- Noise layer: per-note breath/air texture (`noise_mix`)
- Phaser: swept allpass filter chain (`phaser`, `phaser_rate`)
- Configurable FM: `fm_ratio` and `fm_index` params
- **Highpass filter** (12 dB/oct biquad) on any part
- **2 new envelopes:** `bowed` (bow attack with sustain), `mallet` (strike with ringing sustain)
- **Improved `strings_synth`:** additive synthesis with body resonance curve, per-harmonic phase randomization, delayed vibrato onset, bow pressure variation
- **Instrument preset overhaul:** every preset sanity-checked against real instrument behavior
- Mallet instruments (vibraphone, celesta, music box, glockenspiel, tubular bells) now ring properly
- Trumpet uses sustaining envelope instead of pluck
- Woodwinds have breath noise, brass has velocity brightness
- Bass instruments have sub-oscillators, synth presets have filter envelopes
- Piano has velocity-to-brightness and subtle hammer noise
- Signal chain: saturation → tremolo → distortion → chorus → phaser → highpass → lowpass → delay → reverb
- Song #21: Cinematic Showcase (Orchestral)
## 0.31.0
- 3 new synth engines: Karplus-Strong pluck, Hammond organ, string ensemble with body formants
- 38 instrument presets: `score.part("lead", instrument="violin")`
- Keys, strings, woodwinds, brass, plucked, synth, and mallet categories
- 13 total synth waveforms
## 0.30.0
- Drums are a real Part — same effects pipeline as any voice
- `score.drums("rock", split=True)` splits kit into kick/snare/hats/toms/cymbals/percussion Parts
- Each split Part gets independent effects (reverb on snare, LP on hats, etc.)
- `set_drum_effects()` applies to all drum Parts (split or not)
- Sidechain triggers on kick only — hats and snare don't duck the pad
- MIDI import via `Score.from_midi(path)`
## 0.29.3
- Drums are now a real Part — same effects pipeline as any other voice, zero code duplication
- `score.parts["drums"]` is a standard Part with reverb, delay, lowpass, etc.
- `set_drum_effects()` is sugar over the Part's attributes
## 0.29.2
- Add `score.set_drum_effects()` — reverb, delay, lowpass, distortion, chorus on the drum bus
- Same effects engine as parts, zero code duplication
## 0.29.1
- Rename song.py → songs.py
- Polish all 20 example songs with stereo, convolution reverb, humanize, detune, sidechain
## 0.29.0
- Add `Score.from_midi(path)` — import any Standard MIDI File into a Score
- Minimal zero-dependency MIDI parser (Type 0 and Type 1)
- Each channel becomes a named Part, channel 10 becomes drum hits
- Tempo, time signature, velocities, and note durations preserved
- Roundtrip: save_midi → from_midi works
## 0.28.3
- Rewrite `pytheory demo` — 8 moods with stereo, effects, humanize, convolution reverb, sidechain
- Added Dub and Temple moods
## 0.28.2
- Lower drum_humanize default to 0.15 — tighter, more professional feel
## 0.28.1
- Humanize drum hits — random timing jitter and velocity variation (default 0.3)
- Control via `Score(drum_humanize=0.5)` — 0.0 = quantized, 0.3 = natural, 0.5+ = loose
## 0.28.0
- Add figured bass notation: `Chord.figured_bass` and `Chord.analyze_figured()` for classical inversion symbols
- Add pitch class set theory: `pitch_classes`, `normal_form`, `prime_form`, `forte_number` on Chord
- Add `Scale.recommend()` — ranked scale suggestions for a set of notes
- Forte number catalog covers all trichords and tetrachords
## 0.27.1
- Tab completion in REPL — context-aware for commands, drum presets, synths, envelopes, chords, notes, systems
## 0.27.0
- Rewrite all 15 drum sounds for higher quality (inharmonic partials, proper transients, multi-mode resonance, saturation)
- 19 example songs including Dance Party at the Reitz House
## 0.26.3
- Stereo drum panning — each sound placed in the stereo field (hat right, crash left, toms spread, kick/snare center)
- Stereo convolution reverb — different IR per L/R channel for all 7 presets
- 2 new songs: Neon Grid (stereo acid), Glass and Silk (sine+triangle waltz)
## 0.26.2
- Stereo convolution reverb — different IR per L/R channel for all 7 presets
- Both algorithmic and convolution reverbs now output true stereo
## 0.26.1
- Stereo reverb — L and R channels get different early reflection patterns for natural width
- Effects chain now skips mono reverb in favor of stereo reverb in the mixer
## 0.26.0
- **Stereo output** — render_score() now returns stereo (N, 2) arrays
- Add `pan` parameter: -1.0 (left) to 1.0 (right), constant-power panning
- Add `spread` parameter: detuned oscillators spread across L/R channels
- Master bus compressor runs per-channel for stereo
- All playback functions handle stereo natively
## 0.25.7
- Add `detune` parameter — ±cents oscillator spread on any synth (3 oscillators per note)
- Swing now applies to drum hits (offbeats shift with the groove)
- Improved snare and hi-hat sounds (metallic harmonics, faster attack)
## 0.25.6
- Swing now applies to drum hits — offbeats shift with the groove, everything locks into the same pocket
- Improved snare: 220Hz body, transient click, tanh saturation
- Improved hi-hats: metallic harmonics (6k+8.5k+12k Hz), crisper attack, shorter decay
## 0.25.5
- Improved snare: 220Hz body, transient click, tanh saturation — snappier and more present
- Improved hi-hats: metallic harmonics (6k+8.5k+12k Hz), shorter decay, crisper attack
## 0.25.4
- Add master bus compressor/limiter — louder, punchier, more cohesive mixes
- Feed-forward compression with configurable threshold, ratio, attack, release
- Makeup gain restores loudness after compression
- Brick-wall limiter at 0.95 prevents clipping
- Replaces simple normalization in render_score()
## 0.25.3
- Add `pytheory repl` — interactive music theory scratchpad and composition tool
- Context-aware prompt shows key, bpm, drums, active part + effects
- Theory commands: key, chords, modes, scales, circle, interval, identify, system
- Composition: drums, part, add, rest, arp, prog, effects, automation, LFO
- Guitar: fingering, scale diagram
- 6 musical systems with correct default tonics
- REPL guide documentation
## 0.25.1
- Add `pytheory demo` CLI command — plays a randomly generated track, different every time
- Rewrite README to showcase the full feature set (composition, effects, drums, MIDI export)
## 0.25.0
- Add sidechain compression — kick ducks pad/bass for the classic EDM pump effect
- Add song structure: `score.section("verse")`, `score.section("chorus")`, `score.repeat("verse")`
- Punchier kick drum: 808-style with faster pitch sweep (200→45Hz), sub thump, and soft saturation
- Section repeat copies all part notes, drum hits, and automation with proper offset
## 0.24.1
- Add `humanize` parameter on Parts — random micro-timing and velocity variation
- Makes programmed parts feel like a real player (0.1 = subtle, 0.3 = natural, 0.5+ = loose)
## 0.24.0
- Add per-note velocity: `lead.add("C5", Duration.QUARTER, velocity=90)` — dynamics, accents, ghost notes
- Add swing/groove: `Score("4/4", bpm=120, swing=0.5)` — shuffles every other note for human feel
- Add tempo changes mid-song: `score.set_tempo(140)` — accelerando, ritardando, tempo drops
- Add `Part.fade_in(bars)` and `Part.fade_out(bars)` — volume envelopes over sections
- Arpeggiator supports velocity parameter
- Per-part swing override (set independently from score swing)
- Tempo map engine: beat-to-sample conversion handles variable BPM throughout a score
## 0.23.0
- Add convolution reverb with 7 synthetic impulse responses: Taj Mahal, cathedral, plate, spring, cave, parking garage, canyon
- Each IR models real acoustic properties: early reflections, frequency-dependent absorption, diffusion density, and modulation
- FFT-based convolution via `scipy.signal.fftconvolve` for fast processing even with long tails (12s Taj Mahal)
- Select via `reverb_type` parameter on `Score.part()` — drop-in alongside existing algorithmic reverb
- IR cache for zero-cost reuse across parts
- Automatable via `Part.set(reverb_type="cathedral")` mid-song
## 0.22.0
- Add `Part.lfo()` for automated parameter modulation (filter sweeps, tremolo, auto-wah)
- 4 LFO shapes: sine, triangle, saw, square
- Configurable rate (cycles per bar), min/max range, duration, and resolution
- Stack multiple LFOs on different parameters for complex modulation
## 0.21.0
- Add `Part.set()` for mid-song effect automation (filter sweeps, reverb swells, distortion kicks)
- Add chorus effect (LFO-modulated delay, Juno-style)
- Renderer segments audio at automation points for per-section effect processing
- Updated effect chain: distortion → chorus → lowpass → delay → reverb
- Document automation, chorus, and updated signal chain
## 0.20.0
- Add `Part.arpeggio()` — arpeggiator with up/down/updown/downup/random patterns, octave spanning
- Fix Roman numeral parser to handle flat/sharp degree prefixes (bVI, bVII, bIII, #IV)
- Add `song_showoff.py` — generative composition that's different every time, uses every feature
- 4 mood palettes (dark, bright, ethereal, aggressive) with matched keys, progressions, drums, and effects
## 0.19.1
- Add `Part.arpeggio()` — arpeggiator with up/down/updown/downup/random patterns, octave spanning, and division control
- Arpeggiator chains with legato + glide for classic acid/trance sequencer sound
- Rename rhythm docs to "Sequencing: Rhythm and Scores"
- Document arpeggiator, legato, and glide in rhythm guide
## 0.19.0
- Add legato mode for parts — continuous waveform without retriggering envelope per note
- Add glide/portamento — smooth pitch slides between consecutive notes (303-style)
- Legato renders entire phrase as one oscillator with phase-accumulating frequency changes
- Glide uses exponential interpolation for perceptually linear pitch slides
## 0.18.1
- Add distortion effect (tanh soft-clip waveshaping) with drive and mix controls
- 3 new example songs: Dub Delay Madness (separate delay snare), Liquid DnB (174bpm), Late Night Texts (Drake-style trap)
- 16 total songs in the song player
## 0.18.0
- Add per-part audio effects: reverb, delay, and lowpass filter
- Reverb: Schroeder algorithm with configurable mix and decay
- Delay: tempo-synced echoes with feedback control
- Lowpass: 12 dB/octave biquad filter with resonance (Q) control
- All effects set at part creation: `score.part("lead", reverb=0.3, delay=0.25, lowpass=2000, lowpass_q=1.5)`
- Effects applied per-part before mixing for independent processing
## 0.17.0
- Add 10 new groove presets: country, ska, dub, jungle, techno, gospel, swing, bolero, tango, flamenco (58 total)
- Add 10 new fill presets: reggae, afrobeat, bossa nova, house, trap, hip hop, disco, cumbia, highlife, second line (21 total)
- Every major genre family now has matching groove + fill presets
## 0.16.0
- Add drum fill system with 11 genre-specific presets: rock, rock crash, jazz, jazz brush, salsa, samba, funk, metal, blast, buildup, breakdown
- `Pattern.fill("rock")` returns a 1-bar fill pattern
- `Score.fill("rock")` inserts a fill at the current position
- `Score.drums("rock", repeats=8, fill="rock", fill_every=4)` auto-fills every Nth bar
- Without `fill_every`, fill replaces only the last bar
## 0.15.1
- Add `Synth.PWM_SLOW` and `Synth.PWM_FAST` — pulse width modulation with LFO sweep (Juno-style pads)
- Add `Score.drums()` shorthand for `score.add_pattern(Pattern.preset(...), repeats=...)`
- Update all docs to use `score.drums()` syntax and document all 10 synth waveforms
## 0.15.0
- Add 5 new synth waveforms: `Synth.SQUARE`, `Synth.PULSE`, `Synth.FM`, `Synth.NOISE`, `Synth.SUPERSAW`
- Square wave: classic chiptune / 8-bit sound (odd harmonics at 1/n)
- Pulse wave: variable duty cycle for NES-style timbres (25%, 12.5%)
- FM synthesis: DX7-style frequency modulation (electric piano, bells, brass, metallic)
- Noise: white noise for percussion textures and effects
- Supersaw: 7 detuned saw oscillators for trance/EDM pads
- All 8 synths available in both the API (`Synth.FM`) and Part strings (`synth="fm"`)
- CLI play command supports all 8 waveforms
## 0.14.0
- Add `Part` class for multi-voice Score arrangements (lead, bass, pads, etc.)
- `Score.part()` creates named parts with independent synth, envelope, and volume
- `Score.add_pattern()` for attaching drum patterns
- `render_score()` exported for headless buffer rendering
- Parts accept raw float beat values alongside `Duration` enums
- All 10 example songs rewritten with drums + chords + lead + bass parts
## 0.13.1
- Fix drum pattern repeats: hits now correctly offset across cycles instead of piling up on the first bar
## 0.13.0
- Add drum synthesizer with 27 individual instrument voices (kick, snare, hat, conga, timbale, etc.)
- Add `play_pattern()` for playing drum patterns through the speakers
- Add `play_score()` for playing mixed drum patterns + chord progressions together
- Every `DrumSound` has a dedicated synthesis algorithm (pitch sweeps, noise bursts, membrane resonance, metallic rings)
## 0.12.0
- Add rhythm module: `Duration`, `TimeSignature`, `Note`, `Rest`, `Score`
- `Duration` enum with 8 note lengths (whole through sixteenth, dotted, triplet)
- `TimeSignature` with string parsing ("4/4", "3/4", "6/8", "12/8") and beats_per_measure
- `Score` class with fluent `.add()` / `.rest()` chaining, measure counting, and `save_midi()` export
- Measure-aware MIDI export with proper time signature and tempo meta events
- Add `DrumSound` enum with 27 General MIDI percussion sounds
- Add `Pattern` class with 48 drum pattern presets covering:
- **Rock/Pop**: rock, half time, double time, disco, motown, train beat
- **Jazz**: jazz, bebop, shuffle, linear, paradiddle
- **Latin**: salsa, bossa nova, samba, cumbia, merengue, baiao, maracatu
- **Afro-Cuban**: son clave 3-2/2-3, rumba clave 3-2/2-3, cascara, guaguanco, mozambique, nanigo, bembe, 6/8 afro-cuban, tresillo, habanera
- **African**: afrobeat, highlife
- **Caribbean**: reggae, dancehall
- **Electronic**: house, trap, drum and bass, breakbeat
- **Metal/Punk**: metal, blast beat, punk
- **Other**: funk, hip hop, bo diddley, second line, new orleans, waltz, 12/8 blues
- `Pattern.to_score()` renders drum patterns to Score for MIDI export
## 0.11.0
- Add drop voicings: `Chord.close_voicing()`, `Chord.open_voicing()`, `Chord.drop2()`, `Chord.drop3()`
- Add `Key.modulation_path(target)` for chord-by-chord modulation suggestions via pivot chords
- Add `Scale.degree_name(n)` returning traditional names (tonic, dominant, leading tone, etc.)
- Add `Chord.extensions()` to suggest available 9th/11th/13th extensions
- Add `Tone.solfege` property for fixed-Do solfege syllables (Do, Re, Mi, Fi, etc.)
- Add CLI `identify` command for full chord analysis from a symbol
- Add CLI `midi` command for exporting progressions to Standard MIDI Files
- Expand documentation: solfege, Helmholtz, cents, slash chords, drop voicings, chord extensions, borrowed chord analysis, ADSR envelopes, MIDI export, new CLI commands
## 0.10.0
- Add `Scale.fitness()` to score how well a set of notes fits a scale (0.01.0)
- Add `Key.suggest_next(chord)` for chord progression suggestions based on functional harmony
- Add `Tone.helmholtz` and `Tone.scientific` properties for alternate pitch notation
- Add `Chord.slash(bass)` and `Chord.slash_name` for slash chord notation (C/G, Am/E)
- Add `save_midi()` for exporting tones, chords, and progressions as Standard MIDI Files
- Add chord tone highlighting in `Fretboard.scale_diagram()` — chord tones uppercase, passing tones lowercase
- Extend `Chord.analyze()` to recognize borrowed chords (bVI, bVII, bIII, etc.)
## 0.9.0
- Add ADSR envelope system with 8 presets: `Envelope.PIANO`, `ORGAN`, `PLUCK`, `PAD`, `STRINGS`, `BELL`, `STACCATO`, `NONE`
- Add `Chord.from_symbol()` parser — handles any standard chord symbol (e.g. "F#m7b5", "Bbmaj9", "Gsus4") without lookup tables
- Add `Key.pivot_chords(target)` for finding modulation pivot chords between two keys
- Add `Scale.parallel_modes()` to show all modes sharing the same notes (C major → D dorian, E phrygian, etc.)
- Add `Tone.cents_difference(other)` for measuring fine pitch differences in cents
- Add `--envelope` flag to CLI play command
- CLI play command now uses `Chord.from_symbol()` for broader chord parsing
- Replace hardcoded `c_index = 3` with named `C_INDEX` constant throughout
## 0.8.3
- Add `Chord.symbol` property for standard shorthand notation (Cmaj7, Dm, G7, m7b5, etc.)
- Add `Key.common_progressions()` to realize all named progressions in a key
- Add CLI commands: `modes`, `circle`, `progressions`
## 0.8.2
- Use flat spellings in CHARTS `acceptable_tone_names` (e.g. Bbm now shows Bb/Db/F instead of A#/C#/F)
## 0.8.1
- Use musically correct flat spellings in flat keys (F major gives Bb, not A#)
## 0.8.0
- Add `Fretboard.scale_diagram()` for visual scale layouts on any instrument
- Add `play_progression()` for sequential chord playback with gaps
- Add cookbook documentation page with practical recipes
- Curated guitar fingering overrides for common open chords
- Fingering memoization with bounded cache, barre detection, 4-fret span constraint
- API ergonomics: `Fretboard.chord()`, convenience constructors, slow test markers
## 0.7.0
- Add `Fretboard.chord()` method for named chord lookups
- Improve fingering algorithm with better voicing selection
- Rewrite all documentation in REPL style with verified output
## 0.6.1
- Fix sawtooth and triangle wave generation
- Add WAV export via `save()`
- Add CLI tests and play module tests
- Skip play module tests when PortAudio is not available
## 0.6.0
- Support flat note names (Db, Bb, Eb, etc.) throughout the system
- Add `Fingering` class for labeled chord fingerings
- Add `pytheory play` CLI command for playing notes and chords
- Add 12 example scripts showcasing pytheory features
- Expand documentation with undocumented features and CLI guide
## 0.4.1
- Add `--temperament` flag to CLI tone command
- Add Symbolic Pitch section to tones docs
## 0.4.0
- Add key signatures, scale diagrams, chord building, and progression analysis
- Add CLI tool (`pytheory tone`, `pytheory chord`, `pytheory key`, etc.)
- Add Jupyter notebook tutorial
- Improve test coverage from 93% to 97% (476 tests)
- Add type hints, docstrings, and property caching throughout
## 0.3.2
- Add type hints and docstrings throughout the library
## 0.3.1
- Add capo support, chord merging (`+`), tritone substitution
- Add secondary dominants, Nashville number system
- Add more common progressions (blues, jazz, flamenco, modal)
## 0.3.0
- Add interval naming (`Tone.interval_to()`)
- Add MIDI conversion (`Tone.midi`, `Tone.from_midi()`)
- Add `Tone.from_frequency()`, `Tone.transpose()`
- Add `Chord.root`, `Chord.quality` properties
- Add `Chord.from_name()`, `Chord.from_intervals()`, `Chord.from_midi_message()`
- Add `Interval` constants (MINOR_THIRD, PERFECT_FIFTH, etc.)
- Add `PROGRESSIONS` dict with common named progressions
- Add `Tone.enharmonic` property
- Add inversions, harmonize, and Roman numeral progressions
- Add `Key` class with detection, signatures, relative/parallel keys
- Add `Scale.detect()` and `Chord.from_tones()` convenience constructors
- Add 25 instrument presets (mandolin family, violin family, banjo, harp, world instruments, keyboard)
- Add `Tone.circle_of_fifths()` and `Tone.circle_of_fourths()`
- Add chord identification (17 types), voice leading, tension scoring
- Add beat frequencies, Plomp-Levelt dissonance model, harmony scoring
## 0.2.0
- Add `Fretboard` class for guitar fretboards
- Add `play()` function with sine, sawtooth, and triangle wave synthesis
- Add chord harmony and dissonance calculations
- Modernize project structure (pyproject.toml, sounddevice)
## 0.1.0
- Initial release
- Western 12-tone system with tones, scales, and basic chord support
- Temperament support (equal, Pythagorean, meantone)
- Indian (Hindustani), Arabic, Japanese, Blues, and Gamelan systems
+38
View File
@@ -0,0 +1,38 @@
# Claude Code Instructions
## Release Process
When releasing to PyPI, always do all three:
1. **Tag the commit**: `git tag v0.X.Y`
2. **Push the tag**: `git push origin --tags`
3. **Create a GitHub release**: `gh release create v0.X.Y --title "v0.X.Y: Short description" --notes "Release notes" --latest`
Don't forget to update `CHANGELOG.md` *before* the release commit.
## Version Bumping
- `pyproject.toml` and `pytheory/__init__.py` must match
- Run `uv lock` after changing the version
- Patch releases (0.X.Y) for bug fixes and small additions
- Minor releases (0.X.0) for new features
## Testing
```
uv run python -m pytest test_pytheory.py -x -q --tb=short -m "not slow"
```
## Publishing
```
uv build && uv publish --token <token> dist/pytheory-0.X.Y*
```
## Music Preferences
- Detune: keep at 8-15, don't go above 25
- Humanize: 0.2 is the sweet spot for melodic parts
- Drum humanize: 0.15 default is good
- No swing unless specifically asked
- Sine and triangle are underrated — use them more
+21
View File
@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2026 Kenneth Reitz
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
+111 -95
View File
@@ -1,145 +1,161 @@
# PyTheory: Music Theory for Humans
This library makes exploring music theory approachable and fun, treating Python as a musical instrument.
## Installation
Explore music theory, compose multi-part arrangements, and export to MIDI — all in Python.
```
$ pip install pytheory
```
## Tones
## Sketch Ideas Fast
```pycon
>>> from pytheory import Tone
```python
from pytheory import Score, Pattern, Key, Duration, Chord
from pytheory.play import play_score
>>> c4 = Tone.from_string("C4", system="western")
>>> c4.frequency
261.63
score = Score("4/4", bpm=140)
score.drums("bossa nova", repeats=4)
>>> c4 + 7 # perfect fifth
<Tone G4>
chords = score.part("chords", synth="fm", envelope="pad", reverb=0.4)
lead = score.part("lead", synth="saw", envelope="pluck", delay=0.3, lowpass=3000)
bass = score.part("bass", synth="sine", lowpass=500)
>>> c4.interval_to(c4 + 7)
'perfect 5th'
for sym in ["Am", "Dm", "E7", "Am"]:
chords.add(Chord.from_symbol(sym), Duration.WHOLE)
chords.add(Chord.from_symbol(sym), Duration.WHOLE)
>>> c4.midi
60
lead.arpeggio("Am", bars=2, pattern="updown", octaves=2)
lead.arpeggio("Dm", bars=2, pattern="updown", octaves=2)
lead.set(lowpass=5000, reverb=0.4)
lead.arpeggio("E7", bars=2, pattern="up", octaves=2)
lead.arpeggio("Am", bars=2, pattern="updown", octaves=2)
>>> Tone.from_frequency(440)
<Tone A4>
for n in ["A2", "E2", "A2", "C3"] * 4:
bass.add(n, Duration.QUARTER)
>>> Tone.from_midi(69)
<Tone A4>
play_score(score) # hear it now
score.save_midi("sketch.mid") # open in your DAW
```
## Scales and Modes
## Hear It Instantly
```pycon
>>> from pytheory import TonedScale
>>> c_major = TonedScale(tonic="C4")["major"]
>>> c_major.note_names
['C', 'D', 'E', 'F', 'G', 'A', 'B', 'C']
>>> TonedScale(tonic="C4")["dorian"].note_names
['C', 'D', 'D#', 'F', 'G', 'A', 'A#', 'C']
```
$ pytheory demo
```
## Diatonic Harmony
## Music Theory
```pycon
>>> c_major.triad(0).identify()
'C major'
>>> from pytheory import Key, Chord, Tone
>>> c_major.seventh(4).identify()
'G dominant 7th'
>>> [c.identify() for c in c_major.harmonize()]
>>> Key("C", "major").chords
['C major', 'D minor', 'E minor', 'F major', 'G major', 'A minor', 'B diminished']
>>> [c.identify() for c in c_major.progression("I", "V", "vi", "IV")]
['C major', 'G major', 'A minor', 'F major']
>>> [c.symbol for c in Key("G", "major").progression("I", "V", "vi", "IV")]
['G', 'D', 'Em', 'C']
>>> Chord.from_symbol("F#m7b5").identify()
'F# half-diminished 7th'
>>> Tone.from_string("C4").interval_to(Tone.from_string("G4"))
'perfect 5th'
>>> Key("C", "major").pivot_chords(Key("G", "major"))
['A minor', 'B minor', 'C major', 'D major', 'E minor', 'G major']
>>> Chord.from_tones("C", "E", "G").forte_number
'3-11'
>>> from pytheory.scales import Scale
>>> Scale.recommend("C", "Eb", "F", "Gb", "G", "Bb", top=3)
[('C', 'blues', 1.0), ...]
```
## Chord Analysis
## Composition
```pycon
>>> from pytheory import Chord, Tone
```python
score = Score("4/4", bpm=124)
score.drums("house", repeats=16, fill="house", fill_every=8)
>>> C4 = Tone.from_string("C4", system="western")
>>> G4 = Tone.from_string("G4", system="western")
pad = score.part("pad", synth="supersaw", envelope="pad",
reverb=0.5, chorus=0.3, sidechain=0.85)
lead = score.part("lead", synth="saw", envelope="pluck",
legato=True, glide=0.03, humanize=0.3)
bass = score.part("bass", synth="sine", lowpass=300, sidechain=0.7)
>>> g7 = Chord([G4, G4+4, G4+7, G4+10])
>>> g7.identify()
'G dominant 7th'
# Song structure
score.section("verse")
# ... add notes ...
score.section("chorus")
lead.set(lowpass=5000, reverb=0.3)
# ... add notes ...
score.end_section()
>>> g7.analyze("C")
'V7'
>>> g7.tension
{'score': 0.6, 'tritones': 1, 'minor_seconds': 0, 'has_dominant_function': True}
>>> g7.transpose(-7).identify()
'C dominant 7th'
score.repeat("verse")
score.repeat("chorus", times=2)
```
## Six Musical Systems
## 10 Synth Waveforms
```pycon
>>> from pytheory import TonedScale
sine, saw, triangle, square, pulse, FM, noise, supersaw, PWM slow, PWM fast — with detune, stereo pan, and spread.
>>> TonedScale(tonic="Sa4", system="indian")["bhairav"].note_names
['Sa', 'komal Re', 'Ga', 'Ma', 'Pa', 'komal Dha', 'Ni', 'Sa']
## 58 Drum Patterns
>>> TonedScale(tonic="Do4", system="arabic")["hijaz"].note_names
['Do', 'Reb', 'Mi', 'Fa', 'Sol', 'Solb', 'Sib', 'Do']
rock, jazz, bebop, bossa nova, salsa, samba, afrobeat, funk, reggae, house, trap, metal, drum and bass — and 45 more. Plus 21 fill presets. Stereo panned like a real kit.
>>> TonedScale(tonic="C4", system="japanese")["hirajoshi"].note_names
['C', 'D', 'D#', 'G', 'G#', 'C']
## 6 Effects with Automation
>>> TonedScale(tonic="C4", system="blues")["blues"].note_names
['C', 'D#', 'F', 'F#', 'G', 'A#', 'C']
```python
lead = score.part("lead", synth="saw",
distortion=0.7, lowpass=1000, lowpass_q=5.0,
delay=0.3, reverb=0.4, reverb_type="plate",
chorus=0.3)
# Automate mid-song
lead.set(lowpass=4000, distortion=0.9)
# LFO modulation
lead.lfo("lowpass", rate=0.5, min=400, max=3000, bars=8)
```
Signal chain: distortion → chorus → lowpass → delay → reverb. Sidechain compression. Master bus compressor/limiter. Stereo output.
## Convolution Reverb
7 synthetic impulse responses: Taj Mahal (12s), cathedral, plate, spring, cave, parking garage, canyon.
```python
pad = score.part("pad", synth="supersaw",
reverb=0.85, reverb_type="taj_mahal")
```
## 6 Musical Systems
Western, Indian (Hindustani), Arabic (Maqam), Japanese, Blues/Pentatonic, Javanese Gamelan — 40+ scales.
## 25 Instrument Presets
```pycon
>>> from pytheory import Fretboard, CHARTS
Guitar (8 tunings), bass, ukulele, mandolin family, violin family, banjo, harp, oud, sitar, erhu, and more — with chord fingering generation.
>>> Fretboard.guitar() # standard tuning
>>> Fretboard.guitar("drop d") # 8 alternate tunings
>>> Fretboard.mandolin() # + mandola, octave mandolin, mandocello
>>> Fretboard.violin() # + viola, cello, double bass
>>> Fretboard.ukulele() # + banjo, harp, charango, erhu...
>>> Fretboard.keyboard() # 88-key piano
>>> Fretboard.keyboard(25, "C3") # 25-key MIDI controller
## Command Line
>>> CHARTS['western']['Am'].fingering(fretboard=Fretboard.guitar())
(0, 1, 2, 2, 0, 0)
```
$ pytheory repl # interactive scratchpad
$ pytheory demo # hear a generated track
$ pytheory key G major # explore a key
$ pytheory identify Cmaj7 # analyze a chord symbol
$ pytheory progression C major I V vi IV # build a progression
$ pytheory midi C major I V vi IV -o out.mid
$ pytheory play Am7 --synth saw --envelope pluck
$ pytheory modes C # show all modes
$ pytheory circle C # circle of fifths
```
## Audio Playback
## Why Python?
```pycon
>>> from pytheory import play, Synth, Tone
A DAW is great for tweaking sounds. But when you're *thinking about music* — code is faster than clicking. Sketch ideas, hear them instantly, export MIDI, finish in your DAW.
>>> tone = Tone.from_string("A4", system="western")
>>> play(tone, t=1_000) # sine wave, 1 second
>>> play(tone, synth=Synth.SAW, t=1_000) # sawtooth wave
```
## Features
- **6 musical systems**: Western, Indian (Hindustani), Arabic (Maqam), Japanese, Blues/Pentatonic, Javanese Gamelan
- **40+ scales**: major, minor, harmonic minor, 7 modes, 10 thaats, 10 maqamat, pentatonic, blues, hirajoshi, pelog, slendro, and more
- **Chord analysis**: identification (17 types), Roman numeral analysis, tension scoring, voice leading, Plomp-Levelt dissonance, beat frequencies
- **Diatonic harmony**: triads, seventh chords, harmonize entire scales, build progressions from Roman numerals
- **25 instrument presets**: guitar (8 tunings), 12-string, bass, mandolin family, violin family, banjo, harp, oud, sitar, shamisen, erhu, charango, pipa, balalaika, lute, pedal steel, keyboard
- **Pitch tools**: frequency ↔ tone conversion, MIDI ↔ tone, interval naming, circle of fifths, overtone series, transposition
- **3 temperaments**: equal, Pythagorean, quarter-comma meantone
- **Audio synthesis**: sine, sawtooth, and triangle wave playback
Tools like [Claude Code](https://claude.ai/code) can use PyTheory to prototype musical ideas from natural language — "write a bossa nova in A minor with a saw lead and reverb" becomes real, playable music.
## Documentation
Full documentation with music theory guides: **[pytheory.kennethreitz.org](https://pytheory.kennethreitz.org)**
**[pytheory.kennethreitz.org](https://pytheory.kennethreitz.org)**
Binary file not shown.
Binary file not shown.
BIN
View File
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
BIN
View File
Binary file not shown.
Binary file not shown.
Binary file not shown.
BIN
View File
Binary file not shown.
BIN
View File
Binary file not shown.
BIN
View File
Binary file not shown.
Binary file not shown.
BIN
View File
Binary file not shown.
BIN
View File
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
BIN
View File
Binary file not shown.
BIN
View File
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
BIN
View File
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
BIN
View File
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
+4
View File
@@ -0,0 +1,4 @@
<audio controls style="width: 100%; margin: 0.5em 0 1.5em 0;">
<source src="{{ pathto('_static/audio/' + file, 1) }}" type="audio/wav">
Your browser does not support the audio element.
</audio>
+2
View File
@@ -0,0 +1,2 @@
```{include} ../CHANGELOG.md
```
+11 -1
View File
@@ -10,13 +10,16 @@ sys.modules["sounddevice"] = MagicMock()
project = "PyTheory"
copyright = "2026, Kenneth Reitz"
author = "Kenneth Reitz"
release = "0.3.2"
import pytheory
release = pytheory.__version__
version = pytheory.__version__
extensions = [
"sphinx.ext.autodoc",
"sphinx.ext.napoleon",
"sphinx.ext.viewcode",
"sphinx.ext.intersphinx",
"myst_parser",
]
autodoc_member_order = "bysource"
@@ -38,7 +41,14 @@ html_theme_options = {
"github_user": "kennethreitz",
"github_repo": "pytheory",
"github_banner": True,
"github_button": True,
"github_type": "star",
"github_count": True,
"description": "Music Theory for Humans",
"extra_nav_links": {
f"v{pytheory.__version__}": "https://pypi.org/project/pytheory/",
},
"show_powered_by": False,
}
html_static_path = ["_static"]
html_extra_path = ["CNAME"]
File diff suppressed because it is too large Load Diff
+367 -115
View File
@@ -45,18 +45,20 @@ For seventh chords, there's also **third inversion** (7th in bass):
- G7 in third inversion: F G B D (notated G7/F)
.. code-block:: python
.. code-block:: pycon
from pytheory import Chord, Tone
>>> from pytheory import Chord, Tone
# All three are "C major" — identify() finds the root
root = Chord([Tone.from_string(n, system="western") for n in ["C4", "E4", "G4"]])
first = Chord([Tone.from_string(n, system="western") for n in ["E3", "G3", "C4"]])
second = Chord([Tone.from_string(n, system="western") for n in ["G3", "C4", "E4"]])
>>> root = Chord([Tone.from_string(n, system="western") for n in ["C4", "E4", "G4"]])
>>> first = Chord([Tone.from_string(n, system="western") for n in ["E3", "G3", "C4"]])
>>> second = Chord([Tone.from_string(n, system="western") for n in ["G3", "C4", "E4"]])
root.identify() # 'C major'
first.identify() # 'C major'
second.identify() # 'C major'
>>> root.identify()
'C major'
>>> first.identify()
'C major'
>>> second.identify()
'C major'
Extended Chords
---------------
@@ -72,33 +74,42 @@ A full 13th chord contains all 7 notes of the scale! In practice,
tones are usually omitted — the 5th is typically dropped first, then
the 11th (which clashes with the 3rd in dominant chords).
.. code-block:: python
.. code-block:: pycon
from pytheory import TonedScale
>>> from pytheory import TonedScale
scale = TonedScale(tonic="C4")["major"]
>>> scale = TonedScale(tonic="C4")["major"]
# Build a Cmaj9 from the scale: C E G B D
cmaj9 = scale.chord(0, 2, 4, 6, 8)
# Build a full C13 (in theory): C E G B D F A
c13 = scale.chord(0, 2, 4, 6, 8, 10, 12)
>>> cmaj9 = scale.chord(0, 2, 4, 6, 8)
>>> c13 = scale.chord(0, 2, 4, 6, 8, 10, 12)
Using the Chord Chart
---------------------
PyTheory includes 144 pre-built chords (12 roots x 12 qualities):
.. code-block:: python
.. code-block:: pycon
from pytheory import CHARTS
>>> from pytheory import Fretboard
chart = CHARTS["western"]
>>> fb = Fretboard.guitar()
>>> fb.chord("C")
Fingering(e=0, B=1, G=0, D=2, A=3, E=x)
>>> fb.chord("Am")
Fingering(e=0, B=1, G=2, D=2, A=0, E=x)
>>> fb.chord("G7")
Fingering(e=1, B=0, G=0, D=0, A=2, E=3)
c_major = chart["C"] # C major (root position)
a_minor = chart["Am"] # A minor
g_seven = chart["G7"] # G dominant 7th
d_dim = chart["Ddim"] # D diminished
You can also build chords directly with ``Chord.from_name()``:
.. code-block:: pycon
>>> from pytheory import Chord
>>> Chord.from_name("G7").identify()
'G dominant 7th'
>>> Chord.from_name("Ddim").identify()
'D diminished'
Available qualities:
@@ -119,32 +130,48 @@ Quality Intervals Example tones (from C)
``"maj9"`` 4, 7, 11, 14 C E G B D (major 9th)
============ ================ ================================
.. code-block:: python
.. code-block:: pycon
>>> from pytheory import CHARTS
>>> chart = CHARTS["western"]
>>> chart["C"].acceptable_tone_names
('C', 'E', 'G')
>>> chart["Cm7"].acceptable_tone_names
('C', 'D#', 'G', 'A#') # Eb and Bb shown as sharps
('C', 'Eb', 'G', 'Bb')
Building Chords Manually
-------------------------
Building Chords
---------------
.. code-block:: python
Several convenience constructors make chord creation concise:
from pytheory import Tone, Chord
.. code-block:: pycon
c_major = Chord(tones=[
Tone.from_string("C4", system="western"),
Tone.from_string("E4", system="western"),
Tone.from_string("G4", system="western"),
])
>>> from pytheory import Chord
for tone in c_major:
print(tone)
>>> Chord.from_tones("C", "E", "G").identify()
'C major'
>>> Chord.from_tones("A", "C", "E").identify()
'A minor'
len(c_major) # 3
"C" in c_major # True
>>> Chord.from_name("Am7").identify()
'A minor 7th'
>>> Chord.from_name("G7").identify()
'G dominant 7th'
>>> Chord.from_intervals("C", 4, 7).identify()
'C major'
>>> Chord.from_intervals("G", 4, 7, 10).identify()
'G dominant 7th'
>>> Chord.from_midi_message(60, 64, 67).identify()
'C major'
>>> len(Chord.from_name("C"))
3
>>> "C" in Chord.from_name("C")
True
Intervals
---------
@@ -152,13 +179,13 @@ Intervals
The ``intervals`` property returns semitone distances between adjacent
tones — these are musically meaningful and octave-invariant:
.. code-block:: python
.. code-block:: pycon
>>> c_major.intervals
[4, 3] # major 3rd (4) + minor 3rd (3) = major triad
>>> Chord.from_tones("C", "E", "G").intervals
[4, 3]
>>> Chord(tones=[C4, Eb4, G4]).intervals
[3, 4] # minor 3rd + major 3rd = minor triad
>>> Chord.from_tones("C", "Eb", "G").intervals
[3, 4]
Consonance and Dissonance
-------------------------
@@ -185,13 +212,16 @@ Minor 3rd 6:5 Every 6th wave aligns
Tritone 45:32 Waves rarely align
=========== ===== ====================
.. code-block:: python
.. code-block:: pycon
fifth = Chord([C4, G4])
tritone = Chord([C4, F_sharp_4])
>>> from pytheory import Chord, Tone
>>> C4 = Tone.from_string("C4", system="western")
>>> G4 = Tone.from_string("G4", system="western")
fifth.harmony > tritone.harmony # True
# The perfect fifth's 3:2 ratio scores higher
>>> fifth = Chord([C4, G4])
>>> tritone = Chord([C4, C4 + 6])
>>> fifth.harmony > tritone.harmony
True
Dissonance Score
~~~~~~~~~~~~~~~~
@@ -207,14 +237,13 @@ The roughness depends on the frequency difference relative to the
that register). Maximum roughness occurs when the difference equals
the critical bandwidth.
.. code-block:: python
.. code-block:: pycon
# Octave: frequencies far apart → low roughness
octave = Chord([C4, C5])
# Major 3rd: closer frequencies → higher roughness
third = Chord([C4, E4])
octave.dissonance < third.dissonance # True
>>> E4 = Tone.from_string("E4", system="western")
>>> octave = Chord([C4, C4 + 12])
>>> third = Chord([C4, E4])
>>> octave.dissonance < third.dissonance
True
Beat Frequencies
~~~~~~~~~~~~~~~~
@@ -227,16 +256,49 @@ you hear a pulsing at the **beat frequency**: ``|f1 - f2|`` Hz.
- **1530 Hz**: Perceived as buzzing/roughness
- **> 30 Hz**: No longer beating — becomes part of the timbre
.. code-block:: python
.. code-block:: pycon
chord = Chord(tones=[A4, E5, A5])
>>> A4 = Tone.from_string("A4", system="western")
>>> chord = Chord([A4, A4 + 7, A4 + 12])
# All pairwise beat frequencies, sorted ascending
chord.beat_frequencies
# [(A4, E5, 189.6), (E5, A5, 220.0), (A4, A5, 440.0)]
>>> chord.beat_frequencies
[...]
# The slowest (most perceptible) beat
chord.beat_pulse # 189.6 Hz
>>> round(chord.beat_pulse, 1)
219.3
Transposition
-------------
Shift an entire chord up or down by any number of semitones:
.. code-block:: pycon
>>> Chord.from_name("C").transpose(7).identify()
'G major'
>>> Chord.from_name("Am7").transpose(-2).identify()
'G minor 7th'
Chord Manipulation
------------------
Add or remove individual tones from a chord:
.. code-block:: pycon
>>> from pytheory import Chord, Tone
>>> c_major = Chord.from_tones("C", "E", "G")
>>> b4 = Tone.from_string("B4", system="western")
>>> cmaj7 = c_major.add_tone(b4)
>>> cmaj7.identify()
'C major 7th'
>>> c_again = cmaj7.remove_tone("B")
>>> c_again.identify()
'C major'
Chord Identification
--------------------
@@ -245,25 +307,38 @@ Give PyTheory any set of tones and it will tell you what chord it is.
It tries every tone as a potential root and matches the interval pattern
against 17 known chord types (triads, 7ths, 9ths, sus, power chords).
.. code-block:: python
.. code-block:: pycon
from pytheory import Chord, Tone
>>> from pytheory import Chord
# Build a chord and identify it
chord = Chord([
Tone.from_string("A4", system="western"),
Tone.from_string("C5", system="western"),
Tone.from_string("E5", system="western"),
])
chord.identify() # 'A minor'
>>> Chord.from_tones("A", "C", "E").identify()
'A minor'
>>> Chord.from_tones("G", "B", "D", "F").identify()
'G dominant 7th'
# Works with any voicing or inversion
chord2 = Chord([
Tone.from_string("E4", system="western"),
Tone.from_string("G4", system="western"),
Tone.from_string("C5", system="western"),
])
chord2.identify() # 'C major' (first inversion detected)
>>> Chord.from_tones("E", "G", "C").identify()
'C major'
>>> Chord.from_tones("Bb", "D", "F").identify()
'Bb major'
Enharmonic spellings are fully supported — Cb, Fb, E#, B#, double
sharps/flats, and unicode symbols (see :doc:`tones` for details):
.. code-block:: pycon
>>> Chord.from_tones("Cb", "Eb", "Gb").identify()
'B minor'
You can also access the root and quality separately:
.. code-block:: pycon
>>> chord = Chord.from_name("Am7")
>>> chord.root
<Tone A4>
>>> chord.quality
'minor 7th'
Harmonic Analysis
-----------------
@@ -273,22 +348,22 @@ key. This is how musicians describe chord progressions independent of
key — "I-IV-V" means the same thing in C major (C-F-G) as in G major
(G-C-D).
.. code-block:: python
.. code-block:: pycon
from pytheory import Chord, Tone
>>> from pytheory import Chord, Tone
C4 = Tone.from_string("C4", system="western")
D4 = Tone.from_string("D4", system="western")
E4 = Tone.from_string("E4", system="western")
F4 = Tone.from_string("F4", system="western")
G4 = Tone.from_string("G4", system="western")
A4 = Tone.from_string("A4", system="western")
B4 = Tone.from_string("B4", system="western")
>>> C4 = Tone.from_string("C4", system="western")
>>> E4 = Tone.from_string("E4", system="western")
>>> G4 = Tone.from_string("G4", system="western")
Chord([C4, E4, G4]).analyze("C") # 'I' (tonic)
Chord([D4, F4, A4]).analyze("C") # 'ii' (supertonic minor)
Chord([G4, B4, G4+5]).analyze("C") # 'V' (dominant)
Chord([G4, B4, G4+5, G4+10]).analyze("C") # 'V7' (dominant 7th)
>>> Chord([C4, E4, G4]).analyze("C")
'I'
>>> Chord.from_tones("D", "F", "A").analyze("C")
'ii'
>>> Chord([G4, G4+4, G4+7]).analyze("C")
'V'
>>> Chord([G4, G4+4, G4+7, G4+10]).analyze("C")
'V7'
Tension and Resolution
----------------------
@@ -304,18 +379,21 @@ quantifies this based on:
- **Dominant function**: the specific combination of a major 3rd and
minor 7th above the root — the hallmark of the V7 chord.
.. code-block:: python
.. code-block:: pycon
# A C major triad is fully resolved — no tension
c_major = Chord([C4, E4, G4])
c_major.tension['score'] # 0.0
c_major.tension['tritones'] # 0
>>> c_major = Chord([C4, E4, G4])
>>> c_major.tension['score']
0.0
>>> c_major.tension['tritones']
0
# G7 is loaded with tension — it wants to resolve to C
g7 = Chord([G4, B4, G4+5, G4+10])
g7.tension['score'] # 0.6
g7.tension['tritones'] # 1
g7.tension['has_dominant_function'] # True
>>> g7 = Chord([G4, G4+4, G4+7, G4+10])
>>> g7.tension['score']
0.6
>>> g7.tension['tritones']
1
>>> g7.tension['has_dominant_function']
True
Voice Leading
-------------
@@ -325,14 +403,36 @@ jumping all voices to new positions, good voice leading moves each note
the minimum distance to reach the next chord. Bach's chorales are the
gold standard — every voice moves by step whenever possible.
.. code-block:: python
.. code-block:: pycon
c_maj = Chord([C4, E4, G4])
f_maj = Chord([F4, A4, C4+12])
>>> c_maj = Chord.from_tones("C", "E", "G")
>>> f_maj = Chord.from_tones("F", "A", "C")
for src, dst, motion in c_maj.voice_leading(f_maj):
print(f"{src} -> {dst} ({motion:+d} semitones)")
# Each voice moves the minimum distance to reach the target chord
>>> for src, dst, motion in c_maj.voice_leading(f_maj):
... print(f"{src} -> {dst} ({motion:+d} semitones)")
G4 -> A4 (+2 semitones)
E4 -> F4 (+1 semitones)
C4 -> C4 (+0 semitones)
Tritone Substitution
--------------------
In jazz harmony, any `dominant chord <https://en.wikipedia.org/wiki/Dominant_seventh_chord>`_
can be replaced by the dominant chord a
`tritone <https://en.wikipedia.org/wiki/Tritone_substitution>`_ (6
semitones) away. This works because the two chords share the same
tritone interval — the 3rd and 7th simply swap roles.
Common tritone subs: G7 <-> Db7, C7 <-> F#7, D7 <-> Ab7.
.. code-block:: pycon
>>> from pytheory import Chord
>>> g7 = Chord.from_name("G7")
>>> sub = g7.tritone_sub()
>>> sub.identify()
'C# dominant 7th'
The Overtone Series
-------------------
@@ -347,12 +447,164 @@ overtones of C already contain G. The two tones share acoustic energy,
reinforcing each other. A dissonant interval like C and C# shares
almost no overtones — the waves clash.
.. code-block:: python
.. code-block:: pycon
from pytheory import Tone
>>> from pytheory import Tone
a4 = Tone.from_string("A4", system="western")
a4.overtones(8)
# [440.0, 880.0, 1320.0, 1760.0, 2200.0, 2640.0, 3080.0, 3520.0]
# A4 A5 E6 A6 C#7 E7 ~G7 A7
# fund. oct. 5th+oct 2oct 3rd 5th ~7th 3oct
>>> a4 = Tone.from_string("A4", system="western")
>>> [round(f, 1) for f in a4.overtones(8)]
[440.0, 880.0, 1320.0, 1760.0, 2200.0, 2640.0, 3080.0, 3520.0]
Chord Symbols
-------------
The ``symbol`` property returns compact lead-sheet notation, while
``from_symbol()`` parses any standard chord symbol — no lookup table needed:
.. code-block:: pycon
>>> Chord.from_tones("C", "E", "G").symbol
'C'
>>> Chord.from_name("Am7").symbol
'Am7'
>>> Chord.from_symbol("F#m7b5").identify()
'F# half-diminished 7th'
>>> Chord.from_symbol("Bbmaj9").symbol
'Bbmaj9'
Slash Chords
------------
`Slash chords <https://en.wikipedia.org/wiki/Slash_chord>`_ place a specific
note in the bass below the chord. They're written as Chord/Bass in lead sheets:
.. code-block:: pycon
>>> c = Chord.from_symbol("C")
>>> c_over_g = c.slash("G")
>>> c_over_g.slash_name
'C/G'
>>> c.slash("E").slash_name
'C/E'
Drop Voicings
-------------
`Drop voicings <https://en.wikipedia.org/wiki/Voicing_(music)#Drop_voicings>`_
are standard arranging techniques for spreading chord tones across registers:
- **Close voicing** — all tones packed within one octave
- **Open voicing** — alternating tones raised an octave for wider spacing
- **Drop 2** — second-highest voice dropped an octave (standard jazz guitar)
- **Drop 3** — third-highest voice dropped an octave
.. code-block:: pycon
>>> cmaj7 = Chord.from_symbol("Cmaj7")
>>> cmaj7.close_voicing()
<Chord C major 7th>
>>> cmaj7.drop2()
<Chord C major 7th>
Chord Extensions
----------------
The ``extensions()`` method suggests available extensions (9th, 11th, 13th)
that don't clash with existing chord tones:
.. code-block:: pycon
>>> from pytheory import Chord, TonedScale
>>> cm = Chord.from_symbol("C")
>>> cm.extensions()
[...]
>>> # Filter extensions against a scale for diatonic correctness:
>>> scale = TonedScale(tonic="C4")["major"]
>>> cm.extensions(scale=scale)
[...]
Borrowed Chord Analysis
-----------------------
``analyze()`` now recognizes chromatic chords from modal interchange,
labeling them with flat-degree prefixes:
.. code-block:: pycon
>>> Chord.from_symbol("Ab").analyze("C", "major")
'bVI'
>>> Chord.from_symbol("Bb").analyze("C", "major")
'bVII'
Figured Bass
------------
`Figured bass <https://en.wikipedia.org/wiki/Figured_bass>`_ is the
classical notation for chord inversions — numbers below the bass note
describing the intervals above it. It's how Bach, Handel, and every
Baroque composer communicated harmony.
.. code-block:: pycon
>>> from pytheory import Chord, Tone
>>> root = Chord([Tone.from_string("C4"), Tone.from_string("E4"), Tone.from_string("G4")])
>>> root.figured_bass
''
>>> first_inv = Chord([Tone.from_string("E3"), Tone.from_string("G3"), Tone.from_string("C4")])
>>> first_inv.figured_bass
'6'
>>> second_inv = Chord([Tone.from_string("G3"), Tone.from_string("C4"), Tone.from_string("E4")])
>>> second_inv.figured_bass
'6/4'
For seventh chords: root position → ``"7"``, first inversion → ``"6/5"``,
second inversion → ``"4/3"``, third inversion → ``"2"``.
Combine with Roman numeral analysis using ``analyze_figured()``:
.. code-block:: pycon
>>> first_inv.analyze_figured("C")
'I6'
Pitch Class Sets
----------------
`Pitch class set theory <https://en.wikipedia.org/wiki/Set_theory_(music)>`_
is the framework for analyzing atonal and post-tonal music. It reduces
any collection of notes to abstract pitch classes (011, where C=0),
finds the most compact form, and catalogs it with a Forte number.
If you're studying Schoenberg, Webern, Bartók, or any 20th-century
music that doesn't follow traditional harmony, this is the tool.
.. code-block:: pycon
>>> Chord.from_tones("C", "E", "G").pitch_classes
{0, 4, 7}
>>> Chord.from_tones("C", "E", "G").prime_form
(0, 3, 7)
>>> Chord.from_tones("A", "C", "E").prime_form
(0, 3, 7)
Major and minor triads share the same prime form — they're inversions
of each other in pitch class space.
.. code-block:: pycon
>>> Chord.from_tones("C", "E", "G").forte_number
'3-11'
>>> Chord.from_tones("C", "E", "G", "B").forte_number
'4-20'
>>> Chord.from_tones("C", "E", "G#").forte_number
'3-12'
Chords are the vertical dimension of music -- melody tells you where you're going, but harmony tells you how it feels to be there. Between construction, identification, voice leading, tension analysis, and pitch class sets, you've got tools to look at any chord from every angle. Pick a song you love, grab its chords, and start asking questions.
+226
View File
@@ -0,0 +1,226 @@
Command-Line Interface
======================
PyTheory includes a CLI for music theory lookups, composition, and
playback — all from the terminal.
Interactive REPL
----------------
For extended exploration, the REPL is a music theory scratchpad with
tab completion. See the :doc:`repl` guide for details::
$ pytheory repl
Demo
----
The fastest way to hear what PyTheory can do. Generates and plays a
random multi-part track — different every time::
$ pytheory demo
♫ Jazz Club
Bb major | 105 bpm
Bb → Gm → Cm → F
jazz drums | saw lead | fm pad
Tone Lookup
-----------
Look up any note's frequency, MIDI number, enharmonic spelling, and
overtones::
$ pytheory tone A4
Note: A4
Frequency: 440.00 Hz (equal temperament)
MIDI: 69
Overtones: 440.0, 880.0, 1320.0, 1760.0, 2200.0, 2640.0
Compare temperaments with ``--temperament``::
$ pytheory tone C5 --temperament pythagorean
Note: C5
Frequency: 521.48 Hz (pythagorean temperament)
Equal temp: 523.25 Hz (diff: -5.9 cents)
Scale Display
-------------
Show any scale in any system::
$ pytheory scale C major
C major: C D E F G A B C
Intervals: C4 -2- D4 -2- E4 -1- F4 -2- G4 -2- A4 -2- B4 -1- C5
$ pytheory scale C dorian
$ pytheory scale Sa bhairav --system indian
Chord Identification
--------------------
Identify a chord from its notes::
$ pytheory chord C E G
Chord: C major
Tones: C4 E4 G4
Intervals: [4, 3]
Harmony: 0.5833
Dissonance: 0.0712
Tension: 0.00 (tritones=0)
$ pytheory chord G B D F
Chord: G dominant 7th
Key Explorer
------------
Get a complete breakdown of any key — signature, diatonic triads,
seventh chords, relative and parallel keys::
$ pytheory key G major
Key: G major
Signature: 1 sharps, 0 flats (F#)
Scale: G A B C D E F#
Triads:
I G major
ii A minor
iii B minor
IV C major
V D major
vi E minor
vii° F# diminished
7th chords:
G major 7th
A minor 7th
...
Relative: <Key E minor>
Parallel: <Key G minor>
Guitar Fingerings
-----------------
Get tablature for any of the 144 built-in chords::
$ pytheory fingering Am
Am
E|--0--
B|--1--
G|--2--
D|--2--
A|--0--
E|--0--
Use ``--capo`` to see fingerings with a capo::
$ pytheory fingering G --capo 2
Chord Progressions
------------------
Build progressions from Roman numerals::
$ pytheory progression G major I V vi IV
Key: G major
Progression: I → V → vi → IV
I G major
V D major
vi E minor
IV C major
Key Detection
-------------
Detect the most likely key from a set of notes::
$ pytheory detect C E G A D
Detected key: C major
Scale: C D E F G A B C
Audio Playback
--------------
Play individual notes or chords (requires PortAudio)::
$ pytheory play A4 # Single note
$ pytheory play C E G # Notes as chord
$ pytheory play Am7 # Chord by name
$ pytheory play C E G --synth saw # Sawtooth wave
$ pytheory play A4 --duration 2000 # 2 seconds
$ pytheory play C E G --temperament meantone
$ pytheory play Am7 --envelope pad # With ADSR envelope
$ pytheory play C4 --envelope bell # Bell-like ring
Chord Identification (from symbol)
-----------------------------------
Parse any chord symbol and get a full analysis::
$ pytheory identify Cmaj7
Chord: C major 7th
Symbol: Cmaj7
Tones: C4 E4 G4 B4
Intervals: [4, 3, 4]
Harmony: 0.5833
Dissonance: 1.2345
Tension: score=0.00 tritones=0 minor_2nds=0 dominant=False
$ pytheory identify F#m7b5
MIDI Export
-----------
Export a chord progression to a Standard MIDI File::
$ pytheory midi C major I V vi IV -o pop.mid
Key: C major
Progression: I V vi IV
BPM: 120
Duration: 500 ms
Output: pop.mid
$ pytheory midi G major ii V I -o jazz.mid --bpm 140 --duration 800
Modes
-----
Show all 7 modes starting from a note::
$ pytheory modes C
Modes of C:
ionian C D E F G A B C
dorian C D Eb F G A Bb C
phrygian C Db Eb F G Ab Bb C
lydian C D E F# G A B C
mixolydian C D E F G A Bb C
aeolian C D Eb F G Ab Bb C
locrian C Db Eb F Gb Ab Bb C
Circle of Fifths
----------------
Display the circle of fifths and fourths from any note::
$ pytheory circle C
Circle of fifths from C:
→ C → G → D → A → E → B → F# → C# → G# → D# → A# → F
Circle of fourths from C:
→ C → F → A# → D# → G# → C# → F# → B → E → A → D → G
Common Progressions
-------------------
Show all named progressions realized in a key::
$ pytheory progressions C major
Common progressions in C major:
I-IV-V-I C → F → G → C
I-V-vi-IV C → G → Am → F
12-bar blues C → C → C → C → F → F → C → C → G → F → C → G
ii-V-I Dm → G7 → C
...
The CLI is there for quick lookups when you don't want to open a Python session -- just ask your question and get back to playing.
+547
View File
@@ -0,0 +1,547 @@
Cookbook
=======
Real-world recipes for common musical tasks. Each recipe is self-contained
and ready to paste into a Python session.
Analyze a Song
--------------
Take the chord progression from "Let It Be" (C G Am F) and analyze it
in the key of C major:
.. code-block:: pycon
>>> from pytheory import Chord, Key
>>> C = Chord.from_name("C")
>>> G = Chord.from_name("G")
>>> Am = Chord.from_name("Am")
>>> F = Chord.from_name("F")
>>> [c.identify() for c in [C, G, Am, F]]
['C major', 'G major', 'A minor', 'F major']
>>> [c.analyze("C") for c in [C, G, Am, F]]
['I', 'V', 'vi', 'IV']
>>> key = Key("C", "major")
>>> [c.identify() for c in key.progression("I", "V", "vi", "IV")]
['C major', 'G major', 'A minor', 'F major']
Write a 12-Bar Blues
--------------------
The `12-bar blues <https://en.wikipedia.org/wiki/Twelve-bar_blues>`_ is
built from the I, IV, and V chords. Here it is in the key of A:
.. code-block:: pycon
>>> from pytheory import Key, Chord
>>> key = Key("A", "major")
>>> [c.identify() for c in key.progression("I", "IV", "V")]
['A major', 'D major', 'E major']
>>> bars = ["I","I","I","I", "IV","IV","I","I", "V","IV","I","V"]
>>> [c.identify() for c in key.progression(*bars)]
['A major', 'A major', 'A major', 'A major', 'D major', 'D major', 'A major', 'A major', 'E major', 'D major', 'A major', 'E major']
>>> Chord.from_name("A7").identify()
'A dominant 7th'
>>> Chord.from_name("D7").identify()
'D dominant 7th'
>>> Chord.from_name("E7").identify()
'E dominant 7th'
Find Chords in a Key
--------------------
The :class:`~pytheory.scales.Key` class builds diatonic chords for any
key and lets you pull progressions by Roman numeral or Nashville number:
.. code-block:: pycon
>>> from pytheory import Key
>>> key = Key("G", "major")
>>> key.chords
['G major', 'A minor', 'B minor', 'C major', 'D major', 'E minor', 'F# diminished']
>>> [c.identify() for c in key.progression("I", "V", "vi", "IV")]
['G major', 'D major', 'E minor', 'C major']
>>> [c.identify() for c in key.nashville(1, 5, 6, 4)]
['G major', 'D major', 'E minor', 'C major']
Compare Scales
--------------
Play the same tonic through different scales to hear how each mode
reshapes the palette. The western modes share the same notes but start
on different degrees; the blues scale adds the "blue note" (flat 5th):
.. code-block:: pycon
>>> from pytheory import TonedScale
>>> c = TonedScale(tonic="C4")
>>> c["major"].note_names
['C', 'D', 'E', 'F', 'G', 'A', 'B', 'C']
>>> c["minor"].note_names
['C', 'D', 'Eb', 'F', 'G', 'Ab', 'Bb', 'C']
>>> c["dorian"].note_names
['C', 'D', 'Eb', 'F', 'G', 'A', 'Bb', 'C']
>>> c["mixolydian"].note_names
['C', 'D', 'E', 'F', 'G', 'A', 'Bb', 'C']
>>> c_blues = TonedScale(tonic="C4", system="blues")
>>> c_blues["blues"].note_names
['C', 'Eb', 'F', 'Gb', 'G', 'Bb', 'C']
Guitar Chord Chart
------------------
Generate fingerings for guitar and ukulele with
:class:`~pytheory.tones.Fretboard`:
.. code-block:: pycon
>>> from pytheory import Fretboard
>>> fb = Fretboard.guitar()
>>> fb.chord("C")
Fingering(e=0, B=1, G=0, D=2, A=3, E=x)
>>> fb.chord("G")
Fingering(e=3, B=0, G=0, D=0, A=2, E=3)
>>> fb.chord("Am")
Fingering(e=0, B=1, G=2, D=2, A=0, E=x)
>>> fb.chord("D")
Fingering(e=2, B=3, G=2, D=0, A=x, E=x)
>>> uke = Fretboard.ukulele()
>>> uke.chord("C")
Fingering(A=3, E=0, C=0, G=0)
>>> uke.chord("G")
Fingering(A=2, E=3, C=2, G=0)
Explore an Interval
-------------------
Start from A4 (440 Hz) and walk through intervals, checking names and
frequency ratios:
.. code-block:: pycon
>>> from pytheory import Tone
>>> a4 = Tone.from_string("A4", system="western")
>>> a4.frequency
440.0
>>> minor_3rd = a4 + 3
>>> a4.interval_to(minor_3rd)
'minor 3rd'
>>> p5 = a4 + 7
>>> a4.interval_to(p5)
'perfect 5th'
>>> round(p5.frequency / a4.frequency, 4)
1.4983
>>> octave = a4 + 12
>>> a4.interval_to(octave)
'octave'
>>> round(octave.frequency / a4.frequency, 4)
2.0
Walk the Circle of Fifths
-------------------------
The `circle of fifths <https://en.wikipedia.org/wiki/Circle_of_fifths>`_
is the backbone of Western harmony — each step adds one sharp or flat:
.. code-block:: pycon
>>> from pytheory import Tone
>>> c = Tone.from_string("C4", system="western")
>>> [t.name for t in c.circle_of_fifths()]
['C', 'G', 'D', 'A', 'E', 'B', 'F#', 'C#', 'G#', 'D#', 'A#', 'F']
>>> g = Tone.from_string("G4", system="western")
>>> [t.name for t in g.circle_of_fifths()]
['G', 'D', 'A', 'E', 'B', 'F#', 'C#', 'G#', 'D#', 'A#', 'F', 'C']
Voice Leading Between Chords
-----------------------------
Find the smoothest path from one chord to the next — each voice moves
the minimum distance:
.. code-block:: pycon
>>> from pytheory import Chord
>>> c_maj = Chord.from_tones("C", "E", "G")
>>> f_maj = Chord.from_tones("F", "A", "C")
>>> for src, dst, motion in c_maj.voice_leading(f_maj):
... print(f"{src} -> {dst} ({motion:+d} semitones)")
G4 -> A4 (+2 semitones)
E4 -> F4 (+1 semitones)
C4 -> C4 (+0 semitones)
Measure Harmonic Tension
------------------------
Quantify how much a chord "wants to resolve." Dominant 7ths have
the most tension — the tritone between the 3rd and 7th pulls toward
resolution:
.. code-block:: pycon
>>> from pytheory import Chord
>>> for name in ["C", "Am", "G7", "Cmaj7"]:
... ch = Chord.from_name(name)
... t = ch.tension
... print(f"{name:6s} tension={t['score']:.2f} tritones={t['tritones']} dominant={t['has_dominant_function']}")
C tension=0.00 tritones=0 dominant=False
Am tension=0.00 tritones=0 dominant=False
G7 tension=0.60 tritones=1 dominant=True
Cmaj7 tension=0.15 tritones=0 dominant=False
Tritone Substitution (Jazz)
---------------------------
Replace any dominant chord with the one a
`tritone <https://en.wikipedia.org/wiki/Tritone_substitution>`_ away —
they share the same tritone interval:
.. code-block:: pycon
>>> from pytheory import Chord
>>> g7 = Chord.from_name("G7")
>>> g7.tritone_sub().identify()
'C# dominant 7th'
>>> # ii-V-I with tritone sub:
>>> # Dm7 -> G7 -> Cmaj7 (standard)
>>> # Dm7 -> Db7 -> Cmaj7 (chromatic bass line!)
Key Signatures and Detection
-----------------------------
View the accidentals in any key, or detect the key from a set of notes:
.. code-block:: pycon
>>> from pytheory import Key
>>> Key("C", "major").signature
{'sharps': 0, 'flats': 0, 'accidentals': []}
>>> Key("G", "major").signature
{'sharps': 1, 'flats': 0, 'accidentals': ['F#']}
>>> Key("D", "major").signature
{'sharps': 2, 'flats': 0, 'accidentals': ['F#', 'C#']}
>>> Key.detect("C", "E", "G", "A", "D")
<Key C major>
Relative and Parallel Keys
--------------------------
Every major key has a **relative minor** (same notes, different root)
and a **parallel minor** (same root, different notes):
.. code-block:: pycon
>>> from pytheory import Key
>>> c = Key("C", "major")
>>> c.relative
'A minor'
>>> c.parallel
'C minor'
Borrowed Chords and Secondary Dominants
---------------------------------------
Add color by borrowing from the parallel key or building secondary
dominants that approach other scale degrees:
.. code-block:: pycon
>>> from pytheory import Key
>>> c = Key("C", "major")
>>> c.borrowed_chords[:4]
['C minor', 'D diminished', 'Eb major', 'F minor']
>>> c.secondary_dominant(5).identify()
'D dominant 7th'
>>> c.secondary_dominant(2).identify()
'A dominant 7th'
>>> c.secondary_dominant(6).identify()
'E dominant 7th'
The Overtone Series
-------------------
Every musical tone contains a stack of harmonics — the physics behind
why intervals sound consonant:
.. code-block:: pycon
>>> from pytheory import Tone
>>> a4 = Tone.from_string("A4", system="western")
>>> [round(f, 1) for f in a4.overtones(6)]
[440.0, 880.0, 1320.0, 1760.0, 2200.0, 2640.0]
>>> # Harmonic 2 = octave (2:1)
>>> # Harmonic 3 = perfect 5th + octave (3:1)
>>> # Harmonic 5 = major 3rd + two octaves (5:1)
Enharmonic Spellings
--------------------
Find the alternate name for any sharp or flat:
.. code-block:: pycon
>>> from pytheory import Tone
>>> for name in ["C#4", "D#4", "F#4", "G#4"]:
... t = Tone.from_string(name, system="western")
... print(f"{t.name} = {t.enharmonic}")
C# = Db
D# = Eb
F# = Gb
G# = Ab
World Scales
------------
Explore scales from Indian, Arabic, and Japanese traditions:
.. code-block:: pycon
>>> from pytheory import TonedScale
>>> indian = TonedScale(tonic="Sa", system="indian")
>>> indian["bhairav"].note_names
['Sa', 'komal Re', 'Ga', 'Ma', 'Pa', 'komal Dha', 'Ni', 'Sa']
>>> arabic = TonedScale(tonic="Do", system="arabic")
>>> arabic["hijaz"].note_names
['Do', 'Reb', 'Mi', 'Fa', 'Sol', 'Solb', 'Sib', 'Do']
>>> japanese = TonedScale(tonic="C4", system="japanese")
>>> japanese["hirajoshi"].note_names
['C', 'D', 'Eb', 'G', 'Ab', 'C']
Visualize a Scale on Guitar
----------------------------
See where the notes fall across the fretboard — E minor pentatonic,
the most-played scale in rock:
.. code-block:: pycon
>>> from pytheory import Fretboard, Scale
>>> fb = Fretboard.guitar()
>>> pent = Scale(tonic="E4", system="blues")["minor pentatonic"]
>>> print(fb.scale_diagram(pent, frets=12))
0 1 2 3 4 5 6 7 8 9 10 11 12
E| E | - | - | G | - | A | - | B | - | - | D | - | E |
B| B | - | - | D | - | E | - | - | G | - | A | - | B |
G| G | - | A | - | B | - | - | D | - | E | - | - | G |
D| D | - | E | - | - | G | - | A | - | B | - | - | D |
A| A | - | B | - | - | D | - | E | - | - | G | - | A |
E| E | - | - | G | - | A | - | B | - | - | D | - | E |
Composition Recipes
-------------------
These recipes go beyond theory into actual music-making.
Acid House Track
~~~~~~~~~~~~~~~~
303-style acid with sidechain pump:
.. code-block:: python
from pytheory import Score, Pattern, Duration, Chord
from pytheory.play import play_score
score = Score("4/4", bpm=132)
score.drums("house", repeats=8, fill="house", fill_every=8)
pad = score.part(
"pad",
synth="supersaw",
envelope="pad",
reverb=0.4,
chorus=0.3,
sidechain=0.85,
)
acid = score.part(
"acid",
synth="saw",
envelope="pad",
legato=True,
glide=0.03,
distortion=0.8,
distortion_drive=8.0,
lowpass=1000,
lowpass_q=5.0,
)
acid.lfo("lowpass", rate=0.5, min=600, max=2500, bars=8)
for sym in ["Cm", "Fm", "Abm", "Gm"]:
pad.add(Chord.from_symbol(sym), Duration.WHOLE)
pad.add(Chord.from_symbol(sym), Duration.WHOLE)
acid.arpeggio(sym, bars=2, pattern="up", octaves=2)
play_score(score)
.. raw:: html
<audio controls style="width:100%;margin:0.5em 0 1.5em"><source src="../_static/audio/acid_house.wav" type="audio/wav"></audio>
Dub Reggae with Delay Madness
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Sparse notes into infinite echo:
.. code-block:: python
score = Score("4/4", bpm=72)
score.drums("dub", repeats=8)
melodica = score.part(
"melodica",
synth="triangle",
envelope="pluck",
delay=0.5,
delay_time=0.66,
delay_feedback=0.55,
reverb=0.4,
reverb_type="cathedral",
)
bass = score.part("bass", synth="sine", lowpass=400, lowpass_q=1.5)
# Play almost nothing — let the delay do the work
melodica.add("A4", 2).rest(6)
melodica.add("E5", 1.5).rest(6.5)
melodica.add("D5", 1).add("C5", 1).add("A4", 2).rest(4)
for n in ["A1"] * 16:
bass.add(n, Duration.HALF)
play_score(score)
.. raw:: html
<audio controls style="width:100%;margin:0.5em 0 1.5em"><source src="../_static/audio/dub_reggae.wav" type="audio/wav"></audio>
Jazz Ballad with Humanize
~~~~~~~~~~~~~~~~~~~~~~~~~~
The difference between a robot and a musician:
.. code-block:: python
score = Score("4/4", bpm=72, swing=0.5)
score.drums("jazz", repeats=8)
rhodes = score.part(
"rhodes",
synth="fm",
envelope="piano",
reverb=0.4,
reverb_type="plate",
humanize=0.3,
)
lead = score.part(
"lead",
synth="triangle",
envelope="strings",
delay=0.25,
reverb=0.3,
humanize=0.35,
)
key = Key("Bb", "major")
for chord in key.progression("I", "vi", "ii", "V") * 2:
rhodes.add(chord, Duration.WHOLE)
for n, d in [("D5", 1.5), ("F5", 0.5), ("Bb5", 2), (None, 4),
("A5", 1), ("G5", 1), ("F5", 2), (None, 4)]:
lead.rest(d) if n is None else lead.add(n, d)
play_score(score)
.. raw:: html
<audio controls style="width:100%;margin:0.5em 0 1.5em"><source src="../_static/audio/jazz_ballad.wav" type="audio/wav"></audio>
Song with Sections
~~~~~~~~~~~~~~~~~~~
Define once, arrange freely:
.. code-block:: python
score = Score("4/4", bpm=120)
score.drums("rock", repeats=16, fill="rock", fill_every=4)
chords = score.part("chords", synth="saw", envelope="pad")
lead = score.part("lead", synth="triangle", envelope="pluck")
score.section("verse")
for sym in ["Am", "F", "C", "G"]:
chords.add(Chord.from_symbol(sym), Duration.WHOLE)
lead.add("A4", 1).add("C5", 1).add("E5", 1).rest(1)
lead.add("F5", 1).add("E5", 1).add("C5", 2)
score.section("chorus")
lead.set(reverb=0.4, lowpass=5000)
for sym in ["F", "G", "Am", "C"]:
chords.add(Chord.from_symbol(sym), Duration.WHOLE)
lead.add("C6", 2).add("A5", 1).add("G5", 1)
lead.add("F5", 2).add("E5", 2)
score.end_section()
score.repeat("verse")
score.repeat("chorus", times=2)
play_score(score)
score.save_midi("my_song.mid")
.. raw:: html
<audio controls style="width:100%;margin:0.5em 0 1.5em"><source src="../_static/audio/song_sections.wav" type="audio/wav"></audio>
Export Everything to MIDI
~~~~~~~~~~~~~~~~~~~~~~~~~~
The whole point — sketch fast, finish in your DAW:
.. code-block:: python
# Any Score can be saved as MIDI
score.save_midi("track.mid")
# Simple progressions too
from pytheory import save_midi
chords = Key("C", "major").progression("I", "V", "vi", "IV")
save_midi(chords, "pop.mid", t=500, bpm=120)
These are all starting points. Change the key, swap the chords, layer in your own ideas -- the best way to learn is to take something that works and make it yours.
+614
View File
@@ -0,0 +1,614 @@
Drums
=====
Drums are the foundation of almost everything. Change the drum pattern
and you change the genre. The same four chords over a bossa nova
pattern sound like you're in a cafe in Rio. Put those same chords over
a rock beat and you're in a garage in Seattle. Over a trap beat, you're
in Atlanta. Over a dancehall pattern, you're in Kingston. The drums ARE
the genre -- they tell the listener's body how to move before a single
melodic note is played.
PyTheory includes a complete drum system -- 51 synthesized percussion
sounds, 95+ pattern presets across dozens of genres, and 30 fill presets.
Every sound is generated from waveforms; no samples needed.
Drum Sounds
-----------
Drum hits are **humanized by default** — each hit gets a tiny random
timing offset and velocity wobble, just like a real drummer who's never
perfectly on the grid. Control the amount with ``drum_humanize`` on the
Score:
.. code-block:: python
score = Score("4/4", bpm=120, drum_humanize=0.4) # natural feel
score = Score("4/4", bpm=120, drum_humanize=0.0) # perfectly quantized
score = Score("4/4", bpm=120, drum_humanize=0.1) # studio tight
The default is 0.15 — just enough to feel alive without sounding loose.
Drums Are Parts
~~~~~~~~~~~~~~~~
Drums are a real Part — the same as any melodic voice. You can set
effects on them the same way:
.. code-block:: python
score.drums("rock", repeats=4)
score.parts["drums"].reverb_mix = 0.2
score.parts["drums"].reverb_type = "plate"
Or use the shorthand:
.. code-block:: python
score.set_drum_effects(reverb=0.2, reverb_type="plate", lowpass=8000)
Split Drums
~~~~~~~~~~~
For maximum control, split the kit into separate Parts — kick, snare,
hats, toms, cymbals, and percussion — each with independent effects:
.. code-block:: python
score.drums("rock", repeats=4, split=True)
# Now each group is its own Part
score.parts["snare"].reverb_mix = 0.3
score.parts["snare"].reverb_type = "plate"
score.parts["hats"].lowpass = 7000
score.parts["kick"] # dry, no effects
# set_drum_effects still works — applies to all drum Parts
score.set_drum_effects(reverb=0.1)
This is how real studios work — the snare gets its own reverb send,
the hats get their own EQ, the kick stays dry and punchy. Now you
can do the same thing in Python.
Sidechain compression triggers on kick hits only — hi-hats and snares
don't duck the pad.
Every drum sound is stereo-panned like a real kit — kick and snare
center, hi-hat right, crash left, toms spread across the field,
percussion instruments placed naturally. Put on headphones and you'll
hear the kit in front of you.
The ``DrumSound`` enum maps to General MIDI percussion note numbers:
.. code-block:: pycon
>>> from pytheory import DrumSound
>>> DrumSound.KICK.value
36
>>> DrumSound.SNARE.value
38
>>> DrumSound.CLOSED_HAT.value
42
All 51 sounds, organized by type:
**Kicks:** KICK (36)
**Snares:** SNARE (38), RIMSHOT (37), CLAP (39)
**Hi-hats:** CLOSED_HAT (42), OPEN_HAT (46), PEDAL_HAT (44)
**Toms:** LOW_TOM (45), MID_TOM (47), HIGH_TOM (50)
**Cymbals:** CRASH (49), RIDE (51), RIDE_BELL (53)
**Percussion:** COWBELL (56), CLAVE (75), SHAKER (70), TAMBOURINE (54),
CONGA_HIGH (63), CONGA_LOW (64), BONGO_HIGH (60), BONGO_LOW (61),
TIMBALE_HIGH (65), TIMBALE_LOW (66), AGOGO_HIGH (67), AGOGO_LOW (68),
GUIRO (73)
**Tabla:** TABLA_NA (86), TABLA_TIN (87), TABLA_GE (88), TABLA_DHA (89),
TABLA_TIT (90), TABLA_KE (91), TABLA_GE_BEND (108 -- bayan with upward
pitch bend from palm pressing into the head)
**Dhol:** DHOL_DAGGA (92), DHOL_TILLI (93), DHOL_BOTH (94)
**Dholak:** DHOLAK_GE (95), DHOLAK_NA (96), DHOLAK_TIT (97)
**Mridangam:** MRIDANGAM_THAM (98), MRIDANGAM_NAM (99), MRIDANGAM_DIN (100),
MRIDANGAM_THA (101)
**Djembe:** DJEMBE_BASS (102), DJEMBE_TONE (103), DJEMBE_SLAP (104)
**Cajón:** CAJON_BASS (108), CAJON_SLAP (109), CAJON_TAP (110)
**Metal Kit:** METAL_KICK (105), METAL_SNARE (106), METAL_HAT (107)
**Marching Snare:** MARCH_SNARE (115), MARCH_RIMSHOT (116), MARCH_CLICK (118)
**Quads (Tenors):** QUAD_1 (119), QUAD_2 (120), QUAD_3 (121), QUAD_4 (122),
QUAD_SPOCK (123)
**Marching Bass:** BASS_1 (124), BASS_2 (125), BASS_3 (126), BASS_4 (127),
BASS_5 (80)
Drum Synthesis
--------------
Every drum sound here is synthesized from scratch using the same
techniques that real drum machines use. This isn't a shortcut -- it's
the real thing. The 808 kick that defined hip hop is literally a sine
wave with a pitch envelope sweeping from 150 Hz down to 50 Hz. The 909
snare that powered techno is a sine wave body mixed with white noise
rattle. The hi-hat is just filtered noise with a short decay. When
Roland built the TR-808 and TR-909, they weren't sampling real drums;
they were synthesizing them from basic waveforms. PyTheory does the
same thing.
Each sound has a dedicated synthesizer:
- **KICK** -- sine wave with pitch envelope sweep (150 to 50 Hz) + sub click
- **SNARE** -- pitched body (180 Hz) + white noise rattle
- **CLOSED_HAT** -- high-frequency noise, 50ms decay
- **OPEN_HAT** -- high-frequency noise, 250ms decay
- **CLAP** -- layered noise bursts with spacers
- **RIMSHOT** -- bright 800 Hz click + noise
- **TOMS** -- pitched sine with sweep (low=100, mid=150, high=200 Hz)
- **CRASH** -- long noise decay (1.5s)
- **RIDE** -- metallic ring (3500+5100 Hz) + noise
- **RIDE_BELL** -- brighter ring, more sustain
- **COWBELL** -- two detuned tones (545+815 Hz)
- **CLAVE** -- short 2500 Hz click
- **CONGAS/BONGOS** -- pitched membrane with slap transient
- **TIMBALES** -- bright metallic ring with overtones
- **AGOGO** -- pitched bell with harmonics
- **SHAKER/MARACAS** -- short noise burst
- **TAMBOURINE** -- noise + 7000 Hz jingle ring
- **GUIRO** -- scraped noise bursts
Pattern Presets
---------------
80+ patterns spanning genres from rock to Afro-Cuban to electronic to
world percussion. Load them with ``Pattern.preset()``:
.. code-block:: pycon
>>> from pytheory import Pattern
>>> Pattern.list_presets()
['12/8 blues', '6/8 afro-cuban', 'afrobeat', 'baiao', 'bebop', ...]
>>> rock = Pattern.preset("rock")
>>> rock
<Pattern 'rock' 4/4 4.0 beats 12 hits>
**Rock/Pop:** rock, half time, double time, disco, motown, train beat
-- The backbone of Western popular music. Kick on 1 and 3, snare on 2
and 4. Simple, effective, universal.
**Jazz:** jazz, bebop, shuffle, swing, linear, paradiddle -- The ride
cymbal drives everything. The kick and snare comp and converse rather
than keeping strict time. These patterns swing.
**Latin:** salsa, bossa nova, samba, cumbia, merengue, baiao, maracatu,
bolero, tango -- Rich, layered patterns built on clave rhythms, with
congas, timbales, and shakers creating interlocking polyrhythmic webs.
Some of the most sophisticated drumming traditions on the planet.
**Afro-Cuban:** son clave 3-2, son clave 2-3, rumba clave 3-2,
rumba clave 2-3, cascara, guaguanco, mozambique, nanigo, bembe,
6/8 afro-cuban, tresillo, habanera -- The clave is the key that
unlocks all Latin and Afro-Cuban music. It's a five-note rhythmic
cell that everything else revolves around. If you learn one concept
from world music, learn the clave.
**African:** afrobeat, highlife -- Born in West Africa. Fela Kuti's
afrobeat layers multiple percussion voices into hypnotic,
polyrhythmic grooves that can go on for twenty minutes.
**Caribbean:** reggae, dancehall, ska, dub -- The offbeat is king.
Reggae flips rock drumming inside out by emphasizing the "and" of each
beat instead of the beat itself. Ska doubles the tempo, dancehall
adds syncopation.
**Electronic:** house, techno, trap, drum and bass, breakbeat, jungle
-- Machine music. The four-on-the-floor kick of house and techno, the
rattling hi-hats of trap, the breakneck tempo of drum and bass. These
patterns were born in drum machines and they still live there.
**Metal/Punk:** metal, blast beat, punk, double kick, metal blast,
metal groove, metal gallop -- Speed and aggression. The blast beat is
both feet and both hands going as fast as humanly possible. Punk strips
everything to its essentials. The metal kit adds 3 dedicated sounds
(double kick, china cymbal, stack) and 4 patterns for extreme metal
subgenres.
**World Percussion:** tabla, dhol, dholak, mridangam, djembe, cajón --
Deep traditions from across the globe, each with authentic sound sets and
idiomatic patterns. See the World Percussion section below for details.
**Other:** funk, hip hop, bo diddley, second line, new orleans, waltz,
12/8 blues, country, gospel, flamenco -- Everything else. The syncopated
groove of funk, the sampled feel of hip hop, the street-parade swing
of New Orleans second line.
Playing Patterns
----------------
``play_pattern()`` synthesizes every drum sound in real-time:
.. code-block:: python
from pytheory import Pattern
from pytheory.play import play_pattern
play_pattern(Pattern.preset("rock"), repeats=4, bpm=120)
play_pattern(Pattern.preset("bossa nova"), repeats=4, bpm=140)
play_pattern(Pattern.preset("salsa"), repeats=4, bpm=180)
play_pattern(Pattern.preset("afrobeat"), repeats=8, bpm=110)
.. raw:: html
<audio controls style="width:100%;margin:0.3em 0 0.5em"><source src="../_static/audio/rock_beat.wav" type="audio/wav"></audio>
<audio controls style="width:100%;margin:0.3em 0 0.5em"><source src="../_static/audio/bossa_nova_pattern.wav" type="audio/wav"></audio>
<audio controls style="width:100%;margin:0.3em 0 0.5em"><source src="../_static/audio/salsa_pattern.wav" type="audio/wav"></audio>
<audio controls style="width:100%;margin:0.3em 0 1.5em"><source src="../_static/audio/afrobeat_pattern.wav" type="audio/wav"></audio>
Fills
-----
A fill is the drummer's way of saying "something's about to change."
It's that moment at the end of a verse where the drummer breaks the
pattern and rolls around the toms before crashing into the chorus. Fills
signal transitions -- they tell the listener's ear that the section is
ending and a new one is about to begin. Without fills, a drum pattern
just loops. With them, it breathes and has structure.
``Pattern.fill()`` loads a 1-bar drum fill -- a short break that
transitions between sections. 30 fill presets are available:
.. code-block:: pycon
>>> Pattern.list_fills()
['afrobeat', 'blast', 'bossa nova', 'breakdown', 'buildup',
'cajon breakdown', 'cajon flam', 'cajon rumble',
'cumbia', 'disco', 'djembe break', 'djembe call', 'djembe roll',
'funk', 'highlife', 'hip hop', 'house',
'jazz', 'jazz brush', 'metal', 'metal blast', 'metal cascade',
'metal triplet', 'reggae', 'rock', 'rock crash',
'salsa', 'samba', 'second line', 'trap']
>>> fill = Pattern.fill("rock")
>>> fill
<Pattern 'rock fill' 4/4 4.0 beats ...>
Score Integration
-----------------
The ``score.drums()`` shorthand attaches a drum pattern to a score:
.. code-block:: python
from pytheory import Score
score = Score("4/4", bpm=140)
score.drums("bossa nova", repeats=4)
Auto-Fills
~~~~~~~~~~
The ``fill`` and ``fill_every`` parameters automatically insert drum
fills at regular intervals:
.. code-block:: python
score = Score("4/4", bpm=120)
score.drums("rock", repeats=8, fill="rock", fill_every=4)
This plays the rock pattern for 8 bars, replacing every 4th bar with
a rock fill. Useful for adding natural phrasing to longer sections.
.. code-block:: python
# Jazz with brush fills every 8 bars
score.drums("bebop", repeats=16, fill="jazz brush", fill_every=8)
# Salsa with fills every 4 bars
score.drums("salsa", repeats=8, fill="salsa", fill_every=4)
Layering Patterns
-----------------
Combine drum patterns with melodic parts for full arrangements. The
drum pattern and all named parts are mixed together by ``play_score()``:
.. code-block:: python
from pytheory import Score, Key, Duration, Chord
from pytheory.play import play_score
score = Score("4/4", bpm=180)
score.drums("salsa", repeats=4, fill="salsa", fill_every=4)
pads = score.part("pads", synth="sine", envelope="pad", volume=0.3)
lead = score.part("lead", synth="saw", envelope="pluck", volume=0.4)
bass = score.part("bass", synth="sine", envelope="pluck", volume=0.45)
for chord in Key("D", "minor").progression("ii", "V", "i", "i") * 2:
pads.add(chord, Duration.WHOLE)
lead.add("A5", 0.67).add("G5", 0.33).add("F5", 0.67).add("E5", 0.33)
for n in ["D2", "A2", "D2", "F2"] * 2:
bass.add(n, Duration.QUARTER)
play_score(score)
.. raw:: html
<audio controls style="width:100%;margin:0.5em 0 1.5em"><source src="../_static/audio/salsa_layered.wav" type="audio/wav"></audio>
World Percussion
----------------
PyTheory includes dedicated sound sets and pattern presets for
traditional percussion instruments from around the world. Each
instrument has its own synthesized sounds that capture the timbral
character of the real instrument, plus idiomatic rhythmic patterns
drawn from their musical traditions.
Tabla
~~~~~
The tabla is a pair of hand drums from the Indian subcontinent -- the
smaller, higher-pitched *dayan* and the larger, bass *bayan*. It is
the rhythmic backbone of Hindustani classical music, and one of the
most expressive percussion instruments ever created. A single tabla
player can produce an astonishing range of tones by varying finger
placement, pressure, and striking technique.
**7 sounds** -- covering the primary tabla strokes (na, tin, tun, ge,
dha, ke, tit) plus a bayan pitch bend sound (TABLA_GE_BEND) that
models the technique of pressing the palm into the bayan head to bend
the pitch upward.
**7 patterns:** teental (16 beats, the most common taal), jhaptaal
(10 beats), rupak (7 beats), dadra (6 beats), keherwa (8 beats, folk
and light classical), tabla solo, and tiri kita (fast ornamental
pattern).
**5 fills:** tihai (3x crescendo landing on sam), chakkardar (32nd
triplet cascade into slam), tiri kita (rapid 16th-note dayan burst),
bayan (deep bass bends showcase), tabla call (dayan/bayan call-and-response).
.. code-block:: python
score.drums("teental", repeats=4, fill="tihai")
score.drums("keherwa", repeats=4, fill="chakkardar")
.. code-block:: python
score = Score("4/4", bpm=80)
score.drums("teental", repeats=4)
.. raw:: html
<audio controls style="width:100%;margin:0.3em 0 0.5em"><source src="../_static/audio/tabla_teental.wav" type="audio/wav"></audio>
<audio controls style="width:100%;margin:0.3em 0 0.5em"><source src="../_static/audio/tabla_keherwa.wav" type="audio/wav"></audio>
<audio controls style="width:100%;margin:0.3em 0 1.5em"><source src="../_static/audio/tabla_chakradar.wav" type="audio/wav"></audio>
Dhol
~~~~
The dhol is a double-headed barrel drum from Punjab, played with
sticks. It is the driving force behind bhangra music -- loud,
energetic, and physically impossible to sit still to.
**3 sounds** -- bass stroke, treble stroke, and rimshot.
**2 patterns:** bhangra (the classic bhangra groove) and dhol chaal
(a processional rhythm).
.. code-block:: python
score = Score("4/4", bpm=160)
score.drums("bhangra", repeats=4)
.. raw:: html
<audio controls style="width:100%;margin:0.5em 0 1.5em"><source src="../_static/audio/dhol.wav" type="audio/wav"></audio>
Dholak
~~~~~~
The dholak is a smaller, lighter two-headed drum used across South
Asia in folk music, qawwali, and Bollywood. Played with bare hands,
it produces a warm, melodic tone.
**3 sounds** -- bass, treble, and slap.
**2 patterns:** qawwali (the rhythmic foundation of Sufi devotional
music) and dholak folk (a general folk groove).
.. code-block:: python
score = Score("4/4", bpm=120)
score.drums("qawwali", repeats=4)
.. raw:: html
<audio controls style="width:100%;margin:0.5em 0 1.5em"><source src="../_static/audio/dholak.wav" type="audio/wav"></audio>
Mridangam
~~~~~~~~~
The mridangam is a double-headed drum from South India, the
rhythmic anchor of Carnatic classical music. Its tuning system is
extraordinarily precise, and its rhythmic vocabulary is among the
most mathematically complex in the world.
**4 sounds** -- tha, thom, nam, and din.
**2 patterns:** adi talam (the most common Carnatic talam, 8 beats)
and mridangam korvai (a rhythmic cadence pattern).
.. code-block:: python
score = Score("4/4", bpm=90)
score.drums("adi talam", repeats=4)
.. raw:: html
<audio controls style="width:100%;margin:0.5em 0 1.5em"><source src="../_static/audio/mridangam.wav" type="audio/wav"></audio>
Djembe
~~~~~~
The djembe is a rope-tuned goblet drum from West Africa, capable of
producing a wide range of tones from deep bass to sharp slaps. It is
central to the drum ensemble traditions of Mali, Guinea, and Senegal.
**3 sounds** -- bass (open center strike), tone (edge strike), and
slap (sharp edge strike).
**8 patterns:** djembe (basic accompanying rhythm), kuku (Guinean harvest
dance), soli (powerful Mandinka rhythm), dununba (heavy bass-driven),
tiriba (joyful Susu rhythm), yankadi (gentle greeting/welcome), djansa
(fast Malinke dance), mendiani (women's celebratory dance).
**3 fills:** djembe call (bass-tone-slap conversation building to climax),
djembe roll (rapid slaps accelerating into bass), djembe break (syncopated
West African-style break).
.. code-block:: python
score = Score("4/4", bpm=120)
score.drums("djembe", repeats=8, fill="djembe call", fill_every=4)
.. raw:: html
<audio controls style="width:100%;margin:0.5em 0 1.5em"><source src="../_static/audio/djembe.wav" type="audio/wav"></audio>
Metal Kit
~~~~~~~~~
A dedicated percussion kit for extreme metal subgenres, with
specialized sounds and patterns that go beyond the standard drum kit.
**3 sounds** -- double kick (triggered, tight attack), china cymbal,
and stack (a short, trashy cymbal choke).
**4 patterns:** double kick (relentless double bass drum pattern),
metal blast (blast beat with china cymbal accents), metal groove (a
half-time groove with double kick fills), and metal gallop (the
classic triplet-feel gallop rhythm).
**4 fills:** metal (double kick 16ths with descending toms), metal triplet
(double kick triplets with snare accents), metal blast (alternating
snare/kick 32nds into half-time crash), metal cascade (descending snare
roll → kick roll → alternating → crash ending).
.. code-block:: python
score = Score("4/4", bpm=200)
score.drums("metal blast", repeats=8, fill="metal cascade", fill_every=4)
.. raw:: html
<audio controls style="width:100%;margin:0.5em 0 1.5em"><source src="../_static/audio/metal_blast.wav" type="audio/wav"></audio>
Cajón
~~~~~
The cajón is a box-shaped percussion instrument from Peru, now
ubiquitous in acoustic and unplugged settings worldwide. Players sit
on the box and strike the front face with their hands.
**3 sounds** -- bass (deep center thump), slap (sharp, snare-like edge
hit with wire buzz), and tap (light finger tap).
**3 patterns:** cajon (basic groove), cajon rumba (flamenco-style rumba),
and cajon folk (folk/acoustic pattern).
**3 fills:** cajon flam (slaps accelerating into bass hits), cajon rumble
(fast taps building to slap accents), cajon breakdown (syncopated
bass-slap groove).
.. code-block:: python
score = Score("4/4", bpm=100)
score.drums("cajon", repeats=8, fill="cajon flam", fill_every=4)
.. raw:: html
<audio controls style="width:100%;margin:0.5em 0 1.5em"><source src="../_static/audio/cajon.wav" type="audio/wav"></audio>
Marching Percussion
~~~~~~~~~~~~~~~~~~~
A full drumline — snare, quads (tenors), and pitched bass drums.
Every sound is synthesized: kevlar snare heads, aluminum shell ting
on the quads, felt-beater thwack on the basses.
**Snare** -- 3 sounds: MARCH_SNARE (tight kevlar tap), MARCH_RIMSHOT
(woody-metallic crack), MARCH_CLICK (stick click for count-offs).
**Quads** -- 5 sounds: QUAD_1 through QUAD_4 (high to low pitched
tenors) plus QUAD_SPOCK (rim click on the shell).
**Bass drums** -- 5 pitched drums: BASS_1 (highest/smallest) through
BASS_5 (lowest/biggest), each with a prominent felt-beater thwack.
**6 patterns:** march (basic 4/4), cadence (8-beat street beat),
march paradiddle, march roll (buzz crescendo), quad sweep (run across
all 4 drums), quad groove, bass split (cascading across the line),
bass unison (all 5 hit together), drumline (snare + quads + bass).
**Rudiment methods:** ``Part.flam()``, ``Part.diddle()``, and
``Part.cheese()`` for marching rudiments on any drum sound.
**Ensemble rendering:** ``ensemble=N`` on any Part duplicates the
voice with per-player timing tendencies and micro pitch drift.
``ensemble=8`` for a snare line, ``ensemble=20`` for a massive section.
.. code-block:: python
# Full drumline with ensemble
snares = score.part("snares", synth="sine", volume=0.9,
reverb=0.2, ensemble=8)
quads = score.part("quads", synth="sine", volume=0.5,
reverb=0.2, ensemble=4)
basses = score.part("basses", synth="sine", volume=0.55,
reverb=0.2, ensemble=5)
snares.flam(DrumSound.MARCH_SNARE, Duration.QUARTER, velocity=120)
snares.diddle(DrumSound.MARCH_SNARE, Duration.EIGHTH, velocity=60)
# Or use patterns
score.drums("drumline", repeats=4)
.. raw:: html
<audio controls style="width:100%;margin:0.5em 0 1.5em"><source src="../_static/audio/march_snare.wav" type="audio/wav"></audio>
**Sympathetic resonance:** The marching snare builds up snare wire
buzz as hits accumulate, and the buzz decays during rests — just like
a real drum.
MIDI Export
-----------
Convert any pattern to a Score, then export to MIDI (drums are written
to channel 10):
.. code-block:: python
pattern = Pattern.preset("bossa nova")
score = pattern.to_score(repeats=8, bpm=140)
score.save_midi("bossa.mid")
Pattern.preset("afrobeat").to_score(repeats=8, bpm=110).save_midi("afrobeat.mid")
Drums are the foundation. The same chords over a bossa nova feel like a different song than over a rock beat -- change the pattern and you change the genre. Try swapping presets under the same progression and hear how much the drums are really doing.
+912
View File
@@ -0,0 +1,912 @@
Effects
=======
Effects are how recorded music gets its character. A guitar without
reverb sounds like it's being played in a closet. A vocal without
compression sounds thin and amateur. A synth without filtering sounds
like a test signal. Effects are the difference between "notes" and
"music" -- they put the sound in a space, give it texture, and make it
feel alive.
Every record you've ever loved was shaped by effects. The cavernous
reverb on a Phil Collins drum hit. The tape delay on a reggae vocal.
The distortion on a Hendrix guitar. The chorus on an 80s synth pad.
These aren't decorations added after the fact; they're fundamental to
the sound itself.
Each part in a Score can have its own effects chain. Effects are set at
part creation and applied per-part before mixing, so every voice gets
independent processing.
Signal Chain
------------
The order of effects matters -- a lot. Distortion before a lowpass
filter means you're generating all those rich, crunchy harmonics and
then sculpting them with the filter. That's warm, controllable,
musical. Filter before distortion means you're distorting the already-
filtered signal -- a different, often harsher character. The fixed
order in PyTheory matches classic analog synth architecture, the same
signal path used by the Moog, the TB-303, and most hardware synths.
It's a well-tested order that sounds good by default.
Effects are applied in this fixed order::
Signal --> Saturation --> Tremolo --> Distortion --> Cabinet --> Chorus
--> Phaser --> Highpass --> Lowpass --> Delay --> Reverb --> Mix
Additionally, these per-note effects are applied before the part effects chain:
- **Sub-oscillator**: octave-below sine mixed in at the oscillator stage
- **Noise layer**: filtered noise mixed per-note for breath/transients
- **Filter envelope**: per-note lowpass sweep (attack/decay/sustain)
- **Velocity → brightness**: harder velocity = brighter filter cutoff
Part-level effects:
- **Saturation** first: subtle even-harmonic warmth (tape/tube color).
- **Tremolo** second: amplitude LFO modulation.
- **Distortion** third: drives the signal before filtering.
- **Cabinet** fourth: speaker cab simulation (rolloff + presence bump).
- **Chorus** fifth: thickens the signal.
- **Phaser** sixth: swept allpass notches.
- **Highpass** seventh: removes low-frequency mud.
- **Lowpass** eighth: shapes the tone (like a tone knob on an amp).
- **Delay** ninth: echoes the shaped signal (tap delay / tape echo).
- **Reverb** last: places everything in a space (room / hall).
Distortion
----------
You know what distortion sounds like -- it's the sound of rock and roll.
An electric guitar through a cranked amplifier. But at lower levels,
distortion is subtler: it adds warmth, presence, and harmonic richness.
This is why producers run clean signals through tape machines and tube
preamps. A little saturation makes everything sound more "real."
Soft-clip waveshaping using ``tanh`` -- models the warm saturation of an
overdriven tube amplifier. At low drive levels it adds harmonic warmth;
at high levels it becomes an aggressive fuzz.
Parameters:
- ``distortion``: Wet/dry mix, 0.0--1.0.
- ``distortion_drive``: Gain before clipping (default 3.0).
- 0.5--2 = subtle warmth (tube preamp)
- 3--8 = overdrive (cranked amp)
- 10+ = fuzz
.. code-block:: python
# Warm tube saturation on a bass
bass = score.part(
"bass",
synth="sine",
envelope="pluck",
distortion=0.3,
distortion_drive=2.0,
)
# Heavy fuzz on a lead
lead = score.part(
"lead",
synth="saw",
envelope="staccato",
distortion=0.8,
distortion_drive=10.0,
)
Cabinet Simulation
------------------
A real guitar amp doesn't just distort the signal -- the speaker
cabinet shapes the tone dramatically. A 12-inch speaker in a closed
cabinet rolls off the harsh high frequencies above 5 kHz and adds a
presence bump around 2--3 kHz that gives the sound its "in the room"
quality. Without a cabinet, distortion sounds thin and fizzy. With
one, it sounds like a real amp.
PyTheory's cabinet simulation applies a speaker rolloff curve (lowpass
at ~5 kHz) combined with a presence resonance bump, placed in the
signal chain immediately after distortion -- exactly where it sits in
a real amp.
Parameters:
- ``cabinet``: Wet/dry mix, 0.0--1.0 (default 0, off).
- 0.3--0.5 = subtle speaker coloring
- 0.6--0.8 = classic amp-in-a-room
- 1.0 = full cabinet, no dry signal
.. code-block:: python
# Classic rock amp tone: distortion into cabinet
guitar = score.part(
"guitar",
synth="saw",
envelope="pluck",
distortion=0.6,
distortion_drive=5.0,
cabinet=0.8,
)
# Clean amp with just cabinet warmth (no distortion)
clean = score.part(
"clean",
synth="triangle",
envelope="pluck",
cabinet=0.5,
)
Analog Drift
------------
Real analog synthesizers are never perfectly in tune. The voltage-
controlled oscillators drift slightly over time as components warm up
and temperature fluctuates. This imperfection is actually a big part
of why vintage analog synths sound so appealing -- the subtle pitch
wandering gives each note a unique, living quality that static digital
oscillators lack.
The ``analog_drift`` parameter adds slow, random pitch variation to
each oscillator, modeling this vintage behavior.
Parameters:
- ``analog_drift``: Drift amount, 0.0--1.0 (default 0, off).
- 0.05--0.1 = subtle warmth (studio-grade analog)
- 0.15--0.25 = noticeable drift (vintage gear warming up)
- 0.3+ = unstable, wobbly (broken tape machine)
.. code-block:: python
# Warm vintage pad
pad = score.part(
"pad",
synth="supersaw",
envelope="pad",
analog_drift=0.1,
chorus=0.3,
)
# Lo-fi detuned lead
lead = score.part(
"lead",
synth="saw",
envelope="pluck",
analog_drift=0.25,
)
Chorus
------
That shimmery, wide, slightly-out-of-focus sound that defined the
1980s? That's chorus. Think of the intro to "Come As You Are" by
Nirvana, or literally any synth pad from 1983 to 1989. It makes one
instrument sound like two or three playing together, slightly out of
tune with each other -- which is exactly how a real string section or
choir sounds rich and full.
A slightly detuned, LFO-modulated delayed copy mixed back in. Thickens
the sound like two musicians playing the same part -- the signature
effect of the Roland Juno synthesizers.
Parameters:
- ``chorus``: Wet/dry mix, 0.0--1.0.
- ``chorus_rate``: LFO speed in Hz. 0.5--1 = slow shimmer, 2--4 = vibrato.
- ``chorus_depth``: Modulation depth in seconds (default 0.003).
.. code-block:: python
# Juno-style pad chorus
pad = score.part(
"pad",
synth="supersaw",
envelope="pad",
chorus=0.5,
chorus_rate=1.5,
chorus_depth=0.003,
)
# Subtle thickening on a clean lead
lead = score.part(
"lead",
synth="triangle",
envelope="pluck",
chorus=0.2,
chorus_rate=0.8,
)
Lowpass Filter
--------------
You know that sound when a DJ turns the knob and everything goes
underwater? That's a lowpass filter closing down. It removes
high-frequency content, leaving only the warm, round, bassy
frequencies below the cutoff point. The lowpass filter is arguably the
most important effect in all of electronic music -- it's the entire
sound of acid house, the "wah" in auto-wah, and the reason analog
synths sound warm instead of harsh.
A 12 dB/octave biquad lowpass filter with resonance -- the sound of
analog synthesizers. Removes frequencies above the cutoff; the resonance
(Q) parameter adds a peak at the cutoff frequency for that classic
"acid squelch."
Parameters:
- ``lowpass``: Cutoff frequency in Hz (0 = off). Reference points:
- 200--400 Hz = deep sub bass
- 800--1500 Hz = warm / muffled
- 2000--4000 Hz = present lead
- 5000+ Hz = subtle rolloff
- ``lowpass_q``: Resonance / Q factor (default 0.707 = Butterworth flat).
- 1.0 = slight peak
- 2.0 = pronounced
- 5.0+ = aggressive acid squelch
.. code-block:: python
# Round bass with gentle filtering
bass = score.part(
"bass",
synth="sine",
envelope="pluck",
lowpass=400,
lowpass_q=1.5,
)
# Acid squelch on a saw lead
acid = score.part(
"acid",
synth="saw",
envelope="staccato",
lowpass=1500,
lowpass_q=5.0,
legato=True,
glide=0.03,
)
Delay
-----
Delay is echo. Literally. The Edge from U2 built his entire guitar
sound around dotted-eighth-note delays. Dub reggae producers like Lee
"Scratch" Perry and King Tubby turned delay into an art form, feeding
echoes back into themselves until they spiraled into infinity. At short
times with low feedback, delay adds rhythmic interest. At long times
with high feedback, it creates cascading, psychedelic soundscapes.
Tempo-synced echoes with feedback. Each repeat feeds back into the
delay line, creating rhythmic echo trails. High feedback values produce
the cascading, self-oscillating echoes of dub reggae.
Parameters:
- ``delay``: Wet/dry mix, 0.0--1.0.
- ``delay_time``: Time between echoes in seconds. Musically useful
values at 120 bpm: 0.25 (8th note), 0.375 (dotted 8th),
0.5 (quarter note).
- ``delay_feedback``: How much each echo feeds back (0.0--1.0).
- 0.3 = a few repeats
- 0.5 = many repeats
- 0.7+ = runaway (dub style)
.. code-block:: python
# Dotted-eighth slapback on a lead
lead = score.part(
"lead",
synth="triangle",
envelope="strings",
delay=0.3,
delay_time=0.375,
delay_feedback=0.4,
)
# Dub-style runaway echoes
melodica = score.part(
"melodica",
synth="triangle",
envelope="pluck",
delay=0.5,
delay_time=0.66,
delay_feedback=0.55,
)
Reverb
------
Everyone knows what reverb sounds like, even if they don't know the
word -- it's the sound of singing in the shower, or clapping in a
cathedral. It's the natural echo of a space. Without reverb, sounds
feel uncomfortably close and dry, like someone whispering directly into
your ear. With it, sounds feel like they exist in a real place. Reverb
is the most universally used effect in all of recorded music.
PyTheory offers two reverb engines: a fast **algorithmic** reverb for
general use, and **convolution** reverb for photorealistic acoustic
spaces.
Algorithmic Reverb
~~~~~~~~~~~~~~~~~~
A Schroeder reverb using 4 parallel comb filters and 2 series allpass
filters. Fast, lightweight, and good for general-purpose room
simulation.
Parameters:
- ``reverb``: Wet/dry mix, 0.0--1.0.
- 0.2--0.4 = subtle space
- 0.5--0.8 = ambient / dub
- ``reverb_decay``: Tail length in seconds.
- 0.5 = small room
- 1.5 = hall
- 3.0+ = cathedral / dub
.. code-block:: python
# Jazz club ambience
rhodes = score.part(
"rhodes",
synth="fm",
envelope="piano",
reverb=0.4,
reverb_decay=1.8,
)
Convolution Reverb
~~~~~~~~~~~~~~~~~~
Convolution reverb works by convolving your audio with an *impulse
response* -- a recording (or simulation) of a real acoustic space.
Where algorithmic reverb approximates the math of reflections,
convolution reverb *is* the space. You hear every surface, every
angle, every material.
PyTheory generates synthetic impulse responses that model the acoustic
properties of real spaces: early reflection patterns, exponential
decay envelopes, frequency-dependent absorption (high frequencies die
faster in stone), diffusion density, and subtle pitch modulation from
irregular surfaces. The result is dramatically more realistic than
algorithmic reverb, especially for long tails and large spaces.
Set ``reverb_type`` to any preset name instead of ``"algorithmic"``:
- ``"taj_mahal"`` -- Massive marble dome. 12-second tail, bright early
reflections, enormously dense and diffuse. The most dramatic verb
you've ever heard.
- ``"cathedral"`` -- Gothic stone cathedral. 6 seconds, strong early
reflections off parallel walls, dark reverberant tail.
- ``"plate"`` -- EMT 140 plate reverb. 4 seconds, dense, bright, smooth.
The studio classic that defined pop records from the 60s onward.
- ``"spring"`` -- Spring reverb tank. 3 seconds, metallic, boingy, lo-fi.
The sound of surf rock and guitar amps.
- ``"cave"`` -- Natural cave. 8 seconds, very dark, irregular reflections.
High frequencies are aggressively absorbed by rock.
- ``"parking_garage"`` -- Concrete box. 3 seconds, bright, flutter echoes
from parallel hard walls.
- ``"canyon"`` -- Open canyon. 5 seconds, sparse discrete echoes (the
walls are far apart) dissolving into a diffuse tail.
Parameters:
- ``reverb``: Wet/dry mix, 0.0--1.0.
- ``reverb_type``: Preset name (default ``"algorithmic"``).
.. code-block:: python
# FM flute through the Taj Mahal
flute = score.part(
"flute",
synth="fm",
envelope="bell",
reverb=0.85,
reverb_type="taj_mahal",
delay=0.65,
delay_time=0.375,
delay_feedback=0.55,
)
# Cathedral wash for ambient pads
pad = score.part(
"pad",
synth="supersaw",
envelope="pad",
reverb=0.7,
reverb_type="cathedral",
)
# Classic plate on a vocal-style lead
lead = score.part(
"lead",
synth="triangle",
envelope="strings",
reverb=0.5,
reverb_type="plate",
)
# Algorithmic reverb still works as before
rhodes = score.part(
"rhodes",
synth="fm",
envelope="piano",
reverb=0.4,
reverb_decay=1.8,
)
You can switch reverb types mid-song with automation:
.. code-block:: python
lead = score.part("lead", synth="fm", envelope="bell",
reverb=0.5, reverb_type="plate")
lead.add("C5", Duration.WHOLE)
# Switch to cathedral for the big section
lead.set(reverb_type="cathedral", reverb=0.8)
lead.add("E5", Duration.WHOLE)
Combining Effects
-----------------
Effects stack naturally. Here are some real-world combinations:
Dub
~~~
Distortion warmth into filtered delay into deep reverb:
.. code-block:: python
melodica = score.part(
"melodica",
synth="triangle",
envelope="pluck",
distortion=0.2,
distortion_drive=2.0,
lowpass=2000,
lowpass_q=1.2,
delay=0.5,
delay_time=0.66,
delay_feedback=0.55,
reverb=0.4,
reverb_decay=2.5,
)
Acid
~~~~
Resonant lowpass with distortion and delay:
.. code-block:: python
acid = score.part(
"acid",
synth="saw",
envelope="staccato",
lowpass=1500,
lowpass_q=3.0,
distortion=0.4,
distortion_drive=4.0,
delay=0.3,
delay_time=0.242,
delay_feedback=0.4,
)
Ambient
~~~~~~~
Wide chorus, long reverb, gentle delay:
.. code-block:: python
ambient = score.part(
"ambient",
synth="supersaw",
envelope="pad",
chorus=0.4,
chorus_rate=0.5,
delay=0.3,
delay_time=0.5,
delay_feedback=0.5,
reverb=0.7,
reverb_decay=4.0,
)
808 Bass
~~~~~~~~
Subtle saturation and deep filtering for hip-hop sub bass:
.. code-block:: python
bass = score.part(
"bass",
synth="sine",
envelope="pluck",
lowpass=200,
lowpass_q=1.8,
distortion=0.4,
distortion_drive=2.0,
)
Sidechain Compression
---------------------
If you've ever heard a house track where the pad *breathes* — gets
quiet every time the kick hits and swells back up between beats —
that's sidechain compression. It's the pumping effect that defines
modern electronic music. The kick drum triggers a compressor on
another part, ducking its volume in rhythm with the beat.
In PyTheory, the drum hits are the trigger. Any part with
``sidechain > 0`` gets ducked whenever the kick (or any drum) hits:
.. code-block:: python
# Classic EDM pump — pad ducks hard on every kick
pad = score.part(
"pad",
synth="supersaw",
envelope="pad",
sidechain=0.85,
sidechain_release=0.15,
)
# Bass breathes with the kick too, but less aggressively
bass = score.part(
"bass",
synth="sine",
lowpass=250,
sidechain=0.7,
sidechain_release=0.1,
)
Parameters:
- ``sidechain``: How much to duck, 0.01.0 (default 0, off).
0.5 = subtle pump, 0.7 = noticeable, 0.85 = classic EDM, 1.0 = full silence on hits.
- ``sidechain_release``: How fast the volume comes back, in seconds
(default 0.1). Shorter = tighter, longer = more dramatic pump.
The lead stays above the pump — don't sidechain everything or the
whole mix will gasp for air:
.. code-block:: python
# Lead cuts through — no sidechain
lead = score.part(
"lead",
synth="saw",
envelope="pluck",
delay=0.2,
)
Saturation
----------
Saturation is the warm, subtle harmonic enhancement of analog tape
machines and tube preamps. Unlike distortion (which uses ``tanh`` and
adds harsh odd harmonics), saturation uses a polynomial waveshaper
that adds even harmonics -- 2nd and 4th -- which the ear perceives as
warmth and fullness. It's why records mixed through a Neve console
sound "bigger" than the same mix done in the box.
Parameters:
- ``saturation``: Amount, 0.0--1.0 (default 0, off).
- 0.05--0.15 = subtle analog warmth (tape machine)
- 0.2--0.4 = noticeable color (tube preamp)
- 0.5+ = heavy coloring
.. code-block:: python
# Warm up a bass
bass = score.part("bass", synth="saw", saturation=0.2)
# Glue a string ensemble
strings = score.part("strings", instrument="string_ensemble",
saturation=0.1)
Tremolo
-------
Amplitude modulation by a sine LFO. The classic vibrating-amp sound.
Essential for vibraphone (the rotating discs in the resonator tubes),
Rhodes electric piano, and surf guitar. Not to be confused with
vibrato (pitch modulation).
Parameters:
- ``tremolo_depth``: Modulation depth, 0.0--1.0 (default 0, off).
- ``tremolo_rate``: LFO speed in Hz (default 5.0).
- 3--5 Hz = classic tremolo
- 5--7 Hz = vibraphone motor speed
- 8+ Hz = ring-mod territory
.. code-block:: python
# Classic Fender amp tremolo
guitar = score.part("guitar", synth="saw", envelope="pluck",
tremolo_depth=0.3, tremolo_rate=4.0)
# Vibraphone with motor
vib = score.part("vib", instrument="vibraphone") # built in
Phaser
------
A chain of allpass filters whose center frequencies are swept by an
LFO, creating moving notches in the spectrum. The classic "jet
engine" or "underwater" effect. Think Small Stone, MXR Phase 90, or
the intro to "Eruption." Different from chorus -- chorus adds a
detuned copy, phaser cancels specific frequencies.
Parameters:
- ``phaser``: Wet/dry mix, 0.0--1.0 (default 0, off).
- ``phaser_rate``: LFO sweep speed in Hz (default 0.5).
- 0.1--0.3 = slow, lush sweep
- 0.5--1.0 = classic phaser
- 2.0+ = fast, Leslie-like
.. code-block:: python
# Slow sweep on a pad
pad = score.part("pad", synth="supersaw", envelope="pad",
phaser=0.4, phaser_rate=0.2)
# Leslie sim on organ (built in)
organ = score.part("organ", instrument="organ")
Highpass Filter
---------------
The opposite of lowpass -- removes low-frequency content below the
cutoff. Useful for cleaning up mud from pads, keeping multiple bass
parts from masking each other, or thinning out a sound to sit better
in a mix.
Parameters:
- ``highpass``: Cutoff frequency in Hz (0 = off).
- 80--150 Hz = clean up sub rumble
- 200--400 Hz = thin out a pad
- 500+ Hz = telephone / radio effect
- ``highpass_q``: Resonance / Q factor (default 0.707).
.. code-block:: python
# Clean up sub rumble from a pad
pad = score.part("pad", synth="supersaw", highpass=120)
# Thin out rhythm guitar to leave room for bass
rhythm = score.part("rhythm", synth="saw", highpass=250)
Filter Envelope
---------------
A per-note lowpass filter whose cutoff sweeps over time. This is the
core of subtractive synthesis -- the reason a Moog bass goes "bwow"
instead of "boop." The filter opens on the attack and closes during
decay, giving each note a distinctive timbral shape.
Parameters:
- ``filter_amount``: Sweep range in Hz (0 = off). How far the filter
opens above the base cutoff.
- ``filter_attack``: Time to reach peak cutoff, in seconds (default 0.01).
- ``filter_decay``: Time to fall to sustain level (default 0.3).
- ``filter_sustain``: Sustain level as fraction of amount, 0.0--1.0
(default 0.0 = filter closes completely after decay).
.. code-block:: python
# Classic synth bass "bwow"
bass = score.part("bass", instrument="synth_bass") # built in
# Acid squelch
acid = score.part("acid", instrument="acid_bass") # built in
# Custom filter sweep on a lead
lead = score.part("lead", synth="saw",
filter_amount=4000, filter_attack=0.01,
filter_decay=0.4, filter_sustain=0.1)
Velocity to Brightness
~~~~~~~~~~~~~~~~~~~~~~
Real instruments get brighter when played harder. ``vel_to_filter``
maps note velocity to filter cutoff boost, so louder notes have more
high-frequency content.
- ``vel_to_filter``: Cutoff boost in Hz at max velocity (default 0, off).
.. code-block:: python
# Piano: soft = mellow, loud = bright
piano = score.part("piano", instrument="piano") # built in
# Manual: custom velocity mapping on a lead
lead = score.part("lead", synth="saw", vel_to_filter=3000)
Sub-Oscillator
--------------
An octave-below sine wave mixed in with the main oscillator. Adds
low-end weight without muddiness -- the sub fills in the fundamental
while the main oscillator provides harmonic character above.
- ``sub_osc``: Mix level, 0.0--1.0 (default 0, off).
- 0.1--0.2 = subtle weight (tuba, bass guitar)
- 0.3--0.5 = heavy sub (808, synth bass)
.. code-block:: python
# Fat 808 kick-bass
bass = score.part("bass", instrument="808_bass") # built in
# Add weight to any part
lead = score.part("lead", synth="saw", sub_osc=0.3)
Noise Layer
-----------
White noise mixed into each note, following the same amplitude
envelope. Adds breath for woodwinds, hammer/felt noise for piano,
bow rosin for strings, and attack transients for percussion.
- ``noise_mix``: Mix level, 0.0--1.0 (default 0, off).
- 0.02--0.04 = subtle texture (strings, piano)
- 0.05--0.08 = noticeable breath (woodwinds)
- 0.1+ = heavy air/texture
.. code-block:: python
# Breathy flute
flute = score.part("flute", instrument="flute") # noise_mix=0.08
# Add air to any synth
pad = score.part("pad", synth="supersaw", noise_mix=0.05)
Configurable FM
---------------
The FM synth now accepts ``fm_ratio`` and ``fm_index`` parameters,
letting you dial in specific FM timbres instead of using the defaults.
- ``fm_ratio``: Modulator frequency as multiple of carrier (default 2.0).
Integer ratios = harmonic timbres; non-integer = metallic/inharmonic.
- ``fm_index``: Modulation depth (default 3.0). Higher = more harmonics.
.. code-block:: python
# Warm electric piano (low ratio, low index)
ep = score.part("ep", synth="fm", fm_ratio=1.0, fm_index=1.5)
# Bright metallic bell (high ratio, high index)
bell = score.part("bell", synth="fm", fm_ratio=3.5, fm_index=5.0)
# Glockenspiel
glock = score.part("glock", instrument="glockenspiel") # built in
Automation
----------
Static effects are fine for a loop, but music breathes. The filter
*opens* during the chorus. The reverb *swells* before the drop. The
distortion *kicks in* when the guitar solo starts. Automation is what
makes a track feel alive instead of robotic -- it's the difference
between a static loop and a piece of music that has dynamics, tension,
and release. If you've ever felt a song "build" toward something,
you're hearing automation at work.
``Part.set()`` changes effect parameters mid-song at the current beat
position. The renderer splits the audio at automation points and
processes each section independently:
.. code-block:: python
lead = score.part("lead", synth="saw", lowpass=400, lowpass_q=3.0)
# Verse: filtered and clean
lead.arpeggio("Cm", bars=4, pattern="up", octaves=2)
# Chorus: filter opens, chorus kicks in
lead.set(lowpass=2000, chorus=0.3)
lead.arpeggio("Fm", bars=4, pattern="updown", octaves=2)
# Drop: full send
lead.set(lowpass=4000, distortion=0.7, reverb=0.3)
lead.arpeggio("Gm", bars=4, pattern="updown", octaves=2)
Any parameter can be automated: ``lowpass``, ``lowpass_q``, ``highpass``,
``reverb``, ``reverb_decay``, ``reverb_type``, ``delay``, ``delay_time``,
``delay_feedback``, ``distortion``, ``distortion_drive``, ``chorus``,
``phaser``, ``phaser_rate``, ``saturation``, ``tremolo_depth``,
``tremolo_rate``, ``cabinet``, ``cabinet_brightness``, ``analog_drift``,
``volume``.
LFO Automation
--------------
An LFO -- Low Frequency Oscillator -- is just automation that repeats.
Instead of manually setting parameter changes, you let a wave shape do
it for you, cycling back and forth continuously. You already know what
LFOs sound like, even if you don't know the term. The wobble bass in
dubstep? That's an LFO on the filter cutoff. Tremolo on a guitar amp?
LFO on volume. Auto-wah? LFO on filter cutoff with resonance cranked
up. Vibrato? LFO on pitch. It's one simple concept that produces a
huge range of effects.
``Part.lfo()`` automates a parameter with a low-frequency oscillator,
generating smooth sweeps over time. This is how filter sweeps, tremolo,
and auto-wah effects work.
.. code-block:: python
lead = score.part("lead", synth="saw", lowpass=400)
# Slow filter sweep: 400 -> 3000 Hz over 8 bars
lead.lfo("lowpass", rate=0.125, min=400, max=3000, bars=8)
lead.arpeggio("Cm", bars=8, pattern="up", octaves=2)
Parameters:
- ``param``: Parameter name to modulate (``"lowpass"``, ``"reverb"``,
``"distortion"``, ``"volume"``, ``"chorus"``, ``"delay"``).
- ``rate``: LFO speed in cycles per bar (default 0.5 = one sweep
every 2 bars). 0.25 = very slow, 1 = once per bar, 4 = four times
per bar.
- ``min`` / ``max``: Parameter value range.
- ``bars``: Number of bars to run the LFO over (default 4).
- ``shape``: Waveform shape.
- ``"sine"`` -- smooth, natural sweep
- ``"triangle"`` -- linear up/down
- ``"saw"`` -- ramp up, snap back
- ``"square"`` -- abrupt on/off
- ``resolution``: How often to insert automation points, in beats
(default 0.25 = every 16th note). Lower values = smoother curves.
Stacking Multiple LFOs
~~~~~~~~~~~~~~~~~~~~~~~
Call ``.lfo()`` multiple times to modulate different parameters
simultaneously:
.. code-block:: python
lead = score.part("lead", synth="saw", lowpass=800, reverb=0.1)
# Filter opens over 8 bars
lead.lfo("lowpass", rate=0.125, min=400, max=4000, bars=8)
# Reverb swells in and out every 2 bars
lead.lfo("reverb", rate=0.5, min=0.1, max=0.6, bars=8, shape="triangle")
# Volume tremolo
lead.lfo("volume", rate=2, min=0.3, max=0.6, bars=8, shape="sine")
lead.arpeggio("Cm", bars=8, pattern="updown", octaves=2)
Effects are what turn notes into music -- the space, the movement, the character. A dry signal is just information; reverb, delay, and filtering are what make it feel like something. Experiment freely, trust your ears.
+167 -95
View File
@@ -31,29 +31,42 @@ Guitars
This tuning uses intervals of a perfect 4th (5 semitones) between most
strings, except between G and B which is a major 3rd (4 semitones).
.. code-block:: python
.. code-block:: pycon
from pytheory import Fretboard
>>> from pytheory import Fretboard
guitar = Fretboard.guitar() # Standard EADGBE
twelve = Fretboard.twelve_string() # 12-string (6 doubled courses)
bass = Fretboard.bass() # Standard 4-string EADG
bass5 = Fretboard.bass(five_string=True) # 5-string with low B
>>> guitar = Fretboard.guitar() # Standard EADGBE
>>> twelve = Fretboard.twelve_string() # 12-string (6 doubled courses)
>>> bass = Fretboard.bass() # Standard 4-string EADG
>>> bass5 = Fretboard.bass(five_string=True) # 5-string with low B
**Alternate tunings** — 8 built-in presets:
.. code-block:: python
.. code-block:: pycon
Fretboard.guitar("drop d") # DADGBE — heavy riffs, metal
Fretboard.guitar("open g") # DGDGBD — slide guitar, Keith Richards
Fretboard.guitar("open d") # DADF#AD — slide, folk
Fretboard.guitar("open e") # EBEG#BE — slide blues
Fretboard.guitar("open a") # EAC#EAE
Fretboard.guitar("dadgad") # DADGAD — Celtic, fingerstyle
Fretboard.guitar("half step down") # Eb standard — Hendrix, SRV
>>> Fretboard.guitar("drop d") # DADGBE — heavy riffs, metal
>>> Fretboard.guitar("open g") # DGDGBD — slide guitar, Keith Richards
>>> Fretboard.guitar("open d") # DADF#AD — slide, folk
>>> Fretboard.guitar("open e") # EBEG#BE — slide blues
>>> Fretboard.guitar("open a") # EAC#EAE
>>> Fretboard.guitar("dadgad") # DADGAD — Celtic, fingerstyle
>>> Fretboard.guitar("half step down") # Eb standard — Hendrix, SRV
# Custom tuning with any notes
Fretboard.guitar(("C4", "G3", "C3", "G2", "C2", "G1"))
>>> # Custom tuning with any notes
>>> Fretboard.guitar(("C4", "G3", "C3", "G2", "C2", "G1"))
**Capo** — a `capo <https://en.wikipedia.org/wiki/Capo>`_ raises all
strings by a number of frets, letting you play open chord shapes in
higher keys:
.. code-block:: pycon
>>> # Capo on fret 2 — open G shape now sounds as A major
>>> fb = Fretboard.guitar(capo=2)
>>> # Or apply a capo to an existing fretboard
>>> fb = Fretboard.guitar()
>>> fb_capo3 = fb.capo(3)
The Mandolin Family
-------------------
@@ -63,12 +76,12 @@ mirrors the `violin family <https://en.wikipedia.org/wiki/Violin_family>`_
— all tuned in perfect fifths, with each member a fifth or octave
lower than the last:
.. code-block:: python
.. code-block:: pycon
Fretboard.mandolin() # E5 A4 D4 G3 — soprano (= violin)
Fretboard.mandola() # A4 D4 G3 C3 — alto (= viola)
Fretboard.octave_mandolin() # E4 A3 D3 G2 — tenor (octave below mandolin)
Fretboard.mandocello() # A3 D3 G2 C2 — bass (= cello)
>>> Fretboard.mandolin() # E5 A4 D4 G3 — soprano (= violin)
>>> Fretboard.mandola() # A4 D4 G3 C3 — alto (= viola)
>>> Fretboard.octave_mandolin() # E4 A3 D3 G2 — tenor (octave below mandolin)
>>> Fretboard.mandocello() # A3 D3 G2 C2 — bass (= cello)
The mandolin's doubled courses (pairs of strings) create a natural
chorus effect. The `octave mandolin <https://en.wikipedia.org/wiki/Octave_mandolin>`_
@@ -80,12 +93,12 @@ The Bowed String Family
The orchestral `string family <https://en.wikipedia.org/wiki/String_section>`_
is tuned in perfect fifths (except the double bass, which uses fourths):
.. code-block:: python
.. code-block:: pycon
Fretboard.violin() # E5 A4 D4 G3 — soprano
Fretboard.viola() # A4 D4 G3 C3 — alto (5th below violin)
Fretboard.cello() # A3 D3 G2 C2 — tenor/bass (octave below viola)
Fretboard.double_bass() # G2 D2 A1 E1 — bass (tuned in 4ths!)
>>> Fretboard.violin() # E5 A4 D4 G3 — soprano
>>> Fretboard.viola() # A4 D4 G3 C3 — alto (5th below violin)
>>> Fretboard.cello() # A3 D3 G2 C2 — tenor/bass (octave below viola)
>>> Fretboard.double_bass() # G2 D2 A1 E1 — bass (tuned in 4ths!)
Bowed strings have no frets — the player can produce any pitch along
the fingerboard, enabling continuous
@@ -95,19 +108,19 @@ inflections not possible on fretted instruments.
The `erhu <https://en.wikipedia.org/wiki/Erhu>`_ — a 2-stringed Chinese
bowed instrument with a hauntingly vocal quality:
.. code-block:: python
.. code-block:: pycon
Fretboard.erhu() # A4 D4 — tuned a 5th apart, no fingerboard
>>> Fretboard.erhu() # A4 D4 — tuned a 5th apart, no fingerboard
Plucked Strings
---------------
.. code-block:: python
.. code-block:: pycon
Fretboard.ukulele() # A4 E4 C4 G4 — re-entrant tuning
Fretboard.banjo() # Open G (bluegrass, 5th string is high drone)
Fretboard.banjo("open d") # Open D (clawhammer, old-time)
Fretboard.harp() # 47 strings, C1 to G7 (concert pedal harp)
>>> Fretboard.ukulele() # A4 E4 C4 G4 — re-entrant tuning
>>> Fretboard.banjo() # Open G (bluegrass, 5th string is high drone)
>>> Fretboard.banjo("open d") # Open D (clawhammer, old-time)
>>> Fretboard.harp() # 47 strings, C1 to G7 (concert pedal harp)
The `banjo <https://en.wikipedia.org/wiki/Banjo>`_'s short 5th string
is a high drone — a defining feature of the instrument's sound.
@@ -119,28 +132,28 @@ by up to two semitones across all octaves simultaneously.
World Instruments
-----------------
.. code-block:: python
.. code-block:: pycon
# Middle Eastern
Fretboard.oud() # C4 G3 D3 A2 G2 C2 — fretless, ancestor of the lute
Fretboard.sitar() # 7 main strings — Indian classical
>>> # Middle Eastern
>>> Fretboard.oud() # C4 G3 D3 A2 G2 C2 — fretless, ancestor of the lute
>>> Fretboard.sitar() # 7 main strings — Indian classical
# East Asian
Fretboard.shamisen() # C4 G3 C3 — 3-string Japanese, honchoshi tuning
Fretboard.pipa() # D4 A3 E3 A2 — 4-string Chinese lute
Fretboard.erhu() # A4 D4 — 2-string Chinese bowed
>>> # East Asian
>>> Fretboard.shamisen() # C4 G3 C3 — 3-string Japanese, honchoshi tuning
>>> Fretboard.pipa() # D4 A3 E3 A2 — 4-string Chinese lute
>>> Fretboard.erhu() # A4 D4 — 2-string Chinese bowed
# European
Fretboard.bouzouki() # D4 A3 D3 G2 — Irish (Celtic music)
Fretboard.bouzouki("greek") # D4 A3 F3 C3 — Greek
Fretboard.lute() # G4 D4 A3 F3 C3 G2 — Renaissance (6 courses)
Fretboard.balalaika() # A4 E4 E4 — Russian (2 unison strings)
>>> # European
>>> Fretboard.bouzouki() # D4 A3 D3 G2 — Irish (Celtic music)
>>> Fretboard.bouzouki("greek") # D4 A3 F3 C3 — Greek
>>> Fretboard.lute() # G4 D4 A3 F3 C3 G2 — Renaissance (6 courses)
>>> Fretboard.balalaika() # A4 E4 E4 — Russian (2 unison strings)
# Latin American
Fretboard.charango() # E5 A4 E5 C5 G4 — Andean (re-entrant tuning)
>>> # Latin American
>>> Fretboard.charango() # E5 A4 E5 C5 G4 — Andean (re-entrant tuning)
# Steel guitar
Fretboard.pedal_steel() # 10 strings, E9 Nashville — country music
>>> # Steel guitar
>>> Fretboard.pedal_steel() # 10 strings, E9 Nashville — country music
The `oud <https://en.wikipedia.org/wiki/Oud>`_ is fretless, allowing
the quarter-tone inflections essential to
@@ -151,12 +164,12 @@ sympathetic strings that resonate in harmony with the played notes.
Keyboards
---------
.. code-block:: python
.. code-block:: pycon
Fretboard.keyboard() # 88-key piano (A0 to C8)
Fretboard.keyboard(61, "C2") # 61-key synth controller
Fretboard.keyboard(49, "C2") # 49-key controller
Fretboard.keyboard(25, "C3") # 25-key mini MIDI controller
>>> Fretboard.keyboard() # 88-key piano (A0 to C8)
>>> Fretboard.keyboard(61, "C2") # 61-key synth controller
>>> Fretboard.keyboard(49, "C2") # 49-key controller
>>> Fretboard.keyboard(25, "C3") # 25-key mini MIDI controller
While keyboards don't have strings or frets, they map naturally to a
sequence of tones. A full 88-key piano spans over 7 octaves — the
@@ -172,78 +185,137 @@ on any instrument. It scores each possibility by:
2. Preferring **ascending** fret patterns — easier hand position
3. Minimizing the number of **fingers needed**
.. code-block:: python
.. code-block:: pycon
from pytheory import Fretboard, CHARTS
>>> from pytheory import Fretboard
fb = Fretboard.guitar()
c = CHARTS["western"]["C"]
>>> fb = Fretboard.guitar()
>>> f = fb.chord("C")
>>> f
Fingering(e=0, B=1, G=0, D=2, A=3, E=x)
# Best single fingering
print(c.fingering(fretboard=fb))
# (0, 1, 0, 2, 3, 0)
>>> f['A']
3
>>> f[1]
1
# All equally-scored fingerings
all_c = c.fingering(fretboard=fb, multiple=True)
>>> f.identify()
'C major'
# Muted strings appear as None
f = CHARTS["western"]["F"]
print(f.fingering(fretboard=fb))
>>> chord = f.to_chord()
>>> chord.identify()
'C major'
You can also go from fret positions to chord identification:
.. code-block:: pycon
>>> # "What chord am I playing?"
>>> fb = Fretboard.guitar()
>>> f = fb.fingering(0, 0, 0, 2, 2, 0)
>>> f
Fingering(e=0, B=0, G=0, D=2, A=2, E=0)
>>> f.identify()
'E minor'
Reading Fingerings
~~~~~~~~~~~~~~~~~~
The tuple ``(0, 1, 0, 2, 3, 0)`` reads from the highest string to the
lowest::
Each position is labeled with its string name. Duplicate string names
are disambiguated — on a standard guitar, high E appears as ``e`` and
low E as ``E``::
e|--0-- (open — E)
B|--1-- (fret 1 — C)
G|--0-- (open — G)
D|--2-- (fret 2 — E)
A|--3-- (fret 3 — C)
E|--0-- (open — E)
E|--x-- (muted)
A value of ``None`` means the string is muted (not played).
A value of ``x`` (``None``) means the string is muted (not played).
ASCII Tablature
~~~~~~~~~~~~~~~
For a more visual representation, use ``tab()``:
.. code-block:: pycon
>>> print(fb.tab("C"))
C major
e|--0--
B|--1--
G|--0--
D|--2--
A|--3--
E|--x--
Generating Full Charts
----------------------
Generate fingerings for every chord at once:
.. code-block:: python
.. code-block:: pycon
from pytheory import Fretboard, charts_for_fretboard
>>> fb = Fretboard.guitar()
>>> chart = fb.chart()
fb = Fretboard.guitar()
chart = charts_for_fretboard(fretboard=fb)
>>> chart["C"]
Fingering(e=0, B=1, G=0, D=2, A=3, E=x)
for name, fingering in chart.items():
print(f"{name:6s} {fingering}")
>>> # Works with any instrument
>>> uke_chart = Fretboard.ukulele().chart()
>>> mando_chart = Fretboard.mandolin().chart()
# Works with any instrument
uke_chart = charts_for_fretboard(fretboard=Fretboard.ukulele())
mando_chart = charts_for_fretboard(fretboard=Fretboard.mandolin())
Scale Diagrams with Chord Highlighting
---------------------------------------
The ``scale_diagram()`` method renders an ASCII fretboard showing where
scale notes fall on each string. Pass an optional ``chord`` argument to
highlight chord tones in UPPERCASE while scale-only tones appear in
lowercase — a quick way to visualize target notes for soloing:
.. code-block:: pycon
>>> from pytheory import Fretboard, TonedScale, Chord
>>> fb = Fretboard.guitar()
>>> pentatonic = TonedScale(tonic="A4")["minor pentatonic"]
>>> print(fb.scale_diagram(pentatonic, frets=5))
>>> # Highlight Am chord tones within the scale:
>>> am = Chord.from_symbol("Am")
>>> print(fb.scale_diagram(pentatonic, frets=5, chord=am))
Non-String Instruments
----------------------
Looking for drums and percussion? PyTheory also supports drum pattern
programming through the sequencing engine. See the :doc:`drums` guide
for drum kits, patterns, and fills.
Custom Instruments
------------------
Any instrument can be modeled with custom string tunings:
.. code-block:: python
.. code-block:: pycon
from pytheory import Tone, Fretboard
>>> from pytheory import Tone, Fretboard
# Baritone ukulele (DGBE — top 4 guitar strings)
bari_uke = Fretboard(tones=[
Tone.from_string("E4"),
Tone.from_string("B3"),
Tone.from_string("G3"),
Tone.from_string("D3"),
])
>>> # Baritone ukulele (DGBE — top 4 guitar strings)
>>> bari_uke = Fretboard(tones=[
... Tone.from_string("E4"),
... Tone.from_string("B3"),
... Tone.from_string("G3"),
... Tone.from_string("D3"),
... ])
# Tres cubano (Cuban guitar, 3 doubled courses)
tres = Fretboard(tones=[
Tone.from_string("E4"),
Tone.from_string("B3"),
Tone.from_string("G3"),
])
>>> # Tres cubano (Cuban guitar, 3 doubled courses)
>>> tres = Fretboard(tones=[
... Tone.from_string("E4"),
... Tone.from_string("B3"),
... Tone.from_string("G3"),
... ])
If it has strings, you can model it. Define the tuning, and PyTheory handles the rest -- fingerings, charts, scale diagrams, all of it. Got a weird instrument or a custom tuning? That's what the ``Fretboard`` constructor is for.
+194 -50
View File
@@ -1,8 +1,21 @@
Audio Playback
==============
Playback and Export
===================
PyTheory can synthesize and play tones and chords through your speakers
using basic `waveform <https://en.wikipedia.org/wiki/Waveform>`_ synthesis.
This is the output layer. You've built your theory, composed your
arrangement, shaped your sounds -- now you need to hear it. PyTheory
gives you three ways to get your music out: speakers, WAV files, and
MIDI files.
Use **speakers** for immediate feedback while you're sketching and
experimenting. Use **WAV export** when you want to share actual audio
-- post it, send it, drop it into a video. Use **MIDI export** when you
want to bring your sketch into a real DAW and finish it with
professional instruments, mixing, and mastering. Each output serves a
different stage of the creative process.
PyTheory can play audio through your speakers, save to WAV, or export
to MIDI. Everything is synthesized from waveforms -- no samples or
external audio files needed.
.. note::
@@ -10,71 +23,202 @@ using basic `waveform <https://en.wikipedia.org/wiki/Waveform>`_ synthesis.
installed on your system. On macOS: ``brew install portaudio``.
On Ubuntu: ``apt install libportaudio2``.
Playing a Tone
--------------
play() -- Single Tones and Chords
---------------------------------
The simplest way to hear something:
.. code-block:: python
from pytheory import Tone, play
from pytheory import Tone, Chord, play
a4 = Tone.from_string("A4", system="western")
play(a4, t=1_000) # Play A440 for 1 second
play(Tone.from_string("A4"), t=1_000) # A440 for 1 second
play(Chord.from_symbol("Am7"), t=2_000) # chord for 2 seconds
Playing a Chord
---------------
Optional parameters for synth, envelope, and temperament:
.. code-block:: python
from pytheory import Chord, Tone, play
from pytheory import Synth, Envelope
c_major = Chord(tones=[
Tone.from_string("C4", system="western"),
Tone.from_string("E4", system="western"),
Tone.from_string("G4", system="western"),
])
play(c_major, t=2_000) # Play for 2 seconds
play(Tone.from_string("C4"), synth=Synth.SAW, envelope=Envelope.PLUCK, t=1_000)
play(Tone.from_string("C4"), temperament="pythagorean", t=1_000)
Waveform Types
--------------
play_score() -- Full Arrangements
---------------------------------
The waveform shape determines the `timbre <https://en.wikipedia.org/wiki/Timbre>`_ (tonal color) of the sound.
Different waveforms contain different combinations of **harmonics**
integer multiples of the fundamental frequency.
- `Sine wave <https://en.wikipedia.org/wiki/Sine_wave>`_ — the purest tone. Contains only the fundamental
frequency with no harmonics. Sounds smooth, clear, and "electronic."
This is the building block of all other waveforms (`Fourier's theorem <https://en.wikipedia.org/wiki/Fourier_series>`_).
- `Sawtooth wave <https://en.wikipedia.org/wiki/Sawtooth_wave>`_ — contains all harmonics (both odd and even),
each at amplitude 1/n. Sounds bright, buzzy, and aggressive.
Named for its shape. Used extensively in `additive synthesis <https://en.wikipedia.org/wiki/Additive_synthesis>`_ and analog synthesizers.
- `Triangle wave <https://en.wikipedia.org/wiki/Triangle_wave>`_ — contains only odd harmonics, each at amplitude
1/n². Sounds softer and more mellow than sawtooth — somewhere between
sine and sawtooth. Often described as "woody" or "hollow."
Plays a ``Score`` with all its parts and drums mixed together.
Output is **stereo** — each part is panned according to its ``pan``
setting, drums are stereo-panned like a real kit, and reverb tails
have natural stereo width. A **master bus compressor/limiter** (4:1
ratio, brick-wall at 0.95) is applied to prevent clipping and make
the mix louder and punchier:
.. code-block:: python
from pytheory import play, Synth, Tone
from pytheory import Score, Duration, Chord
from pytheory.play import play_score
tone = Tone.from_string("C4", system="western")
score = Score("4/4", bpm=140)
score.drums("bossa nova", repeats=4)
chords = score.part("chords", synth="sine", envelope="pad")
for sym in ["Am", "Dm", "E7", "Am"]:
chords.add(Chord.from_symbol(sym), Duration.WHOLE)
play_score(score)
play(tone, synth=Synth.SINE) # Pure, clean
play(tone, synth=Synth.SAW) # Bright, buzzy
play(tone, synth=Synth.TRIANGLE) # Mellow, hollow
.. raw:: html
Temperaments
------------
<audio controls style="width:100%;margin:0.5em 0 1.5em"><source src="../_static/audio/playback_basic.wav" type="audio/wav"></audio>
Hear the difference between tuning systems:
The render pipeline respects the Score's ``temperament`` and
``reference_pitch`` settings, so Baroque or microtonal scores play back
at the correct tuning:
.. code-block:: python
play(tone, temperament="equal") # Modern standard (since ~1917)
play(tone, temperament="pythagorean") # Pure fifths, wolf intervals
play(tone, temperament="meantone") # Pure thirds, Renaissance sound
score = Score("4/4", bpm=80, temperament="meantone", reference_pitch=415.0)
Try playing a C major chord in each temperament — you'll hear subtle
differences in the "color" of the major third. Equal temperament is
a compromise; the other systems sacrifice some keys to make the good
keys sound better.
Press **Ctrl+C** at any time during playback to stop — PyTheory catches
``KeyboardInterrupt`` and stops audio cleanly.
See :doc:`sequencing` for how to build scores and parts.
render_score() -- Headless Rendering
------------------------------------
Returns a raw audio buffer (numpy float32 array) without playing it.
Useful for saving to WAV or further processing:
.. code-block:: pycon
>>> from pytheory.play import render_score
>>> buf = render_score(score) # numpy float32 array
>>> len(buf)
604800
save() -- WAV Export
--------------------
Render tones or chords to a WAV file. Works without speakers or
PortAudio:
.. code-block:: python
from pytheory import save, Chord, Tone, Synth
save(Tone.from_string("A4"), "a440.wav", t=1_000)
save(Chord.from_name("Am7"), "am7.wav", t=2_000)
save(
Chord.from_name("C"),
"c_triangle.wav",
synth=Synth.TRIANGLE,
temperament="meantone",
t=3_000,
)
save_midi() -- MIDI Export
--------------------------
MIDI export is probably the most useful feature here for working
musicians. The idea is simple: sketch your ideas in Python -- where
iteration is fast, where you can use loops and randomness and music
theory functions -- and then export to MIDI. Open that MIDI file in
Logic, Ableton, Reaper, FL Studio, or whatever you use, and now you've
got your chord progressions, melodies, and bass lines on real tracks.
Swap in your favorite soft synths, add real mixing, finish the track
properly. Python is the sketchpad; the DAW is the canvas.
Export tones, chords, progressions, or full scores as Standard MIDI
Files. MIDI files can be opened in any DAW, edited, transposed, and
assigned to any instrument.
Simple export (single tone, chord, or progression):
.. code-block:: python
from pytheory import save_midi, Key, Tone, Chord
save_midi(Tone.from_string("C4"), "middle_c.mid", t=1000)
save_midi(Chord.from_symbol("Am7"), "am7.mid")
chords = Key("C", "major").progression("I", "V", "vi", "IV")
save_midi(chords, "pop.mid", t=500, bpm=120)
Score-based export (with time signature, tempo, and parts):
.. code-block:: python
from pytheory import Score, Duration, Key
score = Score("4/4", bpm=140)
for chord in Key("G", "major").progression("I", "IV", "V", "I"):
score.add(chord, Duration.WHOLE)
score.save_midi("progression.mid")
play_pattern() -- Drum Patterns
-------------------------------
Play a drum pattern through the speakers:
.. code-block:: python
from pytheory import Pattern
from pytheory.play import play_pattern
play_pattern(Pattern.preset("rock"), repeats=4, bpm=120)
play_pattern(Pattern.preset("bossa nova"), repeats=4, bpm=140)
See :doc:`drums` for the full list of 80+ presets and 21 fills.
play_progression() -- Quick Chord Playback
------------------------------------------
Play a chord progression in sequence with a single call:
.. code-block:: python
from pytheory import Key, play_progression
chords = Key("C", "major").progression("I", "V", "vi", "IV")
play_progression(chords, t=800)
Optional synth, envelope, and gap parameters:
.. code-block:: python
from pytheory import Synth, Envelope
play_progression(chords, t=1000, synth=Synth.TRIANGLE, gap=200)
play_progression(chords, t=2000, envelope=Envelope.PAD)
That's the workflow: hear it, tweak it, hear it again. When it sounds right, export to WAV or MIDI and take it somewhere bigger.
MIDI Import
-----------
Load any Standard MIDI File into a Score — then play it through
PyTheory's synth engine with effects, or analyze the theory:
.. code-block:: python
from pytheory import Score
from pytheory.play import play_score
score = Score.from_midi("song.mid")
# See what's inside
for name, part in score.parts.items():
print(f"{name}: {len(part.notes)} notes")
# Change the synth and add effects
score.parts["ch1"].synth = "saw"
score.parts["ch1"].reverb_mix = 0.3
play_score(score)
Each MIDI channel becomes a named Part (``ch1``, ``ch2``, etc.).
Channel 10 (drums) becomes drum hits. Tempo, time signature,
note durations, and velocities are all preserved.
Download any MIDI file from the internet, load it, play it through
the synth engine with reverb and delay. That's the whole idea.
+217 -40
View File
@@ -1,6 +1,18 @@
Quickstart
==========
PyTheory works at two levels — pick the one that fits what you need:
1. **Music theory** — explore scales, chords, keys, intervals, and
harmony. No audio required. Works anywhere Python runs.
2. **Composition** — build multi-part arrangements with drums, synths,
effects, and export to MIDI. Needs PortAudio for live playback.
Both are first-class. You can use PyTheory purely as a theory
reference and never touch the audio side, or you can jump straight
into composing. This guide covers both paths.
Installation
------------
@@ -8,55 +20,220 @@ Installation
$ pip install pytheory
Basic Usage
-----------
For audio playback through your speakers, you'll also need
`PortAudio <http://www.portaudio.com/>`_:
Create tones, build scales, and explore music theory:
- macOS: ``brew install portaudio``
- Ubuntu: ``apt install libportaudio2``
- Windows: included with the ``sounddevice`` package
PortAudio is only needed for live playback. MIDI export, WAV export,
and all theory functions work without it.
Hear Something Immediately
--------------------------
::
$ pytheory demo
This generates and plays a random track — different every time. It's
the fastest way to hear what PyTheory can do.
Explore Music Theory
--------------------
The theory layer is where most people start. No audio setup needed —
this works everywhere Python runs. Every concept in Western music
theory (and five other systems) has a clean Python API.
Tones and intervals:
.. code-block:: pycon
>>> from pytheory import Tone
>>> c4 = Tone.from_string("C4", system="western")
>>> c4.frequency
261.6255653005986
>>> c4.midi
60
>>> c4 + 7
<Tone G4>
>>> c4.interval_to(c4 + 7)
'perfect 5th'
>>> Tone.from_frequency(440)
<Tone A4>
>>> Tone.from_midi(69)
<Tone A4>
Keys, scales, and chords:
.. code-block:: pycon
>>> from pytheory import Key, Chord
>>> key = Key("C", "major")
>>> key.chords
['C major', 'D minor', 'E minor', 'F major', 'G major', 'A minor', 'B diminished']
>>> [c.symbol for c in key.progression("I", "V", "vi", "IV")]
['C', 'G', 'Am', 'F']
>>> key.signature
{'sharps': 0, 'flats': 0, 'accidentals': []}
>>> Key("F", "major").signature
{'sharps': 0, 'flats': 1, 'accidentals': ['Bb']}
>>> Chord.from_symbol("Am7").identify()
'A minor 7th'
>>> Chord.from_tones("G", "B", "D", "F").analyze("C")
'V7'
Harmonic analysis and modulation:
.. code-block:: pycon
>>> Key("C", "major").pivot_chords(Key("G", "major"))
['A minor', 'B minor', 'C major', 'D major', 'E minor', 'G major']
>>> Key("C", "major").relative
<Key A minor>
>>> key.suggest_next(key.triad(4)) # what follows V?
[<Chord C major>, <Chord A minor>, <Chord F major>]
Scales across 6 musical systems:
.. code-block:: pycon
>>> from pytheory import TonedScale
>>> TonedScale(tonic="Sa4", system="indian")["bhairav"].note_names
['Sa', 'komal Re', 'Ga', 'Ma', 'Pa', 'komal Dha', 'Ni', 'Sa']
>>> TonedScale(tonic="Do4", system="arabic")["hijaz"].note_names
['Do', 'Reb', 'Mi', 'Fa', 'Sol', 'Solb', 'Sib', 'Do']
>>> TonedScale(tonic="C4", system="japanese")["hirajoshi"].note_names
['C', 'D', 'D#', 'G', 'G#', 'C']
Guitar fingerings:
.. code-block:: pycon
>>> from pytheory import Fretboard
>>> fb = Fretboard.guitar()
>>> fb.chord("Am")
Fingering(e=0, B=1, G=2, D=2, A=0, E=x)
All of the above works without PortAudio, without sounddevice,
without any audio setup at all. It's pure Python music theory.
Compose a Track
---------------
This is where it gets fun. A ``Score`` is your arrangement — drums,
chords, melody, bass, each with their own synth and effects:
.. code-block:: python
from pytheory import Tone, TonedScale, Fretboard, CHARTS
from pytheory import Score, Key, Duration
from pytheory.play import play_score
# Create a tone — A4 is the tuning standard (440 Hz)
a4 = Tone.from_string("A4", system="western")
print(a4.frequency) # 440.0
score = Score("4/4", bpm=120)
score.drums("rock", repeats=8, fill="rock", fill_every=4)
# Tone arithmetic — add semitones to move up the chromatic scale
c4 = Tone.from_string("C4", system="western")
e4 = c4 + 4 # Major third up (4 semitones)
g4 = c4 + 7 # Perfect fifth up (7 semitones)
print(e4, g4) # E4 G4
piano = score.part("piano", instrument="piano", reverb=0.3)
lead = score.part("lead", synth="saw", envelope="pluck",
delay=0.2, reverb=0.2, lowpass=4000)
bass = score.part("bass", synth="triangle", lowpass=900)
# Measure intervals between tones
print(g4 - c4) # 7 (semitones — a perfect fifth)
for chord in Key("G", "major").progression("I", "V", "vi", "IV") * 2:
piano.add(chord, Duration.WHOLE)
# Build a C major scale
c_major = TonedScale(tonic="C4")["major"]
print(c_major.note_names)
# ['C', 'D', 'E', 'F', 'G', 'A', 'B', 'C']
lead.add("D5", 1).add("B4", 0.5).add("D5", 0.5)
lead.add("G5", 1).add("E5", 1)
lead.add("D5", 0.5).add("B4", 0.5).add("A4", 1)
lead.add("G4", 2).rest(2)
# Build diatonic triads from the scale
I = c_major.triad(0) # C E G (C major)
IV = c_major.triad(3) # F A C (F major)
V = c_major.triad(4) # G B D (G major)
for n in ["G2", "G2", "D2", "D2", "E2", "E2", "C2", "C2"] * 2:
bass.add(n, Duration.HALF)
# Guitar chord fingerings
fb = Fretboard.guitar()
fingering = CHARTS["western"]["Am"].fingering(fretboard=fb)
print(fingering) # (0, 1, 2, 2, 0, 0)
play_score(score)
What's Included
---------------
.. raw:: html
- **6 musical systems**: Western, Indian (Hindustani), Arabic (Maqam),
Japanese, Blues/Pentatonic, Javanese Gamelan
- **40+ scales**: major, minor, harmonic minor, 7 modes, 10 thaats,
10 maqamat, 6 Japanese pentatonic scales, blues, pentatonic,
slendro, pelog, and more
- **Pitch calculation** in equal, Pythagorean, and meantone temperaments
- **Chord charts** with 144 pre-built chords (12 roots x 12 qualities)
- **Chord analysis**: consonance scoring, Plomp-Levelt dissonance,
beat frequency calculation
- **Fingering generation** for guitar (8 tunings), bass, ukulele, or
any custom fretted instrument
- **Audio playback** with sine, sawtooth, and triangle wave synthesis
<audio controls style="width:100%;margin:0.5em 0 1.5em"><source src="../_static/audio/quickstart.wav" type="audio/wav"></audio>
Export to Your DAW
------------------
The whole point: sketch in Python, finish in Logic / Ableton / Reaper.
.. code-block:: python
score.save_midi("my_sketch.mid")
Open that file in any DAW and you'll see all the notes laid out on
the timeline, ready to assign to real instruments and mix.
You can also save rendered audio:
.. code-block:: python
from pytheory import save
save(Chord.from_symbol("Am7"), "am7.wav", t=2_000)
What's in the Box
-----------------
**Theory** — tones, scales (40+ across 6 musical systems), chords
(17 types, Roman numeral analysis, figured bass, tension scoring,
voice leading, pitch class sets with Forte numbers), keys (detection,
signatures, modulation paths, borrowed chords), scale recommendation.
**Sequencing** — Score, Part, Duration, TimeSignature. Arpeggiator
with 5 patterns. Legato with pitch glide. Per-note velocity. Swing.
Tempo changes. Fade in/out. Song sections with repeat. Humanize.
**Synthesis** — 10 waveforms: sine, saw, triangle, square, pulse, FM,
noise, supersaw, PWM slow, PWM fast. 8 ADSR envelopes. Detune.
Stereo pan and spread.
**Effects** — distortion, chorus, lowpass filter (with resonance),
delay, reverb (algorithmic + 7 stereo convolution presets including
Taj Mahal with 12-second tail). All per-part with automation and
LFO modulation. Sidechain compression. Master bus compressor/limiter.
**Drums** — 58 pattern presets (rock, jazz, salsa, bossa nova,
afrobeat, house, trap, and 50+ more). 21 fill presets. 27 synthesized
drum voices with stereo panning.
**Instruments** — 25 presets (guitar with 8 tunings, bass, ukulele,
mandolin family, violin family, banjo, harp, oud, sitar, erhu, and
more) with chord fingering generation and scale diagrams.
**Output** — stereo playback, WAV export, MIDI import/export.
**Interface** — REPL with tab completion (``pytheory repl``), CLI with
15 commands. ``pytheory demo``, ``pytheory key``, ``pytheory chord``,
``pytheory identify``, ``pytheory midi``, ``pytheory play``, and more.
Where to Go Next
-----------------
- :doc:`theory` — music theory fundamentals
- :doc:`tones` — working with individual notes
- :doc:`scales` — scales, modes, and keys
- :doc:`chords` — chord construction, analysis, and progressions
- :doc:`sequencing` — composing multi-part arrangements
- :doc:`synths` — the 10 waveforms and 8 envelopes
- :doc:`effects` — reverb, delay, distortion, chorus, lowpass, automation
- :doc:`drums` — 58 patterns, 21 fills, drum synthesis
- :doc:`playback` — play, save, export
+276
View File
@@ -0,0 +1,276 @@
Interactive REPL
================
PyTheory includes an interactive scratchpad for exploring music theory,
hearing ideas instantly, and building arrangements — all without writing
a Python script.
::
$ pytheory repl
The REPL is two things at once: a **theory calculator** (what chords
are in this key? what's the interval between these notes?) and a
**composition sketchpad** (add drums, layer parts, tweak effects, hear
it, export MIDI). Use whichever side you need.
Getting Started
---------------
The welcome screen tells you everything you need::
♫ PyTheory REPL
════════════════════════════════════════
try: key Am — set a key
chords — see its chords
prog I V vi IV — hear a progression
drums bossa nova
play_score — hear it all
help for all commands, quit to exit
Type those five things in order and you'll have music playing in
30 seconds.
The Prompt
----------
The prompt shows your current state — key, tempo, drums, active part,
and effects. It starts compact and grows as you add context::
pytheory[key=C | bpm=120]>
pytheory[key=Am | bpm=140]>
pytheory[key=Am | bpm=140 | drums=bossa nova]>
pytheory[key=Am | bpm=140 | drums=bossa nova | →lead(saw)]>
When it gets long, it stacks into two lines::
key=Am | bpm=140 | drums=bossa nova | →lead(saw) rev=0.3 lp=2000
♫>
You always know where you are.
Theory Commands
---------------
These work without any audio setup. Pure theory exploration.
Set a key and explore it::
pytheory> key Am
A minor: A B C D E F G A
pytheory> chords
i A minor
ii° B diminished
III C major
iv D minor
v E minor
VI F major
VII G major
pytheory> modes
ionian A B C# D E F# G# A
dorian A B C D E F# G A
phrygian A Bb C D E F G A
...
pytheory> scales
major A B C# D E F# G# A
minor A B C D E F G A
harmonic minor A B C D E F G# A
...
Build progressions::
pytheory> prog I V vi IV
Am → Em → F → Dm
pytheory> progression i iv V i
Am → Dm → E → Am
Explore intervals and chords::
pytheory> interval C4 G4
C4 → G4: perfect 5th
7 semitones
pytheory> identify C E G
C major
symbol: C
pytheory> identify F#m7b5
F# half-diminished 7th
symbol: F#m7b5
tones: F#4 A4 C5 E5
intervals: [3, 3, 4]
Circle of fifths::
pytheory> circle
fifths: A → E → B → F# → C# → G# → D# → A# → F → C → G → D
fourths: A → D → G → C → F → A# → D# → G# → C# → F# → B → E
Other musical systems::
pytheory> system indian
system: indian
scales: chromatic, bilawal, khamaj, kafi, ...
pytheory> system arabic
system: arabic
scales: chromatic, ajam, nahawand, kurd, hijaz, ...
Guitar::
pytheory> fingering Am
Am
E|--0--
B|--1--
G|--2--
D|--2--
A|--0--
E|--x--
pytheory> diagram minor 5
0 1 2 3 4 5
E| E | F | - | G | - | A |
...
Composition Commands
--------------------
When you're ready to make sound, add drums and parts.
Drums::
pytheory> drums bossa nova
score.drums("bossa nova", repeats=4)
pytheory> drums
(lists all 58 presets)
Parts — each with its own synth and envelope::
pytheory> part lead saw pluck
score.part("lead", synth="saw", envelope="pluck")
pytheory> part chords fm pad
score.part("chords", synth="fm", envelope="pad")
pytheory> part bass sine pluck
score.part("bass", synth="sine", envelope="pluck")
pytheory> part
lead: synth=saw envelope=pluck vol=0.5 ←
chords: synth=fm envelope=pad vol=0.5
bass: synth=sine envelope=pluck vol=0.5
The arrow (````) shows which part is active. Switch with
``part <name>``.
Add notes, chords, arpeggios::
pytheory> add C5 1
.add("C5", 1.0)
pytheory> add Am 4
.add(Chord.from_symbol("Am"), 4.0)
pytheory> add E5 0.67 110
.add("E5", 0.67, velocity=110)
pytheory> rest 2
.rest(2.0)
pytheory> arp Am updown 2 2
.arpeggio("Am", pattern="updown", bars=2.0, octaves=2)
pytheory> prog i iv V i
Am → Dm → E → Am
Effects
-------
Set effects on the active part — mirrors the Python API::
pytheory> reverb 0.4
pytheory> delay 0.3 0.375
pytheory> lowpass 2000 3
pytheory> dist 0.5
pytheory> chorus 0.3
pytheory> sidechain 0.8
pytheory> humanize 0.3
pytheory> legato on
pytheory> glide 0.04
pytheory> volume 0.4
Automation — change effects mid-song::
pytheory> set lowpass 3000
.set(lowpass=3000)
LFO modulation::
pytheory> lfo lowpass 0.5 400 3000 8 sine
.lfo("lowpass", rate=0.5, min=400, max=3000, bars=8, shape="sine")
Playback and Export
-------------------
Hear your work::
pytheory> play_score
♫ play_score()
pytheory> play_pattern
♫ play_pattern("bossa nova")
Export::
pytheory> save_midi sketch.mid
save_midi("sketch.mid")
pytheory> render sketch.wav
saved: sketch.wav
Session management::
pytheory> show
<Score 4/4 140bpm 3 parts 8.0 measures>
lead: saw+pluck 32 notes reverb=0.3 delay=0.25 ←
chords: fm+pad 8 notes
drums: bossa nova (76 hits)
pytheory> status
key=A minor bpm=140 swing=0.0
drums=bossa nova parts=[lead, chords, bass] active=lead
pytheory> clear
cleared (C major, 120 bpm)
Complete Example
----------------
A full session from start to playable track::
pytheory[key=C | bpm=120]> key Am
pytheory[key=Am | bpm=120]> bpm 140
pytheory[key=Am | bpm=140]> drums bossa nova
pytheory[key=Am | bpm=140 | drums=bossa nova]> part chords fm pad
pytheory[...| →chords(fm)]> prog i iv V i
pytheory[...| →chords(fm)]> part lead saw pluck
pytheory[...| →lead(saw)]> reverb 0.3
pytheory[...| →lead(saw) rev=0.3]> delay 0.25
pytheory[...| →lead(saw) rev=0.3 del=0.25]> arp Am updown 4 2
pytheory[...]> play_score
♫ play_score()
pytheory[...]> save_midi my_bossa.mid
save_midi("my_bossa.mid")
Every command you typed maps 1:1 to the Python API. When you're
ready to move from the REPL to a script, the translation is direct.
+329 -88
View File
@@ -30,18 +30,15 @@ Building Scales
Use :class:`~pytheory.scales.TonedScale` to generate scales in any key:
.. code-block:: python
.. code-block:: pycon
from pytheory import TonedScale
c = TonedScale(tonic="C4")
major = c["major"]
minor = c["minor"]
harmonic_minor = c["harmonic minor"]
print(major.note_names)
# ['C', 'D', 'E', 'F', 'G', 'A', 'B', 'C']
>>> from pytheory import TonedScale
>>> c = TonedScale(tonic="C4")
>>> major = c["major"]
>>> minor = c["minor"]
>>> harmonic_minor = c["harmonic minor"]
>>> major.note_names
['C', 'D', 'E', 'F', 'G', 'A', 'B', 'C']
Major and Minor
---------------
@@ -55,13 +52,12 @@ notes but starts from the 6th degree:
- G major → E minor (both have one sharp: F#)
- F major → D minor (both have one flat: Bb)
.. code-block:: python
.. code-block:: pycon
c_major = TonedScale(tonic="C4")["major"]
a_minor = TonedScale(tonic="A4")["minor"]
# Same notes, different starting point
set(c_major.note_names) == set(a_minor.note_names) # True
>>> c_major = TonedScale(tonic="C4")["major"]
>>> a_minor = TonedScale(tonic="A4")["minor"]
>>> set(c_major.note_names) == set(a_minor.note_names)
True
The `harmonic minor <https://en.wikipedia.org/wiki/Harmonic_minor_scale>`_ raises the 7th degree of the natural minor,
creating an augmented 2nd interval (3 semitones) between the 6th and
@@ -79,44 +75,65 @@ The seven `modes <https://en.wikipedia.org/wiki/Mode_(music)>`_ of the major sca
pattern, each starting from a different degree. Each mode has a distinct
emotional character:
.. code-block:: python
.. code-block:: pycon
c = TonedScale(tonic="C4")
>>> c = TonedScale(tonic="C4")
**Ionian** (I) — the major scale itself. Bright, happy, resolved::
**Ionian** (I) — the major scale itself. Bright, happy, resolved:
c["ionian"] # C D E F G A B C
.. code-block:: pycon
>>> c["ionian"].note_names
['C', 'D', 'E', 'F', 'G', 'A', 'B', 'C']
`Dorian <https://en.wikipedia.org/wiki/Dorian_mode>`_ (ii) — minor with a raised 6th. Jazzy, soulful (So What,
Scarborough Fair)::
Scarborough Fair):
c["dorian"] # C D Eb F G A Bb C
.. code-block:: pycon
>>> c["dorian"].note_names
['C', 'D', 'Eb', 'F', 'G', 'A', 'Bb', 'C']
`Phrygian <https://en.wikipedia.org/wiki/Phrygian_mode>`_ (iii) — minor with a flat 2nd. Spanish, flamenco, dark
(White Rabbit)::
(White Rabbit):
c["phrygian"] # C Db Eb F G Ab Bb C
.. code-block:: pycon
>>> c["phrygian"].note_names
['C', 'Db', 'Eb', 'F', 'G', 'Ab', 'Bb', 'C']
`Lydian <https://en.wikipedia.org/wiki/Lydian_mode>`_ (IV) — major with a raised 4th. Dreamy, floating, ethereal
(The Simpsons theme, Flying by ET)::
(The Simpsons theme, Flying by ET):
c["lydian"] # C D E F# G A B C
.. code-block:: pycon
>>> c["lydian"].note_names
['C', 'D', 'E', 'F#', 'G', 'A', 'B', 'C']
`Mixolydian <https://en.wikipedia.org/wiki/Mixolydian_mode>`_ (V) — major with a flat 7th. Bluesy, rock, dominant
(Norwegian Wood, Sweet Home Alabama)::
(Norwegian Wood, Sweet Home Alabama):
c["mixolydian"] # C D E F G A Bb C
.. code-block:: pycon
>>> c["mixolydian"].note_names
['C', 'D', 'E', 'F', 'G', 'A', 'Bb', 'C']
`Aeolian <https://en.wikipedia.org/wiki/Aeolian_mode>`_ (vi) — the natural minor scale. Sad, dark, introspective
(Stairway to Heaven, Losing My Religion)::
(Stairway to Heaven, Losing My Religion):
c["aeolian"] # C D Eb F G Ab Bb C
.. code-block:: pycon
>>> c["aeolian"].note_names
['C', 'D', 'Eb', 'F', 'G', 'Ab', 'Bb', 'C']
`Locrian <https://en.wikipedia.org/wiki/Locrian_mode>`_ (vii) — minor with flat 2nd and flat 5th. Unstable,
rarely used as a home key (used in metal and jazz over diminished
chords)::
chords):
c["locrian"] # C Db Eb F Gb Ab Bb C
.. code-block:: pycon
>>> c["locrian"].note_names
['C', 'Db', 'Eb', 'F', 'Gb', 'Ab', 'Bb', 'C']
Scale Degrees
-------------
@@ -137,32 +154,45 @@ Leading Tone VII One semitone below tonic — pulls upward
Access degrees by index, Roman numeral, or name:
.. code-block:: python
.. code-block:: pycon
major = TonedScale(tonic="C4")["major"]
major[0] # C4 (by index)
major["I"] # C4 (by Roman numeral)
major["tonic"] # C4 (by degree name)
major["V"] # G4 (dominant)
major["dominant"] # G4
major[0:3] # (C4, D4, E4) — slicing works too
>>> major = TonedScale(tonic="C4")["major"]
>>> major[0]
C4
>>> major["I"]
C4
>>> major["tonic"]
C4
>>> major["V"]
G4
>>> major["dominant"]
G4
>>> major[0:3]
(<Tone C4>, <Tone D4>, <Tone E4>)
Iteration
---------
Scales are iterable and support ``len()`` and ``in``:
.. code-block:: python
.. code-block:: pycon
for tone in major:
print(f"{tone.name}: {tone.frequency:.1f} Hz")
len(major) # 8 (7 notes + octave)
"C" in major # True
"C#" in major # False
>>> for tone in major:
... print(f"{tone.name}: {tone.frequency:.1f} Hz")
C: 261.6 Hz
D: 293.7 Hz
E: 329.6 Hz
F: 349.2 Hz
G: 392.0 Hz
A: 440.0 Hz
B: 493.9 Hz
C: 523.3 Hz
>>> len(major)
8
>>> "C" in major
True
>>> "C#" in major
False
Building Chords from Scales
----------------------------
@@ -185,21 +215,25 @@ Notice the pattern: **major** triads on I, IV, V; **minor** triads on
ii, iii, vi; **diminished** on vii°. This pattern holds for every major
key.
.. code-block:: python
.. code-block:: pycon
major = TonedScale(tonic="C4")["major"]
# Build diatonic triads
I = major.triad(0) # C E G (C major)
ii = major.triad(1) # D F A (D minor)
iii = major.triad(2) # E G B (E minor)
IV = major.triad(3) # F A C (F major)
V = major.triad(4) # G B D (G major)
vi = major.triad(5) # A C E (A minor)
# Build seventh chords
Imaj7 = major.chord(0, 2, 4, 6) # C E G B = Cmaj7
V7 = major.chord(4, 6, 8, 10) # G B D F = G7 (dominant 7th)
>>> major = TonedScale(tonic="C4")["major"]
>>> major.triad(0)
C major
>>> major.triad(1)
D minor
>>> major.triad(2)
E minor
>>> major.triad(3)
F major
>>> major.triad(4)
G major
>>> major.triad(5)
A minor
>>> major.chord(0, 2, 4, 6)
C major 7th
>>> major.chord(4, 6, 8, 10)
G dominant 7th
Common Progressions
~~~~~~~~~~~~~~~~~~~
@@ -215,6 +249,29 @@ Some of the most-used chord progressions in Western music:
My Heart Will Go On)
- **IIVviV** — axis of awesome (many, many pop songs)
The :class:`~pytheory.scales.Key` class makes working with progressions
easy:
.. code-block:: pycon
>>> from pytheory import Key
>>> key = Key("G", "major")
>>> chords = key.progression("I", "V", "vi", "IV")
>>> for c in chords:
... print(c.identify())
G major
D major
E minor
C major
>>> key.nashville(1, 5, 6, 4)
[<Chord G major>, <Chord D major>, <Chord E minor>, <Chord C major>]
>>> key.chords
['G major', 'A minor', 'B minor', 'C major', 'D major', 'E minor', 'F# diminished']
>>> key.seventh_chords
['G major 7th', 'A minor 7th', 'B minor 7th', 'C major 7th', 'D dominant 7th', 'E minor 7th', 'F# half-diminished 7th']
>>> Key.detect("C", "E", "G", "A", "D")
C major
The 12-Bar Blues
~~~~~~~~~~~~~~~~
@@ -233,35 +290,219 @@ structure. In the key of A::
| D | D | A | A |
| E | D | A | E |
.. code-block:: python
.. code-block:: pycon
from pytheory import TonedScale
>>> from pytheory import TonedScale
>>> a = TonedScale(tonic="A4")["major"]
>>> I = a.triad(0)
>>> IV = a.triad(3)
>>> V = a.triad(4)
>>> blues_12 = [I, I, I, I, IV, IV, I, I, V, IV, I, V]
a = TonedScale(tonic="A4")["major"]
I = a.triad(0) # A major
IV = a.triad(3) # D major
V = a.triad(4) # E major
Key Signatures
~~~~~~~~~~~~~~
# The 12-bar blues progression
blues_12 = [I, I, I, I, IV, IV, I, I, V, IV, I, V]
The ``signature`` property tells you how many sharps or flats a key has:
Parallel Major and Minor
~~~~~~~~~~~~~~~~~~~~~~~~~
.. code-block:: pycon
Two scales are **relative** if they share the same notes (C major and
A minor). Two scales are `parallel <https://en.wikipedia.org/wiki/Parallel_key>`_ if they share the same tonic but
have different notes (C major and C minor).
>>> Key("G", "major").signature
{'sharps': 1, 'flats': 0, 'accidentals': ['F#']}
>>> Key("F", "major").signature
{'sharps': 0, 'flats': 1, 'accidentals': ['Bb']}
>>> Key("C", "major").signature
{'sharps': 0, 'flats': 0, 'accidentals': []}
Mixing parallel major and minor is a powerful compositional tool —
borrowing chords from the parallel minor in a major key creates
dramatic color shifts. The bVI and bVII chords (Ab and Bb in C major)
are borrowed from C minor and appear constantly in rock and film music.
Relative and Parallel Keys
~~~~~~~~~~~~~~~~~~~~~~~~~~
.. code-block:: python
Two keys are **relative** if they share the same notes (C major and
A minor). Two keys are `parallel <https://en.wikipedia.org/wiki/Parallel_key>`_ if they share the same tonic but
have different notes (C major and C minor):
c_major = TonedScale(tonic="C4")["major"]
c_minor = TonedScale(tonic="C4")["minor"]
.. code-block:: pycon
# Compare: same tonic, different notes
c_major.note_names # ['C', 'D', 'E', 'F', 'G', 'A', 'B', 'C']
c_minor.note_names # ['C', 'D', 'D#', 'F', 'G', 'G#', 'A#', 'C']
>>> Key("C", "major").relative
A minor
>>> Key("A", "minor").relative
C major
>>> Key("C", "major").parallel
C minor
Borrowed Chords
~~~~~~~~~~~~~~~
`Modal interchange <https://en.wikipedia.org/wiki/Borrowed_chord>`_
borrowing chords from the parallel key — is one of the most powerful
tools in songwriting. The bVI and bVII chords (Ab and Bb in C major)
are borrowed from C minor and appear constantly in rock and film music:
.. code-block:: pycon
>>> Key("C", "major").borrowed_chords
['C minor', 'D diminished', 'Eb major', 'F minor', 'G minor', 'Ab major', 'Bb major']
Secondary Dominants
~~~~~~~~~~~~~~~~~~~
A `secondary dominant <https://en.wikipedia.org/wiki/Secondary_dominant>`_
is the V chord *of* a non-tonic chord. It creates a momentary pull
toward that chord, adding harmonic color:
.. code-block:: pycon
>>> key = Key("C", "major")
>>> key.secondary_dominant(5)
D dominant 7th
>>> key.secondary_dominant(2)
A dominant 7th
Random Progressions
~~~~~~~~~~~~~~~~~~~
Need inspiration? Generate weighted random progressions. The weights
favor common chord functions (I and vi most likely, vii least):
.. code-block:: pycon
>>> key = Key("C", "major")
>>> chords = key.random_progression(4)
>>> [c.identify() for c in chords]
['C major', 'F major', 'A minor', 'G major']
All Keys
~~~~~~~~
Enumerate all 24 major and minor keys:
.. code-block:: pycon
>>> Key.all_keys()
[<Key C major>, <Key C minor>, <Key C# major>, <Key C# minor>, ...]
Scale Transposition
~~~~~~~~~~~~~~~~~~~
Transpose an entire scale by a number of semitones:
.. code-block:: pycon
>>> c_major = TonedScale(tonic="C4")["major"]
>>> d_major = c_major.transpose(2)
>>> d_major.note_names
['D', 'E', 'F#', 'G', 'A', 'B', 'C#', 'D']
Degree Names
~~~~~~~~~~~~
Get the traditional function name for any scale degree:
.. code-block:: pycon
>>> major = TonedScale(tonic="C4")["major"]
>>> major.degree_name(0)
'tonic'
>>> major.degree_name(4)
'dominant'
>>> major.degree_name(6)
'leading tone'
>>> major.degree_name(6, minor=True)
'subtonic'
Scale Fitness
~~~~~~~~~~~~~
Score how well a set of notes fits a scale (0.01.0). Useful for melody
analysis or detecting which scale a phrase belongs to:
.. code-block:: pycon
>>> major = TonedScale(tonic="C4")["major"]
>>> major.fitness("C", "D", "E", "G")
1.0
>>> major.fitness("C", "D", "F#", "G")
0.75
Scale Recommendation
~~~~~~~~~~~~~~~~~~~~
Given a melody or set of notes, find the best-matching scales ranked
by fitness. Useful for figuring out what key you're in or finding
alternative scales to improvise over:
.. code-block:: pycon
>>> from pytheory.scales import Scale
>>> Scale.recommend("C", "D", "E", "G", "A", top=3)
[('C', 'major', 1.0), ('A', 'aeolian', 1.0), ...]
>>> Scale.recommend("C", "Eb", "F", "Gb", "G", "Bb", top=3)
[('C', 'blues', 1.0), ...]
Chromatic scales are deprioritized since they match everything.
Parallel Modes
~~~~~~~~~~~~~~
See all 7 modes that share the same notes as a scale:
.. code-block:: pycon
>>> major = TonedScale(tonic="C4")["major"]
>>> for name, notes in major.parallel_modes().items():
... print(f"{name}: {' '.join(notes)}")
C ionian: C D E F G A B C
D dorian: D E F G A B C D
E phrygian: E F G A B C D E
...
Common Progressions
~~~~~~~~~~~~~~~~~~~
Get all named progressions realized in a key with chord symbols:
.. code-block:: pycon
>>> key = Key("C", "major")
>>> progs = key.common_progressions()
>>> for name, chords in list(progs.items())[:3]:
... symbols = [c.symbol for c in chords]
... print(f"{name}: {' → '.join(symbols)}")
I-IV-V-I: C → F → G → C
I-V-vi-IV: C → G → Am → F
I-vi-IV-V: C → Am → F → G
Chord Suggestions
~~~~~~~~~~~~~~~~~
Given a chord in a key, ``suggest_next()`` returns likely next chords
based on functional harmony voice-leading rules:
.. code-block:: pycon
>>> key = Key("C", "major")
>>> g_major = key.triad(4) # V chord
>>> [c.symbol for c in key.suggest_next(g_major)]
['C', 'Am', 'F']
Modulation
~~~~~~~~~~
``modulation_path()`` suggests a chord-by-chord route from one key to
another, using pivot chords when available:
.. code-block:: pycon
>>> path = Key("C", "major").modulation_path(Key("G", "major"))
>>> [c.symbol for c in path]
['C', 'Em', 'D', 'G']
``pivot_chords()`` shows which chords are shared between two keys:
.. code-block:: pycon
>>> Key("C", "major").pivot_chords(Key("G", "major"))
['A minor', 'B minor', 'C major', 'D major', 'E minor', 'G major']
Scales are the map; the key is the territory. Once you know the landscape, you can wander freely -- and you'll always know how to get home.
+926
View File
@@ -0,0 +1,926 @@
Sequencing
==========
The sequencing system lets you compose multi-part arrangements with
durations, time signatures, and instrument voices. This is where
PyTheory goes from theory tool to composition tool.
At the center of everything is the ``Score``. Think of it as your
arrangement, your song, your sketch pad. It holds the tempo, the time
signature, the drum pattern, and every instrument part you create. If
you've ever used a DAW, the Score is your session file. If you haven't,
it's the sheet of paper where the whole piece lives. Everything you
compose -- melodies, chord progressions, bass lines, arpeggios -- gets
added to a Score before you can hear it, export it, or do anything
useful with it.
Duration
--------
In music, all rhythm boils down to one convention: the quarter note
equals one beat. Everything else is relative to that. A whole note is
four beats. An eighth note is half a beat. This is how musicians have
communicated timing for centuries, and it's how PyTheory works too.
Once you internalize "quarter note = 1 beat," durations become
intuitive arithmetic.
A ``Duration`` represents a note length in beats (quarter note = 1 beat):
.. code-block:: pycon
>>> from pytheory import Duration
>>> Duration.WHOLE.value
4.0
>>> Duration.HALF.value
2.0
>>> Duration.QUARTER.value
1.0
>>> Duration.EIGHTH.value
0.5
>>> Duration.SIXTEENTH.value
0.25
>>> Duration.DOTTED_HALF.value
3.0
>>> Duration.DOTTED_QUARTER.value
1.5
>>> Duration.TRIPLET_QUARTER.value
0.6666666666666666
Duration supports arithmetic — multiply, divide, and add to create
compound durations:
.. code-block:: pycon
>>> Duration.WHOLE * 2
8.0
>>> Duration.HALF + Duration.QUARTER
3.0
>>> Duration.WHOLE / 2
2.0
Time Signatures
---------------
If you're not a musician, time signatures can seem mysterious. They're
not. The top number tells you how many beats are in a bar. The bottom
number tells you which note value gets one beat. That's it.
In practice, you only need to know a handful:
- **4/4** -- four beats per bar. This is the default. Almost all pop,
rock, hip hop, electronic, and R&B music is in 4/4. If you're not
sure, use this.
- **3/4** -- three beats per bar. The waltz feel. Think "Blue Danube"
or Radiohead's "Everything in Its Right Place."
- **6/8** -- six eighth notes per bar, grouped in two sets of three.
Each group feels like one big swaying beat. Folk music, slow jams,
ballads.
- **12/8** -- twelve eighth notes per bar, grouped in four sets of
three. The slow blues shuffle, the gospel feel, "At Last" by Etta
James. Each "big beat" has a triplet swing baked into it.
A ``TimeSignature`` holds the meter of a piece -- how many beats per
measure and which note value gets one beat:
.. code-block:: pycon
>>> from pytheory.rhythm import TimeSignature
>>> ts = TimeSignature.from_string("4/4")
>>> ts.beats_per_measure
4.0
>>> TimeSignature.from_string("3/4").beats_per_measure
3.0
>>> TimeSignature.from_string("6/8").beats_per_measure
3.0
>>> TimeSignature.from_string("12/8").beats_per_measure
6.0
The ``beats_per_measure`` is always in quarter-note units. In 6/8,
there are 6 eighth notes per bar = 3 quarter-note beats. In 12/8,
12 eighth notes = 6 quarter-note beats, grouped in four dotted-quarter
pulses.
Score Basics
------------
A ``Score`` is a sequence of notes and rests with a time signature and
tempo. Use ``.add()`` and ``.rest()`` for fluent chaining:
.. code-block:: python
from pytheory import Score, Duration, Tone
score = Score("4/4", bpm=120)
score.add(Tone.from_string("C4", system="western"), Duration.QUARTER)
score.add(Tone.from_string("E4", system="western"), Duration.QUARTER)
score.add(Tone.from_string("G4", system="western"), Duration.HALF)
.. code-block:: pycon
>>> score.total_beats
4.0
>>> score.measures
1.0
>>> score.duration_ms
2000.0
Rests
~~~~~
Add silence with ``.rest()``:
.. code-block:: python
score = Score("4/4", bpm=120)
score.add(Tone.from_string("C4", system="western"), Duration.HALF)
score.rest(Duration.HALF)
.. code-block:: pycon
>>> score.measures
1.0
Chords
~~~~~~
Chords work just like tones — pass any ``Chord`` object:
.. code-block:: python
from pytheory import Score, Duration, Key
key = Key("C", "major")
chords = key.progression("I", "V", "vi", "IV")
score = Score("4/4", bpm=120)
for chord in chords:
score.add(chord, Duration.WHOLE)
.. raw:: html
<audio controls style="width:100%;margin:0.5em 0 1.5em"><source src="../_static/audio/chords_basic.wav" type="audio/wav"></audio>
.. code-block:: pycon
>>> score.measures
4.0
>>> score.duration_ms
8000.0
Compound Time
~~~~~~~~~~~~~
12/8 is a compound meter — 12 eighth notes per bar grouped in four
groups of three. Each group feels like one "big beat":
.. code-block:: python
from pytheory import Score, Duration, Key
key = Key("A", "minor")
chords = key.random_progression(4)
score = Score("12/8", bpm=120)
for c in chords:
score.add(c, Duration.DOTTED_HALF)
score.add(c, Duration.DOTTED_HALF)
.. code-block:: pycon
>>> score.measures
4.0
Parts
-----
Parts are like tracks in a DAW. Each one has its own instrument sound
(synth waveform + envelope), its own volume level, and its own effects
chain. When you call ``play_score()``, all the parts get mixed together
into a single audio stream -- just like hitting play in Logic or
Ableton. You might have a pad part holding down chords, a lead part
playing a melody, and a bass part holding down the low end. Each one
is independent: different synth, different envelope, different effects.
The ``Part`` class lets you layer multiple instrument voices -- each with
its own synth waveform, ADSR envelope, and volume level. Create parts
with ``Score.part()``:
.. code-block:: python
from pytheory import Score, Key, Duration, Chord
from pytheory.play import play_score
score = Score("4/4", bpm=140)
chords = score.part("chords", synth="sine", envelope="pad", volume=0.35)
lead = score.part("lead", synth="saw", envelope="pluck", volume=0.5)
bass = score.part("bass", synth="triangle", envelope="pluck", volume=0.45)
Adding Notes to Parts
~~~~~~~~~~~~~~~~~~~~~
Parts accept note strings directly — no need to wrap in
``Tone.from_string()``. ``.add()`` and ``.rest()`` return self for
fluent chaining:
.. code-block:: python
lead.add("E5", Duration.QUARTER).add("D5", Duration.EIGHTH).rest(Duration.EIGHTH)
Raw float beats work too — useful for swing and tuplets:
.. code-block:: python
lead.add("C5", 0.67).add("B4", 0.33).add("A4", 1.0)
Chords and Tone objects work the same way:
.. code-block:: python
for chord in Key("A", "minor").progression("i", "iv", "V", "i"):
chords.add(chord, Duration.WHOLE)
for note in ["A2", "C3", "E3", "A2", "D2", "F2", "A2", "D2"]:
bass.add(note, Duration.QUARTER)
Polyphonic Hold
---------------
``Part.hold()`` adds a note without advancing the beat position —
the next note starts at the *same* time. This enables polyphonic
overlap on a single part: piano sustain, sitar drone under melody,
guitar strum texture.
.. code-block:: python
piano = score.part("piano", instrument="piano", reverb=0.3)
# Hold a C major chord for 8 beats
piano.hold("C3", Duration.WHOLE * 2, velocity=60)
piano.hold("E3", Duration.WHOLE * 2, velocity=55)
piano.hold("G3", Duration.WHOLE * 2, velocity=55)
# Melody plays simultaneously on top
for n in ["E4", "G4", "C5", "G4", "E4", "D4", "C4", "E4"]:
piano.add(n, Duration.QUARTER, velocity=80)
.. raw:: html
<audio controls style="width:100%;margin:0.5em 0 1.5em"><source src="../_static/audio/piano_hold.wav" type="audio/wav"></audio>
Arpeggiator
------------
An arpeggiator takes a chord and plays its notes one at a time, in a
pattern, automatically. You hold down a chord and it ripples through
the notes -- up, down, up-and-down, random. It's one of the most
iconic sounds in electronic music. The bubbly bass lines of acid house,
the cascading runs of 80s synth pop (think "Jump" or "Take On Me"),
the hypnotic patterns of trance -- all arpeggiators. It turns a simple
three-note chord into a rhythmic, melodic engine.
``Part.arpeggio()`` takes a chord and sequences through its notes
automatically -- like a hardware arpeggiator on a synth:
.. code-block:: python
lead = score.part(
"lead",
synth="saw",
legato=True,
glide=0.03,
distortion=0.8,
lowpass=1000,
lowpass_q=5.0,
)
lead.arpeggio(
Chord.from_symbol("Cm"),
bars=2,
pattern="up",
division=Duration.SIXTEENTH,
octaves=2,
)
Parameters:
- ``chord``: A Chord object or string like ``"Am"``.
- ``bars``: Number of bars to fill (default 1).
- ``pattern``: ``"up"``, ``"down"``, ``"updown"``, ``"downup"``, ``"random"``.
- ``division``: Step length (default ``Duration.SIXTEENTH``).
- ``octaves``: Octave span (default 1). With 2, the pattern repeats one octave up.
Chain arpeggios through a progression:
.. code-block:: python
for sym in ["Cm", "Fm", "Abm", "Gm"]:
lead.arpeggio(sym, bars=2, pattern="updown", octaves=2)
.. raw:: html
<audio controls style="width:100%;margin:0.5em 0 1.5em"><source src="../_static/audio/arpeggio.wav" type="audio/wav"></audio>
Combined with legato, glide, distortion, and a resonant lowpass, this
produces the classic acid/trance arpeggiator sound.
Legato and Glide
----------------
Normally, every note you play has its own life cycle -- the sound
attacks, sustains, and releases before the next note begins. You hear
each note as a separate event. Legato changes that. The Italian word
means "tied together," and that's exactly what it does: the envelope
flows continuously from one note to the next with no retriggering. The
pitch changes, but the sound never dies and restarts.
Glide (also called portamento) takes this further. Instead of the pitch
jumping instantly from one note to the next, it *slides* -- a smooth,
continuous pitch sweep. This is THE sound of the Roland TB-303, the
little silver box that accidentally invented acid house. A saw wave
with legato, glide, a resonant lowpass filter, and some distortion --
that's the entire genre right there.
By default, each note gets its own attack/release envelope. ``legato=True``
renders the entire part as one continuous waveform -- the pitch changes
at note boundaries but the envelope flows unbroken. Add ``glide`` for
portamento (pitch slides between notes):
.. code-block:: python
acid = score.part(
"acid",
synth="saw",
legato=True,
glide=0.04,
lowpass=3000,
lowpass_q=6.0,
distortion=0.3,
)
acid.add("C2", 0.25).add("C3", 0.25).add("G2", 0.25).add("C2", 0.25)
.. raw:: html
<audio controls style="width:100%;margin:0.5em 0 1.5em"><source src="../_static/audio/legato_glide.wav" type="audio/wav"></audio>
- ``legato``: If True, no envelope retrigger between notes (default False).
- ``glide``: Portamento time in seconds (default 0, instant).
0.03--0.05 = quick 303 slide, 0.1--0.2 = slow glide.
Complete Example
----------------
A full multi-part arrangement — rock beat with piano chords, saw
lead, and filtered bass:
.. code-block:: python
from pytheory import Score, Key, Duration, Chord
from pytheory.play import play_score
score = Score("4/4", bpm=120)
score.drums("rock", repeats=8, fill="rock", fill_every=4)
# Piano chords with reverb
piano = score.part("piano", instrument="piano", volume=0.4, reverb=0.3)
# Saw lead with delay
lead = score.part(
"lead", synth="saw", envelope="pluck", volume=0.4,
delay=0.2, delay_time=0.33, reverb=0.2, lowpass=3000,
)
# Filtered bass
bass = score.part("bass", synth="triangle", envelope="pluck",
volume=0.45, lowpass=1200)
for chord in Key("G", "major").progression("I", "V", "vi", "IV") * 2:
piano.add(chord, Duration.WHOLE)
lead.add("D5", 1).add("B4", 0.5).add("D5", 0.5)
lead.add("G5", 1).add("E5", 1)
lead.add("D5", 0.5).add("B4", 0.5).add("A4", 1)
lead.add("G4", 2).rest(2)
lead.add("D5", 1).add("B4", 0.5).add("D5", 0.5)
lead.add("G5", 1).add("A5", 1)
lead.add("G5", 0.5).add("E5", 0.5).add("D5", 1)
lead.add("B4", 2).rest(2)
for n in ["G2", "G2", "D2", "D2", "E2", "E2", "C2", "C2"] * 2:
bass.add(n, Duration.HALF)
play_score(score)
.. raw:: html
<audio controls style="width:100%;margin:0.5em 0 1.5em"><source src="../_static/audio/complete_rock.wav" type="audio/wav"></audio>
Velocity
--------
Real music has dynamics — accents are louder, ghost notes are barely
there, phrases crescendo and decrescendo. Every note can have its own
velocity (1127, where 100 is the default):
.. code-block:: python
lead.add("C5", Duration.QUARTER, velocity=120) # loud accent
lead.add("D5", Duration.QUARTER, velocity=40) # ghost note
lead.add("E5", Duration.QUARTER) # default (100)
The arpeggiator also accepts velocity:
.. code-block:: python
lead.arpeggio("Am", bars=2, pattern="up", velocity=80)
Articulations
-------------
Articulations change *how* a note is played — its attack, duration, and
weight. A staccato note is short and bouncy. A marcato note hits hard.
A legato note melts into the next one. This is the difference between
a melody that sounds like a MIDI file and one that sounds like a
musician played it.
Pass ``articulation=`` to ``Part.add()``:
.. code-block:: python
piano.add("C4", Duration.QUARTER, articulation="staccato") # short, bouncy
piano.add("D4", Duration.QUARTER, articulation="legato") # smooth, overlaps
piano.add("E4", Duration.QUARTER, articulation="marcato") # heavy accent
piano.add("F4", Duration.QUARTER, articulation="tenuto") # held, soft attack
piano.add("G4", Duration.QUARTER, articulation="accent") # louder
piano.add("C5", Duration.HALF, articulation="fermata") # held longer
.. raw:: html
<audio controls style="width:100%;margin:0.5em 0 1.5em"><source src="../_static/audio/articulations.wav" type="audio/wav"></audio>
What each articulation does:
- **staccato** — plays ~40% of the note duration with a quick fade-out. Short and detached.
- **legato** — extends ~15% into the next note. Smooth and connected.
- **marcato** — 25% velocity boost + sharper attack. Heavy and accented.
- **tenuto** — full duration with a softer attack ramp. Held and deliberate.
- **accent** — 20% velocity boost, no duration change.
- **fermata** — stretches the note 50% longer.
Articulations work on ``Part.hold()`` and ``Part.hit()`` too.
Dynamic Curves
--------------
Real music breathes — phrases get louder, get quieter, swell and
recede. Dynamic curves let you shape the velocity across a sequence
of notes instead of setting each one manually.
.. code-block:: python
# Crescendo: quiet to loud
piano.crescendo(["C4","D4","E4","F4","G4","A4","B4","C5"],
Duration.QUARTER, start_vel=30, end_vel=110)
# Decrescendo: loud to quiet
piano.decrescendo(["C5","B4","A4","G4","F4","E4","D4","C4"],
Duration.QUARTER, start_vel=110, end_vel=30)
# Swell: up then back down (orchestral < > shape)
strings.swell(["C4","D4","E4","F4","G4","F4","E4","D4"],
Duration.QUARTER, low_vel=35, peak_vel=110)
# Custom curve: explicit velocity per note
piano.dynamics(["C4","E4","G4","C5"], Duration.QUARTER,
velocities=[50, 80, 110, 90])
.. raw:: html
<audio controls style="width:100%;margin:0.5em 0 1.5em"><source src="../_static/audio/dynamics.wav" type="audio/wav"></audio>
Four methods:
- **crescendo()** — linear velocity ramp from ``start_vel`` to ``end_vel``.
- **decrescendo()** — same thing, but typically loud to quiet.
- **swell()** — ramps up to the midpoint, then back down. The classic
orchestral crescendo-decrescendo.
- **dynamics()** — the general form. Pass a ``(start, end)`` tuple for
a linear ramp, or a list of velocities for a custom curve.
All four accept ``articulation=`` to combine dynamics with articulations:
.. code-block:: python
# Staccato crescendo — bouncy notes getting louder
piano.crescendo(["C4","E4","G4","C5","E5","G5","C6","E6"],
Duration.EIGHTH, start_vel=40, end_vel=110,
articulation="staccato")
Part.hit() — Manual Drum Placement
-----------------------------------
The pattern system is great for grooves, but sometimes you want to
place individual drum hits with full control — articulations, effects,
and all. ``Part.hit()`` puts a drum sound into a Part's note stream:
.. code-block:: python
from pytheory import DrumSound
kit = score.part("kit", synth="sine", volume=0.7)
kit.hit(DrumSound.KICK, Duration.QUARTER, articulation="accent")
kit.hit(DrumSound.CLOSED_HAT, Duration.EIGHTH, velocity=60)
kit.hit(DrumSound.SNARE, Duration.EIGHTH, articulation="marcato")
Because hits go through the normal Part renderer, they get humanize,
effects, and articulations for free. Use this for custom beats that
don't fit a preset pattern, or for one-shot accent hits layered on
top of a pattern.
Rudiments — Flam, Diddle, Cheese
---------------------------------
Marching percussion rudiments as methods on any Part:
.. code-block:: python
from pytheory import DrumSound
p = score.part("snares", synth="sine", volume=0.9)
# Flam: grace note + main hit (gap controls tightness)
p.flam(DrumSound.MARCH_SNARE, Duration.QUARTER, velocity=120)
# Diddle: two equal strokes in one note duration
p.diddle(DrumSound.MARCH_SNARE, Duration.EIGHTH, velocity=60)
# Cheese: flam + diddle combined
p.cheese(DrumSound.MARCH_SNARE, Duration.QUARTER, velocity=120)
Ensemble
--------
Any Part can be rendered as an ensemble — multiple players with
per-player timing tendencies and micro pitch drift:
.. code-block:: python
# 8-player snare line
snares = score.part("snares", synth="sine", volume=0.9, ensemble=8)
# 20-player string section
strings = score.part("strings", instrument="string_ensemble", ensemble=20)
# Single player (default)
solo = score.part("solo", instrument="violin")
Each ensemble voice gets a consistent timing personality (some rush,
some drag) plus small per-note wobble, and slightly different tuning.
The result sounds like a real section — together but alive.
Solo snare, then an 8-player section plays the same pattern:
.. raw:: html
<audio controls style="width:100%;margin:0.5em 0 1.5em"><source src="../_static/audio/ensemble.wav" type="audio/wav"></audio>
Swing and Groove
----------------
Perfectly quantized music sounds robotic. Swing delays every other
subdivision by a percentage, giving the rhythm a human, shuffled feel.
Jazz swings hard. Bossa nova swings gently. Hip hop has its own pocket.
Set swing on the Score (applies to everything) or per-Part:
.. code-block:: python
# Triplet swing — lazy jazz feel
score = Score("4/4", bpm=100, swing=0.55)
# Per-part override — the lead swings harder than the bass
lead = score.part("lead", synth="saw", swing=0.6)
bass = score.part("bass", synth="sine", swing=0.4)
Swing values:
- **0.0** = perfectly straight (default)
- **0.3** = subtle shuffle (pop, R&B)
- **0.5** = triplet feel (jazz, blues)
- **0.67** = hard swing (bebop)
Tempo Changes
-------------
Real music doesn't stay at one tempo. Songs speed up for energy,
slow down for endings, and sometimes shift abruptly. Use
``score.set_tempo()`` to change BPM at the current position:
.. code-block:: python
score = Score("4/4", bpm=90)
# Verse: slow and moody
lead.add("D5", Duration.WHOLE)
lead.add("F5", Duration.WHOLE)
# Chorus: speeds up
score.set_tempo(110)
lead.add("A5", Duration.WHOLE)
lead.add("D6", Duration.WHOLE)
# Outro: slows way down
score.set_tempo(70)
lead.add("D5", Duration.WHOLE)
The tempo map engine handles the math — beat positions are converted
to sample positions accounting for every tempo change.
Fades
-----
``Part.fade_in()`` and ``Part.fade_out()`` ramp the volume over a
number of bars. They work by generating automation points, so they
integrate naturally with the rest of the automation system:
.. code-block:: python
pad = score.part(
"pad",
synth="supersaw",
envelope="pad",
volume=0.3,
reverb=0.5,
)
# Fade in over first 4 bars
pad.fade_in(bars=4)
for chord in chords:
pad.add(chord, Duration.WHOLE)
# Fade out over last 2 bars
pad.fade_out(bars=2)
pad.rest(Duration.WHOLE)
pad.rest(Duration.WHOLE)
Parameter Ramps
---------------
Fades only control volume. ``Part.ramp()`` smoothly sweeps *any*
parameter from its current value to a target — filters, reverb,
distortion, chorus, delay, anything ``.set()`` accepts. This is how
you build filter sweeps, gradual effect sends, and EDM buildups.
.. code-block:: python
lead = score.part("lead", synth="saw", lowpass=200, lowpass_q=3.0)
# Open the filter over 8 bars
lead.ramp(over=Duration.WHOLE * 8, lowpass=8000)
# Ramp multiple params at once
pad.ramp(over=Duration.WHOLE * 4, reverb=0.5, chorus=0.3)
# Close the filter with distortion fading in
lead.ramp(over=Duration.WHOLE * 4, lowpass=400, distortion=0.5)
Four interpolation curves:
- **linear** — constant rate of change (default).
- **ease_in** — starts slow, accelerates. Good for buildups.
- **ease_out** — starts fast, decelerates. Good for releases.
- **ease_in_out** — slow at both ends. Smooth and natural.
.. code-block:: python
# EDM buildup: slow start, accelerating filter sweep
lead.ramp(over=Duration.WHOLE * 8, curve="ease_in", lowpass=8000)
# Smooth reverb wash fading in and settling
pad.ramp(over=Duration.WHOLE * 4, curve="ease_in_out", reverb=0.6)
.. raw:: html
<audio controls style="width:100%;margin:0.5em 0 1.5em"><source src="../_static/audio/filter_ramp.wav" type="audio/wav"></audio>
``ramp()`` generates automation points every quarter-beat by default.
Set ``resolution=0.125`` for smoother curves (every 32nd note), or
``resolution=1.0`` for lighter automation (every beat).
Combine with ``lfo()`` for cyclic modulation and ``ramp()`` for
one-shot sweeps — together they cover the full range of parameter
automation.
Humanize
--------
Perfectly quantized music sounds like a machine made it — because it
did. Real musicians are never exactly on the beat. Their timing drifts
by a few milliseconds, their velocity varies from note to note. These
imperfections are what make music feel *alive*.
The ``humanize`` parameter adds random micro-variations in both timing
and velocity at render time. The score data stays clean and
deterministic — the randomness is only applied during playback.
.. code-block:: python
# Subtle — like a very tight session player
lead = score.part("lead", synth="saw", humanize=0.1)
# Natural — like a good live take
rhodes = score.part("rhodes", synth="fm", humanize=0.3)
# Loose — like a late-night jam after a few drinks
bass = score.part("bass", synth="sine", humanize=0.5)
Humanize values:
- **0.0** = perfectly quantized (default)
- **0.1** = subtle, studio-tight
- **0.20.3** = natural, like a real player
- **0.40.5** = loose, relaxed, human
- **0.6+** = sloppy (sometimes that's what you want)
Combine with swing for the most realistic feel:
.. code-block:: python
score = Score("4/4", bpm=95, swing=0.45)
lead = score.part(
"lead",
synth="saw",
envelope="pluck",
humanize=0.3,
delay=0.2,
reverb=0.25,
)
Song Structure
--------------
Real songs aren't one long stream of notes — they have verses,
choruses, bridges, drops. The section system lets you name blocks
of your arrangement, then repeat them without rewriting everything.
This is how actual songwriting works: you write a verse, you write
a chorus, then you arrange them — verse, verse, chorus, verse,
chorus, chorus, outro. The sections are the building blocks;
the arrangement is the order you play them in.
Define sections with ``score.section()`` and repeat them with
``score.repeat()``:
.. code-block:: python
score = Score("4/4", bpm=124)
score.drums("house", repeats=16)
pad = score.part("pad", synth="supersaw", envelope="pad")
lead = score.part("lead", synth="saw", envelope="pluck")
bass = score.part("bass", synth="sine", lowpass=300)
# ── Define the verse ──
score.section("verse")
for sym in ["Cm", "Ab", "Eb", "Bb"]:
pad.add(Chord.from_symbol(sym), Duration.WHOLE)
lead.add("C5", 1).add("Eb5", 1).rest(2)
for n in ["C1", "C1", "Ab0", "Ab0", "Eb1", "Eb1", "Bb0", "Bb0"]:
bass.add(n, Duration.HALF)
# ── Define the chorus ──
score.section("chorus")
lead.set(lowpass=5000, reverb=0.3)
for sym in ["Cm", "Fm", "Ab", "Gm"]:
pad.add(Chord.from_symbol(sym), Duration.WHOLE)
lead.add("C6", 1).add("Bb5", 1).add("G5", 1).rest(1)
for n in ["C1", "C1", "F1", "F1", "Ab0", "Ab0", "G1", "G1"]:
bass.add(n, Duration.HALF)
score.end_section()
# ── Arrange: verse, chorus, verse, chorus, chorus ──
score.repeat("verse")
score.repeat("chorus")
score.repeat("verse")
score.repeat("chorus", times=2)
Use any names you want — ``"intro"``, ``"verse"``, ``"chorus"``,
``"bridge"``, ``"drop"``, ``"breakdown"``, ``"outro"``, or anything
that makes sense for your song. The names are just labels.
Guitar Strumming
----------------
Any part with a fretboard can strum chords using real fingering
positions. The ``strum()`` method looks up the chord on the fretboard,
gets the correct voicing, and plays all strings as a chord.
.. code-block:: python
from pytheory import Fretboard
guitar = score.part("guitar", instrument="acoustic_guitar",
fretboard=Fretboard.guitar())
guitar.strum("Am", Duration.HALF, direction="down")
guitar.strum("G", Duration.HALF, direction="up")
guitar.strum("F", Duration.WHOLE)
Works with any fretboard instrument — guitar, ukulele, banjo, mandolin.
Works with any guitar preset — clean, crunch, distorted, orange, metal.
Pitch Bends
-----------
Bend a note's pitch up or down over its duration. Essential for guitar
bends, sitar meends, trombone slides, and vocal-style expression.
.. code-block:: python
# Guitar bend: D up to E (2 semitones)
guitar.add("D4", Duration.HALF, bend=2, bend_type="smooth")
# Release bend: E back down to D
guitar.add("E4", Duration.HALF, bend=-2)
# Blues curl: hold then bend at the end
guitar.add("C4", Duration.HALF, bend=1, bend_type="late")
Three bend types:
- ``"smooth"`` — logarithmic (default). Perceptually even pitch change.
- ``"linear"`` — linear frequency interpolation. Mechanical/synth feel.
- ``"late"`` — holds the starting pitch for 60%, bends in the last 40%.
The classic blues "curl."
Rolls
-----
Rapid repeated notes with a velocity ramp — perfect for timpani
rolls, snare rolls, tremolo on any instrument. The velocity ramps
from ``velocity_start`` to ``velocity_end`` for crescendo or
decrescendo effects.
.. code-block:: python
# Timpani crescendo roll
timp = score.part("timp", instrument="timpani")
timp.roll("C3", Duration.WHOLE, velocity_start=20, velocity_end=110)
timp.add("C3", Duration.HALF, velocity=127) # big accent
# Snare roll with 32nd notes
snare = score.part("snare", synth="noise", envelope="pluck")
snare.roll("C4", Duration.HALF, speed=0.125,
velocity_start=40, velocity_end=100)
# Decrescendo (loud to quiet)
timp.roll("G2", Duration.WHOLE, velocity_start=100, velocity_end=30)
Parameters:
- ``velocity_start``: Starting velocity (default 40).
- ``velocity_end``: Ending velocity (default 100).
- ``speed``: Note subdivision (default ``Duration.SIXTEENTH``).
Use ``0.125`` for 32nd notes, ``Duration.EIGHTH`` for 8th notes.
Tuning Systems
--------------
A Score can use any tuning system and temperament:
.. code-block:: python
# Baroque harpsichord — meantone tuning, A=415
score = Score("4/4", bpm=80, temperament="meantone",
reference_pitch=415.0)
# Indian classical — 22-shruti system
score = Score("4/4", bpm=75, system="shruti")
# Just intonation — pure intervals
score = Score("4/4", bpm=90, temperament="just")
The Score constructor accepts these tuning parameters:
- ``system``: Musical system name (default ``"western"``). Any system
from :doc:`systems` works — ``"indian"``, ``"shruti"``, ``"maqam"``,
``"carnatic"``, etc. Note strings in ``Part.add()`` are parsed against
this system.
- ``temperament``: Tuning temperament — ``"equal"`` (default),
``"pythagorean"``, ``"meantone"``, ``"just"``.
- ``reference_pitch``: Concert pitch in Hz (default 440.0). Use 415.0
for Baroque tuning, 432.0 for "Verdi tuning", etc.
Custom equal temperaments via the ``TET()`` factory:
.. code-block:: python
from pytheory import TET
edo19 = TET(19) # 19-tone equal temperament
score = Score("4/4", bpm=100, system=edo19)
File diff suppressed because it is too large Load Diff

Some files were not shown because too many files have changed in this diff Show More