---
title: "Cepstro, ecos y espectro de la envolvente"
description: "El cepstro de potencia, real y complejo con análisis de quefrencia, detección de ecos con el coeficiente de reflexión leído en el pico cepstral, liftering paso bajo y paso alto de un espectro logarítmico, la ida y vuelta homomórfica del cepstro complejo y el espectro de la envolvente que convierte las modulaciones de amplitud en líneas discretas."
references:
  - type: book
    authors: ["Havelock, D.", "Kuwano, S.", "Vorländer, M. (eds.)"]
    year: 2008
    title: "Handbook of signal processing in acoustics"
    publisher: "Springer"
    doi: "10.1007/978-0-387-30441-0"
    note: "Capítulo 27 (Milner: la transformada cepstral como DFT inversa del espectro logarítmico de potencia, la quefrencia, liftering paso bajo/paso alto), capítulo 87 (Neelamani: el cepstro complejo, deconvolución homomórfica, trenes de picos periódicos por reverberación) y capítulo 75 (Tohyama: manipulación fase mínima/pasa-todo en el dominio cepstral). ISBN 978-0-387-77698-9."
  - type: book
    authors: ["Bendat, J. S.", "Piersol, A. G."]
    year: 2010
    title: "Random data: Analysis and measurement procedures"
    edition: "4.ª ed."
    publisher: "Wiley"
    doi: "10.1002/9781118032428"
    note: "Sección 13.1.4 (la relación de Hilbert entre log-magnitud y fase tras el plegado de fase mínima) y sección 13.3 con la figura 13.11 (detección de envolvente seguida de eliminación de la continua, la estructura del espectro de la envolvente). ISBN 978-0-470-24877-5."
---

import ThemeImage from '../../../../components/ThemeImage.astro';

Los [estimadores espectrales](/phonometry/es/guides/spectral-analysis/) describen
*qué frecuencias* contiene una señal; esta página cubre lo que se esconde en
la *forma* de ese espectro. El **cepstro** - la transformada de Fourier
inversa del espectro logarítmico - vive en `phonometry.metrology` y convierte
dos problemas espectrales difíciles en simple localización de picos: el
rizado espectral periódico (un eco, una familia de armónicos) colapsa en un
único pico en la **quefrencia** de su período, y la envolvente espectral
suave se separa de la estructura fina mediante un simple enventanado -
**liftering** - en el dominio de la quefrencia. La misma maquinaria extiende
la [envolvente de Hilbert](/phonometry/es/guides/correlation-delay/) con un
**espectro de la envolvente** en el que las modulaciones de amplitud se
vuelven líneas discretas.

## 1. El cepstro y sus tres variantes

Como el logaritmo convierte la convolución `x = h * u` en la suma
`ln X = ln H + ln U`, componentes que se solapan en el espectro se suman - y
se separan - en el dominio cepstral (Havelock cap. 27, Ecs. (22)-(23)).
`cepstrum` calcula las tres variantes estándar sobre el eje de quefrencia:

- `'power'` (por defecto): la DFT inversa de `ln|X|²` (Fig. 21 de Milner).
  Real, par y ciega a la fase - el caballo de batalla para detectar ecos y
  familias de armónicos;
- `'real'`: la DFT inversa de `ln|X|` - exactamente la mitad del cepstro de
  potencia, y la cantidad cuyo plegado causal es la reconstrucción de fase
  mínima (más abajo);
- `'complex'`: la DFT inversa de `ln|X| + j·arg X` con la fase desenrollada
  y su componente lineal eliminada (Havelock cap. 87, Ec. (14)). Conserva la
  fase, así que es **invertible**: la puerta de entrada a la deconvolución
  homomórfica.

```python
import numpy as np
from phonometry import cepstrum

fs = 48000.0
rng = np.random.default_rng(1)
x = rng.standard_normal(4096)

res = cepstrum(x, fs, kind="power")
print(res.quefrencies[:3], res.cepstrum.shape)   # eje de quefrencia, en s
res.plot(language="es")
```

