Programmieren mit C++

Code-Beispiele & Lösungen

Klassen für allgemeine Aufgaben

MIDI-Files von Ressourcen abspielen

Die Klasse CMIDI erlaubt das Abspielen von MIDI-Dateien direkt aus den Ressourcen, ohne daß zuvor eine temporäre Kopie als Datei erzeugt werden muß. Allerdings kann die Klasse auch MIDI-Dateien als solche abspielen. In diesem Fall wird die gesamte MIDI-Datei in den Speicher geladen und ein Zeiger auf den Buffer an die Methode Create() übergeben.

Öffentliche Methoden

CMIDI();

Der Konstruktor übernimmt keine Parameter und initialisiert lediglich das erzeugte Objekt. Die Zuweisung der MIDI-Daten erfolgt separat über die Methode Create().

Konstruktor CMIDI

BOOL Create(LPVOID pSoundData, 
            DWORD dwSize,
            CWnd * pParent = 0);
BOOL Create(LPCTSTR pszResID, 
            CWnd * pParent = 0);
BOOL Create(UINT uResID,
            CWnd * pParent = 0);

Die Methode Create() erzeugt einen MIDI-Stream und initialisiert alle internen Member und Strukturen, um die MIDI-Daten anschließend abspielen zu können. Die Daten werden im ersten Fall als Zeiger pSoundData auf einen Buffer, der die Daten enthält, übergeben, wobei dwSize die Größe des Buffers bezeichnet. Die beiden anderen Varianten der Methode übergeben einen Ressourcen-Namen bzw. eine Ressourcen-ID.

Alle drei Varianten übergeben als letzten Parameter in pParent eine Referenz auf das besitzende Fenster. Dieser Parameter ist nur dann wichtig, wenn dieses Elternfenster Nachrichten erhalten soll. Eine mögliche Nachricht ist das Ändern des MIDI-Volume auf einem Kanal durch Einflüsse von außen. Die Klasse spezifiziert hierfür eine spezielle Nachricht:

#define WM_MIDI_VOLUMECHANGED WM_USER +23

Die Klasse CMIDI sendet die Nachricht WM_MIDI_VOLUMECHANGED an ihr Elternfenster, sofern ein solches in Create() spezifiziert wurde, immer dann, wenn sich die Lautstärke während des Abspielens ändert, ohne daß die Klasse dies explizit selbst veranlaßt hat. Im WPARAM liefert die Nachricht einen Zeiger auf das CMIDI-Objekt, das die Nachricht absetzte. Das LOWORD des LPARAM beschreibt den Kanal, dessen Lautstärke sich änderte, während im HIWORD die neue Lautstärke in Prozent enthalten ist.

Methode Create

BOOL Play(BOOL bInfinite = FALSE);

Die Methode Play() startet das Abspielen des MIDI-Stücks, beginnend beim ersten Event. Das Funktionsergebnis zeigt den Erfolg der Operation an, wobei TRUE bedeutet, daß die Wiedergabe läuft. Ist bInfinite auf TRUE gesetzt, wiederholt die Methode das Abspielen des Stücks so lange, bis entweder die Wiedergabe mittels Stop() angehalten oder das Wiederholen der Wiedergabe durch SetInfinitePlay(FALSE) unterbunden wird.

Methode Play

BOOL Stop(BOOL bReOpen = TRUE);

Stop() hält die Wiedergabe eines MIDI-Stücks an. Das Flag bReOpen sollte normalerweise nie auf FALSE stehen, es sei denn, es kommt definitiv nicht mehr vor, daß auf das MIDI-Objekt zugegriffen wird. Normalerweise ist dieses Flag jedoch dem Destruktor vorbehalten.

Als Ergebnis liefert die Funktion TRUE, wenn der Aufruf erfolgreich war, ansonsten FALSE.

Methode Stop

BOOL IsPlaying() const

Die Funktion IsPlaying() returniert TRUE, wenn die Wiedergabe aktiv ist, ansonsten FALSE.

Methode IsPlaying

BOOL Pause();

Pause() hält die Wiedergabe des Stücks an, ohne daß die aktuelle Ausgabeposition auf den Anfang des Stücks zurückgesetzt wird. Bei erfolgreichem Aufruf lautet das Funktionsergebnis TRUE, ansonsten FALSE.

Methode Pause

BOOL Continue();

Mittels Continue() kann eine durch Pause() angehaltene Wiedergabe fortgesetzt werden. Die Methode Play() ruft automatisch Continue() auf, wenn beim Aufruf von Play() festgestellt wird, daß die Wiedergabe angehalten ist.

Das Ergebnis liefert den Erfolgsstatus des Funktionsaufrufs.

Methode Continue

BOOL IsPaused() const

