Sintetizador de audio digital (sustractivo)
Sintetizador sustractivo monofónico en STM32F746 Discovery: oscilador wavetable, filtro de variables de estado, envolvente ADSR y LFO calculados a 44,1 kHz dentro de la interrupción del códec y tocados desde un teclado USB MIDI.
Proyecto académico (ENIB, 2023).
- STM32F746
- C
- USB MIDI host
- CMSIS-DSP
- WM8994
- SVF
- ADSR
- Wavetable
- Python/SciPy
El objetivo y el punto de partida
El objetivo era un sintetizador chico que se pueda tocar: un teclado USB MIDI conectado a una placa STM32F746 Discovery, y el sonido saliendo del códec de audio de la placa. El sonido se arma de forma sustractiva: un oscilador genera una forma de onda brillante, un filtro le saca una parte y una envolvente da forma a cómo empieza y se apaga.
Partimos de un proyecto base que dio la materia. Ya traía la configuración de la placa y del códec (del kit educativo de DSP de ARM), una clase USB MIDI host de Xavier Halgand, un oscilador básico y un esqueleto casi vacío de envolvente. Lo que escribí yo es todo lo que lo convierte en sintetizador: las funciones del oscilador, el filtro de variables de estado, la envolvente ADSR, el LFO, la correspondencia de MIDI a sonido y la integración de todo en la interrupción de audio.
Arquitectura
Cada muestra de audio se calcula dentro de la interrupción del códec WM8994 a 44,1 kHz en un STM32F746 a 200 MHz. Los paquetes USB MIDI host (note on/off, control change, program change) se analizan en el bucle principal. Cadena: oscilador seno (wavetable de 1024 puntos), diente de sierra o cuadrada; un LFO (de 1 a 20 Hz aproximadamente) sumado a la frecuencia de corte del filtro; un filtro de variables de estado con corte y resonancia acotados dentro de su límite de estabilidad; y una envolvente ADSR lineal o exponencial. Las salidas izquierda y derecha llevan la misma muestra mono.
Cómo se construyó
El historial de git del proyecto muestra el orden en que fue tomando forma:
- Primero las funciones del oscilador, reescritas, y el primer manejo de MIDI (número de nota a frecuencia).
- Después el filtro de variables de estado, primero como archivos nuevos de filtros y luego funcionando.
- Después la envolvente ADSR. El filtro andaba antes que la envolvente, por cómo esta llevaba el tiempo; la envolvente se terminó más tarde.
- Por último el LFO, la correspondencia de control changes que convierte las perillas del teclado en parámetros, los program changes y la normalización del nivel de los osciladores.
Prototipo en Python y luego C
Las señales se resolvieron primero en la PC con Python y SciPy. Las wavetables (seno, diente de sierra, cuadrada) se generaron en un notebook y se pegaron en C como arreglos. Los osciladores se escribieron en C, se compilaron como librería compartida y se llamaron desde Python con ctypes para compararlos con las formas de onda de referencia de SciPy. El filtro y la envolvente también se esbozaron en Python; para el filtro calculé su respuesta en frecuencia con SciPy y deduje su límite de estabilidad, que pasó a ser un tope sobre la resonancia en el código C. Después se reescribió todo para la placa, donde toda la cadena tiene que terminar dentro de la interrupción de audio.
Las figuras de esta página salen de compilar los propios archivos C del proyecto en una PC y graficar la salida, así que muestran lo que calcula el código del firmware, no lo que sonó en la placa.

El filtro
El filtro es un filtro de variables de estado con salidas pasa-bajos, pasa-banda y pasa-altos; el sintetizador usa el pasa-bajos. Tiene dos coeficientes: uno sale de la frecuencia de corte y otro de la resonancia (Q). El filtro se vuelve inestable si la resonancia es demasiado baja para el corte, así que el código recalcula cada vez un Q mínimo y nunca deja que Q baje de ese valor. Desde el teclado, el corte va de 50 Hz a 10 kHz en escala exponencial y Q llega hasta 10.

La envolvente y el control MIDI
La ADSR es una máquina de estados chica que se actualiza una vez por muestra de audio: reposo, ataque, decay, sustain y release. Las pendientes se recalculan solo cuando cambia un parámetro, así que la interrupción de audio apenas suma un número por muestra. Soltar una tecla solo inicia el release si es la nota que está sonando, de modo que soltar una tecla vieja no corta la nota nueva.
Las notas MIDI se convierten en frecuencias en temperamento igual (La4 = 440 Hz). Los control changes fijan el ataque (5 ms a 15 s), el decay (200 a 500 ms), el nivel de sustain (0,001 a 1), el release (50 ms a 2 s), el corte del filtro, la resonancia, la profundidad del LFO y su velocidad (1 a 20 Hz). Los program changes eligen la forma de onda, el modo de la envolvente y si el LFO está activo.

Problemas conocidos y alcance
Un prototipo funcional, no un instrumento terminado:
- Es monofónico: una nota a la vez.
- El modo exponencial de la envolvente existe en el código pero no funciona bien: al correr el código en una PC la envolvente salta al nivel máximo en lugar de subir progresivamente. Solo el modo lineal se comporta como se espera.
- El LFO suma su salida a la frecuencia de corte del filtro en cada muestra, así que el corte choca contra sus límites (50 Hz y 13 kHz) en lugar de oscilar alrededor del valor fijado. Mis propias notas en el código señalan que el corte suena raro con el LFO activo, y ruido con el seno y el LFO juntos.
- El glide entre notas estaba planeado pero no se hizo.
- El código de audio se compila sin optimización.
- La configuración de FreeRTOS, el código del LCD, los helpers FIR y biquad y una variante de audio por DMA están en el árbol del proyecto pero no forman parte del camino que produce sonido.