mirror of
https://github.com/kennethreitz/pytheory.git
synced 2026-06-05 23:00:20 +00:00
Compare commits
100 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| a0756b3172 | |||
| e3dd706032 | |||
| 9b412906bc | |||
| 54e0421997 | |||
| 109343ad30 | |||
| 28e84de566 | |||
| d353d64298 | |||
| 7ee02e7ed2 | |||
| a5c9a46eb2 | |||
| f9c63ec360 | |||
| b9e88b77d8 | |||
| 1910b09132 | |||
| 0c5287450b | |||
| 5ac1873d83 | |||
| 9fafca2b08 | |||
| af044f68ca | |||
| 60f697f846 | |||
| 7d678e364e | |||
| 3a8d829010 | |||
| 2a67906937 | |||
| b9dcad0454 | |||
| db9726168a | |||
| 26af923789 | |||
| 72e18a9bec | |||
| 7d56ed7a2c | |||
| 6efa4f18ce | |||
| 06fc4cabb7 | |||
| d3a93c18b3 | |||
| 0e10359236 | |||
| df00c3436d | |||
| 2f02df15b8 | |||
| a2740b8d57 | |||
| 840bfcc36c | |||
| 938c1cc132 | |||
| 9dc22db4b2 | |||
| f570e226cd | |||
| 0c5c3abedc | |||
| 35d07b984b | |||
| aec7723ee6 | |||
| b98a40297b | |||
| 9117568b74 | |||
| 11e4417c62 | |||
| 4edf1d983d | |||
| 74b07b1a8a | |||
| c9437209a7 | |||
| 92cb855a49 | |||
| f06c6f77d1 | |||
| 51bd63658f | |||
| 92ade3ee3d | |||
| 833867329e | |||
| 93b9fe9ced | |||
| 88a1171bbe | |||
| 3ca0842b7a | |||
| 00de5eb354 | |||
| d2b0c6f329 | |||
| 76612682f1 | |||
| ce480858e9 | |||
| 70efb0ad40 | |||
| bf6deaab64 | |||
| 7c792c0a2a | |||
| bf8d4b9a77 | |||
| d2d5115c8a | |||
| 3cdd98b158 | |||
| 751d5a49b8 | |||
| 6a836dd891 | |||
| 1f888e2b21 | |||
| fb923f6c76 | |||
| 59e3338892 | |||
| 8cf4145c15 | |||
| b3885b2c15 | |||
| ae04fa60cc | |||
| 6c411e43f8 | |||
| e0427af3cc | |||
| 552836ae5b | |||
| 0fe53fcdeb | |||
| f6fb2a2cd6 | |||
| 70d6e6b8ce | |||
| aec9a999cb | |||
| 3acde86028 | |||
| aa405702a9 | |||
| b7c018fb94 | |||
| 07a52a3a25 | |||
| e12cb9003b | |||
| 28968a1b5c | |||
| 8a4a2df1aa | |||
| f4a90637db | |||
| 90a1a31049 | |||
| 33b2e82594 | |||
| 9f8dd0006d | |||
| 417f7f74a3 | |||
| cd6f814049 | |||
| 83fcdb0a09 | |||
| aa21bf0f2a | |||
| e7e35ad4e4 | |||
| 503dbce937 | |||
| c6bbfae7e6 | |||
| 64ef7f0803 | |||
| 406e5d7e54 | |||
| 267b7284ba | |||
| 9b62b56120 |
+201
@@ -2,6 +2,207 @@
|
||||
|
||||
All notable changes to PyTheory are documented here.
|
||||
|
||||
## 0.39.3
|
||||
|
||||
- **33 audio samples in documentation** — every `play_score()` example
|
||||
now has an embedded stereo audio player. Covers quickstart, sequencing,
|
||||
drums (all world percussion), playback, and cookbook.
|
||||
- **`docs/generate_audio.py`** — renders all doc examples to WAV
|
||||
- Numpy vectorization: cached time arrays, decay envelopes, drum hits;
|
||||
vectorized piano harmonic synthesis
|
||||
- Fixed acid legato example (removed pad envelope, added proper 303 recipe)
|
||||
|
||||
## 0.39.2
|
||||
|
||||
- **Marching percussion** — snare, rimshot, and stick click sounds with
|
||||
high-tension kevlar synthesis and woody-metallic rimshot crack
|
||||
- **`Part.flam()`**, **`Part.diddle()`**, **`Part.cheese()`** — marching
|
||||
rudiment methods for any drum sound
|
||||
- **`Part ensemble=`** — duplicate voices with per-player timing tendencies
|
||||
and micro pitch drift. Works on any Part (drumline, string section, choir).
|
||||
`ensemble=20` for a full snare line, `ensemble=4` for a string quartet.
|
||||
- **Sympathetic resonance** — marching snare buzz builds up with repeated
|
||||
hits, decays during rests (like real snare wire response)
|
||||
- **4 marching patterns** — march, cadence, paradiddle, roll
|
||||
- **Chakradar tabla pattern** — 16-beat tihai of tihais composition
|
||||
- Song #32: Snare Cadence (flams, diddles, cheese, triplets, 32nds)
|
||||
|
||||
## 0.39.1
|
||||
|
||||
- **Chakradar tabla pattern** — 16-beat tihai of tihais composition with
|
||||
3 escalating phrases and a crescendo triplet finale
|
||||
|
||||
## 0.39.0
|
||||
|
||||
- **Dropped `numeral` dependency** — Roman numeral helpers inlined,
|
||||
reducing supply chain surface (#47)
|
||||
- **`Part.ramp()`** — smooth parameter automation with 4 interpolation
|
||||
curves (linear, ease_in, ease_out, ease_in_out)
|
||||
- **Articulations** — staccato, legato, marcato, tenuto, accent, fermata
|
||||
- **Dynamic curves** — crescendo(), decrescendo(), swell(), dynamics()
|
||||
- **`Part.hit()`** — individual drum sounds with articulation support
|
||||
- **Cross-choke drum damping** — djembe, hi-hats, cajón, doumbek
|
||||
- **5 new djembe patterns** + 3 djembe fills (30 fills total)
|
||||
- **6 new drum fills** — 3 cajón, 3 metal
|
||||
- **Duration arithmetic** — multiply, divide, add
|
||||
- **Improved djembe slap** synthesis
|
||||
- Song #31: Acid Tabla
|
||||
|
||||
## 0.38.2
|
||||
|
||||
- **`Part.ramp()`** — smooth parameter automation from current value to
|
||||
target over a duration. Works for lowpass, reverb, distortion, chorus,
|
||||
delay, volume, and any `.set()` parameter. Four interpolation curves:
|
||||
linear, ease_in, ease_out, ease_in_out.
|
||||
|
||||
## 0.38.1
|
||||
|
||||
- **Dynamic curves** — `Part.crescendo()`, `Part.decrescendo()`,
|
||||
`Part.swell()`, and `Part.dynamics()` for velocity ramps and custom
|
||||
curves across a sequence of notes
|
||||
|
||||
## 0.38.0
|
||||
|
||||
- **Articulations** — `staccato`, `legato`, `marcato`, `tenuto`, `accent`,
|
||||
`fermata` via `articulation=` on `Part.add()` and `Part.hold()`
|
||||
- **`Part.hit()`** — place individual drum sounds in a Part's note stream
|
||||
with articulation, velocity, and effects support
|
||||
- **5 new djembe patterns** — dununba, tiriba, yankadi, djansa, mendiani
|
||||
- **3 new djembe fills** — djembe call, djembe roll, djembe break (30 fills total)
|
||||
- **Cross-choke drum damping** — striking one sound fades out related sounds
|
||||
(djembe, hi-hats, cajón, doumbek)
|
||||
- **Improved djembe slap** — dry goatskin pop instead of snare-like noise
|
||||
|
||||
## 0.37.0
|
||||
|
||||
- **5 new djembe patterns** — dununba, tiriba, yankadi, djansa, mendiani
|
||||
- **3 new djembe fills** — djembe call, djembe roll, djembe break (30 fills total)
|
||||
- **Cross-choke drum damping** — striking one sound on a hand drum fades
|
||||
out the ring of related sounds (djembe slap kills bass resonance, closed
|
||||
hat chokes open hat, cajón slap dampens bass, doumbek tek dampens dum)
|
||||
- **Improved djembe slap** — dry, high-pitched goatskin pop instead of
|
||||
snare-like noise rattle
|
||||
|
||||
## 0.36.6
|
||||
|
||||
- **6 new drum fills** — 3 cajón (flam, rumble, breakdown) and 3 metal
|
||||
(triplet, blast, cascade). 27 fills total.
|
||||
- Updated drums documentation with fill lists and examples
|
||||
|
||||
## 0.36.5
|
||||
|
||||
- **Duration arithmetic** — `Duration.WHOLE * 2`, `Duration.HALF + Duration.QUARTER`,
|
||||
division, and reverse multiply all work now (previously raised TypeError)
|
||||
|
||||
## 0.36.3
|
||||
|
||||
- **`Part.hold()`** — polyphonic overlap on a single part. Add notes
|
||||
without advancing the beat position so they play simultaneously.
|
||||
Enables: piano sustain, sitar drone under melody, guitar strum texture.
|
||||
- **Strum uses hold()** — leading string plays simultaneously with chord,
|
||||
no more timing gaps or choppiness
|
||||
- **Improved songs** 1-16: humanize, velocity dynamics, reverb, saxophone
|
||||
for blues
|
||||
- **Ctrl-C handling** — clean stop on all playback functions
|
||||
- **REPL updates** — strum, roll, bend, temperament, reference commands
|
||||
- Song #28 Descent (generative), #29 Pop Rock, #30 Sitar Drone
|
||||
- 862 tests
|
||||
|
||||
## 0.36.1
|
||||
|
||||
- **7 new instrument synths:** pedal steel guitar, theremin, kalimba/thumb
|
||||
piano, steel drum/pan, accordion (musette reeds), didgeridoo (drone +
|
||||
shifting formants), bagpipes (chanter reed)
|
||||
- **9 new demo moods** in ``pytheory demo``: Theremin Noir, Caribbean,
|
||||
Accordion Waltz, Kalimba Dreams, Outback Drone, Highland, Nashville
|
||||
Tears, Tabla Fusion
|
||||
- Improved existing songs with dedicated instrument synths
|
||||
- 41 synth waveforms, 26+ songs, 21 demo moods
|
||||
|
||||
## 0.36.0
|
||||
|
||||
- **Banjo synth** — steel strings on drum-head body, nasal twang,
|
||||
fast decay with membrane resonance
|
||||
- **Mandolin synth** — paired steel strings (natural chorus from
|
||||
doubled courses), bright body resonance
|
||||
- **Ukulele synth** — nylon strings, small mid-heavy body, shorter
|
||||
sustain than guitar
|
||||
- **Cajón drums** — bass (woody box thump), slap (snare wire buzz),
|
||||
tap (ghost note). 3 patterns: cajon, cajon rumba, cajon folk
|
||||
- **Vocal/formant synth** — LF glottal model, 5 Peterson & Barney
|
||||
formant peaks, jitter/shimmer, consonant onsets, per-note lyrics.
|
||||
Presets: vocal, choir
|
||||
- **Granular synthesis** — grain cloud engine with scatter, pitch
|
||||
variation, Hanning windows. Presets: granular_pad, granular_texture
|
||||
- **Strum sweep** — subtle grace notes before chord hit for natural
|
||||
strum feel on all fretboard instruments
|
||||
- Mandola preset, 34 synth waveforms, 26 songs
|
||||
|
||||
## 0.35.0
|
||||
|
||||
- **8.5x faster import** — dropped pytuning/sympy, lazy-load scipy.
|
||||
`import pytheory` now takes ~50ms instead of ~480ms (#44)
|
||||
- **Proper shruti JI ratios** — 22 positions with 5-limit just intonation
|
||||
(pure 3/2 fifths, 5/4 thirds), not 22-TET approximation
|
||||
- **Arabic maqam JI ratios** — Zalzalian 11-limit ratios.
|
||||
Mi↓ (the Rast third) is exactly 27/22 from Do
|
||||
- **B#/Cb octave boundary fix** — B#4 = C5, Cb4 = B3 (#45)
|
||||
- **Int tone names** — `Tone(0, system=TET(22))` works alongside strings.
|
||||
Wrapping: `Tone(22)` → tone 0, octave+1. `System.tone()` convenience.
|
||||
- **Timpani synth** — inharmonic membrane modes, felt mallet, copper kettle
|
||||
resonance, cathedral reverb
|
||||
- **Saxophone synth** — conical bore, reed buzz, brass body warmth.
|
||||
4 presets: saxophone, alto_sax, tenor_sax, bari_sax
|
||||
- **Part.roll()** — rapid repeated notes with velocity ramp for crescendo/
|
||||
decrescendo rolls on any instrument
|
||||
- **Vibrato tuning** — all instruments reduced to 0.001 depth for cleaner
|
||||
ensemble sound
|
||||
- **Granular synthesis** — grain cloud engine with scatter, pitch
|
||||
variation, and Hanning-windowed grains. Two presets: granular_pad,
|
||||
granular_texture.
|
||||
- 30 synth waveforms, 838 tests
|
||||
|
||||
## 0.34.0
|
||||
|
||||
- **16 dedicated instrument synths** — physical modeling and specialized
|
||||
synthesis for: piano (hammer + steel strings + soundboard), bass guitar
|
||||
(thick KS + pickup), flute (breath + tube resonance), trumpet (lip buzz
|
||||
+ bell), clarinet (odd harmonics + reed), oboe (double reed + conical
|
||||
bore), marimba (inharmonic bar modes), harpsichord (quill pluck),
|
||||
cello (deep bowed + body), harp (soft pluck + soundboard bloom),
|
||||
upright bass (pizzicato + wooden body), acoustic guitar (KS + body
|
||||
resonance), electric guitar (KS + pickup comb filter), sitar (jawari
|
||||
+ chikari), plus organ and bowed strings
|
||||
- **Speaker cabinet simulation** — tames distorted guitar fizz
|
||||
- **Guitar strumming** — `Part.strum("Am")` with fretboard lookup
|
||||
- **Analog oscillator drift** — subtle per-note pitch wobble on synth presets
|
||||
- **World percussion:** dhol, dholak, mridangam, djembe, metal kit
|
||||
with 22 new drum patterns
|
||||
- **Piano improvements:** brightness scales with pitch, two-stage decay,
|
||||
hammer impact with felt character
|
||||
- **Vibrato tuning:** reduced across flute, oboe, trumpet, cello for
|
||||
smoother ensemble sound
|
||||
- 27 synth waveforms, 10 envelopes, 40+ instrument presets, 80+ drum patterns
|
||||
|
||||
## 0.33.1
|
||||
|
||||
- **Electric guitar synth** — Karplus-Strong with magnetic pickup comb filter
|
||||
simulation (single-coil honk, proper sustain)
|
||||
- **Speaker cabinet simulation** — steep rolloff above 4-5kHz with presence
|
||||
bump. Makes distorted guitar sound warm instead of fizzy.
|
||||
- **6 guitar presets:** electric_guitar, clean_guitar, crunch_guitar,
|
||||
distorted_guitar, orange_crunch, metal_guitar — all with proper cab sim
|
||||
- **Sitar synth** — Karplus-Strong with jawari bridge buzz, chikari
|
||||
sympathetic strings, variable damping
|
||||
- **Guitar strumming** — `Part.strum("Am", Duration.HALF)` with
|
||||
fretboard fingering lookup, down/up direction, adjustable strum speed
|
||||
- **World drums:** dhol (bhangra, chaal), dholak (qawwali, folk),
|
||||
mridangam (adi talam, korvai), djembe (standard, kuku, soli)
|
||||
— all with bandpass-filtered membrane noise for realistic drum head sound
|
||||
- **Metal drum kit** — clicky kick, bright snare, tight hats
|
||||
with 4 patterns (double kick, metal blast, metal groove, metal gallop)
|
||||
- 15 synth waveforms, 10 envelopes, 40+ instrument presets
|
||||
|
||||
## 0.33.0
|
||||
|
||||
- **Non-12-TET support** — `TET(n)` factory creates any equal temperament
|
||||
|
||||
Vendored
BIN
Binary file not shown.
BIN
Binary file not shown.
Vendored
BIN
Binary file not shown.
Vendored
BIN
Binary file not shown.
Vendored
BIN
Binary file not shown.
BIN
Binary file not shown.
Vendored
BIN
Binary file not shown.
Vendored
BIN
Binary file not shown.
Vendored
BIN
Binary file not shown.
Vendored
BIN
Binary file not shown.
Vendored
BIN
Binary file not shown.
Vendored
BIN
Binary file not shown.
Vendored
BIN
Binary file not shown.
Vendored
BIN
Binary file not shown.
Vendored
BIN
Binary file not shown.
Vendored
BIN
Binary file not shown.
Vendored
BIN
Binary file not shown.
Vendored
BIN
Binary file not shown.
Vendored
BIN
Binary file not shown.
Vendored
BIN
Binary file not shown.
Vendored
BIN
Binary file not shown.
Vendored
BIN
Binary file not shown.
BIN
Binary file not shown.
Vendored
BIN
Binary file not shown.
Vendored
BIN
Binary file not shown.
Vendored
BIN
Binary file not shown.
Vendored
BIN
Binary file not shown.
Vendored
BIN
Binary file not shown.
Vendored
BIN
Binary file not shown.
Vendored
BIN
Binary file not shown.
Vendored
BIN
Binary file not shown.
BIN
Binary file not shown.
Vendored
BIN
Binary file not shown.
Vendored
BIN
Binary file not shown.
Vendored
+4
@@ -0,0 +1,4 @@
|
||||
<audio controls style="width: 100%; margin: 0.5em 0 1.5em 0;">
|
||||
<source src="{{ pathto('_static/audio/' + file, 1) }}" type="audio/wav">
|
||||
Your browser does not support the audio element.
|
||||
</audio>
|
||||
@@ -0,0 +1,573 @@
|
||||
"""Generate audio samples for documentation.
|
||||
|
||||
Renders code examples from the docs as WAV files so they can be
|
||||
embedded as <audio> players on the website.
|
||||
|
||||
Usage:
|
||||
uv run python docs/generate_audio.py
|
||||
"""
|
||||
|
||||
import os
|
||||
import sys
|
||||
|
||||
sys.path.insert(0, os.path.join(os.path.dirname(__file__), ".."))
|
||||
|
||||
from pytheory import Score, Duration, Key, Chord, Fretboard, DrumSound
|
||||
from pytheory.play import render_score, SAMPLE_RATE
|
||||
|
||||
import numpy
|
||||
import struct
|
||||
|
||||
AUDIO_DIR = os.path.join(os.path.dirname(__file__), "_static", "audio")
|
||||
os.makedirs(AUDIO_DIR, exist_ok=True)
|
||||
|
||||
|
||||
def save_wav(buf, path):
|
||||
"""Save a float32 buffer as 16-bit stereo WAV."""
|
||||
# Handle both mono (n,) and stereo (n, 2) buffers
|
||||
if buf.ndim == 1:
|
||||
channels = 1
|
||||
n_frames = len(buf)
|
||||
else:
|
||||
channels = buf.shape[1]
|
||||
n_frames = buf.shape[0]
|
||||
peak = numpy.abs(buf).max()
|
||||
if peak > 0:
|
||||
buf = buf / peak * 0.9
|
||||
samples = (buf * 32767).astype(numpy.int16)
|
||||
byte_rate = SAMPLE_RATE * channels * 2
|
||||
block_align = channels * 2
|
||||
data_size = n_frames * channels * 2
|
||||
with open(path, "wb") as f:
|
||||
f.write(b"RIFF")
|
||||
f.write(struct.pack("<I", 36 + data_size))
|
||||
f.write(b"WAVE")
|
||||
f.write(b"fmt ")
|
||||
f.write(struct.pack("<IHHIIHH", 16, 1, channels, SAMPLE_RATE,
|
||||
byte_rate, block_align, 16))
|
||||
f.write(b"data")
|
||||
f.write(struct.pack("<I", data_size))
|
||||
f.write(samples.tobytes())
|
||||
label = "stereo" if channels == 2 else "mono"
|
||||
print(f" {os.path.basename(path)} ({n_frames/SAMPLE_RATE:.1f}s, {label})")
|
||||
|
||||
|
||||
def render(name, score):
|
||||
"""Render a score to WAV in the audio directory."""
|
||||
buf = render_score(score)
|
||||
save_wav(buf, os.path.join(AUDIO_DIR, f"{name}.wav"))
|
||||
|
||||
|
||||
# ── Piano hold (polyphonic overlap) ──────────────────────────────────────
|
||||
|
||||
def gen_piano_hold():
|
||||
score = Score("4/4", bpm=85)
|
||||
piano = score.part("piano", instrument="piano", reverb=0.3)
|
||||
piano.hold("C3", Duration.WHOLE * 2, velocity=60)
|
||||
piano.hold("E3", Duration.WHOLE * 2, velocity=55)
|
||||
piano.hold("G3", Duration.WHOLE * 2, velocity=55)
|
||||
for n in ["E4", "G4", "C5", "G4", "E4", "D4", "C4", "E4"]:
|
||||
piano.add(n, Duration.QUARTER, velocity=80)
|
||||
render("piano_hold", score)
|
||||
|
||||
|
||||
# ── Articulations ────────────────────────────────────────────────────────
|
||||
|
||||
def gen_articulations():
|
||||
score = Score("4/4", bpm=90)
|
||||
piano = score.part("piano", instrument="piano", reverb=0.25)
|
||||
for n in ["C4", "E4", "G4", "C5"]:
|
||||
piano.add(n, Duration.QUARTER, velocity=80)
|
||||
for n in ["C4", "E4", "G4", "C5"]:
|
||||
piano.add(n, Duration.QUARTER, velocity=80, articulation="staccato")
|
||||
for n in ["C5", "G4", "E4", "C4"]:
|
||||
piano.add(n, Duration.QUARTER, velocity=80, articulation="legato")
|
||||
for n in ["C4", "E4", "G4", "C5"]:
|
||||
piano.add(n, Duration.QUARTER, velocity=80, articulation="marcato")
|
||||
render("articulations", score)
|
||||
|
||||
|
||||
# ── Dynamic curves ───────────────────────────────────────────────────────
|
||||
|
||||
def gen_dynamics():
|
||||
score = Score("4/4", bpm=90)
|
||||
piano = score.part("piano", instrument="piano", reverb=0.3)
|
||||
piano.crescendo(["C4", "D4", "E4", "F4", "G4", "A4", "B4", "C5"],
|
||||
Duration.QUARTER, start_vel=30, end_vel=110)
|
||||
piano.decrescendo(["C5", "B4", "A4", "G4", "F4", "E4", "D4", "C4"],
|
||||
Duration.QUARTER, start_vel=110, end_vel=30)
|
||||
render("dynamics", score)
|
||||
|
||||
|
||||
# ── Filter ramp ──────────────────────────────────────────────────────────
|
||||
|
||||
def gen_filter_ramp():
|
||||
score = Score("4/4", bpm=130)
|
||||
score.drums("house", repeats=8, fill="house", fill_every=8)
|
||||
score.set_drum_effects(volume=0.4)
|
||||
acid = score.part("acid", synth="saw", volume=0.6,
|
||||
lowpass=300, lowpass_q=8.0, distortion=0.3,
|
||||
legato=True, glide=0.03)
|
||||
acid.ramp(over=Duration.WHOLE * 4, curve="ease_in", lowpass=5000)
|
||||
for _ in range(4):
|
||||
for n in ["C2", "C3", "C2", "Eb2", "C2", "G2", "Bb2", "C2"]:
|
||||
acid.add(n, Duration.EIGHTH, velocity=90)
|
||||
acid.ramp(over=Duration.WHOLE * 4, curve="ease_out", lowpass=300)
|
||||
for _ in range(4):
|
||||
for n in ["C2", "C3", "C2", "Eb2", "C2", "G2", "Bb2", "C2"]:
|
||||
acid.add(n, Duration.EIGHTH, velocity=88)
|
||||
render("filter_ramp", score)
|
||||
|
||||
|
||||
# ── Rock beat ────────────────────────────────────────────────────────────
|
||||
|
||||
def gen_rock_beat():
|
||||
score = Score("4/4", bpm=120)
|
||||
score.drums("rock", repeats=4, fill="rock", fill_every=4)
|
||||
render("rock_beat", score)
|
||||
|
||||
|
||||
def gen_bossa_nova_pattern():
|
||||
score = Score("4/4", bpm=140)
|
||||
score.drums("bossa nova", repeats=4)
|
||||
render("bossa_nova_pattern", score)
|
||||
|
||||
|
||||
def gen_salsa_pattern():
|
||||
score = Score("4/4", bpm=180)
|
||||
score.drums("salsa", repeats=4)
|
||||
render("salsa_pattern", score)
|
||||
|
||||
|
||||
def gen_afrobeat_pattern():
|
||||
score = Score("4/4", bpm=110)
|
||||
score.drums("afrobeat", repeats=8)
|
||||
render("afrobeat_pattern", score)
|
||||
|
||||
|
||||
# ── Bossa nova ───────────────────────────────────────────────────────────
|
||||
|
||||
def gen_bossa_nova():
|
||||
score = Score("4/4", bpm=140)
|
||||
score.drums("bossa nova", repeats=4)
|
||||
rhodes = score.part("rhodes", synth="fm", envelope="piano", volume=0.3,
|
||||
reverb=0.4, reverb_decay=1.8)
|
||||
for sym in ["Am", "Am", "Dm", "Dm", "E7", "E7", "Am", "Am"]:
|
||||
rhodes.add(Chord.from_symbol(sym), Duration.WHOLE)
|
||||
render("bossa_nova", score)
|
||||
|
||||
|
||||
# ── Djembe ───────────────────────────────────────────────────────────────
|
||||
|
||||
def gen_djembe():
|
||||
score = Score("4/4", bpm=110)
|
||||
score.drums("tiriba", repeats=4, fill="djembe call", fill_every=4)
|
||||
score.set_drum_effects(reverb=0.2)
|
||||
render("djembe", score)
|
||||
|
||||
|
||||
# ── Tabla ────────────────────────────────────────────────────────────────
|
||||
|
||||
def gen_tabla():
|
||||
score = Score("4/4", bpm=80)
|
||||
score.drums("teental", repeats=2)
|
||||
score.drums("chakradar", repeats=1)
|
||||
score.set_drum_effects(reverb=0.2)
|
||||
render("tabla", score)
|
||||
|
||||
|
||||
# ── Marching snare ───────────────────────────────────────────────────────
|
||||
|
||||
def gen_metal_blast():
|
||||
score = Score("4/4", bpm=200)
|
||||
score.drums("metal blast", repeats=8, fill="metal cascade", fill_every=4)
|
||||
render("metal_blast", score)
|
||||
|
||||
|
||||
def gen_cajon():
|
||||
score = Score("4/4", bpm=100)
|
||||
score.drums("cajon", repeats=8, fill="cajon flam", fill_every=4)
|
||||
render("cajon", score)
|
||||
|
||||
|
||||
def gen_tabla_teental():
|
||||
score = Score("4/4", bpm=160)
|
||||
score.drums("teental", repeats=4)
|
||||
score.set_drum_effects(reverb=0.2)
|
||||
render("tabla_teental", score)
|
||||
|
||||
|
||||
def gen_tabla_keherwa():
|
||||
score = Score("4/4", bpm=180)
|
||||
score.drums("keherwa", repeats=4, fill="chakkardar", fill_every=4)
|
||||
score.set_drum_effects(reverb=0.2)
|
||||
render("tabla_keherwa", score)
|
||||
|
||||
|
||||
def gen_tabla_chakradar():
|
||||
score = Score("4/4", bpm=200)
|
||||
score.drums("teental", repeats=2)
|
||||
score.drums("chakradar", repeats=1)
|
||||
score.set_drum_effects(reverb=0.2)
|
||||
render("tabla_chakradar", score)
|
||||
|
||||
|
||||
def gen_dhol():
|
||||
score = Score("4/4", bpm=160)
|
||||
score.drums("bhangra", repeats=4)
|
||||
render("dhol", score)
|
||||
|
||||
|
||||
def gen_dholak():
|
||||
score = Score("4/4", bpm=120)
|
||||
score.drums("qawwali", repeats=4)
|
||||
render("dholak", score)
|
||||
|
||||
|
||||
def gen_mridangam():
|
||||
score = Score("4/4", bpm=90)
|
||||
score.drums("adi talam", repeats=4)
|
||||
render("mridangam", score)
|
||||
|
||||
|
||||
def gen_march_snare():
|
||||
score = Score("4/4", bpm=120)
|
||||
p = score.part("snare", synth="sine", volume=0.8, reverb=0.15)
|
||||
S = DrumSound.MARCH_SNARE
|
||||
R = DrumSound.MARCH_RIMSHOT
|
||||
C = DrumSound.MARCH_CLICK
|
||||
|
||||
for _ in range(4):
|
||||
p.hit(C, Duration.QUARTER, velocity=95)
|
||||
|
||||
for _ in range(4):
|
||||
p.hit(R, Duration.SIXTEENTH, velocity=118)
|
||||
p.hit(S, Duration.SIXTEENTH, velocity=30)
|
||||
p.hit(S, Duration.SIXTEENTH, velocity=32)
|
||||
p.hit(S, Duration.SIXTEENTH, velocity=28)
|
||||
p.hit(R, Duration.SIXTEENTH, velocity=115)
|
||||
p.hit(S, Duration.SIXTEENTH, velocity=30)
|
||||
p.hit(S, Duration.SIXTEENTH, velocity=28)
|
||||
p.hit(S, Duration.SIXTEENTH, velocity=32)
|
||||
p.hit(R, Duration.SIXTEENTH, velocity=118)
|
||||
p.hit(S, Duration.SIXTEENTH, velocity=35)
|
||||
p.hit(S, Duration.SIXTEENTH, velocity=28)
|
||||
p.hit(S, Duration.SIXTEENTH, velocity=30)
|
||||
p.hit(R, Duration.SIXTEENTH, velocity=120)
|
||||
p.hit(S, Duration.SIXTEENTH, velocity=30)
|
||||
p.hit(S, Duration.SIXTEENTH, velocity=28)
|
||||
p.hit(S, Duration.SIXTEENTH, velocity=32)
|
||||
render("march_snare", score)
|
||||
|
||||
|
||||
# ── Ensemble comparison ──────────────────────────────────────────────────
|
||||
|
||||
def gen_ensemble():
|
||||
score = Score("4/4", bpm=120)
|
||||
S = DrumSound.MARCH_SNARE
|
||||
R = DrumSound.MARCH_RIMSHOT
|
||||
|
||||
# Solo first
|
||||
solo = score.part("solo", synth="sine", volume=0.7, reverb=0.15)
|
||||
for _ in range(2):
|
||||
solo.hit(R, Duration.SIXTEENTH, velocity=118)
|
||||
solo.hit(S, Duration.SIXTEENTH, velocity=30)
|
||||
solo.hit(S, Duration.SIXTEENTH, velocity=32)
|
||||
solo.hit(S, Duration.SIXTEENTH, velocity=28)
|
||||
solo.hit(R, Duration.SIXTEENTH, velocity=115)
|
||||
solo.hit(S, Duration.SIXTEENTH, velocity=30)
|
||||
solo.hit(S, Duration.SIXTEENTH, velocity=28)
|
||||
solo.hit(S, Duration.SIXTEENTH, velocity=32)
|
||||
solo.hit(R, Duration.SIXTEENTH, velocity=118)
|
||||
solo.hit(S, Duration.SIXTEENTH, velocity=35)
|
||||
solo.hit(S, Duration.SIXTEENTH, velocity=28)
|
||||
solo.hit(S, Duration.SIXTEENTH, velocity=30)
|
||||
solo.hit(R, Duration.SIXTEENTH, velocity=120)
|
||||
solo.hit(S, Duration.SIXTEENTH, velocity=30)
|
||||
solo.hit(S, Duration.SIXTEENTH, velocity=28)
|
||||
solo.hit(S, Duration.SIXTEENTH, velocity=32)
|
||||
|
||||
# Then ensemble
|
||||
line = score.part("line", synth="sine", volume=0.7, reverb=0.15, ensemble=8)
|
||||
for _ in range(8):
|
||||
line.rest(Duration.QUARTER)
|
||||
for _ in range(4):
|
||||
line.hit(R, Duration.SIXTEENTH, velocity=118)
|
||||
line.hit(S, Duration.SIXTEENTH, velocity=30)
|
||||
line.hit(S, Duration.SIXTEENTH, velocity=32)
|
||||
line.hit(S, Duration.SIXTEENTH, velocity=28)
|
||||
line.hit(R, Duration.SIXTEENTH, velocity=115)
|
||||
line.hit(S, Duration.SIXTEENTH, velocity=30)
|
||||
line.hit(S, Duration.SIXTEENTH, velocity=28)
|
||||
line.hit(S, Duration.SIXTEENTH, velocity=32)
|
||||
line.hit(R, Duration.SIXTEENTH, velocity=118)
|
||||
line.hit(S, Duration.SIXTEENTH, velocity=35)
|
||||
line.hit(S, Duration.SIXTEENTH, velocity=28)
|
||||
line.hit(S, Duration.SIXTEENTH, velocity=30)
|
||||
line.hit(R, Duration.SIXTEENTH, velocity=120)
|
||||
line.hit(S, Duration.SIXTEENTH, velocity=30)
|
||||
line.hit(S, Duration.SIXTEENTH, velocity=28)
|
||||
line.hit(S, Duration.SIXTEENTH, velocity=32)
|
||||
render("ensemble", score)
|
||||
|
||||
|
||||
# ── Guitar strum ─────────────────────────────────────────────────────────
|
||||
|
||||
def gen_strum():
|
||||
score = Score("4/4", bpm=100)
|
||||
guitar = score.part("guitar", instrument="acoustic_guitar",
|
||||
fretboard=Fretboard.guitar())
|
||||
for ch in ["G", "D", "Em", "C"] * 2:
|
||||
guitar.strum(ch, duration=Duration.WHOLE, velocity=75)
|
||||
render("strum", score)
|
||||
|
||||
|
||||
# ── Swell ────────────────────────────────────────────────────────────────
|
||||
|
||||
def gen_swell():
|
||||
score = Score("4/4", bpm=80)
|
||||
strings = score.part("strings", instrument="string_ensemble",
|
||||
reverb=0.4, ensemble=12)
|
||||
strings.swell(["C4", "D4", "E4", "F4", "G4", "F4", "E4", "D4",
|
||||
"C4", "D4", "E4", "F4", "G4", "A4", "G4", "E4"],
|
||||
Duration.QUARTER, low_vel=30, peak_vel=100)
|
||||
render("swell", score)
|
||||
|
||||
|
||||
# ── Generate all ─────────────────────────────────────────────────────────
|
||||
|
||||
# ── Cookbook: Acid House ───────────────────────────────────────────────────
|
||||
|
||||
def gen_acid_house():
|
||||
score = Score("4/4", bpm=132)
|
||||
score.drums("house", repeats=8, fill="house", fill_every=8)
|
||||
pad = score.part("pad", synth="supersaw", envelope="pad",
|
||||
reverb=0.4, chorus=0.3, sidechain=0.85)
|
||||
acid = score.part("acid", synth="saw", envelope="pad",
|
||||
legato=True, glide=0.03, distortion=0.8,
|
||||
distortion_drive=8.0, lowpass=1000, lowpass_q=5.0)
|
||||
acid.lfo("lowpass", rate=0.5, min=600, max=2500, bars=8)
|
||||
for sym in ["Cm", "Fm", "Abm", "Gm"]:
|
||||
pad.add(Chord.from_symbol(sym), Duration.WHOLE)
|
||||
pad.add(Chord.from_symbol(sym), Duration.WHOLE)
|
||||
acid.arpeggio(sym, bars=2, pattern="up", octaves=2)
|
||||
render("acid_house", score)
|
||||
|
||||
|
||||
# ── Cookbook: Dub Reggae ──────────────────────────────────────────────────
|
||||
|
||||
def gen_dub_reggae():
|
||||
score = Score("4/4", bpm=72)
|
||||
score.drums("dub", repeats=8)
|
||||
melodica = score.part("melodica", synth="triangle", envelope="pluck",
|
||||
delay=0.5, delay_time=0.66, delay_feedback=0.55,
|
||||
reverb=0.4, reverb_type="cathedral")
|
||||
bass = score.part("bass", synth="sine", lowpass=400, lowpass_q=1.5)
|
||||
melodica.add("A4", 2).rest(6)
|
||||
melodica.add("E5", 1.5).rest(6.5)
|
||||
melodica.add("D5", 1).add("C5", 1).add("A4", 2).rest(4)
|
||||
for _ in range(16):
|
||||
bass.add("A1", Duration.HALF)
|
||||
render("dub_reggae", score)
|
||||
|
||||
|
||||
# ── Cookbook: Jazz Ballad ─────────────────────────────────────────────────
|
||||
|
||||
def gen_jazz_ballad():
|
||||
score = Score("4/4", bpm=72, swing=0.5)
|
||||
score.drums("jazz", repeats=8)
|
||||
rhodes = score.part("rhodes", synth="fm", envelope="piano",
|
||||
reverb=0.4, reverb_type="plate", humanize=0.3)
|
||||
lead = score.part("lead", synth="triangle", envelope="strings",
|
||||
delay=0.25, reverb=0.3, humanize=0.35)
|
||||
key = Key("Bb", "major")
|
||||
for chord in key.progression("I", "vi", "ii", "V") * 2:
|
||||
rhodes.add(chord, Duration.WHOLE)
|
||||
for n, d in [("D5", 1.5), ("F5", 0.5), ("Bb5", 2), (None, 4),
|
||||
("A5", 1), ("G5", 1), ("F5", 2), (None, 4)]:
|
||||
lead.rest(d) if n is None else lead.add(n, d)
|
||||
render("jazz_ballad", score)
|
||||
|
||||
|
||||
# ── Quickstart example ───────────────────────────────────────────────────
|
||||
|
||||
def gen_arpeggio():
|
||||
score = Score("4/4", bpm=132)
|
||||
score.drums("house", repeats=8)
|
||||
score.set_drum_effects(volume=0.3)
|
||||
lead = score.part("lead", synth="saw", envelope="pluck", volume=0.5,
|
||||
lowpass=5000, detune=8, chorus=0.15, reverb=0.25,
|
||||
delay=0.2, delay_time=0.33, delay_feedback=0.3)
|
||||
for sym in ["Cm", "Fm", "Abm", "Gm"]:
|
||||
lead.arpeggio(sym, bars=2, pattern="updown", octaves=2)
|
||||
render("arpeggio", score)
|
||||
|
||||
|
||||
def gen_legato_glide():
|
||||
score = Score("4/4", bpm=132)
|
||||
score.drums("house", repeats=4)
|
||||
score.set_drum_effects(volume=0.35)
|
||||
acid = score.part("acid", synth="saw", volume=0.6,
|
||||
legato=True, glide=0.04,
|
||||
lowpass=3000, lowpass_q=6.0,
|
||||
distortion=0.3, distortion_drive=3.0)
|
||||
acid.lfo("lowpass", rate=0.5, min=800, max=4000, bars=4)
|
||||
for _ in range(4):
|
||||
acid.add("C2", 0.25).add("C3", 0.25).add("G2", 0.25).add("C2", 0.25)
|
||||
acid.add("C2", 0.25).add("Eb2", 0.25).add("G2", 0.25).add("Bb2", 0.25)
|
||||
render("legato_glide", score)
|
||||
|
||||
|
||||
def gen_quickstart():
|
||||
score = Score("4/4", bpm=140)
|
||||
score.drums("bossa nova", repeats=4)
|
||||
chords = score.part("chords", synth="sine", envelope="pad",
|
||||
reverb=0.4, volume=0.3)
|
||||
lead = score.part("lead", synth="saw", envelope="pluck",
|
||||
lowpass=2000, lowpass_q=3.0, distortion=0.8,
|
||||
legato=True, glide=0.03, volume=0.4)
|
||||
bass = score.part("bass", synth="sine", lowpass=500)
|
||||
key = Key("A", "minor")
|
||||
for chord in key.progression("i", "iv", "V", "i"):
|
||||
chords.add(chord, Duration.WHOLE)
|
||||
chords.add(chord, Duration.WHOLE)
|
||||
lead.arpeggio("Am", bars=2, pattern="updown", octaves=2)
|
||||
lead.arpeggio("Dm", bars=2, pattern="updown", octaves=2)
|
||||
lead.set(lowpass=5000, reverb=0.3)
|
||||
lead.arpeggio("E7", bars=2, pattern="up", octaves=2)
|
||||
lead.arpeggio("Am", bars=2, pattern="updown", octaves=2)
|
||||
for n in ["A2", "E2", "A2", "C3"] * 4:
|
||||
bass.add(n, Duration.QUARTER)
|
||||
render("quickstart", score)
|
||||
|
||||
|
||||
# ── Sequencing complete example (bossa nova) ─────────────────────────────
|
||||
|
||||
def gen_chords_basic():
|
||||
score = Score("4/4", bpm=120)
|
||||
key = Key("C", "major")
|
||||
chords = key.progression("I", "V", "vi", "IV")
|
||||
for chord in chords:
|
||||
score.add(chord, Duration.WHOLE)
|
||||
render("chords_basic", score)
|
||||
|
||||
|
||||
def gen_complete_rock():
|
||||
score = Score("4/4", bpm=120)
|
||||
score.drums("rock", repeats=8, fill="rock", fill_every=4)
|
||||
piano = score.part("piano", instrument="piano", volume=0.4, reverb=0.3)
|
||||
lead = score.part("lead", synth="saw", envelope="pluck", volume=0.4,
|
||||
delay=0.2, delay_time=0.33, reverb=0.2, lowpass=3000)
|
||||
bass = score.part("bass", synth="triangle", envelope="pluck", volume=0.45,
|
||||
lowpass=600)
|
||||
for chord in Key("G", "major").progression("I", "V", "vi", "IV") * 2:
|
||||
piano.add(chord, Duration.WHOLE)
|
||||
lead.add("D5", 1).add("B4", 0.5).add("D5", 0.5)
|
||||
lead.add("G5", 1).add("E5", 1)
|
||||
lead.add("D5", 0.5).add("B4", 0.5).add("A4", 1)
|
||||
lead.add("G4", 2).rest(2)
|
||||
lead.add("D5", 1).add("B4", 0.5).add("D5", 0.5)
|
||||
lead.add("G5", 1).add("A5", 1)
|
||||
lead.add("G5", 0.5).add("E5", 0.5).add("D5", 1)
|
||||
lead.add("B4", 2).rest(2)
|
||||
for n in ["G2", "G2", "D2", "D2", "E2", "E2", "C2", "C2"] * 2:
|
||||
bass.add(n, Duration.HALF)
|
||||
render("complete_rock", score)
|
||||
|
||||
|
||||
# ── Drums layering (salsa) ───────────────────────────────────────────────
|
||||
|
||||
def gen_salsa_layered():
|
||||
score = Score("4/4", bpm=180)
|
||||
score.drums("salsa", repeats=4, fill="salsa", fill_every=4)
|
||||
pads = score.part("pads", synth="sine", envelope="pad", volume=0.3)
|
||||
lead = score.part("lead", synth="saw", envelope="pluck", volume=0.4)
|
||||
bass = score.part("bass", synth="sine", envelope="pluck", volume=0.45)
|
||||
for chord in Key("D", "minor").progression("ii", "V", "i", "i") * 2:
|
||||
pads.add(chord, Duration.WHOLE)
|
||||
lead.add("A5", 0.67).add("G5", 0.33).add("F5", 0.67).add("E5", 0.33)
|
||||
for n in ["D2", "A2", "D2", "F2"] * 2:
|
||||
bass.add(n, Duration.QUARTER)
|
||||
render("salsa_layered", score)
|
||||
|
||||
|
||||
# ── Playback basic ───────────────────────────────────────────────────────
|
||||
|
||||
def gen_playback_basic():
|
||||
score = Score("4/4", bpm=140)
|
||||
score.drums("bossa nova", repeats=4)
|
||||
chords = score.part("chords", synth="sine", envelope="pad")
|
||||
for sym in ["Am", "Dm", "E7", "Am"]:
|
||||
chords.add(Chord.from_symbol(sym), Duration.WHOLE)
|
||||
render("playback_basic", score)
|
||||
|
||||
|
||||
# ── Cookbook: song with sections ──────────────────────────────────────────
|
||||
|
||||
def gen_song_sections():
|
||||
score = Score("4/4", bpm=120)
|
||||
score.drums("rock", repeats=16, fill="rock", fill_every=4)
|
||||
chords = score.part("chords", synth="saw", envelope="pad")
|
||||
lead = score.part("lead", synth="triangle", envelope="pluck")
|
||||
score.section("verse")
|
||||
for sym in ["Am", "F", "C", "G"]:
|
||||
chords.add(Chord.from_symbol(sym), Duration.WHOLE)
|
||||
lead.add("A4", 1).add("C5", 1).add("E5", 1).rest(1)
|
||||
lead.add("F5", 1).add("E5", 1).add("C5", 2)
|
||||
score.section("chorus")
|
||||
lead.set(reverb=0.4, lowpass=5000)
|
||||
for sym in ["F", "G", "Am", "C"]:
|
||||
chords.add(Chord.from_symbol(sym), Duration.WHOLE)
|
||||
lead.add("C6", 2).add("A5", 1).add("G5", 1)
|
||||
lead.add("F5", 2).add("E5", 2)
|
||||
score.end_section()
|
||||
score.repeat("verse")
|
||||
score.repeat("chorus", times=2)
|
||||
render("song_sections", score)
|
||||
|
||||
|
||||
GENERATORS = [
|
||||
gen_piano_hold,
|
||||
gen_articulations,
|
||||
gen_dynamics,
|
||||
gen_filter_ramp,
|
||||
gen_rock_beat,
|
||||
gen_bossa_nova_pattern,
|
||||
gen_salsa_pattern,
|
||||
gen_afrobeat_pattern,
|
||||
gen_bossa_nova,
|
||||
gen_djembe,
|
||||
gen_tabla,
|
||||
gen_metal_blast,
|
||||
gen_cajon,
|
||||
gen_tabla_teental,
|
||||
gen_tabla_keherwa,
|
||||
gen_tabla_chakradar,
|
||||
gen_dhol,
|
||||
gen_dholak,
|
||||
gen_mridangam,
|
||||
gen_march_snare,
|
||||
gen_ensemble,
|
||||
gen_strum,
|
||||
gen_swell,
|
||||
gen_arpeggio,
|
||||
gen_legato_glide,
|
||||
gen_acid_house,
|
||||
gen_dub_reggae,
|
||||
gen_jazz_ballad,
|
||||
gen_quickstart,
|
||||
gen_chords_basic,
|
||||
gen_complete_rock,
|
||||
gen_salsa_layered,
|
||||
gen_playback_basic,
|
||||
gen_song_sections,
|
||||
]
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
print("Generating audio samples for docs...")
|
||||
print()
|
||||
for gen in GENERATORS:
|
||||
gen()
|
||||
print()
|
||||
print(f"Done. {len(GENERATORS)} files in {AUDIO_DIR}")
|
||||
@@ -322,6 +322,14 @@ against 17 known chord types (triads, 7ths, 9ths, sus, power chords).
|
||||
>>> Chord.from_tones("Bb", "D", "F").identify()
|
||||
'Bb major'
|
||||
|
||||
Enharmonic spellings are fully supported — Cb, Fb, E#, B#, double
|
||||
sharps/flats, and unicode symbols (see :doc:`tones` for details):
|
||||
|
||||
.. code-block:: pycon
|
||||
|
||||
>>> Chord.from_tones("Cb", "Eb", "Gb").identify()
|
||||
'B minor'
|
||||
|
||||
You can also access the root and quality separately:
|
||||
|
||||
.. code-block:: pycon
|
||||
|
||||
@@ -411,6 +411,10 @@ Acid House Track
|
||||
|
||||
play_score(score)
|
||||
|
||||
.. raw:: html
|
||||
|
||||
<audio controls style="width:100%;margin:0.5em 0 1.5em"><source src="../_static/audio/acid_house.wav" type="audio/wav"></audio>
|
||||
|
||||
Dub Reggae with Delay Madness
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
@@ -443,6 +447,10 @@ Sparse notes into infinite echo:
|
||||
|
||||
play_score(score)
|
||||
|
||||
.. raw:: html
|
||||
|
||||
<audio controls style="width:100%;margin:0.5em 0 1.5em"><source src="../_static/audio/dub_reggae.wav" type="audio/wav"></audio>
|
||||
|
||||
Jazz Ballad with Humanize
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
@@ -480,6 +488,10 @@ The difference between a robot and a musician:
|
||||
|
||||
play_score(score)
|
||||
|
||||
.. raw:: html
|
||||
|
||||
<audio controls style="width:100%;margin:0.5em 0 1.5em"><source src="../_static/audio/jazz_ballad.wav" type="audio/wav"></audio>
|
||||
|
||||
Song with Sections
|
||||
~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
@@ -513,6 +525,10 @@ Define once, arrange freely:
|
||||
play_score(score)
|
||||
score.save_midi("my_song.mid")
|
||||
|
||||
.. raw:: html
|
||||
|
||||
<audio controls style="width:100%;margin:0.5em 0 1.5em"><source src="../_static/audio/song_sections.wav" type="audio/wav"></audio>
|
||||
|
||||
Export Everything to MIDI
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
|
||||
+305
-12
@@ -9,8 +9,8 @@ in Atlanta. Over a dancehall pattern, you're in Kingston. The drums ARE
|
||||
the genre -- they tell the listener's body how to move before a single
|
||||
melodic note is played.
|
||||
|
||||
PyTheory includes a complete drum system -- 27 synthesized percussion
|
||||
sounds, 58 pattern presets across dozens of genres, and 21 fill presets.
|
||||
PyTheory includes a complete drum system -- 51 synthesized percussion
|
||||
sounds, 95+ pattern presets across dozens of genres, and 30 fill presets.
|
||||
Every sound is generated from waveforms; no samples needed.
|
||||
|
||||
Drum Sounds
|
||||
@@ -91,7 +91,7 @@ The ``DrumSound`` enum maps to General MIDI percussion note numbers:
|
||||
>>> DrumSound.CLOSED_HAT.value
|
||||
42
|
||||
|
||||
All 27 sounds, organized by type:
|
||||
All 51 sounds, organized by type:
|
||||
|
||||
**Kicks:** KICK (36)
|
||||
|
||||
@@ -106,7 +106,32 @@ All 27 sounds, organized by type:
|
||||
**Percussion:** COWBELL (56), CLAVE (75), SHAKER (70), TAMBOURINE (54),
|
||||
CONGA_HIGH (63), CONGA_LOW (64), BONGO_HIGH (60), BONGO_LOW (61),
|
||||
TIMBALE_HIGH (65), TIMBALE_LOW (66), AGOGO_HIGH (67), AGOGO_LOW (68),
|
||||
GUIRO (73), MARACAS (70)
|
||||
GUIRO (73)
|
||||
|
||||
**Tabla:** TABLA_NA (86), TABLA_TIN (87), TABLA_GE (88), TABLA_DHA (89),
|
||||
TABLA_TIT (90), TABLA_KE (91), TABLA_GE_BEND (108 -- bayan with upward
|
||||
pitch bend from palm pressing into the head)
|
||||
|
||||
**Dhol:** DHOL_DAGGA (92), DHOL_TILLI (93), DHOL_BOTH (94)
|
||||
|
||||
**Dholak:** DHOLAK_GE (95), DHOLAK_NA (96), DHOLAK_TIT (97)
|
||||
|
||||
**Mridangam:** MRIDANGAM_THAM (98), MRIDANGAM_NAM (99), MRIDANGAM_DIN (100),
|
||||
MRIDANGAM_THA (101)
|
||||
|
||||
**Djembe:** DJEMBE_BASS (102), DJEMBE_TONE (103), DJEMBE_SLAP (104)
|
||||
|
||||
**Cajón:** CAJON_BASS (108), CAJON_SLAP (109), CAJON_TAP (110)
|
||||
|
||||
**Metal Kit:** METAL_KICK (105), METAL_SNARE (106), METAL_HAT (107)
|
||||
|
||||
**Marching Snare:** MARCH_SNARE (115), MARCH_RIMSHOT (116), MARCH_CLICK (118)
|
||||
|
||||
**Quads (Tenors):** QUAD_1 (119), QUAD_2 (120), QUAD_3 (121), QUAD_4 (122),
|
||||
QUAD_SPOCK (123)
|
||||
|
||||
**Marching Bass:** BASS_1 (124), BASS_2 (125), BASS_3 (126), BASS_4 (127),
|
||||
BASS_5 (80)
|
||||
|
||||
Drum Synthesis
|
||||
--------------
|
||||
@@ -145,8 +170,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 +218,16 @@ adds syncopation.
|
||||
rattling hi-hats of trap, the breakneck tempo of drum and bass. These
|
||||
patterns were born in drum machines and they still live there.
|
||||
|
||||
**Metal/Punk:** metal, blast beat, punk -- Speed and aggression.
|
||||
The blast beat is both feet and both hands going as fast as humanly
|
||||
possible. Punk strips everything to its essentials.
|
||||
**Metal/Punk:** metal, blast beat, punk, double kick, metal blast,
|
||||
metal groove, metal gallop -- Speed and aggression. The blast beat is
|
||||
both feet and both hands going as fast as humanly possible. Punk strips
|
||||
everything to its essentials. The metal kit adds 3 dedicated sounds
|
||||
(double kick, china cymbal, stack) and 4 patterns for extreme metal
|
||||
subgenres.
|
||||
|
||||
**World Percussion:** tabla, dhol, dholak, mridangam, djembe, cajón --
|
||||
Deep traditions from across the globe, each with authentic sound sets and
|
||||
idiomatic patterns. See the World Percussion section below for details.
|
||||
|
||||
**Other:** funk, hip hop, bo diddley, second line, new orleans, waltz,
|
||||
12/8 blues, country, gospel, flamenco -- Everything else. The syncopated
|
||||
@@ -217,6 +249,13 @@ Playing Patterns
|
||||
play_pattern(Pattern.preset("salsa"), repeats=4, bpm=180)
|
||||
play_pattern(Pattern.preset("afrobeat"), repeats=8, bpm=110)
|
||||
|
||||
.. raw:: html
|
||||
|
||||
<audio controls style="width:100%;margin:0.3em 0 0.5em"><source src="../_static/audio/rock_beat.wav" type="audio/wav"></audio>
|
||||
<audio controls style="width:100%;margin:0.3em 0 0.5em"><source src="../_static/audio/bossa_nova_pattern.wav" type="audio/wav"></audio>
|
||||
<audio controls style="width:100%;margin:0.3em 0 0.5em"><source src="../_static/audio/salsa_pattern.wav" type="audio/wav"></audio>
|
||||
<audio controls style="width:100%;margin:0.3em 0 1.5em"><source src="../_static/audio/afrobeat_pattern.wav" type="audio/wav"></audio>
|
||||
|
||||
Fills
|
||||
-----
|
||||
|
||||
@@ -228,14 +267,17 @@ ending and a new one is about to begin. Without fills, a drum pattern
|
||||
just loops. With them, it breathes and has structure.
|
||||
|
||||
``Pattern.fill()`` loads a 1-bar drum fill -- a short break that
|
||||
transitions between sections. 21 fill presets are available:
|
||||
transitions between sections. 30 fill presets are available:
|
||||
|
||||
.. code-block:: pycon
|
||||
|
||||
>>> Pattern.list_fills()
|
||||
['afrobeat', 'blast', 'bossa nova', 'breakdown', 'buildup',
|
||||
'cumbia', 'disco', 'funk', 'highlife', 'hip hop', 'house',
|
||||
'jazz', 'jazz brush', 'metal', 'reggae', 'rock', 'rock crash',
|
||||
'cajon breakdown', 'cajon flam', 'cajon rumble',
|
||||
'cumbia', 'disco', 'djembe break', 'djembe call', 'djembe roll',
|
||||
'funk', 'highlife', 'hip hop', 'house',
|
||||
'jazz', 'jazz brush', 'metal', 'metal blast', 'metal cascade',
|
||||
'metal triplet', 'reggae', 'rock', 'rock crash',
|
||||
'salsa', 'samba', 'second line', 'trap']
|
||||
|
||||
>>> fill = Pattern.fill("rock")
|
||||
@@ -304,6 +346,257 @@ drum pattern and all named parts are mixed together by ``play_score()``:
|
||||
|
||||
play_score(score)
|
||||
|
||||
.. raw:: html
|
||||
|
||||
<audio controls style="width:100%;margin:0.5em 0 1.5em"><source src="../_static/audio/salsa_layered.wav" type="audio/wav"></audio>
|
||||
|
||||
World Percussion
|
||||
----------------
|
||||
|
||||
PyTheory includes dedicated sound sets and pattern presets for
|
||||
traditional percussion instruments from around the world. Each
|
||||
instrument has its own synthesized sounds that capture the timbral
|
||||
character of the real instrument, plus idiomatic rhythmic patterns
|
||||
drawn from their musical traditions.
|
||||
|
||||
Tabla
|
||||
~~~~~
|
||||
|
||||
The tabla is a pair of hand drums from the Indian subcontinent -- the
|
||||
smaller, higher-pitched *dayan* and the larger, bass *bayan*. It is
|
||||
the rhythmic backbone of Hindustani classical music, and one of the
|
||||
most expressive percussion instruments ever created. A single tabla
|
||||
player can produce an astonishing range of tones by varying finger
|
||||
placement, pressure, and striking technique.
|
||||
|
||||
**7 sounds** -- covering the primary tabla strokes (na, tin, tun, ge,
|
||||
dha, ke, tit) plus a bayan pitch bend sound (TABLA_GE_BEND) that
|
||||
models the technique of pressing the palm into the bayan head to bend
|
||||
the pitch upward.
|
||||
|
||||
**7 patterns:** teental (16 beats, the most common taal), jhaptaal
|
||||
(10 beats), rupak (7 beats), dadra (6 beats), keherwa (8 beats, folk
|
||||
and light classical), tabla solo, and tiri kita (fast ornamental
|
||||
pattern).
|
||||
|
||||
**5 fills:** tihai (3x crescendo landing on sam), chakkardar (32nd
|
||||
triplet cascade into slam), tiri kita (rapid 16th-note dayan burst),
|
||||
bayan (deep bass bends showcase), tabla call (dayan/bayan call-and-response).
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
score.drums("teental", repeats=4, fill="tihai")
|
||||
score.drums("keherwa", repeats=4, fill="chakkardar")
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
score = Score("4/4", bpm=80)
|
||||
score.drums("teental", repeats=4)
|
||||
|
||||
.. raw:: html
|
||||
|
||||
<audio controls style="width:100%;margin:0.3em 0 0.5em"><source src="../_static/audio/tabla_teental.wav" type="audio/wav"></audio>
|
||||
<audio controls style="width:100%;margin:0.3em 0 0.5em"><source src="../_static/audio/tabla_keherwa.wav" type="audio/wav"></audio>
|
||||
<audio controls style="width:100%;margin:0.3em 0 1.5em"><source src="../_static/audio/tabla_chakradar.wav" type="audio/wav"></audio>
|
||||
|
||||
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)
|
||||
|
||||
.. raw:: html
|
||||
|
||||
<audio controls style="width:100%;margin:0.5em 0 1.5em"><source src="../_static/audio/dhol.wav" type="audio/wav"></audio>
|
||||
|
||||
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)
|
||||
|
||||
.. raw:: html
|
||||
|
||||
<audio controls style="width:100%;margin:0.5em 0 1.5em"><source src="../_static/audio/dholak.wav" type="audio/wav"></audio>
|
||||
|
||||
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)
|
||||
|
||||
.. raw:: html
|
||||
|
||||
<audio controls style="width:100%;margin:0.5em 0 1.5em"><source src="../_static/audio/mridangam.wav" type="audio/wav"></audio>
|
||||
|
||||
Djembe
|
||||
~~~~~~
|
||||
|
||||
The djembe is a rope-tuned goblet drum from West Africa, capable of
|
||||
producing a wide range of tones from deep bass to sharp slaps. It is
|
||||
central to the drum ensemble traditions of Mali, Guinea, and Senegal.
|
||||
|
||||
**3 sounds** -- bass (open center strike), tone (edge strike), and
|
||||
slap (sharp edge strike).
|
||||
|
||||
**8 patterns:** djembe (basic accompanying rhythm), kuku (Guinean harvest
|
||||
dance), soli (powerful Mandinka rhythm), dununba (heavy bass-driven),
|
||||
tiriba (joyful Susu rhythm), yankadi (gentle greeting/welcome), djansa
|
||||
(fast Malinke dance), mendiani (women's celebratory dance).
|
||||
|
||||
**3 fills:** djembe call (bass-tone-slap conversation building to climax),
|
||||
djembe roll (rapid slaps accelerating into bass), djembe break (syncopated
|
||||
West African-style break).
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
score = Score("4/4", bpm=120)
|
||||
score.drums("djembe", repeats=8, fill="djembe call", fill_every=4)
|
||||
|
||||
.. raw:: html
|
||||
|
||||
<audio controls style="width:100%;margin:0.5em 0 1.5em"><source src="../_static/audio/djembe.wav" type="audio/wav"></audio>
|
||||
|
||||
Metal Kit
|
||||
~~~~~~~~~
|
||||
|
||||
A dedicated percussion kit for extreme metal subgenres, with
|
||||
specialized sounds and patterns that go beyond the standard drum kit.
|
||||
|
||||
**3 sounds** -- double kick (triggered, tight attack), china cymbal,
|
||||
and stack (a short, trashy cymbal choke).
|
||||
|
||||
**4 patterns:** double kick (relentless double bass drum pattern),
|
||||
metal blast (blast beat with china cymbal accents), metal groove (a
|
||||
half-time groove with double kick fills), and metal gallop (the
|
||||
classic triplet-feel gallop rhythm).
|
||||
|
||||
**4 fills:** metal (double kick 16ths with descending toms), metal triplet
|
||||
(double kick triplets with snare accents), metal blast (alternating
|
||||
snare/kick 32nds into half-time crash), metal cascade (descending snare
|
||||
roll → kick roll → alternating → crash ending).
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
score = Score("4/4", bpm=200)
|
||||
score.drums("metal blast", repeats=8, fill="metal cascade", fill_every=4)
|
||||
|
||||
.. raw:: html
|
||||
|
||||
<audio controls style="width:100%;margin:0.5em 0 1.5em"><source src="../_static/audio/metal_blast.wav" type="audio/wav"></audio>
|
||||
|
||||
Cajón
|
||||
~~~~~
|
||||
|
||||
The cajón is a box-shaped percussion instrument from Peru, now
|
||||
ubiquitous in acoustic and unplugged settings worldwide. Players sit
|
||||
on the box and strike the front face with their hands.
|
||||
|
||||
**3 sounds** -- bass (deep center thump), slap (sharp, snare-like edge
|
||||
hit with wire buzz), and tap (light finger tap).
|
||||
|
||||
**3 patterns:** cajon (basic groove), cajon rumba (flamenco-style rumba),
|
||||
and cajon folk (folk/acoustic pattern).
|
||||
|
||||
**3 fills:** cajon flam (slaps accelerating into bass hits), cajon rumble
|
||||
(fast taps building to slap accents), cajon breakdown (syncopated
|
||||
bass-slap groove).
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
score = Score("4/4", bpm=100)
|
||||
score.drums("cajon", repeats=8, fill="cajon flam", fill_every=4)
|
||||
|
||||
.. raw:: html
|
||||
|
||||
<audio controls style="width:100%;margin:0.5em 0 1.5em"><source src="../_static/audio/cajon.wav" type="audio/wav"></audio>
|
||||
|
||||
Marching Percussion
|
||||
~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
A full drumline — snare, quads (tenors), and pitched bass drums.
|
||||
Every sound is synthesized: kevlar snare heads, aluminum shell ting
|
||||
on the quads, felt-beater thwack on the basses.
|
||||
|
||||
**Snare** -- 3 sounds: MARCH_SNARE (tight kevlar tap), MARCH_RIMSHOT
|
||||
(woody-metallic crack), MARCH_CLICK (stick click for count-offs).
|
||||
|
||||
**Quads** -- 5 sounds: QUAD_1 through QUAD_4 (high to low pitched
|
||||
tenors) plus QUAD_SPOCK (rim click on the shell).
|
||||
|
||||
**Bass drums** -- 5 pitched drums: BASS_1 (highest/smallest) through
|
||||
BASS_5 (lowest/biggest), each with a prominent felt-beater thwack.
|
||||
|
||||
**6 patterns:** march (basic 4/4), cadence (8-beat street beat),
|
||||
march paradiddle, march roll (buzz crescendo), quad sweep (run across
|
||||
all 4 drums), quad groove, bass split (cascading across the line),
|
||||
bass unison (all 5 hit together), drumline (snare + quads + bass).
|
||||
|
||||
**Rudiment methods:** ``Part.flam()``, ``Part.diddle()``, and
|
||||
``Part.cheese()`` for marching rudiments on any drum sound.
|
||||
|
||||
**Ensemble rendering:** ``ensemble=N`` on any Part duplicates the
|
||||
voice with per-player timing tendencies and micro pitch drift.
|
||||
``ensemble=8`` for a snare line, ``ensemble=20`` for a massive section.
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
# Full drumline with ensemble
|
||||
snares = score.part("snares", synth="sine", volume=0.9,
|
||||
reverb=0.2, ensemble=8)
|
||||
quads = score.part("quads", synth="sine", volume=0.5,
|
||||
reverb=0.2, ensemble=4)
|
||||
basses = score.part("basses", synth="sine", volume=0.55,
|
||||
reverb=0.2, ensemble=5)
|
||||
|
||||
snares.flam(DrumSound.MARCH_SNARE, Duration.QUARTER, velocity=120)
|
||||
snares.diddle(DrumSound.MARCH_SNARE, Duration.EIGHTH, velocity=60)
|
||||
|
||||
# Or use patterns
|
||||
score.drums("drumline", repeats=4)
|
||||
|
||||
.. raw:: html
|
||||
|
||||
<audio controls style="width:100%;margin:0.5em 0 1.5em"><source src="../_static/audio/march_snare.wav" type="audio/wav"></audio>
|
||||
|
||||
**Sympathetic resonance:** The marching snare builds up snare wire
|
||||
buzz as hits accumulate, and the buzz decays during rests — just like
|
||||
a real drum.
|
||||
|
||||
MIDI Export
|
||||
-----------
|
||||
|
||||
|
||||
+96
-10
@@ -32,8 +32,8 @@ It's a well-tested order that sounds good by default.
|
||||
|
||||
Effects are applied in this fixed order::
|
||||
|
||||
Signal --> Saturation --> Tremolo --> Distortion --> Chorus --> Phaser
|
||||
--> Highpass --> Lowpass --> Delay --> Reverb --> Mix
|
||||
Signal --> Saturation --> Tremolo --> Distortion --> Cabinet --> Chorus
|
||||
--> Phaser --> Highpass --> Lowpass --> Delay --> Reverb --> Mix
|
||||
|
||||
Additionally, these per-note effects are applied before the part effects chain:
|
||||
|
||||
@@ -47,11 +47,12 @@ Part-level effects:
|
||||
- **Saturation** first: subtle even-harmonic warmth (tape/tube color).
|
||||
- **Tremolo** second: amplitude LFO modulation.
|
||||
- **Distortion** third: drives the signal before filtering.
|
||||
- **Chorus** fourth: thickens the signal.
|
||||
- **Phaser** fifth: swept allpass notches.
|
||||
- **Highpass** sixth: removes low-frequency mud.
|
||||
- **Lowpass** seventh: shapes the tone (like a tone knob on an amp).
|
||||
- **Delay** eighth: echoes the shaped signal (tap delay / tape echo).
|
||||
- **Cabinet** fourth: speaker cab simulation (rolloff + presence bump).
|
||||
- **Chorus** fifth: thickens the signal.
|
||||
- **Phaser** sixth: swept allpass notches.
|
||||
- **Highpass** seventh: removes low-frequency mud.
|
||||
- **Lowpass** eighth: shapes the tone (like a tone knob on an amp).
|
||||
- **Delay** ninth: echoes the shaped signal (tap delay / tape echo).
|
||||
- **Reverb** last: places everything in a space (room / hall).
|
||||
|
||||
Distortion
|
||||
@@ -96,6 +97,89 @@ Parameters:
|
||||
distortion_drive=10.0,
|
||||
)
|
||||
|
||||
Cabinet Simulation
|
||||
------------------
|
||||
|
||||
A real guitar amp doesn't just distort the signal -- the speaker
|
||||
cabinet shapes the tone dramatically. A 12-inch speaker in a closed
|
||||
cabinet rolls off the harsh high frequencies above 5 kHz and adds a
|
||||
presence bump around 2--3 kHz that gives the sound its "in the room"
|
||||
quality. Without a cabinet, distortion sounds thin and fizzy. With
|
||||
one, it sounds like a real amp.
|
||||
|
||||
PyTheory's cabinet simulation applies a speaker rolloff curve (lowpass
|
||||
at ~5 kHz) combined with a presence resonance bump, placed in the
|
||||
signal chain immediately after distortion -- exactly where it sits in
|
||||
a real amp.
|
||||
|
||||
Parameters:
|
||||
|
||||
- ``cabinet``: Wet/dry mix, 0.0--1.0 (default 0, off).
|
||||
|
||||
- 0.3--0.5 = subtle speaker coloring
|
||||
- 0.6--0.8 = classic amp-in-a-room
|
||||
- 1.0 = full cabinet, no dry signal
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
# Classic rock amp tone: distortion into cabinet
|
||||
guitar = score.part(
|
||||
"guitar",
|
||||
synth="saw",
|
||||
envelope="pluck",
|
||||
distortion=0.6,
|
||||
distortion_drive=5.0,
|
||||
cabinet=0.8,
|
||||
)
|
||||
|
||||
# Clean amp with just cabinet warmth (no distortion)
|
||||
clean = score.part(
|
||||
"clean",
|
||||
synth="triangle",
|
||||
envelope="pluck",
|
||||
cabinet=0.5,
|
||||
)
|
||||
|
||||
Analog Drift
|
||||
------------
|
||||
|
||||
Real analog synthesizers are never perfectly in tune. The voltage-
|
||||
controlled oscillators drift slightly over time as components warm up
|
||||
and temperature fluctuates. This imperfection is actually a big part
|
||||
of why vintage analog synths sound so appealing -- the subtle pitch
|
||||
wandering gives each note a unique, living quality that static digital
|
||||
oscillators lack.
|
||||
|
||||
The ``analog_drift`` parameter adds slow, random pitch variation to
|
||||
each oscillator, modeling this vintage behavior.
|
||||
|
||||
Parameters:
|
||||
|
||||
- ``analog_drift``: Drift amount, 0.0--1.0 (default 0, off).
|
||||
|
||||
- 0.05--0.1 = subtle warmth (studio-grade analog)
|
||||
- 0.15--0.25 = noticeable drift (vintage gear warming up)
|
||||
- 0.3+ = unstable, wobbly (broken tape machine)
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
# Warm vintage pad
|
||||
pad = score.part(
|
||||
"pad",
|
||||
synth="supersaw",
|
||||
envelope="pad",
|
||||
analog_drift=0.1,
|
||||
chorus=0.3,
|
||||
)
|
||||
|
||||
# Lo-fi detuned lead
|
||||
lead = score.part(
|
||||
"lead",
|
||||
synth="saw",
|
||||
envelope="pluck",
|
||||
analog_drift=0.25,
|
||||
)
|
||||
|
||||
Chorus
|
||||
------
|
||||
|
||||
@@ -757,9 +841,11 @@ processes each section independently:
|
||||
lead.arpeggio("Gm", bars=4, pattern="updown", octaves=2)
|
||||
|
||||
Any parameter can be automated: ``lowpass``, ``lowpass_q``, ``highpass``,
|
||||
``reverb``, ``reverb_decay``, ``delay``, ``delay_time``, ``delay_feedback``,
|
||||
``distortion``, ``distortion_drive``, ``chorus``, ``phaser``, ``phaser_rate``,
|
||||
``saturation``, ``tremolo_depth``, ``tremolo_rate``, ``volume``.
|
||||
``reverb``, ``reverb_decay``, ``reverb_type``, ``delay``, ``delay_time``,
|
||||
``delay_feedback``, ``distortion``, ``distortion_drive``, ``chorus``,
|
||||
``phaser``, ``phaser_rate``, ``saturation``, ``tremolo_depth``,
|
||||
``tremolo_rate``, ``cabinet``, ``cabinet_brightness``, ``analog_drift``,
|
||||
``volume``.
|
||||
|
||||
LFO Automation
|
||||
--------------
|
||||
|
||||
+16
-1
@@ -66,6 +66,21 @@ the mix louder and punchier:
|
||||
chords.add(Chord.from_symbol(sym), Duration.WHOLE)
|
||||
play_score(score)
|
||||
|
||||
.. raw:: html
|
||||
|
||||
<audio controls style="width:100%;margin:0.5em 0 1.5em"><source src="../_static/audio/playback_basic.wav" type="audio/wav"></audio>
|
||||
|
||||
The render pipeline respects the Score's ``temperament`` and
|
||||
``reference_pitch`` settings, so Baroque or microtonal scores play back
|
||||
at the correct tuning:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
score = Score("4/4", bpm=80, temperament="meantone", reference_pitch=415.0)
|
||||
|
||||
Press **Ctrl+C** at any time during playback to stop — PyTheory catches
|
||||
``KeyboardInterrupt`` and stops audio cleanly.
|
||||
|
||||
See :doc:`sequencing` for how to build scores and parts.
|
||||
|
||||
render_score() -- Headless Rendering
|
||||
@@ -153,7 +168,7 @@ Play a drum pattern through the speakers:
|
||||
play_pattern(Pattern.preset("rock"), repeats=4, bpm=120)
|
||||
play_pattern(Pattern.preset("bossa nova"), repeats=4, bpm=140)
|
||||
|
||||
See :doc:`drums` for the full list of 58 presets and 21 fills.
|
||||
See :doc:`drums` for the full list of 80+ presets and 21 fills.
|
||||
|
||||
play_progression() -- Quick Chord Playback
|
||||
------------------------------------------
|
||||
|
||||
@@ -185,6 +185,10 @@ chords, melody, bass, each with their own synth and effects:
|
||||
|
||||
play_score(score)
|
||||
|
||||
.. raw:: html
|
||||
|
||||
<audio controls style="width:100%;margin:0.5em 0 1.5em"><source src="../_static/audio/quickstart.wav" type="audio/wav"></audio>
|
||||
|
||||
Export to Your DAW
|
||||
------------------
|
||||
|
||||
|
||||
+391
-41
@@ -47,6 +47,18 @@ A ``Duration`` represents a note length in beats (quarter note = 1 beat):
|
||||
>>> Duration.TRIPLET_QUARTER.value
|
||||
0.6666666666666666
|
||||
|
||||
Duration supports arithmetic — multiply, divide, and add to create
|
||||
compound durations:
|
||||
|
||||
.. code-block:: pycon
|
||||
|
||||
>>> Duration.WHOLE * 2
|
||||
8.0
|
||||
>>> Duration.HALF + Duration.QUARTER
|
||||
3.0
|
||||
>>> Duration.WHOLE / 2
|
||||
2.0
|
||||
|
||||
Time Signatures
|
||||
---------------
|
||||
|
||||
@@ -149,6 +161,10 @@ Chords work just like tones — pass any ``Chord`` object:
|
||||
for chord in chords:
|
||||
score.add(chord, Duration.WHOLE)
|
||||
|
||||
.. raw:: html
|
||||
|
||||
<audio controls style="width:100%;margin:0.5em 0 1.5em"><source src="../_static/audio/chords_basic.wav" type="audio/wav"></audio>
|
||||
|
||||
.. code-block:: pycon
|
||||
|
||||
>>> score.measures
|
||||
@@ -232,6 +248,31 @@ Chords and Tone objects work the same way:
|
||||
for note in ["A2", "C3", "E3", "A2", "D2", "F2", "A2", "D2"]:
|
||||
bass.add(note, Duration.QUARTER)
|
||||
|
||||
Polyphonic Hold
|
||||
---------------
|
||||
|
||||
``Part.hold()`` adds a note without advancing the beat position —
|
||||
the next note starts at the *same* time. This enables polyphonic
|
||||
overlap on a single part: piano sustain, sitar drone under melody,
|
||||
guitar strum texture.
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
piano = score.part("piano", instrument="piano", reverb=0.3)
|
||||
|
||||
# Hold a C major chord for 8 beats
|
||||
piano.hold("C3", Duration.WHOLE * 2, velocity=60)
|
||||
piano.hold("E3", Duration.WHOLE * 2, velocity=55)
|
||||
piano.hold("G3", Duration.WHOLE * 2, velocity=55)
|
||||
|
||||
# Melody plays simultaneously on top
|
||||
for n in ["E4", "G4", "C5", "G4", "E4", "D4", "C4", "E4"]:
|
||||
piano.add(n, Duration.QUARTER, velocity=80)
|
||||
|
||||
.. raw:: html
|
||||
|
||||
<audio controls style="width:100%;margin:0.5em 0 1.5em"><source src="../_static/audio/piano_hold.wav" type="audio/wav"></audio>
|
||||
|
||||
Arpeggiator
|
||||
------------
|
||||
|
||||
@@ -280,6 +321,10 @@ Chain arpeggios through a progression:
|
||||
for sym in ["Cm", "Fm", "Abm", "Gm"]:
|
||||
lead.arpeggio(sym, bars=2, pattern="updown", octaves=2)
|
||||
|
||||
.. raw:: html
|
||||
|
||||
<audio controls style="width:100%;margin:0.5em 0 1.5em"><source src="../_static/audio/arpeggio.wav" type="audio/wav"></audio>
|
||||
|
||||
Combined with legato, glide, distortion, and a resonant lowpass, this
|
||||
produces the classic acid/trance arpeggiator sound.
|
||||
|
||||
@@ -310,12 +355,18 @@ portamento (pitch slides between notes):
|
||||
acid = score.part(
|
||||
"acid",
|
||||
synth="saw",
|
||||
envelope="pad",
|
||||
legato=True,
|
||||
glide=0.04,
|
||||
lowpass=3000,
|
||||
lowpass_q=6.0,
|
||||
distortion=0.3,
|
||||
)
|
||||
acid.add("C2", 0.25).add("C3", 0.25).add("G2", 0.25).add("C2", 0.25)
|
||||
|
||||
.. raw:: html
|
||||
|
||||
<audio controls style="width:100%;margin:0.5em 0 1.5em"><source src="../_static/audio/legato_glide.wav" type="audio/wav"></audio>
|
||||
|
||||
- ``legato``: If True, no envelope retrigger between notes (default False).
|
||||
- ``glide``: Portamento time in seconds (default 0, instant).
|
||||
0.03--0.05 = quick 303 slide, 0.1--0.2 = slow glide.
|
||||
@@ -323,63 +374,51 @@ portamento (pitch slides between notes):
|
||||
Complete Example
|
||||
----------------
|
||||
|
||||
A full multi-part arrangement built from scratch — bossa nova with FM
|
||||
rhodes, triangle lead, and filtered bass:
|
||||
A full multi-part arrangement — rock beat with piano chords, saw
|
||||
lead, and filtered bass:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
from pytheory import Score, Pattern, Key, Duration, Chord
|
||||
from pytheory import Score, Key, Duration, Chord
|
||||
from pytheory.play import play_score
|
||||
|
||||
score = Score("4/4", bpm=140)
|
||||
score.drums("bossa nova", repeats=4)
|
||||
score = Score("4/4", bpm=120)
|
||||
score.drums("rock", repeats=8, fill="rock", fill_every=4)
|
||||
|
||||
# FM rhodes with reverb
|
||||
rhodes = score.part(
|
||||
"rhodes",
|
||||
synth="fm",
|
||||
envelope="piano",
|
||||
volume=0.3,
|
||||
reverb=0.4,
|
||||
reverb_decay=1.8,
|
||||
)
|
||||
# Piano chords with reverb
|
||||
piano = score.part("piano", instrument="piano", volume=0.4, reverb=0.3)
|
||||
|
||||
# Triangle lead with delay
|
||||
# Saw lead with delay
|
||||
lead = score.part(
|
||||
"lead",
|
||||
synth="triangle",
|
||||
envelope="pluck",
|
||||
volume=0.45,
|
||||
delay=0.25,
|
||||
delay_time=0.32,
|
||||
delay_feedback=0.35,
|
||||
reverb=0.2,
|
||||
"lead", synth="saw", envelope="pluck", volume=0.4,
|
||||
delay=0.2, delay_time=0.33, reverb=0.2, lowpass=3000,
|
||||
)
|
||||
|
||||
# Filtered bass
|
||||
bass = score.part(
|
||||
"bass",
|
||||
synth="sine",
|
||||
envelope="pluck",
|
||||
volume=0.45,
|
||||
lowpass=600,
|
||||
)
|
||||
bass = score.part("bass", synth="triangle", envelope="pluck",
|
||||
volume=0.45, lowpass=600)
|
||||
|
||||
for sym in ["Am", "Am", "Dm", "Dm", "E7", "E7", "Am", "Am"]:
|
||||
rhodes.add(Chord.from_symbol(sym), Duration.WHOLE)
|
||||
for chord in Key("G", "major").progression("I", "V", "vi", "IV") * 2:
|
||||
piano.add(chord, Duration.WHOLE)
|
||||
|
||||
for n, d in [
|
||||
("E5", 0.67), ("D5", 0.33), ("C5", 0.67), ("B4", 0.33),
|
||||
("A4", 1), ("C5", 0.67), ("E5", 0.33), ("D5", 0.67), ("C5", 0.33),
|
||||
("A4", 1),
|
||||
]:
|
||||
lead.add(n, d)
|
||||
lead.add("D5", 1).add("B4", 0.5).add("D5", 0.5)
|
||||
lead.add("G5", 1).add("E5", 1)
|
||||
lead.add("D5", 0.5).add("B4", 0.5).add("A4", 1)
|
||||
lead.add("G4", 2).rest(2)
|
||||
lead.add("D5", 1).add("B4", 0.5).add("D5", 0.5)
|
||||
lead.add("G5", 1).add("A5", 1)
|
||||
lead.add("G5", 0.5).add("E5", 0.5).add("D5", 1)
|
||||
lead.add("B4", 2).rest(2)
|
||||
|
||||
for n in ["A2", "E2", "A2", "C3", "D2", "A2", "D2", "F2"]:
|
||||
bass.add(n, Duration.QUARTER)
|
||||
for n in ["G2", "G2", "D2", "D2", "E2", "E2", "C2", "C2"] * 2:
|
||||
bass.add(n, Duration.HALF)
|
||||
|
||||
play_score(score)
|
||||
|
||||
.. raw:: html
|
||||
|
||||
<audio controls style="width:100%;margin:0.5em 0 1.5em"><source src="../_static/audio/complete_rock.wav" type="audio/wav"></audio>
|
||||
|
||||
Velocity
|
||||
--------
|
||||
|
||||
@@ -399,6 +438,157 @@ The arpeggiator also accepts velocity:
|
||||
|
||||
lead.arpeggio("Am", bars=2, pattern="up", velocity=80)
|
||||
|
||||
Articulations
|
||||
-------------
|
||||
|
||||
Articulations change *how* a note is played — its attack, duration, and
|
||||
weight. A staccato note is short and bouncy. A marcato note hits hard.
|
||||
A legato note melts into the next one. This is the difference between
|
||||
a melody that sounds like a MIDI file and one that sounds like a
|
||||
musician played it.
|
||||
|
||||
Pass ``articulation=`` to ``Part.add()``:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
piano.add("C4", Duration.QUARTER, articulation="staccato") # short, bouncy
|
||||
piano.add("D4", Duration.QUARTER, articulation="legato") # smooth, overlaps
|
||||
piano.add("E4", Duration.QUARTER, articulation="marcato") # heavy accent
|
||||
piano.add("F4", Duration.QUARTER, articulation="tenuto") # held, soft attack
|
||||
piano.add("G4", Duration.QUARTER, articulation="accent") # louder
|
||||
piano.add("C5", Duration.HALF, articulation="fermata") # held longer
|
||||
|
||||
.. raw:: html
|
||||
|
||||
<audio controls style="width:100%;margin:0.5em 0 1.5em"><source src="../_static/audio/articulations.wav" type="audio/wav"></audio>
|
||||
|
||||
What each articulation does:
|
||||
|
||||
- **staccato** — plays ~40% of the note duration with a quick fade-out. Short and detached.
|
||||
- **legato** — extends ~15% into the next note. Smooth and connected.
|
||||
- **marcato** — 25% velocity boost + sharper attack. Heavy and accented.
|
||||
- **tenuto** — full duration with a softer attack ramp. Held and deliberate.
|
||||
- **accent** — 20% velocity boost, no duration change.
|
||||
- **fermata** — stretches the note 50% longer.
|
||||
|
||||
Articulations work on ``Part.hold()`` and ``Part.hit()`` too.
|
||||
|
||||
Dynamic Curves
|
||||
--------------
|
||||
|
||||
Real music breathes — phrases get louder, get quieter, swell and
|
||||
recede. Dynamic curves let you shape the velocity across a sequence
|
||||
of notes instead of setting each one manually.
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
# Crescendo: quiet to loud
|
||||
piano.crescendo(["C4","D4","E4","F4","G4","A4","B4","C5"],
|
||||
Duration.QUARTER, start_vel=30, end_vel=110)
|
||||
|
||||
# Decrescendo: loud to quiet
|
||||
piano.decrescendo(["C5","B4","A4","G4","F4","E4","D4","C4"],
|
||||
Duration.QUARTER, start_vel=110, end_vel=30)
|
||||
|
||||
# Swell: up then back down (orchestral < > shape)
|
||||
strings.swell(["C4","D4","E4","F4","G4","F4","E4","D4"],
|
||||
Duration.QUARTER, low_vel=35, peak_vel=110)
|
||||
|
||||
# Custom curve: explicit velocity per note
|
||||
piano.dynamics(["C4","E4","G4","C5"], Duration.QUARTER,
|
||||
velocities=[50, 80, 110, 90])
|
||||
|
||||
.. raw:: html
|
||||
|
||||
<audio controls style="width:100%;margin:0.5em 0 1.5em"><source src="../_static/audio/dynamics.wav" type="audio/wav"></audio>
|
||||
|
||||
Four methods:
|
||||
|
||||
- **crescendo()** — linear velocity ramp from ``start_vel`` to ``end_vel``.
|
||||
- **decrescendo()** — same thing, but typically loud to quiet.
|
||||
- **swell()** — ramps up to the midpoint, then back down. The classic
|
||||
orchestral crescendo-decrescendo.
|
||||
- **dynamics()** — the general form. Pass a ``(start, end)`` tuple for
|
||||
a linear ramp, or a list of velocities for a custom curve.
|
||||
|
||||
All four accept ``articulation=`` to combine dynamics with articulations:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
# Staccato crescendo — bouncy notes getting louder
|
||||
piano.crescendo(["C4","E4","G4","C5","E5","G5","C6","E6"],
|
||||
Duration.EIGHTH, start_vel=40, end_vel=110,
|
||||
articulation="staccato")
|
||||
|
||||
Part.hit() — Manual Drum Placement
|
||||
-----------------------------------
|
||||
|
||||
The pattern system is great for grooves, but sometimes you want to
|
||||
place individual drum hits with full control — articulations, effects,
|
||||
and all. ``Part.hit()`` puts a drum sound into a Part's note stream:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
from pytheory import DrumSound
|
||||
|
||||
kit = score.part("kit", synth="sine", volume=0.7)
|
||||
|
||||
kit.hit(DrumSound.KICK, Duration.QUARTER, articulation="accent")
|
||||
kit.hit(DrumSound.CLOSED_HAT, Duration.EIGHTH, velocity=60)
|
||||
kit.hit(DrumSound.SNARE, Duration.EIGHTH, articulation="marcato")
|
||||
|
||||
Because hits go through the normal Part renderer, they get humanize,
|
||||
effects, and articulations for free. Use this for custom beats that
|
||||
don't fit a preset pattern, or for one-shot accent hits layered on
|
||||
top of a pattern.
|
||||
|
||||
Rudiments — Flam, Diddle, Cheese
|
||||
---------------------------------
|
||||
|
||||
Marching percussion rudiments as methods on any Part:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
from pytheory import DrumSound
|
||||
|
||||
p = score.part("snares", synth="sine", volume=0.9)
|
||||
|
||||
# Flam: grace note + main hit (gap controls tightness)
|
||||
p.flam(DrumSound.MARCH_SNARE, Duration.QUARTER, velocity=120)
|
||||
|
||||
# Diddle: two equal strokes in one note duration
|
||||
p.diddle(DrumSound.MARCH_SNARE, Duration.EIGHTH, velocity=60)
|
||||
|
||||
# Cheese: flam + diddle combined
|
||||
p.cheese(DrumSound.MARCH_SNARE, Duration.QUARTER, velocity=120)
|
||||
|
||||
Ensemble
|
||||
--------
|
||||
|
||||
Any Part can be rendered as an ensemble — multiple players with
|
||||
per-player timing tendencies and micro pitch drift:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
# 8-player snare line
|
||||
snares = score.part("snares", synth="sine", volume=0.9, ensemble=8)
|
||||
|
||||
# 20-player string section
|
||||
strings = score.part("strings", instrument="string_ensemble", ensemble=20)
|
||||
|
||||
# Single player (default)
|
||||
solo = score.part("solo", instrument="violin")
|
||||
|
||||
Each ensemble voice gets a consistent timing personality (some rush,
|
||||
some drag) plus small per-note wobble, and slightly different tuning.
|
||||
The result sounds like a real section — together but alive.
|
||||
|
||||
Solo snare, then an 8-player section plays the same pattern:
|
||||
|
||||
.. raw:: html
|
||||
|
||||
<audio controls style="width:100%;margin:0.5em 0 1.5em"><source src="../_static/audio/ensemble.wav" type="audio/wav"></audio>
|
||||
|
||||
Swing and Groove
|
||||
----------------
|
||||
|
||||
@@ -478,6 +668,54 @@ integrate naturally with the rest of the automation system:
|
||||
pad.rest(Duration.WHOLE)
|
||||
pad.rest(Duration.WHOLE)
|
||||
|
||||
Parameter Ramps
|
||||
---------------
|
||||
|
||||
Fades only control volume. ``Part.ramp()`` smoothly sweeps *any*
|
||||
parameter from its current value to a target — filters, reverb,
|
||||
distortion, chorus, delay, anything ``.set()`` accepts. This is how
|
||||
you build filter sweeps, gradual effect sends, and EDM buildups.
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
lead = score.part("lead", synth="saw", lowpass=200, lowpass_q=3.0)
|
||||
|
||||
# Open the filter over 8 bars
|
||||
lead.ramp(over=Duration.WHOLE * 8, lowpass=8000)
|
||||
|
||||
# Ramp multiple params at once
|
||||
pad.ramp(over=Duration.WHOLE * 4, reverb=0.5, chorus=0.3)
|
||||
|
||||
# Close the filter with distortion fading in
|
||||
lead.ramp(over=Duration.WHOLE * 4, lowpass=400, distortion=0.5)
|
||||
|
||||
Four interpolation curves:
|
||||
|
||||
- **linear** — constant rate of change (default).
|
||||
- **ease_in** — starts slow, accelerates. Good for buildups.
|
||||
- **ease_out** — starts fast, decelerates. Good for releases.
|
||||
- **ease_in_out** — slow at both ends. Smooth and natural.
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
# EDM buildup: slow start, accelerating filter sweep
|
||||
lead.ramp(over=Duration.WHOLE * 8, curve="ease_in", lowpass=8000)
|
||||
|
||||
# Smooth reverb wash fading in and settling
|
||||
pad.ramp(over=Duration.WHOLE * 4, curve="ease_in_out", reverb=0.6)
|
||||
|
||||
.. raw:: html
|
||||
|
||||
<audio controls style="width:100%;margin:0.5em 0 1.5em"><source src="../_static/audio/filter_ramp.wav" type="audio/wav"></audio>
|
||||
|
||||
``ramp()`` generates automation points every quarter-beat by default.
|
||||
Set ``resolution=0.125`` for smoother curves (every 32nd note), or
|
||||
``resolution=1.0`` for lighter automation (every beat).
|
||||
|
||||
Combine with ``lfo()`` for cyclic modulation and ``ramp()`` for
|
||||
one-shot sweeps — together they cover the full range of parameter
|
||||
automation.
|
||||
|
||||
Humanize
|
||||
--------
|
||||
|
||||
@@ -574,3 +812,115 @@ Define sections with ``score.section()`` and repeat them with
|
||||
Use any names you want — ``"intro"``, ``"verse"``, ``"chorus"``,
|
||||
``"bridge"``, ``"drop"``, ``"breakdown"``, ``"outro"``, or anything
|
||||
that makes sense for your song. The names are just labels.
|
||||
|
||||
Guitar Strumming
|
||||
----------------
|
||||
|
||||
Any part with a fretboard can strum chords using real fingering
|
||||
positions. The ``strum()`` method looks up the chord on the fretboard,
|
||||
gets the correct voicing, and plays all strings as a chord.
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
from pytheory import Fretboard
|
||||
|
||||
guitar = score.part("guitar", instrument="acoustic_guitar",
|
||||
fretboard=Fretboard.guitar())
|
||||
|
||||
guitar.strum("Am", Duration.HALF, direction="down")
|
||||
guitar.strum("G", Duration.HALF, direction="up")
|
||||
guitar.strum("F", Duration.WHOLE)
|
||||
|
||||
Works with any fretboard instrument — guitar, ukulele, banjo, mandolin.
|
||||
Works with any guitar preset — clean, crunch, distorted, orange, metal.
|
||||
|
||||
Pitch Bends
|
||||
-----------
|
||||
|
||||
Bend a note's pitch up or down over its duration. Essential for guitar
|
||||
bends, sitar meends, trombone slides, and vocal-style expression.
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
# Guitar bend: D up to E (2 semitones)
|
||||
guitar.add("D4", Duration.HALF, bend=2, bend_type="smooth")
|
||||
|
||||
# Release bend: E back down to D
|
||||
guitar.add("E4", Duration.HALF, bend=-2)
|
||||
|
||||
# Blues curl: hold then bend at the end
|
||||
guitar.add("C4", Duration.HALF, bend=1, bend_type="late")
|
||||
|
||||
Three bend types:
|
||||
|
||||
- ``"smooth"`` — logarithmic (default). Perceptually even pitch change.
|
||||
- ``"linear"`` — linear frequency interpolation. Mechanical/synth feel.
|
||||
- ``"late"`` — holds the starting pitch for 60%, bends in the last 40%.
|
||||
The classic blues "curl."
|
||||
|
||||
Rolls
|
||||
-----
|
||||
|
||||
Rapid repeated notes with a velocity ramp — perfect for timpani
|
||||
rolls, snare rolls, tremolo on any instrument. The velocity ramps
|
||||
from ``velocity_start`` to ``velocity_end`` for crescendo or
|
||||
decrescendo effects.
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
# Timpani crescendo roll
|
||||
timp = score.part("timp", instrument="timpani")
|
||||
timp.roll("C3", Duration.WHOLE, velocity_start=20, velocity_end=110)
|
||||
timp.add("C3", Duration.HALF, velocity=127) # big accent
|
||||
|
||||
# Snare roll with 32nd notes
|
||||
snare = score.part("snare", synth="noise", envelope="pluck")
|
||||
snare.roll("C4", Duration.HALF, speed=0.125,
|
||||
velocity_start=40, velocity_end=100)
|
||||
|
||||
# Decrescendo (loud to quiet)
|
||||
timp.roll("G2", Duration.WHOLE, velocity_start=100, velocity_end=30)
|
||||
|
||||
Parameters:
|
||||
|
||||
- ``velocity_start``: Starting velocity (default 40).
|
||||
- ``velocity_end``: Ending velocity (default 100).
|
||||
- ``speed``: Note subdivision (default ``Duration.SIXTEENTH``).
|
||||
Use ``0.125`` for 32nd notes, ``Duration.EIGHTH`` for 8th notes.
|
||||
|
||||
Tuning Systems
|
||||
--------------
|
||||
|
||||
A Score can use any tuning system and temperament:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
# Baroque harpsichord — meantone tuning, A=415
|
||||
score = Score("4/4", bpm=80, temperament="meantone",
|
||||
reference_pitch=415.0)
|
||||
|
||||
# Indian classical — 22-shruti system
|
||||
score = Score("4/4", bpm=75, system="shruti")
|
||||
|
||||
# Just intonation — pure intervals
|
||||
score = Score("4/4", bpm=90, temperament="just")
|
||||
|
||||
The Score constructor accepts these tuning parameters:
|
||||
|
||||
- ``system``: Musical system name (default ``"western"``). Any system
|
||||
from :doc:`systems` works — ``"indian"``, ``"shruti"``, ``"maqam"``,
|
||||
``"carnatic"``, etc. Note strings in ``Part.add()`` are parsed against
|
||||
this system.
|
||||
- ``temperament``: Tuning temperament — ``"equal"`` (default),
|
||||
``"pythagorean"``, ``"meantone"``, ``"just"``.
|
||||
- ``reference_pitch``: Concert pitch in Hz (default 440.0). Use 415.0
|
||||
for Baroque tuning, 432.0 for "Verdi tuning", etc.
|
||||
|
||||
Custom equal temperaments via the ``TET()`` factory:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
from pytheory import TET
|
||||
|
||||
edo19 = TET(19) # 19-tone equal temperament
|
||||
score = Score("4/4", bpm=100, system=edo19)
|
||||
|
||||
+342
-9
@@ -1,7 +1,7 @@
|
||||
Synthesizers
|
||||
============
|
||||
|
||||
PyTheory includes 13 built-in waveforms and 10 ADSR envelope presets.
|
||||
PyTheory includes 41 built-in waveforms and 10 ADSR envelope presets.
|
||||
Every sound is generated from scratch -- no samples or external audio
|
||||
files needed.
|
||||
|
||||
@@ -233,7 +233,7 @@ shapes the amplitude over time for natural-sounding notes:
|
||||
- **Sustain** -- the held volume while the note is on.
|
||||
- **Release** -- how quickly it fades to silence after the note ends.
|
||||
|
||||
PyTheory includes 8 presets:
|
||||
PyTheory includes 10 presets:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
@@ -386,11 +386,336 @@ more "wooden" than a raw saw wave.
|
||||
|
||||
violin = score.part("violin", synth="strings_synth")
|
||||
|
||||
Dedicated Instrument Synths
|
||||
--------------------------
|
||||
|
||||
Beyond the classic and physical modeling waveforms, PyTheory includes
|
||||
31 dedicated instrument synths. Each one uses tailored synthesis
|
||||
techniques -- additive harmonics, formant shaping, body resonance
|
||||
modeling, and specialized envelopes -- to capture the character of a
|
||||
specific acoustic instrument. These are the waveforms that bring the
|
||||
total count to 41.
|
||||
|
||||
Piano Synth
|
||||
~~~~~~~~~~~
|
||||
|
||||
Hammer-strike envelope with body resonance and subtle inharmonicity.
|
||||
Models the way a felt hammer excites steel strings inside a wooden
|
||||
soundboard.
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
piano = score.part("piano", synth="piano_synth")
|
||||
|
||||
Bass Guitar Synth
|
||||
~~~~~~~~~~~~~~~~~
|
||||
|
||||
Plucked string model with finger-damped harmonics and low-end warmth.
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
bass = score.part("bass", synth="bass_guitar_synth")
|
||||
|
||||
Flute Synth
|
||||
~~~~~~~~~~~~
|
||||
|
||||
Breathy noise excitation through a resonant tube model, with
|
||||
overblowing behavior at higher velocities.
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
flute = score.part("flute", synth="flute_synth")
|
||||
|
||||
Trumpet Synth
|
||||
~~~~~~~~~~~~~
|
||||
|
||||
Brass lip-buzz model with spectral brightness that increases with
|
||||
velocity, plus a characteristic brassy edge from shaped harmonics.
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
trumpet = score.part("trumpet", synth="trumpet_synth")
|
||||
|
||||
Clarinet Synth
|
||||
~~~~~~~~~~~~~~
|
||||
|
||||
Cylindrical bore model producing mostly odd harmonics, giving the
|
||||
characteristic hollow, woody tone.
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
clarinet = score.part("clarinet", synth="clarinet_synth")
|
||||
|
||||
Oboe Synth
|
||||
~~~~~~~~~~~
|
||||
|
||||
Double-reed model with nasal formant shaping and a buzzy, penetrating
|
||||
timbre.
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
oboe = score.part("oboe", synth="oboe_synth")
|
||||
|
||||
Marimba Synth
|
||||
~~~~~~~~~~~~~
|
||||
|
||||
Tuned bar model with a soft mallet attack and a warm, resonant decay
|
||||
that emphasizes the fundamental.
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
marimba = score.part("marimba", synth="marimba_synth")
|
||||
|
||||
Harpsichord Synth
|
||||
~~~~~~~~~~~~~~~~~
|
||||
|
||||
Plucked-string model with a bright, immediate attack and rapid decay
|
||||
-- the characteristic "plink" of a quill plucking a string.
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
harpsi = score.part("harpsi", synth="harpsichord_synth")
|
||||
|
||||
Cello Synth
|
||||
~~~~~~~~~~~
|
||||
|
||||
Bowed string model with body formants at cello resonance frequencies,
|
||||
producing a rich, warm, sustained tone.
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
cello = score.part("cello", synth="cello_synth")
|
||||
|
||||
Harp Synth
|
||||
~~~~~~~~~~
|
||||
|
||||
Plucked string with longer sustain and gentle high-frequency rolloff,
|
||||
modeling nylon strings on a resonant frame.
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
harp = score.part("harp", synth="harp_synth")
|
||||
|
||||
Upright Bass Synth
|
||||
~~~~~~~~~~~~~~~~~~
|
||||
|
||||
Pizzicato double bass with woody body resonance and a thumpy low end.
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
bass = score.part("bass", synth="upright_bass_synth")
|
||||
|
||||
Acoustic Guitar Synth
|
||||
~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
Steel-string model with pick transient, body resonance, and natural
|
||||
string decay.
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
guitar = score.part("guitar", synth="acoustic_guitar_synth")
|
||||
|
||||
Electric Guitar Synth
|
||||
~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
Magnetic pickup model with brighter harmonics and less body resonance
|
||||
than the acoustic, ready for effects processing.
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
eguitar = score.part("eguitar", synth="electric_guitar_synth")
|
||||
|
||||
Sitar Synth
|
||||
~~~~~~~~~~~~
|
||||
|
||||
Sympathetic string resonance with the characteristic buzzy "jawari"
|
||||
bridge, producing a shimmering, metallic sustain.
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
sitar = score.part("sitar", synth="sitar_synth")
|
||||
|
||||
Timpani Synth
|
||||
~~~~~~~~~~~~~
|
||||
|
||||
Large kettle drum with definite pitch. Inharmonic membrane modes
|
||||
(1.0, 1.5, 1.99, 2.44), felt mallet attack, copper kettle resonance.
|
||||
Use ``Part.roll()`` for crescendo timpani rolls.
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
timp = score.part("timp", synth="timpani_synth")
|
||||
timp.roll("C3", Duration.WHOLE, velocity_start=20, velocity_end=110)
|
||||
|
||||
Saxophone Synth
|
||||
~~~~~~~~~~~~~~~
|
||||
|
||||
Single reed through a conical brass bore. All harmonics with strong
|
||||
mids, reed buzz, and brass body warmth. Four presets: ``saxophone``,
|
||||
``alto_sax``, ``tenor_sax``, ``bari_sax``.
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
sax = score.part("sax", instrument="tenor_sax")
|
||||
|
||||
Pedal Steel Synth
|
||||
~~~~~~~~~~~~~~~~~
|
||||
|
||||
The Nashville crying sound — singing harmonics with slow vibrato
|
||||
and long sustain. Pairs naturally with spring reverb.
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
steel = score.part("steel", instrument="pedal_steel")
|
||||
|
||||
Theremin Synth
|
||||
~~~~~~~~~~~~~~
|
||||
|
||||
Pure sine with natural hand wobble — the eerie sci-fi sound.
|
||||
Best used with legato and glide for continuous pitch.
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
theremin = score.part("theremin", instrument="theremin")
|
||||
|
||||
Kalimba Synth
|
||||
~~~~~~~~~~~~~
|
||||
|
||||
Metal tines on a wooden body. Bright, bell-like attack with
|
||||
inharmonic overtones (modes at 1x, 2.92x, 5.4x).
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
kalimba = score.part("kalimba", instrument="kalimba")
|
||||
|
||||
Steel Drum Synth
|
||||
~~~~~~~~~~~~~~~~
|
||||
|
||||
Hammered metal pan with bright, ringing, tropical character.
|
||||
Inharmonic partials at 2.0x, 3.01x, 4.1x, 5.3x.
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
pan = score.part("pan", instrument="steel_drum")
|
||||
|
||||
Accordion Synth
|
||||
~~~~~~~~~~~~~~~
|
||||
|
||||
Musette-tuned doubled reeds — two slightly detuned reed sets
|
||||
create natural beating. Bellows pressure swell modulates amplitude.
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
acc = score.part("acc", instrument="accordion")
|
||||
|
||||
Didgeridoo Synth
|
||||
~~~~~~~~~~~~~~~~
|
||||
|
||||
Deep cylindrical drone with shifting formant overtones. The
|
||||
overtone singing effect sweeps a resonant peak between 500-1500Hz.
|
||||
Best with cave reverb.
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
didg = score.part("didg", instrument="didgeridoo")
|
||||
|
||||
Bagpipe Synth
|
||||
~~~~~~~~~~~~~
|
||||
|
||||
Bright chanter reed with constant bag pressure. All harmonics
|
||||
peaked around 3-7 (the piercing brightness). No dynamics — always ff.
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
pipes = score.part("pipes", instrument="bagpipe")
|
||||
|
||||
Banjo Synth
|
||||
~~~~~~~~~~~
|
||||
|
||||
Steel strings on a drum-head membrane body. The membrane gives
|
||||
nasal, ringy resonance with faster decay than guitar.
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
banjo = score.part("banjo", instrument="banjo")
|
||||
|
||||
Mandolin Synth
|
||||
~~~~~~~~~~~~~~
|
||||
|
||||
Paired steel strings in 4 courses — natural chorus from the
|
||||
doubled unison strings. Bright, ringing, fast attack.
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
mando = score.part("mando", instrument="mandolin")
|
||||
|
||||
Ukulele Synth
|
||||
~~~~~~~~~~~~~
|
||||
|
||||
Nylon strings on a small body. Mid-heavy resonance (no deep bass),
|
||||
softer attack than guitar, shorter sustain.
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
uke = score.part("uke", instrument="ukulele")
|
||||
|
||||
Granular Synth
|
||||
~~~~~~~~~~~~~~
|
||||
|
||||
Grain cloud synthesis — chops a source waveform into tiny overlapping
|
||||
grains (10-200ms), each windowed and optionally pitch/time scattered.
|
||||
Creates textures impossible with other synthesis: frozen tones,
|
||||
shimmering clouds, evolving pads, glitchy stutters.
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
# Atmospheric granular pad
|
||||
pad = score.part("pad", instrument="granular_pad")
|
||||
|
||||
# Granular with filter envelope sweep + resonance
|
||||
texture = score.part("texture", synth="granular_synth", envelope="pad",
|
||||
filter_amount=4000, filter_attack=0.5,
|
||||
filter_decay=1.5, filter_sustain=0.3,
|
||||
lowpass=600, lowpass_q=3.0,
|
||||
reverb=0.5, reverb_type="taj_mahal")
|
||||
|
||||
Parameters (passed as synth kwargs):
|
||||
|
||||
- ``grain_size``: Duration per grain in seconds (default 0.04).
|
||||
- ``density``: Grains per second (default 50). Higher = denser cloud.
|
||||
- ``scatter``: Random position jitter 0-1 (default 0.5).
|
||||
- ``pitch_var``: Per-grain pitch randomization in cents (default 12).
|
||||
- ``source``: Base waveform — ``"saw"``, ``"sine"``, ``"triangle"``,
|
||||
``"square"``, ``"noise"``.
|
||||
|
||||
Analog Oscillator Drift
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
All waveform synths support the ``analog_drift`` parameter, which adds
|
||||
subtle, slow random pitch variation to each oscillator -- modeling the
|
||||
voltage instability of vintage analog circuits. This is what makes a
|
||||
real Minimoog sound slightly different on every note, and why analog
|
||||
synths feel "alive" compared to their digital counterparts.
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
# Subtle vintage drift
|
||||
pad = score.part("pad", synth="saw", analog_drift=0.1)
|
||||
|
||||
# More pronounced, wobbly analog character
|
||||
lead = score.part("lead", synth="square", analog_drift=0.3)
|
||||
|
||||
Drift values:
|
||||
|
||||
- **0.05--0.1** = subtle warmth (studio-grade analog)
|
||||
- **0.15--0.25** = noticeable drift (vintage gear warming up)
|
||||
- **0.3+** = unstable, wobbly (broken tape machine)
|
||||
|
||||
Instrument Presets
|
||||
------------------
|
||||
|
||||
Instead of choosing synth + envelope + effects manually, use an
|
||||
instrument preset — 38 predefined combinations that approximate real
|
||||
instrument preset — 60+ predefined combinations that approximate real
|
||||
instruments:
|
||||
|
||||
.. code-block:: python
|
||||
@@ -403,20 +728,28 @@ instruments:
|
||||
|
||||
Available instruments:
|
||||
|
||||
**Keys**: piano, electric_piano, organ, harpsichord, celesta, music_box
|
||||
**Keys**: piano, electric_piano, organ, harpsichord, celesta, music_box,
|
||||
accordion
|
||||
|
||||
**Strings**: violin, viola, cello, contrabass, string_ensemble
|
||||
|
||||
**Woodwinds**: flute, clarinet, oboe, bassoon
|
||||
**Woodwinds**: flute, clarinet, oboe, bassoon, saxophone, alto_sax,
|
||||
tenor_sax, bari_sax
|
||||
|
||||
**Brass**: trumpet, trombone, french_horn, tuba, brass_ensemble
|
||||
|
||||
**Plucked**: acoustic_guitar, electric_guitar, distorted_guitar,
|
||||
bass_guitar, upright_bass, harp, sitar, koto
|
||||
**Plucked**: acoustic_guitar, electric_guitar, clean_guitar, crunch_guitar,
|
||||
distorted_guitar, orange_crunch, metal_guitar, bass_guitar, upright_bass,
|
||||
harp, sitar, koto, banjo, mandolin, mandola, ukulele
|
||||
|
||||
**Synth**: synth_lead, synth_pad, synth_bass, acid_bass, 808_bass
|
||||
**World/Exotic**: pedal_steel, theremin, kalimba, steel_drum, didgeridoo,
|
||||
bagpipe
|
||||
|
||||
**Percussion**: vibraphone, marimba, xylophone, glockenspiel, tubular_bells
|
||||
**Synth**: synth_lead, synth_pad, synth_bass, acid_bass, 808_bass,
|
||||
granular_pad, granular_texture, vocal, choir
|
||||
|
||||
**Percussion**: vibraphone, marimba, xylophone, glockenspiel, tubular_bells,
|
||||
timpani
|
||||
|
||||
Explicit kwargs override preset defaults:
|
||||
|
||||
|
||||
+118
-4
@@ -1,10 +1,11 @@
|
||||
Musical Systems
|
||||
===============
|
||||
|
||||
PyTheory supports **six musical systems**, each with its own tone names,
|
||||
scale patterns, and centuries of tradition behind them. Every system
|
||||
maps onto the same 12-tone equal temperament backbone, so you can
|
||||
compare scales across cultures and even combine them in your own music.
|
||||
PyTheory supports **16 musical systems** — 6 core systems mapped onto
|
||||
12-tone equal temperament, plus 10 microtonal systems with their own
|
||||
native tunings. The core systems let you compare scales across cultures;
|
||||
the microtonal systems go beyond 12-TET into genuinely different pitch
|
||||
universes.
|
||||
|
||||
Western
|
||||
-------
|
||||
@@ -271,4 +272,117 @@ produce the same pitches:
|
||||
>>> do4.frequency
|
||||
261.6255653005986
|
||||
|
||||
Microtonal Systems
|
||||
------------------
|
||||
|
||||
Beyond the six 12-TET core systems, PyTheory includes 10 microtonal
|
||||
systems that use their own native tunings — more notes per octave,
|
||||
just intonation ratios, or entirely alien pitch structures.
|
||||
|
||||
Shruti (22 tones per octave)
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
The Indian 22-shruti system divides the octave into 22 unequal steps
|
||||
using just intonation ratios. These microtonal inflections are what
|
||||
give classical Indian music its characteristic expressiveness — pitches
|
||||
that fall "between the cracks" of the piano.
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
score = Score("4/4", bpm=75, system="shruti")
|
||||
|
||||
Maqam (24 tones per octave)
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
The Arabic 24-tone system adds Zalzalian quarter-tone intervals
|
||||
(derived from just intonation ratios of 11 and 13) to the standard
|
||||
12 tones. These "neutral" intervals — halfway between major and minor —
|
||||
are the soul of maqam music.
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
score = Score("4/4", bpm=90, system="maqam")
|
||||
|
||||
Slendro (5-TET)
|
||||
~~~~~~~~~~~~~~~~
|
||||
|
||||
The Javanese slendro scale — 5 equal divisions of the octave. Each
|
||||
step is 240 cents, wider than any Western interval. Ethereal and
|
||||
floating.
|
||||
|
||||
Pelog (9-TET)
|
||||
~~~~~~~~~~~~~
|
||||
|
||||
Approximation of the Javanese pelog tuning as 9 equal divisions of
|
||||
the octave.
|
||||
|
||||
Thai (7-TET)
|
||||
~~~~~~~~~~~~~
|
||||
|
||||
Thai classical music divides the octave into 7 equal steps of ~171
|
||||
cents each — every interval is the same size.
|
||||
|
||||
Makam (53-TET)
|
||||
~~~~~~~~~~~~~~
|
||||
|
||||
Turkish makam music uses 53 equal divisions of the octave — fine
|
||||
enough to approximate virtually any just interval. The system that
|
||||
underlies Ottoman classical music.
|
||||
|
||||
Carnatic (72-TET)
|
||||
~~~~~~~~~~~~~~~~~
|
||||
|
||||
South Indian Carnatic music theory describes 72 melakarta ragas.
|
||||
The 72-TET system provides enough resolution to represent all the
|
||||
microtonal inflections of Carnatic practice.
|
||||
|
||||
19-TET and 31-TET
|
||||
~~~~~~~~~~~~~~~~~~
|
||||
|
||||
Extended equal temperaments that offer better approximations of
|
||||
just intonation intervals than 12-TET. 19-TET has excellent major
|
||||
thirds; 31-TET closely matches quarter-comma meantone.
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
score = Score("4/4", bpm=100, system="19-tet")
|
||||
|
||||
Bohlen-Pierce (13 equal divisions of the tritave)
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
A genuinely alien tuning system — 13 equal divisions of the
|
||||
**tritave** (3:1 ratio) instead of the octave (2:1). No octaves, no
|
||||
fifths, built on 3:5:7 harmonics. Used by experimental composers.
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
score = Score("4/4", bpm=100, system="bohlen-pierce")
|
||||
|
||||
The TET() Factory
|
||||
~~~~~~~~~~~~~~~~~
|
||||
|
||||
Create any equal temperament on the fly with the ``TET()`` factory:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
from pytheory import TET
|
||||
|
||||
edo19 = TET(19) # 19-tone equal temperament
|
||||
edo31 = TET(31) # 31-tone equal temperament
|
||||
score = Score("4/4", bpm=100, system=edo19)
|
||||
|
||||
Tone names in custom TET systems are integers (0, 1, 2, ..., n-1).
|
||||
|
||||
System.tone() Method
|
||||
~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
Any system can create a Tone directly:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
from pytheory import SYSTEMS
|
||||
|
||||
western = SYSTEMS["western"]
|
||||
c4 = western.tone("C", octave=4)
|
||||
|
||||
Music is universal, but every culture hears it differently. These systems are different maps of the same territory -- explore one you've never played in before and see what you find.
|
||||
|
||||
@@ -357,6 +357,45 @@ every tone knows its enharmonic spelling:
|
||||
>>> Tone.from_string("C4", system="western").enharmonic is None
|
||||
True
|
||||
|
||||
Extended Enharmonics
|
||||
~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
PyTheory supports the full range of enharmonic spellings used in real
|
||||
music theory:
|
||||
|
||||
- **Cb** and **Fb** — musically valid flats (Cb = B, Fb = E)
|
||||
- **E#** and **B#** — musically valid sharps (E# = F, B# = C)
|
||||
- **Double sharps** (``##`` or ``x``) — e.g. F## = G
|
||||
- **Double flats** (``bb``) — e.g. Dbb = C
|
||||
- **Unicode symbols** — ``♯`` (sharp), ``♭`` (flat), ``𝄪`` (double sharp),
|
||||
``𝄫`` (double flat) are all recognized and normalized to ASCII
|
||||
|
||||
.. code-block:: pycon
|
||||
|
||||
>>> Tone.from_string("Cb4") # resolves to B3 (octave boundary fix)
|
||||
<Tone B3>
|
||||
>>> Tone.from_string("B#4") # resolves to C5 (octave boundary fix)
|
||||
<Tone C5>
|
||||
>>> Tone.from_string("E#4") # resolves to F4
|
||||
<Tone F4>
|
||||
>>> Tone.from_string("Fb4") # resolves to E4
|
||||
<Tone E4>
|
||||
|
||||
The octave boundary is correctly handled: B# crosses up to the next
|
||||
octave (B#4 = C5), and Cb crosses down (Cb4 = B3), matching standard
|
||||
scientific pitch notation where the octave number increments at C.
|
||||
|
||||
Tone Validation
|
||||
~~~~~~~~~~~~~~~
|
||||
|
||||
Tones are validated on construction — if a tone name is not recognized
|
||||
in its system, a ``ValueError`` is raised:
|
||||
|
||||
.. code-block:: pycon
|
||||
|
||||
>>> Tone.from_string("X4") # not a valid tone name
|
||||
ValueError: ...
|
||||
|
||||
The Circle of Fifths
|
||||
--------------------
|
||||
|
||||
|
||||
+25
-15
@@ -18,8 +18,8 @@ Theory
|
||||
------
|
||||
|
||||
The theory layer works everywhere Python runs — no audio setup needed.
|
||||
Tones, scales, chords, keys, intervals, harmony, 6 musical systems,
|
||||
25 instruments:
|
||||
Tones, scales, chords, keys, intervals, harmony, 16 musical systems,
|
||||
60+ instruments:
|
||||
|
||||
.. code-block:: pycon
|
||||
|
||||
@@ -62,7 +62,10 @@ it through your speakers, export MIDI, finish in your DAW:
|
||||
lead.arpeggio("Am", bars=4, pattern="updown", octaves=2)
|
||||
|
||||
play_score(score)
|
||||
score.save_midi("sketch.mid")
|
||||
|
||||
.. raw:: html
|
||||
|
||||
<audio controls style="width:100%;margin:0.5em 0 1.5em"><source src="_static/audio/quickstart.wav" type="audio/wav"></audio>
|
||||
|
||||
Or hear a randomly generated track from the command line — different
|
||||
every time::
|
||||
@@ -72,22 +75,29 @@ every time::
|
||||
What's Inside
|
||||
-------------
|
||||
|
||||
- **Theory** — tones, scales (40+ across 6 systems), chords (17 types),
|
||||
- **Theory** — tones, scales (40+ across 16 systems), chords (17 types),
|
||||
keys, Roman numeral analysis, figured bass, pitch class sets (Forte
|
||||
numbers), scale recommendation, modulation, voice leading
|
||||
numbers), scale recommendation, modulation, voice leading, enharmonic
|
||||
support (Cb, Fb, E#, B#, double sharps/flats, unicode symbols)
|
||||
- **Sequencing** — Score, Parts, arpeggiator, legato/glide, velocity,
|
||||
swing, humanize, tempo changes, song sections with repeat
|
||||
- **Synthesis** — 13 waveforms (including Karplus-Strong pluck, Hammond organ,
|
||||
bowed string), 10 envelopes, 38 instrument presets, configurable FM,
|
||||
sub-oscillator, noise layer, filter envelope, velocity-to-brightness,
|
||||
detune, stereo pan/spread, 58 drum patterns (stereo panned), 21 fills
|
||||
swing, humanize, tempo changes, song sections with repeat, strumming,
|
||||
pitch bends (3 types), rolls, tuning systems (TET factory, 4
|
||||
temperaments, reference_pitch)
|
||||
- **Synthesis** — 41 waveforms (including Karplus-Strong pluck, Hammond organ,
|
||||
bowed string, granular, vocal/formant, and 31 dedicated instrument synths),
|
||||
10 envelopes, 60+ instrument presets, configurable FM, sub-oscillator,
|
||||
noise layer, filter envelope, velocity-to-brightness, analog oscillator
|
||||
drift, detune, stereo pan/spread, 80+ drum patterns (stereo panned,
|
||||
including world percussion and cajón), 21 fills, 11 microtonal systems
|
||||
- **Effects** — reverb (algorithmic + 7 convolution IRs, stereo), delay,
|
||||
lowpass/highpass (with resonance), distortion, saturation, chorus,
|
||||
phaser, tremolo, sidechain compression, automation, LFOs. Master bus
|
||||
compressor/limiter
|
||||
- **Instruments** — 25 presets with fingering generation
|
||||
lowpass/highpass (with resonance), distortion, guitar cabinet simulation,
|
||||
saturation, chorus, phaser, tremolo, analog drift, sidechain compression,
|
||||
automation, LFOs. Master bus compressor/limiter
|
||||
- **Instruments** — 60+ presets with fingering generation, guitar strumming,
|
||||
pitch bends, note choking
|
||||
- **Output** — stereo playback, WAV export, MIDI import/export
|
||||
- **Interface** — REPL with tab completion, CLI (15 commands), ``pytheory demo``
|
||||
- **Interface** — REPL with tab completion, CLI (15 commands), ``pytheory demo``,
|
||||
KeyboardInterrupt handling for clean stop
|
||||
- **AI-friendly** — Claude Code can compose
|
||||
and play music through PyTheory from natural language
|
||||
|
||||
|
||||
+2035
-260
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,69 @@
|
||||
"""Sprunki Simon Phase 1 — melody reference.
|
||||
|
||||
Notes transcribed from MIDI. Use as a base for arrangements.
|
||||
|
||||
Usage:
|
||||
python examples/sprunki.py
|
||||
"""
|
||||
|
||||
import sounddevice as sd
|
||||
|
||||
from pytheory import Score, Duration
|
||||
from pytheory.play import render_score, SAMPLE_RATE
|
||||
|
||||
|
||||
def sprunki_simon():
|
||||
score = Score("4/4", bpm=200)
|
||||
|
||||
lead = score.part("lead", synth="square", envelope="pluck", volume=0.5,
|
||||
lowpass=4500, detune=3, reverb=0.1)
|
||||
|
||||
# Phrase A
|
||||
lead.add("E4", 1.0)
|
||||
lead.add("G4", 1.0)
|
||||
lead.rest(1.5)
|
||||
lead.add("A4", 0.5)
|
||||
lead.add("B4", 1.0)
|
||||
lead.add("A4", 1.0)
|
||||
lead.add("G4", 1.0)
|
||||
lead.add("D4", 1.0)
|
||||
|
||||
# Phrase B
|
||||
lead.add("E4", 1.0)
|
||||
lead.add("G4", 1.0)
|
||||
lead.rest(1.5)
|
||||
lead.add("A4", 0.5)
|
||||
lead.add("D4", 2.0)
|
||||
lead.add("B3", 1.0)
|
||||
lead.add("A3", 0.5)
|
||||
lead.add("D4", 0.5)
|
||||
|
||||
# Phrase C
|
||||
lead.add("E4", 1.0)
|
||||
lead.add("G4", 1.0)
|
||||
lead.rest(1.5)
|
||||
lead.add("A4", 0.5)
|
||||
lead.add("B4", 1.0)
|
||||
lead.add("A4", 1.0)
|
||||
lead.add("G4", 1.0)
|
||||
lead.add("B4", 1.0)
|
||||
|
||||
# Phrase D
|
||||
lead.add("A4", 2.0)
|
||||
lead.add("G4", 1.0)
|
||||
lead.add("E4", 1.0)
|
||||
lead.add("B3", 2.0)
|
||||
lead.add("D4", 2.0)
|
||||
|
||||
return score
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
score = sprunki_simon()
|
||||
print(" Sprunki Simon Phase 1")
|
||||
try:
|
||||
buf = render_score(score)
|
||||
sd.play(buf, SAMPLE_RATE)
|
||||
sd.wait()
|
||||
except KeyboardInterrupt:
|
||||
sd.stop()
|
||||
+1
-3
@@ -1,6 +1,6 @@
|
||||
[project]
|
||||
name = "pytheory"
|
||||
version = "0.33.0"
|
||||
version = "0.39.3"
|
||||
description = "Music Theory for Humans"
|
||||
readme = "README.md"
|
||||
license = "MIT"
|
||||
@@ -21,8 +21,6 @@ classifiers = [
|
||||
"Programming Language :: Python :: 3.13",
|
||||
]
|
||||
dependencies = [
|
||||
"pytuning",
|
||||
"numeral",
|
||||
"sounddevice",
|
||||
"scipy",
|
||||
]
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
"""PyTheory: Music Theory for Humans."""
|
||||
|
||||
__version__ = "0.33.0"
|
||||
__version__ = "0.39.3"
|
||||
|
||||
from .tones import Tone, Interval
|
||||
from .systems import System, SYSTEMS, TET
|
||||
@@ -23,7 +23,7 @@ __all__ = [
|
||||
"PROGRESSIONS", "Chord", "Fretboard", "Fingering", "analyze_progression",
|
||||
"System", "SYSTEMS", "TET", "CHARTS", "charts_for_fretboard",
|
||||
"play", "save", "save_midi", "play_progression", "play_pattern",
|
||||
"play_score", "Synth", "Envelope",
|
||||
"play_score", "render_score", "Synth", "Envelope",
|
||||
"Duration", "TimeSignature", "RhythmNote", "Rest", "Score", "Part",
|
||||
"DrumSound", "Pattern", "Section", "INSTRUMENTS",
|
||||
]
|
||||
|
||||
+196
-36
@@ -1,46 +1,101 @@
|
||||
from pytuning import scales
|
||||
import math
|
||||
|
||||
REFERENCE_A = 440
|
||||
|
||||
# ── Roman numeral helpers (replaces `numeral` package) ───────────────────
|
||||
|
||||
_ROMAN_MAP = [
|
||||
(1000, "M"), (900, "CM"), (500, "D"), (400, "CD"),
|
||||
(100, "C"), (90, "XC"), (50, "L"), (40, "XL"),
|
||||
(10, "X"), (9, "IX"), (5, "V"), (4, "IV"), (1, "I"),
|
||||
]
|
||||
|
||||
_ROMAN_VALUES = {
|
||||
"I": 1, "V": 5, "X": 10, "L": 50, "C": 100, "D": 500, "M": 1000,
|
||||
}
|
||||
|
||||
|
||||
def int2roman(n: int) -> str:
|
||||
"""Convert an integer to an uppercase Roman numeral string."""
|
||||
result = []
|
||||
for value, numeral in _ROMAN_MAP:
|
||||
while n >= value:
|
||||
result.append(numeral)
|
||||
n -= value
|
||||
return "".join(result)
|
||||
|
||||
|
||||
def roman2int(s: str) -> int:
|
||||
"""Convert a Roman numeral string (case-insensitive) to an integer."""
|
||||
s = s.upper()
|
||||
total = 0
|
||||
prev = 0
|
||||
for ch in reversed(s):
|
||||
val = _ROMAN_VALUES.get(ch, 0)
|
||||
if val < prev:
|
||||
total -= val
|
||||
else:
|
||||
total += val
|
||||
prev = val
|
||||
return total
|
||||
|
||||
# Index of C in the Western tone list (A=0, A#=1, B=2, C=3, ...).
|
||||
# Scientific pitch notation changes octave at C, not A, so this offset
|
||||
# is needed for all octave arithmetic.
|
||||
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 +350,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 +439,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
|
||||
|
||||
+2
-2
@@ -849,7 +849,7 @@ class Chord:
|
||||
>>> Chord([D4, F4, A4]).analyze("C")
|
||||
'ii'
|
||||
"""
|
||||
import numeral as numeral_mod
|
||||
from ._statics import int2roman
|
||||
from .scales import TonedScale
|
||||
from .systems import SYSTEMS
|
||||
from .tones import Tone
|
||||
@@ -874,7 +874,7 @@ class Chord:
|
||||
scale_names = [t.name for t in scale.tones[:-1]]
|
||||
|
||||
def _build_numeral(root, quality, degree_idx, prefix=""):
|
||||
numeral_str = numeral_mod.int2roman(degree_idx + 1, only_ascii=True)
|
||||
numeral_str = int2roman(degree_idx + 1)
|
||||
suffix = ""
|
||||
if "minor" in quality:
|
||||
numeral_str = numeral_str.lower()
|
||||
|
||||
+76
-1
@@ -275,6 +275,78 @@ def cmd_demo(args):
|
||||
"lead": ("pluck_synth", "none", 0.3, 0.2),
|
||||
"pad": ("strings_synth", "pad", 0.0),
|
||||
"bass_lp": 200, "reverb_type": "taj_mahal"},
|
||||
{"name": "Classical", "key": ("D", "minor"), "drums": "bolero",
|
||||
"fill": "bossa nova", "bpm": 72,
|
||||
"prog": ("i", "iv", "V", "i"),
|
||||
"lead": ("flute_synth", "strings", 0.35, 0.2),
|
||||
"pad": ("cello_synth", "bowed", -0.2),
|
||||
"bass_lp": 400, "reverb_type": "cathedral"},
|
||||
{"name": "Harpsichord Suite", "key": ("A", "minor"), "drums": "bolero",
|
||||
"fill": "bossa nova", "bpm": 92,
|
||||
"prog": ("i", "iv", "V", "i"),
|
||||
"lead": ("harpsichord_synth", "none", 0.2, 0.1),
|
||||
"pad": ("strings_synth", "pad", -0.3),
|
||||
"bass_lp": 500, "reverb_type": "plate"},
|
||||
{"name": "Bhangra", "key": ("G", "minor"), "drums": "bhangra",
|
||||
"fill": "rock", "bpm": 140,
|
||||
"prog": ("i", "iv", "V", "i"),
|
||||
"lead": ("sitar_synth", "none", 0.3, 0.2),
|
||||
"pad": ("strings_synth", "pad", 0.0),
|
||||
"bass_lp": 400, "reverb_type": "taj_mahal"},
|
||||
{"name": "Jazz Trio", "key": ("F", "major"), "drums": "swing",
|
||||
"fill": "jazz", "bpm": 100,
|
||||
"prog": ("I", "vi", "ii", "V"),
|
||||
"lead": ("trumpet_synth", "bowed", 0.3, 0.2),
|
||||
"pad": ("piano_synth", "none", -0.2),
|
||||
"bass_lp": 600, "reverb_type": "plate"},
|
||||
{"name": "Theremin Noir", "key": ("A", "minor"), "drums": "hip hop",
|
||||
"fill": "rock", "bpm": 85,
|
||||
"prog": ("i", "iv", "V", "i"),
|
||||
"lead": ("theremin_synth", "pad", 0.4, 0.0),
|
||||
"pad": ("strings_synth", "pad", 0.0),
|
||||
"bass_lp": 300, "reverb_type": "cave"},
|
||||
{"name": "Caribbean", "key": ("C", "major"), "drums": "reggae",
|
||||
"fill": "reggae", "bpm": 110,
|
||||
"prog": ("I", "IV", "V", "IV"),
|
||||
"lead": ("steel_drum_synth", "none", 0.25, 0.3),
|
||||
"pad": ("acoustic_guitar_synth", "none", -0.3),
|
||||
"bass_lp": 500, "reverb_type": "plate"},
|
||||
{"name": "Accordion Waltz", "key": ("D", "minor"), "drums": "waltz",
|
||||
"fill": "jazz", "bpm": 88,
|
||||
"prog": ("i", "iv", "V", "i"),
|
||||
"lead": ("accordion_synth", "organ", 0.2, 0.1),
|
||||
"pad": ("strings_synth", "pad", -0.2),
|
||||
"bass_lp": 500, "reverb_type": "plate"},
|
||||
{"name": "Kalimba Dreams", "key": ("G", "major"), "drums": "cajon folk",
|
||||
"fill": "bossa nova", "bpm": 95,
|
||||
"prog": ("I", "vi", "IV", "V"),
|
||||
"lead": ("kalimba_synth", "none", 0.35, 0.2),
|
||||
"pad": ("piano_synth", "none", -0.2),
|
||||
"bass_lp": 400, "reverb_type": "taj_mahal"},
|
||||
{"name": "Outback Drone", "key": ("E", "minor"), "drums": "djembe",
|
||||
"fill": "afrobeat", "bpm": 70,
|
||||
"prog": ("i", "iv", "i", "V"),
|
||||
"lead": ("didgeridoo_synth", "pad", 0.3, 0.0),
|
||||
"pad": ("granular_synth", "pad", 0.0),
|
||||
"bass_lp": 200, "reverb_type": "cave"},
|
||||
{"name": "Highland", "key": ("A", "minor"), "drums": "flamenco",
|
||||
"fill": "rock", "bpm": 95,
|
||||
"prog": ("i", "iv", "V", "i"),
|
||||
"lead": ("bagpipe_synth", "organ", 0.15, 0.0),
|
||||
"pad": ("strings_synth", "pad", -0.2),
|
||||
"bass_lp": 400, "reverb_type": "cathedral"},
|
||||
{"name": "Nashville Tears", "key": ("G", "major"), "drums": "country",
|
||||
"fill": "rock", "bpm": 85,
|
||||
"prog": ("I", "IV", "V", "IV"),
|
||||
"lead": ("pedal_steel_synth", "strings", 0.35, 0.2),
|
||||
"pad": ("piano_synth", "none", -0.3),
|
||||
"bass_lp": 500, "reverb_type": "spring"},
|
||||
{"name": "Tabla Fusion", "key": ("E", "minor"), "drums": "teental",
|
||||
"fill": "rock", "bpm": 120,
|
||||
"prog": ("i", "iv", "V", "i"),
|
||||
"lead": ("sitar_synth", "none", 0.3, 0.2),
|
||||
"pad": ("vocal_synth", "pad", 0.0),
|
||||
"bass_lp": 400, "reverb_type": "taj_mahal"},
|
||||
]
|
||||
|
||||
mood = random.choice(moods)
|
||||
@@ -351,7 +423,10 @@ def cmd_demo(args):
|
||||
print(f" {mood['drums']} | {lead_synth} lead | {pad_synth} pad | {mood['reverb_type']} reverb")
|
||||
print()
|
||||
|
||||
play_score(score)
|
||||
try:
|
||||
play_score(score)
|
||||
except KeyboardInterrupt:
|
||||
pass
|
||||
print(" ♫")
|
||||
|
||||
|
||||
|
||||
+2692
-56
File diff suppressed because it is too large
Load Diff
+176
-7
@@ -77,6 +77,7 @@ def cmd_help(session, args):
|
||||
Parts:
|
||||
part lead saw pluck score.part("lead", synth="saw", envelope="pluck")
|
||||
part bass sine score.part("bass", synth="sine")
|
||||
part lead instrument piano score.part("lead", instrument="piano")
|
||||
part list all parts
|
||||
|
||||
Notes (on active part):
|
||||
@@ -85,6 +86,12 @@ def cmd_help(session, args):
|
||||
rest 2 part.rest(2.0)
|
||||
arp Am updown 2 2 part.arpeggio("Am", pattern="updown", bars=2, octaves=2)
|
||||
prog I V vi IV part adds key.progression(...)
|
||||
strum Am 2 down part.strum("Am", 2, direction="down")
|
||||
strum G 2 up 0.1 lazy strum (strum_time=0.1)
|
||||
roll C3 4 part.roll("C3", 4) — timpani/tremolo
|
||||
roll C3 4 30 110 roll with velocity ramp
|
||||
bend C5 1 2 part.add("C5", 1, bend=2) — bend up 2 semitones
|
||||
bend C5 1 -1 bend down a half step
|
||||
|
||||
Effects (on active part):
|
||||
reverb 0.4 reverb=0.4
|
||||
@@ -110,6 +117,12 @@ def cmd_help(session, args):
|
||||
fingering Am guitar chord fingering
|
||||
diagram [mode] [frets] scale diagram on guitar
|
||||
|
||||
Tuning:
|
||||
temperament equal set temperament (equal/pythagorean/meantone/just)
|
||||
temperament show current temperament
|
||||
reference 432 set reference pitch (default 440)
|
||||
instruments list all available instruments
|
||||
|
||||
Session:
|
||||
show score info
|
||||
status current state
|
||||
@@ -197,12 +210,22 @@ def cmd_part(session, args):
|
||||
return
|
||||
|
||||
name = args[0]
|
||||
synth = args[1] if len(args) > 1 else "saw"
|
||||
envelope = args[2] if len(args) > 2 else "pluck"
|
||||
|
||||
if name not in session.parts:
|
||||
session.parts[name] = session.score.part(name, synth=synth, envelope=envelope)
|
||||
print(f" score.part(\"{name}\", synth=\"{synth}\", envelope=\"{envelope}\")")
|
||||
# Check if second arg is "instrument" keyword or an instrument name
|
||||
if len(args) > 1 and args[1] == "instrument" and len(args) > 2:
|
||||
instrument = args[2]
|
||||
session.parts[name] = session.score.part(name, instrument=instrument)
|
||||
print(f" score.part(\"{name}\", instrument=\"{instrument}\")")
|
||||
elif len(args) > 1 and args[1] in _INSTRUMENT_NAMES:
|
||||
instrument = args[1]
|
||||
session.parts[name] = session.score.part(name, instrument=instrument)
|
||||
print(f" score.part(\"{name}\", instrument=\"{instrument}\")")
|
||||
else:
|
||||
synth = args[1] if len(args) > 1 else "saw"
|
||||
envelope = args[2] if len(args) > 2 else "pluck"
|
||||
session.parts[name] = session.score.part(name, synth=synth, envelope=envelope)
|
||||
print(f" score.part(\"{name}\", synth=\"{synth}\", envelope=\"{envelope}\")")
|
||||
else:
|
||||
print(f" → {name}")
|
||||
session.current_part = session.parts[name]
|
||||
@@ -534,6 +557,97 @@ def cmd_identify(session, args):
|
||||
print(f" error: {e}")
|
||||
|
||||
|
||||
def cmd_strum(session, args):
|
||||
"""Strum a chord on a fretboard-equipped part."""
|
||||
if not args:
|
||||
print(" usage: strum Am [beats] [down|up] [strum_time]")
|
||||
return
|
||||
part = _require_part(session)
|
||||
chord_name = args[0]
|
||||
beats = float(args[1]) if len(args) > 1 else 1.0
|
||||
direction = args[2] if len(args) > 2 else "down"
|
||||
strum_time = float(args[3]) if len(args) > 3 else 0.05
|
||||
try:
|
||||
part.strum(chord_name, beats, direction=direction, strum_time=strum_time)
|
||||
print(f" .strum(\"{chord_name}\", {beats}, direction=\"{direction}\", "
|
||||
f"strum_time={strum_time})")
|
||||
except Exception as e:
|
||||
print(f" error: {e}")
|
||||
|
||||
|
||||
def cmd_roll(session, args):
|
||||
"""Play a roll (rapid repeated notes with velocity ramp)."""
|
||||
if not args:
|
||||
print(" usage: roll C3 [beats] [vel_start] [vel_end]")
|
||||
return
|
||||
part = _require_part(session)
|
||||
tone = args[0]
|
||||
beats = float(args[1]) if len(args) > 1 else 4.0
|
||||
vel_start = int(args[2]) if len(args) > 2 else 40
|
||||
vel_end = int(args[3]) if len(args) > 3 else 100
|
||||
try:
|
||||
part.roll(tone, beats, velocity_start=vel_start, velocity_end=vel_end)
|
||||
print(f" .roll(\"{tone}\", {beats}, velocity_start={vel_start}, "
|
||||
f"velocity_end={vel_end})")
|
||||
except Exception as e:
|
||||
print(f" error: {e}")
|
||||
|
||||
|
||||
def cmd_bend(session, args):
|
||||
"""Add a note with pitch bend."""
|
||||
if len(args) < 3:
|
||||
print(" usage: bend C5 1 2 (note, beats, semitones)")
|
||||
print(" bend C5 1 -1 (bend down)")
|
||||
return
|
||||
part = _require_part(session)
|
||||
note = args[0]
|
||||
beats = float(args[1])
|
||||
bend = float(args[2])
|
||||
bend_type = args[3] if len(args) > 3 else "smooth"
|
||||
try:
|
||||
part.add(note, beats, bend=bend, bend_type=bend_type)
|
||||
print(f" .add(\"{note}\", {beats}, bend={bend}, bend_type=\"{bend_type}\")")
|
||||
except Exception as e:
|
||||
print(f" error: {e}")
|
||||
|
||||
|
||||
def cmd_temperament(session, args):
|
||||
"""Set or show the tuning temperament."""
|
||||
if not args:
|
||||
temp = getattr(session.score, 'temperament', 'equal')
|
||||
ref = getattr(session.score, 'reference_pitch', 440.0)
|
||||
print(f" temperament={temp} reference={ref} Hz")
|
||||
print(f" available: equal, pythagorean, meantone, just")
|
||||
return
|
||||
temp = args[0]
|
||||
valid = ["equal", "pythagorean", "meantone", "just"]
|
||||
if temp not in valid:
|
||||
print(f" unknown temperament: {temp}")
|
||||
print(f" available: {', '.join(valid)}")
|
||||
return
|
||||
session.score.temperament = temp
|
||||
print(f" temperament={temp}")
|
||||
|
||||
|
||||
def cmd_reference(session, args):
|
||||
"""Set the reference pitch (A4 frequency)."""
|
||||
if not args:
|
||||
ref = getattr(session.score, 'reference_pitch', 440.0)
|
||||
print(f" reference={ref} Hz")
|
||||
return
|
||||
ref = float(args[0])
|
||||
session.score.reference_pitch = ref
|
||||
print(f" reference={ref} Hz")
|
||||
|
||||
|
||||
def cmd_instruments(session, args):
|
||||
"""List all available instruments."""
|
||||
cols = 3
|
||||
for i in range(0, len(_INSTRUMENT_NAMES), cols):
|
||||
row = _INSTRUMENT_NAMES[i:i + cols]
|
||||
print(" " + " ".join(f"{name:<22s}" for name in row))
|
||||
|
||||
|
||||
def cmd_circle(session, args):
|
||||
"""Show circle of fifths."""
|
||||
tonic = args[0] if args else session.key.tonic_name
|
||||
@@ -560,7 +674,10 @@ def cmd_clear(session, args):
|
||||
def cmd_status(session, args):
|
||||
parts = ", ".join(session.parts.keys()) if session.parts else "none"
|
||||
active = session.current_part.name if session.current_part else "none"
|
||||
temp = getattr(session.score, 'temperament', 'equal')
|
||||
ref = getattr(session.score, 'reference_pitch', 440.0)
|
||||
print(f" key={session.key} bpm={session.bpm} swing={session.swing}")
|
||||
print(f" temperament={temp} reference={ref} Hz")
|
||||
print(f" drums={session._drum_preset or 'none'} parts=[{parts}] active={active}")
|
||||
|
||||
|
||||
@@ -607,6 +724,12 @@ COMMANDS = {
|
||||
"interval": cmd_interval,
|
||||
"identify": cmd_identify, "id": cmd_identify,
|
||||
"circle": cmd_circle,
|
||||
"strum": cmd_strum,
|
||||
"roll": cmd_roll,
|
||||
"bend": cmd_bend,
|
||||
"temperament": cmd_temperament, "temp": cmd_temperament,
|
||||
"reference": cmd_reference, "ref": cmd_reference,
|
||||
"instruments": cmd_instruments,
|
||||
"clear": cmd_clear,
|
||||
"status": cmd_status,
|
||||
}
|
||||
@@ -653,9 +776,43 @@ def _prompt(session):
|
||||
# ── Tab completion ─────────────────────────────────────────────────────────
|
||||
|
||||
_SYNTH_NAMES = ["sine", "saw", "triangle", "square", "pulse", "fm",
|
||||
"noise", "supersaw", "pwm_slow", "pwm_fast"]
|
||||
"noise", "supersaw", "pwm_slow", "pwm_fast",
|
||||
"pedal_steel_synth", "theremin_synth", "kalimba_synth",
|
||||
"steel_drum_synth", "accordion_synth", "didgeridoo_synth",
|
||||
"bagpipe_synth", "banjo_synth", "mandolin_synth",
|
||||
"ukulele_synth", "vocal_synth", "granular_synth",
|
||||
"piano_synth", "organ_synth", "harpsichord_synth",
|
||||
"strings_synth", "cello_synth", "flute_synth",
|
||||
"clarinet_synth", "oboe_synth", "trumpet_synth",
|
||||
"acoustic_guitar_synth", "electric_guitar_synth",
|
||||
"bass_guitar_synth", "upright_bass_synth", "harp_synth",
|
||||
"sitar_synth", "pluck_synth", "saxophone_synth",
|
||||
"marimba_synth", "timpani_synth"]
|
||||
_INSTRUMENT_NAMES = [
|
||||
# Keys
|
||||
"piano", "electric_piano", "organ", "harpsichord", "celesta", "music_box",
|
||||
# Strings
|
||||
"violin", "viola", "cello", "contrabass", "string_ensemble",
|
||||
# Woodwinds
|
||||
"flute", "clarinet", "oboe", "bassoon",
|
||||
# Brass
|
||||
"trumpet", "trombone", "french_horn", "tuba", "brass_ensemble",
|
||||
# Plucked
|
||||
"acoustic_guitar", "electric_guitar", "clean_guitar", "crunch_guitar",
|
||||
"distorted_guitar", "orange_crunch", "metal_guitar", "bass_guitar",
|
||||
"upright_bass", "harp", "sitar", "pedal_steel", "theremin", "kalimba",
|
||||
"steel_drum", "accordion", "didgeridoo", "bagpipe", "banjo", "mandolin",
|
||||
"mandola", "ukulele", "koto",
|
||||
# Synth presets
|
||||
"synth_lead", "synth_pad", "synth_bass", "acid_bass",
|
||||
"granular_pad", "vocal", "choir", "granular_texture", "808_bass",
|
||||
# Percussion / Mallet
|
||||
"vibraphone", "marimba", "xylophone", "glockenspiel", "tubular_bells", "timpani",
|
||||
# Woodwinds (continued)
|
||||
"saxophone", "alto_sax", "tenor_sax", "bari_sax",
|
||||
]
|
||||
_ENVELOPE_NAMES = ["piano", "pluck", "pad", "organ", "bell", "strings",
|
||||
"staccato", "none"]
|
||||
"staccato", "bowed", "mallet", "none"]
|
||||
_ARP_PATTERNS = ["up", "down", "updown", "downup", "random"]
|
||||
_LFO_SHAPES = ["sine", "triangle", "saw", "square"]
|
||||
_SYSTEMS = ["western", "indian", "arabic", "japanese", "blues", "gamelan"]
|
||||
@@ -667,7 +824,7 @@ _CHORD_SUFFIXES = ["", "m", "7", "m7", "maj7", "dim", "aug", "sus2", "sus4",
|
||||
# Context-aware completions for the second word
|
||||
_ARG_COMPLETIONS = {
|
||||
"drums": lambda: Pattern.list_presets(),
|
||||
"part": lambda: _SYNTH_NAMES,
|
||||
"part": lambda: _SYNTH_NAMES + _INSTRUMENT_NAMES,
|
||||
"key": lambda: [f"{n}m" for n in _NOTE_NAMES[:12]] + _NOTE_NAMES[:12],
|
||||
"arp": lambda: [f"{n}{s}" for n in _NOTE_NAMES[:7] for s in _CHORD_SUFFIXES[:6]],
|
||||
"add": lambda: [f"{n}{o}" for n in _NOTE_NAMES[:12] for o in ["3", "4", "5"]],
|
||||
@@ -679,6 +836,12 @@ _ARG_COMPLETIONS = {
|
||||
"lowpass_q", "reverb_decay", "delay_time", "delay_feedback",
|
||||
"distortion_drive"],
|
||||
"identify": lambda: [f"{n}{s}" for n in _NOTE_NAMES[:7] for s in _CHORD_SUFFIXES[:6]],
|
||||
"strum": lambda: [f"{n}{s}" for n in _NOTE_NAMES[:7] for s in _CHORD_SUFFIXES[:6]],
|
||||
"roll": lambda: [f"{n}{o}" for n in _NOTE_NAMES[:12] for o in ["2", "3", "4", "5"]],
|
||||
"bend": lambda: [f"{n}{o}" for n in _NOTE_NAMES[:12] for o in ["3", "4", "5"]],
|
||||
"temperament": lambda: ["equal", "pythagorean", "meantone", "just"],
|
||||
"reference": lambda: ["440", "432", "415", "444"],
|
||||
"instruments": lambda: _INSTRUMENT_NAMES,
|
||||
}
|
||||
|
||||
|
||||
@@ -705,6 +868,12 @@ def _completer(text, state):
|
||||
elif cmd == "arp" and len(tokens) == 3:
|
||||
# Pattern for arp
|
||||
options = [p for p in _ARP_PATTERNS if p.startswith(text)]
|
||||
elif cmd == "strum" and len(tokens) == 4:
|
||||
# Direction for strum
|
||||
options = [d for d in ["down", "up"] if d.startswith(text)]
|
||||
elif cmd == "bend" and len(tokens) == 5:
|
||||
# Bend type
|
||||
options = [t for t in ["smooth", "linear", "late"] if t.startswith(text)]
|
||||
elif cmd == "lfo" and len(tokens) >= 7:
|
||||
# Shape for lfo
|
||||
options = [s for s in _LFO_SHAPES if s.startswith(text)]
|
||||
|
||||
+1728
-46
File diff suppressed because it is too large
Load Diff
+6
-6
@@ -2,8 +2,6 @@ from __future__ import annotations
|
||||
|
||||
from typing import Optional, Union
|
||||
|
||||
import numeral
|
||||
|
||||
from .systems import SYSTEMS, System
|
||||
from .tones import Tone
|
||||
|
||||
@@ -49,7 +47,8 @@ class Scale:
|
||||
def __repr__(self) -> str:
|
||||
r = []
|
||||
for (i, tone) in enumerate(self.tones):
|
||||
degree = numeral.int2roman(i + 1, only_ascii=True)
|
||||
from ._statics import int2roman
|
||||
degree = int2roman(i + 1)
|
||||
r += [f"{degree}={tone.full_name}"]
|
||||
|
||||
r = " ".join(r)
|
||||
@@ -200,7 +199,7 @@ class Scale:
|
||||
>>> scale.progression("I", "IV", "V", "I")
|
||||
[<Chord (C,E,G)>, <Chord (F,A,C)>, <Chord (G,B,D)>, <Chord (C,E,G)>]
|
||||
"""
|
||||
import numeral as numeral_mod
|
||||
from ._statics import roman2int
|
||||
chords = []
|
||||
for num in numerals:
|
||||
is_seventh = num.endswith("7")
|
||||
@@ -213,7 +212,7 @@ class Scale:
|
||||
elif clean.startswith("#") and len(clean) > 1:
|
||||
clean = clean[1:]
|
||||
flat_offset = 1 # one semitone up
|
||||
degree = numeral_mod.roman2int(clean.upper()) - 1
|
||||
degree = roman2int(clean.upper()) - 1
|
||||
if is_seventh:
|
||||
chord = self.seventh(degree)
|
||||
else:
|
||||
@@ -406,7 +405,8 @@ class Scale:
|
||||
if isinstance(item, str):
|
||||
degrees = []
|
||||
for (i, tone) in enumerate(self.tones):
|
||||
degrees.append(numeral.int2roman(i + 1, only_ascii=True))
|
||||
from ._statics import int2roman
|
||||
degrees.append(int2roman(i + 1))
|
||||
|
||||
if item in degrees:
|
||||
item = degrees.index(item)
|
||||
|
||||
+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
|
||||
|
||||
+654
-10
@@ -4869,6 +4869,19 @@ def test_duration_values():
|
||||
assert abs(Duration.TRIPLET_QUARTER.value - 2 / 3) < 1e-9
|
||||
|
||||
|
||||
def test_duration_arithmetic():
|
||||
# Multiplication
|
||||
assert Duration.WHOLE * 2 == 8.0
|
||||
assert 2 * Duration.HALF == 4.0
|
||||
assert Duration.QUARTER * 3 == 3.0
|
||||
# Division
|
||||
assert Duration.WHOLE / 2 == 2.0
|
||||
# Addition
|
||||
assert Duration.HALF + Duration.QUARTER == 3.0
|
||||
assert Duration.HALF + 1.0 == 3.0
|
||||
assert 1.0 + Duration.HALF == 3.0
|
||||
|
||||
|
||||
def test_time_signature_from_string_4_4():
|
||||
ts = TimeSignature.from_string("4/4")
|
||||
assert ts.beats == 4
|
||||
@@ -5320,7 +5333,7 @@ def test_supersaw_wave():
|
||||
@needs_portaudio
|
||||
def test_all_synths_in_enum():
|
||||
from pytheory.play import Synth
|
||||
assert len(Synth) == 13
|
||||
assert len(Synth) == 42
|
||||
for s in Synth:
|
||||
wave = s(440, n_samples=1000)
|
||||
assert len(wave) == 1000
|
||||
@@ -6467,11 +6480,8 @@ def test_instrument_piano():
|
||||
from pytheory import Score, Duration
|
||||
score = Score("4/4", bpm=120)
|
||||
p = score.part("p", instrument="piano")
|
||||
assert p.synth == "fm"
|
||||
assert p.envelope == "piano"
|
||||
assert p.detune == 5
|
||||
assert p.lowpass == 6000
|
||||
assert p.chorus_mix == 0.1
|
||||
assert p.synth == "piano_synth"
|
||||
assert p.vel_to_filter == 3000
|
||||
|
||||
|
||||
def test_instrument_violin():
|
||||
@@ -6488,12 +6498,9 @@ def test_instrument_violin():
|
||||
def test_instrument_override():
|
||||
from pytheory import Score
|
||||
score = Score("4/4", bpm=120)
|
||||
# Explicit synth overrides the preset's "fm"
|
||||
# Explicit synth overrides the preset
|
||||
p = score.part("p", instrument="piano", synth="saw")
|
||||
assert p.synth == "saw"
|
||||
# Other preset values still apply
|
||||
assert p.envelope == "piano"
|
||||
assert p.detune == 5
|
||||
|
||||
|
||||
def test_instrument_unknown_raises():
|
||||
@@ -6717,3 +6724,640 @@ def test_interval_to_non12():
|
||||
a5 = a.add(19)
|
||||
result = a.interval_to(a5)
|
||||
assert "octave" in result
|
||||
|
||||
|
||||
# ── Dedicated instrument synths ──────────────────────────────────────────────
|
||||
|
||||
def test_all_dedicated_synths_render():
|
||||
"""Every dedicated synth waveform produces valid audio."""
|
||||
from pytheory.play import (piano_wave, bass_guitar_wave, flute_wave,
|
||||
trumpet_wave, clarinet_wave, oboe_wave,
|
||||
marimba_wave, harpsichord_wave, cello_wave,
|
||||
harp_wave, upright_bass_wave,
|
||||
acoustic_guitar_wave, electric_guitar_wave,
|
||||
sitar_wave, SAMPLE_RATE)
|
||||
synths = [piano_wave, bass_guitar_wave, flute_wave, trumpet_wave,
|
||||
clarinet_wave, oboe_wave, marimba_wave, harpsichord_wave,
|
||||
cello_wave, harp_wave, upright_bass_wave,
|
||||
acoustic_guitar_wave, electric_guitar_wave, sitar_wave]
|
||||
for fn in synths:
|
||||
wave = fn(440, n_samples=11025)
|
||||
assert len(wave) == 11025
|
||||
assert wave.dtype == numpy.int16
|
||||
assert numpy.abs(wave).max() > 0
|
||||
|
||||
|
||||
def test_piano_brightness_scales():
|
||||
"""High-pitched piano should be brighter (more high harmonics)."""
|
||||
from pytheory.play import piano_wave
|
||||
low = piano_wave(130, n_samples=22050) # C3
|
||||
high = piano_wave(1047, n_samples=22050) # C6
|
||||
# Both should produce valid audio
|
||||
assert numpy.abs(low).max() > 0
|
||||
assert numpy.abs(high).max() > 0
|
||||
|
||||
|
||||
def test_acoustic_guitar_body_resonance():
|
||||
"""Acoustic guitar should produce richer spectrum than raw pluck."""
|
||||
from pytheory.play import acoustic_guitar_wave, pluck_wave
|
||||
ag = acoustic_guitar_wave(220, n_samples=22050)
|
||||
pk = pluck_wave(220, n_samples=22050)
|
||||
assert len(ag) == len(pk) == 22050
|
||||
|
||||
|
||||
def test_cello_has_vibrato():
|
||||
"""Cello synth should produce pitch variation (vibrato)."""
|
||||
from pytheory.play import cello_wave
|
||||
wave = cello_wave(220, n_samples=44100)
|
||||
assert len(wave) == 44100
|
||||
assert numpy.abs(wave).max() > 0
|
||||
|
||||
|
||||
# ── Cabinet simulation ───────────────────────────────────────────────────────
|
||||
|
||||
def test_cabinet_reduces_highs():
|
||||
"""Cabinet sim should reduce high-frequency content."""
|
||||
from pytheory.play import _apply_cabinet
|
||||
# White noise has flat spectrum
|
||||
noise = numpy.random.uniform(-1, 1, 44100).astype(numpy.float32)
|
||||
cabbed = _apply_cabinet(noise, brightness=0.5)
|
||||
# RMS of cabbed should be lower (energy removed by filters)
|
||||
assert numpy.sqrt(numpy.mean(cabbed ** 2)) < numpy.sqrt(numpy.mean(noise ** 2))
|
||||
|
||||
|
||||
def test_cabinet_brightness_param():
|
||||
"""Higher brightness = more high-frequency content passes through."""
|
||||
from pytheory.play import _apply_cabinet
|
||||
noise = numpy.random.uniform(-1, 1, 44100).astype(numpy.float32)
|
||||
dark = _apply_cabinet(noise, brightness=0.0)
|
||||
bright = _apply_cabinet(noise, brightness=1.0)
|
||||
# Bright should have more energy than dark
|
||||
assert numpy.sqrt(numpy.mean(bright ** 2)) > numpy.sqrt(numpy.mean(dark ** 2))
|
||||
|
||||
|
||||
# ── Analog drift ─────────────────────────────────────────────────────────────
|
||||
|
||||
def test_analog_drift_varies_pitch():
|
||||
"""Analog drift should make repeated renders slightly different."""
|
||||
from pytheory import Score, Duration
|
||||
score1 = Score("4/4", bpm=120)
|
||||
p1 = score1.part("t", synth="saw", analog=0.5)
|
||||
p1.add("C4", Duration.QUARTER)
|
||||
p1.add("C4", Duration.QUARTER)
|
||||
# With analog > 0, each C4 gets a random pitch offset
|
||||
# This is hard to test deterministically, just verify it renders
|
||||
from pytheory.play import render_score
|
||||
buf = render_score(score1)
|
||||
assert len(buf) > 0
|
||||
|
||||
|
||||
# ── Guitar strumming ─────────────────────────────────────────────────────────
|
||||
|
||||
def test_strum_requires_fretboard():
|
||||
"""Strumming without a fretboard should raise ValueError."""
|
||||
from pytheory import Score, Duration
|
||||
score = Score("4/4", bpm=120)
|
||||
p = score.part("g", synth="saw")
|
||||
with pytest.raises(ValueError, match="fretboard"):
|
||||
p.strum("Am", Duration.QUARTER)
|
||||
|
||||
|
||||
def test_strum_adds_notes():
|
||||
"""Strumming should add notes to the part."""
|
||||
from pytheory import Score, Duration, Fretboard
|
||||
score = Score("4/4", bpm=120)
|
||||
fb = Fretboard.guitar()
|
||||
p = score.part("g", instrument="acoustic_guitar", fretboard=fb)
|
||||
p.strum("Am", Duration.HALF)
|
||||
assert len(p.notes) > 0
|
||||
|
||||
|
||||
def test_strum_direction():
|
||||
"""Both down and up strums should work."""
|
||||
from pytheory import Score, Duration, Fretboard
|
||||
score = Score("4/4", bpm=120)
|
||||
fb = Fretboard.guitar()
|
||||
p = score.part("g", instrument="acoustic_guitar", fretboard=fb)
|
||||
p.strum("G", Duration.QUARTER, direction="down")
|
||||
p.strum("G", Duration.QUARTER, direction="up")
|
||||
assert len(p.notes) >= 2 # grace notes + chord per strum
|
||||
|
||||
|
||||
# ── World drums ──────────────────────────────────────────────────────────────
|
||||
|
||||
def test_tabla_sounds_render():
|
||||
"""All tabla drum sounds should produce valid audio."""
|
||||
from pytheory.play import _render_drum_hit
|
||||
from pytheory.rhythm import DrumSound
|
||||
for sound in [DrumSound.TABLA_NA, DrumSound.TABLA_TIN, DrumSound.TABLA_GE,
|
||||
DrumSound.TABLA_DHA, DrumSound.TABLA_TIT, DrumSound.TABLA_KE]:
|
||||
wave = _render_drum_hit(sound.value, 22050)
|
||||
assert len(wave) == 22050
|
||||
assert wave.dtype == numpy.float32
|
||||
|
||||
|
||||
def test_dhol_sounds_render():
|
||||
from pytheory.play import _render_drum_hit
|
||||
from pytheory.rhythm import DrumSound
|
||||
for sound in [DrumSound.DHOL_DAGGA, DrumSound.DHOL_TILLI, DrumSound.DHOL_BOTH]:
|
||||
wave = _render_drum_hit(sound.value, 22050)
|
||||
assert len(wave) == 22050
|
||||
|
||||
|
||||
def test_mridangam_sounds_render():
|
||||
from pytheory.play import _render_drum_hit
|
||||
from pytheory.rhythm import DrumSound
|
||||
for sound in [DrumSound.MRIDANGAM_THAM, DrumSound.MRIDANGAM_NAM,
|
||||
DrumSound.MRIDANGAM_DIN, DrumSound.MRIDANGAM_THA]:
|
||||
wave = _render_drum_hit(sound.value, 22050)
|
||||
assert len(wave) == 22050
|
||||
|
||||
|
||||
def test_djembe_sounds_render():
|
||||
from pytheory.play import _render_drum_hit
|
||||
from pytheory.rhythm import DrumSound
|
||||
for sound in [DrumSound.DJEMBE_BASS, DrumSound.DJEMBE_TONE, DrumSound.DJEMBE_SLAP]:
|
||||
wave = _render_drum_hit(sound.value, 22050)
|
||||
assert len(wave) == 22050
|
||||
|
||||
|
||||
def test_metal_kit_sounds_render():
|
||||
from pytheory.play import _render_drum_hit
|
||||
from pytheory.rhythm import DrumSound
|
||||
for sound in [DrumSound.METAL_KICK, DrumSound.METAL_SNARE, DrumSound.METAL_HAT]:
|
||||
wave = _render_drum_hit(sound.value, 22050)
|
||||
assert len(wave) == 22050
|
||||
|
||||
|
||||
def test_tabla_pattern_presets():
|
||||
"""All tabla patterns should load without error."""
|
||||
from pytheory.rhythm import Pattern
|
||||
for name in ["teental", "jhaptaal", "rupak", "dadra",
|
||||
"keherwa", "tabla solo", "tiri kita"]:
|
||||
p = Pattern.preset(name)
|
||||
assert p.beats > 0
|
||||
|
||||
|
||||
def test_world_drum_pattern_presets():
|
||||
"""All world drum patterns should load."""
|
||||
from pytheory.rhythm import Pattern
|
||||
for name in ["bhangra", "dhol chaal", "qawwali", "dholak folk",
|
||||
"adi talam", "mridangam korvai", "djembe", "kuku", "soli",
|
||||
"double kick", "metal blast", "metal groove", "metal gallop"]:
|
||||
p = Pattern.preset(name)
|
||||
assert p.beats > 0
|
||||
|
||||
|
||||
# ── Guitar presets with cabinet sim ──────────────────────────────────────────
|
||||
|
||||
def test_guitar_presets_have_cabinet():
|
||||
"""Distorted guitar presets should have cabinet simulation."""
|
||||
from pytheory import Score
|
||||
for preset in ["distorted_guitar", "orange_crunch", "metal_guitar"]:
|
||||
score = Score("4/4", bpm=120)
|
||||
p = score.part("g", instrument=preset)
|
||||
assert p.cabinet > 0, f"{preset} should have cabinet sim"
|
||||
|
||||
|
||||
def test_clean_guitar_preset():
|
||||
from pytheory import Score
|
||||
score = Score("4/4", bpm=120)
|
||||
p = score.part("g", instrument="clean_guitar")
|
||||
assert p.synth == "electric_guitar_synth"
|
||||
assert p.cabinet > 0
|
||||
|
||||
|
||||
# ── New instrument synths (v0.36+) ──────────────────────────────────────────
|
||||
|
||||
def test_new_synths_render():
|
||||
"""All 7 new synths produce valid audio."""
|
||||
from pytheory.play import (pedal_steel_wave, theremin_wave, kalimba_wave,
|
||||
steel_drum_wave, accordion_wave,
|
||||
didgeridoo_wave, bagpipe_wave,
|
||||
banjo_wave, mandolin_wave, ukulele_wave,
|
||||
vocal_wave, SAMPLE_RATE)
|
||||
synths = [pedal_steel_wave, theremin_wave, kalimba_wave, steel_drum_wave,
|
||||
accordion_wave, didgeridoo_wave, bagpipe_wave,
|
||||
banjo_wave, mandolin_wave, ukulele_wave, vocal_wave]
|
||||
for fn in synths:
|
||||
wave = fn(440, n_samples=11025)
|
||||
assert len(wave) == 11025
|
||||
assert wave.dtype == numpy.int16
|
||||
assert numpy.abs(wave).max() > 0
|
||||
|
||||
|
||||
def test_vocal_synth_with_lyric():
|
||||
"""Vocal synth accepts lyric parameter."""
|
||||
from pytheory.play import vocal_wave
|
||||
for lyric in ["ah", "ee", "oh", "oo", "hi", "la"]:
|
||||
wave = vocal_wave(330, n_samples=11025, lyric=lyric)
|
||||
assert len(wave) == 11025
|
||||
assert numpy.abs(wave).max() > 0
|
||||
|
||||
|
||||
def test_vocal_different_vowels_differ():
|
||||
"""Different vowels should produce different waveforms."""
|
||||
from pytheory.play import vocal_wave
|
||||
ah = vocal_wave(330, n_samples=22050, lyric="ah")
|
||||
ee = vocal_wave(330, n_samples=22050, lyric="ee")
|
||||
# They should differ (different formant peaks)
|
||||
assert not numpy.array_equal(ah, ee)
|
||||
|
||||
|
||||
def test_all_instrument_presets_create():
|
||||
"""Every instrument preset in INSTRUMENTS should create a valid Part."""
|
||||
from pytheory import Score
|
||||
from pytheory.rhythm import INSTRUMENTS
|
||||
for name in INSTRUMENTS:
|
||||
score = Score("4/4", bpm=120)
|
||||
p = score.part("test", instrument=name)
|
||||
assert p.synth is not None
|
||||
|
||||
|
||||
def test_new_instrument_presets():
|
||||
"""New instrument presets have correct synths."""
|
||||
from pytheory import Score
|
||||
presets = {
|
||||
"pedal_steel": "pedal_steel_synth",
|
||||
"theremin": "theremin_synth",
|
||||
"kalimba": "kalimba_synth",
|
||||
"steel_drum": "steel_drum_synth",
|
||||
"accordion": "accordion_synth",
|
||||
"didgeridoo": "didgeridoo_synth",
|
||||
"bagpipe": "bagpipe_synth",
|
||||
"banjo": "banjo_synth",
|
||||
"mandolin": "mandolin_synth",
|
||||
"ukulele": "ukulele_synth",
|
||||
}
|
||||
for name, expected_synth in presets.items():
|
||||
score = Score("4/4", bpm=120)
|
||||
p = score.part("t", instrument=name)
|
||||
assert p.synth == expected_synth, f"{name} has {p.synth}, expected {expected_synth}"
|
||||
|
||||
|
||||
# ── Cajón drums ─────────────────────────────────────────────────────────────
|
||||
|
||||
def test_cajon_sounds_render():
|
||||
from pytheory.play import _render_drum_hit
|
||||
from pytheory.rhythm import DrumSound
|
||||
for sound in [DrumSound.CAJON_BASS, DrumSound.CAJON_SLAP, DrumSound.CAJON_TAP]:
|
||||
wave = _render_drum_hit(sound.value, 22050)
|
||||
assert len(wave) == 22050
|
||||
assert wave.dtype == numpy.float32
|
||||
|
||||
|
||||
def test_cajon_patterns():
|
||||
from pytheory.rhythm import Pattern
|
||||
for name in ["cajon", "cajon rumba", "cajon folk"]:
|
||||
p = Pattern.preset(name)
|
||||
assert p.beats > 0
|
||||
|
||||
|
||||
# ── Pitch bends ─────────────────────────────────────────────────────────────
|
||||
|
||||
def test_pitch_bend_renders():
|
||||
"""Pitch bend should produce valid audio without errors."""
|
||||
from pytheory import Score, Duration
|
||||
from pytheory.play import render_score
|
||||
score = Score("4/4", bpm=120)
|
||||
p = score.part("t", instrument="electric_guitar")
|
||||
p.add("A4", Duration.HALF, bend=2, bend_type="smooth")
|
||||
p.add("A4", Duration.HALF, bend=-1, bend_type="late")
|
||||
p.add("A4", Duration.HALF, bend=3, bend_type="linear")
|
||||
p.add("A4", Duration.HALF)
|
||||
buf = render_score(score)
|
||||
assert len(buf) > 0
|
||||
|
||||
|
||||
def test_pitch_bend_types():
|
||||
"""All three bend types should work."""
|
||||
from pytheory.rhythm import Note, Duration
|
||||
for bt in ["smooth", "linear", "late"]:
|
||||
n = Note(tone=None, duration=Duration.QUARTER, bend=2, bend_type=bt)
|
||||
assert n.bend_type == bt
|
||||
|
||||
|
||||
# ── Roll method ─────────────────────────────────────────────────────────────
|
||||
|
||||
def test_roll_adds_notes():
|
||||
from pytheory import Score, Duration
|
||||
score = Score("4/4", bpm=120)
|
||||
p = score.part("t", instrument="timpani")
|
||||
p.roll("C3", Duration.WHOLE, velocity_start=30, velocity_end=100)
|
||||
assert len(p.notes) > 4 # should be many 16th notes
|
||||
|
||||
|
||||
def test_roll_velocity_ramp():
|
||||
from pytheory import Score, Duration
|
||||
score = Score("4/4", bpm=120)
|
||||
p = score.part("t", instrument="timpani")
|
||||
p.roll("C3", Duration.WHOLE, velocity_start=20, velocity_end=100)
|
||||
velocities = [n.velocity for n in p.notes]
|
||||
# First should be quieter than last
|
||||
assert velocities[0] < velocities[-1]
|
||||
|
||||
|
||||
def test_roll_custom_speed():
|
||||
from pytheory import Score, Duration
|
||||
score = Score("4/4", bpm=120)
|
||||
p = score.part("t", synth="sine")
|
||||
p.roll("A4", Duration.WHOLE, speed=0.125) # 32nd notes
|
||||
# 4 beats / 0.125 = 32 notes
|
||||
assert len(p.notes) == 32
|
||||
|
||||
|
||||
# ── Int tone names ──────────────────────────────────────────────────────────
|
||||
|
||||
def test_int_tone_name():
|
||||
from pytheory import Tone, TET
|
||||
edo = TET(22)
|
||||
t = Tone(0, octave=4, system=edo)
|
||||
assert t.name == "0"
|
||||
assert t.frequency == pytest.approx(440.0, rel=1e-3)
|
||||
|
||||
|
||||
def test_int_tone_wrapping():
|
||||
from pytheory import Tone, TET
|
||||
edo = TET(22)
|
||||
t = Tone(22, octave=4, system=edo)
|
||||
assert t.name == "0"
|
||||
assert t.octave == 5
|
||||
assert t.frequency == pytest.approx(880.0, rel=1e-3)
|
||||
|
||||
|
||||
def test_int_tone_negative():
|
||||
from pytheory import Tone, TET
|
||||
edo = TET(22)
|
||||
t = Tone(-1, octave=4, system=edo)
|
||||
assert t.name == "21"
|
||||
assert t.octave == 3
|
||||
|
||||
|
||||
def test_system_tone_method():
|
||||
from pytheory import TET
|
||||
edo = TET(19)
|
||||
t = edo.tone(5, octave=4)
|
||||
assert t.name == "5"
|
||||
assert t.octave == 4
|
||||
|
||||
|
||||
# ── B#/Cb octave boundary ──────────────────────────────────────────────────
|
||||
|
||||
def test_b_sharp_octave():
|
||||
t = Tone("B#4")
|
||||
assert t.octave == 5
|
||||
assert t.frequency == pytest.approx(Tone("C5").frequency, rel=1e-3)
|
||||
|
||||
|
||||
def test_c_flat_octave():
|
||||
t = Tone("Cb4")
|
||||
assert t.octave == 3
|
||||
assert t.frequency == pytest.approx(Tone("B3").frequency, rel=1e-3)
|
||||
|
||||
|
||||
# ── Note choking ────────────────────────────────────────────────────────────
|
||||
|
||||
def test_note_choking_renders():
|
||||
"""Fast repeated notes should render without errors (choking active)."""
|
||||
from pytheory import Score, Duration
|
||||
from pytheory.play import render_score
|
||||
score = Score("4/4", bpm=200)
|
||||
p = score.part("t", instrument="piano")
|
||||
for _ in range(32):
|
||||
p.add("C4", Duration.SIXTEENTH)
|
||||
buf = render_score(score)
|
||||
assert len(buf) > 0
|
||||
|
||||
|
||||
# ── Score system/temperament ───────────────────────────────────────────────
|
||||
|
||||
def test_score_temperament():
|
||||
from pytheory import Score
|
||||
score = Score("4/4", bpm=120, temperament="just")
|
||||
assert score.temperament == "just"
|
||||
|
||||
|
||||
def test_score_reference_pitch():
|
||||
from pytheory import Score
|
||||
score = Score("4/4", bpm=120, reference_pitch=415.0)
|
||||
assert score.reference_pitch == 415.0
|
||||
|
||||
|
||||
def test_score_system_propagates():
|
||||
from pytheory import Score, SYSTEMS
|
||||
shruti = SYSTEMS["shruti"]
|
||||
score = Score("4/4", bpm=120, system=shruti)
|
||||
p = score.part("t", synth="sine")
|
||||
assert p._system is shruti
|
||||
|
||||
|
||||
# ── Synth enum count ────────────────────────────────────────────────────────
|
||||
|
||||
def test_synth_enum_count():
|
||||
from pytheory.play import Synth
|
||||
assert len(Synth) == 42
|
||||
|
||||
|
||||
def test_all_synths_render_and_enum_match():
|
||||
"""Every Synth enum member should render valid audio."""
|
||||
from pytheory.play import Synth
|
||||
for s in Synth:
|
||||
wave = s(440, n_samples=1000)
|
||||
assert len(wave) == 1000
|
||||
|
||||
|
||||
# ── Articulations ────────────────────────────────────────────────────────
|
||||
|
||||
def test_articulation_field_on_note():
|
||||
from pytheory.rhythm import Note, Duration
|
||||
n = Note(tone=None, duration=Duration.QUARTER, articulation="staccato")
|
||||
assert n.articulation == "staccato"
|
||||
|
||||
|
||||
def test_articulation_default_empty():
|
||||
from pytheory.rhythm import Note, Duration
|
||||
n = Note(tone=None, duration=Duration.QUARTER)
|
||||
assert n.articulation == ""
|
||||
|
||||
|
||||
def test_part_add_articulation():
|
||||
score = pytheory.Score("4/4", bpm=120)
|
||||
p = score.part("test", synth="sine")
|
||||
p.add("C4", Duration.QUARTER, articulation="staccato")
|
||||
p.add("D4", Duration.QUARTER, articulation="legato")
|
||||
p.add("E4", Duration.QUARTER, articulation="marcato")
|
||||
p.add("F4", Duration.QUARTER, articulation="tenuto")
|
||||
p.add("G4", Duration.QUARTER, articulation="accent")
|
||||
p.add("A4", Duration.QUARTER, articulation="fermata")
|
||||
assert len(p.notes) == 6
|
||||
assert p.notes[0].articulation == "staccato"
|
||||
assert p.notes[5].articulation == "fermata"
|
||||
|
||||
|
||||
@needs_portaudio
|
||||
def test_articulations_render():
|
||||
"""Articulations should produce audio without errors."""
|
||||
from pytheory.play import render_score
|
||||
score = pytheory.Score("4/4", bpm=120)
|
||||
p = score.part("test", synth="sine", volume=0.3)
|
||||
for art in ["", "staccato", "legato", "marcato", "tenuto", "accent", "fermata"]:
|
||||
p.add("C4", Duration.QUARTER, articulation=art)
|
||||
buf = render_score(score)
|
||||
assert len(buf) > 0
|
||||
|
||||
|
||||
# ── Dynamic curves ───────────────────────────────────────────────────────
|
||||
|
||||
def test_crescendo_adds_notes():
|
||||
score = pytheory.Score("4/4", bpm=120)
|
||||
p = score.part("test", synth="sine")
|
||||
p.crescendo(["C4", "D4", "E4", "F4"], Duration.QUARTER,
|
||||
start_vel=40, end_vel=100)
|
||||
assert len(p.notes) == 4
|
||||
assert p.notes[0].velocity == 40
|
||||
assert p.notes[3].velocity == 100
|
||||
|
||||
|
||||
def test_decrescendo_adds_notes():
|
||||
score = pytheory.Score("4/4", bpm=120)
|
||||
p = score.part("test", synth="sine")
|
||||
p.decrescendo(["C4", "D4", "E4", "F4"], Duration.QUARTER,
|
||||
start_vel=110, end_vel=40)
|
||||
assert len(p.notes) == 4
|
||||
assert p.notes[0].velocity == 110
|
||||
assert p.notes[3].velocity == 40
|
||||
|
||||
|
||||
def test_swell_velocity_shape():
|
||||
score = pytheory.Score("4/4", bpm=120)
|
||||
p = score.part("test", synth="sine")
|
||||
p.swell(["C4", "D4", "E4", "F4", "G4"], Duration.QUARTER,
|
||||
low_vel=30, peak_vel=110)
|
||||
assert len(p.notes) == 5
|
||||
# First and last should be near low_vel
|
||||
assert p.notes[0].velocity == 30
|
||||
assert p.notes[4].velocity == 30
|
||||
# Middle should be at or near peak
|
||||
assert p.notes[2].velocity == 110
|
||||
|
||||
|
||||
def test_dynamics_custom_velocities():
|
||||
score = pytheory.Score("4/4", bpm=120)
|
||||
p = score.part("test", synth="sine")
|
||||
p.dynamics(["C4", "D4", "E4"], Duration.QUARTER,
|
||||
velocities=[50, 100, 75])
|
||||
assert p.notes[0].velocity == 50
|
||||
assert p.notes[1].velocity == 100
|
||||
assert p.notes[2].velocity == 75
|
||||
|
||||
|
||||
def test_dynamics_with_articulation():
|
||||
score = pytheory.Score("4/4", bpm=120)
|
||||
p = score.part("test", synth="sine")
|
||||
p.crescendo(["C4", "D4"], Duration.QUARTER,
|
||||
start_vel=40, end_vel=100, articulation="staccato")
|
||||
assert p.notes[0].articulation == "staccato"
|
||||
assert p.notes[1].articulation == "staccato"
|
||||
|
||||
|
||||
# ── Part.hit() ───────────────────────────────────────────────────────────
|
||||
|
||||
def test_part_hit_adds_note():
|
||||
from pytheory.rhythm import DrumSound, _DrumTone
|
||||
score = pytheory.Score("4/4", bpm=120)
|
||||
p = score.part("kit", synth="sine")
|
||||
p.hit(DrumSound.KICK, Duration.QUARTER, velocity=100)
|
||||
p.hit(DrumSound.SNARE, Duration.QUARTER, velocity=90, articulation="accent")
|
||||
assert len(p.notes) == 2
|
||||
assert isinstance(p.notes[0].tone, _DrumTone)
|
||||
assert p.notes[0].tone.sound == DrumSound.KICK
|
||||
assert p.notes[1].articulation == "accent"
|
||||
|
||||
|
||||
@needs_portaudio
|
||||
def test_part_hit_renders():
|
||||
"""Part.hit() drum sounds should render through the note pipeline."""
|
||||
from pytheory.rhythm import DrumSound
|
||||
from pytheory.play import render_score
|
||||
score = pytheory.Score("4/4", bpm=120)
|
||||
p = score.part("kit", synth="sine", volume=0.5)
|
||||
p.hit(DrumSound.KICK, Duration.QUARTER)
|
||||
p.hit(DrumSound.SNARE, Duration.QUARTER)
|
||||
p.hit(DrumSound.CLOSED_HAT, Duration.QUARTER)
|
||||
p.hit(DrumSound.CRASH, Duration.QUARTER)
|
||||
buf = render_score(score)
|
||||
assert len(buf) > 0
|
||||
|
||||
|
||||
# ── Part.ramp() ──────────────────────────────────────────────────────────
|
||||
|
||||
def test_ramp_generates_automation():
|
||||
score = pytheory.Score("4/4", bpm=120)
|
||||
p = score.part("test", synth="saw", lowpass=200)
|
||||
p.ramp(over=4.0, lowpass=8000)
|
||||
# Should have generated automation points
|
||||
assert len(p._automation) > 0
|
||||
# First point should be near 200, last near 8000
|
||||
first_lp = p._automation[0][1].get("lowpass", 0)
|
||||
last_lp = p._automation[-1][1].get("lowpass", 0)
|
||||
assert first_lp < 1000 # near start
|
||||
assert last_lp > 7000 # near target
|
||||
|
||||
|
||||
def test_ramp_easing_curves():
|
||||
score = pytheory.Score("4/4", bpm=120)
|
||||
for curve in ["linear", "ease_in", "ease_out", "ease_in_out"]:
|
||||
p = score.part(f"test_{curve}", synth="saw", lowpass=200)
|
||||
p.ramp(over=4.0, curve=curve, lowpass=8000)
|
||||
assert len(p._automation) > 0
|
||||
|
||||
|
||||
def test_ramp_multiple_params():
|
||||
score = pytheory.Score("4/4", bpm=120)
|
||||
p = score.part("test", synth="saw", lowpass=200)
|
||||
p.ramp(over=4.0, lowpass=8000, reverb=0.5)
|
||||
# Should have both params in automation points
|
||||
last_point = p._automation[-1][1]
|
||||
assert "lowpass" in last_point
|
||||
assert "reverb_mix" in last_point # mapped from "reverb"
|
||||
|
||||
|
||||
# ── Cross-choke ──────────────────────────────────────────────────────────
|
||||
|
||||
def test_djembe_patterns_exist():
|
||||
from pytheory.rhythm import Pattern
|
||||
for name in ["djembe", "kuku", "soli", "dununba", "tiriba",
|
||||
"yankadi", "djansa", "mendiani"]:
|
||||
p = Pattern.preset(name)
|
||||
assert p.beats > 0
|
||||
assert len(p.hits) > 0
|
||||
|
||||
|
||||
def test_djembe_fills_exist():
|
||||
from pytheory.rhythm import Pattern
|
||||
for name in ["djembe call", "djembe roll", "djembe break"]:
|
||||
f = Pattern.fill(name)
|
||||
assert f.beats == 4.0
|
||||
assert len(f.hits) > 0
|
||||
|
||||
|
||||
def test_cajon_fills_exist():
|
||||
from pytheory.rhythm import Pattern
|
||||
for name in ["cajon flam", "cajon rumble", "cajon breakdown"]:
|
||||
f = Pattern.fill(name)
|
||||
assert f.beats == 4.0
|
||||
assert len(f.hits) > 0
|
||||
|
||||
|
||||
def test_metal_fills_exist():
|
||||
from pytheory.rhythm import Pattern
|
||||
for name in ["metal triplet", "metal blast", "metal cascade"]:
|
||||
f = Pattern.fill(name)
|
||||
assert f.beats == 4.0
|
||||
assert len(f.hits) > 0
|
||||
|
||||
|
||||
# ── render_score in __all__ ──────────────────────────────────────────────
|
||||
|
||||
def test_render_score_exported():
|
||||
assert "render_score" in pytheory.__all__
|
||||
|
||||
@@ -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"
|
||||
@@ -495,14 +486,6 @@ wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/d3/ac/686789b9145413f1a61878c407210e41bfdb097976864e0913078b24098c/myst_parser-5.0.0-py3-none-any.whl", hash = "sha256:ab31e516024918296e169139072b81592336f2fef55b8986aa31c9f04b5f7211", size = 84533, upload-time = "2026-01-15T09:08:16.788Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "numeral"
|
||||
version = "0.1.0.17"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/17/0d/ac6a186e169fcbdfea316f78fb5e34981bcf8d5c1d7cc8b6581f597e1e4c/numeral-0.1.0.17-py2.py3-none-any.whl", hash = "sha256:7dff0c1efb9b3655c9c1dc93b4666993741b15abcac0dc01dcb96b21cc20f6ae", size = 22066, upload-time = "2020-04-12T08:24:59.129Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "numpy"
|
||||
version = "2.2.6"
|
||||
@@ -707,11 +690,9 @@ wheels = [
|
||||
|
||||
[[package]]
|
||||
name = "pytheory"
|
||||
version = "0.33.0"
|
||||
version = "0.39.3"
|
||||
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" },
|
||||
@@ -731,8 +712,6 @@ docs = [
|
||||
|
||||
[package.metadata]
|
||||
requires-dist = [
|
||||
{ name = "numeral" },
|
||||
{ name = "pytuning" },
|
||||
{ name = "scipy" },
|
||||
{ name = "sounddevice" },
|
||||
]
|
||||
@@ -744,19 +723,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 +1117,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