IsPaused() returniert den Wert TRUE, wenn die Wiedergabe pausiert, anderenfalls FALSE.

Methode IsPaused

BOOL Rewind();

Mit Rewind() wird der Positionszeiger der MIDI-Events auf den Anfang des MIDI-Stücks gesetzt. Konnte die Operation erfolgreich durchgeführt werden, lautet das Funktionsergebnis TRUE, ansonsten FALSE.

Methode Rewind

DWORD GetChannelCount() const;

GetChannelCount() liefert die Anzahl der Kanäle, für die Lautstärkeinformationen eingestellt werden können.

Methode GetChannelCount

void SetChannelVolume(DWORD channel, 
                      DWORD percent);

Die Methode SetChannelVolume() spezifiziert die Lautstärke des Kanals channel im Parameter percent in Form eines Prozentwerts. Voreingestellt sind 100 Prozent. Der durch channel bezeichnete Kanal muß zwischen 0 und GetChannelCount() - 1 liegen.

Methode SetChannelVolume

DWORD GetChannelVolume(DWORD channel) const;

GetChannelVolume() ermittelt die aktuell eingestellte Lautstärke des Kanals channel.

Methode GetChannelVolume

void SetVolume(DWORD percent);

Die Methode SetVolume() setzt die Lautstärke in Prozent für alle Kanäle gleichzeitig mit einem einzigen Aufruf.

Methode SetVolume

DWORD GetVolume() const;

GetVolume() liefert die aktuelle Gesamtlautstärke aller Kanäle in Prozent.

Methode GetVolume

void SetTempo(DWORD percent);

Die Methode SetTempo() setzt das aktuelle Tempo der Wiedergabe in Prozent. Per Default beträgt sie 100 Prozent.

Methode SetTempo

DWORD GetTempo() const;

GetTempo() liefert die aktuell eingestellte Wiedergabegeschwindigkeit in Prozent.

Methode GetTemp

void SetInfinitePlay(BOOL bSet = TRUE);

Während der Wiedergabe eines MIDI-Stücks kann via SetInfinitePlay() festgelegt werden, ob das Stück permanent wiederholt werden soll (TRUE) oder nicht (FALSE).

Ein Aufruf von Play() überschreibt die Vorgabe mit dem Wert, der in Play() als Parameter übergeben wird. Wird der Funktionsaufruf SetInfinitePlay(TRUE) abgesetzt, während eine einmalige Wiedergabe läuft, wird nach dem Beenden der Wiedergabe sofort mit der erneuten Wiedergabe begonnen. Im umgekehrten Fall wird die aktuelle Wiedergabe bis zum Ende des Stücks fortgesetzt und dann beendet.

Virtuelle Methoden

Neben den Standard-Methoden veröffentlicht die Klasse auch einige virtuelle Methoden, die nicht unbedingt überschrieben werden müssen. Falls es doch erfolgt, muß sichergestellt werden, daß die entsprechenden Methoden der abgeleiteten Klasse die Methoden der Basisklasse CMIDI aufrufen.

Methode SetInfinitePlay

virtual void OnMidiOutOpen(); 

Die Methode OnMidiOutOpen() wird aufgerufen, wenn ein MIDI-Ausgabegerät geöffnet wird.

Methode OnMidiOutOpen

virtual void OnMidiOutClose(); 

Wird ein MIDI-Ausgabegerät geschlossen, so ruft die Klasse automatisch OnMidiOutClose() auf.

Methode OnMidiOutClose

virtual void OnMidiOutDone(MIDIHDR &); 

Die Methode OnMidiOutDone() wird aufgerufen, wenn der als Parameter spezifizierte, System-exklusive Befehl oder Streambuffer gespielt wurde und an die Anwendung zurückkehrt.

Methode OnMidiOutDone

virtual void OnMidiOutPositionCB(
    MIDIHDR &, MIDIEVENT &);

Wenn ein MEVT_F_CALLBACK-Event im MIDI-Ausgabe- Stream erreicht wurde, wird OnMidiOutPosition()gerufen.

Implementation

Methode OnMidiOutPosition

Die Klasse CMIDI implementiert die Wiedergabe der MIDI-Daten über das Schreiben auf einen MidiStream. Dazu müssen die MIDI-Daten spurweise verwaltet und verarbeitet werden, bevor sie in der richtigen Reihenfolge und zum richtigen Zeitpunkt in den Stream geschrieben werden. Die Verwaltung eines Tracks erfolgt über die Struktur TRACK, die wie folgt aufgebaut ist:

MidiStream

