Compare commits

..

9 Commits

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

All new synths represented in pytheory demo random rotation.

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

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

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

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

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

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

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

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-27 12:22:36 -04:00
10 changed files with 906 additions and 16 deletions
+30
View File
@@ -2,6 +2,36 @@
All notable changes to PyTheory are documented here.
## 0.36.1
- **7 new instrument synths:** pedal steel guitar, theremin, kalimba/thumb
piano, steel drum/pan, accordion (musette reeds), didgeridoo (drone +
shifting formants), bagpipes (chanter reed)
- **9 new demo moods** in ``pytheory demo``: Theremin Noir, Caribbean,
Accordion Waltz, Kalimba Dreams, Outback Drone, Highland, Nashville
Tears, Tabla Fusion
- Improved existing songs with dedicated instrument synths
- 41 synth waveforms, 26+ songs, 21 demo moods
## 0.36.0
- **Banjo synth** — steel strings on drum-head body, nasal twang,
fast decay with membrane resonance
- **Mandolin synth** — paired steel strings (natural chorus from
doubled courses), bright body resonance
- **Ukulele synth** — nylon strings, small mid-heavy body, shorter
sustain than guitar
- **Cajón drums** — bass (woody box thump), slap (snare wire buzz),
tap (ghost note). 3 patterns: cajon, cajon rumba, cajon folk
- **Vocal/formant synth** — LF glottal model, 5 Peterson & Barney
formant peaks, jitter/shimmer, consonant onsets, per-note lyrics.
Presets: vocal, choir
- **Granular synthesis** — grain cloud engine with scatter, pitch
variation, Hanning windows. Presets: granular_pad, granular_texture
- **Strum sweep** — subtle grace notes before chord hit for natural
strum feel on all fretboard instruments
- Mandola preset, 34 synth waveforms, 26 songs
## 0.35.0
- **8.5x faster import** — dropped pytuning/sympy, lazy-load scipy.
+1 -1
View File
@@ -77,7 +77,7 @@ What's Inside
numbers), scale recommendation, modulation, voice leading
- **Sequencing** — Score, Parts, arpeggiator, legato/glide, velocity,
swing, humanize, tempo changes, song sections with repeat
- **Synthesis**30 waveforms (including Karplus-Strong pluck, Hammond organ,
- **Synthesis**41 waveforms (including Karplus-Strong pluck, Hammond organ,
bowed string, and 14 dedicated instrument synths), 10 envelopes, 40+
instrument presets, configurable FM, sub-oscillator, noise layer, filter
envelope, velocity-to-brightness, analog oscillator drift, detune, stereo
+272 -9
View File
@@ -1315,13 +1315,13 @@ def journey():
# ── Drone — runs the entire piece ──
tanpura = score.part("tanpura", synth="strings_synth", envelope="pad",
detune=3, lowpass=1000, volume=0.12,
reverb=0.5, reverb_type=REV)
reverb=0.6, reverb_type=REV)
for _ in range(40):
tanpura.add("A2", Duration.WHOLE)
# ── Bars 1-8: Piano alone, then cello ──
piano = score.part("piano", instrument="piano", volume=0.35,
reverb=0.35, reverb_type=REV)
reverb=0.6, reverb_type=REV)
for notes in [
["A2","E3","A3","C4","E4","C4","A3","E3"],
["F2","C3","F3","A3","C4","A3","F3","C3"],
@@ -1336,7 +1336,7 @@ def journey():
piano.add(n, Duration.EIGHTH, velocity=68)
cello = score.part("cello", instrument="cello", volume=0.2,
reverb=0.4, reverb_type=REV)
reverb=0.55, reverb_type=REV)
cello.rest(Duration.WHOLE)
for note, dur, vel in [
("A3", 4.0, 55), ("C4", 4.0, 58),
@@ -1347,11 +1347,11 @@ def journey():
# ── Bars 9-16: Harp + oboe + flute + djembe ──
harp = score.part("harp", instrument="harp", volume=0.28,
reverb=0.45, reverb_type=REV)
reverb=0.6, reverb_type=REV)
oboe = score.part("oboe", instrument="oboe", volume=0.22,
reverb=0.4, reverb_type=REV)
reverb=0.55, reverb_type=REV)
flute = score.part("flute", instrument="flute", volume=0.18,
reverb=0.4, reverb_type=REV)
reverb=0.55, reverb_type=REV)
for _ in range(8):
harp.rest(Duration.WHOLE)
for notes in [
@@ -1383,7 +1383,7 @@ def journey():
# ── Bars 15-20: Sitar + tabla ──
sitar = score.part("sitar", instrument="sitar", volume=0.2,
reverb=0.35, reverb_type=REV)
reverb=0.6, reverb_type=REV)
for _ in range(14):
sitar.rest(Duration.WHOLE)
for note, dur, vel in [
@@ -1406,7 +1406,7 @@ def journey():
# Total bars before EDM: 8 piano + 6 harp + 6 djembe + 4 tabla + 9 solo = 33
edm_start = 33
pad = score.part("pad", instrument="synth_pad", volume=0.18,
reverb=0.45, reverb_type=REV,
reverb=0.6, reverb_type=REV,
sidechain=0.6, sidechain_release=0.15)
for _ in range(edm_start):
pad.rest(Duration.WHOLE)
@@ -1551,6 +1551,267 @@ def journey():
play_song(score, "Journey — Piano → World → Sitar EDM (Taj Mahal)")
def epic_bhairav():
"""Epic Bhairav — orchestral + choir + tabla with extended solo finale."""
shruti = SYSTEMS["shruti"]
score = Score("4/4", bpm=90, system=shruti)
REV = "taj_mahal"
T3 = 1.0 / 12.0
T9 = 1.0 / 9.0
ts = TonedScale(system=shruti, tonic=Tone("Sa", octave=4, system=shruti))
bh = list(ts["bhairav"].tones)
S, kR, G, M, P, kD, N, S2 = bh
NA = DrumSound.TABLA_NA
DH = DrumSound.TABLA_DHA
TT = DrumSound.TABLA_TIT
KE = DrumSound.TABLA_KE
GB = DrumSound.TABLA_GE_BEND
GE = DrumSound.TABLA_GE
DJB = DrumSound.DJEMBE_BASS
DJT = DrumSound.DJEMBE_TONE
DJS = DrumSound.DJEMBE_SLAP
# Tanpura
tanpura = score.part("tanpura", synth="strings_synth", envelope="pad",
detune=3, lowpass=900, volume=0.14, reverb=0.4, reverb_type=REV)
tanpura_pa = score.part("tanpura_pa", synth="strings_synth", envelope="pad",
detune=3, lowpass=1200, volume=0.1, reverb=0.4, reverb_type=REV)
sa = Tone("Sa", octave=3, system=shruti)
pa = Tone("Pa", octave=3, system=shruti)
for _ in range(34):
tanpura.add(sa, Duration.WHOLE)
tanpura_pa.add(pa, Duration.WHOLE)
# Timpani
timp = score.part("timp", instrument="timpani")
timp.roll(Tone("Sa", octave=2, system=shruti), Duration.WHOLE,
velocity_start=20, velocity_end=90, speed=0.125)
timp.add(Tone("Sa", octave=2, system=shruti), Duration.HALF, velocity=105)
timp.rest(Duration.HALF)
for _ in range(8):
timp.rest(Duration.WHOLE)
timp.roll(Tone("Sa", octave=2, system=shruti), Duration.WHOLE,
velocity_start=25, velocity_end=115, speed=0.125)
timp.add(Tone("Sa", octave=2, system=shruti), Duration.HALF, velocity=120)
timp.add(Tone("Pa", octave=2, system=shruti), Duration.HALF, velocity=115)
# Choir — bar 3
choir = score.part("choir", synth="vocal_synth", envelope="pad",
detune=8, spread=0.4, reverb=0.4, reverb_type=REV, volume=0.2)
for _ in range(2):
choir.rest(Duration.WHOLE)
for tone, dur, lyric, vel in [
(S, 4.0, "ah", 60), (M, 4.0, "oh", 62), (P, 4.0, "ah", 68),
(S, 4.0, "ee", 65), (kD, 4.0, "oh", 70), (P, 4.0, "ah", 72),
]:
choir.add(tone, dur, velocity=vel, lyric=lyric)
# Bansuri — bar 5
bansuri = score.part("bansuri", instrument="flute", volume=0.22,
reverb=0.4, reverb_type=REV)
for _ in range(4):
bansuri.rest(Duration.WHOLE)
for tone, dur, vel in [
(P, 2.0, 58), (kD, 1.0, 50), (P, 1.0, 55),
(M, 2.0, 55), (G, 1.0, 50), (kR, 1.0, 48), (S, 4.0, 58),
]:
bansuri.add(tone, dur, velocity=vel)
# Cello — bar 3
cello = score.part("cello", instrument="cello", volume=0.22, reverb=0.4, reverb_type=REV)
for _ in range(2):
cello.rest(Duration.WHOLE)
for name, dur, vel in [
("Sa", 4.0, 55), ("Ma", 4.0, 52), ("Pa", 4.0, 58),
("Sa", 4.0, 55), ("komal Dha", 4.0, 58), ("Pa", 4.0, 55),
]:
cello.add(Tone(name, octave=2, system=shruti), dur, velocity=vel)
# Sitar — bar 9
sitar = score.part("sitar", instrument="sitar", volume=0.25, reverb=0.4, reverb_type=REV)
for _ in range(8):
sitar.rest(Duration.WHOLE)
for tone, dur, vel in [
(S, 1.0, 72), (kR, 0.5, 62), (S, 0.5, 68), (G, 2.0, 78),
(M, 1.0, 72), (P, 2.0, 82), (kD, 0.5, 65), (P, 1.0, 75),
(M, 0.5, 65), (G, 0.5, 68), (kR, 0.5, 60), (S, 2.0, 78),
(kR, 0.25, 62), (G, 0.25, 65), (M, 0.25, 70), (P, 0.25, 75),
(kD, 0.25, 70), (N, 0.25, 78), (S2, 0.5, 88),
(N, 0.25, 68), (kD, 0.25, 62), (P, 0.5, 68),
(M, 0.5, 62), (G, 0.5, 65), (kR, 0.5, 58), (S, 2.0, 80),
]:
sitar.add(tone, dur, velocity=vel)
# Strings — bar 13
strings = score.part("strings", instrument="string_ensemble", volume=0.18,
reverb=0.4, reverb_type=REV)
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)]:
strings.add(Tone(name, octave=3, system=shruti), dur, velocity=vel)
# Harp — bar 14
harp = score.part("harp", instrument="harp", volume=0.15, reverb=0.4, reverb_type=REV)
for _ in range(13):
harp.rest(Duration.WHOLE)
for name in ["Sa", "komal Ga", "Pa", "Sa", "Pa", "komal Ga", "Sa", "Sa"]:
oct = 4 if name == "Sa" and harp.total_beats > 55 else 3
harp.add(Tone(name, octave=oct, system=shruti), Duration.EIGHTH, velocity=50)
# Drums
silence = Pattern(name="s", time_signature="4/4", beats=16.0, hits=[])
score.add_pattern(silence, repeats=1)
p_dj = Pattern(name="dj", time_signature="4/4", beats=8.0, hits=[
_Hit(DJB, 0.0, 45), _Hit(DJT, 1.0, 38), _Hit(DJT, 1.5, 32),
_Hit(DJS, 2.0, 42), _Hit(DJT, 3.0, 38),
_Hit(DJB, 4.0, 50), _Hit(DJT, 5.0, 42), _Hit(DJT, 5.5, 35),
_Hit(DJS, 6.0, 48), _Hit(DJT, 6.5, 32), _Hit(DJS, 7.0, 45),
])
score.add_pattern(p_dj, repeats=2)
p_tab = Pattern(name="tab", time_signature="4/4", beats=8.0, hits=[
_Hit(DH, 0.0, 82), _Hit(TT, 0.5, 30), _Hit(NA, 1.0, 65),
_Hit(NA, 2.0, 60), _Hit(DH, 3.0, 82),
_Hit(DH, 4.0, 88), _Hit(TT, 4.25, 32), _Hit(TT, 4.5, 35),
_Hit(NA, 5.0, 68), _Hit(TT, 5.5, 30), _Hit(NA, 6.0, 65),
_Hit(DH, 7.0, 88),
])
score.add_pattern(p_tab, repeats=3)
# Extended tabla finale — whisper → ghosts → call/response → blazing
p_f1 = Pattern(name="f1", time_signature="4/4", beats=8.0, hits=[
_Hit(DH, 0.0, 78), _Hit(NA, 2.0, 55),
_Hit(DH, 4.0, 82), _Hit(TT, 5.0, 30), _Hit(NA, 5.5, 52),
_Hit(DH, 7.0, 78),
])
score.add_pattern(p_f1, repeats=1)
p_f2 = Pattern(name="f2", time_signature="4/4", beats=8.0, hits=[
_Hit(DH, 0.0, 95), _Hit(TT, 0.25, 35), _Hit(TT, 0.5, 38),
_Hit(NA, 1.0, 70), _Hit(TT, 1.25, 30), _Hit(NA, 2.0, 65),
_Hit(TT, 2.5, 35), _Hit(DH, 3.0, 90),
_Hit(DH, 4.0, 98), _Hit(TT, 4.25, 38), _Hit(TT, 4.5, 42),
_Hit(NA, 5.0, 75), _Hit(KE, 5.5, 40), _Hit(NA, 6.0, 70),
_Hit(KE, 6.5, 42), _Hit(DH, 7.0, 100), _Hit(GB, 7.5, 92),
])
score.add_pattern(p_f2, repeats=1)
p_f3 = Pattern(name="f3", time_signature="4/4", beats=8.0, hits=[
_Hit(NA, 0.0, 112), _Hit(NA, 0.25, 58), _Hit(TT, 0.5, 40), _Hit(NA, 0.75, 105),
_Hit(GE, 1.0, 105), _Hit(GE, 1.25, 52), _Hit(GB, 1.5, 95), _Hit(GE, 1.75, 48),
_Hit(NA, 2.0, 115), _Hit(TT, 2.125, 32), _Hit(TT, 2.25, 38),
_Hit(NA, 2.5, 108), _Hit(TT, 2.625, 35), _Hit(TT, 2.75, 42),
_Hit(GB, 3.0, 115), _Hit(KE, 3.25, 52), _Hit(GE, 3.5, 70),
_Hit(DH, 4.0, 118),
*[_Hit(TT if i % 2 == 0 else KE, 5.0 + i * T9, 40 + i * 5) for i in range(9)],
_Hit(DH, 7.0, 120),
])
score.add_pattern(p_f3, repeats=1)
# Part 3.5: polyrhythm — space and conversation, not density
T5 = 4.0 / 5.0
p_poly = Pattern(name="poly", time_signature="4/4", beats=16.0, hits=[
# Bar 1: single Dha, let reverb ring. Bayan answers.
_Hit(DH, 0.0, 95),
_Hit(GB, 3.0, 88),
# Bar 2: one 5-group phrase, then breathe
_Hit(NA, 4.0, 75), _Hit(TT, 4.0 + T5, 42),
_Hit(NA, 4.0 + 2*T5, 70), _Hit(TT, 4.0 + 3*T5, 40),
_Hit(DH, 4.0 + 4*T5, 88),
# Bar 3: bayan, pause, one floating 9-group
_Hit(GB, 8.0, 100),
_Hit(NA, 9.0, 62),
*[_Hit(TT if i % 2 == 0 else KE, 10.0 + i * T9, 35 + i * 4)
for i in range(9)],
_Hit(DH, 11.0, 105),
# Bar 4: simple question-answer into sam
_Hit(DH, 12.0, 100), _Hit(NA, 12.5, 62),
_Hit(GE, 13.0, 88),
_Hit(NA, 14.0, 72), _Hit(TT, 14.25, 40), _Hit(NA, 14.5, 70),
_Hit(DH, 15.0, 112), _Hit(GB, 15.5, 105),
])
score.add_pattern(p_poly, repeats=1)
p_f4 = Pattern(name="f4", time_signature="4/4", beats=12.0, hits=[
*[_Hit(TT, 0.0 + i * T3, 38 + i * 2) for i in range(12)],
_Hit(DH, 1.0, 118), _Hit(GB, 1.5, 110),
_Hit(NA, 2.0, 112), _Hit(KE, 2.125, 48), _Hit(NA, 2.25, 108),
_Hit(KE, 2.375, 50), _Hit(NA, 2.5, 110), _Hit(KE, 2.625, 52), _Hit(NA, 2.75, 115),
_Hit(DH, 3.0, 120),
*[_Hit(TT, 3.5 + i * T3, 30 + i * 4) for i in range(18)],
_Hit(DH, 5.0, 122), _Hit(DH, 5.25, 118), _Hit(GB, 5.5, 115),
_Hit(GE, 6.0, 90), _Hit(GE, 7.0, 88),
*[_Hit(NA if i % 3 == 0 else TT, 6.0 + i * (2.0/9.0), 42 + (i%3)*15) for i in range(9)],
_Hit(DH, 8.0, 110), _Hit(NA, 8.25, 75), _Hit(TT, 8.5, 50),
_Hit(KE, 8.75, 55), _Hit(DH, 9.0, 105),
_Hit(DH, 9.25, 115), _Hit(NA, 9.5, 80), _Hit(TT, 9.75, 55),
_Hit(KE, 10.0, 60), _Hit(DH, 10.25, 110),
_Hit(DH, 10.5, 122), _Hit(NA, 10.75, 85), _Hit(TT, 11.0, 60),
_Hit(KE, 11.25, 65), _Hit(DH, 11.5, 127),
_Hit(GB, 11.875, 127),
])
score.add_pattern(p_f4, repeats=1)
score.set_drum_effects(reverb=0.4, reverb_type=REV)
play_song(score, "Epic Bhairav — Orchestra + Choir + Tabla (22-Shruti JI)")
def acoustic_ensemble():
"""Acoustic Ensemble — guitar, ukulele, mandolin, cajón."""
import random
from pytheory import Fretboard
random.seed(7)
score = Score("4/4", bpm=115)
fb_g = Fretboard.guitar()
guitar = score.part("guitar", instrument="acoustic_guitar", fretboard=fb_g,
reverb=0.3, reverb_type="plate", humanize=0.2, pan=-0.3)
fb_u = Fretboard.ukulele()
uke = score.part("uke", instrument="ukulele", fretboard=fb_u,
reverb=0.25, reverb_type="plate", humanize=0.25, pan=0.3)
fb_m = Fretboard.mandolin()
mando = score.part("mando", instrument="mandolin", fretboard=fb_m,
reverb=0.25, reverb_type="plate", humanize=0.2, pan=0.15)
for sym in ["C", "G", "Am", "F"] * 3:
vd = random.randint(75, 95)
vu = random.randint(58, 78)
guitar.strum(sym, Duration.QUARTER, direction="down", velocity=vd)
guitar.strum(sym, Duration.EIGHTH, direction="up", velocity=vu)
guitar.strum(sym, Duration.EIGHTH, direction="down", velocity=vd - 8)
guitar.strum(sym, Duration.QUARTER, direction="up", velocity=vu)
guitar.strum(sym, Duration.QUARTER, direction="down", velocity=vd)
vd2 = random.randint(65, 88)
vu2 = random.randint(50, 72)
uke.rest(Duration.EIGHTH)
uke.strum(sym, Duration.EIGHTH, direction="up", velocity=vu2)
uke.strum(sym, Duration.QUARTER, direction="down", velocity=vd2)
uke.strum(sym, Duration.EIGHTH, direction="up", velocity=vu2)
uke.strum(sym, Duration.EIGHTH, direction="down", velocity=vd2 - 5)
uke.strum(sym, Duration.QUARTER, direction="up", velocity=vu2)
mando.strum(sym, Duration.EIGHTH, direction="down",
velocity=random.randint(65, 82))
mando.strum(sym, Duration.EIGHTH, direction="up",
velocity=random.randint(55, 72))
mando.strum(sym, Duration.EIGHTH, direction="down",
velocity=random.randint(65, 82))
mando.rest(Duration.EIGHTH)
mando.strum(sym, Duration.EIGHTH, direction="up",
velocity=random.randint(55, 72))
mando.strum(sym, Duration.EIGHTH, direction="down",
velocity=random.randint(68, 85))
mando.strum(sym, Duration.QUARTER, direction="down",
velocity=random.randint(70, 85))
score.drums("cajon", repeats=6)
score.set_drum_effects(reverb=0.15)
play_song(score, "Acoustic Ensemble — Guitar, Uke, Mandolin, Cajón")
SONGS = {
"1": ("Bossa Nova in A minor", bossa_nova_girl),
"2": ("Bebop in Bb major", bebop_in_bb),
@@ -1576,6 +1837,8 @@ SONGS = {
"22": ("Greensleeves (Renaissance Lute)", greensleeves),
"23": ("Tabla Solo (Raga Yaman)", tabla_solo_yaman),
"24": ("Journey (Western → World → Indian)", journey),
"25": ("Epic Bhairav (Orchestral + Tabla)", epic_bhairav),
"26": ("Acoustic Ensemble (Guitar+Uke+Mando+Cajón)", acoustic_ensemble),
}
if __name__ == "__main__":
@@ -1589,7 +1852,7 @@ if __name__ == "__main__":
print(f" {key:>2}. {name}")
print()
choice = input(" Pick a song (1-24, or 'all'): ").strip()
choice = input(" Pick a song (1-26, or 'all'): ").strip()
print()
if choice == "all":
+1 -1
View File
@@ -1,6 +1,6 @@
[project]
name = "pytheory"
version = "0.35.1"
version = "0.36.1"
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.35.1"
__version__ = "0.36.1"
from .tones import Tone, Interval
from .systems import System, SYSTEMS, TET
+48
View File
@@ -299,6 +299,54 @@ def cmd_demo(args):
"lead": ("trumpet_synth", "bowed", 0.3, 0.2),
"pad": ("piano_synth", "none", -0.2),
"bass_lp": 600, "reverb_type": "plate"},
{"name": "Theremin Noir", "key": ("A", "minor"), "drums": "hip hop",
"fill": "rock", "bpm": 85,
"prog": ("i", "iv", "V", "i"),
"lead": ("theremin_synth", "pad", 0.4, 0.0),
"pad": ("granular_synth", "pad", 0.0),
"bass_lp": 300, "reverb_type": "cave"},
{"name": "Caribbean", "key": ("C", "major"), "drums": "reggae",
"fill": "reggae", "bpm": 110,
"prog": ("I", "IV", "V", "IV"),
"lead": ("steel_drum_synth", "none", 0.25, 0.3),
"pad": ("acoustic_guitar_synth", "none", -0.3),
"bass_lp": 500, "reverb_type": "plate"},
{"name": "Accordion Waltz", "key": ("D", "minor"), "drums": "waltz",
"fill": "jazz", "bpm": 88,
"prog": ("i", "iv", "V", "i"),
"lead": ("accordion_synth", "organ", 0.2, 0.1),
"pad": ("strings_synth", "pad", -0.2),
"bass_lp": 500, "reverb_type": "plate"},
{"name": "Kalimba Dreams", "key": ("G", "major"), "drums": "cajon folk",
"fill": "bossa nova", "bpm": 95,
"prog": ("I", "vi", "IV", "V"),
"lead": ("kalimba_synth", "none", 0.35, 0.2),
"pad": ("piano_synth", "none", -0.2),
"bass_lp": 400, "reverb_type": "taj_mahal"},
{"name": "Outback Drone", "key": ("E", "minor"), "drums": "djembe",
"fill": "afrobeat", "bpm": 70,
"prog": ("i", "iv", "i", "V"),
"lead": ("didgeridoo_synth", "pad", 0.3, 0.0),
"pad": ("granular_synth", "pad", 0.0),
"bass_lp": 200, "reverb_type": "cave"},
{"name": "Highland", "key": ("A", "minor"), "drums": "flamenco",
"fill": "rock", "bpm": 95,
"prog": ("i", "iv", "V", "i"),
"lead": ("bagpipe_synth", "organ", 0.15, 0.0),
"pad": ("strings_synth", "pad", -0.2),
"bass_lp": 400, "reverb_type": "cathedral"},
{"name": "Nashville Tears", "key": ("G", "major"), "drums": "country",
"fill": "rock", "bpm": 85,
"prog": ("I", "IV", "V", "IV"),
"lead": ("pedal_steel_synth", "strings", 0.35, 0.2),
"pad": ("piano_synth", "none", -0.3),
"bass_lp": 500, "reverb_type": "spring"},
{"name": "Tabla Fusion", "key": ("E", "minor"), "drums": "teental",
"fill": "rock", "bpm": 120,
"prog": ("i", "iv", "V", "i"),
"lead": ("sitar_synth", "none", 0.3, 0.2),
"pad": ("vocal_synth", "pad", 0.0),
"bass_lp": 400, "reverb_type": "taj_mahal"},
]
mood = random.choice(moods)
+439
View File
@@ -1130,6 +1130,359 @@ def granular_wave(hz, peak=SAMPLE_PEAK, n_samples=SAMPLE_RATE,
return (peak * out).astype(numpy.int16)
def pedal_steel_wave(hz, peak=SAMPLE_PEAK, n_samples=SAMPLE_RATE):
"""Pedal steel guitar — the Nashville crying sound.
Sustained steel string with natural portamento character,
very smooth, lots of harmonics, and a singing quality from
the bar sliding on the strings.
"""
t = numpy.arange(n_samples, dtype=numpy.float64) / SAMPLE_RATE
rng = numpy.random.default_rng(int(hz * 100) % 2**31)
# Slow, singing vibrato — the bar wobbling on the strings
vib = hz * 0.002 * numpy.sin(2 * numpy.pi * 4.0 * t)
# Rich harmonics — steel bar gives a clear, singing tone
wave = numpy.zeros(n_samples, dtype=numpy.float64)
for n in range(1, 12):
f_n = hz * n
if f_n >= SAMPLE_RATE / 2:
break
amp = 1.0 / n * numpy.exp(-0.08 * n)
phase = rng.uniform(0, 2 * numpy.pi)
wave += amp * numpy.sin(2 * numpy.pi * (f_n + vib * n) * t + phase)
# Long sustain envelope
wave *= numpy.exp(-0.8 * t)
mx = numpy.abs(wave).max()
if mx > 0:
wave /= mx
return (peak * wave).astype(numpy.int16)
def theremin_wave(hz, peak=SAMPLE_PEAK, n_samples=SAMPLE_RATE):
"""Theremin — pure sine with natural wobble.
The theremin's sound is a nearly pure sine wave with slight
pitch instability from hand position. The eerie, sci-fi sound
comes from this purity combined with continuous pitch.
"""
t = numpy.arange(n_samples, dtype=numpy.float64) / SAMPLE_RATE
# Natural hand wobble — slightly irregular vibrato
rng = numpy.random.default_rng(int(hz * 100) % 2**31)
wobble = hz * 0.004 * numpy.sin(2 * numpy.pi * 5.8 * t)
wobble += hz * 0.001 * rng.normal(0, 1, n_samples)
wave = numpy.sin(2 * numpy.pi * (hz + wobble) * t)
# Slight 2nd harmonic — real theremins aren't perfectly pure
wave += 0.08 * numpy.sin(2 * numpy.pi * (hz * 2 + wobble * 2) * t)
mx = numpy.abs(wave).max()
if mx > 0:
wave /= mx
return (peak * wave).astype(numpy.int16)
def kalimba_wave(hz, peak=SAMPLE_PEAK, n_samples=SAMPLE_RATE):
"""Kalimba/thumb piano — metal tines on a wooden body.
Bright, bell-like attack with inharmonic overtones from the
metal tines. The wooden resonator gives warmth underneath.
"""
t = numpy.arange(n_samples, dtype=numpy.float64) / SAMPLE_RATE
# Metal tine modes — slightly inharmonic like marimba
wave = numpy.sin(2 * numpy.pi * hz * t) * 0.8
wave += numpy.sin(2 * numpy.pi * hz * 2.92 * t) * 0.25 * numpy.exp(-12 * t)
wave += numpy.sin(2 * numpy.pi * hz * 5.4 * t) * 0.1 * numpy.exp(-20 * t)
# Two-stage decay: bright attack dies fast, fundamental rings
decay = numpy.where(t < 0.1,
numpy.exp(-3 * t),
numpy.exp(-3 * 0.1) * numpy.exp(-1.5 * (t - 0.1)))
wave *= decay
# Wooden body resonance
import scipy.signal as _sig
for center, bw, gain in [(300, 100, 0.2), (600, 120, 0.15)]:
lo, hi = max(20, center - bw), min(SAMPLE_RATE // 2 - 1, center + bw)
if lo < hi:
bp, ap = _sig.butter(2, [lo, hi], btype='band', fs=SAMPLE_RATE)
wave += _sig.lfilter(bp, ap, wave) * gain
mx = numpy.abs(wave).max()
if mx > 0:
wave /= mx
return (peak * wave).astype(numpy.int16)
def steel_drum_wave(hz, peak=SAMPLE_PEAK, n_samples=SAMPLE_RATE):
"""Steel drum/pan — hammered metal with bright, ringing tone.
The steel pan has specific inharmonic partials from the
hand-hammered notes. Bright, tropical, bell-like.
"""
t = numpy.arange(n_samples, dtype=numpy.float64) / SAMPLE_RATE
# Steel pan modes — distinctly metallic
wave = numpy.sin(2 * numpy.pi * hz * t) * 0.7
wave += numpy.sin(2 * numpy.pi * hz * 2.0 * t) * 0.4 * numpy.exp(-5 * t)
wave += numpy.sin(2 * numpy.pi * hz * 3.01 * t) * 0.25 * numpy.exp(-8 * t)
wave += numpy.sin(2 * numpy.pi * hz * 4.1 * t) * 0.15 * numpy.exp(-12 * t)
wave += numpy.sin(2 * numpy.pi * hz * 5.3 * t) * 0.08 * numpy.exp(-18 * t)
# Mallet impact
rng = numpy.random.default_rng(int(hz * 100) % 2**31)
hit_len = min(int(SAMPLE_RATE * 0.008), n_samples)
hit = rng.uniform(-0.2, 0.2, hit_len) * numpy.exp(-numpy.linspace(0, 12, hit_len))
wave[:hit_len] += hit
# Two-stage decay
decay = numpy.where(t < 0.15,
numpy.exp(-4 * t),
numpy.exp(-4 * 0.15) * numpy.exp(-1.2 * (t - 0.15)))
wave *= decay
mx = numpy.abs(wave).max()
if mx > 0:
wave /= mx
return (peak * wave).astype(numpy.int16)
def accordion_wave(hz, peak=SAMPLE_PEAK, n_samples=SAMPLE_RATE):
"""Accordion — bellows-driven free reeds.
Two reeds per note slightly detuned (musette tuning) create
the characteristic beating/tremolo. Rich in harmonics from
the reed vibration.
"""
t = numpy.arange(n_samples, dtype=numpy.float64) / SAMPLE_RATE
rng = numpy.random.default_rng(int(hz * 100) % 2**31)
# Two reeds slightly detuned — musette beating
detune_cents = 8
hz2 = hz * (2 ** (detune_cents / 1200))
# Reed harmonics — rich, like a square-ish wave but warmer
wave = numpy.zeros(n_samples, dtype=numpy.float64)
for reed_hz in [hz, hz2]:
for n in range(1, 10):
f_n = reed_hz * n
if f_n >= SAMPLE_RATE / 2:
break
# Odd harmonics stronger (reed character)
amp = (1.0 / n) * (1.2 if n % 2 == 1 else 0.6)
phase = rng.uniform(0, 2 * numpy.pi)
wave += amp * numpy.sin(2 * numpy.pi * f_n * t + phase)
wave *= 0.5 # normalize for two reeds
# Bellows pressure variation — slow amplitude swell
bellows = 0.85 + 0.15 * numpy.sin(2 * numpy.pi * 0.8 * t)
wave *= bellows
mx = numpy.abs(wave).max()
if mx > 0:
wave /= mx
return (peak * wave).astype(numpy.int16)
def didgeridoo_wave(hz, peak=SAMPLE_PEAK, n_samples=SAMPLE_RATE):
"""Didgeridoo — circular breathing drone through a wooden tube.
Deep fundamental with strong odd harmonics from the cylindrical
bore. The overtone singing technique creates shifting formants.
Buzzy, droning, primal.
"""
t = numpy.arange(n_samples, dtype=numpy.float64) / SAMPLE_RATE
rng = numpy.random.default_rng(int(hz * 100) % 2**31)
# Lip buzz source — rich, raw
phase = numpy.cumsum(2 * numpy.pi * hz / SAMPLE_RATE * numpy.ones(n_samples))
buzz = numpy.zeros(n_samples, dtype=numpy.float64)
for n in range(1, 15):
if hz * n >= SAMPLE_RATE / 2:
break
# Odd harmonics stronger (cylindrical bore, like clarinet)
amp = (1.0 / n) * (1.0 if n % 2 == 1 else 0.4)
buzz += amp * numpy.sin(phase * n + rng.uniform(0, 2 * numpy.pi))
# Shifting formant — the overtone singing effect
# Sweeps slowly between 500Hz and 1500Hz
formant_center = 800 + 400 * numpy.sin(2 * numpy.pi * 0.3 * t)
import scipy.signal as _sig
# Block-process the formant sweep
block = 2048
out = numpy.zeros(n_samples, dtype=numpy.float64)
for i in range(0, n_samples, block):
end = min(i + block, n_samples)
fc = formant_center[(i + end) // 2]
lo = max(20, int(fc - 300))
hi = min(SAMPLE_RATE // 2 - 1, int(fc + 300))
if lo < hi:
bp, ap = _sig.butter(2, [lo, hi], btype='band', fs=SAMPLE_RATE)
seg = _sig.lfilter(bp, ap, buzz[i:end])
out[i:end] = buzz[i:end] * 0.5 + seg * 0.5
else:
out[i:end] = buzz[i:end]
# Breath noise
breath = rng.normal(0, 0.04, n_samples)
out += breath
mx = numpy.abs(out).max()
if mx > 0:
out /= mx
return (peak * out).astype(numpy.int16)
def bagpipe_wave(hz, peak=SAMPLE_PEAK, n_samples=SAMPLE_RATE):
"""Bagpipes — chanter reed with constant drone pressure.
The chanter (melody pipe) uses a double reed like an oboe
but with more buzz and brightness. The constant air pressure
from the bag means no dynamics — always ff.
"""
t = numpy.arange(n_samples, dtype=numpy.float64) / SAMPLE_RATE
rng = numpy.random.default_rng(int(hz * 100) % 2**31)
# Chanter — all harmonics, bright and reedy
wave = numpy.zeros(n_samples, dtype=numpy.float64)
for n in range(1, 18):
f_n = hz * n
if f_n >= SAMPLE_RATE / 2:
break
# Peaked around harmonics 3-7 (the piercing brightness)
amp = (1.0 / n) * numpy.exp(-0.03 * (n - 5) ** 2)
phase = rng.uniform(0, 2 * numpy.pi)
wave += amp * numpy.sin(2 * numpy.pi * f_n * t + phase)
# Reed buzz — more than oboe
reed = rng.normal(0, 0.08, n_samples)
import scipy.signal as _sig
lo = max(20, int(hz * 2))
hi = min(SAMPLE_RATE // 2 - 1, int(hz * 8))
if lo < hi:
br, ar = _sig.butter(2, [lo, hi], btype='band', fs=SAMPLE_RATE)
reed = _sig.lfilter(br, ar, reed).astype(numpy.float64) * 1.5
wave += reed
# Bag pressure wobble — very subtle
bag = 1.0 + 0.02 * numpy.sin(2 * numpy.pi * 1.5 * t)
wave *= bag
mx = numpy.abs(wave).max()
if mx > 0:
wave /= mx
return (peak * wave).astype(numpy.int16)
def banjo_wave(hz, peak=SAMPLE_PEAK, n_samples=SAMPLE_RATE):
"""Banjo — steel strings on a drum-head body.
The banjo's distinctive twang comes from the membrane head
(like a drum skin) instead of a wooden soundboard. This gives
a sharp attack, bright tone, and fast decay with a nasal,
metallic quality. The 5th string drone adds shimmer.
"""
period = int(SAMPLE_RATE / hz)
if period < 2:
period = 2
rng = numpy.random.default_rng(int(hz * 100) % 2**31)
# Steel string — bright, sharp attack
buf = rng.uniform(-0.9, 0.9, period).astype(numpy.float64)
# Minimal filtering — banjo keeps the brightness
for k in range(period - 1):
buf[k] = 0.7 * buf[k] + 0.3 * buf[k + 1]
out = numpy.zeros(n_samples, dtype=numpy.float64)
for i in range(n_samples):
out[i] = buf[i % period]
next_idx = (i + 1) % period
# Moderate decay — drum head rings but shorter than guitar
buf[i % period] = 0.5 * (buf[i % period] + buf[next_idx]) * 0.9988
# Drum-head resonance — nasal, ringy, mid-frequency peaks
# The membrane head rings more than wood — that's the twang
import scipy.signal as _sig
for center, bw, gain in [(600, 200, 0.5), (1500, 300, 0.4), (3000, 500, 0.25)]:
lo = max(20, center - bw)
hi = min(SAMPLE_RATE // 2 - 1, center + bw)
if lo < hi:
bp, ap = _sig.butter(2, [lo, hi], btype='band', fs=SAMPLE_RATE)
out += _sig.lfilter(bp, ap, out) * gain
mx = numpy.abs(out).max()
if mx > 0:
out /= mx
return (peak * out).astype(numpy.int16)
def mandolin_wave(hz, peak=SAMPLE_PEAK, n_samples=SAMPLE_RATE):
"""Mandolin — paired steel strings, bright and ringing.
The mandolin has 4 courses of paired strings, tuned in unison.
The doubled strings create natural chorus. Bright attack from
the plectrum, small body with high-frequency resonance.
"""
period = int(SAMPLE_RATE / hz)
if period < 2:
period = 2
rng = numpy.random.default_rng(int(hz * 100) % 2**31)
# Two strings per course — slightly detuned for natural chorus
buf1 = rng.uniform(-0.8, 0.8, period).astype(numpy.float64)
period2 = max(2, period + rng.integers(-1, 2))
buf2 = rng.uniform(-0.8, 0.8, period2).astype(numpy.float64)
# Light filtering — steel is brighter than nylon
for k in range(period - 1):
buf1[k] = 0.65 * buf1[k] + 0.35 * buf1[k + 1]
for k in range(period2 - 1):
buf2[k] = 0.65 * buf2[k] + 0.35 * buf2[k + 1]
out = numpy.zeros(n_samples, dtype=numpy.float64)
for i in range(n_samples):
s1 = buf1[i % period]
s2 = buf2[i % period2]
out[i] = s1 * 0.55 + s2 * 0.45
next1 = (i + 1) % period
buf1[i % period] = 0.5 * (s1 + buf1[next1]) * 0.9988
next2 = (i + 1) % period2
buf2[i % period2] = 0.5 * (s2 + buf2[next2]) * 0.9988
# Small bright body — higher resonance than guitar
import scipy.signal as _sig
for center, bw, gain in [(500, 120, 0.3), (1000, 200, 0.25), (2000, 300, 0.15)]:
lo = max(20, center - bw)
hi = min(SAMPLE_RATE // 2 - 1, center + bw)
if lo < hi:
bp, ap = _sig.butter(2, [lo, hi], btype='band', fs=SAMPLE_RATE)
out += _sig.lfilter(bp, ap, out) * gain
mx = numpy.abs(out).max()
if mx > 0:
out /= mx
return (peak * out).astype(numpy.int16)
def ukulele_wave(hz, peak=SAMPLE_PEAK, n_samples=SAMPLE_RATE):
"""Ukulele — nylon strings on a small resonant body.
Brighter and thinner than guitar, shorter sustain. The small
body gives a mid-heavy resonance (no deep bass). Nylon strings
have a softer, warmer attack than steel.
"""
period = int(SAMPLE_RATE / hz)
if period < 2:
period = 2
rng = numpy.random.default_rng(int(hz * 100) % 2**31)
# Nylon string — soft noise
buf = rng.uniform(-0.5, 0.5, period).astype(numpy.float64)
for _ in range(5):
for k in range(period - 1):
buf[k] = 0.55 * buf[k] + 0.45 * buf[k + 1]
out = numpy.zeros(n_samples, dtype=numpy.float64)
for i in range(n_samples):
out[i] = buf[i % period]
next_idx = (i + 1) % period
buf[i % period] = 0.5 * (buf[i % period] + buf[next_idx]) * 0.998
# Small body resonance — mid-heavy, no deep bass
import scipy.signal as _sig
for center, bw, gain in [(350, 100, 0.35), (700, 150, 0.25), (1200, 200, 0.15)]:
lo = max(20, center - bw)
hi = min(SAMPLE_RATE // 2 - 1, center + bw)
if lo < hi:
bp, ap = _sig.butter(2, [lo, hi], btype='band', fs=SAMPLE_RATE)
out += _sig.lfilter(bp, ap, out) * gain
bl, al = _sig.butter(2, min(6000, hz * 12), btype='low', fs=SAMPLE_RATE)
out = _sig.lfilter(bl, al, out)
mx = numpy.abs(out).max()
if mx > 0:
out /= mx
return (peak * out).astype(numpy.int16)
def acoustic_guitar_wave(hz, peak=SAMPLE_PEAK, n_samples=SAMPLE_RATE):
"""Acoustic guitar — Karplus-Strong with wooden body resonance.
@@ -1434,6 +1787,16 @@ class Synth(Enum):
SAXOPHONE = "saxophone_synth"
GRANULAR = "granular_synth"
VOCAL = "vocal_synth"
PEDAL_STEEL = "pedal_steel_synth"
THEREMIN = "theremin_synth"
KALIMBA = "kalimba_synth"
STEEL_DRUM = "steel_drum_synth"
ACCORDION = "accordion_synth"
DIDGERIDOO = "didgeridoo_synth"
BAGPIPE = "bagpipe_synth"
BANJO = "banjo_synth"
MANDOLIN = "mandolin_synth"
UKULELE = "ukulele_synth"
ACOUSTIC_GUITAR = "acoustic_guitar_synth"
SITAR = "sitar_synth"
ELECTRIC_GUITAR = "electric_guitar_synth"
@@ -1457,6 +1820,12 @@ _SYNTH_FUNCTIONS = {
"harp_synth": harp_wave, "upright_bass_synth": upright_bass_wave,
"timpani_synth": timpani_wave, "saxophone_synth": saxophone_wave,
"granular_synth": granular_wave, "vocal_synth": vocal_wave,
"pedal_steel_synth": pedal_steel_wave, "theremin_synth": theremin_wave,
"kalimba_synth": kalimba_wave, "steel_drum_synth": steel_drum_wave,
"accordion_synth": accordion_wave, "didgeridoo_synth": didgeridoo_wave,
"bagpipe_synth": bagpipe_wave,
"banjo_synth": banjo_wave, "mandolin_synth": mandolin_wave,
"ukulele_synth": ukulele_wave,
"acoustic_guitar_synth": acoustic_guitar_wave,
"sitar_synth": sitar_wave, "electric_guitar_synth": electric_guitar_wave,
}
@@ -2212,6 +2581,68 @@ def _synth_mridangam_tha(n_samples):
return out
def _synth_cajon_bass(n_samples):
"""Cajón bass — palm strike on center of the face.
Deep woody thump. The box resonates like a bass drum but with
a warmer, more wooden character.
"""
t = numpy.arange(n_samples, dtype=numpy.float32) / SAMPLE_RATE
# Wooden box thump
thump_len = min(int(SAMPLE_RATE * 0.06), 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)
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
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
return numpy.tanh(result * 1.5).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))
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
out = numpy.zeros(n_samples, dtype=numpy.float32)
out[:n] = numpy.tanh(result * 1.5)
return out
def _synth_metal_kick(n_samples):
"""Metal kick — punchy with beater click. Double-bass ready.
@@ -2480,6 +2911,10 @@ def _render_drum_hit(sound_value, n_samples):
DrumSound.DJEMBE_BASS.value: lambda n: _synth_djembe_bass(n),
DrumSound.DJEMBE_TONE.value: lambda n: _synth_djembe_tone(n),
DrumSound.DJEMBE_SLAP.value: lambda n: _synth_djembe_slap(n),
# Cajon
DrumSound.CAJON_BASS.value: lambda n: _synth_cajon_bass(n),
DrumSound.CAJON_SLAP.value: lambda n: _synth_cajon_slap(n),
DrumSound.CAJON_TAP.value: lambda n: _synth_cajon_tap(n),
# Metal kit
DrumSound.METAL_KICK.value: lambda n: _synth_metal_kick(n),
DrumSound.METAL_SNARE.value: lambda n: _synth_metal_snare(n),
@@ -4069,6 +4504,10 @@ def render_score(score):
DrumSound.DJEMBE_BASS.value: 0.0,
DrumSound.DJEMBE_TONE.value: 0.1,
DrumSound.DJEMBE_SLAP.value: -0.1,
# Cajon — centered (single instrument)
DrumSound.CAJON_BASS.value: 0.0,
DrumSound.CAJON_SLAP.value: 0.0,
DrumSound.CAJON_TAP.value: 0.1,
# Metal kit
DrumSound.METAL_KICK.value: 0.0,
DrumSound.METAL_SNARE.value: 0.0,
+112 -2
View File
@@ -195,6 +195,54 @@ INSTRUMENTS = {
"detune": 12, "lowpass": 3000, "lowpass_q": 1.5,
"humanize": 0.2,
},
"pedal_steel": {
"synth": "pedal_steel_synth", "envelope": "strings",
"reverb": 0.3, "reverb_type": "spring",
"humanize": 0.15,
},
"theremin": {
"synth": "theremin_synth", "envelope": "pad",
"legato": True, "glide": 0.05,
"reverb": 0.3, "reverb_type": "plate",
},
"kalimba": {
"synth": "kalimba_synth", "envelope": "none",
"reverb": 0.35, "reverb_type": "plate",
},
"steel_drum": {
"synth": "steel_drum_synth", "envelope": "none",
"reverb": 0.3, "reverb_type": "plate",
},
"accordion": {
"synth": "accordion_synth", "envelope": "organ",
"humanize": 0.15,
},
"didgeridoo": {
"synth": "didgeridoo_synth", "envelope": "pad",
"lowpass": 1500,
"reverb": 0.4, "reverb_type": "cave",
},
"bagpipe": {
"synth": "bagpipe_synth", "envelope": "organ",
"lowpass": 4000,
},
"banjo": {
"synth": "banjo_synth", "envelope": "none",
"humanize": 0.2,
},
"mandolin": {
"synth": "mandolin_synth", "envelope": "none",
"humanize": 0.2,
},
"mandola": {
"synth": "mandolin_synth", "envelope": "none",
"lowpass": 3000,
"humanize": 0.2,
},
"ukulele": {
"synth": "ukulele_synth", "envelope": "none",
"humanize": 0.2,
},
"koto": {
"synth": "pluck_synth", "envelope": "none",
"lowpass": 4000,
@@ -479,6 +527,10 @@ class DrumSound(Enum):
DJEMBE_BASS = 102 # open bass (center of head)
DJEMBE_TONE = 103 # open tone (edge, fingers together)
DJEMBE_SLAP = 104 # slap (edge, fingers spread, sharp crack)
# Cajon sounds
CAJON_BASS = 108 # center of face, deep thump
CAJON_SLAP = 109 # top edge, snare wires buzz
CAJON_TAP = 110 # light finger tap
# Metal kit — tighter, punchier, more attack
METAL_KICK = 105 # clicky, punchy, tight
METAL_SNARE = 106 # crack, bright, cutting
@@ -1510,6 +1562,50 @@ Pattern._PRESETS["tabla solo"] = dict(
],
)
# ── Cajón patterns ────────────────────────────────────────────────────────
CB = DrumSound.CAJON_BASS
CSL = DrumSound.CAJON_SLAP
CT = DrumSound.CAJON_TAP
# Cajón flamenco — the classic acoustic percussion groove
Pattern._PRESETS["cajon"] = dict(
name="cajon",
time_signature="4/4",
beats=4.0,
hits=[
_h(CB, 0.0, 85), _h(CT, 0.5, 35), _h(CT, 0.75, 38),
_h(CSL, 1.0, 80), _h(CT, 1.5, 32),
_h(CB, 2.0, 82), _h(CT, 2.5, 35), _h(CT, 2.75, 40),
_h(CSL, 3.0, 82), _h(CT, 3.25, 30), _h(CT, 3.5, 35),
],
)
# Cajón rumba — Latin-flavored
Pattern._PRESETS["cajon rumba"] = dict(
name="cajon rumba",
time_signature="4/4",
beats=4.0,
hits=[
_h(CB, 0.0, 88), _h(CT, 0.5, 38),
_h(CSL, 1.0, 78), _h(CT, 1.25, 32), _h(CB, 1.5, 72),
_h(CSL, 2.0, 82), _h(CT, 2.5, 35),
_h(CB, 3.0, 75), _h(CSL, 3.5, 80), _h(CT, 3.75, 38),
],
)
# Cajón singer-songwriter — simple, supportive
Pattern._PRESETS["cajon folk"] = dict(
name="cajon folk",
time_signature="4/4",
beats=4.0,
hits=[
_h(CB, 0.0, 80),
_h(CSL, 1.0, 72), _h(CT, 1.5, 30),
_h(CB, 2.0, 78),
_h(CSL, 3.0, 75),
],
)
# ── Metal kit patterns ────────────────────────────────────────────────────
MK = DrumSound.METAL_KICK
MS = DrumSound.METAL_SNARE
@@ -2409,7 +2505,7 @@ class Part:
def strum(self, chord_name: str, duration=Duration.QUARTER, *,
direction: str = "down", velocity: int = 100,
strum_time: float = 0.08) -> "Part":
strum_time: float = 0.05) -> "Part":
"""Strum a chord using the part's fretboard fingering.
Looks up the chord on the fretboard, gets the fingering, and
@@ -2477,7 +2573,21 @@ class Part:
from .chords import Chord as ChordClass
chord_obj = ChordClass(tones=strum_tones)
self.add(chord_obj, total_beats, velocity=velocity)
# Strum sweep: quick individual string hits before the chord.
# Only the first 2-3 strings get a tiny grace note, the rest
# ring together as the full chord. Gives the strum feel without
# sounding like separate plucks.
n_strings = len(strum_tones)
if strum_time > 0.02 and n_strings >= 3:
n_grace = min(2, n_strings - 1)
per_grace = strum_time / n_grace
grace_vel = max(1, int(velocity * 0.25))
for i in range(n_grace):
self.add(strum_tones[i], per_grace, velocity=grace_vel)
ring = max(0.1, total_beats - strum_time)
self.add(chord_obj, ring, velocity=velocity)
else:
self.add(chord_obj, total_beats, velocity=velocity)
return self
+1 -1
View File
@@ -6827,7 +6827,7 @@ def test_strum_direction():
p = score.part("g", instrument="acoustic_guitar", fretboard=fb)
p.strum("G", Duration.QUARTER, direction="down")
p.strum("G", Duration.QUARTER, direction="up")
assert len(p.notes) == 2
assert len(p.notes) >= 2 # grace notes + chord per strum
# ── World drums ──────────────────────────────────────────────────────────────
Generated
+1 -1
View File
@@ -698,7 +698,7 @@ wheels = [
[[package]]
name = "pytheory"
version = "0.35.1"
version = "0.36.1"
source = { editable = "." }
dependencies = [
{ name = "numeral" },