Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions CITATION.cff
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,8 @@ authors:
given-names: Yhonatan
orcid: "https://orcid.org/0009-0009-1156-9087"
affiliation: "Ben-Gurion University of the Negev"
version: 0.2.1
date-released: "2026-07-12"
version: 0.2.2
date-released: "2026-07-30"
license: MIT
repository-code: "https://github.com/Yhonatangayer/shroom"
url: "https://github.com/Yhonatangayer/shroom"
Expand Down
33 changes: 32 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ A Python library for simulating room acoustics using Spherical Harmonics (Ambiso
* **Spatial Signals**: Unified handling of Time, Frequency, Space, and Spherical Harmonics (SH) domains.
* **Processors**: Modular processing chain including:
* `ArrayDecoder`: Simulates spherical microphone arrays.
* `ASMEncoder`: Encodes microphone signals to Ambisonics (ASM).
* `ASMEncoder`: Encodes microphone signals to Ambisonics (ASM, optionally spectrally-equalized — SE-ASM).
* `BinauralDecoder`: Decodes Ambisonics to Binaural audio using HRTFs.
* **Rotation**: Efficient rotation of sound fields and HRTFs using Wigner-D matrices, or via space domain grid rotation.
* **Visualization**: 2D and 3D plotting of room geometry, sources, and receiver orientation.
Expand Down Expand Up @@ -133,6 +133,17 @@ chain = ProcessorChain([
binaural_output = chain.process(room.compute_amb())
```

### Spectrally-Equalized ASM (SE-ASM)

```python
from shroom import ASM

# Rescales each SH channel so its linear spectral magnitude stays at 0 dB across
# the band, instead of collapsing above the array's spatial-aliasing frequency.
se_asm = ASM(sh_order=1, array=array, fs=fs, duration=duration, spectrally_equalized=True)
cnm = se_asm.cnm # (M, (N+1)^2, F)
```

### Optimized Low-Order Rendering (MagLS)

```python
Expand Down Expand Up @@ -173,6 +184,26 @@ If you use shroom in your research, please cite our paper:
```
## Changelog

### 0.2.2

**New: spectrally-equalized ASM (SE-ASM).** `ASM` gained a `spectrally_equalized`
flag (default `False`, so existing code is unchanged). When enabled, each SH channel
of the ASM solution is rescaled by `1 / xi[nm, f]`, where
`xi[nm, f] = ‖cnm[:, nm, f]^H V[:, :, f]‖ / ‖Y[:, nm]‖` is its linear spectral
magnitude. This keeps every channel at 0 dB across the whole band instead of letting
it collapse above the array's spatial-aliasing frequency, at the cost of a larger
complex MSE. The weights are real and positive, so the phase of the ASM filters is
untouched.

```python
ASM(sh_order=1, array=array, fs=fs, duration=duration, spectrally_equalized=True)
```

Also added: `calculate_se_asm_coefficients` and `linear_spectral_magnitude` in
`shroom.encoders.asm`, and the `benchmarks/se_asm_convergence.py` benchmark comparing
ASM and SE-ASM (per-channel MSE/LSE and binaural magnitude error). No API changes to
existing functions.

### 0.2.1

Maintenance release — no functional or API changes. Adds the JOSS paper
Expand Down
2 changes: 2 additions & 0 deletions benchmarks/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ error decays, validating the accuracy of the encoders against high-order referen
| Script | What it validates |
|--------|-------------------|
| `asm_convergence.py` | Ambisonics Signal Matching (ASM) encoder error vs. SH order. |
| `se_asm_convergence.py` | Spectrally-equalized ASM (SE-ASM) vs. plain ASM: per-channel MSE/LSE and binaural magnitude error. |
| `bsm_convergence.py` | Binaural Signal Matching (BSM) encoder error vs. SH order (against a MATLAB reference). |
| `aa_magls_convergence.py` | Array-aware MagLS binaural magnitude error vs. SH order. |

Expand All @@ -27,6 +28,7 @@ benchmarks and the examples.

```bash
python benchmarks/asm_convergence.py
python benchmarks/se_asm_convergence.py
python benchmarks/bsm_convergence.py
python benchmarks/aa_magls_convergence.py
```
Expand Down
156 changes: 156 additions & 0 deletions benchmarks/se_asm_convergence.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,156 @@
"""Spectrally-equalized ASM (SE-ASM) vs. plain ASM.

Validates the ``spectrally_equalized`` flag of :class:`shroom.encoders.asm.ASM`:
SE-ASM rescales every SH channel so that its linear spectral magnitude stays at
0 dB across the whole band, where plain ASM collapses towards zero above the
spatial-aliasing frequency of the array. The price is a larger complex MSE.

Three figures are produced (per-channel complex MSE, per-channel LSE, and
binaural magnitude MSE with a MagLS HRTF), all written to ``benchmarks/figures/``.
"""
import os

import numpy as np

from shroom.geometry.sampling import sphereicalGrid
from shroom.paths import DEFAULT_HRTF_PATH
from shroom.utils.file_utils import load_file
from shroom.acoustics.spherical_array import SphericalArray
from shroom.acoustics.hrtf_processing import magls_hrtf
from shroom.utils.grid_utils import from_fibonacci_grid
from shroom.encoders.asm import ASM
from shroom_dev.errors import asm_bin_magnitude_mse_error, asm_mse_error, linear_spectral_error
from shroom_dev.plot import loglog_plot

FIGURES_DIR = os.path.join(os.path.dirname(os.path.abspath(__file__)), "figures")

SH_LABELS = ["(0,0)", "(1,-1)", "(1,0)", "(1,1)"]
SH_COLORS = ["#1f77b4", "#ff7f0e", "#2ca02c", "#d62728"]
ENCODER_STYLES = {"ASM": "-", "SE-ASM": "--"}

SHOW = True


def _per_channel_curves(errors_by_encoder):
"""Flatten {encoder: (nm, F)} into loglog_plot dicts keyed by label."""
errors, styles, colors = {}, {}, {}
for encoder, err in errors_by_encoder.items():
for i, nm in enumerate(SH_LABELS):
label = f"{encoder} {nm}"
errors[label] = err[i, ...]
styles[label] = ENCODER_STYLES[encoder]
colors[label] = SH_COLORS[i]
return errors, styles, colors


def main():
os.makedirs(FIGURES_DIR, exist_ok=True)

# 1. Setup
fs = 48000
duration = 512 / 48000
n_fft = int(duration * fs)
freqs = np.fft.fftfreq(n_fft, 1 / fs)
pos_freqs = np.fft.rfftfreq(n_fft, 1 / fs)

hrtf = load_file(DEFAULT_HRTF_PATH)
hrtf.resample(fs)
hrtf.zero_pad(n_fft)

source_grid = from_fibonacci_grid(240)

hrtf.toFreq()
hrtf.toSH(30)
hrtf.toSpace(source_grid)
space_hrtf = hrtf.copy()

az = np.deg2rad(np.array([-90, -45, 0, 45, 90]))
co = np.deg2rad(np.array([90, 90 + 18, 90 - 18, 90 + 18, 90]))
mic_grid = sphereicalGrid(az=az, co=co)

array = SphericalArray(
fs=fs,
duration=duration,
r_sphere=0.08,
r_mics=0.08 * np.ones((mic_grid.n_points,)),
source_grid=source_grid,
mics_grid=mic_grid,
sphere_type="rigid",
sh_order_for_sm_calc=14,
convert_to_time=False,
)

Y = array.grid.Y(1)

# 2. Encoders: plain ASM and its spectrally-equalized counterpart
asm = ASM(sh_order=1, array=array, fs=fs, duration=duration)
se_asm = ASM(
sh_order=1,
array=array,
fs=fs,
duration=duration,
spectrally_equalized=True,
)
cnm = {"ASM": asm.cnm.data, "SE-ASM": se_asm.cnm.data}

# 3. Errors
mse = {name: asm_mse_error(c, array.data, Y, freqs) for name, c in cnm.items()}
lse = {name: linear_spectral_error(c, array.data, Y, freqs) for name, c in cnm.items()}

hrtf_magls = magls_hrtf(hrtf=space_hrtf.copy(), sh_order=1, cutoff_over_freq=1200)
hrtf_magls.toFreq()
bin_mse = {
name: asm_bin_magnitude_mse_error(
hrtf_magls.data, c, array.data, space_hrtf.data, freqs
)
for name, c in cnm.items()
}

# 4. Plots
errors, styles, colors = _per_channel_curves(mse)
loglog_plot(
freqs=pos_freqs,
title="ASM vs SE-ASM | Complex MSE per SH Channel",
errors=errors,
styles=styles,
colors=colors,
figsize=(7, 4),
ylim=(-30, 20),
save_path=os.path.join(FIGURES_DIR, "se_asm_mse.png"),
show=SHOW,
)

errors, styles, colors = _per_channel_curves(lse)
loglog_plot(
freqs=pos_freqs,
title="ASM vs SE-ASM | Linear Spectral Error per SH Channel",
errors=errors,
styles=styles,
colors=colors,
figsize=(7, 4),
ylim=(-30, 20),
save_path=os.path.join(FIGURES_DIR, "se_asm_lse.png"),
show=SHOW,
)

loglog_plot(
freqs=pos_freqs,
title="ASM vs SE-ASM | Binaural Magnitude MSE (MagLS HRTF)",
errors={
f"{name} {ear}": err[i, :]
for name, err in bin_mse.items()
for i, ear in enumerate(["left", "right"])
},
styles={
f"{name} {ear}": ENCODER_STYLES[name]
for name in bin_mse
for ear in ["left", "right"]
},
figsize=(7, 4),
save_path=os.path.join(FIGURES_DIR, "se_asm_binaural_magnitude_mse.png"),
show=SHOW,
)


if __name__ == "__main__":
main()
3 changes: 2 additions & 1 deletion examples/binaural_using_asm.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
HEAD_POS = [2.0, 2.0, 1.5]
SOURCE_POS = [4.0, 4.0, 1.5]
HEAD_ROTATION = [0, 0, 0]
SPECTRALY_EQUALIZATION = True


def main():
Expand Down Expand Up @@ -65,7 +66,7 @@ def main():
array_time_sh.toSH(1)

# 3. Setup ASM
asm = ASM(sh_order=1, array=array, fs=FS, duration=DURATION)
asm = ASM(sh_order=1, array=array, fs=FS, duration=DURATION, spectrally_equalized=SPECTRALY_EQUALIZATION)

# 4. Setup HRTF
print("Loading HRTF...")
Expand Down
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"

[project]
name = "pyshroom"
version = "0.2.1"
version = "0.2.2"
description = "Spherical Harmonics Room (shroom): a Python library for room acoustics simulation, Ambisonics processing, and spherical microphone array modelling."
readme = "README.md"
license = "MIT"
Expand Down
Loading
Loading