Skip to main content

python-peass

Build Status PyPI version

This project was ported by Gemini 3.5 Flash from https://gitlab.inria.fr/bass-db/peass/-/tree/22c7fc4ef670f8bb6eea9ab4abea98323006b769/v2.0.1

A Python port of the PEASS v2.0.1 (Perceptual Evaluation methods for Audio Source Separation) toolkit [1].

Installation

For standard execution, you can install the package directly:

pip install "python-peass[numba]"

If you require high-speed execution (using optimized vector libraries like Intel MKL or Apple Accelerate), it is recommended to install NumPy and SciPy via Conda first, and then install the package:

conda install numpy scipy
pip install "python-peass[numba]"

Intel OpenMP conflict on Windows

If you install PyTorch as a PyPI wheel into a conda environment whose NumPy is built against MKL — the exact combination the section above recommends — the process can abort on the first call:

OMP: Error #15: Initializing libiomp5md.dll, but found libiomp5md.dll already initialized.
Fatal Python error: Aborted

conda's MKL ships Library/bin/libiomp5md.dll and the torch wheel ships its own copy in site-packages/torch/lib; whichever initializes second kills the interpreter. There is no Python traceback, and the abort surfaces inside whichever MKL routine happened to trigger the second initialization, so it reads like a numerical bug in the backend rather than a link-time collision.

Merely having torch installed is not enough to trigger it. Dispatch decides whether an input is a tensor by consulting sys.modules, so a process that never imports torch never loads the second runtime and NumPy-only usage is unaffected. (Dispatch used to import torch unconditionally, which made this abort reachable from pure-NumPy code that never asked for it; test_numpy_dispatch_does_not_import_torch guards against the regression.) You are exposed once torch is genuinely in the process — using the PyTorch backend, or importing torch yourself alongside an MKL-backed NumPy.

The real fix is to leave only one OpenMP runtime in the environment, e.g. by taking NumPy and PyTorch from the same toolchain instead of mixing conda MKL with a PyPI wheel. Failing that, set one of these before the first import (both runtimes read their configuration at load time, so setting it afterwards is too late):

workaround supported? cost
KMP_DUPLICATE_LIB_OK=TRUE no — Intel documents it as an unsafe escape hatch none measured; this is what the project's results are recorded under
MKL_THREADING_LAYER=SEQUENTIAL yes single-threads MKL; shifts the last digit of BLAS reductions (~1e-14 relative)

The test suite sets KMP_DUPLICATE_LIB_OK for you in the root conftest.py, so pytest works out of the box; an explicit setting in your environment overrides it. Application code gets no such help — set it yourself, or fix the environment.

Quick Start Examples

1. Perceptual Quality Score Evaluation

Evaluate estimated audio files saved on disk:

from peass import predict_perceptual_evaluation_scores

original_files = [
    "audio/target_source.wav",
    "audio/interference_1.wav",
    "audio/interference_2.wav"
]
estimate_file = "audio/estimated_target.wav"

scores = predict_perceptual_evaluation_scores(original_files, estimate_file)

print(f"Overall Perceptual Score (OPS):  {scores.overall_perceptual_score:.1f}/100")
print(f"Target Preservation Score (TPS): {scores.target_perceptual_score:.1f}/100")
print(f"Interference Rejection (IPS):    {scores.interference_perceptual_score:.1f}/100")
print(f"Artifact-free Score (APS):       {scores.artifact_perceptual_score:.1f}/100")

2. Score Evaluation with Waveform and File Expositions

To run the full perceptual scoring pipeline and simultaneously output the physically separated WAV files (True target, Target distortion, Interference, and Artifacts) to a specific output folder:

from peass import predict_perceptual_evaluation_scores, DecompositionConfiguration

original_files = [
    "audio/target_source.wav",
    "audio/interferer.wav"
]
estimate_file = "audio/estimated_target.wav"

# 1. Configure the output directory for file-writing
config = DecompositionConfiguration(destination_directory="./output_directory/")

# 2. Set return_decomposition=True to expose the physical waveforms and file paths
scores = predict_perceptual_evaluation_scores(
    original_files,
    estimate_file,
    configuration=config,
    return_decomposition=True
)