struct TRACK
{
  DWORD  fdwTrack;
  DWORD  dwTrackLength;
  LPBYTE pTrackStart;
  LPBYTE pTrackCurrent;
  DWORD  tkNextEventDue;
  BYTE   byRunningStatus;
 TRACK()
  : fdwTrack(0),dwTrackLength(0),
    pTrackStart(0), pTrackCurrent(0),
    tkNextEventDue(0), byRunningStatus(0)
  {}
};

Das Feld fdwTrack speichert Flags des Tracks, wobei derzeit jedoch nur ein Flag verwendet wird:

Konstante

Bedeutung

ITS_F_ENDOFTRK

Ende des Tracks

Die Länge des Tracks steht in dwTrackLength. pTrackStart und pTrackCurrent zeigen auf den Anfang des Track-Datenbuffers bzw. das nächste zu lesende Byte im Buffer. In tkNextEventDue ist die absolute Zeit des nächsten Events gespeichert, während in byRunningStatus der Running-Status der letzten Channel-Message enthalten ist.

Für die temporäre Speicherung von Events, die darauf warten, in den MidiStream Buffer geschrieben zu werden, existiert die Struktur TEMPEVENT.

Struktur Track

struct TEMPEVENT
{
  DWORD  tkEvent;
  BYTE   byShortData[4];
  DWORD  dwEventLength;
  LPBYTE pLongData;
};

Das Feld tkEvent nimmt die absolute Zeit des nächsten Events auf. Der Eventtyp und die zugehörigen Parameter stehen in byShortData, sofern es sich um eine Channel-Message handelt. Ansonsten zeigt pLongData auf einen Databuffer, dessen Größe dwEventLength spezifiziert.

Zur Übergabe von Informationen über den Ziel-Streambuffer und den aktuellen Status des Konvertierprozesses an das ConvertToBuffer()-System sowie anschließend zur internen Verwendung durch die zugehörigen Low-Level-Funktionen kommt eine CONVERTINFO-Struktur zum Einsatz.

Datenstruktur TEMPEVENT

struct CONVERTINFO
{
  MIDIHDR mhBuffer;
  DWORD   dwStartOffset;
  DWORD   dwMaxLength;
  DWORD   wBytesRecorded;
  DWORD   tkStart;
  BOOL    bTimesUp;
 
  CONVERTINFO()
  : dwStartOffset(0), dwMaxLength(0),
    dwBytesRecorded(0), tkStart(0),
    bTimesUp(FALSE)
  {
  memset(&mhBuffer, 0, sizeof(MIDIHDR));
  }
};

Das Feld mhBuffer enthält den Header eines Standard-Windows-Strembuffers, dessen Startoffset von mhStreamBuffer. lpStart in dwStartOffset abgelegt ist. dwMaxLength spezifiziert die maximale Länge der im aktuellen Durchgang zu konvertierende Daten. Die drei übrigen Felder dienen der internen Verwendung der Konvertierfunktionen zur Spezifikation der absoluten Event-Zeit in tkEvent, dem booleschen Status bTimesUp bzgl. des weiteren zeitabhängigen Füllens des Buffers, sowie der Anzahl aufgenommener Zeichen in wBytesRecorded.

Das Konvertieren der rohen MIDI-Daten für den MidiStream ist ein Vorgang, der auf den ersten Blick recht komplex aussieht und zudem selten in Windows-Applikationen verwendet wird, weil viele den Aufwand scheuen und sich eher der High-Level-Funktionen bedienen. Nachfolgend werden daher die beteiligten Funktionen etwas genauer beleuchtet. Eine der zentralen Funktionen ist ConvertToBuffer(), die MIDI-Daten von Track-Buffer konvertiert und aufbereitet, nachdem zuvor ConvertInit() die benötigten Streams erzeugt und initialisiert hat. ConvertToBuffer() konvertiert Daten, bis ein Fehler festgestellt wird oder der Ausgabepuffer mit so vielen Daten wie möglich gefüllt wurde, ohne dwMaxLength zu übersteigen.

Im Parameter dwFlags kann der Funktion ConvertToBuffer() das Flag CONVERTF_RESET übergeben werden, um anzuzeigen, daß die Konvertierung zurückgesetzt und neu initialisiert werden soll.

Datenstruktur CONVERTINFO

