Play AVI with DirectX
by Paolo Medici

Introduzione

Questo documento è reso necessario dalla miriade di persone che richiedono delucidazioni sul come playare un AVI su una superficie di DirectDraw.
Applico tutto alla IDirectDrawSurface perchè è più che sufficiente per l'esecuzione. Ovviamente mancheranno alcune variabili globali (o della classe). In questo caso usate un minimo di immaginazione e visto che uso la notazione polacca è facile capire che tipo di variabili sono [non fatemi lavorare troppo].

Windows NT: Requires version 3.1 or later.
Windows: Requires Windows 95 or later.
Windows CE: Unsupported.
Header: Declared in vfw.h.
Import Library: Use vfw32.lib.
 

Inizializzazione

Le seguenti righe sono necessarie per accedere al supporto API dei file AVI:
variabili globali:
CRITICAL_SECTION    m_csAccessBuffer

codice:
AVIFileInit();
InitializeCriticalSection(&m_csAccessBuffer);

La seconda riga permette di accedere a un oggetto di tipo critical section. Se qualcuno vuole approfondire l'argomento c'è tutto quello che desidera su MSDN.
 

Aprire il file AVI

variabili globali:
PAVISTREAM m_pasMovie;
AVISTREAMINFO m_asiMovie;

codice:
ZeroMemory(&m_asiMovie,sizeof(m_asiMovie));
if (AVIStreamOpenFromFile(&m_pasMovie,filename,streamtypeVIDEO, 0,OF_READ,NULL))
    {
    AVIFileExit();
    return FALSE;
    }

Apro il file AVI (il cui nome è nella stringa filename) e preparo due variabili pasMovie e asiMovie.
 

Inizializzare da DirectDraw

LPDIRECTDRAW lpdd;
DDSURFACEDESC ddsd;
ZeroMemory(&ddsd,sizeof(DDSURFACEDESC));
ddsd.dwSize = sizeof(DDSURFACEDESC);
if(lpdd->GetDisplayMode(&ddsd)==0) return Initialize(&ddsd.ddpfPixelFormat);
In questo modo ho ottenuto dall'interfaccia IDirectDraw il pixel format della surface in cui voglio renderizzare il filmato. Questo codice può essere sostituito da quello che richiede il pixel format dalla surface scelta.
 

Inizializzare il decoder video

variabili globali:
LPBITMAPV4HEADER m_lpb4hTargetFmt; Il formato della superficie di Output
LPBITMAPINFOHEADER m_lpScrFmt; Il formato della superficie di Input
LPBYTE m_lpInput, m_lpOutput;
variabili locali:
LONG lFmtLenght;
DWORD bitsize;

AVIStreamFormatSize(m_pasMovie,0,&lFmtLenght);

restituisce informazioni varie e sulla dimensione dell'header
m_lpScrFmt = (LPBITMAPINFOHEADER)stdalloc(lFmtLenght);
m_lpb4hTargetFmt = (LPBITMAPV4HEADER)stdalloc(max(lFmtLenght, sizeof(BITMAPV4HEADER)));
al posto di stdalloc usate il metodo di allocazione prefetito
ZeroMemory(m_lpb4hTargetFmt,sizeof(BITMAPV4HEADER));
AVIStreamReadFormat(m_pasMovie,0,m_lpScrFmt,&lFmtLenght);

leggere l'header

A questo punto hai le informazioni sull'immagine sorgente:
Dimensione: m_lpScrFmt->biSizeImage
Larghezza e altezza: m_lpScrFmt->biWidth, m_lpScrFmt->biHeight
Nome del compressore: m_lpScrFmt->biCompression (in FOURCC)

Chiediamo il numero di frame:

m_lFrames = AVIStreamLength(m_pasMovie);
Chiediamo informazioni sulla compressione:
AVIStreamInfo(m_pasMovie,&m_asiMovie,sizeof(AVISTREAMINFO));
A questo punto hai le informazioni sul tipo di compressione:
FourCC Code: m_asiMovie.fccType
Qualità: m_asiMovie.dwQuality
FPS (Frame al secondo): m_dwFps = m_asiMovie.dwRate / m_asiMovie.dwScale;
Nome del filmato: m_asiMovie.szName

