diff --git a/CHANGELOG.md b/CHANGELOG.md index 835b621..9e79c99 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,25 @@ All notable changes to PyTheory are documented here. +## 0.36.0 + +- **Banjo synth** — steel strings on drum-head body, nasal twang, + fast decay with membrane resonance +- **Mandolin synth** — paired steel strings (natural chorus from + doubled courses), bright body resonance +- **Ukulele synth** — nylon strings, small mid-heavy body, shorter + sustain than guitar +- **Cajón drums** — bass (woody box thump), slap (snare wire buzz), + tap (ghost note). 3 patterns: cajon, cajon rumba, cajon folk +- **Vocal/formant synth** — LF glottal model, 5 Peterson & Barney + formant peaks, jitter/shimmer, consonant onsets, per-note lyrics. + Presets: vocal, choir +- **Granular synthesis** — grain cloud engine with scatter, pitch + variation, Hanning windows. Presets: granular_pad, granular_texture +- **Strum sweep** — subtle grace notes before chord hit for natural + strum feel on all fretboard instruments +- Mandola preset, 34 synth waveforms, 26 songs + ## 0.35.0 - **8.5x faster import** — dropped pytuning/sympy, lazy-load scipy. diff --git a/docs/index.rst b/docs/index.rst index 018c976..7ef8043 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -77,7 +77,7 @@ What's Inside numbers), scale recommendation, modulation, voice leading - **Sequencing** — Score, Parts, arpeggiator, legato/glide, velocity, swing, humanize, tempo changes, song sections with repeat -- **Synthesis** — 30 waveforms (including Karplus-Strong pluck, Hammond organ, +- **Synthesis** — 34 waveforms (including Karplus-Strong pluck, Hammond organ, bowed string, and 14 dedicated instrument synths), 10 envelopes, 40+ instrument presets, configurable FM, sub-oscillator, noise layer, filter envelope, velocity-to-brightness, analog oscillator drift, detune, stereo diff --git a/pyproject.toml b/pyproject.toml index 716d087..baa5b5d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "pytheory" -version = "0.35.1" +version = "0.36.0" description = "Music Theory for Humans" readme = "README.md" license = "MIT" diff --git a/pytheory/__init__.py b/pytheory/__init__.py index 794e05d..83a1a5a 100644 --- a/pytheory/__init__.py +++ b/pytheory/__init__.py @@ -1,6 +1,6 @@ """PyTheory: Music Theory for Humans.""" -__version__ = "0.35.1" +__version__ = "0.36.0" from .tones import Tone, Interval from .systems import System, SYSTEMS, TET diff --git a/pytheory/play.py b/pytheory/play.py index a1a0575..31cbc90 100644 --- a/pytheory/play.py +++ b/pytheory/play.py @@ -1130,6 +1130,48 @@ def granular_wave(hz, peak=SAMPLE_PEAK, n_samples=SAMPLE_RATE, return (peak * out).astype(numpy.int16) +def banjo_wave(hz, peak=SAMPLE_PEAK, n_samples=SAMPLE_RATE): + """Banjo — steel strings on a drum-head body. + + The banjo's distinctive twang comes from the membrane head + (like a drum skin) instead of a wooden soundboard. This gives + a sharp attack, bright tone, and fast decay with a nasal, + metallic quality. The 5th string drone adds shimmer. + """ + period = int(SAMPLE_RATE / hz) + if period < 2: + period = 2 + rng = numpy.random.default_rng(int(hz * 100) % 2**31) + + # Steel string — bright, sharp attack + buf = rng.uniform(-0.9, 0.9, period).astype(numpy.float64) + # Minimal filtering — banjo keeps the brightness + for k in range(period - 1): + buf[k] = 0.7 * buf[k] + 0.3 * buf[k + 1] + + out = numpy.zeros(n_samples, dtype=numpy.float64) + for i in range(n_samples): + out[i] = buf[i % period] + next_idx = (i + 1) % period + # Moderate decay — drum head rings but shorter than guitar + buf[i % period] = 0.5 * (buf[i % period] + buf[next_idx]) * 0.9988 + + # Drum-head resonance — nasal, ringy, mid-frequency peaks + # The membrane head rings more than wood — that's the twang + import scipy.signal as _sig + for center, bw, gain in [(600, 200, 0.5), (1500, 300, 0.4), (3000, 500, 0.25)]: + lo = max(20, center - bw) + hi = min(SAMPLE_RATE // 2 - 1, center + bw) + if lo < hi: + bp, ap = _sig.butter(2, [lo, hi], btype='band', fs=SAMPLE_RATE) + out += _sig.lfilter(bp, ap, out) * gain + + mx = numpy.abs(out).max() + if mx > 0: + out /= mx + return (peak * out).astype(numpy.int16) + + def mandolin_wave(hz, peak=SAMPLE_PEAK, n_samples=SAMPLE_RATE): """Mandolin — paired steel strings, bright and ringing. @@ -1523,6 +1565,7 @@ class Synth(Enum): SAXOPHONE = "saxophone_synth" GRANULAR = "granular_synth" VOCAL = "vocal_synth" + BANJO = "banjo_synth" MANDOLIN = "mandolin_synth" UKULELE = "ukulele_synth" ACOUSTIC_GUITAR = "acoustic_guitar_synth" @@ -1548,7 +1591,8 @@ _SYNTH_FUNCTIONS = { "harp_synth": harp_wave, "upright_bass_synth": upright_bass_wave, "timpani_synth": timpani_wave, "saxophone_synth": saxophone_wave, "granular_synth": granular_wave, "vocal_synth": vocal_wave, - "mandolin_synth": mandolin_wave, "ukulele_synth": ukulele_wave, + "banjo_synth": banjo_wave, "mandolin_synth": mandolin_wave, + "ukulele_synth": ukulele_wave, "acoustic_guitar_synth": acoustic_guitar_wave, "sitar_synth": sitar_wave, "electric_guitar_synth": electric_guitar_wave, } diff --git a/pytheory/rhythm.py b/pytheory/rhythm.py index 2498465..1d970bf 100644 --- a/pytheory/rhythm.py +++ b/pytheory/rhythm.py @@ -195,6 +195,10 @@ INSTRUMENTS = { "detune": 12, "lowpass": 3000, "lowpass_q": 1.5, "humanize": 0.2, }, + "banjo": { + "synth": "banjo_synth", "envelope": "none", + "humanize": 0.2, + }, "mandolin": { "synth": "mandolin_synth", "envelope": "none", "humanize": 0.2, diff --git a/uv.lock b/uv.lock index 6994e90..a1f94db 100644 --- a/uv.lock +++ b/uv.lock @@ -698,7 +698,7 @@ wheels = [ [[package]] name = "pytheory" -version = "0.35.1" +version = "0.36.0" source = { editable = "." } dependencies = [ { name = "numeral" },