El resultado lleva el eje de quefrencia periódico completo
(`0 .. (nfft-1)/fs`); las quefrencias por encima de `nfft/(2·fs)` son las
quefrencias negativas especulares, donde los cepstros de potencia y real (que
son pares) se repiten y el cepstro complejo guarda su contenido anticausal
(de fase no mínima). El relleno de ceros vía `nfft` reduce el aliasing
temporal cepstral cuando el espectro logarítmico tiene rasgos abruptos,
exactamente igual que el relleno `oversample` de
[`minimum_phase`](/phonometry/es/guides/swept-sine-distortion/).

## 2. Detección de ecos: el tren de picos rahmónicos

Una reflexión única `x(t) = s(t) + a·s(t-t0)` multiplica el espectro por
`1 + a·e^{-j2πft0}` - un rizado de período `1/t0` en toda la banda. Su
logaritmo se expande, para `|a| < 1`, en la serie exactamente sumable

$$
\ln\!\left(1 + a e^{-j\theta}\right)
= \sum_{n \ge 1} (-1)^{n+1} \frac{a^n}{n}\, e^{-jn\theta},
$$

así que el cepstro lleva un tren de picos en los **rahmónicos** `n·t0` con
amplitudes `a, -a²/2, a³/3, ...` (su suma es `ln(1+a)`), independientemente
del espectro de la propia `s`, que se concentra en las quefrencias bajas. En
el cepstro de potencia la altura del primer pico es exactamente el
coeficiente de reflexión `a` - una forma cerrada que los tests y la batería
de conformidad fijan a 1e-10. `echo_detection` automatiza la lectura:

```python
import numpy as np
from phonometry import echo_detection

fs = 48000.0
rng = np.random.default_rng(2)
s = rng.standard_normal(12000)               # fuente de banda ancha
x = s + 0.5 * np.roll(s, 384)                # eco: 8 ms, a = 0.5

res = echo_detection(x, fs, min_quefrency=0.002)
print(res.delay, res.reflection_coefficient)  # 0.008 s, ~0.5
res.plot(language="es")
```

<ThemeImage src="https://raw.githubusercontent.com/jmrplens/phonometry/main/.github/images/cepstrum_echo_es.svg" alt="Cepstro de potencia de una respuesta al impulso con una reflexión frente a la quefrencia en milisegundos: un pico positivo afilado exactamente a 8 milisegundos marcado como eco detectado, un segundo rahmónico negativo menor a 16 milisegundos, la envolvente de la fuente en las quefrencias bajas fuera de la banda de búsqueda sombreada y una línea discontinua en el retardo verdadero" width="82%" loading="eager" />

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

```python
import matplotlib.pyplot as plt
import numpy as np
from scipy import signal as sp_signal
from phonometry import echo_detection, noise_signal

fs = 48000.0
n = 12000
impulse = np.zeros(n)
impulse[0] = 1.0
b, a = sp_signal.butter(2, [0.004, 0.9], btype="bandpass")
direct = sp_signal.lfilter(b, a, impulse)              # clic de banda ancha
ir = direct + 0.5 * np.roll(direct, int(0.008 * fs))   # eco a 8 ms
ir += noise_signal(fs, n / fs, color="white", rms=1e-4, seed=13)

res = echo_detection(ir, fs, min_quefrency=0.002)

fig, ax = plt.subplots(figsize=(10, 6))
half = res.nfft // 2 + 1
ax.plot(1e3 * res.quefrencies[:half], res.cepstrum[:half], lw=1.1)
ax.axvline(8.0, ls="--", color="k", label="Retardo verdadero del eco")
ax.plot([1e3 * res.delay], [res.reflection_coefficient], "v", ms=10,
        label="Pico detectado (altura = reflexión a)")
ax.set_xlim(0.0, 30.0)
ax.set_xlabel("Quefrencia [ms]")
ax.set_ylabel("Cepstro")
ax.legend()
plt.show()
```

</details>

