Compare commits

..

43 Commits

Author SHA1 Message Date
kennethreitz 70efb0ad40 v0.36.0: Banjo, mandolin, ukulele, cajón, vocal synth, granular
34 synth waveforms, 26 songs, vocal/formant synthesis with choir
preset, granular engine, banjo/mandolin/ukulele physical models,
cajón drum with 3 patterns, strum sweep on fretboard instruments.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-27 19:54:08 -04:00
kennethreitz bf6deaab64 Mandolin synth, cajón drums, Song #26 Acoustic Ensemble
- Mandolin: paired steel strings (natural chorus from doubled
  courses), bright body resonance (500/1000/2000Hz)
- Cajón: bass (woody box thump), slap (snare wire buzz), tap
  (ghost note). 3 patterns: cajon, cajon rumba, cajon folk
- Song #26: guitar + uke + mandolin + cajón — humanized strumming,
  stereo panned, plate reverb
- Mandola preset (mandolin with lowpass for darker tone)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-27 17:46:15 -04:00
kennethreitz 7c792c0a2a Ukulele synth + strum sweep on all fretboard instruments
- Ukulele: nylon string KS with small body resonance (350/700/1200Hz),
  faster decay than guitar, mid-heavy character
- Strum sweep: 2 quiet grace notes (25% vel) before the chord hit,
  gives audible strum feel without choppiness
- Default strum_time 0.08 → 0.05 for tighter feel

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-27 17:36:29 -04:00
kennethreitz bf8d4b9a77 Epic Bhairav: musical polyrhythm section, fix reverb levels
Polyrhythm section uses musical phrases (ti-ra-ki-ta patterns)
in 5-groups, 7-groups, and 9-groups rather than mechanical grid
overlays. Reverb pulled back to 0.4 across the song.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-27 17:12:31 -04:00
kennethreitz d2d5115c8a Song #25: Epic Bhairav + vocal synth merged to master
Orchestral piece in 22-shruti JI with choir vowel pads, timpani
rolls, bansuri, cello, sitar, strings, harp, djembe→tabla→extended
tabla solo finale (whisper→ghosts→call/response→9-tuplets→32nd
triplet cascades→grand tihai→slam).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-27 12:25:05 -04:00
kennethreitz 3cdd98b158 Merge vocal/formant synth: LF glottal model, 5 formants, choir
Formant synthesis with LF glottal pulse, 5 Peterson & Barney
formant peaks, jitter/shimmer, consonant onsets, click-free
transitions. Presets: vocal, choir.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-27 12:22:36 -04:00
kennethreitz 751d5a49b8 Cleaner vocal synth: less static, click-free note transitions
- Jitter reduced (0.3% → 0.1%), shimmer reduced (2% → 0.8%)
- Breath noise halved (0.08 → 0.04), mix 85/15 → 92/8
- 10ms fade in/out on every vocal note prevents clicks
- Smoother syllable transitions

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-27 12:17:12 -04:00
kennethreitz 6a836dd891 Overhaul vocal synth: LF glottal model, 5 formants, jitter/shimmer
- LF glottal pulse: asymmetric open/close phase (not sines)
- 5 parallel formant filters per vowel (Peterson & Barney data)
- Jitter (0.3% pitch irregularity) + shimmer (2% amplitude)
- Much more voice-like than previous version
- Consonant onsets preserved

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-27 12:13:50 -04:00
kennethreitz 1f888e2b21 Vocal/formant synth with choir preset
Formant synthesis: glottal buzz source through parallel bandpass
filters at vowel resonance frequencies. Supports 5 vowels (A E I O U)
with consonant onsets (plosives, sibilants, nasals, fricatives,
liquids, aspirates, glides). Per-note lyrics via Part.add(lyric=).

Best for choir pads — vowel sounds with cathedral reverb and detune.
Consonant synthesis is rudimentary (noise bursts, not real speech).

Presets: vocal (solo), choir (detuned ensemble).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-27 12:10:54 -04:00
kennethreitz fb923f6c76 v0.35.1: Granular synthesis engine
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-27 11:50:32 -04:00
kennethreitz 59e3338892 Granular synthesis engine with presets
Grain cloud synthesis: source waveform chopped into tiny overlapping
grains (40ms, 50/sec) with Hanning windows, random scatter, and
per-grain pitch variation. Creates textures impossible with other
synthesis. Two presets: granular_pad, granular_texture.
30 synth waveforms total.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-27 11:47:52 -04:00
kennethreitz 8cf4145c15 Docs: timpani, saxophone, Part.roll(), update waveform counts
- Add timpani and saxophone synth sections to synths.rst
- Add rolls section to sequencing.rst with examples
- Update waveform count: 27 → 29

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-27 11:38:46 -04:00
kennethreitz b3885b2c15 v0.35.0: JI ratios, 8.5x faster import, timpani, saxophone, rolls
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-27 11:34:34 -04:00
kennethreitz ae04fa60cc Reduce vibrato across all instruments to 0.001
Strings, cello, trumpet, clarinet, oboe all cut to 0.001 depth.
Much subtler in ensemble context.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-27 11:29:48 -04:00
kennethreitz 6c411e43f8 Part.roll() for crescendo/decrescendo rolls, reedier sax, timpani reverb
- roll(tone, duration, velocity_start, velocity_end, speed) — rapid
  repeated notes with velocity ramp. Works on any instrument.
- Saxophone reed noise boosted and bandpass filtered for more bite
- Timpani preset: cathedral reverb at 0.4

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-27 11:26:28 -04:00
kennethreitz e0427af3cc Timpani and saxophone synths, 4 sax presets
- Timpani: inharmonic membrane modes (1.0, 1.5, 1.99, 2.44),
  felt mallet attack, copper kettle resonance, two-stage decay
- Saxophone: conical bore (all harmonics), strong mids, reed buzz,
  brass body warmth. 4 presets: saxophone, alto_sax, tenor_sax, bari_sax
