mirror of
https://github.com/kennethreitz/pytheory.git
synced 2026-06-05 23:00:20 +00:00
Compare commits
9 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 70efb0ad40 | |||
| bf6deaab64 | |||
| 7c792c0a2a | |||
| bf8d4b9a77 | |||
| d2d5115c8a | |||
| 3cdd98b158 | |||
| 751d5a49b8 | |||
| 6a836dd891 | |||
| 1f888e2b21 |
@@ -2,6 +2,25 @@
|
||||
|
||||
All notable changes to PyTheory are documented here.
|
||||
|
||||
## 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
@@ -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** — 34 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
@@ -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
@@ -1,6 +1,6 @@
|
||||
[project]
|
||||
name = "pytheory"
|
||||
version = "0.35.1"
|
||||
version = "0.36.0"
|
||||
description = "Music Theory for Humans"
|
||||
readme = "README.md"
|
||||
license = "MIT"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
"""PyTheory: Music Theory for Humans."""
|
||||
|
||||
__version__ = "0.35.1"
|
||||
__version__ = "0.36.0"
|
||||
|
||||
from .tones import Tone, Interval
|
||||
from .systems import System, SYSTEMS, TET
|
||||
|
||||
+358
-3
@@ -909,6 +909,149 @@ def saxophone_wave(hz, peak=SAMPLE_PEAK, n_samples=SAMPLE_RATE):
|
||||
return (peak * wave).astype(numpy.int16)
|
||||
|
||||
|
||||
def vocal_wave(hz, peak=SAMPLE_PEAK, n_samples=SAMPLE_RATE, lyric="ah"):
|
||||
"""Vocal/formant synthesis — sings vowel sounds at a given pitch.
|
||||
|
||||
Models the human voice with:
|
||||
1. LF glottal model — asymmetric pulse with sharp closure (not just sines)
|
||||
2. 5 parallel resonant formant filters (real voice has 5 formant peaks)
|
||||
3. Jitter + shimmer (natural pitch/amplitude irregularity)
|
||||
4. Aspiration noise mixed with the glottal source
|
||||
5. Consonant onsets (plosives, sibilants, nasals, etc.)
|
||||
"""
|
||||
import scipy.signal as _sig
|
||||
|
||||
# 5-formant table: (F1, F2, F3, F4, F5) frequencies and bandwidths
|
||||
# Based on Peterson & Barney (1952) measurements, male voice
|
||||
FORMANTS = {
|
||||
'a': [(800, 130), (1200, 100), (2500, 140), (3300, 250), (3750, 300)],
|
||||
'e': [(530, 80), (1850, 100), (2500, 130), (3300, 250), (3750, 300)],
|
||||
'i': [(280, 60), (2250, 100), (2900, 120), (3350, 250), (3750, 300)],
|
||||
'o': [(500, 100), (1000, 80), (2500, 140), (3300, 250), (3750, 300)],
|
||||
'u': ((325, 70), (700, 60), (2530, 140), (3300, 250), (3750, 300)),
|
||||
}
|
||||
# Formant gains (relative amplitude per formant)
|
||||
FGAINS = [1.0, 0.8, 0.5, 0.25, 0.15]
|
||||
|
||||
rng = numpy.random.default_rng(int(hz * 100 + len(lyric) * 7) % 2**31)
|
||||
t = numpy.arange(n_samples, dtype=numpy.float64) / SAMPLE_RATE
|
||||
|
||||
# Parse vowels from lyric
|
||||
vowels_in_lyric = [c.lower() for c in lyric if c.lower() in FORMANTS]
|
||||
if not vowels_in_lyric:
|
||||
vowels_in_lyric = ['a']
|
||||
|
||||
# ── Glottal source: LF model approximation ──
|
||||
# Asymmetric pulse: slow open phase, sharp closure, then closed phase.
|
||||
# Much more "voice-like" than a sine or sawtooth.
|
||||
# Jitter (pitch irregularity) + shimmer (amplitude irregularity)
|
||||
jitter = rng.normal(0, hz * 0.001, n_samples) # ~0.1% pitch jitter
|
||||
shimmer = 1.0 + rng.normal(0, 0.008, n_samples) # ~0.8% amp shimmer
|
||||
# Vibrato
|
||||
vib = hz * 0.001 * numpy.sin(2 * numpy.pi * 5.5 * t)
|
||||
inst_freq = hz + vib + jitter
|
||||
phase = numpy.cumsum(2 * numpy.pi * inst_freq / SAMPLE_RATE)
|
||||
# LF glottal shape: sharper falling edge via phase shaping
|
||||
saw = (phase / (2 * numpy.pi)) % 1.0 # 0 to 1 sawtooth
|
||||
# Asymmetric: slow rise (60%), fast fall (40%)
|
||||
glottal = numpy.where(saw < 0.6,
|
||||
numpy.sin(numpy.pi * saw / 0.6), # smooth rise
|
||||
-numpy.sin(numpy.pi * (saw - 0.6) / 0.4) * 0.8) # sharp fall
|
||||
glottal *= shimmer
|
||||
|
||||
# Aspiration noise (breathiness) — subtle
|
||||
breath = rng.normal(0, 0.04, n_samples)
|
||||
source = glottal * 0.92 + breath * 0.08
|
||||
|
||||
# ── Formant filtering ──
|
||||
n_vowels = len(vowels_in_lyric)
|
||||
out = numpy.zeros(n_samples, dtype=numpy.float64)
|
||||
|
||||
if n_vowels == 1:
|
||||
# Single vowel — filter the whole thing
|
||||
formants = FORMANTS[vowels_in_lyric[0]]
|
||||
for (fc, bw), gain in zip(formants, FGAINS):
|
||||
lo = max(20, fc - bw)
|
||||
hi = min(SAMPLE_RATE // 2 - 1, fc + bw)
|
||||
if lo < hi:
|
||||
bp, ap = _sig.butter(2, [lo, hi], btype='band', fs=SAMPLE_RATE)
|
||||
out += _sig.lfilter(bp, ap, source).astype(numpy.float64) * gain
|
||||
else:
|
||||
# Multiple vowels — crossfade formants
|
||||
samples_per_vowel = n_samples // n_vowels
|
||||
for vi, vowel in enumerate(vowels_in_lyric):
|
||||
formants = FORMANTS[vowel]
|
||||
start = vi * samples_per_vowel
|
||||
end = n_samples if vi == n_vowels - 1 else start + samples_per_vowel
|
||||
seg = source[start:end].copy()
|
||||
seg_out = numpy.zeros_like(seg)
|
||||
for (fc, bw), gain in zip(formants, FGAINS):
|
||||
lo = max(20, fc - bw)
|
||||
hi = min(SAMPLE_RATE // 2 - 1, fc + bw)
|
||||
if lo < hi:
|
||||
bp, ap = _sig.butter(2, [lo, hi], btype='band', fs=SAMPLE_RATE)
|
||||
seg_out += _sig.lfilter(bp, ap, seg).astype(numpy.float64) * gain
|
||||
# Crossfade
|
||||
fade = min(int(SAMPLE_RATE * 0.02), len(seg_out) // 4)
|
||||
if vi > 0 and fade > 0:
|
||||
seg_out[:fade] *= numpy.linspace(0, 1, fade)
|
||||
if vi < n_vowels - 1 and fade > 0:
|
||||
seg_out[-fade:] *= numpy.linspace(1, 0, fade)
|
||||
out[start:end] += seg_out[:end - start]
|
||||
|
||||
# ── Consonant onsets ──
|
||||
lyric_lower = lyric.lower()
|
||||
if lyric_lower and lyric_lower[0] not in 'aeiou':
|
||||
c = lyric_lower[0]
|
||||
cl = min(int(SAMPLE_RATE * 0.035), n_samples)
|
||||
if c in 'tdkpb':
|
||||
burst = rng.uniform(-0.5, 0.5, cl) * numpy.exp(-numpy.linspace(0, 18, cl))
|
||||
out[:cl] = burst + out[:cl] * 0.2
|
||||
elif c in 'sz':
|
||||
sib = rng.uniform(-0.4, 0.4, cl)
|
||||
if cl > 20:
|
||||
bl, al = _sig.butter(2, [3000, min(8000, SAMPLE_RATE//2-1)], btype='band', fs=SAMPLE_RATE)
|
||||
sib = _sig.lfilter(bl, al, numpy.pad(sib, (0, max(0, n_samples-cl))))[:cl]
|
||||
sib *= numpy.exp(-numpy.linspace(0, 10, cl))
|
||||
out[:cl] = sib * 0.6 + out[:cl] * 0.4
|
||||
elif c in 'mn':
|
||||
nl = min(int(SAMPLE_RATE * 0.06), n_samples)
|
||||
nasal = numpy.sin(2*numpy.pi*250*t[:nl]) * 0.4 * numpy.exp(-numpy.linspace(0, 4, nl))
|
||||
out[:nl] = nasal + out[:nl] * 0.4
|
||||
elif c in 'fv':
|
||||
fric = rng.uniform(-0.25, 0.25, cl) * numpy.exp(-numpy.linspace(0, 12, cl))
|
||||
out[:cl] = fric * 0.5 + out[:cl] * 0.5
|
||||
elif c in 'lr':
|
||||
gl = min(int(SAMPLE_RATE * 0.05), n_samples)
|
||||
ghz = hz * 0.7 + hz * 0.3 * numpy.linspace(0, 1, gl)
|
||||
glide = numpy.sin(numpy.cumsum(2*numpy.pi*ghz/SAMPLE_RATE)) * 0.35
|
||||
out[:gl] = glide + out[:gl] * 0.65
|
||||
elif c == 'h':
|
||||
hl = min(int(SAMPLE_RATE * 0.05), n_samples)
|
||||
asp = rng.uniform(-0.4, 0.4, hl) * numpy.exp(-numpy.linspace(0, 5, hl))
|
||||
out[:hl] = asp * 0.6 + out[:hl] * 0.4
|
||||
elif c == 'w':
|
||||
wl = min(int(SAMPLE_RATE * 0.06), n_samples)
|
||||
ws = numpy.sin(numpy.cumsum(2*numpy.pi*hz/SAMPLE_RATE*numpy.ones(wl)))
|
||||
if wl > 20:
|
||||
bp, ap = _sig.butter(2, [max(20,300), min(800, SAMPLE_RATE//2-1)], btype='band', fs=SAMPLE_RATE)
|
||||
ws = _sig.lfilter(bp, ap, ws)
|
||||
ws *= numpy.linspace(0.5, 0, wl)
|
||||
out[:wl] = ws * 0.4 + out[:wl] * 0.6
|
||||
|
||||
# Soft edges — prevent clicks at note boundaries
|
||||
fade_samples = min(int(SAMPLE_RATE * 0.01), n_samples // 4)
|
||||
if fade_samples > 0:
|
||||
out[:fade_samples] *= numpy.linspace(0, 1, fade_samples)
|
||||
out[-fade_samples:] *= numpy.linspace(1, 0, fade_samples)
|
||||
|
||||
mx = numpy.abs(out).max()
|
||||
if mx > 0:
|
||||
out /= mx
|
||||
|
||||
return (peak * out).astype(numpy.int16)
|
||||
|
||||
|
||||
def granular_wave(hz, peak=SAMPLE_PEAK, n_samples=SAMPLE_RATE,
|
||||
grain_size=0.04, density=50, scatter=0.5,
|
||||
pitch_var=12, source="saw"):
|
||||
@@ -987,6 +1130,137 @@ def granular_wave(hz, peak=SAMPLE_PEAK, n_samples=SAMPLE_RATE,
|
||||
return (peak * out).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.
|
||||
|
||||
@@ -1290,6 +1564,10 @@ class Synth(Enum):
|
||||
TIMPANI = "timpani_synth"
|
||||
SAXOPHONE = "saxophone_synth"
|
||||
GRANULAR = "granular_synth"
|
||||
VOCAL = "vocal_synth"
|
||||
BANJO = "banjo_synth"
|
||||
MANDOLIN = "mandolin_synth"
|
||||
UKULELE = "ukulele_synth"
|
||||
ACOUSTIC_GUITAR = "acoustic_guitar_synth"
|
||||
SITAR = "sitar_synth"
|
||||
ELECTRIC_GUITAR = "electric_guitar_synth"
|
||||
@@ -1312,7 +1590,9 @@ _SYNTH_FUNCTIONS = {
|
||||
"harpsichord_synth": harpsichord_wave, "cello_synth": cello_wave,
|
||||
"harp_synth": harp_wave, "upright_bass_synth": upright_bass_wave,
|
||||
"timpani_synth": timpani_wave, "saxophone_synth": saxophone_wave,
|
||||
"granular_synth": granular_wave,
|
||||
"granular_synth": granular_wave, "vocal_synth": vocal_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,
|
||||
}
|
||||
@@ -2068,6 +2348,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.
|
||||
|
||||
@@ -2336,6 +2678,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),
|
||||
@@ -3528,8 +3874,13 @@ def _render_notes_to_buf(notes, buf, samples_per_beat, total_samples,
|
||||
bent = src_f[idx] * (1 - frac) + src_f[numpy.minimum(idx + 1, src_len - 1)] * frac
|
||||
waves.append((bent * SAMPLE_PEAK).astype(numpy.int16))
|
||||
else:
|
||||
# Render oscillators (pass synth_kwargs for FM etc.)
|
||||
waves = [synth_fn(hz, n_samples=n_samples, **_skw)
|
||||
# Per-note kwargs (e.g. lyric for vocal synth)
|
||||
note_skw = dict(_skw)
|
||||
note_lyric = getattr(note, 'lyric', '')
|
||||
if note_lyric:
|
||||
note_skw['lyric'] = note_lyric
|
||||
# Render oscillators
|
||||
waves = [synth_fn(hz, n_samples=n_samples, **note_skw)
|
||||
for hz in pitches]
|
||||
# Sub-oscillator: octave-below sine
|
||||
if sub_osc > 0:
|
||||
@@ -3920,6 +4271,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,
|
||||
|
||||
+94
-4
@@ -195,6 +195,23 @@ INSTRUMENTS = {
|
||||
"detune": 12, "lowpass": 3000, "lowpass_q": 1.5,
|
||||
"humanize": 0.2,
|
||||
},
|
||||
"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,
|
||||
@@ -246,6 +263,16 @@ INSTRUMENTS = {
|
||||
"reverb": 0.4, "reverb_type": "cathedral",
|
||||
"analog": 0.3,
|
||||
},
|
||||
"vocal": {
|
||||
"synth": "vocal_synth", "envelope": "strings",
|
||||
"reverb": 0.3, "reverb_type": "hall",
|
||||
"humanize": 0.15,
|
||||
},
|
||||
"choir": {
|
||||
"synth": "vocal_synth", "envelope": "pad",
|
||||
"detune": 8, "spread": 0.4,
|
||||
"reverb": 0.45, "reverb_type": "cathedral",
|
||||
},
|
||||
"granular_texture": {
|
||||
"synth": "granular_synth", "envelope": "none",
|
||||
"reverb": 0.5, "reverb_type": "taj_mahal",
|
||||
@@ -367,6 +394,7 @@ class Note:
|
||||
velocity: int = 100
|
||||
bend: float = 0.0
|
||||
bend_type: str = "smooth" # "smooth" (log), "linear", "late"
|
||||
lyric: str = "" # syllable for vocal synth
|
||||
|
||||
@property
|
||||
def beats(self) -> float:
|
||||
@@ -468,6 +496,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
|
||||
@@ -1499,6 +1531,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
|
||||
@@ -2095,7 +2171,7 @@ class Part:
|
||||
self._automation: list[tuple[float, dict]] = [] # (beat, {param: value})
|
||||
|
||||
def add(self, tone_or_string, duration=Duration.QUARTER, *, velocity: int = 100,
|
||||
bend: float = 0.0, bend_type: str = "smooth") -> "Part":
|
||||
bend: float = 0.0, bend_type: str = "smooth", lyric: str = "") -> "Part":
|
||||
"""Add a note. Accepts Tone/Chord objects or note strings like ``"E5"``.
|
||||
|
||||
Duration can be a ``Duration`` enum or a raw float (beats).
|
||||
@@ -2113,7 +2189,7 @@ class Part:
|
||||
duration = _RawDuration(duration)
|
||||
self.notes.append(Note(tone=tone_or_string, duration=duration,
|
||||
velocity=velocity, bend=bend,
|
||||
bend_type=bend_type))
|
||||
bend_type=bend_type, lyric=lyric))
|
||||
return self
|
||||
|
||||
def set(self, **params) -> "Part":
|
||||
@@ -2398,7 +2474,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
|
||||
@@ -2466,7 +2542,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
@@ -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 ──────────────────────────────────────────────────────────────
|
||||
|
||||
Reference in New Issue
Block a user