mirror of
https://github.com/kennethreitz/pytheory.git
synced 2026-06-05 23:00:20 +00:00
Compare commits
32 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| a0756b3172 | |||
| e3dd706032 | |||
| 9b412906bc | |||
| 54e0421997 | |||
| 109343ad30 | |||
| 28e84de566 | |||
| d353d64298 | |||
| 7ee02e7ed2 | |||
| a5c9a46eb2 | |||
| f9c63ec360 | |||
| b9e88b77d8 | |||
| 1910b09132 | |||
| 0c5287450b | |||
| 5ac1873d83 | |||
| 9fafca2b08 | |||
| af044f68ca | |||
| 60f697f846 | |||
| 7d678e364e | |||
| 3a8d829010 | |||
| 2a67906937 | |||
| b9dcad0454 | |||
| db9726168a | |||
| 26af923789 | |||
| 72e18a9bec | |||
| 7d56ed7a2c | |||
| 6efa4f18ce | |||
| 06fc4cabb7 | |||
| d3a93c18b3 | |||
| 0e10359236 | |||
| df00c3436d | |||
| 2f02df15b8 | |||
| a2740b8d57 |
@@ -2,6 +2,77 @@
|
||||
|
||||
All notable changes to PyTheory are documented here.
|
||||
|
||||
## 0.39.3
|
||||
|
||||
- **33 audio samples in documentation** — every `play_score()` example
|
||||
now has an embedded stereo audio player. Covers quickstart, sequencing,
|
||||
drums (all world percussion), playback, and cookbook.
|
||||
- **`docs/generate_audio.py`** — renders all doc examples to WAV
|
||||
- Numpy vectorization: cached time arrays, decay envelopes, drum hits;
|
||||
vectorized piano harmonic synthesis
|
||||
- Fixed acid legato example (removed pad envelope, added proper 303 recipe)
|
||||
|
||||
## 0.39.2
|
||||
|
||||
- **Marching percussion** — snare, rimshot, and stick click sounds with
|
||||
high-tension kevlar synthesis and woody-metallic rimshot crack
|
||||
- **`Part.flam()`**, **`Part.diddle()`**, **`Part.cheese()`** — marching
|
||||
rudiment methods for any drum sound
|
||||
- **`Part ensemble=`** — duplicate voices with per-player timing tendencies
|
||||
and micro pitch drift. Works on any Part (drumline, string section, choir).
|
||||
`ensemble=20` for a full snare line, `ensemble=4` for a string quartet.
|
||||
- **Sympathetic resonance** — marching snare buzz builds up with repeated
|
||||
hits, decays during rests (like real snare wire response)
|
||||
- **4 marching patterns** — march, cadence, paradiddle, roll
|
||||
- **Chakradar tabla pattern** — 16-beat tihai of tihais composition
|
||||
- Song #32: Snare Cadence (flams, diddles, cheese, triplets, 32nds)
|
||||
|
||||
## 0.39.1
|
||||
|
||||
- **Chakradar tabla pattern** — 16-beat tihai of tihais composition with
|
||||
3 escalating phrases and a crescendo triplet finale
|
||||
|
||||
## 0.39.0
|
||||
|
||||
- **Dropped `numeral` dependency** — Roman numeral helpers inlined,
|
||||
reducing supply chain surface (#47)
|
||||
- **`Part.ramp()`** — smooth parameter automation with 4 interpolation
|
||||
curves (linear, ease_in, ease_out, ease_in_out)
|
||||
- **Articulations** — staccato, legato, marcato, tenuto, accent, fermata
|
||||
- **Dynamic curves** — crescendo(), decrescendo(), swell(), dynamics()
|
||||
- **`Part.hit()`** — individual drum sounds with articulation support
|
||||
- **Cross-choke drum damping** — djembe, hi-hats, cajón, doumbek
|
||||
- **5 new djembe patterns** + 3 djembe fills (30 fills total)
|
||||
- **6 new drum fills** — 3 cajón, 3 metal
|
||||
- **Duration arithmetic** — multiply, divide, add
|
||||
- **Improved djembe slap** synthesis
|
||||
- Song #31: Acid Tabla
|
||||
|
||||
## 0.38.2
|
||||
|
||||
- **`Part.ramp()`** — smooth parameter automation from current value to
|
||||
target over a duration. Works for lowpass, reverb, distortion, chorus,
|
||||
delay, volume, and any `.set()` parameter. Four interpolation curves:
|
||||
linear, ease_in, ease_out, ease_in_out.
|
||||
|
||||
## 0.38.1
|
||||
|
||||
- **Dynamic curves** — `Part.crescendo()`, `Part.decrescendo()`,
|
||||
`Part.swell()`, and `Part.dynamics()` for velocity ramps and custom
|
||||
curves across a sequence of notes
|
||||
|
||||
## 0.38.0
|
||||
|
||||
- **Articulations** — `staccato`, `legato`, `marcato`, `tenuto`, `accent`,
|
||||
`fermata` via `articulation=` on `Part.add()` and `Part.hold()`
|
||||
- **`Part.hit()`** — place individual drum sounds in a Part's note stream
|
||||
with articulation, velocity, and effects support
|
||||
- **5 new djembe patterns** — dununba, tiriba, yankadi, djansa, mendiani
|
||||
- **3 new djembe fills** — djembe call, djembe roll, djembe break (30 fills total)
|
||||
- **Cross-choke drum damping** — striking one sound fades out related sounds
|
||||
(djembe, hi-hats, cajón, doumbek)
|
||||
- **Improved djembe slap** — dry goatskin pop instead of snare-like noise
|
||||
|
||||
## 0.37.0
|
||||
|
||||
- **5 new djembe patterns** — dununba, tiriba, yankadi, djansa, mendiani
|
||||
|
||||
Vendored
BIN
Binary file not shown.
BIN
Binary file not shown.
Vendored
BIN
Binary file not shown.
Vendored
BIN
Binary file not shown.
Vendored
BIN
Binary file not shown.
BIN
Binary file not shown.
Vendored
BIN
Binary file not shown.
Vendored
BIN
Binary file not shown.
Vendored
BIN
Binary file not shown.
Vendored
BIN
Binary file not shown.
Vendored
BIN
Binary file not shown.
Vendored
BIN
Binary file not shown.
Vendored
BIN
Binary file not shown.
Vendored
BIN
Binary file not shown.
Vendored
BIN
Binary file not shown.
Vendored
BIN
Binary file not shown.
Vendored
BIN
Binary file not shown.
Vendored
BIN
Binary file not shown.
Vendored
BIN
Binary file not shown.
Vendored
BIN
Binary file not shown.
Vendored
BIN
Binary file not shown.
Vendored
BIN
Binary file not shown.
BIN
Binary file not shown.
Vendored
BIN
Binary file not shown.
Vendored
BIN
Binary file not shown.
Vendored
BIN
Binary file not shown.
Vendored
BIN
Binary file not shown.
Vendored
BIN
Binary file not shown.
Vendored
BIN
Binary file not shown.
Vendored
BIN
Binary file not shown.
Vendored
BIN
Binary file not shown.
BIN
Binary file not shown.
Vendored
BIN
Binary file not shown.
Vendored
BIN
Binary file not shown.
Vendored
+4
@@ -0,0 +1,4 @@
|
||||
<audio controls style="width: 100%; margin: 0.5em 0 1.5em 0;">
|
||||
<source src="{{ pathto('_static/audio/' + file, 1) }}" type="audio/wav">
|
||||
Your browser does not support the audio element.
|
||||
</audio>
|
||||
@@ -0,0 +1,573 @@
|
||||
"""Generate audio samples for documentation.
|
||||
|
||||
Renders code examples from the docs as WAV files so they can be
|
||||
embedded as <audio> players on the website.
|
||||
|
||||
Usage:
|
||||
uv run python docs/generate_audio.py
|
||||
"""
|
||||
|
||||
import os
|
||||
import sys
|
||||
|
||||
sys.path.insert(0, os.path.join(os.path.dirname(__file__), ".."))
|
||||
|
||||
from pytheory import Score, Duration, Key, Chord, Fretboard, DrumSound
|
||||
from pytheory.play import render_score, SAMPLE_RATE
|
||||
|
||||
import numpy
|
||||
import struct
|
||||
|
||||
AUDIO_DIR = os.path.join(os.path.dirname(__file__), "_static", "audio")
|
||||
os.makedirs(AUDIO_DIR, exist_ok=True)
|
||||
|
||||
|
||||
def save_wav(buf, path):
|
||||
"""Save a float32 buffer as 16-bit stereo WAV."""
|
||||
# Handle both mono (n,) and stereo (n, 2) buffers
|
||||
if buf.ndim == 1:
|
||||
channels = 1
|
||||
n_frames = len(buf)
|
||||
else:
|
||||
channels = buf.shape[1]
|
||||
n_frames = buf.shape[0]
|
||||
peak = numpy.abs(buf).max()
|
||||
if peak > 0:
|
||||
buf = buf / peak * 0.9
|
||||
samples = (buf * 32767).astype(numpy.int16)
|
||||
byte_rate = SAMPLE_RATE * channels * 2
|
||||
block_align = channels * 2
|
||||
data_size = n_frames * channels * 2
|
||||
with open(path, "wb") as f:
|
||||
f.write(b"RIFF")
|
||||
f.write(struct.pack("<I", 36 + data_size))
|
||||
f.write(b"WAVE")
|
||||
f.write(b"fmt ")
|
||||
f.write(struct.pack("<IHHIIHH", 16, 1, channels, SAMPLE_RATE,
|
||||
byte_rate, block_align, 16))
|
||||
f.write(b"data")
|
||||
f.write(struct.pack("<I", data_size))
|
||||
f.write(samples.tobytes())
|
||||
label = "stereo" if channels == 2 else "mono"
|
||||
print(f" {os.path.basename(path)} ({n_frames/SAMPLE_RATE:.1f}s, {label})")
|
||||
|
||||
|
||||
def render(name, score):
|
||||
"""Render a score to WAV in the audio directory."""
|
||||
buf = render_score(score)
|
||||
save_wav(buf, os.path.join(AUDIO_DIR, f"{name}.wav"))
|
||||
|
||||
|
||||
# ── Piano hold (polyphonic overlap) ──────────────────────────────────────
|
||||
|
||||
def gen_piano_hold():
|
||||
score = Score("4/4", bpm=85)
|
||||
piano = score.part("piano", instrument="piano", reverb=0.3)
|
||||
piano.hold("C3", Duration.WHOLE * 2, velocity=60)
|
||||
piano.hold("E3", Duration.WHOLE * 2, velocity=55)
|
||||
piano.hold("G3", Duration.WHOLE * 2, velocity=55)
|
||||
for n in ["E4", "G4", "C5", "G4", "E4", "D4", "C4", "E4"]:
|
||||
piano.add(n, Duration.QUARTER, velocity=80)
|
||||
render("piano_hold", score)
|
||||
|
||||
|
||||
# ── Articulations ────────────────────────────────────────────────────────
|
||||
|
||||
def gen_articulations():
|
||||
score = Score("4/4", bpm=90)
|
||||
piano = score.part("piano", instrument="piano", reverb=0.25)
|
||||
for n in ["C4", "E4", "G4", "C5"]:
|
||||
piano.add(n, Duration.QUARTER, velocity=80)
|
||||
for n in ["C4", "E4", "G4", "C5"]:
|
||||
piano.add(n, Duration.QUARTER, velocity=80, articulation="staccato")
|
||||
for n in ["C5", "G4", "E4", "C4"]:
|
||||
piano.add(n, Duration.QUARTER, velocity=80, articulation="legato")
|
||||
for n in ["C4", "E4", "G4", "C5"]:
|
||||
piano.add(n, Duration.QUARTER, velocity=80, articulation="marcato")
|
||||
render("articulations", score)
|
||||
|
||||
|
||||
# ── Dynamic curves ───────────────────────────────────────────────────────
|
||||
|
||||
def gen_dynamics():
|
||||
score = Score("4/4", bpm=90)
|
||||
piano = score.part("piano", instrument="piano", reverb=0.3)
|
||||
piano.crescendo(["C4", "D4", "E4", "F4", "G4", "A4", "B4", "C5"],
|
||||
Duration.QUARTER, start_vel=30, end_vel=110)
|
||||
piano.decrescendo(["C5", "B4", "A4", "G4", "F4", "E4", "D4", "C4"],
|
||||
Duration.QUARTER, start_vel=110, end_vel=30)
|
||||
render("dynamics", score)
|
||||
|
||||
|
||||
# ── Filter ramp ──────────────────────────────────────────────────────────
|
||||
|
||||
def gen_filter_ramp():
|
||||
score = Score("4/4", bpm=130)
|
||||
score.drums("house", repeats=8, fill="house", fill_every=8)
|
||||
score.set_drum_effects(volume=0.4)
|
||||
acid = score.part("acid", synth="saw", volume=0.6,
|
||||
lowpass=300, lowpass_q=8.0, distortion=0.3,
|
||||
legato=True, glide=0.03)
|
||||
acid.ramp(over=Duration.WHOLE * 4, curve="ease_in", lowpass=5000)
|
||||
for _ in range(4):
|
||||
for n in ["C2", "C3", "C2", "Eb2", "C2", "G2", "Bb2", "C2"]:
|
||||
acid.add(n, Duration.EIGHTH, velocity=90)
|
||||
acid.ramp(over=Duration.WHOLE * 4, curve="ease_out", lowpass=300)
|
||||
for _ in range(4):
|
||||
for n in ["C2", "C3", "C2", "Eb2", "C2", "G2", "Bb2", "C2"]:
|
||||
acid.add(n, Duration.EIGHTH, velocity=88)
|
||||
render("filter_ramp", score)
|
||||
|
||||
|
||||
# ── Rock beat ────────────────────────────────────────────────────────────
|
||||
|
||||
def gen_rock_beat():
|
||||
score = Score("4/4", bpm=120)
|
||||
score.drums("rock", repeats=4, fill="rock", fill_every=4)
|
||||
render("rock_beat", score)
|
||||
|
||||
|
||||
def gen_bossa_nova_pattern():
|
||||
score = Score("4/4", bpm=140)
|
||||
score.drums("bossa nova", repeats=4)
|
||||
render("bossa_nova_pattern", score)
|
||||
|
||||
|
||||
def gen_salsa_pattern():
|
||||
score = Score("4/4", bpm=180)
|
||||
score.drums("salsa", repeats=4)
|
||||
render("salsa_pattern", score)
|
||||
|
||||
|
||||
def gen_afrobeat_pattern():
|
||||
score = Score("4/4", bpm=110)
|
||||
score.drums("afrobeat", repeats=8)
|
||||
render("afrobeat_pattern", score)
|
||||
|
||||
|
||||
# ── Bossa nova ───────────────────────────────────────────────────────────
|
||||
|
||||
def gen_bossa_nova():
|
||||
score = Score("4/4", bpm=140)
|
||||
score.drums("bossa nova", repeats=4)
|
||||
rhodes = score.part("rhodes", synth="fm", envelope="piano", volume=0.3,
|
||||
reverb=0.4, reverb_decay=1.8)
|
||||
for sym in ["Am", "Am", "Dm", "Dm", "E7", "E7", "Am", "Am"]:
|
||||
rhodes.add(Chord.from_symbol(sym), Duration.WHOLE)
|
||||
render("bossa_nova", score)
|
||||
|
||||
|
||||
# ── Djembe ───────────────────────────────────────────────────────────────
|
||||
|
||||
def gen_djembe():
|
||||
score = Score("4/4", bpm=110)
|
||||
score.drums("tiriba", repeats=4, fill="djembe call", fill_every=4)
|
||||
score.set_drum_effects(reverb=0.2)
|
||||
render("djembe", score)
|
||||
|
||||
|
||||
# ── Tabla ────────────────────────────────────────────────────────────────
|
||||
|
||||
def gen_tabla():
|
||||
score = Score("4/4", bpm=80)
|
||||
score.drums("teental", repeats=2)
|
||||
score.drums("chakradar", repeats=1)
|
||||
score.set_drum_effects(reverb=0.2)
|
||||
render("tabla", score)
|
||||
|
||||
|
||||
# ── Marching snare ───────────────────────────────────────────────────────
|
||||
|
||||
def gen_metal_blast():
|
||||
score = Score("4/4", bpm=200)
|
||||
score.drums("metal blast", repeats=8, fill="metal cascade", fill_every=4)
|
||||
render("metal_blast", score)
|
||||
|
||||
|
||||
def gen_cajon():
|
||||
score = Score("4/4", bpm=100)
|
||||
score.drums("cajon", repeats=8, fill="cajon flam", fill_every=4)
|
||||
render("cajon", score)
|
||||
|
||||
|
||||
def gen_tabla_teental():
|
||||
score = Score("4/4", bpm=160)
|
||||
score.drums("teental", repeats=4)
|
||||
score.set_drum_effects(reverb=0.2)
|
||||
render("tabla_teental", score)
|
||||
|
||||
|
||||
def gen_tabla_keherwa():
|
||||
score = Score("4/4", bpm=180)
|
||||
score.drums("keherwa", repeats=4, fill="chakkardar", fill_every=4)
|
||||
score.set_drum_effects(reverb=0.2)
|
||||
render("tabla_keherwa", score)
|
||||
|
||||
|
||||
def gen_tabla_chakradar():
|
||||
score = Score("4/4", bpm=200)
|
||||
score.drums("teental", repeats=2)
|
||||
score.drums("chakradar", repeats=1)
|
||||
score.set_drum_effects(reverb=0.2)
|
||||
render("tabla_chakradar", score)
|
||||
|
||||
|
||||
def gen_dhol():
|
||||
score = Score("4/4", bpm=160)
|
||||
score.drums("bhangra", repeats=4)
|
||||
render("dhol", score)
|
||||
|
||||
|
||||
def gen_dholak():
|
||||
score = Score("4/4", bpm=120)
|
||||
score.drums("qawwali", repeats=4)
|
||||
render("dholak", score)
|
||||
|
||||
|
||||
def gen_mridangam():
|
||||
score = Score("4/4", bpm=90)
|
||||
score.drums("adi talam", repeats=4)
|
||||
render("mridangam", score)
|
||||
|
||||
|
||||
def gen_march_snare():
|
||||
score = Score("4/4", bpm=120)
|
||||
p = score.part("snare", synth="sine", volume=0.8, reverb=0.15)
|
||||
S = DrumSound.MARCH_SNARE
|
||||
R = DrumSound.MARCH_RIMSHOT
|
||||
C = DrumSound.MARCH_CLICK
|
||||
|
||||
for _ in range(4):
|
||||
p.hit(C, Duration.QUARTER, velocity=95)
|
||||
|
||||
for _ in range(4):
|
||||
p.hit(R, Duration.SIXTEENTH, velocity=118)
|
||||
p.hit(S, Duration.SIXTEENTH, velocity=30)
|
||||
p.hit(S, Duration.SIXTEENTH, velocity=32)
|
||||
p.hit(S, Duration.SIXTEENTH, velocity=28)
|
||||
p.hit(R, Duration.SIXTEENTH, velocity=115)
|
||||
p.hit(S, Duration.SIXTEENTH, velocity=30)
|
||||
p.hit(S, Duration.SIXTEENTH, velocity=28)
|
||||
p.hit(S, Duration.SIXTEENTH, velocity=32)
|
||||
p.hit(R, Duration.SIXTEENTH, velocity=118)
|
||||
p.hit(S, Duration.SIXTEENTH, velocity=35)
|
||||
p.hit(S, Duration.SIXTEENTH, velocity=28)
|
||||
p.hit(S, Duration.SIXTEENTH, velocity=30)
|
||||
p.hit(R, Duration.SIXTEENTH, velocity=120)
|
||||
p.hit(S, Duration.SIXTEENTH, velocity=30)
|
||||
p.hit(S, Duration.SIXTEENTH, velocity=28)
|
||||
p.hit(S, Duration.SIXTEENTH, velocity=32)
|
||||
render("march_snare", score)
|
||||
|
||||
|
||||
# ── Ensemble comparison ──────────────────────────────────────────────────
|
||||
|
||||
def gen_ensemble():
|
||||
score = Score("4/4", bpm=120)
|
||||
S = DrumSound.MARCH_SNARE
|
||||
R = DrumSound.MARCH_RIMSHOT
|
||||
|
||||
# Solo first
|
||||
solo = score.part("solo", synth="sine", volume=0.7, reverb=0.15)
|
||||
for _ in range(2):
|
||||
solo.hit(R, Duration.SIXTEENTH, velocity=118)
|
||||
solo.hit(S, Duration.SIXTEENTH, velocity=30)
|
||||
solo.hit(S, Duration.SIXTEENTH, velocity=32)
|
||||
solo.hit(S, Duration.SIXTEENTH, velocity=28)
|
||||
solo.hit(R, Duration.SIXTEENTH, velocity=115)
|
||||
solo.hit(S, Duration.SIXTEENTH, velocity=30)
|
||||
solo.hit(S, Duration.SIXTEENTH, velocity=28)
|
||||
solo.hit(S, Duration.SIXTEENTH, velocity=32)
|
||||
solo.hit(R, Duration.SIXTEENTH, velocity=118)
|
||||
solo.hit(S, Duration.SIXTEENTH, velocity=35)
|
||||
solo.hit(S, Duration.SIXTEENTH, velocity=28)
|
||||
solo.hit(S, Duration.SIXTEENTH, velocity=30)
|
||||
solo.hit(R, Duration.SIXTEENTH, velocity=120)
|
||||
solo.hit(S, Duration.SIXTEENTH, velocity=30)
|
||||
solo.hit(S, Duration.SIXTEENTH, velocity=28)
|
||||
solo.hit(S, Duration.SIXTEENTH, velocity=32)
|
||||
|
||||
# Then ensemble
|
||||
line = score.part("line", synth="sine", volume=0.7, reverb=0.15, ensemble=8)
|
||||
for _ in range(8):
|
||||
line.rest(Duration.QUARTER)
|
||||
for _ in range(4):
|
||||
line.hit(R, Duration.SIXTEENTH, velocity=118)
|
||||
line.hit(S, Duration.SIXTEENTH, velocity=30)
|
||||
line.hit(S, Duration.SIXTEENTH, velocity=32)
|
||||
line.hit(S, Duration.SIXTEENTH, velocity=28)
|
||||
line.hit(R, Duration.SIXTEENTH, velocity=115)
|
||||
line.hit(S, Duration.SIXTEENTH, velocity=30)
|
||||
line.hit(S, Duration.SIXTEENTH, velocity=28)
|
||||
line.hit(S, Duration.SIXTEENTH, velocity=32)
|
||||
line.hit(R, Duration.SIXTEENTH, velocity=118)
|
||||
line.hit(S, Duration.SIXTEENTH, velocity=35)
|
||||
line.hit(S, Duration.SIXTEENTH, velocity=28)
|
||||
line.hit(S, Duration.SIXTEENTH, velocity=30)
|
||||
line.hit(R, Duration.SIXTEENTH, velocity=120)
|
||||
line.hit(S, Duration.SIXTEENTH, velocity=30)
|
||||
line.hit(S, Duration.SIXTEENTH, velocity=28)
|
||||
line.hit(S, Duration.SIXTEENTH, velocity=32)
|
||||
render("ensemble", score)
|
||||
|
||||
|
||||
# ── Guitar strum ─────────────────────────────────────────────────────────
|
||||
|
||||
def gen_strum():
|
||||
score = Score("4/4", bpm=100)
|
||||
guitar = score.part("guitar", instrument="acoustic_guitar",
|
||||
fretboard=Fretboard.guitar())
|
||||
for ch in ["G", "D", "Em", "C"] * 2:
|
||||
guitar.strum(ch, duration=Duration.WHOLE, velocity=75)
|
||||
render("strum", score)
|
||||
|
||||
|
||||
# ── Swell ────────────────────────────────────────────────────────────────
|
||||
|
||||
def gen_swell():
|
||||
score = Score("4/4", bpm=80)
|
||||
strings = score.part("strings", instrument="string_ensemble",
|
||||
reverb=0.4, ensemble=12)
|
||||
strings.swell(["C4", "D4", "E4", "F4", "G4", "F4", "E4", "D4",
|
||||
"C4", "D4", "E4", "F4", "G4", "A4", "G4", "E4"],
|
||||
Duration.QUARTER, low_vel=30, peak_vel=100)
|
||||
render("swell", score)
|
||||
|
||||
|
||||
# ── Generate all ─────────────────────────────────────────────────────────
|
||||
|
||||
# ── Cookbook: Acid House ───────────────────────────────────────────────────
|
||||
|
||||
def gen_acid_house():
|
||||
score = Score("4/4", bpm=132)
|
||||
score.drums("house", repeats=8, fill="house", fill_every=8)
|
||||
pad = score.part("pad", synth="supersaw", envelope="pad",
|
||||
reverb=0.4, chorus=0.3, sidechain=0.85)
|
||||
acid = score.part("acid", synth="saw", envelope="pad",
|
||||
legato=True, glide=0.03, distortion=0.8,
|
||||
distortion_drive=8.0, lowpass=1000, lowpass_q=5.0)
|
||||
acid.lfo("lowpass", rate=0.5, min=600, max=2500, bars=8)
|
||||
for sym in ["Cm", "Fm", "Abm", "Gm"]:
|
||||
pad.add(Chord.from_symbol(sym), Duration.WHOLE)
|
||||
pad.add(Chord.from_symbol(sym), Duration.WHOLE)
|
||||
acid.arpeggio(sym, bars=2, pattern="up", octaves=2)
|
||||
render("acid_house", score)
|
||||
|
||||
|
||||
# ── Cookbook: Dub Reggae ──────────────────────────────────────────────────
|
||||
|
||||
def gen_dub_reggae():
|
||||
score = Score("4/4", bpm=72)
|
||||
score.drums("dub", repeats=8)
|
||||
melodica = score.part("melodica", synth="triangle", envelope="pluck",
|
||||
delay=0.5, delay_time=0.66, delay_feedback=0.55,
|
||||
reverb=0.4, reverb_type="cathedral")
|
||||
bass = score.part("bass", synth="sine", lowpass=400, lowpass_q=1.5)
|
||||
melodica.add("A4", 2).rest(6)
|
||||
melodica.add("E5", 1.5).rest(6.5)
|
||||
melodica.add("D5", 1).add("C5", 1).add("A4", 2).rest(4)
|
||||
for _ in range(16):
|
||||
bass.add("A1", Duration.HALF)
|
||||
render("dub_reggae", score)
|
||||
|
||||
|
||||
# ── Cookbook: Jazz Ballad ─────────────────────────────────────────────────
|
||||
|
||||
def gen_jazz_ballad():
|
||||
score = Score("4/4", bpm=72, swing=0.5)
|
||||
score.drums("jazz", repeats=8)
|
||||
rhodes = score.part("rhodes", synth="fm", envelope="piano",
|
||||
reverb=0.4, reverb_type="plate", humanize=0.3)
|
||||
lead = score.part("lead", synth="triangle", envelope="strings",
|
||||
delay=0.25, reverb=0.3, humanize=0.35)
|
||||
key = Key("Bb", "major")
|
||||
for chord in key.progression("I", "vi", "ii", "V") * 2:
|
||||
rhodes.add(chord, Duration.WHOLE)
|
||||
for n, d in [("D5", 1.5), ("F5", 0.5), ("Bb5", 2), (None, 4),
|
||||
("A5", 1), ("G5", 1), ("F5", 2), (None, 4)]:
|
||||
lead.rest(d) if n is None else lead.add(n, d)
|
||||
render("jazz_ballad", score)
|
||||
|
||||
|
||||
# ── Quickstart example ───────────────────────────────────────────────────
|
||||
|
||||
def gen_arpeggio():
|
||||
score = Score("4/4", bpm=132)
|
||||
score.drums("house", repeats=8)
|
||||
score.set_drum_effects(volume=0.3)
|
||||
lead = score.part("lead", synth="saw", envelope="pluck", volume=0.5,
|
||||
lowpass=5000, detune=8, chorus=0.15, reverb=0.25,
|
||||
delay=0.2, delay_time=0.33, delay_feedback=0.3)
|
||||
for sym in ["Cm", "Fm", "Abm", "Gm"]:
|
||||
lead.arpeggio(sym, bars=2, pattern="updown", octaves=2)
|
||||
render("arpeggio", score)
|
||||
|
||||
|
||||
def gen_legato_glide():
|
||||
score = Score("4/4", bpm=132)
|
||||
score.drums("house", repeats=4)
|
||||
score.set_drum_effects(volume=0.35)
|
||||
acid = score.part("acid", synth="saw", volume=0.6,
|
||||
legato=True, glide=0.04,
|
||||
lowpass=3000, lowpass_q=6.0,
|
||||
distortion=0.3, distortion_drive=3.0)
|
||||
acid.lfo("lowpass", rate=0.5, min=800, max=4000, bars=4)
|
||||
for _ in range(4):
|
||||
acid.add("C2", 0.25).add("C3", 0.25).add("G2", 0.25).add("C2", 0.25)
|
||||
acid.add("C2", 0.25).add("Eb2", 0.25).add("G2", 0.25).add("Bb2", 0.25)
|
||||
render("legato_glide", score)
|
||||
|
||||
|
||||
def gen_quickstart():
|
||||
score = Score("4/4", bpm=140)
|
||||
score.drums("bossa nova", repeats=4)
|
||||
chords = score.part("chords", synth="sine", envelope="pad",
|
||||
reverb=0.4, volume=0.3)
|
||||
lead = score.part("lead", synth="saw", envelope="pluck",
|
||||
lowpass=2000, lowpass_q=3.0, distortion=0.8,
|
||||
legato=True, glide=0.03, volume=0.4)
|
||||
bass = score.part("bass", synth="sine", lowpass=500)
|
||||
key = Key("A", "minor")
|
||||
for chord in key.progression("i", "iv", "V", "i"):
|
||||
chords.add(chord, Duration.WHOLE)
|
||||
chords.add(chord, Duration.WHOLE)
|
||||
lead.arpeggio("Am", bars=2, pattern="updown", octaves=2)
|
||||
lead.arpeggio("Dm", bars=2, pattern="updown", octaves=2)
|
||||
lead.set(lowpass=5000, reverb=0.3)
|
||||
lead.arpeggio("E7", bars=2, pattern="up", octaves=2)
|
||||
lead.arpeggio("Am", bars=2, pattern="updown", octaves=2)
|
||||
for n in ["A2", "E2", "A2", "C3"] * 4:
|
||||
bass.add(n, Duration.QUARTER)
|
||||
render("quickstart", score)
|
||||
|
||||
|
||||
# ── Sequencing complete example (bossa nova) ─────────────────────────────
|
||||
|
||||
def gen_chords_basic():
|
||||
score = Score("4/4", bpm=120)
|
||||
key = Key("C", "major")
|
||||
chords = key.progression("I", "V", "vi", "IV")
|
||||
for chord in chords:
|
||||
score.add(chord, Duration.WHOLE)
|
||||
render("chords_basic", score)
|
||||
|
||||
|
||||
def gen_complete_rock():
|
||||
score = Score("4/4", bpm=120)
|
||||
score.drums("rock", repeats=8, fill="rock", fill_every=4)
|
||||
piano = score.part("piano", instrument="piano", volume=0.4, reverb=0.3)
|
||||
lead = score.part("lead", synth="saw", envelope="pluck", volume=0.4,
|
||||
delay=0.2, delay_time=0.33, reverb=0.2, lowpass=3000)
|
||||
bass = score.part("bass", synth="triangle", envelope="pluck", volume=0.45,
|
||||
lowpass=600)
|
||||
for chord in Key("G", "major").progression("I", "V", "vi", "IV") * 2:
|
||||
piano.add(chord, Duration.WHOLE)
|
||||
lead.add("D5", 1).add("B4", 0.5).add("D5", 0.5)
|
||||
lead.add("G5", 1).add("E5", 1)
|
||||
lead.add("D5", 0.5).add("B4", 0.5).add("A4", 1)
|
||||
lead.add("G4", 2).rest(2)
|
||||
lead.add("D5", 1).add("B4", 0.5).add("D5", 0.5)
|
||||
lead.add("G5", 1).add("A5", 1)
|
||||
lead.add("G5", 0.5).add("E5", 0.5).add("D5", 1)
|
||||
lead.add("B4", 2).rest(2)
|
||||
for n in ["G2", "G2", "D2", "D2", "E2", "E2", "C2", "C2"] * 2:
|
||||
bass.add(n, Duration.HALF)
|
||||
render("complete_rock", score)
|
||||
|
||||
|
||||
# ── Drums layering (salsa) ───────────────────────────────────────────────
|
||||
|
||||
def gen_salsa_layered():
|
||||
score = Score("4/4", bpm=180)
|
||||
score.drums("salsa", repeats=4, fill="salsa", fill_every=4)
|
||||
pads = score.part("pads", synth="sine", envelope="pad", volume=0.3)
|
||||
lead = score.part("lead", synth="saw", envelope="pluck", volume=0.4)
|
||||
bass = score.part("bass", synth="sine", envelope="pluck", volume=0.45)
|
||||
for chord in Key("D", "minor").progression("ii", "V", "i", "i") * 2:
|
||||
pads.add(chord, Duration.WHOLE)
|
||||
lead.add("A5", 0.67).add("G5", 0.33).add("F5", 0.67).add("E5", 0.33)
|
||||
for n in ["D2", "A2", "D2", "F2"] * 2:
|
||||
bass.add(n, Duration.QUARTER)
|
||||
render("salsa_layered", score)
|
||||
|
||||
|
||||
# ── Playback basic ───────────────────────────────────────────────────────
|
||||
|
||||
def gen_playback_basic():
|
||||
score = Score("4/4", bpm=140)
|
||||
score.drums("bossa nova", repeats=4)
|
||||
chords = score.part("chords", synth="sine", envelope="pad")
|
||||
for sym in ["Am", "Dm", "E7", "Am"]:
|
||||
chords.add(Chord.from_symbol(sym), Duration.WHOLE)
|
||||
render("playback_basic", score)
|
||||
|
||||
|
||||
# ── Cookbook: song with sections ──────────────────────────────────────────
|
||||
|
||||
def gen_song_sections():
|
||||
score = Score("4/4", bpm=120)
|
||||
score.drums("rock", repeats=16, fill="rock", fill_every=4)
|
||||
chords = score.part("chords", synth="saw", envelope="pad")
|
||||
lead = score.part("lead", synth="triangle", envelope="pluck")
|
||||
score.section("verse")
|
||||
for sym in ["Am", "F", "C", "G"]:
|
||||
chords.add(Chord.from_symbol(sym), Duration.WHOLE)
|
||||
lead.add("A4", 1).add("C5", 1).add("E5", 1).rest(1)
|
||||
lead.add("F5", 1).add("E5", 1).add("C5", 2)
|
||||
score.section("chorus")
|
||||
lead.set(reverb=0.4, lowpass=5000)
|
||||
for sym in ["F", "G", "Am", "C"]:
|
||||
chords.add(Chord.from_symbol(sym), Duration.WHOLE)
|
||||
lead.add("C6", 2).add("A5", 1).add("G5", 1)
|
||||
lead.add("F5", 2).add("E5", 2)
|
||||
score.end_section()
|
||||
score.repeat("verse")
|
||||
score.repeat("chorus", times=2)
|
||||
render("song_sections", score)
|
||||
|
||||
|
||||
GENERATORS = [
|
||||
gen_piano_hold,
|
||||
gen_articulations,
|
||||
gen_dynamics,
|
||||
gen_filter_ramp,
|
||||
gen_rock_beat,
|
||||
gen_bossa_nova_pattern,
|
||||
gen_salsa_pattern,
|
||||
gen_afrobeat_pattern,
|
||||
gen_bossa_nova,
|
||||
gen_djembe,
|
||||
gen_tabla,
|
||||
gen_metal_blast,
|
||||
gen_cajon,
|
||||
gen_tabla_teental,
|
||||
gen_tabla_keherwa,
|
||||
gen_tabla_chakradar,
|
||||
gen_dhol,
|
||||
gen_dholak,
|
||||
gen_mridangam,
|
||||
gen_march_snare,
|
||||
gen_ensemble,
|
||||
gen_strum,
|
||||
gen_swell,
|
||||
gen_arpeggio,
|
||||
gen_legato_glide,
|
||||
gen_acid_house,
|
||||
gen_dub_reggae,
|
||||
gen_jazz_ballad,
|
||||
gen_quickstart,
|
||||
gen_chords_basic,
|
||||
gen_complete_rock,
|
||||
gen_salsa_layered,
|
||||
gen_playback_basic,
|
||||
gen_song_sections,
|
||||
]
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
print("Generating audio samples for docs...")
|
||||
print()
|
||||
for gen in GENERATORS:
|
||||
gen()
|
||||
print()
|
||||
print(f"Done. {len(GENERATORS)} files in {AUDIO_DIR}")
|
||||
@@ -411,6 +411,10 @@ Acid House Track
|
||||
|
||||
play_score(score)
|
||||
|
||||
.. raw:: html
|
||||
|
||||
<audio controls style="width:100%;margin:0.5em 0 1.5em"><source src="../_static/audio/acid_house.wav" type="audio/wav"></audio>
|
||||
|
||||
Dub Reggae with Delay Madness
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
@@ -443,6 +447,10 @@ Sparse notes into infinite echo:
|
||||
|
||||
play_score(score)
|
||||
|
||||
.. raw:: html
|
||||
|
||||
<audio controls style="width:100%;margin:0.5em 0 1.5em"><source src="../_static/audio/dub_reggae.wav" type="audio/wav"></audio>
|
||||
|
||||
Jazz Ballad with Humanize
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
@@ -480,6 +488,10 @@ The difference between a robot and a musician:
|
||||
|
||||
play_score(score)
|
||||
|
||||
.. raw:: html
|
||||
|
||||
<audio controls style="width:100%;margin:0.5em 0 1.5em"><source src="../_static/audio/jazz_ballad.wav" type="audio/wav"></audio>
|
||||
|
||||
Song with Sections
|
||||
~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
@@ -513,6 +525,10 @@ Define once, arrange freely:
|
||||
play_score(score)
|
||||
score.save_midi("my_song.mid")
|
||||
|
||||
.. raw:: html
|
||||
|
||||
<audio controls style="width:100%;margin:0.5em 0 1.5em"><source src="../_static/audio/song_sections.wav" type="audio/wav"></audio>
|
||||
|
||||
Export Everything to MIDI
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
|
||||
+103
-2
@@ -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 -- 51 synthesized percussion
|
||||
sounds, 85+ pattern presets across dozens of genres, and 30 fill presets.
|
||||
sounds, 95+ pattern presets across dozens of genres, and 30 fill presets.
|
||||
Every sound is generated from waveforms; no samples needed.
|
||||
|
||||
Drum Sounds
|
||||
@@ -121,10 +121,18 @@ MRIDANGAM_THA (101)
|
||||
|
||||
**Djembe:** DJEMBE_BASS (102), DJEMBE_TONE (103), DJEMBE_SLAP (104)
|
||||
|
||||
**Cajón:** CAJON_SLAP (109), CAJON_TAP (110)
|
||||
**Cajón:** CAJON_BASS (108), CAJON_SLAP (109), CAJON_TAP (110)
|
||||
|
||||
**Metal Kit:** METAL_KICK (105), METAL_SNARE (106), METAL_HAT (107)
|
||||
|
||||
**Marching Snare:** MARCH_SNARE (115), MARCH_RIMSHOT (116), MARCH_CLICK (118)
|
||||
|
||||
**Quads (Tenors):** QUAD_1 (119), QUAD_2 (120), QUAD_3 (121), QUAD_4 (122),
|
||||
QUAD_SPOCK (123)
|
||||
|
||||
**Marching Bass:** BASS_1 (124), BASS_2 (125), BASS_3 (126), BASS_4 (127),
|
||||
BASS_5 (80)
|
||||
|
||||
Drum Synthesis
|
||||
--------------
|
||||
|
||||
@@ -241,6 +249,13 @@ Playing Patterns
|
||||
play_pattern(Pattern.preset("salsa"), repeats=4, bpm=180)
|
||||
play_pattern(Pattern.preset("afrobeat"), repeats=8, bpm=110)
|
||||
|
||||
.. raw:: html
|
||||
|
||||
<audio controls style="width:100%;margin:0.3em 0 0.5em"><source src="../_static/audio/rock_beat.wav" type="audio/wav"></audio>
|
||||
<audio controls style="width:100%;margin:0.3em 0 0.5em"><source src="../_static/audio/bossa_nova_pattern.wav" type="audio/wav"></audio>
|
||||
<audio controls style="width:100%;margin:0.3em 0 0.5em"><source src="../_static/audio/salsa_pattern.wav" type="audio/wav"></audio>
|
||||
<audio controls style="width:100%;margin:0.3em 0 1.5em"><source src="../_static/audio/afrobeat_pattern.wav" type="audio/wav"></audio>
|
||||
|
||||
Fills
|
||||
-----
|
||||
|
||||
@@ -331,6 +346,10 @@ drum pattern and all named parts are mixed together by ``play_score()``:
|
||||
|
||||
play_score(score)
|
||||
|
||||
.. raw:: html
|
||||
|
||||
<audio controls style="width:100%;margin:0.5em 0 1.5em"><source src="../_static/audio/salsa_layered.wav" type="audio/wav"></audio>
|
||||
|
||||
World Percussion
|
||||
----------------
|
||||
|
||||
@@ -374,6 +393,12 @@ bayan (deep bass bends showcase), tabla call (dayan/bayan call-and-response).
|
||||
score = Score("4/4", bpm=80)
|
||||
score.drums("teental", repeats=4)
|
||||
|
||||
.. raw:: html
|
||||
|
||||
<audio controls style="width:100%;margin:0.3em 0 0.5em"><source src="../_static/audio/tabla_teental.wav" type="audio/wav"></audio>
|
||||
<audio controls style="width:100%;margin:0.3em 0 0.5em"><source src="../_static/audio/tabla_keherwa.wav" type="audio/wav"></audio>
|
||||
<audio controls style="width:100%;margin:0.3em 0 1.5em"><source src="../_static/audio/tabla_chakradar.wav" type="audio/wav"></audio>
|
||||
|
||||
Dhol
|
||||
~~~~
|
||||
|
||||
@@ -391,6 +416,10 @@ energetic, and physically impossible to sit still to.
|
||||
score = Score("4/4", bpm=160)
|
||||
score.drums("bhangra", repeats=4)
|
||||
|
||||
.. raw:: html
|
||||
|
||||
<audio controls style="width:100%;margin:0.5em 0 1.5em"><source src="../_static/audio/dhol.wav" type="audio/wav"></audio>
|
||||
|
||||
Dholak
|
||||
~~~~~~
|
||||
|
||||
@@ -408,6 +437,10 @@ music) and dholak folk (a general folk groove).
|
||||
score = Score("4/4", bpm=120)
|
||||
score.drums("qawwali", repeats=4)
|
||||
|
||||
.. raw:: html
|
||||
|
||||
<audio controls style="width:100%;margin:0.5em 0 1.5em"><source src="../_static/audio/dholak.wav" type="audio/wav"></audio>
|
||||
|
||||
Mridangam
|
||||
~~~~~~~~~
|
||||
|
||||
@@ -426,6 +459,10 @@ and mridangam korvai (a rhythmic cadence pattern).
|
||||
score = Score("4/4", bpm=90)
|
||||
score.drums("adi talam", repeats=4)
|
||||
|
||||
.. raw:: html
|
||||
|
||||
<audio controls style="width:100%;margin:0.5em 0 1.5em"><source src="../_static/audio/mridangam.wav" type="audio/wav"></audio>
|
||||
|
||||
Djembe
|
||||
~~~~~~
|
||||
|
||||
@@ -450,6 +487,10 @@ West African-style break).
|
||||
score = Score("4/4", bpm=120)
|
||||
score.drums("djembe", repeats=8, fill="djembe call", fill_every=4)
|
||||
|
||||
.. raw:: html
|
||||
|
||||
<audio controls style="width:100%;margin:0.5em 0 1.5em"><source src="../_static/audio/djembe.wav" type="audio/wav"></audio>
|
||||
|
||||
Metal Kit
|
||||
~~~~~~~~~
|
||||
|
||||
@@ -474,6 +515,10 @@ roll → kick roll → alternating → crash ending).
|
||||
score = Score("4/4", bpm=200)
|
||||
score.drums("metal blast", repeats=8, fill="metal cascade", fill_every=4)
|
||||
|
||||
.. raw:: html
|
||||
|
||||
<audio controls style="width:100%;margin:0.5em 0 1.5em"><source src="../_static/audio/metal_blast.wav" type="audio/wav"></audio>
|
||||
|
||||
Cajón
|
||||
~~~~~
|
||||
|
||||
@@ -496,6 +541,62 @@ bass-slap groove).
|
||||
score = Score("4/4", bpm=100)
|
||||
score.drums("cajon", repeats=8, fill="cajon flam", fill_every=4)
|
||||
|
||||
.. raw:: html
|
||||
|
||||
<audio controls style="width:100%;margin:0.5em 0 1.5em"><source src="../_static/audio/cajon.wav" type="audio/wav"></audio>
|
||||
|
||||
Marching Percussion
|
||||
~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
A full drumline — snare, quads (tenors), and pitched bass drums.
|
||||
Every sound is synthesized: kevlar snare heads, aluminum shell ting
|
||||
on the quads, felt-beater thwack on the basses.
|
||||
|
||||
**Snare** -- 3 sounds: MARCH_SNARE (tight kevlar tap), MARCH_RIMSHOT
|
||||
(woody-metallic crack), MARCH_CLICK (stick click for count-offs).
|
||||
|
||||
**Quads** -- 5 sounds: QUAD_1 through QUAD_4 (high to low pitched
|
||||
tenors) plus QUAD_SPOCK (rim click on the shell).
|
||||
|
||||
**Bass drums** -- 5 pitched drums: BASS_1 (highest/smallest) through
|
||||
BASS_5 (lowest/biggest), each with a prominent felt-beater thwack.
|
||||
|
||||
**6 patterns:** march (basic 4/4), cadence (8-beat street beat),
|
||||
march paradiddle, march roll (buzz crescendo), quad sweep (run across
|
||||
all 4 drums), quad groove, bass split (cascading across the line),
|
||||
bass unison (all 5 hit together), drumline (snare + quads + bass).
|
||||
|
||||
**Rudiment methods:** ``Part.flam()``, ``Part.diddle()``, and
|
||||
``Part.cheese()`` for marching rudiments on any drum sound.
|
||||
|
||||
**Ensemble rendering:** ``ensemble=N`` on any Part duplicates the
|
||||
voice with per-player timing tendencies and micro pitch drift.
|
||||
``ensemble=8`` for a snare line, ``ensemble=20`` for a massive section.
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
# Full drumline with ensemble
|
||||
snares = score.part("snares", synth="sine", volume=0.9,
|
||||
reverb=0.2, ensemble=8)
|
||||
quads = score.part("quads", synth="sine", volume=0.5,
|
||||
reverb=0.2, ensemble=4)
|
||||
basses = score.part("basses", synth="sine", volume=0.55,
|
||||
reverb=0.2, ensemble=5)
|
||||
|
||||
snares.flam(DrumSound.MARCH_SNARE, Duration.QUARTER, velocity=120)
|
||||
snares.diddle(DrumSound.MARCH_SNARE, Duration.EIGHTH, velocity=60)
|
||||
|
||||
# Or use patterns
|
||||
score.drums("drumline", repeats=4)
|
||||
|
||||
.. raw:: html
|
||||
|
||||
<audio controls style="width:100%;margin:0.5em 0 1.5em"><source src="../_static/audio/march_snare.wav" type="audio/wav"></audio>
|
||||
|
||||
**Sympathetic resonance:** The marching snare builds up snare wire
|
||||
buzz as hits accumulate, and the buzz decays during rests — just like
|
||||
a real drum.
|
||||
|
||||
MIDI Export
|
||||
-----------
|
||||
|
||||
|
||||
@@ -66,6 +66,10 @@ the mix louder and punchier:
|
||||
chords.add(Chord.from_symbol(sym), Duration.WHOLE)
|
||||
play_score(score)
|
||||
|
||||
.. raw:: html
|
||||
|
||||
<audio controls style="width:100%;margin:0.5em 0 1.5em"><source src="../_static/audio/playback_basic.wav" type="audio/wav"></audio>
|
||||
|
||||
The render pipeline respects the Score's ``temperament`` and
|
||||
``reference_pitch`` settings, so Baroque or microtonal scores play back
|
||||
at the correct tuning:
|
||||
|
||||
@@ -185,6 +185,10 @@ chords, melody, bass, each with their own synth and effects:
|
||||
|
||||
play_score(score)
|
||||
|
||||
.. raw:: html
|
||||
|
||||
<audio controls style="width:100%;margin:0.5em 0 1.5em"><source src="../_static/audio/quickstart.wav" type="audio/wav"></audio>
|
||||
|
||||
Export to Your DAW
|
||||
------------------
|
||||
|
||||
|
||||
+279
-41
@@ -47,6 +47,18 @@ A ``Duration`` represents a note length in beats (quarter note = 1 beat):
|
||||
>>> Duration.TRIPLET_QUARTER.value
|
||||
0.6666666666666666
|
||||
|
||||
Duration supports arithmetic — multiply, divide, and add to create
|
||||
compound durations:
|
||||
|
||||
.. code-block:: pycon
|
||||
|
||||
>>> Duration.WHOLE * 2
|
||||
8.0
|
||||
>>> Duration.HALF + Duration.QUARTER
|
||||
3.0
|
||||
>>> Duration.WHOLE / 2
|
||||
2.0
|
||||
|
||||
Time Signatures
|
||||
---------------
|
||||
|
||||
@@ -149,6 +161,10 @@ Chords work just like tones — pass any ``Chord`` object:
|
||||
for chord in chords:
|
||||
score.add(chord, Duration.WHOLE)
|
||||
|
||||
.. raw:: html
|
||||
|
||||
<audio controls style="width:100%;margin:0.5em 0 1.5em"><source src="../_static/audio/chords_basic.wav" type="audio/wav"></audio>
|
||||
|
||||
.. code-block:: pycon
|
||||
|
||||
>>> score.measures
|
||||
@@ -232,6 +248,31 @@ Chords and Tone objects work the same way:
|
||||
for note in ["A2", "C3", "E3", "A2", "D2", "F2", "A2", "D2"]:
|
||||
bass.add(note, Duration.QUARTER)
|
||||
|
||||
Polyphonic Hold
|
||||
---------------
|
||||
|
||||
``Part.hold()`` adds a note without advancing the beat position —
|
||||
the next note starts at the *same* time. This enables polyphonic
|
||||
overlap on a single part: piano sustain, sitar drone under melody,
|
||||
guitar strum texture.
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
piano = score.part("piano", instrument="piano", reverb=0.3)
|
||||
|
||||
# Hold a C major chord for 8 beats
|
||||
piano.hold("C3", Duration.WHOLE * 2, velocity=60)
|
||||
piano.hold("E3", Duration.WHOLE * 2, velocity=55)
|
||||
piano.hold("G3", Duration.WHOLE * 2, velocity=55)
|
||||
|
||||
# Melody plays simultaneously on top
|
||||
for n in ["E4", "G4", "C5", "G4", "E4", "D4", "C4", "E4"]:
|
||||
piano.add(n, Duration.QUARTER, velocity=80)
|
||||
|
||||
.. raw:: html
|
||||
|
||||
<audio controls style="width:100%;margin:0.5em 0 1.5em"><source src="../_static/audio/piano_hold.wav" type="audio/wav"></audio>
|
||||
|
||||
Arpeggiator
|
||||
------------
|
||||
|
||||
@@ -280,6 +321,10 @@ Chain arpeggios through a progression:
|
||||
for sym in ["Cm", "Fm", "Abm", "Gm"]:
|
||||
lead.arpeggio(sym, bars=2, pattern="updown", octaves=2)
|
||||
|
||||
.. raw:: html
|
||||
|
||||
<audio controls style="width:100%;margin:0.5em 0 1.5em"><source src="../_static/audio/arpeggio.wav" type="audio/wav"></audio>
|
||||
|
||||
Combined with legato, glide, distortion, and a resonant lowpass, this
|
||||
produces the classic acid/trance arpeggiator sound.
|
||||
|
||||
@@ -310,12 +355,18 @@ portamento (pitch slides between notes):
|
||||
acid = score.part(
|
||||
"acid",
|
||||
synth="saw",
|
||||
envelope="pad",
|
||||
legato=True,
|
||||
glide=0.04,
|
||||
lowpass=3000,
|
||||
lowpass_q=6.0,
|
||||
distortion=0.3,
|
||||
)
|
||||
acid.add("C2", 0.25).add("C3", 0.25).add("G2", 0.25).add("C2", 0.25)
|
||||
|
||||
.. raw:: html
|
||||
|
||||
<audio controls style="width:100%;margin:0.5em 0 1.5em"><source src="../_static/audio/legato_glide.wav" type="audio/wav"></audio>
|
||||
|
||||
- ``legato``: If True, no envelope retrigger between notes (default False).
|
||||
- ``glide``: Portamento time in seconds (default 0, instant).
|
||||
0.03--0.05 = quick 303 slide, 0.1--0.2 = slow glide.
|
||||
@@ -323,63 +374,51 @@ portamento (pitch slides between notes):
|
||||
Complete Example
|
||||
----------------
|
||||
|
||||
A full multi-part arrangement built from scratch — bossa nova with FM
|
||||
rhodes, triangle lead, and filtered bass:
|
||||
A full multi-part arrangement — rock beat with piano chords, saw
|
||||
lead, and filtered bass:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
from pytheory import Score, Pattern, Key, Duration, Chord
|
||||
from pytheory import Score, Key, Duration, Chord
|
||||
from pytheory.play import play_score
|
||||
|
||||
score = Score("4/4", bpm=140)
|
||||
score.drums("bossa nova", repeats=4)
|
||||
score = Score("4/4", bpm=120)
|
||||
score.drums("rock", repeats=8, fill="rock", fill_every=4)
|
||||
|
||||
# FM rhodes with reverb
|
||||
rhodes = score.part(
|
||||
"rhodes",
|
||||
synth="fm",
|
||||
envelope="piano",
|
||||
volume=0.3,
|
||||
reverb=0.4,
|
||||
reverb_decay=1.8,
|
||||
)
|
||||
# Piano chords with reverb
|
||||
piano = score.part("piano", instrument="piano", volume=0.4, reverb=0.3)
|
||||
|
||||
# Triangle lead with delay
|
||||
# Saw lead with delay
|
||||
lead = score.part(
|
||||
"lead",
|
||||
synth="triangle",
|
||||
envelope="pluck",
|
||||
volume=0.45,
|
||||
delay=0.25,
|
||||
delay_time=0.32,
|
||||
delay_feedback=0.35,
|
||||
reverb=0.2,
|
||||
"lead", synth="saw", envelope="pluck", volume=0.4,
|
||||
delay=0.2, delay_time=0.33, reverb=0.2, lowpass=3000,
|
||||
)
|
||||
|
||||
# Filtered bass
|
||||
bass = score.part(
|
||||
"bass",
|
||||
synth="sine",
|
||||
envelope="pluck",
|
||||
volume=0.45,
|
||||
lowpass=600,
|
||||
)
|
||||
bass = score.part("bass", synth="triangle", envelope="pluck",
|
||||
volume=0.45, lowpass=600)
|
||||
|
||||
for sym in ["Am", "Am", "Dm", "Dm", "E7", "E7", "Am", "Am"]:
|
||||
rhodes.add(Chord.from_symbol(sym), Duration.WHOLE)
|
||||
for chord in Key("G", "major").progression("I", "V", "vi", "IV") * 2:
|
||||
piano.add(chord, Duration.WHOLE)
|
||||
|
||||
for n, d in [
|
||||
("E5", 0.67), ("D5", 0.33), ("C5", 0.67), ("B4", 0.33),
|
||||
("A4", 1), ("C5", 0.67), ("E5", 0.33), ("D5", 0.67), ("C5", 0.33),
|
||||
("A4", 1),
|
||||
]:
|
||||
lead.add(n, d)
|
||||
lead.add("D5", 1).add("B4", 0.5).add("D5", 0.5)
|
||||
lead.add("G5", 1).add("E5", 1)
|
||||
lead.add("D5", 0.5).add("B4", 0.5).add("A4", 1)
|
||||
lead.add("G4", 2).rest(2)
|
||||
lead.add("D5", 1).add("B4", 0.5).add("D5", 0.5)
|
||||
lead.add("G5", 1).add("A5", 1)
|
||||
lead.add("G5", 0.5).add("E5", 0.5).add("D5", 1)
|
||||
lead.add("B4", 2).rest(2)
|
||||
|
||||
for n in ["A2", "E2", "A2", "C3", "D2", "A2", "D2", "F2"]:
|
||||
bass.add(n, Duration.QUARTER)
|
||||
for n in ["G2", "G2", "D2", "D2", "E2", "E2", "C2", "C2"] * 2:
|
||||
bass.add(n, Duration.HALF)
|
||||
|
||||
play_score(score)
|
||||
|
||||
.. raw:: html
|
||||
|
||||
<audio controls style="width:100%;margin:0.5em 0 1.5em"><source src="../_static/audio/complete_rock.wav" type="audio/wav"></audio>
|
||||
|
||||
Velocity
|
||||
--------
|
||||
|
||||
@@ -399,6 +438,157 @@ The arpeggiator also accepts velocity:
|
||||
|
||||
lead.arpeggio("Am", bars=2, pattern="up", velocity=80)
|
||||
|
||||
Articulations
|
||||
-------------
|
||||
|
||||
Articulations change *how* a note is played — its attack, duration, and
|
||||
weight. A staccato note is short and bouncy. A marcato note hits hard.
|
||||
A legato note melts into the next one. This is the difference between
|
||||
a melody that sounds like a MIDI file and one that sounds like a
|
||||
musician played it.
|
||||
|
||||
Pass ``articulation=`` to ``Part.add()``:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
piano.add("C4", Duration.QUARTER, articulation="staccato") # short, bouncy
|
||||
piano.add("D4", Duration.QUARTER, articulation="legato") # smooth, overlaps
|
||||
piano.add("E4", Duration.QUARTER, articulation="marcato") # heavy accent
|
||||
piano.add("F4", Duration.QUARTER, articulation="tenuto") # held, soft attack
|
||||
piano.add("G4", Duration.QUARTER, articulation="accent") # louder
|
||||
piano.add("C5", Duration.HALF, articulation="fermata") # held longer
|
||||
|
||||
.. raw:: html
|
||||
|
||||
<audio controls style="width:100%;margin:0.5em 0 1.5em"><source src="../_static/audio/articulations.wav" type="audio/wav"></audio>
|
||||
|
||||
What each articulation does:
|
||||
|
||||
- **staccato** — plays ~40% of the note duration with a quick fade-out. Short and detached.
|
||||
- **legato** — extends ~15% into the next note. Smooth and connected.
|
||||
- **marcato** — 25% velocity boost + sharper attack. Heavy and accented.
|
||||
- **tenuto** — full duration with a softer attack ramp. Held and deliberate.
|
||||
- **accent** — 20% velocity boost, no duration change.
|
||||
- **fermata** — stretches the note 50% longer.
|
||||
|
||||
Articulations work on ``Part.hold()`` and ``Part.hit()`` too.
|
||||
|
||||
Dynamic Curves
|
||||
--------------
|
||||
|
||||
Real music breathes — phrases get louder, get quieter, swell and
|
||||
recede. Dynamic curves let you shape the velocity across a sequence
|
||||
of notes instead of setting each one manually.
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
# Crescendo: quiet to loud
|
||||
piano.crescendo(["C4","D4","E4","F4","G4","A4","B4","C5"],
|
||||
Duration.QUARTER, start_vel=30, end_vel=110)
|
||||
|
||||
# Decrescendo: loud to quiet
|
||||
piano.decrescendo(["C5","B4","A4","G4","F4","E4","D4","C4"],
|
||||
Duration.QUARTER, start_vel=110, end_vel=30)
|
||||
|
||||
# Swell: up then back down (orchestral < > shape)
|
||||
strings.swell(["C4","D4","E4","F4","G4","F4","E4","D4"],
|
||||
Duration.QUARTER, low_vel=35, peak_vel=110)
|
||||
|
||||
# Custom curve: explicit velocity per note
|
||||
piano.dynamics(["C4","E4","G4","C5"], Duration.QUARTER,
|
||||
velocities=[50, 80, 110, 90])
|
||||
|
||||
.. raw:: html
|
||||
|
||||
<audio controls style="width:100%;margin:0.5em 0 1.5em"><source src="../_static/audio/dynamics.wav" type="audio/wav"></audio>
|
||||
|
||||
Four methods:
|
||||
|
||||
- **crescendo()** — linear velocity ramp from ``start_vel`` to ``end_vel``.
|
||||
- **decrescendo()** — same thing, but typically loud to quiet.
|
||||
- **swell()** — ramps up to the midpoint, then back down. The classic
|
||||
orchestral crescendo-decrescendo.
|
||||
- **dynamics()** — the general form. Pass a ``(start, end)`` tuple for
|
||||
a linear ramp, or a list of velocities for a custom curve.
|
||||
|
||||
All four accept ``articulation=`` to combine dynamics with articulations:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
# Staccato crescendo — bouncy notes getting louder
|
||||
piano.crescendo(["C4","E4","G4","C5","E5","G5","C6","E6"],
|
||||
Duration.EIGHTH, start_vel=40, end_vel=110,
|
||||
articulation="staccato")
|
||||
|
||||
Part.hit() — Manual Drum Placement
|
||||
-----------------------------------
|
||||
|
||||
The pattern system is great for grooves, but sometimes you want to
|
||||
place individual drum hits with full control — articulations, effects,
|
||||
and all. ``Part.hit()`` puts a drum sound into a Part's note stream:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
from pytheory import DrumSound
|
||||
|
||||
kit = score.part("kit", synth="sine", volume=0.7)
|
||||
|
||||
kit.hit(DrumSound.KICK, Duration.QUARTER, articulation="accent")
|
||||
kit.hit(DrumSound.CLOSED_HAT, Duration.EIGHTH, velocity=60)
|
||||
kit.hit(DrumSound.SNARE, Duration.EIGHTH, articulation="marcato")
|
||||
|
||||
Because hits go through the normal Part renderer, they get humanize,
|
||||
effects, and articulations for free. Use this for custom beats that
|
||||
don't fit a preset pattern, or for one-shot accent hits layered on
|
||||
top of a pattern.
|
||||
|
||||
Rudiments — Flam, Diddle, Cheese
|
||||
---------------------------------
|
||||
|
||||
Marching percussion rudiments as methods on any Part:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
from pytheory import DrumSound
|
||||
|
||||
p = score.part("snares", synth="sine", volume=0.9)
|
||||
|
||||
# Flam: grace note + main hit (gap controls tightness)
|
||||
p.flam(DrumSound.MARCH_SNARE, Duration.QUARTER, velocity=120)
|
||||
|
||||
# Diddle: two equal strokes in one note duration
|
||||
p.diddle(DrumSound.MARCH_SNARE, Duration.EIGHTH, velocity=60)
|
||||
|
||||
# Cheese: flam + diddle combined
|
||||
p.cheese(DrumSound.MARCH_SNARE, Duration.QUARTER, velocity=120)
|
||||
|
||||
Ensemble
|
||||
--------
|
||||
|
||||
Any Part can be rendered as an ensemble — multiple players with
|
||||
per-player timing tendencies and micro pitch drift:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
# 8-player snare line
|
||||
snares = score.part("snares", synth="sine", volume=0.9, ensemble=8)
|
||||
|
||||
# 20-player string section
|
||||
strings = score.part("strings", instrument="string_ensemble", ensemble=20)
|
||||
|
||||
# Single player (default)
|
||||
solo = score.part("solo", instrument="violin")
|
||||
|
||||
Each ensemble voice gets a consistent timing personality (some rush,
|
||||
some drag) plus small per-note wobble, and slightly different tuning.
|
||||
The result sounds like a real section — together but alive.
|
||||
|
||||
Solo snare, then an 8-player section plays the same pattern:
|
||||
|
||||
.. raw:: html
|
||||
|
||||
<audio controls style="width:100%;margin:0.5em 0 1.5em"><source src="../_static/audio/ensemble.wav" type="audio/wav"></audio>
|
||||
|
||||
Swing and Groove
|
||||
----------------
|
||||
|
||||
@@ -478,6 +668,54 @@ integrate naturally with the rest of the automation system:
|
||||
pad.rest(Duration.WHOLE)
|
||||
pad.rest(Duration.WHOLE)
|
||||
|
||||
Parameter Ramps
|
||||
---------------
|
||||
|
||||
Fades only control volume. ``Part.ramp()`` smoothly sweeps *any*
|
||||
parameter from its current value to a target — filters, reverb,
|
||||
distortion, chorus, delay, anything ``.set()`` accepts. This is how
|
||||
you build filter sweeps, gradual effect sends, and EDM buildups.
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
lead = score.part("lead", synth="saw", lowpass=200, lowpass_q=3.0)
|
||||
|
||||
# Open the filter over 8 bars
|
||||
lead.ramp(over=Duration.WHOLE * 8, lowpass=8000)
|
||||
|
||||
# Ramp multiple params at once
|
||||
pad.ramp(over=Duration.WHOLE * 4, reverb=0.5, chorus=0.3)
|
||||
|
||||
# Close the filter with distortion fading in
|
||||
lead.ramp(over=Duration.WHOLE * 4, lowpass=400, distortion=0.5)
|
||||
|
||||
Four interpolation curves:
|
||||
|
||||
- **linear** — constant rate of change (default).
|
||||
- **ease_in** — starts slow, accelerates. Good for buildups.
|
||||
- **ease_out** — starts fast, decelerates. Good for releases.
|
||||
- **ease_in_out** — slow at both ends. Smooth and natural.
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
# EDM buildup: slow start, accelerating filter sweep
|
||||
lead.ramp(over=Duration.WHOLE * 8, curve="ease_in", lowpass=8000)
|
||||
|
||||
# Smooth reverb wash fading in and settling
|
||||
pad.ramp(over=Duration.WHOLE * 4, curve="ease_in_out", reverb=0.6)
|
||||
|
||||
.. raw:: html
|
||||
|
||||
<audio controls style="width:100%;margin:0.5em 0 1.5em"><source src="../_static/audio/filter_ramp.wav" type="audio/wav"></audio>
|
||||
|
||||
``ramp()`` generates automation points every quarter-beat by default.
|
||||
Set ``resolution=0.125`` for smoother curves (every 32nd note), or
|
||||
``resolution=1.0`` for lighter automation (every beat).
|
||||
|
||||
Combine with ``lfo()`` for cyclic modulation and ``ramp()`` for
|
||||
one-shot sweeps — together they cover the full range of parameter
|
||||
automation.
|
||||
|
||||
Humanize
|
||||
--------
|
||||
|
||||
|
||||
+4
-1
@@ -62,7 +62,10 @@ it through your speakers, export MIDI, finish in your DAW:
|
||||
lead.arpeggio("Am", bars=4, pattern="updown", octaves=2)
|
||||
|
||||
play_score(score)
|
||||
score.save_midi("sketch.mid")
|
||||
|
||||
.. raw:: html
|
||||
|
||||
<audio controls style="width:100%;margin:0.5em 0 1.5em"><source src="_static/audio/quickstart.wav" type="audio/wav"></audio>
|
||||
|
||||
Or hear a randomly generated track from the command line — different
|
||||
every time::
|
||||
|
||||
+688
-10
@@ -543,7 +543,7 @@ def dub_kingston():
|
||||
volume=0.6, pan=0.0, lowpass=400, lowpass_q=1.5,
|
||||
humanize=0.2)
|
||||
siren = score.part("siren", synth="pwm_slow", envelope="pad",
|
||||
volume=0.15, pan=0.5,
|
||||
volume=0.15, pan=0.5, ensemble=4,
|
||||
reverb=0.7, reverb_decay=3.0, reverb_type="cathedral",
|
||||
lowpass=1200, detune=10)
|
||||
|
||||
@@ -1124,14 +1124,14 @@ def cinematic_showcase():
|
||||
bells.rest(Duration.WHOLE)
|
||||
|
||||
# String ensemble — lush wide pad
|
||||
strings = score.part("strings", instrument="string_ensemble",
|
||||
strings = score.part("strings", instrument="string_ensemble", ensemble=8,
|
||||
reverb=0.4, reverb_type="hall")
|
||||
strings.rest(Duration.WHOLE)
|
||||
for sym in ["Am", "F", "C", "G", "Dm", "Am", "E"]:
|
||||
strings.add(Chord.from_symbol(sym), Duration.WHOLE)
|
||||
|
||||
# Cello — deep foundation
|
||||
cello = score.part("cello", instrument="cello",
|
||||
cello = score.part("cello", instrument="cello", ensemble=3,
|
||||
reverb=0.3, reverb_type="hall")
|
||||
cello.rest(Duration.WHOLE)
|
||||
for n in ["A2", "F2", "C3", "G2", "D3", "A2", "E2"]:
|
||||
@@ -1629,7 +1629,7 @@ def epic_bhairav():
|
||||
timp.add(Tone("Pa", octave=2, system=shruti), Duration.HALF, velocity=115)
|
||||
|
||||
# Choir — bar 3
|
||||
choir = score.part("choir", synth="vocal_synth", envelope="pad",
|
||||
choir = score.part("choir", synth="vocal_synth", envelope="pad", ensemble=6,
|
||||
detune=8, spread=0.4, reverb=0.4, reverb_type=REV, volume=0.2)
|
||||
for _ in range(2):
|
||||
choir.rest(Duration.WHOLE)
|
||||
@@ -1651,7 +1651,7 @@ def epic_bhairav():
|
||||
bansuri.add(tone, dur, velocity=vel)
|
||||
|
||||
# Cello — bar 3
|
||||
cello = score.part("cello", instrument="cello", volume=0.22, reverb=0.4, reverb_type=REV)
|
||||
cello = score.part("cello", instrument="cello", volume=0.22, reverb=0.4, reverb_type=REV, ensemble=3)
|
||||
for _ in range(2):
|
||||
cello.rest(Duration.WHOLE)
|
||||
for name, dur, vel in [
|
||||
@@ -1677,7 +1677,7 @@ def epic_bhairav():
|
||||
|
||||
# Strings — bar 13
|
||||
strings = score.part("strings", instrument="string_ensemble", volume=0.18,
|
||||
reverb=0.4, reverb_type=REV)
|
||||
reverb=0.4, reverb_type=REV, ensemble=10)
|
||||
for _ in range(12):
|
||||
strings.rest(Duration.WHOLE)
|
||||
for name, dur, vel in [("Sa", 4.0, 58), ("Ma", 4.0, 62), ("Pa", 4.0, 68), ("Sa", 4.0, 72)]:
|
||||
@@ -1887,7 +1887,7 @@ def ascent():
|
||||
|
||||
# 3: SURFACING (5-8)
|
||||
cello = score.part("cello", instrument="cello", volume=0.22,
|
||||
reverb=0.4, reverb_type=REV)
|
||||
reverb=0.4, reverb_type=REV, ensemble=3)
|
||||
cello.rest(16.0)
|
||||
for note, dur, vel in [("E2",4.0,52),("G2",4.0,55),("B2",4.0,58),("E3",4.0,62)]:
|
||||
cello.add(note, dur, velocity=vel)
|
||||
@@ -1943,7 +1943,7 @@ def ascent():
|
||||
theremin.add(note, dur, velocity=vel)
|
||||
|
||||
strings = score.part("strings", instrument="string_ensemble", volume=0.15,
|
||||
reverb=0.45, reverb_type=REV)
|
||||
reverb=0.45, reverb_type=REV, ensemble=8)
|
||||
strings.rest(40.0)
|
||||
for sym, vel in [("Em",52),("C",55),("Am",58),("B",55),("Em",60),("C",62)]:
|
||||
strings.add(Chord.from_symbol(sym), 4.0, velocity=vel)
|
||||
@@ -2218,7 +2218,7 @@ def pop_rock():
|
||||
cabinet=1.0, cabinet_brightness=0.6,
|
||||
reverb=0.2, reverb_type="plate", pan=0.2)
|
||||
strings = score.part("strings", instrument="string_ensemble", volume=0.12,
|
||||
reverb=0.35, reverb_type="hall")
|
||||
reverb=0.35, reverb_type="hall", ensemble=6)
|
||||
|
||||
prog = ["G", "D", "Em", "C"]
|
||||
|
||||
@@ -2324,6 +2324,681 @@ def sitar_drone():
|
||||
play_song(score, "Sitar Drone — Raga Bhairav (22-Shruti JI, hold() polyphony)")
|
||||
|
||||
|
||||
def acid_tabla():
|
||||
"""Acid Tabla — 303 filter automation meets Indian percussion."""
|
||||
score = Score("4/4", bpm=132)
|
||||
|
||||
# ── House drums ──
|
||||
score.drums("house", repeats=20, fill="house", fill_every=8)
|
||||
score.set_drum_effects(volume=0.45)
|
||||
|
||||
# ── 303 acid bass ──
|
||||
acid = score.part("acid", synth="saw", volume=0.75,
|
||||
legato=True, glide=0.035,
|
||||
distortion=0.35, distortion_drive=4.5,
|
||||
saturation=0.15, humanize=0.05)
|
||||
|
||||
# Intro (4 bars): filter closed, high resonance
|
||||
acid.set(lowpass=600, lowpass_q=12.0)
|
||||
for _ in range(4):
|
||||
for n in ["C3","C3","C2","C3","Eb3","C2","G2","C3"]:
|
||||
acid.add(n, Duration.EIGHTH)
|
||||
|
||||
# Build (4 bars): filter opens
|
||||
acid.ramp(over=Duration.WHOLE * 4, curve="ease_in", lowpass=4500)
|
||||
for _ in range(4):
|
||||
for n in ["C2","G2","C3","Eb3","C2","Bb2","G2","C3"]:
|
||||
acid.add(n, Duration.EIGHTH)
|
||||
|
||||
# Peak (4 bars): wide open, wilder pattern
|
||||
acid.set(lowpass=7000, lowpass_q=7.0)
|
||||
for _ in range(2):
|
||||
for n in ["C2","C3","Eb3","G3","C2","Bb2","G2","Eb3"]:
|
||||
acid.add(n, Duration.EIGHTH)
|
||||
for _ in range(2):
|
||||
for n in ["C2","Eb3","C3","G3","Bb2","C3","G2","C2"]:
|
||||
acid.add(n, Duration.EIGHTH)
|
||||
|
||||
# Tabla section (4 bars): filter pulls back
|
||||
acid.set(lowpass=3000, lowpass_q=5.0)
|
||||
for _ in range(4):
|
||||
for n in ["C2","G2","C3","C2","Eb2","G2","Bb2","C2"]:
|
||||
acid.add(n, Duration.EIGHTH)
|
||||
|
||||
# Outro (4 bars): filter closes
|
||||
acid.ramp(over=Duration.WHOLE * 4, curve="ease_out", lowpass=400, lowpass_q=15.0)
|
||||
for _ in range(4):
|
||||
for n in ["C3","G2","C2","C3","C2","G2","Eb2","C2"]:
|
||||
acid.add(n, Duration.EIGHTH)
|
||||
|
||||
# ── Tabla: enters bar 9, rides through to the end ──
|
||||
tabla = score.part("tabla", synth="sine", volume=0.55, reverb=0.15)
|
||||
|
||||
# 8 bars rest
|
||||
for _ in range(64):
|
||||
tabla.rest(Duration.EIGHTH)
|
||||
|
||||
# Bars 9-12: keherwa groove
|
||||
for _ in range(4):
|
||||
tabla.hit(DrumSound.TABLA_DHA, Duration.EIGHTH, velocity=100, articulation="accent")
|
||||
tabla.hit(DrumSound.TABLA_TIT, Duration.EIGHTH, velocity=55)
|
||||
tabla.hit(DrumSound.TABLA_TIT, Duration.EIGHTH, velocity=50)
|
||||
tabla.hit(DrumSound.TABLA_NA, Duration.EIGHTH, velocity=88)
|
||||
tabla.hit(DrumSound.TABLA_TIN, Duration.EIGHTH, velocity=82)
|
||||
tabla.hit(DrumSound.TABLA_TIT, Duration.EIGHTH, velocity=52)
|
||||
tabla.hit(DrumSound.TABLA_DHA, Duration.EIGHTH, velocity=95, articulation="accent")
|
||||
tabla.hit(DrumSound.TABLA_GE, Duration.EIGHTH, velocity=78)
|
||||
|
||||
# Bars 13-14: busier with 16ths
|
||||
for _ in range(2):
|
||||
tabla.hit(DrumSound.TABLA_DHA, Duration.EIGHTH, velocity=105, articulation="marcato")
|
||||
tabla.hit(DrumSound.TABLA_TIT, Duration.SIXTEENTH, velocity=48)
|
||||
tabla.hit(DrumSound.TABLA_TIT, Duration.SIXTEENTH, velocity=52)
|
||||
tabla.hit(DrumSound.TABLA_NA, Duration.EIGHTH, velocity=90)
|
||||
tabla.hit(DrumSound.TABLA_TIN, Duration.EIGHTH, velocity=85)
|
||||
tabla.hit(DrumSound.TABLA_TIT, Duration.SIXTEENTH, velocity=48)
|
||||
tabla.hit(DrumSound.TABLA_NA, Duration.SIXTEENTH, velocity=58)
|
||||
tabla.hit(DrumSound.TABLA_DHA, Duration.EIGHTH, velocity=100, articulation="accent")
|
||||
tabla.hit(DrumSound.TABLA_GE_BEND, Duration.EIGHTH, velocity=88)
|
||||
|
||||
# Bars 15-16: tihai crescendo ending
|
||||
for vel in [85, 90, 95]:
|
||||
tabla.hit(DrumSound.TABLA_DHA, Duration.EIGHTH, velocity=vel, articulation="accent")
|
||||
tabla.hit(DrumSound.TABLA_TIT, Duration.SIXTEENTH, velocity=int(vel * 0.6))
|
||||
tabla.hit(DrumSound.TABLA_NA, Duration.SIXTEENTH, velocity=int(vel * 0.75))
|
||||
for vel in [100, 105, 110]:
|
||||
tabla.hit(DrumSound.TABLA_DHA, Duration.EIGHTH, velocity=vel, articulation="marcato")
|
||||
tabla.hit(DrumSound.TABLA_TIT, Duration.SIXTEENTH, velocity=int(vel * 0.55))
|
||||
tabla.hit(DrumSound.TABLA_NA, Duration.SIXTEENTH, velocity=int(vel * 0.7))
|
||||
tabla.hit(DrumSound.TABLA_DHA, Duration.QUARTER, velocity=127, articulation="fermata")
|
||||
tabla.hit(DrumSound.TABLA_GE_BEND, Duration.QUARTER, velocity=110)
|
||||
tabla.rest(Duration.HALF)
|
||||
|
||||
# Bars 17-20: tabla continues through outro, lighter
|
||||
for _ in range(4):
|
||||
tabla.hit(DrumSound.TABLA_DHA, Duration.EIGHTH, velocity=85, articulation="accent")
|
||||
tabla.hit(DrumSound.TABLA_TIT, Duration.EIGHTH, velocity=45)
|
||||
tabla.hit(DrumSound.TABLA_TIT, Duration.EIGHTH, velocity=42)
|
||||
tabla.hit(DrumSound.TABLA_NA, Duration.EIGHTH, velocity=75)
|
||||
tabla.hit(DrumSound.TABLA_TIN, Duration.EIGHTH, velocity=70)
|
||||
tabla.hit(DrumSound.TABLA_TIT, Duration.EIGHTH, velocity=42)
|
||||
tabla.hit(DrumSound.TABLA_DHA, Duration.EIGHTH, velocity=80)
|
||||
tabla.hit(DrumSound.TABLA_GE, Duration.EIGHTH, velocity=65)
|
||||
|
||||
# ── Pad: enters at peak, fades during outro ──
|
||||
pad = score.part("pad", synth="supersaw", envelope="pad", volume=0.0,
|
||||
reverb=0.4, chorus=0.2, detune=10, lowpass=2500)
|
||||
for _ in range(32):
|
||||
pad.rest(Duration.QUARTER)
|
||||
pad.ramp(over=Duration.WHOLE * 2, volume=0.18)
|
||||
for sym in ["Cm", "Ab", "Eb", "Bb"] * 3:
|
||||
pad.add(Chord.from_symbol(sym), Duration.WHOLE)
|
||||
pad.ramp(over=Duration.WHOLE * 2, curve="ease_out", volume=0.0)
|
||||
for sym in ["Cm", "Cm"]:
|
||||
pad.add(Chord.from_symbol(sym), Duration.WHOLE)
|
||||
|
||||
play_song(score, "Acid Tabla — 303 filter automation + tabla (ramp, articulations, Part.hit)")
|
||||
|
||||
|
||||
def snare_cadence():
|
||||
"""Snare Cadence — full drumline with ensemble, flams, diddles, cheese."""
|
||||
score = Score("4/4", bpm=120)
|
||||
|
||||
S = DrumSound.MARCH_SNARE
|
||||
R = DrumSound.MARCH_RIMSHOT
|
||||
C = DrumSound.MARCH_CLICK
|
||||
Q1 = DrumSound.QUAD_1
|
||||
Q2 = DrumSound.QUAD_2
|
||||
Q3 = DrumSound.QUAD_3
|
||||
Q4 = DrumSound.QUAD_4
|
||||
QS = DrumSound.QUAD_SPOCK
|
||||
B1 = DrumSound.BASS_1
|
||||
B2 = DrumSound.BASS_2
|
||||
B3 = DrumSound.BASS_3
|
||||
B4 = DrumSound.BASS_4
|
||||
B5 = DrumSound.BASS_5
|
||||
|
||||
# Snare line — 8 players
|
||||
p = score.part("snares", synth="sine", volume=0.9, reverb=0.2, ensemble=8)
|
||||
# Quad line — 4 players
|
||||
q = score.part("quads", synth="sine", volume=0.5, reverb=0.2, ensemble=4)
|
||||
# Bass line — 5 players
|
||||
b = score.part("basses", synth="sine", volume=0.55, reverb=0.2, ensemble=5)
|
||||
|
||||
_trip = 1.0 / 3
|
||||
|
||||
# Helper: bass split run (down or up)
|
||||
def bass_down(dur=Duration.SIXTEENTH):
|
||||
b.hit(B1, dur, velocity=95)
|
||||
b.hit(B2, dur, velocity=90)
|
||||
b.hit(B3, dur, velocity=85)
|
||||
b.hit(B4, dur, velocity=90)
|
||||
|
||||
def bass_up(dur=Duration.SIXTEENTH):
|
||||
b.hit(B4, dur, velocity=90)
|
||||
b.hit(B3, dur, velocity=85)
|
||||
b.hit(B2, dur, velocity=90)
|
||||
b.hit(B1, dur, velocity=95)
|
||||
|
||||
def bass_hit(dur=Duration.QUARTER):
|
||||
b.hit(B3, dur, velocity=100)
|
||||
|
||||
def quad_sweep_down():
|
||||
q.hit(Q1, Duration.SIXTEENTH, velocity=95)
|
||||
q.hit(Q2, Duration.SIXTEENTH, velocity=88)
|
||||
q.hit(Q3, Duration.SIXTEENTH, velocity=82)
|
||||
q.hit(Q4, Duration.SIXTEENTH, velocity=78)
|
||||
|
||||
def quad_sweep_up():
|
||||
q.hit(Q4, Duration.SIXTEENTH, velocity=78)
|
||||
q.hit(Q3, Duration.SIXTEENTH, velocity=82)
|
||||
q.hit(Q2, Duration.SIXTEENTH, velocity=88)
|
||||
q.hit(Q1, Duration.SIXTEENTH, velocity=95)
|
||||
|
||||
# ── Click count-off ──
|
||||
for _ in range(4):
|
||||
p.hit(C, Duration.QUARTER, velocity=95)
|
||||
q.rest(Duration.QUARTER)
|
||||
b.rest(Duration.QUARTER)
|
||||
|
||||
# ── Section 1: 16th groove — snares only (4 bars) ──
|
||||
for _ in range(16):
|
||||
q.rest(Duration.QUARTER)
|
||||
b.rest(Duration.QUARTER)
|
||||
|
||||
for _ in range(2):
|
||||
p.hit(R, Duration.SIXTEENTH, velocity=118)
|
||||
p.hit(S, Duration.SIXTEENTH, velocity=32)
|
||||
p.hit(S, Duration.SIXTEENTH, velocity=35)
|
||||
p.hit(S, Duration.SIXTEENTH, velocity=30)
|
||||
p.hit(R, Duration.SIXTEENTH, velocity=115)
|
||||
p.hit(S, Duration.SIXTEENTH, velocity=30)
|
||||
p.hit(S, Duration.SIXTEENTH, velocity=28)
|
||||
p.hit(S, Duration.SIXTEENTH, velocity=32)
|
||||
p.hit(R, Duration.SIXTEENTH, velocity=118)
|
||||
p.hit(S, Duration.SIXTEENTH, velocity=35)
|
||||
p.hit(S, Duration.SIXTEENTH, velocity=30)
|
||||
p.hit(S, Duration.SIXTEENTH, velocity=32)
|
||||
p.hit(R, Duration.SIXTEENTH, velocity=120)
|
||||
p.hit(S, Duration.SIXTEENTH, velocity=30)
|
||||
p.hit(S, Duration.SIXTEENTH, velocity=28)
|
||||
p.hit(S, Duration.SIXTEENTH, velocity=32)
|
||||
|
||||
# Triplets mixed in
|
||||
for _ in range(2):
|
||||
p.hit(R, _trip, velocity=118)
|
||||
p.hit(S, _trip, velocity=32)
|
||||
p.hit(S, _trip, velocity=30)
|
||||
p.hit(R, _trip, velocity=115)
|
||||
p.hit(S, _trip, velocity=28)
|
||||
p.hit(S, _trip, velocity=32)
|
||||
p.hit(R, Duration.SIXTEENTH, velocity=118)
|
||||
p.hit(S, Duration.SIXTEENTH, velocity=30)
|
||||
p.hit(S, Duration.SIXTEENTH, velocity=32)
|
||||
p.hit(S, Duration.SIXTEENTH, velocity=28)
|
||||
p.hit(R, _trip, velocity=118)
|
||||
p.hit(S, _trip, velocity=30)
|
||||
p.hit(S, _trip, velocity=35)
|
||||
|
||||
# ── Section 2: Quads + bass enter (4 bars) ──
|
||||
for _ in range(2):
|
||||
p.flam(S, Duration.QUARTER, velocity=118)
|
||||
p.hit(S, Duration.SIXTEENTH, velocity=30)
|
||||
p.hit(S, Duration.SIXTEENTH, velocity=32)
|
||||
p.hit(R, _trip, velocity=118)
|
||||
p.hit(S, _trip, velocity=28)
|
||||
p.hit(S, _trip, velocity=30)
|
||||
p.flam(S, Duration.QUARTER, velocity=118)
|
||||
|
||||
quad_sweep_down()
|
||||
q.hit(QS, Duration.QUARTER, velocity=100)
|
||||
quad_sweep_up()
|
||||
q.hit(QS, Duration.QUARTER, velocity=100)
|
||||
|
||||
bass_hit()
|
||||
b.hit(B5, Duration.QUARTER, velocity=95)
|
||||
bass_hit()
|
||||
b.hit(B1, Duration.QUARTER, velocity=95)
|
||||
|
||||
for _ in range(2):
|
||||
p.hit(S, _trip, velocity=35)
|
||||
p.flam(S, _trip * 2, velocity=118)
|
||||
p.hit(S, Duration.SIXTEENTH, velocity=30)
|
||||
p.hit(S, Duration.SIXTEENTH, velocity=32)
|
||||
p.flam(S, Duration.QUARTER, velocity=118)
|
||||
p.hit(S, _trip, velocity=28)
|
||||
p.hit(R, _trip, velocity=122)
|
||||
p.hit(S, _trip, velocity=35)
|
||||
|
||||
quad_sweep_down()
|
||||
quad_sweep_up()
|
||||
q.hit(Q1, Duration.EIGHTH, velocity=95)
|
||||
q.hit(Q4, Duration.EIGHTH, velocity=85)
|
||||
q.hit(QS, Duration.QUARTER, velocity=100)
|
||||
|
||||
bass_down()
|
||||
bass_up()
|
||||
b.hit(B3, Duration.HALF, velocity=100)
|
||||
|
||||
# ── Section 3: Flams + diddles + full line (4 bars) ──
|
||||
for _ in range(2):
|
||||
p.flam(S, Duration.QUARTER, velocity=120)
|
||||
p.diddle(S, Duration.EIGHTH, velocity=45)
|
||||
p.hit(S, _trip, velocity=30)
|
||||
p.hit(S, _trip, velocity=32)
|
||||
p.hit(S, _trip, velocity=28)
|
||||
p.hit(R, Duration.EIGHTH, velocity=122)
|
||||
p.diddle(S, Duration.EIGHTH, velocity=42)
|
||||
|
||||
q.hit(Q1, Duration.QUARTER, velocity=95)
|
||||
q.hit(Q3, Duration.EIGHTH, velocity=55)
|
||||
q.hit(Q2, _trip, velocity=55)
|
||||
q.hit(Q3, _trip, velocity=55)
|
||||
q.hit(Q4, _trip, velocity=55)
|
||||
q.hit(QS, Duration.EIGHTH, velocity=100)
|
||||
q.hit(Q1, Duration.EIGHTH, velocity=55)
|
||||
|
||||
bass_hit()
|
||||
b.hit(B1, Duration.EIGHTH, velocity=90)
|
||||
b.hit(B5, Duration.EIGHTH, velocity=95)
|
||||
bass_hit()
|
||||
b.hit(B5, Duration.EIGHTH, velocity=90)
|
||||
b.hit(B1, Duration.EIGHTH, velocity=95)
|
||||
|
||||
for _ in range(2):
|
||||
p.diddle(S, Duration.EIGHTH, velocity=45)
|
||||
p.hit(R, _trip, velocity=120)
|
||||
p.hit(S, _trip, velocity=30)
|
||||
p.hit(S, _trip, velocity=32)
|
||||
p.diddle(S, Duration.EIGHTH, velocity=48)
|
||||
p.hit(R, _trip, velocity=118)
|
||||
p.hit(S, _trip, velocity=28)
|
||||
p.hit(S, _trip, velocity=30)
|
||||
p.flam(S, Duration.EIGHTH, velocity=122)
|
||||
p.hit(S, Duration.EIGHTH, velocity=35)
|
||||
|
||||
quad_sweep_down()
|
||||
quad_sweep_up()
|
||||
quad_sweep_down()
|
||||
quad_sweep_up()
|
||||
|
||||
bass_down()
|
||||
bass_up()
|
||||
bass_down()
|
||||
bass_up()
|
||||
|
||||
# ── Section 4: Cheese + 32nds (4 bars) ──
|
||||
for _ in range(2):
|
||||
p.cheese(S, Duration.QUARTER, velocity=120)
|
||||
p.hit(S, 0.0625, velocity=30)
|
||||
p.hit(S, 0.0625, velocity=32)
|
||||
p.hit(S, 0.0625, velocity=35)
|
||||
p.hit(S, 0.0625, velocity=30)
|
||||
p.cheese(S, Duration.QUARTER, velocity=118)
|
||||
p.diddle(S, Duration.EIGHTH, velocity=48)
|
||||
p.hit(R, Duration.EIGHTH, velocity=125)
|
||||
|
||||
q.hit(QS, Duration.QUARTER, velocity=105)
|
||||
q.hit(Q1, Duration.SIXTEENTH, velocity=55)
|
||||
q.hit(Q2, Duration.SIXTEENTH, velocity=55)
|
||||
q.hit(Q3, Duration.SIXTEENTH, velocity=55)
|
||||
q.hit(Q4, Duration.SIXTEENTH, velocity=55)
|
||||
q.hit(QS, Duration.QUARTER, velocity=105)
|
||||
q.hit(Q4, Duration.EIGHTH, velocity=55)
|
||||
q.hit(Q1, Duration.EIGHTH, velocity=90)
|
||||
|
||||
bass_hit()
|
||||
b.hit(B1, Duration.EIGHTH, velocity=90)
|
||||
b.hit(B3, Duration.EIGHTH, velocity=85)
|
||||
b.hit(B5, Duration.EIGHTH, velocity=95)
|
||||
b.hit(B3, Duration.EIGHTH, velocity=85)
|
||||
bass_hit()
|
||||
b.rest(Duration.QUARTER)
|
||||
|
||||
# All cheese
|
||||
p.cheese(S, Duration.QUARTER, velocity=122)
|
||||
p.cheese(S, Duration.QUARTER, velocity=120)
|
||||
p.cheese(S, Duration.QUARTER, velocity=125)
|
||||
p.cheese(S, Duration.QUARTER, velocity=122)
|
||||
|
||||
q.hit(QS, Duration.QUARTER, velocity=105)
|
||||
q.hit(QS, Duration.QUARTER, velocity=105)
|
||||
q.hit(QS, Duration.QUARTER, velocity=108)
|
||||
q.hit(QS, Duration.QUARTER, velocity=105)
|
||||
|
||||
b.hit(B5, Duration.QUARTER, velocity=100)
|
||||
b.hit(B3, Duration.QUARTER, velocity=100)
|
||||
b.hit(B1, Duration.QUARTER, velocity=100)
|
||||
b.hit(B3, Duration.QUARTER, velocity=100)
|
||||
|
||||
p.flam(S, Duration.EIGHTH, velocity=120)
|
||||
p.diddle(S, Duration.EIGHTH, velocity=50)
|
||||
p.flam(S, Duration.EIGHTH, velocity=122)
|
||||
p.diddle(S, Duration.EIGHTH, velocity=52)
|
||||
p.flam(S, Duration.EIGHTH, velocity=125)
|
||||
p.diddle(S, Duration.EIGHTH, velocity=55)
|
||||
p.hit(R, Duration.EIGHTH, velocity=127)
|
||||
p.hit(S, Duration.EIGHTH, velocity=38)
|
||||
|
||||
quad_sweep_down()
|
||||
quad_sweep_up()
|
||||
quad_sweep_down()
|
||||
quad_sweep_up()
|
||||
|
||||
bass_down()
|
||||
bass_up()
|
||||
bass_down()
|
||||
bass_up()
|
||||
|
||||
# ── Section 5: 16ths + triplet 16ths + 32nds (4 bars) ──
|
||||
_trip16 = 1.0 / 6
|
||||
|
||||
for _ in range(2):
|
||||
for beat in range(4):
|
||||
p.hit(R, _trip, velocity=118)
|
||||
p.hit(S, _trip, velocity=35)
|
||||
p.hit(S, _trip, velocity=32)
|
||||
|
||||
quad_sweep_down()
|
||||
quad_sweep_up()
|
||||
quad_sweep_down()
|
||||
quad_sweep_up()
|
||||
|
||||
bass_hit()
|
||||
b.hit(B5, Duration.QUARTER, velocity=95)
|
||||
bass_hit()
|
||||
b.hit(B1, Duration.QUARTER, velocity=95)
|
||||
|
||||
# 32nd run crescendo
|
||||
for i in range(32):
|
||||
p.hit(S, 0.0625, velocity=min(22 + i * 3, 92))
|
||||
p.hit(R, Duration.EIGHTH, velocity=125)
|
||||
p.hit(R, Duration.EIGHTH, velocity=127)
|
||||
|
||||
for _ in range(4):
|
||||
q.hit(Q1, 0.0625, velocity=55)
|
||||
q.hit(Q2, 0.0625, velocity=55)
|
||||
q.hit(Q3, 0.0625, velocity=55)
|
||||
q.hit(Q4, 0.0625, velocity=55)
|
||||
q.hit(QS, Duration.QUARTER, velocity=108)
|
||||
|
||||
bass_down()
|
||||
bass_up()
|
||||
bass_down()
|
||||
b.hit(B5, Duration.QUARTER, velocity=100)
|
||||
b.hit(B1, Duration.QUARTER, velocity=100)
|
||||
|
||||
# Triplet 16ths — all sections
|
||||
for _ in range(2):
|
||||
for beat in range(4):
|
||||
p.hit(R, _trip16, velocity=115)
|
||||
p.hit(S, _trip16, velocity=30)
|
||||
p.hit(S, _trip16, velocity=32)
|
||||
p.hit(R, _trip16, velocity=112)
|
||||
p.hit(S, _trip16, velocity=28)
|
||||
p.hit(S, _trip16, velocity=30)
|
||||
|
||||
for beat in range(4):
|
||||
q.hit(Q1, _trip16, velocity=90)
|
||||
q.hit(Q2, _trip16, velocity=55)
|
||||
q.hit(Q3, _trip16, velocity=55)
|
||||
q.hit(Q4, _trip16, velocity=55)
|
||||
q.hit(Q3, _trip16, velocity=55)
|
||||
q.hit(Q2, _trip16, velocity=55)
|
||||
|
||||
bass_down()
|
||||
bass_up()
|
||||
bass_down()
|
||||
bass_up()
|
||||
|
||||
# ── Section 6: Buzz roll climax (2 bars) ──
|
||||
for i in range(64):
|
||||
p.hit(S, 0.0625, velocity=min(20 + i * 1.5, 100))
|
||||
p.hit(R, Duration.EIGHTH, velocity=127)
|
||||
p.hit(R, Duration.EIGHTH, velocity=127)
|
||||
|
||||
for i in range(32):
|
||||
q.hit([Q1, Q2, Q3, Q4][i % 4], 0.0625, velocity=min(40 + i * 2, 95))
|
||||
q.hit(QS, Duration.QUARTER, velocity=110)
|
||||
|
||||
for i in range(16):
|
||||
b.hit([B1, B2, B3, B4, B5, B4, B3, B2,
|
||||
B1, B2, B3, B4, B5, B4, B3, B2][i], Duration.SIXTEENTH, velocity=90)
|
||||
b.hit(B3, Duration.HALF, velocity=100)
|
||||
b.hit(B3, Duration.HALF, velocity=100)
|
||||
|
||||
# ── Ending: big unison hits ──
|
||||
p.hit(R, Duration.EIGHTH, velocity=127)
|
||||
q.hit(QS, Duration.EIGHTH, velocity=110)
|
||||
b.hit(B3, Duration.EIGHTH, velocity=100)
|
||||
|
||||
p.rest(Duration.QUARTER + Duration.EIGHTH)
|
||||
q.rest(Duration.QUARTER + Duration.EIGHTH)
|
||||
b.rest(Duration.QUARTER + Duration.EIGHTH)
|
||||
|
||||
p.hit(R, Duration.EIGHTH, velocity=127)
|
||||
q.hit(QS, Duration.EIGHTH, velocity=110)
|
||||
b.hit(B3, Duration.EIGHTH, velocity=100)
|
||||
|
||||
p.rest(Duration.QUARTER + Duration.EIGHTH)
|
||||
q.rest(Duration.QUARTER + Duration.EIGHTH)
|
||||
b.rest(Duration.QUARTER + Duration.EIGHTH)
|
||||
|
||||
# Flam into final CRACK — all sections
|
||||
p.flam(S, Duration.EIGHTH, velocity=127)
|
||||
q.hit(QS, Duration.EIGHTH, velocity=110)
|
||||
b.hit(B3, Duration.EIGHTH, velocity=100)
|
||||
|
||||
p.rest(Duration.QUARTER + Duration.EIGHTH)
|
||||
q.rest(Duration.QUARTER + Duration.EIGHTH)
|
||||
b.rest(Duration.QUARTER + Duration.EIGHTH)
|
||||
|
||||
p.hit(R, Duration.QUARTER, velocity=127)
|
||||
q.hit(QS, Duration.QUARTER, velocity=110)
|
||||
b.hit(B3, Duration.QUARTER, velocity=100)
|
||||
|
||||
p.rest(Duration.HALF)
|
||||
q.rest(Duration.HALF)
|
||||
b.rest(Duration.HALF)
|
||||
|
||||
play_song(score, "Snare Cadence — full drumline (8 snares, 4 quads, 5 basses)")
|
||||
|
||||
|
||||
def ensemble_showcase():
|
||||
"""Ensemble Showcase — acid bass, tabla solo, strings, snare line."""
|
||||
score = Score("4/4", bpm=128)
|
||||
|
||||
# ── Drums: house kit ──
|
||||
score.drums("house", repeats=24, fill="house", fill_every=8)
|
||||
score.set_drum_effects(volume=0.4, reverb=0.1)
|
||||
|
||||
# ── 303 Acid Bass: detuned, spread, LFO filter, ensemble=3 ──
|
||||
acid = score.part("acid", synth="saw", volume=0.55, ensemble=3,
|
||||
lowpass=400, lowpass_q=10.0, distortion=0.35,
|
||||
distortion_drive=4.0, legato=True, glide=0.03,
|
||||
detune=12, spread=0.4, sub_osc=0.15,
|
||||
sidechain=0.5, sidechain_release=0.08)
|
||||
|
||||
acid.lfo("lowpass", rate=0.5, min=400, max=5000, bars=16, shape="sine")
|
||||
for _ in range(8):
|
||||
for n in ["C2", "C3", "C2", "Eb2", "C2", "G2", "Bb2", "C2"]:
|
||||
acid.add(n, Duration.EIGHTH, velocity=90)
|
||||
|
||||
acid.ramp(over=Duration.WHOLE * 8, curve="ease_in", lowpass=6000)
|
||||
for _ in range(8):
|
||||
for n in ["C2", "Eb3", "C3", "G2", "Bb2", "C3", "G2", "C2"]:
|
||||
acid.add(n, Duration.EIGHTH, velocity=95)
|
||||
|
||||
acid.ramp(over=Duration.WHOLE * 8, curve="ease_out", lowpass=500)
|
||||
for _ in range(8):
|
||||
for n in ["C2", "C3", "C2", "Eb2", "C2", "G2", "Bb2", "C2"]:
|
||||
acid.add(n, Duration.EIGHTH, velocity=88)
|
||||
|
||||
# ── Strings: 16-player ensemble pad ──
|
||||
strings = score.part("strings", instrument="string_ensemble", volume=0.0,
|
||||
reverb=0.4, ensemble=16, detune=8, spread=0.5)
|
||||
|
||||
for _ in range(32):
|
||||
strings.rest(Duration.QUARTER)
|
||||
strings.ramp(over=Duration.WHOLE * 4, curve="ease_in", volume=0.18)
|
||||
for ch in ["Cm", "Ab", "Eb", "Bb"] * 4:
|
||||
strings.add(Chord.from_symbol(ch), Duration.WHOLE, velocity=55)
|
||||
strings.ramp(over=Duration.WHOLE * 4, curve="ease_out", volume=0.0)
|
||||
for ch in ["Cm", "Ab", "Eb", "Bb"]:
|
||||
strings.add(Chord.from_symbol(ch), Duration.WHOLE, velocity=45)
|
||||
for _ in range(16):
|
||||
strings.rest(Duration.QUARTER)
|
||||
|
||||
# ── Tabla: ensemble=3, enters bar 9 ──
|
||||
tabla = score.part("tabla", synth="sine", volume=0.0, reverb=0.15, ensemble=3)
|
||||
|
||||
for _ in range(32):
|
||||
tabla.rest(Duration.QUARTER)
|
||||
tabla.ramp(over=Duration.WHOLE * 2, volume=0.45)
|
||||
|
||||
# Keherwa groove — 8 bars
|
||||
for _ in range(8):
|
||||
tabla.hit(DrumSound.TABLA_DHA, Duration.EIGHTH, velocity=95, articulation="accent")
|
||||
tabla.hit(DrumSound.TABLA_TIT, Duration.EIGHTH, velocity=50)
|
||||
tabla.hit(DrumSound.TABLA_TIT, Duration.EIGHTH, velocity=48)
|
||||
tabla.hit(DrumSound.TABLA_NA, Duration.EIGHTH, velocity=82)
|
||||
tabla.hit(DrumSound.TABLA_TIN, Duration.EIGHTH, velocity=78)
|
||||
tabla.hit(DrumSound.TABLA_TIT, Duration.EIGHTH, velocity=48)
|
||||
tabla.hit(DrumSound.TABLA_DHA, Duration.EIGHTH, velocity=88, articulation="accent")
|
||||
tabla.hit(DrumSound.TABLA_GE, Duration.EIGHTH, velocity=72)
|
||||
|
||||
# Tabla solo — getting busier
|
||||
tabla.hit(DrumSound.TABLA_DHA, Duration.EIGHTH, velocity=100, articulation="marcato")
|
||||
tabla.hit(DrumSound.TABLA_TIT, Duration.SIXTEENTH, velocity=45)
|
||||
tabla.hit(DrumSound.TABLA_TIT, Duration.SIXTEENTH, velocity=48)
|
||||
tabla.hit(DrumSound.TABLA_NA, Duration.EIGHTH, velocity=85)
|
||||
tabla.hit(DrumSound.TABLA_TIN, Duration.EIGHTH, velocity=80)
|
||||
tabla.hit(DrumSound.TABLA_TIT, Duration.SIXTEENTH, velocity=45)
|
||||
tabla.hit(DrumSound.TABLA_NA, Duration.SIXTEENTH, velocity=55)
|
||||
tabla.hit(DrumSound.TABLA_DHA, Duration.EIGHTH, velocity=95, articulation="accent")
|
||||
tabla.hit(DrumSound.TABLA_GE_BEND, Duration.EIGHTH, velocity=82)
|
||||
|
||||
tabla.hit(DrumSound.TABLA_DHA, Duration.EIGHTH, velocity=105, articulation="marcato")
|
||||
tabla.hit(DrumSound.TABLA_TIT, Duration.SIXTEENTH, velocity=48)
|
||||
tabla.hit(DrumSound.TABLA_NA, Duration.SIXTEENTH, velocity=55)
|
||||
tabla.hit(DrumSound.TABLA_TIT, Duration.SIXTEENTH, velocity=45)
|
||||
tabla.hit(DrumSound.TABLA_TIT, Duration.SIXTEENTH, velocity=48)
|
||||
tabla.hit(DrumSound.TABLA_NA, Duration.EIGHTH, velocity=88)
|
||||
tabla.hit(DrumSound.TABLA_DHA, Duration.SIXTEENTH, velocity=100)
|
||||
tabla.hit(DrumSound.TABLA_TIT, Duration.SIXTEENTH, velocity=50)
|
||||
tabla.hit(DrumSound.TABLA_GE_BEND, Duration.EIGHTH, velocity=85)
|
||||
tabla.hit(DrumSound.TABLA_DHA, Duration.EIGHTH, velocity=108, articulation="accent")
|
||||
|
||||
# Tihai crescendo
|
||||
for vel in [85, 90, 95, 100, 105, 110]:
|
||||
tabla.hit(DrumSound.TABLA_DHA, Duration.EIGHTH, velocity=vel, articulation="accent")
|
||||
tabla.hit(DrumSound.TABLA_TIT, Duration.SIXTEENTH, velocity=int(vel * 0.55))
|
||||
tabla.hit(DrumSound.TABLA_NA, Duration.SIXTEENTH, velocity=int(vel * 0.7))
|
||||
tabla.hit(DrumSound.TABLA_DHA, Duration.QUARTER, velocity=125, articulation="fermata")
|
||||
tabla.hit(DrumSound.TABLA_GE_BEND, Duration.QUARTER, velocity=110)
|
||||
|
||||
# Groove out
|
||||
for _ in range(4):
|
||||
tabla.hit(DrumSound.TABLA_DHA, Duration.EIGHTH, velocity=88, articulation="accent")
|
||||
tabla.hit(DrumSound.TABLA_TIT, Duration.EIGHTH, velocity=45)
|
||||
tabla.hit(DrumSound.TABLA_TIT, Duration.EIGHTH, velocity=42)
|
||||
tabla.hit(DrumSound.TABLA_NA, Duration.EIGHTH, velocity=75)
|
||||
tabla.hit(DrumSound.TABLA_TIN, Duration.EIGHTH, velocity=70)
|
||||
tabla.hit(DrumSound.TABLA_TIT, Duration.EIGHTH, velocity=42)
|
||||
tabla.hit(DrumSound.TABLA_DHA, Duration.EIGHTH, velocity=80)
|
||||
tabla.hit(DrumSound.TABLA_GE, Duration.EIGHTH, velocity=65)
|
||||
|
||||
# ── Snare line: 8-player ensemble, enters bar 17 ──
|
||||
S = DrumSound.MARCH_SNARE
|
||||
R = DrumSound.MARCH_RIMSHOT
|
||||
|
||||
snares = score.part("snares", synth="sine", volume=0.0, reverb=0.15, ensemble=8)
|
||||
|
||||
for _ in range(64):
|
||||
snares.rest(Duration.QUARTER)
|
||||
snares.ramp(over=Duration.WHOLE * 2, volume=0.7)
|
||||
|
||||
for _ in range(4):
|
||||
snares.hit(R, Duration.SIXTEENTH, velocity=118)
|
||||
snares.hit(S, Duration.SIXTEENTH, velocity=30)
|
||||
snares.hit(S, Duration.SIXTEENTH, velocity=32)
|
||||
snares.hit(S, Duration.SIXTEENTH, velocity=28)
|
||||
snares.hit(R, Duration.SIXTEENTH, velocity=115)
|
||||
snares.hit(S, Duration.SIXTEENTH, velocity=30)
|
||||
snares.hit(S, Duration.SIXTEENTH, velocity=28)
|
||||
snares.hit(S, Duration.SIXTEENTH, velocity=32)
|
||||
snares.hit(R, Duration.SIXTEENTH, velocity=118)
|
||||
snares.hit(S, Duration.SIXTEENTH, velocity=35)
|
||||
snares.hit(S, Duration.SIXTEENTH, velocity=28)
|
||||
snares.hit(S, Duration.SIXTEENTH, velocity=30)
|
||||
snares.hit(R, Duration.SIXTEENTH, velocity=120)
|
||||
snares.hit(S, Duration.SIXTEENTH, velocity=30)
|
||||
snares.hit(S, Duration.SIXTEENTH, velocity=28)
|
||||
snares.hit(S, Duration.SIXTEENTH, velocity=32)
|
||||
|
||||
# Buzz roll finale
|
||||
for i in range(64):
|
||||
snares.hit(S, 0.0625, velocity=min(20 + i * 1.5, 100))
|
||||
snares.hit(R, Duration.EIGHTH, velocity=127)
|
||||
snares.hit(R, Duration.EIGHTH, velocity=127)
|
||||
|
||||
snares.ramp(over=Duration.WHOLE * 2, curve="ease_out", volume=0.0)
|
||||
for _ in range(2):
|
||||
snares.hit(R, Duration.SIXTEENTH, velocity=110)
|
||||
snares.hit(S, Duration.SIXTEENTH, velocity=30)
|
||||
snares.hit(S, Duration.SIXTEENTH, velocity=28)
|
||||
snares.hit(S, Duration.SIXTEENTH, velocity=32)
|
||||
snares.hit(R, Duration.SIXTEENTH, velocity=108)
|
||||
snares.hit(S, Duration.SIXTEENTH, velocity=30)
|
||||
snares.hit(S, Duration.SIXTEENTH, velocity=28)
|
||||
snares.hit(S, Duration.SIXTEENTH, velocity=32)
|
||||
snares.hit(R, Duration.SIXTEENTH, velocity=105)
|
||||
snares.hit(S, Duration.SIXTEENTH, velocity=30)
|
||||
snares.hit(S, Duration.SIXTEENTH, velocity=28)
|
||||
snares.hit(S, Duration.SIXTEENTH, velocity=32)
|
||||
snares.hit(R, Duration.SIXTEENTH, velocity=100)
|
||||
snares.hit(S, Duration.SIXTEENTH, velocity=30)
|
||||
snares.hit(S, Duration.SIXTEENTH, velocity=28)
|
||||
snares.hit(S, Duration.SIXTEENTH, velocity=32)
|
||||
|
||||
# ── Lead synth: 6-player ensemble, enters bar 5 ──
|
||||
lead = score.part("lead", synth="saw", envelope="pluck", volume=0.0,
|
||||
lowpass=3500, detune=6, chorus=0.1, reverb=0.2,
|
||||
delay=0.15, delay_time=0.33, delay_feedback=0.25,
|
||||
ensemble=6)
|
||||
|
||||
for _ in range(16):
|
||||
lead.rest(Duration.QUARTER)
|
||||
lead.ramp(over=Duration.WHOLE * 2, volume=0.3)
|
||||
|
||||
for _ in range(2):
|
||||
lead.add("Eb5", Duration.QUARTER, velocity=88)
|
||||
lead.add("G5", Duration.QUARTER, velocity=92)
|
||||
lead.add("Bb5", Duration.HALF, velocity=95, articulation="accent")
|
||||
lead.add("Ab5", Duration.QUARTER, velocity=88)
|
||||
lead.add("G5", Duration.QUARTER, velocity=85)
|
||||
lead.add("Eb5", Duration.QUARTER, velocity=82)
|
||||
lead.add("D5", Duration.QUARTER, velocity=80)
|
||||
|
||||
lead.swell(["Eb5", "G5", "Bb5", "C6", "Bb5", "G5", "Eb5", "D5"],
|
||||
Duration.QUARTER, low_vel=75, peak_vel=105)
|
||||
lead.decrescendo(["Eb5", "D5", "C5", "Bb4"], Duration.HALF,
|
||||
start_vel=90, end_vel=60)
|
||||
|
||||
for _ in range(16):
|
||||
lead.rest(Duration.QUARTER)
|
||||
|
||||
lead.ramp(over=Duration.WHOLE * 2, volume=0.35)
|
||||
lead.crescendo(["C5", "Eb5", "G5", "Bb5", "C6", "Eb6", "C6", "Bb5"],
|
||||
Duration.QUARTER, start_vel=80, end_vel=110)
|
||||
lead.add("G5", Duration.HALF, velocity=105, bend=1, bend_type="smooth")
|
||||
lead.add("Eb5", Duration.HALF, velocity=95)
|
||||
lead.decrescendo(["C5", "Bb4", "G4", "Eb4"], Duration.WHOLE,
|
||||
start_vel=85, end_vel=40)
|
||||
|
||||
play_song(score, "Ensemble Showcase — acid bass, tabla solo, 16-player strings, 8-player snare line")
|
||||
|
||||
|
||||
SONGS = {
|
||||
"1": ("Bossa Nova in A minor", bossa_nova_girl),
|
||||
"2": ("Bebop in Bb major", bebop_in_bb),
|
||||
@@ -2355,6 +3030,9 @@ SONGS = {
|
||||
"28": ("Descent (Generative — different every time)", descent),
|
||||
"29": ("Pop Rock (I-V-vi-IV)", pop_rock),
|
||||
"30": ("Sitar Drone (Bhairav, hold() polyphony)", sitar_drone),
|
||||
"31": ("Acid Tabla (303 + tabla, ramp, articulations)", acid_tabla),
|
||||
"32": ("Snare Cadence (marching snare, flams, diddles)", snare_cadence),
|
||||
"33": ("Ensemble Showcase (acid+tabla+strings+snare line)", ensemble_showcase),
|
||||
}
|
||||
|
||||
if __name__ == "__main__":
|
||||
@@ -2368,7 +3046,7 @@ if __name__ == "__main__":
|
||||
print(f" {key:>2}. {name}")
|
||||
|
||||
print()
|
||||
choice = input(" Pick a song (1-30, or 'all'): ").strip()
|
||||
choice = input(" Pick a song (1-33, or 'all'): ").strip()
|
||||
print()
|
||||
|
||||
if choice == "all":
|
||||
|
||||
@@ -0,0 +1,69 @@
|
||||
"""Sprunki Simon Phase 1 — melody reference.
|
||||
|
||||
Notes transcribed from MIDI. Use as a base for arrangements.
|
||||
|
||||
Usage:
|
||||
python examples/sprunki.py
|
||||
"""
|
||||
|
||||
import sounddevice as sd
|
||||
|
||||
from pytheory import Score, Duration
|
||||
from pytheory.play import render_score, SAMPLE_RATE
|
||||
|
||||
|
||||
def sprunki_simon():
|
||||
score = Score("4/4", bpm=200)
|
||||
|
||||
lead = score.part("lead", synth="square", envelope="pluck", volume=0.5,
|
||||
lowpass=4500, detune=3, reverb=0.1)
|
||||
|
||||
# Phrase A
|
||||
lead.add("E4", 1.0)
|
||||
lead.add("G4", 1.0)
|
||||
lead.rest(1.5)
|
||||
lead.add("A4", 0.5)
|
||||
lead.add("B4", 1.0)
|
||||
lead.add("A4", 1.0)
|
||||
lead.add("G4", 1.0)
|
||||
lead.add("D4", 1.0)
|
||||
|
||||
# Phrase B
|
||||
lead.add("E4", 1.0)
|
||||
lead.add("G4", 1.0)
|
||||
lead.rest(1.5)
|
||||
lead.add("A4", 0.5)
|
||||
lead.add("D4", 2.0)
|
||||
lead.add("B3", 1.0)
|
||||
lead.add("A3", 0.5)
|
||||
lead.add("D4", 0.5)
|
||||
|
||||
# Phrase C
|
||||
lead.add("E4", 1.0)
|
||||
lead.add("G4", 1.0)
|
||||
lead.rest(1.5)
|
||||
lead.add("A4", 0.5)
|
||||
lead.add("B4", 1.0)
|
||||
lead.add("A4", 1.0)
|
||||
lead.add("G4", 1.0)
|
||||
lead.add("B4", 1.0)
|
||||
|
||||
# Phrase D
|
||||
lead.add("A4", 2.0)
|
||||
lead.add("G4", 1.0)
|
||||
lead.add("E4", 1.0)
|
||||
lead.add("B3", 2.0)
|
||||
lead.add("D4", 2.0)
|
||||
|
||||
return score
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
score = sprunki_simon()
|
||||
print(" Sprunki Simon Phase 1")
|
||||
try:
|
||||
buf = render_score(score)
|
||||
sd.play(buf, SAMPLE_RATE)
|
||||
sd.wait()
|
||||
except KeyboardInterrupt:
|
||||
sd.stop()
|
||||
+1
-2
@@ -1,6 +1,6 @@
|
||||
[project]
|
||||
name = "pytheory"
|
||||
version = "0.37.0"
|
||||
version = "0.39.3"
|
||||
description = "Music Theory for Humans"
|
||||
readme = "README.md"
|
||||
license = "MIT"
|
||||
@@ -21,7 +21,6 @@ classifiers = [
|
||||
"Programming Language :: Python :: 3.13",
|
||||
]
|
||||
dependencies = [
|
||||
"numeral",
|
||||
"sounddevice",
|
||||
"scipy",
|
||||
]
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
"""PyTheory: Music Theory for Humans."""
|
||||
|
||||
__version__ = "0.37.0"
|
||||
__version__ = "0.39.3"
|
||||
|
||||
from .tones import Tone, Interval
|
||||
from .systems import System, SYSTEMS, TET
|
||||
@@ -23,7 +23,7 @@ __all__ = [
|
||||
"PROGRESSIONS", "Chord", "Fretboard", "Fingering", "analyze_progression",
|
||||
"System", "SYSTEMS", "TET", "CHARTS", "charts_for_fretboard",
|
||||
"play", "save", "save_midi", "play_progression", "play_pattern",
|
||||
"play_score", "Synth", "Envelope",
|
||||
"play_score", "render_score", "Synth", "Envelope",
|
||||
"Duration", "TimeSignature", "RhythmNote", "Rest", "Score", "Part",
|
||||
"DrumSound", "Pattern", "Section", "INSTRUMENTS",
|
||||
]
|
||||
|
||||
@@ -2,6 +2,43 @@ import math
|
||||
|
||||
REFERENCE_A = 440
|
||||
|
||||
# ── Roman numeral helpers (replaces `numeral` package) ───────────────────
|
||||
|
||||
_ROMAN_MAP = [
|
||||
(1000, "M"), (900, "CM"), (500, "D"), (400, "CD"),
|
||||
(100, "C"), (90, "XC"), (50, "L"), (40, "XL"),
|
||||
(10, "X"), (9, "IX"), (5, "V"), (4, "IV"), (1, "I"),
|
||||
]
|
||||
|
||||
_ROMAN_VALUES = {
|
||||
"I": 1, "V": 5, "X": 10, "L": 50, "C": 100, "D": 500, "M": 1000,
|
||||
}
|
||||
|
||||
|
||||
def int2roman(n: int) -> str:
|
||||
"""Convert an integer to an uppercase Roman numeral string."""
|
||||
result = []
|
||||
for value, numeral in _ROMAN_MAP:
|
||||
while n >= value:
|
||||
result.append(numeral)
|
||||
n -= value
|
||||
return "".join(result)
|
||||
|
||||
|
||||
def roman2int(s: str) -> int:
|
||||
"""Convert a Roman numeral string (case-insensitive) to an integer."""
|
||||
s = s.upper()
|
||||
total = 0
|
||||
prev = 0
|
||||
for ch in reversed(s):
|
||||
val = _ROMAN_VALUES.get(ch, 0)
|
||||
if val < prev:
|
||||
total -= val
|
||||
else:
|
||||
total += val
|
||||
prev = val
|
||||
return total
|
||||
|
||||
# Index of C in the Western tone list (A=0, A#=1, B=2, C=3, ...).
|
||||
# Scientific pitch notation changes octave at C, not A, so this offset
|
||||
# is needed for all octave arithmetic.
|
||||
|
||||
+2
-2
@@ -849,7 +849,7 @@ class Chord:
|
||||
>>> Chord([D4, F4, A4]).analyze("C")
|
||||
'ii'
|
||||
"""
|
||||
import numeral as numeral_mod
|
||||
from ._statics import int2roman
|
||||
from .scales import TonedScale
|
||||
from .systems import SYSTEMS
|
||||
from .tones import Tone
|
||||
@@ -874,7 +874,7 @@ class Chord:
|
||||
scale_names = [t.name for t in scale.tones[:-1]]
|
||||
|
||||
def _build_numeral(root, quality, degree_idx, prefix=""):
|
||||
numeral_str = numeral_mod.int2roman(degree_idx + 1, only_ascii=True)
|
||||
numeral_str = int2roman(degree_idx + 1)
|
||||
suffix = ""
|
||||
if "minor" in quality:
|
||||
numeral_str = numeral_str.lower()
|
||||
|
||||
+397
-61
@@ -358,27 +358,42 @@ def piano_wave(hz, peak=SAMPLE_PEAK, n_samples=SAMPLE_RATE):
|
||||
# Harmonics with the metallic spectral shape of steel strings
|
||||
n_harmonics = min(15, int((SAMPLE_RATE / 2) / hz))
|
||||
|
||||
# Vectorized harmonic synthesis — all harmonics at once
|
||||
harmonics = numpy.arange(1, n_harmonics + 1, dtype=numpy.float64)
|
||||
|
||||
# Piano spectral shape as array
|
||||
amps = numpy.zeros(n_harmonics, dtype=numpy.float64)
|
||||
amps[0] = 1.0
|
||||
if n_harmonics > 1:
|
||||
amps[1] = 0.7 + 0.15 * brightness
|
||||
if n_harmonics > 2:
|
||||
amps[2] = 0.45 + 0.2 * brightness
|
||||
for i in range(3, min(6, n_harmonics)):
|
||||
amps[i] = (0.25 + 0.15 * brightness) / (i + 1)
|
||||
for i in range(6, n_harmonics):
|
||||
amps[i] = (0.1 + 0.1 * brightness) / ((i + 1) ** 2)
|
||||
|
||||
# Per-harmonic decay rates
|
||||
h_decay_rates = (1.5 - 0.5 * brightness) * (harmonics - 1)
|
||||
|
||||
# Random phases
|
||||
phases = rng.uniform(0, 2 * numpy.pi, n_harmonics)
|
||||
|
||||
for string_hz in [hz, hz2]:
|
||||
for n in range(1, n_harmonics + 1):
|
||||
f_n = string_hz * n
|
||||
if f_n >= SAMPLE_RATE / 2:
|
||||
break
|
||||
# Piano spectral shape: strong 1-3, then falling
|
||||
# Upper register has more prominent harmonics (brighter)
|
||||
if n == 1:
|
||||
amp = 1.0
|
||||
elif n == 2:
|
||||
amp = 0.7 + 0.15 * brightness
|
||||
elif n == 3:
|
||||
amp = 0.45 + 0.2 * brightness
|
||||
elif n <= 6:
|
||||
amp = (0.25 + 0.15 * brightness) / n
|
||||
else:
|
||||
amp = (0.1 + 0.1 * brightness) / (n * n)
|
||||
# Higher harmonics decay faster, but less so in upper register
|
||||
h_decay = decay * numpy.exp(-(1.5 - 0.5 * brightness) * (n - 1) * t)
|
||||
phase = rng.uniform(0, 2 * numpy.pi)
|
||||
wave += amp * numpy.sin(2 * numpy.pi * f_n * t + phase) * h_decay
|
||||
freqs = string_hz * harmonics # (n_harmonics,)
|
||||
# Mask out harmonics above Nyquist
|
||||
valid = freqs < SAMPLE_RATE / 2
|
||||
if not valid.any():
|
||||
continue
|
||||
v_freqs = freqs[valid]
|
||||
v_amps = amps[valid]
|
||||
v_rates = h_decay_rates[valid]
|
||||
v_phases = phases[valid]
|
||||
|
||||
# 2D: (n_valid, n_samples) — one sin() call for all harmonics
|
||||
phase_matrix = 2 * numpy.pi * v_freqs[:, numpy.newaxis] * t[numpy.newaxis, :] + v_phases[:, numpy.newaxis]
|
||||
decay_matrix = decay[numpy.newaxis, :] * numpy.exp(-v_rates[:, numpy.newaxis] * t[numpy.newaxis, :])
|
||||
wave += (v_amps[:, numpy.newaxis] * numpy.sin(phase_matrix) * decay_matrix).sum(axis=0)
|
||||
|
||||
wave *= 0.5
|
||||
|
||||
@@ -1995,16 +2010,32 @@ def _noise(n_samples):
|
||||
return numpy.random.uniform(-1.0, 1.0, n_samples).astype(numpy.float32)
|
||||
|
||||
|
||||
# ── Cached helpers for hot paths ──────────────────────────────────────────
|
||||
|
||||
_time_cache = {}
|
||||
|
||||
|
||||
def _get_time_array(n_samples):
|
||||
"""Cached time array — avoids reallocation on every synth call."""
|
||||
if n_samples not in _time_cache:
|
||||
_time_cache[n_samples] = numpy.arange(n_samples, dtype=numpy.float32) / SAMPLE_RATE
|
||||
return _time_cache[n_samples]
|
||||
|
||||
|
||||
def _sine_f32(hz, n_samples):
|
||||
"""Float32 sine wave, normalized to ±1."""
|
||||
t = numpy.arange(n_samples, dtype=numpy.float32) / SAMPLE_RATE
|
||||
return numpy.sin(2 * numpy.pi * hz * t)
|
||||
return numpy.sin(2 * numpy.pi * hz * _get_time_array(n_samples))
|
||||
|
||||
|
||||
_decay_cache = {}
|
||||
|
||||
|
||||
def _exp_decay(n_samples, decay_rate):
|
||||
"""Exponential decay envelope from 1→0."""
|
||||
t = numpy.arange(n_samples, dtype=numpy.float32) / SAMPLE_RATE
|
||||
return numpy.exp(-decay_rate * t)
|
||||
"""Exponential decay envelope from 1→0. Cached."""
|
||||
key = (n_samples, decay_rate)
|
||||
if key not in _decay_cache:
|
||||
_decay_cache[key] = numpy.exp(-decay_rate * _get_time_array(n_samples))
|
||||
return _decay_cache[key]
|
||||
|
||||
|
||||
def _synth_kick(n_samples):
|
||||
@@ -2796,6 +2827,151 @@ def _synth_metal_hat(n_samples):
|
||||
return out
|
||||
|
||||
|
||||
def _synth_march_snare(n_samples):
|
||||
"""Marching snare — ultra-tight kevlar head, high and crisp.
|
||||
|
||||
Higher pitched than a kit snare. Very short decay — all attack,
|
||||
no sustain. Tight snare wires give a brief sizzle.
|
||||
"""
|
||||
t = numpy.arange(n_samples, dtype=numpy.float32) / SAMPLE_RATE
|
||||
# Higher-pitched body — tight kevlar pops high
|
||||
body = numpy.sin(2 * numpy.pi * 450 * t) * _exp_decay(n_samples, 60) * 0.4
|
||||
body2 = numpy.sin(2 * numpy.pi * 700 * t) * _exp_decay(n_samples, 75) * 0.2
|
||||
# Sharp stick pop
|
||||
click_len = min(int(SAMPLE_RATE * 0.001), n_samples)
|
||||
click = _noise(click_len) * _exp_decay(click_len, 400) * 1.2
|
||||
# Very tight snare sizzle — higher band, shorter
|
||||
buzz_len = min(int(SAMPLE_RATE * 0.025), n_samples)
|
||||
buzz_raw = _noise(buzz_len)
|
||||
if buzz_len > 20:
|
||||
bl, al = scipy.signal.butter(2, [3500, 8000], btype='band', fs=SAMPLE_RATE)
|
||||
buzz = scipy.signal.lfilter(bl, al, numpy.pad(buzz_raw, (0, max(0, n_samples - buzz_len))))[:buzz_len]
|
||||
else:
|
||||
buzz = buzz_raw
|
||||
buzz *= _exp_decay(buzz_len, 50) * 0.35
|
||||
result = body + body2
|
||||
result[:click_len] += click
|
||||
result[:buzz_len] += buzz
|
||||
return numpy.tanh(result * 2.8)
|
||||
|
||||
|
||||
def _synth_march_rimshot(n_samples):
|
||||
"""Marching rimshot — woody metallic crack.
|
||||
|
||||
The stick catches the rim — you get the full snare hit plus
|
||||
a bright, woody-metallic crack from the aluminum rim. Short
|
||||
ring that dies fast but gives it that cutting edge.
|
||||
"""
|
||||
wave = _synth_march_snare(n_samples)
|
||||
t = numpy.arange(n_samples, dtype=numpy.float32) / SAMPLE_RATE
|
||||
# Rim crack — bright but short, woody-metallic character
|
||||
rim = numpy.sin(2 * numpy.pi * 1100 * t) * _exp_decay(n_samples, 45) * 0.35
|
||||
rim2 = numpy.sin(2 * numpy.pi * 2200 * t) * _exp_decay(n_samples, 55) * 0.2
|
||||
# Hard transient pop
|
||||
pop_len = min(int(SAMPLE_RATE * 0.002), n_samples)
|
||||
pop = _noise(pop_len) * _exp_decay(pop_len, 350) * 1.5
|
||||
# Extra body punch
|
||||
punch = numpy.sin(2 * numpy.pi * 500 * t) * _exp_decay(n_samples, 65) * 0.3
|
||||
result = wave * 1.4 + rim + rim2 + punch
|
||||
result[:pop_len] += pop
|
||||
return numpy.tanh(result * 2.0)
|
||||
|
||||
|
||||
def _synth_march_click(n_samples):
|
||||
"""Stick click — taped hickory sticks clocked together.
|
||||
|
||||
Bright wood-on-wood with a slightly dampened attack from the
|
||||
electrical tape. Not as ringy as a clave — the tape absorbs
|
||||
some of the high overtones — but still bright and snappy.
|
||||
"""
|
||||
t = numpy.arange(n_samples, dtype=numpy.float32) / SAMPLE_RATE
|
||||
# Wood resonance — brighter than before, but tape dampens ring
|
||||
body = numpy.sin(2 * numpy.pi * 1100 * t) * _exp_decay(n_samples, 65) * 0.45
|
||||
body2 = numpy.sin(2 * numpy.pi * 1800 * t) * _exp_decay(n_samples, 80) * 0.25
|
||||
# Woody overtone — gives it that hickory character
|
||||
body3 = numpy.sin(2 * numpy.pi * 2600 * t) * _exp_decay(n_samples, 95) * 0.12
|
||||
# Bright but slightly muffled transient (tape on wood)
|
||||
click_len = min(int(SAMPLE_RATE * 0.001), n_samples)
|
||||
click_raw = _noise(click_len)
|
||||
if click_len > 10:
|
||||
bl, al = scipy.signal.butter(2, [800, 7000], btype='band', fs=SAMPLE_RATE)
|
||||
click = scipy.signal.lfilter(bl, al, numpy.pad(click_raw, (0, max(0, n_samples - click_len))))[:click_len]
|
||||
else:
|
||||
click = click_raw
|
||||
click *= _exp_decay(click_len, 350) * 0.9
|
||||
result = body + body2 + body3
|
||||
result[:click_len] += click
|
||||
return numpy.tanh(result * 2.8)
|
||||
|
||||
|
||||
def _synth_quad(n_samples, pitch=300):
|
||||
"""Marching tenor/quad drum — tuned mylar head, bright and ringy.
|
||||
|
||||
Quads have a distinctive metallic ting from the high-tension
|
||||
mylar head and aluminum shell. More ring than a kit tom,
|
||||
brighter attack, clear pitch.
|
||||
"""
|
||||
t = numpy.arange(n_samples, dtype=numpy.float32) / SAMPLE_RATE
|
||||
# Pitched body — more ring/sustain than snare
|
||||
body = numpy.sin(2 * numpy.pi * pitch * t) * _exp_decay(n_samples, 22) * 0.5
|
||||
# Metallic overtones — the ting
|
||||
ting = numpy.sin(2 * numpy.pi * pitch * 2.3 * t) * _exp_decay(n_samples, 35) * 0.25
|
||||
ting2 = numpy.sin(2 * numpy.pi * pitch * 3.1 * t) * _exp_decay(n_samples, 45) * 0.12
|
||||
# Shell ring
|
||||
shell = numpy.sin(2 * numpy.pi * pitch * 4.7 * t) * _exp_decay(n_samples, 55) * 0.06
|
||||
# Sharp stick attack
|
||||
click_len = min(int(SAMPLE_RATE * 0.001), n_samples)
|
||||
click = _noise(click_len) * _exp_decay(click_len, 400) * 0.8
|
||||
result = body + ting + ting2 + shell
|
||||
result[:click_len] += click
|
||||
return numpy.tanh(result * 2.5)
|
||||
|
||||
|
||||
def _synth_quad_spock(n_samples):
|
||||
"""Quad spock — rim shot on the tenor shell. Bright, ringy, cutting."""
|
||||
t = numpy.arange(n_samples, dtype=numpy.float32) / SAMPLE_RATE
|
||||
ring = numpy.sin(2 * numpy.pi * 1400 * t) * _exp_decay(n_samples, 40) * 0.5
|
||||
ring2 = numpy.sin(2 * numpy.pi * 2100 * t) * _exp_decay(n_samples, 55) * 0.25
|
||||
click_len = min(int(SAMPLE_RATE * 0.001), n_samples)
|
||||
click = _noise(click_len) * _exp_decay(click_len, 400) * 1.0
|
||||
result = ring + ring2
|
||||
result[:click_len] += click
|
||||
return numpy.tanh(result * 2.8)
|
||||
|
||||
|
||||
def _synth_march_bass(n_samples, pitch=60):
|
||||
"""Marching bass drum — deep, boomy, pitched, felt beater thwack.
|
||||
|
||||
The beater hitting the head is a big part of the sound — a round,
|
||||
pillowy thwack followed by the deep pitched boom. More beater
|
||||
sound than a kit bass drum because marching bass drums project
|
||||
outward.
|
||||
"""
|
||||
t = numpy.arange(n_samples, dtype=numpy.float32) / SAMPLE_RATE
|
||||
# Deep pitched body — sustains and rings
|
||||
body = numpy.sin(2 * numpy.pi * pitch * t) * _exp_decay(n_samples, 10) * 0.7
|
||||
body2 = numpy.sin(2 * numpy.pi * pitch * 2 * t) * _exp_decay(n_samples, 16) * 0.2
|
||||
# Sub thump
|
||||
sub = numpy.sin(2 * numpy.pi * pitch * 0.5 * t) * _exp_decay(n_samples, 8) * 0.3
|
||||
# BIG beater thwack — dominant part of the attack
|
||||
thwack_len = min(int(SAMPLE_RATE * 0.025), n_samples)
|
||||
thwack_raw = _noise(thwack_len)
|
||||
if thwack_len > 10:
|
||||
bl, al = scipy.signal.butter(2, [150, 2500], btype='band', fs=SAMPLE_RATE)
|
||||
thwack = scipy.signal.lfilter(bl, al, numpy.pad(thwack_raw, (0, max(0, n_samples - thwack_len))))[:thwack_len]
|
||||
else:
|
||||
thwack = thwack_raw
|
||||
thwack *= _exp_decay(thwack_len, 55) * 1.5
|
||||
# Head slap — the mylar flexing on impact
|
||||
slap_len = min(int(SAMPLE_RATE * 0.008), n_samples)
|
||||
slap = numpy.sin(2 * numpy.pi * pitch * 3 * numpy.arange(slap_len, dtype=numpy.float32) / SAMPLE_RATE)
|
||||
slap *= _exp_decay(slap_len, 90) * 0.4
|
||||
result = body + body2 + sub
|
||||
result[:thwack_len] += thwack
|
||||
result[:slap_len] += slap
|
||||
return numpy.tanh(result * 2.0)
|
||||
|
||||
|
||||
def _synth_tabla_ge_bend(n_samples):
|
||||
"""Tabla Ge with upward pitch bend — palm pressing into bayan head.
|
||||
|
||||
@@ -3012,10 +3188,39 @@ def _render_drum_hit(sound_value, n_samples):
|
||||
DrumSound.METAL_KICK.value: lambda n: _synth_metal_kick(n),
|
||||
DrumSound.METAL_SNARE.value: lambda n: _synth_metal_snare(n),
|
||||
DrumSound.METAL_HAT.value: lambda n: _synth_metal_hat(n),
|
||||
# Marching
|
||||
DrumSound.MARCH_SNARE.value: lambda n: _synth_march_snare(n),
|
||||
DrumSound.MARCH_RIMSHOT.value: lambda n: _synth_march_rimshot(n),
|
||||
DrumSound.MARCH_CLICK.value: lambda n: _synth_march_click(n),
|
||||
# Quads (tenor drums) — pitched high to low
|
||||
DrumSound.QUAD_1.value: lambda n: _synth_quad(n, pitch=400),
|
||||
DrumSound.QUAD_2.value: lambda n: _synth_quad(n, pitch=330),
|
||||
DrumSound.QUAD_3.value: lambda n: _synth_quad(n, pitch=270),
|
||||
DrumSound.QUAD_4.value: lambda n: _synth_quad(n, pitch=220),
|
||||
DrumSound.QUAD_SPOCK.value: lambda n: _synth_quad_spock(n),
|
||||
# Marching bass drums — pitched high to low
|
||||
DrumSound.BASS_1.value: lambda n: _synth_march_bass(n, pitch=90),
|
||||
DrumSound.BASS_2.value: lambda n: _synth_march_bass(n, pitch=75),
|
||||
DrumSound.BASS_3.value: lambda n: _synth_march_bass(n, pitch=62),
|
||||
DrumSound.BASS_4.value: lambda n: _synth_march_bass(n, pitch=52),
|
||||
DrumSound.BASS_5.value: lambda n: _synth_march_bass(n, pitch=42),
|
||||
}
|
||||
|
||||
renderer = _dispatch.get(sound_value, lambda n: _synth_clave(n))
|
||||
return renderer(n_samples)
|
||||
result = renderer(n_samples)
|
||||
return result
|
||||
|
||||
|
||||
# Drum hit cache — same sound at same length sounds identical
|
||||
_drum_cache = {}
|
||||
|
||||
|
||||
def _render_drum_hit_cached(sound_value, n_samples):
|
||||
"""Cached version of _render_drum_hit for pattern playback."""
|
||||
key = (sound_value, n_samples)
|
||||
if key not in _drum_cache:
|
||||
_drum_cache[key] = _render_drum_hit(sound_value, n_samples)
|
||||
return _drum_cache[key].copy() # copy so callers can mutate
|
||||
|
||||
|
||||
def _render_pattern(pattern, bpm=120):
|
||||
@@ -3039,7 +3244,7 @@ def _render_pattern(pattern, bpm=120):
|
||||
remaining = total_samples - start
|
||||
# Render each hit for up to 0.5 seconds
|
||||
hit_len = min(int(SAMPLE_RATE * 0.5), remaining)
|
||||
wave = _render_drum_hit(hit.sound.value, hit_len)
|
||||
wave = _render_drum_hit_cached(hit.sound.value, hit_len)
|
||||
vel_scale = hit.velocity / 127.0
|
||||
buf[start:start + hit_len] += wave * vel_scale
|
||||
|
||||
@@ -4145,10 +4350,48 @@ def _render_notes_to_buf(notes, buf, samples_per_beat, total_samples,
|
||||
start += _rnd.randint(-max_offset, max_offset)
|
||||
start = max(0, start)
|
||||
dur_ms = note.beats * 60_000 / bpm
|
||||
# Articulation: adjust duration and velocity
|
||||
art = getattr(note, 'articulation', '')
|
||||
art_vel_mult = 1.0
|
||||
art_attack_mult = 1.0 # multiplier for envelope attack
|
||||
if art == 'staccato':
|
||||
dur_ms *= 0.4 # short and bouncy
|
||||
elif art == 'legato':
|
||||
dur_ms *= 1.15 # slight overlap into next note
|
||||
elif art == 'marcato':
|
||||
art_vel_mult = 1.25 # heavier
|
||||
art_attack_mult = 0.3 # sharper attack
|
||||
elif art == 'tenuto':
|
||||
art_attack_mult = 1.8 # softer attack, full duration
|
||||
elif art == 'accent':
|
||||
art_vel_mult = 1.2
|
||||
elif art == 'fermata':
|
||||
dur_ms *= 1.5 # held longer
|
||||
n_samples = int(SAMPLE_RATE * dur_ms / 1000)
|
||||
if start + n_samples > total_samples:
|
||||
n_samples = total_samples - start
|
||||
if n_samples > 0 and start >= 0:
|
||||
# Drum hit via Part.hit() — use drum synth directly
|
||||
from .rhythm import _DrumTone
|
||||
if isinstance(note.tone, _DrumTone):
|
||||
drum_wave = _render_drum_hit(note.tone.sound.value, n_samples)
|
||||
mixed = drum_wave.astype(numpy.float32)
|
||||
# Staccato fade-out for drums
|
||||
if art == 'staccato':
|
||||
fade_len = min(int(SAMPLE_RATE * 0.01), len(mixed))
|
||||
if fade_len > 0:
|
||||
mixed[-fade_len:] *= numpy.linspace(1.0, 0.0, fade_len).astype(numpy.float32)
|
||||
vel = getattr(note, 'velocity', 100)
|
||||
vel = min(127, int(vel * art_vel_mult))
|
||||
if humanize > 0.0:
|
||||
vel_jitter = int(humanize * 15)
|
||||
vel = max(1, min(127, vel + _rnd.randint(-vel_jitter, vel_jitter)))
|
||||
vel_scale = vel / 127.0
|
||||
end = min(start + len(mixed), total_samples)
|
||||
buf[start:end] += mixed[:end - start] * volume * vel_scale
|
||||
if not getattr(note, '_hold', False):
|
||||
beat_pos += note.beats
|
||||
continue
|
||||
# Get pitches
|
||||
if hasattr(note.tone, 'tones'):
|
||||
pitches = [t.pitch(temperament=temperament, reference_pitch=reference_pitch) for t in note.tone.tones]
|
||||
@@ -4249,11 +4492,18 @@ def _render_notes_to_buf(notes, buf, samples_per_beat, total_samples,
|
||||
if noise_mix > 0:
|
||||
noise = numpy.random.uniform(-1, 1, n_samples).astype(numpy.float32)
|
||||
mixed = mixed * (1.0 - noise_mix * 0.5) + noise * noise_mix * 0.5
|
||||
# Amplitude envelope
|
||||
if a > 0 or d > 0 or s < 1.0 or r > 0:
|
||||
mixed = _apply_envelope(mixed, a, d, s, r)
|
||||
# Per-note velocity
|
||||
# Amplitude envelope (articulation may adjust attack)
|
||||
art_a = a * art_attack_mult
|
||||
if art_a > 0 or d > 0 or s < 1.0 or r > 0:
|
||||
mixed = _apply_envelope(mixed, art_a, d, s, r)
|
||||
# Staccato: apply a quick fade-out at the end
|
||||
if art == 'staccato':
|
||||
fade_len = min(int(SAMPLE_RATE * 0.01), len(mixed))
|
||||
if fade_len > 0:
|
||||
mixed[-fade_len:] *= numpy.linspace(1.0, 0.0, fade_len).astype(numpy.float32)
|
||||
# Per-note velocity (articulation may boost)
|
||||
vel = getattr(note, 'velocity', 100)
|
||||
vel = min(127, int(vel * art_vel_mult))
|
||||
if humanize > 0.0:
|
||||
vel_jitter = int(humanize * 15)
|
||||
vel = max(1, min(127, vel + _rnd.randint(-vel_jitter, vel_jitter)))
|
||||
@@ -4451,35 +4701,71 @@ def render_score(score):
|
||||
synth_kwargs["mod_index"] = part.fm_index
|
||||
_temperament = getattr(score, 'temperament', 'equal')
|
||||
_ref_pitch = getattr(score, 'reference_pitch', 440.0)
|
||||
if part.legato:
|
||||
_render_legato_to_buf(
|
||||
part.notes, part_buf, samples_per_beat, total_samples,
|
||||
synth_fn, env_tuple, part.volume, score.bpm,
|
||||
glide_time=part.glide, swing=effective_swing,
|
||||
tempo_map=tempo_map if has_tempo_changes else None,
|
||||
temperament=_temperament, reference_pitch=_ref_pitch)
|
||||
else:
|
||||
_render_notes_to_buf(
|
||||
part.notes, part_buf, samples_per_beat, total_samples,
|
||||
synth_fn, env_tuple, part.volume, score.bpm,
|
||||
swing=effective_swing,
|
||||
tempo_map=tempo_map if has_tempo_changes else None,
|
||||
humanize=part.humanize,
|
||||
detune=part.detune,
|
||||
spread=part.spread,
|
||||
stereo_buf=stereo_buf,
|
||||
sub_osc=part.sub_osc,
|
||||
noise_mix=part.noise_mix,
|
||||
filter_attack=part.filter_attack,
|
||||
filter_decay=part.filter_decay,
|
||||
filter_sustain=part.filter_sustain,
|
||||
filter_amount=part.filter_amount,
|
||||
vel_to_filter=part.vel_to_filter,
|
||||
filter_q=part.lowpass_q,
|
||||
synth_kwargs=synth_kwargs,
|
||||
temperament=_temperament,
|
||||
reference_pitch=_ref_pitch,
|
||||
analog=part.analog)
|
||||
|
||||
n_ensemble = max(1, getattr(part, 'ensemble', 1))
|
||||
|
||||
for _ens_i in range(n_ensemble):
|
||||
# Each ensemble voice gets its own buffer
|
||||
ens_buf = part_buf if n_ensemble == 1 else numpy.zeros(total_samples, dtype=numpy.float32)
|
||||
# Ensemble voices get micro-variations
|
||||
ens_humanize = part.humanize
|
||||
ens_analog = part.analog
|
||||
if n_ensemble > 1:
|
||||
import random as _ens_rnd
|
||||
_ens_rnd.seed(42 + _ens_i * 7)
|
||||
# Hybrid approach:
|
||||
# 1. Consistent player tendency (rush/drag) — seeded per player
|
||||
_player_tendency = _ens_rnd.gauss(0, 0.018)
|
||||
# 2. Tiny per-note wobble on top
|
||||
ens_humanize = max(part.humanize, 0.012)
|
||||
# Each player's drum tuned slightly different
|
||||
ens_analog = max(part.analog, 0.06 + _ens_rnd.uniform(0, 0.08))
|
||||
|
||||
if part.legato:
|
||||
_render_legato_to_buf(
|
||||
part.notes, ens_buf, samples_per_beat, total_samples,
|
||||
synth_fn, env_tuple, part.volume, score.bpm,
|
||||
glide_time=part.glide, swing=effective_swing,
|
||||
tempo_map=tempo_map if has_tempo_changes else None,
|
||||
temperament=_temperament, reference_pitch=_ref_pitch)
|
||||
else:
|
||||
_render_notes_to_buf(
|
||||
part.notes, ens_buf, samples_per_beat, total_samples,
|
||||
synth_fn, env_tuple, part.volume, score.bpm,
|
||||
swing=effective_swing,
|
||||
tempo_map=tempo_map if has_tempo_changes else None,
|
||||
humanize=ens_humanize,
|
||||
detune=part.detune,
|
||||
spread=part.spread,
|
||||
stereo_buf=stereo_buf,
|
||||
sub_osc=part.sub_osc,
|
||||
noise_mix=part.noise_mix,
|
||||
filter_attack=part.filter_attack,
|
||||
filter_decay=part.filter_decay,
|
||||
filter_sustain=part.filter_sustain,
|
||||
filter_amount=part.filter_amount,
|
||||
vel_to_filter=part.vel_to_filter,
|
||||
filter_q=part.lowpass_q,
|
||||
synth_kwargs=synth_kwargs,
|
||||
temperament=_temperament,
|
||||
reference_pitch=_ref_pitch,
|
||||
analog=ens_analog)
|
||||
|
||||
if n_ensemble > 1:
|
||||
# Shift the whole voice by the player's consistent tendency
|
||||
# (some players rush, some drag — this is fixed per player)
|
||||
shift_samples = int(_player_tendency * samples_per_beat)
|
||||
if shift_samples > 0 and shift_samples < total_samples:
|
||||
# Player drags — shift right
|
||||
shifted = numpy.zeros_like(ens_buf)
|
||||
shifted[shift_samples:] = ens_buf[:-shift_samples]
|
||||
ens_buf = shifted
|
||||
elif shift_samples < 0 and abs(shift_samples) < total_samples:
|
||||
# Player rushes — shift left
|
||||
shifted = numpy.zeros_like(ens_buf)
|
||||
shifted[:shift_samples] = ens_buf[-shift_samples:]
|
||||
ens_buf = shifted
|
||||
part_buf += ens_buf / n_ensemble
|
||||
|
||||
# Apply effects — segmented if automation exists
|
||||
auto_points = part._get_automation_points()
|
||||
@@ -4612,6 +4898,22 @@ def render_score(score):
|
||||
DrumSound.METAL_KICK.value: 0.0,
|
||||
DrumSound.METAL_SNARE.value: 0.0,
|
||||
DrumSound.METAL_HAT.value: 0.3,
|
||||
# Marching — centered
|
||||
DrumSound.MARCH_SNARE.value: 0.0,
|
||||
DrumSound.MARCH_RIMSHOT.value: 0.0,
|
||||
DrumSound.MARCH_CLICK.value: 0.0,
|
||||
# Quads — spread across the field
|
||||
DrumSound.QUAD_1.value: -0.3,
|
||||
DrumSound.QUAD_2.value: -0.1,
|
||||
DrumSound.QUAD_3.value: 0.1,
|
||||
DrumSound.QUAD_4.value: 0.3,
|
||||
DrumSound.QUAD_SPOCK.value: 0.0,
|
||||
# Bass drums — spread wide
|
||||
DrumSound.BASS_1.value: -0.5,
|
||||
DrumSound.BASS_2.value: -0.25,
|
||||
DrumSound.BASS_3.value: 0.0,
|
||||
DrumSound.BASS_4.value: 0.25,
|
||||
DrumSound.BASS_5.value: 0.5,
|
||||
}
|
||||
|
||||
# Render all drum Parts (may be one "drums" or split into kick/snare/hats/etc.)
|
||||
@@ -4627,6 +4929,7 @@ def render_score(score):
|
||||
# Track last hit position per sound for choke (new hit dampens
|
||||
# the previous ring on the same drum)
|
||||
_last_hit_start = {}
|
||||
_resonance = {} # sound_id → resonance level (0.0–1.0)
|
||||
|
||||
for hit in drum_part._drum_hits:
|
||||
pos = hit.position
|
||||
@@ -4690,12 +4993,45 @@ def render_score(score):
|
||||
|
||||
remaining = total_samples - start
|
||||
hit_len = min(int(SAMPLE_RATE * 0.5), remaining)
|
||||
wave = _render_drum_hit(hit.sound.value, hit_len)
|
||||
wave = _render_drum_hit_cached(hit.sound.value, hit_len)
|
||||
vel = hit.velocity
|
||||
if drum_humanize > 0:
|
||||
vel_jitter = int(drum_humanize * 10)
|
||||
vel = max(1, min(127, vel + _drum_rnd.randint(-vel_jitter, vel_jitter)))
|
||||
vel_scale = vel / 127.0
|
||||
|
||||
# Sympathetic resonance: marching snare builds up buzz
|
||||
# as hits accumulate. Each hit adds to a resonance counter
|
||||
# that scales extra snare wire buzz into the sound.
|
||||
_RESONANCE_SOUNDS = {
|
||||
DrumSound.MARCH_SNARE.value, DrumSound.MARCH_RIMSHOT.value,
|
||||
}
|
||||
if sound_id in _RESONANCE_SOUNDS:
|
||||
reso = _resonance.get(sound_id, 0.0)
|
||||
# Decay based on gap since last hit
|
||||
if sound_id in _last_hit_start:
|
||||
gap_samples = start - _last_hit_start[sound_id]
|
||||
gap_sec = gap_samples / SAMPLE_RATE
|
||||
if gap_sec > 1.0:
|
||||
reso *= 0.2
|
||||
elif gap_sec > 0.5:
|
||||
reso *= 0.5
|
||||
elif gap_sec > 0.25:
|
||||
reso *= 0.8
|
||||
# Build up (caps at 0.6)
|
||||
reso = min(0.6, reso + 0.08)
|
||||
_resonance[sound_id] = reso
|
||||
# Add sympathetic buzz proportional to resonance
|
||||
if reso > 0.1:
|
||||
buzz_len = min(int(SAMPLE_RATE * 0.06), hit_len)
|
||||
buzz = _noise(buzz_len) * reso * 0.18
|
||||
if buzz_len > 20:
|
||||
bl, al = scipy.signal.butter(
|
||||
2, [3000, 9000], btype='band', fs=SAMPLE_RATE)
|
||||
buzz = scipy.signal.lfilter(bl, al, buzz)
|
||||
buzz *= _exp_decay(buzz_len, 25)
|
||||
wave[:buzz_len] = wave[:buzz_len] + buzz.astype(numpy.float32)
|
||||
|
||||
mono_hit = wave * vel_scale * 0.7
|
||||
# Sidechain trigger — kick only
|
||||
if hit.sound.value == DrumSound.KICK.value:
|
||||
|
||||
+579
-5
@@ -449,6 +449,7 @@ class Note:
|
||||
bend: float = 0.0
|
||||
bend_type: str = "smooth" # "smooth" (log), "linear", "late"
|
||||
lyric: str = "" # syllable for vocal synth
|
||||
articulation: str = "" # "", "staccato", "legato", "marcato", "tenuto", "accent", "fermata"
|
||||
_hold: bool = False # if True, don't advance beat position
|
||||
|
||||
@property
|
||||
@@ -563,6 +564,33 @@ class DrumSound(Enum):
|
||||
METAL_KICK = 105 # clicky, punchy, tight
|
||||
METAL_SNARE = 106 # crack, bright, cutting
|
||||
METAL_HAT = 107 # tight, short, precise
|
||||
# Marching percussion
|
||||
MARCH_SNARE = 115 # tight, high-tension kevlar head, snare buzz
|
||||
MARCH_RIMSHOT = 116 # stick hits rim + head simultaneously, cracking
|
||||
MARCH_CLICK = 118 # stick click — sticks hit together, no drum
|
||||
# Quads (tenor drums) — 4 drums high to low + spock (rim)
|
||||
QUAD_1 = 119 # highest tenor drum
|
||||
QUAD_2 = 120 # second tenor
|
||||
QUAD_3 = 121 # third tenor
|
||||
QUAD_4 = 122 # lowest tenor (floor tom-ish)
|
||||
QUAD_SPOCK = 123 # rim click on quad shell
|
||||
# Marching bass drums — 5 drums pitched high to low
|
||||
BASS_1 = 124 # highest (smallest) bass drum
|
||||
BASS_2 = 125 # second
|
||||
BASS_3 = 126 # middle
|
||||
BASS_4 = 127 # fourth
|
||||
BASS_5 = 80 # lowest (biggest) bass drum
|
||||
|
||||
|
||||
class _DrumTone:
|
||||
"""Wrapper so a DrumSound can be placed in a Part's note list."""
|
||||
__slots__ = ('sound',)
|
||||
|
||||
def __init__(self, sound: DrumSound):
|
||||
self.sound = sound
|
||||
|
||||
def pitch(self, **kwargs):
|
||||
return -self.sound.value
|
||||
|
||||
|
||||
class _Hit:
|
||||
@@ -1590,6 +1618,231 @@ Pattern._PRESETS["tabla solo"] = dict(
|
||||
],
|
||||
)
|
||||
|
||||
# ── Marching snare patterns ───────────────────────────────────────────────
|
||||
MS = DrumSound.MARCH_SNARE
|
||||
MR = DrumSound.MARCH_RIMSHOT
|
||||
MC = DrumSound.MARCH_CLICK
|
||||
Q1 = DrumSound.QUAD_1
|
||||
Q2 = DrumSound.QUAD_2
|
||||
Q3 = DrumSound.QUAD_3
|
||||
Q4 = DrumSound.QUAD_4
|
||||
QS = DrumSound.QUAD_SPOCK
|
||||
B1 = DrumSound.BASS_1
|
||||
B2 = DrumSound.BASS_2
|
||||
B3 = DrumSound.BASS_3
|
||||
B4 = DrumSound.BASS_4
|
||||
B5 = DrumSound.BASS_5
|
||||
|
||||
# Marching basic — standard 4/4 march with rimshot accents on 2 and 4
|
||||
Pattern._PRESETS["march"] = dict(
|
||||
name="march",
|
||||
time_signature="4/4",
|
||||
beats=4.0,
|
||||
hits=[
|
||||
_h(MS, 0.0, 80), _h(MS, 0.5, 55),
|
||||
_h(MR, 1.0, 100), _h(MS, 1.5, 55),
|
||||
_h(MS, 2.0, 80), _h(MS, 2.5, 55),
|
||||
_h(MR, 3.0, 100), _h(MS, 3.5, 55),
|
||||
],
|
||||
)
|
||||
|
||||
# Cadence — 8-beat street beat pattern (the classic drumline cadence)
|
||||
Pattern._PRESETS["cadence"] = dict(
|
||||
name="cadence",
|
||||
time_signature="4/4",
|
||||
beats=8.0,
|
||||
hits=[
|
||||
# Bar 1: syncopated groove
|
||||
_h(MR, 0.0, 105), _h(MS, 0.25, 60), _h(MS, 0.5, 65),
|
||||
_h(MS, 0.75, 55), _h(MR, 1.0, 100),
|
||||
_h(MS, 1.5, 60), _h(MS, 1.75, 58),
|
||||
_h(MR, 2.0, 105), _h(MS, 2.5, 62),
|
||||
_h(MS, 2.75, 55), _h(MR, 3.0, 100),
|
||||
_h(MS, 3.25, 58), _h(MS, 3.5, 60), _h(MS, 3.75, 55),
|
||||
# Bar 2: answer phrase with flams
|
||||
_h(MR, 4.0, 110), _h(MS, 4.25, 62), _h(MS, 4.5, 65),
|
||||
_h(MR, 5.0, 105), _h(MS, 5.25, 58),
|
||||
_h(MS, 5.5, 60), _h(MS, 5.75, 55),
|
||||
_h(MR, 6.0, 110), _h(MS, 6.25, 62),
|
||||
_h(MS, 6.5, 65), _h(MS, 6.75, 62),
|
||||
_h(MR, 7.0, 115), _h(MS, 7.25, 60),
|
||||
_h(MR, 7.5, 110), _h(MR, 7.75, 115),
|
||||
],
|
||||
)
|
||||
|
||||
# Paradiddle — RLRR LRLL on marching snare
|
||||
Pattern._PRESETS["march paradiddle"] = dict(
|
||||
name="march paradiddle",
|
||||
time_signature="4/4",
|
||||
beats=4.0,
|
||||
hits=[
|
||||
# RLRR (R=rimshot accent, L=tap)
|
||||
_h(MR, 0.0, 100), _h(MS, 0.25, 58), _h(MR, 0.5, 65), _h(MR, 0.75, 62),
|
||||
# LRLL
|
||||
_h(MS, 1.0, 58), _h(MR, 1.25, 100), _h(MS, 1.5, 58), _h(MS, 1.75, 55),
|
||||
# RLRR
|
||||
_h(MR, 2.0, 102), _h(MS, 2.25, 60), _h(MR, 2.5, 68), _h(MR, 2.75, 65),
|
||||
# LRLL
|
||||
_h(MS, 3.0, 60), _h(MR, 3.25, 102), _h(MS, 3.5, 60), _h(MS, 3.75, 58),
|
||||
],
|
||||
)
|
||||
|
||||
# March roll — buzz roll crescendo
|
||||
Pattern._PRESETS["march roll"] = dict(
|
||||
name="march roll",
|
||||
time_signature="4/4",
|
||||
beats=4.0,
|
||||
hits=[
|
||||
# Buzz roll as rapid 32nds, crescendo
|
||||
*[_h(MS, i * 0.125, 40 + i * 3) for i in range(28)],
|
||||
# Land on rimshot
|
||||
_h(MR, 3.5, 115), _h(MR, 3.75, 120),
|
||||
],
|
||||
)
|
||||
|
||||
# Quad sweep — run across all 4 drums
|
||||
Pattern._PRESETS["quad sweep"] = dict(
|
||||
name="quad sweep",
|
||||
time_signature="4/4",
|
||||
beats=4.0,
|
||||
hits=[
|
||||
# Sweep down
|
||||
_h(Q1, 0.0, 95), _h(Q2, 0.25, 90), _h(Q3, 0.5, 85), _h(Q4, 0.75, 80),
|
||||
# Sweep up
|
||||
_h(Q4, 1.0, 80), _h(Q3, 1.25, 85), _h(Q2, 1.5, 90), _h(Q1, 1.75, 95),
|
||||
# Double sweep with spocks
|
||||
_h(Q1, 2.0, 98), _h(Q2, 2.125, 92), _h(Q3, 2.25, 88), _h(Q4, 2.375, 82),
|
||||
_h(Q4, 2.5, 82), _h(Q3, 2.625, 88), _h(Q2, 2.75, 92), _h(Q1, 2.875, 98),
|
||||
# Spock accents
|
||||
_h(QS, 3.0, 105), _h(Q1, 3.25, 90), _h(QS, 3.5, 105), _h(Q4, 3.75, 85),
|
||||
],
|
||||
)
|
||||
|
||||
# Quad groove — accented pattern with sweeps
|
||||
Pattern._PRESETS["quad groove"] = dict(
|
||||
name="quad groove",
|
||||
time_signature="4/4",
|
||||
beats=4.0,
|
||||
hits=[
|
||||
_h(Q1, 0.0, 100), _h(Q3, 0.25, 55), _h(Q1, 0.5, 60),
|
||||
_h(Q2, 0.75, 55), _h(Q3, 1.0, 95), _h(Q1, 1.25, 55),
|
||||
_h(Q4, 1.5, 58), _h(Q2, 1.75, 55),
|
||||
_h(Q1, 2.0, 100), _h(Q2, 2.25, 55), _h(Q3, 2.5, 58),
|
||||
_h(Q4, 2.75, 55), _h(QS, 3.0, 105), _h(Q3, 3.25, 55),
|
||||
_h(Q2, 3.5, 58), _h(Q1, 3.75, 60),
|
||||
],
|
||||
)
|
||||
|
||||
# Bass split — classic bass drum splits across the line
|
||||
Pattern._PRESETS["bass split"] = dict(
|
||||
name="bass split",
|
||||
time_signature="4/4",
|
||||
beats=4.0,
|
||||
hits=[
|
||||
# Each bass drum takes a 16th, cascading down then up
|
||||
_h(B1, 0.0, 95), _h(B2, 0.25, 90), _h(B3, 0.5, 85),
|
||||
_h(B4, 0.75, 80), _h(B5, 1.0, 95),
|
||||
_h(B5, 1.5, 90), _h(B4, 1.75, 85),
|
||||
_h(B3, 2.0, 95), _h(B2, 2.25, 90), _h(B1, 2.5, 95),
|
||||
_h(B1, 2.75, 85), _h(B3, 3.0, 100),
|
||||
_h(B5, 3.25, 95), _h(B3, 3.5, 90), _h(B1, 3.75, 95),
|
||||
],
|
||||
)
|
||||
|
||||
# Bass unison — all bass drums hit together on accents
|
||||
Pattern._PRESETS["bass unison"] = dict(
|
||||
name="bass unison",
|
||||
time_signature="4/4",
|
||||
beats=4.0,
|
||||
hits=[
|
||||
# All 5 hit on beat 1
|
||||
_h(B1, 0.0, 100), _h(B2, 0.0, 100), _h(B3, 0.0, 100),
|
||||
_h(B4, 0.0, 100), _h(B5, 0.0, 100),
|
||||
# Split on beat 2
|
||||
_h(B1, 1.0, 90), _h(B3, 1.25, 85), _h(B5, 1.5, 90),
|
||||
# All on beat 3
|
||||
_h(B1, 2.0, 100), _h(B2, 2.0, 100), _h(B3, 2.0, 100),
|
||||
_h(B4, 2.0, 100), _h(B5, 2.0, 100),
|
||||
# Cascade into beat 4
|
||||
_h(B5, 2.75, 80), _h(B4, 3.0, 85), _h(B3, 3.25, 90),
|
||||
_h(B2, 3.5, 95), _h(B1, 3.75, 100),
|
||||
],
|
||||
)
|
||||
|
||||
# Full drumline — snare + quads + bass together
|
||||
Pattern._PRESETS["drumline"] = dict(
|
||||
name="drumline",
|
||||
time_signature="4/4",
|
||||
beats=4.0,
|
||||
hits=[
|
||||
# Snare backbone
|
||||
_h(MR, 0.0, 115), _h(MS, 0.25, 35), _h(MS, 0.5, 38), _h(MS, 0.75, 32),
|
||||
_h(MR, 1.0, 112), _h(MS, 1.25, 35), _h(MS, 1.5, 32), _h(MS, 1.75, 38),
|
||||
_h(MR, 2.0, 115), _h(MS, 2.25, 38), _h(MS, 2.5, 32), _h(MS, 2.75, 35),
|
||||
_h(MR, 3.0, 118), _h(MS, 3.25, 35), _h(MS, 3.5, 32), _h(MS, 3.75, 38),
|
||||
# Quads on accents
|
||||
_h(Q1, 0.0, 95), _h(Q3, 0.5, 55), _h(Q2, 1.0, 90),
|
||||
_h(Q4, 1.5, 55), _h(Q1, 2.0, 95), _h(Q3, 2.5, 55),
|
||||
_h(QS, 3.0, 100), _h(Q2, 3.5, 55),
|
||||
# Bass on the big beats
|
||||
_h(B3, 0.0, 100), _h(B5, 1.0, 95),
|
||||
_h(B1, 2.0, 100), _h(B3, 3.0, 95),
|
||||
],
|
||||
)
|
||||
|
||||
# Chakradar — tihai of tihais (16 beats / 4 bars)
|
||||
# A phrase (Dha Tit Tit Dha Ge Na) is played 3x with increasing intensity,
|
||||
# and within each repetition the final 3 hits form a mini-tihai landing on sam.
|
||||
_chakra_phrase_a = [
|
||||
# Phrase 1 (4 beats): moderate
|
||||
_h(TDHA, 0.0, 85), _h(TTIT, 0.25, 48), _h(TTIT, 0.5, 50),
|
||||
_h(TDHA, 0.75, 80), _h(TGE, 1.0, 72),
|
||||
_h(TNA, 1.5, 68), _h(TTIT, 1.75, 45),
|
||||
# Mini-tihai: Na Dha, Na Dha, Na Dha
|
||||
_h(TNA, 2.0, 72), _h(TDHA, 2.25, 82),
|
||||
_h(TNA, 2.5, 75), _h(TDHA, 2.75, 85),
|
||||
_h(TNA, 3.0, 78), _h(TDHA, 3.25, 90),
|
||||
_h(TTIT, 3.5, 42), _h(TTIT, 3.75, 45),
|
||||
]
|
||||
_chakra_phrase_b = [
|
||||
# Phrase 2 (4 beats): louder, busier
|
||||
_h(TDHA, 4.0, 95), _h(TTIT, 4.125, 50), _h(TTIT, 4.25, 52),
|
||||
_h(TKE, 4.375, 48), _h(TDHA, 4.5, 90), _h(TGE, 4.75, 78),
|
||||
_h(TNA, 5.0, 75), _h(TTIT, 5.25, 48), _h(TTIT, 5.5, 50),
|
||||
_h(TNA, 5.75, 72),
|
||||
# Mini-tihai: Na Dha Ge, Na Dha Ge, Na Dha Ge
|
||||
_h(TNA, 6.0, 80), _h(TDHA, 6.25, 92), _h(TGE, 6.5, 78),
|
||||
_h(TNA, 6.75, 82), _h(TDHA, 7.0, 95), _h(TGE, 7.25, 82),
|
||||
_h(TNA, 7.5, 85), _h(TDHA, 7.75, 100),
|
||||
]
|
||||
_chakra_phrase_c = [
|
||||
# Phrase 3 (4 beats): peak intensity, fastest
|
||||
_h(TDHA, 8.0, 105), _h(TTIT, 8.125, 55), _h(TTIT, 8.25, 58),
|
||||
_h(TKE, 8.375, 52), _h(TNA, 8.5, 85), _h(TTIT, 8.625, 50),
|
||||
_h(TDHA, 8.75, 100), _h(TGE, 9.0, 85),
|
||||
_h(TNA, 9.25, 82), _h(TTIT, 9.5, 55), _h(TTIT, 9.625, 58),
|
||||
_h(TKE, 9.75, 52), _h(TNA, 10.0, 88),
|
||||
# Final tihai — 3x Dha Tit Na Dha landing on sam
|
||||
_h(TDHA, 10.5, 105), _h(TTIT, 10.625, 58), _h(TNA, 10.75, 90),
|
||||
_h(TDHA, 11.0, 108), _h(TTIT, 11.125, 60), _h(TNA, 11.25, 92),
|
||||
_h(TDHA, 11.5, 112), _h(TTIT, 11.625, 62), _h(TNA, 11.75, 95),
|
||||
]
|
||||
_chakra_finale = [
|
||||
# Bar 4: crescendo triplets into massive sam
|
||||
*[_h(TTIT, 12.0 + i * (1/6), 50 + i * 5) for i in range(12)],
|
||||
_h(TDHA, 14.0, 115), _h(TNA, 14.25, 85), _h(TDHA, 14.5, 118),
|
||||
_h(TNA, 14.75, 88),
|
||||
_h(TDHA, 15.0, 120), _h(TGE, 15.25, 95),
|
||||
_h(TDHA, 15.5, 125), _h(DrumSound.TABLA_GE_BEND, 15.75, 110),
|
||||
]
|
||||
|
||||
Pattern._PRESETS["chakradar"] = dict(
|
||||
name="chakradar",
|
||||
time_signature="4/4",
|
||||
beats=16.0,
|
||||
hits=_chakra_phrase_a + _chakra_phrase_b + _chakra_phrase_c + _chakra_finale,
|
||||
)
|
||||
|
||||
# ── Doumbek patterns ──────────────────────────────────────────────────────
|
||||
DKD = DrumSound.DOUMBEK_DUM
|
||||
DKT = DrumSound.DOUMBEK_TEK
|
||||
@@ -2568,6 +2821,7 @@ class Part:
|
||||
cabinet: float = 0.0,
|
||||
cabinet_brightness: float = 0.5,
|
||||
analog: float = 0.0,
|
||||
ensemble: int = 1,
|
||||
fm_ratio: float = 2.0,
|
||||
fm_index: float = 3.0):
|
||||
self.name = name
|
||||
@@ -2614,6 +2868,7 @@ class Part:
|
||||
self.cabinet = cabinet
|
||||
self.cabinet_brightness = cabinet_brightness
|
||||
self.analog = analog
|
||||
self.ensemble = ensemble
|
||||
self.fm_ratio = fm_ratio
|
||||
self.fm_index = fm_index
|
||||
self._system = "western" # default, overridden by Score.part()
|
||||
@@ -2624,7 +2879,8 @@ class Part:
|
||||
self._automation: list[tuple[float, dict]] = [] # (beat, {param: value})
|
||||
|
||||
def add(self, tone_or_string, duration=Duration.QUARTER, *, velocity: int = 100,
|
||||
bend: float = 0.0, bend_type: str = "smooth", lyric: str = "") -> "Part":
|
||||
bend: float = 0.0, bend_type: str = "smooth", lyric: str = "",
|
||||
articulation: 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).
|
||||
@@ -2632,6 +2888,10 @@ class Part:
|
||||
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.
|
||||
Articulation changes how the note is played: ``"staccato"`` (short,
|
||||
~40% duration), ``"legato"`` (overlaps next note), ``"marcato"``
|
||||
(heavy accent), ``"tenuto"`` (full duration, soft attack),
|
||||
``"accent"`` (velocity bump), ``"fermata"`` (held ~50% longer).
|
||||
|
||||
Returns self for chaining.
|
||||
"""
|
||||
@@ -2642,11 +2902,13 @@ class Part:
|
||||
duration = _RawDuration(duration)
|
||||
self.notes.append(Note(tone=tone_or_string, duration=duration,
|
||||
velocity=velocity, bend=bend,
|
||||
bend_type=bend_type, lyric=lyric))
|
||||
bend_type=bend_type, lyric=lyric,
|
||||
articulation=articulation))
|
||||
return self
|
||||
|
||||
def hold(self, tone_or_string, duration=Duration.QUARTER, *, velocity: int = 100,
|
||||
bend: float = 0.0, bend_type: str = "smooth", lyric: str = "") -> "Part":
|
||||
bend: float = 0.0, bend_type: str = "smooth", lyric: str = "",
|
||||
articulation: str = "") -> "Part":
|
||||
"""Add a note without advancing the beat position.
|
||||
|
||||
The note plays at the current position but the next note
|
||||
@@ -2671,9 +2933,247 @@ class Part:
|
||||
duration = _RawDuration(duration)
|
||||
self.notes.append(Note(tone=tone_or_string, duration=duration,
|
||||
velocity=velocity, bend=bend,
|
||||
bend_type=bend_type, lyric=lyric, _hold=True))
|
||||
bend_type=bend_type, lyric=lyric,
|
||||
articulation=articulation, _hold=True))
|
||||
return self
|
||||
|
||||
def hit(self, sound, duration=Duration.EIGHTH, *, velocity: int = 100,
|
||||
articulation: str = "") -> "Part":
|
||||
"""Add a drum hit to this part.
|
||||
|
||||
Places a drum sound into the note stream so it goes through the
|
||||
normal renderer — meaning articulations, humanize, and effects
|
||||
all work on individual hits.
|
||||
|
||||
Args:
|
||||
sound: A :class:`DrumSound` enum member (e.g. ``DrumSound.KICK``).
|
||||
duration: How long the hit occupies in the timeline (default 8th note).
|
||||
velocity: Hit loudness 1-127.
|
||||
articulation: ``"accent"``, ``"staccato"``, ``"marcato"``, etc.
|
||||
|
||||
Example::
|
||||
|
||||
>>> drums = score.part("kit", synth="sine")
|
||||
>>> drums.hit(DrumSound.KICK, Duration.QUARTER, articulation="accent")
|
||||
>>> drums.hit(DrumSound.CLOSED_HAT, Duration.EIGHTH)
|
||||
"""
|
||||
if isinstance(duration, (int, float)):
|
||||
duration = _RawDuration(duration)
|
||||
self.notes.append(Note(tone=_DrumTone(sound), duration=duration,
|
||||
velocity=velocity, articulation=articulation))
|
||||
return self
|
||||
|
||||
def flam(self, sound, duration=Duration.QUARTER, *, velocity: int = 110,
|
||||
gap: float = 0.015, grace_vel: float = 0.3,
|
||||
articulation: str = "") -> "Part":
|
||||
"""Add a flam — a grace note immediately before the main hit.
|
||||
|
||||
The grace note is nearly simultaneous with the main hit,
|
||||
thickening the attack. Tighter gap = more like one fat hit,
|
||||
wider gap = audible double.
|
||||
|
||||
Args:
|
||||
sound: A :class:`DrumSound` enum member.
|
||||
duration: Total duration the flam occupies.
|
||||
velocity: Main hit velocity.
|
||||
gap: Beats between grace and main (default 0.008 ≈ 4ms at 120).
|
||||
grace_vel: Grace note velocity as fraction of main (default 0.3).
|
||||
articulation: Optional articulation for the main hit.
|
||||
|
||||
Example::
|
||||
|
||||
>>> p.flam(DrumSound.MARCH_SNARE, Duration.QUARTER, velocity=120)
|
||||
"""
|
||||
if isinstance(duration, (int, float)):
|
||||
dur_val = duration
|
||||
else:
|
||||
dur_val = duration.value if hasattr(duration, 'value') else float(duration)
|
||||
self.hit(sound, gap, velocity=int(velocity * grace_vel))
|
||||
self.hit(sound, dur_val - gap, velocity=velocity, articulation=articulation)
|
||||
return self
|
||||
|
||||
def diddle(self, sound, duration=Duration.EIGHTH, *,
|
||||
velocity: int = 70) -> "Part":
|
||||
"""Add a diddle — two equal strokes in the space of one note.
|
||||
|
||||
A double-stroke roll building block. Two hits split evenly
|
||||
across the duration.
|
||||
|
||||
Args:
|
||||
sound: A :class:`DrumSound` enum member.
|
||||
duration: Total duration (default 8th note). Each stroke
|
||||
gets half.
|
||||
velocity: Velocity for both strokes.
|
||||
|
||||
Example::
|
||||
|
||||
>>> p.diddle(DrumSound.MARCH_SNARE, Duration.EIGHTH, velocity=60)
|
||||
"""
|
||||
if isinstance(duration, (int, float)):
|
||||
dur_val = duration
|
||||
else:
|
||||
dur_val = duration.value if hasattr(duration, 'value') else float(duration)
|
||||
half = dur_val / 2
|
||||
self.hit(sound, half, velocity=velocity)
|
||||
self.hit(sound, half, velocity=int(velocity * 0.9))
|
||||
return self
|
||||
|
||||
def cheese(self, sound, duration=Duration.QUARTER, *, velocity: int = 110,
|
||||
gap: float = 0.008, grace_vel: float = 0.3) -> "Part":
|
||||
"""Add a cheese — a flam followed by a diddle.
|
||||
|
||||
Common marching rudiment: grace-MAIN-tap-tap.
|
||||
|
||||
Args:
|
||||
sound: A :class:`DrumSound` enum member.
|
||||
duration: Total duration.
|
||||
velocity: Main hit velocity.
|
||||
"""
|
||||
if isinstance(duration, (int, float)):
|
||||
dur_val = duration
|
||||
else:
|
||||
dur_val = duration.value if hasattr(duration, 'value') else float(duration)
|
||||
# Flam takes first half, diddle takes second half
|
||||
flam_dur = dur_val * 0.5
|
||||
diddle_dur = dur_val * 0.5
|
||||
self.hit(sound, gap, velocity=int(velocity * grace_vel))
|
||||
self.hit(sound, flam_dur - gap, velocity=velocity)
|
||||
self.hit(sound, diddle_dur / 2, velocity=int(velocity * 0.5))
|
||||
self.hit(sound, diddle_dur / 2, velocity=int(velocity * 0.45))
|
||||
return self
|
||||
|
||||
def crescendo(self, notes, duration=Duration.QUARTER, *,
|
||||
start_vel: int = 40, end_vel: int = 110,
|
||||
articulation: str = "") -> "Part":
|
||||
"""Add notes with velocity ramping up (getting louder).
|
||||
|
||||
Args:
|
||||
notes: List of note strings (e.g. ``["C4", "D4", "E4"]``).
|
||||
duration: Duration for each note.
|
||||
start_vel: Starting velocity (quiet).
|
||||
end_vel: Ending velocity (loud).
|
||||
articulation: Optional articulation for all notes.
|
||||
|
||||
Example::
|
||||
|
||||
>>> piano.crescendo(["C4","D4","E4","F4","G4"], Duration.QUARTER,
|
||||
... start_vel=40, end_vel=110)
|
||||
"""
|
||||
return self.dynamics(notes, duration, velocities=(start_vel, end_vel),
|
||||
articulation=articulation)
|
||||
|
||||
def decrescendo(self, notes, duration=Duration.QUARTER, *,
|
||||
start_vel: int = 110, end_vel: int = 40,
|
||||
articulation: str = "") -> "Part":
|
||||
"""Add notes with velocity ramping down (getting quieter).
|
||||
|
||||
Args:
|
||||
notes: List of note strings.
|
||||
duration: Duration for each note.
|
||||
start_vel: Starting velocity (loud).
|
||||
end_vel: Ending velocity (quiet).
|
||||
articulation: Optional articulation for all notes.
|
||||
|
||||
Example::
|
||||
|
||||
>>> piano.decrescendo(["G4","F4","E4","D4","C4"], Duration.QUARTER,
|
||||
... start_vel=110, end_vel=40)
|
||||
"""
|
||||
return self.dynamics(notes, duration, velocities=(start_vel, end_vel),
|
||||
articulation=articulation)
|
||||
|
||||
def dynamics(self, notes, duration=Duration.QUARTER, *,
|
||||
velocities=None, articulation: str = "") -> "Part":
|
||||
"""Add notes with a velocity curve.
|
||||
|
||||
Args:
|
||||
notes: List of note strings or Tone/Chord objects.
|
||||
duration: Duration for each note (or list of durations).
|
||||
velocities: Velocity curve — either a ``(start, end)`` tuple
|
||||
for a linear ramp, or a list of ints (one per note).
|
||||
articulation: Optional articulation for all notes (or list).
|
||||
|
||||
Example::
|
||||
|
||||
>>> # Linear ramp
|
||||
>>> piano.dynamics(["C4","E4","G4","C5"], Duration.QUARTER,
|
||||
... velocities=(50, 120))
|
||||
>>> # Custom curve (swell and fade)
|
||||
>>> piano.dynamics(["C4","D4","E4","F4","G4","F4","E4","D4"],
|
||||
... Duration.EIGHTH,
|
||||
... velocities=[50, 70, 90, 110, 110, 90, 70, 50])
|
||||
"""
|
||||
n = len(notes)
|
||||
if n == 0:
|
||||
return self
|
||||
|
||||
# Resolve velocities
|
||||
if velocities is None:
|
||||
vels = [100] * n
|
||||
elif isinstance(velocities, (tuple, list)) and len(velocities) == 2 and isinstance(velocities[0], (int, float)):
|
||||
# (start, end) tuple — linear ramp
|
||||
start_v, end_v = velocities
|
||||
if n == 1:
|
||||
vels = [int(start_v)]
|
||||
else:
|
||||
vels = [int(start_v + (end_v - start_v) * i / (n - 1))
|
||||
for i in range(n)]
|
||||
else:
|
||||
vels = list(velocities)
|
||||
|
||||
# Resolve durations
|
||||
if isinstance(duration, (list, tuple)):
|
||||
durs = list(duration)
|
||||
else:
|
||||
durs = [duration] * n
|
||||
|
||||
# Resolve articulations
|
||||
if isinstance(articulation, (list, tuple)):
|
||||
arts = list(articulation)
|
||||
else:
|
||||
arts = [articulation] * n
|
||||
|
||||
for note, vel, dur, art in zip(notes, vels, durs, arts):
|
||||
vel = max(1, min(127, vel))
|
||||
self.add(note, dur, velocity=vel, articulation=art)
|
||||
|
||||
return self
|
||||
|
||||
def swell(self, notes, duration=Duration.QUARTER, *,
|
||||
low_vel: int = 40, peak_vel: int = 110,
|
||||
articulation: str = "") -> "Part":
|
||||
"""Add notes that swell up then fade back down (< > shape).
|
||||
|
||||
The velocity ramps up to the midpoint then back down,
|
||||
creating the classic orchestral swell.
|
||||
|
||||
Args:
|
||||
notes: List of note strings.
|
||||
duration: Duration for each note.
|
||||
low_vel: Velocity at start and end.
|
||||
peak_vel: Velocity at the peak (midpoint).
|
||||
articulation: Optional articulation.
|
||||
|
||||
Example::
|
||||
|
||||
>>> strings.swell(["C4","D4","E4","F4","G4","F4","E4","D4"],
|
||||
... Duration.QUARTER, low_vel=40, peak_vel=110)
|
||||
"""
|
||||
n = len(notes)
|
||||
if n <= 2:
|
||||
return self.dynamics(notes, duration, velocities=[peak_vel] * n,
|
||||
articulation=articulation)
|
||||
mid = n // 2
|
||||
vels = []
|
||||
for i in range(n):
|
||||
if i <= mid:
|
||||
v = low_vel + (peak_vel - low_vel) * i / mid
|
||||
else:
|
||||
v = peak_vel - (peak_vel - low_vel) * (i - mid) / (n - 1 - mid)
|
||||
vels.append(int(v))
|
||||
return self.dynamics(notes, duration, velocities=vels,
|
||||
articulation=articulation)
|
||||
|
||||
def set(self, **params) -> "Part":
|
||||
"""Change effect parameters at the current beat position.
|
||||
|
||||
@@ -2757,6 +3257,79 @@ class Part:
|
||||
points = sorted(set(beat for beat, _ in self._automation))
|
||||
return points
|
||||
|
||||
def ramp(self, over: float = 4.0, resolution: float = 0.25,
|
||||
curve: str = "linear", **params) -> "Part":
|
||||
"""Smoothly ramp parameters from their current values to new targets.
|
||||
|
||||
Generates interpolated automation points — like turning a knob
|
||||
gradually instead of jumping to a new position. Works for any
|
||||
parameter that ``.set()`` accepts.
|
||||
|
||||
Args:
|
||||
over: Duration of the ramp in beats (default 4.0 = 1 bar).
|
||||
Use ``Duration.WHOLE * 4`` for a 4-bar ramp, etc.
|
||||
resolution: How often to insert points, in beats (default 0.25).
|
||||
Lower = smoother but more points.
|
||||
curve: Interpolation shape — ``"linear"`` (default),
|
||||
``"ease_in"`` (slow start, fast end),
|
||||
``"ease_out"`` (fast start, slow end),
|
||||
``"ease_in_out"`` (slow start and end).
|
||||
**params: Target values for any parameter. The ramp starts
|
||||
from the parameter's current value at this beat position.
|
||||
|
||||
Returns:
|
||||
Self for chaining.
|
||||
|
||||
Example::
|
||||
|
||||
>>> lead = score.part("lead", synth="saw", lowpass=200)
|
||||
>>> # Open the filter over 4 bars
|
||||
>>> lead.ramp(over=Duration.WHOLE * 4, lowpass=8000)
|
||||
>>> # Fade reverb in over 2 bars
|
||||
>>> pad.ramp(over=Duration.WHOLE * 2, reverb=0.5)
|
||||
>>> # Multiple params at once with easing
|
||||
>>> lead.ramp(over=8.0, curve="ease_in", lowpass=6000, distortion=0.4)
|
||||
"""
|
||||
current_beat = sum(n.beats for n in self.notes)
|
||||
|
||||
# Map param names to internal names
|
||||
param_map = {
|
||||
"reverb": "reverb_mix", "delay": "delay_mix",
|
||||
"distortion": "distortion_mix", "chorus": "chorus_mix",
|
||||
"phaser": "phaser_mix",
|
||||
}
|
||||
|
||||
# Get current values for each param
|
||||
current_params = self._get_params_at(current_beat)
|
||||
ramps = {}
|
||||
for param, target in params.items():
|
||||
internal = param_map.get(param, param)
|
||||
start = current_params.get(internal, getattr(self, internal, 0.0))
|
||||
ramps[internal] = (float(start), float(target))
|
||||
|
||||
# Generate interpolated points
|
||||
beat = 0.0
|
||||
while beat <= over:
|
||||
t = beat / over if over > 0 else 1.0
|
||||
t = max(0.0, min(1.0, t))
|
||||
|
||||
# Apply curve
|
||||
if curve == "ease_in":
|
||||
t = t * t
|
||||
elif curve == "ease_out":
|
||||
t = 1.0 - (1.0 - t) ** 2
|
||||
elif curve == "ease_in_out":
|
||||
t = 3 * t * t - 2 * t * t * t
|
||||
|
||||
point = {}
|
||||
for internal, (start, end) in ramps.items():
|
||||
point[internal] = start + (end - start) * t
|
||||
|
||||
self._automation.append((current_beat + beat, point))
|
||||
beat += resolution
|
||||
|
||||
return self
|
||||
|
||||
def lfo(self, param: str, *, rate: float = 0.5, min: float = 0.0,
|
||||
max: float = 1.0, bars: float = 4, shape: str = "sine",
|
||||
resolution: float = 0.25) -> "Part":
|
||||
@@ -3284,6 +3857,7 @@ class Score:
|
||||
cabinet: float = None,
|
||||
cabinet_brightness: float = None,
|
||||
analog: float = None,
|
||||
ensemble: int = None,
|
||||
fm_ratio: float = None,
|
||||
fm_index: float = None,
|
||||
fretboard=None) -> Part:
|
||||
@@ -3396,7 +3970,7 @@ class Score:
|
||||
"tremolo_depth": tremolo_depth, "tremolo_rate": tremolo_rate,
|
||||
"phaser": phaser, "phaser_rate": phaser_rate,
|
||||
"cabinet": cabinet, "cabinet_brightness": cabinet_brightness,
|
||||
"analog": analog,
|
||||
"analog": analog, "ensemble": ensemble,
|
||||
"fm_ratio": fm_ratio, "fm_index": fm_index,
|
||||
}
|
||||
for k, v in _locals.items():
|
||||
|
||||
+6
-6
@@ -2,8 +2,6 @@ from __future__ import annotations
|
||||
|
||||
from typing import Optional, Union
|
||||
|
||||
import numeral
|
||||
|
||||
from .systems import SYSTEMS, System
|
||||
from .tones import Tone
|
||||
|
||||
@@ -49,7 +47,8 @@ class Scale:
|
||||
def __repr__(self) -> str:
|
||||
r = []
|
||||
for (i, tone) in enumerate(self.tones):
|
||||
degree = numeral.int2roman(i + 1, only_ascii=True)
|
||||
from ._statics import int2roman
|
||||
degree = int2roman(i + 1)
|
||||
r += [f"{degree}={tone.full_name}"]
|
||||
|
||||
r = " ".join(r)
|
||||
@@ -200,7 +199,7 @@ class Scale:
|
||||
>>> scale.progression("I", "IV", "V", "I")
|
||||
[<Chord (C,E,G)>, <Chord (F,A,C)>, <Chord (G,B,D)>, <Chord (C,E,G)>]
|
||||
"""
|
||||
import numeral as numeral_mod
|
||||
from ._statics import roman2int
|
||||
chords = []
|
||||
for num in numerals:
|
||||
is_seventh = num.endswith("7")
|
||||
@@ -213,7 +212,7 @@ class Scale:
|
||||
elif clean.startswith("#") and len(clean) > 1:
|
||||
clean = clean[1:]
|
||||
flat_offset = 1 # one semitone up
|
||||
degree = numeral_mod.roman2int(clean.upper()) - 1
|
||||
degree = roman2int(clean.upper()) - 1
|
||||
if is_seventh:
|
||||
chord = self.seventh(degree)
|
||||
else:
|
||||
@@ -406,7 +405,8 @@ class Scale:
|
||||
if isinstance(item, str):
|
||||
degrees = []
|
||||
for (i, tone) in enumerate(self.tones):
|
||||
degrees.append(numeral.int2roman(i + 1, only_ascii=True))
|
||||
from ._statics import int2roman
|
||||
degrees.append(int2roman(i + 1))
|
||||
|
||||
if item in degrees:
|
||||
item = degrees.index(item)
|
||||
|
||||
@@ -7164,3 +7164,200 @@ def test_all_synths_render_and_enum_match():
|
||||
for s in Synth:
|
||||
wave = s(440, n_samples=1000)
|
||||
assert len(wave) == 1000
|
||||
|
||||
|
||||
# ── Articulations ────────────────────────────────────────────────────────
|
||||
|
||||
def test_articulation_field_on_note():
|
||||
from pytheory.rhythm import Note, Duration
|
||||
n = Note(tone=None, duration=Duration.QUARTER, articulation="staccato")
|
||||
assert n.articulation == "staccato"
|
||||
|
||||
|
||||
def test_articulation_default_empty():
|
||||
from pytheory.rhythm import Note, Duration
|
||||
n = Note(tone=None, duration=Duration.QUARTER)
|
||||
assert n.articulation == ""
|
||||
|
||||
|
||||
def test_part_add_articulation():
|
||||
score = pytheory.Score("4/4", bpm=120)
|
||||
p = score.part("test", synth="sine")
|
||||
p.add("C4", Duration.QUARTER, articulation="staccato")
|
||||
p.add("D4", Duration.QUARTER, articulation="legato")
|
||||
p.add("E4", Duration.QUARTER, articulation="marcato")
|
||||
p.add("F4", Duration.QUARTER, articulation="tenuto")
|
||||
p.add("G4", Duration.QUARTER, articulation="accent")
|
||||
p.add("A4", Duration.QUARTER, articulation="fermata")
|
||||
assert len(p.notes) == 6
|
||||
assert p.notes[0].articulation == "staccato"
|
||||
assert p.notes[5].articulation == "fermata"
|
||||
|
||||
|
||||
@needs_portaudio
|
||||
def test_articulations_render():
|
||||
"""Articulations should produce audio without errors."""
|
||||
from pytheory.play import render_score
|
||||
score = pytheory.Score("4/4", bpm=120)
|
||||
p = score.part("test", synth="sine", volume=0.3)
|
||||
for art in ["", "staccato", "legato", "marcato", "tenuto", "accent", "fermata"]:
|
||||
p.add("C4", Duration.QUARTER, articulation=art)
|
||||
buf = render_score(score)
|
||||
assert len(buf) > 0
|
||||
|
||||
|
||||
# ── Dynamic curves ───────────────────────────────────────────────────────
|
||||
|
||||
def test_crescendo_adds_notes():
|
||||
score = pytheory.Score("4/4", bpm=120)
|
||||
p = score.part("test", synth="sine")
|
||||
p.crescendo(["C4", "D4", "E4", "F4"], Duration.QUARTER,
|
||||
start_vel=40, end_vel=100)
|
||||
assert len(p.notes) == 4
|
||||
assert p.notes[0].velocity == 40
|
||||
assert p.notes[3].velocity == 100
|
||||
|
||||
|
||||
def test_decrescendo_adds_notes():
|
||||
score = pytheory.Score("4/4", bpm=120)
|
||||
p = score.part("test", synth="sine")
|
||||
p.decrescendo(["C4", "D4", "E4", "F4"], Duration.QUARTER,
|
||||
start_vel=110, end_vel=40)
|
||||
assert len(p.notes) == 4
|
||||
assert p.notes[0].velocity == 110
|
||||
assert p.notes[3].velocity == 40
|
||||
|
||||
|
||||
def test_swell_velocity_shape():
|
||||
score = pytheory.Score("4/4", bpm=120)
|
||||
p = score.part("test", synth="sine")
|
||||
p.swell(["C4", "D4", "E4", "F4", "G4"], Duration.QUARTER,
|
||||
low_vel=30, peak_vel=110)
|
||||
assert len(p.notes) == 5
|
||||
# First and last should be near low_vel
|
||||
assert p.notes[0].velocity == 30
|
||||
assert p.notes[4].velocity == 30
|
||||
# Middle should be at or near peak
|
||||
assert p.notes[2].velocity == 110
|
||||
|
||||
|
||||
def test_dynamics_custom_velocities():
|
||||
score = pytheory.Score("4/4", bpm=120)
|
||||
p = score.part("test", synth="sine")
|
||||
p.dynamics(["C4", "D4", "E4"], Duration.QUARTER,
|
||||
velocities=[50, 100, 75])
|
||||
assert p.notes[0].velocity == 50
|
||||
assert p.notes[1].velocity == 100
|
||||
assert p.notes[2].velocity == 75
|
||||
|
||||
|
||||
def test_dynamics_with_articulation():
|
||||
score = pytheory.Score("4/4", bpm=120)
|
||||
p = score.part("test", synth="sine")
|
||||
p.crescendo(["C4", "D4"], Duration.QUARTER,
|
||||
start_vel=40, end_vel=100, articulation="staccato")
|
||||
assert p.notes[0].articulation == "staccato"
|
||||
assert p.notes[1].articulation == "staccato"
|
||||
|
||||
|
||||
# ── Part.hit() ───────────────────────────────────────────────────────────
|
||||
|
||||
def test_part_hit_adds_note():
|
||||
from pytheory.rhythm import DrumSound, _DrumTone
|
||||
score = pytheory.Score("4/4", bpm=120)
|
||||
p = score.part("kit", synth="sine")
|
||||
p.hit(DrumSound.KICK, Duration.QUARTER, velocity=100)
|
||||
p.hit(DrumSound.SNARE, Duration.QUARTER, velocity=90, articulation="accent")
|
||||
assert len(p.notes) == 2
|
||||
assert isinstance(p.notes[0].tone, _DrumTone)
|
||||
assert p.notes[0].tone.sound == DrumSound.KICK
|
||||
assert p.notes[1].articulation == "accent"
|
||||
|
||||
|
||||
@needs_portaudio
|
||||
def test_part_hit_renders():
|
||||
"""Part.hit() drum sounds should render through the note pipeline."""
|
||||
from pytheory.rhythm import DrumSound
|
||||
from pytheory.play import render_score
|
||||
score = pytheory.Score("4/4", bpm=120)
|
||||
p = score.part("kit", synth="sine", volume=0.5)
|
||||
p.hit(DrumSound.KICK, Duration.QUARTER)
|
||||
p.hit(DrumSound.SNARE, Duration.QUARTER)
|
||||
p.hit(DrumSound.CLOSED_HAT, Duration.QUARTER)
|
||||
p.hit(DrumSound.CRASH, Duration.QUARTER)
|
||||
buf = render_score(score)
|
||||
assert len(buf) > 0
|
||||
|
||||
|
||||
# ── Part.ramp() ──────────────────────────────────────────────────────────
|
||||
|
||||
def test_ramp_generates_automation():
|
||||
score = pytheory.Score("4/4", bpm=120)
|
||||
p = score.part("test", synth="saw", lowpass=200)
|
||||
p.ramp(over=4.0, lowpass=8000)
|
||||
# Should have generated automation points
|
||||
assert len(p._automation) > 0
|
||||
# First point should be near 200, last near 8000
|
||||
first_lp = p._automation[0][1].get("lowpass", 0)
|
||||
last_lp = p._automation[-1][1].get("lowpass", 0)
|
||||
assert first_lp < 1000 # near start
|
||||
assert last_lp > 7000 # near target
|
||||
|
||||
|
||||
def test_ramp_easing_curves():
|
||||
score = pytheory.Score("4/4", bpm=120)
|
||||
for curve in ["linear", "ease_in", "ease_out", "ease_in_out"]:
|
||||
p = score.part(f"test_{curve}", synth="saw", lowpass=200)
|
||||
p.ramp(over=4.0, curve=curve, lowpass=8000)
|
||||
assert len(p._automation) > 0
|
||||
|
||||
|
||||
def test_ramp_multiple_params():
|
||||
score = pytheory.Score("4/4", bpm=120)
|
||||
p = score.part("test", synth="saw", lowpass=200)
|
||||
p.ramp(over=4.0, lowpass=8000, reverb=0.5)
|
||||
# Should have both params in automation points
|
||||
last_point = p._automation[-1][1]
|
||||
assert "lowpass" in last_point
|
||||
assert "reverb_mix" in last_point # mapped from "reverb"
|
||||
|
||||
|
||||
# ── Cross-choke ──────────────────────────────────────────────────────────
|
||||
|
||||
def test_djembe_patterns_exist():
|
||||
from pytheory.rhythm import Pattern
|
||||
for name in ["djembe", "kuku", "soli", "dununba", "tiriba",
|
||||
"yankadi", "djansa", "mendiani"]:
|
||||
p = Pattern.preset(name)
|
||||
assert p.beats > 0
|
||||
assert len(p.hits) > 0
|
||||
|
||||
|
||||
def test_djembe_fills_exist():
|
||||
from pytheory.rhythm import Pattern
|
||||
for name in ["djembe call", "djembe roll", "djembe break"]:
|
||||
f = Pattern.fill(name)
|
||||
assert f.beats == 4.0
|
||||
assert len(f.hits) > 0
|
||||
|
||||
|
||||
def test_cajon_fills_exist():
|
||||
from pytheory.rhythm import Pattern
|
||||
for name in ["cajon flam", "cajon rumble", "cajon breakdown"]:
|
||||
f = Pattern.fill(name)
|
||||
assert f.beats == 4.0
|
||||
assert len(f.hits) > 0
|
||||
|
||||
|
||||
def test_metal_fills_exist():
|
||||
from pytheory.rhythm import Pattern
|
||||
for name in ["metal triplet", "metal blast", "metal cascade"]:
|
||||
f = Pattern.fill(name)
|
||||
assert f.beats == 4.0
|
||||
assert len(f.hits) > 0
|
||||
|
||||
|
||||
# ── render_score in __all__ ──────────────────────────────────────────────
|
||||
|
||||
def test_render_score_exported():
|
||||
assert "render_score" in pytheory.__all__
|
||||
|
||||
@@ -486,14 +486,6 @@ wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/d3/ac/686789b9145413f1a61878c407210e41bfdb097976864e0913078b24098c/myst_parser-5.0.0-py3-none-any.whl", hash = "sha256:ab31e516024918296e169139072b81592336f2fef55b8986aa31c9f04b5f7211", size = 84533, upload-time = "2026-01-15T09:08:16.788Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "numeral"
|
||||
version = "0.1.0.17"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/17/0d/ac6a186e169fcbdfea316f78fb5e34981bcf8d5c1d7cc8b6581f597e1e4c/numeral-0.1.0.17-py2.py3-none-any.whl", hash = "sha256:7dff0c1efb9b3655c9c1dc93b4666993741b15abcac0dc01dcb96b21cc20f6ae", size = 22066, upload-time = "2020-04-12T08:24:59.129Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "numpy"
|
||||
version = "2.2.6"
|
||||
@@ -698,10 +690,9 @@ wheels = [
|
||||
|
||||
[[package]]
|
||||
name = "pytheory"
|
||||
version = "0.37.0"
|
||||
version = "0.39.3"
|
||||
source = { editable = "." }
|
||||
dependencies = [
|
||||
{ name = "numeral" },
|
||||
{ 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" },
|
||||
@@ -721,7 +712,6 @@ docs = [
|
||||
|
||||
[package.metadata]
|
||||
requires-dist = [
|
||||
{ name = "numeral" },
|
||||
{ name = "scipy" },
|
||||
{ name = "sounddevice" },
|
||||
]
|
||||
|
||||
Reference in New Issue
Block a user