Reverse Engineering im Binärformat am Beispiel von Korg SNG-Dateien. Teil 2



In einem früheren Artikel habe ich die Argumentation beim Parsen eines unbekannten Binärdatenformats beschrieben. Mit dem Hex-Editor Synalaze It! Habe ich gezeigt, wie Sie den Header einer Binärdatei analysieren und die Hauptdatenblöcke hervorheben können. Da diese Blöcke im SNG-Format eine hierarchische Struktur bilden, konnte ich die Rekursion in der Grammatik verwenden, um ihre Baumansicht automatisch auf eine für Menschen lesbare Weise zu erstellen.

In diesem Artikel werde ich einen ähnlichen Ansatz beschreiben, mit dem ich die Musikdaten direkt analysiert habe. Mit den integrierten Funktionen des Hex-Editors werde ich einen Prototyp eines Datenkonverters im allgemeinen und einfachen Midi-Format erstellen. Wir werden uns einer Reihe von Fallstricken und Rätseln über die scheinbar einfache Aufgabe stellen müssen, Zeitmuster zu konvertieren. Abschließend werde ich erklären, wie Sie die erhaltenen Ergebnisse und die Grammatik der Binärdatei verwenden können, um einen Teil des Codes für den zukünftigen Konverter zu generieren.

Analysieren von Musikdaten


Es ist also Zeit herauszufinden, wie Musikdaten in .SNG-Dateien gespeichert werden. Zum Teil habe ich dies in einem früheren Artikel erwähnt. In der Synthesizer-Dokumentation heißt es, dass die SNG-Datei bis zu 128 „Songs“ enthalten kann, von denen jeder aus 16 Spuren und einer Master-Spur besteht (zum Aufzeichnen globaler Ereignisse und zum Ändern von Master-Effekten). Im Gegensatz zum Midi-Format, bei dem Musikereignisse einfach mit einem bestimmten Zeitdelta aufeinander folgen, enthält das SNG-Format Musikmaße.

Ein Takt ist eine Art Container für eine Folge von Noten. Die Maßdimension wird in Notenschrift angegeben. Zum Beispiel bedeutet 4/4 -, dass der Takt 4 Schläge enthält, von denen jeder einer Viertelnote entspricht. Einfach ausgedrückt enthält ein solcher Takt 4 Viertelnoten oder 2 halbe Noten oder 8 Achtel.

So sieht es in Notenschrift aus


Die Kennzahlen in der SNG-Datei werden zum Bearbeiten von Spuren im integrierten Synthesizer-Sequenzer verwendet. Über das Menü können Sie Kennzahlen an einer beliebigen Stelle in der Spur löschen, hinzufügen und duplizieren. Sie können auch Zyklen wiederholen oder ihre Dimension ändern. Schließlich können Sie einfach mit der Aufnahme eines Titels aus einem beliebigen Takt beginnen.

Lassen Sie uns versuchen zu sehen, wie all dies in einer Binärdatei gespeichert ist. Der übliche Container für "Songs" ist der SGS1-Block. Die Daten für jeden Song werden in den Blöcken SDT1 gespeichert:



Die SPR1- und BMT1-Blöcke speichern allgemeine Song-Einstellungen (Tempo, Metronom-Einstellungen) und einzelne Track-Einstellungen (Patches, Effekte, Arpeggiator-Einstellungen usw.). Wir interessieren uns für den TRK1-Block - er enthält Musikereignisse. Sie müssen jedoch noch einige Ebenen der Hierarchie nach unten gehen, um MTK1 zu blockieren



Schließlich haben wir unsere Spuren gefunden - das sind die Blöcke MTE1. Versuchen wir, eine leere Spur von kurzer Dauer und noch ein bisschen länger auf dem Synthesizer aufzunehmen - um zu verstehen, wie die Informationen über Kennzahlen in binärer Form gespeichert werden.



Es scheint, dass die Kennzahlen als Acht-Byte-Strukturen gespeichert sind. Fügen Sie ein paar Notizen hinzu:



Wir können also davon ausgehen, dass alle Ereignisse in derselben Form gespeichert sind. Der Anfang des MTE-Blocks enthält noch unbekannte Informationen, dann endet die Folge von Acht-Byte-Strukturen. Öffnen Sie den Grammatikeditor und erstellen Sie eine Ereignisstruktur mit einer Größe von 8 Byte.

Fügen Sie die Struktur mte1Chunk hinzu , die childChunk erbt, und platzieren Sie einen Link zum Ereignis in der Datenstruktur . Wir weisen darauf hin, dass das Ereignis unbegrenzt oft wiederholt werden kann. Als nächstes ermitteln wir durch Experimente die Größe und den Zweck mehrerer Bytes vor dem Start des Ereignisstroms der Spur. Ich habe folgendes bekommen:



Zu Beginn des Blocks MTE1 werden die Anzahl der Spurereignisse, seine Anzahl und vermutlich die Dimension des Ereignisses gespeichert. Nach dem Anwenden der Grammatik sah der Block folgendermaßen aus:



Kommen wir zum Ablauf der Ereignisse. Nach der Analyse mehrerer Dateien mit unterschiedlichen Notenfolgen ergibt sich das folgende Bild:
#TypBinäre Darstellung
1Schlage 101 00 00 ...
2Hinweis09 00 3C ...
3Hinweis09 00 3C ...
4Hinweis09 00 3C ...
5Beat201 C3 90 ...
6Hinweis09 00 3C ...
7Ende der Strecke03 88 70 ...

Es sieht so aus, als ob das erste Byte den Ereignistyp codiert. Fügen Sie der Ereignisstruktur ein Typfeld hinzu. Lassen Sie uns zwei weitere Strukturen erstellen, die das Ereignis erben: Measure und Note . Wir geben für jeden die entsprechenden festen Werte an. Fügen Sie abschließend in den Daten des mte1Chunk- Blocks Links zu diesen Strukturen hinzu .



Übernehmen Sie die Änderungen:



Nun, wir haben gute Fortschritte gemacht. Es bleibt zu verstehen, wie die Höhe und Stärke der Note codiert wird, sowie die Zeitverschiebung jedes Ereignisses relativ zu den anderen. Versuchen wir noch einmal, unsere Dateien mit dem Ergebnis des Exports nach Midi zu vergleichen, der über das Synthesizer-Menü erfolgt. Dieses Mal sind wir speziell an den Ereignissen des Klickens von Notizen interessiert.



Die gleichen Ereignisse in der SNG-Datei


Großartig! Es scheint, dass Tonhöhe und Druck der Noten genauso wie im Midi-Format mit nur wenigen Bytes codiert sind. Fügen Sie der Grammatik die entsprechenden Felder hinzu.

Leider sind die Dinge mit einer vorübergehenden Schicht nicht so einfach.

Wir beschäftigen uns mit der Dauer und dem Delta


Im Midi-Format sind die Ereignisse NoteOn und NoteOff getrennt. Die Dauer einer Note wird durch die Deltazeit zwischen diesen Ereignissen bestimmt. Im SNG-Format, in dem es kein Analogon zum NoteOff-Ereignis gibt, müssen die Delta-Werte für Dauer und Zeit in einer Struktur gespeichert werden.

Um zu verstehen, wie sie gespeichert sind, habe ich mehrere Notenfolgen unterschiedlicher Dauer auf dem Synthesizer aufgezeichnet.



Offensichtlich befinden sich die benötigten Daten in den letzten 4 Bytes der Ereignisstruktur. Die Regelmäßigkeit ist mit bloßem Auge nicht sichtbar, daher wählen wir die für uns interessanten Bytes im Editor aus und verwenden das Datenfenster-Tool.

Versteckter Text


Anscheinend werden sowohl die Dauer der Note als auch die Zeitverschiebung durch ein Paar von Bytes (UInt16) codiert. In diesem Fall ist die Bytereihenfolge umgekehrt - Little Endian. Nachdem ich eine ausreichende Datenmenge verglichen hatte, stellte ich fest, dass das Zeitdelta hier nicht vom vorherigen Ereignis wie im Midi, sondern vom Beginn der Uhr an gezählt wird. Wenn die Note im nächsten Takt endet, beträgt ihre Länge im aktuellen Takt 0x7fff, und im nächsten wird sie mit demselben Delta 0x7fff wiederholt und die Dauer relativ zum Beginn eines neuen Takts gezählt. Wenn eine Note mehrere Takte klingt, sind ihre Dauer und ihr Delta in jedem Zwischentakt gleich 0x7fff.

Kleine Schaltung

Die Zeiteinheiten Delta / Dauer werden in Zellen gezählt. Note 1 klingt normal und Note 2 ertönt im 2. und 3. Takt weiter.

Meiner Meinung nach sieht das alles etwas krücke aus. Auf der anderen Seite werden in der Notenschrift Noten, die kontinuierlich mehrere Takte klingen, in ähnlicher Weise von Legato angezeigt.

In welchen "Papageien" haben wir eine Dauer? Wie bei Midi werden hier Tics verwendet. Aus der Dokumentation ist bekannt, dass die Dauer einer Aktie 480 Ticks beträgt. Bei einem Tempo von 100 Schlägen pro Minute und einer 4/4-Dimension beträgt die Dauer der Viertelnote (60/100) = 0,6 Sekunden. Dementsprechend beträgt die Dauer eines Ticks 0,6 / 480 = 0,00125 Sekunden. Ein Standard-4/4-Schlag dauert 4 * 480 = 1920 Ticks oder 2,4 Sekunden bei einer Geschwindigkeit von 100 Schlägen pro Minute.