int CMIDI :: ConvertToBuffer(
    DWORD dwFlags, 
    CONVERTINFO * lpciInfo) 
{
  int     nChkErr;

Die Funktion ConvertToBuffer() testet zunächst, ob ein Reset angefordert wurde.

  lpciInfo->dwBytesRecorded = 0;
 
  if( dwFlags & CONVERTF_RESET ) {
    m_dwProgressBytes = 0;
    m_dwStatus = 0;
    memset( &m_teTemp, 0, sizeof(TEMPEVENT));
    m_ptsTrack = m_ptsFound = 0;
  }

Ist die Konvertierung bereits beendet, kehrt die Funktion mit dem Status CONVERTERR_DONE zurück.

  if( m_dwStatus & CONVERTF_STATUS_DONE ) {
    if( m_bLooped ) {
      Rewind();
      m_dwProgressBytes = 0;
      m_dwStatus = 0;
    } else
    return CONVERTERR_DONE;

Fordert der Aufrufer ein Fortsetzen der Konvertierung, obwohl zuvor bereits ein Problem erkannt wurde, erfolgt diesmal der Protest etwas lauter.

  } else if( m_dwStatus & CONVERTF_STATUS_STUCK ) {
      return( CONVERTERR_STUCK );

Besagt der Status, daß kein Fehler vorliegt, aber ein Event bereits ermittelt wurde, so wird dieses Flag gelöscht.

    } else if( m_dwStatus & CONVERTF_STATUS_GOTEVENT ) {
      m_dwStatus ^= CONVERTF_STATUS_GOTEVENT;

Für den Fall, daß ein „Nachzügler“-Event vorliegt, der beim letzten Durchlauf noch nicht komplett abgearbeitet wurde, muß dieser jetzt behandelt werden. Es ist sicherzustellen, daß das Flag „Ende des Tracks“ erst dann abgesetzt wird, wenn der Track wirklich beendet ist.

  if( m_teTemp.byShortData[0] == MIDI_META && 
      m_teTemp.byShortData[1] == MIDI_META_EOT ) {
    if( m_dwMallocBlocks ) {
      delete [] m_teTemp.pLongData;
      m_dwMallocBlocks;
    }
  } else if(( nChkErr = AddEventToStreamBuffer( 
                  &m_teTemp, lpciInfo )) != CONVERTERR_NOERROR ) {

Wenn der Buffer voll ist, wird eine entsprechende Fehlermeldung ausgegeben.

  if( nChkErr == CONVERTERR_BUFFERFULL ) {
    m_dwStatus |= CONVERTF_STATUS_GOTEVENT;
    return CONVERTERR_NOERROR;

Alle Meta-Events außer Tempoänderungen werden übersprungen.

  }else if( nChkErr == CONVERTERR_METASKIP ){
   } else {
      TRACE0("Event konnte nicht in Buffer angefügt werden.\n");
      if( m_dwMallocBlocks ) {
        delete [] m_teTemp.pLongData;
        m_dwMallocBlocks--;
      }
      return( TRUE );
     }
   }
  }

Nach diesen Vorabtests fällt die Methode in eine for-Schleife und sucht zunächst den nächsten Event.

  for(;;) {
    m_ptsFound = 0;
    m_tkNext = 0xFFFFFFFFL;
 
    for( register DWORD idx = 0; idx < m_Tracks.size(); ++idx ) {
      m_ptsTrack = &m_Tracks[idx];
      if ( !(m_ptsTrack->fdwTrack & ITS_F_ENDOFTRK) &&
          (m_ptsTrack->tkNextEventDue < m_tkNext) ) {
        m_tkNext = m_ptsTrack->tkNextEventDue;
        m_ptsFound = m_ptsTrack;
      }
    }

Wird kein Event gefunden, muß das Ende erreicht sein. Die Funktion kehrt mit einem positiven Ergebnis zurück und setzt das m_dwStatus-Member entsprechend.

    if( !m_ptsFound ) {
      m_dwStatus |= CONVERTF_STATUS_DONE;
      return CONVERTERR_NOERROR;
    }

Wird ein Event gefunden, ist als nächstes der Event-Header des Tracks zu extrahieren. Schlägt dies fehl, wird mit einem entsprechenden Fehlerstatus returniert.

    if( !GetTrackEvent( m_ptsFound, &m_teTemp )) {
      m_dwStatus |= CONVERTF_STATUS_STUCK;
      return CONVERTERR_CORRUPT;
    }

Abschließend wiederholt sich der Code zur Bestimmung des Trackendes und des Fehlertracing.

  if( m_teTemp.byShortData[0] == MIDI_META && 
      m_teTemp.byShortData[1] == MIDI_META_EOT ) {
    if( m_dwMallocBlocks ) {
      delete [] m_teTemp.pLongData;
      m_dwMallocBlocks;
    }
  } else if(( nChkErr = AddEventToStreamBuffer( 
                  &m_teTemp, lpciInfo )) != CONVERTERR_NOERROR ) {
  if( nChkErr == CONVERTERR_BUFFERFULL ) {
    m_dwStatus |= CONVERTF_STATUS_GOTEVENT;
    return CONVERTERR_NOERROR;
  }else if( nChkErr == CONVERTERR_METASKIP ){
   } else {
      TRACE0("Event konnte nicht in Buffer angefügt werden.\n");
      if( m_dwMallocBlocks ) {
        delete [] m_teTemp.pLongData;
        m_dwMallocBlocks--;
      }
      return( TRUE );
     }
   }
  }
  return CONVERTERR_NOERROR;
}

