0% encontró este documento útil (0 votos)
21 vistas26 páginas

Curso Audio

Este taller trata sobre conceptos de programación de audio y MIDI en tiempo real. Se explican conceptos como resolución, latencia y como implementar un sistema de síntesis de audio en tiempo real con un único ciclo de procesamiento.

Cargado por

Lito Celis
Derechos de autor
© © All Rights Reserved
Nos tomamos en serio los derechos de los contenidos. Si sospechas que se trata de tu contenido, reclámalo aquí.
Formatos disponibles
Descarga como PDF, TXT o lee en línea desde Scribd
0% encontró este documento útil (0 votos)
21 vistas26 páginas

Curso Audio

Este taller trata sobre conceptos de programación de audio y MIDI en tiempo real. Se explican conceptos como resolución, latencia y como implementar un sistema de síntesis de audio en tiempo real con un único ciclo de procesamiento.

Cargado por

Lito Celis
Derechos de autor
© © All Rights Reserved
Nos tomamos en serio los derechos de los contenidos. Si sospechas que se trata de tu contenido, reclámalo aquí.
Formatos disponibles
Descarga como PDF, TXT o lee en línea desde Scribd
Está en la página 1/ 26

Taller de Música Electrònica

Enginyeria Superior Informàtica


Universitat Pompeu Fabra
abril/juny 2003

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

• A priori, el sistema tendrá por lo tanto 2 líneas o threads principales:


§ Captura de datos de control en la entrada
§ Síntesis a partir de estos datos

• Una vez entendidos los conceptos básicos de un sistema de estas


características, el ampliarlo con:
§ Otros sistemas de síntesis
§ Elementos de procesado (filtros, LFOs…)
§ ….
No supone ningún problema conceptual adicional

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

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 – riesgo de clicks

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.

Ejemplo: Sintetizador de audio (y 2)

• Aunque 50 Hz es una buena medida para


un sistema HCI a t.real, a un teclista MIDI
le parecerá poco
• Resolución vs. latencia
§ En este caso la resolución temporal es de 20
ms, pero la latencia puede (suele) ser mayor
(y la latencia es de lo que se quejaría el
teclista)
§ Porqué puede ser mayor la latencia que la
resolución?

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)

•La actualización del GUI podría


3.Síntesis realizarse en cualquier otro punto del
2.Mapeo datos
Frame ciclo
•La latencia se produce porque el
frame que mandamos al driver tal
vez no sea el que vaya a sonar
inmediatamente
•La duración de este ciclo debe ser lo
4. Frame a más rigurosa posible (e.g. 20 ms si
1.Entrada datos
driver audio estamos a 50 Hz)

5. Actualizar GUI

track 1
Esquema
+ sintetizador
track 2 audio (2)
+
track 3

... ... ... Tracks (2 seg)

Bloque (1/50 seg)

track MIX Buffer definitivo


1/50 seg (x2)

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

• Tamaño de la memoria de cada track?


• Resolución (bits) y mezclas?

Estructuras cíclicas más complejas


• Modelos síncronos vs. modelos
asíncronos
• Polling vs. Interrupciones
• Single threaded vs. multithreaded

7
Síncronos – asíncronos :
Introducción
• Volviendo a nuestro sintetizador:

• Si el input fuera mirar la posición del ratón, el valor de un sensor


que nos manda valores continuos, etc…los miramos a cada ciclo y
ya está…
• Pero si el input genera eventos? (e.g. click ratón, teclado MIDI…)
Estos eventos probablemente pueden producirse en cualquier
instante
• En nuestro sistema anterior deberíamos tener un buffer de entrada
que almacene todos los datos que han llegado durante el ciclo
anterior
§ Pila, cola, o…? (más bien cola…. aunque…)
§ Algunos tipos de datos (e.g. varios controles de volumen) tal vez
puedan saltarse y coger sólo el último (si nuestro sistema es sólo capaz
de procesar un control/ciclo, manejar todos los volúmenes que se han
almacenado podría suponer un retardo considerable). Otros tipos de
datos (e.g. notas) no deberán saltarse.
§ En cualquier caso, nuestro sistema no parece ya tan robusto…

Síncronos – asíncronos :
Introducción
• Volviendo a nuestro sintetizador:

