mirror of
https://github.com/kennethreitz/pytheory.git
synced 2026-06-05 23:00:20 +00:00
Compare commits
33 Commits
drums-as-part
...
v0.34.0
| Author | SHA1 | Date | |
|---|---|---|---|
| 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 |
+111
@@ -2,6 +2,117 @@
|
||||
|
||||
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:**
|
||||
- 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
|
||||
|
||||
@@ -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.
|
||||
@@ -29,6 +29,50 @@ Score:
|
||||
|
||||
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
|
||||
|
||||
+238
-9
@@ -32,13 +32,26 @@ It's a well-tested order that sounds good by default.
|
||||
|
||||
Effects are applied in this fixed order::
|
||||
|
||||
Signal --> Distortion --> Chorus --> Lowpass Filter --> Delay --> Reverb --> Mix
|
||||
Signal --> Saturation --> Tremolo --> Distortion --> Chorus --> Phaser
|
||||
--> Highpass --> Lowpass --> Delay --> Reverb --> Mix
|
||||
|
||||
- **Distortion** first: drives the raw signal before filtering (like
|
||||
plugging a guitar into a fuzz pedal before the amp).
|
||||
- **Chorus** second: thickens the distorted signal.
|
||||
- **Lowpass** third: shapes the tone (like a tone knob on an amp).
|
||||
- **Delay** fourth: echoes the shaped signal (tap delay / tape echo).
|
||||
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.
|
||||
- **Chorus** fourth: thickens the signal.
|
||||
- **Phaser** fifth: swept allpass notches.
|
||||
- **Highpass** sixth: removes low-frequency mud.
|
||||
- **Lowpass** seventh: shapes the tone (like a tone knob on an amp).
|
||||
- **Delay** eighth: echoes the shaped signal (tap delay / tape echo).
|
||||
- **Reverb** last: places everything in a space (room / hall).
|
||||
|
||||
Distortion
|
||||
@@ -498,6 +511,221 @@ whole mix will gasp for air:
|
||||
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
|
||||
----------
|
||||
|
||||
@@ -528,9 +756,10 @@ processes each section independently:
|
||||
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``, ``reverb``,
|
||||
``reverb_decay``, ``delay``, ``delay_time``, ``delay_feedback``,
|
||||
``distortion``, ``distortion_drive``, ``chorus``, ``volume``.
|
||||
Any parameter can be automated: ``lowpass``, ``lowpass_q``, ``highpass``,
|
||||
``reverb``, ``reverb_decay``, ``delay``, ``delay_time``, ``delay_feedback``,
|
||||
``distortion``, ``distortion_drive``, ``chorus``, ``phaser``, ``phaser_rate``,
|
||||
``saturation``, ``tremolo_depth``, ``tremolo_rate``, ``volume``.
|
||||
|
||||
LFO Automation
|
||||
--------------
|
||||
|
||||
+89
-2
@@ -1,7 +1,7 @@
|
||||
Synthesizers
|
||||
============
|
||||
|
||||
PyTheory includes 10 built-in waveforms and 8 ADSR envelope presets.
|
||||
PyTheory includes 13 built-in waveforms and 10 ADSR envelope presets.
|
||||
Every sound is generated from scratch -- no samples or external audio
|
||||
files needed.
|
||||
|
||||
@@ -247,6 +247,8 @@ PyTheory includes 8 presets:
|
||||
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
|
||||
|
||||
@@ -260,8 +262,10 @@ Name Character
|
||||
``"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, long ring -- vibraphone, tubular
|
||||
``"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
|
||||
=============== ================================================
|
||||
@@ -341,6 +345,89 @@ 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")
|
||||
|
||||
Instrument Presets
|
||||
------------------
|
||||
|
||||
Instead of choosing synth + envelope + effects manually, use an
|
||||
instrument preset — 38 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
|
||||
|
||||
**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, distorted_guitar,
|
||||
bass_guitar, upright_bass, harp, sitar, koto
|
||||
|
||||
**Synth**: synth_lead, synth_pad, synth_bass, acid_bass, 808_bass
|
||||
|
||||
**Percussion**: vibraphone, marimba, xylophone, glockenspiel, tubular_bells
|
||||
|
||||
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
|
||||
----------------------------------
|
||||
|
||||
|
||||
+7
-4
@@ -77,11 +77,14 @@ What's Inside
|
||||
numbers), scale recommendation, modulation, voice leading
|
||||
- **Sequencing** — Score, Parts, arpeggiator, legato/glide, velocity,
|
||||
swing, humanize, tempo changes, song sections with repeat
|
||||
- **Synthesis** — 10 waveforms, 8 envelopes, detune, stereo pan/spread,
|
||||
58 drum patterns (stereo panned), 21 fills
|
||||
- **Synthesis** — 13 waveforms (including Karplus-Strong pluck, Hammond organ,
|
||||
bowed string), 10 envelopes, 38 instrument presets, configurable FM,
|
||||
sub-oscillator, noise layer, filter envelope, velocity-to-brightness,
|
||||
detune, stereo pan/spread, 58 drum patterns (stereo panned), 21 fills
|
||||
- **Effects** — reverb (algorithmic + 7 convolution IRs, stereo), delay,
|
||||
lowpass (with resonance), distortion, chorus, sidechain compression,
|
||||
automation, LFOs. Master bus compressor/limiter
|
||||
lowpass/highpass (with resonance), distortion, saturation, chorus,
|
||||
phaser, tremolo, sidechain compression, automation, LFOs. Master bus
|
||||
compressor/limiter
|
||||
- **Instruments** — 25 presets with fingering generation
|
||||
- **Output** — stereo playback, WAV export, MIDI import/export
|
||||
- **Interface** — REPL with tab completion, CLI (15 commands), ``pytheory demo``
|
||||
|
||||
+122
-1
@@ -1116,6 +1116,125 @@ def temple_bell():
|
||||
play_song(score)
|
||||
|
||||
|
||||
def cinematic_showcase():
|
||||
"""Cinematic orchestral showcase — tubular bells, strings, organ, harp, acid bass."""
|
||||
score = Score("4/4", bpm=100)
|
||||
|
||||
# Tubular bells — dramatic intro
|
||||
bells = score.part("bells", instrument="tubular_bells",
|
||||
reverb=0.5, reverb_type="cathedral")
|
||||
bells.add("A3", Duration.WHOLE)
|
||||
for _ in range(7):
|
||||
bells.rest(Duration.WHOLE)
|
||||
|
||||
# String ensemble — lush wide pad
|
||||
strings = score.part("strings", instrument="string_ensemble",
|
||||
reverb=0.4, reverb_type="hall")
|
||||
strings.rest(Duration.WHOLE)
|
||||
for sym in ["Am", "F", "C", "G", "Dm", "Am", "E"]:
|
||||
strings.add(Chord.from_symbol(sym), Duration.WHOLE)
|
||||
|
||||
# Cello — deep foundation
|
||||
cello = score.part("cello", instrument="cello",
|
||||
reverb=0.3, reverb_type="hall")
|
||||
cello.rest(Duration.WHOLE)
|
||||
for n in ["A2", "F2", "C3", "G2", "D3", "A2", "E2"]:
|
||||
cello.add(n, Duration.WHOLE)
|
||||
|
||||
# Violin — legato melody enters bar 3
|
||||
violin = score.part("violin", instrument="violin",
|
||||
reverb=0.25, reverb_type="hall", legato=True)
|
||||
violin.rest(Duration.WHOLE)
|
||||
violin.rest(Duration.WHOLE)
|
||||
for note, dur in [
|
||||
("E5", Duration.HALF), ("C5", Duration.HALF),
|
||||
("D5", Duration.QUARTER), ("E5", Duration.QUARTER), ("G5", Duration.HALF),
|
||||
("A5", Duration.HALF), ("G5", Duration.QUARTER), ("E5", Duration.QUARTER),
|
||||
("F5", Duration.WHOLE),
|
||||
("E5", Duration.HALF), ("D5", Duration.HALF),
|
||||
("C5", Duration.HALF), ("B4", Duration.HALF),
|
||||
("A4", Duration.WHOLE),
|
||||
]:
|
||||
violin.add(note, dur)
|
||||
|
||||
# Organ — enters halfway, cathedral weight
|
||||
organ = score.part("organ", instrument="organ",
|
||||
reverb=0.3, reverb_type="cathedral")
|
||||
for _ in range(4):
|
||||
organ.rest(Duration.WHOLE)
|
||||
for sym in ["Dm", "Am", "E", "Am"]:
|
||||
organ.add(Chord.from_symbol(sym), Duration.WHOLE)
|
||||
|
||||
# Harp — arpeggiated flourishes bars 3-4
|
||||
harp = score.part("harp", instrument="harp")
|
||||
harp.rest(Duration.WHOLE)
|
||||
harp.rest(Duration.WHOLE)
|
||||
for n in ["A3", "C4", "E4", "A4", "C5", "E5", "A5", "E5",
|
||||
"G3", "B3", "D4", "G4", "B4", "D5", "G5", "D5"]:
|
||||
harp.add(n, Duration.EIGHTH)
|
||||
for _ in range(4):
|
||||
harp.rest(Duration.WHOLE)
|
||||
|
||||
# Vibraphone — shimmer in last bars with delay
|
||||
vib = score.part("vib", instrument="vibraphone",
|
||||
delay=0.25, delay_time=0.375, delay_feedback=0.35)
|
||||
for _ in range(5):
|
||||
vib.rest(Duration.WHOLE)
|
||||
for note, dur in [
|
||||
("E5", Duration.QUARTER), ("D5", Duration.QUARTER),
|
||||
("C5", Duration.QUARTER), ("A4", Duration.QUARTER),
|
||||
("B4", Duration.HALF), ("E5", Duration.HALF),
|
||||
("A5", Duration.WHOLE),
|
||||
]:
|
||||
vib.add(note, dur)
|
||||
|
||||
# Acid bass — gritty texture bars 4-5
|
||||
acid = score.part("acid", instrument="acid_bass")
|
||||
for _ in range(3):
|
||||
acid.rest(Duration.WHOLE)
|
||||
for n in ["C2", "C2", "E2", "G2", "G2", "G2", "A2", "E2",
|
||||
"D2", "D2", "F2", "A2", "A2", "A2", "E2", "E2"]:
|
||||
acid.add(n, Duration.EIGHTH)
|
||||
for _ in range(2):
|
||||
acid.rest(Duration.WHOLE)
|
||||
|
||||
# Half time drums
|
||||
score.drums("half time", repeats=8)
|
||||
|
||||
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),
|
||||
@@ -1137,6 +1256,8 @@ SONGS = {
|
||||
"18": ("Glass and Silk (Sine+Triangle)", glass_and_silk),
|
||||
"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__":
|
||||
@@ -1150,7 +1271,7 @@ if __name__ == "__main__":
|
||||
print(f" {key:>2}. {name}")
|
||||
|
||||
print()
|
||||
choice = input(" Pick a song (1-20, or 'all'): ").strip()
|
||||
choice = input(" Pick a song (1-22, or 'all'): ").strip()
|
||||
print()
|
||||
|
||||
if choice == "all":
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
[project]
|
||||
name = "pytheory"
|
||||
version = "0.29.0"
|
||||
version = "0.34.0"
|
||||
description = "Music Theory for Humans"
|
||||
readme = "README.md"
|
||||
license = "MIT"
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
"""PyTheory: Music Theory for Humans."""
|
||||
|
||||
__version__ = "0.29.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
|
||||
|
||||
from .rhythm import Duration, TimeSignature, Rest, Score, Part, Section, DrumSound, Pattern
|
||||
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,
|
||||
@@ -21,9 +21,9 @@ 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",
|
||||
"DrumSound", "Pattern", "Section",
|
||||
"DrumSound", "Pattern", "Section", "INSTRUMENTS",
|
||||
]
|
||||
|
||||
@@ -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 = {
|
||||
|
||||
+29
-5
@@ -230,7 +230,7 @@ def cmd_demo(args):
|
||||
{"name": "Bossa Nova", "key": ("A", "minor"), "drums": "bossa nova",
|
||||
"fill": "bossa nova", "bpm": 140,
|
||||
"prog": ("i", "iv", "V", "i"),
|
||||
"lead": ("triangle", "strings", 0.2, -0.1),
|
||||
"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",
|
||||
@@ -254,8 +254,8 @@ def cmd_demo(args):
|
||||
{"name": "Reggae", "key": ("G", "major"), "drums": "reggae",
|
||||
"fill": "reggae", "bpm": 80,
|
||||
"prog": ("I", "IV", "V", "IV"),
|
||||
"lead": ("triangle", "strings", 0.25, 0.15),
|
||||
"pad": ("pwm_slow", "pad", -0.3),
|
||||
"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,
|
||||
@@ -272,9 +272,33 @@ def cmd_demo(args):
|
||||
{"name": "Temple", "key": ("E", "minor"), "drums": "bolero",
|
||||
"fill": "bossa nova", "bpm": 65,
|
||||
"prog": ("i", "iv", "V", "i"),
|
||||
"lead": ("triangle", "pluck", 0.3, 0.2),
|
||||
"pad": ("sine", "pad", 0.0),
|
||||
"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)
|
||||
|
||||
+1752
-61
File diff suppressed because it is too large
Load Diff
+925
-63
File diff suppressed because it is too large
Load Diff
+232
-6
@@ -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",
|
||||
]),
|
||||
}
|
||||
|
||||
+140
-63
@@ -31,6 +31,7 @@ class Tone:
|
||||
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.
|
||||
|
||||
@@ -46,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
|
||||
@@ -68,6 +82,13 @@ class Tone:
|
||||
self.system_name = None
|
||||
self._system = system
|
||||
|
||||
# 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 exists(self) -> bool:
|
||||
"""True if this tone's name is found in the associated system."""
|
||||
@@ -335,17 +356,20 @@ 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)
|
||||
else:
|
||||
return klass(name=tone, octave=octave)
|
||||
return klass(name=tone, octave=octave, _validate=False)
|
||||
|
||||
@classmethod
|
||||
def from_tuple(klass, t: tuple[str, ...]) -> Tone:
|
||||
@@ -381,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
|
||||
@@ -409,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
|
||||
@@ -434,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:
|
||||
@@ -453,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!")
|
||||
|
||||
@@ -467,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)
|
||||
|
||||
@@ -530,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:
|
||||
@@ -555,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)
|
||||
|
||||
@@ -596,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
|
||||
@@ -687,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
|
||||
|
||||
+268
-8
@@ -68,9 +68,16 @@ def test_tone_system():
|
||||
|
||||
def test_tone_exists():
|
||||
c4 = Tone(name="C", octave=4, system="western")
|
||||
invalid_tone = Tone(name="H", octave=4, system="western")
|
||||
assert c4.exists is True
|
||||
assert invalid_tone.exists is False
|
||||
|
||||
|
||||
def test_tone_invalid_raises():
|
||||
"""Invalid tone names raise ValueError at construction time (fixes #39)."""
|
||||
import pytest
|
||||
with pytest.raises(ValueError, match="Unknown tone name"):
|
||||
Tone(name="H", octave=4, system="western")
|
||||
with pytest.raises(ValueError, match="Unknown tone name"):
|
||||
Tone("X")
|
||||
|
||||
|
||||
def test_tone_names_method():
|
||||
@@ -4248,7 +4255,7 @@ def test_parallel_modes_g_major():
|
||||
@needs_portaudio
|
||||
def test_envelope_enum_presets():
|
||||
from pytheory.play import Envelope
|
||||
assert len(Envelope) == 8
|
||||
assert len(Envelope) == 10
|
||||
for e in Envelope:
|
||||
a, d, s, r = e.value
|
||||
assert a >= 0
|
||||
@@ -4839,10 +4846,11 @@ def test_solfege_no_octave():
|
||||
assert t.solfege == "Do"
|
||||
|
||||
|
||||
def test_solfege_unknown_returns_name():
|
||||
"""A non-standard name should be returned unchanged."""
|
||||
t = Tone(name="X", system="western")
|
||||
assert t.solfege == "X"
|
||||
def test_solfege_unknown_raises():
|
||||
"""A non-standard name should raise ValueError at construction (fixes #39)."""
|
||||
import pytest
|
||||
with pytest.raises(ValueError, match="Unknown tone name"):
|
||||
Tone(name="X", system="western")
|
||||
|
||||
|
||||
# ── Rhythm / Duration system ────────────────────────────────────────────────
|
||||
@@ -5312,7 +5320,7 @@ def test_supersaw_wave():
|
||||
@needs_portaudio
|
||||
def test_all_synths_in_enum():
|
||||
from pytheory.play import Synth
|
||||
assert len(Synth) == 10
|
||||
assert len(Synth) == 27
|
||||
for s in Synth:
|
||||
wave = s(440, n_samples=1000)
|
||||
assert len(wave) == 1000
|
||||
@@ -6451,3 +6459,255 @@ def test_from_midi_note_durations(tmp_path):
|
||||
assert len(sounding) == 2
|
||||
assert abs(sounding[0].beats - 4.0) < 0.01
|
||||
assert abs(sounding[1].beats - 2.0) < 0.01
|
||||
|
||||
|
||||
# ── Instrument presets ────────────────────────────────────────────────────────
|
||||
|
||||
def test_instrument_piano():
|
||||
from pytheory import Score, Duration
|
||||
score = Score("4/4", bpm=120)
|
||||
p = score.part("p", instrument="piano")
|
||||
assert p.synth == "piano_synth"
|
||||
assert p.vel_to_filter == 3000
|
||||
|
||||
|
||||
def test_instrument_violin():
|
||||
from pytheory import Score
|
||||
score = Score("4/4", bpm=120)
|
||||
p = score.part("v", instrument="violin")
|
||||
assert p.synth == "strings_synth"
|
||||
assert p.envelope == "bowed"
|
||||
assert p.humanize == 0.15
|
||||
assert p.lowpass == 5000
|
||||
assert p.detune == 2
|
||||
|
||||
|
||||
def test_instrument_override():
|
||||
from pytheory import Score
|
||||
score = Score("4/4", bpm=120)
|
||||
# Explicit synth overrides the preset
|
||||
p = score.part("p", instrument="piano", synth="saw")
|
||||
assert p.synth == "saw"
|
||||
|
||||
|
||||
def test_instrument_unknown_raises():
|
||||
from pytheory import Score
|
||||
score = Score("4/4", bpm=120)
|
||||
with pytest.raises(ValueError, match="Unknown instrument"):
|
||||
score.part("x", instrument="kazoo")
|
||||
|
||||
|
||||
def test_list_instruments():
|
||||
from pytheory import Score, INSTRUMENTS
|
||||
result = Score.list_instruments()
|
||||
assert isinstance(result, list)
|
||||
assert result == sorted(result)
|
||||
assert "piano" in result
|
||||
assert "violin" in result
|
||||
assert "808_bass" in result
|
||||
assert len(result) == len(INSTRUMENTS)
|
||||
|
||||
|
||||
def test_instrument_effects():
|
||||
from pytheory import Score
|
||||
score = Score("4/4", bpm=120)
|
||||
p = score.part("c", instrument="celesta")
|
||||
assert p.reverb_mix == 0.3
|
||||
assert p.reverb_type == "plate"
|
||||
assert p.synth == "fm"
|
||||
assert p.envelope == "mallet"
|
||||
|
||||
|
||||
def test_instrument_808_bass():
|
||||
from pytheory import Score
|
||||
score = Score("4/4", bpm=120)
|
||||
p = score.part("b", instrument="808_bass")
|
||||
assert p.distortion_mix == 0.4
|
||||
assert p.distortion_drive == 2.5
|
||||
assert p.lowpass == 200
|
||||
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
|
||||
|
||||
Reference in New Issue
Block a user