mirror of
https://github.com/kennethreitz/pytheory.git
synced 2026-06-05 23:00:20 +00:00
Compare commits
23 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 6efa4f18ce | |||
| 06fc4cabb7 | |||
| d3a93c18b3 | |||
| 0e10359236 | |||
| df00c3436d | |||
| 2f02df15b8 | |||
| a2740b8d57 | |||
| 840bfcc36c | |||
| 938c1cc132 | |||
| 9dc22db4b2 | |||
| f570e226cd | |||
| 0c5c3abedc | |||
| 35d07b984b | |||
| aec7723ee6 | |||
| b98a40297b | |||
| 9117568b74 | |||
| 11e4417c62 | |||
| 4edf1d983d | |||
| 74b07b1a8a | |||
| c9437209a7 | |||
| 92cb855a49 | |||
| f06c6f77d1 | |||
| 51bd63658f |
@@ -2,6 +2,82 @@
|
||||
|
||||
All notable changes to PyTheory are documented here.
|
||||
|
||||
## 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
|
||||
without advancing the beat position so they play simultaneously.
|
||||
Enables: piano sustain, sitar drone under melody, guitar strum texture.
|
||||
- **Strum uses hold()** — leading string plays simultaneously with chord,
|
||||
no more timing gaps or choppiness
|
||||
- **Improved songs** 1-16: humanize, velocity dynamics, reverb, saxophone
|
||||
for blues
|
||||
- **Ctrl-C handling** — clean stop on all playback functions
|
||||
- **REPL updates** — strum, roll, bend, temperament, reference commands
|
||||
- Song #28 Descent (generative), #29 Pop Rock, #30 Sitar Drone
|
||||
- 862 tests
|
||||
|
||||
## 0.36.1
|
||||
|
||||
- **7 new instrument synths:** pedal steel guitar, theremin, kalimba/thumb
|
||||
|
||||
@@ -322,6 +322,14 @@ against 17 known chord types (triads, 7ths, 9ths, sus, power chords).
|
||||
>>> Chord.from_tones("Bb", "D", "F").identify()
|
||||
'Bb major'
|
||||
|
||||
Enharmonic spellings are fully supported — Cb, Fb, E#, B#, double
|
||||
sharps/flats, and unicode symbols (see :doc:`tones` for details):
|
||||
|
||||
.. code-block:: pycon
|
||||
|
||||
>>> Chord.from_tones("Cb", "Eb", "Gb").identify()
|
||||
'B minor'
|
||||
|
||||
You can also access the root and quality separately:
|
||||
|
||||
.. code-block:: pycon
|
||||
|
||||
+79
-16
@@ -9,8 +9,8 @@ in Atlanta. Over a dancehall pattern, you're in Kingston. The drums ARE
|
||||
the genre -- they tell the listener's body how to move before a single
|
||||
melodic note is played.
|
||||
|
||||
PyTheory includes a complete drum system -- 27 synthesized percussion
|
||||
sounds, 80+ pattern presets across dozens of genres, and 21 fill presets.
|
||||
PyTheory includes a complete drum system -- 51 synthesized percussion
|
||||
sounds, 85+ pattern presets across dozens of genres, and 30 fill presets.
|
||||
Every sound is generated from waveforms; no samples needed.
|
||||
|
||||
Drum Sounds
|
||||
@@ -91,7 +91,7 @@ The ``DrumSound`` enum maps to General MIDI percussion note numbers:
|
||||
>>> DrumSound.CLOSED_HAT.value
|
||||
42
|
||||
|
||||
All 27 sounds, organized by type:
|
||||
All 51 sounds, organized by type:
|
||||
|
||||
**Kicks:** KICK (36)
|
||||
|
||||
@@ -106,7 +106,24 @@ All 27 sounds, organized by type:
|
||||
**Percussion:** COWBELL (56), CLAVE (75), SHAKER (70), TAMBOURINE (54),
|
||||
CONGA_HIGH (63), CONGA_LOW (64), BONGO_HIGH (60), BONGO_LOW (61),
|
||||
TIMBALE_HIGH (65), TIMBALE_LOW (66), AGOGO_HIGH (67), AGOGO_LOW (68),
|
||||
GUIRO (73), MARACAS (70)
|
||||
GUIRO (73)
|
||||
|
||||
**Tabla:** TABLA_NA (86), TABLA_TIN (87), TABLA_GE (88), TABLA_DHA (89),
|
||||
TABLA_TIT (90), TABLA_KE (91), TABLA_GE_BEND (108 -- bayan with upward
|
||||
pitch bend from palm pressing into the head)
|
||||
|
||||
**Dhol:** DHOL_DAGGA (92), DHOL_TILLI (93), DHOL_BOTH (94)
|
||||
|
||||
**Dholak:** DHOLAK_GE (95), DHOLAK_NA (96), DHOLAK_TIT (97)
|
||||
|
||||
**Mridangam:** MRIDANGAM_THAM (98), MRIDANGAM_NAM (99), MRIDANGAM_DIN (100),
|
||||
MRIDANGAM_THA (101)
|
||||
|
||||
**Djembe:** DJEMBE_BASS (102), DJEMBE_TONE (103), DJEMBE_SLAP (104)
|
||||
|
||||
**Cajón:** CAJON_SLAP (109), CAJON_TAP (110)
|
||||
|
||||
**Metal Kit:** METAL_KICK (105), METAL_SNARE (106), METAL_HAT (107)
|
||||
|
||||
Drum Synthesis
|
||||
--------------
|
||||
@@ -200,8 +217,8 @@ everything to its essentials. The metal kit adds 3 dedicated sounds
|
||||
(double kick, china cymbal, stack) and 4 patterns for extreme metal
|
||||
subgenres.
|
||||
|
||||
**World Percussion:** tabla, dhol, dholak, mridangam, djembe -- Deep
|
||||
traditions from across the globe, each with authentic sound sets and
|
||||
**World Percussion:** tabla, dhol, dholak, mridangam, djembe, cajón --
|
||||
Deep traditions from across the globe, each with authentic sound sets and
|
||||
idiomatic patterns. See the World Percussion section below for details.
|
||||
|
||||
**Other:** funk, hip hop, bo diddley, second line, new orleans, waltz,
|
||||
@@ -235,14 +252,17 @@ ending and a new one is about to begin. Without fills, a drum pattern
|
||||
just loops. With them, it breathes and has structure.
|
||||
|
||||
``Pattern.fill()`` loads a 1-bar drum fill -- a short break that
|
||||
transitions between sections. 21 fill presets are available:
|
||||
transitions between sections. 30 fill presets are available:
|
||||
|
||||
.. code-block:: pycon
|
||||
|
||||
>>> Pattern.list_fills()
|
||||
['afrobeat', 'blast', 'bossa nova', 'breakdown', 'buildup',
|
||||
'cumbia', 'disco', 'funk', 'highlife', 'hip hop', 'house',
|
||||
'jazz', 'jazz brush', 'metal', 'reggae', 'rock', 'rock crash',
|
||||
'cajon breakdown', 'cajon flam', 'cajon rumble',
|
||||
'cumbia', 'disco', 'djembe break', 'djembe call', 'djembe roll',
|
||||
'funk', 'highlife', 'hip hop', 'house',
|
||||
'jazz', 'jazz brush', 'metal', 'metal blast', 'metal cascade',
|
||||
'metal triplet', 'reggae', 'rock', 'rock crash',
|
||||
'salsa', 'samba', 'second line', 'trap']
|
||||
|
||||
>>> fill = Pattern.fill("rock")
|
||||
@@ -330,14 +350,25 @@ most expressive percussion instruments ever created. A single tabla
|
||||
player can produce an astonishing range of tones by varying finger
|
||||
placement, pressure, and striking technique.
|
||||
|
||||
**6 sounds** -- covering the primary tabla strokes (na, tin, tun, ge,
|
||||
ke, and ti-ra-ki-ta combinations).
|
||||
**7 sounds** -- covering the primary tabla strokes (na, tin, tun, ge,
|
||||
dha, ke, tit) plus a bayan pitch bend sound (TABLA_GE_BEND) that
|
||||
models the technique of pressing the palm into the bayan head to bend
|
||||
the pitch upward.
|
||||
|
||||
**7 patterns:** teental (16 beats, the most common taal), jhaptaal
|
||||
(10 beats), rupak (7 beats), dadra (6 beats), keherwa (8 beats, folk
|
||||
and light classical), tabla solo, and tiri kita (fast ornamental
|
||||
pattern).
|
||||
|
||||
**5 fills:** tihai (3x crescendo landing on sam), chakkardar (32nd
|
||||
triplet cascade into slam), tiri kita (rapid 16th-note dayan burst),
|
||||
bayan (deep bass bends showcase), tabla call (dayan/bayan call-and-response).
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
score.drums("teental", repeats=4, fill="tihai")
|
||||
score.drums("keherwa", repeats=4, fill="chakkardar")
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
score = Score("4/4", bpm=80)
|
||||
@@ -405,14 +436,19 @@ central to the drum ensemble traditions of Mali, Guinea, and Senegal.
|
||||
**3 sounds** -- bass (open center strike), tone (edge strike), and
|
||||
slap (sharp edge strike).
|
||||
|
||||
**3 patterns:** djembe (a basic accompanying rhythm), kuku (a
|
||||
traditional rhythm from Guinea associated with fishing), and soli (a
|
||||
solo/celebration rhythm).
|
||||
**8 patterns:** djembe (basic accompanying rhythm), kuku (Guinean harvest
|
||||
dance), soli (powerful Mandinka rhythm), dununba (heavy bass-driven),
|
||||
tiriba (joyful Susu rhythm), yankadi (gentle greeting/welcome), djansa
|
||||
(fast Malinke dance), mendiani (women's celebratory dance).
|
||||
|
||||
**3 fills:** djembe call (bass-tone-slap conversation building to climax),
|
||||
djembe roll (rapid slaps accelerating into bass), djembe break (syncopated
|
||||
West African-style break).
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
score = Score("4/4", bpm=120)
|
||||
score.drums("djembe", repeats=4)
|
||||
score.drums("djembe", repeats=8, fill="djembe call", fill_every=4)
|
||||
|
||||
Metal Kit
|
||||
~~~~~~~~~
|
||||
@@ -428,10 +464,37 @@ 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
|
||||
~~~~~
|
||||
|
||||
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.
|
||||
|
||||
**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=8, fill="cajon flam", fill_every=4)
|
||||
|
||||
MIDI Export
|
||||
-----------
|
||||
|
||||
@@ -841,9 +841,11 @@ processes each section independently:
|
||||
lead.arpeggio("Gm", bars=4, pattern="updown", octaves=2)
|
||||
|
||||
Any parameter can be automated: ``lowpass``, ``lowpass_q``, ``highpass``,
|
||||
``reverb``, ``reverb_decay``, ``delay``, ``delay_time``, ``delay_feedback``,
|
||||
``distortion``, ``distortion_drive``, ``chorus``, ``phaser``, ``phaser_rate``,
|
||||
``saturation``, ``tremolo_depth``, ``tremolo_rate``, ``volume``.
|
||||
``reverb``, ``reverb_decay``, ``reverb_type``, ``delay``, ``delay_time``,
|
||||
``delay_feedback``, ``distortion``, ``distortion_drive``, ``chorus``,
|
||||
``phaser``, ``phaser_rate``, ``saturation``, ``tremolo_depth``,
|
||||
``tremolo_rate``, ``cabinet``, ``cabinet_brightness``, ``analog_drift``,
|
||||
``volume``.
|
||||
|
||||
LFO Automation
|
||||
--------------
|
||||
|
||||
+12
-1
@@ -66,6 +66,17 @@ the mix louder and punchier:
|
||||
chords.add(Chord.from_symbol(sym), Duration.WHOLE)
|
||||
play_score(score)
|
||||
|
||||
The render pipeline respects the Score's ``temperament`` and
|
||||
``reference_pitch`` settings, so Baroque or microtonal scores play back
|
||||
at the correct tuning:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
score = Score("4/4", bpm=80, temperament="meantone", reference_pitch=415.0)
|
||||
|
||||
Press **Ctrl+C** at any time during playback to stop — PyTheory catches
|
||||
``KeyboardInterrupt`` and stops audio cleanly.
|
||||
|
||||
See :doc:`sequencing` for how to build scores and parts.
|
||||
|
||||
render_score() -- Headless Rendering
|
||||
@@ -153,7 +164,7 @@ Play a drum pattern through the speakers:
|
||||
play_pattern(Pattern.preset("rock"), repeats=4, bpm=120)
|
||||
play_pattern(Pattern.preset("bossa nova"), repeats=4, bpm=140)
|
||||
|
||||
See :doc:`drums` for the full list of 58 presets and 21 fills.
|
||||
See :doc:`drums` for the full list of 80+ presets and 21 fills.
|
||||
|
||||
play_progression() -- Quick Chord Playback
|
||||
------------------------------------------
|
||||
|
||||
+162
-2
@@ -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,102 @@ 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.
|
||||
|
||||
Swing and Groove
|
||||
----------------
|
||||
|
||||
@@ -478,6 +586,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
|
||||
--------
|
||||
|
||||
@@ -667,8 +819,16 @@ A Score can use any tuning system and temperament:
|
||||
# Just intonation — pure intervals
|
||||
score = Score("4/4", bpm=90, temperament="just")
|
||||
|
||||
Temperaments: ``"equal"`` (default), ``"pythagorean"``, ``"meantone"``,
|
||||
``"just"``.
|
||||
The Score constructor accepts these tuning parameters:
|
||||
|
||||
- ``system``: Musical system name (default ``"western"``). Any system
|
||||
from :doc:`systems` works — ``"indian"``, ``"shruti"``, ``"maqam"``,
|
||||
``"carnatic"``, etc. Note strings in ``Part.add()`` are parsed against
|
||||
this system.
|
||||
- ``temperament``: Tuning temperament — ``"equal"`` (default),
|
||||
``"pythagorean"``, ``"meantone"``, ``"just"``.
|
||||
- ``reference_pitch``: Concert pitch in Hz (default 440.0). Use 415.0
|
||||
for Baroque tuning, 432.0 for "Verdi tuning", etc.
|
||||
|
||||
Custom equal temperaments via the ``TET()`` factory:
|
||||
|
||||
|
||||
+119
-10
@@ -1,7 +1,7 @@
|
||||
Synthesizers
|
||||
============
|
||||
|
||||
PyTheory includes 30 built-in waveforms and 10 ADSR envelope presets.
|
||||
PyTheory includes 41 built-in waveforms and 10 ADSR envelope presets.
|
||||
Every sound is generated from scratch -- no samples or external audio
|
||||
files needed.
|
||||
|
||||
@@ -390,11 +390,11 @@ Dedicated Instrument Synths
|
||||
--------------------------
|
||||
|
||||
Beyond the classic and physical modeling waveforms, PyTheory includes
|
||||
17 dedicated instrument synths. Each one uses tailored synthesis
|
||||
31 dedicated instrument synths. Each one uses tailored synthesis
|
||||
techniques -- additive harmonics, formant shaping, body resonance
|
||||
modeling, and specialized envelopes -- to capture the character of a
|
||||
specific acoustic instrument. These are the waveforms that bring the
|
||||
total count to 30.
|
||||
total count to 41.
|
||||
|
||||
Piano Synth
|
||||
~~~~~~~~~~~
|
||||
@@ -558,6 +558,107 @@ mids, reed buzz, and brass body warmth. Four presets: ``saxophone``,
|
||||
|
||||
sax = score.part("sax", instrument="tenor_sax")
|
||||
|
||||
Pedal Steel Synth
|
||||
~~~~~~~~~~~~~~~~~
|
||||
|
||||
The Nashville crying sound — singing harmonics with slow vibrato
|
||||
and long sustain. Pairs naturally with spring reverb.
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
steel = score.part("steel", instrument="pedal_steel")
|
||||
|
||||
Theremin Synth
|
||||
~~~~~~~~~~~~~~
|
||||
|
||||
Pure sine with natural hand wobble — the eerie sci-fi sound.
|
||||
Best used with legato and glide for continuous pitch.
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
theremin = score.part("theremin", instrument="theremin")
|
||||
|
||||
Kalimba Synth
|
||||
~~~~~~~~~~~~~
|
||||
|
||||
Metal tines on a wooden body. Bright, bell-like attack with
|
||||
inharmonic overtones (modes at 1x, 2.92x, 5.4x).
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
kalimba = score.part("kalimba", instrument="kalimba")
|
||||
|
||||
Steel Drum Synth
|
||||
~~~~~~~~~~~~~~~~
|
||||
|
||||
Hammered metal pan with bright, ringing, tropical character.
|
||||
Inharmonic partials at 2.0x, 3.01x, 4.1x, 5.3x.
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
pan = score.part("pan", instrument="steel_drum")
|
||||
|
||||
Accordion Synth
|
||||
~~~~~~~~~~~~~~~
|
||||
|
||||
Musette-tuned doubled reeds — two slightly detuned reed sets
|
||||
create natural beating. Bellows pressure swell modulates amplitude.
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
acc = score.part("acc", instrument="accordion")
|
||||
|
||||
Didgeridoo Synth
|
||||
~~~~~~~~~~~~~~~~
|
||||
|
||||
Deep cylindrical drone with shifting formant overtones. The
|
||||
overtone singing effect sweeps a resonant peak between 500-1500Hz.
|
||||
Best with cave reverb.
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
didg = score.part("didg", instrument="didgeridoo")
|
||||
|
||||
Bagpipe Synth
|
||||
~~~~~~~~~~~~~
|
||||
|
||||
Bright chanter reed with constant bag pressure. All harmonics
|
||||
peaked around 3-7 (the piercing brightness). No dynamics — always ff.
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
pipes = score.part("pipes", instrument="bagpipe")
|
||||
|
||||
Banjo Synth
|
||||
~~~~~~~~~~~
|
||||
|
||||
Steel strings on a drum-head membrane body. The membrane gives
|
||||
nasal, ringy resonance with faster decay than guitar.
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
banjo = score.part("banjo", instrument="banjo")
|
||||
|
||||
Mandolin Synth
|
||||
~~~~~~~~~~~~~~
|
||||
|
||||
Paired steel strings in 4 courses — natural chorus from the
|
||||
doubled unison strings. Bright, ringing, fast attack.
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
mando = score.part("mando", instrument="mandolin")
|
||||
|
||||
Ukulele Synth
|
||||
~~~~~~~~~~~~~
|
||||
|
||||
Nylon strings on a small body. Mid-heavy resonance (no deep bass),
|
||||
softer attack than guitar, shorter sustain.
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
uke = score.part("uke", instrument="ukulele")
|
||||
|
||||
Granular Synth
|
||||
~~~~~~~~~~~~~~
|
||||
|
||||
@@ -614,7 +715,7 @@ Instrument Presets
|
||||
------------------
|
||||
|
||||
Instead of choosing synth + envelope + effects manually, use an
|
||||
instrument preset — 40+ predefined combinations that approximate real
|
||||
instrument preset — 60+ predefined combinations that approximate real
|
||||
instruments:
|
||||
|
||||
.. code-block:: python
|
||||
@@ -627,20 +728,28 @@ instruments:
|
||||
|
||||
Available instruments:
|
||||
|
||||
**Keys**: piano, electric_piano, organ, harpsichord, celesta, music_box
|
||||
**Keys**: piano, electric_piano, organ, harpsichord, celesta, music_box,
|
||||
accordion
|
||||
|
||||
**Strings**: violin, viola, cello, contrabass, string_ensemble
|
||||
|
||||
**Woodwinds**: flute, clarinet, oboe, bassoon
|
||||
**Woodwinds**: flute, clarinet, oboe, bassoon, saxophone, alto_sax,
|
||||
tenor_sax, bari_sax
|
||||
|
||||
**Brass**: trumpet, trombone, french_horn, tuba, brass_ensemble
|
||||
|
||||
**Plucked**: acoustic_guitar, electric_guitar, distorted_guitar,
|
||||
bass_guitar, upright_bass, harp, sitar, koto
|
||||
**Plucked**: acoustic_guitar, electric_guitar, clean_guitar, crunch_guitar,
|
||||
distorted_guitar, orange_crunch, metal_guitar, bass_guitar, upright_bass,
|
||||
harp, sitar, koto, banjo, mandolin, mandola, ukulele
|
||||
|
||||
**Synth**: synth_lead, synth_pad, synth_bass, acid_bass, 808_bass
|
||||
**World/Exotic**: pedal_steel, theremin, kalimba, steel_drum, didgeridoo,
|
||||
bagpipe
|
||||
|
||||
**Percussion**: vibraphone, marimba, xylophone, glockenspiel, tubular_bells
|
||||
**Synth**: synth_lead, synth_pad, synth_bass, acid_bass, 808_bass,
|
||||
granular_pad, granular_texture, vocal, choir
|
||||
|
||||
**Percussion**: vibraphone, marimba, xylophone, glockenspiel, tubular_bells,
|
||||
timpani
|
||||
|
||||
Explicit kwargs override preset defaults:
|
||||
|
||||
|
||||
+118
-4
@@ -1,10 +1,11 @@
|
||||
Musical Systems
|
||||
===============
|
||||
|
||||
PyTheory supports **six musical systems**, each with its own tone names,
|
||||
scale patterns, and centuries of tradition behind them. Every system
|
||||
maps onto the same 12-tone equal temperament backbone, so you can
|
||||
compare scales across cultures and even combine them in your own music.
|
||||
PyTheory supports **16 musical systems** — 6 core systems mapped onto
|
||||
12-tone equal temperament, plus 10 microtonal systems with their own
|
||||
native tunings. The core systems let you compare scales across cultures;
|
||||
the microtonal systems go beyond 12-TET into genuinely different pitch
|
||||
universes.
|
||||
|
||||
Western
|
||||
-------
|
||||
@@ -271,4 +272,117 @@ produce the same pitches:
|
||||
>>> do4.frequency
|
||||
261.6255653005986
|
||||
|
||||
Microtonal Systems
|
||||
------------------
|
||||
|
||||
Beyond the six 12-TET core systems, PyTheory includes 10 microtonal
|
||||
systems that use their own native tunings — more notes per octave,
|
||||
just intonation ratios, or entirely alien pitch structures.
|
||||
|
||||
Shruti (22 tones per octave)
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
The Indian 22-shruti system divides the octave into 22 unequal steps
|
||||
using just intonation ratios. These microtonal inflections are what
|
||||
give classical Indian music its characteristic expressiveness — pitches
|
||||
that fall "between the cracks" of the piano.
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
score = Score("4/4", bpm=75, system="shruti")
|
||||
|
||||
Maqam (24 tones per octave)
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
The Arabic 24-tone system adds Zalzalian quarter-tone intervals
|
||||
(derived from just intonation ratios of 11 and 13) to the standard
|
||||
12 tones. These "neutral" intervals — halfway between major and minor —
|
||||
are the soul of maqam music.
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
score = Score("4/4", bpm=90, system="maqam")
|
||||
|
||||
Slendro (5-TET)
|
||||
~~~~~~~~~~~~~~~~
|
||||
|
||||
The Javanese slendro scale — 5 equal divisions of the octave. Each
|
||||
step is 240 cents, wider than any Western interval. Ethereal and
|
||||
floating.
|
||||
|
||||
Pelog (9-TET)
|
||||
~~~~~~~~~~~~~
|
||||
|
||||
Approximation of the Javanese pelog tuning as 9 equal divisions of
|
||||
the octave.
|
||||
|
||||
Thai (7-TET)
|
||||
~~~~~~~~~~~~~
|
||||
|
||||
Thai classical music divides the octave into 7 equal steps of ~171
|
||||
cents each — every interval is the same size.
|
||||
|
||||
Makam (53-TET)
|
||||
~~~~~~~~~~~~~~
|
||||
|
||||
Turkish makam music uses 53 equal divisions of the octave — fine
|
||||
enough to approximate virtually any just interval. The system that
|
||||
underlies Ottoman classical music.
|
||||
|
||||
Carnatic (72-TET)
|
||||
~~~~~~~~~~~~~~~~~
|
||||
|
||||
South Indian Carnatic music theory describes 72 melakarta ragas.
|
||||
The 72-TET system provides enough resolution to represent all the
|
||||
microtonal inflections of Carnatic practice.
|
||||
|
||||
19-TET and 31-TET
|
||||
~~~~~~~~~~~~~~~~~~
|
||||
|
||||
Extended equal temperaments that offer better approximations of
|
||||
just intonation intervals than 12-TET. 19-TET has excellent major
|
||||
thirds; 31-TET closely matches quarter-comma meantone.
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
score = Score("4/4", bpm=100, system="19-tet")
|
||||
|
||||
Bohlen-Pierce (13 equal divisions of the tritave)
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
A genuinely alien tuning system — 13 equal divisions of the
|
||||
**tritave** (3:1 ratio) instead of the octave (2:1). No octaves, no
|
||||
fifths, built on 3:5:7 harmonics. Used by experimental composers.
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
score = Score("4/4", bpm=100, system="bohlen-pierce")
|
||||
|
||||
The TET() Factory
|
||||
~~~~~~~~~~~~~~~~~
|
||||
|
||||
Create any equal temperament on the fly with the ``TET()`` factory:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
from pytheory import TET
|
||||
|
||||
edo19 = TET(19) # 19-tone equal temperament
|
||||
edo31 = TET(31) # 31-tone equal temperament
|
||||
score = Score("4/4", bpm=100, system=edo19)
|
||||
|
||||
Tone names in custom TET systems are integers (0, 1, 2, ..., n-1).
|
||||
|
||||
System.tone() Method
|
||||
~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
Any system can create a Tone directly:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
from pytheory import SYSTEMS
|
||||
|
||||
western = SYSTEMS["western"]
|
||||
c4 = western.tone("C", octave=4)
|
||||
|
||||
Music is universal, but every culture hears it differently. These systems are different maps of the same territory -- explore one you've never played in before and see what you find.
|
||||
|
||||
@@ -357,6 +357,45 @@ every tone knows its enharmonic spelling:
|
||||
>>> Tone.from_string("C4", system="western").enharmonic is None
|
||||
True
|
||||
|
||||
Extended Enharmonics
|
||||
~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
PyTheory supports the full range of enharmonic spellings used in real
|
||||
music theory:
|
||||
|
||||
- **Cb** and **Fb** — musically valid flats (Cb = B, Fb = E)
|
||||
- **E#** and **B#** — musically valid sharps (E# = F, B# = C)
|
||||
- **Double sharps** (``##`` or ``x``) — e.g. F## = G
|
||||
- **Double flats** (``bb``) — e.g. Dbb = C
|
||||
- **Unicode symbols** — ``♯`` (sharp), ``♭`` (flat), ``𝄪`` (double sharp),
|
||||
``𝄫`` (double flat) are all recognized and normalized to ASCII
|
||||
|
||||
.. code-block:: pycon
|
||||
|
||||
>>> Tone.from_string("Cb4") # resolves to B3 (octave boundary fix)
|
||||
<Tone B3>
|
||||
>>> Tone.from_string("B#4") # resolves to C5 (octave boundary fix)
|
||||
<Tone C5>
|
||||
>>> Tone.from_string("E#4") # resolves to F4
|
||||
<Tone F4>
|
||||
>>> Tone.from_string("Fb4") # resolves to E4
|
||||
<Tone E4>
|
||||
|
||||
The octave boundary is correctly handled: B# crosses up to the next
|
||||
octave (B#4 = C5), and Cb crosses down (Cb4 = B3), matching standard
|
||||
scientific pitch notation where the octave number increments at C.
|
||||
|
||||
Tone Validation
|
||||
~~~~~~~~~~~~~~~
|
||||
|
||||
Tones are validated on construction — if a tone name is not recognized
|
||||
in its system, a ``ValueError`` is raised:
|
||||
|
||||
.. code-block:: pycon
|
||||
|
||||
>>> Tone.from_string("X4") # not a valid tone name
|
||||
ValueError: ...
|
||||
|
||||
The Circle of Fifths
|
||||
--------------------
|
||||
|
||||
|
||||
+18
-14
@@ -18,8 +18,8 @@ Theory
|
||||
------
|
||||
|
||||
The theory layer works everywhere Python runs — no audio setup needed.
|
||||
Tones, scales, chords, keys, intervals, harmony, 6 musical systems,
|
||||
25 instruments:
|
||||
Tones, scales, chords, keys, intervals, harmony, 16 musical systems,
|
||||
60+ instruments:
|
||||
|
||||
.. code-block:: pycon
|
||||
|
||||
@@ -72,25 +72,29 @@ every time::
|
||||
What's Inside
|
||||
-------------
|
||||
|
||||
- **Theory** — tones, scales (40+ across 6 systems), chords (17 types),
|
||||
- **Theory** — tones, scales (40+ across 16 systems), chords (17 types),
|
||||
keys, Roman numeral analysis, figured bass, pitch class sets (Forte
|
||||
numbers), scale recommendation, modulation, voice leading
|
||||
numbers), scale recommendation, modulation, voice leading, enharmonic
|
||||
support (Cb, Fb, E#, B#, double sharps/flats, unicode symbols)
|
||||
- **Sequencing** — Score, Parts, arpeggiator, legato/glide, velocity,
|
||||
swing, humanize, tempo changes, song sections with repeat
|
||||
swing, humanize, tempo changes, song sections with repeat, strumming,
|
||||
pitch bends (3 types), rolls, tuning systems (TET factory, 4
|
||||
temperaments, reference_pitch)
|
||||
- **Synthesis** — 41 waveforms (including Karplus-Strong pluck, Hammond organ,
|
||||
bowed string, and 14 dedicated instrument synths), 10 envelopes, 40+
|
||||
instrument presets, configurable FM, sub-oscillator, noise layer, filter
|
||||
envelope, velocity-to-brightness, analog oscillator drift, detune, stereo
|
||||
pan/spread, strumming, 80+ drum patterns (stereo panned, including world
|
||||
percussion), 21 fills
|
||||
bowed string, granular, vocal/formant, and 31 dedicated instrument synths),
|
||||
10 envelopes, 60+ instrument presets, configurable FM, sub-oscillator,
|
||||
noise layer, filter envelope, velocity-to-brightness, analog oscillator
|
||||
drift, detune, stereo pan/spread, 80+ drum patterns (stereo panned,
|
||||
including world percussion and cajón), 21 fills, 11 microtonal systems
|
||||
- **Effects** — reverb (algorithmic + 7 convolution IRs, stereo), delay,
|
||||
lowpass/highpass (with resonance), distortion, cabinet simulation,
|
||||
lowpass/highpass (with resonance), distortion, guitar cabinet simulation,
|
||||
saturation, chorus, phaser, tremolo, analog drift, sidechain compression,
|
||||
automation, LFOs. Master bus compressor/limiter
|
||||
- **Instruments** — 49 presets with fingering generation, guitar strumming,
|
||||
pitch bends
|
||||
- **Instruments** — 60+ presets with fingering generation, guitar strumming,
|
||||
pitch bends, note choking
|
||||
- **Output** — stereo playback, WAV export, MIDI import/export
|
||||
- **Interface** — REPL with tab completion, CLI (15 commands), ``pytheory demo``
|
||||
- **Interface** — REPL with tab completion, CLI (15 commands), ``pytheory demo``,
|
||||
KeyboardInterrupt handling for clean stop
|
||||
- **AI-friendly** — Claude Code can compose
|
||||
and play music through PyTheory from natural language
|
||||
|
||||
|
||||
+603
-1
@@ -1843,6 +1843,603 @@ def acoustic_ensemble():
|
||||
play_song(score, "Acoustic Ensemble — Guitar, Uke, Mandolin, Cajón")
|
||||
|
||||
|
||||
def ascent():
|
||||
"""Ascent — from the deep to the sky, theremin solo, tabla solo."""
|
||||
import random
|
||||
from pytheory import Fretboard
|
||||
random.seed(13)
|
||||
score = Score("4/4", bpm=80)
|
||||
REV = "cathedral"
|
||||
T3 = 1.0 / 12.0
|
||||
T9 = 1.0 / 9.0
|
||||
|
||||
NA = DrumSound.TABLA_NA; DH_ = DrumSound.TABLA_DHA
|
||||
TT_ = DrumSound.TABLA_TIT; KE_ = DrumSound.TABLA_KE
|
||||
GB_ = DrumSound.TABLA_GE_BEND; GE_ = DrumSound.TABLA_GE
|
||||
DJB_ = DrumSound.DJEMBE_BASS; DJT_ = DrumSound.DJEMBE_TONE
|
||||
DJS_ = DrumSound.DJEMBE_SLAP
|
||||
CB_ = DrumSound.CAJON_BASS; CSL_ = DrumSound.CAJON_SLAP
|
||||
CT_ = DrumSound.CAJON_TAP
|
||||
|
||||
# Didgeridoo drone
|
||||
didg = score.part("didg", instrument="didgeridoo", volume=0.15)
|
||||
for _ in range(32):
|
||||
didg.add("E1", 4.0, velocity=52)
|
||||
|
||||
# 1: THE DEEP (1-4)
|
||||
grain = score.part("grain", synth="granular_synth", envelope="pad",
|
||||
lowpass=800, reverb=0.55, reverb_type="cave", volume=0.12)
|
||||
for note in ["E2", "B2", "E2", "G2"]:
|
||||
grain.add(note, 4.0, velocity=40)
|
||||
|
||||
# 2: LIGHT (3-6)
|
||||
kal = score.part("kalimba", instrument="kalimba", volume=0.2,
|
||||
delay=0.2, delay_time=0.375, delay_feedback=0.4,
|
||||
reverb=0.45, reverb_type=REV)
|
||||
kal.rest(8.0)
|
||||
for note, vel in [("B4",58),("E5",62),("G5",65),("B5",68),
|
||||
("G5",62),("E5",58),("B4",55),("E4",52)]:
|
||||
kal.add(note, Duration.QUARTER, velocity=vel)
|
||||
kal.rest(4.0)
|
||||
for note, vel in [("E5",60),("G5",65),("B5",70),("E6",72),
|
||||
("B5",65),("G5",60),("E5",58),("B4",55)]:
|
||||
kal.add(note, Duration.QUARTER, velocity=vel)
|
||||
|
||||
# 3: SURFACING (5-8)
|
||||
cello = score.part("cello", instrument="cello", volume=0.22,
|
||||
reverb=0.4, reverb_type=REV)
|
||||
cello.rest(16.0)
|
||||
for note, dur, vel in [("E2",4.0,52),("G2",4.0,55),("B2",4.0,58),("E3",4.0,62)]:
|
||||
cello.add(note, dur, velocity=vel)
|
||||
|
||||
# 4: AIR (7-10) — piano + quiet uke
|
||||
piano = score.part("piano", instrument="piano", volume=0.28,
|
||||
reverb=0.35, reverb_type=REV)
|
||||
piano.rest(24.0)
|
||||
for notes in [
|
||||
["E3","B3","E4","G4","B4","G4","E4","B3"],
|
||||
["C3","G3","C4","E4","G4","E4","C4","G3"],
|
||||
["A2","E3","A3","C4","E4","C4","A3","E3"],
|
||||
["B2","F#3","B3","D4","F#4","D4","B3","F#3"],
|
||||
]:
|
||||
for n in notes:
|
||||
piano.add(n, Duration.EIGHTH, velocity=random.randint(58, 70))
|
||||
|
||||
fb = Fretboard.ukulele()
|
||||
uke = score.part("uke", instrument="ukulele", fretboard=fb,
|
||||
reverb=0.4, reverb_type=REV, humanize=0.2,
|
||||
pan=-0.2, volume=0.15)
|
||||
uke.rest(28.0)
|
||||
for sym in ["Em", "C", "Am", "B"]:
|
||||
vd = random.randint(60, 75)
|
||||
vu = random.randint(45, 62)
|
||||
uke.strum(sym, Duration.QUARTER, direction="down", velocity=vd)
|
||||
uke.strum(sym, Duration.EIGHTH, direction="up", velocity=vu)
|
||||
uke.strum(sym, Duration.EIGHTH, direction="down", velocity=vd - 8)
|
||||
uke.strum(sym, Duration.QUARTER, direction="up", velocity=vu)
|
||||
uke.strum(sym, Duration.QUARTER, direction="down", velocity=vd)
|
||||
|
||||
# 5: THEREMIN SOLO (11-16)
|
||||
steel = score.part("steel", instrument="pedal_steel", volume=0.16,
|
||||
reverb=0.4, reverb_type=REV, pan=0.2)
|
||||
steel.rest(36.0)
|
||||
for note, dur, vel in [("B4",3.0,58),("A4",1.0,50),("G4",2.0,55),("E4",2.0,52)]:
|
||||
steel.add(note, dur, velocity=vel)
|
||||
|
||||
theremin = score.part("theremin", instrument="theremin", volume=0.3,
|
||||
reverb=0.45, reverb_type=REV,
|
||||
delay=0.15, delay_time=0.375, delay_feedback=0.3)
|
||||
theremin.rest(40.0)
|
||||
for note, dur, vel in [
|
||||
("E4",2.0,62),("G4",1.0,58),("B4",1.0,62),
|
||||
("A4",2.0,65),("G4",1.0,58),("E4",1.0,55),("D4",3.0,60),("E4",1.0,58),
|
||||
("G4",1.0,62),("B4",1.5,68),("D5",0.5,65),
|
||||
("E5",2.0,72),("D5",1.0,65),("B4",1.0,62),
|
||||
("G4",1.0,60),("A4",1.0,62),("B4",2.0,68),
|
||||
("E5",1.5,75),("G5",1.5,80),("B5",2.0,85),
|
||||
("A5",1.0,78),("G5",1.0,72),("E5",2.0,75),
|
||||
("D5",1.0,68),("B4",1.0,62),("E4",4.0,70),
|
||||
]:
|
||||
theremin.add(note, dur, velocity=vel)
|
||||
|
||||
strings = score.part("strings", instrument="string_ensemble", volume=0.15,
|
||||
reverb=0.45, reverb_type=REV)
|
||||
strings.rest(40.0)
|
||||
for sym, vel in [("Em",52),("C",55),("Am",58),("B",55),("Em",60),("C",62)]:
|
||||
strings.add(Chord.from_symbol(sym), 4.0, velocity=vel)
|
||||
|
||||
# 6: THE PEAK (17-18)
|
||||
flute = score.part("flute", instrument="flute", volume=0.2, reverb=0.4, reverb_type=REV)
|
||||
flute.rest(64.0)
|
||||
for note, dur, vel in [
|
||||
("B5",2.0,55),("A5",1.0,50),("G5",1.0,52),
|
||||
("E5",2.0,55),("D5",1.0,50),("E5",1.0,52),
|
||||
]:
|
||||
flute.add(note, dur, velocity=vel)
|
||||
|
||||
harp = score.part("harp", instrument="harp", volume=0.16, reverb=0.4, reverb_type=REV)
|
||||
harp.rest(68.0)
|
||||
for n in ["B5","G5","E5","B4","G4","E4","B3","E3"]:
|
||||
harp.add(n, Duration.QUARTER, velocity=random.randint(48, 58))
|
||||
|
||||
timp = score.part("timp", instrument="timpani")
|
||||
timp.rest(64.0)
|
||||
timp.roll("E2", 4.0, velocity_start=20, velocity_end=95, speed=0.125)
|
||||
timp.add("E2", 4.0, velocity=105)
|
||||
|
||||
# Drums: silence → cajón → djembe → tabla solo
|
||||
score.add_pattern(Pattern(name="s", time_signature="4/4", beats=16.0, hits=[]),
|
||||
repeats=1)
|
||||
p_caj = Pattern(name="caj", time_signature="4/4", beats=4.0, hits=[
|
||||
_Hit(CB_, 0.0, 58), _Hit(CT_, 0.5, 22), _Hit(CSL_, 1.0, 50),
|
||||
_Hit(CT_, 1.5, 20), _Hit(CB_, 2.0, 55), _Hit(CT_, 2.5, 22),
|
||||
_Hit(CSL_, 3.0, 52),
|
||||
])
|
||||
score.add_pattern(p_caj, repeats=4)
|
||||
p_dj = Pattern(name="dj", time_signature="4/4", beats=4.0, hits=[
|
||||
_Hit(DJB_, 0.0, 42), _Hit(DJT_, 1.0, 35), _Hit(DJT_, 1.5, 30),
|
||||
_Hit(DJS_, 2.0, 38), _Hit(DJT_, 3.0, 35),
|
||||
])
|
||||
score.add_pattern(p_dj, repeats=10)
|
||||
|
||||
# 7: TABLA SOLO (bars 19-26)
|
||||
score.add_pattern(Pattern(name="ts1", time_signature="4/4", beats=8.0, hits=[
|
||||
_Hit(DH_, 0.0, 72), _Hit(NA, 2.5, 48),
|
||||
_Hit(DH_, 4.0, 75), _Hit(TT_, 5.5, 25), _Hit(NA, 6.0, 45),
|
||||
_Hit(DH_, 7.5, 72),
|
||||
]), repeats=1)
|
||||
score.add_pattern(Pattern(name="ts2", time_signature="4/4", beats=8.0, hits=[
|
||||
_Hit(DH_, 0.0, 85), _Hit(TT_, 0.25, 30), _Hit(TT_, 0.5, 32),
|
||||
_Hit(NA, 1.0, 62), _Hit(TT_, 1.25, 25), _Hit(NA, 2.0, 58),
|
||||
_Hit(TT_, 2.5, 28), _Hit(DH_, 3.0, 82),
|
||||
_Hit(DH_, 4.0, 90), _Hit(TT_, 4.25, 32), _Hit(TT_, 4.5, 35),
|
||||
_Hit(NA, 5.0, 68), _Hit(KE_, 5.5, 35), _Hit(NA, 6.0, 62),
|
||||
_Hit(KE_, 6.5, 38), _Hit(DH_, 7.0, 92), _Hit(GB_, 7.5, 85),
|
||||
]), repeats=1)
|
||||
score.add_pattern(Pattern(name="ts3", time_signature="4/4", beats=8.0, hits=[
|
||||
_Hit(NA, 0.0, 108), _Hit(NA, 0.25, 52), _Hit(TT_, 0.5, 35),
|
||||
_Hit(NA, 0.75, 100),
|
||||
_Hit(GE_, 1.0, 98), _Hit(GE_, 1.25, 48), _Hit(GB_, 1.5, 90),
|
||||
_Hit(GE_, 1.75, 42),
|
||||
_Hit(NA, 2.0, 110), _Hit(TT_, 2.125, 28), _Hit(TT_, 2.25, 32),
|
||||
_Hit(NA, 2.5, 102), _Hit(TT_, 2.625, 30), _Hit(TT_, 2.75, 35),
|
||||
_Hit(GB_, 3.0, 110), _Hit(KE_, 3.25, 45), _Hit(GE_, 3.5, 65),
|
||||
_Hit(DH_, 4.0, 112),
|
||||
*[_Hit(TT_ if i % 2 == 0 else KE_, 5.0 + i * T9, 35 + i * 5)
|
||||
for i in range(9)],
|
||||
_Hit(DH_, 7.0, 115),
|
||||
]), repeats=1)
|
||||
score.add_pattern(Pattern(name="ts4", time_signature="4/4", beats=8.0, hits=[
|
||||
*[_Hit(TT_, i * T3, 32 + i * 2) for i in range(12)],
|
||||
_Hit(DH_, 1.0, 115), _Hit(GB_, 1.5, 105),
|
||||
_Hit(NA, 2.0, 108), _Hit(KE_, 2.125, 42), _Hit(NA, 2.25, 102),
|
||||
_Hit(KE_, 2.375, 45), _Hit(NA, 2.5, 105), _Hit(KE_, 2.625, 48),
|
||||
_Hit(NA, 2.75, 110), _Hit(DH_, 3.0, 118),
|
||||
*[_Hit(TT_, 3.5 + i * T3, 30 + i * 4) for i in range(12)],
|
||||
_Hit(DH_, 4.5, 120), _Hit(DH_, 4.75, 115), _Hit(GB_, 5.0, 112),
|
||||
_Hit(GE_, 5.5, 85), _Hit(GE_, 6.5, 82),
|
||||
*[_Hit(NA if i % 3 == 0 else TT_, 5.5 + i * (2.0 / 9.0),
|
||||
40 + (i % 3) * 12) for i in range(9)],
|
||||
_Hit(DH_, 7.5, 127), _Hit(GB_, 7.875, 127),
|
||||
]), repeats=1)
|
||||
score.set_drum_effects(reverb=0.3, reverb_type=REV)
|
||||
|
||||
play_song(score, "Ascent — Deep → Sky → Theremin Solo → Tabla Solo")
|
||||
|
||||
|
||||
def descent():
|
||||
"""Descent — generative, different every time. From sky to deep."""
|
||||
import random
|
||||
import time
|
||||
from pytheory import Fretboard, TonedScale
|
||||
|
||||
# No seed — truly random every play
|
||||
random.seed(int(time.time() * 1000) % 2**31)
|
||||
|
||||
# Random key — always minor, always dark
|
||||
roots = ["A", "B", "C", "D", "E", "F", "G"]
|
||||
root = random.choice(roots)
|
||||
mode = random.choice(["minor", "harmonic minor"])
|
||||
key = Key(root, mode)
|
||||
scale_tones = [t.name for t in key.scale.tones[:-1]]
|
||||
|
||||
bpm = random.randint(65, 85)
|
||||
score = Score("4/4", bpm=bpm)
|
||||
REV = random.choice(["cathedral", "taj_mahal", "cave"])
|
||||
|
||||
T3 = 1.0 / 12.0
|
||||
NA = DrumSound.TABLA_NA; DH_ = DrumSound.TABLA_DHA
|
||||
TT_ = DrumSound.TABLA_TIT; KE_ = DrumSound.TABLA_KE
|
||||
GB_ = DrumSound.TABLA_GE_BEND; GE_ = DrumSound.TABLA_GE
|
||||
|
||||
print(f" {root} {mode} | {bpm} bpm | {REV}")
|
||||
|
||||
# ── Pad drone — random synth, the whole piece ──
|
||||
pad_synth = random.choice(["strings_synth", "granular_synth", "vocal_synth"])
|
||||
pad = score.part("pad", synth=pad_synth, envelope="pad",
|
||||
detune=random.randint(6, 14), spread=0.4,
|
||||
reverb=0.5, reverb_type=REV, volume=0.15,
|
||||
analog=random.uniform(0.1, 0.4))
|
||||
prog = key.progression("i", "iv", "V", "i")
|
||||
for _ in range(6):
|
||||
for chord in prog:
|
||||
pad.add(chord, Duration.WHOLE, velocity=random.randint(48, 62))
|
||||
|
||||
# ── 1: HIGH — theremin or flute, random melody from scale ──
|
||||
lead_synth = random.choice(["theremin", "flute", "pedal_steel"])
|
||||
lead = score.part("lead", instrument=lead_synth, volume=0.25,
|
||||
reverb=0.4, reverb_type=REV,
|
||||
delay=0.2, delay_time=random.uniform(0.2, 0.5),
|
||||
delay_feedback=random.uniform(0.2, 0.4))
|
||||
lead.rest(4.0)
|
||||
# Generate melody by WALKING the scale — stepwise with occasional leaps
|
||||
# Real melodies move to neighboring notes, not random jumps
|
||||
scale_idx = len(scale_tones) - 1 # start high
|
||||
octave = 5
|
||||
for _ in range(random.randint(10, 16)):
|
||||
note = scale_tones[scale_idx]
|
||||
dur = random.choice([1.5, 2.0, 3.0, 4.0])
|
||||
vel = random.randint(58, 70)
|
||||
lead.add(f"{note}{octave}", dur, velocity=vel)
|
||||
# Mostly step down (descent!), sometimes hold, rare leap
|
||||
r = random.random()
|
||||
if r < 0.5: # step down
|
||||
scale_idx -= 1
|
||||
elif r < 0.65: # step up (tension)
|
||||
scale_idx += 1
|
||||
elif r < 0.8: # leap down
|
||||
scale_idx -= random.randint(2, 3)
|
||||
# else hold same note
|
||||
# Wrap octaves
|
||||
if scale_idx < 0:
|
||||
scale_idx += len(scale_tones)
|
||||
octave -= 1
|
||||
elif scale_idx >= len(scale_tones):
|
||||
scale_idx -= len(scale_tones)
|
||||
octave += 1
|
||||
octave = max(3, min(5, octave))
|
||||
|
||||
# ── 2: Kalimba or steel drum — random arpeggios ──
|
||||
sparkle_inst = random.choice(["kalimba", "steel_drum", "vibraphone", "harp"])
|
||||
sparkle = score.part("sparkle", instrument=sparkle_inst, volume=0.18,
|
||||
delay=0.2, delay_time=random.uniform(0.15, 0.4),
|
||||
delay_feedback=random.uniform(0.3, 0.5),
|
||||
reverb=0.4, reverb_type=REV)
|
||||
sparkle.rest(8.0)
|
||||
# Arpeggios — walk chord tones in patterns, not random
|
||||
for chord in prog * 3:
|
||||
chord_tones = [t.name for t in chord.tones]
|
||||
pattern_type = random.choice(["up", "down", "updown"])
|
||||
oct = random.choice([4, 5])
|
||||
if pattern_type == "up":
|
||||
seq = chord_tones + [chord_tones[0]]
|
||||
elif pattern_type == "down":
|
||||
seq = list(reversed(chord_tones)) + [chord_tones[-1]]
|
||||
else:
|
||||
seq = chord_tones + list(reversed(chord_tones[1:-1]))
|
||||
# Pad to 8 notes
|
||||
while len(seq) < 8:
|
||||
seq = seq + seq
|
||||
seq = seq[:8]
|
||||
for i, n in enumerate(seq):
|
||||
o = oct + (1 if i >= 4 and pattern_type == "up" else 0)
|
||||
sparkle.add(f"{n}{o}", Duration.EIGHTH,
|
||||
velocity=random.randint(48, 58))
|
||||
|
||||
# ── 3: Piano — random broken chords ──
|
||||
piano = score.part("piano", instrument="piano", volume=0.22,
|
||||
reverb=0.35, reverb_type=REV)
|
||||
piano.rest(random.uniform(8.0, 16.0))
|
||||
for chord in prog * 2:
|
||||
chord_tones = [t.name for t in chord.tones]
|
||||
oct = random.choice([3, 4])
|
||||
# Walk up the chord then back down
|
||||
up = [f"{n}{oct}" for n in chord_tones]
|
||||
up.append(f"{chord_tones[0]}{oct+1}")
|
||||
down = [f"{n}{oct}" for n in reversed(chord_tones[:-1])]
|
||||
arp = (up + down)[:8]
|
||||
for n in arp:
|
||||
piano.add(n, Duration.EIGHTH,
|
||||
velocity=random.randint(58, 65))
|
||||
|
||||
# ── 4: Cello — descending long notes ──
|
||||
cello = score.part("cello", instrument="cello", volume=0.2,
|
||||
reverb=0.4, reverb_type=REV)
|
||||
cello.rest(16.0)
|
||||
oct = 3
|
||||
for _ in range(8):
|
||||
note = random.choice(scale_tones)
|
||||
cello.add(f"{note}{oct}", 4.0, velocity=random.randint(48, 62))
|
||||
if random.random() < 0.4 and oct > 2:
|
||||
oct -= 1
|
||||
|
||||
# ── 5: Bass — deep, sparse ──
|
||||
bass_inst = random.choice(["upright_bass", "didgeridoo"])
|
||||
bass = score.part("bass", instrument=bass_inst, volume=0.18)
|
||||
bass.rest(random.uniform(4.0, 12.0))
|
||||
for chord in prog * 4:
|
||||
r = chord.root
|
||||
if r:
|
||||
oct = 2 if bass_inst == "upright_bass" else 1
|
||||
bass.add(f"{r.name}{oct}", 4.0,
|
||||
velocity=random.randint(50, 65))
|
||||
|
||||
# ── Drums: random combination ──
|
||||
drum_start = random.randint(2, 5) * 4 # 8-20 beats silence
|
||||
score.add_pattern(Pattern(name="s", time_signature="4/4",
|
||||
beats=float(drum_start), hits=[]),
|
||||
repeats=1)
|
||||
|
||||
# Pick random world drum combo
|
||||
drum_choice = random.choice(["cajon folk", "djembe", "keherwa", "dadra"])
|
||||
score.drums(drum_choice, repeats=random.randint(6, 12))
|
||||
|
||||
# Tabla solo at the end — always
|
||||
score.add_pattern(Pattern(name="ts1", time_signature="4/4", beats=8.0, hits=[
|
||||
_Hit(DH_, 0.0, 72), _Hit(NA, 2.5, 48),
|
||||
_Hit(DH_, 4.0, 75), _Hit(TT_, 5.5, 25), _Hit(NA, 6.0, 45),
|
||||
_Hit(DH_, 7.5, 72),
|
||||
]), repeats=1)
|
||||
|
||||
# Generate random tabla solo — different every time
|
||||
solo_hits = []
|
||||
beat = 0.0
|
||||
while beat < 16.0:
|
||||
stroke = random.choice([DH_, NA, TT_, KE_, GB_, GE_])
|
||||
vel = random.randint(35, 120)
|
||||
# Ghost notes are quiet, accents are loud
|
||||
if stroke == TT_:
|
||||
vel = random.randint(25, 50)
|
||||
elif stroke in (DH_, GB_):
|
||||
vel = random.randint(70, 120)
|
||||
solo_hits.append(_Hit(stroke, beat, vel))
|
||||
# Random spacing — mix of tight and open
|
||||
beat += random.choice([0.125, 0.25, 0.25, 0.5, 0.5, 1.0])
|
||||
|
||||
score.add_pattern(Pattern(name="ts_rand", time_signature="4/4",
|
||||
beats=16.0, hits=solo_hits), repeats=1)
|
||||
score.set_drum_effects(reverb=0.3, reverb_type=REV)
|
||||
|
||||
play_song(score, f"Descent — {root} {mode} (generative, {REV})")
|
||||
|
||||
|
||||
def pop_rock():
|
||||
"""Pop Rock — the I-V-vi-IV progression that launched a thousand hits."""
|
||||
import random
|
||||
from pytheory import Fretboard
|
||||
random.seed(42)
|
||||
score = Score("4/4", bpm=120)
|
||||
fb = Fretboard.guitar()
|
||||
|
||||
guitar = score.part("guitar", instrument="acoustic_guitar", fretboard=fb,
|
||||
reverb=0.2, reverb_type="plate", humanize=0.15, pan=-0.2)
|
||||
bass = score.part("bass", instrument="bass_guitar", volume=0.35, humanize=0.1)
|
||||
lead = score.part("lead", instrument="electric_guitar",
|
||||
cabinet=1.0, cabinet_brightness=0.6,
|
||||
reverb=0.2, reverb_type="plate", pan=0.2)
|
||||
strings = score.part("strings", instrument="string_ensemble", volume=0.12,
|
||||
reverb=0.35, reverb_type="hall")
|
||||
|
||||
prog = ["G", "D", "Em", "C"]
|
||||
|
||||
# Intro — picked
|
||||
for sym in prog:
|
||||
chord_obj = Chord.from_symbol(sym)
|
||||
tones = [t.name for t in chord_obj.tones]
|
||||
for t in tones:
|
||||
guitar.add(f"{t}3", Duration.QUARTER,
|
||||
velocity=random.randint(62, 72))
|
||||
|
||||
# Verse — strum
|
||||
for _ in range(2):
|
||||
for sym in prog:
|
||||
vd = random.randint(72, 88)
|
||||
vu = random.randint(55, 70)
|
||||
guitar.strum(sym, Duration.QUARTER, direction="down", velocity=vd)
|
||||
guitar.strum(sym, Duration.EIGHTH, direction="up", velocity=vu)
|
||||
guitar.strum(sym, Duration.EIGHTH, direction="down", velocity=vd - 8)
|
||||
guitar.strum(sym, Duration.QUARTER, direction="up", velocity=vu)
|
||||
guitar.strum(sym, Duration.QUARTER, direction="down", velocity=vd)
|
||||
|
||||
bass_notes = ["G2", "G2", "D2", "D2", "E2", "E2", "C2", "C2"]
|
||||
for _ in range(3):
|
||||
for n in bass_notes:
|
||||
bass.add(n, Duration.HALF, velocity=random.randint(68, 78))
|
||||
|
||||
# Melody
|
||||
for _ in range(4):
|
||||
lead.rest(Duration.WHOLE)
|
||||
for note, dur, vel in [
|
||||
("B4",0.5,78),("B4",0.5,75),("B4",0.5,78),("B4",0.5,72),
|
||||
("A4",0.5,75),("A4",0.5,72),("B4",1.0,80),
|
||||
("B4",0.5,78),("B4",0.5,75),("B4",0.5,78),("D5",0.5,82),
|
||||
("G4",0.75,72),("G4",0.25,68),("A4",1.0,78),
|
||||
("B4",0.5,78),("B4",0.5,75),("B4",0.5,78),("B4",0.5,72),
|
||||
("A4",0.5,75),("A4",0.5,72),("B4",0.5,78),("A4",0.5,72),
|
||||
("G4",2.0,80),
|
||||
]:
|
||||
lead.add(note, dur, velocity=vel)
|
||||
|
||||
for _ in range(4):
|
||||
strings.rest(Duration.WHOLE)
|
||||
for sym in prog * 2:
|
||||
strings.add(Chord.from_symbol(sym), Duration.WHOLE,
|
||||
velocity=random.randint(55, 68))
|
||||
|
||||
score.drums("rock", repeats=6)
|
||||
|
||||
play_song(score, "Pop Rock — G D Em C (I-V-vi-IV)")
|
||||
|
||||
|
||||
def sitar_drone():
|
||||
"""Sitar Drone — Raga Bhairav with hold() polyphony, 22-shruti JI."""
|
||||
shruti = SYSTEMS["shruti"]
|
||||
score = Score("4/4", bpm=72, system=shruti)
|
||||
|
||||
ts = TonedScale(system=shruti, tonic=Tone("Sa", octave=4, system=shruti))
|
||||
bh = list(ts["bhairav"].tones)
|
||||
S, kR, G, M, P, kD, N, S2 = bh
|
||||
|
||||
sitar = score.part("sitar", instrument="sitar", volume=0.3,
|
||||
reverb=0.4, reverb_type="taj_mahal")
|
||||
# Sa drone held — rings under the whole melody
|
||||
sitar.hold(Tone("Sa", octave=3, system=shruti), 32.0, velocity=60)
|
||||
sitar.rest(Duration.WHOLE)
|
||||
for tone, dur, vel in [
|
||||
(S, 2.0, 72), (kR, 0.5, 62), (S, 0.5, 68),
|
||||
(G, 2.0, 78), (kR, 0.5, 60), (G, 0.5, 70),
|
||||
(M, 1.5, 75), (P, 2.5, 82),
|
||||
(kD, 0.5, 65), (P, 1.0, 75), (M, 0.5, 65),
|
||||
(G, 0.5, 68), (kR, 0.5, 60), (S, 2.0, 78),
|
||||
(kR, 0.25, 62), (G, 0.25, 65), (M, 0.25, 70), (P, 0.25, 75),
|
||||
(kD, 0.25, 70), (N, 0.25, 78), (S2, 0.5, 85),
|
||||
(N, 0.25, 68), (kD, 0.25, 62), (P, 0.5, 68),
|
||||
(M, 0.5, 62), (G, 0.5, 65), (kR, 0.5, 58),
|
||||
(S, 4.0, 80),
|
||||
]:
|
||||
sitar.add(tone, dur, velocity=vel)
|
||||
|
||||
tanpura = score.part("tanpura", synth="strings_synth", envelope="pad",
|
||||
detune=3, lowpass=900, volume=0.12,
|
||||
reverb=0.5, reverb_type="taj_mahal")
|
||||
tanpura_pa = score.part("tanpura_pa", synth="strings_synth", envelope="pad",
|
||||
detune=3, lowpass=1200, volume=0.1,
|
||||
reverb=0.5, reverb_type="taj_mahal")
|
||||
for _ in range(8):
|
||||
tanpura.add(Tone("Sa", octave=3, system=shruti), Duration.WHOLE)
|
||||
tanpura_pa.add(Tone("Pa", octave=3, system=shruti), Duration.WHOLE)
|
||||
|
||||
NA = DrumSound.TABLA_NA
|
||||
DH_ = DrumSound.TABLA_DHA
|
||||
TT_ = DrumSound.TABLA_TIT
|
||||
silence = Pattern(name="s", time_signature="4/4", beats=8.0, hits=[])
|
||||
score.add_pattern(silence, repeats=1)
|
||||
p = Pattern(name="t", time_signature="4/4", beats=4.0, hits=[
|
||||
_Hit(DH_, 0.0, 68), _Hit(TT_, 0.5, 25), _Hit(NA, 1.0, 55),
|
||||
_Hit(NA, 2.0, 52), _Hit(DH_, 3.0, 68),
|
||||
])
|
||||
score.add_pattern(p, repeats=6)
|
||||
score.set_drum_effects(reverb=0.25, reverb_type="taj_mahal")
|
||||
|
||||
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)")
|
||||
|
||||
|
||||
SONGS = {
|
||||
"1": ("Bossa Nova in A minor", bossa_nova_girl),
|
||||
"2": ("Bebop in Bb major", bebop_in_bb),
|
||||
@@ -1870,6 +2467,11 @@ SONGS = {
|
||||
"24": ("Journey (Western → World → Indian)", journey),
|
||||
"25": ("Epic Bhairav (Orchestral + Tabla)", epic_bhairav),
|
||||
"26": ("Acoustic Ensemble (Guitar+Uke+Mando+Cajón)", acoustic_ensemble),
|
||||
"27": ("Ascent (Deep → Sky → Tabla Solo)", ascent),
|
||||
"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),
|
||||
}
|
||||
|
||||
if __name__ == "__main__":
|
||||
@@ -1883,7 +2485,7 @@ if __name__ == "__main__":
|
||||
print(f" {key:>2}. {name}")
|
||||
|
||||
print()
|
||||
choice = input(" Pick a song (1-26, or 'all'): ").strip()
|
||||
choice = input(" Pick a song (1-31, or 'all'): ").strip()
|
||||
print()
|
||||
|
||||
if choice == "all":
|
||||
|
||||
+1
-2
@@ -1,6 +1,6 @@
|
||||
[project]
|
||||
name = "pytheory"
|
||||
version = "0.36.2"
|
||||
version = "0.39.0"
|
||||
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.2"
|
||||
__version__ = "0.39.0"
|
||||
|
||||
from .tones import Tone, Interval
|
||||
from .systems import System, SYSTEMS, TET
|
||||
|
||||
@@ -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()
|
||||
|
||||
+198
-27
@@ -1237,6 +1237,47 @@ def steel_drum_wave(hz, peak=SAMPLE_PEAK, n_samples=SAMPLE_RATE):
|
||||
return (peak * wave).astype(numpy.int16)
|
||||
|
||||
|
||||
def harmonium_wave(hz, peak=SAMPLE_PEAK, n_samples=SAMPLE_RATE):
|
||||
"""Harmonium — Indian pump organ, single free reed per note.
|
||||
|
||||
Unlike accordion (doubled musette reeds), the harmonium has one
|
||||
reed per note — no beating, just a pure, nasal, reedy tone.
|
||||
Constant bellows pressure, warm but slightly buzzy. The sound
|
||||
of kirtan, qawwali, and devotional music.
|
||||
"""
|
||||
t = numpy.arange(n_samples, dtype=numpy.float64) / SAMPLE_RATE
|
||||
rng = numpy.random.default_rng(int(hz * 100) % 2**31)
|
||||
|
||||
# Single reed — odd harmonics stronger (like clarinet but warmer)
|
||||
wave = numpy.zeros(n_samples, dtype=numpy.float64)
|
||||
for n in range(1, 12):
|
||||
f_n = hz * n
|
||||
if f_n >= SAMPLE_RATE / 2:
|
||||
break
|
||||
amp = (1.0 / n) * (1.0 if n % 2 == 1 else 0.5)
|
||||
phase = rng.uniform(0, 2 * numpy.pi)
|
||||
wave += amp * numpy.sin(2 * numpy.pi * f_n * t + phase)
|
||||
|
||||
# Bellows pressure — gentle swell, slower than accordion
|
||||
bellows = 0.9 + 0.1 * numpy.sin(2 * numpy.pi * 0.5 * t)
|
||||
wave *= bellows
|
||||
|
||||
# Nasal character — slight midrange boost
|
||||
import scipy.signal as _sig
|
||||
center = min(1200, hz * 3)
|
||||
lo = max(20, int(center - 300))
|
||||
hi = min(SAMPLE_RATE // 2 - 1, int(center + 300))
|
||||
if lo < hi:
|
||||
bp, ap = _sig.butter(2, [lo, hi], btype='band', fs=SAMPLE_RATE)
|
||||
nasal = _sig.lfilter(bp, ap, wave) * 0.2
|
||||
wave += nasal
|
||||
|
||||
mx = numpy.abs(wave).max()
|
||||
if mx > 0:
|
||||
wave /= mx
|
||||
return (peak * wave).astype(numpy.int16)
|
||||
|
||||
|
||||
def accordion_wave(hz, peak=SAMPLE_PEAK, n_samples=SAMPLE_RATE):
|
||||
"""Accordion — bellows-driven free reeds.
|
||||
|
||||
@@ -1794,6 +1835,7 @@ class Synth(Enum):
|
||||
THEREMIN = "theremin_synth"
|
||||
KALIMBA = "kalimba_synth"
|
||||
STEEL_DRUM = "steel_drum_synth"
|
||||
HARMONIUM = "harmonium_synth"
|
||||
ACCORDION = "accordion_synth"
|
||||
DIDGERIDOO = "didgeridoo_synth"
|
||||
BAGPIPE = "bagpipe_synth"
|
||||
@@ -1825,7 +1867,7 @@ _SYNTH_FUNCTIONS = {
|
||||
"granular_synth": granular_wave, "vocal_synth": vocal_wave,
|
||||
"pedal_steel_synth": pedal_steel_wave, "theremin_synth": theremin_wave,
|
||||
"kalimba_synth": kalimba_wave, "steel_drum_synth": steel_drum_wave,
|
||||
"accordion_synth": accordion_wave, "didgeridoo_synth": didgeridoo_wave,
|
||||
"harmonium_synth": harmonium_wave, "accordion_synth": accordion_wave, "didgeridoo_synth": didgeridoo_wave,
|
||||
"bagpipe_synth": bagpipe_wave,
|
||||
"banjo_synth": banjo_wave, "mandolin_synth": mandolin_wave,
|
||||
"ukulele_synth": ukulele_wave,
|
||||
@@ -2584,6 +2626,52 @@ def _synth_mridangam_tha(n_samples):
|
||||
return out
|
||||
|
||||
|
||||
def _synth_doumbek_dum(n_samples):
|
||||
"""Doumbek Dum — open center strike, deep and round."""
|
||||
t = numpy.arange(n_samples, dtype=numpy.float32) / SAMPLE_RATE
|
||||
freq = 80 + 40 * numpy.exp(-25 * t)
|
||||
phase = 2 * numpy.pi * numpy.cumsum(freq) / SAMPLE_RATE
|
||||
body = numpy.sin(phase) * _exp_decay(n_samples, 8) * 0.8
|
||||
thump_len = min(int(SAMPLE_RATE * 0.04), n_samples)
|
||||
import scipy.signal as _sig
|
||||
thump = _noise(thump_len)
|
||||
if thump_len > 20:
|
||||
bl, al = _sig.butter(2, [50, 250], btype='band', fs=SAMPLE_RATE)
|
||||
thump = _sig.lfilter(bl, al, numpy.pad(thump, (0, max(0, n_samples - thump_len))))[:thump_len].astype(numpy.float32)
|
||||
thump *= _exp_decay(thump_len, 22) * 0.7
|
||||
body[:thump_len] += thump
|
||||
return numpy.tanh(body * 1.3).astype(numpy.float32)
|
||||
|
||||
|
||||
def _synth_doumbek_tek(n_samples):
|
||||
"""Doumbek Tek — sharp edge strike, bright and cutting."""
|
||||
t = numpy.arange(n_samples, dtype=numpy.float32) / SAMPLE_RATE
|
||||
ring = numpy.sin(2 * numpy.pi * 400 * t) * _exp_decay(n_samples, 22) * 0.5
|
||||
ring2 = numpy.sin(2 * numpy.pi * 900 * t) * 0.3 * _exp_decay(n_samples, 30)
|
||||
click_len = min(int(SAMPLE_RATE * 0.005), n_samples)
|
||||
click = _noise(click_len) * _exp_decay(click_len, 300) * 0.9
|
||||
import scipy.signal as _sig
|
||||
if click_len > 10:
|
||||
bl, al = _sig.butter(2, [2000, min(8000, SAMPLE_RATE // 2 - 1)], btype='band', fs=SAMPLE_RATE)
|
||||
click = _sig.lfilter(bl, al, numpy.pad(click, (0, max(0, n_samples - click_len))))[:click_len].astype(numpy.float32)
|
||||
result = ring + ring2
|
||||
result[:click_len] += click
|
||||
return numpy.tanh(result * 1.8).astype(numpy.float32)
|
||||
|
||||
|
||||
def _synth_doumbek_ka(n_samples):
|
||||
"""Doumbek Ka — muted edge slap, short and dry."""
|
||||
n = min(n_samples, int(SAMPLE_RATE * 0.04))
|
||||
t = numpy.arange(n, dtype=numpy.float32) / SAMPLE_RATE
|
||||
body = numpy.sin(2 * numpy.pi * 350 * t) * _exp_decay(n, 30) * 0.4
|
||||
slap = _noise(min(80, n)) * _exp_decay(min(80, n), 200) * 0.7
|
||||
result = body
|
||||
result[:min(80, n)] += slap
|
||||
out = numpy.zeros(n_samples, dtype=numpy.float32)
|
||||
out[:n] = numpy.tanh(result * 1.5)
|
||||
return out
|
||||
|
||||
|
||||
def _synth_cajon_bass(n_samples):
|
||||
"""Cajón bass — palm strike on center of the face.
|
||||
|
||||
@@ -2802,29 +2890,27 @@ def _synth_djembe_tone(n_samples):
|
||||
def _synth_djembe_slap(n_samples):
|
||||
"""Djembe slap — edge strike with fingers spread, sharp crack.
|
||||
|
||||
The highest, sharpest djembe sound. Fingers fan out on contact
|
||||
creating a loud crack with minimal sustain.
|
||||
The highest, sharpest djembe sound. A dry, high-pitched pop from
|
||||
goatskin membrane — NOT a snare. Tight attack, very short decay,
|
||||
skin character rather than wire rattle.
|
||||
"""
|
||||
t = numpy.arange(n_samples, dtype=numpy.float32) / SAMPLE_RATE
|
||||
# Sharp crack — mostly noise
|
||||
crack_len = min(int(SAMPLE_RATE * 0.02), n_samples)
|
||||
crack = _noise(crack_len) * _exp_decay(crack_len, 100) * 1.0
|
||||
# Brief high-pitched ring
|
||||
ring = numpy.sin(2 * numpy.pi * 600 * t) * _exp_decay(n_samples, 25) * 0.4
|
||||
ring2 = numpy.sin(2 * numpy.pi * 1200 * t) * 0.2 * _exp_decay(n_samples, 35)
|
||||
# Brief membrane pop
|
||||
thump_len = min(int(SAMPLE_RATE * 0.02), n_samples)
|
||||
thump_raw = _noise(thump_len)
|
||||
if thump_len > 20:
|
||||
bl, al = scipy.signal.butter(2, [300, 2000], btype='band', fs=SAMPLE_RATE)
|
||||
thump = scipy.signal.lfilter(bl, al, numpy.pad(thump_raw, (0, max(0, n_samples - thump_len))))[:thump_len]
|
||||
# High membrane pop — goatskin resonance, much higher than snare
|
||||
pop = numpy.sin(2 * numpy.pi * 900 * t) * _exp_decay(n_samples, 50) * 0.5
|
||||
pop2 = numpy.sin(2 * numpy.pi * 1600 * t) * _exp_decay(n_samples, 60) * 0.25
|
||||
pop3 = numpy.sin(2 * numpy.pi * 2400 * t) * _exp_decay(n_samples, 80) * 0.12
|
||||
# Very short filtered click — hand-on-skin transient, not noise rattle
|
||||
click_len = min(int(SAMPLE_RATE * 0.008), n_samples)
|
||||
click_raw = _noise(click_len)
|
||||
if click_len > 20:
|
||||
bl, al = scipy.signal.butter(2, 1800 / (SAMPLE_RATE / 2), btype='high')
|
||||
click = scipy.signal.lfilter(bl, al, numpy.pad(click_raw, (0, max(0, n_samples - click_len))))[:click_len]
|
||||
else:
|
||||
thump = thump_raw
|
||||
thump *= _exp_decay(thump_len, 80) * 0.8
|
||||
result = ring + ring2
|
||||
result[:crack_len] += crack
|
||||
result[:thump_len] += thump
|
||||
return numpy.tanh(result * 1.7)
|
||||
click = click_raw
|
||||
click *= _exp_decay(click_len, 150) * 0.6
|
||||
result = pop + pop2 + pop3
|
||||
result[:click_len] += click
|
||||
return numpy.tanh(result * 1.5)
|
||||
|
||||
|
||||
def _synth_guiro(n_samples):
|
||||
@@ -2914,6 +3000,10 @@ def _render_drum_hit(sound_value, n_samples):
|
||||
DrumSound.DJEMBE_BASS.value: lambda n: _synth_djembe_bass(n),
|
||||
DrumSound.DJEMBE_TONE.value: lambda n: _synth_djembe_tone(n),
|
||||
DrumSound.DJEMBE_SLAP.value: lambda n: _synth_djembe_slap(n),
|
||||
# Doumbek
|
||||
DrumSound.DOUMBEK_DUM.value: lambda n: _synth_doumbek_dum(n),
|
||||
DrumSound.DOUMBEK_TEK.value: lambda n: _synth_doumbek_tek(n),
|
||||
DrumSound.DOUMBEK_KA.value: lambda n: _synth_doumbek_ka(n),
|
||||
# Cajon
|
||||
DrumSound.CAJON_BASS.value: lambda n: _synth_cajon_bass(n),
|
||||
DrumSound.CAJON_SLAP.value: lambda n: _synth_cajon_slap(n),
|
||||
@@ -4055,10 +4145,48 @@ def _render_notes_to_buf(notes, buf, samples_per_beat, total_samples,
|
||||
start += _rnd.randint(-max_offset, max_offset)
|
||||
start = max(0, start)
|
||||
dur_ms = note.beats * 60_000 / bpm
|
||||
# Articulation: adjust duration and velocity
|
||||
art = getattr(note, 'articulation', '')
|
||||
art_vel_mult = 1.0
|
||||
art_attack_mult = 1.0 # multiplier for envelope attack
|
||||
if art == 'staccato':
|
||||
dur_ms *= 0.4 # short and bouncy
|
||||
elif art == 'legato':
|
||||
dur_ms *= 1.15 # slight overlap into next note
|
||||
elif art == 'marcato':
|
||||
art_vel_mult = 1.25 # heavier
|
||||
art_attack_mult = 0.3 # sharper attack
|
||||
elif art == 'tenuto':
|
||||
art_attack_mult = 1.8 # softer attack, full duration
|
||||
elif art == 'accent':
|
||||
art_vel_mult = 1.2
|
||||
elif art == 'fermata':
|
||||
dur_ms *= 1.5 # held longer
|
||||
n_samples = int(SAMPLE_RATE * dur_ms / 1000)
|
||||
if start + n_samples > total_samples:
|
||||
n_samples = total_samples - start
|
||||
if n_samples > 0 and start >= 0:
|
||||
# Drum hit via Part.hit() — use drum synth directly
|
||||
from .rhythm import _DrumTone
|
||||
if isinstance(note.tone, _DrumTone):
|
||||
drum_wave = _render_drum_hit(note.tone.sound.value, n_samples)
|
||||
mixed = drum_wave.astype(numpy.float32)
|
||||
# Staccato fade-out for drums
|
||||
if art == 'staccato':
|
||||
fade_len = min(int(SAMPLE_RATE * 0.01), len(mixed))
|
||||
if fade_len > 0:
|
||||
mixed[-fade_len:] *= numpy.linspace(1.0, 0.0, fade_len).astype(numpy.float32)
|
||||
vel = getattr(note, 'velocity', 100)
|
||||
vel = min(127, int(vel * art_vel_mult))
|
||||
if humanize > 0.0:
|
||||
vel_jitter = int(humanize * 15)
|
||||
vel = max(1, min(127, vel + _rnd.randint(-vel_jitter, vel_jitter)))
|
||||
vel_scale = vel / 127.0
|
||||
end = min(start + len(mixed), total_samples)
|
||||
buf[start:end] += mixed[:end - start] * volume * vel_scale
|
||||
if not getattr(note, '_hold', False):
|
||||
beat_pos += note.beats
|
||||
continue
|
||||
# Get pitches
|
||||
if hasattr(note.tone, 'tones'):
|
||||
pitches = [t.pitch(temperament=temperament, reference_pitch=reference_pitch) for t in note.tone.tones]
|
||||
@@ -4159,11 +4287,18 @@ def _render_notes_to_buf(notes, buf, samples_per_beat, total_samples,
|
||||
if noise_mix > 0:
|
||||
noise = numpy.random.uniform(-1, 1, n_samples).astype(numpy.float32)
|
||||
mixed = mixed * (1.0 - noise_mix * 0.5) + noise * noise_mix * 0.5
|
||||
# Amplitude envelope
|
||||
if a > 0 or d > 0 or s < 1.0 or r > 0:
|
||||
mixed = _apply_envelope(mixed, a, d, s, r)
|
||||
# Per-note velocity
|
||||
# Amplitude envelope (articulation may adjust attack)
|
||||
art_a = a * art_attack_mult
|
||||
if art_a > 0 or d > 0 or s < 1.0 or r > 0:
|
||||
mixed = _apply_envelope(mixed, art_a, d, s, r)
|
||||
# Staccato: apply a quick fade-out at the end
|
||||
if art == 'staccato':
|
||||
fade_len = min(int(SAMPLE_RATE * 0.01), len(mixed))
|
||||
if fade_len > 0:
|
||||
mixed[-fade_len:] *= numpy.linspace(1.0, 0.0, fade_len).astype(numpy.float32)
|
||||
# Per-note velocity (articulation may boost)
|
||||
vel = getattr(note, 'velocity', 100)
|
||||
vel = min(127, int(vel * art_vel_mult))
|
||||
if humanize > 0.0:
|
||||
vel_jitter = int(humanize * 15)
|
||||
vel = max(1, min(127, vel + _rnd.randint(-vel_jitter, vel_jitter)))
|
||||
@@ -4200,7 +4335,9 @@ def _render_notes_to_buf(notes, buf, samples_per_beat, total_samples,
|
||||
# Right channel gets up-detuned, left gets down-detuned
|
||||
stereo_buf[start:end, 1] += up_env * gain * spread_amt
|
||||
stereo_buf[start:end, 0] += down_env * gain * spread_amt
|
||||
beat_pos += note.beats
|
||||
# hold() notes don't advance the beat position
|
||||
if not getattr(note, '_hold', False):
|
||||
beat_pos += note.beats
|
||||
|
||||
|
||||
def _render_legato_to_buf(notes, buf, samples_per_beat, total_samples,
|
||||
@@ -4242,7 +4379,8 @@ def _render_legato_to_buf(notes, buf, samples_per_beat, total_samples,
|
||||
events.append((start, end, hz, vel))
|
||||
else:
|
||||
events.append((start, end, 0, vel)) # rest
|
||||
beat_pos += note.beats
|
||||
if not getattr(note, '_hold', False):
|
||||
beat_pos += note.beats
|
||||
|
||||
if not events:
|
||||
return
|
||||
@@ -4507,6 +4645,10 @@ def render_score(score):
|
||||
DrumSound.DJEMBE_BASS.value: 0.0,
|
||||
DrumSound.DJEMBE_TONE.value: 0.1,
|
||||
DrumSound.DJEMBE_SLAP.value: -0.1,
|
||||
# Doumbek
|
||||
DrumSound.DOUMBEK_DUM.value: 0.0,
|
||||
DrumSound.DOUMBEK_TEK.value: 0.1,
|
||||
DrumSound.DOUMBEK_KA.value: -0.1,
|
||||
# Cajon — centered (single instrument)
|
||||
DrumSound.CAJON_BASS.value: 0.0,
|
||||
DrumSound.CAJON_SLAP.value: 0.0,
|
||||
@@ -4562,6 +4704,35 @@ def render_score(score):
|
||||
part_stereo[fade_start:start, ch] *= fade
|
||||
_last_hit_start[sound_id] = start
|
||||
|
||||
# Cross-choke: a new hit on one sound dampens the ring of
|
||||
# related sounds on the same instrument (e.g. djembe slap
|
||||
# kills the bass resonance, closed hat kills open hat).
|
||||
_CHOKE_GROUPS = {
|
||||
# Djembe — any strike dampens the others
|
||||
DrumSound.DJEMBE_BASS.value: (DrumSound.DJEMBE_TONE.value, DrumSound.DJEMBE_SLAP.value),
|
||||
DrumSound.DJEMBE_TONE.value: (DrumSound.DJEMBE_BASS.value, DrumSound.DJEMBE_SLAP.value),
|
||||
DrumSound.DJEMBE_SLAP.value: (DrumSound.DJEMBE_BASS.value, DrumSound.DJEMBE_TONE.value),
|
||||
# Hi-hats — closed chokes open
|
||||
DrumSound.CLOSED_HAT.value: (DrumSound.OPEN_HAT.value,),
|
||||
DrumSound.PEDAL_HAT.value: (DrumSound.OPEN_HAT.value,),
|
||||
# Cajón — slap dampens bass ring
|
||||
DrumSound.CAJON_SLAP.value: (DrumSound.CAJON_BASS.value,),
|
||||
DrumSound.CAJON_TAP.value: (DrumSound.CAJON_BASS.value,),
|
||||
# Doumbek — tek/ka dampen dum
|
||||
DrumSound.DOUMBEK_TEK.value: (DrumSound.DOUMBEK_DUM.value,),
|
||||
DrumSound.DOUMBEK_KA.value: (DrumSound.DOUMBEK_DUM.value,),
|
||||
}
|
||||
choke_targets = _CHOKE_GROUPS.get(sound_id, ())
|
||||
for target_id in choke_targets:
|
||||
if target_id in _last_hit_start:
|
||||
prev_start = _last_hit_start[target_id]
|
||||
fade_len = min(int(SAMPLE_RATE * 0.004), max(0, start - prev_start))
|
||||
if fade_len > 0 and start > 0:
|
||||
fade = numpy.linspace(1.0, 0.0, fade_len).astype(numpy.float32)
|
||||
fade_start = max(0, start - fade_len)
|
||||
for ch in range(2):
|
||||
part_stereo[fade_start:start, ch] *= fade
|
||||
|
||||
remaining = total_samples - start
|
||||
hit_len = min(int(SAMPLE_RATE * 0.5), remaining)
|
||||
wave = _render_drum_hit(hit.sound.value, hit_len)
|
||||
|
||||
+711
-16
@@ -213,6 +213,11 @@ INSTRUMENTS = {
|
||||
"synth": "steel_drum_synth", "envelope": "none",
|
||||
"reverb": 0.3, "reverb_type": "plate",
|
||||
},
|
||||
"harmonium": {
|
||||
"synth": "harmonium_synth", "envelope": "organ",
|
||||
"reverb": 0.2, "reverb_type": "taj_mahal",
|
||||
"humanize": 0.15,
|
||||
},
|
||||
"accordion": {
|
||||
"synth": "accordion_synth", "envelope": "organ",
|
||||
"humanize": 0.15,
|
||||
@@ -383,6 +388,24 @@ class Duration(Enum):
|
||||
DOTTED_QUARTER = 1.5
|
||||
TRIPLET_QUARTER = 2 / 3
|
||||
|
||||
# Arithmetic — lets you write ``Duration.WHOLE * 2`` → 8.0 beats.
|
||||
def __mul__(self, other):
|
||||
return self.value * other
|
||||
|
||||
def __rmul__(self, other):
|
||||
return self.value * other
|
||||
|
||||
def __truediv__(self, other):
|
||||
return self.value / other
|
||||
|
||||
def __add__(self, other):
|
||||
if isinstance(other, Duration):
|
||||
return self.value + other.value
|
||||
return self.value + other
|
||||
|
||||
def __radd__(self, other):
|
||||
return other + self.value
|
||||
|
||||
|
||||
class TimeSignature:
|
||||
"""A musical time signature like 4/4 or 6/8."""
|
||||
@@ -426,6 +449,8 @@ 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
|
||||
def beats(self) -> float:
|
||||
@@ -527,6 +552,10 @@ class DrumSound(Enum):
|
||||
DJEMBE_BASS = 102 # open bass (center of head)
|
||||
DJEMBE_TONE = 103 # open tone (edge, fingers together)
|
||||
DJEMBE_SLAP = 104 # slap (edge, fingers spread, sharp crack)
|
||||
# Doumbek (darbuka) sounds
|
||||
DOUMBEK_DUM = 112 # center of head, deep bass
|
||||
DOUMBEK_TEK = 113 # edge of head, sharp high
|
||||
DOUMBEK_KA = 114 # muted edge slap
|
||||
# Cajon sounds
|
||||
CAJON_BASS = 108 # center of face, deep thump
|
||||
CAJON_SLAP = 109 # top edge, snare wires buzz
|
||||
@@ -537,6 +566,17 @@ class DrumSound(Enum):
|
||||
METAL_HAT = 107 # tight, short, precise
|
||||
|
||||
|
||||
class _DrumTone:
|
||||
"""Wrapper so a DrumSound can be placed in a Part's note list."""
|
||||
__slots__ = ('sound',)
|
||||
|
||||
def __init__(self, sound: DrumSound):
|
||||
self.sound = sound
|
||||
|
||||
def pitch(self, **kwargs):
|
||||
return -self.sound.value
|
||||
|
||||
|
||||
class _Hit:
|
||||
"""A single drum hit at a specific position in a pattern."""
|
||||
__slots__ = ("sound", "position", "velocity")
|
||||
@@ -1562,6 +1602,61 @@ Pattern._PRESETS["tabla solo"] = dict(
|
||||
],
|
||||
)
|
||||
|
||||
# ── Doumbek patterns ──────────────────────────────────────────────────────
|
||||
DKD = DrumSound.DOUMBEK_DUM
|
||||
DKT = DrumSound.DOUMBEK_TEK
|
||||
DKK = DrumSound.DOUMBEK_KA
|
||||
|
||||
# Maqsoum — the most common Arabic rhythm (4/4)
|
||||
Pattern._PRESETS["maqsoum"] = dict(
|
||||
name="maqsoum",
|
||||
time_signature="4/4",
|
||||
beats=4.0,
|
||||
hits=[
|
||||
_h(DKD, 0.0, 85), _h(DKT, 0.5, 65),
|
||||
_h(DKT, 1.0, 68), _h(DKD, 1.5, 80),
|
||||
_h(DKT, 2.0, 65), _h(DKT, 2.5, 62),
|
||||
_h(DKT, 3.0, 68), _h(DKT, 3.5, 62),
|
||||
],
|
||||
)
|
||||
|
||||
# Baladi — heavy, earthy, belly dance
|
||||
Pattern._PRESETS["baladi"] = dict(
|
||||
name="baladi",
|
||||
time_signature="4/4",
|
||||
beats=4.0,
|
||||
hits=[
|
||||
_h(DKD, 0.0, 88), _h(DKD, 0.5, 78),
|
||||
_h(DKT, 1.0, 70), _h(DKD, 1.5, 82),
|
||||
_h(DKT, 2.0, 68), _h(DKT, 2.5, 62),
|
||||
_h(DKT, 3.0, 68), _h(DKK, 3.25, 45), _h(DKT, 3.5, 65),
|
||||
],
|
||||
)
|
||||
|
||||
# Saidi — Upper Egyptian, strong and driving
|
||||
Pattern._PRESETS["saidi"] = dict(
|
||||
name="saidi",
|
||||
time_signature="4/4",
|
||||
beats=4.0,
|
||||
hits=[
|
||||
_h(DKD, 0.0, 88), _h(DKT, 0.5, 65),
|
||||
_h(DKD, 1.0, 82), _h(DKD, 1.5, 78),
|
||||
_h(DKT, 2.0, 70), _h(DKT, 2.5, 62),
|
||||
_h(DKT, 3.0, 68), _h(DKT, 3.5, 62),
|
||||
],
|
||||
)
|
||||
|
||||
# Ayoub — simple 2/4, trance-like repetition
|
||||
Pattern._PRESETS["ayoub"] = dict(
|
||||
name="ayoub",
|
||||
time_signature="2/4",
|
||||
beats=2.0,
|
||||
hits=[
|
||||
_h(DKD, 0.0, 85), _h(DKK, 0.5, 45),
|
||||
_h(DKT, 1.0, 70), _h(DKT, 1.5, 62),
|
||||
],
|
||||
)
|
||||
|
||||
# ── Cajón patterns ────────────────────────────────────────────────────────
|
||||
CB = DrumSound.CAJON_BASS
|
||||
CSL = DrumSound.CAJON_SLAP
|
||||
@@ -1824,6 +1919,74 @@ Pattern._PRESETS["soli"] = dict(
|
||||
],
|
||||
)
|
||||
|
||||
# Dununba — heavy bass-driven rhythm (accompaniment djembe part)
|
||||
Pattern._PRESETS["dununba"] = dict(
|
||||
name="dununba",
|
||||
time_signature="4/4",
|
||||
beats=4.0,
|
||||
hits=[
|
||||
_h(JB, 0.0, 110), _h(JB, 0.5, 95),
|
||||
_h(JT, 1.0, 75), _h(JB, 1.5, 100),
|
||||
_h(JB, 2.0, 108), _h(JT, 2.5, 70),
|
||||
_h(JB, 3.0, 105), _h(JB, 3.5, 90), _h(JT, 3.75, 65),
|
||||
],
|
||||
)
|
||||
|
||||
# Tiriba — joyful Susu rhythm from Guinea
|
||||
Pattern._PRESETS["tiriba"] = dict(
|
||||
name="tiriba",
|
||||
time_signature="4/4",
|
||||
beats=4.0,
|
||||
hits=[
|
||||
_h(JT, 0.0, 85), _h(JS, 0.25, 95), _h(JT, 0.5, 80),
|
||||
_h(JB, 1.0, 100), _h(JT, 1.5, 75),
|
||||
_h(JS, 2.0, 92), _h(JT, 2.25, 78), _h(JT, 2.5, 80),
|
||||
_h(JB, 3.0, 105), _h(JS, 3.5, 88), _h(JT, 3.75, 72),
|
||||
],
|
||||
)
|
||||
|
||||
# Yankadi — gentle greeting/welcome rhythm from Guinea
|
||||
Pattern._PRESETS["yankadi"] = dict(
|
||||
name="yankadi",
|
||||
time_signature="4/4",
|
||||
beats=4.0,
|
||||
hits=[
|
||||
_h(JB, 0.0, 90), _h(JT, 0.5, 70),
|
||||
_h(JT, 1.0, 72), _h(JS, 1.5, 85),
|
||||
_h(JB, 2.0, 88), _h(JT, 2.5, 68),
|
||||
_h(JS, 3.0, 82), _h(JT, 3.5, 65),
|
||||
],
|
||||
)
|
||||
|
||||
# Djansa — fast Malinke dance rhythm
|
||||
Pattern._PRESETS["djansa"] = dict(
|
||||
name="djansa",
|
||||
time_signature="4/4",
|
||||
beats=4.0,
|
||||
hits=[
|
||||
_h(JS, 0.0, 100), _h(JT, 0.25, 72), _h(JT, 0.5, 70),
|
||||
_h(JB, 0.75, 95),
|
||||
_h(JS, 1.0, 98), _h(JT, 1.25, 68), _h(JB, 1.5, 92),
|
||||
_h(JS, 2.0, 102), _h(JT, 2.25, 75), _h(JT, 2.5, 72),
|
||||
_h(JB, 2.75, 90),
|
||||
_h(JS, 3.0, 105), _h(JT, 3.25, 70), _h(JB, 3.5, 95),
|
||||
_h(JS, 3.75, 88),
|
||||
],
|
||||
)
|
||||
|
||||
# Mendiani — women's dance rhythm, celebratory
|
||||
Pattern._PRESETS["mendiani"] = dict(
|
||||
name="mendiani",
|
||||
time_signature="4/4",
|
||||
beats=4.0,
|
||||
hits=[
|
||||
_h(JB, 0.0, 100), _h(JT, 0.25, 65), _h(JS, 0.5, 90),
|
||||
_h(JT, 1.0, 70), _h(JB, 1.5, 95), _h(JT, 1.75, 68),
|
||||
_h(JS, 2.0, 92), _h(JT, 2.5, 72), _h(JS, 2.75, 85),
|
||||
_h(JB, 3.0, 105), _h(JT, 3.25, 65), _h(JS, 3.5, 95),
|
||||
],
|
||||
)
|
||||
|
||||
# ── Fill presets ──────────────────────────────────────────────────────────
|
||||
|
||||
Pattern._FILLS["rock"] = dict(
|
||||
@@ -2092,6 +2255,277 @@ Pattern._FILLS["second line"] = dict(
|
||||
],
|
||||
)
|
||||
|
||||
# ── Doumbek fills ────────────────────────────────────────────────────────
|
||||
_DKD = DrumSound.DOUMBEK_DUM
|
||||
_DKT = DrumSound.DOUMBEK_TEK
|
||||
_DKK = DrumSound.DOUMBEK_KA
|
||||
|
||||
# Doumbek roll — rapid teks building to dum
|
||||
Pattern._FILLS["doumbek roll"] = dict(
|
||||
name="doumbek roll fill",
|
||||
time_signature="4/4",
|
||||
beats=4.0,
|
||||
hits=[
|
||||
*[_h(_DKT, i * 0.125, 40 + i * 4) for i in range(16)],
|
||||
_h(_DKD, 2.0, 100), _h(_DKT, 2.25, 65), _h(_DKT, 2.5, 68),
|
||||
_h(_DKD, 3.0, 110), _h(_DKD, 3.25, 105),
|
||||
_h(_DKD, 3.5, 115), _h(_DKT, 3.75, 80),
|
||||
],
|
||||
)
|
||||
|
||||
# Doumbek accent — syncopated dum-tek-ka pattern
|
||||
Pattern._FILLS["doumbek accent"] = dict(
|
||||
name="doumbek accent fill",
|
||||
time_signature="4/4",
|
||||
beats=4.0,
|
||||
hits=[
|
||||
_h(_DKD, 0.0, 95), _h(_DKT, 0.25, 65), _h(_DKK, 0.5, 50),
|
||||
_h(_DKT, 0.75, 68), _h(_DKD, 1.0, 90),
|
||||
_h(_DKT, 1.5, 72), _h(_DKK, 1.75, 52), _h(_DKD, 2.0, 100),
|
||||
_h(_DKT, 2.25, 68), _h(_DKT, 2.5, 70), _h(_DKT, 2.75, 72),
|
||||
_h(_DKD, 3.0, 110), _h(_DKD, 3.5, 115),
|
||||
],
|
||||
)
|
||||
|
||||
# ── Tabla fills ──────────────────────────────────────────────────────────
|
||||
_TNA = DrumSound.TABLA_NA
|
||||
_TDH = DrumSound.TABLA_DHA
|
||||
_TTT = DrumSound.TABLA_TIT
|
||||
_TKE = DrumSound.TABLA_KE
|
||||
_TGB = DrumSound.TABLA_GE_BEND
|
||||
_TGE = DrumSound.TABLA_GE
|
||||
_TTI = DrumSound.TABLA_TIN
|
||||
_T3 = 1.0 / 12.0
|
||||
|
||||
# Tihai — the classic 3x pattern landing on sam
|
||||
Pattern._FILLS["tihai"] = dict(
|
||||
name="tihai fill",
|
||||
time_signature="4/4",
|
||||
beats=4.0,
|
||||
hits=[
|
||||
_h(_TDH, 0.0, 105), _h(_TNA, 0.25, 72), _h(_TTT, 0.5, 48),
|
||||
_h(_TKE, 0.75, 52), _h(_TDH, 1.0, 100),
|
||||
_h(_TDH, 1.25, 110), _h(_TNA, 1.5, 78), _h(_TTT, 1.75, 52),
|
||||
_h(_TKE, 2.0, 55), _h(_TDH, 2.25, 105),
|
||||
_h(_TDH, 2.5, 118), _h(_TNA, 2.75, 82), _h(_TTT, 3.0, 58),
|
||||
_h(_TKE, 3.25, 60), _h(_TDH, 3.5, 127),
|
||||
],
|
||||
)
|
||||
|
||||
# Chakkardar — 32nd triplet cascade into slam
|
||||
Pattern._FILLS["chakkardar"] = dict(
|
||||
name="chakkardar fill",
|
||||
time_signature="4/4",
|
||||
beats=4.0,
|
||||
hits=[
|
||||
*[_h(_TTT, i * _T3, 32 + i * 3) for i in range(12)],
|
||||
_h(_TDH, 1.0, 115), _h(_TGB, 1.5, 108),
|
||||
*[_h(_TTT, 2.0 + i * _T3, 35 + i * 3) for i in range(12)],
|
||||
_h(_TDH, 3.0, 120), _h(_TDH, 3.25, 115),
|
||||
_h(_TGB, 3.5, 120), _h(_TDH, 3.75, 127),
|
||||
],
|
||||
)
|
||||
|
||||
# Tiri kita fill — rapid 16th note dayan burst
|
||||
Pattern._FILLS["tiri kita"] = dict(
|
||||
name="tiri kita fill",
|
||||
time_signature="4/4",
|
||||
beats=4.0,
|
||||
hits=[
|
||||
_h(_TTT, 0.0, 50), _h(_TTT, 0.125, 38), _h(_TKE, 0.25, 48),
|
||||
_h(_TNA, 0.5, 72), _h(_TTT, 0.75, 42),
|
||||
_h(_TDH, 1.0, 95), _h(_TTT, 1.25, 38), _h(_TTT, 1.5, 42),
|
||||
_h(_TKE, 1.75, 48), _h(_TNA, 2.0, 75),
|
||||
_h(_TTT, 2.25, 40), _h(_TTT, 2.5, 45), _h(_TKE, 2.75, 50),
|
||||
_h(_TDH, 3.0, 100), _h(_TNA, 3.25, 70),
|
||||
_h(_TDH, 3.5, 110), _h(_TGB, 3.75, 105),
|
||||
],
|
||||
)
|
||||
|
||||
# Bayan showcase — deep bass bends
|
||||
Pattern._FILLS["bayan"] = dict(
|
||||
name="bayan fill",
|
||||
time_signature="4/4",
|
||||
beats=4.0,
|
||||
hits=[
|
||||
_h(_TGB, 0.0, 100), _h(_TNA, 0.5, 65),
|
||||
_h(_TGE, 1.0, 85), _h(_TGB, 1.5, 105),
|
||||
_h(_TNA, 2.0, 70), _h(_TKE, 2.25, 48),
|
||||
_h(_TGB, 2.5, 110), _h(_TDH, 3.0, 115),
|
||||
_h(_TGB, 3.5, 120),
|
||||
],
|
||||
)
|
||||
|
||||
# Call and response — dayan speaks, bayan answers
|
||||
Pattern._FILLS["tabla call"] = dict(
|
||||
name="tabla call fill",
|
||||
time_signature="4/4",
|
||||
beats=4.0,
|
||||
hits=[
|
||||
_h(_TNA, 0.0, 105), _h(_TNA, 0.25, 55), _h(_TTT, 0.5, 38),
|
||||
_h(_TNA, 0.75, 100),
|
||||
_h(_TGE, 1.0, 95), _h(_TGE, 1.25, 48), _h(_TGB, 1.5, 90),
|
||||
_h(_TNA, 2.0, 108), _h(_TTT, 2.125, 30), _h(_TTT, 2.25, 35),
|
||||
_h(_TNA, 2.5, 100),
|
||||
_h(_TGB, 3.0, 112), _h(_TKE, 3.25, 48),
|
||||
_h(_TDH, 3.5, 120),
|
||||
],
|
||||
)
|
||||
|
||||
# ── Djembe fills ─────────────────────────────────────────────────────────
|
||||
|
||||
# Djembe call — bass-tone-slap conversation building to climax
|
||||
Pattern._FILLS["djembe call"] = dict(
|
||||
name="djembe call fill",
|
||||
time_signature="4/4",
|
||||
beats=4.0,
|
||||
hits=[
|
||||
_h(JB, 0.0, 100), _h(JT, 0.25, 70), _h(JT, 0.5, 72),
|
||||
_h(JS, 0.75, 90),
|
||||
_h(JB, 1.0, 95), _h(JT, 1.25, 68), _h(JS, 1.5, 88),
|
||||
_h(JT, 1.75, 75),
|
||||
_h(JS, 2.0, 100), _h(JS, 2.25, 95), _h(JT, 2.5, 78),
|
||||
_h(JB, 2.75, 105),
|
||||
_h(JS, 3.0, 110), _h(JT, 3.25, 80), _h(JS, 3.5, 112),
|
||||
_h(JB, 3.75, 120),
|
||||
],
|
||||
)
|
||||
|
||||
# Djembe roll — rapid slaps accelerating into bass
|
||||
Pattern._FILLS["djembe roll"] = dict(
|
||||
name="djembe roll fill",
|
||||
time_signature="4/4",
|
||||
beats=4.0,
|
||||
hits=[
|
||||
# Accelerating slap roll
|
||||
*[_h(JS, i * 0.125, 50 + i * 4) for i in range(16)],
|
||||
# Bass accents punching through
|
||||
_h(JB, 2.0, 105), _h(JB, 2.5, 108),
|
||||
_h(JB, 3.0, 112), _h(JT, 3.25, 85),
|
||||
_h(JB, 3.5, 115), _h(JS, 3.75, 100),
|
||||
],
|
||||
)
|
||||
|
||||
# Djembe break — syncopated West African-style break
|
||||
Pattern._FILLS["djembe break"] = dict(
|
||||
name="djembe break fill",
|
||||
time_signature="4/4",
|
||||
beats=4.0,
|
||||
hits=[
|
||||
_h(JB, 0.0, 105), _h(JT, 0.25, 65), _h(JS, 0.5, 90),
|
||||
_h(JT, 0.75, 70), _h(JB, 1.0, 100),
|
||||
_h(JS, 1.25, 85), _h(JS, 1.5, 88),
|
||||
_h(JB, 1.75, 95), _h(JT, 2.0, 72),
|
||||
_h(JS, 2.25, 92), _h(JB, 2.5, 108),
|
||||
_h(JT, 2.75, 68), _h(JS, 2.875, 55),
|
||||
_h(JB, 3.0, 115), _h(JS, 3.25, 100),
|
||||
_h(JB, 3.5, 118), _h(JB, 3.75, 120),
|
||||
],
|
||||
)
|
||||
|
||||
# ── Cajón fills ──────────────────────────────────────────────────────────
|
||||
|
||||
# Cajón flam run — slaps accelerating into bass hit
|
||||
Pattern._FILLS["cajon flam"] = dict(
|
||||
name="cajon flam fill",
|
||||
time_signature="4/4",
|
||||
beats=4.0,
|
||||
hits=[
|
||||
_h(CSL, 0.0, 95), _h(CT, 0.125, 45), _h(CSL, 0.25, 90),
|
||||
_h(CB, 0.5, 100), _h(CT, 0.75, 50),
|
||||
_h(CSL, 1.0, 88), _h(CT, 1.125, 42), _h(CSL, 1.25, 92),
|
||||
_h(CT, 1.5, 55), _h(CSL, 1.75, 85),
|
||||
_h(CB, 2.0, 105), _h(CSL, 2.25, 75), _h(CT, 2.5, 48),
|
||||
_h(CSL, 2.75, 80), _h(CT, 2.875, 40),
|
||||
_h(CB, 3.0, 110), _h(CSL, 3.25, 90), _h(CSL, 3.5, 95),
|
||||
_h(CB, 3.75, 120),
|
||||
],
|
||||
)
|
||||
|
||||
# Cajón rumble — fast taps building to slap accents
|
||||
Pattern._FILLS["cajon rumble"] = dict(
|
||||
name="cajon rumble fill",
|
||||
time_signature="4/4",
|
||||
beats=4.0,
|
||||
hits=[
|
||||
*[_h(CT, i * 0.125, 35 + i * 3) for i in range(16)],
|
||||
_h(CSL, 2.0, 95), _h(CSL, 2.5, 100),
|
||||
_h(CB, 3.0, 108), _h(CSL, 3.25, 88),
|
||||
_h(CB, 3.5, 112), _h(CSL, 3.75, 95),
|
||||
],
|
||||
)
|
||||
|
||||
# Cajón breakdown — syncopated bass-slap groove
|
||||
Pattern._FILLS["cajon breakdown"] = dict(
|
||||
name="cajon breakdown fill",
|
||||
time_signature="4/4",
|
||||
beats=4.0,
|
||||
hits=[
|
||||
_h(CB, 0.0, 100), _h(CT, 0.25, 45), _h(CSL, 0.5, 85),
|
||||
_h(CB, 1.0, 95), _h(CSL, 1.25, 78), _h(CT, 1.5, 50),
|
||||
_h(CSL, 1.75, 82),
|
||||
_h(CB, 2.0, 105), _h(CT, 2.125, 40), _h(CT, 2.25, 42),
|
||||
_h(CSL, 2.5, 90), _h(CT, 2.75, 48),
|
||||
_h(CB, 3.0, 115), _h(CSL, 3.25, 95),
|
||||
_h(CB, 3.5, 110), _h(CSL, 3.75, 100),
|
||||
],
|
||||
)
|
||||
|
||||
# ── Metal fills (using metal kit) ────────────────────────────────────────
|
||||
|
||||
# Metal triplet — double kick triplets with snare accents
|
||||
Pattern._FILLS["metal triplet"] = dict(
|
||||
name="metal triplet fill",
|
||||
time_signature="4/4",
|
||||
beats=4.0,
|
||||
hits=[
|
||||
# Triplet kick pattern (12 kicks across 4 beats = triplet 8ths)
|
||||
*[_h(MK, i * (1/3), 95 + (i % 3 == 0) * 15) for i in range(12)],
|
||||
# Snare accents on downbeats
|
||||
_h(MS, 0.0, 110), _h(MS, 1.0, 105),
|
||||
_h(MS, 2.0, 110), _h(MS, 3.0, 115),
|
||||
# Hat on upbeats
|
||||
_h(MH, 0.5, 60), _h(MH, 1.5, 60),
|
||||
_h(MH, 2.5, 65), _h(MH, 3.5, 70),
|
||||
],
|
||||
)
|
||||
|
||||
# Metal blastbeat variant — alternating snare/kick 32nds
|
||||
Pattern._FILLS["metal blast"] = dict(
|
||||
name="metal blast fill",
|
||||
time_signature="4/4",
|
||||
beats=4.0,
|
||||
hits=[
|
||||
# Alternating kick-snare at 32nd note speed for 2 beats
|
||||
*[_h(MK if i % 2 == 0 else MS, i * 0.125, 100 + i) for i in range(16)],
|
||||
# Then crash into half-time for 2 beats
|
||||
_h(MK, 2.0, 120), _h(MS, 2.5, 115),
|
||||
_h(MK, 3.0, 120), _h(MH, 3.25, 80),
|
||||
_h(MS, 3.5, 120),
|
||||
],
|
||||
)
|
||||
|
||||
# Metal cascade — descending snare/kick rolls
|
||||
Pattern._FILLS["metal cascade"] = dict(
|
||||
name="metal cascade fill",
|
||||
time_signature="4/4",
|
||||
beats=4.0,
|
||||
hits=[
|
||||
# Fast snare roll beat 1
|
||||
*[_h(MS, i * 0.125, 80 + i * 5) for i in range(8)],
|
||||
# Double kick beat 2
|
||||
*[_h(MK, 1.0 + i * 0.125, 90 + i * 3) for i in range(8)],
|
||||
# Alternating beat 3
|
||||
_h(MS, 2.0, 105), _h(MK, 2.125, 95),
|
||||
_h(MS, 2.25, 108), _h(MK, 2.375, 98),
|
||||
_h(MS, 2.5, 110), _h(MK, 2.625, 100),
|
||||
_h(MS, 2.75, 112), _h(MK, 2.875, 102),
|
||||
# Crash ending
|
||||
_h(MK, 3.0, 120), _h(MS, 3.0, 120),
|
||||
_h(MK, 3.5, 120), _h(MS, 3.5, 120),
|
||||
],
|
||||
)
|
||||
|
||||
|
||||
class Part:
|
||||
"""A named voice within a Score, with its own synth, envelope, and effects.
|
||||
@@ -2202,7 +2636,8 @@ class Part:
|
||||
self._automation: list[tuple[float, dict]] = [] # (beat, {param: value})
|
||||
|
||||
def add(self, tone_or_string, duration=Duration.QUARTER, *, velocity: int = 100,
|
||||
bend: float = 0.0, bend_type: str = "smooth", lyric: str = "") -> "Part":
|
||||
bend: float = 0.0, bend_type: str = "smooth", lyric: str = "",
|
||||
articulation: str = "") -> "Part":
|
||||
"""Add a note. Accepts Tone/Chord objects or note strings like ``"E5"``.
|
||||
|
||||
Duration can be a ``Duration`` enum or a raw float (beats).
|
||||
@@ -2210,6 +2645,10 @@ class Part:
|
||||
Bend specifies a pitch bend in semitones over the note duration
|
||||
(e.g. ``bend=2`` bends up a whole step, ``bend=-1`` bends down
|
||||
a half step). Used for guitar bends, sitar meends, slides.
|
||||
Articulation changes how the note is played: ``"staccato"`` (short,
|
||||
~40% duration), ``"legato"`` (overlaps next note), ``"marcato"``
|
||||
(heavy accent), ``"tenuto"`` (full duration, soft attack),
|
||||
``"accent"`` (velocity bump), ``"fermata"`` (held ~50% longer).
|
||||
|
||||
Returns self for chaining.
|
||||
"""
|
||||
@@ -2220,9 +2659,199 @@ 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 = "",
|
||||
articulation: str = "") -> "Part":
|
||||
"""Add a note without advancing the beat position.
|
||||
|
||||
The note plays at the current position but the next note
|
||||
starts at the *same* time — enabling polyphonic overlap
|
||||
on a single part.
|
||||
|
||||
Use this for: piano sustain pedal (bass note rings while
|
||||
melody plays above), guitar strumming with individual
|
||||
string timing, held drone notes under a melody.
|
||||
|
||||
Example::
|
||||
|
||||
>>> piano = score.part("piano", instrument="piano")
|
||||
>>> piano.hold("C3", Duration.WHOLE) # bass rings for 4 beats
|
||||
>>> piano.add("E4", Duration.HALF) # starts at same time as C3
|
||||
>>> piano.add("G4", Duration.HALF) # starts at beat 2
|
||||
"""
|
||||
if isinstance(tone_or_string, str):
|
||||
from .tones import Tone
|
||||
tone_or_string = Tone.from_string(tone_or_string, system=self._system)
|
||||
if isinstance(duration, (int, float)):
|
||||
duration = _RawDuration(duration)
|
||||
self.notes.append(Note(tone=tone_or_string, duration=duration,
|
||||
velocity=velocity, bend=bend,
|
||||
bend_type=bend_type, lyric=lyric,
|
||||
articulation=articulation, _hold=True))
|
||||
return self
|
||||
|
||||
def hit(self, sound, duration=Duration.EIGHTH, *, velocity: int = 100,
|
||||
articulation: str = "") -> "Part":
|
||||
"""Add a drum hit to this part.
|
||||
|
||||
Places a drum sound into the note stream so it goes through the
|
||||
normal renderer — meaning articulations, humanize, and effects
|
||||
all work on individual hits.
|
||||
|
||||
Args:
|
||||
sound: A :class:`DrumSound` enum member (e.g. ``DrumSound.KICK``).
|
||||
duration: How long the hit occupies in the timeline (default 8th note).
|
||||
velocity: Hit loudness 1-127.
|
||||
articulation: ``"accent"``, ``"staccato"``, ``"marcato"``, etc.
|
||||
|
||||
Example::
|
||||
|
||||
>>> drums = score.part("kit", synth="sine")
|
||||
>>> drums.hit(DrumSound.KICK, Duration.QUARTER, articulation="accent")
|
||||
>>> drums.hit(DrumSound.CLOSED_HAT, Duration.EIGHTH)
|
||||
"""
|
||||
if isinstance(duration, (int, float)):
|
||||
duration = _RawDuration(duration)
|
||||
self.notes.append(Note(tone=_DrumTone(sound), duration=duration,
|
||||
velocity=velocity, articulation=articulation))
|
||||
return self
|
||||
|
||||
def crescendo(self, notes, duration=Duration.QUARTER, *,
|
||||
start_vel: int = 40, end_vel: int = 110,
|
||||
articulation: str = "") -> "Part":
|
||||
"""Add notes with velocity ramping up (getting louder).
|
||||
|
||||
Args:
|
||||
notes: List of note strings (e.g. ``["C4", "D4", "E4"]``).
|
||||
duration: Duration for each note.
|
||||
start_vel: Starting velocity (quiet).
|
||||
end_vel: Ending velocity (loud).
|
||||
articulation: Optional articulation for all notes.
|
||||
|
||||
Example::
|
||||
|
||||
>>> piano.crescendo(["C4","D4","E4","F4","G4"], Duration.QUARTER,
|
||||
... start_vel=40, end_vel=110)
|
||||
"""
|
||||
return self.dynamics(notes, duration, velocities=(start_vel, end_vel),
|
||||
articulation=articulation)
|
||||
|
||||
def decrescendo(self, notes, duration=Duration.QUARTER, *,
|
||||
start_vel: int = 110, end_vel: int = 40,
|
||||
articulation: str = "") -> "Part":
|
||||
"""Add notes with velocity ramping down (getting quieter).
|
||||
|
||||
Args:
|
||||
notes: List of note strings.
|
||||
duration: Duration for each note.
|
||||
start_vel: Starting velocity (loud).
|
||||
end_vel: Ending velocity (quiet).
|
||||
articulation: Optional articulation for all notes.
|
||||
|
||||
Example::
|
||||
|
||||
>>> piano.decrescendo(["G4","F4","E4","D4","C4"], Duration.QUARTER,
|
||||
... start_vel=110, end_vel=40)
|
||||
"""
|
||||
return self.dynamics(notes, duration, velocities=(start_vel, end_vel),
|
||||
articulation=articulation)
|
||||
|
||||
def dynamics(self, notes, duration=Duration.QUARTER, *,
|
||||
velocities=None, articulation: str = "") -> "Part":
|
||||
"""Add notes with a velocity curve.
|
||||
|
||||
Args:
|
||||
notes: List of note strings or Tone/Chord objects.
|
||||
duration: Duration for each note (or list of durations).
|
||||
velocities: Velocity curve — either a ``(start, end)`` tuple
|
||||
for a linear ramp, or a list of ints (one per note).
|
||||
articulation: Optional articulation for all notes (or list).
|
||||
|
||||
Example::
|
||||
|
||||
>>> # Linear ramp
|
||||
>>> piano.dynamics(["C4","E4","G4","C5"], Duration.QUARTER,
|
||||
... velocities=(50, 120))
|
||||
>>> # Custom curve (swell and fade)
|
||||
>>> piano.dynamics(["C4","D4","E4","F4","G4","F4","E4","D4"],
|
||||
... Duration.EIGHTH,
|
||||
... velocities=[50, 70, 90, 110, 110, 90, 70, 50])
|
||||
"""
|
||||
n = len(notes)
|
||||
if n == 0:
|
||||
return self
|
||||
|
||||
# Resolve velocities
|
||||
if velocities is None:
|
||||
vels = [100] * n
|
||||
elif isinstance(velocities, (tuple, list)) and len(velocities) == 2 and isinstance(velocities[0], (int, float)):
|
||||
# (start, end) tuple — linear ramp
|
||||
start_v, end_v = velocities
|
||||
if n == 1:
|
||||
vels = [int(start_v)]
|
||||
else:
|
||||
vels = [int(start_v + (end_v - start_v) * i / (n - 1))
|
||||
for i in range(n)]
|
||||
else:
|
||||
vels = list(velocities)
|
||||
|
||||
# Resolve durations
|
||||
if isinstance(duration, (list, tuple)):
|
||||
durs = list(duration)
|
||||
else:
|
||||
durs = [duration] * n
|
||||
|
||||
# Resolve articulations
|
||||
if isinstance(articulation, (list, tuple)):
|
||||
arts = list(articulation)
|
||||
else:
|
||||
arts = [articulation] * n
|
||||
|
||||
for note, vel, dur, art in zip(notes, vels, durs, arts):
|
||||
vel = max(1, min(127, vel))
|
||||
self.add(note, dur, velocity=vel, articulation=art)
|
||||
|
||||
return self
|
||||
|
||||
def swell(self, notes, duration=Duration.QUARTER, *,
|
||||
low_vel: int = 40, peak_vel: int = 110,
|
||||
articulation: str = "") -> "Part":
|
||||
"""Add notes that swell up then fade back down (< > shape).
|
||||
|
||||
The velocity ramps up to the midpoint then back down,
|
||||
creating the classic orchestral swell.
|
||||
|
||||
Args:
|
||||
notes: List of note strings.
|
||||
duration: Duration for each note.
|
||||
low_vel: Velocity at start and end.
|
||||
peak_vel: Velocity at the peak (midpoint).
|
||||
articulation: Optional articulation.
|
||||
|
||||
Example::
|
||||
|
||||
>>> strings.swell(["C4","D4","E4","F4","G4","F4","E4","D4"],
|
||||
... Duration.QUARTER, low_vel=40, peak_vel=110)
|
||||
"""
|
||||
n = len(notes)
|
||||
if n <= 2:
|
||||
return self.dynamics(notes, duration, velocities=[peak_vel] * n,
|
||||
articulation=articulation)
|
||||
mid = n // 2
|
||||
vels = []
|
||||
for i in range(n):
|
||||
if i <= mid:
|
||||
v = low_vel + (peak_vel - low_vel) * i / mid
|
||||
else:
|
||||
v = peak_vel - (peak_vel - low_vel) * (i - mid) / (n - 1 - mid)
|
||||
vels.append(int(v))
|
||||
return self.dynamics(notes, duration, velocities=vels,
|
||||
articulation=articulation)
|
||||
|
||||
def set(self, **params) -> "Part":
|
||||
"""Change effect parameters at the current beat position.
|
||||
|
||||
@@ -2306,6 +2935,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":
|
||||
@@ -2573,21 +3275,14 @@ class Part:
|
||||
from .chords import Chord as ChordClass
|
||||
chord_obj = ChordClass(tones=strum_tones)
|
||||
|
||||
# Strum sweep: quick individual string hits before the chord.
|
||||
# Only the first 2-3 strings get a tiny grace note, the rest
|
||||
# ring together as the full chord. Gives the strum feel without
|
||||
# sounding like separate plucks.
|
||||
# Strum: hold a quiet leading string simultaneously with the
|
||||
# full chord using hold(). No timing gap — both start at the
|
||||
# same beat position. The leading string adds strum texture.
|
||||
n_strings = len(strum_tones)
|
||||
if strum_time > 0.02 and n_strings >= 3:
|
||||
n_grace = min(2, n_strings - 1)
|
||||
per_grace = strum_time / n_grace
|
||||
grace_vel = max(1, int(velocity * 0.25))
|
||||
for i in range(n_grace):
|
||||
self.add(strum_tones[i], per_grace, velocity=grace_vel)
|
||||
ring = max(0.1, total_beats - strum_time)
|
||||
self.add(chord_obj, ring, velocity=velocity)
|
||||
else:
|
||||
self.add(chord_obj, total_beats, velocity=velocity)
|
||||
if strum_time > 0 and n_strings >= 3:
|
||||
grace_vel = max(1, int(velocity * 0.15))
|
||||
self.hold(strum_tones[0], total_beats, velocity=grace_vel)
|
||||
self.add(chord_obj, total_beats, velocity=velocity)
|
||||
|
||||
return self
|
||||
|
||||
|
||||
+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)
|
||||
|
||||
+15
-2
@@ -4869,6 +4869,19 @@ def test_duration_values():
|
||||
assert abs(Duration.TRIPLET_QUARTER.value - 2 / 3) < 1e-9
|
||||
|
||||
|
||||
def test_duration_arithmetic():
|
||||
# Multiplication
|
||||
assert Duration.WHOLE * 2 == 8.0
|
||||
assert 2 * Duration.HALF == 4.0
|
||||
assert Duration.QUARTER * 3 == 3.0
|
||||
# Division
|
||||
assert Duration.WHOLE / 2 == 2.0
|
||||
# Addition
|
||||
assert Duration.HALF + Duration.QUARTER == 3.0
|
||||
assert Duration.HALF + 1.0 == 3.0
|
||||
assert 1.0 + Duration.HALF == 3.0
|
||||
|
||||
|
||||
def test_time_signature_from_string_4_4():
|
||||
ts = TimeSignature.from_string("4/4")
|
||||
assert ts.beats == 4
|
||||
@@ -5320,7 +5333,7 @@ def test_supersaw_wave():
|
||||
@needs_portaudio
|
||||
def test_all_synths_in_enum():
|
||||
from pytheory.play import Synth
|
||||
assert len(Synth) == 41
|
||||
assert len(Synth) == 42
|
||||
for s in Synth:
|
||||
wave = s(440, n_samples=1000)
|
||||
assert len(wave) == 1000
|
||||
@@ -7142,7 +7155,7 @@ def test_score_system_propagates():
|
||||
|
||||
def test_synth_enum_count():
|
||||
from pytheory.play import Synth
|
||||
assert len(Synth) == 41
|
||||
assert len(Synth) == 42
|
||||
|
||||
|
||||
def test_all_synths_render_and_enum_match():
|
||||
|
||||
@@ -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.2"
|
||||
version = "0.39.0"
|
||||
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