Die bereits benutzte Funktion GetTrackEvent() füllt eine Event-Struktur mit den Daten des nächsten Events des Tracks aus.

Methode ConvertToBuffer

BOOL CMIDI :: GetTrackEvent(
    TRACK     * ptsTrack, 
    TEMPEVENT * pteTemp) 
{
  DWORD   idx;
  UINT    dwEventLength;

Das Feld pteTemp->tkEvent des zweiten Parameters nimmt die absolute Zeit des Events (in Ticks) auf, während pteTemp->byShortData[0] MIDI_META enthält, wenn es sich um einen Meta-Event handelt. In diesem Fall steht in pteTemp->byShortData[1] die Meta-Klasse, die entweder MIDI_ SYSEX oder MIDI_SYSEXEND lautet, wenn es sich um einen SysEx-Event handelt. Anderenfalls handelt es sich um eine Channel-Message, und pteTemp->byShortData[1] und pteTemp->byShortData[2] speichern den Rest des Events.

Die Länge des Events steht in pteTemp->dwEventLength und gibt entweder die Länge der Channel-Message in pteTemp-> byShortData oder die Gesamtlänge der Daten in pteTemp-> pLongData an. In dem Buffer, auf den pteTemp->pLongData zeigt, werden die erweiterten Daten eines SysEx- oder Meta-Events abgelegt. Anderenfalls handelt es sich um einen NULL-Zeiger.

Die Rückgabe der Funktion lautet TRUE, wenn ein Event in die Struktur kopiert wurde, anderenfalls FALSE. Eigene Fehlermeldungen werden nur in der DEBUG-Version ausgegeben. Als Nebeneffekt wird der Status des Eingabetracks in ptsTrack->pTrackPointers und ptsTrack->byRunningStatus aktualisiert. Eventuelle alte Daten der temporären Event-Struktur werden eingangs gelöscht.

  memset( pteTemp, 0, sizeof(TEMPEVENT));

Anschließend erfolgt ein Test auf das Ende des Tracks.

  if( ptsTrack->fdwTrack & ITS_F_ENDOFTRK )
    return FALSE;

Liegt ein Event an, beschreibt das erste Byte den Typ des Events, der zunächst ermittelt werden muß.

Methode GetTrackEvent

  BYTE byByte;
  if( !GetTrackByte(ptsTrack, &byByte) )
    return FALSE;

Ist das höchstwertige Bit nicht gesetzt, handelt es sich um eine Channel-Message, die das Statusbyte der letzten Channel-Message benutzt. Die Running-Status-Informationen werden im Falle eines SysEx- oder Meta-Events nicht gelöscht, obwohl dies laut MIDI-Spezifikation eigentlich so sein sollte. Es gibt jedoch einige MIDI-Files, die diese Byte-Sequenz aufweisen. Liegt das Statusbyte nicht vor, liegt für die Methode ein Fehlerstatus vor.

  if( !(byByte & 0x80) ) {
    if( !ptsTrack->byRunningStatus ) {
       TrackError(ptsTrack, gteBadRunStat);
       return FALSE;
     }

Die Übernahme der ersten Daten erfolgt durch:

    pteTemp->byShortData[0] = ptsTrack->byRunningStatus;
    pteTemp->byShortData[1] = byByte;
 
    byByte = pteTemp->byShortData[0] & 0xF0;
    pteTemp->dwEventLength = 2;

Lediglich Programmwechsel- und Channel-Pressure-Events haben eine Länge von 2 Byte. Alle anderen Events sind 3 Byte lang und haben ein weiteres Byte.

  if(( byByte != MIDI_PRGMCHANGE ) && 
     ( byByte != MIDI_CHANPRESS )) {
    if( !GetTrackByte( ptsTrack, &pteTemp->byShortData[2] ))
      return FALSE;
    ++pteTemp->dwEventLength;
  }

Ist es kein Running Status und kein SysEx-Bereich, muß eine normale Channel-Message (0x80-0xEF) vorliegen.

 } else if(( byByte & 0xF0 ) != MIDI_SYSEX ){
  pteTemp->byShortData[0] = byByte;
  ptsTrack->byRunningStatus = byByte;

Die Kanalinformation wird nicht weiter benötigt und das entsprechende Bit gelöscht, so daß nur die Message übrig bleibt.

  byByte &= 0xF0;

Abschließend wird die Länge der Daten korrigiert, und die Daten werden in die Struktur byShortData kopiert.

  dwEventLength = ( byByte == MIDI_PRGMCHANGE || 
                    byByte == MIDI_CHANPRESS ) ? 1 : 2;
  pteTemp->dwEventLength = dwEventLength + 1;
if( !GetTrackByte( ptsTrack, &pteTemp->byShortData[1] ))
   return FALSE;
  if( dwEventLength == 2 )
   if( !GetTrackByte( ptsTrack, &pteTemp->byShortData[2] ))
    return FALSE;

Bezüglich der SysEx-Typen muß das allgemeine Format geparst werden:

BYTE  bEvent (MIDI_SYSEX or MIDI_SYSEXEND)
VDWORD  cbParms
BYTE  abParms[cbParms]
} else if(( byByte == MIDI_SYSEX ) || ( 
              byByte == MIDI_SYSEXEND )) {
  pteTemp->byShortData[0] = byByte;
  if( !GetTrackVDWord( ptsTrack, &pteTemp->dwEventLength )) {
    TrackError( ptsTrack, gteSysExLenTrunc );
    return FALSE;
  }

Für die Aufnahme der Parameter wird ein temporärer Speicherblock allokiert.

  pteTemp->pLongData = new BYTE [pteTemp->dwEventLength];
  if( pteTemp->pLongData == 0 ) {
    TrackError( ptsTrack, gteNoMem );
    return FALSE;
  }

Nach dem erfolgreichen Allokieren des Speicherblocks wird der interne Zähler inkrementiert, um die Blöcke durchzuzählen, die später wieder freizugeben sind.

  ++m_dwMallocBlocks;

Die Daten können nun vom Eingabepuffer in den Parameter data-Buffer kopiert werden.

  for( idx = 0; idx < pteTemp->dwEventLength; idx++ )
   if( !GetTrackByte( ptsTrack, pteTemp->pLongData + idx )) {
     TrackError( ptsTrack, gteSysExTrunc );
     return FALSE;
   }

Das allgemeine Format eines Meta-Events lautet:

BYTE bEvent (MIDI_META)
BYTE bClass
VDWORD cbParms
BYTE abParms[cbParms]

Das Parsing dieser Blöcke lautet:

   } else if( byByte == MIDI_META ) {
    pteTemp->byShortData[0] = byByte;
 
    if( !GetTrackByte( ptsTrack, &pteTemp->byShortData[1] ))
     return FALSE;
 
    if( !GetTrackVDWord( ptsTrack, &pteTemp->dwEventLength )) { 
      TrackError( ptsTrack, gteMetaLenTrunc );
      return FALSE;
    }

Da es einer gültigen Syntax entspricht, Meta-Events ohne Daten zu haben, muß das Vorliegen von Daten verifiziert werden, da ansonsten ein Datenblock der Länge 0 mit NULL-Zeiger vorliegt. Ist ein gültiger Datenblock vorhanden, muß ein neuer Speicherblock für die Aufnahme der Daten allokiert werden.

  if( pteTemp->dwEventLength ) {  
    pteTemp->pLongData = new BYTE [pteTemp->dwEventLength];
    if( pteTemp->pLongData == 0 ) {
      TrackError( ptsTrack, gteNoMem );
      return FALSE;
    }
    ++m_dwMallocBlocks;

Anschließend können die Daten des Events in den Datenbuffer des Parameters kopiert werden.

    for( idx = 0; idx < pteTemp->dwEventLength; idx++ )
      if( !GetTrackByte( ptsTrack, pteTemp->pLongData + idx )) {
        TrackError( ptsTrack, gteMetaTrunc );
        return FALSE;
      }
    }

Messages im nachfolgenden Bereich werden nicht erwartet und sollten auch nicht in einer normalen MIDI-Datei stehen.

    if( pteTemp->byShortData[1] == MIDI_META_EOT )
      ptsTrack->fdwTrack |= ITS_F_ENDOFTRK;
    } else {
        return FALSE;
      }

Die Zeit des Events war bereits als aktuelle Track-Zeit gespeichert worden.

 pteTemp->tkEvent = ptsTrack->tkNextEventDue;

Jetzt muß die Zeit des nächsten Events aktualisiert werden, wobei der obige Code penibel das Ende des Tracks erfassen muß, für den Fall daß der Meta-Event für das Ende des Tracks fehlt.

if( !( ptsTrack->fdwTrack & ITS_F_ENDOFTRK )) {
     DWORD tkDelta;
 
     if( !GetTrackVDWord( ptsTrack, &tkDelta ))
       return FALSE;
 
     ptsTrack->tkNextEventDue += tkDelta;
   }
 
  return TRUE;
}