• Si el input fuera mirar la posición del ratón, el valor de un sensor


que nos manda valores continuos, etc…los miramos a cada ciclo y
ya está…
• Pero si el input genera eventos? (e.g. click ratón, teclado MIDI…)
Estos eventos probablemente puedan producirse en cualquier
instante…
• En nuestro sistema anterior deberíamos tener un buffer de entrada
que almacene todos los datos que han llegado durante el ciclo
anterior
§ Pila, cola, o…? (más bien cola…. aunque…)
§ Algunos tipos de datos (e.g. varios controles de volumen) tal vez
puedan saltarse y coger sólo el último (si nuestro sistema es sólo capaz
de procesar un control/ciclo, manejar todos los volúmenes que se han
almacenado podría suponer un retardo considerable). Otros tipos de
datos (e.g. notas) no deberán saltarse.
§ En cualquier caso, nuestro sistema no parece ya tan robusto ni tan
sencillo…

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

• No trataremos aquí como el sistema operativo gestiona el time-


sharing de todos los procesos-programas activos en un instante
dado
• Aunque a la hora de diseñar-programar un sistema interactivo en
t.real, no deberemos olvidar que normalmente nuestro programa no
será el único que se esté ejecutando.
• Los siguientes ejemplos muestran problemas ocasionados por no
tener en cuenta esto

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) do { No se tiene en cuenta el tiempo de ejecución


executeController(); del proceso
sleep(h);
}

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);
}

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) do { No se tiene en cuenta el tiempo de ejecución


executeController(); del proceso
sleep(h);
}

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) do { No se tiene en cuenta el tiempo de ejecución


executeController(); del proceso
sleep(h);
}

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);
}

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) do { No se tiene en cuenta el tiempo de ejecución


executeController(); del proceso
sleep(h);
}

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

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 (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…

• Sin embargo, en bastantes casos, el programador no gestiona directamente


estos threads, cuyo uso puede venir facilitado por las librerías que se
utilicen
• Ejemplos:
§ Salida de audio a la tarjeta: una librería (e.g. DirectX) se encarga de esto a partir
de unos frames de tamaño predefinido (y “rellenados” por nuestro programa)
§ Entrada de datos MIDI o serie: las librerías suele invocar un thread que coloca
los datos en una cola
§ Procesado de imagen a t.real: las librerías suele llamar a una función a la que le
pasan el frame (imagen) recién capturado. En estos casos, nuestro programa
NO controla el bucle principal…
§ Si utilizamos alguna librería de audio de más alto nivel que DirectX (e.g. CLAM,
PortAudio…) la librería puede comportarse de forma similar al caso de vídeo. En
lugar de responsabilizarnos del bucle principal, debemos escribir la función que
se ejecuta periódicamente y que se encarga de “rellenar” un frame de audio
(calcular todas sus muestras). La llamada a esta función queda c omo
responsabilidad del scheduler de la librería

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 …

• Sin embargo, en bastantes casos, el programador no gestiona directamente


estos threads, cuyo uso puede venir facilitado por las librerías que se
utilicen
• Ejemplos:
§ Salida de audio a la tarjeta: una librería (e.g. DirectX) se encarga de esto a partir
de unos frames de tamaño predefinido (y “rellenados” por nuestro programa)
§ Entrada de datos MIDI o serie: las librerías suele invocar un thread que coloca
los datos en una cola
§ Procesado de imagen a t.real: las librerías suele llamar a una función a la que le
pasan el frame (imagen) recién capturado. En estos casos, nuestro programa
NO controla el bucle principal…
§ Si utilizamos alguna librería de audio de más alto nivel que DirectX (e.g. CLAM,
PortAudio…) la librería puede comportarse de forma similar al caso de vídeo. En
lugar de responsabilizarnos del bucle principal, debemos escribir la función que
se ejecuta periódicamente y que se encarga de “rellenar” un frame de audio
(calcular todas sus muestras). La llamada a esta función (callback) queda como
responsabilidad del scheduler de la librería

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

Insert: Punteros a funciones (1)


• Los punteros a funciones son punteros al inicio de la función en memoria, a la
dirección inicial dentro del código donde comienza la función.

• Se utilizan para ofrecer algoritmos alternativos dentro de un algoritmo mayor…


• Para personalizar fragmentos de código dentro de procesos predef inidos…
• Permiten crear arrays de funciones de forma que cada elemento del array toma la
dirección de una función …

• 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

• En el prototipo de la función f1 deberemos indicar también el prototipo de la función


f2. Por ejemplo si f2 es: int f2 (char *), y f1 recibe f2 como argumento, entonces la
declaración de f1 será: f1(int(*f2)(char *))
• Para llamar a f2, desde el código de f1, escribiremos: (*f2)(…)
f1(int(*f2)(char *)) {
char *p;
int a;
....
a=(*f2)(p);
...
}
• En el caso de funciones callback, f1 suele estar escrito. Lo que debemos hacer es
“rellenar” el código de f2

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.

• Otra aplicación de los punteros a funciones es la de crear menús u opciones


diferentes:

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] )( );

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
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" */

