mirror of
https://github.com/kennethreitz/pytheory.git
synced 2026-06-05 14:50:18 +00:00
v0.18.1: Distortion effect, 3 new songs (dub delay, DnB, Drake)
- Soft-clip distortion (tanh waveshaping) with drive and mix controls - Dub Delay Madness: separate snare track with massive delay/reverb - Liquid DnB: 174bpm rollers with flowing lead - Late Night Texts: Drake-style trap with 808 bass + distortion - 16 total example songs Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -2,6 +2,12 @@
|
||||
|
||||
All notable changes to PyTheory are documented here.
|
||||
|
||||
## 0.18.1
|
||||
|
||||
- Add distortion effect (tanh soft-clip waveshaping) with drive and mix controls
|
||||
- 3 new example songs: Dub Delay Madness (separate delay snare), Liquid DnB (174bpm), Late Night Texts (Drake-style trap)
|
||||
- 16 total songs in the song player
|
||||
|
||||
## 0.18.0
|
||||
|
||||
- Add per-part audio effects: reverb, delay, and lowpass filter
|
||||
|
||||
+162
-1
@@ -596,6 +596,164 @@ def gospel_shuffle():
|
||||
play_song(score)
|
||||
|
||||
|
||||
def dub_delay_madness():
|
||||
"""Dub with separate delay snare track — King Tubby style."""
|
||||
print(" Dub Delay Madness in E minor")
|
||||
print(" dub drums + separate snare w/ massive delay + reverb")
|
||||
print(" square skank + reverb | sine sub bass | PWM siren")
|
||||
|
||||
score = Score("4/4", bpm=68)
|
||||
score.drums("dub", repeats=8)
|
||||
|
||||
# Separate snare hits — fed through massive delay and reverb
|
||||
# This is how Tubby did it: mute the snare from the main mix,
|
||||
# then send it to a separate channel drowning in effects
|
||||
from pytheory.rhythm import _Hit, DrumSound
|
||||
for bar in range(8):
|
||||
offset = bar * 4.0
|
||||
# Snare on beat 3 of every bar
|
||||
score._drum_hits.append(_Hit(DrumSound.SNARE, offset + 2.0, 110))
|
||||
# Occasional rimshot ghost that the delay catches
|
||||
if bar % 2 == 1:
|
||||
score._drum_hits.append(_Hit(DrumSound.RIMSHOT, offset + 3.5, 60))
|
||||
|
||||
chords = score.part("skank", synth="square", envelope="staccato",
|
||||
volume=0.15, reverb=0.7, reverb_decay=3.0,
|
||||
lowpass=1200)
|
||||
bass = score.part("bass", synth="sine", envelope="pluck",
|
||||
volume=0.6, lowpass=350, lowpass_q=1.5)
|
||||
siren = score.part("siren", synth="pwm_slow", envelope="pad",
|
||||
volume=0.12, reverb=0.8, reverb_decay=4.0,
|
||||
delay=0.4, delay_time=0.88, delay_feedback=0.6,
|
||||
lowpass=900)
|
||||
# Melodica stabs — sparse, lots of delay
|
||||
melodica = score.part("melodica", synth="triangle", envelope="pluck",
|
||||
volume=0.35, delay=0.6, delay_time=0.66,
|
||||
delay_feedback=0.55, reverb=0.5, reverb_decay=2.5)
|
||||
|
||||
for sym in ["Em", "Em", "Am", "Am", "Em", "Em", "Bm", "Em"]:
|
||||
chords.add(Chord.from_symbol(sym), Duration.WHOLE)
|
||||
|
||||
for n in ["E1","E1","E1","E1","A1","A1","A1","A1",
|
||||
"E1","E1","E1","E1","B1","B1","E1","E1"]:
|
||||
bass.add(n, Duration.HALF)
|
||||
|
||||
# Melodica: very sparse — let the delay do the work
|
||||
for n, d in [
|
||||
("E5", 1.5), (None, 6.5),
|
||||
("G5", 1), ("A5", 1), (None, 6),
|
||||
(None, 4), ("B5", 2), (None, 6),
|
||||
("E5", 1), (None, 3), ("D5", 1.5), (None, 2.5),
|
||||
]:
|
||||
melodica.rest(d) if n is None else melodica.add(n, d)
|
||||
|
||||
# Siren: long notes that disappear into the void
|
||||
for n, d in [
|
||||
(None, 12), ("B5", 6), (None, 6),
|
||||
("E5", 4), (None, 4),
|
||||
]:
|
||||
siren.rest(d) if n is None else siren.add(n, d)
|
||||
|
||||
play_song(score)
|
||||
|
||||
|
||||
def drum_and_bass():
|
||||
"""Drum and bass in A minor — 174 bpm liquid rollers."""
|
||||
print(" Liquid DnB in A minor")
|
||||
print(" drum and bass drums + fill | supersaw pads + reverb")
|
||||
print(" triangle lead + delay | sine sub bass + LP 300Hz")
|
||||
|
||||
score = Score("4/4", bpm=174)
|
||||
score.drums("drum and bass", repeats=8, fill="buildup", fill_every=8)
|
||||
|
||||
pads = score.part("pads", synth="supersaw", envelope="pad",
|
||||
volume=0.25, reverb=0.5, reverb_decay=2.5,
|
||||
lowpass=4000)
|
||||
lead = score.part("lead", synth="triangle", envelope="strings",
|
||||
volume=0.4, delay=0.3, delay_time=0.172,
|
||||
delay_feedback=0.4, reverb=0.25)
|
||||
bass = score.part("bass", synth="sine", envelope="pluck",
|
||||
volume=0.55, lowpass=300)
|
||||
|
||||
for sym in ["Am", "F", "C", "G"] * 2:
|
||||
pads.add(Chord.from_symbol(sym), Duration.WHOLE)
|
||||
|
||||
# Liquid melody — flowing, emotional
|
||||
for n, d in [
|
||||
("A5", 1), ("G5", .5), ("E5", .5), ("C5", 1), (None, 1),
|
||||
("D5", .5), ("E5", .5), ("G5", 1), ("A5", 1.5), (None, .5),
|
||||
("C6", 1), ("B5", .5), ("A5", .5), ("G5", 1), ("E5", 1),
|
||||
("F5", .5), ("G5", .5), ("A5", 1.5), (None, .5), ("G5", 1),
|
||||
("A5", 1), ("G5", .5), ("E5", .5), ("C5", 1), (None, 1),
|
||||
("E5", .5), ("G5", .5), ("B5", 1), ("A5", 2),
|
||||
("G5", 1), ("E5", .5), ("D5", .5), ("C5", 1.5), (None, .5),
|
||||
("E5", .5), ("G5", .5), ("A5", 2), (None, 1),
|
||||
]:
|
||||
lead.rest(d) if n is None else lead.add(n, d)
|
||||
|
||||
# Sub bass — half note roots, deep
|
||||
for n in ["A1","A1","F1","F1","C1","C1","G1","G1"] * 2:
|
||||
bass.add(n, Duration.HALF)
|
||||
|
||||
play_song(score)
|
||||
|
||||
|
||||
def drake_vibes():
|
||||
"""Drake-style moody hip hop — 808s, pads, and melancholy."""
|
||||
print(" Late Night Texts (Drake-style)")
|
||||
print(" trap drums | FM bells + reverb + delay | supersaw pads + LP")
|
||||
print(" sine 808 bass + distortion | PWM slow lead")
|
||||
|
||||
score = Score("4/4", bpm=68)
|
||||
score.drums("trap", repeats=8, fill="trap", fill_every=8)
|
||||
|
||||
pads = score.part("pads", synth="supersaw", envelope="pad",
|
||||
volume=0.2, reverb=0.5, reverb_decay=3.0,
|
||||
lowpass=2500)
|
||||
bells = score.part("bells", synth="fm", envelope="bell",
|
||||
volume=0.3, reverb=0.4, reverb_decay=2.0,
|
||||
delay=0.25, delay_time=0.44,
|
||||
delay_feedback=0.35)
|
||||
lead = score.part("lead", synth="pwm_slow", envelope="strings",
|
||||
volume=0.35, reverb=0.3, lowpass=2000,
|
||||
delay=0.2, delay_time=0.88, delay_feedback=0.3)
|
||||
bass = score.part("bass", synth="sine", envelope="pluck",
|
||||
volume=0.6, lowpass=200, lowpass_q=1.8,
|
||||
distortion=0.4, distortion_drive=2.0)
|
||||
|
||||
for sym in ["Ebm", "B", "Gb", "Db"] * 2:
|
||||
pads.add(Chord.from_symbol(sym), Duration.WHOLE)
|
||||
|
||||
# FM bells — sparse, melancholy arpeggios
|
||||
for n, d in [
|
||||
("Eb5", .5), ("Gb5", .5), ("Bb5", 1), (None, 2),
|
||||
("Db5", .5), ("F5", .5), ("Ab5", 1), (None, 2),
|
||||
("Gb5", .5), ("Bb5", .5), ("Db6", 1), (None, 2),
|
||||
("F5", .5), ("Ab5", .5), ("Db6", 1.5), (None, 1.5),
|
||||
("Eb5", .5), ("Gb5", .5), ("Bb5", 1), (None, 2),
|
||||
("B4", .5), ("Eb5", .5), ("Gb5", 1), (None, 2),
|
||||
("Gb5", 1), ("F5", .5), ("Eb5", .5), ("Db5", 2),
|
||||
(None, 4),
|
||||
]:
|
||||
bells.rest(d) if n is None else bells.add(n, d)
|
||||
|
||||
# PWM lead — long held notes, moody
|
||||
for n, d in [
|
||||
(None, 4), ("Bb5", 3), (None, 1),
|
||||
(None, 2), ("Ab5", 2), ("Gb5", 2), (None, 2),
|
||||
("Db6", 4), (None, 4),
|
||||
("Bb5", 2), ("Ab5", 2), (None, 4),
|
||||
]:
|
||||
lead.rest(d) if n is None else lead.add(n, d)
|
||||
|
||||
# 808 bass — sustained with distortion warmth
|
||||
for n in ["Eb1","Eb1","Eb1","Eb1","B0","B0","B0","B0",
|
||||
"Gb1","Gb1","Gb1","Gb1","Db1","Db1","Db1","Db1"] * 2:
|
||||
bass.add(n, Duration.QUARTER)
|
||||
|
||||
play_song(score)
|
||||
|
||||
|
||||
# ── Main ───────────────────────────────────────────────────────────────────
|
||||
|
||||
SONGS = {
|
||||
@@ -612,6 +770,9 @@ SONGS = {
|
||||
"11": ("Kingston After Dark (Dub)", dub_kingston),
|
||||
"12": ("Minimal Techno in F minor", techno_minimal),
|
||||
"13": ("Gospel Shuffle in C major", gospel_shuffle),
|
||||
"14": ("Dub Delay Madness in E minor", dub_delay_madness),
|
||||
"15": ("Liquid DnB in A minor", drum_and_bass),
|
||||
"16": ("Late Night Texts (Drake-style)", drake_vibes),
|
||||
}
|
||||
|
||||
if __name__ == "__main__":
|
||||
@@ -625,7 +786,7 @@ if __name__ == "__main__":
|
||||
print(f" {key:>2}. {name}")
|
||||
|
||||
print()
|
||||
choice = input(" Pick a song (1-13, or 'all'): ").strip()
|
||||
choice = input(" Pick a song (1-16, or 'all'): ").strip()
|
||||
print()
|
||||
|
||||
if choice == "all":
|
||||
|
||||
@@ -838,8 +838,41 @@ def _apply_lowpass(samples, cutoff, q=0.707, sample_rate=SAMPLE_RATE):
|
||||
return scipy.signal.lfilter(b, a, samples).astype(numpy.float32)
|
||||
|
||||
|
||||
def _apply_distortion(samples, drive=1.0, mix=1.0):
|
||||
"""Apply soft-clip distortion (tanh waveshaping).
|
||||
|
||||
Models the warm saturation of an overdriven tube amplifier.
|
||||
Low drive values add subtle harmonic warmth; high values
|
||||
produce aggressive fuzz.
|
||||
|
||||
The tanh function is the classic soft clipper — it smoothly
|
||||
compresses peaks rather than hard-clipping them, which is
|
||||
why tube amps sound "warm" when overdriven while digital
|
||||
clipping sounds harsh.
|
||||
|
||||
Args:
|
||||
samples: Float32 numpy array.
|
||||
drive: Gain before clipping, 0.5–20.0.
|
||||
0.5–2 = subtle warmth (tube preamp)
|
||||
3–8 = overdrive (cranked amp)
|
||||
10+ = fuzz/distortion
|
||||
mix: Wet/dry ratio 0.0–1.0.
|
||||
|
||||
Returns:
|
||||
Float32 array with distortion applied.
|
||||
"""
|
||||
if mix <= 0 or drive <= 0:
|
||||
return samples
|
||||
driven = numpy.tanh(samples * drive)
|
||||
return samples * (1 - mix) + driven * mix
|
||||
|
||||
|
||||
def _apply_part_effects(samples, part):
|
||||
"""Apply all effects configured on a Part to a float32 buffer."""
|
||||
# Distortion first (before filter, like a real signal chain)
|
||||
if part.distortion_mix > 0:
|
||||
samples = _apply_distortion(samples, drive=part.distortion_drive,
|
||||
mix=part.distortion_mix)
|
||||
if part.lowpass > 0:
|
||||
samples = _apply_lowpass(samples, part.lowpass, part.lowpass_q)
|
||||
if part.delay_mix > 0:
|
||||
|
||||
+11
-3
@@ -1358,7 +1358,8 @@ class Part:
|
||||
reverb: float = 0.0, reverb_decay: float = 1.0,
|
||||
delay: float = 0.0, delay_time: float = 0.375,
|
||||
delay_feedback: float = 0.4,
|
||||
lowpass: float = 0.0, lowpass_q: float = 0.707):
|
||||
lowpass: float = 0.0, lowpass_q: float = 0.707,
|
||||
distortion: float = 0.0, distortion_drive: float = 3.0):
|
||||
self.name = name
|
||||
self.synth = synth
|
||||
self.envelope = envelope
|
||||
@@ -1370,6 +1371,8 @@ class Part:
|
||||
self.delay_feedback = delay_feedback
|
||||
self.lowpass = lowpass
|
||||
self.lowpass_q = lowpass_q
|
||||
self.distortion_mix = distortion
|
||||
self.distortion_drive = distortion_drive
|
||||
self.notes: list[Note] = []
|
||||
|
||||
def add(self, tone_or_string, duration=Duration.QUARTER) -> "Part":
|
||||
@@ -1452,7 +1455,8 @@ class Score:
|
||||
reverb: float = 0.0, reverb_decay: float = 1.0,
|
||||
delay: float = 0.0, delay_time: float = 0.375,
|
||||
delay_feedback: float = 0.4,
|
||||
lowpass: float = 0.0, lowpass_q: float = 0.707) -> Part:
|
||||
lowpass: float = 0.0, lowpass_q: float = 0.707,
|
||||
distortion: float = 0.0, distortion_drive: float = 3.0) -> Part:
|
||||
"""Create a named part with its own synth voice and effects.
|
||||
|
||||
Args:
|
||||
@@ -1475,6 +1479,9 @@ class Score:
|
||||
lowpass_q: Filter resonance/Q factor (default 0.707, flat).
|
||||
Higher values add a resonant peak at the cutoff —
|
||||
1.0 = slight peak, 2.0 = pronounced, 5.0+ = aggressive.
|
||||
distortion: Distortion wet/dry mix, 0.0–1.0 (default 0, off).
|
||||
distortion_drive: Gain before soft clipping (default 3.0).
|
||||
0.5–2 = subtle warmth, 3–8 = overdrive, 10+ = fuzz.
|
||||
|
||||
Returns:
|
||||
A :class:`Part` object. Add notes with ``.add()`` and ``.rest()``.
|
||||
@@ -1488,7 +1495,8 @@ class Score:
|
||||
reverb=reverb, reverb_decay=reverb_decay,
|
||||
delay=delay, delay_time=delay_time,
|
||||
delay_feedback=delay_feedback,
|
||||
lowpass=lowpass, lowpass_q=lowpass_q)
|
||||
lowpass=lowpass, lowpass_q=lowpass_q,
|
||||
distortion=distortion, distortion_drive=distortion_drive)
|
||||
self.parts[name] = p
|
||||
return p
|
||||
|
||||
|
||||
Reference in New Issue
Block a user