
Dans un
article précédent, j'ai décrit le raisonnement lors de l'analyse d'un format de données binaires inconnu. En utilisant l'éditeur hexadécimal Synalaze It!, J'ai montré comment analyser l'en-tête d'un fichier binaire et mettre en évidence les principaux blocs de données. Étant donné que dans le cas du format SNG, ces blocs forment une structure hiérarchique, j'ai réussi à utiliser la récursivité dans la grammaire pour créer automatiquement leur arborescence sous une forme lisible par l'homme.
Dans cet article, je décrirai une approche similaire que j'ai utilisée pour analyser directement les données musicales. En utilisant les fonctionnalités intégrées de l'éditeur Hex, je vais créer un prototype de convertisseur de données au format Midi commun et simple. Nous devrons faire face à un certain nombre d'embûches et de casse-tête concernant la tâche apparemment simple de convertir des échantillons de temps. Enfin, je vais vous expliquer comment vous pouvez utiliser les résultats obtenus et la grammaire du fichier binaire pour générer une partie du code du futur convertisseur.
Analyse des données musicales
Il est donc temps de comprendre comment les données musicales sont stockées dans des fichiers .SNG. En partie, je l'ai mentionné dans un article précédent. La documentation du synthétiseur indique que le fichier SNG peut contenir jusqu'à 128 «morceaux», chacun composé de 16 pistes et d'une piste master (pour enregistrer des événements globaux et changer les effets master). Contrairement au format Midi, où les événements musicaux se suivent simplement avec un delta temporel spécifique, le format SNG contient des mesures musicales.
Une mesure est une sorte de conteneur pour une séquence de notes. La dimension de mesure est indiquée en notation musicale. Par exemple, 4/4 - signifie que la mesure contient 4 temps, dont chacun est égal en durée à une noire. Autrement dit, une telle mesure contiendra 4 noires, ou 2 demi-notes, ou 8 croches.
Voici à quoi cela ressemble en notation musicale Les mesures du fichier SNG sont utilisées pour éditer des pistes dans le séquenceur de synthétiseur intégré. En utilisant le menu, vous pouvez supprimer, ajouter et dupliquer des mesures n'importe où dans la piste. Vous pouvez également boucler des cycles ou modifier leur dimension. Enfin, vous pouvez simplement commencer à enregistrer une piste à partir de n'importe quelle mesure.
Essayons de voir comment tout cela est stocké dans un fichier binaire. Le conteneur commun pour les "chansons" est le bloc SGS1. Les données de chaque morceau sont stockées dans les blocs SDT1:

Les blocs SPR1 et BMT1 stockent les paramètres généraux des morceaux (tempo, paramètres du métronome) et les paramètres des pistes individuelles (patchs, effets, paramètres d'arpégiateur, etc.). Nous sommes intéressés par le bloc TRK1 - il contient des événements musicaux. Mais vous devez descendre quelques niveaux de plus dans la hiérarchie - pour bloquer MTK1

Enfin, nous avons trouvé nos traces - ce sont des blocs MTE1. Essayons d'enregistrer une piste vide de courte durée sur le synthétiseur et encore un peu plus longtemps - pour comprendre comment les informations sur les mesures sous forme binaire sont stockées.

Il semble que les mesures soient stockées sous forme de structures à huit octets. Ajoutez quelques notes:

Ainsi, nous pouvons supposer que tous les événements sont stockés sous la même forme. Le début du bloc MTE contient des informations encore inconnues, puis la séquence de structures à huit octets se termine. Ouvrez l'éditeur de grammaire et créez une structure d'
événements d'une taille de 8 octets.
Ajoutez la structure
mte1Chunk qui hérite de
childChunk et placez un lien vers l'
événement dans la structure de
données . Nous indiquons que l'
événement peut être répété un nombre illimité de fois. Ensuite, à travers des expériences, nous découvrons la taille et le but de plusieurs octets avant le début du flux d'événements de la piste. J'ai obtenu ce qui suit:

Au début du bloc MTE1, le nombre d'événements de piste, son nombre et, vraisemblablement, la dimension de l'événement sont stockés. Après avoir appliqué la grammaire, le bloc a commencé à ressembler à ceci:

Passons au flux des événements. Après avoir analysé plusieurs fichiers avec différentes séquences de notes, l'image suivante apparaît:
# | Tapez | Représentation binaire |
---|
1 | Beat1 | 01 00 00 ... |
2 | Remarque | 09 00 3C ... |
3 | Remarque | 09 00 3C ... |
4 | Remarque | 09 00 3C ... |
5 | Beat2 | 01 C3 90 ... |
6 | Remarque | 09 00 3C ... |
7 | Fin de piste | 03 88 70 ... |
Il semble que le premier octet code le type d'événement. Ajoutez un champ de
type à la structure d'
événement . Créons deux autres structures héritant de l'
événement :
mesurer et
noter . Nous indiquons les valeurs fixes correspondantes pour chacun d'eux. Et enfin, ajoutez des liens vers ces structures dans les
données du bloc
mte1Chunk .

Appliquez les modifications:

Eh bien, nous avons bien progressé. Reste à comprendre comment la hauteur et la force de la note sont encodées, ainsi que le décalage temporel de chaque événement par rapport aux autres. Essayons à nouveau de comparer nos fichiers avec le résultat de l'export vers midi, effectué via le menu synthétiseur. Cette fois, nous sommes spécifiquement intéressés par les événements de notes de clic.

Les mêmes événements dans le fichier SNG Super! Il semble que la hauteur et la pression des notes soient encodées exactement de la même manière qu'au format midi avec seulement quelques octets. Ajoutez les champs appropriés à la grammaire.
Malheureusement, les choses ne sont pas si simples avec un changement temporaire.
Nous traitons la durée et le delta
Au format midi, les événements NoteOn et NoteOff sont séparés. La durée d'une note est déterminée par le temps delta entre ces événements. Dans le cas du format SNG, où il n'y a pas d'analogue de l'événement NoteOff, les valeurs delta de durée et de temps doivent être stockées dans une structure.
Pour comprendre comment ils sont stockés, j'ai enregistré plusieurs séquences de notes de durées différentes sur le synthétiseur.

De toute évidence, les données dont nous avons besoin se trouvent dans les 4 derniers octets de la structure d'événements. La régularité n'est pas visible à l'œil nu, nous sélectionnons donc les octets qui nous intéressent dans l'éditeur et utilisons l'outil Data Panel.
Apparemment, la durée de la note et le décalage temporel sont codés par une paire d'octets (UInt16). Dans ce cas, l'ordre des octets est inversé - Little Endian. Après avoir comparé une quantité suffisante de données, j'ai découvert que le delta de temps ici n'est pas compté à partir de l'événement précédent comme en midi, mais à partir du début de l'horloge. Si une note se termine dans la mesure suivante, alors dans la mesure actuelle, sa longueur sera 0x7fff, et dans la suivante, elle sera répétée avec le même delta 0x7fff et la durée mesurée depuis le début d'une nouvelle mesure. De même, si une note émet plusieurs mesures, alors dans chaque intermédiaire sa durée et son delta seront égaux à 0x7fff.
Petit circuit
Les unités de temps delta / durée sont comptées dans les cellules. La note 1 semble normale et la note 2 continue de retentir dans les 2e et 3e mesures. À mon avis, tout cela semble un peu béquille. En revanche, en notation musicale, les notes qui sonnent en continu plusieurs mesures sont indiquées de façon similaire par legato.
Dans quels «perroquets» avons-nous une durée? Comme le midi, les tics sont utilisés ici. D'après la documentation, il est connu que la durée d'une part est de 480 ticks. Avec un tempo de 100 battements par minute et une dimension 4/4, la durée de la noire est de (60/100) = 0,6 seconde. En conséquence, la durée d'un tick est de 0,6 / 480 = 0,00125 seconde. Un battement standard de 4/4 durera 4 * 480 = 1920 ticks ou 2,4 secondes à un rythme de 100 bpm.
Tout cela nous sera utile à l'avenir. En attendant, ajoutez la durée et le delta à notre structure de
notes . Notez également qu'il existe un champ dans la structure de tact qui stocke le nombre d'événements. Un autre champ contient le numéro de série de la mesure - ajoutez-les à la structure de la
mesure .

Prototype de convertisseur
Nous avons maintenant suffisamment d'informations pour essayer de convertir les données. L'éditeur Hex Synalaze It dans la version pro vous permet d'écrire des scripts en python ou lua. Lors de la création d'un script, vous devez décider avec quoi nous voulons travailler: avec la grammaire elle-même, avec des fichiers individuels sur le disque ou en quelque sorte traiter les données analysées. Malheureusement, chacun des modèles a certaines limites. Le programme fournit un certain nombre de classes et de méthodes de travail, mais elles ne sont pas toutes accessibles à partir de tous les modèles. C'est peut-être une faille dans la documentation, mais je n'ai pas trouvé comment charger la grammaire d'une liste de fichiers, les analyser et utiliser les structures résultantes pour exporter des données.
Par conséquent, nous allons créer un script pour travailler avec le résultat de l'analyse du fichier actuel. Ce modèle implémente trois méthodes: init, terminate et processResult. Ce dernier est appelé automatiquement et passe récursivement à travers toutes les structures et données reçues lors de l'analyse.
Pour écrire les données converties en midi, nous utilisons la boîte à outils Python MIDI (https://github.com/vishnubob/python-midi). Étant donné que nous mettons en œuvre la preuve de concept, nous n'effectuerons pas la conversion des durées de note et de delta. Au lieu de cela, nous définissons des valeurs fixes. Les notes d'une durée de 0x7fff ou avec un delta similaire sont simplement rejetées pour l'instant.
Les capacités de l'éditeur de script intégré sont très limitées, donc tout le code devra être placé dans un seul fichier.
gist.github.com/bkotov/71d7dfafebfe775616c4bd17d6ddfe7bEssayons donc de convertir le fichier et d'écouter ce que nous avons obtenu
Hmm ... Et ça s'est avéré assez intéressant. La première chose qui m'est venue à l'esprit lorsque j'ai essayé de formuler à quoi cela ressemblait était une musique sans structure. Je vais essayer de donner une définition:
Musique non structurée - un morceau de musique avec une structure réduite, construit sur l'harmonie. Les durées et intervalles entre les notes sont annulés ou réduits aux mêmes valeurs.Une sorte de bruit harmonieux. Qu'elle soit nacrée (par analogie avec le blanc, le bleu, le rouge, le rose, etc.), il semble que personne n'ait pris cette combinaison.
Peut-être que nous devrions essayer de former un réseau neuronal sur mes données, peut-être que le résultat sera intéressant.
La tâche de réchauffer l'esprit
Tout cela est merveilleux, mais le problème principal n'est toujours pas résolu. Nous devons convertir la durée des notes en événements NoteOff et le décalage temporel de l'événement par rapport au début de la mesure en un delta temporel entre les événements adjacents. J'essaierai de formuler plus formellement les conditions du problème.
Défi:
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 tâche est un peu simplifiée. Dans un vrai fichier SNG, chaque mesure peut avoir une dimension différente. En plus des événements Note On / Off, d'autres événements se produisent également dans le flux, par exemple, en appuyant sur la pédale de sustain ou en changeant la hauteur à l'aide de pitchBend.
Je donnerai ma solution à ce problème dans le prochain article (s'il y en a un).
Résultats actuels
Étant donné que la solution avec le script ne s'adapte pas à un nombre arbitraire de fichiers, j'ai décidé d'écrire un convertisseur de console dans Swift. Si j'écrivais un convertisseur bidirectionnel, les structures de grammaire créées me seraient utiles dans le code. Vous pouvez les exporter vers des structures C ou tout autre langage en utilisant la même fonctionnalité de script intégrée à Synalize It! Un fichier contenant un exemple d'une telle exportation est créé automatiquement lorsque vous sélectionnez un modèle de grammaire.

Pour le moment, le convertisseur est complet à 99% (sous la forme qui me convient en termes de fonctionnalité). J'ai l'intention de mettre le code et la grammaire sur github.
Un exemple, pour lequel tout a commencé,
vous pouvez l'écouter ici .
Comment cette pièce semble prête à l'emploi.