Die bereits verwendete Funktion GetTrackVDWord() zum Einlesen der VDWord-Daten des Meta-Events parst eine DWORD-Sequenz mit variabler Länge von einem gegebenen Track. Zu beachten ist dabei, daß ein VDWORD im lo-hi- Format abgelegt ist und jedes Byte außer dem letzten das Hi-Bit gesetzt hat.

Als Rückgabe liefert die Funktion die DWORD Daten in lpdw und das Funktionsergebnis TRUE, wenn der Aufruf erfolgreich war, bzw. FALSE, wenn das Ende des Tracks zuvor erreicht wurde.

Event-Typ bestimmen

BOOL CMIDI :: GetTrackVDWord(
    TRACK * ptsTrack, LPDWORD lpdw) 
{
 ASSERT(ptsTrack != 0);
 ASSERT(lpdw != 0);
 
 if( ptsTrack->fdwTrack & ITS_F_ENDOFTRK )
  return FALSE;
 
  BYTE byByte;
  DWORD dw = 0;
 
 do {
  if( !GetTrackByte( ptsTrack, &byByte ))
   return FALSE;
 
  dw = ( dw << 7 ) | ( byByte & 0x7F );
  } while( byByte & 0x80 );
 
  *lpdw = dw;
 
  return TRUE;
}

