Metrics and alignment¶
kudio.core.evaluator
¶
Speech-enhancement quality metrics: PESQ, STOI, SDR, and the SNR family.
The SNR family (snr, si_sdr, segmental_snr,
sdi) is pure numpy. The rest are optional dependencies:
pip install kudio[eval] # mir_eval + pystoi: SDR, STOI
pip install kudio[pesq] # pesq: PESQ -- built from source, needs a C compiler
Which PESQ. ITU-T P.862 scores narrowband speech and P.862.2 wideband, and
each can be reported raw or mapped to MOS-LQO. They are different numbers for
one file: a raw narrowband 2.0 is a MOS-LQO of about 1.6. Papers that just
say "PESQ" do not agree on which they mean -- some quote the raw narrowband
score the ITU reference binary prints, and much VoiceBank-DEMAND work reports
wideband MOS-LQO from the pesq package -- so read how a paper scored before
comparing with it. pesq takes the bandwidth and the scale as arguments
rather than picking one silently.
Classes:
| Name | Description |
|---|---|
AudioEvaluate |
Evaluate enhanced audio against clean references. |
Functions:
| Name | Description |
|---|---|
snr |
Signal-to-noise ratio (dB) of deg relative to ref (pure numpy). |
si_sdr |
Scale-invariant SDR (dB) — immune to overall gain differences. |
segmental_snr |
Frame-averaged (segmental) SNR in dB, clipped to [-10, 35] per frame. |
sdi |
Speech distortion index: |
pesq |
PESQ of deg against ref (ITU-T P.862 / P.862.2). |
score |
Score deg against ref: |
eval_metrics |
Score a single (reference, degraded) pair with every available metric. |
Attributes:
| Name | Type | Description |
|---|---|---|
MAX_LENGTH_MISMATCH |
How much of the longer signal a comparison may leave unmeasured, as a |
|
METRICS |
the names |
MAX_LENGTH_MISMATCH
module-attribute
¶
How much of the longer signal a comparison may leave unmeasured, as a fraction of it. Every metric here lines the pair up at sample zero and cuts both to the common length, so the tail of the longer one is never looked at; past half of it the score describes the overlap and not the file whose name it is reported under. Half is also well clear of the slop a real pipeline produces -- a window or two lost at the tail, a resampler's rounding -- which is the case truncation is there to absorb in the first place. Raise it to compare a pair that really is that lopsided.
AudioEvaluate
¶
AudioEvaluate(
clean_list: Sequence,
noisy_list: Sequence,
*,
pesq_mode: str = "auto",
pesq_scale: str = "lqo",
)
Evaluate enhanced audio against clean references.
>>> audio_eval = AudioEvaluate(clean_list, noisy_list)
>>> audio_eval.eval('ddae', enhanced_list)
>>> df = audio_eval.get_df() # pandas.DataFrame, with a Baseline row
>>> per = audio_eval.per_file() # one row per method x file
File lists must be aligned: clean_list[i] / noisy_list[i] /
enhanced_list[i] refer to the same utterance. Files are read with
kudio.file_load -- float, mono, and any format soundfile reads,
NIST SPHERE included.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
pesq_scale
|
(pesq_mode,)
|
as in A file that cannot be scored -- unreadable, at another rate, too far from its reference in length -- gets nan in every column and a warning naming it, and the rest of the list is still scored.
|
'lqo'
|
Methods:
| Name | Description |
|---|---|
eval |
Score enhanced_list with every available metric. |
get_df |
Return the collected scores (plus a Baseline row) as a DataFrame. |
per_file |
Every file's scores as a DataFrame, one row per method and file. |
sdr_score |
|
stoi_score |
|
pesq_score |
|
pesq_score_exe |
Legacy PESQ via the ITU |
Attributes:
| Name | Type | Description |
|---|---|---|
rows |
List[dict]
|
one dict per method and file -- |
Source code in kudio/core/evaluator.py
rows
instance-attribute
¶
one dict per method and file -- method, clean, noisy,
file (the one scored) and a value per metric, NaN where it
failed. The noisy files themselves are scored once, as 'Baseline'.
eval
¶
Score enhanced_list with every available metric.
Source code in kudio/core/evaluator.py
get_df
¶
Return the collected scores (plus a Baseline row) as a DataFrame.
Before anything has been scored the DataFrame is empty, with the columns it will have, rather than a list.
Source code in kudio/core/evaluator.py
per_file
¶
Every file's scores as a DataFrame, one row per method and file.
The 'Baseline' rows score the noisy files; rows holds the
method rows as plain dicts, for use without pandas.
Source code in kudio/core/evaluator.py
sdr_score
¶
stoi_score
¶
pesq_score
¶
pesq_score_exe
¶
Legacy PESQ via the ITU pesq.exe binary on PATH (Windows).
The binary prints the raw narrowband P.862 score, the same scale as
pesq_mode='nb', pesq_scale='raw'.
Source code in kudio/core/evaluator.py
snr
¶
Signal-to-noise ratio (dB) of deg relative to ref (pure numpy).
Source code in kudio/core/evaluator.py
si_sdr
¶
Scale-invariant SDR (dB) — immune to overall gain differences.
Source code in kudio/core/evaluator.py
segmental_snr
¶
segmental_snr(
ref: ndarray,
deg: ndarray,
frame_length: int = 512,
hop_length: int = 256,
) -> float
Frame-averaged (segmental) SNR in dB, clipped to [-10, 35] per frame.
Source code in kudio/core/evaluator.py
sdi
¶
Speech distortion index: sum((ref - deg)**2) / sum(ref**2).
0 is a perfect copy and 1 is as much error as there is speech; lower is
better. It is snr on a linear scale, 10 ** (-snr / 10): the
error energy of the whole output against the clean signal. The
speech-distortion index of the noise-reduction literature (Chen, Benesty,
Huang and Doclo, 2006) is taken differently, on the clean speech passed
through the filter alone, so the two are not the same number.
Source code in kudio/core/evaluator.py
pesq
¶
PESQ of deg against ref (ITU-T P.862 / P.862.2).
>>> kudio.pesq(clean, enhanced, 16000) # WB MOS-LQO
>>> kudio.pesq(clean, enhanced, 16000, mode='nb', scale='raw') # raw P.862
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
mode
|
str
|
|
'auto'
|
scale
|
str
|
|
'lqo'
|
Raises:
| Type | Description |
|---|---|
ValueError
|
off 8 / 16 kHz, or for a combination the standards do not define (wideband at 8 kHz, a raw wideband score). |
DependencyError
|
without the The pair is checked for length like every other metric here: a mismatch
warns, and past Runs on the |
Source code in kudio/core/evaluator.py
score
¶
score(
ref: ndarray,
deg: ndarray,
sr: int,
metrics: Optional[Sequence[str]] = None,
*,
pesq_mode: str = "auto",
pesq_scale: str = "lqo",
) -> Dict[str, float]
Score deg against ref: {metric: value}.
>>> kudio.score(clean, enhanced, 16000, ['pesq', 'stoi', 'sdi'],
... pesq_mode='nb', pesq_scale='raw')
{'pesq': 2.41, 'stoi': 0.874, 'sdi': 0.105}
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
metrics
|
Optional[Sequence[str]]
|
names from |
None
|
pesq_scale
|
(pesq_mode,)
|
as mode / scale of The two signals are compared sample for sample over their common length,
as the rest of kudio's reference metrics are -- see |
'lqo'
|
Source code in kudio/core/evaluator.py
check_metrics_install
¶
Report availability of the optional metric back-ends.
Returns (pesq_ok, stoi_ok, sdr_ok). A missing back-end is logged once
per process, with how to install it; nothing is installed automatically.
Source code in kudio/core/evaluator.py
eval_metrics
¶
eval_metrics(
rate: int,
ref: ndarray,
deg: ndarray,
*,
pesq_mode: str = "auto",
pesq_scale: str = "lqo",
) -> Tuple[
Optional[float], Optional[float], Optional[float]
]
Score a single (reference, degraded) pair with every available metric.
Returns (pesq, stoi, sdr) -- always three values, whatever is
installed. A metric comes back as None when its back-end is not
installed, when it cannot run on this pair -- PESQ off 8 and 16 kHz, STOI
on less than about 0.4 s of speech -- or when it fails on it, which is
logged.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
pesq_mode
|
str
|
|
'auto'
|
pesq_scale
|
str
|
|
'lqo'
|
Source code in kudio/core/evaluator.py
kudio.core.align
¶
Line two recordings up before measuring one against the other.
The SNR family in kudio -- kudio.si_sdr, kudio.snr,
kudio.segmental_snr, kudio.sdi -- compares sample i of one
signal with sample i of the other. Every reference metric trims to the
shorter of the two; the SNR family also assumes the rest lines up, which is
right for a process that returns what it was given and wrong for anything that
introduces a delay. PESQ runs its own time alignment and STOI shrugs off a few
milliseconds, so the damage below is the SNR family's. A trim large enough to matter
is warned about, and past kudio.core.evaluator.MAX_LENGTH_MISMATCH
refused -- but a delay that leaves the lengths equal is invisible to that
check, and this is the module for it.
How wrong is worth seeing. A clip scored against itself, shifted:
========= ========== shift SI-SDR ========= ========== 0 +145 dB 1 sample +10.6 dB 1 ms -10.1 dB 10 ms -11.1 dB ========= ==========
One sample costs 134 dB. A millisecond scores worse than not processing at all, and nothing in the number says "these are misaligned" rather than "your denoiser destroyed the signal" -- which is the wrong conclusion to hand somebody about work that may be fine.
>>> shift = kudio.find_delay(clean, processed, sr)
>>> print(shift)
lags by 128 samples (8.0 ms), correlation 0.998
>>> a, b = kudio.align(clean, processed, sr)
>>> kudio.si_sdr(a, b)
18.4
The confidence is half of it. Cross-correlation always has a maximum, so
it always returns a delay -- including for two recordings that have nothing to
do with each other. Alignment.correlation is what separates "these are
the same take, 8 ms apart" from "these are different recordings and the number
means nothing", and align refuses rather than lining up two unrelated
signals and letting a metric put a number on the result.
Polarity is reported, not silently fixed. An inverted copy correlates at -1: it is the same take, and something in the chain flipped it. That is a finding, so it is named rather than corrected on the way past.
Classes:
| Name | Description |
|---|---|
Alignment |
Where two recordings sit relative to each other. |
Functions:
| Name | Description |
|---|---|
find_delay |
Measure how far deg sits after ref, to the nearest sample. |
align |
Return ref and deg trimmed to the region they share, lined up. |
Attributes:
| Name | Type | Description |
|---|---|---|
MIN_CORRELATION |
Below this, two signals are not the same recording and the delay between |
MIN_CORRELATION
module-attribute
¶
Below this, two signals are not the same recording and the delay between them does not mean anything. Chosen low: real pairs -- a denoised take against its source, a resampled copy -- sit well above 0.8 even when the processing is aggressive, and the point of the threshold is to catch "somebody picked the wrong file", not to grade the processing.
Alignment
dataclass
¶
Where two recordings sit relative to each other.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
delay
|
int
|
samples by which deg sits after ref. Negative means it starts first. Zero means they already line up. |
required |
correlation
|
float
|
normalised cross-correlation at that delay, in
|
required |
overlap
|
int
|
samples the two share once the delay is taken out. |
required |
Methods:
| Name | Description |
|---|---|
confident |
Are these plausibly the same recording? minimum defaults to |
apply |
Trim both signals to the region they share at this delay. |
Attributes:
| Name | Type | Description |
|---|---|---|
inverted |
bool
|
Is one of the two polarity-flipped relative to the other? |
strength |
float
|
How alike they are, ignoring polarity: |
confident
¶
Are these plausibly the same recording? minimum defaults to
MIN_CORRELATION as it is when called, so raising that raises
it everywhere.
Source code in kudio/core/align.py
apply
¶
Trim both signals to the region they share at this delay.
Separate from find_delay so a delay measured once -- on a loud
passage, say -- can be applied to a whole set of files that came
through the same chain.
Source code in kudio/core/align.py
find_delay
¶
find_delay(
ref: ndarray,
deg: ndarray,
sr: int,
*,
max_seconds: Optional[float] = None,
) -> Alignment
Measure how far deg sits after ref, to the nearest sample.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
max_seconds
|
Optional[float]
|
bound the search to this much delay either way. Worth setting when you know the answer is small: an unbounded search over a long file can find a better-correlating match half a minute away in something as repetitive as speech, and be perfectly right about a passage nobody meant. |
None
|
Returns:
| Type | Description |
|---|---|
Alignment
|
an Both signals must already be at sr; loudness and pitch survive a resample and a sample offset does not, so aligning two rates would be measuring a delay in units that do not exist yet. |
Source code in kudio/core/align.py
align
¶
align(
ref: ndarray,
deg: ndarray,
sr: int,
*,
max_seconds: Optional[float] = None,
min_correlation: Optional[float] = None,
) -> Tuple[ndarray, ndarray]
Return ref and deg trimmed to the region they share, lined up.
The pair comes back ready for kudio.si_sdr and friends.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
min_correlation
|
Optional[float]
|
refuse below this ( |
None
|
Raises:
| Type | Description |
|---|---|
FeatureError
|