Compare commits

...

17 Commits

Author SHA1 Message Date
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
48 changed files with 1027 additions and 83 deletions
+10
View File
@@ -2,6 +2,16 @@
All notable changes to PyTheory are documented here.
## 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
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.
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>
+573
View File
@@ -0,0 +1,573 @@
"""Generate audio samples for documentation.
Renders code examples from the docs as WAV files so they can be
embedded as <audio> players on the website.
Usage:
uv run python docs/generate_audio.py
"""
import os
import sys
sys.path.insert(0, os.path.join(os.path.dirname(__file__), ".."))
from pytheory import Score, Duration, Key, Chord, Fretboard, DrumSound
from pytheory.play import render_score, SAMPLE_RATE
import numpy
import struct
AUDIO_DIR = os.path.join(os.path.dirname(__file__), "_static", "audio")
os.makedirs(AUDIO_DIR, exist_ok=True)
def save_wav(buf, path):
"""Save a float32 buffer as 16-bit stereo WAV."""
# Handle both mono (n,) and stereo (n, 2) buffers
if buf.ndim == 1:
channels = 1
n_frames = len(buf)
else:
channels = buf.shape[1]
n_frames = buf.shape[0]
peak = numpy.abs(buf).max()
if peak > 0:
buf = buf / peak * 0.9
samples = (buf * 32767).astype(numpy.int16)
byte_rate = SAMPLE_RATE * channels * 2
block_align = channels * 2
data_size = n_frames * channels * 2
with open(path, "wb") as f:
f.write(b"RIFF")
f.write(struct.pack("<I", 36 + data_size))
f.write(b"WAVE")
f.write(b"fmt ")
f.write(struct.pack("<IHHIIHH", 16, 1, channels, SAMPLE_RATE,
byte_rate, block_align, 16))
f.write(b"data")
f.write(struct.pack("<I", data_size))
f.write(samples.tobytes())
label = "stereo" if channels == 2 else "mono"
print(f" {os.path.basename(path)} ({n_frames/SAMPLE_RATE:.1f}s, {label})")
def render(name, score):
"""Render a score to WAV in the audio directory."""
buf = render_score(score)
save_wav(buf, os.path.join(AUDIO_DIR, f"{name}.wav"))
# ── Piano hold (polyphonic overlap) ──────────────────────────────────────
def gen_piano_hold():
score = Score("4/4", bpm=85)
piano = score.part("piano", instrument="piano", reverb=0.3)
piano.hold("C3", Duration.WHOLE * 2, velocity=60)
piano.hold("E3", Duration.WHOLE * 2, velocity=55)
piano.hold("G3", Duration.WHOLE * 2, velocity=55)
for n in ["E4", "G4", "C5", "G4", "E4", "D4", "C4", "E4"]:
piano.add(n, Duration.QUARTER, velocity=80)
render("piano_hold", score)
# ── Articulations ────────────────────────────────────────────────────────
def gen_articulations():
score = Score("4/4", bpm=90)
piano = score.part("piano", instrument="piano", reverb=0.25)
for n in ["C4", "E4", "G4", "C5"]:
piano.add(n, Duration.QUARTER, velocity=80)
for n in ["C4", "E4", "G4", "C5"]:
piano.add(n, Duration.QUARTER, velocity=80, articulation="staccato")
for n in ["C5", "G4", "E4", "C4"]:
piano.add(n, Duration.QUARTER, velocity=80, articulation="legato")
for n in ["C4", "E4", "G4", "C5"]:
piano.add(n, Duration.QUARTER, velocity=80, articulation="marcato")
render("articulations", score)
# ── Dynamic curves ───────────────────────────────────────────────────────
def gen_dynamics():
score = Score("4/4", bpm=90)
piano = score.part("piano", instrument="piano", reverb=0.3)
piano.crescendo(["C4", "D4", "E4", "F4", "G4", "A4", "B4", "C5"],
Duration.QUARTER, start_vel=30, end_vel=110)
piano.decrescendo(["C5", "B4", "A4", "G4", "F4", "E4", "D4", "C4"],
Duration.QUARTER, start_vel=110, end_vel=30)
render("dynamics", score)
# ── Filter ramp ──────────────────────────────────────────────────────────
def gen_filter_ramp():
score = Score("4/4", bpm=130)
score.drums("house", repeats=8, fill="house", fill_every=8)
score.set_drum_effects(volume=0.4)
acid = score.part("acid", synth="saw", volume=0.6,
lowpass=300, lowpass_q=8.0, distortion=0.3,
legato=True, glide=0.03)
acid.ramp(over=Duration.WHOLE * 4, curve="ease_in", lowpass=5000)
for _ in range(4):
for n in ["C2", "C3", "C2", "Eb2", "C2", "G2", "Bb2", "C2"]:
acid.add(n, Duration.EIGHTH, velocity=90)
acid.ramp(over=Duration.WHOLE * 4, curve="ease_out", lowpass=300)
for _ in range(4):
for n in ["C2", "C3", "C2", "Eb2", "C2", "G2", "Bb2", "C2"]:
acid.add(n, Duration.EIGHTH, velocity=88)
render("filter_ramp", score)
# ── Rock beat ────────────────────────────────────────────────────────────
def gen_rock_beat():
score = Score("4/4", bpm=120)
score.drums("rock", repeats=4, fill="rock", fill_every=4)
render("rock_beat", score)
def gen_bossa_nova_pattern():
score = Score("4/4", bpm=140)
score.drums("bossa nova", repeats=4)
render("bossa_nova_pattern", score)
def gen_salsa_pattern():
score = Score("4/4", bpm=180)
score.drums("salsa", repeats=4)
render("salsa_pattern", score)
def gen_afrobeat_pattern():
score = Score("4/4", bpm=110)
score.drums("afrobeat", repeats=8)
render("afrobeat_pattern", score)
# ── Bossa nova ───────────────────────────────────────────────────────────
def gen_bossa_nova():
score = Score("4/4", bpm=140)
score.drums("bossa nova", repeats=4)
rhodes = score.part("rhodes", synth="fm", envelope="piano", volume=0.3,
reverb=0.4, reverb_decay=1.8)
for sym in ["Am", "Am", "Dm", "Dm", "E7", "E7", "Am", "Am"]:
rhodes.add(Chord.from_symbol(sym), Duration.WHOLE)
render("bossa_nova", score)
# ── Djembe ───────────────────────────────────────────────────────────────
def gen_djembe():
score = Score("4/4", bpm=110)
score.drums("tiriba", repeats=4, fill="djembe call", fill_every=4)
score.set_drum_effects(reverb=0.2)
render("djembe", score)
# ── Tabla ────────────────────────────────────────────────────────────────
def gen_tabla():
score = Score("4/4", bpm=80)
score.drums("teental", repeats=2)
score.drums("chakradar", repeats=1)
score.set_drum_effects(reverb=0.2)
render("tabla", score)
# ── Marching snare ───────────────────────────────────────────────────────
def gen_metal_blast():
score = Score("4/4", bpm=200)
score.drums("metal blast", repeats=8, fill="metal cascade", fill_every=4)
render("metal_blast", score)
def gen_cajon():
score = Score("4/4", bpm=100)
score.drums("cajon", repeats=8, fill="cajon flam", fill_every=4)
render("cajon", score)
def gen_tabla_teental():
score = Score("4/4", bpm=160)
score.drums("teental", repeats=4)
score.set_drum_effects(reverb=0.2)
render("tabla_teental", score)
def gen_tabla_keherwa():
score = Score("4/4", bpm=180)
score.drums("keherwa", repeats=4, fill="chakkardar", fill_every=4)
score.set_drum_effects(reverb=0.2)
render("tabla_keherwa", score)
def gen_tabla_chakradar():
score = Score("4/4", bpm=200)
score.drums("teental", repeats=2)
score.drums("chakradar", repeats=1)
score.set_drum_effects(reverb=0.2)
render("tabla_chakradar", score)
def gen_dhol():
score = Score("4/4", bpm=160)
score.drums("bhangra", repeats=4)
render("dhol", score)
def gen_dholak():
score = Score("4/4", bpm=120)
score.drums("qawwali", repeats=4)
render("dholak", score)
def gen_mridangam():
score = Score("4/4", bpm=90)
score.drums("adi talam", repeats=4)
render("mridangam", score)
def gen_march_snare():
score = Score("4/4", bpm=120)
p = score.part("snare", synth="sine", volume=0.8, reverb=0.15)
S = DrumSound.MARCH_SNARE
R = DrumSound.MARCH_RIMSHOT
C = DrumSound.MARCH_CLICK
for _ in range(4):
p.hit(C, Duration.QUARTER, velocity=95)
for _ in range(4):
p.hit(R, Duration.SIXTEENTH, velocity=118)
p.hit(S, Duration.SIXTEENTH, velocity=30)
p.hit(S, Duration.SIXTEENTH, velocity=32)
p.hit(S, Duration.SIXTEENTH, velocity=28)
p.hit(R, Duration.SIXTEENTH, velocity=115)
p.hit(S, Duration.SIXTEENTH, velocity=30)
p.hit(S, Duration.SIXTEENTH, velocity=28)
p.hit(S, Duration.SIXTEENTH, velocity=32)
p.hit(R, Duration.SIXTEENTH, velocity=118)
p.hit(S, Duration.SIXTEENTH, velocity=35)
p.hit(S, Duration.SIXTEENTH, velocity=28)
p.hit(S, Duration.SIXTEENTH, velocity=30)
p.hit(R, Duration.SIXTEENTH, velocity=120)
p.hit(S, Duration.SIXTEENTH, velocity=30)
p.hit(S, Duration.SIXTEENTH, velocity=28)
p.hit(S, Duration.SIXTEENTH, velocity=32)
render("march_snare", score)
# ── Ensemble comparison ──────────────────────────────────────────────────
def gen_ensemble():
score = Score("4/4", bpm=120)
S = DrumSound.MARCH_SNARE
R = DrumSound.MARCH_RIMSHOT
# Solo first
solo = score.part("solo", synth="sine", volume=0.7, reverb=0.15)
for _ in range(2):
solo.hit(R, Duration.SIXTEENTH, velocity=118)
solo.hit(S, Duration.SIXTEENTH, velocity=30)
solo.hit(S, Duration.SIXTEENTH, velocity=32)
solo.hit(S, Duration.SIXTEENTH, velocity=28)
solo.hit(R, Duration.SIXTEENTH, velocity=115)
solo.hit(S, Duration.SIXTEENTH, velocity=30)
solo.hit(S, Duration.SIXTEENTH, velocity=28)
solo.hit(S, Duration.SIXTEENTH, velocity=32)
solo.hit(R, Duration.SIXTEENTH, velocity=118)
solo.hit(S, Duration.SIXTEENTH, velocity=35)
solo.hit(S, Duration.SIXTEENTH, velocity=28)
solo.hit(S, Duration.SIXTEENTH, velocity=30)
solo.hit(R, Duration.SIXTEENTH, velocity=120)
solo.hit(S, Duration.SIXTEENTH, velocity=30)
solo.hit(S, Duration.SIXTEENTH, velocity=28)
solo.hit(S, Duration.SIXTEENTH, velocity=32)
# Then ensemble
line = score.part("line", synth="sine", volume=0.7, reverb=0.15, ensemble=8)
for _ in range(8):
line.rest(Duration.QUARTER)
for _ in range(4):
line.hit(R, Duration.SIXTEENTH, velocity=118)
line.hit(S, Duration.SIXTEENTH, velocity=30)
line.hit(S, Duration.SIXTEENTH, velocity=32)
line.hit(S, Duration.SIXTEENTH, velocity=28)
line.hit(R, Duration.SIXTEENTH, velocity=115)
line.hit(S, Duration.SIXTEENTH, velocity=30)
line.hit(S, Duration.SIXTEENTH, velocity=28)
line.hit(S, Duration.SIXTEENTH, velocity=32)
line.hit(R, Duration.SIXTEENTH, velocity=118)
line.hit(S, Duration.SIXTEENTH, velocity=35)
line.hit(S, Duration.SIXTEENTH, velocity=28)
line.hit(S, Duration.SIXTEENTH, velocity=30)
line.hit(R, Duration.SIXTEENTH, velocity=120)
line.hit(S, Duration.SIXTEENTH, velocity=30)
line.hit(S, Duration.SIXTEENTH, velocity=28)
line.hit(S, Duration.SIXTEENTH, velocity=32)
render("ensemble", score)
# ── Guitar strum ─────────────────────────────────────────────────────────
def gen_strum():
score = Score("4/4", bpm=100)
guitar = score.part("guitar", instrument="acoustic_guitar",
fretboard=Fretboard.guitar())
for ch in ["G", "D", "Em", "C"] * 2:
guitar.strum(ch, duration=Duration.WHOLE, velocity=75)
render("strum", score)
# ── Swell ────────────────────────────────────────────────────────────────
def gen_swell():
score = Score("4/4", bpm=80)
strings = score.part("strings", instrument="string_ensemble",
reverb=0.4, ensemble=12)
strings.swell(["C4", "D4", "E4", "F4", "G4", "F4", "E4", "D4",
"C4", "D4", "E4", "F4", "G4", "A4", "G4", "E4"],
Duration.QUARTER, low_vel=30, peak_vel=100)
render("swell", score)
# ── Generate all ─────────────────────────────────────────────────────────
# ── Cookbook: Acid House ───────────────────────────────────────────────────
def gen_acid_house():
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)
render("acid_house", score)
# ── Cookbook: Dub Reggae ──────────────────────────────────────────────────
def gen_dub_reggae():
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)
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 _ in range(16):
bass.add("A1", Duration.HALF)
render("dub_reggae", score)
# ── Cookbook: Jazz Ballad ─────────────────────────────────────────────────
def gen_jazz_ballad():
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)
render("jazz_ballad", score)
# ── Quickstart example ───────────────────────────────────────────────────
def gen_arpeggio():
score = Score("4/4", bpm=132)
score.drums("house", repeats=8)
score.set_drum_effects(volume=0.3)
lead = score.part("lead", synth="saw", envelope="pluck", volume=0.5,
lowpass=5000, detune=8, chorus=0.15, reverb=0.25,
delay=0.2, delay_time=0.33, delay_feedback=0.3)
for sym in ["Cm", "Fm", "Abm", "Gm"]:
lead.arpeggio(sym, bars=2, pattern="updown", octaves=2)
render("arpeggio", score)
def gen_legato_glide():
score = Score("4/4", bpm=132)
score.drums("house", repeats=4)
score.set_drum_effects(volume=0.35)
acid = score.part("acid", synth="saw", volume=0.6,
legato=True, glide=0.04,
lowpass=3000, lowpass_q=6.0,
distortion=0.3, distortion_drive=3.0)
acid.lfo("lowpass", rate=0.5, min=800, max=4000, bars=4)
for _ in range(4):
acid.add("C2", 0.25).add("C3", 0.25).add("G2", 0.25).add("C2", 0.25)
acid.add("C2", 0.25).add("Eb2", 0.25).add("G2", 0.25).add("Bb2", 0.25)
render("legato_glide", score)
def gen_quickstart():
score = Score("4/4", bpm=140)
score.drums("bossa nova", repeats=4)
chords = score.part("chords", synth="sine", envelope="pad",
reverb=0.4, volume=0.3)
lead = score.part("lead", synth="saw", envelope="pluck",
lowpass=2000, lowpass_q=3.0, distortion=0.8,
legato=True, glide=0.03, volume=0.4)
bass = score.part("bass", synth="sine", lowpass=500)
key = Key("A", "minor")
for chord in key.progression("i", "iv", "V", "i"):
chords.add(chord, Duration.WHOLE)
chords.add(chord, Duration.WHOLE)
lead.arpeggio("Am", bars=2, pattern="updown", octaves=2)
lead.arpeggio("Dm", bars=2, pattern="updown", octaves=2)
lead.set(lowpass=5000, reverb=0.3)
lead.arpeggio("E7", bars=2, pattern="up", octaves=2)
lead.arpeggio("Am", bars=2, pattern="updown", octaves=2)
for n in ["A2", "E2", "A2", "C3"] * 4:
bass.add(n, Duration.QUARTER)
render("quickstart", score)
# ── Sequencing complete example (bossa nova) ─────────────────────────────
def gen_chords_basic():
score = Score("4/4", bpm=120)
key = Key("C", "major")
chords = key.progression("I", "V", "vi", "IV")
for chord in chords:
score.add(chord, Duration.WHOLE)
render("chords_basic", score)
def gen_complete_rock():
score = Score("4/4", bpm=120)
score.drums("rock", repeats=8, fill="rock", fill_every=4)
piano = score.part("piano", instrument="piano", volume=0.4, reverb=0.3)
lead = score.part("lead", synth="saw", envelope="pluck", volume=0.4,
delay=0.2, delay_time=0.33, reverb=0.2, lowpass=3000)
bass = score.part("bass", synth="triangle", envelope="pluck", volume=0.45,
lowpass=600)
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)
render("complete_rock", score)
# ── Drums layering (salsa) ───────────────────────────────────────────────
def gen_salsa_layered():
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)
render("salsa_layered", score)
# ── Playback basic ───────────────────────────────────────────────────────
def gen_playback_basic():
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)
render("playback_basic", score)
# ── Cookbook: song with sections ──────────────────────────────────────────
def gen_song_sections():
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)
render("song_sections", score)
GENERATORS = [
gen_piano_hold,
gen_articulations,
gen_dynamics,
gen_filter_ramp,
gen_rock_beat,
gen_bossa_nova_pattern,
gen_salsa_pattern,
gen_afrobeat_pattern,
gen_bossa_nova,
gen_djembe,
gen_tabla,
gen_metal_blast,
gen_cajon,
gen_tabla_teental,
gen_tabla_keherwa,
gen_tabla_chakradar,
gen_dhol,
gen_dholak,
gen_mridangam,
gen_march_snare,
gen_ensemble,
gen_strum,
gen_swell,
gen_arpeggio,
gen_legato_glide,
gen_acid_house,
gen_dub_reggae,
gen_jazz_ballad,
gen_quickstart,
gen_chords_basic,
gen_complete_rock,
gen_salsa_layered,
gen_playback_basic,
gen_song_sections,
]
if __name__ == "__main__":
print("Generating audio samples for docs...")
print()
for gen in GENERATORS:
gen()
print()
print(f"Done. {len(GENERATORS)} files in {AUDIO_DIR}")
+16
View File
@@ -411,6 +411,10 @@ Acid House Track
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
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
@@ -443,6 +447,10 @@ Sparse notes into infinite echo:
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
~~~~~~~~~~~~~~~~~~~~~~~~~~
@@ -480,6 +488,10 @@ The difference between a robot and a musician:
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
~~~~~~~~~~~~~~~~~~~
@@ -513,6 +525,10 @@ Define once, arrange freely:
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
~~~~~~~~~~~~~~~~~~~~~~~~~~
+45
View File
@@ -249,6 +249,13 @@ Playing Patterns
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
-----
@@ -339,6 +346,10 @@ drum pattern and all named parts are mixed together by ``play_score()``:
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
----------------
@@ -382,6 +393,12 @@ bayan (deep bass bends showcase), tabla call (dayan/bayan call-and-response).
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
~~~~
@@ -399,6 +416,10 @@ energetic, and physically impossible to sit still to.
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
~~~~~~
@@ -416,6 +437,10 @@ music) and dholak folk (a general folk groove).
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
~~~~~~~~~
@@ -434,6 +459,10 @@ and mridangam korvai (a rhythmic cadence pattern).
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
~~~~~~
@@ -458,6 +487,10 @@ West African-style break).
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
~~~~~~~~~
@@ -482,6 +515,10 @@ roll → kick roll → alternating → crash ending).
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
~~~~~
@@ -504,6 +541,10 @@ bass-slap groove).
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
~~~~~~~~~~~~~~~~~~~
@@ -548,6 +589,10 @@ voice with per-player timing tendencies and micro pitch drift.
# 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.
+4
View File
@@ -66,6 +66,10 @@ the mix louder and punchier:
chords.add(Chord.from_symbol(sym), Duration.WHOLE)
play_score(score)
.. raw:: html
<audio controls style="width:100%;margin:0.5em 0 1.5em"><source src="../_static/audio/playback_basic.wav" type="audio/wav"></audio>
The render pipeline respects the Score's ``temperament`` and
``reference_pitch`` settings, so Baroque or microtonal scores play back
at the correct tuning:
+4
View File
@@ -185,6 +185,10 @@ chords, melody, bass, each with their own synth and effects:
play_score(score)
.. raw:: html
<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
------------------
+86 -41
View File
@@ -161,6 +161,10 @@ Chords work just like tones — pass any ``Chord`` object:
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
@@ -244,6 +248,31 @@ Chords and Tone objects work the same way:
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
------------
@@ -292,6 +321,10 @@ Chain arpeggios through a progression:
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.
@@ -322,12 +355,18 @@ portamento (pitch slides between notes):
acid = score.part(
"acid",
synth="saw",
envelope="pad",
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.
@@ -335,63 +374,51 @@ portamento (pitch slides between notes):
Complete Example
----------------
A full multi-part arrangement built from scratch — bossa nova with FM
rhodes, triangle lead, and filtered bass:
A full multi-part arrangement — rock beat with piano chords, saw
lead, and filtered bass:
.. code-block:: python
from pytheory import Score, Pattern, Key, Duration, Chord
from pytheory import Score, Key, Duration, Chord
from pytheory.play import play_score
score = Score("4/4", bpm=140)
score.drums("bossa nova", repeats=4)
score = Score("4/4", bpm=120)
score.drums("rock", repeats=8, fill="rock", fill_every=4)
# FM rhodes with reverb
rhodes = score.part(
"rhodes",
synth="fm",
envelope="piano",
volume=0.3,
reverb=0.4,
reverb_decay=1.8,
)
# Piano chords with reverb
piano = score.part("piano", instrument="piano", volume=0.4, reverb=0.3)
# Triangle lead with delay
# Saw lead with delay
lead = score.part(
"lead",
synth="triangle",
envelope="pluck",
volume=0.45,
delay=0.25,
delay_time=0.32,
delay_feedback=0.35,
reverb=0.2,
"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="sine",
envelope="pluck",
volume=0.45,
lowpass=600,
)
bass = score.part("bass", synth="triangle", envelope="pluck",
volume=0.45, lowpass=600)
for sym in ["Am", "Am", "Dm", "Dm", "E7", "E7", "Am", "Am"]:
rhodes.add(Chord.from_symbol(sym), Duration.WHOLE)
for chord in Key("G", "major").progression("I", "V", "vi", "IV") * 2:
piano.add(chord, Duration.WHOLE)
for n, d in [
("E5", 0.67), ("D5", 0.33), ("C5", 0.67), ("B4", 0.33),
("A4", 1), ("C5", 0.67), ("E5", 0.33), ("D5", 0.67), ("C5", 0.33),
("A4", 1),
]:
lead.add(n, d)
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 ["A2", "E2", "A2", "C3", "D2", "A2", "D2", "F2"]:
bass.add(n, Duration.QUARTER)
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
--------
@@ -431,6 +458,10 @@ Pass ``articulation=`` to ``Part.add()``:
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.
@@ -467,6 +498,10 @@ of notes instead of setting each one manually.
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``.
@@ -548,6 +583,12 @@ 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
----------------
@@ -663,6 +704,10 @@ Four interpolation curves:
# 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).
+4 -1
View File
@@ -62,7 +62,10 @@ it through your speakers, export MIDI, finish in your DAW:
lead.arpeggio("Am", bars=4, pattern="updown", octaves=2)
play_score(score)
score.save_midi("sketch.mid")
.. raw:: html
<audio controls style="width:100%;margin:0.5em 0 1.5em"><source src="_static/audio/quickstart.wav" type="audio/wav"></audio>
Or hear a randomly generated track from the command line — different
every time::
+206 -10
View File
@@ -543,7 +543,7 @@ def dub_kingston():
volume=0.6, pan=0.0, lowpass=400, lowpass_q=1.5,
humanize=0.2)
siren = score.part("siren", synth="pwm_slow", envelope="pad",
volume=0.15, pan=0.5,
volume=0.15, pan=0.5, ensemble=4,
reverb=0.7, reverb_decay=3.0, reverb_type="cathedral",
lowpass=1200, detune=10)
@@ -1124,14 +1124,14 @@ def cinematic_showcase():
bells.rest(Duration.WHOLE)
# String ensemble — lush wide pad
strings = score.part("strings", instrument="string_ensemble",
strings = score.part("strings", instrument="string_ensemble", ensemble=8,
reverb=0.4, reverb_type="hall")
strings.rest(Duration.WHOLE)
for sym in ["Am", "F", "C", "G", "Dm", "Am", "E"]:
strings.add(Chord.from_symbol(sym), Duration.WHOLE)
# Cello — deep foundation
cello = score.part("cello", instrument="cello",
cello = score.part("cello", instrument="cello", ensemble=3,
reverb=0.3, reverb_type="hall")
cello.rest(Duration.WHOLE)
for n in ["A2", "F2", "C3", "G2", "D3", "A2", "E2"]:
@@ -1629,7 +1629,7 @@ def epic_bhairav():
timp.add(Tone("Pa", octave=2, system=shruti), Duration.HALF, velocity=115)
# Choir — bar 3
choir = score.part("choir", synth="vocal_synth", envelope="pad",
choir = score.part("choir", synth="vocal_synth", envelope="pad", ensemble=6,
detune=8, spread=0.4, reverb=0.4, reverb_type=REV, volume=0.2)
for _ in range(2):
choir.rest(Duration.WHOLE)
@@ -1651,7 +1651,7 @@ def epic_bhairav():
bansuri.add(tone, dur, velocity=vel)
# Cello — bar 3
cello = score.part("cello", instrument="cello", volume=0.22, reverb=0.4, reverb_type=REV)
cello = score.part("cello", instrument="cello", volume=0.22, reverb=0.4, reverb_type=REV, ensemble=3)
for _ in range(2):
cello.rest(Duration.WHOLE)
for name, dur, vel in [
@@ -1677,7 +1677,7 @@ def epic_bhairav():
# Strings — bar 13
strings = score.part("strings", instrument="string_ensemble", volume=0.18,
reverb=0.4, reverb_type=REV)
reverb=0.4, reverb_type=REV, ensemble=10)
for _ in range(12):
strings.rest(Duration.WHOLE)
for name, dur, vel in [("Sa", 4.0, 58), ("Ma", 4.0, 62), ("Pa", 4.0, 68), ("Sa", 4.0, 72)]:
@@ -1887,7 +1887,7 @@ def ascent():
# 3: SURFACING (5-8)
cello = score.part("cello", instrument="cello", volume=0.22,
reverb=0.4, reverb_type=REV)
reverb=0.4, reverb_type=REV, ensemble=3)
cello.rest(16.0)
for note, dur, vel in [("E2",4.0,52),("G2",4.0,55),("B2",4.0,58),("E3",4.0,62)]:
cello.add(note, dur, velocity=vel)
@@ -1943,7 +1943,7 @@ def ascent():
theremin.add(note, dur, velocity=vel)
strings = score.part("strings", instrument="string_ensemble", volume=0.15,
reverb=0.45, reverb_type=REV)
reverb=0.45, reverb_type=REV, ensemble=8)
strings.rest(40.0)
for sym, vel in [("Em",52),("C",55),("Am",58),("B",55),("Em",60),("C",62)]:
strings.add(Chord.from_symbol(sym), 4.0, velocity=vel)
@@ -2218,7 +2218,7 @@ def pop_rock():
cabinet=1.0, cabinet_brightness=0.6,
reverb=0.2, reverb_type="plate", pan=0.2)
strings = score.part("strings", instrument="string_ensemble", volume=0.12,
reverb=0.35, reverb_type="hall")
reverb=0.35, reverb_type="hall", ensemble=6)
prog = ["G", "D", "Em", "C"]
@@ -2804,6 +2804,201 @@ def snare_cadence():
play_song(score, "Snare Cadence — full drumline (8 snares, 4 quads, 5 basses)")
def ensemble_showcase():
"""Ensemble Showcase — acid bass, tabla solo, strings, snare line."""
score = Score("4/4", bpm=128)
# ── Drums: house kit ──
score.drums("house", repeats=24, fill="house", fill_every=8)
score.set_drum_effects(volume=0.4, reverb=0.1)
# ── 303 Acid Bass: detuned, spread, LFO filter, ensemble=3 ──
acid = score.part("acid", synth="saw", volume=0.55, ensemble=3,
lowpass=400, lowpass_q=10.0, distortion=0.35,
distortion_drive=4.0, legato=True, glide=0.03,
detune=12, spread=0.4, sub_osc=0.15,
sidechain=0.5, sidechain_release=0.08)
acid.lfo("lowpass", rate=0.5, min=400, max=5000, bars=16, shape="sine")
for _ in range(8):
for n in ["C2", "C3", "C2", "Eb2", "C2", "G2", "Bb2", "C2"]:
acid.add(n, Duration.EIGHTH, velocity=90)
acid.ramp(over=Duration.WHOLE * 8, curve="ease_in", lowpass=6000)
for _ in range(8):
for n in ["C2", "Eb3", "C3", "G2", "Bb2", "C3", "G2", "C2"]:
acid.add(n, Duration.EIGHTH, velocity=95)
acid.ramp(over=Duration.WHOLE * 8, curve="ease_out", lowpass=500)
for _ in range(8):
for n in ["C2", "C3", "C2", "Eb2", "C2", "G2", "Bb2", "C2"]:
acid.add(n, Duration.EIGHTH, velocity=88)
# ── Strings: 16-player ensemble pad ──
strings = score.part("strings", instrument="string_ensemble", volume=0.0,
reverb=0.4, ensemble=16, detune=8, spread=0.5)
for _ in range(32):
strings.rest(Duration.QUARTER)
strings.ramp(over=Duration.WHOLE * 4, curve="ease_in", volume=0.18)
for ch in ["Cm", "Ab", "Eb", "Bb"] * 4:
strings.add(Chord.from_symbol(ch), Duration.WHOLE, velocity=55)
strings.ramp(over=Duration.WHOLE * 4, curve="ease_out", volume=0.0)
for ch in ["Cm", "Ab", "Eb", "Bb"]:
strings.add(Chord.from_symbol(ch), Duration.WHOLE, velocity=45)
for _ in range(16):
strings.rest(Duration.QUARTER)
# ── Tabla: ensemble=3, enters bar 9 ──
tabla = score.part("tabla", synth="sine", volume=0.0, reverb=0.15, ensemble=3)
for _ in range(32):
tabla.rest(Duration.QUARTER)
tabla.ramp(over=Duration.WHOLE * 2, volume=0.45)
# Keherwa groove — 8 bars
for _ in range(8):
tabla.hit(DrumSound.TABLA_DHA, Duration.EIGHTH, velocity=95, articulation="accent")
tabla.hit(DrumSound.TABLA_TIT, Duration.EIGHTH, velocity=50)
tabla.hit(DrumSound.TABLA_TIT, Duration.EIGHTH, velocity=48)
tabla.hit(DrumSound.TABLA_NA, Duration.EIGHTH, velocity=82)
tabla.hit(DrumSound.TABLA_TIN, Duration.EIGHTH, velocity=78)
tabla.hit(DrumSound.TABLA_TIT, Duration.EIGHTH, velocity=48)
tabla.hit(DrumSound.TABLA_DHA, Duration.EIGHTH, velocity=88, articulation="accent")
tabla.hit(DrumSound.TABLA_GE, Duration.EIGHTH, velocity=72)
# Tabla solo — getting busier
tabla.hit(DrumSound.TABLA_DHA, Duration.EIGHTH, velocity=100, articulation="marcato")
tabla.hit(DrumSound.TABLA_TIT, Duration.SIXTEENTH, velocity=45)
tabla.hit(DrumSound.TABLA_TIT, Duration.SIXTEENTH, velocity=48)
tabla.hit(DrumSound.TABLA_NA, Duration.EIGHTH, velocity=85)
tabla.hit(DrumSound.TABLA_TIN, Duration.EIGHTH, velocity=80)
tabla.hit(DrumSound.TABLA_TIT, Duration.SIXTEENTH, velocity=45)
tabla.hit(DrumSound.TABLA_NA, Duration.SIXTEENTH, velocity=55)
tabla.hit(DrumSound.TABLA_DHA, Duration.EIGHTH, velocity=95, articulation="accent")
tabla.hit(DrumSound.TABLA_GE_BEND, Duration.EIGHTH, velocity=82)
tabla.hit(DrumSound.TABLA_DHA, Duration.EIGHTH, velocity=105, articulation="marcato")
tabla.hit(DrumSound.TABLA_TIT, Duration.SIXTEENTH, velocity=48)
tabla.hit(DrumSound.TABLA_NA, Duration.SIXTEENTH, velocity=55)
tabla.hit(DrumSound.TABLA_TIT, Duration.SIXTEENTH, velocity=45)
tabla.hit(DrumSound.TABLA_TIT, Duration.SIXTEENTH, velocity=48)
tabla.hit(DrumSound.TABLA_NA, Duration.EIGHTH, velocity=88)
tabla.hit(DrumSound.TABLA_DHA, Duration.SIXTEENTH, velocity=100)
tabla.hit(DrumSound.TABLA_TIT, Duration.SIXTEENTH, velocity=50)
tabla.hit(DrumSound.TABLA_GE_BEND, Duration.EIGHTH, velocity=85)
tabla.hit(DrumSound.TABLA_DHA, Duration.EIGHTH, velocity=108, articulation="accent")
# Tihai crescendo
for vel in [85, 90, 95, 100, 105, 110]:
tabla.hit(DrumSound.TABLA_DHA, Duration.EIGHTH, velocity=vel, articulation="accent")
tabla.hit(DrumSound.TABLA_TIT, Duration.SIXTEENTH, velocity=int(vel * 0.55))
tabla.hit(DrumSound.TABLA_NA, Duration.SIXTEENTH, velocity=int(vel * 0.7))
tabla.hit(DrumSound.TABLA_DHA, Duration.QUARTER, velocity=125, articulation="fermata")
tabla.hit(DrumSound.TABLA_GE_BEND, Duration.QUARTER, velocity=110)
# Groove out
for _ in range(4):
tabla.hit(DrumSound.TABLA_DHA, Duration.EIGHTH, velocity=88, articulation="accent")
tabla.hit(DrumSound.TABLA_TIT, Duration.EIGHTH, velocity=45)
tabla.hit(DrumSound.TABLA_TIT, Duration.EIGHTH, velocity=42)
tabla.hit(DrumSound.TABLA_NA, Duration.EIGHTH, velocity=75)
tabla.hit(DrumSound.TABLA_TIN, Duration.EIGHTH, velocity=70)
tabla.hit(DrumSound.TABLA_TIT, Duration.EIGHTH, velocity=42)
tabla.hit(DrumSound.TABLA_DHA, Duration.EIGHTH, velocity=80)
tabla.hit(DrumSound.TABLA_GE, Duration.EIGHTH, velocity=65)
# ── Snare line: 8-player ensemble, enters bar 17 ──
S = DrumSound.MARCH_SNARE
R = DrumSound.MARCH_RIMSHOT
snares = score.part("snares", synth="sine", volume=0.0, reverb=0.15, ensemble=8)
for _ in range(64):
snares.rest(Duration.QUARTER)
snares.ramp(over=Duration.WHOLE * 2, volume=0.7)
for _ in range(4):
snares.hit(R, Duration.SIXTEENTH, velocity=118)
snares.hit(S, Duration.SIXTEENTH, velocity=30)
snares.hit(S, Duration.SIXTEENTH, velocity=32)
snares.hit(S, Duration.SIXTEENTH, velocity=28)
snares.hit(R, Duration.SIXTEENTH, velocity=115)
snares.hit(S, Duration.SIXTEENTH, velocity=30)
snares.hit(S, Duration.SIXTEENTH, velocity=28)
snares.hit(S, Duration.SIXTEENTH, velocity=32)
snares.hit(R, Duration.SIXTEENTH, velocity=118)
snares.hit(S, Duration.SIXTEENTH, velocity=35)
snares.hit(S, Duration.SIXTEENTH, velocity=28)
snares.hit(S, Duration.SIXTEENTH, velocity=30)
snares.hit(R, Duration.SIXTEENTH, velocity=120)
snares.hit(S, Duration.SIXTEENTH, velocity=30)
snares.hit(S, Duration.SIXTEENTH, velocity=28)
snares.hit(S, Duration.SIXTEENTH, velocity=32)
# Buzz roll finale
for i in range(64):
snares.hit(S, 0.0625, velocity=min(20 + i * 1.5, 100))
snares.hit(R, Duration.EIGHTH, velocity=127)
snares.hit(R, Duration.EIGHTH, velocity=127)
snares.ramp(over=Duration.WHOLE * 2, curve="ease_out", volume=0.0)
for _ in range(2):
snares.hit(R, Duration.SIXTEENTH, velocity=110)
snares.hit(S, Duration.SIXTEENTH, velocity=30)
snares.hit(S, Duration.SIXTEENTH, velocity=28)
snares.hit(S, Duration.SIXTEENTH, velocity=32)
snares.hit(R, Duration.SIXTEENTH, velocity=108)
snares.hit(S, Duration.SIXTEENTH, velocity=30)
snares.hit(S, Duration.SIXTEENTH, velocity=28)
snares.hit(S, Duration.SIXTEENTH, velocity=32)
snares.hit(R, Duration.SIXTEENTH, velocity=105)
snares.hit(S, Duration.SIXTEENTH, velocity=30)
snares.hit(S, Duration.SIXTEENTH, velocity=28)
snares.hit(S, Duration.SIXTEENTH, velocity=32)
snares.hit(R, Duration.SIXTEENTH, velocity=100)
snares.hit(S, Duration.SIXTEENTH, velocity=30)
snares.hit(S, Duration.SIXTEENTH, velocity=28)
snares.hit(S, Duration.SIXTEENTH, velocity=32)
# ── Lead synth: 6-player ensemble, enters bar 5 ──
lead = score.part("lead", synth="saw", envelope="pluck", volume=0.0,
lowpass=3500, detune=6, chorus=0.1, reverb=0.2,
delay=0.15, delay_time=0.33, delay_feedback=0.25,
ensemble=6)
for _ in range(16):
lead.rest(Duration.QUARTER)
lead.ramp(over=Duration.WHOLE * 2, volume=0.3)
for _ in range(2):
lead.add("Eb5", Duration.QUARTER, velocity=88)
lead.add("G5", Duration.QUARTER, velocity=92)
lead.add("Bb5", Duration.HALF, velocity=95, articulation="accent")
lead.add("Ab5", Duration.QUARTER, velocity=88)
lead.add("G5", Duration.QUARTER, velocity=85)
lead.add("Eb5", Duration.QUARTER, velocity=82)
lead.add("D5", Duration.QUARTER, velocity=80)
lead.swell(["Eb5", "G5", "Bb5", "C6", "Bb5", "G5", "Eb5", "D5"],
Duration.QUARTER, low_vel=75, peak_vel=105)
lead.decrescendo(["Eb5", "D5", "C5", "Bb4"], Duration.HALF,
start_vel=90, end_vel=60)
for _ in range(16):
lead.rest(Duration.QUARTER)
lead.ramp(over=Duration.WHOLE * 2, volume=0.35)
lead.crescendo(["C5", "Eb5", "G5", "Bb5", "C6", "Eb6", "C6", "Bb5"],
Duration.QUARTER, start_vel=80, end_vel=110)
lead.add("G5", Duration.HALF, velocity=105, bend=1, bend_type="smooth")
lead.add("Eb5", Duration.HALF, velocity=95)
lead.decrescendo(["C5", "Bb4", "G4", "Eb4"], Duration.WHOLE,
start_vel=85, end_vel=40)
play_song(score, "Ensemble Showcase — acid bass, tabla solo, 16-player strings, 8-player snare line")
SONGS = {
"1": ("Bossa Nova in A minor", bossa_nova_girl),
"2": ("Bebop in Bb major", bebop_in_bb),
@@ -2837,6 +3032,7 @@ SONGS = {
"30": ("Sitar Drone (Bhairav, hold() polyphony)", sitar_drone),
"31": ("Acid Tabla (303 + tabla, ramp, articulations)", acid_tabla),
"32": ("Snare Cadence (marching snare, flams, diddles)", snare_cadence),
"33": ("Ensemble Showcase (acid+tabla+strings+snare line)", ensemble_showcase),
}
if __name__ == "__main__":
@@ -2850,7 +3046,7 @@ if __name__ == "__main__":
print(f" {key:>2}. {name}")
print()
choice = input(" Pick a song (1-32, or 'all'): ").strip()
choice = input(" Pick a song (1-33, or 'all'): ").strip()
print()
if choice == "all":
+1 -1
View File
@@ -1,6 +1,6 @@
[project]
name = "pytheory"
version = "0.39.2"
version = "0.39.3"
description = "Music Theory for Humans"
readme = "README.md"
license = "MIT"
+1 -1
View File
@@ -1,6 +1,6 @@
"""PyTheory: Music Theory for Humans."""
__version__ = "0.39.2"
__version__ = "0.39.3"
from .tones import Tone, Interval
from .systems import System, SYSTEMS, TET
+72 -28
View File
@@ -358,27 +358,42 @@ def piano_wave(hz, peak=SAMPLE_PEAK, n_samples=SAMPLE_RATE):
# Harmonics with the metallic spectral shape of steel strings
n_harmonics = min(15, int((SAMPLE_RATE / 2) / hz))
# Vectorized harmonic synthesis — all harmonics at once
harmonics = numpy.arange(1, n_harmonics + 1, dtype=numpy.float64)
# Piano spectral shape as array
amps = numpy.zeros(n_harmonics, dtype=numpy.float64)
amps[0] = 1.0
if n_harmonics > 1:
amps[1] = 0.7 + 0.15 * brightness
if n_harmonics > 2:
amps[2] = 0.45 + 0.2 * brightness
for i in range(3, min(6, n_harmonics)):
amps[i] = (0.25 + 0.15 * brightness) / (i + 1)
for i in range(6, n_harmonics):
amps[i] = (0.1 + 0.1 * brightness) / ((i + 1) ** 2)
# Per-harmonic decay rates
h_decay_rates = (1.5 - 0.5 * brightness) * (harmonics - 1)
# Random phases
phases = rng.uniform(0, 2 * numpy.pi, n_harmonics)
for string_hz in [hz, hz2]:
for n in range(1, n_harmonics + 1):
f_n = string_hz * n
if f_n >= SAMPLE_RATE / 2:
break
# Piano spectral shape: strong 1-3, then falling
# Upper register has more prominent harmonics (brighter)
if n == 1:
amp = 1.0
elif n == 2:
amp = 0.7 + 0.15 * brightness
elif n == 3:
amp = 0.45 + 0.2 * brightness
elif n <= 6:
amp = (0.25 + 0.15 * brightness) / n
else:
amp = (0.1 + 0.1 * brightness) / (n * n)
# Higher harmonics decay faster, but less so in upper register
h_decay = decay * numpy.exp(-(1.5 - 0.5 * brightness) * (n - 1) * t)
phase = rng.uniform(0, 2 * numpy.pi)
wave += amp * numpy.sin(2 * numpy.pi * f_n * t + phase) * h_decay
freqs = string_hz * harmonics # (n_harmonics,)
# Mask out harmonics above Nyquist
valid = freqs < SAMPLE_RATE / 2
if not valid.any():
continue
v_freqs = freqs[valid]
v_amps = amps[valid]
v_rates = h_decay_rates[valid]
v_phases = phases[valid]
# 2D: (n_valid, n_samples) — one sin() call for all harmonics
phase_matrix = 2 * numpy.pi * v_freqs[:, numpy.newaxis] * t[numpy.newaxis, :] + v_phases[:, numpy.newaxis]
decay_matrix = decay[numpy.newaxis, :] * numpy.exp(-v_rates[:, numpy.newaxis] * t[numpy.newaxis, :])
wave += (v_amps[:, numpy.newaxis] * numpy.sin(phase_matrix) * decay_matrix).sum(axis=0)
wave *= 0.5
@@ -1995,16 +2010,32 @@ def _noise(n_samples):
return numpy.random.uniform(-1.0, 1.0, n_samples).astype(numpy.float32)
# ── Cached helpers for hot paths ──────────────────────────────────────────
_time_cache = {}
def _get_time_array(n_samples):
"""Cached time array — avoids reallocation on every synth call."""
if n_samples not in _time_cache:
_time_cache[n_samples] = numpy.arange(n_samples, dtype=numpy.float32) / SAMPLE_RATE
return _time_cache[n_samples]
def _sine_f32(hz, n_samples):
"""Float32 sine wave, normalized to ±1."""
t = numpy.arange(n_samples, dtype=numpy.float32) / SAMPLE_RATE
return numpy.sin(2 * numpy.pi * hz * t)
return numpy.sin(2 * numpy.pi * hz * _get_time_array(n_samples))
_decay_cache = {}
def _exp_decay(n_samples, decay_rate):
"""Exponential decay envelope from 1→0."""
t = numpy.arange(n_samples, dtype=numpy.float32) / SAMPLE_RATE
return numpy.exp(-decay_rate * t)
"""Exponential decay envelope from 1→0. Cached."""
key = (n_samples, decay_rate)
if key not in _decay_cache:
_decay_cache[key] = numpy.exp(-decay_rate * _get_time_array(n_samples))
return _decay_cache[key]
def _synth_kick(n_samples):
@@ -3176,7 +3207,20 @@ def _render_drum_hit(sound_value, n_samples):
}
renderer = _dispatch.get(sound_value, lambda n: _synth_clave(n))
return renderer(n_samples)
result = renderer(n_samples)
return result
# Drum hit cache — same sound at same length sounds identical
_drum_cache = {}
def _render_drum_hit_cached(sound_value, n_samples):
"""Cached version of _render_drum_hit for pattern playback."""
key = (sound_value, n_samples)
if key not in _drum_cache:
_drum_cache[key] = _render_drum_hit(sound_value, n_samples)
return _drum_cache[key].copy() # copy so callers can mutate
def _render_pattern(pattern, bpm=120):
@@ -3200,7 +3244,7 @@ def _render_pattern(pattern, bpm=120):
remaining = total_samples - start
# Render each hit for up to 0.5 seconds
hit_len = min(int(SAMPLE_RATE * 0.5), remaining)
wave = _render_drum_hit(hit.sound.value, hit_len)
wave = _render_drum_hit_cached(hit.sound.value, hit_len)
vel_scale = hit.velocity / 127.0
buf[start:start + hit_len] += wave * vel_scale
@@ -4949,7 +4993,7 @@ def render_score(score):
remaining = total_samples - start
hit_len = min(int(SAMPLE_RATE * 0.5), remaining)
wave = _render_drum_hit(hit.sound.value, hit_len)
wave = _render_drum_hit_cached(hit.sound.value, hit_len)
vel = hit.velocity
if drum_humanize > 0:
vel_jitter = int(drum_humanize * 10)
Generated
+1 -1
View File
@@ -690,7 +690,7 @@ wheels = [
[[package]]
name = "pytheory"
version = "0.39.2"
version = "0.39.3"
source = { editable = "." }
dependencies = [
{ name = "scipy", version = "1.15.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" },