Compare commits

...

21 Commits

Author SHA1 Message Date
kennethreitz aa21bf0f2a v0.34.0: 27 synth waveforms, world drums, guitar strumming
16 dedicated instrument synths, speaker cab sim, analog drift,
strumming with fretboard lookup, dhol/dholak/mridangam/djembe/
metal kit with 22 patterns, 5 new demo moods.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-27 02:00:57 -04:00
kennethreitz e7e35ad4e4 5 more dedicated synths: oboe, harpsichord, cello, harp, upright bass
- Oboe: double reed buzz + conical bore (all harmonics, peaked 3-5)
- Harpsichord: KS with quill chiff, bright metallic pluck
- Cello: deep bowed string with 250/500Hz body resonance
- Harp: soft KS pluck with large soundboard bloom
- Upright bass: thick string pizzicato with wooden body resonance
- 27 synth waveforms total

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-27 01:59:20 -04:00
kennethreitz 503dbce937 6 dedicated instrument synths: piano, bass, flute, trumpet, clarinet, marimba
- Piano: hammer strike + detuned strings + inharmonicity + soundboard
- Bass guitar: heavy KS with thick string damping + low-mid pickup
- Flute: breath noise + tube resonance + developing vibrato
- Trumpet: lip buzz harmonics + brass bell resonance + vibrato
- Clarinet: odd harmonics (cylindrical bore) + reed noise
- Marimba: inharmonic bar modes (1x, 4x, 9.2x) + resonator tube
- 22 synth waveforms total

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-27 01:44:00 -04:00
kennethreitz c6bbfae7e6 Acoustic guitar synth with body resonance, fix strum
- New acoustic_guitar_synth: Karplus-Strong with wooden body
  resonance (3 formant peaks at 110/250/500 Hz), warmer initial
  noise, gentle rolloff. Sounds woody, not harsh.
- Strum renders as a single chord hit — no more exposed grace
  notes that sounded digital. Clean, full chord sound.
- 16 synth waveforms total

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-27 01:38:35 -04:00
kennethreitz 64ef7f0803 Add analog oscillator drift for synth warmth
Per-note random pitch wobble (gaussian, ±cents scaled by analog param)
simulates analog oscillator instability. Applied to synth_lead (0.3),
synth_pad (0.4), synth_bass (0.2), acid_bass (0.3), electric_piano
(0.2), organ (0.15). Subtle enough to add life without sounding
out of tune.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-27 01:32:03 -04:00
kennethreitz 406e5d7e54 Electric guitar synth, cab sim, strumming, world drums, metal kit
- Electric guitar: Karplus-Strong + magnetic pickup comb filter
- Cabinet simulation: speaker rolloff + presence bump (tames fizz)
- 6 guitar presets: clean, crunch, distorted, orange, metal
- Part.strum(): fretboard fingering lookup with down/up strumming
- Sitar synth: jawari buzz + chikari sympathetic strings
- Dhol, dholak, mridangam, djembe synthesis (membrane noise)
- Metal drum kit (kick click, bright snare, tight hats)
- 11 world patterns + 4 metal patterns + 7 tabla patterns

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-27 01:25:53 -04:00
kennethreitz 267b7284ba Add dhol, dholak, mridangam, djembe drums + 11 world patterns
Drum synthesis:
- Dhol: dagga (heavy bass), tilli (treble crack), both
- Dholak: ge (bass palm), na (treble fingers), tit (light tap)
- Mridangam: tham (clay body bass), nam (rich overtone ring),
  din (both heads), tha (muted)
- Djembe: bass (center palm), tone (edge ring), slap (sharp crack)
All with bandpass-filtered membrane noise for drum head character.

Patterns:
- Dhol: bhangra, dhol chaal
- Dholak: qawwali, dholak folk
- Mridangam: adi talam, mridangam korvai
- Djembe: djembe (standard), kuku, soli

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-27 00:59:27 -04:00
kennethreitz 9b62b56120 Sitar synth, tabla drums with wood/metal shells, 7 tabla patterns
- Sitar synth: Karplus-Strong with gentle jawari bridge buzz,
  variable damping (bright attack fades to warm sustain), chikari
  sympathetic string shimmer
- Tabla: 6 synthesized strokes (Na, Tin, Ge, Dha, Tit, Ke) with
  goatskin membrane noise (bandpass filtered), wooden shell resonance
  on dayan, copper/metal shell resonance on bayan
- 7 tabla patterns: teental (16 beats), jhaptaal (10), rupak (7),
  dadra (6), keherwa (8), tabla solo, tiri kita (fast 16th-note)
- Sitar instrument preset with proper lowpass

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-27 00:54:19 -04:00
kennethreitz 4fe7771d83 v0.33.0: Microtonal systems, historical tuning, Bohlen-Pierce
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-27 00:34:59 -04:00
kennethreitz 57079a43ac Merge feature/non-12-tet: microtonal systems, historical tuning
11 microtonal systems, Bohlen-Pierce tritave, just intonation,
reference pitch, Score(system=, temperament=, reference_pitch=).
TET(n) factory for any equal temperament. 819 tests passing.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-27 00:34:12 -04:00
kennethreitz 1d07b06968 Add Greensleeves (Renaissance lute, meantone A=415) to songs.py
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-27 00:33:50 -04:00
kennethreitz 9887b59cfb Add reference_pitch to Score and playback pipeline
Score(reference_pitch=415.0, temperament="meantone") renders an
entire piece at Baroque pitch with historical tuning. Flows through
to all .pitch() calls in both normal and legato renderers.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-27 00:32:11 -04:00
kennethreitz 9850a8016e Bohlen-Pierce, just intonation, temperament in Score/playback
- Bohlen-Pierce (13-TET tritave): period=3.0 support in pitch(),
  System, and TET factory. 13 equal divisions of the 3:1 ratio.
- Just intonation temperament: 5-limit JI ratios (pure 3/2 fifths,
  5/4 thirds). Use temperament="just" anywhere.
- Score(temperament="just") flows through to playback — all .pitch()
  calls in the render pipeline use the Score's temperament.
- Carnatic 72-TET system with 10 melakartas.
- Fix c_index for Indian, Arabic, and Gamelan 12-TET systems.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-27 00:24:03 -04:00
kennethreitz 35f5f35dc5 Carnatic 72-TET, Score system param, 22 microtonal tests
- Carnatic (72-TET): 10 melakartas including shankarabharanam,
  kalyani, mayamalavagowla, kharaharapriya, etc.
- Score(system=) param passes tuning system to all parts, so
  Part.add("Sa") resolves through the correct system
- 22 new tests covering all microtonal systems: TET factory,
  19/31-TET, shruti, maqam, slendro, pelog, thai, makam,
  carnatic, circle of fifths, from_frequency, Score integration

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-27 00:18:09 -04:00
kennethreitz 47ca94111f Add gamelan slendro/pelog, Thai 7-TET, Turkish 53-TET makam
- Slendro (5-TET): true equal 5-tone gamelan tuning, 240 cents/step
- Pelog (9-TET): 7-of-9 gamelan tuning with pathet nem/lima/barang
- Thai classical (7-TET): 7 equal divisions (~171 cents each)
- Turkish makam (53-TET): Arel-Ezgi-Uzdilek system with 9 makams
  (rast, hicaz, ussak, nihavend, huseyni, kurdi, segah, saba, huzzam)