Creiamo la caratteristiche della superficie destinazione:

memcpy(m_lpb4hTargetFmt,m_lpScrFmt,lFmtLenght);
m_lpb4hTargetFmt->bV4Size = max(lFmtLenght,sizeof(BITMAPV4HEADER));
Settiamo il bit depth della destinazione con il pixelformat passato alla procedura e ottenuto dal DirectDraw:
m_lpb4hTargetFmt->bV4BitCount = lppf->dwRGBBitCount;
Per comodità precalcoliamo i byte per pixel:
bitsize = (m_lpb4hTargetFmt->bV4BitCount+7)>>3;
Per via empirica a questo punto io ho fatto questi settaggi. Funzionano ed è meglio tenerli così
m_lpb4hTargetFmt->bV4V4Compression = BI_BITFIELDS;
if ((m_lpb4hTargetFmt->bV4BitCount==24)
 || (m_lpb4hTargetFmt->bV4BitCount==32))
   m_lpb4hTargetFmt->bV4V4Compression = BI_RGB;

m_lpb4hTargetFmt->bV4ClrUsed = 0;

Prendiamo i valori delle maschere di BIT direttamente dal pixel format del directdraw, un puntatore LPDDPIXELFORMAT lppf che avevo ottenuto precedentemente e ho passato adesso.
m_lpb4hTargetFmt->bV4RedMask   = lppf->dwRBitMask;
m_lpb4hTargetFmt->bV4GreenMask = lppf->dwGBitMask;
m_lpb4hTargetFmt->bV4BlueMask  = lppf->dwBBitMask;
m_lpb4hTargetFmt->bV4AlphaMask = lppf->dwRGBAlphaBitMask;
Precalcoliamo le dimensioni delle superfici di rendering, serviranno in futuro.
m_lLinePitch = m_lpb4hTargetFmt->bV4Width * bitsize;
m_lLength = m_lpb4hTargetFmt->bV4SizeImage = m_lLinePitch * m_lpb4hTargetFmt->bV4Height;
Se invece il file suggerisce una dimensione usiamo questa
if (m_asiMovie.dwSuggestedBufferSize)
 m_lLength = m_asiMovie.dwSuggestedBufferSize;

Creiamo il decompressore

m_hicDecompressor = ICDecompressOpen(ICTYPE_VIDEO,
       m_asiMovie.fccHandler,
     m_lpScrFmt,
     (LPBITMAPINFOHEADER)m_lpb4hTargetFmt);
if (!m_hicDecompressor)
  return FALSE;
Allochiamo la memoria per le superfici di input e output:
m_lpInput = (BYTE *) stdalloc(m_lLength);
m_lpOutput = (BYTE *) stdalloc(m_lpb4hTargetFmt->bV4SizeImage);

ICDecompressBegin(m_hicDecompressor,m_lpScrFmt,
            (LPBITMAPINFOHEADER)m_lpb4hTargetFmt);

m_iTimeTick = (1000*m_asiMovie.dwScale +
               (m_asiMovie.dwRate>>1)) /m_asiMovie.dwRate;

Frame per Frame

variabili Globali:
UINT m_update;
codice:
m_update++;
if (iFrame<m_lFrames) {
   AVIStreamRead(m_pasMovie, iFrame, 1, m_lpInput, m_lLength, NULL, NULL);
   EnterCriticalSection(&m_csAccessBuffer);
   ICDecompress(m_hicDecompressor, 0, m_lpScrFmt, m_lpInput,
    (LPBITMAPINFOHEADER)m_lpb4hTargetFmt, m_lpOutput);
   LeaveCriticalSection(&m_csAccessBuffer);
   return m_lpOutput;
 }
Questa procedura richiede in ingresso un integer iFrame che indica il numero del frame di cui si vuole fare il rendering.
A questo punto chiunque penserebbe di copiare il puntatore m_lpOutput nella surface o dove desidera. Ma l'immagine, come insegna ma Microsoft è down-top, cioè è invertita in altezza. Avete il pitch della sorgente, avete la sua altezza, non vi resta che copiare la surface, o (cosa che io non ho ancora provato) usare il flip hardware del BitBlt.
 

Chiusura

chiusura del file avi e liberazione della memoria