Nachdem die Konvertierung und Zerlegung der Eingangsdaten beleuchtet wurde, ist noch ein Blick auf die Weitergabe der Events zu werfen, da hier zeitabhängige Ausgaben erfolgen, die entsprechend zu synchronisieren sind.

Die Methode AddEventToStreamBuffer() schreibt die gegebenen Events in den vorliegenden Streambuffer an der vorgegebenen Position. pteTemp muß dazu auf eine ausgefüllte Event-Struktur zeigen, wie sie von GetTrackEvent() erzeugt wird.

Fehler werden bei der Ausgabe direkt auf dem aktiven Gerät ausgegeben.

int CMIDI :: AddEventToStreamBuffer(TEMPEVENT * pteTemp, 
                                    CONVERTINFO *lpciInfo ) 
{
  MIDIEVENT * pmeEvent = (MIDIEVENT *)( lpciInfo->mhBuffer.lpData
                             + lpciInfo->dwStartOffset
                             + lpciInfo->dwBytesRecorded );

Ist ein neuer, leerer Buffer vorhanden, muß die Startzeit gesetzt werden.

  if( !lpciInfo->dwBytesRecorded )
    lpciInfo->tkStart = m_tkCurrentTime;

Abhängig von der gerade gesetzten Startzeit ist nun festzustellen, wie lange der Buffer weiter gefüllt werden soll, bis er offiziell für „voll“ erklärt wird.

  if( m_tkCurrentTime - lpciInfo->tkStart > 
      m_dwBufferTickLength )
    if( lpciInfo->bTimesUp ) {
      lpciInfo->bTimesUp = FALSE;
      return CONVERTERR_BUFFERFULL;
    } else
    lpciInfo->bTimesUp = TRUE;
 
  DWORD tkNow = m_tkCurrentTime;

Die Delta-Zeit ist die absolute Event-Zeit minus der absoluten Zeit, die bereits für diesen Track abgelaufen ist.

Sie ergibt sich als:

  DWORD tkDelta = pteTemp->tkEvent - m_tkCurrentTime;

Die Event-Zeit ist nun die aktuelle Zeit des Tracks.

  m_tkCurrentTime = pteTemp->tkEvent;

Die Methode kann nun damit beginnen, den Stream zu füllen, wobei zunächst die Geschwindigkeit festgelegt wird.

 if( m_bInsertTempo ) {
   m_bInsertTempo = FALSE;
 
  if( lpciInfo->dwMaxLength-
      lpciInfo->dwBytesRecorded < 3*sizeof(DWORD)) {
   return CONVERTERR_BUFFERFULL;
  }
 
  if( m_dwCurrentTempo ) {
   pmeEvent->dwDeltaTime = 0;
   pmeEvent->dwStreamID = 0;
   pmeEvent->dwEvent = ( m_dwCurrentTempo * 100 ) / 
                       m_dwTempoMultiplier;
   pmeEvent->dwEvent |= (((DWORD)MEVT_TEMPO ) << 24 ) | 
                        MEVT_F_SHORT;
 
   lpciInfo->dwBytesRecorded += 3 * sizeof(DWORD);
   pmeEvent += 3 * sizeof(DWORD);
  }
 }

Von Channel-Messages ist bekannt, wie lang sie sind, so daß lediglich drei DWORDs zu kopieren sind: die Delta-Zeit, die Stream-ID und der Event.

  if( pteTemp->byShortData[0] < MIDI_SYSEX ) {
   if( lpciInfo->dwMaxLength-
       lpciInfo->dwBytesRecorded < 3*sizeof(DWORD)) {
     return CONVERTERR_BUFFERFULL;
   }
 
  pmeEvent->dwDeltaTime = tkDelta;
  pmeEvent->dwStreamID = 0;
  pmeEvent->dwEvent = ( pteTemp->byShortData[0] )
                    | (((DWORD)pteTemp->byShortData[1] ) << 8 )
                    | (((DWORD)pteTemp->byShortData[2] ) << 16 )
                    | MEVT_F_SHORT;

Im Falle einer Lautstärkeänderung muß eine Callback-Funktion erzeugt werden, um die neue Lautstärke abzufangen.

  if((( pteTemp->byShortData[0] & 0xF0) == MIDI_CTRLCHANGE ) &&
     ( pteTemp->byShortData[1] == MIDICTRL_VOLUME )) {
     pmeEvent->dwEvent |= MEVT_F_CALLBACK;
   }
   lpciInfo->dwBytesRecorded += 3 *sizeof(DWORD);
  } else if(( pteTemp->byShortData[0] == MIDI_SYSEX ) || 
            ( pteTemp->byShortData[0] == MIDI_SYSEXEND )) {
      TRACE0("AddEventToStreamBuffer: SysEx_Event.ignoriert\n");
 
  if( m_dwMallocBlocks ) {
   delete [] pteTemp->pLongData;
   --m_dwMallocBlocks;
  }

Ansonsten geht es um einen Meta-Event, wobei der einzige Meta-Event, der berücksichtigt wird, ein Tempowechsel ist.

 } else {
    ASSERT( pteTemp->byShortData[0] == MIDI_META );
 
  if( pteTemp->byShortData[1] != MIDI_META_TEMPO ) {
   if( m_dwMallocBlocks ) {
    delete [] pteTemp->pLongData;
    --m_dwMallocBlocks;
   }
   return CONVERTERR_METASKIP;
  }

Es sollten drei Bytes an Parameterdaten vorliegen: Delta-Zeit, Stream-ID und Event-Daten.

  ASSERT(pteTemp->dwEventLength == 3);
 
  if( lpciInfo->dwMaxLength - 
      lpciInfo->dwBytesRecorded < 3 *sizeof(DWORD)) {
   if( m_dwMallocBlocks ) {
    delete [] pteTemp->pLongData;
    --m_dwMallocBlocks;
   }
   return CONVERTERR_BUFFERFULL;
  }
 
  pmeEvent->dwDeltaTime = tkDelta;
  pmeEvent->dwStreamID = 0;

Vor dem Schreiben der Event-Daten muß das hi-lo-Format erneut in ein lo-hi-Format zurückgewandelt werden.

  pmeEvent->dwEvent = ( pteTemp->pLongData[2] )
    | (((DWORD)pteTemp->pLongData[1] ) << 8 )
    | (((DWORD)pteTemp->pLongData[0] ) << 16 );
  pmeEvent->dwEvent |= (((DWORD)MEVT_TEMPO ) << 24 ) | 
      MEVT_F_SHORT;

Die nächsten Anweisungen ermitteln die Länge des Buffers, was primär für Debugzwecke interessant ist.

  m_dwBufferTickLength = (m_dwTimeDivision * 1000 * 
                         BUFFER_TIME_LENGTH) / m_dwCurrentTempo;
  TRACE1("m_dwBufferTickLength = %lu\n", m_dwBufferTickLength);

Des weiteren sind die allokierten Speicherblöcke wieder freizugeben.

  if( m_dwMallocBlocks ) {
    delete [] pteTemp->pLongData;
    --m_dwMallocBlocks;
  }
  lpciInfo->dwBytesRecorded += 3 *sizeof(DWORD);
 }
 
 return CONVERTERR_NOERROR;
}

Initialisiert werden die Streambuffer in der Methode StreamBufferSetup(), die benötigte Streams öffnet.

Die hier gezeigte Vorgehensweise ist zwar sehr aufwendig und geht in die Tiefen der MIDI-Format-Aufschlüsselung, erlaubt aber die bestmögliche Kontrolle.

Allerdings erfordert der Einsatz der Klasse keine Eingriffe in die gezeigten Mechanismen. Man kann sich somit auf das reine Aufrufen der dokumentierten Methoden beschränken.

Methode GetTrackVDWord





Sachgebiet


© 2009-2012 by Alojado Publishing. Alle Rechte vorbehalten. Ausgewiesene Marken gehören ihren jeweiligen Eigentümern.
Mit der Benutzung dieser Seite erkennen Sie die Nutzungsbedingungen und die Datenschutzerklärung an. Der Betreiber übernimmt keine Haftung für den Inhalt verlinkter externer Internetseiten.
Seite erzeugt 2012-05-20 02:40:41 von textarchiv.alojado.de