From bbb91f8610c82f864bc6fc4b386c504cbf5edc48 Mon Sep 17 00:00:00 2001 From: Brad Barnett <127794626+bdbarnett@users.noreply.github.com> Date: Mon, 9 Nov 2026 02:16:45 -0600 Subject: [PATCH] audioeffects: each effect loads the first time it's used `import audioeffects` imported all 45 effect modules, so a program that uses a reverb paid for every effect's code in RAM, and a firmware that freezes the package had to freeze all of it, about 400 KB of bytecode on an ESP32. Each class is now imported on first use, through the package's __getattr__. `from audioeffects import Reverb`, `audioeffects.Reverb`, `create()` and `ALL` work as before. A firmware can freeze only the effects it plays plus `__init__` and `_component`. One difference: on MicroPython, `from audioeffects import *` now brings in only the effects already used, since MicroPython's star import reads the module's names rather than `__all__`. Checked: tools/scoped_tests.py contract, tools/validate_api.py (45 effects), tests/parity/effects_library_smoke.py (45 classes, 325 patches, 0 failures) and flake8. On CPython and the unix MicroPython port, importing the package loads only _component, and importing Reverb and Chorus loads just those two. --- lib/audioeffects/__init__.py | 135 ++++++++++++++++++----------------- 1 file changed, 71 insertions(+), 64 deletions(-) diff --git a/lib/audioeffects/__init__.py b/lib/audioeffects/__init__.py index dccd714..00b41d9 100644 --- a/lib/audioeffects/__init__.py +++ b/lib/audioeffects/__init__.py @@ -47,55 +47,68 @@ def channel_count(): return _FORMAT[1] -from .compressor import Compressor -from .limiter import Limiter -from .expander import Expander -from .noisegate import NoiseGate -from .deesser import DeEsser -from .transientshaper import TransientShaper -from .multibandcompressor import MultibandCompressor -from .parametriceq import ParametricEQ -from .graphiceq import GraphicEQ -from .lowpass import LowPass -from .highpass import HighPass -from .bandpass import BandPass -from .notch import Notch -from .ladderfilter import LadderFilter -from .combfilter import CombFilter -from .dynamiceq import DynamicEQ -from .reverb import Reverb -from .convolutionreverb import ConvolutionReverb -from .digitaldelay import DigitalDelay -from .slapbackdelay import SlapbackDelay -from .tapedelay import TapeDelay -from .analogdelay import AnalogDelay -from .pingpongdelay import PingPongDelay -from .multitapdelay import MultiTapDelay -from .chorus import Chorus -from .phaser import Phaser -from .tremolo import Tremolo -from .autopan import AutoPan -from .vibrato import Vibrato -from .ringmod import RingMod -from .flanger import Flanger -from .overdrive import Overdrive -from .distortion import Distortion -from .bitcrusher import Bitcrusher -from .cabinetsim import CabinetSim -from .fuzz import Fuzz -from .saturation import Saturation -from .exciter import Exciter -from .pitchshifter import PitchShifter -from .harmonizer import Harmonizer -from .octaver import Octaver -from .stereowidener import StereoWidener -from .rack import Rack -from .shimmerhall import ShimmerHall -from .airspace import AirSpace - - -#: The package's whole surface, grouped as the modules above are. Every -#: name here is re-exported; nothing else in a submodule is public. +# Each effect's class and the module that holds it. A class is imported the +# first time it is used, so a firmware can freeze only the effects it plays +# and a program pays only for the ones it touches; `from audioeffects import +# Reverb` and `audioeffects.Reverb` work as before. +_MODULES = { + "Compressor": "compressor", + "Limiter": "limiter", + "Expander": "expander", + "NoiseGate": "noisegate", + "DeEsser": "deesser", + "TransientShaper": "transientshaper", + "MultibandCompressor": "multibandcompressor", + "ParametricEQ": "parametriceq", + "GraphicEQ": "graphiceq", + "LowPass": "lowpass", + "HighPass": "highpass", + "BandPass": "bandpass", + "Notch": "notch", + "LadderFilter": "ladderfilter", + "CombFilter": "combfilter", + "DynamicEQ": "dynamiceq", + "Reverb": "reverb", + "ConvolutionReverb": "convolutionreverb", + "DigitalDelay": "digitaldelay", + "SlapbackDelay": "slapbackdelay", + "TapeDelay": "tapedelay", + "AnalogDelay": "analogdelay", + "PingPongDelay": "pingpongdelay", + "MultiTapDelay": "multitapdelay", + "Chorus": "chorus", + "Phaser": "phaser", + "Tremolo": "tremolo", + "AutoPan": "autopan", + "Vibrato": "vibrato", + "RingMod": "ringmod", + "Flanger": "flanger", + "Overdrive": "overdrive", + "Distortion": "distortion", + "Bitcrusher": "bitcrusher", + "CabinetSim": "cabinetsim", + "Fuzz": "fuzz", + "Saturation": "saturation", + "Exciter": "exciter", + "PitchShifter": "pitchshifter", + "Harmonizer": "harmonizer", + "Octaver": "octaver", + "StereoWidener": "stereowidener", + "Rack": "rack", + "ShimmerHall": "shimmerhall", + "AirSpace": "airspace", +} + + +def __getattr__(name): + module = _MODULES.get(name) + if module is None: + raise AttributeError(name) + cls = getattr(__import__("audioeffects." + module, None, None, [name]), name) + globals()[name] = cls + return cls + + __all__ = [ "configure", "sample_rate", "channel_count", "create", "ALL", # dynamics @@ -141,18 +154,12 @@ def create(name, source, sample_rate, transport=None, **options): selected class's ``create`` method owns the actual construction, so a consumer does not need to know which module contains it. """ - for exported in __all__: - cls = globals().get(exported) - if _is_provider(cls) and cls.NAME == name: - return cls.create(source, sample_rate, transport=transport, - **options) - raise ImportError("audioeffects has no %s" % name) - - -# Stable provider names, not implementation module names. This is computed -# from the already-imported public classes and never constructs an effect. -ALL = tuple(sorted( - globals()[name].NAME - for name in __all__ - if _is_provider(globals().get(name)) -)) + if name not in _MODULES: + raise ImportError("audioeffects has no %s" % name) + cls = __getattr__(name) + return cls.create(source, sample_rate, transport=transport, **options) + + +# Stable provider names, not implementation module names. Each class's NAME +# is its class name, so this needs no effect imported. +ALL = tuple(sorted(_MODULES))