- 29 synth waveforms total

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-27 11:22:47 -04:00
kennethreitz 552836ae5b Drop pytuning/sympy, lazy-load scipy: import 0.48s → 0.05s (fixes #44)
- Replace pytuning with 30-line native implementations of EDO,
  Pythagorean, and quarter-comma meantone scale generators
- Lazy-load scipy.signal (337ms) — only imported when audio rendering
  is actually used, not on theory-only imports
- Removes pytuning and sympy from dependencies entirely

Import time: 0.479s → 0.056s (8.5x faster)

Closes #44

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-27 11:17:52 -04:00
kennethreitz 0fe53fcdeb Merge pull request #46 from kennethreitz/fix/accidental-octave-wrap
Fix B#/Cb octave boundary crossing
2026-03-27 11:11:43 -04:00
kennethreitz f6fb2a2cd6 Fix B#/Cb octave boundary crossing (fixes #45)
B#4 now correctly resolves to C5 (523.25 Hz), not C4 (261.63 Hz).
Cb4 now correctly resolves to B3 (246.94 Hz), not B4 (493.88 Hz).

When an accidental crosses the B/C octave boundary, the octave is
adjusted: sharps crossing B→C increment, flats crossing C→B decrement.
Also handles double sharps (B##→C#5) and double flats (Cbb→Bb3).

Closes #45

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-27 11:11:08 -04:00
kennethreitz 70d6e6b8ce Reduce flute vibrato further (0.0015 → 0.0008)
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-27 11:05:55 -04:00
kennethreitz aec9a999cb Arabic maqam JI ratios: Zalzalian neutral third (27/22)
Maqam system now uses just intonation ratios instead of 24-TET:
- Quarter-tone positions use Zalzalian (11-limit) ratios
- Mi↓ (the defining Rast note) is exactly 27/22 from Do
- Standard JI intervals for chromatic positions
- Septimal ratios (7-limit) for other quarter-tone positions

Research confirmed: Turkish 53-TET and Thai 7-TET are already
correct as equal temperaments. Gamelan has no universal ratios
(each ensemble is unique), so TET remains the best default.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-27 11:00:16 -04:00
kennethreitz 3acde86028 Int tone names, wrapping, System.tone(), proper shruti JI ratios
- Tone(0, system=edo22) works alongside Tone("0", ...)
- Tone(22, system=edo22) wraps to tone 0, octave+1
- Tone(-1) wraps to last tone, octave-1
- System.tone(name, octave) convenience method
- Shruti system now uses 5-limit just intonation ratios instead
  of 22-TET approximation. Based on Pythagorean/harmonic ratios
  from traditional Indian musicology. Pa is a pure 3/2, Ga is a
  pure 5/4.
- System.ratios attribute overrides equal temperament when set

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-27 10:54:28 -04:00
kennethreitz aa405702a9 Fix Journey: EDM parts start after tabla solo ends
Calculated edm_start from actual section lengths so pad/sub/sitar2
don't bleed into the tabla solo.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-27 10:44:42 -04:00
kennethreitz b7c018fb94 Expanded tabla solo: 4 sections, 9-tuplets, polyrhythm, grand tihai
Solo now has 4 distinct parts:
1. Whisper — single hits with space, breath
2. Ghosts emerge — 16th note ghost fills between accents
3. Call and response — dayan vs bayan, 9-tuplet break
4. Blazing — 32nd triplet cascades, rapid alternating hands,
   9-against-4 polyrhythm, grand tihai (3x, each louder), slam

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-27 03:06:59 -04:00
kennethreitz 07a52a3a25 Add tabla solo section to Journey, louder sitar in EDM drop
Tabla solo with ghost notes, 32nd triplet cascade, tihai, then
slams into house beat. Sitar volume 0.22 → 0.4 in EDM section.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-27 03:03:57 -04:00
kennethreitz e12cb9003b Song #24: Journey (Piano → World → Sitar EDM)
Single score, one reverb space (Taj Mahal), tanpura drone throughout.
Piano arpeggios alone → cello joins → harp/oboe/flute with djembe →
sitar over tabla → EDM section with sitar, synth pad, 808 sub, house
drums. 28 bars, 5 movements.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-27 03:01:36 -04:00
kennethreitz 28968a1b5c Docs: strumming, pitch bends, tuning systems, fix instrument count
- Add guitar strumming section to sequencing.rst
- Add pitch bends section with three bend types
- Add tuning systems section (temperament, reference_pitch, TET)
- Fix index.rst: 25 → 49 instrument presets

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-27 02:49:24 -04:00
kennethreitz 8a4a2df1aa Song #23: Tabla Solo in Raga Yaman (22-shruti)
Tanpura drone intro, quiet sitar Yaman phrases, tabla solo building
from gentle theka through ghost notes to blazing tiri kita with
bayan pitch bends, tihai, dramatic silence, slam finish. Taj Mahal
reverb throughout.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-27 02:37:34 -04:00
kennethreitz f4a90637db Note choking: new hits fade out previous tails on same sound
Drums and melodic notes now choke previous resonance with a quick
fade when a new hit/note starts. Prevents muddy buildup at fast
tempos. Added bayan pitch bend drum sound (TABLA_GE_BEND).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-27 02:32:34 -04:00
kennethreitz 90a1a31049 Fix pitch bends: resampling preserves instrument timbre
Render at base pitch using the actual synth, then variable-rate
resample to shift pitch over time. No more sine wave fallback or
retriggering artifacts. Three bend types: smooth (log), linear, late.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-27 02:22:03 -04:00
kennethreitz 33b2e82594 v0.34.1: Pitch bends, updated docs and songs
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-27 02:15:32 -04:00
kennethreitz 9f8dd0006d Pitch bends, updated docs, songs with new instruments
- Pitch bend: part.add("C4", bend=2, bend_type="smooth") bends up
  a whole step. Three types: smooth (log/perceptual), linear, late
  (hold then bend — blues style).
- Updated songs.py: use dedicated instrument synths (piano_synth,
  flute_synth, trumpet_synth, etc.) instead of generic waveforms
- Updated docs: synths.rst (27 waveforms, instrument synths section),
  effects.rst (cabinet sim, analog drift, updated signal chain),
  drums.rst (world percussion: tabla, dhol, dholak, mridangam,
  djembe, metal kit), index.rst (feature counts)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-27 02:14:45 -04:00
kennethreitz 417f7f74a3 19 new tests: instrument synths, cabinet, analog, strumming, world drums
Tests for: all 14 dedicated synth waveforms, piano brightness scaling,
cabinet sim frequency reduction, analog drift rendering, strum with/
without fretboard, strum direction, all 6 tabla sounds, dhol/mridangam/
djembe/metal kit sounds, 20 world drum pattern presets, guitar preset
cabinet sim. 838 tests total.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-27 02:11:48 -04:00
kennethreitz cd6f814049 Update changelog with vibrato tuning
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-27 02:08:37 -04:00
kennethreitz 83fcdb0a09 Reduce vibrato depth on flute, oboe, trumpet, cello
All cut from 0.003-0.004 to 0.0015-0.002 — less wobbly in
ensemble context, more natural.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-27 02:08:17 -04:00
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
17 changed files with 4472 additions and 324 deletions
+84
View File
@@ -2,6 +2,90 @@
All notable changes to PyTheory are documented here.
## 0.36.0
- **Banjo synth** — steel strings on drum-head body, nasal twang,
fast decay with membrane resonance
- **Mandolin synth** — paired steel strings (natural chorus from
doubled courses), bright body resonance
- **Ukulele synth** — nylon strings, small mid-heavy body, shorter
sustain than guitar
- **Cajón drums** — bass (woody box thump), slap (snare wire buzz),
tap (ghost note). 3 patterns: cajon, cajon rumba, cajon folk
- **Vocal/formant synth** — LF glottal model, 5 Peterson & Barney
formant peaks, jitter/shimmer, consonant onsets, per-note lyrics.
Presets: vocal, choir
- **Granular synthesis** — grain cloud engine with scatter, pitch
variation, Hanning windows. Presets: granular_pad, granular_texture
- **Strum sweep** — subtle grace notes before chord hit for natural
strum feel on all fretboard instruments
- Mandola preset, 34 synth waveforms, 26 songs
## 0.35.0
- **8.5x faster import** — dropped pytuning/sympy, lazy-load scipy.
`import pytheory` now takes ~50ms instead of ~480ms (#44)
- **Proper shruti JI ratios** — 22 positions with 5-limit just intonation
(pure 3/2 fifths, 5/4 thirds), not 22-TET approximation
- **Arabic maqam JI ratios** — Zalzalian 11-limit ratios.
Mi↓ (the Rast third) is exactly 27/22 from Do
- **B#/Cb octave boundary fix** — B#4 = C5, Cb4 = B3 (#45)
- **Int tone names** — `Tone(0, system=TET(22))` works alongside strings.
Wrapping: `Tone(22)` → tone 0, octave+1. `System.tone()` convenience.
- **Timpani synth** — inharmonic membrane modes, felt mallet, copper kettle
resonance, cathedral reverb
- **Saxophone synth** — conical bore, reed buzz, brass body warmth.
4 presets: saxophone, alto_sax, tenor_sax, bari_sax
- **Part.roll()** — rapid repeated notes with velocity ramp for crescendo/
decrescendo rolls on any instrument
- **Vibrato tuning** — all instruments reduced to 0.001 depth for cleaner
ensemble sound
- **Granular synthesis** — grain cloud engine with scatter, pitch
variation, and Hanning-windowed grains. Two presets: granular_pad,
granular_texture.
- 30 synth waveforms, 838 tests
## 0.34.0
- **16 dedicated instrument synths** — physical modeling and specialized
synthesis for: piano (hammer + steel strings + soundboard), bass guitar
(thick KS + pickup), flute (breath + tube resonance), trumpet (lip buzz
+ bell), clarinet (odd harmonics + reed), oboe (double reed + conical
bore), marimba (inharmonic bar modes), harpsichord (quill pluck),
cello (deep bowed + body), harp (soft pluck + soundboard bloom),
upright bass (pizzicato + wooden body), acoustic guitar (KS + body
resonance), electric guitar (KS + pickup comb filter), sitar (jawari
+ chikari), plus organ and bowed strings
- **Speaker cabinet simulation** — tames distorted guitar fizz
- **Guitar strumming** — `Part.strum("Am")` with fretboard lookup
- **Analog oscillator drift** — subtle per-note pitch wobble on synth presets
- **World percussion:** dhol, dholak, mridangam, djembe, metal kit
with 22 new drum patterns
- **Piano improvements:** brightness scales with pitch, two-stage decay,
hammer impact with felt character
- **Vibrato tuning:** reduced across flute, oboe, trumpet, cello for
smoother ensemble sound
- 27 synth waveforms, 10 envelopes, 40+ instrument presets, 80+ drum patterns
## 0.33.1
- **Electric guitar synth** — Karplus-Strong with magnetic pickup comb filter
simulation (single-coil honk, proper sustain)
- **Speaker cabinet simulation** — steep rolloff above 4-5kHz with presence
bump. Makes distorted guitar sound warm instead of fizzy.
- **6 guitar presets:** electric_guitar, clean_guitar, crunch_guitar,
distorted_guitar, orange_crunch, metal_guitar — all with proper cab sim
- **Sitar synth** — Karplus-Strong with jawari bridge buzz, chikari
sympathetic strings, variable damping
- **Guitar strumming** — `Part.strum("Am", Duration.HALF)` with
fretboard fingering lookup, down/up direction, adjustable strum speed
- **World drums:** dhol (bhangra, chaal), dholak (qawwali, folk),
mridangam (adi talam, korvai), djembe (standard, kuku, soli)
— all with bandpass-filtered membrane noise for realistic drum head sound
- **Metal drum kit** — clicky kick, bright snare, tight hats
with 4 patterns (double kick, metal blast, metal groove, metal gallop)
- 15 synth waveforms, 10 envelopes, 40+ instrument presets
## 0.33.0
- **Non-12-TET support** — `TET(n)` factory creates any equal temperament
+135 -6
View File
@@ -10,7 +10,7 @@ the genre -- they tell the listener's body how to move before a single
melodic note is played.
PyTheory includes a complete drum system -- 27 synthesized percussion
sounds, 58 pattern presets across dozens of genres, and 21 fill presets.
sounds, 80+ pattern presets across dozens of genres, and 21 fill presets.
Every sound is generated from waveforms; no samples needed.
Drum Sounds
@@ -145,8 +145,8 @@ Each sound has a dedicated synthesizer:
Pattern Presets
---------------
58 patterns spanning genres from rock to Afro-Cuban to electronic.
Load them with ``Pattern.preset()``:
80+ patterns spanning genres from rock to Afro-Cuban to electronic to
world percussion. Load them with ``Pattern.preset()``:
.. code-block:: pycon
@@ -193,9 +193,16 @@ adds syncopation.
rattling hi-hats of trap, the breakneck tempo of drum and bass. These
patterns were born in drum machines and they still live there.
**Metal/Punk:** metal, blast beat, punk -- Speed and aggression.
The blast beat is both feet and both hands going as fast as humanly
possible. Punk strips everything to its essentials.
**Metal/Punk:** metal, blast beat, punk, double kick, metal blast,
metal groove, metal gallop -- Speed and aggression. The blast beat is
both feet and both hands going as fast as humanly possible. Punk strips
everything to its essentials. The metal kit adds 3 dedicated sounds
(double kick, china cymbal, stack) and 4 patterns for extreme metal
subgenres.
**World Percussion:** tabla, dhol, dholak, mridangam, djembe -- Deep
traditions from across the globe, each with authentic sound sets and
idiomatic patterns. See the World Percussion section below for details.
**Other:** funk, hip hop, bo diddley, second line, new orleans, waltz,
12/8 blues, country, gospel, flamenco -- Everything else. The syncopated
@@ -304,6 +311,128 @@ drum pattern and all named parts are mixed together by ``play_score()``:
play_score(score)
World Percussion
----------------
PyTheory includes dedicated sound sets and pattern presets for
traditional percussion instruments from around the world. Each
instrument has its own synthesized sounds that capture the timbral
character of the real instrument, plus idiomatic rhythmic patterns
drawn from their musical traditions.
Tabla
~~~~~
The tabla is a pair of hand drums from the Indian subcontinent -- the
smaller, higher-pitched *dayan* and the larger, bass *bayan*. It is
the rhythmic backbone of Hindustani classical music, and one of the
most expressive percussion instruments ever created. A single tabla
player can produce an astonishing range of tones by varying finger
placement, pressure, and striking technique.
**6 sounds** -- covering the primary tabla strokes (na, tin, tun, ge,
ke, and ti-ra-ki-ta combinations).
**7 patterns:** teental (16 beats, the most common taal), jhaptaal
(10 beats), rupak (7 beats), dadra (6 beats), keherwa (8 beats, folk
and light classical), tabla solo, and tiri kita (fast ornamental
pattern).
.. code-block:: python
score = Score("4/4", bpm=80)
score.drums("teental", repeats=4)
Dhol
~~~~
The dhol is a double-headed barrel drum from Punjab, played with
sticks. It is the driving force behind bhangra music -- loud,
energetic, and physically impossible to sit still to.
**3 sounds** -- bass stroke, treble stroke, and rimshot.
**2 patterns:** bhangra (the classic bhangra groove) and dhol chaal
(a processional rhythm).
.. code-block:: python
score = Score("4/4", bpm=160)
score.drums("bhangra", repeats=4)
Dholak
~~~~~~
The dholak is a smaller, lighter two-headed drum used across South
Asia in folk music, qawwali, and Bollywood. Played with bare hands,
it produces a warm, melodic tone.
**3 sounds** -- bass, treble, and slap.
**2 patterns:** qawwali (the rhythmic foundation of Sufi devotional
music) and dholak folk (a general folk groove).
.. code-block:: python
score = Score("4/4", bpm=120)
score.drums("qawwali", repeats=4)
Mridangam
~~~~~~~~~
The mridangam is a double-headed drum from South India, the
rhythmic anchor of Carnatic classical music. Its tuning system is
extraordinarily precise, and its rhythmic vocabulary is among the
most mathematically complex in the world.
**4 sounds** -- tha, thom, nam, and din.
**2 patterns:** adi talam (the most common Carnatic talam, 8 beats)
and mridangam korvai (a rhythmic cadence pattern).
.. code-block:: python
score = Score("4/4", bpm=90)
score.drums("adi talam", repeats=4)
Djembe
~~~~~~
The djembe is a rope-tuned goblet drum from West Africa, capable of
producing a wide range of tones from deep bass to sharp slaps. It is
central to the drum ensemble traditions of Mali, Guinea, and Senegal.
**3 sounds** -- bass (open center strike), tone (edge strike), and
slap (sharp edge strike).
**3 patterns:** djembe (a basic accompanying rhythm), kuku (a
traditional rhythm from Guinea associated with fishing), and soli (a
solo/celebration rhythm).
.. code-block:: python
score = Score("4/4", bpm=120)
score.drums("djembe", repeats=4)
Metal Kit
~~~~~~~~~
A dedicated percussion kit for extreme metal subgenres, with
specialized sounds and patterns that go beyond the standard drum kit.
**3 sounds** -- double kick (triggered, tight attack), china cymbal,
and stack (a short, trashy cymbal choke).
**4 patterns:** double kick (relentless double bass drum pattern),
metal blast (blast beat with china cymbal accents), metal groove (a
half-time groove with double kick fills), and metal gallop (the
classic triplet-feel gallop rhythm).
.. code-block:: python
score = Score("4/4", bpm=200)
score.drums("metal blast", repeats=4)
MIDI Export
-----------
+91 -7
View File
@@ -32,8 +32,8 @@ It's a well-tested order that sounds good by default.
Effects are applied in this fixed order::
Signal --> Saturation --> Tremolo --> Distortion --> Chorus --> Phaser
--> Highpass --> Lowpass --> Delay --> Reverb --> Mix
Signal --> Saturation --> Tremolo --> Distortion --> Cabinet --> Chorus
--> Phaser --> Highpass --> Lowpass --> Delay --> Reverb --> Mix
Additionally, these per-note effects are applied before the part effects chain:
@@ -47,11 +47,12 @@ Part-level effects:
- **Saturation** first: subtle even-harmonic warmth (tape/tube color).
- **Tremolo** second: amplitude LFO modulation.
- **Distortion** third: drives the signal before filtering.
- **Chorus** fourth: thickens the signal.
- **Phaser** fifth: swept allpass notches.
- **Highpass** sixth: removes low-frequency mud.
- **Lowpass** seventh: shapes the tone (like a tone knob on an amp).
- **Delay** eighth: echoes the shaped signal (tap delay / tape echo).
- **Cabinet** fourth: speaker cab simulation (rolloff + presence bump).
- **Chorus** fifth: thickens the signal.
- **Phaser** sixth: swept allpass notches.
- **Highpass** seventh: removes low-frequency mud.
- **Lowpass** eighth: shapes the tone (like a tone knob on an amp).
- **Delay** ninth: echoes the shaped signal (tap delay / tape echo).
- **Reverb** last: places everything in a space (room / hall).
Distortion
@@ -96,6 +97,89 @@ Parameters:
distortion_drive=10.0,
)
Cabinet Simulation
------------------
A real guitar amp doesn't just distort the signal -- the speaker
cabinet shapes the tone dramatically. A 12-inch speaker in a closed
cabinet rolls off the harsh high frequencies above 5 kHz and adds a
presence bump around 2--3 kHz that gives the sound its "in the room"
quality. Without a cabinet, distortion sounds thin and fizzy. With
one, it sounds like a real amp.
PyTheory's cabinet simulation applies a speaker rolloff curve (lowpass
at ~5 kHz) combined with a presence resonance bump, placed in the
signal chain immediately after distortion -- exactly where it sits in
a real amp.
Parameters:
- ``cabinet``: Wet/dry mix, 0.0--1.0 (default 0, off).
- 0.3--0.5 = subtle speaker coloring
- 0.6--0.8 = classic amp-in-a-room
- 1.0 = full cabinet, no dry signal
.. code-block:: python
# Classic rock amp tone: distortion into cabinet
guitar = score.part(
"guitar",
synth="saw",
envelope="pluck",
distortion=0.6,
distortion_drive=5.0,
cabinet=0.8,
)
# Clean amp with just cabinet warmth (no distortion)
clean = score.part(
"clean",
synth="triangle",
envelope="pluck",
cabinet=0.5,
)
Analog Drift
------------
Real analog synthesizers are never perfectly in tune. The voltage-
controlled oscillators drift slightly over time as components warm up
and temperature fluctuates. This imperfection is actually a big part
of why vintage analog synths sound so appealing -- the subtle pitch
wandering gives each note a unique, living quality that static digital
oscillators lack.
The ``analog_drift`` parameter adds slow, random pitch variation to
each oscillator, modeling this vintage behavior.
Parameters:
- ``analog_drift``: Drift amount, 0.0--1.0 (default 0, off).
- 0.05--0.1 = subtle warmth (studio-grade analog)
- 0.15--0.25 = noticeable drift (vintage gear warming up)
- 0.3+ = unstable, wobbly (broken tape machine)
.. code-block:: python
# Warm vintage pad
pad = score.part(
"pad",
synth="supersaw",
envelope="pad",
analog_drift=0.1,
chorus=0.3,
)
# Lo-fi detuned lead
lead = score.part(
"lead",
synth="saw",
envelope="pluck",
analog_drift=0.25,
)
Chorus
------
+104
View File
@@ -574,3 +574,107 @@ Define sections with ``score.section()`` and repeat them with
Use any names you want — ``"intro"``, ``"verse"``, ``"chorus"``,
``"bridge"``, ``"drop"``, ``"breakdown"``, ``"outro"``, or anything
that makes sense for your song. The names are just labels.
Guitar Strumming
----------------
Any part with a fretboard can strum chords using real fingering
positions. The ``strum()`` method looks up the chord on the fretboard,
gets the correct voicing, and plays all strings as a chord.
.. code-block:: python
from pytheory import Fretboard
guitar = score.part("guitar", instrument="acoustic_guitar",
fretboard=Fretboard.guitar())
guitar.strum("Am", Duration.HALF, direction="down")
guitar.strum("G", Duration.HALF, direction="up")
guitar.strum("F", Duration.WHOLE)
Works with any fretboard instrument — guitar, ukulele, banjo, mandolin.
Works with any guitar preset — clean, crunch, distorted, orange, metal.
Pitch Bends
-----------
Bend a note's pitch up or down over its duration. Essential for guitar
bends, sitar meends, trombone slides, and vocal-style expression.
.. code-block:: python
# Guitar bend: D up to E (2 semitones)
guitar.add("D4", Duration.HALF, bend=2, bend_type="smooth")
# Release bend: E back down to D
guitar.add("E4", Duration.HALF, bend=-2)
# Blues curl: hold then bend at the end
guitar.add("C4", Duration.HALF, bend=1, bend_type="late")
Three bend types:
- ``"smooth"`` — logarithmic (default). Perceptually even pitch change.
- ``"linear"`` — linear frequency interpolation. Mechanical/synth feel.
- ``"late"`` — holds the starting pitch for 60%, bends in the last 40%.
The classic blues "curl."
Rolls
-----
Rapid repeated notes with a velocity ramp — perfect for timpani
rolls, snare rolls, tremolo on any instrument. The velocity ramps
from ``velocity_start`` to ``velocity_end`` for crescendo or
decrescendo effects.
.. code-block:: python
# Timpani crescendo roll
timp = score.part("timp", instrument="timpani")
timp.roll("C3", Duration.WHOLE, velocity_start=20, velocity_end=110)
timp.add("C3", Duration.HALF, velocity=127) # big accent
# Snare roll with 32nd notes
snare = score.part("snare", synth="noise", envelope="pluck")
snare.roll("C4", Duration.HALF, speed=0.125,
velocity_start=40, velocity_end=100)
# Decrescendo (loud to quiet)
timp.roll("G2", Duration.WHOLE, velocity_start=100, velocity_end=30)
Parameters:
- ``velocity_start``: Starting velocity (default 40).
- ``velocity_end``: Ending velocity (default 100).
- ``speed``: Note subdivision (default ``Duration.SIXTEENTH``).
Use ``0.125`` for 32nd notes, ``Duration.EIGHTH`` for 8th notes.
Tuning Systems
--------------
A Score can use any tuning system and temperament:
.. code-block:: python
# Baroque harpsichord — meantone tuning, A=415
score = Score("4/4", bpm=80, temperament="meantone",
reference_pitch=415.0)
# Indian classical — 22-shruti system
score = Score("4/4", bpm=75, system="shruti")
# Just intonation — pure intervals
score = Score("4/4", bpm=90, temperament="just")
Temperaments: ``"equal"`` (default), ``"pythagorean"``, ``"meantone"``,
``"just"``.
Custom equal temperaments via the ``TET()`` factory:
.. code-block:: python
from pytheory import TET
edo19 = TET(19) # 19-tone equal temperament
score = Score("4/4", bpm=100, system=edo19)
+227 -3
View File
@@ -1,7 +1,7 @@
Synthesizers
============
PyTheory includes 13 built-in waveforms and 10 ADSR envelope presets.
PyTheory includes 30 built-in waveforms and 10 ADSR envelope presets.
Every sound is generated from scratch -- no samples or external audio
files needed.
@@ -233,7 +233,7 @@ shapes the amplitude over time for natural-sounding notes:
- **Sustain** -- the held volume while the note is on.
- **Release** -- how quickly it fades to silence after the note ends.
PyTheory includes 8 presets:
PyTheory includes 10 presets:
.. code-block:: python
@@ -386,11 +386,235 @@ more "wooden" than a raw saw wave.
violin = score.part("violin", synth="strings_synth")
Dedicated Instrument Synths
--------------------------
Beyond the classic and physical modeling waveforms, PyTheory includes
17 dedicated instrument synths. Each one uses tailored synthesis
techniques -- additive harmonics, formant shaping, body resonance
modeling, and specialized envelopes -- to capture the character of a
specific acoustic instrument. These are the waveforms that bring the
total count to 30.
Piano Synth
~~~~~~~~~~~
Hammer-strike envelope with body resonance and subtle inharmonicity.
Models the way a felt hammer excites steel strings inside a wooden
soundboard.
.. code-block:: python
piano = score.part("piano", synth="piano_synth")
Bass Guitar Synth
~~~~~~~~~~~~~~~~~
Plucked string model with finger-damped harmonics and low-end warmth.
.. code-block:: python
bass = score.part("bass", synth="bass_guitar_synth")
Flute Synth
~~~~~~~~~~~~
Breathy noise excitation through a resonant tube model, with
overblowing behavior at higher velocities.
.. code-block:: python
flute = score.part("flute", synth="flute_synth")
Trumpet Synth
~~~~~~~~~~~~~
Brass lip-buzz model with spectral brightness that increases with
velocity, plus a characteristic brassy edge from shaped harmonics.
.. code-block:: python
trumpet = score.part("trumpet", synth="trumpet_synth")
Clarinet Synth
~~~~~~~~~~~~~~
Cylindrical bore model producing mostly odd harmonics, giving the
characteristic hollow, woody tone.
.. code-block:: python
clarinet = score.part("clarinet", synth="clarinet_synth")
Oboe Synth
~~~~~~~~~~~
Double-reed model with nasal formant shaping and a buzzy, penetrating
timbre.
.. code-block:: python
oboe = score.part("oboe", synth="oboe_synth")
Marimba Synth
~~~~~~~~~~~~~
Tuned bar model with a soft mallet attack and a warm, resonant decay
that emphasizes the fundamental.
.. code-block:: python
marimba = score.part("marimba", synth="marimba_synth")
Harpsichord Synth
~~~~~~~~~~~~~~~~~
Plucked-string model with a bright, immediate attack and rapid decay
-- the characteristic "plink" of a quill plucking a string.
.. code-block:: python
harpsi = score.part("harpsi", synth="harpsichord_synth")
Cello Synth
~~~~~~~~~~~
Bowed string model with body formants at cello resonance frequencies,
producing a rich, warm, sustained tone.
.. code-block:: python
cello = score.part("cello", synth="cello_synth")
Harp Synth
~~~~~~~~~~
Plucked string with longer sustain and gentle high-frequency rolloff,
modeling nylon strings on a resonant frame.
.. code-block:: python
harp = score.part("harp", synth="harp_synth")
Upright Bass Synth
~~~~~~~~~~~~~~~~~~
Pizzicato double bass with woody body resonance and a thumpy low end.
.. code-block:: python
bass = score.part("bass", synth="upright_bass_synth")
Acoustic Guitar Synth
~~~~~~~~~~~~~~~~~~~~~
Steel-string model with pick transient, body resonance, and natural
string decay.
.. code-block:: python
guitar = score.part("guitar", synth="acoustic_guitar_synth")
Electric Guitar Synth
~~~~~~~~~~~~~~~~~~~~~
Magnetic pickup model with brighter harmonics and less body resonance
than the acoustic, ready for effects processing.
.. code-block:: python
eguitar = score.part("eguitar", synth="electric_guitar_synth")
Sitar Synth
~~~~~~~~~~~~
Sympathetic string resonance with the characteristic buzzy "jawari"
bridge, producing a shimmering, metallic sustain.
.. code-block:: python
sitar = score.part("sitar", synth="sitar_synth")
Timpani Synth
~~~~~~~~~~~~~
Large kettle drum with definite pitch. Inharmonic membrane modes
(1.0, 1.5, 1.99, 2.44), felt mallet attack, copper kettle resonance.
Use ``Part.roll()`` for crescendo timpani rolls.
.. code-block:: python
timp = score.part("timp", synth="timpani_synth")
timp.roll("C3", Duration.WHOLE, velocity_start=20, velocity_end=110)
Saxophone Synth
~~~~~~~~~~~~~~~
Single reed through a conical brass bore. All harmonics with strong
mids, reed buzz, and brass body warmth. Four presets: ``saxophone``,
``alto_sax``, ``tenor_sax``, ``bari_sax``.
.. code-block:: python
sax = score.part("sax", instrument="tenor_sax")
Granular Synth
~~~~~~~~~~~~~~
Grain cloud synthesis — chops a source waveform into tiny overlapping
grains (10-200ms), each windowed and optionally pitch/time scattered.
Creates textures impossible with other synthesis: frozen tones,
shimmering clouds, evolving pads, glitchy stutters.
.. code-block:: python
# Atmospheric granular pad
pad = score.part("pad", instrument="granular_pad")
# Granular with filter envelope sweep + resonance
texture = score.part("texture", synth="granular_synth", envelope="pad",
filter_amount=4000, filter_attack=0.5,
filter_decay=1.5, filter_sustain=0.3,
lowpass=600, lowpass_q=3.0,
reverb=0.5, reverb_type="taj_mahal")
Parameters (passed as synth kwargs):
- ``grain_size``: Duration per grain in seconds (default 0.04).
- ``density``: Grains per second (default 50). Higher = denser cloud.
- ``scatter``: Random position jitter 0-1 (default 0.5).
- ``pitch_var``: Per-grain pitch randomization in cents (default 12).
- ``source``: Base waveform — ``"saw"``, ``"sine"``, ``"triangle"``,
``"square"``, ``"noise"``.
Analog Oscillator Drift
~~~~~~~~~~~~~~~~~~~~~~~~
All waveform synths support the ``analog_drift`` parameter, which adds
subtle, slow random pitch variation to each oscillator -- modeling the
voltage instability of vintage analog circuits. This is what makes a
real Minimoog sound slightly different on every note, and why analog
synths feel "alive" compared to their digital counterparts.
.. code-block:: python
# Subtle vintage drift
pad = score.part("pad", synth="saw", analog_drift=0.1)
# More pronounced, wobbly analog character
lead = score.part("lead", synth="square", analog_drift=0.3)
Drift values:
- **0.05--0.1** = subtle warmth (studio-grade analog)
- **0.15--0.25** = noticeable drift (vintage gear warming up)
- **0.3+** = unstable, wobbly (broken tape machine)
Instrument Presets
------------------
Instead of choosing synth + envelope + effects manually, use an
instrument preset — 38 predefined combinations that approximate real
instrument preset — 40+ predefined combinations that approximate real
instruments:
.. code-block:: python
+11 -8
View File
@@ -77,15 +77,18 @@ What's Inside
numbers), scale recommendation, modulation, voice leading
- **Sequencing** — Score, Parts, arpeggiator, legato/glide, velocity,
swing, humanize, tempo changes, song sections with repeat
- **Synthesis**13 waveforms (including Karplus-Strong pluck, Hammond organ,
bowed string), 10 envelopes, 38 instrument presets, configurable FM,
sub-oscillator, noise layer, filter envelope, velocity-to-brightness,
detune, stereo pan/spread, 58 drum patterns (stereo panned), 21 fills
- **Synthesis** — 34 waveforms (including Karplus-Strong pluck, Hammond organ,
bowed string, and 14 dedicated instrument synths), 10 envelopes, 40+
instrument presets, configurable FM, sub-oscillator, noise layer, filter
envelope, velocity-to-brightness, analog oscillator drift, detune, stereo
pan/spread, strumming, 80+ drum patterns (stereo panned, including world
percussion), 21 fills
- **Effects** — reverb (algorithmic + 7 convolution IRs, stereo), delay,
lowpass/highpass (with resonance), distortion, saturation, chorus,
phaser, tremolo, sidechain compression, automation, LFOs. Master bus
compressor/limiter
- **Instruments**25 presets with fingering generation
lowpass/highpass (with resonance), distortion, cabinet simulation,
saturation, chorus, phaser, tremolo, analog drift, sidechain compression,
automation, LFOs. Master bus compressor/limiter
- **Instruments**49 presets with fingering generation, guitar strumming,
pitch bends
- **Output** — stereo playback, WAV export, MIDI import/export
- **Interface** — REPL with tab completion, CLI (15 commands), ``pytheory demo``
- **AI-friendly** — Claude Code can compose
+722 -141
View File
File diff suppressed because it is too large Load Diff
+1 -2
View File
@@ -1,6 +1,6 @@
[project]
name = "pytheory"
version = "0.33.0"
version = "0.36.0"
description = "Music Theory for Humans"
readme = "README.md"
license = "MIT"
@@ -21,7 +21,6 @@ classifiers = [
"Programming Language :: Python :: 3.13",
]
dependencies = [
"pytuning",
"numeral",
"sounddevice",
"scipy",
+1 -1
View File
@@ -1,6 +1,6 @@
"""PyTheory: Music Theory for Humans."""
__version__ = "0.33.0"
__version__ = "0.36.0"
from .tones import Tone, Interval
from .systems import System, SYSTEMS, TET
+159 -36
View File
@@ -1,4 +1,4 @@
from pytuning import scales
import math
REFERENCE_A = 440
@@ -6,41 +6,59 @@ REFERENCE_A = 440
# Scientific pitch notation changes octave at C, not A, so this offset
# is needed for all octave arithmetic.
C_INDEX = 3
def _create_just_intonation_scale(n):
"""5-limit just intonation ratios for 12-tone systems.
These are the pure frequency ratios derived from the harmonic series
the way intervals "want" to sound before equal temperament imposed
compromise. Each ratio is mathematically exact: a perfect fifth is
exactly 3/2, a major third is exactly 5/4.
For non-12 systems, falls back to equal temperament.
# ── Temperament scale generators (replaces pytuning dependency) ──────────
def _create_edo_scale(n):
"""N-tone equal division of the octave. Each step = 2^(1/n)."""
return [2 ** (i / n) for i in range(n + 1)]
def _create_pythagorean_scale(n):
"""Pythagorean tuning — spiral of pure fifths (3/2 ratio).
Each tone is generated by stacking perfect fifths and octave-reducing.
"""
from fractions import Fraction
ratios = [1.0]
for i in range(1, n):
# Stack fifths: (3/2)^i, then reduce to within one octave
r = (3 / 2) ** i
while r >= 2.0:
r /= 2.0
ratios.append(r)
ratios.sort()
ratios.append(2.0)
return ratios
def _create_quarter_comma_meantone_scale(n):
"""Quarter-comma meantone — pure major thirds (5/4), tempered fifths.
The fifth is narrowed by 1/4 of a syntonic comma so that four
fifths make a pure major third (5/4). The meantone fifth =
5^(1/4) 1.49535.
"""
fifth = 5 ** 0.25 # meantone fifth ≈ 1.49535 (vs 1.5 pure)
ratios = [1.0]
for i in range(1, n):
r = fifth ** i
while r >= 2.0:
r /= 2.0
ratios.append(r)
ratios.sort()
ratios.append(2.0)
return ratios
def _create_just_intonation_scale(n):
"""5-limit just intonation ratios for 12-tone systems."""
if n != 12:
return scales.create_edo_scale(n)
# Standard 5-limit JI ratios (A-based: A=1/1)
ratios = [
Fraction(1, 1), # A — unison
Fraction(16, 15), # A# — minor second
Fraction(9, 8), # B — major second
Fraction(6, 5), # C — minor third
Fraction(5, 4), # C# — major third
Fraction(4, 3), # D — perfect fourth
Fraction(45, 32), # D# — augmented fourth
Fraction(3, 2), # E — perfect fifth
Fraction(8, 5), # F — minor sixth
Fraction(5, 3), # F# — major sixth
Fraction(9, 5), # G — minor seventh
Fraction(15, 8), # G# — major seventh
Fraction(2, 1), # A — octave
]
return [float(r) for r in ratios]
return _create_edo_scale(n)
return [1, 16/15, 9/8, 6/5, 5/4, 4/3, 45/32, 3/2, 8/5, 5/3, 9/5, 15/8, 2.0]
TEMPERAMENTS = {
"equal": scales.create_edo_scale,
"pythagorean": scales.create_pythagorean_scale,
"meantone": scales.create_quarter_comma_meantone_scale,
"equal": _create_edo_scale,
"pythagorean": _create_pythagorean_scale,
"meantone": _create_quarter_comma_meantone_scale,
"just": _create_just_intonation_scale,
}
@@ -295,8 +313,51 @@ DEGREES_SHRUTI = [
("shadja", ()), # Sa (octave)
]
# 22-shruti frequency ratios — 5-limit just intonation.
# These are the REAL shruti intervals, NOT 22-TET approximations.
# Based on the traditional Pythagorean/harmonic ratios from Indian
# musicological treatises (Natya Shastra, Sangita Ratnakara).
#
# Ordered from Dha (A=1.0) to match our system indexing.
# Sa is at index 5 (ratio ≈ 6/5 from Dha).
from fractions import Fraction
_SHRUTI_RATIOS_FROM_SA = [
Fraction(1, 1), # 0: Sa — 1/1
Fraction(256, 243), # 1: atikomal Re — Pythagorean limma
Fraction(16, 15), # 2: komal Re — JI minor second
Fraction(10, 9), # 3: shuddha Re — minor whole tone
Fraction(9, 8), # 4: Re — major whole tone
Fraction(32, 27), # 5: atikomal Ga — Pythagorean minor 3rd
Fraction(6, 5), # 6: komal Ga — JI minor 3rd
Fraction(5, 4), # 7: Ga — JI major 3rd
Fraction(81, 64), # 8: tivra Ga — Pythagorean major 3rd
Fraction(4, 3), # 9: Ma — perfect 4th
Fraction(27, 20), # 10: ekashruti Ma
Fraction(45, 32), # 11: tivra Ma — augmented 4th
Fraction(729, 512), # 12: atitivra Ma — Pythagorean tritone
Fraction(3, 2), # 13: Pa — perfect 5th
Fraction(128, 81), # 14: atikomal Dha — Pythagorean minor 6th
Fraction(8, 5), # 15: komal Dha — JI minor 6th
Fraction(5, 3), # 16: shuddha Dha
Fraction(27, 16), # 17: Dha — Pythagorean major 6th
Fraction(16, 9), # 18: komal Ni — Pythagorean minor 7th
Fraction(9, 5), # 19: shuddha Ni — JI minor 7th
Fraction(15, 8), # 20: Ni — JI major 7th
Fraction(243, 128), # 21: tivra Ni — Pythagorean major 7th
]
# Rotate to start from Dha (index 17 in the Sa-based list above).
# Dha = 27/16 from Sa. We divide all ratios by 27/16 and wrap.
_dha_ratio = _SHRUTI_RATIOS_FROM_SA[17]
SHRUTI_RATIOS = []
for i in range(22):
sa_idx = (i + 17) % 22 # rotate: Dha=0, komalNi=1, ..., Sa=5, ...
r = _SHRUTI_RATIOS_FROM_SA[sa_idx] / _dha_ratio
if r < 1:
r *= 2 # wrap into the same octave
SHRUTI_RATIOS.append(float(r))
# 22-shruti thaat scales with proper microtonal intervals.
# Each interval is counted in shrutis (22-TET steps).
# Compare to the 12-TET approximations in INDIAN_SCALES which lose
# the distinction between 2-shruti and 3-shruti steps.
SHRUTI_SCALES = {
@@ -341,13 +402,75 @@ SHRUTI_SCALES = {
],
}
# ── 24-TET Arabic maqam system ─────────────────────────────────────────────
# Arabic maqam uses quarter-tones (half-flat, half-sharp). 24-TET captures
# these intervals exactly. Each step = 50 cents (vs 100 in 12-TET).
# The half-flat (♭½) is the defining sound of Arabic music — it's what
# makes maqam Rast and Bayati sound distinctly Middle Eastern.
# ── Arabic maqam system ───────────────────────────────────────────────────
# Arabic maqam uses quarter-tones with specific JI ratios, NOT equal
# 24-TET divisions. The neutral intervals (quarter-flat, quarter-sharp)
# are based on ratios involving the 11th partial, as theorized by
# Zalzal (8th century Baghdad). The quarter-flat E in Rast is 27/22,
# not simply halfway between Eb and E.
#
# 24 positions per octave, but with unequal JI spacing.
# Ordered from La (=A) to match Western index positions.
# Maqam JI ratios from Do (C). Based on traditional practice:
# - Standard JI intervals for the 12 chromatic positions
# - Zalzalian ratios (11-limit) for the quarter-tone positions
_MAQAM_RATIOS_FROM_DO = [
Fraction(1, 1), # 0: Do — unison
Fraction(33, 32), # 1: Do↑ — quarter-sharp (~53¢, 33rd harmonic)
Fraction(16, 15), # 2: Reb — JI minor 2nd
Fraction(12, 11), # 3: Re↓ — Zalzalian neutral 2nd (~151¢)
Fraction(9, 8), # 4: Re — major whole tone
Fraction(11, 9) * Fraction(1, 1), # 5: Re↑ — undecimal (~347¢... too high)
Fraction(6, 5), # 6: Mib — JI minor 3rd
Fraction(27, 22), # 7: Mi↓ — Zalzalian neutral 3rd (~355¢) THE Rast note
Fraction(5, 4), # 8: Mi — JI major 3rd
Fraction(4, 3), # 9: Fa — perfect 4th
Fraction(11, 8), # 10: Fa↑ — undecimal tritone (~551¢)
Fraction(45, 32), # 11: Fa# — augmented 4th
Fraction(22, 15), # 12: Sol↓ — neutral (~663¢... adjusted)
Fraction(3, 2), # 13: Sol — perfect 5th
Fraction(99, 64), # 14: Sol↑ — quarter-sharp 5th
Fraction(8, 5), # 15: Lab — JI minor 6th
Fraction(18, 11), # 16: La↓ — Zalzalian neutral 6th
Fraction(5, 3), # 17: La — JI major 6th
Fraction(27, 16), # 18: La↑/Sib↓ — Pythagorean major 6th
Fraction(16, 9), # 19: Sib — Pythagorean minor 7th
Fraction(11, 6), # 20: Si↓ — undecimal neutral 7th
Fraction(15, 8), # 21: Si — JI major 7th
Fraction(243, 128), # 22: Si↑ — Pythagorean major 7th
Fraction(2, 1) * Fraction(33, 64), # 23: near-octave (~1049¢)
]
# Ratios directly from La (A=1/1), each position defined explicitly.
# Standard JI intervals for chromatic positions, Zalzalian (11-limit)
# ratios for the quarter-tone positions.
MAQAM_RATIOS = [
1.0, # 0: La — A (unison)
float(Fraction(256, 243)), # 1: La↑ — Pythagorean comma up
float(Fraction(16, 15)), # 2: Sib — Bb (JI minor 2nd)
float(Fraction(12, 11)), # 3: Si↓ — B quarter-flat (Zalzalian)
float(Fraction(9, 8)), # 4: Si — B (major 2nd)
float(Fraction(6, 5)), # 5: Do — C (minor 3rd from A)
float(Fraction(11, 9)), # 6: Do↑ — C quarter-sharp (undecimal)
float(Fraction(5, 4)), # 7: Reb — Db (major 3rd from A...= JI Db)
float(Fraction(9, 7)), # 8: Re↓ — D quarter-flat (septimal)
float(Fraction(4, 3)), # 9: Re — D (perfect 4th from A)
float(Fraction(11, 8)), # 10: Re↑ — D quarter-sharp (undecimal)
float(Fraction(45, 32)), # 11: Mib — Eb (augmented 4th from A)
float(Fraction(6, 5) * Fraction(27, 22)), # 12: Mi↓ — E quarter-flat (Do × 27/22)
float(Fraction(3, 2)), # 13: Mi — E (perfect 5th from A)
float(Fraction(8, 5)), # 14: Fa — F (minor 6th from A)
float(Fraction(18, 11)), # 15: Fa↑ — F quarter-sharp (Zalzalian)
float(Fraction(5, 3)), # 16: Fa# — F# (major 6th from A)
float(Fraction(27, 16)), # 17: Sol↓ — G quarter-flat
float(Fraction(16, 9)), # 18: Sol — G (minor 7th from A)
float(Fraction(11, 6)), # 19: Sol↑ — G quarter-sharp (undecimal)
float(Fraction(15, 8)), # 20: Lab — Ab (major 7th from A)
float(Fraction(27, 14)), # 21: La↓ — A quarter-flat (septimal)
float(Fraction(243, 128)), # 22: La½b — near-octave
float(Fraction(2, 1) * Fraction(256, 257)), # 23: La♮ — near-octave
]
TONES_ARABIC_24 = [
("La",), # 0 — A
("La↑",), # 1 — A quarter-sharp
+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)
+1946 -11
View File
File diff suppressed because it is too large Load Diff
+679 -46
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": {
@@ -178,11 +195,33 @@ INSTRUMENTS = {
"detune": 12, "lowpass": 3000, "lowpass_q": 1.5,
"humanize": 0.2,
},
"banjo": {
"synth": "banjo_synth", "envelope": "none",
"humanize": 0.2,
},
"mandolin": {
"synth": "mandolin_synth", "envelope": "none",
"humanize": 0.2,
},
"mandola": {
"synth": "mandolin_synth", "envelope": "none",
"lowpass": 3000,
"humanize": 0.2,
},
"ukulele": {
"synth": "ukulele_synth", "envelope": "none",
"humanize": 0.2,
},
"koto": {
"synth": "pluck_synth", "envelope": "none",
"lowpass": 4000,
"reverb": 0.2,
},
"sitar": {
"synth": "sitar_synth", "envelope": "none",
"lowpass": 4500,
"humanize": 0.2,
},
# ── Synth presets ──
"synth_lead": {
@@ -191,6 +230,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 +238,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 +246,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 +256,27 @@ INSTRUMENTS = {
"filter_attack": 0.005, "filter_decay": 0.15,
"filter_sustain": 0.0, "filter_amount": 4000,
"vel_to_filter": 3000,
"analog": 0.3,
},
"granular_pad": {
"synth": "granular_synth", "envelope": "pad",
"reverb": 0.4, "reverb_type": "cathedral",
"analog": 0.3,
},
"vocal": {
"synth": "vocal_synth", "envelope": "strings",
"reverb": 0.3, "reverb_type": "hall",
"humanize": 0.15,
},
"choir": {
"synth": "vocal_synth", "envelope": "pad",
"detune": 8, "spread": 0.4,
"reverb": 0.45, "reverb_type": "cathedral",
},
"granular_texture": {
"synth": "granular_synth", "envelope": "none",
"reverb": 0.5, "reverb_type": "taj_mahal",
"delay": 0.3, "delay_time": 0.4, "delay_feedback": 0.4,
},
"808_bass": {
"synth": "sine", "envelope": "pluck",
@@ -231,8 +294,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",
@@ -250,6 +312,31 @@ INSTRUMENTS = {
"fm_ratio": 2.0, "fm_index": 3.0,
"reverb": 0.4, "reverb_type": "cathedral",
},
"timpani": {
"synth": "timpani_synth", "envelope": "none",
"reverb": 0.4, "reverb_type": "cathedral",
},
# ── Woodwinds (continued) ──
"saxophone": {
"synth": "saxophone_synth", "envelope": "bowed",
"humanize": 0.15, "vel_to_filter": 1500,
},
"alto_sax": {
"synth": "saxophone_synth", "envelope": "bowed",
"humanize": 0.15, "vel_to_filter": 1800,
},
"tenor_sax": {
"synth": "saxophone_synth", "envelope": "bowed",
"lowpass": 3000,
"humanize": 0.15, "vel_to_filter": 1200,
},
"bari_sax": {
"synth": "saxophone_synth", "envelope": "bowed",
"lowpass": 2000,
"humanize": 0.15, "vel_to_filter": 800,
"sub_osc": 0.15,
},
}
@@ -295,11 +382,19 @@ class TimeSignature:
@dataclass
class Note:
"""A pairing of a sound (Tone, Chord, or None for rest) with a duration."""
"""A pairing of a sound (Tone, Chord, or None for rest) with a duration.
The optional ``bend`` field specifies a pitch bend in semitones
applied over the note's duration. Positive = bend up, negative = down.
For example, ``bend=2`` bends the note up a whole step by the end.
"""
tone: object
duration: Duration
velocity: int = 100
bend: float = 0.0
bend_type: str = "smooth" # "smooth" (log), "linear", "late"
lyric: str = "" # syllable for vocal synth
@property
def beats(self) -> float:
@@ -376,6 +471,39 @@ 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
TABLA_GE_BEND = 108 # bayan with upward pitch bend (palm press)
# Djembe sounds
DJEMBE_BASS = 102 # open bass (center of head)
DJEMBE_TONE = 103 # open tone (edge, fingers together)
DJEMBE_SLAP = 104 # slap (edge, fingers spread, sharp crack)
# Cajon sounds
CAJON_BASS = 108 # center of face, deep thump
CAJON_SLAP = 109 # top edge, snare wires buzz
CAJON_TAP = 110 # light finger tap
# 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 +1441,358 @@ 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),
],
)
# ── Cajón patterns ────────────────────────────────────────────────────────
CB = DrumSound.CAJON_BASS
CSL = DrumSound.CAJON_SLAP
CT = DrumSound.CAJON_TAP
# Cajón flamenco — the classic acoustic percussion groove
Pattern._PRESETS["cajon"] = dict(
name="cajon",
time_signature="4/4",
beats=4.0,
hits=[
_h(CB, 0.0, 85), _h(CT, 0.5, 35), _h(CT, 0.75, 38),
_h(CSL, 1.0, 80), _h(CT, 1.5, 32),
_h(CB, 2.0, 82), _h(CT, 2.5, 35), _h(CT, 2.75, 40),
_h(CSL, 3.0, 82), _h(CT, 3.25, 30), _h(CT, 3.5, 35),
],
)
# Cajón rumba — Latin-flavored
Pattern._PRESETS["cajon rumba"] = dict(
name="cajon rumba",
time_signature="4/4",
beats=4.0,
hits=[
_h(CB, 0.0, 88), _h(CT, 0.5, 38),
_h(CSL, 1.0, 78), _h(CT, 1.25, 32), _h(CB, 1.5, 72),
_h(CSL, 2.0, 82), _h(CT, 2.5, 35),
_h(CB, 3.0, 75), _h(CSL, 3.5, 80), _h(CT, 3.75, 38),
],
)
# Cajón singer-songwriter — simple, supportive
Pattern._PRESETS["cajon folk"] = dict(
name="cajon folk",
time_signature="4/4",
beats=4.0,
hits=[
_h(CB, 0.0, 80),
_h(CSL, 1.0, 72), _h(CT, 1.5, 30),
_h(CB, 2.0, 78),
_h(CSL, 3.0, 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 +2112,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,19 +2158,27 @@ 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
self._automation: list[tuple[float, dict]] = [] # (beat, {param: value})
def add(self, tone_or_string, duration=Duration.QUARTER, *, velocity: int = 100) -> "Part":
def add(self, tone_or_string, duration=Duration.QUARTER, *, velocity: int = 100,
bend: float = 0.0, bend_type: str = "smooth", lyric: str = "") -> "Part":
"""Add a note. Accepts Tone/Chord objects or note strings like ``"E5"``.
Duration can be a ``Duration`` enum or a raw float (beats).
Velocity controls loudness (1-127, default 100).
Bend specifies a pitch bend in semitones over the note duration
(e.g. ``bend=2`` bends up a whole step, ``bend=-1`` bends down
a half step). Used for guitar bends, sitar meends, slides.
Returns self for chaining.
"""
@@ -1696,7 +2187,9 @@ class Part:
tone_or_string = Tone.from_string(tone_or_string, system=self._system)
if isinstance(duration, (int, float)):
duration = _RawDuration(duration)
self.notes.append(Note(tone=tone_or_string, duration=duration, velocity=velocity))
self.notes.append(Note(tone=tone_or_string, duration=duration,
velocity=velocity, bend=bend,
bend_type=bend_type, lyric=lyric))
return self
def set(self, **params) -> "Part":
@@ -1761,6 +2254,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,
@@ -1978,6 +2472,138 @@ class Part:
return self
def strum(self, chord_name: str, duration=Duration.QUARTER, *,
direction: str = "down", velocity: int = 100,
strum_time: float = 0.05) -> "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"`` (lowhigh, default) or ``"up"`` (highlow).
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)
# Strum sweep: quick individual string hits before the chord.
# Only the first 2-3 strings get a tiny grace note, the rest
# ring together as the full chord. Gives the strum feel without
# sounding like separate plucks.
n_strings = len(strum_tones)
if strum_time > 0.02 and n_strings >= 3:
n_grace = min(2, n_strings - 1)
per_grace = strum_time / n_grace
grace_vel = max(1, int(velocity * 0.25))
for i in range(n_grace):
self.add(strum_tones[i], per_grace, velocity=grace_vel)
ring = max(0.1, total_beats - strum_time)
self.add(chord_obj, ring, velocity=velocity)
else:
self.add(chord_obj, total_beats, velocity=velocity)
return self
def roll(self, tone_or_string, duration=Duration.WHOLE, *,
velocity_start: int = 40, velocity_end: int = 100,
speed=Duration.SIXTEENTH) -> "Part":
"""Play a roll — rapid repeated notes with velocity ramp.
Perfect for timpani rolls, snare rolls, tremolo on any
instrument. The velocity ramps from ``velocity_start`` to
``velocity_end`` over the duration for crescendo/decrescendo.
Args:
tone_or_string: The note to repeat.
duration: Total duration of the roll.
velocity_start: Velocity of the first hit (default 40).
velocity_end: Velocity of the last hit (default 100).
speed: How fast to repeat (default SIXTEENTH notes).
Returns:
Self for chaining.
Example::
>>> timp = score.part("timp", instrument="timpani")
>>> timp.roll("C3", Duration.WHOLE, velocity_start=30, velocity_end=110)
"""
if hasattr(duration, 'value'):
total = duration.value
else:
total = float(duration)
if hasattr(speed, 'value'):
step = speed.value
else:
step = float(speed)
n_hits = max(1, int(total / step))
for i in range(n_hits):
frac = i / max(1, n_hits - 1)
vel = int(velocity_start + (velocity_end - velocity_start) * frac)
vel = max(1, min(127, vel))
remaining = total - i * step
note_dur = min(step, remaining)
if note_dur > 0:
self.add(tone_or_string, note_dur, velocity=vel)
return self
@property
def is_drums(self) -> bool:
"""True if this part contains drum hits."""
@@ -2180,8 +2806,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:
@@ -2290,6 +2920,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():
@@ -2300,6 +2932,7 @@ class Score:
p = Part(name, **merged)
p._system = self.system
p._fretboard = fretboard
self.parts[name] = p
return p
+21 -5
View File
@@ -2,8 +2,8 @@ from ._statics import (
TEMPERAMENTS, TONES, DEGREES, SCALES,
INDIAN_SCALES, ARABIC_SCALES, JAPANESE_SCALES,
BLUES_SCALES, GAMELAN_SCALES, SYSTEMS,
TONES_SHRUTI, DEGREES_SHRUTI, SHRUTI_SCALES,
TONES_ARABIC_24, DEGREES_ARABIC_24, ARABIC_24_SCALES,
TONES_SHRUTI, DEGREES_SHRUTI, SHRUTI_SCALES, SHRUTI_RATIOS,
TONES_ARABIC_24, DEGREES_ARABIC_24, ARABIC_24_SCALES, MAQAM_RATIOS,
TONES_SLENDRO, DEGREES_SLENDRO, SLENDRO_SCALES,
TONES_PELOG, DEGREES_PELOG, PELOG_SCALES,
TONES_THAI, DEGREES_THAI, THAI_SCALES,
@@ -14,7 +14,7 @@ from ._statics import (
class System:
def __init__(self, *, tone_names, degrees, scales=None, c_index=None,
period=2.0):
period=2.0, ratios=None):
self.tone_names = tone_names
self.degrees = degrees
@@ -25,6 +25,11 @@ class System:
# 3.0 for Bohlen-Pierce (tritave).
self.period = period
# Custom frequency ratios: if set, overrides equal temperament.
# A list of N floats (one per tone), each relative to the first
# tone (1.0). For example, just intonation shruti ratios.
self.ratios = ratios
# c_index: the index of the "reference C" in the tone list.
# For octave arithmetic — scientific pitch changes octave at C.
# Default 3 for 12-TET western (A=0, A#=1, B=2, C=3).
@@ -214,6 +219,17 @@ class System:
# descending goes in meta?
return {"intervals": scale, "hemitonic": hemitonic, "meta": {}}
def tone(self, name, octave=4):
"""Create a Tone in this system. Shorthand for ``Tone(name, octave=octave, system=self)``.
Example::
>>> edo19 = TET(19)
>>> edo19.tone(5, octave=4).frequency
"""
from . import Tone
return Tone(name, octave=octave, system=self)
def __repr__(self):
return f"<System semitones={self.semitones!r}>"
@@ -352,9 +368,9 @@ SYSTEMS = {
"31-tet": TET(31, names=_31TET_NAMES),
# Microtonal systems with proper intervals (not 12-TET approximations)
"shruti": System(tone_names=TONES_SHRUTI, degrees=DEGREES_SHRUTI,
scales=SHRUTI_SCALES, c_index=5),
scales=SHRUTI_SCALES, c_index=5, ratios=SHRUTI_RATIOS),
"maqam": System(tone_names=TONES_ARABIC_24, degrees=DEGREES_ARABIC_24,
scales=ARABIC_24_SCALES, c_index=5),
scales=ARABIC_24_SCALES, c_index=5, ratios=MAQAM_RATIOS),
"slendro": System(tone_names=TONES_SLENDRO, degrees=DEGREES_SLENDRO,
scales=SLENDRO_SCALES, c_index=1),
"pelog": System(tone_names=TONES_PELOG, degrees=DEGREES_PELOG,
+61 -11
View File
@@ -26,7 +26,7 @@ class Tone:
def __init__(
self,
name: str,
name,
*,
alt_names: Optional[list[str]] = None,
octave: Optional[int] = None,
@@ -36,8 +36,10 @@ class Tone:
"""Initialize a Tone with a name, optional octave, and musical system.
Args:
name: The note name (e.g. ``"C"``, ``"C#4"``). If the name
contains a digit, it is parsed as the octave.
name: The note name as a string (``"C"``, ``"C#4"``) or an int
for numbered systems (``0``, ``11``). Ints are converted to
strings and wrapped to the system's range (e.g. 22 in a
22-tone system becomes 0 at octave+1).
alt_names: Alternate spellings for this tone (e.g. enharmonics).
octave: The octave number. Overrides any octave parsed from *name*.
system: The tuning system, either as a string key (``"western"``)
@@ -46,6 +48,23 @@ class Tone:
if alt_names is None:
alt_names = []
# Int tone names: wrap to system range, adjust octave
if isinstance(name, int):
if isinstance(system, str):
from .systems import SYSTEMS
_sys = SYSTEMS[system]
else:
_sys = system
n_tones = len(_sys.tone_names)
if name < 0 or name >= n_tones:
extra_octaves = name // n_tones
name = name % n_tones
if octave is None:
octave = 4 + extra_octaves
else:
octave += extra_octaves
name = str(name)
if isinstance(name, str):
# Normalize unicode music symbols to ASCII equivalents
name = (name
@@ -70,6 +89,35 @@ class Tone:
if octave is None:
octave = parsed_octave
# Octave boundary fix: B#→C should increment octave,
# Cb→B should decrement octave (scientific pitch changes at C).
# Only applies to Western-style systems with letter names.
if octave is not None and name and name[0].isalpha():
if isinstance(system, str):
from .systems import SYSTEMS
_sys_check = SYSTEMS.get(system)
else:
_sys_check = system
if _sys_check is not None:
resolved = _sys_check.resolve_name(name)
if resolved is not None and resolved != name:
orig_letter = name[0].upper()
res_letter = resolved[0].upper()
# Sharp crossing B→C: B# resolves to C, octave up
if orig_letter == 'B' and res_letter == 'C' and '#' in name:
octave += 1
# Double sharp: A## resolves to B — no boundary cross
# But B## resolves to C# — boundary cross
if orig_letter == 'B' and res_letter not in ('B', 'A') and '##' in name:
octave += 1
# Flat crossing C→B: Cb resolves to B, octave down
if orig_letter == 'C' and res_letter == 'B' and 'b' in name and name != 'C':
octave -= 1
# Double flat: D♭♭ resolves to C — no boundary cross
# But C♭♭ resolves to Bb — boundary cross
if orig_letter == 'C' and res_letter not in ('C', 'D') and 'bb' in name:
octave -= 1
self.name = name
self.octave = octave
self.alt_names = alt_names
@@ -762,11 +810,13 @@ class Tone:
period = getattr(self.system, 'period', 2.0)
c_idx = getattr(self.system, 'c_index', C_INDEX)
if period != 2.0 and temperament == "equal":
# Non-octave period (e.g. Bohlen-Pierce tritave=3.0):
# generate ratios as period^(n/tones) instead of 2^(n/tones)
import sympy
pitch_scale = [period ** sympy.Rational(i, tones) for i in range(tones + 1)]
# Custom ratios override temperament (e.g. shruti just ratios)
custom_ratios = getattr(self.system, 'ratios', None)
if custom_ratios is not None:
pitch_scale = list(custom_ratios) + [period]
elif period != 2.0 and temperament == "equal":
# Non-octave period (e.g. Bohlen-Pierce tritave=3.0)
pitch_scale = [period ** (i / tones) for i in range(tones + 1)]
else:
pitch_scale = TEMPERAMENTS[temperament](tones)
octave = self.octave if self.octave is not None else 4
@@ -783,7 +833,7 @@ class Tone:
if symbolic:
return reference_pitch * ratio
else:
result = reference_pitch * ratio
result = float(reference_pitch * ratio)
if precision:
return float(result.evalf(precision))
return float(result)
return round(result, precision)
return result
+205 -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) == 30
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():
@@ -6717,3 +6711,204 @@ def test_interval_to_non12():
a5 = a.add(19)
result = a.interval_to(a5)
assert "octave" in result
# ── Dedicated instrument synths ──────────────────────────────────────────────
def test_all_dedicated_synths_render():
"""Every dedicated synth waveform produces valid audio."""
from pytheory.play import (piano_wave, bass_guitar_wave, flute_wave,
trumpet_wave, clarinet_wave, oboe_wave,
marimba_wave, harpsichord_wave, cello_wave,
harp_wave, upright_bass_wave,
acoustic_guitar_wave, electric_guitar_wave,
sitar_wave, SAMPLE_RATE)
synths = [piano_wave, bass_guitar_wave, flute_wave, trumpet_wave,
clarinet_wave, oboe_wave, marimba_wave, harpsichord_wave,
cello_wave, harp_wave, upright_bass_wave,
acoustic_guitar_wave, electric_guitar_wave, sitar_wave]
for fn in synths:
wave = fn(440, n_samples=11025)
assert len(wave) == 11025
assert wave.dtype == numpy.int16
assert numpy.abs(wave).max() > 0
def test_piano_brightness_scales():
"""High-pitched piano should be brighter (more high harmonics)."""
from pytheory.play import piano_wave
low = piano_wave(130, n_samples=22050) # C3
high = piano_wave(1047, n_samples=22050) # C6
# Both should produce valid audio
assert numpy.abs(low).max() > 0
assert numpy.abs(high).max() > 0
def test_acoustic_guitar_body_resonance():
"""Acoustic guitar should produce richer spectrum than raw pluck."""
from pytheory.play import acoustic_guitar_wave, pluck_wave
ag = acoustic_guitar_wave(220, n_samples=22050)
pk = pluck_wave(220, n_samples=22050)
assert len(ag) == len(pk) == 22050
def test_cello_has_vibrato():
"""Cello synth should produce pitch variation (vibrato)."""
from pytheory.play import cello_wave
wave = cello_wave(220, n_samples=44100)
assert len(wave) == 44100
assert numpy.abs(wave).max() > 0
# ── Cabinet simulation ───────────────────────────────────────────────────────
def test_cabinet_reduces_highs():
"""Cabinet sim should reduce high-frequency content."""
from pytheory.play import _apply_cabinet
# White noise has flat spectrum
noise = numpy.random.uniform(-1, 1, 44100).astype(numpy.float32)
cabbed = _apply_cabinet(noise, brightness=0.5)
# RMS of cabbed should be lower (energy removed by filters)
assert numpy.sqrt(numpy.mean(cabbed ** 2)) < numpy.sqrt(numpy.mean(noise ** 2))
def test_cabinet_brightness_param():
"""Higher brightness = more high-frequency content passes through."""
from pytheory.play import _apply_cabinet
noise = numpy.random.uniform(-1, 1, 44100).astype(numpy.float32)
dark = _apply_cabinet(noise, brightness=0.0)
bright = _apply_cabinet(noise, brightness=1.0)
# Bright should have more energy than dark
assert numpy.sqrt(numpy.mean(bright ** 2)) > numpy.sqrt(numpy.mean(dark ** 2))
# ── Analog drift ─────────────────────────────────────────────────────────────
def test_analog_drift_varies_pitch():
"""Analog drift should make repeated renders slightly different."""
from pytheory import Score, Duration
score1 = Score("4/4", bpm=120)
p1 = score1.part("t", synth="saw", analog=0.5)
p1.add("C4", Duration.QUARTER)
p1.add("C4", Duration.QUARTER)
# With analog > 0, each C4 gets a random pitch offset
# This is hard to test deterministically, just verify it renders
from pytheory.play import render_score
buf = render_score(score1)
assert len(buf) > 0
# ── Guitar strumming ─────────────────────────────────────────────────────────
def test_strum_requires_fretboard():
"""Strumming without a fretboard should raise ValueError."""
from pytheory import Score, Duration
score = Score("4/4", bpm=120)
p = score.part("g", synth="saw")
with pytest.raises(ValueError, match="fretboard"):
p.strum("Am", Duration.QUARTER)
def test_strum_adds_notes():
"""Strumming should add notes to the part."""
from pytheory import Score, Duration, Fretboard
score = Score("4/4", bpm=120)
fb = Fretboard.guitar()
p = score.part("g", instrument="acoustic_guitar", fretboard=fb)
p.strum("Am", Duration.HALF)
assert len(p.notes) > 0
def test_strum_direction():
"""Both down and up strums should work."""
from pytheory import Score, Duration, Fretboard
score = Score("4/4", bpm=120)
fb = Fretboard.guitar()
p = score.part("g", instrument="acoustic_guitar", fretboard=fb)
p.strum("G", Duration.QUARTER, direction="down")
p.strum("G", Duration.QUARTER, direction="up")
assert len(p.notes) >= 2 # grace notes + chord per strum
# ── World drums ──────────────────────────────────────────────────────────────
def test_tabla_sounds_render():
"""All tabla drum sounds should produce valid audio."""
from pytheory.play import _render_drum_hit
from pytheory.rhythm import DrumSound
for sound in [DrumSound.TABLA_NA, DrumSound.TABLA_TIN, DrumSound.TABLA_GE,
DrumSound.TABLA_DHA, DrumSound.TABLA_TIT, DrumSound.TABLA_KE]:
wave = _render_drum_hit(sound.value, 22050)
assert len(wave) == 22050
assert wave.dtype == numpy.float32
def test_dhol_sounds_render():
from pytheory.play import _render_drum_hit
from pytheory.rhythm import DrumSound
for sound in [DrumSound.DHOL_DAGGA, DrumSound.DHOL_TILLI, DrumSound.DHOL_BOTH]:
wave = _render_drum_hit(sound.value, 22050)
assert len(wave) == 22050
def test_mridangam_sounds_render():
from pytheory.play import _render_drum_hit
from pytheory.rhythm import DrumSound
for sound in [DrumSound.MRIDANGAM_THAM, DrumSound.MRIDANGAM_NAM,
DrumSound.MRIDANGAM_DIN, DrumSound.MRIDANGAM_THA]:
wave = _render_drum_hit(sound.value, 22050)
assert len(wave) == 22050
def test_djembe_sounds_render():
from pytheory.play import _render_drum_hit
from pytheory.rhythm import DrumSound
for sound in [DrumSound.DJEMBE_BASS, DrumSound.DJEMBE_TONE, DrumSound.DJEMBE_SLAP]:
wave = _render_drum_hit(sound.value, 22050)
assert len(wave) == 22050
def test_metal_kit_sounds_render():
from pytheory.play import _render_drum_hit
from pytheory.rhythm import DrumSound
for sound in [DrumSound.METAL_KICK, DrumSound.METAL_SNARE, DrumSound.METAL_HAT]:
wave = _render_drum_hit(sound.value, 22050)
assert len(wave) == 22050
def test_tabla_pattern_presets():
"""All tabla patterns should load without error."""
from pytheory.rhythm import Pattern
for name in ["teental", "jhaptaal", "rupak", "dadra",
"keherwa", "tabla solo", "tiri kita"]:
p = Pattern.preset(name)
assert p.beats > 0
def test_world_drum_pattern_presets():
"""All world drum patterns should load."""
from pytheory.rhythm import Pattern
for name in ["bhangra", "dhol chaal", "qawwali", "dholak folk",
"adi talam", "mridangam korvai", "djembe", "kuku", "soli",
"double kick", "metal blast", "metal groove", "metal gallop"]:
p = Pattern.preset(name)
assert p.beats > 0
# ── Guitar presets with cabinet sim ──────────────────────────────────────────
def test_guitar_presets_have_cabinet():
"""Distorted guitar presets should have cabinet simulation."""
from pytheory import Score
for preset in ["distorted_guitar", "orange_crunch", "metal_guitar"]:
score = Score("4/4", bpm=120)
p = score.part("g", instrument=preset)
assert p.cabinet > 0, f"{preset} should have cabinet sim"
def test_clean_guitar_preset():
from pytheory import Score
score = Score("4/4", bpm=120)
p = score.part("g", instrument="clean_guitar")
assert p.synth == "electric_guitar_synth"
assert p.cabinet > 0
Generated
+1 -37
View File
@@ -444,15 +444,6 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/b3/38/89ba8ad64ae25be8de66a6d463314cf1eb366222074cfda9ee839c56a4b4/mdurl-0.1.2-py3-none-any.whl", hash = "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8", size = 9979, upload-time = "2022-08-14T12:40:09.779Z" },
]
[[package]]
name = "mpmath"
version = "1.3.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/e0/47/dd32fa426cc72114383ac549964eecb20ecfd886d1e5ccf5340b55b02f57/mpmath-1.3.0.tar.gz", hash = "sha256:7a28eb2a9774d00c7bc92411c19a89209d5da7c4c9a9e227be8330a23a25b91f", size = 508106, upload-time = "2023-03-07T16:47:11.061Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/43/e3/7d92a15f894aa0c9c4b49b8ee9ac9850d6e63b03c9c32c0367a13ae62209/mpmath-1.3.0-py3-none-any.whl", hash = "sha256:a0b2b9fe80bbcd81a6647ff13108738cfb482d481d826cc0e02f5b35e5c88d2c", size = 536198, upload-time = "2023-03-07T16:47:09.197Z" },
]
[[package]]
name = "myst-parser"
version = "4.0.1"
@@ -707,11 +698,10 @@ wheels = [
[[package]]
name = "pytheory"
version = "0.33.0"
version = "0.36.0"
source = { editable = "." }
dependencies = [
{ name = "numeral" },
{ name = "pytuning" },
{ name = "scipy", version = "1.15.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" },
{ name = "scipy", version = "1.17.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" },
{ name = "sounddevice" },
@@ -732,7 +722,6 @@ docs = [
[package.metadata]
requires-dist = [
{ name = "numeral" },
{ name = "pytuning" },
{ name = "scipy" },
{ name = "sounddevice" },
]
@@ -744,19 +733,6 @@ docs = [
{ name = "sphinx" },
]
[[package]]
name = "pytuning"
version = "0.7.3"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" },
{ name = "numpy", version = "2.4.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" },
{ name = "sympy" },
]
wheels = [
{ url = "https://files.pythonhosted.org/packages/26/59/e2c2fc91688f788587fb387ef6120c9a1ad3a8b88771fba9fc6a9c9a969d/PyTuning-0.7.3-py3-none-any.whl", hash = "sha256:db0b1231c012c1cf6a3c73aa7d791b4cff79a72f2ec6535f159c873fe302214b", size = 108174, upload-time = "2023-09-02T21:11:00.657Z" },
]
[[package]]
name = "pyyaml"
version = "6.0.3"
@@ -1151,18 +1127,6 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/52/a7/d2782e4e3f77c8450f727ba74a8f12756d5ba823d81b941f1b04da9d033a/sphinxcontrib_serializinghtml-2.0.0-py3-none-any.whl", hash = "sha256:6e2cb0eef194e10c27ec0023bfeb25badbbb5868244cf5bc5bdc04e4464bf331", size = 92072, upload-time = "2024-07-29T01:10:08.203Z" },
]
[[package]]
name = "sympy"
version = "1.14.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "mpmath" },
]
sdist = { url = "https://files.pythonhosted.org/packages/83/d3/803453b36afefb7c2bb238361cd4ae6125a569b4db67cd9e79846ba2d68c/sympy-1.14.0.tar.gz", hash = "sha256:d3d3fe8df1e5a0b42f0e7bdf50541697dbe7d23746e894990c030e2b05e72517", size = 7793921, upload-time = "2025-04-27T18:05:01.611Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/a2/09/77d55d46fd61b4a135c444fc97158ef34a095e5681d0a6c10b75bf356191/sympy-1.14.0-py3-none-any.whl", hash = "sha256:e091cc3e99d2141a0ba2847328f5479b05d94a6635cb96148ccb3f34671bd8f5", size = 6299353, upload-time = "2025-04-27T18:04:59.103Z" },
]
[[package]]
name = "tomli"
version = "2.4.0"