Engenharia reversa de formato binário usando arquivos SNG Korg como exemplo. Parte 2



Em um artigo anterior, descrevi a linha de raciocínio ao analisar um formato de dados binários desconhecido. Usando o editor hexadecimal Synalaze It!, Mostrei como você pode analisar o cabeçalho de um arquivo binário e destacar os principais blocos de dados. Como no caso do formato SNG, esses blocos formam uma estrutura hierárquica, consegui usar a recursão na gramática para criar automaticamente sua visualização em árvore de forma legível por humanos.

Neste artigo, descreverei uma abordagem semelhante usada para analisar diretamente os dados da música. Usando os recursos internos do editor Hex, criarei um protótipo de um conversor de dados no formato Midi comum e simples. Teremos que enfrentar várias armadilhas e resolver a tarefa aparentemente simples de converter amostras de tempo. Por fim, explicarei como você pode usar os resultados obtidos e a gramática do arquivo binário para gerar parte do código para o futuro conversor.

Analisando dados de música


Então, é hora de descobrir como os dados da música são armazenados em arquivos .SNG. Em parte, mencionei isso em um artigo anterior. A documentação do sintetizador afirma que o arquivo SNG pode conter até 128 "músicas", cada uma composta por 16 faixas e uma faixa principal (para gravar eventos globais e alterar os efeitos principais). Diferentemente do formato Midi, onde os eventos de música simplesmente se seguem com um delta de tempo específico, o formato SNG contém medidas de música.

Uma medida é um tipo de contêiner para uma sequência de notas. A dimensão da medida é indicada em notação musical. Por exemplo, 4/4 - significa que a medida contém 4 batidas, cada uma com duração igual a uma semínima. Simplificando, essa medida conterá quatro semínimas, ou duas semitons ou oito colcheias.

Veja como fica na notação musical


As medidas no arquivo SNG são usadas para editar faixas no seqüenciador de sintetizador incorporado. Usando o menu, você pode excluir, adicionar e duplicar medidas em qualquer lugar da faixa. Você também pode repetir ciclos ou alterar suas dimensões. Finalmente, você pode simplesmente começar a gravar uma faixa a partir de qualquer medida.

Vamos tentar ver como tudo isso é armazenado em um arquivo binário. O contêiner comum para "músicas" é o bloco SGS1. Os dados para cada música são armazenados nos blocos SDT1:



Os blocos SPR1 e BMT1 armazenam configurações gerais da música (andamento, configurações do metrônomo) e configurações individuais da faixa (patches, efeitos, configurações do arpejador, etc.). Estamos interessados ​​no bloco TRK1 - ele contém eventos de música. Mas você precisa descer mais alguns níveis da hierarquia - para bloquear o MTK1



Finalmente, encontramos nossas faixas - esses são os blocos MTE1. Vamos tentar gravar uma faixa vazia de curta duração no sintetizador e mais um pouco - para entender como as informações sobre medidas em forma binária são armazenadas.



Parece que as medidas são armazenadas como estruturas de oito bytes. Adicione algumas notas:



Portanto, podemos assumir que todos os eventos são armazenados da mesma forma. O início do bloco MTE contém informações ainda desconhecidas e, em seguida, a sequência de estruturas de oito bytes chega ao fim. Abra o editor de gramática e crie uma estrutura de eventos com um tamanho de 8 bytes.

Adicione a estrutura mte1Chunk que herda childChunk e coloque um link para o evento na estrutura de dados . Indicamos que o evento pode ser repetido um número ilimitado de vezes. Em seguida, através de experimentos, descobrimos o tamanho e o objetivo de vários bytes antes do início do fluxo de eventos da trilha. Eu tenho o seguinte:



No início do bloco MTE1, o número de eventos da faixa, seu número e, presumivelmente, a dimensão do evento são armazenados. Depois de aplicar a gramática, o bloco começou a ficar assim:



Vamos seguir para o fluxo de eventos. Depois de analisar vários arquivos com diferentes seqüências de notas, a seguinte imagem aparece:
#TipoRepresentação binária
1Beat 101 00 00 ...
2Nota09 00 3C ...
3Nota09 00 3C ...
4Nota09 00 3C ...
5Beat201 C3 90 ...
6Nota09 00 3C ...
7Fim da trilha03 88 70 ...

Parece que o primeiro byte codifica o tipo de evento. Adicione um campo de tipo à estrutura do evento . Vamos criar mais duas estruturas que herdam o evento : medir e anotar . Indicamos os valores fixos correspondentes para cada um deles. E, finalmente, adicione links para essas estruturas nos dados do bloco mte1Chunk .



Aplique as alterações:



Bem, fizemos um bom progresso. Resta entender como a altura e a força da nota são codificadas, bem como a mudança de horário de cada evento em relação aos outros. Vamos tentar novamente comparar nossos arquivos com o resultado da exportação para o midi, feito através do menu do sintetizador. Desta vez, estamos especificamente interessados ​​nos eventos de clicar em notas.



Os mesmos eventos no arquivo SNG


Ótimo! Parece que o tom e a pressão das notas são codificados exatamente da mesma maneira que no formato midi, com apenas alguns bytes. Adicione os campos apropriados à gramática.

Infelizmente, as coisas não são tão simples com uma mudança temporária.

Lidamos com a duração e delta


No formato midi, os eventos NoteOn e NoteOff são separados. A duração de uma nota é determinada pelo tempo delta entre esses eventos. No caso do formato SNG, onde não há análogo do evento NoteOff, os valores delta de duração e tempo devem ser armazenados em uma estrutura.

Para entender como eles são armazenados, gravei várias seqüências de notas de diferentes durações no sintetizador.



