Úvod
Instalace Ogg SDK
Torie k DirectSound
Část 1
Část 2
Část 3
Část 4
Funkce knihovny vorbisfile.dll
Odkazy
Ke stažení

R.Henys@seznam.cz
ICQ: 159629842

Poslední část. Napíšeme funkci CreateOggCodecObject pro poskytování ukazatelů na rozhraní která poskytujek knihovna OggCodec kterou jsme napsali v předchozích částech. Ve druhé části se pokusíme přehrát testovací soubor v projektu Tester.

Obsah:

4.1 Funkce CreateOggCodecObject

Ve třetí části jste si jistě všimli funkce CreateOggCodecObject. Použili jsme ji k získání ukazatele na rozhraní IOggFile. Nyní se jí podíváme pod kapotu. Opět se jedná o funkci která už byla probírána v kurzu Direct X. Začneme hlavičkovým souborem s definicí. Manager.h:


    #pragma once

    enum OGGCODEC_API OGGIID
    {
       OGGIID_ISoundManager,
       OGGIID_IOggFile
    };

    OGGCODEC_API HRESULT CreateOggCodecObject(OGGIID IntrfaceID,void** ppv);

Jeden z nejkratších souborů celého řešení :) Nejdříve definujeme výčtový datový typ s konstantami pro různá rozhraní jež poskytuje naše knihovna OggCodec. V našem případě je to jen IOggFile a ISoundManager. Další je prototyp funkce CreateOggCodecObject. Je exportovaná z knihovny stejně jako zmíněná rozhraní. Prvním parametrem je konstanta určující rozhraní na které chceme získat ukazatel. Druhý parametr je ukazatel na proměnnou do které se má uložit ukazatel na rozhraní. (Příšerná věta :)). Teď implementace, soubor Manager.cpp:

    #include "StdAfx.h"
    #include "Common.h"
    #include "Interfaces.h"
    #include "OggFile.h"
    #include "SoundManager.h"
    #include "Manager.h"

    OGGCODEC_API HRESULT CreateOggCodecObject(OGGIID InterfaceID,void** ppv)
    {
       static ISoundManager* g_SoundManager = NULL;

       switch (InterfaceID)
       {
       case OGGIID_ISoundManager:
          if (!g_SoundManager)
             *ppv = g_SoundManager = new CSoundManager;
          else
             *ppv = g_SoundManager;
          ((ISoundManager*)(*ppv))->AddRef();
          break;
       case OGGIID_IOggFile:
          *ppv = new COggFile;
          ((IOggFile*)(*ppv))->AddRef();
          break;
       default:
          return E_NOINTERFACE;
       }

       return S_OK;
    }

Ve funkci se nachází statická proměnná typu ukazatel na ISoundManager. To je jen kvůli tomu, aby vždy když si zažádáme o ukazatel na rozhraní ISoundManager byl vrácen stejný ukazatel. Tj. třída CSoundManager bude vytvořena jen při prvním volání. Protože je CSoundManager potomek ISoundManager, můžeme vrátit ukazatel na ISoundManager, které poskytuje jen metody. Při požadavku na rozhraní IOggFile se vytváří vždy nová třída COggFile a je vrácen ukazatel na IOggFile. Pro obě rozhraní se před návratem z funkce volá funkce AddRef, která zvýší počet odkazů na třídu. Snad jsem to moc nezamotal. Pro zdárné pochopení rozhraní je nutné znát dědičnost. Probírala se v kurzu C++ od 13. dílu.

Obsah

4.2 Projekt Tester

Zbývá nám snad už jen použít vytvořená rozhraní v projektu Tester a přehrát si nějaký ten soubor. Vývojové prostředí za nás vytvořilo základní oeknní aplikaci pro Windows. Nemusíme se tedy zabývat vytvářením okýnka, ale jen dopsáním našeho kódu a modifikací toho co vygeneroval průvodce. Některé části nám toiž nevyhovují. Nejdříve se podívjeme na hlavičkové soubory vložené do souboru Tester.cpp

    #include "stdafx.h"
    #include "Tester.h"
    #include "../oggcodec/common.h"
    #include "../oggcodec/interfaces.h"
    #include "../oggcodec/manager.h"

