Source code for audio_samples_qoe

"""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)