Obviamente, os dados que precisamos estão nos últimos 4 bytes da estrutura de eventos. A regularidade não é visível a olho nu, portanto, selecionamos os bytes de interesse para nós no editor e usamos a ferramenta Painel de Dados.

Texto oculto


Aparentemente, a duração da nota e o deslocamento no tempo são codificados por um par de bytes (UInt16). Nesse caso, a ordem dos bytes é inversa - Little Endian. Tendo comparado uma quantidade suficiente de dados, descobri que o delta de tempo aqui não é contado a partir do evento anterior, como no midi, mas desde o início do relógio. Se a nota terminar no próximo compasso, no compasso atual seu comprimento será 0x7fff e no próximo será repetido com o mesmo delta 0x7fff e a duração contada em relação ao início de um novo compasso. Da mesma forma, se uma nota emitir várias medidas, em cada uma delas a duração e o delta serão iguais a 0x7fff.

Circuito pequeno

As unidades de tempo delta / duração são contadas em células. A nota 1 soa normal e a nota 2 continua a soar nos segundo e terceiro compassos.

Na minha opinião, tudo isso parece um pouco intrometido. Por outro lado, na notação musical, as notas que tocam continuamente várias medidas são indicadas de maneira semelhante pelo legato.

Em quais "papagaios" temos uma duração? Como midi, os tiques são usados ​​aqui. A partir da documentação, sabe-se que a duração de um compartilhamento é de 480 ticks. Com um andamento de 100 batidas por minuto e uma dimensão 4/4, a duração da semínima é (60/100) = 0,6 segundos. Consequentemente, a duração de um tick é de 0,6 / 480 = 0,00125 segundos. Uma batida 4/4 padrão dura 4 * 480 = 1920 ticks ou 2,4 segundos a uma taxa de 100 bpm.

Tudo isso será útil para nós no futuro. Enquanto isso, adicione a duração e o delta à nossa estrutura de notas . Além disso, observe que há um campo na estrutura do tato que armazena o número de eventos. Outro campo contém o número de série da medida - adicione-os à estrutura da medida .



Protótipo do conversor


Agora temos informações suficientes para tentar converter os dados. O editor Hex Synalaze It na versão pro permite que você escreva scripts em python ou lua. Ao criar um script, você precisa decidir com o que queremos trabalhar: com a própria gramática, com arquivos individuais no disco ou de alguma forma processar os dados analisados. Infelizmente, cada um dos modelos tem algumas limitações. O programa fornece várias classes e métodos para trabalhar, mas nem todos são acessíveis a partir de todos os modelos. Talvez seja uma falha na documentação, mas não encontrei como você pode carregar a gramática de uma lista de arquivos, analisá-los e usar as estruturas resultantes para exportar dados.

Portanto, criaremos um script para trabalhar com o resultado da análise do arquivo atual. Este modelo implementa três métodos: init, terminate e processResult. O último é chamado automaticamente e recursivamente passa por todas as estruturas e dados recebidos durante a análise.

Para gravar os dados convertidos no midi, usamos o Python MIDI toolkit (https://github.com/vishnubob/python-midi). Como estamos implementando a Prova de Conceito, não realizaremos a conversão de durações de notas e delta. Em vez disso, definimos valores fixos. Notas com duração de 0x7fff ou delta semelhante são simplesmente descartadas por enquanto.

Os recursos do editor de script interno são muito limitados, portanto todo o código deverá ser colocado em um arquivo.

gist.github.com/bkotov/71d7dfafebfe775616c4bd17d6ddfe7b

Então, vamos tentar converter o arquivo e ouvir o que temos


Hmm ... E ficou bem interessante. A primeira coisa que me ocorreu quando tentei formular como era a música sem estrutura. Vou tentar dar uma definição:

Música não estruturada - uma música com uma estrutura reduzida, construída em harmonia. As durações e os intervalos das notas são cancelados ou reduzidos para os mesmos valores.

Uma espécie de barulho harmonioso. Seja perolado (por analogia com branco, azul, vermelho, rosa etc.), parece que ninguém adotou essa combinação.

Talvez devêssemos tentar treinar uma rede neural nos meus dados, talvez o resultado seja interessante.

A tarefa de aquecer a mente


Tudo isso é maravilhoso, mas o principal problema ainda não está resolvido. Precisamos converter as durações das notas em eventos NoteOff e o deslocamento de tempo do evento relativo ao início da medida em um delta de tempo entre eventos adjacentes. Vou tentar formular as condições do problema de maneira mais formal.

Desafio
:
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


A tarefa é um pouco simplificada. Em um arquivo SNG real, cada medida pode ter uma dimensão diferente. Além dos eventos Note On / Off, outros eventos também ocorrerão no fluxo, por exemplo, pressionando o pedal de sustentação ou alterando o tom usando pitchBend.

Vou dar a minha solução para esse problema no próximo artigo (se houver).

Resultados atuais


Como a solução com o script não se ajusta a um número arbitrário de arquivos, decidi escrever um conversor de console no Swift. Se eu escrevesse um conversor bidirecional, as estruturas gramaticais criadas seriam úteis para mim no código. Você pode exportá-los para estruturas C ou qualquer outro idioma usando a mesma funcionalidade de script criada no Synalize It! Um arquivo com um exemplo dessa exportação é criado automaticamente quando você seleciona um modelo de gramática.



No momento, o conversor está 99% completo (no formato que me convém em termos de funcionalidade). Eu pretendo colocar o código e a gramática no github.

Um exemplo, para o qual tudo foi iniciado, você pode ouvir aqui .

Como esta peça soa pronta.

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


All Articles