Soubory stdafx.h a Tester.h za nás vložilo již vývojové prostředí. Zbývající tři musíme vložit my. V soubor Common.h jsou společné definicie které vyžaduje veškerý kód projektu. Intrfaces.h obsahuje definice rozhraní a Manager.h obsahuje definici funkce CreateOggCodecObject. Jedna malá úprava která nás čeká je vymazání proměnné HWND hWnd z těla funkce InitInstance. Vymažte jen definici. Potřebujeme mít tuto proměnnou globální a ne aby si ji syslila jedna funkce pro sebe. Nadefinujte ji tedy jako globální proměnnou na začátku souboru před implemtací všech funkcí a za vloženými hlavičkovými soubory. Když už jsme u těch globálních proměnných, definujte ještě jednu typu ukazatel na ISoundManager (ISoundManager* gSoundManager = NULL;). Teď se podíváme na funkci _tWinMain, která obsahuje hlavní programovou smyčku.

    int APIENTRY _tWinMain(HINSTANCE hInstance,HINSTANCE hPrevInstance,LPTSTR lpCmdLine,int nCmdShow)
    {
    // TODO: Place code here.
       MSG msg;
       HACCEL hAccelTable;

       // Initialize global strings
       LoadString(hInstance, IDS_APP_TITLE, szTitle, MAX_LOADSTRING);
       LoadString(hInstance, IDC_TESTER, szWindowClass, MAX_LOADSTRING);
       MyRegisterClass(hInstance);

       // Perform application initialization:
       if (!InitInstance (hInstance, nCmdShow))
       {
          return FALSE;
       }

       hAccelTable = LoadAccelerators(hInstance, (LPCTSTR)IDC_TESTER);

       HRESULT hRet = CreateOggCodecObject(OGGIID_ISoundManager,(void**)&gSoundManager);
       if (FAILED(hRet))
          return hRet;

       hRet = gSoundManager->CreateDirectSoundSystem(hWnd);
       if (FAILED(hRet))
          return hRet;

       hRet = gSoundManager->LoadOgg(0,"test.ogg");

       IOggFile* gOgg = gSoundManager->GetOggFile(0);
       if (gOgg)
       {
          FILE* fstream = fopen("ogg_info.txt","w");
          fprintf(fstream,"Info about file: test.ogg\n");
          fprintf(fstream,"File Vendor: %s\n",gOgg->GetOggFileVendor());
          fprintf(fstream,"File Version: %d\n",gOgg->GetOggFileVersion());
          fprintf(fstream,"File Playing Time: %g\n",gOgg->GetOggFilePlayingTime());
          fprintf(fstream,"File PCM Size: %ld\n",gOgg->GetOggFilePCMSize());

          for (UINT i = 0; i < gOgg->GetCountOfOggFileComments(); i++)
             fprintf(fstream,"Comment %d, %s\n",i,gOgg->GetOggFileComment(i));

          fclose(fstream);
       }

       gOgg->ConvertOggToWav("test.ogg","test.wav");
       hRet = gSoundManager->PlayOgg(0);

       // Main message loop:
       while(true)
       {
        // Look for messages, if none are found then
       // update the state and display it

          if( PeekMessage( &msg, NULL, 0, 0, PM_NOREMOVE ) )
          {
             if( 0 == GetMessage(&msg, NULL, 0, 0 ) )
             {
                // WM_QUIT was posted, so exit
                SAFE_RELEASE(gSoundManager);
                return (int)msg.wParam;
             }

             // Translate and dispatch the message
             TranslateMessage( &msg );
             DispatchMessage( &msg );
          }
          else
          {
             hRet = gSoundManager->CheckAllOggIfNeedReFillBuffer();
          }
       }

       SAFE_RELEASE(gSoundManager);
       return (int) msg.wParam;

    }