- Fix octave parser to only match trailing digits (not "Mib+3")
- Fix _index to use _name_to_index (avoid creating Tone objects)
- Fix _math to use per-system c_index

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-27 00:11:19 -04:00
kennethreitz 62cfbb2591 Add 22-shruti Indian and 24-TET Arabic maqam systems
- "shruti" system: 22 named shrutis with proper microtonal intervals
  for all 10 thaats (bilawal, bhairav, todi, etc.) and pentatonic
  scales (bhupali, malkauns, durga). Captures the 2-shruti vs 3-shruti
  distinctions that 12-TET approximations lose.

- "maqam" system: 24-TET with quarter-tone positions (↑/↓ notation).
  True maqam Rast with quarter-flat E and B. Bayati, Saba, Sikah,
  Hijaz, and 6 more maqamat with exact quarter-tone intervals.

- 12-TET "indian" and "arabic" systems preserved for backwards compat.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-27 00:04:27 -04:00
kennethreitz de855a3fe6 Add non-12-TET support: TET() factory, 19-TET, 31-TET
- TET(n) factory creates N-tone equal temperament systems
- Built-in named systems: "19-tet" and "31-tet" with proper note
  names and scale definitions (major, minor, harmonic minor, pentatonic)
- Per-system c_index replaces global C_INDEX constant
- Fix 6 hardcoded '12's in tones.py: from_frequency, from_midi,
  interval_to, midi property, circle_of_fifths/fourths
- Numbered pitch classes for custom EDOs: TET(17) uses "0"-"16"
- Octave parser skips numeric-only names (fixes "0" being eaten)

Refs #38

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-26 23:58:33 -04:00
kennethreitz dc9f7b3342 Update changelog for 0.32.1
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-26 23:50:52 -04:00
kennethreitz 60fdff6d36 Merge pull request #43 from kennethreitz/fix/enharmonics-and-double-accidentals
Support enharmonic spellings and double accidentals
2026-03-26 23:50:33 -04:00
kennethreitz f42d38d1fd Support Cb, Fb, E#, B#, double sharps/flats, unicode symbols
- Cb, Fb, E#, B# resolve to their enharmonic equivalents (fixes #40)
- C##, Dbb, etc. resolve via semitone arithmetic (fixes #41)
- Unicode symbols accepted: ♯ ♭ 𝄪 𝄫
- 'x'/'X' accepted as double sharp (Bach notation): Fx = F##
- resolve_name handles all accidentals dynamically

