Ingeniería inversa de formato binario utilizando archivos Korg SNG como ejemplo. Parte 2



En un artículo anterior, describí la línea de razonamiento al analizar un formato de datos binarios desconocido. Usando el editor hexadecimal Synalaze It!, Mostré cómo puede analizar el encabezado de un archivo binario y resaltar los bloques de datos principales. Dado que en el caso del formato SNG, estos bloques forman una estructura jerárquica, logré usar la recursividad en la gramática para construir automáticamente su vista de árbol en una forma legible para los humanos.

En este artículo, describiré un enfoque similar que utilicé para analizar directamente los datos de música. Usando las características integradas del editor Hex, crearé un prototipo de un convertidor de datos en el formato Midi simple y común. Tendremos que enfrentar una serie de trampas y rompecabezas sobre la aparentemente simple tarea de convertir muestras de tiempo. Finalmente, explicaré cómo puede usar los resultados obtenidos y la gramática del archivo binario para generar parte del código para el futuro convertidor.

Analizando datos de música


Entonces, es hora de descubrir cómo se almacenan los datos de música en archivos .SNG. En parte, mencioné esto en un artículo anterior. La documentación del sintetizador indica que el archivo SNG puede contener hasta 128 "canciones", cada una de las cuales consta de 16 pistas y una pista maestra (para grabar eventos globales y cambiar los efectos maestros). A diferencia del formato Midi, donde los eventos musicales simplemente se suceden con un delta de tiempo específico, el formato SNG contiene medidas musicales.

Una medida es un tipo de contenedor para una secuencia de notas. La dimensión de medida se indica en notación musical. Por ejemplo, 4/4 - significa que la medida contiene 4 tiempos, cada uno de los cuales tiene la misma duración que un cuarto de nota. En pocas palabras, dicha medida contendrá 4 notas negras, o 2 medias notas, u 8 octavas.

Así es como se ve en notación musical


Las medidas en el archivo SNG se utilizan para editar pistas en el secuenciador de sintetizador incorporado. Usando el menú, puede eliminar, agregar y duplicar medidas en cualquier parte de la pista. También puede realizar ciclos en bucle o cambiar su dimensión. Finalmente, simplemente puede comenzar a grabar una pista desde cualquier medida.

Tratemos de ver cómo se almacena todo esto en un archivo binario. El contenedor común para "canciones" es el bloque SGS1. Los datos para cada canción se almacenan en bloques SDT1:



Los bloques SPR1 y BMT1 almacenan configuraciones generales de canciones (tempo, configuraciones de metrónomo) y configuraciones de pistas individuales (parches, efectos, configuraciones de arpegiador, etc.). Estamos interesados ​​en el bloque TRK1: contiene eventos musicales. Pero debe bajar un par de niveles más de la jerarquía para bloquear MTK1



Finalmente, encontramos nuestras pistas: estos son bloques MTE1. Intentemos grabar una pista vacía de corta duración en el sintetizador y un poco más, para comprender cómo se almacena la información sobre las medidas en forma binaria.



Parece que las medidas se almacenan como estructuras de ocho bytes. Agrega un par de notas:



Por lo tanto, podemos suponer que todos los eventos se almacenan de la misma forma. El comienzo del bloque MTE contiene información aún desconocida, luego la secuencia de estructuras de ocho bytes llega al final. Abra el editor de gramática y cree una estructura de eventos con un tamaño de 8 bytes.

Agregue la estructura mte1Chunk que hereda childChunk y coloque un enlace al evento en la estructura de datos . Indicamos que el evento puede repetirse un número ilimitado de veces. A continuación, a través de experimentos, descubrimos el tamaño y el propósito de varios bytes antes del inicio de la secuencia de eventos de la pista. Tengo lo siguiente:



Al comienzo del bloque MTE1, se almacenan el número de eventos de pista, su número y, presumiblemente, la dimensión del evento. Después de aplicar la gramática, el bloque comenzó a verse así:



Pasemos al flujo de eventos. Después de analizar varios archivos con diferentes secuencias de notas, aparece la siguiente imagen:
# #TipoRepresentación binaria
1Batir 101 00 00 ...
2Nota09 00 3C ...
3Nota09 00 3C ...
4 4Nota09 00 3C ...
5 5Beat201 C3 90 ...
6 6Nota09 00 3C ...
7 7Fin de pista03 88 70 ...

Parece que el primer byte codifica el tipo de evento. Agregue un campo de tipo a la estructura del evento . Creemos dos estructuras más heredando el evento : medir y anotar . Indicamos los valores fijos correspondientes para cada uno de ellos. Y finalmente, agregue enlaces a estas estructuras en los datos del bloque mte1Chunk .



Aplicar los cambios:



Bueno, hemos hecho un buen progreso. Queda por comprender cómo se codifica la altura y la fuerza de la nota, así como el cambio de tiempo de cada evento en relación con los demás. Intentemos nuevamente comparar nuestros archivos con el resultado de exportar a midi, a través del menú del sintetizador. Esta vez estamos específicamente interesados ​​en los eventos de hacer clic en las notas.



Los mismos eventos en el archivo SNG


Genial Parece que el tono y la presión de las notas se codifican exactamente de la misma manera que en el formato midi con solo un par de bytes. Agregue los campos apropiados a la gramática.

Desafortunadamente, las cosas no son tan simples con un cambio temporal.

Nos ocupamos de la duración y delta


En el formato midi, los eventos NoteOn y NoteOff están separados. La duración de una nota está determinada por el tiempo delta entre estos eventos. En el caso del formato SNG, donde no hay análogo al evento NoteOff, la duración y los valores delta deben almacenarse en una estructura.

Para entender cómo se almacenan, grabé varias secuencias de notas de diferentes duraciones en el sintetizador.



Obviamente, los datos que necesitamos están en los últimos 4 bytes de la estructura del evento. La regularidad no es visible a simple vista, por lo que seleccionamos los bytes que nos interesan en el editor y utilizamos la herramienta Panel de datos.

Texto oculto


Aparentemente, tanto la duración de la nota como el cambio de tiempo están codificados por un par de bytes (UInt16). En este caso, el orden de los bytes es inverso: Little Endian. Después de comparar una cantidad suficiente de datos, descubrí que el delta de tiempo aquí no se cuenta desde el evento anterior como en midi, sino desde el comienzo del reloj. Si una nota termina en el siguiente compás, en el compás actual su longitud será 0x7fff, y en el siguiente se repetirá con el mismo delta 0x7fff y la duración medida desde el comienzo de un nuevo compás. En consecuencia, si una nota suena varias medidas, en cada intermedio su duración y delta será igual a 0x7fff.

Pequeño circuito

Las unidades de tiempo delta / duración se cuentan en celdas. La Nota 1 suena normal, y la Nota 2 continúa sonando en las medidas 2 y 3.

En mi opinión, todo esto parece un poco chungo. Por otro lado, en notación musical, las notas que suenan continuamente varias medidas se indican de manera similar por legato.

¿En qué "loros" tenemos una duración? Como midi, los tics se usan aquí. De la documentación se sabe que la duración de un recurso compartido es de 480 ticks. Con un tempo de 100 latidos por minuto y una dimensión de 4/4, la duración del cuarto de nota es (60/100) = 0.6 segundos. En consecuencia, la duración de un tic es 0.6 / 480 = 0.00125 segundos. Un latido estándar de 4/4 durará 4 * 480 = 1920 tics o 2.4 segundos a una velocidad de 100 lpm.

Todo esto nos será útil en el futuro. Mientras tanto, agregue la duración y el delta a nuestra estructura de notas . Además, tenga en cuenta que hay un campo en la estructura táctil que almacena el número de eventos. Otro campo contiene el número de serie de la medida: agréguelos a la estructura de la medida .



Prototipo convertidor


