mirror of
https://github.com/kennethreitz/pytheory.git
synced 2026-06-05 23:00:20 +00:00
Compare commits
277 Commits
improvements
...
v0.36.4
| Author | SHA1 | Date | |
|---|---|---|---|
| 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 | |||
| 4fe7771d83 | |||
| 57079a43ac | |||
| 1d07b06968 | |||
| 9887b59cfb | |||
| 9850a8016e | |||
| 35f5f35dc5 | |||
| 47ca94111f | |||
| 62cfbb2591 | |||
| de855a3fe6 | |||
| dc9f7b3342 | |||
| 60fdff6d36 | |||
| f42d38d1fd | |||
| 5a4122d61f | |||
| 3e4ba54a32 | |||
| 5dd1c5e15d | |||
| e46732fb5a | |||
| 833ab56857 | |||
| 6b2b1e201e | |||
| f9c81fe05f | |||
| 931ec905c3 | |||
| 799ffbdac9 | |||
| b29b33524f | |||
| 25f25c1f23 | |||
| 3f1d632285 | |||
| 1938037458 | |||
| f7c05e1b31 | |||
| c375785bb9 | |||
| 9ebd54b7fc | |||
| ce68ad8f19 | |||
| f402e76480 | |||
| 4d3c7e0d6c | |||
| 5a74a6f715 | |||
| 5416674858 | |||
| 9a5f305ac6 | |||
| bc38ce73f0 | |||
| 081b924d29 | |||
| 427ff44ce9 | |||
| 360a908464 | |||
| a8dd4d6542 | |||
| 866b110afa | |||
| 0fc0b87017 | |||
| 1a724a94b0 | |||
| b239e9a997 | |||
| a766737707 | |||
| 0843c21884 | |||
| eb7a2bf27d | |||
| c78530611d | |||
| 7267d25fb5 | |||
| 53db299b5f | |||
| a4fa233edf | |||
| e7e90382c5 | |||
| 1ef32ecc92 | |||
| f9af708f0a | |||
| 0e93e45853 | |||
| f0802ae614 | |||
| 4a992eba2b | |||
| 398cc68166 | |||
| 3b0a63d57c | |||
| c86ae7b118 | |||
| d2044f1f53 | |||
| 7991516c3e | |||
| a0e0dbc807 | |||
| a77db557f3 | |||
| f4c3b2dd88 | |||
| 32387e2d23 | |||
| 2ecb1e5ce8 | |||
| 30dacc4fbf | |||
| 2a5ffcf78a | |||
| fe00644e3f | |||
| 89df5c4201 | |||
| b396f42f84 | |||
| 45789f7af0 | |||
| e0bf637de5 | |||
| e5f258bc21 | |||
| a294887215 | |||
| 20932f48ab | |||
| 9e0faf840b | |||
| c7c733044c | |||
| 866065d7d7 | |||
| aa9d4282c9 | |||
| 3593735243 | |||
| 54fd4c2241 | |||
| a4b11e6f35 | |||
| 044e9a7eac | |||
| 094887c849 | |||
| 2fc5aae678 | |||
| ac0cc0b6ce | |||
| ce5f3e7626 | |||
| 0c4ba83b0c | |||
| f81b1e882d | |||
| ebf26cfbfa | |||
| f9654fcdea | |||
| f3f4174783 | |||
| 119dd2d921 | |||
| 80698ccc3a | |||
| a41f20e805 | |||
| 2de263c814 | |||
| a7ad8a374b | |||
| e75c35a099 | |||
| b9f0a3870a | |||
| d425d6b624 | |||
| 8c61cb146b | |||
| a11523e889 | |||
| cf061e3783 | |||
| 5d746ed0b1 | |||
| 89323c0eb3 | |||
| 5a44d619d0 | |||
| f5bf7ce505 | |||
| 4f3b706336 | |||
| 83a988d085 | |||
| d0e8e43b56 | |||
| c67a08a34e | |||
| e72ef4a6a7 | |||
| 994c4e244a | |||
| adfbc3079a | |||
| 772fa84b4f | |||
| b97378c836 | |||
| f00cf10c41 | |||
| d57e780f6f | |||
| 4f03bb6616 | |||
| 4aafd8d0b0 | |||
| c74600d42f | |||
| b3ef0ddc58 | |||
| dd3b7bd03e | |||
| 62111de2da | |||
| c3ae02ec4f | |||
| f4d2cca663 | |||
| 3b5a07dfce | |||
| aa454ea7e9 | |||
| 5aed586187 | |||
| 09d90b3425 | |||
| 96131da59c | |||
| d2058668a6 | |||
| a5ffdc6104 | |||
| 724a0df7b5 | |||
| 4750061b87 | |||
| d53d8b60dd | |||
| de1db0aa8d | |||
| b22b3c063f | |||
| 7e1d9e76bd | |||
| 447d03a2d2 | |||
| 7b82d70ad6 | |||
| 44f8b902e2 | |||
| 03eb61cd5d | |||
| eba299d406 | |||
| d11c930308 | |||
| 19663ed6c5 | |||
| f949ca5b45 | |||
| d9f847603a | |||
| ee41691728 | |||
| 02df87af09 | |||
| b3110c6e0e | |||
| fd82dccbfd | |||
| 6f7f9008b0 | |||
| acb92171a1 | |||
| c006f5b3da | |||
| 9da3ac8b28 | |||
| e94ef5dcfd | |||
| a5e47c37cd | |||
| 8a9651f989 | |||
| cc4a25e70d | |||
| 904c61b2d6 | |||
| d23de92713 | |||
| e8bfeb884a | |||
| 6aad427fb8 | |||
| e9c630705e | |||
| e78ba203d9 | |||
| c307c1e41f | |||
| b1f6996cd7 | |||
| 51ca98779d | |||
| 37b41e1bbf | |||
| da40189845 | |||
| 54b82440c4 | |||
| ef003dbd1d | |||
| 890c3cfbe2 | |||
| 599a00f066 | |||
| 66b216cd08 | |||
| f6c110a995 | |||
| 9da0434774 | |||
| 59f69079bf | |||
| d8619f2e8f | |||
| db04ab0796 | |||
| 875f281633 | |||
| c53584d733 | |||
| 7153dc908f | |||
| 240d2564a4 | |||
| fd6ae888cf | |||
| c6e3f56136 | |||
| 5850ac00d5 | |||
| 74ce7e39f8 | |||
| d5beab46f2 | |||
| 5e09e64c2d | |||
| 8bf716d1db | |||
| fcfba20ec5 | |||
| 858f0c1c06 | |||
| 4b9534eb9b | |||
| fdb724f788 | |||
| 245a8a1257 | |||
| 4ab8be49a5 | |||
| 427a3fc3b1 | |||
| c3f9e98332 | |||
| e1c2ef03d7 | |||
| 21cd99425b | |||
| 69ddb1eb64 | |||
| 23c17589eb | |||
| e09dfe70b8 | |||
| cceac40a88 | |||
| da08d30e8d | |||
| e79b3ac15e | |||
| cb3fd4526a | |||
| 7229399135 | |||
| 0d740cd6fe |
@@ -0,0 +1,48 @@
|
||||
name: Docs
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [master]
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
pages: write
|
||||
id-token: write
|
||||
|
||||
concurrency:
|
||||
group: pages
|
||||
cancel-in-progress: false
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Install uv
|
||||
uses: astral-sh/setup-uv@v4
|
||||
|
||||
- name: Set up Python
|
||||
run: uv python install 3.13
|
||||
|
||||
- name: Install dependencies
|
||||
run: uv sync --all-groups
|
||||
|
||||
- name: Build docs
|
||||
run: uv run sphinx-build -b html docs docs/_build/html
|
||||
|
||||
- name: Upload artifact
|
||||
uses: actions/upload-pages-artifact@v3
|
||||
with:
|
||||
path: docs/_build/html
|
||||
|
||||
deploy:
|
||||
environment:
|
||||
name: github-pages
|
||||
url: ${{ steps.deployment.outputs.page_url }}
|
||||
runs-on: ubuntu-latest
|
||||
needs: build
|
||||
steps:
|
||||
- name: Deploy to GitHub Pages
|
||||
id: deployment
|
||||
uses: actions/deploy-pages@v4
|
||||
+580
@@ -0,0 +1,580 @@
|
||||
# Changelog
|
||||
|
||||
All notable changes to PyTheory are documented here.
|
||||
|
||||
## 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
|
||||
- **11 microtonal systems:**
|
||||
- `"shruti"` (22-TET Indian, 10 thaats with proper shruti intervals)
|
||||
- `"maqam"` (24-TET Arabic, quarter-tone Rast/Bayati/Hijaz + 7 more)
|
||||
- `"slendro"` (5-TET gamelan), `"pelog"` (9-TET gamelan with 3 pathet)
|
||||
- `"thai"` (7-TET, 171 cents/step)
|
||||
- `"makam"` (53-TET Turkish Arel-Ezgi-Uzdilek, 9 makams)
|
||||
- `"carnatic"` (72-TET, 10 melakartas)
|
||||
- `"19-tet"`, `"31-tet"` (historical Western)
|
||||
- `"bohlen-pierce"` (13 divisions of the tritave 3:1 — non-octave!)
|
||||
- **Just intonation** — `temperament="just"` for pure 5-limit ratios
|
||||
- **Historical pitch** — `Score(reference_pitch=415.0)` for Baroque A=415
|
||||
- **`Score(system=, temperament=, reference_pitch=)`** flows through to all playback
|
||||
- Per-system `c_index` and `period` replace hardcoded constants
|
||||
- Fixed all hardcoded `12`s in tone arithmetic
|
||||
- Song #22: Greensleeves (Renaissance lute, meantone, A=415)
|
||||
- 22 new microtonal tests (819 total)
|
||||
|
||||
## 0.32.1
|
||||
|
||||
- `Tone("X")` now raises `ValueError` immediately instead of silently accepting invalid names (#39)
|
||||
- Support enharmonic spellings: `Cb`, `Fb`, `E#`, `B#` resolve correctly (#40)
|
||||
- Support double sharps (`C##`, `Fx`) and double flats (`Dbb`) via semitone arithmetic (#41)
|
||||
- Accept unicode music symbols: `♯` `♭` `𝄪` `𝄫`
|
||||
|
||||
## 0.32.0
|
||||
|
||||
- **8 new synth engine features:**
|
||||
- Filter envelope: per-note lowpass sweep (`filter_amount`, `filter_attack`, `filter_decay`, `filter_sustain`)
|
||||
- Velocity → brightness: harder notes = brighter filter (`vel_to_filter`)
|
||||
- Sub-oscillator: octave-below sine for bass weight (`sub_osc`)
|
||||
- Tremolo: amplitude LFO modulation (`tremolo_depth`, `tremolo_rate`)
|
||||
- Saturation: even-harmonic tape/tube warmth (`saturation`)
|
||||
- Noise layer: per-note breath/air texture (`noise_mix`)
|
||||
- Phaser: swept allpass filter chain (`phaser`, `phaser_rate`)
|
||||
- Configurable FM: `fm_ratio` and `fm_index` params
|
||||
- **Highpass filter** (12 dB/oct biquad) on any part
|
||||
- **2 new envelopes:** `bowed` (bow attack with sustain), `mallet` (strike with ringing sustain)
|
||||
- **Improved `strings_synth`:** additive synthesis with body resonance curve, per-harmonic phase randomization, delayed vibrato onset, bow pressure variation
|
||||
- **Instrument preset overhaul:** every preset sanity-checked against real instrument behavior
|
||||
- Mallet instruments (vibraphone, celesta, music box, glockenspiel, tubular bells) now ring properly
|
||||
- Trumpet uses sustaining envelope instead of pluck
|
||||
- Woodwinds have breath noise, brass has velocity brightness
|
||||
- Bass instruments have sub-oscillators, synth presets have filter envelopes
|
||||
- Piano has velocity-to-brightness and subtle hammer noise
|
||||
- Signal chain: saturation → tremolo → distortion → chorus → phaser → highpass → lowpass → delay → reverb
|
||||
- Song #21: Cinematic Showcase (Orchestral)
|
||||
|
||||
## 0.31.0
|
||||
|
||||
- 3 new synth engines: Karplus-Strong pluck, Hammond organ, string ensemble with body formants
|
||||
- 38 instrument presets: `score.part("lead", instrument="violin")`
|
||||
- Keys, strings, woodwinds, brass, plucked, synth, and mallet categories
|
||||
- 13 total synth waveforms
|
||||
|
||||
## 0.30.0
|
||||
|
||||
- Drums are a real Part — same effects pipeline as any voice
|
||||
- `score.drums("rock", split=True)` splits kit into kick/snare/hats/toms/cymbals/percussion Parts
|
||||
- Each split Part gets independent effects (reverb on snare, LP on hats, etc.)
|
||||
- `set_drum_effects()` applies to all drum Parts (split or not)
|
||||
- Sidechain triggers on kick only — hats and snare don't duck the pad
|
||||
- MIDI import via `Score.from_midi(path)`
|
||||
|
||||
## 0.29.3
|
||||
|
||||
- Drums are now a real Part — same effects pipeline as any other voice, zero code duplication
|
||||
- `score.parts["drums"]` is a standard Part with reverb, delay, lowpass, etc.
|
||||
- `set_drum_effects()` is sugar over the Part's attributes
|
||||
|
||||
## 0.29.2
|
||||
|
||||
- Add `score.set_drum_effects()` — reverb, delay, lowpass, distortion, chorus on the drum bus
|
||||
- Same effects engine as parts, zero code duplication
|
||||
|
||||
## 0.29.1
|
||||
|
||||
- Rename song.py → songs.py
|
||||
- Polish all 20 example songs with stereo, convolution reverb, humanize, detune, sidechain
|
||||
|
||||
## 0.29.0
|
||||
|
||||
- Add `Score.from_midi(path)` — import any Standard MIDI File into a Score
|
||||
- Minimal zero-dependency MIDI parser (Type 0 and Type 1)
|
||||
- Each channel becomes a named Part, channel 10 becomes drum hits
|
||||
- Tempo, time signature, velocities, and note durations preserved
|
||||
- Roundtrip: save_midi → from_midi works
|
||||
|
||||
## 0.28.3
|
||||
|
||||
- Rewrite `pytheory demo` — 8 moods with stereo, effects, humanize, convolution reverb, sidechain
|
||||
- Added Dub and Temple moods
|
||||
|
||||
## 0.28.2
|
||||
|
||||
- Lower drum_humanize default to 0.15 — tighter, more professional feel
|
||||
|
||||
## 0.28.1
|
||||
|
||||
- Humanize drum hits — random timing jitter and velocity variation (default 0.3)
|
||||
- Control via `Score(drum_humanize=0.5)` — 0.0 = quantized, 0.3 = natural, 0.5+ = loose
|
||||
|
||||
## 0.28.0
|
||||
|
||||
- Add figured bass notation: `Chord.figured_bass` and `Chord.analyze_figured()` for classical inversion symbols
|
||||
- Add pitch class set theory: `pitch_classes`, `normal_form`, `prime_form`, `forte_number` on Chord
|
||||
- Add `Scale.recommend()` — ranked scale suggestions for a set of notes
|
||||
- Forte number catalog covers all trichords and tetrachords
|
||||
|
||||
## 0.27.1
|
||||
|
||||
- Tab completion in REPL — context-aware for commands, drum presets, synths, envelopes, chords, notes, systems
|
||||
|
||||
## 0.27.0
|
||||
|
||||
- Rewrite all 15 drum sounds for higher quality (inharmonic partials, proper transients, multi-mode resonance, saturation)
|
||||
- 19 example songs including Dance Party at the Reitz House
|
||||
|
||||
## 0.26.3
|
||||
|
||||
- Stereo drum panning — each sound placed in the stereo field (hat right, crash left, toms spread, kick/snare center)
|
||||
- Stereo convolution reverb — different IR per L/R channel for all 7 presets
|
||||
- 2 new songs: Neon Grid (stereo acid), Glass and Silk (sine+triangle waltz)
|
||||
|
||||
## 0.26.2
|
||||
|
||||
- Stereo convolution reverb — different IR per L/R channel for all 7 presets
|
||||
- Both algorithmic and convolution reverbs now output true stereo
|
||||
|
||||
## 0.26.1
|
||||
|
||||
- Stereo reverb — L and R channels get different early reflection patterns for natural width
|
||||
- Effects chain now skips mono reverb in favor of stereo reverb in the mixer
|
||||
|
||||
## 0.26.0
|
||||
|
||||
- **Stereo output** — render_score() now returns stereo (N, 2) arrays
|
||||
- Add `pan` parameter: -1.0 (left) to 1.0 (right), constant-power panning
|
||||
- Add `spread` parameter: detuned oscillators spread across L/R channels
|
||||
- Master bus compressor runs per-channel for stereo
|
||||
- All playback functions handle stereo natively
|
||||
|
||||
## 0.25.7
|
||||
|
||||
- Add `detune` parameter — ±cents oscillator spread on any synth (3 oscillators per note)
|
||||
- Swing now applies to drum hits (offbeats shift with the groove)
|
||||
- Improved snare and hi-hat sounds (metallic harmonics, faster attack)
|
||||
|
||||
## 0.25.6
|
||||
|
||||
- Swing now applies to drum hits — offbeats shift with the groove, everything locks into the same pocket
|
||||
- Improved snare: 220Hz body, transient click, tanh saturation
|
||||
- Improved hi-hats: metallic harmonics (6k+8.5k+12k Hz), crisper attack, shorter decay
|
||||
|
||||
## 0.25.5
|
||||
|
||||
- Improved snare: 220Hz body, transient click, tanh saturation — snappier and more present
|
||||
- Improved hi-hats: metallic harmonics (6k+8.5k+12k Hz), shorter decay, crisper attack
|
||||
|
||||
## 0.25.4
|
||||
|
||||
- Add master bus compressor/limiter — louder, punchier, more cohesive mixes
|
||||
- Feed-forward compression with configurable threshold, ratio, attack, release
|
||||
- Makeup gain restores loudness after compression
|
||||
- Brick-wall limiter at 0.95 prevents clipping
|
||||
- Replaces simple normalization in render_score()
|
||||
|
||||
## 0.25.3
|
||||
|
||||
- Add `pytheory repl` — interactive music theory scratchpad and composition tool
|
||||
- Context-aware prompt shows key, bpm, drums, active part + effects
|
||||
- Theory commands: key, chords, modes, scales, circle, interval, identify, system
|
||||
- Composition: drums, part, add, rest, arp, prog, effects, automation, LFO
|
||||
- Guitar: fingering, scale diagram
|
||||
- 6 musical systems with correct default tonics
|
||||
- REPL guide documentation
|
||||
|
||||
## 0.25.1
|
||||
|
||||
- Add `pytheory demo` CLI command — plays a randomly generated track, different every time
|
||||
- Rewrite README to showcase the full feature set (composition, effects, drums, MIDI export)
|
||||
|
||||
## 0.25.0
|
||||
|
||||
- Add sidechain compression — kick ducks pad/bass for the classic EDM pump effect
|
||||
- Add song structure: `score.section("verse")`, `score.section("chorus")`, `score.repeat("verse")`
|
||||
- Punchier kick drum: 808-style with faster pitch sweep (200→45Hz), sub thump, and soft saturation
|
||||
- Section repeat copies all part notes, drum hits, and automation with proper offset
|
||||
|
||||
## 0.24.1
|
||||
|
||||
- Add `humanize` parameter on Parts — random micro-timing and velocity variation
|
||||
- Makes programmed parts feel like a real player (0.1 = subtle, 0.3 = natural, 0.5+ = loose)
|
||||
|
||||
## 0.24.0
|
||||
|
||||
- Add per-note velocity: `lead.add("C5", Duration.QUARTER, velocity=90)` — dynamics, accents, ghost notes
|
||||
- Add swing/groove: `Score("4/4", bpm=120, swing=0.5)` — shuffles every other note for human feel
|
||||
- Add tempo changes mid-song: `score.set_tempo(140)` — accelerando, ritardando, tempo drops
|
||||
- Add `Part.fade_in(bars)` and `Part.fade_out(bars)` — volume envelopes over sections
|
||||
- Arpeggiator supports velocity parameter
|
||||
- Per-part swing override (set independently from score swing)
|
||||
- Tempo map engine: beat-to-sample conversion handles variable BPM throughout a score
|
||||
|
||||
## 0.23.0
|
||||
|
||||
- Add convolution reverb with 7 synthetic impulse responses: Taj Mahal, cathedral, plate, spring, cave, parking garage, canyon
|
||||
- Each IR models real acoustic properties: early reflections, frequency-dependent absorption, diffusion density, and modulation
|
||||
- FFT-based convolution via `scipy.signal.fftconvolve` for fast processing even with long tails (12s Taj Mahal)
|
||||
- Select via `reverb_type` parameter on `Score.part()` — drop-in alongside existing algorithmic reverb
|
||||
- IR cache for zero-cost reuse across parts
|
||||
- Automatable via `Part.set(reverb_type="cathedral")` mid-song
|
||||
|
||||
## 0.22.0
|
||||
|
||||
- Add `Part.lfo()` for automated parameter modulation (filter sweeps, tremolo, auto-wah)
|
||||
- 4 LFO shapes: sine, triangle, saw, square
|
||||
- Configurable rate (cycles per bar), min/max range, duration, and resolution
|
||||
- Stack multiple LFOs on different parameters for complex modulation
|
||||
|
||||
## 0.21.0
|
||||
|
||||
- Add `Part.set()` for mid-song effect automation (filter sweeps, reverb swells, distortion kicks)
|
||||
- Add chorus effect (LFO-modulated delay, Juno-style)
|
||||
- Renderer segments audio at automation points for per-section effect processing
|
||||
- Updated effect chain: distortion → chorus → lowpass → delay → reverb
|
||||
- Document automation, chorus, and updated signal chain
|
||||
|
||||
## 0.20.0
|
||||
|
||||
- Add `Part.arpeggio()` — arpeggiator with up/down/updown/downup/random patterns, octave spanning
|
||||
- Fix Roman numeral parser to handle flat/sharp degree prefixes (bVI, bVII, bIII, #IV)
|
||||
- Add `song_showoff.py` — generative composition that's different every time, uses every feature
|
||||
- 4 mood palettes (dark, bright, ethereal, aggressive) with matched keys, progressions, drums, and effects
|
||||
|
||||
## 0.19.1
|
||||
|
||||
- Add `Part.arpeggio()` — arpeggiator with up/down/updown/downup/random patterns, octave spanning, and division control
|
||||
- Arpeggiator chains with legato + glide for classic acid/trance sequencer sound
|
||||
- Rename rhythm docs to "Sequencing: Rhythm and Scores"
|
||||
- Document arpeggiator, legato, and glide in rhythm guide
|
||||
|
||||
## 0.19.0
|
||||
|
||||
- Add legato mode for parts — continuous waveform without retriggering envelope per note
|
||||
- Add glide/portamento — smooth pitch slides between consecutive notes (303-style)
|
||||
- Legato renders entire phrase as one oscillator with phase-accumulating frequency changes
|
||||
- Glide uses exponential interpolation for perceptually linear pitch slides
|
||||
|
||||
## 0.18.1
|
||||
|
||||
- Add distortion effect (tanh soft-clip waveshaping) with drive and mix controls
|
||||
- 3 new example songs: Dub Delay Madness (separate delay snare), Liquid DnB (174bpm), Late Night Texts (Drake-style trap)
|
||||
- 16 total songs in the song player
|
||||
|
||||
## 0.18.0
|
||||
|
||||
- Add per-part audio effects: reverb, delay, and lowpass filter
|
||||
- Reverb: Schroeder algorithm with configurable mix and decay
|
||||
- Delay: tempo-synced echoes with feedback control
|
||||
- Lowpass: 12 dB/octave biquad filter with resonance (Q) control
|
||||
- All effects set at part creation: `score.part("lead", reverb=0.3, delay=0.25, lowpass=2000, lowpass_q=1.5)`
|
||||
- Effects applied per-part before mixing for independent processing
|
||||
|
||||
## 0.17.0
|
||||
|
||||
- Add 10 new groove presets: country, ska, dub, jungle, techno, gospel, swing, bolero, tango, flamenco (58 total)
|
||||
- Add 10 new fill presets: reggae, afrobeat, bossa nova, house, trap, hip hop, disco, cumbia, highlife, second line (21 total)
|
||||
- Every major genre family now has matching groove + fill presets
|
||||
|
||||
## 0.16.0
|
||||
|
||||
- Add drum fill system with 11 genre-specific presets: rock, rock crash, jazz, jazz brush, salsa, samba, funk, metal, blast, buildup, breakdown
|
||||
- `Pattern.fill("rock")` returns a 1-bar fill pattern
|
||||
- `Score.fill("rock")` inserts a fill at the current position
|
||||
- `Score.drums("rock", repeats=8, fill="rock", fill_every=4)` auto-fills every Nth bar
|
||||
- Without `fill_every`, fill replaces only the last bar
|
||||
|
||||
## 0.15.1
|
||||
|
||||
- Add `Synth.PWM_SLOW` and `Synth.PWM_FAST` — pulse width modulation with LFO sweep (Juno-style pads)
|
||||
- Add `Score.drums()` shorthand for `score.add_pattern(Pattern.preset(...), repeats=...)`
|
||||
- Update all docs to use `score.drums()` syntax and document all 10 synth waveforms
|
||||
|
||||
## 0.15.0
|
||||
|
||||
- Add 5 new synth waveforms: `Synth.SQUARE`, `Synth.PULSE`, `Synth.FM`, `Synth.NOISE`, `Synth.SUPERSAW`
|
||||
- Square wave: classic chiptune / 8-bit sound (odd harmonics at 1/n)
|
||||
- Pulse wave: variable duty cycle for NES-style timbres (25%, 12.5%)
|
||||
- FM synthesis: DX7-style frequency modulation (electric piano, bells, brass, metallic)
|
||||
- Noise: white noise for percussion textures and effects
|
||||
- Supersaw: 7 detuned saw oscillators for trance/EDM pads
|
||||
- All 8 synths available in both the API (`Synth.FM`) and Part strings (`synth="fm"`)
|
||||
- CLI play command supports all 8 waveforms
|
||||
|
||||
## 0.14.0
|
||||
|
||||
- Add `Part` class for multi-voice Score arrangements (lead, bass, pads, etc.)
|
||||
- `Score.part()` creates named parts with independent synth, envelope, and volume
|
||||
- `Score.add_pattern()` for attaching drum patterns
|
||||
- `render_score()` exported for headless buffer rendering
|
||||
- Parts accept raw float beat values alongside `Duration` enums
|
||||
- All 10 example songs rewritten with drums + chords + lead + bass parts
|
||||
|
||||
## 0.13.1
|
||||
|
||||
- Fix drum pattern repeats: hits now correctly offset across cycles instead of piling up on the first bar
|
||||
|
||||
## 0.13.0
|
||||
|
||||
- Add drum synthesizer with 27 individual instrument voices (kick, snare, hat, conga, timbale, etc.)
|
||||
- Add `play_pattern()` for playing drum patterns through the speakers
|
||||
- Add `play_score()` for playing mixed drum patterns + chord progressions together
|
||||
- Every `DrumSound` has a dedicated synthesis algorithm (pitch sweeps, noise bursts, membrane resonance, metallic rings)
|
||||
|
||||
## 0.12.0
|
||||
|
||||
- Add rhythm module: `Duration`, `TimeSignature`, `Note`, `Rest`, `Score`
|
||||
- `Duration` enum with 8 note lengths (whole through sixteenth, dotted, triplet)
|
||||
- `TimeSignature` with string parsing ("4/4", "3/4", "6/8", "12/8") and beats_per_measure
|
||||
- `Score` class with fluent `.add()` / `.rest()` chaining, measure counting, and `save_midi()` export
|
||||
- Measure-aware MIDI export with proper time signature and tempo meta events
|
||||
- Add `DrumSound` enum with 27 General MIDI percussion sounds
|
||||
- Add `Pattern` class with 48 drum pattern presets covering:
|
||||
- **Rock/Pop**: rock, half time, double time, disco, motown, train beat
|
||||
- **Jazz**: jazz, bebop, shuffle, linear, paradiddle
|
||||
- **Latin**: salsa, bossa nova, samba, cumbia, merengue, baiao, maracatu
|
||||
- **Afro-Cuban**: son clave 3-2/2-3, rumba clave 3-2/2-3, cascara, guaguanco, mozambique, nanigo, bembe, 6/8 afro-cuban, tresillo, habanera
|
||||
- **African**: afrobeat, highlife
|
||||
- **Caribbean**: reggae, dancehall
|
||||
- **Electronic**: house, trap, drum and bass, breakbeat
|
||||
- **Metal/Punk**: metal, blast beat, punk
|
||||
- **Other**: funk, hip hop, bo diddley, second line, new orleans, waltz, 12/8 blues
|
||||
- `Pattern.to_score()` renders drum patterns to Score for MIDI export
|
||||
|
||||
## 0.11.0
|
||||
|
||||
- Add drop voicings: `Chord.close_voicing()`, `Chord.open_voicing()`, `Chord.drop2()`, `Chord.drop3()`
|
||||
- Add `Key.modulation_path(target)` for chord-by-chord modulation suggestions via pivot chords
|
||||
- Add `Scale.degree_name(n)` returning traditional names (tonic, dominant, leading tone, etc.)
|
||||
- Add `Chord.extensions()` to suggest available 9th/11th/13th extensions
|
||||
- Add `Tone.solfege` property for fixed-Do solfege syllables (Do, Re, Mi, Fi, etc.)
|
||||
- Add CLI `identify` command for full chord analysis from a symbol
|
||||
- Add CLI `midi` command for exporting progressions to Standard MIDI Files
|
||||
- Expand documentation: solfege, Helmholtz, cents, slash chords, drop voicings, chord extensions, borrowed chord analysis, ADSR envelopes, MIDI export, new CLI commands
|
||||
|
||||
## 0.10.0
|
||||
|
||||
- Add `Scale.fitness()` to score how well a set of notes fits a scale (0.0–1.0)
|
||||
- Add `Key.suggest_next(chord)` for chord progression suggestions based on functional harmony
|
||||
- Add `Tone.helmholtz` and `Tone.scientific` properties for alternate pitch notation
|
||||
- Add `Chord.slash(bass)` and `Chord.slash_name` for slash chord notation (C/G, Am/E)
|
||||
- Add `save_midi()` for exporting tones, chords, and progressions as Standard MIDI Files
|
||||
- Add chord tone highlighting in `Fretboard.scale_diagram()` — chord tones uppercase, passing tones lowercase
|
||||
- Extend `Chord.analyze()` to recognize borrowed chords (bVI, bVII, bIII, etc.)
|
||||
|
||||
## 0.9.0
|
||||
|
||||
- Add ADSR envelope system with 8 presets: `Envelope.PIANO`, `ORGAN`, `PLUCK`, `PAD`, `STRINGS`, `BELL`, `STACCATO`, `NONE`
|
||||
- Add `Chord.from_symbol()` parser — handles any standard chord symbol (e.g. "F#m7b5", "Bbmaj9", "Gsus4") without lookup tables
|
||||
- Add `Key.pivot_chords(target)` for finding modulation pivot chords between two keys
|
||||
- Add `Scale.parallel_modes()` to show all modes sharing the same notes (C major → D dorian, E phrygian, etc.)
|
||||
- Add `Tone.cents_difference(other)` for measuring fine pitch differences in cents
|
||||
- Add `--envelope` flag to CLI play command
|
||||
- CLI play command now uses `Chord.from_symbol()` for broader chord parsing
|
||||
- Replace hardcoded `c_index = 3` with named `C_INDEX` constant throughout
|
||||
|
||||
## 0.8.3
|
||||
|
||||
- Add `Chord.symbol` property for standard shorthand notation (Cmaj7, Dm, G7, m7b5, etc.)
|
||||
- Add `Key.common_progressions()` to realize all named progressions in a key
|
||||
- Add CLI commands: `modes`, `circle`, `progressions`
|
||||
|
||||
## 0.8.2
|
||||
|
||||
- Use flat spellings in CHARTS `acceptable_tone_names` (e.g. Bbm now shows Bb/Db/F instead of A#/C#/F)
|
||||
|
||||
## 0.8.1
|
||||
|
||||
- Use musically correct flat spellings in flat keys (F major gives Bb, not A#)
|
||||
|
||||
## 0.8.0
|
||||
|
||||
- Add `Fretboard.scale_diagram()` for visual scale layouts on any instrument
|
||||
- Add `play_progression()` for sequential chord playback with gaps
|
||||
- Add cookbook documentation page with practical recipes
|
||||
- Curated guitar fingering overrides for common open chords
|
||||
- Fingering memoization with bounded cache, barre detection, 4-fret span constraint
|
||||
- API ergonomics: `Fretboard.chord()`, convenience constructors, slow test markers
|
||||
|
||||
## 0.7.0
|
||||
|
||||
- Add `Fretboard.chord()` method for named chord lookups
|
||||
- Improve fingering algorithm with better voicing selection
|
||||
- Rewrite all documentation in REPL style with verified output
|
||||
|
||||
## 0.6.1
|
||||
|
||||
- Fix sawtooth and triangle wave generation
|
||||
- Add WAV export via `save()`
|
||||
- Add CLI tests and play module tests
|
||||
- Skip play module tests when PortAudio is not available
|
||||
|
||||
## 0.6.0
|
||||
|
||||
- Support flat note names (Db, Bb, Eb, etc.) throughout the system
|
||||
- Add `Fingering` class for labeled chord fingerings
|
||||
- Add `pytheory play` CLI command for playing notes and chords
|
||||
- Add 12 example scripts showcasing pytheory features
|
||||
- Expand documentation with undocumented features and CLI guide
|
||||
|
||||
## 0.4.1
|
||||
|
||||
- Add `--temperament` flag to CLI tone command
|
||||
- Add Symbolic Pitch section to tones docs
|
||||
|
||||
## 0.4.0
|
||||
|
||||
- Add key signatures, scale diagrams, chord building, and progression analysis
|
||||
- Add CLI tool (`pytheory tone`, `pytheory chord`, `pytheory key`, etc.)
|
||||
- Add Jupyter notebook tutorial
|
||||
- Improve test coverage from 93% to 97% (476 tests)
|
||||
- Add type hints, docstrings, and property caching throughout
|
||||
|
||||
## 0.3.2
|
||||
|
||||
- Add type hints and docstrings throughout the library
|
||||
|
||||
## 0.3.1
|
||||
|
||||
- Add capo support, chord merging (`+`), tritone substitution
|
||||
- Add secondary dominants, Nashville number system
|
||||
- Add more common progressions (blues, jazz, flamenco, modal)
|
||||
|
||||
## 0.3.0
|
||||
|
||||
- Add interval naming (`Tone.interval_to()`)
|
||||
- Add MIDI conversion (`Tone.midi`, `Tone.from_midi()`)
|
||||
- Add `Tone.from_frequency()`, `Tone.transpose()`
|
||||
- Add `Chord.root`, `Chord.quality` properties
|
||||
- Add `Chord.from_name()`, `Chord.from_intervals()`, `Chord.from_midi_message()`
|
||||
- Add `Interval` constants (MINOR_THIRD, PERFECT_FIFTH, etc.)
|
||||
- Add `PROGRESSIONS` dict with common named progressions
|
||||
- Add `Tone.enharmonic` property
|
||||
- Add inversions, harmonize, and Roman numeral progressions
|
||||
- Add `Key` class with detection, signatures, relative/parallel keys
|
||||
- Add `Scale.detect()` and `Chord.from_tones()` convenience constructors
|
||||
- Add 25 instrument presets (mandolin family, violin family, banjo, harp, world instruments, keyboard)
|
||||
- Add `Tone.circle_of_fifths()` and `Tone.circle_of_fourths()`
|
||||
- Add chord identification (17 types), voice leading, tension scoring
|
||||
- Add beat frequencies, Plomp-Levelt dissonance model, harmony scoring
|
||||
|
||||
## 0.2.0
|
||||
|
||||
- Add `Fretboard` class for guitar fretboards
|
||||
- Add `play()` function with sine, sawtooth, and triangle wave synthesis
|
||||
- Add chord harmony and dissonance calculations
|
||||
- Modernize project structure (pyproject.toml, sounddevice)
|
||||
|
||||
## 0.1.0
|
||||
|
||||
- Initial release
|
||||
- Western 12-tone system with tones, scales, and basic chord support
|
||||
- Temperament support (equal, Pythagorean, meantone)
|
||||
- Indian (Hindustani), Arabic, Japanese, Blues, and Gamelan systems
|
||||
@@ -0,0 +1,38 @@
|
||||
# Claude Code Instructions
|
||||
|
||||
## Release Process
|
||||
|
||||
When releasing to PyPI, always do all three:
|
||||
|
||||
1. **Tag the commit**: `git tag v0.X.Y`
|
||||
2. **Push the tag**: `git push origin --tags`
|
||||
3. **Create a GitHub release**: `gh release create v0.X.Y --title "v0.X.Y: Short description" --notes "Release notes" --latest`
|
||||
|
||||
Don't forget to update `CHANGELOG.md` *before* the release commit.
|
||||
|
||||
## Version Bumping
|
||||
|
||||
- `pyproject.toml` and `pytheory/__init__.py` must match
|
||||
- Run `uv lock` after changing the version
|
||||
- Patch releases (0.X.Y) for bug fixes and small additions
|
||||
- Minor releases (0.X.0) for new features
|
||||
|
||||
## Testing
|
||||
|
||||
```
|
||||
uv run python -m pytest test_pytheory.py -x -q --tb=short -m "not slow"
|
||||
```
|
||||
|
||||
## Publishing
|
||||
|
||||
```
|
||||
uv build && uv publish --token <token> dist/pytheory-0.X.Y*
|
||||
```
|
||||
|
||||
## Music Preferences
|
||||
|
||||
- Detune: keep at 8-15, don't go above 25
|
||||
- Humanize: 0.2 is the sweet spot for melodic parts
|
||||
- Drum humanize: 0.15 default is good
|
||||
- No swing unless specifically asked
|
||||
- Sine and triangle are underrated — use them more
|
||||
@@ -0,0 +1,21 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2026 Kenneth Reitz
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
@@ -1,57 +1,161 @@
|
||||
# PyTheory: Music Theory for Humans
|
||||
|
||||
This (work in progress) library attempts to make exploring music theory approachable to humans.
|
||||
Explore music theory, compose multi-part arrangements, and export to MIDI — all in Python.
|
||||
|
||||

|
||||
|
||||
## True Scale -> Pitch Evaluation
|
||||
|
||||
```pycon
|
||||
>>> from pytheory import TonedScale
|
||||
|
||||
>>> c_minor = TonedScale(tonic='C4')['minor']
|
||||
|
||||
>>> c_minor
|
||||
<Scale I=C4 II=D4 III=Eb4 IV=F4 V=G4 VI=Ab4 VII=Bb5 VIII=C5>
|
||||
|
||||
>>> c_minor[0].pitch()
|
||||
523.251130601197
|
||||
|
||||
>>> c_minor["I"].pitch(symbolic=True)
|
||||
440*2**(1/4)
|
||||
|
||||
>>> c_minor["tonic"].pitch(temperament='pythagorean', symbolic=True)
|
||||
14080/27
|
||||
```
|
||||
$ pip install pytheory
|
||||
```
|
||||
|
||||
## Audibly play a note (or chord)
|
||||
## Sketch Ideas Fast
|
||||
|
||||
>>> from pytheory import play
|
||||
play(c_minor[0], t=1_000)
|
||||
```python
|
||||
from pytheory import Score, Pattern, Key, Duration, Chord
|
||||
from pytheory.play import play_score
|
||||
|
||||
score = Score("4/4", bpm=140)
|
||||
score.drums("bossa nova", repeats=4)
|
||||
|
||||
## Chord Fingerings for Custom Tunings
|
||||
chords = score.part("chords", synth="fm", envelope="pad", reverb=0.4)
|
||||
lead = score.part("lead", synth="saw", envelope="pluck", delay=0.3, lowpass=3000)
|
||||
bass = score.part("bass", synth="sine", lowpass=500)
|
||||
|
||||
```pycon
|
||||
>>> from pytheory import Tone, Fretboard, CHARTS
|
||||
for sym in ["Am", "Dm", "E7", "Am"]:
|
||||
chords.add(Chord.from_symbol(sym), Duration.WHOLE)
|
||||
chords.add(Chord.from_symbol(sym), Duration.WHOLE)
|
||||
|
||||
>>> tones = (
|
||||
... Tone.from_string("F2"),
|
||||
... Tone.from_string("C3"),
|
||||
... Tone.from_string("G3"),
|
||||
... Tone.from_string("D4"),
|
||||
... Tone.from_string("A5"),
|
||||
... Tone.from_string("E5")
|
||||
... )
|
||||
lead.arpeggio("Am", bars=2, pattern="updown", octaves=2)
|
||||
lead.arpeggio("Dm", bars=2, pattern="updown", octaves=2)
|
||||
lead.set(lowpass=5000, reverb=0.4)
|
||||
lead.arpeggio("E7", bars=2, pattern="up", octaves=2)
|
||||
lead.arpeggio("Am", bars=2, pattern="updown", octaves=2)
|
||||
|
||||
>>> fretboard = Fretboard(tones=tones)
|
||||
>>>
|
||||
>>> c_chord = CHARTS['western']["C"]
|
||||
for n in ["A2", "E2", "A2", "C3"] * 4:
|
||||
bass.add(n, Duration.QUARTER)
|
||||
|
||||
>>> print(c_chord.fingering(fretboard=fretboard))
|
||||
(0, 0, 0, 3, 3, 3)
|
||||
play_score(score) # hear it now
|
||||
score.save_midi("sketch.mid") # open in your DAW
|
||||
```
|
||||
|
||||
It can also [generate charts for all known chords](https://gist.github.com/kennethreitz/b363660145064fc330c206294cff92fc) for any instrument (accuracy to be determined!).
|
||||
## Hear It Instantly
|
||||
|
||||
✨🍰✨
|
||||
```
|
||||
$ pytheory demo
|
||||
```
|
||||
|
||||
## Music Theory
|
||||
|
||||
```pycon
|
||||
>>> from pytheory import Key, Chord, Tone
|
||||
|
||||
>>> Key("C", "major").chords
|
||||
['C major', 'D minor', 'E minor', 'F major', 'G major', 'A minor', 'B diminished']
|
||||
|
||||
>>> [c.symbol for c in Key("G", "major").progression("I", "V", "vi", "IV")]
|
||||
['G', 'D', 'Em', 'C']
|
||||
|
||||
>>> Chord.from_symbol("F#m7b5").identify()
|
||||
'F# half-diminished 7th'
|
||||
|
||||
>>> Tone.from_string("C4").interval_to(Tone.from_string("G4"))
|
||||
'perfect 5th'
|
||||
|
||||
>>> Key("C", "major").pivot_chords(Key("G", "major"))
|
||||
['A minor', 'B minor', 'C major', 'D major', 'E minor', 'G major']
|
||||
|
||||
>>> Chord.from_tones("C", "E", "G").forte_number
|
||||
'3-11'
|
||||
|
||||
>>> from pytheory.scales import Scale
|
||||
>>> Scale.recommend("C", "Eb", "F", "Gb", "G", "Bb", top=3)
|
||||
[('C', 'blues', 1.0), ...]
|
||||
```
|
||||
|
||||
## Composition
|
||||
|
||||
```python
|
||||
score = Score("4/4", bpm=124)
|
||||
score.drums("house", repeats=16, fill="house", fill_every=8)
|
||||
|
||||
pad = score.part("pad", synth="supersaw", envelope="pad",
|
||||
reverb=0.5, chorus=0.3, sidechain=0.85)
|
||||
lead = score.part("lead", synth="saw", envelope="pluck",
|
||||
legato=True, glide=0.03, humanize=0.3)
|
||||
bass = score.part("bass", synth="sine", lowpass=300, sidechain=0.7)
|
||||
|
||||
# Song structure
|
||||
score.section("verse")
|
||||
# ... add notes ...
|
||||
score.section("chorus")
|
||||
lead.set(lowpass=5000, reverb=0.3)
|
||||
# ... add notes ...
|
||||
score.end_section()
|
||||
|
||||
score.repeat("verse")
|
||||
score.repeat("chorus", times=2)
|
||||
```
|
||||
|
||||
## 10 Synth Waveforms
|
||||
|
||||
sine, saw, triangle, square, pulse, FM, noise, supersaw, PWM slow, PWM fast — with detune, stereo pan, and spread.
|
||||
|
||||
## 58 Drum Patterns
|
||||
|
||||
rock, jazz, bebop, bossa nova, salsa, samba, afrobeat, funk, reggae, house, trap, metal, drum and bass — and 45 more. Plus 21 fill presets. Stereo panned like a real kit.
|
||||
|
||||
## 6 Effects with Automation
|
||||
|
||||
```python
|
||||
lead = score.part("lead", synth="saw",
|
||||
distortion=0.7, lowpass=1000, lowpass_q=5.0,
|
||||
delay=0.3, reverb=0.4, reverb_type="plate",
|
||||
chorus=0.3)
|
||||
|
||||
# Automate mid-song
|
||||
lead.set(lowpass=4000, distortion=0.9)
|
||||
|
||||
# LFO modulation
|
||||
lead.lfo("lowpass", rate=0.5, min=400, max=3000, bars=8)
|
||||
```
|
||||
|
||||
Signal chain: distortion → chorus → lowpass → delay → reverb. Sidechain compression. Master bus compressor/limiter. Stereo output.
|
||||
|
||||
## Convolution Reverb
|
||||
|
||||
7 synthetic impulse responses: Taj Mahal (12s), cathedral, plate, spring, cave, parking garage, canyon.
|
||||
|
||||
```python
|
||||
pad = score.part("pad", synth="supersaw",
|
||||
reverb=0.85, reverb_type="taj_mahal")
|
||||
```
|
||||
|
||||
## 6 Musical Systems
|
||||
|
||||
Western, Indian (Hindustani), Arabic (Maqam), Japanese, Blues/Pentatonic, Javanese Gamelan — 40+ scales.
|
||||
|
||||
## 25 Instrument Presets
|
||||
|
||||
Guitar (8 tunings), bass, ukulele, mandolin family, violin family, banjo, harp, oud, sitar, erhu, and more — with chord fingering generation.
|
||||
|
||||
## Command Line
|
||||
|
||||
```
|
||||
$ pytheory repl # interactive scratchpad
|
||||
$ pytheory demo # hear a generated track
|
||||
$ pytheory key G major # explore a key
|
||||
$ pytheory identify Cmaj7 # analyze a chord symbol
|
||||
$ pytheory progression C major I V vi IV # build a progression
|
||||
$ pytheory midi C major I V vi IV -o out.mid
|
||||
$ pytheory play Am7 --synth saw --envelope pluck
|
||||
$ pytheory modes C # show all modes
|
||||
$ pytheory circle C # circle of fifths
|
||||
```
|
||||
|
||||
## Why Python?
|
||||
|
||||
A DAW is great for tweaking sounds. But when you're *thinking about music* — code is faster than clicking. Sketch ideas, hear them instantly, export MIDI, finish in your DAW.
|
||||
|
||||
Tools like [Claude Code](https://claude.ai/code) can use PyTheory to prototype musical ideas from natural language — "write a bossa nova in A minor with a saw lead and reverb" becomes real, playable music.
|
||||
|
||||
## Documentation
|
||||
|
||||
**[pytheory.kennethreitz.org](https://pytheory.kennethreitz.org)**
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
pytheory.kennethreitz.org
|
||||
Vendored
BIN
Binary file not shown.
|
After Width: | Height: | Size: 78 KiB |
Vendored
+18
@@ -0,0 +1,18 @@
|
||||
{% extends "!layout.html" %}
|
||||
{% block footer %}
|
||||
{{ super() }}
|
||||
<script type="text/javascript">
|
||||
var _gauges = _gauges || [];
|
||||
(function() {
|
||||
var t = document.createElement('script');
|
||||
t.type = 'text/javascript';
|
||||
t.async = true;
|
||||
t.id = 'gauges-tracker';
|
||||
t.setAttribute('data-site-id', '69bfc431e7e47c1200fc74bc');
|
||||
t.setAttribute('data-track-path', 'https://track.gaug.es/track.gif');
|
||||
t.src = 'https://d2fuc4clr7gvcn.cloudfront.net/track.js';
|
||||
var s = document.getElementsByTagName('script')[0];
|
||||
s.parentNode.insertBefore(t, s);
|
||||
})();
|
||||
</script>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,2 @@
|
||||
```{include} ../CHANGELOG.md
|
||||
```
|
||||
+23
-3
@@ -1,18 +1,25 @@
|
||||
import os
|
||||
import sys
|
||||
from unittest.mock import MagicMock
|
||||
|
||||
sys.path.insert(0, os.path.abspath(".."))
|
||||
|
||||
# Mock sounddevice so Sphinx can import pytheory.play without PortAudio
|
||||
sys.modules["sounddevice"] = MagicMock()
|
||||
|
||||
project = "PyTheory"
|
||||
copyright = "2024, Kenneth Reitz"
|
||||
copyright = "2026, Kenneth Reitz"
|
||||
author = "Kenneth Reitz"
|
||||
release = "0.2.0"
|
||||
import pytheory
|
||||
release = pytheory.__version__
|
||||
version = pytheory.__version__
|
||||
|
||||
extensions = [
|
||||
"sphinx.ext.autodoc",
|
||||
"sphinx.ext.napoleon",
|
||||
"sphinx.ext.viewcode",
|
||||
"sphinx.ext.intersphinx",
|
||||
"myst_parser",
|
||||
]
|
||||
|
||||
autodoc_member_order = "bysource"
|
||||
@@ -30,5 +37,18 @@ templates_path = ["_templates"]
|
||||
exclude_patterns = ["_build"]
|
||||
|
||||
html_theme = "alabaster"
|
||||
html_title = "PyTheory"
|
||||
html_theme_options = {
|
||||
"github_user": "kennethreitz",
|
||||
"github_repo": "pytheory",
|
||||
"github_banner": True,
|
||||
"github_button": True,
|
||||
"github_type": "star",
|
||||
"github_count": True,
|
||||
"description": "Music Theory for Humans",
|
||||
"extra_nav_links": {
|
||||
f"v{pytheory.__version__}": "https://pypi.org/project/pytheory/",
|
||||
},
|
||||
"show_powered_by": False,
|
||||
}
|
||||
html_static_path = ["_static"]
|
||||
html_extra_path = ["CNAME"]
|
||||
|
||||
+574
-47
@@ -1,83 +1,610 @@
|
||||
Working with Chords
|
||||
===================
|
||||
|
||||
Chords and Chord Charts
|
||||
-----------------------
|
||||
A `chord <https://en.wikipedia.org/wiki/Chord_(music)>`_ is two or more tones sounding simultaneously. Chords are the
|
||||
vertical dimension of music — while melody moves horizontally through
|
||||
time, harmony stacks tones on top of each other.
|
||||
|
||||
PyTheory provides two chord-related classes:
|
||||
Chord Construction
|
||||
------------------
|
||||
|
||||
- :class:`~pytheory.chords.Chord` — a collection of tones played together
|
||||
- :class:`~pytheory.charts.NamedChord` — a chord from the chart database with
|
||||
fingering support
|
||||
Chords are built by stacking **intervals** above a **root** note. The
|
||||
most common chord type is the `triad <https://en.wikipedia.org/wiki/Triad_(music)>`_ — three notes built from
|
||||
alternating scale degrees (root, 3rd, 5th).
|
||||
|
||||
The four triad types::
|
||||
|
||||
Major root + major 3rd (4) + perfect 5th (7) Bright, stable
|
||||
Minor root + minor 3rd (3) + perfect 5th (7) Dark, sad
|
||||
Diminished root + minor 3rd (3) + diminished 5th (6) Tense, unstable
|
||||
Augmented root + major 3rd (4) + augmented 5th (8) Eerie, unresolved
|
||||
|
||||
Adding a 7th creates a `seventh chord <https://en.wikipedia.org/wiki/Seventh_chord>`_ — the foundation of jazz
|
||||
harmony::
|
||||
|
||||
Dominant 7th root + 4 + 7 + 10 Bluesy, wants to resolve (G7)
|
||||
Major 7th root + 4 + 7 + 11 Dreamy, sophisticated (Cmaj7)
|
||||
Minor 7th root + 3 + 7 + 10 Warm, mellow (Am7)
|
||||
Diminished 7th root + 3 + 6 + 9 Dramatic, symmetrical
|
||||
|
||||
Inversions
|
||||
----------
|
||||
|
||||
A chord is in **root position** when the root is the lowest note.
|
||||
When a different chord tone is in the bass, the chord is `inverted <https://en.wikipedia.org/wiki/Inversion_(music)>`_:
|
||||
|
||||
- **Root position**: C E G (root in bass)
|
||||
- **First inversion**: E G C (3rd in bass) — notated C/E
|
||||
- **Second inversion**: G C E (5th in bass) — notated C/G
|
||||
|
||||
Inversions change the color and weight of a chord without changing its
|
||||
identity. First inversion sounds lighter; second inversion sounds
|
||||
suspended, often used as a passing chord.
|
||||
|
||||
For seventh chords, there's also **third inversion** (7th in bass):
|
||||
|
||||
- G7 in third inversion: F G B D (notated G7/F)
|
||||
|
||||
.. code-block:: pycon
|
||||
|
||||
>>> from pytheory import Chord, Tone
|
||||
|
||||
>>> root = Chord([Tone.from_string(n, system="western") for n in ["C4", "E4", "G4"]])
|
||||
>>> first = Chord([Tone.from_string(n, system="western") for n in ["E3", "G3", "C4"]])
|
||||
>>> second = Chord([Tone.from_string(n, system="western") for n in ["G3", "C4", "E4"]])
|
||||
|
||||
>>> root.identify()
|
||||
'C major'
|
||||
>>> first.identify()
|
||||
'C major'
|
||||
>>> second.identify()
|
||||
'C major'
|
||||
|
||||
Extended Chords
|
||||
---------------
|
||||
|
||||
Beyond seventh chords, jazz harmony builds `extended chords <https://en.wikipedia.org/wiki/Extended_chord>`_ by
|
||||
continuing to stack thirds:
|
||||
|
||||
- **9th chord**: adds the 9th (= 2nd, one octave up)
|
||||
- **11th chord**: adds the 9th and 11th (= 4th)
|
||||
- **13th chord**: adds the 9th, 11th, and 13th (= 6th)
|
||||
|
||||
A full 13th chord contains all 7 notes of the scale! In practice,
|
||||
tones are usually omitted — the 5th is typically dropped first, then
|
||||
the 11th (which clashes with the 3rd in dominant chords).
|
||||
|
||||
.. code-block:: pycon
|
||||
|
||||
>>> from pytheory import TonedScale
|
||||
|
||||
>>> scale = TonedScale(tonic="C4")["major"]
|
||||
|
||||
>>> cmaj9 = scale.chord(0, 2, 4, 6, 8)
|
||||
>>> c13 = scale.chord(0, 2, 4, 6, 8, 10, 12)
|
||||
|
||||
Using the Chord Chart
|
||||
---------------------
|
||||
|
||||
The built-in chart contains 144 chords (12 roots x 12 qualities):
|
||||
PyTheory includes 144 pre-built chords (12 roots x 12 qualities):
|
||||
|
||||
.. code-block:: python
|
||||
.. code-block:: pycon
|
||||
|
||||
from pytheory import CHARTS
|
||||
>>> from pytheory import Fretboard
|
||||
|
||||
chart = CHARTS["western"]
|
||||
>>> fb = Fretboard.guitar()
|
||||
>>> fb.chord("C")
|
||||
Fingering(e=0, B=1, G=0, D=2, A=3, E=x)
|
||||
>>> fb.chord("Am")
|
||||
Fingering(e=0, B=1, G=2, D=2, A=0, E=x)
|
||||
>>> fb.chord("G7")
|
||||
Fingering(e=1, B=0, G=0, D=0, A=2, E=3)
|
||||
|
||||
# Access a chord
|
||||
c_major = chart["C"]
|
||||
a_minor = chart["Am"]
|
||||
g_seven = chart["G7"]
|
||||
You can also build chords directly with ``Chord.from_name()``:
|
||||
|
||||
# Available qualities: "", "maj", "m", "5", "7", "9",
|
||||
# "dim", "m6", "m7", "m9", "maj7", "maj9"
|
||||
.. code-block:: pycon
|
||||
|
||||
Chord Tones
|
||||
-----------
|
||||
>>> from pytheory import Chord
|
||||
|
||||
Each named chord knows which tones it contains:
|
||||
>>> Chord.from_name("G7").identify()
|
||||
'G dominant 7th'
|
||||
>>> Chord.from_name("Ddim").identify()
|
||||
'D diminished'
|
||||
|
||||
.. code-block:: python
|
||||
Available qualities:
|
||||
|
||||
============ ================ ================================
|
||||
Quality Intervals Example tones (from C)
|
||||
============ ================ ================================
|
||||
``""`` 4, 7 C E G (major triad)
|
||||
``"maj"`` 4, 7 C E G (explicit major)
|
||||
``"m"`` 3, 7 C Eb G (minor triad)
|
||||
``"5"`` 7 C G (power chord)
|
||||
``"7"`` 4, 7, 10 C E G Bb (dominant 7th)
|
||||
``"9"`` 4, 7, 10, 14 C E G Bb D (dominant 9th)
|
||||
``"dim"`` 3, 6 C Eb Gb (diminished)
|
||||
``"m6"`` 3, 7, 9 C Eb G A (minor 6th)
|
||||
``"m7"`` 3, 7, 10 C Eb G Bb (minor 7th)
|
||||
``"m9"`` 3, 7, 10, 14 C Eb G Bb D (minor 9th)
|
||||
``"maj7"`` 4, 7, 11 C E G B (major 7th)
|
||||
``"maj9"`` 4, 7, 11, 14 C E G B D (major 9th)
|
||||
============ ================ ================================
|
||||
|
||||
.. code-block:: pycon
|
||||
|
||||
>>> from pytheory import CHARTS
|
||||
>>> chart = CHARTS["western"]
|
||||
|
||||
>>> chart["C"].acceptable_tone_names
|
||||
('C', 'E', 'G')
|
||||
|
||||
>>> chart["Am"].acceptable_tone_names
|
||||
('A', 'C', 'E')
|
||||
>>> chart["Cm7"].acceptable_tone_names
|
||||
('C', 'Eb', 'G', 'Bb')
|
||||
|
||||
>>> chart["G7"].acceptable_tone_names
|
||||
('G', 'B', 'D', 'F')
|
||||
Building Chords
|
||||
---------------
|
||||
|
||||
Building Chords Manually
|
||||
Several convenience constructors make chord creation concise:
|
||||
|
||||
.. code-block:: pycon
|
||||
|
||||
>>> from pytheory import Chord
|
||||
|
||||
>>> Chord.from_tones("C", "E", "G").identify()
|
||||
'C major'
|
||||
>>> Chord.from_tones("A", "C", "E").identify()
|
||||
'A minor'
|
||||
|
||||
>>> Chord.from_name("Am7").identify()
|
||||
'A minor 7th'
|
||||
>>> Chord.from_name("G7").identify()
|
||||
'G dominant 7th'
|
||||
|
||||
>>> Chord.from_intervals("C", 4, 7).identify()
|
||||
'C major'
|
||||
>>> Chord.from_intervals("G", 4, 7, 10).identify()
|
||||
'G dominant 7th'
|
||||
|
||||
>>> Chord.from_midi_message(60, 64, 67).identify()
|
||||
'C major'
|
||||
|
||||
>>> len(Chord.from_name("C"))
|
||||
3
|
||||
>>> "C" in Chord.from_name("C")
|
||||
True
|
||||
|
||||
Intervals
|
||||
---------
|
||||
|
||||
The ``intervals`` property returns semitone distances between adjacent
|
||||
tones — these are musically meaningful and octave-invariant:
|
||||
|
||||
.. code-block:: pycon
|
||||
|
||||
>>> Chord.from_tones("C", "E", "G").intervals
|
||||
[4, 3]
|
||||
|
||||
>>> Chord.from_tones("C", "Eb", "G").intervals
|
||||
[3, 4]
|
||||
|
||||
Consonance and Dissonance
|
||||
-------------------------
|
||||
|
||||
.. code-block:: python
|
||||
**Consonance** is the perception of stability and "pleasantness" when
|
||||
tones sound together. **Dissonance** is the perception of tension and
|
||||
roughness. Neither is inherently good or bad — music needs both.
|
||||
|
||||
from pytheory import Tone, Chord
|
||||
Harmony Score
|
||||
~~~~~~~~~~~~~
|
||||
|
||||
c_major = Chord(tones=[
|
||||
Tone.from_string("C4", system="western"),
|
||||
Tone.from_string("E4", system="western"),
|
||||
Tone.from_string("G4", system="western"),
|
||||
])
|
||||
The ``harmony`` property measures consonance using **frequency ratio
|
||||
simplicity**. The insight dates back to Pythagoras (6th century BC):
|
||||
intervals whose frequencies form simple integer ratios sound consonant.
|
||||
|
||||
# Iteration
|
||||
for tone in c_major:
|
||||
print(tone)
|
||||
=========== ===== ====================
|
||||
Interval Ratio Why it sounds "good"
|
||||
=========== ===== ====================
|
||||
Octave 2:1 Every 2nd wave aligns
|
||||
Perfect 5th 3:2 Every 3rd wave aligns
|
||||
Perfect 4th 4:3 Every 4th wave aligns
|
||||
Major 3rd 5:4 Every 5th wave aligns
|
||||
Minor 3rd 6:5 Every 6th wave aligns
|
||||
Tritone 45:32 Waves rarely align
|
||||
=========== ===== ====================
|
||||
|
||||
len(c_major) # 3
|
||||
"C" in c_major # True
|
||||
.. code-block:: pycon
|
||||
|
||||
Chord Properties
|
||||
>>> from pytheory import Chord, Tone
|
||||
>>> C4 = Tone.from_string("C4", system="western")
|
||||
>>> G4 = Tone.from_string("G4", system="western")
|
||||
|
||||
>>> fifth = Chord([C4, G4])
|
||||
>>> tritone = Chord([C4, C4 + 6])
|
||||
>>> fifth.harmony > tritone.harmony
|
||||
True
|
||||
|
||||
Dissonance Score
|
||||
~~~~~~~~~~~~~~~~
|
||||
|
||||
The ``dissonance`` property uses the Plomp-Levelt `roughness <https://en.wikipedia.org/wiki/Roughness_(psychoacoustics)>`_ model
|
||||
(1965). When two frequencies are close together, their sound waves
|
||||
interfere and produce rapid amplitude fluctuations called `beating <https://en.wikipedia.org/wiki/Beat_(acoustics)>`_.
|
||||
This beating is perceived as roughness — the physiological basis of
|
||||
dissonance.
|
||||
|
||||
The roughness depends on the frequency difference relative to the
|
||||
**critical bandwidth** of the human ear (~25% of the frequency at
|
||||
that register). Maximum roughness occurs when the difference equals
|
||||
the critical bandwidth.
|
||||
|
||||
.. code-block:: pycon
|
||||
|
||||
>>> E4 = Tone.from_string("E4", system="western")
|
||||
>>> octave = Chord([C4, C4 + 12])
|
||||
>>> third = Chord([C4, E4])
|
||||
>>> octave.dissonance < third.dissonance
|
||||
True
|
||||
|
||||
Beat Frequencies
|
||||
~~~~~~~~~~~~~~~~
|
||||
|
||||
When two tones with slightly different frequencies are played together,
|
||||
you hear a pulsing at the **beat frequency**: ``|f1 - f2|`` Hz.
|
||||
|
||||
- **< 1 Hz**: Slow pulsing, used for tuning instruments
|
||||
- **1–15 Hz**: Audible rhythmic beating
|
||||
- **15–30 Hz**: Perceived as buzzing/roughness
|
||||
- **> 30 Hz**: No longer beating — becomes part of the timbre
|
||||
|
||||
.. code-block:: pycon
|
||||
|
||||
>>> A4 = Tone.from_string("A4", system="western")
|
||||
>>> chord = Chord([A4, A4 + 7, A4 + 12])
|
||||
|
||||
>>> chord.beat_frequencies
|
||||
[...]
|
||||
|
||||
>>> round(chord.beat_pulse, 1)
|
||||
219.3
|
||||
|
||||
Transposition
|
||||
-------------
|
||||
|
||||
Shift an entire chord up or down by any number of semitones:
|
||||
|
||||
.. code-block:: pycon
|
||||
|
||||
>>> Chord.from_name("C").transpose(7).identify()
|
||||
'G major'
|
||||
|
||||
>>> Chord.from_name("Am7").transpose(-2).identify()
|
||||
'G minor 7th'
|
||||
|
||||
Chord Manipulation
|
||||
------------------
|
||||
|
||||
Add or remove individual tones from a chord:
|
||||
|
||||
.. code-block:: pycon
|
||||
|
||||
>>> from pytheory import Chord, Tone
|
||||
|
||||
>>> c_major = Chord.from_tones("C", "E", "G")
|
||||
|
||||
>>> b4 = Tone.from_string("B4", system="western")
|
||||
>>> cmaj7 = c_major.add_tone(b4)
|
||||
>>> cmaj7.identify()
|
||||
'C major 7th'
|
||||
|
||||
>>> c_again = cmaj7.remove_tone("B")
|
||||
>>> c_again.identify()
|
||||
'C major'
|
||||
|
||||
Chord Identification
|
||||
--------------------
|
||||
|
||||
Give PyTheory any set of tones and it will tell you what chord it is.
|
||||
It tries every tone as a potential root and matches the interval pattern
|
||||
against 17 known chord types (triads, 7ths, 9ths, sus, power chords).
|
||||
|
||||
.. code-block:: pycon
|
||||
|
||||
>>> from pytheory import Chord
|
||||
|
||||
>>> Chord.from_tones("A", "C", "E").identify()
|
||||
'A minor'
|
||||
>>> Chord.from_tones("G", "B", "D", "F").identify()
|
||||
'G dominant 7th'
|
||||
|
||||
>>> Chord.from_tones("E", "G", "C").identify()
|
||||
'C major'
|
||||
|
||||
>>> 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
|
||||
|
||||
>>> chord = Chord.from_name("Am7")
|
||||
>>> chord.root
|
||||
<Tone A4>
|
||||
>>> chord.quality
|
||||
'minor 7th'
|
||||
|
||||
Harmonic Analysis
|
||||
-----------------
|
||||
|
||||
`Roman numeral analysis <https://en.wikipedia.org/wiki/Roman_numeral_analysis>`_ labels each chord by its function within a
|
||||
key. This is how musicians describe chord progressions independent of
|
||||
key — "I-IV-V" means the same thing in C major (C-F-G) as in G major
|
||||
(G-C-D).
|
||||
|
||||
.. code-block:: pycon
|
||||
|
||||
>>> from pytheory import Chord, Tone
|
||||
|
||||
>>> C4 = Tone.from_string("C4", system="western")
|
||||
>>> E4 = Tone.from_string("E4", system="western")
|
||||
>>> G4 = Tone.from_string("G4", system="western")
|
||||
|
||||
>>> Chord([C4, E4, G4]).analyze("C")
|
||||
'I'
|
||||
>>> Chord.from_tones("D", "F", "A").analyze("C")
|
||||
'ii'
|
||||
>>> Chord([G4, G4+4, G4+7]).analyze("C")
|
||||
'V'
|
||||
>>> Chord([G4, G4+4, G4+7, G4+10]).analyze("C")
|
||||
'V7'
|
||||
|
||||
Tension and Resolution
|
||||
----------------------
|
||||
|
||||
**Tension** is what makes music move forward. Without it, there's no
|
||||
desire to resolve — no drama, no narrative. The ``tension`` property
|
||||
quantifies this based on:
|
||||
|
||||
- **Tritones** (6 semitones): the most unstable interval. The tritone
|
||||
between the 3rd and 7th of a dominant chord (e.g. B and F in G7)
|
||||
creates the strongest pull toward resolution.
|
||||
- **Minor 2nds**: semitone clashes that add bite and urgency.
|
||||
- **Dominant function**: the specific combination of a major 3rd and
|
||||
minor 7th above the root — the hallmark of the V7 chord.
|
||||
|
||||
.. code-block:: pycon
|
||||
|
||||
>>> c_major = Chord([C4, E4, G4])
|
||||
>>> c_major.tension['score']
|
||||
0.0
|
||||
>>> c_major.tension['tritones']
|
||||
0
|
||||
|
||||
>>> g7 = Chord([G4, G4+4, G4+7, G4+10])
|
||||
>>> g7.tension['score']
|
||||
0.6
|
||||
>>> g7.tension['tritones']
|
||||
1
|
||||
>>> g7.tension['has_dominant_function']
|
||||
True
|
||||
|
||||
Voice Leading
|
||||
-------------
|
||||
|
||||
`Voice leading <https://en.wikipedia.org/wiki/Voice_leading>`_ is the art of connecting chords smoothly. Instead of
|
||||
jumping all voices to new positions, good voice leading moves each note
|
||||
the minimum distance to reach the next chord. Bach's chorales are the
|
||||
gold standard — every voice moves by step whenever possible.
|
||||
|
||||
.. code-block:: pycon
|
||||
|
||||
>>> c_maj = Chord.from_tones("C", "E", "G")
|
||||
>>> f_maj = Chord.from_tones("F", "A", "C")
|
||||
|
||||
>>> for src, dst, motion in c_maj.voice_leading(f_maj):
|
||||
... print(f"{src} -> {dst} ({motion:+d} semitones)")
|
||||
G4 -> A4 (+2 semitones)
|
||||
E4 -> F4 (+1 semitones)
|
||||
C4 -> C4 (+0 semitones)
|
||||
|
||||
Tritone Substitution
|
||||
--------------------
|
||||
|
||||
In jazz harmony, any `dominant chord <https://en.wikipedia.org/wiki/Dominant_seventh_chord>`_
|
||||
can be replaced by the dominant chord a
|
||||
`tritone <https://en.wikipedia.org/wiki/Tritone_substitution>`_ (6
|
||||
semitones) away. This works because the two chords share the same
|
||||
tritone interval — the 3rd and 7th simply swap roles.
|
||||
|
||||
Common tritone subs: G7 <-> Db7, C7 <-> F#7, D7 <-> Ab7.
|
||||
|
||||
.. code-block:: pycon
|
||||
|
||||
>>> from pytheory import Chord
|
||||
|
||||
>>> g7 = Chord.from_name("G7")
|
||||
>>> sub = g7.tritone_sub()
|
||||
>>> sub.identify()
|
||||
'C# dominant 7th'
|
||||
|
||||
The Overtone Series
|
||||
-------------------
|
||||
|
||||
Every musical tone is actually a stack of frequencies — the
|
||||
**fundamental** plus its `overtones <https://en.wikipedia.org/wiki/Overtone>`_ (harmonics). The overtone series
|
||||
is nature's chord: it contains the octave, perfect fifth, perfect
|
||||
fourth, major third, and more, in that order.
|
||||
|
||||
This is *why* consonance exists. When you play C and G together, the
|
||||
overtones of C already contain G. The two tones share acoustic energy,
|
||||
reinforcing each other. A dissonant interval like C and C# shares
|
||||
almost no overtones — the waves clash.
|
||||
|
||||
.. code-block:: pycon
|
||||
|
||||
>>> from pytheory import Tone
|
||||
|
||||
>>> a4 = Tone.from_string("A4", system="western")
|
||||
>>> [round(f, 1) for f in a4.overtones(8)]
|
||||
[440.0, 880.0, 1320.0, 1760.0, 2200.0, 2640.0, 3080.0, 3520.0]
|
||||
|
||||
Chord Symbols
|
||||
-------------
|
||||
|
||||
The ``symbol`` property returns compact lead-sheet notation, while
|
||||
``from_symbol()`` parses any standard chord symbol — no lookup table needed:
|
||||
|
||||
.. code-block:: pycon
|
||||
|
||||
>>> Chord.from_tones("C", "E", "G").symbol
|
||||
'C'
|
||||
>>> Chord.from_name("Am7").symbol
|
||||
'Am7'
|
||||
>>> Chord.from_symbol("F#m7b5").identify()
|
||||
'F# half-diminished 7th'
|
||||
>>> Chord.from_symbol("Bbmaj9").symbol
|
||||
'Bbmaj9'
|
||||
|
||||
Slash Chords
|
||||
------------
|
||||
|
||||
`Slash chords <https://en.wikipedia.org/wiki/Slash_chord>`_ place a specific
|
||||
note in the bass below the chord. They're written as Chord/Bass in lead sheets:
|
||||
|
||||
.. code-block:: pycon
|
||||
|
||||
>>> c = Chord.from_symbol("C")
|
||||
>>> c_over_g = c.slash("G")
|
||||
>>> c_over_g.slash_name
|
||||
'C/G'
|
||||
>>> c.slash("E").slash_name
|
||||
'C/E'
|
||||
|
||||
Drop Voicings
|
||||
-------------
|
||||
|
||||
`Drop voicings <https://en.wikipedia.org/wiki/Voicing_(music)#Drop_voicings>`_
|
||||
are standard arranging techniques for spreading chord tones across registers:
|
||||
|
||||
- **Close voicing** — all tones packed within one octave
|
||||
- **Open voicing** — alternating tones raised an octave for wider spacing
|
||||
- **Drop 2** — second-highest voice dropped an octave (standard jazz guitar)
|
||||
- **Drop 3** — third-highest voice dropped an octave
|
||||
|
||||
.. code-block:: pycon
|
||||
|
||||
>>> cmaj7 = Chord.from_symbol("Cmaj7")
|
||||
>>> cmaj7.close_voicing()
|
||||
<Chord C major 7th>
|
||||
>>> cmaj7.drop2()
|
||||
<Chord C major 7th>
|
||||
|
||||
Chord Extensions
|
||||
----------------
|
||||
|
||||
.. code-block:: python
|
||||
The ``extensions()`` method suggests available extensions (9th, 11th, 13th)
|
||||
that don't clash with existing chord tones:
|
||||
|
||||
# Frequency intervals between adjacent tones (Hz)
|
||||
c_major.intervals
|
||||
.. code-block:: pycon
|
||||
|
||||
# Harmony score (higher = more consonant intervals)
|
||||
c_major.harmony
|
||||
>>> from pytheory import Chord, TonedScale
|
||||
>>> cm = Chord.from_symbol("C")
|
||||
>>> cm.extensions()
|
||||
[...]
|
||||
|
||||
# Dissonance score (higher = wider intervals)
|
||||
c_major.dissonance
|
||||
>>> # Filter extensions against a scale for diatonic correctness:
|
||||
>>> scale = TonedScale(tonic="C4")["major"]
|
||||
>>> cm.extensions(scale=scale)
|
||||
[...]
|
||||
|
||||
# Beat frequency between closest tone pair
|
||||
c_major.beat_pulse
|
||||
Borrowed Chord Analysis
|
||||
-----------------------
|
||||
|
||||
``analyze()`` now recognizes chromatic chords from modal interchange,
|
||||
labeling them with flat-degree prefixes:
|
||||
|
||||
.. code-block:: pycon
|
||||
|
||||
>>> Chord.from_symbol("Ab").analyze("C", "major")
|
||||
'bVI'
|
||||
>>> Chord.from_symbol("Bb").analyze("C", "major")
|
||||
'bVII'
|
||||
|
||||
Figured Bass
|
||||
------------
|
||||
|
||||
`Figured bass <https://en.wikipedia.org/wiki/Figured_bass>`_ is the
|
||||
classical notation for chord inversions — numbers below the bass note
|
||||
describing the intervals above it. It's how Bach, Handel, and every
|
||||
Baroque composer communicated harmony.
|
||||
|
||||
.. code-block:: pycon
|
||||
|
||||
>>> from pytheory import Chord, Tone
|
||||
|
||||
>>> root = Chord([Tone.from_string("C4"), Tone.from_string("E4"), Tone.from_string("G4")])
|
||||
>>> root.figured_bass
|
||||
''
|
||||
|
||||
>>> first_inv = Chord([Tone.from_string("E3"), Tone.from_string("G3"), Tone.from_string("C4")])
|
||||
>>> first_inv.figured_bass
|
||||
'6'
|
||||
|
||||
>>> second_inv = Chord([Tone.from_string("G3"), Tone.from_string("C4"), Tone.from_string("E4")])
|
||||
>>> second_inv.figured_bass
|
||||
'6/4'
|
||||
|
||||
For seventh chords: root position → ``"7"``, first inversion → ``"6/5"``,
|
||||
second inversion → ``"4/3"``, third inversion → ``"2"``.
|
||||
|
||||
Combine with Roman numeral analysis using ``analyze_figured()``:
|
||||
|
||||
.. code-block:: pycon
|
||||
|
||||
>>> first_inv.analyze_figured("C")
|
||||
'I6'
|
||||
|
||||
Pitch Class Sets
|
||||
----------------
|
||||
|
||||
`Pitch class set theory <https://en.wikipedia.org/wiki/Set_theory_(music)>`_
|
||||
is the framework for analyzing atonal and post-tonal music. It reduces
|
||||
any collection of notes to abstract pitch classes (0–11, where C=0),
|
||||
finds the most compact form, and catalogs it with a Forte number.
|
||||
|
||||
If you're studying Schoenberg, Webern, Bartók, or any 20th-century
|
||||
music that doesn't follow traditional harmony, this is the tool.
|
||||
|
||||
.. code-block:: pycon
|
||||
|
||||
>>> Chord.from_tones("C", "E", "G").pitch_classes
|
||||
{0, 4, 7}
|
||||
|
||||
>>> Chord.from_tones("C", "E", "G").prime_form
|
||||
(0, 3, 7)
|
||||
|
||||
>>> Chord.from_tones("A", "C", "E").prime_form
|
||||
(0, 3, 7)
|
||||
|
||||
Major and minor triads share the same prime form — they're inversions
|
||||
of each other in pitch class space.
|
||||
|
||||
.. code-block:: pycon
|
||||
|
||||
>>> Chord.from_tones("C", "E", "G").forte_number
|
||||
'3-11'
|
||||
|
||||
>>> Chord.from_tones("C", "E", "G", "B").forte_number
|
||||
'4-20'
|
||||
|
||||
>>> Chord.from_tones("C", "E", "G#").forte_number
|
||||
'3-12'
|
||||
|
||||
Chords are the vertical dimension of music -- melody tells you where you're going, but harmony tells you how it feels to be there. Between construction, identification, voice leading, tension analysis, and pitch class sets, you've got tools to look at any chord from every angle. Pick a song you love, grab its chords, and start asking questions.
|
||||
|
||||
@@ -0,0 +1,226 @@
|
||||
Command-Line Interface
|
||||
======================
|
||||
|
||||
PyTheory includes a CLI for music theory lookups, composition, and
|
||||
playback — all from the terminal.
|
||||
|
||||
Interactive REPL
|
||||
----------------
|
||||
|
||||
For extended exploration, the REPL is a music theory scratchpad with
|
||||
tab completion. See the :doc:`repl` guide for details::
|
||||
|
||||
$ pytheory repl
|
||||
|
||||
Demo
|
||||
----
|
||||
|
||||
The fastest way to hear what PyTheory can do. Generates and plays a
|
||||
random multi-part track — different every time::
|
||||
|
||||
$ pytheory demo
|
||||
♫ Jazz Club
|
||||
Bb major | 105 bpm
|
||||
Bb → Gm → Cm → F
|
||||
jazz drums | saw lead | fm pad
|
||||
|
||||
Tone Lookup
|
||||
-----------
|
||||
|
||||
Look up any note's frequency, MIDI number, enharmonic spelling, and
|
||||
overtones::
|
||||
|
||||
$ pytheory tone A4
|
||||
Note: A4
|
||||
Frequency: 440.00 Hz (equal temperament)
|
||||
MIDI: 69
|
||||
Overtones: 440.0, 880.0, 1320.0, 1760.0, 2200.0, 2640.0
|
||||
|
||||
Compare temperaments with ``--temperament``::
|
||||
|
||||
$ pytheory tone C5 --temperament pythagorean
|
||||
Note: C5
|
||||
Frequency: 521.48 Hz (pythagorean temperament)
|
||||
Equal temp: 523.25 Hz (diff: -5.9 cents)
|
||||
|
||||
Scale Display
|
||||
-------------
|
||||
|
||||
Show any scale in any system::
|
||||
|
||||
$ pytheory scale C major
|
||||
C major: C D E F G A B C
|
||||
Intervals: C4 -2- D4 -2- E4 -1- F4 -2- G4 -2- A4 -2- B4 -1- C5
|
||||
|
||||
$ pytheory scale C dorian
|
||||
$ pytheory scale Sa bhairav --system indian
|
||||
|
||||
Chord Identification
|
||||
--------------------
|
||||
|
||||
Identify a chord from its notes::
|
||||
|
||||
$ pytheory chord C E G
|
||||
Chord: C major
|
||||
Tones: C4 E4 G4
|
||||
Intervals: [4, 3]
|
||||
Harmony: 0.5833
|
||||
Dissonance: 0.0712
|
||||
Tension: 0.00 (tritones=0)
|
||||
|
||||
$ pytheory chord G B D F
|
||||
Chord: G dominant 7th
|
||||
|
||||
Key Explorer
|
||||
------------
|
||||
|
||||
Get a complete breakdown of any key — signature, diatonic triads,
|
||||
seventh chords, relative and parallel keys::
|
||||
|
||||
$ pytheory key G major
|
||||
Key: G major
|
||||
Signature: 1 sharps, 0 flats (F#)
|
||||
Scale: G A B C D E F#
|
||||
Triads:
|
||||
I G major
|
||||
ii A minor
|
||||
iii B minor
|
||||
IV C major
|
||||
V D major
|
||||
vi E minor
|
||||
vii° F# diminished
|
||||
7th chords:
|
||||
G major 7th
|
||||
A minor 7th
|
||||
...
|
||||
Relative: <Key E minor>
|
||||
Parallel: <Key G minor>
|
||||
|
||||
Guitar Fingerings
|
||||
-----------------
|
||||
|
||||
Get tablature for any of the 144 built-in chords::
|
||||
|
||||
$ pytheory fingering Am
|
||||
Am
|
||||
E|--0--
|
||||
B|--1--
|
||||
G|--2--
|
||||
D|--2--
|
||||
A|--0--
|
||||
E|--0--
|
||||
|
||||
Use ``--capo`` to see fingerings with a capo::
|
||||
|
||||
$ pytheory fingering G --capo 2
|
||||
|
||||
Chord Progressions
|
||||
------------------
|
||||
|
||||
Build progressions from Roman numerals::
|
||||
|
||||
$ pytheory progression G major I V vi IV
|
||||
Key: G major
|
||||
Progression: I → V → vi → IV
|
||||
|
||||
I G major
|
||||
V D major
|
||||
vi E minor
|
||||
IV C major
|
||||
|
||||
Key Detection
|
||||
-------------
|
||||
|
||||
Detect the most likely key from a set of notes::
|
||||
|
||||
$ pytheory detect C E G A D
|
||||
Detected key: C major
|
||||
Scale: C D E F G A B C
|
||||
|
||||
Audio Playback
|
||||
--------------
|
||||
|
||||
Play individual notes or chords (requires PortAudio)::
|
||||
|
||||
$ pytheory play A4 # Single note
|
||||
$ pytheory play C E G # Notes as chord
|
||||
$ pytheory play Am7 # Chord by name
|
||||
$ pytheory play C E G --synth saw # Sawtooth wave
|
||||
$ pytheory play A4 --duration 2000 # 2 seconds
|
||||
$ pytheory play C E G --temperament meantone
|
||||
$ pytheory play Am7 --envelope pad # With ADSR envelope
|
||||
$ pytheory play C4 --envelope bell # Bell-like ring
|
||||
|
||||
Chord Identification (from symbol)
|
||||
-----------------------------------
|
||||
|
||||
Parse any chord symbol and get a full analysis::
|
||||
|
||||
$ pytheory identify Cmaj7
|
||||
Chord: C major 7th
|
||||
Symbol: Cmaj7
|
||||
Tones: C4 E4 G4 B4
|
||||
Intervals: [4, 3, 4]
|
||||
Harmony: 0.5833
|
||||
Dissonance: 1.2345
|
||||
Tension: score=0.00 tritones=0 minor_2nds=0 dominant=False
|
||||
|
||||
$ pytheory identify F#m7b5
|
||||
|
||||
MIDI Export
|
||||
-----------
|
||||
|
||||
Export a chord progression to a Standard MIDI File::
|
||||
|
||||
$ pytheory midi C major I V vi IV -o pop.mid
|
||||
Key: C major
|
||||
Progression: I V vi IV
|
||||
BPM: 120
|
||||
Duration: 500 ms
|
||||
Output: pop.mid
|
||||
|
||||
$ pytheory midi G major ii V I -o jazz.mid --bpm 140 --duration 800
|
||||
|
||||
Modes
|
||||
-----
|
||||
|
||||
Show all 7 modes starting from a note::
|
||||
|
||||
$ pytheory modes C
|
||||
Modes of C:
|
||||
|
||||
ionian C D E F G A B C
|
||||
dorian C D Eb F G A Bb C
|
||||
phrygian C Db Eb F G Ab Bb C
|
||||
lydian C D E F# G A B C
|
||||
mixolydian C D E F G A Bb C
|
||||
aeolian C D Eb F G Ab Bb C
|
||||
locrian C Db Eb F Gb Ab Bb C
|
||||
|
||||
Circle of Fifths
|
||||
----------------
|
||||
|
||||
Display the circle of fifths and fourths from any note::
|
||||
|
||||
$ pytheory circle C
|
||||
Circle of fifths from C:
|
||||
→ C → G → D → A → E → B → F# → C# → G# → D# → A# → F
|
||||
|
||||
Circle of fourths from C:
|
||||
→ C → F → A# → D# → G# → C# → F# → B → E → A → D → G
|
||||
|
||||
Common Progressions
|
||||
-------------------
|
||||
|
||||
Show all named progressions realized in a key::
|
||||
|
||||
$ pytheory progressions C major
|
||||
Common progressions in C major:
|
||||
|
||||
I-IV-V-I C → F → G → C
|
||||
I-V-vi-IV C → G → Am → F
|
||||
12-bar blues C → C → C → C → F → F → C → C → G → F → C → G
|
||||
ii-V-I Dm → G7 → C
|
||||
...
|
||||
|
||||
The CLI is there for quick lookups when you don't want to open a Python session -- just ask your question and get back to playing.
|
||||
@@ -0,0 +1,531 @@
|
||||
Cookbook
|
||||
=======
|
||||
|
||||
Real-world recipes for common musical tasks. Each recipe is self-contained
|
||||
and ready to paste into a Python session.
|
||||
|
||||
Analyze a Song
|
||||
--------------
|
||||
|
||||
Take the chord progression from "Let It Be" (C G Am F) and analyze it
|
||||
in the key of C major:
|
||||
|
||||
.. code-block:: pycon
|
||||
|
||||
>>> from pytheory import Chord, Key
|
||||
|
||||
>>> C = Chord.from_name("C")
|
||||
>>> G = Chord.from_name("G")
|
||||
>>> Am = Chord.from_name("Am")
|
||||
>>> F = Chord.from_name("F")
|
||||
|
||||
>>> [c.identify() for c in [C, G, Am, F]]
|
||||
['C major', 'G major', 'A minor', 'F major']
|
||||
|
||||
>>> [c.analyze("C") for c in [C, G, Am, F]]
|
||||
['I', 'V', 'vi', 'IV']
|
||||
|
||||
>>> key = Key("C", "major")
|
||||
>>> [c.identify() for c in key.progression("I", "V", "vi", "IV")]
|
||||
['C major', 'G major', 'A minor', 'F major']
|
||||
|
||||
Write a 12-Bar Blues
|
||||
--------------------
|
||||
|
||||
The `12-bar blues <https://en.wikipedia.org/wiki/Twelve-bar_blues>`_ is
|
||||
built from the I, IV, and V chords. Here it is in the key of A:
|
||||
|
||||
.. code-block:: pycon
|
||||
|
||||
>>> from pytheory import Key, Chord
|
||||
|
||||
>>> key = Key("A", "major")
|
||||
>>> [c.identify() for c in key.progression("I", "IV", "V")]
|
||||
['A major', 'D major', 'E major']
|
||||
|
||||
>>> bars = ["I","I","I","I", "IV","IV","I","I", "V","IV","I","V"]
|
||||
>>> [c.identify() for c in key.progression(*bars)]
|
||||
['A major', 'A major', 'A major', 'A major', 'D major', 'D major', 'A major', 'A major', 'E major', 'D major', 'A major', 'E major']
|
||||
|
||||
>>> Chord.from_name("A7").identify()
|
||||
'A dominant 7th'
|
||||
>>> Chord.from_name("D7").identify()
|
||||
'D dominant 7th'
|
||||
>>> Chord.from_name("E7").identify()
|
||||
'E dominant 7th'
|
||||
|
||||
Find Chords in a Key
|
||||
--------------------
|
||||
|
||||
The :class:`~pytheory.scales.Key` class builds diatonic chords for any
|
||||
key and lets you pull progressions by Roman numeral or Nashville number:
|
||||
|
||||
.. code-block:: pycon
|
||||
|
||||
>>> from pytheory import Key
|
||||
|
||||
>>> key = Key("G", "major")
|
||||
>>> key.chords
|
||||
['G major', 'A minor', 'B minor', 'C major', 'D major', 'E minor', 'F# diminished']
|
||||
|
||||
>>> [c.identify() for c in key.progression("I", "V", "vi", "IV")]
|
||||
['G major', 'D major', 'E minor', 'C major']
|
||||
|
||||
>>> [c.identify() for c in key.nashville(1, 5, 6, 4)]
|
||||
['G major', 'D major', 'E minor', 'C major']
|
||||
|
||||
Compare Scales
|
||||
--------------
|
||||
|
||||
Play the same tonic through different scales to hear how each mode
|
||||
reshapes the palette. The western modes share the same notes but start
|
||||
on different degrees; the blues scale adds the "blue note" (flat 5th):
|
||||
|
||||
.. code-block:: pycon
|
||||
|
||||
>>> from pytheory import TonedScale
|
||||
|
||||
>>> c = TonedScale(tonic="C4")
|
||||
>>> c["major"].note_names
|
||||
['C', 'D', 'E', 'F', 'G', 'A', 'B', 'C']
|
||||
>>> c["minor"].note_names
|
||||
['C', 'D', 'Eb', 'F', 'G', 'Ab', 'Bb', 'C']
|
||||
>>> c["dorian"].note_names
|
||||
['C', 'D', 'Eb', 'F', 'G', 'A', 'Bb', 'C']
|
||||
>>> c["mixolydian"].note_names
|
||||
['C', 'D', 'E', 'F', 'G', 'A', 'Bb', 'C']
|
||||
|
||||
>>> c_blues = TonedScale(tonic="C4", system="blues")
|
||||
>>> c_blues["blues"].note_names
|
||||
['C', 'Eb', 'F', 'Gb', 'G', 'Bb', 'C']
|
||||
|
||||
Guitar Chord Chart
|
||||
------------------
|
||||
|
||||
Generate fingerings for guitar and ukulele with
|
||||
:class:`~pytheory.tones.Fretboard`:
|
||||
|
||||
.. code-block:: pycon
|
||||
|
||||
>>> from pytheory import Fretboard
|
||||
|
||||
>>> fb = Fretboard.guitar()
|
||||
>>> fb.chord("C")
|
||||
Fingering(e=0, B=1, G=0, D=2, A=3, E=x)
|
||||
>>> fb.chord("G")
|
||||
Fingering(e=3, B=0, G=0, D=0, A=2, E=3)
|
||||
>>> fb.chord("Am")
|
||||
Fingering(e=0, B=1, G=2, D=2, A=0, E=x)
|
||||
>>> fb.chord("D")
|
||||
Fingering(e=2, B=3, G=2, D=0, A=x, E=x)
|
||||
|
||||
>>> uke = Fretboard.ukulele()
|
||||
>>> uke.chord("C")
|
||||
Fingering(A=3, E=0, C=0, G=0)
|
||||
>>> uke.chord("G")
|
||||
Fingering(A=2, E=3, C=2, G=0)
|
||||
|
||||
Explore an Interval
|
||||
-------------------
|
||||
|
||||
Start from A4 (440 Hz) and walk through intervals, checking names and
|
||||
frequency ratios:
|
||||
|
||||
.. code-block:: pycon
|
||||
|
||||
>>> from pytheory import Tone
|
||||
|
||||
>>> a4 = Tone.from_string("A4", system="western")
|
||||
>>> a4.frequency
|
||||
440.0
|
||||
|
||||
>>> minor_3rd = a4 + 3
|
||||
>>> a4.interval_to(minor_3rd)
|
||||
'minor 3rd'
|
||||
|
||||
>>> p5 = a4 + 7
|
||||
>>> a4.interval_to(p5)
|
||||
'perfect 5th'
|
||||
>>> round(p5.frequency / a4.frequency, 4)
|
||||
1.4983
|
||||
|
||||
>>> octave = a4 + 12
|
||||
>>> a4.interval_to(octave)
|
||||
'octave'
|
||||
>>> round(octave.frequency / a4.frequency, 4)
|
||||
2.0
|
||||
|
||||
Walk the Circle of Fifths
|
||||
-------------------------
|
||||
|
||||
The `circle of fifths <https://en.wikipedia.org/wiki/Circle_of_fifths>`_
|
||||
is the backbone of Western harmony — each step adds one sharp or flat:
|
||||
|
||||
.. code-block:: pycon
|
||||
|
||||
>>> from pytheory import Tone
|
||||
|
||||
>>> c = Tone.from_string("C4", system="western")
|
||||
>>> [t.name for t in c.circle_of_fifths()]
|
||||
['C', 'G', 'D', 'A', 'E', 'B', 'F#', 'C#', 'G#', 'D#', 'A#', 'F']
|
||||
|
||||
>>> g = Tone.from_string("G4", system="western")
|
||||
>>> [t.name for t in g.circle_of_fifths()]
|
||||
['G', 'D', 'A', 'E', 'B', 'F#', 'C#', 'G#', 'D#', 'A#', 'F', 'C']
|
||||
|
||||
Voice Leading Between Chords
|
||||
-----------------------------
|
||||
|
||||
Find the smoothest path from one chord to the next — each voice moves
|
||||
the minimum distance:
|
||||
|
||||
.. code-block:: pycon
|
||||
|
||||
>>> from pytheory import Chord
|
||||
|
||||
>>> c_maj = Chord.from_tones("C", "E", "G")
|
||||
>>> f_maj = Chord.from_tones("F", "A", "C")
|
||||
|
||||
>>> for src, dst, motion in c_maj.voice_leading(f_maj):
|
||||
... print(f"{src} -> {dst} ({motion:+d} semitones)")
|
||||
G4 -> A4 (+2 semitones)
|
||||
E4 -> F4 (+1 semitones)
|
||||
C4 -> C4 (+0 semitones)
|
||||
|
||||
Measure Harmonic Tension
|
||||
------------------------
|
||||
|
||||
Quantify how much a chord "wants to resolve." Dominant 7ths have
|
||||
the most tension — the tritone between the 3rd and 7th pulls toward
|
||||
resolution:
|
||||
|
||||
.. code-block:: pycon
|
||||
|
||||
>>> from pytheory import Chord
|
||||
|
||||
>>> for name in ["C", "Am", "G7", "Cmaj7"]:
|
||||
... ch = Chord.from_name(name)
|
||||
... t = ch.tension
|
||||
... print(f"{name:6s} tension={t['score']:.2f} tritones={t['tritones']} dominant={t['has_dominant_function']}")
|
||||
C tension=0.00 tritones=0 dominant=False
|
||||
Am tension=0.00 tritones=0 dominant=False
|
||||
G7 tension=0.60 tritones=1 dominant=True
|
||||
Cmaj7 tension=0.15 tritones=0 dominant=False
|
||||
|
||||
Tritone Substitution (Jazz)
|
||||
---------------------------
|
||||
|
||||
Replace any dominant chord with the one a
|
||||
`tritone <https://en.wikipedia.org/wiki/Tritone_substitution>`_ away —
|
||||
they share the same tritone interval:
|
||||
|
||||
.. code-block:: pycon
|
||||
|
||||
>>> from pytheory import Chord
|
||||
|
||||
>>> g7 = Chord.from_name("G7")
|
||||
>>> g7.tritone_sub().identify()
|
||||
'C# dominant 7th'
|
||||
|
||||
>>> # ii-V-I with tritone sub:
|
||||
>>> # Dm7 -> G7 -> Cmaj7 (standard)
|
||||
>>> # Dm7 -> Db7 -> Cmaj7 (chromatic bass line!)
|
||||
|
||||
Key Signatures and Detection
|
||||
-----------------------------
|
||||
|
||||
View the accidentals in any key, or detect the key from a set of notes:
|
||||
|
||||
.. code-block:: pycon
|
||||
|
||||
>>> from pytheory import Key
|
||||
|
||||
>>> Key("C", "major").signature
|
||||
{'sharps': 0, 'flats': 0, 'accidentals': []}
|
||||
>>> Key("G", "major").signature
|
||||
{'sharps': 1, 'flats': 0, 'accidentals': ['F#']}
|
||||
>>> Key("D", "major").signature
|
||||
{'sharps': 2, 'flats': 0, 'accidentals': ['F#', 'C#']}
|
||||
|
||||
>>> Key.detect("C", "E", "G", "A", "D")
|
||||
<Key C major>
|
||||
|
||||
Relative and Parallel Keys
|
||||
--------------------------
|
||||
|
||||
Every major key has a **relative minor** (same notes, different root)
|
||||
and a **parallel minor** (same root, different notes):
|
||||
|
||||
.. code-block:: pycon
|
||||
|
||||
>>> from pytheory import Key
|
||||
|
||||
>>> c = Key("C", "major")
|
||||
>>> c.relative
|
||||
'A minor'
|
||||
>>> c.parallel
|
||||
'C minor'
|
||||
|
||||
Borrowed Chords and Secondary Dominants
|
||||
---------------------------------------
|
||||
|
||||
Add color by borrowing from the parallel key or building secondary
|
||||
dominants that approach other scale degrees:
|
||||
|
||||
.. code-block:: pycon
|
||||
|
||||
>>> from pytheory import Key
|
||||
|
||||
>>> c = Key("C", "major")
|
||||
|
||||
>>> c.borrowed_chords[:4]
|
||||
['C minor', 'D diminished', 'Eb major', 'F minor']
|
||||
|
||||
>>> c.secondary_dominant(5).identify()
|
||||
'D dominant 7th'
|
||||
>>> c.secondary_dominant(2).identify()
|
||||
'A dominant 7th'
|
||||
>>> c.secondary_dominant(6).identify()
|
||||
'E dominant 7th'
|
||||
|
||||
The Overtone Series
|
||||
-------------------
|
||||
|
||||
Every musical tone contains a stack of harmonics — the physics behind
|
||||
why intervals sound consonant:
|
||||
|
||||
.. code-block:: pycon
|
||||
|
||||
>>> from pytheory import Tone
|
||||
|
||||
>>> a4 = Tone.from_string("A4", system="western")
|
||||
>>> [round(f, 1) for f in a4.overtones(6)]
|
||||
[440.0, 880.0, 1320.0, 1760.0, 2200.0, 2640.0]
|
||||
|
||||
>>> # Harmonic 2 = octave (2:1)
|
||||
>>> # Harmonic 3 = perfect 5th + octave (3:1)
|
||||
>>> # Harmonic 5 = major 3rd + two octaves (5:1)
|
||||
|
||||
Enharmonic Spellings
|
||||
--------------------
|
||||
|
||||
Find the alternate name for any sharp or flat:
|
||||
|
||||
.. code-block:: pycon
|
||||
|
||||
>>> from pytheory import Tone
|
||||
|
||||
>>> for name in ["C#4", "D#4", "F#4", "G#4"]:
|
||||
... t = Tone.from_string(name, system="western")
|
||||
... print(f"{t.name} = {t.enharmonic}")
|
||||
C# = Db
|
||||
D# = Eb
|
||||
F# = Gb
|
||||
G# = Ab
|
||||
|
||||
World Scales
|
||||
------------
|
||||
|
||||
Explore scales from Indian, Arabic, and Japanese traditions:
|
||||
|
||||
.. code-block:: pycon
|
||||
|
||||
>>> from pytheory import TonedScale
|
||||
|
||||
>>> indian = TonedScale(tonic="Sa", system="indian")
|
||||
>>> indian["bhairav"].note_names
|
||||
['Sa', 'komal Re', 'Ga', 'Ma', 'Pa', 'komal Dha', 'Ni', 'Sa']
|
||||
|
||||
>>> arabic = TonedScale(tonic="Do", system="arabic")
|
||||
>>> arabic["hijaz"].note_names
|
||||
['Do', 'Reb', 'Mi', 'Fa', 'Sol', 'Solb', 'Sib', 'Do']
|
||||
|
||||
>>> japanese = TonedScale(tonic="C4", system="japanese")
|
||||
>>> japanese["hirajoshi"].note_names
|
||||
['C', 'D', 'Eb', 'G', 'Ab', 'C']
|
||||
|
||||
Visualize a Scale on Guitar
|
||||
----------------------------
|
||||
|
||||
See where the notes fall across the fretboard — E minor pentatonic,
|
||||
the most-played scale in rock:
|
||||
|
||||
.. code-block:: pycon
|
||||
|
||||
>>> from pytheory import Fretboard, Scale
|
||||
|
||||
>>> fb = Fretboard.guitar()
|
||||
>>> pent = Scale(tonic="E4", system="blues")["minor pentatonic"]
|
||||
>>> print(fb.scale_diagram(pent, frets=12))
|
||||
0 1 2 3 4 5 6 7 8 9 10 11 12
|
||||
E| E | - | - | G | - | A | - | B | - | - | D | - | E |
|
||||
B| B | - | - | D | - | E | - | - | G | - | A | - | B |
|
||||
G| G | - | A | - | B | - | - | D | - | E | - | - | G |
|
||||
D| D | - | E | - | - | G | - | A | - | B | - | - | D |
|
||||
A| A | - | B | - | - | D | - | E | - | - | G | - | A |
|
||||
E| E | - | - | G | - | A | - | B | - | - | D | - | E |
|
||||
|
||||
Composition Recipes
|
||||
-------------------
|
||||
|
||||
These recipes go beyond theory into actual music-making.
|
||||
|
||||
Acid House Track
|
||||
~~~~~~~~~~~~~~~~
|
||||
|
||||
303-style acid with sidechain pump:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
from pytheory import Score, Pattern, Duration, Chord
|
||||
from pytheory.play import play_score
|
||||
|
||||
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)
|
||||
|
||||
play_score(score)
|
||||
|
||||
Dub Reggae with Delay Madness
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
Sparse notes into infinite echo:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
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)
|
||||
|
||||
# Play almost nothing — let the delay do the work
|
||||
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 n in ["A1"] * 16:
|
||||
bass.add(n, Duration.HALF)
|
||||
|
||||
play_score(score)
|
||||
|
||||
Jazz Ballad with Humanize
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
The difference between a robot and a musician:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
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)
|
||||
|
||||
play_score(score)
|
||||
|
||||
Song with Sections
|
||||
~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
Define once, arrange freely:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
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)
|
||||
|
||||
play_score(score)
|
||||
score.save_midi("my_song.mid")
|
||||
|
||||
Export Everything to MIDI
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
The whole point — sketch fast, finish in your DAW:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
# Any Score can be saved as MIDI
|
||||
score.save_midi("track.mid")
|
||||
|
||||
# Simple progressions too
|
||||
from pytheory import save_midi
|
||||
chords = Key("C", "major").progression("I", "V", "vi", "IV")
|
||||
save_midi(chords, "pop.mid", t=500, bpm=120)
|
||||
|
||||
These are all starting points. Change the key, swap the chords, layer in your own ideas -- the best way to learn is to take something that works and make it yours.
|
||||
@@ -0,0 +1,495 @@
|
||||
Drums
|
||||
=====
|
||||
|
||||
Drums are the foundation of almost everything. Change the drum pattern
|
||||
and you change the genre. The same four chords over a bossa nova
|
||||
pattern sound like you're in a cafe in Rio. Put those same chords over
|
||||
a rock beat and you're in a garage in Seattle. Over a trap beat, you're
|
||||
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 -- 51 synthesized percussion
|
||||
sounds, 80+ pattern presets across dozens of genres, and 21 fill presets.
|
||||
Every sound is generated from waveforms; no samples needed.
|
||||
|
||||
Drum Sounds
|
||||
-----------
|
||||
|
||||
Drum hits are **humanized by default** — each hit gets a tiny random
|
||||
timing offset and velocity wobble, just like a real drummer who's never
|
||||
perfectly on the grid. Control the amount with ``drum_humanize`` on the
|
||||
Score:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
score = Score("4/4", bpm=120, drum_humanize=0.4) # natural feel
|
||||
score = Score("4/4", bpm=120, drum_humanize=0.0) # perfectly quantized
|
||||
score = Score("4/4", bpm=120, drum_humanize=0.1) # studio tight
|
||||
|
||||
The default is 0.15 — just enough to feel alive without sounding loose.
|
||||
|
||||
Drums Are Parts
|
||||
~~~~~~~~~~~~~~~~
|
||||
|
||||
Drums are a real Part — the same as any melodic voice. You can set
|
||||
effects on them the same way:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
score.drums("rock", repeats=4)
|
||||
score.parts["drums"].reverb_mix = 0.2
|
||||
score.parts["drums"].reverb_type = "plate"
|
||||
|
||||
Or use the shorthand:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
score.set_drum_effects(reverb=0.2, reverb_type="plate", lowpass=8000)
|
||||
|
||||
Split Drums
|
||||
~~~~~~~~~~~
|
||||
|
||||
For maximum control, split the kit into separate Parts — kick, snare,
|
||||
hats, toms, cymbals, and percussion — each with independent effects:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
score.drums("rock", repeats=4, split=True)
|
||||
|
||||
# Now each group is its own Part
|
||||
score.parts["snare"].reverb_mix = 0.3
|
||||
score.parts["snare"].reverb_type = "plate"
|
||||
score.parts["hats"].lowpass = 7000
|
||||
score.parts["kick"] # dry, no effects
|
||||
|
||||
# set_drum_effects still works — applies to all drum Parts
|
||||
score.set_drum_effects(reverb=0.1)
|
||||
|
||||
This is how real studios work — the snare gets its own reverb send,
|
||||
the hats get their own EQ, the kick stays dry and punchy. Now you
|
||||
can do the same thing in Python.
|
||||
|
||||
Sidechain compression triggers on kick hits only — hi-hats and snares
|
||||
don't duck the pad.
|
||||
|
||||
Every drum sound is stereo-panned like a real kit — kick and snare
|
||||
center, hi-hat right, crash left, toms spread across the field,
|
||||
percussion instruments placed naturally. Put on headphones and you'll
|
||||
hear the kit in front of you.
|
||||
|
||||
The ``DrumSound`` enum maps to General MIDI percussion note numbers:
|
||||
|
||||
.. code-block:: pycon
|
||||
|
||||
>>> from pytheory import DrumSound
|
||||
|
||||
>>> DrumSound.KICK.value
|
||||
36
|
||||
>>> DrumSound.SNARE.value
|
||||
38
|
||||
>>> DrumSound.CLOSED_HAT.value
|
||||
42
|
||||
|
||||
All 51 sounds, organized by type:
|
||||
|
||||
**Kicks:** KICK (36)
|
||||
|
||||
**Snares:** SNARE (38), RIMSHOT (37), CLAP (39)
|
||||
|
||||
**Hi-hats:** CLOSED_HAT (42), OPEN_HAT (46), PEDAL_HAT (44)
|
||||
|
||||
**Toms:** LOW_TOM (45), MID_TOM (47), HIGH_TOM (50)
|
||||
|
||||
**Cymbals:** CRASH (49), RIDE (51), RIDE_BELL (53)
|
||||
|
||||
**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)
|
||||
|
||||
**Tabla:** TABLA_NA (86), TABLA_TIN (87), TABLA_GE (88), TABLA_DHA (89),
|
||||
TABLA_TIT (90), TABLA_KE (91), TABLA_GE_BEND (108 -- bayan with upward
|
||||
pitch bend from palm pressing into the head)
|
||||
|
||||
**Dhol:** DHOL_DAGGA (92), DHOL_TILLI (93), DHOL_BOTH (94)
|
||||
|
||||
**Dholak:** DHOLAK_GE (95), DHOLAK_NA (96), DHOLAK_TIT (97)
|
||||
|
||||
**Mridangam:** MRIDANGAM_THAM (98), MRIDANGAM_NAM (99), MRIDANGAM_DIN (100),
|
||||
MRIDANGAM_THA (101)
|
||||
|
||||
**Djembe:** DJEMBE_BASS (102), DJEMBE_TONE (103), DJEMBE_SLAP (104)
|
||||
|
||||
**Cajón:** CAJON_SLAP (109), CAJON_TAP (110)
|
||||
|
||||
**Metal Kit:** METAL_KICK (105), METAL_SNARE (106), METAL_HAT (107)
|
||||
|
||||
Drum Synthesis
|
||||
--------------
|
||||
|
||||
Every drum sound here is synthesized from scratch using the same
|
||||
techniques that real drum machines use. This isn't a shortcut -- it's
|
||||
the real thing. The 808 kick that defined hip hop is literally a sine
|
||||
wave with a pitch envelope sweeping from 150 Hz down to 50 Hz. The 909
|
||||
snare that powered techno is a sine wave body mixed with white noise
|
||||
rattle. The hi-hat is just filtered noise with a short decay. When
|
||||
Roland built the TR-808 and TR-909, they weren't sampling real drums;
|
||||
they were synthesizing them from basic waveforms. PyTheory does the
|
||||
same thing.
|
||||
|
||||
Each sound has a dedicated synthesizer:
|
||||
|
||||
- **KICK** -- sine wave with pitch envelope sweep (150 to 50 Hz) + sub click
|
||||
- **SNARE** -- pitched body (180 Hz) + white noise rattle
|
||||
- **CLOSED_HAT** -- high-frequency noise, 50ms decay
|
||||
- **OPEN_HAT** -- high-frequency noise, 250ms decay
|
||||
- **CLAP** -- layered noise bursts with spacers
|
||||
- **RIMSHOT** -- bright 800 Hz click + noise
|
||||
- **TOMS** -- pitched sine with sweep (low=100, mid=150, high=200 Hz)
|
||||
- **CRASH** -- long noise decay (1.5s)
|
||||
- **RIDE** -- metallic ring (3500+5100 Hz) + noise
|
||||
- **RIDE_BELL** -- brighter ring, more sustain
|
||||
- **COWBELL** -- two detuned tones (545+815 Hz)
|
||||
- **CLAVE** -- short 2500 Hz click
|
||||
- **CONGAS/BONGOS** -- pitched membrane with slap transient
|
||||
- **TIMBALES** -- bright metallic ring with overtones
|
||||
- **AGOGO** -- pitched bell with harmonics
|
||||
- **SHAKER/MARACAS** -- short noise burst
|
||||
- **TAMBOURINE** -- noise + 7000 Hz jingle ring
|
||||
- **GUIRO** -- scraped noise bursts
|
||||
|
||||
Pattern Presets
|
||||
---------------
|
||||
|
||||
80+ patterns spanning genres from rock to Afro-Cuban to electronic to
|
||||
world percussion. Load them with ``Pattern.preset()``:
|
||||
|
||||
.. code-block:: pycon
|
||||
|
||||
>>> from pytheory import Pattern
|
||||
|
||||
>>> Pattern.list_presets()
|
||||
['12/8 blues', '6/8 afro-cuban', 'afrobeat', 'baiao', 'bebop', ...]
|
||||
|
||||
>>> rock = Pattern.preset("rock")
|
||||
>>> rock
|
||||
<Pattern 'rock' 4/4 4.0 beats 12 hits>
|
||||
|
||||
**Rock/Pop:** rock, half time, double time, disco, motown, train beat
|
||||
-- The backbone of Western popular music. Kick on 1 and 3, snare on 2
|
||||
and 4. Simple, effective, universal.
|
||||
|
||||
**Jazz:** jazz, bebop, shuffle, swing, linear, paradiddle -- The ride
|
||||
cymbal drives everything. The kick and snare comp and converse rather
|
||||
than keeping strict time. These patterns swing.
|
||||
|
||||
**Latin:** salsa, bossa nova, samba, cumbia, merengue, baiao, maracatu,
|
||||
bolero, tango -- Rich, layered patterns built on clave rhythms, with
|
||||
congas, timbales, and shakers creating interlocking polyrhythmic webs.
|
||||
Some of the most sophisticated drumming traditions on the planet.
|
||||
|
||||
**Afro-Cuban:** son clave 3-2, son clave 2-3, rumba clave 3-2,
|
||||
rumba clave 2-3, cascara, guaguanco, mozambique, nanigo, bembe,
|
||||
6/8 afro-cuban, tresillo, habanera -- The clave is the key that
|
||||
unlocks all Latin and Afro-Cuban music. It's a five-note rhythmic
|
||||
cell that everything else revolves around. If you learn one concept
|
||||
from world music, learn the clave.
|
||||
|
||||
**African:** afrobeat, highlife -- Born in West Africa. Fela Kuti's
|
||||
afrobeat layers multiple percussion voices into hypnotic,
|
||||
polyrhythmic grooves that can go on for twenty minutes.
|
||||
|
||||
**Caribbean:** reggae, dancehall, ska, dub -- The offbeat is king.
|
||||
Reggae flips rock drumming inside out by emphasizing the "and" of each
|
||||
beat instead of the beat itself. Ska doubles the tempo, dancehall
|
||||
adds syncopation.
|
||||
|
||||
**Electronic:** house, techno, trap, drum and bass, breakbeat, jungle
|
||||
-- Machine music. The four-on-the-floor kick of house and techno, the
|
||||
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, 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
|
||||
groove of funk, the sampled feel of hip hop, the street-parade swing
|
||||
of New Orleans second line.
|
||||
|
||||
Playing Patterns
|
||||
----------------
|
||||
|
||||
``play_pattern()`` synthesizes every drum sound in real-time:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
from pytheory import Pattern
|
||||
from pytheory.play import play_pattern
|
||||
|
||||
play_pattern(Pattern.preset("rock"), repeats=4, bpm=120)
|
||||
play_pattern(Pattern.preset("bossa nova"), repeats=4, bpm=140)
|
||||
play_pattern(Pattern.preset("salsa"), repeats=4, bpm=180)
|
||||
play_pattern(Pattern.preset("afrobeat"), repeats=8, bpm=110)
|
||||
|
||||
Fills
|
||||
-----
|
||||
|
||||
A fill is the drummer's way of saying "something's about to change."
|
||||
It's that moment at the end of a verse where the drummer breaks the
|
||||
pattern and rolls around the toms before crashing into the chorus. Fills
|
||||
signal transitions -- they tell the listener's ear that the section is
|
||||
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:
|
||||
|
||||
.. 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',
|
||||
'salsa', 'samba', 'second line', 'trap']
|
||||
|
||||
>>> fill = Pattern.fill("rock")
|
||||
>>> fill
|
||||
<Pattern 'rock fill' 4/4 4.0 beats ...>
|
||||
|
||||
Score Integration
|
||||
-----------------
|
||||
|
||||
The ``score.drums()`` shorthand attaches a drum pattern to a score:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
from pytheory import Score
|
||||
|
||||
score = Score("4/4", bpm=140)
|
||||
score.drums("bossa nova", repeats=4)
|
||||
|
||||
Auto-Fills
|
||||
~~~~~~~~~~
|
||||
|
||||
The ``fill`` and ``fill_every`` parameters automatically insert drum
|
||||
fills at regular intervals:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
score = Score("4/4", bpm=120)
|
||||
score.drums("rock", repeats=8, fill="rock", fill_every=4)
|
||||
|
||||
This plays the rock pattern for 8 bars, replacing every 4th bar with
|
||||
a rock fill. Useful for adding natural phrasing to longer sections.
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
# Jazz with brush fills every 8 bars
|
||||
score.drums("bebop", repeats=16, fill="jazz brush", fill_every=8)
|
||||
|
||||
# Salsa with fills every 4 bars
|
||||
score.drums("salsa", repeats=8, fill="salsa", fill_every=4)
|
||||
|
||||
Layering Patterns
|
||||
-----------------
|
||||
|
||||
Combine drum patterns with melodic parts for full arrangements. The
|
||||
drum pattern and all named parts are mixed together by ``play_score()``:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
from pytheory import Score, Key, Duration, Chord
|
||||
from pytheory.play import play_score
|
||||
|
||||
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)
|
||||
|
||||
play_score(score)
|
||||
|
||||
World Percussion
|
||||
----------------
|
||||
|
||||
PyTheory includes dedicated sound sets and pattern presets for
|
||||
traditional percussion instruments from around the world. Each
|
||||
instrument has its own synthesized sounds that capture the timbral
|
||||
character of the real instrument, plus idiomatic rhythmic patterns
|
||||
drawn from their musical traditions.
|
||||
|
||||
Tabla
|
||||
~~~~~
|
||||
|
||||
The tabla is a pair of hand drums from the Indian subcontinent -- the
|
||||
smaller, higher-pitched *dayan* and the larger, bass *bayan*. It is
|
||||
the rhythmic backbone of Hindustani classical music, and one of the
|
||||
most expressive percussion instruments ever created. A single tabla
|
||||
player can produce an astonishing range of tones by varying finger
|
||||
placement, pressure, and striking technique.
|
||||
|
||||
**7 sounds** -- covering the primary tabla strokes (na, tin, tun, ge,
|
||||
dha, ke, tit) plus a bayan pitch bend sound (TABLA_GE_BEND) that
|
||||
models the technique of pressing the palm into the bayan head to bend
|
||||
the pitch upward.
|
||||
|
||||
**7 patterns:** teental (16 beats, the most common taal), jhaptaal
|
||||
(10 beats), rupak (7 beats), dadra (6 beats), keherwa (8 beats, folk
|
||||
and light classical), tabla solo, and tiri kita (fast ornamental
|
||||
pattern).
|
||||
|
||||
**5 fills:** tihai (3x crescendo landing on sam), chakkardar (32nd
|
||||
triplet cascade into slam), tiri kita (rapid 16th-note dayan burst),
|
||||
bayan (deep bass bends showcase), tabla call (dayan/bayan call-and-response).
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
score.drums("teental", repeats=4, fill="tihai")
|
||||
score.drums("keherwa", repeats=4, fill="chakkardar")
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
score = Score("4/4", bpm=80)
|
||||
score.drums("teental", repeats=4)
|
||||
|
||||
Dhol
|
||||
~~~~
|
||||
|
||||
The dhol is a double-headed barrel drum from Punjab, played with
|
||||
sticks. It is the driving force behind bhangra music -- loud,
|
||||
energetic, and physically impossible to sit still to.
|
||||
|
||||
**3 sounds** -- bass stroke, treble stroke, and rimshot.
|
||||
|
||||
**2 patterns:** bhangra (the classic bhangra groove) and dhol chaal
|
||||
(a processional rhythm).
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
score = Score("4/4", bpm=160)
|
||||
score.drums("bhangra", repeats=4)
|
||||
|
||||
Dholak
|
||||
~~~~~~
|
||||
|
||||
The dholak is a smaller, lighter two-headed drum used across South
|
||||
Asia in folk music, qawwali, and Bollywood. Played with bare hands,
|
||||
it produces a warm, melodic tone.
|
||||
|
||||
**3 sounds** -- bass, treble, and slap.
|
||||
|
||||
**2 patterns:** qawwali (the rhythmic foundation of Sufi devotional
|
||||
music) and dholak folk (a general folk groove).
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
score = Score("4/4", bpm=120)
|
||||
score.drums("qawwali", repeats=4)
|
||||
|
||||
Mridangam
|
||||
~~~~~~~~~
|
||||
|
||||
The mridangam is a double-headed drum from South India, the
|
||||
rhythmic anchor of Carnatic classical music. Its tuning system is
|
||||
extraordinarily precise, and its rhythmic vocabulary is among the
|
||||
most mathematically complex in the world.
|
||||
|
||||
**4 sounds** -- tha, thom, nam, and din.
|
||||
|
||||
**2 patterns:** adi talam (the most common Carnatic talam, 8 beats)
|
||||
and mridangam korvai (a rhythmic cadence pattern).
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
score = Score("4/4", bpm=90)
|
||||
score.drums("adi talam", repeats=4)
|
||||
|
||||
Djembe
|
||||
~~~~~~
|
||||
|
||||
The djembe is a rope-tuned goblet drum from West Africa, capable of
|
||||
producing a wide range of tones from deep bass to sharp slaps. It is
|
||||
central to the drum ensemble traditions of Mali, Guinea, and Senegal.
|
||||
|
||||
**3 sounds** -- bass (open center strike), tone (edge strike), and
|
||||
slap (sharp edge strike).
|
||||
|
||||
**3 patterns:** djembe (a basic accompanying rhythm), kuku (a
|
||||
traditional rhythm from Guinea associated with fishing), and soli (a
|
||||
solo/celebration rhythm).
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
score = Score("4/4", bpm=120)
|
||||
score.drums("djembe", repeats=4)
|
||||
|
||||
Metal Kit
|
||||
~~~~~~~~~
|
||||
|
||||
A dedicated percussion kit for extreme metal subgenres, with
|
||||
specialized sounds and patterns that go beyond the standard drum kit.
|
||||
|
||||
**3 sounds** -- double kick (triggered, tight attack), china cymbal,
|
||||
and stack (a short, trashy cymbal choke).
|
||||
|
||||
**4 patterns:** double kick (relentless double bass drum pattern),
|
||||
metal blast (blast beat with china cymbal accents), metal groove (a
|
||||
half-time groove with double kick fills), and metal gallop (the
|
||||
classic triplet-feel gallop rhythm).
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
score = Score("4/4", bpm=200)
|
||||
score.drums("metal blast", repeats=4)
|
||||
|
||||
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.
|
||||
|
||||
**2 sounds** -- slap (sharp, snare-like) and tap (bass-like).
|
||||
|
||||
**3 patterns:** cajon (basic groove), cajon rumba (flamenco-style rumba),
|
||||
and cajon folk (folk/acoustic pattern).
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
score = Score("4/4", bpm=100)
|
||||
score.drums("cajon", repeats=4)
|
||||
|
||||
MIDI Export
|
||||
-----------
|
||||
|
||||
Convert any pattern to a Score, then export to MIDI (drums are written
|
||||
to channel 10):
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
pattern = Pattern.preset("bossa nova")
|
||||
score = pattern.to_score(repeats=8, bpm=140)
|
||||
score.save_midi("bossa.mid")
|
||||
|
||||
Pattern.preset("afrobeat").to_score(repeats=8, bpm=110).save_midi("afrobeat.mid")
|
||||
|
||||
Drums are the foundation. The same chords over a bossa nova feel like a different song than over a rock beat -- change the pattern and you change the genre. Try swapping presets under the same progression and hear how much the drums are really doing.
|
||||
@@ -0,0 +1,912 @@
|
||||
Effects
|
||||
=======
|
||||
|
||||
Effects are how recorded music gets its character. A guitar without
|
||||
reverb sounds like it's being played in a closet. A vocal without
|
||||
compression sounds thin and amateur. A synth without filtering sounds
|
||||
like a test signal. Effects are the difference between "notes" and
|
||||
"music" -- they put the sound in a space, give it texture, and make it
|
||||
feel alive.
|
||||
|
||||
Every record you've ever loved was shaped by effects. The cavernous
|
||||
reverb on a Phil Collins drum hit. The tape delay on a reggae vocal.
|
||||
The distortion on a Hendrix guitar. The chorus on an 80s synth pad.
|
||||
These aren't decorations added after the fact; they're fundamental to
|
||||
the sound itself.
|
||||
|
||||
Each part in a Score can have its own effects chain. Effects are set at
|
||||
part creation and applied per-part before mixing, so every voice gets
|
||||
independent processing.
|
||||
|
||||
Signal Chain
|
||||
------------
|
||||
|
||||
The order of effects matters -- a lot. Distortion before a lowpass
|
||||
filter means you're generating all those rich, crunchy harmonics and
|
||||
then sculpting them with the filter. That's warm, controllable,
|
||||
musical. Filter before distortion means you're distorting the already-
|
||||
filtered signal -- a different, often harsher character. The fixed
|
||||
order in PyTheory matches classic analog synth architecture, the same
|
||||
signal path used by the Moog, the TB-303, and most hardware synths.
|
||||
It's a well-tested order that sounds good by default.
|
||||
|
||||
Effects are applied in this fixed order::
|
||||
|
||||
Signal --> Saturation --> Tremolo --> Distortion --> Cabinet --> Chorus
|
||||
--> Phaser --> Highpass --> Lowpass --> Delay --> Reverb --> Mix
|
||||
|
||||
Additionally, these per-note effects are applied before the part effects chain:
|
||||
|
||||
- **Sub-oscillator**: octave-below sine mixed in at the oscillator stage
|
||||
- **Noise layer**: filtered noise mixed per-note for breath/transients
|
||||
- **Filter envelope**: per-note lowpass sweep (attack/decay/sustain)
|
||||
- **Velocity → brightness**: harder velocity = brighter filter cutoff
|
||||
|
||||
Part-level effects:
|
||||
|
||||
- **Saturation** first: subtle even-harmonic warmth (tape/tube color).
|
||||
- **Tremolo** second: amplitude LFO modulation.
|
||||
- **Distortion** third: drives the signal before filtering.
|
||||
- **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
|
||||
----------
|
||||
|
||||
You know what distortion sounds like -- it's the sound of rock and roll.
|
||||
An electric guitar through a cranked amplifier. But at lower levels,
|
||||
distortion is subtler: it adds warmth, presence, and harmonic richness.
|
||||
This is why producers run clean signals through tape machines and tube
|
||||
preamps. A little saturation makes everything sound more "real."
|
||||
|
||||
Soft-clip waveshaping using ``tanh`` -- models the warm saturation of an
|
||||
overdriven tube amplifier. At low drive levels it adds harmonic warmth;
|
||||
at high levels it becomes an aggressive fuzz.
|
||||
|
||||
Parameters:
|
||||
|
||||
- ``distortion``: Wet/dry mix, 0.0--1.0.
|
||||
- ``distortion_drive``: Gain before clipping (default 3.0).
|
||||
|
||||
- 0.5--2 = subtle warmth (tube preamp)
|
||||
- 3--8 = overdrive (cranked amp)
|
||||
- 10+ = fuzz
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
# Warm tube saturation on a bass
|
||||
bass = score.part(
|
||||
"bass",
|
||||
synth="sine",
|
||||
envelope="pluck",
|
||||
distortion=0.3,
|
||||
distortion_drive=2.0,
|
||||
)
|
||||
|
||||
# Heavy fuzz on a lead
|
||||
lead = score.part(
|
||||
"lead",
|
||||
synth="saw",
|
||||
envelope="staccato",
|
||||
distortion=0.8,
|
||||
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
|
||||
------
|
||||
|
||||
That shimmery, wide, slightly-out-of-focus sound that defined the
|
||||
1980s? That's chorus. Think of the intro to "Come As You Are" by
|
||||
Nirvana, or literally any synth pad from 1983 to 1989. It makes one
|
||||
instrument sound like two or three playing together, slightly out of
|
||||
tune with each other -- which is exactly how a real string section or
|
||||
choir sounds rich and full.
|
||||
|
||||
A slightly detuned, LFO-modulated delayed copy mixed back in. Thickens
|
||||
the sound like two musicians playing the same part -- the signature
|
||||
effect of the Roland Juno synthesizers.
|
||||
|
||||
Parameters:
|
||||
|
||||
- ``chorus``: Wet/dry mix, 0.0--1.0.
|
||||
- ``chorus_rate``: LFO speed in Hz. 0.5--1 = slow shimmer, 2--4 = vibrato.
|
||||
- ``chorus_depth``: Modulation depth in seconds (default 0.003).
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
# Juno-style pad chorus
|
||||
pad = score.part(
|
||||
"pad",
|
||||
synth="supersaw",
|
||||
envelope="pad",
|
||||
chorus=0.5,
|
||||
chorus_rate=1.5,
|
||||
chorus_depth=0.003,
|
||||
)
|
||||
|
||||
# Subtle thickening on a clean lead
|
||||
lead = score.part(
|
||||
"lead",
|
||||
synth="triangle",
|
||||
envelope="pluck",
|
||||
chorus=0.2,
|
||||
chorus_rate=0.8,
|
||||
)
|
||||
|
||||
Lowpass Filter
|
||||
--------------
|
||||
|
||||
You know that sound when a DJ turns the knob and everything goes
|
||||
underwater? That's a lowpass filter closing down. It removes
|
||||
high-frequency content, leaving only the warm, round, bassy
|
||||
frequencies below the cutoff point. The lowpass filter is arguably the
|
||||
most important effect in all of electronic music -- it's the entire
|
||||
sound of acid house, the "wah" in auto-wah, and the reason analog
|
||||
synths sound warm instead of harsh.
|
||||
|
||||
A 12 dB/octave biquad lowpass filter with resonance -- the sound of
|
||||
analog synthesizers. Removes frequencies above the cutoff; the resonance
|
||||
(Q) parameter adds a peak at the cutoff frequency for that classic
|
||||
"acid squelch."
|
||||
|
||||
Parameters:
|
||||
|
||||
- ``lowpass``: Cutoff frequency in Hz (0 = off). Reference points:
|
||||
|
||||
- 200--400 Hz = deep sub bass
|
||||
- 800--1500 Hz = warm / muffled
|
||||
- 2000--4000 Hz = present lead
|
||||
- 5000+ Hz = subtle rolloff
|
||||
|
||||
- ``lowpass_q``: Resonance / Q factor (default 0.707 = Butterworth flat).
|
||||
|
||||
- 1.0 = slight peak
|
||||
- 2.0 = pronounced
|
||||
- 5.0+ = aggressive acid squelch
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
# Round bass with gentle filtering
|
||||
bass = score.part(
|
||||
"bass",
|
||||
synth="sine",
|
||||
envelope="pluck",
|
||||
lowpass=400,
|
||||
lowpass_q=1.5,
|
||||
)
|
||||
|
||||
# Acid squelch on a saw lead
|
||||
acid = score.part(
|
||||
"acid",
|
||||
synth="saw",
|
||||
envelope="staccato",
|
||||
lowpass=1500,
|
||||
lowpass_q=5.0,
|
||||
legato=True,
|
||||
glide=0.03,
|
||||
)
|
||||
|
||||
Delay
|
||||
-----
|
||||
|
||||
Delay is echo. Literally. The Edge from U2 built his entire guitar
|
||||
sound around dotted-eighth-note delays. Dub reggae producers like Lee
|
||||
"Scratch" Perry and King Tubby turned delay into an art form, feeding
|
||||
echoes back into themselves until they spiraled into infinity. At short
|
||||
times with low feedback, delay adds rhythmic interest. At long times
|
||||
with high feedback, it creates cascading, psychedelic soundscapes.
|
||||
|
||||
Tempo-synced echoes with feedback. Each repeat feeds back into the
|
||||
delay line, creating rhythmic echo trails. High feedback values produce
|
||||
the cascading, self-oscillating echoes of dub reggae.
|
||||
|
||||
Parameters:
|
||||
|
||||
- ``delay``: Wet/dry mix, 0.0--1.0.
|
||||
- ``delay_time``: Time between echoes in seconds. Musically useful
|
||||
values at 120 bpm: 0.25 (8th note), 0.375 (dotted 8th),
|
||||
0.5 (quarter note).
|
||||
- ``delay_feedback``: How much each echo feeds back (0.0--1.0).
|
||||
|
||||
- 0.3 = a few repeats
|
||||
- 0.5 = many repeats
|
||||
- 0.7+ = runaway (dub style)
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
# Dotted-eighth slapback on a lead
|
||||
lead = score.part(
|
||||
"lead",
|
||||
synth="triangle",
|
||||
envelope="strings",
|
||||
delay=0.3,
|
||||
delay_time=0.375,
|
||||
delay_feedback=0.4,
|
||||
)
|
||||
|
||||
# Dub-style runaway echoes
|
||||
melodica = score.part(
|
||||
"melodica",
|
||||
synth="triangle",
|
||||
envelope="pluck",
|
||||
delay=0.5,
|
||||
delay_time=0.66,
|
||||
delay_feedback=0.55,
|
||||
)
|
||||
|
||||
Reverb
|
||||
------
|
||||
|
||||
Everyone knows what reverb sounds like, even if they don't know the
|
||||
word -- it's the sound of singing in the shower, or clapping in a
|
||||
cathedral. It's the natural echo of a space. Without reverb, sounds
|
||||
feel uncomfortably close and dry, like someone whispering directly into
|
||||
your ear. With it, sounds feel like they exist in a real place. Reverb
|
||||
is the most universally used effect in all of recorded music.
|
||||
|
||||
PyTheory offers two reverb engines: a fast **algorithmic** reverb for
|
||||
general use, and **convolution** reverb for photorealistic acoustic
|
||||
spaces.
|
||||
|
||||
Algorithmic Reverb
|
||||
~~~~~~~~~~~~~~~~~~
|
||||
|
||||
A Schroeder reverb using 4 parallel comb filters and 2 series allpass
|
||||
filters. Fast, lightweight, and good for general-purpose room
|
||||
simulation.
|
||||
|
||||
Parameters:
|
||||
|
||||
- ``reverb``: Wet/dry mix, 0.0--1.0.
|
||||
|
||||
- 0.2--0.4 = subtle space
|
||||
- 0.5--0.8 = ambient / dub
|
||||
|
||||
- ``reverb_decay``: Tail length in seconds.
|
||||
|
||||
- 0.5 = small room
|
||||
- 1.5 = hall
|
||||
- 3.0+ = cathedral / dub
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
# Jazz club ambience
|
||||
rhodes = score.part(
|
||||
"rhodes",
|
||||
synth="fm",
|
||||
envelope="piano",
|
||||
reverb=0.4,
|
||||
reverb_decay=1.8,
|
||||
)
|
||||
|
||||
Convolution Reverb
|
||||
~~~~~~~~~~~~~~~~~~
|
||||
|
||||
Convolution reverb works by convolving your audio with an *impulse
|
||||
response* -- a recording (or simulation) of a real acoustic space.
|
||||
Where algorithmic reverb approximates the math of reflections,
|
||||
convolution reverb *is* the space. You hear every surface, every
|
||||
angle, every material.
|
||||
|
||||
PyTheory generates synthetic impulse responses that model the acoustic
|
||||
properties of real spaces: early reflection patterns, exponential
|
||||
decay envelopes, frequency-dependent absorption (high frequencies die
|
||||
faster in stone), diffusion density, and subtle pitch modulation from
|
||||
irregular surfaces. The result is dramatically more realistic than
|
||||
algorithmic reverb, especially for long tails and large spaces.
|
||||
|
||||
Set ``reverb_type`` to any preset name instead of ``"algorithmic"``:
|
||||
|
||||
- ``"taj_mahal"`` -- Massive marble dome. 12-second tail, bright early
|
||||
reflections, enormously dense and diffuse. The most dramatic verb
|
||||
you've ever heard.
|
||||
- ``"cathedral"`` -- Gothic stone cathedral. 6 seconds, strong early
|
||||
reflections off parallel walls, dark reverberant tail.
|
||||
- ``"plate"`` -- EMT 140 plate reverb. 4 seconds, dense, bright, smooth.
|
||||
The studio classic that defined pop records from the 60s onward.
|
||||
- ``"spring"`` -- Spring reverb tank. 3 seconds, metallic, boingy, lo-fi.
|
||||
The sound of surf rock and guitar amps.
|
||||
- ``"cave"`` -- Natural cave. 8 seconds, very dark, irregular reflections.
|
||||
High frequencies are aggressively absorbed by rock.
|
||||
- ``"parking_garage"`` -- Concrete box. 3 seconds, bright, flutter echoes
|
||||
from parallel hard walls.
|
||||
- ``"canyon"`` -- Open canyon. 5 seconds, sparse discrete echoes (the
|
||||
walls are far apart) dissolving into a diffuse tail.
|
||||
|
||||
Parameters:
|
||||
|
||||
- ``reverb``: Wet/dry mix, 0.0--1.0.
|
||||
- ``reverb_type``: Preset name (default ``"algorithmic"``).
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
# FM flute through the Taj Mahal
|
||||
flute = score.part(
|
||||
"flute",
|
||||
synth="fm",
|
||||
envelope="bell",
|
||||
reverb=0.85,
|
||||
reverb_type="taj_mahal",
|
||||
delay=0.65,
|
||||
delay_time=0.375,
|
||||
delay_feedback=0.55,
|
||||
)
|
||||
|
||||
# Cathedral wash for ambient pads
|
||||
pad = score.part(
|
||||
"pad",
|
||||
synth="supersaw",
|
||||
envelope="pad",
|
||||
reverb=0.7,
|
||||
reverb_type="cathedral",
|
||||
)
|
||||
|
||||
# Classic plate on a vocal-style lead
|
||||
lead = score.part(
|
||||
"lead",
|
||||
synth="triangle",
|
||||
envelope="strings",
|
||||
reverb=0.5,
|
||||
reverb_type="plate",
|
||||
)
|
||||
|
||||
# Algorithmic reverb still works as before
|
||||
rhodes = score.part(
|
||||
"rhodes",
|
||||
synth="fm",
|
||||
envelope="piano",
|
||||
reverb=0.4,
|
||||
reverb_decay=1.8,
|
||||
)
|
||||
|
||||
You can switch reverb types mid-song with automation:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
lead = score.part("lead", synth="fm", envelope="bell",
|
||||
reverb=0.5, reverb_type="plate")
|
||||
lead.add("C5", Duration.WHOLE)
|
||||
|
||||
# Switch to cathedral for the big section
|
||||
lead.set(reverb_type="cathedral", reverb=0.8)
|
||||
lead.add("E5", Duration.WHOLE)
|
||||
|
||||
Combining Effects
|
||||
-----------------
|
||||
|
||||
Effects stack naturally. Here are some real-world combinations:
|
||||
|
||||
Dub
|
||||
~~~
|
||||
|
||||
Distortion warmth into filtered delay into deep reverb:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
melodica = score.part(
|
||||
"melodica",
|
||||
synth="triangle",
|
||||
envelope="pluck",
|
||||
distortion=0.2,
|
||||
distortion_drive=2.0,
|
||||
lowpass=2000,
|
||||
lowpass_q=1.2,
|
||||
delay=0.5,
|
||||
delay_time=0.66,
|
||||
delay_feedback=0.55,
|
||||
reverb=0.4,
|
||||
reverb_decay=2.5,
|
||||
)
|
||||
|
||||
Acid
|
||||
~~~~
|
||||
|
||||
Resonant lowpass with distortion and delay:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
acid = score.part(
|
||||
"acid",
|
||||
synth="saw",
|
||||
envelope="staccato",
|
||||
lowpass=1500,
|
||||
lowpass_q=3.0,
|
||||
distortion=0.4,
|
||||
distortion_drive=4.0,
|
||||
delay=0.3,
|
||||
delay_time=0.242,
|
||||
delay_feedback=0.4,
|
||||
)
|
||||
|
||||
Ambient
|
||||
~~~~~~~
|
||||
|
||||
Wide chorus, long reverb, gentle delay:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
ambient = score.part(
|
||||
"ambient",
|
||||
synth="supersaw",
|
||||
envelope="pad",
|
||||
chorus=0.4,
|
||||
chorus_rate=0.5,
|
||||
delay=0.3,
|
||||
delay_time=0.5,
|
||||
delay_feedback=0.5,
|
||||
reverb=0.7,
|
||||
reverb_decay=4.0,
|
||||
)
|
||||
|
||||
808 Bass
|
||||
~~~~~~~~
|
||||
|
||||
Subtle saturation and deep filtering for hip-hop sub bass:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
bass = score.part(
|
||||
"bass",
|
||||
synth="sine",
|
||||
envelope="pluck",
|
||||
lowpass=200,
|
||||
lowpass_q=1.8,
|
||||
distortion=0.4,
|
||||
distortion_drive=2.0,
|
||||
)
|
||||
|
||||
Sidechain Compression
|
||||
---------------------
|
||||
|
||||
If you've ever heard a house track where the pad *breathes* — gets
|
||||
quiet every time the kick hits and swells back up between beats —
|
||||
that's sidechain compression. It's the pumping effect that defines
|
||||
modern electronic music. The kick drum triggers a compressor on
|
||||
another part, ducking its volume in rhythm with the beat.
|
||||
|
||||
In PyTheory, the drum hits are the trigger. Any part with
|
||||
``sidechain > 0`` gets ducked whenever the kick (or any drum) hits:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
# Classic EDM pump — pad ducks hard on every kick
|
||||
pad = score.part(
|
||||
"pad",
|
||||
synth="supersaw",
|
||||
envelope="pad",
|
||||
sidechain=0.85,
|
||||
sidechain_release=0.15,
|
||||
)
|
||||
|
||||
# Bass breathes with the kick too, but less aggressively
|
||||
bass = score.part(
|
||||
"bass",
|
||||
synth="sine",
|
||||
lowpass=250,
|
||||
sidechain=0.7,
|
||||
sidechain_release=0.1,
|
||||
)
|
||||
|
||||
Parameters:
|
||||
|
||||
- ``sidechain``: How much to duck, 0.0–1.0 (default 0, off).
|
||||
0.5 = subtle pump, 0.7 = noticeable, 0.85 = classic EDM, 1.0 = full silence on hits.
|
||||
- ``sidechain_release``: How fast the volume comes back, in seconds
|
||||
(default 0.1). Shorter = tighter, longer = more dramatic pump.
|
||||
|
||||
The lead stays above the pump — don't sidechain everything or the
|
||||
whole mix will gasp for air:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
# Lead cuts through — no sidechain
|
||||
lead = score.part(
|
||||
"lead",
|
||||
synth="saw",
|
||||
envelope="pluck",
|
||||
delay=0.2,
|
||||
)
|
||||
|
||||
Saturation
|
||||
----------
|
||||
|
||||
Saturation is the warm, subtle harmonic enhancement of analog tape
|
||||
machines and tube preamps. Unlike distortion (which uses ``tanh`` and
|
||||
adds harsh odd harmonics), saturation uses a polynomial waveshaper
|
||||
that adds even harmonics -- 2nd and 4th -- which the ear perceives as
|
||||
warmth and fullness. It's why records mixed through a Neve console
|
||||
sound "bigger" than the same mix done in the box.
|
||||
|
||||
Parameters:
|
||||
|
||||
- ``saturation``: Amount, 0.0--1.0 (default 0, off).
|
||||
|
||||
- 0.05--0.15 = subtle analog warmth (tape machine)
|
||||
- 0.2--0.4 = noticeable color (tube preamp)
|
||||
- 0.5+ = heavy coloring
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
# Warm up a bass
|
||||
bass = score.part("bass", synth="saw", saturation=0.2)
|
||||
|
||||
# Glue a string ensemble
|
||||
strings = score.part("strings", instrument="string_ensemble",
|
||||
saturation=0.1)
|
||||
|
||||
Tremolo
|
||||
-------
|
||||
|
||||
Amplitude modulation by a sine LFO. The classic vibrating-amp sound.
|
||||
Essential for vibraphone (the rotating discs in the resonator tubes),
|
||||
Rhodes electric piano, and surf guitar. Not to be confused with
|
||||
vibrato (pitch modulation).
|
||||
|
||||
Parameters:
|
||||
|
||||
- ``tremolo_depth``: Modulation depth, 0.0--1.0 (default 0, off).
|
||||
- ``tremolo_rate``: LFO speed in Hz (default 5.0).
|
||||
|
||||
- 3--5 Hz = classic tremolo
|
||||
- 5--7 Hz = vibraphone motor speed
|
||||
- 8+ Hz = ring-mod territory
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
# Classic Fender amp tremolo
|
||||
guitar = score.part("guitar", synth="saw", envelope="pluck",
|
||||
tremolo_depth=0.3, tremolo_rate=4.0)
|
||||
|
||||
# Vibraphone with motor
|
||||
vib = score.part("vib", instrument="vibraphone") # built in
|
||||
|
||||
Phaser
|
||||
------
|
||||
|
||||
A chain of allpass filters whose center frequencies are swept by an
|
||||
LFO, creating moving notches in the spectrum. The classic "jet
|
||||
engine" or "underwater" effect. Think Small Stone, MXR Phase 90, or
|
||||
the intro to "Eruption." Different from chorus -- chorus adds a
|
||||
detuned copy, phaser cancels specific frequencies.
|
||||
|
||||
Parameters:
|
||||
|
||||
- ``phaser``: Wet/dry mix, 0.0--1.0 (default 0, off).
|
||||
- ``phaser_rate``: LFO sweep speed in Hz (default 0.5).
|
||||
|
||||
- 0.1--0.3 = slow, lush sweep
|
||||
- 0.5--1.0 = classic phaser
|
||||
- 2.0+ = fast, Leslie-like
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
# Slow sweep on a pad
|
||||
pad = score.part("pad", synth="supersaw", envelope="pad",
|
||||
phaser=0.4, phaser_rate=0.2)
|
||||
|
||||
# Leslie sim on organ (built in)
|
||||
organ = score.part("organ", instrument="organ")
|
||||
|
||||
Highpass Filter
|
||||
---------------
|
||||
|
||||
The opposite of lowpass -- removes low-frequency content below the
|
||||
cutoff. Useful for cleaning up mud from pads, keeping multiple bass
|
||||
parts from masking each other, or thinning out a sound to sit better
|
||||
in a mix.
|
||||
|
||||
Parameters:
|
||||
|
||||
- ``highpass``: Cutoff frequency in Hz (0 = off).
|
||||
|
||||
- 80--150 Hz = clean up sub rumble
|
||||
- 200--400 Hz = thin out a pad
|
||||
- 500+ Hz = telephone / radio effect
|
||||
|
||||
- ``highpass_q``: Resonance / Q factor (default 0.707).
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
# Clean up sub rumble from a pad
|
||||
pad = score.part("pad", synth="supersaw", highpass=120)
|
||||
|
||||
# Thin out rhythm guitar to leave room for bass
|
||||
rhythm = score.part("rhythm", synth="saw", highpass=250)
|
||||
|
||||
Filter Envelope
|
||||
---------------
|
||||
|
||||
A per-note lowpass filter whose cutoff sweeps over time. This is the
|
||||
core of subtractive synthesis -- the reason a Moog bass goes "bwow"
|
||||
instead of "boop." The filter opens on the attack and closes during
|
||||
decay, giving each note a distinctive timbral shape.
|
||||
|
||||
Parameters:
|
||||
|
||||
- ``filter_amount``: Sweep range in Hz (0 = off). How far the filter
|
||||
opens above the base cutoff.
|
||||
- ``filter_attack``: Time to reach peak cutoff, in seconds (default 0.01).
|
||||
- ``filter_decay``: Time to fall to sustain level (default 0.3).
|
||||
- ``filter_sustain``: Sustain level as fraction of amount, 0.0--1.0
|
||||
(default 0.0 = filter closes completely after decay).
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
# Classic synth bass "bwow"
|
||||
bass = score.part("bass", instrument="synth_bass") # built in
|
||||
|
||||
# Acid squelch
|
||||
acid = score.part("acid", instrument="acid_bass") # built in
|
||||
|
||||
# Custom filter sweep on a lead
|
||||
lead = score.part("lead", synth="saw",
|
||||
filter_amount=4000, filter_attack=0.01,
|
||||
filter_decay=0.4, filter_sustain=0.1)
|
||||
|
||||
Velocity to Brightness
|
||||
~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
Real instruments get brighter when played harder. ``vel_to_filter``
|
||||
maps note velocity to filter cutoff boost, so louder notes have more
|
||||
high-frequency content.
|
||||
|
||||
- ``vel_to_filter``: Cutoff boost in Hz at max velocity (default 0, off).
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
# Piano: soft = mellow, loud = bright
|
||||
piano = score.part("piano", instrument="piano") # built in
|
||||
|
||||
# Manual: custom velocity mapping on a lead
|
||||
lead = score.part("lead", synth="saw", vel_to_filter=3000)
|
||||
|
||||
Sub-Oscillator
|
||||
--------------
|
||||
|
||||
An octave-below sine wave mixed in with the main oscillator. Adds
|
||||
low-end weight without muddiness -- the sub fills in the fundamental
|
||||
while the main oscillator provides harmonic character above.
|
||||
|
||||
- ``sub_osc``: Mix level, 0.0--1.0 (default 0, off).
|
||||
|
||||
- 0.1--0.2 = subtle weight (tuba, bass guitar)
|
||||
- 0.3--0.5 = heavy sub (808, synth bass)
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
# Fat 808 kick-bass
|
||||
bass = score.part("bass", instrument="808_bass") # built in
|
||||
|
||||
# Add weight to any part
|
||||
lead = score.part("lead", synth="saw", sub_osc=0.3)
|
||||
|
||||
Noise Layer
|
||||
-----------
|
||||
|
||||
White noise mixed into each note, following the same amplitude
|
||||
envelope. Adds breath for woodwinds, hammer/felt noise for piano,
|
||||
bow rosin for strings, and attack transients for percussion.
|
||||
|
||||
- ``noise_mix``: Mix level, 0.0--1.0 (default 0, off).
|
||||
|
||||
- 0.02--0.04 = subtle texture (strings, piano)
|
||||
- 0.05--0.08 = noticeable breath (woodwinds)
|
||||
- 0.1+ = heavy air/texture
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
# Breathy flute
|
||||
flute = score.part("flute", instrument="flute") # noise_mix=0.08
|
||||
|
||||
# Add air to any synth
|
||||
pad = score.part("pad", synth="supersaw", noise_mix=0.05)
|
||||
|
||||
Configurable FM
|
||||
---------------
|
||||
|
||||
The FM synth now accepts ``fm_ratio`` and ``fm_index`` parameters,
|
||||
letting you dial in specific FM timbres instead of using the defaults.
|
||||
|
||||
- ``fm_ratio``: Modulator frequency as multiple of carrier (default 2.0).
|
||||
Integer ratios = harmonic timbres; non-integer = metallic/inharmonic.
|
||||
- ``fm_index``: Modulation depth (default 3.0). Higher = more harmonics.
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
# Warm electric piano (low ratio, low index)
|
||||
ep = score.part("ep", synth="fm", fm_ratio=1.0, fm_index=1.5)
|
||||
|
||||
# Bright metallic bell (high ratio, high index)
|
||||
bell = score.part("bell", synth="fm", fm_ratio=3.5, fm_index=5.0)
|
||||
|
||||
# Glockenspiel
|
||||
glock = score.part("glock", instrument="glockenspiel") # built in
|
||||
|
||||
Automation
|
||||
----------
|
||||
|
||||
Static effects are fine for a loop, but music breathes. The filter
|
||||
*opens* during the chorus. The reverb *swells* before the drop. The
|
||||
distortion *kicks in* when the guitar solo starts. Automation is what
|
||||
makes a track feel alive instead of robotic -- it's the difference
|
||||
between a static loop and a piece of music that has dynamics, tension,
|
||||
and release. If you've ever felt a song "build" toward something,
|
||||
you're hearing automation at work.
|
||||
|
||||
``Part.set()`` changes effect parameters mid-song at the current beat
|
||||
position. The renderer splits the audio at automation points and
|
||||
processes each section independently:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
lead = score.part("lead", synth="saw", lowpass=400, lowpass_q=3.0)
|
||||
|
||||
# Verse: filtered and clean
|
||||
lead.arpeggio("Cm", bars=4, pattern="up", octaves=2)
|
||||
|
||||
# Chorus: filter opens, chorus kicks in
|
||||
lead.set(lowpass=2000, chorus=0.3)
|
||||
lead.arpeggio("Fm", bars=4, pattern="updown", octaves=2)
|
||||
|
||||
# Drop: full send
|
||||
lead.set(lowpass=4000, distortion=0.7, reverb=0.3)
|
||||
lead.arpeggio("Gm", bars=4, pattern="updown", octaves=2)
|
||||
|
||||
Any parameter can be automated: ``lowpass``, ``lowpass_q``, ``highpass``,
|
||||
``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
|
||||
--------------
|
||||
|
||||
An LFO -- Low Frequency Oscillator -- is just automation that repeats.
|
||||
Instead of manually setting parameter changes, you let a wave shape do
|
||||
it for you, cycling back and forth continuously. You already know what
|
||||
LFOs sound like, even if you don't know the term. The wobble bass in
|
||||
dubstep? That's an LFO on the filter cutoff. Tremolo on a guitar amp?
|
||||
LFO on volume. Auto-wah? LFO on filter cutoff with resonance cranked
|
||||
up. Vibrato? LFO on pitch. It's one simple concept that produces a
|
||||
huge range of effects.
|
||||
|
||||
``Part.lfo()`` automates a parameter with a low-frequency oscillator,
|
||||
generating smooth sweeps over time. This is how filter sweeps, tremolo,
|
||||
and auto-wah effects work.
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
lead = score.part("lead", synth="saw", lowpass=400)
|
||||
|
||||
# Slow filter sweep: 400 -> 3000 Hz over 8 bars
|
||||
lead.lfo("lowpass", rate=0.125, min=400, max=3000, bars=8)
|
||||
lead.arpeggio("Cm", bars=8, pattern="up", octaves=2)
|
||||
|
||||
Parameters:
|
||||
|
||||
- ``param``: Parameter name to modulate (``"lowpass"``, ``"reverb"``,
|
||||
``"distortion"``, ``"volume"``, ``"chorus"``, ``"delay"``).
|
||||
- ``rate``: LFO speed in cycles per bar (default 0.5 = one sweep
|
||||
every 2 bars). 0.25 = very slow, 1 = once per bar, 4 = four times
|
||||
per bar.
|
||||
- ``min`` / ``max``: Parameter value range.
|
||||
- ``bars``: Number of bars to run the LFO over (default 4).
|
||||
- ``shape``: Waveform shape.
|
||||
|
||||
- ``"sine"`` -- smooth, natural sweep
|
||||
- ``"triangle"`` -- linear up/down
|
||||
- ``"saw"`` -- ramp up, snap back
|
||||
- ``"square"`` -- abrupt on/off
|
||||
|
||||
- ``resolution``: How often to insert automation points, in beats
|
||||
(default 0.25 = every 16th note). Lower values = smoother curves.
|
||||
|
||||
Stacking Multiple LFOs
|
||||
~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
Call ``.lfo()`` multiple times to modulate different parameters
|
||||
simultaneously:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
lead = score.part("lead", synth="saw", lowpass=800, reverb=0.1)
|
||||
|
||||
# Filter opens over 8 bars
|
||||
lead.lfo("lowpass", rate=0.125, min=400, max=4000, bars=8)
|
||||
# Reverb swells in and out every 2 bars
|
||||
lead.lfo("reverb", rate=0.5, min=0.1, max=0.6, bars=8, shape="triangle")
|
||||
# Volume tremolo
|
||||
lead.lfo("volume", rate=2, min=0.3, max=0.6, bars=8, shape="sine")
|
||||
|
||||
lead.arpeggio("Cm", bars=8, pattern="updown", octaves=2)
|
||||
|
||||
Effects are what turn notes into music -- the space, the movement, the character. A dry signal is just information; reverb, delay, and filtering are what make it feel like something. Experiment freely, trust your ears.
|
||||
+291
-48
@@ -1,78 +1,321 @@
|
||||
Fretboard and Fingerings
|
||||
========================
|
||||
Instruments and Fingerings
|
||||
==========================
|
||||
|
||||
The :class:`~pytheory.chords.Fretboard` class represents a fretted instrument's
|
||||
tuning and generates chord fingerings.
|
||||
The :class:`~pytheory.chords.Fretboard` class models any stringed
|
||||
instrument and generates chord fingerings. PyTheory includes **25
|
||||
instrument presets** spanning Western, Asian, Middle Eastern, Latin
|
||||
American, and Russian traditions.
|
||||
|
||||
Preset Tunings
|
||||
--------------
|
||||
How It Works
|
||||
------------
|
||||
|
||||
.. code-block:: python
|
||||
Each `fret <https://en.wikipedia.org/wiki/Fret>`_ on a stringed
|
||||
instrument raises the pitch by exactly **one semitone**. The open
|
||||
string is fret 0; fret 1 is one semitone up, and so on. Even fretless
|
||||
instruments (violin, oud, erhu) can be modeled this way — the "fret"
|
||||
positions are just semitone steps along the fingerboard.
|
||||
|
||||
from pytheory import Fretboard
|
||||
Guitars
|
||||
-------
|
||||
|
||||
guitar = Fretboard.guitar() # E4 B3 G3 D3 A2 E2
|
||||
bass = Fretboard.bass() # G2 D2 A1 E1
|
||||
ukulele = Fretboard.ukulele() # A4 E4 C4 G4
|
||||
`Standard guitar tuning <https://en.wikipedia.org/wiki/Guitar_tunings>`_
|
||||
(high to low)::
|
||||
|
||||
Custom Tunings
|
||||
--------------
|
||||
String 1: E4 (highest)
|
||||
String 2: B3
|
||||
String 3: G3
|
||||
String 4: D3
|
||||
String 5: A2
|
||||
String 6: E2 (lowest)
|
||||
|
||||
.. code-block:: python
|
||||
This tuning uses intervals of a perfect 4th (5 semitones) between most
|
||||
strings, except between G and B which is a major 3rd (4 semitones).
|
||||
|
||||
from pytheory import Tone, Fretboard
|
||||
.. code-block:: pycon
|
||||
|
||||
# Open D tuning
|
||||
open_d = Fretboard(tones=[
|
||||
Tone.from_string("D4"),
|
||||
Tone.from_string("A3"),
|
||||
Tone.from_string("F#3"),
|
||||
Tone.from_string("D3"),
|
||||
Tone.from_string("A2"),
|
||||
Tone.from_string("D2"),
|
||||
])
|
||||
>>> from pytheory import Fretboard
|
||||
|
||||
>>> guitar = Fretboard.guitar() # Standard EADGBE
|
||||
>>> twelve = Fretboard.twelve_string() # 12-string (6 doubled courses)
|
||||
>>> bass = Fretboard.bass() # Standard 4-string EADG
|
||||
>>> bass5 = Fretboard.bass(five_string=True) # 5-string with low B
|
||||
|
||||
**Alternate tunings** — 8 built-in presets:
|
||||
|
||||
.. code-block:: pycon
|
||||
|
||||
>>> Fretboard.guitar("drop d") # DADGBE — heavy riffs, metal
|
||||
>>> Fretboard.guitar("open g") # DGDGBD — slide guitar, Keith Richards
|
||||
>>> Fretboard.guitar("open d") # DADF#AD — slide, folk
|
||||
>>> Fretboard.guitar("open e") # EBEG#BE — slide blues
|
||||
>>> Fretboard.guitar("open a") # EAC#EAE
|
||||
>>> Fretboard.guitar("dadgad") # DADGAD — Celtic, fingerstyle
|
||||
>>> Fretboard.guitar("half step down") # Eb standard — Hendrix, SRV
|
||||
|
||||
>>> # Custom tuning with any notes
|
||||
>>> Fretboard.guitar(("C4", "G3", "C3", "G2", "C2", "G1"))
|
||||
|
||||
**Capo** — a `capo <https://en.wikipedia.org/wiki/Capo>`_ raises all
|
||||
strings by a number of frets, letting you play open chord shapes in
|
||||
higher keys:
|
||||
|
||||
.. code-block:: pycon
|
||||
|
||||
>>> # Capo on fret 2 — open G shape now sounds as A major
|
||||
>>> fb = Fretboard.guitar(capo=2)
|
||||
|
||||
>>> # Or apply a capo to an existing fretboard
|
||||
>>> fb = Fretboard.guitar()
|
||||
>>> fb_capo3 = fb.capo(3)
|
||||
|
||||
The Mandolin Family
|
||||
-------------------
|
||||
|
||||
The `mandolin family <https://en.wikipedia.org/wiki/Mandolin_family>`_
|
||||
mirrors the `violin family <https://en.wikipedia.org/wiki/Violin_family>`_
|
||||
— all tuned in perfect fifths, with each member a fifth or octave
|
||||
lower than the last:
|
||||
|
||||
.. code-block:: pycon
|
||||
|
||||
>>> Fretboard.mandolin() # E5 A4 D4 G3 — soprano (= violin)
|
||||
>>> Fretboard.mandola() # A4 D4 G3 C3 — alto (= viola)
|
||||
>>> Fretboard.octave_mandolin() # E4 A3 D3 G2 — tenor (octave below mandolin)
|
||||
>>> Fretboard.mandocello() # A3 D3 G2 C2 — bass (= cello)
|
||||
|
||||
The mandolin's doubled courses (pairs of strings) create a natural
|
||||
chorus effect. The `octave mandolin <https://en.wikipedia.org/wiki/Octave_mandolin>`_
|
||||
is popular in Irish and Celtic folk music.
|
||||
|
||||
The Bowed String Family
|
||||
-----------------------
|
||||
|
||||
The orchestral `string family <https://en.wikipedia.org/wiki/String_section>`_
|
||||
is tuned in perfect fifths (except the double bass, which uses fourths):
|
||||
|
||||
.. code-block:: pycon
|
||||
|
||||
>>> Fretboard.violin() # E5 A4 D4 G3 — soprano
|
||||
>>> Fretboard.viola() # A4 D4 G3 C3 — alto (5th below violin)
|
||||
>>> Fretboard.cello() # A3 D3 G2 C2 — tenor/bass (octave below viola)
|
||||
>>> Fretboard.double_bass() # G2 D2 A1 E1 — bass (tuned in 4ths!)
|
||||
|
||||
Bowed strings have no frets — the player can produce any pitch along
|
||||
the fingerboard, enabling continuous
|
||||
`vibrato <https://en.wikipedia.org/wiki/Vibrato>`_ and microtonal
|
||||
inflections not possible on fretted instruments.
|
||||
|
||||
The `erhu <https://en.wikipedia.org/wiki/Erhu>`_ — a 2-stringed Chinese
|
||||
bowed instrument with a hauntingly vocal quality:
|
||||
|
||||
.. code-block:: pycon
|
||||
|
||||
>>> Fretboard.erhu() # A4 D4 — tuned a 5th apart, no fingerboard
|
||||
|
||||
Plucked Strings
|
||||
---------------
|
||||
|
||||
.. code-block:: pycon
|
||||
|
||||
>>> Fretboard.ukulele() # A4 E4 C4 G4 — re-entrant tuning
|
||||
>>> Fretboard.banjo() # Open G (bluegrass, 5th string is high drone)
|
||||
>>> Fretboard.banjo("open d") # Open D (clawhammer, old-time)
|
||||
>>> Fretboard.harp() # 47 strings, C1 to G7 (concert pedal harp)
|
||||
|
||||
The `banjo <https://en.wikipedia.org/wiki/Banjo>`_'s short 5th string
|
||||
is a high drone — a defining feature of the instrument's sound.
|
||||
|
||||
The `harp <https://en.wikipedia.org/wiki/Harp>`_ has one string per
|
||||
diatonic note across nearly 7 octaves. Pedals alter each note name
|
||||
by up to two semitones across all octaves simultaneously.
|
||||
|
||||
World Instruments
|
||||
-----------------
|
||||
|
||||
.. code-block:: pycon
|
||||
|
||||
>>> # Middle Eastern
|
||||
>>> Fretboard.oud() # C4 G3 D3 A2 G2 C2 — fretless, ancestor of the lute
|
||||
>>> Fretboard.sitar() # 7 main strings — Indian classical
|
||||
|
||||
>>> # East Asian
|
||||
>>> Fretboard.shamisen() # C4 G3 C3 — 3-string Japanese, honchoshi tuning
|
||||
>>> Fretboard.pipa() # D4 A3 E3 A2 — 4-string Chinese lute
|
||||
>>> Fretboard.erhu() # A4 D4 — 2-string Chinese bowed
|
||||
|
||||
>>> # European
|
||||
>>> Fretboard.bouzouki() # D4 A3 D3 G2 — Irish (Celtic music)
|
||||
>>> Fretboard.bouzouki("greek") # D4 A3 F3 C3 — Greek
|
||||
>>> Fretboard.lute() # G4 D4 A3 F3 C3 G2 — Renaissance (6 courses)
|
||||
>>> Fretboard.balalaika() # A4 E4 E4 — Russian (2 unison strings)
|
||||
|
||||
>>> # Latin American
|
||||
>>> Fretboard.charango() # E5 A4 E5 C5 G4 — Andean (re-entrant tuning)
|
||||
|
||||
>>> # Steel guitar
|
||||
>>> Fretboard.pedal_steel() # 10 strings, E9 Nashville — country music
|
||||
|
||||
The `oud <https://en.wikipedia.org/wiki/Oud>`_ is fretless, allowing
|
||||
the quarter-tone inflections essential to
|
||||
`maqam <https://en.wikipedia.org/wiki/Maqam>`_ performance. The
|
||||
`sitar <https://en.wikipedia.org/wiki/Sitar>`_ has moveable frets and
|
||||
sympathetic strings that resonate in harmony with the played notes.
|
||||
|
||||
Keyboards
|
||||
---------
|
||||
|
||||
.. code-block:: pycon
|
||||
|
||||
>>> Fretboard.keyboard() # 88-key piano (A0 to C8)
|
||||
>>> Fretboard.keyboard(61, "C2") # 61-key synth controller
|
||||
>>> Fretboard.keyboard(49, "C2") # 49-key controller
|
||||
>>> Fretboard.keyboard(25, "C3") # 25-key mini MIDI controller
|
||||
|
||||
While keyboards don't have strings or frets, they map naturally to a
|
||||
sequence of tones. A full 88-key piano spans over 7 octaves — the
|
||||
widest range of any standard acoustic instrument.
|
||||
|
||||
Getting Fingerings
|
||||
------------------
|
||||
|
||||
.. code-block:: python
|
||||
The fingering algorithm finds the most playable voicing for any chord
|
||||
on any instrument. It scores each possibility by:
|
||||
|
||||
from pytheory import Fretboard, CHARTS
|
||||
1. Preferring **open strings** (fret 0) — they ring freely
|
||||
2. Preferring **ascending** fret patterns — easier hand position
|
||||
3. Minimizing the number of **fingers needed**
|
||||
|
||||
fb = Fretboard.guitar()
|
||||
.. code-block:: pycon
|
||||
|
||||
# Best fingering for a chord
|
||||
c = CHARTS["western"]["C"]
|
||||
print(c.fingering(fretboard=fb))
|
||||
# (0, 1, 0, 2, 3, 0)
|
||||
>>> from pytheory import Fretboard
|
||||
|
||||
# All possible fingerings
|
||||
all_c = c.fingering(fretboard=fb, multiple=True)
|
||||
>>> fb = Fretboard.guitar()
|
||||
>>> f = fb.chord("C")
|
||||
>>> f
|
||||
Fingering(e=0, B=1, G=0, D=2, A=3, E=x)
|
||||
|
||||
# Muted strings appear as None
|
||||
f = CHARTS["western"]["F"]
|
||||
print(f.fingering(fretboard=fb))
|
||||
>>> f['A']
|
||||
3
|
||||
>>> f[1]
|
||||
1
|
||||
|
||||
>>> f.identify()
|
||||
'C major'
|
||||
|
||||
>>> chord = f.to_chord()
|
||||
>>> chord.identify()
|
||||
'C major'
|
||||
|
||||
You can also go from fret positions to chord identification:
|
||||
|
||||
.. code-block:: pycon
|
||||
|
||||
>>> # "What chord am I playing?"
|
||||
>>> fb = Fretboard.guitar()
|
||||
>>> f = fb.fingering(0, 0, 0, 2, 2, 0)
|
||||
>>> f
|
||||
Fingering(e=0, B=0, G=0, D=2, A=2, E=0)
|
||||
>>> f.identify()
|
||||
'E minor'
|
||||
|
||||
Reading Fingerings
|
||||
~~~~~~~~~~~~~~~~~~
|
||||
|
||||
Each position is labeled with its string name. Duplicate string names
|
||||
are disambiguated — on a standard guitar, high E appears as ``e`` and
|
||||
low E as ``E``::
|
||||
|
||||
e|--0-- (open — E)
|
||||
B|--1-- (fret 1 — C)
|
||||
G|--0-- (open — G)
|
||||
D|--2-- (fret 2 — E)
|
||||
A|--3-- (fret 3 — C)
|
||||
E|--x-- (muted)
|
||||
|
||||
A value of ``x`` (``None``) means the string is muted (not played).
|
||||
|
||||
ASCII Tablature
|
||||
~~~~~~~~~~~~~~~
|
||||
|
||||
For a more visual representation, use ``tab()``:
|
||||
|
||||
.. code-block:: pycon
|
||||
|
||||
>>> print(fb.tab("C"))
|
||||
C major
|
||||
e|--0--
|
||||
B|--1--
|
||||
G|--0--
|
||||
D|--2--
|
||||
A|--3--
|
||||
E|--x--
|
||||
|
||||
Generating Full Charts
|
||||
----------------------
|
||||
|
||||
Generate fingerings for every chord at once:
|
||||
|
||||
.. code-block:: python
|
||||
.. code-block:: pycon
|
||||
|
||||
from pytheory import Fretboard, charts_for_fretboard
|
||||
>>> fb = Fretboard.guitar()
|
||||
>>> chart = fb.chart()
|
||||
|
||||
fb = Fretboard.guitar()
|
||||
chart = charts_for_fretboard(fretboard=fb)
|
||||
>>> chart["C"]
|
||||
Fingering(e=0, B=1, G=0, D=2, A=3, E=x)
|
||||
|
||||
for name, fingering in chart.items():
|
||||
print(f"{name:6s} {fingering}")
|
||||
>>> # Works with any instrument
|
||||
>>> uke_chart = Fretboard.ukulele().chart()
|
||||
>>> mando_chart = Fretboard.mandolin().chart()
|
||||
|
||||
Ukulele Example
|
||||
---------------
|
||||
Scale Diagrams with Chord Highlighting
|
||||
---------------------------------------
|
||||
|
||||
.. code-block:: python
|
||||
The ``scale_diagram()`` method renders an ASCII fretboard showing where
|
||||
scale notes fall on each string. Pass an optional ``chord`` argument to
|
||||
highlight chord tones in UPPERCASE while scale-only tones appear in
|
||||
lowercase — a quick way to visualize target notes for soloing:
|
||||
|
||||
fb = Fretboard.ukulele()
|
||||
c = CHARTS["western"]["C"]
|
||||
print(c.fingering(fretboard=fb)) # 4-string fingering
|
||||
.. code-block:: pycon
|
||||
|
||||
>>> from pytheory import Fretboard, TonedScale, Chord
|
||||
|
||||
>>> fb = Fretboard.guitar()
|
||||
>>> pentatonic = TonedScale(tonic="A4")["minor pentatonic"]
|
||||
>>> print(fb.scale_diagram(pentatonic, frets=5))
|
||||
|
||||
>>> # Highlight Am chord tones within the scale:
|
||||
>>> am = Chord.from_symbol("Am")
|
||||
>>> print(fb.scale_diagram(pentatonic, frets=5, chord=am))
|
||||
|
||||
Non-String Instruments
|
||||
----------------------
|
||||
|
||||
Looking for drums and percussion? PyTheory also supports drum pattern
|
||||
programming through the sequencing engine. See the :doc:`drums` guide
|
||||
for drum kits, patterns, and fills.
|
||||
|
||||
Custom Instruments
|
||||
------------------
|
||||
|
||||
Any instrument can be modeled with custom string tunings:
|
||||
|
||||
.. code-block:: pycon
|
||||
|
||||
>>> from pytheory import Tone, Fretboard
|
||||
|
||||
>>> # Baritone ukulele (DGBE — top 4 guitar strings)
|
||||
>>> bari_uke = Fretboard(tones=[
|
||||
... Tone.from_string("E4"),
|
||||
... Tone.from_string("B3"),
|
||||
... Tone.from_string("G3"),
|
||||
... Tone.from_string("D3"),
|
||||
... ])
|
||||
|
||||
>>> # Tres cubano (Cuban guitar, 3 doubled courses)
|
||||
>>> tres = Fretboard(tones=[
|
||||
... Tone.from_string("E4"),
|
||||
... Tone.from_string("B3"),
|
||||
... Tone.from_string("G3"),
|
||||
... ])
|
||||
|
||||
If it has strings, you can model it. Define the tuning, and PyTheory handles the rest -- fingerings, charts, scale diagrams, all of it. Got a weird instrument or a custom tuning? That's what the ``Fretboard`` constructor is for.
|
||||
|
||||
+193
-33
@@ -1,7 +1,21 @@
|
||||
Audio Playback
|
||||
==============
|
||||
Playback and Export
|
||||
===================
|
||||
|
||||
PyTheory can synthesize and play tones and chords through your speakers.
|
||||
This is the output layer. You've built your theory, composed your
|
||||
arrangement, shaped your sounds -- now you need to hear it. PyTheory
|
||||
gives you three ways to get your music out: speakers, WAV files, and
|
||||
MIDI files.
|
||||
|
||||
Use **speakers** for immediate feedback while you're sketching and
|
||||
experimenting. Use **WAV export** when you want to share actual audio
|
||||
-- post it, send it, drop it into a video. Use **MIDI export** when you
|
||||
want to bring your sketch into a real DAW and finish it with
|
||||
professional instruments, mixing, and mastering. Each output serves a
|
||||
different stage of the creative process.
|
||||
|
||||
PyTheory can play audio through your speakers, save to WAV, or export
|
||||
to MIDI. Everything is synthesized from waveforms -- no samples or
|
||||
external audio files needed.
|
||||
|
||||
.. note::
|
||||
|
||||
@@ -9,52 +23,198 @@ PyTheory can synthesize and play tones and chords through your speakers.
|
||||
installed on your system. On macOS: ``brew install portaudio``.
|
||||
On Ubuntu: ``apt install libportaudio2``.
|
||||
|
||||
Playing a Tone
|
||||
--------------
|
||||
play() -- Single Tones and Chords
|
||||
---------------------------------
|
||||
|
||||
The simplest way to hear something:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
from pytheory import Tone, play
|
||||
from pytheory import Tone, Chord, play
|
||||
|
||||
a4 = Tone.from_string("A4", system="western")
|
||||
play(a4, t=1_000) # Play A440 for 1 second
|
||||
play(Tone.from_string("A4"), t=1_000) # A440 for 1 second
|
||||
play(Chord.from_symbol("Am7"), t=2_000) # chord for 2 seconds
|
||||
|
||||
Playing a Chord
|
||||
---------------
|
||||
Optional parameters for synth, envelope, and temperament:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
from pytheory import Chord, Tone, play
|
||||
from pytheory import Synth, Envelope
|
||||
|
||||
c_major = Chord(tones=[
|
||||
Tone.from_string("C4", system="western"),
|
||||
Tone.from_string("E4", system="western"),
|
||||
Tone.from_string("G4", system="western"),
|
||||
])
|
||||
play(c_major, t=2_000) # Play for 2 seconds
|
||||
play(Tone.from_string("C4"), synth=Synth.SAW, envelope=Envelope.PLUCK, t=1_000)
|
||||
play(Tone.from_string("C4"), temperament="pythagorean", t=1_000)
|
||||
|
||||
Waveform Types
|
||||
--------------
|
||||
play_score() -- Full Arrangements
|
||||
---------------------------------
|
||||
|
||||
Choose between sine, sawtooth, and triangle wave synthesis:
|
||||
Plays a ``Score`` with all its parts and drums mixed together.
|
||||
Output is **stereo** — each part is panned according to its ``pan``
|
||||
setting, drums are stereo-panned like a real kit, and reverb tails
|
||||
have natural stereo width. A **master bus compressor/limiter** (4:1
|
||||
ratio, brick-wall at 0.95) is applied to prevent clipping and make
|
||||
the mix louder and punchier:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
from pytheory import play, Synth, Tone
|
||||
from pytheory import Score, Duration, Chord
|
||||
from pytheory.play import play_score
|
||||
|
||||
tone = Tone.from_string("C4", system="western")
|
||||
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)
|
||||
play_score(score)
|
||||
|
||||
play(tone, synth=Synth.SINE) # Smooth, pure tone
|
||||
play(tone, synth=Synth.SAW) # Bright, buzzy
|
||||
play(tone, synth=Synth.TRIANGLE) # Softer than sawtooth
|
||||
|
||||
Temperaments
|
||||
------------
|
||||
|
||||
Play in different tuning systems:
|
||||
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
|
||||
|
||||
play(tone, temperament="equal") # Default, modern tuning
|
||||
play(tone, temperament="pythagorean") # Ancient Greek tuning
|
||||
play(tone, temperament="meantone") # Renaissance tuning
|
||||
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
|
||||
------------------------------------
|
||||
|
||||
Returns a raw audio buffer (numpy float32 array) without playing it.
|
||||
Useful for saving to WAV or further processing:
|
||||
|
||||
.. code-block:: pycon
|
||||
|
||||
>>> from pytheory.play import render_score
|
||||
>>> buf = render_score(score) # numpy float32 array
|
||||
>>> len(buf)
|
||||
604800
|
||||
|
||||
save() -- WAV Export
|
||||
--------------------
|
||||
|
||||
Render tones or chords to a WAV file. Works without speakers or
|
||||
PortAudio:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
from pytheory import save, Chord, Tone, Synth
|
||||
|
||||
save(Tone.from_string("A4"), "a440.wav", t=1_000)
|
||||
save(Chord.from_name("Am7"), "am7.wav", t=2_000)
|
||||
save(
|
||||
Chord.from_name("C"),
|
||||
"c_triangle.wav",
|
||||
synth=Synth.TRIANGLE,
|
||||
temperament="meantone",
|
||||
t=3_000,
|
||||
)
|
||||
|
||||
save_midi() -- MIDI Export
|
||||
--------------------------
|
||||
|
||||
MIDI export is probably the most useful feature here for working
|
||||
musicians. The idea is simple: sketch your ideas in Python -- where
|
||||
iteration is fast, where you can use loops and randomness and music
|
||||
theory functions -- and then export to MIDI. Open that MIDI file in
|
||||
Logic, Ableton, Reaper, FL Studio, or whatever you use, and now you've
|
||||
got your chord progressions, melodies, and bass lines on real tracks.
|
||||
Swap in your favorite soft synths, add real mixing, finish the track
|
||||
properly. Python is the sketchpad; the DAW is the canvas.
|
||||
|
||||
Export tones, chords, progressions, or full scores as Standard MIDI
|
||||
Files. MIDI files can be opened in any DAW, edited, transposed, and
|
||||
assigned to any instrument.
|
||||
|
||||
Simple export (single tone, chord, or progression):
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
from pytheory import save_midi, Key, Tone, Chord
|
||||
|
||||
save_midi(Tone.from_string("C4"), "middle_c.mid", t=1000)
|
||||
save_midi(Chord.from_symbol("Am7"), "am7.mid")
|
||||
|
||||
chords = Key("C", "major").progression("I", "V", "vi", "IV")
|
||||
save_midi(chords, "pop.mid", t=500, bpm=120)
|
||||
|
||||
Score-based export (with time signature, tempo, and parts):
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
from pytheory import Score, Duration, Key
|
||||
|
||||
score = Score("4/4", bpm=140)
|
||||
for chord in Key("G", "major").progression("I", "IV", "V", "I"):
|
||||
score.add(chord, Duration.WHOLE)
|
||||
score.save_midi("progression.mid")
|
||||
|
||||
play_pattern() -- Drum Patterns
|
||||
-------------------------------
|
||||
|
||||
Play a drum pattern through the speakers:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
from pytheory import Pattern
|
||||
from pytheory.play import play_pattern
|
||||
|
||||
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 80+ presets and 21 fills.
|
||||
|
||||
play_progression() -- Quick Chord Playback
|
||||
------------------------------------------
|
||||
|
||||
Play a chord progression in sequence with a single call:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
from pytheory import Key, play_progression
|
||||
|
||||
chords = Key("C", "major").progression("I", "V", "vi", "IV")
|
||||
play_progression(chords, t=800)
|
||||
|
||||
Optional synth, envelope, and gap parameters:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
from pytheory import Synth, Envelope
|
||||
|
||||
play_progression(chords, t=1000, synth=Synth.TRIANGLE, gap=200)
|
||||
play_progression(chords, t=2000, envelope=Envelope.PAD)
|
||||
|
||||
That's the workflow: hear it, tweak it, hear it again. When it sounds right, export to WAV or MIDI and take it somewhere bigger.
|
||||
|
||||
MIDI Import
|
||||
-----------
|
||||
|
||||
Load any Standard MIDI File into a Score — then play it through
|
||||
PyTheory's synth engine with effects, or analyze the theory:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
from pytheory import Score
|
||||
from pytheory.play import play_score
|
||||
|
||||
score = Score.from_midi("song.mid")
|
||||
|
||||
# See what's inside
|
||||
for name, part in score.parts.items():
|
||||
print(f"{name}: {len(part.notes)} notes")
|
||||
|
||||
# Change the synth and add effects
|
||||
score.parts["ch1"].synth = "saw"
|
||||
score.parts["ch1"].reverb_mix = 0.3
|
||||
|
||||
play_score(score)
|
||||
|
||||
Each MIDI channel becomes a named Part (``ch1``, ``ch2``, etc.).
|
||||
Channel 10 (drums) becomes drum hits. Tempo, time signature,
|
||||
note durations, and velocities are all preserved.
|
||||
|
||||
Download any MIDI file from the internet, load it, play it through
|
||||
the synth engine with reverb and delay. That's the whole idea.
|
||||
|
||||
+230
-39
@@ -1,62 +1,253 @@
|
||||
Quickstart
|
||||
==========
|
||||
|
||||
PyTheory works at two levels — pick the one that fits what you need:
|
||||
|
||||
1. **Music theory** — explore scales, chords, keys, intervals, and
|
||||
harmony. No audio required. Works anywhere Python runs.
|
||||
|
||||
2. **Composition** — build multi-part arrangements with drums, synths,
|
||||
effects, and export to MIDI. Needs PortAudio for live playback.
|
||||
|
||||
Both are first-class. You can use PyTheory purely as a theory
|
||||
reference and never touch the audio side, or you can jump straight
|
||||
into composing. This guide covers both paths.
|
||||
|
||||
Installation
|
||||
------------
|
||||
|
||||
.. code-block:: bash
|
||||
::
|
||||
|
||||
pip install pytheory
|
||||
$ pip install pytheory
|
||||
|
||||
Or with `uv <https://github.com/astral-sh/uv>`_:
|
||||
For audio playback through your speakers, you'll also need
|
||||
`PortAudio <http://www.portaudio.com/>`_:
|
||||
|
||||
.. code-block:: bash
|
||||
- macOS: ``brew install portaudio``
|
||||
- Ubuntu: ``apt install libportaudio2``
|
||||
- Windows: included with the ``sounddevice`` package
|
||||
|
||||
uv add pytheory
|
||||
PortAudio is only needed for live playback. MIDI export, WAV export,
|
||||
and all theory functions work without it.
|
||||
|
||||
Basic Usage
|
||||
-----------
|
||||
Hear Something Immediately
|
||||
--------------------------
|
||||
|
||||
Create tones, build scales, and explore music theory:
|
||||
::
|
||||
|
||||
$ pytheory demo
|
||||
|
||||
This generates and plays a random track — different every time. It's
|
||||
the fastest way to hear what PyTheory can do.
|
||||
|
||||
Explore Music Theory
|
||||
--------------------
|
||||
|
||||
The theory layer is where most people start. No audio setup needed —
|
||||
this works everywhere Python runs. Every concept in Western music
|
||||
theory (and five other systems) has a clean Python API.
|
||||
|
||||
Tones and intervals:
|
||||
|
||||
.. code-block:: pycon
|
||||
|
||||
>>> from pytheory import Tone
|
||||
|
||||
>>> c4 = Tone.from_string("C4", system="western")
|
||||
>>> c4.frequency
|
||||
261.6255653005986
|
||||
>>> c4.midi
|
||||
60
|
||||
|
||||
>>> c4 + 7
|
||||
<Tone G4>
|
||||
>>> c4.interval_to(c4 + 7)
|
||||
'perfect 5th'
|
||||
|
||||
>>> Tone.from_frequency(440)
|
||||
<Tone A4>
|
||||
>>> Tone.from_midi(69)
|
||||
<Tone A4>
|
||||
|
||||
Keys, scales, and chords:
|
||||
|
||||
.. code-block:: pycon
|
||||
|
||||
>>> from pytheory import Key, Chord
|
||||
|
||||
>>> key = Key("C", "major")
|
||||
>>> key.chords
|
||||
['C major', 'D minor', 'E minor', 'F major', 'G major', 'A minor', 'B diminished']
|
||||
|
||||
>>> [c.symbol for c in key.progression("I", "V", "vi", "IV")]
|
||||
['C', 'G', 'Am', 'F']
|
||||
|
||||
>>> key.signature
|
||||
{'sharps': 0, 'flats': 0, 'accidentals': []}
|
||||
|
||||
>>> Key("F", "major").signature
|
||||
{'sharps': 0, 'flats': 1, 'accidentals': ['Bb']}
|
||||
|
||||
>>> Chord.from_symbol("Am7").identify()
|
||||
'A minor 7th'
|
||||
|
||||
>>> Chord.from_tones("G", "B", "D", "F").analyze("C")
|
||||
'V7'
|
||||
|
||||
Harmonic analysis and modulation:
|
||||
|
||||
.. code-block:: pycon
|
||||
|
||||
>>> Key("C", "major").pivot_chords(Key("G", "major"))
|
||||
['A minor', 'B minor', 'C major', 'D major', 'E minor', 'G major']
|
||||
|
||||
>>> Key("C", "major").relative
|
||||
<Key A minor>
|
||||
|
||||
>>> key.suggest_next(key.triad(4)) # what follows V?
|
||||
[<Chord C major>, <Chord A minor>, <Chord F major>]
|
||||
|
||||
Scales across 6 musical systems:
|
||||
|
||||
.. code-block:: pycon
|
||||
|
||||
>>> from pytheory import TonedScale
|
||||
|
||||
>>> TonedScale(tonic="Sa4", system="indian")["bhairav"].note_names
|
||||
['Sa', 'komal Re', 'Ga', 'Ma', 'Pa', 'komal Dha', 'Ni', 'Sa']
|
||||
|
||||
>>> TonedScale(tonic="Do4", system="arabic")["hijaz"].note_names
|
||||
['Do', 'Reb', 'Mi', 'Fa', 'Sol', 'Solb', 'Sib', 'Do']
|
||||
|
||||
>>> TonedScale(tonic="C4", system="japanese")["hirajoshi"].note_names
|
||||
['C', 'D', 'D#', 'G', 'G#', 'C']
|
||||
|
||||
Guitar fingerings:
|
||||
|
||||
.. code-block:: pycon
|
||||
|
||||
>>> from pytheory import Fretboard
|
||||
|
||||
>>> fb = Fretboard.guitar()
|
||||
>>> fb.chord("Am")
|
||||
Fingering(e=0, B=1, G=2, D=2, A=0, E=x)
|
||||
|
||||
All of the above works without PortAudio, without sounddevice,
|
||||
without any audio setup at all. It's pure Python music theory.
|
||||
|
||||
Compose a Track
|
||||
---------------
|
||||
|
||||
This is where it gets fun. A ``Score`` is your arrangement — drums,
|
||||
chords, melody, bass, each with their own synth and effects:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
from pytheory import Tone, TonedScale, Fretboard, CHARTS
|
||||
from pytheory import Score, Pattern, Key, Duration, Chord
|
||||
from pytheory.play import play_score
|
||||
|
||||
# Create a tone
|
||||
c4 = Tone.from_string("C4")
|
||||
print(c4) # C4
|
||||
print(c4.frequency) # 261.63 Hz
|
||||
score = Score("4/4", bpm=140)
|
||||
score.drums("bossa nova", repeats=4)
|
||||
|
||||
# Tone arithmetic
|
||||
e4 = c4 + 4 # Major third up
|
||||
g4 = c4 + 7 # Perfect fifth up
|
||||
print(e4, g4) # E4 G4
|
||||
chords = score.part(
|
||||
"chords",
|
||||
synth="fm",
|
||||
envelope="pad",
|
||||
reverb=0.4,
|
||||
)
|
||||
lead = score.part(
|
||||
"lead",
|
||||
synth="saw",
|
||||
envelope="pluck",
|
||||
delay=0.3,
|
||||
lowpass=3000,
|
||||
humanize=0.2,
|
||||
)
|
||||
bass = score.part(
|
||||
"bass",
|
||||
synth="sine",
|
||||
lowpass=500,
|
||||
)
|
||||
|
||||
# Measure intervals
|
||||
print(g4 - c4) # 7 (semitones)
|
||||
key = Key("A", "minor")
|
||||
for chord in key.progression("i", "iv", "V", "i"):
|
||||
chords.add(chord, Duration.WHOLE)
|
||||
chords.add(chord, Duration.WHOLE)
|
||||
|
||||
# Build a scale
|
||||
c_major = TonedScale(tonic="C4")["major"]
|
||||
print(c_major.note_names)
|
||||
# ['C', 'D', 'E', 'F', 'G', 'A', 'B', 'C']
|
||||
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)
|
||||
|
||||
# Build chords from the scale
|
||||
I = c_major.triad(0) # C major
|
||||
IV = c_major.triad(3) # F major
|
||||
V = c_major.triad(4) # G major
|
||||
for n in ["A2", "E2", "A2", "C3"] * 4:
|
||||
bass.add(n, Duration.QUARTER)
|
||||
|
||||
# Guitar chord fingerings
|
||||
fb = Fretboard.guitar()
|
||||
fingering = CHARTS["western"]["Am"].fingering(fretboard=fb)
|
||||
print(fingering) # (0, 1, 2, 2, 0, 0)
|
||||
play_score(score)
|
||||
|
||||
What's Included
|
||||
---------------
|
||||
Export to Your DAW
|
||||
------------------
|
||||
|
||||
- **12-tone Western system** with all chromatic notes
|
||||
- **Scales**: major, minor, harmonic minor, and all 7 modes
|
||||
- **Pitch calculation** in equal, Pythagorean, and meantone temperaments
|
||||
- **Chord charts** with 144 pre-built chords (12 roots x 12 qualities)
|
||||
- **Fingering generation** for any fretted instrument
|
||||
- **Audio playback** with sine, sawtooth, and triangle wave synthesis
|
||||
The whole point: sketch in Python, finish in Logic / Ableton / Reaper.
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
score.save_midi("my_sketch.mid")
|
||||
|
||||
Open that file in any DAW and you'll see all the notes laid out on
|
||||
the timeline, ready to assign to real instruments and mix.
|
||||
|
||||
You can also save rendered audio:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
from pytheory import save
|
||||
save(Chord.from_symbol("Am7"), "am7.wav", t=2_000)
|
||||
|
||||
What's in the Box
|
||||
-----------------
|
||||
|
||||
**Theory** — tones, scales (40+ across 6 musical systems), chords
|
||||
(17 types, Roman numeral analysis, figured bass, tension scoring,
|
||||
voice leading, pitch class sets with Forte numbers), keys (detection,
|
||||
signatures, modulation paths, borrowed chords), scale recommendation.
|
||||
|
||||
**Sequencing** — Score, Part, Duration, TimeSignature. Arpeggiator
|
||||
with 5 patterns. Legato with pitch glide. Per-note velocity. Swing.
|
||||
Tempo changes. Fade in/out. Song sections with repeat. Humanize.
|
||||
|
||||
**Synthesis** — 10 waveforms: sine, saw, triangle, square, pulse, FM,
|
||||
noise, supersaw, PWM slow, PWM fast. 8 ADSR envelopes. Detune.
|
||||
Stereo pan and spread.
|
||||
|
||||
**Effects** — distortion, chorus, lowpass filter (with resonance),
|
||||
delay, reverb (algorithmic + 7 stereo convolution presets including
|
||||
Taj Mahal with 12-second tail). All per-part with automation and
|
||||
LFO modulation. Sidechain compression. Master bus compressor/limiter.
|
||||
|
||||
**Drums** — 58 pattern presets (rock, jazz, salsa, bossa nova,
|
||||
afrobeat, house, trap, and 50+ more). 21 fill presets. 27 synthesized
|
||||
drum voices with stereo panning.
|
||||
|
||||
**Instruments** — 25 presets (guitar with 8 tunings, bass, ukulele,
|
||||
mandolin family, violin family, banjo, harp, oud, sitar, erhu, and
|
||||
more) with chord fingering generation and scale diagrams.
|
||||
|
||||
**Output** — stereo playback, WAV export, MIDI import/export.
|
||||
|
||||
**Interface** — REPL with tab completion (``pytheory repl``), CLI with
|
||||
15 commands. ``pytheory demo``, ``pytheory key``, ``pytheory chord``,
|
||||
``pytheory identify``, ``pytheory midi``, ``pytheory play``, and more.
|
||||
|
||||
Where to Go Next
|
||||
-----------------
|
||||
|
||||
- :doc:`theory` — music theory fundamentals
|
||||
- :doc:`tones` — working with individual notes
|
||||
- :doc:`scales` — scales, modes, and keys
|
||||
- :doc:`chords` — chord construction, analysis, and progressions
|
||||
- :doc:`sequencing` — composing multi-part arrangements
|
||||
- :doc:`synths` — the 10 waveforms and 8 envelopes
|
||||
- :doc:`effects` — reverb, delay, distortion, chorus, lowpass, automation
|
||||
- :doc:`drums` — 58 patterns, 21 fills, drum synthesis
|
||||
- :doc:`playback` — play, save, export
|
||||
|
||||
@@ -0,0 +1,276 @@
|
||||
Interactive REPL
|
||||
================
|
||||
|
||||
PyTheory includes an interactive scratchpad for exploring music theory,
|
||||
hearing ideas instantly, and building arrangements — all without writing
|
||||
a Python script.
|
||||
|
||||
::
|
||||
|
||||
$ pytheory repl
|
||||
|
||||
The REPL is two things at once: a **theory calculator** (what chords
|
||||
are in this key? what's the interval between these notes?) and a
|
||||
**composition sketchpad** (add drums, layer parts, tweak effects, hear
|
||||
it, export MIDI). Use whichever side you need.
|
||||
|
||||
Getting Started
|
||||
---------------
|
||||
|
||||
The welcome screen tells you everything you need::
|
||||
|
||||
♫ PyTheory REPL
|
||||
════════════════════════════════════════
|
||||
|
||||
try: key Am — set a key
|
||||
chords — see its chords
|
||||
prog I V vi IV — hear a progression
|
||||
drums bossa nova
|
||||
play_score — hear it all
|
||||
|
||||
help for all commands, quit to exit
|
||||
|
||||
Type those five things in order and you'll have music playing in
|
||||
30 seconds.
|
||||
|
||||
The Prompt
|
||||
----------
|
||||
|
||||
The prompt shows your current state — key, tempo, drums, active part,
|
||||
and effects. It starts compact and grows as you add context::
|
||||
|
||||
pytheory[key=C | bpm=120]>
|
||||
|
||||
pytheory[key=Am | bpm=140]>
|
||||
|
||||
pytheory[key=Am | bpm=140 | drums=bossa nova]>
|
||||
|
||||
pytheory[key=Am | bpm=140 | drums=bossa nova | →lead(saw)]>
|
||||
|
||||
When it gets long, it stacks into two lines::
|
||||
|
||||
key=Am | bpm=140 | drums=bossa nova | →lead(saw) rev=0.3 lp=2000
|
||||
♫>
|
||||
|
||||
You always know where you are.
|
||||
|
||||
Theory Commands
|
||||
---------------
|
||||
|
||||
These work without any audio setup. Pure theory exploration.
|
||||
|
||||
Set a key and explore it::
|
||||
|
||||
pytheory> key Am
|
||||
A minor: A B C D E F G A
|
||||
|
||||
pytheory> chords
|
||||
i A minor
|
||||
ii° B diminished
|
||||
III C major
|
||||
iv D minor
|
||||
v E minor
|
||||
VI F major
|
||||
VII G major
|
||||
|
||||
pytheory> modes
|
||||
ionian A B C# D E F# G# A
|
||||
dorian A B C D E F# G A
|
||||
phrygian A Bb C D E F G A
|
||||
...
|
||||
|
||||
pytheory> scales
|
||||
major A B C# D E F# G# A
|
||||
minor A B C D E F G A
|
||||
harmonic minor A B C D E F G# A
|
||||
...
|
||||
|
||||
Build progressions::
|
||||
|
||||
pytheory> prog I V vi IV
|
||||
Am → Em → F → Dm
|
||||
|
||||
pytheory> progression i iv V i
|
||||
Am → Dm → E → Am
|
||||
|
||||
Explore intervals and chords::
|
||||
|
||||
pytheory> interval C4 G4
|
||||
C4 → G4: perfect 5th
|
||||
7 semitones
|
||||
|
||||
pytheory> identify C E G
|
||||
C major
|
||||
symbol: C
|
||||
|
||||
pytheory> identify F#m7b5
|
||||
F# half-diminished 7th
|
||||
symbol: F#m7b5
|
||||
tones: F#4 A4 C5 E5
|
||||
intervals: [3, 3, 4]
|
||||
|
||||
Circle of fifths::
|
||||
|
||||
pytheory> circle
|
||||
fifths: A → E → B → F# → C# → G# → D# → A# → F → C → G → D
|
||||
fourths: A → D → G → C → F → A# → D# → G# → C# → F# → B → E
|
||||
|
||||
Other musical systems::
|
||||
|
||||
pytheory> system indian
|
||||
system: indian
|
||||
scales: chromatic, bilawal, khamaj, kafi, ...
|
||||
|
||||
pytheory> system arabic
|
||||
system: arabic
|
||||
scales: chromatic, ajam, nahawand, kurd, hijaz, ...
|
||||
|
||||
Guitar::
|
||||
|
||||
pytheory> fingering Am
|
||||
Am
|
||||
E|--0--
|
||||
B|--1--
|
||||
G|--2--
|
||||
D|--2--
|
||||
A|--0--
|
||||
E|--x--
|
||||
|
||||
pytheory> diagram minor 5
|
||||
0 1 2 3 4 5
|
||||
E| E | F | - | G | - | A |
|
||||
...
|
||||
|
||||
Composition Commands
|
||||
--------------------
|
||||
|
||||
When you're ready to make sound, add drums and parts.
|
||||
|
||||
Drums::
|
||||
|
||||
pytheory> drums bossa nova
|
||||
score.drums("bossa nova", repeats=4)
|
||||
|
||||
pytheory> drums
|
||||
(lists all 58 presets)
|
||||
|
||||
Parts — each with its own synth and envelope::
|
||||
|
||||
pytheory> part lead saw pluck
|
||||
score.part("lead", synth="saw", envelope="pluck")
|
||||
|
||||
pytheory> part chords fm pad
|
||||
score.part("chords", synth="fm", envelope="pad")
|
||||
|
||||
pytheory> part bass sine pluck
|
||||
score.part("bass", synth="sine", envelope="pluck")
|
||||
|
||||
pytheory> part
|
||||
lead: synth=saw envelope=pluck vol=0.5 ←
|
||||
chords: synth=fm envelope=pad vol=0.5
|
||||
bass: synth=sine envelope=pluck vol=0.5
|
||||
|
||||
The arrow (``←``) shows which part is active. Switch with
|
||||
``part <name>``.
|
||||
|
||||
Add notes, chords, arpeggios::
|
||||
|
||||
pytheory> add C5 1
|
||||
.add("C5", 1.0)
|
||||
|
||||
pytheory> add Am 4
|
||||
.add(Chord.from_symbol("Am"), 4.0)
|
||||
|
||||
pytheory> add E5 0.67 110
|
||||
.add("E5", 0.67, velocity=110)
|
||||
|
||||
pytheory> rest 2
|
||||
.rest(2.0)
|
||||
|
||||
pytheory> arp Am updown 2 2
|
||||
.arpeggio("Am", pattern="updown", bars=2.0, octaves=2)
|
||||
|
||||
pytheory> prog i iv V i
|
||||
Am → Dm → E → Am
|
||||
|
||||
Effects
|
||||
-------
|
||||
|
||||
Set effects on the active part — mirrors the Python API::
|
||||
|
||||
pytheory> reverb 0.4
|
||||
pytheory> delay 0.3 0.375
|
||||
pytheory> lowpass 2000 3
|
||||
pytheory> dist 0.5
|
||||
pytheory> chorus 0.3
|
||||
pytheory> sidechain 0.8
|
||||
pytheory> humanize 0.3
|
||||
pytheory> legato on
|
||||
pytheory> glide 0.04
|
||||
pytheory> volume 0.4
|
||||
|
||||
Automation — change effects mid-song::
|
||||
|
||||
pytheory> set lowpass 3000
|
||||
.set(lowpass=3000)
|
||||
|
||||
LFO modulation::
|
||||
|
||||
pytheory> lfo lowpass 0.5 400 3000 8 sine
|
||||
.lfo("lowpass", rate=0.5, min=400, max=3000, bars=8, shape="sine")
|
||||
|
||||
Playback and Export
|
||||
-------------------
|
||||
|
||||
Hear your work::
|
||||
|
||||
pytheory> play_score
|
||||
♫ play_score()
|
||||
|
||||
pytheory> play_pattern
|
||||
♫ play_pattern("bossa nova")
|
||||
|
||||
Export::
|
||||
|
||||
pytheory> save_midi sketch.mid
|
||||
save_midi("sketch.mid")
|
||||
|
||||
pytheory> render sketch.wav
|
||||
saved: sketch.wav
|
||||
|
||||
Session management::
|
||||
|
||||
pytheory> show
|
||||
<Score 4/4 140bpm 3 parts 8.0 measures>
|
||||
lead: saw+pluck 32 notes reverb=0.3 delay=0.25 ←
|
||||
chords: fm+pad 8 notes
|
||||
drums: bossa nova (76 hits)
|
||||
|
||||
pytheory> status
|
||||
key=A minor bpm=140 swing=0.0
|
||||
drums=bossa nova parts=[lead, chords, bass] active=lead
|
||||
|
||||
pytheory> clear
|
||||
cleared (C major, 120 bpm)
|
||||
|
||||
Complete Example
|
||||
----------------
|
||||
|
||||
A full session from start to playable track::
|
||||
|
||||
pytheory[key=C | bpm=120]> key Am
|
||||
pytheory[key=Am | bpm=120]> bpm 140
|
||||
pytheory[key=Am | bpm=140]> drums bossa nova
|
||||
pytheory[key=Am | bpm=140 | drums=bossa nova]> part chords fm pad
|
||||
pytheory[...| →chords(fm)]> prog i iv V i
|
||||
pytheory[...| →chords(fm)]> part lead saw pluck
|
||||
pytheory[...| →lead(saw)]> reverb 0.3
|
||||
pytheory[...| →lead(saw) rev=0.3]> delay 0.25
|
||||
pytheory[...| →lead(saw) rev=0.3 del=0.25]> arp Am updown 4 2
|
||||
pytheory[...]> play_score
|
||||
♫ play_score()
|
||||
pytheory[...]> save_midi my_bossa.mid
|
||||
save_midi("my_bossa.mid")
|
||||
|
||||
Every command you typed maps 1:1 to the Python API. When you're
|
||||
ready to move from the REPL to a script, the translation is direct.
|
||||
+468
-66
@@ -1,106 +1,508 @@
|
||||
Working with Scales
|
||||
===================
|
||||
|
||||
Scales are sequences of tones following a specific interval pattern.
|
||||
A **scale** is an ordered set of tones spanning an octave, defined by a
|
||||
pattern of intervals. Scales are the foundation of melody and harmony —
|
||||
they determine which notes "belong" in a piece of music and shape its
|
||||
emotional character.
|
||||
|
||||
Scale Construction
|
||||
------------------
|
||||
|
||||
Every scale is defined by its **interval pattern** — the sequence of
|
||||
whole steps (W = 2 semitones) and half steps (H = 1 semitone) between
|
||||
consecutive tones.
|
||||
|
||||
The `major scale <https://en.wikipedia.org/wiki/Major_scale>`_::
|
||||
|
||||
W W H W W W H
|
||||
C D E F G A B C
|
||||
2 2 1 2 2 2 1 ← semitones between each note
|
||||
|
||||
The `natural minor scale <https://en.wikipedia.org/wiki/Minor_scale>`_::
|
||||
|
||||
W H W W H W W
|
||||
C D Eb F G Ab Bb C
|
||||
2 1 2 2 1 2 2
|
||||
|
||||
Building Scales
|
||||
---------------
|
||||
|
||||
Use :class:`~pytheory.scales.TonedScale` to generate scales in any key:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
from pytheory import TonedScale
|
||||
|
||||
c = TonedScale(tonic="C4")
|
||||
|
||||
# Access scales by name
|
||||
major = c["major"]
|
||||
minor = c["minor"]
|
||||
harmonic_minor = c["harmonic minor"]
|
||||
|
||||
print(major.note_names)
|
||||
# ['C', 'D', 'E', 'F', 'G', 'A', 'B', 'C']
|
||||
|
||||
Available Scales
|
||||
----------------
|
||||
|
||||
.. code-block:: python
|
||||
.. code-block:: pycon
|
||||
|
||||
>>> from pytheory import TonedScale
|
||||
>>> c = TonedScale(tonic="C4")
|
||||
>>> c.scales
|
||||
('chromatic', 'major', 'minor', 'harmonic minor',
|
||||
'ionian', 'dorian', 'phrygian', 'lydian',
|
||||
'mixolydian', 'aeolian', 'locrian')
|
||||
>>> major = c["major"]
|
||||
>>> minor = c["minor"]
|
||||
>>> harmonic_minor = c["harmonic minor"]
|
||||
>>> major.note_names
|
||||
['C', 'D', 'E', 'F', 'G', 'A', 'B', 'C']
|
||||
|
||||
Major and Minor
|
||||
---------------
|
||||
|
||||
The **major scale** (`Ionian <https://en.wikipedia.org/wiki/Ionian_mode>`_ mode) is the foundation of Western tonal
|
||||
music. Its pattern of whole and half steps creates a bright, resolved
|
||||
sound. Every major key has a `relative minor <https://en.wikipedia.org/wiki/Relative_key>`_ that shares the same
|
||||
notes but starts from the 6th degree:
|
||||
|
||||
- C major → A minor (both use only white keys)
|
||||
- G major → E minor (both have one sharp: F#)
|
||||
- F major → D minor (both have one flat: Bb)
|
||||
|
||||
.. code-block:: pycon
|
||||
|
||||
>>> c_major = TonedScale(tonic="C4")["major"]
|
||||
>>> a_minor = TonedScale(tonic="A4")["minor"]
|
||||
>>> set(c_major.note_names) == set(a_minor.note_names)
|
||||
True
|
||||
|
||||
The `harmonic minor <https://en.wikipedia.org/wiki/Harmonic_minor_scale>`_ raises the 7th degree of the natural minor,
|
||||
creating an augmented 2nd interval (3 semitones) between the 6th and
|
||||
7th degrees. This gives it a distinctive "Middle Eastern" or "classical"
|
||||
sound and provides the leading tone needed for dominant harmony::
|
||||
|
||||
Natural minor: C D Eb F G Ab Bb C
|
||||
Harmonic minor: C D Eb F G Ab B C
|
||||
↑ raised 7th
|
||||
|
||||
Modes
|
||||
-----
|
||||
|
||||
All seven modes of the major scale are supported:
|
||||
The seven `modes <https://en.wikipedia.org/wiki/Mode_(music)>`_ of the major scale are rotations of the same interval
|
||||
pattern, each starting from a different degree. Each mode has a distinct
|
||||
emotional character:
|
||||
|
||||
.. code-block:: python
|
||||
.. code-block:: pycon
|
||||
|
||||
c = TonedScale(tonic="C4")
|
||||
>>> c = TonedScale(tonic="C4")
|
||||
|
||||
c["ionian"] # Same as major: C D E F G A B C
|
||||
c["dorian"] # C D Eb F G A Bb C
|
||||
c["phrygian"] # C Db Eb F G Ab Bb C
|
||||
c["lydian"] # C D E F# G A B C
|
||||
c["mixolydian"] # C D E F G A Bb C
|
||||
c["aeolian"] # Same as minor: C D Eb F G Ab Bb C
|
||||
c["locrian"] # C Db Eb F Gb Ab Bb C
|
||||
**Ionian** (I) — the major scale itself. Bright, happy, resolved:
|
||||
|
||||
Accessing Degrees
|
||||
-----------------
|
||||
.. code-block:: pycon
|
||||
|
||||
Scale tones can be accessed by index, Roman numeral, or degree name:
|
||||
>>> c["ionian"].note_names
|
||||
['C', 'D', 'E', 'F', 'G', 'A', 'B', 'C']
|
||||
|
||||
.. code-block:: python
|
||||
`Dorian <https://en.wikipedia.org/wiki/Dorian_mode>`_ (ii) — minor with a raised 6th. Jazzy, soulful (So What,
|
||||
Scarborough Fair):
|
||||
|
||||
major = TonedScale(tonic="C4")["major"]
|
||||
.. code-block:: pycon
|
||||
|
||||
# By index
|
||||
major[0] # C4
|
||||
major[4] # G4
|
||||
>>> c["dorian"].note_names
|
||||
['C', 'D', 'Eb', 'F', 'G', 'A', 'Bb', 'C']
|
||||
|
||||
# By Roman numeral
|
||||
major["I"] # C4
|
||||
major["V"] # G4
|
||||
`Phrygian <https://en.wikipedia.org/wiki/Phrygian_mode>`_ (iii) — minor with a flat 2nd. Spanish, flamenco, dark
|
||||
(White Rabbit):
|
||||
|
||||
# By degree name
|
||||
major["tonic"] # C4
|
||||
major["dominant"] # G4
|
||||
.. code-block:: pycon
|
||||
|
||||
# Slicing
|
||||
major[0:3] # (C4, D4, E4)
|
||||
>>> c["phrygian"].note_names
|
||||
['C', 'Db', 'Eb', 'F', 'G', 'Ab', 'Bb', 'C']
|
||||
|
||||
`Lydian <https://en.wikipedia.org/wiki/Lydian_mode>`_ (IV) — major with a raised 4th. Dreamy, floating, ethereal
|
||||
(The Simpsons theme, Flying by ET):
|
||||
|
||||
.. code-block:: pycon
|
||||
|
||||
>>> c["lydian"].note_names
|
||||
['C', 'D', 'E', 'F#', 'G', 'A', 'B', 'C']
|
||||
|
||||
`Mixolydian <https://en.wikipedia.org/wiki/Mixolydian_mode>`_ (V) — major with a flat 7th. Bluesy, rock, dominant
|
||||
(Norwegian Wood, Sweet Home Alabama):
|
||||
|
||||
.. code-block:: pycon
|
||||
|
||||
>>> c["mixolydian"].note_names
|
||||
['C', 'D', 'E', 'F', 'G', 'A', 'Bb', 'C']
|
||||
|
||||
`Aeolian <https://en.wikipedia.org/wiki/Aeolian_mode>`_ (vi) — the natural minor scale. Sad, dark, introspective
|
||||
(Stairway to Heaven, Losing My Religion):
|
||||
|
||||
.. code-block:: pycon
|
||||
|
||||
>>> c["aeolian"].note_names
|
||||
['C', 'D', 'Eb', 'F', 'G', 'Ab', 'Bb', 'C']
|
||||
|
||||
`Locrian <https://en.wikipedia.org/wiki/Locrian_mode>`_ (vii) — minor with flat 2nd and flat 5th. Unstable,
|
||||
rarely used as a home key (used in metal and jazz over diminished
|
||||
chords):
|
||||
|
||||
.. code-block:: pycon
|
||||
|
||||
>>> c["locrian"].note_names
|
||||
['C', 'Db', 'Eb', 'F', 'Gb', 'Ab', 'Bb', 'C']
|
||||
|
||||
Scale Degrees
|
||||
-------------
|
||||
|
||||
Each note in a scale has a **degree name** that describes its function:
|
||||
|
||||
============ ====== =======================================
|
||||
Degree Number Function
|
||||
============ ====== =======================================
|
||||
Tonic I Home base — the key center
|
||||
Supertonic II One step above tonic
|
||||
Mediant III Halfway between tonic and dominant
|
||||
Subdominant IV A fifth below tonic (or fourth above)
|
||||
Dominant V The strongest pull back to tonic
|
||||
Submediant VI Root of the relative minor (or major)
|
||||
Leading Tone VII One semitone below tonic — pulls upward
|
||||
============ ====== =======================================
|
||||
|
||||
Access degrees by index, Roman numeral, or name:
|
||||
|
||||
.. code-block:: pycon
|
||||
|
||||
>>> major = TonedScale(tonic="C4")["major"]
|
||||
>>> major[0]
|
||||
C4
|
||||
>>> major["I"]
|
||||
C4
|
||||
>>> major["tonic"]
|
||||
C4
|
||||
>>> major["V"]
|
||||
G4
|
||||
>>> major["dominant"]
|
||||
G4
|
||||
>>> major[0:3]
|
||||
(<Tone C4>, <Tone D4>, <Tone E4>)
|
||||
|
||||
Iteration
|
||||
---------
|
||||
|
||||
Scales are iterable:
|
||||
Scales are iterable and support ``len()`` and ``in``:
|
||||
|
||||
.. code-block:: python
|
||||
.. code-block:: pycon
|
||||
|
||||
for tone in major:
|
||||
print(f"{tone.name}: {tone.frequency:.1f} Hz")
|
||||
|
||||
len(major) # 8 (7 notes + octave)
|
||||
"C" in major # True
|
||||
"C#" in major # False
|
||||
>>> for tone in major:
|
||||
... print(f"{tone.name}: {tone.frequency:.1f} Hz")
|
||||
C: 261.6 Hz
|
||||
D: 293.7 Hz
|
||||
E: 329.6 Hz
|
||||
F: 349.2 Hz
|
||||
G: 392.0 Hz
|
||||
A: 440.0 Hz
|
||||
B: 493.9 Hz
|
||||
C: 523.3 Hz
|
||||
>>> len(major)
|
||||
8
|
||||
>>> "C" in major
|
||||
True
|
||||
>>> "C#" in major
|
||||
False
|
||||
|
||||
Building Chords from Scales
|
||||
----------------------------
|
||||
|
||||
Build chords directly from scale degrees:
|
||||
`Diatonic <https://en.wikipedia.org/wiki/Diatonic_and_chromatic>`_ harmony builds chords by stacking every other note of the
|
||||
scale. A **triad** takes the 1st, 3rd, and 5th; a **seventh chord** adds
|
||||
the 7th.
|
||||
|
||||
.. code-block:: python
|
||||
In the C major scale, the diatonic triads are::
|
||||
|
||||
major = TonedScale(tonic="C4")["major"]
|
||||
I C E G = C major
|
||||
ii D F A = D minor
|
||||
iii E G B = E minor
|
||||
IV F A C = F major
|
||||
V G B D = G major
|
||||
vi A C E = A minor
|
||||
vii° B D F = B diminished
|
||||
|
||||
# Build a triad (root, 3rd, 5th)
|
||||
I = major.triad(0) # C E G (C major)
|
||||
ii = major.triad(1) # D F A (D minor)
|
||||
V = major.triad(4) # G B D (G major)
|
||||
Notice the pattern: **major** triads on I, IV, V; **minor** triads on
|
||||
ii, iii, vi; **diminished** on vii°. This pattern holds for every major
|
||||
key.
|
||||
|
||||
# Custom chord voicings
|
||||
cmaj7 = major.chord(0, 2, 4, 6) # C E G B
|
||||
.. code-block:: pycon
|
||||
|
||||
>>> major = TonedScale(tonic="C4")["major"]
|
||||
>>> major.triad(0)
|
||||
C major
|
||||
>>> major.triad(1)
|
||||
D minor
|
||||
>>> major.triad(2)
|
||||
E minor
|
||||
>>> major.triad(3)
|
||||
F major
|
||||
>>> major.triad(4)
|
||||
G major
|
||||
>>> major.triad(5)
|
||||
A minor
|
||||
>>> major.chord(0, 2, 4, 6)
|
||||
C major 7th
|
||||
>>> major.chord(4, 6, 8, 10)
|
||||
G dominant 7th
|
||||
|
||||
Common Progressions
|
||||
~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
Some of the most-used chord progressions in Western music:
|
||||
|
||||
- **I–IV–V–I** — the foundation of blues, rock, country, folk
|
||||
- **I–V–vi–IV** — the "pop progression" (Let It Be, No Woman No Cry,
|
||||
With or Without You, Someone Like You)
|
||||
- **ii–V–I** — the backbone of jazz harmony
|
||||
- **I–vi–IV–V** — the "50s progression" (Stand By Me, Every Breath You Take)
|
||||
- **i–bVI–bIII–bVII** — the "epic" minor progression (Stairway to Heaven,
|
||||
My Heart Will Go On)
|
||||
- **I–IV–vi–V** — axis of awesome (many, many pop songs)
|
||||
|
||||
The :class:`~pytheory.scales.Key` class makes working with progressions
|
||||
easy:
|
||||
|
||||
.. code-block:: pycon
|
||||
|
||||
>>> from pytheory import Key
|
||||
>>> key = Key("G", "major")
|
||||
>>> chords = key.progression("I", "V", "vi", "IV")
|
||||
>>> for c in chords:
|
||||
... print(c.identify())
|
||||
G major
|
||||
D major
|
||||
E minor
|
||||
C major
|
||||
>>> key.nashville(1, 5, 6, 4)
|
||||
[<Chord G major>, <Chord D major>, <Chord E minor>, <Chord C major>]
|
||||
>>> key.chords
|
||||
['G major', 'A minor', 'B minor', 'C major', 'D major', 'E minor', 'F# diminished']
|
||||
>>> key.seventh_chords
|
||||
['G major 7th', 'A minor 7th', 'B minor 7th', 'C major 7th', 'D dominant 7th', 'E minor 7th', 'F# half-diminished 7th']
|
||||
>>> Key.detect("C", "E", "G", "A", "D")
|
||||
C major
|
||||
|
||||
The 12-Bar Blues
|
||||
~~~~~~~~~~~~~~~~
|
||||
|
||||
The `12-bar blues <https://en.wikipedia.org/wiki/Twelve-bar_blues>`_ is the most influential chord progression in
|
||||
American music. It's 12 measures long and uses only three chords
|
||||
(I, IV, V)::
|
||||
|
||||
| I | I | I | I |
|
||||
| IV | IV | I | I |
|
||||
| V | IV | I | V |
|
||||
|
||||
Every blues, early rock and roll, and much of jazz is built on this
|
||||
structure. In the key of A::
|
||||
|
||||
| A | A | A | A |
|
||||
| D | D | A | A |
|
||||
| E | D | A | E |
|
||||
|
||||
.. code-block:: pycon
|
||||
|
||||
>>> from pytheory import TonedScale
|
||||
>>> a = TonedScale(tonic="A4")["major"]
|
||||
>>> I = a.triad(0)
|
||||
>>> IV = a.triad(3)
|
||||
>>> V = a.triad(4)
|
||||
>>> blues_12 = [I, I, I, I, IV, IV, I, I, V, IV, I, V]
|
||||
|
||||
Key Signatures
|
||||
~~~~~~~~~~~~~~
|
||||
|
||||
The ``signature`` property tells you how many sharps or flats a key has:
|
||||
|
||||
.. code-block:: pycon
|
||||
|
||||
>>> Key("G", "major").signature
|
||||
{'sharps': 1, 'flats': 0, 'accidentals': ['F#']}
|
||||
>>> Key("F", "major").signature
|
||||
{'sharps': 0, 'flats': 1, 'accidentals': ['Bb']}
|
||||
>>> Key("C", "major").signature
|
||||
{'sharps': 0, 'flats': 0, 'accidentals': []}
|
||||
|
||||
Relative and Parallel Keys
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
Two keys are **relative** if they share the same notes (C major and
|
||||
A minor). Two keys are `parallel <https://en.wikipedia.org/wiki/Parallel_key>`_ if they share the same tonic but
|
||||
have different notes (C major and C minor):
|
||||
|
||||
.. code-block:: pycon
|
||||
|
||||
>>> Key("C", "major").relative
|
||||
A minor
|
||||
>>> Key("A", "minor").relative
|
||||
C major
|
||||
>>> Key("C", "major").parallel
|
||||
C minor
|
||||
|
||||
Borrowed Chords
|
||||
~~~~~~~~~~~~~~~
|
||||
|
||||
`Modal interchange <https://en.wikipedia.org/wiki/Borrowed_chord>`_ —
|
||||
borrowing chords from the parallel key — is one of the most powerful
|
||||
tools in songwriting. The bVI and bVII chords (Ab and Bb in C major)
|
||||
are borrowed from C minor and appear constantly in rock and film music:
|
||||
|
||||
.. code-block:: pycon
|
||||
|
||||
>>> Key("C", "major").borrowed_chords
|
||||
['C minor', 'D diminished', 'Eb major', 'F minor', 'G minor', 'Ab major', 'Bb major']
|
||||
|
||||
Secondary Dominants
|
||||
~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
A `secondary dominant <https://en.wikipedia.org/wiki/Secondary_dominant>`_
|
||||
is the V chord *of* a non-tonic chord. It creates a momentary pull
|
||||
toward that chord, adding harmonic color:
|
||||
|
||||
.. code-block:: pycon
|
||||
|
||||
>>> key = Key("C", "major")
|
||||
>>> key.secondary_dominant(5)
|
||||
D dominant 7th
|
||||
>>> key.secondary_dominant(2)
|
||||
A dominant 7th
|
||||
|
||||
Random Progressions
|
||||
~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
Need inspiration? Generate weighted random progressions. The weights
|
||||
favor common chord functions (I and vi most likely, vii least):
|
||||
|
||||
.. code-block:: pycon
|
||||
|
||||
>>> key = Key("C", "major")
|
||||
>>> chords = key.random_progression(4)
|
||||
>>> [c.identify() for c in chords]
|
||||
['C major', 'F major', 'A minor', 'G major']
|
||||
|
||||
All Keys
|
||||
~~~~~~~~
|
||||
|
||||
Enumerate all 24 major and minor keys:
|
||||
|
||||
.. code-block:: pycon
|
||||
|
||||
>>> Key.all_keys()
|
||||
[<Key C major>, <Key C minor>, <Key C# major>, <Key C# minor>, ...]
|
||||
|
||||
Scale Transposition
|
||||
~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
Transpose an entire scale by a number of semitones:
|
||||
|
||||
.. code-block:: pycon
|
||||
|
||||
>>> c_major = TonedScale(tonic="C4")["major"]
|
||||
>>> d_major = c_major.transpose(2)
|
||||
>>> d_major.note_names
|
||||
['D', 'E', 'F#', 'G', 'A', 'B', 'C#', 'D']
|
||||
|
||||
Degree Names
|
||||
~~~~~~~~~~~~
|
||||
|
||||
Get the traditional function name for any scale degree:
|
||||
|
||||
.. code-block:: pycon
|
||||
|
||||
>>> major = TonedScale(tonic="C4")["major"]
|
||||
>>> major.degree_name(0)
|
||||
'tonic'
|
||||
>>> major.degree_name(4)
|
||||
'dominant'
|
||||
>>> major.degree_name(6)
|
||||
'leading tone'
|
||||
>>> major.degree_name(6, minor=True)
|
||||
'subtonic'
|
||||
|
||||
Scale Fitness
|
||||
~~~~~~~~~~~~~
|
||||
|
||||
Score how well a set of notes fits a scale (0.0–1.0). Useful for melody
|
||||
analysis or detecting which scale a phrase belongs to:
|
||||
|
||||
.. code-block:: pycon
|
||||
|
||||
>>> major = TonedScale(tonic="C4")["major"]
|
||||
>>> major.fitness("C", "D", "E", "G")
|
||||
1.0
|
||||
>>> major.fitness("C", "D", "F#", "G")
|
||||
0.75
|
||||
|
||||
Scale Recommendation
|
||||
~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
Given a melody or set of notes, find the best-matching scales ranked
|
||||
by fitness. Useful for figuring out what key you're in or finding
|
||||
alternative scales to improvise over:
|
||||
|
||||
.. code-block:: pycon
|
||||
|
||||
>>> from pytheory.scales import Scale
|
||||
|
||||
>>> Scale.recommend("C", "D", "E", "G", "A", top=3)
|
||||
[('C', 'major', 1.0), ('A', 'aeolian', 1.0), ...]
|
||||
|
||||
>>> Scale.recommend("C", "Eb", "F", "Gb", "G", "Bb", top=3)
|
||||
[('C', 'blues', 1.0), ...]
|
||||
|
||||
Chromatic scales are deprioritized since they match everything.
|
||||
|
||||
Parallel Modes
|
||||
~~~~~~~~~~~~~~
|
||||
|
||||
See all 7 modes that share the same notes as a scale:
|
||||
|
||||
.. code-block:: pycon
|
||||
|
||||
>>> major = TonedScale(tonic="C4")["major"]
|
||||
>>> for name, notes in major.parallel_modes().items():
|
||||
... print(f"{name}: {' '.join(notes)}")
|
||||
C ionian: C D E F G A B C
|
||||
D dorian: D E F G A B C D
|
||||
E phrygian: E F G A B C D E
|
||||
...
|
||||
|
||||
Common Progressions
|
||||
~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
Get all named progressions realized in a key with chord symbols:
|
||||
|
||||
.. code-block:: pycon
|
||||
|
||||
>>> key = Key("C", "major")
|
||||
>>> progs = key.common_progressions()
|
||||
>>> for name, chords in list(progs.items())[:3]:
|
||||
... symbols = [c.symbol for c in chords]
|
||||
... print(f"{name}: {' → '.join(symbols)}")
|
||||
I-IV-V-I: C → F → G → C
|
||||
I-V-vi-IV: C → G → Am → F
|
||||
I-vi-IV-V: C → Am → F → G
|
||||
|
||||
Chord Suggestions
|
||||
~~~~~~~~~~~~~~~~~
|
||||
|
||||
Given a chord in a key, ``suggest_next()`` returns likely next chords
|
||||
based on functional harmony voice-leading rules:
|
||||
|
||||
.. code-block:: pycon
|
||||
|
||||
>>> key = Key("C", "major")
|
||||
>>> g_major = key.triad(4) # V chord
|
||||
>>> [c.symbol for c in key.suggest_next(g_major)]
|
||||
['C', 'Am', 'F']
|
||||
|
||||
Modulation
|
||||
~~~~~~~~~~
|
||||
|
||||
``modulation_path()`` suggests a chord-by-chord route from one key to
|
||||
another, using pivot chords when available:
|
||||
|
||||
.. code-block:: pycon
|
||||
|
||||
>>> path = Key("C", "major").modulation_path(Key("G", "major"))
|
||||
>>> [c.symbol for c in path]
|
||||
['C', 'Em', 'D', 'G']
|
||||
|
||||
``pivot_chords()`` shows which chords are shared between two keys:
|
||||
|
||||
.. code-block:: pycon
|
||||
|
||||
>>> Key("C", "major").pivot_chords(Key("G", "major"))
|
||||
['A minor', 'B minor', 'C major', 'D major', 'E minor', 'G major']
|
||||
|
||||
Scales are the map; the key is the territory. Once you know the landscape, you can wander freely -- and you'll always know how to get home.
|
||||
|
||||
@@ -0,0 +1,688 @@
|
||||
Sequencing
|
||||
==========
|
||||
|
||||
The sequencing system lets you compose multi-part arrangements with
|
||||
durations, time signatures, and instrument voices. This is where
|
||||
PyTheory goes from theory tool to composition tool.
|
||||
|
||||
At the center of everything is the ``Score``. Think of it as your
|
||||
arrangement, your song, your sketch pad. It holds the tempo, the time
|
||||
signature, the drum pattern, and every instrument part you create. If
|
||||
you've ever used a DAW, the Score is your session file. If you haven't,
|
||||
it's the sheet of paper where the whole piece lives. Everything you
|
||||
compose -- melodies, chord progressions, bass lines, arpeggios -- gets
|
||||
added to a Score before you can hear it, export it, or do anything
|
||||
useful with it.
|
||||
|
||||
Duration
|
||||
--------
|
||||
|
||||
In music, all rhythm boils down to one convention: the quarter note
|
||||
equals one beat. Everything else is relative to that. A whole note is
|
||||
four beats. An eighth note is half a beat. This is how musicians have
|
||||
communicated timing for centuries, and it's how PyTheory works too.
|
||||
Once you internalize "quarter note = 1 beat," durations become
|
||||
intuitive arithmetic.
|
||||
|
||||
A ``Duration`` represents a note length in beats (quarter note = 1 beat):
|
||||
|
||||
.. code-block:: pycon
|
||||
|
||||
>>> from pytheory import Duration
|
||||
|
||||
>>> Duration.WHOLE.value
|
||||
4.0
|
||||
>>> Duration.HALF.value
|
||||
2.0
|
||||
>>> Duration.QUARTER.value
|
||||
1.0
|
||||
>>> Duration.EIGHTH.value
|
||||
0.5
|
||||
>>> Duration.SIXTEENTH.value
|
||||
0.25
|
||||
>>> Duration.DOTTED_HALF.value
|
||||
3.0
|
||||
>>> Duration.DOTTED_QUARTER.value
|
||||
1.5
|
||||
>>> Duration.TRIPLET_QUARTER.value
|
||||
0.6666666666666666
|
||||
|
||||
Time Signatures
|
||||
---------------
|
||||
|
||||
If you're not a musician, time signatures can seem mysterious. They're
|
||||
not. The top number tells you how many beats are in a bar. The bottom
|
||||
number tells you which note value gets one beat. That's it.
|
||||
|
||||
In practice, you only need to know a handful:
|
||||
|
||||
- **4/4** -- four beats per bar. This is the default. Almost all pop,
|
||||
rock, hip hop, electronic, and R&B music is in 4/4. If you're not
|
||||
sure, use this.
|
||||
- **3/4** -- three beats per bar. The waltz feel. Think "Blue Danube"
|
||||
or Radiohead's "Everything in Its Right Place."
|
||||
- **6/8** -- six eighth notes per bar, grouped in two sets of three.
|
||||
Each group feels like one big swaying beat. Folk music, slow jams,
|
||||
ballads.
|
||||
- **12/8** -- twelve eighth notes per bar, grouped in four sets of
|
||||
three. The slow blues shuffle, the gospel feel, "At Last" by Etta
|
||||
James. Each "big beat" has a triplet swing baked into it.
|
||||
|
||||
A ``TimeSignature`` holds the meter of a piece -- how many beats per
|
||||
measure and which note value gets one beat:
|
||||
|
||||
.. code-block:: pycon
|
||||
|
||||
>>> from pytheory.rhythm import TimeSignature
|
||||
|
||||
>>> ts = TimeSignature.from_string("4/4")
|
||||
>>> ts.beats_per_measure
|
||||
4.0
|
||||
|
||||
>>> TimeSignature.from_string("3/4").beats_per_measure
|
||||
3.0
|
||||
|
||||
>>> TimeSignature.from_string("6/8").beats_per_measure
|
||||
3.0
|
||||
|
||||
>>> TimeSignature.from_string("12/8").beats_per_measure
|
||||
6.0
|
||||
|
||||
The ``beats_per_measure`` is always in quarter-note units. In 6/8,
|
||||
there are 6 eighth notes per bar = 3 quarter-note beats. In 12/8,
|
||||
12 eighth notes = 6 quarter-note beats, grouped in four dotted-quarter
|
||||
pulses.
|
||||
|
||||
Score Basics
|
||||
------------
|
||||
|
||||
A ``Score`` is a sequence of notes and rests with a time signature and
|
||||
tempo. Use ``.add()`` and ``.rest()`` for fluent chaining:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
from pytheory import Score, Duration, Tone
|
||||
|
||||
score = Score("4/4", bpm=120)
|
||||
score.add(Tone.from_string("C4", system="western"), Duration.QUARTER)
|
||||
score.add(Tone.from_string("E4", system="western"), Duration.QUARTER)
|
||||
score.add(Tone.from_string("G4", system="western"), Duration.HALF)
|
||||
|
||||
.. code-block:: pycon
|
||||
|
||||
>>> score.total_beats
|
||||
4.0
|
||||
>>> score.measures
|
||||
1.0
|
||||
>>> score.duration_ms
|
||||
2000.0
|
||||
|
||||
Rests
|
||||
~~~~~
|
||||
|
||||
Add silence with ``.rest()``:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
score = Score("4/4", bpm=120)
|
||||
score.add(Tone.from_string("C4", system="western"), Duration.HALF)
|
||||
score.rest(Duration.HALF)
|
||||
|
||||
.. code-block:: pycon
|
||||
|
||||
>>> score.measures
|
||||
1.0
|
||||
|
||||
Chords
|
||||
~~~~~~
|
||||
|
||||
Chords work just like tones — pass any ``Chord`` object:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
from pytheory import Score, Duration, Key
|
||||
|
||||
key = Key("C", "major")
|
||||
chords = key.progression("I", "V", "vi", "IV")
|
||||
|
||||
score = Score("4/4", bpm=120)
|
||||
for chord in chords:
|
||||
score.add(chord, Duration.WHOLE)
|
||||
|
||||
.. code-block:: pycon
|
||||
|
||||
>>> score.measures
|
||||
4.0
|
||||
>>> score.duration_ms
|
||||
8000.0
|
||||
|
||||
Compound Time
|
||||
~~~~~~~~~~~~~
|
||||
|
||||
12/8 is a compound meter — 12 eighth notes per bar grouped in four
|
||||
groups of three. Each group feels like one "big beat":
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
from pytheory import Score, Duration, Key
|
||||
|
||||
key = Key("A", "minor")
|
||||
chords = key.random_progression(4)
|
||||
|
||||
score = Score("12/8", bpm=120)
|
||||
for c in chords:
|
||||
score.add(c, Duration.DOTTED_HALF)
|
||||
score.add(c, Duration.DOTTED_HALF)
|
||||
|
||||
.. code-block:: pycon
|
||||
|
||||
>>> score.measures
|
||||
4.0
|
||||
|
||||
Parts
|
||||
-----
|
||||
|
||||
Parts are like tracks in a DAW. Each one has its own instrument sound
|
||||
(synth waveform + envelope), its own volume level, and its own effects
|
||||
chain. When you call ``play_score()``, all the parts get mixed together
|
||||
into a single audio stream -- just like hitting play in Logic or
|
||||
Ableton. You might have a pad part holding down chords, a lead part
|
||||
playing a melody, and a bass part holding down the low end. Each one
|
||||
is independent: different synth, different envelope, different effects.
|
||||
|
||||
The ``Part`` class lets you layer multiple instrument voices -- each with
|
||||
its own synth waveform, ADSR envelope, and volume level. Create parts
|
||||
with ``Score.part()``:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
from pytheory import Score, Key, Duration, Chord
|
||||
from pytheory.play import play_score
|
||||
|
||||
score = Score("4/4", bpm=140)
|
||||
|
||||
chords = score.part("chords", synth="sine", envelope="pad", volume=0.35)
|
||||
lead = score.part("lead", synth="saw", envelope="pluck", volume=0.5)
|
||||
bass = score.part("bass", synth="triangle", envelope="pluck", volume=0.45)
|
||||
|
||||
Adding Notes to Parts
|
||||
~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
Parts accept note strings directly — no need to wrap in
|
||||
``Tone.from_string()``. ``.add()`` and ``.rest()`` return self for
|
||||
fluent chaining:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
lead.add("E5", Duration.QUARTER).add("D5", Duration.EIGHTH).rest(Duration.EIGHTH)
|
||||
|
||||
Raw float beats work too — useful for swing and tuplets:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
lead.add("C5", 0.67).add("B4", 0.33).add("A4", 1.0)
|
||||
|
||||
Chords and Tone objects work the same way:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
for chord in Key("A", "minor").progression("i", "iv", "V", "i"):
|
||||
chords.add(chord, Duration.WHOLE)
|
||||
|
||||
for note in ["A2", "C3", "E3", "A2", "D2", "F2", "A2", "D2"]:
|
||||
bass.add(note, Duration.QUARTER)
|
||||
|
||||
Arpeggiator
|
||||
------------
|
||||
|
||||
An arpeggiator takes a chord and plays its notes one at a time, in a
|
||||
pattern, automatically. You hold down a chord and it ripples through
|
||||
the notes -- up, down, up-and-down, random. It's one of the most
|
||||
iconic sounds in electronic music. The bubbly bass lines of acid house,
|
||||
the cascading runs of 80s synth pop (think "Jump" or "Take On Me"),
|
||||
the hypnotic patterns of trance -- all arpeggiators. It turns a simple
|
||||
three-note chord into a rhythmic, melodic engine.
|
||||
|
||||
``Part.arpeggio()`` takes a chord and sequences through its notes
|
||||
automatically -- like a hardware arpeggiator on a synth:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
lead = score.part(
|
||||
"lead",
|
||||
synth="saw",
|
||||
legato=True,
|
||||
glide=0.03,
|
||||
distortion=0.8,
|
||||
lowpass=1000,
|
||||
lowpass_q=5.0,
|
||||
)
|
||||
lead.arpeggio(
|
||||
Chord.from_symbol("Cm"),
|
||||
bars=2,
|
||||
pattern="up",
|
||||
division=Duration.SIXTEENTH,
|
||||
octaves=2,
|
||||
)
|
||||
|
||||
Parameters:
|
||||
|
||||
- ``chord``: A Chord object or string like ``"Am"``.
|
||||
- ``bars``: Number of bars to fill (default 1).
|
||||
- ``pattern``: ``"up"``, ``"down"``, ``"updown"``, ``"downup"``, ``"random"``.
|
||||
- ``division``: Step length (default ``Duration.SIXTEENTH``).
|
||||
- ``octaves``: Octave span (default 1). With 2, the pattern repeats one octave up.
|
||||
|
||||
Chain arpeggios through a progression:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
for sym in ["Cm", "Fm", "Abm", "Gm"]:
|
||||
lead.arpeggio(sym, bars=2, pattern="updown", octaves=2)
|
||||
|
||||
Combined with legato, glide, distortion, and a resonant lowpass, this
|
||||
produces the classic acid/trance arpeggiator sound.
|
||||
|
||||
Legato and Glide
|
||||
----------------
|
||||
|
||||
Normally, every note you play has its own life cycle -- the sound
|
||||
attacks, sustains, and releases before the next note begins. You hear
|
||||
each note as a separate event. Legato changes that. The Italian word
|
||||
means "tied together," and that's exactly what it does: the envelope
|
||||
flows continuously from one note to the next with no retriggering. The
|
||||
pitch changes, but the sound never dies and restarts.
|
||||
|
||||
Glide (also called portamento) takes this further. Instead of the pitch
|
||||
jumping instantly from one note to the next, it *slides* -- a smooth,
|
||||
continuous pitch sweep. This is THE sound of the Roland TB-303, the
|
||||
little silver box that accidentally invented acid house. A saw wave
|
||||
with legato, glide, a resonant lowpass filter, and some distortion --
|
||||
that's the entire genre right there.
|
||||
|
||||
By default, each note gets its own attack/release envelope. ``legato=True``
|
||||
renders the entire part as one continuous waveform -- the pitch changes
|
||||
at note boundaries but the envelope flows unbroken. Add ``glide`` for
|
||||
portamento (pitch slides between notes):
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
acid = score.part(
|
||||
"acid",
|
||||
synth="saw",
|
||||
envelope="pad",
|
||||
legato=True,
|
||||
glide=0.04,
|
||||
)
|
||||
acid.add("C2", 0.25).add("C3", 0.25).add("G2", 0.25).add("C2", 0.25)
|
||||
|
||||
- ``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.
|
||||
|
||||
Complete Example
|
||||
----------------
|
||||
|
||||
A full multi-part arrangement built from scratch — bossa nova with FM
|
||||
rhodes, triangle lead, and filtered bass:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
from pytheory import Score, Pattern, Key, Duration, Chord
|
||||
from pytheory.play import play_score
|
||||
|
||||
score = Score("4/4", bpm=140)
|
||||
score.drums("bossa nova", repeats=4)
|
||||
|
||||
# FM rhodes with reverb
|
||||
rhodes = score.part(
|
||||
"rhodes",
|
||||
synth="fm",
|
||||
envelope="piano",
|
||||
volume=0.3,
|
||||
reverb=0.4,
|
||||
reverb_decay=1.8,
|
||||
)
|
||||
|
||||
# Triangle 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,
|
||||
)
|
||||
|
||||
# Filtered bass
|
||||
bass = score.part(
|
||||
"bass",
|
||||
synth="sine",
|
||||
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 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)
|
||||
|
||||
for n in ["A2", "E2", "A2", "C3", "D2", "A2", "D2", "F2"]:
|
||||
bass.add(n, Duration.QUARTER)
|
||||
|
||||
play_score(score)
|
||||
|
||||
Velocity
|
||||
--------
|
||||
|
||||
Real music has dynamics — accents are louder, ghost notes are barely
|
||||
there, phrases crescendo and decrescendo. Every note can have its own
|
||||
velocity (1–127, where 100 is the default):
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
lead.add("C5", Duration.QUARTER, velocity=120) # loud accent
|
||||
lead.add("D5", Duration.QUARTER, velocity=40) # ghost note
|
||||
lead.add("E5", Duration.QUARTER) # default (100)
|
||||
|
||||
The arpeggiator also accepts velocity:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
lead.arpeggio("Am", bars=2, pattern="up", velocity=80)
|
||||
|
||||
Swing and Groove
|
||||
----------------
|
||||
|
||||
Perfectly quantized music sounds robotic. Swing delays every other
|
||||
subdivision by a percentage, giving the rhythm a human, shuffled feel.
|
||||
Jazz swings hard. Bossa nova swings gently. Hip hop has its own pocket.
|
||||
|
||||
Set swing on the Score (applies to everything) or per-Part:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
# Triplet swing — lazy jazz feel
|
||||
score = Score("4/4", bpm=100, swing=0.55)
|
||||
|
||||
# Per-part override — the lead swings harder than the bass
|
||||
lead = score.part("lead", synth="saw", swing=0.6)
|
||||
bass = score.part("bass", synth="sine", swing=0.4)
|
||||
|
||||
Swing values:
|
||||
|
||||
- **0.0** = perfectly straight (default)
|
||||
- **0.3** = subtle shuffle (pop, R&B)
|
||||
- **0.5** = triplet feel (jazz, blues)
|
||||
- **0.67** = hard swing (bebop)
|
||||
|
||||
Tempo Changes
|
||||
-------------
|
||||
|
||||
Real music doesn't stay at one tempo. Songs speed up for energy,
|
||||
slow down for endings, and sometimes shift abruptly. Use
|
||||
``score.set_tempo()`` to change BPM at the current position:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
score = Score("4/4", bpm=90)
|
||||
|
||||
# Verse: slow and moody
|
||||
lead.add("D5", Duration.WHOLE)
|
||||
lead.add("F5", Duration.WHOLE)
|
||||
|
||||
# Chorus: speeds up
|
||||
score.set_tempo(110)
|
||||
lead.add("A5", Duration.WHOLE)
|
||||
lead.add("D6", Duration.WHOLE)
|
||||
|
||||
# Outro: slows way down
|
||||
score.set_tempo(70)
|
||||
lead.add("D5", Duration.WHOLE)
|
||||
|
||||
The tempo map engine handles the math — beat positions are converted
|
||||
to sample positions accounting for every tempo change.
|
||||
|
||||
Fades
|
||||
-----
|
||||
|
||||
``Part.fade_in()`` and ``Part.fade_out()`` ramp the volume over a
|
||||
number of bars. They work by generating automation points, so they
|
||||
integrate naturally with the rest of the automation system:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
pad = score.part(
|
||||
"pad",
|
||||
synth="supersaw",
|
||||
envelope="pad",
|
||||
volume=0.3,
|
||||
reverb=0.5,
|
||||
)
|
||||
|
||||
# Fade in over first 4 bars
|
||||
pad.fade_in(bars=4)
|
||||
for chord in chords:
|
||||
pad.add(chord, Duration.WHOLE)
|
||||
|
||||
# Fade out over last 2 bars
|
||||
pad.fade_out(bars=2)
|
||||
pad.rest(Duration.WHOLE)
|
||||
pad.rest(Duration.WHOLE)
|
||||
|
||||
Humanize
|
||||
--------
|
||||
|
||||
Perfectly quantized music sounds like a machine made it — because it
|
||||
did. Real musicians are never exactly on the beat. Their timing drifts
|
||||
by a few milliseconds, their velocity varies from note to note. These
|
||||
imperfections are what make music feel *alive*.
|
||||
|
||||
The ``humanize`` parameter adds random micro-variations in both timing
|
||||
and velocity at render time. The score data stays clean and
|
||||
deterministic — the randomness is only applied during playback.
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
# Subtle — like a very tight session player
|
||||
lead = score.part("lead", synth="saw", humanize=0.1)
|
||||
|
||||
# Natural — like a good live take
|
||||
rhodes = score.part("rhodes", synth="fm", humanize=0.3)
|
||||
|
||||
# Loose — like a late-night jam after a few drinks
|
||||
bass = score.part("bass", synth="sine", humanize=0.5)
|
||||
|
||||
Humanize values:
|
||||
|
||||
- **0.0** = perfectly quantized (default)
|
||||
- **0.1** = subtle, studio-tight
|
||||
- **0.2–0.3** = natural, like a real player
|
||||
- **0.4–0.5** = loose, relaxed, human
|
||||
- **0.6+** = sloppy (sometimes that's what you want)
|
||||
|
||||
Combine with swing for the most realistic feel:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
score = Score("4/4", bpm=95, swing=0.45)
|
||||
lead = score.part(
|
||||
"lead",
|
||||
synth="saw",
|
||||
envelope="pluck",
|
||||
humanize=0.3,
|
||||
delay=0.2,
|
||||
reverb=0.25,
|
||||
)
|
||||
|
||||
Song Structure
|
||||
--------------
|
||||
|
||||
Real songs aren't one long stream of notes — they have verses,
|
||||
choruses, bridges, drops. The section system lets you name blocks
|
||||
of your arrangement, then repeat them without rewriting everything.
|
||||
|
||||
This is how actual songwriting works: you write a verse, you write
|
||||
a chorus, then you arrange them — verse, verse, chorus, verse,
|
||||
chorus, chorus, outro. The sections are the building blocks;
|
||||
the arrangement is the order you play them in.
|
||||
|
||||
Define sections with ``score.section()`` and repeat them with
|
||||
``score.repeat()``:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
score = Score("4/4", bpm=124)
|
||||
score.drums("house", repeats=16)
|
||||
|
||||
pad = score.part("pad", synth="supersaw", envelope="pad")
|
||||
lead = score.part("lead", synth="saw", envelope="pluck")
|
||||
bass = score.part("bass", synth="sine", lowpass=300)
|
||||
|
||||
# ── Define the verse ──
|
||||
score.section("verse")
|
||||
for sym in ["Cm", "Ab", "Eb", "Bb"]:
|
||||
pad.add(Chord.from_symbol(sym), Duration.WHOLE)
|
||||
lead.add("C5", 1).add("Eb5", 1).rest(2)
|
||||
for n in ["C1", "C1", "Ab0", "Ab0", "Eb1", "Eb1", "Bb0", "Bb0"]:
|
||||
bass.add(n, Duration.HALF)
|
||||
|
||||
# ── Define the chorus ──
|
||||
score.section("chorus")
|
||||
lead.set(lowpass=5000, reverb=0.3)
|
||||
for sym in ["Cm", "Fm", "Ab", "Gm"]:
|
||||
pad.add(Chord.from_symbol(sym), Duration.WHOLE)
|
||||
lead.add("C6", 1).add("Bb5", 1).add("G5", 1).rest(1)
|
||||
for n in ["C1", "C1", "F1", "F1", "Ab0", "Ab0", "G1", "G1"]:
|
||||
bass.add(n, Duration.HALF)
|
||||
score.end_section()
|
||||
|
||||
# ── Arrange: verse, chorus, verse, chorus, chorus ──
|
||||
score.repeat("verse")
|
||||
score.repeat("chorus")
|
||||
score.repeat("verse")
|
||||
score.repeat("chorus", times=2)
|
||||
|
||||
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)
|
||||
@@ -0,0 +1,792 @@
|
||||
Synthesizers
|
||||
============
|
||||
|
||||
PyTheory includes 41 built-in waveforms and 10 ADSR envelope presets.
|
||||
Every sound is generated from scratch -- no samples or external audio
|
||||
files needed.
|
||||
|
||||
Here's the beautiful thing about synthesis: all of it comes from math.
|
||||
Sine waves, addition, and shaping. That's the entire foundation. Every
|
||||
legendary synth in history -- the Moog Minimoog, the Sequential Prophet-5,
|
||||
the Yamaha DX7, the Roland Juno-106, the Roland TB-303 -- uses some
|
||||
combination of these building blocks. When you choose a waveform in
|
||||
PyTheory, you're reaching for the same raw materials that defined
|
||||
decades of music. The difference between a Moog bass and a DX7 bell
|
||||
isn't magic; it's which waveforms you start with and how you shape them.
|
||||
|
||||
Classic Waveforms
|
||||
-----------------
|
||||
|
||||
These four are the fundamentals. Every analog synthesizer ever built
|
||||
starts here. If you learn nothing else about synthesis, learn these --
|
||||
they're the primary colors you mix everything else from.
|
||||
|
||||
Sine
|
||||
~~~~
|
||||
|
||||
The purest tone possible. Contains only the fundamental frequency with
|
||||
no harmonics. Sounds smooth, clean, and "electronic." This is the
|
||||
building block of all other waveforms (Fourier's theorem).
|
||||
|
||||
**Use for:** sub bass, clean pads, test tones, blending under other voices.
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
from pytheory import play, Synth, Tone
|
||||
|
||||
tone = Tone.from_string("C4", system="western")
|
||||
play(tone, synth=Synth.SINE)
|
||||
|
||||
Sawtooth
|
||||
~~~~~~~~
|
||||
|
||||
Contains all harmonics (both odd and even), each at amplitude 1/n.
|
||||
The richest of the classic waveforms — bright, buzzy, and aggressive.
|
||||
Named for its ramp shape.
|
||||
|
||||
**Use for:** leads, brass, pads, anything that needs presence and bite.
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
play(tone, synth=Synth.SAW)
|
||||
|
||||
Triangle
|
||||
~~~~~~~~
|
||||
|
||||
Contains only odd harmonics, each at amplitude 1/n-squared. Softer and
|
||||
more mellow than sawtooth — somewhere between sine and saw. Often
|
||||
described as "woody" or "hollow."
|
||||
|
||||
**Use for:** flute-like leads, mellow bass, gentle pads.
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
play(tone, synth=Synth.TRIANGLE)
|
||||
|
||||
Square
|
||||
~~~~~~
|
||||
|
||||
Contains only odd harmonics, each at amplitude 1/n. Sounds hollow and
|
||||
punchy — the classic chiptune / 8-bit sound. A special case of the
|
||||
pulse wave with a 50% duty cycle.
|
||||
|
||||
**Use for:** chiptune, 8-bit game music, hollow leads, sub-octave bass.
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
play(tone, synth=Synth.SQUARE)
|
||||
|
||||
Extended Waveforms
|
||||
------------------
|
||||
|
||||
These go beyond the basics into territory that defined specific
|
||||
instruments and eras. The pulse wave is the sound of the NES. FM
|
||||
synthesis is the sound of the 1980s. If the classic waveforms are
|
||||
primary colors, these are the specific pigments that painters actually
|
||||
reach for.
|
||||
|
||||
Pulse
|
||||
~~~~~
|
||||
|
||||
A pulse wave with a variable duty cycle. Narrower pulses sound thinner
|
||||
and more nasal. At 50% it equals a square wave; at 10--20% it produces
|
||||
the classic NES-style buzzy tone.
|
||||
|
||||
**Use for:** NES/chiptune sounds, nasal leads, retro textures.
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
lead = score.part("lead", synth="pulse", envelope="pluck")
|
||||
|
||||
FM Synthesis
|
||||
~~~~~~~~~~~~
|
||||
|
||||
Frequency modulation -- one oscillator (the modulator) modulates the
|
||||
frequency of another (the carrier), producing complex inharmonic
|
||||
spectra. This is how the Yamaha DX7 works -- the best-selling
|
||||
synthesizer of all time. Released in 1983, it was suddenly everywhere:
|
||||
the electric piano in every Whitney Houston ballad, the bass in every
|
||||
Depeche Mode track, the bells in a thousand TV jingles. If you heard
|
||||
pop music in the 80s, you heard FM synthesis.
|
||||
|
||||
**Use for:** electric piano (rhodes), bells, metallic leads, jazz chords.
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
rhodes = score.part(
|
||||
"rhodes",
|
||||
synth="fm",
|
||||
envelope="piano",
|
||||
volume=0.3,
|
||||
reverb=0.4,
|
||||
)
|
||||
|
||||
Noise
|
||||
-----
|
||||
|
||||
White Noise
|
||||
~~~~~~~~~~~
|
||||
|
||||
Equal energy at all frequencies — pure randomness with no pitch.
|
||||
Useful as a texture layer, a percussion source, or a wind/ocean effect.
|
||||
|
||||
**Use for:** snare layers, hi-hats, wind effects, risers, ambient texture.
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
texture = score.part(
|
||||
"texture",
|
||||
synth="noise",
|
||||
envelope="pad",
|
||||
volume=0.1,
|
||||
lowpass=2000,
|
||||
)
|
||||
|
||||
Ensemble Waveforms
|
||||
------------------
|
||||
|
||||
These all create "bigger" sounds by layering or modulating multiple
|
||||
oscillators. Where a single sawtooth wave sounds like one instrument,
|
||||
these sound like a section -- a string ensemble, a choir, a wall of
|
||||
synths. They're the pad and atmosphere machines, the sounds that fill
|
||||
out a mix and make it feel wide and immersive.
|
||||
|
||||
Supersaw
|
||||
~~~~~~~~
|
||||
|
||||
Seven detuned sawtooth oscillators stacked together. The slight pitch
|
||||
differences create a shimmering, massive wall of sound. This is the
|
||||
Roland JP-8000's gift to the world -- the waveform that launched an
|
||||
entire genre. Every trance anthem from the late 90s and early 2000s,
|
||||
every euphoric EDM drop, every J-pop power chord owes something to the
|
||||
supersaw.
|
||||
|
||||
**Use for:** trance pads, EDM leads, massive chords, anthem hooks.
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
pad = score.part(
|
||||
"pad",
|
||||
synth="supersaw",
|
||||
envelope="pad",
|
||||
volume=0.4,
|
||||
chorus=0.3,
|
||||
reverb=0.5,
|
||||
)
|
||||
|
||||
PWM Slow
|
||||
~~~~~~~~
|
||||
|
||||
Pulse width modulation with a slow LFO. The duty cycle sweeps back and
|
||||
forth, creating a lush, animated pad sound. This is the Roland Juno-106
|
||||
in a nutshell -- arguably the most recorded synth pad sound in history.
|
||||
That warm, slowly evolving, slightly chorused wash you hear in everything
|
||||
from Boards of Canada to Drake? PWM with a slow LFO.
|
||||
|
||||
**Use for:** lush analog pads, slow evolving textures, Juno-style warmth.
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
pad = score.part(
|
||||
"pad",
|
||||
synth="pwm_slow",
|
||||
envelope="pad",
|
||||
volume=0.35,
|
||||
reverb=0.4,
|
||||
)
|
||||
|
||||
PWM Fast
|
||||
~~~~~~~~
|
||||
|
||||
Pulse width modulation with a fast LFO. The rapid duty cycle sweep
|
||||
produces a natural chorus/vibrato effect built into the waveform itself.
|
||||
|
||||
**Use for:** animated leads, vibrato textures, movement without effects.
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
lead = score.part("lead", synth="pwm_fast", envelope="pluck", volume=0.5)
|
||||
|
||||
ADSR Envelopes
|
||||
--------------
|
||||
|
||||
Here's a question: a piano and an organ can play the exact same note at
|
||||
the exact same frequency. Why do they sound completely different? The
|
||||
answer is the envelope -- the *shape* of the sound over time. A piano
|
||||
hits hard and immediately starts fading. An organ snaps on at full
|
||||
volume and stays there until you lift the key. A violin swells in
|
||||
gradually. The frequency is the same; the envelope is what makes each
|
||||
instrument feel like itself.
|
||||
|
||||
ADSR stands for Attack, Decay, Sustain, Release -- four stages that
|
||||
describe how any sound's volume changes from the moment you press a key
|
||||
to the moment it falls silent. Understanding ADSR is the single most
|
||||
important thing you can learn about synthesis, because it's the
|
||||
difference between a sound that feels like an instrument and a sound
|
||||
that feels like a test tone.
|
||||
|
||||
Raw waveforms click at the start and end of each note. An ADSR envelope
|
||||
shapes the amplitude over time for natural-sounding notes:
|
||||
|
||||
- **Attack** -- how quickly the sound reaches full volume.
|
||||
- **Decay** -- how quickly it drops to the sustain level.
|
||||
- **Sustain** -- the held volume while the note is on.
|
||||
- **Release** -- how quickly it fades to silence after the note ends.
|
||||
|
||||
PyTheory includes 10 presets:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
from pytheory import play, Envelope, Tone
|
||||
|
||||
tone = Tone.from_string("C4", system="western")
|
||||
|
||||
play(tone, envelope=Envelope.PIANO) # Quick attack, natural decay
|
||||
play(tone, envelope=Envelope.PLUCK) # Sharp attack, fast decay
|
||||
play(tone, envelope=Envelope.PAD) # Slow fade in, lush sustain
|
||||
play(tone, envelope=Envelope.ORGAN) # Instant on/off, no shaping
|
||||
play(tone, envelope=Envelope.BELL) # Instant attack, long ring
|
||||
play(tone, envelope=Envelope.STRINGS) # Gradual bow attack
|
||||
play(tone, envelope=Envelope.BOWED) # Bow bite into sustain
|
||||
play(tone, envelope=Envelope.MALLET) # Strike with ringing sustain
|
||||
play(tone, envelope=Envelope.STACCATO) # Short and punchy
|
||||
play(tone, envelope=Envelope.NONE) # Raw waveform, no shaping
|
||||
|
||||
Envelope Descriptions
|
||||
~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
=============== ================================================
|
||||
Name Character
|
||||
=============== ================================================
|
||||
``"piano"`` Quick attack, natural decay -- acoustic piano feel
|
||||
``"pluck"`` Sharp attack, fast decay -- guitar pick, harp
|
||||
``"pad"`` Slow fade in, lush sustain -- strings, synth pads
|
||||
``"organ"`` Instant on/off -- Hammond organ, no shaping
|
||||
``"bell"`` Instant attack, no sustain -- short metallic ring
|
||||
``"strings"`` Gradual bow attack -- orchestral strings, slow
|
||||
``"bowed"`` Bow bite into sustain -- solo strings, brass
|
||||
``"mallet"`` Strike with ringing sustain -- vibraphone, celesta
|
||||
``"staccato"`` Short and punchy -- funk stabs, percussive hits
|
||||
``"none"`` Raw waveform, no amplitude shaping at all
|
||||
=============== ================================================
|
||||
|
||||
Detune
|
||||
------
|
||||
|
||||
Any synth can be fattened with the ``detune`` parameter — it renders
|
||||
three oscillators per note: the center pitch plus one shifted up and
|
||||
one shifted down by the specified number of cents. The slight frequency
|
||||
differences create beating and width, like an analog synth with
|
||||
oscillator drift.
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
# Juno-style analog drift — subtle, warm
|
||||
pad = score.part("pad", synth="saw", detune=15)
|
||||
|
||||
# Trance supersaw territory — wide, shimmery
|
||||
lead = score.part("lead", synth="saw", detune=25)
|
||||
|
||||
# Subtle thickening on a bass
|
||||
bass = score.part("bass", synth="pulse", detune=8)
|
||||
|
||||
# Works on any synth — even FM
|
||||
bells = score.part("bells", synth="fm", detune=12)
|
||||
|
||||
Detune values:
|
||||
|
||||
- **5–10** = subtle thickening (barely noticeable, just warmer)
|
||||
- **12–18** = classic analog drift (Juno, Prophet)
|
||||
- **20–30** = wide and shimmery (trance, EDM)
|
||||
- **40+** = extreme, almost chorus-like
|
||||
|
||||
This is different from the ``chorus`` effect — detune creates
|
||||
additional oscillators at render time (three per note), while chorus
|
||||
processes the audio after rendering with a modulated delay line.
|
||||
Detune is "wider at the source," chorus is "wider after the fact."
|
||||
Stack both for maximum fatness.
|
||||
|
||||
Stereo Placement
|
||||
----------------
|
||||
|
||||
Every part can be placed in the stereo field with ``pan`` and ``spread``.
|
||||
|
||||
**Pan** positions a part left or right. Constant-power panning keeps
|
||||
the perceived loudness even as you move across the field:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
rhythm = score.part("rhythm", synth="saw", pan=-0.7) # left
|
||||
lead = score.part("lead", synth="saw", pan=0.6) # right
|
||||
bass = score.part("bass", synth="sine", pan=0.0) # center
|
||||
hats = score.part("hats", synth="noise", pan=0.3) # slightly right
|
||||
|
||||
Pan values: -1.0 (hard left), 0.0 (center), 1.0 (hard right).
|
||||
|
||||
**Spread** works with ``detune`` — the up-detuned oscillator goes to
|
||||
the right channel and the down-detuned goes to the left, creating
|
||||
stereo width at the source:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
# Wide pad: detuned + spread across the stereo field
|
||||
pad = score.part(
|
||||
"pad",
|
||||
synth="saw",
|
||||
detune=20,
|
||||
spread=1.0, # full L/R separation of detuned voices
|
||||
reverb=0.4,
|
||||
)
|
||||
|
||||
Spread values: 0.0 (detuned voices stay mono), 1.0 (full L/R split).
|
||||
Stack with pan to offset the center of the spread.
|
||||
|
||||
Reverb is also stereo — the left and right channels get different
|
||||
early reflection patterns, so the reverb tail occupies real space
|
||||
in the stereo field rather than sitting dead center.
|
||||
|
||||
Physical Modeling
|
||||
-----------------
|
||||
|
||||
Three synths go beyond traditional waveform synthesis into physical
|
||||
modeling territory — they simulate how real instruments produce sound.
|
||||
|
||||
Karplus-Strong Pluck
|
||||
~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
A burst of noise fed into a short delay line. The delay length sets
|
||||
the pitch, the feedback filter models the string decaying. This is
|
||||
how every physical modeling synth since 1983 does plucked strings.
|
||||
It sounds genuinely like a real guitar, harp, or koto.
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
guitar = score.part("guitar", synth="pluck_synth")
|
||||
harp = score.part("harp", instrument="harp") # uses pluck_synth
|
||||
|
||||
Hammond Organ
|
||||
~~~~~~~~~~~~~
|
||||
|
||||
Additive synthesis with drawbar harmonics — sine waves at the
|
||||
fundamental plus 2nd, 3rd, 4th, 5th, 6th, and 8th harmonics mixed
|
||||
at musical levels. Warm, round, unmistakably organ.
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
organ = score.part("organ", synth="organ_synth")
|
||||
|
||||
String Ensemble
|
||||
~~~~~~~~~~~~~~~
|
||||
|
||||
Filtered sawtooth with body resonance formants at ~500 Hz and ~1500 Hz,
|
||||
modeling the way a violin or cello body shapes the sound. Warmer and
|
||||
more "wooden" than a raw saw wave.
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
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 — 60+ predefined combinations that approximate real
|
||||
instruments:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
piano = score.part("piano", instrument="piano")
|
||||
violin = score.part("violin", instrument="violin")
|
||||
guitar = score.part("guitar", instrument="acoustic_guitar")
|
||||
organ = score.part("organ", instrument="organ")
|
||||
bass = score.part("bass", instrument="upright_bass")
|
||||
|
||||
Available instruments:
|
||||
|
||||
**Keys**: piano, electric_piano, organ, harpsichord, celesta, music_box,
|
||||
accordion
|
||||
|
||||
**Strings**: violin, viola, cello, contrabass, string_ensemble
|
||||
|
||||
**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, clean_guitar, crunch_guitar,
|
||||
distorted_guitar, orange_crunch, metal_guitar, bass_guitar, upright_bass,
|
||||
harp, sitar, koto, banjo, mandolin, mandola, ukulele
|
||||
|
||||
**World/Exotic**: pedal_steel, theremin, kalimba, steel_drum, didgeridoo,
|
||||
bagpipe
|
||||
|
||||
**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:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
# Piano with extra reverb
|
||||
piano = score.part("piano", instrument="piano", reverb=0.5)
|
||||
|
||||
# Violin panned left
|
||||
violin = score.part("v", instrument="violin", pan=-0.4)
|
||||
|
||||
Choosing Synth and Envelope Combos
|
||||
----------------------------------
|
||||
|
||||
The right combination of synth and envelope defines the character of a
|
||||
voice. This is where you stop thinking about waveforms and start
|
||||
thinking about *instruments*. Here are some starting points:
|
||||
|
||||
- **Funk stabs:** ``saw`` + ``staccato`` -- bright, punchy, rhythmic.
|
||||
- **Jazz keys:** ``fm`` + ``bell`` -- glassy DX7 electric piano.
|
||||
- **Ambient pads:** ``supersaw`` + ``pad`` -- massive, slow-building wash.
|
||||
- **Acid bass:** ``saw`` + ``pluck`` with lowpass and glide -- 303-style.
|
||||
- **Chiptune lead:** ``square`` + ``none`` -- raw 8-bit.
|
||||
- **Film strings:** ``triangle`` + ``strings`` -- soft, bowed, organic.
|
||||
- **Sub bass:** ``sine`` + ``pluck`` with lowpass -- deep and round.
|
||||
- **Retro synth:** ``pwm_slow`` + ``pad`` -- Juno-style analog warmth.
|
||||
- **Percussive hit:** ``noise`` + ``staccato`` with lowpass -- snare layer.
|
||||
- **E-piano ballad:** ``fm`` + ``piano`` with reverb -- intimate jazz club.
|
||||
|
||||
Some practical combos worth memorizing:
|
||||
|
||||
- ``saw`` + ``staccato`` + legato = **acid 303 line.** Add a resonant
|
||||
lowpass and some glide and you're in a warehouse in 1988.
|
||||
- ``fm`` + ``bell`` = **jazz vibraphone.** The glassy, harmonic-rich
|
||||
attack with a long ring-out. Add reverb for a late-night club feel.
|
||||
- ``supersaw`` + ``pad`` = **ambient wash.** The slow attack lets the
|
||||
detuned oscillators build into a shimmering wall. Add chorus and
|
||||
long reverb and you're scoring a nature documentary.
|
||||
- ``saw`` + ``pluck`` = **funk stab.** Short, sharp, bright. The
|
||||
sound of Nile Rodgers' right hand.
|
||||
@@ -0,0 +1,388 @@
|
||||
Musical Systems
|
||||
===============
|
||||
|
||||
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
|
||||
-------
|
||||
|
||||
The standard 12-tone equal temperament system — the common language of
|
||||
European classical music, American popular music, and virtually all
|
||||
commercially recorded music since the early 20th century. Its
|
||||
major/minor tonality system, seven diatonic modes, and rich harmonic
|
||||
vocabulary form the foundation that most listeners around the world
|
||||
grew up hearing. If you've ever hummed along to a pop song, played
|
||||
piano, or picked up a guitar, you've been working within this system.
|
||||
|
||||
.. code-block:: pycon
|
||||
|
||||
>>> from pytheory import TonedScale
|
||||
|
||||
>>> c = TonedScale(tonic="C4")
|
||||
>>> c["major"].note_names
|
||||
['C', 'D', 'E', 'F', 'G', 'A', 'B', 'C']
|
||||
|
||||
>>> c["dorian"].note_names
|
||||
['C', 'D', 'Eb', 'F', 'G', 'A', 'Bb', 'C']
|
||||
|
||||
**Scales:** major, minor, harmonic minor, ionian, dorian, phrygian,
|
||||
lydian, mixolydian, aeolian, locrian, chromatic
|
||||
|
||||
Indian Classical (Hindustani)
|
||||
-----------------------------
|
||||
|
||||
One of the oldest and most sophisticated musical traditions on earth,
|
||||
`Hindustani classical music <https://en.wikipedia.org/wiki/Hindustani_classical_music>`_
|
||||
dates back over two thousand years to the *Natya Shastra*. Where Western
|
||||
music emphasizes harmony (chords), Indian music emphasizes *raga* —
|
||||
melodic frameworks that evoke specific moods, times of day, and seasons.
|
||||
The sound is meditative, ornamental, and deeply expressive. You'll hear
|
||||
it in classical sitar and tabla performances, Bollywood film scores,
|
||||
and the improvisatory traditions that influenced musicians from
|
||||
George Harrison to John Coltrane.
|
||||
|
||||
The Hindustani system uses **swaras** (Sa, Re, Ga, Ma, Pa, Dha, Ni) and
|
||||
organizes scales into `thaats <https://en.wikipedia.org/wiki/Thaat>`_ — the 10 parent scales from which `ragas <https://en.wikipedia.org/wiki/Raga>`_
|
||||
are derived.
|
||||
|
||||
.. code-block:: pycon
|
||||
|
||||
>>> from pytheory import TonedScale
|
||||
|
||||
>>> sa = TonedScale(tonic="Sa4", system="indian")
|
||||
|
||||
>>> sa["bilawal"].note_names # = major scale
|
||||
['Sa', 'Re', 'Ga', 'Ma', 'Pa', 'Dha', 'Ni', 'Sa']
|
||||
|
||||
>>> sa["bhairav"].note_names # unique to Indian music
|
||||
['Sa', 'komal Re', 'Ga', 'Ma', 'Pa', 'komal Dha', 'Ni', 'Sa']
|
||||
|
||||
>>> sa["todi"].note_names
|
||||
['Sa', 'komal Re', 'komal Ga', 'tivra Ma', 'Pa', 'komal Dha', 'Ni', 'Sa']
|
||||
|
||||
**Thaats:** bilawal, khamaj, kafi, asavari, bhairavi, kalyan, bhairav,
|
||||
poorvi, marwa, todi
|
||||
|
||||
**Swara notation:**
|
||||
|
||||
- Uppercase = shuddha (natural): Sa, Re, Ga, Ma, Pa, Dha, Ni
|
||||
- ``komal`` prefix = flat: komal Re, komal Ga, komal Dha, komal Ni
|
||||
- ``tivra`` prefix = sharp: tivra Ma
|
||||
|
||||
Arabic Maqam
|
||||
------------
|
||||
|
||||
The `maqam <https://en.wikipedia.org/wiki/Arabic_maqam>`_ tradition spans
|
||||
the entire Arab world, Turkey, Iran, and Central Asia — a vast musical
|
||||
heritage stretching from medieval Andalusia to modern Cairo. Maqam music
|
||||
is melodically rich, often featuring microtonal inflections, elaborate
|
||||
ornamentation, and a sense of yearning that's unmistakable once you've
|
||||
heard it. Think of the oud-driven classical traditions of Umm Kulthum
|
||||
and Fairuz, the call to prayer, Sufi devotional music, and the
|
||||
underpinning of much Middle Eastern and North African popular music
|
||||
today.
|
||||
|
||||
The Arabic system uses **solfège-based names** (Do, Re, Mi, Fa, Sol, La, Si)
|
||||
and organizes scales into **maqamat** (plural of `maqam <https://en.wikipedia.org/wiki/Maqam>`_).
|
||||
|
||||
.. note::
|
||||
|
||||
True maqam music uses quarter-tones that cannot be represented in
|
||||
12-tone equal temperament. These scales are the closest 12-TET
|
||||
approximations.
|
||||
|
||||
.. code-block:: pycon
|
||||
|
||||
>>> from pytheory import TonedScale
|
||||
|
||||
>>> do = TonedScale(tonic="Do4", system="arabic")
|
||||
|
||||
>>> do["ajam"].note_names # = major scale
|
||||
['Do', 'Re', 'Mi', 'Fa', 'Sol', 'La', 'Si', 'Do']
|
||||
|
||||
>>> do["hijaz"].note_names # characteristic augmented 2nd
|
||||
['Do', 'Reb', 'Mi', 'Fa', 'Sol', 'Solb', 'Sib', 'Do']
|
||||
|
||||
>>> do["nikriz"].note_names
|
||||
['Do', 'Re', 'Mib', 'Fa#', 'Sol', 'La', 'Sib', 'Do']
|
||||
|
||||
**Maqamat:** ajam, nahawand, kurd, hijaz, nikriz, bayati, rast, saba,
|
||||
sikah, jiharkah
|
||||
|
||||
Japanese
|
||||
--------
|
||||
|
||||
Japan's traditional scales have a hauntingly beautiful quality that is
|
||||
immediately recognizable — dark, sparse, and full of tension. The
|
||||
pentatonic scales (especially *hirajoshi* and *in*) use semitone steps
|
||||
that give them an unmistakably Japanese character, distinct from the
|
||||
wider-spaced pentatonics found in Chinese and Western folk music.
|
||||
You'll hear these scales in `koto <https://en.wikipedia.org/wiki/Koto_(instrument)>`_
|
||||
and `shamisen <https://en.wikipedia.org/wiki/Shamisen>`_ music, the
|
||||
`gagaku <https://en.wikipedia.org/wiki/Gagaku>`_ court orchestra,
|
||||
Kabuki and Noh theater, taiko drumming, anime and video game
|
||||
soundtracks, and the compositions of Toru Takemitsu.
|
||||
|
||||
The Japanese system uses Western note names with traditional pentatonic
|
||||
and heptatonic scales from Japanese music.
|
||||
|
||||
.. code-block:: pycon
|
||||
|
||||
>>> from pytheory import TonedScale
|
||||
|
||||
>>> c = TonedScale(tonic="C4", system="japanese")
|
||||
|
||||
>>> c["hirajoshi"].note_names # most iconic Japanese scale
|
||||
['C', 'D', 'Eb', 'G', 'Ab', 'C']
|
||||
|
||||
>>> c["in"].note_names # Miyako-bushi, used in koto music
|
||||
['C', 'Db', 'F', 'G', 'Ab', 'C']
|
||||
|
||||
>>> c["yo"].note_names # folk music scale
|
||||
['C', 'D', 'F', 'G', 'A#', 'C']
|
||||
|
||||
>>> c["ritsu"].note_names # gagaku court music (= Dorian)
|
||||
['C', 'D', 'Eb', 'F', 'G', 'A', 'Bb', 'C']
|
||||
|
||||
**Pentatonic scales:** hirajoshi, in, yo, iwato, kumoi, insen
|
||||
|
||||
**Heptatonic scales:** ritsu, ryo
|
||||
|
||||
Blues and Pentatonic
|
||||
--------------------
|
||||
|
||||
The blues is America's deepest musical root — born from the African
|
||||
American experience in the Mississippi Delta, it gave rise to jazz,
|
||||
rock and roll, R&B, soul, funk, and hip-hop. The *blue note* (a
|
||||
flattened 5th that bends between major and minor) is the sound of
|
||||
emotional truth in music, from Robert Johnson to B.B. King to
|
||||
Jimi Hendrix.
|
||||
|
||||
The blues system provides the scales foundational to blues, rock, jazz,
|
||||
and folk music worldwide. `Pentatonic scales <https://en.wikipedia.org/wiki/Pentatonic_scale>`_ (5 notes) are the oldest
|
||||
known musical scales, found independently in cultures across every
|
||||
continent.
|
||||
|
||||
The `blues scale <https://en.wikipedia.org/wiki/Blues_scale>`_ adds the "`blue note <https://en.wikipedia.org/wiki/Blue_note>`_" (flat 5th / sharp 4th) to the
|
||||
minor pentatonic — this chromatic passing tone is the defining sound
|
||||
of the blues.
|
||||
|
||||
.. code-block:: pycon
|
||||
|
||||
>>> from pytheory import TonedScale
|
||||
|
||||
>>> c = TonedScale(tonic="C4", system="blues")
|
||||
|
||||
>>> c["major pentatonic"].note_names # the "happy" pentatonic
|
||||
['C', 'D', 'E', 'G', 'A', 'C']
|
||||
|
||||
>>> c["minor pentatonic"].note_names # the "sad" pentatonic
|
||||
['C', 'D#', 'F', 'G', 'A#', 'C']
|
||||
|
||||
>>> c["blues"].note_names # minor pentatonic + blue note
|
||||
['C', 'Eb', 'F', 'Gb', 'G', 'Bb', 'C']
|
||||
|
||||
>>> c["major blues"].note_names # major pentatonic + blue note
|
||||
['C', 'D', 'Eb', 'E', 'G', 'A', 'C']
|
||||
|
||||
**Pentatonic:** major pentatonic, minor pentatonic
|
||||
|
||||
**Hexatonic:** blues, major blues
|
||||
|
||||
**Heptatonic:** dominant (Mixolydian — the dominant 7th sound),
|
||||
minor (Dorian — the jazz minor sound)
|
||||
|
||||
|
||||
Javanese Gamelan
|
||||
----------------
|
||||
|
||||
`Gamelan <https://en.wikipedia.org/wiki/Gamelan>`_ is the shimmering,
|
||||
interlocking percussion orchestra of Java and Bali — one of the most
|
||||
otherworldly sounds in all of music. Ensembles of bronze metallophones,
|
||||
gongs, drums, and bamboo flutes create waves of resonance that
|
||||
influenced Claude Debussy, Steve Reich, and countless ambient and
|
||||
electronic artists. Each gamelan is tuned uniquely, so no two
|
||||
ensembles sound exactly alike. The music is communal, ceremonial, and
|
||||
deeply tied to Javanese and Balinese culture — it accompanies
|
||||
shadow puppet theater (*wayang*), dance, and religious ritual.
|
||||
|
||||
The gamelan system approximates the scales of the Javanese and Balinese
|
||||
gamelan orchestra in 12-tone equal temperament. True gamelan tuning is
|
||||
unique to each ensemble and does not conform to Western intonation —
|
||||
these are the closest 12-TET approximations.
|
||||
|
||||
`Slendro <https://en.wikipedia.org/wiki/Slendro>`_ is a roughly equal 5-tone division of the octave, producing
|
||||
an ethereal, floating quality. `Pelog <https://en.wikipedia.org/wiki/Pelog>`_ is a 7-tone scale with unequal
|
||||
intervals, typically performed using 5-note subsets called *pathet*.
|
||||
|
||||
.. code-block:: pycon
|
||||
|
||||
>>> from pytheory import TonedScale
|
||||
|
||||
>>> ji = TonedScale(tonic="ji4", system="gamelan")
|
||||
|
||||
>>> ji["slendro"].note_names # the 5-tone equidistant scale
|
||||
['ji', 'ro', 'pat', 'mo', 'pi', 'ji']
|
||||
|
||||
>>> ji["pelog"].note_names # full 7-tone pelog
|
||||
['ji', 'ro-', 'lu', 'pat', 'mo', 'nem-', 'barang', 'ji']
|
||||
|
||||
>>> ji["pelog nem"].note_names # pathet nem subset
|
||||
['ji', 'ro-', 'lu', 'pat', 'mo', 'ji']
|
||||
|
||||
**Pentatonic:** slendro, pelog nem, pelog barang, pelog lima
|
||||
|
||||
**Heptatonic:** pelog (full 7-tone)
|
||||
|
||||
.. note::
|
||||
|
||||
Gamelan tone names follow Javanese numbering: ji (1), ro (2),
|
||||
lu (3), pat (4), mo (5), nem (6), pi/barang (7). Suffixes
|
||||
indicate microtonal variants approximated to the nearest semitone.
|
||||
|
||||
|
||||
Cross-System Comparison
|
||||
-----------------------
|
||||
|
||||
Since all systems use 12-tone equal temperament, equivalent scales
|
||||
produce the same pitches:
|
||||
|
||||
.. code-block:: pycon
|
||||
|
||||
>>> from pytheory import TonedScale, Tone
|
||||
|
||||
>>> # These are all the same scale with different names
|
||||
>>> western = TonedScale(tonic="C4")["major"]
|
||||
>>> indian = TonedScale(tonic="Sa4", system="indian")["bilawal"]
|
||||
>>> arabic = TonedScale(tonic="Do4", system="arabic")["ajam"]
|
||||
|
||||
>>> # Same pitches
|
||||
>>> c4 = Tone.from_string("C4", system="western")
|
||||
>>> sa4 = Tone.from_string("Sa4", system="indian")
|
||||
>>> do4 = Tone.from_string("Do4", system="arabic")
|
||||
|
||||
>>> c4.frequency
|
||||
261.6255653005986
|
||||
>>> sa4.frequency
|
||||
261.6255653005986
|
||||
>>> 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.
|
||||
@@ -0,0 +1,345 @@
|
||||
Music Theory Fundamentals
|
||||
=========================
|
||||
|
||||
This page covers the essential concepts of music theory — the framework
|
||||
behind everything PyTheory does. Don't worry if you're new to this:
|
||||
music theory isn't a set of rules you have to memorize, it's a
|
||||
vocabulary for describing what you already hear. Every concept below
|
||||
connects to something you've felt while listening to music — this page
|
||||
just gives it a name.
|
||||
|
||||
Sound and Pitch
|
||||
---------------
|
||||
|
||||
All sound is vibration. When an object vibrates, it pushes air molecules
|
||||
back and forth, creating pressure waves that travel to your ears. The
|
||||
speed of this vibration — measured in cycles per second
|
||||
(`Hertz <https://en.wikipedia.org/wiki/Hertz>`_, Hz) — determines the
|
||||
`pitch <https://en.wikipedia.org/wiki/Pitch_(music)>`_ you hear.
|
||||
|
||||
- **20 Hz**: the lowest pitch most humans can hear
|
||||
- **60–250 Hz**: the range of the human voice (speaking)
|
||||
- **261.63 Hz**: `middle C <https://en.wikipedia.org/wiki/C_(musical_note)#Middle_C>`_ (C4)
|
||||
- **440 Hz**: the `concert pitch <https://en.wikipedia.org/wiki/Concert_pitch>`_ tuning standard A (A4)
|
||||
- **4186 Hz**: the highest C on a piano (C8)
|
||||
- **20,000 Hz**: the upper limit of `human hearing <https://en.wikipedia.org/wiki/Hearing_range>`_
|
||||
|
||||
The relationship between pitch and frequency is **logarithmic** — each
|
||||
`octave <https://en.wikipedia.org/wiki/Octave>`_ doubles the frequency.
|
||||
This means the distance from A3 (220 Hz) to A4 (440 Hz) is 220 Hz, but
|
||||
the distance from A4 to A5 (880 Hz) is 440 Hz. Both sound like "one
|
||||
octave" to our ears.
|
||||
|
||||
Why Twelve Notes?
|
||||
-----------------
|
||||
|
||||
The Western `chromatic scale <https://en.wikipedia.org/wiki/Chromatic_scale>`_
|
||||
has 12 notes per octave. This isn't arbitrary — it emerges from the
|
||||
physics of vibrating strings and air columns.
|
||||
|
||||
The `harmonic series <https://en.wikipedia.org/wiki/Harmonic_series_(music)>`_
|
||||
is the sequence of frequencies produced when a string vibrates: f, 2f,
|
||||
3f, 4f, 5f... The relationships between these harmonics create the
|
||||
intervals we perceive as `consonant <https://en.wikipedia.org/wiki/Consonance_and_dissonance>`_:
|
||||
|
||||
- 2:1 = `octave <https://en.wikipedia.org/wiki/Octave>`_ (the most fundamental)
|
||||
- 3:2 = `perfect fifth <https://en.wikipedia.org/wiki/Perfect_fifth>`_
|
||||
- 4:3 = `perfect fourth <https://en.wikipedia.org/wiki/Perfect_fourth>`_
|
||||
- 5:4 = `major third <https://en.wikipedia.org/wiki/Major_third>`_
|
||||
- 6:5 = `minor third <https://en.wikipedia.org/wiki/Minor_third>`_
|
||||
|
||||
If you stack perfect fifths (multiply by 3/2 repeatedly) and reduce to
|
||||
within one octave, you get 12 roughly evenly-spaced notes before the
|
||||
cycle almost closes. The tiny gap where it doesn't close perfectly is
|
||||
the `Pythagorean comma <https://en.wikipedia.org/wiki/Pythagorean_comma>`_
|
||||
— the reason we need `temperament <https://en.wikipedia.org/wiki/Musical_temperament>`_.
|
||||
|
||||
.. code-block:: pycon
|
||||
|
||||
>>> from pytheory import Tone
|
||||
|
||||
>>> c = Tone.from_string("C4", system="western")
|
||||
>>> [t.name for t in c.circle_of_fifths()]
|
||||
['C', 'G', 'D', 'A', 'E', 'B', 'F#', 'C#', 'G#', 'D#', 'A#', 'F']
|
||||
|
||||
Other cultures divide the octave differently: Indonesian
|
||||
`gamelan <https://en.wikipedia.org/wiki/Gamelan>`_ uses 5 or 7 unequal
|
||||
divisions; Indian classical music theoretically has 22
|
||||
`shrutis <https://en.wikipedia.org/wiki/Shruti_(music)>`_ (microtones);
|
||||
Arabic `maqam <https://en.wikipedia.org/wiki/Maqam>`_ uses
|
||||
`quarter-tones <https://en.wikipedia.org/wiki/Quarter_tone>`_.
|
||||
|
||||
Intervals: The Atoms of Music
|
||||
------------------------------
|
||||
|
||||
An `interval <https://en.wikipedia.org/wiki/Interval_(music)>`_ is the
|
||||
distance between two pitches. Intervals are the building blocks of
|
||||
everything — melodies are sequences of intervals, chords are stacks
|
||||
of intervals, and scales are patterns of intervals.
|
||||
|
||||
Every interval has two properties:
|
||||
|
||||
**Size** (how many scale steps)::
|
||||
|
||||
Unison → 2nd → 3rd → 4th → 5th → 6th → 7th → Octave
|
||||
|
||||
**Quality** (exact number of semitones)::
|
||||
|
||||
Perfect: unison (0), 4th (5), 5th (7), octave (12)
|
||||
Major: 2nd (2), 3rd (4), 6th (9), 7th (11)
|
||||
Minor: 2nd (1), 3rd (3), 6th (8), 7th (10)
|
||||
Augmented: one semitone larger than perfect or major
|
||||
Diminished: one semitone smaller than perfect or minor
|
||||
|
||||
The "`perfect <https://en.wikipedia.org/wiki/Perfect_fifth>`_" intervals
|
||||
(unison, 4th, 5th, octave) are called perfect because they appear in
|
||||
both major AND minor scales unchanged. They've been considered consonant
|
||||
across virtually all musical cultures throughout history.
|
||||
|
||||
The `tritone <https://en.wikipedia.org/wiki/Tritone>`_ (augmented 4th /
|
||||
diminished 5th = 6 semitones) divides the octave exactly in half.
|
||||
Medieval theorists called it *diabolus in musica* ("the devil in music")
|
||||
because of its extreme instability. Today it's the foundation of
|
||||
`dominant harmony <https://en.wikipedia.org/wiki/Dominant_(music)>`_
|
||||
and the `blues <https://en.wikipedia.org/wiki/Blue_note>`_.
|
||||
|
||||
Keys and Key Signatures
|
||||
-----------------------
|
||||
|
||||
A `key <https://en.wikipedia.org/wiki/Key_(music)>`_ is a group of
|
||||
notes that form the tonal center of a piece. The key of C major uses
|
||||
only the white keys on the piano: C D E F G A B. The key of G major
|
||||
uses the same notes except F becomes F#.
|
||||
|
||||
`Key signatures <https://en.wikipedia.org/wiki/Key_signature>`_ tell
|
||||
you which notes are sharped or flatted throughout a piece. They follow
|
||||
the `circle of fifths <https://en.wikipedia.org/wiki/Circle_of_fifths>`_:
|
||||
|
||||
**Sharp keys** (add one sharp per step clockwise)::
|
||||
|
||||
C major: no sharps or flats
|
||||
G major: F#
|
||||
D major: F# C#
|
||||
A major: F# C# G#
|
||||
E major: F# C# G# D#
|
||||
B major: F# C# G# D# A#
|
||||
|
||||
**Flat keys** (add one flat per step counter-clockwise)::
|
||||
|
||||
C major: no sharps or flats
|
||||
F major: Bb
|
||||
Bb major: Bb Eb
|
||||
Eb major: Bb Eb Ab
|
||||
Ab major: Bb Eb Ab Db
|
||||
Db major: Bb Eb Ab Db Gb
|
||||
|
||||
The order of sharps is always F C G D A E B (Father Charles Goes Down
|
||||
And Ends Battle). The order of flats is the reverse: B E A D G C F.
|
||||
|
||||
Harmony: How Chords Work
|
||||
-------------------------
|
||||
|
||||
`Harmony <https://en.wikipedia.org/wiki/Harmony>`_ is the art of
|
||||
combining tones simultaneously. While
|
||||
`melody <https://en.wikipedia.org/wiki/Melody>`_ is horizontal (tones
|
||||
in sequence), harmony is vertical (tones stacked).
|
||||
|
||||
The simplest harmony is the `triad <https://en.wikipedia.org/wiki/Triad_(music)>`_
|
||||
— three notes built by stacking `thirds <https://en.wikipedia.org/wiki/Third_(music)>`_.
|
||||
The quality of each third determines the chord type:
|
||||
|
||||
- **Major triad** = major 3rd + minor 3rd (e.g. C-E-G)
|
||||
- **Minor triad** = minor 3rd + major 3rd (e.g. C-Eb-G)
|
||||
- `Diminished triad <https://en.wikipedia.org/wiki/Diminished_triad>`_ = minor 3rd + minor 3rd (e.g. B-D-F)
|
||||
- `Augmented triad <https://en.wikipedia.org/wiki/Augmented_triad>`_ = major 3rd + major 3rd (e.g. C-E-G#)
|
||||
|
||||
In any major key, the triads built on each
|
||||
`scale degree <https://en.wikipedia.org/wiki/Degree_(music)>`_ always
|
||||
follow the same pattern::
|
||||
|
||||
Degree Quality Function
|
||||
I Major Tonic (home)
|
||||
ii Minor Pre-dominant
|
||||
iii Minor Tonic substitute
|
||||
IV Major Subdominant (departure)
|
||||
V Major Dominant (tension, wants to go home)
|
||||
vi Minor Tonic substitute, relative minor
|
||||
vii° Diminished Dominant substitute (leading tone chord)
|
||||
|
||||
This pattern is the DNA of Western harmony. Pop songs, classical
|
||||
sonatas, jazz standards, and church hymns all derive from it.
|
||||
|
||||
Functional Harmony
|
||||
~~~~~~~~~~~~~~~~~~
|
||||
|
||||
Chords don't just have names — they have
|
||||
`functions <https://en.wikipedia.org/wiki/Function_(music)>`_:
|
||||
|
||||
- **Tonic function** (I, iii, vi): stability, rest, home
|
||||
- **Subdominant function** (ii, IV): motion away from home
|
||||
- **Dominant function** (V, vii°): tension, desire to return home
|
||||
|
||||
The most fundamental progression in Western music is **T → S → D → T**
|
||||
(tonic → subdominant → dominant → tonic). The classic
|
||||
`I-IV-V-I <https://en.wikipedia.org/wiki/I%E2%80%93IV%E2%80%93V%E2%80%93I>`_
|
||||
is exactly this pattern. Every "Louie Louie" and every
|
||||
`Bach chorale <https://en.wikipedia.org/wiki/Bach_chorale>`_ follows
|
||||
this basic tonal gravity.
|
||||
|
||||
.. code-block:: pycon
|
||||
|
||||
>>> from pytheory import TonedScale
|
||||
|
||||
>>> scale = TonedScale(tonic="C4")["major"]
|
||||
|
||||
>>> scale.triad(0).identify()
|
||||
'C major'
|
||||
>>> scale.triad(3).identify()
|
||||
'F major'
|
||||
>>> scale.triad(4).identify()
|
||||
'G major'
|
||||
|
||||
The Dominant Seventh
|
||||
~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
The most important chord in `tonal music <https://en.wikipedia.org/wiki/Tonality>`_
|
||||
is the `dominant seventh <https://en.wikipedia.org/wiki/Dominant_seventh_chord>`_
|
||||
— the V7 chord. In C major, this is G-B-D-F. It contains:
|
||||
|
||||
- A `leading tone <https://en.wikipedia.org/wiki/Leading-tone>`_ (B) that pulls up to the tonic (C) by half step
|
||||
- A `tritone <https://en.wikipedia.org/wiki/Tritone>`_ (B-F) that wants to resolve inward (B→C, F→E)
|
||||
- The `dominant note <https://en.wikipedia.org/wiki/Dominant_(music)>`_ (G) that falls to the tonic by a fifth
|
||||
|
||||
This combination creates the strongest possible pull toward
|
||||
`resolution <https://en.wikipedia.org/wiki/Resolution_(music)>`_.
|
||||
When you hear V7→I, you feel arrival.
|
||||
|
||||
.. code-block:: pycon
|
||||
|
||||
>>> from pytheory import Chord, Tone
|
||||
|
||||
>>> C4 = Tone.from_string("C4", system="western")
|
||||
>>> G4 = Tone.from_string("G4", system="western")
|
||||
|
||||
>>> g7 = Chord([G4, G4+4, G4+7, G4+10])
|
||||
>>> g7.identify()
|
||||
'G dominant 7th'
|
||||
>>> g7.tension['has_dominant_function']
|
||||
True
|
||||
>>> g7.tension['tritones']
|
||||
1
|
||||
|
||||
>>> c_major = Chord([C4, C4+4, C4+7])
|
||||
>>> c_major.tension['score']
|
||||
0.0
|
||||
|
||||
Rhythm and Meter
|
||||
----------------
|
||||
|
||||
While PyTheory focuses on pitch,
|
||||
`rhythm <https://en.wikipedia.org/wiki/Rhythm>`_ is the other half
|
||||
of music.
|
||||
|
||||
**Rhythm** is the pattern of durations.
|
||||
`Meter <https://en.wikipedia.org/wiki/Metre_(music)>`_ is the recurring
|
||||
pattern of strong and weak beats that organizes rhythm.
|
||||
|
||||
- `4/4 time <https://en.wikipedia.org/wiki/Time_signature#Simple_time_signatures>`_: the most common meter. Strong-weak-medium-weak.
|
||||
Used in rock, pop, hip-hop, most Western music.
|
||||
- `3/4 time <https://en.wikipedia.org/wiki/Triple_metre>`_: waltz time. Strong-weak-weak. A lilting, circular feel.
|
||||
- `6/8 time <https://en.wikipedia.org/wiki/Compound_meter_(music)>`_: compound duple. Two groups of three. Irish jigs, many
|
||||
ballads.
|
||||
- `12/8 time <https://en.wikipedia.org/wiki/Compound_meter_(music)>`_: compound quadruple. Four groups of three. Slow blues,
|
||||
doo-wop, gospel. Has a triplet feel over a 4/4 pulse — the shuffle
|
||||
groove of "Stormy Monday" and "Oh! Darling."
|
||||
- 5/4 time: asymmetric. "`Take Five <https://en.wikipedia.org/wiki/Take_Five>`_"
|
||||
by Dave Brubeck. Creates constant forward momentum because it never
|
||||
fully settles.
|
||||
- `7/8 time <https://en.wikipedia.org/wiki/Additive_rhythm_and_divisive_rhythm>`_: common in Balkan folk music. Often felt as 2+2+3 or
|
||||
3+2+2.
|
||||
|
||||
The Physics of Consonance
|
||||
-------------------------
|
||||
|
||||
Why do some intervals sound "good" and others "bad"? The answer lies
|
||||
in the physics of sound waves and the
|
||||
`Plomp-Levelt <https://en.wikipedia.org/wiki/Consonance_and_dissonance#Physiological_basis>`_
|
||||
model of sensory dissonance.
|
||||
|
||||
When two frequencies are related by a simple ratio (like 3:2 for a
|
||||
perfect fifth), their waveforms align regularly. The combined wave
|
||||
is smooth and periodic — the brain perceives this as consonant.
|
||||
|
||||
When two frequencies are related by a complex ratio (like 45:32 for
|
||||
a tritone), their waveforms rarely align. The combined wave is
|
||||
irregular and the brain perceives
|
||||
`roughness <https://en.wikipedia.org/wiki/Roughness_(psychoacoustics)>`_
|
||||
— dissonance.
|
||||
|
||||
But `consonance and dissonance <https://en.wikipedia.org/wiki/Consonance_and_dissonance>`_
|
||||
are also cultural. The
|
||||
`major third <https://en.wikipedia.org/wiki/Major_third>`_ (5:4) was
|
||||
considered dissonant in medieval European music but consonant since the
|
||||
Renaissance. The tritone was forbidden in church music but is the
|
||||
foundation of blues and jazz. Indonesian gamelan embraces
|
||||
`beating <https://en.wikipedia.org/wiki/Beat_(acoustics)>`_ between
|
||||
paired instruments as a core aesthetic.
|
||||
|
||||
.. code-block:: pycon
|
||||
|
||||
>>> from pytheory import Chord, Tone
|
||||
|
||||
>>> C4 = Tone.from_string("C4", system="western")
|
||||
>>> E4 = Tone.from_string("E4", system="western")
|
||||
>>> G4 = Tone.from_string("G4", system="western")
|
||||
|
||||
>>> [round(f, 2) for f in C4.overtones(6)]
|
||||
[261.63, 523.25, 784.88, 1046.5, 1308.13, 1569.75]
|
||||
|
||||
>>> fifth = Chord([C4, G4])
|
||||
>>> tritone = Chord([C4, C4 + 6])
|
||||
>>> fifth.harmony > tritone.harmony
|
||||
True
|
||||
|
||||
>>> octave = Chord([C4, C4 + 12])
|
||||
>>> third = Chord([C4, E4])
|
||||
>>> octave.dissonance < third.dissonance
|
||||
True
|
||||
|
||||
>>> c_major = Chord([C4, E4, G4])
|
||||
>>> c_major.tension['score']
|
||||
0.0
|
||||
|
||||
>>> g7 = Chord([G4, G4+4, G4+7, G4+10])
|
||||
>>> g7.tension['score']
|
||||
0.6
|
||||
>>> g7.tension['tritones']
|
||||
1
|
||||
>>> g7.tension['has_dominant_function']
|
||||
True
|
||||
|
||||
From Theory to Composition
|
||||
--------------------------
|
||||
|
||||
Everything on this page — tones, intervals, chords, scales, keys — is
|
||||
the foundation. But PyTheory goes further: you can use these building
|
||||
blocks to compose and play actual music. See the :doc:`sequencing`
|
||||
guide to learn how to arrange multi-part scores with melodies, chord
|
||||
pads, bass lines, drum patterns, and audio effects — all driven by the
|
||||
theory concepts you've just learned.
|
||||
|
||||
Further Reading
|
||||
---------------
|
||||
|
||||
- `Music theory <https://en.wikipedia.org/wiki/Music_theory>`_ — Wikipedia overview
|
||||
- `Equal temperament <https://en.wikipedia.org/wiki/Equal_temperament>`_ — the modern tuning system
|
||||
- `Circle of fifths <https://en.wikipedia.org/wiki/Circle_of_fifths>`_ — key relationships
|
||||
- `Chord progression <https://en.wikipedia.org/wiki/Chord_progression>`_ — common patterns
|
||||
- `Voice leading <https://en.wikipedia.org/wiki/Voice_leading>`_ — smooth chord connections
|
||||
- `Raga <https://en.wikipedia.org/wiki/Raga>`_ — Indian melodic framework
|
||||
- `Maqam <https://en.wikipedia.org/wiki/Maqam>`_ — Arabic melodic system
|
||||
- `Gamelan <https://en.wikipedia.org/wiki/Gamelan>`_ — Indonesian ensemble music
|
||||
- `Blues <https://en.wikipedia.org/wiki/Blues>`_ — the foundation of American popular music
|
||||
- `Twelve-bar blues <https://en.wikipedia.org/wiki/Twelve-bar_blues>`_ — the most common blues form
|
||||
|
||||
Theory is just a vocabulary for what you already hear. You don't need it to make music -- but once you have the words, you can talk about what you're doing, understand why it works, and find new places to go.
|
||||
+400
-31
@@ -2,88 +2,284 @@ Working with Tones
|
||||
==================
|
||||
|
||||
A :class:`~pytheory.tones.Tone` represents a single musical note, optionally
|
||||
with an octave number (scientific pitch notation).
|
||||
with an octave number in `scientific pitch notation <https://en.wikipedia.org/wiki/Scientific_pitch_notation>`_ (e.g. C4 = middle C).
|
||||
|
||||
What is a Tone?
|
||||
---------------
|
||||
|
||||
A musical tone is a sound with a definite pitch — a periodic vibration at
|
||||
a specific frequency. In the Western 12-tone system, the octave (a 2:1
|
||||
frequency ratio) is divided into 12 equal steps called **semitones** or
|
||||
**half steps**. Two semitones make a **whole step** (whole tone).
|
||||
|
||||
The 12 chromatic tones are::
|
||||
|
||||
C C#/Db D D#/Eb E F F#/Gb G G#/Ab A A#/Bb B
|
||||
|
||||
Notes with two names (like C# and Db) are `enharmonic equivalents <https://en.wikipedia.org/wiki/Enharmonic>`_ —
|
||||
different names for the same pitch. Whether you call it C# or Db depends
|
||||
on the musical context (key signature, harmonic function).
|
||||
|
||||
Scientific Pitch Notation
|
||||
-------------------------
|
||||
|
||||
Each tone can be assigned an octave number. The standard is **scientific
|
||||
pitch notation**, where the octave number increments at C::
|
||||
|
||||
... B3 C4 C#4 D4 ... A4 B4 C5 C#5 ...
|
||||
^ ^
|
||||
middle C one octave up
|
||||
|
||||
Key reference points:
|
||||
|
||||
- `A4 = 440 Hz <https://en.wikipedia.org/wiki/A440_(pitch_standard)>`_ — the international tuning standard (ISO 16)
|
||||
- **C4 = 261.63 Hz** — middle C on the piano
|
||||
- **A0 = 27.5 Hz** — the lowest A on a standard piano
|
||||
- **C8 = 4186 Hz** — the highest C on a standard piano
|
||||
|
||||
Creating Tones
|
||||
--------------
|
||||
|
||||
.. code-block:: python
|
||||
.. code-block:: pycon
|
||||
|
||||
from pytheory import Tone
|
||||
>>> from pytheory import Tone
|
||||
|
||||
# From a string
|
||||
c4 = Tone.from_string("C4")
|
||||
cs4 = Tone.from_string("C#4")
|
||||
>>> c4 = Tone.from_string("C4")
|
||||
>>> cs4 = Tone.from_string("C#4")
|
||||
>>> db4 = Tone.from_string("Db4")
|
||||
|
||||
# Direct construction
|
||||
d = Tone(name="D", octave=3)
|
||||
>>> d = Tone(name="D", octave=3)
|
||||
|
||||
# With a specific system
|
||||
a4 = Tone.from_string("A4", system="western")
|
||||
>>> a4 = Tone.from_string("A4", system="western")
|
||||
|
||||
>>> Tone.from_frequency(440)
|
||||
<Tone A4>
|
||||
>>> Tone.from_frequency(261.63)
|
||||
<Tone C4>
|
||||
|
||||
>>> Tone.from_midi(60)
|
||||
<Tone C4>
|
||||
>>> Tone.from_midi(69)
|
||||
<Tone A4>
|
||||
|
||||
Properties
|
||||
----------
|
||||
|
||||
.. code-block:: python
|
||||
.. code-block:: pycon
|
||||
|
||||
>>> c4 = Tone.from_string("C4")
|
||||
>>> c4 = Tone.from_string("C4", system="western")
|
||||
>>> c4.name
|
||||
'C'
|
||||
>>> c4.octave
|
||||
4
|
||||
>>> c4.full_name
|
||||
'C4'
|
||||
>>> str(c4)
|
||||
'C4'
|
||||
>>> c4.letter
|
||||
'C'
|
||||
>>> c4.midi
|
||||
60
|
||||
>>> c4.exists
|
||||
True
|
||||
|
||||
Pitch and Frequency
|
||||
-------------------
|
||||
|
||||
.. code-block:: python
|
||||
Every tone vibrates at a specific frequency measured in Hertz (Hz —
|
||||
cycles per second). The relationship between pitch and frequency is
|
||||
**logarithmic**: each octave doubles the frequency, and each semitone
|
||||
multiplies by the 12th root of 2 (~1.05946).
|
||||
|
||||
.. code-block:: pycon
|
||||
|
||||
>>> a4 = Tone.from_string("A4", system="western")
|
||||
>>> a4.frequency
|
||||
440.0
|
||||
>>> a4.pitch()
|
||||
440.0
|
||||
|
||||
# Different temperaments
|
||||
>>> Tone.from_string("A3", system="western").frequency
|
||||
220.0
|
||||
|
||||
>>> Tone.from_string("C4", system="western").frequency
|
||||
261.6255653005986
|
||||
|
||||
Temperament
|
||||
~~~~~~~~~~~
|
||||
|
||||
**Temperament** is the system used to tune the intervals between notes.
|
||||
Different temperaments produce slightly different frequencies for the
|
||||
same note name:
|
||||
|
||||
- `Equal temperament <https://en.wikipedia.org/wiki/Equal_temperament>`_ (default): Every semitone has an identical
|
||||
frequency ratio of 2^(1/12). This is the modern standard — it allows
|
||||
free modulation between all keys but no interval is acoustically
|
||||
"pure" except the octave.
|
||||
|
||||
- `Pythagorean temperament <https://en.wikipedia.org/wiki/Pythagorean_tuning>`_: Built entirely from pure perfect fifths
|
||||
(3:2 ratio). Produces beatless fifths but introduces the "Pythagorean
|
||||
comma" — a small discrepancy when 12 fifths don't quite equal 7
|
||||
octaves. Used in medieval European music.
|
||||
|
||||
- `Quarter-comma meantone <https://en.wikipedia.org/wiki/Quarter-comma_meantone>`_: Tunes major thirds to the pure ratio of
|
||||
5:4, distributing the resulting error across the fifths. Dominant in
|
||||
Renaissance and Baroque music (15th–18th century). Sounds beautiful
|
||||
in closely related keys but "wolf intervals" make distant keys
|
||||
unusable.
|
||||
|
||||
.. code-block:: pycon
|
||||
|
||||
>>> a4.pitch(temperament="equal")
|
||||
440.0
|
||||
>>> a4.pitch(temperament="pythagorean")
|
||||
440.0
|
||||
|
||||
# Symbolic (SymPy expression)
|
||||
>>> c5 = Tone.from_string("C5", system="western")
|
||||
>>> c5.pitch(temperament="equal")
|
||||
523.2511306011972
|
||||
>>> c5.pitch(temperament="pythagorean")
|
||||
521.4814814814815
|
||||
|
||||
Symbolic Pitch
|
||||
~~~~~~~~~~~~~~
|
||||
|
||||
Pass ``symbolic=True`` to get exact pitch ratios as
|
||||
`SymPy <https://en.wikipedia.org/wiki/SymPy>`_ expressions instead of
|
||||
floating-point approximations. This is useful for mathematical analysis,
|
||||
proving tuning relationships, or comparing temperaments with exact
|
||||
arithmetic.
|
||||
|
||||
.. code-block:: pycon
|
||||
|
||||
>>> a4 = Tone.from_string("A4", system="western")
|
||||
|
||||
>>> a4.pitch(symbolic=True)
|
||||
440
|
||||
>>> Tone.from_string("C5", system="western").pitch(symbolic=True)
|
||||
440*2**(1/4)
|
||||
|
||||
Arithmetic
|
||||
----------
|
||||
>>> Tone.from_string("G4", system="western").pitch(
|
||||
... temperament="pythagorean", symbolic=True)
|
||||
391.111111111111
|
||||
|
||||
>>> e4 = Tone.from_string("E4", system="western")
|
||||
>>> e4.pitch(temperament="equal", symbolic=True)
|
||||
220.0*2**(7/12)
|
||||
>>> e4.pitch(temperament="pythagorean", symbolic=True)
|
||||
330.000000000000
|
||||
>>> e4.pitch(temperament="meantone", symbolic=True)
|
||||
220.0*5**(1/4)
|
||||
|
||||
>>> e4.pitch(symbolic=True).evalf(50)
|
||||
329.62755691286992973584176104655507518647334182098
|
||||
|
||||
The symbolic output reveals *why* temperaments differ: equal temperament
|
||||
uses irrational numbers (roots of 2), Pythagorean uses powers of 3/2
|
||||
(rational but accumulating error), and meantone tunes thirds to the
|
||||
pure 5/4 ratio (sacrificing fifths).
|
||||
|
||||
Intervals and Arithmetic
|
||||
-------------------------
|
||||
|
||||
An **interval** is the distance between two pitches, measured in
|
||||
semitones. Intervals have both a **quantity** (number of scale steps)
|
||||
and a **quality** (perfect, major, minor, augmented, diminished).
|
||||
|
||||
Common intervals::
|
||||
|
||||
Semitones Name Sound
|
||||
───────── ──── ─────
|
||||
0 Unison Same note
|
||||
1 Minor 2nd Tense, dissonant (Jaws theme)
|
||||
2 Major 2nd A whole step (Do-Re)
|
||||
3 Minor 3rd Sad, dark (Greensleeves)
|
||||
4 Major 3rd Happy, bright (Kumbaya)
|
||||
5 Perfect 4th Open, hollow (Here Comes the Bride)
|
||||
6 Tritone Unstable, tense (The Simpsons)
|
||||
7 Perfect 5th Strong, stable (Star Wars)
|
||||
8 Minor 6th Bittersweet
|
||||
9 Major 6th Warm (My Bonnie)
|
||||
10 Minor 7th Bluesy (Star Trek TOS)
|
||||
11 Major 7th Dreamy, yearning
|
||||
12 Octave Same note, higher
|
||||
|
||||
Tones support ``+`` and ``-`` operators for semitone math:
|
||||
|
||||
.. code-block:: python
|
||||
.. code-block:: pycon
|
||||
|
||||
>>> c4 = Tone.from_string("C4", system="western")
|
||||
>>> c4 + 4 # Major third up
|
||||
>>> c4 + 4
|
||||
<Tone E4>
|
||||
>>> c4 + 7 # Perfect fifth up
|
||||
>>> c4 + 7
|
||||
<Tone G4>
|
||||
>>> c4 + 12 # Octave up
|
||||
>>> c4 + 12
|
||||
<Tone C5>
|
||||
|
||||
Subtracting two tones gives the semitone distance:
|
||||
|
||||
.. code-block:: python
|
||||
.. code-block:: pycon
|
||||
|
||||
>>> g4 = Tone.from_string("G4", system="western")
|
||||
>>> g4 - c4 # Semitone distance
|
||||
>>> g4 - c4
|
||||
7
|
||||
|
||||
>>> c5 = Tone.from_string("C5", system="western")
|
||||
>>> c5 - c4
|
||||
12
|
||||
|
||||
Naming Intervals
|
||||
~~~~~~~~~~~~~~~~
|
||||
|
||||
The ``interval_to`` method gives the musical name of the interval
|
||||
between two tones, including compound intervals that span more than
|
||||
one octave:
|
||||
|
||||
.. code-block:: pycon
|
||||
|
||||
>>> c4.interval_to(g4)
|
||||
'perfect 5th'
|
||||
>>> c4.interval_to(c4 + 4)
|
||||
'major 3rd'
|
||||
>>> c4.interval_to(c5)
|
||||
'octave'
|
||||
|
||||
>>> c4.interval_to(c4 + 19)
|
||||
'perfect 5th + 1 octave'
|
||||
|
||||
Transposition
|
||||
~~~~~~~~~~~~~
|
||||
|
||||
The ``transpose`` method returns a new tone shifted by a number of
|
||||
semitones — equivalent to the ``+`` operator but reads more clearly
|
||||
in some contexts:
|
||||
|
||||
.. code-block:: pycon
|
||||
|
||||
>>> c4.transpose(7)
|
||||
<Tone G4>
|
||||
>>> c4.transpose(-2)
|
||||
<Tone A#3>
|
||||
|
||||
MIDI
|
||||
~~~~
|
||||
|
||||
Every tone maps to a `MIDI note number <https://en.wikipedia.org/wiki/MIDI>`_
|
||||
(0–127), the standard for communicating with synthesizers, DAWs, and
|
||||
digital instruments:
|
||||
|
||||
.. code-block:: pycon
|
||||
|
||||
>>> c4.midi
|
||||
60
|
||||
>>> Tone.from_string("A4", system="western").midi
|
||||
69
|
||||
|
||||
>>> Tone.from_midi(60).midi
|
||||
60
|
||||
|
||||
Comparison and Sorting
|
||||
----------------------
|
||||
|
||||
Tones can be compared and sorted by pitch:
|
||||
Tones can be compared and sorted by pitch frequency:
|
||||
|
||||
.. code-block:: python
|
||||
.. code-block:: pycon
|
||||
|
||||
>>> c4 < g4
|
||||
True
|
||||
@@ -92,9 +288,182 @@ Tones can be compared and sorted by pitch:
|
||||
|
||||
Equality checks note name and octave:
|
||||
|
||||
.. code-block:: python
|
||||
.. code-block:: pycon
|
||||
|
||||
>>> c4 == "C" # Compare with string
|
||||
>>> c4 == "C"
|
||||
True
|
||||
>>> c4 == Tone(name="C", octave=4)
|
||||
True
|
||||
|
||||
The Overtone Series
|
||||
-------------------
|
||||
|
||||
Every tone you hear is actually a composite of many frequencies. When
|
||||
a string vibrates, it doesn't just vibrate as a whole — it also vibrates
|
||||
in halves, thirds, quarters, and so on, producing the `harmonic series <https://en.wikipedia.org/wiki/Harmonic_series_(music)>`_:
|
||||
|
||||
.. code-block:: pycon
|
||||
|
||||
>>> a4 = Tone.from_string("A4", system="western")
|
||||
>>> a4.overtones(8)
|
||||
[440.0, 880.0, 1320.0, 1760.0, 2200.0, 2640.0, 3080.0, 3520.0]
|
||||
|
||||
These harmonics correspond to musical intervals::
|
||||
|
||||
Harmonic Frequency Interval from fundamental
|
||||
1st 440 Hz Unison (A4)
|
||||
2nd 880 Hz Octave (A5)
|
||||
3rd 1320 Hz Octave + perfect 5th (E6)
|
||||
4th 1760 Hz Two octaves (A6)
|
||||
5th 2200 Hz Two octaves + major 3rd (C#7)
|
||||
6th 2640 Hz Two octaves + perfect 5th (E7)
|
||||
7th 3080 Hz Two octaves + minor 7th (≈G7, slightly flat)
|
||||
8th 3520 Hz Three octaves (A7)
|
||||
|
||||
The overtone series is why a perfect fifth sounds consonant — the 3rd
|
||||
harmonic of the lower note matches the 2nd harmonic of the upper note.
|
||||
It's also why the major triad (root, major 3rd, perfect 5th) feels
|
||||
"natural" — these intervals appear in the first 6 harmonics.
|
||||
|
||||
Different instruments emphasize different harmonics, which is why a
|
||||
violin and a flute playing the same note sound different. This quality
|
||||
is called `timbre <https://en.wikipedia.org/wiki/Timbre>`_.
|
||||
|
||||
Enharmonic Equivalents
|
||||
----------------------
|
||||
|
||||
In equal temperament, C# and Db are the same pitch (they have the
|
||||
same frequency). They're called **enharmonic equivalents**. Which name
|
||||
you use depends on context:
|
||||
|
||||
- In the key of **D major** (2 sharps), you write **C#**
|
||||
- In the key of **Gb major** (6 flats), you write **Db**
|
||||
|
||||
The rule: each letter name should appear exactly once in a scale. The
|
||||
D major scale is D E F# G A B C# — not D E Gb G A B Db, even though
|
||||
F#=Gb and C#=Db.
|
||||
|
||||
PyTheory uses sharps by default (following the tone list ordering), but
|
||||
every tone knows its enharmonic spelling:
|
||||
|
||||
.. code-block:: pycon
|
||||
|
||||
>>> Tone.from_string("C#4", system="western").enharmonic
|
||||
'Db'
|
||||
|
||||
>>> Tone.from_string("A#4", system="western").enharmonic
|
||||
'Bb'
|
||||
|
||||
>>> 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
|
||||
--------------------
|
||||
|
||||
The `circle of fifths <https://en.wikipedia.org/wiki/Circle_of_fifths>`_ is the most important diagram in Western music
|
||||
theory. Starting from any note and ascending by perfect fifths (7
|
||||
semitones), you pass through all 12 chromatic tones before returning
|
||||
to the starting note:
|
||||
|
||||
.. code-block:: pycon
|
||||
|
||||
>>> c4 = Tone.from_string("C4", system="western")
|
||||
|
||||
>>> [t.name for t in c4.circle_of_fifths()]
|
||||
['C', 'G', 'D', 'A', 'E', 'B', 'F#', 'C#', 'G#', 'D#', 'A#', 'F']
|
||||
|
||||
>>> [t.name for t in c4.circle_of_fourths()]
|
||||
['C', 'F', 'A#', 'D#', 'G#', 'C#', 'F#', 'B', 'E', 'A', 'D', 'G']
|
||||
|
||||
Each step clockwise adds one sharp to the key signature; each step
|
||||
counter-clockwise (ascending by fourths = 5 semitones) adds one flat.
|
||||
|
||||
Solfege
|
||||
-------
|
||||
|
||||
The fixed-Do `solfege <https://en.wikipedia.org/wiki/Solf%C3%A8ge>`_ system
|
||||
maps each note to a singable syllable. PyTheory uses fixed Do (C is always Do):
|
||||
|
||||
.. code-block:: pycon
|
||||
|
||||
>>> Tone.from_string("C4").solfege
|
||||
'Do'
|
||||
>>> Tone.from_string("D4").solfege
|
||||
'Re'
|
||||
>>> Tone.from_string("F#4").solfege
|
||||
'Fi'
|
||||
>>> Tone.from_string("Bb4").solfege
|
||||
'Te'
|
||||
|
||||
Helmholtz Notation
|
||||
------------------
|
||||
|
||||
The older `Helmholtz notation <https://en.wikipedia.org/wiki/Helmholtz_pitch_notation>`_
|
||||
uses case and tick marks instead of numbers:
|
||||
|
||||
.. code-block:: pycon
|
||||
|
||||
>>> Tone.from_string("C3").helmholtz # Great octave
|
||||
'C'
|
||||
>>> Tone.from_string("C4").helmholtz # Middle C
|
||||
'c'
|
||||
>>> Tone.from_string("C5").helmholtz # One-line octave
|
||||
"c'"
|
||||
>>> Tone.from_string("C2").helmholtz # Contra octave
|
||||
'CC'
|
||||
|
||||
Cents
|
||||
-----
|
||||
|
||||
A **cent** is 1/100th of a semitone — the standard unit for measuring
|
||||
fine pitch differences. Use ``cents_difference`` to compare tones or
|
||||
temperaments:
|
||||
|
||||
.. code-block:: pycon
|
||||
|
||||
>>> c4 = Tone.from_string("C4", system="western")
|
||||
>>> c4.cents_difference(c4 + 1) # One semitone = 100 cents
|
||||
100.0
|
||||
>>> c4.cents_difference(c4 + 7) # Perfect fifth
|
||||
700.0
|
||||
|
||||
Tones are the atoms of music -- everything else is built from them. Get comfortable here, and chords, scales, and harmony all start to make intuitive sense.
|
||||
|
||||
+108
-14
@@ -1,37 +1,122 @@
|
||||
PyTheory: Music Theory for Humans
|
||||
=================================
|
||||
|
||||
**PyTheory** is a Python library that makes exploring music theory approachable.
|
||||
Work with tones, scales, chords, and fretboards using a clean, Pythonic API.
|
||||
**PyTheory** is a Python library for exploring music theory, composing
|
||||
multi-part arrangements, and exporting them to MIDI for your DAW.
|
||||
|
||||
Use it to learn theory by doing — build chords from intervals and hear
|
||||
the result. Use it to sketch song ideas faster than clicking through a
|
||||
DAW. Use it with Claude Code to prototype
|
||||
music from natural language. Or just use it to answer "what chords are
|
||||
in G major?" without opening a browser.
|
||||
|
||||
::
|
||||
|
||||
$ pip install pytheory
|
||||
|
||||
Theory
|
||||
------
|
||||
|
||||
The theory layer works everywhere Python runs — no audio setup needed.
|
||||
Tones, scales, chords, keys, intervals, harmony, 16 musical systems,
|
||||
60+ instruments:
|
||||
|
||||
.. code-block:: pycon
|
||||
|
||||
>>> from pytheory import Key, Chord, Tone
|
||||
|
||||
>>> Key("C", "major").chords
|
||||
['C major', 'D minor', 'E minor', 'F major', 'G major', 'A minor', 'B diminished']
|
||||
|
||||
>>> [c.symbol for c in Key("G", "major").progression("I", "V", "vi", "IV")]
|
||||
['G', 'D', 'Em', 'C']
|
||||
|
||||
>>> Chord.from_symbol("F#m7b5").identify()
|
||||
'F# half-diminished 7th'
|
||||
|
||||
>>> Tone.from_string("C4").interval_to(Tone.from_string("G4"))
|
||||
'perfect 5th'
|
||||
|
||||
Composition
|
||||
-----------
|
||||
|
||||
When you're ready to make noise, the composition layer adds drums,
|
||||
synths, effects, and multi-part arrangements. Sketch an idea, hear
|
||||
it through your speakers, export MIDI, finish in your DAW:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
from pytheory import TonedScale, Fretboard, CHARTS
|
||||
from pytheory import Score, Pattern, Key, Duration, Chord
|
||||
from pytheory.play import play_score
|
||||
|
||||
# Build a C major scale
|
||||
c_major = TonedScale(tonic="C4")["major"]
|
||||
print(c_major.note_names)
|
||||
# ['C', 'D', 'E', 'F', 'G', 'A', 'B', 'C']
|
||||
score = Score("4/4", bpm=140)
|
||||
score.drums("bossa nova", repeats=4)
|
||||
|
||||
# Build a triad from the scale
|
||||
chord = c_major.triad(0) # C major triad
|
||||
for tone in chord:
|
||||
print(f"{tone}: {tone.frequency:.1f} Hz")
|
||||
chords = score.part("chords", synth="fm", envelope="pad", reverb=0.4)
|
||||
lead = score.part("lead", synth="saw", envelope="pluck", delay=0.3)
|
||||
bass = score.part("bass", synth="sine", lowpass=500)
|
||||
|
||||
# Get guitar fingerings
|
||||
fb = Fretboard.guitar()
|
||||
print(CHARTS["western"]["C"].fingering(fretboard=fb))
|
||||
for chord in Key("A", "minor").progression("i", "iv", "V", "i"):
|
||||
chords.add(chord, Duration.WHOLE)
|
||||
|
||||
lead.arpeggio("Am", bars=4, pattern="updown", octaves=2)
|
||||
|
||||
play_score(score)
|
||||
score.save_midi("sketch.mid")
|
||||
|
||||
Or hear a randomly generated track from the command line — different
|
||||
every time::
|
||||
|
||||
$ pytheory demo
|
||||
|
||||
What's Inside
|
||||
-------------
|
||||
|
||||
- **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, 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, 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, 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``,
|
||||
KeyboardInterrupt handling for clean stop
|
||||
- **AI-friendly** — Claude Code can compose
|
||||
and play music through PyTheory from natural language
|
||||
|
||||
.. toctree::
|
||||
:maxdepth: 2
|
||||
:caption: User Guide
|
||||
|
||||
guide/quickstart
|
||||
guide/theory
|
||||
guide/tones
|
||||
guide/scales
|
||||
guide/chords
|
||||
guide/fretboard
|
||||
guide/systems
|
||||
guide/sequencing
|
||||
guide/synths
|
||||
guide/effects
|
||||
guide/drums
|
||||
guide/playback
|
||||
guide/repl
|
||||
guide/cli
|
||||
guide/cookbook
|
||||
|
||||
.. toctree::
|
||||
:maxdepth: 2
|
||||
@@ -43,3 +128,12 @@ Work with tones, scales, chords, and fretboards using a clean, Pythonic API.
|
||||
api/charts
|
||||
api/play
|
||||
api/systems
|
||||
|
||||
.. toctree::
|
||||
:maxdepth: 1
|
||||
:caption: Project
|
||||
|
||||
changelog.md
|
||||
|
||||
Music is math that makes you feel something. PyTheory gives you the
|
||||
math. What you feel is up to you.
|
||||
|
||||
@@ -0,0 +1,46 @@
|
||||
"""Identify chords from notes or guitar fingerings."""
|
||||
|
||||
from pytheory import Chord, Fretboard
|
||||
|
||||
print("=== Chord Identification from Notes ===")
|
||||
print()
|
||||
|
||||
test_chords = [
|
||||
("C", "E", "G"),
|
||||
("A", "C", "E"),
|
||||
("G", "B", "D", "F"),
|
||||
("D", "F#", "A"),
|
||||
("Bb", "D", "F"),
|
||||
("E", "G#", "B"),
|
||||
("C", "Eb", "Gb"),
|
||||
("C", "G"),
|
||||
("C", "F", "G"),
|
||||
("C", "D", "G"),
|
||||
]
|
||||
|
||||
for notes in test_chords:
|
||||
chord = Chord.from_tones(*notes)
|
||||
name = chord.identify() or "Unknown"
|
||||
print(f" {', '.join(notes):20s} → {name}")
|
||||
|
||||
print()
|
||||
print("=== Chord Identification from Guitar Fingerings ===")
|
||||
print()
|
||||
|
||||
fb = Fretboard.guitar()
|
||||
|
||||
# Common guitar chord shapes
|
||||
shapes = [
|
||||
("Open C", (0, 1, 0, 2, 3, 0)),
|
||||
("Open G", (3, 0, 0, 0, 2, 3)),
|
||||
("Open D", (2, 3, 2, 0, 0, 0)),
|
||||
("Open Am", (0, 1, 2, 2, 0, 0)),
|
||||
("Open Em", (0, 0, 0, 2, 2, 0)),
|
||||
("Barre F", (1, 1, 2, 3, 3, 1)),
|
||||
("Power E5", (0, 0, 0, 0, 2, 0)),
|
||||
]
|
||||
|
||||
for label, positions in shapes:
|
||||
f = fb.fingering(*positions)
|
||||
name = f.identify() or "Unknown"
|
||||
print(f" {label:12s} {f} → {name}")
|
||||
@@ -0,0 +1,52 @@
|
||||
"""Analyze harmonic tension and resolution across chords."""
|
||||
|
||||
from pytheory import Chord
|
||||
|
||||
print("Chord Tension Analysis")
|
||||
print("=" * 70)
|
||||
print()
|
||||
print(f"{'Chord':>20s} {'Tension':>8s} {'Harmony':>8s} {'Dissonance':>11s} {'Notes'}")
|
||||
print(f"{'─' * 20} {'─' * 8} {'─' * 8} {'─' * 11} {'─' * 15}")
|
||||
|
||||
chords = [
|
||||
# Stable chords
|
||||
"C", "Am",
|
||||
# Moderate tension
|
||||
"Dm7", "Cmaj7",
|
||||
# High tension
|
||||
"G7", "Bdim",
|
||||
# Extended
|
||||
"Am7", "Cmaj9",
|
||||
]
|
||||
|
||||
for name in chords:
|
||||
chord = Chord.from_name(name)
|
||||
t = chord.tension
|
||||
tones = " ".join(tone.name for tone in chord.tones)
|
||||
print(
|
||||
f"{name:>20s} {t['score']:>8.2f} {chord.harmony:>8.4f}"
|
||||
f" {chord.dissonance:>11.4f} {tones}"
|
||||
)
|
||||
|
||||
# Show the V7 → I resolution
|
||||
print()
|
||||
print("─" * 70)
|
||||
print()
|
||||
print("The V7 → I resolution (the strongest pull in tonal music):")
|
||||
print()
|
||||
|
||||
g7 = Chord.from_name("G7")
|
||||
c = Chord.from_name("C")
|
||||
|
||||
print(f" G7 (dominant): tension={g7.tension['score']:.2f} "
|
||||
f"tritones={g7.tension['tritones']} "
|
||||
f"dominant_function={g7.tension['has_dominant_function']}")
|
||||
print(f" C (tonic): tension={c.tension['score']:.2f} "
|
||||
f"tritones={c.tension['tritones']} "
|
||||
f"dominant_function={c.tension['has_dominant_function']}")
|
||||
|
||||
print()
|
||||
print("Voice leading (G7 → C):")
|
||||
for src, dst, motion in g7.voice_leading(c):
|
||||
direction = "↑" if motion > 0 else "↓" if motion < 0 else "="
|
||||
print(f" {src.name:3s} → {dst.name:3s} ({direction} {abs(motion)} semitones)")
|
||||
@@ -0,0 +1,34 @@
|
||||
"""Visualize the circle of fifths with key signatures."""
|
||||
|
||||
from pytheory import Tone, Key
|
||||
|
||||
c = Tone.from_string("C4", system="western")
|
||||
|
||||
print("╔══════════════════════════════════════════════╗")
|
||||
print("║ THE CIRCLE OF FIFTHS ║")
|
||||
print("╠══════════════════════════════════════════════╣")
|
||||
print("║ Key Sig Accidentals ║")
|
||||
print("╠══════════════════════════════════════════════╣")
|
||||
|
||||
for tone in c.circle_of_fifths():
|
||||
key = Key(tone.name, "major")
|
||||
sig = key.signature
|
||||
relative = key.relative
|
||||
|
||||
if sig["sharps"]:
|
||||
mark = f'{sig["sharps"]}#'
|
||||
elif sig["flats"]:
|
||||
mark = f'{sig["flats"]}b'
|
||||
else:
|
||||
mark = "--"
|
||||
|
||||
accidentals = ", ".join(sig["accidentals"]) if sig["accidentals"] else "none"
|
||||
print(f"║ {tone.name:3s} {mark:3s} {accidentals:20s} rel: {relative.tonic_name} {relative.mode:5s} ║")
|
||||
|
||||
print("╚══════════════════════════════════════════════╝")
|
||||
|
||||
# Show that 12 fifths returns to the start
|
||||
print()
|
||||
print("Proof: 12 perfect fifths cycle through all 12 tones")
|
||||
names = [t.name for t in c.circle_of_fifths()]
|
||||
print(f" {' → '.join(names)} → {names[0]}")
|
||||
@@ -0,0 +1,74 @@
|
||||
"""Explore music theory with PyTheory."""
|
||||
|
||||
from pytheory import Key, Chord, Tone, Interval, PROGRESSIONS, Fretboard
|
||||
|
||||
# ── Keys and Scales ──────────────────────────────────────────────────────
|
||||
|
||||
key = Key("C", "major")
|
||||
print(f"Key: {key}")
|
||||
print(f"Notes: {key.note_names}")
|
||||
print()
|
||||
|
||||
# ── Diatonic Harmony ─────────────────────────────────────────────────────
|
||||
|
||||
print("Diatonic triads:")
|
||||
for i, chord in enumerate(key.scale.harmonize()):
|
||||
analysis = chord.analyze("C")
|
||||
print(f" {analysis:4s} {chord}")
|
||||
|
||||
print()
|
||||
print("Diatonic seventh chords:")
|
||||
for name in key.seventh_chords:
|
||||
print(f" {name}")
|
||||
|
||||
# ── Progressions ─────────────────────────────────────────────────────────
|
||||
|
||||
print()
|
||||
print("Common progressions in C major:")
|
||||
for name, numerals in PROGRESSIONS.items():
|
||||
chords = key.progression(*numerals)
|
||||
chord_names = [str(c) for c in chords]
|
||||
print(f" {name:20s} {' → '.join(chord_names)}")
|
||||
|
||||
# ── Intervals ────────────────────────────────────────────────────────────
|
||||
|
||||
print()
|
||||
c4 = Tone.from_string("C4", system="western")
|
||||
print("Intervals from C4:")
|
||||
for semitones in range(13):
|
||||
tone = c4 + semitones
|
||||
name = c4.interval_to(tone)
|
||||
print(f" {semitones:2d} semitones = {tone.name:3s} ({name})")
|
||||
|
||||
# ── Circle of Fifths ─────────────────────────────────────────────────────
|
||||
|
||||
print()
|
||||
print("Circle of fifths:", " → ".join(t.name for t in c4.circle_of_fifths()))
|
||||
|
||||
# ── Chord Analysis ───────────────────────────────────────────────────────
|
||||
|
||||
print()
|
||||
g7 = Chord.from_name("G7")
|
||||
print(f"Chord: {g7}")
|
||||
print(f" Intervals: {g7.intervals}")
|
||||
print(f" Tension: {g7.tension}")
|
||||
print(f" Analysis in C: {g7.analyze('C')}")
|
||||
|
||||
# ── Guitar Fingerings ────────────────────────────────────────────────────
|
||||
|
||||
print()
|
||||
fb = Fretboard.guitar()
|
||||
print("Guitar fingerings:")
|
||||
for name in ["C", "G", "Am", "F", "Dm", "E7"]:
|
||||
from pytheory import CHARTS
|
||||
fingering = CHARTS["western"][name].fingering(fretboard=fb)
|
||||
print(f" {name:4s} {fingering}")
|
||||
|
||||
# ── Overtone Series ──────────────────────────────────────────────────────
|
||||
|
||||
print()
|
||||
a4 = Tone.from_string("A4", system="western")
|
||||
print(f"Overtone series of {a4}:")
|
||||
for i, hz in enumerate(a4.overtones(8), 1):
|
||||
nearest = Tone.from_frequency(hz)
|
||||
print(f" Harmonic {i}: {hz:8.1f} Hz ≈ {nearest.full_name}")
|
||||
@@ -0,0 +1,74 @@
|
||||
"""Explore instruments, tunings, and chord fingerings."""
|
||||
|
||||
from pytheory import Fretboard, CHARTS
|
||||
|
||||
# ── Compare Instruments ─────────────────────────────────────────────────
|
||||
|
||||
print("Instrument Tunings")
|
||||
print("=" * 55)
|
||||
|
||||
instruments = [
|
||||
("Guitar (standard)", Fretboard.guitar()),
|
||||
("Guitar (drop D)", Fretboard.guitar("drop d")),
|
||||
("Guitar (open G)", Fretboard.guitar("open g")),
|
||||
("Guitar (DADGAD)", Fretboard.guitar("dadgad")),
|
||||
("Bass", Fretboard.bass()),
|
||||
("Ukulele", Fretboard.ukulele()),
|
||||
("Mandolin", Fretboard.mandolin()),
|
||||
("Violin", Fretboard.violin()),
|
||||
("Banjo", Fretboard.banjo()),
|
||||
("Bouzouki (Irish)", Fretboard.bouzouki()),
|
||||
]
|
||||
|
||||
for name, fb in instruments:
|
||||
tuning = " ".join(t.full_name for t in fb.tones)
|
||||
print(f" {name:22s} {tuning}")
|
||||
|
||||
# ── Guitar Chord Chart ──────────────────────────────────────────────────
|
||||
|
||||
print()
|
||||
print("Guitar Chord Chart (standard tuning)")
|
||||
print("=" * 55)
|
||||
|
||||
fb = Fretboard.guitar()
|
||||
chart = CHARTS["western"]
|
||||
|
||||
for chord_name in ["C", "G", "D", "Am", "Em", "F", "A", "E", "Dm", "G7", "C7", "Am7"]:
|
||||
f = chart[chord_name].fingering(fretboard=fb)
|
||||
print(f" {chord_name:5s} {f}")
|
||||
|
||||
# ── Capo Magic ──────────────────────────────────────────────────────────
|
||||
|
||||
print()
|
||||
print("Capo Transposition")
|
||||
print("=" * 55)
|
||||
print(" Playing open chord shapes with a capo changes the key:")
|
||||
print()
|
||||
|
||||
open_shapes = ["C", "G", "D", "Am", "Em"]
|
||||
|
||||
for capo_fret in range(1, 6):
|
||||
fb_capo = Fretboard.guitar(capo=capo_fret)
|
||||
results = []
|
||||
for shape in open_shapes:
|
||||
f = chart[shape].fingering(fretboard=fb_capo)
|
||||
actual = f.identify() or "?"
|
||||
results.append(f"{shape}→{actual.split()[0]}")
|
||||
print(f" Capo {capo_fret}: {', '.join(results)}")
|
||||
|
||||
# ── Same Chord on Different Instruments ─────────────────────────────────
|
||||
|
||||
print()
|
||||
print("C Major on Different Instruments")
|
||||
print("=" * 55)
|
||||
|
||||
c_chord = chart["C"]
|
||||
for name, fb in [("Guitar", Fretboard.guitar()),
|
||||
("Ukulele", Fretboard.ukulele()),
|
||||
("Mandolin", Fretboard.mandolin()),
|
||||
("Banjo", Fretboard.banjo())]:
|
||||
try:
|
||||
f = c_chord.fingering(fretboard=fb)
|
||||
print(f" {name:12s} {f}")
|
||||
except Exception:
|
||||
print(f" {name:12s} (not available for this tuning)")
|
||||
@@ -0,0 +1,93 @@
|
||||
"""Learn intervals — names, sounds, and relationships."""
|
||||
|
||||
from pytheory import Tone, Chord, Interval
|
||||
|
||||
c4 = Tone.from_string("C4", system="western")
|
||||
|
||||
# ── Interval Reference ──────────────────────────────────────────────────
|
||||
|
||||
print("Interval Reference (from C4)")
|
||||
print("=" * 70)
|
||||
print()
|
||||
print(f"{'Semitones':>10s} {'Note':>5s} {'Interval Name':>18s} {'Sound / Song'}")
|
||||
print(f"{'─' * 10} {'─' * 5} {'─' * 18} {'─' * 30}")
|
||||
|
||||
songs = {
|
||||
0: "Same note",
|
||||
1: "Jaws",
|
||||
2: "Happy Birthday",
|
||||
3: "Greensleeves",
|
||||
4: "Here Comes the Sun",
|
||||
5: "Here Comes the Bride",
|
||||
6: "The Simpsons",
|
||||
7: "Star Wars (main theme)",
|
||||
8: "Love Story",
|
||||
9: "My Bonnie Lies Over the Ocean",
|
||||
10: "Somewhere (West Side Story)",
|
||||
11: "Take On Me (chorus)",
|
||||
12: "Somewhere Over the Rainbow",
|
||||
}
|
||||
|
||||
for semitones in range(13):
|
||||
tone = c4 + semitones
|
||||
name = c4.interval_to(tone)
|
||||
song = songs.get(semitones, "")
|
||||
print(f"{semitones:>10d} {tone.name:>5s} {name:>18s} {song}")
|
||||
|
||||
# ── Interval Constants ──────────────────────────────────────────────────
|
||||
|
||||
print()
|
||||
print("Interval Constants (pytheory.Interval)")
|
||||
print("=" * 40)
|
||||
|
||||
constants = [
|
||||
("UNISON", Interval.UNISON),
|
||||
("MINOR_SECOND", Interval.MINOR_SECOND),
|
||||
("MAJOR_SECOND", Interval.MAJOR_SECOND),
|
||||
("MINOR_THIRD", Interval.MINOR_THIRD),
|
||||
("MAJOR_THIRD", Interval.MAJOR_THIRD),
|
||||
("PERFECT_FOURTH", Interval.PERFECT_FOURTH),
|
||||
("TRITONE", Interval.TRITONE),
|
||||
("PERFECT_FIFTH", Interval.PERFECT_FIFTH),
|
||||
("MINOR_SIXTH", Interval.MINOR_SIXTH),
|
||||
("MAJOR_SIXTH", Interval.MAJOR_SIXTH),
|
||||
("MINOR_SEVENTH", Interval.MINOR_SEVENTH),
|
||||
("MAJOR_SEVENTH", Interval.MAJOR_SEVENTH),
|
||||
("OCTAVE", Interval.OCTAVE),
|
||||
]
|
||||
|
||||
for name, value in constants:
|
||||
print(f" Interval.{name:16s} = {value}")
|
||||
|
||||
# ── Compound Intervals ─────────────────────────────────────────────────
|
||||
|
||||
print()
|
||||
print("Compound Intervals (beyond one octave)")
|
||||
print("=" * 50)
|
||||
|
||||
for semitones in [13, 14, 15, 16, 19, 24]:
|
||||
tone = c4 + semitones
|
||||
name = c4.interval_to(tone)
|
||||
print(f" {semitones:2d} semitones {tone.full_name:5s} {name}")
|
||||
|
||||
# ── Consonance Ranking ──────────────────────────────────────────────────
|
||||
|
||||
print()
|
||||
print("Intervals Ranked by Consonance")
|
||||
print("=" * 50)
|
||||
|
||||
intervals = []
|
||||
for semitones in range(1, 13):
|
||||
tone = c4 + semitones
|
||||
dyad = Chord.from_tones("C", tone.name)
|
||||
name = c4.interval_to(tone)
|
||||
intervals.append((dyad.harmony, dyad.dissonance, semitones, name))
|
||||
|
||||
# Sort by harmony score (descending)
|
||||
intervals.sort(key=lambda x: x[0], reverse=True)
|
||||
|
||||
print(f"{'Rank':>5s} {'Interval':>18s} {'Harmony':>8s} {'Dissonance':>11s}")
|
||||
print(f"{'─' * 5} {'─' * 18} {'─' * 8} {'─' * 11}")
|
||||
|
||||
for rank, (harmony, dissonance, _, name) in enumerate(intervals, 1):
|
||||
print(f"{rank:>5d} {name:>18s} {harmony:>8.4f} {dissonance:>11.4f}")
|
||||
@@ -0,0 +1,64 @@
|
||||
"""Detect the key of a melody or chord progression."""
|
||||
|
||||
from pytheory import Key, Chord
|
||||
|
||||
print("Key Detection")
|
||||
print("=" * 55)
|
||||
print()
|
||||
|
||||
# ── Detect from Melody Notes ────────────────────────────────────────────
|
||||
|
||||
melodies = [
|
||||
("Twinkle Twinkle", ["C", "G", "A", "F", "E", "D"]),
|
||||
("Happy Birthday", ["G", "A", "B", "C", "D", "F#"]),
|
||||
("Yesterday", ["F", "E", "D", "C", "Bb", "A", "G"]),
|
||||
("Minor melody", ["A", "B", "C", "D", "E", "F", "G"]),
|
||||
("Blues lick", ["E", "G", "A", "B", "D"]),
|
||||
("Chromatic fragment", ["C", "C#", "D", "D#", "E"]),
|
||||
]
|
||||
|
||||
print("Detecting key from melody notes:")
|
||||
print()
|
||||
for label, notes in melodies:
|
||||
key = Key.detect(*notes)
|
||||
print(f" {label:22s} {', '.join(notes):30s} → {key}")
|
||||
|
||||
# ── Detect from Chord Progression ──────────────────────────────────────
|
||||
|
||||
print()
|
||||
print("Detecting key from chord tones:")
|
||||
print()
|
||||
|
||||
progressions = [
|
||||
("I-IV-V", [("C", "E", "G"), ("F", "A", "C"), ("G", "B", "D")]),
|
||||
("Pop in G", [("G", "B", "D"), ("D", "F#", "A"), ("E", "G", "B"), ("C", "E", "G")]),
|
||||
("Jazz ii-V-I", [("D", "F", "A"), ("G", "B", "D", "F"), ("C", "E", "G", "B")]),
|
||||
]
|
||||
|
||||
for label, chord_tones in progressions:
|
||||
# Collect all unique note names
|
||||
all_notes = set()
|
||||
for tones in chord_tones:
|
||||
all_notes.update(tones)
|
||||
|
||||
key = Key.detect(*all_notes)
|
||||
chord_names = [Chord.from_tones(*t).identify() for t in chord_tones]
|
||||
print(f" {label:15s} {' → '.join(chord_names):40s} → {key}")
|
||||
|
||||
# ── All 24 Keys ─────────────────────────────────────────────────────────
|
||||
|
||||
print()
|
||||
print("All 24 Major and Minor Keys")
|
||||
print("=" * 55)
|
||||
print()
|
||||
|
||||
for key in Key.all_keys():
|
||||
sig = key.signature
|
||||
acc = ", ".join(sig["accidentals"]) if sig["accidentals"] else "none"
|
||||
rel = key.relative
|
||||
print(
|
||||
f" {str(key):12s} "
|
||||
f"{sig['sharps']}# {sig['flats']}b "
|
||||
f"({acc:15s}) "
|
||||
f"rel: {rel}"
|
||||
)
|
||||
@@ -0,0 +1,58 @@
|
||||
"""Explore a key — its chords, progressions, and relationships."""
|
||||
|
||||
from pytheory import Key
|
||||
|
||||
def explore_key(tonic, mode="major"):
|
||||
key = Key(tonic, mode)
|
||||
sig = key.signature
|
||||
acc = ", ".join(sig["accidentals"]) or "none"
|
||||
|
||||
print(f"{'=' * 60}")
|
||||
print(f" {key}")
|
||||
print(f"{'=' * 60}")
|
||||
print()
|
||||
print(f" Scale: {' '.join(key.note_names)}")
|
||||
print(f" Signature: {sig['sharps']} sharps, {sig['flats']} flats ({acc})")
|
||||
print(f" Relative: {key.relative}")
|
||||
print(f" Parallel: {key.parallel}")
|
||||
print()
|
||||
|
||||
# Diatonic triads
|
||||
print(" Diatonic Triads:")
|
||||
for chord in key.scale.harmonize():
|
||||
numeral = chord.analyze(tonic, mode) or "?"
|
||||
print(f" {numeral:6s} {chord.identify()}")
|
||||
print()
|
||||
|
||||
# Seventh chords
|
||||
print(" Seventh Chords:")
|
||||
for name in key.seventh_chords:
|
||||
print(f" {name}")
|
||||
print()
|
||||
|
||||
# Common progressions
|
||||
print(" Common Progressions:")
|
||||
progressions = {
|
||||
"Pop": ("I", "V", "vi", "IV"),
|
||||
"Blues": ("I", "IV", "V"),
|
||||
"50s": ("I", "vi", "IV", "V"),
|
||||
"Jazz": ("ii", "V", "I"),
|
||||
}
|
||||
for label, numerals in progressions.items():
|
||||
chords = key.progression(*numerals)
|
||||
names = [c.identify() for c in chords]
|
||||
print(f" {label:8s} {' → '.join(numerals):20s} {' → '.join(names)}")
|
||||
print()
|
||||
|
||||
# Borrowed chords
|
||||
borrowed = key.borrowed_chords
|
||||
if borrowed:
|
||||
print(f" Borrowed from {key.parallel}:")
|
||||
for name in borrowed[:4]:
|
||||
print(f" {name}")
|
||||
print()
|
||||
|
||||
|
||||
# Explore several keys
|
||||
for tonic, mode in [("C", "major"), ("G", "major"), ("A", "minor"), ("E", "major")]:
|
||||
explore_key(tonic, mode)
|
||||
@@ -0,0 +1,35 @@
|
||||
"""Convert between MIDI note numbers, frequencies, and note names."""
|
||||
|
||||
from pytheory import Tone
|
||||
|
||||
print("MIDI ↔ Note ↔ Frequency Reference")
|
||||
print("=" * 50)
|
||||
print()
|
||||
print(f"{'MIDI':>5s} {'Note':>5s} {'Freq (Hz)':>10s} {'Octave':>6s}")
|
||||
print(f"{'─' * 5} {'─' * 5} {'─' * 10} {'─' * 6}")
|
||||
|
||||
# Show all notes from C2 to C7
|
||||
for midi in range(36, 97):
|
||||
tone = Tone.from_midi(midi)
|
||||
freq = tone.frequency
|
||||
print(f"{midi:>5d} {tone.full_name:>5s} {freq:>10.2f} {tone.octave:>6d}")
|
||||
|
||||
# Useful reference points
|
||||
print()
|
||||
print("Key Reference Points:")
|
||||
print(f" Lowest piano note: A0 = MIDI {Tone.from_string('A0', system='western').midi}")
|
||||
print(f" Middle C: C4 = MIDI {Tone.from_string('C4', system='western').midi}")
|
||||
print(f" Concert A: A4 = MIDI {Tone.from_string('A4', system='western').midi}")
|
||||
print(f" Highest piano note: C8 = MIDI {Tone.from_string('C8', system='western').midi}")
|
||||
|
||||
# Round-trip demo
|
||||
print()
|
||||
print("Round-trip conversions:")
|
||||
for start in ["C4", "A4", "F#3", "Bb5"]:
|
||||
tone = Tone.from_string(start, system="western")
|
||||
midi = tone.midi
|
||||
freq = tone.frequency
|
||||
from_midi = Tone.from_midi(midi)
|
||||
from_freq = Tone.from_frequency(freq)
|
||||
print(f" {start:4s} → MIDI {midi} → {from_midi.full_name:4s} | "
|
||||
f"{start:4s} → {freq:.2f} Hz → {from_freq.full_name}")
|
||||
@@ -0,0 +1,68 @@
|
||||
"""Explore the overtone series — nature's chord."""
|
||||
|
||||
from pytheory import Tone, Chord
|
||||
|
||||
a4 = Tone.from_string("A4", system="western")
|
||||
|
||||
print("The Overtone Series")
|
||||
print("=" * 65)
|
||||
print()
|
||||
print("When you play a note, you're actually hearing many frequencies")
|
||||
print("at once. The fundamental plus its integer multiples:")
|
||||
print()
|
||||
print(f"{'Harmonic':>9s} {'Frequency':>10s} {'Nearest Note':>13s} {'Interval from Root'}")
|
||||
print(f"{'─' * 9} {'─' * 10} {'─' * 13} {'─' * 25}")
|
||||
|
||||
overtones = a4.overtones(16)
|
||||
|
||||
for i, hz in enumerate(overtones, 1):
|
||||
nearest = Tone.from_frequency(hz)
|
||||
if i == 1:
|
||||
interval = "Fundamental"
|
||||
else:
|
||||
interval = a4.interval_to(nearest)
|
||||
print(f"{i:>9d} {hz:>10.1f} {nearest.full_name:>13s} {interval}")
|
||||
|
||||
# ── Why Chords Sound Good ───────────────────────────────────────────────
|
||||
|
||||
print()
|
||||
print("Why the Major Triad Sounds 'Natural'")
|
||||
print("=" * 65)
|
||||
print()
|
||||
print("The first 6 harmonics contain: root, octave, 5th, 2nd octave, 3rd, 5th")
|
||||
print("That's a major triad! The major chord is literally embedded in physics.")
|
||||
print()
|
||||
|
||||
c4 = Tone.from_string("C4", system="western")
|
||||
harmonics = c4.overtones(6)
|
||||
harmonic_names = [Tone.from_frequency(hz).name for hz in harmonics]
|
||||
unique = []
|
||||
for n in harmonic_names:
|
||||
if n not in unique:
|
||||
unique.append(n)
|
||||
print(f" First 6 harmonics of C: {', '.join(harmonic_names)}")
|
||||
print(f" Unique pitch classes: {', '.join(unique)}")
|
||||
print(f" C major triad: C, E, G")
|
||||
print()
|
||||
|
||||
# ── Shared Overtones = Consonance ───────────────────────────────────────
|
||||
|
||||
print("Shared Overtones Between Intervals")
|
||||
print("=" * 65)
|
||||
print()
|
||||
print("The more overtones two notes share, the more consonant they sound.")
|
||||
print()
|
||||
|
||||
root = Tone.from_string("C4", system="western")
|
||||
root_overtones = set(round(h, 1) for h in root.overtones(12))
|
||||
|
||||
for semitones, label in [(7, "Perfect 5th (C→G)"),
|
||||
(4, "Major 3rd (C→E)"),
|
||||
(5, "Perfect 4th (C→F)"),
|
||||
(3, "Minor 3rd (C→Eb)"),
|
||||
(6, "Tritone (C→F#)"),
|
||||
(1, "Minor 2nd (C→C#)")]:
|
||||
other = root + semitones
|
||||
other_overtones = set(round(h, 1) for h in other.overtones(12))
|
||||
shared = root_overtones & other_overtones
|
||||
print(f" {label:25s} {len(shared):2d} shared overtones (of first 12)")
|
||||
@@ -0,0 +1,81 @@
|
||||
"""Build and analyze chord progressions in any key."""
|
||||
|
||||
from pytheory import Key, Chord
|
||||
|
||||
def show_progression(key, numerals, label=""):
|
||||
chords = key.progression(*numerals)
|
||||
if label:
|
||||
print(f" {label}")
|
||||
print(f" Key: {key}")
|
||||
print(f" Progression: {' – '.join(numerals)}")
|
||||
print()
|
||||
for numeral, chord in zip(numerals, chords):
|
||||
t = chord.tension
|
||||
print(
|
||||
f" {numeral:6s} {chord.identify():20s} "
|
||||
f"tension={t['score']:.2f} "
|
||||
f"{'*** DOMINANT ***' if t['has_dominant_function'] else ''}"
|
||||
)
|
||||
print()
|
||||
|
||||
|
||||
# ── Famous Progressions ─────────────────────────────────────────────────
|
||||
|
||||
print("Famous Chord Progressions")
|
||||
print("=" * 65)
|
||||
print()
|
||||
|
||||
key_c = Key("C", "major")
|
||||
|
||||
show_progression(key_c, ("I", "V", "vi", "IV"),
|
||||
"The Pop Progression (Let It Be, No Woman No Cry, Someone Like You)")
|
||||
|
||||
show_progression(key_c, ("I", "vi", "IV", "V"),
|
||||
"The 50s Progression (Stand By Me, Every Breath You Take)")
|
||||
|
||||
show_progression(key_c, ("ii", "V", "I"),
|
||||
"Jazz ii–V–I (the backbone of jazz harmony)")
|
||||
|
||||
show_progression(key_c, ("I", "IV", "V", "I"),
|
||||
"The Three-Chord Trick (blues, rock, country)")
|
||||
|
||||
# ── Same Progression in Different Keys ──────────────────────────────────
|
||||
|
||||
print("─" * 65)
|
||||
print()
|
||||
print("I – V – vi – IV in every key:")
|
||||
print()
|
||||
|
||||
for tonic in ["C", "G", "D", "A", "E", "F", "Bb", "Eb"]:
|
||||
key = Key(tonic, "major")
|
||||
chords = key.progression("I", "V", "vi", "IV")
|
||||
names = [c.identify() for c in chords]
|
||||
print(f" {tonic} major: {' → '.join(names)}")
|
||||
|
||||
# ── Nashville Number System ─────────────────────────────────────────────
|
||||
|
||||
print()
|
||||
print("─" * 65)
|
||||
print()
|
||||
print("Nashville Number System:")
|
||||
print(" (Same thing as Roman numerals, but with integers)")
|
||||
print()
|
||||
|
||||
key_g = Key("G", "major")
|
||||
chords = key_g.nashville(1, 5, 6, 4)
|
||||
names = [c.identify() for c in chords]
|
||||
print(f" G major: 1 – 5 – 6 – 4 → {' → '.join(names)}")
|
||||
|
||||
# ── Random Progression Generator ────────────────────────────────────────
|
||||
|
||||
print()
|
||||
print("─" * 65)
|
||||
print()
|
||||
print("Random 8-bar progressions:")
|
||||
print()
|
||||
|
||||
for _ in range(3):
|
||||
key = Key("C", "major")
|
||||
chords = key.random_progression(8)
|
||||
names = [c.identify().split()[0] for c in chords] # Just root names
|
||||
print(f" | {' | '.join(names)} |")
|
||||
@@ -1,78 +0,0 @@
|
||||
from time import sleep
|
||||
|
||||
from pytheory import TonedScale, Tone, CHARTS, play
|
||||
|
||||
|
||||
# Add this constant at the top of the file, after the imports
|
||||
EIGHTH_NOTE = 0.25
|
||||
QUARTER_NOTE = 0.5
|
||||
|
||||
# Add scale definition after the constants
|
||||
C_MAJOR = TonedScale(tonic="C4")
|
||||
|
||||
|
||||
def play_note(note, t=0.1):
|
||||
# Convert scale degree (1-7) to note name (0-based index)
|
||||
scale_notes = ["C4", "D4", "E4", "F4", "G4", "A4", "B4"]
|
||||
note_name = scale_notes[note - 1] # Subtract 1 because scale degrees are 1-based
|
||||
tone = Tone(note_name)
|
||||
play(tone, t=t * 1_000)
|
||||
sleep(t)
|
||||
|
||||
|
||||
# Twinkle Twinkle Little Star in C major
|
||||
# C C G G A A G (first line)
|
||||
# F F E E D D C (second line)
|
||||
# G G F F E E D (third line)
|
||||
# G G F F E E D (fourth line)
|
||||
# C C G G A A G (fifth line)
|
||||
# F F E E D D C (sixth line)
|
||||
|
||||
|
||||
def play_twinkle():
|
||||
# Define the patterns using scale degrees instead of note names
|
||||
line1 = [
|
||||
(1, EIGHTH_NOTE), # C4
|
||||
(1, EIGHTH_NOTE), # C4
|
||||
(5, EIGHTH_NOTE), # G4
|
||||
(5, EIGHTH_NOTE), # G4
|
||||
(6, EIGHTH_NOTE), # A4
|
||||
(6, EIGHTH_NOTE), # A4
|
||||
(5, QUARTER_NOTE), # G4
|
||||
]
|
||||
line2 = [
|
||||
(4, EIGHTH_NOTE), # F4
|
||||
(4, EIGHTH_NOTE), # F4
|
||||
(3, EIGHTH_NOTE), # E4
|
||||
(3, EIGHTH_NOTE), # E4
|
||||
(2, EIGHTH_NOTE), # D4
|
||||
(2, EIGHTH_NOTE), # D4
|
||||
(1, QUARTER_NOTE), # C4
|
||||
]
|
||||
line3 = [
|
||||
(5, EIGHTH_NOTE), # G4
|
||||
(5, EIGHTH_NOTE), # G4
|
||||
(4, EIGHTH_NOTE), # F4
|
||||
(4, EIGHTH_NOTE), # F4
|
||||
(3, EIGHTH_NOTE), # E4
|
||||
(3, EIGHTH_NOTE), # E4
|
||||
(2, QUARTER_NOTE), # D4
|
||||
]
|
||||
|
||||
# Construct the full melody using the patterns
|
||||
melody = (
|
||||
line1 # Twinkle twinkle little star
|
||||
+ line2 # How I wonder what you are
|
||||
+ line3 # Up above the world so high
|
||||
+ line3 # Like a diamond in the sky
|
||||
+ line1 # Twinkle twinkle little star
|
||||
+ line2 # How I wonder what you are
|
||||
)
|
||||
|
||||
print("Playing Twinkle Twinkle Little Star...")
|
||||
for note, duration in melody:
|
||||
play_note(note, duration)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
play_twinkle()
|
||||
@@ -0,0 +1,335 @@
|
||||
"""PyTheory Showoff — a generative composition that's different every time.
|
||||
|
||||
Demonstrates every feature of the library in one elegant piece:
|
||||
- Key detection and modulation
|
||||
- Chord progressions with Roman numerals
|
||||
- 58 drum patterns with genre-matched fills
|
||||
- 10 synth waveforms and 8 envelope presets
|
||||
- Arpeggiator with legato and glide
|
||||
- Per-part effects: reverb, delay, lowpass filter, distortion
|
||||
- Random but musically coherent — always sounds good, never repeats
|
||||
|
||||
Usage:
|
||||
python examples/song_showoff.py
|
||||
"""
|
||||
|
||||
import random
|
||||
import sounddevice as sd
|
||||
|
||||
from pytheory import Chord, Key, Pattern, Duration, Score
|
||||
from pytheory.play import render_score, SAMPLE_RATE
|
||||
|
||||
|
||||
# ── Musical building blocks ────────────────────────────────────────────────
|
||||
|
||||
MOODS = {
|
||||
"dark": {
|
||||
"keys": [("D", "minor"), ("A", "minor"), ("E", "minor"),
|
||||
("B", "minor"), ("G", "minor"), ("C", "minor")],
|
||||
"progressions": [
|
||||
("i", "iv", "V", "i"),
|
||||
("i", "bVI", "bVII", "i"),
|
||||
("i", "iv", "bVI", "V"),
|
||||
("i", "bVII", "bVI", "V"),
|
||||
],
|
||||
"drums": ["afrobeat", "dub", "trap", "reggae", "techno",
|
||||
"hip hop", "drum and bass"],
|
||||
"fills": ["afrobeat", "trap", "buildup", "breakdown"],
|
||||
"tempo_range": (70, 140),
|
||||
},
|
||||
"bright": {
|
||||
"keys": [("C", "major"), ("G", "major"), ("D", "major"),
|
||||
("A", "major"), ("F", "major"), ("Bb", "major")],
|
||||
"progressions": [
|
||||
("I", "V", "vi", "IV"),
|
||||
("I", "IV", "V", "I"),
|
||||
("I", "vi", "ii", "V"),
|
||||
("I", "IV", "vi", "V"),
|
||||
],
|
||||
"drums": ["bossa nova", "samba", "funk", "disco", "gospel",
|
||||
"ska", "swing", "country"],
|
||||
"fills": ["rock", "funk", "samba", "buildup"],
|
||||
"tempo_range": (100, 170),
|
||||
},
|
||||
"ethereal": {
|
||||
"keys": [("Eb", "minor"), ("Ab", "minor"), ("F", "minor"),
|
||||
("Bb", "minor"), ("Db", "major"), ("Gb", "major")],
|
||||
"progressions": [
|
||||
("I", "V", "vi", "IV"),
|
||||
("i", "bVI", "bIII", "bVII"),
|
||||
("i", "iv", "V", "i"),
|
||||
("I", "vi", "IV", "V"),
|
||||
],
|
||||
"drums": ["jazz", "waltz", "house", "bebop", "bolero"],
|
||||
"fills": ["jazz", "jazz brush", "house", "bossa nova"],
|
||||
"tempo_range": (68, 120),
|
||||
},
|
||||
"aggressive": {
|
||||
"keys": [("E", "minor"), ("A", "minor"), ("D", "minor"),
|
||||
("B", "minor"), ("F#", "minor"), ("C", "minor")],
|
||||
"progressions": [
|
||||
("i", "bVII", "bVI", "V"),
|
||||
("i", "iv", "V", "i"),
|
||||
("i", "bVI", "bVII", "i"),
|
||||
("i", "V", "bVI", "iv"),
|
||||
],
|
||||
"drums": ["metal", "punk", "drum and bass", "jungle",
|
||||
"breakbeat", "techno"],
|
||||
"fills": ["metal", "blast", "rock crash", "buildup"],
|
||||
"tempo_range": (130, 180),
|
||||
},
|
||||
}
|
||||
|
||||
SYNTH_PALETTES = [
|
||||
# (lead_synth, pad_synth, bass_synth, arp_synth)
|
||||
("saw", "supersaw", "sine", "saw"),
|
||||
("triangle", "pwm_slow", "sine", "fm"),
|
||||
("fm", "supersaw", "pulse", "saw"),
|
||||
("square", "pwm_slow", "sine", "square"),
|
||||
("saw", "pwm_fast", "pulse", "fm"),
|
||||
("triangle", "supersaw", "sine", "saw"),
|
||||
]
|
||||
|
||||
ARP_PATTERNS = ["up", "down", "updown", "downup"]
|
||||
|
||||
|
||||
# ── The generator ──────────────────────────────────────────────────────────
|
||||
|
||||
def generate():
|
||||
"""Generate a unique composition. Different every time."""
|
||||
|
||||
# Pick a mood
|
||||
mood_name = random.choice(list(MOODS.keys()))
|
||||
mood = MOODS[mood_name]
|
||||
|
||||
# Pick a key
|
||||
tonic, mode = random.choice(mood["keys"])
|
||||
key = Key(tonic, mode)
|
||||
|
||||
# Pick a progression
|
||||
numerals = random.choice(mood["progressions"])
|
||||
|
||||
# Pick tempo
|
||||
bpm = random.randint(*mood["tempo_range"])
|
||||
|
||||
# Pick drum pattern and fill
|
||||
drum_preset = random.choice(mood["drums"])
|
||||
fill_preset = random.choice(mood["fills"])
|
||||
|
||||
# Pick synth palette
|
||||
lead_synth, pad_synth, bass_synth, arp_synth = random.choice(SYNTH_PALETTES)
|
||||
|
||||
# Pick time signature based on drum pattern
|
||||
waltz_patterns = {"waltz", "bolero"}
|
||||
time_sig = "3/4" if drum_preset in waltz_patterns else "4/4"
|
||||
bars_per_chord = 2
|
||||
total_bars = bars_per_chord * len(numerals) * 2 # play progression twice
|
||||
|
||||
# Determine repeats based on time signature
|
||||
pattern_obj = Pattern.preset(drum_preset)
|
||||
beats_per_bar = 3.0 if time_sig == "3/4" else 4.0
|
||||
pattern_bars = pattern_obj.beats / beats_per_bar
|
||||
drum_repeats = max(1, int(total_bars / pattern_bars))
|
||||
|
||||
# ── Build the score ──────────────────────────────────────────
|
||||
|
||||
score = Score(time_sig, bpm=bpm)
|
||||
score.drums(drum_preset, repeats=drum_repeats,
|
||||
fill=fill_preset, fill_every=len(numerals) * bars_per_chord)
|
||||
|
||||
# Effect amounts scale with mood intensity
|
||||
reverb_amount = {"dark": 0.35, "bright": 0.25,
|
||||
"ethereal": 0.55, "aggressive": 0.2}[mood_name]
|
||||
delay_amount = {"dark": 0.3, "bright": 0.15,
|
||||
"ethereal": 0.35, "aggressive": 0.15}[mood_name]
|
||||
dist_amount = {"dark": 0.3, "bright": 0.0,
|
||||
"ethereal": 0.0, "aggressive": 0.7}[mood_name]
|
||||
filter_cutoff = {"dark": 2500, "bright": 5000,
|
||||
"ethereal": 2000, "aggressive": 3500}[mood_name]
|
||||
|
||||
# Pad — lush background
|
||||
pad = score.part("pad", synth=pad_synth, envelope="pad",
|
||||
volume=0.2,
|
||||
reverb=min(0.8, reverb_amount * 2),
|
||||
reverb_decay=random.uniform(2.0, 4.0),
|
||||
lowpass=filter_cutoff)
|
||||
|
||||
# Lead melody
|
||||
lead_envelope = random.choice(["pluck", "strings", "piano"])
|
||||
lead = score.part("lead", synth=lead_synth, envelope=lead_envelope,
|
||||
volume=0.4,
|
||||
reverb=reverb_amount,
|
||||
reverb_decay=random.uniform(1.0, 2.0),
|
||||
delay=delay_amount,
|
||||
delay_time=random.choice([0.25, 0.375, 0.5]) * 60 / bpm,
|
||||
delay_feedback=random.uniform(0.25, 0.45),
|
||||
lowpass=filter_cutoff,
|
||||
distortion=dist_amount * 0.3,
|
||||
distortion_drive=random.uniform(2.0, 5.0))
|
||||
|
||||
# Arp layer
|
||||
arp_pattern = random.choice(ARP_PATTERNS)
|
||||
arp_octaves = random.choice([1, 2])
|
||||
arp_division = random.choice([Duration.EIGHTH, Duration.SIXTEENTH])
|
||||
use_legato = random.random() > 0.4
|
||||
glide_time = random.uniform(0.02, 0.06) if use_legato else 0.0
|
||||
arp = score.part("arp", synth=arp_synth,
|
||||
envelope="staccato" if not use_legato else "pad",
|
||||
volume=0.3,
|
||||
legato=use_legato, glide=glide_time,
|
||||
distortion=dist_amount,
|
||||
distortion_drive=random.uniform(3.0, 10.0),
|
||||
lowpass=random.uniform(800, 2000),
|
||||
lowpass_q=random.uniform(1.0, 5.0),
|
||||
delay=delay_amount * 0.7,
|
||||
delay_time=random.uniform(0.15, 0.3),
|
||||
delay_feedback=random.uniform(0.2, 0.4))
|
||||
|
||||
# Bass
|
||||
bass = score.part("bass", synth=bass_synth, envelope="pluck",
|
||||
volume=0.5,
|
||||
lowpass=random.uniform(300, 600),
|
||||
lowpass_q=random.uniform(1.0, 1.8),
|
||||
distortion=dist_amount * 0.5,
|
||||
distortion_drive=random.uniform(2.0, 4.0))
|
||||
|
||||
# Bells / texture — sparse accents
|
||||
bells = score.part("bells", synth="fm", envelope="bell",
|
||||
volume=0.15,
|
||||
reverb=min(0.7, reverb_amount * 2.5),
|
||||
reverb_decay=random.uniform(2.0, 4.0),
|
||||
delay=0.2,
|
||||
delay_time=random.uniform(0.3, 0.8),
|
||||
delay_feedback=random.uniform(0.25, 0.4))
|
||||
|
||||
# ── Compose the parts ────────────────────────────────────────
|
||||
|
||||
chords = key.progression(*numerals)
|
||||
scale = key.scale
|
||||
|
||||
# Get scale tones for melody generation
|
||||
scale_tones = [t.name for t in scale.tones[:-1]]
|
||||
|
||||
for pass_num in range(2):
|
||||
for i, chord in enumerate(chords):
|
||||
chord_dur = Duration.DOTTED_HALF if time_sig == "3/4" else Duration.WHOLE
|
||||
|
||||
# Pad: whole notes
|
||||
for _ in range(bars_per_chord):
|
||||
pad.add(chord, chord_dur)
|
||||
|
||||
# Arp: arpeggiate the chord
|
||||
arp.arpeggio(chord, bars=bars_per_chord,
|
||||
pattern=arp_pattern, division=arp_division,
|
||||
octaves=arp_octaves)
|
||||
|
||||
# Bass: root note pattern
|
||||
root = chord.root
|
||||
if root:
|
||||
bass_note = f"{root.name}2"
|
||||
fifth = root.add(7)
|
||||
bass_fifth = f"{fifth.name}2"
|
||||
if time_sig == "3/4":
|
||||
for _ in range(bars_per_chord):
|
||||
bass.add(bass_note, Duration.QUARTER)
|
||||
bass.add(bass_fifth, Duration.QUARTER)
|
||||
bass.add(bass_note, Duration.QUARTER)
|
||||
else:
|
||||
for _ in range(bars_per_chord):
|
||||
bass.add(bass_note, Duration.QUARTER)
|
||||
bass.add(bass_note, Duration.QUARTER)
|
||||
bass.add(bass_fifth, Duration.QUARTER)
|
||||
bass.add(bass_note, Duration.QUARTER)
|
||||
|
||||
# Lead: generate a melodic phrase from scale tones
|
||||
chord_tones = [t.name for t in chord.tones]
|
||||
beats_remaining = bars_per_chord * beats_per_bar
|
||||
|
||||
while beats_remaining > 0.5:
|
||||
# Pick a note — prefer chord tones, allow scale tones
|
||||
if random.random() < 0.6:
|
||||
note_name = random.choice(chord_tones)
|
||||
else:
|
||||
note_name = random.choice(scale_tones)
|
||||
|
||||
octave = random.choice([4, 5]) if pass_num == 0 else random.choice([5, 5, 6])
|
||||
|
||||
# Pick a duration
|
||||
dur_choices = [0.5, 0.67, 1.0, 1.5, 2.0]
|
||||
dur = random.choice([d for d in dur_choices if d <= beats_remaining])
|
||||
|
||||
# Occasional rest for breathing room
|
||||
if random.random() < 0.2:
|
||||
rest_dur = random.choice([0.5, 1.0])
|
||||
rest_dur = min(rest_dur, beats_remaining)
|
||||
lead.rest(rest_dur)
|
||||
beats_remaining -= rest_dur
|
||||
if beats_remaining <= 0:
|
||||
break
|
||||
|
||||
lead.add(f"{note_name}{octave}", dur)
|
||||
beats_remaining -= dur
|
||||
|
||||
# Bells: sparse accents on chord tones
|
||||
bell_beats = bars_per_chord * beats_per_bar
|
||||
bell_pos = 0
|
||||
while bell_pos < bell_beats:
|
||||
if random.random() < 0.25:
|
||||
note_name = random.choice(chord_tones)
|
||||
bells.add(f"{note_name}6", random.choice([1.5, 2.0, 3.0]))
|
||||
bell_pos += 3
|
||||
else:
|
||||
bells.rest(random.choice([1.0, 2.0]))
|
||||
bell_pos += 2
|
||||
|
||||
return score, {
|
||||
"mood": mood_name,
|
||||
"key": f"{tonic} {mode}",
|
||||
"progression": " → ".join(numerals),
|
||||
"bpm": bpm,
|
||||
"time_sig": time_sig,
|
||||
"drums": f"{drum_preset} + {fill_preset} fill",
|
||||
"lead": f"{lead_synth} ({lead_envelope})",
|
||||
"arp": f"{arp_synth} {'legato+glide' if use_legato else 'staccato'} {arp_pattern} {arp_octaves}oct",
|
||||
"pad": pad_synth,
|
||||
"bass": bass_synth,
|
||||
}
|
||||
|
||||
|
||||
# ── Main ───────────────────────────────────────────────────────────────────
|
||||
|
||||
if __name__ == "__main__":
|
||||
try:
|
||||
print()
|
||||
print(" PyTheory Showoff")
|
||||
print(" " + "=" * 50)
|
||||
print(" Generative composition — different every time")
|
||||
print()
|
||||
|
||||
while True:
|
||||
score, info = generate()
|
||||
|
||||
print(f" ♫ Mood: {info['mood']}")
|
||||
print(f" Key: {info['key']}")
|
||||
print(f" Progression: {info['progression']}")
|
||||
print(f" Tempo: {info['bpm']} bpm ({info['time_sig']})")
|
||||
print(f" Drums: {info['drums']}")
|
||||
print(f" Lead: {info['lead']}")
|
||||
print(f" Arp: {info['arp']}")
|
||||
print(f" Pad: {info['pad']} | Bass: {info['bass']}")
|
||||
print(f" {score}")
|
||||
print()
|
||||
|
||||
buf = render_score(score)
|
||||
sd.play(buf, SAMPLE_RATE)
|
||||
sd.wait()
|
||||
|
||||
print()
|
||||
again = input(" Play another? (y/n): ").strip().lower()
|
||||
if again != "y":
|
||||
break
|
||||
print()
|
||||
|
||||
except KeyboardInterrupt:
|
||||
sd.stop()
|
||||
print("\n\n ♫")
|
||||
+2387
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,49 @@
|
||||
"""Compare equal, Pythagorean, and meantone temperaments."""
|
||||
|
||||
import math
|
||||
from pytheory import Tone
|
||||
|
||||
a4 = Tone.from_string("A4", system="western")
|
||||
|
||||
print("Temperament Comparison")
|
||||
print("=" * 75)
|
||||
print()
|
||||
print(f"{'Note':>5s} {'Equal (Hz)':>12s} {'Pythag (Hz)':>12s} {'Meantone (Hz)':>14s} {'P diff':>8s} {'M diff':>8s}")
|
||||
print(f"{'─' * 5} {'─' * 12} {'─' * 12} {'─' * 14} {'─' * 8} {'─' * 8}")
|
||||
|
||||
for semitones in range(13):
|
||||
tone = a4 + semitones
|
||||
|
||||
equal = tone.pitch(temperament="equal")
|
||||
pyth = tone.pitch(temperament="pythagorean")
|
||||
mean = tone.pitch(temperament="meantone")
|
||||
|
||||
# Difference in cents (1 cent = 1/100 of a semitone)
|
||||
pyth_cents = 1200 * math.log2(pyth / equal) if pyth > 0 else 0
|
||||
mean_cents = 1200 * math.log2(mean / equal) if mean > 0 else 0
|
||||
|
||||
print(
|
||||
f"{tone.name:>5s} {equal:>12.3f} {pyth:>12.3f} {mean:>14.3f}"
|
||||
f" {pyth_cents:>+7.1f}¢ {mean_cents:>+7.1f}¢"
|
||||
)
|
||||
|
||||
print()
|
||||
print("Key intervals to listen for:")
|
||||
print()
|
||||
|
||||
intervals = [
|
||||
(4, "Major 3rd", "Meantone is pure (5:4), equal is sharp, Pythagorean sharper still"),
|
||||
(7, "Perfect 5th", "Pythagorean is pure (3:2), equal is slightly flat, meantone flatter"),
|
||||
(6, "Tritone", "The 'devil's interval' — all three temperaments handle it differently"),
|
||||
]
|
||||
|
||||
for semitones, name, note in intervals:
|
||||
tone = a4 + semitones
|
||||
equal = tone.pitch(temperament="equal")
|
||||
pyth = tone.pitch(temperament="pythagorean")
|
||||
mean = tone.pitch(temperament="meantone")
|
||||
|
||||
print(f" {name} ({a4.name}→{tone.name}):")
|
||||
print(f" Equal: {equal:.3f} Hz | Pythagorean: {pyth:.3f} Hz | Meantone: {mean:.3f} Hz")
|
||||
print(f" {note}")
|
||||
print()
|
||||
@@ -0,0 +1,677 @@
|
||||
{
|
||||
"cells": [
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"# PyTheory: Music Theory for Humans\n",
|
||||
"\n",
|
||||
"A hands-on tutorial exploring music theory with Python.\n",
|
||||
"\n",
|
||||
"PyTheory lets you reason about tones, scales, chords, and progressions\n",
|
||||
"using an intuitive, Pythonic API. Whether you're a musician who codes\n",
|
||||
"or a coder who plays music, this library gives you the building blocks\n",
|
||||
"to explore harmony, composition, and world music systems."
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"## 1. Getting Started\n",
|
||||
"\n",
|
||||
"Everything begins with a **Tone** -- the fundamental unit of music.\n",
|
||||
"A tone has a name (like `C`, `F#`, or `Bb`), an optional octave number,\n",
|
||||
"and a frequency in Hz computed from equal temperament tuning (A4 = 440 Hz)."
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"from pytheory import Tone, TonedScale, Key, Chord, Fretboard, CHARTS, Interval\n",
|
||||
"from pytheory import analyze_progression\n",
|
||||
"from pytheory.scales import PROGRESSIONS"
|
||||
],
|
||||
"outputs": [],
|
||||
"execution_count": null
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"# Create tones with octave numbers (scientific pitch notation)\n",
|
||||
"middle_c = Tone.from_string(\"C4\")\n",
|
||||
"concert_a = Tone.from_string(\"A4\")\n",
|
||||
"\n",
|
||||
"print(f\"Middle C: {middle_c} -> {middle_c.frequency:.2f} Hz\")\n",
|
||||
"print(f\"Concert A: {concert_a} -> {concert_a.frequency:.2f} Hz\")\n",
|
||||
"print(f\"MIDI note: {middle_c.midi}\")\n",
|
||||
"print(f\"Is natural? {middle_c.is_natural}\")"
|
||||
],
|
||||
"outputs": [],
|
||||
"execution_count": null
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"# Create tones from frequencies or MIDI numbers\n",
|
||||
"from_hz = Tone.from_frequency(440.0)\n",
|
||||
"from_midi = Tone.from_midi(60)\n",
|
||||
"\n",
|
||||
"print(f\"440 Hz -> {from_hz}\")\n",
|
||||
"print(f\"MIDI 60 -> {from_midi}\")"
|
||||
],
|
||||
"outputs": [],
|
||||
"execution_count": null
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"# Explore the harmonic series -- the physics behind consonance\n",
|
||||
"c3 = Tone.from_string(\"C3\")\n",
|
||||
"harmonics = c3.overtones(8)\n",
|
||||
"print(f\"Harmonic series of {c3} ({c3.frequency:.1f} Hz):\")\n",
|
||||
"for i, hz in enumerate(harmonics, 1):\n",
|
||||
" print(f\" Harmonic {i}: {hz:.1f} Hz\")"
|
||||
],
|
||||
"outputs": [],
|
||||
"execution_count": null
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"## 2. Tone Arithmetic\n",
|
||||
"\n",
|
||||
"Tones support arithmetic operations. Adding an integer to a tone raises it\n",
|
||||
"by that many **semitones** (half steps). Subtracting two tones gives the\n",
|
||||
"semitone distance between them. You can also compare tones by pitch."
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"c4 = Tone.from_string(\"C4\")\n",
|
||||
"\n",
|
||||
"# Add semitones: C + 4 semitones = E (a major third)\n",
|
||||
"e4 = c4 + 4\n",
|
||||
"g4 = c4 + Interval.PERFECT_FIFTH\n",
|
||||
"print(f\"{c4} + 4 semitones = {e4}\")\n",
|
||||
"print(f\"{c4} + perfect 5th = {g4}\")\n",
|
||||
"\n",
|
||||
"# Subtract to find interval distance\n",
|
||||
"distance = g4 - c4\n",
|
||||
"print(f\"\\nDistance from {c4} to {g4}: {distance} semitones\")"
|
||||
],
|
||||
"outputs": [],
|
||||
"execution_count": null
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"# Name the interval between two tones\n",
|
||||
"print(f\"{c4} -> {e4}: {c4.interval_to(e4)}\")\n",
|
||||
"print(f\"{c4} -> {g4}: {c4.interval_to(g4)}\")\n",
|
||||
"\n",
|
||||
"c5 = Tone.from_string(\"C5\")\n",
|
||||
"print(f\"{c4} -> {c5}: {c4.interval_to(c5)}\")\n",
|
||||
"\n",
|
||||
"# Compare tones by pitch\n",
|
||||
"print(f\"\\n{c4} < {g4}? {c4 < g4}\")\n",
|
||||
"print(f\"{c4} == {c4}? {c4 == c4}\")"
|
||||
],
|
||||
"outputs": [],
|
||||
"execution_count": null
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"# The circle of fifths -- the backbone of Western harmony\n",
|
||||
"c = Tone.from_string(\"C4\")\n",
|
||||
"fifths = c.circle_of_fifths()\n",
|
||||
"print(\"Circle of fifths from C:\")\n",
|
||||
"print(\" -> \".join(str(t) for t in fifths))"
|
||||
],
|
||||
"outputs": [],
|
||||
"execution_count": null
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"## 3. Scales and Modes\n",
|
||||
"\n",
|
||||
"A **scale** is a collection of tones arranged in ascending order.\n",
|
||||
"The `TonedScale` class provides access to dozens of scales from a given tonic.\n",
|
||||
"\n",
|
||||
"**Modes** are rotations of the same set of intervals. The seven modes of the\n",
|
||||
"major scale each have a distinct character:\n",
|
||||
"\n",
|
||||
"| Mode | Character |\n",
|
||||
"|------------|--------------------|\n",
|
||||
"| Ionian | Bright, happy |\n",
|
||||
"| Dorian | Jazzy, soulful |\n",
|
||||
"| Phrygian | Spanish, dark |\n",
|
||||
"| Lydian | Dreamy, floating |\n",
|
||||
"| Mixolydian | Bluesy, rock |\n",
|
||||
"| Aeolian | Sad, natural minor |\n",
|
||||
"| Locrian | Tense, unstable |"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"# Build a scale from a tonic\n",
|
||||
"ts = TonedScale(tonic=\"C4\")\n",
|
||||
"\n",
|
||||
"# See all available scale names\n",
|
||||
"print(\"Available scales:\")\n",
|
||||
"for name in ts.scales:\n",
|
||||
" print(f\" {name}\")"
|
||||
],
|
||||
"outputs": [],
|
||||
"execution_count": null
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"# Get a specific scale and iterate its tones\n",
|
||||
"c_major = ts[\"major\"]\n",
|
||||
"print(f\"C major: {c_major.note_names}\")\n",
|
||||
"\n",
|
||||
"c_minor = ts[\"minor\"]\n",
|
||||
"print(f\"C minor: {c_minor.note_names}\")\n",
|
||||
"\n",
|
||||
"# Check if a note belongs to the scale\n",
|
||||
"print(f\"\\nIs F# in C major? {'F#' in c_major}\")\n",
|
||||
"print(f\"Is G in C major? {'G' in c_major}\")"
|
||||
],
|
||||
"outputs": [],
|
||||
"execution_count": null
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"metadata": {},
|
||||
"source": "from pytheory.scales import Scale\n\n# Transpose a scale\nd_major = c_major.transpose(2)\nprint(f\"D major (C major transposed up 2): {d_major.note_names}\")\n\n# Detect a scale from a set of notes\nresult = Scale.detect(\"A\", \"B\", \"C#\", \"D\", \"E\", \"F#\", \"G#\")\nprint(f\"\\nDetected scale: {result}\")",
|
||||
"outputs": [],
|
||||
"execution_count": null
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"## 4. The Key Class\n",
|
||||
"\n",
|
||||
"A **Key** is the most convenient entry point for working with harmony.\n",
|
||||
"It wraps a tonic and mode, giving you instant access to scales, diatonic\n",
|
||||
"chords, key signatures, and related keys."
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"key = Key(\"C\", \"major\")\n",
|
||||
"\n",
|
||||
"print(f\"Key: {key}\")\n",
|
||||
"print(f\"Notes: {key.note_names}\")\n",
|
||||
"print(f\"Signature: {key.signature}\")"
|
||||
],
|
||||
"outputs": [],
|
||||
"execution_count": null
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"# Diatonic triads -- the seven chords built from the scale\n",
|
||||
"print(\"Diatonic triads in C major:\")\n",
|
||||
"for i, name in enumerate(key.chords, 1):\n",
|
||||
" print(f\" {i}. {name}\")"
|
||||
],
|
||||
"outputs": [],
|
||||
"execution_count": null
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"# Seventh chords add richness and color\n",
|
||||
"print(\"Diatonic seventh chords in C major:\")\n",
|
||||
"for i, name in enumerate(key.seventh_chords, 1):\n",
|
||||
" print(f\" {i}. {name}\")"
|
||||
],
|
||||
"outputs": [],
|
||||
"execution_count": null
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"# Related keys\n",
|
||||
"print(f\"Relative minor of C major: {key.relative}\")\n",
|
||||
"print(f\"Parallel minor of C major: {key.parallel}\")\n",
|
||||
"\n",
|
||||
"# Key signatures for sharp and flat keys\n",
|
||||
"for tonic in [\"G\", \"D\", \"F\", \"Bb\"]:\n",
|
||||
" k = Key(tonic, \"major\")\n",
|
||||
" sig = k.signature\n",
|
||||
" print(f\"{k}: {sig['sharps']} sharps, {sig['flats']} flats -> {sig['accidentals']}\")"
|
||||
],
|
||||
"outputs": [],
|
||||
"execution_count": null
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"## 5. Chord Analysis\n",
|
||||
"\n",
|
||||
"Chords can be created from note names, intervals, chord symbols, or MIDI.\n",
|
||||
"PyTheory can identify chord quality, measure tension and consonance,\n",
|
||||
"and compute optimal voice leading between chords."
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"# Multiple ways to create chords\n",
|
||||
"c_major_chord = Chord.from_tones(\"C\", \"E\", \"G\")\n",
|
||||
"g7 = Chord.from_intervals(\"G\", 4, 7, 10)\n",
|
||||
"am = Chord.from_name(\"Am\")\n",
|
||||
"\n",
|
||||
"print(f\"{c_major_chord} (intervals: {c_major_chord.intervals})\")\n",
|
||||
"print(f\"{g7} (intervals: {g7.intervals})\")\n",
|
||||
"print(f\"{am} (intervals: {am.intervals})\")"
|
||||
],
|
||||
"outputs": [],
|
||||
"execution_count": null
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"# Analyze harmonic tension\n",
|
||||
"# The dominant 7th chord is the most tension-filled chord in tonal music\n",
|
||||
"print(\"Tension analysis:\")\n",
|
||||
"for chord in [c_major_chord, am, g7]:\n",
|
||||
" t = chord.tension\n",
|
||||
" print(f\" {chord.identify():20s} -> score={t['score']:.2f}, \"\n",
|
||||
" f\"tritones={t['tritones']}, dominant={t['has_dominant_function']}\")"
|
||||
],
|
||||
"outputs": [],
|
||||
"execution_count": null
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"# Consonance vs dissonance (psychoacoustic measures)\n",
|
||||
"print(f\"{'Chord':20s} {'Harmony':>10s} {'Dissonance':>12s}\")\n",
|
||||
"print(\"-\" * 44)\n",
|
||||
"for chord in [c_major_chord, am, g7]:\n",
|
||||
" print(f\"{chord.identify():20s} {chord.harmony:10.4f} {chord.dissonance:12.4f}\")"
|
||||
],
|
||||
"outputs": [],
|
||||
"execution_count": null
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"# Voice leading: how individual notes move between chords\n",
|
||||
"f_major = Chord.from_tones(\"F\", \"A\", \"C\")\n",
|
||||
"vl = c_major_chord.voice_leading(f_major)\n",
|
||||
"\n",
|
||||
"print(f\"Voice leading: {c_major_chord.identify()} -> {f_major.identify()}\")\n",
|
||||
"for src, dst, movement in vl:\n",
|
||||
" direction = \"up\" if movement > 0 else \"down\" if movement < 0 else \"stays\"\n",
|
||||
" print(f\" {src} -> {dst} ({movement:+d} semitones, {direction})\")"
|
||||
],
|
||||
"outputs": [],
|
||||
"execution_count": null
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"# Inversions rearrange chord voicings\n",
|
||||
"print(f\"Root position: {[t.full_name for t in c_major_chord.tones]}\")\n",
|
||||
"print(f\"1st inversion: {[t.full_name for t in c_major_chord.inversion(1).tones]}\")\n",
|
||||
"print(f\"2nd inversion: {[t.full_name for t in c_major_chord.inversion(2).tones]}\")"
|
||||
],
|
||||
"outputs": [],
|
||||
"execution_count": null
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"## 6. Chord Progressions\n",
|
||||
"\n",
|
||||
"Chord progressions are the harmonic backbone of songs. PyTheory supports\n",
|
||||
"both **Roman numeral** analysis (classical/jazz) and the **Nashville number\n",
|
||||
"system** (studio shorthand). It also ships with common progressions built in."
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"key = Key(\"G\", \"major\")\n",
|
||||
"\n",
|
||||
"# Build a progression from Roman numerals\n",
|
||||
"prog = key.progression(\"I\", \"V\", \"vi\", \"IV\")\n",
|
||||
"print(\"I - V - vi - IV in G major (the 'four chord song'):\")\n",
|
||||
"for chord in prog:\n",
|
||||
" print(f\" {chord.identify()}\")"
|
||||
],
|
||||
"outputs": [],
|
||||
"execution_count": null
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"# Nashville number system -- same thing, Arabic numerals\n",
|
||||
"nashville = key.nashville(1, 5, 6, 4)\n",
|
||||
"print(\"Nashville 1-5-6-4 in G major:\")\n",
|
||||
"for chord in nashville:\n",
|
||||
" print(f\" {chord.identify()}\")"
|
||||
],
|
||||
"outputs": [],
|
||||
"execution_count": null
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"# Browse the built-in progression library\n",
|
||||
"print(\"Built-in progressions:\")\n",
|
||||
"for name, numerals in PROGRESSIONS.items():\n",
|
||||
" print(f\" {name:25s} -> {' '.join(numerals)}\")"
|
||||
],
|
||||
"outputs": [],
|
||||
"execution_count": null
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"# Analyze an existing chord progression\n",
|
||||
"chords = [Chord.from_name(\"C\"), Chord.from_name(\"Am\"),\n",
|
||||
" Chord.from_name(\"F\"), Chord.from_name(\"G\")]\n",
|
||||
"numerals = analyze_progression(chords, key=\"C\")\n",
|
||||
"print(\"Progression analysis in C:\")\n",
|
||||
"for chord, numeral in zip(chords, numerals):\n",
|
||||
" print(f\" {chord.identify():15s} -> {numeral}\")"
|
||||
],
|
||||
"outputs": [],
|
||||
"execution_count": null
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"## 7. World Music Systems\n",
|
||||
"\n",
|
||||
"Music theory extends far beyond Western harmony. PyTheory includes scale\n",
|
||||
"systems from several traditions:\n",
|
||||
"\n",
|
||||
"- **Indian** (raga/thaat) -- 10 parent scales covering all of Hindustani music\n",
|
||||
"- **Arabic** (maqam) -- modal systems with characteristic augmented seconds\n",
|
||||
"- **Japanese** -- pentatonic scales used in koto, shamisen, and folk music\n",
|
||||
"- **Blues** -- the scales that built American popular music\n",
|
||||
"- **Gamelan** -- Javanese/Balinese tuning systems (12-TET approximations)"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"from pytheory import SYSTEMS\n",
|
||||
"\n",
|
||||
"# Indian thaat system\n",
|
||||
"indian = TonedScale(tonic=\"C4\", system=SYSTEMS[\"indian\"])\n",
|
||||
"print(\"Indian thaats from C:\")\n",
|
||||
"for name in indian.scales:\n",
|
||||
" scale = indian[name]\n",
|
||||
" print(f\" {name:12s} -> {scale.note_names}\")"
|
||||
],
|
||||
"outputs": [],
|
||||
"execution_count": null
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"# Arabic maqam -- the Hijaz scale has a distinctive augmented 2nd\n",
|
||||
"arabic = TonedScale(tonic=\"D4\", system=SYSTEMS[\"arabic\"])\n",
|
||||
"hijaz = arabic[\"hijaz\"]\n",
|
||||
"print(f\"Maqam Hijaz from D: {hijaz.note_names}\")\n",
|
||||
"\n",
|
||||
"# Japanese hirajoshi -- hauntingly beautiful pentatonic\n",
|
||||
"japanese = TonedScale(tonic=\"A4\", system=SYSTEMS[\"japanese\"])\n",
|
||||
"hirajoshi = japanese[\"hirajoshi\"]\n",
|
||||
"print(f\"Hirajoshi from A: {hirajoshi.note_names}\")"
|
||||
],
|
||||
"outputs": [],
|
||||
"execution_count": null
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"# Blues scales -- the foundation of rock, jazz, and R&B\n",
|
||||
"blues = TonedScale(tonic=\"A4\", system=SYSTEMS[\"blues\"])\n",
|
||||
"print(\"Blues scales from A:\")\n",
|
||||
"for name in blues.scales:\n",
|
||||
" scale = blues[name]\n",
|
||||
" print(f\" {name:20s} -> {scale.note_names}\")\n",
|
||||
"\n",
|
||||
"# Gamelan -- approximations of non-Western tuning\n",
|
||||
"gamelan = TonedScale(tonic=\"C4\", system=SYSTEMS[\"gamelan\"])\n",
|
||||
"slendro = gamelan[\"slendro\"]\n",
|
||||
"print(f\"\\nGamelan slendro from C: {slendro.note_names}\")"
|
||||
],
|
||||
"outputs": [],
|
||||
"execution_count": null
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"## 8. Guitar and Instruments\n",
|
||||
"\n",
|
||||
"The `Fretboard` class models stringed instruments. You can look up\n",
|
||||
"chord fingerings, render tab diagrams, apply a capo, and visualize\n",
|
||||
"scale patterns across the neck."
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"# Standard guitar fretboard\n",
|
||||
"guitar = Fretboard.guitar()\n",
|
||||
"print(f\"Standard tuning: {guitar}\")\n",
|
||||
"\n",
|
||||
"# Look up chord fingerings from the chart\n",
|
||||
"c_chart = CHARTS[\"western\"][\"C\"]\n",
|
||||
"print(f\"\\n{c_chart.tab(fretboard=guitar)}\")"
|
||||
],
|
||||
"outputs": [],
|
||||
"execution_count": null
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"# Show several common chord shapes\n",
|
||||
"for chord_name in [\"G\", \"Am\", \"Em\", \"D\"]:\n",
|
||||
" chart = CHARTS[\"western\"][chord_name]\n",
|
||||
" print(chart.tab(fretboard=guitar))\n",
|
||||
" print()"
|
||||
],
|
||||
"outputs": [],
|
||||
"execution_count": null
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"# Apply a capo -- raises all strings by N semitones\n",
|
||||
"capo2 = Fretboard.guitar(capo=2)\n",
|
||||
"print(f\"Capo on fret 2: {capo2}\")\n",
|
||||
"print(\"Playing 'G shape' with capo 2 = A major voicing\")"
|
||||
],
|
||||
"outputs": [],
|
||||
"execution_count": null
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"# Scale diagram -- see where notes fall on the neck\n",
|
||||
"c_major_scale = TonedScale(tonic=\"C4\")[\"major\"]\n",
|
||||
"diagram = guitar.scale_diagram(c_major_scale, frets=12)\n",
|
||||
"print(\"C major scale on guitar:\")\n",
|
||||
"print(diagram)"
|
||||
],
|
||||
"outputs": [],
|
||||
"execution_count": null
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"## 9. Building a Song\n",
|
||||
"\n",
|
||||
"Let's put it all together: pick a key, explore its chords, build a\n",
|
||||
"progression, and analyze the harmonic movement."
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"# Step 1: Choose a key\n",
|
||||
"song_key = Key(\"E\", \"minor\")\n",
|
||||
"print(f\"Key: {song_key}\")\n",
|
||||
"print(f\"Notes: {song_key.note_names}\")\n",
|
||||
"print(f\"Relative major: {song_key.relative}\")\n",
|
||||
"print(f\"Signature: {song_key.signature}\")"
|
||||
],
|
||||
"outputs": [],
|
||||
"execution_count": null
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"# Step 2: See what chords are available\n",
|
||||
"print(\"Diatonic chords in E minor:\")\n",
|
||||
"for i, name in enumerate(song_key.chords, 1):\n",
|
||||
" print(f\" {i}. {name}\")\n",
|
||||
"\n",
|
||||
"print(\"\\nBorrowed chords from E major:\")\n",
|
||||
"for name in song_key.borrowed_chords[:4]:\n",
|
||||
" print(f\" {name}\")"
|
||||
],
|
||||
"outputs": [],
|
||||
"execution_count": null
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"# Step 3: Build a progression\n",
|
||||
"# i - VI - III - VII is a classic minor key progression\n",
|
||||
"prog = song_key.progression(\"i\", \"VI\", \"III\", \"VII\")\n",
|
||||
"\n",
|
||||
"print(\"Progression: i - VI - III - VII\")\n",
|
||||
"for chord in prog:\n",
|
||||
" name = chord.identify()\n",
|
||||
" numeral = chord.analyze(\"E\", \"minor\")\n",
|
||||
" t = chord.tension\n",
|
||||
" print(f\" {name:18s} [{numeral:5s}] tension={t['score']:.2f}\")"
|
||||
],
|
||||
"outputs": [],
|
||||
"execution_count": null
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"# Step 4: Analyze voice leading through the progression\n",
|
||||
"print(\"Voice leading through the progression:\\n\")\n",
|
||||
"for i in range(len(prog) - 1):\n",
|
||||
" src = prog[i]\n",
|
||||
" dst = prog[i + 1]\n",
|
||||
" vl = src.voice_leading(dst)\n",
|
||||
" total = sum(abs(m) for _, _, m in vl)\n",
|
||||
" print(f\"{src.identify()} -> {dst.identify()} (total movement: {total} semitones)\")\n",
|
||||
" for s, d, m in vl:\n",
|
||||
" print(f\" {s} -> {d} ({m:+d})\")\n",
|
||||
" print()"
|
||||
],
|
||||
"outputs": [],
|
||||
"execution_count": null
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"# Step 5: Show the chords on guitar\n",
|
||||
"guitar = Fretboard.guitar()\n",
|
||||
"chord_names = [\"Em\", \"C\", \"G\", \"D\"]\n",
|
||||
"\n",
|
||||
"print(\"Guitar charts for the progression:\\n\")\n",
|
||||
"for name in chord_names:\n",
|
||||
" chart = CHARTS[\"western\"][name]\n",
|
||||
" print(chart.tab(fretboard=guitar))\n",
|
||||
" print()"
|
||||
],
|
||||
"outputs": [],
|
||||
"execution_count": null
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"# Bonus: Detect the key from a set of notes\n",
|
||||
"detected = Key.detect(\"E\", \"G\", \"A\", \"B\", \"D\")\n",
|
||||
"print(f\"Key detected from [E, G, A, B, D]: {detected}\")\n",
|
||||
"\n",
|
||||
"# Secondary dominant -- adds harmonic color\n",
|
||||
"v_of_v = song_key.secondary_dominant(5)\n",
|
||||
"print(f\"\\nSecondary dominant V/V in E minor: {v_of_v.identify()}\")\n",
|
||||
"print(f\"Tension score: {v_of_v.tension['score']:.2f}\")"
|
||||
],
|
||||
"outputs": [],
|
||||
"execution_count": null
|
||||
}
|
||||
],
|
||||
"metadata": {
|
||||
"kernelspec": {
|
||||
"display_name": "Python 3",
|
||||
"language": "python",
|
||||
"name": "python3"
|
||||
},
|
||||
"language_info": {
|
||||
"name": "python",
|
||||
"version": "3.12.0"
|
||||
}
|
||||
},
|
||||
"nbformat": 4,
|
||||
"nbformat_minor": 5
|
||||
}
|
||||
@@ -0,0 +1,68 @@
|
||||
"""Explore scales from six musical traditions around the world."""
|
||||
|
||||
from pytheory import TonedScale
|
||||
|
||||
systems = [
|
||||
("western", "C4", [
|
||||
("major", "The foundation of Western tonal music"),
|
||||
("minor", "Natural minor — dark and introspective"),
|
||||
("harmonic minor", "Raised 7th — classical, Middle Eastern flavor"),
|
||||
("dorian", "Jazz, funk, soul (So What, Scarborough Fair)"),
|
||||
("mixolydian", "Blues, rock (Norwegian Wood, Sweet Home Alabama)"),
|
||||
("phrygian", "Flamenco, metal (White Rabbit)"),
|
||||
("lydian", "Dreamy, floating (The Simpsons theme)"),
|
||||
]),
|
||||
("indian", "Sa4", [
|
||||
("bilawal", "Equivalent to Western major scale"),
|
||||
("bhairav", "Morning raga — devotional, meditative"),
|
||||
("kafi", "Equivalent to Dorian mode — romantic, earthy"),
|
||||
("bhairavi", "Equivalent to Phrygian — melancholic, devotional"),
|
||||
("kalyan", "Equivalent to Lydian — serene, uplifting"),
|
||||
]),
|
||||
("arabic", "Do4", [
|
||||
("ajam", "Equivalent to Western major scale"),
|
||||
("hijaz", "The quintessential 'Middle Eastern' sound"),
|
||||
("bayati", "Contemplative, spiritual — most common maqam"),
|
||||
("rast", "Bright, festive — the 'mother' of maqamat"),
|
||||
("nahawand", "Equivalent to Western minor — melancholic"),
|
||||
]),
|
||||
("japanese", "C4", [
|
||||
("hirajoshi", "Haunting pentatonic — koto music"),
|
||||
("in", "Dark pentatonic — court music, Buddhist chant"),
|
||||
("yo", "Bright pentatonic — folk songs, festival music"),
|
||||
("iwato", "Sparse, mysterious — zen atmosphere"),
|
||||
("kumoi", "Gentle pentatonic — lyrical, nostalgic"),
|
||||
("ritsu", "Elegant heptatonic — gagaku court music"),
|
||||
]),
|
||||
("blues", "C4", [
|
||||
("blues", "The 6-note blues scale with the 'blue note'"),
|
||||
("minor pentatonic", "The backbone of rock guitar solos"),
|
||||
("major pentatonic", "Bright, open — country, folk, pop"),
|
||||
]),
|
||||
("gamelan", "nem4", [
|
||||
("slendro", "5-note near-equal division — metallic, shimmering"),
|
||||
("pelog", "7-note unequal — mysterious, otherworldly"),
|
||||
("pelog nem", "Pelog mode on nem — the most common mode"),
|
||||
("pelog barang", "Pelog mode on barang — bright, festive"),
|
||||
]),
|
||||
]
|
||||
|
||||
for system_name, tonic, scales in systems:
|
||||
print(f"{'═' * 65}")
|
||||
print(f" {system_name.upper()}")
|
||||
print(f"{'═' * 65}")
|
||||
|
||||
ts = TonedScale(tonic=tonic, system=system_name)
|
||||
|
||||
for scale_name, description in scales:
|
||||
try:
|
||||
scale = ts[scale_name]
|
||||
notes = " ".join(scale.note_names)
|
||||
print(f" {scale_name:20s} {notes}")
|
||||
print(f" {'':20s} {description}")
|
||||
print()
|
||||
except (KeyError, IndexError, ValueError):
|
||||
print(f" {scale_name:20s} (not available)")
|
||||
print()
|
||||
|
||||
print(f"{'═' * 65}")
|
||||
+34
-4
@@ -1,20 +1,50 @@
|
||||
[project]
|
||||
name = "pytheory"
|
||||
version = "0.2.0"
|
||||
version = "0.36.4"
|
||||
description = "Music Theory for Humans"
|
||||
readme = "README.md"
|
||||
license = "MIT"
|
||||
requires-python = ">=3.10"
|
||||
|
||||
authors = [
|
||||
{ name = "Kenneth Reitz", email = "me@kennethreitz.org" },
|
||||
]
|
||||
classifiers = [
|
||||
"Development Status :: 3 - Alpha",
|
||||
"Intended Audience :: Developers",
|
||||
"Intended Audience :: Education",
|
||||
"Topic :: Multimedia :: Sound/Audio",
|
||||
"Topic :: Multimedia :: Sound/Audio :: Analysis",
|
||||
"Programming Language :: Python :: 3",
|
||||
"Programming Language :: Python :: 3.10",
|
||||
"Programming Language :: Python :: 3.11",
|
||||
"Programming Language :: Python :: 3.12",
|
||||
"Programming Language :: Python :: 3.13",
|
||||
]
|
||||
dependencies = [
|
||||
"pytuning",
|
||||
"numeral",
|
||||
"sounddevice",
|
||||
"scipy",
|
||||
]
|
||||
|
||||
[project.urls]
|
||||
Homepage = "https://github.com/kennethreitz/pytheory"
|
||||
Documentation = "https://pytheory.kennethreitz.org"
|
||||
Repository = "https://github.com/kennethreitz/pytheory"
|
||||
Issues = "https://github.com/kennethreitz/pytheory/issues"
|
||||
|
||||
[project.scripts]
|
||||
pytheory = "pytheory.cli:main"
|
||||
|
||||
[dependency-groups]
|
||||
dev = ["pytest"]
|
||||
docs = ["sphinx"]
|
||||
docs = ["sphinx", "myst-parser"]
|
||||
|
||||
[build-system]
|
||||
requires = ["setuptools"]
|
||||
build-backend = "setuptools.build_meta"
|
||||
|
||||
[tool.pytest.ini_options]
|
||||
markers = ["slow: marks tests as slow (deselect with '-m \"not slow\"')"]
|
||||
|
||||
[tool.setuptools]
|
||||
packages = ["pytheory"]
|
||||
|
||||
+28
-12
@@ -1,13 +1,29 @@
|
||||
from math import ceil, floor
|
||||
"""PyTheory: Music Theory for Humans."""
|
||||
|
||||
from .tones import Tone
|
||||
from .systems import System, SYSTEMS
|
||||
from .scales import Scale, TonedScale
|
||||
from .chords import Chord, Fretboard
|
||||
from .charts import CHARTS, charts_for_fretboard
|
||||
try:
|
||||
from .play import play, Synth
|
||||
except OSError:
|
||||
# sounddevice requires PortAudio; gracefully degrade if unavailable
|
||||
play = None
|
||||
Synth = None
|
||||
__version__ = "0.36.4"
|
||||
|
||||
from .tones import Tone, Interval
|
||||
from .systems import System, SYSTEMS, TET
|
||||
from .scales import TonedScale, Key, PROGRESSIONS
|
||||
from .chords import Chord, Fretboard, analyze_progression
|
||||
from .charts import CHARTS, Fingering, charts_for_fretboard
|
||||
|
||||
from .rhythm import Duration, TimeSignature, Rest, Score, Part, Section, DrumSound, Pattern, INSTRUMENTS
|
||||
from .rhythm import Note as RhythmNote # rhythm.Note (tone + duration pairing)
|
||||
|
||||
from .play import (play, save, save_midi, play_progression, play_pattern,
|
||||
play_score, render_score, Synth, Envelope)
|
||||
|
||||
# Aliases for discoverability.
|
||||
Note = Tone
|
||||
Scale = TonedScale
|
||||
|
||||
__all__ = [
|
||||
"Tone", "Note", "Interval", "Scale", "TonedScale", "Key",
|
||||
"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",
|
||||
"Duration", "TimeSignature", "RhythmNote", "Rest", "Score", "Part",
|
||||
"DrumSound", "Pattern", "Section", "INSTRUMENTS",
|
||||
]
|
||||
|
||||
+901
-20
@@ -1,10 +1,65 @@
|
||||
from pytuning import scales
|
||||
import math
|
||||
|
||||
REFERENCE_A = 440
|
||||
|
||||
# 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
|
||||
|
||||
|
||||
# ── 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.
|
||||
"""
|
||||
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 _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,
|
||||
}
|
||||
|
||||
TONES = {
|
||||
@@ -21,7 +76,84 @@ TONES = {
|
||||
("F#", "Gb"),
|
||||
("G",),
|
||||
("G#", "Ab"),
|
||||
]
|
||||
],
|
||||
# Indian classical (Hindustani) system.
|
||||
# Ordered A-based to match Western index positions (Sa = index 3 = C).
|
||||
"indian": [
|
||||
("Dha",), # A — shuddha dhaivat
|
||||
("komal Ni",), # Bb — komal nishad
|
||||
("Ni",), # B — shuddha nishad
|
||||
("Sa",), # C — shadja
|
||||
("komal Re",), # Db — komal rishabh
|
||||
("Re",), # D — shuddha rishabh
|
||||
("komal Ga",), # Eb — komal gandhar
|
||||
("Ga",), # E — shuddha gandhar
|
||||
("Ma",), # F — shuddha madhyam
|
||||
("tivra Ma",), # F# — tivra madhyam
|
||||
("Pa",), # G — pancham
|
||||
("komal Dha",), # Ab — komal dhaivat
|
||||
],
|
||||
# Arabic maqam system — Arabic solfège names.
|
||||
"arabic": [
|
||||
("La",), # A
|
||||
("Sib",), # Bb — Si bemol
|
||||
("Si",), # B
|
||||
("Do",), # C
|
||||
("Reb",), # Db — Re bemol
|
||||
("Re",), # D
|
||||
("Mib",), # Eb — Mi bemol
|
||||
("Mi",), # E
|
||||
("Fa",), # F
|
||||
("Fa#",), # F#
|
||||
("Sol",), # G
|
||||
("Solb",), # Ab — Sol bemol
|
||||
],
|
||||
# Japanese system — uses Western names; scales are the unique part.
|
||||
"japanese": [
|
||||
("A",),
|
||||
("A#", "Bb"),
|
||||
("B",),
|
||||
("C",),
|
||||
("C#", "Db"),
|
||||
("D",),
|
||||
("D#", "Eb"),
|
||||
("E",),
|
||||
("F",),
|
||||
("F#", "Gb"),
|
||||
("G",),
|
||||
("G#", "Ab"),
|
||||
],
|
||||
# Blues/Pentatonic — Western names with blues and pentatonic scales.
|
||||
"blues": [
|
||||
("A",),
|
||||
("A#", "Bb"),
|
||||
("B",),
|
||||
("C",),
|
||||
("C#", "Db"),
|
||||
("D",),
|
||||
("D#", "Eb"),
|
||||
("E",),
|
||||
("F",),
|
||||
("F#", "Gb"),
|
||||
("G",),
|
||||
("G#", "Ab"),
|
||||
],
|
||||
# Javanese gamelan — pelog approximation in 12-TET.
|
||||
# True gamelan uses non-Western intonation; these are closest 12-TET fits.
|
||||
"gamelan": [
|
||||
("nem",), # A — 6
|
||||
("pi",), # Bb — 7 (barang in some)
|
||||
("barang",), # B — 7
|
||||
("ji",), # C — 1
|
||||
("ro-",), # Db — 2b
|
||||
("ro",), # D — 2
|
||||
("lu-",), # Eb — 3b
|
||||
("lu",), # E — 3
|
||||
("pat",), # F — 4
|
||||
("pat+",), # F# — 4#
|
||||
("mo",), # G — 5
|
||||
("nem-",), # Ab — 6b
|
||||
],
|
||||
}
|
||||
|
||||
DEGREES = {
|
||||
@@ -34,7 +166,53 @@ DEGREES = {
|
||||
("submediant", ("aeolian", "lydian")),
|
||||
("leading tone", ("locrian", "mixolydian")),
|
||||
("octave", ("ionian", "aeolian")),
|
||||
]
|
||||
],
|
||||
"indian": [
|
||||
("shadja", ()), # Sa — the tonic
|
||||
("rishabh", ()), # Re — 2nd
|
||||
("gandhar", ()), # Ga — 3rd
|
||||
("madhyam", ()), # Ma — 4th
|
||||
("pancham", ()), # Pa — 5th
|
||||
("dhaivat", ()), # Dha — 6th
|
||||
("nishad", ()), # Ni — 7th
|
||||
("saptak", ()), # Sa — octave
|
||||
],
|
||||
"arabic": [
|
||||
("qarar", ()), # 1st — root
|
||||
("nawa", ()), # 2nd
|
||||
("thalth", ()), # 3rd
|
||||
("arba", ()), # 4th
|
||||
("khamis", ()), # 5th
|
||||
("sadis", ()), # 6th
|
||||
("sabi", ()), # 7th
|
||||
("jawab", ()), # octave
|
||||
],
|
||||
"japanese": [
|
||||
("ichi", ()), # 1st
|
||||
("ni", ()), # 2nd
|
||||
("san", ()), # 3rd
|
||||
("shi", ()), # 4th
|
||||
("go", ()), # 5th
|
||||
("roku", ()), # 6th
|
||||
],
|
||||
"blues": [
|
||||
("tonic", ()),
|
||||
("supertonic", ()),
|
||||
("mediant", ()),
|
||||
("subdominant", ()),
|
||||
("dominant", ()),
|
||||
("submediant", ()),
|
||||
("subtonic", ()),
|
||||
],
|
||||
"gamelan": [
|
||||
("ji", ()), # 1
|
||||
("ro", ()), # 2
|
||||
("lu", ()), # 3
|
||||
("pat", ()), # 4
|
||||
("mo", ()), # 5
|
||||
("nem", ()), # 6
|
||||
("pi", ()), # 7
|
||||
],
|
||||
}
|
||||
|
||||
SCALES = {
|
||||
@@ -52,20 +230,723 @@ SCALES = {
|
||||
# "melodic minor": {"minor": True, "melodic": True, "hemitonic": True},
|
||||
},
|
||||
],
|
||||
# TODO: understand this
|
||||
# "hexatonic": (
|
||||
# 6,
|
||||
# {
|
||||
# # name, arguments to scale generator.
|
||||
# "wholetone": {},
|
||||
# "augmented": {},
|
||||
# "prometheus": {},
|
||||
# "blues": {},
|
||||
# },
|
||||
# ),
|
||||
# "pentatonic": (5, {}),
|
||||
# "tetratonic": (4, {}),
|
||||
# "monotonic": (1, {"monotonic": {"hemitonic": False}}),
|
||||
}
|
||||
}
|
||||
|
||||
# Indian scales — the 10 thaats of Hindustani classical music.
|
||||
# Each thaat defines a parent scale from which ragas are derived.
|
||||
INDIAN_SCALES = {
|
||||
12: {
|
||||
"chromatic": (12, {}),
|
||||
"thaat": [
|
||||
7,
|
||||
{
|
||||
# Bilawal = Western major / Ionian
|
||||
"bilawal": {"intervals": (2, 2, 1, 2, 2, 2, 1)},
|
||||
# Khamaj = Western Mixolydian
|
||||
"khamaj": {"intervals": (2, 2, 1, 2, 2, 1, 2)},
|
||||
# Kafi = Western Dorian
|
||||
"kafi": {"intervals": (2, 1, 2, 2, 2, 1, 2)},
|
||||
# Asavari = Western natural minor / Aeolian
|
||||
"asavari": {"intervals": (2, 1, 2, 2, 1, 2, 2)},
|
||||
# Bhairavi = Western Phrygian
|
||||
"bhairavi": {"intervals": (1, 2, 2, 2, 1, 2, 2)},
|
||||
# Kalyan = Western Lydian
|
||||
"kalyan": {"intervals": (2, 2, 2, 1, 2, 2, 1)},
|
||||
# Bhairav — unique to Indian music (no Western equivalent)
|
||||
# Sa re Ga Ma Pa dha Ni
|
||||
"bhairav": {"intervals": (1, 3, 1, 2, 1, 3, 1)},
|
||||
# Poorvi — unique to Indian music
|
||||
# Sa re Ga tivra-Ma Pa dha Ni
|
||||
"poorvi": {"intervals": (1, 3, 2, 1, 1, 3, 1)},
|
||||
# Marwa — unique to Indian music
|
||||
# Sa re Ga tivra-Ma Pa Dha Ni
|
||||
"marwa": {"intervals": (1, 3, 2, 1, 2, 2, 1)},
|
||||
# Todi — unique to Indian music
|
||||
# Sa re komal-Ga tivra-Ma Pa dha Ni
|
||||
"todi": {"intervals": (1, 2, 3, 1, 1, 3, 1)},
|
||||
},
|
||||
],
|
||||
}
|
||||
}
|
||||
|
||||
# ── 22-shruti Indian system ──────────────────────────────────────────────────
|
||||
# The shruti system divides the octave into 22 microtonal steps, capturing
|
||||
# the melodic nuances that 12-TET cannot represent. Each of the 7 swaras
|
||||
# has multiple shruti positions (e.g. komal Re at shruti 2, shuddha Re at
|
||||
# shruti 4). 22-TET is the standard equal-tempered approximation.
|
||||
#
|
||||
# Ordered from Dha (=A) to match Western index positions (Sa at index 5 ≈ C).
|
||||
TONES_SHRUTI = [
|
||||
("Dha",), # 0 — A — shuddha dhaivat (reference = 440 Hz)
|
||||
("atikomal Ni",), # 1 — shruti between Dha and komal Ni
|
||||
("komal Ni",), # 2 — Bb — komal nishad
|
||||
("shuddha Ni",), # 3 — between komal Ni and Ni
|
||||
("Ni",), # 4 — B — shuddha (kakali) nishad
|
||||
("Sa",), # 5 — C — shadja (tonic)
|
||||
("atikomal Re",), # 6 — shruti between Sa and komal Re
|
||||
("komal Re",), # 7 — Db — komal rishabh
|
||||
("shuddha Re",), # 8 — between komal Re and Re
|
||||
("Re",), # 9 — D — chatushruti rishabh
|
||||
("atikomal Ga",), # 10 — shruti between Re and komal Ga
|
||||
("komal Ga",), # 11 — Eb — komal gandhar
|
||||
("Ga",), # 12 — E — antara gandhar
|
||||
("tivra Ga",), # 13 — shruti between Ga and Ma
|
||||
("Ma",), # 14 — F — shuddha madhyam
|
||||
("ekashruti Ma",), # 15 — shruti between Ma and tivra Ma
|
||||
("tivra Ma",), # 16 — F# — tivra madhyam
|
||||
("atitivra Ma",), # 17 — shruti between tivra Ma and Pa
|
||||
("Pa",), # 18 — G — pancham
|
||||
("atikomal Dha",), # 19 — shruti between Pa and komal Dha
|
||||
("komal Dha",), # 20 — Ab — komal dhaivat
|
||||
("shuddha Dha",), # 21 — shruti between komal Dha and Dha
|
||||
]
|
||||
|
||||
DEGREES_SHRUTI = [
|
||||
("shadja", ("bilawal",)), # Sa — tonic
|
||||
("rishabh", ("marwa",)), # Re
|
||||
("gandhar", ("bhairavi",)), # Ga
|
||||
("madhyam", ("kalyan",)), # Ma
|
||||
("pancham", ("kafi",)), # Pa
|
||||
("dhaivat", ("asavari",)), # Dha
|
||||
("nishad", ("khamaj",)), # Ni
|
||||
("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.
|
||||
# Compare to the 12-TET approximations in INDIAN_SCALES which lose
|
||||
# the distinction between 2-shruti and 3-shruti steps.
|
||||
SHRUTI_SCALES = {
|
||||
"chromatic": (22, {}),
|
||||
"thaat": [
|
||||
7,
|
||||
{
|
||||
# Bilawal (≈ Ionian) — Sa Re Ga Ma Pa Dha Ni
|
||||
"bilawal": {"intervals": (4, 3, 2, 4, 4, 3, 2)},
|
||||
# Khamaj (≈ Mixolydian) — Sa Re Ga Ma Pa Dha komal-Ni
|
||||
"khamaj": {"intervals": (4, 3, 2, 4, 4, 1, 4)},
|
||||
# Kafi (≈ Dorian) — Sa Re komal-Ga Ma Pa Dha komal-Ni
|
||||
"kafi": {"intervals": (4, 2, 3, 4, 4, 1, 4)},
|
||||
# Asavari (≈ Aeolian) — Sa Re komal-Ga Ma Pa komal-Dha komal-Ni
|
||||
"asavari": {"intervals": (4, 2, 3, 4, 2, 3, 4)},
|
||||
# Bhairavi (≈ Phrygian) — Sa komal-Re komal-Ga Ma Pa komal-Dha komal-Ni
|
||||
"bhairavi": {"intervals": (2, 4, 3, 4, 2, 3, 4)},
|
||||
# Bhairav — Sa komal-Re Ga Ma Pa komal-Dha Ni (unique to Indian music)
|
||||
"bhairav": {"intervals": (2, 5, 2, 4, 2, 5, 2)},
|
||||
# Kalyan (≈ Lydian) — Sa Re Ga tivra-Ma Pa Dha Ni
|
||||
"kalyan": {"intervals": (4, 3, 4, 2, 4, 3, 2)},
|
||||
# Marwa — Sa komal-Re Ga tivra-Ma Pa Dha Ni (unique)
|
||||
"marwa": {"intervals": (2, 5, 4, 2, 4, 3, 2)},
|
||||
# Poorvi — Sa komal-Re Ga tivra-Ma Pa komal-Dha Ni (unique)
|
||||
"poorvi": {"intervals": (2, 5, 4, 2, 2, 5, 2)},
|
||||
# Todi — Sa komal-Re komal-Ga tivra-Ma Pa komal-Dha Ni (unique)
|
||||
"todi": {"intervals": (2, 4, 5, 2, 2, 5, 2)},
|
||||
},
|
||||
],
|
||||
"pentatonic": [
|
||||
5,
|
||||
{
|
||||
# Bhupali (≈ major pentatonic) — Sa Re Ga Pa Dha
|
||||
"bhupali": {"intervals": (4, 3, 6, 4, 5)},
|
||||
# Malkauns — Sa komal-Ga Ma komal-Dha komal-Ni
|
||||
"malkauns": {"intervals": (6, 3, 4, 5, 4)},
|
||||
# Durga — Sa Re Ma Pa Dha
|
||||
"durga": {"intervals": (4, 5, 4, 4, 5)},
|
||||
# Bhairavi pentatonic — Sa komal-Re Ma Pa komal-Ni
|
||||
"bhairavi pentatonic": {"intervals": (2, 7, 4, 2, 7)},
|
||||
},
|
||||
],
|
||||
}
|
||||
|
||||
# ── 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
|
||||
("Sib",), # 2 — Bb
|
||||
("Si↓",), # 3 — B quarter-flat
|
||||
("Si",), # 4 — B
|
||||
("Do",), # 5 — C
|
||||
("Do↑",), # 6 — C quarter-sharp
|
||||
("Reb",), # 7 — Db
|
||||
("Re↓",), # 8 — D quarter-flat
|
||||
("Re",), # 9 — D
|
||||
("Re↑",), # 10 — D quarter-sharp
|
||||
("Mib",), # 11 — Eb
|
||||
("Mi↓",), # 12 — E quarter-flat
|
||||
("Mi",), # 13 — E
|
||||
("Fa",), # 14 — F
|
||||
("Fa↑",), # 15 — F quarter-sharp
|
||||
("Fa#",), # 16 — F#
|
||||
("Sol↓",), # 17 — G quarter-flat
|
||||
("Sol",), # 18 — G
|
||||
("Sol↑",), # 19 — G quarter-sharp
|
||||
("Lab",), # 20 — Ab
|
||||
("La↓",), # 21 — A quarter-flat
|
||||
("La½b",), # 22 — between Ab and A (rarely used)
|
||||
("La♮",), # 23 — enharmonic A (rarely used)
|
||||
]
|
||||
|
||||
DEGREES_ARABIC_24 = [
|
||||
("tonic", ()),
|
||||
("second", ()),
|
||||
("third", ()),
|
||||
("fourth", ()),
|
||||
("fifth", ()),
|
||||
("sixth", ()),
|
||||
("seventh", ()),
|
||||
("octave", ()),
|
||||
]
|
||||
|
||||
# 24-TET maqam scales with true quarter-tone intervals.
|
||||
# Each step = 1 quarter-tone (50 cents). A 12-TET semitone = 2 steps.
|
||||
ARABIC_24_SCALES = {
|
||||
"chromatic": (24, {}),
|
||||
"maqam": [
|
||||
7,
|
||||
{
|
||||
# Rast — the foundational maqam. E and B are quarter-flat.
|
||||
# Do Re Mi↓ Fa Sol La Si↓ Do
|
||||
"rast": {"intervals": (4, 3, 3, 4, 4, 3, 3)},
|
||||
# Bayati — starts on D with quarter-flat 2nd.
|
||||
# Re Mi↓ Fa Sol La Sib Do Re
|
||||
"bayati": {"intervals": (3, 3, 4, 4, 2, 4, 4)},
|
||||
# Saba — similar to Bayati with flattened 4th
|
||||
"saba": {"intervals": (3, 3, 2, 6, 2, 4, 4)},
|
||||
# Sikah — starts on E quarter-flat
|
||||
"sikah": {"intervals": (3, 4, 3, 4, 3, 4, 3)},
|
||||
# Hijaz — augmented 2nd (6 quarter-tones) between 2nd and 3rd
|
||||
"hijaz": {"intervals": (2, 6, 2, 4, 2, 4, 4)},
|
||||
# Nahawand (≈ harmonic minor)
|
||||
"nahawand": {"intervals": (4, 2, 4, 4, 2, 6, 2)},
|
||||
# Ajam (≈ major)
|
||||
"ajam": {"intervals": (4, 4, 2, 4, 4, 4, 2)},
|
||||
# Kurd (≈ Phrygian)
|
||||
"kurd": {"intervals": (2, 4, 4, 4, 2, 4, 4)},
|
||||
# Nikriz — augmented 2nd between 3rd and 4th
|
||||
"nikriz": {"intervals": (4, 2, 6, 2, 4, 2, 4)},
|
||||
# Jiharkah — like Rast but with natural B
|
||||
"jiharkah": {"intervals": (4, 4, 2, 4, 4, 3, 3)},
|
||||
},
|
||||
],
|
||||
}
|
||||
|
||||
# ── 5-TET Gamelan Slendro ────────────────────────────────────────────────────
|
||||
# Slendro is a 5-tone equal temperament — each step is 240 cents.
|
||||
# The actual tuning varies between gamelans (each set is unique), but
|
||||
# 5-TET is the theoretical ideal that all slendro tunings approximate.
|
||||
# Ordered from nem (≈A) to loosely match Western indexing.
|
||||
TONES_SLENDRO = [
|
||||
("nem",), # 0 — 6 (≈A)
|
||||
("ji",), # 1 — 1 (≈C)
|
||||
("ro",), # 2 — 2 (≈D)
|
||||
("lu",), # 3 — 3 (≈F)
|
||||
("mo",), # 4 — 5 (≈G)
|
||||
]
|
||||
|
||||
DEGREES_SLENDRO = [
|
||||
("nem", ()), ("ji", ()), ("ro", ()), ("lu", ()), ("mo", ()),
|
||||
]
|
||||
|
||||
SLENDRO_SCALES = {
|
||||
"chromatic": (5, {}),
|
||||
"pentatonic": [5, {
|
||||
# The full slendro IS the pentatonic — all 5 tones
|
||||
"slendro": {"intervals": (1, 1, 1, 1, 1)},
|
||||
}],
|
||||
}
|
||||
|
||||
# ── 9-TET Gamelan Pelog ─────────────────────────────────────────────────────
|
||||
# Pelog uses 7 tones from a roughly 9-step division of the octave.
|
||||
# 9-TET (133 cents/step) approximates the unequal pelog intervals.
|
||||
# The 3 pathet (modes) select 5 tones from the 7.
|
||||
TONES_PELOG = [
|
||||
("nem",), # 0 — 6
|
||||
("pi",), # 1 — 7
|
||||
("ji",), # 2 — 1
|
||||
("ro",), # 3 — 2
|
||||
("lu",), # 4 — 3
|
||||
("pat",), # 5 — 4
|
||||
("barang",), # 6 — complementary
|
||||
("mo",), # 7 — 5
|
||||
("nem+",), # 8 — auxiliary
|
||||
]
|
||||
|
||||
DEGREES_PELOG = [
|
||||
("nem", ()), ("pi", ()), ("ji", ()), ("ro", ()),
|
||||
("lu", ()), ("pat", ()), ("barang", ()), ("mo", ()), ("nem+", ()),
|
||||
]
|
||||
|
||||
PELOG_SCALES = {
|
||||
"chromatic": (9, {}),
|
||||
"heptatonic": [7, {
|
||||
# Full pelog — 7 tones from 9 steps
|
||||
"pelog": {"intervals": (1, 2, 1, 1, 2, 1, 1)},
|
||||
}],
|
||||
"pentatonic": [5, {
|
||||
# Pathet nem — the most common mode
|
||||
"pelog nem": {"intervals": (1, 2, 2, 2, 2)},
|
||||
# Pathet lima
|
||||
"pelog lima": {"intervals": (1, 2, 2, 1, 3)},
|
||||
# Pathet barang
|
||||
"pelog barang": {"intervals": (2, 1, 2, 2, 2)},
|
||||
}],
|
||||
}
|
||||
|
||||
# ── 7-TET Thai classical ────────────────────────────────────────────────────
|
||||
# Thai classical music divides the octave into 7 exactly equal steps
|
||||
# (~171 cents each). This is unique — no Western equivalent exists.
|
||||
# The 7 tones are numbered 1-7 in Thai theory.
|
||||
TONES_THAI = [
|
||||
("do",), # 0 — 1st degree
|
||||
("re",), # 1 — 2nd
|
||||
("mi",), # 2 — 3rd
|
||||
("fa",), # 3 — 4th
|
||||
("sol",), # 4 — 5th
|
||||
("la",), # 5 — 6th
|
||||
("si",), # 6 — 7th
|
||||
]
|
||||
|
||||
DEGREES_THAI = [
|
||||
("thang 1", ()), ("thang 2", ()), ("thang 3", ()),
|
||||
("thang 4", ()), ("thang 5", ()), ("thang 6", ()), ("thang 7", ()),
|
||||
]
|
||||
|
||||
THAI_SCALES = {
|
||||
"chromatic": (7, {}),
|
||||
"pentatonic": [5, {
|
||||
# The standard Thai pentatonic — 5 of 7 equal steps
|
||||
"thai pentatonic": {"intervals": (1, 1, 2, 1, 2)},
|
||||
# Alternate selection
|
||||
"thai pentatonic 2": {"intervals": (2, 1, 1, 2, 1)},
|
||||
}],
|
||||
"heptatonic": [7, {
|
||||
# The full 7-TET scale
|
||||
"thai": {"intervals": (1, 1, 1, 1, 1, 1, 1)},
|
||||
}],
|
||||
}
|
||||
|
||||
# ── 53-TET Turkish makam (Arel-Ezgi-Uzdilek) ───────────────────────────────
|
||||
# The gold standard for Turkish music theory. 53-TET has nearly perfect
|
||||
# fifths (31 steps = 701.89 cents vs 701.96 just) and excellent thirds.
|
||||
# A comma (1 step) = 22.6 cents. The basic intervals:
|
||||
# Bakiye (B) = 4 commas ≈ 90 cents (like a limma)
|
||||
# Küçük mücenneb (S) = 5 commas ≈ 113 cents
|
||||
# Büyük mücenneb (K) = 8 commas ≈ 181 cents
|
||||
# Tanini (T) = 9 commas ≈ 204 cents (like a whole tone)
|
||||
TONES_TURKISH = [
|
||||
("La",), # 0 — A (Dügah reference)
|
||||
("La+1",), # 1
|
||||
("La+2",), # 2
|
||||
("La+3",), # 3
|
||||
("Sib",), # 4 — Bb (4 commas from A)
|
||||
("Sib+1",), # 5
|
||||
("Sib+2",), # 6
|
||||
("Sib+3",), # 7
|
||||
("Sib+4",), # 8
|
||||
("Si",), # 9 — B
|
||||
("Si+1",), # 10
|
||||
("Si+2",), # 11
|
||||
("Si+3",), # 12
|
||||
("Do",), # 13 — C (Rast)
|
||||
("Do+1",), # 14
|
||||
("Do+2",), # 15
|
||||
("Do+3",), # 16
|
||||
("Do+4",), # 17
|
||||
("Reb",), # 18 — Db
|
||||
("Reb+1",), # 19
|
||||
("Reb+2",), # 20
|
||||
("Reb+3",), # 21
|
||||
("Re",), # 22 — D (Dügah)
|
||||
("Re+1",), # 23
|
||||
("Re+2",), # 24
|
||||
("Re+3",), # 25
|
||||
("Re+4",), # 26
|
||||
("Mib",), # 27 — Eb
|
||||
("Mib+1",), # 28
|
||||
("Mib+2",), # 29
|
||||
("Mib+3",), # 30
|
||||
("Mi",), # 31 — E (Segah)
|
||||
("Mi+1",), # 32
|
||||
("Mi+2",), # 33
|
||||
("Mi+3",), # 34
|
||||
("Mi+4",), # 35
|
||||
("Fa",), # 36 — F
|
||||
("Fa+1",), # 37
|
||||
("Fa+2",), # 38
|
||||
("Fa+3",), # 39
|
||||
("Fa#",), # 40 — F#
|
||||
("Fa#+1",), # 41
|
||||
("Fa#+2",), # 42
|
||||
("Fa#+3",), # 43
|
||||
("Sol",), # 44 — G (Neva)
|
||||
("Sol+1",), # 45
|
||||
("Sol+2",), # 46
|
||||
("Sol+3",), # 47
|
||||
("Lab",), # 48 — Ab
|
||||
("Lab+1",), # 49
|
||||
("Lab+2",), # 50
|
||||
("Lab+3",), # 51
|
||||
("Lab+4",), # 52
|
||||
]
|
||||
|
||||
DEGREES_TURKISH = [(f"perde {i+1}", ()) for i in range(53)]
|
||||
|
||||
# Turkish makam scales in 53-TET commas.
|
||||
# T=9 commas (whole tone), S=5 (small), K=8 (large), B=4 (limma)
|
||||
TURKISH_SCALES = {
|
||||
"chromatic": (53, {}),
|
||||
"makam": [
|
||||
7,
|
||||
{
|
||||
# Rast — the foundational makam. Uses segah (≈ neutral 3rd)
|
||||
# T + T + S + T + T + T + S = 9+9+5+9+9+9+4 = 53...
|
||||
# Actually: 9+8+5+9+9+8+5 = 53
|
||||
"rast": {"intervals": (9, 8, 5, 9, 9, 8, 5)},
|
||||
# Nihavend (≈ harmonic minor)
|
||||
"nihavend": {"intervals": (9, 4, 9, 9, 4, 13, 5)},
|
||||
# Hicaz — the augmented 2nd makam
|
||||
"hicaz": {"intervals": (5, 12, 5, 9, 4, 9, 9)},
|
||||
# Ussak — one of the most common makams
|
||||
"ussak": {"intervals": (8, 5, 9, 9, 8, 5, 9)},
|
||||
# Huseyni
|
||||
"huseyni": {"intervals": (8, 5, 9, 9, 5, 8, 9)},
|
||||
# Kurdi (≈ Phrygian)
|
||||
"kurdi": {"intervals": (4, 9, 9, 9, 4, 9, 9)},
|
||||
# Segah — starts on the neutral 3rd
|
||||
"segah": {"intervals": (5, 9, 9, 8, 5, 9, 8)},
|
||||
# Saba — descending differs from ascending
|
||||
"saba": {"intervals": (8, 5, 4, 14, 4, 9, 9)},
|
||||
# Hüzzam
|
||||
"huzzam": {"intervals": (5, 9, 8, 5, 9, 8, 9)},
|
||||
},
|
||||
],
|
||||
}
|
||||
|
||||
# ── 72-TET Carnatic (South Indian) ───────────────────────────────────────────
|
||||
# The 72 melakarta system classifies all possible 7-note scales with
|
||||
# fixed Sa and Pa. 72-TET (16.67 cents/step) captures the srutis used
|
||||
# in Carnatic music with high precision. Each 12-TET semitone = 6 steps.
|
||||
#
|
||||
# Tone names: 12 swaras × 6 microtonal variants each.
|
||||
# Main swaras at positions: Sa=0, Ri1=6, Ri2=12, Ga1=12, Ga2=18,
|
||||
# Ma1=30, Ma2=36, Pa=42, Da1=48, Da2=54, Ni1=60, Ni2=66
|
||||
TONES_CARNATIC = []
|
||||
_SWARA_NAMES = [
|
||||
"Sa", "atikomal Ri", "komal Ri", "shuddha Ri",
|
||||
"Ri", "tivra Ri", "komal Ga", "atikomal Ga",
|
||||
"Ga", "shuddha Ga", "tivra Ga", "antara Ga",
|
||||
"komal Ma", "shuddha Ma", "Ma", "tivra shuddha Ma",
|
||||
"ekashruti Ma", "chatushruti Ma", "tivra Ma", "atitivra Ma",
|
||||
"prati Ma", "tivratara Ma", "atikomal Pa-", "komal Pa-",
|
||||
"shuddha Pa-", "Pa-", "Pa-+1", "Pa-+2",
|
||||
"Pa-+3", "Pa-+4", "Pa", "Pa+1",
|
||||
"Pa+2", "Pa+3", "Pa+4", "Pa+5",
|
||||
"komal Da", "atikomal Da", "Da-", "shuddha Da-",
|
||||
"Da", "shuddha Da", "tivra Da", "atitivra Da",
|
||||
"komal Ni", "atikomal Ni", "Ni-", "shuddha Ni-",
|
||||
"Ni", "shuddha Ni", "tivra Ni", "chatushruti Ni",
|
||||
"kakali Ni", "atikakali Ni",
|
||||
]
|
||||
# Generate 72 tone names: use standard names for the 12 main positions,
|
||||
# numbered variants for the intermediates
|
||||
for i in range(72):
|
||||
main_pos = i // 6 # which semitone group (0-11)
|
||||
micro = i % 6 # microtonal position within group
|
||||
_base_names = ["Sa", "komal Ri", "Ri", "komal Ga", "Ga", "Ma",
|
||||
"tivra Ma", "Pa", "komal Da", "Da", "komal Ni", "Ni"]
|
||||
if micro == 0:
|
||||
TONES_CARNATIC.append((_base_names[main_pos],))
|
||||
else:
|
||||
TONES_CARNATIC.append((f"{_base_names[main_pos]}+{micro}",))
|
||||
|
||||
DEGREES_CARNATIC = [(f"swara {i+1}", ()) for i in range(72)]
|
||||
|
||||
# A selection of important melakartas in 72-TET intervals.
|
||||
# Each step = 1/72 of an octave ≈ 16.67 cents.
|
||||
CARNATIC_SCALES = {
|
||||
"chromatic": (72, {}),
|
||||
"melakarta": [
|
||||
7,
|
||||
{
|
||||
# Kanakangi (melakarta 1) — Sa Ri1 Ga1 Ma1 Pa Da1 Ni1
|
||||
"kanakangi": {"intervals": (6, 6, 18, 12, 6, 6, 18)},
|
||||
# Shankarabharanam (melakarta 29) — Sa Ri2 Ga3 Ma1 Pa Da2 Ni3
|
||||
# The Carnatic equivalent of the major scale
|
||||
"shankarabharanam": {"intervals": (12, 12, 6, 12, 12, 12, 6)},
|
||||
# Kalyani (melakarta 65) — Sa Ri2 Ga3 Ma2 Pa Da2 Ni3
|
||||
# Carnatic Lydian equivalent
|
||||
"kalyani": {"intervals": (12, 12, 12, 6, 12, 12, 6)},
|
||||
# Kharaharapriya (melakarta 22) — Sa Ri2 Ga2 Ma1 Pa Da2 Ni2
|
||||
# Carnatic Dorian equivalent
|
||||
"kharaharapriya": {"intervals": (12, 6, 12, 12, 12, 6, 12)},
|
||||
# Hanumathodi (melakarta 8) — Sa Ri1 Ga2 Ma1 Pa Da1 Ni2
|
||||
# Carnatic Phrygian equivalent
|
||||
"hanumathodi": {"intervals": (6, 12, 12, 12, 6, 12, 12)},
|
||||
# Natabhairavi (melakarta 20) — Sa Ri2 Ga2 Ma1 Pa Da1 Ni2
|
||||
# Natural minor equivalent
|
||||
"natabhairavi": {"intervals": (12, 6, 12, 12, 6, 12, 12)},
|
||||
# Mayamalavagowla (melakarta 15) — Sa Ri1 Ga3 Ma1 Pa Da1 Ni3
|
||||
# The "lesson scale" — first raga taught to students
|
||||
"mayamalavagowla": {"intervals": (6, 18, 6, 12, 6, 18, 6)},
|
||||
# Simhendramadhyamam (melakarta 57) — Sa Ri2 Ga3 Ma2 Pa Da1 Ni3
|
||||
"simhendramadhyamam": {"intervals": (12, 12, 12, 6, 6, 18, 6)},
|
||||
# Charukesi (melakarta 26) — Sa Ri2 Ga3 Ma1 Pa Da1 Ni2
|
||||
"charukesi": {"intervals": (12, 12, 6, 12, 6, 12, 12)},
|
||||
# Harikambhoji (melakarta 28) — Sa Ri2 Ga3 Ma1 Pa Da2 Ni2
|
||||
# Mixolydian equivalent
|
||||
"harikambhoji": {"intervals": (12, 12, 6, 12, 12, 6, 12)},
|
||||
},
|
||||
],
|
||||
}
|
||||
|
||||
# Arabic maqam scales (12-TET approximations).
|
||||
# True maqam uses quarter-tones; these are the closest 12-tone equivalents.
|
||||
ARABIC_SCALES = {
|
||||
12: {
|
||||
"chromatic": (12, {}),
|
||||
"maqam": [
|
||||
7,
|
||||
{
|
||||
# Ajam = Western major
|
||||
"ajam": {"intervals": (2, 2, 1, 2, 2, 2, 1)},
|
||||
# Nahawand = Western harmonic minor
|
||||
"nahawand": {"intervals": (2, 1, 2, 2, 1, 3, 1)},
|
||||
# Kurd = Western Phrygian
|
||||
"kurd": {"intervals": (1, 2, 2, 2, 1, 2, 2)},
|
||||
# Hijaz — augmented 2nd between 2nd and 3rd degrees
|
||||
"hijaz": {"intervals": (1, 3, 1, 2, 1, 2, 2)},
|
||||
# Nikriz — augmented 2nd between 3rd and 4th
|
||||
"nikriz": {"intervals": (2, 1, 3, 1, 2, 1, 2)},
|
||||
# Bayati (12-TET approx) — true bayati has quarter-flat 2nd
|
||||
"bayati": {"intervals": (1, 2, 2, 2, 1, 2, 2)},
|
||||
# Rast (12-TET approx) — true rast has quarter-flat 3rd and 7th
|
||||
"rast": {"intervals": (2, 1, 2, 2, 2, 1, 2)},
|
||||
# Saba (12-TET approx) — true saba has quarter-flat 2nd
|
||||
"saba": {"intervals": (1, 2, 1, 3, 1, 2, 2)},
|
||||
# Sikah (12-TET approx) — true sikah starts on quarter-flat
|
||||
"sikah": {"intervals": (1, 2, 2, 2, 1, 2, 2)},
|
||||
# Jiharkah
|
||||
"jiharkah": {"intervals": (2, 2, 1, 2, 2, 1, 2)},
|
||||
},
|
||||
],
|
||||
}
|
||||
}
|
||||
|
||||
# Japanese pentatonic scales.
|
||||
JAPANESE_SCALES = {
|
||||
12: {
|
||||
"chromatic": (12, {}),
|
||||
"pentatonic": [
|
||||
5,
|
||||
{
|
||||
# Hirajoshi — the most well-known Japanese scale
|
||||
# C D Eb G Ab
|
||||
"hirajoshi": {"intervals": (2, 1, 4, 1, 4)},
|
||||
# In (Miyako-bushi) — used in koto music
|
||||
# C Db F G Ab
|
||||
"in": {"intervals": (1, 4, 2, 1, 4)},
|
||||
# Yo — folk music scale
|
||||
# C D F G Bb
|
||||
"yo": {"intervals": (2, 3, 2, 3, 2)},
|
||||
# Iwato — dark, dissonant pentatonic
|
||||
# C Db F Gb Bb
|
||||
"iwato": {"intervals": (1, 4, 1, 4, 2)},
|
||||
# Kumoi — similar to minor pentatonic
|
||||
# C D Eb G A
|
||||
"kumoi": {"intervals": (2, 1, 4, 2, 3)},
|
||||
# Insen — modern Japanese scale
|
||||
# C Db F G Bb
|
||||
"insen": {"intervals": (1, 4, 2, 3, 2)},
|
||||
},
|
||||
],
|
||||
"heptatonic": [
|
||||
7,
|
||||
{
|
||||
# Ritsu — gagaku court music scale
|
||||
# C D Eb F G A Bb (= Dorian)
|
||||
"ritsu": {"intervals": (2, 1, 2, 2, 2, 1, 2)},
|
||||
# Ryo — gagaku court music scale
|
||||
# C D E F# G A B (= Lydian)
|
||||
"ryo": {"intervals": (2, 2, 2, 1, 2, 2, 1)},
|
||||
},
|
||||
],
|
||||
}
|
||||
}
|
||||
|
||||
# Blues and pentatonic scales — foundational to American music.
|
||||
BLUES_SCALES = {
|
||||
12: {
|
||||
"chromatic": (12, {}),
|
||||
"pentatonic": [
|
||||
5,
|
||||
{
|
||||
# Major pentatonic — C D E G A
|
||||
"major pentatonic": {"intervals": (2, 2, 3, 2, 3)},
|
||||
# Minor pentatonic — C Eb F G Bb
|
||||
"minor pentatonic": {"intervals": (3, 2, 2, 3, 2)},
|
||||
},
|
||||
],
|
||||
"hexatonic": [
|
||||
6,
|
||||
{
|
||||
# Blues scale — C Eb F F# G Bb
|
||||
"blues": {"intervals": (3, 2, 1, 1, 3, 2)},
|
||||
# Major blues — C D D# E G A
|
||||
"major blues": {"intervals": (2, 1, 1, 3, 2, 3)},
|
||||
},
|
||||
],
|
||||
"heptatonic": [
|
||||
7,
|
||||
{
|
||||
# Mixolydian (dominant blues sound) — C D E F G A Bb
|
||||
"dominant": {"intervals": (2, 2, 1, 2, 2, 1, 2)},
|
||||
# Dorian (minor blues/jazz) — C D Eb F G A Bb
|
||||
"minor": {"intervals": (2, 1, 2, 2, 2, 1, 2)},
|
||||
},
|
||||
],
|
||||
}
|
||||
}
|
||||
|
||||
# Javanese gamelan scales — 12-TET approximations.
|
||||
# True gamelan tuning varies between ensembles and does not conform
|
||||
# to equal temperament. These approximations capture the melodic
|
||||
# character of the scales.
|
||||
GAMELAN_SCALES = {
|
||||
12: {
|
||||
"chromatic": (12, {}),
|
||||
"pentatonic": [
|
||||
5,
|
||||
{
|
||||
# Slendro — roughly equal 5-tone division of the octave
|
||||
# Approximated as: C D F G Bb
|
||||
"slendro": {"intervals": (2, 3, 2, 3, 2)},
|
||||
# Pelog pathet nem — C Db E F G (approx)
|
||||
"pelog nem": {"intervals": (1, 3, 1, 2, 5)},
|
||||
# Pelog pathet barang — C Db E F# B (approx)
|
||||
"pelog barang": {"intervals": (1, 3, 3, 4, 1)},
|
||||
# Pelog pathet lima — C Db E F Ab (approx)
|
||||
"pelog lima": {"intervals": (1, 3, 1, 3, 4)},
|
||||
},
|
||||
],
|
||||
"heptatonic": [
|
||||
7,
|
||||
{
|
||||
# Full pelog — all 7 tones: C Db E F G Ab B (approx)
|
||||
"pelog": {"intervals": (1, 3, 1, 2, 1, 3, 1)},
|
||||
},
|
||||
],
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+389
-36
@@ -1,4 +1,6 @@
|
||||
import functools
|
||||
import itertools
|
||||
from typing import Optional
|
||||
|
||||
from .systems import SYSTEMS
|
||||
from .tones import Tone
|
||||
@@ -6,6 +8,166 @@ from .tones import Tone
|
||||
QUALITIES = ("", "maj", "m", "5", "7", "9", "dim", "m6", "m7", "m9", "maj7", "maj9")
|
||||
MAX_FRET = 7
|
||||
|
||||
# Standard guitar tuning (high to low): E4 B3 G3 D3 A2 E2
|
||||
STANDARD_GUITAR_TUNING = ("E4", "B3", "G3", "D3", "A2", "E2")
|
||||
|
||||
# Curated override fingerings for common guitar chords in standard tuning.
|
||||
# Key: chord name, Value: tuple of fret positions (-1 = muted string).
|
||||
# Order is high-to-low (matching Fretboard.guitar() string order).
|
||||
GUITAR_OVERRIDES = {
|
||||
"C": (0, 1, 0, 2, 3, -1),
|
||||
"D": (2, 3, 2, 0, -1, -1),
|
||||
"Dm": (1, 3, 2, 0, -1, -1),
|
||||
"D7": (2, 1, 2, 0, -1, -1),
|
||||
"E": (0, 0, 1, 2, 2, 0),
|
||||
"Em": (0, 0, 0, 2, 2, 0),
|
||||
"F": (1, 1, 2, 3, 3, 1),
|
||||
"G": (3, 0, 0, 0, 2, 3),
|
||||
"G7": (1, 0, 0, 0, 2, 3),
|
||||
"A": (0, 2, 2, 2, 0, -1),
|
||||
"Am": (0, 1, 2, 2, 0, -1),
|
||||
"Am7": (0, 1, 0, 2, 0, -1),
|
||||
"B": (2, 4, 4, 4, 2, -1),
|
||||
"Bm": (2, 3, 4, 4, 2, -1),
|
||||
"B7": (2, 0, 2, 1, 2, -1),
|
||||
}
|
||||
|
||||
# Memoization cache for fingering lookups.
|
||||
# Key: (chord_name, fretboard_tuning_tuple)
|
||||
# Value: Fingering object (single) or tuple of Fingerings (multiple)
|
||||
# Bounded to _CACHE_MAX_SIZE entries; cleared entirely when full.
|
||||
_CACHE_MAX_SIZE = 1024
|
||||
_fingering_cache: dict[tuple, "Fingering"] = {}
|
||||
_fingering_multi_cache: dict[tuple, tuple] = {}
|
||||
_possible_cache: dict[tuple, tuple] = {}
|
||||
|
||||
|
||||
class Fingering:
|
||||
"""A chord fingering labeled with string names.
|
||||
|
||||
Provides both index and named access to fret positions, making it
|
||||
clear which string each position corresponds to.
|
||||
|
||||
Example::
|
||||
|
||||
>>> f = Fingering(positions=(0, 3, 2, 0, 1, 0),
|
||||
... string_names=('E', 'A', 'D', 'G', 'B', 'e'))
|
||||
>>> f
|
||||
Fingering(E=0, A=3, D=2, G=0, B=1, e=0)
|
||||
>>> f['A']
|
||||
3
|
||||
>>> f[1]
|
||||
3
|
||||
"""
|
||||
|
||||
def __init__(self, positions: tuple, string_names: tuple[str, ...], *, fretboard=None) -> None:
|
||||
self.positions = tuple(positions)
|
||||
self._fretboard = fretboard
|
||||
# Disambiguate duplicate names: for standard guitar tuning
|
||||
# (high-to-low), the first occurrence of a duplicate becomes
|
||||
# lowercase (e.g. high E → 'e') while the last keeps uppercase.
|
||||
from collections import Counter
|
||||
name_counts = Counter(string_names)
|
||||
seen: dict[str, int] = {}
|
||||
unique_names: list[str] = []
|
||||
for name in string_names:
|
||||
seen[name] = seen.get(name, 0) + 1
|
||||
if name_counts[name] > 1 and seen[name] < name_counts[name]:
|
||||
unique_names.append(name.lower())
|
||||
else:
|
||||
unique_names.append(name)
|
||||
|
||||
self.string_names = tuple(unique_names)
|
||||
self._map = dict(zip(self.string_names, self.positions))
|
||||
|
||||
def __repr__(self) -> str:
|
||||
pairs = ", ".join(
|
||||
f"{name}={'x' if pos is None else pos}"
|
||||
for name, pos in zip(self.string_names, self.positions)
|
||||
)
|
||||
return f"Fingering({pairs})"
|
||||
|
||||
def __getitem__(self, key):
|
||||
if isinstance(key, int):
|
||||
return self.positions[key]
|
||||
return self._map[key]
|
||||
|
||||
def __iter__(self):
|
||||
return iter(self.positions)
|
||||
|
||||
def __len__(self):
|
||||
return len(self.positions)
|
||||
|
||||
def __eq__(self, other):
|
||||
if isinstance(other, Fingering):
|
||||
return self.positions == other.positions and self.string_names == other.string_names
|
||||
if isinstance(other, tuple):
|
||||
return self.positions == other
|
||||
return NotImplemented
|
||||
|
||||
@property
|
||||
def tones(self):
|
||||
"""Return the sounding tones for this fingering.
|
||||
|
||||
Requires that the Fingering was created with a fretboard reference.
|
||||
Muted strings (``None``) are excluded.
|
||||
"""
|
||||
if self._fretboard is None:
|
||||
raise ValueError("Cannot resolve tones without a fretboard reference.")
|
||||
tones = []
|
||||
for pos, tone in zip(self.positions, self._fretboard.tones):
|
||||
if pos is not None:
|
||||
tones.append(tone.add(pos))
|
||||
return tones
|
||||
|
||||
def to_chord(self, fretboard=None) -> "Chord":
|
||||
"""Apply this fingering to a fretboard, returning a Chord.
|
||||
|
||||
Strings with ``None`` positions (muted) are excluded.
|
||||
If no fretboard is given, uses the one stored at creation time.
|
||||
"""
|
||||
from .chords import Chord
|
||||
|
||||
fb = fretboard or self._fretboard
|
||||
if fb is None:
|
||||
raise ValueError("No fretboard provided.")
|
||||
tones = []
|
||||
for pos, tone in zip(self.positions, fb.tones):
|
||||
if pos is not None:
|
||||
tones.append(tone.add(pos))
|
||||
return Chord(tones=tones)
|
||||
|
||||
def identify(self) -> Optional[str]:
|
||||
"""Identify the chord name from this fingering."""
|
||||
return self.to_chord().identify()
|
||||
|
||||
def tab(self) -> str:
|
||||
"""Render this fingering as ASCII guitar tablature.
|
||||
|
||||
Requires that the Fingering was created with a fretboard reference.
|
||||
|
||||
Example::
|
||||
|
||||
>>> fb = Fretboard.guitar()
|
||||
>>> print(fb.chord("C").tab())
|
||||
C
|
||||
e|--0--
|
||||
B|--1--
|
||||
G|--0--
|
||||
D|--2--
|
||||
A|--3--
|
||||
E|--0--
|
||||
"""
|
||||
if self._fretboard is None:
|
||||
raise ValueError("Cannot render tab without a fretboard reference.")
|
||||
name = self.identify() or "?"
|
||||
lines = [name]
|
||||
max_name = max(len(n) for n in self.string_names)
|
||||
for sname, fret in zip(self.string_names, self.positions):
|
||||
fret_str = "x" if fret is None else str(fret)
|
||||
lines.append(f"{sname:>{max_name}}|--{fret_str}--")
|
||||
return "\n".join(lines)
|
||||
|
||||
CHARTS = {}
|
||||
CHARTS["western"] = []
|
||||
|
||||
@@ -31,65 +193,108 @@ class NamedChord:
|
||||
def __repr__(self):
|
||||
return f"<NamedChord name={self.name!r}>"
|
||||
|
||||
@property
|
||||
def _prefer_flats(self):
|
||||
"""Determine whether this chord's tones should use flat spellings.
|
||||
|
||||
Uses the circle-of-fifths convention:
|
||||
- Flat-root notes (Bb, Eb, Ab, Db, Gb) always prefer flats.
|
||||
- Major-type qualities prefer flats for roots: F, Bb, Eb, Ab, Db, Gb.
|
||||
- Minor-type qualities prefer flats for roots: D, G, C, F, Bb, Eb, Ab.
|
||||
"""
|
||||
# Root is itself a flat note — always prefer flats
|
||||
if "b" in self.tone_name and self.tone_name != "B":
|
||||
return True
|
||||
|
||||
_FLAT_MAJOR_ROOTS = {"F", "Bb", "Eb", "Ab", "Db", "Gb"}
|
||||
_FLAT_MINOR_ROOTS = {"D", "G", "C", "F", "Bb", "Eb", "Ab"}
|
||||
# Dominant 7th/9th chords contain a minor 7th (b7), so they
|
||||
# follow the same flat-preference roots as minor chords.
|
||||
_FLAT_DOMINANT_ROOTS = {"C", "F", "G", "Bb", "Eb", "Ab", "Db", "Gb"}
|
||||
|
||||
minor_qualities = {"m", "m6", "m7", "m9", "dim"}
|
||||
dominant_qualities = {"7", "9"}
|
||||
major_qualities = {"", "maj", "5", "maj7", "maj9"}
|
||||
|
||||
if self.quality in minor_qualities and self.tone_name in _FLAT_MINOR_ROOTS:
|
||||
return True
|
||||
if self.quality in dominant_qualities and self.tone_name in _FLAT_DOMINANT_ROOTS:
|
||||
return True
|
||||
if self.quality in major_qualities and self.tone_name in _FLAT_MAJOR_ROOTS:
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
@property
|
||||
def acceptable_tones(self):
|
||||
acceptable = [self.tone]
|
||||
flats = self._prefer_flats
|
||||
|
||||
if self.quality == "maj":
|
||||
# Major triad: root, major 3rd, perfect 5th
|
||||
acceptable += [self.tone.add(4), self.tone.add(7)]
|
||||
acceptable += [self.tone.add(4, prefer_flats=flats), self.tone.add(7, prefer_flats=flats)]
|
||||
|
||||
elif self.quality == "m":
|
||||
# Minor triad: root, minor 3rd, perfect 5th
|
||||
acceptable += [self.tone.add(3), self.tone.add(7)]
|
||||
acceptable += [self.tone.add(3, prefer_flats=flats), self.tone.add(7, prefer_flats=flats)]
|
||||
|
||||
elif self.quality == "5":
|
||||
# Power chord: root, perfect 5th
|
||||
acceptable += [self.tone.add(7)]
|
||||
acceptable += [self.tone.add(7, prefer_flats=flats)]
|
||||
|
||||
elif self.quality == "7":
|
||||
# Dominant 7th: root, major 3rd, perfect 5th, minor 7th
|
||||
acceptable += [self.tone.add(4), self.tone.add(7), self.tone.add(10)]
|
||||
acceptable += [self.tone.add(4, prefer_flats=flats), self.tone.add(7, prefer_flats=flats), self.tone.add(10, prefer_flats=flats)]
|
||||
|
||||
elif self.quality == "9":
|
||||
# Dominant 9th: root, major 3rd, perfect 5th, minor 7th, major 9th
|
||||
acceptable += [self.tone.add(4), self.tone.add(7), self.tone.add(10), self.tone.add(2)]
|
||||
acceptable += [self.tone.add(4, prefer_flats=flats), self.tone.add(7, prefer_flats=flats), self.tone.add(10, prefer_flats=flats), self.tone.add(2, prefer_flats=flats)]
|
||||
|
||||
elif self.quality == "dim":
|
||||
# Diminished: root, minor 3rd, diminished 5th
|
||||
acceptable += [self.tone.add(3), self.tone.add(6)]
|
||||
acceptable += [self.tone.add(3, prefer_flats=flats), self.tone.add(6, prefer_flats=flats)]
|
||||
|
||||
elif self.quality == "m6":
|
||||
# Minor 6th: root, minor 3rd, perfect 5th, major 6th
|
||||
acceptable += [self.tone.add(3), self.tone.add(7), self.tone.add(9)]
|
||||
acceptable += [self.tone.add(3, prefer_flats=flats), self.tone.add(7, prefer_flats=flats), self.tone.add(9, prefer_flats=flats)]
|
||||
|
||||
elif self.quality == "m7":
|
||||
# Minor 7th: root, minor 3rd, perfect 5th, minor 7th
|
||||
acceptable += [self.tone.add(3), self.tone.add(7), self.tone.add(10)]
|
||||
acceptable += [self.tone.add(3, prefer_flats=flats), self.tone.add(7, prefer_flats=flats), self.tone.add(10, prefer_flats=flats)]
|
||||
|
||||
elif self.quality == "m9":
|
||||
# Minor 9th: root, minor 3rd, perfect 5th, minor 7th, major 9th
|
||||
acceptable += [self.tone.add(3), self.tone.add(7), self.tone.add(10), self.tone.add(2)]
|
||||
acceptable += [self.tone.add(3, prefer_flats=flats), self.tone.add(7, prefer_flats=flats), self.tone.add(10, prefer_flats=flats), self.tone.add(2, prefer_flats=flats)]
|
||||
|
||||
elif self.quality == "maj7":
|
||||
# Major 7th: root, major 3rd, perfect 5th, major 7th
|
||||
acceptable += [self.tone.add(4), self.tone.add(7), self.tone.add(11)]
|
||||
acceptable += [self.tone.add(4, prefer_flats=flats), self.tone.add(7, prefer_flats=flats), self.tone.add(11, prefer_flats=flats)]
|
||||
|
||||
elif self.quality == "maj9":
|
||||
# Major 9th: root, major 3rd, perfect 5th, major 7th, major 9th
|
||||
acceptable += [self.tone.add(4), self.tone.add(7), self.tone.add(11), self.tone.add(2)]
|
||||
acceptable += [self.tone.add(4, prefer_flats=flats), self.tone.add(7, prefer_flats=flats), self.tone.add(11, prefer_flats=flats), self.tone.add(2, prefer_flats=flats)]
|
||||
|
||||
else:
|
||||
# Default (no quality): major triad
|
||||
acceptable += [self.tone.add(4), self.tone.add(7)]
|
||||
acceptable += [self.tone.add(4, prefer_flats=flats), self.tone.add(7, prefer_flats=flats)]
|
||||
|
||||
return tuple(acceptable)
|
||||
|
||||
@property
|
||||
def acceptable_tone_names(self):
|
||||
return tuple([tone.name for tone in self.acceptable_tones])
|
||||
names = [tone.name for tone in self.acceptable_tones]
|
||||
# The root tone is stored internally with sharp spelling (e.g. A#
|
||||
# for Bb) via flat_to_sharp mapping; restore the original flat name.
|
||||
if names and names[0] != self.tone_name:
|
||||
names[0] = self.tone_name
|
||||
return tuple(names)
|
||||
|
||||
def _possible_fingerings(self, *, fretboard):
|
||||
# Check the _possible_cache first
|
||||
key = self._cache_key(fretboard)
|
||||
if key in _possible_cache:
|
||||
return _possible_cache[key]
|
||||
|
||||
def find_fingerings(tone):
|
||||
fingerings = []
|
||||
for j in range(MAX_FRET):
|
||||
@@ -102,13 +307,21 @@ class NamedChord:
|
||||
|
||||
fingering = []
|
||||
for i, tone in enumerate(fretboard.tones):
|
||||
fingering.append(find_fingerings(tone))
|
||||
frets = find_fingerings(tone)
|
||||
# Always allow muting as an option
|
||||
if frets:
|
||||
fingering.append((*frets, -1))
|
||||
else:
|
||||
fingering.append((-1,))
|
||||
|
||||
for i, finger in enumerate(fingering):
|
||||
if finger == ():
|
||||
fingering[i] = (-1,)
|
||||
result = tuple(fingering)
|
||||
|
||||
return tuple(fingering)
|
||||
# Bounded cache: clear entirely if over limit
|
||||
if len(_possible_cache) >= _CACHE_MAX_SIZE:
|
||||
_possible_cache.clear()
|
||||
_possible_cache[key] = result
|
||||
|
||||
return result
|
||||
|
||||
@staticmethod
|
||||
def fix_fingering(fingering):
|
||||
@@ -121,38 +334,178 @@ class NamedChord:
|
||||
def fingerings(self, *, fretboard):
|
||||
return tuple(itertools.product(*self._possible_fingerings(fretboard=fretboard)))
|
||||
|
||||
def _cache_key(self, fretboard):
|
||||
"""Return a hashable key for memoization."""
|
||||
return (self.name, tuple(t.full_name for t in fretboard.tones))
|
||||
|
||||
def fingering(self, *, fretboard, multiple=False):
|
||||
# Check cache first
|
||||
key = self._cache_key(fretboard)
|
||||
if multiple:
|
||||
if key in _fingering_multi_cache:
|
||||
return _fingering_multi_cache[key]
|
||||
else:
|
||||
if key in _fingering_cache:
|
||||
return _fingering_cache[key]
|
||||
|
||||
# Check for curated guitar chord overrides in standard tuning
|
||||
tuning = tuple(t.full_name for t in fretboard.tones)
|
||||
if tuning == STANDARD_GUITAR_TUNING and self.name in GUITAR_OVERRIDES:
|
||||
string_names = tuple(t.name for t in fretboard.tones)
|
||||
override = GUITAR_OVERRIDES[self.name]
|
||||
if not multiple:
|
||||
result = Fingering(self.fix_fingering(override), string_names, fretboard=fretboard)
|
||||
if len(_fingering_cache) >= _CACHE_MAX_SIZE:
|
||||
_fingering_cache.clear()
|
||||
_fingering_cache[key] = result
|
||||
return result
|
||||
else:
|
||||
result = (Fingering(self.fix_fingering(override), string_names, fretboard=fretboard),)
|
||||
if len(_fingering_multi_cache) >= _CACHE_MAX_SIZE:
|
||||
_fingering_multi_cache.clear()
|
||||
_fingering_multi_cache[key] = result
|
||||
return result
|
||||
|
||||
MAX_SPAN = 4 # max fret span for a human hand
|
||||
|
||||
def fingering_score(fingering):
|
||||
def number_of_fingers(fingering):
|
||||
zeros = 0
|
||||
for finger in fingering:
|
||||
if finger == 0:
|
||||
zeros += 1
|
||||
return len(fingering) - zeros
|
||||
score = 0.0
|
||||
fretted = [f for f in fingering if f not in (0, -1)]
|
||||
muted = sum(1 for f in fingering if f == -1)
|
||||
sounding = len(fingering) - muted
|
||||
|
||||
def ascending(fingering):
|
||||
fingering = [f for f in fingering if f != 0]
|
||||
# Must have at least 2 sounding strings
|
||||
if sounding < 2:
|
||||
return -100.0
|
||||
|
||||
return sorted(fingering) == fingering
|
||||
# Hard constraint: fret span must be playable
|
||||
if fretted:
|
||||
span = max(fretted) - min(fretted)
|
||||
if span > MAX_SPAN:
|
||||
return -100.0
|
||||
else:
|
||||
span = 0
|
||||
|
||||
ascending = int(ascending(fingering))
|
||||
finger_count = number_of_fingers(fingering)
|
||||
return ascending + (1 / finger_count)
|
||||
# Check that all chord tones are present in the voicing
|
||||
sounding_names = set()
|
||||
for i, f in enumerate(fingering):
|
||||
if f != -1:
|
||||
sounding_names.add(fretboard.tones[i].add(f).name)
|
||||
required = set(t.name for t in self.acceptable_tones)
|
||||
missing = required - sounding_names
|
||||
score -= len(missing) * 5.0
|
||||
|
||||
# Reward open strings
|
||||
open_strings = sum(1 for f in fingering if f == 0)
|
||||
score += open_strings * 2.0
|
||||
|
||||
# Penalize muted strings, but only mildly
|
||||
score -= muted * 0.3
|
||||
|
||||
# Penalize fret span
|
||||
score -= span * 2.0
|
||||
|
||||
# Penalize high fret positions (prefer open position)
|
||||
if fretted:
|
||||
score -= (sum(fretted) / len(fretted)) * 0.8
|
||||
|
||||
# Barre chord detection: if multiple strings share the same
|
||||
# fret and it's the lowest fret in the shape, one finger can
|
||||
# cover them all — so they cost only 1 finger, not N.
|
||||
# Also check that barre strings are contiguous (no gaps).
|
||||
if fretted:
|
||||
min_fret = min(fretted)
|
||||
barre_indices = [i for i, f in enumerate(fingering) if f == min_fret and f > 0]
|
||||
barre_count = len(barre_indices)
|
||||
|
||||
if barre_count >= 2:
|
||||
unique_higher = len(set(f for f in fretted if f > min_fret))
|
||||
fingers_needed = unique_higher + 1 # 1 for barre
|
||||
# Mild reward for barre efficiency (saves fingers)
|
||||
score += (barre_count - 1) * 0.5
|
||||
else:
|
||||
fingers_needed = len(fretted)
|
||||
else:
|
||||
fingers_needed = 0
|
||||
|
||||
# Penalize fingers needed (max 4 on a guitar)
|
||||
score -= fingers_needed * 0.3
|
||||
if fingers_needed > 4:
|
||||
score -= (fingers_needed - 4) * 5.0
|
||||
|
||||
# Reward root in bass — the lowest sounding string
|
||||
for i in range(len(fingering) - 1, -1, -1):
|
||||
f = fingering[i]
|
||||
if f == -1:
|
||||
continue
|
||||
bass_tone = fretboard.tones[i].add(f)
|
||||
if bass_tone.name == self.tone.name:
|
||||
score += 4.0
|
||||
else:
|
||||
score -= 1.5
|
||||
break
|
||||
|
||||
# Prefer muting from the bass side (contiguous muting)
|
||||
# e.g. xx0232 is good, x0x232 is awkward
|
||||
mute_from_bass = 0
|
||||
for i in range(len(fingering) - 1, -1, -1):
|
||||
if fingering[i] == -1:
|
||||
mute_from_bass += 1
|
||||
else:
|
||||
break
|
||||
interior_mutes = muted - mute_from_bass
|
||||
score -= interior_mutes * 3.0
|
||||
|
||||
return score
|
||||
|
||||
def gen():
|
||||
fingerings = self.fingerings(fretboard=fretboard)
|
||||
score_map = tuple(map(fingering_score, fingerings))
|
||||
max_score = max(score_map)
|
||||
scored = [(fingering_score(f), f) for f in fingerings]
|
||||
max_score = max(s for s, _ in scored)
|
||||
|
||||
for possible_fingering in fingerings:
|
||||
if fingering_score(possible_fingering) == max_score:
|
||||
for s, possible_fingering in scored:
|
||||
if s == max_score:
|
||||
yield possible_fingering
|
||||
|
||||
string_names = tuple(t.name for t in fretboard.tones)
|
||||
best_fingerings = tuple([g for g in gen()])
|
||||
if not multiple:
|
||||
return self.fix_fingering(best_fingerings[0])
|
||||
result = Fingering(self.fix_fingering(best_fingerings[0]), string_names, fretboard=fretboard)
|
||||
# Bounded cache: clear entirely if over limit
|
||||
if len(_fingering_cache) >= _CACHE_MAX_SIZE:
|
||||
_fingering_cache.clear()
|
||||
_fingering_cache[key] = result
|
||||
return result
|
||||
else:
|
||||
return tuple([self.fix_fingering(f) for f in best_fingerings])
|
||||
result = tuple([Fingering(self.fix_fingering(f), string_names, fretboard=fretboard) for f in best_fingerings])
|
||||
# Bounded cache: clear entirely if over limit
|
||||
if len(_fingering_multi_cache) >= _CACHE_MAX_SIZE:
|
||||
_fingering_multi_cache.clear()
|
||||
_fingering_multi_cache[key] = result
|
||||
return result
|
||||
|
||||
def tab(self, *, fretboard):
|
||||
"""Render this chord as ASCII guitar tablature.
|
||||
|
||||
Example::
|
||||
|
||||
>>> print(CHARTS["western"]["C"].tab(fretboard=Fretboard.guitar()))
|
||||
C
|
||||
e|--0--
|
||||
B|--1--
|
||||
G|--0--
|
||||
D|--2--
|
||||
A|--3--
|
||||
E|--0--
|
||||
"""
|
||||
fingering = self.fingering(fretboard=fretboard)
|
||||
string_names = [t.name for t in fretboard.tones]
|
||||
lines = [self.name]
|
||||
max_name = max(len(n) for n in string_names)
|
||||
for i, (name, fret) in enumerate(zip(string_names, fingering)):
|
||||
fret_str = "x" if fret is None else str(fret)
|
||||
lines.append(f"{name:>{max_name}}|--{fret_str}--")
|
||||
return "\n".join(lines)
|
||||
|
||||
|
||||
western_chart = {}
|
||||
|
||||
+1954
-76
File diff suppressed because it is too large
Load Diff
+563
@@ -0,0 +1,563 @@
|
||||
"""PyTheory CLI — music theory from the command line."""
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import sys
|
||||
|
||||
|
||||
def cmd_tone(args):
|
||||
from .tones import Tone
|
||||
tone = Tone.from_string(args.note, system="western")
|
||||
freq = tone.pitch(temperament=args.temperament)
|
||||
print(f" Note: {tone.full_name}")
|
||||
print(f" Frequency: {freq:.2f} Hz ({args.temperament} temperament)")
|
||||
if args.temperament != "equal":
|
||||
import math
|
||||
equal_freq = tone.pitch(temperament="equal")
|
||||
diff_cents = 1200 * math.log2(freq / equal_freq) if freq > 0 else 0
|
||||
print(f" Equal temp: {equal_freq:.2f} Hz (diff: {diff_cents:+.1f} cents)")
|
||||
if tone.midi is not None:
|
||||
print(f" MIDI: {tone.midi}")
|
||||
if tone.enharmonic:
|
||||
print(f" Enharmonic: {tone.enharmonic}")
|
||||
print(f" Overtones: {', '.join(f'{h:.1f}' for h in tone.overtones(6))}")
|
||||
|
||||
|
||||
def cmd_scale(args):
|
||||
from .scales import TonedScale
|
||||
ts = TonedScale(tonic=f"{args.tonic}4", system=args.system)
|
||||
scale = ts[args.mode]
|
||||
print(f" {args.tonic} {args.mode}: {' '.join(scale.note_names)}")
|
||||
print(f" Intervals: {scale.tones[0].full_name}", end="")
|
||||
for i in range(1, len(scale.tones)):
|
||||
semitones = abs(scale.tones[i] - scale.tones[i-1])
|
||||
print(f" -{semitones}- {scale.tones[i].full_name}", end="")
|
||||
print()
|
||||
|
||||
|
||||
def cmd_chord(args):
|
||||
from .tones import Tone
|
||||
from .chords import Chord
|
||||
tones = [Tone.from_string(f"{n}4", system="western") for n in args.notes]
|
||||
chord = Chord(tones=tones)
|
||||
name = chord.identify() or "Unknown"
|
||||
print(f" Chord: {name}")
|
||||
print(f" Tones: {' '.join(t.full_name for t in chord.tones)}")
|
||||
print(f" Intervals: {chord.intervals}")
|
||||
print(f" Harmony: {chord.harmony:.4f}")
|
||||
print(f" Dissonance: {chord.dissonance:.4f}")
|
||||
t = chord.tension
|
||||
print(f" Tension: {t['score']:.2f} (tritones={t['tritones']})")
|
||||
|
||||
|
||||
def cmd_key(args):
|
||||
from .scales import Key
|
||||
key = Key(args.tonic, args.mode)
|
||||
sig = key.signature
|
||||
acc = ", ".join(sig["accidentals"]) if sig["accidentals"] else "none"
|
||||
print(f" Key: {key}")
|
||||
print(f" Signature: {sig['sharps']} sharps, {sig['flats']} flats ({acc})")
|
||||
print(f" Scale: {' '.join(key.note_names)}")
|
||||
print(f" Triads:")
|
||||
for chord in key.scale.harmonize():
|
||||
analysis = chord.analyze(args.tonic, args.mode) or "?"
|
||||
print(f" {analysis:6s} {chord}")
|
||||
print(f" 7th chords:")
|
||||
for name in key.seventh_chords:
|
||||
print(f" {name}")
|
||||
print(f" Relative: {key.relative}")
|
||||
print(f" Parallel: {key.parallel}")
|
||||
|
||||
|
||||
def cmd_fingering(args):
|
||||
from .charts import CHARTS
|
||||
from .chords import Fretboard
|
||||
chart = CHARTS.get("western", {})
|
||||
if args.chord not in chart:
|
||||
print(f" Unknown chord: {args.chord}")
|
||||
sys.exit(1)
|
||||
fb = Fretboard.guitar(capo=args.capo)
|
||||
print(chart[args.chord].tab(fretboard=fb))
|
||||
|
||||
|
||||
def cmd_progression(args):
|
||||
from .scales import Key
|
||||
key = Key(args.tonic, args.mode)
|
||||
chords = key.progression(*args.numerals)
|
||||
print(f" Key: {key}")
|
||||
print(f" Progression: {' → '.join(args.numerals)}")
|
||||
print()
|
||||
for numeral, chord in zip(args.numerals, chords):
|
||||
print(f" {numeral:6s} {chord}")
|
||||
|
||||
|
||||
def cmd_play(args):
|
||||
from .tones import Tone
|
||||
from .chords import Chord
|
||||
from .play import play, Synth, Envelope
|
||||
|
||||
synth_map = {
|
||||
"sine": Synth.SINE, "saw": Synth.SAW, "triangle": Synth.TRIANGLE,
|
||||
"square": Synth.SQUARE, "pulse": Synth.PULSE, "fm": Synth.FM,
|
||||
"noise": Synth.NOISE, "supersaw": Synth.SUPERSAW,
|
||||
"pwm_slow": Synth.PWM_SLOW, "pwm_fast": Synth.PWM_FAST,
|
||||
}
|
||||
synth = synth_map[args.synth]
|
||||
envelope = Envelope[args.envelope.upper()]
|
||||
duration = args.duration
|
||||
|
||||
# Try chord symbol first (e.g. "Am", "Cmaj7", "F#m7b5"),
|
||||
# then chart lookup, then fall back to individual notes.
|
||||
if len(args.notes) == 1:
|
||||
note = args.notes[0]
|
||||
target = None
|
||||
# Try as chord symbol first
|
||||
try:
|
||||
target = Chord.from_symbol(note)
|
||||
name = target.identify() or note
|
||||
label = f"{name} ({' '.join(t.full_name for t in target.tones)})"
|
||||
except ValueError:
|
||||
pass
|
||||
# Try chart lookup
|
||||
if target is None:
|
||||
try:
|
||||
target = Chord.from_name(note)
|
||||
name = target.identify() or note
|
||||
label = f"{name} ({' '.join(t.full_name for t in target.tones)})"
|
||||
except (ValueError, KeyError):
|
||||
pass
|
||||
# Fall back to single tone
|
||||
if target is None:
|
||||
target = Tone.from_string(
|
||||
note if any(c.isdigit() for c in note) else f"{note}4",
|
||||
system="western")
|
||||
label = target.full_name
|
||||
else:
|
||||
tones = [Tone.from_string(n if any(c.isdigit() for c in n) else f"{n}4",
|
||||
system="western") for n in args.notes]
|
||||
target = Chord(tones=tones)
|
||||
name = target.identify() or "Custom"
|
||||
label = f"{name} ({' '.join(t.full_name for t in tones)})"
|
||||
|
||||
print(f" Playing: {label}")
|
||||
print(f" Synth: {args.synth}")
|
||||
print(f" Envelope: {args.envelope}")
|
||||
print(f" Duration: {duration} ms")
|
||||
play(target, temperament=args.temperament, synth=synth, t=duration,
|
||||
envelope=envelope)
|
||||
|
||||
|
||||
def cmd_identify(args):
|
||||
from .chords import Chord
|
||||
chord = Chord.from_symbol(args.symbol)
|
||||
name = chord.identify() or "Unknown"
|
||||
sym = chord.symbol or args.symbol
|
||||
tones_str = " ".join(t.full_name for t in chord.tones)
|
||||
intervals = chord.intervals
|
||||
harmony = chord.harmony
|
||||
dissonance = chord.dissonance
|
||||
tension = chord.tension
|
||||
|
||||
print(f" Chord: {name}")
|
||||
print(f" Symbol: {sym}")
|
||||
print(f" Tones: {tones_str}")
|
||||
print(f" Intervals: {intervals}")
|
||||
print(f" Harmony: {harmony:.4f}")
|
||||
print(f" Dissonance: {dissonance:.4f}")
|
||||
print(f" Tension: score={tension['score']:.2f} tritones={tension['tritones']} "
|
||||
f"minor_2nds={tension['minor_seconds']} dominant={tension['has_dominant_function']}")
|
||||
|
||||
|
||||
def cmd_midi(args):
|
||||
from .scales import Key
|
||||
from .play import save_midi
|
||||
key = Key(args.tonic, args.mode)
|
||||
chords = key.progression(*args.numerals)
|
||||
save_midi(chords, args.output, t=args.duration, bpm=args.bpm)
|
||||
print(f" Key: {key}")
|
||||
print(f" Progression: {' '.join(args.numerals)}")
|
||||
print(f" BPM: {args.bpm}")
|
||||
print(f" Duration: {args.duration} ms")
|
||||
print(f" Output: {args.output}")
|
||||
|
||||
|
||||
def cmd_modes(args):
|
||||
from .scales import TonedScale
|
||||
ts = TonedScale(tonic=f"{args.tonic}4", system=args.system)
|
||||
mode_names = ["ionian", "dorian", "phrygian", "lydian",
|
||||
"mixolydian", "aeolian", "locrian"]
|
||||
print(f" Modes of {args.tonic}:\n")
|
||||
for mode in mode_names:
|
||||
try:
|
||||
scale = ts[mode]
|
||||
notes = " ".join(scale.note_names)
|
||||
print(f" {mode:<12s} {notes}")
|
||||
except KeyError:
|
||||
continue
|
||||
|
||||
|
||||
def cmd_circle(args):
|
||||
from .tones import Tone
|
||||
tone = Tone.from_string(f"{args.tonic}4", system="western")
|
||||
fifths = tone.circle_of_fifths()
|
||||
fourths = tone.circle_of_fourths()
|
||||
|
||||
print(f" Circle of fifths from {args.tonic}:")
|
||||
print(f" → {' → '.join(t.name for t in fifths)}")
|
||||
print()
|
||||
print(f" Circle of fourths from {args.tonic}:")
|
||||
print(f" → {' → '.join(t.name for t in fourths)}")
|
||||
|
||||
|
||||
def cmd_progressions(args):
|
||||
from .scales import Key
|
||||
key = Key(args.tonic, args.mode)
|
||||
progs = key.common_progressions()
|
||||
print(f" Common progressions in {key}:\n")
|
||||
for name, chords in progs.items():
|
||||
symbols = [c.symbol or str(c) for c in chords]
|
||||
print(f" {name:<20s} {' → '.join(symbols)}")
|
||||
|
||||
|
||||
def cmd_demo(args):
|
||||
import random
|
||||
from .rhythm import Score, Pattern, Duration
|
||||
from .chords import Chord
|
||||
from .scales import Key
|
||||
from .play import play_score
|
||||
|
||||
moods = [
|
||||
{"name": "Bossa Nova", "key": ("A", "minor"), "drums": "bossa nova",
|
||||
"fill": "bossa nova", "bpm": 140,
|
||||
"prog": ("i", "iv", "V", "i"),
|
||||
"lead": ("pluck_synth", "none", 0.2, -0.1),
|
||||
"pad": ("fm", "pad", -0.2),
|
||||
"bass_lp": 600, "reverb_type": "plate"},
|
||||
{"name": "Jazz Club", "key": ("Bb", "major"), "drums": "jazz",
|
||||
"fill": "jazz", "bpm": 108,
|
||||
"prog": ("I", "vi", "ii", "V"),
|
||||
"lead": ("triangle", "strings", 0.3, 0.2),
|
||||
"pad": ("fm", "piano", -0.3),
|
||||
"bass_lp": 700, "reverb_type": "plate"},
|
||||
{"name": "Afrobeat", "key": ("E", "minor"), "drums": "afrobeat",
|
||||
"fill": "afrobeat", "bpm": 115,
|
||||
"prog": ("i", "iv", "V", "i"),
|
||||
"lead": ("saw", "pluck", 0.15, 0.3),
|
||||
"pad": ("supersaw", "pad", 0.0),
|
||||
"bass_lp": 500, "reverb_type": "cathedral"},
|
||||
{"name": "House", "key": ("C", "minor"), "drums": "house",
|
||||
"fill": "house", "bpm": 124,
|
||||
"prog": ("i", "IV", "V", "i"),
|
||||
"lead": ("saw", "staccato", 0.2, 0.4),
|
||||
"pad": ("supersaw", "pad", 0.0),
|
||||
"bass_lp": 300, "reverb_type": "plate"},
|
||||
{"name": "Reggae", "key": ("G", "major"), "drums": "reggae",
|
||||
"fill": "reggae", "bpm": 80,
|
||||
"prog": ("I", "IV", "V", "IV"),
|
||||
"lead": ("pluck_synth", "none", 0.25, 0.15),
|
||||
"pad": ("organ_synth", "organ", -0.3),
|
||||
"bass_lp": 400, "reverb_type": "cathedral"},
|
||||
{"name": "Funk", "key": ("E", "minor"), "drums": "funk",
|
||||
"fill": "funk", "bpm": 100,
|
||||
"prog": ("i", "iv", "V", "i"),
|
||||
"lead": ("saw", "pluck", 0.15, 0.3),
|
||||
"pad": ("square", "staccato", -0.4),
|
||||
"bass_lp": 500, "reverb_type": "plate"},
|
||||
{"name": "Dub", "key": ("A", "minor"), "drums": "dub",
|
||||
"fill": "reggae", "bpm": 72,
|
||||
"prog": ("i", "iv", "i", "V"),
|
||||
"lead": ("triangle", "strings", 0.4, 0.2),
|
||||
"pad": ("pwm_slow", "pad", -0.3),
|
||||
"bass_lp": 350, "reverb_type": "cathedral"},
|
||||
{"name": "Temple", "key": ("E", "minor"), "drums": "bolero",
|
||||
"fill": "bossa nova", "bpm": 65,
|
||||
"prog": ("i", "iv", "V", "i"),
|
||||
"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)
|
||||
tonic, mode = mood["key"]
|
||||
key = Key(tonic, mode)
|
||||
chords = key.progression(*mood["prog"])
|
||||
lead_synth, lead_env, lead_reverb, lead_pan = mood["lead"]
|
||||
pad_synth, pad_env, pad_pan = mood["pad"]
|
||||
|
||||
score = Score("4/4", bpm=mood["bpm"], drum_humanize=0.15)
|
||||
score.drums(mood["drums"], repeats=4, fill=mood["fill"])
|
||||
|
||||
pad = score.part(
|
||||
"pad", synth=pad_synth, envelope=pad_env,
|
||||
volume=0.2, pan=pad_pan,
|
||||
detune=10, spread=0.5,
|
||||
reverb=0.4, reverb_type=mood["reverb_type"],
|
||||
chorus=0.2,
|
||||
sidechain=0.4 if mood["bpm"] > 100 else 0.0,
|
||||
)
|
||||
lead = score.part(
|
||||
"lead", synth=lead_synth, envelope=lead_env,
|
||||
volume=0.4, pan=lead_pan,
|
||||
delay=0.2, delay_time=round(30 / mood["bpm"], 3),
|
||||
delay_feedback=0.35,
|
||||
reverb=lead_reverb, reverb_type=mood["reverb_type"],
|
||||
lowpass=3500,
|
||||
humanize=0.2,
|
||||
)
|
||||
bass = score.part(
|
||||
"bass", synth="sine", envelope="pluck",
|
||||
volume=0.45, pan=0.0,
|
||||
lowpass=mood["bass_lp"],
|
||||
humanize=0.15,
|
||||
)
|
||||
|
||||
for chord in chords * 2:
|
||||
pad.add(chord, Duration.WHOLE)
|
||||
|
||||
# Melody: chord tones with passing tones, rests for breathing
|
||||
scale_tones = [t.name for t in key.scale.tones[:-1]]
|
||||
for i, chord in enumerate(chords):
|
||||
chord_tones = [t.name for t in chord.tones]
|
||||
beats_left = 4.0
|
||||
while beats_left > 0.5:
|
||||
if random.random() < 0.25:
|
||||
r = random.choice([0.5, 1.0, 1.5])
|
||||
r = min(r, beats_left)
|
||||
lead.rest(r)
|
||||
beats_left -= r
|
||||
else:
|
||||
n = random.choice(chord_tones if random.random() < 0.65 else scale_tones)
|
||||
oct = 5 if random.random() < 0.6 else 4
|
||||
dur = random.choice([0.67, 1.0, 1.5, 2.0])
|
||||
dur = min(dur, beats_left)
|
||||
vel = random.randint(65, 105)
|
||||
lead.add(f"{n}{oct}", dur, velocity=vel)
|
||||
beats_left -= dur
|
||||
|
||||
# Bass: root-fifth with velocity accents
|
||||
for chord in chords * 2:
|
||||
r = chord.root
|
||||
if r:
|
||||
fifth = r.add(7)
|
||||
bass.add(f"{r.name}2", Duration.QUARTER, velocity=95)
|
||||
bass.add(f"{r.name}2", Duration.QUARTER, velocity=55)
|
||||
bass.add(f"{fifth.name}2", Duration.QUARTER, velocity=65)
|
||||
bass.add(f"{r.name}2", Duration.QUARTER, velocity=75)
|
||||
|
||||
prog_str = " → ".join(c.symbol or str(c) for c in chords)
|
||||
print(f" ♫ {mood['name']}")
|
||||
print(f" {tonic} {mode} | {mood['bpm']} bpm")
|
||||
print(f" {prog_str}")
|
||||
print(f" {mood['drums']} | {lead_synth} lead | {pad_synth} pad | {mood['reverb_type']} reverb")
|
||||
print()
|
||||
|
||||
try:
|
||||
play_score(score)
|
||||
except KeyboardInterrupt:
|
||||
pass
|
||||
print(" ♫")
|
||||
|
||||
|
||||
def cmd_detect(args):
|
||||
from .scales import Key
|
||||
key = Key.detect(*args.notes)
|
||||
if key:
|
||||
print(f" Detected key: {key}")
|
||||
print(f" Scale: {' '.join(key.note_names)}")
|
||||
else:
|
||||
print(" Could not detect key")
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(
|
||||
prog="pytheory",
|
||||
description="Music Theory for Humans — from the command line",
|
||||
)
|
||||
sub = parser.add_subparsers(dest="command")
|
||||
|
||||
# tone
|
||||
p = sub.add_parser("tone", help="Look up a tone (e.g. pytheory tone C4)")
|
||||
p.add_argument("note", help="Note name with octave (e.g. C4, A#3)")
|
||||
p.add_argument("--temperament", "-t", default="equal",
|
||||
choices=["equal", "pythagorean", "meantone"],
|
||||
help="Tuning temperament (default: equal)")
|
||||
|
||||
# scale
|
||||
p = sub.add_parser("scale", help="Show a scale (e.g. pytheory scale C major)")
|
||||
p.add_argument("tonic", help="Tonic note (e.g. C, G, Sa)")
|
||||
p.add_argument("mode", help="Scale/mode name (e.g. major, minor, dorian)")
|
||||
p.add_argument("--system", default="western", help="Musical system (default: western)")
|
||||
|
||||
# chord
|
||||
p = sub.add_parser("chord", help="Identify a chord (e.g. pytheory chord C E G)")
|
||||
p.add_argument("notes", nargs="+", help="Note names (e.g. C E G)")
|
||||
|
||||
# key
|
||||
p = sub.add_parser("key", help="Explore a key (e.g. pytheory key C major)")
|
||||
p.add_argument("tonic", help="Tonic note (e.g. C, G)")
|
||||
p.add_argument("mode", nargs="?", default="major", help="Mode (default: major)")
|
||||
|
||||
# fingering
|
||||
p = sub.add_parser("fingering", help="Guitar fingering (e.g. pytheory fingering Am)")
|
||||
p.add_argument("chord", help="Chord name (e.g. C, Am, G7)")
|
||||
p.add_argument("--capo", type=int, default=0, help="Capo fret (default: 0)")
|
||||
|
||||
# progression
|
||||
p = sub.add_parser("progression", help="Build a progression (e.g. pytheory progression C major I V vi IV)")
|
||||
p.add_argument("tonic", help="Tonic note")
|
||||
p.add_argument("mode", help="Mode (e.g. major, minor)")
|
||||
p.add_argument("numerals", nargs="+", help="Roman numerals (e.g. I V vi IV)")
|
||||
|
||||
# play
|
||||
p = sub.add_parser("play", help="Play notes or chords (e.g. pytheory play C E G)")
|
||||
p.add_argument("notes", nargs="+", help="Note names, with optional octave (e.g. C4, A#3, or just C E G)")
|
||||
p.add_argument("--synth", "-s", default="sine",
|
||||
choices=["sine", "saw", "triangle", "square", "pulse",
|
||||
"fm", "noise", "supersaw", "pwm_slow", "pwm_fast"],
|
||||
help="Waveform (default: sine)")
|
||||
p.add_argument("--duration", "-d", type=int, default=1000,
|
||||
help="Duration in milliseconds (default: 1000)")
|
||||
p.add_argument("--temperament", "-t", default="equal",
|
||||
choices=["equal", "pythagorean", "meantone"],
|
||||
help="Tuning temperament (default: equal)")
|
||||
p.add_argument("--envelope", "-e", default="piano",
|
||||
choices=["none", "piano", "organ", "pluck", "pad",
|
||||
"strings", "bell", "staccato"],
|
||||
help="ADSR envelope preset (default: piano)")
|
||||
|
||||
# identify
|
||||
p = sub.add_parser("identify", help="Identify a chord symbol (e.g. pytheory identify Cmaj7)")
|
||||
p.add_argument("symbol", help="Chord symbol (e.g. Cmaj7, Am, F#m7b5)")
|
||||
|
||||
# midi
|
||||
p = sub.add_parser("midi", help="Export a progression to MIDI (e.g. pytheory midi C major I V vi IV)")
|
||||
p.add_argument("tonic", help="Tonic note")
|
||||
p.add_argument("mode", help="Mode (e.g. major, minor)")
|
||||
p.add_argument("numerals", nargs="+", help="Roman numerals (e.g. I V vi IV)")
|
||||
p.add_argument("-o", "--output", default="output.mid", help="Output file (default: output.mid)")
|
||||
p.add_argument("--bpm", type=int, default=120, help="BPM (default: 120)")
|
||||
p.add_argument("--duration", type=int, default=500, help="Duration per chord in ms (default: 500)")
|
||||
|
||||
# demo
|
||||
sub.add_parser("demo", help="Play a randomly generated track (different every time)")
|
||||
|
||||
# repl
|
||||
sub.add_parser("repl", help="Interactive music theory scratchpad")
|
||||
|
||||
# detect
|
||||
p = sub.add_parser("detect", help="Detect key from notes (e.g. pytheory detect C E G)")
|
||||
p.add_argument("notes", nargs="+", help="Note names")
|
||||
|
||||
# modes
|
||||
p = sub.add_parser("modes", help="Show all modes of a note (e.g. pytheory modes C)")
|
||||
p.add_argument("tonic", help="Tonic note (e.g. C, G)")
|
||||
p.add_argument("--system", default="western", help="Musical system (default: western)")
|
||||
|
||||
# circle
|
||||
p = sub.add_parser("circle", help="Circle of fifths/fourths (e.g. pytheory circle C)")
|
||||
p.add_argument("tonic", help="Starting note (e.g. C, G)")
|
||||
|
||||
# progressions
|
||||
p = sub.add_parser("progressions", help="Common progressions in a key (e.g. pytheory progressions C major)")
|
||||
p.add_argument("tonic", help="Tonic note")
|
||||
p.add_argument("mode", nargs="?", default="major", help="Mode (default: major)")
|
||||
|
||||
args = parser.parse_args()
|
||||
if not args.command:
|
||||
parser.print_help()
|
||||
sys.exit(0)
|
||||
|
||||
commands = {
|
||||
"tone": cmd_tone,
|
||||
"scale": cmd_scale,
|
||||
"chord": cmd_chord,
|
||||
"key": cmd_key,
|
||||
"fingering": cmd_fingering,
|
||||
"progression": cmd_progression,
|
||||
"play": cmd_play,
|
||||
"identify": cmd_identify,
|
||||
"midi": cmd_midi,
|
||||
"demo": cmd_demo,
|
||||
"repl": lambda args: __import__('pytheory.repl', fromlist=['main']).main(),
|
||||
"detect": cmd_detect,
|
||||
"modes": cmd_modes,
|
||||
"circle": cmd_circle,
|
||||
"progressions": cmd_progressions,
|
||||
}
|
||||
commands[args.command](args)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
+4808
-40
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,938 @@
|
||||
"""PyTheory REPL — make music interactively.
|
||||
|
||||
Commands mirror the Python API so there's no new vocabulary to learn.
|
||||
What you type in the REPL is what you'd type in a script.
|
||||
|
||||
Usage:
|
||||
pytheory repl
|
||||
"""
|
||||
|
||||
try:
|
||||
import readline
|
||||
except ImportError:
|
||||
readline = None
|
||||
import sys
|
||||
|
||||
from .scales import Key, TonedScale
|
||||
from .chords import Chord
|
||||
from .tones import Tone
|
||||
from .rhythm import Score, Pattern, Duration, Part
|
||||
|
||||
|
||||
# ── State ──────────────────────────────────────────────────────────────────
|
||||
|
||||
class Session:
|
||||
"""The live session state."""
|
||||
|
||||
def __init__(self):
|
||||
self.key = Key("C", "major")
|
||||
self.bpm = 120
|
||||
self.time_sig = "4/4"
|
||||
self.swing = 0.0
|
||||
self.score = Score(self.time_sig, bpm=self.bpm)
|
||||
self.current_part = None
|
||||
self.parts = {}
|
||||
self._drum_preset = None
|
||||
|
||||
def rebuild(self):
|
||||
"""Rebuild score from settings."""
|
||||
self.score = Score(self.time_sig, bpm=self.bpm, swing=self.swing)
|
||||
self.parts = {}
|
||||
self.current_part = None
|
||||
if self._drum_preset:
|
||||
self.score.drums(self._drum_preset, repeats=4)
|
||||
|
||||
def ensure_part(self, name="lead"):
|
||||
if name not in self.parts:
|
||||
self.parts[name] = self.score.part(name)
|
||||
return self.parts[name]
|
||||
|
||||
|
||||
# ── Commands ───────────────────────────────────────────────────────────────
|
||||
|
||||
def cmd_help(session, args):
|
||||
print("""
|
||||
PyTheory REPL — commands mirror the Python API
|
||||
|
||||
Theory:
|
||||
key Am Key("A", "minor")
|
||||
key G major Key("G", "major")
|
||||
chords key.chords
|
||||
prog I V vi IV key.progression(...)
|
||||
modes show all modes
|
||||
scales list available scales
|
||||
circle [C] circle of fifths/fourths
|
||||
interval C4 G4 name the interval
|
||||
identify C E G identify a chord from notes
|
||||
identify Cmaj7 analyze a chord symbol
|
||||
system [indian] switch musical system
|
||||
|
||||
Score:
|
||||
bpm 140 Score("4/4", bpm=140)
|
||||
time 3/4 TimeSignature
|
||||
swing 0.5 Score(swing=0.5)
|
||||
drums bossa nova score.drums("bossa nova")
|
||||
drums list all presets
|
||||
|
||||
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):
|
||||
add C5 1 part.add("C5", 1.0)
|
||||
add Am 4 part.add(Chord.from_symbol("Am"), 4.0)
|
||||
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
|
||||
delay 0.3 0.375 delay=0.3, delay_time=0.375
|
||||
lowpass 2000 3 lowpass=2000, lowpass_q=3
|
||||
distortion 0.5 distortion=0.5
|
||||
chorus 0.3 chorus=0.3
|
||||
sidechain 0.8 sidechain=0.8
|
||||
humanize 0.3 humanize=0.3
|
||||
volume 0.5 volume=0.5
|
||||
legato on legato=True
|
||||
glide 0.04 glide=0.04
|
||||
set lowpass 3000 part.set(lowpass=3000)
|
||||
lfo lowpass 0.5 400 3000 8 part.lfo("lowpass", rate=0.5, ...)
|
||||
|
||||
Playback:
|
||||
play_score play the full score
|
||||
play_pattern play just the drums
|
||||
render sketch.wav render to WAV
|
||||
save_midi sketch.mid save as MIDI
|
||||
|
||||
Guitar:
|
||||
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
|
||||
clear reset everything
|
||||
help this message
|
||||
quit exit
|
||||
""")
|
||||
|
||||
|
||||
def cmd_key(session, args):
|
||||
if not args:
|
||||
notes = " ".join(session.key.note_names)
|
||||
print(f" {session.key}: {notes}")
|
||||
return
|
||||
if len(args) == 1:
|
||||
name = args[0]
|
||||
if name.endswith("m") and len(name) <= 3:
|
||||
tonic, mode = name[:-1], "minor"
|
||||
else:
|
||||
tonic, mode = name, "major"
|
||||
else:
|
||||
tonic, mode = args[0], " ".join(args[1:])
|
||||
try:
|
||||
session.key = Key(tonic, mode)
|
||||
notes = " ".join(session.key.note_names)
|
||||
print(f" {session.key}: {notes}")
|
||||
except (KeyError, ValueError) as e:
|
||||
print(f" error: {e}")
|
||||
|
||||
|
||||
def cmd_bpm(session, args):
|
||||
if not args:
|
||||
print(f" bpm={session.bpm}")
|
||||
return
|
||||
session.bpm = int(args[0])
|
||||
session.score.bpm = session.bpm
|
||||
print(f" bpm={session.bpm}")
|
||||
|
||||
|
||||
def cmd_swing(session, args):
|
||||
if not args:
|
||||
print(f" swing={session.swing}")
|
||||
return
|
||||
session.swing = float(args[0])
|
||||
session.score.swing = session.swing
|
||||
print(f" swing={session.swing}")
|
||||
|
||||
|
||||
def cmd_drums(session, args):
|
||||
if not args:
|
||||
presets = Pattern.list_presets()
|
||||
cols = 4
|
||||
for i in range(0, len(presets), cols):
|
||||
row = presets[i:i + cols]
|
||||
print(" " + " ".join(f"{p:<18s}" for p in row))
|
||||
return
|
||||
preset = " ".join(args[:-1]) if args[-1].isdigit() else " ".join(args)
|
||||
repeats = int(args[-1]) if args[-1].isdigit() else 4
|
||||
try:
|
||||
session.score.drums(preset, repeats=repeats)
|
||||
session._drum_preset = preset # only persist after success
|
||||
print(f" score.drums(\"{preset}\", repeats={repeats})")
|
||||
except ValueError as e:
|
||||
print(f" error: {e}")
|
||||
|
||||
|
||||
def cmd_time(session, args):
|
||||
if not args:
|
||||
print(f" time={session.time_sig}")
|
||||
return
|
||||
session.time_sig = args[0]
|
||||
session.rebuild()
|
||||
print(f" time={session.time_sig}")
|
||||
|
||||
|
||||
def cmd_part(session, args):
|
||||
if not args:
|
||||
if session.parts:
|
||||
for name, part in session.parts.items():
|
||||
active = " ←" if part is session.current_part else ""
|
||||
print(f" {name}: synth={part.synth} envelope={part.envelope} "
|
||||
f"vol={part.volume}{active}")
|
||||
else:
|
||||
print(" no parts (type: part lead saw pluck)")
|
||||
return
|
||||
|
||||
name = args[0]
|
||||
|
||||
if name not in session.parts:
|
||||
# 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]
|
||||
|
||||
|
||||
def _require_part(session):
|
||||
if session.current_part is None:
|
||||
session.parts["lead"] = session.score.part("lead", synth="saw", envelope="pluck")
|
||||
session.current_part = session.parts["lead"]
|
||||
print(" (auto-created lead: saw + pluck)")
|
||||
return session.current_part
|
||||
|
||||
|
||||
def cmd_add(session, args):
|
||||
if not args:
|
||||
print(" usage: add C5 1 or add Am 4")
|
||||
return
|
||||
part = _require_part(session)
|
||||
name = args[0]
|
||||
beats = float(args[1]) if len(args) > 1 else 1.0
|
||||
velocity = int(args[2]) if len(args) > 2 else 100
|
||||
|
||||
# Try as chord first, then as note
|
||||
try:
|
||||
chord = Chord.from_symbol(name)
|
||||
part.add(chord, beats)
|
||||
print(f" .add(Chord.from_symbol(\"{name}\"), {beats})")
|
||||
return
|
||||
except (ValueError, KeyError):
|
||||
pass
|
||||
|
||||
try:
|
||||
part.add(name, beats, velocity=velocity)
|
||||
vel_str = f", velocity={velocity}" if velocity != 100 else ""
|
||||
print(f" .add(\"{name}\", {beats}{vel_str})")
|
||||
except Exception as e:
|
||||
print(f" error: {e}")
|
||||
|
||||
|
||||
def cmd_rest(session, args):
|
||||
part = _require_part(session)
|
||||
beats = float(args[0]) if args else 1.0
|
||||
part.rest(beats)
|
||||
print(f" .rest({beats})")
|
||||
|
||||
|
||||
def cmd_arp(session, args):
|
||||
if not args:
|
||||
print(" usage: arp Am [pattern] [bars] [octaves]")
|
||||
return
|
||||
part = _require_part(session)
|
||||
chord_name = args[0]
|
||||
pattern = args[1] if len(args) > 1 else "up"
|
||||
bars = float(args[2]) if len(args) > 2 else 2
|
||||
octaves = int(args[3]) if len(args) > 3 else 1
|
||||
try:
|
||||
part.arpeggio(chord_name, bars=bars, pattern=pattern, octaves=octaves)
|
||||
print(f" .arpeggio(\"{chord_name}\", pattern=\"{pattern}\", "
|
||||
f"bars={bars}, octaves={octaves})")
|
||||
except Exception as e:
|
||||
print(f" error: {e}")
|
||||
|
||||
|
||||
def cmd_prog(session, args):
|
||||
if not args:
|
||||
print(" usage: prog I V vi IV")
|
||||
return
|
||||
part = _require_part(session)
|
||||
try:
|
||||
chords = session.key.progression(*args)
|
||||
for chord in chords:
|
||||
part.add(chord, Duration.WHOLE)
|
||||
symbols = [c.symbol or str(c) for c in chords]
|
||||
print(f" {' → '.join(symbols)}")
|
||||
except Exception as e:
|
||||
print(f" error: {e}")
|
||||
|
||||
|
||||
def _set_effect(session, param, args, default=0.3):
|
||||
part = _require_part(session)
|
||||
value = float(args[0]) if args else default
|
||||
attr_map = {
|
||||
"reverb": "reverb_mix", "delay": "delay_mix",
|
||||
"distortion": "distortion_mix", "chorus": "chorus_mix",
|
||||
}
|
||||
attr = attr_map.get(param, param)
|
||||
setattr(part, attr, value)
|
||||
print(f" {part.name}: {param}={value}")
|
||||
|
||||
if param == "delay" and len(args) > 1:
|
||||
part.delay_time = float(args[1])
|
||||
print(f" {part.name}: delay_time={part.delay_time}")
|
||||
if param == "lowpass" and len(args) > 1:
|
||||
part.lowpass_q = float(args[1])
|
||||
print(f" {part.name}: lowpass_q={part.lowpass_q}")
|
||||
|
||||
|
||||
def cmd_set(session, args):
|
||||
"""Automation: part.set() at current beat."""
|
||||
if len(args) < 2:
|
||||
print(" usage: set lowpass 3000")
|
||||
return
|
||||
part = _require_part(session)
|
||||
param = args[0]
|
||||
value = float(args[1])
|
||||
part.set(**{param: value})
|
||||
print(f" .set({param}={value})")
|
||||
|
||||
|
||||
def cmd_lfo(session, args):
|
||||
"""LFO automation."""
|
||||
if len(args) < 4:
|
||||
print(" usage: lfo lowpass 0.5 400 3000 [bars] [shape]")
|
||||
return
|
||||
part = _require_part(session)
|
||||
param = args[0]
|
||||
rate = float(args[1])
|
||||
min_val = float(args[2])
|
||||
max_val = float(args[3])
|
||||
bars = float(args[4]) if len(args) > 4 else 4
|
||||
shape = args[5] if len(args) > 5 else "sine"
|
||||
part.lfo(param, rate=rate, min=min_val, max=max_val, bars=bars, shape=shape)
|
||||
print(f" .lfo(\"{param}\", rate={rate}, min={min_val}, max={max_val}, "
|
||||
f"bars={bars}, shape=\"{shape}\")")
|
||||
|
||||
|
||||
def cmd_legato(session, args):
|
||||
part = _require_part(session)
|
||||
part.legato = not (args and args[0] == "off")
|
||||
print(f" legato={'on' if part.legato else 'off'}")
|
||||
|
||||
|
||||
def cmd_play_score(session, args):
|
||||
try:
|
||||
from .play import play_score
|
||||
print(" ♫ play_score()")
|
||||
play_score(session.score)
|
||||
except Exception as e:
|
||||
print(f" error: {e}")
|
||||
|
||||
|
||||
def cmd_play_pattern(session, args):
|
||||
if not session._drum_preset:
|
||||
print(" no drums set")
|
||||
return
|
||||
try:
|
||||
from .play import play_pattern
|
||||
print(f" ♫ play_pattern(\"{session._drum_preset}\")")
|
||||
play_pattern(Pattern.preset(session._drum_preset),
|
||||
repeats=4, bpm=session.bpm)
|
||||
except Exception as e:
|
||||
print(f" error: {e}")
|
||||
|
||||
|
||||
def cmd_render(session, args):
|
||||
path = args[0] if args else "output.wav"
|
||||
try:
|
||||
from .play import render_score, SAMPLE_RATE
|
||||
import scipy.io.wavfile
|
||||
import numpy
|
||||
buf = render_score(session.score)
|
||||
pcm = (buf * 32767).astype(numpy.int16)
|
||||
scipy.io.wavfile.write(path, SAMPLE_RATE, pcm)
|
||||
print(f" saved: {path}")
|
||||
except Exception as e:
|
||||
print(f" error: {e}")
|
||||
|
||||
|
||||
def cmd_save_midi(session, args):
|
||||
path = args[0] if args else "output.mid"
|
||||
session.score.save_midi(path)
|
||||
print(f" save_midi(\"{path}\")")
|
||||
|
||||
|
||||
def cmd_show(session, args):
|
||||
s = session.score
|
||||
print(f" {s}")
|
||||
for name, part in session.parts.items():
|
||||
active = " ←" if part is session.current_part else ""
|
||||
fx = []
|
||||
if part.reverb_mix > 0:
|
||||
fx.append(f"reverb={part.reverb_mix}")
|
||||
if part.delay_mix > 0:
|
||||
fx.append(f"delay={part.delay_mix}")
|
||||
if part.lowpass > 0:
|
||||
fx.append(f"lp={part.lowpass}")
|
||||
if part.distortion_mix > 0:
|
||||
fx.append(f"dist={part.distortion_mix}")
|
||||
if part.chorus_mix > 0:
|
||||
fx.append(f"chorus={part.chorus_mix}")
|
||||
if part.legato:
|
||||
fx.append("legato")
|
||||
if part.humanize > 0:
|
||||
fx.append(f"humanize={part.humanize}")
|
||||
fx_str = " " + " ".join(fx) if fx else ""
|
||||
print(f" {name}: {part.synth}+{part.envelope} "
|
||||
f"{len(part.notes)} notes{fx_str}{active}")
|
||||
if s._drum_hits:
|
||||
print(f" drums: {session._drum_preset} ({len(s._drum_hits)} hits)")
|
||||
|
||||
|
||||
def cmd_chords(session, args):
|
||||
chords = session.key.chords
|
||||
for i, chord in enumerate(chords):
|
||||
from .chords import Chord as ChordClass
|
||||
# Build actual chord to get proper Roman numeral analysis
|
||||
c = session.key.triad(i)
|
||||
analysis = c.analyze(session.key.tonic_name, session.key.mode)
|
||||
label = analysis or str(i + 1)
|
||||
print(f" {label:6s} {chord}")
|
||||
|
||||
|
||||
def cmd_modes(session, args):
|
||||
ts = TonedScale(tonic=f"{session.key.tonic_name}4")
|
||||
for mode in ["ionian", "dorian", "phrygian", "lydian",
|
||||
"mixolydian", "aeolian", "locrian"]:
|
||||
try:
|
||||
print(f" {mode:<12s} {' '.join(ts[mode].note_names)}")
|
||||
except KeyError:
|
||||
continue
|
||||
|
||||
|
||||
def cmd_scales(session, args):
|
||||
ts = TonedScale(tonic=f"{session.key.tonic_name}4")
|
||||
for name in ts.scales:
|
||||
print(f" {name:<20s} {' '.join(ts[name].note_names)}")
|
||||
|
||||
|
||||
def cmd_fingering(session, args):
|
||||
"""Show guitar fingering for a chord."""
|
||||
if not args:
|
||||
print(" usage: fingering Am")
|
||||
return
|
||||
from .chords import Fretboard
|
||||
from .charts import CHARTS
|
||||
fb = Fretboard.guitar()
|
||||
name = args[0]
|
||||
chart = CHARTS.get("western", {})
|
||||
if name in chart:
|
||||
print(chart[name].tab(fretboard=fb))
|
||||
else:
|
||||
# Try from_symbol
|
||||
try:
|
||||
f = fb.chord(name)
|
||||
print(f" {f}")
|
||||
except (ValueError, KeyError) as e:
|
||||
print(f" error: {e}")
|
||||
|
||||
|
||||
def cmd_diagram(session, args):
|
||||
"""Show a scale diagram on guitar."""
|
||||
from .chords import Fretboard
|
||||
fb = Fretboard.guitar()
|
||||
mode = args[0] if args else session.key.mode
|
||||
frets = int(args[1]) if len(args) > 1 else 12
|
||||
|
||||
ts = TonedScale(tonic=f"{session.key.tonic_name}4")
|
||||
try:
|
||||
scale = ts[mode]
|
||||
print(fb.scale_diagram(scale, frets=frets))
|
||||
except KeyError:
|
||||
print(f" unknown scale: {mode}")
|
||||
|
||||
|
||||
def cmd_system(session, args):
|
||||
"""Switch musical system or show current."""
|
||||
if not args:
|
||||
from .systems import SYSTEMS
|
||||
for name in SYSTEMS:
|
||||
print(f" {name}")
|
||||
return
|
||||
system = args[0]
|
||||
# Default tonics per system
|
||||
default_tonics = {
|
||||
"western": "C", "indian": "Sa", "arabic": "Do",
|
||||
"japanese": "C", "blues": "C", "gamelan": "C",
|
||||
}
|
||||
tonic = args[1] if len(args) > 1 else default_tonics.get(system, "C")
|
||||
try:
|
||||
ts = TonedScale(tonic=f"{tonic}4", system=system)
|
||||
available = list(ts.scales)[:10]
|
||||
print(f" system: {system}")
|
||||
print(f" scales: {', '.join(available)}")
|
||||
if available:
|
||||
first = ts[available[0]]
|
||||
print(f" {available[0]}: {' '.join(first.note_names)}")
|
||||
except Exception as e:
|
||||
print(f" error: {e}")
|
||||
|
||||
|
||||
def cmd_interval(session, args):
|
||||
"""Show the interval between two notes."""
|
||||
if len(args) < 2:
|
||||
print(" usage: interval C4 G4")
|
||||
return
|
||||
try:
|
||||
t1 = Tone.from_string(args[0], system="western")
|
||||
t2 = Tone.from_string(args[1], system="western")
|
||||
print(f" {t1.full_name} → {t2.full_name}: {t1.interval_to(t2)}")
|
||||
print(f" {abs(t1 - t2)} semitones")
|
||||
except Exception as e:
|
||||
print(f" error: {e}")
|
||||
|
||||
|
||||
def cmd_identify(session, args):
|
||||
"""Identify a chord from notes or a symbol."""
|
||||
if not args:
|
||||
print(" usage: identify C E G or identify Cmaj7")
|
||||
return
|
||||
if len(args) == 1:
|
||||
try:
|
||||
chord = Chord.from_symbol(args[0])
|
||||
print(f" {chord.identify()}")
|
||||
print(f" symbol: {chord.symbol}")
|
||||
print(f" tones: {' '.join(t.full_name for t in chord.tones)}")
|
||||
print(f" intervals: {chord.intervals}")
|
||||
return
|
||||
except ValueError:
|
||||
pass
|
||||
# Try as individual notes
|
||||
try:
|
||||
tones = [Tone.from_string(f"{n}4", system="western") for n in args]
|
||||
chord = Chord(tones=tones)
|
||||
name = chord.identify() or "unknown"
|
||||
print(f" {name}")
|
||||
if chord.symbol:
|
||||
print(f" symbol: {chord.symbol}")
|
||||
except Exception as e:
|
||||
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
|
||||
tone = Tone.from_string(f"{tonic}4", system="western")
|
||||
fifths = [t.name for t in tone.circle_of_fifths()]
|
||||
fourths = [t.name for t in tone.circle_of_fourths()]
|
||||
print(f" fifths: {' → '.join(fifths)}")
|
||||
print(f" fourths: {' → '.join(fourths)}")
|
||||
|
||||
|
||||
def cmd_clear(session, args):
|
||||
"""Full reset — back to initial state."""
|
||||
session.key = Key("C", "major")
|
||||
session.bpm = 120
|
||||
session.time_sig = "4/4"
|
||||
session.swing = 0.0
|
||||
session._drum_preset = None
|
||||
session.score = Score(session.time_sig, bpm=session.bpm)
|
||||
session.parts = {}
|
||||
session.current_part = None
|
||||
print(" cleared (C major, 120 bpm)")
|
||||
|
||||
|
||||
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}")
|
||||
|
||||
|
||||
# ── Dispatch ───────────────────────────────────────────────────────────────
|
||||
|
||||
COMMANDS = {
|
||||
"help": cmd_help, "?": cmd_help,
|
||||
"key": cmd_key,
|
||||
"bpm": cmd_bpm,
|
||||
"swing": cmd_swing,
|
||||
"drums": cmd_drums,
|
||||
"time": cmd_time,
|
||||
"part": cmd_part,
|
||||
"add": cmd_add,
|
||||
"rest": cmd_rest,
|
||||
"arp": cmd_arp,
|
||||
"prog": cmd_prog, "progression": cmd_prog,
|
||||
"reverb": lambda s, a: _set_effect(s, "reverb", a),
|
||||
"delay": lambda s, a: _set_effect(s, "delay", a),
|
||||
"lowpass": lambda s, a: _set_effect(s, "lowpass", a, 2000),
|
||||
"lp": lambda s, a: _set_effect(s, "lowpass", a, 2000),
|
||||
"distortion": lambda s, a: _set_effect(s, "distortion", a),
|
||||
"dist": lambda s, a: _set_effect(s, "distortion", a),
|
||||
"chorus": lambda s, a: _set_effect(s, "chorus", a),
|
||||
"sidechain": lambda s, a: _set_effect(s, "sidechain", a),
|
||||
"humanize": lambda s, a: _set_effect(s, "humanize", a),
|
||||
"volume": lambda s, a: _set_effect(s, "volume", a, 0.5),
|
||||
"vol": lambda s, a: _set_effect(s, "volume", a, 0.5),
|
||||
"glide": lambda s, a: _set_effect(s, "glide", a, 0.04),
|
||||
"legato": cmd_legato,
|
||||
"set": cmd_set,
|
||||
"lfo": cmd_lfo,
|
||||
"play_score": cmd_play_score,
|
||||
"play_pattern": cmd_play_pattern,
|
||||
"render": cmd_render,
|
||||
"save_midi": cmd_save_midi,
|
||||
"show": cmd_show,
|
||||
"chords": cmd_chords,
|
||||
"modes": cmd_modes,
|
||||
"scales": cmd_scales,
|
||||
"fingering": cmd_fingering, "f": cmd_fingering,
|
||||
"diagram": cmd_diagram,
|
||||
"system": cmd_system,
|
||||
"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,
|
||||
}
|
||||
|
||||
|
||||
# ── Main ───────────────────────────────────────────────────────────────────
|
||||
|
||||
def _prompt(session):
|
||||
"""Build a context-aware multiline prompt."""
|
||||
key_str = f"{session.key.tonic_name}{('m' if session.key.mode == 'minor' else '')}"
|
||||
ctx = [f"key={key_str}", f"bpm={session.bpm}"]
|
||||
if session.swing > 0:
|
||||
ctx.append(f"swing={session.swing}")
|
||||
if session._drum_preset:
|
||||
ctx.append(f"drums={session._drum_preset}")
|
||||
if session.current_part is not None:
|
||||
p = session.current_part
|
||||
fx = []
|
||||
if p.reverb_mix > 0:
|
||||
fx.append(f"rev={p.reverb_mix}")
|
||||
if p.delay_mix > 0:
|
||||
fx.append(f"del={p.delay_mix}")
|
||||
if p.lowpass > 0:
|
||||
fx.append(f"lp={int(p.lowpass)}")
|
||||
if p.distortion_mix > 0:
|
||||
fx.append(f"dist={p.distortion_mix}")
|
||||
if p.legato:
|
||||
fx.append("legato")
|
||||
part_str = f"{p.name}({p.synth})"
|
||||
if fx:
|
||||
part_str += f" {' '.join(fx)}"
|
||||
ctx.append(f"→{part_str}")
|
||||
|
||||
# Single line if short, multiline if long
|
||||
oneline = f"pytheory[{' | '.join(ctx)}]> "
|
||||
if len(oneline) <= 60:
|
||||
return oneline
|
||||
|
||||
# Multiline
|
||||
lines = " " + " | ".join(ctx)
|
||||
return f"{lines}\n♫> "
|
||||
|
||||
|
||||
# ── Tab completion ─────────────────────────────────────────────────────────
|
||||
|
||||
_SYNTH_NAMES = ["sine", "saw", "triangle", "square", "pulse", "fm",
|
||||
"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", "bowed", "mallet", "none"]
|
||||
_ARP_PATTERNS = ["up", "down", "updown", "downup", "random"]
|
||||
_LFO_SHAPES = ["sine", "triangle", "saw", "square"]
|
||||
_SYSTEMS = ["western", "indian", "arabic", "japanese", "blues", "gamelan"]
|
||||
_NOTE_NAMES = ["C", "C#", "Db", "D", "D#", "Eb", "E", "F", "F#", "Gb",
|
||||
"G", "G#", "Ab", "A", "A#", "Bb", "B"]
|
||||
_CHORD_SUFFIXES = ["", "m", "7", "m7", "maj7", "dim", "aug", "sus2", "sus4",
|
||||
"m7b5", "dim7", "9", "m9", "maj9"]
|
||||
|
||||
# Context-aware completions for the second word
|
||||
_ARG_COMPLETIONS = {
|
||||
"drums": lambda: Pattern.list_presets(),
|
||||
"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"]],
|
||||
"chord": lambda: [f"{n}{s}" for n in _NOTE_NAMES[:7] for s in _CHORD_SUFFIXES[:6]],
|
||||
"fingering": lambda: [f"{n}{s}" for n in _NOTE_NAMES[:7] for s in _CHORD_SUFFIXES[:4]],
|
||||
"system": lambda: _SYSTEMS,
|
||||
"lfo": lambda: ["lowpass", "reverb", "delay", "distortion", "chorus", "volume"],
|
||||
"set": lambda: ["lowpass", "reverb", "delay", "distortion", "chorus", "volume",
|
||||
"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,
|
||||
}
|
||||
|
||||
|
||||
def _completer(text, state):
|
||||
"""Tab completion for the REPL."""
|
||||
line = readline.get_line_buffer() if readline else ""
|
||||
tokens = line.split()
|
||||
|
||||
if len(tokens) <= 1:
|
||||
# First word: complete command names
|
||||
options = [cmd for cmd in COMMANDS if cmd.startswith(text)]
|
||||
else:
|
||||
# Second+ word: context-aware
|
||||
cmd = tokens[0].lower()
|
||||
if cmd in _ARG_COMPLETIONS:
|
||||
try:
|
||||
candidates = _ARG_COMPLETIONS[cmd]()
|
||||
options = [c for c in candidates if c.startswith(text)]
|
||||
except Exception:
|
||||
options = []
|
||||
elif cmd == "part" and len(tokens) == 3:
|
||||
# Third arg for part is envelope
|
||||
options = [e for e in _ENVELOPE_NAMES if e.startswith(text)]
|
||||
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)]
|
||||
else:
|
||||
options = []
|
||||
|
||||
if state < len(options):
|
||||
return options[state] + " "
|
||||
return None
|
||||
|
||||
|
||||
def main():
|
||||
session = Session()
|
||||
|
||||
# Set up tab completion
|
||||
if readline:
|
||||
readline.set_completer(_completer)
|
||||
readline.parse_and_bind("tab: complete")
|
||||
readline.set_completer_delims(" ")
|
||||
|
||||
print()
|
||||
print(" ♫ PyTheory REPL")
|
||||
print(" ════════════════════════════════════════")
|
||||
print()
|
||||
print(" try: key Am — set a key")
|
||||
print(" chords — see its chords")
|
||||
print(" prog I V vi IV — hear a progression")
|
||||
print(" drums bossa nova")
|
||||
print(" play_score — hear it all")
|
||||
print()
|
||||
print(" help for all commands, quit to exit")
|
||||
print()
|
||||
|
||||
while True:
|
||||
try:
|
||||
line = input(_prompt(session)).strip()
|
||||
except (EOFError, KeyboardInterrupt):
|
||||
print("\n ♫")
|
||||
break
|
||||
|
||||
if not line:
|
||||
continue
|
||||
if line in ("quit", "exit", "q"):
|
||||
print(" ♫")
|
||||
break
|
||||
|
||||
tokens = line.split()
|
||||
cmd = tokens[0].lower()
|
||||
args = tokens[1:]
|
||||
|
||||
if cmd in COMMANDS:
|
||||
try:
|
||||
COMMANDS[cmd](session, args)
|
||||
except Exception:
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
else:
|
||||
print(f" unknown: {cmd} (type 'help')")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
+3802
File diff suppressed because it is too large
Load Diff
+867
-22
@@ -1,11 +1,25 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Optional, Union
|
||||
|
||||
import numeral
|
||||
|
||||
from .systems import SYSTEMS
|
||||
from .systems import SYSTEMS, System
|
||||
from .tones import Tone
|
||||
|
||||
|
||||
class Scale:
|
||||
def __init__(self, *, tones, degrees=None, system='western'):
|
||||
def __init__(self, *, tones: tuple[Tone, ...], degrees: Optional[tuple[str, ...]] = None, system: Union[str, System] = 'western') -> None:
|
||||
"""Initialize a Scale from a sequence of Tones.
|
||||
|
||||
Args:
|
||||
tones: The tones that make up the scale.
|
||||
degrees: Optional names for each scale degree (must match length of *tones*).
|
||||
system: A tone system name or :class:`System` instance.
|
||||
|
||||
Raises:
|
||||
ValueError: If *degrees* is provided but its length differs from *tones*.
|
||||
"""
|
||||
self.tones = tones
|
||||
self.degrees = degrees
|
||||
|
||||
@@ -21,14 +35,18 @@ class Scale:
|
||||
raise ValueError("The number of tones and degrees must be equal!")
|
||||
|
||||
@property
|
||||
def system(self):
|
||||
def system(self) -> Optional[System]:
|
||||
"""Return the tone system for this scale.
|
||||
|
||||
Resolves a system name to a :class:`System` object on first access.
|
||||
"""
|
||||
if self._system:
|
||||
return self._system
|
||||
|
||||
if self.system_name:
|
||||
return SYSTEMS[self.system_name]
|
||||
|
||||
def __repr__(self):
|
||||
def __repr__(self) -> str:
|
||||
r = []
|
||||
for (i, tone) in enumerate(self.tones):
|
||||
degree = numeral.int2roman(i + 1, only_ascii=True)
|
||||
@@ -38,22 +56,86 @@ class Scale:
|
||||
return f"<Scale {r}>"
|
||||
|
||||
def __iter__(self):
|
||||
"""Iterate over the tones in this scale."""
|
||||
return iter(self.tones)
|
||||
|
||||
def __len__(self):
|
||||
def __len__(self) -> int:
|
||||
"""Return the number of tones in this scale (including the octave)."""
|
||||
return len(self.tones)
|
||||
|
||||
def __contains__(self, item):
|
||||
def __contains__(self, item: Union[str, Tone]) -> bool:
|
||||
"""Check whether a tone or note name belongs to this scale."""
|
||||
if isinstance(item, str):
|
||||
return any(item == t.name for t in self.tones)
|
||||
return item in self.tones
|
||||
|
||||
@property
|
||||
def note_names(self):
|
||||
def note_names(self) -> list[str]:
|
||||
"""List of note names in this scale."""
|
||||
return [t.name for t in self.tones]
|
||||
|
||||
def chord(self, *degrees):
|
||||
def fitness(self, *note_names: str) -> float:
|
||||
"""Score how well a set of notes fits this scale (0.0–1.0).
|
||||
|
||||
Returns the fraction of the given notes that appear in the
|
||||
scale. Useful for melody analysis — testing whether a phrase
|
||||
belongs to a particular scale or mode.
|
||||
|
||||
Args:
|
||||
*note_names: Note name strings (e.g. ``"C"``, ``"F#"``).
|
||||
|
||||
Returns:
|
||||
A float from 0.0 (no notes match) to 1.0 (all notes match).
|
||||
|
||||
Example::
|
||||
|
||||
>>> c_major = TonedScale(tonic="C4")["major"]
|
||||
>>> c_major.fitness("C", "D", "E", "G")
|
||||
1.0
|
||||
>>> c_major.fitness("C", "D", "F#", "G")
|
||||
0.75
|
||||
>>> c_major.fitness("C#", "D#", "F#")
|
||||
0.0
|
||||
"""
|
||||
if not note_names:
|
||||
return 0.0
|
||||
scale_notes = set(self.note_names)
|
||||
matches = sum(1 for n in note_names if n in scale_notes)
|
||||
return matches / len(note_names)
|
||||
|
||||
_DEGREE_NAMES = [
|
||||
"tonic", "supertonic", "mediant", "subdominant",
|
||||
"dominant", "submediant", "leading tone",
|
||||
]
|
||||
|
||||
_MINOR_DEGREE_NAMES = [
|
||||
"tonic", "supertonic", "mediant", "subdominant",
|
||||
"dominant", "submediant", "subtonic",
|
||||
]
|
||||
|
||||
def degree_name(self, n: int, *, minor: bool = False) -> str:
|
||||
"""Return the traditional name for the nth scale degree (0-indexed).
|
||||
|
||||
Args:
|
||||
n: The scale degree index (0 = tonic, 1 = supertonic, etc.).
|
||||
minor: If True, use "subtonic" instead of "leading tone" for degree 6.
|
||||
|
||||
Returns:
|
||||
A string like "tonic", "dominant", etc.
|
||||
|
||||
Example::
|
||||
|
||||
>>> TonedScale(tonic="C4")["major"].degree_name(0)
|
||||
'tonic'
|
||||
>>> TonedScale(tonic="C4")["major"].degree_name(4)
|
||||
'dominant'
|
||||
"""
|
||||
names = self._MINOR_DEGREE_NAMES if minor else self._DEGREE_NAMES
|
||||
if 0 <= n < len(names):
|
||||
return names[n]
|
||||
return f"degree {n}"
|
||||
|
||||
def chord(self, *degrees: int) -> Chord:
|
||||
"""Build a Chord from scale degrees (0-indexed).
|
||||
|
||||
Wraps around if degrees exceed the scale length, transposing
|
||||
@@ -75,15 +157,246 @@ class Scale:
|
||||
result.append(tone)
|
||||
return Chord(tones=result)
|
||||
|
||||
def triad(self, root=0):
|
||||
def transpose(self, semitones: int) -> Scale:
|
||||
"""Return a new Scale transposed by the given number of semitones.
|
||||
|
||||
Every tone is shifted by the same interval, preserving the
|
||||
scale's interval pattern.
|
||||
|
||||
Example::
|
||||
|
||||
>>> c_major = TonedScale(tonic="C4")["major"]
|
||||
>>> d_major = c_major.transpose(2)
|
||||
>>> d_major.note_names
|
||||
['D', 'E', 'F#', 'G', 'A', 'B', 'C#', 'D']
|
||||
"""
|
||||
from .chords import Chord
|
||||
new_tones = tuple(t.add(semitones) for t in self.tones)
|
||||
return Scale(tones=new_tones)
|
||||
|
||||
def triad(self, root: int = 0) -> Chord:
|
||||
"""Build a triad starting from the given scale degree (0-indexed).
|
||||
|
||||
Returns a chord with the root, 3rd, and 5th above it.
|
||||
"""
|
||||
return self.chord(root, root + 2, root + 4)
|
||||
|
||||
def degree(self, item, major=None, minor=False):
|
||||
# TODO: cleanup degrees.
|
||||
def seventh(self, root: int = 0) -> Chord:
|
||||
"""Build a seventh chord from the given scale degree (0-indexed).
|
||||
|
||||
Returns a chord with the root, 3rd, 5th, and 7th.
|
||||
"""
|
||||
return self.chord(root, root + 2, root + 4, root + 6)
|
||||
|
||||
def progression(self, *numerals: str) -> list[Chord]:
|
||||
"""Build a chord progression from Roman numeral strings.
|
||||
|
||||
Accepts Roman numerals like ``"I"``, ``"IV"``, ``"V"``,
|
||||
``"ii"``, ``"vi"``. Lowercase = minor triad, uppercase = major
|
||||
triad. Add ``"7"`` suffix for seventh chords.
|
||||
|
||||
Example::
|
||||
|
||||
>>> 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
|
||||
chords = []
|
||||
for num in numerals:
|
||||
is_seventh = num.endswith("7")
|
||||
clean = num.rstrip("7")
|
||||
# Handle flat-degree prefixes: bVI, bVII, bIII, etc.
|
||||
flat_offset = 0
|
||||
if clean.startswith("b") and len(clean) > 1:
|
||||
clean = clean[1:]
|
||||
flat_offset = -1 # one semitone down
|
||||
elif clean.startswith("#") and len(clean) > 1:
|
||||
clean = clean[1:]
|
||||
flat_offset = 1 # one semitone up
|
||||
degree = numeral_mod.roman2int(clean.upper()) - 1
|
||||
if is_seventh:
|
||||
chord = self.seventh(degree)
|
||||
else:
|
||||
chord = self.triad(degree)
|
||||
if flat_offset != 0:
|
||||
chord = chord.transpose(flat_offset)
|
||||
chords.append(chord)
|
||||
return chords
|
||||
|
||||
def nashville(self, *numbers: Union[int, str]) -> list[Chord]:
|
||||
"""Build a chord progression using Nashville number system.
|
||||
|
||||
The `Nashville number system <https://en.wikipedia.org/wiki/Nashville_Number_System>`_
|
||||
uses Arabic numerals instead of Roman numerals.
|
||||
It's the standard chart system in Nashville recording studios.
|
||||
|
||||
Numbers 1-7 build diatonic triads. Suffix ``"7"`` for seventh
|
||||
chords, ``"m"`` to force minor.
|
||||
|
||||
Example::
|
||||
|
||||
>>> scale.nashville(1, 4, 5, 1)
|
||||
[<Chord C major>, <Chord F major>, <Chord G major>, <Chord C major>]
|
||||
"""
|
||||
from .chords import Chord
|
||||
chords = []
|
||||
for num in numbers:
|
||||
s = str(num)
|
||||
is_seventh = s.endswith("7")
|
||||
clean = s.rstrip("7m")
|
||||
degree = int(clean) - 1
|
||||
if is_seventh:
|
||||
chords.append(self.seventh(degree))
|
||||
else:
|
||||
chords.append(self.triad(degree))
|
||||
return chords
|
||||
|
||||
@staticmethod
|
||||
def detect(*note_names: str) -> Optional[tuple[str, str, int]]:
|
||||
"""Detect the most likely scale from a set of note names.
|
||||
|
||||
Tries all scales in the Western system and returns the best
|
||||
match as a ``(tonic, scale_name, match_count)`` tuple.
|
||||
|
||||
Example::
|
||||
|
||||
>>> Scale.detect("C", "D", "E", "F", "G", "A", "B")
|
||||
('C', 'major', 7)
|
||||
>>> Scale.detect("C", "D", "Eb", "F", "G", "Ab", "Bb")
|
||||
('C', 'minor', 7)
|
||||
"""
|
||||
if not note_names:
|
||||
return None
|
||||
|
||||
notes = set(note_names)
|
||||
best = None
|
||||
|
||||
chromatic = ["C", "C#", "D", "D#", "E", "F", "F#", "G", "G#", "A", "A#", "B"]
|
||||
scale_names = ["major", "minor", "harmonic minor",
|
||||
"dorian", "phrygian", "lydian", "mixolydian",
|
||||
"aeolian", "locrian"]
|
||||
|
||||
for tonic in chromatic:
|
||||
ts = TonedScale(tonic=f"{tonic}4")
|
||||
for scale_name in ts.scales:
|
||||
try:
|
||||
scale = ts[scale_name]
|
||||
scale_notes = set(scale.note_names)
|
||||
match = len(notes & scale_notes)
|
||||
score = (match, 1 if scale_name == "major" else 0)
|
||||
if best is None or score > best[0]:
|
||||
best = (score, tonic, scale_name, match)
|
||||
except (KeyError, ValueError):
|
||||
continue
|
||||
|
||||
if best:
|
||||
return (best[1], best[2], best[3])
|
||||
return None
|
||||
|
||||
@staticmethod
|
||||
def recommend(*note_names: str, top: int = 5) -> list[tuple[str, str, float]]:
|
||||
"""Recommend the best-matching scales for a set of notes, ranked by fitness.
|
||||
|
||||
Tests the given notes against every scale in the Western system
|
||||
and returns the top matches. Useful for figuring out what scale
|
||||
a melody or chord progression belongs to, or finding alternative
|
||||
scales to play over a set of changes.
|
||||
|
||||
Args:
|
||||
*note_names: Note name strings (e.g. ``"C"``, ``"E"``, ``"G"``).
|
||||
top: Number of results to return (default 5).
|
||||
|
||||
Returns:
|
||||
A list of ``(tonic, scale_name, fitness)`` tuples sorted
|
||||
by fitness descending. Fitness is 0.0–1.0.
|
||||
|
||||
Example::
|
||||
|
||||
>>> Scale.recommend("C", "D", "E", "G", "A")
|
||||
[('C', 'major', 1.0), ('G', 'major', 1.0), ...]
|
||||
>>> Scale.recommend("C", "Eb", "F", "Gb", "G", "Bb")
|
||||
[('C', 'blues', 1.0), ...]
|
||||
"""
|
||||
if not note_names:
|
||||
return []
|
||||
|
||||
results = []
|
||||
chromatic = ["C", "C#", "D", "D#", "E", "F",
|
||||
"F#", "G", "G#", "A", "A#", "B"]
|
||||
|
||||
for tonic in chromatic:
|
||||
ts = TonedScale(tonic=f"{tonic}4")
|
||||
for scale_name in ts.scales:
|
||||
try:
|
||||
scale = ts[scale_name]
|
||||
fit = scale.fitness(*note_names)
|
||||
if fit > 0:
|
||||
results.append((tonic, scale_name, fit))
|
||||
except (KeyError, ValueError):
|
||||
continue
|
||||
|
||||
# Penalize chromatic scale — it matches everything but tells you nothing
|
||||
# Also prefer scales whose length is closer to the input length
|
||||
input_len = len(note_names)
|
||||
|
||||
def _score(r):
|
||||
tonic, name, fit = r
|
||||
penalty = 0.5 if "chromatic" in name else 0
|
||||
return (-fit + penalty, abs(input_len - 7), name, tonic)
|
||||
|
||||
results.sort(key=_score)
|
||||
return results[:top]
|
||||
|
||||
def harmonize(self) -> list[Chord]:
|
||||
"""Build diatonic triads on every scale degree.
|
||||
|
||||
Returns a list of Chords — one triad for each degree of the
|
||||
scale. In a major scale this produces: I, ii, iii, IV, V, vi, vii°.
|
||||
|
||||
Example::
|
||||
|
||||
>>> [c.identify() for c in TonedScale(tonic="C4")["major"].harmonize()]
|
||||
['C major', 'D minor', 'E minor', 'F major', 'G major', 'A minor', 'B diminished']
|
||||
"""
|
||||
unique = len(self.tones) - 1
|
||||
return [self.triad(i) for i in range(unique)]
|
||||
|
||||
def parallel_modes(self) -> dict[str, list[str]]:
|
||||
"""All modes that share the same notes as this scale.
|
||||
|
||||
For example, C major shares its notes with D dorian,
|
||||
E phrygian, F lydian, G mixolydian, A aeolian, and
|
||||
B locrian.
|
||||
|
||||
Returns:
|
||||
A dict mapping ``"tonic mode"`` to note name lists.
|
||||
|
||||
Example::
|
||||
|
||||
>>> c_major = TonedScale(tonic="C4")["major"]
|
||||
>>> c_major.parallel_modes()
|
||||
{'C ionian': ['C', 'D', 'E', ...], 'D dorian': ['D', 'E', 'F', ...], ...}
|
||||
"""
|
||||
mode_names = ["ionian", "dorian", "phrygian", "lydian",
|
||||
"mixolydian", "aeolian", "locrian"]
|
||||
unique = len(self.tones) - 1
|
||||
if unique != 7:
|
||||
return {}
|
||||
|
||||
result = {}
|
||||
for i, mode in enumerate(mode_names):
|
||||
if i >= unique:
|
||||
break
|
||||
tonic = self.tones[i]
|
||||
ts = TonedScale(tonic=tonic.full_name)
|
||||
try:
|
||||
scale = ts[mode]
|
||||
result[f"{tonic.name} {mode}"] = scale.note_names
|
||||
except KeyError:
|
||||
continue
|
||||
return result
|
||||
|
||||
def degree(self, item: Union[str, int, slice], major: Optional[bool] = None, minor: bool = False) -> Optional[Union[Tone, tuple[Tone, ...]]]:
|
||||
|
||||
# Ensure that both major and minor aren't passed.
|
||||
if all((major, minor)):
|
||||
@@ -115,58 +428,590 @@ class Scale:
|
||||
if isinstance(item, int) or isinstance(item, slice):
|
||||
return self.tones[item]
|
||||
|
||||
def __getitem__(self, item):
|
||||
def __getitem__(self, item: Union[str, int, slice]) -> Union[Tone, tuple[Tone, ...]]:
|
||||
"""Retrieve a tone by scale degree (integer, Roman numeral, or degree name).
|
||||
|
||||
Raises:
|
||||
KeyError: If the given degree is not found in this scale.
|
||||
"""
|
||||
result = self.degree(item)
|
||||
if result is None:
|
||||
raise KeyError(item)
|
||||
return result
|
||||
|
||||
|
||||
PROGRESSIONS = {
|
||||
# Rock / Pop / Folk
|
||||
"I-IV-V-I": ("I", "IV", "V", "I"),
|
||||
"I-V-vi-IV": ("I", "V", "vi", "IV"),
|
||||
"I-vi-IV-V": ("I", "vi", "IV", "V"),
|
||||
"I-IV-vi-V": ("I", "IV", "vi", "V"),
|
||||
"vi-IV-I-V": ("vi", "IV", "I", "V"),
|
||||
# Blues
|
||||
"12-bar blues": ("I", "I", "I", "I", "IV", "IV", "I", "I", "V", "IV", "I", "V"),
|
||||
# Jazz
|
||||
"ii-V-I": ("ii", "V7", "I"),
|
||||
"I-vi-ii-V": ("I", "vi", "ii", "V"), # rhythm changes A section
|
||||
"iii-vi-ii-V": ("iii", "vi", "ii", "V"), # jazz turnaround
|
||||
# Classical / Film
|
||||
"i-bVI-bIII-bVII": ("i", "VI", "III", "VII"),
|
||||
"Pachelbel": ("I", "V", "vi", "iii", "IV", "I", "IV", "V"),
|
||||
# Flamenco / Spanish
|
||||
"Andalusian": ("i", "VII", "VI", "V"),
|
||||
# Modal
|
||||
"Dorian vamp": ("i", "IV"),
|
||||
"Mixolydian vamp": ("I", "VII"),
|
||||
}
|
||||
"""Common chord progressions as Roman numeral tuples.
|
||||
|
||||
Use with :meth:`Scale.progression` or :meth:`Key.progression`::
|
||||
|
||||
Key("C", "major").progression(*PROGRESSIONS["I-V-vi-IV"])
|
||||
"""
|
||||
|
||||
|
||||
class Key:
|
||||
"""A musical key — a convenient entry point for scales and harmony.
|
||||
|
||||
A Key represents a tonic note and a mode. It provides quick access
|
||||
to the scale, diatonic chords, and common progressions.
|
||||
|
||||
Example::
|
||||
|
||||
>>> key = Key("C", "major")
|
||||
>>> key.scale.note_names
|
||||
['C', 'D', 'E', 'F', 'G', 'A', 'B', 'C']
|
||||
>>> key.chords
|
||||
['C major', 'D minor', 'E minor', 'F major', ...]
|
||||
>>> key.progression("I", "V", "vi", "IV")
|
||||
[<Chord (C,E,G)>, <Chord (G,B,D)>, ...]
|
||||
"""
|
||||
|
||||
def __init__(self, tonic: str, mode: str = "major", system: Optional[Union[str, System]] = None) -> None:
|
||||
if system is None:
|
||||
system = SYSTEMS["western"]
|
||||
elif isinstance(system, str):
|
||||
system = SYSTEMS[system]
|
||||
self.tonic_name = tonic
|
||||
self.mode = mode
|
||||
self._system = system
|
||||
self._toned_scale = TonedScale(tonic=f"{tonic}4", system=system)
|
||||
self._scale = self._toned_scale[mode]
|
||||
|
||||
@classmethod
|
||||
def detect(cls, *note_names: str) -> Optional[Key]:
|
||||
"""Detect the most likely key from a set of note names.
|
||||
|
||||
Tries every possible major and minor key and returns the one
|
||||
whose scale contains the most of the given notes.
|
||||
|
||||
Example::
|
||||
|
||||
>>> Key.detect("C", "D", "E", "F", "G", "A", "B")
|
||||
<Key C major>
|
||||
>>> Key.detect("A", "B", "C", "D", "E", "F", "G")
|
||||
<Key C major>
|
||||
>>> Key.detect("A", "C", "E")
|
||||
<Key C major>
|
||||
|
||||
Returns:
|
||||
The best-matching Key, or None if no notes given.
|
||||
"""
|
||||
if not note_names:
|
||||
return None
|
||||
|
||||
notes = set(note_names)
|
||||
best_key = None
|
||||
best_score = (-1, 0)
|
||||
|
||||
chromatic = ["C", "C#", "D", "D#", "E", "F", "F#", "G", "G#", "A", "A#", "B"]
|
||||
for tonic in chromatic:
|
||||
for mode in ("major", "minor"):
|
||||
try:
|
||||
k = cls(tonic, mode)
|
||||
scale_notes = set(k.note_names)
|
||||
match = len(notes & scale_notes)
|
||||
# Tiebreak: prefer major over minor
|
||||
score = (match, 1 if mode == "major" else 0)
|
||||
if score > best_score:
|
||||
best_score = score
|
||||
best_key = k
|
||||
except (KeyError, ValueError):
|
||||
continue
|
||||
|
||||
return best_key
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return f"<Key {self.tonic_name} {self.mode}>"
|
||||
|
||||
def __str__(self) -> str:
|
||||
return f"{self.tonic_name} {self.mode}"
|
||||
|
||||
@property
|
||||
def scale(self) -> Scale:
|
||||
"""The scale for this key."""
|
||||
return self._scale
|
||||
|
||||
@property
|
||||
def note_names(self) -> list[str]:
|
||||
"""Note names in this key's scale."""
|
||||
return self._scale.note_names
|
||||
|
||||
@property
|
||||
def chords(self) -> list[str]:
|
||||
"""Names of all diatonic triads in this key."""
|
||||
return [c.identify() for c in self._scale.harmonize()]
|
||||
|
||||
@property
|
||||
def seventh_chords(self) -> list[str]:
|
||||
"""Names of all diatonic seventh chords in this key."""
|
||||
unique = len(self._scale.tones) - 1
|
||||
return [self._scale.seventh(i).identify() for i in range(unique)]
|
||||
|
||||
def triad(self, degree: int) -> Chord:
|
||||
"""Build a diatonic triad on the given degree (0-indexed)."""
|
||||
return self._scale.triad(degree)
|
||||
|
||||
def seventh(self, degree: int) -> Chord:
|
||||
"""Build a diatonic seventh chord on the given degree (0-indexed)."""
|
||||
return self._scale.seventh(degree)
|
||||
|
||||
def progression(self, *numerals: str) -> list[Chord]:
|
||||
"""Build a chord progression from Roman numerals.
|
||||
|
||||
Example::
|
||||
|
||||
>>> Key("G", "major").progression("I", "IV", "V7", "I")
|
||||
"""
|
||||
return self._scale.progression(*numerals)
|
||||
|
||||
def nashville(self, *numbers: Union[int, str]) -> list[Chord]:
|
||||
"""Build a chord progression using Nashville numbers.
|
||||
|
||||
Example::
|
||||
|
||||
>>> Key("G", "major").nashville(1, 4, 5, 1)
|
||||
"""
|
||||
return self._scale.nashville(*numbers)
|
||||
|
||||
def secondary_dominant(self, degree: int) -> Chord:
|
||||
"""Build a secondary dominant (V/x) for the given scale degree.
|
||||
|
||||
A secondary dominant is the dominant chord of a non-tonic
|
||||
degree. For example, in C major, V/V is D major (the V chord
|
||||
of G). Secondary dominants create momentary tonicizations
|
||||
that add color and forward motion.
|
||||
|
||||
Common secondary dominants:
|
||||
|
||||
- V/V (e.g. D7 in C major) — approaches the dominant
|
||||
- V/ii (e.g. A7 in C major) — approaches the supertonic
|
||||
- V/vi (e.g. E7 in C major) — approaches the relative minor
|
||||
|
||||
Args:
|
||||
degree: Scale degree to target (1-indexed). ``5`` means
|
||||
"build the V of the 5th degree."
|
||||
|
||||
Returns:
|
||||
A dominant 7th Chord that resolves to the given degree.
|
||||
|
||||
Example::
|
||||
|
||||
>>> Key("C", "major").secondary_dominant(5) # V/V = D7
|
||||
<Chord D dominant 7th>
|
||||
"""
|
||||
target = self._scale.tones[degree - 1]
|
||||
# Build a dominant 7th a perfect 5th above the target
|
||||
from .chords import Chord
|
||||
root = target.add(7)
|
||||
return Chord(tones=[root, root.add(4), root.add(7), root.add(10)])
|
||||
|
||||
def common_progressions(self) -> dict[str, list]:
|
||||
"""Named chord progressions realized in this key.
|
||||
|
||||
Returns a dict mapping progression names (from ``PROGRESSIONS``)
|
||||
to lists of Chord objects built in this key.
|
||||
|
||||
Example::
|
||||
|
||||
>>> key = Key("C", "major")
|
||||
>>> for name, chords in key.common_progressions().items():
|
||||
... symbols = [c.symbol or str(c) for c in chords]
|
||||
... print(f"{name}: {' → '.join(symbols)}")
|
||||
I-IV-V-I: C → F → G → C
|
||||
I-V-vi-IV: C → G → Am → F
|
||||
...
|
||||
"""
|
||||
result = {}
|
||||
for name, numerals in PROGRESSIONS.items():
|
||||
try:
|
||||
result[name] = self.progression(*numerals)
|
||||
except (KeyError, ValueError, IndexError):
|
||||
continue
|
||||
return result
|
||||
|
||||
@classmethod
|
||||
def all_keys(cls) -> list[Key]:
|
||||
"""Return all 24 major and minor keys.
|
||||
|
||||
Returns:
|
||||
A list of Key objects for all 12 major and 12 minor keys.
|
||||
|
||||
Example::
|
||||
|
||||
>>> for k in Key.all_keys():
|
||||
... print(k)
|
||||
"""
|
||||
chromatic = ["C", "C#", "D", "D#", "E", "F",
|
||||
"F#", "G", "G#", "A", "A#", "B"]
|
||||
keys = []
|
||||
for tonic in chromatic:
|
||||
keys.append(cls(tonic, "major"))
|
||||
keys.append(cls(tonic, "minor"))
|
||||
return keys
|
||||
|
||||
@property
|
||||
def signature(self) -> dict:
|
||||
"""The key signature — number and names of sharps or flats.
|
||||
|
||||
In Western music, each key has a unique key signature that tells
|
||||
you which notes are sharped or flatted throughout a piece.
|
||||
|
||||
Returns:
|
||||
A dict with:
|
||||
- ``sharps`` (int): number of sharps (0 if flat key)
|
||||
- ``flats`` (int): number of flats (0 if sharp key)
|
||||
- ``accidentals`` (list[str]): the sharped/flatted note names
|
||||
|
||||
Example::
|
||||
|
||||
>>> Key("G", "major").signature
|
||||
{'sharps': 1, 'flats': 0, 'accidentals': ['F#']}
|
||||
>>> Key("F", "major").signature
|
||||
{'sharps': 0, 'flats': 1, 'accidentals': ['Bb']}
|
||||
>>> Key("C", "major").signature
|
||||
{'sharps': 0, 'flats': 0, 'accidentals': []}
|
||||
"""
|
||||
# Compare scale notes against the natural notes C D E F G A B
|
||||
naturals = {"C", "D", "E", "F", "G", "A", "B"}
|
||||
scale_notes = set(self.note_names[:-1]) # exclude octave
|
||||
|
||||
sharps = [n for n in scale_notes if "#" in n]
|
||||
flats = [n for n in scale_notes if "b" in n[1:]] # skip first char for B
|
||||
|
||||
# Order sharps: F C G D A E B
|
||||
sharp_order = ["F#", "C#", "G#", "D#", "A#", "E#", "B#"]
|
||||
flat_order = ["Bb", "Eb", "Ab", "Db", "Gb", "Cb", "Fb"]
|
||||
|
||||
sharps_sorted = [s for s in sharp_order if s in sharps]
|
||||
flats_sorted = [f for f in flat_order if f in flats]
|
||||
|
||||
if sharps_sorted:
|
||||
return {"sharps": len(sharps_sorted), "flats": 0, "accidentals": sharps_sorted}
|
||||
elif flats_sorted:
|
||||
return {"sharps": 0, "flats": len(flats_sorted), "accidentals": flats_sorted}
|
||||
else:
|
||||
return {"sharps": 0, "flats": 0, "accidentals": []}
|
||||
|
||||
@property
|
||||
def borrowed_chords(self) -> list[str]:
|
||||
"""Chords borrowed from the parallel key.
|
||||
|
||||
Modal interchange (or modal mixture) borrows chords from the
|
||||
parallel major or minor key. In C major, the parallel minor
|
||||
is C minor, which provides chords like Ab major, Bb major,
|
||||
and Eb major — commonly heard in rock, film, and pop music.
|
||||
|
||||
Returns:
|
||||
A list of chord names from the parallel key that are NOT
|
||||
in the current key's diatonic chords.
|
||||
|
||||
Example::
|
||||
|
||||
>>> Key("C", "major").borrowed_chords
|
||||
['C minor', 'D diminished', 'D# major', ...]
|
||||
"""
|
||||
par = self.parallel
|
||||
if par is None:
|
||||
return []
|
||||
own = set(self.chords)
|
||||
return [c for c in par.chords if c not in own]
|
||||
|
||||
def random_progression(self, length: int = 4) -> list:
|
||||
"""Generate a random diatonic chord progression.
|
||||
|
||||
Uses weighted probabilities based on common chord function:
|
||||
I and vi are most common, IV and V are very common, ii is
|
||||
common, iii and viidim are rare. Always starts on I and
|
||||
ends on I or V.
|
||||
|
||||
Args:
|
||||
length: Number of chords (default 4).
|
||||
|
||||
Returns:
|
||||
A list of Chord objects.
|
||||
|
||||
Example::
|
||||
|
||||
>>> Key("C", "major").random_progression(4)
|
||||
[<Chord C major>, <Chord F major>, <Chord G major>, <Chord C major>]
|
||||
"""
|
||||
import random
|
||||
|
||||
harmonized = self._scale.harmonize()
|
||||
unique = len(harmonized)
|
||||
# Weights: I=high, ii=med, iii=low, IV=high, V=high, vi=med, vii=low
|
||||
weights = [10, 5, 2, 8, 8, 5, 1]
|
||||
if unique < len(weights):
|
||||
weights = weights[:unique]
|
||||
|
||||
chords = [harmonized[0]] # Start on I
|
||||
for _ in range(length - 2):
|
||||
chords.append(random.choices(harmonized, weights=weights, k=1)[0])
|
||||
if length > 1:
|
||||
# End on I or V
|
||||
chords.append(random.choice([harmonized[0], harmonized[4 % unique]]))
|
||||
return chords
|
||||
|
||||
# Common chord movement tendencies in major keys (0-indexed degrees).
|
||||
# Maps each degree to a list of likely next degrees, ordered by frequency.
|
||||
_TENDENCY = {
|
||||
0: [3, 4, 5, 1], # I → IV, V, vi, ii
|
||||
1: [4, 0, 6], # ii → V, I, vii
|
||||
2: [5, 3, 1], # iii → vi, IV, ii
|
||||
3: [4, 0, 1], # IV → V, I, ii
|
||||
4: [0, 5, 3], # V → I, vi, IV
|
||||
5: [1, 3, 4], # vi → ii, IV, V
|
||||
6: [0, 5], # vii° → I, vi
|
||||
}
|
||||
|
||||
def suggest_next(self, chord) -> list:
|
||||
"""Suggest likely next chords based on voice-leading tendencies.
|
||||
|
||||
Given a chord in this key, returns a ranked list of chords
|
||||
that commonly follow it, based on standard functional harmony
|
||||
rules (e.g. V → I, ii → V, IV → V).
|
||||
|
||||
Args:
|
||||
chord: A Chord object currently being played.
|
||||
|
||||
Returns:
|
||||
A list of Chord objects, most likely first.
|
||||
|
||||
Example::
|
||||
|
||||
>>> key = Key("C", "major")
|
||||
>>> g = key.triad(4) # G major (V)
|
||||
>>> [c.symbol for c in key.suggest_next(g)]
|
||||
['C', 'Am', 'F']
|
||||
"""
|
||||
harmonized = self._scale.harmonize()
|
||||
unique = len(harmonized)
|
||||
|
||||
# Find which degree this chord is
|
||||
chord_id = chord.identify()
|
||||
if not chord_id:
|
||||
return harmonized[:3]
|
||||
|
||||
degree = None
|
||||
for i, h in enumerate(harmonized):
|
||||
if h.identify() == chord_id:
|
||||
degree = i
|
||||
break
|
||||
|
||||
if degree is None:
|
||||
return harmonized[:3]
|
||||
|
||||
tendencies = self._TENDENCY.get(degree, [0])
|
||||
return [harmonized[d % unique] for d in tendencies]
|
||||
|
||||
@property
|
||||
def relative(self) -> Optional[Key]:
|
||||
"""The relative major or minor key.
|
||||
|
||||
If this is a major key, returns the relative minor (vi).
|
||||
If this is a minor key, returns the relative major (bIII).
|
||||
"""
|
||||
if self.mode == "major":
|
||||
# Relative minor starts on the 6th degree
|
||||
minor_tonic = self._scale.tones[5].name
|
||||
return Key(minor_tonic, "minor")
|
||||
elif self.mode in ("minor", "aeolian"):
|
||||
# Relative major starts on the 3rd degree
|
||||
major_tonic = self._scale.tones[2].name
|
||||
return Key(major_tonic, "major")
|
||||
return None
|
||||
|
||||
@property
|
||||
def parallel(self) -> Optional[Key]:
|
||||
"""The parallel major or minor key (same tonic, different mode)."""
|
||||
if self.mode == "major":
|
||||
return Key(self.tonic_name, "minor")
|
||||
elif self.mode in ("minor", "aeolian"):
|
||||
return Key(self.tonic_name, "major")
|
||||
return None
|
||||
|
||||
def modulation_path(self, target: Key) -> list:
|
||||
"""Suggest a chord-by-chord path from this key to a target key.
|
||||
|
||||
Strategy:
|
||||
- Find pivot chords (common to both keys)
|
||||
- Build: [I of current key, pivot chord, V of target key, I of target key]
|
||||
- If no pivot chord exists, use chromatic approach:
|
||||
[current I, target V, target I]
|
||||
|
||||
Args:
|
||||
target: The target Key to modulate to.
|
||||
|
||||
Returns:
|
||||
A list of Chord objects forming a modulation path.
|
||||
|
||||
Example::
|
||||
|
||||
>>> path = Key("C", "major").modulation_path(Key("G", "major"))
|
||||
>>> len(path)
|
||||
4
|
||||
"""
|
||||
from .chords import Chord
|
||||
|
||||
current_I = self.triad(0)
|
||||
target_V = target.triad(4)
|
||||
target_I = target.triad(0)
|
||||
|
||||
# Find pivot chords
|
||||
own_chords = self._scale.harmonize()
|
||||
target_chord_names = set(target.chords)
|
||||
|
||||
pivot = None
|
||||
for c in own_chords:
|
||||
cid = c.identify()
|
||||
if cid and cid in target_chord_names and cid != current_I.identify():
|
||||
pivot = c
|
||||
break
|
||||
|
||||
if pivot is not None:
|
||||
return [current_I, pivot, target_V, target_I]
|
||||
else:
|
||||
# Chromatic approach - no pivot chord
|
||||
return [current_I, target_V, target_I]
|
||||
|
||||
def pivot_chords(self, target: Key) -> list[str]:
|
||||
"""Find chords common to this key and a target key.
|
||||
|
||||
Pivot chords are the bridge for modulation — they belong to
|
||||
both keys, so a listener accepts them in either context. The
|
||||
more pivot chords two keys share, the smoother the modulation.
|
||||
|
||||
Closely related keys (e.g. C major → G major) share many
|
||||
pivot chords. Distant keys (e.g. C major → F# major) share
|
||||
few or none.
|
||||
|
||||
Args:
|
||||
target: The key to modulate to.
|
||||
|
||||
Returns:
|
||||
A list of chord name strings common to both keys.
|
||||
|
||||
Example::
|
||||
|
||||
>>> Key("C", "major").pivot_chords(Key("G", "major"))
|
||||
['G major', 'A minor', 'B minor', 'C major', 'D major', 'E minor']
|
||||
"""
|
||||
own = set(self.chords)
|
||||
other = set(target.chords)
|
||||
return sorted(own & other)
|
||||
|
||||
|
||||
class TonedScale:
|
||||
def __init__(self, *, system=SYSTEMS["western"], tonic):
|
||||
def __init__(self, *, system: Union[str, System] = SYSTEMS["western"], tonic: Union[str, Tone]) -> None:
|
||||
"""Initialize a TonedScale with a tonic note and tone system.
|
||||
|
||||
Args:
|
||||
system: A tone system name or :class:`System` instance.
|
||||
tonic: The tonic note as a string (e.g. ``"C4"``) or :class:`Tone`.
|
||||
"""
|
||||
if isinstance(system, str):
|
||||
system = SYSTEMS[system]
|
||||
self.system = system
|
||||
|
||||
if not isinstance(tonic, Tone):
|
||||
tonic = Tone.from_string(tonic, system=self.system)
|
||||
|
||||
self.tonic = tonic
|
||||
self._cached_scales: Optional[dict[str, Scale]] = None
|
||||
|
||||
def __repr__(self):
|
||||
def __repr__(self) -> str:
|
||||
return f"<TonedScale system={self.system!r} tonic={self.tonic}>"
|
||||
|
||||
def __getitem__(self, scale):
|
||||
def __getitem__(self, scale: str) -> Scale:
|
||||
"""Retrieve a scale by name.
|
||||
|
||||
Raises:
|
||||
KeyError: If the named scale is not found in this system.
|
||||
"""
|
||||
result = self.get(scale)
|
||||
if result is None:
|
||||
raise KeyError(scale)
|
||||
return result
|
||||
|
||||
def get(self, scale):
|
||||
def get(self, scale: str) -> Optional[Scale]:
|
||||
"""Look up a scale by name, returning ``None`` if not found."""
|
||||
try:
|
||||
return self._scales[scale]
|
||||
except KeyError:
|
||||
pass
|
||||
return None
|
||||
|
||||
@property
|
||||
def scales(self):
|
||||
def scales(self) -> tuple[str, ...]:
|
||||
"""Tuple of all available scale names in this system."""
|
||||
return tuple(self._scales.keys())
|
||||
|
||||
@staticmethod
|
||||
def _should_prefer_flats(tones: list) -> bool:
|
||||
"""Determine if a scale should use flat spellings.
|
||||
|
||||
Uses the "no duplicate letters" rule: build the scale with sharps
|
||||
first, and if any letter name appears twice (excluding the octave
|
||||
repeat at the end), try flats instead. This correctly handles all
|
||||
keys on the circle of fifths.
|
||||
"""
|
||||
# Exclude the last tone (octave repeat of the tonic)
|
||||
unique_tones = tones[:-1] if len(tones) > 1 else tones
|
||||
letters = [t.name[0] for t in unique_tones]
|
||||
return len(letters) != len(set(letters))
|
||||
|
||||
@property
|
||||
def _scales(self):
|
||||
def _scales(self) -> dict[str, Scale]:
|
||||
"""Lazily computed (and cached) mapping of scale names to Scale objects."""
|
||||
if self._cached_scales is not None:
|
||||
return self._cached_scales
|
||||
|
||||
# Also check if tonic itself is a flat (always prefer flats then)
|
||||
tonic_is_flat = "b" in self.tonic.name and self.tonic.name != "B"
|
||||
|
||||
scales = {}
|
||||
|
||||
for scale_type in self.system.scales:
|
||||
for scale in self.system.scales[scale_type]:
|
||||
|
||||
working_scale = []
|
||||
reference_scale = self.system.scales[scale_type][scale]["intervals"]
|
||||
|
||||
# First pass: build with sharps (default)
|
||||
working_scale = [self.tonic]
|
||||
current_tone = self.tonic
|
||||
working_scale.append(current_tone)
|
||||
|
||||
for interval in reference_scale:
|
||||
current_tone = current_tone.add(interval)
|
||||
working_scale.append(current_tone)
|
||||
|
||||
# Check if we need flats (duplicate letter names)
|
||||
if tonic_is_flat or self._should_prefer_flats(working_scale):
|
||||
working_scale = [self.tonic]
|
||||
current_tone = self.tonic
|
||||
for interval in reference_scale:
|
||||
current_tone = current_tone.add(interval, prefer_flats=True)
|
||||
working_scale.append(current_tone)
|
||||
|
||||
scales[scale] = Scale(tones=tuple(working_scale))
|
||||
|
||||
self._cached_scales = scales
|
||||
return scales
|
||||
|
||||
+275
-6
@@ -1,15 +1,59 @@
|
||||
from ._statics import TEMPERAMENTS, TONES, DEGREES, SCALES, SYSTEMS
|
||||
from ._statics import (
|
||||
TEMPERAMENTS, TONES, DEGREES, SCALES,
|
||||
INDIAN_SCALES, ARABIC_SCALES, JAPANESE_SCALES,
|
||||
BLUES_SCALES, GAMELAN_SCALES, SYSTEMS,
|
||||
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,
|
||||
TONES_TURKISH, DEGREES_TURKISH, TURKISH_SCALES,
|
||||
TONES_CARNATIC, DEGREES_CARNATIC, CARNATIC_SCALES,
|
||||
)
|
||||
|
||||
|
||||
class System:
|
||||
def __init__(self, *, tone_names, degrees, scales=None):
|
||||
def __init__(self, *, tone_names, degrees, scales=None, c_index=None,
|
||||
period=2.0, ratios=None):
|
||||
self.tone_names = tone_names
|
||||
|
||||
self.degrees = degrees
|
||||
self._scales = scales
|
||||
|
||||
# Period: the frequency ratio of one "octave" in this system.
|
||||
# 2.0 for standard octave-based systems.
|
||||
# 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).
|
||||
# For non-12-TET systems, this is the index of the tone nearest C,
|
||||
# or 0 if no C equivalent exists.
|
||||
if c_index is not None:
|
||||
self.c_index = c_index
|
||||
else:
|
||||
# Try to find C in the tone names, fall back to 0
|
||||
self.c_index = 0
|
||||
for i, names in enumerate(tone_names):
|
||||
if "C" in names:
|
||||
self.c_index = i
|
||||
break
|
||||
|
||||
if scales is None:
|
||||
self._scales = SCALES[self.semitones]
|
||||
n = self.semitones
|
||||
if n in SCALES:
|
||||
self._scales = SCALES[n]
|
||||
else:
|
||||
# Generate chromatic scale for unknown sizes
|
||||
self._scales = {
|
||||
"chromatic": (n, {}),
|
||||
}
|
||||
|
||||
@property
|
||||
def semitones(self):
|
||||
@@ -20,6 +64,59 @@ class System:
|
||||
from . import Tone
|
||||
return tuple([Tone.from_tuple(tone) for tone in self.tone_names])
|
||||
|
||||
def resolve_name(self, name: str) -> str | None:
|
||||
"""Resolve a note name (including flats, double sharps/flats) to the canonical name.
|
||||
|
||||
Handles enharmonic equivalents:
|
||||
- Standard names and their alternates (e.g. Bb, C#)
|
||||
- Double sharps (C## = D, F## = G)
|
||||
- Double flats (Dbb = C, Ebb = D)
|
||||
|
||||
Returns the primary name if found, or None if not recognized.
|
||||
"""
|
||||
# Direct lookup first
|
||||
for names in self.tone_names:
|
||||
if name in names:
|
||||
return names[0]
|
||||
|
||||
# Handle double sharps (e.g. C## → D, F## → G)
|
||||
if name.endswith('##') and len(name) >= 3:
|
||||
base = name[:-2]
|
||||
base_idx = self._name_to_index(base)
|
||||
if base_idx is not None:
|
||||
resolved_idx = (base_idx + 2) % len(self.tone_names)
|
||||
return self.tone_names[resolved_idx][0]
|
||||
|
||||
# Handle double flats (e.g. Dbb → C, Ebb → D)
|
||||
if name.endswith('bb') and len(name) >= 3 and name[0] != 'b':
|
||||
base = name[:-2]
|
||||
base_idx = self._name_to_index(base)
|
||||
if base_idx is not None:
|
||||
resolved_idx = (base_idx - 2) % len(self.tone_names)
|
||||
return self.tone_names[resolved_idx][0]
|
||||
|
||||
# Handle single sharps/flats on natural notes (e.g. Cb → B, E# → F)
|
||||
if len(name) == 2:
|
||||
base = name[0]
|
||||
modifier = name[1]
|
||||
base_idx = self._name_to_index(base)
|
||||
if base_idx is not None:
|
||||
if modifier == '#':
|
||||
resolved_idx = (base_idx + 1) % len(self.tone_names)
|
||||
return self.tone_names[resolved_idx][0]
|
||||
elif modifier == 'b':
|
||||
resolved_idx = (base_idx - 1) % len(self.tone_names)
|
||||
return self.tone_names[resolved_idx][0]
|
||||
|
||||
return None
|
||||
|
||||
def _name_to_index(self, name: str) -> int | None:
|
||||
"""Return the index of a tone name, or None if not found."""
|
||||
for i, names in enumerate(self.tone_names):
|
||||
if name in names:
|
||||
return i
|
||||
return None
|
||||
|
||||
|
||||
@property
|
||||
def scales(self):
|
||||
@@ -55,6 +152,7 @@ class System:
|
||||
*,
|
||||
tones=7,
|
||||
semitones=12,
|
||||
intervals=None,
|
||||
major=False,
|
||||
minor=False,
|
||||
hemitonic=False, # Contains semitones.
|
||||
@@ -63,7 +161,13 @@ class System:
|
||||
offset=None,
|
||||
):
|
||||
"""Generates the primary scale for a given number of semitones/tones."""
|
||||
# TODO: Support minor, support harmonic, support melodic.
|
||||
|
||||
# Direct interval pattern — bypass generation logic.
|
||||
if intervals is not None:
|
||||
scale = list(intervals)
|
||||
if offset:
|
||||
scale = scale[offset:] + scale[:offset]
|
||||
return {"intervals": scale, "hemitonic": 1 in scale, "meta": {}}
|
||||
|
||||
# Sanity check.
|
||||
if major and minor:
|
||||
@@ -94,7 +198,6 @@ class System:
|
||||
yield step
|
||||
else:
|
||||
for i in range(tones):
|
||||
# TODO: figure out how to make this work with monotonic.
|
||||
yield 1
|
||||
|
||||
scale = [
|
||||
@@ -116,7 +219,173 @@ 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}>"
|
||||
|
||||
SYSTEMS = {"western": System(tone_names=TONES["western"], degrees=DEGREES["western"])}
|
||||
|
||||
def TET(n, *, names=None, reference_index=0, period=2.0):
|
||||
"""Create an N-tone equal temperament system.
|
||||
|
||||
Each step divides the period into *n* equal parts. The frequency
|
||||
ratio between adjacent tones is ``period^(1/n)``.
|
||||
|
||||
For standard tunings the period is 2.0 (octave). For exotic systems
|
||||
like Bohlen-Pierce, set ``period=3.0`` (tritave).
|
||||
|
||||
Args:
|
||||
n: Number of equal divisions of the octave (e.g. 19, 24, 31, 53).
|
||||
names: Optional list of *n* tone name strings. If omitted,
|
||||
tones are numbered ``"0"`` through ``"n-1"``.
|
||||
reference_index: Index of the tone that corresponds to A440
|
||||
(default 0, meaning tone "0" = A4 = 440 Hz).
|
||||
|
||||
Returns:
|
||||
A :class:`System` instance.
|
||||
|
||||
Example::
|
||||
|
||||
>>> edo19 = TET(19)
|
||||
>>> from pytheory import Tone
|
||||
>>> t = Tone("0", octave=4, system=edo19)
|
||||
>>> t.frequency # 440.0 Hz (tone 0 = A4)
|
||||
440.0
|
||||
|
||||
>>> edo31 = TET(31)
|
||||
>>> t = Tone("18", octave=4, system=edo31)
|
||||
>>> t.frequency # 18 steps above A in 31-TET
|
||||
"""
|
||||
if names is not None:
|
||||
if len(names) != n:
|
||||
raise ValueError(f"Expected {n} names, got {len(names)}")
|
||||
tone_names = [(name,) for name in names]
|
||||
else:
|
||||
tone_names = [(str(i),) for i in range(n)]
|
||||
|
||||
# Degrees: numbered, with no modal names
|
||||
degrees = [(f"degree {i+1}", ()) for i in range(n)]
|
||||
|
||||
# Scales: chromatic (all steps = 1) plus MOS scales for common EDOs
|
||||
scale_data = {
|
||||
"chromatic": (n, {}),
|
||||
}
|
||||
|
||||
# Add well-known scales for specific EDOs
|
||||
if n == 19:
|
||||
# 19-TET: major and minor have different step sizes
|
||||
# Major: 3 3 2 3 3 3 2 (sums to 19)
|
||||
# Minor: 3 2 3 3 2 3 3
|
||||
scale_data["heptatonic"] = [7, {
|
||||
"major": {"intervals": (3, 3, 2, 3, 3, 3, 2)},
|
||||
"minor": {"intervals": (3, 2, 3, 3, 2, 3, 3)},
|
||||
"harmonic minor": {"intervals": (3, 2, 3, 3, 2, 4, 2)},
|
||||
}]
|
||||
scale_data["pentatonic"] = [5, {
|
||||
"major pentatonic": {"intervals": (3, 3, 5, 3, 5)},
|
||||
"minor pentatonic": {"intervals": (5, 3, 3, 5, 3)},
|
||||
}]
|
||||
elif n == 24:
|
||||
# 24-TET (quarter-tone): standard 12-TET scales with doubled steps
|
||||
scale_data["heptatonic"] = [7, {
|
||||
"major": {"intervals": (4, 4, 2, 4, 4, 4, 2)},
|
||||
"minor": {"intervals": (4, 2, 4, 4, 2, 4, 4)},
|
||||
}]
|
||||
elif n == 31:
|
||||
# 31-TET: excellent approximation of quarter-comma meantone
|
||||
# Major: 5 5 3 5 5 5 3 (sums to 31)
|
||||
# Minor: 5 3 5 5 3 5 5
|
||||
scale_data["heptatonic"] = [7, {
|
||||
"major": {"intervals": (5, 5, 3, 5, 5, 5, 3)},
|
||||
"minor": {"intervals": (5, 3, 5, 5, 3, 5, 5)},
|
||||
"harmonic minor": {"intervals": (5, 3, 5, 5, 3, 7, 3)},
|
||||
}]
|
||||
scale_data["pentatonic"] = [5, {
|
||||
"major pentatonic": {"intervals": (5, 5, 8, 5, 8)},
|
||||
"minor pentatonic": {"intervals": (8, 5, 5, 8, 5)},
|
||||
}]
|
||||
elif n == 53:
|
||||
# 53-TET: nearly perfect fifths and thirds
|
||||
# Major: 9 9 4 9 9 9 4 (sums to 53)
|
||||
scale_data["heptatonic"] = [7, {
|
||||
"major": {"intervals": (9, 9, 4, 9, 9, 9, 4)},
|
||||
"minor": {"intervals": (9, 4, 9, 9, 4, 9, 9)},
|
||||
}]
|
||||
|
||||
# Find C equivalent for c_index (reference_index is A, C is 3 steps in 12-TET)
|
||||
# Proportionally: C is 3/12 of the way around from A
|
||||
c_idx = round(n * 3 / 12) if n != 12 else 3
|
||||
|
||||
return System(
|
||||
tone_names=tone_names,
|
||||
degrees=degrees,
|
||||
scales=scale_data,
|
||||
c_index=c_idx,
|
||||
period=period,
|
||||
)
|
||||
|
||||
|
||||
# ── 19-TET named system ──
|
||||
# Traditional note names for 19-TET: all 12 western notes plus
|
||||
# 7 quarter-tone positions (enharmonic splits)
|
||||
_19TET_NAMES = [
|
||||
"A", "A#", "Bb", "B", "B#",
|
||||
"C", "C#", "Db", "D", "D#",
|
||||
"Eb", "E", "E#", "F", "F#",
|
||||
"Gb", "G", "G#", "Ab",
|
||||
]
|
||||
|
||||
# ── 31-TET named system ──
|
||||
# Adriaan Fokker's naming: sharps and flats are distinct pitches
|
||||
_31TET_NAMES = [
|
||||
"A", "A↑", "A#", "Bb", "B↓",
|
||||
"B", "B↑", "C", "C↑", "C#",
|
||||
"Db", "D↓", "D", "D↑", "D#",
|
||||
"Eb", "E↓", "E", "E↑", "E#",
|
||||
"F", "F↑", "F#", "Gb", "G↓",
|
||||
"G", "G↑", "G#", "Ab", "A↓",
|
||||
"A♮", # enharmonic return (distinct from "A" by a diesis)
|
||||
]
|
||||
|
||||
|
||||
SYSTEMS = {
|
||||
"western": System(tone_names=TONES["western"], degrees=DEGREES["western"]),
|
||||
"indian": System(tone_names=TONES["indian"], degrees=DEGREES["indian"], scales=INDIAN_SCALES[12], c_index=3),
|
||||
"arabic": System(tone_names=TONES["arabic"], degrees=DEGREES["arabic"], scales=ARABIC_SCALES[12], c_index=3),
|
||||
"japanese": System(tone_names=TONES["japanese"], degrees=DEGREES["japanese"], scales=JAPANESE_SCALES[12]),
|
||||
"blues": System(tone_names=TONES["blues"], degrees=DEGREES["blues"], scales=BLUES_SCALES[12]),
|
||||
"gamelan": System(tone_names=TONES["gamelan"], degrees=DEGREES["gamelan"], scales=GAMELAN_SCALES[12], c_index=3),
|
||||
"19-tet": TET(19, names=_19TET_NAMES),
|
||||
"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, ratios=SHRUTI_RATIOS),
|
||||
"maqam": System(tone_names=TONES_ARABIC_24, degrees=DEGREES_ARABIC_24,
|
||||
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,
|
||||
scales=PELOG_SCALES, c_index=2),
|
||||
"thai": System(tone_names=TONES_THAI, degrees=DEGREES_THAI,
|
||||
scales=THAI_SCALES, c_index=0),
|
||||
"makam": System(tone_names=TONES_TURKISH, degrees=DEGREES_TURKISH,
|
||||
scales=TURKISH_SCALES, c_index=13),
|
||||
"carnatic": System(tone_names=TONES_CARNATIC, degrees=DEGREES_CARNATIC,
|
||||
scales=CARNATIC_SCALES, c_index=18), # Sa ≈ C, 18 steps from A
|
||||
# Bohlen-Pierce: 13 equal divisions of the tritave (3:1).
|
||||
# Genuinely alien — no octaves, no fifths, built on 3:5:7 harmonics.
|
||||
# Used by composers like Heinz Bohlen, Kees van Prooijen, Georg Hajdu.
|
||||
"bohlen-pierce": TET(13, period=3.0, names=[
|
||||
"A", "B", "C", "D", "E", "F", "G",
|
||||
"H", "J", "K", "L", "M", "N",
|
||||
]),
|
||||
}
|
||||
|
||||
+685
-77
@@ -1,26 +1,127 @@
|
||||
from ._statics import REFERENCE_A, TEMPERAMENTS
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Optional, Union
|
||||
|
||||
from ._statics import REFERENCE_A, TEMPERAMENTS, C_INDEX
|
||||
|
||||
|
||||
class Interval:
|
||||
"""Named constants for common musical intervals (in semitones)."""
|
||||
UNISON = 0
|
||||
MINOR_SECOND = 1
|
||||
MAJOR_SECOND = 2
|
||||
MINOR_THIRD = 3
|
||||
MAJOR_THIRD = 4
|
||||
PERFECT_FOURTH = 5
|
||||
TRITONE = 6
|
||||
PERFECT_FIFTH = 7
|
||||
MINOR_SIXTH = 8
|
||||
MAJOR_SIXTH = 9
|
||||
MINOR_SEVENTH = 10
|
||||
MAJOR_SEVENTH = 11
|
||||
OCTAVE = 12
|
||||
|
||||
|
||||
class Tone:
|
||||
|
||||
def __init__(self, name, *, alt_names=None, octave=None, system="western"):
|
||||
def __init__(
|
||||
self,
|
||||
name,
|
||||
*,
|
||||
alt_names: Optional[list[str]] = None,
|
||||
octave: Optional[int] = None,
|
||||
system: Union[str, object] = "western",
|
||||
_validate: bool = True,
|
||||
) -> None:
|
||||
"""Initialize a Tone with a name, optional octave, and musical system.
|
||||
|
||||
Args:
|
||||
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"``)
|
||||
or a ``ToneSystem`` instance.
|
||||
"""
|
||||
if alt_names is None:
|
||||
alt_names = []
|
||||
|
||||
if isinstance(name, str):
|
||||
try:
|
||||
parsed_octave = int("".join([c for c in filter(str.isdigit, name)]))
|
||||
except ValueError:
|
||||
parsed_octave = None
|
||||
|
||||
if parsed_octave is not None:
|
||||
name = name.replace(str(parsed_octave), "")
|
||||
# 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 = parsed_octave
|
||||
octave = 4 + extra_octaves
|
||||
else:
|
||||
octave += extra_octaves
|
||||
name = str(name)
|
||||
|
||||
if isinstance(name, str):
|
||||
# Normalize unicode music symbols to ASCII equivalents
|
||||
name = (name
|
||||
.replace('\u266f', '#') # ♯ → #
|
||||
.replace('\u266d', 'b') # ♭ → b
|
||||
.replace('\U0001d12a', '##') # 𝄪 → ##
|
||||
.replace('\U0001d12b', 'bb') # 𝄫 → bb
|
||||
)
|
||||
# Normalize 'x' / 'X' as double sharp (only after letter name)
|
||||
if len(name) >= 2 and name[1] in ('x', 'X') and name[0].isalpha():
|
||||
name = name[0] + '##' + name[2:]
|
||||
|
||||
# Only parse trailing digits as octave (e.g. "C4" → "C", octave=4).
|
||||
# Digits embedded in the name (e.g. "Mib+1") are NOT octaves.
|
||||
# Numeric pitch class names ("0", "11") are also left alone.
|
||||
if name and name[0].isalpha():
|
||||
import re as _re
|
||||
m = _re.search(r'(\d+)$', name)
|
||||
if m:
|
||||
parsed_octave = int(m.group(1))
|
||||
name = name[:m.start()]
|
||||
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
|
||||
self._frequency: Optional[float] = None
|
||||
|
||||
if isinstance(system, str):
|
||||
self.system_name = system
|
||||
@@ -29,12 +130,24 @@ class Tone:
|
||||
self.system_name = None
|
||||
self._system = system
|
||||
|
||||
@property
|
||||
def exists(self):
|
||||
return self.name in self.system.tones
|
||||
# Validate tone name against the system early (fixes #39).
|
||||
if _validate and self.system.resolve_name(name) is None:
|
||||
raise ValueError(
|
||||
f"Unknown tone name: {name!r}. "
|
||||
f"Not found in the {system!r} system."
|
||||
)
|
||||
|
||||
@property
|
||||
def system(self):
|
||||
def exists(self) -> bool:
|
||||
"""True if this tone's name is found in the associated system."""
|
||||
return self.system.resolve_name(self.name) is not None
|
||||
|
||||
@property
|
||||
def system(self) -> object:
|
||||
"""The ``ToneSystem`` associated with this tone.
|
||||
|
||||
Lazily resolved from ``system_name`` on first access and cached.
|
||||
"""
|
||||
from .systems import SYSTEMS
|
||||
|
||||
if self._system:
|
||||
@@ -45,61 +158,224 @@ class Tone:
|
||||
return self.system
|
||||
|
||||
@property
|
||||
def full_name(self):
|
||||
if self.octave:
|
||||
def full_name(self) -> str:
|
||||
"""The tone name with octave appended, e.g. ``'C4'`` or ``'C'``."""
|
||||
if self.octave is not None:
|
||||
return f"{self.name}{self.octave}"
|
||||
else:
|
||||
return self.name
|
||||
|
||||
def names(self):
|
||||
def names(self) -> list[str]:
|
||||
"""Return a list containing the primary name and all alternate names."""
|
||||
return [self.name] + self.alt_names
|
||||
|
||||
def __repr__(self):
|
||||
return f"<Tone {self.full_name}>"
|
||||
@property
|
||||
def scientific(self) -> str:
|
||||
"""Scientific pitch notation (e.g. ``'C4'``, ``'A#3'``).
|
||||
|
||||
def __str__(self):
|
||||
This is the default notation used throughout PyTheory —
|
||||
note name followed by octave number. Middle C is C4.
|
||||
Same as ``full_name``.
|
||||
"""
|
||||
return self.full_name
|
||||
|
||||
def __add__(self, interval):
|
||||
@property
|
||||
def helmholtz(self) -> str:
|
||||
"""Helmholtz pitch notation.
|
||||
|
||||
The older European convention still used in some contexts:
|
||||
|
||||
- C2 → ``CC`` (sub-contra)
|
||||
- C3 → ``C`` (great octave)
|
||||
- C4 → ``c`` (small octave / middle C)
|
||||
- C5 → ``c'`` (one-line)
|
||||
- C6 → ``c''`` (two-line)
|
||||
- C7 → ``c'''``
|
||||
|
||||
Accidentals are preserved as-is (e.g. ``c#'``).
|
||||
|
||||
Example::
|
||||
|
||||
>>> Tone.from_string("C4").helmholtz
|
||||
'c'
|
||||
>>> Tone.from_string("C3").helmholtz
|
||||
'C'
|
||||
>>> Tone.from_string("C5").helmholtz
|
||||
"c'"
|
||||
>>> Tone.from_string("A2").helmholtz
|
||||
'AA'
|
||||
"""
|
||||
if self.octave is None:
|
||||
return self.name
|
||||
letter = self.name[0]
|
||||
accidental = self.name[1:]
|
||||
if self.octave <= 2:
|
||||
# Contra and sub-contra: uppercase repeated
|
||||
# Octave 2 = contra (CC), 1 = sub-contra (CCC), 0 = (CCCC)
|
||||
repeats = 4 - self.octave
|
||||
return (letter.upper() * repeats) + accidental
|
||||
elif self.octave == 3:
|
||||
# Great octave: single uppercase
|
||||
return letter.upper() + accidental
|
||||
else:
|
||||
# Octave 4+: lowercase with tick marks
|
||||
ticks = self.octave - 4
|
||||
tick_str = "'" * ticks if ticks > 0 else ""
|
||||
return letter.lower() + accidental + tick_str
|
||||
|
||||
@property
|
||||
def is_natural(self) -> bool:
|
||||
"""True if this is a natural note (no sharp or flat)."""
|
||||
return not self.is_sharp and not self.is_flat
|
||||
|
||||
@property
|
||||
def is_sharp(self) -> bool:
|
||||
"""True if this tone has a sharp (#)."""
|
||||
return "#" in self.name
|
||||
|
||||
@property
|
||||
def is_flat(self) -> bool:
|
||||
"""True if this tone has a flat (b after the first character)."""
|
||||
return "b" in self.name[1:]
|
||||
|
||||
@property
|
||||
def letter(self) -> str:
|
||||
"""The letter name without any accidental.
|
||||
|
||||
Example::
|
||||
|
||||
>>> Tone.from_string("C#4").letter
|
||||
'C'
|
||||
>>> Tone.from_string("Bb4").letter
|
||||
'B'
|
||||
>>> Tone.from_string("G4").letter
|
||||
'G'
|
||||
"""
|
||||
return self.name[0]
|
||||
|
||||
@property
|
||||
def enharmonic(self) -> Optional[str]:
|
||||
"""The enharmonic equivalent of this tone, or None if there isn't one.
|
||||
|
||||
Returns the alternate spelling: C# → Db, Db → C#, etc.
|
||||
Natural notes (C, D, E, F, G, A, B) have no enharmonic.
|
||||
|
||||
Example::
|
||||
|
||||
>>> Tone.from_string("C#4").enharmonic
|
||||
'Db'
|
||||
"""
|
||||
if self.alt_names:
|
||||
return self.alt_names[0] if isinstance(self.alt_names, (list, tuple)) else self.alt_names
|
||||
# Check the system for alt names
|
||||
try:
|
||||
for tone in self.system.tones:
|
||||
if tone.name == self.name and tone.alt_names:
|
||||
return tone.alt_names[0]
|
||||
except (AttributeError, TypeError):
|
||||
pass
|
||||
return None
|
||||
|
||||
_SOLFEGE_MAP = {
|
||||
"C": "Do", "D": "Re", "E": "Mi", "F": "Fa",
|
||||
"G": "Sol", "A": "La", "B": "Ti",
|
||||
}
|
||||
|
||||
_SOLFEGE_SHARP_MAP = {
|
||||
"C#": "Di", "D#": "Ri", "F#": "Fi", "G#": "Si", "A#": "Li",
|
||||
"E#": "Mi", "B#": "Do",
|
||||
}
|
||||
|
||||
_SOLFEGE_FLAT_MAP = {
|
||||
"Db": "Ra", "Eb": "Me", "Gb": "Se", "Ab": "Le", "Bb": "Te",
|
||||
"Fb": "Mi", "Cb": "Ti",
|
||||
}
|
||||
|
||||
@property
|
||||
def solfege(self) -> str:
|
||||
"""Map Western note names to fixed-Do solfege syllables.
|
||||
|
||||
Uses fixed Do system where C is always Do regardless of key.
|
||||
|
||||
- C->Do, D->Re, E->Mi, F->Fa, G->Sol, A->La, B->Ti
|
||||
- Sharps: C#->Di, D#->Ri, F#->Fi, G#->Si, A#->Li
|
||||
- Flats: Db->Ra, Eb->Me, Gb->Se, Ab->Le, Bb->Te
|
||||
|
||||
Returns the note name unchanged if the system isn't western
|
||||
or the name isn't recognized.
|
||||
|
||||
Example::
|
||||
|
||||
>>> Tone.from_string("C4").solfege
|
||||
'Do'
|
||||
>>> Tone.from_string("F#4").solfege
|
||||
'Fi'
|
||||
"""
|
||||
# Check system
|
||||
sys_name = self.system_name
|
||||
if sys_name is not None and sys_name != "western":
|
||||
return self.name
|
||||
if self._system is not None:
|
||||
try:
|
||||
if hasattr(self._system, 'name') and self._system.name != "western":
|
||||
return self.name
|
||||
except (AttributeError, TypeError):
|
||||
pass
|
||||
|
||||
name = self.name
|
||||
if name in self._SOLFEGE_MAP:
|
||||
return self._SOLFEGE_MAP[name]
|
||||
if name in self._SOLFEGE_SHARP_MAP:
|
||||
return self._SOLFEGE_SHARP_MAP[name]
|
||||
if name in self._SOLFEGE_FLAT_MAP:
|
||||
return self._SOLFEGE_FLAT_MAP[name]
|
||||
return name
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return f"<Tone {self.full_name}>"
|
||||
|
||||
def __str__(self) -> str:
|
||||
return self.full_name
|
||||
|
||||
def __add__(self, interval: int) -> Tone:
|
||||
return self.add(interval)
|
||||
|
||||
def __sub__(self, other):
|
||||
def __sub__(self, other: Union[int, Tone]) -> Union[Tone, int]:
|
||||
# Tone - int: subtract semitones
|
||||
if isinstance(other, int):
|
||||
return self.subtract(other)
|
||||
# Tone - Tone: semitone distance
|
||||
if isinstance(other, Tone):
|
||||
c_index = 3
|
||||
try:
|
||||
mod = len(self.system.tones)
|
||||
except AttributeError:
|
||||
raise ValueError("Tone math can only be computed with an associated system!")
|
||||
self_from_c0 = ((self._index - c_index) % mod) + ((self.octave or 0) * mod)
|
||||
other_from_c0 = ((other._index - c_index) % mod) + ((other.octave or 0) * mod)
|
||||
self_from_c0 = ((self._index - C_INDEX) % mod) + ((self.octave or 0) * mod)
|
||||
other_from_c0 = ((other._index - C_INDEX) % mod) + ((other.octave or 0) * mod)
|
||||
return self_from_c0 - other_from_c0
|
||||
return NotImplemented
|
||||
|
||||
def __lt__(self, other):
|
||||
def __lt__(self, other: Tone) -> bool:
|
||||
if not isinstance(other, Tone):
|
||||
return NotImplemented
|
||||
return self.pitch() < other.pitch()
|
||||
|
||||
def __le__(self, other):
|
||||
def __le__(self, other: Tone) -> bool:
|
||||
if not isinstance(other, Tone):
|
||||
return NotImplemented
|
||||
return self.pitch() <= other.pitch()
|
||||
|
||||
def __gt__(self, other):
|
||||
def __gt__(self, other: Tone) -> bool:
|
||||
if not isinstance(other, Tone):
|
||||
return NotImplemented
|
||||
return self.pitch() > other.pitch()
|
||||
|
||||
def __ge__(self, other):
|
||||
def __ge__(self, other: Tone) -> bool:
|
||||
if not isinstance(other, Tone):
|
||||
return NotImplemented
|
||||
return self.pitch() >= other.pitch()
|
||||
|
||||
def __eq__(self, other):
|
||||
def __eq__(self, other: object) -> bool:
|
||||
|
||||
# Comparing string literals.
|
||||
if isinstance(other, str):
|
||||
@@ -114,25 +390,46 @@ class Tone:
|
||||
|
||||
return False
|
||||
|
||||
def __hash__(self):
|
||||
def __hash__(self) -> int:
|
||||
return hash((self.name, self.octave))
|
||||
|
||||
@classmethod
|
||||
def from_string(klass, s, system=None):
|
||||
try:
|
||||
octave = int("".join([c for c in filter(str.isdigit, s)]))
|
||||
except ValueError:
|
||||
octave = None
|
||||
def from_string(klass, s: str, system: Optional[Union[str, object]] = None) -> Tone:
|
||||
"""Create a Tone by parsing a string like ``'C#4'`` or ``'Bb'``.
|
||||
|
||||
tone = s.replace(str(octave), "") if octave else s
|
||||
Args:
|
||||
s: A note string, optionally including an octave number.
|
||||
system: The tuning system to associate with the tone.
|
||||
|
||||
Returns:
|
||||
A new ``Tone`` instance.
|
||||
"""
|
||||
import re as _re
|
||||
octave = None
|
||||
tone = s
|
||||
# Only parse trailing digits as octave
|
||||
if s and s[0].isalpha():
|
||||
m = _re.search(r'(\d+)$', s)
|
||||
if m:
|
||||
octave = int(m.group(1))
|
||||
tone = s[:m.start()]
|
||||
|
||||
if system:
|
||||
return klass(name=tone, octave=octave, system=system)
|
||||
else:
|
||||
return klass(name=tone, octave=octave)
|
||||
return klass(name=tone, octave=octave, _validate=False)
|
||||
|
||||
@classmethod
|
||||
def from_tuple(klass, t):
|
||||
def from_tuple(klass, t: tuple[str, ...]) -> Tone:
|
||||
"""Create a Tone from a tuple of ``(name, *alt_names)``.
|
||||
|
||||
Args:
|
||||
t: A tuple where the first element is the primary name and
|
||||
any remaining elements are alternate names (enharmonics).
|
||||
|
||||
Returns:
|
||||
A new ``Tone`` instance.
|
||||
"""
|
||||
if len(t) == 1:
|
||||
return klass.from_string(s=t[0])
|
||||
else:
|
||||
@@ -141,18 +438,130 @@ class Tone:
|
||||
return tone
|
||||
|
||||
@classmethod
|
||||
def from_index(klass, i, *, octave, system):
|
||||
tone = system.tones[i].name
|
||||
return klass(name=tone, octave=octave, system=system)
|
||||
def from_frequency(klass, hz: float, system: Union[str, object] = "western") -> Tone:
|
||||
"""Create a Tone from a frequency in Hz.
|
||||
|
||||
Finds the nearest note in 12-TET tuning (A4=440Hz).
|
||||
|
||||
Example::
|
||||
|
||||
>>> Tone.from_frequency(440)
|
||||
<Tone A4>
|
||||
>>> Tone.from_frequency(261.63)
|
||||
<Tone C4>
|
||||
"""
|
||||
import math
|
||||
if hz <= 0:
|
||||
raise ValueError("Frequency must be positive")
|
||||
if isinstance(system, str):
|
||||
from .systems import SYSTEMS
|
||||
system = SYSTEMS[system]
|
||||
n = len(system.tone_names)
|
||||
c_idx = getattr(system, 'c_index', C_INDEX)
|
||||
# Steps from A4 in this EDO
|
||||
steps_from_a4 = n * math.log2(hz / REFERENCE_A)
|
||||
steps = round(steps_from_a4)
|
||||
# A4 is index 0, octave 4. Convert to absolute position from C0.
|
||||
a4_from_c0 = ((0 - c_idx) % n) + (4 * n)
|
||||
abs_pos = a4_from_c0 + steps
|
||||
octave = abs_pos // n
|
||||
relative = abs_pos % n
|
||||
index = (relative + c_idx) % n
|
||||
return klass.from_index(index, octave=octave, system=system)
|
||||
|
||||
@classmethod
|
||||
def from_midi(klass, note_number: int, system: Union[str, object] = "western") -> Tone:
|
||||
"""Create a Tone from a MIDI note number.
|
||||
|
||||
MIDI note 60 = C4 (middle C), 69 = A4 (440 Hz).
|
||||
|
||||
Example::
|
||||
|
||||
>>> Tone.from_midi(60)
|
||||
<Tone C4>
|
||||
>>> Tone.from_midi(69)
|
||||
<Tone A4>
|
||||
"""
|
||||
if isinstance(system, str):
|
||||
from .systems import SYSTEMS
|
||||
system = SYSTEMS[system]
|
||||
# MIDI is a 12-TET standard. Convert to Hz and use from_frequency
|
||||
# for non-12 systems.
|
||||
n = len(system.tone_names)
|
||||
if n != 12:
|
||||
hz = REFERENCE_A * (2 ** ((note_number - 69) / 12))
|
||||
return klass.from_frequency(hz, system=system)
|
||||
adjusted = note_number - 12 # MIDI C0=12
|
||||
octave = adjusted // 12
|
||||
relative = adjusted % 12
|
||||
index = (relative + C_INDEX) % 12
|
||||
return klass.from_index(index, octave=octave, system=system)
|
||||
|
||||
@classmethod
|
||||
def from_index(klass, i: int, *, octave: int, system: object, prefer_flats: bool = False) -> Tone:
|
||||
"""Create a Tone from its index within a tuning system.
|
||||
|
||||
Args:
|
||||
i: The index of the tone in the system's tone list.
|
||||
octave: The octave number.
|
||||
system: The ``ToneSystem`` instance.
|
||||
prefer_flats: If True and the tone has a flat spelling,
|
||||
use it instead of the default sharp spelling.
|
||||
|
||||
Returns:
|
||||
A new ``Tone`` instance.
|
||||
"""
|
||||
tone_names = system.tone_names[i]
|
||||
if prefer_flats and len(tone_names) > 1:
|
||||
# Find the first flat spelling (contains 'b' but isn't just 'B')
|
||||
tone = tone_names[0] # fallback to primary
|
||||
for tn in tone_names[1:]:
|
||||
if 'b' in tn and tn != 'B':
|
||||
tone = tn
|
||||
break
|
||||
else:
|
||||
tone = tone_names[0] # primary spelling
|
||||
# Bypass parsing and validation — name comes from a known system index
|
||||
obj = klass.__new__(klass)
|
||||
obj.name = tone
|
||||
obj.octave = octave
|
||||
obj.alt_names = list(tone_names[1:]) if len(tone_names) > 1 else []
|
||||
obj._frequency = None
|
||||
if isinstance(system, str):
|
||||
obj.system_name = system
|
||||
obj._system = None
|
||||
else:
|
||||
obj.system_name = None
|
||||
obj._system = system
|
||||
return obj
|
||||
|
||||
@property
|
||||
def _index(self):
|
||||
def _index(self) -> int:
|
||||
"""The index of this tone within its associated system's tone list.
|
||||
|
||||
Resolves enharmonic names (e.g. 'Db' → 'C#') before lookup.
|
||||
|
||||
Raises:
|
||||
ValueError: If no system is associated with this tone or
|
||||
the name is not found.
|
||||
"""
|
||||
try:
|
||||
return self.system.tones.index(self.name)
|
||||
canonical = self.system.resolve_name(self.name)
|
||||
if canonical is None:
|
||||
raise ValueError(f"Tone {self.name!r} not found in system")
|
||||
# Use _name_to_index for direct lookup (avoids creating Tone objects)
|
||||
idx = self.system._name_to_index(canonical)
|
||||
if idx is not None:
|
||||
return idx
|
||||
# Fallback: linear search through tone_names
|
||||
for i, names in enumerate(self.system.tone_names):
|
||||
if canonical in names:
|
||||
return i
|
||||
raise ValueError(f"Tone {self.name!r} not found in system")
|
||||
except AttributeError:
|
||||
raise ValueError("Tone index cannot be referenced without a system!")
|
||||
|
||||
def _math(self, interval):
|
||||
def _math(self, interval: int) -> tuple[int, int]:
|
||||
"""Returns (new index, new octave).
|
||||
|
||||
Octave boundaries follow scientific pitch notation, where the
|
||||
@@ -162,70 +571,269 @@ class Tone:
|
||||
octave = self.octave or 0
|
||||
|
||||
try:
|
||||
mod = len(self.system.tones)
|
||||
mod = len(self.system.tone_names)
|
||||
except AttributeError:
|
||||
raise ValueError(
|
||||
"Tone math can only be computed with an associated system!"
|
||||
)
|
||||
|
||||
# C is at index 3 in the Western tone list (A=0, A#=1, B=2, C=3, ...)
|
||||
# Scientific pitch notation changes octave at C, not A.
|
||||
c_index = 3
|
||||
c_idx = getattr(self.system, 'c_index', C_INDEX)
|
||||
|
||||
# Convert to absolute semitones from C0
|
||||
note_from_c0 = ((self._index - c_index) % mod) + (octave * mod)
|
||||
# Convert to absolute steps from C0
|
||||
note_from_c0 = ((self._index - c_idx) % mod) + (octave * mod)
|
||||
note_from_c0 += interval
|
||||
|
||||
new_octave = note_from_c0 // mod
|
||||
relative = note_from_c0 % mod
|
||||
new_index = (relative + c_index) % mod
|
||||
new_index = (relative + c_idx) % mod
|
||||
|
||||
return (new_index, new_octave)
|
||||
|
||||
def add(self, interval):
|
||||
index, octave = self._math(interval)
|
||||
return self.from_index(index, octave=octave, system=self.system)
|
||||
def add(self, interval: int, *, prefer_flats: bool = False) -> Tone:
|
||||
"""Return a new Tone that is *interval* semitones above this one.
|
||||
|
||||
def subtract(self, interval):
|
||||
Args:
|
||||
interval: Number of semitones to add (positive = up).
|
||||
prefer_flats: If True, use flat spellings (Bb, Eb) instead
|
||||
of sharp spellings (A#, D#) for accidentals.
|
||||
|
||||
Returns:
|
||||
A new ``Tone`` instance.
|
||||
"""
|
||||
index, octave = self._math(interval)
|
||||
return self.from_index(index, octave=octave, system=self.system, prefer_flats=prefer_flats)
|
||||
|
||||
def subtract(self, interval: int) -> Tone:
|
||||
"""Return a new Tone that is *interval* semitones below this one.
|
||||
|
||||
Args:
|
||||
interval: Number of semitones to subtract (positive = down).
|
||||
|
||||
Returns:
|
||||
A new ``Tone`` instance.
|
||||
"""
|
||||
return self.add((-1 * interval))
|
||||
|
||||
_INTERVAL_NAMES = {
|
||||
0: "unison", 1: "minor 2nd", 2: "major 2nd", 3: "minor 3rd",
|
||||
4: "major 3rd", 5: "perfect 4th", 6: "tritone", 7: "perfect 5th",
|
||||
8: "minor 6th", 9: "major 6th", 10: "minor 7th", 11: "major 7th",
|
||||
12: "octave",
|
||||
}
|
||||
|
||||
def interval_to(self, other: Tone) -> str:
|
||||
"""Name the interval between this tone and another.
|
||||
|
||||
Returns a string like ``"perfect 5th"``, ``"major 3rd"``, or
|
||||
``"octave"``. For intervals larger than an octave, returns
|
||||
the compound form (e.g. ``"minor 2nd + 1 octave"``).
|
||||
|
||||
Example::
|
||||
|
||||
>>> C4.interval_to(G4)
|
||||
'perfect 5th'
|
||||
>>> C4.interval_to(C5)
|
||||
'octave'
|
||||
"""
|
||||
semitones = abs(self - other)
|
||||
n = len(self.system.tones)
|
||||
octaves = semitones // n
|
||||
remainder = semitones % n
|
||||
name = self._INTERVAL_NAMES.get(remainder, f"{remainder} steps")
|
||||
if octaves == 0:
|
||||
return name
|
||||
if remainder == 0:
|
||||
if octaves == 1:
|
||||
return "octave"
|
||||
return f"{octaves} octaves"
|
||||
if octaves == 1:
|
||||
return f"{name} + 1 octave"
|
||||
return f"{name} + {octaves} octaves"
|
||||
|
||||
@property
|
||||
def frequency(self):
|
||||
"""The frequency of this tone in Hz (equal temperament, A4=440)."""
|
||||
return self.pitch()
|
||||
def midi(self) -> Optional[int]:
|
||||
"""MIDI note number (C4 = 60, A4 = 69).
|
||||
|
||||
The MIDI standard assigns integer note numbers from 0–127.
|
||||
Middle C (C4) is 60, and each semitone increments by 1.
|
||||
|
||||
Returns:
|
||||
int: the MIDI note number, or None if no octave is set.
|
||||
"""
|
||||
if self.octave is None:
|
||||
return None
|
||||
n = len(self.system.tones)
|
||||
if n != 12:
|
||||
# Non-12-TET: approximate MIDI via frequency
|
||||
import math
|
||||
hz = self.pitch()
|
||||
return round(69 + 12 * math.log2(hz / REFERENCE_A))
|
||||
semitones_from_c0 = ((self._index - C_INDEX) % 12) + (self.octave * 12)
|
||||
return semitones_from_c0 + 12 # MIDI C0 = 12 (C-1 = 0)
|
||||
|
||||
def transpose(self, semitones: int) -> Tone:
|
||||
"""Return a new Tone transposed by the given number of semitones.
|
||||
|
||||
Alias for ``tone + semitones`` / ``tone - semitones``. Positive
|
||||
values transpose up, negative values transpose down.
|
||||
"""
|
||||
return self.add(semitones)
|
||||
|
||||
def cents_difference(self, other: Tone, *, temperament: str = "equal") -> float:
|
||||
"""Difference in cents between this tone and another.
|
||||
|
||||
One semitone = 100 cents. Musicians use cents to measure fine
|
||||
pitch differences — e.g. comparing equal temperament to
|
||||
Pythagorean tuning, or checking how far out of tune a note is.
|
||||
|
||||
Args:
|
||||
other: The tone to compare against.
|
||||
temperament: Tuning temperament for both tones.
|
||||
|
||||
Returns:
|
||||
Signed float — positive means *other* is higher.
|
||||
|
||||
Example::
|
||||
|
||||
>>> a4 = Tone.from_string("A4", system="western")
|
||||
>>> a4.cents_difference(a4 + 1) # one semitone
|
||||
100.0
|
||||
>>> a4_pyth = a4.pitch(temperament="pythagorean")
|
||||
>>> a4_equal = a4.pitch(temperament="equal")
|
||||
"""
|
||||
import math
|
||||
f1 = self.pitch(temperament=temperament)
|
||||
f2 = other.pitch(temperament=temperament)
|
||||
if f1 <= 0 or f2 <= 0:
|
||||
raise ValueError("Both tones must have positive frequencies")
|
||||
return 1200 * math.log2(f2 / f1)
|
||||
|
||||
def circle_of_fifths(self) -> list[Tone]:
|
||||
"""The circle of fifths starting from this tone.
|
||||
|
||||
Each step ascends by a perfect fifth (7 semitones in 12-TET).
|
||||
After N steps (where N = number of tones in the system) you
|
||||
return to the starting tone. The circle of fifths is the
|
||||
backbone of Western harmony — it determines key signatures,
|
||||
chord relationships, and modulation paths.
|
||||
|
||||
Returns:
|
||||
A list of Tones (12 for Western, N for other systems).
|
||||
"""
|
||||
n = len(self.system.tones)
|
||||
# Perfect fifth: the closest approximation to 3:2 ratio
|
||||
fifth = round(n * 7 / 12) # 7 in 12-TET, 11 in 19-TET, 18 in 31-TET
|
||||
tones: list[Tone] = []
|
||||
t = self
|
||||
for _ in range(n):
|
||||
tones.append(t)
|
||||
t = t.add(fifth)
|
||||
return tones
|
||||
|
||||
def circle_of_fourths(self) -> list[Tone]:
|
||||
"""The circle of fourths starting from this tone.
|
||||
|
||||
Each step ascends by a perfect fourth — the reverse direction
|
||||
of the circle of fifths.
|
||||
|
||||
Returns:
|
||||
A list of Tones (12 for Western, N for other systems).
|
||||
"""
|
||||
n = len(self.system.tones)
|
||||
fourth = round(n * 5 / 12) # 5 in 12-TET, 8 in 19-TET, 13 in 31-TET
|
||||
tones: list[Tone] = []
|
||||
t = self
|
||||
for _ in range(n):
|
||||
tones.append(t)
|
||||
t = t.add(fourth)
|
||||
return tones
|
||||
|
||||
@property
|
||||
def frequency(self) -> float:
|
||||
"""The frequency of this tone in Hz (equal temperament, A4=440).
|
||||
|
||||
The result is cached after the first computation.
|
||||
"""
|
||||
if self._frequency is None:
|
||||
self._frequency = self.pitch()
|
||||
return self._frequency
|
||||
|
||||
def overtones(self, n: int = 8) -> list[float]:
|
||||
"""The first *n* overtones (harmonic series) of this tone.
|
||||
|
||||
The harmonic series is the foundation of timbre and consonance.
|
||||
When a string or air column vibrates, it produces not just the
|
||||
fundamental frequency but also integer multiples: 2f, 3f, 4f...
|
||||
|
||||
The intervals between consecutive harmonics form the basis of
|
||||
Western harmony::
|
||||
|
||||
Harmonic Ratio Interval from fundamental
|
||||
1 1:1 Unison (the fundamental)
|
||||
2 2:1 Octave
|
||||
3 3:1 Octave + perfect 5th
|
||||
4 4:1 Two octaves
|
||||
5 5:1 Two octaves + major 3rd
|
||||
6 6:1 Two octaves + perfect 5th
|
||||
7 7:1 Two octaves + minor 7th (slightly flat)
|
||||
8 8:1 Three octaves
|
||||
|
||||
The reason a perfect fifth sounds consonant is that the 3rd
|
||||
harmonic of the lower note aligns with the 2nd harmonic of the
|
||||
upper note (when the upper note is a fifth above). More shared
|
||||
harmonics = more consonance.
|
||||
|
||||
Args:
|
||||
n: Number of harmonics to return (default 8).
|
||||
|
||||
Returns:
|
||||
List of frequencies in Hz.
|
||||
"""
|
||||
f = self.pitch()
|
||||
return [f * i for i in range(1, n + 1)]
|
||||
|
||||
def pitch(
|
||||
self,
|
||||
*,
|
||||
reference_pitch=REFERENCE_A,
|
||||
temperament="equal",
|
||||
symbolic=False,
|
||||
precision=None,
|
||||
):
|
||||
reference_pitch: float = REFERENCE_A,
|
||||
temperament: str = "equal",
|
||||
symbolic: bool = False,
|
||||
precision: Optional[int] = None,
|
||||
) -> float:
|
||||
try:
|
||||
tones = len(self.system.tones)
|
||||
tones = len(self.system.tone_names)
|
||||
except AttributeError:
|
||||
raise ValueError("Pitches can only be computed with an associated system!")
|
||||
|
||||
pitch_scale = TEMPERAMENTS[temperament](tones)
|
||||
octave = self.octave or 4
|
||||
# Period ratio: 2.0 for standard octave-based systems,
|
||||
# 3.0 for Bohlen-Pierce (tritave), configurable per system.
|
||||
period = getattr(self.system, 'period', 2.0)
|
||||
c_idx = getattr(self.system, 'c_index', C_INDEX)
|
||||
|
||||
# C is at index 3; convert to semitones from C0 for both
|
||||
# this note and the reference A4.
|
||||
c_index = 3
|
||||
note_from_c0 = ((self._index - c_index) % tones) + (octave * tones)
|
||||
a4_from_c0 = ((0 - c_index) % tones) + (4 * tones) # A4
|
||||
# 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
|
||||
|
||||
note_from_c0 = ((self._index - c_idx) % tones) + (octave * tones)
|
||||
a4_from_c0 = ((0 - c_idx) % tones) + (4 * tones) # A4
|
||||
|
||||
diff = note_from_c0 - a4_from_c0
|
||||
octave_shift = diff // tones
|
||||
within_octave = diff % tones
|
||||
|
||||
ratio = pitch_scale[within_octave] * (2 ** octave_shift)
|
||||
ratio = pitch_scale[within_octave] * (period ** octave_shift)
|
||||
|
||||
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
|
||||
|
||||
+5417
-38
File diff suppressed because it is too large
Load Diff
@@ -2,10 +2,38 @@ version = 1
|
||||
revision = 3
|
||||
requires-python = ">=3.10"
|
||||
resolution-markers = [
|
||||
"python_full_version >= '3.11'",
|
||||
"python_full_version >= '3.12'",
|
||||
"python_full_version == '3.11.*'",
|
||||
"python_full_version < '3.11'",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "alabaster"
|
||||
version = "1.0.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/a6/f8/d9c74d0daf3f742840fd818d69cfae176fa332022fd44e3469487d5a9420/alabaster-1.0.0.tar.gz", hash = "sha256:c00dca57bca26fa62a6d7d0a9fcce65f3e026e9bfe33e9c538fd3fbb2144fd9e", size = 24210, upload-time = "2024-07-26T18:15:03.762Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/7e/b3/6b4067be973ae96ba0d615946e314c5ae35f9f993eca561b356540bb0c2b/alabaster-1.0.0-py3-none-any.whl", hash = "sha256:fc6786402dc3fcb2de3cabd5fe455a2db534b371124f1f21de8731783dec828b", size = 13929, upload-time = "2024-07-26T18:15:02.05Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "babel"
|
||||
version = "2.18.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/7d/b2/51899539b6ceeeb420d40ed3cd4b7a40519404f9baf3d4ac99dc413a834b/babel-2.18.0.tar.gz", hash = "sha256:b80b99a14bd085fcacfa15c9165f651fbb3406e66cc603abf11c5750937c992d", size = 9959554, upload-time = "2026-02-01T12:30:56.078Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/77/f5/21d2de20e8b8b0408f0681956ca2c69f1320a3848ac50e6e7f39c6159675/babel-2.18.0-py3-none-any.whl", hash = "sha256:e2b422b277c2b9a9630c1d7903c2a00d0830c409c59ac8cae9081c92f1aeba35", size = 10196845, upload-time = "2026-02-01T12:30:53.445Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "certifi"
|
||||
version = "2026.2.25"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/af/2d/7bf41579a8986e348fa033a31cdd0e4121114f6bce2457e8876010b092dd/certifi-2026.2.25.tar.gz", hash = "sha256:e887ab5cee78ea814d3472169153c2d12cd43b14bd03329a39a9c6e2e80bfba7", size = 155029, upload-time = "2026-02-25T02:54:17.342Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/9a/3c/c17fb3ca2d9c3acff52e30b309f538586f9f5b9c9cf454f3845fc9af4881/certifi-2026.2.25-py3-none-any.whl", hash = "sha256:027692e4402ad994f1c42e52a4997a9763c646b73e4096e4d5d6db8af1d6f0fa", size = 153684, upload-time = "2026-02-25T02:54:15.766Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "cffi"
|
||||
version = "2.0.0"
|
||||
@@ -88,6 +116,111 @@ wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/ae/3a/dbeec9d1ee0844c679f6bb5d6ad4e9f198b1224f4e7a32825f47f6192b0c/cffi-2.0.0-cp314-cp314t-win_arm64.whl", hash = "sha256:0a1527a803f0a659de1af2e1fd700213caba79377e27e4693648c2923da066f9", size = 184195, upload-time = "2025-09-08T23:23:43.004Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "charset-normalizer"
|
||||
version = "3.4.6"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/7b/60/e3bec1881450851b087e301bedc3daa9377a4d45f1c26aa90b0b235e38aa/charset_normalizer-3.4.6.tar.gz", hash = "sha256:1ae6b62897110aa7c79ea2f5dd38d1abca6db663687c0b1ad9aed6f6bae3d9d6", size = 143363, upload-time = "2026-03-15T18:53:25.478Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/e6/8c/2c56124c6dc53a774d435f985b5973bc592f42d437be58c0c92d65ae7296/charset_normalizer-3.4.6-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:2e1d8ca8611099001949d1cdfaefc510cf0f212484fe7c565f735b68c78c3c95", size = 298751, upload-time = "2026-03-15T18:50:00.003Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/86/2a/2a7db6b314b966a3bcad8c731c0719c60b931b931de7ae9f34b2839289ee/charset_normalizer-3.4.6-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e25369dc110d58ddf29b949377a93e0716d72a24f62bad72b2b39f155949c1fd", size = 200027, upload-time = "2026-03-15T18:50:01.702Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/68/f2/0fe775c74ae25e2a3b07b01538fc162737b3e3f795bada3bc26f4d4d495c/charset_normalizer-3.4.6-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:259695e2ccc253feb2a016303543d691825e920917e31f894ca1a687982b1de4", size = 220741, upload-time = "2026-03-15T18:50:03.194Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/10/98/8085596e41f00b27dd6aa1e68413d1ddda7e605f34dd546833c61fddd709/charset_normalizer-3.4.6-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:dda86aba335c902b6149a02a55b38e96287157e609200811837678214ba2b1db", size = 215802, upload-time = "2026-03-15T18:50:05.859Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/fd/ce/865e4e09b041bad659d682bbd98b47fb490b8e124f9398c9448065f64fee/charset_normalizer-3.4.6-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:51fb3c322c81d20567019778cb5a4a6f2dc1c200b886bc0d636238e364848c89", size = 207908, upload-time = "2026-03-15T18:50:07.676Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a8/54/8c757f1f7349262898c2f169e0d562b39dcb977503f18fdf0814e923db78/charset_normalizer-3.4.6-cp310-cp310-manylinux_2_31_armv7l.whl", hash = "sha256:4482481cb0572180b6fd976a4d5c72a30263e98564da68b86ec91f0fe35e8565", size = 194357, upload-time = "2026-03-15T18:50:09.327Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/6f/29/e88f2fac9218907fc7a70722b393d1bbe8334c61fe9c46640dba349b6e66/charset_normalizer-3.4.6-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:39f5068d35621da2881271e5c3205125cc456f54e9030d3f723288c873a71bf9", size = 205610, upload-time = "2026-03-15T18:50:10.732Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/4c/c5/21d7bb0cb415287178450171d130bed9d664211fdd59731ed2c34267b07d/charset_normalizer-3.4.6-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:8bea55c4eef25b0b19a0337dc4e3f9a15b00d569c77211fa8cde38684f234fb7", size = 203512, upload-time = "2026-03-15T18:50:12.535Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a4/be/ce52f3c7fdb35cc987ad38a53ebcef52eec498f4fb6c66ecfe62cfe57ba2/charset_normalizer-3.4.6-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:f0cdaecd4c953bfae0b6bb64910aaaca5a424ad9c72d85cb88417bb9814f7550", size = 195398, upload-time = "2026-03-15T18:50:14.236Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/81/a0/3ab5dd39d4859a3555e5dadfc8a9fa7f8352f8c183d1a65c90264517da0e/charset_normalizer-3.4.6-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:150b8ce8e830eb7ccb029ec9ca36022f756986aaaa7956aad6d9ec90089338c0", size = 221772, upload-time = "2026-03-15T18:50:15.581Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/04/6e/6a4e41a97ba6b2fa87f849c41e4d229449a586be85053c4d90135fe82d26/charset_normalizer-3.4.6-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:e68c14b04827dd76dcbd1aeea9e604e3e4b78322d8faf2f8132c7138efa340a8", size = 205759, upload-time = "2026-03-15T18:50:17.047Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/db/3b/34a712a5ee64a6957bf355b01dc17b12de457638d436fdb05d01e463cd1c/charset_normalizer-3.4.6-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:3778fd7d7cd04ae8f54651f4a7a0bd6e39a0cf20f801720a4c21d80e9b7ad6b0", size = 216938, upload-time = "2026-03-15T18:50:18.44Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/cb/05/5bd1e12da9ab18790af05c61aafd01a60f489778179b621ac2a305243c62/charset_normalizer-3.4.6-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:dad6e0f2e481fffdcf776d10ebee25e0ef89f16d691f1e5dee4b586375fdc64b", size = 210138, upload-time = "2026-03-15T18:50:19.852Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/bd/8e/3cb9e2d998ff6b21c0a1860343cb7b83eba9cdb66b91410e18fc4969d6ab/charset_normalizer-3.4.6-cp310-cp310-win32.whl", hash = "sha256:74a2e659c7ecbc73562e2a15e05039f1e22c75b7c7618b4b574a3ea9118d1557", size = 144137, upload-time = "2026-03-15T18:50:21.505Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d8/8f/78f5489ffadb0db3eb7aff53d31c24531d33eb545f0c6f6567c25f49a5ff/charset_normalizer-3.4.6-cp310-cp310-win_amd64.whl", hash = "sha256:aa9cccf4a44b9b62d8ba8b4dd06c649ba683e4bf04eea606d2e94cfc2d6ff4d6", size = 154244, upload-time = "2026-03-15T18:50:22.81Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e4/74/e472659dffb0cadb2f411282d2d76c60da1fc94076d7fffed4ae8a93ec01/charset_normalizer-3.4.6-cp310-cp310-win_arm64.whl", hash = "sha256:e985a16ff513596f217cee86c21371b8cd011c0f6f056d0920aa2d926c544058", size = 143312, upload-time = "2026-03-15T18:50:24.074Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/62/28/ff6f234e628a2de61c458be2779cb182bc03f6eec12200d4a525bbfc9741/charset_normalizer-3.4.6-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:82060f995ab5003a2d6e0f4ad29065b7672b6593c8c63559beefe5b443242c3e", size = 293582, upload-time = "2026-03-15T18:50:25.454Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/1c/b7/b1a117e5385cbdb3205f6055403c2a2a220c5ea80b8716c324eaf75c5c95/charset_normalizer-3.4.6-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:60c74963d8350241a79cb8feea80e54d518f72c26db618862a8f53e5023deaf9", size = 197240, upload-time = "2026-03-15T18:50:27.196Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a1/5f/2574f0f09f3c3bc1b2f992e20bce6546cb1f17e111c5be07308dc5427956/charset_normalizer-3.4.6-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f6e4333fb15c83f7d1482a76d45a0818897b3d33f00efd215528ff7c51b8e35d", size = 217363, upload-time = "2026-03-15T18:50:28.601Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/4a/d1/0ae20ad77bc949ddd39b51bf383b6ca932f2916074c95cad34ae465ab71f/charset_normalizer-3.4.6-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:bc72863f4d9aba2e8fd9085e63548a324ba706d2ea2c83b260da08a59b9482de", size = 212994, upload-time = "2026-03-15T18:50:30.102Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/60/ac/3233d262a310c1b12633536a07cde5ddd16985e6e7e238e9f3f9423d8eb9/charset_normalizer-3.4.6-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9cc4fc6c196d6a8b76629a70ddfcd4635a6898756e2d9cac5565cf0654605d73", size = 204697, upload-time = "2026-03-15T18:50:31.654Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/25/3c/8a18fc411f085b82303cfb7154eed5bd49c77035eb7608d049468b53f87c/charset_normalizer-3.4.6-cp311-cp311-manylinux_2_31_armv7l.whl", hash = "sha256:0c173ce3a681f309f31b87125fecec7a5d1347261ea11ebbb856fa6006b23c8c", size = 191673, upload-time = "2026-03-15T18:50:33.433Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ff/a7/11cfe61d6c5c5c7438d6ba40919d0306ed83c9ab957f3d4da2277ff67836/charset_normalizer-3.4.6-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:c907cdc8109f6c619e6254212e794d6548373cc40e1ec75e6e3823d9135d29cc", size = 201120, upload-time = "2026-03-15T18:50:35.105Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b5/10/cf491fa1abd47c02f69687046b896c950b92b6cd7337a27e6548adbec8e4/charset_normalizer-3.4.6-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:404a1e552cf5b675a87f0651f8b79f5f1e6fd100ee88dc612f89aa16abd4486f", size = 200911, upload-time = "2026-03-15T18:50:36.819Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/28/70/039796160b48b18ed466fde0af84c1b090c4e288fae26cd674ad04a2d703/charset_normalizer-3.4.6-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:e3c701e954abf6fc03a49f7c579cc80c2c6cc52525340ca3186c41d3f33482ef", size = 192516, upload-time = "2026-03-15T18:50:38.228Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ff/34/c56f3223393d6ff3124b9e78f7de738047c2d6bc40a4f16ac0c9d7a1cb3c/charset_normalizer-3.4.6-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:7a6967aaf043bceabab5412ed6bd6bd26603dae84d5cb75bf8d9a74a4959d398", size = 218795, upload-time = "2026-03-15T18:50:39.664Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e8/3b/ce2d4f86c5282191a041fdc5a4ce18f1c6bd40a5bd1f74cf8625f08d51c1/charset_normalizer-3.4.6-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:5feb91325bbceade6afab43eb3b508c63ee53579fe896c77137ded51c6b6958e", size = 201833, upload-time = "2026-03-15T18:50:41.552Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/3b/9b/b6a9f76b0fd7c5b5ec58b228ff7e85095370282150f0bd50b3126f5506d6/charset_normalizer-3.4.6-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:f820f24b09e3e779fe84c3c456cb4108a7aa639b0d1f02c28046e11bfcd088ed", size = 213920, upload-time = "2026-03-15T18:50:43.33Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ae/98/7bc23513a33d8172365ed30ee3a3b3fe1ece14a395e5fc94129541fc6003/charset_normalizer-3.4.6-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:b35b200d6a71b9839a46b9b7fff66b6638bb52fc9658aa58796b0326595d3021", size = 206951, upload-time = "2026-03-15T18:50:44.789Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/32/73/c0b86f3d1458468e11aec870e6b3feac931facbe105a894b552b0e518e79/charset_normalizer-3.4.6-cp311-cp311-win32.whl", hash = "sha256:9ca4c0b502ab399ef89248a2c84c54954f77a070f28e546a85e91da627d1301e", size = 143703, upload-time = "2026-03-15T18:50:46.103Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c6/e3/76f2facfe8eddee0bbd38d2594e709033338eae44ebf1738bcefe0a06185/charset_normalizer-3.4.6-cp311-cp311-win_amd64.whl", hash = "sha256:a9e68c9d88823b274cf1e72f28cb5dc89c990edf430b0bfd3e2fb0785bfeabf4", size = 153857, upload-time = "2026-03-15T18:50:47.563Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e2/dc/9abe19c9b27e6cd3636036b9d1b387b78c40dedbf0b47f9366737684b4b0/charset_normalizer-3.4.6-cp311-cp311-win_arm64.whl", hash = "sha256:97d0235baafca5f2b09cf332cc275f021e694e8362c6bb9c96fc9a0eb74fc316", size = 142751, upload-time = "2026-03-15T18:50:49.234Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e5/62/c0815c992c9545347aeea7859b50dc9044d147e2e7278329c6e02ac9a616/charset_normalizer-3.4.6-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:2ef7fedc7a6ecbe99969cd09632516738a97eeb8bd7258bf8a0f23114c057dab", size = 295154, upload-time = "2026-03-15T18:50:50.88Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a8/37/bdca6613c2e3c58c7421891d80cc3efa1d32e882f7c4a7ee6039c3fc951a/charset_normalizer-3.4.6-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a4ea868bc28109052790eb2b52a9ab33f3aa7adc02f96673526ff47419490e21", size = 199191, upload-time = "2026-03-15T18:50:52.658Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/6c/92/9934d1bbd69f7f398b38c5dae1cbf9cc672e7c34a4adf7b17c0a9c17d15d/charset_normalizer-3.4.6-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:836ab36280f21fc1a03c99cd05c6b7af70d2697e374c7af0b61ed271401a72a2", size = 218674, upload-time = "2026-03-15T18:50:54.102Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/af/90/25f6ab406659286be929fd89ab0e78e38aa183fc374e03aa3c12d730af8a/charset_normalizer-3.4.6-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:f1ce721c8a7dfec21fcbdfe04e8f68174183cf4e8188e0645e92aa23985c57ff", size = 215259, upload-time = "2026-03-15T18:50:55.616Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/4e/ef/79a463eb0fff7f96afa04c1d4c51f8fc85426f918db467854bfb6a569ce3/charset_normalizer-3.4.6-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0e28d62a8fc7a1fa411c43bd65e346f3bce9716dc51b897fbe930c5987b402d5", size = 207276, upload-time = "2026-03-15T18:50:57.054Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f7/72/d0426afec4b71dc159fa6b4e68f868cd5a3ecd918fec5813a15d292a7d10/charset_normalizer-3.4.6-cp312-cp312-manylinux_2_31_armv7l.whl", hash = "sha256:530d548084c4a9f7a16ed4a294d459b4f229db50df689bfe92027452452943a0", size = 195161, upload-time = "2026-03-15T18:50:58.686Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/bf/18/c82b06a68bfcb6ce55e508225d210c7e6a4ea122bfc0748892f3dc4e8e11/charset_normalizer-3.4.6-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:30f445ae60aad5e1f8bdbb3108e39f6fbc09f4ea16c815c66578878325f8f15a", size = 203452, upload-time = "2026-03-15T18:51:00.196Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/44/d6/0c25979b92f8adafdbb946160348d8d44aa60ce99afdc27df524379875cb/charset_normalizer-3.4.6-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:ac2393c73378fea4e52aa56285a3d64be50f1a12395afef9cce47772f60334c2", size = 202272, upload-time = "2026-03-15T18:51:01.703Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/2e/3d/7fea3e8fe84136bebbac715dd1221cc25c173c57a699c030ab9b8900cbb7/charset_normalizer-3.4.6-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:90ca27cd8da8118b18a52d5f547859cc1f8354a00cd1e8e5120df3e30d6279e5", size = 195622, upload-time = "2026-03-15T18:51:03.526Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/57/8a/d6f7fd5cb96c58ef2f681424fbca01264461336d2a7fc875e4446b1f1346/charset_normalizer-3.4.6-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:8e5a94886bedca0f9b78fecd6afb6629142fd2605aa70a125d49f4edc6037ee6", size = 220056, upload-time = "2026-03-15T18:51:05.269Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/16/50/478cdda782c8c9c3fb5da3cc72dd7f331f031e7f1363a893cdd6ca0f8de0/charset_normalizer-3.4.6-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:695f5c2823691a25f17bc5d5ffe79fa90972cc34b002ac6c843bb8a1720e950d", size = 203751, upload-time = "2026-03-15T18:51:06.858Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/75/fc/cc2fcac943939c8e4d8791abfa139f685e5150cae9f94b60f12520feaa9b/charset_normalizer-3.4.6-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:231d4da14bcd9301310faf492051bee27df11f2bc7549bc0bb41fef11b82daa2", size = 216563, upload-time = "2026-03-15T18:51:08.564Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a8/b7/a4add1d9a5f68f3d037261aecca83abdb0ab15960a3591d340e829b37298/charset_normalizer-3.4.6-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:a056d1ad2633548ca18ffa2f85c202cfb48b68615129143915b8dc72a806a923", size = 209265, upload-time = "2026-03-15T18:51:10.312Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/6c/18/c094561b5d64a24277707698e54b7f67bd17a4f857bbfbb1072bba07c8bf/charset_normalizer-3.4.6-cp312-cp312-win32.whl", hash = "sha256:c2274ca724536f173122f36c98ce188fd24ce3dad886ec2b7af859518ce008a4", size = 144229, upload-time = "2026-03-15T18:51:11.694Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ab/20/0567efb3a8fd481b8f34f739ebddc098ed062a59fed41a8d193a61939e8f/charset_normalizer-3.4.6-cp312-cp312-win_amd64.whl", hash = "sha256:c8ae56368f8cc97c7e40a7ee18e1cedaf8e780cd8bc5ed5ac8b81f238614facb", size = 154277, upload-time = "2026-03-15T18:51:13.004Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/15/57/28d79b44b51933119e21f65479d0864a8d5893e494cf5daab15df0247c17/charset_normalizer-3.4.6-cp312-cp312-win_arm64.whl", hash = "sha256:899d28f422116b08be5118ef350c292b36fc15ec2daeb9ea987c89281c7bb5c4", size = 142817, upload-time = "2026-03-15T18:51:14.408Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/1e/1d/4fdabeef4e231153b6ed7567602f3b68265ec4e5b76d6024cf647d43d981/charset_normalizer-3.4.6-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:11afb56037cbc4b1555a34dd69151e8e069bee82e613a73bef6e714ce733585f", size = 294823, upload-time = "2026-03-15T18:51:15.755Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/47/7b/20e809b89c69d37be748d98e84dce6820bf663cf19cf6b942c951a3e8f41/charset_normalizer-3.4.6-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:423fb7e748a08f854a08a222b983f4df1912b1daedce51a72bd24fe8f26a1843", size = 198527, upload-time = "2026-03-15T18:51:17.177Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/37/a6/4f8d27527d59c039dce6f7622593cdcd3d70a8504d87d09eb11e9fdc6062/charset_normalizer-3.4.6-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:d73beaac5e90173ac3deb9928a74763a6d230f494e4bfb422c217a0ad8e629bf", size = 218388, upload-time = "2026-03-15T18:51:18.934Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f6/9b/4770ccb3e491a9bacf1c46cc8b812214fe367c86a96353ccc6daf87b01ec/charset_normalizer-3.4.6-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d60377dce4511655582e300dc1e5a5f24ba0cb229005a1d5c8d0cb72bb758ab8", size = 214563, upload-time = "2026-03-15T18:51:20.374Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/2b/58/a199d245894b12db0b957d627516c78e055adc3a0d978bc7f65ddaf7c399/charset_normalizer-3.4.6-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:530e8cebeea0d76bdcf93357aa5e41336f48c3dc709ac52da2bb167c5b8271d9", size = 206587, upload-time = "2026-03-15T18:51:21.807Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/7e/70/3def227f1ec56f5c69dfc8392b8bd63b11a18ca8178d9211d7cc5e5e4f27/charset_normalizer-3.4.6-cp313-cp313-manylinux_2_31_armv7l.whl", hash = "sha256:a26611d9987b230566f24a0a125f17fe0de6a6aff9f25c9f564aaa2721a5fb88", size = 194724, upload-time = "2026-03-15T18:51:23.508Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/58/ab/9318352e220c05efd31c2779a23b50969dc94b985a2efa643ed9077bfca5/charset_normalizer-3.4.6-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:34315ff4fc374b285ad7f4a0bf7dcbfe769e1b104230d40f49f700d4ab6bbd84", size = 202956, upload-time = "2026-03-15T18:51:25.239Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/75/13/f3550a3ac25b70f87ac98c40d3199a8503676c2f1620efbf8d42095cfc40/charset_normalizer-3.4.6-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:5f8ddd609f9e1af8c7bd6e2aca279c931aefecd148a14402d4e368f3171769fd", size = 201923, upload-time = "2026-03-15T18:51:26.682Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/1b/db/c5c643b912740b45e8eec21de1bbab8e7fc085944d37e1e709d3dcd9d72f/charset_normalizer-3.4.6-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:80d0a5615143c0b3225e5e3ef22c8d5d51f3f72ce0ea6fb84c943546c7b25b6c", size = 195366, upload-time = "2026-03-15T18:51:28.129Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/5a/67/3b1c62744f9b2448443e0eb160d8b001c849ec3fef591e012eda6484787c/charset_normalizer-3.4.6-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:92734d4d8d187a354a556626c221cd1a892a4e0802ccb2af432a1d85ec012194", size = 219752, upload-time = "2026-03-15T18:51:29.556Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f6/98/32ffbaf7f0366ffb0445930b87d103f6b406bc2c271563644bde8a2b1093/charset_normalizer-3.4.6-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:613f19aa6e082cf96e17e3ffd89383343d0d589abda756b7764cf78361fd41dc", size = 203296, upload-time = "2026-03-15T18:51:30.921Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/41/12/5d308c1bbe60cabb0c5ef511574a647067e2a1f631bc8634fcafaccd8293/charset_normalizer-3.4.6-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:2b1a63e8224e401cafe7739f77efd3f9e7f5f2026bda4aead8e59afab537784f", size = 215956, upload-time = "2026-03-15T18:51:32.399Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/53/e9/5f85f6c5e20669dbe56b165c67b0260547dea97dba7e187938833d791687/charset_normalizer-3.4.6-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:6cceb5473417d28edd20c6c984ab6fee6c6267d38d906823ebfe20b03d607dc2", size = 208652, upload-time = "2026-03-15T18:51:34.214Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f1/11/897052ea6af56df3eef3ca94edafee410ca699ca0c7b87960ad19932c55e/charset_normalizer-3.4.6-cp313-cp313-win32.whl", hash = "sha256:d7de2637729c67d67cf87614b566626057e95c303bc0a55ffe391f5205e7003d", size = 143940, upload-time = "2026-03-15T18:51:36.15Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a1/5c/724b6b363603e419829f561c854b87ed7c7e31231a7908708ac086cdf3e2/charset_normalizer-3.4.6-cp313-cp313-win_amd64.whl", hash = "sha256:572d7c822caf521f0525ba1bce1a622a0b85cf47ffbdae6c9c19e3b5ac3c4389", size = 154101, upload-time = "2026-03-15T18:51:37.876Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/01/a5/7abf15b4c0968e47020f9ca0935fb3274deb87cb288cd187cad92e8cdffd/charset_normalizer-3.4.6-cp313-cp313-win_arm64.whl", hash = "sha256:a4474d924a47185a06411e0064b803c68be044be2d60e50e8bddcc2649957c1f", size = 143109, upload-time = "2026-03-15T18:51:39.565Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/25/6f/ffe1e1259f384594063ea1869bfb6be5cdb8bc81020fc36c3636bc8302a1/charset_normalizer-3.4.6-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:9cc6e6d9e571d2f863fa77700701dae73ed5f78881efc8b3f9a4398772ff53e8", size = 294458, upload-time = "2026-03-15T18:51:41.134Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/56/60/09bb6c13a8c1016c2ed5c6a6488e4ffef506461aa5161662bd7636936fb1/charset_normalizer-3.4.6-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ef5960d965e67165d75b7c7ffc60a83ec5abfc5c11b764ec13ea54fbef8b4421", size = 199277, upload-time = "2026-03-15T18:51:42.953Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/00/50/dcfbb72a5138bbefdc3332e8d81a23494bf67998b4b100703fd15fa52d81/charset_normalizer-3.4.6-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:b3694e3f87f8ac7ce279d4355645b3c878d24d1424581b46282f24b92f5a4ae2", size = 218758, upload-time = "2026-03-15T18:51:44.339Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/03/b3/d79a9a191bb75f5aa81f3aaaa387ef29ce7cb7a9e5074ba8ea095cc073c2/charset_normalizer-3.4.6-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5d11595abf8dd942a77883a39d81433739b287b6aa71620f15164f8096221b30", size = 215299, upload-time = "2026-03-15T18:51:45.871Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/76/7e/bc8911719f7084f72fd545f647601ea3532363927f807d296a8c88a62c0d/charset_normalizer-3.4.6-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7bda6eebafd42133efdca535b04ccb338ab29467b3f7bf79569883676fc628db", size = 206811, upload-time = "2026-03-15T18:51:47.308Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e2/40/c430b969d41dda0c465aa36cc7c2c068afb67177bef50905ac371b28ccc7/charset_normalizer-3.4.6-cp314-cp314-manylinux_2_31_armv7l.whl", hash = "sha256:bbc8c8650c6e51041ad1be191742b8b421d05bbd3410f43fa2a00c8db87678e8", size = 193706, upload-time = "2026-03-15T18:51:48.849Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/48/15/e35e0590af254f7df984de1323640ef375df5761f615b6225ba8deb9799a/charset_normalizer-3.4.6-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:22c6f0c2fbc31e76c3b8a86fba1a56eda6166e238c29cdd3d14befdb4a4e4815", size = 202706, upload-time = "2026-03-15T18:51:50.257Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/5e/bd/f736f7b9cc5e93a18b794a50346bb16fbfd6b37f99e8f306f7951d27c17c/charset_normalizer-3.4.6-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:7edbed096e4a4798710ed6bc75dcaa2a21b68b6c356553ac4823c3658d53743a", size = 202497, upload-time = "2026-03-15T18:51:52.012Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/9d/ba/2cc9e3e7dfdf7760a6ed8da7446d22536f3d0ce114ac63dee2a5a3599e62/charset_normalizer-3.4.6-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:7f9019c9cb613f084481bd6a100b12e1547cf2efe362d873c2e31e4035a6fa43", size = 193511, upload-time = "2026-03-15T18:51:53.723Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/9e/cb/5be49b5f776e5613be07298c80e1b02a2d900f7a7de807230595c85a8b2e/charset_normalizer-3.4.6-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:58c948d0d086229efc484fe2f30c2d382c86720f55cd9bc33591774348ad44e0", size = 220133, upload-time = "2026-03-15T18:51:55.333Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/83/43/99f1b5dad345accb322c80c7821071554f791a95ee50c1c90041c157ae99/charset_normalizer-3.4.6-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:419a9d91bd238052642a51938af8ac05da5b3343becde08d5cdeab9046df9ee1", size = 203035, upload-time = "2026-03-15T18:51:56.736Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/87/9a/62c2cb6a531483b55dddff1a68b3d891a8b498f3ca555fbcf2978e804d9d/charset_normalizer-3.4.6-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:5273b9f0b5835ff0350c0828faea623c68bfa65b792720c453e22b25cc72930f", size = 216321, upload-time = "2026-03-15T18:51:58.17Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/6e/79/94a010ff81e3aec7c293eb82c28f930918e517bc144c9906a060844462eb/charset_normalizer-3.4.6-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:0e901eb1049fdb80f5bd11ed5ea1e498ec423102f7a9b9e4645d5b8204ff2815", size = 208973, upload-time = "2026-03-15T18:51:59.998Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/2a/57/4ecff6d4ec8585342f0c71bc03efaa99cb7468f7c91a57b105bcd561cea8/charset_normalizer-3.4.6-cp314-cp314-win32.whl", hash = "sha256:b4ff1d35e8c5bd078be89349b6f3a845128e685e751b6ea1169cf2160b344c4d", size = 144610, upload-time = "2026-03-15T18:52:02.213Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/80/94/8434a02d9d7f168c25767c64671fead8d599744a05d6a6c877144c754246/charset_normalizer-3.4.6-cp314-cp314-win_amd64.whl", hash = "sha256:74119174722c4349af9708993118581686f343adc1c8c9c007d59be90d077f3f", size = 154962, upload-time = "2026-03-15T18:52:03.658Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/46/4c/48f2cdbfd923026503dfd67ccea45c94fd8fe988d9056b468579c66ed62b/charset_normalizer-3.4.6-cp314-cp314-win_arm64.whl", hash = "sha256:e5bcc1a1ae744e0bb59641171ae53743760130600da8db48cbb6e4918e186e4e", size = 143595, upload-time = "2026-03-15T18:52:05.123Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/31/93/8878be7569f87b14f1d52032946131bcb6ebbd8af3e20446bc04053dc3f1/charset_normalizer-3.4.6-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:ad8faf8df23f0378c6d527d8b0b15ea4a2e23c89376877c598c4870d1b2c7866", size = 314828, upload-time = "2026-03-15T18:52:06.831Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/06/b6/fae511ca98aac69ecc35cde828b0a3d146325dd03d99655ad38fc2cc3293/charset_normalizer-3.4.6-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f5ea69428fa1b49573eef0cc44a1d43bebd45ad0c611eb7d7eac760c7ae771bc", size = 208138, upload-time = "2026-03-15T18:52:08.239Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/54/57/64caf6e1bf07274a1e0b7c160a55ee9e8c9ec32c46846ce59b9c333f7008/charset_normalizer-3.4.6-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:06a7e86163334edfc5d20fe104db92fcd666e5a5df0977cb5680a506fe26cc8e", size = 224679, upload-time = "2026-03-15T18:52:10.043Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/aa/cb/9ff5a25b9273ef160861b41f6937f86fae18b0792fe0a8e75e06acb08f1d/charset_normalizer-3.4.6-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:e1f6e2f00a6b8edb562826e4632e26d063ac10307e80f7461f7de3ad8ef3f077", size = 223475, upload-time = "2026-03-15T18:52:11.854Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/fc/97/440635fc093b8d7347502a377031f9605a1039c958f3cd18dcacffb37743/charset_normalizer-3.4.6-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:95b52c68d64c1878818687a473a10547b3292e82b6f6fe483808fb1468e2f52f", size = 215230, upload-time = "2026-03-15T18:52:13.325Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/cd/24/afff630feb571a13f07c8539fbb502d2ab494019492aaffc78ef41f1d1d0/charset_normalizer-3.4.6-cp314-cp314t-manylinux_2_31_armv7l.whl", hash = "sha256:7504e9b7dc05f99a9bbb4525c67a2c155073b44d720470a148b34166a69c054e", size = 199045, upload-time = "2026-03-15T18:52:14.752Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e5/17/d1399ecdaf7e0498c327433e7eefdd862b41236a7e484355b8e0e5ebd64b/charset_normalizer-3.4.6-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:172985e4ff804a7ad08eebec0a1640ece87ba5041d565fff23c8f99c1f389484", size = 211658, upload-time = "2026-03-15T18:52:16.278Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b5/38/16baa0affb957b3d880e5ac2144caf3f9d7de7bc4a91842e447fbb5e8b67/charset_normalizer-3.4.6-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:4be9f4830ba8741527693848403e2c457c16e499100963ec711b1c6f2049b7c7", size = 210769, upload-time = "2026-03-15T18:52:17.782Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/05/34/c531bc6ac4c21da9ddfddb3107be2287188b3ea4b53b70fc58f2a77ac8d8/charset_normalizer-3.4.6-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:79090741d842f564b1b2827c0b82d846405b744d31e84f18d7a7b41c20e473ff", size = 201328, upload-time = "2026-03-15T18:52:19.553Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/fa/73/a5a1e9ca5f234519c1953608a03fe109c306b97fdfb25f09182babad51a7/charset_normalizer-3.4.6-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:87725cfb1a4f1f8c2fc9890ae2f42094120f4b44db9360be5d99a4c6b0e03a9e", size = 225302, upload-time = "2026-03-15T18:52:21.043Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ba/f6/cd782923d112d296294dea4bcc7af5a7ae0f86ab79f8fefbda5526b6cfc0/charset_normalizer-3.4.6-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:fcce033e4021347d80ed9c66dcf1e7b1546319834b74445f561d2e2221de5659", size = 211127, upload-time = "2026-03-15T18:52:22.491Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/0e/c5/0b6898950627af7d6103a449b22320372c24c6feda91aa24e201a478d161/charset_normalizer-3.4.6-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:ca0276464d148c72defa8bb4390cce01b4a0e425f3b50d1435aa6d7a18107602", size = 222840, upload-time = "2026-03-15T18:52:24.113Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/7d/25/c4bba773bef442cbdc06111d40daa3de5050a676fa26e85090fc54dd12f0/charset_normalizer-3.4.6-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:197c1a244a274bb016dd8b79204850144ef77fe81c5b797dc389327adb552407", size = 216890, upload-time = "2026-03-15T18:52:25.541Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/35/1a/05dacadb0978da72ee287b0143097db12f2e7e8d3ffc4647da07a383b0b7/charset_normalizer-3.4.6-cp314-cp314t-win32.whl", hash = "sha256:2a24157fa36980478dd1770b585c0f30d19e18f4fb0c47c13aa568f871718579", size = 155379, upload-time = "2026-03-15T18:52:27.05Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/5d/7a/d269d834cb3a76291651256f3b9a5945e81d0a49ab9f4a498964e83c0416/charset_normalizer-3.4.6-cp314-cp314t-win_amd64.whl", hash = "sha256:cd5e2801c89992ed8c0a3f0293ae83c159a60d9a5d685005383ef4caca77f2c4", size = 169043, upload-time = "2026-03-15T18:52:28.502Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/23/06/28b29fba521a37a8932c6a84192175c34d49f84a6d4773fa63d05f9aff22/charset_normalizer-3.4.6-cp314-cp314t-win_arm64.whl", hash = "sha256:47955475ac79cc504ef2704b192364e51d0d473ad452caedd0002605f780101c", size = 148523, upload-time = "2026-03-15T18:52:29.956Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/2a/68/687187c7e26cb24ccbd88e5069f5ef00eba804d36dde11d99aad0838ab45/charset_normalizer-3.4.6-py3-none-any.whl", hash = "sha256:947cf925bc916d90adba35a64c82aace04fa39b46b52d4630ece166655905a69", size = 61455, upload-time = "2026-03-15T18:53:23.833Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "colorama"
|
||||
version = "0.4.6"
|
||||
@@ -97,6 +230,31 @@ wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "docutils"
|
||||
version = "0.21.2"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
resolution-markers = [
|
||||
"python_full_version < '3.11'",
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/ae/ed/aefcc8cd0ba62a0560c3c18c33925362d46c6075480bfa4df87b28e169a9/docutils-0.21.2.tar.gz", hash = "sha256:3a6b18732edf182daa3cd12775bbb338cf5691468f91eeeb109deff6ebfa986f", size = 2204444, upload-time = "2024-04-23T18:57:18.24Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/8f/d7/9322c609343d929e75e7e5e6255e614fcc67572cfd083959cdef3b7aad79/docutils-0.21.2-py3-none-any.whl", hash = "sha256:dafca5b9e384f0e419294eb4d2ff9fa826435bf15f15b7bd45723e8ad76811b2", size = 587408, upload-time = "2024-04-23T18:57:14.835Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "docutils"
|
||||
version = "0.22.4"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
resolution-markers = [
|
||||
"python_full_version >= '3.12'",
|
||||
"python_full_version == '3.11.*'",
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/ae/b6/03bb70946330e88ffec97aefd3ea75ba575cb2e762061e0e62a213befee8/docutils-0.22.4.tar.gz", hash = "sha256:4db53b1fde9abecbb74d91230d32ab626d94f6badfc575d6db9194a49df29968", size = 2291750, upload-time = "2025-12-18T19:00:26.443Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/02/10/5da547df7a391dcde17f59520a231527b8571e6f46fc8efb02ccb370ab12/docutils-0.22.4-py3-none-any.whl", hash = "sha256:d0013f540772d1420576855455d050a2180186c91c15779301ac2ccb3eeb68de", size = 633196, upload-time = "2025-12-18T19:00:18.077Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "exceptiongroup"
|
||||
version = "1.3.1"
|
||||
@@ -109,6 +267,24 @@ wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/8a/0e/97c33bf5009bdbac74fd2beace167cab3f978feb69cc36f1ef79360d6c4e/exceptiongroup-1.3.1-py3-none-any.whl", hash = "sha256:a7a39a3bd276781e98394987d3a5701d0c4edffb633bb7a5144577f82c773598", size = 16740, upload-time = "2025-11-21T23:01:53.443Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "idna"
|
||||
version = "3.11"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/6f/6d/0703ccc57f3a7233505399edb88de3cbd678da106337b9fcde432b65ed60/idna-3.11.tar.gz", hash = "sha256:795dafcc9c04ed0c1fb032c2aa73654d8e8c5023a7df64a53f39190ada629902", size = 194582, upload-time = "2025-10-12T14:55:20.501Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/0e/61/66938bbb5fc52dbdf84594873d5b51fb1f7c7794e9c0f5bd885f30bc507b/idna-3.11-py3-none-any.whl", hash = "sha256:771a87f49d9defaf64091e6e6fe9c18d4833f140bd19464795bc32d966ca37ea", size = 71008, upload-time = "2025-10-12T14:55:18.883Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "imagesize"
|
||||
version = "2.0.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/6c/e6/7bf14eeb8f8b7251141944835abd42eb20a658d89084b7e1f3e5fe394090/imagesize-2.0.0.tar.gz", hash = "sha256:8e8358c4a05c304f1fccf7ff96f036e7243a189e9e42e90851993c558cfe9ee3", size = 1773045, upload-time = "2026-03-03T14:18:29.941Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/5f/53/fb7122b71361a0d121b669dcf3d31244ef75badbbb724af388948de543e2/imagesize-2.0.0-py2.py3-none-any.whl", hash = "sha256:5667c5bbb57ab3f1fa4bc366f4fbc971db3d5ed011fd2715fd8001f782718d96", size = 9441, upload-time = "2026-03-03T14:18:27.892Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "iniconfig"
|
||||
version = "2.3.0"
|
||||
@@ -119,12 +295,195 @@ wheels = [
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "mpmath"
|
||||
version = "1.3.0"
|
||||
name = "jinja2"
|
||||
version = "3.1.6"
|
||||
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" }
|
||||
dependencies = [
|
||||
{ name = "markupsafe" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/df/bf/f7da0350254c0ed7c72f3e33cef02e048281fec7ecec5f032d4aac52226b/jinja2-3.1.6.tar.gz", hash = "sha256:0137fb05990d35f1275a587e9aee6d56da821fc83491a0fb838183be43f66d6d", size = 245115, upload-time = "2025-03-05T20:05:02.478Z" }
|
||||
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" },
|
||||
{ url = "https://files.pythonhosted.org/packages/62/a1/3d680cbfd5f4b8f15abc1d571870c5fc3e594bb582bc3b64ea099db13e56/jinja2-3.1.6-py3-none-any.whl", hash = "sha256:85ece4451f492d0c13c5dd7c13a64681a86afae63a5f347908daf103ce6d2f67", size = 134899, upload-time = "2025-03-05T20:05:00.369Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "markdown-it-py"
|
||||
version = "3.0.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
resolution-markers = [
|
||||
"python_full_version < '3.11'",
|
||||
]
|
||||
dependencies = [
|
||||
{ name = "mdurl", marker = "python_full_version < '3.11'" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/38/71/3b932df36c1a044d397a1f92d1cf91ee0a503d91e470cbd670aa66b07ed0/markdown-it-py-3.0.0.tar.gz", hash = "sha256:e3f60a94fa066dc52ec76661e37c851cb232d92f9886b15cb560aaada2df8feb", size = 74596, upload-time = "2023-06-03T06:41:14.443Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/42/d7/1ec15b46af6af88f19b8e5ffea08fa375d433c998b8a7639e76935c14f1f/markdown_it_py-3.0.0-py3-none-any.whl", hash = "sha256:355216845c60bd96232cd8d8c40e8f9765cc86f46880e43a8fd22dc1a1a8cab1", size = 87528, upload-time = "2023-06-03T06:41:11.019Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "markdown-it-py"
|
||||
version = "4.0.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
resolution-markers = [
|
||||
"python_full_version >= '3.12'",
|
||||
"python_full_version == '3.11.*'",
|
||||
]
|
||||
dependencies = [
|
||||
{ name = "mdurl", marker = "python_full_version >= '3.11'" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/5b/f5/4ec618ed16cc4f8fb3b701563655a69816155e79e24a17b651541804721d/markdown_it_py-4.0.0.tar.gz", hash = "sha256:cb0a2b4aa34f932c007117b194e945bd74e0ec24133ceb5bac59009cda1cb9f3", size = 73070, upload-time = "2025-08-11T12:57:52.854Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/94/54/e7d793b573f298e1c9013b8c4dade17d481164aa517d1d7148619c2cedbf/markdown_it_py-4.0.0-py3-none-any.whl", hash = "sha256:87327c59b172c5011896038353a81343b6754500a08cd7a4973bb48c6d578147", size = 87321, upload-time = "2025-08-11T12:57:51.923Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "markupsafe"
|
||||
version = "3.0.3"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/7e/99/7690b6d4034fffd95959cbe0c02de8deb3098cc577c67bb6a24fe5d7caa7/markupsafe-3.0.3.tar.gz", hash = "sha256:722695808f4b6457b320fdc131280796bdceb04ab50fe1795cd540799ebe1698", size = 80313, upload-time = "2025-09-27T18:37:40.426Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/e8/4b/3541d44f3937ba468b75da9eebcae497dcf67adb65caa16760b0a6807ebb/markupsafe-3.0.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:2f981d352f04553a7171b8e44369f2af4055f888dfb147d55e42d29e29e74559", size = 11631, upload-time = "2025-09-27T18:36:05.558Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/98/1b/fbd8eed11021cabd9226c37342fa6ca4e8a98d8188a8d9b66740494960e4/markupsafe-3.0.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:e1c1493fb6e50ab01d20a22826e57520f1284df32f2d8601fdd90b6304601419", size = 12057, upload-time = "2025-09-27T18:36:07.165Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/40/01/e560d658dc0bb8ab762670ece35281dec7b6c1b33f5fbc09ebb57a185519/markupsafe-3.0.3-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1ba88449deb3de88bd40044603fafffb7bc2b055d626a330323a9ed736661695", size = 22050, upload-time = "2025-09-27T18:36:08.005Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/af/cd/ce6e848bbf2c32314c9b237839119c5a564a59725b53157c856e90937b7a/markupsafe-3.0.3-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f42d0984e947b8adf7dd6dde396e720934d12c506ce84eea8476409563607591", size = 20681, upload-time = "2025-09-27T18:36:08.881Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c9/2a/b5c12c809f1c3045c4d580b035a743d12fcde53cf685dbc44660826308da/markupsafe-3.0.3-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:c0c0b3ade1c0b13b936d7970b1d37a57acde9199dc2aecc4c336773e1d86049c", size = 20705, upload-time = "2025-09-27T18:36:10.131Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/cf/e3/9427a68c82728d0a88c50f890d0fc072a1484de2f3ac1ad0bfc1a7214fd5/markupsafe-3.0.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:0303439a41979d9e74d18ff5e2dd8c43ed6c6001fd40e5bf2e43f7bd9bbc523f", size = 21524, upload-time = "2025-09-27T18:36:11.324Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/bc/36/23578f29e9e582a4d0278e009b38081dbe363c5e7165113fad546918a232/markupsafe-3.0.3-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:d2ee202e79d8ed691ceebae8e0486bd9a2cd4794cec4824e1c99b6f5009502f6", size = 20282, upload-time = "2025-09-27T18:36:12.573Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/56/21/dca11354e756ebd03e036bd8ad58d6d7168c80ce1fe5e75218e4945cbab7/markupsafe-3.0.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:177b5253b2834fe3678cb4a5f0059808258584c559193998be2601324fdeafb1", size = 20745, upload-time = "2025-09-27T18:36:13.504Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/87/99/faba9369a7ad6e4d10b6a5fbf71fa2a188fe4a593b15f0963b73859a1bbd/markupsafe-3.0.3-cp310-cp310-win32.whl", hash = "sha256:2a15a08b17dd94c53a1da0438822d70ebcd13f8c3a95abe3a9ef9f11a94830aa", size = 14571, upload-time = "2025-09-27T18:36:14.779Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d6/25/55dc3ab959917602c96985cb1253efaa4ff42f71194bddeb61eb7278b8be/markupsafe-3.0.3-cp310-cp310-win_amd64.whl", hash = "sha256:c4ffb7ebf07cfe8931028e3e4c85f0357459a3f9f9490886198848f4fa002ec8", size = 15056, upload-time = "2025-09-27T18:36:16.125Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d0/9e/0a02226640c255d1da0b8d12e24ac2aa6734da68bff14c05dd53b94a0fc3/markupsafe-3.0.3-cp310-cp310-win_arm64.whl", hash = "sha256:e2103a929dfa2fcaf9bb4e7c091983a49c9ac3b19c9061b6d5427dd7d14d81a1", size = 13932, upload-time = "2025-09-27T18:36:17.311Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/08/db/fefacb2136439fc8dd20e797950e749aa1f4997ed584c62cfb8ef7c2be0e/markupsafe-3.0.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:1cc7ea17a6824959616c525620e387f6dd30fec8cb44f649e31712db02123dad", size = 11631, upload-time = "2025-09-27T18:36:18.185Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e1/2e/5898933336b61975ce9dc04decbc0a7f2fee78c30353c5efba7f2d6ff27a/markupsafe-3.0.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:4bd4cd07944443f5a265608cc6aab442e4f74dff8088b0dfc8238647b8f6ae9a", size = 12058, upload-time = "2025-09-27T18:36:19.444Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/1d/09/adf2df3699d87d1d8184038df46a9c80d78c0148492323f4693df54e17bb/markupsafe-3.0.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6b5420a1d9450023228968e7e6a9ce57f65d148ab56d2313fcd589eee96a7a50", size = 24287, upload-time = "2025-09-27T18:36:20.768Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/30/ac/0273f6fcb5f42e314c6d8cd99effae6a5354604d461b8d392b5ec9530a54/markupsafe-3.0.3-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0bf2a864d67e76e5c9a34dc26ec616a66b9888e25e7b9460e1c76d3293bd9dbf", size = 22940, upload-time = "2025-09-27T18:36:22.249Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/19/ae/31c1be199ef767124c042c6c3e904da327a2f7f0cd63a0337e1eca2967a8/markupsafe-3.0.3-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:bc51efed119bc9cfdf792cdeaa4d67e8f6fcccab66ed4bfdd6bde3e59bfcbb2f", size = 21887, upload-time = "2025-09-27T18:36:23.535Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b2/76/7edcab99d5349a4532a459e1fe64f0b0467a3365056ae550d3bcf3f79e1e/markupsafe-3.0.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:068f375c472b3e7acbe2d5318dea141359e6900156b5b2ba06a30b169086b91a", size = 23692, upload-time = "2025-09-27T18:36:24.823Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a4/28/6e74cdd26d7514849143d69f0bf2399f929c37dc2b31e6829fd2045b2765/markupsafe-3.0.3-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:7be7b61bb172e1ed687f1754f8e7484f1c8019780f6f6b0786e76bb01c2ae115", size = 21471, upload-time = "2025-09-27T18:36:25.95Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/62/7e/a145f36a5c2945673e590850a6f8014318d5577ed7e5920a4b3448e0865d/markupsafe-3.0.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:f9e130248f4462aaa8e2552d547f36ddadbeaa573879158d721bbd33dfe4743a", size = 22923, upload-time = "2025-09-27T18:36:27.109Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/0f/62/d9c46a7f5c9adbeeeda52f5b8d802e1094e9717705a645efc71b0913a0a8/markupsafe-3.0.3-cp311-cp311-win32.whl", hash = "sha256:0db14f5dafddbb6d9208827849fad01f1a2609380add406671a26386cdf15a19", size = 14572, upload-time = "2025-09-27T18:36:28.045Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/83/8a/4414c03d3f891739326e1783338e48fb49781cc915b2e0ee052aa490d586/markupsafe-3.0.3-cp311-cp311-win_amd64.whl", hash = "sha256:de8a88e63464af587c950061a5e6a67d3632e36df62b986892331d4620a35c01", size = 15077, upload-time = "2025-09-27T18:36:29.025Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/35/73/893072b42e6862f319b5207adc9ae06070f095b358655f077f69a35601f0/markupsafe-3.0.3-cp311-cp311-win_arm64.whl", hash = "sha256:3b562dd9e9ea93f13d53989d23a7e775fdfd1066c33494ff43f5418bc8c58a5c", size = 13876, upload-time = "2025-09-27T18:36:29.954Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/5a/72/147da192e38635ada20e0a2e1a51cf8823d2119ce8883f7053879c2199b5/markupsafe-3.0.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:d53197da72cc091b024dd97249dfc7794d6a56530370992a5e1a08983ad9230e", size = 11615, upload-time = "2025-09-27T18:36:30.854Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/9a/81/7e4e08678a1f98521201c3079f77db69fb552acd56067661f8c2f534a718/markupsafe-3.0.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:1872df69a4de6aead3491198eaf13810b565bdbeec3ae2dc8780f14458ec73ce", size = 12020, upload-time = "2025-09-27T18:36:31.971Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/1e/2c/799f4742efc39633a1b54a92eec4082e4f815314869865d876824c257c1e/markupsafe-3.0.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3a7e8ae81ae39e62a41ec302f972ba6ae23a5c5396c8e60113e9066ef893da0d", size = 24332, upload-time = "2025-09-27T18:36:32.813Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/3c/2e/8d0c2ab90a8c1d9a24f0399058ab8519a3279d1bd4289511d74e909f060e/markupsafe-3.0.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d6dd0be5b5b189d31db7cda48b91d7e0a9795f31430b7f271219ab30f1d3ac9d", size = 22947, upload-time = "2025-09-27T18:36:33.86Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/2c/54/887f3092a85238093a0b2154bd629c89444f395618842e8b0c41783898ea/markupsafe-3.0.3-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:94c6f0bb423f739146aec64595853541634bde58b2135f27f61c1ffd1cd4d16a", size = 21962, upload-time = "2025-09-27T18:36:35.099Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c9/2f/336b8c7b6f4a4d95e91119dc8521402461b74a485558d8f238a68312f11c/markupsafe-3.0.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:be8813b57049a7dc738189df53d69395eba14fb99345e0a5994914a3864c8a4b", size = 23760, upload-time = "2025-09-27T18:36:36.001Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/32/43/67935f2b7e4982ffb50a4d169b724d74b62a3964bc1a9a527f5ac4f1ee2b/markupsafe-3.0.3-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:83891d0e9fb81a825d9a6d61e3f07550ca70a076484292a70fde82c4b807286f", size = 21529, upload-time = "2025-09-27T18:36:36.906Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/89/e0/4486f11e51bbba8b0c041098859e869e304d1c261e59244baa3d295d47b7/markupsafe-3.0.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:77f0643abe7495da77fb436f50f8dab76dbc6e5fd25d39589a0f1fe6548bfa2b", size = 23015, upload-time = "2025-09-27T18:36:37.868Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/2f/e1/78ee7a023dac597a5825441ebd17170785a9dab23de95d2c7508ade94e0e/markupsafe-3.0.3-cp312-cp312-win32.whl", hash = "sha256:d88b440e37a16e651bda4c7c2b930eb586fd15ca7406cb39e211fcff3bf3017d", size = 14540, upload-time = "2025-09-27T18:36:38.761Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/aa/5b/bec5aa9bbbb2c946ca2733ef9c4ca91c91b6a24580193e891b5f7dbe8e1e/markupsafe-3.0.3-cp312-cp312-win_amd64.whl", hash = "sha256:26a5784ded40c9e318cfc2bdb30fe164bdb8665ded9cd64d500a34fb42067b1c", size = 15105, upload-time = "2025-09-27T18:36:39.701Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e5/f1/216fc1bbfd74011693a4fd837e7026152e89c4bcf3e77b6692fba9923123/markupsafe-3.0.3-cp312-cp312-win_arm64.whl", hash = "sha256:35add3b638a5d900e807944a078b51922212fb3dedb01633a8defc4b01a3c85f", size = 13906, upload-time = "2025-09-27T18:36:40.689Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/38/2f/907b9c7bbba283e68f20259574b13d005c121a0fa4c175f9bed27c4597ff/markupsafe-3.0.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:e1cf1972137e83c5d4c136c43ced9ac51d0e124706ee1c8aa8532c1287fa8795", size = 11622, upload-time = "2025-09-27T18:36:41.777Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/9c/d9/5f7756922cdd676869eca1c4e3c0cd0df60ed30199ffd775e319089cb3ed/markupsafe-3.0.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:116bb52f642a37c115f517494ea5feb03889e04df47eeff5b130b1808ce7c219", size = 12029, upload-time = "2025-09-27T18:36:43.257Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/00/07/575a68c754943058c78f30db02ee03a64b3c638586fba6a6dd56830b30a3/markupsafe-3.0.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:133a43e73a802c5562be9bbcd03d090aa5a1fe899db609c29e8c8d815c5f6de6", size = 24374, upload-time = "2025-09-27T18:36:44.508Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a9/21/9b05698b46f218fc0e118e1f8168395c65c8a2c750ae2bab54fc4bd4e0e8/markupsafe-3.0.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ccfcd093f13f0f0b7fdd0f198b90053bf7b2f02a3927a30e63f3ccc9df56b676", size = 22980, upload-time = "2025-09-27T18:36:45.385Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/7f/71/544260864f893f18b6827315b988c146b559391e6e7e8f7252839b1b846a/markupsafe-3.0.3-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:509fa21c6deb7a7a273d629cf5ec029bc209d1a51178615ddf718f5918992ab9", size = 21990, upload-time = "2025-09-27T18:36:46.916Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c2/28/b50fc2f74d1ad761af2f5dcce7492648b983d00a65b8c0e0cb457c82ebbe/markupsafe-3.0.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:a4afe79fb3de0b7097d81da19090f4df4f8d3a2b3adaa8764138aac2e44f3af1", size = 23784, upload-time = "2025-09-27T18:36:47.884Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ed/76/104b2aa106a208da8b17a2fb72e033a5a9d7073c68f7e508b94916ed47a9/markupsafe-3.0.3-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:795e7751525cae078558e679d646ae45574b47ed6e7771863fcc079a6171a0fc", size = 21588, upload-time = "2025-09-27T18:36:48.82Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b5/99/16a5eb2d140087ebd97180d95249b00a03aa87e29cc224056274f2e45fd6/markupsafe-3.0.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:8485f406a96febb5140bfeca44a73e3ce5116b2501ac54fe953e488fb1d03b12", size = 23041, upload-time = "2025-09-27T18:36:49.797Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/19/bc/e7140ed90c5d61d77cea142eed9f9c303f4c4806f60a1044c13e3f1471d0/markupsafe-3.0.3-cp313-cp313-win32.whl", hash = "sha256:bdd37121970bfd8be76c5fb069c7751683bdf373db1ed6c010162b2a130248ed", size = 14543, upload-time = "2025-09-27T18:36:51.584Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/05/73/c4abe620b841b6b791f2edc248f556900667a5a1cf023a6646967ae98335/markupsafe-3.0.3-cp313-cp313-win_amd64.whl", hash = "sha256:9a1abfdc021a164803f4d485104931fb8f8c1efd55bc6b748d2f5774e78b62c5", size = 15113, upload-time = "2025-09-27T18:36:52.537Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f0/3a/fa34a0f7cfef23cf9500d68cb7c32dd64ffd58a12b09225fb03dd37d5b80/markupsafe-3.0.3-cp313-cp313-win_arm64.whl", hash = "sha256:7e68f88e5b8799aa49c85cd116c932a1ac15caaa3f5db09087854d218359e485", size = 13911, upload-time = "2025-09-27T18:36:53.513Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e4/d7/e05cd7efe43a88a17a37b3ae96e79a19e846f3f456fe79c57ca61356ef01/markupsafe-3.0.3-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:218551f6df4868a8d527e3062d0fb968682fe92054e89978594c28e642c43a73", size = 11658, upload-time = "2025-09-27T18:36:54.819Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/99/9e/e412117548182ce2148bdeacdda3bb494260c0b0184360fe0d56389b523b/markupsafe-3.0.3-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:3524b778fe5cfb3452a09d31e7b5adefeea8c5be1d43c4f810ba09f2ceb29d37", size = 12066, upload-time = "2025-09-27T18:36:55.714Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/bc/e6/fa0ffcda717ef64a5108eaa7b4f5ed28d56122c9a6d70ab8b72f9f715c80/markupsafe-3.0.3-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4e885a3d1efa2eadc93c894a21770e4bc67899e3543680313b09f139e149ab19", size = 25639, upload-time = "2025-09-27T18:36:56.908Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/96/ec/2102e881fe9d25fc16cb4b25d5f5cde50970967ffa5dddafdb771237062d/markupsafe-3.0.3-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8709b08f4a89aa7586de0aadc8da56180242ee0ada3999749b183aa23df95025", size = 23569, upload-time = "2025-09-27T18:36:57.913Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/4b/30/6f2fce1f1f205fc9323255b216ca8a235b15860c34b6798f810f05828e32/markupsafe-3.0.3-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:b8512a91625c9b3da6f127803b166b629725e68af71f8184ae7e7d54686a56d6", size = 23284, upload-time = "2025-09-27T18:36:58.833Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/58/47/4a0ccea4ab9f5dcb6f79c0236d954acb382202721e704223a8aafa38b5c8/markupsafe-3.0.3-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:9b79b7a16f7fedff2495d684f2b59b0457c3b493778c9eed31111be64d58279f", size = 24801, upload-time = "2025-09-27T18:36:59.739Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/6a/70/3780e9b72180b6fecb83a4814d84c3bf4b4ae4bf0b19c27196104149734c/markupsafe-3.0.3-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:12c63dfb4a98206f045aa9563db46507995f7ef6d83b2f68eda65c307c6829eb", size = 22769, upload-time = "2025-09-27T18:37:00.719Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/98/c5/c03c7f4125180fc215220c035beac6b9cb684bc7a067c84fc69414d315f5/markupsafe-3.0.3-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:8f71bc33915be5186016f675cd83a1e08523649b0e33efdb898db577ef5bb009", size = 23642, upload-time = "2025-09-27T18:37:01.673Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/80/d6/2d1b89f6ca4bff1036499b1e29a1d02d282259f3681540e16563f27ebc23/markupsafe-3.0.3-cp313-cp313t-win32.whl", hash = "sha256:69c0b73548bc525c8cb9a251cddf1931d1db4d2258e9599c28c07ef3580ef354", size = 14612, upload-time = "2025-09-27T18:37:02.639Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/2b/98/e48a4bfba0a0ffcf9925fe2d69240bfaa19c6f7507b8cd09c70684a53c1e/markupsafe-3.0.3-cp313-cp313t-win_amd64.whl", hash = "sha256:1b4b79e8ebf6b55351f0d91fe80f893b4743f104bff22e90697db1590e47a218", size = 15200, upload-time = "2025-09-27T18:37:03.582Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/0e/72/e3cc540f351f316e9ed0f092757459afbc595824ca724cbc5a5d4263713f/markupsafe-3.0.3-cp313-cp313t-win_arm64.whl", hash = "sha256:ad2cf8aa28b8c020ab2fc8287b0f823d0a7d8630784c31e9ee5edea20f406287", size = 13973, upload-time = "2025-09-27T18:37:04.929Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/33/8a/8e42d4838cd89b7dde187011e97fe6c3af66d8c044997d2183fbd6d31352/markupsafe-3.0.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:eaa9599de571d72e2daf60164784109f19978b327a3910d3e9de8c97b5b70cfe", size = 11619, upload-time = "2025-09-27T18:37:06.342Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b5/64/7660f8a4a8e53c924d0fa05dc3a55c9cee10bbd82b11c5afb27d44b096ce/markupsafe-3.0.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:c47a551199eb8eb2121d4f0f15ae0f923d31350ab9280078d1e5f12b249e0026", size = 12029, upload-time = "2025-09-27T18:37:07.213Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/da/ef/e648bfd021127bef5fa12e1720ffed0c6cbb8310c8d9bea7266337ff06de/markupsafe-3.0.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f34c41761022dd093b4b6896d4810782ffbabe30f2d443ff5f083e0cbbb8c737", size = 24408, upload-time = "2025-09-27T18:37:09.572Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/41/3c/a36c2450754618e62008bf7435ccb0f88053e07592e6028a34776213d877/markupsafe-3.0.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:457a69a9577064c05a97c41f4e65148652db078a3a509039e64d3467b9e7ef97", size = 23005, upload-time = "2025-09-27T18:37:10.58Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/bc/20/b7fdf89a8456b099837cd1dc21974632a02a999ec9bf7ca3e490aacd98e7/markupsafe-3.0.3-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e8afc3f2ccfa24215f8cb28dcf43f0113ac3c37c2f0f0806d8c70e4228c5cf4d", size = 22048, upload-time = "2025-09-27T18:37:11.547Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/9a/a7/591f592afdc734f47db08a75793a55d7fbcc6902a723ae4cfbab61010cc5/markupsafe-3.0.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:ec15a59cf5af7be74194f7ab02d0f59a62bdcf1a537677ce67a2537c9b87fcda", size = 23821, upload-time = "2025-09-27T18:37:12.48Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/7d/33/45b24e4f44195b26521bc6f1a82197118f74df348556594bd2262bda1038/markupsafe-3.0.3-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:0eb9ff8191e8498cca014656ae6b8d61f39da5f95b488805da4bb029cccbfbaf", size = 21606, upload-time = "2025-09-27T18:37:13.485Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ff/0e/53dfaca23a69fbfbbf17a4b64072090e70717344c52eaaaa9c5ddff1e5f0/markupsafe-3.0.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:2713baf880df847f2bece4230d4d094280f4e67b1e813eec43b4c0e144a34ffe", size = 23043, upload-time = "2025-09-27T18:37:14.408Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/46/11/f333a06fc16236d5238bfe74daccbca41459dcd8d1fa952e8fbd5dccfb70/markupsafe-3.0.3-cp314-cp314-win32.whl", hash = "sha256:729586769a26dbceff69f7a7dbbf59ab6572b99d94576a5592625d5b411576b9", size = 14747, upload-time = "2025-09-27T18:37:15.36Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/28/52/182836104b33b444e400b14f797212f720cbc9ed6ba34c800639d154e821/markupsafe-3.0.3-cp314-cp314-win_amd64.whl", hash = "sha256:bdc919ead48f234740ad807933cdf545180bfbe9342c2bb451556db2ed958581", size = 15341, upload-time = "2025-09-27T18:37:16.496Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/6f/18/acf23e91bd94fd7b3031558b1f013adfa21a8e407a3fdb32745538730382/markupsafe-3.0.3-cp314-cp314-win_arm64.whl", hash = "sha256:5a7d5dc5140555cf21a6fefbdbf8723f06fcd2f63ef108f2854de715e4422cb4", size = 14073, upload-time = "2025-09-27T18:37:17.476Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/3c/f0/57689aa4076e1b43b15fdfa646b04653969d50cf30c32a102762be2485da/markupsafe-3.0.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:1353ef0c1b138e1907ae78e2f6c63ff67501122006b0f9abad68fda5f4ffc6ab", size = 11661, upload-time = "2025-09-27T18:37:18.453Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/89/c3/2e67a7ca217c6912985ec766c6393b636fb0c2344443ff9d91404dc4c79f/markupsafe-3.0.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:1085e7fbddd3be5f89cc898938f42c0b3c711fdcb37d75221de2666af647c175", size = 12069, upload-time = "2025-09-27T18:37:19.332Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f0/00/be561dce4e6ca66b15276e184ce4b8aec61fe83662cce2f7d72bd3249d28/markupsafe-3.0.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1b52b4fb9df4eb9ae465f8d0c228a00624de2334f216f178a995ccdcf82c4634", size = 25670, upload-time = "2025-09-27T18:37:20.245Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/50/09/c419f6f5a92e5fadde27efd190eca90f05e1261b10dbd8cbcb39cd8ea1dc/markupsafe-3.0.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:fed51ac40f757d41b7c48425901843666a6677e3e8eb0abcff09e4ba6e664f50", size = 23598, upload-time = "2025-09-27T18:37:21.177Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/22/44/a0681611106e0b2921b3033fc19bc53323e0b50bc70cffdd19f7d679bb66/markupsafe-3.0.3-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:f190daf01f13c72eac4efd5c430a8de82489d9cff23c364c3ea822545032993e", size = 23261, upload-time = "2025-09-27T18:37:22.167Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/5f/57/1b0b3f100259dc9fffe780cfb60d4be71375510e435efec3d116b6436d43/markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:e56b7d45a839a697b5eb268c82a71bd8c7f6c94d6fd50c3d577fa39a9f1409f5", size = 24835, upload-time = "2025-09-27T18:37:23.296Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/26/6a/4bf6d0c97c4920f1597cc14dd720705eca0bf7c787aebc6bb4d1bead5388/markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:f3e98bb3798ead92273dc0e5fd0f31ade220f59a266ffd8a4f6065e0a3ce0523", size = 22733, upload-time = "2025-09-27T18:37:24.237Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/14/c7/ca723101509b518797fedc2fdf79ba57f886b4aca8a7d31857ba3ee8281f/markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:5678211cb9333a6468fb8d8be0305520aa073f50d17f089b5b4b477ea6e67fdc", size = 23672, upload-time = "2025-09-27T18:37:25.271Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/fb/df/5bd7a48c256faecd1d36edc13133e51397e41b73bb77e1a69deab746ebac/markupsafe-3.0.3-cp314-cp314t-win32.whl", hash = "sha256:915c04ba3851909ce68ccc2b8e2cd691618c4dc4c4232fb7982bca3f41fd8c3d", size = 14819, upload-time = "2025-09-27T18:37:26.285Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/1a/8a/0402ba61a2f16038b48b39bccca271134be00c5c9f0f623208399333c448/markupsafe-3.0.3-cp314-cp314t-win_amd64.whl", hash = "sha256:4faffd047e07c38848ce017e8725090413cd80cbc23d86e55c587bf979e579c9", size = 15426, upload-time = "2025-09-27T18:37:27.316Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/70/bc/6f1c2f612465f5fa89b95bead1f44dcb607670fd42891d8fdcd5d039f4f4/markupsafe-3.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:32001d6a8fc98c8cb5c947787c5d08b0a50663d139f1305bac5885d98d9b40fa", size = 14146, upload-time = "2025-09-27T18:37:28.327Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "mdit-py-plugins"
|
||||
version = "0.5.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "markdown-it-py", version = "3.0.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" },
|
||||
{ name = "markdown-it-py", version = "4.0.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/b2/fd/a756d36c0bfba5f6e39a1cdbdbfdd448dc02692467d83816dff4592a1ebc/mdit_py_plugins-0.5.0.tar.gz", hash = "sha256:f4918cb50119f50446560513a8e311d574ff6aaed72606ddae6d35716fe809c6", size = 44655, upload-time = "2025-08-11T07:25:49.083Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/fb/86/dd6e5db36df29e76c7a7699123569a4a18c1623ce68d826ed96c62643cae/mdit_py_plugins-0.5.0-py3-none-any.whl", hash = "sha256:07a08422fc1936a5d26d146759e9155ea466e842f5ab2f7d2266dd084c8dab1f", size = 57205, upload-time = "2025-08-11T07:25:47.597Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "mdurl"
|
||||
version = "0.1.2"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/d6/54/cfe61301667036ec958cb99bd3efefba235e65cdeb9c84d24a8293ba1d90/mdurl-0.1.2.tar.gz", hash = "sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba", size = 8729, upload-time = "2022-08-14T12:40:10.846Z" }
|
||||
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 = "myst-parser"
|
||||
version = "4.0.1"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
resolution-markers = [
|
||||
"python_full_version < '3.11'",
|
||||
]
|
||||
dependencies = [
|
||||
{ name = "docutils", version = "0.21.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" },
|
||||
{ name = "jinja2", marker = "python_full_version < '3.11'" },
|
||||
{ name = "markdown-it-py", version = "3.0.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" },
|
||||
{ name = "mdit-py-plugins", marker = "python_full_version < '3.11'" },
|
||||
{ name = "pyyaml", marker = "python_full_version < '3.11'" },
|
||||
{ name = "sphinx", version = "8.1.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/66/a5/9626ba4f73555b3735ad86247a8077d4603aa8628537687c839ab08bfe44/myst_parser-4.0.1.tar.gz", hash = "sha256:5cfea715e4f3574138aecbf7d54132296bfd72bb614d31168f48c477a830a7c4", size = 93985, upload-time = "2025-02-12T10:53:03.833Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/5f/df/76d0321c3797b54b60fef9ec3bd6f4cfd124b9e422182156a1dd418722cf/myst_parser-4.0.1-py3-none-any.whl", hash = "sha256:9134e88959ec3b5780aedf8a99680ea242869d012e8821db3126d427edc9c95d", size = 84579, upload-time = "2025-02-12T10:53:02.078Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "myst-parser"
|
||||
version = "5.0.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
resolution-markers = [
|
||||
"python_full_version >= '3.12'",
|
||||
"python_full_version == '3.11.*'",
|
||||
]
|
||||
dependencies = [
|
||||
{ name = "docutils", version = "0.22.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" },
|
||||
{ name = "jinja2", marker = "python_full_version >= '3.11'" },
|
||||
{ name = "markdown-it-py", version = "4.0.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" },
|
||||
{ name = "mdit-py-plugins", marker = "python_full_version >= '3.11'" },
|
||||
{ name = "pyyaml", marker = "python_full_version >= '3.11'" },
|
||||
{ name = "sphinx", version = "9.0.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.11.*'" },
|
||||
{ name = "sphinx", version = "9.1.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.12'" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/33/fa/7b45eef11b7971f0beb29d27b7bfe0d747d063aa29e170d9edd004733c8a/myst_parser-5.0.0.tar.gz", hash = "sha256:f6f231452c56e8baa662cc352c548158f6a16fcbd6e3800fc594978002b94f3a", size = 98535, upload-time = "2026-01-15T09:08:18.036Z" }
|
||||
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]]
|
||||
@@ -205,7 +564,8 @@ name = "numpy"
|
||||
version = "2.4.3"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
resolution-markers = [
|
||||
"python_full_version >= '3.11'",
|
||||
"python_full_version >= '3.12'",
|
||||
"python_full_version == '3.11.*'",
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/10/8b/c265f4823726ab832de836cdd184d0986dcf94480f81e8739692a7ac7af2/numpy-2.4.3.tar.gz", hash = "sha256:483a201202b73495f00dbc83796c6ae63137a9bdade074f7648b3e32613412dd", size = 20727743, upload-time = "2026-03-09T07:58:53.426Z" }
|
||||
wheels = [
|
||||
@@ -338,11 +698,10 @@ wheels = [
|
||||
|
||||
[[package]]
|
||||
name = "pytheory"
|
||||
version = "0.2.0"
|
||||
source = { virtual = "." }
|
||||
version = "0.36.4"
|
||||
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" },
|
||||
@@ -352,29 +711,114 @@ dependencies = [
|
||||
dev = [
|
||||
{ name = "pytest" },
|
||||
]
|
||||
docs = [
|
||||
{ name = "myst-parser", version = "4.0.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" },
|
||||
{ name = "myst-parser", version = "5.0.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" },
|
||||
{ name = "sphinx", version = "8.1.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" },
|
||||
{ name = "sphinx", version = "9.0.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.11.*'" },
|
||||
{ name = "sphinx", version = "9.1.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.12'" },
|
||||
]
|
||||
|
||||
[package.metadata]
|
||||
requires-dist = [
|
||||
{ name = "numeral" },
|
||||
{ name = "pytuning" },
|
||||
{ name = "scipy" },
|
||||
{ name = "sounddevice" },
|
||||
]
|
||||
|
||||
[package.metadata.requires-dev]
|
||||
dev = [{ name = "pytest" }]
|
||||
docs = [
|
||||
{ name = "myst-parser" },
|
||||
{ name = "sphinx" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pytuning"
|
||||
version = "0.7.3"
|
||||
name = "pyyaml"
|
||||
version = "6.0.3"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/05/8e/961c0007c59b8dd7729d542c61a4d537767a59645b82a0b521206e1e25c2/pyyaml-6.0.3.tar.gz", hash = "sha256:d76623373421df22fb4cf8817020cbb7ef15c725b9d5e45f17e189bfc384190f", size = 130960, upload-time = "2025-09-25T21:33:16.546Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/f4/a0/39350dd17dd6d6c6507025c0e53aef67a9293a6d37d3511f23ea510d5800/pyyaml-6.0.3-cp310-cp310-macosx_10_13_x86_64.whl", hash = "sha256:214ed4befebe12df36bcc8bc2b64b396ca31be9304b8f59e25c11cf94a4c033b", size = 184227, upload-time = "2025-09-25T21:31:46.04Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/05/14/52d505b5c59ce73244f59c7a50ecf47093ce4765f116cdb98286a71eeca2/pyyaml-6.0.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:02ea2dfa234451bbb8772601d7b8e426c2bfa197136796224e50e35a78777956", size = 174019, upload-time = "2025-09-25T21:31:47.706Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/43/f7/0e6a5ae5599c838c696adb4e6330a59f463265bfa1e116cfd1fbb0abaaae/pyyaml-6.0.3-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b30236e45cf30d2b8e7b3e85881719e98507abed1011bf463a8fa23e9c3e98a8", size = 740646, upload-time = "2025-09-25T21:31:49.21Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/2f/3a/61b9db1d28f00f8fd0ae760459a5c4bf1b941baf714e207b6eb0657d2578/pyyaml-6.0.3-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:66291b10affd76d76f54fad28e22e51719ef9ba22b29e1d7d03d6777a9174198", size = 840793, upload-time = "2025-09-25T21:31:50.735Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/7a/1e/7acc4f0e74c4b3d9531e24739e0ab832a5edf40e64fbae1a9c01941cabd7/pyyaml-6.0.3-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9c7708761fccb9397fe64bbc0395abcae8c4bf7b0eac081e12b809bf47700d0b", size = 770293, upload-time = "2025-09-25T21:31:51.828Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/8b/ef/abd085f06853af0cd59fa5f913d61a8eab65d7639ff2a658d18a25d6a89d/pyyaml-6.0.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:418cf3f2111bc80e0933b2cd8cd04f286338bb88bdc7bc8e6dd775ebde60b5e0", size = 732872, upload-time = "2025-09-25T21:31:53.282Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/1f/15/2bc9c8faf6450a8b3c9fc5448ed869c599c0a74ba2669772b1f3a0040180/pyyaml-6.0.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:5e0b74767e5f8c593e8c9b5912019159ed0533c70051e9cce3e8b6aa699fcd69", size = 758828, upload-time = "2025-09-25T21:31:54.807Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a3/00/531e92e88c00f4333ce359e50c19b8d1de9fe8d581b1534e35ccfbc5f393/pyyaml-6.0.3-cp310-cp310-win32.whl", hash = "sha256:28c8d926f98f432f88adc23edf2e6d4921ac26fb084b028c733d01868d19007e", size = 142415, upload-time = "2025-09-25T21:31:55.885Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/2a/fa/926c003379b19fca39dd4634818b00dec6c62d87faf628d1394e137354d4/pyyaml-6.0.3-cp310-cp310-win_amd64.whl", hash = "sha256:bdb2c67c6c1390b63c6ff89f210c8fd09d9a1217a465701eac7316313c915e4c", size = 158561, upload-time = "2025-09-25T21:31:57.406Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/6d/16/a95b6757765b7b031c9374925bb718d55e0a9ba8a1b6a12d25962ea44347/pyyaml-6.0.3-cp311-cp311-macosx_10_13_x86_64.whl", hash = "sha256:44edc647873928551a01e7a563d7452ccdebee747728c1080d881d68af7b997e", size = 185826, upload-time = "2025-09-25T21:31:58.655Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/16/19/13de8e4377ed53079ee996e1ab0a9c33ec2faf808a4647b7b4c0d46dd239/pyyaml-6.0.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:652cb6edd41e718550aad172851962662ff2681490a8a711af6a4d288dd96824", size = 175577, upload-time = "2025-09-25T21:32:00.088Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/0c/62/d2eb46264d4b157dae1275b573017abec435397aa59cbcdab6fc978a8af4/pyyaml-6.0.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:10892704fc220243f5305762e276552a0395f7beb4dbf9b14ec8fd43b57f126c", size = 775556, upload-time = "2025-09-25T21:32:01.31Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/10/cb/16c3f2cf3266edd25aaa00d6c4350381c8b012ed6f5276675b9eba8d9ff4/pyyaml-6.0.3-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:850774a7879607d3a6f50d36d04f00ee69e7fc816450e5f7e58d7f17f1ae5c00", size = 882114, upload-time = "2025-09-25T21:32:03.376Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/71/60/917329f640924b18ff085ab889a11c763e0b573da888e8404ff486657602/pyyaml-6.0.3-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b8bb0864c5a28024fac8a632c443c87c5aa6f215c0b126c449ae1a150412f31d", size = 806638, upload-time = "2025-09-25T21:32:04.553Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/dd/6f/529b0f316a9fd167281a6c3826b5583e6192dba792dd55e3203d3f8e655a/pyyaml-6.0.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:1d37d57ad971609cf3c53ba6a7e365e40660e3be0e5175fa9f2365a379d6095a", size = 767463, upload-time = "2025-09-25T21:32:06.152Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f2/6a/b627b4e0c1dd03718543519ffb2f1deea4a1e6d42fbab8021936a4d22589/pyyaml-6.0.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:37503bfbfc9d2c40b344d06b2199cf0e96e97957ab1c1b546fd4f87e53e5d3e4", size = 794986, upload-time = "2025-09-25T21:32:07.367Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/45/91/47a6e1c42d9ee337c4839208f30d9f09caa9f720ec7582917b264defc875/pyyaml-6.0.3-cp311-cp311-win32.whl", hash = "sha256:8098f252adfa6c80ab48096053f512f2321f0b998f98150cea9bd23d83e1467b", size = 142543, upload-time = "2025-09-25T21:32:08.95Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/da/e3/ea007450a105ae919a72393cb06f122f288ef60bba2dc64b26e2646fa315/pyyaml-6.0.3-cp311-cp311-win_amd64.whl", hash = "sha256:9f3bfb4965eb874431221a3ff3fdcddc7e74e3b07799e0e84ca4a0f867d449bf", size = 158763, upload-time = "2025-09-25T21:32:09.96Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d1/33/422b98d2195232ca1826284a76852ad5a86fe23e31b009c9886b2d0fb8b2/pyyaml-6.0.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:7f047e29dcae44602496db43be01ad42fc6f1cc0d8cd6c83d342306c32270196", size = 182063, upload-time = "2025-09-25T21:32:11.445Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/89/a0/6cf41a19a1f2f3feab0e9c0b74134aa2ce6849093d5517a0c550fe37a648/pyyaml-6.0.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:fc09d0aa354569bc501d4e787133afc08552722d3ab34836a80547331bb5d4a0", size = 173973, upload-time = "2025-09-25T21:32:12.492Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ed/23/7a778b6bd0b9a8039df8b1b1d80e2e2ad78aa04171592c8a5c43a56a6af4/pyyaml-6.0.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9149cad251584d5fb4981be1ecde53a1ca46c891a79788c0df828d2f166bda28", size = 775116, upload-time = "2025-09-25T21:32:13.652Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/65/30/d7353c338e12baef4ecc1b09e877c1970bd3382789c159b4f89d6a70dc09/pyyaml-6.0.3-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5fdec68f91a0c6739b380c83b951e2c72ac0197ace422360e6d5a959d8d97b2c", size = 844011, upload-time = "2025-09-25T21:32:15.21Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/8b/9d/b3589d3877982d4f2329302ef98a8026e7f4443c765c46cfecc8858c6b4b/pyyaml-6.0.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ba1cc08a7ccde2d2ec775841541641e4548226580ab850948cbfda66a1befcdc", size = 807870, upload-time = "2025-09-25T21:32:16.431Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/05/c0/b3be26a015601b822b97d9149ff8cb5ead58c66f981e04fedf4e762f4bd4/pyyaml-6.0.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:8dc52c23056b9ddd46818a57b78404882310fb473d63f17b07d5c40421e47f8e", size = 761089, upload-time = "2025-09-25T21:32:17.56Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/be/8e/98435a21d1d4b46590d5459a22d88128103f8da4c2d4cb8f14f2a96504e1/pyyaml-6.0.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:41715c910c881bc081f1e8872880d3c650acf13dfa8214bad49ed4cede7c34ea", size = 790181, upload-time = "2025-09-25T21:32:18.834Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/74/93/7baea19427dcfbe1e5a372d81473250b379f04b1bd3c4c5ff825e2327202/pyyaml-6.0.3-cp312-cp312-win32.whl", hash = "sha256:96b533f0e99f6579b3d4d4995707cf36df9100d67e0c8303a0c55b27b5f99bc5", size = 137658, upload-time = "2025-09-25T21:32:20.209Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/86/bf/899e81e4cce32febab4fb42bb97dcdf66bc135272882d1987881a4b519e9/pyyaml-6.0.3-cp312-cp312-win_amd64.whl", hash = "sha256:5fcd34e47f6e0b794d17de1b4ff496c00986e1c83f7ab2fb8fcfe9616ff7477b", size = 154003, upload-time = "2025-09-25T21:32:21.167Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/1a/08/67bd04656199bbb51dbed1439b7f27601dfb576fb864099c7ef0c3e55531/pyyaml-6.0.3-cp312-cp312-win_arm64.whl", hash = "sha256:64386e5e707d03a7e172c0701abfb7e10f0fb753ee1d773128192742712a98fd", size = 140344, upload-time = "2025-09-25T21:32:22.617Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d1/11/0fd08f8192109f7169db964b5707a2f1e8b745d4e239b784a5a1dd80d1db/pyyaml-6.0.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:8da9669d359f02c0b91ccc01cac4a67f16afec0dac22c2ad09f46bee0697eba8", size = 181669, upload-time = "2025-09-25T21:32:23.673Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b1/16/95309993f1d3748cd644e02e38b75d50cbc0d9561d21f390a76242ce073f/pyyaml-6.0.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:2283a07e2c21a2aa78d9c4442724ec1eb15f5e42a723b99cb3d822d48f5f7ad1", size = 173252, upload-time = "2025-09-25T21:32:25.149Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/50/31/b20f376d3f810b9b2371e72ef5adb33879b25edb7a6d072cb7ca0c486398/pyyaml-6.0.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ee2922902c45ae8ccada2c5b501ab86c36525b883eff4255313a253a3160861c", size = 767081, upload-time = "2025-09-25T21:32:26.575Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/49/1e/a55ca81e949270d5d4432fbbd19dfea5321eda7c41a849d443dc92fd1ff7/pyyaml-6.0.3-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a33284e20b78bd4a18c8c2282d549d10bc8408a2a7ff57653c0cf0b9be0afce5", size = 841159, upload-time = "2025-09-25T21:32:27.727Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/74/27/e5b8f34d02d9995b80abcef563ea1f8b56d20134d8f4e5e81733b1feceb2/pyyaml-6.0.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0f29edc409a6392443abf94b9cf89ce99889a1dd5376d94316ae5145dfedd5d6", size = 801626, upload-time = "2025-09-25T21:32:28.878Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f9/11/ba845c23988798f40e52ba45f34849aa8a1f2d4af4b798588010792ebad6/pyyaml-6.0.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:f7057c9a337546edc7973c0d3ba84ddcdf0daa14533c2065749c9075001090e6", size = 753613, upload-time = "2025-09-25T21:32:30.178Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/3d/e0/7966e1a7bfc0a45bf0a7fb6b98ea03fc9b8d84fa7f2229e9659680b69ee3/pyyaml-6.0.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:eda16858a3cab07b80edaf74336ece1f986ba330fdb8ee0d6c0d68fe82bc96be", size = 794115, upload-time = "2025-09-25T21:32:31.353Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/de/94/980b50a6531b3019e45ddeada0626d45fa85cbe22300844a7983285bed3b/pyyaml-6.0.3-cp313-cp313-win32.whl", hash = "sha256:d0eae10f8159e8fdad514efdc92d74fd8d682c933a6dd088030f3834bc8e6b26", size = 137427, upload-time = "2025-09-25T21:32:32.58Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/97/c9/39d5b874e8b28845e4ec2202b5da735d0199dbe5b8fb85f91398814a9a46/pyyaml-6.0.3-cp313-cp313-win_amd64.whl", hash = "sha256:79005a0d97d5ddabfeeea4cf676af11e647e41d81c9a7722a193022accdb6b7c", size = 154090, upload-time = "2025-09-25T21:32:33.659Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/73/e8/2bdf3ca2090f68bb3d75b44da7bbc71843b19c9f2b9cb9b0f4ab7a5a4329/pyyaml-6.0.3-cp313-cp313-win_arm64.whl", hash = "sha256:5498cd1645aa724a7c71c8f378eb29ebe23da2fc0d7a08071d89469bf1d2defb", size = 140246, upload-time = "2025-09-25T21:32:34.663Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/9d/8c/f4bd7f6465179953d3ac9bc44ac1a8a3e6122cf8ada906b4f96c60172d43/pyyaml-6.0.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:8d1fab6bb153a416f9aeb4b8763bc0f22a5586065f86f7664fc23339fc1c1fac", size = 181814, upload-time = "2025-09-25T21:32:35.712Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/bd/9c/4d95bb87eb2063d20db7b60faa3840c1b18025517ae857371c4dd55a6b3a/pyyaml-6.0.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:34d5fcd24b8445fadc33f9cf348c1047101756fd760b4dacb5c3e99755703310", size = 173809, upload-time = "2025-09-25T21:32:36.789Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/92/b5/47e807c2623074914e29dabd16cbbdd4bf5e9b2db9f8090fa64411fc5382/pyyaml-6.0.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:501a031947e3a9025ed4405a168e6ef5ae3126c59f90ce0cd6f2bfc477be31b7", size = 766454, upload-time = "2025-09-25T21:32:37.966Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/02/9e/e5e9b168be58564121efb3de6859c452fccde0ab093d8438905899a3a483/pyyaml-6.0.3-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:b3bc83488de33889877a0f2543ade9f70c67d66d9ebb4ac959502e12de895788", size = 836355, upload-time = "2025-09-25T21:32:39.178Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/88/f9/16491d7ed2a919954993e48aa941b200f38040928474c9e85ea9e64222c3/pyyaml-6.0.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c458b6d084f9b935061bc36216e8a69a7e293a2f1e68bf956dcd9e6cbcd143f5", size = 794175, upload-time = "2025-09-25T21:32:40.865Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/dd/3f/5989debef34dc6397317802b527dbbafb2b4760878a53d4166579111411e/pyyaml-6.0.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:7c6610def4f163542a622a73fb39f534f8c101d690126992300bf3207eab9764", size = 755228, upload-time = "2025-09-25T21:32:42.084Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d7/ce/af88a49043cd2e265be63d083fc75b27b6ed062f5f9fd6cdc223ad62f03e/pyyaml-6.0.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:5190d403f121660ce8d1d2c1bb2ef1bd05b5f68533fc5c2ea899bd15f4399b35", size = 789194, upload-time = "2025-09-25T21:32:43.362Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/23/20/bb6982b26a40bb43951265ba29d4c246ef0ff59c9fdcdf0ed04e0687de4d/pyyaml-6.0.3-cp314-cp314-win_amd64.whl", hash = "sha256:4a2e8cebe2ff6ab7d1050ecd59c25d4c8bd7e6f400f5f82b96557ac0abafd0ac", size = 156429, upload-time = "2025-09-25T21:32:57.844Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f4/f4/a4541072bb9422c8a883ab55255f918fa378ecf083f5b85e87fc2b4eda1b/pyyaml-6.0.3-cp314-cp314-win_arm64.whl", hash = "sha256:93dda82c9c22deb0a405ea4dc5f2d0cda384168e466364dec6255b293923b2f3", size = 143912, upload-time = "2025-09-25T21:32:59.247Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/7c/f9/07dd09ae774e4616edf6cda684ee78f97777bdd15847253637a6f052a62f/pyyaml-6.0.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:02893d100e99e03eda1c8fd5c441d8c60103fd175728e23e431db1b589cf5ab3", size = 189108, upload-time = "2025-09-25T21:32:44.377Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/4e/78/8d08c9fb7ce09ad8c38ad533c1191cf27f7ae1effe5bb9400a46d9437fcf/pyyaml-6.0.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:c1ff362665ae507275af2853520967820d9124984e0f7466736aea23d8611fba", size = 183641, upload-time = "2025-09-25T21:32:45.407Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/7b/5b/3babb19104a46945cf816d047db2788bcaf8c94527a805610b0289a01c6b/pyyaml-6.0.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6adc77889b628398debc7b65c073bcb99c4a0237b248cacaf3fe8a557563ef6c", size = 831901, upload-time = "2025-09-25T21:32:48.83Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/8b/cc/dff0684d8dc44da4d22a13f35f073d558c268780ce3c6ba1b87055bb0b87/pyyaml-6.0.3-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a80cb027f6b349846a3bf6d73b5e95e782175e52f22108cfa17876aaeff93702", size = 861132, upload-time = "2025-09-25T21:32:50.149Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b1/5e/f77dc6b9036943e285ba76b49e118d9ea929885becb0a29ba8a7c75e29fe/pyyaml-6.0.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:00c4bdeba853cc34e7dd471f16b4114f4162dc03e6b7afcc2128711f0eca823c", size = 839261, upload-time = "2025-09-25T21:32:51.808Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ce/88/a9db1376aa2a228197c58b37302f284b5617f56a5d959fd1763fb1675ce6/pyyaml-6.0.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:66e1674c3ef6f541c35191caae2d429b967b99e02040f5ba928632d9a7f0f065", size = 805272, upload-time = "2025-09-25T21:32:52.941Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/da/92/1446574745d74df0c92e6aa4a7b0b3130706a4142b2d1a5869f2eaa423c6/pyyaml-6.0.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:16249ee61e95f858e83976573de0f5b2893b3677ba71c9dd36b9cf8be9ac6d65", size = 829923, upload-time = "2025-09-25T21:32:54.537Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f0/7a/1c7270340330e575b92f397352af856a8c06f230aa3e76f86b39d01b416a/pyyaml-6.0.3-cp314-cp314t-win_amd64.whl", hash = "sha256:4ad1906908f2f5ae4e5a8ddfce73c320c2a1429ec52eafd27138b7f1cbe341c9", size = 174062, upload-time = "2025-09-25T21:32:55.767Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f1/12/de94a39c2ef588c7e6455cfbe7343d3b2dc9d6b6b2f40c4c6565744c873d/pyyaml-6.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:ebc55a14a21cb14062aa4162f906cd962b28e2e9ea38f9b4391244cd8de4ae0b", size = 149341, upload-time = "2025-09-25T21:32:56.828Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "requests"
|
||||
version = "2.32.5"
|
||||
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" },
|
||||
{ name = "certifi" },
|
||||
{ name = "charset-normalizer" },
|
||||
{ name = "idna" },
|
||||
{ name = "urllib3" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/c9/74/b3ff8e6c8446842c3f5c837e9c3dfcfe2018ea6ecef224c710c85ef728f4/requests-2.32.5.tar.gz", hash = "sha256:dbba0bac56e100853db0ea71b82b4dfd5fe2bf6d3754a8893c3af500cec7d7cf", size = 134517, upload-time = "2025-08-18T20:46:02.573Z" }
|
||||
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" },
|
||||
{ url = "https://files.pythonhosted.org/packages/1e/db/4254e3eabe8020b458f1a747140d32277ec7a271daf1d235b70dc0b4e6e3/requests-2.32.5-py3-none-any.whl", hash = "sha256:2462f94637a34fd532264295e186976db0f5d453d1cdd31473c85a6a161affb6", size = 64738, upload-time = "2025-08-18T20:46:00.542Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "roman-numerals"
|
||||
version = "4.1.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/ae/f9/41dc953bbeb056c17d5f7a519f50fdf010bd0553be2d630bc69d1e022703/roman_numerals-4.1.0.tar.gz", hash = "sha256:1af8b147eb1405d5839e78aeb93131690495fe9da5c91856cb33ad55a7f1e5b2", size = 9077, upload-time = "2025-12-17T18:25:34.381Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/04/54/6f679c435d28e0a568d8e8a7c0a93a09010818634c3c3907fc98d8983770/roman_numerals-4.1.0-py3-none-any.whl", hash = "sha256:647ba99caddc2cc1e55a51e4360689115551bf4476d90e8162cf8c345fe233c7", size = 7676, upload-time = "2025-12-17T18:25:33.098Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -441,7 +885,8 @@ name = "scipy"
|
||||
version = "1.17.1"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
resolution-markers = [
|
||||
"python_full_version >= '3.11'",
|
||||
"python_full_version >= '3.12'",
|
||||
"python_full_version == '3.11.*'",
|
||||
]
|
||||
dependencies = [
|
||||
{ name = "numpy", version = "2.4.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" },
|
||||
@@ -510,6 +955,15 @@ wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/07/39/338d9219c4e87f3e708f18857ecd24d22a0c3094752393319553096b98af/scipy-1.17.1-cp314-cp314t-win_arm64.whl", hash = "sha256:200e1050faffacc162be6a486a984a0497866ec54149a01270adc8a59b7c7d21", size = 25489165, upload-time = "2026-02-23T00:22:29.563Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "snowballstemmer"
|
||||
version = "3.0.1"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/75/a7/9810d872919697c9d01295633f5d574fb416d47e535f258272ca1f01f447/snowballstemmer-3.0.1.tar.gz", hash = "sha256:6d5eeeec8e9f84d4d56b847692bacf79bc2c8e90c7f80ca4444ff8b6f2e52895", size = 105575, upload-time = "2025-05-09T16:34:51.843Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/c8/78/3565d011c61f5a43488987ee32b6f3f656e7f107ac2782dd57bdd7d91d9a/snowballstemmer-3.0.1-py3-none-any.whl", hash = "sha256:6cd7b3897da8d6c9ffb968a6781fa6532dce9c3618a4b127d920dab764a19064", size = 103274, upload-time = "2025-05-09T16:34:50.371Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "sounddevice"
|
||||
version = "0.5.5"
|
||||
@@ -527,15 +981,150 @@ wheels = [
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "sympy"
|
||||
version = "1.14.0"
|
||||
name = "sphinx"
|
||||
version = "8.1.3"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "mpmath" },
|
||||
resolution-markers = [
|
||||
"python_full_version < '3.11'",
|
||||
]
|
||||
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" }
|
||||
dependencies = [
|
||||
{ name = "alabaster", marker = "python_full_version < '3.11'" },
|
||||
{ name = "babel", marker = "python_full_version < '3.11'" },
|
||||
{ name = "colorama", marker = "python_full_version < '3.11' and sys_platform == 'win32'" },
|
||||
{ name = "docutils", version = "0.21.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" },
|
||||
{ name = "imagesize", marker = "python_full_version < '3.11'" },
|
||||
{ name = "jinja2", marker = "python_full_version < '3.11'" },
|
||||
{ name = "packaging", marker = "python_full_version < '3.11'" },
|
||||
{ name = "pygments", marker = "python_full_version < '3.11'" },
|
||||
{ name = "requests", marker = "python_full_version < '3.11'" },
|
||||
{ name = "snowballstemmer", marker = "python_full_version < '3.11'" },
|
||||
{ name = "sphinxcontrib-applehelp", marker = "python_full_version < '3.11'" },
|
||||
{ name = "sphinxcontrib-devhelp", marker = "python_full_version < '3.11'" },
|
||||
{ name = "sphinxcontrib-htmlhelp", marker = "python_full_version < '3.11'" },
|
||||
{ name = "sphinxcontrib-jsmath", marker = "python_full_version < '3.11'" },
|
||||
{ name = "sphinxcontrib-qthelp", marker = "python_full_version < '3.11'" },
|
||||
{ name = "sphinxcontrib-serializinghtml", marker = "python_full_version < '3.11'" },
|
||||
{ name = "tomli", marker = "python_full_version < '3.11'" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/6f/6d/be0b61178fe2cdcb67e2a92fc9ebb488e3c51c4f74a36a7824c0adf23425/sphinx-8.1.3.tar.gz", hash = "sha256:43c1911eecb0d3e161ad78611bc905d1ad0e523e4ddc202a58a821773dc4c927", size = 8184611, upload-time = "2024-10-13T20:27:13.93Z" }
|
||||
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" },
|
||||
{ url = "https://files.pythonhosted.org/packages/26/60/1ddff83a56d33aaf6f10ec8ce84b4c007d9368b21008876fceda7e7381ef/sphinx-8.1.3-py3-none-any.whl", hash = "sha256:09719015511837b76bf6e03e42eb7595ac8c2e41eeb9c29c5b755c6b677992a2", size = 3487125, upload-time = "2024-10-13T20:27:10.448Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "sphinx"
|
||||
version = "9.0.4"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
resolution-markers = [
|
||||
"python_full_version == '3.11.*'",
|
||||
]
|
||||
dependencies = [
|
||||
{ name = "alabaster", marker = "python_full_version == '3.11.*'" },
|
||||
{ name = "babel", marker = "python_full_version == '3.11.*'" },
|
||||
{ name = "colorama", marker = "python_full_version == '3.11.*' and sys_platform == 'win32'" },
|
||||
{ name = "docutils", version = "0.22.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.11.*'" },
|
||||
{ name = "imagesize", marker = "python_full_version == '3.11.*'" },
|
||||
{ name = "jinja2", marker = "python_full_version == '3.11.*'" },
|
||||
{ name = "packaging", marker = "python_full_version == '3.11.*'" },
|
||||
{ name = "pygments", marker = "python_full_version == '3.11.*'" },
|
||||
{ name = "requests", marker = "python_full_version == '3.11.*'" },
|
||||
{ name = "roman-numerals", marker = "python_full_version == '3.11.*'" },
|
||||
{ name = "snowballstemmer", marker = "python_full_version == '3.11.*'" },
|
||||
{ name = "sphinxcontrib-applehelp", marker = "python_full_version == '3.11.*'" },
|
||||
{ name = "sphinxcontrib-devhelp", marker = "python_full_version == '3.11.*'" },
|
||||
{ name = "sphinxcontrib-htmlhelp", marker = "python_full_version == '3.11.*'" },
|
||||
{ name = "sphinxcontrib-jsmath", marker = "python_full_version == '3.11.*'" },
|
||||
{ name = "sphinxcontrib-qthelp", marker = "python_full_version == '3.11.*'" },
|
||||
{ name = "sphinxcontrib-serializinghtml", marker = "python_full_version == '3.11.*'" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/42/50/a8c6ccc36d5eacdfd7913ddccd15a9cee03ecafc5ee2bc40e1f168d85022/sphinx-9.0.4.tar.gz", hash = "sha256:594ef59d042972abbc581d8baa577404abe4e6c3b04ef61bd7fc2acbd51f3fa3", size = 8710502, upload-time = "2025-12-04T07:45:27.343Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/c6/3f/4bbd76424c393caead2e1eb89777f575dee5c8653e2d4b6afd7a564f5974/sphinx-9.0.4-py3-none-any.whl", hash = "sha256:5bebc595a5e943ea248b99c13814c1c5e10b3ece718976824ffa7959ff95fffb", size = 3917713, upload-time = "2025-12-04T07:45:24.944Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "sphinx"
|
||||
version = "9.1.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
resolution-markers = [
|
||||
"python_full_version >= '3.12'",
|
||||
]
|
||||
dependencies = [
|
||||
{ name = "alabaster", marker = "python_full_version >= '3.12'" },
|
||||
{ name = "babel", marker = "python_full_version >= '3.12'" },
|
||||
{ name = "colorama", marker = "python_full_version >= '3.12' and sys_platform == 'win32'" },
|
||||
{ name = "docutils", version = "0.22.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.12'" },
|
||||
{ name = "imagesize", marker = "python_full_version >= '3.12'" },
|
||||
{ name = "jinja2", marker = "python_full_version >= '3.12'" },
|
||||
{ name = "packaging", marker = "python_full_version >= '3.12'" },
|
||||
{ name = "pygments", marker = "python_full_version >= '3.12'" },
|
||||
{ name = "requests", marker = "python_full_version >= '3.12'" },
|
||||
{ name = "roman-numerals", marker = "python_full_version >= '3.12'" },
|
||||
{ name = "snowballstemmer", marker = "python_full_version >= '3.12'" },
|
||||
{ name = "sphinxcontrib-applehelp", marker = "python_full_version >= '3.12'" },
|
||||
{ name = "sphinxcontrib-devhelp", marker = "python_full_version >= '3.12'" },
|
||||
{ name = "sphinxcontrib-htmlhelp", marker = "python_full_version >= '3.12'" },
|
||||
{ name = "sphinxcontrib-jsmath", marker = "python_full_version >= '3.12'" },
|
||||
{ name = "sphinxcontrib-qthelp", marker = "python_full_version >= '3.12'" },
|
||||
{ name = "sphinxcontrib-serializinghtml", marker = "python_full_version >= '3.12'" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/cd/bd/f08eb0f4eed5c83f1ba2a3bd18f7745a2b1525fad70660a1c00224ec468a/sphinx-9.1.0.tar.gz", hash = "sha256:7741722357dd75f8190766926071fed3bdc211c74dd2d7d4df5404da95930ddb", size = 8718324, upload-time = "2025-12-31T15:09:27.646Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/73/f7/b1884cb3188ab181fc81fa00c266699dab600f927a964df02ec3d5d1916a/sphinx-9.1.0-py3-none-any.whl", hash = "sha256:c84fdd4e782504495fe4f2c0b3413d6c2bf388589bb352d439b2a3bb99991978", size = 3921742, upload-time = "2025-12-31T15:09:25.561Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "sphinxcontrib-applehelp"
|
||||
version = "2.0.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/ba/6e/b837e84a1a704953c62ef8776d45c3e8d759876b4a84fe14eba2859106fe/sphinxcontrib_applehelp-2.0.0.tar.gz", hash = "sha256:2f29ef331735ce958efa4734873f084941970894c6090408b079c61b2e1c06d1", size = 20053, upload-time = "2024-07-29T01:09:00.465Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/5d/85/9ebeae2f76e9e77b952f4b274c27238156eae7979c5421fba91a28f4970d/sphinxcontrib_applehelp-2.0.0-py3-none-any.whl", hash = "sha256:4cd3f0ec4ac5dd9c17ec65e9ab272c9b867ea77425228e68ecf08d6b28ddbdb5", size = 119300, upload-time = "2024-07-29T01:08:58.99Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "sphinxcontrib-devhelp"
|
||||
version = "2.0.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/f6/d2/5beee64d3e4e747f316bae86b55943f51e82bb86ecd325883ef65741e7da/sphinxcontrib_devhelp-2.0.0.tar.gz", hash = "sha256:411f5d96d445d1d73bb5d52133377b4248ec79db5c793ce7dbe59e074b4dd1ad", size = 12967, upload-time = "2024-07-29T01:09:23.417Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/35/7a/987e583882f985fe4d7323774889ec58049171828b58c2217e7f79cdf44e/sphinxcontrib_devhelp-2.0.0-py3-none-any.whl", hash = "sha256:aefb8b83854e4b0998877524d1029fd3e6879210422ee3780459e28a1f03a8a2", size = 82530, upload-time = "2024-07-29T01:09:21.945Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "sphinxcontrib-htmlhelp"
|
||||
version = "2.1.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/43/93/983afd9aa001e5201eab16b5a444ed5b9b0a7a010541e0ddfbbfd0b2470c/sphinxcontrib_htmlhelp-2.1.0.tar.gz", hash = "sha256:c9e2916ace8aad64cc13a0d233ee22317f2b9025b9cf3295249fa985cc7082e9", size = 22617, upload-time = "2024-07-29T01:09:37.889Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/0a/7b/18a8c0bcec9182c05a0b3ec2a776bba4ead82750a55ff798e8d406dae604/sphinxcontrib_htmlhelp-2.1.0-py3-none-any.whl", hash = "sha256:166759820b47002d22914d64a075ce08f4c46818e17cfc9470a9786b759b19f8", size = 98705, upload-time = "2024-07-29T01:09:36.407Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "sphinxcontrib-jsmath"
|
||||
version = "1.0.1"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/b2/e8/9ed3830aeed71f17c026a07a5097edcf44b692850ef215b161b8ad875729/sphinxcontrib-jsmath-1.0.1.tar.gz", hash = "sha256:a9925e4a4587247ed2191a22df5f6970656cb8ca2bd6284309578f2153e0c4b8", size = 5787, upload-time = "2019-01-21T16:10:16.347Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/c2/42/4c8646762ee83602e3fb3fbe774c2fac12f317deb0b5dbeeedd2d3ba4b77/sphinxcontrib_jsmath-1.0.1-py2.py3-none-any.whl", hash = "sha256:2ec2eaebfb78f3f2078e73666b1415417a116cc848b72e5172e596c871103178", size = 5071, upload-time = "2019-01-21T16:10:14.333Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "sphinxcontrib-qthelp"
|
||||
version = "2.0.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/68/bc/9104308fc285eb3e0b31b67688235db556cd5b0ef31d96f30e45f2e51cae/sphinxcontrib_qthelp-2.0.0.tar.gz", hash = "sha256:4fe7d0ac8fc171045be623aba3e2a8f613f8682731f9153bb2e40ece16b9bbab", size = 17165, upload-time = "2024-07-29T01:09:56.435Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/27/83/859ecdd180cacc13b1f7e857abf8582a64552ea7a061057a6c716e790fce/sphinxcontrib_qthelp-2.0.0-py3-none-any.whl", hash = "sha256:b18a828cdba941ccd6ee8445dbe72ffa3ef8cbe7505d8cd1fa0d42d3f2d5f3eb", size = 88743, upload-time = "2024-07-29T01:09:54.885Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "sphinxcontrib-serializinghtml"
|
||||
version = "2.0.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/3b/44/6716b257b0aa6bfd51a1b31665d1c205fb12cb5ad56de752dfa15657de2f/sphinxcontrib_serializinghtml-2.0.0.tar.gz", hash = "sha256:e9d912827f872c029017a53f0ef2180b327c3f7fd23c87229f7a8e8b70031d4d", size = 16080, upload-time = "2024-07-29T01:10:09.332Z" }
|
||||
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]]
|
||||
@@ -600,3 +1189,12 @@ sdist = { url = "https://files.pythonhosted.org/packages/72/94/1a15dd82efb362ac8
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/18/67/36e9267722cc04a6b9f15c7f3441c2363321a3ea07da7ae0c0707beb2a9c/typing_extensions-4.15.0-py3-none-any.whl", hash = "sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548", size = 44614, upload-time = "2025-08-25T13:49:24.86Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "urllib3"
|
||||
version = "2.6.3"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/c7/24/5f1b3bdffd70275f6661c76461e25f024d5a38a46f04aaca912426a2b1d3/urllib3-2.6.3.tar.gz", hash = "sha256:1b62b6884944a57dbe321509ab94fd4d3b307075e0c2eae991ac71ee15ad38ed", size = 435556, upload-time = "2026-01-07T16:24:43.925Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/39/08/aaaad47bc4e9dc8c725e68f9d04865dbcb2052843ff09c97b08904852d84/urllib3-2.6.3-py3-none-any.whl", hash = "sha256:bf272323e553dfb2e87d9bfd225ca7b0f467b919d7bbd355436d3fd37cb0acd4", size = 131584, upload-time = "2026-01-07T16:24:42.685Z" },
|
||||
]
|
||||
|
||||
Reference in New Issue
Block a user