<!-- canonical: https://jmrplens.github.io/phonometry/es/perception/speech/objective-intelligibility/ -->
Source: https://jmrplens.github.io/phonometry/es/perception/speech/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 no correlacionado. 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/perception/speech/speech-transmission/)
valora un canal de transmisión a partir de su respuesta al impulso, y el
[índice de inteligibilidad del
habla](/phonometry/es/perception/speech/speech-intelligibility/)
predice la audibilidad a partir de espectros de habla, ruido y umbral. Usa
STOI/ESTOI cuando dispongas 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 con diferencias 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.

Ambas medidas están definidas para **habla continua**, y el procesado inicial es
donde muerde esa suposición: la regla de los 40 dB se apoya en el rango dinámico
de la señal *limpia*, así que existe para impedir que las pausas entre palabras
dominen la correlación. Una señal sin pausas, ruido estacionario, un tono
sostenido o música sin huecos, no pierde nada por ella y el índice pierde su
sentido; una grabación que es casi toda silencio lo pierde casi todo y la llamada
da error en lugar de devolver un número. Tienen que sobrevivir al menos unos
**0,4 s de habla activa**.

```python

from phonometry import stoi

fs = 10000
rng = np.random.default_rng(11)
n = 4 * fs
t = np.arange(n) / fs

# Material tipo habla: ruido de banda limitada bajo una envolvente silábica de
# 3,5 Hz, para que la regla de tramas silenciosas tenga pausas sobre las que
# actuar.
spectrum = np.fft.rfft(rng.standard_normal(n))
spectrum[(np.fft.rfftfreq(n, 1 / fs) < 200) | (np.fft.rfftfreq(n, 1 / fs) > 4000)] = 0
carrier = np.fft.irfft(spectrum, n)
clean = carrier * (0.15 + 0.85 * np.abs(np.sin(2 * np.pi * 3.5 * t)) ** 2)

noise = rng.standard_normal(n)
noise *= np.sqrt(np.mean(clean ** 2) / np.mean(noise ** 2))   # SNR de 0 dB
degraded = clean + noise

d = stoi(clean, degraded, fs)               # STOI, remuestreado a 10 kHz por dentro
print(round(d.value, 3))                    # 0.708
print(round(stoi(clean, clean, fs).value, 6))   # 1.0  (una señal contra sí misma)
```

Todo hasta los segmentos de 384 ms es común; las dos medidas solo se separan
en la correlación. El diagrama despliega la cadena con los números del
ejemplo del enmascarador plano.

### Conseguir un par válido

Los dos argumentos no son «dos señales» sin más. Tienen que ser la **misma
locución**, con la **misma longitud** y **alineadas con precisión de muestra**, y
esto último es el fallo que más puntuaciones cuesta en la práctica, porque nada
en el resultado lo distingue de una degradación genuina. Ambas medidas
correlacionan envolventes de corto plazo dentro de segmentos de 384 ms, así que
un retardo constante las descorrelaciona directamente. Sobre el par de arriba,
desplazar la señal degradada 20 ms lleva la puntuación de 0,708 a 0,329; sobre un
par *perfecto*, esos mismos 20 ms llevan el 1,0 a 0,459 y 50 ms lo vuelven
negativo. Media sílaba basta para convertir un buen realce en uno malo sobre el
papel.

La biblioteca impone la igualdad de longitud y nada más: no alinea, así que es
quien llama el que compensa antes cualquier latencia algorítmica. Correlaciona
las envolventes de banda ancha (o antepón un chirp corto de marca), desplaza y
recorta luego ambas al tramo común. Si las dos señales pasaron por relojes de
reproducción y de captura distintos, vigila además la *deriva*: un desplazamiento
fijo no puede corregir una diferencia de frecuencia de muestreo, así que
remuestrea al reloj de referencia o graba a través de un solo equipo. La
comprobación que merece la pena hacer una vez en cualquier cadena nueva es el par
de líneas de arriba: `stoi(x, x, fs).value` vale 1.0, y
`stoi(x, np.roll(x, k), fs)` se desploma deprisa con `k`.