# 3. Print overall scores
print(f"Overall Perceptual Score (OPS): {scores.overall_perceptual_score:.1f}/100")

# 4. Access the file paths of the generated WAV files on disk
print(f"True target file saved at:      {scores.decomposition_files.true_target}")
print(f"Interference file saved at:     {scores.decomposition_files.interference}")
print(f"Artifacts file saved at:        {scores.decomposition_files.artifacts}")

# 5. Read the raw NumPy arrays directly from memory
target_distortion_array = scores.decomposition_waveforms.target_distortion

3. Independent Subband Least-Squares Decomposition

You can run the auditory Gammatone/least-squares decomposition engine independently to obtain the isolated physical sub-components:

import numpy as np
from peass import decompose_distortion_components

# In-memory arrays
target_array = np.random.randn(16000, 1)
noise_array = np.random.randn(16000, 1)
estimate_array = target_array + 0.05 * noise_array

# Run subband least-squares decomposer
result = decompose_distortion_components(
    source_files=[target_array, noise_array],
    estimate_file=estimate_array,
    sampling_frequency_hz=16000.0
)

waveforms = result.waveforms
true_target, target_distortion, interference, artifacts = (
    waveforms.true_target,
    waveforms.target_distortion,
    waveforms.interference,
    waveforms.artifacts
)

Scientific Highlights

Traditional evaluation metrics rely purely on linear energy ratios [1]. However, human hearing relies on non-linear auditory transduction, temporal masking, and cognitive thresholds [2]. This package replaces traditional energy ratio metrics (SDR, SIR, SAR) with perceptually motivated objective scores— OPS, TPS, IPS, and APS—which align closely with subjective human listening evaluations [1].

peass executes a multi-stage cognitive simulation pipeline to assess separation quality:

  1. Subband Least-Squares Decomposition: Signals are divided into subbands using a complex-valued Hohmann Gammatone Filterbank [1, 3]. Overlapping temporal frames are projected onto estimated subspaces to isolate physical target distortion, interference, and artifact components [1].
  2. Inner Hair Cell Transduction: Approximates the shearing limits of physical hair bundles via half-wave rectification and first-order 1 kHz membrane-limit lowpass filters [1, 2].
  3. Auditory Nerve Adaptation: Models physiological forward masking and metabolic neural depletion via five cascaded stages of non-linear feedback loops [2].
  4. Perceptual Assimilation: Models cognitive threshold masking where noise below a target reference threshold is partially assimilated or masked [2].
  5. Score Prediction: Feeds weighted similarity percentiles into a multi-criteria trained sigmoidal neural network to output scores scaled from 0 to 100 [1].

Test Suite & CI/CD

Installation for Development & Testing

The validation suite implements rigorous numerical, physical, and integration checks. You will need to pip install -r requirements.txt to set up.

You should also install the package in editable mode along with its dependencies:

pip install -e .
pytest -n auto --cov=peass --cov-report=json

Alternatively, if you want to run tests without installing the package, run pytest as a Python module:

python -m pytest -n auto --cov=peass --cov-report=json

Regression Verification

We test our output waveforms directly against the original .wav reference waveforms generated by the official MATLAB PEASS toolbox (located in references/peass_master_22c7fc4e/v2.0.1/example/). Python's outputs must achieve a cross-correlation coefficient exceeding $0.95$ with the MATLAB reference to pass.

NumPy vs PyTorch backends

The package dispatches to a NumPy backend for array/file inputs and a PyTorch backend for tensor inputs (selected automatically by input type). The NumPy backend is the numerical reference and matches the MATLAB toolbox very closely (cross-correlation > 0.999 with the default full-order resampling; lower DecompositionConfiguration.resample_filter_half_length_factor from 10 toward 3 to trade a little fidelity for ~25% faster decomposition). The PyTorch backend is designed to be fully differentiable (usable inside a training loop): it replaces the hard non-linearities and IIR recursions of the reference with smooth, backprop-safe surrogates (softplus, FIR-truncated filters). As a result its outputs match the NumPy backend by high correlation rather than to floating-point precision.

NumPy backend performance and numerical reproducibility