Ahora tenemos suficiente información para intentar convertir los datos. El editor Hex Synalaze It en la versión pro le permite escribir scripts en python o lua. Al crear un script, debe decidir con qué queremos trabajar: con la gramática en sí, con archivos individuales en el disco o de alguna manera procesar los datos analizados. Desafortunadamente, cada una de las plantillas tiene algunas limitaciones. El programa proporciona varias clases y métodos para trabajar, pero no todas son accesibles desde todas las plantillas. Tal vez esto sea una falla en la documentación, pero no he encontrado cómo puede cargar la gramática para una lista de archivos, analizarlos y usar las estructuras resultantes para exportar datos.

Por lo tanto, crearemos un script para trabajar con el resultado de analizar el archivo actual. Esta plantilla implementa tres métodos: init, terminate y processResult. Este último se llama automáticamente y pasa recursivamente a través de todas las estructuras y datos recibidos durante el análisis.

Para escribir los datos convertidos en midi, utilizamos el kit de herramientas MIDI Python (https://github.com/vishnubob/python-midi). Dado que estamos implementando la Prueba de concepto, no realizaremos la conversión de duraciones de nota y delta. En cambio, establecemos valores fijos. Las notas con una duración de 0x7fff o con un delta similar simplemente se descartan por ahora.

Las capacidades del editor de scripts incorporado son muy limitadas, por lo que todo el código tendrá que colocarse en un archivo.

gist.github.com/bkotov/71d7dfafebfe775616c4bd17d6ddfe7b

Entonces, intentemos convertir el archivo y escuchar lo que tenemos


Hmm ... Y resultó bastante interesante. Lo primero que se me ocurrió cuando traté de formular lo que parecía era música sin estructura. Intentaré dar una definición:

Música no estructurada: una pieza musical con una estructura reducida, construida sobre la armonía. Las duraciones e intervalos entre notas se cancelan o reducen a los mismos valores.

Una especie de ruido armonioso. Sea perlado (por analogía con blanco, azul, rojo, rosa, etc.), parece que nadie ha tomado esta combinación.

Tal vez deberíamos intentar entrenar una red neuronal en mis datos, tal vez el resultado sea interesante.

La tarea de calentar la mente.


Todo esto es maravilloso, pero el problema principal aún no está resuelto. Necesitamos convertir las duraciones de las notas en eventos NoteOff, y el desplazamiento de tiempo del evento relativo al inicio de la medida en un delta de tiempo entre eventos adyacentes. Trataré de formular las condiciones del problema de manera más formal.

Desafío
:
1
1
2
3
...
N
2
...
N
1
...



: 1
: 1920
: Int
: Int


: 9
: 0-127
: 0-127
: 0-1920 0xFF
: 0-1920 0xFF

, , 0xFF, =0xFF . , . = = 0xFF.

.

midi. :

:
: 9
: 0-127
: 0-127
: Int

:
: 8
: 0-127
: 0-127
: Int


La tarea está un poco simplificada. En un archivo SNG real, cada medida puede tener una dimensión diferente. Además de los eventos Note On / Off, otros eventos también ocurrirán en la transmisión, por ejemplo, presionando el pedal de sostenido o cambiando el tono usando pitchBend.

Daré mi solución a este problema en el próximo artículo (si hay uno).

Resultados actuales


Como la solución con el script no escala a un número arbitrario de archivos, decidí escribir un convertidor de consola en Swift. Si escribiera un convertidor bidireccional, las estructuras gramaticales creadas me serían útiles en el código. Puede exportarlos a estructuras C o cualquier otro lenguaje utilizando la misma funcionalidad de scripting integrada en Synalize It! Un archivo con un ejemplo de dicha exportación se crea automáticamente cuando selecciona una plantilla de gramática.



Por el momento, el convertidor está completo al 99% (en la forma que más me convenga en términos de funcionalidad). Planeo poner el código y la gramática en github.

Un ejemplo, para el que todo comenzó, puede escucharlo aquí .

Cómo suena esta pieza ya hecha.

Source: https://habr.com/ru/post/442740/


All Articles