mirror of
https://github.com/kennethreitz/pytheory.git
synced 2026-06-05 23:00:20 +00:00
Compare commits
29 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 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 |
@@ -2,6 +2,30 @@
|
||||
|
||||
All notable changes to PyTheory are documented here.
|
||||
|
||||
## 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
|
||||
@@ -20,6 +44,8 @@ All notable changes to PyTheory are documented here.
|
||||
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
|
||||
|
||||
+135
-6
@@ -10,7 +10,7 @@ the genre -- they tell the listener's body how to move before a single
|
||||
melodic note is played.
|
||||
|
||||
PyTheory includes a complete drum system -- 27 synthesized percussion
|
||||
sounds, 58 pattern presets across dozens of genres, and 21 fill presets.
|
||||
sounds, 80+ pattern presets across dozens of genres, and 21 fill presets.
|
||||
Every sound is generated from waveforms; no samples needed.
|
||||
|
||||
Drum Sounds
|
||||
@@ -145,8 +145,8 @@ Each sound has a dedicated synthesizer:
|
||||
Pattern Presets
|
||||
---------------
|
||||
|
||||
58 patterns spanning genres from rock to Afro-Cuban to electronic.
|
||||
Load them with ``Pattern.preset()``:
|
||||
80+ patterns spanning genres from rock to Afro-Cuban to electronic to
|
||||
world percussion. Load them with ``Pattern.preset()``:
|
||||
|
||||
.. code-block:: pycon
|
||||
|
||||
@@ -193,9 +193,16 @@ adds syncopation.
|
||||
rattling hi-hats of trap, the breakneck tempo of drum and bass. These
|
||||
patterns were born in drum machines and they still live there.
|
||||
|
||||
**Metal/Punk:** metal, blast beat, punk -- Speed and aggression.
|
||||
The blast beat is both feet and both hands going as fast as humanly
|
||||
possible. Punk strips everything to its essentials.
|
||||
**Metal/Punk:** metal, blast beat, punk, double kick, metal blast,
|
||||
metal groove, metal gallop -- Speed and aggression. The blast beat is
|
||||
both feet and both hands going as fast as humanly possible. Punk strips
|
||||
everything to its essentials. The metal kit adds 3 dedicated sounds
|
||||
(double kick, china cymbal, stack) and 4 patterns for extreme metal
|
||||
subgenres.
|
||||
|
||||
**World Percussion:** tabla, dhol, dholak, mridangam, djembe -- Deep
|
||||
traditions from across the globe, each with authentic sound sets and
|
||||
idiomatic patterns. See the World Percussion section below for details.
|
||||
|
||||
**Other:** funk, hip hop, bo diddley, second line, new orleans, waltz,
|
||||
12/8 blues, country, gospel, flamenco -- Everything else. The syncopated
|
||||
@@ -304,6 +311,128 @@ drum pattern and all named parts are mixed together by ``play_score()``:
|
||||
|
||||
play_score(score)
|
||||
|
||||
World Percussion
|
||||
----------------
|
||||
|
||||
PyTheory includes dedicated sound sets and pattern presets for
|
||||
traditional percussion instruments from around the world. Each
|
||||
instrument has its own synthesized sounds that capture the timbral
|
||||
character of the real instrument, plus idiomatic rhythmic patterns
|
||||
drawn from their musical traditions.
|
||||
|
||||
Tabla
|
||||
~~~~~
|
||||
|
||||
The tabla is a pair of hand drums from the Indian subcontinent -- the
|
||||
smaller, higher-pitched *dayan* and the larger, bass *bayan*. It is
|
||||
the rhythmic backbone of Hindustani classical music, and one of the
|
||||
most expressive percussion instruments ever created. A single tabla
|
||||
player can produce an astonishing range of tones by varying finger
|
||||
placement, pressure, and striking technique.
|
||||
|
||||
**6 sounds** -- covering the primary tabla strokes (na, tin, tun, ge,
|
||||
ke, and ti-ra-ki-ta combinations).
|
||||
|
||||
**7 patterns:** teental (16 beats, the most common taal), jhaptaal
|
||||
(10 beats), rupak (7 beats), dadra (6 beats), keherwa (8 beats, folk
|
||||
and light classical), tabla solo, and tiri kita (fast ornamental
|
||||
pattern).
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
score = Score("4/4", bpm=80)
|
||||
score.drums("teental", repeats=4)
|
||||
|
||||
Dhol
|
||||
~~~~
|
||||
|
||||
The dhol is a double-headed barrel drum from Punjab, played with
|
||||
sticks. It is the driving force behind bhangra music -- loud,
|
||||
energetic, and physically impossible to sit still to.
|
||||
|
||||
**3 sounds** -- bass stroke, treble stroke, and rimshot.
|
||||
|
||||
**2 patterns:** bhangra (the classic bhangra groove) and dhol chaal
|
||||
(a processional rhythm).
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
score = Score("4/4", bpm=160)
|
||||
score.drums("bhangra", repeats=4)
|
||||
|
||||
Dholak
|
||||
~~~~~~
|
||||
|
||||
The dholak is a smaller, lighter two-headed drum used across South
|
||||
Asia in folk music, qawwali, and Bollywood. Played with bare hands,
|
||||
it produces a warm, melodic tone.
|
||||
|
||||
**3 sounds** -- bass, treble, and slap.
|
||||
|
||||
**2 patterns:** qawwali (the rhythmic foundation of Sufi devotional
|
||||
music) and dholak folk (a general folk groove).
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
score = Score("4/4", bpm=120)
|
||||
score.drums("qawwali", repeats=4)
|
||||
|
||||
Mridangam
|
||||
~~~~~~~~~
|
||||
|
||||
The mridangam is a double-headed drum from South India, the
|
||||
rhythmic anchor of Carnatic classical music. Its tuning system is
|
||||
extraordinarily precise, and its rhythmic vocabulary is among the
|
||||
most mathematically complex in the world.
|
||||
|
||||
**4 sounds** -- tha, thom, nam, and din.
|
||||
|
||||
**2 patterns:** adi talam (the most common Carnatic talam, 8 beats)
|
||||
and mridangam korvai (a rhythmic cadence pattern).
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
score = Score("4/4", bpm=90)
|
||||
score.drums("adi talam", repeats=4)
|
||||
|
||||
Djembe
|
||||
~~~~~~
|
||||
|
||||
The djembe is a rope-tuned goblet drum from West Africa, capable of
|
||||
producing a wide range of tones from deep bass to sharp slaps. It is
|
||||
central to the drum ensemble traditions of Mali, Guinea, and Senegal.
|
||||
|
||||
**3 sounds** -- bass (open center strike), tone (edge strike), and
|
||||
slap (sharp edge strike).
|
||||
|
||||
**3 patterns:** djembe (a basic accompanying rhythm), kuku (a
|
||||
traditional rhythm from Guinea associated with fishing), and soli (a
|
||||
solo/celebration rhythm).
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
score = Score("4/4", bpm=120)
|
||||
score.drums("djembe", repeats=4)
|
||||
|
||||
Metal Kit
|
||||
~~~~~~~~~
|
||||
|
||||
A dedicated percussion kit for extreme metal subgenres, with
|
||||
specialized sounds and patterns that go beyond the standard drum kit.
|
||||
|
||||
**3 sounds** -- double kick (triggered, tight attack), china cymbal,
|
||||
and stack (a short, trashy cymbal choke).
|
||||
|
||||
**4 patterns:** double kick (relentless double bass drum pattern),
|
||||
metal blast (blast beat with china cymbal accents), metal groove (a
|
||||
half-time groove with double kick fills), and metal gallop (the
|
||||
classic triplet-feel gallop rhythm).
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
score = Score("4/4", bpm=200)
|
||||
score.drums("metal blast", repeats=4)
|
||||
|
||||
MIDI Export
|
||||
-----------
|
||||
|
||||
|
||||
+91
-7
@@ -32,8 +32,8 @@ It's a well-tested order that sounds good by default.
|
||||
|
||||
Effects are applied in this fixed order::
|
||||
|
||||
Signal --> Saturation --> Tremolo --> Distortion --> Chorus --> Phaser
|
||||
--> Highpass --> Lowpass --> Delay --> Reverb --> Mix
|
||||
Signal --> Saturation --> Tremolo --> Distortion --> Cabinet --> Chorus
|
||||
--> Phaser --> Highpass --> Lowpass --> Delay --> Reverb --> Mix
|
||||
|
||||
Additionally, these per-note effects are applied before the part effects chain:
|
||||
|
||||
@@ -47,11 +47,12 @@ Part-level effects:
|
||||
- **Saturation** first: subtle even-harmonic warmth (tape/tube color).
|
||||
- **Tremolo** second: amplitude LFO modulation.
|
||||
- **Distortion** third: drives the signal before filtering.
|
||||
- **Chorus** fourth: thickens the signal.
|
||||
- **Phaser** fifth: swept allpass notches.
|
||||
- **Highpass** sixth: removes low-frequency mud.
|
||||
- **Lowpass** seventh: shapes the tone (like a tone knob on an amp).
|
||||
- **Delay** eighth: echoes the shaped signal (tap delay / tape echo).
|
||||
- **Cabinet** fourth: speaker cab simulation (rolloff + presence bump).
|
||||
- **Chorus** fifth: thickens the signal.
|
||||
- **Phaser** sixth: swept allpass notches.
|
||||
- **Highpass** seventh: removes low-frequency mud.
|
||||
- **Lowpass** eighth: shapes the tone (like a tone knob on an amp).
|
||||
- **Delay** ninth: echoes the shaped signal (tap delay / tape echo).
|
||||
- **Reverb** last: places everything in a space (room / hall).
|
||||
|
||||
Distortion
|
||||
@@ -96,6 +97,89 @@ Parameters:
|
||||
distortion_drive=10.0,
|
||||
)
|
||||
|
||||
Cabinet Simulation
|
||||
------------------
|
||||
|
||||
A real guitar amp doesn't just distort the signal -- the speaker
|
||||
cabinet shapes the tone dramatically. A 12-inch speaker in a closed
|
||||
cabinet rolls off the harsh high frequencies above 5 kHz and adds a
|
||||
presence bump around 2--3 kHz that gives the sound its "in the room"
|
||||
quality. Without a cabinet, distortion sounds thin and fizzy. With
|
||||
one, it sounds like a real amp.
|
||||
|
||||
PyTheory's cabinet simulation applies a speaker rolloff curve (lowpass
|
||||
at ~5 kHz) combined with a presence resonance bump, placed in the
|
||||
signal chain immediately after distortion -- exactly where it sits in
|
||||
a real amp.
|
||||
|
||||
Parameters:
|
||||
|
||||
- ``cabinet``: Wet/dry mix, 0.0--1.0 (default 0, off).
|
||||
|
||||
- 0.3--0.5 = subtle speaker coloring
|
||||
- 0.6--0.8 = classic amp-in-a-room
|
||||
- 1.0 = full cabinet, no dry signal
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
# Classic rock amp tone: distortion into cabinet
|
||||
guitar = score.part(
|
||||
"guitar",
|
||||
synth="saw",
|
||||
envelope="pluck",
|
||||
distortion=0.6,
|
||||
distortion_drive=5.0,
|
||||
cabinet=0.8,
|
||||
)
|
||||
|
||||
# Clean amp with just cabinet warmth (no distortion)
|
||||
clean = score.part(
|
||||
"clean",
|
||||
synth="triangle",
|
||||
envelope="pluck",
|
||||
cabinet=0.5,
|
||||
)
|
||||
|
||||
Analog Drift
|
||||
------------
|
||||
|
||||
Real analog synthesizers are never perfectly in tune. The voltage-
|
||||
controlled oscillators drift slightly over time as components warm up
|
||||
and temperature fluctuates. This imperfection is actually a big part
|
||||
of why vintage analog synths sound so appealing -- the subtle pitch
|
||||
wandering gives each note a unique, living quality that static digital
|
||||
oscillators lack.
|
||||
|
||||
The ``analog_drift`` parameter adds slow, random pitch variation to
|
||||
each oscillator, modeling this vintage behavior.
|
||||
|
||||
Parameters:
|
||||
|
||||
- ``analog_drift``: Drift amount, 0.0--1.0 (default 0, off).
|
||||
|
||||
- 0.05--0.1 = subtle warmth (studio-grade analog)
|
||||
- 0.15--0.25 = noticeable drift (vintage gear warming up)
|
||||
- 0.3+ = unstable, wobbly (broken tape machine)
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
# Warm vintage pad
|
||||
pad = score.part(
|
||||
"pad",
|
||||
synth="supersaw",
|
||||
envelope="pad",
|
||||
analog_drift=0.1,
|
||||
chorus=0.3,
|
||||
)
|
||||
|
||||
# Lo-fi detuned lead
|
||||
lead = score.part(
|
||||
"lead",
|
||||
synth="saw",
|
||||
envelope="pluck",
|
||||
analog_drift=0.25,
|
||||
)
|
||||
|
||||
Chorus
|
||||
------
|
||||
|
||||
|
||||
@@ -574,3 +574,107 @@ 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")
|
||||
|
||||
Temperaments: ``"equal"`` (default), ``"pythagorean"``, ``"meantone"``,
|
||||
``"just"``.
|
||||
|
||||
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)
|
||||
|
||||
+227
-3
@@ -1,7 +1,7 @@
|
||||
Synthesizers
|
||||
============
|
||||
|
||||
PyTheory includes 13 built-in waveforms and 10 ADSR envelope presets.
|
||||
PyTheory includes 30 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,235 @@ 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
|
||||
17 dedicated instrument synths. Each one uses tailored synthesis
|
||||
techniques -- additive harmonics, formant shaping, body resonance
|
||||
modeling, and specialized envelopes -- to capture the character of a
|
||||
specific acoustic instrument. These are the waveforms that bring the
|
||||
total count to 30.
|
||||
|
||||
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")
|
||||
|
||||
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 — 40+ predefined combinations that approximate real
|
||||
instruments:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
+11
-8
@@ -77,15 +77,18 @@ What's Inside
|
||||
numbers), scale recommendation, modulation, voice leading
|
||||
- **Sequencing** — Score, Parts, arpeggiator, legato/glide, velocity,
|
||||
swing, humanize, tempo changes, song sections with repeat
|
||||
- **Synthesis** — 13 waveforms (including Karplus-Strong pluck, Hammond organ,
|
||||
bowed string), 10 envelopes, 38 instrument presets, configurable FM,
|
||||
sub-oscillator, noise layer, filter envelope, velocity-to-brightness,
|
||||
detune, stereo pan/spread, 58 drum patterns (stereo panned), 21 fills
|
||||
- **Synthesis** — 30 waveforms (including Karplus-Strong pluck, Hammond organ,
|
||||
bowed string, and 14 dedicated instrument synths), 10 envelopes, 40+
|
||||
instrument presets, configurable FM, sub-oscillator, noise layer, filter
|
||||
envelope, velocity-to-brightness, analog oscillator drift, detune, stereo
|
||||
pan/spread, strumming, 80+ drum patterns (stereo panned, including world
|
||||
percussion), 21 fills
|
||||
- **Effects** — reverb (algorithmic + 7 convolution IRs, stereo), delay,
|
||||
lowpass/highpass (with resonance), distortion, saturation, chorus,
|
||||
phaser, tremolo, sidechain compression, automation, LFOs. Master bus
|
||||
compressor/limiter
|
||||
- **Instruments** — 25 presets with fingering generation
|
||||
lowpass/highpass (with resonance), distortion, cabinet simulation,
|
||||
saturation, chorus, phaser, tremolo, analog drift, sidechain compression,
|
||||
automation, LFOs. Master bus compressor/limiter
|
||||
- **Instruments** — 49 presets with fingering generation, guitar strumming,
|
||||
pitch bends
|
||||
- **Output** — stereo playback, WAV export, MIDI import/export
|
||||
- **Interface** — REPL with tab completion, CLI (15 commands), ``pytheory demo``
|
||||
- **AI-friendly** — Claude Code can compose
|
||||
|
||||
+459
-141
@@ -15,7 +15,8 @@ Usage:
|
||||
|
||||
import sounddevice as sd
|
||||
|
||||
from pytheory import Chord, Key, Pattern, Duration, Score
|
||||
from pytheory import Chord, Key, Pattern, Duration, Score, Tone, TonedScale, SYSTEMS
|
||||
from pytheory.rhythm import DrumSound, _Hit
|
||||
from pytheory.play import render_score, SAMPLE_RATE
|
||||
|
||||
|
||||
@@ -45,18 +46,15 @@ def bossa_nova_girl():
|
||||
score = Score("4/4", bpm=140)
|
||||
score.drums("bossa nova", repeats=4)
|
||||
|
||||
rhodes = score.part("rhodes", synth="fm", envelope="piano",
|
||||
rhodes = score.part("rhodes", instrument="electric_piano",
|
||||
volume=0.3, pan=-0.3,
|
||||
reverb=0.4, reverb_decay=1.8, reverb_type="plate",
|
||||
detune=8, humanize=0.2)
|
||||
lead = score.part("lead", synth="triangle", envelope="pluck",
|
||||
reverb=0.4, reverb_decay=1.8, reverb_type="plate")
|
||||
lead = score.part("lead", instrument="flute",
|
||||
volume=0.45, pan=0.3,
|
||||
delay=0.25, delay_time=0.32, delay_feedback=0.35,
|
||||
reverb=0.2, reverb_type="plate",
|
||||
humanize=0.2)
|
||||
bass = score.part("bass", synth="sine", envelope="pluck",
|
||||
volume=0.45, pan=0.0, lowpass=600,
|
||||
humanize=0.15)
|
||||
reverb=0.2, reverb_type="plate")
|
||||
bass = score.part("bass", instrument="upright_bass",
|
||||
volume=0.45, pan=0.0, lowpass=600)
|
||||
|
||||
for sym in ["Am", "Am", "Dm", "Dm", "E7", "E7", "Am", "Am"]:
|
||||
rhodes.add(Chord.from_symbol(sym), Duration.WHOLE)
|
||||
@@ -90,19 +88,16 @@ def bebop_in_bb():
|
||||
score = Score("4/4", bpm=160)
|
||||
score.drums("bebop", repeats=8, fill="jazz", fill_every=8)
|
||||
|
||||
rhodes = score.part("rhodes", synth="fm", envelope="piano",
|
||||
rhodes = score.part("rhodes", instrument="electric_piano",
|
||||
volume=0.25, pan=-0.3,
|
||||
reverb=0.35, reverb_decay=1.2, reverb_type="plate",
|
||||
detune=8, humanize=0.2)
|
||||
lead = score.part("lead", synth="saw", envelope="pluck",
|
||||
reverb=0.35, reverb_decay=1.2, reverb_type="plate")
|
||||
lead = score.part("lead", instrument="trumpet",
|
||||
volume=0.4, pan=0.25,
|
||||
lowpass=4000, lowpass_q=1.1,
|
||||
delay=0.15, delay_time=0.19, delay_feedback=0.25,
|
||||
reverb=0.15, reverb_type="plate",
|
||||
humanize=0.2)
|
||||
bass = score.part("bass", synth="triangle", envelope="pluck",
|
||||
volume=0.4, pan=0.0, lowpass=500,
|
||||
humanize=0.15)
|
||||
reverb=0.15, reverb_type="plate")
|
||||
bass = score.part("bass", instrument="upright_bass",
|
||||
volume=0.4, pan=0.0, lowpass=500)
|
||||
|
||||
for sym in ["Bb", "Gm", "Cm", "F7"] * 2:
|
||||
rhodes.add(Chord.from_symbol(sym), Duration.WHOLE)
|
||||
@@ -148,14 +143,12 @@ def salsa_descarga():
|
||||
volume=0.2, pan=-0.35,
|
||||
reverb=0.3, reverb_type="plate", lowpass=2000,
|
||||
detune=10, humanize=0.2)
|
||||
lead = score.part("lead", synth="saw", envelope="pluck",
|
||||
lead = score.part("lead", instrument="trumpet",
|
||||
volume=0.4, pan=0.3,
|
||||
delay=0.2, delay_time=0.167, delay_feedback=0.3,
|
||||
reverb=0.15, reverb_type="plate",
|
||||
humanize=0.2)
|
||||
bass = score.part("bass", synth="pulse", envelope="pluck",
|
||||
volume=0.45, pan=0.0, lowpass=500, lowpass_q=1.3,
|
||||
humanize=0.15)
|
||||
reverb=0.15, reverb_type="plate")
|
||||
bass = score.part("bass", instrument="synth_bass",
|
||||
volume=0.45, pan=0.0, lowpass=500, lowpass_q=1.3)
|
||||
|
||||
for sym in ["Em7b5", "A7", "Dm7", "Bbmaj7"] * 2:
|
||||
pads.add(Chord.from_symbol(sym), Duration.WHOLE)
|
||||
@@ -191,20 +184,17 @@ def afrobeat_groove():
|
||||
score = Score("4/4", bpm=115)
|
||||
score.drums("afrobeat", repeats=8, fill="afrobeat", fill_every=8)
|
||||
|
||||
pads = score.part("pads", synth="supersaw", envelope="pad",
|
||||
pads = score.part("pads", instrument="synth_pad",
|
||||
volume=0.2, pan=-0.3,
|
||||
reverb=0.4, reverb_decay=2.0, reverb_type="cathedral",
|
||||
lowpass=3000, detune=10, spread=0.6,
|
||||
humanize=0.2)
|
||||
lead = score.part("lead", synth="saw", envelope="pluck",
|
||||
lowpass=3000)
|
||||
lead = score.part("lead", instrument="trumpet",
|
||||
volume=0.4, pan=0.3,
|
||||
lowpass=3000, lowpass_q=1.0,
|
||||
delay=0.2, delay_time=0.26, delay_feedback=0.3,
|
||||
reverb=0.15, reverb_type="plate",
|
||||
humanize=0.2)
|
||||
bass = score.part("bass", synth="sine", envelope="pluck",
|
||||
volume=0.5, pan=0.0, lowpass=500,
|
||||
humanize=0.15)
|
||||
reverb=0.15, reverb_type="plate")
|
||||
bass = score.part("bass", instrument="bass_guitar",
|
||||
volume=0.5, pan=0.0, lowpass=500)
|
||||
|
||||
for sym in ["Em", "Am", "D", "C"] * 2:
|
||||
pads.add(Chord.from_symbol(sym), Duration.WHOLE)
|
||||
@@ -241,14 +231,12 @@ def reggae_one_drop():
|
||||
reverb=0.5, reverb_decay=2.0, reverb_type="cathedral",
|
||||
lowpass=2000, detune=8,
|
||||
humanize=0.2)
|
||||
lead = score.part("lead", synth="triangle", envelope="strings",
|
||||
lead = score.part("lead", instrument="flute",
|
||||
volume=0.4, pan=0.3,
|
||||
delay=0.35, delay_time=0.5625, delay_feedback=0.45,
|
||||
reverb=0.3, reverb_type="cathedral",
|
||||
humanize=0.2)
|
||||
bass = score.part("bass", synth="sine", envelope="pluck",
|
||||
volume=0.55, pan=0.0, lowpass=400, lowpass_q=1.3,
|
||||
humanize=0.15)
|
||||
reverb=0.3, reverb_type="cathedral")
|
||||
bass = score.part("bass", instrument="bass_guitar",
|
||||
volume=0.55, pan=0.0, lowpass=400, lowpass_q=1.3)
|
||||
|
||||
for sym in ["G", "C", "D", "C"] * 2:
|
||||
chords.add(Chord.from_symbol(sym), Duration.WHOLE)
|
||||
@@ -286,15 +274,13 @@ def funk_workout():
|
||||
volume=0.25, pan=-0.4,
|
||||
lowpass=2500, reverb=0.15, reverb_type="plate",
|
||||
sidechain=0.4, humanize=0.2)
|
||||
lead = score.part("lead", synth="saw", envelope="pluck",
|
||||
lead = score.part("lead", instrument="synth_lead",
|
||||
volume=0.4, pan=0.35,
|
||||
lowpass=3500, lowpass_q=1.5,
|
||||
delay=0.15, delay_time=0.15, delay_feedback=0.25,
|
||||
reverb=0.1, reverb_type="plate",
|
||||
humanize=0.2)
|
||||
bass = score.part("bass", synth="pulse", envelope="pluck",
|
||||
volume=0.5, pan=0.0, lowpass=600, lowpass_q=1.2,
|
||||
humanize=0.15)
|
||||
reverb=0.1, reverb_type="plate")
|
||||
bass = score.part("bass", instrument="synth_bass",
|
||||
volume=0.5, pan=0.0, lowpass=600, lowpass_q=1.2)
|
||||
|
||||
for sym in ["Em", "Am", "D", "B7"] * 2:
|
||||
chords.add(Chord.from_symbol(sym), Duration.WHOLE)
|
||||
@@ -332,18 +318,16 @@ def blues_shuffle():
|
||||
score = Score("12/8", bpm=70)
|
||||
score.drums("12/8 blues", repeats=6)
|
||||
|
||||
chords = score.part("chords", synth="fm", envelope="piano",
|
||||
chords = score.part("chords", instrument="electric_piano",
|
||||
volume=0.3, pan=-0.3,
|
||||
reverb=0.3, reverb_decay=1.5, reverb_type="plate",
|
||||
detune=8, humanize=0.2)
|
||||
lead = score.part("lead", synth="saw", envelope="pluck",
|
||||
reverb=0.3, reverb_decay=1.5, reverb_type="plate")
|
||||
lead = score.part("lead", instrument="trumpet",
|
||||
volume=0.45, pan=0.25,
|
||||
reverb=0.3, reverb_decay=1.2, reverb_type="plate",
|
||||
delay=0.2, delay_time=0.43, delay_feedback=0.3,
|
||||
lowpass=3500, humanize=0.2)
|
||||
bass = score.part("bass", synth="sine", envelope="pluck",
|
||||
volume=0.5, pan=0.0, lowpass=500,
|
||||
humanize=0.15)
|
||||
lowpass=3500)
|
||||
bass = score.part("bass", instrument="upright_bass",
|
||||
volume=0.5, pan=0.0, lowpass=500)
|
||||
|
||||
for sym in ["A", "A", "D", "D", "E7", "A"]:
|
||||
chords.add(Chord.from_symbol(sym), Duration.DOTTED_HALF)
|
||||
@@ -381,19 +365,16 @@ def samba_de_janeiro():
|
||||
score = Score("4/4", bpm=170)
|
||||
score.drums("samba", repeats=8, fill="samba", fill_every=8)
|
||||
|
||||
pads = score.part("pads", synth="supersaw", envelope="pad",
|
||||
pads = score.part("pads", instrument="synth_pad",
|
||||
volume=0.2, pan=-0.3,
|
||||
reverb=0.45, reverb_decay=2.0, reverb_type="plate",
|
||||
lowpass=4000, detune=10, spread=0.5,
|
||||
humanize=0.2)
|
||||
lead = score.part("lead", synth="triangle", envelope="pluck",
|
||||
lowpass=4000)
|
||||
lead = score.part("lead", instrument="flute",
|
||||
volume=0.45, pan=0.3,
|
||||
delay=0.2, delay_time=0.176, delay_feedback=0.3,
|
||||
reverb=0.15, reverb_type="plate",
|
||||
humanize=0.2)
|
||||
bass = score.part("bass", synth="sine", envelope="pluck",
|
||||
volume=0.45, pan=0.0, lowpass=500,
|
||||
humanize=0.15)
|
||||
reverb=0.15, reverb_type="plate")
|
||||
bass = score.part("bass", instrument="bass_guitar",
|
||||
volume=0.45, pan=0.0, lowpass=500)
|
||||
|
||||
for sym in ["G", "Em", "Am", "D7"] * 2:
|
||||
pads.add(Chord.from_symbol(sym), Duration.WHOLE)
|
||||
@@ -426,18 +407,15 @@ def jazz_waltz():
|
||||
score = Score("3/4", bpm=150)
|
||||
score.drums("waltz", repeats=16)
|
||||
|
||||
rhodes = score.part("rhodes", synth="fm", envelope="piano",
|
||||
rhodes = score.part("rhodes", instrument="electric_piano",
|
||||
volume=0.3, pan=-0.3,
|
||||
reverb=0.4, reverb_decay=2.0, reverb_type="cathedral",
|
||||
detune=8, humanize=0.2)
|
||||
lead = score.part("lead", synth="triangle", envelope="strings",
|
||||
reverb=0.4, reverb_decay=2.0, reverb_type="cathedral")
|
||||
lead = score.part("lead", instrument="flute",
|
||||
volume=0.4, pan=0.25,
|
||||
reverb=0.3, reverb_decay=1.5, reverb_type="plate",
|
||||
delay=0.2, delay_time=0.4, delay_feedback=0.3,
|
||||
humanize=0.2)
|
||||
bass = score.part("bass", synth="sine", envelope="pluck",
|
||||
volume=0.4, pan=0.0, lowpass=500,
|
||||
humanize=0.15)
|
||||
delay=0.2, delay_time=0.4, delay_feedback=0.3)
|
||||
bass = score.part("bass", instrument="upright_bass",
|
||||
volume=0.4, pan=0.0, lowpass=500)
|
||||
|
||||
for _ in range(2):
|
||||
for sym in ["Fmaj7", "Gm", "C7", "Fmaj7"]:
|
||||
@@ -471,20 +449,19 @@ def house_anthem():
|
||||
score = Score("4/4", bpm=124)
|
||||
score.drums("house", repeats=8, fill="house", fill_every=8)
|
||||
|
||||
pads = score.part("pads", synth="supersaw", envelope="pad",
|
||||
pads = score.part("pads", instrument="synth_pad",
|
||||
volume=0.25, pan=-0.3,
|
||||
reverb=0.5, reverb_decay=2.5, reverb_type="cathedral",
|
||||
lowpass=5000, detune=12, spread=0.7,
|
||||
sidechain=0.6, humanize=0.2)
|
||||
lead = score.part("lead", synth="saw", envelope="staccato",
|
||||
lowpass=5000,
|
||||
sidechain=0.6)
|
||||
lead = score.part("lead", instrument="synth_lead",
|
||||
volume=0.35, pan=0.3,
|
||||
lowpass=2000, lowpass_q=2.0,
|
||||
delay=0.2, delay_time=0.242, delay_feedback=0.35,
|
||||
reverb=0.15, reverb_type="plate",
|
||||
humanize=0.2)
|
||||
bass = score.part("bass", synth="sine", envelope="pluck",
|
||||
volume=0.55, pan=0.0, lowpass=300,
|
||||
sidechain=0.5, humanize=0.15)
|
||||
reverb=0.15, reverb_type="plate")
|
||||
bass = score.part("bass", instrument="808_bass",
|
||||
volume=0.55, pan=0.0,
|
||||
sidechain=0.5)
|
||||
|
||||
for sym in ["Cm", "Ab", "Bb", "Cm"] * 2:
|
||||
pads.add(Chord.from_symbol(sym), Duration.WHOLE)
|
||||
@@ -538,14 +515,12 @@ def dub_kingston():
|
||||
reverb=0.6, reverb_decay=2.5, reverb_type="cathedral",
|
||||
lowpass=1500, lowpass_q=0.9, detune=8,
|
||||
humanize=0.2)
|
||||
lead = score.part("lead", synth="triangle", envelope="strings",
|
||||
lead = score.part("lead", instrument="flute",
|
||||
volume=0.4, pan=0.3,
|
||||
delay=0.45, delay_time=0.625, delay_feedback=0.5,
|
||||
reverb=0.35, reverb_decay=2.0, reverb_type="cathedral",
|
||||
humanize=0.2)
|
||||
bass = score.part("bass", synth="sine", envelope="pluck",
|
||||
volume=0.6, pan=0.0, lowpass=400, lowpass_q=1.5,
|
||||
humanize=0.15)
|
||||
reverb=0.35, reverb_decay=2.0, reverb_type="cathedral")
|
||||
bass = score.part("bass", instrument="bass_guitar",
|
||||
volume=0.6, pan=0.0, lowpass=400, lowpass_q=1.5)
|
||||
siren = score.part("siren", synth="pwm_slow", envelope="pad",
|
||||
volume=0.15, pan=0.5,
|
||||
reverb=0.7, reverb_decay=3.0, reverb_type="cathedral",
|
||||
@@ -585,20 +560,20 @@ def techno_minimal():
|
||||
score = Score("4/4", bpm=130)
|
||||
score.drums("techno", repeats=8, fill="house", fill_every=8)
|
||||
|
||||
pad = score.part("pad", synth="supersaw", envelope="pad",
|
||||
pad = score.part("pad", instrument="synth_pad",
|
||||
volume=0.2, pan=-0.3,
|
||||
reverb=0.5, reverb_decay=3.0, reverb_type="cathedral",
|
||||
lowpass=3000, detune=12, spread=0.7,
|
||||
sidechain=0.6, humanize=0.2)
|
||||
lowpass=3000,
|
||||
sidechain=0.6)
|
||||
lead = score.part("lead", synth="pwm_fast", envelope="staccato",
|
||||
volume=0.35, pan=0.3,
|
||||
lowpass=1500, lowpass_q=3.0,
|
||||
delay=0.3, delay_time=0.231, delay_feedback=0.4,
|
||||
reverb=0.1, reverb_type="plate",
|
||||
humanize=0.2)
|
||||
bass = score.part("bass", synth="sine", envelope="pluck",
|
||||
volume=0.55, pan=0.0, lowpass=250,
|
||||
sidechain=0.5, humanize=0.15)
|
||||
bass = score.part("bass", instrument="808_bass",
|
||||
volume=0.55, pan=0.0,
|
||||
sidechain=0.5)
|
||||
|
||||
for sym in ["Fm", "Db", "Eb", "Fm"] * 2:
|
||||
pad.add(Chord.from_symbol(sym), Duration.WHOLE)
|
||||
@@ -624,18 +599,15 @@ def gospel_shuffle():
|
||||
score = Score("4/4", bpm=108)
|
||||
score.drums("gospel", repeats=8, fill="buildup", fill_every=8)
|
||||
|
||||
organ = score.part("organ", synth="fm", envelope="organ",
|
||||
organ = score.part("organ", instrument="organ",
|
||||
volume=0.3, pan=-0.3,
|
||||
reverb=0.45, reverb_decay=2.0, reverb_type="cathedral",
|
||||
detune=8, humanize=0.2)
|
||||
lead = score.part("lead", synth="triangle", envelope="pluck",
|
||||
reverb=0.45, reverb_decay=2.0, reverb_type="cathedral")
|
||||
lead = score.part("lead", instrument="flute",
|
||||
volume=0.4, pan=0.3,
|
||||
delay=0.2, delay_time=0.278, delay_feedback=0.3,
|
||||
reverb=0.2, reverb_type="plate",
|
||||
humanize=0.2)
|
||||
bass = score.part("bass", synth="sine", envelope="pluck",
|
||||
volume=0.45, pan=0.0, lowpass=500,
|
||||
humanize=0.15)
|
||||
reverb=0.2, reverb_type="plate")
|
||||
bass = score.part("bass", instrument="upright_bass",
|
||||
volume=0.45, pan=0.0, lowpass=500)
|
||||
|
||||
for sym in ["C", "Am", "F", "G"] * 2:
|
||||
organ.add(Chord.from_symbol(sym), Duration.WHOLE)
|
||||
@@ -691,20 +663,18 @@ def dub_delay_madness():
|
||||
volume=0.15, pan=-0.4,
|
||||
reverb=0.7, reverb_decay=3.0, reverb_type="cathedral",
|
||||
lowpass=1200, detune=8, humanize=0.2)
|
||||
bass = score.part("bass", synth="sine", envelope="pluck",
|
||||
volume=0.6, pan=0.0, lowpass=350, lowpass_q=1.5,
|
||||
humanize=0.15)
|
||||
bass = score.part("bass", instrument="bass_guitar",
|
||||
volume=0.6, pan=0.0, lowpass=350, lowpass_q=1.5)
|
||||
siren = score.part("siren", synth="pwm_slow", envelope="pad",
|
||||
volume=0.12, pan=0.5,
|
||||
reverb=0.8, reverb_decay=4.0, reverb_type="cathedral",
|
||||
delay=0.4, delay_time=0.88, delay_feedback=0.6,
|
||||
lowpass=900, detune=10)
|
||||
# Melodica stabs — sparse, lots of delay
|
||||
melodica = score.part("melodica", synth="triangle", envelope="pluck",
|
||||
melodica = score.part("melodica", instrument="flute",
|
||||
volume=0.35, pan=0.3,
|
||||
delay=0.6, delay_time=0.66, delay_feedback=0.55,
|
||||
reverb=0.5, reverb_decay=2.5, reverb_type="cathedral",
|
||||
humanize=0.2)
|
||||
reverb=0.5, reverb_decay=2.5, reverb_type="cathedral")
|
||||
|
||||
for sym in ["Em", "Em", "Am", "Am", "Em", "Em", "Bm", "Em"]:
|
||||
chords.add(Chord.from_symbol(sym), Duration.WHOLE)
|
||||
@@ -741,19 +711,16 @@ def drum_and_bass():
|
||||
score = Score("4/4", bpm=174)
|
||||
score.drums("drum and bass", repeats=8, fill="buildup", fill_every=8)
|
||||
|
||||
pads = score.part("pads", synth="supersaw", envelope="pad",
|
||||
pads = score.part("pads", instrument="synth_pad",
|
||||
volume=0.25, pan=-0.3,
|
||||
reverb=0.5, reverb_decay=2.5, reverb_type="plate",
|
||||
lowpass=4000, detune=10, spread=0.6,
|
||||
humanize=0.2)
|
||||
lead = score.part("lead", synth="triangle", envelope="strings",
|
||||
lowpass=4000)
|
||||
lead = score.part("lead", instrument="flute",
|
||||
volume=0.4, pan=0.3,
|
||||
delay=0.3, delay_time=0.172, delay_feedback=0.4,
|
||||
reverb=0.25, reverb_type="plate",
|
||||
humanize=0.2)
|
||||
bass = score.part("bass", synth="sine", envelope="pluck",
|
||||
volume=0.55, pan=0.0, lowpass=300,
|
||||
humanize=0.15)
|
||||
reverb=0.25, reverb_type="plate")
|
||||
bass = score.part("bass", instrument="808_bass",
|
||||
volume=0.55, pan=0.0)
|
||||
|
||||
for sym in ["Am", "F", "C", "G"] * 2:
|
||||
pads.add(Chord.from_symbol(sym), Duration.WHOLE)
|
||||
@@ -787,25 +754,24 @@ def drake_vibes():
|
||||
score = Score("4/4", bpm=68)
|
||||
score.drums("trap", repeats=8, fill="trap", fill_every=8)
|
||||
|
||||
pads = score.part("pads", synth="supersaw", envelope="pad",
|
||||
pads = score.part("pads", instrument="synth_pad",
|
||||
volume=0.2, pan=-0.25,
|
||||
reverb=0.5, reverb_decay=3.0, reverb_type="cathedral",
|
||||
lowpass=2500, detune=12, spread=0.6,
|
||||
sidechain=0.4, humanize=0.2)
|
||||
bells = score.part("bells", synth="fm", envelope="bell",
|
||||
lowpass=2500,
|
||||
sidechain=0.4)
|
||||
bells = score.part("bells", instrument="vibraphone",
|
||||
volume=0.3, pan=0.4,
|
||||
reverb=0.4, reverb_decay=2.0, reverb_type="plate",
|
||||
delay=0.25, delay_time=0.44, delay_feedback=0.35,
|
||||
humanize=0.2)
|
||||
delay=0.25, delay_time=0.44, delay_feedback=0.35)
|
||||
lead = score.part("lead", synth="pwm_slow", envelope="strings",
|
||||
volume=0.35, pan=-0.2,
|
||||
reverb=0.3, reverb_type="cathedral", lowpass=2000,
|
||||
delay=0.2, delay_time=0.88, delay_feedback=0.3,
|
||||
humanize=0.2)
|
||||
bass = score.part("bass", synth="sine", envelope="pluck",
|
||||
volume=0.6, pan=0.0, lowpass=200, lowpass_q=1.8,
|
||||
bass = score.part("bass", instrument="808_bass",
|
||||
volume=0.6, pan=0.0,
|
||||
distortion=0.4, distortion_drive=2.0,
|
||||
sidechain=0.3, humanize=0.15)
|
||||
sidechain=0.3)
|
||||
|
||||
for sym in ["Ebm", "B", "Gb", "Db"] * 2:
|
||||
pads.add(Chord.from_symbol(sym), Duration.WHOLE)
|
||||
@@ -858,14 +824,14 @@ def neon_grid():
|
||||
score._drum_pattern_beats = max(score._drum_pattern_beats, 32.0)
|
||||
|
||||
sky = score.part(
|
||||
"sky", synth="supersaw", envelope="pad",
|
||||
"sky", instrument="synth_pad",
|
||||
volume=0.18, pan=0.0,
|
||||
detune=30, spread=1.0,
|
||||
reverb=0.6, reverb_decay=3.5,
|
||||
chorus=0.3, sidechain=0.5,
|
||||
)
|
||||
acid_l = score.part(
|
||||
"acid_l", synth="saw", envelope="pad",
|
||||
"acid_l", instrument="acid_bass",
|
||||
volume=0.35, pan=-0.7,
|
||||
legato=True, glide=0.025,
|
||||
distortion=0.9, distortion_drive=12.0,
|
||||
@@ -875,7 +841,7 @@ def neon_grid():
|
||||
acid_l.lfo("lowpass", rate=0.5, min=400, max=3000, bars=8, shape="sine")
|
||||
|
||||
acid_r = score.part(
|
||||
"acid_r", synth="saw", envelope="pad",
|
||||
"acid_r", instrument="acid_bass",
|
||||
volume=0.3, pan=0.7,
|
||||
legato=True, glide=0.02,
|
||||
distortion=0.85, distortion_drive=10.0,
|
||||
@@ -885,7 +851,7 @@ def neon_grid():
|
||||
acid_r.lfo("lowpass", rate=0.33, min=500, max=2500, bars=8, shape="triangle")
|
||||
|
||||
sub = score.part(
|
||||
"sub", synth="sine", envelope="pluck",
|
||||
"sub", instrument="808_bass",
|
||||
volume=0.55, pan=0.0,
|
||||
lowpass=160, sidechain=0.85, sidechain_release=0.08,
|
||||
)
|
||||
@@ -896,12 +862,12 @@ def neon_grid():
|
||||
detune=15, spread=0.7,
|
||||
)
|
||||
bell_l = score.part(
|
||||
"bell_l", synth="fm", envelope="bell",
|
||||
"bell_l", instrument="vibraphone",
|
||||
volume=0.1, pan=-1.0,
|
||||
reverb=0.7, reverb_decay=3.0,
|
||||
)
|
||||
bell_r = score.part(
|
||||
"bell_r", synth="fm", envelope="bell",
|
||||
"bell_r", instrument="vibraphone",
|
||||
volume=0.1, pan=1.0,
|
||||
reverb=0.7, reverb_decay=3.0,
|
||||
delay=0.2, delay_time=0.8, delay_feedback=0.4,
|
||||
@@ -1026,14 +992,14 @@ def dance_party():
|
||||
score._drum_hits.append(_Hit(DrumSound.KICK, bar * 4.0 + beat, 120))
|
||||
score._drum_pattern_beats = max(score._drum_pattern_beats, 32.0)
|
||||
|
||||
bass = score.part("bass", synth="square", envelope="pluck",
|
||||
bass = score.part("bass", instrument="synth_bass",
|
||||
volume=0.45, lowpass=500, lowpass_q=1.3,
|
||||
sidechain=0.75, sidechain_release=0.12)
|
||||
sparkle = score.part("sparkle", synth="fm", envelope="bell",
|
||||
sparkle = score.part("sparkle", instrument="vibraphone",
|
||||
volume=0.3, pan=0.4, reverb=0.3, reverb_decay=1.5,
|
||||
delay=0.2, delay_time=0.234, delay_feedback=0.3)
|
||||
chords_part = score.part("chords", synth="supersaw", envelope="pad",
|
||||
volume=0.2, detune=12, spread=0.7,
|
||||
chords_part = score.part("chords", instrument="synth_pad",
|
||||
volume=0.2,
|
||||
reverb=0.4, reverb_type="plate", sidechain=0.7)
|
||||
fun = score.part("fun", synth="square", envelope="staccato",
|
||||
volume=0.2, pan=-0.5, delay=0.15, delay_time=0.117,
|
||||
@@ -1080,14 +1046,13 @@ def temple_bell():
|
||||
score = Score("4/4", bpm=65)
|
||||
score.drums("bolero", repeats=8)
|
||||
|
||||
koto = score.part("koto", synth="triangle", envelope="pluck",
|
||||
koto = score.part("koto", instrument="koto",
|
||||
volume=0.45, pan=0.2,
|
||||
reverb=0.5, reverb_type="taj_mahal",
|
||||
humanize=0.3)
|
||||
reverb=0.5, reverb_type="taj_mahal")
|
||||
drone = score.part("drone", synth="sine", envelope="pad",
|
||||
volume=0.15, reverb=0.6, reverb_type="taj_mahal",
|
||||
chorus=0.15, chorus_rate=0.2)
|
||||
bell = score.part("bell", synth="fm", envelope="bell",
|
||||
bell = score.part("bell", instrument="vibraphone",
|
||||
volume=0.1, pan=-0.6,
|
||||
reverb=0.8, reverb_type="taj_mahal")
|
||||
|
||||
@@ -1235,6 +1200,357 @@ def greensleeves():
|
||||
play_song(score, "Greensleeves — Renaissance Lute (Meantone, A=415)")
|
||||
|
||||
|
||||
def tabla_solo_yaman():
|
||||
"""Tabla solo with tanpura drone, sitar, and Raga Yaman — 22-shruti tuning."""
|
||||
shruti = SYSTEMS["shruti"]
|
||||
score = Score("4/4", bpm=160, system=shruti)
|
||||
h = _Hit
|
||||
|
||||
NA = DrumSound.TABLA_NA
|
||||
TI = DrumSound.TABLA_TIN
|
||||
GE = DrumSound.TABLA_GE
|
||||
DH = DrumSound.TABLA_DHA
|
||||
TT = DrumSound.TABLA_TIT
|
||||
KE = DrumSound.TABLA_KE
|
||||
GB = DrumSound.TABLA_GE_BEND
|
||||
|
||||
# Tanpura drone — Sa + Pa
|
||||
tanpura_sa = score.part("tanpura_sa", synth="strings_synth", envelope="pad",
|
||||
detune=3, lowpass=1000, volume=0.18,
|
||||
reverb=0.5, reverb_type="taj_mahal")
|
||||
tanpura_pa = score.part("tanpura_pa", synth="strings_synth", envelope="pad",
|
||||
detune=3, lowpass=1400, volume=0.14,
|
||||
reverb=0.5, reverb_type="taj_mahal")
|
||||
sa3 = Tone("Sa", octave=3, system=shruti)
|
||||
pa3 = Tone("Pa", octave=3, system=shruti)
|
||||
for _ in range(16):
|
||||
tanpura_sa.add(sa3, Duration.WHOLE)
|
||||
tanpura_pa.add(pa3, Duration.WHOLE)
|
||||
|
||||
# Quiet sitar — Raga Yaman (Kalyan thaat)
|
||||
sitar = score.part("sitar", instrument="sitar", volume=0.12,
|
||||
reverb=0.4, reverb_type="taj_mahal")
|
||||
ts = TonedScale(system=shruti, tonic=Tone("Sa", octave=4, system=shruti))
|
||||
y = list(ts["kalyan"].tones)
|
||||
S, R, G, M, P, D, N, S2 = y
|
||||
sitar.rest(Duration.WHOLE)
|
||||
sitar.rest(Duration.WHOLE)
|
||||
for tone, dur, vel in [
|
||||
(S, 3.0, 55), (R, 1.0, 50), (G, 3.0, 58), (R, 1.0, 48),
|
||||
(S, 4.0, 55), (G, 1.0, 50), (M, 1.0, 52), (P, 3.0, 58),
|
||||
(M, 1.0, 48), (G, 1.0, 50), (R, 1.0, 48), (S, 4.0, 55),
|
||||
(P, 2.0, 52), (D, 1.0, 55), (N, 2.0, 58), (S2, 3.0, 60),
|
||||
(N, 1.0, 52), (D, 1.0, 50), (P, 1.0, 52), (G, 1.0, 48),
|
||||
(R, 1.0, 48), (S, 4.0, 55),
|
||||
]:
|
||||
sitar.add(tone, dur, velocity=vel)
|
||||
|
||||
# 4 bars drone intro (silence for drums)
|
||||
silence = Pattern(name="silence", time_signature="4/4", beats=16.0, hits=[])
|
||||
score.add_pattern(silence, repeats=1)
|
||||
|
||||
# Gentle opening
|
||||
p1 = Pattern(name="gentle", time_signature="4/4", beats=8.0, hits=[
|
||||
h(DH, 0.0, 80), h(NA, 2.0, 60),
|
||||
h(DH, 4.0, 85), h(NA, 5.0, 55), h(NA, 6.0, 60), h(DH, 7.0, 80),
|
||||
])
|
||||
|
||||
# Building with ghost notes
|
||||
p2 = Pattern(name="build", time_signature="4/4", beats=16.0, hits=[
|
||||
h(DH, 0.0, 95), h(TT, 0.5, 35), h(NA, 1.0, 70), h(TT, 1.5, 30),
|
||||
h(NA, 2.0, 65), h(DH, 3.0, 90),
|
||||
h(DH, 4.0, 100), h(TT, 4.25, 35), h(TT, 4.5, 40), h(NA, 5.0, 75),
|
||||
h(TT, 5.5, 35), h(NA, 6.0, 70), h(TT, 6.5, 30), h(DH, 7.0, 95),
|
||||
h(DH, 8.0, 95), h(TI, 9.0, 70), h(TI, 10.0, 72), h(NA, 11.0, 80),
|
||||
h(TT, 11.25, 40), h(TT, 11.5, 42), h(KE, 11.75, 45),
|
||||
h(TT, 12.0, 50), h(TT, 12.25, 55), h(KE, 12.5, 58), h(NA, 12.75, 70),
|
||||
h(DH, 13.0, 100), h(TT, 13.25, 40), h(TT, 13.5, 45), h(KE, 13.75, 50),
|
||||
h(NA, 14.0, 75), h(KE, 14.25, 50), h(DH, 14.5, 85), h(NA, 14.75, 70),
|
||||
h(DH, 15.0, 110), h(GB, 15.5, 100),
|
||||
])
|
||||
|
||||
# Full intensity
|
||||
p3 = Pattern(name="fire", time_signature="4/4", beats=16.0, hits=[
|
||||
h(TT, 0.0, 50), h(TT, 0.125, 35), h(TT, 0.25, 45), h(KE, 0.5, 55),
|
||||
h(NA, 0.75, 85),
|
||||
h(DH, 1.0, 115), h(TT, 1.25, 38), h(DH, 1.5, 70), h(NA, 1.75, 60),
|
||||
h(TT, 2.0, 50), h(TT, 2.125, 35), h(TT, 2.25, 48), h(KE, 2.5, 55),
|
||||
h(NA, 2.75, 88),
|
||||
h(DH, 3.0, 115), h(GB, 3.5, 105), h(NA, 3.75, 72),
|
||||
h(NA, 4.0, 115), h(NA, 4.25, 60), h(TT, 4.5, 40), h(NA, 4.75, 105),
|
||||
h(GE, 5.0, 105), h(GE, 5.25, 55), h(GB, 5.5, 95), h(GE, 5.75, 50),
|
||||
h(NA, 6.0, 115), h(TT, 6.125, 30), h(TT, 6.25, 38), h(NA, 6.5, 100),
|
||||
h(TT, 6.625, 32), h(TT, 6.75, 42),
|
||||
h(GB, 7.0, 115), h(KE, 7.25, 52), h(GE, 7.5, 72), h(KE, 7.75, 48),
|
||||
# Tihai
|
||||
h(DH, 8.0, 115), h(NA, 8.25, 78), h(TT, 8.5, 52), h(KE, 8.75, 58),
|
||||
h(DH, 9.0, 105),
|
||||
h(DH, 9.5, 110), h(NA, 9.75, 78), h(TT, 10.0, 52), h(KE, 10.25, 58),
|
||||
h(DH, 10.5, 105),
|
||||
h(DH, 11.0, 120), h(NA, 11.25, 82), h(TT, 11.5, 58), h(KE, 11.75, 62),
|
||||
h(DH, 12.0, 120),
|
||||
# Silence... then finish
|
||||
h(GB, 14.5, 120),
|
||||
h(DH, 15.5, 127), h(DH, 15.75, 127),
|
||||
])
|
||||
|
||||
score.add_pattern(p1, repeats=1)
|
||||
score.add_pattern(p2, repeats=1)
|
||||
score.add_pattern(p3, repeats=1)
|
||||
score.set_drum_effects(reverb=0.4, reverb_type="taj_mahal")
|
||||
|
||||
play_song(score, "Tabla Solo — Raga Yaman (22-Shruti, Taj Mahal)")
|
||||
|
||||
|
||||
def journey():
|
||||
"""Journey — piano → orchestra → world → sitar EDM.
|
||||
|
||||
One reverb space (Taj Mahal), tanpura drone throughout. Piano opens
|
||||
alone, cello joins, harp/oboe/flute take over with djembe, sitar
|
||||
arrives over tabla, builds to an EDM section with house drums.
|
||||
"""
|
||||
REV = "taj_mahal"
|
||||
score = Score("4/4", bpm=72)
|
||||
|
||||
# ── Drone — runs the entire piece ──
|
||||
tanpura = score.part("tanpura", synth="strings_synth", envelope="pad",
|
||||
detune=3, lowpass=1000, volume=0.12,
|
||||
reverb=0.5, reverb_type=REV)
|
||||
for _ in range(40):
|
||||
tanpura.add("A2", Duration.WHOLE)
|
||||
|
||||
# ── Bars 1-8: Piano alone, then cello ──
|
||||
piano = score.part("piano", instrument="piano", volume=0.35,
|
||||
reverb=0.35, reverb_type=REV)
|
||||
for notes in [
|
||||
["A2","E3","A3","C4","E4","C4","A3","E3"],
|
||||
["F2","C3","F3","A3","C4","A3","F3","C3"],
|
||||
["G2","D3","G3","B3","D4","B3","G3","D3"],
|
||||
["E2","B2","E3","G#3","B3","G#3","E3","B2"],
|
||||
["A2","E3","A3","C4","E4","C4","A3","E3"],
|
||||
["D2","A2","D3","F3","A3","F3","D3","A2"],
|
||||
["E2","B2","E3","G#3","B3","G#3","E3","B2"],
|
||||
["A2","E3","A3","C4","E4","A4","E4","C4"],
|
||||
]:
|
||||
for n in notes:
|
||||
piano.add(n, Duration.EIGHTH, velocity=68)
|
||||
|
||||
cello = score.part("cello", instrument="cello", volume=0.2,
|
||||
reverb=0.4, reverb_type=REV)
|
||||
cello.rest(Duration.WHOLE)
|
||||
for note, dur, vel in [
|
||||
("A3", 4.0, 55), ("C4", 4.0, 58),
|
||||
("B3", 2.0, 52), ("A3", 2.0, 50), ("G3", 4.0, 55),
|
||||
("F3", 4.0, 52), ("E3", 4.0, 55), ("A3", 4.0, 58),
|
||||
]:
|
||||
cello.add(note, dur, velocity=vel)
|
||||
|
||||
# ── Bars 9-16: Harp + oboe + flute + djembe ──
|
||||
harp = score.part("harp", instrument="harp", volume=0.28,
|
||||
reverb=0.45, reverb_type=REV)
|
||||
oboe = score.part("oboe", instrument="oboe", volume=0.22,
|
||||
reverb=0.4, reverb_type=REV)
|
||||
flute = score.part("flute", instrument="flute", volume=0.18,
|
||||
reverb=0.4, reverb_type=REV)
|
||||
for _ in range(8):
|
||||
harp.rest(Duration.WHOLE)
|
||||
for notes in [
|
||||
["A3","C4","E4","A4","C5","E5","A5","E5"],
|
||||
["D3","F3","A3","D4","F4","A4","D5","A4"],
|
||||
["E3","G3","B3","E4","G4","B4","E5","B4"],
|
||||
["A3","C4","E4","A4","E5","C5","A4","E4"],
|
||||
["F3","A3","C4","F4","A4","C5","F5","C5"],
|
||||
["E3","G#3","B3","E4","G#4","B4","E5","B4"],
|
||||
]:
|
||||
for n in notes:
|
||||
harp.add(n, Duration.EIGHTH, velocity=58)
|
||||
for _ in range(9):
|
||||
oboe.rest(Duration.WHOLE)
|
||||
for note, dur, vel in [
|
||||
("E5", 1.5, 62), ("D5", 0.5, 55), ("C5", 1.0, 58),
|
||||
("A4", 1.0, 62), ("G4", 1.0, 55), ("A4", 1.5, 58),
|
||||
("B4", 0.5, 52), ("C5", 2.0, 62), ("A4", 4.0, 58),
|
||||
]:
|
||||
oboe.add(note, dur, velocity=vel)
|
||||
for _ in range(10):
|
||||
flute.rest(Duration.WHOLE)
|
||||
for note, dur, vel in [
|
||||
("A5", 2.0, 50), ("G5", 1.0, 45), ("E5", 1.0, 48),
|
||||
("D5", 2.0, 50), ("C5", 1.0, 45), ("A4", 1.0, 48),
|
||||
("G4", 2.0, 50), ("A4", 2.0, 52),
|
||||
]:
|
||||
flute.add(note, dur, velocity=vel)
|
||||
|
||||
# ── Bars 15-20: Sitar + tabla ──
|
||||
sitar = score.part("sitar", instrument="sitar", volume=0.2,
|
||||
reverb=0.35, reverb_type=REV)
|
||||
for _ in range(14):
|
||||
sitar.rest(Duration.WHOLE)
|
||||
for note, dur, vel in [
|
||||
("A4", 1.0, 70), ("Bb4", 0.5, 60), ("A4", 0.5, 65),
|
||||
("C5", 1.5, 75), ("Bb4", 0.5, 60),
|
||||
("D5", 1.0, 70), ("E5", 1.5, 78),
|
||||
("F5", 0.5, 62), ("E5", 1.0, 70),
|
||||
("D5", 0.5, 62), ("C5", 0.5, 65), ("Bb4", 0.5, 58),
|
||||
("A4", 2.0, 75),
|
||||
("Bb4", 0.25, 55), ("C5", 0.25, 58), ("D5", 0.25, 62),
|
||||
("E5", 0.25, 68),
|
||||
("F5", 0.25, 65), ("G5", 0.25, 70), ("A5", 0.5, 80),
|
||||
("G5", 0.25, 62), ("F5", 0.25, 58), ("E5", 0.5, 62),
|
||||
("C5", 0.5, 58), ("Bb4", 0.5, 55), ("A4", 2.0, 75),
|
||||
]:
|
||||
sitar.add(note, dur, velocity=vel)
|
||||
|
||||
# ── EDM section — sitar over house beat ──
|
||||
# Solo sections: 8+8+8+12 = 36 beats = 9 bars
|
||||
# Total bars before EDM: 8 piano + 6 harp + 6 djembe + 4 tabla + 9 solo = 33
|
||||
edm_start = 33
|
||||
pad = score.part("pad", instrument="synth_pad", volume=0.18,
|
||||
reverb=0.45, reverb_type=REV,
|
||||
sidechain=0.6, sidechain_release=0.15)
|
||||
for _ in range(edm_start):
|
||||
pad.rest(Duration.WHOLE)
|
||||
for sym in ["Am", "F", "G", "Em"] * 2:
|
||||
pad.add(Chord.from_symbol(sym), Duration.WHOLE)
|
||||
|
||||
sub = score.part("sub", instrument="808_bass", volume=0.4)
|
||||
for _ in range(edm_start):
|
||||
sub.rest(Duration.WHOLE)
|
||||
for n in ["A1","A1","F1","F1","G1","G1","E1","E1"] * 2:
|
||||
sub.add(n, Duration.HALF)
|
||||
|
||||
sitar2 = score.part("sitar2", instrument="sitar", volume=0.4,
|
||||
reverb=0.3, reverb_type=REV)
|
||||
for _ in range(edm_start + 2):
|
||||
sitar2.rest(Duration.WHOLE)
|
||||
for note, dur, vel in [
|
||||
("A4", 0.25, 75), ("C5", 0.25, 78), ("E5", 0.5, 85),
|
||||
("D5", 0.25, 72), ("C5", 0.25, 70), ("A4", 0.5, 75),
|
||||
("G4", 0.25, 68), ("A4", 0.25, 72), ("C5", 0.5, 78),
|
||||
("A4", 0.5, 72),
|
||||
("E5", 0.5, 82), ("D5", 0.25, 72), ("C5", 0.25, 70),
|
||||
("A4", 0.5, 75), ("G4", 0.5, 68), ("A4", 1.0, 78),
|
||||
] * 2:
|
||||
sitar2.add(note, dur, velocity=vel)
|
||||
|
||||
# Drums: djembe bars 9-14, tabla bars 15-20, house bars 21-28
|
||||
DJB = DrumSound.DJEMBE_BASS
|
||||
DJT = DrumSound.DJEMBE_TONE
|
||||
DJS = DrumSound.DJEMBE_SLAP
|
||||
NA = DrumSound.TABLA_NA
|
||||
DH = DrumSound.TABLA_DHA
|
||||
TT = DrumSound.TABLA_TIT
|
||||
GB = DrumSound.TABLA_GE_BEND
|
||||
|
||||
silence = Pattern(name="s", time_signature="4/4", beats=32.0, hits=[])
|
||||
score.add_pattern(silence, repeats=1)
|
||||
p_dj = Pattern(name="dj", time_signature="4/4", beats=8.0, hits=[
|
||||
_Hit(DJB, 0.0, 48), _Hit(DJT, 1.0, 40), _Hit(DJT, 1.5, 35),
|
||||
_Hit(DJS, 2.0, 45), _Hit(DJT, 3.0, 40),
|
||||
_Hit(DJB, 4.0, 52), _Hit(DJT, 5.0, 42), _Hit(DJT, 5.5, 38),
|
||||
_Hit(DJS, 6.0, 48), _Hit(DJT, 6.5, 35), _Hit(DJS, 7.0, 45),
|
||||
])
|
||||
score.add_pattern(p_dj, repeats=3)
|
||||
p_tab = Pattern(name="tab", time_signature="4/4", beats=8.0, hits=[
|
||||
_Hit(DH, 0.0, 80), _Hit(TT, 0.5, 30), _Hit(NA, 1.0, 65),
|
||||
_Hit(NA, 2.0, 60), _Hit(DH, 3.0, 80),
|
||||
_Hit(DH, 4.0, 85), _Hit(TT, 4.25, 32), _Hit(TT, 4.5, 35),
|
||||
_Hit(NA, 5.0, 68), _Hit(TT, 5.5, 30), _Hit(NA, 6.0, 65),
|
||||
_Hit(DH, 7.0, 85),
|
||||
])
|
||||
score.add_pattern(p_tab, repeats=2)
|
||||
|
||||
# Tabla solo — everything drops out, just tabla and drone
|
||||
KE = DrumSound.TABLA_KE
|
||||
TI = DrumSound.TABLA_TIN
|
||||
GE = DrumSound.TABLA_GE
|
||||
T3 = 1.0 / 12.0 # 32nd triplet
|
||||
T9 = 1.0 / 9.0 # ninth note
|
||||
|
||||
# Part 1: whisper — space, breath, single hits
|
||||
p_solo1 = Pattern(name="solo1", time_signature="4/4", beats=8.0, hits=[
|
||||
_Hit(DH, 0.0, 78),
|
||||
_Hit(NA, 2.0, 55),
|
||||
_Hit(DH, 4.0, 82),
|
||||
_Hit(TT, 5.0, 30), _Hit(NA, 5.5, 52), _Hit(TT, 6.0, 28),
|
||||
_Hit(DH, 7.0, 78),
|
||||
])
|
||||
score.add_pattern(p_solo1, repeats=1)
|
||||
|
||||
# Part 2: ghosts emerge — 16th note ghost fills between accents
|
||||
p_solo2 = Pattern(name="solo2", time_signature="4/4", beats=8.0, hits=[
|
||||
_Hit(DH, 0.0, 92), _Hit(TT, 0.25, 32), _Hit(TT, 0.5, 35),
|
||||
_Hit(NA, 1.0, 68), _Hit(TT, 1.25, 30), _Hit(TT, 1.5, 28),
|
||||
_Hit(NA, 2.0, 62), _Hit(TT, 2.5, 32), _Hit(DH, 3.0, 88),
|
||||
_Hit(TT, 3.25, 35), _Hit(TT, 3.5, 38),
|
||||
_Hit(DH, 4.0, 95), _Hit(TT, 4.25, 38), _Hit(TT, 4.5, 42),
|
||||
_Hit(NA, 5.0, 72), _Hit(TT, 5.25, 32), _Hit(TT, 5.5, 35),
|
||||
_Hit(KE, 5.75, 40),
|
||||
_Hit(NA, 6.0, 68), _Hit(TT, 6.25, 35), _Hit(KE, 6.5, 38),
|
||||
_Hit(DH, 7.0, 95), _Hit(GB, 7.5, 88),
|
||||
])
|
||||
score.add_pattern(p_solo2, repeats=1)
|
||||
|
||||
# Part 3: call and response — dayan vs bayan, dynamics wide open
|
||||
p_solo3 = Pattern(name="solo3", time_signature="4/4", beats=8.0, hits=[
|
||||
# Dayan speaks
|
||||
_Hit(NA, 0.0, 110), _Hit(NA, 0.25, 55), _Hit(TT, 0.5, 38),
|
||||
_Hit(NA, 0.75, 100),
|
||||
# Bayan answers
|
||||
_Hit(GE, 1.0, 100), _Hit(GE, 1.25, 50), _Hit(GB, 1.5, 90),
|
||||
_Hit(GE, 1.75, 45),
|
||||
# Dayan louder
|
||||
_Hit(NA, 2.0, 115), _Hit(TT, 2.125, 30), _Hit(TT, 2.25, 35),
|
||||
_Hit(NA, 2.5, 105), _Hit(TT, 2.625, 32), _Hit(TT, 2.75, 38),
|
||||
# Bayan louder
|
||||
_Hit(GB, 3.0, 112), _Hit(KE, 3.25, 50), _Hit(GE, 3.5, 68),
|
||||
_Hit(KE, 3.75, 45),
|
||||
# Together — explosion
|
||||
_Hit(DH, 4.0, 120), _Hit(TT, 4.25, 45), _Hit(TT, 4.5, 48),
|
||||
_Hit(DH, 5.0, 115), _Hit(TT, 5.25, 42), _Hit(NA, 5.5, 72),
|
||||
# 9-tuplet — the weird one, breaks the grid
|
||||
*[_Hit(TT if i % 2 == 0 else KE, 6.0 + i * T9, 38 + i * 5)
|
||||
for i in range(9)],
|
||||
_Hit(DH, 7.0, 118),
|
||||
])
|
||||
score.add_pattern(p_solo3, repeats=1)
|
||||
|
||||
# Part 4: blazing — 32nd triplets, cascades, tihai finale
|
||||
p_solo4 = Pattern(name="solo4", time_signature="4/4", beats=12.0, hits=[
|
||||
# 32nd triplet opening flourish
|
||||
*[_Hit(TT, 0.0 + i * T3, 40 + i * 2) for i in range(12)],
|
||||
_Hit(DH, 1.0, 120), _Hit(GB, 1.5, 108),
|
||||
# Rapid alternating hands
|
||||
_Hit(NA, 2.0, 110), _Hit(KE, 2.125, 45), _Hit(NA, 2.25, 105),
|
||||
_Hit(KE, 2.375, 48), _Hit(NA, 2.5, 108), _Hit(KE, 2.625, 50),
|
||||
_Hit(NA, 2.75, 112),
|
||||
_Hit(DH, 3.0, 118),
|
||||
# Another 32nd triplet burst — longer, crescendo
|
||||
*[_Hit(TT, 3.5 + i * T3, 32 + i * 4) for i in range(18)],
|
||||
_Hit(DH, 5.0, 122), _Hit(DH, 5.25, 118), _Hit(GB, 5.5, 115),
|
||||
# 9 against 4 polyrhythm moment
|
||||
_Hit(GE, 6.0, 88), _Hit(GE, 7.0, 85),
|
||||
*[_Hit(NA if i % 3 == 0 else TT, 6.0 + i * (2.0 / 9.0), 42 + (i % 3) * 15)
|
||||
for i in range(9)],
|
||||
# Grand tihai — 3x pattern, each louder
|
||||
_Hit(DH, 8.0, 108), _Hit(NA, 8.25, 72), _Hit(TT, 8.5, 48),
|
||||
_Hit(KE, 8.75, 52), _Hit(DH, 9.0, 100),
|
||||
_Hit(DH, 9.25, 112), _Hit(NA, 9.5, 78), _Hit(TT, 9.75, 52),
|
||||
_Hit(KE, 10.0, 58), _Hit(DH, 10.25, 108),
|
||||
_Hit(DH, 10.5, 120), _Hit(NA, 10.75, 85), _Hit(TT, 11.0, 58),
|
||||
_Hit(KE, 11.25, 62), _Hit(DH, 11.5, 127),
|
||||
# Silence.....
|
||||
# SLAM
|
||||
_Hit(GB, 11.875, 127),
|
||||
])
|
||||
score.add_pattern(p_solo4, repeats=1)
|
||||
|
||||
score.drums("house", repeats=8)
|
||||
score.set_drum_effects(reverb=0.3, reverb_type=REV)
|
||||
|
||||
play_song(score, "Journey — Piano → World → Sitar EDM (Taj Mahal)")
|
||||
|
||||
|
||||
SONGS = {
|
||||
"1": ("Bossa Nova in A minor", bossa_nova_girl),
|
||||
"2": ("Bebop in Bb major", bebop_in_bb),
|
||||
@@ -1258,6 +1574,8 @@ SONGS = {
|
||||
"20": ("Temple Bell (Japanese)", temple_bell),
|
||||
"21": ("Cinematic Showcase (Orchestral)", cinematic_showcase),
|
||||
"22": ("Greensleeves (Renaissance Lute)", greensleeves),
|
||||
"23": ("Tabla Solo (Raga Yaman)", tabla_solo_yaman),
|
||||
"24": ("Journey (Western → World → Indian)", journey),
|
||||
}
|
||||
|
||||
if __name__ == "__main__":
|
||||
@@ -1271,7 +1589,7 @@ if __name__ == "__main__":
|
||||
print(f" {key:>2}. {name}")
|
||||
|
||||
print()
|
||||
choice = input(" Pick a song (1-22, or 'all'): ").strip()
|
||||
choice = input(" Pick a song (1-24, or 'all'): ").strip()
|
||||
print()
|
||||
|
||||
if choice == "all":
|
||||
|
||||
+1
-2
@@ -1,6 +1,6 @@
|
||||
[project]
|
||||
name = "pytheory"
|
||||
version = "0.34.0"
|
||||
version = "0.35.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.34.0"
|
||||
__version__ = "0.35.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
|
||||
|
||||
+467
-10
@@ -2,11 +2,24 @@ from enum import Enum
|
||||
import time
|
||||
|
||||
import numpy
|
||||
import scipy.signal
|
||||
|
||||
from .tones import Tone
|
||||
|
||||
|
||||
class _LazyModule:
|
||||
"""Lazy import wrapper — module loaded on first attribute access."""
|
||||
def __init__(self, name):
|
||||
self._name = name
|
||||
self._mod = None
|
||||
def __getattr__(self, attr):
|
||||
if self._mod is None:
|
||||
import importlib
|
||||
self._mod = importlib.import_module(self._name)
|
||||
return getattr(self._mod, attr)
|
||||
|
||||
scipy = type('scipy', (), {'signal': _LazyModule('scipy.signal')})()
|
||||
|
||||
|
||||
def _get_sd():
|
||||
"""Lazy import sounddevice — only needed for actual audio playback."""
|
||||
import sounddevice as sd
|
||||
@@ -257,7 +270,7 @@ def strings_wave(hz, peak=SAMPLE_PEAK, n_samples=SAMPLE_RATE):
|
||||
|
||||
# Delayed vibrato: ramps in over ~200ms, like a real bow
|
||||
vib_rate = 5.2 + rng.uniform(-0.3, 0.3) # slight randomness per note
|
||||
vib_depth = hz * 0.003 # ~5 cents
|
||||
vib_depth = hz * 0.001 # subtle
|
||||
vib_onset = numpy.clip(t / 0.2, 0.0, 1.0) # ramp over 200ms
|
||||
vibrato = vib_depth * vib_onset * numpy.sin(2 * numpy.pi * vib_rate * t)
|
||||
|
||||
@@ -442,7 +455,7 @@ def flute_wave(hz, peak=SAMPLE_PEAK, n_samples=SAMPLE_RATE):
|
||||
|
||||
# Vibrato — develops after ~200ms
|
||||
vib_onset = numpy.clip(t / 0.2, 0.0, 1.0)
|
||||
vib = hz * 0.003 * vib_onset * numpy.sin(2 * numpy.pi * 5.0 * t)
|
||||
vib = hz * 0.0008 * vib_onset * numpy.sin(2 * numpy.pi * 5.0 * t)
|
||||
|
||||
# Tube resonance — mostly fundamental + weak odd harmonics
|
||||
wave = numpy.sin(2 * numpy.pi * (hz + vib) * t) * 0.7
|
||||
@@ -481,7 +494,7 @@ def trumpet_wave(hz, peak=SAMPLE_PEAK, n_samples=SAMPLE_RATE):
|
||||
|
||||
# Vibrato
|
||||
vib_onset = numpy.clip(t / 0.15, 0.0, 1.0)
|
||||
vib = hz * 0.004 * vib_onset * numpy.sin(2 * numpy.pi * 5.5 * t)
|
||||
vib = hz * 0.001 * vib_onset * numpy.sin(2 * numpy.pi * 5.5 * t)
|
||||
|
||||
# Lip buzz — additive with brass spectral shape
|
||||
# Trumpet has strong even AND odd harmonics (unlike clarinet)
|
||||
@@ -523,7 +536,7 @@ def clarinet_wave(hz, peak=SAMPLE_PEAK, n_samples=SAMPLE_RATE):
|
||||
rng = numpy.random.default_rng(int(hz * 100) % 2**31)
|
||||
|
||||
vib_onset = numpy.clip(t / 0.3, 0.0, 1.0)
|
||||
vib = hz * 0.002 * vib_onset * numpy.sin(2 * numpy.pi * 4.5 * t)
|
||||
vib = hz * 0.001 * vib_onset * numpy.sin(2 * numpy.pi * 4.5 * t)
|
||||
|
||||
# Cylindrical bore: odd harmonics dominate
|
||||
wave = numpy.zeros(n_samples, dtype=numpy.float64)
|
||||
@@ -592,7 +605,7 @@ def oboe_wave(hz, peak=SAMPLE_PEAK, n_samples=SAMPLE_RATE):
|
||||
rng = numpy.random.default_rng(int(hz * 100) % 2**31)
|
||||
|
||||
vib_onset = numpy.clip(t / 0.2, 0.0, 1.0)
|
||||
vib = hz * 0.004 * vib_onset * numpy.sin(2 * numpy.pi * 5.0 * t)
|
||||
vib = hz * 0.001 * vib_onset * numpy.sin(2 * numpy.pi * 5.0 * t)
|
||||
|
||||
wave = numpy.zeros(n_samples, dtype=numpy.float64)
|
||||
n_harmonics = min(18, int((SAMPLE_RATE / 2) / hz))
|
||||
@@ -668,7 +681,7 @@ def cello_wave(hz, peak=SAMPLE_PEAK, n_samples=SAMPLE_RATE):
|
||||
|
||||
# Delayed vibrato
|
||||
vib_rate = 5.0 + rng.uniform(-0.3, 0.3)
|
||||
vib_depth = hz * 0.004
|
||||
vib_depth = hz * 0.001
|
||||
vib_onset = numpy.clip(t / 0.25, 0.0, 1.0)
|
||||
vibrato = vib_depth * vib_onset * numpy.sin(2 * numpy.pi * vib_rate * t)
|
||||
|
||||
@@ -787,6 +800,336 @@ def upright_bass_wave(hz, peak=SAMPLE_PEAK, n_samples=SAMPLE_RATE):
|
||||
return (peak * out).astype(numpy.int16)
|
||||
|
||||
|
||||
def timpani_wave(hz, peak=SAMPLE_PEAK, n_samples=SAMPLE_RATE):
|
||||
"""Timpani — large kettle drum with definite pitch.
|
||||
|
||||
The copper kettle creates a tuned resonance with inharmonic
|
||||
overtones. The head modes are at ratios 1.0, 1.5, 1.99, 2.44
|
||||
(not integer multiples like strings). The felt mallet gives a
|
||||
soft attack with a deep, booming body.
|
||||
"""
|
||||
t = numpy.arange(n_samples, dtype=numpy.float64) / SAMPLE_RATE
|
||||
|
||||
# Timpani head modes — inharmonic but definite pitch
|
||||
# Mode ratios from vibrating circular membrane physics
|
||||
wave = numpy.sin(2 * numpy.pi * hz * t) * 0.8
|
||||
wave += numpy.sin(2 * numpy.pi * hz * 1.5 * t) * 0.35 * numpy.exp(-6 * t)
|
||||
wave += numpy.sin(2 * numpy.pi * hz * 1.99 * t) * 0.2 * numpy.exp(-10 * t)
|
||||
wave += numpy.sin(2 * numpy.pi * hz * 2.44 * t) * 0.1 * numpy.exp(-15 * t)
|
||||
|
||||
# Two-stage decay: initial thump fades fast, fundamental rings
|
||||
decay = numpy.where(t < 0.15,
|
||||
numpy.exp(-4 * t),
|
||||
numpy.exp(-4 * 0.15) * numpy.exp(-1.5 * (t - 0.15)))
|
||||
wave *= decay
|
||||
|
||||
# Felt mallet impact — warm, not sharp
|
||||
mallet_len = min(int(SAMPLE_RATE * 0.02), n_samples)
|
||||
rng = numpy.random.default_rng(int(hz * 100) % 2**31)
|
||||
mallet = rng.uniform(-0.3, 0.3, mallet_len)
|
||||
mallet *= numpy.exp(-numpy.linspace(0, 8, mallet_len))
|
||||
wave[:mallet_len] += mallet
|
||||
|
||||
# Copper kettle resonance — boosts low-mids
|
||||
import scipy.signal as _sig
|
||||
lo, hi = max(20, int(hz * 0.7)), min(SAMPLE_RATE // 2 - 1, int(hz * 2))
|
||||
if lo < hi:
|
||||
bp, ap = _sig.butter(2, [lo, hi], btype='band', fs=SAMPLE_RATE)
|
||||
kettle = _sig.lfilter(bp, ap, wave) * 0.3
|
||||
wave += kettle
|
||||
|
||||
mx = numpy.abs(wave).max()
|
||||
if mx > 0:
|
||||
wave /= mx
|
||||
|
||||
return (peak * wave).astype(numpy.int16)
|
||||
|
||||
|
||||
def saxophone_wave(hz, peak=SAMPLE_PEAK, n_samples=SAMPLE_RATE):
|
||||
"""Saxophone — single reed through a conical brass bore.
|
||||
|
||||
The conical bore produces all harmonics (like oboe), but the
|
||||
brass body and larger mouthpiece give a warmer, fatter, more
|
||||
vocal quality. The reed adds a slight buzz. Saxophone is
|
||||
between clarinet (odd harmonics) and oboe (nasal even+odd) —
|
||||
it has everything, with a strong fundamental and rich mids.
|
||||
"""
|
||||
t = numpy.arange(n_samples, dtype=numpy.float64) / SAMPLE_RATE
|
||||
rng = numpy.random.default_rng(int(hz * 100) % 2**31)
|
||||
|
||||
# Vibrato — develops after ~250ms, wider than flute
|
||||
vib_onset = numpy.clip(t / 0.25, 0.0, 1.0)
|
||||
vib = hz * 0.0012 * vib_onset * numpy.sin(2 * numpy.pi * 5.2 * t)
|
||||
|
||||
wave = numpy.zeros(n_samples, dtype=numpy.float64)
|
||||
n_harmonics = min(20, int((SAMPLE_RATE / 2) / hz))
|
||||
|
||||
for n in range(1, n_harmonics + 1):
|
||||
f_n = hz * n
|
||||
if f_n >= SAMPLE_RATE / 2:
|
||||
break
|
||||
# Sax spectral shape: strong fundamental, broad mid peak (3-6),
|
||||
# slower rolloff than oboe (brass body carries harmonics further)
|
||||
if n == 1:
|
||||
amp = 1.0
|
||||
elif n <= 3:
|
||||
amp = 0.6
|
||||
elif n <= 6:
|
||||
amp = 0.4 * numpy.exp(-0.1 * (n - 4) ** 2)
|
||||
else:
|
||||
amp = 0.2 / n
|
||||
phase = rng.uniform(0, 2 * numpy.pi)
|
||||
wave += amp * numpy.sin(2 * numpy.pi * (f_n + vib * n) * t + phase)
|
||||
|
||||
# Reed buzz — more present than oboe but still warm
|
||||
reed = rng.normal(0, 0.07, n_samples)
|
||||
# Bandpass the reed noise around 1-3kHz (the "honk" range)
|
||||
import scipy.signal as _sig
|
||||
reed_lo = max(20, int(hz * 2))
|
||||
reed_hi = min(SAMPLE_RATE // 2 - 1, int(hz * 6))
|
||||
if reed_lo < reed_hi:
|
||||
br, ar = _sig.butter(2, [reed_lo, reed_hi], btype='band', fs=SAMPLE_RATE)
|
||||
reed = _sig.lfilter(br, ar, reed).astype(numpy.float64) * 2.0
|
||||
wave += reed
|
||||
|
||||
# Brass body warmth — low-mid boost
|
||||
center = min(1500, hz * 4)
|
||||
bw = 500
|
||||
lo = max(20, int(center - bw))
|
||||
hi = min(SAMPLE_RATE // 2 - 1, int(center + bw))
|
||||
if lo < hi:
|
||||
bp, ap = _sig.butter(2, [lo, hi], btype='band', fs=SAMPLE_RATE)
|
||||
body = _sig.lfilter(bp, ap, wave) * 0.2
|
||||
wave += body
|
||||
|
||||
mx = numpy.abs(wave).max()
|
||||
if mx > 0:
|
||||
wave /= mx
|
||||
|
||||
return (peak * wave).astype(numpy.int16)
|
||||
|
||||
|
||||
def vocal_wave(hz, peak=SAMPLE_PEAK, n_samples=SAMPLE_RATE, lyric="ah"):
|
||||
"""Vocal/formant synthesis — sings vowel sounds at a given pitch.
|
||||
|
||||
Models the human voice with:
|
||||
1. LF glottal model — asymmetric pulse with sharp closure (not just sines)
|
||||
2. 5 parallel resonant formant filters (real voice has 5 formant peaks)
|
||||
3. Jitter + shimmer (natural pitch/amplitude irregularity)
|
||||
4. Aspiration noise mixed with the glottal source
|
||||
5. Consonant onsets (plosives, sibilants, nasals, etc.)
|
||||
"""
|
||||
import scipy.signal as _sig
|
||||
|
||||
# 5-formant table: (F1, F2, F3, F4, F5) frequencies and bandwidths
|
||||
# Based on Peterson & Barney (1952) measurements, male voice
|
||||
FORMANTS = {
|
||||
'a': [(800, 130), (1200, 100), (2500, 140), (3300, 250), (3750, 300)],
|
||||
'e': [(530, 80), (1850, 100), (2500, 130), (3300, 250), (3750, 300)],
|
||||
'i': [(280, 60), (2250, 100), (2900, 120), (3350, 250), (3750, 300)],
|
||||
'o': [(500, 100), (1000, 80), (2500, 140), (3300, 250), (3750, 300)],
|
||||
'u': ((325, 70), (700, 60), (2530, 140), (3300, 250), (3750, 300)),
|
||||
}
|
||||
# Formant gains (relative amplitude per formant)
|
||||
FGAINS = [1.0, 0.8, 0.5, 0.25, 0.15]
|
||||
|
||||
rng = numpy.random.default_rng(int(hz * 100 + len(lyric) * 7) % 2**31)
|
||||
t = numpy.arange(n_samples, dtype=numpy.float64) / SAMPLE_RATE
|
||||
|
||||
# Parse vowels from lyric
|
||||
vowels_in_lyric = [c.lower() for c in lyric if c.lower() in FORMANTS]
|
||||
if not vowels_in_lyric:
|
||||
vowels_in_lyric = ['a']
|
||||
|
||||
# ── Glottal source: LF model approximation ──
|
||||
# Asymmetric pulse: slow open phase, sharp closure, then closed phase.
|
||||
# Much more "voice-like" than a sine or sawtooth.
|
||||
# Jitter (pitch irregularity) + shimmer (amplitude irregularity)
|
||||
jitter = rng.normal(0, hz * 0.001, n_samples) # ~0.1% pitch jitter
|
||||
shimmer = 1.0 + rng.normal(0, 0.008, n_samples) # ~0.8% amp shimmer
|
||||
# Vibrato
|
||||
vib = hz * 0.001 * numpy.sin(2 * numpy.pi * 5.5 * t)
|
||||
inst_freq = hz + vib + jitter
|
||||
phase = numpy.cumsum(2 * numpy.pi * inst_freq / SAMPLE_RATE)
|
||||
# LF glottal shape: sharper falling edge via phase shaping
|
||||
saw = (phase / (2 * numpy.pi)) % 1.0 # 0 to 1 sawtooth
|
||||
# Asymmetric: slow rise (60%), fast fall (40%)
|
||||
glottal = numpy.where(saw < 0.6,
|
||||
numpy.sin(numpy.pi * saw / 0.6), # smooth rise
|
||||
-numpy.sin(numpy.pi * (saw - 0.6) / 0.4) * 0.8) # sharp fall
|
||||
glottal *= shimmer
|
||||
|
||||
# Aspiration noise (breathiness) — subtle
|
||||
breath = rng.normal(0, 0.04, n_samples)
|
||||
source = glottal * 0.92 + breath * 0.08
|
||||
|
||||
# ── Formant filtering ──
|
||||
n_vowels = len(vowels_in_lyric)
|
||||
out = numpy.zeros(n_samples, dtype=numpy.float64)
|
||||
|
||||
if n_vowels == 1:
|
||||
# Single vowel — filter the whole thing
|
||||
formants = FORMANTS[vowels_in_lyric[0]]
|
||||
for (fc, bw), gain in zip(formants, FGAINS):
|
||||
lo = max(20, fc - bw)
|
||||
hi = min(SAMPLE_RATE // 2 - 1, fc + bw)
|
||||
if lo < hi:
|
||||
bp, ap = _sig.butter(2, [lo, hi], btype='band', fs=SAMPLE_RATE)
|
||||
out += _sig.lfilter(bp, ap, source).astype(numpy.float64) * gain
|
||||
else:
|
||||
# Multiple vowels — crossfade formants
|
||||
samples_per_vowel = n_samples // n_vowels
|
||||
for vi, vowel in enumerate(vowels_in_lyric):
|
||||
formants = FORMANTS[vowel]
|
||||
start = vi * samples_per_vowel
|
||||
end = n_samples if vi == n_vowels - 1 else start + samples_per_vowel
|
||||
seg = source[start:end].copy()
|
||||
seg_out = numpy.zeros_like(seg)
|
||||
for (fc, bw), gain in zip(formants, FGAINS):
|
||||
lo = max(20, fc - bw)
|
||||
hi = min(SAMPLE_RATE // 2 - 1, fc + bw)
|
||||
if lo < hi:
|
||||
bp, ap = _sig.butter(2, [lo, hi], btype='band', fs=SAMPLE_RATE)
|
||||
seg_out += _sig.lfilter(bp, ap, seg).astype(numpy.float64) * gain
|
||||
# Crossfade
|
||||
fade = min(int(SAMPLE_RATE * 0.02), len(seg_out) // 4)
|
||||
if vi > 0 and fade > 0:
|
||||
seg_out[:fade] *= numpy.linspace(0, 1, fade)
|
||||
if vi < n_vowels - 1 and fade > 0:
|
||||
seg_out[-fade:] *= numpy.linspace(1, 0, fade)
|
||||
out[start:end] += seg_out[:end - start]
|
||||
|
||||
# ── Consonant onsets ──
|
||||
lyric_lower = lyric.lower()
|
||||
if lyric_lower and lyric_lower[0] not in 'aeiou':
|
||||
c = lyric_lower[0]
|
||||
cl = min(int(SAMPLE_RATE * 0.035), n_samples)
|
||||
if c in 'tdkpb':
|
||||
burst = rng.uniform(-0.5, 0.5, cl) * numpy.exp(-numpy.linspace(0, 18, cl))
|
||||
out[:cl] = burst + out[:cl] * 0.2
|
||||
elif c in 'sz':
|
||||
sib = rng.uniform(-0.4, 0.4, cl)
|
||||
if cl > 20:
|
||||
bl, al = _sig.butter(2, [3000, min(8000, SAMPLE_RATE//2-1)], btype='band', fs=SAMPLE_RATE)
|
||||
sib = _sig.lfilter(bl, al, numpy.pad(sib, (0, max(0, n_samples-cl))))[:cl]
|
||||
sib *= numpy.exp(-numpy.linspace(0, 10, cl))
|
||||
out[:cl] = sib * 0.6 + out[:cl] * 0.4
|
||||
elif c in 'mn':
|
||||
nl = min(int(SAMPLE_RATE * 0.06), n_samples)
|
||||
nasal = numpy.sin(2*numpy.pi*250*t[:nl]) * 0.4 * numpy.exp(-numpy.linspace(0, 4, nl))
|
||||
out[:nl] = nasal + out[:nl] * 0.4
|
||||
elif c in 'fv':
|
||||
fric = rng.uniform(-0.25, 0.25, cl) * numpy.exp(-numpy.linspace(0, 12, cl))
|
||||
out[:cl] = fric * 0.5 + out[:cl] * 0.5
|
||||
elif c in 'lr':
|
||||
gl = min(int(SAMPLE_RATE * 0.05), n_samples)
|
||||
ghz = hz * 0.7 + hz * 0.3 * numpy.linspace(0, 1, gl)
|
||||
glide = numpy.sin(numpy.cumsum(2*numpy.pi*ghz/SAMPLE_RATE)) * 0.35
|
||||
out[:gl] = glide + out[:gl] * 0.65
|
||||
elif c == 'h':
|
||||
hl = min(int(SAMPLE_RATE * 0.05), n_samples)
|
||||
asp = rng.uniform(-0.4, 0.4, hl) * numpy.exp(-numpy.linspace(0, 5, hl))
|
||||
out[:hl] = asp * 0.6 + out[:hl] * 0.4
|
||||
elif c == 'w':
|
||||
wl = min(int(SAMPLE_RATE * 0.06), n_samples)
|
||||
ws = numpy.sin(numpy.cumsum(2*numpy.pi*hz/SAMPLE_RATE*numpy.ones(wl)))
|
||||
if wl > 20:
|
||||
bp, ap = _sig.butter(2, [max(20,300), min(800, SAMPLE_RATE//2-1)], btype='band', fs=SAMPLE_RATE)
|
||||
ws = _sig.lfilter(bp, ap, ws)
|
||||
ws *= numpy.linspace(0.5, 0, wl)
|
||||
out[:wl] = ws * 0.4 + out[:wl] * 0.6
|
||||
|
||||
# Soft edges — prevent clicks at note boundaries
|
||||
fade_samples = min(int(SAMPLE_RATE * 0.01), n_samples // 4)
|
||||
if fade_samples > 0:
|
||||
out[:fade_samples] *= numpy.linspace(0, 1, fade_samples)
|
||||
out[-fade_samples:] *= numpy.linspace(1, 0, fade_samples)
|
||||
|
||||
mx = numpy.abs(out).max()
|
||||
if mx > 0:
|
||||
out /= mx
|
||||
|
||||
return (peak * out).astype(numpy.int16)
|
||||
|
||||
|
||||
def granular_wave(hz, peak=SAMPLE_PEAK, n_samples=SAMPLE_RATE,
|
||||
grain_size=0.04, density=50, scatter=0.5,
|
||||
pitch_var=12, source="saw"):
|
||||
"""Granular synthesis — clouds of tiny sound grains.
|
||||
|
||||
Chops a source waveform into overlapping micro-grains (10-200ms),
|
||||
each independently windowed and optionally pitch/time scattered.
|
||||
Creates textures impossible with other synthesis: frozen tones,
|
||||
shimmering clouds, evolving pads, glitchy stutters.
|
||||
|
||||
Args:
|
||||
hz: Base frequency.
|
||||
grain_size: Duration of each grain in seconds (default 0.05 = 50ms).
|
||||
density: Grains per second (default 20). Higher = denser cloud.
|
||||
scatter: Random position jitter 0-1 (default 0.3). How much each
|
||||
grain's read position varies from sequential order.
|
||||
pitch_var: Random pitch variation per grain in cents (default 5).
|
||||
source: Base waveform — ``"saw"``, ``"sine"``, ``"triangle"``,
|
||||
``"square"``, ``"noise"`` (default ``"saw"``).
|
||||
"""
|
||||
rng = numpy.random.default_rng(int(hz * 100) % 2**31)
|
||||
|
||||
# Generate source material — longer than needed for scatter headroom
|
||||
src_len = n_samples + int(SAMPLE_RATE * scatter * 2)
|
||||
src_fns = {
|
||||
"saw": sawtooth_wave, "sine": sine_wave, "triangle": triangle_wave,
|
||||
"square": square_wave, "noise": noise_wave,
|
||||
}
|
||||
src_fn = src_fns.get(source, sawtooth_wave)
|
||||
src = src_fn(hz, n_samples=src_len).astype(numpy.float64) / SAMPLE_PEAK
|
||||
|
||||
# Grain parameters
|
||||
grain_samples = max(64, int(grain_size * SAMPLE_RATE))
|
||||
n_grains = max(1, int(n_samples / SAMPLE_RATE * density))
|
||||
|
||||
# Hanning window for each grain (smooth fade in/out, no clicks)
|
||||
window = numpy.hanning(grain_samples).astype(numpy.float64)
|
||||
|
||||
out = numpy.zeros(n_samples, dtype=numpy.float64)
|
||||
|
||||
for i in range(n_grains):
|
||||
# Output position — evenly spaced with jitter
|
||||
base_pos = int(i * n_samples / n_grains)
|
||||
jitter = int(rng.uniform(-0.5, 0.5) * n_samples / n_grains * 0.3)
|
||||
out_pos = max(0, min(n_samples - grain_samples, base_pos + jitter))
|
||||
|
||||
# Source read position — sequential with scatter
|
||||
src_pos = int(base_pos * src_len / n_samples)
|
||||
src_jitter = int(rng.uniform(-scatter, scatter) * grain_samples * 4)
|
||||
src_pos = max(0, min(src_len - grain_samples, src_pos + src_jitter))
|
||||
|
||||
# Per-grain pitch variation via resampling
|
||||
if pitch_var > 0:
|
||||
cents = rng.uniform(-pitch_var, pitch_var)
|
||||
rate = 2 ** (cents / 1200)
|
||||
read_len = max(2, min(int(grain_samples * rate), src_len - src_pos))
|
||||
grain_src = src[src_pos:src_pos + read_len]
|
||||
x_old = numpy.linspace(0, 1, len(grain_src))
|
||||
x_new = numpy.linspace(0, 1, grain_samples)
|
||||
grain = numpy.interp(x_new, x_old, grain_src)
|
||||
else:
|
||||
end = min(src_pos + grain_samples, src_len)
|
||||
grain = src[src_pos:end]
|
||||
if len(grain) < grain_samples:
|
||||
grain = numpy.pad(grain, (0, grain_samples - len(grain)))
|
||||
|
||||
# Apply window and mix
|
||||
grain *= window[:len(grain)]
|
||||
end = min(out_pos + len(grain), n_samples)
|
||||
out[out_pos:end] += grain[:end - out_pos]
|
||||
|
||||
mx = numpy.abs(out).max()
|
||||
if mx > 0:
|
||||
out /= mx
|
||||
|
||||
return (peak * out).astype(numpy.int16)
|
||||
|
||||
|
||||
def acoustic_guitar_wave(hz, peak=SAMPLE_PEAK, n_samples=SAMPLE_RATE):
|
||||
"""Acoustic guitar — Karplus-Strong with wooden body resonance.
|
||||
|
||||
@@ -1087,6 +1430,10 @@ class Synth(Enum):
|
||||
CELLO = "cello_synth"
|
||||
HARP = "harp_synth"
|
||||
UPRIGHT_BASS = "upright_bass_synth"
|
||||
TIMPANI = "timpani_synth"
|
||||
SAXOPHONE = "saxophone_synth"
|
||||
GRANULAR = "granular_synth"
|
||||
VOCAL = "vocal_synth"
|
||||
ACOUSTIC_GUITAR = "acoustic_guitar_synth"
|
||||
SITAR = "sitar_synth"
|
||||
ELECTRIC_GUITAR = "electric_guitar_synth"
|
||||
@@ -1108,6 +1455,8 @@ _SYNTH_FUNCTIONS = {
|
||||
"marimba_synth": marimba_wave, "oboe_synth": oboe_wave,
|
||||
"harpsichord_synth": harpsichord_wave, "cello_synth": cello_wave,
|
||||
"harp_synth": harp_wave, "upright_bass_synth": upright_bass_wave,
|
||||
"timpani_synth": timpani_wave, "saxophone_synth": saxophone_wave,
|
||||
"granular_synth": granular_wave, "vocal_synth": vocal_wave,
|
||||
"acoustic_guitar_synth": acoustic_guitar_wave,
|
||||
"sitar_synth": sitar_wave, "electric_guitar_synth": electric_guitar_wave,
|
||||
}
|
||||
@@ -1925,6 +2274,42 @@ def _synth_metal_hat(n_samples):
|
||||
return out
|
||||
|
||||
|
||||
def _synth_tabla_ge_bend(n_samples):
|
||||
"""Tabla Ge with upward pitch bend — palm pressing into bayan head.
|
||||
|
||||
The player strikes the bayan and then presses their palm into the
|
||||
head, raising the pitch dramatically. The signature bayan sound
|
||||
in Bollywood and fusion music.
|
||||
"""
|
||||
t = numpy.arange(n_samples, dtype=numpy.float32) / SAMPLE_RATE
|
||||
# Membrane thud
|
||||
thump_len = min(int(SAMPLE_RATE * 0.07), n_samples)
|
||||
thump_raw = _noise(thump_len)
|
||||
if thump_len > 20:
|
||||
bl, al = scipy.signal.butter(2, [40, 250], btype='band', fs=SAMPLE_RATE)
|
||||
thump = scipy.signal.lfilter(bl, al, numpy.pad(thump_raw, (0, max(0, n_samples - thump_len))))[:thump_len]
|
||||
else:
|
||||
thump = thump_raw
|
||||
thump *= _exp_decay(thump_len, 20) * 0.8
|
||||
# Pitch sweep UP — 60 Hz rising to 200+ Hz as palm presses
|
||||
# Gets quieter as pitch rises (palm mutes the head as it presses)
|
||||
freq = 60 + 180 * (1 - numpy.exp(-4 * t))
|
||||
phase = 2 * numpy.pi * numpy.cumsum(freq) / SAMPLE_RATE
|
||||
body = numpy.sin(phase) * _exp_decay(n_samples, 6) * 0.9
|
||||
# Metal shell resonance
|
||||
metal_len = min(int(SAMPLE_RATE * 0.1), n_samples)
|
||||
metal = numpy.sin(2 * numpy.pi * 150 * t[:metal_len]) * _exp_decay(metal_len, 8) * 0.3
|
||||
# Sub
|
||||
sub = _sine_f32(50, n_samples) * _exp_decay(n_samples, 5) * 0.4
|
||||
click_len = min(250, n_samples)
|
||||
click = _noise(click_len) * _exp_decay(click_len, 35) * 0.3
|
||||
result = body + sub
|
||||
result[:thump_len] += thump
|
||||
result[:metal_len] += metal
|
||||
result[:click_len] += click
|
||||
return numpy.tanh(result * 1.3).astype(numpy.float32)
|
||||
|
||||
|
||||
def _synth_djembe_bass(n_samples):
|
||||
"""Djembe bass — open palm strike in center of goatskin head.
|
||||
|
||||
@@ -2077,6 +2462,7 @@ def _render_drum_hit(sound_value, n_samples):
|
||||
DrumSound.TABLA_DHA.value: lambda n: _synth_tabla_dha(n),
|
||||
DrumSound.TABLA_TIT.value: lambda n: _synth_tabla_tit(n),
|
||||
DrumSound.TABLA_KE.value: lambda n: _synth_tabla_ke(n),
|
||||
DrumSound.TABLA_GE_BEND.value: lambda n: _synth_tabla_ge_bend(n),
|
||||
# Dhol
|
||||
DrumSound.DHOL_DAGGA.value: lambda n: _synth_dhol_dagga(n),
|
||||
DrumSound.DHOL_TILLI.value: lambda n: _synth_dhol_tilli(n),
|
||||
@@ -3246,9 +3632,54 @@ def _render_notes_to_buf(notes, buf, samples_per_beat, total_samples,
|
||||
if analog > 0:
|
||||
pitches = [hz * (2 ** (_rnd.gauss(0, analog * 5) / 1200))
|
||||
for hz in pitches]
|
||||
# Render oscillators (pass synth_kwargs for FM etc.)
|
||||
waves = [synth_fn(hz, n_samples=n_samples, **_skw)
|
||||
for hz in pitches]
|
||||
# Pitch bend: render at base pitch, then resample to shift
|
||||
# pitch over time. Resampling preserves the synth's timbre
|
||||
# perfectly — no sine waves, no retriggering.
|
||||
bend_amt = getattr(note, 'bend', 0.0)
|
||||
if bend_amt != 0:
|
||||
bend_type = getattr(note, 'bend_type', 'smooth')
|
||||
t_norm = numpy.linspace(0, 1, n_samples)
|
||||
|
||||
waves = []
|
||||
for hz in pitches:
|
||||
hz_end = hz * (2 ** (bend_amt / 12))
|
||||
# Build pitch ratio curve (1.0 = no shift)
|
||||
if bend_type == 'smooth':
|
||||
ratio = (hz_end / hz) ** t_norm
|
||||
elif bend_type == 'linear':
|
||||
ratio = 1.0 + (hz_end / hz - 1.0) * t_norm
|
||||
elif bend_type == 'late':
|
||||
late_t = numpy.clip((t_norm - 0.6) / 0.4, 0.0, 1.0)
|
||||
ratio = (hz_end / hz) ** late_t
|
||||
else:
|
||||
ratio = (hz_end / hz) ** t_norm
|
||||
|
||||
# Render a longer buffer at base pitch
|
||||
max_ratio = max(ratio.max(), 1.0)
|
||||
src_len = int(n_samples * max_ratio) + 100
|
||||
src = synth_fn(hz, n_samples=src_len, **_skw)
|
||||
src_f = src.astype(numpy.float64) / SAMPLE_PEAK
|
||||
|
||||
# Variable-rate resampling: read through source
|
||||
# at speed determined by the ratio curve
|
||||
read_pos = numpy.cumsum(ratio)
|
||||
read_pos = (read_pos - read_pos[0]).astype(numpy.float64)
|
||||
# Clamp to source bounds
|
||||
read_pos = numpy.clip(read_pos, 0, src_len - 2)
|
||||
# Linear interpolation
|
||||
idx = read_pos.astype(numpy.int64)
|
||||
frac = read_pos - idx
|
||||
bent = src_f[idx] * (1 - frac) + src_f[numpy.minimum(idx + 1, src_len - 1)] * frac
|
||||
waves.append((bent * SAMPLE_PEAK).astype(numpy.int16))
|
||||
else:
|
||||
# Per-note kwargs (e.g. lyric for vocal synth)
|
||||
note_skw = dict(_skw)
|
||||
note_lyric = getattr(note, 'lyric', '')
|
||||
if note_lyric:
|
||||
note_skw['lyric'] = note_lyric
|
||||
# Render oscillators
|
||||
waves = [synth_fn(hz, n_samples=n_samples, **note_skw)
|
||||
for hz in pitches]
|
||||
# Sub-oscillator: octave-below sine
|
||||
if sub_osc > 0:
|
||||
for hz in pitches:
|
||||
@@ -3312,6 +3743,12 @@ def _render_notes_to_buf(notes, buf, samples_per_beat, total_samples,
|
||||
vel_cutoff = vel_to_filter * vel_scale + 1000
|
||||
mixed = _apply_lowpass(mixed, vel_cutoff, q=filter_q)
|
||||
end = min(start + len(mixed), total_samples)
|
||||
# Choke: fade out any existing signal at this point
|
||||
# so new notes don't pile up on previous tails
|
||||
choke_len = min(int(SAMPLE_RATE * 0.003), start)
|
||||
if choke_len > 0:
|
||||
fade = numpy.linspace(1.0, 0.0, choke_len).astype(numpy.float32)
|
||||
buf[start - choke_len:start] *= fade
|
||||
buf[start:end] += mixed[:end - start] * volume * vel_scale
|
||||
# Spread detuned oscillators into stereo L/R
|
||||
if detune_up is not None and stereo_buf is not None:
|
||||
@@ -3614,6 +4051,7 @@ def render_score(score):
|
||||
DrumSound.TABLA_GE.value: -0.2,
|
||||
DrumSound.TABLA_KE.value: -0.2,
|
||||
DrumSound.TABLA_DHA.value: 0.0, # both drums = center
|
||||
DrumSound.TABLA_GE_BEND.value: -0.2,
|
||||
# Dhol: bass left, treble right
|
||||
DrumSound.DHOL_DAGGA.value: -0.2,
|
||||
DrumSound.DHOL_TILLI.value: 0.2,
|
||||
@@ -3647,6 +4085,10 @@ def render_score(score):
|
||||
drum_parts = [p for p in score.parts.values() if p.is_drums]
|
||||
for drum_part in drum_parts:
|
||||
part_stereo = numpy.zeros((total_samples, 2), dtype=numpy.float32)
|
||||
# Track last hit position per sound for choke (new hit dampens
|
||||
# the previous ring on the same drum)
|
||||
_last_hit_start = {}
|
||||
|
||||
for hit in drum_part._drum_hits:
|
||||
pos = hit.position
|
||||
if drum_swing > 0:
|
||||
@@ -3663,6 +4105,21 @@ def render_score(score):
|
||||
start = max(0, start)
|
||||
if start >= total_samples or start < 0:
|
||||
continue
|
||||
|
||||
# Choke: if the same sound was hit recently, fade out
|
||||
# the tail at this point (new hit dampens old resonance)
|
||||
sound_id = hit.sound.value
|
||||
if sound_id in _last_hit_start:
|
||||
prev_start = _last_hit_start[sound_id]
|
||||
# Quick 2ms fade-out at the new hit position
|
||||
fade_len = min(int(SAMPLE_RATE * 0.002), max(0, start - prev_start))
|
||||
if fade_len > 0 and start > 0:
|
||||
fade = numpy.linspace(1.0, 0.0, fade_len).astype(numpy.float32)
|
||||
fade_start = max(0, start - fade_len)
|
||||
for ch in range(2):
|
||||
part_stereo[fade_start:start, ch] *= fade
|
||||
_last_hit_start[sound_id] = start
|
||||
|
||||
remaining = total_samples - start
|
||||
hit_len = min(int(SAMPLE_RATE * 0.5), remaining)
|
||||
wave = _render_drum_hit(hit.sound.value, hit_len)
|
||||
|
||||
+107
-3
@@ -241,6 +241,26 @@ INSTRUMENTS = {
|
||||
"vel_to_filter": 3000,
|
||||
"analog": 0.3,
|
||||
},
|
||||
"granular_pad": {
|
||||
"synth": "granular_synth", "envelope": "pad",
|
||||
"reverb": 0.4, "reverb_type": "cathedral",
|
||||
"analog": 0.3,
|
||||
},
|
||||
"vocal": {
|
||||
"synth": "vocal_synth", "envelope": "strings",
|
||||
"reverb": 0.3, "reverb_type": "hall",
|
||||
"humanize": 0.15,
|
||||
},
|
||||
"choir": {
|
||||
"synth": "vocal_synth", "envelope": "pad",
|
||||
"detune": 8, "spread": 0.4,
|
||||
"reverb": 0.45, "reverb_type": "cathedral",
|
||||
},
|
||||
"granular_texture": {
|
||||
"synth": "granular_synth", "envelope": "none",
|
||||
"reverb": 0.5, "reverb_type": "taj_mahal",
|
||||
"delay": 0.3, "delay_time": 0.4, "delay_feedback": 0.4,
|
||||
},
|
||||
"808_bass": {
|
||||
"synth": "sine", "envelope": "pluck",
|
||||
"distortion": 0.4, "distortion_drive": 2.5,
|
||||
@@ -275,6 +295,31 @@ INSTRUMENTS = {
|
||||
"fm_ratio": 2.0, "fm_index": 3.0,
|
||||
"reverb": 0.4, "reverb_type": "cathedral",
|
||||
},
|
||||
"timpani": {
|
||||
"synth": "timpani_synth", "envelope": "none",
|
||||
"reverb": 0.4, "reverb_type": "cathedral",
|
||||
},
|
||||
|
||||
# ── Woodwinds (continued) ──
|
||||
"saxophone": {
|
||||
"synth": "saxophone_synth", "envelope": "bowed",
|
||||
"humanize": 0.15, "vel_to_filter": 1500,
|
||||
},
|
||||
"alto_sax": {
|
||||
"synth": "saxophone_synth", "envelope": "bowed",
|
||||
"humanize": 0.15, "vel_to_filter": 1800,
|
||||
},
|
||||
"tenor_sax": {
|
||||
"synth": "saxophone_synth", "envelope": "bowed",
|
||||
"lowpass": 3000,
|
||||
"humanize": 0.15, "vel_to_filter": 1200,
|
||||
},
|
||||
"bari_sax": {
|
||||
"synth": "saxophone_synth", "envelope": "bowed",
|
||||
"lowpass": 2000,
|
||||
"humanize": 0.15, "vel_to_filter": 800,
|
||||
"sub_osc": 0.15,
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
@@ -320,11 +365,19 @@ class TimeSignature:
|
||||
|
||||
@dataclass
|
||||
class Note:
|
||||
"""A pairing of a sound (Tone, Chord, or None for rest) with a duration."""
|
||||
"""A pairing of a sound (Tone, Chord, or None for rest) with a duration.
|
||||
|
||||
The optional ``bend`` field specifies a pitch bend in semitones
|
||||
applied over the note's duration. Positive = bend up, negative = down.
|
||||
For example, ``bend=2`` bends the note up a whole step by the end.
|
||||
"""
|
||||
|
||||
tone: object
|
||||
duration: Duration
|
||||
velocity: int = 100
|
||||
bend: float = 0.0
|
||||
bend_type: str = "smooth" # "smooth" (log), "linear", "late"
|
||||
lyric: str = "" # syllable for vocal synth
|
||||
|
||||
@property
|
||||
def beats(self) -> float:
|
||||
@@ -421,6 +474,7 @@ class DrumSound(Enum):
|
||||
MRIDANGAM_NAM = 99 # treble ring (valanthalai/right head)
|
||||
MRIDANGAM_DIN = 100 # both heads
|
||||
MRIDANGAM_THA = 101 # muted treble
|
||||
TABLA_GE_BEND = 108 # bayan with upward pitch bend (palm press)
|
||||
# Djembe sounds
|
||||
DJEMBE_BASS = 102 # open bass (center of head)
|
||||
DJEMBE_TONE = 103 # open tone (edge, fingers together)
|
||||
@@ -2051,11 +2105,15 @@ class Part:
|
||||
self._drum_pattern_beats: float = 0.0
|
||||
self._automation: list[tuple[float, dict]] = [] # (beat, {param: value})
|
||||
|
||||
def add(self, tone_or_string, duration=Duration.QUARTER, *, velocity: int = 100) -> "Part":
|
||||
def add(self, tone_or_string, duration=Duration.QUARTER, *, velocity: int = 100,
|
||||
bend: float = 0.0, bend_type: str = "smooth", lyric: str = "") -> "Part":
|
||||
"""Add a note. Accepts Tone/Chord objects or note strings like ``"E5"``.
|
||||
|
||||
Duration can be a ``Duration`` enum or a raw float (beats).
|
||||
Velocity controls loudness (1-127, default 100).
|
||||
Bend specifies a pitch bend in semitones over the note duration
|
||||
(e.g. ``bend=2`` bends up a whole step, ``bend=-1`` bends down
|
||||
a half step). Used for guitar bends, sitar meends, slides.
|
||||
|
||||
Returns self for chaining.
|
||||
"""
|
||||
@@ -2064,7 +2122,9 @@ class Part:
|
||||
tone_or_string = Tone.from_string(tone_or_string, system=self._system)
|
||||
if isinstance(duration, (int, float)):
|
||||
duration = _RawDuration(duration)
|
||||
self.notes.append(Note(tone=tone_or_string, duration=duration, velocity=velocity))
|
||||
self.notes.append(Note(tone=tone_or_string, duration=duration,
|
||||
velocity=velocity, bend=bend,
|
||||
bend_type=bend_type, lyric=lyric))
|
||||
return self
|
||||
|
||||
def set(self, **params) -> "Part":
|
||||
@@ -2421,6 +2481,50 @@ class Part:
|
||||
|
||||
return self
|
||||
|
||||
def roll(self, tone_or_string, duration=Duration.WHOLE, *,
|
||||
velocity_start: int = 40, velocity_end: int = 100,
|
||||
speed=Duration.SIXTEENTH) -> "Part":
|
||||
"""Play a roll — rapid repeated notes with velocity ramp.
|
||||
|
||||
Perfect for timpani rolls, snare rolls, tremolo on any
|
||||
instrument. The velocity ramps from ``velocity_start`` to
|
||||
``velocity_end`` over the duration for crescendo/decrescendo.
|
||||
|
||||
Args:
|
||||
tone_or_string: The note to repeat.
|
||||
duration: Total duration of the roll.
|
||||
velocity_start: Velocity of the first hit (default 40).
|
||||
velocity_end: Velocity of the last hit (default 100).
|
||||
speed: How fast to repeat (default SIXTEENTH notes).
|
||||
|
||||
Returns:
|
||||
Self for chaining.
|
||||
|
||||
Example::
|
||||
|
||||
>>> timp = score.part("timp", instrument="timpani")
|
||||
>>> timp.roll("C3", Duration.WHOLE, velocity_start=30, velocity_end=110)
|
||||
"""
|
||||
if hasattr(duration, 'value'):
|
||||
total = duration.value
|
||||
else:
|
||||
total = float(duration)
|
||||
if hasattr(speed, 'value'):
|
||||
step = speed.value
|
||||
else:
|
||||
step = float(speed)
|
||||
|
||||
n_hits = max(1, int(total / step))
|
||||
for i in range(n_hits):
|
||||
frac = i / max(1, n_hits - 1)
|
||||
vel = int(velocity_start + (velocity_end - velocity_start) * frac)
|
||||
vel = max(1, min(127, vel))
|
||||
remaining = total - i * step
|
||||
note_dur = min(step, remaining)
|
||||
if note_dur > 0:
|
||||
self.add(tone_or_string, note_dur, velocity=vel)
|
||||
return self
|
||||
|
||||
@property
|
||||
def is_drums(self) -> bool:
|
||||
"""True if this part contains drum hits."""
|
||||
|
||||
+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
|
||||
|
||||
+202
-1
@@ -5320,7 +5320,7 @@ def test_supersaw_wave():
|
||||
@needs_portaudio
|
||||
def test_all_synths_in_enum():
|
||||
from pytheory.play import Synth
|
||||
assert len(Synth) == 27
|
||||
assert len(Synth) == 30
|
||||
for s in Synth:
|
||||
wave = s(440, n_samples=1000)
|
||||
assert len(wave) == 1000
|
||||
@@ -6711,3 +6711,204 @@ def test_interval_to_non12():
|
||||
a5 = a.add(19)
|
||||
result = a.interval_to(a5)
|
||||
assert "octave" in result
|
||||
|
||||
|
||||
# ── Dedicated instrument synths ──────────────────────────────────────────────
|
||||
|
||||
def test_all_dedicated_synths_render():
|
||||
"""Every dedicated synth waveform produces valid audio."""
|
||||
from pytheory.play import (piano_wave, bass_guitar_wave, flute_wave,
|
||||
trumpet_wave, clarinet_wave, oboe_wave,
|
||||
marimba_wave, harpsichord_wave, cello_wave,
|
||||
harp_wave, upright_bass_wave,
|
||||
acoustic_guitar_wave, electric_guitar_wave,
|
||||
sitar_wave, SAMPLE_RATE)
|
||||
synths = [piano_wave, bass_guitar_wave, flute_wave, trumpet_wave,
|
||||
clarinet_wave, oboe_wave, marimba_wave, harpsichord_wave,
|
||||
cello_wave, harp_wave, upright_bass_wave,
|
||||
acoustic_guitar_wave, electric_guitar_wave, sitar_wave]
|
||||
for fn in synths:
|
||||
wave = fn(440, n_samples=11025)
|
||||
assert len(wave) == 11025
|
||||
assert wave.dtype == numpy.int16
|
||||
assert numpy.abs(wave).max() > 0
|
||||
|
||||
|
||||
def test_piano_brightness_scales():
|
||||
"""High-pitched piano should be brighter (more high harmonics)."""
|
||||
from pytheory.play import piano_wave
|
||||
low = piano_wave(130, n_samples=22050) # C3
|
||||
high = piano_wave(1047, n_samples=22050) # C6
|
||||
# Both should produce valid audio
|
||||
assert numpy.abs(low).max() > 0
|
||||
assert numpy.abs(high).max() > 0
|
||||
|
||||
|
||||
def test_acoustic_guitar_body_resonance():
|
||||
"""Acoustic guitar should produce richer spectrum than raw pluck."""
|
||||
from pytheory.play import acoustic_guitar_wave, pluck_wave
|
||||
ag = acoustic_guitar_wave(220, n_samples=22050)
|
||||
pk = pluck_wave(220, n_samples=22050)
|
||||
assert len(ag) == len(pk) == 22050
|
||||
|
||||
|
||||
def test_cello_has_vibrato():
|
||||
"""Cello synth should produce pitch variation (vibrato)."""
|
||||
from pytheory.play import cello_wave
|
||||
wave = cello_wave(220, n_samples=44100)
|
||||
assert len(wave) == 44100
|
||||
assert numpy.abs(wave).max() > 0
|
||||
|
||||
|
||||
# ── Cabinet simulation ───────────────────────────────────────────────────────
|
||||
|
||||
def test_cabinet_reduces_highs():
|
||||
"""Cabinet sim should reduce high-frequency content."""
|
||||
from pytheory.play import _apply_cabinet
|
||||
# White noise has flat spectrum
|
||||
noise = numpy.random.uniform(-1, 1, 44100).astype(numpy.float32)
|
||||
cabbed = _apply_cabinet(noise, brightness=0.5)
|
||||
# RMS of cabbed should be lower (energy removed by filters)
|
||||
assert numpy.sqrt(numpy.mean(cabbed ** 2)) < numpy.sqrt(numpy.mean(noise ** 2))
|
||||
|
||||
|
||||
def test_cabinet_brightness_param():
|
||||
"""Higher brightness = more high-frequency content passes through."""
|
||||
from pytheory.play import _apply_cabinet
|
||||
noise = numpy.random.uniform(-1, 1, 44100).astype(numpy.float32)
|
||||
dark = _apply_cabinet(noise, brightness=0.0)
|
||||
bright = _apply_cabinet(noise, brightness=1.0)
|
||||
# Bright should have more energy than dark
|
||||
assert numpy.sqrt(numpy.mean(bright ** 2)) > numpy.sqrt(numpy.mean(dark ** 2))
|
||||
|
||||
|
||||
# ── Analog drift ─────────────────────────────────────────────────────────────
|
||||
|
||||
def test_analog_drift_varies_pitch():
|
||||
"""Analog drift should make repeated renders slightly different."""
|
||||
from pytheory import Score, Duration
|
||||
score1 = Score("4/4", bpm=120)
|
||||
p1 = score1.part("t", synth="saw", analog=0.5)
|
||||
p1.add("C4", Duration.QUARTER)
|
||||
p1.add("C4", Duration.QUARTER)
|
||||
# With analog > 0, each C4 gets a random pitch offset
|
||||
# This is hard to test deterministically, just verify it renders
|
||||
from pytheory.play import render_score
|
||||
buf = render_score(score1)
|
||||
assert len(buf) > 0
|
||||
|
||||
|
||||
# ── Guitar strumming ─────────────────────────────────────────────────────────
|
||||
|
||||
def test_strum_requires_fretboard():
|
||||
"""Strumming without a fretboard should raise ValueError."""
|
||||
from pytheory import Score, Duration
|
||||
score = Score("4/4", bpm=120)
|
||||
p = score.part("g", synth="saw")
|
||||
with pytest.raises(ValueError, match="fretboard"):
|
||||
p.strum("Am", Duration.QUARTER)
|
||||
|
||||
|
||||
def test_strum_adds_notes():
|
||||
"""Strumming should add notes to the part."""
|
||||
from pytheory import Score, Duration, Fretboard
|
||||
score = Score("4/4", bpm=120)
|
||||
fb = Fretboard.guitar()
|
||||
p = score.part("g", instrument="acoustic_guitar", fretboard=fb)
|
||||
p.strum("Am", Duration.HALF)
|
||||
assert len(p.notes) > 0
|
||||
|
||||
|
||||
def test_strum_direction():
|
||||
"""Both down and up strums should work."""
|
||||
from pytheory import Score, Duration, Fretboard
|
||||
score = Score("4/4", bpm=120)
|
||||
fb = Fretboard.guitar()
|
||||
p = score.part("g", instrument="acoustic_guitar", fretboard=fb)
|
||||
p.strum("G", Duration.QUARTER, direction="down")
|
||||
p.strum("G", Duration.QUARTER, direction="up")
|
||||
assert len(p.notes) == 2
|
||||
|
||||
|
||||
# ── World drums ──────────────────────────────────────────────────────────────
|
||||
|
||||
def test_tabla_sounds_render():
|
||||
"""All tabla drum sounds should produce valid audio."""
|
||||
from pytheory.play import _render_drum_hit
|
||||
from pytheory.rhythm import DrumSound
|
||||
for sound in [DrumSound.TABLA_NA, DrumSound.TABLA_TIN, DrumSound.TABLA_GE,
|
||||
DrumSound.TABLA_DHA, DrumSound.TABLA_TIT, DrumSound.TABLA_KE]:
|
||||
wave = _render_drum_hit(sound.value, 22050)
|
||||
assert len(wave) == 22050
|
||||
assert wave.dtype == numpy.float32
|
||||
|
||||
|
||||
def test_dhol_sounds_render():
|
||||
from pytheory.play import _render_drum_hit
|
||||
from pytheory.rhythm import DrumSound
|
||||
for sound in [DrumSound.DHOL_DAGGA, DrumSound.DHOL_TILLI, DrumSound.DHOL_BOTH]:
|
||||
wave = _render_drum_hit(sound.value, 22050)
|
||||
assert len(wave) == 22050
|
||||
|
||||
|
||||
def test_mridangam_sounds_render():
|
||||
from pytheory.play import _render_drum_hit
|
||||
from pytheory.rhythm import DrumSound
|
||||
for sound in [DrumSound.MRIDANGAM_THAM, DrumSound.MRIDANGAM_NAM,
|
||||
DrumSound.MRIDANGAM_DIN, DrumSound.MRIDANGAM_THA]:
|
||||
wave = _render_drum_hit(sound.value, 22050)
|
||||
assert len(wave) == 22050
|
||||
|
||||
|
||||
def test_djembe_sounds_render():
|
||||
from pytheory.play import _render_drum_hit
|
||||
from pytheory.rhythm import DrumSound
|
||||
for sound in [DrumSound.DJEMBE_BASS, DrumSound.DJEMBE_TONE, DrumSound.DJEMBE_SLAP]:
|
||||
wave = _render_drum_hit(sound.value, 22050)
|
||||
assert len(wave) == 22050
|
||||
|
||||
|
||||
def test_metal_kit_sounds_render():
|
||||
from pytheory.play import _render_drum_hit
|
||||
from pytheory.rhythm import DrumSound
|
||||
for sound in [DrumSound.METAL_KICK, DrumSound.METAL_SNARE, DrumSound.METAL_HAT]:
|
||||
wave = _render_drum_hit(sound.value, 22050)
|
||||
assert len(wave) == 22050
|
||||
|
||||
|
||||
def test_tabla_pattern_presets():
|
||||
"""All tabla patterns should load without error."""
|
||||
from pytheory.rhythm import Pattern
|
||||
for name in ["teental", "jhaptaal", "rupak", "dadra",
|
||||
"keherwa", "tabla solo", "tiri kita"]:
|
||||
p = Pattern.preset(name)
|
||||
assert p.beats > 0
|
||||
|
||||
|
||||
def test_world_drum_pattern_presets():
|
||||
"""All world drum patterns should load."""
|
||||
from pytheory.rhythm import Pattern
|
||||
for name in ["bhangra", "dhol chaal", "qawwali", "dholak folk",
|
||||
"adi talam", "mridangam korvai", "djembe", "kuku", "soli",
|
||||
"double kick", "metal blast", "metal groove", "metal gallop"]:
|
||||
p = Pattern.preset(name)
|
||||
assert p.beats > 0
|
||||
|
||||
|
||||
# ── Guitar presets with cabinet sim ──────────────────────────────────────────
|
||||
|
||||
def test_guitar_presets_have_cabinet():
|
||||
"""Distorted guitar presets should have cabinet simulation."""
|
||||
from pytheory import Score
|
||||
for preset in ["distorted_guitar", "orange_crunch", "metal_guitar"]:
|
||||
score = Score("4/4", bpm=120)
|
||||
p = score.part("g", instrument=preset)
|
||||
assert p.cabinet > 0, f"{preset} should have cabinet sim"
|
||||
|
||||
|
||||
def test_clean_guitar_preset():
|
||||
from pytheory import Score
|
||||
score = Score("4/4", bpm=120)
|
||||
p = score.part("g", instrument="clean_guitar")
|
||||
assert p.synth == "electric_guitar_synth"
|
||||
assert p.cabinet > 0
|
||||
|
||||
@@ -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.34.0"
|
||||
version = "0.35.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