Principios Fundamentales y Arquitectura
1.1 ¿Qué es el remuestreo de audio?
El remuestreo de audio es el proceso de convertir una señal de audio desde una frecuencia de muestreo de origen (Fs_origen) a una frecuencia de muestreo de destino (Fs_destino). Por ejemplo: convertir voz capturada a 48kHz de un micrófono a 16kHz para su uso en algoritmos de reconocimiento de voz.
1.2 Esencia matemática del remuestreo
Señal original x[n] (Fs_origen) → Interpolación/Submuestreo → Nueva señal y[m] (Fs_destino)
1.3 Dos métodos de remuestreo
| Tipo | Fórmula | Características | Casos de uso |
|---|---|---|---|
| Remuestreo de múltiplo entero | Fs_destino = L × Fs_origen | Simple y eficiente | Procesamiento en tiempo real en MCUs |
| Remuestreo de fracción | Fs_destino = (L/M) × Fs_origen | Flexible y general | PC/Embebido de uso general |
Implementación de Remuestreo de Múltiplo Entero en STM32 (Recomendado)
2.1 Arquitectura del sistema
Entrada de audio/micrófono
↓
Filtro anti-aliasing (FIR)
↓
Interpolación (relleno de ceros)
↓
Filtro pasa-bajo (FIR)
↓
Submuestreo
↓
Salida a DAC/algoritmo
2.2 Implementación completa del código fuente
(1) Archivo de encabezado (audio_remuestreo.h)
/**
* @file audio_remuestreo.h
* @brief Biblioteca de remuestreo de audio para STM32 (múltiplo entero)
*/
#ifndef __AUDIO_REMUESTREO_H
#define __AUDIO_REMUESTREO_H
#include <stdint.h>
#include <stdbool.h>
/* Configuración de remuestreo */
#define FACTOR_MAXIMO_REMUESTREO 8 // Factor máximo de interpolación
#define ORDEN_FIR 33 // Orden del filtro FIR (impar)
#define TAMANO_BUFFER_AUDIO 256 // Tamaño del buffer de audio
/* Estructura de estado de remuestreo */
typedef struct {
uint8_t factor_interpolacion; // Factor de interpolación L
uint8_t factor_submuestreo; // Factor de submuestreo M
int16_t coeficientes_fir[ORDEN_FIR]; // Coeficientes del filtro FIR
int32_t historico[ORDEN_FIR]; // Caché de muestras históricas
uint8_t indice_historico; // Índice de caché histórico
uint32_t muestras_entrada; // Contador de muestras de entrada
uint32_t muestras_salida; // Contador de muestras de salida
bool inicializado; // Bandera de inicialización
} EstadoRemuestreo;
/* Declaración de funciones */
void IniciarRemuestreo(EstadoRemuestreo* er, uint8_t L, uint8_t M);
int16_t ProcesarMuestra(EstadoRemuestreo* er, int16_t muestra_entrada);
void ProcesarBloque(EstadoRemuestreo* er,
int16_t* entrada, uint16_t longitud_entrada,
int16_t* salida, uint16_t* longitud_salida);
void ReiniciarRemuestreo(EstadoRemuestreo* er);
#endif /* __AUDIO_REMUESTREO_H */
(2) Generación de coeficientes del filtro FIR (audio_remuestreo.c)
/**
* @file audio_remuestreo.c
* @brief Implementación de remuestreo de audio (basado en filtro FIR)
*/
#include "audio_remuestreo.h"
#include <string.h>
#include <math.h>
/* Generación de coeficientes FIR pasa-bajo (ventana Hamming) */
static void GenerarCoeficientesFIR(int16_t* coef, uint8_t orden, float frecuencia_corte)
{
float suma = 0.0f;
for (int i = 0; i < orden; i++) {
float n = (float)i - (orden - 1) / 2.0f;
float sinc = (n == 0) ? 1.0f : sinf(2.0f * M_PI * frecuencia_corte * n) / (2.0f * M_PI * frecuencia_corte * n);
float ventana = 0.54f - 0.46f * cosf(2.0f * M_PI * i / (orden - 1)); // Ventana Hamming
coef[i] = (int16_t)(sinc * ventana * 32767.0f);
suma += coef[i];
}
// Normalización
for (int i = 0; i < orden; i++) {
coef[i] = (int16_t)((float)coef[i] * 32767.0f / suma);
}
}
/* Inicializador del remuestreador */
void IniciarRemuestreo(EstadoRemuestreo* er, uint8_t L, uint8_t M)
{
if (er == NULL || L == 0 || M == 0) return;
er->factor_interpolacion = L;
er->factor_submuestreo = M;
// Generación de coeficientes FIR
float frecuencia_corte = 0.5f / (L > M ? L : M); // Frecuencia de corte
GenerarCoeficientesFIR(er->coeficientes_fir, ORDEN_FIR, frecuencia_corte);
// Inicialización del caché histórico
memset(er->historico, 0, sizeof(er->historico));
er->indice_historico = 0;
er->muestras_entrada = 0;
er->muestras_salida = 0;
er->inicializado = true;
}
/* Procesamiento de una sola muestra de remuestreo */
int16_t ProcesarMuestra(EstadoRemuestreo* er, int16_t muestra_entrada)
{
if (!er->inicializado) return 0;
int32_t acumulador = 0;
int16_t muestra_salida = 0;
// 1. Almacenar la muestra de entrada en el caché histórico
er->historico[er->indice_historico] = muestra_entrada;
er->indice_historico = (er->indice_historico + 1) % ORDEN_FIR;
// 2. Interpolación (relleno de ceros)
static uint8_t fase = 0;
if (fase == 0) {
// 3. Filtrado FIR
uint8_t hist_idx = er->indice_historico;
for (int i = 0; i < ORDEN_FIR; i++) {
hist_idx = (hist_idx == 0) ? ORDEN_FIR - 1 : hist_idx - 1;
acumulador += (int32_t)er->historico[hist_idx] * er->coeficientes_fir[i];
}
muestra_salida = (int16_t)(acumulador >> 15); // Desplazar 15 bits para restaurar escala
}
// 4. Submuestreo
fase++;
if (fase >= er->factor_interpolacion) {
fase = 0;
}
er->muestras_entrada++;
if (fase == 0) {
er->muestras_salida++;
return muestra_salida;
}
return 0; // Devuelve 0 en momentos no de salida
}
/* Procesamiento de bloque de remuestreo */
void ProcesarBloque(EstadoRemuestreo* er,
int16_t* entrada, uint16_t longitud_entrada,
int16_t* salida, uint16_t* longitud_salida)
{
uint16_t idx_salida = 0;
for (uint16_t i = 0; i < longitud_entrada; i++) {
int16_t muestra = ProcesarMuestra(er, entrada[i]);
if (muestra != 0) { // No cero indica salida válida
if (idx_salida < *longitud_salida) {
salida[idx_salida++] = muestra;
}
}
}
*longitud_salida = idx_salida;
}
/* Reinicio del estado del remuestreador */
void ReiniciarRemuestreo(EstadoRemuestreo* er)
{
memset(er->historico, 0, sizeof(er->historico));
er->indice_historico = 0;
er->muestras_entrada = 0;
er->muestras_salida = 0;
}
(3) Ejemplo de prograam principal (main.c)
/**
* @file main.c
* @brief Ejemplo de remuestreo de audio para STM32
*/
#include "stm32f10x.h"
#include "audio_remuestreo.h"
#include "i2s.h"
#include "dma.h"
/* Variables globales */
EstadoRemuestreo remuestreador;
int16_t audio_entrada[TAMANO_BUFFER_AUDIO];
int16_t audio_salida[TAMANO_BUFFER_AUDIO * 4]; // Buffer 4 veces más grande
uint16_t longitud_salida = 0;
int main(void)
{
/* 1. Inicialización del sistema */
InicializarSistema();
InicializarI2S();
InicializarDMA();
/* 2. Inicializar el remuestreador (48kHz → 16kHz, L=1, M=3) */
IniciarRemuestreo(&remuestreador, 1, 3); // 48/3 = 16kHz
/* 3. Bucle principal */
while (1) {
/* 3.1 Esperar a que la recepción I2S se complete */
if (RecepcionI2SCompleta()) {
/* 3.2 Obtener datos de audio */
ObtenerDatosI2S(audio_entrada, TAMANO_BUFFER_AUDIO);
/* 3.3 Procesamiento de remuestreo */
uint16_t maxima_salida = TAMANO_BUFFER_AUDIO * 4;
ProcesarBloque(&remuestreador,
audio_entrada, TAMANO_BUFFER_AUDIO,
audio_salida, &maxima_salida);
longitud_salida = maxima_salida;
/* 3.4 Enviar datos procesados al algoritmo de reconocimiento de voz */
ProcesarVoz(audio_salida, longitud_salida);
}
}
}
Remuestreo de Fracción en PC/Linux (General)
3.1 Implementación ligera basada en interpolación lineal
Adecuado para sistemas con recursos limitados, calidad de audio aceptable.
/**
* @file interpolacion_lineal.c
* @brief Remuestreo por interpolación lineal (fracción)
*/
#include <stdio.h>
#include <stdlib.h>
#include <stdint.h>
#include <string.h>
#include <math.h>
/* Remuestreo por interpolación lineal */
uint32_t InterpolacionLineal(int16_t* entrada, uint32_t longitud_entrada,
int16_t* salida, uint32_t capacidad_salida,
uint32_t fs_entrada, uint32_t fs_salida)
{
if (fs_entrada == fs_salida) {
uint32_t longitud_copia = (longitud_entrada < capacidad_salida) ? longitud_entrada : capacidad_salida;
memcpy(salida, entrada, longitud_copia * sizeof(int16_t));
return longitud_copia;
}
double ratio = (double)fs_salida / fs_entrada;
uint32_t longitud_salida = (uint32_t)(longitud_entrada * ratio);
if (longitud_salida > capacidad_salida) {
longitud_salida = capacidad_salida;
}
for (uint32_t i = 0; i < longitud_salida; i++) {
double indice_origen = i / ratio;
uint32_t indice_bajo = (uint32_t)indice_origen;
uint32_t indice_alto = indice_bajo + 1;
double fraccion = indice_origen - indice_bajo;
if (indice_alto >= longitud_entrada) {
salida[i] = entrada[indice_bajo];
} else {
// Interpolación lineal: y = y0 + (y1 - y0) * fraccion
salida[i] = (int16_t)(entrada[indice_bajo] +
(entrada[indice_alto] - entrada[indice_bajo]) * fraccion);
}
}
return longitud_salida;
}
/* Ejemplo de prueba */
int main()
{
int16_t entrada[10] = {0, 1000, 2000, 3000, 4000, 5000, 6000, 7000, 8000, 9000};
int16_t salida[20];
uint32_t longitud_salida = InterpolacionLineal(entrada, 10, salida, 20, 48000, 16000);
printf("Salida Remuestreada (%d muestras):\n", longitud_salida);
for (uint32_t i = 0; i < longitud_salida; i++) {
printf("%d ", salida[i]);
}
printf("\n");
return 0;
}
3.2 Remuestreo de alta calidad basado en SpeexDSP
Esta es la biblioteca de remuestreo de alta calidad comúnmente utilizada en la industria.
/**
* @file speex_remuestreo.c
* @brief Ejemplo de remuestreo usando la biblioteca SpeexDSP
*/
#include <speex/speex_resampler.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#define TAMANO_MUESTRA 160
int main()
{
SpeexResamplerState *remuestreador;
int err;
spx_uint32_t cuadros_entrada = TAMANO_MUESTRA;
spx_uint32_t cuadros_salida = TAMANO_MUESTRA;
spx_uint32_t cuadros_entrada_usados, cuadros_salida_generados;
short buffer_entrada[TAMANO_MUESTRA];
short buffer_salida[TAMANO_MUESTRA * 2]; // Buffer ampliado
// 1. Inicializar el remuestreador (mono, 48kHz → 16kHz)
remuestreador = speex_resampler_init(1, 48000, 16000, 10, &err);
if (err != 0) {
printf("Fallo en la inicialización del remuestreador: %d\n", err);
return -1;
}
// 2. Establecer la calidad de remuestreo (0-10, cuanto mayor mejor)
speex_resampler_set_quality(remuestreador, 10);
// 3. Procesar datos de audio
while (1) {
// Leer un cuadro de datos de audio
if (leer_cuadro_audio(buffer_entrada, TAMANO_MUESTRA) <= 0) break;
// Realizar el remuestreo
speex_resampler_process_int(remuestreador, 0,
buffer_entrada, &cuadros_entrada,
buffer_salida, &cuadros_salida);
// Salida de datos remuestreados
escribir_cuadro_audio(buffer_salida, cuadros_salida);
}
// 4. Liberar recursos
speex_resampler_destroy(remuestreador);
return 0;
}
Sugerencias de Optimización de Rendimiento
4.1 Optimización algorítmica
/* Optimización de punto fijo (evitar punto flotante) */
typedef struct {
int32_t coeficientes_fir_punto_fijo[ORDEN_FIR]; // Formato Q15
int64_t acumulador; // Acumulador de 64 bits
} RemuestreadorRapido;
/* Optimización con instrucciones SIMD (ARM NEON) */
#ifdef __ARM_NEON
#include <arm_neon.h>
void FiltradoFIR_NEON(int16_t* entrada, int16_t* coef, int16_t* salida, int longitud)
{
int16x8_t vec_entrada, vec_coef;
int32x4_t acum_bajo, acum_alto;
for (int i = 0; i < longitud; i += 8) {
vec_entrada = vld1q_s16(entrada + i);
vec_coef = vld1q_s16(coef);
acum_bajo = vmull_s16(vget_low_s16(vec_entrada), vget_low_s16(vec_coef));
acum_alto = vmull_s16(vget_high_s16(vec_entrada), vget_high_s16(vec_coef));
// ... Procesamiento de acumulación
}
}
#endif
4.2 Optimización de memoria
/* Buffer circular en lugar de buffer lineal */
typedef struct {
int16_t buffer[TAMANO_BUFFER_AUDIO];
uint16_t ptr_escritura;
uint16_t ptr_lectura;
uint16_t cuenta;
} BufferCircular;
/* Remuestreo in situ (evitar copia adicional) */
void RemuestreoInSitu(int16_t* buffer, uint32_t longitud, uint32_t factor)
{
// Procesar de atrás hacia adelante para evitar sobrescribir datos no procesados
for (int i = longitud - 1; i >= 0; i--) {
buffer[i * factor] = buffer[i];
for (int j = 1; j < factor; j++) {
buffer[i * factor + j] = 0; // Rellenar con ceros para interpolación
}
}
}
Pruebas y Verificación
5.1 Código de prueba
/* Prueba de calidad de remuestreo */
void PruebaCalidadRemuestreo(void)
{
int16_t onda_seno[1000];
int16_t remuestreado[3000];
uint32_t longitud_salida;
// Generar onda seno de 1kHz (frecuencia de muestreo 48kHz)
for (int i = 0; i < 1000; i++) {
onda_seno[i] = (int16_t)(32767 * sin(2 * M_PI * 1000 * i / 48000.0));
}
// Remuestrear a 16kHz
EstadoRemuestreo er;
IniciarRemuestreo(&er, 1, 3); // 48kHz → 16kHz
ProcesarBloque(&er, onda_seno, 1000, remuestreado, &longitud_salida);
printf("Muestras de entrada: 1000, Muestras de salida: %d\n", longitud_salida);
// Verificar si la frecuencia es correcta
// Teóricamente, la salida debería ser una onda seno de 1kHz con 333 muestras
}
5.2 Rendimiento de referencia
| Algoritmo | Complejidad | Latencia | Calidad de audio | Casos de uso |
|---|---|---|---|---|
| Vecino más cercano | O(n) | 0 | Baja | Prototipado rápido |
| Interpolación lineal | O(n) | Baja | Media | Embebido en tiempo real |
| Filtrado FIR | O(n×orden) | Media | Buena | Audio general |
| SpeexDSP | O(n×log n) | Alta | Excelente | Audio profesional |
Solución de Problemas Comunes
6.1 Manejo de anti-aliasing
/* Se debe agregar un filtro anti-aliasing antes del remuestreo */
void FiltradoAntiAlias(int16_t* entrada, uint32_t longitud, uint32_t fs_salida)
{
// Frecuencia de corte establecida en fs_salida/2
float frecuencia_corte = fs_salida / 2.0f;
// Usar filtro FIR o IIR
}
6.2 Distorsión de fase
/* Usar filtro FIR de fase lineal para evitar distorsión de fase */
#define FIR_FASE_LINEAL 1
#define FIR_FASE_MINIMA 0
6.3 Desbordamiento de buffer
/* Calcular tamaño seguro del buffer de salida */
uint32_t TamanoSalidaSeguro(uint32_t longitud_entrada, uint32_t fs_entrada, uint32_t fs_salida)
{
return (uint32_t)ceil((double)longitud_entrada * fs_salida / fs_entrada) + 10;
}