Ejercicio obligatorio 5
Fecha de entrega: Domingo 12 de julio
Introducción
Endianness
Distintas arquitecturas de procesador se diferencian según cómo se almacenan en memoria los distintos bytes de un entero.
Supongamos que se quiere guardar en memoria el entero 0xDDCCBBAA
en una
arquitectura de tipo big-endian el mismo se guardará como
{0xDD, 0xCC, 0xBB, 0xAA}
mientras que en una arquitectura de tipo
little-endian se almacenará como {0xAA, 0xBB, 0xCC, 0xDD}
. Es decir,
mientras en big-endian se almacena primero el byte más pesado, en little-endian
se almacena primero el byte menos pesado.
El formato MIDI
Ya sabemos (ver EJ4) que un archivo MIDI está compuesto por un encabezado, que define las particularidades de ese archivo, y luego por pistas que cada una corresponde a un instrumento particular. Cada pista consiste en una sucesión de eventos que codifican las cosas que suceden en la pista.
El archivo MIDI es un formato binario donde todos los valores numéricos son enteros sin signo y se almacenan según convención big-endian.
Imagen: Archivo MIDI con N
pistas.
Encabezado
El encabezado del archivo tiene la siguiente estructura:
Campo |
Sgnificado |
Tipo |
Valores |
---|---|---|---|
Header ID |
Identifica un archivo MIDI |
|
Vale siempre |
Size |
Tamaño del encabezado |
|
Vale siempre 6 |
Format |
Formato de archivo |
|
0: Pista única 1: Múltiples pistas sincrónicas 2: Múltiples piastas asincrónicas |
N of tracks |
Número de pistas |
|
Numérico |
PPQ |
Número pulsos por negra |
|
Numérico |
Pista
Campo |
Sgnificado |
Tipo |
Valores |
---|---|---|---|
Track ID |
Identifica una pista MIDI |
|
Vale siempre |
Size (S) |
Tamaño de la pista |
|
Numérico |
Eventos |
Eventos de la pista |
|
Ver eventos |
Eventos
La estructura de cualquier evento de una pista es una secuencia: delta de tiempo + evento.
Todos los eventos se encuentran precedidos por un valor de delta de tiempo, que indica a cuánto tiempo del evento anterior ocurre el actual.
En la codificación MIDI los valores de tiempo tienen una cantidad de bytes
variables (nunca más de 4). Los primeros bytes del número tienen el bit más
pesado (MSB) puesto en 1
mientras que el último byte del número tiene
su MSB puesto en 0
. El número final se obtiene concatenando los 7-bits
más livianos de cada uno de los bytes.
Ejemplo:
{
0x93, // 1 001 0011
0xF8, // 1 111 1000
0x1C, // 0 001 1100
...
}
En este caso el número ocupa 3 bytes dado que el tercer byte tiene su MSB en
0
. El número resultante va a ser la concatenación de los 7-bits más
livianos de cada byte: 001 0011 111 1000 001 1100
o, lo que es lo mismo,
0 0100 1111 1100 0001 1100
o, lo que es lo mismo, 0x4FC1C
o, lo
que es lo mismo, 326684
.
(En la sección 1.1 del Standard MIDI-File Format Spec. 1.1 que se encuentra en la sección de referencias hay 12 ejemplos de números y su codificación.)
Luego del delta de tiempo viene el evento, que es una secuencia: evento + mensaje + (metaevento).
El evento consiste en un byte y ya fue especificado en el EJ4.
Luego del evento sigue el mensaje del mismo, que es una secuencia de n
bytes
que representa los parámetros de ese evento en particular. La cantidad de bytes
de cada tipo de evento fue especificada en el EJ4.
Cada mensaje dependerá del evento, pero por ejemplo para los eventos de nota el mensaje tendrá dos bytes, donde el primer byte indica la nota y la octava con el formato desarrollado en el EJ4 y el segundo byte será la velocidad con la que se aprieta la tecla, como un número entre 0 y 127.
Los metaeventos son eventos que no entran dentro de los 7 eventos estándar y en ese caso los dos bytes representan el primero de qué metaevento se trata (hay una lista de más de 100 en la bibliografía de referencia) y el segundo cuánto miden esos datos. En el caso de nuestra implementación, como no vamos a procesar metaeventos, podemos utilizar esta longitud para descartar esa información.
Entonces dijimos que para cada evento va a haber una secuencia que será o: tiempo + evento + mensaje, o tiempo + evento + mensaje + metaevento, en el caso de que evento sea de tipo metaevento. En principio esto es así, pero hay una particularidad adicional.
Antes que nada, ya sabemos del EJ4 que todos los eventos tienen su MSB en 1, del mismo modo todos los mensajes tienen su MSB en 0 (como ya habíamos visto para la nota y la velocidad).
Como suele ser frecuente repetir reiteradas veces el mismo evento sobre un determinado canal, existe una variante de evento que es: tiempo + mensaje, o tiempo + mensaje + metaevento. En esta forma ¿cuál es el evento y el canal?, es el mismo del evento anterior. ¿Cómo detectamos si estamos ante esta forma o ante la otra?, simplemente asumamos que se trata de un evento, si la validación fallara entonces no estábamos leyendo la especificación del evento, sino que ya estamos leyendo su mensaje.
Imagen: 4 eventos de una pista.
- Evento 14:
Tiempo de 1 byte. Evento genérico. Mensaje de 3 bytes.
- Evento 15:
Tiempo de 2 bytes. Evento nota encendida en canal 0. Mensaje de 2 bytes que representan el primero la nota y el segundo la velocidad.
- Evento 16:
Tiempo de 1 byte. Evento faltante, entonces se repiten valores del evento 15: nota encendida en canal cero. Mensaje de 2 bytes que representan el primero la nota y el segundo la velocidad.
- Evento 17:
Tiempo de 3 bytes. Metaevento en el canal 16. Mensaje de 2 bytes que el primero representa el tipo de metaevento y el segundo la longitud. Longitud bytes con los parámetros del metaevento.
Trabajo
Endianness
Escribir una función
uint8_t leer_uint8_t(FILE *f);
que reciba un descriptor de archivo f
y que
devuelva el resultado de leer del archivo un entero sin signo de 8 bits.
Escribir una función
uint16_t leer_uint16_big_endian(FILE *f);
que
reciba un archivo f
y que devuelva el resultado de leer del mismo un
entero sin signo de 16 bits en formato big-endian.
Escribir una función
uint32_t leer_uint32_big_endian(FILE *f);
similar a la anterior
pero que lea un entero sin signo de 32 bits.
MIDI
Escribir una función
bool leer_encabezado(FILE *f, formato_t *formato, uint16_t *numero_pistas, uint16_t *pulsos_negra);
que lea del archivo f
un encabezado y devuelva por la interfaz el
formato
, el número de pistas numero_pistas
y la cantidad de
pulsos por negra pulsos_negra
. Si el encabezado es correcto debe
devolverse true
, en caso contrario false
.
Escribir una función
bool leer_pista(FILE *f, uint32_t *tamagno);
que lea del archivo
f
el encabezado de una pista y devuelva por la interfaz el tamaño tamagno
de la
misma. Si la pista es correcta debe devolverse true
.
Escribir una función
bool leer_tiempo(FILE *f, uint32_t *tiempo);
que lea del archivo
f
el delta de tiempo tiempo
de un evento. Debe devolver true
si el
tiempo es correcto.
Escribir una función
bool leer_evento(FILE *f, evento_t *evento, char *canal, int *longitud, uint8_t mensaje[]);
que lea del archivo f
un evento y su mensaje. La secuencia para
realizar esta operación es leer un byte y llamar a decodificar_evento()
. Si el
evento era válido entonces hay que leer tantos bytes como diga la longitud del
evento y guardarlos en el mensaje. En caso de que el evento sea inválido, entonces
el byte que se leyó no correspondía al evento sino que era el primer byte del
mensaje, en ese caso se leerán longitud - 1 bytes adicionales y se guardarán todos
ellos en mensaje. Los parámetros evento
, canal
y
longitud
son de entrada/salida. En el caso de que se lea un evento nuevo
se modificarán (releer la documentación de decodificar_evento()
), en caso
contrario se podrá usar el valor que ya tenían las variables que corresponde al
último evento leído. La función debe devolver true
si todo funcionó
correctamente.
Escribir una función
void descartar_metaevento(FILE *f, uint8_t tamagno);
que ya
habiendo sido decodificado del archivo f
un metaevento ignore los
tamagno
bytes que corresponden a la información del mismo.
Programa
Se provee un programa de pruebas main.c
que implementa la lógica de
alto nivel de procesar un archivo MIDI utilizando las funciones que se
desarrollan en este trabajo.
Se entregan además archivos MIDI de ejemplo y salidas de ejemplo de qué hace este programa al procesar esos archivos.
Como en el EJ4 se dejó libertad al alumno al respecto de cómo nombrar las
etiquetas de los tipos enumerativos y como traducir dichas etiquetas en cadenas
entonces habrá que modificar el archivo main.c
para ajustar las
etiquetas a las del alumno, y la salida de las pruebas no será idéntica a la
salida provista.
Sólo se permite modificar el archivo main.c
para modificar las claves
de los enumerativos y para incluir los encabezados propios de la modularización
del alumno. Todo el resto de la entrega debe hacerse en fuentes independientes
desarrollados por el alumno.
Material
El material provisto se descarga de acá: archivos_20201_ej5.tar.gz
Entrega
Deberá entregarse:
El código fuente de los módulos desarrollados en este ejercicion y en el EJ4.
El código fuente del
main.c
modificado.El archivo
Makefile
para compilar el proyecto.
La entrega se realiza a través del sistema de entregas.
El ejercicio es de entrega individual.