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:
2026-03-29 18:21:12 -04:00
parent b8d1fe5e81
commit f7d8f08446
8 changed files with 359 additions and 6 deletions
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
+39
View File
@@ -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,
+66
View File
@@ -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
~~~~~~~~~~~~~~~~~
+242
View File
@@ -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
View File
@@ -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": {