mirror of
https://github.com/kennethreitz/pytheory.git
synced 2026-06-05 23:00:20 +00:00
Compare commits
13 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 33b2e82594 | |||
| 9f8dd0006d | |||
| 417f7f74a3 | |||
| cd6f814049 | |||
| 83fcdb0a09 | |||
| aa21bf0f2a | |||
| e7e35ad4e4 | |||
| 503dbce937 | |||
| c6bbfae7e6 | |||
| 64ef7f0803 | |||
| 406e5d7e54 | |||
| 267b7284ba | |||
| 9b62b56120 |
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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,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
|
||||
|
||||
@@ -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
File diff suppressed because it is too large
Load Diff
+509
-46
@@ -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"`` (low→high, default) or ``"up"`` (high→low).
|
||||
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
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user