diff --git a/docs/_static/audio/synth_fm.wav b/docs/_static/audio/synth_fm.wav
index 705e3cc..6b8992f 100644
Binary files a/docs/_static/audio/synth_fm.wav and b/docs/_static/audio/synth_fm.wav differ
diff --git a/docs/_static/audio/synth_rhodes.wav b/docs/_static/audio/synth_rhodes.wav
new file mode 100644
index 0000000..a66d362
Binary files /dev/null and b/docs/_static/audio/synth_rhodes.wav differ
diff --git a/docs/generate_audio.py b/docs/generate_audio.py
index d667806..2a32649 100644
--- a/docs/generate_audio.py
+++ b/docs/generate_audio.py
@@ -465,7 +465,23 @@ def gen_synth_pwm_fast():
_synth_demo("pwm_fast", "pwm_fast")
def gen_synth_fm():
- _synth_demo("fm", "fm", envelope="piano")
+ score = Score("4/4", bpm=100)
+ p = score.part("demo", synth="fm", envelope="bell", volume=0.5,
+ fm_ratio=3.0, fm_index=5.0, reverb=0.3)
+ for n in ["C5", "E5", "G5", "C6", "G5", "E5", "C5", "E5"]:
+ p.add(n, Duration.QUARTER, velocity=80)
+ render("synth_fm", score)
+
+def gen_synth_rhodes():
+ score = Score("4/4", bpm=80)
+ p = score.part("demo", instrument="electric_piano", volume=0.5, reverb=0.3)
+ # Jazz chords with hold
+ p.hold("C3", Duration.WHOLE * 2, velocity=60)
+ p.hold("E3", Duration.WHOLE * 2, velocity=55)
+ p.hold("Bb3", Duration.WHOLE * 2, velocity=55)
+ for n in ["G4", "Bb4", "C5", "Bb4", "G4", "F4", "E4", "G4"]:
+ p.add(n, Duration.QUARTER, velocity=75)
+ render("synth_rhodes", score)
def gen_synth_supersaw():
_synth_demo("supersaw", "supersaw", envelope="pad")
@@ -823,6 +839,7 @@ GENERATORS = [
gen_synth_pwm_slow,
gen_synth_pwm_fast,
gen_synth_fm,
+ gen_synth_rhodes,
gen_synth_supersaw,
gen_synth_piano,
gen_synth_bass_guitar,
diff --git a/docs/guide/synths.rst b/docs/guide/synths.rst
index 7a29a32..a8c8edf 100644
--- a/docs/guide/synths.rst
+++ b/docs/guide/synths.rst
@@ -129,14 +129,16 @@ the electric piano in every Whitney Houston ballad, the bass in every
Depeche Mode track, the bells in a thousand TV jingles. If you heard
pop music in the 80s, you heard FM synthesis.
-**Use for:** electric piano (rhodes), bells, metallic leads, jazz chords.
+**Use for:** bells, metallic leads, glassy pads, DX7-style sounds.
.. code-block:: python
- rhodes = score.part(
- "rhodes",
+ bells = score.part(
+ "bells",
synth="fm",
- envelope="piano",
+ envelope="bell",
+ fm_ratio=3.0,
+ fm_index=5.0,
volume=0.3,
reverb=0.4,
)
@@ -459,6 +461,24 @@ soundboard.
+Rhodes Electric Piano
+~~~~~~~~~~~~~~~~~~~~~
+
+The Fender Rhodes — a rubber-tipped hammer strikes a steel tine
+next to a tonebar, picked up by an electromagnetic pickup. Warm,
+bell-like, with a bright metallic attack that mellows into a
+singing sustain. The sound of jazz clubs, soul, and neo-soul.
+
+.. code-block:: python
+
+ rhodes = score.part("rhodes", synth="rhodes_synth")
+ # Or use the instrument preset (adds tremolo + chorus)
+ rhodes = score.part("rhodes", instrument="electric_piano")
+
+.. raw:: html
+
+
+
Bass Guitar Synth
~~~~~~~~~~~~~~~~~
diff --git a/pytheory/play.py b/pytheory/play.py
index 96557df..cdc18df 100644
--- a/pytheory/play.py
+++ b/pytheory/play.py
@@ -416,6 +416,68 @@ def piano_wave(hz, peak=SAMPLE_PEAK, n_samples=SAMPLE_RATE):
return (peak * wave).astype(numpy.int16)
+def rhodes_wave(hz, peak=SAMPLE_PEAK, n_samples=SAMPLE_RATE):
+ """Rhodes electric piano — tine struck by hammer, electromagnetic pickup.
+
+ The Rhodes sound comes from a rubber-tipped hammer hitting a thin
+ steel tine next to a tonebar. The tine vibrates near an electromagnetic
+ pickup (like a guitar pickup), producing a warm, bell-like tone with:
+ 1. Strong fundamental + 2nd harmonic (tine character)
+ 2. Bright metallic attack that mellows quickly (hammer on tine)
+ 3. Bell-like inharmonic partials on soft hits, bark on hard hits
+ 4. Asymmetric waveform from the pickup's nonlinear response
+ """
+ t = numpy.arange(n_samples, dtype=numpy.float64) / SAMPLE_RATE
+ rng = numpy.random.default_rng(int(hz * 100) % 2**31)
+
+ # Two-stage decay: quick initial drop, then long sustain
+ decay = numpy.where(t < 0.15,
+ numpy.exp(-4.0 * t),
+ numpy.exp(-4.0 * 0.15) * numpy.exp(-1.2 * (t - 0.15)))
+
+ wave = numpy.zeros(n_samples, dtype=numpy.float64)
+
+ # Brightness scales with pitch
+ brightness = numpy.clip((hz - 65) / 800, 0.0, 1.0)
+
+ # Tine harmonics — strong fundamental, prominent 2nd, bell-like upper
+ tine_harmonics = [
+ (1, 1.0), # fundamental
+ (2, 0.6 + 0.15 * brightness), # 2nd — the Rhodes character
+ (3, 0.15 + 0.1 * brightness), # 3rd — adds warmth
+ (4, 0.08 + 0.08 * brightness), # 4th — bell quality
+ (5, 0.04), # 5th — subtle shimmer
+ (6, 0.02), # 6th
+ ]
+
+ for n, amp in tine_harmonics:
+ f_n = hz * n
+ if f_n >= SAMPLE_RATE / 2:
+ break
+ # Higher harmonics decay faster
+ h_decay = decay * numpy.exp(-(0.8 + 0.3 * brightness) * (n - 1) * t)
+ phase = rng.uniform(0, 2 * numpy.pi)
+ wave += amp * numpy.sin(2 * numpy.pi * f_n * t + phase) * h_decay
+
+ # Hammer-on-tine transient — bright metallic click
+ click_len = min(int(SAMPLE_RATE * 0.008), n_samples)
+ click_t = numpy.arange(click_len, dtype=numpy.float64) / SAMPLE_RATE
+ # Inharmonic tine ring at attack (bell partials)
+ click = (numpy.sin(2 * numpy.pi * hz * 5.3 * click_t) * 0.15 +
+ numpy.sin(2 * numpy.pi * hz * 7.1 * click_t) * 0.08)
+ click *= numpy.exp(-numpy.linspace(0, 20, click_len))
+ wave[:click_len] += click
+
+ # Subtle asymmetry from pickup nonlinearity (soft saturation)
+ wave = numpy.tanh(wave * 1.2) / 1.2
+
+ 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.
@@ -1872,7 +1934,7 @@ _SYNTH_FUNCTIONS = {
"noise": noise_wave, "supersaw": supersaw_wave,
"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,
+ "strings_synth": strings_wave, "piano_synth": piano_wave, "rhodes_synth": rhodes_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,
diff --git a/pytheory/rhythm.py b/pytheory/rhythm.py
index 463fd57..210be57 100644
--- a/pytheory/rhythm.py
+++ b/pytheory/rhythm.py
@@ -17,13 +17,11 @@ INSTRUMENTS = {
"synth": "piano_synth", "envelope": "none",
"vel_to_filter": 3000,
},
- "electric_piano": { # Rhodes/Wurlitzer
- "synth": "fm", "envelope": "piano",
- "fm_ratio": 1.0, "fm_index": 2.0,
- "detune": 6, "chorus": 0.2, "chorus_rate": 1.0,
- "lowpass": 4000, "saturation": 0.15,
- "tremolo_depth": 0.15, "tremolo_rate": 4.5,
- "analog": 0.2,
+ "electric_piano": { # Rhodes
+ "synth": "rhodes_synth", "envelope": "none",
+ "chorus": 0.15, "chorus_rate": 1.0,
+ "tremolo_depth": 0.12, "tremolo_rate": 4.5,
+ "analog": 0.15,
},
"organ": {
"synth": "organ_synth", "envelope": "organ",