La banda de búsqueda empieza por encima de la región de quefrencias bajas
que ocupa la envolvente espectral de la propia fuente (`min_quefrency`, por
defecto 16 muestras) y termina en la mitad no ambigua del eje
(`max_quefrency`). Los trenes de picos por reverberación de la sismología
(Havelock cap. 87) son la misma firma a escala geofísica. Obsérvese el
segundo rahmónico negativo en `2·t0` de la figura: el término `-a²/2` de la
serie, una confirmación útil de que un pico es de verdad un eco y no una
periodicidad espectral sin relación.

## 3. Liftering: envolvente frente a estructura fina

Filtrar en el dominio de la quefrencia se llama **liftering** (Havelock
cap. 27, sec. 4.3). Un lifter *paso bajo* conserva las quefrencias por
debajo del corte y devuelve la envolvente log-espectral suave con el rizado
eliminado; un lifter *paso alto* conserva el complemento - solo el rizado.
Los dos modos son exactamente complementarios en dB, porque la partición es
lineal en el dominio logarítmico:

```python
import numpy as np
from phonometry import lifter

fs = 48000.0
rng = np.random.default_rng(3)
s = rng.standard_normal(12000)
x = s + 0.5 * np.roll(s, 384)                # el mismo eco de 8 ms

low = lifter(x, fs, cutoff=0.004, mode="lowpass")    # envolvente de ln|X|
high = lifter(x, fs, cutoff=0.004, mode="highpass")  # el rizado del eco
print(np.allclose(low.liftered_db + high.liftered_db, low.spectrum_db))
low.plot(language="es")
```

Para la señal de eco puro el rizado del paso alto oscila entre las formas
cerradas `20·log10(1+a)` y `20·log10(1-a)` dB, otro oráculo que fijan los
tests. En análisis de voz la misma operación separa la envolvente del tracto
vocal (formantes) de los armónicos de la excitación; aquí es la herramienta
general para separar "lo suave de lo periódico" en cualquier respuesta en
magnitud medida.

## 4. El cepstro complejo y la conexión con la fase mínima

El cepstro complejo conserva la fase desenrollada, así que la transformada
es de ida y vuelta: `CepstrumResult.invert()` restaura el registro con
precisión de máquina, incluida la componente de fase lineal (retardo puro)
que la transformada directa elimina y guarda en `linear_phase_samples`:

```python
import numpy as np
from scipy import signal as sp_signal
from phonometry import cepstrum

fs = 48000.0
x = np.zeros(2048)
b, a = sp_signal.butter(2, 0.3)
x[37:293] = sp_signal.lfilter(b, a, np.r_[1.0, np.zeros(255)])

res = cepstrum(x, fs, kind="complex")
print(res.linear_phase_samples)              # negativo: retardo eliminado
print(np.max(np.abs(res.invert() - x)))      # ~1e-14
```

Entre el logaritmo y la transformada inversa se puede editar lo que se
quiera - eso es la deconvolución homomórfica (Havelock cap. 87, sec. 3.3):
anular los rahmónicos elimina un eco, conservar solo las quefrencias bajas
extrae la ondícula de la fuente. Una señal de fase mínima tiene un cepstro
complejo causal, y por eso plegar el cepstro real sobre las quefrencias
positivas reconstruye la fase mínima a partir de `|H|` únicamente:
[`minimum_phase`](/phonometry/es/guides/swept-sine-distortion/) y
`phase_decomposition` corren sobre ese mismo núcleo de plegado (Bendat y
Piersol sec. 13.1.4; Tohyama, en Havelock cap. 75, edita la reverberación
manipulando exactamente estas partes causal y anticausal).

## 5. El espectro de la envolvente: modulaciones como líneas

Donde el cepstro encuentra periodicidades *del espectro*, el **espectro de
la envolvente** encuentra periodicidades *de la amplitud*. La sección 13.3
de Bendat y Piersol (Fig. 13.11) formaliza la estructura: un detector de
envolvente, un eliminador de continua y una vista espectral de lo que queda.
`envelope_spectrum` pasa la
[envolvente de Hilbert](/phonometry/es/guides/correlation-delay/)
(`kind="magnitude"`, el valor práctico por defecto) o el detector cuadrático
del libro (`kind="squared"`) por exactamente esa cadena, escalada para que
una modulación sinusoidal se lea como una línea con su amplitud exacta.

