Compare commits

...

13 Commits

Author SHA1 Message Date
kennethreitz 33b2e82594 v0.34.1: Pitch bends, updated docs and songs
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-27 02:15:32 -04:00
kennethreitz 9f8dd0006d Pitch bends, updated docs, songs with new instruments
- Pitch bend: part.add("C4", bend=2, bend_type="smooth") bends up
  a whole step. Three types: smooth (log/perceptual), linear, late
  (hold then bend — blues style).
- Updated songs.py: use dedicated instrument synths (piano_synth,
  flute_synth, trumpet_synth, etc.) instead of generic waveforms
- Updated docs: synths.rst (27 waveforms, instrument synths section),
  effects.rst (cabinet sim, analog drift, updated signal chain),
  drums.rst (world percussion: tabla, dhol, dholak, mridangam,
  djembe, metal kit), index.rst (feature counts)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-27 02:14:45 -04:00
kennethreitz 417f7f74a3 19 new tests: instrument synths, cabinet, analog, strumming, world drums
Tests for: all 14 dedicated synth waveforms, piano brightness scaling,
cabinet sim frequency reduction, analog drift rendering, strum with/
without fretboard, strum direction, all 6 tabla sounds, dhol/mridangam/
djembe/metal kit sounds, 20 world drum pattern presets, guitar preset
cabinet sim. 838 tests total.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-27 02:11:48 -04:00
kennethreitz cd6f814049 Update changelog with vibrato tuning
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-27 02:08:37 -04:00
kennethreitz 83fcdb0a09 Reduce vibrato depth on flute, oboe, trumpet, cello
All cut from 0.003-0.004 to 0.0015-0.002 — less wobbly in
ensemble context, more natural.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-27 02:08:17 -04:00
kennethreitz aa21bf0f2a v0.34.0: 27 synth waveforms, world drums, guitar strumming
16 dedicated instrument synths, speaker cab sim, analog drift,
strumming with fretboard lookup, dhol/dholak/mridangam/djembe/
metal kit with 22 patterns, 5 new demo moods.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-27 02:00:57 -04:00
kennethreitz e7e35ad4e4 5 more dedicated synths: oboe, harpsichord, cello, harp, upright bass
- Oboe: double reed buzz + conical bore (all harmonics, peaked 3-5)
- Harpsichord: KS with quill chiff, bright metallic pluck
- Cello: deep bowed string with 250/500Hz body resonance
- Harp: soft KS pluck with large soundboard bloom
- Upright bass: thick string pizzicato with wooden body resonance
- 27 synth waveforms total

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-27 01:59:20 -04:00
kennethreitz 503dbce937 6 dedicated instrument synths: piano, bass, flute, trumpet, clarinet, marimba
- Piano: hammer strike + detuned strings + inharmonicity + soundboard
- Bass guitar: heavy KS with thick string damping + low-mid pickup
- Flute: breath noise + tube resonance + developing vibrato
- Trumpet: lip buzz harmonics + brass bell resonance + vibrato
- Clarinet: odd harmonics (cylindrical bore) + reed noise
- Marimba: inharmonic bar modes (1x, 4x, 9.2x) + resonator tube
- 22 synth waveforms total

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-27 01:44:00 -04:00
kennethreitz c6bbfae7e6 Acoustic guitar synth with body resonance, fix strum
- New acoustic_guitar_synth: Karplus-Strong with wooden body
  resonance (3 formant peaks at 110/250/500 Hz), warmer initial
  noise, gentle rolloff. Sounds woody, not harsh.
- Strum renders as a single chord hit — no more exposed grace
  notes that sounded digital. Clean, full chord sound.
- 16 synth waveforms total

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-27 01:38:35 -04:00
kennethreitz 64ef7f0803 Add analog oscillator drift for synth warmth
Per-note random pitch wobble (gaussian, ±cents scaled by analog param)
simulates analog oscillator instability. Applied to synth_lead (0.3),
synth_pad (0.4), synth_bass (0.2), acid_bass (0.3), electric_piano
(0.2), organ (0.15). Subtle enough to add life without sounding
out of tune.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-27 01:32:03 -04:00
kennethreitz 406e5d7e54 Electric guitar synth, cab sim, strumming, world drums, metal kit
- Electric guitar: Karplus-Strong + magnetic pickup comb filter
- Cabinet simulation: speaker rolloff + presence bump (tames fizz)
- 6 guitar presets: clean, crunch, distorted, orange, metal
- Part.strum(): fretboard fingering lookup with down/up strumming
- Sitar synth: jawari buzz + chikari sympathetic strings
- Dhol, dholak, mridangam, djembe synthesis (membrane noise)
- Metal drum kit (kick click, bright snare, tight hats)
- 11 world patterns + 4 metal patterns + 7 tabla patterns

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-27 01:25:53 -04:00
kennethreitz 267b7284ba Add dhol, dholak, mridangam, djembe drums + 11 world patterns
Drum synthesis:
- Dhol: dagga (heavy bass), tilli (treble crack), both
- Dholak: ge (bass palm), na (treble fingers), tit (light tap)
- Mridangam: tham (clay body bass), nam (rich overtone ring),
  din (both heads), tha (muted)
- Djembe: bass (center palm), tone (edge ring), slap (sharp crack)
All with bandpass-filtered membrane noise for drum head character.

Patterns:
- Dhol: bhangra, dhol chaal
- Dholak: qawwali, dholak folk
- Mridangam: adi talam, mridangam korvai
- Djembe: djembe (standard), kuku, soli

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-27 00:59:27 -04:00
kennethreitz 9b62b56120 Sitar synth, tabla drums with wood/metal shells, 7 tabla patterns
- Sitar synth: Karplus-Strong with gentle jawari bridge buzz,
  variable damping (bright attack fades to warm sustain), chikari
  sympathetic string shimmer
- Tabla: 6 synthesized strokes (Na, Tin, Ge, Dha, Tit, Ke) with
  goatskin membrane noise (bandpass filtered), wooden shell resonance
  on dayan, copper/metal shell resonance on bayan
- 7 tabla patterns: teental (16 beats), jhaptaal (10), rupak (7),
  dadra (6), keherwa (8), tabla solo, tiri kita (fast 16th-note)
