<!-- canonical: https://jmrplens.github.io/phonometry/es/guides/objective-intelligibility/ -->
Source: https://jmrplens.github.io/phonometry/es/guides/objective-intelligibility/

**STOI** y **ESTOI** son medidas de inteligibilidad objetiva basadas en
correlación. Cada una compara una referencia limpia con una versión degradada o
procesada de la misma habla y devuelve un escalar con relación monótona con la
fracción de palabras que un oyente entendería: `1` cuando la señal degradada
coincide con la limpia y cerca de `0` para ruido incorrelado. Trabajan
directamente sobre las dos formas de onda, lo que las convierte en la vara de
medir habitual para el habla con **ponderación tiempo-frecuencia**: reducción de
ruido, separación de fuentes y procesado por máscara binaria, donde separar el
habla limpia de su distorsión no es inmediato.

:::note
**STOI/ESTOI frente a STI y SII.** STOI y ESTOI necesitan la referencia limpia
*y* la señal degradada, y valoran el procesado entre ambas. El
[índice de transmisión del habla](/phonometry/es/guides/speech-transmission/)
valora un canal de transmisión a partir de su respuesta al impulso, y el
[índice de inteligibilidad del habla](/phonometry/es/guides/speech-intelligibility/)
predice la audibilidad a partir de espectros de habla, ruido y umbral. Usa
STOI/ESTOI cuando dispones de un par antes/después.
:::

:::caution
`pystoi` (una reimplementación pública del MATLAB de los autores) se usa solo
como contraste externo en la batería de pruebas. phonometry reimplementa ambas
medidas a partir de los dos artículos y nunca importa `pystoi` en tiempo de
ejecución; ambas coinciden muy por debajo de `1e-3` sobre entradas comunes.
:::

## 1. El procesado inicial común

Ambas medidas ejecutan el mismo procesado antes de divergir (Taal et al. 2011,
sección II): remuestreo a **10 kHz**; una transformada de corto plazo de 256
muestras (25,6 ms) con ventana de Hann y solape del 50 %, rellenada con ceros
hasta 512 puntos; eliminación de las tramas cuya energía *limpia* está más de
**40 dB** por debajo de la trama limpia más intensa; una agrupación por **15
bandas de tercio de octava** de las magnitudes DFT desde un centro mínimo de
150 Hz; y segmentos de análisis de **384 ms** (30 tramas) como unidad de
comparación. La frecuencia de muestreo de las entradas es libre, la biblioteca
remuestrea internamente.

```python
from phonometry import stoi

d = stoi(clean, degraded, fs)          # STOI, remuestreado a 10 kHz por dentro
print(round(d.value, 3))               # un escalar en torno a [0, 1]
print(stoi(clean, clean, fs).value)    # 1.0  (una señal contra sí misma)
```

## 2. STOI: correlación de envolventes con recorte

Para cada banda y segmento STOI normaliza la envolvente degradada respecto a la
limpia, la recorta en un límite inferior de relación señal-distorsión
($\beta = -15$ dB) para que una unidad completamente degradada no pueda arrastrar
la puntuación por debajo de su suelo, y toma la correlación muestral de ambas
envolventes (Taal et al. 2011, Ecs. 3-6):

$$
d_{j,m} = \frac{\sum_n (x_j(n) - \bar{x}_j)\,(\bar{y}_j(n) - \bar{\bar{y}}_j)}
               {\lVert x_j - \bar{x}_j \rVert\,\lVert \bar{y}_j - \bar{\bar{y}}_j \rVert}.
$$

El índice es el promedio de esas correlaciones intermedias sobre todas las
bandas $j$ y segmentos $m$ (Ec. 6). Como la normalización divide una ganancia por
segmento, STOI es **invariante al nivel de reproducción** de la señal degradada,
y a mayor SNR corresponde una puntuación monótonamente mayor.

```python

from phonometry import stoi

fs = 10000
rng = np.random.default_rng(1)
clean = rng.standard_normal(3 * fs)
for snr_db in (-10, 0, 10, 20):
    g = 10.0 ** (-snr_db / 20.0)
    noisy = clean + g * rng.standard_normal(clean.size)
    print(snr_db, round(stoi(clean, noisy, fs).value, 3))
```

