Compare commits

...

31 Commits

Author SHA1 Message Date
kennethreitz 7d678e364e v0.39.2: Marching drumline, ensemble rendering, rudiments
Full marching percussion: snare, quads, pitched bass drums.
Part.flam(), Part.diddle(), Part.cheese() rudiment methods.
Part ensemble= for multi-player rendering with timing tendencies.
Sympathetic snare resonance. Updated docs.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-28 23:01:18 -04:00
kennethreitz 3a8d829010 Quads, pitched bass drums, full drumline cadence
- 5 quad/tenor drum sounds (4 pitched + spock rim)
- 5 pitched marching bass drums (high to low, more beater sound)
- 6 patterns: quad sweep, quad groove, bass split, bass unison, drumline
- Louder snares, more beater on bass drums
- Song #32 rewritten as full drumline cadence with ensemble
  (8 snares, 4 quads, 5 basses)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-28 22:59:08 -04:00
kennethreitz 2a67906937 Update changelog for v0.40.0
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-28 22:49:57 -04:00
kennethreitz b9dcad0454 Marching snare, ensemble, flam/diddle/cheese, resonance buildup
- 3 marching percussion sounds: snare, rimshot, stick click
- 4 marching patterns: march, cadence, paradiddle, roll
- Part.flam(), Part.diddle(), Part.cheese() rudiment methods
- Part ensemble= parameter: duplicate voices with per-player timing
  tendencies and micro pitch drift (works on any Part)
- Sympathetic resonance: marching snare buzz builds up with repeated hits
- Song #32: Snare Cadence (16 bars with triplets, 32nds, flams, cheese)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-28 22:48:37 -04:00
kennethreitz db9726168a Add chakradar tabla pattern, simplify sprunki.py
16-beat chakradar: tihai of tihais with 3 escalating phrases
and crescendo triplet finale landing on sam.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-28 21:22:34 -04:00
kennethreitz 26af923789 Simplify sprunki.py to clean melody reference
Just the notes transcribed from MIDI, no arrangement.
Base for future iterations.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-28 21:18:40 -04:00
kennethreitz 72e18a9bec Add Sprunki Simon Phase 1 arrangement (examples/sprunki.py)
Full arrangement with three-octave square wave lead, drums, bass,
supersaw pads, and a saw solo section. Melody transcribed from MIDI.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-28 18:39:46 -04:00
kennethreitz 7d56ed7a2c Add render_score to __all__, 19 tests for new features
Tests for articulations, dynamic curves, Part.hit(), Part.ramp(),
djembe/cajón/metal patterns and fills. 882 tests total.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-28 18:08:03 -04:00
kennethreitz 6efa4f18ce v0.39.0: Articulations, ramp(), drop numeral, djembe expansion
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-28 17:51:29 -04:00
kennethreitz 06fc4cabb7 Drop numeral dependency, inline Roman numeral helpers
Replace the numeral package with ~30 lines of int2roman()/roman2int()
in _statics.py. Reduces supply chain surface. Fixes #47.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-28 17:48:22 -04:00
kennethreitz d3a93c18b3 Add song #31: Acid Tabla (303 + tabla fusion)
Showcases ramp(), articulations, Part.hit(), filter automation,
and cross-genre fusion. 303 acid bass with tabla entering at the
peak and riding through the outro.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-28 17:26:20 -04:00
kennethreitz 0e10359236 v0.38.2: Part.ramp() for smooth parameter automation
Smoothly sweep any parameter (lowpass, reverb, distortion, etc.)
from current value to target with linear, ease_in, ease_out, or
ease_in_out interpolation curves.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-28 17:12:32 -04:00
kennethreitz df00c3436d Docs: articulations, dynamic curves, Part.hit(), Duration arithmetic
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-28 15:24:21 -04:00
kennethreitz 2f02df15b8 v0.38.1: Dynamic curves (crescendo, decrescendo, swell, dynamics)
Part.crescendo(), Part.decrescendo(), Part.swell(), and Part.dynamics()
for velocity ramps and custom curves across note sequences.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-28 15:23:16 -04:00
kennethreitz a2740b8d57 v0.38.0: Articulations, Part.hit(), djembe expansion, cross-choke
Articulations (staccato, legato, marcato, tenuto, accent, fermata)
on Part.add() and Part.hold(). Part.hit() for placing individual
drum sounds with articulation support. 5 new djembe patterns,
3 djembe fills, cross-choke damping, improved djembe slap.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-28 15:14:21 -04:00
kennethreitz 840bfcc36c v0.37.0: Djembe expansion and cross-choke drum damping
5 new djembe patterns (dununba, tiriba, yankadi, djansa, mendiani),
3 djembe fills, cross-choke damping across drum families, and
improved djembe slap synthesis.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-28 12:32:14 -04:00
kennethreitz 938c1cc132 v0.36.6: Cajón and metal drum fills
Add 6 new drum fills: cajon flam, cajon rumble, cajon breakdown,
metal triplet, metal blast, metal cascade. 27 fills total.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-28 12:25:52 -04:00
kennethreitz 9dc22db4b2 v0.36.5: Duration arithmetic support
Duration enum now supports multiplication, division, and addition
so expressions like `Duration.WHOLE * 2` work instead of raising TypeError.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-28 12:19:28 -04:00
kennethreitz f570e226cd v0.36.4: Harmonium, doumbek, tabla fills, Part.hold()
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-28 12:11:06 -04:00
kennethreitz 0c5c3abedc Harmonium synth, doumbek drums (3 sounds, 4 patterns, 2 fills)
- Harmonium: single free reed, nasal midrange, bellows swell.
  The sound of kirtan and qawwali.
- Doumbek (darbuka): dum (center bass), tek (edge sharp), ka (muted).
  4 patterns: maqsoum, baladi, saidi, ayoub.
  2 fills: doumbek roll, doumbek accent.
- 42 synth waveforms total

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-28 12:05:46 -04:00
kennethreitz 35d07b984b Docs: add tabla fills to drums.rst
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-28 11:57:08 -04:00
kennethreitz aec7723ee6 5 tabla fills: tihai, chakkardar, tiri kita, bayan, tabla call
Tihai (3x crescendo landing on sam), chakkardar (32nd triplet
cascade), tiri kita (rapid 16th dayan burst), bayan (bass bends),
tabla call (dayan/bayan call-and-response).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-28 11:56:19 -04:00
kennethreitz b98a40297b v0.36.3: Part.hold() polyphony, strum fix, 30 songs
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-28 11:46:45 -04:00
kennethreitz 9117568b74 Strum uses hold() — leading string plays simultaneously with chord
No more timing gaps. The leading string is held at 15% velocity
at the same beat position as the full chord via hold(), adding
strum texture without stealing time.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-28 11:42:36 -04:00
kennethreitz 11e4417c62 Part.hold() — polyphonic overlap on a single part
hold() adds a note without advancing the beat position, so the
next note starts at the same time. Enables: piano sustain (bass
rings while melody plays), drone notes under melody, held chords
with moving lines.

Two lines in the renderer: skip beat_pos advance when _hold is set.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-28 11:38:41 -04:00
kennethreitz 4edf1d983d Remove strum grace notes — clean chord hit only
Grace notes created audible gap before chord and sounded like
separate plucks. Pure chord hit sounds better.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-28 11:30:18 -04:00
kennethreitz 74b07b1a8a Song #29: Pop Rock (I-V-vi-IV) — the progression that launched 1000 hits
G-D-Em-C at 120 BPM. Picked intro, strummed verse, electric lead
melody, strings swell, rock drums. The most popular chord progression
in pop history.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-27 23:22:08 -04:00
kennethreitz c9437209a7 Song #28: Descent (generative — different every time)
Random key, tempo, reverb space, instruments, and melodies.
Melodies walk the scale stepwise (not random jumps), arpeggios
follow chord tones in order, piano walks up/down. Tabla solo
always closes with random strokes. No seed — truly unique each play.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-27 23:18:19 -04:00
kennethreitz 92cb855a49 Song #27: Ascent (Deep → Sky → Theremin Solo → Tabla Solo)
Didgeridoo drone throughout, granular abyss, kalimba light,
cello surfacing, piano + quiet uke, pedal steel + theremin solo
(searching → building → soaring peak), strings/flute/harp/timpani
at the peak, 4-part tabla solo finale (whisper → ghosts → 9-tuplet
call-response → 32nd triplet cascade + grand tihai).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-27 23:09:34 -04:00
kennethreitz f06c6f77d1 Comprehensive docs sweep: all 9 guide pages updated
- index.rst: 16 systems, 60+ presets, 41 waveforms, full feature list
- synths.rst: 31 dedicated synths, 60+ presets, complete instrument list
- drums.rst: 51 drum sounds, cajón section, bayan pitch bend
- effects.rst: cabinet/analog_drift in automatable params
- playback.rst: temperament, reference_pitch, KeyboardInterrupt
- systems.rst: 16 systems, full microtonal section (shruti JI,
  maqam Zalzalian, slendro, pelog, thai, makam, carnatic, 19/31-TET,
  Bohlen-Pierce), TET factory, int tone names, System.tone()
