mirror of
https://github.com/kennethreitz/pytheory.git
synced 2026-06-05 23:00:20 +00:00
Compare commits
7 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| e0a1ce9d18 | |||
| de7575fe0a | |||
| 665a6f5de5 | |||
| 63362df697 | |||
| 755b33a63b | |||
| 40901d603d | |||
| 9b3cbd9065 |
@@ -2,6 +2,54 @@
|
||||
|
||||
All notable changes to PyTheory are documented here.
|
||||
|
||||
## 0.40.8
|
||||
|
||||
- **Fix hold() inflating duration** — `Note.beats` was returning the full
|
||||
duration for held notes (`_hold=True`), causing `Part.total_beats` and
|
||||
`Score.duration_ms` to overcount. A part with `hold(Sa, WHOLE * 4)` followed
|
||||
by `add(Pa, QUARTER)` would report 17 beats instead of 1. Now held notes
|
||||
return 0 beats, matching the renderer which already skipped advancing the
|
||||
timeline for held notes.
|
||||
|
||||
## 0.40.7
|
||||
|
||||
- **Expose missing Synth enum entries** — rhodes, wurlitzer, vibraphone,
|
||||
pipe organ, and choir wave functions were already implemented but not
|
||||
accessible via the Synth enum. Now available as `Synth.RHODES`,
|
||||
`Synth.WURLITZER`, `Synth.VIBRAPHONE`, `Synth.PIPE_ORGAN`, `Synth.CHOIR`.
|
||||
|
||||
## 0.40.6
|
||||
|
||||
- **Saxophone presets cleaned up** — removed lowpass filters and vel_to_filter
|
||||
from all sax instrument presets (saxophone, alto_sax, tenor_sax, bari_sax).
|
||||
The saxophone wave function already shapes its own spectrum; the extra
|
||||
filters were dulling the tone.
|
||||
|
||||
## 0.40.5
|
||||
|
||||
- **Saxophone synth overhaul** — reed nonlinearity (asymmetric soft clipping),
|
||||
conical bore formant resonances, breath noise with attack envelope, separate
|
||||
reed buzz, key click transient, and sub-harmonic warmth. Vibrato dialed back
|
||||
to subtle, delayed onset.
|
||||
|
||||
## 0.40.4
|
||||
|
||||
- **Distortion overhaul** — multi-stage clipping (preamp → power amp →
|
||||
asymmetric rectifier) replaces single-stage tanh. Crunch, distorted,
|
||||
orange crunch, and metal guitar presets now sound properly driven.
|
||||
|
||||
## 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,
|
||||
|
||||
Vendored
BIN
Binary file not shown.
Vendored
BIN
Binary file not shown.
Vendored
BIN
Binary file not shown.
Vendored
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
Vendored
BIN
Binary file not shown.
Vendored
BIN
Binary file not shown.
@@ -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,
|
||||
|
||||
+106
-2
@@ -930,6 +930,40 @@ Parameters (passed as synth kwargs):
|
||||
|
||||
<audio controls style="width:100%;margin:0.3em 0 0.5em"><source src="../_static/audio/synth_granular.wav" type="audio/wav"></audio>
|
||||
|
||||
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
|
||||
|
||||
<audio controls style="width:100%;margin:0.3em 0 0.5em"><source src="../_static/audio/synth_crotales.wav" type="audio/wav"></audio>
|
||||
|
||||
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
|
||||
|
||||
<audio controls style="width:100%;margin:0.3em 0 0.5em"><source src="../_static/audio/synth_tingsha.wav" type="audio/wav"></audio>
|
||||
|
||||
Singing Bowl (Strike)
|
||||
~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
@@ -963,6 +997,76 @@ and out as the bowl resonates.
|
||||
|
||||
<audio controls style="width:100%;margin:0.3em 0 0.5em"><source src="../_static/audio/synth_singing_bowl_ring.wav" type="audio/wav"></audio>
|
||||
|
||||
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
|
||||
|
||||
<audio controls style="width:100%;margin:0.3em 0 0.5em"><source src="../_static/audio/rainstick.wav" type="audio/wav"></audio>
|
||||
<audio controls style="width:100%;margin:0.3em 0 0.5em"><source src="../_static/audio/rainstick_slow.wav" type="audio/wav"></audio>
|
||||
|
||||
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
|
||||
|
||||
<audio controls style="width:100%;margin:0.3em 0 0.5em"><source src="../_static/audio/ocean_drum.wav" type="audio/wav"></audio>
|
||||
|
||||
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
|
||||
|
||||
<audio controls style="width:100%;margin:0.3em 0 0.5em"><source src="../_static/audio/cabasa.wav" type="audio/wav"></audio>
|
||||
|
||||
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
|
||||
|
||||
<audio controls style="width:100%;margin:0.3em 0 0.5em"><source src="../_static/audio/wind_chimes.wav" type="audio/wav"></audio>
|
||||
|
||||
Finger Cymbal
|
||||
~~~~~~~~~~~~~
|
||||
|
||||
Single small cymbal tap (zill) — bright metallic ping.
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
p.hit(DrumSound.FINGER_CYMBAL, Duration.HALF)
|
||||
|
||||
.. raw:: html
|
||||
|
||||
<audio controls style="width:100%;margin:0.3em 0 0.5em"><source src="../_static/audio/finger_cymbal.wav" type="audio/wav"></audio>
|
||||
|
||||
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:
|
||||
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
[project]
|
||||
name = "pytheory"
|
||||
version = "0.40.2"
|
||||
version = "0.40.8"
|
||||
description = "Music Theory for Humans"
|
||||
readme = "README.md"
|
||||
license = "MIT"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
"""PyTheory: Music Theory for Humans."""
|
||||
|
||||
__version__ = "0.40.2"
|
||||
__version__ = "0.40.7"
|
||||
|
||||
from .tones import Tone, Interval
|
||||
from .systems import System, SYSTEMS, TET
|
||||
|
||||
+446
-35
@@ -1187,62 +1187,131 @@ def timpani_wave(hz, peak=SAMPLE_PEAK, n_samples=SAMPLE_RATE):
|
||||
|
||||
|
||||
def saxophone_wave(hz, peak=SAMPLE_PEAK, n_samples=SAMPLE_RATE):
|
||||
"""Saxophone — single reed through a conical brass bore.
|
||||
"""Saxophone — single reed driving a conical brass bore.
|
||||
|
||||
The conical bore produces all harmonics (like oboe), but the
|
||||
brass body and larger mouthpiece give a warmer, fatter, more
|
||||
vocal quality. The reed adds a slight buzz. Saxophone is
|
||||
between clarinet (odd harmonics) and oboe (nasal even+odd) —
|
||||
it has everything, with a strong fundamental and rich mids.
|
||||
Models the key acoustic properties of a saxophone:
|
||||
1. Reed-bore interaction — nonlinear clipping creates the characteristic
|
||||
bright, edgy tone (not just additive sines)
|
||||
2. Conical bore formants — vocal-like resonances at ~500, ~1400, ~2300,
|
||||
~3200 Hz that give sax its singing quality
|
||||
3. Breath noise — turbulent airflow through the mouthpiece, strongest
|
||||
at attack and blending into sustained tone
|
||||
4. Sub-harmonic warmth — the conical bore's coupling creates warmth
|
||||
below the fundamental
|
||||
5. Vibrato — delayed onset, ~5 Hz, characteristic of jazz/classical sax
|
||||
"""
|
||||
import scipy.signal as _sig
|
||||
|
||||
t = numpy.arange(n_samples, dtype=numpy.float64) / SAMPLE_RATE
|
||||
rng = numpy.random.default_rng(int(hz * 100) % 2**31)
|
||||
|
||||
# Vibrato — develops after ~250ms, wider than flute
|
||||
vib_onset = numpy.clip(t / 0.25, 0.0, 1.0)
|
||||
vib = hz * 0.0012 * vib_onset * numpy.sin(2 * numpy.pi * 5.2 * t)
|
||||
# --- Vibrato: delayed onset, subtle depth ---
|
||||
vib_onset = numpy.clip((t - 0.3) / 0.3, 0.0, 1.0)
|
||||
vib_rate = 5.0 + 0.15 * numpy.sin(2 * numpy.pi * 0.4 * t)
|
||||
vib = hz * 0.0006 * vib_onset * numpy.sin(2 * numpy.pi * vib_rate * t)
|
||||
|
||||
# --- Core tone: sawtooth-like waveform with reed clipping ---
|
||||
# Real sax reed creates a quasi-sawtooth pressure wave, not pure sines.
|
||||
# Build from harmonics with sax-specific spectral envelope, then clip.
|
||||
wave = numpy.zeros(n_samples, dtype=numpy.float64)
|
||||
n_harmonics = min(20, int((SAMPLE_RATE / 2) / hz))
|
||||
n_harmonics = min(25, int((SAMPLE_RATE / 2) / hz))
|
||||
|
||||
for n in range(1, n_harmonics + 1):
|
||||
f_n = hz * n
|
||||
if f_n >= SAMPLE_RATE / 2:
|
||||
break
|
||||
# Sax spectral shape: strong fundamental, broad mid peak (3-6),
|
||||
# slower rolloff than oboe (brass body carries harmonics further)
|
||||
# Saxophone spectral envelope from acoustic measurements:
|
||||
# Strong fundamental, nearly-as-strong 2nd and 3rd harmonics,
|
||||
# broad energy peak around harmonics 4-8 (the "body"), then
|
||||
# gradual rolloff — but slower than other woodwinds (brass bore
|
||||
# sustains upper partials).
|
||||
if n == 1:
|
||||
amp = 1.0
|
||||
elif n <= 3:
|
||||
amp = 0.6
|
||||
elif n <= 6:
|
||||
amp = 0.4 * numpy.exp(-0.1 * (n - 4) ** 2)
|
||||
elif n == 2:
|
||||
amp = 0.85
|
||||
elif n == 3:
|
||||
amp = 0.7
|
||||
elif n <= 8:
|
||||
# Broad mid peak — this is the sax "meat"
|
||||
amp = 0.55 * numpy.exp(-0.06 * (n - 5) ** 2)
|
||||
else:
|
||||
amp = 0.2 / n
|
||||
# Slower rolloff than oboe/clarinet
|
||||
amp = 0.35 / (n ** 0.7)
|
||||
|
||||
# Slight even/odd asymmetry — conical bore has all harmonics
|
||||
# but evens are ~10% weaker (midway between cylinder and cone)
|
||||
if n % 2 == 0:
|
||||
amp *= 0.9
|
||||
|
||||
phase = rng.uniform(0, 2 * numpy.pi)
|
||||
wave += amp * numpy.sin(2 * numpy.pi * (f_n + vib * n) * t + phase)
|
||||
|
||||
# Reed buzz — more present than oboe but still warm
|
||||
reed = rng.normal(0, 0.07, n_samples)
|
||||
# Bandpass the reed noise around 1-3kHz (the "honk" range)
|
||||
import scipy.signal as _sig
|
||||
reed_lo = max(20, int(hz * 2))
|
||||
reed_hi = min(SAMPLE_RATE // 2 - 1, int(hz * 6))
|
||||
# --- Reed nonlinearity: soft clipping ---
|
||||
# The reed closes against the mouthpiece, creating asymmetric clipping
|
||||
# that adds brightness and "edge". This is what makes sax sound like
|
||||
# sax and not a flute.
|
||||
wave_max = numpy.abs(wave).max()
|
||||
if wave_max > 0:
|
||||
wave /= wave_max
|
||||
# Asymmetric soft clip: positive peaks clip harder (reed closure)
|
||||
wave = numpy.tanh(1.8 * wave) * 0.7 + numpy.tanh(2.5 * wave) * 0.3
|
||||
|
||||
# --- Formant resonances: conical bore creates vocal quality ---
|
||||
# These fixed resonances are what make sax sound "vocal" — they
|
||||
# emphasize certain frequency bands regardless of the note played.
|
||||
formant_freqs = [520, 1380, 2300, 3200]
|
||||
formant_bws = [120, 200, 280, 350]
|
||||
formant_gains = [0.25, 0.18, 0.12, 0.08]
|
||||
|
||||
formant_sum = numpy.zeros(n_samples, dtype=numpy.float64)
|
||||
for fc, bw, gain in zip(formant_freqs, formant_bws, formant_gains):
|
||||
lo = max(20, int(fc - bw))
|
||||
hi = min(SAMPLE_RATE // 2 - 1, int(fc + bw))
|
||||
if lo < hi:
|
||||
bf, af = _sig.butter(2, [lo, hi], btype='band', fs=SAMPLE_RATE)
|
||||
formant_sum += _sig.lfilter(bf, af, wave) * gain
|
||||
wave = wave * 0.7 + formant_sum
|
||||
|
||||
# --- Breath noise: turbulent air through the mouthpiece ---
|
||||
# Strongest at the attack, then settles to a subtle constant hiss
|
||||
# that gives the tone "life" and prevents it from sounding synthetic.
|
||||
breath = rng.normal(0, 1.0, n_samples)
|
||||
# Shape breath noise into the sax's "hiss" band (2-6 kHz)
|
||||
breath_lo = max(20, 2000)
|
||||
breath_hi = min(SAMPLE_RATE // 2 - 1, 6000)
|
||||
if breath_lo < breath_hi:
|
||||
bb, ab = _sig.butter(2, [breath_lo, breath_hi], btype='band', fs=SAMPLE_RATE)
|
||||
breath = _sig.lfilter(bb, ab, breath)
|
||||
# Attack envelope for breath — strong at onset, then quiet
|
||||
breath_env = 0.15 * numpy.exp(-8.0 * t) + 0.03
|
||||
wave += breath * breath_env
|
||||
|
||||
# --- Reed buzz: low-frequency interaction noise ---
|
||||
# Different from breath — this is the "buzz" from reed vibration
|
||||
# against the mouthpiece, centered around the playing frequency.
|
||||
reed_noise = rng.normal(0, 1.0, n_samples)
|
||||
reed_lo = max(20, int(hz * 0.8))
|
||||
reed_hi = min(SAMPLE_RATE // 2 - 1, int(hz * 4))
|
||||
if reed_lo < reed_hi:
|
||||
br, ar = _sig.butter(2, [reed_lo, reed_hi], btype='band', fs=SAMPLE_RATE)
|
||||
reed = _sig.lfilter(br, ar, reed).astype(numpy.float64) * 2.0
|
||||
wave += reed
|
||||
reed_noise = _sig.lfilter(br, ar, reed_noise) * 0.06
|
||||
wave += reed_noise
|
||||
|
||||
# Brass body warmth — low-mid boost
|
||||
center = min(1500, hz * 4)
|
||||
bw = 500
|
||||
lo = max(20, int(center - bw))
|
||||
hi = min(SAMPLE_RATE // 2 - 1, int(center + bw))
|
||||
if lo < hi:
|
||||
bp, ap = _sig.butter(2, [lo, hi], btype='band', fs=SAMPLE_RATE)
|
||||
body = _sig.lfilter(bp, ap, wave) * 0.2
|
||||
wave += body
|
||||
# --- Attack transient: key click + breath burst ---
|
||||
attack_len = min(int(SAMPLE_RATE * 0.015), n_samples)
|
||||
if attack_len > 0:
|
||||
click = rng.uniform(-1.0, 1.0, attack_len)
|
||||
click *= numpy.exp(-numpy.linspace(0, 8, attack_len))
|
||||
wave[:attack_len] += click * 0.12
|
||||
|
||||
# --- Sub-harmonic warmth ---
|
||||
# Conical bore coupling produces energy slightly below fundamental
|
||||
if hz > 80: # only if there's room
|
||||
sub = numpy.sin(2 * numpy.pi * (hz * 0.5) * t) * 0.04
|
||||
sub *= numpy.clip(t / 0.1, 0.0, 1.0) # fade in gently
|
||||
wave += sub
|
||||
|
||||
# --- Final shaping ---
|
||||
mx = numpy.abs(wave).max()
|
||||
if mx > 0:
|
||||
wave /= mx
|
||||
@@ -2048,6 +2117,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,8 +2449,15 @@ 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"
|
||||
RHODES = "rhodes_synth"
|
||||
WURLITZER = "wurlitzer_synth"
|
||||
VIBRAPHONE = "vibraphone_synth"
|
||||
PIPE_ORGAN = "pipe_organ_synth"
|
||||
CHOIR = "choir_synth"
|
||||
|
||||
def __call__(self, hz, **kwargs):
|
||||
"""Make Synth members callable — dispatches to the wave function."""
|
||||
@@ -2319,6 +2488,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 +3746,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 +4061,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))
|
||||
@@ -4577,7 +4976,19 @@ def _apply_distortion(samples, drive=1.0, mix=1.0):
|
||||
"""
|
||||
if mix <= 0 or drive <= 0:
|
||||
return samples
|
||||
driven = numpy.tanh(samples * drive)
|
||||
# Multi-stage gain + clipping like a real amp:
|
||||
# Stage 1: preamp gain — push the signal hard
|
||||
stage1 = numpy.tanh(samples * drive)
|
||||
# Stage 2: power amp — clip again with more gain for sustain and grit
|
||||
stage2 = numpy.tanh(stage1 * drive * 0.5)
|
||||
# Stage 3: at high drive, add asymmetric clipping (tube rectifier sag)
|
||||
if drive > 3.0:
|
||||
# Positive peaks clip harder than negative — asymmetric harmonics
|
||||
driven = numpy.where(stage2 > 0,
|
||||
numpy.tanh(stage2 * 1.5),
|
||||
numpy.tanh(stage2 * 1.2))
|
||||
else:
|
||||
driven = stage2
|
||||
return samples * (1 - mix) + driven * mix
|
||||
|
||||
|
||||
|
||||
+23
-7
@@ -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,
|
||||
@@ -370,22 +380,19 @@ INSTRUMENTS = {
|
||||
# ── Woodwinds (continued) ──
|
||||
"saxophone": {
|
||||
"synth": "saxophone_synth", "envelope": "bowed",
|
||||
"humanize": 0.15, "vel_to_filter": 1500,
|
||||
"humanize": 0.15,
|
||||
},
|
||||
"alto_sax": {
|
||||
"synth": "saxophone_synth", "envelope": "bowed",
|
||||
"humanize": 0.15, "vel_to_filter": 1800,
|
||||
"humanize": 0.15,
|
||||
},
|
||||
"tenor_sax": {
|
||||
"synth": "saxophone_synth", "envelope": "bowed",
|
||||
"lowpass": 3000,
|
||||
"humanize": 0.15, "vel_to_filter": 1200,
|
||||
"humanize": 0.15,
|
||||
},
|
||||
"bari_sax": {
|
||||
"synth": "saxophone_synth", "envelope": "bowed",
|
||||
"lowpass": 2000,
|
||||
"humanize": 0.15, "vel_to_filter": 800,
|
||||
"sub_osc": 0.15,
|
||||
"humanize": 0.15, "sub_osc": 0.15,
|
||||
},
|
||||
}
|
||||
|
||||
@@ -468,6 +475,8 @@ class Note:
|
||||
|
||||
@property
|
||||
def beats(self) -> float:
|
||||
if self._hold:
|
||||
return 0.0
|
||||
return self.duration.value
|
||||
|
||||
|
||||
@@ -595,6 +604,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:
|
||||
|
||||
+2
-2
@@ -5333,7 +5333,7 @@ def test_supersaw_wave():
|
||||
@needs_portaudio
|
||||
def test_all_synths_in_enum():
|
||||
from pytheory.play import Synth
|
||||
assert len(Synth) == 42
|
||||
assert len(Synth) == 51
|
||||
for s in Synth:
|
||||
wave = s(440, n_samples=1000)
|
||||
assert len(wave) == 1000
|
||||
@@ -7155,7 +7155,7 @@ def test_score_system_propagates():
|
||||
|
||||
def test_synth_enum_count():
|
||||
from pytheory.play import Synth
|
||||
assert len(Synth) == 42
|
||||
assert len(Synth) == 51
|
||||
|
||||
|
||||
def test_all_synths_render_and_enum_match():
|
||||
|
||||
Reference in New Issue
Block a user