Compare commits

...

36 Commits

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-29 11:15:27 -04:00
85 changed files with 979 additions and 219 deletions
+12
View File
@@ -2,6 +2,18 @@
All notable changes to PyTheory are documented here.
## 0.40.0
- **Rhodes electric piano synth** — tine + tonebar + electromagnetic
pickup model. `electric_piano` preset now uses dedicated `rhodes_synth`
instead of FM
- **73 audio demos in docs** — every synth, every drum pattern, every
code example with `play_score()` now has an embedded audio player
- Idiomatic demos: harp arpeggiates, guitars strum, cello bows, sitar
drones, strings use ensemble
- Trailing silence trimming on all audio exports
- Raw waveform demos (no envelope) for classic waveforms
## 0.39.3
- **33 audio samples in documentation** — every `play_score()` example
Binary file not shown.
Binary file not shown.
BIN
View File
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
BIN
View File
Binary file not shown.
Binary file not shown.
Binary file not shown.
BIN
View File
Binary file not shown.
BIN
View File
Binary file not shown.
BIN
View File
Binary file not shown.
Binary file not shown.
BIN
View File
Binary file not shown.
BIN
View File
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
BIN
View File
Binary file not shown.
BIN
View File
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
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.
BIN
View File
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
+520 -49
View File
@@ -23,7 +23,20 @@ os.makedirs(AUDIO_DIR, exist_ok=True)
def save_wav(buf, path):
"""Save a float32 buffer as 16-bit stereo WAV."""
"""Save a float32 buffer as 16-bit stereo WAV, trimming trailing silence."""
# Trim trailing silence (below -60dB threshold)
threshold = 0.001
if buf.ndim == 2:
amplitude = numpy.abs(buf).max(axis=1)
else:
amplitude = numpy.abs(buf)
# Find last sample above threshold
above = numpy.where(amplitude > threshold)[0]
if len(above) > 0:
# Keep 0.2s of tail after last audible sample for natural decay
tail = min(int(SAMPLE_RATE * 0.2), len(buf) - above[-1])
buf = buf[:above[-1] + tail]
# Handle both mono (n,) and stereo (n, 2) buffers
if buf.ndim == 1:
channels = 1
@@ -172,43 +185,74 @@ 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)
score.set_drum_effects(reverb=0.3, reverb_type="hall")
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)
score = Score("4/4", bpm=190)
# Showcase all metal patterns: groove → gallop → triplet fill → blast
score.drums("metal groove", repeats=2)
score.drums("metal gallop", repeats=4, fill="metal triplet", fill_every=4)
score.drums("metal blast", repeats=2, fill="metal cascade", fill_every=2)
score.drums("double kick", repeats=2, fill="metal blast", fill_every=2)
render("metal_blast", score)
def gen_cajon():
score = Score("4/4", bpm=100)
score.drums("cajon", repeats=8, fill="cajon flam", fill_every=4)
score.set_drum_effects(reverb=0.25, reverb_type="room")
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)
score.drums("teental", repeats=3)
score.drums("teental", repeats=1, fill="bayan", fill_every=1)
score.set_drum_effects(reverb=0.3, reverb_type="hall")
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)
# Manual part so we can add ge_bend hits
tabla = score.part("tabla", synth="sine", volume=0.5, reverb=0.3, reverb_type="hall")
DHA = DrumSound.TABLA_DHA
NA = DrumSound.TABLA_NA
TIN = DrumSound.TABLA_TIN
TIT = DrumSound.TABLA_TIT
GE = DrumSound.TABLA_GE
GB = DrumSound.TABLA_GE_BEND
# Keherwa with ge_bend accents
for _ in range(3):
tabla.hit(DHA, Duration.EIGHTH, velocity=90, articulation="accent")
tabla.hit(GE, Duration.EIGHTH, velocity=65)
tabla.hit(NA, Duration.EIGHTH, velocity=72)
tabla.hit(TIT, Duration.EIGHTH, velocity=45)
tabla.hit(NA, Duration.EIGHTH, velocity=68)
tabla.hit(TIT, Duration.EIGHTH, velocity=42)
tabla.hit(DHA, Duration.EIGHTH, velocity=85, articulation="accent")
tabla.hit(NA, Duration.EIGHTH, velocity=70)
# Last bar with bayan bends
tabla.hit(DHA, Duration.EIGHTH, velocity=95, articulation="marcato")
tabla.hit(GB, Duration.EIGHTH, velocity=80)
tabla.hit(NA, Duration.EIGHTH, velocity=72)
tabla.hit(GB, Duration.EIGHTH, velocity=82)
tabla.hit(DHA, Duration.EIGHTH, velocity=100, articulation="accent")
tabla.hit(GB, Duration.EIGHTH, velocity=85)
tabla.hit(DHA, Duration.QUARTER, velocity=110, articulation="fermata")
render("tabla_keherwa", score)
def gen_tabla_chakradar():
score = Score("4/4", bpm=200)
score.drums("teental", repeats=2)
score.drums("teental", repeats=1)
score.drums("teental", repeats=1, fill="bayan", fill_every=1)
score.drums("chakradar", repeats=1)
score.set_drum_effects(reverb=0.2)
score.set_drum_effects(reverb=0.3, reverb_type="hall")
render("tabla_chakradar", score)
@@ -232,31 +276,120 @@ def gen_mridangam():
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
Q1 = DrumSound.QUAD_1
Q2 = DrumSound.QUAD_2
Q3 = DrumSound.QUAD_3
Q4 = DrumSound.QUAD_4
QS = DrumSound.QUAD_SPOCK
B1 = DrumSound.BASS_1
B2 = DrumSound.BASS_2
B3 = DrumSound.BASS_3
B4 = DrumSound.BASS_4
B5 = DrumSound.BASS_5
for _ in range(4):
p.hit(C, Duration.QUARTER, velocity=95)
# Snare line — 8 players (high volume to compensate for ensemble division)
sn = score.part("snares", synth="sine", volume=1.5, reverb=0.2, ensemble=8)
# Quads — 4 players
q = score.part("quads", synth="sine", volume=0.5, reverb=0.2, ensemble=4)
# Basses — 5 players
b = score.part("basses", synth="sine", volume=0.55, reverb=0.2, ensemble=5)
# Click count-off
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)
sn.hit(C, Duration.QUARTER, velocity=95)
q.rest(Duration.QUARTER)
b.rest(Duration.QUARTER)
# Bar 1-2: snare groove, quads accent, bass on beats
for _ in range(2):
sn.hit(R, Duration.SIXTEENTH, velocity=118)
sn.hit(S, Duration.SIXTEENTH, velocity=30)
sn.hit(S, Duration.SIXTEENTH, velocity=32)
sn.hit(S, Duration.SIXTEENTH, velocity=28)
sn.hit(R, Duration.SIXTEENTH, velocity=115)
sn.hit(S, Duration.SIXTEENTH, velocity=30)
sn.hit(S, Duration.SIXTEENTH, velocity=28)
sn.hit(S, Duration.SIXTEENTH, velocity=32)
sn.hit(R, Duration.SIXTEENTH, velocity=118)
sn.hit(S, Duration.SIXTEENTH, velocity=35)
sn.hit(S, Duration.SIXTEENTH, velocity=28)
sn.hit(S, Duration.SIXTEENTH, velocity=30)
sn.hit(R, Duration.SIXTEENTH, velocity=120)
sn.hit(S, Duration.SIXTEENTH, velocity=30)
sn.hit(S, Duration.SIXTEENTH, velocity=28)
sn.hit(S, Duration.SIXTEENTH, velocity=32)
q.hit(Q1, Duration.SIXTEENTH, velocity=95)
q.hit(Q2, Duration.SIXTEENTH, velocity=55)
q.hit(Q3, Duration.SIXTEENTH, velocity=55)
q.hit(Q4, Duration.SIXTEENTH, velocity=55)
q.hit(Q4, Duration.SIXTEENTH, velocity=55)
q.hit(Q3, Duration.SIXTEENTH, velocity=55)
q.hit(Q2, Duration.SIXTEENTH, velocity=55)
q.hit(Q1, Duration.SIXTEENTH, velocity=95)
q.hit(QS, Duration.SIXTEENTH, velocity=100)
q.hit(Q1, Duration.SIXTEENTH, velocity=55)
q.hit(Q3, Duration.SIXTEENTH, velocity=55)
q.hit(Q1, Duration.SIXTEENTH, velocity=55)
q.hit(QS, Duration.SIXTEENTH, velocity=100)
q.hit(Q4, Duration.SIXTEENTH, velocity=55)
q.hit(Q2, Duration.SIXTEENTH, velocity=55)
q.hit(Q1, Duration.SIXTEENTH, velocity=90)
b.hit(B3, Duration.QUARTER, velocity=100)
b.hit(B1, Duration.EIGHTH, velocity=90)
b.hit(B5, Duration.EIGHTH, velocity=95)
b.hit(B3, Duration.QUARTER, velocity=100)
b.hit(B5, Duration.EIGHTH, velocity=90)
b.hit(B1, Duration.EIGHTH, velocity=95)
# Bar 3: flams + diddles
sn.flam(S, Duration.QUARTER, velocity=120)
sn.diddle(S, Duration.EIGHTH, velocity=45)
sn.hit(S, Duration.SIXTEENTH, velocity=30)
sn.hit(S, Duration.SIXTEENTH, velocity=32)
sn.flam(S, Duration.QUARTER, velocity=118)
sn.diddle(S, Duration.EIGHTH, velocity=42)
sn.hit(S, Duration.SIXTEENTH, velocity=28)
sn.hit(S, Duration.SIXTEENTH, velocity=30)
q.hit(Q1, Duration.QUARTER, velocity=95)
q.hit(Q3, Duration.EIGHTH, velocity=55)
q.hit(Q2, Duration.SIXTEENTH, velocity=55)
q.hit(Q4, Duration.SIXTEENTH, velocity=55)
q.hit(QS, Duration.QUARTER, velocity=100)
q.hit(Q4, Duration.EIGHTH, velocity=55)
q.hit(Q1, Duration.EIGHTH, velocity=90)
b.hit(B5, Duration.QUARTER, velocity=100)
b.hit(B3, Duration.QUARTER, velocity=95)
b.hit(B1, Duration.QUARTER, velocity=100)
b.hit(B3, Duration.QUARTER, velocity=95)
# Bar 4: buzz roll finale into big hit
for i in range(28):
sn.hit(S, 0.0625, velocity=min(25 + i * 3, 100))
sn.hit(R, Duration.EIGHTH, velocity=125)
sn.hit(R, Duration.EIGHTH, velocity=127)
for i in range(8):
q.hit([Q1,Q2,Q3,Q4,Q4,Q3,Q2,Q1][i], Duration.SIXTEENTH, velocity=60+i*4)
q.hit(QS, Duration.HALF, velocity=110)
b.hit(B1, Duration.SIXTEENTH, velocity=90)
b.hit(B2, Duration.SIXTEENTH, velocity=90)
b.hit(B3, Duration.SIXTEENTH, velocity=90)
b.hit(B4, Duration.SIXTEENTH, velocity=90)
b.hit(B5, Duration.SIXTEENTH, velocity=95)
b.hit(B4, Duration.SIXTEENTH, velocity=90)
b.hit(B3, Duration.SIXTEENTH, velocity=90)
b.hit(B2, Duration.SIXTEENTH, velocity=90)
b.hit(B3, Duration.HALF, velocity=100)
render("march_snare", score)
@@ -391,6 +524,305 @@ def gen_jazz_ballad():
# ── Quickstart example ───────────────────────────────────────────────────
# ── Synth waveform demos ──────────────────────────────────────────────────
def _synth_demo(name, synth, envelope="none", **kwargs):
"""Short C major melody on a given synth."""
score = Score("4/4", bpm=100)
p = score.part("demo", synth=synth, envelope=envelope, volume=0.5,
reverb=0.2, **kwargs)
for n in ["C4", "E4", "G4", "C5", "G4", "E4", "C4", "E4"]:
p.add(n, Duration.QUARTER, velocity=85)
render(f"synth_{name}", score)
def gen_synth_sine():
_synth_demo("sine", "sine")
def gen_synth_saw():
_synth_demo("saw", "saw")
def gen_synth_triangle():
_synth_demo("triangle", "triangle")
def gen_synth_square():
_synth_demo("square", "square")
def gen_synth_piano():
score = Score("4/4", bpm=85)
p = score.part("demo", instrument="piano", volume=0.5, reverb=0.3)
# Hold chords with melody on top
p.hold("C3", Duration.WHOLE * 2, velocity=60)
p.hold("E3", Duration.WHOLE * 2, velocity=55)
p.hold("G3", Duration.WHOLE * 2, velocity=55)
for n in ["E4", "G4", "C5", "G4", "E4", "D4", "C4", "E4"]:
p.add(n, Duration.QUARTER, velocity=80)
render("synth_piano", score)
def gen_synth_acoustic_guitar():
from pytheory import Fretboard
score = Score("4/4", bpm=100)
p = score.part("demo", instrument="acoustic_guitar", volume=0.5,
reverb=0.25, fretboard=Fretboard.guitar())
for ch in ["G", "D", "Em", "C"]:
p.strum(ch, Duration.WHOLE, velocity=75)
render("synth_acoustic_guitar", score)
def gen_synth_pulse():
_synth_demo("pulse", "pulse")
def gen_synth_noise():
score = Score("4/4", bpm=100)
p = score.part("demo", synth="noise", envelope="pad", volume=0.3,
lowpass=2000, reverb=0.3)
p.add("C4", Duration.WHOLE * 2, velocity=80)
render("synth_noise", score)
def gen_synth_pwm_slow():
_synth_demo("pwm_slow", "pwm_slow", envelope="pad")
def gen_synth_pwm_fast():
_synth_demo("pwm_fast", "pwm_fast")
def gen_synth_fm():
score = Score("4/4", bpm=100)
p = score.part("demo", synth="fm", envelope="bell", volume=0.5,
fm_ratio=3.0, fm_index=5.0, reverb=0.3)
for n in ["C5", "E5", "G5", "C6", "G5", "E5", "C5", "E5"]:
p.add(n, Duration.QUARTER, velocity=80)
render("synth_fm", score)
def gen_synth_rhodes():
score = Score("4/4", bpm=80)
p = score.part("demo", instrument="electric_piano", volume=0.5, reverb=0.3)
# Jazz chords with hold
p.hold("C3", Duration.WHOLE * 2, velocity=60)
p.hold("E3", Duration.WHOLE * 2, velocity=55)
p.hold("Bb3", Duration.WHOLE * 2, velocity=55)
for n in ["G4", "Bb4", "C5", "Bb4", "G4", "F4", "E4", "G4"]:
p.add(n, Duration.QUARTER, velocity=75)
render("synth_rhodes", score)
def gen_synth_supersaw():
_synth_demo("supersaw", "supersaw", envelope="pad")
def gen_synth_bass_guitar():
score = Score("4/4", bpm=100)
p = score.part("demo", synth="bass_guitar_synth", envelope="none",
volume=0.5, reverb=0.2)
for n in ["C2", "E2", "G2", "C3", "G2", "E2", "C2", "E2"]:
p.add(n, Duration.QUARTER, velocity=85)
render("synth_bass_guitar", score)
def gen_synth_flute():
_synth_demo("flute", "flute_synth", envelope="none")
def gen_synth_trumpet():
_synth_demo("trumpet", "trumpet_synth", envelope="none")
def gen_synth_clarinet():
_synth_demo("clarinet", "clarinet_synth", envelope="none")
def gen_synth_oboe():
_synth_demo("oboe", "oboe_synth", envelope="none")
def gen_synth_cello():
score = Score("4/4", bpm=70)
p = score.part("demo", instrument="cello", volume=0.5, reverb=0.3, ensemble=3)
for n in ["C3", "E3", "G3", "C4"]:
p.add(n, Duration.WHOLE, velocity=80)
render("synth_cello", score)
def gen_synth_harpsichord():
score = Score("4/4", bpm=100)
p = score.part("demo", synth="harpsichord_synth", envelope="none",
volume=0.5, reverb=0.25)
# Baroque ornamental runs
p.hold("C3", Duration.WHOLE, velocity=70)
for n in ["C4", "D4", "E4", "F4", "G4", "A4", "B4", "C5"]:
p.add(n, Duration.EIGHTH, velocity=80)
p.hold("G3", Duration.WHOLE, velocity=70)
for n in ["C5", "B4", "A4", "G4", "F4", "E4", "D4", "C4"]:
p.add(n, Duration.EIGHTH, velocity=78)
render("synth_harpsichord", score)
def gen_synth_electric_guitar():
from pytheory import Fretboard
score = Score("4/4", bpm=110)
p = score.part("demo", instrument="electric_guitar", volume=0.5,
reverb=0.15, fretboard=Fretboard.guitar())
for ch in ["Am", "F", "C", "G"]:
p.strum(ch, Duration.WHOLE, velocity=80)
render("synth_electric_guitar", score)
def gen_synth_kalimba():
score = Score("4/4", bpm=100)
p = score.part("demo", instrument="kalimba", volume=0.5, reverb=0.3)
for n in ["C5", "E5", "G5", "C6", "G5", "E5", "C5", "E5"]:
p.add(n, Duration.QUARTER, velocity=85)
render("synth_kalimba", score)
def gen_synth_organ():
_synth_demo("organ", "organ_synth", envelope="organ")
def gen_synth_marimba():
_synth_demo("marimba", "marimba_synth", envelope="mallet")
def gen_synth_sitar():
score = Score("4/4", bpm=80)
p = score.part("demo", instrument="sitar", volume=0.4, reverb=0.35)
# Drone under melody
p.hold("C3", Duration.WHOLE * 4, velocity=55)
for n, d in [("C4", 1.0), ("D4", 0.5), ("E4", 0.5), ("G4", 1.0),
("A4", 0.5), ("G4", 0.5), ("E4", 1.0), ("D4", 0.5),
("C4", 0.5), ("D4", 1.0), ("C4", 2.0)]:
p.add(n, d, velocity=75)
render("synth_sitar", score)
def gen_synth_harp():
score = Score("4/4", bpm=80)
p = score.part("demo", synth="harp_synth", envelope="none",
volume=0.5, reverb=0.3)
# Arpeggiated chords with hold — harp style
p.hold("C3", Duration.WHOLE * 2, velocity=70)
for n in ["E4", "G4", "C5", "E5", "G5", "C5", "G4", "E4"]:
p.add(n, Duration.QUARTER, velocity=75)
render("synth_harp", score)
def gen_synth_upright_bass():
score = Score("4/4", bpm=100)
p = score.part("demo", synth="upright_bass_synth", envelope="none",
volume=0.5, reverb=0.2)
for n in ["C2", "E2", "G2", "C3", "G2", "E2", "C2", "E2"]:
p.add(n, Duration.QUARTER, velocity=85)
render("synth_upright_bass", score)
def gen_synth_timpani():
score = Score("4/4", bpm=140)
timp = score.part("timp", synth="timpani_synth", volume=0.5, reverb=0.25)
timp.roll("C3", Duration.WHOLE, velocity_start=20, velocity_end=110, speed=0.125)
timp.add("C3", Duration.HALF, velocity=127)
timp.rest(Duration.HALF)
timp.roll("G2", Duration.WHOLE, velocity_start=20, velocity_end=110, speed=0.125)
timp.add("G2", Duration.HALF, velocity=127)
render("synth_timpani", score)
def gen_synth_strings():
score = Score("4/4", bpm=70)
p = score.part("demo", synth="strings_synth", envelope="bowed",
volume=0.5, reverb=0.35, ensemble=8)
for n in ["C4", "E4", "G4", "C5"]:
p.add(n, Duration.WHOLE, velocity=75)
render("synth_strings", score)
def gen_synth_saxophone():
score = Score("4/4", bpm=100)
p = score.part("demo", instrument="tenor_sax", 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_saxophone", score)
def gen_synth_pedal_steel():
score = Score("4/4", bpm=100)
p = score.part("demo", instrument="pedal_steel", volume=0.5, reverb=0.3)
for n in ["C4", "E4", "G4", "C5", "G4", "E4", "C4", "E4"]:
p.add(n, Duration.QUARTER, velocity=85)
render("synth_pedal_steel", score)
def gen_synth_theremin():
score = Score("4/4", bpm=100)
p = score.part("demo", instrument="theremin", volume=0.5, reverb=0.3)
for n in ["C4", "E4", "G4", "C5", "G4", "E4", "C4", "E4"]:
p.add(n, Duration.QUARTER, velocity=85)
render("synth_theremin", score)
def gen_synth_steel_drum():
score = Score("4/4", bpm=100)
p = score.part("demo", instrument="steel_drum", volume=0.5, reverb=0.2)
for n in ["C5", "E5", "G5", "C6", "G5", "E5", "C5", "E5"]:
p.add(n, Duration.QUARTER, velocity=85)
render("synth_steel_drum", score)
def gen_synth_accordion():
score = Score("4/4", bpm=110)
p = score.part("demo", instrument="accordion", volume=0.5, reverb=0.2)
# Waltz feel with held chords
p.hold("C3", Duration.WHOLE, velocity=65)
p.hold("E3", Duration.WHOLE, velocity=60)
p.hold("G3", Duration.WHOLE, velocity=60)
for n in ["E4", "G4", "C5", "G4"]:
p.add(n, Duration.QUARTER, velocity=78)
p.hold("F3", Duration.WHOLE, velocity=65)
p.hold("A3", Duration.WHOLE, velocity=60)
p.hold("C4", Duration.WHOLE, velocity=60)
for n in ["A4", "C5", "F5", "C5"]:
p.add(n, Duration.QUARTER, velocity=78)
render("synth_accordion", score)
def gen_synth_didgeridoo():
score = Score("4/4", bpm=80)
p = score.part("demo", instrument="didgeridoo", volume=0.5, reverb=0.3)
p.add("C2", Duration.WHOLE * 2, velocity=85)
render("synth_didgeridoo", score)
def gen_synth_bagpipe():
score = Score("4/4", bpm=90)
p = score.part("demo", instrument="bagpipe", volume=0.4, reverb=0.2)
# Drone on low A + E (like real Highland pipes)
p.hold("A3", Duration.WHOLE * 4, velocity=70)
p.hold("E3", Duration.WHOLE * 4, velocity=65)
# Chanter melody on top
for n, d in [("A4", 1.0), ("B4", 0.5), ("C5", 0.5), ("D5", 1.0),
("E5", 1.0), ("D5", 0.5), ("C5", 0.5), ("B4", 1.0),
("A4", 1.0), ("G4", 1.0), ("A4", 2.0)]:
p.add(n, d, velocity=80)
render("synth_bagpipe", score)
def gen_synth_banjo():
from pytheory import Fretboard
score = Score("4/4", bpm=130)
p = score.part("demo", instrument="banjo", volume=0.5, reverb=0.15,
fretboard=Fretboard.guitar())
# Strum into a picking lick
p.strum("G", Duration.WHOLE, velocity=80)
p.strum("C", Duration.WHOLE, velocity=78)
# Bluegrass lick — 16th note picking
for n in ["G4", "B4", "D5", "G5", "D5", "B4", "A4", "G4",
"D4", "G4", "B4", "D5", "B4", "G4", "D4", "G4"]:
p.add(n, Duration.SIXTEENTH, velocity=82)
render("synth_banjo", score)
def gen_synth_mandolin():
score = Score("4/4", bpm=110)
p = score.part("demo", instrument="mandolin", volume=0.5, reverb=0.2)
# Tremolo rolls on held notes — the mandolin signature
p.roll("G4", Duration.WHOLE, velocity_start=65, velocity_end=85, speed=Duration.SIXTEENTH)
p.roll("A4", Duration.WHOLE, velocity_start=65, velocity_end=85, speed=Duration.SIXTEENTH)
# Quick melody
for n in ["B4", "C5", "D5", "C5", "B4", "A4", "G4", "A4"]:
p.add(n, Duration.EIGHTH, velocity=80)
# End on a roll
p.roll("G4", Duration.WHOLE, velocity_start=70, velocity_end=90, speed=Duration.SIXTEENTH)
render("synth_mandolin", score)
def gen_synth_ukulele():
from pytheory import Fretboard
score = Score("4/4", bpm=110)
p = score.part("demo", instrument="ukulele", volume=0.5, reverb=0.25,
fretboard=Fretboard.ukulele())
for ch in ["C", "Am", "F", "G"]:
p.strum(ch, Duration.WHOLE, velocity=72)
render("synth_ukulele", score)
def gen_synth_granular():
score = Score("4/4", bpm=80)
p = score.part("demo", instrument="granular_pad", volume=0.5, reverb=0.4)
for n in ["C4", "E4", "G4", "C5"]:
p.add(n, Duration.WHOLE, velocity=75)
render("synth_granular", score)
def gen_arpeggio():
score = Score("4/4", bpm=132)
score.drums("house", repeats=8)
@@ -419,25 +851,24 @@ def gen_legato_glide():
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)
score = Score("4/4", bpm=120)
score.drums("rock", repeats=8, fill="rock", fill_every=4)
piano = score.part("piano", instrument="piano", reverb=0.3)
lead = score.part("lead", synth="saw", envelope="pluck", volume=0.4,
delay=0.2, reverb=0.2, lowpass=4000)
bass = score.part("bass", synth="triangle", lowpass=900)
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("quickstart", score)
@@ -459,7 +890,7 @@ def gen_complete_rock():
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)
lowpass=1200)
for chord in Key("G", "major").progression("I", "V", "vi", "IV") * 2:
piano.add(chord, Duration.WHOLE)
lead.add("D5", 1).add("B4", 0.5).add("D5", 0.5)
@@ -550,6 +981,46 @@ GENERATORS = [
gen_ensemble,
gen_strum,
gen_swell,
gen_synth_sine,
gen_synth_saw,
gen_synth_triangle,
gen_synth_square,
gen_synth_pulse,
gen_synth_noise,
gen_synth_pwm_slow,
gen_synth_pwm_fast,
gen_synth_fm,
gen_synth_rhodes,
gen_synth_supersaw,
gen_synth_piano,
gen_synth_bass_guitar,
gen_synth_flute,
gen_synth_trumpet,
gen_synth_clarinet,
gen_synth_oboe,
gen_synth_cello,
gen_synth_harpsichord,
gen_synth_acoustic_guitar,
gen_synth_electric_guitar,
gen_synth_sitar,
gen_synth_kalimba,
gen_synth_organ,
gen_synth_marimba,
gen_synth_harp,
gen_synth_upright_bass,
gen_synth_timpani,
gen_synth_strings,
gen_synth_saxophone,
gen_synth_pedal_steel,
gen_synth_theremin,
gen_synth_steel_drum,
gen_synth_accordion,
gen_synth_didgeridoo,
gen_synth_bagpipe,
gen_synth_banjo,
gen_synth_mandolin,
gen_synth_ukulele,
gen_synth_granular,
gen_arpeggio,
gen_legato_glide,
gen_acid_house,
+15 -33
View File
@@ -143,45 +143,27 @@ chords, melody, bass, each with their own synth and effects:
.. code-block:: python
from pytheory import Score, Pattern, Key, Duration, Chord
from pytheory import Score, Key, Duration
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)
chords = score.part(
"chords",
synth="fm",
envelope="pad",
reverb=0.4,
)
lead = score.part(
"lead",
synth="saw",
envelope="pluck",
delay=0.3,
lowpass=3000,
humanize=0.2,
)
bass = score.part(
"bass",
synth="sine",
lowpass=500,
)
piano = score.part("piano", instrument="piano", reverb=0.3)
lead = score.part("lead", synth="saw", envelope="pluck",
delay=0.2, reverb=0.2, lowpass=4000)
bass = score.part("bass", synth="triangle", lowpass=900)
key = Key("A", "minor")
for chord in key.progression("i", "iv", "V", "i"):
chords.add(chord, Duration.WHOLE)
chords.add(chord, Duration.WHOLE)
for chord in Key("G", "major").progression("I", "V", "vi", "IV") * 2:
piano.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)
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)
for n in ["A2", "E2", "A2", "C3"] * 4:
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)
+1 -1
View File
@@ -396,7 +396,7 @@ lead, and filtered bass:
# Filtered bass
bass = score.part("bass", synth="triangle", envelope="pluck",
volume=0.45, lowpass=600)
volume=0.45, lowpass=1200)
for chord in Key("G", "major").progression("I", "V", "vi", "IV") * 2:
piano.add(chord, Duration.WHOLE)
+180 -4
View File
@@ -37,6 +37,10 @@ building block of all other waveforms (Fourier's theorem).
tone = Tone.from_string("C4", system="western")
play(tone, synth=Synth.SINE)
.. raw:: html
<audio controls style="width:100%;margin:0.3em 0 0.5em"><source src="../_static/audio/synth_sine.wav" type="audio/wav"></audio>
Sawtooth
~~~~~~~~
@@ -50,6 +54,10 @@ Named for its ramp shape.
play(tone, synth=Synth.SAW)
.. raw:: html
<audio controls style="width:100%;margin:0.3em 0 0.5em"><source src="../_static/audio/synth_saw.wav" type="audio/wav"></audio>
Triangle
~~~~~~~~
@@ -63,6 +71,10 @@ described as "woody" or "hollow."
play(tone, synth=Synth.TRIANGLE)
.. raw:: html
<audio controls style="width:100%;margin:0.3em 0 0.5em"><source src="../_static/audio/synth_triangle.wav" type="audio/wav"></audio>
Square
~~~~~~
@@ -76,6 +88,10 @@ pulse wave with a 50% duty cycle.
play(tone, synth=Synth.SQUARE)
.. raw:: html
<audio controls style="width:100%;margin:0.3em 0 0.5em"><source src="../_static/audio/synth_square.wav" type="audio/wav"></audio>
Extended Waveforms
------------------
@@ -98,6 +114,10 @@ the classic NES-style buzzy tone.
lead = score.part("lead", synth="pulse", envelope="pluck")
.. raw:: html
<audio controls style="width:100%;margin:0.3em 0 0.5em"><source src="../_static/audio/synth_pulse.wav" type="audio/wav"></audio>
FM Synthesis
~~~~~~~~~~~~
@@ -109,18 +129,24 @@ the electric piano in every Whitney Houston ballad, the bass in every
Depeche Mode track, the bells in a thousand TV jingles. If you heard
pop music in the 80s, you heard FM synthesis.
**Use for:** electric piano (rhodes), bells, metallic leads, jazz chords.
**Use for:** bells, metallic leads, glassy pads, DX7-style sounds.
.. code-block:: python
rhodes = score.part(
"rhodes",
bells = score.part(
"bells",
synth="fm",
envelope="piano",
envelope="bell",
fm_ratio=3.0,
fm_index=5.0,
volume=0.3,
reverb=0.4,
)
.. raw:: html
<audio controls style="width:100%;margin:0.3em 0 0.5em"><source src="../_static/audio/synth_fm.wav" type="audio/wav"></audio>
Noise
-----
@@ -142,6 +168,10 @@ Useful as a texture layer, a percussion source, or a wind/ocean effect.
lowpass=2000,
)
.. raw:: html
<audio controls style="width:100%;margin:0.3em 0 0.5em"><source src="../_static/audio/synth_noise.wav" type="audio/wav"></audio>
Ensemble Waveforms
------------------
@@ -174,6 +204,10 @@ supersaw.
reverb=0.5,
)
.. raw:: html
<audio controls style="width:100%;margin:0.3em 0 0.5em"><source src="../_static/audio/synth_supersaw.wav" type="audio/wav"></audio>
PWM Slow
~~~~~~~~
@@ -195,6 +229,10 @@ from Boards of Canada to Drake? PWM with a slow LFO.
reverb=0.4,
)
.. raw:: html
<audio controls style="width:100%;margin:0.3em 0 0.5em"><source src="../_static/audio/synth_pwm_slow.wav" type="audio/wav"></audio>
PWM Fast
~~~~~~~~
@@ -207,6 +245,10 @@ produces a natural chorus/vibrato effect built into the waveform itself.
lead = score.part("lead", synth="pwm_fast", envelope="pluck", volume=0.5)
.. raw:: html
<audio controls style="width:100%;margin:0.3em 0 0.5em"><source src="../_static/audio/synth_pwm_fast.wav" type="audio/wav"></audio>
ADSR Envelopes
--------------
@@ -375,6 +417,10 @@ at musical levels. Warm, round, unmistakably organ.
organ = score.part("organ", synth="organ_synth")
.. raw:: html
<audio controls style="width:100%;margin:0.3em 0 0.5em"><source src="../_static/audio/synth_organ.wav" type="audio/wav"></audio>
String Ensemble
~~~~~~~~~~~~~~~
@@ -386,6 +432,10 @@ more "wooden" than a raw saw wave.
violin = score.part("violin", synth="strings_synth")
.. raw:: html
<audio controls style="width:100%;margin:0.3em 0 0.5em"><source src="../_static/audio/synth_strings.wav" type="audio/wav"></audio>
Dedicated Instrument Synths
--------------------------
@@ -407,6 +457,28 @@ soundboard.
piano = score.part("piano", synth="piano_synth")
.. raw:: html
<audio controls style="width:100%;margin:0.3em 0 0.5em"><source src="../_static/audio/synth_piano.wav" type="audio/wav"></audio>
Rhodes Electric Piano
~~~~~~~~~~~~~~~~~~~~~
The Fender Rhodes — a rubber-tipped hammer strikes a steel tine
next to a tonebar, picked up by an electromagnetic pickup. Warm,
bell-like, with a bright metallic attack that mellows into a
singing sustain. The sound of jazz clubs, soul, and neo-soul.
.. code-block:: python
rhodes = score.part("rhodes", synth="rhodes_synth")
# Or use the instrument preset (adds tremolo + chorus)
rhodes = score.part("rhodes", instrument="electric_piano")
.. raw:: html
<audio controls style="width:100%;margin:0.3em 0 0.5em"><source src="../_static/audio/synth_rhodes.wav" type="audio/wav"></audio>
Bass Guitar Synth
~~~~~~~~~~~~~~~~~
@@ -416,6 +488,10 @@ Plucked string model with finger-damped harmonics and low-end warmth.
bass = score.part("bass", synth="bass_guitar_synth")
.. raw:: html
<audio controls style="width:100%;margin:0.3em 0 0.5em"><source src="../_static/audio/synth_bass_guitar.wav" type="audio/wav"></audio>
Flute Synth
~~~~~~~~~~~~
@@ -426,6 +502,10 @@ overblowing behavior at higher velocities.
flute = score.part("flute", synth="flute_synth")
.. raw:: html
<audio controls style="width:100%;margin:0.3em 0 0.5em"><source src="../_static/audio/synth_flute.wav" type="audio/wav"></audio>
Trumpet Synth
~~~~~~~~~~~~~
@@ -436,6 +516,10 @@ velocity, plus a characteristic brassy edge from shaped harmonics.
trumpet = score.part("trumpet", synth="trumpet_synth")
.. raw:: html
<audio controls style="width:100%;margin:0.3em 0 0.5em"><source src="../_static/audio/synth_trumpet.wav" type="audio/wav"></audio>
Clarinet Synth
~~~~~~~~~~~~~~
@@ -446,6 +530,10 @@ characteristic hollow, woody tone.
clarinet = score.part("clarinet", synth="clarinet_synth")
.. raw:: html
<audio controls style="width:100%;margin:0.3em 0 0.5em"><source src="../_static/audio/synth_clarinet.wav" type="audio/wav"></audio>
Oboe Synth
~~~~~~~~~~~
@@ -456,6 +544,10 @@ timbre.
oboe = score.part("oboe", synth="oboe_synth")
.. raw:: html
<audio controls style="width:100%;margin:0.3em 0 0.5em"><source src="../_static/audio/synth_oboe.wav" type="audio/wav"></audio>
Marimba Synth
~~~~~~~~~~~~~
@@ -466,6 +558,10 @@ that emphasizes the fundamental.
marimba = score.part("marimba", synth="marimba_synth")
.. raw:: html
<audio controls style="width:100%;margin:0.3em 0 0.5em"><source src="../_static/audio/synth_marimba.wav" type="audio/wav"></audio>
Harpsichord Synth
~~~~~~~~~~~~~~~~~
@@ -476,6 +572,10 @@ Plucked-string model with a bright, immediate attack and rapid decay
harpsi = score.part("harpsi", synth="harpsichord_synth")
.. raw:: html
<audio controls style="width:100%;margin:0.3em 0 0.5em"><source src="../_static/audio/synth_harpsichord.wav" type="audio/wav"></audio>
Cello Synth
~~~~~~~~~~~
@@ -486,6 +586,10 @@ producing a rich, warm, sustained tone.
cello = score.part("cello", synth="cello_synth")
.. raw:: html
<audio controls style="width:100%;margin:0.3em 0 0.5em"><source src="../_static/audio/synth_cello.wav" type="audio/wav"></audio>
Harp Synth
~~~~~~~~~~
@@ -496,6 +600,10 @@ modeling nylon strings on a resonant frame.
harp = score.part("harp", synth="harp_synth")
.. raw:: html
<audio controls style="width:100%;margin:0.3em 0 0.5em"><source src="../_static/audio/synth_harp.wav" type="audio/wav"></audio>
Upright Bass Synth
~~~~~~~~~~~~~~~~~~
@@ -505,6 +613,10 @@ Pizzicato double bass with woody body resonance and a thumpy low end.
bass = score.part("bass", synth="upright_bass_synth")
.. raw:: html
<audio controls style="width:100%;margin:0.3em 0 0.5em"><source src="../_static/audio/synth_upright_bass.wav" type="audio/wav"></audio>
Acoustic Guitar Synth
~~~~~~~~~~~~~~~~~~~~~
@@ -515,6 +627,10 @@ string decay.
guitar = score.part("guitar", synth="acoustic_guitar_synth")
.. raw:: html
<audio controls style="width:100%;margin:0.3em 0 0.5em"><source src="../_static/audio/synth_acoustic_guitar.wav" type="audio/wav"></audio>
Electric Guitar Synth
~~~~~~~~~~~~~~~~~~~~~
@@ -525,6 +641,10 @@ than the acoustic, ready for effects processing.
eguitar = score.part("eguitar", synth="electric_guitar_synth")
.. raw:: html
<audio controls style="width:100%;margin:0.3em 0 0.5em"><source src="../_static/audio/synth_electric_guitar.wav" type="audio/wav"></audio>
Sitar Synth
~~~~~~~~~~~~
@@ -535,6 +655,10 @@ bridge, producing a shimmering, metallic sustain.
sitar = score.part("sitar", synth="sitar_synth")
.. raw:: html
<audio controls style="width:100%;margin:0.3em 0 0.5em"><source src="../_static/audio/synth_sitar.wav" type="audio/wav"></audio>
Timpani Synth
~~~~~~~~~~~~~
@@ -547,6 +671,10 @@ Use ``Part.roll()`` for crescendo timpani rolls.
timp = score.part("timp", synth="timpani_synth")
timp.roll("C3", Duration.WHOLE, velocity_start=20, velocity_end=110)
.. raw:: html
<audio controls style="width:100%;margin:0.3em 0 0.5em"><source src="../_static/audio/synth_timpani.wav" type="audio/wav"></audio>
Saxophone Synth
~~~~~~~~~~~~~~~
@@ -558,6 +686,10 @@ mids, reed buzz, and brass body warmth. Four presets: ``saxophone``,
sax = score.part("sax", instrument="tenor_sax")
.. raw:: html
<audio controls style="width:100%;margin:0.3em 0 0.5em"><source src="../_static/audio/synth_saxophone.wav" type="audio/wav"></audio>
Pedal Steel Synth
~~~~~~~~~~~~~~~~~
@@ -568,6 +700,10 @@ and long sustain. Pairs naturally with spring reverb.
steel = score.part("steel", instrument="pedal_steel")
.. raw:: html
<audio controls style="width:100%;margin:0.3em 0 0.5em"><source src="../_static/audio/synth_pedal_steel.wav" type="audio/wav"></audio>
Theremin Synth
~~~~~~~~~~~~~~
@@ -578,6 +714,10 @@ Best used with legato and glide for continuous pitch.
theremin = score.part("theremin", instrument="theremin")
.. raw:: html
<audio controls style="width:100%;margin:0.3em 0 0.5em"><source src="../_static/audio/synth_theremin.wav" type="audio/wav"></audio>
Kalimba Synth
~~~~~~~~~~~~~
@@ -588,6 +728,10 @@ inharmonic overtones (modes at 1x, 2.92x, 5.4x).
kalimba = score.part("kalimba", instrument="kalimba")
.. raw:: html
<audio controls style="width:100%;margin:0.3em 0 0.5em"><source src="../_static/audio/synth_kalimba.wav" type="audio/wav"></audio>
Steel Drum Synth
~~~~~~~~~~~~~~~~
@@ -598,6 +742,10 @@ Inharmonic partials at 2.0x, 3.01x, 4.1x, 5.3x.
pan = score.part("pan", instrument="steel_drum")
.. raw:: html
<audio controls style="width:100%;margin:0.3em 0 0.5em"><source src="../_static/audio/synth_steel_drum.wav" type="audio/wav"></audio>
Accordion Synth
~~~~~~~~~~~~~~~
@@ -608,6 +756,10 @@ create natural beating. Bellows pressure swell modulates amplitude.
acc = score.part("acc", instrument="accordion")
.. raw:: html
<audio controls style="width:100%;margin:0.3em 0 0.5em"><source src="../_static/audio/synth_accordion.wav" type="audio/wav"></audio>
Didgeridoo Synth
~~~~~~~~~~~~~~~~
@@ -619,6 +771,10 @@ Best with cave reverb.
didg = score.part("didg", instrument="didgeridoo")
.. raw:: html
<audio controls style="width:100%;margin:0.3em 0 0.5em"><source src="../_static/audio/synth_didgeridoo.wav" type="audio/wav"></audio>
Bagpipe Synth
~~~~~~~~~~~~~
@@ -629,6 +785,10 @@ peaked around 3-7 (the piercing brightness). No dynamics — always ff.
pipes = score.part("pipes", instrument="bagpipe")
.. raw:: html
<audio controls style="width:100%;margin:0.3em 0 0.5em"><source src="../_static/audio/synth_bagpipe.wav" type="audio/wav"></audio>
Banjo Synth
~~~~~~~~~~~
@@ -639,6 +799,10 @@ nasal, ringy resonance with faster decay than guitar.
banjo = score.part("banjo", instrument="banjo")
.. raw:: html
<audio controls style="width:100%;margin:0.3em 0 0.5em"><source src="../_static/audio/synth_banjo.wav" type="audio/wav"></audio>
Mandolin Synth
~~~~~~~~~~~~~~
@@ -649,6 +813,10 @@ doubled unison strings. Bright, ringing, fast attack.
mando = score.part("mando", instrument="mandolin")
.. raw:: html
<audio controls style="width:100%;margin:0.3em 0 0.5em"><source src="../_static/audio/synth_mandolin.wav" type="audio/wav"></audio>
Ukulele Synth
~~~~~~~~~~~~~
@@ -659,6 +827,10 @@ softer attack than guitar, shorter sustain.
uke = score.part("uke", instrument="ukulele")
.. raw:: html
<audio controls style="width:100%;margin:0.3em 0 0.5em"><source src="../_static/audio/synth_ukulele.wav" type="audio/wav"></audio>
Granular Synth
~~~~~~~~~~~~~~
@@ -688,6 +860,10 @@ Parameters (passed as synth kwargs):
- ``source``: Base waveform — ``"saw"``, ``"sine"``, ``"triangle"``,
``"square"``, ``"noise"``.
.. raw:: html
<audio controls style="width:100%;margin:0.3em 0 0.5em"><source src="../_static/audio/synth_granular.wav" type="audio/wav"></audio>
Analog Oscillator Drift
~~~~~~~~~~~~~~~~~~~~~~~~
+16 -9
View File
@@ -46,20 +46,27 @@ it through your speakers, export MIDI, finish in your DAW:
.. code-block:: python
from pytheory import Score, Pattern, Key, Duration, Chord
from pytheory import Score, Key, Duration
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)
chords = score.part("chords", synth="fm", envelope="pad", reverb=0.4)
lead = score.part("lead", synth="saw", envelope="pluck", delay=0.3)
bass = score.part("bass", synth="sine", lowpass=500)
piano = score.part("piano", instrument="piano", reverb=0.3)
lead = score.part("lead", synth="saw", envelope="pluck",
delay=0.2, reverb=0.2, lowpass=4000)
bass = score.part("bass", synth="triangle", lowpass=900)
for chord in Key("A", "minor").progression("i", "iv", "V", "i"):
chords.add(chord, Duration.WHOLE)
for chord in Key("G", "major").progression("I", "V", "vi", "IV") * 2:
piano.add(chord, Duration.WHOLE)
lead.arpeggio("Am", bars=4, pattern="updown", octaves=2)
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)
for n in ["G2", "G2", "D2", "D2", "E2", "E2", "C2", "C2"] * 2:
bass.add(n, Duration.HALF)
play_score(score)
+1 -1
View File
@@ -1,6 +1,6 @@
[project]
name = "pytheory"
version = "0.39.3"
version = "0.40.0"
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.3"
__version__ = "0.40.0"
from .tones import Tone, Interval
from .systems import System, SYSTEMS, TET
+225 -112
View File
@@ -416,6 +416,68 @@ def piano_wave(hz, peak=SAMPLE_PEAK, n_samples=SAMPLE_RATE):
return (peak * wave).astype(numpy.int16)
def rhodes_wave(hz, peak=SAMPLE_PEAK, n_samples=SAMPLE_RATE):
"""Rhodes electric piano — tine struck by hammer, electromagnetic pickup.
The Rhodes sound comes from a rubber-tipped hammer hitting a thin
steel tine next to a tonebar. The tine vibrates near an electromagnetic
pickup (like a guitar pickup), producing a warm, bell-like tone with:
1. Strong fundamental + 2nd harmonic (tine character)
2. Bright metallic attack that mellows quickly (hammer on tine)
3. Bell-like inharmonic partials on soft hits, bark on hard hits
4. Asymmetric waveform from the pickup's nonlinear response
"""
t = numpy.arange(n_samples, dtype=numpy.float64) / SAMPLE_RATE
rng = numpy.random.default_rng(int(hz * 100) % 2**31)
# Two-stage decay: quick initial drop, then long sustain
decay = numpy.where(t < 0.15,
numpy.exp(-4.0 * t),
numpy.exp(-4.0 * 0.15) * numpy.exp(-1.2 * (t - 0.15)))
wave = numpy.zeros(n_samples, dtype=numpy.float64)
# Brightness scales with pitch
brightness = numpy.clip((hz - 65) / 800, 0.0, 1.0)
# Tine harmonics — strong fundamental, prominent 2nd, bell-like upper
tine_harmonics = [
(1, 1.0), # fundamental
(2, 0.6 + 0.15 * brightness), # 2nd — the Rhodes character
(3, 0.15 + 0.1 * brightness), # 3rd — adds warmth
(4, 0.08 + 0.08 * brightness), # 4th — bell quality
(5, 0.04), # 5th — subtle shimmer
(6, 0.02), # 6th
]
for n, amp in tine_harmonics:
f_n = hz * n
if f_n >= SAMPLE_RATE / 2:
break
# Higher harmonics decay faster
h_decay = decay * numpy.exp(-(0.8 + 0.3 * brightness) * (n - 1) * t)
phase = rng.uniform(0, 2 * numpy.pi)
wave += amp * numpy.sin(2 * numpy.pi * f_n * t + phase) * h_decay
# Hammer-on-tine transient — bright metallic click
click_len = min(int(SAMPLE_RATE * 0.008), n_samples)
click_t = numpy.arange(click_len, dtype=numpy.float64) / SAMPLE_RATE
# Inharmonic tine ring at attack (bell partials)
click = (numpy.sin(2 * numpy.pi * hz * 5.3 * click_t) * 0.15 +
numpy.sin(2 * numpy.pi * hz * 7.1 * click_t) * 0.08)
click *= numpy.exp(-numpy.linspace(0, 20, click_len))
wave[:click_len] += click
# Subtle asymmetry from pickup nonlinearity (soft saturation)
wave = numpy.tanh(wave * 1.2) / 1.2
mx = numpy.abs(wave).max()
if mx > 0:
wave /= mx
return (peak * wave).astype(numpy.int16)
def bass_guitar_wave(hz, peak=SAMPLE_PEAK, n_samples=SAMPLE_RATE):
"""Bass guitar — plucked thick string with magnetic pickup.
@@ -733,45 +795,62 @@ def cello_wave(hz, peak=SAMPLE_PEAK, n_samples=SAMPLE_RATE):
def harp_wave(hz, peak=SAMPLE_PEAK, n_samples=SAMPLE_RATE):
"""Harp — plucked string with large resonant soundboard.
"""Harp — pure, singing tone with gentle pluck and long sustain.
Warmer than guitar, longer sustain, with the distinctive
"bloom" as the soundboard absorbs and re-radiates energy.
Nylon/gut strings on a large resonant frame. The tone is warm
and clean — mostly fundamental with gentle upper harmonics that
decay faster, leaving a pure singing sustain.
"""
period = int(SAMPLE_RATE / hz)
if period < 2:
period = 2
t = numpy.arange(n_samples, dtype=numpy.float64) / SAMPLE_RATE
rng = numpy.random.default_rng(int(hz * 100) % 2**31)
# Soft pluck — fingers, not pick
buf = rng.uniform(-0.5, 0.5, period).astype(numpy.float64)
for _ in range(3):
# Long, gentle decay
decay = numpy.exp(-1.5 * t)
wave = numpy.zeros(n_samples, dtype=numpy.float64)
# Clean harmonics — strong fundamental, gentle upper partials
n_harmonics = min(10, int((SAMPLE_RATE / 2) / hz))
for n in range(1, n_harmonics + 1):
f_n = hz * n
if f_n >= SAMPLE_RATE / 2:
break
# Fundamental dominates, upper partials gentle and fast-decaying
if n == 1:
amp = 1.0
elif n == 2:
amp = 0.3
elif n == 3:
amp = 0.15
else:
amp = 0.06 / n
h_decay = decay * numpy.exp(-1.5 * (n - 1) * t)
phase = rng.uniform(0, 2 * numpy.pi)
wave += amp * numpy.sin(2 * numpy.pi * f_n * t + phase) * h_decay
# Karplus-Strong pluck transient — just the first ~50ms
# Gives the finger-on-string attack, then the pure tone takes over
period = max(2, int(SAMPLE_RATE / hz))
ks_rng = numpy.random.default_rng(int(hz * 77) % 2**31)
ks_buf = ks_rng.uniform(-0.3, 0.3, period).astype(numpy.float64)
# Pre-filter for soft finger pluck
for _ in range(4):
for k in range(period - 1):
buf[k] = 0.6 * buf[k] + 0.4 * buf[k + 1]
ks_buf[k] = 0.6 * ks_buf[k] + 0.4 * ks_buf[k + 1]
pluck_len = min(int(SAMPLE_RATE * 0.05), n_samples)
pluck = numpy.zeros(pluck_len, dtype=numpy.float64)
for i in range(pluck_len):
pluck[i] = ks_buf[i % period]
nxt = (i + 1) % period
ks_buf[i % period] = 0.5 * (ks_buf[i % period] + ks_buf[nxt])
# Fade the KS pluck out quickly so the pure tone takes over
pluck *= numpy.exp(-numpy.linspace(0, 8, pluck_len)) * 0.4
wave[:pluck_len] += pluck
out = numpy.zeros(n_samples, dtype=numpy.float64)
for i in range(n_samples):
out[i] = buf[i % period]
next_idx = (i + 1) % period
# Long sustain — harp strings ring
buf[i % period] = 0.5 * (buf[i % period] + buf[next_idx]) * 0.9995
# Large soundboard resonance — bloom effect
for center, bw, gain in [(150, 60, 0.3), (350, 100, 0.25), (700, 150, 0.15)]:
lo = max(20, center - bw)
hi = min(SAMPLE_RATE // 2 - 1, center + bw)
if lo < hi:
bp, ap = scipy.signal.butter(2, [lo, hi], btype='band', fs=SAMPLE_RATE)
out += scipy.signal.lfilter(bp, ap, out) * gain
# Warm rolloff
bl, al = scipy.signal.butter(2, min(5000, hz * 10), btype='low', fs=SAMPLE_RATE)
out = scipy.signal.lfilter(bl, al, out)
mx = numpy.abs(out).max()
mx = numpy.abs(wave).max()
if mx > 0:
out /= mx
return (peak * out).astype(numpy.int16)
wave /= mx
return (peak * wave).astype(numpy.int16)
def upright_bass_wave(hz, peak=SAMPLE_PEAK, n_samples=SAMPLE_RATE):
@@ -832,11 +911,19 @@ def timpani_wave(hz, peak=SAMPLE_PEAK, n_samples=SAMPLE_RATE):
wave += numpy.sin(2 * numpy.pi * hz * 1.99 * t) * 0.2 * numpy.exp(-10 * t)
wave += numpy.sin(2 * numpy.pi * hz * 2.44 * t) * 0.1 * numpy.exp(-15 * t)
# Two-stage decay: initial thump fades fast, fundamental rings
decay = numpy.where(t < 0.15,
numpy.exp(-4 * t),
numpy.exp(-4 * 0.15) * numpy.exp(-1.5 * (t - 0.15)))
wave *= decay
# Two-stage decay: thump fades, but fundamental SUSTAINS long
# This is what makes the "oooh" — the fundamental rings and rings,
# so rapid hits in a roll stack into a singing resonance
thump_decay = numpy.exp(-6 * t) # upper modes die fast
fund_decay = numpy.exp(-0.35 * t) # fundamental sustains very long
# Apply different decays: fundamental gets long sustain
fund = numpy.sin(2 * numpy.pi * hz * t) * 0.8 * fund_decay
upper = (numpy.sin(2 * numpy.pi * hz * 1.5 * t) * 0.35 * numpy.exp(-6 * t) +
numpy.sin(2 * numpy.pi * hz * 1.99 * t) * 0.2 * numpy.exp(-10 * t) +
numpy.sin(2 * numpy.pi * hz * 2.44 * t) * 0.1 * numpy.exp(-15 * t))
upper *= thump_decay
wave = fund + upper
# Felt mallet impact — warm, not sharp
mallet_len = min(int(SAMPLE_RATE * 0.02), n_samples)
@@ -845,12 +932,11 @@ def timpani_wave(hz, peak=SAMPLE_PEAK, n_samples=SAMPLE_RATE):
mallet *= numpy.exp(-numpy.linspace(0, 8, mallet_len))
wave[:mallet_len] += mallet
# Copper kettle resonance — boosts low-mids
import scipy.signal as _sig
lo, hi = max(20, int(hz * 0.7)), min(SAMPLE_RATE // 2 - 1, int(hz * 2))
# Copper kettle resonance — boosts the fundamental ring
lo, hi = max(20, int(hz * 0.7)), min(SAMPLE_RATE // 2 - 1, int(hz * 1.3))
if lo < hi:
bp, ap = _sig.butter(2, [lo, hi], btype='band', fs=SAMPLE_RATE)
kettle = _sig.lfilter(bp, ap, wave) * 0.3
bp, ap = scipy.signal.butter(2, [lo, hi], btype='band', fs=SAMPLE_RATE)
kettle = scipy.signal.lfilter(bp, ap, wave) * 0.4
wave += kettle
mx = numpy.abs(wave).max()
@@ -1872,7 +1958,7 @@ _SYNTH_FUNCTIONS = {
"noise": noise_wave, "supersaw": supersaw_wave,
"pwm_slow": pwm_slow_wave, "pwm_fast": pwm_fast_wave,
"pluck_synth": pluck_wave, "organ_synth": organ_wave,
"strings_synth": strings_wave, "piano_synth": piano_wave,
"strings_synth": strings_wave, "piano_synth": piano_wave, "rhodes_synth": rhodes_wave,
"bass_guitar_synth": bass_guitar_wave, "flute_synth": flute_wave,
"trumpet_synth": trumpet_wave, "clarinet_synth": clarinet_wave,
"marimba_synth": marimba_wave, "oboe_synth": oboe_wave,
@@ -2457,34 +2543,36 @@ def _synth_tabla_ke(n_samples):
def _synth_dhol_dagga(n_samples):
"""Dhol dagga — heavy bass side hit with thick stick.
"""Dhol dagga — thunderous bass side hit with thick stick.
The dhol's bass head is thick goatskin, hit with a heavy curved
stick (dagga). Massive low-end punch, the sound of bhangra.
stick (dagga). Massive, thunderous low-end the kind of hit
you feel in your chest before you hear it.
"""
t = numpy.arange(n_samples, dtype=numpy.float32) / SAMPLE_RATE
# Heavy membrane thud
thump_len = min(int(SAMPLE_RATE * 0.08), n_samples)
# Heavy membrane thud — longer, wider band
thump_len = min(int(SAMPLE_RATE * 0.12), n_samples)
thump_raw = _noise(thump_len)
if thump_len > 20:
bl, al = scipy.signal.butter(2, [30, 180], btype='band', fs=SAMPLE_RATE)
bl, al = scipy.signal.butter(2, [25, 150], btype='band', fs=SAMPLE_RATE)
thump = scipy.signal.lfilter(bl, al, numpy.pad(thump_raw, (0, max(0, n_samples - thump_len))))[:thump_len]
else:
thump = thump_raw
thump *= _exp_decay(thump_len, 15) * 1.0
# Deep pitched body — lower than tabla bayan
freq = 50 + 60 * numpy.exp(-20 * t)
thump *= _exp_decay(thump_len, 10) * 1.2
# Deep pitched body with pitch sweep — thunderous boom
freq = 45 + 80 * numpy.exp(-15 * t)
phase = 2 * numpy.pi * numpy.cumsum(freq) / SAMPLE_RATE
body = numpy.sin(phase) * _exp_decay(n_samples, 8) * 0.9
# Sub boom
sub = _sine_f32(35, n_samples) * _exp_decay(n_samples, 10) * 0.6
# Stick attack — heavier than tabla palm
click_len = min(200, n_samples)
click = _noise(click_len) * _exp_decay(click_len, 60) * 0.5
result = body + sub
body = numpy.sin(phase) * _exp_decay(n_samples, 5) * 1.0
# Massive sub boom — sustained
sub = _sine_f32(30, n_samples) * _exp_decay(n_samples, 4) * 0.8
sub2 = _sine_f32(45, n_samples) * _exp_decay(n_samples, 6) * 0.5
# Heavy stick impact
click_len = min(300, n_samples)
click = _noise(click_len) * _exp_decay(click_len, 40) * 0.6
result = body + sub + sub2
result[:thump_len] += thump
result[:click_len] += click
return numpy.tanh(result * 1.5)
return numpy.tanh(result * 1.8)
def _synth_dhol_tilli(n_samples):
@@ -2516,10 +2604,10 @@ def _synth_dhol_tilli(n_samples):
def _synth_dhol_both(n_samples):
"""Dhol both sides — full power bhangra hit."""
dagga = _synth_dhol_dagga(n_samples) * 0.6
tilli = _synth_dhol_tilli(n_samples) * 0.5
return numpy.tanh(dagga + tilli)
"""Dhol both sides — full power bhangra hit. Thunderous."""
dagga = _synth_dhol_dagga(n_samples) * 0.7
tilli = _synth_dhol_tilli(n_samples) * 0.45
return numpy.tanh((dagga + tilli) * 1.2)
def _synth_dholak_ge(n_samples):
@@ -2704,62 +2792,91 @@ def _synth_doumbek_ka(n_samples):
def _synth_cajon_bass(n_samples):
"""Cajón bass — palm strike on center of the face.
"""Cajón bass — open palm slaps the center of the plywood face.
Deep woody thump. The box resonates like a bass drum but with
a warmer, more wooden character.
Two sounds happening at once: the fleshy THUD of the palm
hitting wood, then the box resonating. The hand sound is
a soft, round impact — skin on plywood — followed by the
hollow chamber boom.
"""
t = numpy.arange(n_samples, dtype=numpy.float32) / SAMPLE_RATE
# Wooden box thump
thump_len = min(int(SAMPLE_RATE * 0.06), n_samples)
# HAND IMPACT — fleshy palm on wood, round and thuddy
hand_len = min(int(SAMPLE_RATE * 0.015), n_samples)
hand_raw = _noise(hand_len)
if hand_len > 10:
# Lowpassed — palm is soft, not bright
bl, al = scipy.signal.butter(2, 1500 / (SAMPLE_RATE / 2), btype='low')
hand = scipy.signal.lfilter(bl, al, numpy.pad(hand_raw, (0, max(0, n_samples - hand_len))))[:hand_len].astype(numpy.float32)
else:
hand = hand_raw
hand *= _exp_decay(hand_len, 100) * 1.0
# BOX RESONANCE — hollow chamber thud
body = numpy.sin(2 * numpy.pi * 75 * t) * _exp_decay(n_samples, 5) * 0.7
box1 = numpy.sin(2 * numpy.pi * 170 * t) * _exp_decay(n_samples, 10) * 0.4
box2 = numpy.sin(2 * numpy.pi * 300 * t) * _exp_decay(n_samples, 16) * 0.2
# Panel flex — deep sub thud
sub = _sine_f32(45, n_samples) * _exp_decay(n_samples, 6) * 0.5
# Broader thump from the air cavity
thump_len = min(int(SAMPLE_RATE * 0.1), n_samples)
thump_raw = _noise(thump_len)
import scipy.signal as _sig
if thump_len > 20:
bl, al = _sig.butter(2, [40, 200], btype='band', fs=SAMPLE_RATE)
thump = _sig.lfilter(bl, al, numpy.pad(thump_raw, (0, max(0, n_samples - thump_len))))[:thump_len].astype(numpy.float32)
bl, al = scipy.signal.butter(2, [40, 350], btype='band', fs=SAMPLE_RATE)
thump = scipy.signal.lfilter(bl, al, numpy.pad(thump_raw, (0, max(0, n_samples - thump_len))))[:thump_len].astype(numpy.float32)
else:
thump = thump_raw
thump *= _exp_decay(thump_len, 18) * 0.8
body = numpy.sin(2 * numpy.pi * 70 * t) * _exp_decay(n_samples, 7) * 0.8
sub = _sine_f32(45, n_samples) * _exp_decay(n_samples, 9) * 0.4
click_len = min(200, n_samples)
click = _noise(click_len) * _exp_decay(click_len, 45) * 0.3
result = body + sub
thump *= _exp_decay(thump_len, 12) * 0.6
result = body + box1 + box2 + sub
result[:thump_len] += thump
result[:click_len] += click
return numpy.tanh(result * 1.3).astype(numpy.float32)
def _synth_cajon_slap(n_samples):
"""Cajón slap — fingers near the top edge, snare wires buzz.
Bright crack with a buzzy rattle from the internal snare wires.
The signature cajón sound — like a snare but woodier.
"""
t = numpy.arange(n_samples, dtype=numpy.float32) / SAMPLE_RATE
# Snare wire buzz
wire = _noise(n_samples) * _exp_decay(n_samples, 18) * 0.6
import scipy.signal as _sig
bl, al = _sig.butter(2, [1500, 6000], btype='band', fs=SAMPLE_RATE)
wire = _sig.lfilter(bl, al, wire).astype(numpy.float32) * 1.2
# Wood body
body = numpy.sin(2 * numpy.pi * 200 * t) * _exp_decay(n_samples, 22) * 0.4
# Sharp slap
slap_len = min(int(SAMPLE_RATE * 0.008), n_samples)
slap = _noise(slap_len) * _exp_decay(slap_len, 200) * 0.8
result = body + wire
result[:slap_len] += slap
result[:hand_len] += hand
return numpy.tanh(result * 1.5).astype(numpy.float32)
def _synth_cajon_slap(n_samples):
"""Cajón slap — fingers near the top edge, pure wood crack.
No snare wires. Just the sharp crack of fingers on the plywood
edge with the box resonance underneath. Dry and woody.
"""
t = numpy.arange(n_samples, dtype=numpy.float32) / SAMPLE_RATE
# Wood panel resonance — boxy mid
body = numpy.sin(2 * numpy.pi * 240 * t) * _exp_decay(n_samples, 28) * 0.4
box = numpy.sin(2 * numpy.pi * 400 * t) * _exp_decay(n_samples, 38) * 0.2
box2 = numpy.sin(2 * numpy.pi * 600 * t) * _exp_decay(n_samples, 50) * 0.1
# Sharp edge slap — fingers on plywood
slap_len = min(int(SAMPLE_RATE * 0.004), n_samples)
slap = _noise(slap_len) * _exp_decay(slap_len, 300) * 1.0
result = body + box + box2
result[:slap_len] += slap
return numpy.tanh(result * 1.8).astype(numpy.float32)
def _synth_cajon_slap_snare(n_samples):
"""Cajón slap with snare wires — the buzzy version.
Same edge slap but with internal snare wires rattling.
"""
wood = _synth_cajon_slap(n_samples)
# Add snare wire buzz on top
wire_len = min(int(SAMPLE_RATE * 0.06), n_samples)
wire = _noise(wire_len) * _exp_decay(wire_len, 20) * 0.45
if wire_len > 20:
bl, al = scipy.signal.butter(2, [1500, 5000], btype='band', fs=SAMPLE_RATE)
wire = scipy.signal.lfilter(bl, al, numpy.pad(wire, (0, max(0, n_samples - wire_len))))[:wire_len].astype(numpy.float32)
result = wood.copy()
result[:wire_len] += wire
return numpy.tanh(result * 1.2).astype(numpy.float32)
def _synth_cajon_tap(n_samples):
"""Cajón tap — light fingertip on the face. Ghost note."""
n = min(n_samples, int(SAMPLE_RATE * 0.04))
"""Cajón tap — light fingertip on the plywood face. Ghost note."""
n = min(n_samples, int(SAMPLE_RATE * 0.05))
t = numpy.arange(n, dtype=numpy.float32) / SAMPLE_RATE
tap = numpy.sin(2 * numpy.pi * 300 * t) * _exp_decay(n, 35) * 0.3
pop = _noise(min(50, n)) * _exp_decay(min(50, n), 250) * 0.5
result = tap
result[:min(50, n)] += pop
# Finger on wood — hollow tap
tap = numpy.sin(2 * numpy.pi * 280 * t) * _exp_decay(n, 40) * 0.3
box = numpy.sin(2 * numpy.pi * 450 * t) * _exp_decay(n, 55) * 0.12
pop = _noise(min(60, n)) * _exp_decay(min(60, n), 200) * 0.4
result = tap + box
result[:min(60, n)] += pop
out = numpy.zeros(n_samples, dtype=numpy.float32)
out[:n] = numpy.tanh(result * 1.5)
return out
@@ -3183,6 +3300,7 @@ def _render_drum_hit(sound_value, n_samples):
# Cajon
DrumSound.CAJON_BASS.value: lambda n: _synth_cajon_bass(n),
DrumSound.CAJON_SLAP.value: lambda n: _synth_cajon_slap(n),
DrumSound.CAJON_SLAP_SNARE.value: lambda n: _synth_cajon_slap_snare(n),
DrumSound.CAJON_TAP.value: lambda n: _synth_cajon_tap(n),
# Metal kit
DrumSound.METAL_KICK.value: lambda n: _synth_metal_kick(n),
@@ -4521,12 +4639,6 @@ def _render_notes_to_buf(notes, buf, samples_per_beat, total_samples,
vel_cutoff = vel_to_filter * vel_scale + 1000
mixed = _apply_lowpass(mixed, vel_cutoff, q=filter_q)
end = min(start + len(mixed), total_samples)
# Choke: fade out any existing signal at this point
# so new notes don't pile up on previous tails
choke_len = min(int(SAMPLE_RATE * 0.003), start)
if choke_len > 0:
fade = numpy.linspace(1.0, 0.0, choke_len).astype(numpy.float32)
buf[start - choke_len:start] *= fade
buf[start:end] += mixed[:end - start] * volume * vel_scale
# Spread detuned oscillators into stereo L/R
if detune_up is not None and stereo_buf is not None:
@@ -4893,6 +5005,7 @@ def render_score(score):
# Cajon — centered (single instrument)
DrumSound.CAJON_BASS.value: 0.0,
DrumSound.CAJON_SLAP.value: 0.0,
DrumSound.CAJON_SLAP_SNARE.value: 0.0,
DrumSound.CAJON_TAP.value: 0.1,
# Metal kit
DrumSound.METAL_KICK.value: 0.0,
+7 -8
View File
@@ -17,13 +17,11 @@ INSTRUMENTS = {
"synth": "piano_synth", "envelope": "none",
"vel_to_filter": 3000,
},
"electric_piano": { # Rhodes/Wurlitzer
"synth": "fm", "envelope": "piano",
"fm_ratio": 1.0, "fm_index": 2.0,
"detune": 6, "chorus": 0.2, "chorus_rate": 1.0,
"lowpass": 4000, "saturation": 0.15,
"tremolo_depth": 0.15, "tremolo_rate": 4.5,
"analog": 0.2,
"electric_piano": { # Rhodes
"synth": "rhodes_synth", "envelope": "none",
"chorus": 0.15, "chorus_rate": 1.0,
"tremolo_depth": 0.12, "tremolo_rate": 4.5,
"analog": 0.15,
},
"organ": {
"synth": "organ_synth", "envelope": "organ",
@@ -558,8 +556,9 @@ class DrumSound(Enum):
DOUMBEK_KA = 114 # muted edge slap
# Cajon sounds
CAJON_BASS = 108 # center of face, deep thump
CAJON_SLAP = 109 # top edge, snare wires buzz
CAJON_SLAP = 109 # top edge, wood crack (no snare wires)
CAJON_TAP = 110 # light finger tap
CAJON_SLAP_SNARE = 111 # top edge with snare wires engaged
# Metal kit — tighter, punchier, more attack
METAL_KICK = 105 # clicky, punchy, tight
METAL_SNARE = 106 # crack, bright, cutting
Generated
+1 -1
View File
@@ -690,7 +690,7 @@ wheels = [
[[package]]
name = "pytheory"
version = "0.39.3"
version = "0.40.0"
source = { editable = "." }
dependencies = [
{ name = "scipy", version = "1.15.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" },