mirror of
https://github.com/kennethreitz/pytheory.git
synced 2026-06-05 23:00:20 +00:00
Compare commits
9 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 2f02df15b8 | |||
| a2740b8d57 | |||
| 840bfcc36c | |||
| 938c1cc132 | |||
| 9dc22db4b2 | |||
| f570e226cd | |||
| 0c5c3abedc | |||
| 35d07b984b | |||
| aec7723ee6 |
@@ -2,6 +2,45 @@
|
||||
|
||||
All notable changes to PyTheory are documented here.
|
||||
|
||||
## 0.38.1
|
||||
|
||||
- **Dynamic curves** — `Part.crescendo()`, `Part.decrescendo()`,
|
||||
`Part.swell()`, and `Part.dynamics()` for velocity ramps and custom
|
||||
curves across a sequence of notes
|
||||
|
||||
## 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
@@ -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
@@ -1,6 +1,6 @@
|
||||
[project]
|
||||
name = "pytheory"
|
||||
version = "0.36.3"
|
||||
version = "0.38.1"
|
||||
description = "Music Theory for Humans"
|
||||
readme = "README.md"
|
||||
license = "MIT"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
"""PyTheory: Music Theory for Humans."""
|
||||
|
||||
__version__ = "0.36.3"
|
||||
__version__ = "0.38.1"
|
||||
|
||||
from .tones import Tone, Interval
|
||||
from .systems import System, SYSTEMS, TET
|
||||
|
||||
+193
-25
@@ -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)
|
||||
|
||||
+603
-4
@@ -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,9 +2690,168 @@ 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 crescendo(self, notes, duration=Duration.QUARTER, *,
|
||||
start_vel: int = 40, end_vel: int = 110,
|
||||
articulation: str = "") -> "Part":
|
||||
"""Add notes with velocity ramping up (getting louder).
|
||||
|
||||
Args:
|
||||
notes: List of note strings (e.g. ``["C4", "D4", "E4"]``).
|
||||
duration: Duration for each note.
|
||||
start_vel: Starting velocity (quiet).
|
||||
end_vel: Ending velocity (loud).
|
||||
articulation: Optional articulation for all notes.
|
||||
|
||||
Example::
|
||||
|
||||
>>> piano.crescendo(["C4","D4","E4","F4","G4"], Duration.QUARTER,
|
||||
... start_vel=40, end_vel=110)
|
||||
"""
|
||||
return self.dynamics(notes, duration, velocities=(start_vel, end_vel),
|
||||
articulation=articulation)
|
||||
|
||||
def decrescendo(self, notes, duration=Duration.QUARTER, *,
|
||||
start_vel: int = 110, end_vel: int = 40,
|
||||
articulation: str = "") -> "Part":
|
||||
"""Add notes with velocity ramping down (getting quieter).
|
||||
|
||||
Args:
|
||||
notes: List of note strings.
|
||||
duration: Duration for each note.
|
||||
start_vel: Starting velocity (loud).
|
||||
end_vel: Ending velocity (quiet).
|
||||
articulation: Optional articulation for all notes.
|
||||
|
||||
Example::
|
||||
|
||||
>>> piano.decrescendo(["G4","F4","E4","D4","C4"], Duration.QUARTER,
|
||||
... start_vel=110, end_vel=40)
|
||||
"""
|
||||
return self.dynamics(notes, duration, velocities=(start_vel, end_vel),
|
||||
articulation=articulation)
|
||||
|
||||
def dynamics(self, notes, duration=Duration.QUARTER, *,
|
||||
velocities=None, articulation: str = "") -> "Part":
|
||||
"""Add notes with a velocity curve.
|
||||
|
||||
Args:
|
||||
notes: List of note strings or Tone/Chord objects.
|
||||
duration: Duration for each note (or list of durations).
|
||||
velocities: Velocity curve — either a ``(start, end)`` tuple
|
||||
for a linear ramp, or a list of ints (one per note).
|
||||
articulation: Optional articulation for all notes (or list).
|
||||
|
||||
Example::
|
||||
|
||||
>>> # Linear ramp
|
||||
>>> piano.dynamics(["C4","E4","G4","C5"], Duration.QUARTER,
|
||||
... velocities=(50, 120))
|
||||
>>> # Custom curve (swell and fade)
|
||||
>>> piano.dynamics(["C4","D4","E4","F4","G4","F4","E4","D4"],
|
||||
... Duration.EIGHTH,
|
||||
... velocities=[50, 70, 90, 110, 110, 90, 70, 50])
|
||||
"""
|
||||
n = len(notes)
|
||||
if n == 0:
|
||||
return self
|
||||
|
||||
# Resolve velocities
|
||||
if velocities is None:
|
||||
vels = [100] * n
|
||||
elif isinstance(velocities, (tuple, list)) and len(velocities) == 2 and isinstance(velocities[0], (int, float)):
|
||||
# (start, end) tuple — linear ramp
|
||||
start_v, end_v = velocities
|
||||
if n == 1:
|
||||
vels = [int(start_v)]
|
||||
else:
|
||||
vels = [int(start_v + (end_v - start_v) * i / (n - 1))
|
||||
for i in range(n)]
|
||||
else:
|
||||
vels = list(velocities)
|
||||
|
||||
# Resolve durations
|
||||
if isinstance(duration, (list, tuple)):
|
||||
durs = list(duration)
|
||||
else:
|
||||
durs = [duration] * n
|
||||
|
||||
# Resolve articulations
|
||||
if isinstance(articulation, (list, tuple)):
|
||||
arts = list(articulation)
|
||||
else:
|
||||
arts = [articulation] * n
|
||||
|
||||
for note, vel, dur, art in zip(notes, vels, durs, arts):
|
||||
vel = max(1, min(127, vel))
|
||||
self.add(note, dur, velocity=vel, articulation=art)
|
||||
|
||||
return self
|
||||
|
||||
def swell(self, notes, duration=Duration.QUARTER, *,
|
||||
low_vel: int = 40, peak_vel: int = 110,
|
||||
articulation: str = "") -> "Part":
|
||||
"""Add notes that swell up then fade back down (< > shape).
|
||||
|
||||
The velocity ramps up to the midpoint then back down,
|
||||
creating the classic orchestral swell.
|
||||
|
||||
Args:
|
||||
notes: List of note strings.
|
||||
duration: Duration for each note.
|
||||
low_vel: Velocity at start and end.
|
||||
peak_vel: Velocity at the peak (midpoint).
|
||||
articulation: Optional articulation.
|
||||
|
||||
Example::
|
||||
|
||||
>>> strings.swell(["C4","D4","E4","F4","G4","F4","E4","D4"],
|
||||
... Duration.QUARTER, low_vel=40, peak_vel=110)
|
||||
"""
|
||||
n = len(notes)
|
||||
if n <= 2:
|
||||
return self.dynamics(notes, duration, velocities=[peak_vel] * n,
|
||||
articulation=articulation)
|
||||
mid = n // 2
|
||||
vels = []
|
||||
for i in range(n):
|
||||
if i <= mid:
|
||||
v = low_vel + (peak_vel - low_vel) * i / mid
|
||||
else:
|
||||
v = peak_vel - (peak_vel - low_vel) * (i - mid) / (n - 1 - mid)
|
||||
vels.append(int(v))
|
||||
return self.dynamics(notes, duration, velocities=vels,
|
||||
articulation=articulation)
|
||||
|
||||
def set(self, **params) -> "Part":
|
||||
"""Change effect parameters at the current beat position.
|
||||
|
||||
|
||||
+15
-2
@@ -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():
|
||||
|
||||
Reference in New Issue
Block a user