- Sitar instrument preset with proper lowpass

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-27 00:54:19 -04:00
13 changed files with 2603 additions and 230 deletions
+41
View File
@@ -2,6 +2,47 @@
All notable changes to PyTheory are documented here.
## 0.34.0
- **16 dedicated instrument synths** — physical modeling and specialized
synthesis for: piano (hammer + steel strings + soundboard), bass guitar
(thick KS + pickup), flute (breath + tube resonance), trumpet (lip buzz
+ bell), clarinet (odd harmonics + reed), oboe (double reed + conical
bore), marimba (inharmonic bar modes), harpsichord (quill pluck),
cello (deep bowed + body), harp (soft pluck + soundboard bloom),
upright bass (pizzicato + wooden body), acoustic guitar (KS + body
resonance), electric guitar (KS + pickup comb filter), sitar (jawari
+ chikari), plus organ and bowed strings
- **Speaker cabinet simulation** — tames distorted guitar fizz
- **Guitar strumming** — `Part.strum("Am")` with fretboard lookup
- **Analog oscillator drift** — subtle per-note pitch wobble on synth presets
- **World percussion:** dhol, dholak, mridangam, djembe, metal kit
with 22 new drum patterns
- **Piano improvements:** brightness scales with pitch, two-stage decay,
hammer impact with felt character
- **Vibrato tuning:** reduced across flute, oboe, trumpet, cello for
smoother ensemble sound
- 27 synth waveforms, 10 envelopes, 40+ instrument presets, 80+ drum patterns
## 0.33.1
- **Electric guitar synth** — Karplus-Strong with magnetic pickup comb filter
simulation (single-coil honk, proper sustain)
- **Speaker cabinet simulation** — steep rolloff above 4-5kHz with presence
bump. Makes distorted guitar sound warm instead of fizzy.
- **6 guitar presets:** electric_guitar, clean_guitar, crunch_guitar,
distorted_guitar, orange_crunch, metal_guitar — all with proper cab sim
- **Sitar synth** — Karplus-Strong with jawari bridge buzz, chikari
sympathetic strings, variable damping
- **Guitar strumming** — `Part.strum("Am", Duration.HALF)` with
fretboard fingering lookup, down/up direction, adjustable strum speed
- **World drums:** dhol (bhangra, chaal), dholak (qawwali, folk),
mridangam (adi talam, korvai), djembe (standard, kuku, soli)
— all with bandpass-filtered membrane noise for realistic drum head sound
- **Metal drum kit** — clicky kick, bright snare, tight hats
with 4 patterns (double kick, metal blast, metal groove, metal gallop)
- 15 synth waveforms, 10 envelopes, 40+ instrument presets
## 0.33.0
- **Non-12-TET support** — `TET(n)` factory creates any equal temperament
+135 -6
View File
@@ -10,7 +10,7 @@ the genre -- they tell the listener's body how to move before a single
melodic note is played.
PyTheory includes a complete drum system -- 27 synthesized percussion
sounds, 58 pattern presets across dozens of genres, and 21 fill presets.
sounds, 80+ pattern presets across dozens of genres, and 21 fill presets.
Every sound is generated from waveforms; no samples needed.
Drum Sounds
@@ -145,8 +145,8 @@ Each sound has a dedicated synthesizer:
Pattern Presets
---------------
58 patterns spanning genres from rock to Afro-Cuban to electronic.
Load them with ``Pattern.preset()``:
80+ patterns spanning genres from rock to Afro-Cuban to electronic to
world percussion. Load them with ``Pattern.preset()``:
.. code-block:: pycon
@@ -193,9 +193,16 @@ adds syncopation.
rattling hi-hats of trap, the breakneck tempo of drum and bass. These
patterns were born in drum machines and they still live there.
**Metal/Punk:** metal, blast beat, punk -- Speed and aggression.
The blast beat is both feet and both hands going as fast as humanly
possible. Punk strips everything to its essentials.
**Metal/Punk:** metal, blast beat, punk, double kick, metal blast,
metal groove, metal gallop -- Speed and aggression. The blast beat is
both feet and both hands going as fast as humanly possible. Punk strips
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
idiomatic patterns. See the World Percussion section below for details.
**Other:** funk, hip hop, bo diddley, second line, new orleans, waltz,
12/8 blues, country, gospel, flamenco -- Everything else. The syncopated
@@ -304,6 +311,128 @@ drum pattern and all named parts are mixed together by ``play_score()``:
play_score(score)
World Percussion
----------------
PyTheory includes dedicated sound sets and pattern presets for
traditional percussion instruments from around the world. Each
instrument has its own synthesized sounds that capture the timbral
character of the real instrument, plus idiomatic rhythmic patterns
drawn from their musical traditions.
Tabla
~~~~~
The tabla is a pair of hand drums from the Indian subcontinent -- the
smaller, higher-pitched *dayan* and the larger, bass *bayan*. It is
the rhythmic backbone of Hindustani classical music, and one of the
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 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).
.. code-block:: python
score = Score("4/4", bpm=80)
score.drums("teental", repeats=4)
Dhol
~~~~
The dhol is a double-headed barrel drum from Punjab, played with
sticks. It is the driving force behind bhangra music -- loud,
energetic, and physically impossible to sit still to.
**3 sounds** -- bass stroke, treble stroke, and rimshot.
**2 patterns:** bhangra (the classic bhangra groove) and dhol chaal
(a processional rhythm).
.. code-block:: python
score = Score("4/4", bpm=160)
score.drums("bhangra", repeats=4)
Dholak
~~~~~~
The dholak is a smaller, lighter two-headed drum used across South
Asia in folk music, qawwali, and Bollywood. Played with bare hands,
it produces a warm, melodic tone.
**3 sounds** -- bass, treble, and slap.
**2 patterns:** qawwali (the rhythmic foundation of Sufi devotional
music) and dholak folk (a general folk groove).
.. code-block:: python
score = Score("4/4", bpm=120)
score.drums("qawwali", repeats=4)
Mridangam
~~~~~~~~~
The mridangam is a double-headed drum from South India, the
rhythmic anchor of Carnatic classical music. Its tuning system is
extraordinarily precise, and its rhythmic vocabulary is among the
most mathematically complex in the world.
**4 sounds** -- tha, thom, nam, and din.
**2 patterns:** adi talam (the most common Carnatic talam, 8 beats)
and mridangam korvai (a rhythmic cadence pattern).
.. code-block:: python
score = Score("4/4", bpm=90)
score.drums("adi talam", repeats=4)
Djembe
~~~~~~
The djembe is a rope-tuned goblet drum from West Africa, capable of
producing a wide range of tones from deep bass to sharp slaps. It is
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).
.. code-block:: python
score = Score("4/4", bpm=120)
score.drums("djembe", repeats=4)
Metal Kit
~~~~~~~~~
A dedicated percussion kit for extreme metal subgenres, with
specialized sounds and patterns that go beyond the standard drum kit.
**3 sounds** -- double kick (triggered, tight attack), china cymbal,
and stack (a short, trashy cymbal choke).
**4 patterns:** double kick (relentless double bass drum pattern),
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).
.. code-block:: python
score = Score("4/4", bpm=200)
score.drums("metal blast", repeats=4)
MIDI Export
-----------
+91 -7
View File
@@ -32,8 +32,8 @@ It's a well-tested order that sounds good by default.
Effects are applied in this fixed order::
Signal --> Saturation --> Tremolo --> Distortion --> Chorus --> Phaser
--> Highpass --> Lowpass --> Delay --> Reverb --> Mix
Signal --> Saturation --> Tremolo --> Distortion --> Cabinet --> Chorus
--> Phaser --> Highpass --> Lowpass --> Delay --> Reverb --> Mix
Additionally, these per-note effects are applied before the part effects chain:
@@ -47,11 +47,12 @@ Part-level effects:
- **Saturation** first: subtle even-harmonic warmth (tape/tube color).
- **Tremolo** second: amplitude LFO modulation.
- **Distortion** third: drives the signal before filtering.
- **Chorus** fourth: thickens the signal.
- **Phaser** fifth: swept allpass notches.
- **Highpass** sixth: removes low-frequency mud.
- **Lowpass** seventh: shapes the tone (like a tone knob on an amp).
- **Delay** eighth: echoes the shaped signal (tap delay / tape echo).
- **Cabinet** fourth: speaker cab simulation (rolloff + presence bump).
- **Chorus** fifth: thickens the signal.
- **Phaser** sixth: swept allpass notches.
- **Highpass** seventh: removes low-frequency mud.
- **Lowpass** eighth: shapes the tone (like a tone knob on an amp).
- **Delay** ninth: echoes the shaped signal (tap delay / tape echo).
- **Reverb** last: places everything in a space (room / hall).
Distortion
@@ -96,6 +97,89 @@ Parameters:
distortion_drive=10.0,
)
Cabinet Simulation
------------------
A real guitar amp doesn't just distort the signal -- the speaker
cabinet shapes the tone dramatically. A 12-inch speaker in a closed
cabinet rolls off the harsh high frequencies above 5 kHz and adds a
presence bump around 2--3 kHz that gives the sound its "in the room"
quality. Without a cabinet, distortion sounds thin and fizzy. With
one, it sounds like a real amp.
PyTheory's cabinet simulation applies a speaker rolloff curve (lowpass
at ~5 kHz) combined with a presence resonance bump, placed in the
signal chain immediately after distortion -- exactly where it sits in
a real amp.
Parameters:
- ``cabinet``: Wet/dry mix, 0.0--1.0 (default 0, off).
- 0.3--0.5 = subtle speaker coloring
- 0.6--0.8 = classic amp-in-a-room
- 1.0 = full cabinet, no dry signal
.. code-block:: python
# Classic rock amp tone: distortion into cabinet
guitar = score.part(
"guitar",
synth="saw",
envelope="pluck",
distortion=0.6,
distortion_drive=5.0,
cabinet=0.8,
)
# Clean amp with just cabinet warmth (no distortion)
clean = score.part(
"clean",
synth="triangle",
envelope="pluck",
cabinet=0.5,
)
Analog Drift
------------
Real analog synthesizers are never perfectly in tune. The voltage-
controlled oscillators drift slightly over time as components warm up
and temperature fluctuates. This imperfection is actually a big part
of why vintage analog synths sound so appealing -- the subtle pitch
wandering gives each note a unique, living quality that static digital
oscillators lack.
The ``analog_drift`` parameter adds slow, random pitch variation to
each oscillator, modeling this vintage behavior.
Parameters:
- ``analog_drift``: Drift amount, 0.0--1.0 (default 0, off).
- 0.05--0.1 = subtle warmth (studio-grade analog)
- 0.15--0.25 = noticeable drift (vintage gear warming up)
- 0.3+ = unstable, wobbly (broken tape machine)
.. code-block:: python
# Warm vintage pad
pad = score.part(
"pad",
synth="supersaw",
envelope="pad",
analog_drift=0.1,
chorus=0.3,
)
# Lo-fi detuned lead
lead = score.part(
"lead",
synth="saw",
envelope="pluck",
analog_drift=0.25,
)
Chorus
------
+175 -3
View File
@@ -1,7 +1,7 @@
Synthesizers
============
PyTheory includes 13 built-in waveforms and 10 ADSR envelope presets.
PyTheory includes 27 built-in waveforms and 10 ADSR envelope presets.
Every sound is generated from scratch -- no samples or external audio
files needed.
@@ -233,7 +233,7 @@ shapes the amplitude over time for natural-sounding notes:
- **Sustain** -- the held volume while the note is on.
- **Release** -- how quickly it fades to silence after the note ends.
PyTheory includes 8 presets:
PyTheory includes 10 presets:
.. code-block:: python
@@ -386,11 +386,183 @@ more "wooden" than a raw saw wave.
violin = score.part("violin", synth="strings_synth")
Dedicated Instrument Synths
--------------------------
Beyond the classic and physical modeling waveforms, PyTheory includes
14 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 27.
Piano Synth
~~~~~~~~~~~
Hammer-strike envelope with body resonance and subtle inharmonicity.
Models the way a felt hammer excites steel strings inside a wooden
soundboard.
.. code-block:: python
piano = score.part("piano", synth="piano_synth")
Bass Guitar Synth
~~~~~~~~~~~~~~~~~
Plucked string model with finger-damped harmonics and low-end warmth.
.. code-block:: python
bass = score.part("bass", synth="bass_guitar_synth")
Flute Synth
~~~~~~~~~~~~
Breathy noise excitation through a resonant tube model, with
overblowing behavior at higher velocities.
.. code-block:: python
flute = score.part("flute", synth="flute_synth")
Trumpet Synth
~~~~~~~~~~~~~
Brass lip-buzz model with spectral brightness that increases with
velocity, plus a characteristic brassy edge from shaped harmonics.
.. code-block:: python
trumpet = score.part("trumpet", synth="trumpet_synth")
Clarinet Synth
~~~~~~~~~~~~~~
Cylindrical bore model producing mostly odd harmonics, giving the
characteristic hollow, woody tone.
.. code-block:: python
clarinet = score.part("clarinet", synth="clarinet_synth")
Oboe Synth
~~~~~~~~~~~
Double-reed model with nasal formant shaping and a buzzy, penetrating
timbre.
.. code-block:: python
oboe = score.part("oboe", synth="oboe_synth")
Marimba Synth
~~~~~~~~~~~~~
Tuned bar model with a soft mallet attack and a warm, resonant decay
that emphasizes the fundamental.
.. code-block:: python
marimba = score.part("marimba", synth="marimba_synth")
Harpsichord Synth
~~~~~~~~~~~~~~~~~
Plucked-string model with a bright, immediate attack and rapid decay
-- the characteristic "plink" of a quill plucking a string.
.. code-block:: python
harpsi = score.part("harpsi", synth="harpsichord_synth")
Cello Synth
~~~~~~~~~~~
Bowed string model with body formants at cello resonance frequencies,
producing a rich, warm, sustained tone.
.. code-block:: python
cello = score.part("cello", synth="cello_synth")
Harp Synth
~~~~~~~~~~
Plucked string with longer sustain and gentle high-frequency rolloff,
modeling nylon strings on a resonant frame.
.. code-block:: python
harp = score.part("harp", synth="harp_synth")
Upright Bass Synth
~~~~~~~~~~~~~~~~~~
Pizzicato double bass with woody body resonance and a thumpy low end.
.. code-block:: python
bass = score.part("bass", synth="upright_bass_synth")
Acoustic Guitar Synth
~~~~~~~~~~~~~~~~~~~~~
Steel-string model with pick transient, body resonance, and natural
string decay.
.. code-block:: python
guitar = score.part("guitar", synth="acoustic_guitar_synth")
Electric Guitar Synth
~~~~~~~~~~~~~~~~~~~~~
Magnetic pickup model with brighter harmonics and less body resonance
than the acoustic, ready for effects processing.
.. code-block:: python
eguitar = score.part("eguitar", synth="electric_guitar_synth")
Sitar Synth
~~~~~~~~~~~~
Sympathetic string resonance with the characteristic buzzy "jawari"
bridge, producing a shimmering, metallic sustain.
.. code-block:: python
sitar = score.part("sitar", synth="sitar_synth")
Analog Oscillator Drift
~~~~~~~~~~~~~~~~~~~~~~~~
All waveform synths support the ``analog_drift`` parameter, which adds
subtle, slow random pitch variation to each oscillator -- modeling the
voltage instability of vintage analog circuits. This is what makes a
real Minimoog sound slightly different on every note, and why analog
synths feel "alive" compared to their digital counterparts.
.. code-block:: python
# Subtle vintage drift
pad = score.part("pad", synth="saw", analog_drift=0.1)
# More pronounced, wobbly analog character
lead = score.part("lead", synth="square", analog_drift=0.3)
Drift values:
- **0.05--0.1** = subtle warmth (studio-grade analog)
- **0.15--0.25** = noticeable drift (vintage gear warming up)
- **0.3+** = unstable, wobbly (broken tape machine)
Instrument Presets
------------------
Instead of choosing synth + envelope + effects manually, use an
instrument preset — 38 predefined combinations that approximate real
instrument preset — 40+ predefined combinations that approximate real
instruments:
.. code-block:: python
+9 -7
View File
@@ -77,14 +77,16 @@ What's Inside
numbers), scale recommendation, modulation, voice leading
- **Sequencing** — Score, Parts, arpeggiator, legato/glide, velocity,
swing, humanize, tempo changes, song sections with repeat
- **Synthesis**13 waveforms (including Karplus-Strong pluck, Hammond organ,
bowed string), 10 envelopes, 38 instrument presets, configurable FM,
sub-oscillator, noise layer, filter envelope, velocity-to-brightness,
detune, stereo pan/spread, 58 drum patterns (stereo panned), 21 fills
- **Synthesis**27 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
- **Effects** — reverb (algorithmic + 7 convolution IRs, stereo), delay,
lowpass/highpass (with resonance), distortion, saturation, chorus,
phaser, tremolo, sidechain compression, automation, LFOs. Master bus
compressor/limiter
lowpass/highpass (with resonance), distortion, cabinet simulation,
saturation, chorus, phaser, tremolo, analog drift, sidechain compression,
automation, LFOs. Master bus compressor/limiter
- **Instruments** — 25 presets with fingering generation
- **Output** — stereo playback, WAV export, MIDI import/export
- **Interface** — REPL with tab completion, CLI (15 commands), ``pytheory demo``
+103 -139
View File
@@ -45,18 +45,15 @@ def bossa_nova_girl():
score = Score("4/4", bpm=140)
score.drums("bossa nova", repeats=4)
rhodes = score.part("rhodes", synth="fm", envelope="piano",
rhodes = score.part("rhodes", instrument="electric_piano",
volume=0.3, pan=-0.3,
reverb=0.4, reverb_decay=1.8, reverb_type="plate",
detune=8, humanize=0.2)
lead = score.part("lead", synth="triangle", envelope="pluck",
reverb=0.4, reverb_decay=1.8, reverb_type="plate")
lead = score.part("lead", instrument="flute",
volume=0.45, pan=0.3,
delay=0.25, delay_time=0.32, delay_feedback=0.35,
reverb=0.2, reverb_type="plate",
humanize=0.2)
bass = score.part("bass", synth="sine", envelope="pluck",
volume=0.45, pan=0.0, lowpass=600,
humanize=0.15)
reverb=0.2, reverb_type="plate")
bass = score.part("bass", instrument="upright_bass",
volume=0.45, pan=0.0, lowpass=600)
for sym in ["Am", "Am", "Dm", "Dm", "E7", "E7", "Am", "Am"]:
rhodes.add(Chord.from_symbol(sym), Duration.WHOLE)
@@ -90,19 +87,16 @@ def bebop_in_bb():
score = Score("4/4", bpm=160)
score.drums("bebop", repeats=8, fill="jazz", fill_every=8)
rhodes = score.part("rhodes", synth="fm", envelope="piano",
rhodes = score.part("rhodes", instrument="electric_piano",
volume=0.25, pan=-0.3,
reverb=0.35, reverb_decay=1.2, reverb_type="plate",
detune=8, humanize=0.2)
lead = score.part("lead", synth="saw", envelope="pluck",
reverb=0.35, reverb_decay=1.2, reverb_type="plate")
lead = score.part("lead", instrument="trumpet",
volume=0.4, pan=0.25,
lowpass=4000, lowpass_q=1.1,
delay=0.15, delay_time=0.19, delay_feedback=0.25,
reverb=0.15, reverb_type="plate",
humanize=0.2)
bass = score.part("bass", synth="triangle", envelope="pluck",
volume=0.4, pan=0.0, lowpass=500,
humanize=0.15)
reverb=0.15, reverb_type="plate")
bass = score.part("bass", instrument="upright_bass",
volume=0.4, pan=0.0, lowpass=500)
for sym in ["Bb", "Gm", "Cm", "F7"] * 2:
rhodes.add(Chord.from_symbol(sym), Duration.WHOLE)
@@ -148,14 +142,12 @@ def salsa_descarga():
volume=0.2, pan=-0.35,
reverb=0.3, reverb_type="plate", lowpass=2000,
detune=10, humanize=0.2)
lead = score.part("lead", synth="saw", envelope="pluck",
lead = score.part("lead", instrument="trumpet",
volume=0.4, pan=0.3,
delay=0.2, delay_time=0.167, delay_feedback=0.3,
reverb=0.15, reverb_type="plate",
humanize=0.2)
bass = score.part("bass", synth="pulse", envelope="pluck",
volume=0.45, pan=0.0, lowpass=500, lowpass_q=1.3,
humanize=0.15)
reverb=0.15, reverb_type="plate")
bass = score.part("bass", instrument="synth_bass",
volume=0.45, pan=0.0, lowpass=500, lowpass_q=1.3)
for sym in ["Em7b5", "A7", "Dm7", "Bbmaj7"] * 2:
pads.add(Chord.from_symbol(sym), Duration.WHOLE)
@@ -191,20 +183,17 @@ def afrobeat_groove():
score = Score("4/4", bpm=115)
score.drums("afrobeat", repeats=8, fill="afrobeat", fill_every=8)
pads = score.part("pads", synth="supersaw", envelope="pad",
pads = score.part("pads", instrument="synth_pad",
volume=0.2, pan=-0.3,
reverb=0.4, reverb_decay=2.0, reverb_type="cathedral",
lowpass=3000, detune=10, spread=0.6,
humanize=0.2)
lead = score.part("lead", synth="saw", envelope="pluck",
lowpass=3000)
lead = score.part("lead", instrument="trumpet",
volume=0.4, pan=0.3,
lowpass=3000, lowpass_q=1.0,
delay=0.2, delay_time=0.26, delay_feedback=0.3,
reverb=0.15, reverb_type="plate",
humanize=0.2)
bass = score.part("bass", synth="sine", envelope="pluck",
volume=0.5, pan=0.0, lowpass=500,
humanize=0.15)
reverb=0.15, reverb_type="plate")
bass = score.part("bass", instrument="bass_guitar",
volume=0.5, pan=0.0, lowpass=500)
for sym in ["Em", "Am", "D", "C"] * 2:
pads.add(Chord.from_symbol(sym), Duration.WHOLE)
@@ -241,14 +230,12 @@ def reggae_one_drop():
reverb=0.5, reverb_decay=2.0, reverb_type="cathedral",
lowpass=2000, detune=8,
humanize=0.2)
lead = score.part("lead", synth="triangle", envelope="strings",
lead = score.part("lead", instrument="flute",
volume=0.4, pan=0.3,
delay=0.35, delay_time=0.5625, delay_feedback=0.45,
reverb=0.3, reverb_type="cathedral",
humanize=0.2)
bass = score.part("bass", synth="sine", envelope="pluck",
volume=0.55, pan=0.0, lowpass=400, lowpass_q=1.3,
humanize=0.15)
reverb=0.3, reverb_type="cathedral")
bass = score.part("bass", instrument="bass_guitar",
volume=0.55, pan=0.0, lowpass=400, lowpass_q=1.3)
for sym in ["G", "C", "D", "C"] * 2:
chords.add(Chord.from_symbol(sym), Duration.WHOLE)
@@ -286,15 +273,13 @@ def funk_workout():
volume=0.25, pan=-0.4,
lowpass=2500, reverb=0.15, reverb_type="plate",
sidechain=0.4, humanize=0.2)
lead = score.part("lead", synth="saw", envelope="pluck",
lead = score.part("lead", instrument="synth_lead",
volume=0.4, pan=0.35,
lowpass=3500, lowpass_q=1.5,
delay=0.15, delay_time=0.15, delay_feedback=0.25,
reverb=0.1, reverb_type="plate",
humanize=0.2)
bass = score.part("bass", synth="pulse", envelope="pluck",
volume=0.5, pan=0.0, lowpass=600, lowpass_q=1.2,
humanize=0.15)
reverb=0.1, reverb_type="plate")
bass = score.part("bass", instrument="synth_bass",
volume=0.5, pan=0.0, lowpass=600, lowpass_q=1.2)
for sym in ["Em", "Am", "D", "B7"] * 2:
chords.add(Chord.from_symbol(sym), Duration.WHOLE)
@@ -332,18 +317,16 @@ def blues_shuffle():
score = Score("12/8", bpm=70)
score.drums("12/8 blues", repeats=6)
chords = score.part("chords", synth="fm", envelope="piano",
chords = score.part("chords", instrument="electric_piano",
volume=0.3, pan=-0.3,
reverb=0.3, reverb_decay=1.5, reverb_type="plate",
detune=8, humanize=0.2)
lead = score.part("lead", synth="saw", envelope="pluck",
reverb=0.3, reverb_decay=1.5, reverb_type="plate")
lead = score.part("lead", instrument="trumpet",
volume=0.45, pan=0.25,
reverb=0.3, reverb_decay=1.2, reverb_type="plate",
delay=0.2, delay_time=0.43, delay_feedback=0.3,
lowpass=3500, humanize=0.2)
bass = score.part("bass", synth="sine", envelope="pluck",
volume=0.5, pan=0.0, lowpass=500,
humanize=0.15)
lowpass=3500)
bass = score.part("bass", instrument="upright_bass",
volume=0.5, pan=0.0, lowpass=500)
for sym in ["A", "A", "D", "D", "E7", "A"]:
chords.add(Chord.from_symbol(sym), Duration.DOTTED_HALF)
@@ -381,19 +364,16 @@ def samba_de_janeiro():
score = Score("4/4", bpm=170)
score.drums("samba", repeats=8, fill="samba", fill_every=8)
pads = score.part("pads", synth="supersaw", envelope="pad",
pads = score.part("pads", instrument="synth_pad",
volume=0.2, pan=-0.3,
reverb=0.45, reverb_decay=2.0, reverb_type="plate",
lowpass=4000, detune=10, spread=0.5,
humanize=0.2)
lead = score.part("lead", synth="triangle", envelope="pluck",
lowpass=4000)
lead = score.part("lead", instrument="flute",
volume=0.45, pan=0.3,
delay=0.2, delay_time=0.176, delay_feedback=0.3,
reverb=0.15, reverb_type="plate",
humanize=0.2)
bass = score.part("bass", synth="sine", envelope="pluck",
volume=0.45, pan=0.0, lowpass=500,
humanize=0.15)
reverb=0.15, reverb_type="plate")
bass = score.part("bass", instrument="bass_guitar",
volume=0.45, pan=0.0, lowpass=500)
for sym in ["G", "Em", "Am", "D7"] * 2:
pads.add(Chord.from_symbol(sym), Duration.WHOLE)
@@ -426,18 +406,15 @@ def jazz_waltz():
score = Score("3/4", bpm=150)
score.drums("waltz", repeats=16)
rhodes = score.part("rhodes", synth="fm", envelope="piano",
rhodes = score.part("rhodes", instrument="electric_piano",
volume=0.3, pan=-0.3,
reverb=0.4, reverb_decay=2.0, reverb_type="cathedral",
detune=8, humanize=0.2)
lead = score.part("lead", synth="triangle", envelope="strings",
reverb=0.4, reverb_decay=2.0, reverb_type="cathedral")
lead = score.part("lead", instrument="flute",
volume=0.4, pan=0.25,
reverb=0.3, reverb_decay=1.5, reverb_type="plate",
delay=0.2, delay_time=0.4, delay_feedback=0.3,
humanize=0.2)
bass = score.part("bass", synth="sine", envelope="pluck",
volume=0.4, pan=0.0, lowpass=500,
humanize=0.15)
delay=0.2, delay_time=0.4, delay_feedback=0.3)
bass = score.part("bass", instrument="upright_bass",
volume=0.4, pan=0.0, lowpass=500)
for _ in range(2):
for sym in ["Fmaj7", "Gm", "C7", "Fmaj7"]:
@@ -471,20 +448,19 @@ def house_anthem():
score = Score("4/4", bpm=124)
score.drums("house", repeats=8, fill="house", fill_every=8)
pads = score.part("pads", synth="supersaw", envelope="pad",
pads = score.part("pads", instrument="synth_pad",
volume=0.25, pan=-0.3,
reverb=0.5, reverb_decay=2.5, reverb_type="cathedral",
lowpass=5000, detune=12, spread=0.7,
sidechain=0.6, humanize=0.2)
lead = score.part("lead", synth="saw", envelope="staccato",
lowpass=5000,
sidechain=0.6)
lead = score.part("lead", instrument="synth_lead",
volume=0.35, pan=0.3,
lowpass=2000, lowpass_q=2.0,
delay=0.2, delay_time=0.242, delay_feedback=0.35,
reverb=0.15, reverb_type="plate",
humanize=0.2)
bass = score.part("bass", synth="sine", envelope="pluck",
volume=0.55, pan=0.0, lowpass=300,
sidechain=0.5, humanize=0.15)
reverb=0.15, reverb_type="plate")
bass = score.part("bass", instrument="808_bass",
volume=0.55, pan=0.0,
sidechain=0.5)
for sym in ["Cm", "Ab", "Bb", "Cm"] * 2:
pads.add(Chord.from_symbol(sym), Duration.WHOLE)
@@ -538,14 +514,12 @@ def dub_kingston():
reverb=0.6, reverb_decay=2.5, reverb_type="cathedral",
lowpass=1500, lowpass_q=0.9, detune=8,
humanize=0.2)
lead = score.part("lead", synth="triangle", envelope="strings",
lead = score.part("lead", instrument="flute",
volume=0.4, pan=0.3,
delay=0.45, delay_time=0.625, delay_feedback=0.5,
reverb=0.35, reverb_decay=2.0, reverb_type="cathedral",
humanize=0.2)
bass = score.part("bass", synth="sine", envelope="pluck",
volume=0.6, pan=0.0, lowpass=400, lowpass_q=1.5,
humanize=0.15)
reverb=0.35, reverb_decay=2.0, reverb_type="cathedral")
bass = score.part("bass", instrument="bass_guitar",
volume=0.6, pan=0.0, lowpass=400, lowpass_q=1.5)
siren = score.part("siren", synth="pwm_slow", envelope="pad",
volume=0.15, pan=0.5,
reverb=0.7, reverb_decay=3.0, reverb_type="cathedral",
@@ -585,20 +559,20 @@ def techno_minimal():
score = Score("4/4", bpm=130)
score.drums("techno", repeats=8, fill="house", fill_every=8)
pad = score.part("pad", synth="supersaw", envelope="pad",
pad = score.part("pad", instrument="synth_pad",
volume=0.2, pan=-0.3,
reverb=0.5, reverb_decay=3.0, reverb_type="cathedral",
lowpass=3000, detune=12, spread=0.7,
sidechain=0.6, humanize=0.2)
lowpass=3000,
sidechain=0.6)
lead = score.part("lead", synth="pwm_fast", envelope="staccato",
volume=0.35, pan=0.3,
lowpass=1500, lowpass_q=3.0,
delay=0.3, delay_time=0.231, delay_feedback=0.4,
reverb=0.1, reverb_type="plate",
humanize=0.2)
bass = score.part("bass", synth="sine", envelope="pluck",
volume=0.55, pan=0.0, lowpass=250,
sidechain=0.5, humanize=0.15)
bass = score.part("bass", instrument="808_bass",
volume=0.55, pan=0.0,
sidechain=0.5)
for sym in ["Fm", "Db", "Eb", "Fm"] * 2:
pad.add(Chord.from_symbol(sym), Duration.WHOLE)
@@ -624,18 +598,15 @@ def gospel_shuffle():
score = Score("4/4", bpm=108)
score.drums("gospel", repeats=8, fill="buildup", fill_every=8)
organ = score.part("organ", synth="fm", envelope="organ",
organ = score.part("organ", instrument="organ",
volume=0.3, pan=-0.3,
reverb=0.45, reverb_decay=2.0, reverb_type="cathedral",
detune=8, humanize=0.2)
lead = score.part("lead", synth="triangle", envelope="pluck",
reverb=0.45, reverb_decay=2.0, reverb_type="cathedral")
lead = score.part("lead", instrument="flute",
volume=0.4, pan=0.3,
delay=0.2, delay_time=0.278, delay_feedback=0.3,
reverb=0.2, reverb_type="plate",
humanize=0.2)
bass = score.part("bass", synth="sine", envelope="pluck",
volume=0.45, pan=0.0, lowpass=500,
humanize=0.15)
reverb=0.2, reverb_type="plate")
bass = score.part("bass", instrument="upright_bass",
volume=0.45, pan=0.0, lowpass=500)
for sym in ["C", "Am", "F", "G"] * 2:
organ.add(Chord.from_symbol(sym), Duration.WHOLE)
@@ -691,20 +662,18 @@ def dub_delay_madness():
volume=0.15, pan=-0.4,
reverb=0.7, reverb_decay=3.0, reverb_type="cathedral",
lowpass=1200, detune=8, humanize=0.2)
bass = score.part("bass", synth="sine", envelope="pluck",
volume=0.6, pan=0.0, lowpass=350, lowpass_q=1.5,
humanize=0.15)
bass = score.part("bass", instrument="bass_guitar",
volume=0.6, pan=0.0, lowpass=350, lowpass_q=1.5)
siren = score.part("siren", synth="pwm_slow", envelope="pad",
volume=0.12, pan=0.5,
reverb=0.8, reverb_decay=4.0, reverb_type="cathedral",
delay=0.4, delay_time=0.88, delay_feedback=0.6,
lowpass=900, detune=10)
# Melodica stabs — sparse, lots of delay
melodica = score.part("melodica", synth="triangle", envelope="pluck",
melodica = score.part("melodica", instrument="flute",
volume=0.35, pan=0.3,
delay=0.6, delay_time=0.66, delay_feedback=0.55,
reverb=0.5, reverb_decay=2.5, reverb_type="cathedral",
humanize=0.2)
reverb=0.5, reverb_decay=2.5, reverb_type="cathedral")
for sym in ["Em", "Em", "Am", "Am", "Em", "Em", "Bm", "Em"]:
chords.add(Chord.from_symbol(sym), Duration.WHOLE)
@@ -741,19 +710,16 @@ def drum_and_bass():
score = Score("4/4", bpm=174)
score.drums("drum and bass", repeats=8, fill="buildup", fill_every=8)
pads = score.part("pads", synth="supersaw", envelope="pad",
pads = score.part("pads", instrument="synth_pad",
volume=0.25, pan=-0.3,
reverb=0.5, reverb_decay=2.5, reverb_type="plate",
lowpass=4000, detune=10, spread=0.6,
humanize=0.2)
lead = score.part("lead", synth="triangle", envelope="strings",
lowpass=4000)
lead = score.part("lead", instrument="flute",
volume=0.4, pan=0.3,
delay=0.3, delay_time=0.172, delay_feedback=0.4,
reverb=0.25, reverb_type="plate",
humanize=0.2)
bass = score.part("bass", synth="sine", envelope="pluck",
volume=0.55, pan=0.0, lowpass=300,
humanize=0.15)
reverb=0.25, reverb_type="plate")
bass = score.part("bass", instrument="808_bass",
volume=0.55, pan=0.0)
for sym in ["Am", "F", "C", "G"] * 2:
pads.add(Chord.from_symbol(sym), Duration.WHOLE)
@@ -787,25 +753,24 @@ def drake_vibes():
score = Score("4/4", bpm=68)
score.drums("trap", repeats=8, fill="trap", fill_every=8)
pads = score.part("pads", synth="supersaw", envelope="pad",
pads = score.part("pads", instrument="synth_pad",
volume=0.2, pan=-0.25,
reverb=0.5, reverb_decay=3.0, reverb_type="cathedral",
lowpass=2500, detune=12, spread=0.6,
sidechain=0.4, humanize=0.2)
bells = score.part("bells", synth="fm", envelope="bell",
lowpass=2500,
sidechain=0.4)
bells = score.part("bells", instrument="vibraphone",
volume=0.3, pan=0.4,
reverb=0.4, reverb_decay=2.0, reverb_type="plate",
delay=0.25, delay_time=0.44, delay_feedback=0.35,
humanize=0.2)
delay=0.25, delay_time=0.44, delay_feedback=0.35)
lead = score.part("lead", synth="pwm_slow", envelope="strings",
volume=0.35, pan=-0.2,
reverb=0.3, reverb_type="cathedral", lowpass=2000,
delay=0.2, delay_time=0.88, delay_feedback=0.3,
humanize=0.2)
bass = score.part("bass", synth="sine", envelope="pluck",
volume=0.6, pan=0.0, lowpass=200, lowpass_q=1.8,
bass = score.part("bass", instrument="808_bass",
volume=0.6, pan=0.0,
distortion=0.4, distortion_drive=2.0,
sidechain=0.3, humanize=0.15)
sidechain=0.3)
for sym in ["Ebm", "B", "Gb", "Db"] * 2:
pads.add(Chord.from_symbol(sym), Duration.WHOLE)
@@ -858,14 +823,14 @@ def neon_grid():
score._drum_pattern_beats = max(score._drum_pattern_beats, 32.0)
sky = score.part(
"sky", synth="supersaw", envelope="pad",
"sky", instrument="synth_pad",
volume=0.18, pan=0.0,
detune=30, spread=1.0,
reverb=0.6, reverb_decay=3.5,
chorus=0.3, sidechain=0.5,
)
acid_l = score.part(
"acid_l", synth="saw", envelope="pad",
"acid_l", instrument="acid_bass",
volume=0.35, pan=-0.7,
legato=True, glide=0.025,
distortion=0.9, distortion_drive=12.0,
@@ -875,7 +840,7 @@ def neon_grid():
acid_l.lfo("lowpass", rate=0.5, min=400, max=3000, bars=8, shape="sine")
acid_r = score.part(
"acid_r", synth="saw", envelope="pad",
"acid_r", instrument="acid_bass",
volume=0.3, pan=0.7,
legato=True, glide=0.02,
distortion=0.85, distortion_drive=10.0,
@@ -885,7 +850,7 @@ def neon_grid():
acid_r.lfo("lowpass", rate=0.33, min=500, max=2500, bars=8, shape="triangle")
sub = score.part(
"sub", synth="sine", envelope="pluck",
"sub", instrument="808_bass",
volume=0.55, pan=0.0,
lowpass=160, sidechain=0.85, sidechain_release=0.08,
)
@@ -896,12 +861,12 @@ def neon_grid():
detune=15, spread=0.7,
)
bell_l = score.part(
"bell_l", synth="fm", envelope="bell",
"bell_l", instrument="vibraphone",
volume=0.1, pan=-1.0,
reverb=0.7, reverb_decay=3.0,
)
bell_r = score.part(
"bell_r", synth="fm", envelope="bell",
"bell_r", instrument="vibraphone",
volume=0.1, pan=1.0,
reverb=0.7, reverb_decay=3.0,
delay=0.2, delay_time=0.8, delay_feedback=0.4,
@@ -1026,14 +991,14 @@ def dance_party():
score._drum_hits.append(_Hit(DrumSound.KICK, bar * 4.0 + beat, 120))
score._drum_pattern_beats = max(score._drum_pattern_beats, 32.0)
bass = score.part("bass", synth="square", envelope="pluck",
bass = score.part("bass", instrument="synth_bass",
volume=0.45, lowpass=500, lowpass_q=1.3,
sidechain=0.75, sidechain_release=0.12)
sparkle = score.part("sparkle", synth="fm", envelope="bell",
sparkle = score.part("sparkle", instrument="vibraphone",
volume=0.3, pan=0.4, reverb=0.3, reverb_decay=1.5,
delay=0.2, delay_time=0.234, delay_feedback=0.3)
chords_part = score.part("chords", synth="supersaw", envelope="pad",
volume=0.2, detune=12, spread=0.7,
chords_part = score.part("chords", instrument="synth_pad",
volume=0.2,
reverb=0.4, reverb_type="plate", sidechain=0.7)
fun = score.part("fun", synth="square", envelope="staccato",
volume=0.2, pan=-0.5, delay=0.15, delay_time=0.117,
@@ -1080,14 +1045,13 @@ def temple_bell():
score = Score("4/4", bpm=65)
score.drums("bolero", repeats=8)
koto = score.part("koto", synth="triangle", envelope="pluck",
koto = score.part("koto", instrument="koto",
volume=0.45, pan=0.2,
reverb=0.5, reverb_type="taj_mahal",
humanize=0.3)
reverb=0.5, reverb_type="taj_mahal")
drone = score.part("drone", synth="sine", envelope="pad",
volume=0.15, reverb=0.6, reverb_type="taj_mahal",
chorus=0.15, chorus_rate=0.2)
bell = score.part("bell", synth="fm", envelope="bell",
bell = score.part("bell", instrument="vibraphone",
volume=0.1, pan=-0.6,
reverb=0.8, reverb_type="taj_mahal")
+1 -1
View File
@@ -1,6 +1,6 @@
[project]
name = "pytheory"
version = "0.33.0"
version = "0.34.1"
description = "Music Theory for Humans"
readme = "README.md"
license = "MIT"
+1 -1
View File
@@ -1,6 +1,6 @@
"""PyTheory: Music Theory for Humans."""
__version__ = "0.33.0"
__version__ = "0.34.1"
from .tones import Tone, Interval
from .systems import System, SYSTEMS, TET
+24
View File
@@ -275,6 +275,30 @@ def cmd_demo(args):
"lead": ("pluck_synth", "none", 0.3, 0.2),
"pad": ("strings_synth", "pad", 0.0),
"bass_lp": 200, "reverb_type": "taj_mahal"},
{"name": "Classical", "key": ("D", "minor"), "drums": "bolero",
"fill": "bossa nova", "bpm": 72,
"prog": ("i", "iv", "V", "i"),
"lead": ("flute_synth", "strings", 0.35, 0.2),
"pad": ("cello_synth", "bowed", -0.2),
"bass_lp": 400, "reverb_type": "cathedral"},
{"name": "Harpsichord Suite", "key": ("A", "minor"), "drums": "bolero",
"fill": "bossa nova", "bpm": 92,
"prog": ("i", "iv", "V", "i"),
"lead": ("harpsichord_synth", "none", 0.2, 0.1),
"pad": ("strings_synth", "pad", -0.3),
"bass_lp": 500, "reverb_type": "plate"},
{"name": "Bhangra", "key": ("G", "minor"), "drums": "bhangra",
"fill": "rock", "bpm": 140,
"prog": ("i", "iv", "V", "i"),
"lead": ("sitar_synth", "none", 0.3, 0.2),
"pad": ("strings_synth", "pad", 0.0),
"bass_lp": 400, "reverb_type": "taj_mahal"},
{"name": "Jazz Trio", "key": ("F", "major"), "drums": "swing",
"fill": "jazz", "bpm": 100,
"prog": ("I", "vi", "ii", "V"),
"lead": ("trumpet_synth", "bowed", 0.3, 0.2),
"pad": ("piano_synth", "none", -0.2),
"bass_lp": 600, "reverb_type": "plate"},
]
mood = random.choice(moods)
+1308 -9
View File
File diff suppressed because it is too large Load Diff
+509 -46
View File
@@ -14,11 +14,8 @@ from typing import Optional
INSTRUMENTS = {
# ── Keys ──
"piano": {
"synth": "fm", "envelope": "piano",
"fm_ratio": 1.0, "fm_index": 1.5,
"detune": 5, "chorus": 0.1, "chorus_rate": 0.3,
"lowpass": 6000, "saturation": 0.1,
"vel_to_filter": 3000, "noise_mix": 0.02,
"synth": "piano_synth", "envelope": "none",
"vel_to_filter": 3000,
},
"electric_piano": { # Rhodes/Wurlitzer
"synth": "fm", "envelope": "piano",
@@ -26,16 +23,17 @@ INSTRUMENTS = {
"detune": 6, "chorus": 0.2, "chorus_rate": 1.0,
"lowpass": 4000, "saturation": 0.15,
"tremolo_depth": 0.15, "tremolo_rate": 4.5,
"analog": 0.2,
},
"organ": {
"synth": "organ_synth", "envelope": "organ",
"chorus": 0.2, "chorus_rate": 5.5,
"lowpass": 5000,
"phaser": 0.15, "phaser_rate": 0.4,
"analog": 0.15,
},
"harpsichord": {
"synth": "pluck_synth", "envelope": "none",
"lowpass": 3500,
"synth": "harpsichord_synth", "envelope": "none",
},
"celesta": {
"synth": "fm", "envelope": "mallet",
@@ -63,10 +61,8 @@ INSTRUMENTS = {
"noise_mix": 0.03,
},
"cello": {
"synth": "strings_synth", "envelope": "bowed",
"detune": 2, "lowpass": 2500,
"synth": "cello_synth", "envelope": "bowed",
"humanize": 0.15, "vel_to_filter": 1000,
"noise_mix": 0.02,
},
"contrabass": {
"synth": "strings_synth", "envelope": "bowed",
@@ -84,21 +80,18 @@ INSTRUMENTS = {
# ── Woodwinds ──
"flute": {
"synth": "sine", "envelope": "strings",
"lowpass": 4000,
"humanize": 0.2, "noise_mix": 0.08,
"synth": "flute_synth", "envelope": "strings",
"humanize": 0.2,
"vel_to_filter": 2000,
},
"clarinet": {
"synth": "square", "envelope": "strings",
"lowpass": 3000,
"humanize": 0.15, "noise_mix": 0.05,
"synth": "clarinet_synth", "envelope": "strings",
"humanize": 0.15,
"vel_to_filter": 1500,
},
"oboe": {
"synth": "saw", "envelope": "strings",
"lowpass": 3500, "lowpass_q": 1.2,
"humanize": 0.15, "noise_mix": 0.04,
"synth": "oboe_synth", "envelope": "strings",
"humanize": 0.15,
"vel_to_filter": 1000,
},
"bassoon": {
@@ -110,16 +103,13 @@ INSTRUMENTS = {
# ── Brass ──
"trumpet": {
"synth": "saw", "envelope": "bowed",
"detune": 3, "lowpass": 4000, "lowpass_q": 1.1,
"synth": "trumpet_synth", "envelope": "bowed",
"humanize": 0.15, "vel_to_filter": 2000,
"saturation": 0.1,
},
"trombone": {
"synth": "saw", "envelope": "strings",
"detune": 3, "lowpass": 2500,
"synth": "trumpet_synth", "envelope": "strings",
"lowpass": 2500,
"humanize": 0.15, "vel_to_filter": 1500,
"saturation": 0.1,
},
"french_horn": {
"synth": "saw", "envelope": "strings",
@@ -143,34 +133,61 @@ INSTRUMENTS = {
# ── Plucked ──
"acoustic_guitar": {
"synth": "pluck_synth", "envelope": "none",
"lowpass": 4000,
"humanize": 0.2,
"synth": "acoustic_guitar_synth", "envelope": "none",
"humanize": 0.2, "saturation": 0.05,
},
"electric_guitar": {
"synth": "saw", "envelope": "pluck",
"detune": 5, "lowpass": 3500,
"synth": "electric_guitar_synth", "envelope": "none",
"cabinet": 1.0, "cabinet_brightness": 0.6,
"humanize": 0.15,
},
"clean_guitar": {
"synth": "electric_guitar_synth", "envelope": "none",
"cabinet": 1.0, "cabinet_brightness": 0.7,
"chorus": 0.15, "chorus_rate": 1.0,
"reverb": 0.2, "reverb_type": "spring",
"humanize": 0.15,
},
"crunch_guitar": {
"synth": "electric_guitar_synth", "envelope": "none",
"saturation": 0.3,
"distortion": 0.5, "distortion_drive": 4.0,
"cabinet": 1.0, "cabinet_brightness": 0.5,
"humanize": 0.15,
},
"distorted_guitar": {
"synth": "saw", "envelope": "pluck",
"detune": 8, "distortion": 0.6, "distortion_drive": 5.0,
"lowpass": 3000, "saturation": 0.3,
"synth": "electric_guitar_synth", "envelope": "none",
"saturation": 0.3,
"distortion": 0.7, "distortion_drive": 5.0,
"cabinet": 1.0, "cabinet_brightness": 0.5,
"humanize": 0.15,
},
"orange_crunch": {
"synth": "electric_guitar_synth", "envelope": "none",
"saturation": 0.4,
"distortion": 0.7, "distortion_drive": 6.0,
"cabinet": 1.0, "cabinet_brightness": 0.4,
"humanize": 0.15,
},
"metal_guitar": {
"synth": "electric_guitar_synth", "envelope": "none",
"saturation": 0.35,
"distortion": 0.8, "distortion_drive": 7.0,
"cabinet": 1.0, "cabinet_brightness": 0.5,
"highpass": 80,
"detune": 4,
"humanize": 0.1,
},
"bass_guitar": {
"synth": "triangle", "envelope": "pluck",
"lowpass": 1000,
"humanize": 0.1, "sub_osc": 0.2,
"synth": "bass_guitar_synth", "envelope": "none",
"humanize": 0.1, "sub_osc": 0.15,
},
"upright_bass": {
"synth": "triangle", "envelope": "pluck",
"lowpass": 800,
"synth": "upright_bass_synth", "envelope": "none",
"humanize": 0.15, "saturation": 0.1,
},
"harp": {
"synth": "pluck_synth", "envelope": "none",
"lowpass": 5000,
"synth": "harp_synth", "envelope": "none",
"reverb": 0.3, "reverb_type": "plate",
},
"sitar": {
@@ -183,6 +200,11 @@ INSTRUMENTS = {
"lowpass": 4000,
"reverb": 0.2,
},
"sitar": {
"synth": "sitar_synth", "envelope": "none",
"lowpass": 4500,
"humanize": 0.2,
},
# ── Synth presets ──
"synth_lead": {
@@ -191,6 +213,7 @@ INSTRUMENTS = {
"delay": 0.2, "delay_time": 0.25, "delay_feedback": 0.3,
"filter_attack": 0.01, "filter_decay": 0.3,
"filter_sustain": 0.2, "filter_amount": 3000,
"analog": 0.3,
},
"synth_pad": {
"synth": "supersaw", "envelope": "pad",
@@ -198,6 +221,7 @@ INSTRUMENTS = {
"chorus": 0.2,
"phaser": 0.3, "phaser_rate": 0.3,
"sub_osc": 0.2,
"analog": 0.4,
},
"synth_bass": {
"synth": "saw", "envelope": "pluck",
@@ -205,6 +229,7 @@ INSTRUMENTS = {
"filter_attack": 0.005, "filter_decay": 0.2,
"filter_sustain": 0.0, "filter_amount": 2000,
"sub_osc": 0.4,
"analog": 0.2,
},
"acid_bass": {
"synth": "saw", "envelope": "pad",
@@ -214,6 +239,7 @@ INSTRUMENTS = {
"filter_attack": 0.005, "filter_decay": 0.15,
"filter_sustain": 0.0, "filter_amount": 4000,
"vel_to_filter": 3000,
"analog": 0.3,
},
"808_bass": {
"synth": "sine", "envelope": "pluck",
@@ -231,8 +257,7 @@ INSTRUMENTS = {
"reverb": 0.3, "reverb_type": "plate",
},
"marimba": {
"synth": "sine", "envelope": "mallet",
"lowpass": 3000,
"synth": "marimba_synth", "envelope": "mallet",
},
"xylophone": {
"synth": "fm", "envelope": "pluck",
@@ -295,11 +320,18 @@ class TimeSignature:
@dataclass
class Note:
"""A pairing of a sound (Tone, Chord, or None for rest) with a duration."""
"""A pairing of a sound (Tone, Chord, or None for rest) with a duration.
The optional ``bend`` field specifies a pitch bend in semitones
applied over the note's duration. Positive = bend up, negative = down.
For example, ``bend=2`` bends the note up a whole step by the end.
"""
tone: object
duration: Duration
velocity: int = 100
bend: float = 0.0
bend_type: str = "smooth" # "smooth" (log), "linear", "late"
@property
def beats(self) -> float:
@@ -376,6 +408,34 @@ class DrumSound(Enum):
AGOGO_LOW = 68
GUIRO = 73
MARACAS = 70
# Tabla sounds
TABLA_NA = 86 # sharp dayan (right drum) rim hit
TABLA_TIN = 87 # open dayan ring
TABLA_GE = 88 # deep bayan (left drum) bass
TABLA_DHA = 89 # both drums (Na + Ge)
TABLA_TIT = 90 # light dayan flick
TABLA_KE = 91 # muted bayan slap
# Dhol sounds
DHOL_DAGGA = 92 # heavy bass side (dagga stick)
DHOL_TILLI = 93 # thin treble side (tilli stick)
DHOL_BOTH = 94 # both sides
# Dholak sounds
DHOLAK_GE = 95 # bass side (open palm)
DHOLAK_NA = 96 # treble side (fingers)
DHOLAK_TIT = 97 # light treble tap
# Mridangam sounds
MRIDANGAM_THAM = 98 # bass stroke (thoppi/left head)
MRIDANGAM_NAM = 99 # treble ring (valanthalai/right head)
MRIDANGAM_DIN = 100 # both heads
MRIDANGAM_THA = 101 # muted treble
# Djembe sounds
DJEMBE_BASS = 102 # open bass (center of head)
DJEMBE_TONE = 103 # open tone (edge, fingers together)
DJEMBE_SLAP = 104 # slap (edge, fingers spread, sharp crack)
# Metal kit — tighter, punchier, more attack
METAL_KICK = 105 # clicky, punchy, tight
METAL_SNARE = 106 # crack, bright, cutting
METAL_HAT = 107 # tight, short, precise
class _Hit:
@@ -1313,6 +1373,314 @@ Pattern._PRESETS["flamenco"] = dict(
],
)
# ── Tabla patterns ────────────────────────────────────────────────────────
# Shortcuts for tabla sounds
TNA = DrumSound.TABLA_NA
TTI = DrumSound.TABLA_TIN
TGE = DrumSound.TABLA_GE
TDHA = DrumSound.TABLA_DHA
TTIT = DrumSound.TABLA_TIT
TKE = DrumSound.TABLA_KE
# Teental — the most common taal (16 beats / 4+4+4+4)
Pattern._PRESETS["teental"] = dict(
name="teental",
time_signature="4/4",
beats=16.0,
hits=[
# Vibhag 1: Dha Dhin Dhin Dha
_h(TDHA, 0.0), _h(TNA, 1.0), _h(TNA, 2.0), _h(TDHA, 3.0),
# Vibhag 2: Dha Dhin Dhin Dha
_h(TDHA, 4.0), _h(TNA, 5.0), _h(TNA, 6.0), _h(TDHA, 7.0),
# Vibhag 3 (khali): Dha Tin Tin Ta
_h(TDHA, 8.0), _h(TTI, 9.0), _h(TTI, 10.0), _h(TNA, 11.0),
# Vibhag 4: Dha Dhin Dhin Dha
_h(TDHA, 12.0), _h(TNA, 13.0), _h(TNA, 14.0), _h(TDHA, 15.0),
],
)
# Jhaptaal — 10 beats (2+3+2+3)
Pattern._PRESETS["jhaptaal"] = dict(
name="jhaptaal",
time_signature="4/4",
beats=10.0,
hits=[
# Dhi Na | Dhi Dhi Na | Ti Na | Dhi Dhi Na
_h(TDHA, 0.0), _h(TNA, 1.0),
_h(TDHA, 2.0), _h(TDHA, 3.0), _h(TNA, 4.0),
_h(TTI, 5.0), _h(TNA, 6.0),
_h(TDHA, 7.0), _h(TDHA, 8.0), _h(TNA, 9.0),
],
)
# Rupak taal — 7 beats (3+2+2), starts on khali (unusual)
Pattern._PRESETS["rupak"] = dict(
name="rupak",
time_signature="7/4",
beats=7.0,
hits=[
# Tin Tin Na | Dhi Na | Dhi Na
_h(TTI, 0.0), _h(TTI, 1.0), _h(TNA, 2.0),
_h(TDHA, 3.0), _h(TNA, 4.0),
_h(TDHA, 5.0), _h(TNA, 6.0),
],
)
# Dadra — 6 beats (3+3), light and folk
Pattern._PRESETS["dadra"] = dict(
name="dadra",
time_signature="6/4",
beats=6.0,
hits=[
# Dha Dhi Na | Dha Tin Na
_h(TDHA, 0.0), _h(TNA, 1.0), _h(TNA, 2.0),
_h(TDHA, 3.0), _h(TTI, 4.0), _h(TNA, 5.0),
],
)
# Keherwa — 8 beats (4+4), the most common light taal
Pattern._PRESETS["keherwa"] = dict(
name="keherwa",
time_signature="4/4",
beats=8.0,
hits=[
# Dha Ge Na Ti | Na Ke Dhi Na
_h(TDHA, 0.0), _h(TGE, 1.0), _h(TNA, 2.0), _h(TTIT, 3.0),
_h(TNA, 4.0), _h(TKE, 5.0), _h(TDHA, 6.0), _h(TNA, 7.0),
],
)
# Tabla solo theka — fast 16th note pattern for rhythmic display
Pattern._PRESETS["tabla solo"] = dict(
name="tabla solo",
time_signature="4/4",
beats=4.0,
hits=[
_h(TDHA, 0.0), _h(TTIT, 0.25), _h(TTIT, 0.5), _h(TKE, 0.75),
_h(TNA, 1.0), _h(TTIT, 1.25), _h(TGE, 1.5), _h(TNA, 1.75),
_h(TDHA, 2.0), _h(TNA, 2.25), _h(TTI, 2.5), _h(TNA, 2.75),
_h(TDHA, 3.0), _h(TTIT, 3.5), _h(TGE, 3.75),
],
)
# ── Metal kit patterns ────────────────────────────────────────────────────
MK = DrumSound.METAL_KICK
MS = DrumSound.METAL_SNARE
MH = DrumSound.METAL_HAT
# Metal double kick — the classic thrash/death metal beat
Pattern._PRESETS["double kick"] = dict(
name="double kick",
time_signature="4/4",
beats=4.0,
hits=[
# Double kick 16ths, snare on 2 and 4, tight hats
*[_h(MK, i * 0.25) for i in range(16)],
_h(MS, 1.0), _h(MS, 3.0),
*[_h(MH, i * 0.5) for i in range(8)],
],
)
# Metal blast — blast beat with metal kit sounds
Pattern._PRESETS["metal blast"] = dict(
name="metal blast",
time_signature="4/4",
beats=4.0,
hits=[
*[_h(MK, i * 0.25) for i in range(16)],
*[_h(MS, i * 0.25) for i in range(16)],
*[_h(MH, i * 0.25) for i in range(16)],
],
)
# Metal groove — half time with double kick fills
Pattern._PRESETS["metal groove"] = dict(
name="metal groove",
time_signature="4/4",
beats=4.0,
hits=[
_h(MK, 0.0), _h(MH, 0.0),
_h(MH, 0.5),
_h(MS, 1.0), _h(MH, 1.0),
_h(MK, 1.5), _h(MH, 1.5),
_h(MK, 2.0), _h(MH, 2.0),
_h(MK, 2.25),
_h(MK, 2.5), _h(MH, 2.5),
_h(MK, 2.75),
_h(MS, 3.0), _h(MH, 3.0),
_h(MH, 3.5),
],
)
# Metal gallop — the classic Iron Maiden triplet feel
Pattern._PRESETS["metal gallop"] = dict(
name="metal gallop",
time_signature="4/4",
beats=4.0,
hits=[
_h(MK, 0.0), _h(MH, 0.0),
_h(MK, 0.33), _h(MK, 0.67),
_h(MS, 1.0), _h(MH, 1.0),
_h(MK, 1.33), _h(MK, 1.67),
_h(MK, 2.0), _h(MH, 2.0),
_h(MK, 2.33), _h(MK, 2.67),
_h(MS, 3.0), _h(MH, 3.0),
_h(MK, 3.33), _h(MK, 3.67),
],
)
# Tabla tiri-kita — rapid 16th-note dayan patter
Pattern._PRESETS["tiri kita"] = dict(
name="tiri kita",
time_signature="4/4",
beats=4.0,
hits=[
# Ti ri ki ta | dha ti ri ki | ta ka dhi na | dha — ti dha
_h(TTIT, 0.0), _h(TTIT, 0.25), _h(TKE, 0.5), _h(TNA, 0.75),
_h(TDHA, 1.0), _h(TTIT, 1.25), _h(TTIT, 1.5), _h(TKE, 1.75),
_h(TNA, 2.0), _h(TKE, 2.25), _h(TDHA, 2.5), _h(TNA, 2.75),
_h(TDHA, 3.0), _h(TTIT, 3.5), _h(TDHA, 3.75),
],
)
# ── Dhol patterns ────────────────────────────────────────────────────────
DD = DrumSound.DHOL_DAGGA
DT = DrumSound.DHOL_TILLI
DB = DrumSound.DHOL_BOTH
# Bhangra — the classic punjabi groove
Pattern._PRESETS["bhangra"] = dict(
name="bhangra",
time_signature="4/4",
beats=4.0,
hits=[
# Dagga on 1, tilli fills, both on 3
_h(DD, 0.0), _h(DT, 0.5), _h(DT, 0.75),
_h(DT, 1.0), _h(DT, 1.5),
_h(DB, 2.0), _h(DT, 2.5), _h(DT, 2.75),
_h(DD, 3.0), _h(DT, 3.25), _h(DT, 3.5), _h(DT, 3.75),
],
)
# Dhol chaal — driving folk pattern
Pattern._PRESETS["dhol chaal"] = dict(
name="dhol chaal",
time_signature="4/4",
beats=4.0,
hits=[
_h(DB, 0.0), _h(DT, 0.25), _h(DD, 0.5),
_h(DT, 1.0), _h(DT, 1.25), _h(DT, 1.5), _h(DD, 1.75),
_h(DB, 2.0), _h(DT, 2.25), _h(DD, 2.5),
_h(DT, 3.0), _h(DT, 3.25), _h(DT, 3.5), _h(DT, 3.75),
],
)
# ── Dholak patterns ─────────────────────────────────────────────────────
DKG = DrumSound.DHOLAK_GE
DKN = DrumSound.DHOLAK_NA
DKT = DrumSound.DHOLAK_TIT
# Qawwali — driving devotional pattern
Pattern._PRESETS["qawwali"] = dict(
name="qawwali",
time_signature="4/4",
beats=4.0,
hits=[
_h(DKG, 0.0), _h(DKN, 0.5), _h(DKT, 0.75),
_h(DKN, 1.0), _h(DKG, 1.5),
_h(DKG, 2.0), _h(DKN, 2.5), _h(DKT, 2.75),
_h(DKN, 3.0), _h(DKT, 3.25), _h(DKN, 3.5), _h(DKG, 3.75),
],
)
# Dholak folk — light folk music pattern
Pattern._PRESETS["dholak folk"] = dict(
name="dholak folk",
time_signature="4/4",
beats=4.0,
hits=[
_h(DKG, 0.0), _h(DKN, 1.0), _h(DKT, 1.5),
_h(DKG, 2.0), _h(DKN, 3.0), _h(DKT, 3.5),
],
)
# ── Mridangam patterns ──────────────────────────────────────────────────
MTH = DrumSound.MRIDANGAM_THAM
MN = DrumSound.MRIDANGAM_NAM
MD = DrumSound.MRIDANGAM_DIN
MTA = DrumSound.MRIDANGAM_THA
# Adi talam — the fundamental Carnatic rhythm (8 beats: 4+2+2)
Pattern._PRESETS["adi talam"] = dict(
name="adi talam",
time_signature="4/4",
beats=8.0,
hits=[
# Tha Din | Tha ka | Dhi na | Tha ka
_h(MD, 0.0), _h(MN, 1.0),
_h(MTH, 2.0), _h(MTA, 3.0),
_h(MD, 4.0), _h(MN, 5.0),
_h(MTH, 6.0), _h(MTA, 7.0),
],
)
# Mridangam korvai — rhythmic cadence pattern
Pattern._PRESETS["mridangam korvai"] = dict(
name="mridangam korvai",
time_signature="4/4",
beats=4.0,
hits=[
_h(MD, 0.0), _h(MN, 0.25), _h(MTA, 0.5), _h(MN, 0.75),
_h(MTH, 1.0), _h(MN, 1.25), _h(MN, 1.5), _h(MTH, 1.75),
_h(MD, 2.0), _h(MTA, 2.25), _h(MN, 2.5), _h(MTA, 2.75),
_h(MD, 3.0), _h(MN, 3.5), _h(MD, 3.75),
],
)
# ── Djembe patterns ─────────────────────────────────────────────────────
JB = DrumSound.DJEMBE_BASS
JT = DrumSound.DJEMBE_TONE
JS = DrumSound.DJEMBE_SLAP
# Djembe — standard West African pattern
Pattern._PRESETS["djembe"] = dict(
name="djembe",
time_signature="4/4",
beats=4.0,
hits=[
_h(JB, 0.0), _h(JT, 0.5), _h(JT, 0.75),
_h(JS, 1.0), _h(JT, 1.5),
_h(JB, 2.0), _h(JT, 2.5), _h(JT, 2.75),
_h(JS, 3.0), _h(JT, 3.25), _h(JS, 3.5),
],
)
# Kuku — traditional Guinean harvest dance rhythm
Pattern._PRESETS["kuku"] = dict(
name="kuku",
time_signature="4/4",
beats=4.0,
hits=[
_h(JS, 0.0), _h(JS, 0.5),
_h(JT, 1.0), _h(JB, 1.5),
_h(JS, 2.0), _h(JS, 2.5),
_h(JT, 3.0), _h(JT, 3.25), _h(JB, 3.5),
],
)
# Soli — powerful Mandinka rhythm
Pattern._PRESETS["soli"] = dict(
name="soli",
time_signature="4/4",
beats=4.0,
hits=[
_h(JB, 0.0), _h(JT, 0.25), _h(JS, 0.5), _h(JT, 0.75),
_h(JB, 1.0), _h(JS, 1.5),
_h(JB, 2.0), _h(JT, 2.25), _h(JS, 2.5), _h(JT, 2.75),
_h(JB, 3.0), _h(JT, 3.5), _h(JS, 3.75),
],
)
# ── Fill presets ──────────────────────────────────────────────────────────
Pattern._FILLS["rock"] = dict(
@@ -1632,6 +2000,9 @@ class Part:
tremolo_rate: float = 5.0,
phaser: float = 0.0,
phaser_rate: float = 0.5,
cabinet: float = 0.0,
cabinet_brightness: float = 0.5,
analog: float = 0.0,
fm_ratio: float = 2.0,
fm_index: float = 3.0):
self.name = name
@@ -1675,19 +2046,27 @@ class Part:
self.tremolo_rate = tremolo_rate
self.phaser_mix = phaser
self.phaser_rate = phaser_rate
self.cabinet = cabinet
self.cabinet_brightness = cabinet_brightness
self.analog = analog
self.fm_ratio = fm_ratio
self.fm_index = fm_index
self._system = "western" # default, overridden by Score.part()
self._fretboard = None # set by Score.part(fretboard=...)
self.notes: list[Note] = []
self._drum_hits: list[_Hit] = []
self._drum_pattern_beats: float = 0.0
self._automation: list[tuple[float, dict]] = [] # (beat, {param: value})
def add(self, tone_or_string, duration=Duration.QUARTER, *, velocity: int = 100) -> "Part":
def add(self, tone_or_string, duration=Duration.QUARTER, *, velocity: int = 100,
bend: float = 0.0, bend_type: str = "smooth") -> "Part":
"""Add a note. Accepts Tone/Chord objects or note strings like ``"E5"``.
Duration can be a ``Duration`` enum or a raw float (beats).
Velocity controls loudness (1-127, default 100).
Bend specifies a pitch bend in semitones over the note duration
(e.g. ``bend=2`` bends up a whole step, ``bend=-1`` bends down
a half step). Used for guitar bends, sitar meends, slides.
Returns self for chaining.
"""
@@ -1696,7 +2075,9 @@ class Part:
tone_or_string = Tone.from_string(tone_or_string, system=self._system)
if isinstance(duration, (int, float)):
duration = _RawDuration(duration)
self.notes.append(Note(tone=tone_or_string, duration=duration, velocity=velocity))
self.notes.append(Note(tone=tone_or_string, duration=duration,
velocity=velocity, bend=bend,
bend_type=bend_type))
return self
def set(self, **params) -> "Part":
@@ -1761,6 +2142,7 @@ class Part:
"delay_mix": self.delay_mix, "delay_time": self.delay_time,
"delay_feedback": self.delay_feedback,
"phaser_mix": self.phaser_mix, "phaser_rate": self.phaser_rate,
"cabinet": self.cabinet, "cabinet_brightness": self.cabinet_brightness,
"highpass": self.highpass, "highpass_q": self.highpass_q,
"lowpass": self.lowpass, "lowpass_q": self.lowpass_q,
"distortion_mix": self.distortion_mix,
@@ -1978,6 +2360,80 @@ class Part:
return self
def strum(self, chord_name: str, duration=Duration.QUARTER, *,
direction: str = "down", velocity: int = 100,
strum_time: float = 0.08) -> "Part":
"""Strum a chord using the part's fretboard fingering.
Looks up the chord on the fretboard, gets the fingering, and
adds each string as a rapid sequence with tiny time offsets
like a real guitar strum. Muted strings are skipped.
Args:
chord_name: Chord name (e.g. ``"Am"``, ``"G"``, ``"D"``).
duration: Total duration of the strum (default QUARTER).
direction: ``"down"`` (lowhigh, default) or ``"up"`` (highlow).
velocity: Base velocity (each string gets slight variation).
strum_time: Time in beats for the full strum sweep
(default 0.03 = very fast). Larger values = slower,
more audible strum. Try 0.1 for a lazy strum.
Returns:
Self for chaining.
Example::
>>> guitar = score.part("guitar", instrument="acoustic_guitar",
... fretboard=Fretboard.guitar())
>>> guitar.strum("Am", Duration.HALF)
>>> guitar.strum("G", Duration.HALF, direction="up")
"""
if self._fretboard is None:
raise ValueError(
"Cannot strum without a fretboard. "
"Set fretboard= when creating the part."
)
from .charts import CHARTS
# Get the fingering
system_name = self._system if isinstance(self._system, str) else "western"
if system_name in CHARTS:
chart = CHARTS[system_name]
else:
chart = CHARTS["western"]
if chord_name in chart:
fingering = chart[chord_name].fingering(fretboard=self._fretboard)
else:
# Try fretboard.chord() as fallback
fingering = self._fretboard.chord(chord_name)
# Get the sounding tones (skips muted strings)
tones = fingering.tones # list of Tone objects, high to low
if not tones:
self.rest(duration)
return self
# Order: down strum = low to high (reverse since tones are high-to-low)
if direction == "down":
strum_tones = list(reversed(tones))
else:
strum_tones = list(tones)
if hasattr(duration, 'value'):
total_beats = duration.value
else:
total_beats = float(duration)
# Build a Chord — all strings ring together through the
# shared body resonance, like a real guitar
from .chords import Chord as ChordClass
chord_obj = ChordClass(tones=strum_tones)
self.add(chord_obj, total_beats, velocity=velocity)
return self
@property
def is_drums(self) -> bool:
"""True if this part contains drum hits."""
@@ -2180,8 +2636,12 @@ class Score:
tremolo_rate: float = None,
phaser: float = None,
phaser_rate: float = None,
cabinet: float = None,
cabinet_brightness: float = None,
analog: float = None,
fm_ratio: float = None,
fm_index: float = None) -> Part:
fm_index: float = None,
fretboard=None) -> Part:
"""Create a named part with its own synth voice and effects.
Args:
@@ -2290,6 +2750,8 @@ class Score:
"saturation": saturation,
"tremolo_depth": tremolo_depth, "tremolo_rate": tremolo_rate,
"phaser": phaser, "phaser_rate": phaser_rate,
"cabinet": cabinet, "cabinet_brightness": cabinet_brightness,
"analog": analog,
"fm_ratio": fm_ratio, "fm_index": fm_index,
}
for k, v in _locals.items():
@@ -2300,6 +2762,7 @@ class Score:
p = Part(name, **merged)
p._system = self.system
p._fretboard = fretboard
self.parts[name] = p
return p
+205 -10
View File
@@ -5320,7 +5320,7 @@ def test_supersaw_wave():
@needs_portaudio
def test_all_synths_in_enum():
from pytheory.play import Synth
assert len(Synth) == 13
assert len(Synth) == 27
for s in Synth:
wave = s(440, n_samples=1000)
assert len(wave) == 1000
@@ -6467,11 +6467,8 @@ def test_instrument_piano():
from pytheory import Score, Duration
score = Score("4/4", bpm=120)
p = score.part("p", instrument="piano")
assert p.synth == "fm"
assert p.envelope == "piano"
assert p.detune == 5
assert p.lowpass == 6000
assert p.chorus_mix == 0.1
assert p.synth == "piano_synth"
assert p.vel_to_filter == 3000
def test_instrument_violin():
@@ -6488,12 +6485,9 @@ def test_instrument_violin():
def test_instrument_override():
from pytheory import Score
score = Score("4/4", bpm=120)
# Explicit synth overrides the preset's "fm"
# Explicit synth overrides the preset
p = score.part("p", instrument="piano", synth="saw")
assert p.synth == "saw"
# Other preset values still apply
assert p.envelope == "piano"
assert p.detune == 5
def test_instrument_unknown_raises():
@@ -6717,3 +6711,204 @@ def test_interval_to_non12():
a5 = a.add(19)
result = a.interval_to(a5)
assert "octave" in result
# ── Dedicated instrument synths ──────────────────────────────────────────────
def test_all_dedicated_synths_render():
"""Every dedicated synth waveform produces valid audio."""
from pytheory.play import (piano_wave, bass_guitar_wave, flute_wave,
trumpet_wave, clarinet_wave, oboe_wave,
marimba_wave, harpsichord_wave, cello_wave,
harp_wave, upright_bass_wave,
acoustic_guitar_wave, electric_guitar_wave,
sitar_wave, SAMPLE_RATE)
synths = [piano_wave, bass_guitar_wave, flute_wave, trumpet_wave,
clarinet_wave, oboe_wave, marimba_wave, harpsichord_wave,
cello_wave, harp_wave, upright_bass_wave,
acoustic_guitar_wave, electric_guitar_wave, sitar_wave]
for fn in synths:
wave = fn(440, n_samples=11025)
assert len(wave) == 11025
assert wave.dtype == numpy.int16
assert numpy.abs(wave).max() > 0
def test_piano_brightness_scales():
"""High-pitched piano should be brighter (more high harmonics)."""
from pytheory.play import piano_wave
low = piano_wave(130, n_samples=22050) # C3
high = piano_wave(1047, n_samples=22050) # C6
# Both should produce valid audio
assert numpy.abs(low).max() > 0
assert numpy.abs(high).max() > 0
def test_acoustic_guitar_body_resonance():
"""Acoustic guitar should produce richer spectrum than raw pluck."""
from pytheory.play import acoustic_guitar_wave, pluck_wave
ag = acoustic_guitar_wave(220, n_samples=22050)
pk = pluck_wave(220, n_samples=22050)
assert len(ag) == len(pk) == 22050
def test_cello_has_vibrato():
"""Cello synth should produce pitch variation (vibrato)."""
from pytheory.play import cello_wave
wave = cello_wave(220, n_samples=44100)
assert len(wave) == 44100
assert numpy.abs(wave).max() > 0
# ── Cabinet simulation ───────────────────────────────────────────────────────
def test_cabinet_reduces_highs():
"""Cabinet sim should reduce high-frequency content."""
from pytheory.play import _apply_cabinet
# White noise has flat spectrum
noise = numpy.random.uniform(-1, 1, 44100).astype(numpy.float32)
cabbed = _apply_cabinet(noise, brightness=0.5)
# RMS of cabbed should be lower (energy removed by filters)
assert numpy.sqrt(numpy.mean(cabbed ** 2)) < numpy.sqrt(numpy.mean(noise ** 2))
def test_cabinet_brightness_param():
"""Higher brightness = more high-frequency content passes through."""
from pytheory.play import _apply_cabinet
noise = numpy.random.uniform(-1, 1, 44100).astype(numpy.float32)
dark = _apply_cabinet(noise, brightness=0.0)
bright = _apply_cabinet(noise, brightness=1.0)
# Bright should have more energy than dark
assert numpy.sqrt(numpy.mean(bright ** 2)) > numpy.sqrt(numpy.mean(dark ** 2))
# ── Analog drift ─────────────────────────────────────────────────────────────
def test_analog_drift_varies_pitch():
"""Analog drift should make repeated renders slightly different."""
from pytheory import Score, Duration
score1 = Score("4/4", bpm=120)
p1 = score1.part("t", synth="saw", analog=0.5)
p1.add("C4", Duration.QUARTER)
p1.add("C4", Duration.QUARTER)
# With analog > 0, each C4 gets a random pitch offset
# This is hard to test deterministically, just verify it renders
from pytheory.play import render_score
buf = render_score(score1)
assert len(buf) > 0
# ── Guitar strumming ─────────────────────────────────────────────────────────
def test_strum_requires_fretboard():
"""Strumming without a fretboard should raise ValueError."""
from pytheory import Score, Duration
score = Score("4/4", bpm=120)
p = score.part("g", synth="saw")
with pytest.raises(ValueError, match="fretboard"):
p.strum("Am", Duration.QUARTER)
def test_strum_adds_notes():
"""Strumming should add notes to the part."""
from pytheory import Score, Duration, Fretboard
score = Score("4/4", bpm=120)
fb = Fretboard.guitar()
p = score.part("g", instrument="acoustic_guitar", fretboard=fb)
p.strum("Am", Duration.HALF)
assert len(p.notes) > 0
def test_strum_direction():
"""Both down and up strums should work."""
from pytheory import Score, Duration, Fretboard
score = Score("4/4", bpm=120)
fb = Fretboard.guitar()
p = score.part("g", instrument="acoustic_guitar", fretboard=fb)
p.strum("G", Duration.QUARTER, direction="down")
p.strum("G", Duration.QUARTER, direction="up")
assert len(p.notes) == 2
# ── World drums ──────────────────────────────────────────────────────────────
def test_tabla_sounds_render():
"""All tabla drum sounds should produce valid audio."""
from pytheory.play import _render_drum_hit
from pytheory.rhythm import DrumSound
for sound in [DrumSound.TABLA_NA, DrumSound.TABLA_TIN, DrumSound.TABLA_GE,
DrumSound.TABLA_DHA, DrumSound.TABLA_TIT, DrumSound.TABLA_KE]:
wave = _render_drum_hit(sound.value, 22050)
assert len(wave) == 22050
assert wave.dtype == numpy.float32
def test_dhol_sounds_render():
from pytheory.play import _render_drum_hit
from pytheory.rhythm import DrumSound
for sound in [DrumSound.DHOL_DAGGA, DrumSound.DHOL_TILLI, DrumSound.DHOL_BOTH]:
wave = _render_drum_hit(sound.value, 22050)
assert len(wave) == 22050
def test_mridangam_sounds_render():
from pytheory.play import _render_drum_hit
from pytheory.rhythm import DrumSound
for sound in [DrumSound.MRIDANGAM_THAM, DrumSound.MRIDANGAM_NAM,
DrumSound.MRIDANGAM_DIN, DrumSound.MRIDANGAM_THA]:
wave = _render_drum_hit(sound.value, 22050)
assert len(wave) == 22050
def test_djembe_sounds_render():
from pytheory.play import _render_drum_hit
from pytheory.rhythm import DrumSound
for sound in [DrumSound.DJEMBE_BASS, DrumSound.DJEMBE_TONE, DrumSound.DJEMBE_SLAP]:
wave = _render_drum_hit(sound.value, 22050)
assert len(wave) == 22050
def test_metal_kit_sounds_render():
from pytheory.play import _render_drum_hit
from pytheory.rhythm import DrumSound
for sound in [DrumSound.METAL_KICK, DrumSound.METAL_SNARE, DrumSound.METAL_HAT]:
wave = _render_drum_hit(sound.value, 22050)
assert len(wave) == 22050
def test_tabla_pattern_presets():
"""All tabla patterns should load without error."""
from pytheory.rhythm import Pattern
for name in ["teental", "jhaptaal", "rupak", "dadra",
"keherwa", "tabla solo", "tiri kita"]:
p = Pattern.preset(name)
assert p.beats > 0
def test_world_drum_pattern_presets():
"""All world drum patterns should load."""
from pytheory.rhythm import Pattern
for name in ["bhangra", "dhol chaal", "qawwali", "dholak folk",
"adi talam", "mridangam korvai", "djembe", "kuku", "soli",
"double kick", "metal blast", "metal groove", "metal gallop"]:
p = Pattern.preset(name)
assert p.beats > 0
# ── Guitar presets with cabinet sim ──────────────────────────────────────────
def test_guitar_presets_have_cabinet():
"""Distorted guitar presets should have cabinet simulation."""
from pytheory import Score
for preset in ["distorted_guitar", "orange_crunch", "metal_guitar"]:
score = Score("4/4", bpm=120)
p = score.part("g", instrument=preset)
assert p.cabinet > 0, f"{preset} should have cabinet sim"
def test_clean_guitar_preset():
from pytheory import Score
score = Score("4/4", bpm=120)
p = score.part("g", instrument="clean_guitar")
assert p.synth == "electric_guitar_synth"
assert p.cabinet > 0
Generated
+1 -1
View File
@@ -707,7 +707,7 @@ wheels = [
[[package]]
name = "pytheory"
version = "0.33.0"
version = "0.34.1"
source = { editable = "." }
dependencies = [
{ name = "numeral" },