diff --git a/CHANGELOG.md b/CHANGELOG.md index b185703..dc716ae 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,18 @@ All notable changes to PyTheory are documented here. +## 0.40.3 + +- **Crotales synth** — tuned bronze discs with long ring and bright harmonics +- **Tingsha synth** — paired Tibetan cymbals with beating from two detuned discs +- **Rain stick** — cascading pebbles (steep and slow/shallow variants) +- **Ocean drum** — steel beads rolling inside a frame drum, surf wash +- **Cabasa** — metal bead chain on cylinder, bright metallic scrape +- **Wind chimes** — multiple suspended metal tubes ringing at random offsets +- **Finger cymbal** — single zill tap, bright metallic ping +- `crotales`, `tingsha`, `singing_bowl`, `singing_bowl_ring` instrument presets +- Audio demos in docs for all new sounds + ## 0.40.2 - **Master compressor dialed back** — threshold raised from 0.5 to 0.7, diff --git a/docs/_static/audio/cabasa.wav b/docs/_static/audio/cabasa.wav new file mode 100644 index 0000000..8ad97eb Binary files /dev/null and b/docs/_static/audio/cabasa.wav differ diff --git a/docs/_static/audio/finger_cymbal.wav b/docs/_static/audio/finger_cymbal.wav new file mode 100644 index 0000000..ef2f6a0 Binary files /dev/null and b/docs/_static/audio/finger_cymbal.wav differ diff --git a/docs/_static/audio/ocean_drum.wav b/docs/_static/audio/ocean_drum.wav new file mode 100644 index 0000000..9172c2c Binary files /dev/null and b/docs/_static/audio/ocean_drum.wav differ diff --git a/docs/_static/audio/rainstick.wav b/docs/_static/audio/rainstick.wav new file mode 100644 index 0000000..01f176a Binary files /dev/null and b/docs/_static/audio/rainstick.wav differ diff --git a/docs/_static/audio/rainstick_slow.wav b/docs/_static/audio/rainstick_slow.wav new file mode 100644 index 0000000..54fc58a Binary files /dev/null and b/docs/_static/audio/rainstick_slow.wav differ diff --git a/docs/_static/audio/synth_crotales.wav b/docs/_static/audio/synth_crotales.wav new file mode 100644 index 0000000..c330259 Binary files /dev/null and b/docs/_static/audio/synth_crotales.wav differ diff --git a/docs/_static/audio/synth_tingsha.wav b/docs/_static/audio/synth_tingsha.wav new file mode 100644 index 0000000..abc7f77 Binary files /dev/null and b/docs/_static/audio/synth_tingsha.wav differ diff --git a/docs/_static/audio/wind_chimes.wav b/docs/_static/audio/wind_chimes.wav new file mode 100644 index 0000000..7832b55 Binary files /dev/null and b/docs/_static/audio/wind_chimes.wav differ diff --git a/docs/generate_audio.py b/docs/generate_audio.py index 576baf4..63e2c1a 100644 --- a/docs/generate_audio.py +++ b/docs/generate_audio.py @@ -858,6 +858,68 @@ def gen_synth_granular(): render("synth_granular", score) +def gen_synth_crotales(): + score = Score("4/4", bpm=60) + p = score.part("demo", synth="crotales_synth", envelope="none", + volume=0.5, reverb=0.3) + for n in ["C6", "E6", "G6", "C7", "G6", "E6", "C6"]: + p.add(n, Duration.HALF, velocity=80) + render("synth_crotales", score) + + +def gen_synth_tingsha(): + score = Score("4/4", bpm=40) + p = score.part("demo", synth="tingsha_synth", envelope="none", + volume=0.5, reverb=0.4) + for n in ["E5", "A5", "E6", "A5"]: + p.add(n, Duration.WHOLE, velocity=75) + render("synth_tingsha", score) + + +def gen_rainstick(): + score = Score("4/4", bpm=60) + p = score.part("demo", synth="sine", volume=1.0) + p.hit(DrumSound.RAINSTICK, Duration.WHOLE * 3, velocity=90) + render("rainstick", score) + + +def gen_rainstick_slow(): + score = Score("4/4", bpm=60) + p = score.part("demo", synth="sine", volume=1.0) + p.hit(DrumSound.RAINSTICK_SLOW, Duration.WHOLE * 4, velocity=85) + render("rainstick_slow", score) + + +def gen_ocean_drum(): + score = Score("4/4", bpm=60) + p = score.part("demo", synth="sine", volume=1.0) + p.hit(DrumSound.OCEAN_DRUM, Duration.WHOLE * 3, velocity=85) + render("ocean_drum", score) + + +def gen_cabasa(): + score = Score("4/4", bpm=100) + p = score.part("demo", synth="sine", volume=1.0) + for _ in range(16): + p.hit(DrumSound.CABASA, Duration.EIGHTH, velocity=100) + render("cabasa", score) + + +def gen_wind_chimes(): + score = Score("4/4", bpm=60) + p = score.part("demo", synth="sine", volume=1.0) + p.hit(DrumSound.WIND_CHIMES, Duration.WHOLE * 3, velocity=85) + render("wind_chimes", score) + + +def gen_finger_cymbal(): + score = Score("4/4", bpm=80) + p = score.part("demo", synth="sine", volume=1.0) + for _ in range(8): + p.hit(DrumSound.FINGER_CYMBAL, Duration.QUARTER, velocity=85) + render("finger_cymbal", score) + + def gen_synth_singing_bowl_strike(): score = Score("4/4", bpm=40) p = score.part("demo", synth="singing_bowl_strike_synth", envelope="none", @@ -1078,8 +1140,16 @@ GENERATORS = [ gen_synth_mandolin, gen_synth_ukulele, gen_synth_granular, + gen_synth_crotales, + gen_synth_tingsha, gen_synth_singing_bowl_strike, gen_synth_singing_bowl_ring, + gen_rainstick, + gen_rainstick_slow, + gen_ocean_drum, + gen_cabasa, + gen_wind_chimes, + gen_finger_cymbal, gen_arpeggio, gen_legato_glide, gen_acid_house, diff --git a/docs/guide/synths.rst b/docs/guide/synths.rst index 0dfc0dd..5d78fa7 100644 --- a/docs/guide/synths.rst +++ b/docs/guide/synths.rst @@ -930,6 +930,40 @@ Parameters (passed as synth kwargs): +Crotales +~~~~~~~~ + +Small tuned bronze discs (antique cymbals) struck with brass mallets. +Bright, crystalline, bell-like tone with strong upper harmonics that +rings for a long time. Nearly harmonic partials give crotales their +penetrating brilliance — they cut through any orchestra. + +.. code-block:: python + + crotales = score.part("crotales", synth="crotales_synth", envelope="none", + reverb=0.3) + +.. raw:: html + + + +Tingsha +~~~~~~~ + +Two small Tibetan cymbals joined by a cord, clashed together. Both discs +ring at slightly different frequencies, producing a bright ping with +pronounced beating — the wavering interference between the two is the +whole character of the sound. + +.. code-block:: python + + tingsha = score.part("tingsha", synth="tingsha_synth", envelope="none", + reverb=0.4) + +.. raw:: html + + + Singing Bowl (Strike) ~~~~~~~~~~~~~~~~~~~~~ @@ -963,6 +997,76 @@ and out as the bowl resonates. +Rain Stick +~~~~~~~~~~ + +Cascading pebbles through a cactus tube with internal pins. Two variants: +steep angle (fast cascade) and shallow angle (slow trickle). + +.. code-block:: python + + p.hit(DrumSound.RAINSTICK, Duration.WHOLE * 3) # steep — fast cascade + p.hit(DrumSound.RAINSTICK_SLOW, Duration.WHOLE * 4) # shallow — gentle trickle + +.. raw:: html + + + + +Ocean Drum +~~~~~~~~~~ + +Steel beads rolling inside a frame drum — tilting produces a smooth surf wash. + +.. code-block:: python + + p.hit(DrumSound.OCEAN_DRUM, Duration.WHOLE * 3) + +.. raw:: html + + + +Cabasa +~~~~~~ + +Metal bead chain scraped against a textured cylinder — brighter and +more metallic than a shaker. + +.. code-block:: python + + p.hit(DrumSound.CABASA, Duration.EIGHTH) + +.. raw:: html + + + +Wind Chimes +~~~~~~~~~~~ + +Suspended metal tubes struck by hand or breeze. Each tube rings at +its own pitch with slight time offsets. + +.. code-block:: python + + p.hit(DrumSound.WIND_CHIMES, Duration.WHOLE * 3) + +.. raw:: html + + + +Finger Cymbal +~~~~~~~~~~~~~ + +Single small cymbal tap (zill) — bright metallic ping. + +.. code-block:: python + + p.hit(DrumSound.FINGER_CYMBAL, Duration.HALF) + +.. raw:: html + + + Analog Oscillator Drift ~~~~~~~~~~~~~~~~~~~~~~~~ @@ -1018,13 +1122,13 @@ distorted_guitar, orange_crunch, metal_guitar, bass_guitar, upright_bass, harp, sitar, koto, banjo, mandolin, mandola, ukulele **World/Exotic**: pedal_steel, theremin, kalimba, steel_drum, didgeridoo, -bagpipe, singing_bowl, singing_bowl_ring +bagpipe, singing_bowl, singing_bowl_ring, tingsha **Synth**: synth_lead, synth_pad, synth_bass, acid_bass, 808_bass, granular_pad, granular_texture, vocal, choir **Percussion**: vibraphone, marimba, xylophone, glockenspiel, tubular_bells, -timpani +timpani, crotales Explicit kwargs override preset defaults: diff --git a/pyproject.toml b/pyproject.toml index 9d2b2b9..a609b81 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "pytheory" -version = "0.40.2" +version = "0.40.3" description = "Music Theory for Humans" readme = "README.md" license = "MIT" diff --git a/pytheory/__init__.py b/pytheory/__init__.py index 8bcda4c..edca49c 100644 --- a/pytheory/__init__.py +++ b/pytheory/__init__.py @@ -1,6 +1,6 @@ """PyTheory: Music Theory for Humans.""" -__version__ = "0.40.2" +__version__ = "0.40.3" from .tones import Tone, Interval from .systems import System, SYSTEMS, TET diff --git a/pytheory/play.py b/pytheory/play.py index 38037b9..4a29f5c 100644 --- a/pytheory/play.py +++ b/pytheory/play.py @@ -2048,6 +2048,99 @@ def sitar_wave(hz, peak=SAMPLE_PEAK, n_samples=SAMPLE_RATE): return (peak * out).astype(numpy.int16) +def crotales_wave(hz, peak=SAMPLE_PEAK, n_samples=SAMPLE_RATE): + """Crotales — small tuned bronze discs struck with brass mallets. + + Antique cymbals. Bright, crystalline, bell-like tone that rings + for a very long time. The partials are nearly harmonic (closer + to a bell than a bar) with strong upper harmonics that give + crotales their penetrating brilliance. Played in the octave + above written — they cut through any orchestra. + """ + t = numpy.arange(n_samples, dtype=numpy.float64) / SAMPLE_RATE + rng = numpy.random.default_rng(int(hz * 100) % 2**31) + + wave = numpy.zeros(n_samples, dtype=numpy.float64) + + # Bronze disc modes — nearly harmonic, very bright. + # Higher partials are stronger than in most percussion, + # which is what gives crotales their cutting brilliance. + # (ratio, amplitude, decay_rate) + disc_modes = [ + (1.0, 1.0, 0.3), # fundamental — rings for ages + (2.0, 0.6, 0.4), # octave — strong + (3.01, 0.35, 0.6), # near-12th — slight inharmonicity + (4.03, 0.25, 0.9), # double octave + (5.06, 0.15, 1.3), # bright + (6.1, 0.08, 2.0), # shimmer + (8.15, 0.04, 3.0), # sparkle at the top + ] + + for ratio, amp, decay_rate in disc_modes: + f = hz * ratio + if f >= SAMPLE_RATE / 2: + break + phase = rng.uniform(0, 2 * numpy.pi) + mode_decay = numpy.exp(-decay_rate * t) + wave += amp * numpy.sin(2 * numpy.pi * f * t + phase) * mode_decay + + # Hard mallet strike — brass on bronze, bright transient + strike_len = min(int(SAMPLE_RATE * 0.002), n_samples) + strike_t = numpy.linspace(0, 1, strike_len) + strike = 0.5 * numpy.sin(2 * numpy.pi * hz * 8 * strike_t) * numpy.exp(-strike_t * 25) + wave[:strike_len] += strike + + mx = numpy.abs(wave).max() + if mx > 0: + wave /= mx + + return (peak * wave).astype(numpy.int16) + + +def tingsha_wave(hz, peak=SAMPLE_PEAK, n_samples=SAMPLE_RATE): + """Tingsha — two small Tibetan cymbals clashed together on a cord. + + When the pair strikes, both discs ring simultaneously at slightly + different frequencies (no two are identical), producing a bright + ping with pronounced beating. The sound is thinner and higher + than a singing bowl — a clear, cutting tone that fades over a + few seconds. The two-disc interference is the whole character. + """ + t = numpy.arange(n_samples, dtype=numpy.float64) / SAMPLE_RATE + rng = numpy.random.default_rng(int(hz * 100) % 2**31) + + # Two discs at slightly different pitches — this IS the tingsha sound + detune = hz * 0.008 # ~14 cents apart, creates ~3-4 Hz beat at middle C + disc_a = numpy.sin(2 * numpy.pi * (hz - detune) * t) + disc_b = numpy.sin(2 * numpy.pi * (hz + detune) * t + rng.uniform(0, 2 * numpy.pi)) + wave = (disc_a + disc_b) * 0.5 + + # Upper partials — both discs, slightly different inharmonicity + for ratio, amp, dec in [(2.72, 0.3, 5.0), (5.1, 0.12, 10.0), (8.3, 0.05, 18.0)]: + if hz * ratio >= SAMPLE_RATE / 2: + break + p1 = rng.uniform(0, 2 * numpy.pi) + p2 = rng.uniform(0, 2 * numpy.pi) + wave += amp * numpy.sin(2 * numpy.pi * hz * ratio * 0.998 * t + p1) * numpy.exp(-dec * t) + wave += amp * numpy.sin(2 * numpy.pi * hz * ratio * 1.002 * t + p2) * numpy.exp(-dec * t) + + # Decay — medium ring, not as long as a singing bowl + decay = numpy.exp(-1.8 * t) + wave *= decay + + # Clash transient — metal on metal, sharper than a mallet hit + clash_len = min(int(SAMPLE_RATE * 0.003), n_samples) + clash = rng.uniform(-0.4, 0.4, clash_len).astype(numpy.float64) + clash *= numpy.exp(-numpy.linspace(0, 20, clash_len)) + wave[:clash_len] += clash + + mx = numpy.abs(wave).max() + if mx > 0: + wave /= mx + + return (peak * wave).astype(numpy.int16) + + def singing_bowl_strike_wave(hz, peak=SAMPLE_PEAK, n_samples=SAMPLE_RATE): """Singing bowl strike — mallet hit that excites all modes at once. @@ -2287,6 +2380,8 @@ class Synth(Enum): ACOUSTIC_GUITAR = "acoustic_guitar_synth" SITAR = "sitar_synth" ELECTRIC_GUITAR = "electric_guitar_synth" + CROTALES = "crotales_synth" + TINGSHA = "tingsha_synth" SINGING_BOWL_STRIKE = "singing_bowl_strike_synth" SINGING_BOWL_RING = "singing_bowl_ring_synth" @@ -2319,6 +2414,8 @@ _SYNTH_FUNCTIONS = { "ukulele_synth": ukulele_wave, "acoustic_guitar_synth": acoustic_guitar_wave, "sitar_synth": sitar_wave, "electric_guitar_synth": electric_guitar_wave, + "crotales_synth": crotales_wave, + "tingsha_synth": tingsha_wave, "singing_bowl_strike_synth": singing_bowl_strike_wave, "singing_bowl_ring_synth": singing_bowl_ring_wave, } @@ -3575,6 +3672,227 @@ def _synth_guiro(n_samples): return wave +def _synth_rainstick_slow(n_samples): + """Rain stick (shallow angle): slow trickle, longer cascade, sparser impacts.""" + wave = numpy.zeros(n_samples, dtype=numpy.float32) + rng = numpy.random.default_rng(77) + + cascade_len = min(n_samples, int(SAMPLE_RATE * 4.0)) + n_pebbles = 800 + # More uniform distribution — shallow angle means steadier flow + positions = rng.beta(1.2, 1.8, n_pebbles) * cascade_len + positions = positions.astype(int) + + for pos in positions: + if pos >= n_samples - 100: + continue + peb_len = rng.integers(25, 90) + end = min(pos + peb_len, n_samples) + actual = end - pos + click = rng.uniform(-1.0, 1.0, actual).astype(numpy.float32) + click *= numpy.exp(-numpy.linspace(0, 10, actual).astype(numpy.float32)) + click *= rng.uniform(0.03, 0.18) + wave[pos:end] += click + + t = numpy.arange(cascade_len, dtype=numpy.float32) / SAMPLE_RATE + body = numpy.sin(2 * numpy.pi * 160 * t) * 0.04 + body *= numpy.exp(-0.8 * t) + wave[:cascade_len] += body + + full_env = numpy.ones(n_samples, dtype=numpy.float32) + fade_len = min(int(SAMPLE_RATE * 1.2), n_samples) + if fade_len > 0 and cascade_len > fade_len: + full_env[cascade_len - fade_len:cascade_len] = numpy.linspace( + 1.0, 0.0, fade_len).astype(numpy.float32) + full_env[cascade_len:] = 0.0 + wave *= full_env + + mx = numpy.abs(wave).max() + if mx > 0: + wave /= mx * 1.5 + return wave + + +def _synth_ocean_drum(n_samples): + """Ocean drum: steel beads rolling inside a frame drum — surf wash. + + Tilt the drum and the beads cascade across the internal head, + producing a smooth wash that sounds like ocean waves. + """ + wave = numpy.zeros(n_samples, dtype=numpy.float32) + rng = numpy.random.default_rng(55) + + wash_len = min(n_samples, int(SAMPLE_RATE * 2.5)) + t = numpy.arange(wash_len, dtype=numpy.float32) / SAMPLE_RATE + + # Dense bead noise — smoother than rain stick (steel beads on drum head) + noise = rng.standard_normal(wash_len).astype(numpy.float32) + # Bandpass to ~1-6kHz — beads on mylar head + import scipy.signal as _sig + bp, ap = _sig.butter(2, [1000, 6000], btype='band', fs=SAMPLE_RATE) + noise = _sig.lfilter(bp, ap, noise).astype(numpy.float32) + + # Swell envelope — wave comes in, peaks, recedes + swell = numpy.abs(numpy.sin(numpy.pi * t / t[-1])) ** 0.7 if wash_len > 0 else noise + noise *= swell * 0.5 + + # Drum body resonance + body = numpy.sin(2 * numpy.pi * 120 * t) * 0.08 * swell + + wave[:wash_len] = noise + body + + mx = numpy.abs(wave).max() + if mx > 0: + wave /= mx * 1.3 + return wave + + +def _synth_cabasa(n_samples): + """Cabasa: metal bead chain scraped against a cylinder. + + Brighter and more metallic than a shaker — the beads are steel + chain wrapped around a textured metal cylinder. + """ + n = min(n_samples, int(SAMPLE_RATE * 0.08)) + t = numpy.arange(n, dtype=numpy.float32) / SAMPLE_RATE + rng = numpy.random.default_rng(33) + + # Metallic noise — brighter than shaker + noise = rng.standard_normal(n).astype(numpy.float32) + # High-pass to emphasize the metallic chain sound + env = numpy.exp(-25 * t) + 0.4 * numpy.exp(-6 * t) + wave = noise * env * 0.5 + # Metal bead resonances + wave += numpy.sin(2 * numpy.pi * 7500 * t) * 0.12 * numpy.exp(-30 * t) + wave += numpy.sin(2 * numpy.pi * 9200 * t) * 0.08 * numpy.exp(-35 * t) + + out = numpy.zeros(n_samples, dtype=numpy.float32) + out[:n] = wave + return out + + +def _synth_wind_chimes(n_samples): + """Wind chimes: multiple suspended metal tubes ringing at random intervals. + + Each tube has its own pitch and decay. A hand strike or breeze + sets several ringing at once with slight time offsets. + """ + wave = numpy.zeros(n_samples, dtype=numpy.float32) + rng = numpy.random.default_rng(22) + + chime_len = min(n_samples, int(SAMPLE_RATE * 3.0)) + t = numpy.arange(chime_len, dtype=numpy.float32) / SAMPLE_RATE + + # 6-8 tubes at different pitches — pentatonic-ish spread + tube_freqs = [1200, 1450, 1700, 2000, 2400, 2850, 3300] + for freq in tube_freqs: + # Each tube starts at a random offset (breeze hits them at different times) + offset = rng.integers(0, int(SAMPLE_RATE * 0.3)) + if offset >= chime_len: + continue + tube_t = t[offset:] + tube_local = tube_t - tube_t[0] + # Tube mode with slight inharmonicity + tone = numpy.sin(2 * numpy.pi * freq * tube_local) * 0.2 + tone += numpy.sin(2 * numpy.pi * freq * 2.73 * tube_local) * 0.06 + # Each tube decays independently + decay = numpy.exp(-rng.uniform(2.0, 4.0) * tube_local) + tone *= decay + # Slight amplitude variation + tone *= rng.uniform(0.5, 1.0) + wave[offset:chime_len] += tone[:chime_len - offset].astype(numpy.float32) + + mx = numpy.abs(wave).max() + if mx > 0: + wave /= mx + return wave + + +def _synth_finger_cymbal(n_samples): + """Finger cymbal (zill): single small cymbal tap — bright metallic ping.""" + n = min(n_samples, int(SAMPLE_RATE * 0.8)) + t = numpy.arange(n, dtype=numpy.float32) / SAMPLE_RATE + rng = numpy.random.default_rng(11) + + # High-pitched metallic modes + wave = numpy.sin(2 * numpy.pi * 3200 * t).astype(numpy.float32) * 0.5 + wave += numpy.sin(2 * numpy.pi * 3210 * t).astype(numpy.float32) * 0.5 # beating pair + wave += numpy.sin(2 * numpy.pi * 7800 * t).astype(numpy.float32) * 0.15 * numpy.exp(-8 * t).astype(numpy.float32) + wave += numpy.sin(2 * numpy.pi * 12500 * t).astype(numpy.float32) * 0.06 * numpy.exp(-15 * t).astype(numpy.float32) + + wave *= numpy.exp(-3.0 * t).astype(numpy.float32) + + # Tap transient + tap_len = min(int(SAMPLE_RATE * 0.001), n) + wave[:tap_len] += rng.uniform(-0.2, 0.2, tap_len).astype(numpy.float32) + + out = numpy.zeros(n_samples, dtype=numpy.float32) + out[:n] = wave + mx = numpy.abs(out).max() + if mx > 0: + out /= mx + return out + + +def _synth_rainstick(n_samples): + """Rain stick: cascading pebbles through a cactus tube with internal pins. + + Hundreds of tiny seed/pebble impacts falling through the tube, + each one a brief high-frequency click with a hint of resonance + from the hollow body. The density tapers off as gravity runs out. + """ + wave = numpy.zeros(n_samples, dtype=numpy.float32) + rng = numpy.random.default_rng(42) + + # Duration of the cascade — up to 2.5 seconds + cascade_len = min(n_samples, int(SAMPLE_RATE * 2.5)) + + # Generate random pebble impacts — denser at the start, sparse at the end + n_pebbles = 800 + # Positions weighted toward the beginning (gravity) + positions = rng.beta(1.5, 3.0, n_pebbles) * cascade_len + positions = positions.astype(int) + + for pos in positions: + if pos >= n_samples - 100: + continue + # Each pebble: tiny noise click with random pitch resonance + peb_len = rng.integers(20, 80) + end = min(pos + peb_len, n_samples) + actual = end - pos + + # Noise click + click = rng.uniform(-1.0, 1.0, actual).astype(numpy.float32) + # Fast decay + click *= numpy.exp(-numpy.linspace(0, 12, actual).astype(numpy.float32)) + # Random amplitude — some pebbles louder than others + click *= rng.uniform(0.05, 0.25) + wave[pos:end] += click + + # Tube body resonance — hollow cactus, low rumble underneath + t = numpy.arange(cascade_len, dtype=numpy.float32) / SAMPLE_RATE + body = numpy.sin(2 * numpy.pi * 180 * t) * 0.06 + body *= numpy.exp(-1.5 * t) + # Modulate body resonance by the cascade density + env = numpy.exp(-1.2 * t) + body *= env + wave[:cascade_len] += body + + # Overall envelope — smooth fade + full_env = numpy.ones(n_samples, dtype=numpy.float32) + fade_len = min(int(SAMPLE_RATE * 0.8), n_samples) + if fade_len > 0 and cascade_len > fade_len: + full_env[cascade_len - fade_len:cascade_len] = numpy.linspace( + 1.0, 0.0, fade_len).astype(numpy.float32) + full_env[cascade_len:] = 0.0 + wave *= full_env + + mx = numpy.abs(wave).max() + if mx > 0: + wave /= mx * 1.5 # leave headroom + return wave + + def _render_drum_hit(sound_value, n_samples): """Render a single drum sound to a float32 array. @@ -3669,6 +3987,13 @@ def _render_drum_hit(sound_value, n_samples): DrumSound.BASS_3.value: lambda n: _synth_march_bass(n, pitch=62), DrumSound.BASS_4.value: lambda n: _synth_march_bass(n, pitch=52), DrumSound.BASS_5.value: lambda n: _synth_march_bass(n, pitch=42), + # Effects / world + DrumSound.RAINSTICK.value: lambda n: _synth_rainstick(n), + DrumSound.RAINSTICK_SLOW.value: lambda n: _synth_rainstick_slow(n), + DrumSound.OCEAN_DRUM.value: lambda n: _synth_ocean_drum(n), + DrumSound.CABASA.value: lambda n: _synth_cabasa(n), + DrumSound.WIND_CHIMES.value: lambda n: _synth_wind_chimes(n), + DrumSound.FINGER_CYMBAL.value: lambda n: _synth_finger_cymbal(n), } renderer = _dispatch.get(sound_value, lambda n: _synth_clave(n)) diff --git a/pytheory/rhythm.py b/pytheory/rhythm.py index fd5b558..a77325e 100644 --- a/pytheory/rhythm.py +++ b/pytheory/rhythm.py @@ -265,6 +265,16 @@ INSTRUMENTS = { "lowpass": 4500, "humanize": 0.2, }, + "crotales": { + "synth": "crotales_synth", "envelope": "none", + "reverb": 0.3, + "humanize": 0.2, + }, + "tingsha": { + "synth": "tingsha_synth", "envelope": "none", + "reverb": 0.4, + "humanize": 0.2, + }, "singing_bowl": { "synth": "singing_bowl_strike_synth", "envelope": "none", "reverb": 0.5, @@ -595,6 +605,13 @@ class DrumSound(Enum): BASS_3 = 126 # middle BASS_4 = 127 # fourth BASS_5 = 80 # lowest (biggest) bass drum + # Effects / world percussion + RAINSTICK = 81 # cascading pebbles through cactus tube (steep angle) + RAINSTICK_SLOW = 128 # gentle trickle (shallow angle) + OCEAN_DRUM = 82 # tilting drum with steel beads — surf wash + CABASA = 83 # metal bead chain wrapped around cylinder + WIND_CHIMES = 84 # suspended metal tubes struck by wind/hand + FINGER_CYMBAL = 85 # single small cymbal tap (zill) class _DrumTone: diff --git a/uv.lock b/uv.lock index 625e8ee..b493bb2 100644 --- a/uv.lock +++ b/uv.lock @@ -690,7 +690,7 @@ wheels = [ [[package]] name = "pytheory" -version = "0.40.2" +version = "0.40.3" source = { editable = "." } dependencies = [ { name = "rich" },