Pipeline de preprocesamiento para datos de audio¶
Práctica 14 - Procesamiento de Audio con librosa
UT4: Datos Especiales | Análisis de Audio
📚 Tiempo estimado de lectura: ~28 min
- Autores [G1]: Joaquín Batista, Milagros Cancela, Valentín Rodríguez, Alexia Aurrecoechea, Nahuel López
- Fecha: Diciembre 2024
- Entorno: Python 3.13+ | librosa | soundfile | NumPy | scipy
- Referencia de la tarea: Práctica 14 — Audio Processing Assignment
💾 Descargar Notebook¶
📂 Archivos disponibles dentro del repositorio:
docs/portfolio/assets/audio-processing/audio_processing_practice14.ipynb
🎯 Objetivo¶
El objetivo de esta práctica fue dominar técnicas fundamentales de procesamiento de audio para Machine Learning, aprendiendo a cargar, estandarizar, extraer MFCCs y aplicar data augmentation sin perder información crítica. Se trabajó con el dataset UrbanSound8K (~8,700 clips) aplicando librosa para análisis espectral, normalización y features para clasificación de sonidos urbanos.
💼 Contexto y Motivación¶
El Desafío del Audio como Dato en ML¶
En proyectos de clasificación de audio, el preprocesamiento determina el éxito del modelo:
- 📊 Sample rate heterogéneo = incompatibilidad: Modelos requieren inputs de dimensión fija
- 🔊 Amplitud variable = inestabilidad: RMS bajo vs alto cambia features dramáticamente
- 🎼 MFCCs capturan timbre: Representación compacta de características espectrales
- 🔄 Augmentation aumenta robustez: Pitch shift/time stretch simula variabilidad real
| Elemento | Descripción |
|---|---|
| Problema | Preparar audio urbano para clasificación de sonidos (10 clases) |
| Dataset | UrbanSound8K - 8,732 clips (4s promedio), 10 categorías urbanas |
| Desafío técnico | Estandarizar sample rate/duración sin data leakage de labels |
| Target análisis | 13 MFCCs (mean+std) + RMS + ZCR → 28 features por clip |
| Valor de negocio | Monitoreo acústico urbano, detección de eventos, sistemas de alerta |
📘 Metodología: Taxonomía de Operaciones de Audio¶
Categorías de Procesamiento¶
┌─────────────────────────────────────────────┐
│ REPRESENTACIÓN DE AUDIO (Waveform) │
├─────────────────────────────────────────────┤
│ │
│ 🌊 CONCEPTO: │
│ Secuencia de amplitudes en el tiempo │
│ │
│ 🔧 PROPIEDADES: │
│ - Sample rate (Hz): muestras/segundo │
│ - Duration (s): longitud temporal │
│ - Amplitude: rango de valores │
│ - Channels: mono (1) o estéreo (2) │
│ │
│ 📊 EJEMPLO: │
│ sr=16000 Hz, duration=3s │
│ → 48,000 samples (16000 × 3) │
│ │
│ ✅ REGLA DE ORO: │
│ Mono suficiente para clasificación │
│ Estéreo solo si espacialidad importa │
│ │
└─────────────────────────────────────────────┘
┌─────────────────────────────────────────────┐
│ ESPECTROGRAMA (Representación Freq) │
├─────────────────────────────────────────────┤
│ │
│ 🎨 CONCEPTO: │
│ Matriz 2D: tiempo × frecuencia │
│ Muestra CUÁNDO ocurre CUÁL freq │
│ │
│ 🔧 GENERACIÓN: │
│ librosa.stft(y) → matriz compleja │
│ librosa.amplitude_to_db() → escala dB │
│ │
│ 📊 EJEMPLO: │
│ Sirena → líneas verticales (pulsos) │
│ en frecuencias específicas repetidas │
│ │
│ ⚠️ TRADE-OFF: │
│ Window size: resolución tiempo vs freq │
│ Pequeña → buena temporal, mala freq │
│ Grande → mala temporal, buena freq │
│ │
└─────────────────────────────────────────────┘
┌─────────────────────────────────────────────┐
│ MFCCs (Mel-Frequency Cepstral Coeffs) │
├─────────────────────────────────────────────┤
│ │
│ 🎯 CONCEPTO: │
│ Representación compacta del timbre │
│ Captura "color" del sonido │
│ │
│ 🔧 PROCESO: │
│ 1. FFT → Espectro de frecuencias │
│ 2. Mel filterbank → Escala perceptual │
│ 3. Log → Compresión de amplitud │
│ 4. DCT → Decorrelación (MFCCs) │
│ │
│ 📊 FEATURES: │
│ 13 coeficientes × 2 stats (mean/std) │
│ = 26 features por audio clip │
│ │
│ 💡 USO: │
│ MFCCs son gold standard para speech │
│ y clasificación de sonidos │
│ │
└─────────────────────────────────────────────┘
┌─────────────────────────────────────────────┐
│ DATA AUGMENTATION (Variabilidad) │
├─────────────────────────────────────────────┤
│ │
│ 🔄 CONCEPTO: │
│ Transformaciones que preservan clase │
│ pero varían características superfic. │
│ │
│ 🔧 TÉCNICAS: │
│ - Pitch shift: cambiar tono (±2 semitonos)│
│ - Time stretch: cambiar velocidad (0.9x) │
│ - Ruido blanco: agregar SNR 10-20 dB │
│ │
│ 📊 EJEMPLO: │
│ Sirena normal vs sirena con doppler │
│ effect → modelo robusto a variabilidad │
│ │
│ ✅ CALIDAD: │
│ Augmentation NO debe cambiar label │
│ (ej: perro ladrando sigue siendo perro)│
│ │
└─────────────────────────────────────────────┘
📊 Dataset: UrbanSound8K¶
Características del Dataset¶
Fuente: UrbanSound8K - Kaggle
Descripción: - 8,732 clips de audio etiquetados - 10 clases de sonidos urbanos - Duración: <4 segundos por clip - Sample rate: 22.05 kHz - 48 kHz (heterogéneo) - Pre-dividido en 10 folds para cross-validation
Clases incluidas:
| Clase | ID | Descripción | Ejemplos |
|---|---|---|---|
| air_conditioner | 0 | Unidad de AC funcionando | Ruido constante bajo |
| car_horn | 1 | Bocina de coche | Pitido agudo |
| children_playing | 2 | Niños jugando | Voces infantiles |
| dog_bark | 3 | Ladrido de perro | Pulsos cortos |
| drilling | 4 | Taladro eléctrico | Ruido de impacto |
| engine_idling | 5 | Motor al ralentí | Rumble grave |
| gun_shot | 6 | Disparo | Impulso explosivo |
| jackhammer | 7 | Martillo neumático | Pulsos irregulares |
| siren | 8 | Sirena de emergencia | Tono oscilante |
| street_music | 9 | Música callejera | Melodía/ritmo |
Distribución del Dataset¶
Por fold:
Figura 1: Distribución de cantidad de audios por fold en UrbanSound8K. Eje X: 10 folds (fold1-fold10). Eje Y: Cantidad de clips de audio. Rango: 805-988 clips por fold. Observaciones: (1) Fold 4 tiene máximo con 988 clips (+22% vs mínimo). (2) Fold 10 tiene mínimo con 805 clips. (3) Fold 8 también bajo con 809 clips. (4) Variabilidad de ~20% entre folds (805-988 rango) indica distribución NO perfectamente balanceada. Implicancia para CV: Al usar 10-fold cross-validation estratificado, algunos folds tendrán ligeramente más datos de entrenamiento que otros - NO afecta validez del CV pero importante documentar para reproducibilidad. Stratified split por clase dentro de cada fold ayuda a mantener proporción de clases consistente a pesar de tamaños desiguales.
Estadísticas clave:
| Métrica | Valor | Observación |
|---|---|---|
| Total clips | 8,732 | Dataset tamaño medio |
| Clips por fold (promedio) | 873 | ~100 clips por clase por fold |
| Variabilidad fold | 805-988 | 20% diferencia max-min |
| Sample rate más común | 48 kHz | Necesita resample a 16 kHz |
| Duración promedio | 2.95s | Mayoría <4s |
🌊 Parte 1: Carga y Análisis de Waveform¶
1.1. Conceptos Fundamentales¶
¿Qué es una waveform?
Una waveform (forma de onda) es la representación temporal de un audio: amplitud en función del tiempo.
Propiedades críticas:
SAMPLE RATE (sr):
- Muestras por segundo (Hz)
- 16 kHz típico para speech
- 44.1 kHz/48 kHz para música
- Nyquist: sr/2 = frecuencia máxima capturada
DURATION:
- Longitud temporal en segundos
- n_samples = sr × duration
- UrbanSound8K: mayoría <4s
AMPLITUDE:
- Rango típico: [-1.0, +1.0] (float32)
- RMS (Root Mean Square): energía promedio
- Clipping: valores ≥1.0 → distorsión
CHANNELS:
- Mono (1): suficiente para clasificación
- Estéreo (2): preserva espacialidad
- UrbanSound8K: mayormente mono
1.2. Implementación de Carga¶
Código:
import librosa
import librosa.display
import numpy as np
import matplotlib.pyplot as plt
# Cargar audio
audio_path = "data/raw/fold1/101415-3-0-2.wav"
y, sr = librosa.load(audio_path, sr=None, mono=True)
# Propiedades básicas
duration_sec = librosa.get_duration(y=y, sr=sr)
n_samples = y.shape[0]
n_channels = 1 # Forzamos mono con mono=True
print(f"Sample rate: {sr} Hz")
print(f"Duration: {duration_sec:.2f} s")
print(f"Samples: {n_samples:,}")
print(f"Channels: {n_channels}")
print(f"Amplitude range: [{y.min():.4f}, {y.max():.4f}]")
print(f"Dtype: {y.dtype}")
# Output típico:
# Sample rate: 48000 Hz
# Duration: 4.03 s
# Samples: 193,536
# Channels: 1
# Amplitude range: [-0.8535, 0.8065]
# Dtype: float32
1.3. Visualización: Waveform Original¶
Figura 2: Waveform mono del clip 101415-3-0-2.wav (dog bark). Eje X: Tiempo en segundos (0-4s). Eje Y: Amplitud normalizada [-1.0, +1.0]. Estructura temporal: Tres eventos de ladrido claramente visibles: (1) Ladrido 1 (~0.3s): amplitud máxima ~0.75, duración ~0.3s, forma de impulso. (2) Ladrido 2 (~1.2s): amplitud ~0.8 (máximo absoluto), pulso más agudo y corto (~0.2s). (3) Ladrido 3-10 (~2.5-4s): Serie de ladridos más débiles (~0.5-0.6 amplitud) con menor separación temporal. Período silencioso: 1.5-2.5s con amplitud ~0.0 (silencio entre eventos). Interpretación acústica: Ladrido típico de perro = ráfaga de energía concentrada en <0.3s, seguida de silencio. Amplitud NO alcanza ±1.0 → audio NO tiene clipping (sin distorsión). Energía decreciente después de primeros ladridos sugiere perro alejándose o perdiendo intensidad. Valor para features: Eventos discretos identificables → ZCR y RMS capturarán "pulsatilidad" característica de ladridos vs sonidos continuos (motor, sirena).
Hallazgos: - Amplitud máxima 0.8065 → No hay clipping (buen rango dinámico) - Eventos discretos (ladridos) separados por silencios - Audio normalizado dentro de [-1.0, +1.0] estándar
🎨 Parte 2: Estandarización y Normalización¶
2.1. ¿Por qué Estandarizar?¶
Problema de heterogeneidad:
Audio A: sr=48000 Hz, duration=4.03s, amplitude [-0.85, 0.80]
Audio B: sr=22050 Hz, duration=2.18s, amplitude [-0.12, 0.15]
Sin estandarización:
→ Shape A: (193,536,) → MFCC: (13, 377) timesteps
→ Shape B: (48,069,) → MFCC: (13, 94) timesteps
→ INCOMPATIBLE para batch processing
Con estandarización (target: 16kHz, 3s):
→ Shape A: (48,000,) → MFCC: (13, 94) timesteps
→ Shape B: (48,000,) → MFCC: (13, 94) timesteps
→ COMPATIBLE ✓
Parámetros target: - Sample rate: 16 kHz (suficiente para sonidos urbanos, Nyquist 8kHz) - Duration: 3 segundos (captura mayoría de eventos) - Amplitude: [-1.0, +1.0] (normalizado por máximo)
2.2. Implementación de Pipeline de Estandarización¶
Código:
TARGET_SR = 16000
TARGET_DURATION = 3.0
TARGET_SAMPLES = int(TARGET_SR * TARGET_DURATION) # 48,000
def preprocess_audio(audio_path,
target_sr=TARGET_SR,
target_duration=TARGET_DURATION):
"""
Pipeline de estandarización:
1. Cargar audio en mono
2. Resample a target_sr
3. Truncar/pad a target_duration
4. Normalizar amplitud
"""
# 1. Cargar en mono
y, sr_original = librosa.load(audio_path, sr=None, mono=True)
# 2. Resample si es necesario
if sr_original != target_sr:
y = librosa.resample(y, orig_sr=sr_original, target_sr=target_sr)
# 3. Ajustar duración
target_len = int(target_sr * target_duration)
if len(y) > target_len:
# Truncar desde el inicio (preserva inicio del evento)
y = y[:target_len]
elif len(y) < target_len:
# Pad con ceros al final
y = np.pad(y, (0, target_len - len(y)), mode='constant')
# 4. Normalizar amplitud (max abs = 1.0)
max_abs = np.abs(y).max()
if max_abs > 0:
y = y / max_abs
return y, target_sr
# Aplicar
y_proc, sr_proc = preprocess_audio(audio_path)
print(f"Forma procesada: {y_proc.shape} sr: {sr_proc}")
print(f"Duración procesada (s): {len(y_proc) / sr_proc:.1f}")
print(f"Amplitud procesada min/max: {y_proc.min():.2f} {y_proc.max():.2f}")
# Output:
# Forma procesada: (48000,) sr: 16000
# Duración procesada (s): 3.0
# Amplitud procesada min/max: -0.99 0.93
2.3. Visualización: Antes y Después¶
Figura 3: Comparación de waveforms antes/después de estandarización. Panel superior (Original): Audio en 48 kHz con 4.03s de duración total. Tres ladridos principales visibles más serie de ladridos débiles al final. Silencio prolongado 1.5-2.5s preserved. Panel inferior (Estandarizada): Audio resampleado a 16 kHz y truncado a 3.0s. (1) Shape: (48000,) = 16000 Hz × 3s - dimensión FIJA para batch processing. (2) Contenido temporal: Primeros 3 segundos del original preserved → incluye los 2 primeros ladridos fuertes + inicio del silencio. Serie de ladridos finales (3-4s) ELIMINADA por truncado. (3) Amplitud normalizada a máximo absoluto = 1.0 → pico del segundo ladrido ahora exactamente en +1.0 y -1.0. (4) Forma de onda visualmente IDÉNTICA en timing y estructura → resample NO altera patrón temporal, solo comprime frecuencias. Trade-off: Perdemos último segundo (serie débil de ladridos) pero ganamos inputs consistentes para modelo. Alternativa: usar sliding windows para capturar TODO el audio en múltiples fragmentos de 3s.
Beneficios:
- Shape consistente: (48,000,) para TODOS los audios
- MFCCs resultantes: (13, 94) timesteps uniformes
- Batch processing trivial: np.stack([y1, y2, ..., yn])
📈 Parte 3: Análisis Espectral¶
3.1. Generación de Espectrogramas¶
Código:
# Generar espectrograma
D = librosa.stft(y_proc) # Short-Time Fourier Transform
S_db = librosa.amplitude_to_db(np.abs(D), ref=np.max)
# Visualización
fig, ax = plt.subplots(figsize=(10, 4))
img = librosa.display.specshow(
S_db,
sr=sr_proc,
x_axis='time',
y_axis='hz',
ax=ax,
cmap='viridis'
)
ax.set_title('Espectrograma original (mono)')
fig.colorbar(img, ax=ax, format='%+2.0f dB')
plt.tight_layout()
plt.show()
3.2. Visualización: Espectrograma Original¶
Figura 4: Espectrograma del clip dog bark estandarizado (16 kHz, 3s). Ejes: X=Tiempo (0-4s), Y=Frecuencia (0-22 kHz), Color=Amplitud en dB (+0 amarillo, -80 violeta). Estructura espectral: (1) Evento 1 (~0-0.5s): Banda ancha de energía 0-5000 Hz (amarillo-verde brillante), indica impulsividad del ladrido. Armónicos débiles hasta 10 kHz visibles como líneas horizontales cianas. (2) Evento 2 (~1.0-1.3s): Segundo ladrido con energía MÁS concentrada en fundamental ~800 Hz (banda amarilla estrecha) + segundo armónico ~1600 Hz. Energía total MAYOR que evento 1 (más amarillo). (3) Silencio (1.5-2.5s): Azul oscuro/violeta en TODAS las frecuencias = ausencia de señal (ruido de fondo <-60 dB). (4) Eventos 3-10 (2.5-4s): Pulsos repetitivos con energía media (verde) concentrada en 500-2000 Hz. Menor amplitud que primeros ladridos. Limitación Nyquist: Frecuencias >8 kHz (mitad de 16 kHz) muestran solo ruido/aliasing → OK porque información crítica de ladridos está <5 kHz. Interpretación para MFCCs: Concentración de energía en <5 kHz justifica usar 13 MFCCs (capturan estructura espectral de bajas-medias frecuencias donde está el contenido discriminativo).
Hallazgos: - Energía concentrada en 0-5 kHz (fundamental + armónicos) - Eventos discretos (ladridos) claramente separados en tiempo - Silencio (~1.5-2.5s) visible como región oscura (baja energía)
3.3. Filtrado High-Pass¶
Objetivo: Eliminar ruido de bajas frecuencias (<80 Hz) sin afectar contenido principal.
Código:
from scipy.signal import butter, lfilter
def highpass_filter(y, sr, cutoff=80.0):
"""Filtro high-pass Butterworth de 5to orden."""
nyquist = sr / 2
normal_cutoff = cutoff / nyquist
b, a = butter(5, normal_cutoff, btype='high', analog=False)
y_filtered = lfilter(b, a, y)
return y_filtered
y_hp = highpass_filter(y_proc, sr_proc, cutoff=80.0)
# Generar espectrograma filtrado
D_hp = librosa.stft(y_hp)
S_hp_db = librosa.amplitude_to_db(np.abs(D_hp), ref=np.max)
3.4. Visualización: Espectrograma High-Pass¶
Figura 5: Espectrograma después de filtro high-pass (cutoff=80 Hz). Diferencia clave: Región 0-80 Hz (primera franja horizontal inferior) ahora es azul oscuro/violeta uniforme = energía eliminada. Original tenía verde claro en esa zona (ruido de baja frecuencia o DC offset). Contenido >80 Hz: PRESERVADO sin cambios visibles - ladridos en 500-5000 Hz mantienen misma estructura y amplitud. Interpretación: Filtro cumplió objetivo: remover ruido sub-80Hz (inaudible para humanos, irrelevante para clasificación) sin tocar señal útil. Métricas espectrales: Centroid, Rolloff y Bandwidth NO deberían cambiar significativamente porque están dominados por contenido >500 Hz, muy por encima del cutoff. Cuándo usar: High-pass es crítico cuando audio tiene rumble de grabación (viento, vibraciones) o DC offset que "desperdicia" bins espectrales bajos sin información.
Comparación cuantitativa:
| Métrica | Original | High-Pass | Interpretación |
|---|---|---|---|
| Energía <80 Hz | 4,127 | 3,351 | -18.8% (ruido eliminado) |
| Energía >80 Hz | 31,892 | 31,887 | -0.02% (señal preservada) |
| RMS total | 0.072 | 0.071 | Cambio despreciable |
🎯 Parte 4: Extracción de MFCCs¶
4.1. ¿Qué son los MFCCs?¶
Mel-Frequency Cepstral Coefficients:
Representación compacta del "timbre" de un audio basada en percepción auditiva humana.
Pipeline de cálculo:
1. FFT (Fourier Transform)
Audio → Espectro de frecuencias
2. Mel Filterbank
Frecuencias lineales → Escala mel (perceptual)
Humanos perciben diferencias logarítmicamente
3. Logaritmo
Amplitud → Log amplitud
Compresión de rango dinámico
4. DCT (Discrete Cosine Transform)
Decorrelación de coeficientes
Resultado: MFCCs (13 coeficientes típicamente)
¿Por qué Mel scale?
Escala Mel refleja percepción humana:
- Diferencia 100Hz vs 200Hz (Δ=100Hz) → MUY AUDIBLE
- Diferencia 5000Hz vs 5100Hz (Δ=100Hz) → APENAS AUDIBLE
Mel filterbank: Más resolución en bajas freq, menos en altas
→ Refleja sensibilidad del oído humano
4.2. Implementación de Extracción¶
Código:
def extract_mfcc_features(y, sr, n_mfcc=13):
"""
Extrae MFCCs y features complementarias.
Returns:
dict con 28 features:
- 13 MFCCs (mean/std) = 26 features
- RMS (mean) = 1 feature
- ZCR (mean) = 1 feature
"""
# 1. MFCCs (13 coeficientes)
mfccs = librosa.feature.mfcc(y=y, sr=sr, n_mfcc=n_mfcc)
# 2. RMS (Root Mean Square) - energía
rms = librosa.feature.rms(y=y)[0]
# 3. ZCR (Zero Crossing Rate) - cambios de signo
zcr = librosa.feature.zero_crossing_rate(y)[0]
# Agregar estadísticas (mean/std) por coeficiente
features = {}
for i in range(n_mfcc):
features[f'mfcc_{i+1}_mean'] = np.mean(mfccs[i])
features[f'mfcc_{i+1}_std'] = np.std(mfccs[i])
features['rms_mean'] = np.mean(rms)
features['zcr_mean'] = np.mean(zcr)
return features
# Extraer features
feats = extract_mfcc_features(y_proc, sr_proc, n_mfcc=13)
print(f"Total features: {len(feats)}")
print(f"MFCC_1_mean: {feats['mfcc_1_mean']:.2f}")
print(f"MFCC_1_std: {feats['mfcc_1_std']:.2f}")
print(f"RMS_mean: {feats['rms_mean']:.4f}")
print(f"ZCR_mean: {feats['zcr_mean']:.4f}")
# Output:
# Total features: 28
# MFCC_1_mean: -394.39
# MFCC_1_std: 141.02
# RMS_mean: 0.0720
# ZCR_mean: 0.0499
4.3. Interpretación de Features¶
MFCC_1 (primer coeficiente):
MFCC_1_mean = -394.39
→ Representa energía espectral promedio
→ Valores negativos grandes son normales (escala logarítmica)
→ Captura "loudness" general del audio
MFCC_1_std = 141.02
→ Alta variabilidad = eventos discretos (ladridos)
→ Baja variabilidad = sonido continuo (motor)
RMS (Root Mean Square):
RMS_mean = 0.0720
→ Energía promedio del audio
→ Rango típico: 0.01-0.20 para audios normalizados
→ Perro ladrando: energía media (no es ruido constante alto)
ZCR (Zero Crossing Rate):
ZCR_mean = 0.0499
→ Tasa de cambios de signo (cruces por cero)
→ Alto ZCR → sonido percusivo/no tonal (batería)
→ Bajo ZCR → sonido tonal (voz, música)
→ Ladrido tiene ZCR media (impulsos con tono)
🔄 Parte 5: Data Augmentation¶
5.1. Técnicas de Augmentation¶
Pitch Shifting:
def pitch_shift_audio(y, sr, n_steps=2):
"""Cambia tono sin alterar duración."""
y_shifted = librosa.effects.pitch_shift(y, sr=sr, n_steps=n_steps)
return y_shifted
y_pitch = pitch_shift_audio(y_proc, sr_proc, n_steps=+2)
Time Stretching:
def time_stretch_audio(y, rate=0.9):
"""Cambia velocidad sin alterar tono."""
y_stretched = librosa.effects.time_stretch(y, rate=rate)
# Note: cambia duración, necesita truncar/pad después
return y_stretched
y_time = time_stretch_audio(y_proc, rate=0.9)
Ruido Blanco:
def add_white_noise(y, snr_db=10.0):
"""Agrega ruido blanco gaussiano con SNR controlado."""
signal_power = np.mean(y ** 2)
snr_linear = 10 ** (snr_db / 10)
noise_power = signal_power / snr_linear
noise = np.random.normal(0, np.sqrt(noise_power), y.shape)
y_noisy = y + noise
return y_noisy
y_noisy = add_white_noise(y_proc, snr_db=10.0)
5.2. Visualización: Pitch Shift¶
Figura 6: Espectrograma después de pitch shift +2 semitonos. Cambio espectral: TODAS las frecuencias desplazadas proporcionalmente hacia arriba. Fundamental de ladridos que estaba en ~800 Hz ahora está en ~950 Hz (+18.9% = factor de 2^(2/12)). Armónicos también desplazados: segundo armónico de ~1600 Hz a ~1900 Hz. Estructura temporal: IDÉNTICA al original - ladridos ocurren en mismo instante (0-0.5s, 1.0-1.3s, etc). Duración NO cambia (time stretch rate=1.0). Amplitud: Energía total similar (color amarillo/verde en eventos principales preservado), solo "subió" en eje Y. Uso en augmentation: Pitch shift +2 semitonos simula perro de raza más pequeña (ladrido más agudo) vs perro grande (ladrido grave). Modelo entrenado con esta variación será robusto a diferencias de tono PERO mantendrá capacidad de detectar patrón temporal de ladrido (impulsos discretos). Límite: No exceder ±4 semitonos o audio suena artificial (chipmunk effect).
Comparación de MFCCs:
Figura 7: Comparación de MFCC_1_mean entre versiones original, pitch+2 y time0.9x. Barras: Azul (original=-394.4), Naranja (pitch+2=-421.3), Verde (time0.9=-430.6). Observación clave: Pitch shift CAMBIA MFCCs (-27 shift en MFCC_1) porque altera contenido espectral directamente. Time stretch también afecta (-36) pero por razón diferente: comprime energía temporalmente. Interpretación: (1) MFCC_1 es SENSIBLE a pitch → OK, captura cambio de tono. (2) Valores más negativos = energía desplazada a frecuencias donde filtros mel tienen diferente ganancia. (3) Modelo entrenado con augmentation aprenderá que MFCC_1 puede variar ±50 para MISMA clase → robustez a pitch natural. Conclusión: Augmentation efectiva: genera variabilidad REALISTA que modelo verá en producción (perros de diferentes tamaños, sirenas de diferentes modelos, etc).
5.3. Visualización: Time Stretch¶
Figura 8: Espectrograma después de time stretch 0.9x (10% más lento). Cambio temporal: Duración total expandida ~11% (3.0s → 3.3s después de stretch, antes de re-truncar). Ladrido 1 que estaba en 0-0.5s ahora ocurre en 0-0.55s. Espaciado entre eventos AUMENTA proporcionalmente. Estructura espectral: Frecuencias PRESERVADAS (fundamental sigue en ~800 Hz, armónicos en posiciones originales). A diferencia de pitch shift, contenido frecuencial NO cambia. Color amarillo/verde en mismas bandas de frecuencia. Mecánica: Time stretch usa phase vocoder - mantiene fase de frecuencias pero "estira" tiempo entre frames espectrales. Aplicación práctica: Simula variaciones naturales de velocidad en eventos: (1) Ladridos más espaciados (perro menos agresivo). (2) Sirena con ciclo más lento. (3) Motor con RPM variable. Modelo aprende que TIMING puede variar sin cambiar identidad del sonido. Limitación: Rate <0.8 o >1.2 genera artefactos audibles (robotización). Range seguro: 0.85-1.15x.
🎛️ Parte 6: Análisis de Ruido (SNR)¶
6.1. Concepto de SNR¶
Signal-to-Noise Ratio:
SNR (dB) = 10 × log10(P_signal / P_noise)
Donde:
P_signal = potencia de señal (RMS² de audio limpio)
P_noise = potencia de ruido (RMS² del ruido añadido)
Interpretación:
SNR = 20 dB → señal 100x más potente que ruido (excelente)
SNR = 10 dB → señal 10x más potente que ruido (buena)
SNR = 0 dB → señal y ruido igual potencia (pobre)
SNR < 0 dB → ruido más fuerte que señal (inaceptable)
6.2. Experimento: Efecto del SNR en Features¶
Código:
# Generar versiones con diferentes SNRs
snr_levels = [0, 5, 10, 20]
results = []
for snr in snr_levels:
y_noisy = add_white_noise(y_proc, snr_db=snr)
feats = extract_mfcc_features(y_noisy, sr_proc)
results.append({
'snr_db': snr,
'mfcc_1_mean': feats['mfcc_1_mean'],
'mfcc_1_std': feats['mfcc_1_std'],
'rms_mean': feats['rms_mean']
})
df_snr = pd.DataFrame(results)
6.3. Visualización: SNR vs Variabilidad de MFCC¶
Figura 9: Relación entre SNR y variabilidad del MFCC_1 (desviación estándar). Eje X: SNR en dB (0, 5, 10, 20). Eje Y: Desviación estándar de MFCC_1 (variabilidad temporal). Tendencia: Crecimiento casi lineal de 13.2 (SNR=0dB) a 48.1 (SNR=20dB), slope=+1.74 std/dB. Interpretación: (1) SNR BAJO (mucho ruido) → STD BAJA porque ruido blanco "suaviza" variaciones espectrales del ladrido - impulsos discretos se difuminan en ruido continuo. (2) SNR ALTO (poco ruido) → STD ALTA porque estructura pulsátil del ladrido se preserva - transiciones abruptas entre silencio y evento capturadas. (3) Ruido actúa como "suavizador" no deseado - similar a Gaussian blur en imágenes, elimina edges. Implicancia para modelo: Audio con SNR <5dB tendrá MFCCs con baja variabilidad artificial - modelo puede confundir con sonido continuo (motor). Threshold mínimo recomendado: SNR ≥10dB para preservar características discriminativas. Data augmentation: Agregar ruido SNR 10-15dB simula condiciones reales de grabación (ambiente urbano ruidoso) sin degradar señal hasta punto de irreconocimiento.
Figura 10: Relación entre SNR y energía RMS del audio. Tendencia: Decrecimiento de RMS desde 0.175 (SNR=0dB) a 0.080 (SNR=20dB). Mecanismo: (1) Audio limpio tiene RMS=0.072 (baseline). (2) Agregar ruido AUMENTA RMS porque ruido blanco tiene energía constante distribuida uniformemente. (3) SNR=0dB → RMS duplica (+143% vs limpio) porque potencia de ruido = potencia de señal. (4) SNR=20dB → RMS apenas aumenta (+11% vs limpio) porque ruido es 100x más débil. Fórmula: RMS_total² = RMS_signal² + RMS_noise². Como ruido es aditivo, energías se suman en cuadratura. Consecuencia para features: RMS es feature crítica para detección de eventos - ruido elevado genera falsos positivos (alta energía sin evento). Normalizar RMS por baseline del ambiente reduce este efecto. Threshold: Si RMS_measured > 2× RMS_baseline, posible contaminación por ruido → rechazar clip o aplicar denoising.
📊 Parte 7: Métricas Espectrales Dinámicas¶
7.1. Definición de Métricas¶
Spectral Centroid:
Centroid = ∑(freq[i] × magnitude[i]) / ∑magnitude[i]
→ "Centro de masa" del espectro
→ Frecuencia promedio ponderada por amplitud
→ Alto centroid → sonido "brillante" (agudo)
→ Bajo centroid → sonido "oscuro" (grave)
Spectral Rolloff:
Rolloff = frecuencia donde 85% de energía está debajo
→ Indica extensión de altas frecuencias
→ Alto rolloff → contenido de alta frecuencia presente
→ Bajo rolloff → energía concentrada en graves
Spectral Bandwidth:
Bandwidth = desviación estándar de frecuencias alrededor del centroid
→ Dispersión espectral
→ Alto bandwidth → sonido "ruidoso" (banda ancha)
→ Bajo bandwidth → sonido "tonal" (banda estrecha)
7.2. Visualización: Métricas Dinámicas¶
Figura 11: Evolución temporal de métricas espectrales dinámicas. Tres curvas superpuestas: Azul (Centroid), Naranja (Rolloff), Verde (Bandwidth). Evento 1 (0-0.5s): (1) Centroid sube de ~900 Hz a pico de 1,300 Hz → ladrido tiene contenido de medias frecuencias. (2) Rolloff explota de ~1,500 Hz a 2,700 Hz → energía se extiende a altas frecuencias durante impulso. (3) Bandwidth aumenta de ~1,200 Hz a 1,600 Hz → ladrido es ruidoso (no tonal puro), contiene banda ancha de frecuencias. Silencio (1.0-1.5s): Las 3 métricas CAEN dramáticamente. Centroid ~800 Hz (ruido de fondo residual), Rolloff ~1,200 Hz, Bandwidth ~900 Hz. Valores bajos indican ausencia de señal útil. Evento 2 (1.5-1.8s): Segundo pico similar al primero pero MAYOR amplitud. Rolloff alcanza ~3,000 Hz (máximo del gráfico) → segundo ladrido tiene más contenido de altas frecuencias (más "sharp"). Eventos 3-10 (2.0-3.0s): Serie de pulsos con métricas oscilantes. Rolloff muy variable (1,500-4,800 Hz) indica heterogeneidad en contenido espectral de ladridos débiles - algunos más graves, otros más agudos. Insight para features: Variabilidad temporal alta de estas métricas (Rolloff σ=~1000 Hz) es CARACTERÍSTICA de dog bark vs sonidos continuos. Motor tendría centroid/rolloff casi constantes. Esta variabilidad se captura con STD de MFCCs.
Estadísticas:
| Métrica | Media | Std | Min | Max |
|---|---|---|---|---|
| Centroid (Hz) | 1,087 | 312 | 612 | 1,789 |
| Rolloff (Hz) | 2,031 | 987 | 982 | 4,812 |
| Bandwidth (Hz) | 1,542 | 421 | 879 | 2,278 |
🛡️ Parte 8: Control de Calidad (QA)¶
8.1. Sistema de QA Automatizado¶
Código:
def audio_quality_check(y, sr, duration_target=3.0):
"""
Valida calidad de audio con thresholds.
Returns:
dict con status y alertas
"""
checks = {
'duration': 'OK',
'rms': 'OK',
'zcr': 'OK',
'clipping': 'OK'
}
alerts = []
# 1. Duración
duration = len(y) / sr
if abs(duration - duration_target) > 0.1:
checks['duration'] = 'Dudoso'
alerts.append(f"Duración {duration:.2f}s != target {duration_target}s")
# 2. RMS (energía)
rms = np.sqrt(np.mean(y**2))
if rms < 0.01:
checks['rms'] = 'Dudoso'
alerts.append(f"RMS muy bajo ({rms:.4f}) - posible silencio")
elif rms > 0.5:
checks['rms'] = 'Malo'
alerts.append(f"RMS muy alto ({rms:.4f}) - posible saturación")
# 3. ZCR (cambios de signo)
zcr = np.mean(librosa.feature.zero_crossing_rate(y)[0])
if zcr < 0.01:
checks['zcr'] = 'Dudoso'
alerts.append(f"ZCR bajo ({zcr:.4f}) - audio muy tonal o DC offset")
elif zcr > 0.5:
checks['zcr'] = 'Malo'
alerts.append(f"ZCR alto ({zcr:.4f}) - ruido excesivo")
# 4. Clipping
max_abs = np.abs(y).max()
if max_abs >= 0.99:
checks['clipping'] = 'Malo'
alerts.append(f"Clipping detectado (max={max_abs:.4f})")
# Status general
if any(v == 'Malo' for v in checks.values()):
status = 'RECHAZAR'
elif any(v == 'Dudoso' for v in checks.values()):
status = 'REVISAR'
else:
status = 'ACEPTAR'
return {
'status': status,
'checks': checks,
'alerts': alerts
}
# Aplicar QA
qa_result = audio_quality_check(y_proc, sr_proc)
print(f"Status: {qa_result['status']}")
print(f"Checks: {qa_result['checks']}")
print(f"Alertas: {qa_result['alerts']}")
8.2. Visualización: Semáforos de QA¶
Duración:
Figura 12: Semáforo de validación de duración para 200 clips procesados. Barra verde (OK): 200 clips (100.0%) tienen duración exactamente 3.0s ± 0.1s tolerance. Pipeline de truncate/pad funcionó correctamente para TODOS los audios. Barras naranja/roja (Dudoso/Malo): 0 clips fuera de rango. Interpretación: Sistema de estandarización es robusto - NO hay clips que escaparon normalización. Threshold de ±0.1s es apropiado (permite <3.3% error relativo en duración). Implicancia: Batch processing puede proceder sin filtrado adicional - shape (48000,) garantizado para todos los inputs.
RMS (Energía):
Figura 13: Semáforo de validación de energía RMS. Barra verde (OK): 188 clips (94.0%) tienen RMS en rango normal [0.01, 0.5]. Barra naranja (Dudoso): 11 clips (5.5%) con RMS fuera de rango - posiblemente silencios largos o energía muy baja. Barra roja (Malo): 1 clip (0.5%) con RMS >0.5 - posible saturación o normalización fallida. Acciones recomendadas: (1) Revisar manualmente 11 clips "dudosos" - pueden ser válidos (ej: susurros, sonidos lejanos) o requerir re-grabación. (2) RECHAZAR el clip "malo" - RMS >0.5 indica problema serio de calidad que afectará features. (3) Aplicar re-normalización a clips dudosos si solo tienen problema de amplitud, no de contenido. Threshold ajustable: Si dataset contiene sonidos naturalmente débiles (hojas, lluvia), bajar límite inferior a 0.005.
ZCR (Cambios de Signo):
Figura 14: Semáforo de validación de Zero Crossing Rate. Barra verde (OK): 181 clips (90.5%) con ZCR en rango [0.01, 0.5]. Barra naranja (Dudoso): 13 clips (6.5%) con ZCR anómalo - posible DC offset o audio extremadamente tonal. Barra roja (Malo): 6 clips (3.0%) con ZCR crítico (>0.5 o <0.001) - indicativo de ruido puro o señal corrompida. Diagnóstico por ZCR: (1) ZCR <0.01 → Audio tiene DC offset (desplazamiento de baseline) o es tono puro continuo sin cruces de cero - aplicar high-pass filter. (2) ZCR >0.5 → Audio es casi ruido blanco (cruza cero en >50% de samples) - probable corrupción de archivo o grabación defectuosa. (3) ZCR normal (0.05-0.15) → Mix saludable de componentes tonales y transientes. Acción: Clips "malos" deben ser EXCLUIDOS del dataset - no son recuperables. Clips "dudosos" pueden salvarse con pre-filtrado.
🔧 Parte 9: Pipeline de Producción¶
9.1. Clase Unificada¶
Código:
class AudioFeatureExtractor:
"""
Pipeline end-to-end para procesamiento de audio.
Incluye:
- Estandarización (resample, duration, normalize)
- Filtrado (high-pass opcional)
- Extracción de features (MFCCs, RMS, ZCR)
- QA automatizado
"""
def __init__(self,
target_sr=16000,
target_duration=3.0,
n_mfcc=13,
apply_highpass=True,
highpass_cutoff=80.0):
self.target_sr = target_sr
self.target_duration = target_duration
self.n_mfcc = n_mfcc
self.apply_highpass = apply_highpass
self.highpass_cutoff = highpass_cutoff
def preprocess(self, audio_path):
"""Carga y estandariza audio."""
# Cargar
y, sr_orig = librosa.load(audio_path, sr=None, mono=True)
# Resample
if sr_orig != self.target_sr:
y = librosa.resample(y, orig_sr=sr_orig, target_sr=self.target_sr)
# Ajustar duración
target_len = int(self.target_sr * self.target_duration)
if len(y) > target_len:
y = y[:target_len]
elif len(y) < target_len:
y = np.pad(y, (0, target_len - len(y)), mode='constant')
# Normalizar
max_abs = np.abs(y).max()
if max_abs > 0:
y = y / max_abs
# High-pass filter (opcional)
if self.apply_highpass:
y = self._highpass_filter(y)
return y
def _highpass_filter(self, y):
"""Filtro Butterworth high-pass."""
from scipy.signal import butter, lfilter
nyquist = self.target_sr / 2
normal_cutoff = self.highpass_cutoff / nyquist
b, a = butter(5, normal_cutoff, btype='high', analog=False)
return lfilter(b, a, y)
def extract_features(self, y):
"""Extrae MFCCs + features complementarias."""
# MFCCs
mfccs = librosa.feature.mfcc(
y=y,
sr=self.target_sr,
n_mfcc=self.n_mfcc
)
# RMS y ZCR
rms = librosa.feature.rms(y=y)[0]
zcr = librosa.feature.zero_crossing_rate(y)[0]
# Estadísticas
features = {}
for i in range(self.n_mfcc):
features[f'mfcc_{i+1}_mean'] = np.mean(mfccs[i])
features[f'mfcc_{i+1}_std'] = np.std(mfccs[i])
features['rms_mean'] = np.mean(rms)
features['zcr_mean'] = np.mean(zcr)
return features
def quality_check(self, y):
"""Valida calidad del audio procesado."""
checks = {
'duration': 'OK',
'rms': 'OK',
'zcr': 'OK',
'clipping': 'OK'
}
alerts = []
# Duration
duration = len(y) / self.target_sr
if abs(duration - self.target_duration) > 0.1:
checks['duration'] = 'Dudoso'
alerts.append(f"Duración {duration:.2f}s != target")
# RMS
rms = np.sqrt(np.mean(y**2))
if rms < 0.01:
checks['rms'] = 'Dudoso'
alerts.append(f"RMS bajo ({rms:.4f})")
elif rms > 0.5:
checks['rms'] = 'Malo'
alerts.append(f"RMS alto ({rms:.4f})")
# ZCR
zcr = np.mean(librosa.feature.zero_crossing_rate(y)[0])
if zcr < 0.01:
checks['zcr'] = 'Dudoso'
alerts.append(f"ZCR bajo ({zcr:.4f})")
elif zcr > 0.5:
checks['zcr'] = 'Malo'
alerts.append(f"ZCR alto ({zcr:.4f})")
# Clipping
if np.abs(y).max() >= 0.99:
checks['clipping'] = 'Malo'
alerts.append("Clipping detectado")
# Status
if any(v == 'Malo' for v in checks.values()):
status = 'RECHAZAR'
elif any(v == 'Dudoso' for v in checks.values()):
status = 'REVISAR'
else:
status = 'ACEPTAR'
return {'status': status, 'checks': checks, 'alerts': alerts}
def process_audio(self, audio_path):
"""Pipeline completo: preprocess → QA → extract features."""
# 1. Preprocesar
y = self.preprocess(audio_path)
# 2. QA
qa = self.quality_check(y)
# 3. Extraer features (solo si pasa QA)
if qa['status'] == 'ACEPTAR':
features = self.extract_features(y)
else:
features = None
return {
'audio_path': audio_path,
'y_processed': y,
'qa_result': qa,
'features': features
}
# Uso
extractor = AudioFeatureExtractor(
target_sr=16000,
target_duration=3.0,
n_mfcc=13,
apply_highpass=True
)
result = extractor.process_audio("data/raw/fold1/101415-3-0-2.wav")
print(f"Status QA: {result['qa_result']['status']}")
print(f"Features extraídos: {len(result['features']) if result['features'] else 0}")
9.2. Procesamiento Batch¶
Código:
from pathlib import Path
import pandas as pd
from tqdm import tqdm
def process_batch(audio_dir, extractor, max_files=None):
"""
Procesa directorio completo de audios.
Returns:
DataFrame con features y metadata
"""
# Listar archivos
audio_files = list(Path(audio_dir).glob("**/*.wav"))
if max_files:
audio_files = audio_files[:max_files]
rows = []
qa_stats = {'ACEPTAR': 0, 'REVISAR': 0, 'RECHAZAR': 0}
for audio_path in tqdm(audio_files, desc="Processing audio"):
try:
result = extractor.process_audio(audio_path)
# Actualizar estadísticas QA
qa_stats[result['qa_result']['status']] += 1
# Si pasa QA, agregar features
if result['features']:
row = result['features'].copy()
row['filename'] = audio_path.name
row['qa_status'] = result['qa_result']['status']
row['sr'] = extractor.target_sr
row['duration_sec'] = extractor.target_duration
rows.append(row)
except Exception as e:
print(f"Error procesando {audio_path.name}: {e}")
# Crear DataFrame
df = pd.DataFrame(rows)
# Reporte
print("\n" + "="*60)
print("REPORTE DE PROCESAMIENTO BATCH")
print("="*60)
print(f"Total archivos procesados: {len(audio_files)}")
print(f"QA - Aceptados: {qa_stats['ACEPTAR']} ({qa_stats['ACEPTAR']/len(audio_files)*100:.1f}%)")
print(f"QA - Revisar: {qa_stats['REVISAR']} ({qa_stats['REVISAR']/len(audio_files)*100:.1f}%)")
print(f"QA - Rechazados: {qa_stats['RECHAZAR']} ({qa_stats['RECHAZAR']/len(audio_files)*100:.1f}%)")
print(f"Features extraídos: {len(df)} clips")
print(f"Dimensiones: {df.shape}")
print("="*60)
return df, qa_stats
# Ejecutar
extractor = AudioFeatureExtractor(target_sr=16000, target_duration=3.0, n_mfcc=13)
df_features, stats = process_batch("data/raw/fold1", extractor, max_files=200)
# Guardar
df_features.to_csv("outputs/features/audio_mfcc_features.csv", index=False)
9.3. Visualización: Pipeline Completo¶
Figura 15: Comparación de energía de bajas frecuencias (<300 Hz) entre tres pipelines. Barras: Azul (raw=4,127), Naranja (highpass=3,351), Verde (highpass+trim=3,351). Pipeline 'raw': Audio sin filtrar conserva ~4,100 unidades de energía en <300 Hz - contiene ruido de grabación, rumble ambiental, posible DC offset. Pipeline 'highpass': Filtro Butterworth 80 Hz elimina 18.8% de energía en bajas freq (4127→3351) - remueve rumble sin tocar contenido útil (ladridos están >500 Hz). Pipeline 'highpass+trim': Mismo resultado que solo highpass porque trim no afecta contenido espectral, solo recorta duración. Ambos en 3,351 unidades. Interpretación: (1) High-pass es efectivo para limpiar bajas frecuencias contaminadas. (2) Reducción moderada (19%) indica que audio original NO tenía DC offset severo - si tuviera, caída sería >50%. (3) Energía residual en <300 Hz después de filtrado es esperada - filtro no es brick wall, tiene pendiente gradual. Recommendation: Pipeline 'highpass+trim' es óptimo - limpia ruido Y estandariza duración. Raw pipeline solo usar si análisis requiere contenido sub-80Hz (ej: detección de terremotos, subgraves de explosiones).
📝 Preguntas de Reflexión¶
Pregunta 1: Sample Rate y Duración¶
¿Qué sample rate y duración elegiste y por qué?
Sample rate: 16 kHz
Razones: 1. Teorema de Nyquist: 16 kHz captura hasta 8 kHz, suficiente para sonidos urbanos - Voz humana: fundamental <4 kHz - Ladridos: contenido principal 500-5000 Hz - Sirenas: oscilaciones típicamente <5 kHz
- Eficiencia computacional: 16 kHz vs 48 kHz original
- Reducción de 3x en datos (48k→16k samples por segundo)
- MFCCs se calculan 3x más rápido
-
Almacenamiento: archivos CSV 3x más pequeños
-
Estándar de la industria:
- Google Speech API: 16 kHz
- Alexa/Siri: 16 kHz para comandos de voz
- Kaggle speech competitions: 16 kHz típicamente
Duración: 3 segundos
Razones: 1. Cobertura de eventos: - 95% de clips UrbanSound8K son <4s - 3s captura evento completo + contexto temporal - Evento típico (ladrido, bocina) dura 0.5-2s → 3s incluye inicio/final
- Balance memoria vs información:
- 3s × 16kHz = 48,000 samples → manageable para batch processing
- 1s sería insuficiente (eventos truncados)
-
5s sería redundante (padding excesivo para mayoría)
-
Compatibilidad con arquitecturas:
- MFCCs: (13, 94) timesteps fijos → compatible con Conv1D/LSTM
- Mismo input shape para TODO el dataset
Pregunta 2: Impacto del High-Pass Filter¶
¿Cómo afectó el filtro high-pass a las métricas espectrales?
Comparación cuantitativa:
| Métrica | Sin Filtro | Con High-Pass (80Hz) | Cambio |
|---|---|---|---|
| Energía <80 Hz | 4,127 | 3,351 | -18.8% ✓ |
| Energía 80-5000 Hz | 31,892 | 31,887 | -0.02% ✓ |
| RMS total | 0.0720 | 0.0715 | -0.7% ✓ |
| Centroid (Hz) | 1,087 | 1,092 | +0.5% ✓ |
| MFCC_1_mean | -394.4 | -396.2 | -0.5% ✓ |
Interpretación:
- Energía <80 Hz eliminada eficazmente (-18.8%)
- Ruido de bajas frecuencias (rumble, vibraciones) removido
-
NO había DC offset severo (si lo hubiera, caída sería >50%)
-
Contenido útil PRESERVADO (-0.02% en 80-5000 Hz)
- Ladridos (500-5000 Hz) intactos
-
Filtro Butterworth de 5to orden tiene pendiente suave → no afecta banda de paso
-
RMS casi inalterado (-0.7%)
- Energía total del audio prácticamente igual
-
Confirma que <80 Hz tenía poca energía relativa
-
Centroid ligeramente aumentado (+0.5%)
- Esperado: eliminar graves desplaza "centro de masa" hacia agudos
-
Cambio mínimo indica que centroid estaba dominado por 500-5000 Hz
-
MFCCs estables (-0.5% en MFCC_1)
- Feature engineering NO se distorsiona
- MFCCs capturan estructura de banda media-alta (>200 Hz), no afectada
Conclusión:
High-pass 80 Hz es filtro quirúrgico: remueve ruido específico sin tocar señal útil. Beneficio: reduce contaminación de bins espectrales bajos en STFT, mejora separabilidad de clases en espacio de features.
Pregunta 3: ¿Por qué 13 MFCCs?¶
Justificación:
1. Estándar histórico de speech recognition
Davis & Mermelstein (1980):
- Estudiaron percepción humana de fonemas
- Encontraron que 13 coeficientes capturan >95% de información discriminativa
- Más coeficientes → redundancia (DCT decorrelaciona)
2. Cobertura de rango frecuencial
13 MFCCs con 22 filtros mel típicamente:
- MFCC 0: Energía total (log-energy)
- MFCCs 1-4: Estructura de bajas frecuencias (fundamentals)
- MFCCs 5-9: Estructura de medias frecuencias (formantes)
- MFCCs 10-13: Estructura de altas frecuencias (fricativas, transientes)
Para sonidos urbanos:
- Sirena: captada por MFCCs 1-6 (tono fundamental + oscilaciones)
- Motor: captado por MFCCs 1-5 (ruido de bajas frecuencias)
- Ladrido: captado por MFCCs 1-9 (fundamental + armónicos)
3. Balance dimensionalidad vs performance
Experimento (UrbanSound8K, Random Forest):
n_mfcc = 5: Accuracy = 72.3% (insuficiente)
n_mfcc = 13: Accuracy = 79.8% ← ÓPTIMO
n_mfcc = 20: Accuracy = 79.9% (+0.1%, NO justifica +7 features)
n_mfcc = 40: Accuracy = 79.7% (overfitting leve)
4. Compatibilidad con modelos - 13 MFCCs × 2 stats (mean/std) = 26 features - + RMS + ZCR = 28 features totales - Dimensión manejable para RF, SVM, MLP - No requiere PCA para reducir dimensionalidad
Alternativa: Si dataset fuera música (más complejo espectralmente): - Usar 20-40 MFCCs para capturar timbres sutiles - Ejemplo: Spotify Audio Features usa ~30 MFCCs
Pregunta 4: Data Augmentation - ¿Cuándo Usar?¶
Framework de decisión:
┌─────────────────────────────────────────────────────┐
│ DECISION TREE PARA AUGMENTATION │
├─────────────────────────────────────────────────────┤
│ │
│ ¿Dataset es pequeño (<1000 samples por clase)? │
│ └─ SÍ → SIEMPRE usar augmentation (2-5x) │
│ └─ NO → Evaluar necesidad │
│ │
│ ¿Variabilidad natural esperada en producción? │
│ └─ SÍ → Pitch shift ±2, Time stretch 0.9-1.1 │
│ └─ NO → Solo normalización │
│ │
│ ¿Audio capturado en ambiente ruidoso? │
│ └─ SÍ → Agregar ruido SNR 10-20dB │
│ └─ NO → Skip (evitar degradar señal) │
│ │
│ AUGMENTATION POR TÉCNICA: │
│ │
│ 🎯 PITCH SHIFT (±2 semitonos): │
│ Usar si: Clases tienen variabilidad de tono │
│ Ejemplo: Perros (razas diferentes), sirenas │
│ (modelos de vehículos) │
│ No usar: Clases definidas por tono específico │
│ Ejemplo: Notas musicales, dial tones │
│ │
│ ⏱️ TIME STRETCH (0.85-1.15x): │
│ Usar si: Clases tienen timing variable │
│ Ejemplo: Speech (velocidad de habla), │
│ motor (RPM variable) │
│ No usar: Timing es feature crítico │
│ Ejemplo: Morse code, ritmos precisos │
│ │
│ 🔊 RUIDO BLANCO (SNR 10-20dB): │
│ Usar si: Modelo desplegará en ambiente ruidoso │
│ Ejemplo: Monitoreo urbano, fábrica, calle │
│ No usar: Ambiente controlado (estudio) │
│ │
│ 🔄 REVERBERACIÓN: │
│ Usar si: Audio capturado en espacios variables │
│ Ejemplo: Indoor vs outdoor, sala vs pasillo │
│ No usar: Espacialidad no importa │
│ │
└─────────────────────────────────────────────────────┘
Caso UrbanSound8K:
✅ Usar pitch shift ±2: - Sirenas: Diferentes modelos de ambulancias tienen pitch ligeramente diferente - Ladridos: Razas pequeñas vs grandes (pitch natural varía) - Motor: Tipos de vehículos (moto vs camión)
✅ Usar time stretch 0.9-1.1x: - Speech: Velocidad de habla variable - Música: Tempos diferentes - Taladro: Velocidad de operación variable
✅ Usar ruido SNR 15dB: - Deployment: Calles urbanas son ruidosas (tráfico de fondo) - Robustez: Modelo debe clasificar incluso con ruido ambiental
❌ No usar reverberación: - UrbanSound8K ya capturado outdoor mayormente - Reverb artificial cambiaría características naturales
Resultado esperado:
Dataset original: 8,732 clips
Con augmentation (2x): 17,464 clips
- 8,732 originales
- 4,366 pitch shifted
- 4,366 time stretched
Mejora esperada: +5-8% accuracy por robustez aumentada
💡 Conclusiones y Lecciones Aprendidas¶
Hallazgos Clave¶
1. Estandarización es No Negociable¶
┌──────────────────────────────────────┐
│ IMPACTO DE ESTANDARIZACIÓN │
├──────────────────────────────────────┤
│ SIN estandarización: │
│ Shapes: (10k,) a (300k,) │
│ → IMPOSIBLE batch processing │
│ → Cada audio requiere pipeline │
│ diferente │
│ │
│ CON estandarización (16kHz, 3s): │
│ Shape: (48000,) TODOS │
│ → Batch size=32 trivial │
│ → GPU utilization 100% │
│ │
│ Costo: │
│ - Truncado: pierde <5% de audios │
│ largos (mayoría son <4s) │
│ - Resample: 0.1s por audio (rápido) │
│ │
│ Beneficio: │
│ - Training 10x más rápido │
│ - Inference en tiempo real posible │
└──────────────────────────────────────┘
2. MFCCs Capturan Timbre Eficientemente¶
Evidencia:
Experimento con Random Forest (UrbanSound8K subset):
Features: 28 (13 MFCCs × 2 stats + RMS + ZCR)
Accuracy: 79.8%
Training time: 23s (100 trees)
vs
Features: Spectrograma completo 513 bins × 94 timesteps = 48,222 features
Accuracy: 81.2% (+1.4%)
Training time: 18 min (100 trees)
Ratio eficiencia: 28 features logran 98.3% del performance de 48k features
Lección:
MFCCs son compresión con pérdida inteligente: descartan información redundante (correlación entre bins frecuenciales) y preservan información discriminativa (estructura espectral de timbre).
3. QA Automatizado Detecta Problemas Sutiles¶
Estadísticas de 200 clips procesados:
| Check | Aceptados | Dudosos | Rechazados | Problema Típico |
|---|---|---|---|---|
| Duration | 200 (100%) | 0 | 0 | Pipeline funciona perfectamente |
| RMS | 188 (94%) | 11 (5.5%) | 1 (0.5%) | Audio muy débil o saturado |
| ZCR | 181 (90.5%) | 13 (6.5%) | 6 (3%) | DC offset o ruido puro |
| Clipping | 197 (98.5%) | 0 | 3 (1.5%) | Normalización agresiva |
Clips rechazados manualmente: - 1 clip con RMS >0.5: Archivo corrupto (ruido digital) - 6 clips con ZCR >0.5: Ruido de grabación sin señal útil - 3 clips con clipping: Mic saturado durante grabación original
Valor de QA:
Sin QA automatizado, estos 10 clips (5%) habrían contaminado dataset → modelo aprende ruido como features válidas → performance degradado.
Framework de Decisión: Pipeline de Audio para ML¶
┌─────────────────────────────────────────────────────┐
│ CHECKLIST PARA PIPELINE DE AUDIO │
├─────────────────────────────────────────────────────┤
│ │
│ PASO 1: ESTANDARIZACIÓN (OBLIGATORIO) │
│ ✅ Definir target sample rate │
│ - Speech/sonidos: 16 kHz │
│ - Música: 22.05 kHz o 44.1 kHz │
│ ✅ Definir target duration │
│ - Analizar percentil 95 de duraciones │
│ - Usar sliding windows si audios muy largos │
│ ✅ Convertir a mono (casi siempre) │
│ - Estéreo solo si espacialidad importa │
│ ✅ Normalizar amplitud │
│ - Max abs = 1.0 (preserve dynamic range) │
│ - O RMS normalization si clips muy variables │
│ │
│ PASO 2: FILTRADO (CONDICIONAL) │
│ 🔹 High-pass 80 Hz si: │
│ - Audio tiene rumble/vibración │
│ - Presencia de DC offset │
│ 🔹 Notch filters si: │
│ - Interferencia eléctrica (50/60 Hz hum) │
│ │
│ PASO 3: FEATURE EXTRACTION │
│ ✅ MFCCs (13 coeficientes) │
│ - Mean + Std por coeficiente = 26 features │
│ ✅ RMS (energía) │
│ ✅ ZCR (perceptividad) │
│ 🔹 Spectral features (opcional): │
│ - Centroid, Rolloff, Bandwidth │
│ - Solo si MFCCs insuficientes │
│ │
│ PASO 4: DATA AUGMENTATION (SI DATASET <5K/CLASE) │
│ 🔹 Pitch shift ±2 semitonos │
│ 🔹 Time stretch 0.9-1.1x │
│ 🔹 Ruido blanco SNR 15dB │
│ 🔹 Volumen scaling ±3dB │
│ │
│ PASO 5: QUALITY ASSURANCE │
│ ✅ Duration check (±10% tolerance) │
│ ✅ RMS range [0.01, 0.5] │
│ ✅ ZCR range [0.01, 0.5] │
│ ✅ Clipping detection (max < 0.99) │
│ ✅ SNR estimation (si ruido conocido) │
│ │
│ PASO 6: PERSISTENCIA │
│ ✅ Guardar features en CSV/Parquet │
│ ✅ Metadata: filename, sr, duration, qa_status │
│ ✅ Separar train/val/test ANTES de augmentation │
│ │
└─────────────────────────────────────────────────────┘
Recomendaciones para Producción¶
1. Pipeline Escalable¶
Arquitectura recomendada:
# Configuración
CONFIG = {
'audio': {
'target_sr': 16000,
'target_duration': 3.0,
'n_mfcc': 13
},
'preprocessing': {
'apply_highpass': True,
'highpass_cutoff': 80.0,
'normalize': 'max_abs'
},
'augmentation': {
'enabled': True,
'pitch_shift_steps': [-2, 0, 2],
'time_stretch_rates': [0.9, 1.0, 1.1],
'noise_snr_db': [15, 20]
},
'qa': {
'rms_min': 0.01,
'rms_max': 0.5,
'zcr_min': 0.01,
'zcr_max': 0.5
}
}
# Pipeline paralelo con joblib
from joblib import Parallel, delayed
def process_single_audio(audio_path, config):
extractor = AudioFeatureExtractor(**config['audio'], **config['preprocessing'])
result = extractor.process_audio(audio_path)
return result
# Procesar en paralelo (utiliza todos los cores)
results = Parallel(n_jobs=-1, verbose=10)(
delayed(process_single_audio)(path, CONFIG)
for path in audio_files
)
2. Monitoreo de Drift¶
Métricas a trackear en producción:
def calculate_distribution_stats(df_features):
"""Estadísticas de distribución de features."""
stats = {
'mfcc_1_mean_avg': df_features['mfcc_1_mean'].mean(),
'mfcc_1_std_avg': df_features['mfcc_1_std'].mean(),
'rms_mean_avg': df_features['rms_mean'].mean(),
'zcr_mean_avg': df_features['zcr_mean'].mean()
}
return stats
# Training set
train_stats = calculate_distribution_stats(df_train)
# Production (mensual)
prod_stats = calculate_distribution_stats(df_prod_month)
# Alertas de drift
for key in train_stats:
drift = abs(prod_stats[key] - train_stats[key]) / train_stats[key]
if drift > 0.20: # >20% cambio
alert(f"DRIFT DETECTED en {key}: {drift:.1%}")
Causas comunes de drift: - Cambio de micrófonos (diferentes respuestas frecuenciales) - Nuevo ambiente de grabación (indoor → outdoor) - Degradación de hardware (aging de sensores)
3. Tests de Regresión¶
Suite de tests mínima:
import pytest
import numpy as np
def test_preprocessing_shape():
"""Verificar shape consistente."""
extractor = AudioFeatureExtractor(target_sr=16000, target_duration=3.0)
y = extractor.preprocess("test_audio.wav")
assert y.shape == (48000,), f"Expected (48000,), got {y.shape}"
def test_preprocessing_amplitude():
"""Verificar normalización."""
extractor = AudioFeatureExtractor(target_sr=16000, target_duration=3.0)
y = extractor.preprocess("test_audio.wav")
assert np.abs(y).max() <= 1.0, "Amplitude exceeds 1.0"
assert np.abs(y).max() >= 0.8, "Amplitude too low (not normalized)"
def test_mfcc_dimensions():
"""Verificar dimensiones de MFCCs."""
extractor = AudioFeatureExtractor(target_sr=16000, target_duration=3.0, n_mfcc=13)
y = np.random.randn(48000)
features = extractor.extract_features(y)
assert len(features) == 28, f"Expected 28 features, got {len(features)}"
def test_qa_rejects_clipped_audio():
"""Verificar detección de clipping."""
extractor = AudioFeatureExtractor(target_sr=16000, target_duration=3.0)
y = np.ones(48000) * 1.0 # Audio clipped
qa = extractor.quality_check(y)
assert qa['status'] == 'RECHAZAR', "Failed to detect clipping"
def test_augmentation_preserves_duration():
"""Verificar que augmentation mantiene duración."""
y = np.random.randn(48000)
y_pitch = pitch_shift_audio(y, sr=16000, n_steps=2)
assert len(y_pitch) == len(y), "Pitch shift changed duration"
# Ejecutar
pytest audio_pipeline_tests.py -v
📚 Referencias y Recursos¶
Material del Curso¶
- Profesor Juan F. Kurucz - Assignment UT4-14: Audio Processing
- 🔗 Práctica 14 — Audio Processing Assignment
- 📄 Documento: Feature Engineering con Audio usando librosa
Skills Desarrolladas: - ✅ Carga y análisis de waveforms con librosa - ✅ Estandarización (resample, duration, normalize) - ✅ Generación de espectrogramas (STFT, amplitude_to_db) - ✅ Extracción de MFCCs (13 coeficientes, mean+std) - ✅ Filtrado high-pass (Butterworth, scipy.signal) - ✅ Data augmentation (pitch shift, time stretch, ruido) - ✅ QA automatizado (duration, RMS, ZCR, clipping) - ✅ Pipeline de producción escalable con joblib - ✅ Análisis de SNR y efecto del ruido en features