The NumPy backend carries a few single-threaded optimizations (no threads or subprocesses are spawned; the speedup comes from SIMD and from removing per-call overhead). Worth ~1.2x end-to-end on the reference example:

change speedup contribution bitwise effect
Numba polyphase resampler replacing SciPy upfirdn ~1.13x reassociated, see below
LAPACK ?posv called directly instead of scipy.linalg.solve(assume_a='pos') 11x on that call bit-identical
sources pre-padded once instead of a per-frame np.vstack bit-identical
hoisted the conjugate transpose that was built twice per frame bit-identical
one block-diagonal matmul instead of one per source ~1 ULP (1.9e-15)

None of these is an approximation: every one computes the same quantity in exact arithmetic. Two of them reassociate floating-point sums, and floating-point addition is not associative, so output is not bitwise identical to older releases.

The observed difference is ~2e-11 absolute (~1e-10 of full scale). It looks larger in relative terms for artifacts (~1e-9) purely because that component is a difference of comparable quantities, so cancellation shrinks the denominator; true_target shows 2.5e-15. Correlation against the previous output is 1.0 to all 15 digits, and the four quality features (and hence OPS/TPS/IPS/APS) agree to 10 significant figures.

To remove the dominant term, disable the Numba resampler before the first call:

import peass.backend_numpy.gammatone as gammatone
gammatone.USE_NUMBA_RESAMPLER = False

That falls back to the SciPy path (itself bit-exact against the pre-optimization resampler) and costs the ~1.13x. Measured against the pre-optimization output, this takes the difference from 2.3e-11 down to 1.9e-15 — not to zero, because the block-diagonal projection matmul in perform_least_squares_projection also reassociates, at about 1 ULP. If you need exactly zero difference, revert that hunk too; it is worth ~1.03x on its own.

Do not instead edit fastmath=False onto the resampler kernels: that is bit-exact but measures ~5% slower than SciPy, so it loses both ways, and it requires clearing Numba's on-disk cache to take effect.

Note that this backend was never bitwise reproducible across machines: any BLAS build or CPU that reassociates differently is subject to the same ~1e-10 amplification, because the per-frame least-squares systems are ill-conditioned (regularized at only 1e-15). Treat ~1e-10 as the backend's reproducibility floor rather than expecting exactness.

Parallel Execution

To run the test suite across multiple CPU cores using pytest-xdist, execute:

pytest -n auto

Known deviations from the MATLAB reference

segmentation_factor is accepted but ignored

DecompositionConfiguration.segmentation_factor exists for API compatibility with the MATLAB options.segmentationFactor, but nothing in this package reads it. Setting it to anything other than 1 silently has no effect: the signal is always decomposed in one piece.

MATLAB takes a separate code path when segmentationFactor > 1 (extractDistortionComponents.m, the branch at ~lines 107-110 dispatching to aux_segmentAndDecompose at ~lines 270-386): it cuts the signal into that many segments, decomposes each independently with the shade-in/out suppressed on interior edges, and overlap-adds the four components under a periodic Hann window, dividing by the accumulated window. That path was never ported. It is tracked in TODO.md.

+0.257% level offset against the MATLAB gold WAVs

The port's decomposition output is a flat, frequency-independent factor 1.0025651 (+0.0223 dB) larger in amplitude than the MATLAB v2.0.1 gold WAVs in tests/resources/matlab_reference/, on all four output components. This is understood, deliberate, and not fixed.

The cause is resampler filter normalization. scipy.signal.firwin defaults to scale=True, which normalizes the Kaiser-windowed sinc to exactly unit DC gain (peass/backend_numpy/gammatone.py:707, peass/backend_torch/utils.py:85). MATLAB's resample filter is not DC-normalized; its raw DC gain is 0.9993253. The decomposition performs four resamples per signal path (16k→24k, decimate by Ndec, interpolate by Ndec, 24k→16k), so MATLAB accumulates:

step MATLAB filter DC gain
16k → 24k and 24k → 16k 0.999394194 each
decimate by Ndec and interpolate by Ndec 0.999325320 each
product 0.997441484
reciprocal 1.00256514

