mirror of
https://github.com/kennethreitz/pytheory.git
synced 2026-06-05 23:00:20 +00:00
Compare commits
12 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 751d5a49b8 | |||
| 6a836dd891 | |||
| 1f888e2b21 | |||
| fb923f6c76 | |||
| 59e3338892 | |||
| 8cf4145c15 | |||
| b3885b2c15 | |||
| ae04fa60cc | |||
| 6c411e43f8 | |||
| e0427af3cc | |||
| 552836ae5b | |||
| 0fe53fcdeb |
@@ -2,6 +2,30 @@
|
||||
|
||||
All notable changes to PyTheory are documented here.
|
||||
|
||||
## 0.35.0
|
||||
|
||||
- **8.5x faster import** — dropped pytuning/sympy, lazy-load scipy.
|
||||
`import pytheory` now takes ~50ms instead of ~480ms (#44)
|
||||
- **Proper shruti JI ratios** — 22 positions with 5-limit just intonation
|
||||
(pure 3/2 fifths, 5/4 thirds), not 22-TET approximation
|
||||
- **Arabic maqam JI ratios** — Zalzalian 11-limit ratios.
|
||||
Mi↓ (the Rast third) is exactly 27/22 from Do
|
||||
- **B#/Cb octave boundary fix** — B#4 = C5, Cb4 = B3 (#45)
|
||||
- **Int tone names** — `Tone(0, system=TET(22))` works alongside strings.
|
||||
Wrapping: `Tone(22)` → tone 0, octave+1. `System.tone()` convenience.
|
||||
- **Timpani synth** — inharmonic membrane modes, felt mallet, copper kettle
|
||||
resonance, cathedral reverb
|
||||
- **Saxophone synth** — conical bore, reed buzz, brass body warmth.
|
||||
4 presets: saxophone, alto_sax, tenor_sax, bari_sax
|
||||
- **Part.roll()** — rapid repeated notes with velocity ramp for crescendo/
|
||||
decrescendo rolls on any instrument
|
||||
- **Vibrato tuning** — all instruments reduced to 0.001 depth for cleaner
|
||||
ensemble sound
|
||||
- **Granular synthesis** — grain cloud engine with scatter, pitch
|
||||
variation, and Hanning-windowed grains. Two presets: granular_pad,
|
||||
granular_texture.
|
||||
- 30 synth waveforms, 838 tests
|
||||
|
||||
## 0.34.0
|
||||
|
||||
- **16 dedicated instrument synths** — physical modeling and specialized
|
||||
|
||||
@@ -620,6 +620,36 @@ Three bend types:
|
||||
- ``"late"`` — holds the starting pitch for 60%, bends in the last 40%.
|
||||
The classic blues "curl."
|
||||
|
||||
Rolls
|
||||
-----
|
||||
|
||||
Rapid repeated notes with a velocity ramp — perfect for timpani
|
||||
rolls, snare rolls, tremolo on any instrument. The velocity ramps
|
||||
from ``velocity_start`` to ``velocity_end`` for crescendo or
|
||||
decrescendo effects.
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
# Timpani crescendo roll
|
||||
timp = score.part("timp", instrument="timpani")
|
||||
timp.roll("C3", Duration.WHOLE, velocity_start=20, velocity_end=110)
|
||||
timp.add("C3", Duration.HALF, velocity=127) # big accent
|
||||
|
||||
# Snare roll with 32nd notes
|
||||
snare = score.part("snare", synth="noise", envelope="pluck")
|
||||
snare.roll("C4", Duration.HALF, speed=0.125,
|
||||
velocity_start=40, velocity_end=100)
|
||||
|
||||
# Decrescendo (loud to quiet)
|
||||
timp.roll("G2", Duration.WHOLE, velocity_start=100, velocity_end=30)
|
||||
|
||||
Parameters:
|
||||
|
||||
- ``velocity_start``: Starting velocity (default 40).
|
||||
- ``velocity_end``: Ending velocity (default 100).
|
||||
- ``speed``: Note subdivision (default ``Duration.SIXTEENTH``).
|
||||
Use ``0.125`` for 32nd notes, ``Duration.EIGHTH`` for 8th notes.
|
||||
|
||||
Tuning Systems
|
||||
--------------
|
||||
|
||||
|
||||
+55
-3
@@ -1,7 +1,7 @@
|
||||
Synthesizers
|
||||
============
|
||||
|
||||
PyTheory includes 27 built-in waveforms and 10 ADSR envelope presets.
|
||||
PyTheory includes 30 built-in waveforms and 10 ADSR envelope presets.
|
||||
Every sound is generated from scratch -- no samples or external audio
|
||||
files needed.
|
||||
|
||||
@@ -390,11 +390,11 @@ Dedicated Instrument Synths
|
||||
--------------------------
|
||||
|
||||
Beyond the classic and physical modeling waveforms, PyTheory includes
|
||||
14 dedicated instrument synths. Each one uses tailored synthesis
|
||||
17 dedicated instrument synths. Each one uses tailored synthesis
|
||||
techniques -- additive harmonics, formant shaping, body resonance
|
||||
modeling, and specialized envelopes -- to capture the character of a
|
||||
specific acoustic instrument. These are the waveforms that bring the
|
||||
total count to 27.
|
||||
total count to 30.
|
||||
|
||||
Piano Synth
|
||||
~~~~~~~~~~~
|
||||
@@ -535,6 +535,58 @@ bridge, producing a shimmering, metallic sustain.
|
||||
|
||||
sitar = score.part("sitar", synth="sitar_synth")
|
||||
|
||||
Timpani Synth
|
||||
~~~~~~~~~~~~~
|
||||
|
||||
Large kettle drum with definite pitch. Inharmonic membrane modes
|
||||
(1.0, 1.5, 1.99, 2.44), felt mallet attack, copper kettle resonance.
|
||||
Use ``Part.roll()`` for crescendo timpani rolls.
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
timp = score.part("timp", synth="timpani_synth")
|
||||
timp.roll("C3", Duration.WHOLE, velocity_start=20, velocity_end=110)
|
||||
|
||||
Saxophone Synth
|
||||
~~~~~~~~~~~~~~~
|
||||
|
||||
Single reed through a conical brass bore. All harmonics with strong
|
||||
mids, reed buzz, and brass body warmth. Four presets: ``saxophone``,
|
||||
``alto_sax``, ``tenor_sax``, ``bari_sax``.
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
sax = score.part("sax", instrument="tenor_sax")
|
||||
|
||||
Granular Synth
|
||||
~~~~~~~~~~~~~~
|
||||
|
||||
Grain cloud synthesis — chops a source waveform into tiny overlapping
|
||||
grains (10-200ms), each windowed and optionally pitch/time scattered.
|
||||
Creates textures impossible with other synthesis: frozen tones,
|
||||
shimmering clouds, evolving pads, glitchy stutters.
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
# Atmospheric granular pad
|
||||
pad = score.part("pad", instrument="granular_pad")
|
||||
|
||||
# Granular with filter envelope sweep + resonance
|
||||
texture = score.part("texture", synth="granular_synth", envelope="pad",
|
||||
filter_amount=4000, filter_attack=0.5,
|
||||
filter_decay=1.5, filter_sustain=0.3,
|
||||
lowpass=600, lowpass_q=3.0,
|
||||
reverb=0.5, reverb_type="taj_mahal")
|
||||
|
||||
Parameters (passed as synth kwargs):
|
||||
|
||||
- ``grain_size``: Duration per grain in seconds (default 0.04).
|
||||
- ``density``: Grains per second (default 50). Higher = denser cloud.
|
||||
- ``scatter``: Random position jitter 0-1 (default 0.5).
|
||||
- ``pitch_var``: Per-grain pitch randomization in cents (default 12).
|
||||
- ``source``: Base waveform — ``"saw"``, ``"sine"``, ``"triangle"``,
|
||||
``"square"``, ``"noise"``.
|
||||
|
||||
Analog Oscillator Drift
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
|
||||
+1
-1
@@ -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** — 27 waveforms (including Karplus-Strong pluck, Hammond organ,
|
||||
- **Synthesis** — 30 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
|
||||
|
||||
+1
-2
@@ -1,6 +1,6 @@
|
||||
[project]
|
||||
name = "pytheory"
|
||||
version = "0.34.1"
|
||||
version = "0.35.1"
|
||||
description = "Music Theory for Humans"
|
||||
readme = "README.md"
|
||||
license = "MIT"
|
||||
@@ -21,7 +21,6 @@ classifiers = [
|
||||
"Programming Language :: Python :: 3.13",
|
||||
]
|
||||
dependencies = [
|
||||
"pytuning",
|
||||
"numeral",
|
||||
"sounddevice",
|
||||
"scipy",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
"""PyTheory: Music Theory for Humans."""
|
||||
|
||||
__version__ = "0.34.1"
|
||||
__version__ = "0.35.1"
|
||||
|
||||
from .tones import Tone, Interval
|
||||
from .systems import System, SYSTEMS, TET
|
||||
|
||||
+48
-30
@@ -1,4 +1,4 @@
|
||||
from pytuning import scales
|
||||
import math
|
||||
|
||||
REFERENCE_A = 440
|
||||
|
||||
@@ -6,41 +6,59 @@ REFERENCE_A = 440
|
||||
# Scientific pitch notation changes octave at C, not A, so this offset
|
||||
# is needed for all octave arithmetic.
|
||||
C_INDEX = 3
|
||||
def _create_just_intonation_scale(n):
|
||||
"""5-limit just intonation ratios for 12-tone systems.
|
||||
|
||||
These are the pure frequency ratios derived from the harmonic series —
|
||||
the way intervals "want" to sound before equal temperament imposed
|
||||
compromise. Each ratio is mathematically exact: a perfect fifth is
|
||||
exactly 3/2, a major third is exactly 5/4.
|
||||
|
||||
For non-12 systems, falls back to equal temperament.
|
||||
# ── Temperament scale generators (replaces pytuning dependency) ──────────
|
||||
|
||||
def _create_edo_scale(n):
|
||||
"""N-tone equal division of the octave. Each step = 2^(1/n)."""
|
||||
return [2 ** (i / n) for i in range(n + 1)]
|
||||
|
||||
|
||||
def _create_pythagorean_scale(n):
|
||||
"""Pythagorean tuning — spiral of pure fifths (3/2 ratio).
|
||||
|
||||
Each tone is generated by stacking perfect fifths and octave-reducing.
|
||||
"""
|
||||
from fractions import Fraction
|
||||
ratios = [1.0]
|
||||
for i in range(1, n):
|
||||
# Stack fifths: (3/2)^i, then reduce to within one octave
|
||||
r = (3 / 2) ** i
|
||||
while r >= 2.0:
|
||||
r /= 2.0
|
||||
ratios.append(r)
|
||||
ratios.sort()
|
||||
ratios.append(2.0)
|
||||
return ratios
|
||||
|
||||
|
||||
def _create_quarter_comma_meantone_scale(n):
|
||||
"""Quarter-comma meantone — pure major thirds (5/4), tempered fifths.
|
||||
|
||||
The fifth is narrowed by 1/4 of a syntonic comma so that four
|
||||
fifths make a pure major third (5/4). The meantone fifth =
|
||||
5^(1/4) ≈ 1.49535.
|
||||
"""
|
||||
fifth = 5 ** 0.25 # meantone fifth ≈ 1.49535 (vs 1.5 pure)
|
||||
ratios = [1.0]
|
||||
for i in range(1, n):
|
||||
r = fifth ** i
|
||||
while r >= 2.0:
|
||||
r /= 2.0
|
||||
ratios.append(r)
|
||||
ratios.sort()
|
||||
ratios.append(2.0)
|
||||
return ratios
|
||||
def _create_just_intonation_scale(n):
|
||||
"""5-limit just intonation ratios for 12-tone systems."""
|
||||
if n != 12:
|
||||
return scales.create_edo_scale(n)
|
||||
# Standard 5-limit JI ratios (A-based: A=1/1)
|
||||
ratios = [
|
||||
Fraction(1, 1), # A — unison
|
||||
Fraction(16, 15), # A# — minor second
|
||||
Fraction(9, 8), # B — major second
|
||||
Fraction(6, 5), # C — minor third
|
||||
Fraction(5, 4), # C# — major third
|
||||
Fraction(4, 3), # D — perfect fourth
|
||||
Fraction(45, 32), # D# — augmented fourth
|
||||
Fraction(3, 2), # E — perfect fifth
|
||||
Fraction(8, 5), # F — minor sixth
|
||||
Fraction(5, 3), # F# — major sixth
|
||||
Fraction(9, 5), # G — minor seventh
|
||||
Fraction(15, 8), # G# — major seventh
|
||||
Fraction(2, 1), # A — octave
|
||||
]
|
||||
return [float(r) for r in ratios]
|
||||
return _create_edo_scale(n)
|
||||
return [1, 16/15, 9/8, 6/5, 5/4, 4/3, 45/32, 3/2, 8/5, 5/3, 9/5, 15/8, 2.0]
|
||||
|
||||
TEMPERAMENTS = {
|
||||
"equal": scales.create_edo_scale,
|
||||
"pythagorean": scales.create_pythagorean_scale,
|
||||
"meantone": scales.create_quarter_comma_meantone_scale,
|
||||
"equal": _create_edo_scale,
|
||||
"pythagorean": _create_pythagorean_scale,
|
||||
"meantone": _create_quarter_comma_meantone_scale,
|
||||
"just": _create_just_intonation_scale,
|
||||
}
|
||||
|
||||
|
||||
+362
-8
@@ -2,11 +2,24 @@ from enum import Enum
|
||||
import time
|
||||
|
||||
import numpy
|
||||
import scipy.signal
|
||||
|
||||
from .tones import Tone
|
||||
|
||||
|
||||
class _LazyModule:
|
||||
"""Lazy import wrapper — module loaded on first attribute access."""
|
||||
def __init__(self, name):
|
||||
self._name = name
|
||||
self._mod = None
|
||||
def __getattr__(self, attr):
|
||||
if self._mod is None:
|
||||
import importlib
|
||||
self._mod = importlib.import_module(self._name)
|
||||
return getattr(self._mod, attr)
|
||||
|
||||
scipy = type('scipy', (), {'signal': _LazyModule('scipy.signal')})()
|
||||
|
||||
|
||||
def _get_sd():
|
||||
"""Lazy import sounddevice — only needed for actual audio playback."""
|
||||
import sounddevice as sd
|
||||
@@ -257,7 +270,7 @@ def strings_wave(hz, peak=SAMPLE_PEAK, n_samples=SAMPLE_RATE):
|
||||
|
||||
# Delayed vibrato: ramps in over ~200ms, like a real bow
|
||||
vib_rate = 5.2 + rng.uniform(-0.3, 0.3) # slight randomness per note
|
||||
vib_depth = hz * 0.003 # ~5 cents
|
||||
vib_depth = hz * 0.001 # subtle
|
||||
vib_onset = numpy.clip(t / 0.2, 0.0, 1.0) # ramp over 200ms
|
||||
vibrato = vib_depth * vib_onset * numpy.sin(2 * numpy.pi * vib_rate * t)
|
||||
|
||||
@@ -481,7 +494,7 @@ def trumpet_wave(hz, peak=SAMPLE_PEAK, n_samples=SAMPLE_RATE):
|
||||
|
||||
# Vibrato
|
||||
vib_onset = numpy.clip(t / 0.15, 0.0, 1.0)
|
||||
vib = hz * 0.002 * vib_onset * numpy.sin(2 * numpy.pi * 5.5 * t)
|
||||
vib = hz * 0.001 * vib_onset * numpy.sin(2 * numpy.pi * 5.5 * t)
|
||||
|
||||
# Lip buzz — additive with brass spectral shape
|
||||
# Trumpet has strong even AND odd harmonics (unlike clarinet)
|
||||
@@ -523,7 +536,7 @@ def clarinet_wave(hz, peak=SAMPLE_PEAK, n_samples=SAMPLE_RATE):
|
||||
rng = numpy.random.default_rng(int(hz * 100) % 2**31)
|
||||
|
||||
vib_onset = numpy.clip(t / 0.3, 0.0, 1.0)
|
||||
vib = hz * 0.002 * vib_onset * numpy.sin(2 * numpy.pi * 4.5 * t)
|
||||
vib = hz * 0.001 * vib_onset * numpy.sin(2 * numpy.pi * 4.5 * t)
|
||||
|
||||
# Cylindrical bore: odd harmonics dominate
|
||||
wave = numpy.zeros(n_samples, dtype=numpy.float64)
|
||||
@@ -592,7 +605,7 @@ def oboe_wave(hz, peak=SAMPLE_PEAK, n_samples=SAMPLE_RATE):
|
||||
rng = numpy.random.default_rng(int(hz * 100) % 2**31)
|
||||
|
||||
vib_onset = numpy.clip(t / 0.2, 0.0, 1.0)
|
||||
vib = hz * 0.002 * vib_onset * numpy.sin(2 * numpy.pi * 5.0 * t)
|
||||
vib = hz * 0.001 * vib_onset * numpy.sin(2 * numpy.pi * 5.0 * t)
|
||||
|
||||
wave = numpy.zeros(n_samples, dtype=numpy.float64)
|
||||
n_harmonics = min(18, int((SAMPLE_RATE / 2) / hz))
|
||||
@@ -668,7 +681,7 @@ def cello_wave(hz, peak=SAMPLE_PEAK, n_samples=SAMPLE_RATE):
|
||||
|
||||
# Delayed vibrato
|
||||
vib_rate = 5.0 + rng.uniform(-0.3, 0.3)
|
||||
vib_depth = hz * 0.002
|
||||
vib_depth = hz * 0.001
|
||||
vib_onset = numpy.clip(t / 0.25, 0.0, 1.0)
|
||||
vibrato = vib_depth * vib_onset * numpy.sin(2 * numpy.pi * vib_rate * t)
|
||||
|
||||
@@ -787,6 +800,336 @@ def upright_bass_wave(hz, peak=SAMPLE_PEAK, n_samples=SAMPLE_RATE):
|
||||
return (peak * out).astype(numpy.int16)
|
||||
|
||||
|
||||
def timpani_wave(hz, peak=SAMPLE_PEAK, n_samples=SAMPLE_RATE):
|
||||
"""Timpani — large kettle drum with definite pitch.
|
||||
|
||||
The copper kettle creates a tuned resonance with inharmonic
|
||||
overtones. The head modes are at ratios 1.0, 1.5, 1.99, 2.44
|
||||
(not integer multiples like strings). The felt mallet gives a
|
||||
soft attack with a deep, booming body.
|
||||
"""
|
||||
t = numpy.arange(n_samples, dtype=numpy.float64) / SAMPLE_RATE
|
||||
|
||||
# Timpani head modes — inharmonic but definite pitch
|
||||
# Mode ratios from vibrating circular membrane physics
|
||||
wave = numpy.sin(2 * numpy.pi * hz * t) * 0.8
|
||||
wave += numpy.sin(2 * numpy.pi * hz * 1.5 * t) * 0.35 * numpy.exp(-6 * t)
|
||||
wave += numpy.sin(2 * numpy.pi * hz * 1.99 * t) * 0.2 * numpy.exp(-10 * t)
|
||||
wave += numpy.sin(2 * numpy.pi * hz * 2.44 * t) * 0.1 * numpy.exp(-15 * t)
|
||||
|
||||
# Two-stage decay: initial thump fades fast, fundamental rings
|
||||
decay = numpy.where(t < 0.15,
|
||||
numpy.exp(-4 * t),
|
||||
numpy.exp(-4 * 0.15) * numpy.exp(-1.5 * (t - 0.15)))
|
||||
wave *= decay
|
||||
|
||||
# Felt mallet impact — warm, not sharp
|
||||
mallet_len = min(int(SAMPLE_RATE * 0.02), n_samples)
|
||||
rng = numpy.random.default_rng(int(hz * 100) % 2**31)
|
||||
mallet = rng.uniform(-0.3, 0.3, mallet_len)
|
||||
mallet *= numpy.exp(-numpy.linspace(0, 8, mallet_len))
|
||||
wave[:mallet_len] += mallet
|
||||
|
||||
# Copper kettle resonance — boosts low-mids
|
||||
import scipy.signal as _sig
|
||||
lo, hi = max(20, int(hz * 0.7)), min(SAMPLE_RATE // 2 - 1, int(hz * 2))
|
||||
if lo < hi:
|
||||
bp, ap = _sig.butter(2, [lo, hi], btype='band', fs=SAMPLE_RATE)
|
||||
kettle = _sig.lfilter(bp, ap, wave) * 0.3
|
||||
wave += kettle
|
||||
|
||||
mx = numpy.abs(wave).max()
|
||||
if mx > 0:
|
||||
wave /= mx
|
||||
|
||||
return (peak * wave).astype(numpy.int16)
|
||||
|
||||
|
||||
def saxophone_wave(hz, peak=SAMPLE_PEAK, n_samples=SAMPLE_RATE):
|
||||
"""Saxophone — single reed through 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.
|
||||
"""
|
||||
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)
|
||||
|
||||
wave = numpy.zeros(n_samples, dtype=numpy.float64)
|
||||
n_harmonics = min(20, 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)
|
||||
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)
|
||||
else:
|
||||
amp = 0.2 / n
|
||||
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))
|
||||
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
|
||||
|
||||
# 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
|
||||
|
||||
mx = numpy.abs(wave).max()
|
||||
if mx > 0:
|
||||
wave /= mx
|
||||
|
||||
return (peak * wave).astype(numpy.int16)
|
||||
|
||||
|
||||
def vocal_wave(hz, peak=SAMPLE_PEAK, n_samples=SAMPLE_RATE, lyric="ah"):
|
||||
"""Vocal/formant synthesis — sings vowel sounds at a given pitch.
|
||||
|
||||
Models the human voice with:
|
||||
1. LF glottal model — asymmetric pulse with sharp closure (not just sines)
|
||||
2. 5 parallel resonant formant filters (real voice has 5 formant peaks)
|
||||
3. Jitter + shimmer (natural pitch/amplitude irregularity)
|
||||
4. Aspiration noise mixed with the glottal source
|
||||
5. Consonant onsets (plosives, sibilants, nasals, etc.)
|
||||
"""
|
||||
import scipy.signal as _sig
|
||||
|
||||
# 5-formant table: (F1, F2, F3, F4, F5) frequencies and bandwidths
|
||||
# Based on Peterson & Barney (1952) measurements, male voice
|
||||
FORMANTS = {
|
||||
'a': [(800, 130), (1200, 100), (2500, 140), (3300, 250), (3750, 300)],
|
||||
'e': [(530, 80), (1850, 100), (2500, 130), (3300, 250), (3750, 300)],
|
||||
'i': [(280, 60), (2250, 100), (2900, 120), (3350, 250), (3750, 300)],
|
||||
'o': [(500, 100), (1000, 80), (2500, 140), (3300, 250), (3750, 300)],
|
||||
'u': ((325, 70), (700, 60), (2530, 140), (3300, 250), (3750, 300)),
|
||||
}
|
||||
# Formant gains (relative amplitude per formant)
|
||||
FGAINS = [1.0, 0.8, 0.5, 0.25, 0.15]
|
||||
|
||||
rng = numpy.random.default_rng(int(hz * 100 + len(lyric) * 7) % 2**31)
|
||||
t = numpy.arange(n_samples, dtype=numpy.float64) / SAMPLE_RATE
|
||||
|
||||
# Parse vowels from lyric
|
||||
vowels_in_lyric = [c.lower() for c in lyric if c.lower() in FORMANTS]
|
||||
if not vowels_in_lyric:
|
||||
vowels_in_lyric = ['a']
|
||||
|
||||
# ── Glottal source: LF model approximation ──
|
||||
# Asymmetric pulse: slow open phase, sharp closure, then closed phase.
|
||||
# Much more "voice-like" than a sine or sawtooth.
|
||||
# Jitter (pitch irregularity) + shimmer (amplitude irregularity)
|
||||
jitter = rng.normal(0, hz * 0.001, n_samples) # ~0.1% pitch jitter
|
||||
shimmer = 1.0 + rng.normal(0, 0.008, n_samples) # ~0.8% amp shimmer
|
||||
# Vibrato
|
||||
vib = hz * 0.001 * numpy.sin(2 * numpy.pi * 5.5 * t)
|
||||
inst_freq = hz + vib + jitter
|
||||
phase = numpy.cumsum(2 * numpy.pi * inst_freq / SAMPLE_RATE)
|
||||
# LF glottal shape: sharper falling edge via phase shaping
|
||||
saw = (phase / (2 * numpy.pi)) % 1.0 # 0 to 1 sawtooth
|
||||
# Asymmetric: slow rise (60%), fast fall (40%)
|
||||
glottal = numpy.where(saw < 0.6,
|
||||
numpy.sin(numpy.pi * saw / 0.6), # smooth rise
|
||||
-numpy.sin(numpy.pi * (saw - 0.6) / 0.4) * 0.8) # sharp fall
|
||||
glottal *= shimmer
|
||||
|
||||
# Aspiration noise (breathiness) — subtle
|
||||
breath = rng.normal(0, 0.04, n_samples)
|
||||
source = glottal * 0.92 + breath * 0.08
|
||||
|
||||
# ── Formant filtering ──
|
||||
n_vowels = len(vowels_in_lyric)
|
||||
out = numpy.zeros(n_samples, dtype=numpy.float64)
|
||||
|
||||
if n_vowels == 1:
|
||||
# Single vowel — filter the whole thing
|
||||
formants = FORMANTS[vowels_in_lyric[0]]
|
||||
for (fc, bw), gain in zip(formants, FGAINS):
|
||||
lo = max(20, fc - bw)
|
||||
hi = min(SAMPLE_RATE // 2 - 1, fc + bw)
|
||||
if lo < hi:
|
||||
bp, ap = _sig.butter(2, [lo, hi], btype='band', fs=SAMPLE_RATE)
|
||||
out += _sig.lfilter(bp, ap, source).astype(numpy.float64) * gain
|
||||
else:
|
||||
# Multiple vowels — crossfade formants
|
||||
samples_per_vowel = n_samples // n_vowels
|
||||
for vi, vowel in enumerate(vowels_in_lyric):
|
||||
formants = FORMANTS[vowel]
|
||||
start = vi * samples_per_vowel
|
||||
end = n_samples if vi == n_vowels - 1 else start + samples_per_vowel
|
||||
seg = source[start:end].copy()
|
||||
seg_out = numpy.zeros_like(seg)
|
||||
for (fc, bw), gain in zip(formants, FGAINS):
|
||||
lo = max(20, fc - bw)
|
||||
hi = min(SAMPLE_RATE // 2 - 1, fc + bw)
|
||||
if lo < hi:
|
||||
bp, ap = _sig.butter(2, [lo, hi], btype='band', fs=SAMPLE_RATE)
|
||||
seg_out += _sig.lfilter(bp, ap, seg).astype(numpy.float64) * gain
|
||||
# Crossfade
|
||||
fade = min(int(SAMPLE_RATE * 0.02), len(seg_out) // 4)
|
||||
if vi > 0 and fade > 0:
|
||||
seg_out[:fade] *= numpy.linspace(0, 1, fade)
|
||||
if vi < n_vowels - 1 and fade > 0:
|
||||
seg_out[-fade:] *= numpy.linspace(1, 0, fade)
|
||||
out[start:end] += seg_out[:end - start]
|
||||
|
||||
# ── Consonant onsets ──
|
||||
lyric_lower = lyric.lower()
|
||||
if lyric_lower and lyric_lower[0] not in 'aeiou':
|
||||
c = lyric_lower[0]
|
||||
cl = min(int(SAMPLE_RATE * 0.035), n_samples)
|
||||
if c in 'tdkpb':
|
||||
burst = rng.uniform(-0.5, 0.5, cl) * numpy.exp(-numpy.linspace(0, 18, cl))
|
||||
out[:cl] = burst + out[:cl] * 0.2
|
||||
elif c in 'sz':
|
||||
sib = rng.uniform(-0.4, 0.4, cl)
|
||||
if cl > 20:
|
||||
bl, al = _sig.butter(2, [3000, min(8000, SAMPLE_RATE//2-1)], btype='band', fs=SAMPLE_RATE)
|
||||
sib = _sig.lfilter(bl, al, numpy.pad(sib, (0, max(0, n_samples-cl))))[:cl]
|
||||
sib *= numpy.exp(-numpy.linspace(0, 10, cl))
|
||||
out[:cl] = sib * 0.6 + out[:cl] * 0.4
|
||||
elif c in 'mn':
|
||||
nl = min(int(SAMPLE_RATE * 0.06), n_samples)
|
||||
nasal = numpy.sin(2*numpy.pi*250*t[:nl]) * 0.4 * numpy.exp(-numpy.linspace(0, 4, nl))
|
||||
out[:nl] = nasal + out[:nl] * 0.4
|
||||
elif c in 'fv':
|
||||
fric = rng.uniform(-0.25, 0.25, cl) * numpy.exp(-numpy.linspace(0, 12, cl))
|
||||
out[:cl] = fric * 0.5 + out[:cl] * 0.5
|
||||
elif c in 'lr':
|
||||
gl = min(int(SAMPLE_RATE * 0.05), n_samples)
|
||||
ghz = hz * 0.7 + hz * 0.3 * numpy.linspace(0, 1, gl)
|
||||
glide = numpy.sin(numpy.cumsum(2*numpy.pi*ghz/SAMPLE_RATE)) * 0.35
|
||||
out[:gl] = glide + out[:gl] * 0.65
|
||||
elif c == 'h':
|
||||
hl = min(int(SAMPLE_RATE * 0.05), n_samples)
|
||||
asp = rng.uniform(-0.4, 0.4, hl) * numpy.exp(-numpy.linspace(0, 5, hl))
|
||||
out[:hl] = asp * 0.6 + out[:hl] * 0.4
|
||||
elif c == 'w':
|
||||
wl = min(int(SAMPLE_RATE * 0.06), n_samples)
|
||||
ws = numpy.sin(numpy.cumsum(2*numpy.pi*hz/SAMPLE_RATE*numpy.ones(wl)))
|
||||
if wl > 20:
|
||||
bp, ap = _sig.butter(2, [max(20,300), min(800, SAMPLE_RATE//2-1)], btype='band', fs=SAMPLE_RATE)
|
||||
ws = _sig.lfilter(bp, ap, ws)
|
||||
ws *= numpy.linspace(0.5, 0, wl)
|
||||
out[:wl] = ws * 0.4 + out[:wl] * 0.6
|
||||
|
||||
# Soft edges — prevent clicks at note boundaries
|
||||
fade_samples = min(int(SAMPLE_RATE * 0.01), n_samples // 4)
|
||||
if fade_samples > 0:
|
||||
out[:fade_samples] *= numpy.linspace(0, 1, fade_samples)
|
||||
out[-fade_samples:] *= numpy.linspace(1, 0, fade_samples)
|
||||
|
||||
mx = numpy.abs(out).max()
|
||||
if mx > 0:
|
||||
out /= mx
|
||||
|
||||
return (peak * out).astype(numpy.int16)
|
||||
|
||||
|
||||
def granular_wave(hz, peak=SAMPLE_PEAK, n_samples=SAMPLE_RATE,
|
||||
grain_size=0.04, density=50, scatter=0.5,
|
||||
pitch_var=12, source="saw"):
|
||||
"""Granular synthesis — clouds of tiny sound grains.
|
||||
|
||||
Chops a source waveform into overlapping micro-grains (10-200ms),
|
||||
each independently windowed and optionally pitch/time scattered.
|
||||
Creates textures impossible with other synthesis: frozen tones,
|
||||
shimmering clouds, evolving pads, glitchy stutters.
|
||||
|
||||
Args:
|
||||
hz: Base frequency.
|
||||
grain_size: Duration of each grain in seconds (default 0.05 = 50ms).
|
||||
density: Grains per second (default 20). Higher = denser cloud.
|
||||
scatter: Random position jitter 0-1 (default 0.3). How much each
|
||||
grain's read position varies from sequential order.
|
||||
pitch_var: Random pitch variation per grain in cents (default 5).
|
||||
source: Base waveform — ``"saw"``, ``"sine"``, ``"triangle"``,
|
||||
``"square"``, ``"noise"`` (default ``"saw"``).
|
||||
"""
|
||||
rng = numpy.random.default_rng(int(hz * 100) % 2**31)
|
||||
|
||||
# Generate source material — longer than needed for scatter headroom
|
||||
src_len = n_samples + int(SAMPLE_RATE * scatter * 2)
|
||||
src_fns = {
|
||||
"saw": sawtooth_wave, "sine": sine_wave, "triangle": triangle_wave,
|
||||
"square": square_wave, "noise": noise_wave,
|
||||
}
|
||||
src_fn = src_fns.get(source, sawtooth_wave)
|
||||
src = src_fn(hz, n_samples=src_len).astype(numpy.float64) / SAMPLE_PEAK
|
||||
|
||||
# Grain parameters
|
||||
grain_samples = max(64, int(grain_size * SAMPLE_RATE))
|
||||
n_grains = max(1, int(n_samples / SAMPLE_RATE * density))
|
||||
|
||||
# Hanning window for each grain (smooth fade in/out, no clicks)
|
||||
window = numpy.hanning(grain_samples).astype(numpy.float64)
|
||||
|
||||
out = numpy.zeros(n_samples, dtype=numpy.float64)
|
||||
|
||||
for i in range(n_grains):
|
||||
# Output position — evenly spaced with jitter
|
||||
base_pos = int(i * n_samples / n_grains)
|
||||
jitter = int(rng.uniform(-0.5, 0.5) * n_samples / n_grains * 0.3)
|
||||
out_pos = max(0, min(n_samples - grain_samples, base_pos + jitter))
|
||||
|
||||
# Source read position — sequential with scatter
|
||||
src_pos = int(base_pos * src_len / n_samples)
|
||||
src_jitter = int(rng.uniform(-scatter, scatter) * grain_samples * 4)
|
||||
src_pos = max(0, min(src_len - grain_samples, src_pos + src_jitter))
|
||||
|
||||
# Per-grain pitch variation via resampling
|
||||
if pitch_var > 0:
|
||||
cents = rng.uniform(-pitch_var, pitch_var)
|
||||
rate = 2 ** (cents / 1200)
|
||||
read_len = max(2, min(int(grain_samples * rate), src_len - src_pos))
|
||||
grain_src = src[src_pos:src_pos + read_len]
|
||||
x_old = numpy.linspace(0, 1, len(grain_src))
|
||||
x_new = numpy.linspace(0, 1, grain_samples)
|
||||
grain = numpy.interp(x_new, x_old, grain_src)
|
||||
else:
|
||||
end = min(src_pos + grain_samples, src_len)
|
||||
grain = src[src_pos:end]
|
||||
if len(grain) < grain_samples:
|
||||
grain = numpy.pad(grain, (0, grain_samples - len(grain)))
|
||||
|
||||
# Apply window and mix
|
||||
grain *= window[:len(grain)]
|
||||
end = min(out_pos + len(grain), n_samples)
|
||||
out[out_pos:end] += grain[:end - out_pos]
|
||||
|
||||
mx = numpy.abs(out).max()
|
||||
if mx > 0:
|
||||
out /= mx
|
||||
|
||||
return (peak * out).astype(numpy.int16)
|
||||
|
||||
|
||||
def acoustic_guitar_wave(hz, peak=SAMPLE_PEAK, n_samples=SAMPLE_RATE):
|
||||
"""Acoustic guitar — Karplus-Strong with wooden body resonance.
|
||||
|
||||
@@ -1087,6 +1430,10 @@ class Synth(Enum):
|
||||
CELLO = "cello_synth"
|
||||
HARP = "harp_synth"
|
||||
UPRIGHT_BASS = "upright_bass_synth"
|
||||
TIMPANI = "timpani_synth"
|
||||
SAXOPHONE = "saxophone_synth"
|
||||
GRANULAR = "granular_synth"
|
||||
VOCAL = "vocal_synth"
|
||||
ACOUSTIC_GUITAR = "acoustic_guitar_synth"
|
||||
SITAR = "sitar_synth"
|
||||
ELECTRIC_GUITAR = "electric_guitar_synth"
|
||||
@@ -1108,6 +1455,8 @@ _SYNTH_FUNCTIONS = {
|
||||
"marimba_synth": marimba_wave, "oboe_synth": oboe_wave,
|
||||
"harpsichord_synth": harpsichord_wave, "cello_synth": cello_wave,
|
||||
"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,
|
||||
"acoustic_guitar_synth": acoustic_guitar_wave,
|
||||
"sitar_synth": sitar_wave, "electric_guitar_synth": electric_guitar_wave,
|
||||
}
|
||||
@@ -3323,8 +3672,13 @@ def _render_notes_to_buf(notes, buf, samples_per_beat, total_samples,
|
||||
bent = src_f[idx] * (1 - frac) + src_f[numpy.minimum(idx + 1, src_len - 1)] * frac
|
||||
waves.append((bent * SAMPLE_PEAK).astype(numpy.int16))
|
||||
else:
|
||||
# Render oscillators (pass synth_kwargs for FM etc.)
|
||||
waves = [synth_fn(hz, n_samples=n_samples, **_skw)
|
||||
# Per-note kwargs (e.g. lyric for vocal synth)
|
||||
note_skw = dict(_skw)
|
||||
note_lyric = getattr(note, 'lyric', '')
|
||||
if note_lyric:
|
||||
note_skw['lyric'] = note_lyric
|
||||
# Render oscillators
|
||||
waves = [synth_fn(hz, n_samples=n_samples, **note_skw)
|
||||
for hz in pitches]
|
||||
# Sub-oscillator: octave-below sine
|
||||
if sub_osc > 0:
|
||||
|
||||
+92
-2
@@ -241,6 +241,26 @@ INSTRUMENTS = {
|
||||
"vel_to_filter": 3000,
|
||||
"analog": 0.3,
|
||||
},
|
||||
"granular_pad": {
|
||||
"synth": "granular_synth", "envelope": "pad",
|
||||
"reverb": 0.4, "reverb_type": "cathedral",
|
||||
"analog": 0.3,
|
||||
},
|
||||
"vocal": {
|
||||
"synth": "vocal_synth", "envelope": "strings",
|
||||
"reverb": 0.3, "reverb_type": "hall",
|
||||
"humanize": 0.15,
|
||||
},
|
||||
"choir": {
|
||||
"synth": "vocal_synth", "envelope": "pad",
|
||||
"detune": 8, "spread": 0.4,
|
||||
"reverb": 0.45, "reverb_type": "cathedral",
|
||||
},
|
||||
"granular_texture": {
|
||||
"synth": "granular_synth", "envelope": "none",
|
||||
"reverb": 0.5, "reverb_type": "taj_mahal",
|
||||
"delay": 0.3, "delay_time": 0.4, "delay_feedback": 0.4,
|
||||
},
|
||||
"808_bass": {
|
||||
"synth": "sine", "envelope": "pluck",
|
||||
"distortion": 0.4, "distortion_drive": 2.5,
|
||||
@@ -275,6 +295,31 @@ INSTRUMENTS = {
|
||||
"fm_ratio": 2.0, "fm_index": 3.0,
|
||||
"reverb": 0.4, "reverb_type": "cathedral",
|
||||
},
|
||||
"timpani": {
|
||||
"synth": "timpani_synth", "envelope": "none",
|
||||
"reverb": 0.4, "reverb_type": "cathedral",
|
||||
},
|
||||
|
||||
# ── Woodwinds (continued) ──
|
||||
"saxophone": {
|
||||
"synth": "saxophone_synth", "envelope": "bowed",
|
||||
"humanize": 0.15, "vel_to_filter": 1500,
|
||||
},
|
||||
"alto_sax": {
|
||||
"synth": "saxophone_synth", "envelope": "bowed",
|
||||
"humanize": 0.15, "vel_to_filter": 1800,
|
||||
},
|
||||
"tenor_sax": {
|
||||
"synth": "saxophone_synth", "envelope": "bowed",
|
||||
"lowpass": 3000,
|
||||
"humanize": 0.15, "vel_to_filter": 1200,
|
||||
},
|
||||
"bari_sax": {
|
||||
"synth": "saxophone_synth", "envelope": "bowed",
|
||||
"lowpass": 2000,
|
||||
"humanize": 0.15, "vel_to_filter": 800,
|
||||
"sub_osc": 0.15,
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
@@ -332,6 +377,7 @@ class Note:
|
||||
velocity: int = 100
|
||||
bend: float = 0.0
|
||||
bend_type: str = "smooth" # "smooth" (log), "linear", "late"
|
||||
lyric: str = "" # syllable for vocal synth
|
||||
|
||||
@property
|
||||
def beats(self) -> float:
|
||||
@@ -2060,7 +2106,7 @@ class Part:
|
||||
self._automation: list[tuple[float, dict]] = [] # (beat, {param: value})
|
||||
|
||||
def add(self, tone_or_string, duration=Duration.QUARTER, *, velocity: int = 100,
|
||||
bend: float = 0.0, bend_type: str = "smooth") -> "Part":
|
||||
bend: float = 0.0, bend_type: str = "smooth", lyric: str = "") -> "Part":
|
||||
"""Add a note. Accepts Tone/Chord objects or note strings like ``"E5"``.
|
||||
|
||||
Duration can be a ``Duration`` enum or a raw float (beats).
|
||||
@@ -2078,7 +2124,7 @@ class Part:
|
||||
duration = _RawDuration(duration)
|
||||
self.notes.append(Note(tone=tone_or_string, duration=duration,
|
||||
velocity=velocity, bend=bend,
|
||||
bend_type=bend_type))
|
||||
bend_type=bend_type, lyric=lyric))
|
||||
return self
|
||||
|
||||
def set(self, **params) -> "Part":
|
||||
@@ -2435,6 +2481,50 @@ class Part:
|
||||
|
||||
return self
|
||||
|
||||
def roll(self, tone_or_string, duration=Duration.WHOLE, *,
|
||||
velocity_start: int = 40, velocity_end: int = 100,
|
||||
speed=Duration.SIXTEENTH) -> "Part":
|
||||
"""Play a roll — rapid repeated notes with velocity ramp.
|
||||
|
||||
Perfect for timpani rolls, snare rolls, tremolo on any
|
||||
instrument. The velocity ramps from ``velocity_start`` to
|
||||
``velocity_end`` over the duration for crescendo/decrescendo.
|
||||
|
||||
Args:
|
||||
tone_or_string: The note to repeat.
|
||||
duration: Total duration of the roll.
|
||||
velocity_start: Velocity of the first hit (default 40).
|
||||
velocity_end: Velocity of the last hit (default 100).
|
||||
speed: How fast to repeat (default SIXTEENTH notes).
|
||||
|
||||
Returns:
|
||||
Self for chaining.
|
||||
|
||||
Example::
|
||||
|
||||
>>> timp = score.part("timp", instrument="timpani")
|
||||
>>> timp.roll("C3", Duration.WHOLE, velocity_start=30, velocity_end=110)
|
||||
"""
|
||||
if hasattr(duration, 'value'):
|
||||
total = duration.value
|
||||
else:
|
||||
total = float(duration)
|
||||
if hasattr(speed, 'value'):
|
||||
step = speed.value
|
||||
else:
|
||||
step = float(speed)
|
||||
|
||||
n_hits = max(1, int(total / step))
|
||||
for i in range(n_hits):
|
||||
frac = i / max(1, n_hits - 1)
|
||||
vel = int(velocity_start + (velocity_end - velocity_start) * frac)
|
||||
vel = max(1, min(127, vel))
|
||||
remaining = total - i * step
|
||||
note_dur = min(step, remaining)
|
||||
if note_dur > 0:
|
||||
self.add(tone_or_string, note_dur, velocity=vel)
|
||||
return self
|
||||
|
||||
@property
|
||||
def is_drums(self) -> bool:
|
||||
"""True if this part contains drum hits."""
|
||||
|
||||
+4
-5
@@ -816,8 +816,7 @@ class Tone:
|
||||
pitch_scale = list(custom_ratios) + [period]
|
||||
elif period != 2.0 and temperament == "equal":
|
||||
# Non-octave period (e.g. Bohlen-Pierce tritave=3.0)
|
||||
import sympy
|
||||
pitch_scale = [period ** sympy.Rational(i, tones) for i in range(tones + 1)]
|
||||
pitch_scale = [period ** (i / tones) for i in range(tones + 1)]
|
||||
else:
|
||||
pitch_scale = TEMPERAMENTS[temperament](tones)
|
||||
octave = self.octave if self.octave is not None else 4
|
||||
@@ -834,7 +833,7 @@ class Tone:
|
||||
if symbolic:
|
||||
return reference_pitch * ratio
|
||||
else:
|
||||
result = reference_pitch * ratio
|
||||
result = float(reference_pitch * ratio)
|
||||
if precision:
|
||||
return float(result.evalf(precision))
|
||||
return float(result)
|
||||
return round(result, precision)
|
||||
return result
|
||||
|
||||
+1
-1
@@ -5320,7 +5320,7 @@ def test_supersaw_wave():
|
||||
@needs_portaudio
|
||||
def test_all_synths_in_enum():
|
||||
from pytheory.play import Synth
|
||||
assert len(Synth) == 27
|
||||
assert len(Synth) == 30
|
||||
for s in Synth:
|
||||
wave = s(440, n_samples=1000)
|
||||
assert len(wave) == 1000
|
||||
|
||||
@@ -444,15 +444,6 @@ wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/b3/38/89ba8ad64ae25be8de66a6d463314cf1eb366222074cfda9ee839c56a4b4/mdurl-0.1.2-py3-none-any.whl", hash = "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8", size = 9979, upload-time = "2022-08-14T12:40:09.779Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "mpmath"
|
||||
version = "1.3.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/e0/47/dd32fa426cc72114383ac549964eecb20ecfd886d1e5ccf5340b55b02f57/mpmath-1.3.0.tar.gz", hash = "sha256:7a28eb2a9774d00c7bc92411c19a89209d5da7c4c9a9e227be8330a23a25b91f", size = 508106, upload-time = "2023-03-07T16:47:11.061Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/43/e3/7d92a15f894aa0c9c4b49b8ee9ac9850d6e63b03c9c32c0367a13ae62209/mpmath-1.3.0-py3-none-any.whl", hash = "sha256:a0b2b9fe80bbcd81a6647ff13108738cfb482d481d826cc0e02f5b35e5c88d2c", size = 536198, upload-time = "2023-03-07T16:47:09.197Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "myst-parser"
|
||||
version = "4.0.1"
|
||||
@@ -707,11 +698,10 @@ wheels = [
|
||||
|
||||
[[package]]
|
||||
name = "pytheory"
|
||||
version = "0.34.1"
|
||||
version = "0.35.1"
|
||||
source = { editable = "." }
|
||||
dependencies = [
|
||||
{ name = "numeral" },
|
||||
{ name = "pytuning" },
|
||||
{ name = "scipy", version = "1.15.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" },
|
||||
{ name = "scipy", version = "1.17.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" },
|
||||
{ name = "sounddevice" },
|
||||
@@ -732,7 +722,6 @@ docs = [
|
||||
[package.metadata]
|
||||
requires-dist = [
|
||||
{ name = "numeral" },
|
||||
{ name = "pytuning" },
|
||||
{ name = "scipy" },
|
||||
{ name = "sounddevice" },
|
||||
]
|
||||
@@ -744,19 +733,6 @@ docs = [
|
||||
{ name = "sphinx" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pytuning"
|
||||
version = "0.7.3"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" },
|
||||
{ name = "numpy", version = "2.4.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" },
|
||||
{ name = "sympy" },
|
||||
]
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/26/59/e2c2fc91688f788587fb387ef6120c9a1ad3a8b88771fba9fc6a9c9a969d/PyTuning-0.7.3-py3-none-any.whl", hash = "sha256:db0b1231c012c1cf6a3c73aa7d791b4cff79a72f2ec6535f159c873fe302214b", size = 108174, upload-time = "2023-09-02T21:11:00.657Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pyyaml"
|
||||
version = "6.0.3"
|
||||
@@ -1151,18 +1127,6 @@ wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/52/a7/d2782e4e3f77c8450f727ba74a8f12756d5ba823d81b941f1b04da9d033a/sphinxcontrib_serializinghtml-2.0.0-py3-none-any.whl", hash = "sha256:6e2cb0eef194e10c27ec0023bfeb25badbbb5868244cf5bc5bdc04e4464bf331", size = 92072, upload-time = "2024-07-29T01:10:08.203Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "sympy"
|
||||
version = "1.14.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "mpmath" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/83/d3/803453b36afefb7c2bb238361cd4ae6125a569b4db67cd9e79846ba2d68c/sympy-1.14.0.tar.gz", hash = "sha256:d3d3fe8df1e5a0b42f0e7bdf50541697dbe7d23746e894990c030e2b05e72517", size = 7793921, upload-time = "2025-04-27T18:05:01.611Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/a2/09/77d55d46fd61b4a135c444fc97158ef34a095e5681d0a6c10b75bf356191/sympy-1.14.0-py3-none-any.whl", hash = "sha256:e091cc3e99d2141a0ba2847328f5479b05d94a6635cb96148ccb3f34671bd8f5", size = 6299353, upload-time = "2025-04-27T18:04:59.103Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tomli"
|
||||
version = "2.4.0"
|
||||
|
||||
Reference in New Issue
Block a user