chiudo il decompressore

 if (m_hicDecompressor) {
   ICDecompressEnd(m_hicDecompressor);
   ICClose(m_hicDecompressor);
   }
libero la memoria dei buffer io
stdfree(m_lpOutput);
stdfree(m_lpInput);
Libero la memroria delle strutture bitmap
stdfree(m_lpScrFmt);
stdfree(m_lpb4hTargetFmt);
Rilascio lo stream AVI
AVIStreamRelease(m_pasMovie);
Come al solito al posto di stdfree usate la procedura che usate voi solitamente per liberare la memoria.
 

Fine dell'applicazione

Quando non voglio più usare file AVI nell'applicazione, o in ogni caso prima di uscire chiudo la librerie:
AVIFileExit();
DeleteCriticalSection(&m_csAccessBuffer);

Sonoro

I File Avi con sonoro presentano dei frame iniziali che servono solo per riempire il buffer del suono. Il mio decoder supporta solo file a 16 bit non compressi (PCM compression) per ragioni di qualità e velocità di esecuzione. Per compattezza ho rimosso gran parte del controllo di errore. Se non si riesce a inizializzare il buffer sonoro infatti bisogna che almeno il video si possa vedere.
 

Aprire il file avi con sonoro

Dopo aver aperto il file avi per il video posso aprire lo stesso file per il sonoro:
Variabili globali:
PAVISTREAM m_pasSound;          Handle dello stream sonoro nel file AVI
AVISTREAMINFO m_asiSound;      Informazioni sullo stream sonoro
DWORD m_dwLoadPos, m_dwLoadSize;
DWORD m_dwSpf;

Inizializzazione delle variabili globali

m_iSoundFramesAhead = 0;
m_dwSoundFrame = m_dwBufferSize = 0;
m_dwLoadPos = m_dwLoadSize = 0;
m_dwSpf = 0;
m_lpdsbTon = NULL;

ZeroMemory(&m_pasSound,sizeof(m_pasSound));

m_lpSoundScrFmt = 0;

if (AVIStreamOpenFromFile(&m_pasSound,filename,streamtypeAUDIO,
  0,OF_READ,NULL))
  {
   AVIFileExit();
  return FALSE;
  }

m_iSoundFramesAhead = 0; Spiegazione successiva dei Frames AHead

Inizializzare il sonoro

Dopo aver inizializzato il modulo video, va inizializzato il modulo sonoro.
variabili globali:
LPWAVEFORMATEX lpSoundScrFmt;
DWORD m_dwBufferSize, m_dwLoadSize, m_dwSoundFrame;
LPDIRECTSOUNDBUFFER m_lpdsbTon;
variabili locali:
LONG lFmtLenght;
DSBUFFERDESC dsbd;
Come prima, adesso prendiamo le informazioni dal buffer sonoro:
AVIStreamFormatSize(m_pasSound,0,&lFmtLenght);
m_lpSoundScrFmt = (WAVEFORMATEX *)stdalloc(lFmtLenght);
AVIStreamReadFormat(m_pasSound,0,m_lpSoundScrFmt,&lFmtLenght);
AVIStreamInfo(m_pasSound,&m_asiSound,sizeof(AVISTREAMINFO));
Preparazione del buffer sonoro:
m_dwBufferSize = (m_lpSoundScrFmt->nAvgBytesPerSec * dwBufferTime) / 1000;
m_dwLoadSize = (m_lpSoundScrFmt->nAvgBytesPerSec + m_dwFps - 1 ) / m_dwFps;
m_dwSoundFrame = m_dwBufferSize / m_dwLoadSize;

dsbd.dwSize = sizeof(dsbd);
dsbd.dwFlags = 0;
dsbd.dwBufferBytes = m_dwBufferSize;

dsbd.dwReserved = 0;
dsbd.lpwfxFormat = m_lpSoundScrFmt;

La frequenza del buffer è m_lpSoundScrFmt->nSamplesPerSec
 if(m_lpSoundScrFmt->nChannels==1) {
  m_dwLoadSize&=~0x1; correzzione per allineamento
  m_dwSpf = m_dwLoadSize>>1; 16 bit, mono
 }
  else {
   m_dwLoadSize&=~0x3; correzzione per allineamento
   m_dwSpf = m_dwLoadSize>>2; 16 bit, stereo
   }