Lo que *no* hace falta igualar es el nivel: la normalización por segmento hace a
ambas medidas invariantes a la ganancia de la señal degradada. Lo que hay que
acertar es la alineación; la ganancia, no.

Tres condiciones dan error en lugar de devolver un valor, y conviene
reconocerlas: longitudes distintas, muestras no finitas y que sobrevivan menos de
30 tramas de corto plazo a la eliminación de tramas silenciosas, que son los
~0,4 s de habla activa mencionados arriba.

### Capturar la señal degradada a través de un equipo real

Todo lo anterior supone que la señal degradada se produjo numéricamente. En
cuanto se captura acústicamente, un audífono sobre un oído artificial en una caja
de ensayo, unos auriculares o un bucle de altavoz y micrófono, cuatro cosas pasan
a ser responsabilidad de quien mide:

- **La referencia limpia sigue siendo el archivo original.** Volver a grabarla
  por la misma cadena elimina justo la distorsión que se está evaluando, y
  además rompe la selección de tramas silenciosas, que se deriva solo de la señal
  limpia.
- **La cadena de captura se puntúa como degradación.** La respuesta del altavoz,
  el ruido de fondo de la caja de ensayo y el ruido propio del micrófono entran
  todos en el índice medido, así que mide el bucle una vez con el equipo puenteado
  y declara ese número como el suelo de la comparación.
- **Reproduce al nivel de trabajo del equipo.** El *índice* es invariante al
  nivel; el *equipo* no lo es: un compresor o un reductor de ruido se comportan
  de otra manera a otro nivel de entrada.
- **Repite.** Una sola captura arrastra la variabilidad entre repeticiones del
  camino acústico; declara la dispersión sobre varias capturas, no un solo
  número.

## 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}.
$$

donde $x_j(n)$ es la envolvente temporal de corto plazo limpia de la banda $j$ en
la trama $n$ dentro del segmento $m$ de 30 tramas; $\bar{y}_j(n)$ es la
envolvente *degradada* después de la normalización de ganancia por segmento y del
recorte a $\beta = -15$ dB, de modo que ese acento marca el procesado y no una
media; una barra sobre una magnitud completa ($\bar{x}_j$, $\bar{\bar{y}}_j$) es
su media sobre el segmento; y $\lVert\cdot\rVert$ es la norma euclídea sobre esas
mismas 30 tramas. Así que $d_{j,m}$ es una correlación de Pearson corriente en
$[-1, 1]$.

El índice es el promedio de esas correlaciones intermedias sobre todas las
bandas $j$ y segmentos $m$ (Ec. 6), y las dos marginales de ese promedio son lo
que exponen `band_scores` y `segment_scores`. 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

# La señal `clean` tipo habla de la sección 1, degradada con cuatro relaciones
# señal-ruido. Conviene que la referencia siga siendo tipo habla: una referencia
# gaussiana plana no tiene pausas, así que la regla de los 40 dB no elimina nada
# y el índice pierde su sentido.
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))
# -10 0.083 | 0 0.484 | 10 0.858 | 20 0.956
```

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 de banda limitada 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 perfil por bandas es una de las marginales del promedio; la otra es el tiempo,
y responde a una pregunta que la vista por bandas no puede responder:

*Dos degradaciones de la misma referencia, y el escalar las ordena al revés: el
corte de señal puntúa **0,874** frente al **0,772** del enmascarador
estacionario, porque una catástrofe corta se diluye en el promedio y una
degradación leve en todas partes no. La marginal temporal las separa de
inmediato: una serie es plana y la otra es perfecta salvo en un puñado de
segmentos. Ese es el diagnóstico al que hay que recurrir
cuando un STOI bajo no tiene causa evidente: un transitorio, un corte o un
artefacto de procesado aparecen aquí y en ningún otro sitio. Las tramas
sombreadas de arriba son las que descarta la regla de los 40 dB, que es lo que
significa en concreto que «la entrada tiene que tener dinámica de habla».*

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

```python
# El mismo material de la sección 1 pero con silencio entre las palabras, para
# que algunas tramas caigan de verdad 40 dB por debajo de la más intensa.
fig_rng = np.random.default_rng(11)
freq = np.fft.rfftfreq(n, 1 / fs)
spec = np.fft.rfft(fig_rng.standard_normal(n))
spec[(freq < 200) | (freq > 4000)] = 0
env = np.abs(np.sin(2 * np.pi * 3.5 * t)) ** 2
ref = np.fft.irfft(spec, n) * np.where(env < 0.08, 0.0, env)