Ejemplo: Callback en PortAudio

15
Ejemplo: callbacks, resolución y latencia

• Supongamos una variable unsigned long framesPerBuffer


que es el número de samples que se pasan en una llamada a la
función 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

• En una función callback, no es conveniente realizar gestión de


memoria, acceso a ficheros, refresco de pantalla… y ningún tipo de
acceso a los recursos del sistema que pueda suponer un retardo
para el resto de los procesos

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)

• Esto es generalizable al uso de cualquier dato compartido por 2 o


más threads
§ Orden de actualización?
§ Problema más grave: que el thread A que escribe los datos, se
interrumpa durante la escritura para dar paso al thread B que los leerá
(parcialmente actualizados) è 2 procesos leyendo y escribiendo
“simultáneamente” de las mismas posiciones de memoria
§ Soluciones:
• Semáforos: mecanismos de bloqueo de acceso
• Más simple: intercambio de datos entre threads mediante colas de
mensajes
§ Sólo uno de los threads debe tener acceso de escritura
§ Si 2 threads deben intercambiar datos mutuamente (ambos con
posibilidad I/O) è 2 colas

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?

Cada dispositivo-puerto MIDI (hardware o


virtual) tiene un driver asociado
(drivers virtuales: e.g. MidiYoke)
MIDI en Windows Entre la aplicación y el driver existe una
capa intermedia (winmm.dll) con
centenares de funciones para
•Audio (W a v e), MIDI, Timers, Video,
Joystick
En cualquiera de estos casos, deberemos linkar
nuestros programas con winmm.lib

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

Entrada y salida (varias opciones)


1. El puerto de entrada ejecuta un thread independiente que se activa (otra
callback ) cuando recibe mensajes
2. El puerto de entrada deja mensajes en una cola. La callback de salida mira
si hay mensajes en la cola (poll) y los procesa (aún así tenemos como
mínimo 2 threads de que preocuparnos: principal + timer-callback)

18
Reloj Multimedia
Problemas de sincronía (pueden variar dependiendo del
S.O)

Caso A) Imaginemos que al definir la callback indicamos una variable de intercambio de 32


bits
int iData;
StartTimer (1,&process_midi,(void*)&iData );//1 ms, callback , variable de intercambio
Ó
StartTimer (1,&process_midi,(void*)iData); //NB. StartTimer es un nombre inventado
Thread principal y callbackcomparten esta información. OK.

Caso B)
struct Data data[10];
StartTimer (1,&process_midi,(void*)data);//1 ms, callback, zona memoria de intercambio

Ahora pueden surgir problemas. Porqué? Cual es la diferencia?


Solución? Utilizar dos colas adicionales: una de main->midi y otra de midi->main

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…

Uso de la API PortMusic (en C)


• Audio + MIDI
• Multiplataforma (Win MME, Win DX, Win
Asio, Linux ALSA, Mac OS)
• Encapsula timers y colas de entrada
• Facilita gestión de colas adicionales
• Todo el código es abierto
• En el caso de Win, la API sigue llamando a
winmm.dll por lo que habrá que seguir
linkando con winmm.lib (además de las
propias de PortMusic)
• Bajo nivel, no añade latencia

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

#define Pm_Message(status, ch, data1, data2)


