Compare commits

...

3 Commits

Author SHA1 Message Date
kennethreitz f9c81fe05f v0.31.0: 3 new synths, 38 instrument presets
- Karplus-Strong pluck (physical modeling for guitar/harp/koto)
- Hammond organ (additive drawbar synthesis)
- String ensemble (filtered saw with body resonance formants)
- 38 instrument presets: score.part("lead", instrument="violin")
- Demo updated with pluck_synth, organ_synth, strings_synth

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-26 21:18:27 -04:00
kennethreitz 931ec905c3 Add 3 new synths + 38 instrument presets
New synths:
- pluck_synth: Karplus-Strong physical modeling (guitar, harp, koto)
- organ_synth: Hammond-style additive drawbar synthesis
- strings_synth: Filtered saw with body resonance formants

38 instrument presets across 7 categories: keys, strings, woodwinds,
brass, plucked, synth, percussion/mallet. Each preset combines synth,
envelope, and effects to approximate real instruments.

score.part("lead", instrument="violin")
Score.list_instruments()

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-26 21:15:56 -04:00
kennethreitz 799ffbdac9 Add MIT LICENSE file
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-26 21:02:44 -04:00
10 changed files with 588 additions and 41 deletions
+7
View File
@@ -2,6 +2,13 @@
All notable changes to PyTheory are documented here.
## 0.31.0
- 3 new synth engines: Karplus-Strong pluck, Hammond organ, string ensemble with body formants
- 38 instrument presets: `score.part("lead", instrument="violin")`
- Keys, strings, woodwinds, brass, plucked, synth, and mallet categories
- 13 total synth waveforms
## 0.30.0
- Drums are a real Part — same effects pipeline as any voice
+21
View File
@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2026 Kenneth Reitz
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
+83
View File
@@ -341,6 +341,89 @@ Reverb is also stereo — the left and right channels get different
early reflection patterns, so the reverb tail occupies real space
in the stereo field rather than sitting dead center.
Physical Modeling
-----------------
Three synths go beyond traditional waveform synthesis into physical
modeling territory — they simulate how real instruments produce sound.
Karplus-Strong Pluck
~~~~~~~~~~~~~~~~~~~~
A burst of noise fed into a short delay line. The delay length sets
the pitch, the feedback filter models the string decaying. This is
how every physical modeling synth since 1983 does plucked strings.
It sounds genuinely like a real guitar, harp, or koto.
.. code-block:: python
guitar = score.part("guitar", synth="pluck_synth")
harp = score.part("harp", instrument="harp") # uses pluck_synth
Hammond Organ
~~~~~~~~~~~~~
Additive synthesis with drawbar harmonics — sine waves at the
fundamental plus 2nd, 3rd, 4th, 5th, 6th, and 8th harmonics mixed
at musical levels. Warm, round, unmistakably organ.
.. code-block:: python
organ = score.part("organ", synth="organ_synth")
String Ensemble
~~~~~~~~~~~~~~~
Filtered sawtooth with body resonance formants at ~500 Hz and ~1500 Hz,
modeling the way a violin or cello body shapes the sound. Warmer and
more "wooden" than a raw saw wave.
.. code-block:: python
violin = score.part("violin", synth="strings_synth")
Instrument Presets
------------------
Instead of choosing synth + envelope + effects manually, use an
instrument preset — 38 predefined combinations that approximate real
instruments:
.. code-block:: python
piano = score.part("piano", instrument="piano")
violin = score.part("violin", instrument="violin")
guitar = score.part("guitar", instrument="acoustic_guitar")
organ = score.part("organ", instrument="organ")
bass = score.part("bass", instrument="upright_bass")
Available instruments:
**Keys**: piano, electric_piano, organ, harpsichord, celesta, music_box
**Strings**: violin, viola, cello, contrabass, string_ensemble
**Woodwinds**: flute, clarinet, oboe, bassoon
**Brass**: trumpet, trombone, french_horn, tuba, brass_ensemble
**Plucked**: acoustic_guitar, electric_guitar, distorted_guitar,
bass_guitar, upright_bass, harp, sitar, koto
**Synth**: synth_lead, synth_pad, synth_bass, acid_bass, 808_bass
**Percussion**: vibraphone, marimba, xylophone, glockenspiel, tubular_bells
Explicit kwargs override preset defaults:
.. code-block:: python
# Piano with extra reverb
piano = score.part("piano", instrument="piano", reverb=0.5)
# Violin panned left
violin = score.part("v", instrument="violin", pan=-0.4)
Choosing Synth and Envelope Combos
----------------------------------
+1 -1
View File
@@ -1,6 +1,6 @@
[project]
name = "pytheory"
version = "0.30.0"
version = "0.31.0"
description = "Music Theory for Humans"
readme = "README.md"
license = "MIT"
+3 -3
View File
@@ -1,6 +1,6 @@
"""PyTheory: Music Theory for Humans."""
__version__ = "0.30.0"
__version__ = "0.31.0"
from .tones import Tone, Interval
from .systems import System, SYSTEMS
@@ -8,7 +8,7 @@ from .scales import TonedScale, Key, PROGRESSIONS
from .chords import Chord, Fretboard, analyze_progression
from .charts import CHARTS, Fingering, charts_for_fretboard
from .rhythm import Duration, TimeSignature, Rest, Score, Part, Section, DrumSound, Pattern
from .rhythm import Duration, TimeSignature, Rest, Score, Part, Section, DrumSound, Pattern, INSTRUMENTS
from .rhythm import Note as RhythmNote # rhythm.Note (tone + duration pairing)
from .play import (play, save, save_midi, play_progression, play_pattern,
@@ -25,5 +25,5 @@ __all__ = [
"play", "save", "save_midi", "play_progression", "play_pattern",
"play_score", "Synth", "Envelope",
"Duration", "TimeSignature", "RhythmNote", "Rest", "Score", "Part",
"DrumSound", "Pattern", "Section",
"DrumSound", "Pattern", "Section", "INSTRUMENTS",
]
+5 -5
View File
@@ -230,7 +230,7 @@ def cmd_demo(args):
{"name": "Bossa Nova", "key": ("A", "minor"), "drums": "bossa nova",
"fill": "bossa nova", "bpm": 140,
"prog": ("i", "iv", "V", "i"),
"lead": ("triangle", "strings", 0.2, -0.1),
"lead": ("pluck_synth", "none", 0.2, -0.1),
"pad": ("fm", "pad", -0.2),
"bass_lp": 600, "reverb_type": "plate"},
{"name": "Jazz Club", "key": ("Bb", "major"), "drums": "jazz",
@@ -254,8 +254,8 @@ def cmd_demo(args):
{"name": "Reggae", "key": ("G", "major"), "drums": "reggae",
"fill": "reggae", "bpm": 80,
"prog": ("I", "IV", "V", "IV"),
"lead": ("triangle", "strings", 0.25, 0.15),
"pad": ("pwm_slow", "pad", -0.3),
"lead": ("pluck_synth", "none", 0.25, 0.15),
"pad": ("organ_synth", "organ", -0.3),
"bass_lp": 400, "reverb_type": "cathedral"},
{"name": "Funk", "key": ("E", "minor"), "drums": "funk",
"fill": "funk", "bpm": 100,
@@ -272,8 +272,8 @@ def cmd_demo(args):
{"name": "Temple", "key": ("E", "minor"), "drums": "bolero",
"fill": "bossa nova", "bpm": 65,
"prog": ("i", "iv", "V", "i"),
"lead": ("triangle", "pluck", 0.3, 0.2),
"pad": ("sine", "pad", 0.0),
"lead": ("pluck_synth", "none", 0.3, 0.2),
"pad": ("strings_synth", "pad", 0.0),
"bass_lp": 200, "reverb_type": "taj_mahal"},
]
+96
View File
@@ -190,6 +190,97 @@ def pwm_fast_wave(hz, peak=SAMPLE_PEAK, n_samples=SAMPLE_RATE):
return pwm_wave(hz, peak, n_samples, lfo_rate=3.0)
def pluck_wave(hz, peak=SAMPLE_PEAK, n_samples=SAMPLE_RATE):
"""Karplus-Strong plucked string synthesis.
A burst of noise is fed into a short delay line with feedback —
the delay length determines the pitch, and the feedback filter
determines the decay. This is how every physical modeling synth
since 1983 does plucked strings. It sounds genuinely like a real
guitar, harp, or koto — not a synth approximation.
The algorithm: fill a buffer with random noise the length of one
period, then repeatedly average adjacent samples. The averaging
acts as a lowpass filter, gradually removing high harmonics —
exactly what a real vibrating string does as energy dissipates.
"""
period = int(SAMPLE_RATE / hz)
if period < 2:
period = 2
# Initial noise burst — the "pluck"
buf = numpy.random.uniform(-1.0, 1.0, period).astype(numpy.float64)
out = numpy.zeros(n_samples, dtype=numpy.float64)
for i in range(n_samples):
out[i] = buf[i % period]
# Averaging filter: smooth adjacent samples (Karplus-Strong)
buf[i % period] = 0.5 * (buf[i % period] + buf[(i + 1) % period]) * 0.998
return (peak * out).astype(numpy.int16)
def organ_wave(hz, peak=SAMPLE_PEAK, n_samples=SAMPLE_RATE):
"""Hammond organ — additive synthesis with drawbar harmonics.
A real Hammond B3 has 9 drawbars that mix sine waves at different
harmonics. This models the classic "full" registration with all
drawbars pulled: fundamental, 2nd, 3rd, 4th, 5th, 6th, and 8th
harmonics at musical levels.
The result is warm, rich, and unmistakably organ — somewhere
between a sine wave and a square wave, with that characteristic
hollow roundness.
"""
t = numpy.arange(n_samples, dtype=numpy.float64) / SAMPLE_RATE
# Drawbar levels (inspired by 888800000 — full even harmonics)
wave = (numpy.sin(2 * numpy.pi * hz * t) * 1.0 + # 16' fundamental
numpy.sin(2 * numpy.pi * hz * 2 * t) * 0.8 + # 8'
numpy.sin(2 * numpy.pi * hz * 3 * t) * 0.6 + # 5 1/3'
numpy.sin(2 * numpy.pi * hz * 4 * t) * 0.5 + # 4'
numpy.sin(2 * numpy.pi * hz * 5 * t) * 0.3 + # 2 2/3'
numpy.sin(2 * numpy.pi * hz * 6 * t) * 0.25 + # 2'
numpy.sin(2 * numpy.pi * hz * 8 * t) * 0.15) # 1 3/5'
wave /= 3.5 # normalize
return (peak * wave).astype(numpy.int16)
def strings_wave(hz, peak=SAMPLE_PEAK, n_samples=SAMPLE_RATE):
"""String ensemble — filtered saw with body resonance formants.
Goes beyond raw sawtooth by modeling the resonant body of a
stringed instrument. Two formant peaks (at ~500 Hz and ~1500 Hz)
shape the spectrum the way a violin or cello body does — boosting
certain frequencies and cutting others.
The result is warmer and more "wooden" than a raw saw wave,
with the characteristic nasal quality of bowed strings.
"""
# Base: sawtooth (all harmonics, like a bowed string)
length = SAMPLE_RATE / float(hz)
omega = numpy.pi * 2 / length
xvalues = numpy.arange(int(length)) * omega
onecycle = scipy.signal.sawtooth(xvalues, width=1)
wave = numpy.resize(onecycle, (n_samples,)).astype(numpy.float64)
# Body resonance formants — two bandpass peaks
# Formant 1: ~500 Hz (body resonance)
f1 = 500
bw1 = 200
b1, a1 = scipy.signal.butter(2, [max(20, f1 - bw1), f1 + bw1],
btype='band', fs=SAMPLE_RATE)
formant1 = scipy.signal.lfilter(b1, a1, wave)
# Formant 2: ~1500 Hz (bridge/top plate)
f2 = 1500
bw2 = 400
b2, a2 = scipy.signal.butter(2, [f2 - bw2, f2 + bw2],
btype='band', fs=SAMPLE_RATE)
formant2 = scipy.signal.lfilter(b2, a2, wave)
# Mix: original (attenuated) + formants
mixed = wave * 0.3 + formant1 * 0.4 + formant2 * 0.3
return (peak * mixed).astype(numpy.int16)
def _apply_envelope(samples, attack, decay, sustain, release, sample_rate=SAMPLE_RATE):
"""Apply an ADSR amplitude envelope to a sample array.
@@ -291,6 +382,9 @@ class Synth(Enum):
SUPERSAW = "supersaw"
PWM_SLOW = "pwm_slow"
PWM_FAST = "pwm_fast"
PLUCK = "pluck_synth"
ORGAN = "organ_synth"
STRINGS = "strings_synth"
def __call__(self, hz, **kwargs):
"""Make Synth members callable — dispatches to the wave function."""
@@ -302,6 +396,8 @@ _SYNTH_FUNCTIONS = {
"square": square_wave, "pulse": pulse_wave, "fm": fm_wave,
"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,
}
+296 -30
View File
@@ -7,6 +7,217 @@ from enum import Enum
from typing import Optional
# ── Instrument presets ────────────────────────────────────────────────────────
# Predefined combinations of synth, envelope, effects, and parameters that
# approximate real instruments. Used by ``Score.part(instrument=...)``.
INSTRUMENTS = {
# ── Keys ──
"piano": {
"synth": "fm", "envelope": "piano",
"detune": 5, "chorus": 0.1, "chorus_rate": 0.3,
"lowpass": 6000,
},
"electric_piano": { # Rhodes/Wurlitzer
"synth": "fm", "envelope": "piano",
"detune": 6, "chorus": 0.2, "chorus_rate": 1.0,
"lowpass": 4000,
},
"organ": {
"synth": "organ_synth", "envelope": "organ",
"chorus": 0.2, "chorus_rate": 5.5,
"lowpass": 5000,
},
"harpsichord": {
"synth": "pluck_synth", "envelope": "none",
"lowpass": 3500,
},
"celesta": {
"synth": "fm", "envelope": "bell",
"lowpass": 8000,
"reverb": 0.3, "reverb_type": "plate",
},
"music_box": {
"synth": "sine", "envelope": "bell",
"lowpass": 6000,
"reverb": 0.25, "reverb_type": "plate",
},
# ── Strings ──
"violin": {
"synth": "strings_synth", "envelope": "strings",
"detune": 4, "lowpass": 5000,
"humanize": 0.15,
},
"viola": {
"synth": "strings_synth", "envelope": "strings",
"detune": 4, "lowpass": 3500,
"humanize": 0.15,
},
"cello": {
"synth": "strings_synth", "envelope": "strings",
"detune": 4, "lowpass": 2500,
"humanize": 0.15,
},
"contrabass": {
"synth": "strings_synth", "envelope": "strings",
"detune": 3, "lowpass": 1500,
"humanize": 0.1,
},
"string_ensemble": {
"synth": "strings_synth", "envelope": "pad",
"detune": 12, "spread": 0.6,
"chorus": 0.2, "chorus_rate": 0.5,
"lowpass": 4000,
},
# ── Woodwinds ──
"flute": {
"synth": "sine", "envelope": "strings",
"lowpass": 4000,
"humanize": 0.2,
},
"clarinet": {
"synth": "square", "envelope": "strings",
"lowpass": 3000,
"humanize": 0.15,
},
"oboe": {
"synth": "saw", "envelope": "strings",
"lowpass": 3500, "lowpass_q": 1.2,
"humanize": 0.15,
},
"bassoon": {
"synth": "saw", "envelope": "strings",
"lowpass": 2000,
"humanize": 0.15,
},
# ── Brass ──
"trumpet": {
"synth": "saw", "envelope": "pluck",
"detune": 3, "lowpass": 4000, "lowpass_q": 1.1,
"humanize": 0.15,
},
"trombone": {
"synth": "saw", "envelope": "strings",
"detune": 3, "lowpass": 2500,
"humanize": 0.15,
},
"french_horn": {
"synth": "saw", "envelope": "strings",
"detune": 4, "lowpass": 2000,
"chorus": 0.1,
"humanize": 0.15,
},
"tuba": {
"synth": "saw", "envelope": "strings",
"detune": 3, "lowpass": 1200,
"humanize": 0.1,
},
"brass_ensemble": {
"synth": "saw", "envelope": "strings",
"detune": 10, "spread": 0.4,
"lowpass": 3000,
"chorus": 0.15,
},
# ── Plucked ──
"acoustic_guitar": {
"synth": "pluck_synth", "envelope": "none",
"lowpass": 4000,
"humanize": 0.2,
},
"electric_guitar": {
"synth": "saw", "envelope": "pluck",
"detune": 5, "lowpass": 3500,
"humanize": 0.15,
},
"distorted_guitar": {
"synth": "saw", "envelope": "pluck",
"detune": 8, "distortion": 0.6, "distortion_drive": 5.0,
"lowpass": 3000,
"humanize": 0.15,
},
"bass_guitar": {
"synth": "triangle", "envelope": "pluck",
"lowpass": 1000,
"humanize": 0.1,
},
"upright_bass": {
"synth": "sine", "envelope": "pluck",
"lowpass": 800,
"humanize": 0.15,
},
"harp": {
"synth": "pluck_synth", "envelope": "none",
"lowpass": 5000,
"reverb": 0.3, "reverb_type": "plate",
},
"sitar": {
"synth": "saw", "envelope": "pluck",
"detune": 12, "lowpass": 3000, "lowpass_q": 1.5,
"humanize": 0.2,
},
"koto": {
"synth": "pluck_synth", "envelope": "none",
"lowpass": 4000,
"reverb": 0.2,
},
# ── Synth presets ──
"synth_lead": {
"synth": "saw", "envelope": "pluck",
"detune": 8, "lowpass": 3000,
"delay": 0.2, "delay_time": 0.25, "delay_feedback": 0.3,
},
"synth_pad": {
"synth": "supersaw", "envelope": "pad",
"detune": 12, "spread": 0.6,
"chorus": 0.2,
},
"synth_bass": {
"synth": "saw", "envelope": "pluck",
"lowpass": 800, "lowpass_q": 1.3,
},
"acid_bass": {
"synth": "saw", "envelope": "pad",
"legato": True, "glide": 0.03,
"distortion": 0.7, "distortion_drive": 8.0,
"lowpass": 800, "lowpass_q": 5.0,
},
"808_bass": {
"synth": "sine", "envelope": "pluck",
"distortion": 0.4, "distortion_drive": 2.5,
"lowpass": 200, "lowpass_q": 1.5,
},
# ── Percussion / Mallet ──
"vibraphone": {
"synth": "fm", "envelope": "bell",
"lowpass": 5000,
"reverb": 0.3, "reverb_type": "plate",
},
"marimba": {
"synth": "sine", "envelope": "pluck",
"lowpass": 3000,
},
"xylophone": {
"synth": "fm", "envelope": "pluck",
"lowpass": 6000,
},
"glockenspiel": {
"synth": "fm", "envelope": "bell",
"lowpass": 8000,
"reverb": 0.2,
},
"tubular_bells": {
"synth": "fm", "envelope": "bell",
"reverb": 0.4, "reverb_type": "cathedral",
},
}
class Duration(Enum):
"""Note durations in beats (quarter note = 1 beat)."""
@@ -1857,28 +2068,34 @@ class Score:
setattr(p, attr, v)
return self
def part(self, name: str, *, synth: str = "sine",
envelope: str = "piano", volume: float = 0.5,
reverb: float = 0.0, reverb_decay: float = 1.0,
reverb_type: str = "algorithmic",
delay: float = 0.0, delay_time: float = 0.375,
delay_feedback: float = 0.4,
lowpass: float = 0.0, lowpass_q: float = 0.707,
distortion: float = 0.0, distortion_drive: float = 3.0,
legato: bool = False, glide: float = 0.0,
chorus: float = 0.0, chorus_rate: float = 1.5,
chorus_depth: float = 0.003,
def part(self, name: str, *, instrument: str = None,
synth: str = None, envelope: str = None,
volume: float = None,
reverb: float = None, reverb_decay: float = None,
reverb_type: str = None,
delay: float = None, delay_time: float = None,
delay_feedback: float = None,
lowpass: float = None, lowpass_q: float = None,
distortion: float = None, distortion_drive: float = None,
legato: bool = None, glide: float = None,
chorus: float = None, chorus_rate: float = None,
chorus_depth: float = None,
swing: Optional[float] = None,
humanize: float = 0.0,
sidechain: float = 0.0,
sidechain_release: float = 0.1,
detune: float = 0.0,
pan: float = 0.0,
spread: float = 0.0) -> Part:
humanize: float = None,
sidechain: float = None,
sidechain_release: float = None,
detune: float = None,
pan: float = None,
spread: float = None) -> Part:
"""Create a named part with its own synth voice and effects.
Args:
name: Part name (e.g. ``"lead"``, ``"bass"``, ``"pads"``).
instrument: Instrument preset name (e.g. ``"piano"``,
``"violin"``, ``"808_bass"``). See :data:`INSTRUMENTS`
for the full list. When set, the preset's synth, envelope,
and effects are used as defaults; any explicit keyword
argument still overrides the preset value.
synth: Waveform — ``"sine"``, ``"saw"``, ``"triangle"``,
``"square"``, ``"pulse"``, ``"fm"``, ``"noise"``,
``"supersaw"``, ``"pwm_slow"``, ``"pwm_fast"``.
@@ -1926,23 +2143,72 @@ class Score:
lead = score.part("lead", synth="saw", envelope="pluck",
reverb=0.3, delay=0.25, lowpass=3000)
# Or use an instrument preset:
piano = score.part("keys", instrument="piano")
"""
p = Part(name, synth=synth, envelope=envelope, volume=volume,
reverb=reverb, reverb_decay=reverb_decay,
reverb_type=reverb_type,
delay=delay, delay_time=delay_time,
delay_feedback=delay_feedback,
lowpass=lowpass, lowpass_q=lowpass_q,
distortion=distortion, distortion_drive=distortion_drive,
legato=legato, glide=glide,
chorus=chorus, chorus_rate=chorus_rate,
chorus_depth=chorus_depth,
swing=swing, humanize=humanize,
sidechain=sidechain, sidechain_release=sidechain_release,
detune=detune, pan=pan, spread=spread)
# Default values for all Part parameters.
_defaults = {
"synth": "sine", "envelope": "piano", "volume": 0.5,
"reverb": 0.0, "reverb_decay": 1.0, "reverb_type": "algorithmic",
"delay": 0.0, "delay_time": 0.375, "delay_feedback": 0.4,
"lowpass": 0.0, "lowpass_q": 0.707,
"distortion": 0.0, "distortion_drive": 3.0,
"legato": False, "glide": 0.0,
"chorus": 0.0, "chorus_rate": 1.5, "chorus_depth": 0.003,
"swing": None, "humanize": 0.0,
"sidechain": 0.0, "sidechain_release": 0.1,
"detune": 0.0, "pan": 0.0, "spread": 0.0,
}
# If an instrument preset is specified, layer it on top of defaults.
if instrument is not None:
preset = INSTRUMENTS.get(instrument)
if preset is None:
raise ValueError(
f"Unknown instrument: {instrument!r}. "
f"Use Score.list_instruments() to see available presets."
)
_defaults.update(preset)
# Collect explicitly-provided kwargs (non-None) and override defaults.
explicit = {}
_locals = {
"synth": synth, "envelope": envelope, "volume": volume,
"reverb": reverb, "reverb_decay": reverb_decay,
"reverb_type": reverb_type,
"delay": delay, "delay_time": delay_time,
"delay_feedback": delay_feedback,
"lowpass": lowpass, "lowpass_q": lowpass_q,
"distortion": distortion, "distortion_drive": distortion_drive,
"legato": legato, "glide": glide,
"chorus": chorus, "chorus_rate": chorus_rate,
"chorus_depth": chorus_depth,
"swing": swing, "humanize": humanize,
"sidechain": sidechain, "sidechain_release": sidechain_release,
"detune": detune, "pan": pan, "spread": spread,
}
for k, v in _locals.items():
if v is not None:
explicit[k] = v
merged = {**_defaults, **explicit}
p = Part(name, **merged)
self.parts[name] = p
return p
@classmethod
def list_instruments(cls) -> list:
"""Return a sorted list of available instrument preset names.
Example::
Score.list_instruments()
# ['808_bass', 'acid_bass', 'acoustic_guitar', ...]
"""
return sorted(INSTRUMENTS.keys())
def add_pattern(self, pattern, repeats: int = 1) -> "Score":
"""Add a drum pattern to this score.
+75 -1
View File
@@ -5312,7 +5312,7 @@ def test_supersaw_wave():
@needs_portaudio
def test_all_synths_in_enum():
from pytheory.play import Synth
assert len(Synth) == 10
assert len(Synth) == 13
for s in Synth:
wave = s(440, n_samples=1000)
assert len(wave) == 1000
@@ -6451,3 +6451,77 @@ def test_from_midi_note_durations(tmp_path):
assert len(sounding) == 2
assert abs(sounding[0].beats - 4.0) < 0.01
assert abs(sounding[1].beats - 2.0) < 0.01
# ── Instrument presets ────────────────────────────────────────────────────────
def test_instrument_piano():
from pytheory import Score, Duration
score = Score("4/4", bpm=120)
p = score.part("p", instrument="piano")
assert p.synth == "fm"
assert p.envelope == "piano"
assert p.detune == 5
assert p.lowpass == 6000
assert p.chorus_mix == 0.1
def test_instrument_violin():
from pytheory import Score
score = Score("4/4", bpm=120)
p = score.part("v", instrument="violin")
assert p.synth == "strings_synth"
assert p.envelope == "strings"
assert p.humanize == 0.15
assert p.lowpass == 5000
def test_instrument_override():
from pytheory import Score
score = Score("4/4", bpm=120)
# Explicit synth overrides the preset's "fm"
p = score.part("p", instrument="piano", synth="saw")
assert p.synth == "saw"
# Other preset values still apply
assert p.envelope == "piano"
assert p.detune == 5
def test_instrument_unknown_raises():
from pytheory import Score
score = Score("4/4", bpm=120)
with pytest.raises(ValueError, match="Unknown instrument"):
score.part("x", instrument="kazoo")
def test_list_instruments():
from pytheory import Score, INSTRUMENTS
result = Score.list_instruments()
assert isinstance(result, list)
assert result == sorted(result)
assert "piano" in result
assert "violin" in result
assert "808_bass" in result
assert len(result) == len(INSTRUMENTS)
def test_instrument_effects():
from pytheory import Score
score = Score("4/4", bpm=120)
p = score.part("c", instrument="celesta")
assert p.reverb_mix == 0.3
assert p.reverb_type == "plate"
assert p.synth == "fm"
assert p.envelope == "bell"
def test_instrument_808_bass():
from pytheory import Score
score = Score("4/4", bpm=120)
p = score.part("b", instrument="808_bass")
assert p.distortion_mix == 0.4
assert p.distortion_drive == 2.5
assert p.lowpass == 200
assert p.lowpass_q == 1.5
assert p.synth == "sine"
assert p.envelope == "pluck"
Generated
+1 -1
View File
@@ -707,7 +707,7 @@ wheels = [
[[package]]
name = "pytheory"
version = "0.30.0"
version = "0.31.0"
source = { editable = "." }
dependencies = [
{ name = "numeral" },