7 new synths: pedal steel, theremin, kalimba, steel drum, accordion, didgeridoo, bagpipe

- Pedal steel: singing harmonics, slow vibrato, spring reverb
- Theremin: pure sine with hand wobble, legato+glide preset
- Kalimba: inharmonic metal tine modes, wooden body, bell-like
- Steel drum: hammered metal partials, bright Caribbean ring
- Accordion: musette-tuned doubled reeds, bellows pressure swell
- Didgeridoo: deep cylindrical drone, shifting formant overtones
- Bagpipe: bright chanter reed, constant bag pressure
- 41 synth waveforms total

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-03-27 20:11:32 -04:00
parent 70efb0ad40
commit ce480858e9
2 changed files with 264 additions and 0 deletions
+233
View File
@@ -1130,6 +1130,228 @@ def granular_wave(hz, peak=SAMPLE_PEAK, n_samples=SAMPLE_RATE,
return (peak * out).astype(numpy.int16)
def pedal_steel_wave(hz, peak=SAMPLE_PEAK, n_samples=SAMPLE_RATE):
"""Pedal steel guitar — the Nashville crying sound.
Sustained steel string with natural portamento character,
very smooth, lots of harmonics, and a singing quality from
the bar sliding on the strings.
"""
t = numpy.arange(n_samples, dtype=numpy.float64) / SAMPLE_RATE
rng = numpy.random.default_rng(int(hz * 100) % 2**31)
# Slow, singing vibrato — the bar wobbling on the strings
vib = hz * 0.002 * numpy.sin(2 * numpy.pi * 4.0 * t)
# Rich harmonics — steel bar gives a clear, singing tone
wave = numpy.zeros(n_samples, dtype=numpy.float64)
for n in range(1, 12):
f_n = hz * n
if f_n >= SAMPLE_RATE / 2:
break
amp = 1.0 / n * numpy.exp(-0.08 * n)
phase = rng.uniform(0, 2 * numpy.pi)
wave += amp * numpy.sin(2 * numpy.pi * (f_n + vib * n) * t + phase)
# Long sustain envelope
wave *= numpy.exp(-0.8 * t)
mx = numpy.abs(wave).max()
if mx > 0:
wave /= mx
return (peak * wave).astype(numpy.int16)
def theremin_wave(hz, peak=SAMPLE_PEAK, n_samples=SAMPLE_RATE):
"""Theremin — pure sine with natural wobble.
The theremin's sound is a nearly pure sine wave with slight
pitch instability from hand position. The eerie, sci-fi sound
comes from this purity combined with continuous pitch.
"""
t = numpy.arange(n_samples, dtype=numpy.float64) / SAMPLE_RATE
# Natural hand wobble — slightly irregular vibrato
rng = numpy.random.default_rng(int(hz * 100) % 2**31)
wobble = hz * 0.004 * numpy.sin(2 * numpy.pi * 5.8 * t)
wobble += hz * 0.001 * rng.normal(0, 1, n_samples)
wave = numpy.sin(2 * numpy.pi * (hz + wobble) * t)
# Slight 2nd harmonic — real theremins aren't perfectly pure
wave += 0.08 * numpy.sin(2 * numpy.pi * (hz * 2 + wobble * 2) * t)
mx = numpy.abs(wave).max()
if mx > 0:
wave /= mx
return (peak * wave).astype(numpy.int16)
def kalimba_wave(hz, peak=SAMPLE_PEAK, n_samples=SAMPLE_RATE):
"""Kalimba/thumb piano — metal tines on a wooden body.
Bright, bell-like attack with inharmonic overtones from the
metal tines. The wooden resonator gives warmth underneath.
"""
t = numpy.arange(n_samples, dtype=numpy.float64) / SAMPLE_RATE
# Metal tine modes — slightly inharmonic like marimba
wave = numpy.sin(2 * numpy.pi * hz * t) * 0.8
wave += numpy.sin(2 * numpy.pi * hz * 2.92 * t) * 0.25 * numpy.exp(-12 * t)
wave += numpy.sin(2 * numpy.pi * hz * 5.4 * t) * 0.1 * numpy.exp(-20 * t)
# Two-stage decay: bright attack dies fast, fundamental rings
decay = numpy.where(t < 0.1,
numpy.exp(-3 * t),
numpy.exp(-3 * 0.1) * numpy.exp(-1.5 * (t - 0.1)))
wave *= decay
# Wooden body resonance
import scipy.signal as _sig
for center, bw, gain in [(300, 100, 0.2), (600, 120, 0.15)]:
lo, hi = max(20, center - bw), min(SAMPLE_RATE // 2 - 1, center + bw)
if lo < hi:
bp, ap = _sig.butter(2, [lo, hi], btype='band', fs=SAMPLE_RATE)
wave += _sig.lfilter(bp, ap, wave) * gain
mx = numpy.abs(wave).max()
if mx > 0:
wave /= mx
return (peak * wave).astype(numpy.int16)
def steel_drum_wave(hz, peak=SAMPLE_PEAK, n_samples=SAMPLE_RATE):
"""Steel drum/pan — hammered metal with bright, ringing tone.
The steel pan has specific inharmonic partials from the
hand-hammered notes. Bright, tropical, bell-like.
"""
t = numpy.arange(n_samples, dtype=numpy.float64) / SAMPLE_RATE
# Steel pan modes — distinctly metallic
wave = numpy.sin(2 * numpy.pi * hz * t) * 0.7
wave += numpy.sin(2 * numpy.pi * hz * 2.0 * t) * 0.4 * numpy.exp(-5 * t)
wave += numpy.sin(2 * numpy.pi * hz * 3.01 * t) * 0.25 * numpy.exp(-8 * t)
wave += numpy.sin(2 * numpy.pi * hz * 4.1 * t) * 0.15 * numpy.exp(-12 * t)
wave += numpy.sin(2 * numpy.pi * hz * 5.3 * t) * 0.08 * numpy.exp(-18 * t)
# Mallet impact
rng = numpy.random.default_rng(int(hz * 100) % 2**31)
hit_len = min(int(SAMPLE_RATE * 0.008), n_samples)
hit = rng.uniform(-0.2, 0.2, hit_len) * numpy.exp(-numpy.linspace(0, 12, hit_len))
wave[:hit_len] += hit
# Two-stage decay
decay = numpy.where(t < 0.15,
numpy.exp(-4 * t),
numpy.exp(-4 * 0.15) * numpy.exp(-1.2 * (t - 0.15)))
wave *= decay
mx = numpy.abs(wave).max()
if mx > 0:
wave /= mx
return (peak * wave).astype(numpy.int16)
def accordion_wave(hz, peak=SAMPLE_PEAK, n_samples=SAMPLE_RATE):
"""Accordion — bellows-driven free reeds.
Two reeds per note slightly detuned (musette tuning) create
the characteristic beating/tremolo. Rich in harmonics from
the reed vibration.
"""
t = numpy.arange(n_samples, dtype=numpy.float64) / SAMPLE_RATE
rng = numpy.random.default_rng(int(hz * 100) % 2**31)
# Two reeds slightly detuned — musette beating
detune_cents = 8
hz2 = hz * (2 ** (detune_cents / 1200))
# Reed harmonics — rich, like a square-ish wave but warmer
wave = numpy.zeros(n_samples, dtype=numpy.float64)
for reed_hz in [hz, hz2]:
for n in range(1, 10):
f_n = reed_hz * n
if f_n >= SAMPLE_RATE / 2:
break
# Odd harmonics stronger (reed character)
amp = (1.0 / n) * (1.2 if n % 2 == 1 else 0.6)
phase = rng.uniform(0, 2 * numpy.pi)
wave += amp * numpy.sin(2 * numpy.pi * f_n * t + phase)
wave *= 0.5 # normalize for two reeds
# Bellows pressure variation — slow amplitude swell
bellows = 0.85 + 0.15 * numpy.sin(2 * numpy.pi * 0.8 * t)
wave *= bellows
mx = numpy.abs(wave).max()
if mx > 0:
wave /= mx
return (peak * wave).astype(numpy.int16)
def didgeridoo_wave(hz, peak=SAMPLE_PEAK, n_samples=SAMPLE_RATE):
"""Didgeridoo — circular breathing drone through a wooden tube.
Deep fundamental with strong odd harmonics from the cylindrical
bore. The overtone singing technique creates shifting formants.
Buzzy, droning, primal.
"""
t = numpy.arange(n_samples, dtype=numpy.float64) / SAMPLE_RATE
rng = numpy.random.default_rng(int(hz * 100) % 2**31)
# Lip buzz source — rich, raw
phase = numpy.cumsum(2 * numpy.pi * hz / SAMPLE_RATE * numpy.ones(n_samples))
buzz = numpy.zeros(n_samples, dtype=numpy.float64)
for n in range(1, 15):
if hz * n >= SAMPLE_RATE / 2:
break
# Odd harmonics stronger (cylindrical bore, like clarinet)
amp = (1.0 / n) * (1.0 if n % 2 == 1 else 0.4)
buzz += amp * numpy.sin(phase * n + rng.uniform(0, 2 * numpy.pi))
# Shifting formant — the overtone singing effect
# Sweeps slowly between 500Hz and 1500Hz
formant_center = 800 + 400 * numpy.sin(2 * numpy.pi * 0.3 * t)
import scipy.signal as _sig
# Block-process the formant sweep
block = 2048
out = numpy.zeros(n_samples, dtype=numpy.float64)
for i in range(0, n_samples, block):
end = min(i + block, n_samples)
fc = formant_center[(i + end) // 2]
lo = max(20, int(fc - 300))
hi = min(SAMPLE_RATE // 2 - 1, int(fc + 300))
if lo < hi:
bp, ap = _sig.butter(2, [lo, hi], btype='band', fs=SAMPLE_RATE)
seg = _sig.lfilter(bp, ap, buzz[i:end])
out[i:end] = buzz[i:end] * 0.5 + seg * 0.5
else:
out[i:end] = buzz[i:end]
# Breath noise
breath = rng.normal(0, 0.04, n_samples)
out += breath
mx = numpy.abs(out).max()
if mx > 0:
out /= mx
return (peak * out).astype(numpy.int16)
def bagpipe_wave(hz, peak=SAMPLE_PEAK, n_samples=SAMPLE_RATE):
"""Bagpipes — chanter reed with constant drone pressure.
The chanter (melody pipe) uses a double reed like an oboe
but with more buzz and brightness. The constant air pressure
from the bag means no dynamics — always ff.
"""
t = numpy.arange(n_samples, dtype=numpy.float64) / SAMPLE_RATE
rng = numpy.random.default_rng(int(hz * 100) % 2**31)
# Chanter — all harmonics, bright and reedy
wave = numpy.zeros(n_samples, dtype=numpy.float64)
for n in range(1, 18):
f_n = hz * n
if f_n >= SAMPLE_RATE / 2:
break
# Peaked around harmonics 3-7 (the piercing brightness)
amp = (1.0 / n) * numpy.exp(-0.03 * (n - 5) ** 2)
phase = rng.uniform(0, 2 * numpy.pi)
wave += amp * numpy.sin(2 * numpy.pi * f_n * t + phase)
# Reed buzz — more than oboe
reed = rng.normal(0, 0.08, n_samples)
import scipy.signal as _sig
lo = max(20, int(hz * 2))
hi = min(SAMPLE_RATE // 2 - 1, int(hz * 8))
if lo < hi:
br, ar = _sig.butter(2, [lo, hi], btype='band', fs=SAMPLE_RATE)
reed = _sig.lfilter(br, ar, reed).astype(numpy.float64) * 1.5
wave += reed
# Bag pressure wobble — very subtle
bag = 1.0 + 0.02 * numpy.sin(2 * numpy.pi * 1.5 * t)
wave *= bag
mx = numpy.abs(wave).max()
if mx > 0:
wave /= mx
return (peak * wave).astype(numpy.int16)
def banjo_wave(hz, peak=SAMPLE_PEAK, n_samples=SAMPLE_RATE):
"""Banjo — steel strings on a drum-head body.
@@ -1565,6 +1787,13 @@ class Synth(Enum):
SAXOPHONE = "saxophone_synth"
GRANULAR = "granular_synth"
VOCAL = "vocal_synth"
PEDAL_STEEL = "pedal_steel_synth"
THEREMIN = "theremin_synth"
KALIMBA = "kalimba_synth"
STEEL_DRUM = "steel_drum_synth"
ACCORDION = "accordion_synth"
DIDGERIDOO = "didgeridoo_synth"
BAGPIPE = "bagpipe_synth"
BANJO = "banjo_synth"
MANDOLIN = "mandolin_synth"
UKULELE = "ukulele_synth"
@@ -1591,6 +1820,10 @@ _SYNTH_FUNCTIONS = {
"harp_synth": harp_wave, "upright_bass_synth": upright_bass_wave,
"timpani_synth": timpani_wave, "saxophone_synth": saxophone_wave,
"granular_synth": granular_wave, "vocal_synth": vocal_wave,
"pedal_steel_synth": pedal_steel_wave, "theremin_synth": theremin_wave,
"kalimba_synth": kalimba_wave, "steel_drum_synth": steel_drum_wave,
"accordion_synth": accordion_wave, "didgeridoo_synth": didgeridoo_wave,
"bagpipe_synth": bagpipe_wave,
"banjo_synth": banjo_wave, "mandolin_synth": mandolin_wave,
"ukulele_synth": ukulele_wave,
"acoustic_guitar_synth": acoustic_guitar_wave,
+31
View File
@@ -195,6 +195,37 @@ INSTRUMENTS = {
"detune": 12, "lowpass": 3000, "lowpass_q": 1.5,
"humanize": 0.2,
},
"pedal_steel": {
"synth": "pedal_steel_synth", "envelope": "strings",
"reverb": 0.3, "reverb_type": "spring",
"humanize": 0.15,
},
"theremin": {
"synth": "theremin_synth", "envelope": "pad",
"legato": True, "glide": 0.05,
"reverb": 0.3, "reverb_type": "plate",
},
"kalimba": {
"synth": "kalimba_synth", "envelope": "none",
"reverb": 0.35, "reverb_type": "plate",
},
"steel_drum": {
"synth": "steel_drum_synth", "envelope": "none",
"reverb": 0.3, "reverb_type": "plate",
},
"accordion": {
"synth": "accordion_synth", "envelope": "organ",
"humanize": 0.15,
},
"didgeridoo": {
"synth": "didgeridoo_synth", "envelope": "pad",
"lowpass": 1500,
"reverb": 0.4, "reverb_type": "cave",
},
"bagpipe": {
"synth": "bagpipe_synth", "envelope": "organ",
"lowpass": 4000,
},
"banjo": {
"synth": "banjo_synth", "envelope": "none",
"humanize": 0.2,