mirror of
https://github.com/kennethreitz/pytheory.git
synced 2026-06-05 23:00:20 +00:00
Compare commits
12 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| f6fb2a2cd6 | |||
| 70d6e6b8ce | |||
| aec9a999cb | |||
| 3acde86028 | |||
| aa405702a9 | |||
| b7c018fb94 | |||
| 07a52a3a25 | |||
| e12cb9003b | |||
| 28968a1b5c | |||
| 8a4a2df1aa | |||
| f4a90637db | |||
| 90a1a31049 |
@@ -574,3 +574,77 @@ 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."
|
||||
|
||||
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)
|
||||
|
||||
+2
-1
@@ -87,7 +87,8 @@ What's Inside
|
||||
lowpass/highpass (with resonance), distortion, cabinet simulation,
|
||||
saturation, chorus, phaser, tremolo, analog drift, sidechain compression,
|
||||
automation, LFOs. Master bus compressor/limiter
|
||||
- **Instruments** — 25 presets with fingering generation
|
||||
- **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
|
||||
|
||||
+356
-2
@@ -15,7 +15,8 @@ Usage:
|
||||
|
||||
import sounddevice as sd
|
||||
|
||||
from pytheory import Chord, Key, Pattern, Duration, Score
|
||||
from pytheory import Chord, Key, Pattern, Duration, Score, Tone, TonedScale, SYSTEMS
|
||||
from pytheory.rhythm import DrumSound, _Hit
|
||||
from pytheory.play import render_score, SAMPLE_RATE
|
||||
|
||||
|
||||
@@ -1199,6 +1200,357 @@ def greensleeves():
|
||||
play_song(score, "Greensleeves — Renaissance Lute (Meantone, A=415)")
|
||||
|
||||
|
||||
def tabla_solo_yaman():
|
||||
"""Tabla solo with tanpura drone, sitar, and Raga Yaman — 22-shruti tuning."""
|
||||
shruti = SYSTEMS["shruti"]
|
||||
score = Score("4/4", bpm=160, system=shruti)
|
||||
h = _Hit
|
||||
|
||||
NA = DrumSound.TABLA_NA
|
||||
TI = DrumSound.TABLA_TIN
|
||||
GE = DrumSound.TABLA_GE
|
||||
DH = DrumSound.TABLA_DHA
|
||||
TT = DrumSound.TABLA_TIT
|
||||
KE = DrumSound.TABLA_KE
|
||||
GB = DrumSound.TABLA_GE_BEND
|
||||
|
||||
# Tanpura drone — Sa + Pa
|
||||
tanpura_sa = score.part("tanpura_sa", synth="strings_synth", envelope="pad",
|
||||
detune=3, lowpass=1000, volume=0.18,
|
||||
reverb=0.5, reverb_type="taj_mahal")
|
||||
tanpura_pa = score.part("tanpura_pa", synth="strings_synth", envelope="pad",
|
||||
detune=3, lowpass=1400, volume=0.14,
|
||||
reverb=0.5, reverb_type="taj_mahal")
|
||||
sa3 = Tone("Sa", octave=3, system=shruti)
|
||||
pa3 = Tone("Pa", octave=3, system=shruti)
|
||||
for _ in range(16):
|
||||
tanpura_sa.add(sa3, Duration.WHOLE)
|
||||
tanpura_pa.add(pa3, Duration.WHOLE)
|
||||
|
||||
# Quiet sitar — Raga Yaman (Kalyan thaat)
|
||||
sitar = score.part("sitar", instrument="sitar", volume=0.12,
|
||||
reverb=0.4, reverb_type="taj_mahal")
|
||||
ts = TonedScale(system=shruti, tonic=Tone("Sa", octave=4, system=shruti))
|
||||
y = list(ts["kalyan"].tones)
|
||||
S, R, G, M, P, D, N, S2 = y
|
||||
sitar.rest(Duration.WHOLE)
|
||||
sitar.rest(Duration.WHOLE)
|
||||
for tone, dur, vel in [
|
||||
(S, 3.0, 55), (R, 1.0, 50), (G, 3.0, 58), (R, 1.0, 48),
|
||||
(S, 4.0, 55), (G, 1.0, 50), (M, 1.0, 52), (P, 3.0, 58),
|
||||
(M, 1.0, 48), (G, 1.0, 50), (R, 1.0, 48), (S, 4.0, 55),
|
||||
(P, 2.0, 52), (D, 1.0, 55), (N, 2.0, 58), (S2, 3.0, 60),
|
||||
(N, 1.0, 52), (D, 1.0, 50), (P, 1.0, 52), (G, 1.0, 48),
|
||||
(R, 1.0, 48), (S, 4.0, 55),
|
||||
]:
|
||||
sitar.add(tone, dur, velocity=vel)
|
||||
|
||||
# 4 bars drone intro (silence for drums)
|
||||
silence = Pattern(name="silence", time_signature="4/4", beats=16.0, hits=[])
|
||||
score.add_pattern(silence, repeats=1)
|
||||
|
||||
# Gentle opening
|
||||
p1 = Pattern(name="gentle", time_signature="4/4", beats=8.0, hits=[
|
||||
h(DH, 0.0, 80), h(NA, 2.0, 60),
|
||||
h(DH, 4.0, 85), h(NA, 5.0, 55), h(NA, 6.0, 60), h(DH, 7.0, 80),
|
||||
])
|
||||
|
||||
# Building with ghost notes
|
||||
p2 = Pattern(name="build", time_signature="4/4", beats=16.0, hits=[
|
||||
h(DH, 0.0, 95), h(TT, 0.5, 35), h(NA, 1.0, 70), h(TT, 1.5, 30),
|
||||
h(NA, 2.0, 65), h(DH, 3.0, 90),
|
||||
h(DH, 4.0, 100), h(TT, 4.25, 35), h(TT, 4.5, 40), h(NA, 5.0, 75),
|
||||
h(TT, 5.5, 35), h(NA, 6.0, 70), h(TT, 6.5, 30), h(DH, 7.0, 95),
|
||||
h(DH, 8.0, 95), h(TI, 9.0, 70), h(TI, 10.0, 72), h(NA, 11.0, 80),
|
||||
h(TT, 11.25, 40), h(TT, 11.5, 42), h(KE, 11.75, 45),
|
||||
h(TT, 12.0, 50), h(TT, 12.25, 55), h(KE, 12.5, 58), h(NA, 12.75, 70),
|
||||
h(DH, 13.0, 100), h(TT, 13.25, 40), h(TT, 13.5, 45), h(KE, 13.75, 50),
|
||||
h(NA, 14.0, 75), h(KE, 14.25, 50), h(DH, 14.5, 85), h(NA, 14.75, 70),
|
||||
h(DH, 15.0, 110), h(GB, 15.5, 100),
|
||||
])
|
||||
|
||||
# Full intensity
|
||||
p3 = Pattern(name="fire", time_signature="4/4", beats=16.0, hits=[
|
||||
h(TT, 0.0, 50), h(TT, 0.125, 35), h(TT, 0.25, 45), h(KE, 0.5, 55),
|
||||
h(NA, 0.75, 85),
|
||||
h(DH, 1.0, 115), h(TT, 1.25, 38), h(DH, 1.5, 70), h(NA, 1.75, 60),
|
||||
h(TT, 2.0, 50), h(TT, 2.125, 35), h(TT, 2.25, 48), h(KE, 2.5, 55),
|
||||
h(NA, 2.75, 88),
|
||||
h(DH, 3.0, 115), h(GB, 3.5, 105), h(NA, 3.75, 72),
|
||||
h(NA, 4.0, 115), h(NA, 4.25, 60), h(TT, 4.5, 40), h(NA, 4.75, 105),
|
||||
h(GE, 5.0, 105), h(GE, 5.25, 55), h(GB, 5.5, 95), h(GE, 5.75, 50),
|
||||
h(NA, 6.0, 115), h(TT, 6.125, 30), h(TT, 6.25, 38), h(NA, 6.5, 100),
|
||||
h(TT, 6.625, 32), h(TT, 6.75, 42),
|
||||
h(GB, 7.0, 115), h(KE, 7.25, 52), h(GE, 7.5, 72), h(KE, 7.75, 48),
|
||||
# Tihai
|
||||
h(DH, 8.0, 115), h(NA, 8.25, 78), h(TT, 8.5, 52), h(KE, 8.75, 58),
|
||||
h(DH, 9.0, 105),
|
||||
h(DH, 9.5, 110), h(NA, 9.75, 78), h(TT, 10.0, 52), h(KE, 10.25, 58),
|
||||
h(DH, 10.5, 105),
|
||||
h(DH, 11.0, 120), h(NA, 11.25, 82), h(TT, 11.5, 58), h(KE, 11.75, 62),
|
||||
h(DH, 12.0, 120),
|
||||
# Silence... then finish
|
||||
h(GB, 14.5, 120),
|
||||
h(DH, 15.5, 127), h(DH, 15.75, 127),
|
||||
])
|
||||
|
||||
score.add_pattern(p1, repeats=1)
|
||||
score.add_pattern(p2, repeats=1)
|
||||
score.add_pattern(p3, repeats=1)
|
||||
score.set_drum_effects(reverb=0.4, reverb_type="taj_mahal")
|
||||
|
||||
play_song(score, "Tabla Solo — Raga Yaman (22-Shruti, Taj Mahal)")
|
||||
|
||||
|
||||
def journey():
|
||||
"""Journey — piano → orchestra → world → sitar EDM.
|
||||
|
||||
One reverb space (Taj Mahal), tanpura drone throughout. Piano opens
|
||||
alone, cello joins, harp/oboe/flute take over with djembe, sitar
|
||||
arrives over tabla, builds to an EDM section with house drums.
|
||||
"""
|
||||
REV = "taj_mahal"
|
||||
score = Score("4/4", bpm=72)
|
||||
|
||||
# ── Drone — runs the entire piece ──
|
||||
tanpura = score.part("tanpura", synth="strings_synth", envelope="pad",
|
||||
detune=3, lowpass=1000, volume=0.12,
|
||||
reverb=0.5, reverb_type=REV)
|
||||
for _ in range(40):
|
||||
tanpura.add("A2", Duration.WHOLE)
|
||||
|
||||
# ── Bars 1-8: Piano alone, then cello ──
|
||||
piano = score.part("piano", instrument="piano", volume=0.35,
|
||||
reverb=0.35, reverb_type=REV)
|
||||
for notes in [
|
||||
["A2","E3","A3","C4","E4","C4","A3","E3"],
|
||||
["F2","C3","F3","A3","C4","A3","F3","C3"],
|
||||
["G2","D3","G3","B3","D4","B3","G3","D3"],
|
||||
["E2","B2","E3","G#3","B3","G#3","E3","B2"],
|
||||
["A2","E3","A3","C4","E4","C4","A3","E3"],
|
||||
["D2","A2","D3","F3","A3","F3","D3","A2"],
|
||||
["E2","B2","E3","G#3","B3","G#3","E3","B2"],
|
||||
["A2","E3","A3","C4","E4","A4","E4","C4"],
|
||||
]:
|
||||
for n in notes:
|
||||
piano.add(n, Duration.EIGHTH, velocity=68)
|
||||
|
||||
cello = score.part("cello", instrument="cello", volume=0.2,
|
||||
reverb=0.4, reverb_type=REV)
|
||||
cello.rest(Duration.WHOLE)
|
||||
for note, dur, vel in [
|
||||
("A3", 4.0, 55), ("C4", 4.0, 58),
|
||||
("B3", 2.0, 52), ("A3", 2.0, 50), ("G3", 4.0, 55),
|
||||
("F3", 4.0, 52), ("E3", 4.0, 55), ("A3", 4.0, 58),
|
||||
]:
|
||||
cello.add(note, dur, velocity=vel)
|
||||
|
||||
# ── Bars 9-16: Harp + oboe + flute + djembe ──
|
||||
harp = score.part("harp", instrument="harp", volume=0.28,
|
||||
reverb=0.45, reverb_type=REV)
|
||||
oboe = score.part("oboe", instrument="oboe", volume=0.22,
|
||||
reverb=0.4, reverb_type=REV)
|
||||
flute = score.part("flute", instrument="flute", volume=0.18,
|
||||
reverb=0.4, reverb_type=REV)
|
||||
for _ in range(8):
|
||||
harp.rest(Duration.WHOLE)
|
||||
for notes in [
|
||||
["A3","C4","E4","A4","C5","E5","A5","E5"],
|
||||
["D3","F3","A3","D4","F4","A4","D5","A4"],
|
||||
["E3","G3","B3","E4","G4","B4","E5","B4"],
|
||||
["A3","C4","E4","A4","E5","C5","A4","E4"],
|
||||
["F3","A3","C4","F4","A4","C5","F5","C5"],
|
||||
["E3","G#3","B3","E4","G#4","B4","E5","B4"],
|
||||
]:
|
||||
for n in notes:
|
||||
harp.add(n, Duration.EIGHTH, velocity=58)
|
||||
for _ in range(9):
|
||||
oboe.rest(Duration.WHOLE)
|
||||
for note, dur, vel in [
|
||||
("E5", 1.5, 62), ("D5", 0.5, 55), ("C5", 1.0, 58),
|
||||
("A4", 1.0, 62), ("G4", 1.0, 55), ("A4", 1.5, 58),
|
||||
("B4", 0.5, 52), ("C5", 2.0, 62), ("A4", 4.0, 58),
|
||||
]:
|
||||
oboe.add(note, dur, velocity=vel)
|
||||
for _ in range(10):
|
||||
flute.rest(Duration.WHOLE)
|
||||
for note, dur, vel in [
|
||||
("A5", 2.0, 50), ("G5", 1.0, 45), ("E5", 1.0, 48),
|
||||
("D5", 2.0, 50), ("C5", 1.0, 45), ("A4", 1.0, 48),
|
||||
("G4", 2.0, 50), ("A4", 2.0, 52),
|
||||
]:
|
||||
flute.add(note, dur, velocity=vel)
|
||||
|
||||
# ── Bars 15-20: Sitar + tabla ──
|
||||
sitar = score.part("sitar", instrument="sitar", volume=0.2,
|
||||
reverb=0.35, reverb_type=REV)
|
||||
for _ in range(14):
|
||||
sitar.rest(Duration.WHOLE)
|
||||
for note, dur, vel in [
|
||||
("A4", 1.0, 70), ("Bb4", 0.5, 60), ("A4", 0.5, 65),
|
||||
("C5", 1.5, 75), ("Bb4", 0.5, 60),
|
||||
("D5", 1.0, 70), ("E5", 1.5, 78),
|
||||
("F5", 0.5, 62), ("E5", 1.0, 70),
|
||||
("D5", 0.5, 62), ("C5", 0.5, 65), ("Bb4", 0.5, 58),
|
||||
("A4", 2.0, 75),
|
||||
("Bb4", 0.25, 55), ("C5", 0.25, 58), ("D5", 0.25, 62),
|
||||
("E5", 0.25, 68),
|
||||
("F5", 0.25, 65), ("G5", 0.25, 70), ("A5", 0.5, 80),
|
||||
("G5", 0.25, 62), ("F5", 0.25, 58), ("E5", 0.5, 62),
|
||||
("C5", 0.5, 58), ("Bb4", 0.5, 55), ("A4", 2.0, 75),
|
||||
]:
|
||||
sitar.add(note, dur, velocity=vel)
|
||||
|
||||
# ── EDM section — sitar over house beat ──
|
||||
# Solo sections: 8+8+8+12 = 36 beats = 9 bars
|
||||
# Total bars before EDM: 8 piano + 6 harp + 6 djembe + 4 tabla + 9 solo = 33
|
||||
edm_start = 33
|
||||
pad = score.part("pad", instrument="synth_pad", volume=0.18,
|
||||
reverb=0.45, reverb_type=REV,
|
||||
sidechain=0.6, sidechain_release=0.15)
|
||||
for _ in range(edm_start):
|
||||
pad.rest(Duration.WHOLE)
|
||||
for sym in ["Am", "F", "G", "Em"] * 2:
|
||||
pad.add(Chord.from_symbol(sym), Duration.WHOLE)
|
||||
|
||||
sub = score.part("sub", instrument="808_bass", volume=0.4)
|
||||
for _ in range(edm_start):
|
||||
sub.rest(Duration.WHOLE)
|
||||
for n in ["A1","A1","F1","F1","G1","G1","E1","E1"] * 2:
|
||||
sub.add(n, Duration.HALF)
|
||||
|
||||
sitar2 = score.part("sitar2", instrument="sitar", volume=0.4,
|
||||
reverb=0.3, reverb_type=REV)
|
||||
for _ in range(edm_start + 2):
|
||||
sitar2.rest(Duration.WHOLE)
|
||||
for note, dur, vel in [
|
||||
("A4", 0.25, 75), ("C5", 0.25, 78), ("E5", 0.5, 85),
|
||||
("D5", 0.25, 72), ("C5", 0.25, 70), ("A4", 0.5, 75),
|
||||
("G4", 0.25, 68), ("A4", 0.25, 72), ("C5", 0.5, 78),
|
||||
("A4", 0.5, 72),
|
||||
("E5", 0.5, 82), ("D5", 0.25, 72), ("C5", 0.25, 70),
|
||||
("A4", 0.5, 75), ("G4", 0.5, 68), ("A4", 1.0, 78),
|
||||
] * 2:
|
||||
sitar2.add(note, dur, velocity=vel)
|
||||
|
||||
# Drums: djembe bars 9-14, tabla bars 15-20, house bars 21-28
|
||||
DJB = DrumSound.DJEMBE_BASS
|
||||
DJT = DrumSound.DJEMBE_TONE
|
||||
DJS = DrumSound.DJEMBE_SLAP
|
||||
NA = DrumSound.TABLA_NA
|
||||
DH = DrumSound.TABLA_DHA
|
||||
TT = DrumSound.TABLA_TIT
|
||||
GB = DrumSound.TABLA_GE_BEND
|
||||
|
||||
silence = Pattern(name="s", time_signature="4/4", beats=32.0, hits=[])
|
||||
score.add_pattern(silence, repeats=1)
|
||||
p_dj = Pattern(name="dj", time_signature="4/4", beats=8.0, hits=[
|
||||
_Hit(DJB, 0.0, 48), _Hit(DJT, 1.0, 40), _Hit(DJT, 1.5, 35),
|
||||
_Hit(DJS, 2.0, 45), _Hit(DJT, 3.0, 40),
|
||||
_Hit(DJB, 4.0, 52), _Hit(DJT, 5.0, 42), _Hit(DJT, 5.5, 38),
|
||||
_Hit(DJS, 6.0, 48), _Hit(DJT, 6.5, 35), _Hit(DJS, 7.0, 45),
|
||||
])
|
||||
score.add_pattern(p_dj, repeats=3)
|
||||
p_tab = Pattern(name="tab", time_signature="4/4", beats=8.0, hits=[
|
||||
_Hit(DH, 0.0, 80), _Hit(TT, 0.5, 30), _Hit(NA, 1.0, 65),
|
||||
_Hit(NA, 2.0, 60), _Hit(DH, 3.0, 80),
|
||||
_Hit(DH, 4.0, 85), _Hit(TT, 4.25, 32), _Hit(TT, 4.5, 35),
|
||||
_Hit(NA, 5.0, 68), _Hit(TT, 5.5, 30), _Hit(NA, 6.0, 65),
|
||||
_Hit(DH, 7.0, 85),
|
||||
])
|
||||
score.add_pattern(p_tab, repeats=2)
|
||||
|
||||
# Tabla solo — everything drops out, just tabla and drone
|
||||
KE = DrumSound.TABLA_KE
|
||||
TI = DrumSound.TABLA_TIN
|
||||
GE = DrumSound.TABLA_GE
|
||||
T3 = 1.0 / 12.0 # 32nd triplet
|
||||
T9 = 1.0 / 9.0 # ninth note
|
||||
|
||||
# Part 1: whisper — space, breath, single hits
|
||||
p_solo1 = Pattern(name="solo1", time_signature="4/4", beats=8.0, hits=[
|
||||
_Hit(DH, 0.0, 78),
|
||||
_Hit(NA, 2.0, 55),
|
||||
_Hit(DH, 4.0, 82),
|
||||
_Hit(TT, 5.0, 30), _Hit(NA, 5.5, 52), _Hit(TT, 6.0, 28),
|
||||
_Hit(DH, 7.0, 78),
|
||||
])
|
||||
score.add_pattern(p_solo1, repeats=1)
|
||||
|
||||
# Part 2: ghosts emerge — 16th note ghost fills between accents
|
||||
p_solo2 = Pattern(name="solo2", time_signature="4/4", beats=8.0, hits=[
|
||||
_Hit(DH, 0.0, 92), _Hit(TT, 0.25, 32), _Hit(TT, 0.5, 35),
|
||||
_Hit(NA, 1.0, 68), _Hit(TT, 1.25, 30), _Hit(TT, 1.5, 28),
|
||||
_Hit(NA, 2.0, 62), _Hit(TT, 2.5, 32), _Hit(DH, 3.0, 88),
|
||||
_Hit(TT, 3.25, 35), _Hit(TT, 3.5, 38),
|
||||
_Hit(DH, 4.0, 95), _Hit(TT, 4.25, 38), _Hit(TT, 4.5, 42),
|
||||
_Hit(NA, 5.0, 72), _Hit(TT, 5.25, 32), _Hit(TT, 5.5, 35),
|
||||
_Hit(KE, 5.75, 40),
|
||||
_Hit(NA, 6.0, 68), _Hit(TT, 6.25, 35), _Hit(KE, 6.5, 38),
|
||||
_Hit(DH, 7.0, 95), _Hit(GB, 7.5, 88),
|
||||
])
|
||||
score.add_pattern(p_solo2, repeats=1)
|
||||
|
||||
# Part 3: call and response — dayan vs bayan, dynamics wide open
|
||||
p_solo3 = Pattern(name="solo3", time_signature="4/4", beats=8.0, hits=[
|
||||
# Dayan speaks
|
||||
_Hit(NA, 0.0, 110), _Hit(NA, 0.25, 55), _Hit(TT, 0.5, 38),
|
||||
_Hit(NA, 0.75, 100),
|
||||
# Bayan answers
|
||||
_Hit(GE, 1.0, 100), _Hit(GE, 1.25, 50), _Hit(GB, 1.5, 90),
|
||||
_Hit(GE, 1.75, 45),
|
||||
# Dayan louder
|
||||
_Hit(NA, 2.0, 115), _Hit(TT, 2.125, 30), _Hit(TT, 2.25, 35),
|
||||
_Hit(NA, 2.5, 105), _Hit(TT, 2.625, 32), _Hit(TT, 2.75, 38),
|
||||
# Bayan louder
|
||||
_Hit(GB, 3.0, 112), _Hit(KE, 3.25, 50), _Hit(GE, 3.5, 68),
|
||||
_Hit(KE, 3.75, 45),
|
||||
# Together — explosion
|
||||
_Hit(DH, 4.0, 120), _Hit(TT, 4.25, 45), _Hit(TT, 4.5, 48),
|
||||
_Hit(DH, 5.0, 115), _Hit(TT, 5.25, 42), _Hit(NA, 5.5, 72),
|
||||
# 9-tuplet — the weird one, breaks the grid
|
||||
*[_Hit(TT if i % 2 == 0 else KE, 6.0 + i * T9, 38 + i * 5)
|
||||
for i in range(9)],
|
||||
_Hit(DH, 7.0, 118),
|
||||
])
|
||||
score.add_pattern(p_solo3, repeats=1)
|
||||
|
||||
# Part 4: blazing — 32nd triplets, cascades, tihai finale
|
||||
p_solo4 = Pattern(name="solo4", time_signature="4/4", beats=12.0, hits=[
|
||||
# 32nd triplet opening flourish
|
||||
*[_Hit(TT, 0.0 + i * T3, 40 + i * 2) for i in range(12)],
|
||||
_Hit(DH, 1.0, 120), _Hit(GB, 1.5, 108),
|
||||
# Rapid alternating hands
|
||||
_Hit(NA, 2.0, 110), _Hit(KE, 2.125, 45), _Hit(NA, 2.25, 105),
|
||||
_Hit(KE, 2.375, 48), _Hit(NA, 2.5, 108), _Hit(KE, 2.625, 50),
|
||||
_Hit(NA, 2.75, 112),
|
||||
_Hit(DH, 3.0, 118),
|
||||
# Another 32nd triplet burst — longer, crescendo
|
||||
*[_Hit(TT, 3.5 + i * T3, 32 + i * 4) for i in range(18)],
|
||||
_Hit(DH, 5.0, 122), _Hit(DH, 5.25, 118), _Hit(GB, 5.5, 115),
|
||||
# 9 against 4 polyrhythm moment
|
||||
_Hit(GE, 6.0, 88), _Hit(GE, 7.0, 85),
|
||||
*[_Hit(NA if i % 3 == 0 else TT, 6.0 + i * (2.0 / 9.0), 42 + (i % 3) * 15)
|
||||
for i in range(9)],
|
||||
# Grand tihai — 3x pattern, each louder
|
||||
_Hit(DH, 8.0, 108), _Hit(NA, 8.25, 72), _Hit(TT, 8.5, 48),
|
||||
_Hit(KE, 8.75, 52), _Hit(DH, 9.0, 100),
|
||||
_Hit(DH, 9.25, 112), _Hit(NA, 9.5, 78), _Hit(TT, 9.75, 52),
|
||||
_Hit(KE, 10.0, 58), _Hit(DH, 10.25, 108),
|
||||
_Hit(DH, 10.5, 120), _Hit(NA, 10.75, 85), _Hit(TT, 11.0, 58),
|
||||
_Hit(KE, 11.25, 62), _Hit(DH, 11.5, 127),
|
||||
# Silence.....
|
||||
# SLAM
|
||||
_Hit(GB, 11.875, 127),
|
||||
])
|
||||
score.add_pattern(p_solo4, repeats=1)
|
||||
|
||||
score.drums("house", repeats=8)
|
||||
score.set_drum_effects(reverb=0.3, reverb_type=REV)
|
||||
|
||||
play_song(score, "Journey — Piano → World → Sitar EDM (Taj Mahal)")
|
||||
|
||||
|
||||
SONGS = {
|
||||
"1": ("Bossa Nova in A minor", bossa_nova_girl),
|
||||
"2": ("Bebop in Bb major", bebop_in_bb),
|
||||
@@ -1222,6 +1574,8 @@ SONGS = {
|
||||
"20": ("Temple Bell (Japanese)", temple_bell),
|
||||
"21": ("Cinematic Showcase (Orchestral)", cinematic_showcase),
|
||||
"22": ("Greensleeves (Renaissance Lute)", greensleeves),
|
||||
"23": ("Tabla Solo (Raga Yaman)", tabla_solo_yaman),
|
||||
"24": ("Journey (Western → World → Indian)", journey),
|
||||
}
|
||||
|
||||
if __name__ == "__main__":
|
||||
@@ -1235,7 +1589,7 @@ if __name__ == "__main__":
|
||||
print(f" {key:>2}. {name}")
|
||||
|
||||
print()
|
||||
choice = input(" Pick a song (1-22, or 'all'): ").strip()
|
||||
choice = input(" Pick a song (1-24, or 'all'): ").strip()
|
||||
print()
|
||||
|
||||
if choice == "all":
|
||||
|
||||
+111
-6
@@ -295,8 +295,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 +384,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
|
||||
|
||||
+89
-13
@@ -442,7 +442,7 @@ def flute_wave(hz, peak=SAMPLE_PEAK, n_samples=SAMPLE_RATE):
|
||||
|
||||
# Vibrato — develops after ~200ms
|
||||
vib_onset = numpy.clip(t / 0.2, 0.0, 1.0)
|
||||
vib = hz * 0.0015 * vib_onset * numpy.sin(2 * numpy.pi * 5.0 * t)
|
||||
vib = hz * 0.0008 * vib_onset * numpy.sin(2 * numpy.pi * 5.0 * t)
|
||||
|
||||
# Tube resonance — mostly fundamental + weak odd harmonics
|
||||
wave = numpy.sin(2 * numpy.pi * (hz + vib) * t) * 0.7
|
||||
@@ -1925,6 +1925,42 @@ def _synth_metal_hat(n_samples):
|
||||
return out
|
||||
|
||||
|
||||
def _synth_tabla_ge_bend(n_samples):
|
||||
"""Tabla Ge with upward pitch bend — palm pressing into bayan head.
|
||||
|
||||
The player strikes the bayan and then presses their palm into the
|
||||
head, raising the pitch dramatically. The signature bayan sound
|
||||
in Bollywood and fusion music.
|
||||
"""
|
||||
t = numpy.arange(n_samples, dtype=numpy.float32) / SAMPLE_RATE
|
||||
# Membrane thud
|
||||
thump_len = min(int(SAMPLE_RATE * 0.07), n_samples)
|
||||
thump_raw = _noise(thump_len)
|
||||
if thump_len > 20:
|
||||
bl, al = scipy.signal.butter(2, [40, 250], btype='band', fs=SAMPLE_RATE)
|
||||
thump = scipy.signal.lfilter(bl, al, numpy.pad(thump_raw, (0, max(0, n_samples - thump_len))))[:thump_len]
|
||||
else:
|
||||
thump = thump_raw
|
||||
thump *= _exp_decay(thump_len, 20) * 0.8
|
||||
# Pitch sweep UP — 60 Hz rising to 200+ Hz as palm presses
|
||||
# Gets quieter as pitch rises (palm mutes the head as it presses)
|
||||
freq = 60 + 180 * (1 - numpy.exp(-4 * t))
|
||||
phase = 2 * numpy.pi * numpy.cumsum(freq) / SAMPLE_RATE
|
||||
body = numpy.sin(phase) * _exp_decay(n_samples, 6) * 0.9
|
||||
# Metal shell resonance
|
||||
metal_len = min(int(SAMPLE_RATE * 0.1), n_samples)
|
||||
metal = numpy.sin(2 * numpy.pi * 150 * t[:metal_len]) * _exp_decay(metal_len, 8) * 0.3
|
||||
# Sub
|
||||
sub = _sine_f32(50, n_samples) * _exp_decay(n_samples, 5) * 0.4
|
||||
click_len = min(250, n_samples)
|
||||
click = _noise(click_len) * _exp_decay(click_len, 35) * 0.3
|
||||
result = body + sub
|
||||
result[:thump_len] += thump
|
||||
result[:metal_len] += metal
|
||||
result[:click_len] += click
|
||||
return numpy.tanh(result * 1.3).astype(numpy.float32)
|
||||
|
||||
|
||||
def _synth_djembe_bass(n_samples):
|
||||
"""Djembe bass — open palm strike in center of goatskin head.
|
||||
|
||||
@@ -2077,6 +2113,7 @@ def _render_drum_hit(sound_value, n_samples):
|
||||
DrumSound.TABLA_DHA.value: lambda n: _synth_tabla_dha(n),
|
||||
DrumSound.TABLA_TIT.value: lambda n: _synth_tabla_tit(n),
|
||||
DrumSound.TABLA_KE.value: lambda n: _synth_tabla_ke(n),
|
||||
DrumSound.TABLA_GE_BEND.value: lambda n: _synth_tabla_ge_bend(n),
|
||||
# Dhol
|
||||
DrumSound.DHOL_DAGGA.value: lambda n: _synth_dhol_dagga(n),
|
||||
DrumSound.DHOL_TILLI.value: lambda n: _synth_dhol_tilli(n),
|
||||
@@ -3246,31 +3283,44 @@ def _render_notes_to_buf(notes, buf, samples_per_beat, total_samples,
|
||||
if analog > 0:
|
||||
pitches = [hz * (2 ** (_rnd.gauss(0, analog * 5) / 1200))
|
||||
for hz in pitches]
|
||||
# Pitch bend: render with time-varying frequency
|
||||
# Pitch bend: render at base pitch, then resample to shift
|
||||
# pitch over time. Resampling preserves the synth's timbre
|
||||
# perfectly — no sine waves, no retriggering.
|
||||
bend_amt = getattr(note, 'bend', 0.0)
|
||||
if bend_amt != 0:
|
||||
bend_type = getattr(note, 'bend_type', 'smooth')
|
||||
t_bend = numpy.arange(n_samples, dtype=numpy.float64) / SAMPLE_RATE
|
||||
t_norm = numpy.clip(t_bend / (dur_ms / 1000), 0.0, 1.0)
|
||||
t_norm = numpy.linspace(0, 1, n_samples)
|
||||
|
||||
waves = []
|
||||
for hz in pitches:
|
||||
hz_end = hz * (2 ** (bend_amt / 12))
|
||||
# Build pitch ratio curve (1.0 = no shift)
|
||||
if bend_type == 'smooth':
|
||||
# Log interpolation — perceptually linear pitch
|
||||
freq_curve = hz * (hz_end / hz) ** t_norm
|
||||
ratio = (hz_end / hz) ** t_norm
|
||||
elif bend_type == 'linear':
|
||||
freq_curve = hz + (hz_end - hz) * t_norm
|
||||
ratio = 1.0 + (hz_end / hz - 1.0) * t_norm
|
||||
elif bend_type == 'late':
|
||||
# Hold pitch for 60%, bend in last 40%
|
||||
late_t = numpy.clip((t_norm - 0.6) / 0.4, 0.0, 1.0)
|
||||
freq_curve = hz * (hz_end / hz) ** late_t
|
||||
ratio = (hz_end / hz) ** late_t
|
||||
else:
|
||||
freq_curve = hz * (hz_end / hz) ** t_norm
|
||||
ratio = (hz_end / hz) ** t_norm
|
||||
|
||||
# Phase accumulation for smooth frequency change
|
||||
phase = numpy.cumsum(2 * numpy.pi * freq_curve / SAMPLE_RATE)
|
||||
bent = numpy.sin(phase).astype(numpy.float64)
|
||||
# Render a longer buffer at base pitch
|
||||
max_ratio = max(ratio.max(), 1.0)
|
||||
src_len = int(n_samples * max_ratio) + 100
|
||||
src = synth_fn(hz, n_samples=src_len, **_skw)
|
||||
src_f = src.astype(numpy.float64) / SAMPLE_PEAK
|
||||
|
||||
# Variable-rate resampling: read through source
|
||||
# at speed determined by the ratio curve
|
||||
read_pos = numpy.cumsum(ratio)
|
||||
read_pos = (read_pos - read_pos[0]).astype(numpy.float64)
|
||||
# Clamp to source bounds
|
||||
read_pos = numpy.clip(read_pos, 0, src_len - 2)
|
||||
# Linear interpolation
|
||||
idx = read_pos.astype(numpy.int64)
|
||||
frac = read_pos - idx
|
||||
bent = src_f[idx] * (1 - frac) + src_f[numpy.minimum(idx + 1, src_len - 1)] * frac
|
||||
waves.append((bent * SAMPLE_PEAK).astype(numpy.int16))
|
||||
else:
|
||||
# Render oscillators (pass synth_kwargs for FM etc.)
|
||||
@@ -3339,6 +3389,12 @@ def _render_notes_to_buf(notes, buf, samples_per_beat, total_samples,
|
||||
vel_cutoff = vel_to_filter * vel_scale + 1000
|
||||
mixed = _apply_lowpass(mixed, vel_cutoff, q=filter_q)
|
||||
end = min(start + len(mixed), total_samples)
|
||||
# Choke: fade out any existing signal at this point
|
||||
# so new notes don't pile up on previous tails
|
||||
choke_len = min(int(SAMPLE_RATE * 0.003), start)
|
||||
if choke_len > 0:
|
||||
fade = numpy.linspace(1.0, 0.0, choke_len).astype(numpy.float32)
|
||||
buf[start - choke_len:start] *= fade
|
||||
buf[start:end] += mixed[:end - start] * volume * vel_scale
|
||||
# Spread detuned oscillators into stereo L/R
|
||||
if detune_up is not None and stereo_buf is not None:
|
||||
@@ -3641,6 +3697,7 @@ def render_score(score):
|
||||
DrumSound.TABLA_GE.value: -0.2,
|
||||
DrumSound.TABLA_KE.value: -0.2,
|
||||
DrumSound.TABLA_DHA.value: 0.0, # both drums = center
|
||||
DrumSound.TABLA_GE_BEND.value: -0.2,
|
||||
# Dhol: bass left, treble right
|
||||
DrumSound.DHOL_DAGGA.value: -0.2,
|
||||
DrumSound.DHOL_TILLI.value: 0.2,
|
||||
@@ -3674,6 +3731,10 @@ def render_score(score):
|
||||
drum_parts = [p for p in score.parts.values() if p.is_drums]
|
||||
for drum_part in drum_parts:
|
||||
part_stereo = numpy.zeros((total_samples, 2), dtype=numpy.float32)
|
||||
# Track last hit position per sound for choke (new hit dampens
|
||||
# the previous ring on the same drum)
|
||||
_last_hit_start = {}
|
||||
|
||||
for hit in drum_part._drum_hits:
|
||||
pos = hit.position
|
||||
if drum_swing > 0:
|
||||
@@ -3690,6 +3751,21 @@ def render_score(score):
|
||||
start = max(0, start)
|
||||
if start >= total_samples or start < 0:
|
||||
continue
|
||||
|
||||
# Choke: if the same sound was hit recently, fade out
|
||||
# the tail at this point (new hit dampens old resonance)
|
||||
sound_id = hit.sound.value
|
||||
if sound_id in _last_hit_start:
|
||||
prev_start = _last_hit_start[sound_id]
|
||||
# Quick 2ms fade-out at the new hit position
|
||||
fade_len = min(int(SAMPLE_RATE * 0.002), max(0, start - prev_start))
|
||||
if fade_len > 0 and start > 0:
|
||||
fade = numpy.linspace(1.0, 0.0, fade_len).astype(numpy.float32)
|
||||
fade_start = max(0, start - fade_len)
|
||||
for ch in range(2):
|
||||
part_stereo[fade_start:start, ch] *= fade
|
||||
_last_hit_start[sound_id] = start
|
||||
|
||||
remaining = total_samples - start
|
||||
hit_len = min(int(SAMPLE_RATE * 0.5), remaining)
|
||||
wave = _render_drum_hit(hit.sound.value, hit_len)
|
||||
|
||||
@@ -428,6 +428,7 @@ class DrumSound(Enum):
|
||||
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)
|
||||
|
||||
+21
-5
@@ -2,8 +2,8 @@ from ._statics import (
|
||||
TEMPERAMENTS, TONES, DEGREES, SCALES,
|
||||
INDIAN_SCALES, ARABIC_SCALES, JAPANESE_SCALES,
|
||||
BLUES_SCALES, GAMELAN_SCALES, SYSTEMS,
|
||||
TONES_SHRUTI, DEGREES_SHRUTI, SHRUTI_SCALES,
|
||||
TONES_ARABIC_24, DEGREES_ARABIC_24, ARABIC_24_SCALES,
|
||||
TONES_SHRUTI, DEGREES_SHRUTI, SHRUTI_SCALES, SHRUTI_RATIOS,
|
||||
TONES_ARABIC_24, DEGREES_ARABIC_24, ARABIC_24_SCALES, MAQAM_RATIOS,
|
||||
TONES_SLENDRO, DEGREES_SLENDRO, SLENDRO_SCALES,
|
||||
TONES_PELOG, DEGREES_PELOG, PELOG_SCALES,
|
||||
TONES_THAI, DEGREES_THAI, THAI_SCALES,
|
||||
@@ -14,7 +14,7 @@ from ._statics import (
|
||||
|
||||
class System:
|
||||
def __init__(self, *, tone_names, degrees, scales=None, c_index=None,
|
||||
period=2.0):
|
||||
period=2.0, ratios=None):
|
||||
self.tone_names = tone_names
|
||||
|
||||
self.degrees = degrees
|
||||
@@ -25,6 +25,11 @@ class System:
|
||||
# 3.0 for Bohlen-Pierce (tritave).
|
||||
self.period = period
|
||||
|
||||
# Custom frequency ratios: if set, overrides equal temperament.
|
||||
# A list of N floats (one per tone), each relative to the first
|
||||
# tone (1.0). For example, just intonation shruti ratios.
|
||||
self.ratios = ratios
|
||||
|
||||
# c_index: the index of the "reference C" in the tone list.
|
||||
# For octave arithmetic — scientific pitch changes octave at C.
|
||||
# Default 3 for 12-TET western (A=0, A#=1, B=2, C=3).
|
||||
@@ -214,6 +219,17 @@ class System:
|
||||
# descending goes in meta?
|
||||
return {"intervals": scale, "hemitonic": hemitonic, "meta": {}}
|
||||
|
||||
def tone(self, name, octave=4):
|
||||
"""Create a Tone in this system. Shorthand for ``Tone(name, octave=octave, system=self)``.
|
||||
|
||||
Example::
|
||||
|
||||
>>> edo19 = TET(19)
|
||||
>>> edo19.tone(5, octave=4).frequency
|
||||
"""
|
||||
from . import Tone
|
||||
return Tone(name, octave=octave, system=self)
|
||||
|
||||
def __repr__(self):
|
||||
return f"<System semitones={self.semitones!r}>"
|
||||
|
||||
@@ -352,9 +368,9 @@ SYSTEMS = {
|
||||
"31-tet": TET(31, names=_31TET_NAMES),
|
||||
# Microtonal systems with proper intervals (not 12-TET approximations)
|
||||
"shruti": System(tone_names=TONES_SHRUTI, degrees=DEGREES_SHRUTI,
|
||||
scales=SHRUTI_SCALES, c_index=5),
|
||||
scales=SHRUTI_SCALES, c_index=5, ratios=SHRUTI_RATIOS),
|
||||
"maqam": System(tone_names=TONES_ARABIC_24, degrees=DEGREES_ARABIC_24,
|
||||
scales=ARABIC_24_SCALES, c_index=5),
|
||||
scales=ARABIC_24_SCALES, c_index=5, ratios=MAQAM_RATIOS),
|
||||
"slendro": System(tone_names=TONES_SLENDRO, degrees=DEGREES_SLENDRO,
|
||||
scales=SLENDRO_SCALES, c_index=1),
|
||||
"pelog": System(tone_names=TONES_PELOG, degrees=DEGREES_PELOG,
|
||||
|
||||
+57
-6
@@ -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,9 +810,12 @@ 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)
|
||||
# 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)
|
||||
import sympy
|
||||
pitch_scale = [period ** sympy.Rational(i, tones) for i in range(tones + 1)]
|
||||
else:
|
||||
|
||||
Reference in New Issue
Block a user