mirror of
https://github.com/kennethreitz/pytheory.git
synced 2026-06-05 23:00:20 +00:00
Compare commits
70 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 2f02df15b8 | |||
| a2740b8d57 | |||
| 840bfcc36c | |||
| 938c1cc132 | |||
| 9dc22db4b2 | |||
| f570e226cd | |||
| 0c5c3abedc | |||
| 35d07b984b | |||
| aec7723ee6 | |||
| b98a40297b | |||
| 9117568b74 | |||
| 11e4417c62 | |||
| 4edf1d983d | |||
| 74b07b1a8a | |||
| c9437209a7 | |||
| 92cb855a49 | |||
| f06c6f77d1 | |||
| 51bd63658f | |||
| 92ade3ee3d | |||
| 833867329e | |||
| 93b9fe9ced | |||
| 88a1171bbe | |||
| 3ca0842b7a | |||
| 00de5eb354 | |||
| d2b0c6f329 | |||
| 76612682f1 | |||
| ce480858e9 | |||
| 70efb0ad40 | |||
| bf6deaab64 | |||
| 7c792c0a2a | |||
| bf8d4b9a77 | |||
| d2d5115c8a | |||
| 3cdd98b158 | |||
| 751d5a49b8 | |||
| 6a836dd891 | |||
| 1f888e2b21 | |||
| fb923f6c76 | |||
| 59e3338892 | |||
| 8cf4145c15 | |||
| b3885b2c15 | |||
| ae04fa60cc | |||
| 6c411e43f8 | |||
| e0427af3cc | |||
| 552836ae5b | |||
| 0fe53fcdeb | |||
| f6fb2a2cd6 | |||
| 70d6e6b8ce | |||
| aec9a999cb | |||
| 3acde86028 | |||
| aa405702a9 | |||
| b7c018fb94 | |||
| 07a52a3a25 | |||
| e12cb9003b | |||
| 28968a1b5c | |||
| 8a4a2df1aa | |||
| f4a90637db | |||
| 90a1a31049 | |||
| 33b2e82594 | |||
| 9f8dd0006d | |||
| 417f7f74a3 | |||
| cd6f814049 | |||
| 83fcdb0a09 | |||
| aa21bf0f2a | |||
| e7e35ad4e4 | |||
| 503dbce937 | |||
| c6bbfae7e6 | |||
| 64ef7f0803 | |||
| 406e5d7e54 | |||
| 267b7284ba | |||
| 9b62b56120 |
+148
@@ -2,6 +2,154 @@
|
||||
|
||||
All notable changes to PyTheory are documented here.
|
||||
|
||||
## 0.38.1
|
||||
|
||||
- **Dynamic curves** — `Part.crescendo()`, `Part.decrescendo()`,
|
||||
`Part.swell()`, and `Part.dynamics()` for velocity ramps and custom
|
||||
curves across a sequence of notes
|
||||
|
||||
## 0.38.0
|
||||
|
||||
- **Articulations** — `staccato`, `legato`, `marcato`, `tenuto`, `accent`,
|
||||
`fermata` via `articulation=` on `Part.add()` and `Part.hold()`
|
||||
- **`Part.hit()`** — place individual drum sounds in a Part's note stream
|
||||
with articulation, velocity, and effects support
|
||||
- **5 new djembe patterns** — dununba, tiriba, yankadi, djansa, mendiani
|
||||
- **3 new djembe fills** — djembe call, djembe roll, djembe break (30 fills total)
|
||||
- **Cross-choke drum damping** — striking one sound fades out related sounds
|
||||
(djembe, hi-hats, cajón, doumbek)
|
||||
- **Improved djembe slap** — dry goatskin pop instead of snare-like noise
|
||||
|
||||
## 0.37.0
|
||||
|
||||
- **5 new djembe patterns** — dununba, tiriba, yankadi, djansa, mendiani
|
||||
- **3 new djembe fills** — djembe call, djembe roll, djembe break (30 fills total)
|
||||
- **Cross-choke drum damping** — striking one sound on a hand drum fades
|
||||
out the ring of related sounds (djembe slap kills bass resonance, closed
|
||||
hat chokes open hat, cajón slap dampens bass, doumbek tek dampens dum)
|
||||
- **Improved djembe slap** — dry, high-pitched goatskin pop instead of
|
||||
snare-like noise rattle
|
||||
|
||||
## 0.36.6
|
||||
|
||||
- **6 new drum fills** — 3 cajón (flam, rumble, breakdown) and 3 metal
|
||||
(triplet, blast, cascade). 27 fills total.
|
||||
- Updated drums documentation with fill lists and examples
|
||||
|
||||
## 0.36.5
|
||||
|
||||
- **Duration arithmetic** — `Duration.WHOLE * 2`, `Duration.HALF + Duration.QUARTER`,
|
||||
division, and reverse multiply all work now (previously raised TypeError)
|
||||
|
||||
## 0.36.3
|
||||
|
||||
- **`Part.hold()`** — polyphonic overlap on a single part. Add notes
|
||||
without advancing the beat position so they play simultaneously.
|
||||
Enables: piano sustain, sitar drone under melody, guitar strum texture.
|
||||
- **Strum uses hold()** — leading string plays simultaneously with chord,
|
||||
no more timing gaps or choppiness
|
||||
- **Improved songs** 1-16: humanize, velocity dynamics, reverb, saxophone
|
||||
for blues
|
||||
- **Ctrl-C handling** — clean stop on all playback functions
|
||||
- **REPL updates** — strum, roll, bend, temperament, reference commands
|
||||
- Song #28 Descent (generative), #29 Pop Rock, #30 Sitar Drone
|
||||
- 862 tests
|
||||
|
||||
## 0.36.1
|
||||
|
||||
- **7 new instrument synths:** pedal steel guitar, theremin, kalimba/thumb
|
||||
piano, steel drum/pan, accordion (musette reeds), didgeridoo (drone +
|
||||
shifting formants), bagpipes (chanter reed)
|
||||
- **9 new demo moods** in ``pytheory demo``: Theremin Noir, Caribbean,
|
||||
Accordion Waltz, Kalimba Dreams, Outback Drone, Highland, Nashville
|
||||
Tears, Tabla Fusion
|
||||
- Improved existing songs with dedicated instrument synths
|
||||
- 41 synth waveforms, 26+ songs, 21 demo moods
|
||||
|
||||
## 0.36.0
|
||||
|
||||
- **Banjo synth** — steel strings on drum-head body, nasal twang,
|
||||
fast decay with membrane resonance
|
||||
- **Mandolin synth** — paired steel strings (natural chorus from
|
||||
doubled courses), bright body resonance
|
||||
- **Ukulele synth** — nylon strings, small mid-heavy body, shorter
|
||||
sustain than guitar
|
||||
- **Cajón drums** — bass (woody box thump), slap (snare wire buzz),
|
||||
tap (ghost note). 3 patterns: cajon, cajon rumba, cajon folk
|
||||
- **Vocal/formant synth** — LF glottal model, 5 Peterson & Barney
|
||||
formant peaks, jitter/shimmer, consonant onsets, per-note lyrics.
|
||||
Presets: vocal, choir
|
||||
- **Granular synthesis** — grain cloud engine with scatter, pitch
|
||||
variation, Hanning windows. Presets: granular_pad, granular_texture
|
||||
- **Strum sweep** — subtle grace notes before chord hit for natural
|
||||
strum feel on all fretboard instruments
|
||||
- Mandola preset, 34 synth waveforms, 26 songs
|
||||
|
||||
## 0.35.0
|
||||
|
||||
- **8.5x faster import** — dropped pytuning/sympy, lazy-load scipy.
|
||||
`import pytheory` now takes ~50ms instead of ~480ms (#44)
|
||||
- **Proper shruti JI ratios** — 22 positions with 5-limit just intonation
|
||||
(pure 3/2 fifths, 5/4 thirds), not 22-TET approximation
|
||||
- **Arabic maqam JI ratios** — Zalzalian 11-limit ratios.
|
||||
Mi↓ (the Rast third) is exactly 27/22 from Do
|
||||
- **B#/Cb octave boundary fix** — B#4 = C5, Cb4 = B3 (#45)
|
||||
- **Int tone names** — `Tone(0, system=TET(22))` works alongside strings.
|
||||
Wrapping: `Tone(22)` → tone 0, octave+1. `System.tone()` convenience.
|
||||
- **Timpani synth** — inharmonic membrane modes, felt mallet, copper kettle
|
||||
resonance, cathedral reverb
|
||||
- **Saxophone synth** — conical bore, reed buzz, brass body warmth.
|
||||
4 presets: saxophone, alto_sax, tenor_sax, bari_sax
|
||||
- **Part.roll()** — rapid repeated notes with velocity ramp for crescendo/
|
||||
decrescendo rolls on any instrument
|
||||
- **Vibrato tuning** — all instruments reduced to 0.001 depth for cleaner
|
||||
ensemble sound
|
||||
- **Granular synthesis** — grain cloud engine with scatter, pitch
|
||||
variation, and Hanning-windowed grains. Two presets: granular_pad,
|
||||
granular_texture.
|
||||
- 30 synth waveforms, 838 tests
|
||||
|
||||
## 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
|
||||
|
||||
@@ -322,6 +322,14 @@ against 17 known chord types (triads, 7ths, 9ths, sus, power chords).
|
||||
>>> Chord.from_tones("Bb", "D", "F").identify()
|
||||
'Bb major'
|
||||
|
||||
Enharmonic spellings are fully supported — Cb, Fb, E#, B#, double
|
||||
sharps/flats, and unicode symbols (see :doc:`tones` for details):
|
||||
|
||||
.. code-block:: pycon
|
||||
|
||||
>>> Chord.from_tones("Cb", "Eb", "Gb").identify()
|
||||
'B minor'
|
||||
|
||||
You can also access the root and quality separately:
|
||||
|
||||
.. code-block:: pycon
|
||||
|
||||
+204
-12
@@ -9,8 +9,8 @@ in Atlanta. Over a dancehall pattern, you're in Kingston. The drums ARE
|
||||
the genre -- they tell the listener's body how to move before a single
|
||||
melodic note is played.
|
||||
|
||||
PyTheory includes a complete drum system -- 27 synthesized percussion
|
||||
sounds, 58 pattern presets across dozens of genres, and 21 fill presets.
|
||||
PyTheory includes a complete drum system -- 51 synthesized percussion
|
||||
sounds, 85+ pattern presets across dozens of genres, and 30 fill presets.
|
||||
Every sound is generated from waveforms; no samples needed.
|
||||
|
||||
Drum Sounds
|
||||
@@ -91,7 +91,7 @@ The ``DrumSound`` enum maps to General MIDI percussion note numbers:
|
||||
>>> DrumSound.CLOSED_HAT.value
|
||||
42
|
||||
|
||||
All 27 sounds, organized by type:
|
||||
All 51 sounds, organized by type:
|
||||
|
||||
**Kicks:** KICK (36)
|
||||
|
||||
@@ -106,7 +106,24 @@ All 27 sounds, organized by type:
|
||||
**Percussion:** COWBELL (56), CLAVE (75), SHAKER (70), TAMBOURINE (54),
|
||||
CONGA_HIGH (63), CONGA_LOW (64), BONGO_HIGH (60), BONGO_LOW (61),
|
||||
TIMBALE_HIGH (65), TIMBALE_LOW (66), AGOGO_HIGH (67), AGOGO_LOW (68),
|
||||
GUIRO (73), MARACAS (70)
|
||||
GUIRO (73)
|
||||
|
||||
**Tabla:** TABLA_NA (86), TABLA_TIN (87), TABLA_GE (88), TABLA_DHA (89),
|
||||
TABLA_TIT (90), TABLA_KE (91), TABLA_GE_BEND (108 -- bayan with upward
|
||||
pitch bend from palm pressing into the head)
|
||||
|
||||
**Dhol:** DHOL_DAGGA (92), DHOL_TILLI (93), DHOL_BOTH (94)
|
||||
|
||||
**Dholak:** DHOLAK_GE (95), DHOLAK_NA (96), DHOLAK_TIT (97)
|
||||
|
||||
**Mridangam:** MRIDANGAM_THAM (98), MRIDANGAM_NAM (99), MRIDANGAM_DIN (100),
|
||||
MRIDANGAM_THA (101)
|
||||
|
||||
**Djembe:** DJEMBE_BASS (102), DJEMBE_TONE (103), DJEMBE_SLAP (104)
|
||||
|
||||
**Cajón:** CAJON_SLAP (109), CAJON_TAP (110)
|
||||
|
||||
**Metal Kit:** METAL_KICK (105), METAL_SNARE (106), METAL_HAT (107)
|
||||
|
||||
Drum Synthesis
|
||||
--------------
|
||||
@@ -145,8 +162,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 +210,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, cajón --
|
||||
Deep traditions from across the globe, each with authentic sound sets and
|
||||
idiomatic patterns. See the World Percussion section below for details.
|
||||
|
||||
**Other:** funk, hip hop, bo diddley, second line, new orleans, waltz,
|
||||
12/8 blues, country, gospel, flamenco -- Everything else. The syncopated
|
||||
@@ -228,14 +252,17 @@ ending and a new one is about to begin. Without fills, a drum pattern
|
||||
just loops. With them, it breathes and has structure.
|
||||
|
||||
``Pattern.fill()`` loads a 1-bar drum fill -- a short break that
|
||||
transitions between sections. 21 fill presets are available:
|
||||
transitions between sections. 30 fill presets are available:
|
||||
|
||||
.. code-block:: pycon
|
||||
|
||||
>>> Pattern.list_fills()
|
||||
['afrobeat', 'blast', 'bossa nova', 'breakdown', 'buildup',
|
||||
'cumbia', 'disco', 'funk', 'highlife', 'hip hop', 'house',
|
||||
'jazz', 'jazz brush', 'metal', 'reggae', 'rock', 'rock crash',
|
||||
'cajon breakdown', 'cajon flam', 'cajon rumble',
|
||||
'cumbia', 'disco', 'djembe break', 'djembe call', 'djembe roll',
|
||||
'funk', 'highlife', 'hip hop', 'house',
|
||||
'jazz', 'jazz brush', 'metal', 'metal blast', 'metal cascade',
|
||||
'metal triplet', 'reggae', 'rock', 'rock crash',
|
||||
'salsa', 'samba', 'second line', 'trap']
|
||||
|
||||
>>> fill = Pattern.fill("rock")
|
||||
@@ -304,6 +331,171 @@ 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.
|
||||
|
||||
**7 sounds** -- covering the primary tabla strokes (na, tin, tun, ge,
|
||||
dha, ke, tit) plus a bayan pitch bend sound (TABLA_GE_BEND) that
|
||||
models the technique of pressing the palm into the bayan head to bend
|
||||
the pitch upward.
|
||||
|
||||
**7 patterns:** teental (16 beats, the most common taal), jhaptaal
|
||||
(10 beats), rupak (7 beats), dadra (6 beats), keherwa (8 beats, folk
|
||||
and light classical), tabla solo, and tiri kita (fast ornamental
|
||||
pattern).
|
||||
|
||||
**5 fills:** tihai (3x crescendo landing on sam), chakkardar (32nd
|
||||
triplet cascade into slam), tiri kita (rapid 16th-note dayan burst),
|
||||
bayan (deep bass bends showcase), tabla call (dayan/bayan call-and-response).
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
score.drums("teental", repeats=4, fill="tihai")
|
||||
score.drums("keherwa", repeats=4, fill="chakkardar")
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
score = Score("4/4", bpm=80)
|
||||
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).
|
||||
|
||||
**8 patterns:** djembe (basic accompanying rhythm), kuku (Guinean harvest
|
||||
dance), soli (powerful Mandinka rhythm), dununba (heavy bass-driven),
|
||||
tiriba (joyful Susu rhythm), yankadi (gentle greeting/welcome), djansa
|
||||
(fast Malinke dance), mendiani (women's celebratory dance).
|
||||
|
||||
**3 fills:** djembe call (bass-tone-slap conversation building to climax),
|
||||
djembe roll (rapid slaps accelerating into bass), djembe break (syncopated
|
||||
West African-style break).
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
score = Score("4/4", bpm=120)
|
||||
score.drums("djembe", repeats=8, fill="djembe call", fill_every=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).
|
||||
|
||||
**4 fills:** metal (double kick 16ths with descending toms), metal triplet
|
||||
(double kick triplets with snare accents), metal blast (alternating
|
||||
snare/kick 32nds into half-time crash), metal cascade (descending snare
|
||||
roll → kick roll → alternating → crash ending).
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
score = Score("4/4", bpm=200)
|
||||
score.drums("metal blast", repeats=8, fill="metal cascade", fill_every=4)
|
||||
|
||||
Cajón
|
||||
~~~~~
|
||||
|
||||
The cajón is a box-shaped percussion instrument from Peru, now
|
||||
ubiquitous in acoustic and unplugged settings worldwide. Players sit
|
||||
on the box and strike the front face with their hands.
|
||||
|
||||
**3 sounds** -- bass (deep center thump), slap (sharp, snare-like edge
|
||||
hit with wire buzz), and tap (light finger tap).
|
||||
|
||||
**3 patterns:** cajon (basic groove), cajon rumba (flamenco-style rumba),
|
||||
and cajon folk (folk/acoustic pattern).
|
||||
|
||||
**3 fills:** cajon flam (slaps accelerating into bass hits), cajon rumble
|
||||
(fast taps building to slap accents), cajon breakdown (syncopated
|
||||
bass-slap groove).
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
score = Score("4/4", bpm=100)
|
||||
score.drums("cajon", repeats=8, fill="cajon flam", fill_every=4)
|
||||
|
||||
MIDI Export
|
||||
-----------
|
||||
|
||||
|
||||
+96
-10
@@ -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
|
||||
------
|
||||
|
||||
@@ -757,9 +841,11 @@ processes each section independently:
|
||||
lead.arpeggio("Gm", bars=4, pattern="updown", octaves=2)
|
||||
|
||||
Any parameter can be automated: ``lowpass``, ``lowpass_q``, ``highpass``,
|
||||
``reverb``, ``reverb_decay``, ``delay``, ``delay_time``, ``delay_feedback``,
|
||||
``distortion``, ``distortion_drive``, ``chorus``, ``phaser``, ``phaser_rate``,
|
||||
``saturation``, ``tremolo_depth``, ``tremolo_rate``, ``volume``.
|
||||
``reverb``, ``reverb_decay``, ``reverb_type``, ``delay``, ``delay_time``,
|
||||
``delay_feedback``, ``distortion``, ``distortion_drive``, ``chorus``,
|
||||
``phaser``, ``phaser_rate``, ``saturation``, ``tremolo_depth``,
|
||||
``tremolo_rate``, ``cabinet``, ``cabinet_brightness``, ``analog_drift``,
|
||||
``volume``.
|
||||
|
||||
LFO Automation
|
||||
--------------
|
||||
|
||||
+12
-1
@@ -66,6 +66,17 @@ the mix louder and punchier:
|
||||
chords.add(Chord.from_symbol(sym), Duration.WHOLE)
|
||||
play_score(score)
|
||||
|
||||
The render pipeline respects the Score's ``temperament`` and
|
||||
``reference_pitch`` settings, so Baroque or microtonal scores play back
|
||||
at the correct tuning:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
score = Score("4/4", bpm=80, temperament="meantone", reference_pitch=415.0)
|
||||
|
||||
Press **Ctrl+C** at any time during playback to stop — PyTheory catches
|
||||
``KeyboardInterrupt`` and stops audio cleanly.
|
||||
|
||||
See :doc:`sequencing` for how to build scores and parts.
|
||||
|
||||
render_score() -- Headless Rendering
|
||||
@@ -153,7 +164,7 @@ Play a drum pattern through the speakers:
|
||||
play_pattern(Pattern.preset("rock"), repeats=4, bpm=120)
|
||||
play_pattern(Pattern.preset("bossa nova"), repeats=4, bpm=140)
|
||||
|
||||
See :doc:`drums` for the full list of 58 presets and 21 fills.
|
||||
See :doc:`drums` for the full list of 80+ presets and 21 fills.
|
||||
|
||||
play_progression() -- Quick Chord Playback
|
||||
------------------------------------------
|
||||
|
||||
@@ -574,3 +574,115 @@ Define sections with ``score.section()`` and repeat them with
|
||||
Use any names you want — ``"intro"``, ``"verse"``, ``"chorus"``,
|
||||
``"bridge"``, ``"drop"``, ``"breakdown"``, ``"outro"``, or anything
|
||||
that makes sense for your song. The names are just labels.
|
||||
|
||||
Guitar Strumming
|
||||
----------------
|
||||
|
||||
Any part with a fretboard can strum chords using real fingering
|
||||
positions. The ``strum()`` method looks up the chord on the fretboard,
|
||||
gets the correct voicing, and plays all strings as a chord.
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
from pytheory import Fretboard
|
||||
|
||||
guitar = score.part("guitar", instrument="acoustic_guitar",
|
||||
fretboard=Fretboard.guitar())
|
||||
|
||||
guitar.strum("Am", Duration.HALF, direction="down")
|
||||
guitar.strum("G", Duration.HALF, direction="up")
|
||||
guitar.strum("F", Duration.WHOLE)
|
||||
|
||||
Works with any fretboard instrument — guitar, ukulele, banjo, mandolin.
|
||||
Works with any guitar preset — clean, crunch, distorted, orange, metal.
|
||||
|
||||
Pitch Bends
|
||||
-----------
|
||||
|
||||
Bend a note's pitch up or down over its duration. Essential for guitar
|
||||
bends, sitar meends, trombone slides, and vocal-style expression.
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
# Guitar bend: D up to E (2 semitones)
|
||||
guitar.add("D4", Duration.HALF, bend=2, bend_type="smooth")
|
||||
|
||||
# Release bend: E back down to D
|
||||
guitar.add("E4", Duration.HALF, bend=-2)
|
||||
|
||||
# Blues curl: hold then bend at the end
|
||||
guitar.add("C4", Duration.HALF, bend=1, bend_type="late")
|
||||
|
||||
Three bend types:
|
||||
|
||||
- ``"smooth"`` — logarithmic (default). Perceptually even pitch change.
|
||||
- ``"linear"`` — linear frequency interpolation. Mechanical/synth feel.
|
||||
- ``"late"`` — holds the starting pitch for 60%, bends in the last 40%.
|
||||
The classic blues "curl."
|
||||
|
||||
Rolls
|
||||
-----
|
||||
|
||||
Rapid repeated notes with a velocity ramp — perfect for timpani
|
||||
rolls, snare rolls, tremolo on any instrument. The velocity ramps
|
||||
from ``velocity_start`` to ``velocity_end`` for crescendo or
|
||||
decrescendo effects.
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
# Timpani crescendo roll
|
||||
timp = score.part("timp", instrument="timpani")
|
||||
timp.roll("C3", Duration.WHOLE, velocity_start=20, velocity_end=110)
|
||||
timp.add("C3", Duration.HALF, velocity=127) # big accent
|
||||
|
||||
# Snare roll with 32nd notes
|
||||
snare = score.part("snare", synth="noise", envelope="pluck")
|
||||
snare.roll("C4", Duration.HALF, speed=0.125,
|
||||
velocity_start=40, velocity_end=100)
|
||||
|
||||
# Decrescendo (loud to quiet)
|
||||
timp.roll("G2", Duration.WHOLE, velocity_start=100, velocity_end=30)
|
||||
|
||||
Parameters:
|
||||
|
||||
- ``velocity_start``: Starting velocity (default 40).
|
||||
- ``velocity_end``: Ending velocity (default 100).
|
||||
- ``speed``: Note subdivision (default ``Duration.SIXTEENTH``).
|
||||
Use ``0.125`` for 32nd notes, ``Duration.EIGHTH`` for 8th notes.
|
||||
|
||||
Tuning Systems
|
||||
--------------
|
||||
|
||||
A Score can use any tuning system and temperament:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
# Baroque harpsichord — meantone tuning, A=415
|
||||
score = Score("4/4", bpm=80, temperament="meantone",
|
||||
reference_pitch=415.0)
|
||||
|
||||
# Indian classical — 22-shruti system
|
||||
score = Score("4/4", bpm=75, system="shruti")
|
||||
|
||||
# Just intonation — pure intervals
|
||||
score = Score("4/4", bpm=90, temperament="just")
|
||||
|
||||
The Score constructor accepts these tuning parameters:
|
||||
|
||||
- ``system``: Musical system name (default ``"western"``). Any system
|
||||
from :doc:`systems` works — ``"indian"``, ``"shruti"``, ``"maqam"``,
|
||||
``"carnatic"``, etc. Note strings in ``Part.add()`` are parsed against
|
||||
this system.
|
||||
- ``temperament``: Tuning temperament — ``"equal"`` (default),
|
||||
``"pythagorean"``, ``"meantone"``, ``"just"``.
|
||||
- ``reference_pitch``: Concert pitch in Hz (default 440.0). Use 415.0
|
||||
for Baroque tuning, 432.0 for "Verdi tuning", etc.
|
||||
|
||||
Custom equal temperaments via the ``TET()`` factory:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
from pytheory import TET
|
||||
|
||||
edo19 = TET(19) # 19-tone equal temperament
|
||||
score = Score("4/4", bpm=100, system=edo19)
|
||||
|
||||
+342
-9
@@ -1,7 +1,7 @@
|
||||
Synthesizers
|
||||
============
|
||||
|
||||
PyTheory includes 13 built-in waveforms and 10 ADSR envelope presets.
|
||||
PyTheory includes 41 built-in waveforms and 10 ADSR envelope presets.
|
||||
Every sound is generated from scratch -- no samples or external audio
|
||||
files needed.
|
||||
|
||||
@@ -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,336 @@ 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
|
||||
31 dedicated instrument synths. Each one uses tailored synthesis
|
||||
techniques -- additive harmonics, formant shaping, body resonance
|
||||
modeling, and specialized envelopes -- to capture the character of a
|
||||
specific acoustic instrument. These are the waveforms that bring the
|
||||
total count to 41.
|
||||
|
||||
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")
|
||||
|
||||
Timpani Synth
|
||||
~~~~~~~~~~~~~
|
||||
|
||||
Large kettle drum with definite pitch. Inharmonic membrane modes
|
||||
(1.0, 1.5, 1.99, 2.44), felt mallet attack, copper kettle resonance.
|
||||
Use ``Part.roll()`` for crescendo timpani rolls.
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
timp = score.part("timp", synth="timpani_synth")
|
||||
timp.roll("C3", Duration.WHOLE, velocity_start=20, velocity_end=110)
|
||||
|
||||
Saxophone Synth
|
||||
~~~~~~~~~~~~~~~
|
||||
|
||||
Single reed through a conical brass bore. All harmonics with strong
|
||||
mids, reed buzz, and brass body warmth. Four presets: ``saxophone``,
|
||||
``alto_sax``, ``tenor_sax``, ``bari_sax``.
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
sax = score.part("sax", instrument="tenor_sax")
|
||||
|
||||
Pedal Steel Synth
|
||||
~~~~~~~~~~~~~~~~~
|
||||
|
||||
The Nashville crying sound — singing harmonics with slow vibrato
|
||||
and long sustain. Pairs naturally with spring reverb.
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
steel = score.part("steel", instrument="pedal_steel")
|
||||
|
||||
Theremin Synth
|
||||
~~~~~~~~~~~~~~
|
||||
|
||||
Pure sine with natural hand wobble — the eerie sci-fi sound.
|
||||
Best used with legato and glide for continuous pitch.
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
theremin = score.part("theremin", instrument="theremin")
|
||||
|
||||
Kalimba Synth
|
||||
~~~~~~~~~~~~~
|
||||
|
||||
Metal tines on a wooden body. Bright, bell-like attack with
|
||||
inharmonic overtones (modes at 1x, 2.92x, 5.4x).
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
kalimba = score.part("kalimba", instrument="kalimba")
|
||||
|
||||
Steel Drum Synth
|
||||
~~~~~~~~~~~~~~~~
|
||||
|
||||
Hammered metal pan with bright, ringing, tropical character.
|
||||
Inharmonic partials at 2.0x, 3.01x, 4.1x, 5.3x.
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
pan = score.part("pan", instrument="steel_drum")
|
||||
|
||||
Accordion Synth
|
||||
~~~~~~~~~~~~~~~
|
||||
|
||||
Musette-tuned doubled reeds — two slightly detuned reed sets
|
||||
create natural beating. Bellows pressure swell modulates amplitude.
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
acc = score.part("acc", instrument="accordion")
|
||||
|
||||
Didgeridoo Synth
|
||||
~~~~~~~~~~~~~~~~
|
||||
|
||||
Deep cylindrical drone with shifting formant overtones. The
|
||||
overtone singing effect sweeps a resonant peak between 500-1500Hz.
|
||||
Best with cave reverb.
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
didg = score.part("didg", instrument="didgeridoo")
|
||||
|
||||
Bagpipe Synth
|
||||
~~~~~~~~~~~~~
|
||||
|
||||
Bright chanter reed with constant bag pressure. All harmonics
|
||||
peaked around 3-7 (the piercing brightness). No dynamics — always ff.
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
pipes = score.part("pipes", instrument="bagpipe")
|
||||
|
||||
Banjo Synth
|
||||
~~~~~~~~~~~
|
||||
|
||||
Steel strings on a drum-head membrane body. The membrane gives
|
||||
nasal, ringy resonance with faster decay than guitar.
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
banjo = score.part("banjo", instrument="banjo")
|
||||
|
||||
Mandolin Synth
|
||||
~~~~~~~~~~~~~~
|
||||
|
||||
Paired steel strings in 4 courses — natural chorus from the
|
||||
doubled unison strings. Bright, ringing, fast attack.
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
mando = score.part("mando", instrument="mandolin")
|
||||
|
||||
Ukulele Synth
|
||||
~~~~~~~~~~~~~
|
||||
|
||||
Nylon strings on a small body. Mid-heavy resonance (no deep bass),
|
||||
softer attack than guitar, shorter sustain.
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
uke = score.part("uke", instrument="ukulele")
|
||||
|
||||
Granular Synth
|
||||
~~~~~~~~~~~~~~
|
||||
|
||||
Grain cloud synthesis — chops a source waveform into tiny overlapping
|
||||
grains (10-200ms), each windowed and optionally pitch/time scattered.
|
||||
Creates textures impossible with other synthesis: frozen tones,
|
||||
shimmering clouds, evolving pads, glitchy stutters.
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
# Atmospheric granular pad
|
||||
pad = score.part("pad", instrument="granular_pad")
|
||||
|
||||
# Granular with filter envelope sweep + resonance
|
||||
texture = score.part("texture", synth="granular_synth", envelope="pad",
|
||||
filter_amount=4000, filter_attack=0.5,
|
||||
filter_decay=1.5, filter_sustain=0.3,
|
||||
lowpass=600, lowpass_q=3.0,
|
||||
reverb=0.5, reverb_type="taj_mahal")
|
||||
|
||||
Parameters (passed as synth kwargs):
|
||||
|
||||
- ``grain_size``: Duration per grain in seconds (default 0.04).
|
||||
- ``density``: Grains per second (default 50). Higher = denser cloud.
|
||||
- ``scatter``: Random position jitter 0-1 (default 0.5).
|
||||
- ``pitch_var``: Per-grain pitch randomization in cents (default 12).
|
||||
- ``source``: Base waveform — ``"saw"``, ``"sine"``, ``"triangle"``,
|
||||
``"square"``, ``"noise"``.
|
||||
|
||||
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 — 60+ predefined combinations that approximate real
|
||||
instruments:
|
||||
|
||||
.. code-block:: python
|
||||
@@ -403,20 +728,28 @@ instruments:
|
||||
|
||||
Available instruments:
|
||||
|
||||
**Keys**: piano, electric_piano, organ, harpsichord, celesta, music_box
|
||||
**Keys**: piano, electric_piano, organ, harpsichord, celesta, music_box,
|
||||
accordion
|
||||
|
||||
**Strings**: violin, viola, cello, contrabass, string_ensemble
|
||||
|
||||
**Woodwinds**: flute, clarinet, oboe, bassoon
|
||||
**Woodwinds**: flute, clarinet, oboe, bassoon, saxophone, alto_sax,
|
||||
tenor_sax, bari_sax
|
||||
|
||||
**Brass**: trumpet, trombone, french_horn, tuba, brass_ensemble
|
||||
|
||||
**Plucked**: acoustic_guitar, electric_guitar, distorted_guitar,
|
||||
bass_guitar, upright_bass, harp, sitar, koto
|
||||
**Plucked**: acoustic_guitar, electric_guitar, clean_guitar, crunch_guitar,
|
||||
distorted_guitar, orange_crunch, metal_guitar, bass_guitar, upright_bass,
|
||||
harp, sitar, koto, banjo, mandolin, mandola, ukulele
|
||||
|
||||
**Synth**: synth_lead, synth_pad, synth_bass, acid_bass, 808_bass
|
||||
**World/Exotic**: pedal_steel, theremin, kalimba, steel_drum, didgeridoo,
|
||||
bagpipe
|
||||
|
||||
**Percussion**: vibraphone, marimba, xylophone, glockenspiel, tubular_bells
|
||||
**Synth**: synth_lead, synth_pad, synth_bass, acid_bass, 808_bass,
|
||||
granular_pad, granular_texture, vocal, choir
|
||||
|
||||
**Percussion**: vibraphone, marimba, xylophone, glockenspiel, tubular_bells,
|
||||
timpani
|
||||
|
||||
Explicit kwargs override preset defaults:
|
||||
|
||||
|
||||
+118
-4
@@ -1,10 +1,11 @@
|
||||
Musical Systems
|
||||
===============
|
||||
|
||||
PyTheory supports **six musical systems**, each with its own tone names,
|
||||
scale patterns, and centuries of tradition behind them. Every system
|
||||
maps onto the same 12-tone equal temperament backbone, so you can
|
||||
compare scales across cultures and even combine them in your own music.
|
||||
PyTheory supports **16 musical systems** — 6 core systems mapped onto
|
||||
12-tone equal temperament, plus 10 microtonal systems with their own
|
||||
native tunings. The core systems let you compare scales across cultures;
|
||||
the microtonal systems go beyond 12-TET into genuinely different pitch
|
||||
universes.
|
||||
|
||||
Western
|
||||
-------
|
||||
@@ -271,4 +272,117 @@ produce the same pitches:
|
||||
>>> do4.frequency
|
||||
261.6255653005986
|
||||
|
||||
Microtonal Systems
|
||||
------------------
|
||||
|
||||
Beyond the six 12-TET core systems, PyTheory includes 10 microtonal
|
||||
systems that use their own native tunings — more notes per octave,
|
||||
just intonation ratios, or entirely alien pitch structures.
|
||||
|
||||
Shruti (22 tones per octave)
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
The Indian 22-shruti system divides the octave into 22 unequal steps
|
||||
using just intonation ratios. These microtonal inflections are what
|
||||
give classical Indian music its characteristic expressiveness — pitches
|
||||
that fall "between the cracks" of the piano.
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
score = Score("4/4", bpm=75, system="shruti")
|
||||
|
||||
Maqam (24 tones per octave)
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
The Arabic 24-tone system adds Zalzalian quarter-tone intervals
|
||||
(derived from just intonation ratios of 11 and 13) to the standard
|
||||
12 tones. These "neutral" intervals — halfway between major and minor —
|
||||
are the soul of maqam music.
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
score = Score("4/4", bpm=90, system="maqam")
|
||||
|
||||
Slendro (5-TET)
|
||||
~~~~~~~~~~~~~~~~
|
||||
|
||||
The Javanese slendro scale — 5 equal divisions of the octave. Each
|
||||
step is 240 cents, wider than any Western interval. Ethereal and
|
||||
floating.
|
||||
|
||||
Pelog (9-TET)
|
||||
~~~~~~~~~~~~~
|
||||
|
||||
Approximation of the Javanese pelog tuning as 9 equal divisions of
|
||||
the octave.
|
||||
|
||||
Thai (7-TET)
|
||||
~~~~~~~~~~~~~
|
||||
|
||||
Thai classical music divides the octave into 7 equal steps of ~171
|
||||
cents each — every interval is the same size.
|
||||
|
||||
Makam (53-TET)
|
||||
~~~~~~~~~~~~~~
|
||||
|
||||
Turkish makam music uses 53 equal divisions of the octave — fine
|
||||
enough to approximate virtually any just interval. The system that
|
||||
underlies Ottoman classical music.
|
||||
|
||||
Carnatic (72-TET)
|
||||
~~~~~~~~~~~~~~~~~
|
||||
|
||||
South Indian Carnatic music theory describes 72 melakarta ragas.
|
||||
The 72-TET system provides enough resolution to represent all the
|
||||
microtonal inflections of Carnatic practice.
|
||||
|
||||
19-TET and 31-TET
|
||||
~~~~~~~~~~~~~~~~~~
|
||||
|
||||
Extended equal temperaments that offer better approximations of
|
||||
just intonation intervals than 12-TET. 19-TET has excellent major
|
||||
thirds; 31-TET closely matches quarter-comma meantone.
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
score = Score("4/4", bpm=100, system="19-tet")
|
||||
|
||||
Bohlen-Pierce (13 equal divisions of the tritave)
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
A genuinely alien tuning system — 13 equal divisions of the
|
||||
**tritave** (3:1 ratio) instead of the octave (2:1). No octaves, no
|
||||
fifths, built on 3:5:7 harmonics. Used by experimental composers.
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
score = Score("4/4", bpm=100, system="bohlen-pierce")
|
||||
|
||||
The TET() Factory
|
||||
~~~~~~~~~~~~~~~~~
|
||||
|
||||
Create any equal temperament on the fly with the ``TET()`` factory:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
from pytheory import TET
|
||||
|
||||
edo19 = TET(19) # 19-tone equal temperament
|
||||
edo31 = TET(31) # 31-tone equal temperament
|
||||
score = Score("4/4", bpm=100, system=edo19)
|
||||
|
||||
Tone names in custom TET systems are integers (0, 1, 2, ..., n-1).
|
||||
|
||||
System.tone() Method
|
||||
~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
Any system can create a Tone directly:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
from pytheory import SYSTEMS
|
||||
|
||||
western = SYSTEMS["western"]
|
||||
c4 = western.tone("C", octave=4)
|
||||
|
||||
Music is universal, but every culture hears it differently. These systems are different maps of the same territory -- explore one you've never played in before and see what you find.
|
||||
|
||||
@@ -357,6 +357,45 @@ every tone knows its enharmonic spelling:
|
||||
>>> Tone.from_string("C4", system="western").enharmonic is None
|
||||
True
|
||||
|
||||
Extended Enharmonics
|
||||
~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
PyTheory supports the full range of enharmonic spellings used in real
|
||||
music theory:
|
||||
|
||||
- **Cb** and **Fb** — musically valid flats (Cb = B, Fb = E)
|
||||
- **E#** and **B#** — musically valid sharps (E# = F, B# = C)
|
||||
- **Double sharps** (``##`` or ``x``) — e.g. F## = G
|
||||
- **Double flats** (``bb``) — e.g. Dbb = C
|
||||
- **Unicode symbols** — ``♯`` (sharp), ``♭`` (flat), ``𝄪`` (double sharp),
|
||||
``𝄫`` (double flat) are all recognized and normalized to ASCII
|
||||
|
||||
.. code-block:: pycon
|
||||
|
||||
>>> Tone.from_string("Cb4") # resolves to B3 (octave boundary fix)
|
||||
<Tone B3>
|
||||
>>> Tone.from_string("B#4") # resolves to C5 (octave boundary fix)
|
||||
<Tone C5>
|
||||
>>> Tone.from_string("E#4") # resolves to F4
|
||||
<Tone F4>
|
||||
>>> Tone.from_string("Fb4") # resolves to E4
|
||||
<Tone E4>
|
||||
|
||||
The octave boundary is correctly handled: B# crosses up to the next
|
||||
octave (B#4 = C5), and Cb crosses down (Cb4 = B3), matching standard
|
||||
scientific pitch notation where the octave number increments at C.
|
||||
|
||||
Tone Validation
|
||||
~~~~~~~~~~~~~~~
|
||||
|
||||
Tones are validated on construction — if a tone name is not recognized
|
||||
in its system, a ``ValueError`` is raised:
|
||||
|
||||
.. code-block:: pycon
|
||||
|
||||
>>> Tone.from_string("X4") # not a valid tone name
|
||||
ValueError: ...
|
||||
|
||||
The Circle of Fifths
|
||||
--------------------
|
||||
|
||||
|
||||
+21
-14
@@ -18,8 +18,8 @@ Theory
|
||||
------
|
||||
|
||||
The theory layer works everywhere Python runs — no audio setup needed.
|
||||
Tones, scales, chords, keys, intervals, harmony, 6 musical systems,
|
||||
25 instruments:
|
||||
Tones, scales, chords, keys, intervals, harmony, 16 musical systems,
|
||||
60+ instruments:
|
||||
|
||||
.. code-block:: pycon
|
||||
|
||||
@@ -72,22 +72,29 @@ every time::
|
||||
What's Inside
|
||||
-------------
|
||||
|
||||
- **Theory** — tones, scales (40+ across 6 systems), chords (17 types),
|
||||
- **Theory** — tones, scales (40+ across 16 systems), chords (17 types),
|
||||
keys, Roman numeral analysis, figured bass, pitch class sets (Forte
|
||||
numbers), scale recommendation, modulation, voice leading
|
||||
numbers), scale recommendation, modulation, voice leading, enharmonic
|
||||
support (Cb, Fb, E#, B#, double sharps/flats, unicode symbols)
|
||||
- **Sequencing** — Score, Parts, arpeggiator, legato/glide, velocity,
|
||||
swing, humanize, tempo changes, song sections with repeat
|
||||
- **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
|
||||
swing, humanize, tempo changes, song sections with repeat, strumming,
|
||||
pitch bends (3 types), rolls, tuning systems (TET factory, 4
|
||||
temperaments, reference_pitch)
|
||||
- **Synthesis** — 41 waveforms (including Karplus-Strong pluck, Hammond organ,
|
||||
bowed string, granular, vocal/formant, and 31 dedicated instrument synths),
|
||||
10 envelopes, 60+ instrument presets, configurable FM, sub-oscillator,
|
||||
noise layer, filter envelope, velocity-to-brightness, analog oscillator
|
||||
drift, detune, stereo pan/spread, 80+ drum patterns (stereo panned,
|
||||
including world percussion and cajón), 21 fills, 11 microtonal systems
|
||||
- **Effects** — reverb (algorithmic + 7 convolution IRs, stereo), delay,
|
||||
lowpass/highpass (with resonance), distortion, saturation, chorus,
|
||||
phaser, tremolo, sidechain compression, automation, LFOs. Master bus
|
||||
compressor/limiter
|
||||
- **Instruments** — 25 presets with fingering generation
|
||||
lowpass/highpass (with resonance), distortion, guitar cabinet simulation,
|
||||
saturation, chorus, phaser, tremolo, analog drift, sidechain compression,
|
||||
automation, LFOs. Master bus compressor/limiter
|
||||
- **Instruments** — 60+ presets with fingering generation, guitar strumming,
|
||||
pitch bends, note choking
|
||||
- **Output** — stereo playback, WAV export, MIDI import/export
|
||||
- **Interface** — REPL with tab completion, CLI (15 commands), ``pytheory demo``
|
||||
- **Interface** — REPL with tab completion, CLI (15 commands), ``pytheory demo``,
|
||||
KeyboardInterrupt handling for clean stop
|
||||
- **AI-friendly** — Claude Code can compose
|
||||
and play music through PyTheory from natural language
|
||||
|
||||
|
||||
+1354
-257
File diff suppressed because it is too large
Load Diff
+1
-2
@@ -1,6 +1,6 @@
|
||||
[project]
|
||||
name = "pytheory"
|
||||
version = "0.33.0"
|
||||
version = "0.38.1"
|
||||
description = "Music Theory for Humans"
|
||||
readme = "README.md"
|
||||
license = "MIT"
|
||||
@@ -21,7 +21,6 @@ classifiers = [
|
||||
"Programming Language :: Python :: 3.13",
|
||||
]
|
||||
dependencies = [
|
||||
"pytuning",
|
||||
"numeral",
|
||||
"sounddevice",
|
||||
"scipy",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
"""PyTheory: Music Theory for Humans."""
|
||||
|
||||
__version__ = "0.33.0"
|
||||
__version__ = "0.38.1"
|
||||
|
||||
from .tones import Tone, Interval
|
||||
from .systems import System, SYSTEMS, TET
|
||||
|
||||
+159
-36
@@ -1,4 +1,4 @@
|
||||
from pytuning import scales
|
||||
import math
|
||||
|
||||
REFERENCE_A = 440
|
||||
|
||||
@@ -6,41 +6,59 @@ REFERENCE_A = 440
|
||||
# Scientific pitch notation changes octave at C, not A, so this offset
|
||||
# is needed for all octave arithmetic.
|
||||
C_INDEX = 3
|
||||
def _create_just_intonation_scale(n):
|
||||
"""5-limit just intonation ratios for 12-tone systems.
|
||||
|
||||
These are the pure frequency ratios derived from the harmonic series —
|
||||
the way intervals "want" to sound before equal temperament imposed
|
||||
compromise. Each ratio is mathematically exact: a perfect fifth is
|
||||
exactly 3/2, a major third is exactly 5/4.
|
||||
|
||||
For non-12 systems, falls back to equal temperament.
|
||||
# ── Temperament scale generators (replaces pytuning dependency) ──────────
|
||||
|
||||
def _create_edo_scale(n):
|
||||
"""N-tone equal division of the octave. Each step = 2^(1/n)."""
|
||||
return [2 ** (i / n) for i in range(n + 1)]
|
||||
|
||||
|
||||
def _create_pythagorean_scale(n):
|
||||
"""Pythagorean tuning — spiral of pure fifths (3/2 ratio).
|
||||
|
||||
Each tone is generated by stacking perfect fifths and octave-reducing.
|
||||
"""
|
||||
from fractions import Fraction
|
||||
ratios = [1.0]
|
||||
for i in range(1, n):
|
||||
# Stack fifths: (3/2)^i, then reduce to within one octave
|
||||
r = (3 / 2) ** i
|
||||
while r >= 2.0:
|
||||
r /= 2.0
|
||||
ratios.append(r)
|
||||
ratios.sort()
|
||||
ratios.append(2.0)
|
||||
return ratios
|
||||
|
||||
|
||||
def _create_quarter_comma_meantone_scale(n):
|
||||
"""Quarter-comma meantone — pure major thirds (5/4), tempered fifths.
|
||||
|
||||
The fifth is narrowed by 1/4 of a syntonic comma so that four
|
||||
fifths make a pure major third (5/4). The meantone fifth =
|
||||
5^(1/4) ≈ 1.49535.
|
||||
"""
|
||||
fifth = 5 ** 0.25 # meantone fifth ≈ 1.49535 (vs 1.5 pure)
|
||||
ratios = [1.0]
|
||||
for i in range(1, n):
|
||||
r = fifth ** i
|
||||
while r >= 2.0:
|
||||
r /= 2.0
|
||||
ratios.append(r)
|
||||
ratios.sort()
|
||||
ratios.append(2.0)
|
||||
return ratios
|
||||
def _create_just_intonation_scale(n):
|
||||
"""5-limit just intonation ratios for 12-tone systems."""
|
||||
if n != 12:
|
||||
return scales.create_edo_scale(n)
|
||||
# Standard 5-limit JI ratios (A-based: A=1/1)
|
||||
ratios = [
|
||||
Fraction(1, 1), # A — unison
|
||||
Fraction(16, 15), # A# — minor second
|
||||
Fraction(9, 8), # B — major second
|
||||
Fraction(6, 5), # C — minor third
|
||||
Fraction(5, 4), # C# — major third
|
||||
Fraction(4, 3), # D — perfect fourth
|
||||
Fraction(45, 32), # D# — augmented fourth
|
||||
Fraction(3, 2), # E — perfect fifth
|
||||
Fraction(8, 5), # F — minor sixth
|
||||
Fraction(5, 3), # F# — major sixth
|
||||
Fraction(9, 5), # G — minor seventh
|
||||
Fraction(15, 8), # G# — major seventh
|
||||
Fraction(2, 1), # A — octave
|
||||
]
|
||||
return [float(r) for r in ratios]
|
||||
return _create_edo_scale(n)
|
||||
return [1, 16/15, 9/8, 6/5, 5/4, 4/3, 45/32, 3/2, 8/5, 5/3, 9/5, 15/8, 2.0]
|
||||
|
||||
TEMPERAMENTS = {
|
||||
"equal": scales.create_edo_scale,
|
||||
"pythagorean": scales.create_pythagorean_scale,
|
||||
"meantone": scales.create_quarter_comma_meantone_scale,
|
||||
"equal": _create_edo_scale,
|
||||
"pythagorean": _create_pythagorean_scale,
|
||||
"meantone": _create_quarter_comma_meantone_scale,
|
||||
"just": _create_just_intonation_scale,
|
||||
}
|
||||
|
||||
@@ -295,8 +313,51 @@ DEGREES_SHRUTI = [
|
||||
("shadja", ()), # Sa (octave)
|
||||
]
|
||||
|
||||
# 22-shruti frequency ratios — 5-limit just intonation.
|
||||
# These are the REAL shruti intervals, NOT 22-TET approximations.
|
||||
# Based on the traditional Pythagorean/harmonic ratios from Indian
|
||||
# musicological treatises (Natya Shastra, Sangita Ratnakara).
|
||||
#
|
||||
# Ordered from Dha (A=1.0) to match our system indexing.
|
||||
# Sa is at index 5 (ratio ≈ 6/5 from Dha).
|
||||
from fractions import Fraction
|
||||
_SHRUTI_RATIOS_FROM_SA = [
|
||||
Fraction(1, 1), # 0: Sa — 1/1
|
||||
Fraction(256, 243), # 1: atikomal Re — Pythagorean limma
|
||||
Fraction(16, 15), # 2: komal Re — JI minor second
|
||||
Fraction(10, 9), # 3: shuddha Re — minor whole tone
|
||||
Fraction(9, 8), # 4: Re — major whole tone
|
||||
Fraction(32, 27), # 5: atikomal Ga — Pythagorean minor 3rd
|
||||
Fraction(6, 5), # 6: komal Ga — JI minor 3rd
|
||||
Fraction(5, 4), # 7: Ga — JI major 3rd
|
||||
Fraction(81, 64), # 8: tivra Ga — Pythagorean major 3rd
|
||||
Fraction(4, 3), # 9: Ma — perfect 4th
|
||||
Fraction(27, 20), # 10: ekashruti Ma
|
||||
Fraction(45, 32), # 11: tivra Ma — augmented 4th
|
||||
Fraction(729, 512), # 12: atitivra Ma — Pythagorean tritone
|
||||
Fraction(3, 2), # 13: Pa — perfect 5th
|
||||
Fraction(128, 81), # 14: atikomal Dha — Pythagorean minor 6th
|
||||
Fraction(8, 5), # 15: komal Dha — JI minor 6th
|
||||
Fraction(5, 3), # 16: shuddha Dha
|
||||
Fraction(27, 16), # 17: Dha — Pythagorean major 6th
|
||||
Fraction(16, 9), # 18: komal Ni — Pythagorean minor 7th
|
||||
Fraction(9, 5), # 19: shuddha Ni — JI minor 7th
|
||||
Fraction(15, 8), # 20: Ni — JI major 7th
|
||||
Fraction(243, 128), # 21: tivra Ni — Pythagorean major 7th
|
||||
]
|
||||
|
||||
# Rotate to start from Dha (index 17 in the Sa-based list above).
|
||||
# Dha = 27/16 from Sa. We divide all ratios by 27/16 and wrap.
|
||||
_dha_ratio = _SHRUTI_RATIOS_FROM_SA[17]
|
||||
SHRUTI_RATIOS = []
|
||||
for i in range(22):
|
||||
sa_idx = (i + 17) % 22 # rotate: Dha=0, komalNi=1, ..., Sa=5, ...
|
||||
r = _SHRUTI_RATIOS_FROM_SA[sa_idx] / _dha_ratio
|
||||
if r < 1:
|
||||
r *= 2 # wrap into the same octave
|
||||
SHRUTI_RATIOS.append(float(r))
|
||||
|
||||
# 22-shruti thaat scales with proper microtonal intervals.
|
||||
# Each interval is counted in shrutis (22-TET steps).
|
||||
# Compare to the 12-TET approximations in INDIAN_SCALES which lose
|
||||
# the distinction between 2-shruti and 3-shruti steps.
|
||||
SHRUTI_SCALES = {
|
||||
@@ -341,13 +402,75 @@ SHRUTI_SCALES = {
|
||||
],
|
||||
}
|
||||
|
||||
# ── 24-TET Arabic maqam system ─────────────────────────────────────────────
|
||||
# Arabic maqam uses quarter-tones (half-flat, half-sharp). 24-TET captures
|
||||
# these intervals exactly. Each step = 50 cents (vs 100 in 12-TET).
|
||||
# The half-flat (♭½) is the defining sound of Arabic music — it's what
|
||||
# makes maqam Rast and Bayati sound distinctly Middle Eastern.
|
||||
# ── Arabic maqam system ───────────────────────────────────────────────────
|
||||
# Arabic maqam uses quarter-tones with specific JI ratios, NOT equal
|
||||
# 24-TET divisions. The neutral intervals (quarter-flat, quarter-sharp)
|
||||
# are based on ratios involving the 11th partial, as theorized by
|
||||
# Zalzal (8th century Baghdad). The quarter-flat E in Rast is 27/22,
|
||||
# not simply halfway between Eb and E.
|
||||
#
|
||||
# 24 positions per octave, but with unequal JI spacing.
|
||||
# Ordered from La (=A) to match Western index positions.
|
||||
|
||||
# Maqam JI ratios from Do (C). Based on traditional practice:
|
||||
# - Standard JI intervals for the 12 chromatic positions
|
||||
# - Zalzalian ratios (11-limit) for the quarter-tone positions
|
||||
_MAQAM_RATIOS_FROM_DO = [
|
||||
Fraction(1, 1), # 0: Do — unison
|
||||
Fraction(33, 32), # 1: Do↑ — quarter-sharp (~53¢, 33rd harmonic)
|
||||
Fraction(16, 15), # 2: Reb — JI minor 2nd
|
||||
Fraction(12, 11), # 3: Re↓ — Zalzalian neutral 2nd (~151¢)
|
||||
Fraction(9, 8), # 4: Re — major whole tone
|
||||
Fraction(11, 9) * Fraction(1, 1), # 5: Re↑ — undecimal (~347¢... too high)
|
||||
Fraction(6, 5), # 6: Mib — JI minor 3rd
|
||||
Fraction(27, 22), # 7: Mi↓ — Zalzalian neutral 3rd (~355¢) THE Rast note
|
||||
Fraction(5, 4), # 8: Mi — JI major 3rd
|
||||
Fraction(4, 3), # 9: Fa — perfect 4th
|
||||
Fraction(11, 8), # 10: Fa↑ — undecimal tritone (~551¢)
|
||||
Fraction(45, 32), # 11: Fa# — augmented 4th
|
||||
Fraction(22, 15), # 12: Sol↓ — neutral (~663¢... adjusted)
|
||||
Fraction(3, 2), # 13: Sol — perfect 5th
|
||||
Fraction(99, 64), # 14: Sol↑ — quarter-sharp 5th
|
||||
Fraction(8, 5), # 15: Lab — JI minor 6th
|
||||
Fraction(18, 11), # 16: La↓ — Zalzalian neutral 6th
|
||||
Fraction(5, 3), # 17: La — JI major 6th
|
||||
Fraction(27, 16), # 18: La↑/Sib↓ — Pythagorean major 6th
|
||||
Fraction(16, 9), # 19: Sib — Pythagorean minor 7th
|
||||
Fraction(11, 6), # 20: Si↓ — undecimal neutral 7th
|
||||
Fraction(15, 8), # 21: Si — JI major 7th
|
||||
Fraction(243, 128), # 22: Si↑ — Pythagorean major 7th
|
||||
Fraction(2, 1) * Fraction(33, 64), # 23: near-octave (~1049¢)
|
||||
]
|
||||
|
||||
# Ratios directly from La (A=1/1), each position defined explicitly.
|
||||
# Standard JI intervals for chromatic positions, Zalzalian (11-limit)
|
||||
# ratios for the quarter-tone positions.
|
||||
MAQAM_RATIOS = [
|
||||
1.0, # 0: La — A (unison)
|
||||
float(Fraction(256, 243)), # 1: La↑ — Pythagorean comma up
|
||||
float(Fraction(16, 15)), # 2: Sib — Bb (JI minor 2nd)
|
||||
float(Fraction(12, 11)), # 3: Si↓ — B quarter-flat (Zalzalian)
|
||||
float(Fraction(9, 8)), # 4: Si — B (major 2nd)
|
||||
float(Fraction(6, 5)), # 5: Do — C (minor 3rd from A)
|
||||
float(Fraction(11, 9)), # 6: Do↑ — C quarter-sharp (undecimal)
|
||||
float(Fraction(5, 4)), # 7: Reb — Db (major 3rd from A...= JI Db)
|
||||
float(Fraction(9, 7)), # 8: Re↓ — D quarter-flat (septimal)
|
||||
float(Fraction(4, 3)), # 9: Re — D (perfect 4th from A)
|
||||
float(Fraction(11, 8)), # 10: Re↑ — D quarter-sharp (undecimal)
|
||||
float(Fraction(45, 32)), # 11: Mib — Eb (augmented 4th from A)
|
||||
float(Fraction(6, 5) * Fraction(27, 22)), # 12: Mi↓ — E quarter-flat (Do × 27/22)
|
||||
float(Fraction(3, 2)), # 13: Mi — E (perfect 5th from A)
|
||||
float(Fraction(8, 5)), # 14: Fa — F (minor 6th from A)
|
||||
float(Fraction(18, 11)), # 15: Fa↑ — F quarter-sharp (Zalzalian)
|
||||
float(Fraction(5, 3)), # 16: Fa# — F# (major 6th from A)
|
||||
float(Fraction(27, 16)), # 17: Sol↓ — G quarter-flat
|
||||
float(Fraction(16, 9)), # 18: Sol — G (minor 7th from A)
|
||||
float(Fraction(11, 6)), # 19: Sol↑ — G quarter-sharp (undecimal)
|
||||
float(Fraction(15, 8)), # 20: Lab — Ab (major 7th from A)
|
||||
float(Fraction(27, 14)), # 21: La↓ — A quarter-flat (septimal)
|
||||
float(Fraction(243, 128)), # 22: La½b — near-octave
|
||||
float(Fraction(2, 1) * Fraction(256, 257)), # 23: La♮ — near-octave
|
||||
]
|
||||
TONES_ARABIC_24 = [
|
||||
("La",), # 0 — A
|
||||
("La↑",), # 1 — A quarter-sharp
|
||||
|
||||
+76
-1
@@ -275,6 +275,78 @@ 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"},
|
||||
{"name": "Theremin Noir", "key": ("A", "minor"), "drums": "hip hop",
|
||||
"fill": "rock", "bpm": 85,
|
||||
"prog": ("i", "iv", "V", "i"),
|
||||
"lead": ("theremin_synth", "pad", 0.4, 0.0),
|
||||
"pad": ("strings_synth", "pad", 0.0),
|
||||
"bass_lp": 300, "reverb_type": "cave"},
|
||||
{"name": "Caribbean", "key": ("C", "major"), "drums": "reggae",
|
||||
"fill": "reggae", "bpm": 110,
|
||||
"prog": ("I", "IV", "V", "IV"),
|
||||
"lead": ("steel_drum_synth", "none", 0.25, 0.3),
|
||||
"pad": ("acoustic_guitar_synth", "none", -0.3),
|
||||
"bass_lp": 500, "reverb_type": "plate"},
|
||||
{"name": "Accordion Waltz", "key": ("D", "minor"), "drums": "waltz",
|
||||
"fill": "jazz", "bpm": 88,
|
||||
"prog": ("i", "iv", "V", "i"),
|
||||
"lead": ("accordion_synth", "organ", 0.2, 0.1),
|
||||
"pad": ("strings_synth", "pad", -0.2),
|
||||
"bass_lp": 500, "reverb_type": "plate"},
|
||||
{"name": "Kalimba Dreams", "key": ("G", "major"), "drums": "cajon folk",
|
||||
"fill": "bossa nova", "bpm": 95,
|
||||
"prog": ("I", "vi", "IV", "V"),
|
||||
"lead": ("kalimba_synth", "none", 0.35, 0.2),
|
||||
"pad": ("piano_synth", "none", -0.2),
|
||||
"bass_lp": 400, "reverb_type": "taj_mahal"},
|
||||
{"name": "Outback Drone", "key": ("E", "minor"), "drums": "djembe",
|
||||
"fill": "afrobeat", "bpm": 70,
|
||||
"prog": ("i", "iv", "i", "V"),
|
||||
"lead": ("didgeridoo_synth", "pad", 0.3, 0.0),
|
||||
"pad": ("granular_synth", "pad", 0.0),
|
||||
"bass_lp": 200, "reverb_type": "cave"},
|
||||
{"name": "Highland", "key": ("A", "minor"), "drums": "flamenco",
|
||||
"fill": "rock", "bpm": 95,
|
||||
"prog": ("i", "iv", "V", "i"),
|
||||
"lead": ("bagpipe_synth", "organ", 0.15, 0.0),
|
||||
"pad": ("strings_synth", "pad", -0.2),
|
||||
"bass_lp": 400, "reverb_type": "cathedral"},
|
||||
{"name": "Nashville Tears", "key": ("G", "major"), "drums": "country",
|
||||
"fill": "rock", "bpm": 85,
|
||||
"prog": ("I", "IV", "V", "IV"),
|
||||
"lead": ("pedal_steel_synth", "strings", 0.35, 0.2),
|
||||
"pad": ("piano_synth", "none", -0.3),
|
||||
"bass_lp": 500, "reverb_type": "spring"},
|
||||
{"name": "Tabla Fusion", "key": ("E", "minor"), "drums": "teental",
|
||||
"fill": "rock", "bpm": 120,
|
||||
"prog": ("i", "iv", "V", "i"),
|
||||
"lead": ("sitar_synth", "none", 0.3, 0.2),
|
||||
"pad": ("vocal_synth", "pad", 0.0),
|
||||
"bass_lp": 400, "reverb_type": "taj_mahal"},
|
||||
]
|
||||
|
||||
mood = random.choice(moods)
|
||||
@@ -351,7 +423,10 @@ def cmd_demo(args):
|
||||
print(f" {mood['drums']} | {lead_synth} lead | {pad_synth} pad | {mood['reverb_type']} reverb")
|
||||
print()
|
||||
|
||||
play_score(score)
|
||||
try:
|
||||
play_score(score)
|
||||
except KeyboardInterrupt:
|
||||
pass
|
||||
print(" ♫")
|
||||
|
||||
|
||||
|
||||
+2366
-21
File diff suppressed because it is too large
Load Diff
+176
-7
@@ -77,6 +77,7 @@ def cmd_help(session, args):
|
||||
Parts:
|
||||
part lead saw pluck score.part("lead", synth="saw", envelope="pluck")
|
||||
part bass sine score.part("bass", synth="sine")
|
||||
part lead instrument piano score.part("lead", instrument="piano")
|
||||
part list all parts
|
||||
|
||||
Notes (on active part):
|
||||
@@ -85,6 +86,12 @@ def cmd_help(session, args):
|
||||
rest 2 part.rest(2.0)
|
||||
arp Am updown 2 2 part.arpeggio("Am", pattern="updown", bars=2, octaves=2)
|
||||
prog I V vi IV part adds key.progression(...)
|
||||
strum Am 2 down part.strum("Am", 2, direction="down")
|
||||
strum G 2 up 0.1 lazy strum (strum_time=0.1)
|
||||
roll C3 4 part.roll("C3", 4) — timpani/tremolo
|
||||
roll C3 4 30 110 roll with velocity ramp
|
||||
bend C5 1 2 part.add("C5", 1, bend=2) — bend up 2 semitones
|
||||
bend C5 1 -1 bend down a half step
|
||||
|
||||
Effects (on active part):
|
||||
reverb 0.4 reverb=0.4
|
||||
@@ -110,6 +117,12 @@ def cmd_help(session, args):
|
||||
fingering Am guitar chord fingering
|
||||
diagram [mode] [frets] scale diagram on guitar
|
||||
|
||||
Tuning:
|
||||
temperament equal set temperament (equal/pythagorean/meantone/just)
|
||||
temperament show current temperament
|
||||
reference 432 set reference pitch (default 440)
|
||||
instruments list all available instruments
|
||||
|
||||
Session:
|
||||
show score info
|
||||
status current state
|
||||
@@ -197,12 +210,22 @@ def cmd_part(session, args):
|
||||
return
|
||||
|
||||
name = args[0]
|
||||
synth = args[1] if len(args) > 1 else "saw"
|
||||
envelope = args[2] if len(args) > 2 else "pluck"
|
||||
|
||||
if name not in session.parts:
|
||||
session.parts[name] = session.score.part(name, synth=synth, envelope=envelope)
|
||||
print(f" score.part(\"{name}\", synth=\"{synth}\", envelope=\"{envelope}\")")
|
||||
# Check if second arg is "instrument" keyword or an instrument name
|
||||
if len(args) > 1 and args[1] == "instrument" and len(args) > 2:
|
||||
instrument = args[2]
|
||||
session.parts[name] = session.score.part(name, instrument=instrument)
|
||||
print(f" score.part(\"{name}\", instrument=\"{instrument}\")")
|
||||
elif len(args) > 1 and args[1] in _INSTRUMENT_NAMES:
|
||||
instrument = args[1]
|
||||
session.parts[name] = session.score.part(name, instrument=instrument)
|
||||
print(f" score.part(\"{name}\", instrument=\"{instrument}\")")
|
||||
else:
|
||||
synth = args[1] if len(args) > 1 else "saw"
|
||||
envelope = args[2] if len(args) > 2 else "pluck"
|
||||
session.parts[name] = session.score.part(name, synth=synth, envelope=envelope)
|
||||
print(f" score.part(\"{name}\", synth=\"{synth}\", envelope=\"{envelope}\")")
|
||||
else:
|
||||
print(f" → {name}")
|
||||
session.current_part = session.parts[name]
|
||||
@@ -534,6 +557,97 @@ def cmd_identify(session, args):
|
||||
print(f" error: {e}")
|
||||
|
||||
|
||||
def cmd_strum(session, args):
|
||||
"""Strum a chord on a fretboard-equipped part."""
|
||||
if not args:
|
||||
print(" usage: strum Am [beats] [down|up] [strum_time]")
|
||||
return
|
||||
part = _require_part(session)
|
||||
chord_name = args[0]
|
||||
beats = float(args[1]) if len(args) > 1 else 1.0
|
||||
direction = args[2] if len(args) > 2 else "down"
|
||||
strum_time = float(args[3]) if len(args) > 3 else 0.05
|
||||
try:
|
||||
part.strum(chord_name, beats, direction=direction, strum_time=strum_time)
|
||||
print(f" .strum(\"{chord_name}\", {beats}, direction=\"{direction}\", "
|
||||
f"strum_time={strum_time})")
|
||||
except Exception as e:
|
||||
print(f" error: {e}")
|
||||
|
||||
|
||||
def cmd_roll(session, args):
|
||||
"""Play a roll (rapid repeated notes with velocity ramp)."""
|
||||
if not args:
|
||||
print(" usage: roll C3 [beats] [vel_start] [vel_end]")
|
||||
return
|
||||
part = _require_part(session)
|
||||
tone = args[0]
|
||||
beats = float(args[1]) if len(args) > 1 else 4.0
|
||||
vel_start = int(args[2]) if len(args) > 2 else 40
|
||||
vel_end = int(args[3]) if len(args) > 3 else 100
|
||||
try:
|
||||
part.roll(tone, beats, velocity_start=vel_start, velocity_end=vel_end)
|
||||
print(f" .roll(\"{tone}\", {beats}, velocity_start={vel_start}, "
|
||||
f"velocity_end={vel_end})")
|
||||
except Exception as e:
|
||||
print(f" error: {e}")
|
||||
|
||||
|
||||
def cmd_bend(session, args):
|
||||
"""Add a note with pitch bend."""
|
||||
if len(args) < 3:
|
||||
print(" usage: bend C5 1 2 (note, beats, semitones)")
|
||||
print(" bend C5 1 -1 (bend down)")
|
||||
return
|
||||
part = _require_part(session)
|
||||
note = args[0]
|
||||
beats = float(args[1])
|
||||
bend = float(args[2])
|
||||
bend_type = args[3] if len(args) > 3 else "smooth"
|
||||
try:
|
||||
part.add(note, beats, bend=bend, bend_type=bend_type)
|
||||
print(f" .add(\"{note}\", {beats}, bend={bend}, bend_type=\"{bend_type}\")")
|
||||
except Exception as e:
|
||||
print(f" error: {e}")
|
||||
|
||||
|
||||
def cmd_temperament(session, args):
|
||||
"""Set or show the tuning temperament."""
|
||||
if not args:
|
||||
temp = getattr(session.score, 'temperament', 'equal')
|
||||
ref = getattr(session.score, 'reference_pitch', 440.0)
|
||||
print(f" temperament={temp} reference={ref} Hz")
|
||||
print(f" available: equal, pythagorean, meantone, just")
|
||||
return
|
||||
temp = args[0]
|
||||
valid = ["equal", "pythagorean", "meantone", "just"]
|
||||
if temp not in valid:
|
||||
print(f" unknown temperament: {temp}")
|
||||
print(f" available: {', '.join(valid)}")
|
||||
return
|
||||
session.score.temperament = temp
|
||||
print(f" temperament={temp}")
|
||||
|
||||
|
||||
def cmd_reference(session, args):
|
||||
"""Set the reference pitch (A4 frequency)."""
|
||||
if not args:
|
||||
ref = getattr(session.score, 'reference_pitch', 440.0)
|
||||
print(f" reference={ref} Hz")
|
||||
return
|
||||
ref = float(args[0])
|
||||
session.score.reference_pitch = ref
|
||||
print(f" reference={ref} Hz")
|
||||
|
||||
|
||||
def cmd_instruments(session, args):
|
||||
"""List all available instruments."""
|
||||
cols = 3
|
||||
for i in range(0, len(_INSTRUMENT_NAMES), cols):
|
||||
row = _INSTRUMENT_NAMES[i:i + cols]
|
||||
print(" " + " ".join(f"{name:<22s}" for name in row))
|
||||
|
||||
|
||||
def cmd_circle(session, args):
|
||||
"""Show circle of fifths."""
|
||||
tonic = args[0] if args else session.key.tonic_name
|
||||
@@ -560,7 +674,10 @@ def cmd_clear(session, args):
|
||||
def cmd_status(session, args):
|
||||
parts = ", ".join(session.parts.keys()) if session.parts else "none"
|
||||
active = session.current_part.name if session.current_part else "none"
|
||||
temp = getattr(session.score, 'temperament', 'equal')
|
||||
ref = getattr(session.score, 'reference_pitch', 440.0)
|
||||
print(f" key={session.key} bpm={session.bpm} swing={session.swing}")
|
||||
print(f" temperament={temp} reference={ref} Hz")
|
||||
print(f" drums={session._drum_preset or 'none'} parts=[{parts}] active={active}")
|
||||
|
||||
|
||||
@@ -607,6 +724,12 @@ COMMANDS = {
|
||||
"interval": cmd_interval,
|
||||
"identify": cmd_identify, "id": cmd_identify,
|
||||
"circle": cmd_circle,
|
||||
"strum": cmd_strum,
|
||||
"roll": cmd_roll,
|
||||
"bend": cmd_bend,
|
||||
"temperament": cmd_temperament, "temp": cmd_temperament,
|
||||
"reference": cmd_reference, "ref": cmd_reference,
|
||||
"instruments": cmd_instruments,
|
||||
"clear": cmd_clear,
|
||||
"status": cmd_status,
|
||||
}
|
||||
@@ -653,9 +776,43 @@ def _prompt(session):
|
||||
# ── Tab completion ─────────────────────────────────────────────────────────
|
||||
|
||||
_SYNTH_NAMES = ["sine", "saw", "triangle", "square", "pulse", "fm",
|
||||
"noise", "supersaw", "pwm_slow", "pwm_fast"]
|
||||
"noise", "supersaw", "pwm_slow", "pwm_fast",
|
||||
"pedal_steel_synth", "theremin_synth", "kalimba_synth",
|
||||
"steel_drum_synth", "accordion_synth", "didgeridoo_synth",
|
||||
"bagpipe_synth", "banjo_synth", "mandolin_synth",
|
||||
"ukulele_synth", "vocal_synth", "granular_synth",
|
||||
"piano_synth", "organ_synth", "harpsichord_synth",
|
||||
"strings_synth", "cello_synth", "flute_synth",
|
||||
"clarinet_synth", "oboe_synth", "trumpet_synth",
|
||||
"acoustic_guitar_synth", "electric_guitar_synth",
|
||||
"bass_guitar_synth", "upright_bass_synth", "harp_synth",
|
||||
"sitar_synth", "pluck_synth", "saxophone_synth",
|
||||
"marimba_synth", "timpani_synth"]
|
||||
_INSTRUMENT_NAMES = [
|
||||
# Keys
|
||||
"piano", "electric_piano", "organ", "harpsichord", "celesta", "music_box",
|
||||
# Strings
|
||||
"violin", "viola", "cello", "contrabass", "string_ensemble",
|
||||
# Woodwinds
|
||||
"flute", "clarinet", "oboe", "bassoon",
|
||||
# Brass
|
||||
"trumpet", "trombone", "french_horn", "tuba", "brass_ensemble",
|
||||
# Plucked
|
||||
"acoustic_guitar", "electric_guitar", "clean_guitar", "crunch_guitar",
|
||||
"distorted_guitar", "orange_crunch", "metal_guitar", "bass_guitar",
|
||||
"upright_bass", "harp", "sitar", "pedal_steel", "theremin", "kalimba",
|
||||
"steel_drum", "accordion", "didgeridoo", "bagpipe", "banjo", "mandolin",
|
||||
"mandola", "ukulele", "koto",
|
||||
# Synth presets
|
||||
"synth_lead", "synth_pad", "synth_bass", "acid_bass",
|
||||
"granular_pad", "vocal", "choir", "granular_texture", "808_bass",
|
||||
# Percussion / Mallet
|
||||
"vibraphone", "marimba", "xylophone", "glockenspiel", "tubular_bells", "timpani",
|
||||
# Woodwinds (continued)
|
||||
"saxophone", "alto_sax", "tenor_sax", "bari_sax",
|
||||
]
|
||||
_ENVELOPE_NAMES = ["piano", "pluck", "pad", "organ", "bell", "strings",
|
||||
"staccato", "none"]
|
||||
"staccato", "bowed", "mallet", "none"]
|
||||
_ARP_PATTERNS = ["up", "down", "updown", "downup", "random"]
|
||||
_LFO_SHAPES = ["sine", "triangle", "saw", "square"]
|
||||
_SYSTEMS = ["western", "indian", "arabic", "japanese", "blues", "gamelan"]
|
||||
@@ -667,7 +824,7 @@ _CHORD_SUFFIXES = ["", "m", "7", "m7", "maj7", "dim", "aug", "sus2", "sus4",
|
||||
# Context-aware completions for the second word
|
||||
_ARG_COMPLETIONS = {
|
||||
"drums": lambda: Pattern.list_presets(),
|
||||
"part": lambda: _SYNTH_NAMES,
|
||||
"part": lambda: _SYNTH_NAMES + _INSTRUMENT_NAMES,
|
||||
"key": lambda: [f"{n}m" for n in _NOTE_NAMES[:12]] + _NOTE_NAMES[:12],
|
||||
"arp": lambda: [f"{n}{s}" for n in _NOTE_NAMES[:7] for s in _CHORD_SUFFIXES[:6]],
|
||||
"add": lambda: [f"{n}{o}" for n in _NOTE_NAMES[:12] for o in ["3", "4", "5"]],
|
||||
@@ -679,6 +836,12 @@ _ARG_COMPLETIONS = {
|
||||
"lowpass_q", "reverb_decay", "delay_time", "delay_feedback",
|
||||
"distortion_drive"],
|
||||
"identify": lambda: [f"{n}{s}" for n in _NOTE_NAMES[:7] for s in _CHORD_SUFFIXES[:6]],
|
||||
"strum": lambda: [f"{n}{s}" for n in _NOTE_NAMES[:7] for s in _CHORD_SUFFIXES[:6]],
|
||||
"roll": lambda: [f"{n}{o}" for n in _NOTE_NAMES[:12] for o in ["2", "3", "4", "5"]],
|
||||
"bend": lambda: [f"{n}{o}" for n in _NOTE_NAMES[:12] for o in ["3", "4", "5"]],
|
||||
"temperament": lambda: ["equal", "pythagorean", "meantone", "just"],
|
||||
"reference": lambda: ["440", "432", "415", "444"],
|
||||
"instruments": lambda: _INSTRUMENT_NAMES,
|
||||
}
|
||||
|
||||
|
||||
@@ -705,6 +868,12 @@ def _completer(text, state):
|
||||
elif cmd == "arp" and len(tokens) == 3:
|
||||
# Pattern for arp
|
||||
options = [p for p in _ARP_PATTERNS if p.startswith(text)]
|
||||
elif cmd == "strum" and len(tokens) == 4:
|
||||
# Direction for strum
|
||||
options = [d for d in ["down", "up"] if d.startswith(text)]
|
||||
elif cmd == "bend" and len(tokens) == 5:
|
||||
# Bend type
|
||||
options = [t for t in ["smooth", "linear", "late"] if t.startswith(text)]
|
||||
elif cmd == "lfo" and len(tokens) >= 7:
|
||||
# Shape for lfo
|
||||
options = [s for s in _LFO_SHAPES if s.startswith(text)]
|
||||
|
||||
+1332
-46
File diff suppressed because it is too large
Load Diff
+21
-5
@@ -2,8 +2,8 @@ from ._statics import (
|
||||
TEMPERAMENTS, TONES, DEGREES, SCALES,
|
||||
INDIAN_SCALES, ARABIC_SCALES, JAPANESE_SCALES,
|
||||
BLUES_SCALES, GAMELAN_SCALES, SYSTEMS,
|
||||
TONES_SHRUTI, DEGREES_SHRUTI, SHRUTI_SCALES,
|
||||
TONES_ARABIC_24, DEGREES_ARABIC_24, ARABIC_24_SCALES,
|
||||
TONES_SHRUTI, DEGREES_SHRUTI, SHRUTI_SCALES, SHRUTI_RATIOS,
|
||||
TONES_ARABIC_24, DEGREES_ARABIC_24, ARABIC_24_SCALES, MAQAM_RATIOS,
|
||||
TONES_SLENDRO, DEGREES_SLENDRO, SLENDRO_SCALES,
|
||||
TONES_PELOG, DEGREES_PELOG, PELOG_SCALES,
|
||||
TONES_THAI, DEGREES_THAI, THAI_SCALES,
|
||||
@@ -14,7 +14,7 @@ from ._statics import (
|
||||
|
||||
class System:
|
||||
def __init__(self, *, tone_names, degrees, scales=None, c_index=None,
|
||||
period=2.0):
|
||||
period=2.0, ratios=None):
|
||||
self.tone_names = tone_names
|
||||
|
||||
self.degrees = degrees
|
||||
@@ -25,6 +25,11 @@ class System:
|
||||
# 3.0 for Bohlen-Pierce (tritave).
|
||||
self.period = period
|
||||
|
||||
# Custom frequency ratios: if set, overrides equal temperament.
|
||||
# A list of N floats (one per tone), each relative to the first
|
||||
# tone (1.0). For example, just intonation shruti ratios.
|
||||
self.ratios = ratios
|
||||
|
||||
# c_index: the index of the "reference C" in the tone list.
|
||||
# For octave arithmetic — scientific pitch changes octave at C.
|
||||
# Default 3 for 12-TET western (A=0, A#=1, B=2, C=3).
|
||||
@@ -214,6 +219,17 @@ class System:
|
||||
# descending goes in meta?
|
||||
return {"intervals": scale, "hemitonic": hemitonic, "meta": {}}
|
||||
|
||||
def tone(self, name, octave=4):
|
||||
"""Create a Tone in this system. Shorthand for ``Tone(name, octave=octave, system=self)``.
|
||||
|
||||
Example::
|
||||
|
||||
>>> edo19 = TET(19)
|
||||
>>> edo19.tone(5, octave=4).frequency
|
||||
"""
|
||||
from . import Tone
|
||||
return Tone(name, octave=octave, system=self)
|
||||
|
||||
def __repr__(self):
|
||||
return f"<System semitones={self.semitones!r}>"
|
||||
|
||||
@@ -352,9 +368,9 @@ SYSTEMS = {
|
||||
"31-tet": TET(31, names=_31TET_NAMES),
|
||||
# Microtonal systems with proper intervals (not 12-TET approximations)
|
||||
"shruti": System(tone_names=TONES_SHRUTI, degrees=DEGREES_SHRUTI,
|
||||
scales=SHRUTI_SCALES, c_index=5),
|
||||
scales=SHRUTI_SCALES, c_index=5, ratios=SHRUTI_RATIOS),
|
||||
"maqam": System(tone_names=TONES_ARABIC_24, degrees=DEGREES_ARABIC_24,
|
||||
scales=ARABIC_24_SCALES, c_index=5),
|
||||
scales=ARABIC_24_SCALES, c_index=5, ratios=MAQAM_RATIOS),
|
||||
"slendro": System(tone_names=TONES_SLENDRO, degrees=DEGREES_SLENDRO,
|
||||
scales=SLENDRO_SCALES, c_index=1),
|
||||
"pelog": System(tone_names=TONES_PELOG, degrees=DEGREES_PELOG,
|
||||
|
||||
+61
-11
@@ -26,7 +26,7 @@ class Tone:
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
name: str,
|
||||
name,
|
||||
*,
|
||||
alt_names: Optional[list[str]] = None,
|
||||
octave: Optional[int] = None,
|
||||
@@ -36,8 +36,10 @@ class Tone:
|
||||
"""Initialize a Tone with a name, optional octave, and musical system.
|
||||
|
||||
Args:
|
||||
name: The note name (e.g. ``"C"``, ``"C#4"``). If the name
|
||||
contains a digit, it is parsed as the octave.
|
||||
name: The note name as a string (``"C"``, ``"C#4"``) or an int
|
||||
for numbered systems (``0``, ``11``). Ints are converted to
|
||||
strings and wrapped to the system's range (e.g. 22 in a
|
||||
22-tone system becomes 0 at octave+1).
|
||||
alt_names: Alternate spellings for this tone (e.g. enharmonics).
|
||||
octave: The octave number. Overrides any octave parsed from *name*.
|
||||
system: The tuning system, either as a string key (``"western"``)
|
||||
@@ -46,6 +48,23 @@ class Tone:
|
||||
if alt_names is None:
|
||||
alt_names = []
|
||||
|
||||
# Int tone names: wrap to system range, adjust octave
|
||||
if isinstance(name, int):
|
||||
if isinstance(system, str):
|
||||
from .systems import SYSTEMS
|
||||
_sys = SYSTEMS[system]
|
||||
else:
|
||||
_sys = system
|
||||
n_tones = len(_sys.tone_names)
|
||||
if name < 0 or name >= n_tones:
|
||||
extra_octaves = name // n_tones
|
||||
name = name % n_tones
|
||||
if octave is None:
|
||||
octave = 4 + extra_octaves
|
||||
else:
|
||||
octave += extra_octaves
|
||||
name = str(name)
|
||||
|
||||
if isinstance(name, str):
|
||||
# Normalize unicode music symbols to ASCII equivalents
|
||||
name = (name
|
||||
@@ -70,6 +89,35 @@ class Tone:
|
||||
if octave is None:
|
||||
octave = parsed_octave
|
||||
|
||||
# Octave boundary fix: B#→C should increment octave,
|
||||
# Cb→B should decrement octave (scientific pitch changes at C).
|
||||
# Only applies to Western-style systems with letter names.
|
||||
if octave is not None and name and name[0].isalpha():
|
||||
if isinstance(system, str):
|
||||
from .systems import SYSTEMS
|
||||
_sys_check = SYSTEMS.get(system)
|
||||
else:
|
||||
_sys_check = system
|
||||
if _sys_check is not None:
|
||||
resolved = _sys_check.resolve_name(name)
|
||||
if resolved is not None and resolved != name:
|
||||
orig_letter = name[0].upper()
|
||||
res_letter = resolved[0].upper()
|
||||
# Sharp crossing B→C: B# resolves to C, octave up
|
||||
if orig_letter == 'B' and res_letter == 'C' and '#' in name:
|
||||
octave += 1
|
||||
# Double sharp: A## resolves to B — no boundary cross
|
||||
# But B## resolves to C# — boundary cross
|
||||
if orig_letter == 'B' and res_letter not in ('B', 'A') and '##' in name:
|
||||
octave += 1
|
||||
# Flat crossing C→B: Cb resolves to B, octave down
|
||||
if orig_letter == 'C' and res_letter == 'B' and 'b' in name and name != 'C':
|
||||
octave -= 1
|
||||
# Double flat: D♭♭ resolves to C — no boundary cross
|
||||
# But C♭♭ resolves to Bb — boundary cross
|
||||
if orig_letter == 'C' and res_letter not in ('C', 'D') and 'bb' in name:
|
||||
octave -= 1
|
||||
|
||||
self.name = name
|
||||
self.octave = octave
|
||||
self.alt_names = alt_names
|
||||
@@ -762,11 +810,13 @@ class Tone:
|
||||
period = getattr(self.system, 'period', 2.0)
|
||||
c_idx = getattr(self.system, 'c_index', C_INDEX)
|
||||
|
||||
if period != 2.0 and temperament == "equal":
|
||||
# Non-octave period (e.g. Bohlen-Pierce tritave=3.0):
|
||||
# generate ratios as period^(n/tones) instead of 2^(n/tones)
|
||||
import sympy
|
||||
pitch_scale = [period ** sympy.Rational(i, tones) for i in range(tones + 1)]
|
||||
# Custom ratios override temperament (e.g. shruti just ratios)
|
||||
custom_ratios = getattr(self.system, 'ratios', None)
|
||||
if custom_ratios is not None:
|
||||
pitch_scale = list(custom_ratios) + [period]
|
||||
elif period != 2.0 and temperament == "equal":
|
||||
# Non-octave period (e.g. Bohlen-Pierce tritave=3.0)
|
||||
pitch_scale = [period ** (i / tones) for i in range(tones + 1)]
|
||||
else:
|
||||
pitch_scale = TEMPERAMENTS[temperament](tones)
|
||||
octave = self.octave if self.octave is not None else 4
|
||||
@@ -783,7 +833,7 @@ class Tone:
|
||||
if symbolic:
|
||||
return reference_pitch * ratio
|
||||
else:
|
||||
result = reference_pitch * ratio
|
||||
result = float(reference_pitch * ratio)
|
||||
if precision:
|
||||
return float(result.evalf(precision))
|
||||
return float(result)
|
||||
return round(result, precision)
|
||||
return result
|
||||
|
||||
+457
-10
@@ -4869,6 +4869,19 @@ def test_duration_values():
|
||||
assert abs(Duration.TRIPLET_QUARTER.value - 2 / 3) < 1e-9
|
||||
|
||||
|
||||
def test_duration_arithmetic():
|
||||
# Multiplication
|
||||
assert Duration.WHOLE * 2 == 8.0
|
||||
assert 2 * Duration.HALF == 4.0
|
||||
assert Duration.QUARTER * 3 == 3.0
|
||||
# Division
|
||||
assert Duration.WHOLE / 2 == 2.0
|
||||
# Addition
|
||||
assert Duration.HALF + Duration.QUARTER == 3.0
|
||||
assert Duration.HALF + 1.0 == 3.0
|
||||
assert 1.0 + Duration.HALF == 3.0
|
||||
|
||||
|
||||
def test_time_signature_from_string_4_4():
|
||||
ts = TimeSignature.from_string("4/4")
|
||||
assert ts.beats == 4
|
||||
@@ -5320,7 +5333,7 @@ def test_supersaw_wave():
|
||||
@needs_portaudio
|
||||
def test_all_synths_in_enum():
|
||||
from pytheory.play import Synth
|
||||
assert len(Synth) == 13
|
||||
assert len(Synth) == 42
|
||||
for s in Synth:
|
||||
wave = s(440, n_samples=1000)
|
||||
assert len(wave) == 1000
|
||||
@@ -6467,11 +6480,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 +6498,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 +6724,443 @@ 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 # grace notes + chord per strum
|
||||
|
||||
|
||||
# ── 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
|
||||
|
||||
|
||||
# ── New instrument synths (v0.36+) ──────────────────────────────────────────
|
||||
|
||||
def test_new_synths_render():
|
||||
"""All 7 new synths produce valid audio."""
|
||||
from pytheory.play import (pedal_steel_wave, theremin_wave, kalimba_wave,
|
||||
steel_drum_wave, accordion_wave,
|
||||
didgeridoo_wave, bagpipe_wave,
|
||||
banjo_wave, mandolin_wave, ukulele_wave,
|
||||
vocal_wave, SAMPLE_RATE)
|
||||
synths = [pedal_steel_wave, theremin_wave, kalimba_wave, steel_drum_wave,
|
||||
accordion_wave, didgeridoo_wave, bagpipe_wave,
|
||||
banjo_wave, mandolin_wave, ukulele_wave, vocal_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_vocal_synth_with_lyric():
|
||||
"""Vocal synth accepts lyric parameter."""
|
||||
from pytheory.play import vocal_wave
|
||||
for lyric in ["ah", "ee", "oh", "oo", "hi", "la"]:
|
||||
wave = vocal_wave(330, n_samples=11025, lyric=lyric)
|
||||
assert len(wave) == 11025
|
||||
assert numpy.abs(wave).max() > 0
|
||||
|
||||
|
||||
def test_vocal_different_vowels_differ():
|
||||
"""Different vowels should produce different waveforms."""
|
||||
from pytheory.play import vocal_wave
|
||||
ah = vocal_wave(330, n_samples=22050, lyric="ah")
|
||||
ee = vocal_wave(330, n_samples=22050, lyric="ee")
|
||||
# They should differ (different formant peaks)
|
||||
assert not numpy.array_equal(ah, ee)
|
||||
|
||||
|
||||
def test_all_instrument_presets_create():
|
||||
"""Every instrument preset in INSTRUMENTS should create a valid Part."""
|
||||
from pytheory import Score
|
||||
from pytheory.rhythm import INSTRUMENTS
|
||||
for name in INSTRUMENTS:
|
||||
score = Score("4/4", bpm=120)
|
||||
p = score.part("test", instrument=name)
|
||||
assert p.synth is not None
|
||||
|
||||
|
||||
def test_new_instrument_presets():
|
||||
"""New instrument presets have correct synths."""
|
||||
from pytheory import Score
|
||||
presets = {
|
||||
"pedal_steel": "pedal_steel_synth",
|
||||
"theremin": "theremin_synth",
|
||||
"kalimba": "kalimba_synth",
|
||||
"steel_drum": "steel_drum_synth",
|
||||
"accordion": "accordion_synth",
|
||||
"didgeridoo": "didgeridoo_synth",
|
||||
"bagpipe": "bagpipe_synth",
|
||||
"banjo": "banjo_synth",
|
||||
"mandolin": "mandolin_synth",
|
||||
"ukulele": "ukulele_synth",
|
||||
}
|
||||
for name, expected_synth in presets.items():
|
||||
score = Score("4/4", bpm=120)
|
||||
p = score.part("t", instrument=name)
|
||||
assert p.synth == expected_synth, f"{name} has {p.synth}, expected {expected_synth}"
|
||||
|
||||
|
||||
# ── Cajón drums ─────────────────────────────────────────────────────────────
|
||||
|
||||
def test_cajon_sounds_render():
|
||||
from pytheory.play import _render_drum_hit
|
||||
from pytheory.rhythm import DrumSound
|
||||
for sound in [DrumSound.CAJON_BASS, DrumSound.CAJON_SLAP, DrumSound.CAJON_TAP]:
|
||||
wave = _render_drum_hit(sound.value, 22050)
|
||||
assert len(wave) == 22050
|
||||
assert wave.dtype == numpy.float32
|
||||
|
||||
|
||||
def test_cajon_patterns():
|
||||
from pytheory.rhythm import Pattern
|
||||
for name in ["cajon", "cajon rumba", "cajon folk"]:
|
||||
p = Pattern.preset(name)
|
||||
assert p.beats > 0
|
||||
|
||||
|
||||
# ── Pitch bends ─────────────────────────────────────────────────────────────
|
||||
|
||||
def test_pitch_bend_renders():
|
||||
"""Pitch bend should produce valid audio without errors."""
|
||||
from pytheory import Score, Duration
|
||||
from pytheory.play import render_score
|
||||
score = Score("4/4", bpm=120)
|
||||
p = score.part("t", instrument="electric_guitar")
|
||||
p.add("A4", Duration.HALF, bend=2, bend_type="smooth")
|
||||
p.add("A4", Duration.HALF, bend=-1, bend_type="late")
|
||||
p.add("A4", Duration.HALF, bend=3, bend_type="linear")
|
||||
p.add("A4", Duration.HALF)
|
||||
buf = render_score(score)
|
||||
assert len(buf) > 0
|
||||
|
||||
|
||||
def test_pitch_bend_types():
|
||||
"""All three bend types should work."""
|
||||
from pytheory.rhythm import Note, Duration
|
||||
for bt in ["smooth", "linear", "late"]:
|
||||
n = Note(tone=None, duration=Duration.QUARTER, bend=2, bend_type=bt)
|
||||
assert n.bend_type == bt
|
||||
|
||||
|
||||
# ── Roll method ─────────────────────────────────────────────────────────────
|
||||
|
||||
def test_roll_adds_notes():
|
||||
from pytheory import Score, Duration
|
||||
score = Score("4/4", bpm=120)
|
||||
p = score.part("t", instrument="timpani")
|
||||
p.roll("C3", Duration.WHOLE, velocity_start=30, velocity_end=100)
|
||||
assert len(p.notes) > 4 # should be many 16th notes
|
||||
|
||||
|
||||
def test_roll_velocity_ramp():
|
||||
from pytheory import Score, Duration
|
||||
score = Score("4/4", bpm=120)
|
||||
p = score.part("t", instrument="timpani")
|
||||
p.roll("C3", Duration.WHOLE, velocity_start=20, velocity_end=100)
|
||||
velocities = [n.velocity for n in p.notes]
|
||||
# First should be quieter than last
|
||||
assert velocities[0] < velocities[-1]
|
||||
|
||||
|
||||
def test_roll_custom_speed():
|
||||
from pytheory import Score, Duration
|
||||
score = Score("4/4", bpm=120)
|
||||
p = score.part("t", synth="sine")
|
||||
p.roll("A4", Duration.WHOLE, speed=0.125) # 32nd notes
|
||||
# 4 beats / 0.125 = 32 notes
|
||||
assert len(p.notes) == 32
|
||||
|
||||
|
||||
# ── Int tone names ──────────────────────────────────────────────────────────
|
||||
|
||||
def test_int_tone_name():
|
||||
from pytheory import Tone, TET
|
||||
edo = TET(22)
|
||||
t = Tone(0, octave=4, system=edo)
|
||||
assert t.name == "0"
|
||||
assert t.frequency == pytest.approx(440.0, rel=1e-3)
|
||||
|
||||
|
||||
def test_int_tone_wrapping():
|
||||
from pytheory import Tone, TET
|
||||
edo = TET(22)
|
||||
t = Tone(22, octave=4, system=edo)
|
||||
assert t.name == "0"
|
||||
assert t.octave == 5
|
||||
assert t.frequency == pytest.approx(880.0, rel=1e-3)
|
||||
|
||||
|
||||
def test_int_tone_negative():
|
||||
from pytheory import Tone, TET
|
||||
edo = TET(22)
|
||||
t = Tone(-1, octave=4, system=edo)
|
||||
assert t.name == "21"
|
||||
assert t.octave == 3
|
||||
|
||||
|
||||
def test_system_tone_method():
|
||||
from pytheory import TET
|
||||
edo = TET(19)
|
||||
t = edo.tone(5, octave=4)
|
||||
assert t.name == "5"
|
||||
assert t.octave == 4
|
||||
|
||||
|
||||
# ── B#/Cb octave boundary ──────────────────────────────────────────────────
|
||||
|
||||
def test_b_sharp_octave():
|
||||
t = Tone("B#4")
|
||||
assert t.octave == 5
|
||||
assert t.frequency == pytest.approx(Tone("C5").frequency, rel=1e-3)
|
||||
|
||||
|
||||
def test_c_flat_octave():
|
||||
t = Tone("Cb4")
|
||||
assert t.octave == 3
|
||||
assert t.frequency == pytest.approx(Tone("B3").frequency, rel=1e-3)
|
||||
|
||||
|
||||
# ── Note choking ────────────────────────────────────────────────────────────
|
||||
|
||||
def test_note_choking_renders():
|
||||
"""Fast repeated notes should render without errors (choking active)."""
|
||||
from pytheory import Score, Duration
|
||||
from pytheory.play import render_score
|
||||
score = Score("4/4", bpm=200)
|
||||
p = score.part("t", instrument="piano")
|
||||
for _ in range(32):
|
||||
p.add("C4", Duration.SIXTEENTH)
|
||||
buf = render_score(score)
|
||||
assert len(buf) > 0
|
||||
|
||||
|
||||
# ── Score system/temperament ───────────────────────────────────────────────
|
||||
|
||||
def test_score_temperament():
|
||||
from pytheory import Score
|
||||
score = Score("4/4", bpm=120, temperament="just")
|
||||
assert score.temperament == "just"
|
||||
|
||||
|
||||
def test_score_reference_pitch():
|
||||
from pytheory import Score
|
||||
score = Score("4/4", bpm=120, reference_pitch=415.0)
|
||||
assert score.reference_pitch == 415.0
|
||||
|
||||
|
||||
def test_score_system_propagates():
|
||||
from pytheory import Score, SYSTEMS
|
||||
shruti = SYSTEMS["shruti"]
|
||||
score = Score("4/4", bpm=120, system=shruti)
|
||||
p = score.part("t", synth="sine")
|
||||
assert p._system is shruti
|
||||
|
||||
|
||||
# ── Synth enum count ────────────────────────────────────────────────────────
|
||||
|
||||
def test_synth_enum_count():
|
||||
from pytheory.play import Synth
|
||||
assert len(Synth) == 42
|
||||
|
||||
|
||||
def test_all_synths_render_and_enum_match():
|
||||
"""Every Synth enum member should render valid audio."""
|
||||
from pytheory.play import Synth
|
||||
for s in Synth:
|
||||
wave = s(440, n_samples=1000)
|
||||
assert len(wave) == 1000
|
||||
|
||||
@@ -444,15 +444,6 @@ wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/b3/38/89ba8ad64ae25be8de66a6d463314cf1eb366222074cfda9ee839c56a4b4/mdurl-0.1.2-py3-none-any.whl", hash = "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8", size = 9979, upload-time = "2022-08-14T12:40:09.779Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "mpmath"
|
||||
version = "1.3.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/e0/47/dd32fa426cc72114383ac549964eecb20ecfd886d1e5ccf5340b55b02f57/mpmath-1.3.0.tar.gz", hash = "sha256:7a28eb2a9774d00c7bc92411c19a89209d5da7c4c9a9e227be8330a23a25b91f", size = 508106, upload-time = "2023-03-07T16:47:11.061Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/43/e3/7d92a15f894aa0c9c4b49b8ee9ac9850d6e63b03c9c32c0367a13ae62209/mpmath-1.3.0-py3-none-any.whl", hash = "sha256:a0b2b9fe80bbcd81a6647ff13108738cfb482d481d826cc0e02f5b35e5c88d2c", size = 536198, upload-time = "2023-03-07T16:47:09.197Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "myst-parser"
|
||||
version = "4.0.1"
|
||||
@@ -707,11 +698,10 @@ wheels = [
|
||||
|
||||
[[package]]
|
||||
name = "pytheory"
|
||||
version = "0.33.0"
|
||||
version = "0.38.1"
|
||||
source = { editable = "." }
|
||||
dependencies = [
|
||||
{ name = "numeral" },
|
||||
{ name = "pytuning" },
|
||||
{ name = "scipy", version = "1.15.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" },
|
||||
{ name = "scipy", version = "1.17.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" },
|
||||
{ name = "sounddevice" },
|
||||
@@ -732,7 +722,6 @@ docs = [
|
||||
[package.metadata]
|
||||
requires-dist = [
|
||||
{ name = "numeral" },
|
||||
{ name = "pytuning" },
|
||||
{ name = "scipy" },
|
||||
{ name = "sounddevice" },
|
||||
]
|
||||
@@ -744,19 +733,6 @@ docs = [
|
||||
{ name = "sphinx" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pytuning"
|
||||
version = "0.7.3"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" },
|
||||
{ name = "numpy", version = "2.4.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" },
|
||||
{ name = "sympy" },
|
||||
]
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/26/59/e2c2fc91688f788587fb387ef6120c9a1ad3a8b88771fba9fc6a9c9a969d/PyTuning-0.7.3-py3-none-any.whl", hash = "sha256:db0b1231c012c1cf6a3c73aa7d791b4cff79a72f2ec6535f159c873fe302214b", size = 108174, upload-time = "2023-09-02T21:11:00.657Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pyyaml"
|
||||
version = "6.0.3"
|
||||
@@ -1151,18 +1127,6 @@ wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/52/a7/d2782e4e3f77c8450f727ba74a8f12756d5ba823d81b941f1b04da9d033a/sphinxcontrib_serializinghtml-2.0.0-py3-none-any.whl", hash = "sha256:6e2cb0eef194e10c27ec0023bfeb25badbbb5868244cf5bc5bdc04e4464bf331", size = 92072, upload-time = "2024-07-29T01:10:08.203Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "sympy"
|
||||
version = "1.14.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "mpmath" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/83/d3/803453b36afefb7c2bb238361cd4ae6125a569b4db67cd9e79846ba2d68c/sympy-1.14.0.tar.gz", hash = "sha256:d3d3fe8df1e5a0b42f0e7bdf50541697dbe7d23746e894990c030e2b05e72517", size = 7793921, upload-time = "2025-04-27T18:05:01.611Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/a2/09/77d55d46fd61b4a135c444fc97158ef34a095e5681d0a6c10b75bf356191/sympy-1.14.0-py3-none-any.whl", hash = "sha256:e091cc3e99d2141a0ba2847328f5479b05d94a6635cb96148ccb3f34671bd8f5", size = 6299353, upload-time = "2025-04-27T18:04:59.103Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tomli"
|
||||
version = "2.4.0"
|
||||
|
||||
Reference in New Issue
Block a user