mirror of
https://github.com/kennethreitz/pytheory.git
synced 2026-06-05 23:00:20 +00:00
Compare commits
18 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 7d678e364e | |||
| 3a8d829010 | |||
| 2a67906937 | |||
| b9dcad0454 | |||
| db9726168a | |||
| 26af923789 | |||
| 72e18a9bec | |||
| 7d56ed7a2c | |||
| 6efa4f18ce | |||
| 06fc4cabb7 | |||
| d3a93c18b3 | |||
| 0e10359236 | |||
| df00c3436d | |||
| 2f02df15b8 | |||
| a2740b8d57 | |||
| 840bfcc36c | |||
| 938c1cc132 | |||
| 9dc22db4b2 |
@@ -2,6 +2,88 @@
|
||||
|
||||
All notable changes to PyTheory are documented here.
|
||||
|
||||
## 0.39.2
|
||||
|
||||
- **Marching percussion** — snare, rimshot, and stick click sounds with
|
||||
high-tension kevlar synthesis and woody-metallic rimshot crack
|
||||
- **`Part.flam()`**, **`Part.diddle()`**, **`Part.cheese()`** — marching
|
||||
rudiment methods for any drum sound
|
||||
- **`Part ensemble=`** — duplicate voices with per-player timing tendencies
|
||||
and micro pitch drift. Works on any Part (drumline, string section, choir).
|
||||
`ensemble=20` for a full snare line, `ensemble=4` for a string quartet.
|
||||
- **Sympathetic resonance** — marching snare buzz builds up with repeated
|
||||
hits, decays during rests (like real snare wire response)
|
||||
- **4 marching patterns** — march, cadence, paradiddle, roll
|
||||
- **Chakradar tabla pattern** — 16-beat tihai of tihais composition
|
||||
- Song #32: Snare Cadence (flams, diddles, cheese, triplets, 32nds)
|
||||
|
||||
## 0.39.1
|
||||
|
||||
- **Chakradar tabla pattern** — 16-beat tihai of tihais composition with
|
||||
3 escalating phrases and a crescendo triplet finale
|
||||
|
||||
## 0.39.0
|
||||
|
||||
- **Dropped `numeral` dependency** — Roman numeral helpers inlined,
|
||||
reducing supply chain surface (#47)
|
||||
- **`Part.ramp()`** — smooth parameter automation with 4 interpolation
|
||||
curves (linear, ease_in, ease_out, ease_in_out)
|
||||
- **Articulations** — staccato, legato, marcato, tenuto, accent, fermata
|
||||
- **Dynamic curves** — crescendo(), decrescendo(), swell(), dynamics()
|
||||
- **`Part.hit()`** — individual drum sounds with articulation support
|
||||
- **Cross-choke drum damping** — djembe, hi-hats, cajón, doumbek
|
||||
- **5 new djembe patterns** + 3 djembe fills (30 fills total)
|
||||
- **6 new drum fills** — 3 cajón, 3 metal
|
||||
- **Duration arithmetic** — multiply, divide, add
|
||||
- **Improved djembe slap** synthesis
|
||||
- Song #31: Acid Tabla
|
||||
|
||||
## 0.38.2
|
||||
|
||||
- **`Part.ramp()`** — smooth parameter automation from current value to
|
||||
target over a duration. Works for lowpass, reverb, distortion, chorus,
|
||||
delay, volume, and any `.set()` parameter. Four interpolation curves:
|
||||
linear, ease_in, ease_out, ease_in_out.
|
||||
|
||||
## 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
|
||||
|
||||
+86
-12
@@ -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, 95+ pattern presets across dozens of genres, and 30 fill presets.
|
||||
Every sound is generated from waveforms; no samples needed.
|
||||
|
||||
Drum Sounds
|
||||
@@ -121,10 +121,18 @@ MRIDANGAM_THA (101)
|
||||
|
||||
**Djembe:** DJEMBE_BASS (102), DJEMBE_TONE (103), DJEMBE_SLAP (104)
|
||||
|
||||
**Cajón:** CAJON_SLAP (109), CAJON_TAP (110)
|
||||
**Cajón:** CAJON_BASS (108), CAJON_SLAP (109), CAJON_TAP (110)
|
||||
|
||||
**Metal Kit:** METAL_KICK (105), METAL_SNARE (106), METAL_HAT (107)
|
||||
|
||||
**Marching Snare:** MARCH_SNARE (115), MARCH_RIMSHOT (116), MARCH_CLICK (118)
|
||||
|
||||
**Quads (Tenors):** QUAD_1 (119), QUAD_2 (120), QUAD_3 (121), QUAD_4 (122),
|
||||
QUAD_SPOCK (123)
|
||||
|
||||
**Marching Bass:** BASS_1 (124), BASS_2 (125), BASS_3 (126), BASS_4 (127),
|
||||
BASS_5 (80)
|
||||
|
||||
Drum Synthesis
|
||||
--------------
|
||||
|
||||
@@ -252,14 +260,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")
|
||||
@@ -433,14 +444,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
|
||||
~~~~~~~~~
|
||||
@@ -456,10 +472,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
|
||||
~~~~~
|
||||
@@ -468,15 +489,68 @@ 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)
|
||||
|
||||
Marching Percussion
|
||||
~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
A full drumline — snare, quads (tenors), and pitched bass drums.
|
||||
Every sound is synthesized: kevlar snare heads, aluminum shell ting
|
||||
on the quads, felt-beater thwack on the basses.
|
||||
|
||||
**Snare** -- 3 sounds: MARCH_SNARE (tight kevlar tap), MARCH_RIMSHOT
|
||||
(woody-metallic crack), MARCH_CLICK (stick click for count-offs).
|
||||
|
||||
**Quads** -- 5 sounds: QUAD_1 through QUAD_4 (high to low pitched
|
||||
tenors) plus QUAD_SPOCK (rim click on the shell).
|
||||
|
||||
**Bass drums** -- 5 pitched drums: BASS_1 (highest/smallest) through
|
||||
BASS_5 (lowest/biggest), each with a prominent felt-beater thwack.
|
||||
|
||||
**6 patterns:** march (basic 4/4), cadence (8-beat street beat),
|
||||
march paradiddle, march roll (buzz crescendo), quad sweep (run across
|
||||
all 4 drums), quad groove, bass split (cascading across the line),
|
||||
bass unison (all 5 hit together), drumline (snare + quads + bass).
|
||||
|
||||
**Rudiment methods:** ``Part.flam()``, ``Part.diddle()``, and
|
||||
``Part.cheese()`` for marching rudiments on any drum sound.
|
||||
|
||||
**Ensemble rendering:** ``ensemble=N`` on any Part duplicates the
|
||||
voice with per-player timing tendencies and micro pitch drift.
|
||||
``ensemble=8`` for a snare line, ``ensemble=20`` for a massive section.
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
# Full drumline with ensemble
|
||||
snares = score.part("snares", synth="sine", volume=0.9,
|
||||
reverb=0.2, ensemble=8)
|
||||
quads = score.part("quads", synth="sine", volume=0.5,
|
||||
reverb=0.2, ensemble=4)
|
||||
basses = score.part("basses", synth="sine", volume=0.55,
|
||||
reverb=0.2, ensemble=5)
|
||||
|
||||
snares.flam(DrumSound.MARCH_SNARE, Duration.QUARTER, velocity=120)
|
||||
snares.diddle(DrumSound.MARCH_SNARE, Duration.EIGHTH, velocity=60)
|
||||
|
||||
# Or use patterns
|
||||
score.drums("drumline", repeats=4)
|
||||
|
||||
**Sympathetic resonance:** The marching snare builds up snare wire
|
||||
buzz as hits accumulate, and the buzz decays during rests — just like
|
||||
a real drum.
|
||||
|
||||
MIDI Export
|
||||
-----------
|
||||
|
||||
@@ -47,6 +47,18 @@ A ``Duration`` represents a note length in beats (quarter note = 1 beat):
|
||||
>>> Duration.TRIPLET_QUARTER.value
|
||||
0.6666666666666666
|
||||
|
||||
Duration supports arithmetic — multiply, divide, and add to create
|
||||
compound durations:
|
||||
|
||||
.. code-block:: pycon
|
||||
|
||||
>>> Duration.WHOLE * 2
|
||||
8.0
|
||||
>>> Duration.HALF + Duration.QUARTER
|
||||
3.0
|
||||
>>> Duration.WHOLE / 2
|
||||
2.0
|
||||
|
||||
Time Signatures
|
||||
---------------
|
||||
|
||||
@@ -399,6 +411,143 @@ The arpeggiator also accepts velocity:
|
||||
|
||||
lead.arpeggio("Am", bars=2, pattern="up", velocity=80)
|
||||
|
||||
Articulations
|
||||
-------------
|
||||
|
||||
Articulations change *how* a note is played — its attack, duration, and
|
||||
weight. A staccato note is short and bouncy. A marcato note hits hard.
|
||||
A legato note melts into the next one. This is the difference between
|
||||
a melody that sounds like a MIDI file and one that sounds like a
|
||||
musician played it.
|
||||
|
||||
Pass ``articulation=`` to ``Part.add()``:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
piano.add("C4", Duration.QUARTER, articulation="staccato") # short, bouncy
|
||||
piano.add("D4", Duration.QUARTER, articulation="legato") # smooth, overlaps
|
||||
piano.add("E4", Duration.QUARTER, articulation="marcato") # heavy accent
|
||||
piano.add("F4", Duration.QUARTER, articulation="tenuto") # held, soft attack
|
||||
piano.add("G4", Duration.QUARTER, articulation="accent") # louder
|
||||
piano.add("C5", Duration.HALF, articulation="fermata") # held longer
|
||||
|
||||
What each articulation does:
|
||||
|
||||
- **staccato** — plays ~40% of the note duration with a quick fade-out. Short and detached.
|
||||
- **legato** — extends ~15% into the next note. Smooth and connected.
|
||||
- **marcato** — 25% velocity boost + sharper attack. Heavy and accented.
|
||||
- **tenuto** — full duration with a softer attack ramp. Held and deliberate.
|
||||
- **accent** — 20% velocity boost, no duration change.
|
||||
- **fermata** — stretches the note 50% longer.
|
||||
|
||||
Articulations work on ``Part.hold()`` and ``Part.hit()`` too.
|
||||
|
||||
Dynamic Curves
|
||||
--------------
|
||||
|
||||
Real music breathes — phrases get louder, get quieter, swell and
|
||||
recede. Dynamic curves let you shape the velocity across a sequence
|
||||
of notes instead of setting each one manually.
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
# Crescendo: quiet to loud
|
||||
piano.crescendo(["C4","D4","E4","F4","G4","A4","B4","C5"],
|
||||
Duration.QUARTER, start_vel=30, end_vel=110)
|
||||
|
||||
# Decrescendo: loud to quiet
|
||||
piano.decrescendo(["C5","B4","A4","G4","F4","E4","D4","C4"],
|
||||
Duration.QUARTER, start_vel=110, end_vel=30)
|
||||
|
||||
# Swell: up then back down (orchestral < > shape)
|
||||
strings.swell(["C4","D4","E4","F4","G4","F4","E4","D4"],
|
||||
Duration.QUARTER, low_vel=35, peak_vel=110)
|
||||
|
||||
# Custom curve: explicit velocity per note
|
||||
piano.dynamics(["C4","E4","G4","C5"], Duration.QUARTER,
|
||||
velocities=[50, 80, 110, 90])
|
||||
|
||||
Four methods:
|
||||
|
||||
- **crescendo()** — linear velocity ramp from ``start_vel`` to ``end_vel``.
|
||||
- **decrescendo()** — same thing, but typically loud to quiet.
|
||||
- **swell()** — ramps up to the midpoint, then back down. The classic
|
||||
orchestral crescendo-decrescendo.
|
||||
- **dynamics()** — the general form. Pass a ``(start, end)`` tuple for
|
||||
a linear ramp, or a list of velocities for a custom curve.
|
||||
|
||||
All four accept ``articulation=`` to combine dynamics with articulations:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
# Staccato crescendo — bouncy notes getting louder
|
||||
piano.crescendo(["C4","E4","G4","C5","E5","G5","C6","E6"],
|
||||
Duration.EIGHTH, start_vel=40, end_vel=110,
|
||||
articulation="staccato")
|
||||
|
||||
Part.hit() — Manual Drum Placement
|
||||
-----------------------------------
|
||||
|
||||
The pattern system is great for grooves, but sometimes you want to
|
||||
place individual drum hits with full control — articulations, effects,
|
||||
and all. ``Part.hit()`` puts a drum sound into a Part's note stream:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
from pytheory import DrumSound
|
||||
|
||||
kit = score.part("kit", synth="sine", volume=0.7)
|
||||
|
||||
kit.hit(DrumSound.KICK, Duration.QUARTER, articulation="accent")
|
||||
kit.hit(DrumSound.CLOSED_HAT, Duration.EIGHTH, velocity=60)
|
||||
kit.hit(DrumSound.SNARE, Duration.EIGHTH, articulation="marcato")
|
||||
|
||||
Because hits go through the normal Part renderer, they get humanize,
|
||||
effects, and articulations for free. Use this for custom beats that
|
||||
don't fit a preset pattern, or for one-shot accent hits layered on
|
||||
top of a pattern.
|
||||
|
||||
Rudiments — Flam, Diddle, Cheese
|
||||
---------------------------------
|
||||
|
||||
Marching percussion rudiments as methods on any Part:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
from pytheory import DrumSound
|
||||
|
||||
p = score.part("snares", synth="sine", volume=0.9)
|
||||
|
||||
# Flam: grace note + main hit (gap controls tightness)
|
||||
p.flam(DrumSound.MARCH_SNARE, Duration.QUARTER, velocity=120)
|
||||
|
||||
# Diddle: two equal strokes in one note duration
|
||||
p.diddle(DrumSound.MARCH_SNARE, Duration.EIGHTH, velocity=60)
|
||||
|
||||
# Cheese: flam + diddle combined
|
||||
p.cheese(DrumSound.MARCH_SNARE, Duration.QUARTER, velocity=120)
|
||||
|
||||
Ensemble
|
||||
--------
|
||||
|
||||
Any Part can be rendered as an ensemble — multiple players with
|
||||
per-player timing tendencies and micro pitch drift:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
# 8-player snare line
|
||||
snares = score.part("snares", synth="sine", volume=0.9, ensemble=8)
|
||||
|
||||
# 20-player string section
|
||||
strings = score.part("strings", instrument="string_ensemble", ensemble=20)
|
||||
|
||||
# Single player (default)
|
||||
solo = score.part("solo", instrument="violin")
|
||||
|
||||
Each ensemble voice gets a consistent timing personality (some rush,
|
||||
some drag) plus small per-note wobble, and slightly different tuning.
|
||||
The result sounds like a real section — together but alive.
|
||||
|
||||
Swing and Groove
|
||||
----------------
|
||||
|
||||
@@ -478,6 +627,50 @@ integrate naturally with the rest of the automation system:
|
||||
pad.rest(Duration.WHOLE)
|
||||
pad.rest(Duration.WHOLE)
|
||||
|
||||
Parameter Ramps
|
||||
---------------
|
||||
|
||||
Fades only control volume. ``Part.ramp()`` smoothly sweeps *any*
|
||||
parameter from its current value to a target — filters, reverb,
|
||||
distortion, chorus, delay, anything ``.set()`` accepts. This is how
|
||||
you build filter sweeps, gradual effect sends, and EDM buildups.
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
lead = score.part("lead", synth="saw", lowpass=200, lowpass_q=3.0)
|
||||
|
||||
# Open the filter over 8 bars
|
||||
lead.ramp(over=Duration.WHOLE * 8, lowpass=8000)
|
||||
|
||||
# Ramp multiple params at once
|
||||
pad.ramp(over=Duration.WHOLE * 4, reverb=0.5, chorus=0.3)
|
||||
|
||||
# Close the filter with distortion fading in
|
||||
lead.ramp(over=Duration.WHOLE * 4, lowpass=400, distortion=0.5)
|
||||
|
||||
Four interpolation curves:
|
||||
|
||||
- **linear** — constant rate of change (default).
|
||||
- **ease_in** — starts slow, accelerates. Good for buildups.
|
||||
- **ease_out** — starts fast, decelerates. Good for releases.
|
||||
- **ease_in_out** — slow at both ends. Smooth and natural.
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
# EDM buildup: slow start, accelerating filter sweep
|
||||
lead.ramp(over=Duration.WHOLE * 8, curve="ease_in", lowpass=8000)
|
||||
|
||||
# Smooth reverb wash fading in and settling
|
||||
pad.ramp(over=Duration.WHOLE * 4, curve="ease_in_out", reverb=0.6)
|
||||
|
||||
``ramp()`` generates automation points every quarter-beat by default.
|
||||
Set ``resolution=0.125`` for smoother curves (every 32nd note), or
|
||||
``resolution=1.0`` for lighter automation (every beat).
|
||||
|
||||
Combine with ``lfo()`` for cyclic modulation and ``ramp()`` for
|
||||
one-shot sweeps — together they cover the full range of parameter
|
||||
automation.
|
||||
|
||||
Humanize
|
||||
--------
|
||||
|
||||
|
||||
+483
-1
@@ -2324,6 +2324,486 @@ def sitar_drone():
|
||||
play_song(score, "Sitar Drone — Raga Bhairav (22-Shruti JI, hold() polyphony)")
|
||||
|
||||
|
||||
def acid_tabla():
|
||||
"""Acid Tabla — 303 filter automation meets Indian percussion."""
|
||||
score = Score("4/4", bpm=132)
|
||||
|
||||
# ── House drums ──
|
||||
score.drums("house", repeats=20, fill="house", fill_every=8)
|
||||
score.set_drum_effects(volume=0.45)
|
||||
|
||||
# ── 303 acid bass ──
|
||||
acid = score.part("acid", synth="saw", volume=0.75,
|
||||
legato=True, glide=0.035,
|
||||
distortion=0.35, distortion_drive=4.5,
|
||||
saturation=0.15, humanize=0.05)
|
||||
|
||||
# Intro (4 bars): filter closed, high resonance
|
||||
acid.set(lowpass=600, lowpass_q=12.0)
|
||||
for _ in range(4):
|
||||
for n in ["C3","C3","C2","C3","Eb3","C2","G2","C3"]:
|
||||
acid.add(n, Duration.EIGHTH)
|
||||
|
||||
# Build (4 bars): filter opens
|
||||
acid.ramp(over=Duration.WHOLE * 4, curve="ease_in", lowpass=4500)
|
||||
for _ in range(4):
|
||||
for n in ["C2","G2","C3","Eb3","C2","Bb2","G2","C3"]:
|
||||
acid.add(n, Duration.EIGHTH)
|
||||
|
||||
# Peak (4 bars): wide open, wilder pattern
|
||||
acid.set(lowpass=7000, lowpass_q=7.0)
|
||||
for _ in range(2):
|
||||
for n in ["C2","C3","Eb3","G3","C2","Bb2","G2","Eb3"]:
|
||||
acid.add(n, Duration.EIGHTH)
|
||||
for _ in range(2):
|
||||
for n in ["C2","Eb3","C3","G3","Bb2","C3","G2","C2"]:
|
||||
acid.add(n, Duration.EIGHTH)
|
||||
|
||||
# Tabla section (4 bars): filter pulls back
|
||||
acid.set(lowpass=3000, lowpass_q=5.0)
|
||||
for _ in range(4):
|
||||
for n in ["C2","G2","C3","C2","Eb2","G2","Bb2","C2"]:
|
||||
acid.add(n, Duration.EIGHTH)
|
||||
|
||||
# Outro (4 bars): filter closes
|
||||
acid.ramp(over=Duration.WHOLE * 4, curve="ease_out", lowpass=400, lowpass_q=15.0)
|
||||
for _ in range(4):
|
||||
for n in ["C3","G2","C2","C3","C2","G2","Eb2","C2"]:
|
||||
acid.add(n, Duration.EIGHTH)
|
||||
|
||||
# ── Tabla: enters bar 9, rides through to the end ──
|
||||
tabla = score.part("tabla", synth="sine", volume=0.55, reverb=0.15)
|
||||
|
||||
# 8 bars rest
|
||||
for _ in range(64):
|
||||
tabla.rest(Duration.EIGHTH)
|
||||
|
||||
# Bars 9-12: keherwa groove
|
||||
for _ in range(4):
|
||||
tabla.hit(DrumSound.TABLA_DHA, Duration.EIGHTH, velocity=100, articulation="accent")
|
||||
tabla.hit(DrumSound.TABLA_TIT, Duration.EIGHTH, velocity=55)
|
||||
tabla.hit(DrumSound.TABLA_TIT, Duration.EIGHTH, velocity=50)
|
||||
tabla.hit(DrumSound.TABLA_NA, Duration.EIGHTH, velocity=88)
|
||||
tabla.hit(DrumSound.TABLA_TIN, Duration.EIGHTH, velocity=82)
|
||||
tabla.hit(DrumSound.TABLA_TIT, Duration.EIGHTH, velocity=52)
|
||||
tabla.hit(DrumSound.TABLA_DHA, Duration.EIGHTH, velocity=95, articulation="accent")
|
||||
tabla.hit(DrumSound.TABLA_GE, Duration.EIGHTH, velocity=78)
|
||||
|
||||
# Bars 13-14: busier with 16ths
|
||||
for _ in range(2):
|
||||
tabla.hit(DrumSound.TABLA_DHA, Duration.EIGHTH, velocity=105, articulation="marcato")
|
||||
tabla.hit(DrumSound.TABLA_TIT, Duration.SIXTEENTH, velocity=48)
|
||||
tabla.hit(DrumSound.TABLA_TIT, Duration.SIXTEENTH, velocity=52)
|
||||
tabla.hit(DrumSound.TABLA_NA, Duration.EIGHTH, velocity=90)
|
||||
tabla.hit(DrumSound.TABLA_TIN, Duration.EIGHTH, velocity=85)
|
||||
tabla.hit(DrumSound.TABLA_TIT, Duration.SIXTEENTH, velocity=48)
|
||||
tabla.hit(DrumSound.TABLA_NA, Duration.SIXTEENTH, velocity=58)
|
||||
tabla.hit(DrumSound.TABLA_DHA, Duration.EIGHTH, velocity=100, articulation="accent")
|
||||
tabla.hit(DrumSound.TABLA_GE_BEND, Duration.EIGHTH, velocity=88)
|
||||
|
||||
# Bars 15-16: tihai crescendo ending
|
||||
for vel in [85, 90, 95]:
|
||||
tabla.hit(DrumSound.TABLA_DHA, Duration.EIGHTH, velocity=vel, articulation="accent")
|
||||
tabla.hit(DrumSound.TABLA_TIT, Duration.SIXTEENTH, velocity=int(vel * 0.6))
|
||||
tabla.hit(DrumSound.TABLA_NA, Duration.SIXTEENTH, velocity=int(vel * 0.75))
|
||||
for vel in [100, 105, 110]:
|
||||
tabla.hit(DrumSound.TABLA_DHA, Duration.EIGHTH, velocity=vel, articulation="marcato")
|
||||
tabla.hit(DrumSound.TABLA_TIT, Duration.SIXTEENTH, velocity=int(vel * 0.55))
|
||||
tabla.hit(DrumSound.TABLA_NA, Duration.SIXTEENTH, velocity=int(vel * 0.7))
|
||||
tabla.hit(DrumSound.TABLA_DHA, Duration.QUARTER, velocity=127, articulation="fermata")
|
||||
tabla.hit(DrumSound.TABLA_GE_BEND, Duration.QUARTER, velocity=110)
|
||||
tabla.rest(Duration.HALF)
|
||||
|
||||
# Bars 17-20: tabla continues through outro, lighter
|
||||
for _ in range(4):
|
||||
tabla.hit(DrumSound.TABLA_DHA, Duration.EIGHTH, velocity=85, articulation="accent")
|
||||
tabla.hit(DrumSound.TABLA_TIT, Duration.EIGHTH, velocity=45)
|
||||
tabla.hit(DrumSound.TABLA_TIT, Duration.EIGHTH, velocity=42)
|
||||
tabla.hit(DrumSound.TABLA_NA, Duration.EIGHTH, velocity=75)
|
||||
tabla.hit(DrumSound.TABLA_TIN, Duration.EIGHTH, velocity=70)
|
||||
tabla.hit(DrumSound.TABLA_TIT, Duration.EIGHTH, velocity=42)
|
||||
tabla.hit(DrumSound.TABLA_DHA, Duration.EIGHTH, velocity=80)
|
||||
tabla.hit(DrumSound.TABLA_GE, Duration.EIGHTH, velocity=65)
|
||||
|
||||
# ── Pad: enters at peak, fades during outro ──
|
||||
pad = score.part("pad", synth="supersaw", envelope="pad", volume=0.0,
|
||||
reverb=0.4, chorus=0.2, detune=10, lowpass=2500)
|
||||
for _ in range(32):
|
||||
pad.rest(Duration.QUARTER)
|
||||
pad.ramp(over=Duration.WHOLE * 2, volume=0.18)
|
||||
for sym in ["Cm", "Ab", "Eb", "Bb"] * 3:
|
||||
pad.add(Chord.from_symbol(sym), Duration.WHOLE)
|
||||
pad.ramp(over=Duration.WHOLE * 2, curve="ease_out", volume=0.0)
|
||||
for sym in ["Cm", "Cm"]:
|
||||
pad.add(Chord.from_symbol(sym), Duration.WHOLE)
|
||||
|
||||
play_song(score, "Acid Tabla — 303 filter automation + tabla (ramp, articulations, Part.hit)")
|
||||
|
||||
|
||||
def snare_cadence():
|
||||
"""Snare Cadence — full drumline with ensemble, flams, diddles, cheese."""
|
||||
score = Score("4/4", bpm=120)
|
||||
|
||||
S = DrumSound.MARCH_SNARE
|
||||
R = DrumSound.MARCH_RIMSHOT
|
||||
C = DrumSound.MARCH_CLICK
|
||||
Q1 = DrumSound.QUAD_1
|
||||
Q2 = DrumSound.QUAD_2
|
||||
Q3 = DrumSound.QUAD_3
|
||||
Q4 = DrumSound.QUAD_4
|
||||
QS = DrumSound.QUAD_SPOCK
|
||||
B1 = DrumSound.BASS_1
|
||||
B2 = DrumSound.BASS_2
|
||||
B3 = DrumSound.BASS_3
|
||||
B4 = DrumSound.BASS_4
|
||||
B5 = DrumSound.BASS_5
|
||||
|
||||
# Snare line — 8 players
|
||||
p = score.part("snares", synth="sine", volume=0.9, reverb=0.2, ensemble=8)
|
||||
# Quad line — 4 players
|
||||
q = score.part("quads", synth="sine", volume=0.5, reverb=0.2, ensemble=4)
|
||||
# Bass line — 5 players
|
||||
b = score.part("basses", synth="sine", volume=0.55, reverb=0.2, ensemble=5)
|
||||
|
||||
_trip = 1.0 / 3
|
||||
|
||||
# Helper: bass split run (down or up)
|
||||
def bass_down(dur=Duration.SIXTEENTH):
|
||||
b.hit(B1, dur, velocity=95)
|
||||
b.hit(B2, dur, velocity=90)
|
||||
b.hit(B3, dur, velocity=85)
|
||||
b.hit(B4, dur, velocity=90)
|
||||
|
||||
def bass_up(dur=Duration.SIXTEENTH):
|
||||
b.hit(B4, dur, velocity=90)
|
||||
b.hit(B3, dur, velocity=85)
|
||||
b.hit(B2, dur, velocity=90)
|
||||
b.hit(B1, dur, velocity=95)
|
||||
|
||||
def bass_hit(dur=Duration.QUARTER):
|
||||
b.hit(B3, dur, velocity=100)
|
||||
|
||||
def quad_sweep_down():
|
||||
q.hit(Q1, Duration.SIXTEENTH, velocity=95)
|
||||
q.hit(Q2, Duration.SIXTEENTH, velocity=88)
|
||||
q.hit(Q3, Duration.SIXTEENTH, velocity=82)
|
||||
q.hit(Q4, Duration.SIXTEENTH, velocity=78)
|
||||
|
||||
def quad_sweep_up():
|
||||
q.hit(Q4, Duration.SIXTEENTH, velocity=78)
|
||||
q.hit(Q3, Duration.SIXTEENTH, velocity=82)
|
||||
q.hit(Q2, Duration.SIXTEENTH, velocity=88)
|
||||
q.hit(Q1, Duration.SIXTEENTH, velocity=95)
|
||||
|
||||
# ── Click count-off ──
|
||||
for _ in range(4):
|
||||
p.hit(C, Duration.QUARTER, velocity=95)
|
||||
q.rest(Duration.QUARTER)
|
||||
b.rest(Duration.QUARTER)
|
||||
|
||||
# ── Section 1: 16th groove — snares only (4 bars) ──
|
||||
for _ in range(16):
|
||||
q.rest(Duration.QUARTER)
|
||||
b.rest(Duration.QUARTER)
|
||||
|
||||
for _ in range(2):
|
||||
p.hit(R, Duration.SIXTEENTH, velocity=118)
|
||||
p.hit(S, Duration.SIXTEENTH, velocity=32)
|
||||
p.hit(S, Duration.SIXTEENTH, velocity=35)
|
||||
p.hit(S, Duration.SIXTEENTH, velocity=30)
|
||||
p.hit(R, Duration.SIXTEENTH, velocity=115)
|
||||
p.hit(S, Duration.SIXTEENTH, velocity=30)
|
||||
p.hit(S, Duration.SIXTEENTH, velocity=28)
|
||||
p.hit(S, Duration.SIXTEENTH, velocity=32)
|
||||
p.hit(R, Duration.SIXTEENTH, velocity=118)
|
||||
p.hit(S, Duration.SIXTEENTH, velocity=35)
|
||||
p.hit(S, Duration.SIXTEENTH, velocity=30)
|
||||
p.hit(S, Duration.SIXTEENTH, velocity=32)
|
||||
p.hit(R, Duration.SIXTEENTH, velocity=120)
|
||||
p.hit(S, Duration.SIXTEENTH, velocity=30)
|
||||
p.hit(S, Duration.SIXTEENTH, velocity=28)
|
||||
p.hit(S, Duration.SIXTEENTH, velocity=32)
|
||||
|
||||
# Triplets mixed in
|
||||
for _ in range(2):
|
||||
p.hit(R, _trip, velocity=118)
|
||||
p.hit(S, _trip, velocity=32)
|
||||
p.hit(S, _trip, velocity=30)
|
||||
p.hit(R, _trip, velocity=115)
|
||||
p.hit(S, _trip, velocity=28)
|
||||
p.hit(S, _trip, velocity=32)
|
||||
p.hit(R, Duration.SIXTEENTH, velocity=118)
|
||||
p.hit(S, Duration.SIXTEENTH, velocity=30)
|
||||
p.hit(S, Duration.SIXTEENTH, velocity=32)
|
||||
p.hit(S, Duration.SIXTEENTH, velocity=28)
|
||||
p.hit(R, _trip, velocity=118)
|
||||
p.hit(S, _trip, velocity=30)
|
||||
p.hit(S, _trip, velocity=35)
|
||||
|
||||
# ── Section 2: Quads + bass enter (4 bars) ──
|
||||
for _ in range(2):
|
||||
p.flam(S, Duration.QUARTER, velocity=118)
|
||||
p.hit(S, Duration.SIXTEENTH, velocity=30)
|
||||
p.hit(S, Duration.SIXTEENTH, velocity=32)
|
||||
p.hit(R, _trip, velocity=118)
|
||||
p.hit(S, _trip, velocity=28)
|
||||
p.hit(S, _trip, velocity=30)
|
||||
p.flam(S, Duration.QUARTER, velocity=118)
|
||||
|
||||
quad_sweep_down()
|
||||
q.hit(QS, Duration.QUARTER, velocity=100)
|
||||
quad_sweep_up()
|
||||
q.hit(QS, Duration.QUARTER, velocity=100)
|
||||
|
||||
bass_hit()
|
||||
b.hit(B5, Duration.QUARTER, velocity=95)
|
||||
bass_hit()
|
||||
b.hit(B1, Duration.QUARTER, velocity=95)
|
||||
|
||||
for _ in range(2):
|
||||
p.hit(S, _trip, velocity=35)
|
||||
p.flam(S, _trip * 2, velocity=118)
|
||||
p.hit(S, Duration.SIXTEENTH, velocity=30)
|
||||
p.hit(S, Duration.SIXTEENTH, velocity=32)
|
||||
p.flam(S, Duration.QUARTER, velocity=118)
|
||||
p.hit(S, _trip, velocity=28)
|
||||
p.hit(R, _trip, velocity=122)
|
||||
p.hit(S, _trip, velocity=35)
|
||||
|
||||
quad_sweep_down()
|
||||
quad_sweep_up()
|
||||
q.hit(Q1, Duration.EIGHTH, velocity=95)
|
||||
q.hit(Q4, Duration.EIGHTH, velocity=85)
|
||||
q.hit(QS, Duration.QUARTER, velocity=100)
|
||||
|
||||
bass_down()
|
||||
bass_up()
|
||||
b.hit(B3, Duration.HALF, velocity=100)
|
||||
|
||||
# ── Section 3: Flams + diddles + full line (4 bars) ──
|
||||
for _ in range(2):
|
||||
p.flam(S, Duration.QUARTER, velocity=120)
|
||||
p.diddle(S, Duration.EIGHTH, velocity=45)
|
||||
p.hit(S, _trip, velocity=30)
|
||||
p.hit(S, _trip, velocity=32)
|
||||
p.hit(S, _trip, velocity=28)
|
||||
p.hit(R, Duration.EIGHTH, velocity=122)
|
||||
p.diddle(S, Duration.EIGHTH, velocity=42)
|
||||
|
||||
q.hit(Q1, Duration.QUARTER, velocity=95)
|
||||
q.hit(Q3, Duration.EIGHTH, velocity=55)
|
||||
q.hit(Q2, _trip, velocity=55)
|
||||
q.hit(Q3, _trip, velocity=55)
|
||||
q.hit(Q4, _trip, velocity=55)
|
||||
q.hit(QS, Duration.EIGHTH, velocity=100)
|
||||
q.hit(Q1, Duration.EIGHTH, velocity=55)
|
||||
|
||||
bass_hit()
|
||||
b.hit(B1, Duration.EIGHTH, velocity=90)
|
||||
b.hit(B5, Duration.EIGHTH, velocity=95)
|
||||
bass_hit()
|
||||
b.hit(B5, Duration.EIGHTH, velocity=90)
|
||||
b.hit(B1, Duration.EIGHTH, velocity=95)
|
||||
|
||||
for _ in range(2):
|
||||
p.diddle(S, Duration.EIGHTH, velocity=45)
|
||||
p.hit(R, _trip, velocity=120)
|
||||
p.hit(S, _trip, velocity=30)
|
||||
p.hit(S, _trip, velocity=32)
|
||||
p.diddle(S, Duration.EIGHTH, velocity=48)
|
||||
p.hit(R, _trip, velocity=118)
|
||||
p.hit(S, _trip, velocity=28)
|
||||
p.hit(S, _trip, velocity=30)
|
||||
p.flam(S, Duration.EIGHTH, velocity=122)
|
||||
p.hit(S, Duration.EIGHTH, velocity=35)
|
||||
|
||||
quad_sweep_down()
|
||||
quad_sweep_up()
|
||||
quad_sweep_down()
|
||||
quad_sweep_up()
|
||||
|
||||
bass_down()
|
||||
bass_up()
|
||||
bass_down()
|
||||
bass_up()
|
||||
|
||||
# ── Section 4: Cheese + 32nds (4 bars) ──
|
||||
for _ in range(2):
|
||||
p.cheese(S, Duration.QUARTER, velocity=120)
|
||||
p.hit(S, 0.0625, velocity=30)
|
||||
p.hit(S, 0.0625, velocity=32)
|
||||
p.hit(S, 0.0625, velocity=35)
|
||||
p.hit(S, 0.0625, velocity=30)
|
||||
p.cheese(S, Duration.QUARTER, velocity=118)
|
||||
p.diddle(S, Duration.EIGHTH, velocity=48)
|
||||
p.hit(R, Duration.EIGHTH, velocity=125)
|
||||
|
||||
q.hit(QS, Duration.QUARTER, velocity=105)
|
||||
q.hit(Q1, Duration.SIXTEENTH, velocity=55)
|
||||
q.hit(Q2, Duration.SIXTEENTH, velocity=55)
|
||||
q.hit(Q3, Duration.SIXTEENTH, velocity=55)
|
||||
q.hit(Q4, Duration.SIXTEENTH, velocity=55)
|
||||
q.hit(QS, Duration.QUARTER, velocity=105)
|
||||
q.hit(Q4, Duration.EIGHTH, velocity=55)
|
||||
q.hit(Q1, Duration.EIGHTH, velocity=90)
|
||||
|
||||
bass_hit()
|
||||
b.hit(B1, Duration.EIGHTH, velocity=90)
|
||||
b.hit(B3, Duration.EIGHTH, velocity=85)
|
||||
b.hit(B5, Duration.EIGHTH, velocity=95)
|
||||
b.hit(B3, Duration.EIGHTH, velocity=85)
|
||||
bass_hit()
|
||||
b.rest(Duration.QUARTER)
|
||||
|
||||
# All cheese
|
||||
p.cheese(S, Duration.QUARTER, velocity=122)
|
||||
p.cheese(S, Duration.QUARTER, velocity=120)
|
||||
p.cheese(S, Duration.QUARTER, velocity=125)
|
||||
p.cheese(S, Duration.QUARTER, velocity=122)
|
||||
|
||||
q.hit(QS, Duration.QUARTER, velocity=105)
|
||||
q.hit(QS, Duration.QUARTER, velocity=105)
|
||||
q.hit(QS, Duration.QUARTER, velocity=108)
|
||||
q.hit(QS, Duration.QUARTER, velocity=105)
|
||||
|
||||
b.hit(B5, Duration.QUARTER, velocity=100)
|
||||
b.hit(B3, Duration.QUARTER, velocity=100)
|
||||
b.hit(B1, Duration.QUARTER, velocity=100)
|
||||
b.hit(B3, Duration.QUARTER, velocity=100)
|
||||
|
||||
p.flam(S, Duration.EIGHTH, velocity=120)
|
||||
p.diddle(S, Duration.EIGHTH, velocity=50)
|
||||
p.flam(S, Duration.EIGHTH, velocity=122)
|
||||
p.diddle(S, Duration.EIGHTH, velocity=52)
|
||||
p.flam(S, Duration.EIGHTH, velocity=125)
|
||||
p.diddle(S, Duration.EIGHTH, velocity=55)
|
||||
p.hit(R, Duration.EIGHTH, velocity=127)
|
||||
p.hit(S, Duration.EIGHTH, velocity=38)
|
||||
|
||||
quad_sweep_down()
|
||||
quad_sweep_up()
|
||||
quad_sweep_down()
|
||||
quad_sweep_up()
|
||||
|
||||
bass_down()
|
||||
bass_up()
|
||||
bass_down()
|
||||
bass_up()
|
||||
|
||||
# ── Section 5: 16ths + triplet 16ths + 32nds (4 bars) ──
|
||||
_trip16 = 1.0 / 6
|
||||
|
||||
for _ in range(2):
|
||||
for beat in range(4):
|
||||
p.hit(R, _trip, velocity=118)
|
||||
p.hit(S, _trip, velocity=35)
|
||||
p.hit(S, _trip, velocity=32)
|
||||
|
||||
quad_sweep_down()
|
||||
quad_sweep_up()
|
||||
quad_sweep_down()
|
||||
quad_sweep_up()
|
||||
|
||||
bass_hit()
|
||||
b.hit(B5, Duration.QUARTER, velocity=95)
|
||||
bass_hit()
|
||||
b.hit(B1, Duration.QUARTER, velocity=95)
|
||||
|
||||
# 32nd run crescendo
|
||||
for i in range(32):
|
||||
p.hit(S, 0.0625, velocity=min(22 + i * 3, 92))
|
||||
p.hit(R, Duration.EIGHTH, velocity=125)
|
||||
p.hit(R, Duration.EIGHTH, velocity=127)
|
||||
|
||||
for _ in range(4):
|
||||
q.hit(Q1, 0.0625, velocity=55)
|
||||
q.hit(Q2, 0.0625, velocity=55)
|
||||
q.hit(Q3, 0.0625, velocity=55)
|
||||
q.hit(Q4, 0.0625, velocity=55)
|
||||
q.hit(QS, Duration.QUARTER, velocity=108)
|
||||
|
||||
bass_down()
|
||||
bass_up()
|
||||
bass_down()
|
||||
b.hit(B5, Duration.QUARTER, velocity=100)
|
||||
b.hit(B1, Duration.QUARTER, velocity=100)
|
||||
|
||||
# Triplet 16ths — all sections
|
||||
for _ in range(2):
|
||||
for beat in range(4):
|
||||
p.hit(R, _trip16, velocity=115)
|
||||
p.hit(S, _trip16, velocity=30)
|
||||
p.hit(S, _trip16, velocity=32)
|
||||
p.hit(R, _trip16, velocity=112)
|
||||
p.hit(S, _trip16, velocity=28)
|
||||
p.hit(S, _trip16, velocity=30)
|
||||
|
||||
for beat in range(4):
|
||||
q.hit(Q1, _trip16, velocity=90)
|
||||
q.hit(Q2, _trip16, velocity=55)
|
||||
q.hit(Q3, _trip16, velocity=55)
|
||||
q.hit(Q4, _trip16, velocity=55)
|
||||
q.hit(Q3, _trip16, velocity=55)
|
||||
q.hit(Q2, _trip16, velocity=55)
|
||||
|
||||
bass_down()
|
||||
bass_up()
|
||||
bass_down()
|
||||
bass_up()
|
||||
|
||||
# ── Section 6: Buzz roll climax (2 bars) ──
|
||||
for i in range(64):
|
||||
p.hit(S, 0.0625, velocity=min(20 + i * 1.5, 100))
|
||||
p.hit(R, Duration.EIGHTH, velocity=127)
|
||||
p.hit(R, Duration.EIGHTH, velocity=127)
|
||||
|
||||
for i in range(32):
|
||||
q.hit([Q1, Q2, Q3, Q4][i % 4], 0.0625, velocity=min(40 + i * 2, 95))
|
||||
q.hit(QS, Duration.QUARTER, velocity=110)
|
||||
|
||||
for i in range(16):
|
||||
b.hit([B1, B2, B3, B4, B5, B4, B3, B2,
|
||||
B1, B2, B3, B4, B5, B4, B3, B2][i], Duration.SIXTEENTH, velocity=90)
|
||||
b.hit(B3, Duration.HALF, velocity=100)
|
||||
b.hit(B3, Duration.HALF, velocity=100)
|
||||
|
||||
# ── Ending: big unison hits ──
|
||||
p.hit(R, Duration.EIGHTH, velocity=127)
|
||||
q.hit(QS, Duration.EIGHTH, velocity=110)
|
||||
b.hit(B3, Duration.EIGHTH, velocity=100)
|
||||
|
||||
p.rest(Duration.QUARTER + Duration.EIGHTH)
|
||||
q.rest(Duration.QUARTER + Duration.EIGHTH)
|
||||
b.rest(Duration.QUARTER + Duration.EIGHTH)
|
||||
|
||||
p.hit(R, Duration.EIGHTH, velocity=127)
|
||||
q.hit(QS, Duration.EIGHTH, velocity=110)
|
||||
b.hit(B3, Duration.EIGHTH, velocity=100)
|
||||
|
||||
p.rest(Duration.QUARTER + Duration.EIGHTH)
|
||||
q.rest(Duration.QUARTER + Duration.EIGHTH)
|
||||
b.rest(Duration.QUARTER + Duration.EIGHTH)
|
||||
|
||||
# Flam into final CRACK — all sections
|
||||
p.flam(S, Duration.EIGHTH, velocity=127)
|
||||
q.hit(QS, Duration.EIGHTH, velocity=110)
|
||||
b.hit(B3, Duration.EIGHTH, velocity=100)
|
||||
|
||||
p.rest(Duration.QUARTER + Duration.EIGHTH)
|
||||
q.rest(Duration.QUARTER + Duration.EIGHTH)
|
||||
b.rest(Duration.QUARTER + Duration.EIGHTH)
|
||||
|
||||
p.hit(R, Duration.QUARTER, velocity=127)
|
||||
q.hit(QS, Duration.QUARTER, velocity=110)
|
||||
b.hit(B3, Duration.QUARTER, velocity=100)
|
||||
|
||||
p.rest(Duration.HALF)
|
||||
q.rest(Duration.HALF)
|
||||
b.rest(Duration.HALF)
|
||||
|
||||
play_song(score, "Snare Cadence — full drumline (8 snares, 4 quads, 5 basses)")
|
||||
|
||||
|
||||
SONGS = {
|
||||
"1": ("Bossa Nova in A minor", bossa_nova_girl),
|
||||
"2": ("Bebop in Bb major", bebop_in_bb),
|
||||
@@ -2355,6 +2835,8 @@ SONGS = {
|
||||
"28": ("Descent (Generative — different every time)", descent),
|
||||
"29": ("Pop Rock (I-V-vi-IV)", pop_rock),
|
||||
"30": ("Sitar Drone (Bhairav, hold() polyphony)", sitar_drone),
|
||||
"31": ("Acid Tabla (303 + tabla, ramp, articulations)", acid_tabla),
|
||||
"32": ("Snare Cadence (marching snare, flams, diddles)", snare_cadence),
|
||||
}
|
||||
|
||||
if __name__ == "__main__":
|
||||
@@ -2368,7 +2850,7 @@ if __name__ == "__main__":
|
||||
print(f" {key:>2}. {name}")
|
||||
|
||||
print()
|
||||
choice = input(" Pick a song (1-30, or 'all'): ").strip()
|
||||
choice = input(" Pick a song (1-32, or 'all'): ").strip()
|
||||
print()
|
||||
|
||||
if choice == "all":
|
||||
|
||||
@@ -0,0 +1,69 @@
|
||||
"""Sprunki Simon Phase 1 — melody reference.
|
||||
|
||||
Notes transcribed from MIDI. Use as a base for arrangements.
|
||||
|
||||
Usage:
|
||||
python examples/sprunki.py
|
||||
"""
|
||||
|
||||
import sounddevice as sd
|
||||
|
||||
from pytheory import Score, Duration
|
||||
from pytheory.play import render_score, SAMPLE_RATE
|
||||
|
||||
|
||||
def sprunki_simon():
|
||||
score = Score("4/4", bpm=200)
|
||||
|
||||
lead = score.part("lead", synth="square", envelope="pluck", volume=0.5,
|
||||
lowpass=4500, detune=3, reverb=0.1)
|
||||
|
||||
# Phrase A
|
||||
lead.add("E4", 1.0)
|
||||
lead.add("G4", 1.0)
|
||||
lead.rest(1.5)
|
||||
lead.add("A4", 0.5)
|
||||
lead.add("B4", 1.0)
|
||||
lead.add("A4", 1.0)
|
||||
lead.add("G4", 1.0)
|
||||
lead.add("D4", 1.0)
|
||||
|
||||
# Phrase B
|
||||
lead.add("E4", 1.0)
|
||||
lead.add("G4", 1.0)
|
||||
lead.rest(1.5)
|
||||
lead.add("A4", 0.5)
|
||||
lead.add("D4", 2.0)
|
||||
lead.add("B3", 1.0)
|
||||
lead.add("A3", 0.5)
|
||||
lead.add("D4", 0.5)
|
||||
|
||||
# Phrase C
|
||||
lead.add("E4", 1.0)
|
||||
lead.add("G4", 1.0)
|
||||
lead.rest(1.5)
|
||||
lead.add("A4", 0.5)
|
||||
lead.add("B4", 1.0)
|
||||
lead.add("A4", 1.0)
|
||||
lead.add("G4", 1.0)
|
||||
lead.add("B4", 1.0)
|
||||
|
||||
# Phrase D
|
||||
lead.add("A4", 2.0)
|
||||
lead.add("G4", 1.0)
|
||||
lead.add("E4", 1.0)
|
||||
lead.add("B3", 2.0)
|
||||
lead.add("D4", 2.0)
|
||||
|
||||
return score
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
score = sprunki_simon()
|
||||
print(" Sprunki Simon Phase 1")
|
||||
try:
|
||||
buf = render_score(score)
|
||||
sd.play(buf, SAMPLE_RATE)
|
||||
sd.wait()
|
||||
except KeyboardInterrupt:
|
||||
sd.stop()
|
||||
+1
-2
@@ -1,6 +1,6 @@
|
||||
[project]
|
||||
name = "pytheory"
|
||||
version = "0.36.4"
|
||||
version = "0.39.2"
|
||||
description = "Music Theory for Humans"
|
||||
readme = "README.md"
|
||||
license = "MIT"
|
||||
@@ -21,7 +21,6 @@ classifiers = [
|
||||
"Programming Language :: Python :: 3.13",
|
||||
]
|
||||
dependencies = [
|
||||
"numeral",
|
||||
"sounddevice",
|
||||
"scipy",
|
||||
]
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
"""PyTheory: Music Theory for Humans."""
|
||||
|
||||
__version__ = "0.36.4"
|
||||
__version__ = "0.39.2"
|
||||
|
||||
from .tones import Tone, Interval
|
||||
from .systems import System, SYSTEMS, TET
|
||||
@@ -23,7 +23,7 @@ __all__ = [
|
||||
"PROGRESSIONS", "Chord", "Fretboard", "Fingering", "analyze_progression",
|
||||
"System", "SYSTEMS", "TET", "CHARTS", "charts_for_fretboard",
|
||||
"play", "save", "save_midi", "play_progression", "play_pattern",
|
||||
"play_score", "Synth", "Envelope",
|
||||
"play_score", "render_score", "Synth", "Envelope",
|
||||
"Duration", "TimeSignature", "RhythmNote", "Rest", "Score", "Part",
|
||||
"DrumSound", "Pattern", "Section", "INSTRUMENTS",
|
||||
]
|
||||
|
||||
@@ -2,6 +2,43 @@ import math
|
||||
|
||||
REFERENCE_A = 440
|
||||
|
||||
# ── Roman numeral helpers (replaces `numeral` package) ───────────────────
|
||||
|
||||
_ROMAN_MAP = [
|
||||
(1000, "M"), (900, "CM"), (500, "D"), (400, "CD"),
|
||||
(100, "C"), (90, "XC"), (50, "L"), (40, "XL"),
|
||||
(10, "X"), (9, "IX"), (5, "V"), (4, "IV"), (1, "I"),
|
||||
]
|
||||
|
||||
_ROMAN_VALUES = {
|
||||
"I": 1, "V": 5, "X": 10, "L": 50, "C": 100, "D": 500, "M": 1000,
|
||||
}
|
||||
|
||||
|
||||
def int2roman(n: int) -> str:
|
||||
"""Convert an integer to an uppercase Roman numeral string."""
|
||||
result = []
|
||||
for value, numeral in _ROMAN_MAP:
|
||||
while n >= value:
|
||||
result.append(numeral)
|
||||
n -= value
|
||||
return "".join(result)
|
||||
|
||||
|
||||
def roman2int(s: str) -> int:
|
||||
"""Convert a Roman numeral string (case-insensitive) to an integer."""
|
||||
s = s.upper()
|
||||
total = 0
|
||||
prev = 0
|
||||
for ch in reversed(s):
|
||||
val = _ROMAN_VALUES.get(ch, 0)
|
||||
if val < prev:
|
||||
total -= val
|
||||
else:
|
||||
total += val
|
||||
prev = val
|
||||
return total
|
||||
|
||||
# Index of C in the Western tone list (A=0, A#=1, B=2, C=3, ...).
|
||||
# Scientific pitch notation changes octave at C, not A, so this offset
|
||||
# is needed for all octave arithmetic.
|
||||
|
||||
+2
-2
@@ -849,7 +849,7 @@ class Chord:
|
||||
>>> Chord([D4, F4, A4]).analyze("C")
|
||||
'ii'
|
||||
"""
|
||||
import numeral as numeral_mod
|
||||
from ._statics import int2roman
|
||||
from .scales import TonedScale
|
||||
from .systems import SYSTEMS
|
||||
from .tones import Tone
|
||||
@@ -874,7 +874,7 @@ class Chord:
|
||||
scale_names = [t.name for t in scale.tones[:-1]]
|
||||
|
||||
def _build_numeral(root, quality, degree_idx, prefix=""):
|
||||
numeral_str = numeral_mod.int2roman(degree_idx + 1, only_ascii=True)
|
||||
numeral_str = int2roman(degree_idx + 1)
|
||||
suffix = ""
|
||||
if "minor" in quality:
|
||||
numeral_str = numeral_str.lower()
|
||||
|
||||
+372
-53
@@ -2796,6 +2796,151 @@ def _synth_metal_hat(n_samples):
|
||||
return out
|
||||
|
||||
|
||||
def _synth_march_snare(n_samples):
|
||||
"""Marching snare — ultra-tight kevlar head, high and crisp.
|
||||
|
||||
Higher pitched than a kit snare. Very short decay — all attack,
|
||||
no sustain. Tight snare wires give a brief sizzle.
|
||||
"""
|
||||
t = numpy.arange(n_samples, dtype=numpy.float32) / SAMPLE_RATE
|
||||
# Higher-pitched body — tight kevlar pops high
|
||||
body = numpy.sin(2 * numpy.pi * 450 * t) * _exp_decay(n_samples, 60) * 0.4
|
||||
body2 = numpy.sin(2 * numpy.pi * 700 * t) * _exp_decay(n_samples, 75) * 0.2
|
||||
# Sharp stick pop
|
||||
click_len = min(int(SAMPLE_RATE * 0.001), n_samples)
|
||||
click = _noise(click_len) * _exp_decay(click_len, 400) * 1.2
|
||||
# Very tight snare sizzle — higher band, shorter
|
||||
buzz_len = min(int(SAMPLE_RATE * 0.025), n_samples)
|
||||
buzz_raw = _noise(buzz_len)
|
||||
if buzz_len > 20:
|
||||
bl, al = scipy.signal.butter(2, [3500, 8000], btype='band', fs=SAMPLE_RATE)
|
||||
buzz = scipy.signal.lfilter(bl, al, numpy.pad(buzz_raw, (0, max(0, n_samples - buzz_len))))[:buzz_len]
|
||||
else:
|
||||
buzz = buzz_raw
|
||||
buzz *= _exp_decay(buzz_len, 50) * 0.35
|
||||
result = body + body2
|
||||
result[:click_len] += click
|
||||
result[:buzz_len] += buzz
|
||||
return numpy.tanh(result * 2.8)
|
||||
|
||||
|
||||
def _synth_march_rimshot(n_samples):
|
||||
"""Marching rimshot — woody metallic crack.
|
||||
|
||||
The stick catches the rim — you get the full snare hit plus
|
||||
a bright, woody-metallic crack from the aluminum rim. Short
|
||||
ring that dies fast but gives it that cutting edge.
|
||||
"""
|
||||
wave = _synth_march_snare(n_samples)
|
||||
t = numpy.arange(n_samples, dtype=numpy.float32) / SAMPLE_RATE
|
||||
# Rim crack — bright but short, woody-metallic character
|
||||
rim = numpy.sin(2 * numpy.pi * 1100 * t) * _exp_decay(n_samples, 45) * 0.35
|
||||
rim2 = numpy.sin(2 * numpy.pi * 2200 * t) * _exp_decay(n_samples, 55) * 0.2
|
||||
# Hard transient pop
|
||||
pop_len = min(int(SAMPLE_RATE * 0.002), n_samples)
|
||||
pop = _noise(pop_len) * _exp_decay(pop_len, 350) * 1.5
|
||||
# Extra body punch
|
||||
punch = numpy.sin(2 * numpy.pi * 500 * t) * _exp_decay(n_samples, 65) * 0.3
|
||||
result = wave * 1.4 + rim + rim2 + punch
|
||||
result[:pop_len] += pop
|
||||
return numpy.tanh(result * 2.0)
|
||||
|
||||
|
||||
def _synth_march_click(n_samples):
|
||||
"""Stick click — taped hickory sticks clocked together.
|
||||
|
||||
Bright wood-on-wood with a slightly dampened attack from the
|
||||
electrical tape. Not as ringy as a clave — the tape absorbs
|
||||
some of the high overtones — but still bright and snappy.
|
||||
"""
|
||||
t = numpy.arange(n_samples, dtype=numpy.float32) / SAMPLE_RATE
|
||||
# Wood resonance — brighter than before, but tape dampens ring
|
||||
body = numpy.sin(2 * numpy.pi * 1100 * t) * _exp_decay(n_samples, 65) * 0.45
|
||||
body2 = numpy.sin(2 * numpy.pi * 1800 * t) * _exp_decay(n_samples, 80) * 0.25
|
||||
# Woody overtone — gives it that hickory character
|
||||
body3 = numpy.sin(2 * numpy.pi * 2600 * t) * _exp_decay(n_samples, 95) * 0.12
|
||||
# Bright but slightly muffled transient (tape on wood)
|
||||
click_len = min(int(SAMPLE_RATE * 0.001), n_samples)
|
||||
click_raw = _noise(click_len)
|
||||
if click_len > 10:
|
||||
bl, al = scipy.signal.butter(2, [800, 7000], btype='band', fs=SAMPLE_RATE)
|
||||
click = scipy.signal.lfilter(bl, al, numpy.pad(click_raw, (0, max(0, n_samples - click_len))))[:click_len]
|
||||
else:
|
||||
click = click_raw
|
||||
click *= _exp_decay(click_len, 350) * 0.9
|
||||
result = body + body2 + body3
|
||||
result[:click_len] += click
|
||||
return numpy.tanh(result * 2.8)
|
||||
|
||||
|
||||
def _synth_quad(n_samples, pitch=300):
|
||||
"""Marching tenor/quad drum — tuned mylar head, bright and ringy.
|
||||
|
||||
Quads have a distinctive metallic ting from the high-tension
|
||||
mylar head and aluminum shell. More ring than a kit tom,
|
||||
brighter attack, clear pitch.
|
||||
"""
|
||||
t = numpy.arange(n_samples, dtype=numpy.float32) / SAMPLE_RATE
|
||||
# Pitched body — more ring/sustain than snare
|
||||
body = numpy.sin(2 * numpy.pi * pitch * t) * _exp_decay(n_samples, 22) * 0.5
|
||||
# Metallic overtones — the ting
|
||||
ting = numpy.sin(2 * numpy.pi * pitch * 2.3 * t) * _exp_decay(n_samples, 35) * 0.25
|
||||
ting2 = numpy.sin(2 * numpy.pi * pitch * 3.1 * t) * _exp_decay(n_samples, 45) * 0.12
|
||||
# Shell ring
|
||||
shell = numpy.sin(2 * numpy.pi * pitch * 4.7 * t) * _exp_decay(n_samples, 55) * 0.06
|
||||
# Sharp stick attack
|
||||
click_len = min(int(SAMPLE_RATE * 0.001), n_samples)
|
||||
click = _noise(click_len) * _exp_decay(click_len, 400) * 0.8
|
||||
result = body + ting + ting2 + shell
|
||||
result[:click_len] += click
|
||||
return numpy.tanh(result * 2.5)
|
||||
|
||||
|
||||
def _synth_quad_spock(n_samples):
|
||||
"""Quad spock — rim shot on the tenor shell. Bright, ringy, cutting."""
|
||||
t = numpy.arange(n_samples, dtype=numpy.float32) / SAMPLE_RATE
|
||||
ring = numpy.sin(2 * numpy.pi * 1400 * t) * _exp_decay(n_samples, 40) * 0.5
|
||||
ring2 = numpy.sin(2 * numpy.pi * 2100 * t) * _exp_decay(n_samples, 55) * 0.25
|
||||
click_len = min(int(SAMPLE_RATE * 0.001), n_samples)
|
||||
click = _noise(click_len) * _exp_decay(click_len, 400) * 1.0
|
||||
result = ring + ring2
|
||||
result[:click_len] += click
|
||||
return numpy.tanh(result * 2.8)
|
||||
|
||||
|
||||
def _synth_march_bass(n_samples, pitch=60):
|
||||
"""Marching bass drum — deep, boomy, pitched, felt beater thwack.
|
||||
|
||||
The beater hitting the head is a big part of the sound — a round,
|
||||
pillowy thwack followed by the deep pitched boom. More beater
|
||||
sound than a kit bass drum because marching bass drums project
|
||||
outward.
|
||||
"""
|
||||
t = numpy.arange(n_samples, dtype=numpy.float32) / SAMPLE_RATE
|
||||
# Deep pitched body — sustains and rings
|
||||
body = numpy.sin(2 * numpy.pi * pitch * t) * _exp_decay(n_samples, 10) * 0.7
|
||||
body2 = numpy.sin(2 * numpy.pi * pitch * 2 * t) * _exp_decay(n_samples, 16) * 0.2
|
||||
# Sub thump
|
||||
sub = numpy.sin(2 * numpy.pi * pitch * 0.5 * t) * _exp_decay(n_samples, 8) * 0.3
|
||||
# BIG beater thwack — dominant part of the attack
|
||||
thwack_len = min(int(SAMPLE_RATE * 0.025), n_samples)
|
||||
thwack_raw = _noise(thwack_len)
|
||||
if thwack_len > 10:
|
||||
bl, al = scipy.signal.butter(2, [150, 2500], btype='band', fs=SAMPLE_RATE)
|
||||
thwack = scipy.signal.lfilter(bl, al, numpy.pad(thwack_raw, (0, max(0, n_samples - thwack_len))))[:thwack_len]
|
||||
else:
|
||||
thwack = thwack_raw
|
||||
thwack *= _exp_decay(thwack_len, 55) * 1.5
|
||||
# Head slap — the mylar flexing on impact
|
||||
slap_len = min(int(SAMPLE_RATE * 0.008), n_samples)
|
||||
slap = numpy.sin(2 * numpy.pi * pitch * 3 * numpy.arange(slap_len, dtype=numpy.float32) / SAMPLE_RATE)
|
||||
slap *= _exp_decay(slap_len, 90) * 0.4
|
||||
result = body + body2 + sub
|
||||
result[:thwack_len] += thwack
|
||||
result[:slap_len] += slap
|
||||
return numpy.tanh(result * 2.0)
|
||||
|
||||
|
||||
def _synth_tabla_ge_bend(n_samples):
|
||||
"""Tabla Ge with upward pitch bend — palm pressing into bayan head.
|
||||
|
||||
@@ -2890,29 +3035,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):
|
||||
@@ -3014,6 +3157,22 @@ def _render_drum_hit(sound_value, n_samples):
|
||||
DrumSound.METAL_KICK.value: lambda n: _synth_metal_kick(n),
|
||||
DrumSound.METAL_SNARE.value: lambda n: _synth_metal_snare(n),
|
||||
DrumSound.METAL_HAT.value: lambda n: _synth_metal_hat(n),
|
||||
# Marching
|
||||
DrumSound.MARCH_SNARE.value: lambda n: _synth_march_snare(n),
|
||||
DrumSound.MARCH_RIMSHOT.value: lambda n: _synth_march_rimshot(n),
|
||||
DrumSound.MARCH_CLICK.value: lambda n: _synth_march_click(n),
|
||||
# Quads (tenor drums) — pitched high to low
|
||||
DrumSound.QUAD_1.value: lambda n: _synth_quad(n, pitch=400),
|
||||
DrumSound.QUAD_2.value: lambda n: _synth_quad(n, pitch=330),
|
||||
DrumSound.QUAD_3.value: lambda n: _synth_quad(n, pitch=270),
|
||||
DrumSound.QUAD_4.value: lambda n: _synth_quad(n, pitch=220),
|
||||
DrumSound.QUAD_SPOCK.value: lambda n: _synth_quad_spock(n),
|
||||
# Marching bass drums — pitched high to low
|
||||
DrumSound.BASS_1.value: lambda n: _synth_march_bass(n, pitch=90),
|
||||
DrumSound.BASS_2.value: lambda n: _synth_march_bass(n, pitch=75),
|
||||
DrumSound.BASS_3.value: lambda n: _synth_march_bass(n, pitch=62),
|
||||
DrumSound.BASS_4.value: lambda n: _synth_march_bass(n, pitch=52),
|
||||
DrumSound.BASS_5.value: lambda n: _synth_march_bass(n, pitch=42),
|
||||
}
|
||||
|
||||
renderer = _dispatch.get(sound_value, lambda n: _synth_clave(n))
|
||||
@@ -4147,10 +4306,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]
|
||||
@@ -4251,11 +4448,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)))
|
||||
@@ -4453,35 +4657,71 @@ def render_score(score):
|
||||
synth_kwargs["mod_index"] = part.fm_index
|
||||
_temperament = getattr(score, 'temperament', 'equal')
|
||||
_ref_pitch = getattr(score, 'reference_pitch', 440.0)
|
||||
if part.legato:
|
||||
_render_legato_to_buf(
|
||||
part.notes, part_buf, samples_per_beat, total_samples,
|
||||
synth_fn, env_tuple, part.volume, score.bpm,
|
||||
glide_time=part.glide, swing=effective_swing,
|
||||
tempo_map=tempo_map if has_tempo_changes else None,
|
||||
temperament=_temperament, reference_pitch=_ref_pitch)
|
||||
else:
|
||||
_render_notes_to_buf(
|
||||
part.notes, part_buf, samples_per_beat, total_samples,
|
||||
synth_fn, env_tuple, part.volume, score.bpm,
|
||||
swing=effective_swing,
|
||||
tempo_map=tempo_map if has_tempo_changes else None,
|
||||
humanize=part.humanize,
|
||||
detune=part.detune,
|
||||
spread=part.spread,
|
||||
stereo_buf=stereo_buf,
|
||||
sub_osc=part.sub_osc,
|
||||
noise_mix=part.noise_mix,
|
||||
filter_attack=part.filter_attack,
|
||||
filter_decay=part.filter_decay,
|
||||
filter_sustain=part.filter_sustain,
|
||||
filter_amount=part.filter_amount,
|
||||
vel_to_filter=part.vel_to_filter,
|
||||
filter_q=part.lowpass_q,
|
||||
synth_kwargs=synth_kwargs,
|
||||
temperament=_temperament,
|
||||
reference_pitch=_ref_pitch,
|
||||
analog=part.analog)
|
||||
|
||||
n_ensemble = max(1, getattr(part, 'ensemble', 1))
|
||||
|
||||
for _ens_i in range(n_ensemble):
|
||||
# Each ensemble voice gets its own buffer
|
||||
ens_buf = part_buf if n_ensemble == 1 else numpy.zeros(total_samples, dtype=numpy.float32)
|
||||
# Ensemble voices get micro-variations
|
||||
ens_humanize = part.humanize
|
||||
ens_analog = part.analog
|
||||
if n_ensemble > 1:
|
||||
import random as _ens_rnd
|
||||
_ens_rnd.seed(42 + _ens_i * 7)
|
||||
# Hybrid approach:
|
||||
# 1. Consistent player tendency (rush/drag) — seeded per player
|
||||
_player_tendency = _ens_rnd.gauss(0, 0.018)
|
||||
# 2. Tiny per-note wobble on top
|
||||
ens_humanize = max(part.humanize, 0.012)
|
||||
# Each player's drum tuned slightly different
|
||||
ens_analog = max(part.analog, 0.06 + _ens_rnd.uniform(0, 0.08))
|
||||
|
||||
if part.legato:
|
||||
_render_legato_to_buf(
|
||||
part.notes, ens_buf, samples_per_beat, total_samples,
|
||||
synth_fn, env_tuple, part.volume, score.bpm,
|
||||
glide_time=part.glide, swing=effective_swing,
|
||||
tempo_map=tempo_map if has_tempo_changes else None,
|
||||
temperament=_temperament, reference_pitch=_ref_pitch)
|
||||
else:
|
||||
_render_notes_to_buf(
|
||||
part.notes, ens_buf, samples_per_beat, total_samples,
|
||||
synth_fn, env_tuple, part.volume, score.bpm,
|
||||
swing=effective_swing,
|
||||
tempo_map=tempo_map if has_tempo_changes else None,
|
||||
humanize=ens_humanize,
|
||||
detune=part.detune,
|
||||
spread=part.spread,
|
||||
stereo_buf=stereo_buf,
|
||||
sub_osc=part.sub_osc,
|
||||
noise_mix=part.noise_mix,
|
||||
filter_attack=part.filter_attack,
|
||||
filter_decay=part.filter_decay,
|
||||
filter_sustain=part.filter_sustain,
|
||||
filter_amount=part.filter_amount,
|
||||
vel_to_filter=part.vel_to_filter,
|
||||
filter_q=part.lowpass_q,
|
||||
synth_kwargs=synth_kwargs,
|
||||
temperament=_temperament,
|
||||
reference_pitch=_ref_pitch,
|
||||
analog=ens_analog)
|
||||
|
||||
if n_ensemble > 1:
|
||||
# Shift the whole voice by the player's consistent tendency
|
||||
# (some players rush, some drag — this is fixed per player)
|
||||
shift_samples = int(_player_tendency * samples_per_beat)
|
||||
if shift_samples > 0 and shift_samples < total_samples:
|
||||
# Player drags — shift right
|
||||
shifted = numpy.zeros_like(ens_buf)
|
||||
shifted[shift_samples:] = ens_buf[:-shift_samples]
|
||||
ens_buf = shifted
|
||||
elif shift_samples < 0 and abs(shift_samples) < total_samples:
|
||||
# Player rushes — shift left
|
||||
shifted = numpy.zeros_like(ens_buf)
|
||||
shifted[:shift_samples] = ens_buf[-shift_samples:]
|
||||
ens_buf = shifted
|
||||
part_buf += ens_buf / n_ensemble
|
||||
|
||||
# Apply effects — segmented if automation exists
|
||||
auto_points = part._get_automation_points()
|
||||
@@ -4614,6 +4854,22 @@ def render_score(score):
|
||||
DrumSound.METAL_KICK.value: 0.0,
|
||||
DrumSound.METAL_SNARE.value: 0.0,
|
||||
DrumSound.METAL_HAT.value: 0.3,
|
||||
# Marching — centered
|
||||
DrumSound.MARCH_SNARE.value: 0.0,
|
||||
DrumSound.MARCH_RIMSHOT.value: 0.0,
|
||||
DrumSound.MARCH_CLICK.value: 0.0,
|
||||
# Quads — spread across the field
|
||||
DrumSound.QUAD_1.value: -0.3,
|
||||
DrumSound.QUAD_2.value: -0.1,
|
||||
DrumSound.QUAD_3.value: 0.1,
|
||||
DrumSound.QUAD_4.value: 0.3,
|
||||
DrumSound.QUAD_SPOCK.value: 0.0,
|
||||
# Bass drums — spread wide
|
||||
DrumSound.BASS_1.value: -0.5,
|
||||
DrumSound.BASS_2.value: -0.25,
|
||||
DrumSound.BASS_3.value: 0.0,
|
||||
DrumSound.BASS_4.value: 0.25,
|
||||
DrumSound.BASS_5.value: 0.5,
|
||||
}
|
||||
|
||||
# Render all drum Parts (may be one "drums" or split into kick/snare/hats/etc.)
|
||||
@@ -4629,6 +4885,7 @@ def render_score(score):
|
||||
# Track last hit position per sound for choke (new hit dampens
|
||||
# the previous ring on the same drum)
|
||||
_last_hit_start = {}
|
||||
_resonance = {} # sound_id → resonance level (0.0–1.0)
|
||||
|
||||
for hit in drum_part._drum_hits:
|
||||
pos = hit.position
|
||||
@@ -4661,6 +4918,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)
|
||||
@@ -4669,6 +4955,39 @@ def render_score(score):
|
||||
vel_jitter = int(drum_humanize * 10)
|
||||
vel = max(1, min(127, vel + _drum_rnd.randint(-vel_jitter, vel_jitter)))
|
||||
vel_scale = vel / 127.0
|
||||
|
||||
# Sympathetic resonance: marching snare builds up buzz
|
||||
# as hits accumulate. Each hit adds to a resonance counter
|
||||
# that scales extra snare wire buzz into the sound.
|
||||
_RESONANCE_SOUNDS = {
|
||||
DrumSound.MARCH_SNARE.value, DrumSound.MARCH_RIMSHOT.value,
|
||||
}
|
||||
if sound_id in _RESONANCE_SOUNDS:
|
||||
reso = _resonance.get(sound_id, 0.0)
|
||||
# Decay based on gap since last hit
|
||||
if sound_id in _last_hit_start:
|
||||
gap_samples = start - _last_hit_start[sound_id]
|
||||
gap_sec = gap_samples / SAMPLE_RATE
|
||||
if gap_sec > 1.0:
|
||||
reso *= 0.2
|
||||
elif gap_sec > 0.5:
|
||||
reso *= 0.5
|
||||
elif gap_sec > 0.25:
|
||||
reso *= 0.8
|
||||
# Build up (caps at 0.6)
|
||||
reso = min(0.6, reso + 0.08)
|
||||
_resonance[sound_id] = reso
|
||||
# Add sympathetic buzz proportional to resonance
|
||||
if reso > 0.1:
|
||||
buzz_len = min(int(SAMPLE_RATE * 0.06), hit_len)
|
||||
buzz = _noise(buzz_len) * reso * 0.18
|
||||
if buzz_len > 20:
|
||||
bl, al = scipy.signal.butter(
|
||||
2, [3000, 9000], btype='band', fs=SAMPLE_RATE)
|
||||
buzz = scipy.signal.lfilter(bl, al, buzz)
|
||||
buzz *= _exp_decay(buzz_len, 25)
|
||||
wave[:buzz_len] = wave[:buzz_len] + buzz.astype(numpy.float32)
|
||||
|
||||
mono_hit = wave * vel_scale * 0.7
|
||||
# Sidechain trigger — kick only
|
||||
if hit.sound.value == DrumSound.KICK.value:
|
||||
|
||||
+819
-5
@@ -388,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."""
|
||||
@@ -431,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
|
||||
@@ -545,6 +564,33 @@ class DrumSound(Enum):
|
||||
METAL_KICK = 105 # clicky, punchy, tight
|
||||
METAL_SNARE = 106 # crack, bright, cutting
|
||||
METAL_HAT = 107 # tight, short, precise
|
||||
# Marching percussion
|
||||
MARCH_SNARE = 115 # tight, high-tension kevlar head, snare buzz
|
||||
MARCH_RIMSHOT = 116 # stick hits rim + head simultaneously, cracking
|
||||
MARCH_CLICK = 118 # stick click — sticks hit together, no drum
|
||||
# Quads (tenor drums) — 4 drums high to low + spock (rim)
|
||||
QUAD_1 = 119 # highest tenor drum
|
||||
QUAD_2 = 120 # second tenor
|
||||
QUAD_3 = 121 # third tenor
|
||||
QUAD_4 = 122 # lowest tenor (floor tom-ish)
|
||||
QUAD_SPOCK = 123 # rim click on quad shell
|
||||
# Marching bass drums — 5 drums pitched high to low
|
||||
BASS_1 = 124 # highest (smallest) bass drum
|
||||
BASS_2 = 125 # second
|
||||
BASS_3 = 126 # middle
|
||||
BASS_4 = 127 # fourth
|
||||
BASS_5 = 80 # lowest (biggest) bass drum
|
||||
|
||||
|
||||
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:
|
||||
@@ -1572,6 +1618,231 @@ Pattern._PRESETS["tabla solo"] = dict(
|
||||
],
|
||||
)
|
||||
|
||||
# ── Marching snare patterns ───────────────────────────────────────────────
|
||||
MS = DrumSound.MARCH_SNARE
|
||||
MR = DrumSound.MARCH_RIMSHOT
|
||||
MC = DrumSound.MARCH_CLICK
|
||||
Q1 = DrumSound.QUAD_1
|
||||
Q2 = DrumSound.QUAD_2
|
||||
Q3 = DrumSound.QUAD_3
|
||||
Q4 = DrumSound.QUAD_4
|
||||
QS = DrumSound.QUAD_SPOCK
|
||||
B1 = DrumSound.BASS_1
|
||||
B2 = DrumSound.BASS_2
|
||||
B3 = DrumSound.BASS_3
|
||||
B4 = DrumSound.BASS_4
|
||||
B5 = DrumSound.BASS_5
|
||||
|
||||
# Marching basic — standard 4/4 march with rimshot accents on 2 and 4
|
||||
Pattern._PRESETS["march"] = dict(
|
||||
name="march",
|
||||
time_signature="4/4",
|
||||
beats=4.0,
|
||||
hits=[
|
||||
_h(MS, 0.0, 80), _h(MS, 0.5, 55),
|
||||
_h(MR, 1.0, 100), _h(MS, 1.5, 55),
|
||||
_h(MS, 2.0, 80), _h(MS, 2.5, 55),
|
||||
_h(MR, 3.0, 100), _h(MS, 3.5, 55),
|
||||
],
|
||||
)
|
||||
|
||||
# Cadence — 8-beat street beat pattern (the classic drumline cadence)
|
||||
Pattern._PRESETS["cadence"] = dict(
|
||||
name="cadence",
|
||||
time_signature="4/4",
|
||||
beats=8.0,
|
||||
hits=[
|
||||
# Bar 1: syncopated groove
|
||||
_h(MR, 0.0, 105), _h(MS, 0.25, 60), _h(MS, 0.5, 65),
|
||||
_h(MS, 0.75, 55), _h(MR, 1.0, 100),
|
||||
_h(MS, 1.5, 60), _h(MS, 1.75, 58),
|
||||
_h(MR, 2.0, 105), _h(MS, 2.5, 62),
|
||||
_h(MS, 2.75, 55), _h(MR, 3.0, 100),
|
||||
_h(MS, 3.25, 58), _h(MS, 3.5, 60), _h(MS, 3.75, 55),
|
||||
# Bar 2: answer phrase with flams
|
||||
_h(MR, 4.0, 110), _h(MS, 4.25, 62), _h(MS, 4.5, 65),
|
||||
_h(MR, 5.0, 105), _h(MS, 5.25, 58),
|
||||
_h(MS, 5.5, 60), _h(MS, 5.75, 55),
|
||||
_h(MR, 6.0, 110), _h(MS, 6.25, 62),
|
||||
_h(MS, 6.5, 65), _h(MS, 6.75, 62),
|
||||
_h(MR, 7.0, 115), _h(MS, 7.25, 60),
|
||||
_h(MR, 7.5, 110), _h(MR, 7.75, 115),
|
||||
],
|
||||
)
|
||||
|
||||
# Paradiddle — RLRR LRLL on marching snare
|
||||
Pattern._PRESETS["march paradiddle"] = dict(
|
||||
name="march paradiddle",
|
||||
time_signature="4/4",
|
||||
beats=4.0,
|
||||
hits=[
|
||||
# RLRR (R=rimshot accent, L=tap)
|
||||
_h(MR, 0.0, 100), _h(MS, 0.25, 58), _h(MR, 0.5, 65), _h(MR, 0.75, 62),
|
||||
# LRLL
|
||||
_h(MS, 1.0, 58), _h(MR, 1.25, 100), _h(MS, 1.5, 58), _h(MS, 1.75, 55),
|
||||
# RLRR
|
||||
_h(MR, 2.0, 102), _h(MS, 2.25, 60), _h(MR, 2.5, 68), _h(MR, 2.75, 65),
|
||||
# LRLL
|
||||
_h(MS, 3.0, 60), _h(MR, 3.25, 102), _h(MS, 3.5, 60), _h(MS, 3.75, 58),
|
||||
],
|
||||
)
|
||||
|
||||
# March roll — buzz roll crescendo
|
||||
Pattern._PRESETS["march roll"] = dict(
|
||||
name="march roll",
|
||||
time_signature="4/4",
|
||||
beats=4.0,
|
||||
hits=[
|
||||
# Buzz roll as rapid 32nds, crescendo
|
||||
*[_h(MS, i * 0.125, 40 + i * 3) for i in range(28)],
|
||||
# Land on rimshot
|
||||
_h(MR, 3.5, 115), _h(MR, 3.75, 120),
|
||||
],
|
||||
)
|
||||
|
||||
# Quad sweep — run across all 4 drums
|
||||
Pattern._PRESETS["quad sweep"] = dict(
|
||||
name="quad sweep",
|
||||
time_signature="4/4",
|
||||
beats=4.0,
|
||||
hits=[
|
||||
# Sweep down
|
||||
_h(Q1, 0.0, 95), _h(Q2, 0.25, 90), _h(Q3, 0.5, 85), _h(Q4, 0.75, 80),
|
||||
# Sweep up
|
||||
_h(Q4, 1.0, 80), _h(Q3, 1.25, 85), _h(Q2, 1.5, 90), _h(Q1, 1.75, 95),
|
||||
# Double sweep with spocks
|
||||
_h(Q1, 2.0, 98), _h(Q2, 2.125, 92), _h(Q3, 2.25, 88), _h(Q4, 2.375, 82),
|
||||
_h(Q4, 2.5, 82), _h(Q3, 2.625, 88), _h(Q2, 2.75, 92), _h(Q1, 2.875, 98),
|
||||
# Spock accents
|
||||
_h(QS, 3.0, 105), _h(Q1, 3.25, 90), _h(QS, 3.5, 105), _h(Q4, 3.75, 85),
|
||||
],
|
||||
)
|
||||
|
||||
# Quad groove — accented pattern with sweeps
|
||||
Pattern._PRESETS["quad groove"] = dict(
|
||||
name="quad groove",
|
||||
time_signature="4/4",
|
||||
beats=4.0,
|
||||
hits=[
|
||||
_h(Q1, 0.0, 100), _h(Q3, 0.25, 55), _h(Q1, 0.5, 60),
|
||||
_h(Q2, 0.75, 55), _h(Q3, 1.0, 95), _h(Q1, 1.25, 55),
|
||||
_h(Q4, 1.5, 58), _h(Q2, 1.75, 55),
|
||||
_h(Q1, 2.0, 100), _h(Q2, 2.25, 55), _h(Q3, 2.5, 58),
|
||||
_h(Q4, 2.75, 55), _h(QS, 3.0, 105), _h(Q3, 3.25, 55),
|
||||
_h(Q2, 3.5, 58), _h(Q1, 3.75, 60),
|
||||
],
|
||||
)
|
||||
|
||||
# Bass split — classic bass drum splits across the line
|
||||
Pattern._PRESETS["bass split"] = dict(
|
||||
name="bass split",
|
||||
time_signature="4/4",
|
||||
beats=4.0,
|
||||
hits=[
|
||||
# Each bass drum takes a 16th, cascading down then up
|
||||
_h(B1, 0.0, 95), _h(B2, 0.25, 90), _h(B3, 0.5, 85),
|
||||
_h(B4, 0.75, 80), _h(B5, 1.0, 95),
|
||||
_h(B5, 1.5, 90), _h(B4, 1.75, 85),
|
||||
_h(B3, 2.0, 95), _h(B2, 2.25, 90), _h(B1, 2.5, 95),
|
||||
_h(B1, 2.75, 85), _h(B3, 3.0, 100),
|
||||
_h(B5, 3.25, 95), _h(B3, 3.5, 90), _h(B1, 3.75, 95),
|
||||
],
|
||||
)
|
||||
|
||||
# Bass unison — all bass drums hit together on accents
|
||||
Pattern._PRESETS["bass unison"] = dict(
|
||||
name="bass unison",
|
||||
time_signature="4/4",
|
||||
beats=4.0,
|
||||
hits=[
|
||||
# All 5 hit on beat 1
|
||||
_h(B1, 0.0, 100), _h(B2, 0.0, 100), _h(B3, 0.0, 100),
|
||||
_h(B4, 0.0, 100), _h(B5, 0.0, 100),
|
||||
# Split on beat 2
|
||||
_h(B1, 1.0, 90), _h(B3, 1.25, 85), _h(B5, 1.5, 90),
|
||||
# All on beat 3
|
||||
_h(B1, 2.0, 100), _h(B2, 2.0, 100), _h(B3, 2.0, 100),
|
||||
_h(B4, 2.0, 100), _h(B5, 2.0, 100),
|
||||
# Cascade into beat 4
|
||||
_h(B5, 2.75, 80), _h(B4, 3.0, 85), _h(B3, 3.25, 90),
|
||||
_h(B2, 3.5, 95), _h(B1, 3.75, 100),
|
||||
],
|
||||
)
|
||||
|
||||
# Full drumline — snare + quads + bass together
|
||||
Pattern._PRESETS["drumline"] = dict(
|
||||
name="drumline",
|
||||
time_signature="4/4",
|
||||
beats=4.0,
|
||||
hits=[
|
||||
# Snare backbone
|
||||
_h(MR, 0.0, 115), _h(MS, 0.25, 35), _h(MS, 0.5, 38), _h(MS, 0.75, 32),
|
||||
_h(MR, 1.0, 112), _h(MS, 1.25, 35), _h(MS, 1.5, 32), _h(MS, 1.75, 38),
|
||||
_h(MR, 2.0, 115), _h(MS, 2.25, 38), _h(MS, 2.5, 32), _h(MS, 2.75, 35),
|
||||
_h(MR, 3.0, 118), _h(MS, 3.25, 35), _h(MS, 3.5, 32), _h(MS, 3.75, 38),
|
||||
# Quads on accents
|
||||
_h(Q1, 0.0, 95), _h(Q3, 0.5, 55), _h(Q2, 1.0, 90),
|
||||
_h(Q4, 1.5, 55), _h(Q1, 2.0, 95), _h(Q3, 2.5, 55),
|
||||
_h(QS, 3.0, 100), _h(Q2, 3.5, 55),
|
||||
# Bass on the big beats
|
||||
_h(B3, 0.0, 100), _h(B5, 1.0, 95),
|
||||
_h(B1, 2.0, 100), _h(B3, 3.0, 95),
|
||||
],
|
||||
)
|
||||
|
||||
# Chakradar — tihai of tihais (16 beats / 4 bars)
|
||||
# A phrase (Dha Tit Tit Dha Ge Na) is played 3x with increasing intensity,
|
||||
# and within each repetition the final 3 hits form a mini-tihai landing on sam.
|
||||
_chakra_phrase_a = [
|
||||
# Phrase 1 (4 beats): moderate
|
||||
_h(TDHA, 0.0, 85), _h(TTIT, 0.25, 48), _h(TTIT, 0.5, 50),
|
||||
_h(TDHA, 0.75, 80), _h(TGE, 1.0, 72),
|
||||
_h(TNA, 1.5, 68), _h(TTIT, 1.75, 45),
|
||||
# Mini-tihai: Na Dha, Na Dha, Na Dha
|
||||
_h(TNA, 2.0, 72), _h(TDHA, 2.25, 82),
|
||||
_h(TNA, 2.5, 75), _h(TDHA, 2.75, 85),
|
||||
_h(TNA, 3.0, 78), _h(TDHA, 3.25, 90),
|
||||
_h(TTIT, 3.5, 42), _h(TTIT, 3.75, 45),
|
||||
]
|
||||
_chakra_phrase_b = [
|
||||
# Phrase 2 (4 beats): louder, busier
|
||||
_h(TDHA, 4.0, 95), _h(TTIT, 4.125, 50), _h(TTIT, 4.25, 52),
|
||||
_h(TKE, 4.375, 48), _h(TDHA, 4.5, 90), _h(TGE, 4.75, 78),
|
||||
_h(TNA, 5.0, 75), _h(TTIT, 5.25, 48), _h(TTIT, 5.5, 50),
|
||||
_h(TNA, 5.75, 72),
|
||||
# Mini-tihai: Na Dha Ge, Na Dha Ge, Na Dha Ge
|
||||
_h(TNA, 6.0, 80), _h(TDHA, 6.25, 92), _h(TGE, 6.5, 78),
|
||||
_h(TNA, 6.75, 82), _h(TDHA, 7.0, 95), _h(TGE, 7.25, 82),
|
||||
_h(TNA, 7.5, 85), _h(TDHA, 7.75, 100),
|
||||
]
|
||||
_chakra_phrase_c = [
|
||||
# Phrase 3 (4 beats): peak intensity, fastest
|
||||
_h(TDHA, 8.0, 105), _h(TTIT, 8.125, 55), _h(TTIT, 8.25, 58),
|
||||
_h(TKE, 8.375, 52), _h(TNA, 8.5, 85), _h(TTIT, 8.625, 50),
|
||||
_h(TDHA, 8.75, 100), _h(TGE, 9.0, 85),
|
||||
_h(TNA, 9.25, 82), _h(TTIT, 9.5, 55), _h(TTIT, 9.625, 58),
|
||||
_h(TKE, 9.75, 52), _h(TNA, 10.0, 88),
|
||||
# Final tihai — 3x Dha Tit Na Dha landing on sam
|
||||
_h(TDHA, 10.5, 105), _h(TTIT, 10.625, 58), _h(TNA, 10.75, 90),
|
||||
_h(TDHA, 11.0, 108), _h(TTIT, 11.125, 60), _h(TNA, 11.25, 92),
|
||||
_h(TDHA, 11.5, 112), _h(TTIT, 11.625, 62), _h(TNA, 11.75, 95),
|
||||
]
|
||||
_chakra_finale = [
|
||||
# Bar 4: crescendo triplets into massive sam
|
||||
*[_h(TTIT, 12.0 + i * (1/6), 50 + i * 5) for i in range(12)],
|
||||
_h(TDHA, 14.0, 115), _h(TNA, 14.25, 85), _h(TDHA, 14.5, 118),
|
||||
_h(TNA, 14.75, 88),
|
||||
_h(TDHA, 15.0, 120), _h(TGE, 15.25, 95),
|
||||
_h(TDHA, 15.5, 125), _h(DrumSound.TABLA_GE_BEND, 15.75, 110),
|
||||
]
|
||||
|
||||
Pattern._PRESETS["chakradar"] = dict(
|
||||
name="chakradar",
|
||||
time_signature="4/4",
|
||||
beats=16.0,
|
||||
hits=_chakra_phrase_a + _chakra_phrase_b + _chakra_phrase_c + _chakra_finale,
|
||||
)
|
||||
|
||||
# ── Doumbek patterns ──────────────────────────────────────────────────────
|
||||
DKD = DrumSound.DOUMBEK_DUM
|
||||
DKT = DrumSound.DOUMBEK_TEK
|
||||
@@ -1889,6 +2160,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(
|
||||
@@ -2274,6 +2613,160 @@ Pattern._FILLS["tabla call"] = dict(
|
||||
],
|
||||
)
|
||||
|
||||
# ── Djembe fills ─────────────────────────────────────────────────────────
|
||||
|
||||
# Djembe call — bass-tone-slap conversation building to climax
|
||||
Pattern._FILLS["djembe call"] = dict(
|
||||
name="djembe call fill",
|
||||
time_signature="4/4",
|
||||
beats=4.0,
|
||||
hits=[
|
||||
_h(JB, 0.0, 100), _h(JT, 0.25, 70), _h(JT, 0.5, 72),
|
||||
_h(JS, 0.75, 90),
|
||||
_h(JB, 1.0, 95), _h(JT, 1.25, 68), _h(JS, 1.5, 88),
|
||||
_h(JT, 1.75, 75),
|
||||
_h(JS, 2.0, 100), _h(JS, 2.25, 95), _h(JT, 2.5, 78),
|
||||
_h(JB, 2.75, 105),
|
||||
_h(JS, 3.0, 110), _h(JT, 3.25, 80), _h(JS, 3.5, 112),
|
||||
_h(JB, 3.75, 120),
|
||||
],
|
||||
)
|
||||
|
||||
# Djembe roll — rapid slaps accelerating into bass
|
||||
Pattern._FILLS["djembe roll"] = dict(
|
||||
name="djembe roll fill",
|
||||
time_signature="4/4",
|
||||
beats=4.0,
|
||||
hits=[
|
||||
# Accelerating slap roll
|
||||
*[_h(JS, i * 0.125, 50 + i * 4) for i in range(16)],
|
||||
# Bass accents punching through
|
||||
_h(JB, 2.0, 105), _h(JB, 2.5, 108),
|
||||
_h(JB, 3.0, 112), _h(JT, 3.25, 85),
|
||||
_h(JB, 3.5, 115), _h(JS, 3.75, 100),
|
||||
],
|
||||
)
|
||||
|
||||
# Djembe break — syncopated West African-style break
|
||||
Pattern._FILLS["djembe break"] = dict(
|
||||
name="djembe break fill",
|
||||
time_signature="4/4",
|
||||
beats=4.0,
|
||||
hits=[
|
||||
_h(JB, 0.0, 105), _h(JT, 0.25, 65), _h(JS, 0.5, 90),
|
||||
_h(JT, 0.75, 70), _h(JB, 1.0, 100),
|
||||
_h(JS, 1.25, 85), _h(JS, 1.5, 88),
|
||||
_h(JB, 1.75, 95), _h(JT, 2.0, 72),
|
||||
_h(JS, 2.25, 92), _h(JB, 2.5, 108),
|
||||
_h(JT, 2.75, 68), _h(JS, 2.875, 55),
|
||||
_h(JB, 3.0, 115), _h(JS, 3.25, 100),
|
||||
_h(JB, 3.5, 118), _h(JB, 3.75, 120),
|
||||
],
|
||||
)
|
||||
|
||||
# ── Cajón fills ──────────────────────────────────────────────────────────
|
||||
|
||||
# Cajón flam run — slaps accelerating into bass hit
|
||||
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.
|
||||
@@ -2328,6 +2821,7 @@ class Part:
|
||||
cabinet: float = 0.0,
|
||||
cabinet_brightness: float = 0.5,
|
||||
analog: float = 0.0,
|
||||
ensemble: int = 1,
|
||||
fm_ratio: float = 2.0,
|
||||
fm_index: float = 3.0):
|
||||
self.name = name
|
||||
@@ -2374,6 +2868,7 @@ class Part:
|
||||
self.cabinet = cabinet
|
||||
self.cabinet_brightness = cabinet_brightness
|
||||
self.analog = analog
|
||||
self.ensemble = ensemble
|
||||
self.fm_ratio = fm_ratio
|
||||
self.fm_index = fm_index
|
||||
self._system = "western" # default, overridden by Score.part()
|
||||
@@ -2384,7 +2879,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).
|
||||
@@ -2392,6 +2888,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.
|
||||
"""
|
||||
@@ -2402,11 +2902,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
|
||||
@@ -2431,9 +2933,247 @@ 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 flam(self, sound, duration=Duration.QUARTER, *, velocity: int = 110,
|
||||
gap: float = 0.015, grace_vel: float = 0.3,
|
||||
articulation: str = "") -> "Part":
|
||||
"""Add a flam — a grace note immediately before the main hit.
|
||||
|
||||
The grace note is nearly simultaneous with the main hit,
|
||||
thickening the attack. Tighter gap = more like one fat hit,
|
||||
wider gap = audible double.
|
||||
|
||||
Args:
|
||||
sound: A :class:`DrumSound` enum member.
|
||||
duration: Total duration the flam occupies.
|
||||
velocity: Main hit velocity.
|
||||
gap: Beats between grace and main (default 0.008 ≈ 4ms at 120).
|
||||
grace_vel: Grace note velocity as fraction of main (default 0.3).
|
||||
articulation: Optional articulation for the main hit.
|
||||
|
||||
Example::
|
||||
|
||||
>>> p.flam(DrumSound.MARCH_SNARE, Duration.QUARTER, velocity=120)
|
||||
"""
|
||||
if isinstance(duration, (int, float)):
|
||||
dur_val = duration
|
||||
else:
|
||||
dur_val = duration.value if hasattr(duration, 'value') else float(duration)
|
||||
self.hit(sound, gap, velocity=int(velocity * grace_vel))
|
||||
self.hit(sound, dur_val - gap, velocity=velocity, articulation=articulation)
|
||||
return self
|
||||
|
||||
def diddle(self, sound, duration=Duration.EIGHTH, *,
|
||||
velocity: int = 70) -> "Part":
|
||||
"""Add a diddle — two equal strokes in the space of one note.
|
||||
|
||||
A double-stroke roll building block. Two hits split evenly
|
||||
across the duration.
|
||||
|
||||
Args:
|
||||
sound: A :class:`DrumSound` enum member.
|
||||
duration: Total duration (default 8th note). Each stroke
|
||||
gets half.
|
||||
velocity: Velocity for both strokes.
|
||||
|
||||
Example::
|
||||
|
||||
>>> p.diddle(DrumSound.MARCH_SNARE, Duration.EIGHTH, velocity=60)
|
||||
"""
|
||||
if isinstance(duration, (int, float)):
|
||||
dur_val = duration
|
||||
else:
|
||||
dur_val = duration.value if hasattr(duration, 'value') else float(duration)
|
||||
half = dur_val / 2
|
||||
self.hit(sound, half, velocity=velocity)
|
||||
self.hit(sound, half, velocity=int(velocity * 0.9))
|
||||
return self
|
||||
|
||||
def cheese(self, sound, duration=Duration.QUARTER, *, velocity: int = 110,
|
||||
gap: float = 0.008, grace_vel: float = 0.3) -> "Part":
|
||||
"""Add a cheese — a flam followed by a diddle.
|
||||
|
||||
Common marching rudiment: grace-MAIN-tap-tap.
|
||||
|
||||
Args:
|
||||
sound: A :class:`DrumSound` enum member.
|
||||
duration: Total duration.
|
||||
velocity: Main hit velocity.
|
||||
"""
|
||||
if isinstance(duration, (int, float)):
|
||||
dur_val = duration
|
||||
else:
|
||||
dur_val = duration.value if hasattr(duration, 'value') else float(duration)
|
||||
# Flam takes first half, diddle takes second half
|
||||
flam_dur = dur_val * 0.5
|
||||
diddle_dur = dur_val * 0.5
|
||||
self.hit(sound, gap, velocity=int(velocity * grace_vel))
|
||||
self.hit(sound, flam_dur - gap, velocity=velocity)
|
||||
self.hit(sound, diddle_dur / 2, velocity=int(velocity * 0.5))
|
||||
self.hit(sound, diddle_dur / 2, velocity=int(velocity * 0.45))
|
||||
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.
|
||||
|
||||
@@ -2517,6 +3257,79 @@ class Part:
|
||||
points = sorted(set(beat for beat, _ in self._automation))
|
||||
return points
|
||||
|
||||
def ramp(self, over: float = 4.0, resolution: float = 0.25,
|
||||
curve: str = "linear", **params) -> "Part":
|
||||
"""Smoothly ramp parameters from their current values to new targets.
|
||||
|
||||
Generates interpolated automation points — like turning a knob
|
||||
gradually instead of jumping to a new position. Works for any
|
||||
parameter that ``.set()`` accepts.
|
||||
|
||||
Args:
|
||||
over: Duration of the ramp in beats (default 4.0 = 1 bar).
|
||||
Use ``Duration.WHOLE * 4`` for a 4-bar ramp, etc.
|
||||
resolution: How often to insert points, in beats (default 0.25).
|
||||
Lower = smoother but more points.
|
||||
curve: Interpolation shape — ``"linear"`` (default),
|
||||
``"ease_in"`` (slow start, fast end),
|
||||
``"ease_out"`` (fast start, slow end),
|
||||
``"ease_in_out"`` (slow start and end).
|
||||
**params: Target values for any parameter. The ramp starts
|
||||
from the parameter's current value at this beat position.
|
||||
|
||||
Returns:
|
||||
Self for chaining.
|
||||
|
||||
Example::
|
||||
|
||||
>>> lead = score.part("lead", synth="saw", lowpass=200)
|
||||
>>> # Open the filter over 4 bars
|
||||
>>> lead.ramp(over=Duration.WHOLE * 4, lowpass=8000)
|
||||
>>> # Fade reverb in over 2 bars
|
||||
>>> pad.ramp(over=Duration.WHOLE * 2, reverb=0.5)
|
||||
>>> # Multiple params at once with easing
|
||||
>>> lead.ramp(over=8.0, curve="ease_in", lowpass=6000, distortion=0.4)
|
||||
"""
|
||||
current_beat = sum(n.beats for n in self.notes)
|
||||
|
||||
# Map param names to internal names
|
||||
param_map = {
|
||||
"reverb": "reverb_mix", "delay": "delay_mix",
|
||||
"distortion": "distortion_mix", "chorus": "chorus_mix",
|
||||
"phaser": "phaser_mix",
|
||||
}
|
||||
|
||||
# Get current values for each param
|
||||
current_params = self._get_params_at(current_beat)
|
||||
ramps = {}
|
||||
for param, target in params.items():
|
||||
internal = param_map.get(param, param)
|
||||
start = current_params.get(internal, getattr(self, internal, 0.0))
|
||||
ramps[internal] = (float(start), float(target))
|
||||
|
||||
# Generate interpolated points
|
||||
beat = 0.0
|
||||
while beat <= over:
|
||||
t = beat / over if over > 0 else 1.0
|
||||
t = max(0.0, min(1.0, t))
|
||||
|
||||
# Apply curve
|
||||
if curve == "ease_in":
|
||||
t = t * t
|
||||
elif curve == "ease_out":
|
||||
t = 1.0 - (1.0 - t) ** 2
|
||||
elif curve == "ease_in_out":
|
||||
t = 3 * t * t - 2 * t * t * t
|
||||
|
||||
point = {}
|
||||
for internal, (start, end) in ramps.items():
|
||||
point[internal] = start + (end - start) * t
|
||||
|
||||
self._automation.append((current_beat + beat, point))
|
||||
beat += resolution
|
||||
|
||||
return self
|
||||
|
||||
def lfo(self, param: str, *, rate: float = 0.5, min: float = 0.0,
|
||||
max: float = 1.0, bars: float = 4, shape: str = "sine",
|
||||
resolution: float = 0.25) -> "Part":
|
||||
@@ -3044,6 +3857,7 @@ class Score:
|
||||
cabinet: float = None,
|
||||
cabinet_brightness: float = None,
|
||||
analog: float = None,
|
||||
ensemble: int = None,
|
||||
fm_ratio: float = None,
|
||||
fm_index: float = None,
|
||||
fretboard=None) -> Part:
|
||||
@@ -3156,7 +3970,7 @@ class Score:
|
||||
"tremolo_depth": tremolo_depth, "tremolo_rate": tremolo_rate,
|
||||
"phaser": phaser, "phaser_rate": phaser_rate,
|
||||
"cabinet": cabinet, "cabinet_brightness": cabinet_brightness,
|
||||
"analog": analog,
|
||||
"analog": analog, "ensemble": ensemble,
|
||||
"fm_ratio": fm_ratio, "fm_index": fm_index,
|
||||
}
|
||||
for k, v in _locals.items():
|
||||
|
||||
+6
-6
@@ -2,8 +2,6 @@ from __future__ import annotations
|
||||
|
||||
from typing import Optional, Union
|
||||
|
||||
import numeral
|
||||
|
||||
from .systems import SYSTEMS, System
|
||||
from .tones import Tone
|
||||
|
||||
@@ -49,7 +47,8 @@ class Scale:
|
||||
def __repr__(self) -> str:
|
||||
r = []
|
||||
for (i, tone) in enumerate(self.tones):
|
||||
degree = numeral.int2roman(i + 1, only_ascii=True)
|
||||
from ._statics import int2roman
|
||||
degree = int2roman(i + 1)
|
||||
r += [f"{degree}={tone.full_name}"]
|
||||
|
||||
r = " ".join(r)
|
||||
@@ -200,7 +199,7 @@ class Scale:
|
||||
>>> scale.progression("I", "IV", "V", "I")
|
||||
[<Chord (C,E,G)>, <Chord (F,A,C)>, <Chord (G,B,D)>, <Chord (C,E,G)>]
|
||||
"""
|
||||
import numeral as numeral_mod
|
||||
from ._statics import roman2int
|
||||
chords = []
|
||||
for num in numerals:
|
||||
is_seventh = num.endswith("7")
|
||||
@@ -213,7 +212,7 @@ class Scale:
|
||||
elif clean.startswith("#") and len(clean) > 1:
|
||||
clean = clean[1:]
|
||||
flat_offset = 1 # one semitone up
|
||||
degree = numeral_mod.roman2int(clean.upper()) - 1
|
||||
degree = roman2int(clean.upper()) - 1
|
||||
if is_seventh:
|
||||
chord = self.seventh(degree)
|
||||
else:
|
||||
@@ -406,7 +405,8 @@ class Scale:
|
||||
if isinstance(item, str):
|
||||
degrees = []
|
||||
for (i, tone) in enumerate(self.tones):
|
||||
degrees.append(numeral.int2roman(i + 1, only_ascii=True))
|
||||
from ._statics import int2roman
|
||||
degrees.append(int2roman(i + 1))
|
||||
|
||||
if item in degrees:
|
||||
item = degrees.index(item)
|
||||
|
||||
@@ -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
|
||||
@@ -7151,3 +7164,200 @@ def test_all_synths_render_and_enum_match():
|
||||
for s in Synth:
|
||||
wave = s(440, n_samples=1000)
|
||||
assert len(wave) == 1000
|
||||
|
||||
|
||||
# ── Articulations ────────────────────────────────────────────────────────
|
||||
|
||||
def test_articulation_field_on_note():
|
||||
from pytheory.rhythm import Note, Duration
|
||||
n = Note(tone=None, duration=Duration.QUARTER, articulation="staccato")
|
||||
assert n.articulation == "staccato"
|
||||
|
||||
|
||||
def test_articulation_default_empty():
|
||||
from pytheory.rhythm import Note, Duration
|
||||
n = Note(tone=None, duration=Duration.QUARTER)
|
||||
assert n.articulation == ""
|
||||
|
||||
|
||||
def test_part_add_articulation():
|
||||
score = pytheory.Score("4/4", bpm=120)
|
||||
p = score.part("test", synth="sine")
|
||||
p.add("C4", Duration.QUARTER, articulation="staccato")
|
||||
p.add("D4", Duration.QUARTER, articulation="legato")
|
||||
p.add("E4", Duration.QUARTER, articulation="marcato")
|
||||
p.add("F4", Duration.QUARTER, articulation="tenuto")
|
||||
p.add("G4", Duration.QUARTER, articulation="accent")
|
||||
p.add("A4", Duration.QUARTER, articulation="fermata")
|
||||
assert len(p.notes) == 6
|
||||
assert p.notes[0].articulation == "staccato"
|
||||
assert p.notes[5].articulation == "fermata"
|
||||
|
||||
|
||||
@needs_portaudio
|
||||
def test_articulations_render():
|
||||
"""Articulations should produce audio without errors."""
|
||||
from pytheory.play import render_score
|
||||
score = pytheory.Score("4/4", bpm=120)
|
||||
p = score.part("test", synth="sine", volume=0.3)
|
||||
for art in ["", "staccato", "legato", "marcato", "tenuto", "accent", "fermata"]:
|
||||
p.add("C4", Duration.QUARTER, articulation=art)
|
||||
buf = render_score(score)
|
||||
assert len(buf) > 0
|
||||
|
||||
|
||||
# ── Dynamic curves ───────────────────────────────────────────────────────
|
||||
|
||||
def test_crescendo_adds_notes():
|
||||
score = pytheory.Score("4/4", bpm=120)
|
||||
p = score.part("test", synth="sine")
|
||||
p.crescendo(["C4", "D4", "E4", "F4"], Duration.QUARTER,
|
||||
start_vel=40, end_vel=100)
|
||||
assert len(p.notes) == 4
|
||||
assert p.notes[0].velocity == 40
|
||||
assert p.notes[3].velocity == 100
|
||||
|
||||
|
||||
def test_decrescendo_adds_notes():
|
||||
score = pytheory.Score("4/4", bpm=120)
|
||||
p = score.part("test", synth="sine")
|
||||
p.decrescendo(["C4", "D4", "E4", "F4"], Duration.QUARTER,
|
||||
start_vel=110, end_vel=40)
|
||||
assert len(p.notes) == 4
|
||||
assert p.notes[0].velocity == 110
|
||||
assert p.notes[3].velocity == 40
|
||||
|
||||
|
||||
def test_swell_velocity_shape():
|
||||
score = pytheory.Score("4/4", bpm=120)
|
||||
p = score.part("test", synth="sine")
|
||||
p.swell(["C4", "D4", "E4", "F4", "G4"], Duration.QUARTER,
|
||||
low_vel=30, peak_vel=110)
|
||||
assert len(p.notes) == 5
|
||||
# First and last should be near low_vel
|
||||
assert p.notes[0].velocity == 30
|
||||
assert p.notes[4].velocity == 30
|
||||
# Middle should be at or near peak
|
||||
assert p.notes[2].velocity == 110
|
||||
|
||||
|
||||
def test_dynamics_custom_velocities():
|
||||
score = pytheory.Score("4/4", bpm=120)
|
||||
p = score.part("test", synth="sine")
|
||||
p.dynamics(["C4", "D4", "E4"], Duration.QUARTER,
|
||||
velocities=[50, 100, 75])
|
||||
assert p.notes[0].velocity == 50
|
||||
assert p.notes[1].velocity == 100
|
||||
assert p.notes[2].velocity == 75
|
||||
|
||||
|
||||
def test_dynamics_with_articulation():
|
||||
score = pytheory.Score("4/4", bpm=120)
|
||||
p = score.part("test", synth="sine")
|
||||
p.crescendo(["C4", "D4"], Duration.QUARTER,
|
||||
start_vel=40, end_vel=100, articulation="staccato")
|
||||
assert p.notes[0].articulation == "staccato"
|
||||
assert p.notes[1].articulation == "staccato"
|
||||
|
||||
|
||||
# ── Part.hit() ───────────────────────────────────────────────────────────
|
||||
|
||||
def test_part_hit_adds_note():
|
||||
from pytheory.rhythm import DrumSound, _DrumTone
|
||||
score = pytheory.Score("4/4", bpm=120)
|
||||
p = score.part("kit", synth="sine")
|
||||
p.hit(DrumSound.KICK, Duration.QUARTER, velocity=100)
|
||||
p.hit(DrumSound.SNARE, Duration.QUARTER, velocity=90, articulation="accent")
|
||||
assert len(p.notes) == 2
|
||||
assert isinstance(p.notes[0].tone, _DrumTone)
|
||||
assert p.notes[0].tone.sound == DrumSound.KICK
|
||||
assert p.notes[1].articulation == "accent"
|
||||
|
||||
|
||||
@needs_portaudio
|
||||
def test_part_hit_renders():
|
||||
"""Part.hit() drum sounds should render through the note pipeline."""
|
||||
from pytheory.rhythm import DrumSound
|
||||
from pytheory.play import render_score
|
||||
score = pytheory.Score("4/4", bpm=120)
|
||||
p = score.part("kit", synth="sine", volume=0.5)
|
||||
p.hit(DrumSound.KICK, Duration.QUARTER)
|
||||
p.hit(DrumSound.SNARE, Duration.QUARTER)
|
||||
p.hit(DrumSound.CLOSED_HAT, Duration.QUARTER)
|
||||
p.hit(DrumSound.CRASH, Duration.QUARTER)
|
||||
buf = render_score(score)
|
||||
assert len(buf) > 0
|
||||
|
||||
|
||||
# ── Part.ramp() ──────────────────────────────────────────────────────────
|
||||
|
||||
def test_ramp_generates_automation():
|
||||
score = pytheory.Score("4/4", bpm=120)
|
||||
p = score.part("test", synth="saw", lowpass=200)
|
||||
p.ramp(over=4.0, lowpass=8000)
|
||||
# Should have generated automation points
|
||||
assert len(p._automation) > 0
|
||||
# First point should be near 200, last near 8000
|
||||
first_lp = p._automation[0][1].get("lowpass", 0)
|
||||
last_lp = p._automation[-1][1].get("lowpass", 0)
|
||||
assert first_lp < 1000 # near start
|
||||
assert last_lp > 7000 # near target
|
||||
|
||||
|
||||
def test_ramp_easing_curves():
|
||||
score = pytheory.Score("4/4", bpm=120)
|
||||
for curve in ["linear", "ease_in", "ease_out", "ease_in_out"]:
|
||||
p = score.part(f"test_{curve}", synth="saw", lowpass=200)
|
||||
p.ramp(over=4.0, curve=curve, lowpass=8000)
|
||||
assert len(p._automation) > 0
|
||||
|
||||
|
||||
def test_ramp_multiple_params():
|
||||
score = pytheory.Score("4/4", bpm=120)
|
||||
p = score.part("test", synth="saw", lowpass=200)
|
||||
p.ramp(over=4.0, lowpass=8000, reverb=0.5)
|
||||
# Should have both params in automation points
|
||||
last_point = p._automation[-1][1]
|
||||
assert "lowpass" in last_point
|
||||
assert "reverb_mix" in last_point # mapped from "reverb"
|
||||
|
||||
|
||||
# ── Cross-choke ──────────────────────────────────────────────────────────
|
||||
|
||||
def test_djembe_patterns_exist():
|
||||
from pytheory.rhythm import Pattern
|
||||
for name in ["djembe", "kuku", "soli", "dununba", "tiriba",
|
||||
"yankadi", "djansa", "mendiani"]:
|
||||
p = Pattern.preset(name)
|
||||
assert p.beats > 0
|
||||
assert len(p.hits) > 0
|
||||
|
||||
|
||||
def test_djembe_fills_exist():
|
||||
from pytheory.rhythm import Pattern
|
||||
for name in ["djembe call", "djembe roll", "djembe break"]:
|
||||
f = Pattern.fill(name)
|
||||
assert f.beats == 4.0
|
||||
assert len(f.hits) > 0
|
||||
|
||||
|
||||
def test_cajon_fills_exist():
|
||||
from pytheory.rhythm import Pattern
|
||||
for name in ["cajon flam", "cajon rumble", "cajon breakdown"]:
|
||||
f = Pattern.fill(name)
|
||||
assert f.beats == 4.0
|
||||
assert len(f.hits) > 0
|
||||
|
||||
|
||||
def test_metal_fills_exist():
|
||||
from pytheory.rhythm import Pattern
|
||||
for name in ["metal triplet", "metal blast", "metal cascade"]:
|
||||
f = Pattern.fill(name)
|
||||
assert f.beats == 4.0
|
||||
assert len(f.hits) > 0
|
||||
|
||||
|
||||
# ── render_score in __all__ ──────────────────────────────────────────────
|
||||
|
||||
def test_render_score_exported():
|
||||
assert "render_score" in pytheory.__all__
|
||||
|
||||
@@ -486,14 +486,6 @@ wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/d3/ac/686789b9145413f1a61878c407210e41bfdb097976864e0913078b24098c/myst_parser-5.0.0-py3-none-any.whl", hash = "sha256:ab31e516024918296e169139072b81592336f2fef55b8986aa31c9f04b5f7211", size = 84533, upload-time = "2026-01-15T09:08:16.788Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "numeral"
|
||||
version = "0.1.0.17"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/17/0d/ac6a186e169fcbdfea316f78fb5e34981bcf8d5c1d7cc8b6581f597e1e4c/numeral-0.1.0.17-py2.py3-none-any.whl", hash = "sha256:7dff0c1efb9b3655c9c1dc93b4666993741b15abcac0dc01dcb96b21cc20f6ae", size = 22066, upload-time = "2020-04-12T08:24:59.129Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "numpy"
|
||||
version = "2.2.6"
|
||||
@@ -698,10 +690,9 @@ wheels = [
|
||||
|
||||
[[package]]
|
||||
name = "pytheory"
|
||||
version = "0.36.4"
|
||||
version = "0.39.2"
|
||||
source = { editable = "." }
|
||||
dependencies = [
|
||||
{ name = "numeral" },
|
||||
{ name = "scipy", version = "1.15.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" },
|
||||
{ name = "scipy", version = "1.17.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" },
|
||||
{ name = "sounddevice" },
|
||||
@@ -721,7 +712,6 @@ docs = [
|
||||
|
||||
[package.metadata]
|
||||
requires-dist = [
|
||||
{ name = "numeral" },
|
||||
{ name = "scipy" },
|
||||
{ name = "sounddevice" },
|
||||
]
|
||||
|
||||
Reference in New Issue
Block a user