Compare commits

..

52 Commits

Author SHA1 Message Date
kennethreitz 1d897c6609 Auto bass clef detection in to_abc() — v0.41.2
Parts with average note octave below C4 get clef=bass automatically.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-07 07:43:51 -04:00
kennethreitz 4113aad5d0 Fix to_abc() crash on parts with drum tones — v0.41.1
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-07 07:40:07 -04:00
kennethreitz 6ecef688e1 ABC notation export via Score.to_abc() — v0.41.0
New method converts scores to ABC notation with support for multi-voice,
chords, rests, accidentals, and all durations. Pass html=True for a
self-contained HTML page with abcjs sheet music rendering.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-07 07:37:56 -04:00
kennethreitz fcc5db8e3d Add Score.to_abc() for ABC notation export with optional HTML rendering
Supports multi-voice scores, chords, rests, accidentals, and all durations.
Pass html=True to get a self-contained page using abcjs for sheet music.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-07 07:31:02 -04:00
kennethreitz 9de113b6e7 Add sound examples for hard sync, ring mod, wavefold, drift, karplus-strong, mellotron docs
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-04 14:04:37 -04:00
kennethreitz 0b98f7bd77 Mellotron, hard sync, ring mod, wavefold, drift synths + analog presets — v0.40.9
Five new synth waveforms: tape-replay Mellotron (strings/flute/choir
tapes with wow, flutter, saturation, 8s fadeout), hard sync oscillator,
ring modulation, wavefolding, and analog drift VCO with pitch
instability. 14 new instrument presets for Score.part(). Synth kwargs
now pass through play()/save()/_render(). 808 bass envelope fixed
from pluck to piano.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-02 23:14:18 -04:00
kennethreitz e0a1ce9d18 Fix hold() inflating Part.total_beats and Score.duration_ms — v0.40.8
Note.beats now returns 0.0 for held notes (_hold=True), matching the
renderer which already skipped advancing the beat position. Previously
every hold() call added its full duration to the part's total, causing
duration reports to be 2-3x too long on tracks with drone notes.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-01 13:27:08 -04:00
kennethreitz de7575fe0a Expose rhodes, wurlitzer, vibraphone, pipe organ, choir in Synth enum — v0.40.7
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-01 01:59:11 -04:00
kennethreitz 665a6f5de5 Remove lowpass/vel_to_filter from sax presets, let wave shape its own tone — v0.40.6
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-01 00:44:01 -04:00
kennethreitz 63362df697 Saxophone synth overhaul: reed clipping, formants, breath noise — v0.40.5
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-01 00:38:10 -04:00
kennethreitz 755b33a63b Fix test: update Synth enum count 42 → 46
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-31 07:12:43 -04:00
kennethreitz 40901d603d Multi-stage distortion: preamp, power amp, asymmetric clipping — v0.40.4
Single tanh was too mild. Now chains preamp gain → power amp clip →
asymmetric rectifier sag for proper overdrive/fuzz character.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-31 07:08:10 -04:00
kennethreitz 9b3cbd9065 Add crotales, tingsha, rain stick, ocean drum, cabasa, wind chimes, finger cymbal — v0.40.3
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-30 14:50:03 -04:00
kennethreitz 0911947971 v0.40.2 — dial back master compressor
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-30 04:43:53 -04:00
kennethreitz c2f748d5f3 Dial back master compressor: raise threshold, cap makeup gain
Threshold 0.5 → 0.7 so more dynamics survive. Makeup gain capped
at 3x so sparse arrangements (solo singing bowl, etc.) don't get
over-amplified to clipping.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-30 04:42:52 -04:00
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
110 changed files with 4912 additions and 136 deletions
+105
View File
@@ -2,6 +2,111 @@
All notable changes to PyTheory are documented here.
## 0.41.2
- **Auto bass clef** — `to_abc()` detects low-register parts (808, bass, timpani)
and assigns `clef=bass` automatically based on average note octave.
## 0.41.1
- **Fix** — `to_abc()` no longer crashes on parts containing drum tones.
## 0.41.0
- **ABC notation export** — `Score.to_abc()` converts scores to ABC notation
strings. Supports multi-voice scores (via `V:` directives), chords, rests,
accidentals, and all standard durations. Pass `html=True` to get a
self-contained HTML page that renders sheet music in the browser via abcjs.
## 0.40.9
- **Mellotron synth** — tape-replay keyboard with wow/flutter, tape saturation,
bandwidth limiting, hiss, and 8-second tape fadeout. Three tape banks via the
`tape` parameter: `"strings"` (default), `"flute"`, and `"choir"`.
- **Analog oscillator synths** — four new waveform generators for fat, alive,
analog-style sounds:
- `Synth.HARD_SYNC` — slave oscillator hard-synced to a master (Prophet-5
leads). `slave_ratio` parameter controls harmonic content.
- `Synth.RING_MOD` — two oscillators multiplied for metallic, bell-like
inharmonic tones. `mod_ratio` parameter.
- `Synth.WAVEFOLD` — west coast wavefolding (Buchla-style). `folds` parameter
sweeps from warm to gnarly.
- `Synth.DRIFT` — analog VCO with pitch drift, jitter, and noise floor.
`shape` parameter (`"saw"`, `"square"`, `"triangle"`, `"pulse"`) and
`drift_amount` for instability level.
- **Synth kwargs passthrough** — `play()`, `save()`, and `_render()` now accept
`**synth_kw` for forwarding parameters to synth wave functions (e.g.
`play(tone, synth=Synth.MELLOTRON, tape="choir")`).
- **14 new instrument presets** — `mellotron`, `mellotron_strings`,
`mellotron_flute`, `mellotron_choir`, `sync_lead`, `sync_lead_bright`,
`ring_mod_bell`, `ring_mod_metallic`, `wavefold_warm`, `wavefold_gnarly`,
`drift_saw`, `drift_square`, `analog_pad`, `analog_bass`.
- **808 bass envelope fix** — changed from `pluck` (zero sustain, wrong for 808)
to `piano` (sharp attack with long decay tail).
## 0.40.8
- **Fix hold() inflating duration** — `Note.beats` was returning the full
duration for held notes (`_hold=True`), causing `Part.total_beats` and
`Score.duration_ms` to overcount. A part with `hold(Sa, WHOLE * 4)` followed
by `add(Pa, QUARTER)` would report 17 beats instead of 1. Now held notes
return 0 beats, matching the renderer which already skipped advancing the
timeline for held notes.
## 0.40.7
- **Expose missing Synth enum entries** — rhodes, wurlitzer, vibraphone,
pipe organ, and choir wave functions were already implemented but not
accessible via the Synth enum. Now available as `Synth.RHODES`,
`Synth.WURLITZER`, `Synth.VIBRAPHONE`, `Synth.PIPE_ORGAN`, `Synth.CHOIR`.
## 0.40.6
- **Saxophone presets cleaned up** — removed lowpass filters and vel_to_filter
from all sax instrument presets (saxophone, alto_sax, tenor_sax, bari_sax).
The saxophone wave function already shapes its own spectrum; the extra
filters were dulling the tone.
## 0.40.5
- **Saxophone synth overhaul** — reed nonlinearity (asymmetric soft clipping),
conical bore formant resonances, breath noise with attack envelope, separate
reed buzz, key click transient, and sub-harmonic warmth. Vibrato dialed back
to subtle, delayed onset.
## 0.40.4
- **Distortion overhaul** — multi-stage clipping (preamp → power amp →
asymmetric rectifier) replaces single-stage tanh. Crunch, distorted,
orange crunch, and metal guitar presets now sound properly driven.
## 0.40.3
- **Crotales synth** — tuned bronze discs with long ring and bright harmonics
- **Tingsha synth** — paired Tibetan cymbals with beating from two detuned discs
- **Rain stick** — cascading pebbles (steep and slow/shallow variants)
- **Ocean drum** — steel beads rolling inside a frame drum, surf wash
- **Cabasa** — metal bead chain on cylinder, bright metallic scrape
- **Wind chimes** — multiple suspended metal tubes ringing at random offsets
- **Finger cymbal** — single zill tap, bright metallic ping
- `crotales`, `tingsha`, `singing_bowl`, `singing_bowl_ring` instrument presets
- Audio demos in docs for all new sounds
## 0.40.2
- **Master compressor dialed back** — threshold raised from 0.5 to 0.7,
makeup gain capped at 3x. Sparse arrangements no longer get
over-amplified to clipping.
## 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
+207
View File
@@ -0,0 +1,207 @@
"""Demo the 5 new synths: Mellotron, Hard Sync, Ring Mod, Wavefold, Drift.
Each synth gets a short musical phrase — not just a scale run — with
reverb and rhythmic variety to show off its character.
"""
from pytheory import Score, Duration, play_score
EIGHTH = Duration.EIGHTH
QUARTER = Duration.QUARTER
HALF = Duration.HALF
DOTTED_Q = Duration.DOTTED_QUARTER
WHOLE = Duration.WHOLE
# ── Mellotron Strings ────────────────────────────────────────────────────────
# Strawberry Fields vibes — slow, haunted, with rests that breathe.
print("=== MELLOTRON STRINGS ===")
s = Score("4/4", bpm=72)
p = s.part("tape", instrument="mellotron_strings",
reverb=0.45, reverb_type="cathedral", reverb_decay=2.0)
p.add("G4", HALF).add("B4", QUARTER).add("D5", QUARTER)
p.add("C5", DOTTED_Q).add("B4", EIGHTH).add("A4", HALF)
p.rest(QUARTER)
p.add("G4", DOTTED_Q).add("F#4", EIGHTH).add("G4", WHOLE)
play_score(s)
# ── Mellotron Flute ──────────────────────────────────────────────────────────
# Lonely, breathy, with space between phrases.
print("\n=== MELLOTRON FLUTE ===")
s = Score("3/4", bpm=84)
p = s.part("flute", instrument="mellotron_flute",
reverb=0.5, reverb_type="taj_mahal", reverb_decay=2.5)
p.add("E5", HALF).add("D5", QUARTER)
p.add("C5", DOTTED_Q).add("B4", EIGHTH).rest(QUARTER)
p.add("A4", HALF).add("G4", QUARTER)
p.add("A4", HALF).rest(QUARTER)
p.add("E5", QUARTER).add("D5", QUARTER).add("C5", QUARTER)
p.add("B4", HALF + QUARTER)
play_score(s)
# ── Mellotron Choir ──────────────────────────────────────────────────────────
# Ghostly pad — slow chords, big reverb.
print("\n=== MELLOTRON CHOIR ===")
s = Score("4/4", bpm=60)
p = s.part("choir", instrument="mellotron_choir",
reverb=0.6, reverb_type="cathedral", reverb_decay=3.0)
p.add("C4", WHOLE)
p.add("E4", HALF).add("G4", HALF)
p.add("A4", DOTTED_Q).add("G4", EIGHTH).add("F4", HALF)
p.add("E4", WHOLE)
play_score(s)
# ── Hard Sync Lead ───────────────────────────────────────────────────────────
# Aggressive, punchy — fast 16ths and syncopation.
print("\n=== HARD SYNC LEAD ===")
s = Score("4/4", bpm=128)
p = s.part("sync", instrument="sync_lead",
reverb=0.25, reverb_type="plate")
p.add("E4", EIGHTH).add("E4", EIGHTH).rest(EIGHTH).add("G4", EIGHTH)
p.add("A4", QUARTER).add("G4", EIGHTH).add("E4", EIGHTH)
p.add("D4", EIGHTH).rest(EIGHTH).add("E4", EIGHTH).add("G4", EIGHTH)
p.add("A4", HALF)
p.rest(QUARTER).add("B4", EIGHTH).add("A4", EIGHTH)
p.add("G4", QUARTER).add("E4", QUARTER).add("D4", HALF)
play_score(s)
# ── Hard Sync Bright ─────────────────────────────────────────────────────────
# Higher slave ratio — more harmonics, screaming lead.
print("\n=== HARD SYNC BRIGHT ===")
s = Score("4/4", bpm=138)
p = s.part("sync2", instrument="sync_lead_bright",
reverb=0.2, reverb_type="plate")
p.add("A4", EIGHTH).add("C5", EIGHTH).add("D5", QUARTER)
p.rest(EIGHTH).add("E5", EIGHTH).add("D5", EIGHTH).add("C5", EIGHTH)
p.add("A4", QUARTER).rest(QUARTER).add("G4", EIGHTH).add("A4", EIGHTH)
p.add("C5", HALF)
play_score(s)
# ── Ring Mod Bell ────────────────────────────────────────────────────────────
# Shimmery, metallic — sparse hits with long reverb tail.
print("\n=== RING MOD BELL ===")
s = Score("4/4", bpm=66)
p = s.part("bell", instrument="ring_mod_bell",
reverb=0.6, reverb_type="cave", reverb_decay=3.0)
p.add("C5", HALF).rest(QUARTER).add("G4", QUARTER)
p.rest(HALF).add("E5", HALF)
p.add("D5", QUARTER).rest(QUARTER).add("C5", HALF)
p.rest(WHOLE)
p.add("G4", QUARTER).add("A4", QUARTER).add("C5", HALF)
play_score(s)
# ── Ring Mod Metallic ────────────────────────────────────────────────────────
# Alien, inharmonic — atonal stabs.
print("\n=== RING MOD METALLIC ===")
s = Score("4/4", bpm=100)
p = s.part("metal", instrument="ring_mod_metallic",
reverb=0.4, reverb_type="parking_garage", reverb_decay=2.0)
p.add("F4", EIGHTH).rest(EIGHTH).add("Ab4", EIGHTH).add("F4", EIGHTH)
p.rest(QUARTER).add("Db5", QUARTER).rest(QUARTER)
p.add("C5", EIGHTH).add("Ab4", EIGHTH).rest(QUARTER).add("F4", HALF)
p.rest(HALF).add("Db5", QUARTER).add("C5", QUARTER)
play_score(s)
# ── Wavefold Warm ────────────────────────────────────────────────────────────
# Gentle folds — round and musical, like a filtered saw with overtones.
print("\n=== WAVEFOLD WARM ===")
s = Score("4/4", bpm=108)
p = s.part("fold", instrument="wavefold_warm",
reverb=0.3, reverb_type="plate")
p.add("A3", QUARTER).add("C4", QUARTER).add("E4", QUARTER).add("A4", QUARTER)
p.add("G4", DOTTED_Q).add("E4", EIGHTH).add("C4", HALF)
p.add("D4", QUARTER).add("F4", QUARTER).add("A4", HALF)
p.add("G4", WHOLE)
play_score(s)
# ── Wavefold Gnarly ──────────────────────────────────────────────────────────
# Cranked folds — buzzy, aggressive, with syncopation.
print("\n=== WAVEFOLD GNARLY ===")
s = Score("4/4", bpm=130)
p = s.part("gnarly", instrument="wavefold_gnarly",
reverb=0.2, reverb_type="spring")
p.add("E3", EIGHTH).add("E3", EIGHTH).rest(EIGHTH).add("G3", EIGHTH)
p.add("A3", EIGHTH).rest(EIGHTH).add("B3", EIGHTH).add("A3", EIGHTH)
p.add("E3", QUARTER).add("G3", EIGHTH).add("A3", EIGHTH).add("B3", QUARTER)
p.rest(QUARTER)
p.add("E4", EIGHTH).add("D4", EIGHTH).add("B3", QUARTER).add("A3", HALF)
play_score(s)
# ── Drift Saw ────────────────────────────────────────────────────────────────
# Warm, alive analog saw — the Minimoog pad.
print("\n=== DRIFT SAW (vintage VCO) ===")
s = Score("4/4", bpm=88)
p = s.part("drift", instrument="drift_saw",
reverb=0.35, reverb_type="taj_mahal", reverb_decay=2.0)
p.add("D4", HALF).add("F4", HALF)
p.add("A4", DOTTED_Q).add("G4", EIGHTH).add("F4", QUARTER).rest(QUARTER)
p.add("D4", QUARTER).add("E4", QUARTER).add("F4", HALF)
p.add("D4", WHOLE)
play_score(s)
# ── Drift Square ─────────────────────────────────────────────────────────────
# Hollow, wobbly — 8-bit with analog soul.
print("\n=== DRIFT SQUARE ===")
s = Score("4/4", bpm=110)
p = s.part("dsq", instrument="drift_square",
reverb=0.25, reverb_type="plate")
p.add("C4", EIGHTH).add("E4", EIGHTH).add("G4", QUARTER).add("E4", QUARTER)
p.rest(QUARTER)
p.add("A4", EIGHTH).add("G4", EIGHTH).add("E4", QUARTER).add("C4", HALF)
p.add("D4", QUARTER).add("F4", EIGHTH).add("G4", EIGHTH).add("A4", HALF)
p.add("G4", WHOLE)
play_score(s)
# ── Analog Pad ───────────────────────────────────────────────────────────────
# Slow, drifting chords — Juno-style lushness.
print("\n=== ANALOG PAD ===")
s = Score("4/4", bpm=70)
p = s.part("pad", instrument="analog_pad",
reverb=0.5, reverb_type="taj_mahal", reverb_decay=3.0)
p.add("A3", WHOLE)
p.add("C4", HALF).add("E4", HALF)
p.add("F4", WHOLE)
p.add("E4", HALF).add("D4", HALF)
p.add("C4", WHOLE)
play_score(s)
# ── Analog Bass ──────────────────────────────────────────────────────────────
# Tight, punchy — Moog bass with filter sweep.
print("\n=== ANALOG BASS ===")
s = Score("4/4", bpm=120)
p = s.part("bass", instrument="analog_bass",
reverb=0.1, reverb_type="plate")
p.add("E2", EIGHTH).add("E2", EIGHTH).rest(EIGHTH).add("G2", EIGHTH)
p.add("A2", QUARTER).rest(QUARTER)
p.add("E2", EIGHTH).rest(EIGHTH).add("B2", EIGHTH).add("A2", EIGHTH)
p.add("G2", QUARTER).add("E2", QUARTER).rest(HALF)
p.add("E2", EIGHTH).add("E2", EIGHTH).add("G2", EIGHTH).add("A2", EIGHTH)
p.add("B2", QUARTER).add("A2", QUARTER).add("E2", HALF)
play_score(s)
print("\nDone!")
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.
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.
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.
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.
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.
+185
View File
@@ -662,6 +662,41 @@ def gen_synth_kalimba():
p.add(n, Duration.QUARTER, velocity=85)
render("synth_kalimba", score)
def gen_synth_wurlitzer():
score = Score("4/4", bpm=90)
p = score.part("demo", instrument="wurlitzer", volume=0.5, reverb=0.25)
p.hold("C3", Duration.WHOLE * 2, velocity=60)
p.hold("Eb3", Duration.WHOLE * 2, velocity=55)
p.hold("G3", Duration.WHOLE * 2, velocity=55)
for n in ["G4", "Bb4", "C5", "Bb4", "G4", "F4", "Eb4", "G4"]:
p.add(n, Duration.QUARTER, velocity=78)
render("synth_wurlitzer", score)
def gen_synth_vibraphone():
score = Score("4/4", bpm=90)
p = score.part("demo", instrument="vibraphone", volume=0.5)
for n in ["C5", "E5", "G5", "C6", "G5", "E5", "C5", "E5"]:
p.add(n, Duration.QUARTER, velocity=75)
render("synth_vibraphone", score)
def gen_synth_pipe_organ():
score = Score("4/4", bpm=70)
p = score.part("demo", instrument="pipe_organ", volume=0.5)
p.hold("C3", Duration.WHOLE * 4, velocity=70)
p.hold("G3", Duration.WHOLE * 4, velocity=65)
for n in ["C4", "D4", "E4", "F4", "G4", "F4", "E4", "D4",
"C4", "E4", "G4", "C5", "G4", "E4", "C4", "C4"]:
p.add(n, Duration.QUARTER, velocity=75)
render("synth_pipe_organ", score)
def gen_synth_choir():
score = Score("4/4", bpm=70)
p = score.part("demo", instrument="choir", volume=0.5)
for n, v in [("C4", "ah"), ("E4", "oh"), ("G4", "ah"), ("C5", "ee"),
("G4", "oh"), ("E4", "ah"), ("C4", "oo"), ("C4", "ah")]:
p.add(n, Duration.HALF, velocity=70, lyric=v)
render("synth_choir", score)
def gen_synth_organ():
_synth_demo("organ", "organ_synth", envelope="organ")
@@ -815,6 +850,56 @@ def gen_synth_ukulele():
p.strum(ch, Duration.WHOLE, velocity=72)
render("synth_ukulele", score)
def gen_synth_hard_sync():
score = Score("4/4", bpm=120)
p = score.part("demo", instrument="sync_lead_bright", volume=0.5)
for n in ["C4", "E4", "G4", "C5", "G4", "E4", "C4", "E4"]:
p.add(n, Duration.QUARTER, velocity=90)
render("synth_hard_sync", score)
def gen_synth_ring_mod():
score = Score("4/4", bpm=90)
p = score.part("demo", instrument="ring_mod_bell", volume=0.5)
for n in ["C5", "E5", "G5", "C6", "G5", "E5", "C5", "E5"]:
p.add(n, Duration.QUARTER, velocity=80)
render("synth_ring_mod", score)
def gen_synth_wavefold():
score = Score("4/4", bpm=110)
p = score.part("demo", instrument="wavefold_warm", volume=0.5)
for n in ["C4", "E4", "G4", "C5", "G4", "E4", "C4", "E4"]:
p.add(n, Duration.QUARTER, velocity=85)
render("synth_wavefold", score)
def gen_synth_drift():
score = Score("4/4", bpm=90)
p = score.part("demo", instrument="drift_saw", volume=0.5, reverb=0.35,
reverb_type="taj_mahal")
for n in ["C4", "E4", "G4", "C5", "G4", "E4", "C4", "E4"]:
p.add(n, Duration.HALF, velocity=75)
render("synth_drift", score)
def gen_synth_karplus():
score = Score("4/4", bpm=100)
p = score.part("demo", synth="pluck_synth", envelope="none",
volume=0.5, reverb=0.2)
for n in ["C4", "E4", "G4", "C5", "G4", "E4", "C4", "E4"]:
p.add(n, Duration.QUARTER, velocity=85)
render("synth_karplus", score)
def gen_synth_mellotron():
score = Score("4/4", bpm=80)
p = score.part("demo", instrument="mellotron_flute", volume=0.5)
for n in ["C4", "E4", "G4", "C5"]:
p.add(n, Duration.WHOLE, velocity=75)
render("synth_mellotron", score)
def gen_synth_granular():
score = Score("4/4", bpm=80)
p = score.part("demo", instrument="granular_pad", volume=0.5, reverb=0.4)
@@ -823,6 +908,86 @@ def gen_synth_granular():
render("synth_granular", score)
def gen_synth_crotales():
score = Score("4/4", bpm=60)
p = score.part("demo", synth="crotales_synth", envelope="none",
volume=0.5, reverb=0.3)
for n in ["C6", "E6", "G6", "C7", "G6", "E6", "C6"]:
p.add(n, Duration.HALF, velocity=80)
render("synth_crotales", score)
def gen_synth_tingsha():
score = Score("4/4", bpm=40)
p = score.part("demo", synth="tingsha_synth", envelope="none",
volume=0.5, reverb=0.4)
for n in ["E5", "A5", "E6", "A5"]:
p.add(n, Duration.WHOLE, velocity=75)
render("synth_tingsha", score)
def gen_rainstick():
score = Score("4/4", bpm=60)
p = score.part("demo", synth="sine", volume=1.0)
p.hit(DrumSound.RAINSTICK, Duration.WHOLE * 3, velocity=90)
render("rainstick", score)
def gen_rainstick_slow():
score = Score("4/4", bpm=60)
p = score.part("demo", synth="sine", volume=1.0)
p.hit(DrumSound.RAINSTICK_SLOW, Duration.WHOLE * 4, velocity=85)
render("rainstick_slow", score)
def gen_ocean_drum():
score = Score("4/4", bpm=60)
p = score.part("demo", synth="sine", volume=1.0)
p.hit(DrumSound.OCEAN_DRUM, Duration.WHOLE * 3, velocity=85)
render("ocean_drum", score)
def gen_cabasa():
score = Score("4/4", bpm=100)
p = score.part("demo", synth="sine", volume=1.0)
for _ in range(16):
p.hit(DrumSound.CABASA, Duration.EIGHTH, velocity=100)
render("cabasa", score)
def gen_wind_chimes():
score = Score("4/4", bpm=60)
p = score.part("demo", synth="sine", volume=1.0)
p.hit(DrumSound.WIND_CHIMES, Duration.WHOLE * 3, velocity=85)
render("wind_chimes", score)
def gen_finger_cymbal():
score = Score("4/4", bpm=80)
p = score.part("demo", synth="sine", volume=1.0)
for _ in range(8):
p.hit(DrumSound.FINGER_CYMBAL, Duration.QUARTER, velocity=85)
render("finger_cymbal", score)
def gen_synth_singing_bowl_strike():
score = Score("4/4", bpm=40)
p = score.part("demo", synth="singing_bowl_strike_synth", envelope="none",
volume=0.5, reverb=0.4)
for n in ["A3", "D4", "F4", "A4"]:
p.add(n, Duration.WHOLE, velocity=80)
render("synth_singing_bowl_strike", score)
def gen_synth_singing_bowl_ring():
score = Score("4/4", bpm=30)
p = score.part("demo", synth="singing_bowl_ring_synth", envelope="none",
volume=0.5, reverb=0.4)
for n in ["A3", "D4", "A4"]:
p.add(n, Duration.WHOLE * 2, velocity=75)
render("synth_singing_bowl_ring", score)
def gen_arpeggio():
score = Score("4/4", bpm=132)
score.drums("house", repeats=8)
@@ -1004,6 +1169,10 @@ GENERATORS = [
gen_synth_electric_guitar,
gen_synth_sitar,
gen_synth_kalimba,
gen_synth_wurlitzer,
gen_synth_vibraphone,
gen_synth_pipe_organ,
gen_synth_choir,
gen_synth_organ,
gen_synth_marimba,
gen_synth_harp,
@@ -1020,7 +1189,23 @@ GENERATORS = [
gen_synth_banjo,
gen_synth_mandolin,
gen_synth_ukulele,
gen_synth_hard_sync,
gen_synth_ring_mod,
gen_synth_wavefold,
gen_synth_drift,
gen_synth_karplus,
gen_synth_mellotron,
gen_synth_granular,
gen_synth_crotales,
gen_synth_tingsha,
gen_synth_singing_bowl_strike,
gen_synth_singing_bowl_ring,
gen_rainstick,
gen_rainstick_slow,
gen_ocean_drum,
gen_cabasa,
gen_wind_chimes,
gen_finger_cymbal,
gen_arpeggio,
gen_legato_glide,
gen_acid_house,
+16
View File
@@ -44,6 +44,22 @@ Optional parameters for synth, envelope, and temperament:
play(Tone.from_string("C4"), synth=Synth.SAW, envelope=Envelope.PLUCK, t=1_000)
play(Tone.from_string("C4"), temperament="pythagorean", t=1_000)
Synth-specific parameters are passed through as keyword arguments:
.. code-block:: python
# Mellotron with flute tape
play(Tone.from_string("C4"), synth=Synth.MELLOTRON, tape="choir", t=2_000)
# Hard sync with custom slave ratio
play(Tone.from_string("C4"), synth=Synth.HARD_SYNC, slave_ratio=2.5)
# Wavefolding with 4 folds
play(Tone.from_string("C4"), synth=Synth.WAVEFOLD, folds=4.0)
# Drift oscillator with square shape
play(Tone.from_string("C4"), synth=Synth.DRIFT, shape="square")
play_score() -- Full Arrangements
---------------------------------
+386 -5
View File
@@ -1,7 +1,7 @@
Synthesizers
============
PyTheory includes 41 built-in waveforms and 10 ADSR envelope presets.
PyTheory includes 56 built-in waveforms and 10 ADSR envelope presets.
Every sound is generated from scratch -- no samples or external audio
files needed.
@@ -249,6 +249,130 @@ produces a natural chorus/vibrato effect built into the waveform itself.
<audio controls style="width:100%;margin:0.3em 0 0.5em"><source src="../_static/audio/synth_pwm_fast.wav" type="audio/wav"></audio>
Analog Synthesis
----------------
These waveforms model the behavior of real analog hardware — the
imperfections, interactions, and nonlinearities that make a room full
of vintage synths sound so much more alive than a room full of VSTs.
Each one is a different approach to the same question: how do you make
a digital oscillator sound like it has a soul?
Hard Sync
~~~~~~~~~
A "slave" oscillator is forced to restart its cycle every time a
"master" oscillator completes one. The abrupt restart creates bright
formant peaks that sweep as the slave ratio changes. This is THE sound
of the Prophet-5, Moog Prodigy, and every screaming analog lead since
1978.
**Use for:** aggressive leads, formant sweeps, cutting solos.
.. code-block:: python
lead = score.part("lead", synth="hard_sync", envelope="pluck")
# Higher slave ratio = more harmonics, brighter
from pytheory import play, Synth, Tone
play(Tone.from_string("C4"), synth=Synth.HARD_SYNC, slave_ratio=2.5)
.. raw:: html
<audio controls style="width:100%;margin:0.3em 0 0.5em"><source src="../_static/audio/synth_hard_sync.wav" type="audio/wav"></audio>
Ring Modulation
~~~~~~~~~~~~~~~
Two oscillators multiplied together, producing sum and difference
frequencies. Unlike FM, ring mod outputs only sidebands — no carrier
or modulator fundamental. The result is metallic, bell-like, and often
inharmonic. Classic Dalek voice, Stockhausen, and every sci-fi
soundtrack.
**Use for:** metallic bells, alien textures, inharmonic percussion.
.. code-block:: python
bells = score.part("bells", instrument="ring_mod_bell",
reverb=0.5, reverb_type="cave")
# Non-integer ratios = more inharmonic
play(Tone.from_string("C4"), synth=Synth.RING_MOD, mod_ratio=2.1)
.. raw:: html
<audio controls style="width:100%;margin:0.3em 0 0.5em"><source src="../_static/audio/synth_ring_mod.wav" type="audio/wav"></audio>
Wavefolding
~~~~~~~~~~~
The heart of west coast synthesis (Buchla, Make Noise, Verbos). A sine
wave is amplified past ±1.0, then "folded" — the overflow bounces back
instead of clipping. Each fold adds new harmonic pairs. At low fold
counts it's warm and round; crank it up and it gets buzzy, gnarly, and
alive.
This sounds completely different from subtractive synthesis — instead of
*removing* harmonics with a filter, you're *generating* them by shaping
the wave. Pairs beautifully with a lowpass filter after the fold.
**Use for:** complex leads, evolving textures, west coast basslines.
.. code-block:: python
# Warm, musical folding
warm = score.part("fold", instrument="wavefold_warm")
# Cranked and aggressive
gnarly = score.part("gnarly", instrument="wavefold_gnarly")
# Direct control over fold amount
play(Tone.from_string("C4"), synth=Synth.WAVEFOLD, folds=3.0)
.. raw:: html
<audio controls style="width:100%;margin:0.3em 0 0.5em"><source src="../_static/audio/synth_wavefold.wav" type="audio/wav"></audio>
Drift Oscillator
~~~~~~~~~~~~~~~~
Real analog oscillators are never perfectly stable. Capacitor charging,
thermal variations, and component tolerances make the pitch wander
slightly. This is what makes a Minimoog sound "fat" and a VST sound
"thin" — the constant micro-motion of imperfect hardware.
The drift oscillator models slow pitch drift (< 1 Hz wander), fast
jitter (per-cycle randomness), a soft analog noise floor, and slightly
rounded waveform edges. It turns any basic shape into something that
breathes.
**Use for:** analog-style pads, warm basses, vintage leads, any voice
that needs to feel "alive."
.. code-block:: python
# Vintage Minimoog-style saw
pad = score.part("pad", instrument="drift_saw",
reverb=0.35, reverb_type="taj_mahal")
# Hollow square with analog wobble
sq = score.part("sq", instrument="drift_square")
# Control the shape and instability directly
play(Tone.from_string("C4"), synth=Synth.DRIFT,
shape="triangle", drift_amount=0.25)
.. raw:: html
<audio controls style="width:100%;margin:0.3em 0 0.5em"><source src="../_static/audio/synth_drift.wav" type="audio/wav"></audio>
Drift amount controls how unstable the oscillator is:
- **0.05** = studio-grade (Sequential, Oberheim)
- **0.15** = classic vintage (Minimoog, ARP) — the default
- **0.30** = barely-holding-it-together (old SH-101)
ADSR Envelopes
--------------
@@ -406,6 +530,10 @@ It sounds genuinely like a real guitar, harp, or koto.
guitar = score.part("guitar", synth="pluck_synth")
harp = score.part("harp", instrument="harp") # uses pluck_synth
.. raw:: html
<audio controls style="width:100%;margin:0.3em 0 0.5em"><source src="../_static/audio/synth_karplus.wav" type="audio/wav"></audio>
Hammond Organ
~~~~~~~~~~~~~
@@ -440,11 +568,11 @@ Dedicated Instrument Synths
--------------------------
Beyond the classic and physical modeling waveforms, PyTheory includes
31 dedicated instrument synths. Each one uses tailored synthesis
36 dedicated instrument synths. Each one uses tailored synthesis
techniques -- additive harmonics, formant shaping, body resonance
modeling, and specialized envelopes -- to capture the character of a
specific acoustic instrument. These are the waveforms that bring the
total count to 41.
total count to 56.
Piano Synth
~~~~~~~~~~~
@@ -479,6 +607,108 @@ singing sustain. The sound of jazz clubs, soul, and neo-soul.
<audio controls style="width:100%;margin:0.3em 0 0.5em"><source src="../_static/audio/synth_rhodes.wav" type="audio/wav"></audio>
Wurlitzer Electric Piano
~~~~~~~~~~~~~~~~~~~~~~~~
The Wurlitzer uses a vibrating steel reed (not a tine like Rhodes)
picked up by an electrostatic pickup. More nasal, reedy, and biting
— it barks and growls when played hard. Think Supertramp, Ray Charles.
.. code-block:: python
wurli = score.part("wurli", synth="wurlitzer_synth")
wurli = score.part("wurli", instrument="wurlitzer")
.. raw:: html
<audio controls style="width:100%;margin:0.3em 0 0.5em"><source src="../_static/audio/synth_wurlitzer.wav" type="audio/wav"></audio>
Mellotron
~~~~~~~~~
The original "sampler" — a 1960s keyboard where each key triggers a
strip of magnetic tape with a pre-recorded instrument. The mechanical
tape transport gives it a haunted, lo-fi quality that no digital
emulation fully captures: pitch wobbles from uneven capstan speed,
bandwidth limited to 300 Hz6 kHz (like a worn cassette), soft tape
saturation, and tapes that physically run out after 8 seconds.
The Mellotron defined the sound of *Strawberry Fields Forever*,
*Stairway to Heaven*, and every prog rock record from 19691977.
Three tape banks are available via the ``tape`` parameter:
- ``"strings"`` (default) — the iconic MkII string section
- ``"flute"`` — breathy, haunting solo flute
- ``"choir"`` — ghostly vocal pad
**Use for:** prog rock, haunted textures, vintage orchestral color.
.. code-block:: python
# Use instrument presets (includes reverb)
strings = score.part("strings", instrument="mellotron_strings")
flute = score.part("flute", instrument="mellotron_flute")
choir = score.part("choir", instrument="mellotron_choir")
# Or select the tape directly
from pytheory import play, Synth, Tone
play(Tone.from_string("C4"), synth=Synth.MELLOTRON, tape="flute", t=3000)
.. raw:: html
<audio controls style="width:100%;margin:0.3em 0 0.5em"><source src="../_static/audio/synth_mellotron.wav" type="audio/wav"></audio>
Vibraphone Synth
~~~~~~~~~~~~~~~~
Struck aluminum bars with motor-driven tremolo discs. The spinning
motor modulates the sound through the resonator tubes, creating the
signature vibraphone shimmer. Inharmonic bar modes at 1x, 2.76x, 5.4x.
.. code-block:: python
vib = score.part("vib", synth="vibraphone_synth")
vib = score.part("vib", instrument="vibraphone")
.. raw:: html
<audio controls style="width:100%;margin:0.3em 0 0.5em"><source src="../_static/audio/synth_vibraphone.wav" type="audio/wav"></audio>
Pipe Organ Synth
~~~~~~~~~~~~~~~~
Multiple ranks of pipes — principal 8', octave 4', fifteenth 2'.
Constant air pressure means no dynamics. Wind chiff at the attack.
Best with cathedral reverb.
.. code-block:: python
organ = score.part("organ", synth="pipe_organ_synth")
organ = score.part("organ", instrument="pipe_organ")
.. raw:: html
<audio controls style="width:100%;margin:0.3em 0 0.5em"><source src="../_static/audio/synth_pipe_organ.wav" type="audio/wav"></audio>
Choir Synth
~~~~~~~~~~~
Voices singing vowels shaped by formant bandpass filters. The glottal
source is filtered through vocal tract resonances — F1, F2, F3, F4 —
which is what makes "ah" sound different from "oo". Use ``lyric=``
to control the vowel. Best with ``ensemble=`` for a full section.
.. code-block:: python
choir = score.part("choir", synth="choir_synth")
choir = score.part("choir", instrument="choir") # ensemble=6 + cathedral reverb
choir.add("C4", Duration.WHOLE, lyric="ah")
.. raw:: html
<audio controls style="width:100%;margin:0.3em 0 0.5em"><source src="../_static/audio/synth_choir.wav" type="audio/wav"></audio>
Bass Guitar Synth
~~~~~~~~~~~~~~~~~
@@ -864,6 +1094,143 @@ Parameters (passed as synth kwargs):
<audio controls style="width:100%;margin:0.3em 0 0.5em"><source src="../_static/audio/synth_granular.wav" type="audio/wav"></audio>
Crotales
~~~~~~~~
Small tuned bronze discs (antique cymbals) struck with brass mallets.
Bright, crystalline, bell-like tone with strong upper harmonics that
rings for a long time. Nearly harmonic partials give crotales their
penetrating brilliance — they cut through any orchestra.
.. code-block:: python
crotales = score.part("crotales", synth="crotales_synth", envelope="none",
reverb=0.3)
.. raw:: html
<audio controls style="width:100%;margin:0.3em 0 0.5em"><source src="../_static/audio/synth_crotales.wav" type="audio/wav"></audio>
Tingsha
~~~~~~~
Two small Tibetan cymbals joined by a cord, clashed together. Both discs
ring at slightly different frequencies, producing a bright ping with
pronounced beating — the wavering interference between the two is the
whole character of the sound.
.. code-block:: python
tingsha = score.part("tingsha", synth="tingsha_synth", envelope="none",
reverb=0.4)
.. raw:: html
<audio controls style="width:100%;margin:0.3em 0 0.5em"><source src="../_static/audio/synth_tingsha.wav" type="audio/wav"></audio>
Singing Bowl (Strike)
~~~~~~~~~~~~~~~~~~~~~
Tibetan/Himalayan singing bowl struck with a mallet. The impact excites
inharmonic partials that ring and slowly beat against each other as
near-degenerate mode pairs interfere. Higher modes fade quickly, leaving
the fundamental shimmering for seconds.
.. code-block:: python
bowl = score.part("bowl", synth="singing_bowl_strike_synth", envelope="none",
reverb=0.4)
.. raw:: html
<audio controls style="width:100%;margin:0.3em 0 0.5em"><source src="../_static/audio/synth_singing_bowl_strike.wav" type="audio/wav"></audio>
Singing Bowl (Ring)
~~~~~~~~~~~~~~~~~~~
Rim-rubbed singing bowl — the mallet traces the rim, slowly building the
fundamental into a sustained, pulsing tone. Upper harmonics shimmer in
and out as the bowl resonates.
.. code-block:: python
bowl = score.part("bowl", synth="singing_bowl_ring_synth", envelope="none",
reverb=0.4)
.. raw:: html
<audio controls style="width:100%;margin:0.3em 0 0.5em"><source src="../_static/audio/synth_singing_bowl_ring.wav" type="audio/wav"></audio>
Rain Stick
~~~~~~~~~~
Cascading pebbles through a cactus tube with internal pins. Two variants:
steep angle (fast cascade) and shallow angle (slow trickle).
.. code-block:: python
p.hit(DrumSound.RAINSTICK, Duration.WHOLE * 3) # steep — fast cascade
p.hit(DrumSound.RAINSTICK_SLOW, Duration.WHOLE * 4) # shallow — gentle trickle
.. raw:: html
<audio controls style="width:100%;margin:0.3em 0 0.5em"><source src="../_static/audio/rainstick.wav" type="audio/wav"></audio>
<audio controls style="width:100%;margin:0.3em 0 0.5em"><source src="../_static/audio/rainstick_slow.wav" type="audio/wav"></audio>
Ocean Drum
~~~~~~~~~~
Steel beads rolling inside a frame drum — tilting produces a smooth surf wash.
.. code-block:: python
p.hit(DrumSound.OCEAN_DRUM, Duration.WHOLE * 3)
.. raw:: html
<audio controls style="width:100%;margin:0.3em 0 0.5em"><source src="../_static/audio/ocean_drum.wav" type="audio/wav"></audio>
Cabasa
~~~~~~
Metal bead chain scraped against a textured cylinder — brighter and
more metallic than a shaker.
.. code-block:: python
p.hit(DrumSound.CABASA, Duration.EIGHTH)
.. raw:: html
<audio controls style="width:100%;margin:0.3em 0 0.5em"><source src="../_static/audio/cabasa.wav" type="audio/wav"></audio>
Wind Chimes
~~~~~~~~~~~
Suspended metal tubes struck by hand or breeze. Each tube rings at
its own pitch with slight time offsets.
.. code-block:: python
p.hit(DrumSound.WIND_CHIMES, Duration.WHOLE * 3)
.. raw:: html
<audio controls style="width:100%;margin:0.3em 0 0.5em"><source src="../_static/audio/wind_chimes.wav" type="audio/wav"></audio>
Finger Cymbal
~~~~~~~~~~~~~
Single small cymbal tap (zill) — bright metallic ping.
.. code-block:: python
p.hit(DrumSound.FINGER_CYMBAL, Duration.HALF)
.. raw:: html
<audio controls style="width:100%;margin:0.3em 0 0.5em"><source src="../_static/audio/finger_cymbal.wav" type="audio/wav"></audio>
Analog Oscillator Drift
~~~~~~~~~~~~~~~~~~~~~~~~
@@ -919,13 +1286,19 @@ distorted_guitar, orange_crunch, metal_guitar, bass_guitar, upright_bass,
harp, sitar, koto, banjo, mandolin, mandola, ukulele
**World/Exotic**: pedal_steel, theremin, kalimba, steel_drum, didgeridoo,
bagpipe
bagpipe, singing_bowl, singing_bowl_ring, tingsha
**Synth**: synth_lead, synth_pad, synth_bass, acid_bass, 808_bass,
granular_pad, granular_texture, vocal, choir
**Mellotron**: mellotron, mellotron_strings, mellotron_flute, mellotron_choir
**Analog**: sync_lead, sync_lead_bright, ring_mod_bell, ring_mod_metallic,
wavefold_warm, wavefold_gnarly, drift_saw, drift_square, analog_pad,
analog_bass
**Percussion**: vibraphone, marimba, xylophone, glockenspiel, tubular_bells,
timpani
timpani, crotales
Explicit kwargs override preset defaults:
@@ -966,3 +1339,11 @@ Some practical combos worth memorizing:
long reverb and you're scoring a nature documentary.
- ``saw`` + ``pluck`` = **funk stab.** Short, sharp, bright. The
sound of Nile Rodgers' right hand.
- ``hard_sync`` + ``pluck`` = **prophet lead.** Bright formant peak
that cuts through any mix. The opening riff of every 80s synth solo.
- ``wavefold`` + ``organ`` = **west coast bass.** Warm, harmonically
rich sine-derivative that pairs beautifully with a lowpass after.
- ``drift`` + ``pad`` = **analog pad.** A sawtooth that breathes and
wobbles like a real VCO. Add chorus and reverb for Juno vibes.
- ``mellotron_synth`` + ``organ`` = **prog strings.** Haunted tape
machine. Add cathedral reverb and you're in 1972.
+55
View File
@@ -0,0 +1,55 @@
"""Play a recorded MIDI file through pytheory's full renderer.
Takes a MIDI file captured by the live engine and plays it back
through the complete synthesis pipeline — with ensemble, effects,
reverb, and master compression.
Usage:
python play_recording.py recording.mid
python play_recording.py recording.mid --bpm 110
"""
import sys
import sounddevice as sd
from pytheory import Score
from pytheory.play import render_score, SAMPLE_RATE
def main():
if len(sys.argv) < 2:
print(" Usage: python play_recording.py <file.mid> [--bpm N]")
return
filename = sys.argv[1]
bpm = None
if "--bpm" in sys.argv:
idx = sys.argv.index("--bpm")
if idx + 1 < len(sys.argv):
bpm = int(sys.argv[idx + 1])
print(f" Loading {filename}...")
score = Score.from_midi(filename)
if bpm:
score.bpm = bpm
print(f" {score}")
print(f" Rendering...")
buf = render_score(score)
duration = len(buf) / SAMPLE_RATE
print(f" Playing ({duration:.1f}s)...")
try:
sd.play(buf, SAMPLE_RATE)
sd.wait()
except KeyboardInterrupt:
sd.stop()
print("\n Stopped.")
print(" Done.")
if __name__ == "__main__":
main()

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