mirror of
https://github.com/kennethreitz/pytheory.git
synced 2026-06-05 14:50:18 +00:00
Add Wurlitzer, vibraphone, pipe organ, choir synths
- Wurlitzer: reed-based, nasal, biting — bark on hard hits - Vibraphone: aluminum bars with motor tremolo (spinning disc) - Pipe organ: multi-rank (8'+4'+2'), constant air, wind chiff - Choir: formant-filtered glottal source, vowel control via lyric=, no vibrato (ensemble handles voice variation) - All four with instrument presets, audio demos, and docs Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Vendored
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
@@ -662,6 +662,41 @@ def gen_synth_kalimba():
|
||||
p.add(n, Duration.QUARTER, velocity=85)
|
||||
render("synth_kalimba", score)
|
||||
|
||||
def gen_synth_wurlitzer():
|
||||
score = Score("4/4", bpm=90)
|
||||
p = score.part("demo", instrument="wurlitzer", volume=0.5, reverb=0.25)
|
||||
p.hold("C3", Duration.WHOLE * 2, velocity=60)
|
||||
p.hold("Eb3", Duration.WHOLE * 2, velocity=55)
|
||||
p.hold("G3", Duration.WHOLE * 2, velocity=55)
|
||||
for n in ["G4", "Bb4", "C5", "Bb4", "G4", "F4", "Eb4", "G4"]:
|
||||
p.add(n, Duration.QUARTER, velocity=78)
|
||||
render("synth_wurlitzer", score)
|
||||
|
||||
def gen_synth_vibraphone():
|
||||
score = Score("4/4", bpm=90)
|
||||
p = score.part("demo", instrument="vibraphone", volume=0.5)
|
||||
for n in ["C5", "E5", "G5", "C6", "G5", "E5", "C5", "E5"]:
|
||||
p.add(n, Duration.QUARTER, velocity=75)
|
||||
render("synth_vibraphone", score)
|
||||
|
||||
def gen_synth_pipe_organ():
|
||||
score = Score("4/4", bpm=70)
|
||||
p = score.part("demo", instrument="pipe_organ", volume=0.5)
|
||||
p.hold("C3", Duration.WHOLE * 4, velocity=70)
|
||||
p.hold("G3", Duration.WHOLE * 4, velocity=65)
|
||||
for n in ["C4", "D4", "E4", "F4", "G4", "F4", "E4", "D4",
|
||||
"C4", "E4", "G4", "C5", "G4", "E4", "C4", "C4"]:
|
||||
p.add(n, Duration.QUARTER, velocity=75)
|
||||
render("synth_pipe_organ", score)
|
||||
|
||||
def gen_synth_choir():
|
||||
score = Score("4/4", bpm=70)
|
||||
p = score.part("demo", instrument="choir", volume=0.5)
|
||||
for n, v in [("C4", "ah"), ("E4", "oh"), ("G4", "ah"), ("C5", "ee"),
|
||||
("G4", "oh"), ("E4", "ah"), ("C4", "oo"), ("C4", "ah")]:
|
||||
p.add(n, Duration.HALF, velocity=70, lyric=v)
|
||||
render("synth_choir", score)
|
||||
|
||||
def gen_synth_organ():
|
||||
_synth_demo("organ", "organ_synth", envelope="organ")
|
||||
|
||||
@@ -1004,6 +1039,10 @@ GENERATORS = [
|
||||
gen_synth_electric_guitar,
|
||||
gen_synth_sitar,
|
||||
gen_synth_kalimba,
|
||||
gen_synth_wurlitzer,
|
||||
gen_synth_vibraphone,
|
||||
gen_synth_pipe_organ,
|
||||
gen_synth_choir,
|
||||
gen_synth_organ,
|
||||
gen_synth_marimba,
|
||||
gen_synth_harp,
|
||||
|
||||
@@ -479,6 +479,72 @@ singing sustain. The sound of jazz clubs, soul, and neo-soul.
|
||||
|
||||
<audio controls style="width:100%;margin:0.3em 0 0.5em"><source src="../_static/audio/synth_rhodes.wav" type="audio/wav"></audio>
|
||||
|
||||
Wurlitzer Electric Piano
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
The Wurlitzer uses a vibrating steel reed (not a tine like Rhodes)
|
||||
picked up by an electrostatic pickup. More nasal, reedy, and biting
|
||||
— it barks and growls when played hard. Think Supertramp, Ray Charles.
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
wurli = score.part("wurli", synth="wurlitzer_synth")
|
||||
wurli = score.part("wurli", instrument="wurlitzer")
|
||||
|
||||
.. raw:: html
|
||||
|
||||
<audio controls style="width:100%;margin:0.3em 0 0.5em"><source src="../_static/audio/synth_wurlitzer.wav" type="audio/wav"></audio>
|
||||
|
||||
Vibraphone Synth
|
||||
~~~~~~~~~~~~~~~~
|
||||
|
||||
Struck aluminum bars with motor-driven tremolo discs. The spinning
|
||||
motor modulates the sound through the resonator tubes, creating the
|
||||
signature vibraphone shimmer. Inharmonic bar modes at 1x, 2.76x, 5.4x.
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
vib = score.part("vib", synth="vibraphone_synth")
|
||||
vib = score.part("vib", instrument="vibraphone")
|
||||
|
||||
.. raw:: html
|
||||
|
||||
<audio controls style="width:100%;margin:0.3em 0 0.5em"><source src="../_static/audio/synth_vibraphone.wav" type="audio/wav"></audio>
|
||||
|
||||
Pipe Organ Synth
|
||||
~~~~~~~~~~~~~~~~
|
||||
|
||||
Multiple ranks of pipes — principal 8', octave 4', fifteenth 2'.
|
||||
Constant air pressure means no dynamics. Wind chiff at the attack.
|
||||
Best with cathedral reverb.
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
organ = score.part("organ", synth="pipe_organ_synth")
|
||||
organ = score.part("organ", instrument="pipe_organ")
|
||||
|
||||
.. raw:: html
|
||||
|
||||
<audio controls style="width:100%;margin:0.3em 0 0.5em"><source src="../_static/audio/synth_pipe_organ.wav" type="audio/wav"></audio>
|
||||
|
||||
Choir Synth
|
||||
~~~~~~~~~~~
|
||||
|
||||
Voices singing vowels shaped by formant bandpass filters. The glottal
|
||||
source is filtered through vocal tract resonances — F1, F2, F3, F4 —
|
||||
which is what makes "ah" sound different from "oo". Use ``lyric=``
|
||||
to control the vowel. Best with ``ensemble=`` for a full section.
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
choir = score.part("choir", synth="choir_synth")
|
||||
choir = score.part("choir", instrument="choir") # ensemble=6 + cathedral reverb
|
||||
choir.add("C4", Duration.WHOLE, lyric="ah")
|
||||
|
||||
.. raw:: html
|
||||
|
||||
<audio controls style="width:100%;margin:0.3em 0 0.5em"><source src="../_static/audio/synth_choir.wav" type="audio/wav"></audio>
|
||||
|
||||
Bass Guitar Synth
|
||||
~~~~~~~~~~~~~~~~~
|
||||
|
||||
|
||||
@@ -478,6 +478,246 @@ def rhodes_wave(hz, peak=SAMPLE_PEAK, n_samples=SAMPLE_RATE):
|
||||
return (peak * wave).astype(numpy.int16)
|
||||
|
||||
|
||||
def wurlitzer_wave(hz, peak=SAMPLE_PEAK, n_samples=SAMPLE_RATE):
|
||||
"""Wurlitzer electric piano — vibrating steel reed over a pickup.
|
||||
|
||||
Unlike the Rhodes (tine + tonebar), the Wurlitzer uses a flat
|
||||
steel reed that vibrates near an electrostatic pickup. The result
|
||||
is more nasal, reedy, and biting — especially when driven hard.
|
||||
Think Supertramp, Ray Charles, early Billy Joel. It barks and
|
||||
growls in a way the Rhodes never does.
|
||||
"""
|
||||
t = numpy.arange(n_samples, dtype=numpy.float64) / SAMPLE_RATE
|
||||
rng = numpy.random.default_rng(int(hz * 100) % 2**31)
|
||||
|
||||
# Faster decay than Rhodes — reeds don't sustain like tines
|
||||
decay = numpy.where(t < 0.1,
|
||||
numpy.exp(-5.0 * t),
|
||||
numpy.exp(-5.0 * 0.1) * numpy.exp(-2.0 * (t - 0.1)))
|
||||
|
||||
wave = numpy.zeros(n_samples, dtype=numpy.float64)
|
||||
brightness = numpy.clip((hz - 65) / 800, 0.0, 1.0)
|
||||
|
||||
# Reed harmonics — more odd harmonics than Rhodes (nasal character)
|
||||
reed_harmonics = [
|
||||
(1, 1.0),
|
||||
(2, 0.4), # less 2nd than Rhodes
|
||||
(3, 0.5 + 0.15 * brightness), # strong 3rd — the nasal quality
|
||||
(4, 0.15),
|
||||
(5, 0.25 + 0.1 * brightness), # strong odd harmonics
|
||||
(6, 0.08),
|
||||
(7, 0.12), # 7th present — reed buzz
|
||||
]
|
||||
|
||||
for n, amp in reed_harmonics:
|
||||
f_n = hz * n
|
||||
if f_n >= SAMPLE_RATE / 2:
|
||||
break
|
||||
h_decay = decay * numpy.exp(-(1.0 + 0.4 * brightness) * (n - 1) * t)
|
||||
phase = rng.uniform(0, 2 * numpy.pi)
|
||||
wave += amp * numpy.sin(2 * numpy.pi * f_n * t + phase) * h_decay
|
||||
|
||||
# Reed buzz — slight asymmetric distortion at attack
|
||||
# This is the "bark" when you hit hard
|
||||
attack_len = min(int(SAMPLE_RATE * 0.03), n_samples)
|
||||
attack_env = numpy.zeros(n_samples, dtype=numpy.float64)
|
||||
attack_env[:attack_len] = numpy.exp(-numpy.linspace(0, 6, attack_len))
|
||||
wave += numpy.tanh(wave * 3.0 * attack_env) * 0.15
|
||||
|
||||
# Electrostatic pickup character — slightly compressed/nasal
|
||||
wave = numpy.tanh(wave * 1.1) / 1.1
|
||||
|
||||
mx = numpy.abs(wave).max()
|
||||
if mx > 0:
|
||||
wave /= mx
|
||||
|
||||
return (peak * wave).astype(numpy.int16)
|
||||
|
||||
|
||||
def vibraphone_wave(hz, peak=SAMPLE_PEAK, n_samples=SAMPLE_RATE):
|
||||
"""Vibraphone — struck aluminum bars with motor-driven tremolo.
|
||||
|
||||
Metal bars hit with soft mallets, resonator tubes underneath,
|
||||
and a spinning disc (motor) that modulates the sound creating
|
||||
the signature vibraphone shimmer/tremolo.
|
||||
"""
|
||||
t = numpy.arange(n_samples, dtype=numpy.float64) / SAMPLE_RATE
|
||||
rng = numpy.random.default_rng(int(hz * 100) % 2**31)
|
||||
|
||||
# Long sustain — bars ring for seconds
|
||||
decay = numpy.exp(-0.8 * t)
|
||||
|
||||
wave = numpy.zeros(n_samples, dtype=numpy.float64)
|
||||
|
||||
# Metal bar modes — slightly inharmonic
|
||||
bar_modes = [
|
||||
(1.0, 1.0), # fundamental
|
||||
(2.76, 0.3), # first overtone (not 2x — bars are inharmonic)
|
||||
(5.4, 0.12), # second overtone
|
||||
(8.93, 0.04), # third
|
||||
]
|
||||
|
||||
for ratio, amp in bar_modes:
|
||||
f = hz * ratio
|
||||
if f >= SAMPLE_RATE / 2:
|
||||
break
|
||||
mode_decay = decay * numpy.exp(-0.5 * (ratio - 1) * t)
|
||||
phase = rng.uniform(0, 2 * numpy.pi)
|
||||
wave += amp * numpy.sin(2 * numpy.pi * f * t + phase) * mode_decay
|
||||
|
||||
# Motor tremolo — spinning disc modulates amplitude at ~5-7 Hz
|
||||
motor_rate = 5.5
|
||||
motor_depth = 0.35
|
||||
# Motor takes a moment to spin up
|
||||
motor_env = 1.0 - numpy.exp(-2.0 * t)
|
||||
tremolo = 1.0 - motor_depth * motor_env * (0.5 + 0.5 * numpy.sin(2 * numpy.pi * motor_rate * t))
|
||||
wave *= tremolo
|
||||
|
||||
# Soft mallet attack
|
||||
mallet_len = min(int(SAMPLE_RATE * 0.005), n_samples)
|
||||
mallet = rng.uniform(-0.15, 0.15, mallet_len).astype(numpy.float64)
|
||||
mallet *= numpy.exp(-numpy.linspace(0, 12, mallet_len))
|
||||
wave[:mallet_len] += mallet
|
||||
|
||||
mx = numpy.abs(wave).max()
|
||||
if mx > 0:
|
||||
wave /= mx
|
||||
|
||||
return (peak * wave).astype(numpy.int16)
|
||||
|
||||
|
||||
def pipe_organ_wave(hz, peak=SAMPLE_PEAK, n_samples=SAMPLE_RATE):
|
||||
"""Pipe organ — air through ranks of pipes, multiple stops.
|
||||
|
||||
The pipe organ is additive synthesis incarnate — each stop adds
|
||||
a rank of pipes at a specific harmonic. We model a classic
|
||||
registration: principal 8', octave 4', fifteenth 2', mixture.
|
||||
Constant air pressure means no dynamics — always full and sustained.
|
||||
"""
|
||||
t = numpy.arange(n_samples, dtype=numpy.float64) / SAMPLE_RATE
|
||||
|
||||
wave = numpy.zeros(n_samples, dtype=numpy.float64)
|
||||
|
||||
# Principal 8' — the fundamental organ tone
|
||||
# Pipe harmonics with subtle wind noise
|
||||
for n in range(1, 12):
|
||||
f_n = hz * n
|
||||
if f_n >= SAMPLE_RATE / 2:
|
||||
break
|
||||
# Pipe spectral shape — principalish
|
||||
if n == 1:
|
||||
amp = 1.0
|
||||
elif n == 2:
|
||||
amp = 0.6
|
||||
elif n == 3:
|
||||
amp = 0.4
|
||||
elif n <= 6:
|
||||
amp = 0.2 / n
|
||||
else:
|
||||
amp = 0.08 / n
|
||||
wave += amp * numpy.sin(2 * numpy.pi * f_n * t)
|
||||
|
||||
# Octave 4' stop — one octave up
|
||||
for n in range(1, 8):
|
||||
f_n = hz * 2 * n
|
||||
if f_n >= SAMPLE_RATE / 2:
|
||||
break
|
||||
amp = (0.4 if n == 1 else 0.15 / n)
|
||||
wave += amp * numpy.sin(2 * numpy.pi * f_n * t)
|
||||
|
||||
# Fifteenth 2' — two octaves up, brightness
|
||||
wave += 0.2 * numpy.sin(2 * numpy.pi * hz * 4 * t)
|
||||
wave += 0.08 * numpy.sin(2 * numpy.pi * hz * 5 * t)
|
||||
|
||||
# Subtle wind/chiff noise at attack
|
||||
chiff_len = min(int(SAMPLE_RATE * 0.04), n_samples)
|
||||
chiff = _noise(chiff_len).astype(numpy.float64) * 0.08
|
||||
chiff *= numpy.exp(-numpy.linspace(0, 10, chiff_len))
|
||||
wave[:chiff_len] += chiff
|
||||
|
||||
# Constant amplitude — organ doesn't decay
|
||||
# Just a tiny fade-in to avoid click
|
||||
fadein = min(int(SAMPLE_RATE * 0.01), n_samples)
|
||||
wave[:fadein] *= numpy.linspace(0, 1, fadein)
|
||||
|
||||
mx = numpy.abs(wave).max()
|
||||
if mx > 0:
|
||||
wave /= mx
|
||||
|
||||
return (peak * wave).astype(numpy.int16)
|
||||
|
||||
|
||||
def choir_wave(hz, peak=SAMPLE_PEAK, n_samples=SAMPLE_RATE, lyric="ah"):
|
||||
"""Choir — voices singing vowels shaped by strong formant filters.
|
||||
|
||||
The key to vocal sound is FORMANTS — resonant peaks from the
|
||||
vocal tract shape. We generate a rich glottal source then filter
|
||||
it hard through formant bandpass filters. The formants are what
|
||||
make "ah" sound different from "oo".
|
||||
"""
|
||||
t = numpy.arange(n_samples, dtype=numpy.float64) / SAMPLE_RATE
|
||||
|
||||
# Vowel formant frequencies + bandwidths (Hz) — F1, F2, F3, F4
|
||||
_FORMANTS = {
|
||||
"ah": [(730, 90), (1090, 110), (2440, 170), (3400, 250)],
|
||||
"ee": [(270, 60), (2290, 200), (3010, 300), (3500, 250)],
|
||||
"oh": [(570, 80), (840, 100), (2410, 170), (3400, 250)],
|
||||
"oo": [(300, 50), (870, 90), (2240, 170), (3400, 250)],
|
||||
"eh": [(530, 70), (1840, 150), (2480, 200), (3400, 250)],
|
||||
}
|
||||
formants = _FORMANTS.get(lyric, _FORMANTS["ah"])
|
||||
|
||||
# Glottal source — rich buzz with all harmonics
|
||||
n_harmonics = min(25, int((SAMPLE_RATE / 2) / hz))
|
||||
|
||||
# No per-harmonic vibrato — it causes amplitude wobble through formants.
|
||||
# Choir vibrato comes from the ensemble= parameter instead (natural
|
||||
# pitch variation between voices).
|
||||
source = numpy.zeros(n_samples, dtype=numpy.float64)
|
||||
for n in range(1, n_harmonics + 1):
|
||||
f_n = hz * n
|
||||
if f_n >= SAMPLE_RATE / 2:
|
||||
break
|
||||
# Glottal slope: -12dB/octave
|
||||
amp = 1.0 / (n * n) * 4.0
|
||||
source += amp * numpy.sin(2 * numpy.pi * f_n * t)
|
||||
|
||||
# Filter through formants — this is where the voice happens
|
||||
wave = numpy.zeros(n_samples, dtype=numpy.float64)
|
||||
for fc, bw in formants:
|
||||
lo = max(20, fc - bw)
|
||||
hi = min(SAMPLE_RATE // 2 - 1, fc + bw)
|
||||
if lo < hi:
|
||||
bp, ap = scipy.signal.butter(2, [lo, hi], btype='band', fs=SAMPLE_RATE)
|
||||
filtered = scipy.signal.lfilter(bp, ap, source)
|
||||
# Boost formants proportionally
|
||||
gain = 1.0 if fc < 1000 else 0.7
|
||||
wave += filtered * gain
|
||||
|
||||
# Breathy onset — air before phonation
|
||||
breath_len = min(int(SAMPLE_RATE * 0.08), n_samples)
|
||||
breath = _noise(breath_len).astype(numpy.float64) * 0.04
|
||||
# Filter breath through formants too
|
||||
for fc, bw in formants[:2]:
|
||||
lo = max(20, fc - bw * 2)
|
||||
hi = min(SAMPLE_RATE // 2 - 1, fc + bw * 2)
|
||||
if lo < hi:
|
||||
bp, ap = scipy.signal.butter(1, [lo, hi], btype='band', fs=SAMPLE_RATE)
|
||||
breath = scipy.signal.lfilter(bp, ap, numpy.pad(breath, (0, max(0, n_samples - breath_len))))[:breath_len]
|
||||
breath *= numpy.exp(-numpy.linspace(0, 5, breath_len))
|
||||
wave[:breath_len] += breath
|
||||
|
||||
# Gentle attack
|
||||
attack_len = min(int(SAMPLE_RATE * 0.06), n_samples)
|
||||
wave[:attack_len] *= numpy.linspace(0, 1, attack_len)
|
||||
|
||||
mx = numpy.abs(wave).max()
|
||||
if mx > 0:
|
||||
wave /= mx
|
||||
|
||||
return (peak * wave).astype(numpy.int16)
|
||||
|
||||
|
||||
def bass_guitar_wave(hz, peak=SAMPLE_PEAK, n_samples=SAMPLE_RATE):
|
||||
"""Bass guitar — plucked thick string with magnetic pickup.
|
||||
|
||||
@@ -1959,6 +2199,8 @@ _SYNTH_FUNCTIONS = {
|
||||
"pwm_slow": pwm_slow_wave, "pwm_fast": pwm_fast_wave,
|
||||
"pluck_synth": pluck_wave, "organ_synth": organ_wave,
|
||||
"strings_synth": strings_wave, "piano_synth": piano_wave, "rhodes_synth": rhodes_wave,
|
||||
"wurlitzer_synth": wurlitzer_wave, "vibraphone_synth": vibraphone_wave,
|
||||
"pipe_organ_synth": pipe_organ_wave, "choir_synth": choir_wave,
|
||||
"bass_guitar_synth": bass_guitar_wave, "flute_synth": flute_wave,
|
||||
"trumpet_synth": trumpet_wave, "clarinet_synth": clarinet_wave,
|
||||
"marimba_synth": marimba_wave, "oboe_synth": oboe_wave,
|
||||
|
||||
+12
-6
@@ -23,6 +23,15 @@ INSTRUMENTS = {
|
||||
"tremolo_depth": 0.12, "tremolo_rate": 4.5,
|
||||
"analog": 0.15,
|
||||
},
|
||||
"wurlitzer": {
|
||||
"synth": "wurlitzer_synth", "envelope": "none",
|
||||
"tremolo_depth": 0.18, "tremolo_rate": 5.0,
|
||||
"analog": 0.2,
|
||||
},
|
||||
"pipe_organ": {
|
||||
"synth": "pipe_organ_synth", "envelope": "none",
|
||||
"reverb": 0.5, "reverb_type": "cathedral",
|
||||
},
|
||||
"organ": {
|
||||
"synth": "organ_synth", "envelope": "organ",
|
||||
"chorus": 0.2, "chorus_rate": 5.5,
|
||||
@@ -303,8 +312,8 @@ INSTRUMENTS = {
|
||||
"humanize": 0.15,
|
||||
},
|
||||
"choir": {
|
||||
"synth": "vocal_synth", "envelope": "pad",
|
||||
"detune": 8, "spread": 0.4,
|
||||
"synth": "choir_synth", "envelope": "none",
|
||||
"detune": 6, "spread": 0.3, "ensemble": 6,
|
||||
"reverb": 0.45, "reverb_type": "cathedral",
|
||||
},
|
||||
"granular_texture": {
|
||||
@@ -321,10 +330,7 @@ INSTRUMENTS = {
|
||||
|
||||
# ── Percussion / Mallet ──
|
||||
"vibraphone": {
|
||||
"synth": "fm", "envelope": "mallet",
|
||||
"fm_ratio": 1.0, "fm_index": 1.0,
|
||||
"lowpass": 5000,
|
||||
"tremolo_depth": 0.3, "tremolo_rate": 5.5,
|
||||
"synth": "vibraphone_synth", "envelope": "none",
|
||||
"reverb": 0.3, "reverb_type": "plate",
|
||||
},
|
||||
"marimba": {
|
||||
|
||||
Reference in New Issue
Block a user