Metadata-Version: 2.4
Name: multiafx
Version: 0.1.0
Summary: Multi-library audio effects registry with a unified chain-application API
Author-email: Barry Cheng <im31132@gmail.com>
License: MIT
Project-URL: Homepage, https://github.com/barry-mir/multiafx
Project-URL: Repository, https://github.com/barry-mir/multiafx
Project-URL: Issues, https://github.com/barry-mir/multiafx/issues
Keywords: audio,dsp,effects,signal-processing,augmentation
Classifier: Development Status :: 3 - Alpha
Classifier: Intended Audience :: Developers
Classifier: Intended Audience :: Science/Research
Classifier: License :: OSI Approved :: MIT License
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Topic :: Multimedia :: Sound/Audio
Classifier: Topic :: Scientific/Engineering
Requires-Python: >=3.10
Description-Content-Type: text/markdown
License-File: LICENSE
Requires-Dist: numpy<3,>=1.24
Requires-Dist: scipy>=1.10
Requires-Dist: pyyaml>=6.0
Requires-Dist: audiomentations>=0.43.1
Requires-Dist: sox>=1.4
Requires-Dist: torch>=2.0
Requires-Dist: torchaudio>=2.0
Requires-Dist: librosa>=0.10
Requires-Dist: pyloudnorm>=0.1.1
Requires-Dist: loudness>=0.2
Provides-Extra: dev
Requires-Dist: pytest>=7.0; extra == "dev"
Requires-Dist: pytest-cov; extra == "dev"
Requires-Dist: soundfile>=0.12; extra == "dev"
Requires-Dist: build>=1.0; extra == "dev"
Requires-Dist: twine>=4.0; extra == "dev"
Dynamic: license-file

<p align="center">
  <img src="https://raw.githubusercontent.com/barry-mir/multiafx/main/assets/icon.png" alt="MultiAFX" width="200">
</p>

