"""Perceptual audio quality metrics, implemented in Rust.
This package provides two full-reference/no-reference perceptual quality
metrics:
- :func:`visqol` — ViSQOL (Virtual Speech Quality Objective Listener), a
full-reference DSP metric that compares a degraded signal against a clean
reference and predicts a MOS-LQO (mean opinion score — listening quality
objective) in the range 1.0–5.0.
- :class:`Scoreq` — SCOREQ (Speech Contrastive Regression for Quality
Assessment), a neural metric that either predicts a MOS from a single
signal (no-reference mode) or returns an embedding distance between a test
and reference signal (reference mode).
Both accept, for each signal, either an :class:`audio_samples.AudioSamples`
object or a path to an audio file (WAV or FLAC), in any combination. Path
arguments are decoded with ``audio_samples.read()`` — there is exactly one
file-reading implementation, the one ``audio_samples`` itself provides. Raw
sample arrays should be wrapped first with
``audio_samples.AudioSamples.new_mono`` / ``new_multi``, which is also where
the sample rate is attached.
Example:
>>> from audio_samples import sine_wave, white_noise
>>> from audio_samples_qoe import visqol
>>> ref = sine_wave(440, 5.0, 48000)
>>> deg = ref + white_noise(5.0, 48000, amplitude=0.01, seed=0)
>>> visqol(ref, deg) # doctest: +SKIP
4.4...
>>> visqol("reference.wav", "degraded.wav") # doctest: +SKIP
3.2...
"""
from os import PathLike
from typing import Literal, TypeAlias, Union
from audio_samples import AudioSamples
from audio_samples_qoe._native import VisqolError, __version__, _score
try:
from audio_samples_qoe._native import ScoreqError, _Scoreq
_HAS_SCOREQ = True
except ImportError: # built without the `scoreq` Cargo feature
_HAS_SCOREQ = False
VisqolMode: TypeAlias = Literal["audio", "speech"]
"""Scoring mode accepted by :func:`visqol`: ``"audio"`` (full-band, 32
gammatone bands up to Nyquist) or ``"speech"`` (21 bands capped at 8 kHz,
voice-activity-gated patch selection)."""
AudioInput: TypeAlias = Union[AudioSamples, str, "PathLike[str]"]
"""A signal argument to :func:`visqol` or :class:`Scoreq`: an in-memory
:class:`audio_samples.AudioSamples` or a path to a WAV/FLAC file. Forwarded
straight through to the native extension, which decodes paths via
``audio_samples.read()``."""
ScoreqDomain: TypeAlias = Literal["natural", "synthetic"]
"""Which pre-trained :class:`Scoreq` model family to use: ``"natural"``
(recorded human speech — codecs, VoIP, telephony, enhancement) or
``"synthetic"`` (machine-generated speech — TTS, voice conversion)."""
ScoreqMode: TypeAlias = Literal["nr", "ref"]
"""Operating mode for :class:`Scoreq`: ``"nr"`` (no-reference — predict a
MOS from the test signal alone) or ``"ref"`` (reference-based — Euclidean
distance between test and reference embeddings)."""
__all__ = [
"AudioInput",
"VisqolError",
"VisqolMode",
"__version__",
"visqol",
]
if _HAS_SCOREQ:
__all__ += ["Scoreq", "ScoreqDomain", "ScoreqError", "ScoreqMode"]
[docs]
def visqol(
reference: AudioInput,
degraded: AudioInput,
*,
mode: VisqolMode = "audio",
) -> float:
"""Compute the ViSQOL MOS-LQO score between a reference and a degraded signal.
ViSQOL (Virtual Speech Quality Objective Listener) is a full-reference
perceptual quality metric: it compares a degraded signal against a clean
reference and predicts a mean opinion score.
Each signal may be given as an :class:`audio_samples.AudioSamples`
object or as a path to a WAV or FLAC file (decoded with
``audio_samples.read()``), in any combination. Multi-channel signals are
mixed down to mono by channel averaging, and any sample type is accepted.
In ``"audio"`` mode both signals are resampled internally to the 48 kHz
rate the model is calibrated for, and the score is predicted by a
support vector regression over 32 gammatone bands. In ``"speech"`` mode
the pipeline matches the C++ reference: it runs at the reference
signal's native sample rate (the degraded signal is resampled to match)
with 21 gammatone bands capped at 8 kHz, reference patches are gated on
voice activity so silence is excluded, and the score comes from an
exponential NSIM→MOS fit (identical signals map to 5.0). Upstream
recommends 16 kHz input for speech mode.
The GIL is released while the score is computed, so scoring parallelises
across Python threads.
Args:
reference: Clean reference signal — an ``AudioSamples`` or a
WAV/FLAC file path.
degraded: Degraded signal to evaluate — an ``AudioSamples`` or a
WAV/FLAC file path.
mode: ``"audio"`` (default) for full-band audio, ``"speech"`` for
speech.
Returns:
The MOS-LQO score, a float in [1.0, 5.0]. Higher is better.
Identical signals score ~4.73 in audio mode and 5.0 in speech mode.
Raises:
TypeError: If an argument is neither an ``AudioSamples`` nor a path.
ValueError: If ``mode`` is not ``"audio"`` or ``"speech"``.
OSError: If a file path cannot be read or decoded.
VisqolError: If the metric pipeline fails — most commonly because a
signal is too short to extract a single analysis patch
(roughly 0.6 s in audio mode, 0.5 s in speech mode).
Example:
>>> from audio_samples import sine_wave
>>> from audio_samples_qoe import visqol
>>> ref = sine_wave(440, 5.0, 48000)
>>> score = visqol(ref, "degraded.wav") # doctest: +SKIP
"""
return _score(reference, degraded, mode)
if _HAS_SCOREQ:
[docs]
class Scoreq:
"""A loaded SCOREQ model — a neural, wav2vec2-based speech quality metric.
SCOREQ (Speech Contrastive Regression for Quality Assessment) is a
neural alternative to :func:`visqol`. It runs a trained model via ONNX
Runtime rather than a pure DSP pipeline, in one of two operating modes:
- ``mode="nr"`` (no-reference): :meth:`predict` returns a predicted MOS
from the test signal alone.
- ``mode="ref"`` (reference-based): :meth:`predict_ref` returns the
Euclidean distance between the test and reference embeddings —
smaller means closer to the reference's quality.
Loading a model is expensive: the first use of a given
``(domain, mode)`` pair downloads ~378 MB of weights from Zenodo into
``~/.cache/scoreq/``, and every construction loads an ONNX Runtime
session. Construct one ``Scoreq`` per ``(domain, mode)`` pair and reuse
it across calls rather than re-instantiating per signal.
Args:
domain: ``"natural"`` for recorded human speech (codecs, VoIP,
telephony, enhancement, restoration), or ``"synthetic"`` for
machine-generated speech (TTS, voice conversion, generative
models). Choosing the wrong domain yields meaningless scores.
mode: ``"nr"`` for no-reference MOS prediction, ``"ref"`` for
reference-based embedding distance.
Raises:
ValueError: If ``domain`` is not ``"natural"``/``"synthetic"`` or
``mode`` is not ``"nr"``/``"ref"``.
ScoreqError: If loading the model fails (e.g. the weight download
fails).
Example:
>>> from audio_samples import sine_wave
>>> from audio_samples_qoe import Scoreq
>>> model = Scoreq(domain="natural", mode="nr")
>>> test = sine_wave(220, 3.0, 16000)
>>> model.predict(test) # doctest: +SKIP
3.1...
"""
__slots__ = ("_inner",)
def __init__(self, domain: ScoreqDomain, mode: ScoreqMode = "nr") -> None:
self._inner = _Scoreq(domain, mode)
@property
def domain(self) -> ScoreqDomain:
"""The domain this model was loaded for."""
return self._inner.domain # type: ignore[return-value]
@property
def mode(self) -> ScoreqMode:
"""The mode this model was loaded for."""
return self._inner.mode # type: ignore[return-value]
[docs]
def predict(self, test: AudioInput) -> float:
"""Predict a no-reference MOS for `test`.
Requires a model loaded with ``mode="nr"``. The GIL is released
while the model runs.
Args:
test: Signal to score — an ``AudioSamples`` or a WAV/FLAC file
path. Resampled to 16 kHz internally if not already at that
rate; feed 16 kHz audio for exact agreement with the
upstream Python/torchaudio implementation.
Returns:
The predicted MOS.
Raises:
TypeError: If `test` is neither an ``AudioSamples`` nor a path.
OSError: If a file path cannot be read or decoded.
ScoreqError: If this model was loaded with ``mode="ref"``, or
the pipeline otherwise fails.
"""
return self._inner._predict(test)
[docs]
def predict_ref(self, test: AudioInput, reference: AudioInput) -> float:
"""Return the embedding distance between `test` and `reference`.
Requires a model loaded with ``mode="ref"``. Smaller distances mean
`test` is closer in quality to `reference`. The GIL is released
while the model runs.
Args:
test: Signal to evaluate — an ``AudioSamples`` or a WAV/FLAC
file path.
reference: Reference signal — an ``AudioSamples`` or a
WAV/FLAC file path.
Returns:
The Euclidean distance between the two embeddings.
Raises:
TypeError: If an argument is neither an ``AudioSamples`` nor a
path.
OSError: If a file path cannot be read or decoded.
ScoreqError: If this model was loaded with ``mode="nr"``, or
the pipeline otherwise fails.
"""
return self._inner._predict_ref(test, reference)