Compare commits

..

8 Commits

Author SHA1 Message Date
kennethreitz a2740b8d57 v0.38.0: Articulations, Part.hit(), djembe expansion, cross-choke
Articulations (staccato, legato, marcato, tenuto, accent, fermata)
on Part.add() and Part.hold(). Part.hit() for placing individual
drum sounds with articulation support. 5 new djembe patterns,
3 djembe fills, cross-choke damping, improved djembe slap.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-28 15:14:21 -04:00
kennethreitz 840bfcc36c 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) <noreply@anthropic.com>
2026-03-28 12:32:14 -04:00
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 753 additions and 45 deletions
+33
View File
@@ -2,6 +2,39 @@
All notable changes to PyTheory are documented here.
## 0.38.0
- **Articulations** — `staccato`, `legato`, `marcato`, `tenuto`, `accent`,
`fermata` via `articulation=` on `Part.add()` and `Part.hold()`
- **`Part.hit()`** — place individual drum sounds in a Part's note stream
with articulation, velocity, and effects support
- **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 fades out related sounds
(djembe, hi-hats, cajón, doumbek)
- **Improved djembe slap** — dry goatskin pop instead of snare-like noise
## 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
(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
+38 -11
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, 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,17 @@ 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. 30 fill presets are available:
.. code-block:: pycon
>>> Pattern.list_fills()
['afrobeat', 'blast', 'bossa nova', 'breakdown', 'buildup',
'cumbia', 'disco', 'funk', 'highlife', 'hip hop', 'house',
'jazz', 'jazz brush', 'metal', 'reggae', 'rock', 'rock crash',
'cajon breakdown', 'cajon flam', 'cajon rumble',
'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']
>>> fill = Pattern.fill("rock")
@@ -357,6 +360,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)
@@ -424,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
~~~~~~~~~
@@ -447,10 +464,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 +481,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.38.0"
description = "Music Theory for Humans"
readme = "README.md"
license = "MIT"
+1 -1
View File
@@ -1,6 +1,6 @@
"""PyTheory: Music Theory for Humans."""
__version__ = "0.36.3"
__version__ = "0.38.0"
from .tones import Tone, Interval
from .systems import System, SYSTEMS, TET
+193 -25
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.
@@ -2802,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):
@@ -2914,6 +3000,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),
@@ -4055,10 +4145,48 @@ def _render_notes_to_buf(notes, buf, samples_per_beat, total_samples,
start += _rnd.randint(-max_offset, max_offset)
start = max(0, start)
dur_ms = note.beats * 60_000 / bpm
# Articulation: adjust duration and velocity
art = getattr(note, 'articulation', '')
art_vel_mult = 1.0
art_attack_mult = 1.0 # multiplier for envelope attack
if art == 'staccato':
dur_ms *= 0.4 # short and bouncy
elif art == 'legato':
dur_ms *= 1.15 # slight overlap into next note
elif art == 'marcato':
art_vel_mult = 1.25 # heavier
art_attack_mult = 0.3 # sharper attack
elif art == 'tenuto':
art_attack_mult = 1.8 # softer attack, full duration
elif art == 'accent':
art_vel_mult = 1.2
elif art == 'fermata':
dur_ms *= 1.5 # held longer
n_samples = int(SAMPLE_RATE * dur_ms / 1000)
if start + n_samples > total_samples:
n_samples = total_samples - start
if n_samples > 0 and start >= 0:
# Drum hit via Part.hit() — use drum synth directly
from .rhythm import _DrumTone
if isinstance(note.tone, _DrumTone):
drum_wave = _render_drum_hit(note.tone.sound.value, n_samples)
mixed = drum_wave.astype(numpy.float32)
# Staccato fade-out for drums
if art == 'staccato':
fade_len = min(int(SAMPLE_RATE * 0.01), len(mixed))
if fade_len > 0:
mixed[-fade_len:] *= numpy.linspace(1.0, 0.0, fade_len).astype(numpy.float32)
vel = getattr(note, 'velocity', 100)
vel = min(127, int(vel * art_vel_mult))
if humanize > 0.0:
vel_jitter = int(humanize * 15)
vel = max(1, min(127, vel + _rnd.randint(-vel_jitter, vel_jitter)))
vel_scale = vel / 127.0
end = min(start + len(mixed), total_samples)
buf[start:end] += mixed[:end - start] * volume * vel_scale
if not getattr(note, '_hold', False):
beat_pos += note.beats
continue
# Get pitches
if hasattr(note.tone, 'tones'):
pitches = [t.pitch(temperament=temperament, reference_pitch=reference_pitch) for t in note.tone.tones]
@@ -4159,11 +4287,18 @@ def _render_notes_to_buf(notes, buf, samples_per_beat, total_samples,
if noise_mix > 0:
noise = numpy.random.uniform(-1, 1, n_samples).astype(numpy.float32)
mixed = mixed * (1.0 - noise_mix * 0.5) + noise * noise_mix * 0.5
# Amplitude envelope
if a > 0 or d > 0 or s < 1.0 or r > 0:
mixed = _apply_envelope(mixed, a, d, s, r)
# Per-note velocity
# Amplitude envelope (articulation may adjust attack)
art_a = a * art_attack_mult
if art_a > 0 or d > 0 or s < 1.0 or r > 0:
mixed = _apply_envelope(mixed, art_a, d, s, r)
# Staccato: apply a quick fade-out at the end
if art == 'staccato':
fade_len = min(int(SAMPLE_RATE * 0.01), len(mixed))
if fade_len > 0:
mixed[-fade_len:] *= numpy.linspace(1.0, 0.0, fade_len).astype(numpy.float32)
# Per-note velocity (articulation may boost)
vel = getattr(note, 'velocity', 100)
vel = min(127, int(vel * art_vel_mult))
if humanize > 0.0:
vel_jitter = int(humanize * 15)
vel = max(1, min(127, vel + _rnd.randint(-vel_jitter, vel_jitter)))
@@ -4510,6 +4645,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,
@@ -4565,6 +4704,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)
+471 -4
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."""
@@ -426,6 +449,7 @@ class Note:
bend: float = 0.0
bend_type: str = "smooth" # "smooth" (log), "linear", "late"
lyric: str = "" # syllable for vocal synth
articulation: str = "" # "", "staccato", "legato", "marcato", "tenuto", "accent", "fermata"
_hold: bool = False # if True, don't advance beat position
@property
@@ -528,6 +552,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
@@ -538,6 +566,17 @@ class DrumSound(Enum):
METAL_HAT = 107 # tight, short, precise
class _DrumTone:
"""Wrapper so a DrumSound can be placed in a Part's note list."""
__slots__ = ('sound',)
def __init__(self, sound: DrumSound):
self.sound = sound
def pitch(self, **kwargs):
return -self.sound.value
class _Hit:
"""A single drum hit at a specific position in a pattern."""
__slots__ = ("sound", "position", "velocity")
@@ -1563,6 +1602,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
@@ -1825,6 +1919,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(
@@ -2093,6 +2255,277 @@ 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),
],
)
# ── 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
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.
@@ -2203,7 +2636,8 @@ 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", lyric: str = "") -> "Part":
bend: float = 0.0, bend_type: str = "smooth", lyric: str = "",
articulation: 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).
@@ -2211,6 +2645,10 @@ class Part:
Bend specifies a pitch bend in semitones over the note duration
(e.g. ``bend=2`` bends up a whole step, ``bend=-1`` bends down
a half step). Used for guitar bends, sitar meends, slides.
Articulation changes how the note is played: ``"staccato"`` (short,
~40% duration), ``"legato"`` (overlaps next note), ``"marcato"``
(heavy accent), ``"tenuto"`` (full duration, soft attack),
``"accent"`` (velocity bump), ``"fermata"`` (held ~50% longer).
Returns self for chaining.
"""
@@ -2221,11 +2659,13 @@ class Part:
duration = _RawDuration(duration)
self.notes.append(Note(tone=tone_or_string, duration=duration,
velocity=velocity, bend=bend,
bend_type=bend_type, lyric=lyric))
bend_type=bend_type, lyric=lyric,
articulation=articulation))
return self
def hold(self, tone_or_string, duration=Duration.QUARTER, *, velocity: int = 100,
bend: float = 0.0, bend_type: str = "smooth", lyric: str = "") -> "Part":
bend: float = 0.0, bend_type: str = "smooth", lyric: str = "",
articulation: str = "") -> "Part":
"""Add a note without advancing the beat position.
The note plays at the current position but the next note
@@ -2250,7 +2690,34 @@ class Part:
duration = _RawDuration(duration)
self.notes.append(Note(tone=tone_or_string, duration=duration,
velocity=velocity, bend=bend,
bend_type=bend_type, lyric=lyric, _hold=True))
bend_type=bend_type, lyric=lyric,
articulation=articulation, _hold=True))
return self
def hit(self, sound, duration=Duration.EIGHTH, *, velocity: int = 100,
articulation: str = "") -> "Part":
"""Add a drum hit to this part.
Places a drum sound into the note stream so it goes through the
normal renderer — meaning articulations, humanize, and effects
all work on individual hits.
Args:
sound: A :class:`DrumSound` enum member (e.g. ``DrumSound.KICK``).
duration: How long the hit occupies in the timeline (default 8th note).
velocity: Hit loudness 1-127.
articulation: ``"accent"``, ``"staccato"``, ``"marcato"``, etc.
Example::
>>> drums = score.part("kit", synth="sine")
>>> drums.hit(DrumSound.KICK, Duration.QUARTER, articulation="accent")
>>> drums.hit(DrumSound.CLOSED_HAT, Duration.EIGHTH)
"""
if isinstance(duration, (int, float)):
duration = _RawDuration(duration)
self.notes.append(Note(tone=_DrumTone(sound), duration=duration,
velocity=velocity, articulation=articulation))
return self
def set(self, **params) -> "Part":
+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.38.0"
source = { editable = "." }
dependencies = [
{ name = "numeral" },