((((data2) << 16) & 0xFF0000) | \
(((data1) << 8) & 0xFF00) | \
((status | ch) & 0xFF))

• Evento MIDI: mensaje + timestamp


• El timestamp no se mide en ms sino en ticks (ULONG)

typedef long PmMessage;


typedef struct {
PmMessage message;
PmTimestamp timestamp;
} PmEvent;

• Win proporciona el primero /PortMusic los dos

Tempo y ticks
• Tempo = negras/minuto
• Resolución del secuenciador = ticks/negra
• Duración de un tick = ms/tick

• Cómo calcular la duración de un tick en función del tempo y la


resolución?

60.000 [ms] 1 [min]


----------------- x ----------------------------------
1 [min] Tempo*Resolución [tick]

Problemas?

20
Tempo y ticks
• Tempo = negras/minuto
• Resolución del secuenciador = ticks/negra
• Duración de un tick = ms/tick

• Cómo calcular la duración de un tick en función del tempo y la


resolución?

60.000 [ms] 1 [min]


----------------- x ----------------------------------
1 [min] Tempo*Resolución [tick]

Problemas?

En la práctica, no todos los tempos son posibles, ya que la


duración de 1 tick es int
Soluciones:
èCuantizar los tempos (si el usuario introduce un tempo
imposible, se le avisa del redondeo – poco prof!)
èAcumular errores y corregir (e.g. si la duración teórica
de 1 tick es 1,5 ms, cada 2 ticks de 1 ms se a ñade 1
tick fantasma que no se cuenta – años bisiestos)

Además, El timestamp puede


ser absoluto o relativo (en
SMF es relativo, en PortMusic
es abierto)
Si es relativo, al insertar un
evento hay que corregir el
TimeStamp del siguiente

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)

Para reproducir el fichero


1. Se convierte cada pista a t absolutos
2. Se combinan todas las pistas en una sola en memoria
3. Opcionalmente (si nuestro reloj funciona con relativos), se vuelven a “relativizar”

Si no trabajamos con SMF, y lo hacemos con t absolutos, no es necesario todo esto:

• Podemos tener un array 2D (un array de secuencias donde cada secuencia es un


array de eventos)
• Y un array de índices (cada índice indica el próximo evento de la secuencia)
• A cada tick, miramos para cada secuencia si el timestamp del próximo evento coincide
con el tick actual. Si sí, lo mandamos a la salida e incrementamos el índice.
• Si además de un secuenciador, quisiéramos un editor, los arrays no son suficientes
(hay que insertar/borrar…). Habría que trabajar con listas enlazadas.

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)

Mecanismo básico similar a PortMusic (MIDI)

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

“Todo” está en el código que ejecute la función callback


NB. Las aplicaciones deben ser DLL multithreated (Debug o no según el caso)

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);

• Estudiar pa_devs.c para ver el uso de estas funciones


• Ejecutar utils/devs para ver dispositivos (i/o) disponibles y cuales son los por-
defecto

>SET PA_RECOMMENDED_INPUT_DEVICE=X donde X [0 - N-1]


>SET PA_RECOMMENDED_OUTPUT_DEVICE=X
Para cambiar dispositivos defecto

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*/

• Los streams se crean: Pa_OpenStream(PortAudioStream** stream ,….) ó


Pa_OpenDefaultStream(PortAudioStream**
stream,….)
• Se abren: Pa_StartStream(PortAudioStream*)
• Se paran: Pa_StopStream(PortAudioStream *) ó
Pa_AbortStream(PortAudioStream *) (no vacía
buffers pendientes)
• Se cierran: Pa_CloseStream(PortAudioStream *)
• Se consultan Pa_StreamActive(PortAudioStream *)
Pa_StreamTime(PortAudioStream *)
Pa_GetCPULoad(PortAudioStream *) (entre 0 y 1)…

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" */

También existe la alternativa más compleja PaError Pa_OpenStream() que utiliza


más parámetros

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)

Mezclar varias pistas


*out++=((*pista1++)+(*pista2++)+(*pista3++)+(*pista4++))/4; //L
*out++=((*pista1++)+(*pista2++)+(*pista3++)+(*pista4++))/4; //R

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

También podría gustarte