lpds è un puntatore a una IDirectSound Interface. Da questo creo il buffer secondario:
if(lpds)
 if (lpds->CreateSoundBuffer(&dsbd,&m_lpdsbTon,NULL)!=0) {
  CloseSound();
  return FALSE;
  }

m_dwLoadPos = 0;

Usare i frame ahead per riempire il buffer sonoro
UINT iEligible;
UINT m_dwSoundSpace;
UINT m_dwAheadSpace;

Frames ahead, i frame prima del video per avere un minimo di prestreaming del suono:

m_iSoundFramesAhead = m_asiSound.dwInitialFrames/m_asiSound.dwScale;
m_dwSoundSpace = m_dwSoundFrame >> 1;
m_dwAheadSpace = m_iSoundFramesAhead - 1;
Calcolo della dimensione dei frame di solo suono che possono essere caricati in memoria:
iEligible = min(m_dwAheadSpace, m_dwSoundSpace);
if(iEligible<1) iEligible = 1;
Riempimento del sound buffer. ReadNextFrame è spiegata di seguito.
for(UINT i=0;i<iEligible;i++) ReadNextFrame();
Inizio del play:
return (m_lpdsbTon) ? (m_lpdsbTon->Play(0,0,DSBPLAY_LOOPING)==DS_OK) : FALSE;

ReadNextFrame

Read Next Frame, svolge (come anche nel caso di solo video) le funzioni principali di controllo. In questo caso è ancora più importante perchè deve controllare anche il sonoro.
Perciò se il numero di frame diventa uguale al massimo numero dei frame bisogna (a seconda della scelta) fermare la riproduzione del buffer sonoro, o ricominciare da capo.
Se iFrame - m_iSoundFramesAhead < 0 bisogna aspettare ancora per caricare il primo frame, perchè sono ancora frames ahead. Superati i frames-ahead basta chiedere il frame non iFrame, ma iFrame - m_iSoundFramesAhead, perchè i frame video cominciano sempre da 0, anche se esistono dei frames fantasma per il sonoro.

Riempiamo il buffer sonoro

DWORD dwSize1,dwSize2;
LPVOID Data1,Data2;
HRESULT hr;
hr = m_lpdsbTon->Lock(m_dwLoadPos*m_dwLoadSize, m_dwLoadSize,&Data1,&dwSize1,&Data2,&dwSize2,0);
if (hr!=0) return hr;
Finalmente ricevo i dati dallo stream direttamente[senza dover usare l'ACM, visto che il suono occupa il meno in un filmato]
AVIStreamRead(m_pasSound, m_lIndex * m_dwSpf, m_dwSpf,Data1,m_dwLoadSize,NULL,NULL);
hr=m_lpdsbTon->Unlock(Data1,dwSize1,Data2,dwSize2);
Uso un riempimento ciclico:
m_dwLoadPos++;
if(m_dwLoadPos>=m_dwSoundFrame) m_dwLoadPos = 0;


Shutdown della sezione sonoro

if (m_lpSoundScrFmt) {
  stdfree(m_lpSoundScrFmt);
  m_lpSoundScrFmt = NULL;
  }
if (m_lpdsbTon)
    {
    m_lpdsbTon->Release();
    m_lpdsbTon = NULL;
    }

Ultime considerazioni

Ci sono diversi metodi per sincronizzare il video e l'audio e riuscire a seguire bene il FPS. Sta comunque a ognuno cercare il metodo preferito (Timer Multimediali, WM_TIMER, Notify Interface del DirectSound). Come sta a ognuno cercare di ottimizzare il codice down-top in assembler o creare direttamente dal puntatore m_lpOutput una surface client e usare PageLock e PageUnlock per velocizzare il tutto.
Paolo Medici, che ultimamente aveva del tempo da perdere in retorica. Dalla Serie delle Guide Veloci per fare Software Miliardari Queste informazioni sono parte mie, parte degli esempi del DirectX e parte di un tutorial che ho trovato su Internet di cui non mi ricordo la provenienza, ma solo l'autore: Gert Wollny. Discuti questo articolo nel forum
Questo articolo ha avuto 12531 contatti. Pagina Aggiornata il 24 luglio 2001