diff --git a/pyproject.toml b/pyproject.toml index fae3ade..e5c87d5 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -21,7 +21,6 @@ classifiers = [ "Programming Language :: Python :: 3.13", ] dependencies = [ - "pytuning", "numeral", "sounddevice", "scipy", diff --git a/pytheory/_statics.py b/pytheory/_statics.py index 291b854..0b820da 100644 --- a/pytheory/_statics.py +++ b/pytheory/_statics.py @@ -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, } diff --git a/pytheory/play.py b/pytheory/play.py index ad8484d..9b453d7 100644 --- a/pytheory/play.py +++ b/pytheory/play.py @@ -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 diff --git a/pytheory/tones.py b/pytheory/tones.py index 8df4dc8..e764562 100644 --- a/pytheory/tones.py +++ b/pytheory/tones.py @@ -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 diff --git a/uv.lock b/uv.lock index 8896faf..562b39f 100644 --- a/uv.lock +++ b/uv.lock @@ -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" @@ -711,7 +702,6 @@ version = "0.34.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"