against a measured 1.0025651. Because the filter half-length is 10*pqmax and the cutoff is 1/(2*pqmax), the tap set is scale-invariant in pqmax, so the factor is identical for every Ndec from 14 to 409 — which is why the discrepancy is perfectly frequency-flat rather than a filter-shape difference.

Confirmed by a literal line-by-line MATLAB transcription: flipping only that one normalization moves the ratio from 1.0025651 to 1.0000001, and the remaining 5.4e-4 residual is pure PCM16 quantization noise (0.575 LSB rms measured, 0.577 expected for a sum of four independent 16-bit roundings, white spectrum).

Note also that the gold WAVs are byte-identical to the precomputed outputs shipped in the PEASS distribution's example/ folder, and the v2.0 and v2.0.1 shipped outputs are identical to each other — the authors never regenerated them for 2.0.1. They are stale artifacts, not a fresh run of the v2.0.1 source.

Why it is not fixed: a resampler should preserve DC gain. Matching the reference would mean deliberately introducing a 0.26% (0.022 dB) attenuation to chase a reference that is itself arguably wrong.

The offset originally went unflagged because the regression test asserted on cross-correlation, which is scale-invariant, plus a 0.5 < ratio < 2.0 sanity band that 0.26% passes trivially. tests/regression/test_matlab_regression.py now asserts the RMS ratio equals 1.0025651 to within 1e-3 instead, so the known offset is locked rather than merely tolerated: any new gain regression, in either direction, breaks the test. The worst measured deviation from the constant is 1.5e-5, on the artifacts component — the quietest of the four, and so the one whose gold WAV sits closest to the PCM16 quantization floor.


References

  1. V. Emiya, E. Vincent, N. Harlander, and V. Hohmann, "Subjective and objective quality assessment of audio source separation", IEEE Transactions on Audio, Speech, and Language Processing, 19(7):2046–2057, 2011.
  2. R. Huber and B. Kollmeier, "PEMO-Q — A New Method for Objective Audio Quality Assessment Using a Model of Auditory Perception", IEEE Transactions on Audio, Speech, and Language Processing, 14(6):1902–1911, 2006.
  3. V. Hohmann, "Frequency analysis and synthesis using a Gammatone filterbank", Acustica/Acta Acustica, 88(3):433–442, 2002.

Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

python_peass-2.0.1.5.tar.gz (75.9 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

python_peass-2.0.1.5-py3-none-any.whl (78.8 kB view details)

Uploaded Python 3

File details

Details for the file python_peass-2.0.1.5.tar.gz.

File metadata

  • Download URL: python_peass-2.0.1.5.tar.gz
  • Upload date:
  • Size: 75.9 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/7.0.0 CPython/3.13.14

File hashes

Hashes for python_peass-2.0.1.5.tar.gz
Algorithm Hash digest
SHA256 2a39997c3863adb7522e4d38d5150ef659871b6639b262bca1ec581d9c998e14
MD5 8abffe538f4d4e00e22182c5b24626b0
BLAKE2b-256 8b1a477abf761ccdea89ef2d44c67b92fd77aa87613a4681734a9d8c607071c5

See more details on using hashes here.

Provenance

The following attestation bundles were made for python_peass-2.0.1.5.tar.gz:

Publisher: release.yml on averykhoo/python-peass

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file python_peass-2.0.1.5-py3-none-any.whl.

File metadata

  • Download URL: python_peass-2.0.1.5-py3-none-any.whl
  • Upload date:
  • Size: 78.8 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/7.0.0 CPython/3.13.14

File hashes

Hashes for python_peass-2.0.1.5-py3-none-any.whl
Algorithm Hash digest
SHA256 bbffb87b645b5259087811b476c403e07f2bfb12bf0bab1a7f016fd5459d17b8
MD5 4351a4f6896911a0452f32bbbafbe338
BLAKE2b-256 aeaec2b2d1fd512db18dcacab5d1c2c66cd0e7c1c791388bb10561e908fa2442

See more details on using hashes here.

Provenance

The following attestation bundles were made for python_peass-2.0.1.5-py3-none-any.whl:

Publisher: release.yml on averykhoo/python-peass

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page