Unified registry of **85 audio effects** across **7 libraries** (audiomentations, sox, torchaudio, scipy, librosa, pyloudnorm, numpy), with a clean chain-application API inspired by [pedalboard](https://github.com/spotify/pedalboard).

- One call signature for every effect: `(audio, sr, **params) -> np.ndarray`
- JSON / YAML / pickle serialization of effect chains
- Pure data format: chains are lists of dicts, portable across languages
- Parameter ranges attached to every effect for random chain generation

---

## Install

```bash
pip install multiafx
```

Requires Python ≥ 3.10.

---

## Quickstart

```python
import multiafx
import soundfile as sf

# Build a chain inline
chain = multiafx.FXChain([
    {"effect": "sox_highpass", "params": {"frequency": 80.0, "width_q": 0.707}},
    {"effect": "sox_compand",  "params": {"attack_time": 0.005,
                                          "decay_time": 0.1,
                                          "soft_knee_db": 6.0}},
    {"effect": "sox_reverb",   "params": {"reverberance": 40.0,
                                          "high_freq_damping": 50.0,
                                          "room_scale": 60.0,
                                          "stereo_depth": 80.0,
                                          "pre_delay": 20.0,
                                          "wet_gain": -6.0}},
])

# Load audio as (channels, samples) float32
audio, sr = sf.read("input.wav", always_2d=True)
audio = audio.T.astype("float32")

# Apply — pedalboard style
processed = chain(audio, sr)

sf.write("output.wav", processed.T, sr)
```

---

## Loading chains from files

```python
chain = multiafx.FXChain.load("preset.json")     # auto-detect by extension
chain = multiafx.FXChain.load("preset.yaml")
chain = multiafx.FXChain.load("preset.pkl")

# or explicitly
chain = multiafx.FXChain.from_json("preset.json")
chain = multiafx.FXChain.from_yaml("preset.yaml")
chain = multiafx.FXChain.from_pickle("preset.pkl")
```

**JSON format** (`preset.json`):

```json
[
  {"effect": "sox_highpass", "params": {"frequency": 80.0, "width_q": 0.707}},
  {"effect": "sox_compand",  "params": {"attack_time": 0.005,
                                         "decay_time": 0.1,
                                         "soft_knee_db": 6.0}}
]
```

**YAML format** (`preset.yaml`):

```yaml
- effect: sox_highpass
  params: {frequency: 80.0, width_q: 0.707}
- effect: sox_compand
  params:
    attack_time: 0.005
    decay_time: 0.1
    soft_knee_db: 6.0
```

Saving is symmetric: `chain.to_json(path)`, `chain.to_yaml(path)`, `chain.to_pickle(path)`.

---

## Building chains programmatically

```python
chain = multiafx.FXChain()
chain.add("sox_highpass", frequency=100.0, width_q=0.707)
chain.add("sox_compand", attack_time=0.005, decay_time=0.1, soft_knee_db=4.0)
chain.add("sox_reverb", reverberance=30.0, high_freq_damping=50.0, room_scale=40.0,
          stereo_depth=100.0, pre_delay=15.0, wet_gain=-8.0)

# List-like mutation
chain.append({"effect": "sox_gain", "params": {"gain_db": 2.0}})
chain.insert(0, {"effect": "sox_highpass", "params": {"frequency": 40.0, "width_q": 0.7}})
del chain[-1]
chain.pop()
len(chain)
for step in chain: ...
```

### Default parameters

Every effect has sensible defaults, so you can omit `params` entirely or pass
only the parameters you care about:

```python
# All defaults — just effect names
chain = multiafx.FXChain([
    {"effect": "sox_highpass"},
    {"effect": "sox_compand"},
    {"effect": "sox_reverb"},
])

# .add() with no kwargs also works
chain = multiafx.FXChain()
chain.add("sox_highpass")
chain.add("sox_compand")

# Override only the parameters you want; the rest fall back to defaults
chain = multiafx.FXChain([
    {"effect": "sox_highpass", "params": {"frequency": 200.0}},  # width_q defaults
    {"effect": "sox_compand"},                                    # all defaults
])
```

---

## Random chain generation

```python
import multiafx

chain = multiafx.generate_random_chain(
    num_fx=8,                                   # or (1, 8) for a random range
    seed=42,
    exclude_categories=["PITCH", "TIME"],       # skip content-altering effects
    exclude_effects=["sox_reverb"],
    no_consecutive_same_category=True,          # no EQ → EQ back-to-back
)
```

---

## Registry introspection

```python
import multiafx

multiafx.registry.list_effects()                  # all 85 names
multiafx.registry.list_effects(library="sox")     # filter by library
multiafx.registry.list_effects(category="EQ")     # filter by macro category
multiafx.registry.libraries()                     # 7 libraries
multiafx.registry.categories()                    # 12 MacroCategory enums

eff = multiafx.registry.get("sox_compand")
eff.name                # "sox_compand"
eff.library             # "sox"
eff.macro_category      # <MacroCategory.DYNAMICS: 'DYNAMICS'>
eff.param_ranges        # {"attack_time": ParamRange(0.001, 0.1), ...}
```

---

## Effect Categories


| Category   | Count | Example effects                                                    |
| ---------- | ----- | ------------------------------------------------------------------ |
| EQ         | 37    | `sox_highpass`, `am_peaking_filter`, `ta_equalizer_biquad`         |
| DYNAMICS   | 11    | `sox_compand`, `sox_gain`, `am_normalize`                          |
| DISTORTION | 5     | `sox_overdrive`, `am_tanh_distortion`, `am_bit_crush`              |
| REVERB     | 2     | `sox_reverb`, `am_air_absorption`                                  |
| DELAY      | 2     | `sox_echo`, `sox_echos`                                            |
| MODULATION | 4     | `sox_chorus`, `sox_flanger`, `sox_phaser`, `sox_tremolo`           |
| PITCH      | 3     | `sox_pitch`, `lib_pitch_shift`, `am_pitch_shift`                   |
| TIME       | 4     | `sox_tempo`, `sox_speed`, `lib_time_stretch`, `am_time_stretch`    |
| SPECTRAL   | 5     | `sox_deemph`, `ta_riaa_biquad`, `lib_preemphasis`                  |
| STEREO     | 4     | `sox_oops`, `sox_earwax`, `npy_lr_pan`, `npy_stereo_widener`       |
| NOISE      | 2     | `am_add_gaussian_noise`, `am_add_color_noise`                      |
| OTHER      | 4     | `am_polarity_inversion`, `am_reverse`, `sox_dcshift`, `ta_dcshift` |


The full effect reference is in [docs/effects.md](docs/effects.md).

---

## Audio format

- All effects take and return `np.ndarray` with shape `(channels, samples)` and dtype `float32`.
- Sample rate is a second positional argument.
- Applying an empty chain returns the input clipped to `[-1, 1]`.

---

## Development

```bash
git clone https://github.com/barry-mir/multiafx
cd multiafx
conda create -n multiafx python=3.10 -y
conda activate multiafx
pip install -e ".[dev]"
pytest tests/
```

The test suite runs every one of the 85 effects at min / mid / max of each parameter range. **No test is expected to be skipped for any reason other than "effect has no parameters"**. If you add an effect, add nothing else — the parameterized tests pick it up automatically.

---

## License

MIT.