El `STOIResult` incluye la correlación media por banda (`band_scores`) y las
puntuaciones por segmento (`segment_scores`) que promedian a `value`, y su
`.plot()` dibuja la correlación intermedia por banda. Merece la pena mirar esa
vista por bandas antes de citar el índice: muestra *dónde* muerde la
degradación, algo que el número único no puede indicar.

<details>
<summary>Mostrar el código de esta figura</summary>

```python

from scipy.signal import butter, lfilter
from phonometry import stoi

# Material tipo habla: ruido limitado en banda con envolvente silábica de
# 3,5 Hz, en un enmascarador plano con SNR de 0 dB.
fs = 10000
rng = np.random.default_rng(11)
t = np.arange(4 * fs) / fs
b, a = butter(2, [200 / (fs / 2), 4000 / (fs / 2)], btype="band")
carrier = lfilter(b, a, rng.standard_normal(t.size))
clean = carrier * (0.15 + 0.85 * np.abs(np.sin(2 * np.pi * 3.5 * t)) ** 2)
masker = rng.standard_normal(clean.size)
gain = np.sqrt(np.mean(clean ** 2)) / np.sqrt(np.mean(masker ** 2))
res = stoi(clean, clean + gain * masker, fs)
print(round(res.value, 3))    # 0.727

# En una línea: la correlación intermedia por banda que hay tras el índice.
res.plot(language="es")
plt.show()

# A mano, reproduciendo lo que dibuja STOIResult.plot():
pos = np.arange(res.band_scores.size)
fig, ax = plt.subplots()
ax.bar(pos, res.band_scores)
ax.set_xticks(pos)
ax.set_xticklabels([f"{f:.0f}" for f in res.band_frequencies], rotation=45, ha="right")
ax.set_xlabel("Banda de tercio de octava [Hz]")
ax.set_ylabel("Correlación intermedia media")
ax.set_title(f"STOI = {res.value:.3f}")
plt.show()
```

</details>

El enmascarador plano castiga sobre todo a las bandas graves, porque ahí el
espectro del habla cae y el del enmascarador no, y la correlación se recupera
en las bandas consonánticas por encima de 1 kHz. Un perfil por bandas plano, o
que se hunde solo en una banda, apunta a algo distinto del ruido de banda
ancha: un filtro de rechazo, una resonancia o un artefacto de procesado del
realce que se está evaluando.

## 3. ESTOI: correlación espectral para enmascaradores modulados

ESTOI (`extended=True`) sustituye la correlación por banda independiente por una
conjunta espectro-temporal. Dentro de cada segmento de 384 ms normaliza en media
y varianza las **filas** del espectrograma (las envolventes de banda) y luego
sus **columnas** (los espectros por trama), y promedia la correlación de las
columnas normalizadas (Jensen y Taal 2016, Ecs. 4-8). Hacer competir las
columnas significa que un enmascarador que deja silencios, donde el habla limpia
es audible por instantes, recibe crédito por el habla vislumbrada ahí, algo que
el promedio por banda de STOI apenas capta.

<details>
<summary>Mostrar el código de esta figura</summary>

```python

from phonometry import stoi

fs = 10000
rng = np.random.default_rng(20)
t = np.arange(3 * fs) / fs
# Una señal limpia tipo habla: tonos con modulación de amplitud.
clean = np.zeros_like(t)
for f0 in (200.0, 400.0, 700.0, 1100.0, 1800.0, 2600.0):
    depth = 0.5 * (1.0 + np.sin(2 * np.pi * rng.uniform(2.0, 6.0) * t + rng.uniform(0.0, 2 * np.pi)))
    clean += depth * np.sin(2 * np.pi * f0 * t + rng.uniform(0.0, 2 * np.pi))
p_clean = np.sqrt(np.mean(clean ** 2))

base = rng.standard_normal(clean.size)
gate = 0.5 * (1.0 + np.sign(np.sin(2 * np.pi * 5.0 * t)))   # puerta on/off a 5 Hz
modulated = base * (0.05 + 0.95 * gate)
snrs = np.arange(-15.0, 20.1, 5.0)

def curve(masker, extended):
    p_m = np.sqrt(np.mean(masker ** 2))
    return [stoi(clean, clean + (p_clean / (p_m * 10.0 ** (s / 20.0))) * masker,
                 fs, extended=extended).value for s in snrs]

fig, (a, b) = plt.subplots(1, 2, figsize=(12, 5), sharey=True)
for ax, extended, title in ((a, False, "STOI"), (b, True, "ESTOI")):
    ax.plot(snrs, curve(base, extended), "o-", label="Enmascarador estacionario")
    ax.plot(snrs, curve(modulated, extended), "s--", label="Enmascarador modulado")
    ax.set_title(title); ax.set_xlabel("SNR [dB]"); ax.set_ylim(0, 1); ax.legend()
a.set_ylabel("Índice de inteligibilidad")
plt.show()
```

