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",