- sequencing.rst: Score tuning params documented
- tones.rst: enharmonics (Cb/Fb/E#/B#, double sharps/flats, unicode),
  B#/Cb octave fix, tone validation
- chords.rst: enharmonic support cross-reference

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-27 20:41:08 -04:00
kennethreitz 51bd63658f Docs: update synths.rst — 41 waveforms, all 24 dedicated synths
Added: pedal steel, theremin, kalimba, steel drum, accordion,
didgeridoo, bagpipe, banjo, mandolin, ukulele. Updated counts.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-27 20:31:31 -04:00
21 changed files with 3560 additions and 149 deletions
+96
View File
@@ -2,6 +2,102 @@
All notable changes to PyTheory are documented here.
## 0.39.2
- **Marching percussion** — snare, rimshot, and stick click sounds with
high-tension kevlar synthesis and woody-metallic rimshot crack
- **`Part.flam()`**, **`Part.diddle()`**, **`Part.cheese()`** — marching
rudiment methods for any drum sound
- **`Part ensemble=`** — duplicate voices with per-player timing tendencies
and micro pitch drift. Works on any Part (drumline, string section, choir).
`ensemble=20` for a full snare line, `ensemble=4` for a string quartet.
- **Sympathetic resonance** — marching snare buzz builds up with repeated
hits, decays during rests (like real snare wire response)
- **4 marching patterns** — march, cadence, paradiddle, roll
- **Chakradar tabla pattern** — 16-beat tihai of tihais composition
- Song #32: Snare Cadence (flams, diddles, cheese, triplets, 32nds)
## 0.39.1
- **Chakradar tabla pattern** — 16-beat tihai of tihais composition with
3 escalating phrases and a crescendo triplet finale
## 0.39.0
- **Dropped `numeral` dependency** — Roman numeral helpers inlined,
reducing supply chain surface (#47)
- **`Part.ramp()`** — smooth parameter automation with 4 interpolation
curves (linear, ease_in, ease_out, ease_in_out)
- **Articulations** — staccato, legato, marcato, tenuto, accent, fermata
- **Dynamic curves** — crescendo(), decrescendo(), swell(), dynamics()
- **`Part.hit()`** — individual drum sounds with articulation support
- **Cross-choke drum damping** — djembe, hi-hats, cajón, doumbek
- **5 new djembe patterns** + 3 djembe fills (30 fills total)
- **6 new drum fills** — 3 cajón, 3 metal
- **Duration arithmetic** — multiply, divide, add
- **Improved djembe slap** synthesis
- Song #31: Acid Tabla
## 0.38.2
- **`Part.ramp()`** — smooth parameter automation from current value to
target over a duration. Works for lowpass, reverb, distortion, chorus,
delay, volume, and any `.set()` parameter. Four interpolation curves:
linear, ease_in, ease_out, ease_in_out.
## 0.38.1
- **Dynamic curves** — `Part.crescendo()`, `Part.decrescendo()`,
`Part.swell()`, and `Part.dynamics()` for velocity ramps and custom
curves across a sequence of notes
## 0.38.0
- **Articulations** — `staccato`, `legato`, `marcato`, `tenuto`, `accent`,
`fermata` via `articulation=` on `Part.add()` and `Part.hold()`
- **`Part.hit()`** — place individual drum sounds in a Part's note stream
with articulation, velocity, and effects support
- **5 new djembe patterns** — dununba, tiriba, yankadi, djansa, mendiani
- **3 new djembe fills** — djembe call, djembe roll, djembe break (30 fills total)
- **Cross-choke drum damping** — striking one sound fades out related sounds
(djembe, hi-hats, cajón, doumbek)
- **Improved djembe slap** — dry goatskin pop instead of snare-like noise
## 0.37.0
- **5 new djembe patterns** — dununba, tiriba, yankadi, djansa, mendiani
- **3 new djembe fills** — djembe call, djembe roll, djembe break (30 fills total)
- **Cross-choke drum damping** — striking one sound on a hand drum fades
out the ring of related sounds (djembe slap kills bass resonance, closed
hat chokes open hat, cajón slap dampens bass, doumbek tek dampens dum)
- **Improved djembe slap** — dry, high-pitched goatskin pop instead of
snare-like noise rattle
## 0.36.6
- **6 new drum fills** — 3 cajón (flam, rumble, breakdown) and 3 metal
(triplet, blast, cascade). 27 fills total.
- Updated drums documentation with fill lists and examples
## 0.36.5
- **Duration arithmetic** — `Duration.WHOLE * 2`, `Duration.HALF + Duration.QUARTER`,
division, and reverse multiply all work now (previously raised TypeError)
## 0.36.3
- **`Part.hold()`** — polyphonic overlap on a single part. Add notes
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
+8
View File
@@ -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
+135 -16
View File
@@ -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, 95+ 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,32 @@ 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_BASS (108), CAJON_SLAP (109), CAJON_TAP (110)
**Metal Kit:** METAL_KICK (105), METAL_SNARE (106), METAL_HAT (107)
**Marching Snare:** MARCH_SNARE (115), MARCH_RIMSHOT (116), MARCH_CLICK (118)
**Quads (Tenors):** QUAD_1 (119), QUAD_2 (120), QUAD_3 (121), QUAD_4 (122),
QUAD_SPOCK (123)
**Marching Bass:** BASS_1 (124), BASS_2 (125), BASS_3 (126), BASS_4 (127),
BASS_5 (80)
Drum Synthesis
--------------
@@ -200,8 +225,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 +260,17 @@ ending and a new one is about to begin. Without fills, a drum pattern
just loops. With them, it breathes and has structure.
``Pattern.fill()`` loads a 1-bar drum fill -- a short break that
transitions between sections. 21 fill presets are available:
transitions between sections. 30 fill presets are available:
.. code-block:: pycon
>>> Pattern.list_fills()
['afrobeat', 'blast', 'bossa nova', 'breakdown', 'buildup',
'cumbia', 'disco', 'funk', 'highlife', 'hip hop', 'house',
'jazz', 'jazz brush', 'metal', 'reggae', 'rock', 'rock crash',
'cajon breakdown', 'cajon flam', 'cajon rumble',
'cumbia', 'disco', 'djembe break', 'djembe call', 'djembe roll',
'funk', 'highlife', 'hip hop', 'house',
'jazz', 'jazz brush', 'metal', 'metal blast', 'metal cascade',
'metal triplet', 'reggae', 'rock', 'rock crash',
'salsa', 'samba', 'second line', 'trap']
>>> fill = Pattern.fill("rock")
@@ -330,14 +358,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 +444,19 @@ central to the drum ensemble traditions of Mali, Guinea, and Senegal.
**3 sounds** -- bass (open center strike), tone (edge strike), and
slap (sharp edge strike).
**3 patterns:** djembe (a basic accompanying rhythm), kuku (a
traditional rhythm from Guinea associated with fishing), and soli (a
solo/celebration rhythm).
**8 patterns:** djembe (basic accompanying rhythm), kuku (Guinean harvest
dance), soli (powerful Mandinka rhythm), dununba (heavy bass-driven),
tiriba (joyful Susu rhythm), yankadi (gentle greeting/welcome), djansa
(fast Malinke dance), mendiani (women's celebratory dance).
**3 fills:** djembe call (bass-tone-slap conversation building to climax),
djembe roll (rapid slaps accelerating into bass), djembe break (syncopated
West African-style break).
.. code-block:: python
score = Score("4/4", bpm=120)
score.drums("djembe", repeats=4)
score.drums("djembe", repeats=8, fill="djembe call", fill_every=4)
Metal Kit
~~~~~~~~~
@@ -428,10 +472,85 @@ 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)
Marching Percussion
~~~~~~~~~~~~~~~~~~~
A full drumline — snare, quads (tenors), and pitched bass drums.
Every sound is synthesized: kevlar snare heads, aluminum shell ting
on the quads, felt-beater thwack on the basses.
**Snare** -- 3 sounds: MARCH_SNARE (tight kevlar tap), MARCH_RIMSHOT
(woody-metallic crack), MARCH_CLICK (stick click for count-offs).
**Quads** -- 5 sounds: QUAD_1 through QUAD_4 (high to low pitched
tenors) plus QUAD_SPOCK (rim click on the shell).
**Bass drums** -- 5 pitched drums: BASS_1 (highest/smallest) through
BASS_5 (lowest/biggest), each with a prominent felt-beater thwack.
**6 patterns:** march (basic 4/4), cadence (8-beat street beat),
march paradiddle, march roll (buzz crescendo), quad sweep (run across
all 4 drums), quad groove, bass split (cascading across the line),
bass unison (all 5 hit together), drumline (snare + quads + bass).
**Rudiment methods:** ``Part.flam()``, ``Part.diddle()``, and
``Part.cheese()`` for marching rudiments on any drum sound.
**Ensemble rendering:** ``ensemble=N`` on any Part duplicates the
voice with per-player timing tendencies and micro pitch drift.
``ensemble=8`` for a snare line, ``ensemble=20`` for a massive section.
.. code-block:: python
# Full drumline with ensemble
snares = score.part("snares", synth="sine", volume=0.9,
reverb=0.2, ensemble=8)
quads = score.part("quads", synth="sine", volume=0.5,
reverb=0.2, ensemble=4)
basses = score.part("basses", synth="sine", volume=0.55,
reverb=0.2, ensemble=5)
snares.flam(DrumSound.MARCH_SNARE, Duration.QUARTER, velocity=120)
snares.diddle(DrumSound.MARCH_SNARE, Duration.EIGHTH, velocity=60)
# Or use patterns
score.drums("drumline", repeats=4)
**Sympathetic resonance:** The marching snare builds up snare wire
buzz as hits accumulate, and the buzz decays during rests — just like
a real drum.
MIDI Export
-----------
+5 -3
View File
@@ -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
View File
@@ -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
------------------------------------------
+203 -2
View File
@@ -47,6 +47,18 @@ A ``Duration`` represents a note length in beats (quarter note = 1 beat):
>>> Duration.TRIPLET_QUARTER.value
0.6666666666666666
Duration supports arithmetic — multiply, divide, and add to create
compound durations:
.. code-block:: pycon
>>> Duration.WHOLE * 2
8.0
>>> Duration.HALF + Duration.QUARTER
3.0
>>> Duration.WHOLE / 2
2.0
Time Signatures
---------------
@@ -399,6 +411,143 @@ The arpeggiator also accepts velocity:
lead.arpeggio("Am", bars=2, pattern="up", velocity=80)
Articulations
-------------
Articulations change *how* a note is played — its attack, duration, and
weight. A staccato note is short and bouncy. A marcato note hits hard.
A legato note melts into the next one. This is the difference between
a melody that sounds like a MIDI file and one that sounds like a
musician played it.
Pass ``articulation=`` to ``Part.add()``:
.. code-block:: python
piano.add("C4", Duration.QUARTER, articulation="staccato") # short, bouncy
piano.add("D4", Duration.QUARTER, articulation="legato") # smooth, overlaps
piano.add("E4", Duration.QUARTER, articulation="marcato") # heavy accent
piano.add("F4", Duration.QUARTER, articulation="tenuto") # held, soft attack
piano.add("G4", Duration.QUARTER, articulation="accent") # louder
piano.add("C5", Duration.HALF, articulation="fermata") # held longer
What each articulation does:
- **staccato** — plays ~40% of the note duration with a quick fade-out. Short and detached.
- **legato** — extends ~15% into the next note. Smooth and connected.
- **marcato** — 25% velocity boost + sharper attack. Heavy and accented.
- **tenuto** — full duration with a softer attack ramp. Held and deliberate.
- **accent** — 20% velocity boost, no duration change.
- **fermata** — stretches the note 50% longer.
Articulations work on ``Part.hold()`` and ``Part.hit()`` too.
Dynamic Curves
--------------
Real music breathes — phrases get louder, get quieter, swell and
recede. Dynamic curves let you shape the velocity across a sequence
of notes instead of setting each one manually.
.. code-block:: python
# Crescendo: quiet to loud
piano.crescendo(["C4","D4","E4","F4","G4","A4","B4","C5"],
Duration.QUARTER, start_vel=30, end_vel=110)
# Decrescendo: loud to quiet
piano.decrescendo(["C5","B4","A4","G4","F4","E4","D4","C4"],
Duration.QUARTER, start_vel=110, end_vel=30)
# Swell: up then back down (orchestral < > shape)
strings.swell(["C4","D4","E4","F4","G4","F4","E4","D4"],
Duration.QUARTER, low_vel=35, peak_vel=110)
# Custom curve: explicit velocity per note
piano.dynamics(["C4","E4","G4","C5"], Duration.QUARTER,
velocities=[50, 80, 110, 90])
Four methods:
- **crescendo()** — linear velocity ramp from ``start_vel`` to ``end_vel``.
- **decrescendo()** — same thing, but typically loud to quiet.
- **swell()** — ramps up to the midpoint, then back down. The classic
orchestral crescendo-decrescendo.
- **dynamics()** — the general form. Pass a ``(start, end)`` tuple for
a linear ramp, or a list of velocities for a custom curve.
All four accept ``articulation=`` to combine dynamics with articulations:
.. code-block:: python
# Staccato crescendo — bouncy notes getting louder
piano.crescendo(["C4","E4","G4","C5","E5","G5","C6","E6"],
Duration.EIGHTH, start_vel=40, end_vel=110,
articulation="staccato")
Part.hit() — Manual Drum Placement
-----------------------------------
The pattern system is great for grooves, but sometimes you want to
place individual drum hits with full control — articulations, effects,
and all. ``Part.hit()`` puts a drum sound into a Part's note stream:
.. code-block:: python
from pytheory import DrumSound
kit = score.part("kit", synth="sine", volume=0.7)
kit.hit(DrumSound.KICK, Duration.QUARTER, articulation="accent")
kit.hit(DrumSound.CLOSED_HAT, Duration.EIGHTH, velocity=60)
kit.hit(DrumSound.SNARE, Duration.EIGHTH, articulation="marcato")
Because hits go through the normal Part renderer, they get humanize,
effects, and articulations for free. Use this for custom beats that
don't fit a preset pattern, or for one-shot accent hits layered on
top of a pattern.
Rudiments — Flam, Diddle, Cheese
---------------------------------
Marching percussion rudiments as methods on any Part:
.. code-block:: python
from pytheory import DrumSound
p = score.part("snares", synth="sine", volume=0.9)
# Flam: grace note + main hit (gap controls tightness)
p.flam(DrumSound.MARCH_SNARE, Duration.QUARTER, velocity=120)
# Diddle: two equal strokes in one note duration
p.diddle(DrumSound.MARCH_SNARE, Duration.EIGHTH, velocity=60)
# Cheese: flam + diddle combined
p.cheese(DrumSound.MARCH_SNARE, Duration.QUARTER, velocity=120)
Ensemble
--------
Any Part can be rendered as an ensemble — multiple players with
per-player timing tendencies and micro pitch drift:
.. code-block:: python
# 8-player snare line
snares = score.part("snares", synth="sine", volume=0.9, ensemble=8)
# 20-player string section
strings = score.part("strings", instrument="string_ensemble", ensemble=20)
# Single player (default)
solo = score.part("solo", instrument="violin")
Each ensemble voice gets a consistent timing personality (some rush,
some drag) plus small per-note wobble, and slightly different tuning.
The result sounds like a real section — together but alive.
Swing and Groove
----------------
@@ -478,6 +627,50 @@ integrate naturally with the rest of the automation system:
pad.rest(Duration.WHOLE)
pad.rest(Duration.WHOLE)
Parameter Ramps
---------------
Fades only control volume. ``Part.ramp()`` smoothly sweeps *any*
parameter from its current value to a target — filters, reverb,
distortion, chorus, delay, anything ``.set()`` accepts. This is how
you build filter sweeps, gradual effect sends, and EDM buildups.
.. code-block:: python
lead = score.part("lead", synth="saw", lowpass=200, lowpass_q=3.0)
# Open the filter over 8 bars
lead.ramp(over=Duration.WHOLE * 8, lowpass=8000)
# Ramp multiple params at once
pad.ramp(over=Duration.WHOLE * 4, reverb=0.5, chorus=0.3)
# Close the filter with distortion fading in
lead.ramp(over=Duration.WHOLE * 4, lowpass=400, distortion=0.5)
Four interpolation curves:
- **linear** — constant rate of change (default).
- **ease_in** — starts slow, accelerates. Good for buildups.
- **ease_out** — starts fast, decelerates. Good for releases.
- **ease_in_out** — slow at both ends. Smooth and natural.
.. code-block:: python
# EDM buildup: slow start, accelerating filter sweep
lead.ramp(over=Duration.WHOLE * 8, curve="ease_in", lowpass=8000)
# Smooth reverb wash fading in and settling
pad.ramp(over=Duration.WHOLE * 4, curve="ease_in_out", reverb=0.6)
``ramp()`` generates automation points every quarter-beat by default.
Set ``resolution=0.125`` for smoother curves (every 32nd note), or
``resolution=1.0`` for lighter automation (every beat).
Combine with ``lfo()`` for cyclic modulation and ``ramp()`` for
one-shot sweeps — together they cover the full range of parameter
automation.
Humanize
--------
@@ -667,8 +860,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
View File
@@ -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
View File
@@ -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.
+39
View File
@@ -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
View File
@@ -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
+968 -1
View File
@@ -1843,6 +1843,967 @@ 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)")
def snare_cadence():
"""Snare Cadence — full drumline with ensemble, flams, diddles, cheese."""
score = Score("4/4", bpm=120)
S = DrumSound.MARCH_SNARE
R = DrumSound.MARCH_RIMSHOT
C = DrumSound.MARCH_CLICK
Q1 = DrumSound.QUAD_1
Q2 = DrumSound.QUAD_2
Q3 = DrumSound.QUAD_3
Q4 = DrumSound.QUAD_4
QS = DrumSound.QUAD_SPOCK
B1 = DrumSound.BASS_1
B2 = DrumSound.BASS_2
B3 = DrumSound.BASS_3
B4 = DrumSound.BASS_4
B5 = DrumSound.BASS_5
# Snare line — 8 players
p = score.part("snares", synth="sine", volume=0.9, reverb=0.2, ensemble=8)
# Quad line — 4 players
q = score.part("quads", synth="sine", volume=0.5, reverb=0.2, ensemble=4)
# Bass line — 5 players
b = score.part("basses", synth="sine", volume=0.55, reverb=0.2, ensemble=5)
_trip = 1.0 / 3
# Helper: bass split run (down or up)
def bass_down(dur=Duration.SIXTEENTH):
b.hit(B1, dur, velocity=95)
b.hit(B2, dur, velocity=90)
b.hit(B3, dur, velocity=85)
b.hit(B4, dur, velocity=90)
def bass_up(dur=Duration.SIXTEENTH):
b.hit(B4, dur, velocity=90)
b.hit(B3, dur, velocity=85)
b.hit(B2, dur, velocity=90)
b.hit(B1, dur, velocity=95)
def bass_hit(dur=Duration.QUARTER):
b.hit(B3, dur, velocity=100)
def quad_sweep_down():
q.hit(Q1, Duration.SIXTEENTH, velocity=95)
q.hit(Q2, Duration.SIXTEENTH, velocity=88)
q.hit(Q3, Duration.SIXTEENTH, velocity=82)
q.hit(Q4, Duration.SIXTEENTH, velocity=78)
def quad_sweep_up():
q.hit(Q4, Duration.SIXTEENTH, velocity=78)
q.hit(Q3, Duration.SIXTEENTH, velocity=82)
q.hit(Q2, Duration.SIXTEENTH, velocity=88)
q.hit(Q1, Duration.SIXTEENTH, velocity=95)
# ── Click count-off ──
for _ in range(4):
p.hit(C, Duration.QUARTER, velocity=95)
q.rest(Duration.QUARTER)
b.rest(Duration.QUARTER)
# ── Section 1: 16th groove — snares only (4 bars) ──
for _ in range(16):
q.rest(Duration.QUARTER)
b.rest(Duration.QUARTER)
for _ in range(2):
p.hit(R, Duration.SIXTEENTH, velocity=118)
p.hit(S, Duration.SIXTEENTH, velocity=32)
p.hit(S, Duration.SIXTEENTH, velocity=35)
p.hit(S, Duration.SIXTEENTH, velocity=30)
p.hit(R, Duration.SIXTEENTH, velocity=115)
p.hit(S, Duration.SIXTEENTH, velocity=30)
p.hit(S, Duration.SIXTEENTH, velocity=28)
p.hit(S, Duration.SIXTEENTH, velocity=32)
p.hit(R, Duration.SIXTEENTH, velocity=118)
p.hit(S, Duration.SIXTEENTH, velocity=35)
p.hit(S, Duration.SIXTEENTH, velocity=30)
p.hit(S, Duration.SIXTEENTH, velocity=32)
p.hit(R, Duration.SIXTEENTH, velocity=120)
p.hit(S, Duration.SIXTEENTH, velocity=30)
p.hit(S, Duration.SIXTEENTH, velocity=28)
p.hit(S, Duration.SIXTEENTH, velocity=32)
# Triplets mixed in
for _ in range(2):
p.hit(R, _trip, velocity=118)
p.hit(S, _trip, velocity=32)
p.hit(S, _trip, velocity=30)
p.hit(R, _trip, velocity=115)
p.hit(S, _trip, velocity=28)
p.hit(S, _trip, velocity=32)
p.hit(R, Duration.SIXTEENTH, velocity=118)
p.hit(S, Duration.SIXTEENTH, velocity=30)
p.hit(S, Duration.SIXTEENTH, velocity=32)
p.hit(S, Duration.SIXTEENTH, velocity=28)
p.hit(R, _trip, velocity=118)
p.hit(S, _trip, velocity=30)
p.hit(S, _trip, velocity=35)
# ── Section 2: Quads + bass enter (4 bars) ──
for _ in range(2):
p.flam(S, Duration.QUARTER, velocity=118)
p.hit(S, Duration.SIXTEENTH, velocity=30)
p.hit(S, Duration.SIXTEENTH, velocity=32)
p.hit(R, _trip, velocity=118)
p.hit(S, _trip, velocity=28)
p.hit(S, _trip, velocity=30)
p.flam(S, Duration.QUARTER, velocity=118)
quad_sweep_down()
q.hit(QS, Duration.QUARTER, velocity=100)
quad_sweep_up()
q.hit(QS, Duration.QUARTER, velocity=100)
bass_hit()
b.hit(B5, Duration.QUARTER, velocity=95)
bass_hit()
b.hit(B1, Duration.QUARTER, velocity=95)
for _ in range(2):
p.hit(S, _trip, velocity=35)
p.flam(S, _trip * 2, velocity=118)
p.hit(S, Duration.SIXTEENTH, velocity=30)
p.hit(S, Duration.SIXTEENTH, velocity=32)
p.flam(S, Duration.QUARTER, velocity=118)
p.hit(S, _trip, velocity=28)
p.hit(R, _trip, velocity=122)
p.hit(S, _trip, velocity=35)
quad_sweep_down()
quad_sweep_up()
q.hit(Q1, Duration.EIGHTH, velocity=95)
q.hit(Q4, Duration.EIGHTH, velocity=85)
q.hit(QS, Duration.QUARTER, velocity=100)
bass_down()
bass_up()
b.hit(B3, Duration.HALF, velocity=100)
# ── Section 3: Flams + diddles + full line (4 bars) ──
for _ in range(2):
p.flam(S, Duration.QUARTER, velocity=120)
p.diddle(S, Duration.EIGHTH, velocity=45)
p.hit(S, _trip, velocity=30)
p.hit(S, _trip, velocity=32)
p.hit(S, _trip, velocity=28)
p.hit(R, Duration.EIGHTH, velocity=122)
p.diddle(S, Duration.EIGHTH, velocity=42)
q.hit(Q1, Duration.QUARTER, velocity=95)
q.hit(Q3, Duration.EIGHTH, velocity=55)
q.hit(Q2, _trip, velocity=55)
q.hit(Q3, _trip, velocity=55)
q.hit(Q4, _trip, velocity=55)
q.hit(QS, Duration.EIGHTH, velocity=100)
q.hit(Q1, Duration.EIGHTH, velocity=55)
bass_hit()
b.hit(B1, Duration.EIGHTH, velocity=90)
b.hit(B5, Duration.EIGHTH, velocity=95)
bass_hit()
b.hit(B5, Duration.EIGHTH, velocity=90)
b.hit(B1, Duration.EIGHTH, velocity=95)
for _ in range(2):
p.diddle(S, Duration.EIGHTH, velocity=45)
p.hit(R, _trip, velocity=120)
p.hit(S, _trip, velocity=30)
p.hit(S, _trip, velocity=32)
p.diddle(S, Duration.EIGHTH, velocity=48)
p.hit(R, _trip, velocity=118)
p.hit(S, _trip, velocity=28)
p.hit(S, _trip, velocity=30)
p.flam(S, Duration.EIGHTH, velocity=122)
p.hit(S, Duration.EIGHTH, velocity=35)
quad_sweep_down()
quad_sweep_up()
quad_sweep_down()
quad_sweep_up()
bass_down()
bass_up()
bass_down()
bass_up()
# ── Section 4: Cheese + 32nds (4 bars) ──
for _ in range(2):
p.cheese(S, Duration.QUARTER, velocity=120)
p.hit(S, 0.0625, velocity=30)
p.hit(S, 0.0625, velocity=32)
p.hit(S, 0.0625, velocity=35)
p.hit(S, 0.0625, velocity=30)
p.cheese(S, Duration.QUARTER, velocity=118)
p.diddle(S, Duration.EIGHTH, velocity=48)
p.hit(R, Duration.EIGHTH, velocity=125)
q.hit(QS, Duration.QUARTER, velocity=105)
q.hit(Q1, Duration.SIXTEENTH, velocity=55)
q.hit(Q2, Duration.SIXTEENTH, velocity=55)
q.hit(Q3, Duration.SIXTEENTH, velocity=55)
q.hit(Q4, Duration.SIXTEENTH, velocity=55)
q.hit(QS, Duration.QUARTER, velocity=105)
q.hit(Q4, Duration.EIGHTH, velocity=55)
q.hit(Q1, Duration.EIGHTH, velocity=90)
bass_hit()
b.hit(B1, Duration.EIGHTH, velocity=90)
b.hit(B3, Duration.EIGHTH, velocity=85)
b.hit(B5, Duration.EIGHTH, velocity=95)
b.hit(B3, Duration.EIGHTH, velocity=85)
bass_hit()
b.rest(Duration.QUARTER)
# All cheese
p.cheese(S, Duration.QUARTER, velocity=122)
p.cheese(S, Duration.QUARTER, velocity=120)
p.cheese(S, Duration.QUARTER, velocity=125)
p.cheese(S, Duration.QUARTER, velocity=122)
q.hit(QS, Duration.QUARTER, velocity=105)
q.hit(QS, Duration.QUARTER, velocity=105)
q.hit(QS, Duration.QUARTER, velocity=108)
q.hit(QS, Duration.QUARTER, velocity=105)
b.hit(B5, Duration.QUARTER, velocity=100)
b.hit(B3, Duration.QUARTER, velocity=100)
b.hit(B1, Duration.QUARTER, velocity=100)
b.hit(B3, Duration.QUARTER, velocity=100)
p.flam(S, Duration.EIGHTH, velocity=120)
p.diddle(S, Duration.EIGHTH, velocity=50)
p.flam(S, Duration.EIGHTH, velocity=122)
p.diddle(S, Duration.EIGHTH, velocity=52)
p.flam(S, Duration.EIGHTH, velocity=125)
p.diddle(S, Duration.EIGHTH, velocity=55)
p.hit(R, Duration.EIGHTH, velocity=127)
p.hit(S, Duration.EIGHTH, velocity=38)
quad_sweep_down()
quad_sweep_up()
quad_sweep_down()
quad_sweep_up()
bass_down()
bass_up()
bass_down()
bass_up()
# ── Section 5: 16ths + triplet 16ths + 32nds (4 bars) ──
_trip16 = 1.0 / 6
for _ in range(2):
for beat in range(4):
p.hit(R, _trip, velocity=118)
p.hit(S, _trip, velocity=35)
p.hit(S, _trip, velocity=32)
quad_sweep_down()
quad_sweep_up()
quad_sweep_down()
quad_sweep_up()
bass_hit()
b.hit(B5, Duration.QUARTER, velocity=95)
bass_hit()
b.hit(B1, Duration.QUARTER, velocity=95)
# 32nd run crescendo
for i in range(32):
p.hit(S, 0.0625, velocity=min(22 + i * 3, 92))
p.hit(R, Duration.EIGHTH, velocity=125)
p.hit(R, Duration.EIGHTH, velocity=127)
for _ in range(4):
q.hit(Q1, 0.0625, velocity=55)
q.hit(Q2, 0.0625, velocity=55)
q.hit(Q3, 0.0625, velocity=55)
q.hit(Q4, 0.0625, velocity=55)
q.hit(QS, Duration.QUARTER, velocity=108)
bass_down()
bass_up()
bass_down()
b.hit(B5, Duration.QUARTER, velocity=100)
b.hit(B1, Duration.QUARTER, velocity=100)
# Triplet 16ths — all sections
for _ in range(2):
for beat in range(4):
p.hit(R, _trip16, velocity=115)
p.hit(S, _trip16, velocity=30)
p.hit(S, _trip16, velocity=32)
p.hit(R, _trip16, velocity=112)
p.hit(S, _trip16, velocity=28)
p.hit(S, _trip16, velocity=30)
for beat in range(4):
q.hit(Q1, _trip16, velocity=90)
q.hit(Q2, _trip16, velocity=55)
q.hit(Q3, _trip16, velocity=55)
q.hit(Q4, _trip16, velocity=55)
q.hit(Q3, _trip16, velocity=55)
q.hit(Q2, _trip16, velocity=55)
bass_down()
bass_up()
bass_down()
bass_up()
# ── Section 6: Buzz roll climax (2 bars) ──
for i in range(64):
p.hit(S, 0.0625, velocity=min(20 + i * 1.5, 100))
p.hit(R, Duration.EIGHTH, velocity=127)
p.hit(R, Duration.EIGHTH, velocity=127)
for i in range(32):
q.hit([Q1, Q2, Q3, Q4][i % 4], 0.0625, velocity=min(40 + i * 2, 95))
q.hit(QS, Duration.QUARTER, velocity=110)
for i in range(16):
b.hit([B1, B2, B3, B4, B5, B4, B3, B2,
B1, B2, B3, B4, B5, B4, B3, B2][i], Duration.SIXTEENTH, velocity=90)
b.hit(B3, Duration.HALF, velocity=100)
b.hit(B3, Duration.HALF, velocity=100)
# ── Ending: big unison hits ──
p.hit(R, Duration.EIGHTH, velocity=127)
q.hit(QS, Duration.EIGHTH, velocity=110)
b.hit(B3, Duration.EIGHTH, velocity=100)
p.rest(Duration.QUARTER + Duration.EIGHTH)
q.rest(Duration.QUARTER + Duration.EIGHTH)
b.rest(Duration.QUARTER + Duration.EIGHTH)
p.hit(R, Duration.EIGHTH, velocity=127)
q.hit(QS, Duration.EIGHTH, velocity=110)
b.hit(B3, Duration.EIGHTH, velocity=100)
p.rest(Duration.QUARTER + Duration.EIGHTH)
q.rest(Duration.QUARTER + Duration.EIGHTH)
b.rest(Duration.QUARTER + Duration.EIGHTH)
# Flam into final CRACK — all sections
p.flam(S, Duration.EIGHTH, velocity=127)
q.hit(QS, Duration.EIGHTH, velocity=110)
b.hit(B3, Duration.EIGHTH, velocity=100)
p.rest(Duration.QUARTER + Duration.EIGHTH)
q.rest(Duration.QUARTER + Duration.EIGHTH)
b.rest(Duration.QUARTER + Duration.EIGHTH)
p.hit(R, Duration.QUARTER, velocity=127)
q.hit(QS, Duration.QUARTER, velocity=110)
b.hit(B3, Duration.QUARTER, velocity=100)
p.rest(Duration.HALF)
q.rest(Duration.HALF)
b.rest(Duration.HALF)
play_song(score, "Snare Cadence — full drumline (8 snares, 4 quads, 5 basses)")
SONGS = {
"1": ("Bossa Nova in A minor", bossa_nova_girl),
"2": ("Bebop in Bb major", bebop_in_bb),
@@ -1870,6 +2831,12 @@ 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),
"32": ("Snare Cadence (marching snare, flams, diddles)", snare_cadence),
}
if __name__ == "__main__":
@@ -1883,7 +2850,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-32, or 'all'): ").strip()
print()
if choice == "all":
+69
View File
@@ -0,0 +1,69 @@
"""Sprunki Simon Phase 1 — melody reference.
Notes transcribed from MIDI. Use as a base for arrangements.
Usage:
python examples/sprunki.py
"""
import sounddevice as sd
from pytheory import Score, Duration
from pytheory.play import render_score, SAMPLE_RATE
def sprunki_simon():
score = Score("4/4", bpm=200)
lead = score.part("lead", synth="square", envelope="pluck", volume=0.5,
lowpass=4500, detune=3, reverb=0.1)
# Phrase A
lead.add("E4", 1.0)
lead.add("G4", 1.0)
lead.rest(1.5)
lead.add("A4", 0.5)
lead.add("B4", 1.0)
lead.add("A4", 1.0)
lead.add("G4", 1.0)
lead.add("D4", 1.0)
# Phrase B
lead.add("E4", 1.0)
lead.add("G4", 1.0)
lead.rest(1.5)
lead.add("A4", 0.5)
lead.add("D4", 2.0)
lead.add("B3", 1.0)
lead.add("A3", 0.5)
lead.add("D4", 0.5)
# Phrase C
lead.add("E4", 1.0)
lead.add("G4", 1.0)
lead.rest(1.5)
lead.add("A4", 0.5)
lead.add("B4", 1.0)
lead.add("A4", 1.0)
lead.add("G4", 1.0)
lead.add("B4", 1.0)
# Phrase D
lead.add("A4", 2.0)
lead.add("G4", 1.0)
lead.add("E4", 1.0)
lead.add("B3", 2.0)
lead.add("D4", 2.0)
return score
if __name__ == "__main__":
score = sprunki_simon()
print(" Sprunki Simon Phase 1")
try:
buf = render_score(score)
sd.play(buf, SAMPLE_RATE)
sd.wait()
except KeyboardInterrupt:
sd.stop()
+1 -2
View File
@@ -1,6 +1,6 @@
[project]
name = "pytheory"
version = "0.36.2"
version = "0.39.2"
description = "Music Theory for Humans"
readme = "README.md"
license = "MIT"
@@ -21,7 +21,6 @@ classifiers = [
"Programming Language :: Python :: 3.13",
]
dependencies = [
"numeral",
"sounddevice",
"scipy",
]
+2 -2
View File
@@ -1,6 +1,6 @@
"""PyTheory: Music Theory for Humans."""
__version__ = "0.36.2"
__version__ = "0.39.2"
from .tones import Tone, Interval
from .systems import System, SYSTEMS, TET
@@ -23,7 +23,7 @@ __all__ = [
"PROGRESSIONS", "Chord", "Fretboard", "Fingering", "analyze_progression",
"System", "SYSTEMS", "TET", "CHARTS", "charts_for_fretboard",
"play", "save", "save_midi", "play_progression", "play_pattern",
"play_score", "Synth", "Envelope",
"play_score", "render_score", "Synth", "Envelope",
"Duration", "TimeSignature", "RhythmNote", "Rest", "Score", "Part",
"DrumSound", "Pattern", "Section", "INSTRUMENTS",
]
+37
View File
@@ -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
View File
@@ -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()
+474 -56
View File
@@ -1237,6 +1237,47 @@ def steel_drum_wave(hz, peak=SAMPLE_PEAK, n_samples=SAMPLE_RATE):
return (peak * wave).astype(numpy.int16)
def harmonium_wave(hz, peak=SAMPLE_PEAK, n_samples=SAMPLE_RATE):
"""Harmonium — Indian pump organ, single free reed per note.
Unlike accordion (doubled musette reeds), the harmonium has one
reed per note no beating, just a pure, nasal, reedy tone.
Constant bellows pressure, warm but slightly buzzy. The sound
of kirtan, qawwali, and devotional music.
"""
t = numpy.arange(n_samples, dtype=numpy.float64) / SAMPLE_RATE
rng = numpy.random.default_rng(int(hz * 100) % 2**31)
# Single reed — odd harmonics stronger (like clarinet but warmer)
wave = numpy.zeros(n_samples, dtype=numpy.float64)
for n in range(1, 12):
f_n = hz * n
if f_n >= SAMPLE_RATE / 2:
break
amp = (1.0 / n) * (1.0 if n % 2 == 1 else 0.5)
phase = rng.uniform(0, 2 * numpy.pi)
wave += amp * numpy.sin(2 * numpy.pi * f_n * t + phase)
# Bellows pressure — gentle swell, slower than accordion
bellows = 0.9 + 0.1 * numpy.sin(2 * numpy.pi * 0.5 * t)
wave *= bellows
# Nasal character — slight midrange boost
import scipy.signal as _sig
center = min(1200, hz * 3)
lo = max(20, int(center - 300))
hi = min(SAMPLE_RATE // 2 - 1, int(center + 300))
if lo < hi:
bp, ap = _sig.butter(2, [lo, hi], btype='band', fs=SAMPLE_RATE)
nasal = _sig.lfilter(bp, ap, wave) * 0.2
wave += nasal
mx = numpy.abs(wave).max()
if mx > 0:
wave /= mx
return (peak * wave).astype(numpy.int16)
def accordion_wave(hz, peak=SAMPLE_PEAK, n_samples=SAMPLE_RATE):
"""Accordion — bellows-driven free reeds.
@@ -1794,6 +1835,7 @@ class Synth(Enum):
THEREMIN = "theremin_synth"
KALIMBA = "kalimba_synth"
STEEL_DRUM = "steel_drum_synth"
HARMONIUM = "harmonium_synth"
ACCORDION = "accordion_synth"
DIDGERIDOO = "didgeridoo_synth"
BAGPIPE = "bagpipe_synth"
@@ -1825,7 +1867,7 @@ _SYNTH_FUNCTIONS = {
"granular_synth": granular_wave, "vocal_synth": vocal_wave,
"pedal_steel_synth": pedal_steel_wave, "theremin_synth": theremin_wave,
"kalimba_synth": kalimba_wave, "steel_drum_synth": steel_drum_wave,
"accordion_synth": accordion_wave, "didgeridoo_synth": didgeridoo_wave,
"harmonium_synth": harmonium_wave, "accordion_synth": accordion_wave, "didgeridoo_synth": didgeridoo_wave,
"bagpipe_synth": bagpipe_wave,
"banjo_synth": banjo_wave, "mandolin_synth": mandolin_wave,
"ukulele_synth": ukulele_wave,
@@ -2584,6 +2626,52 @@ def _synth_mridangam_tha(n_samples):
return out
def _synth_doumbek_dum(n_samples):
"""Doumbek Dum — open center strike, deep and round."""
t = numpy.arange(n_samples, dtype=numpy.float32) / SAMPLE_RATE
freq = 80 + 40 * numpy.exp(-25 * t)
phase = 2 * numpy.pi * numpy.cumsum(freq) / SAMPLE_RATE
body = numpy.sin(phase) * _exp_decay(n_samples, 8) * 0.8
thump_len = min(int(SAMPLE_RATE * 0.04), n_samples)
import scipy.signal as _sig
thump = _noise(thump_len)
if thump_len > 20:
bl, al = _sig.butter(2, [50, 250], btype='band', fs=SAMPLE_RATE)
thump = _sig.lfilter(bl, al, numpy.pad(thump, (0, max(0, n_samples - thump_len))))[:thump_len].astype(numpy.float32)
thump *= _exp_decay(thump_len, 22) * 0.7
body[:thump_len] += thump
return numpy.tanh(body * 1.3).astype(numpy.float32)
def _synth_doumbek_tek(n_samples):
"""Doumbek Tek — sharp edge strike, bright and cutting."""
t = numpy.arange(n_samples, dtype=numpy.float32) / SAMPLE_RATE
ring = numpy.sin(2 * numpy.pi * 400 * t) * _exp_decay(n_samples, 22) * 0.5
ring2 = numpy.sin(2 * numpy.pi * 900 * t) * 0.3 * _exp_decay(n_samples, 30)
click_len = min(int(SAMPLE_RATE * 0.005), n_samples)
click = _noise(click_len) * _exp_decay(click_len, 300) * 0.9
import scipy.signal as _sig
if click_len > 10:
bl, al = _sig.butter(2, [2000, min(8000, SAMPLE_RATE // 2 - 1)], btype='band', fs=SAMPLE_RATE)
click = _sig.lfilter(bl, al, numpy.pad(click, (0, max(0, n_samples - click_len))))[:click_len].astype(numpy.float32)
result = ring + ring2
result[:click_len] += click
return numpy.tanh(result * 1.8).astype(numpy.float32)
def _synth_doumbek_ka(n_samples):
"""Doumbek Ka — muted edge slap, short and dry."""
n = min(n_samples, int(SAMPLE_RATE * 0.04))
t = numpy.arange(n, dtype=numpy.float32) / SAMPLE_RATE
body = numpy.sin(2 * numpy.pi * 350 * t) * _exp_decay(n, 30) * 0.4
slap = _noise(min(80, n)) * _exp_decay(min(80, n), 200) * 0.7
result = body
result[:min(80, n)] += slap
out = numpy.zeros(n_samples, dtype=numpy.float32)
out[:n] = numpy.tanh(result * 1.5)
return out
def _synth_cajon_bass(n_samples):
"""Cajón bass — palm strike on center of the face.
@@ -2708,6 +2796,151 @@ def _synth_metal_hat(n_samples):
return out
def _synth_march_snare(n_samples):
"""Marching snare — ultra-tight kevlar head, high and crisp.
Higher pitched than a kit snare. Very short decay all attack,
no sustain. Tight snare wires give a brief sizzle.
"""
t = numpy.arange(n_samples, dtype=numpy.float32) / SAMPLE_RATE
# Higher-pitched body — tight kevlar pops high
body = numpy.sin(2 * numpy.pi * 450 * t) * _exp_decay(n_samples, 60) * 0.4
body2 = numpy.sin(2 * numpy.pi * 700 * t) * _exp_decay(n_samples, 75) * 0.2
# Sharp stick pop
click_len = min(int(SAMPLE_RATE * 0.001), n_samples)
click = _noise(click_len) * _exp_decay(click_len, 400) * 1.2
# Very tight snare sizzle — higher band, shorter
buzz_len = min(int(SAMPLE_RATE * 0.025), n_samples)
buzz_raw = _noise(buzz_len)
if buzz_len > 20:
bl, al = scipy.signal.butter(2, [3500, 8000], btype='band', fs=SAMPLE_RATE)
buzz = scipy.signal.lfilter(bl, al, numpy.pad(buzz_raw, (0, max(0, n_samples - buzz_len))))[:buzz_len]
else:
buzz = buzz_raw
buzz *= _exp_decay(buzz_len, 50) * 0.35
result = body + body2
result[:click_len] += click
result[:buzz_len] += buzz
return numpy.tanh(result * 2.8)
def _synth_march_rimshot(n_samples):
"""Marching rimshot — woody metallic crack.
The stick catches the rim you get the full snare hit plus
a bright, woody-metallic crack from the aluminum rim. Short
ring that dies fast but gives it that cutting edge.
"""
wave = _synth_march_snare(n_samples)
t = numpy.arange(n_samples, dtype=numpy.float32) / SAMPLE_RATE
# Rim crack — bright but short, woody-metallic character
rim = numpy.sin(2 * numpy.pi * 1100 * t) * _exp_decay(n_samples, 45) * 0.35
rim2 = numpy.sin(2 * numpy.pi * 2200 * t) * _exp_decay(n_samples, 55) * 0.2
# Hard transient pop
pop_len = min(int(SAMPLE_RATE * 0.002), n_samples)
pop = _noise(pop_len) * _exp_decay(pop_len, 350) * 1.5
# Extra body punch
punch = numpy.sin(2 * numpy.pi * 500 * t) * _exp_decay(n_samples, 65) * 0.3
result = wave * 1.4 + rim + rim2 + punch
result[:pop_len] += pop
return numpy.tanh(result * 2.0)
def _synth_march_click(n_samples):
"""Stick click — taped hickory sticks clocked together.
Bright wood-on-wood with a slightly dampened attack from the
electrical tape. Not as ringy as a clave the tape absorbs
some of the high overtones but still bright and snappy.
"""
t = numpy.arange(n_samples, dtype=numpy.float32) / SAMPLE_RATE
# Wood resonance — brighter than before, but tape dampens ring
body = numpy.sin(2 * numpy.pi * 1100 * t) * _exp_decay(n_samples, 65) * 0.45
body2 = numpy.sin(2 * numpy.pi * 1800 * t) * _exp_decay(n_samples, 80) * 0.25
# Woody overtone — gives it that hickory character
body3 = numpy.sin(2 * numpy.pi * 2600 * t) * _exp_decay(n_samples, 95) * 0.12
# Bright but slightly muffled transient (tape on wood)
click_len = min(int(SAMPLE_RATE * 0.001), n_samples)
click_raw = _noise(click_len)
if click_len > 10:
bl, al = scipy.signal.butter(2, [800, 7000], btype='band', fs=SAMPLE_RATE)
click = scipy.signal.lfilter(bl, al, numpy.pad(click_raw, (0, max(0, n_samples - click_len))))[:click_len]
else:
click = click_raw
click *= _exp_decay(click_len, 350) * 0.9
result = body + body2 + body3
result[:click_len] += click
return numpy.tanh(result * 2.8)
def _synth_quad(n_samples, pitch=300):
"""Marching tenor/quad drum — tuned mylar head, bright and ringy.
Quads have a distinctive metallic ting from the high-tension
mylar head and aluminum shell. More ring than a kit tom,
brighter attack, clear pitch.
"""
t = numpy.arange(n_samples, dtype=numpy.float32) / SAMPLE_RATE
# Pitched body — more ring/sustain than snare
body = numpy.sin(2 * numpy.pi * pitch * t) * _exp_decay(n_samples, 22) * 0.5
# Metallic overtones — the ting
ting = numpy.sin(2 * numpy.pi * pitch * 2.3 * t) * _exp_decay(n_samples, 35) * 0.25
ting2 = numpy.sin(2 * numpy.pi * pitch * 3.1 * t) * _exp_decay(n_samples, 45) * 0.12
# Shell ring
shell = numpy.sin(2 * numpy.pi * pitch * 4.7 * t) * _exp_decay(n_samples, 55) * 0.06
# Sharp stick attack
click_len = min(int(SAMPLE_RATE * 0.001), n_samples)
click = _noise(click_len) * _exp_decay(click_len, 400) * 0.8
result = body + ting + ting2 + shell
result[:click_len] += click
return numpy.tanh(result * 2.5)
def _synth_quad_spock(n_samples):
"""Quad spock — rim shot on the tenor shell. Bright, ringy, cutting."""
t = numpy.arange(n_samples, dtype=numpy.float32) / SAMPLE_RATE
ring = numpy.sin(2 * numpy.pi * 1400 * t) * _exp_decay(n_samples, 40) * 0.5
ring2 = numpy.sin(2 * numpy.pi * 2100 * t) * _exp_decay(n_samples, 55) * 0.25
click_len = min(int(SAMPLE_RATE * 0.001), n_samples)
click = _noise(click_len) * _exp_decay(click_len, 400) * 1.0
result = ring + ring2
result[:click_len] += click
return numpy.tanh(result * 2.8)
def _synth_march_bass(n_samples, pitch=60):
"""Marching bass drum — deep, boomy, pitched, felt beater thwack.
The beater hitting the head is a big part of the sound a round,
pillowy thwack followed by the deep pitched boom. More beater
sound than a kit bass drum because marching bass drums project
outward.
"""
t = numpy.arange(n_samples, dtype=numpy.float32) / SAMPLE_RATE
# Deep pitched body — sustains and rings
body = numpy.sin(2 * numpy.pi * pitch * t) * _exp_decay(n_samples, 10) * 0.7
body2 = numpy.sin(2 * numpy.pi * pitch * 2 * t) * _exp_decay(n_samples, 16) * 0.2
# Sub thump
sub = numpy.sin(2 * numpy.pi * pitch * 0.5 * t) * _exp_decay(n_samples, 8) * 0.3
# BIG beater thwack — dominant part of the attack
thwack_len = min(int(SAMPLE_RATE * 0.025), n_samples)
thwack_raw = _noise(thwack_len)
if thwack_len > 10:
bl, al = scipy.signal.butter(2, [150, 2500], btype='band', fs=SAMPLE_RATE)
thwack = scipy.signal.lfilter(bl, al, numpy.pad(thwack_raw, (0, max(0, n_samples - thwack_len))))[:thwack_len]
else:
thwack = thwack_raw
thwack *= _exp_decay(thwack_len, 55) * 1.5
# Head slap — the mylar flexing on impact
slap_len = min(int(SAMPLE_RATE * 0.008), n_samples)
slap = numpy.sin(2 * numpy.pi * pitch * 3 * numpy.arange(slap_len, dtype=numpy.float32) / SAMPLE_RATE)
slap *= _exp_decay(slap_len, 90) * 0.4
result = body + body2 + sub
result[:thwack_len] += thwack
result[:slap_len] += slap
return numpy.tanh(result * 2.0)
def _synth_tabla_ge_bend(n_samples):
"""Tabla Ge with upward pitch bend — palm pressing into bayan head.
@@ -2802,29 +3035,27 @@ def _synth_djembe_tone(n_samples):
def _synth_djembe_slap(n_samples):
"""Djembe slap — edge strike with fingers spread, sharp crack.
The highest, sharpest djembe sound. Fingers fan out on contact
creating a loud crack with minimal sustain.
The highest, sharpest djembe sound. A dry, high-pitched pop from
goatskin membrane NOT a snare. Tight attack, very short decay,
skin character rather than wire rattle.
"""
t = numpy.arange(n_samples, dtype=numpy.float32) / SAMPLE_RATE
# Sharp crack — mostly noise
crack_len = min(int(SAMPLE_RATE * 0.02), n_samples)
crack = _noise(crack_len) * _exp_decay(crack_len, 100) * 1.0
# Brief high-pitched ring
ring = numpy.sin(2 * numpy.pi * 600 * t) * _exp_decay(n_samples, 25) * 0.4
ring2 = numpy.sin(2 * numpy.pi * 1200 * t) * 0.2 * _exp_decay(n_samples, 35)
# Brief membrane pop
thump_len = min(int(SAMPLE_RATE * 0.02), n_samples)
thump_raw = _noise(thump_len)
if thump_len > 20:
bl, al = scipy.signal.butter(2, [300, 2000], btype='band', fs=SAMPLE_RATE)
thump = scipy.signal.lfilter(bl, al, numpy.pad(thump_raw, (0, max(0, n_samples - thump_len))))[:thump_len]
# High membrane pop — goatskin resonance, much higher than snare
pop = numpy.sin(2 * numpy.pi * 900 * t) * _exp_decay(n_samples, 50) * 0.5
pop2 = numpy.sin(2 * numpy.pi * 1600 * t) * _exp_decay(n_samples, 60) * 0.25
pop3 = numpy.sin(2 * numpy.pi * 2400 * t) * _exp_decay(n_samples, 80) * 0.12
# Very short filtered click — hand-on-skin transient, not noise rattle
click_len = min(int(SAMPLE_RATE * 0.008), n_samples)
click_raw = _noise(click_len)
if click_len > 20:
bl, al = scipy.signal.butter(2, 1800 / (SAMPLE_RATE / 2), btype='high')
click = scipy.signal.lfilter(bl, al, numpy.pad(click_raw, (0, max(0, n_samples - click_len))))[:click_len]
else:
thump = thump_raw
thump *= _exp_decay(thump_len, 80) * 0.8
result = ring + ring2
result[:crack_len] += crack
result[:thump_len] += thump
return numpy.tanh(result * 1.7)
click = click_raw
click *= _exp_decay(click_len, 150) * 0.6
result = pop + pop2 + pop3
result[:click_len] += click
return numpy.tanh(result * 1.5)
def _synth_guiro(n_samples):
@@ -2914,6 +3145,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),
@@ -2922,6 +3157,22 @@ def _render_drum_hit(sound_value, n_samples):
DrumSound.METAL_KICK.value: lambda n: _synth_metal_kick(n),
DrumSound.METAL_SNARE.value: lambda n: _synth_metal_snare(n),
DrumSound.METAL_HAT.value: lambda n: _synth_metal_hat(n),
# Marching
DrumSound.MARCH_SNARE.value: lambda n: _synth_march_snare(n),
DrumSound.MARCH_RIMSHOT.value: lambda n: _synth_march_rimshot(n),
DrumSound.MARCH_CLICK.value: lambda n: _synth_march_click(n),
# Quads (tenor drums) — pitched high to low
DrumSound.QUAD_1.value: lambda n: _synth_quad(n, pitch=400),
DrumSound.QUAD_2.value: lambda n: _synth_quad(n, pitch=330),
DrumSound.QUAD_3.value: lambda n: _synth_quad(n, pitch=270),
DrumSound.QUAD_4.value: lambda n: _synth_quad(n, pitch=220),
DrumSound.QUAD_SPOCK.value: lambda n: _synth_quad_spock(n),
# Marching bass drums — pitched high to low
DrumSound.BASS_1.value: lambda n: _synth_march_bass(n, pitch=90),
DrumSound.BASS_2.value: lambda n: _synth_march_bass(n, pitch=75),
DrumSound.BASS_3.value: lambda n: _synth_march_bass(n, pitch=62),
DrumSound.BASS_4.value: lambda n: _synth_march_bass(n, pitch=52),
DrumSound.BASS_5.value: lambda n: _synth_march_bass(n, pitch=42),
}
renderer = _dispatch.get(sound_value, lambda n: _synth_clave(n))
@@ -4055,10 +4306,48 @@ def _render_notes_to_buf(notes, buf, samples_per_beat, total_samples,
start += _rnd.randint(-max_offset, max_offset)
start = max(0, start)
dur_ms = note.beats * 60_000 / bpm
# Articulation: adjust duration and velocity
art = getattr(note, 'articulation', '')
art_vel_mult = 1.0
art_attack_mult = 1.0 # multiplier for envelope attack
if art == 'staccato':
dur_ms *= 0.4 # short and bouncy
elif art == 'legato':
dur_ms *= 1.15 # slight overlap into next note
elif art == 'marcato':
art_vel_mult = 1.25 # heavier
art_attack_mult = 0.3 # sharper attack
elif art == 'tenuto':
art_attack_mult = 1.8 # softer attack, full duration
elif art == 'accent':
art_vel_mult = 1.2
elif art == 'fermata':
dur_ms *= 1.5 # held longer
n_samples = int(SAMPLE_RATE * dur_ms / 1000)
if start + n_samples > total_samples:
n_samples = total_samples - start
if n_samples > 0 and start >= 0:
# Drum hit via Part.hit() — use drum synth directly
from .rhythm import _DrumTone
if isinstance(note.tone, _DrumTone):
drum_wave = _render_drum_hit(note.tone.sound.value, n_samples)
mixed = drum_wave.astype(numpy.float32)
# Staccato fade-out for drums
if art == 'staccato':
fade_len = min(int(SAMPLE_RATE * 0.01), len(mixed))
if fade_len > 0:
mixed[-fade_len:] *= numpy.linspace(1.0, 0.0, fade_len).astype(numpy.float32)
vel = getattr(note, 'velocity', 100)
vel = min(127, int(vel * art_vel_mult))
if humanize > 0.0:
vel_jitter = int(humanize * 15)
vel = max(1, min(127, vel + _rnd.randint(-vel_jitter, vel_jitter)))
vel_scale = vel / 127.0
end = min(start + len(mixed), total_samples)
buf[start:end] += mixed[:end - start] * volume * vel_scale
if not getattr(note, '_hold', False):
beat_pos += note.beats
continue
# Get pitches
if hasattr(note.tone, 'tones'):
pitches = [t.pitch(temperament=temperament, reference_pitch=reference_pitch) for t in note.tone.tones]
@@ -4159,11 +4448,18 @@ def _render_notes_to_buf(notes, buf, samples_per_beat, total_samples,
if noise_mix > 0:
noise = numpy.random.uniform(-1, 1, n_samples).astype(numpy.float32)
mixed = mixed * (1.0 - noise_mix * 0.5) + noise * noise_mix * 0.5
# Amplitude envelope
if a > 0 or d > 0 or s < 1.0 or r > 0:
mixed = _apply_envelope(mixed, a, d, s, r)
# Per-note velocity
# Amplitude envelope (articulation may adjust attack)
art_a = a * art_attack_mult
if art_a > 0 or d > 0 or s < 1.0 or r > 0:
mixed = _apply_envelope(mixed, art_a, d, s, r)
# Staccato: apply a quick fade-out at the end
if art == 'staccato':
fade_len = min(int(SAMPLE_RATE * 0.01), len(mixed))
if fade_len > 0:
mixed[-fade_len:] *= numpy.linspace(1.0, 0.0, fade_len).astype(numpy.float32)
# Per-note velocity (articulation may boost)
vel = getattr(note, 'velocity', 100)
vel = min(127, int(vel * art_vel_mult))
if humanize > 0.0:
vel_jitter = int(humanize * 15)
vel = max(1, min(127, vel + _rnd.randint(-vel_jitter, vel_jitter)))
@@ -4200,7 +4496,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 +4540,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
@@ -4358,35 +4657,71 @@ def render_score(score):
synth_kwargs["mod_index"] = part.fm_index
_temperament = getattr(score, 'temperament', 'equal')
_ref_pitch = getattr(score, 'reference_pitch', 440.0)
if part.legato:
_render_legato_to_buf(
part.notes, part_buf, samples_per_beat, total_samples,
synth_fn, env_tuple, part.volume, score.bpm,
glide_time=part.glide, swing=effective_swing,
tempo_map=tempo_map if has_tempo_changes else None,
temperament=_temperament, reference_pitch=_ref_pitch)
else:
_render_notes_to_buf(
part.notes, part_buf, samples_per_beat, total_samples,
synth_fn, env_tuple, part.volume, score.bpm,
swing=effective_swing,
tempo_map=tempo_map if has_tempo_changes else None,
humanize=part.humanize,
detune=part.detune,
spread=part.spread,
stereo_buf=stereo_buf,
sub_osc=part.sub_osc,
noise_mix=part.noise_mix,
filter_attack=part.filter_attack,
filter_decay=part.filter_decay,
filter_sustain=part.filter_sustain,
filter_amount=part.filter_amount,
vel_to_filter=part.vel_to_filter,
filter_q=part.lowpass_q,
synth_kwargs=synth_kwargs,
temperament=_temperament,
reference_pitch=_ref_pitch,
analog=part.analog)
n_ensemble = max(1, getattr(part, 'ensemble', 1))
for _ens_i in range(n_ensemble):
# Each ensemble voice gets its own buffer
ens_buf = part_buf if n_ensemble == 1 else numpy.zeros(total_samples, dtype=numpy.float32)
# Ensemble voices get micro-variations
ens_humanize = part.humanize
ens_analog = part.analog
if n_ensemble > 1:
import random as _ens_rnd
_ens_rnd.seed(42 + _ens_i * 7)
# Hybrid approach:
# 1. Consistent player tendency (rush/drag) — seeded per player
_player_tendency = _ens_rnd.gauss(0, 0.018)
# 2. Tiny per-note wobble on top
ens_humanize = max(part.humanize, 0.012)
# Each player's drum tuned slightly different
ens_analog = max(part.analog, 0.06 + _ens_rnd.uniform(0, 0.08))
if part.legato:
_render_legato_to_buf(
part.notes, ens_buf, samples_per_beat, total_samples,
synth_fn, env_tuple, part.volume, score.bpm,
glide_time=part.glide, swing=effective_swing,
tempo_map=tempo_map if has_tempo_changes else None,
temperament=_temperament, reference_pitch=_ref_pitch)
else:
_render_notes_to_buf(
part.notes, ens_buf, samples_per_beat, total_samples,
synth_fn, env_tuple, part.volume, score.bpm,
swing=effective_swing,
tempo_map=tempo_map if has_tempo_changes else None,
humanize=ens_humanize,
detune=part.detune,
spread=part.spread,
stereo_buf=stereo_buf,
sub_osc=part.sub_osc,
noise_mix=part.noise_mix,
filter_attack=part.filter_attack,
filter_decay=part.filter_decay,
filter_sustain=part.filter_sustain,
filter_amount=part.filter_amount,
vel_to_filter=part.vel_to_filter,
filter_q=part.lowpass_q,
synth_kwargs=synth_kwargs,
temperament=_temperament,
reference_pitch=_ref_pitch,
analog=ens_analog)
if n_ensemble > 1:
# Shift the whole voice by the player's consistent tendency
# (some players rush, some drag — this is fixed per player)
shift_samples = int(_player_tendency * samples_per_beat)
if shift_samples > 0 and shift_samples < total_samples:
# Player drags — shift right
shifted = numpy.zeros_like(ens_buf)
shifted[shift_samples:] = ens_buf[:-shift_samples]
ens_buf = shifted
elif shift_samples < 0 and abs(shift_samples) < total_samples:
# Player rushes — shift left
shifted = numpy.zeros_like(ens_buf)
shifted[:shift_samples] = ens_buf[-shift_samples:]
ens_buf = shifted
part_buf += ens_buf / n_ensemble
# Apply effects — segmented if automation exists
auto_points = part._get_automation_points()
@@ -4507,6 +4842,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,
@@ -4515,6 +4854,22 @@ def render_score(score):
DrumSound.METAL_KICK.value: 0.0,
DrumSound.METAL_SNARE.value: 0.0,
DrumSound.METAL_HAT.value: 0.3,
# Marching — centered
DrumSound.MARCH_SNARE.value: 0.0,
DrumSound.MARCH_RIMSHOT.value: 0.0,
DrumSound.MARCH_CLICK.value: 0.0,
# Quads — spread across the field
DrumSound.QUAD_1.value: -0.3,
DrumSound.QUAD_2.value: -0.1,
DrumSound.QUAD_3.value: 0.1,
DrumSound.QUAD_4.value: 0.3,
DrumSound.QUAD_SPOCK.value: 0.0,
# Bass drums — spread wide
DrumSound.BASS_1.value: -0.5,
DrumSound.BASS_2.value: -0.25,
DrumSound.BASS_3.value: 0.0,
DrumSound.BASS_4.value: 0.25,
DrumSound.BASS_5.value: 0.5,
}
# Render all drum Parts (may be one "drums" or split into kick/snare/hats/etc.)
@@ -4530,6 +4885,7 @@ def render_score(score):
# Track last hit position per sound for choke (new hit dampens
# the previous ring on the same drum)
_last_hit_start = {}
_resonance = {} # sound_id → resonance level (0.01.0)
for hit in drum_part._drum_hits:
pos = hit.position
@@ -4562,6 +4918,35 @@ def render_score(score):
part_stereo[fade_start:start, ch] *= fade
_last_hit_start[sound_id] = start
# Cross-choke: a new hit on one sound dampens the ring of
# related sounds on the same instrument (e.g. djembe slap
# kills the bass resonance, closed hat kills open hat).
_CHOKE_GROUPS = {
# Djembe — any strike dampens the others
DrumSound.DJEMBE_BASS.value: (DrumSound.DJEMBE_TONE.value, DrumSound.DJEMBE_SLAP.value),
DrumSound.DJEMBE_TONE.value: (DrumSound.DJEMBE_BASS.value, DrumSound.DJEMBE_SLAP.value),
DrumSound.DJEMBE_SLAP.value: (DrumSound.DJEMBE_BASS.value, DrumSound.DJEMBE_TONE.value),
# Hi-hats — closed chokes open
DrumSound.CLOSED_HAT.value: (DrumSound.OPEN_HAT.value,),
DrumSound.PEDAL_HAT.value: (DrumSound.OPEN_HAT.value,),
# Cajón — slap dampens bass ring
DrumSound.CAJON_SLAP.value: (DrumSound.CAJON_BASS.value,),
DrumSound.CAJON_TAP.value: (DrumSound.CAJON_BASS.value,),
# Doumbek — tek/ka dampen dum
DrumSound.DOUMBEK_TEK.value: (DrumSound.DOUMBEK_DUM.value,),
DrumSound.DOUMBEK_KA.value: (DrumSound.DOUMBEK_DUM.value,),
}
choke_targets = _CHOKE_GROUPS.get(sound_id, ())
for target_id in choke_targets:
if target_id in _last_hit_start:
prev_start = _last_hit_start[target_id]
fade_len = min(int(SAMPLE_RATE * 0.004), max(0, start - prev_start))
if fade_len > 0 and start > 0:
fade = numpy.linspace(1.0, 0.0, fade_len).astype(numpy.float32)
fade_start = max(0, start - fade_len)
for ch in range(2):
part_stereo[fade_start:start, ch] *= fade
remaining = total_samples - start
hit_len = min(int(SAMPLE_RATE * 0.5), remaining)
wave = _render_drum_hit(hit.sound.value, hit_len)
@@ -4570,6 +4955,39 @@ def render_score(score):
vel_jitter = int(drum_humanize * 10)
vel = max(1, min(127, vel + _drum_rnd.randint(-vel_jitter, vel_jitter)))
vel_scale = vel / 127.0
# Sympathetic resonance: marching snare builds up buzz
# as hits accumulate. Each hit adds to a resonance counter
# that scales extra snare wire buzz into the sound.
_RESONANCE_SOUNDS = {
DrumSound.MARCH_SNARE.value, DrumSound.MARCH_RIMSHOT.value,
}
if sound_id in _RESONANCE_SOUNDS:
reso = _resonance.get(sound_id, 0.0)
# Decay based on gap since last hit
if sound_id in _last_hit_start:
gap_samples = start - _last_hit_start[sound_id]
gap_sec = gap_samples / SAMPLE_RATE
if gap_sec > 1.0:
reso *= 0.2
elif gap_sec > 0.5:
reso *= 0.5
elif gap_sec > 0.25:
reso *= 0.8
# Build up (caps at 0.6)
reso = min(0.6, reso + 0.08)
_resonance[sound_id] = reso
# Add sympathetic buzz proportional to resonance
if reso > 0.1:
buzz_len = min(int(SAMPLE_RATE * 0.06), hit_len)
buzz = _noise(buzz_len) * reso * 0.18
if buzz_len > 20:
bl, al = scipy.signal.butter(
2, [3000, 9000], btype='band', fs=SAMPLE_RATE)
buzz = scipy.signal.lfilter(bl, al, buzz)
buzz *= _exp_decay(buzz_len, 25)
wave[:buzz_len] = wave[:buzz_len] + buzz.astype(numpy.float32)
mono_hit = wave * vel_scale * 0.7
# Sidechain trigger — kick only
if hit.sound.value == DrumSound.KICK.value:
+1035 -17
View File
File diff suppressed because it is too large Load Diff
+6 -6
View File
@@ -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)
+212 -2
View File
@@ -4869,6 +4869,19 @@ def test_duration_values():
assert abs(Duration.TRIPLET_QUARTER.value - 2 / 3) < 1e-9
def test_duration_arithmetic():
# Multiplication
assert Duration.WHOLE * 2 == 8.0
assert 2 * Duration.HALF == 4.0
assert Duration.QUARTER * 3 == 3.0
# Division
assert Duration.WHOLE / 2 == 2.0
# Addition
assert Duration.HALF + Duration.QUARTER == 3.0
assert Duration.HALF + 1.0 == 3.0
assert 1.0 + Duration.HALF == 3.0
def test_time_signature_from_string_4_4():
ts = TimeSignature.from_string("4/4")
assert ts.beats == 4
@@ -5320,7 +5333,7 @@ def test_supersaw_wave():
@needs_portaudio
def test_all_synths_in_enum():
from pytheory.play import Synth
assert len(Synth) == 41
assert len(Synth) == 42
for s in Synth:
wave = s(440, n_samples=1000)
assert len(wave) == 1000
@@ -7142,7 +7155,7 @@ def test_score_system_propagates():
def test_synth_enum_count():
from pytheory.play import Synth
assert len(Synth) == 41
assert len(Synth) == 42
def test_all_synths_render_and_enum_match():
@@ -7151,3 +7164,200 @@ def test_all_synths_render_and_enum_match():
for s in Synth:
wave = s(440, n_samples=1000)
assert len(wave) == 1000
# ── Articulations ────────────────────────────────────────────────────────
def test_articulation_field_on_note():
from pytheory.rhythm import Note, Duration
n = Note(tone=None, duration=Duration.QUARTER, articulation="staccato")
assert n.articulation == "staccato"
def test_articulation_default_empty():
from pytheory.rhythm import Note, Duration
n = Note(tone=None, duration=Duration.QUARTER)
assert n.articulation == ""
def test_part_add_articulation():
score = pytheory.Score("4/4", bpm=120)
p = score.part("test", synth="sine")
p.add("C4", Duration.QUARTER, articulation="staccato")
p.add("D4", Duration.QUARTER, articulation="legato")
p.add("E4", Duration.QUARTER, articulation="marcato")
p.add("F4", Duration.QUARTER, articulation="tenuto")
p.add("G4", Duration.QUARTER, articulation="accent")
p.add("A4", Duration.QUARTER, articulation="fermata")
assert len(p.notes) == 6
assert p.notes[0].articulation == "staccato"
assert p.notes[5].articulation == "fermata"
@needs_portaudio
def test_articulations_render():
"""Articulations should produce audio without errors."""
from pytheory.play import render_score
score = pytheory.Score("4/4", bpm=120)
p = score.part("test", synth="sine", volume=0.3)
for art in ["", "staccato", "legato", "marcato", "tenuto", "accent", "fermata"]:
p.add("C4", Duration.QUARTER, articulation=art)
buf = render_score(score)
assert len(buf) > 0
# ── Dynamic curves ───────────────────────────────────────────────────────
def test_crescendo_adds_notes():
score = pytheory.Score("4/4", bpm=120)
p = score.part("test", synth="sine")
p.crescendo(["C4", "D4", "E4", "F4"], Duration.QUARTER,
start_vel=40, end_vel=100)
assert len(p.notes) == 4
assert p.notes[0].velocity == 40
assert p.notes[3].velocity == 100
def test_decrescendo_adds_notes():
score = pytheory.Score("4/4", bpm=120)
p = score.part("test", synth="sine")
p.decrescendo(["C4", "D4", "E4", "F4"], Duration.QUARTER,
start_vel=110, end_vel=40)
assert len(p.notes) == 4
assert p.notes[0].velocity == 110
assert p.notes[3].velocity == 40
def test_swell_velocity_shape():
score = pytheory.Score("4/4", bpm=120)
p = score.part("test", synth="sine")
p.swell(["C4", "D4", "E4", "F4", "G4"], Duration.QUARTER,
low_vel=30, peak_vel=110)
assert len(p.notes) == 5
# First and last should be near low_vel
assert p.notes[0].velocity == 30
assert p.notes[4].velocity == 30
# Middle should be at or near peak
assert p.notes[2].velocity == 110
def test_dynamics_custom_velocities():
score = pytheory.Score("4/4", bpm=120)
p = score.part("test", synth="sine")
p.dynamics(["C4", "D4", "E4"], Duration.QUARTER,
velocities=[50, 100, 75])
assert p.notes[0].velocity == 50
assert p.notes[1].velocity == 100
assert p.notes[2].velocity == 75
def test_dynamics_with_articulation():
score = pytheory.Score("4/4", bpm=120)
p = score.part("test", synth="sine")
p.crescendo(["C4", "D4"], Duration.QUARTER,
start_vel=40, end_vel=100, articulation="staccato")
assert p.notes[0].articulation == "staccato"
assert p.notes[1].articulation == "staccato"
# ── Part.hit() ───────────────────────────────────────────────────────────
def test_part_hit_adds_note():
from pytheory.rhythm import DrumSound, _DrumTone
score = pytheory.Score("4/4", bpm=120)
p = score.part("kit", synth="sine")
p.hit(DrumSound.KICK, Duration.QUARTER, velocity=100)
p.hit(DrumSound.SNARE, Duration.QUARTER, velocity=90, articulation="accent")
assert len(p.notes) == 2
assert isinstance(p.notes[0].tone, _DrumTone)
assert p.notes[0].tone.sound == DrumSound.KICK
assert p.notes[1].articulation == "accent"
@needs_portaudio
def test_part_hit_renders():
"""Part.hit() drum sounds should render through the note pipeline."""
from pytheory.rhythm import DrumSound
from pytheory.play import render_score
score = pytheory.Score("4/4", bpm=120)
p = score.part("kit", synth="sine", volume=0.5)
p.hit(DrumSound.KICK, Duration.QUARTER)
p.hit(DrumSound.SNARE, Duration.QUARTER)
p.hit(DrumSound.CLOSED_HAT, Duration.QUARTER)
p.hit(DrumSound.CRASH, Duration.QUARTER)
buf = render_score(score)
assert len(buf) > 0
# ── Part.ramp() ──────────────────────────────────────────────────────────
def test_ramp_generates_automation():
score = pytheory.Score("4/4", bpm=120)
p = score.part("test", synth="saw", lowpass=200)
p.ramp(over=4.0, lowpass=8000)
# Should have generated automation points
assert len(p._automation) > 0
# First point should be near 200, last near 8000
first_lp = p._automation[0][1].get("lowpass", 0)
last_lp = p._automation[-1][1].get("lowpass", 0)
assert first_lp < 1000 # near start
assert last_lp > 7000 # near target
def test_ramp_easing_curves():
score = pytheory.Score("4/4", bpm=120)
for curve in ["linear", "ease_in", "ease_out", "ease_in_out"]:
p = score.part(f"test_{curve}", synth="saw", lowpass=200)
p.ramp(over=4.0, curve=curve, lowpass=8000)
assert len(p._automation) > 0
def test_ramp_multiple_params():
score = pytheory.Score("4/4", bpm=120)
p = score.part("test", synth="saw", lowpass=200)
p.ramp(over=4.0, lowpass=8000, reverb=0.5)
# Should have both params in automation points
last_point = p._automation[-1][1]
assert "lowpass" in last_point
assert "reverb_mix" in last_point # mapped from "reverb"
# ── Cross-choke ──────────────────────────────────────────────────────────
def test_djembe_patterns_exist():
from pytheory.rhythm import Pattern
for name in ["djembe", "kuku", "soli", "dununba", "tiriba",
"yankadi", "djansa", "mendiani"]:
p = Pattern.preset(name)
assert p.beats > 0
assert len(p.hits) > 0
def test_djembe_fills_exist():
from pytheory.rhythm import Pattern
for name in ["djembe call", "djembe roll", "djembe break"]:
f = Pattern.fill(name)
assert f.beats == 4.0
assert len(f.hits) > 0
def test_cajon_fills_exist():
from pytheory.rhythm import Pattern
for name in ["cajon flam", "cajon rumble", "cajon breakdown"]:
f = Pattern.fill(name)
assert f.beats == 4.0
assert len(f.hits) > 0
def test_metal_fills_exist():
from pytheory.rhythm import Pattern
for name in ["metal triplet", "metal blast", "metal cascade"]:
f = Pattern.fill(name)
assert f.beats == 4.0
assert len(f.hits) > 0
# ── render_score in __all__ ──────────────────────────────────────────────
def test_render_score_exported():
assert "render_score" in pytheory.__all__
Generated
+1 -11
View File
@@ -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.2"
source = { editable = "." }
dependencies = [
{ name = "numeral" },
{ name = "scipy", version = "1.15.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" },
{ name = "scipy", version = "1.17.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" },
{ name = "sounddevice" },
@@ -721,7 +712,6 @@ docs = [
[package.metadata]
requires-dist = [
{ name = "numeral" },
{ name = "scipy" },
{ name = "sounddevice" },
]