Para un tono AM `A0·(1 + m·cos(2πfm·t))·cos(2πfc·t)` las formas cerradas
son:

| `kind` | nivel medio | línea en `fm` | línea en `2fm` |
|---|---|---|---|
| `'magnitude'` | `A0` | `A0·m` | - |
| `'squared'` | `A0²·(1 + m²/2)` | `2·A0²·m` | `A0²·m²/2` |

```python
import numpy as np
from phonometry import envelope_spectrum

fs = 8192.0
t = np.arange(int(4 * fs)) / fs
x = (1.0 + 0.4 * np.cos(2 * np.pi * 25.0 * t)) * np.cos(2 * np.pi * 1000.0 * t)

res = envelope_spectrum(x, fs)
k = int(round(25.0 * res.nfft / fs))
print(res.mean_level, res.amplitude[k])      # ~1.0 y ~0.4
res.plot(language="es")
```

<ThemeImage src="https://raw.githubusercontent.com/jmrplens/phonometry/main/.github/images/envelope_spectrum_es.svg" alt="Espectro de la envolvente de un tono de un kilohercio modulado en amplitud dentro de ruido, frente a la frecuencia hasta cien hercios: una única línea afilada en la frecuencia de modulación de veinticinco hercios que alcanza exactamente la referencia punteada de amplitud cero coma cuatro, con un suelo de ruido plano en el resto" width="82%" loading="eager" />

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

```python
import matplotlib.pyplot as plt
import numpy as np
from phonometry import envelope_spectrum, noise_signal

fs = 8192.0
seconds = 4.0
t = np.arange(int(seconds * fs)) / fs
x = (1.0 + 0.4 * np.cos(2 * np.pi * 25.0 * t)) * np.cos(2 * np.pi * 1000.0 * t)
x += noise_signal(fs, seconds, color="white", rms=0.03, seed=8)

res = envelope_spectrum(x, fs)

fig, ax = plt.subplots(figsize=(10, 6))
ax.plot(res.frequencies, res.amplitude, lw=1.4,
        label="Espectro de la envolvente")
ax.axvline(25.0, ls="--", color="k", label="Frecuencia de modulación")
ax.axhline(0.4, ls=":", color="r",
           label=r"Amplitud exacta de la línea $A_0 m$")
ax.set_xlim(0.0, 100.0)
ax.set_xlabel("Frecuencia [Hz]")
ax.set_ylabel("Amplitud de modulación")
ax.legend()
plt.show()
```

</details>

Los defectos de rodamientos y engranajes, el zumbido de la red eléctrica y
la modulación de amplitud de los aerogeneradores aparecen así: líneas en la
frecuencia de modulación y sus armónicos, limpiamente separadas del espectro
de la propia portadora. La media de la envolvente que elimina la etapa de
continua se conserva en `mean_level` (la amplitud de la portadora para el
detector de magnitud), y `remove_dc=False` omite el eliminador cuando
importa la línea de continua absoluta.

## Relación con los demás estimadores

El cepstro parte de las mismas convenciones FFT de registro único que los
[estimadores espectrales calibrados](/phonometry/es/guides/spectral-analysis/),
y su núcleo de plegado es literalmente el que hay dentro de
[`minimum_phase`](/phonometry/es/guides/swept-sine-distortion/) - la
refactorización queda fijada bit a bit en los tests. El espectro de la
envolvente es la vista en frecuencia de la misma señal analítica que la
[envolvente de Hilbert](/phonometry/es/guides/correlation-delay/) devuelve en
el tiempo, y un preanálisis natural antes de las métricas dedicadas de
[modulación de amplitud de aerogeneradores](/phonometry/es/guides/wind-turbine-noise/):
el espectro de la envolvente dice *si hay modulación y a qué ritmo*, las
métricas del dominio la cuantifican normativamente.
