Curso Audio
Curso Audio
Sergi Jordà
Music Technology Group (IUA – UPF)
Ocata, 1 (despatx 330)
[email protected]
Conceptos de programación
MIDI y audio en tiempo real
1
Índice
1. Conceptos de programación de audio a
t.real
2. MIDI bajo Windows (ej. PortMIDI)
3. Audio (ej. PortAudio)
Introducción
• Hemos visto como programar en C un sintetizador sencillo a partir de una
tabla con forma de onda, previamente almacenada en memoria
§ La tabla de la función seno que hemos utilizado podría sustituirse por cualquier
otra sin que esto suponga ningún cambio conceptual
• Las novedades que veremos hoy es como hacer para implementar algo
similar en tiempo real
• En este caso, en tiempo real no sólo significa que el sonido se escucha
inmediatamente, sinó que mientras se escucha podemos ir modificando sus
parámetros de forma interactiva
2
Sistema con 1 solo ciclo
• En el caso m ás sencillo, el sistema tiene un único ciclo
• En este ciclo se deben
§ Mirar las entradas
§ Mapearlas y procesarlas
§ Generar y mostrar las salidas
• A nivel de programación esto conlleva un único bucle principal que realiza estas
tareas siempre en el mismo orden
• Se debería garantizar que los procesos de un ciclo se pueden completar en
menos de lo que dura un ciclo (antes del inicio del nuevo ciclo) (τ < h)
• El programa debe también garantizar que no se “adelantará”. Es decir, una vez
terminado el bucle, no deberá comenzar uno nuevo “hasta que toque” (más
adelante veremos varios algoritmos para controlar estos tiempos) (h = cte)
• Muchos sintetizadores de audio por software funcionan as í. A continuación se
describen varios aspectos tomando un programa de este tipo como ejemplo
3
Ejemplo: Sintetizador de audio (1)
• Porqué es importante mantener bien constante la frecuencia del
ciclo ?
§ Aunque no es un sistema “duro” que ponga nada en peligro, cualquier
mínima discontinuidad podría producir cliks audibles
• Si solo hay un ciclo, va a 44.100 Hz ?
§ Que esta sea la frecuencia del sonido no significa que el programa
deba ir a esta frecuencia
§ Estos programas sintetizan en cada ciclo el número de muestras
necesario (e.g. si el programa funciona a 50 Hz, en cada ciclo s e
calcularán 1764 muestras, suponiendo estéreo y audio a 44.100 Hz)
[44.100/50*2]
§ El programa manda este paquete (frame) con 1764 muestras a los
drivers de la tarjeta de sonido y no se preocupa de como los reproduce
el hardware (se supone que bien!)
• Que ventajas e inconvenientes presupone aumentar esta frecuencia
(e.g. 50 Hz) ?
§ PRO: mayor suavidad en el control y en el resultado sonoro – podemos
aportar modificaciones más veces/seg
§ CONS: estressar + el sistema, con + riesgos de clicks , etc.
4
Resolución y latencia
• La latencia es el retardo entre el tiempo en que se produce un
cambio en la entrada y que este cambio surte efecto en la salida
• Veremos más adelante que hay un compromiso entre la resolución-
precisión temporal (en la sincronía antes mencionada, la resolución
temporal es de 20 ms), pero la latencia puede ser superior (nunca
inferior)
• Otro ejemplo:
§ Supongamos una variable de tipo unsigned long
framesPerBuffer que es el número de samples que se procesan en
cada bloque. Esto determina la resolución o granularidad del sistema,
ya que los parámetros de control (de síntesis por ej.) sólo se reciben a
cada bloque.
§ Si SR=44100 y framesPerBuffer = 64, la resolución sería de …
1,45 ms (64/44100)
§ Este tamaño puede venir condicionado también si queremos hacer
FFTs, ya que lo más fácil será que el tamaño de la ventana sea igual al
tamaño del buffer.
§ El retardo o latencia puede ser superior y se indicaría normalmente con
una variable tipo numberOfBuffers (latencia = numberOfBuffers *
framesPerBuffer/SR)
Resolución y latencia
• La latencia es el retardo entre el tiempo en que se produce un
cambio en la entrada y que este cambio surte efecto en la salida
• Veremos más adelante que hay un compromiso entre la resolución-
precisión temporal (en la sincronía antes mencionada, la resolución
temporal es de 20 ms), pero la latencia puede ser superior (nunca
inferior)
• Otro ejemplo:
§ Supongamos una variable de tipo unsigned long
framesPerBuffer que es el número de samples que se procesan en
cada bloque. Esto determina la resolución o granularidad del sistema,
ya que los parámetros de control (de síntesis por ej.) sólo se reciben a
cada bloque.
§ Si SR=44100 y framesPerBuffer = 64, la resolución sería de …
1,45 ms (64/44100)
§ Este tamaño puede venir condicionado también si queremos hacer
FFTs, ya que lo más fácil será que el tamaño de la ventana sea igual al
tamaño del buffer.
§ El retardo o latencia puede ser superior y se indicaría normalmente con
una variable tipo numberOfBuffers (latencia = numberOfBuffers *
framesPerBuffer/SR)
5
Esquema sintetizador audio (1)
5. Actualizar GUI
track 1
Esquema
+ sintetizador
track 2 audio (2)
+
track 3
circular
Ejemplo de configuración a 50 Hz
1/50 seg 1/50 seg
En este ejemplo, la latencia es igual a la resolución,
porque el buffer definitivo tan sólo presenta 2 frames ,
pero en muchos sistemas se definen con más frames
para evitar clicks
6
Consideraciones sobre el esquema anterior
7
Síncronos – asíncronos :
Introducción
• Volviendo a nuestro sintetizador:
Síncronos – asíncronos :
Introducción
• Volviendo a nuestro sintetizador:
8
Polling vs. Interrupciones
Existen 2 grandes formas de implementar un sistema con entradas en
t.real
• Polling
§ Es el caso más sencillo
§ el sistema mira entradas externas periódicamente
§ Adecuado para un input de tipo continuo (analógico), pero no
para uno con eventos (ya que podrían perderse)
• Interrupciones
§ Los eventos externos provocan un servicio de interrupción, que
ejecuta un programa breve (que debería terminar antes de que
deban comenzar otras tareas)
§ En el más “breve” de los casos, este programa o proceso
simplemente pone el evento en una cola de entrada
§ Una vez terminado este proceso el programa principal retoma el
punto en el que estaba
Clock Interrupts
• Estas interrupciones generadas periódicamente por el reloj de la
CPU son fundamentales para el control de un sistema en t.real
• Determinan la mínima unidad temporal (o resolución) del sistema
9
Ejemplos de bucles control
• Cuales son los problemas de estos ejemplos?
§ sleep(h) indica que el proceso espera un tiempo relativo h (podría ser wait,
delay, etc.)
§ sleepUntil(t) indica que el proceso espera hasta el instante t
while (true) {
start = getCurrentTime(); Podría ser que la tarea se haya interrumpido
executeController(); entre las líneas 4 y 5
end = getCurrentTime();
sleep(h - (end - start));
}
while (true) {
start = getCurrentTime();
executeController(); Tampoco se tiene en cuenta el tiempo
nexttime = start + h; transcurrido entre las líneas 5 y 2
sleepUntil(nexttime);
}
while (true) {
start = getCurrentTime(); Podría ser que la tarea se haya interrumpido
executeController(); entre las líneas 4 y 5
end = getCurrentTime();
sleep(h - (end - start));
}
while (true) {
start = getCurrentTime();
executeController(); Tampoco se tiene en cuenta el tiempo
nexttime = start + h; transcurrido entre las líneas 5 y 2
sleepUntil(nexttime);
}
10
Ejemplos de bucles control
• Cuales son los problemas de estos ejemplos?
§ sleep(h) indica que el proceso espera un tiempo relativo h (podría ser wait,
delay, etc.)
§ sleepUntil(t) indica que el proceso espera hasta el instante t
while (true) {
start = getCurrentTime(); Pudiera ser que la tarea se hubiera
executeController(); interrumpido entre las líneas 4 y 5
end = getCurrentTime();
sleep(h - (end - start));
}
while (true) {
start = getCurrentTime();
executeController(); Tampoco se tiene en cuenta el tiempo
nexttime = start + h; transcurrido entre las líneas 5 y 2
sleepUntil(nexttime);
}
while (true) {
start = getCurrentTime(); Pudiera ser que la tarea se hubiera
executeController(); interrumpido entre las líneas 4 y 5
end = getCurrentTime();
sleep(h - (end - start));
}
while (true) {
start = getCurrentTime();
executeController(); Tampoco se tiene en cuenta el tiempo
nexttime = start + h; transcurrido entre las líneas 5 y 2
sleepUntil(nexttime);
}
11
Ejemplos de bucles control (y 2)
nexttime = getCurrentTime();
while (true) {
executeController();
nexttime = nexttime + h;
sleepUntil(nexttime);
}
• Esto ya es correcto
• Que sucede cuando un ciclo dura más de la cuenta?
è Que el siguiente dura menos
• Esto ya es correcto
• Que sucede cuando un ciclo dura más de la cuenta?
è Que el siguiente dura menos (se mantiene el promedio)
Esto puede ser lo que busquemos o puede que no… Por ello, se deja al
estudiante, la modificación de este fragmento, para el caso en que se
deseara que el ciclo siguiente a un ciclo demasiado largo, siguiera teniendo
la duración correcta (para ello, tal vez haya que descomponer
executeController() en varias fases [input, output…] )
12
Multithreading
• En los últimos ejemplos hemos supuesto que un único bucle principal
controlaba todo el proceso.
• En muchos sistemas de t.real, no se adopta esta filosofía.
• Por ejemplo si usamos interrupciones en lugar de polling, tendremos al
menos dos “procesos” (el principal y el llamado de vez en cuando por las
interrupciones) è multithreading
§ NOTA: la diferencia entre threads y procesos es que … los primeros comparten
el mismo espacio de memoria, variables, etc…
Multithreading
• En los últimos ejemplos hemos supuesto que un único bucle principal
controlaba todo el proceso.
• En muchos sistemas de t.real, no se adopta esta filosofía.
• Por ejemplo si usamos interrupciones en lugar de polling, tendremos al
menos dos “procesos” (el principal y el llamado de vez en cuando por las
interrupciones) è multithreading
§ NOTA: la diferencia entre threads y procesos es que los primeros comparten el
mismo espacio de memoria, variables, etc …
13
Funciones Callback
• Una función callback es una función que se
llama desde un proceso que nosotros no
controlamos (normalmente desde una API)
• Esta llamada puede ser periódica (eg. llenar un
frame de audio) o motivada por determinados
eventos (e.g. recibir mensaje MIDI, procesar
frame video)
§ En realidad están siempre generadas por eventos (en
el primer caso, un evento de RELOJ)
• Se suelen implementar mediante punteros a
funciones
• Para pasar una función (f2) como parámetro (de una función f1) s e escribe: f1(f2)
Esto es así, porqué al igual que el nombre de un array (sin [ ]) es igual a la dirección de su
primer elemento, el nombre de una función (sin ( ) ) equivale a la dirección de inicio de esta
función en memoria
14
Insert: Punteros a funciones (y 2)
• Los punteros a funciones se utilizan mucho par ordenaciones. En las ordenaciones
se realizan dos acciones básicas: la de comparación y la de cambio de lugar.
• La comparación no es algo trivial. No es válido el símbolo < para comparar cadenas,
o muchos otros tipos de datos. A una función de ordenación le podremos determinar
cómo se compara para que sea genérica. El prototipo de una función de este estilo
podría ser:
int ordenar (void *array_a_ordenar, unsigned int n_elem, unsigned int tam_elem, int
(*pfcomparacion)(const void *, const void *))
§ El const se utiliza para evitar que la función de comparación pueda modi ficar los elementos a comparar
§ El puntero a elementos a ordenar es de tipo void para poder ordenar cualquier cosa
§ Por esta misma razón, es necesario indicar el tamaño de cada elemento
• En ANSI C existe la función qsort, que utiliza el algoritmo quick sort. Lo único que
deberemos escribir para que funcione es la función que compara.
int f1(void);
int f2(void);
int f3(void);
inf (*pf[3])( ); // esto es un array de punteros a cada una de estas tres funciones
Para llamar a una de estas funciones podríamos hacer:
while ((i=opcion( ) ) > 0 && i < 3) (*pf[i] )( );
• #define paFloat32 /*always available*/ /*el más cómodo y sencillo: valores entre +-
1*/
• #define paInt16 /*always available*/
• #define paInt32 /*always available*/
• #define paInt24
• #define paPackedInt24
• #define paInt8
• #define paUInt8 /* unsigned 8 bit, 128 is "ground" */
15
Ejemplo: callbacks, resolución y latencia
Sincronía en Multithreading
• Por definición, si dos threads son independientes es imposible
garantizar la sincronía entre ellos, con una precisión “máxima”
§ Ej. Resta de 2 vídeos idénticos en Eyesweb (versión asíncrona vs.
versión síncrona)
16
Varios artículos sobre diseño de lenguajes y sistemas
para interactividad en tiempo real
(especialmente en audio)
§ Dannenberg, ``Software Design for Interactive Multimedia
Performance,'' Interface - Journal of New Music Research, 22(3)
(August 1993), pp. 213-228.
§ Brandt and Dannenberg, ``Low-Latency Music Software Using Off-
The-Shelf Operating Systems,'' in Proceedings of the International
Computer Music Conference, San Francisco: International Computer
Music Association, (1998), pp.137-141.
§ Brandt and Dannenberg, ``Time in Distributed Real-Time Systems,''
in Proceedings of the 1999 International Computer Music
Conference, San Francisco: International Computer Music
Association, (1999), pp. 523-526.
§ Bencina and Burk, “PortAudio – an Open Source Cross Platform
Audio API”, in Proceedings of the 2001 International Computer
Music Conference, San Francisco: International Computer Music
Association, (2001).
§ è What is latency and how to tune it?
17
• En el caso de MIDI, las funciones
básicas nos permiten
§ Preguntar por puertos disponibles (I & O) y sus propiedades
§ Abrirlos / cerrarlos
§ Recibir mensajes de los puertos de entrada
§ Mandar mensajes a los puertos de salida
§ Además también habrá que utilizar las de relojes…
• Control de tiempos
§ Caso más simple: salida sin reloj
§ Salida con reloj
§ Entrada y salida
Reloj Multimedia
Salida con Reloj
• Los timers son fundamentales para que la aplicación realice operaciones
periódicas con máxima precisión temporal (1 ms del Timer Multimedia vs.
40-50 ms del Timer Windows Standard)
• Información adicional (creación y uso de Timers multimedia en VC++)
• Al iniciar un Timer dos de sus argumentos son la resolución en ms y una
Callback function que se ejecutará periódicamente
• Esta función podría estar mirando en un array (o lista o array, etc.) de
eventos (con timestamp) para ver si debe mandar algo a la salida (los ticks
los debe controlar la función)
• Esta función puede recibir un argumento de 32 bits (para enteros o
punteros): e.g. transposición
• Esta función no debería ejecutar ninguna llamada tipo leer/escribir en
fichero, consola… se podría retrasar
18
Reloj Multimedia
Problemas de sincronía (pueden variar dependiendo del
S.O)
Caso B)
struct Data data[10];
StartTimer (1,&process_midi,(void*)data);//1 ms, callback, zona memoria de intercambio
Otra cosa a evitar es que main acceda directamente a los puertos MIDI
Más complicaciones si también threads de audio…
Todo esto lo tiene que controlar el programador…
19
Eventos y Control de tiempos
• Mensaje MIDI 3 bytes
• Win usa un ULONG con el status en el LSB:
• MSBà 00 data2 data1 status ß LSB
Tempo y ticks
• Tempo = negras/minuto
• Resolución del secuenciador = ticks/negra
• Duración de un tick = ms/tick
Problemas?
20
Tempo y ticks
• Tempo = negras/minuto
• Resolución del secuenciador = ticks/negra
• Duración de un tick = ms/tick
Problemas?
21
Standard MIDI Files
http://www.sfu.ca/sca/Manuals/247/midi/fileformat.html
http://www.sonicspot.com /guide/midifiles.html
Un SMF se almacena como varias pistas, cada una de ellas con TimeStamps relativos (a los
eventos de esta pista)
PortAudio
• API audio para t.real : http://www.portaudio.com
• C, bajo nivel
• Multiplataforma (WinDS, WinMME, Linux, Mac OS)
• Para elegir la plataforma, tan sólo deberemos seleccionar con que librería
linkar (DSound: PAStaticDS[D].lib + dsound.lib + winmm.lib ). Además
siempre habrá que incluir portaudio.h (que es también la fuente principal
de documentación)
1. Inicializar librería
2. Declarar un stream de audio (puede ser i/o)
3. Mostrar – seleccionar – abrir dispositivos audio de i/o
4. Iniciar el stream (que llamará al callback )
5. …..
6. Parar stream , cerrar stream y cerrar librería
22
Inicio y dispositivos i/o
PaError Pa_Initialize(void); //lo primero
PaError Pa_Terminate(void); //lo último
const char *Pa_GetErrorText(PaError errnum); //convierte num.error en texto
Gestión de devices
int Pa_CountDevices(void);
PaDeviceID Pa_GetDefaultInputDeviceID(void);
PaDeviceID Pa_GetDefaultOutputDeviceID(void);
const PaDeviceInfo* Pa_GetDeviceInfo(PaDeviceID devID);
Streams
• Todo programa PortAudio requiere un (y un único) stream , ya que un
mismo stream puede ser a la vez de i/o (también se pueden usar varios)
• Un stream tiene un número de canales i, canales o, sample rate, formato de
las muestras, dispositivos asociados, muestras por buffer,etc.
• PortAudioStream *stream=NULL; /*se declara siempre un puntero NULL y
se reserva automáticamente al crearlo y libera al cerrarlo*/
23
PaError Pa_OpenDefaultStream( PortAudioStream** stream,
int numInputChannels, /* 0,1,2 –estereo-…maxInputChannels*/
int numOutputChannels, /* 0,1,2 –estereo-… maxOutputChannels*/
PaSampleFormat sampleFormat, /* ver defines más abajo */
double sampleRate, /* igual para i y o */
unsigned long framesPerBuffer, /* samples/ventana (y por canal)*/
unsigned long numberOfBuffers, /* num. de ventanas. (ver latencia y resolución)*/
PortAudioCallback *callback, /* función callback */
void *userData ); /* datos intercambio que se pasarán a callback*/
• #define paFloat32 /*always available*/ /*el más cómodo y sencillo: valores entre +-
1*/
• #define paInt16 /*always available*/
• #define paInt32 /*always available*/
• #define paInt24
• #define paPackedInt24
• #define paInt8
• #define paUInt8 /* unsigned 8 bit, 128 is "ground" */
Resolución y latencia
• unsigned long framesPerBuffer es el número de samples que se pasan en una
llamada a la callback. Esto determina la resolución o granularidad del sistema, ya
que los parámetros de control (de síntesis por ej.) sólo se reciben a cada callback.
• Si SR=44100 y framesPerBuffer = 64, la resolución sería de 1,45 ms (64/44100)
• Este tamaño puede venir condicionado también si queremos hacer FFTs dentro de la
callback, ya que lo más fácil será que el tamaño de la ventana sea igual al tamaño
del buffer.
• El retardo o latencia puede ser muy superior ya que depende también de
numberOfBuffers que es el número de callbacks (latencia = numberOfBuffers *
framesPerBuffer)
• Pa_GetMinNumBuffers(int framesPerBuffer,double sampleRate) no hace ninguna
evaluación real del sistema
• Deberemos probar nosotros (ej. Con >Minlat.exe samples_buffer) e ir reduciendo el
número de buffers hasta que el sonido sea discontinuo (en este caso, el número de
buffers sería demasiado pequeño)
• A continuación ajustar opcionalmente los ms por línea de comandos, de acuerdo con
los cálculos realizados Minlat.exe è : set PA_MIN_LATENCY_MSEC=10
24
#include "stdio.h"
#include "portaudio.h"
static int myCallback( void *inputBuffer , void *outputBuffer , unsigned long framesPerBuffer , PaTimestamp outTime , void *userData )
{
float *out = ( float *) outputBuffer ; //cast al tipo que estemos usando
float *in = ( float *) inputBuffer ;
float leftInput, rightInput;
unsigned int i;
if( inputBuffer == NULL ) return 0;
for( i=0; i<framesPerBuffer ; i++ )
{
leftInput = *in++; /* Get interleaved samples from input buffer. */
rightInput = *in++;
*out++ = leftInput * rightInput ; /* ring modulation * /
*out++ = 0.5f * ( leftInput + rightInput); /* mix* /
}
return 0; //si !=0 se para el stream
}
int main(void)
{
PortAudioStream *stream;
Pa_Initialize();
Pa_OpenDefaultStream(&stream,2, 2 /* stereo input and output */,paFloat32 , 44100.0,64, 0, /* 64 frames per buffer, let PA
determine numBuffers */ myCallback, NULL /*no hay parámetros userData */ );
Pa_StartStream( stream );
......
Pa_StopStream( stream );
Pa_CloseStream( stream );
Pa_Terminate();
return 0;
}
Callbacks
• No llamar a funciones de gestión de memoria, acceso a ficheros, etc.
• No llamar a ninguna función PortAudio, salvo Pa_StreamTime() y Pa_GetCPULoad()
• Si el formato es estéreo, una muestra es L y la siguiente R (framesPerBuffer es en realidad /
canal)
• Si el formato es paFloat32, las muestras de entrada varían entre -+1. Las de salida deberían hacer
lo mismo, aunque temporalmente podemos almacenar valores mayores.
• Si hay silencio, hay que poner 0 en la salida
• Si queremos trabajar con sonidos almacenados o sintetizados, estos deberán estar accesibles
(almacenados en arrays) desde la función callback
• SI queremos recibir controles, lo podremos hacer desde UserData o mediante colas de mensajes
Reproducir WAVs :
• Almacenar cada uno en un array (pref. de 32b float , normalizados entre +-1)
• Tener un índice de lectura para cada uno de estos arrays
• Si queremos reproducir en loop, cuando i==tamaño_array è i = 0
• Si no queremos reproducir en loop, cuando i==tamaño_array , seguiremos poniendo ceros
Sintetizar sonido
• Si es con tablas (ej. Sinusoidal) ésta se habrá calculado al inicio del programa. El problema es
después el mismo que el reproducir WAVs
• También se puede sintetizar sobre la marcha si no es muy costoso computacionalmente (ej.
Diente sierra)
25
Tablas
• El tamaño de una tabla dependerá de la frecuencia mínima que
queramos sintetizar
• Cuanto mayor sea la tabla, más precisión obtendremos, y en
muchos casos podremos cambiar de frecuencia sin interpolar
• Por ejemplo, si la tabla fuese del tamaño del SR (44.100), para
reproducir una frecuencia de 100 Hz, habrá que coger un elemento
de cada 100
• Como llenar una tabla de senos de N [e.g.44.100] valores?
Tabla_sinus[i] = (float)(sin(i/N)*PI*2);
• Si la frecuencia no es un número entero, podemos interpolar el valor
o acumular y corregir errores
• En el caso de un WAV, el tamaño no lo elegimos nosotros. Si
queremos modificar su frecuencia deberemos conocer su frecuencia
original
Control MIDI
• Habrá un thread para audio y otro para MIDI
• Será mejor recibir los mensajes (de MIDI a
audio) mediante una cola
• Si no suena, se pondrá la pista a 0
• Si un sonido termina (Off) no debería hacerlo en
un valor de muestra != 0 pues se producirían
“clicks”. Al recibir un mensaje de NOTEOFF se
debería hacer un fade rápido
• Polifonía y resolución del sonido en bits (si 16
pistas, 12 bits cada pista) (16=2 4)
26