Add dhol, dholak, mridangam, djembe drums + 11 world patterns

Drum synthesis:
- Dhol: dagga (heavy bass), tilli (treble crack), both
- Dholak: ge (bass palm), na (treble fingers), tit (light tap)
- Mridangam: tham (clay body bass), nam (rich overtone ring),
  din (both heads), tha (muted)
- Djembe: bass (center palm), tone (edge ring), slap (sharp crack)
All with bandpass-filtered membrane noise for drum head character.

Patterns:
- Dhol: bhangra, dhol chaal
- Dholak: qawwali, dholak folk
- Mridangam: adi talam, mridangam korvai
- Djembe: djembe (standard), kuku, soli

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-03-27 00:59:27 -04:00
parent 9b62b56120
commit 267b7284ba
2 changed files with 473 additions and 0 deletions
+318
View File
@@ -1055,6 +1055,290 @@ def _synth_tabla_ke(n_samples):
return out
def _synth_dhol_dagga(n_samples):
"""Dhol dagga — heavy bass side hit with thick stick.
The dhol's bass head is thick goatskin, hit with a heavy curved
stick (dagga). Massive low-end punch, the sound of bhangra.
"""
t = numpy.arange(n_samples, dtype=numpy.float32) / SAMPLE_RATE
# Heavy membrane thud
thump_len = min(int(SAMPLE_RATE * 0.08), n_samples)
thump_raw = _noise(thump_len)
if thump_len > 20:
bl, al = scipy.signal.butter(2, [30, 180], btype='band', fs=SAMPLE_RATE)
thump = scipy.signal.lfilter(bl, al, numpy.pad(thump_raw, (0, max(0, n_samples - thump_len))))[:thump_len]
else:
thump = thump_raw
thump *= _exp_decay(thump_len, 15) * 1.0
# Deep pitched body — lower than tabla bayan
freq = 50 + 60 * numpy.exp(-20 * t)
phase = 2 * numpy.pi * numpy.cumsum(freq) / SAMPLE_RATE
body = numpy.sin(phase) * _exp_decay(n_samples, 8) * 0.9
# Sub boom
sub = _sine_f32(35, n_samples) * _exp_decay(n_samples, 10) * 0.6
# Stick attack — heavier than tabla palm
click_len = min(200, n_samples)
click = _noise(click_len) * _exp_decay(click_len, 60) * 0.5
result = body + sub
result[:thump_len] += thump
result[:click_len] += click
return numpy.tanh(result * 1.5)
def _synth_dhol_tilli(n_samples):
"""Dhol tilli — thin treble side hit with light stick.
The treble head is thinner goatskin, hit with a thin bamboo stick
(tilli). Bright, cutting, high-pitched crack.
"""
t = numpy.arange(n_samples, dtype=numpy.float32) / SAMPLE_RATE
# Thin membrane snap
thump_len = min(int(SAMPLE_RATE * 0.03), n_samples)
thump_raw = _noise(thump_len)
if thump_len > 20:
bl, al = scipy.signal.butter(2, [400, 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]
else:
thump = thump_raw
thump *= _exp_decay(thump_len, 60) * 0.9
# Higher pitched ring
ring = numpy.sin(2 * numpy.pi * 500 * t) * _exp_decay(n_samples, 20) * 0.5
ring2 = numpy.sin(2 * numpy.pi * 1200 * t) * 0.3 * _exp_decay(n_samples, 30)
# Sharp stick crack
click_len = min(80, n_samples)
click = _noise(click_len) * _exp_decay(click_len, 300) * 0.9
result = ring + ring2
result[:thump_len] += thump
result[:click_len] += click
return numpy.tanh(result * 1.6)
def _synth_dhol_both(n_samples):
"""Dhol both sides — full power bhangra hit."""
dagga = _synth_dhol_dagga(n_samples) * 0.6
tilli = _synth_dhol_tilli(n_samples) * 0.5
return numpy.tanh(dagga + tilli)
def _synth_dholak_ge(n_samples):
"""Dholak Ge — bass side open palm hit.
The dholak is lighter and higher-pitched than the dhol, used
in folk music and qawwali. Bass side has cotton/thread tuning.
"""
t = numpy.arange(n_samples, dtype=numpy.float32) / SAMPLE_RATE
thump_len = min(int(SAMPLE_RATE * 0.05), n_samples)
thump_raw = _noise(thump_len)
if thump_len > 20:
bl, al = scipy.signal.butter(2, [60, 300], btype='band', fs=SAMPLE_RATE)
thump = scipy.signal.lfilter(bl, al, numpy.pad(thump_raw, (0, max(0, n_samples - thump_len))))[:thump_len]
else:
thump = thump_raw
thump *= _exp_decay(thump_len, 25) * 0.7
body = numpy.sin(2 * numpy.pi * 90 * t) * _exp_decay(n_samples, 8) * 0.7
sub = _sine_f32(55, n_samples) * _exp_decay(n_samples, 10) * 0.4
click_len = min(150, n_samples)
click = _noise(click_len) * _exp_decay(click_len, 50) * 0.3
result = body + sub
result[:thump_len] += thump
result[:click_len] += click
return numpy.tanh(result * 1.3)
def _synth_dholak_na(n_samples):
"""Dholak Na — treble side finger strike."""
t = numpy.arange(n_samples, dtype=numpy.float32) / SAMPLE_RATE
thump_len = min(int(SAMPLE_RATE * 0.03), n_samples)
thump_raw = _noise(thump_len)
if thump_len > 20:
bl, al = scipy.signal.butter(2, [250, 1200], btype='band', fs=SAMPLE_RATE)
thump = scipy.signal.lfilter(bl, al, numpy.pad(thump_raw, (0, max(0, n_samples - thump_len))))[:thump_len]
else:
thump = thump_raw
thump *= _exp_decay(thump_len, 40) * 0.7
ring = numpy.sin(2 * numpy.pi * 400 * t) * _exp_decay(n_samples, 12) * 0.5
ring2 = numpy.sin(2 * numpy.pi * 850 * t) * 0.3 * _exp_decay(n_samples, 18)
click_len = min(80, n_samples)
click = _noise(click_len) * _exp_decay(click_len, 200) * 0.6
result = ring + ring2
result[:thump_len] += thump
result[:click_len] += click
return numpy.tanh(result * 1.4)
def _synth_dholak_tit(n_samples):
"""Dholak light tap — fast finger pattern sound."""
n = min(n_samples, int(SAMPLE_RATE * 0.05))
pop = _noise(min(60, n)) * _exp_decay(min(60, n), 300) * 0.8
t = numpy.arange(n, dtype=numpy.float32) / SAMPLE_RATE
ring = numpy.sin(2 * numpy.pi * 500 * t) * _exp_decay(n, 30) * 0.4
result = ring
result[:min(60, n)] += pop
out = numpy.zeros(n_samples, dtype=numpy.float32)
out[:n] = numpy.tanh(result * 1.6)
return out
def _synth_mridangam_tham(n_samples):
"""Mridangam Tham — bass stroke on the left head (thoppi).
The mridangam's left head is tuned with wet wheat paste, giving
a darker, more muted bass than tabla's bayan. Clay body resonance.
"""
t = numpy.arange(n_samples, dtype=numpy.float32) / SAMPLE_RATE
# Membrane with wheat paste — darker character
thump_len = min(int(SAMPLE_RATE * 0.06), n_samples)
thump_raw = _noise(thump_len)
if thump_len > 20:
bl, al = scipy.signal.butter(2, [40, 200], btype='band', fs=SAMPLE_RATE)
thump = scipy.signal.lfilter(bl, al, numpy.pad(thump_raw, (0, max(0, n_samples - thump_len))))[:thump_len]
else:
thump = thump_raw
thump *= _exp_decay(thump_len, 18) * 0.8
# Clay body resonance — warmer than metal bayan
body = numpy.sin(2 * numpy.pi * 70 * t) * _exp_decay(n_samples, 6) * 0.8
clay = numpy.sin(2 * numpy.pi * 140 * t) * 0.25 * _exp_decay(n_samples, 9)
click_len = min(200, n_samples)
click = _noise(click_len) * _exp_decay(click_len, 40) * 0.25
result = body + clay
result[:thump_len] += thump
result[:click_len] += click
return numpy.tanh(result * 1.2)
def _synth_mridangam_nam(n_samples):
"""Mridangam Nam — treble ring on the right head (valanthalai).
The right head has a permanent syahi (called soru) that gives
a clear, bell-like pitch. More overtones than tabla dayan.
"""
t = numpy.arange(n_samples, dtype=numpy.float32) / SAMPLE_RATE
thump_len = min(int(SAMPLE_RATE * 0.04), n_samples)
thump_raw = _noise(thump_len)
if thump_len > 20:
bl, al = scipy.signal.butter(2, [200, 900], btype='band', fs=SAMPLE_RATE)
thump = scipy.signal.lfilter(bl, al, numpy.pad(thump_raw, (0, max(0, n_samples - thump_len))))[:thump_len]
else:
thump = thump_raw
thump *= _exp_decay(thump_len, 35) * 0.7
# Rich overtone ring — more harmonics than tabla
ring = numpy.sin(2 * numpy.pi * 300 * t) * _exp_decay(n_samples, 7) * 0.6
ring2 = numpy.sin(2 * numpy.pi * 600 * t) * 0.4 * _exp_decay(n_samples, 9)
ring3 = numpy.sin(2 * numpy.pi * 920 * t) * 0.25 * _exp_decay(n_samples, 12)
ring4 = numpy.sin(2 * numpy.pi * 1250 * t) * 0.15 * _exp_decay(n_samples, 16)
click_len = min(100, n_samples)
click = _noise(click_len) * _exp_decay(click_len, 200) * 0.5
result = ring + ring2 + ring3 + ring4
result[:thump_len] += thump
result[:click_len] += click
return numpy.tanh(result * 1.3)
def _synth_mridangam_din(n_samples):
"""Mridangam Din — both heads simultaneously."""
nam = _synth_mridangam_nam(n_samples) * 0.5
tham = _synth_mridangam_tham(n_samples) * 0.6
return numpy.tanh(nam + tham)
def _synth_mridangam_tha(n_samples):
"""Mridangam Tha — muted treble stroke."""
n = min(n_samples, int(SAMPLE_RATE * 0.07))
t = numpy.arange(n, dtype=numpy.float32) / SAMPLE_RATE
thump_len = min(int(SAMPLE_RATE * 0.03), n)
thump = _noise(thump_len) * _exp_decay(thump_len, 50) * 0.7
body = numpy.sin(2 * numpy.pi * 280 * t) * _exp_decay(n, 22) * 0.5
result = body
result[:thump_len] += thump
out = numpy.zeros(n_samples, dtype=numpy.float32)
out[:n] = numpy.tanh(result * 1.4)
return out
def _synth_djembe_bass(n_samples):
"""Djembe bass — open palm strike in center of goatskin head.
Deep, warm, round bass. The goblet-shaped wooden body amplifies
the low frequencies. Played with a flat palm in the center.
"""
t = numpy.arange(n_samples, dtype=numpy.float32) / SAMPLE_RATE
# Goatskin membrane — prominent, round
thump_len = min(int(SAMPLE_RATE * 0.08), n_samples)
thump_raw = _noise(thump_len)
if thump_len > 20:
bl, al = scipy.signal.butter(2, [50, 250], btype='band', fs=SAMPLE_RATE)
thump = scipy.signal.lfilter(bl, al, numpy.pad(thump_raw, (0, max(0, n_samples - thump_len))))[:thump_len]
else:
thump = thump_raw
thump *= _exp_decay(thump_len, 15) * 0.9
# Round bass body — goblet resonance
body = numpy.sin(2 * numpy.pi * 65 * t) * _exp_decay(n_samples, 6) * 0.9
sub = _sine_f32(45, n_samples) * _exp_decay(n_samples, 8) * 0.5
# Warm palm attack
click_len = min(200, n_samples)
click = _noise(click_len) * _exp_decay(click_len, 35) * 0.25
result = body + sub
result[:thump_len] += thump
result[:click_len] += click
return numpy.tanh(result * 1.3)
def _synth_djembe_tone(n_samples):
"""Djembe tone — open edge strike, fingers together.
Clear, pitched, ringing. Fingers strike the edge of the head with
the palm off the surface so the head rings freely.
"""
t = numpy.arange(n_samples, dtype=numpy.float32) / SAMPLE_RATE
# Goatskin membrane at the edge — brighter than center
thump_len = min(int(SAMPLE_RATE * 0.04), n_samples)
thump_raw = _noise(thump_len)
if thump_len > 20:
bl, al = scipy.signal.butter(2, [150, 800], btype='band', fs=SAMPLE_RATE)
thump = scipy.signal.lfilter(bl, al, numpy.pad(thump_raw, (0, max(0, n_samples - thump_len))))[:thump_len]
else:
thump = thump_raw
thump *= _exp_decay(thump_len, 35) * 0.7
# Clear pitched ring
ring = numpy.sin(2 * numpy.pi * 250 * t) * _exp_decay(n_samples, 8) * 0.6
ring2 = numpy.sin(2 * numpy.pi * 500 * t) * 0.3 * _exp_decay(n_samples, 12)
click_len = min(100, n_samples)
click = _noise(click_len) * _exp_decay(click_len, 150) * 0.5
result = ring + ring2
result[:thump_len] += thump
result[:click_len] += click
return numpy.tanh(result * 1.3)
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.
"""
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]
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)
def _synth_guiro(n_samples):
"""Guiro: scraped ridged surface — rhythmic noise bursts."""
wave = numpy.zeros(n_samples, dtype=numpy.float32)
@@ -1124,6 +1408,23 @@ def _render_drum_hit(sound_value, n_samples):
DrumSound.TABLA_DHA.value: lambda n: _synth_tabla_dha(n),
DrumSound.TABLA_TIT.value: lambda n: _synth_tabla_tit(n),
DrumSound.TABLA_KE.value: lambda n: _synth_tabla_ke(n),
# Dhol
DrumSound.DHOL_DAGGA.value: lambda n: _synth_dhol_dagga(n),
DrumSound.DHOL_TILLI.value: lambda n: _synth_dhol_tilli(n),
DrumSound.DHOL_BOTH.value: lambda n: _synth_dhol_both(n),
# Dholak
DrumSound.DHOLAK_GE.value: lambda n: _synth_dholak_ge(n),
DrumSound.DHOLAK_NA.value: lambda n: _synth_dholak_na(n),
DrumSound.DHOLAK_TIT.value: lambda n: _synth_dholak_tit(n),
# Mridangam
DrumSound.MRIDANGAM_THAM.value: lambda n: _synth_mridangam_tham(n),
DrumSound.MRIDANGAM_NAM.value: lambda n: _synth_mridangam_nam(n),
DrumSound.MRIDANGAM_DIN.value: lambda n: _synth_mridangam_din(n),
DrumSound.MRIDANGAM_THA.value: lambda n: _synth_mridangam_tha(n),
# Djembe
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),
}
renderer = _dispatch.get(sound_value, lambda n: _synth_clave(n))
@@ -2592,6 +2893,23 @@ def render_score(score):
DrumSound.TABLA_GE.value: -0.2,
DrumSound.TABLA_KE.value: -0.2,
DrumSound.TABLA_DHA.value: 0.0, # both drums = center
# Dhol: bass left, treble right
DrumSound.DHOL_DAGGA.value: -0.2,
DrumSound.DHOL_TILLI.value: 0.2,
DrumSound.DHOL_BOTH.value: 0.0,
# Dholak: similar to dhol
DrumSound.DHOLAK_GE.value: -0.15,
DrumSound.DHOLAK_NA.value: 0.15,
DrumSound.DHOLAK_TIT.value: 0.2,
# Mridangam: bass left, treble right
DrumSound.MRIDANGAM_THAM.value: -0.2,
DrumSound.MRIDANGAM_NAM.value: 0.2,
DrumSound.MRIDANGAM_DIN.value: 0.0,
DrumSound.MRIDANGAM_THA.value: 0.15,
# Djembe: centered (single drum)
DrumSound.DJEMBE_BASS.value: 0.0,
DrumSound.DJEMBE_TONE.value: 0.1,
DrumSound.DJEMBE_SLAP.value: -0.1,
}
# Render all drum Parts (may be one "drums" or split into kick/snare/hats/etc.)
+155
View File
@@ -388,6 +388,23 @@ class DrumSound(Enum):
TABLA_DHA = 89 # both drums (Na + Ge)
TABLA_TIT = 90 # light dayan flick
TABLA_KE = 91 # muted bayan slap
# Dhol sounds
DHOL_DAGGA = 92 # heavy bass side (dagga stick)
DHOL_TILLI = 93 # thin treble side (tilli stick)
DHOL_BOTH = 94 # both sides
# Dholak sounds
DHOLAK_GE = 95 # bass side (open palm)
DHOLAK_NA = 96 # treble side (fingers)
DHOLAK_TIT = 97 # light treble tap
# Mridangam sounds
MRIDANGAM_THAM = 98 # bass stroke (thoppi/left head)
MRIDANGAM_NAM = 99 # treble ring (valanthalai/right head)
MRIDANGAM_DIN = 100 # both heads
MRIDANGAM_THA = 101 # muted treble
# Djembe sounds
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)
class _Hit:
@@ -1429,6 +1446,144 @@ Pattern._PRESETS["tiri kita"] = dict(
],
)
# ── Dhol patterns ────────────────────────────────────────────────────────
DD = DrumSound.DHOL_DAGGA
DT = DrumSound.DHOL_TILLI
DB = DrumSound.DHOL_BOTH
# Bhangra — the classic punjabi groove
Pattern._PRESETS["bhangra"] = dict(
name="bhangra",
time_signature="4/4",
beats=4.0,
hits=[
# Dagga on 1, tilli fills, both on 3
_h(DD, 0.0), _h(DT, 0.5), _h(DT, 0.75),
_h(DT, 1.0), _h(DT, 1.5),
_h(DB, 2.0), _h(DT, 2.5), _h(DT, 2.75),
_h(DD, 3.0), _h(DT, 3.25), _h(DT, 3.5), _h(DT, 3.75),
],
)
# Dhol chaal — driving folk pattern
Pattern._PRESETS["dhol chaal"] = dict(
name="dhol chaal",
time_signature="4/4",
beats=4.0,
hits=[
_h(DB, 0.0), _h(DT, 0.25), _h(DD, 0.5),
_h(DT, 1.0), _h(DT, 1.25), _h(DT, 1.5), _h(DD, 1.75),
_h(DB, 2.0), _h(DT, 2.25), _h(DD, 2.5),
_h(DT, 3.0), _h(DT, 3.25), _h(DT, 3.5), _h(DT, 3.75),
],
)
# ── Dholak patterns ─────────────────────────────────────────────────────
DKG = DrumSound.DHOLAK_GE
DKN = DrumSound.DHOLAK_NA
DKT = DrumSound.DHOLAK_TIT
# Qawwali — driving devotional pattern
Pattern._PRESETS["qawwali"] = dict(
name="qawwali",
time_signature="4/4",
beats=4.0,
hits=[
_h(DKG, 0.0), _h(DKN, 0.5), _h(DKT, 0.75),
_h(DKN, 1.0), _h(DKG, 1.5),
_h(DKG, 2.0), _h(DKN, 2.5), _h(DKT, 2.75),
_h(DKN, 3.0), _h(DKT, 3.25), _h(DKN, 3.5), _h(DKG, 3.75),
],
)
# Dholak folk — light folk music pattern
Pattern._PRESETS["dholak folk"] = dict(
name="dholak folk",
time_signature="4/4",
beats=4.0,
hits=[
_h(DKG, 0.0), _h(DKN, 1.0), _h(DKT, 1.5),
_h(DKG, 2.0), _h(DKN, 3.0), _h(DKT, 3.5),
],
)
# ── Mridangam patterns ──────────────────────────────────────────────────
MTH = DrumSound.MRIDANGAM_THAM
MN = DrumSound.MRIDANGAM_NAM
MD = DrumSound.MRIDANGAM_DIN
MTA = DrumSound.MRIDANGAM_THA
# Adi talam — the fundamental Carnatic rhythm (8 beats: 4+2+2)
Pattern._PRESETS["adi talam"] = dict(
name="adi talam",
time_signature="4/4",
beats=8.0,
hits=[
# Tha Din | Tha ka | Dhi na | Tha ka
_h(MD, 0.0), _h(MN, 1.0),
_h(MTH, 2.0), _h(MTA, 3.0),
_h(MD, 4.0), _h(MN, 5.0),
_h(MTH, 6.0), _h(MTA, 7.0),
],
)
# Mridangam korvai — rhythmic cadence pattern
Pattern._PRESETS["mridangam korvai"] = dict(
name="mridangam korvai",
time_signature="4/4",
beats=4.0,
hits=[
_h(MD, 0.0), _h(MN, 0.25), _h(MTA, 0.5), _h(MN, 0.75),
_h(MTH, 1.0), _h(MN, 1.25), _h(MN, 1.5), _h(MTH, 1.75),
_h(MD, 2.0), _h(MTA, 2.25), _h(MN, 2.5), _h(MTA, 2.75),
_h(MD, 3.0), _h(MN, 3.5), _h(MD, 3.75),
],
)
# ── Djembe patterns ─────────────────────────────────────────────────────
JB = DrumSound.DJEMBE_BASS
JT = DrumSound.DJEMBE_TONE
JS = DrumSound.DJEMBE_SLAP
# Djembe — standard West African pattern
Pattern._PRESETS["djembe"] = dict(
name="djembe",
time_signature="4/4",
beats=4.0,
hits=[
_h(JB, 0.0), _h(JT, 0.5), _h(JT, 0.75),
_h(JS, 1.0), _h(JT, 1.5),
_h(JB, 2.0), _h(JT, 2.5), _h(JT, 2.75),
_h(JS, 3.0), _h(JT, 3.25), _h(JS, 3.5),
],
)
# Kuku — traditional Guinean harvest dance rhythm
Pattern._PRESETS["kuku"] = dict(
name="kuku",
time_signature="4/4",
beats=4.0,
hits=[
_h(JS, 0.0), _h(JS, 0.5),
_h(JT, 1.0), _h(JB, 1.5),
_h(JS, 2.0), _h(JS, 2.5),
_h(JT, 3.0), _h(JT, 3.25), _h(JB, 3.5),
],
)
# Soli — powerful Mandinka rhythm
Pattern._PRESETS["soli"] = dict(
name="soli",
time_signature="4/4",
beats=4.0,
hits=[
_h(JB, 0.0), _h(JT, 0.25), _h(JS, 0.5), _h(JT, 0.75),
_h(JB, 1.0), _h(JS, 1.5),
_h(JB, 2.0), _h(JT, 2.25), _h(JS, 2.5), _h(JT, 2.75),
_h(JB, 3.0), _h(JT, 3.5), _h(JS, 3.75),
],
)
# ── Fill presets ──────────────────────────────────────────────────────────
Pattern._FILLS["rock"] = dict(