Compare commits

...

6 Commits

Author SHA1 Message Date
kennethreitz 938c1cc132 v0.36.6: Cajón and metal drum fills
Add 6 new drum fills: cajon flam, cajon rumble, cajon breakdown,
metal triplet, metal blast, metal cascade. 27 fills total.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-28 12:25:52 -04:00
kennethreitz 9dc22db4b2 v0.36.5: Duration arithmetic support
Duration enum now supports multiplication, division, and addition
so expressions like `Duration.WHOLE * 2` work instead of raising TypeError.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-28 12:19:28 -04:00
kennethreitz f570e226cd v0.36.4: Harmonium, doumbek, tabla fills, Part.hold()
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-28 12:11:06 -04:00
kennethreitz 0c5c3abedc Harmonium synth, doumbek drums (3 sounds, 4 patterns, 2 fills)
- Harmonium: single free reed, nasal midrange, bellows swell.
  The sound of kirtan and qawwali.
- Doumbek (darbuka): dum (center bass), tek (edge sharp), ka (muted).
  4 patterns: maqsoum, baladi, saidi, ayoub.
  2 fills: doumbek roll, doumbek accent.
- 42 synth waveforms total

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-28 12:05:46 -04:00
kennethreitz 35d07b984b Docs: add tabla fills to drums.rst
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-28 11:57:08 -04:00
kennethreitz aec7723ee6 5 tabla fills: tihai, chakkardar, tiri kita, bayan, tabla call
Tihai (3x crescendo landing on sam), chakkardar (32nd triplet
cascade), tiri kita (rapid 16th dayan burst), bayan (bass bends),
tabla call (dayan/bayan call-and-response).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-28 11:56:19 -04:00
8 changed files with 455 additions and 12 deletions
+11
View File
@@ -2,6 +2,17 @@
All notable changes to PyTheory are documented here.
## 0.36.6
- **6 new drum fills** — 3 cajón (flam, rumble, breakdown) and 3 metal
(triplet, blast, cascade). 27 fills total.
- Updated drums documentation with fill lists and examples
## 0.36.5
- **Duration arithmetic** — `Duration.WHOLE * 2`, `Duration.HALF + Duration.QUARTER`,
division, and reverse multiply all work now (previously raised TypeError)
## 0.36.3
- **`Part.hold()`** — polyphonic overlap on a single part. Add notes
+27 -6
View File
@@ -10,7 +10,7 @@ the genre -- they tell the listener's body how to move before a single
melodic note is played.
PyTheory includes a complete drum system -- 51 synthesized percussion
sounds, 80+ pattern presets across dozens of genres, and 21 fill presets.
sounds, 80+ pattern presets across dozens of genres, and 27 fill presets.
Every sound is generated from waveforms; no samples needed.
Drum Sounds
@@ -252,14 +252,16 @@ ending and a new one is about to begin. Without fills, a drum pattern
just loops. With them, it breathes and has structure.
``Pattern.fill()`` loads a 1-bar drum fill -- a short break that
transitions between sections. 21 fill presets are available:
transitions between sections. 27 fill presets are available:
.. code-block:: pycon
>>> Pattern.list_fills()
['afrobeat', 'blast', 'bossa nova', 'breakdown', 'buildup',
'cajon breakdown', 'cajon flam', 'cajon rumble',
'cumbia', 'disco', 'funk', 'highlife', 'hip hop', 'house',
'jazz', 'jazz brush', 'metal', 'reggae', 'rock', 'rock crash',
'jazz', 'jazz brush', 'metal', 'metal blast', 'metal cascade',
'metal triplet', 'reggae', 'rock', 'rock crash',
'salsa', 'samba', 'second line', 'trap']
>>> fill = Pattern.fill("rock")
@@ -357,6 +359,15 @@ the pitch upward.
and light classical), tabla solo, and tiri kita (fast ornamental
pattern).
**5 fills:** tihai (3x crescendo landing on sam), chakkardar (32nd
triplet cascade into slam), tiri kita (rapid 16th-note dayan burst),
bayan (deep bass bends showcase), tabla call (dayan/bayan call-and-response).
.. code-block:: python
score.drums("teental", repeats=4, fill="tihai")
score.drums("keherwa", repeats=4, fill="chakkardar")
.. code-block:: python
score = Score("4/4", bpm=80)
@@ -447,10 +458,15 @@ metal blast (blast beat with china cymbal accents), metal groove (a
half-time groove with double kick fills), and metal gallop (the
classic triplet-feel gallop rhythm).
**4 fills:** metal (double kick 16ths with descending toms), metal triplet
(double kick triplets with snare accents), metal blast (alternating
snare/kick 32nds into half-time crash), metal cascade (descending snare
roll → kick roll → alternating → crash ending).
.. code-block:: python
score = Score("4/4", bpm=200)
score.drums("metal blast", repeats=4)
score.drums("metal blast", repeats=8, fill="metal cascade", fill_every=4)
Cajón
~~~~~
@@ -459,15 +475,20 @@ The cajón is a box-shaped percussion instrument from Peru, now
ubiquitous in acoustic and unplugged settings worldwide. Players sit
on the box and strike the front face with their hands.
**2 sounds** -- slap (sharp, snare-like) and tap (bass-like).
**3 sounds** -- bass (deep center thump), slap (sharp, snare-like edge
hit with wire buzz), and tap (light finger tap).
**3 patterns:** cajon (basic groove), cajon rumba (flamenco-style rumba),
and cajon folk (folk/acoustic pattern).
**3 fills:** cajon flam (slaps accelerating into bass hits), cajon rumble
(fast taps building to slap accents), cajon breakdown (syncopated
bass-slap groove).
.. code-block:: python
score = Score("4/4", bpm=100)
score.drums("cajon", repeats=4)
score.drums("cajon", repeats=8, fill="cajon flam", fill_every=4)
MIDI Export
-----------
+1 -1
View File
@@ -1,6 +1,6 @@
[project]
name = "pytheory"
version = "0.36.3"
version = "0.36.6"
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.36.3"
__version__ = "0.36.6"
from .tones import Tone, Interval
from .systems import System, SYSTEMS, TET
+97 -1
View File
@@ -1237,6 +1237,47 @@ def steel_drum_wave(hz, peak=SAMPLE_PEAK, n_samples=SAMPLE_RATE):
return (peak * wave).astype(numpy.int16)
def harmonium_wave(hz, peak=SAMPLE_PEAK, n_samples=SAMPLE_RATE):
"""Harmonium — Indian pump organ, single free reed per note.
Unlike accordion (doubled musette reeds), the harmonium has one
reed per note — no beating, just a pure, nasal, reedy tone.
Constant bellows pressure, warm but slightly buzzy. The sound
of kirtan, qawwali, and devotional music.
"""
t = numpy.arange(n_samples, dtype=numpy.float64) / SAMPLE_RATE
rng = numpy.random.default_rng(int(hz * 100) % 2**31)
# Single reed — odd harmonics stronger (like clarinet but warmer)
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) * (1.0 if n % 2 == 1 else 0.5)
phase = rng.uniform(0, 2 * numpy.pi)
wave += amp * numpy.sin(2 * numpy.pi * f_n * t + phase)
# Bellows pressure — gentle swell, slower than accordion
bellows = 0.9 + 0.1 * numpy.sin(2 * numpy.pi * 0.5 * t)
wave *= bellows
# Nasal character — slight midrange boost
import scipy.signal as _sig
center = min(1200, hz * 3)
lo = max(20, int(center - 300))
hi = min(SAMPLE_RATE // 2 - 1, int(center + 300))
if lo < hi:
bp, ap = _sig.butter(2, [lo, hi], btype='band', fs=SAMPLE_RATE)
nasal = _sig.lfilter(bp, ap, wave) * 0.2
wave += nasal
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.
@@ -1794,6 +1835,7 @@ class Synth(Enum):
THEREMIN = "theremin_synth"
KALIMBA = "kalimba_synth"
STEEL_DRUM = "steel_drum_synth"
HARMONIUM = "harmonium_synth"
ACCORDION = "accordion_synth"
DIDGERIDOO = "didgeridoo_synth"
BAGPIPE = "bagpipe_synth"
@@ -1825,7 +1867,7 @@ _SYNTH_FUNCTIONS = {
"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,
"harmonium_synth": harmonium_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,
@@ -2584,6 +2626,52 @@ def _synth_mridangam_tha(n_samples):
return out
def _synth_doumbek_dum(n_samples):
"""Doumbek Dum — open center strike, deep and round."""
t = numpy.arange(n_samples, dtype=numpy.float32) / SAMPLE_RATE
freq = 80 + 40 * numpy.exp(-25 * t)
phase = 2 * numpy.pi * numpy.cumsum(freq) / SAMPLE_RATE
body = numpy.sin(phase) * _exp_decay(n_samples, 8) * 0.8
thump_len = min(int(SAMPLE_RATE * 0.04), n_samples)
import scipy.signal as _sig
thump = _noise(thump_len)
if thump_len > 20:
bl, al = _sig.butter(2, [50, 250], btype='band', fs=SAMPLE_RATE)
thump = _sig.lfilter(bl, al, numpy.pad(thump, (0, max(0, n_samples - thump_len))))[:thump_len].astype(numpy.float32)
thump *= _exp_decay(thump_len, 22) * 0.7
body[:thump_len] += thump
return numpy.tanh(body * 1.3).astype(numpy.float32)
def _synth_doumbek_tek(n_samples):
"""Doumbek Tek — sharp edge strike, bright and cutting."""
t = numpy.arange(n_samples, dtype=numpy.float32) / SAMPLE_RATE
ring = numpy.sin(2 * numpy.pi * 400 * t) * _exp_decay(n_samples, 22) * 0.5
ring2 = numpy.sin(2 * numpy.pi * 900 * t) * 0.3 * _exp_decay(n_samples, 30)
click_len = min(int(SAMPLE_RATE * 0.005), n_samples)
click = _noise(click_len) * _exp_decay(click_len, 300) * 0.9
import scipy.signal as _sig
if click_len > 10:
bl, al = _sig.butter(2, [2000, min(8000, SAMPLE_RATE // 2 - 1)], btype='band', fs=SAMPLE_RATE)
click = _sig.lfilter(bl, al, numpy.pad(click, (0, max(0, n_samples - click_len))))[:click_len].astype(numpy.float32)
result = ring + ring2
result[:click_len] += click
return numpy.tanh(result * 1.8).astype(numpy.float32)
def _synth_doumbek_ka(n_samples):
"""Doumbek Ka — muted edge slap, short and dry."""
n = min(n_samples, int(SAMPLE_RATE * 0.04))
t = numpy.arange(n, dtype=numpy.float32) / SAMPLE_RATE
body = numpy.sin(2 * numpy.pi * 350 * t) * _exp_decay(n, 30) * 0.4
slap = _noise(min(80, n)) * _exp_decay(min(80, n), 200) * 0.7
result = body
result[:min(80, n)] += slap
out = numpy.zeros(n_samples, dtype=numpy.float32)
out[:n] = numpy.tanh(result * 1.5)
return out
def _synth_cajon_bass(n_samples):
"""Cajón bass — palm strike on center of the face.
@@ -2914,6 +3002,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),
# Doumbek
DrumSound.DOUMBEK_DUM.value: lambda n: _synth_doumbek_dum(n),
DrumSound.DOUMBEK_TEK.value: lambda n: _synth_doumbek_tek(n),
DrumSound.DOUMBEK_KA.value: lambda n: _synth_doumbek_ka(n),
# Cajon
DrumSound.CAJON_BASS.value: lambda n: _synth_cajon_bass(n),
DrumSound.CAJON_SLAP.value: lambda n: _synth_cajon_slap(n),
@@ -4510,6 +4602,10 @@ def render_score(score):
DrumSound.DJEMBE_BASS.value: 0.0,
DrumSound.DJEMBE_TONE.value: 0.1,
DrumSound.DJEMBE_SLAP.value: -0.1,
# Doumbek
DrumSound.DOUMBEK_DUM.value: 0.0,
DrumSound.DOUMBEK_TEK.value: 0.1,
DrumSound.DOUMBEK_KA.value: -0.1,
# Cajon — centered (single instrument)
DrumSound.CAJON_BASS.value: 0.0,
DrumSound.CAJON_SLAP.value: 0.0,
+302
View File
@@ -213,6 +213,11 @@ INSTRUMENTS = {
"synth": "steel_drum_synth", "envelope": "none",
"reverb": 0.3, "reverb_type": "plate",
},
"harmonium": {
"synth": "harmonium_synth", "envelope": "organ",
"reverb": 0.2, "reverb_type": "taj_mahal",
"humanize": 0.15,
},
"accordion": {
"synth": "accordion_synth", "envelope": "organ",
"humanize": 0.15,
@@ -383,6 +388,24 @@ class Duration(Enum):
DOTTED_QUARTER = 1.5
TRIPLET_QUARTER = 2 / 3
# Arithmetic — lets you write ``Duration.WHOLE * 2`` → 8.0 beats.
def __mul__(self, other):
return self.value * other
def __rmul__(self, other):
return self.value * other
def __truediv__(self, other):
return self.value / other
def __add__(self, other):
if isinstance(other, Duration):
return self.value + other.value
return self.value + other
def __radd__(self, other):
return other + self.value
class TimeSignature:
"""A musical time signature like 4/4 or 6/8."""
@@ -528,6 +551,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)
# Doumbek (darbuka) sounds
DOUMBEK_DUM = 112 # center of head, deep bass
DOUMBEK_TEK = 113 # edge of head, sharp high
DOUMBEK_KA = 114 # muted edge slap
# Cajon sounds
CAJON_BASS = 108 # center of face, deep thump
CAJON_SLAP = 109 # top edge, snare wires buzz
@@ -1563,6 +1590,61 @@ Pattern._PRESETS["tabla solo"] = dict(
],
)
# ── Doumbek patterns ──────────────────────────────────────────────────────
DKD = DrumSound.DOUMBEK_DUM
DKT = DrumSound.DOUMBEK_TEK
DKK = DrumSound.DOUMBEK_KA
# Maqsoum — the most common Arabic rhythm (4/4)
Pattern._PRESETS["maqsoum"] = dict(
name="maqsoum",
time_signature="4/4",
beats=4.0,
hits=[
_h(DKD, 0.0, 85), _h(DKT, 0.5, 65),
_h(DKT, 1.0, 68), _h(DKD, 1.5, 80),
_h(DKT, 2.0, 65), _h(DKT, 2.5, 62),
_h(DKT, 3.0, 68), _h(DKT, 3.5, 62),
],
)
# Baladi — heavy, earthy, belly dance
Pattern._PRESETS["baladi"] = dict(
name="baladi",
time_signature="4/4",
beats=4.0,
hits=[
_h(DKD, 0.0, 88), _h(DKD, 0.5, 78),
_h(DKT, 1.0, 70), _h(DKD, 1.5, 82),
_h(DKT, 2.0, 68), _h(DKT, 2.5, 62),
_h(DKT, 3.0, 68), _h(DKK, 3.25, 45), _h(DKT, 3.5, 65),
],
)
# Saidi — Upper Egyptian, strong and driving
Pattern._PRESETS["saidi"] = dict(
name="saidi",
time_signature="4/4",
beats=4.0,
hits=[
_h(DKD, 0.0, 88), _h(DKT, 0.5, 65),
_h(DKD, 1.0, 82), _h(DKD, 1.5, 78),
_h(DKT, 2.0, 70), _h(DKT, 2.5, 62),
_h(DKT, 3.0, 68), _h(DKT, 3.5, 62),
],
)
# Ayoub — simple 2/4, trance-like repetition
Pattern._PRESETS["ayoub"] = dict(
name="ayoub",
time_signature="2/4",
beats=2.0,
hits=[
_h(DKD, 0.0, 85), _h(DKK, 0.5, 45),
_h(DKT, 1.0, 70), _h(DKT, 1.5, 62),
],
)
# ── Cajón patterns ────────────────────────────────────────────────────────
CB = DrumSound.CAJON_BASS
CSL = DrumSound.CAJON_SLAP
@@ -2093,6 +2175,226 @@ Pattern._FILLS["second line"] = dict(
],
)
# ── Doumbek fills ────────────────────────────────────────────────────────
_DKD = DrumSound.DOUMBEK_DUM
_DKT = DrumSound.DOUMBEK_TEK
_DKK = DrumSound.DOUMBEK_KA
# Doumbek roll — rapid teks building to dum
Pattern._FILLS["doumbek roll"] = dict(
name="doumbek roll fill",
time_signature="4/4",
beats=4.0,
hits=[
*[_h(_DKT, i * 0.125, 40 + i * 4) for i in range(16)],
_h(_DKD, 2.0, 100), _h(_DKT, 2.25, 65), _h(_DKT, 2.5, 68),
_h(_DKD, 3.0, 110), _h(_DKD, 3.25, 105),
_h(_DKD, 3.5, 115), _h(_DKT, 3.75, 80),
],
)
# Doumbek accent — syncopated dum-tek-ka pattern
Pattern._FILLS["doumbek accent"] = dict(
name="doumbek accent fill",
time_signature="4/4",
beats=4.0,
hits=[
_h(_DKD, 0.0, 95), _h(_DKT, 0.25, 65), _h(_DKK, 0.5, 50),
_h(_DKT, 0.75, 68), _h(_DKD, 1.0, 90),
_h(_DKT, 1.5, 72), _h(_DKK, 1.75, 52), _h(_DKD, 2.0, 100),
_h(_DKT, 2.25, 68), _h(_DKT, 2.5, 70), _h(_DKT, 2.75, 72),
_h(_DKD, 3.0, 110), _h(_DKD, 3.5, 115),
],
)
# ── Tabla fills ──────────────────────────────────────────────────────────
_TNA = DrumSound.TABLA_NA
_TDH = DrumSound.TABLA_DHA
_TTT = DrumSound.TABLA_TIT
_TKE = DrumSound.TABLA_KE
_TGB = DrumSound.TABLA_GE_BEND
_TGE = DrumSound.TABLA_GE
_TTI = DrumSound.TABLA_TIN
_T3 = 1.0 / 12.0
# Tihai — the classic 3x pattern landing on sam
Pattern._FILLS["tihai"] = dict(
name="tihai fill",
time_signature="4/4",
beats=4.0,
hits=[
_h(_TDH, 0.0, 105), _h(_TNA, 0.25, 72), _h(_TTT, 0.5, 48),
_h(_TKE, 0.75, 52), _h(_TDH, 1.0, 100),
_h(_TDH, 1.25, 110), _h(_TNA, 1.5, 78), _h(_TTT, 1.75, 52),
_h(_TKE, 2.0, 55), _h(_TDH, 2.25, 105),
_h(_TDH, 2.5, 118), _h(_TNA, 2.75, 82), _h(_TTT, 3.0, 58),
_h(_TKE, 3.25, 60), _h(_TDH, 3.5, 127),
],
)
# Chakkardar — 32nd triplet cascade into slam
Pattern._FILLS["chakkardar"] = dict(
name="chakkardar fill",
time_signature="4/4",
beats=4.0,
hits=[
*[_h(_TTT, i * _T3, 32 + i * 3) for i in range(12)],
_h(_TDH, 1.0, 115), _h(_TGB, 1.5, 108),
*[_h(_TTT, 2.0 + i * _T3, 35 + i * 3) for i in range(12)],
_h(_TDH, 3.0, 120), _h(_TDH, 3.25, 115),
_h(_TGB, 3.5, 120), _h(_TDH, 3.75, 127),
],
)
# Tiri kita fill — rapid 16th note dayan burst
Pattern._FILLS["tiri kita"] = dict(
name="tiri kita fill",
time_signature="4/4",
beats=4.0,
hits=[
_h(_TTT, 0.0, 50), _h(_TTT, 0.125, 38), _h(_TKE, 0.25, 48),
_h(_TNA, 0.5, 72), _h(_TTT, 0.75, 42),
_h(_TDH, 1.0, 95), _h(_TTT, 1.25, 38), _h(_TTT, 1.5, 42),
_h(_TKE, 1.75, 48), _h(_TNA, 2.0, 75),
_h(_TTT, 2.25, 40), _h(_TTT, 2.5, 45), _h(_TKE, 2.75, 50),
_h(_TDH, 3.0, 100), _h(_TNA, 3.25, 70),
_h(_TDH, 3.5, 110), _h(_TGB, 3.75, 105),
],
)
# Bayan showcase — deep bass bends
Pattern._FILLS["bayan"] = dict(
name="bayan fill",
time_signature="4/4",
beats=4.0,
hits=[
_h(_TGB, 0.0, 100), _h(_TNA, 0.5, 65),
_h(_TGE, 1.0, 85), _h(_TGB, 1.5, 105),
_h(_TNA, 2.0, 70), _h(_TKE, 2.25, 48),
_h(_TGB, 2.5, 110), _h(_TDH, 3.0, 115),
_h(_TGB, 3.5, 120),
],
)
# Call and response — dayan speaks, bayan answers
Pattern._FILLS["tabla call"] = dict(
name="tabla call fill",
time_signature="4/4",
beats=4.0,
hits=[
_h(_TNA, 0.0, 105), _h(_TNA, 0.25, 55), _h(_TTT, 0.5, 38),
_h(_TNA, 0.75, 100),
_h(_TGE, 1.0, 95), _h(_TGE, 1.25, 48), _h(_TGB, 1.5, 90),
_h(_TNA, 2.0, 108), _h(_TTT, 2.125, 30), _h(_TTT, 2.25, 35),
_h(_TNA, 2.5, 100),
_h(_TGB, 3.0, 112), _h(_TKE, 3.25, 48),
_h(_TDH, 3.5, 120),
],
)
# ── Cajón fills ──────────────────────────────────────────────────────────
# Cajón flam run — slaps accelerating into bass hit
Pattern._FILLS["cajon flam"] = dict(
name="cajon flam fill",
time_signature="4/4",
beats=4.0,
hits=[
_h(CSL, 0.0, 95), _h(CT, 0.125, 45), _h(CSL, 0.25, 90),
_h(CB, 0.5, 100), _h(CT, 0.75, 50),
_h(CSL, 1.0, 88), _h(CT, 1.125, 42), _h(CSL, 1.25, 92),
_h(CT, 1.5, 55), _h(CSL, 1.75, 85),
_h(CB, 2.0, 105), _h(CSL, 2.25, 75), _h(CT, 2.5, 48),
_h(CSL, 2.75, 80), _h(CT, 2.875, 40),
_h(CB, 3.0, 110), _h(CSL, 3.25, 90), _h(CSL, 3.5, 95),
_h(CB, 3.75, 120),
],
)
# Cajón rumble — fast taps building to slap accents
Pattern._FILLS["cajon rumble"] = dict(
name="cajon rumble fill",
time_signature="4/4",
beats=4.0,
hits=[
*[_h(CT, i * 0.125, 35 + i * 3) for i in range(16)],
_h(CSL, 2.0, 95), _h(CSL, 2.5, 100),
_h(CB, 3.0, 108), _h(CSL, 3.25, 88),
_h(CB, 3.5, 112), _h(CSL, 3.75, 95),
],
)
# Cajón breakdown — syncopated bass-slap groove
Pattern._FILLS["cajon breakdown"] = dict(
name="cajon breakdown fill",
time_signature="4/4",
beats=4.0,
hits=[
_h(CB, 0.0, 100), _h(CT, 0.25, 45), _h(CSL, 0.5, 85),
_h(CB, 1.0, 95), _h(CSL, 1.25, 78), _h(CT, 1.5, 50),
_h(CSL, 1.75, 82),
_h(CB, 2.0, 105), _h(CT, 2.125, 40), _h(CT, 2.25, 42),
_h(CSL, 2.5, 90), _h(CT, 2.75, 48),
_h(CB, 3.0, 115), _h(CSL, 3.25, 95),
_h(CB, 3.5, 110), _h(CSL, 3.75, 100),
],
)
# ── Metal fills (using metal kit) ────────────────────────────────────────
# Metal triplet — double kick triplets with snare accents
Pattern._FILLS["metal triplet"] = dict(
name="metal triplet fill",
time_signature="4/4",
beats=4.0,
hits=[
# Triplet kick pattern (12 kicks across 4 beats = triplet 8ths)
*[_h(MK, i * (1/3), 95 + (i % 3 == 0) * 15) for i in range(12)],
# Snare accents on downbeats
_h(MS, 0.0, 110), _h(MS, 1.0, 105),
_h(MS, 2.0, 110), _h(MS, 3.0, 115),
# Hat on upbeats
_h(MH, 0.5, 60), _h(MH, 1.5, 60),
_h(MH, 2.5, 65), _h(MH, 3.5, 70),
],
)
# Metal blastbeat variant — alternating snare/kick 32nds
Pattern._FILLS["metal blast"] = dict(
name="metal blast fill",
time_signature="4/4",
beats=4.0,
hits=[
# Alternating kick-snare at 32nd note speed for 2 beats
*[_h(MK if i % 2 == 0 else MS, i * 0.125, 100 + i) for i in range(16)],
# Then crash into half-time for 2 beats
_h(MK, 2.0, 120), _h(MS, 2.5, 115),
_h(MK, 3.0, 120), _h(MH, 3.25, 80),
_h(MS, 3.5, 120),
],
)
# Metal cascade — descending snare/kick rolls
Pattern._FILLS["metal cascade"] = dict(
name="metal cascade fill",
time_signature="4/4",
beats=4.0,
hits=[
# Fast snare roll beat 1
*[_h(MS, i * 0.125, 80 + i * 5) for i in range(8)],
# Double kick beat 2
*[_h(MK, 1.0 + i * 0.125, 90 + i * 3) for i in range(8)],
# Alternating beat 3
_h(MS, 2.0, 105), _h(MK, 2.125, 95),
_h(MS, 2.25, 108), _h(MK, 2.375, 98),
_h(MS, 2.5, 110), _h(MK, 2.625, 100),
_h(MS, 2.75, 112), _h(MK, 2.875, 102),
# Crash ending
_h(MK, 3.0, 120), _h(MS, 3.0, 120),
_h(MK, 3.5, 120), _h(MS, 3.5, 120),
],
)
class Part:
"""A named voice within a Score, with its own synth, envelope, and effects.
+15 -2
View File
@@ -4869,6 +4869,19 @@ def test_duration_values():
assert abs(Duration.TRIPLET_QUARTER.value - 2 / 3) < 1e-9
def test_duration_arithmetic():
# Multiplication
assert Duration.WHOLE * 2 == 8.0
assert 2 * Duration.HALF == 4.0
assert Duration.QUARTER * 3 == 3.0
# Division
assert Duration.WHOLE / 2 == 2.0
# Addition
assert Duration.HALF + Duration.QUARTER == 3.0
assert Duration.HALF + 1.0 == 3.0
assert 1.0 + Duration.HALF == 3.0
def test_time_signature_from_string_4_4():
ts = TimeSignature.from_string("4/4")
assert ts.beats == 4
@@ -5320,7 +5333,7 @@ def test_supersaw_wave():
@needs_portaudio
def test_all_synths_in_enum():
from pytheory.play import Synth
assert len(Synth) == 41
assert len(Synth) == 42
for s in Synth:
wave = s(440, n_samples=1000)
assert len(wave) == 1000
@@ -7142,7 +7155,7 @@ def test_score_system_propagates():
def test_synth_enum_count():
from pytheory.play import Synth
assert len(Synth) == 41
assert len(Synth) == 42
def test_all_synths_render_and_enum_match():
Generated
+1 -1
View File
@@ -698,7 +698,7 @@ wheels = [
[[package]]
name = "pytheory"
version = "0.36.3"
version = "0.36.6"
source = { editable = "." }
dependencies = [
{ name = "numeral" },