Letzte Änderungen: 12.03.2009

Zur Hauptseite kurs9


Die Verwaltung der MIDI-Events

Die grosse verkettete Liste (die ListNodes)

Einen Sequencer kann man als eine grosse verkettete Strukturen-Liste ansehen.

Es gibt die Ober-Struktur (den eigentlichen Song), vertikale-Strukturen (Tracks) und die eigentlichen Daten die horizontal und (zeitlich aufsteigend sortiert) angeordneten MIDI-Events (oder auch Audio/Video Events), die selber noch der Übersichtlichkeit wegen in Pattern zusammengefasst sind. Innerhalb dieser Strukturen ist alles mit Listen und Zeigern-den Nodes (Knoten) verkettet.

Wichtig ist das man die Strukturen durch Semaphores absichert, damit grad im Multitasking nicht zwei Tasks/Processe gleichzeitig auf eine der wichtigen Strukturen zugreift - z.B. das Hauptprogramm möchte eine Spur löschen, muss allerdings warten, bis der Abspielprocess mit dem Zugriff auf die Spur fertig ist usw...

Über ListNodes ist also der gesamt Song, die Tracks und die Pattern aufgebaut.

UQWORD ist ein 64-Bit Wert, ein 32 Bit Wert reicht auch völlig aus, falls 64 Bit nicht von der CPU/Compiler unterstützt wird

Diese Strukturen können natürlich nach Belieben geändert und erweitert werden

struct ListNode
{
struct ListNode *ln_Succ; // Pointer to next ListNode (Successor)
struct ListNode *ln_Pred; // Pointer to previous ListNode (Predecessor)
UQWORD timestamp; // die Uhrzeit oder NULL
};
typedef struct ListNode ListNode_s;
typedef struct ListNode *ListNode_p;

Die globale Songverwaltungsstruktur für mehrere Songs
struct Global
{
Semaphore_s global_sema;
ListNode_s Songs_Node; // Zeiger auf den ersten Song, so können auch mehrere Song im Speicher verwaltet werden
...
};

1. Der Song

struct Song
{
Semaphore_s Song_sema;
ListNode_s Tracks_Node; // Zeiger auf den ersten Track innerhalb des Song, es können also beliebig viele Tracks verwendet werden
...
};


2. Die Tracks im Song

struct Track
{
Semaphore_s Track_sema;
ListNode_s Pattern_Node; // Zeiger auf das erste Pattern innerhalb des Tracks, es können also beliebig viele Pattern verwendet werden

...
};


3. Die auf den Tracks angeordneten Pattern

struct Pattern
{
Semaphore_s Pattern_sema;
ListNode_s Event_Node; / Zeiger auf das erste Event innerhalb des Pattern, es können also beliebig viele Events verwendet werden

UQWORD Pattern_start;
UQWORD Pattern_end;
...
};

Die Start/End-Position des Pattern wird durch die Startposition des 1. Event festgelegt, die Endposition eben durch das letzte Event, bzw. ein Noteende.


4. Die innerhalb der Pattern angeordnenten Events (MIDI, interne Events oder Audio-Events)

struct EventNode
{
struct EventNode *ln_Succ; // Pointer to next ListNode (Successor)
struct EventNode *ln_Pred; // Pointer to previous ListNode (Predecessor)

UQWORD en_TimeStamp; // Die Uhrzeit des MIDI-Events - 64 Bit
struct *en_Pattern;// Zeiger auf das Pattern

UBYTE status; // MIDI-Type z.B. Note, ProgChg

// Hier folgen die eigentlich MIDI-Daten...
};
typedef struct EventNode EventNode_s;
typedef struct EventNode *EventNode_p;


// Die MIDI-Events
struct Event_NON // Note
{
struct EventNode *ln_Succ; // Pointer to next ListNode (Successor)
struct EventNode *ln_Pred; // Pointer to previous ListNode (Predecessor)

UQWORD en_TimeStamp; // Die Uhrzeit des MIDI-Events - 64 Bit, 32 Bit reichen wohl auch aus
struct *en_Pattern;// Zeiger auf das Pattern

UBYTE status; // MIDI-Type z.B. Note, ProgChg

// Hier folgen die eigentlich MIDI-Daten...

UBYTE key;
UBYTE velo;
UBYTE velooff;

UQWORD end;
};
#define NON_LEN sizeof(Event_NON)

Hinweis: Eine Note muss immer ein passendes Notenende (end>start) besitzen, welches entweder bei der Aufnahme der Note festgelegt wird (Keyboard-Taste wird losgelassen) oder in einem Editor wird eine Note erzeugt. Eine Note ohne Notenende würde einen hässlichen Notenhänger verursachen, da der Synthesizer kein NoteOff Befehl mehr erhält. Es gibt somit keine NoteOFF-Events, da eintreffende NoteOFF Events nur die Länge von bestehenden NoteOn-Events festlegen. Nach der Aufnahme von MIDI-Noten muss der Sequencer also auch überprüfen ob jede Note auch eine korrekte Länge besitzt.

struct Event_PROGCHG
{
struct EventNode *ln_Succ; // Pointer to next ListNode (Successor)
struct EventNode *ln_Pred; // Pointer to previous ListNode (Predecessor)

UQWORD en_TimeStamp; // Die Uhrzeit des MIDI-Events - 64 Bit, 32 Bit reichen wohl auch aus
struct *en_Pattern;// Zeiger auf das Pattern

UBYTE status; // MIDI-Type z.B. Note, ProgChg

// Hier folgen die eigentlich MIDI-Daten...

UBYTE program;
};
#define PROGCHG_LEN sizeof(Event_PROGCHG)