Closes #40, closes #41

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-26 23:48:42 -04:00
kennethreitz 5a4122d61f Merge pull request #42 from kennethreitz/fix/tone-validate-early
Validate tone name at construction time
2026-03-26 23:43:41 -04:00
12 changed files with 2938 additions and 141 deletions
+66
View File
@@ -2,6 +2,72 @@
All notable changes to PyTheory are documented here.
## 0.34.0
- **16 dedicated instrument synths** — physical modeling and specialized
synthesis for: piano (hammer + steel strings + soundboard), bass guitar
(thick KS + pickup), flute (breath + tube resonance), trumpet (lip buzz
+ bell), clarinet (odd harmonics + reed), oboe (double reed + conical
bore), marimba (inharmonic bar modes), harpsichord (quill pluck),
cello (deep bowed + body), harp (soft pluck + soundboard bloom),
upright bass (pizzicato + wooden body), acoustic guitar (KS + body
resonance), electric guitar (KS + pickup comb filter), sitar (jawari
+ chikari), plus organ and bowed strings
- **Speaker cabinet simulation** — tames distorted guitar fizz
- **Guitar strumming** — `Part.strum("Am")` with fretboard lookup
- **Analog oscillator drift** — subtle per-note pitch wobble on synth presets
- **World percussion:** dhol, dholak, mridangam, djembe, metal kit
with 22 new drum patterns
- **Piano improvements:** brightness scales with pitch, two-stage decay,
hammer impact with felt character
- 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:**
+33 -1
View File
@@ -1204,6 +1204,37 @@ def cinematic_showcase():
play_song(score, "Cinematic Showcase — A minor")
def greensleeves():
"""Greensleeves — Renaissance lute, meantone tuning, A=415 Hz."""
score = Score("3/4", bpm=120, temperament="meantone", reference_pitch=415.0)
lute = score.part("lute", instrument="acoustic_guitar",
reverb=0.3, reverb_type="taj_mahal")
melody = [
("A4", 1.0, 80),
("C5", 2.0, 85), ("D5", 1.0, 80),
("E5", 3.0, 90),
("F5", 1.0, 75), ("E5", 2.0, 85),
("D5", 1.0, 80),
("B4", 3.0, 85),
("G4", 1.0, 70), ("B4", 2.0, 80),
("C5", 1.0, 75),
("A4", 3.0, 85),
("A4", 1.0, 70), ("A4", 2.0, 75),
("G#4", 1.0, 70),
("A4", 2.0, 80), ("B4", 1.0, 75),
("G4", 3.0, 85),
("E4", 1.0, 70),
("A4", 3.0, 90),
]
for note, dur, vel in melody:
lute.add(note, dur, velocity=vel)
play_song(score, "Greensleeves — Renaissance Lute (Meantone, A=415)")
SONGS = {
"1": ("Bossa Nova in A minor", bossa_nova_girl),
"2": ("Bebop in Bb major", bebop_in_bb),
@@ -1226,6 +1257,7 @@ SONGS = {
"19": ("Dance Party at the Reitz House", dance_party),
"20": ("Temple Bell (Japanese)", temple_bell),
"21": ("Cinematic Showcase (Orchestral)", cinematic_showcase),
"22": ("Greensleeves (Renaissance Lute)", greensleeves),
}
if __name__ == "__main__":
@@ -1239,7 +1271,7 @@ if __name__ == "__main__":
print(f" {key:>2}. {name}")
print()
choice = input(" Pick a song (1-21, or 'all'): ").strip()
choice = input(" Pick a song (1-22, or 'all'): ").strip()
print()
if choice == "all":
+1 -1
View File
@@ -1,6 +1,6 @@
[project]
name = "pytheory"
version = "0.32.0"
version = "0.34.0"
description = "Music Theory for Humans"
readme = "README.md"
license = "MIT"
+3 -3
View File
@@ -1,9 +1,9 @@
"""PyTheory: Music Theory for Humans."""
__version__ = "0.32.0"
__version__ = "0.34.0"
from .tones import Tone, Interval
from .systems import System, SYSTEMS
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
@@ -21,7 +21,7 @@ Scale = TonedScale
__all__ = [
"Tone", "Note", "Interval", "Scale", "TonedScale", "Key",
"PROGRESSIONS", "Chord", "Fretboard", "Fingering", "analyze_progression",
"System", "SYSTEMS", "CHARTS", "charts_for_fretboard",
"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",
+468
View File
@@ -6,10 +6,42 @@ REFERENCE_A = 440
# Scientific pitch notation changes octave at C, not A, so this offset
# is needed for all octave arithmetic.
C_INDEX = 3
def _create_just_intonation_scale(n):
"""5-limit just intonation ratios for 12-tone systems.
These are the pure frequency ratios derived from the harmonic series —
the way intervals "want" to sound before equal temperament imposed
compromise. Each ratio is mathematically exact: a perfect fifth is
exactly 3/2, a major third is exactly 5/4.
For non-12 systems, falls back to equal temperament.
"""
from fractions import Fraction
if n != 12:
return scales.create_edo_scale(n)
# Standard 5-limit JI ratios (A-based: A=1/1)
ratios = [
Fraction(1, 1), # A — unison
Fraction(16, 15), # A# — minor second
Fraction(9, 8), # B — major second
Fraction(6, 5), # C — minor third
Fraction(5, 4), # C# — major third
Fraction(4, 3), # D — perfect fourth
Fraction(45, 32), # D# — augmented fourth
Fraction(3, 2), # E — perfect fifth
Fraction(8, 5), # F — minor sixth
Fraction(5, 3), # F# — major sixth
Fraction(9, 5), # G — minor seventh
Fraction(15, 8), # G# — major seventh
Fraction(2, 1), # A — octave
]
return [float(r) for r in ratios]
TEMPERAMENTS = {
"equal": scales.create_edo_scale,
"pythagorean": scales.create_pythagorean_scale,
"meantone": scales.create_quarter_comma_meantone_scale,
"just": _create_just_intonation_scale,
}
TONES = {
@@ -220,6 +252,442 @@ INDIAN_SCALES = {
}
}
# ── 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 thaat scales with proper microtonal intervals.
# Each interval is counted in shrutis (22-TET steps).
# Compare to the 12-TET approximations in INDIAN_SCALES which lose
# the distinction between 2-shruti and 3-shruti steps.
SHRUTI_SCALES = {
"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)},
},
],
}
# ── 24-TET Arabic maqam system ─────────────────────────────────────────────
# Arabic maqam uses quarter-tones (half-flat, half-sharp). 24-TET captures
# these intervals exactly. Each step = 50 cents (vs 100 in 12-TET).
# The half-flat (♭½) is the defining sound of Arabic music — it's what
# makes maqam Rast and Bayati sound distinctly Middle Eastern.
#
# Ordered from La (=A) to match Western index positions.
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 = {
+24
View File
@@ -275,6 +275,30 @@ def cmd_demo(args):
"lead": ("pluck_synth", "none", 0.3, 0.2),
"pad": ("strings_synth", "pad", 0.0),
"bass_lp": 200, "reverb_type": "taj_mahal"},
{"name": "Classical", "key": ("D", "minor"), "drums": "bolero",
"fill": "bossa nova", "bpm": 72,
"prog": ("i", "iv", "V", "i"),
"lead": ("flute_synth", "strings", 0.35, 0.2),
"pad": ("cello_synth", "bowed", -0.2),
"bass_lp": 400, "reverb_type": "cathedral"},
{"name": "Harpsichord Suite", "key": ("A", "minor"), "drums": "bolero",
"fill": "bossa nova", "bpm": 92,
"prog": ("i", "iv", "V", "i"),
"lead": ("harpsichord_synth", "none", 0.2, 0.1),
"pad": ("strings_synth", "pad", -0.3),
"bass_lp": 500, "reverb_type": "plate"},
{"name": "Bhangra", "key": ("G", "minor"), "drums": "bhangra",
"fill": "rock", "bpm": 140,
"prog": ("i", "iv", "V", "i"),
"lead": ("sitar_synth", "none", 0.3, 0.2),
"pad": ("strings_synth", "pad", 0.0),
"bass_lp": 400, "reverb_type": "taj_mahal"},
{"name": "Jazz Trio", "key": ("F", "major"), "drums": "swing",
"fill": "jazz", "bpm": 100,
"prog": ("I", "vi", "ii", "V"),
"lead": ("trumpet_synth", "bowed", 0.3, 0.2),
"pad": ("piano_synth", "none", -0.2),
"bass_lp": 600, "reverb_type": "plate"},
]
mood = random.choice(moods)
+1291 -12
View File
File diff suppressed because it is too large Load Diff
+501 -45
View File
@@ -14,11 +14,8 @@ from typing import Optional
INSTRUMENTS = {
# ── Keys ──
"piano": {
"synth": "fm", "envelope": "piano",
"fm_ratio": 1.0, "fm_index": 1.5,
"detune": 5, "chorus": 0.1, "chorus_rate": 0.3,
"lowpass": 6000, "saturation": 0.1,
"vel_to_filter": 3000, "noise_mix": 0.02,
"synth": "piano_synth", "envelope": "none",
"vel_to_filter": 3000,
},
"electric_piano": { # Rhodes/Wurlitzer
"synth": "fm", "envelope": "piano",
@@ -26,16 +23,17 @@ INSTRUMENTS = {
"detune": 6, "chorus": 0.2, "chorus_rate": 1.0,
"lowpass": 4000, "saturation": 0.15,
"tremolo_depth": 0.15, "tremolo_rate": 4.5,
"analog": 0.2,
},
"organ": {
"synth": "organ_synth", "envelope": "organ",
"chorus": 0.2, "chorus_rate": 5.5,
"lowpass": 5000,
"phaser": 0.15, "phaser_rate": 0.4,
"analog": 0.15,
},
"harpsichord": {
"synth": "pluck_synth", "envelope": "none",
"lowpass": 3500,
"synth": "harpsichord_synth", "envelope": "none",
},
"celesta": {
"synth": "fm", "envelope": "mallet",
@@ -63,10 +61,8 @@ INSTRUMENTS = {
"noise_mix": 0.03,
},
"cello": {
"synth": "strings_synth", "envelope": "bowed",
"detune": 2, "lowpass": 2500,
"synth": "cello_synth", "envelope": "bowed",
"humanize": 0.15, "vel_to_filter": 1000,
"noise_mix": 0.02,
},
"contrabass": {
"synth": "strings_synth", "envelope": "bowed",
@@ -84,21 +80,18 @@ INSTRUMENTS = {
# ── Woodwinds ──
"flute": {
"synth": "sine", "envelope": "strings",
"lowpass": 4000,
"humanize": 0.2, "noise_mix": 0.08,
"synth": "flute_synth", "envelope": "strings",
"humanize": 0.2,
"vel_to_filter": 2000,
},
"clarinet": {
"synth": "square", "envelope": "strings",
"lowpass": 3000,
"humanize": 0.15, "noise_mix": 0.05,
"synth": "clarinet_synth", "envelope": "strings",
"humanize": 0.15,
"vel_to_filter": 1500,
},
"oboe": {
"synth": "saw", "envelope": "strings",
"lowpass": 3500, "lowpass_q": 1.2,
"humanize": 0.15, "noise_mix": 0.04,
"synth": "oboe_synth", "envelope": "strings",
"humanize": 0.15,
"vel_to_filter": 1000,
},
"bassoon": {
@@ -110,16 +103,13 @@ INSTRUMENTS = {
# ── Brass ──
"trumpet": {
"synth": "saw", "envelope": "bowed",
"detune": 3, "lowpass": 4000, "lowpass_q": 1.1,
"synth": "trumpet_synth", "envelope": "bowed",
"humanize": 0.15, "vel_to_filter": 2000,
"saturation": 0.1,
},
"trombone": {
"synth": "saw", "envelope": "strings",
"detune": 3, "lowpass": 2500,
"synth": "trumpet_synth", "envelope": "strings",
"lowpass": 2500,
"humanize": 0.15, "vel_to_filter": 1500,
"saturation": 0.1,
},
"french_horn": {
"synth": "saw", "envelope": "strings",
@@ -143,34 +133,61 @@ INSTRUMENTS = {
# ── Plucked ──
"acoustic_guitar": {
"synth": "pluck_synth", "envelope": "none",
"lowpass": 4000,
"humanize": 0.2,
"synth": "acoustic_guitar_synth", "envelope": "none",
"humanize": 0.2, "saturation": 0.05,
},
"electric_guitar": {
"synth": "saw", "envelope": "pluck",
"detune": 5, "lowpass": 3500,
"synth": "electric_guitar_synth", "envelope": "none",
"cabinet": 1.0, "cabinet_brightness": 0.6,
"humanize": 0.15,
},
"clean_guitar": {
"synth": "electric_guitar_synth", "envelope": "none",
"cabinet": 1.0, "cabinet_brightness": 0.7,
"chorus": 0.15, "chorus_rate": 1.0,
"reverb": 0.2, "reverb_type": "spring",
"humanize": 0.15,
},
"crunch_guitar": {
"synth": "electric_guitar_synth", "envelope": "none",
"saturation": 0.3,
"distortion": 0.5, "distortion_drive": 4.0,
"cabinet": 1.0, "cabinet_brightness": 0.5,
"humanize": 0.15,
},
"distorted_guitar": {
"synth": "saw", "envelope": "pluck",
"detune": 8, "distortion": 0.6, "distortion_drive": 5.0,
"lowpass": 3000, "saturation": 0.3,
"synth": "electric_guitar_synth", "envelope": "none",
"saturation": 0.3,
"distortion": 0.7, "distortion_drive": 5.0,
"cabinet": 1.0, "cabinet_brightness": 0.5,
"humanize": 0.15,
},
"orange_crunch": {
"synth": "electric_guitar_synth", "envelope": "none",
"saturation": 0.4,
"distortion": 0.7, "distortion_drive": 6.0,
"cabinet": 1.0, "cabinet_brightness": 0.4,
"humanize": 0.15,
},
"metal_guitar": {
"synth": "electric_guitar_synth", "envelope": "none",
"saturation": 0.35,
"distortion": 0.8, "distortion_drive": 7.0,
"cabinet": 1.0, "cabinet_brightness": 0.5,
"highpass": 80,
"detune": 4,
"humanize": 0.1,
},
"bass_guitar": {
"synth": "triangle", "envelope": "pluck",
"lowpass": 1000,
"humanize": 0.1, "sub_osc": 0.2,
"synth": "bass_guitar_synth", "envelope": "none",
"humanize": 0.1, "sub_osc": 0.15,
},
"upright_bass": {
"synth": "triangle", "envelope": "pluck",
"lowpass": 800,
"synth": "upright_bass_synth", "envelope": "none",
"humanize": 0.15, "saturation": 0.1,
},
"harp": {
"synth": "pluck_synth", "envelope": "none",
"lowpass": 5000,
"synth": "harp_synth", "envelope": "none",
"reverb": 0.3, "reverb_type": "plate",
},
"sitar": {
@@ -183,6 +200,11 @@ INSTRUMENTS = {
"lowpass": 4000,
"reverb": 0.2,
},
"sitar": {
"synth": "sitar_synth", "envelope": "none",
"lowpass": 4500,
"humanize": 0.2,
},
# ── Synth presets ──
"synth_lead": {
@@ -191,6 +213,7 @@ INSTRUMENTS = {
"delay": 0.2, "delay_time": 0.25, "delay_feedback": 0.3,
"filter_attack": 0.01, "filter_decay": 0.3,
"filter_sustain": 0.2, "filter_amount": 3000,
"analog": 0.3,
},
"synth_pad": {
"synth": "supersaw", "envelope": "pad",
@@ -198,6 +221,7 @@ INSTRUMENTS = {
"chorus": 0.2,
"phaser": 0.3, "phaser_rate": 0.3,
"sub_osc": 0.2,
"analog": 0.4,
},
"synth_bass": {
"synth": "saw", "envelope": "pluck",
@@ -205,6 +229,7 @@ INSTRUMENTS = {
"filter_attack": 0.005, "filter_decay": 0.2,
"filter_sustain": 0.0, "filter_amount": 2000,
"sub_osc": 0.4,
"analog": 0.2,
},
"acid_bass": {
"synth": "saw", "envelope": "pad",
@@ -214,6 +239,7 @@ INSTRUMENTS = {
"filter_attack": 0.005, "filter_decay": 0.15,
"filter_sustain": 0.0, "filter_amount": 4000,
"vel_to_filter": 3000,
"analog": 0.3,
},
"808_bass": {
"synth": "sine", "envelope": "pluck",
@@ -231,8 +257,7 @@ INSTRUMENTS = {
"reverb": 0.3, "reverb_type": "plate",
},
"marimba": {
"synth": "sine", "envelope": "mallet",
"lowpass": 3000,
"synth": "marimba_synth", "envelope": "mallet",
},
"xylophone": {
"synth": "fm", "envelope": "pluck",
@@ -376,6 +401,34 @@ class DrumSound(Enum):
AGOGO_LOW = 68
GUIRO = 73
MARACAS = 70
# Tabla sounds
TABLA_NA = 86 # sharp dayan (right drum) rim hit
TABLA_TIN = 87 # open dayan ring
TABLA_GE = 88 # deep bayan (left drum) bass
TABLA_DHA = 89 # both drums (Na + Ge)
TABLA_TIT = 90 # light dayan flick
TABLA_KE = 91 # muted bayan slap
# Dhol sounds
DHOL_DAGGA = 92 # heavy bass side (dagga stick)
DHOL_TILLI = 93 # thin treble side (tilli stick)
DHOL_BOTH = 94 # both sides
# Dholak sounds
DHOLAK_GE = 95 # bass side (open palm)
DHOLAK_NA = 96 # treble side (fingers)
DHOLAK_TIT = 97 # light treble tap
# Mridangam sounds
MRIDANGAM_THAM = 98 # bass stroke (thoppi/left head)
MRIDANGAM_NAM = 99 # treble ring (valanthalai/right head)
MRIDANGAM_DIN = 100 # both heads
MRIDANGAM_THA = 101 # muted treble
# Djembe sounds
DJEMBE_BASS = 102 # open bass (center of head)
DJEMBE_TONE = 103 # open tone (edge, fingers together)
DJEMBE_SLAP = 104 # slap (edge, fingers spread, sharp crack)
# Metal kit — tighter, punchier, more attack
METAL_KICK = 105 # clicky, punchy, tight
METAL_SNARE = 106 # crack, bright, cutting
METAL_HAT = 107 # tight, short, precise
class _Hit:
@@ -1313,6 +1366,314 @@ Pattern._PRESETS["flamenco"] = dict(
],
)
# ── Tabla patterns ────────────────────────────────────────────────────────
# Shortcuts for tabla sounds
TNA = DrumSound.TABLA_NA
TTI = DrumSound.TABLA_TIN
TGE = DrumSound.TABLA_GE
TDHA = DrumSound.TABLA_DHA
TTIT = DrumSound.TABLA_TIT
TKE = DrumSound.TABLA_KE
# Teental — the most common taal (16 beats / 4+4+4+4)
Pattern._PRESETS["teental"] = dict(
name="teental",
time_signature="4/4",
beats=16.0,
hits=[
# Vibhag 1: Dha Dhin Dhin Dha
_h(TDHA, 0.0), _h(TNA, 1.0), _h(TNA, 2.0), _h(TDHA, 3.0),
# Vibhag 2: Dha Dhin Dhin Dha
_h(TDHA, 4.0), _h(TNA, 5.0), _h(TNA, 6.0), _h(TDHA, 7.0),
# Vibhag 3 (khali): Dha Tin Tin Ta
_h(TDHA, 8.0), _h(TTI, 9.0), _h(TTI, 10.0), _h(TNA, 11.0),
# Vibhag 4: Dha Dhin Dhin Dha
_h(TDHA, 12.0), _h(TNA, 13.0), _h(TNA, 14.0), _h(TDHA, 15.0),
],
)
# Jhaptaal — 10 beats (2+3+2+3)
Pattern._PRESETS["jhaptaal"] = dict(
name="jhaptaal",
time_signature="4/4",
beats=10.0,
hits=[
# Dhi Na | Dhi Dhi Na | Ti Na | Dhi Dhi Na
_h(TDHA, 0.0), _h(TNA, 1.0),
_h(TDHA, 2.0), _h(TDHA, 3.0), _h(TNA, 4.0),
_h(TTI, 5.0), _h(TNA, 6.0),
_h(TDHA, 7.0), _h(TDHA, 8.0), _h(TNA, 9.0),
],
)
# Rupak taal — 7 beats (3+2+2), starts on khali (unusual)
Pattern._PRESETS["rupak"] = dict(
name="rupak",
time_signature="7/4",
beats=7.0,
hits=[
# Tin Tin Na | Dhi Na | Dhi Na
_h(TTI, 0.0), _h(TTI, 1.0), _h(TNA, 2.0),
_h(TDHA, 3.0), _h(TNA, 4.0),
_h(TDHA, 5.0), _h(TNA, 6.0),
],
)
# Dadra — 6 beats (3+3), light and folk
Pattern._PRESETS["dadra"] = dict(
name="dadra",
time_signature="6/4",
beats=6.0,
hits=[
# Dha Dhi Na | Dha Tin Na
_h(TDHA, 0.0), _h(TNA, 1.0), _h(TNA, 2.0),
_h(TDHA, 3.0), _h(TTI, 4.0), _h(TNA, 5.0),
],
)
# Keherwa — 8 beats (4+4), the most common light taal
Pattern._PRESETS["keherwa"] = dict(
name="keherwa",
time_signature="4/4",
beats=8.0,
hits=[
# Dha Ge Na Ti | Na Ke Dhi Na
_h(TDHA, 0.0), _h(TGE, 1.0), _h(TNA, 2.0), _h(TTIT, 3.0),
_h(TNA, 4.0), _h(TKE, 5.0), _h(TDHA, 6.0), _h(TNA, 7.0),
],
)
# Tabla solo theka — fast 16th note pattern for rhythmic display
Pattern._PRESETS["tabla solo"] = dict(
name="tabla solo",
time_signature="4/4",
beats=4.0,
hits=[
_h(TDHA, 0.0), _h(TTIT, 0.25), _h(TTIT, 0.5), _h(TKE, 0.75),
_h(TNA, 1.0), _h(TTIT, 1.25), _h(TGE, 1.5), _h(TNA, 1.75),
_h(TDHA, 2.0), _h(TNA, 2.25), _h(TTI, 2.5), _h(TNA, 2.75),
_h(TDHA, 3.0), _h(TTIT, 3.5), _h(TGE, 3.75),
],
)
# ── Metal kit patterns ────────────────────────────────────────────────────
MK = DrumSound.METAL_KICK
MS = DrumSound.METAL_SNARE
MH = DrumSound.METAL_HAT
# Metal double kick — the classic thrash/death metal beat
Pattern._PRESETS["double kick"] = dict(
name="double kick",
time_signature="4/4",
beats=4.0,
hits=[
# Double kick 16ths, snare on 2 and 4, tight hats
*[_h(MK, i * 0.25) for i in range(16)],
_h(MS, 1.0), _h(MS, 3.0),
*[_h(MH, i * 0.5) for i in range(8)],
],
)
# Metal blast — blast beat with metal kit sounds
Pattern._PRESETS["metal blast"] = dict(
name="metal blast",
time_signature="4/4",
beats=4.0,
hits=[
*[_h(MK, i * 0.25) for i in range(16)],
*[_h(MS, i * 0.25) for i in range(16)],
*[_h(MH, i * 0.25) for i in range(16)],
],
)
# Metal groove — half time with double kick fills
Pattern._PRESETS["metal groove"] = dict(
name="metal groove",
time_signature="4/4",
beats=4.0,
hits=[
_h(MK, 0.0), _h(MH, 0.0),
_h(MH, 0.5),
_h(MS, 1.0), _h(MH, 1.0),
_h(MK, 1.5), _h(MH, 1.5),
_h(MK, 2.0), _h(MH, 2.0),
_h(MK, 2.25),
_h(MK, 2.5), _h(MH, 2.5),
_h(MK, 2.75),
_h(MS, 3.0), _h(MH, 3.0),
_h(MH, 3.5),
],
)
# Metal gallop — the classic Iron Maiden triplet feel
Pattern._PRESETS["metal gallop"] = dict(
name="metal gallop",
time_signature="4/4",
beats=4.0,
hits=[
_h(MK, 0.0), _h(MH, 0.0),
_h(MK, 0.33), _h(MK, 0.67),
_h(MS, 1.0), _h(MH, 1.0),
_h(MK, 1.33), _h(MK, 1.67),
_h(MK, 2.0), _h(MH, 2.0),
_h(MK, 2.33), _h(MK, 2.67),
_h(MS, 3.0), _h(MH, 3.0),
_h(MK, 3.33), _h(MK, 3.67),
],
)
# Tabla tiri-kita — rapid 16th-note dayan patter
Pattern._PRESETS["tiri kita"] = dict(
name="tiri kita",
time_signature="4/4",
beats=4.0,
hits=[
# Ti ri ki ta | dha ti ri ki | ta ka dhi na | dha — ti dha
_h(TTIT, 0.0), _h(TTIT, 0.25), _h(TKE, 0.5), _h(TNA, 0.75),
_h(TDHA, 1.0), _h(TTIT, 1.25), _h(TTIT, 1.5), _h(TKE, 1.75),
_h(TNA, 2.0), _h(TKE, 2.25), _h(TDHA, 2.5), _h(TNA, 2.75),
_h(TDHA, 3.0), _h(TTIT, 3.5), _h(TDHA, 3.75),
],
)
# ── Dhol patterns ────────────────────────────────────────────────────────
DD = DrumSound.DHOL_DAGGA
DT = DrumSound.DHOL_TILLI
DB = DrumSound.DHOL_BOTH
# Bhangra — the classic punjabi groove
Pattern._PRESETS["bhangra"] = dict(
name="bhangra",
time_signature="4/4",
beats=4.0,
hits=[
# Dagga on 1, tilli fills, both on 3
_h(DD, 0.0), _h(DT, 0.5), _h(DT, 0.75),
_h(DT, 1.0), _h(DT, 1.5),
_h(DB, 2.0), _h(DT, 2.5), _h(DT, 2.75),
_h(DD, 3.0), _h(DT, 3.25), _h(DT, 3.5), _h(DT, 3.75),
],
)
# Dhol chaal — driving folk pattern
Pattern._PRESETS["dhol chaal"] = dict(
name="dhol chaal",
time_signature="4/4",
beats=4.0,
hits=[
_h(DB, 0.0), _h(DT, 0.25), _h(DD, 0.5),
_h(DT, 1.0), _h(DT, 1.25), _h(DT, 1.5), _h(DD, 1.75),
_h(DB, 2.0), _h(DT, 2.25), _h(DD, 2.5),
_h(DT, 3.0), _h(DT, 3.25), _h(DT, 3.5), _h(DT, 3.75),
],
)
# ── Dholak patterns ─────────────────────────────────────────────────────
DKG = DrumSound.DHOLAK_GE
DKN = DrumSound.DHOLAK_NA
DKT = DrumSound.DHOLAK_TIT
# Qawwali — driving devotional pattern
Pattern._PRESETS["qawwali"] = dict(
name="qawwali",
time_signature="4/4",
beats=4.0,
hits=[
_h(DKG, 0.0), _h(DKN, 0.5), _h(DKT, 0.75),
_h(DKN, 1.0), _h(DKG, 1.5),
_h(DKG, 2.0), _h(DKN, 2.5), _h(DKT, 2.75),
_h(DKN, 3.0), _h(DKT, 3.25), _h(DKN, 3.5), _h(DKG, 3.75),
],
)
# Dholak folk — light folk music pattern
Pattern._PRESETS["dholak folk"] = dict(
name="dholak folk",
time_signature="4/4",
beats=4.0,
hits=[
_h(DKG, 0.0), _h(DKN, 1.0), _h(DKT, 1.5),
_h(DKG, 2.0), _h(DKN, 3.0), _h(DKT, 3.5),
],
)
# ── Mridangam patterns ──────────────────────────────────────────────────
MTH = DrumSound.MRIDANGAM_THAM
MN = DrumSound.MRIDANGAM_NAM
MD = DrumSound.MRIDANGAM_DIN
MTA = DrumSound.MRIDANGAM_THA
# Adi talam — the fundamental Carnatic rhythm (8 beats: 4+2+2)
Pattern._PRESETS["adi talam"] = dict(
name="adi talam",
time_signature="4/4",
beats=8.0,
hits=[
# Tha Din | Tha ka | Dhi na | Tha ka
_h(MD, 0.0), _h(MN, 1.0),
_h(MTH, 2.0), _h(MTA, 3.0),
_h(MD, 4.0), _h(MN, 5.0),
_h(MTH, 6.0), _h(MTA, 7.0),
],
)
# Mridangam korvai — rhythmic cadence pattern
Pattern._PRESETS["mridangam korvai"] = dict(
name="mridangam korvai",
time_signature="4/4",
beats=4.0,
hits=[
_h(MD, 0.0), _h(MN, 0.25), _h(MTA, 0.5), _h(MN, 0.75),
_h(MTH, 1.0), _h(MN, 1.25), _h(MN, 1.5), _h(MTH, 1.75),
_h(MD, 2.0), _h(MTA, 2.25), _h(MN, 2.5), _h(MTA, 2.75),
_h(MD, 3.0), _h(MN, 3.5), _h(MD, 3.75),
],
)
# ── Djembe patterns ─────────────────────────────────────────────────────
JB = DrumSound.DJEMBE_BASS
JT = DrumSound.DJEMBE_TONE
JS = DrumSound.DJEMBE_SLAP
# Djembe — standard West African pattern
Pattern._PRESETS["djembe"] = dict(
name="djembe",
time_signature="4/4",
beats=4.0,
hits=[
_h(JB, 0.0), _h(JT, 0.5), _h(JT, 0.75),
_h(JS, 1.0), _h(JT, 1.5),
_h(JB, 2.0), _h(JT, 2.5), _h(JT, 2.75),
_h(JS, 3.0), _h(JT, 3.25), _h(JS, 3.5),
],
)
# Kuku — traditional Guinean harvest dance rhythm
Pattern._PRESETS["kuku"] = dict(
name="kuku",
time_signature="4/4",
beats=4.0,
hits=[
_h(JS, 0.0), _h(JS, 0.5),
_h(JT, 1.0), _h(JB, 1.5),
_h(JS, 2.0), _h(JS, 2.5),
_h(JT, 3.0), _h(JT, 3.25), _h(JB, 3.5),
],
)
# Soli — powerful Mandinka rhythm
Pattern._PRESETS["soli"] = dict(
name="soli",
time_signature="4/4",
beats=4.0,
hits=[
_h(JB, 0.0), _h(JT, 0.25), _h(JS, 0.5), _h(JT, 0.75),
_h(JB, 1.0), _h(JS, 1.5),
_h(JB, 2.0), _h(JT, 2.25), _h(JS, 2.5), _h(JT, 2.75),
_h(JB, 3.0), _h(JT, 3.5), _h(JS, 3.75),
],
)
# ── Fill presets ──────────────────────────────────────────────────────────
Pattern._FILLS["rock"] = dict(
@@ -1632,6 +1993,9 @@ class Part:
tremolo_rate: float = 5.0,
phaser: float = 0.0,
phaser_rate: float = 0.5,
cabinet: float = 0.0,
cabinet_brightness: float = 0.5,
analog: float = 0.0,
fm_ratio: float = 2.0,
fm_index: float = 3.0):
self.name = name
@@ -1675,8 +2039,13 @@ class Part:
self.tremolo_rate = tremolo_rate
self.phaser_mix = phaser
self.phaser_rate = phaser_rate
self.cabinet = cabinet
self.cabinet_brightness = cabinet_brightness
self.analog = analog
self.fm_ratio = fm_ratio
self.fm_index = fm_index
self._system = "western" # default, overridden by Score.part()
self._fretboard = None # set by Score.part(fretboard=...)
self.notes: list[Note] = []
self._drum_hits: list[_Hit] = []
self._drum_pattern_beats: float = 0.0
@@ -1692,7 +2061,7 @@ class Part:
"""
if isinstance(tone_or_string, str):
from .tones import Tone
tone_or_string = Tone.from_string(tone_or_string, system="western")
tone_or_string = Tone.from_string(tone_or_string, system=self._system)
if isinstance(duration, (int, float)):
duration = _RawDuration(duration)
self.notes.append(Note(tone=tone_or_string, duration=duration, velocity=velocity))
@@ -1760,6 +2129,7 @@ class Part:
"delay_mix": self.delay_mix, "delay_time": self.delay_time,
"delay_feedback": self.delay_feedback,
"phaser_mix": self.phaser_mix, "phaser_rate": self.phaser_rate,
"cabinet": self.cabinet, "cabinet_brightness": self.cabinet_brightness,
"highpass": self.highpass, "highpass_q": self.highpass_q,
"lowpass": self.lowpass, "lowpass_q": self.lowpass_q,
"distortion_mix": self.distortion_mix,
@@ -1977,6 +2347,80 @@ class Part:
return self
def strum(self, chord_name: str, duration=Duration.QUARTER, *,
direction: str = "down", velocity: int = 100,
strum_time: float = 0.08) -> "Part":
"""Strum a chord using the part's fretboard fingering.
Looks up the chord on the fretboard, gets the fingering, and
adds each string as a rapid sequence with tiny time offsets —
like a real guitar strum. Muted strings are skipped.
Args:
chord_name: Chord name (e.g. ``"Am"``, ``"G"``, ``"D"``).
duration: Total duration of the strum (default QUARTER).
direction: ``"down"`` (low→high, default) or ``"up"`` (high→low).
velocity: Base velocity (each string gets slight variation).
strum_time: Time in beats for the full strum sweep
(default 0.03 = very fast). Larger values = slower,
more audible strum. Try 0.1 for a lazy strum.
Returns:
Self for chaining.
Example::
>>> guitar = score.part("guitar", instrument="acoustic_guitar",
... fretboard=Fretboard.guitar())
>>> guitar.strum("Am", Duration.HALF)
>>> guitar.strum("G", Duration.HALF, direction="up")
"""
if self._fretboard is None:
raise ValueError(
"Cannot strum without a fretboard. "
"Set fretboard= when creating the part."
)
from .charts import CHARTS
# Get the fingering
system_name = self._system if isinstance(self._system, str) else "western"
if system_name in CHARTS:
chart = CHARTS[system_name]
else:
chart = CHARTS["western"]
if chord_name in chart:
fingering = chart[chord_name].fingering(fretboard=self._fretboard)
else:
# Try fretboard.chord() as fallback
fingering = self._fretboard.chord(chord_name)
# Get the sounding tones (skips muted strings)
tones = fingering.tones # list of Tone objects, high to low
if not tones:
self.rest(duration)
return self
# Order: down strum = low to high (reverse since tones are high-to-low)
if direction == "down":
strum_tones = list(reversed(tones))
else:
strum_tones = list(tones)
if hasattr(duration, 'value'):
total_beats = duration.value
else:
total_beats = float(duration)
# Build a Chord — all strings ring together through the
# shared body resonance, like a real guitar
from .chords import Chord as ChordClass
chord_obj = ChordClass(tones=strum_tones)
self.add(chord_obj, total_beats, velocity=velocity)
return self
@property
def is_drums(self) -> bool:
"""True if this part contains drum hits."""
@@ -2072,13 +2516,17 @@ class Score:
"""
def __init__(self, time_signature="4/4", bpm=120, swing: float = 0.0,
drum_humanize: float = 0.15):
drum_humanize: float = 0.15, system: str = "western",
temperament: str = "equal", reference_pitch: float = 440.0):
if isinstance(time_signature, str):
self.time_signature = TimeSignature.from_string(time_signature)
else:
self.time_signature = time_signature
self.bpm = bpm
self.swing = swing
self.system = system
self.temperament = temperament
self.reference_pitch = reference_pitch
self._drum_humanize = drum_humanize
self.notes: list[Note] = []
self.parts: dict[str, Part] = {}
@@ -2175,8 +2623,12 @@ class Score:
tremolo_rate: float = None,
phaser: float = None,
phaser_rate: float = None,
cabinet: float = None,
cabinet_brightness: float = None,
analog: float = None,
fm_ratio: float = None,
fm_index: float = None) -> Part:
fm_index: float = None,
fretboard=None) -> Part:
"""Create a named part with its own synth voice and effects.
Args:
@@ -2285,6 +2737,8 @@ class Score:
"saturation": saturation,
"tremolo_depth": tremolo_depth, "tremolo_rate": tremolo_rate,
"phaser": phaser, "phaser_rate": phaser_rate,
"cabinet": cabinet, "cabinet_brightness": cabinet_brightness,
"analog": analog,
"fm_ratio": fm_ratio, "fm_index": fm_index,
}
for k, v in _locals.items():
@@ -2294,6 +2748,8 @@ class Score:
merged = {**_defaults, **explicit}
p = Part(name, **merged)
p._system = self.system
p._fretboard = fretboard
self.parts[name] = p
return p
+232 -6
View File
@@ -2,18 +2,53 @@ from ._statics import (
TEMPERAMENTS, TONES, DEGREES, SCALES,
INDIAN_SCALES, ARABIC_SCALES, JAPANESE_SCALES,
BLUES_SCALES, GAMELAN_SCALES, SYSTEMS,
TONES_SHRUTI, DEGREES_SHRUTI, SHRUTI_SCALES,
TONES_ARABIC_24, DEGREES_ARABIC_24, ARABIC_24_SCALES,
TONES_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):
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
# 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):
@@ -25,13 +60,56 @@ class System:
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) to the canonical name.
"""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
@@ -139,11 +217,159 @@ class System:
def __repr__(self):
return f"<System semitones={self.semitones!r}>"
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]),
"arabic": System(tone_names=TONES["arabic"], degrees=DEGREES["arabic"], scales=ARABIC_SCALES[12]),
"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]),
"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),
"maqam": System(tone_names=TONES_ARABIC_24, degrees=DEGREES_ARABIC_24,
scales=ARABIC_24_SCALES, c_index=5),
"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",
]),
}
+131 -62
View File
@@ -47,15 +47,28 @@ class Tone:
alt_names = []
if isinstance(name, str):
try:
parsed_octave = int("".join([c for c in filter(str.isdigit, name)]))
except ValueError:
parsed_octave = None
# 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:]
if parsed_octave is not None:
name = name.replace(str(parsed_octave), "")
if octave is None:
octave = parsed_octave
# 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
self.name = name
self.octave = octave
@@ -343,12 +356,15 @@ class Tone:
Returns:
A new ``Tone`` instance.
"""
try:
octave = int("".join([c for c in filter(str.isdigit, s)]))
except ValueError:
octave = None
tone = s.replace(str(octave), "") if octave else s
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)
@@ -389,19 +405,20 @@ class Tone:
import math
if hz <= 0:
raise ValueError("Frequency must be positive")
# Semitones from A4
semitones_from_a4 = 12 * math.log2(hz / REFERENCE_A)
semitones = round(semitones_from_a4)
# A4 is index 0 in the Western system, octave 4
# Convert to absolute position from C0
a4_from_c0 = ((0 - C_INDEX) % 12) + (4 * 12) # = 57
abs_pos = a4_from_c0 + semitones
octave = abs_pos // 12
relative = abs_pos % 12
index = (relative + C_INDEX) % 12
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
@@ -417,13 +434,19 @@ class Tone:
>>> 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
if isinstance(system, str):
from .systems import SYSTEMS
system = SYSTEMS[system]
return klass.from_index(index, octave=octave, system=system)
@classmethod
@@ -442,10 +465,27 @@ class Tone:
"""
tone_names = system.tone_names[i]
if prefer_flats and len(tone_names) > 1:
tone = tone_names[1] # flat spelling (e.g. "Bb")
# 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] # sharp spelling (e.g. "A#")
return klass(name=tone, octave=octave, system=system)
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) -> int:
@@ -461,7 +501,15 @@ class Tone:
canonical = self.system.resolve_name(self.name)
if canonical is None:
raise ValueError(f"Tone {self.name!r} not found in system")
return self.system.tones.index(canonical)
# 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!")
@@ -475,19 +523,21 @@ 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!"
)
# Convert to absolute semitones from C0
note_from_c0 = ((self._index - C_INDEX) % mod) + (octave * mod)
c_idx = getattr(self.system, 'c_index', C_INDEX)
# 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)
@@ -538,9 +588,10 @@ class Tone:
'octave'
"""
semitones = abs(self - other)
octaves = semitones // 12
remainder = semitones % 12
name = self._INTERVAL_NAMES.get(remainder, f"{remainder} semitones")
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:
@@ -563,6 +614,12 @@ class Tone:
"""
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)
@@ -604,42 +661,43 @@ class Tone:
return 1200 * math.log2(f2 / f1)
def circle_of_fifths(self) -> list[Tone]:
"""The 12 tones of the circle of fifths starting from this tone.
"""The circle of fifths starting from this tone.
Each step ascends by a perfect fifth (7 semitones). After 12
steps 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.
Clockwise = add sharps: C G D A E B F# → ...
Counter-clockwise = add flats (see ``circle_of_fourths``).
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 12 Tones.
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(12):
for _ in range(n):
tones.append(t)
t = t.add(7)
t = t.add(fifth)
return tones
def circle_of_fourths(self) -> list[Tone]:
"""The 12 tones of the circle of fourths starting from this tone.
"""The circle of fourths starting from this tone.
Each step ascends by a perfect fourth (5 semitones) the
reverse direction of the circle of fifths.
Clockwise = add flats: C F Bb Eb Ab ...
Each step ascends by a perfect fourth the reverse direction
of the circle of fifths.
Returns:
A list of 12 Tones.
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(12):
for _ in range(n):
tones.append(t)
t = t.add(5)
t = t.add(fourth)
return tones
@property
@@ -695,21 +753,32 @@ class Tone:
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)
# 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)
if period != 2.0 and temperament == "equal":
# Non-octave period (e.g. Bohlen-Pierce tritave=3.0):
# generate ratios as period^(n/tones) instead of 2^(n/tones)
import sympy
pitch_scale = [period ** sympy.Rational(i, tones) for i in range(tones + 1)]
else:
pitch_scale = TEMPERAMENTS[temperament](tones)
octave = self.octave if self.octave is not None else 4
note_from_c0 = ((self._index - C_INDEX) % tones) + (octave * tones)
a4_from_c0 = ((0 - C_INDEX) % tones) + (4 * tones) # A4
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
+187 -10
View File
@@ -5320,7 +5320,7 @@ def test_supersaw_wave():
@needs_portaudio
def test_all_synths_in_enum():
from pytheory.play import Synth
assert len(Synth) == 13
assert len(Synth) == 27
for s in Synth:
wave = s(440, n_samples=1000)
assert len(wave) == 1000
@@ -6467,11 +6467,8 @@ def test_instrument_piano():
from pytheory import Score, Duration
score = Score("4/4", bpm=120)
p = score.part("p", instrument="piano")
assert p.synth == "fm"
assert p.envelope == "piano"
assert p.detune == 5
assert p.lowpass == 6000
assert p.chorus_mix == 0.1
assert p.synth == "piano_synth"
assert p.vel_to_filter == 3000
def test_instrument_violin():
@@ -6488,12 +6485,9 @@ def test_instrument_violin():
def test_instrument_override():
from pytheory import Score
score = Score("4/4", bpm=120)
# Explicit synth overrides the preset's "fm"
# Explicit synth overrides the preset
p = score.part("p", instrument="piano", synth="saw")
assert p.synth == "saw"
# Other preset values still apply
assert p.envelope == "piano"
assert p.detune == 5
def test_instrument_unknown_raises():
@@ -6534,3 +6528,186 @@ def test_instrument_808_bass():
assert p.lowpass_q == 1.5
assert p.synth == "sine"
assert p.envelope == "pluck"
# ── Non-12-TET / Microtonal systems ─────────────────────────────────────────
from pytheory import TET
def test_tet_factory_creates_system():
edo17 = TET(17)
assert len(edo17.tone_names) == 17
assert edo17.semitones == 17
def test_tet_factory_numbered_tones():
edo17 = TET(17)
t = Tone("0", octave=4, system=edo17)
assert t.frequency == pytest.approx(440.0, rel=1e-3)
# One octave up
t_up = t.add(17)
assert t_up.frequency == pytest.approx(880.0, rel=1e-3)
def test_tet_factory_custom_names():
names = ["A", "B", "C", "D", "E"]
edo5 = TET(5, names=names)
assert len(edo5.tone_names) == 5
t = Tone("A", octave=4, system=edo5)
assert t.frequency == pytest.approx(440.0, rel=1e-3)
def test_tet_factory_wrong_name_count():
with pytest.raises(ValueError):
TET(5, names=["A", "B", "C"])
def test_19tet_system():
sys19 = SYSTEMS["19-tet"]
assert sys19.semitones == 19
a = Tone("A", octave=4, system=sys19)
assert a.frequency == pytest.approx(440.0, rel=1e-3)
# Octave should double
a5 = a.add(19)
assert a5.frequency == pytest.approx(880.0, rel=1e-3)
def test_19tet_scale():
sys19 = SYSTEMS["19-tet"]
ts = TonedScale(system=sys19, tonic=Tone("C", octave=4, system=sys19))
major = ts["major"]
assert len(major.tones) == 8 # 7 + octave
def test_31tet_system():
sys31 = SYSTEMS["31-tet"]
assert sys31.semitones == 31
a = Tone("A", octave=4, system=sys31)
assert a.frequency == pytest.approx(440.0, rel=1e-3)
def test_shruti_system():
shruti = SYSTEMS["shruti"]
assert shruti.semitones == 22
sa = Tone("Sa", octave=4, system=shruti)
# Sa should be near C4 (261.63 Hz) — not exact due to 22-TET
assert 250 < sa.frequency < 270
def test_shruti_octave():
shruti = SYSTEMS["shruti"]
sa4 = Tone("Sa", octave=4, system=shruti)
sa5 = sa4.add(22)
assert sa5.frequency == pytest.approx(sa4.frequency * 2, rel=1e-3)
def test_shruti_bhairav_scale():
shruti = SYSTEMS["shruti"]
ts = TonedScale(system=shruti, tonic=Tone("Sa", octave=4, system=shruti))
bhairav = ts["bhairav"]
names = [t.name for t in bhairav.tones]
assert names[0] == "Sa"
assert "komal Re" in names # the microtonal komal Re
assert len(bhairav.tones) == 8
def test_maqam_system():
maqam = SYSTEMS["maqam"]
assert maqam.semitones == 24
do = Tone("Do", octave=4, system=maqam)
assert 250 < do.frequency < 270
def test_maqam_rast_has_quarter_tones():
maqam = SYSTEMS["maqam"]
ts = TonedScale(system=maqam, tonic=Tone("Do", octave=4, system=maqam))
rast = ts["rast"]
names = [t.name for t in rast.tones]
# Rast should contain quarter-tone positions
assert any("" in n or "" in n for n in names)
def test_slendro_system():
slendro = SYSTEMS["slendro"]
assert slendro.semitones == 5
ji = Tone("ji", octave=4, system=slendro)
# 5 steps = octave
ji_up = ji.add(5)
assert ji_up.frequency == pytest.approx(ji.frequency * 2, rel=1e-3)
def test_pelog_system():
pelog = SYSTEMS["pelog"]
assert pelog.semitones == 9
ts = TonedScale(system=pelog, tonic=Tone("ji", octave=4, system=pelog))
full_pelog = ts["pelog"]
assert len(full_pelog.tones) == 8
def test_thai_system():
thai = SYSTEMS["thai"]
assert thai.semitones == 7
do = Tone("do", octave=4, system=thai)
# 7 steps = octave
do_up = do.add(7)
assert do_up.frequency == pytest.approx(do.frequency * 2, rel=1e-3)
def test_turkish_makam_system():
makam = SYSTEMS["makam"]
assert makam.semitones == 53
ts = TonedScale(system=makam, tonic=Tone("Do", octave=4, system=makam))
rast = ts["rast"]
assert len(rast.tones) == 8
def test_carnatic_system():
carnatic = SYSTEMS["carnatic"]
assert carnatic.semitones == 72
ts = TonedScale(system=carnatic, tonic=Tone("Sa", octave=4, system=carnatic))
shankarabharanam = ts["shankarabharanam"]
assert len(shankarabharanam.tones) == 8
def test_circle_of_fifths_19tet():
sys19 = SYSTEMS["19-tet"]
c = Tone("C", octave=4, system=sys19)
cof = c.circle_of_fifths()
assert len(cof) == 19 # should cycle through all 19 tones
def test_circle_of_fifths_western_unchanged():
"""Existing 12-TET circle of fifths should not be affected."""
c = Tone("C", octave=4, system="western")
cof = c.circle_of_fifths()
assert len(cof) == 12
assert cof[0].name == "C"
assert cof[1].name == "G"
def test_from_frequency_non12():
sys19 = SYSTEMS["19-tet"]
t = Tone.from_frequency(440.0, system=sys19)
assert t.name == "A"
assert t.octave == 4
def test_score_system_param():
"""Score passes system to parts for string→Tone resolution."""
from pytheory import Score, Duration
shruti = SYSTEMS["shruti"]
score = Score("4/4", bpm=120, system=shruti)
p = score.part("test", synth="sine")
assert p._system is shruti
# String "Sa" should resolve via shruti system, not western
p.add(Tone("Sa", octave=4, system=shruti), Duration.QUARTER)
assert len(p.notes) == 1
def test_interval_to_non12():
sys19 = SYSTEMS["19-tet"]
a = Tone("A", octave=4, system=sys19)
a5 = a.add(19)
result = a.interval_to(a5)
assert "octave" in result
Generated
+1 -1
View File
@@ -707,7 +707,7 @@ wheels = [
[[package]]
name = "pytheory"
version = "0.32.0"
version = "0.34.0"
source = { editable = "." }
dependencies = [
{ name = "numeral" },