Kód vyznačený tučně je ten který jsem přidal nebo upravil. Upravená je hlavní programová smyčka (cyklus while). Zbytek je kód generovaný při vytváření projektu. Začneme tím že požádáme funkci CreateOggCodecObject o ukazatel na ISoundManager stejně jako jsme to dělali v případě rozhraní IOggFile. Jen první parametr je OGGIID_ISoundManager. Máme ukazatel na rozhraní ISoundManager, ale ještě než začneme volat funkce pro práci s ogg, měli bychom inicializovat DirectSound. Zavoláme funkci CreateDirectSoundSystem rozhraní ISoundManager. Zadáváme jen první parametr, handle okna (globální proměnná hWnd). Druhý parametr, mód spolupráce je nepovinný (má výchozí hodnotu DSSCL_PRIORITY). Pokud se to podaří, můžeme se pokusit načíst ogg soubor. Já v tomto příkladu používám pevný název test.ogg, ale není problém brát jméno souboru například z příkazové řádky. Otevřeme soubor test.ogg pomocí funkce LoadOgg. Měli bychom vždy kontrolovat vrácenou hodnotu, zda nedošlo k chybě. Zde to nemám jen z toho důvodu že soubor je zadán na pevno a při testování k žádné chybě nedošlo. Pokud ale budete dělat univerzální přehrávač, vždy kontrolujte vrácenou hodnotu. Platí to dvojnásob hlavně při programování s DirectX. Tedy tam jsem to pocítil nejvíc. Většina kódu je jen kontrola vrácených hodnot a předcházení chybám. Dále se v kódu ptáme na ukazatel rozhraní IOggFile právě otevřeného test.ogg. Následně toto rozhraní používáme k přístupu k funkcím které rozhraní ISoundManager neposkytuje a používáme je k výpisu základních informací o souboru do souboru ogg_info.txt. Poté testujeme funkci ConvertOggToWav. O její funkčnosti by vás měl přesvědčit dekódovaný soubor na disku. Posledním příkazem před hlavní programovou smyčkou je spuštění přehrávání pomocí funkce PlayOgg. V hlavní programové smyčce se kód věnuje hlavně zpracování zpráv a pokud zrovna žádné nejsou (netaháte oknem z místa na místo nebo na něj neklikáme jako diví) volá funkci CheckAllOggIfNeedReFillBuffer aby bylo zajištěno přehrávání souboru. Pokud dojde k ukončení smyčky, tj. aplikace je ukončena, nesmíme zapomenout uvolnit rozhraní pomocí makra SAFE_RELEASE. Poslední úpravy jsou ve funkci WndProc:

    LRESULT CALLBACK WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam)
    {
       int wmId, wmEvent;
       PAINTSTRUCT ps;
       HDC hdc;

       switch (message)
       {
       case WM_ACTIVATEAPP:
          //zjisteni zda je aplikace aktivni, pokud je, prijde zprava WM_ACTIVATE
          IsAppActive = (bool)wParam;
          if (gSoundManager)
             gSoundManager->HandleAppActiveStateChanges(IsAppActive);

          break;

       case WM_COMMAND:
          wmId = LOWORD(wParam);
          wmEvent = HIWORD(wParam);
          // Parse the menu selections:
          switch (wmId)
          {
          case IDM_ABOUT:
             DialogBox(hInst, (LPCTSTR)IDD_ABOUTBOX, hWnd, (DLGPROC)About);
             break;
          case IDM_EXIT:
             DestroyWindow(hWnd);
             break;
          default:
             return DefWindowProc(hWnd, message, wParam, lParam);
          }
       break;
       case WM_PAINT:
          hdc = BeginPaint(hWnd, &ps);
          // TODO: Add any drawing code here...
          EndPaint(hWnd, &ps);
          break;
       case WM_DESTROY:
          gSoundManager->StopAllOgg();
          PostQuitMessage(0);
          break;
       default:
          return DefWindowProc(hWnd, message, wParam, lParam);
       }
       return 0;
    }