</details>

## 4. Qué medida usar y cuándo

| | STOI | ESTOI |
| :--- | :--- | :--- |
| Magnitud intermedia | Correlación de envolventes por banda, con recorte | Correlación espectral normalizada por filas y por columnas |
| Invariancia al nivel | Sí (normalización por segmento) | Sí (normalización por fila y por columna) |
| Enmascaradores estacionarios | Bien validada | Bien validada |
| Enmascaradores modulados, hablantes competidores | Infravalora el beneficio de los vistazos | Lo sigue |
| Coste | Menor | Algo mayor |

Para ruido aditivo estacionario las dos medidas son intercambiables y STOI es la
opción ligera por defecto; cuando la interferencia fluctúa en el tiempo, o al
comparar procesadores que remodelan el habla en tiempo y frecuencia, prefiere
ESTOI.

Ambas responden a una pregunta más estrecha que las dos métricas normalizadas
de habla de la biblioteca, y la elección entre las tres familias es en realidad
una elección de qué se puede medir. STOI y ESTOI necesitan una *referencia
limpia* y la señal degradada, así que pertenecen al trabajo de procesado:
reducción de ruido, separación de fuentes, códecs, algoritmos de audífonos. El
[STI](/phonometry/es/guides/speech-transmission/) (IEC 60268-16) no necesita
referencia limpia, solo el canal, y es lo que valora una sala o un sistema de
megafonía. El [SII](/phonometry/es/guides/speech-intelligibility/)
(ANSI S3.5-1997) no necesita ninguna señal, solo espectros, y es lo que predice
la audibilidad para un oyente con un umbral de audición dado. Cuando hay más de
una disponible, responden a preguntas distintas en lugar de confirmarse entre
sí: un procesador puede subir el STOI mientras la sala mantiene un STI pobre.

## Qué cubre esta guía

**Cubierto.** Taal et al. 2011 (STOI) y Jensen y Taal 2016 (ESTOI), las dos
medidas basadas en correlación que implementa `stoi()`: el procesado inicial
común de remuestreo a 10 kHz, tramas Hann de 256 muestras, 15 bandas de tercio
de octava, segmentos de 384 ms y la eliminación de tramas silenciosas a 40 dB
(Taal et al. 2011, Ecs. 1-4); la correlación de envolventes por banda y el
índice promediado (Ecs. 5-6); y, con `extended=True`, la normalización por
filas y columnas del espectrograma y el índice de correlación espectral
(Jensen y Taal 2016, Ecs. 4-8).

**No cubierto.** Taal et al. 2011 también ajusta una función logística que
convierte el índice $d$ en un porcentaje de palabras entendidas previsto,
calibrada sobre corpus concretos de pruebas de escucha. `stoi()` devuelve
solo $d$, como `STOIResult.value`, no ese porcentaje: la API no tiene salida
de porcentaje ni coeficientes ajustados. El artículo de ICASSP de 2010 se
cita solo como la versión corta de congreso del mismo algoritmo, sin añadir
nada implementado más allá de las ecuaciones de 2011/2016 anteriores.

## Véase también

- [Índice de transmisión del habla](/phonometry/es/guides/speech-transmission/):
  valora un canal de transmisión a partir de su respuesta al impulso o de una
  grabación STIPA.
- [Índice de inteligibilidad del habla](/phonometry/es/guides/speech-intelligibility/):
  predice la inteligibilidad a partir de espectros de habla, ruido y umbral.
- [Bancos de filtros](/phonometry/es/guides/filter-banks/): las bandas de tercio
  de octava en las que el procesado inicial agrupa la DFT.
- Referencia de la API: [`hearing.objective_intelligibility`](/phonometry/es/reference/api/speech/objective-intelligibility/).