All dies wird uns in Zukunft nützlich sein. Fügen Sie in der Zwischenzeit die Dauer und das Delta zu unserer Notenstruktur hinzu . Beachten Sie außerdem, dass in der Taktstruktur ein Feld vorhanden ist, in dem die Anzahl der Ereignisse gespeichert ist. Ein weiteres Feld enthält die Seriennummer der Kennzahl - fügen Sie sie der Kennzahlstruktur hinzu .



Konverter-Prototyp


Jetzt haben wir genug Informationen, um zu versuchen, die Daten zu konvertieren. Mit dem Hex-Editor Synalaze It in der Pro-Version können Sie Skripte in Python oder Lua schreiben. Wenn Sie ein Skript erstellen, müssen Sie entscheiden, mit was wir arbeiten möchten: mit der Grammatik selbst, mit einzelnen Dateien auf der Festplatte oder irgendwie die analysierten Daten verarbeiten. Leider hat jede der Vorlagen einige Einschränkungen. Das Programm bietet eine Reihe von Klassen und Arbeitsmethoden, auf die jedoch nicht über alle Vorlagen zugegriffen werden kann. Vielleicht ist dies ein Fehler in der Dokumentation, aber ich habe nicht gefunden, wie ich die Grammatik für die Liste der Dateien laden, sie analysieren und die resultierenden Strukturen zum Exportieren von Daten verwenden kann.

Daher erstellen wir ein Skript, das mit dem Ergebnis des Parsens der aktuellen Datei arbeitet. Diese Vorlage implementiert drei Methoden: init, terminate und processResult. Letzteres wird automatisch aufgerufen und durchläuft rekursiv alle beim Parsen empfangenen Strukturen und Daten.

Um die konvertierten Daten in Midi zu schreiben, verwenden wir das Python MIDI Toolkit (https://github.com/vishnubob/python-midi). Da wir den Proof of Concept implementieren, werden wir keine Konvertierung der Noten- und Delta-Dauer durchführen. Stattdessen setzen wir feste Werte. Noten mit einer Dauer von 0x7fff oder einem ähnlichen Delta werden vorerst einfach verworfen.

Die Funktionen des integrierten Skripteditors sind sehr begrenzt, sodass der gesamte Code in einer Datei abgelegt werden muss.

gist.github.com/bkotov/71d7dfafebfe775616c4bd17d6ddfe7b

Versuchen wir also, die Datei zu konvertieren und zu hören, was wir haben


Hmm ... und es stellte sich als ziemlich interessant heraus. Das erste, was mir einfiel, als ich versuchte zu formulieren, wie es aussah, war strukturlose Musik. Ich werde versuchen, eine Definition zu geben:

Unstrukturierte Musik - ein Musikstück mit reduzierter Struktur, das auf Harmonie basiert. Notendauern und Intervalle zwischen Noten werden abgebrochen oder auf die gleichen Werte reduziert.

Eine Art harmonisches Geräusch. Lassen Sie es perlmuttartig sein (in Analogie zu Weiß, Blau, Rot, Rosa usw.), es scheint, dass niemand diese Kombination genommen hat.

Vielleicht sollten wir versuchen, ein neuronales Netzwerk auf meine Daten zu trainieren, vielleicht ist das Ergebnis interessant.

Die Aufgabe, den Geist aufzuwärmen


Das ist alles wunderbar, aber das Hauptproblem ist immer noch nicht gelöst. Wir müssen die Notendauer in NoteOff-Ereignisse und den Zeitversatz des Ereignisses relativ zum Beginn des Takts in ein Zeitdelta zwischen benachbarten Ereignissen konvertieren. Ich werde versuchen, die Bedingungen des Problems formeller zu formulieren.

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


Die Aufgabe ist etwas vereinfacht. In einer echten SNG-Datei kann jede Kennzahl eine andere Dimension haben. Zusätzlich zu Note On / Off-Ereignissen treten im Stream auch andere Ereignisse auf, z. B. Drücken des Sustain-Pedals oder Ändern der Tonhöhe mit PitchBend.

Ich werde meine Lösung für dieses Problem im nächsten Artikel geben (falls es eine gibt).

Aktuelle Ergebnisse


Da die Lösung mit dem Skript nicht auf eine beliebige Anzahl von Dateien skaliert werden kann, habe ich beschlossen, einen Konsolenkonverter in Swift zu schreiben. Wenn ich einen Zwei-Wege-Konverter schreiben würde, wären die erstellten Grammatikstrukturen für mich im Code nützlich. Sie können sie in C-Strukturen oder eine andere Sprache exportieren, indem Sie dieselbe Skriptfunktion verwenden, die in Synalize It! Eine Datei mit einem Beispiel für einen solchen Export wird automatisch erstellt, wenn Sie eine Grammatikvorlage auswählen.



Im Moment ist der Konverter zu 99% fertig (in der Form, die mir in Bezug auf Funktionalität passt). Ich habe vor, den Code und die Grammatik auf Github zu setzen.

Ein Beispiel, für das alles gestartet wurde, können Sie hier anhören .

Wie dieses Stück fertig klingt.

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


All Articles