masker = fig_rng.standard_normal(n)
masker *= np.sqrt(np.mean(ref ** 2) / np.mean(masker ** 2))
steady = stoi(ref, ref + masker, fs)

hole = np.ones(n)
hole[int(1.6 * fs):int(1.95 * fs)] = 0.0        # un corte de 0,35 s
dropout = stoi(ref, ref * hole, fs)

print(round(steady.value, 3), round(dropout.value, 3))   # 0.772 0.874
print(round(float(dropout.segment_scores.min()), 3))     # -0.078
```

</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>

### Parámetros de `stoi()`

| Parámetro | Tipo | Unidades | Rango / def. | Notas |
| :--- | :--- | :--- | :--- | :--- |
| `clean` | array 1D | cualquiera | no vacío | La referencia limpia; la medida es invariante al nivel, así que la calibración da igual |
| `degraded` | array 1D | cualquiera | misma longitud que `clean` | Misma locución, misma frecuencia de muestreo, alineada muestra a muestra |
| `fs` | int | Hz | > 0 | Frecuencia de muestreo de *ambas* señales; se remuestrea a 10 kHz por dentro |
| `extended` | bool | — | def. `False` | `True` selecciona ESTOI |

Devuelve un `STOIResult`: `value`, `band_scores` (15), `segment_scores`,
`band_frequencies` y `extended`. Da error con longitudes distintas, con entradas
no finitas y cuando sobreviven menos de 30 tramas de corto plazo a la eliminación
de tramas silenciosas.

### Cómo leer el valor

El índice bruto no tiene interpretación absoluta, y tampoco pretende tenerla. El
paso de $d$ a un porcentaje de palabras entendidas es una logística ajustada a un
corpus y a un grupo de oyentes concretos, que la biblioteca deliberadamente no
implementa, así que un mismo $d$ predice puntuaciones distintas para materiales
distintos, y citar «STOI = 0,73» como una inteligibilidad es un error de
categoría.

De ahí sale la práctica de trabajo. Mantén fijos el material, los hablantes, la
alineación y la duración de análisis, y declara la **diferencia** entre
condiciones (el ΔSTOI entre un procesador y su entrada sin procesar) junto con el
número de locuciones sobre el que se promedió, porque los valores de una sola
locución dispersan. Atiende también a la forma: el índice satura cerca de 1 para
las degradaciones leves y se aplana por el extremo bajo, así que una mejora de
0,85 a 0,90 vale mucho más en inteligibilidad que una de 0,35 a 0,40; monótono no
significa lineal. Y cuando dos condiciones puntúan parecido, las vistas por banda
y por segmento son los diagnósticos que dicen si llegaron ahí por el mismo
camino.

## 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 del habla vislumbrada | 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/perception/speech/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/perception/speech/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

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

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/perception/speech/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/perception/speech/speech-intelligibility/):
  predice la inteligibilidad a partir de espectros de habla, ruido y umbral.
- [Bancos de filtros](/phonometry/es/signals/filters/filter-banks/): las bandas de tercio
  de octava en las que el procesado inicial agrupa la DFT.
- Referencia de la API: [`speech.objective_intelligibility`](/phonometry/es/reference/api/speech/objective-intelligibility/).
- Teoría: [Transferencia de modulación y STI](/phonometry/es/reference/theory/perception/#transferencia-de-modulación-y-sti-iec-60268-16): la derivación de la transferencia de modulación con la que se comparan las medidas intrusivas de esta página.