struct Event_PITCHBEND
{
struct EventNode *ln_Succ; // Pointer to next ListNode (Successor)
struct EventNode *ln_Pred; // Pointer to previous ListNode (Predecessor)

UQWORD en_TimeStamp; // Die Uhrzeit des MIDI-Events - 64 Bit, 32 Bit reichen wohl auch aus
struct *en_Pattern;// Zeiger auf das Pattern

UBYTE status; // MIDI-Type z.B. Note, ProgChg

// Hier folgen die eigentlich MIDI-Daten...

UBYTE lsb;
UBYTE msb;
};
#define PITCHBEND_LEN sizeof(Event_PITCHBEND)

struct Event_CHANNELPRESSURE
{
struct EventNode *ln_Succ; // Pointer to next ListNode (Successor)
struct EventNode *ln_Pred; // Pointer to previous ListNode (Predecessor)

UQWORD en_TimeStamp; // Die Uhrzeit des MIDI-Events - 64 Bit, 32 Bit reichen wohl auch aus
struct *en_Pattern;// Zeiger auf das Pattern

UBYTE status; // MIDI-Type z.B. Note, ProgChg

// Hier folgen die eigentlich MIDI-Daten...

UBYTE pressure;
};
#define CHANNELPRESSURE_LEN sizeof(Event_CHANNELPRESSURE)

struct Event_POLYPRESSURE
{
struct EventNode *ln_Succ; // Pointer to next ListNode (Successor)
struct EventNode *ln_Pred; // Pointer to previous ListNode (Predecessor)

UQWORD en_TimeStamp; // Die Uhrzeit des MIDI-Events - 64 Bit, 32 Bit reichen wohl auch aus
struct *en_Pattern;// Zeiger auf das Pattern

UBYTE status; // MIDI-Type z.B. Note, ProgChg

// Hier folgen die eigentlich MIDI-Daten...
UBYTE key;
UBYTE pressure;
};
#define POLYPRESSURE_LEN sizeof(Event_POLYPRESSURE)

struct Event_CONTROLCHG
{
struct EventNode *ln_Succ; // Pointer to next ListNode (Successor)
struct EventNode *ln_Pred; // Pointer to previous ListNode (Predecessor)

UQWORD en_TimeStamp; // Die Uhrzeit des MIDI-Events - 64 Bit, 32 Bit reichen wohl auch aus
struct *en_Pattern;// Zeiger auf das Pattern

UBYTE status; // MIDI-Type z.B. Note, ProgChg

// Hier folgen die eigentlich MIDI-Daten...
UBYTE nummer;
UBYTE value;
};
#define CONTROLCHG_LEN sizeof(Event_CONTROLCHG)

struct Event_SYSDATA
{
struct EventNode *ln_Succ; // Pointer to next ListNode (Successor)
struct EventNode *ln_Pred; // Pointer to previous ListNode (Predecessor)

UQWORD en_TimeStamp; // Die Uhrzeit des MIDI-Events - 64 Bit, 32 Bit reichen wohl auch aus
struct *en_Pattern;// Zeiger auf das Pattern

UBYTE status; // MIDI-Type z.B. Note, ProgChg

// Hier folgen die eigentlich MIDI-Daten...

UBYTE *data;
ULONG len, end;
};
#define SYSEX_LEN sizeof(Event_SYSDATA)

SysEx-Daten haben keine definierte Länge, deshalb sind die eigentlichen SysEx-Daten in einem eigenen Speicherbereich (UBYTE *data) abgelegt.


Die benötigten Funktionen

Man benötigt zur Verwaltung dieser Strukturen mindestens 3 Funktionen:

Mit AddNodeSort werden dann z.B. neue MIDI-Events in ein Pattern einsortiert, oder ein neues Pattern innerhalb eines Tracks.

Zu beachten ist, dass MIDI-Events IMMER nach ihrer Startposition (UQWORD en_TimeStamp) sortiert sind, ebenfalls die Pattern innerhalb des Tracks.Somit kann der Abspiel-Process beim Abspielen eines Song ganz einfach alle Strukturen von "links" nach "rechts" abspielen.

Ein MIDI-Sequencer ist also eine kleine, nach Startposition sortierte Echtzeit-Datenbank.

Die Event-Memory Pools

Hinweis noch. Das OS sollte MemoryPool-Funktionen zur Verfügung stellen, da ein durchschnittlicher MIDI-Song schon mal 3000 MIDI-Events enthalten kann. Man fordert somit 3000 "kleine" Speicherbereiche vom OS an. Um einen schnellen Zugriff zu bekommen, und um eine Speicherdefragmentierung zu verhindern, sollten diese Speicherbereiche aus sogenannten Pools geholt werden. MIDI-Events sind eben nur ein paar Bytes gross. Für den Camouflage-Sequencer habe ich solche MemPool-Routinen selber programmiert - für jeden MIDI - Datentyp einen eigenen Memory-Pool, denn alle Noten z.B. haben ja die gleiche Byte-Grösse (lässt sich mit sizeof feststellen) und können so prima in eigenen Speicher-Pools verwaltet werden. Durch diese Event-Pools wurde der Zugriff (löschen, erzeugen) auf diese Events auch stark beschleunigt, sie liegen ja alle im "gleichen" Speicherbereich und können somit besser durch den CPU-Cache verwaltet werden.