Zde jsem přidal jen reakci na zprávu WM_ACTIVATEAPP a zastavení přehrávání všech souborů (StopAllOgg) při zprávě WM_DESTROY (aplikace je ukončována). Pokud obdržíme zprávu WM_ACTIVATEAPP, zavoláme funkci HandleAppActiveStateChanges která pozastaví nebo opět spustí přehrávání souborů. To je z toho důbodu, že když ztratíme fokus, nemusí se stihnout volat funkce pro obnovení zvukového bufferu v dostatečných intervalech. Mohly by vznikat mezery, slyšeli bychom nějakou část vícekrát než by došlo k dalšímu obnovení. To je vše. Pokud nyní sestavíte celé řešení ... což se vám nepodaří, ne že by bylo něco špatně, jen z principu, překlepy, tu zapomenete vložit hlavičkový soubor atd. Mě se snad ještě nikdy nepodařilo sestavit příklad z nějakého chytrého výukového kurzu napoprvé :) Až se vám to podaří, měl by se soubor přehrávat korektně. (Ale nevím, některé moje programy mají tendenci fungovat jen mě a jinde mi dělat ostudu :(). Ale...

Obsah

4.3 Úmyslná chyba v COggFile

Bohužel to není překlep ani apríl, ve třídě COggFile máme chybu, a o docela vážnou. Ale jak říkají někteří z mých vyučujících na vysoké škole, každá chyba kterou udělám, je čistě úmyslná aby jste se z ní poučili a abych věděl zda dáváte pozor a všimnete si jí. Ne teď vážne, samozřejmě že to nebyla úmyslná chyba. Našel jsem ji když jsem dolaďoval ukázkový projekt pro tento kurz, ale řekl jsem si že možná bude lepší než opravovat druhou lekci ji tam nechat a vrátit se k ní až tady, abychom se z ní mohli poučit a viděli že nikdo není neomylný. Doufám že to byla jedna z posledních. Jde o to, že máme špatně naprogramovanou obnovu bufferu. Přesněji řečeno, zapoměl jsem hlídat stav kdy je dekódován celý soubor a přehrávání ještě neskončilo. Takový stav nastane vždy před koncem přehrávání ogg souboru. Soubor bude vždy dekódován dřív než bude celý přehrán. Když je ale dekódován, nemá smysl už volat funkci WriteDataToBuffer pro zapsání nových dat, což současná třída nedělá. Díky tomu docházelo k tomu, že funkce WriteDataToBuffer začala vracet v proměnné dwDataDelta veliké hodnoty. Tj. v proměnné která udává kolik z dwBytes požadovaných bajtů nebylo dekódováno. Při krokování jsem pak měl hodnotu dwDataDelta = dwBytes - 1. Vzdálenost kurzorů začala stále růst a funkce WriteDataToBuffer se začala volat při každém volání funkce CheckIfNeedReFillBuffer se stále větší vzdáleností kurzorů. Vzdálenost kurzorů jak si jistě také vzpomínáte určuje kolik bajtů chceme zapsat do zvukového bufferu, jak velkou pamět alokujeme při mezizápisu do paměti. Netrvalo dlouho a došlo k chybě v programu protože vzdálenost kurzorů vzrostla natolik, že už nebylo možné alokovat tolik paměti. Prostě došlo k chybné alokaci a program spadl. Dřív jsem si toho nevšiml, protože ve své původní třídě používám místo DataBuffer = new char[dwBytes] vlastní třídu pro práci s řetězcovými, bajtovými poli, která to nějak vstřebala. Řešení není až tak složité, ale bude si vyžadovat přidání členské proměnné m_dwWriteProgress do třídy COggFile. Tato proměnná bude hlídat průběh zapisování, dekódování ogg souboru. V konstruktoru třídy nastavte tuto proměnnou na nula. Dále ve funkci WriteDataToBuffer napište m_dwWriteProgress += lSize; za dekódovací cyklus while s funkcí ov_read. Poslední jsou úpravy ve funkci CheckIfNeedReFillBuffer. Změny a přídavky jsou opět tučně:

    //**********************************************************************************************************
    //Fce pro kontrolu nutnosti zapisu novych dat do bufferu
    //**********************************************************************************************************

    HRESULT COggFile::CheckIfNeedReFillBuffer()
    {
       HRESULT hRet = S_FALSE;
       bool bRet = false;
       DWORD dwCurrPlayPos = 0;
       DWORD dwDataDelta = 0;
       DWORD dwCursorsDistance = 0;

       if (m_StreamedBuffer->IsSoundPlaying() || m_bStatus == OGGSTATUS_PAUSED)
       {
          //ziskani aktualni pozice hraciho kurzoru
          if(FAILED(hRet = m_StreamedBuffer->GetBuffer()->GetCurrentPosition(&dwCurrPlayPos,NULL)))
             return hRet;

          if ((m_dwPlayProgress + dwCurrPlayPos) >= m_dwPCMSize) //tady to se zpracovava pokud dojde k prehrani celeho souboru
          {
             hRet = m_StreamedBuffer->Stop(); //pokud dojde k prehrani celeho souboru, zastavime prehravani bufferu
             hRet = m_StreamedBuffer->Reset(); //a resetujeme buffer a prislusne promenne
             m_dwNextWriteOffset = 0; //zapis bude zacinat opet na nule
             m_dwPlayProgress = 0; //stejne tak pocet prehranzch bytu je nula
             m_dwLastPlayPos = 0; //posledni pozice hraciho kurzoru taktez
             dwCursorsDistance = 0; //vzdalenost hraciho a zapisovaciho kurzoru taktez nulova
             dwCurrPlayPos = 0;
             m_dwWriteProgress = 0;
             ov_raw_seek(&m_OggVorbisFile,0); //presun na zacatek v ogg souboru
             WriteDataToBuffer(0,m_dwBufferSize,NULL); //naplneni bufferu daty z ogg souboru od zacatku, kdyby byla pozdeji spustena fce play
             //tak at to tam je
             if (!m_iRepeatCount) //pokud je pocet opakovani 0
             {
                //nastavime stav zastaveno
                m_bStatus = OGGSTATUS_STOPPED;
                return S_FALSE; //vratime false jako indikaci ze nedoslo k obnoveni dat v bufferu
             }

             if (m_iRepeatCount != OGGPLAY_REPEATINFINITE) //pokud pocet opakovani neni nula a neni ani roven konstante pro
                m_iRepeatCount--; //nekonecno opakovani, snizime pocet opakovani

             hRet = m_StreamedBuffer->Play(0,DSBPLAY_LOOPING); //zacneme znovu prehravat buffer, pac se este neprehral tolikrat jak je nastaveno
             return hRet;
          }

          //pokud je posledni ulozena pozice hraciho kurzoru vetsi nez pozice kurzoru aktualni, znamena to ze
          //buffer dosahl konce a preskocil zpet na zacatek, podle toho musime pocitat vzdalenost kurzoru rozdilne
          if (m_dwLastPlayPos > dwCurrPlayPos)
             //hraci kurzor presel zpet na zacatek, musime vypocitat kolik bytu zbylo na konci bufferu neaktualizovanzch od posledni aktualizace
             //a pricist k nim pocet bytu co uz byly prehrany znova od zacatku a tak ziskame vzdalenost kurzoru
             //vzdalenost mezi hracim a zapisovacim kurzorem
             dwCursorsDistance = (m_dwBufferSize - m_dwNextWriteOffset) + dwCurrPlayPos;
          else
             //hraci kurzor je pred koncem, zapisovaci za nim, vzdalenost je rozdil jejich pozic
             //abychom nepocitali i bajt na kterem hraci kurzor zrovna je
             dwCursorsDistance = dwCurrPlayPos - m_dwNextWriteOffset;

          //ted se mrknem jestli vzdalenost kurzoru presahla pocet bajtu po kterych se ma buffer obnovovat
          if (dwCursorsDistance >= m_dwNotifySize) //a pokud ano, zapiseme do bufferu tolik bytu jako je vzdalenost kurzoru
          {
             if (m_dwWriteProgress < m_dwPCMSize)
             {
                bRet = WriteDataToBuffer(m_dwNextWriteOffset,dwCursorsDistance - 1,&dwDataDelta); //hraciho kurzoru, tedy mela by, muze se stat
                //vzdalenost mezi kurzory bude licha a v tom pripade nam fce ov_read ktera dekoduje ogg soubor

                m_dwNextWriteOffset += dwCursorsDistance - 1 - dwDataDelta; //vrati o nekolik bytu min, dekoduje totiz vzdy sudy pocet a ten pocet bajtu
                //musime snizit pozici dalsiho zapisovani, aby nevznikla jakasi dira, viz dwDataDelta
                if (m_dwNextWriteOffset >= m_dwBufferSize) //tady se hlida situace kdy je psaci kurzor skoro na konci a jeho aktualizaci o pocet zapanzch bajtu by doslo k prekroceni bufferu
                   m_dwNextWriteOffset -= m_dwBufferSize; //odecteme proto velikost bufferu a psaci kurzor se dostane na spravnou pozici od zacatku bufferu
             }


             if (m_dwLastPlayPos > dwCurrPlayPos)
             m_dwPlayProgress += m_dwBufferSize;

             m_dwLastPlayPos = dwCurrPlayPos; //aktualizace posledni pozice hraciho kurzoru
          }
       }
       return hRet;
    }

Tak doufám že to byla poslední závažná chyba v programu. Nějaké menší se najdou vždy. Doufám že se mi podařilo aspoň trošku srozumitelně vysvětlit jak to funguje a že vám to něco dalo. Pokud budete mít něco na srdci (něco ohledně kurzu, nejsem psychologická poradna :)), můj email a icq pod hlavním menu nemají jen copyright funkci :) Klidně pište a ptejte se. Rady na zlepšení také rád přijmu. Kritiku..., no budu se s tím muset nějak srovnat. Pokud budu vědět, rád s případnými problémy pomůžu. Snažte se ale dát co nejvíc informací. Na dotaz typu: nefunguje mi to! Co s tím uděláš? toho asi moc nevymyslím :)

Obsah