From 840bfcc36ce64e59f6180dcade09feefc9abcc82 Mon Sep 17 00:00:00 2001 From: Kenneth Reitz Date: Sat, 28 Mar 2026 12:32:14 -0400 Subject: [PATCH] v0.37.0: Djembe expansion and cross-choke drum damping 5 new djembe patterns (dununba, tiriba, yankadi, djansa, mendiani), 3 djembe fills, cross-choke damping across drum families, and improved djembe slap synthesis. Co-Authored-By: Claude Opus 4.6 (1M context) --- CHANGELOG.md | 10 ++++ docs/guide/drums.rst | 20 +++++--- pyproject.toml | 2 +- pytheory/__init__.py | 2 +- pytheory/play.py | 67 ++++++++++++++++-------- pytheory/rhythm.py | 119 +++++++++++++++++++++++++++++++++++++++++++ uv.lock | 2 +- 7 files changed, 192 insertions(+), 30 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3841584..e0be76a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,16 @@ All notable changes to PyTheory are documented here. +## 0.37.0 + +- **5 new djembe patterns** — dununba, tiriba, yankadi, djansa, mendiani +- **3 new djembe fills** — djembe call, djembe roll, djembe break (30 fills total) +- **Cross-choke drum damping** — striking one sound on a hand drum fades + out the ring of related sounds (djembe slap kills bass resonance, closed + hat chokes open hat, cajón slap dampens bass, doumbek tek dampens dum) +- **Improved djembe slap** — dry, high-pitched goatskin pop instead of + snare-like noise rattle + ## 0.36.6 - **6 new drum fills** — 3 cajón (flam, rumble, breakdown) and 3 metal diff --git a/docs/guide/drums.rst b/docs/guide/drums.rst index de3fc57..e3e1ecb 100644 --- a/docs/guide/drums.rst +++ b/docs/guide/drums.rst @@ -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 27 fill presets. +sounds, 85+ pattern presets across dozens of genres, and 30 fill presets. Every sound is generated from waveforms; no samples needed. Drum Sounds @@ -252,14 +252,15 @@ 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. 27 fill presets are available: +transitions between sections. 30 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', + 'cumbia', 'disco', 'djembe break', 'djembe call', 'djembe roll', + 'funk', 'highlife', 'hip hop', 'house', 'jazz', 'jazz brush', 'metal', 'metal blast', 'metal cascade', 'metal triplet', 'reggae', 'rock', 'rock crash', 'salsa', 'samba', 'second line', 'trap'] @@ -435,14 +436,19 @@ central to the drum ensemble traditions of Mali, Guinea, and Senegal. **3 sounds** -- bass (open center strike), tone (edge strike), and slap (sharp edge strike). -**3 patterns:** djembe (a basic accompanying rhythm), kuku (a -traditional rhythm from Guinea associated with fishing), and soli (a -solo/celebration rhythm). +**8 patterns:** djembe (basic accompanying rhythm), kuku (Guinean harvest +dance), soli (powerful Mandinka rhythm), dununba (heavy bass-driven), +tiriba (joyful Susu rhythm), yankadi (gentle greeting/welcome), djansa +(fast Malinke dance), mendiani (women's celebratory dance). + +**3 fills:** djembe call (bass-tone-slap conversation building to climax), +djembe roll (rapid slaps accelerating into bass), djembe break (syncopated +West African-style break). .. code-block:: python score = Score("4/4", bpm=120) - score.drums("djembe", repeats=4) + score.drums("djembe", repeats=8, fill="djembe call", fill_every=4) Metal Kit ~~~~~~~~~ diff --git a/pyproject.toml b/pyproject.toml index 2677ece..03c0f86 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "pytheory" -version = "0.36.6" +version = "0.37.0" description = "Music Theory for Humans" readme = "README.md" license = "MIT" diff --git a/pytheory/__init__.py b/pytheory/__init__.py index 0bea38e..b77672f 100644 --- a/pytheory/__init__.py +++ b/pytheory/__init__.py @@ -1,6 +1,6 @@ """PyTheory: Music Theory for Humans.""" -__version__ = "0.36.6" +__version__ = "0.37.0" from .tones import Tone, Interval from .systems import System, SYSTEMS, TET diff --git a/pytheory/play.py b/pytheory/play.py index a93fcfa..19707ff 100644 --- a/pytheory/play.py +++ b/pytheory/play.py @@ -2890,29 +2890,27 @@ def _synth_djembe_tone(n_samples): def _synth_djembe_slap(n_samples): """Djembe slap — edge strike with fingers spread, sharp crack. - The highest, sharpest djembe sound. Fingers fan out on contact - creating a loud crack with minimal sustain. + The highest, sharpest djembe sound. A dry, high-pitched pop from + goatskin membrane — NOT a snare. Tight attack, very short decay, + skin character rather than wire rattle. """ t = numpy.arange(n_samples, dtype=numpy.float32) / SAMPLE_RATE - # Sharp crack — mostly noise - crack_len = min(int(SAMPLE_RATE * 0.02), n_samples) - crack = _noise(crack_len) * _exp_decay(crack_len, 100) * 1.0 - # Brief high-pitched ring - ring = numpy.sin(2 * numpy.pi * 600 * t) * _exp_decay(n_samples, 25) * 0.4 - ring2 = numpy.sin(2 * numpy.pi * 1200 * t) * 0.2 * _exp_decay(n_samples, 35) - # Brief membrane pop - thump_len = min(int(SAMPLE_RATE * 0.02), n_samples) - thump_raw = _noise(thump_len) - if thump_len > 20: - bl, al = scipy.signal.butter(2, [300, 2000], btype='band', fs=SAMPLE_RATE) - thump = scipy.signal.lfilter(bl, al, numpy.pad(thump_raw, (0, max(0, n_samples - thump_len))))[:thump_len] + # High membrane pop — goatskin resonance, much higher than snare + pop = numpy.sin(2 * numpy.pi * 900 * t) * _exp_decay(n_samples, 50) * 0.5 + pop2 = numpy.sin(2 * numpy.pi * 1600 * t) * _exp_decay(n_samples, 60) * 0.25 + pop3 = numpy.sin(2 * numpy.pi * 2400 * t) * _exp_decay(n_samples, 80) * 0.12 + # Very short filtered click — hand-on-skin transient, not noise rattle + click_len = min(int(SAMPLE_RATE * 0.008), n_samples) + click_raw = _noise(click_len) + if click_len > 20: + bl, al = scipy.signal.butter(2, 1800 / (SAMPLE_RATE / 2), btype='high') + click = scipy.signal.lfilter(bl, al, numpy.pad(click_raw, (0, max(0, n_samples - click_len))))[:click_len] else: - thump = thump_raw - thump *= _exp_decay(thump_len, 80) * 0.8 - result = ring + ring2 - result[:crack_len] += crack - result[:thump_len] += thump - return numpy.tanh(result * 1.7) + click = click_raw + click *= _exp_decay(click_len, 150) * 0.6 + result = pop + pop2 + pop3 + result[:click_len] += click + return numpy.tanh(result * 1.5) def _synth_guiro(n_samples): @@ -4661,6 +4659,35 @@ def render_score(score): part_stereo[fade_start:start, ch] *= fade _last_hit_start[sound_id] = start + # Cross-choke: a new hit on one sound dampens the ring of + # related sounds on the same instrument (e.g. djembe slap + # kills the bass resonance, closed hat kills open hat). + _CHOKE_GROUPS = { + # Djembe — any strike dampens the others + DrumSound.DJEMBE_BASS.value: (DrumSound.DJEMBE_TONE.value, DrumSound.DJEMBE_SLAP.value), + DrumSound.DJEMBE_TONE.value: (DrumSound.DJEMBE_BASS.value, DrumSound.DJEMBE_SLAP.value), + DrumSound.DJEMBE_SLAP.value: (DrumSound.DJEMBE_BASS.value, DrumSound.DJEMBE_TONE.value), + # Hi-hats — closed chokes open + DrumSound.CLOSED_HAT.value: (DrumSound.OPEN_HAT.value,), + DrumSound.PEDAL_HAT.value: (DrumSound.OPEN_HAT.value,), + # Cajón — slap dampens bass ring + DrumSound.CAJON_SLAP.value: (DrumSound.CAJON_BASS.value,), + DrumSound.CAJON_TAP.value: (DrumSound.CAJON_BASS.value,), + # Doumbek — tek/ka dampen dum + DrumSound.DOUMBEK_TEK.value: (DrumSound.DOUMBEK_DUM.value,), + DrumSound.DOUMBEK_KA.value: (DrumSound.DOUMBEK_DUM.value,), + } + choke_targets = _CHOKE_GROUPS.get(sound_id, ()) + for target_id in choke_targets: + if target_id in _last_hit_start: + prev_start = _last_hit_start[target_id] + fade_len = min(int(SAMPLE_RATE * 0.004), max(0, start - prev_start)) + if fade_len > 0 and start > 0: + fade = numpy.linspace(1.0, 0.0, fade_len).astype(numpy.float32) + fade_start = max(0, start - fade_len) + for ch in range(2): + part_stereo[fade_start:start, ch] *= fade + remaining = total_samples - start hit_len = min(int(SAMPLE_RATE * 0.5), remaining) wave = _render_drum_hit(hit.sound.value, hit_len) diff --git a/pytheory/rhythm.py b/pytheory/rhythm.py index 1c54b26..9d7ef6f 100644 --- a/pytheory/rhythm.py +++ b/pytheory/rhythm.py @@ -1907,6 +1907,74 @@ Pattern._PRESETS["soli"] = dict( ], ) +# Dununba — heavy bass-driven rhythm (accompaniment djembe part) +Pattern._PRESETS["dununba"] = dict( + name="dununba", + time_signature="4/4", + beats=4.0, + hits=[ + _h(JB, 0.0, 110), _h(JB, 0.5, 95), + _h(JT, 1.0, 75), _h(JB, 1.5, 100), + _h(JB, 2.0, 108), _h(JT, 2.5, 70), + _h(JB, 3.0, 105), _h(JB, 3.5, 90), _h(JT, 3.75, 65), + ], +) + +# Tiriba — joyful Susu rhythm from Guinea +Pattern._PRESETS["tiriba"] = dict( + name="tiriba", + time_signature="4/4", + beats=4.0, + hits=[ + _h(JT, 0.0, 85), _h(JS, 0.25, 95), _h(JT, 0.5, 80), + _h(JB, 1.0, 100), _h(JT, 1.5, 75), + _h(JS, 2.0, 92), _h(JT, 2.25, 78), _h(JT, 2.5, 80), + _h(JB, 3.0, 105), _h(JS, 3.5, 88), _h(JT, 3.75, 72), + ], +) + +# Yankadi — gentle greeting/welcome rhythm from Guinea +Pattern._PRESETS["yankadi"] = dict( + name="yankadi", + time_signature="4/4", + beats=4.0, + hits=[ + _h(JB, 0.0, 90), _h(JT, 0.5, 70), + _h(JT, 1.0, 72), _h(JS, 1.5, 85), + _h(JB, 2.0, 88), _h(JT, 2.5, 68), + _h(JS, 3.0, 82), _h(JT, 3.5, 65), + ], +) + +# Djansa — fast Malinke dance rhythm +Pattern._PRESETS["djansa"] = dict( + name="djansa", + time_signature="4/4", + beats=4.0, + hits=[ + _h(JS, 0.0, 100), _h(JT, 0.25, 72), _h(JT, 0.5, 70), + _h(JB, 0.75, 95), + _h(JS, 1.0, 98), _h(JT, 1.25, 68), _h(JB, 1.5, 92), + _h(JS, 2.0, 102), _h(JT, 2.25, 75), _h(JT, 2.5, 72), + _h(JB, 2.75, 90), + _h(JS, 3.0, 105), _h(JT, 3.25, 70), _h(JB, 3.5, 95), + _h(JS, 3.75, 88), + ], +) + +# Mendiani — women's dance rhythm, celebratory +Pattern._PRESETS["mendiani"] = dict( + name="mendiani", + time_signature="4/4", + beats=4.0, + hits=[ + _h(JB, 0.0, 100), _h(JT, 0.25, 65), _h(JS, 0.5, 90), + _h(JT, 1.0, 70), _h(JB, 1.5, 95), _h(JT, 1.75, 68), + _h(JS, 2.0, 92), _h(JT, 2.5, 72), _h(JS, 2.75, 85), + _h(JB, 3.0, 105), _h(JT, 3.25, 65), _h(JS, 3.5, 95), + ], +) + # ── Fill presets ────────────────────────────────────────────────────────── Pattern._FILLS["rock"] = dict( @@ -2292,6 +2360,57 @@ Pattern._FILLS["tabla call"] = dict( ], ) +# ── Djembe fills ───────────────────────────────────────────────────────── + +# Djembe call — bass-tone-slap conversation building to climax +Pattern._FILLS["djembe call"] = dict( + name="djembe call fill", + time_signature="4/4", + beats=4.0, + hits=[ + _h(JB, 0.0, 100), _h(JT, 0.25, 70), _h(JT, 0.5, 72), + _h(JS, 0.75, 90), + _h(JB, 1.0, 95), _h(JT, 1.25, 68), _h(JS, 1.5, 88), + _h(JT, 1.75, 75), + _h(JS, 2.0, 100), _h(JS, 2.25, 95), _h(JT, 2.5, 78), + _h(JB, 2.75, 105), + _h(JS, 3.0, 110), _h(JT, 3.25, 80), _h(JS, 3.5, 112), + _h(JB, 3.75, 120), + ], +) + +# Djembe roll — rapid slaps accelerating into bass +Pattern._FILLS["djembe roll"] = dict( + name="djembe roll fill", + time_signature="4/4", + beats=4.0, + hits=[ + # Accelerating slap roll + *[_h(JS, i * 0.125, 50 + i * 4) for i in range(16)], + # Bass accents punching through + _h(JB, 2.0, 105), _h(JB, 2.5, 108), + _h(JB, 3.0, 112), _h(JT, 3.25, 85), + _h(JB, 3.5, 115), _h(JS, 3.75, 100), + ], +) + +# Djembe break — syncopated West African-style break +Pattern._FILLS["djembe break"] = dict( + name="djembe break fill", + time_signature="4/4", + beats=4.0, + hits=[ + _h(JB, 0.0, 105), _h(JT, 0.25, 65), _h(JS, 0.5, 90), + _h(JT, 0.75, 70), _h(JB, 1.0, 100), + _h(JS, 1.25, 85), _h(JS, 1.5, 88), + _h(JB, 1.75, 95), _h(JT, 2.0, 72), + _h(JS, 2.25, 92), _h(JB, 2.5, 108), + _h(JT, 2.75, 68), _h(JS, 2.875, 55), + _h(JB, 3.0, 115), _h(JS, 3.25, 100), + _h(JB, 3.5, 118), _h(JB, 3.75, 120), + ], +) + # ── Cajón fills ────────────────────────────────────────────────────────── # Cajón flam run — slaps accelerating into bass hit diff --git a/uv.lock b/uv.lock index 0646b4b..4a4577a 100644 --- a/uv.lock +++ b/uv.lock @@ -698,7 +698,7 @@ wheels = [ [[package]] name = "pytheory" -version = "0.36.6" +version = "0.37.0" source = { editable = "." } dependencies = [ { name = "numeral" },