mirror of
https://github.com/kennethreitz/pytheory.git
synced 2026-06-05 23:00:20 +00:00
Compare commits
31 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 7d678e364e | |||
| 3a8d829010 | |||
| 2a67906937 | |||
| b9dcad0454 | |||
| db9726168a | |||
| 26af923789 | |||
| 72e18a9bec | |||
| 7d56ed7a2c | |||
| 6efa4f18ce | |||
| 06fc4cabb7 | |||
| d3a93c18b3 | |||
| 0e10359236 | |||
| df00c3436d | |||
| 2f02df15b8 | |||
| a2740b8d57 | |||
| 840bfcc36c | |||
| 938c1cc132 | |||
| 9dc22db4b2 | |||
| f570e226cd | |||
| 0c5c3abedc | |||
| 35d07b984b | |||
| aec7723ee6 | |||
| b98a40297b | |||
| 9117568b74 | |||
| 11e4417c62 | |||
| 4edf1d983d | |||
| 74b07b1a8a | |||
| c9437209a7 | |||
| 92cb855a49 | |||
| f06c6f77d1 | |||
| 51bd63658f |
@@ -2,6 +2,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
|
||||
|
||||
@@ -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
@@ -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
|
||||
-----------
|
||||
|
||||
@@ -841,9 +841,11 @@ processes each section independently:
|
||||
lead.arpeggio("Gm", bars=4, pattern="updown", octaves=2)
|
||||
|
||||
Any parameter can be automated: ``lowpass``, ``lowpass_q``, ``highpass``,
|
||||
``reverb``, ``reverb_decay``, ``delay``, ``delay_time``, ``delay_feedback``,
|
||||
``distortion``, ``distortion_drive``, ``chorus``, ``phaser``, ``phaser_rate``,
|
||||
``saturation``, ``tremolo_depth``, ``tremolo_rate``, ``volume``.
|
||||
``reverb``, ``reverb_decay``, ``reverb_type``, ``delay``, ``delay_time``,
|
||||
``delay_feedback``, ``distortion``, ``distortion_drive``, ``chorus``,
|
||||
``phaser``, ``phaser_rate``, ``saturation``, ``tremolo_depth``,
|
||||
``tremolo_rate``, ``cabinet``, ``cabinet_brightness``, ``analog_drift``,
|
||||
``volume``.
|
||||
|
||||
LFO Automation
|
||||
--------------
|
||||
|
||||
+12
-1
@@ -66,6 +66,17 @@ the mix louder and punchier:
|
||||
chords.add(Chord.from_symbol(sym), Duration.WHOLE)
|
||||
play_score(score)
|
||||
|
||||
The render pipeline respects the Score's ``temperament`` and
|
||||
``reference_pitch`` settings, so Baroque or microtonal scores play back
|
||||
at the correct tuning:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
score = Score("4/4", bpm=80, temperament="meantone", reference_pitch=415.0)
|
||||
|
||||
Press **Ctrl+C** at any time during playback to stop — PyTheory catches
|
||||
``KeyboardInterrupt`` and stops audio cleanly.
|
||||
|
||||
See :doc:`sequencing` for how to build scores and parts.
|
||||
|
||||
render_score() -- Headless Rendering
|
||||
@@ -153,7 +164,7 @@ Play a drum pattern through the speakers:
|
||||
play_pattern(Pattern.preset("rock"), repeats=4, bpm=120)
|
||||
play_pattern(Pattern.preset("bossa nova"), repeats=4, bpm=140)
|
||||
|
||||
See :doc:`drums` for the full list of 58 presets and 21 fills.
|
||||
See :doc:`drums` for the full list of 80+ presets and 21 fills.
|
||||
|
||||
play_progression() -- Quick Chord Playback
|
||||
------------------------------------------
|
||||
|
||||
+203
-2
@@ -47,6 +47,18 @@ A ``Duration`` represents a note length in beats (quarter note = 1 beat):
|
||||
>>> Duration.TRIPLET_QUARTER.value
|
||||
0.6666666666666666
|
||||
|
||||
Duration supports arithmetic — multiply, divide, and add to create
|
||||
compound durations:
|
||||
|
||||
.. code-block:: pycon
|
||||
|
||||
>>> Duration.WHOLE * 2
|
||||
8.0
|
||||
>>> Duration.HALF + Duration.QUARTER
|
||||
3.0
|
||||
>>> Duration.WHOLE / 2
|
||||
2.0
|
||||
|
||||
Time Signatures
|
||||
---------------
|
||||
|
||||
@@ -399,6 +411,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
@@ -1,7 +1,7 @@
|
||||
Synthesizers
|
||||
============
|
||||
|
||||
PyTheory includes 30 built-in waveforms and 10 ADSR envelope presets.
|
||||
PyTheory includes 41 built-in waveforms and 10 ADSR envelope presets.
|
||||
Every sound is generated from scratch -- no samples or external audio
|
||||
files needed.
|
||||
|
||||
@@ -390,11 +390,11 @@ Dedicated Instrument Synths
|
||||
--------------------------
|
||||
|
||||
Beyond the classic and physical modeling waveforms, PyTheory includes
|
||||
17 dedicated instrument synths. Each one uses tailored synthesis
|
||||
31 dedicated instrument synths. Each one uses tailored synthesis
|
||||
techniques -- additive harmonics, formant shaping, body resonance
|
||||
modeling, and specialized envelopes -- to capture the character of a
|
||||
specific acoustic instrument. These are the waveforms that bring the
|
||||
total count to 30.
|
||||
total count to 41.
|
||||
|
||||
Piano Synth
|
||||
~~~~~~~~~~~
|
||||
@@ -558,6 +558,107 @@ mids, reed buzz, and brass body warmth. Four presets: ``saxophone``,
|
||||
|
||||
sax = score.part("sax", instrument="tenor_sax")
|
||||
|
||||
Pedal Steel Synth
|
||||
~~~~~~~~~~~~~~~~~
|
||||
|
||||
The Nashville crying sound — singing harmonics with slow vibrato
|
||||
and long sustain. Pairs naturally with spring reverb.
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
steel = score.part("steel", instrument="pedal_steel")
|
||||
|
||||
Theremin Synth
|
||||
~~~~~~~~~~~~~~
|
||||
|
||||
Pure sine with natural hand wobble — the eerie sci-fi sound.
|
||||
Best used with legato and glide for continuous pitch.
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
theremin = score.part("theremin", instrument="theremin")
|
||||
|
||||
Kalimba Synth
|
||||
~~~~~~~~~~~~~
|
||||
|
||||
Metal tines on a wooden body. Bright, bell-like attack with
|
||||
inharmonic overtones (modes at 1x, 2.92x, 5.4x).
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
kalimba = score.part("kalimba", instrument="kalimba")
|
||||
|
||||
Steel Drum Synth
|
||||
~~~~~~~~~~~~~~~~
|
||||
|
||||
Hammered metal pan with bright, ringing, tropical character.
|
||||
Inharmonic partials at 2.0x, 3.01x, 4.1x, 5.3x.
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
pan = score.part("pan", instrument="steel_drum")
|
||||
|
||||
Accordion Synth
|
||||
~~~~~~~~~~~~~~~
|
||||
|
||||
Musette-tuned doubled reeds — two slightly detuned reed sets
|
||||
create natural beating. Bellows pressure swell modulates amplitude.
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
acc = score.part("acc", instrument="accordion")
|
||||
|
||||
Didgeridoo Synth
|
||||
~~~~~~~~~~~~~~~~
|
||||
|
||||
Deep cylindrical drone with shifting formant overtones. The
|
||||
overtone singing effect sweeps a resonant peak between 500-1500Hz.
|
||||
Best with cave reverb.
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
didg = score.part("didg", instrument="didgeridoo")
|
||||
|
||||
Bagpipe Synth
|
||||
~~~~~~~~~~~~~
|
||||
|
||||
Bright chanter reed with constant bag pressure. All harmonics
|
||||
peaked around 3-7 (the piercing brightness). No dynamics — always ff.
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
pipes = score.part("pipes", instrument="bagpipe")
|
||||
|
||||
Banjo Synth
|
||||
~~~~~~~~~~~
|
||||
|
||||
Steel strings on a drum-head membrane body. The membrane gives
|
||||
nasal, ringy resonance with faster decay than guitar.
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
banjo = score.part("banjo", instrument="banjo")
|
||||
|
||||
Mandolin Synth
|
||||
~~~~~~~~~~~~~~
|
||||
|
||||
Paired steel strings in 4 courses — natural chorus from the
|
||||
doubled unison strings. Bright, ringing, fast attack.
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
mando = score.part("mando", instrument="mandolin")
|
||||
|
||||
Ukulele Synth
|
||||
~~~~~~~~~~~~~
|
||||
|
||||
Nylon strings on a small body. Mid-heavy resonance (no deep bass),
|
||||
softer attack than guitar, shorter sustain.
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
uke = score.part("uke", instrument="ukulele")
|
||||
|
||||
Granular Synth
|
||||
~~~~~~~~~~~~~~
|
||||
|
||||
@@ -614,7 +715,7 @@ Instrument Presets
|
||||
------------------
|
||||
|
||||
Instead of choosing synth + envelope + effects manually, use an
|
||||
instrument preset — 40+ predefined combinations that approximate real
|
||||
instrument preset — 60+ predefined combinations that approximate real
|
||||
instruments:
|
||||
|
||||
.. code-block:: python
|
||||
@@ -627,20 +728,28 @@ instruments:
|
||||
|
||||
Available instruments:
|
||||
|
||||
**Keys**: piano, electric_piano, organ, harpsichord, celesta, music_box
|
||||
**Keys**: piano, electric_piano, organ, harpsichord, celesta, music_box,
|
||||
accordion
|
||||
|
||||
**Strings**: violin, viola, cello, contrabass, string_ensemble
|
||||
|
||||
**Woodwinds**: flute, clarinet, oboe, bassoon
|
||||
**Woodwinds**: flute, clarinet, oboe, bassoon, saxophone, alto_sax,
|
||||
tenor_sax, bari_sax
|
||||
|
||||
**Brass**: trumpet, trombone, french_horn, tuba, brass_ensemble
|
||||
|
||||
**Plucked**: acoustic_guitar, electric_guitar, distorted_guitar,
|
||||
bass_guitar, upright_bass, harp, sitar, koto
|
||||
**Plucked**: acoustic_guitar, electric_guitar, clean_guitar, crunch_guitar,
|
||||
distorted_guitar, orange_crunch, metal_guitar, bass_guitar, upright_bass,
|
||||
harp, sitar, koto, banjo, mandolin, mandola, ukulele
|
||||
|
||||
**Synth**: synth_lead, synth_pad, synth_bass, acid_bass, 808_bass
|
||||
**World/Exotic**: pedal_steel, theremin, kalimba, steel_drum, didgeridoo,
|
||||
bagpipe
|
||||
|
||||
**Percussion**: vibraphone, marimba, xylophone, glockenspiel, tubular_bells
|
||||
**Synth**: synth_lead, synth_pad, synth_bass, acid_bass, 808_bass,
|
||||
granular_pad, granular_texture, vocal, choir
|
||||
|
||||
**Percussion**: vibraphone, marimba, xylophone, glockenspiel, tubular_bells,
|
||||
timpani
|
||||
|
||||
Explicit kwargs override preset defaults:
|
||||
|
||||
|
||||
+118
-4
@@ -1,10 +1,11 @@
|
||||
Musical Systems
|
||||
===============
|
||||
|
||||
PyTheory supports **six musical systems**, each with its own tone names,
|
||||
scale patterns, and centuries of tradition behind them. Every system
|
||||
maps onto the same 12-tone equal temperament backbone, so you can
|
||||
compare scales across cultures and even combine them in your own music.
|
||||
PyTheory supports **16 musical systems** — 6 core systems mapped onto
|
||||
12-tone equal temperament, plus 10 microtonal systems with their own
|
||||
native tunings. The core systems let you compare scales across cultures;
|
||||
the microtonal systems go beyond 12-TET into genuinely different pitch
|
||||
universes.
|
||||
|
||||
Western
|
||||
-------
|
||||
@@ -271,4 +272,117 @@ produce the same pitches:
|
||||
>>> do4.frequency
|
||||
261.6255653005986
|
||||
|
||||
Microtonal Systems
|
||||
------------------
|
||||
|
||||
Beyond the six 12-TET core systems, PyTheory includes 10 microtonal
|
||||
systems that use their own native tunings — more notes per octave,
|
||||
just intonation ratios, or entirely alien pitch structures.
|
||||
|
||||
Shruti (22 tones per octave)
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
The Indian 22-shruti system divides the octave into 22 unequal steps
|
||||
using just intonation ratios. These microtonal inflections are what
|
||||
give classical Indian music its characteristic expressiveness — pitches
|
||||
that fall "between the cracks" of the piano.
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
score = Score("4/4", bpm=75, system="shruti")
|
||||
|
||||
Maqam (24 tones per octave)
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
The Arabic 24-tone system adds Zalzalian quarter-tone intervals
|
||||
(derived from just intonation ratios of 11 and 13) to the standard
|
||||
12 tones. These "neutral" intervals — halfway between major and minor —
|
||||
are the soul of maqam music.
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
score = Score("4/4", bpm=90, system="maqam")
|
||||
|
||||
Slendro (5-TET)
|
||||
~~~~~~~~~~~~~~~~
|
||||
|
||||
The Javanese slendro scale — 5 equal divisions of the octave. Each
|
||||
step is 240 cents, wider than any Western interval. Ethereal and
|
||||
floating.
|
||||
|
||||
Pelog (9-TET)
|
||||
~~~~~~~~~~~~~
|
||||
|
||||
Approximation of the Javanese pelog tuning as 9 equal divisions of
|
||||
the octave.
|
||||
|
||||
Thai (7-TET)
|
||||
~~~~~~~~~~~~~
|
||||
|
||||
Thai classical music divides the octave into 7 equal steps of ~171
|
||||
cents each — every interval is the same size.
|
||||
|
||||
Makam (53-TET)
|
||||
~~~~~~~~~~~~~~
|
||||
|
||||
Turkish makam music uses 53 equal divisions of the octave — fine
|
||||
enough to approximate virtually any just interval. The system that
|
||||
underlies Ottoman classical music.
|
||||
|
||||
Carnatic (72-TET)
|
||||
~~~~~~~~~~~~~~~~~
|
||||
|
||||
South Indian Carnatic music theory describes 72 melakarta ragas.
|
||||
The 72-TET system provides enough resolution to represent all the
|
||||
microtonal inflections of Carnatic practice.
|
||||
|
||||
19-TET and 31-TET
|
||||
~~~~~~~~~~~~~~~~~~
|
||||
|
||||
Extended equal temperaments that offer better approximations of
|
||||
just intonation intervals than 12-TET. 19-TET has excellent major
|
||||
thirds; 31-TET closely matches quarter-comma meantone.
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
score = Score("4/4", bpm=100, system="19-tet")
|
||||
|
||||
Bohlen-Pierce (13 equal divisions of the tritave)
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
A genuinely alien tuning system — 13 equal divisions of the
|
||||
**tritave** (3:1 ratio) instead of the octave (2:1). No octaves, no
|
||||
fifths, built on 3:5:7 harmonics. Used by experimental composers.
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
score = Score("4/4", bpm=100, system="bohlen-pierce")
|
||||
|
||||
The TET() Factory
|
||||
~~~~~~~~~~~~~~~~~
|
||||
|
||||
Create any equal temperament on the fly with the ``TET()`` factory:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
from pytheory import TET
|
||||
|
||||
edo19 = TET(19) # 19-tone equal temperament
|
||||
edo31 = TET(31) # 31-tone equal temperament
|
||||
score = Score("4/4", bpm=100, system=edo19)
|
||||
|
||||
Tone names in custom TET systems are integers (0, 1, 2, ..., n-1).
|
||||
|
||||
System.tone() Method
|
||||
~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
Any system can create a Tone directly:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
from pytheory import SYSTEMS
|
||||
|
||||
western = SYSTEMS["western"]
|
||||
c4 = western.tone("C", octave=4)
|
||||
|
||||
Music is universal, but every culture hears it differently. These systems are different maps of the same territory -- explore one you've never played in before and see what you find.
|
||||
|
||||
@@ -357,6 +357,45 @@ every tone knows its enharmonic spelling:
|
||||
>>> Tone.from_string("C4", system="western").enharmonic is None
|
||||
True
|
||||
|
||||
Extended Enharmonics
|
||||
~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
PyTheory supports the full range of enharmonic spellings used in real
|
||||
music theory:
|
||||
|
||||
- **Cb** and **Fb** — musically valid flats (Cb = B, Fb = E)
|
||||
- **E#** and **B#** — musically valid sharps (E# = F, B# = C)
|
||||
- **Double sharps** (``##`` or ``x``) — e.g. F## = G
|
||||
- **Double flats** (``bb``) — e.g. Dbb = C
|
||||
- **Unicode symbols** — ``♯`` (sharp), ``♭`` (flat), ``𝄪`` (double sharp),
|
||||
``𝄫`` (double flat) are all recognized and normalized to ASCII
|
||||
|
||||
.. code-block:: pycon
|
||||
|
||||
>>> Tone.from_string("Cb4") # resolves to B3 (octave boundary fix)
|
||||
<Tone B3>
|
||||
>>> Tone.from_string("B#4") # resolves to C5 (octave boundary fix)
|
||||
<Tone C5>
|
||||
>>> Tone.from_string("E#4") # resolves to F4
|
||||
<Tone F4>
|
||||
>>> Tone.from_string("Fb4") # resolves to E4
|
||||
<Tone E4>
|
||||
|
||||
The octave boundary is correctly handled: B# crosses up to the next
|
||||
octave (B#4 = C5), and Cb crosses down (Cb4 = B3), matching standard
|
||||
scientific pitch notation where the octave number increments at C.
|
||||
|
||||
Tone Validation
|
||||
~~~~~~~~~~~~~~~
|
||||
|
||||
Tones are validated on construction — if a tone name is not recognized
|
||||
in its system, a ``ValueError`` is raised:
|
||||
|
||||
.. code-block:: pycon
|
||||
|
||||
>>> Tone.from_string("X4") # not a valid tone name
|
||||
ValueError: ...
|
||||
|
||||
The Circle of Fifths
|
||||
--------------------
|
||||
|
||||
|
||||
+18
-14
@@ -18,8 +18,8 @@ Theory
|
||||
------
|
||||
|
||||
The theory layer works everywhere Python runs — no audio setup needed.
|
||||
Tones, scales, chords, keys, intervals, harmony, 6 musical systems,
|
||||
25 instruments:
|
||||
Tones, scales, chords, keys, intervals, harmony, 16 musical systems,
|
||||
60+ instruments:
|
||||
|
||||
.. code-block:: pycon
|
||||
|
||||
@@ -72,25 +72,29 @@ every time::
|
||||
What's Inside
|
||||
-------------
|
||||
|
||||
- **Theory** — tones, scales (40+ across 6 systems), chords (17 types),
|
||||
- **Theory** — tones, scales (40+ across 16 systems), chords (17 types),
|
||||
keys, Roman numeral analysis, figured bass, pitch class sets (Forte
|
||||
numbers), scale recommendation, modulation, voice leading
|
||||
numbers), scale recommendation, modulation, voice leading, enharmonic
|
||||
support (Cb, Fb, E#, B#, double sharps/flats, unicode symbols)
|
||||
- **Sequencing** — Score, Parts, arpeggiator, legato/glide, velocity,
|
||||
swing, humanize, tempo changes, song sections with repeat
|
||||
swing, humanize, tempo changes, song sections with repeat, strumming,
|
||||
pitch bends (3 types), rolls, tuning systems (TET factory, 4
|
||||
temperaments, reference_pitch)
|
||||
- **Synthesis** — 41 waveforms (including Karplus-Strong pluck, Hammond organ,
|
||||
bowed string, and 14 dedicated instrument synths), 10 envelopes, 40+
|
||||
instrument presets, configurable FM, sub-oscillator, noise layer, filter
|
||||
envelope, velocity-to-brightness, analog oscillator drift, detune, stereo
|
||||
pan/spread, strumming, 80+ drum patterns (stereo panned, including world
|
||||
percussion), 21 fills
|
||||
bowed string, granular, vocal/formant, and 31 dedicated instrument synths),
|
||||
10 envelopes, 60+ instrument presets, configurable FM, sub-oscillator,
|
||||
noise layer, filter envelope, velocity-to-brightness, analog oscillator
|
||||
drift, detune, stereo pan/spread, 80+ drum patterns (stereo panned,
|
||||
including world percussion and cajón), 21 fills, 11 microtonal systems
|
||||
- **Effects** — reverb (algorithmic + 7 convolution IRs, stereo), delay,
|
||||
lowpass/highpass (with resonance), distortion, cabinet simulation,
|
||||
lowpass/highpass (with resonance), distortion, guitar cabinet simulation,
|
||||
saturation, chorus, phaser, tremolo, analog drift, sidechain compression,
|
||||
automation, LFOs. Master bus compressor/limiter
|
||||
- **Instruments** — 49 presets with fingering generation, guitar strumming,
|
||||
pitch bends
|
||||
- **Instruments** — 60+ presets with fingering generation, guitar strumming,
|
||||
pitch bends, note choking
|
||||
- **Output** — stereo playback, WAV export, MIDI import/export
|
||||
- **Interface** — REPL with tab completion, CLI (15 commands), ``pytheory demo``
|
||||
- **Interface** — REPL with tab completion, CLI (15 commands), ``pytheory demo``,
|
||||
KeyboardInterrupt handling for clean stop
|
||||
- **AI-friendly** — Claude Code can compose
|
||||
and play music through PyTheory from natural language
|
||||
|
||||
|
||||
+968
-1
@@ -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":
|
||||
|
||||
@@ -0,0 +1,69 @@
|
||||
"""Sprunki Simon Phase 1 — melody reference.
|
||||
|
||||
Notes transcribed from MIDI. Use as a base for arrangements.
|
||||
|
||||
Usage:
|
||||
python examples/sprunki.py
|
||||
"""
|
||||
|
||||
import sounddevice as sd
|
||||
|
||||
from pytheory import Score, Duration
|
||||
from pytheory.play import render_score, SAMPLE_RATE
|
||||
|
||||
|
||||
def sprunki_simon():
|
||||
score = Score("4/4", bpm=200)
|
||||
|
||||
lead = score.part("lead", synth="square", envelope="pluck", volume=0.5,
|
||||
lowpass=4500, detune=3, reverb=0.1)
|
||||
|
||||
# Phrase A
|
||||
lead.add("E4", 1.0)
|
||||
lead.add("G4", 1.0)
|
||||
lead.rest(1.5)
|
||||
lead.add("A4", 0.5)
|
||||
lead.add("B4", 1.0)
|
||||
lead.add("A4", 1.0)
|
||||
lead.add("G4", 1.0)
|
||||
lead.add("D4", 1.0)
|
||||
|
||||
# Phrase B
|
||||
lead.add("E4", 1.0)
|
||||
lead.add("G4", 1.0)
|
||||
lead.rest(1.5)
|
||||
lead.add("A4", 0.5)
|
||||
lead.add("D4", 2.0)
|
||||
lead.add("B3", 1.0)
|
||||
lead.add("A3", 0.5)
|
||||
lead.add("D4", 0.5)
|
||||
|
||||
# Phrase C
|
||||
lead.add("E4", 1.0)
|
||||
lead.add("G4", 1.0)
|
||||
lead.rest(1.5)
|
||||
lead.add("A4", 0.5)
|
||||
lead.add("B4", 1.0)
|
||||
lead.add("A4", 1.0)
|
||||
lead.add("G4", 1.0)
|
||||
lead.add("B4", 1.0)
|
||||
|
||||
# Phrase D
|
||||
lead.add("A4", 2.0)
|
||||
lead.add("G4", 1.0)
|
||||
lead.add("E4", 1.0)
|
||||
lead.add("B3", 2.0)
|
||||
lead.add("D4", 2.0)
|
||||
|
||||
return score
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
score = sprunki_simon()
|
||||
print(" Sprunki Simon Phase 1")
|
||||
try:
|
||||
buf = render_score(score)
|
||||
sd.play(buf, SAMPLE_RATE)
|
||||
sd.wait()
|
||||
except KeyboardInterrupt:
|
||||
sd.stop()
|
||||
+1
-2
@@ -1,6 +1,6 @@
|
||||
[project]
|
||||
name = "pytheory"
|
||||
version = "0.36.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",
|
||||
]
|
||||
|
||||
@@ -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",
|
||||
]
|
||||
|
||||
@@ -2,6 +2,43 @@ import math
|
||||
|
||||
REFERENCE_A = 440
|
||||
|
||||
# ── Roman numeral helpers (replaces `numeral` package) ───────────────────
|
||||
|
||||
_ROMAN_MAP = [
|
||||
(1000, "M"), (900, "CM"), (500, "D"), (400, "CD"),
|
||||
(100, "C"), (90, "XC"), (50, "L"), (40, "XL"),
|
||||
(10, "X"), (9, "IX"), (5, "V"), (4, "IV"), (1, "I"),
|
||||
]
|
||||
|
||||
_ROMAN_VALUES = {
|
||||
"I": 1, "V": 5, "X": 10, "L": 50, "C": 100, "D": 500, "M": 1000,
|
||||
}
|
||||
|
||||
|
||||
def int2roman(n: int) -> str:
|
||||
"""Convert an integer to an uppercase Roman numeral string."""
|
||||
result = []
|
||||
for value, numeral in _ROMAN_MAP:
|
||||
while n >= value:
|
||||
result.append(numeral)
|
||||
n -= value
|
||||
return "".join(result)
|
||||
|
||||
|
||||
def roman2int(s: str) -> int:
|
||||
"""Convert a Roman numeral string (case-insensitive) to an integer."""
|
||||
s = s.upper()
|
||||
total = 0
|
||||
prev = 0
|
||||
for ch in reversed(s):
|
||||
val = _ROMAN_VALUES.get(ch, 0)
|
||||
if val < prev:
|
||||
total -= val
|
||||
else:
|
||||
total += val
|
||||
prev = val
|
||||
return total
|
||||
|
||||
# Index of C in the Western tone list (A=0, A#=1, B=2, C=3, ...).
|
||||
# Scientific pitch notation changes octave at C, not A, so this offset
|
||||
# is needed for all octave arithmetic.
|
||||
|
||||
+2
-2
@@ -849,7 +849,7 @@ class Chord:
|
||||
>>> Chord([D4, F4, A4]).analyze("C")
|
||||
'ii'
|
||||
"""
|
||||
import numeral as numeral_mod
|
||||
from ._statics import int2roman
|
||||
from .scales import TonedScale
|
||||
from .systems import SYSTEMS
|
||||
from .tones import Tone
|
||||
@@ -874,7 +874,7 @@ class Chord:
|
||||
scale_names = [t.name for t in scale.tones[:-1]]
|
||||
|
||||
def _build_numeral(root, quality, degree_idx, prefix=""):
|
||||
numeral_str = numeral_mod.int2roman(degree_idx + 1, only_ascii=True)
|
||||
numeral_str = int2roman(degree_idx + 1)
|
||||
suffix = ""
|
||||
if "minor" in quality:
|
||||
numeral_str = numeral_str.lower()
|
||||
|
||||
+474
-56
@@ -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.0–1.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
File diff suppressed because it is too large
Load Diff
+6
-6
@@ -2,8 +2,6 @@ from __future__ import annotations
|
||||
|
||||
from typing import Optional, Union
|
||||
|
||||
import numeral
|
||||
|
||||
from .systems import SYSTEMS, System
|
||||
from .tones import Tone
|
||||
|
||||
@@ -49,7 +47,8 @@ class Scale:
|
||||
def __repr__(self) -> str:
|
||||
r = []
|
||||
for (i, tone) in enumerate(self.tones):
|
||||
degree = numeral.int2roman(i + 1, only_ascii=True)
|
||||
from ._statics import int2roman
|
||||
degree = int2roman(i + 1)
|
||||
r += [f"{degree}={tone.full_name}"]
|
||||
|
||||
r = " ".join(r)
|
||||
@@ -200,7 +199,7 @@ class Scale:
|
||||
>>> scale.progression("I", "IV", "V", "I")
|
||||
[<Chord (C,E,G)>, <Chord (F,A,C)>, <Chord (G,B,D)>, <Chord (C,E,G)>]
|
||||
"""
|
||||
import numeral as numeral_mod
|
||||
from ._statics import roman2int
|
||||
chords = []
|
||||
for num in numerals:
|
||||
is_seventh = num.endswith("7")
|
||||
@@ -213,7 +212,7 @@ class Scale:
|
||||
elif clean.startswith("#") and len(clean) > 1:
|
||||
clean = clean[1:]
|
||||
flat_offset = 1 # one semitone up
|
||||
degree = numeral_mod.roman2int(clean.upper()) - 1
|
||||
degree = roman2int(clean.upper()) - 1
|
||||
if is_seventh:
|
||||
chord = self.seventh(degree)
|
||||
else:
|
||||
@@ -406,7 +405,8 @@ class Scale:
|
||||
if isinstance(item, str):
|
||||
degrees = []
|
||||
for (i, tone) in enumerate(self.tones):
|
||||
degrees.append(numeral.int2roman(i + 1, only_ascii=True))
|
||||
from ._statics import int2roman
|
||||
degrees.append(int2roman(i + 1))
|
||||
|
||||
if item in degrees:
|
||||
item = degrees.index(item)
|
||||
|
||||
+212
-2
@@ -4869,6 +4869,19 @@ def test_duration_values():
|
||||
assert abs(Duration.TRIPLET_QUARTER.value - 2 / 3) < 1e-9
|
||||
|
||||
|
||||
def test_duration_arithmetic():
|
||||
# Multiplication
|
||||
assert Duration.WHOLE * 2 == 8.0
|
||||
assert 2 * Duration.HALF == 4.0
|
||||
assert Duration.QUARTER * 3 == 3.0
|
||||
# Division
|
||||
assert Duration.WHOLE / 2 == 2.0
|
||||
# Addition
|
||||
assert Duration.HALF + Duration.QUARTER == 3.0
|
||||
assert Duration.HALF + 1.0 == 3.0
|
||||
assert 1.0 + Duration.HALF == 3.0
|
||||
|
||||
|
||||
def test_time_signature_from_string_4_4():
|
||||
ts = TimeSignature.from_string("4/4")
|
||||
assert ts.beats == 4
|
||||
@@ -5320,7 +5333,7 @@ def test_supersaw_wave():
|
||||
@needs_portaudio
|
||||
def test_all_synths_in_enum():
|
||||
from pytheory.play import Synth
|
||||
assert len(Synth) == 41
|
||||
assert len(Synth) == 42
|
||||
for s in Synth:
|
||||
wave = s(440, n_samples=1000)
|
||||
assert len(wave) == 1000
|
||||
@@ -7142,7 +7155,7 @@ def test_score_system_propagates():
|
||||
|
||||
def test_synth_enum_count():
|
||||
from pytheory.play import Synth
|
||||
assert len(Synth) == 41
|
||||
assert len(Synth) == 42
|
||||
|
||||
|
||||
def test_all_synths_render_and_enum_match():
|
||||
@@ -7151,3 +7164,200 @@ def test_all_synths_render_and_enum_match():
|
||||
for s in Synth:
|
||||
wave = s(440, n_samples=1000)
|
||||
assert len(wave) == 1000
|
||||
|
||||
|
||||
# ── Articulations ────────────────────────────────────────────────────────
|
||||
|
||||
def test_articulation_field_on_note():
|
||||
from pytheory.rhythm import Note, Duration
|
||||
n = Note(tone=None, duration=Duration.QUARTER, articulation="staccato")
|
||||
assert n.articulation == "staccato"
|
||||
|
||||
|
||||
def test_articulation_default_empty():
|
||||
from pytheory.rhythm import Note, Duration
|
||||
n = Note(tone=None, duration=Duration.QUARTER)
|
||||
assert n.articulation == ""
|
||||
|
||||
|
||||
def test_part_add_articulation():
|
||||
score = pytheory.Score("4/4", bpm=120)
|
||||
p = score.part("test", synth="sine")
|
||||
p.add("C4", Duration.QUARTER, articulation="staccato")
|
||||
p.add("D4", Duration.QUARTER, articulation="legato")
|
||||
p.add("E4", Duration.QUARTER, articulation="marcato")
|
||||
p.add("F4", Duration.QUARTER, articulation="tenuto")
|
||||
p.add("G4", Duration.QUARTER, articulation="accent")
|
||||
p.add("A4", Duration.QUARTER, articulation="fermata")
|
||||
assert len(p.notes) == 6
|
||||
assert p.notes[0].articulation == "staccato"
|
||||
assert p.notes[5].articulation == "fermata"
|
||||
|
||||
|
||||
@needs_portaudio
|
||||
def test_articulations_render():
|
||||
"""Articulations should produce audio without errors."""
|
||||
from pytheory.play import render_score
|
||||
score = pytheory.Score("4/4", bpm=120)
|
||||
p = score.part("test", synth="sine", volume=0.3)
|
||||
for art in ["", "staccato", "legato", "marcato", "tenuto", "accent", "fermata"]:
|
||||
p.add("C4", Duration.QUARTER, articulation=art)
|
||||
buf = render_score(score)
|
||||
assert len(buf) > 0
|
||||
|
||||
|
||||
# ── Dynamic curves ───────────────────────────────────────────────────────
|
||||
|
||||
def test_crescendo_adds_notes():
|
||||
score = pytheory.Score("4/4", bpm=120)
|
||||
p = score.part("test", synth="sine")
|
||||
p.crescendo(["C4", "D4", "E4", "F4"], Duration.QUARTER,
|
||||
start_vel=40, end_vel=100)
|
||||
assert len(p.notes) == 4
|
||||
assert p.notes[0].velocity == 40
|
||||
assert p.notes[3].velocity == 100
|
||||
|
||||
|
||||
def test_decrescendo_adds_notes():
|
||||
score = pytheory.Score("4/4", bpm=120)
|
||||
p = score.part("test", synth="sine")
|
||||
p.decrescendo(["C4", "D4", "E4", "F4"], Duration.QUARTER,
|
||||
start_vel=110, end_vel=40)
|
||||
assert len(p.notes) == 4
|
||||
assert p.notes[0].velocity == 110
|
||||
assert p.notes[3].velocity == 40
|
||||
|
||||
|
||||
def test_swell_velocity_shape():
|
||||
score = pytheory.Score("4/4", bpm=120)
|
||||
p = score.part("test", synth="sine")
|
||||
p.swell(["C4", "D4", "E4", "F4", "G4"], Duration.QUARTER,
|
||||
low_vel=30, peak_vel=110)
|
||||
assert len(p.notes) == 5
|
||||
# First and last should be near low_vel
|
||||
assert p.notes[0].velocity == 30
|
||||
assert p.notes[4].velocity == 30
|
||||
# Middle should be at or near peak
|
||||
assert p.notes[2].velocity == 110
|
||||
|
||||
|
||||
def test_dynamics_custom_velocities():
|
||||
score = pytheory.Score("4/4", bpm=120)
|
||||
p = score.part("test", synth="sine")
|
||||
p.dynamics(["C4", "D4", "E4"], Duration.QUARTER,
|
||||
velocities=[50, 100, 75])
|
||||
assert p.notes[0].velocity == 50
|
||||
assert p.notes[1].velocity == 100
|
||||
assert p.notes[2].velocity == 75
|
||||
|
||||
|
||||
def test_dynamics_with_articulation():
|
||||
score = pytheory.Score("4/4", bpm=120)
|
||||
p = score.part("test", synth="sine")
|
||||
p.crescendo(["C4", "D4"], Duration.QUARTER,
|
||||
start_vel=40, end_vel=100, articulation="staccato")
|
||||
assert p.notes[0].articulation == "staccato"
|
||||
assert p.notes[1].articulation == "staccato"
|
||||
|
||||
|
||||
# ── Part.hit() ───────────────────────────────────────────────────────────
|
||||
|
||||
def test_part_hit_adds_note():
|
||||
from pytheory.rhythm import DrumSound, _DrumTone
|
||||
score = pytheory.Score("4/4", bpm=120)
|
||||
p = score.part("kit", synth="sine")
|
||||
p.hit(DrumSound.KICK, Duration.QUARTER, velocity=100)
|
||||
p.hit(DrumSound.SNARE, Duration.QUARTER, velocity=90, articulation="accent")
|
||||
assert len(p.notes) == 2
|
||||
assert isinstance(p.notes[0].tone, _DrumTone)
|
||||
assert p.notes[0].tone.sound == DrumSound.KICK
|
||||
assert p.notes[1].articulation == "accent"
|
||||
|
||||
|
||||
@needs_portaudio
|
||||
def test_part_hit_renders():
|
||||
"""Part.hit() drum sounds should render through the note pipeline."""
|
||||
from pytheory.rhythm import DrumSound
|
||||
from pytheory.play import render_score
|
||||
score = pytheory.Score("4/4", bpm=120)
|
||||
p = score.part("kit", synth="sine", volume=0.5)
|
||||
p.hit(DrumSound.KICK, Duration.QUARTER)
|
||||
p.hit(DrumSound.SNARE, Duration.QUARTER)
|
||||
p.hit(DrumSound.CLOSED_HAT, Duration.QUARTER)
|
||||
p.hit(DrumSound.CRASH, Duration.QUARTER)
|
||||
buf = render_score(score)
|
||||
assert len(buf) > 0
|
||||
|
||||
|
||||
# ── Part.ramp() ──────────────────────────────────────────────────────────
|
||||
|
||||
def test_ramp_generates_automation():
|
||||
score = pytheory.Score("4/4", bpm=120)
|
||||
p = score.part("test", synth="saw", lowpass=200)
|
||||
p.ramp(over=4.0, lowpass=8000)
|
||||
# Should have generated automation points
|
||||
assert len(p._automation) > 0
|
||||
# First point should be near 200, last near 8000
|
||||
first_lp = p._automation[0][1].get("lowpass", 0)
|
||||
last_lp = p._automation[-1][1].get("lowpass", 0)
|
||||
assert first_lp < 1000 # near start
|
||||
assert last_lp > 7000 # near target
|
||||
|
||||
|
||||
def test_ramp_easing_curves():
|
||||
score = pytheory.Score("4/4", bpm=120)
|
||||
for curve in ["linear", "ease_in", "ease_out", "ease_in_out"]:
|
||||
p = score.part(f"test_{curve}", synth="saw", lowpass=200)
|
||||
p.ramp(over=4.0, curve=curve, lowpass=8000)
|
||||
assert len(p._automation) > 0
|
||||
|
||||
|
||||
def test_ramp_multiple_params():
|
||||
score = pytheory.Score("4/4", bpm=120)
|
||||
p = score.part("test", synth="saw", lowpass=200)
|
||||
p.ramp(over=4.0, lowpass=8000, reverb=0.5)
|
||||
# Should have both params in automation points
|
||||
last_point = p._automation[-1][1]
|
||||
assert "lowpass" in last_point
|
||||
assert "reverb_mix" in last_point # mapped from "reverb"
|
||||
|
||||
|
||||
# ── Cross-choke ──────────────────────────────────────────────────────────
|
||||
|
||||
def test_djembe_patterns_exist():
|
||||
from pytheory.rhythm import Pattern
|
||||
for name in ["djembe", "kuku", "soli", "dununba", "tiriba",
|
||||
"yankadi", "djansa", "mendiani"]:
|
||||
p = Pattern.preset(name)
|
||||
assert p.beats > 0
|
||||
assert len(p.hits) > 0
|
||||
|
||||
|
||||
def test_djembe_fills_exist():
|
||||
from pytheory.rhythm import Pattern
|
||||
for name in ["djembe call", "djembe roll", "djembe break"]:
|
||||
f = Pattern.fill(name)
|
||||
assert f.beats == 4.0
|
||||
assert len(f.hits) > 0
|
||||
|
||||
|
||||
def test_cajon_fills_exist():
|
||||
from pytheory.rhythm import Pattern
|
||||
for name in ["cajon flam", "cajon rumble", "cajon breakdown"]:
|
||||
f = Pattern.fill(name)
|
||||
assert f.beats == 4.0
|
||||
assert len(f.hits) > 0
|
||||
|
||||
|
||||
def test_metal_fills_exist():
|
||||
from pytheory.rhythm import Pattern
|
||||
for name in ["metal triplet", "metal blast", "metal cascade"]:
|
||||
f = Pattern.fill(name)
|
||||
assert f.beats == 4.0
|
||||
assert len(f.hits) > 0
|
||||
|
||||
|
||||
# ── render_score in __all__ ──────────────────────────────────────────────
|
||||
|
||||
def test_render_score_exported():
|
||||
assert "render_score" in pytheory.__all__
|
||||
|
||||
@@ -486,14 +486,6 @@ wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/d3/ac/686789b9145413f1a61878c407210e41bfdb097976864e0913078b24098c/myst_parser-5.0.0-py3-none-any.whl", hash = "sha256:ab31e516024918296e169139072b81592336f2fef55b8986aa31c9f04b5f7211", size = 84533, upload-time = "2026-01-15T09:08:16.788Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "numeral"
|
||||
version = "0.1.0.17"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/17/0d/ac6a186e169fcbdfea316f78fb5e34981bcf8d5c1d7cc8b6581f597e1e4c/numeral-0.1.0.17-py2.py3-none-any.whl", hash = "sha256:7dff0c1efb9b3655c9c1dc93b4666993741b15abcac0dc01dcb96b21cc20f6ae", size = 22066, upload-time = "2020-04-12T08:24:59.129Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "numpy"
|
||||
version = "2.2.6"
|
||||
@@ -698,10 +690,9 @@ wheels = [
|
||||
|
||||
[[package]]
|
||||
name = "pytheory"
|
||||
version = "0.36.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" },
|
||||
]
|
||||
|
||||
Reference in New Issue
Block a user