Winapi

Krecik

[WinApi][CppBuilder] Odtwarzanie i rejestrowanie dźwięku na niskim poziomie. <mmsystem.h>

PARTII

Kolejna część artykułu, dotycząca odtwarzania dźwięku na niskim poziomie (na poziomie próbek, sampli) [low level audio].
Wspomnę też krótko o innych możliwych sposobach wykorzystania funkcji z rodziny waveOut...

//----------------------------------

W środowisku Windows do odtwarzania dźwięku przez urządzenia audio służy grupa funkcji waveOut..., np. weveOutClose, waveOutWrite, waveOutOpen.
Proces odgrywania zapisanego dźwięku można podzielić na: 1.zdobycie danych, 2. otwarcie u|rz|ądzenia, 3. stworzenie nagłówka (waveheader) i 4. odgrywanie dźwięku 5. wiedzieć, kiedy przestało grać i posprzątać po sobie.

Tak więc od początku...

Trzeba mieć co grać...
W sumie dane dźwiękowe można uzyskiwać na kilka sposobów. Najbardziej popularnym i najczęsciej używanym jest oczywiście odgrywanie dźwięku z plików *.wav. Jednak nie jest to jedyna metoda. Można dane dźwiekowe przechowywać w resourcach programu (nie będę tego tłumaczył, bo zasadniczo chodzi o to samo... wczytać odpowiednie dane do odpowiednich struktur), a nawet, co jest chyba najciekawszą możliwością (i najrzadziej spotykaną) można tworzyć dźwięk na podstawie algorytmu.. Coś w stylu graficznych .
Poprzedni artykuł dotyczył tak naprawdę właśnie otwierania plików Wave.
Trzeba najpierw dwie zmienne globalne:

char*           Buffer = NULL;    //bufor dla danych dźwięku
unsigned int    BufferSize = 0;

i deklarujemy typ struktury opisanej w poprzednim artykule.

typedef struct _WAVEHEAD {
 struct {
        char            RIFF[4];
        unsigned long   Size;
        char            WAVE[4]; //okresla format pliku
 } RIFF;
 struct {
        char            fmt[4] ;
        unsigned long   BlockSize;
        struct {
          unsigned short  AudioFormat;
          unsigned short  NumChannels;
          unsigned long SampleRate;
          unsigned long ByteRate;
          unsigned short  BlockAlign;
          unsigned short  BitsPerSample;
          } Format;
 } fmt;
 struct {
        char            data[4];
        unsigned long   DataSize;
 } data;
} WAVEHEAD;

no i piszemy funkcje do odczytywania tych danych z pliku:

#include <stdio.h>

WAVEHEAD head;
if(!OpenDialog1->Execute()) return;  //fragment z VCL borlandowskim
char *FileName = OpenDialog1->FileName.c_str();

FILE *plik = fopen(FileName,"r");

fread(&head, sizeof(WAVEHEAD), 1, plik);

BufferSize = head.data.DataSize;
Buffer = new char[BufferSize];
fread(Buffer,BufferSize,1,plik);

fclose(plik);

I to tak naprawdę tyle, jeśli chodzi o wczytywanie danych z pliku...

Otwieranie u|rz|ądzenia wyjściowego...
Może zacznę nie od początku, tylko jakby od środka. Przed omówieniem funkcji służących do otwierana wyjścia omówię najpierw ściśle z nią związaną strukturę opisującą format pliku.
biblioteka mmsystem.h zawiera klasę o nazwie WAVEFORMATEX, zawierającą informcje o jakości, precyzji i parametrach dźwięku. Jest ona właściwie identyczna jak część napisanej przez nas struktury nagłówka. Właśnie dlatego podzieliłem WAVEHEAD na sekcje. Struktura WAVEHEAD.fmt.Format jest identyczna co początkowe pola WAVEFORMATEX (WAVEFORMATEX zawiera jeszcze pole na informacje dodatkowe, ale wykorzystywane jest ono tylko przy plikach zapisywanych w niestandardowych formatach, np jakichś rodzajach kompresji). WAVEHEAD.fmt.Format zawiera wszystkie dane potrzebne nam do odtwarzania dźwięków.
Tak więc deklarujemy najpierw globalną zmienną:

WAVEFORMATEX format;

Będziemy jej właściwie co krok używać. Teraz wypelniamy ją potrzebnymi nam danymi, które przepisujemy ze struktury WAVEHEAD:

memcpy(&format,&(head.fmt.Format),sizeof(WAVEFORMATEX));

Teraz już pierwszy kork mamy za sobą.
Otwieramy device`a więc. Napier musimy stworzyć zmienną globalną na uchwyt (handle) do naszego urządzenia:

HWAVEOUT WaveHandle;

Do otwierania urządzenia słyży f-kcja waveOutOpen.
Zanim jednak rzeczywiście otworzymy wejście do sprzętu sprawdźmy, czy nasz hardware obsługuje format dźwięku, który wczytalismy. Wykorzystamy do tego tą samą funkcję, którą wywyołujemy z parametrem WAVE_FORMAT_QUERY, czyli wywołujemy ją tak:

int Res = waveOutOpen(&WaveHandle, WAVE_MAPPER, &WaveFormat, 0,0, WAVE_FORMAT_QUERY);

W rzeczywistości tak wywołana f-kcja nie inicjalizuje urządzania, a tylko sprawdza, czy nasz sprzęt sobie poradzi. Jeżeli nie było błędów funkcja zwraca 0, w innym wypadku wartości nie zerowe (z tego co pamiętazm zazwyczaj WAVERR_BADFORMAT - dziesiętnie 32).

Jeżeli jednak funkcja zwróciła nam 0, mozemy iść dalej i faktycznie inicjalizować urządzenie:

Res = waveOutOpen(&WaveHandle, WAVE_MAPPER, &WaveFormat, MAKELONG(Handle, 0), 0, CALLBACK_WINDOW);

Może postaram sie ją teraz trochę dokładniej opisać.
Pierwszy parametr to wskaźnik do uchwytu urządzenia (to co inicjalizujemy).
Drugi parametr to uDeviceID, czyli numer urządzenia, które będziemy wykorzystywać. Może sie zdarzyć, że maszyna posiada więcej niż jedno urządzenie muzyczne i wtedy ten argument może przyjmować różne wartosci. Jednak jest ratunek:) jeżeli wrzucimy tam wartość WAVE_MAPPER system sam wybierze pierwsze wolne urządzenie mogące obsługiwać dany format dźwiękowy (jeżeli w systemie jest jedna karta muzyczna, to wybierze ją) - czyli odwala za nas robotę.
Dalej jest wskaźnik na strukturę WAVEFORMATEX. To już odwaliliśmy i tylko wsatawiamy tam pointer do niej.
Kolejny argument to kombinowany wskaźnik do okna (Handle - wskaźnik do naszego okna), które będzie przyjmowało callbacki z systemu (np. będzie wywoływany gdy skończy się odtwarzanie buforu). Makro MAKELONG w tym wypadku słyży do skonwertowania uchwytu do okna do DWORD.
Dalej jest wartość, której nie wykorzystujemy (nie jest urzywana przy systemie callback).
I ostatnia wartość to flaga. Wybieramy CALLBACK_WINDOW, bo wtedy wyjątki będą wysyłane do okna.

Jeżeli funkcja się powiedzie, to zwróci 0, jeżeli nie to jakąś inną wartość. Jeżeli więc mamy szczęscie, to mamy gotowy uchwyt do urządzenia w WaveHandle. Jeżeli nie to przez funkcję waveOutGetErrorText.

Przygotowujemy nagłówek pliku...
Kolejna zmienna globalna:

WAVEHDR WaveHeader;

Klasa ta przechowuje informacje potrzebne systemowi przy odgrywaniu dźwięku. Właściwie są funkcje, które robią tą robotę za nas, ale trochę i tak musimy:)

memset(&WaveHeader, 0, sizeof(WaveHeader));
WaveHeader.lpData = Buffer;
WaveHeader.dwBufferLength = BufferSize;

Najpierw zerujemy zawartość WaveHeader;
I dalej podajemy wskaźnik do (tablicę) danych i ich ilość.
A resztę zrobi za nas:

Res = waveOutPrepareHeader(WaveHandle, &WaveHeader, sizeof(WAVEHDR));

No i w sumie zrobiliśmy najważniejsze.
A teraz...

Niech zagra!
To już bardzo prosta rzecz:)

Res = waveOutWrite(WaveHandle, &WaveHeader, sizeof(WAVEHDR));

Funkcja nie czeka na zakończenie grania muzyczki, tylko jeżeli się uda, to zwraca 0, a jak nie, to nie...
WaveHandle mówi Windowsowi, gdzie , pointer do struktury WaveHeader mówi mu co, a jej sizeof() mówi ile ma wysłać danych do urządzenia.
Jeżeli zwróciło 0, to możemy się napawać tym cudownym dźwiękiem zwycięstwa. Ale nie, to jeszcze nie koniec, bo...

Trzeba po sobie postprzątać.
Żeby dowiedzieć się, kiedy zakończyło się odtwarzanie musimy zastawić pułapkę w naszym programie na odpowiedni message od Windowsa;
W [BCB] wstawiamy pliku nagłówkowaszej funkcji w sekcji public taka oto sekwencję:

        void TForm1::OnWaveDone (TMessage& msg);
BEGIN_MESSAGE_MAP
 MESSAGE_HANDLER( MM_WOM_DONE, TMessage, OnWaveDone)
END_MESSAGE_MAP(TForm)

I definiujemy tą f-kcje w pliku *.cpp:

void TForm1::OnWaveDone (TMessage& msg)
{
if (msg.Msg == MM_WOM_DONE)
 {
 waveOutUnprepareHeader(WaveHandle, &WaveHeader, sizeof(WAVEHDR));
 waveOutClose(WaveHandle);
 delete []Buffer;
 ShowMessage("WELL DONE");
 }
}

No i teraz możemy sobie tylko "rzyczć durzo szczenscia" ;)

Kolejna część artykułu będzie dotyczyła nagrywania dźwięku i zapisywania go na dysk (by móc to sobie po tem np. ku radosci odtwarzać w WinAmpi).

Sorry, ze nie zamieścilem na razie programu, ale ten który mam na własne potrzeby, jest jak sama nazwa wskazuje na własne potrzeby i raczej mało ładny.
PS> Zaś przepraszam za orty i setki literówek (mam nadzieję, że nie w kodach), ale naprawdę nie mam czym poprawić a na reczne mam za mało wiedzy w tym zakresie [skrucha]
PPS> Szczerze też przepraszam, za poszczeólne błęd gramatyczne. Sam jestem uczulony na poprawność stylistyki, ale pisałem to na żywioł, w notatniku przed dosłownie chwilą i na pewno jest sporo takich błędów.

3 komentarzy

Nie do końca wszystko działa. Nagłówki .wav mają pare różnych formatów, np domyślnie w XP między Format zawiera jeszcze jedno pole short, a pomiędzy Format a data znajduje się struktura
struct
{
char fact[4];
unsigned long chunkSize;
unsigned long dwSampleLength;
} fact;
W tym przykładzie dźwięk owszem odtworzy się, ale trochę zniekształcony na początku (bo położenie danych jest źle obliczone).

Mysle, ze w:

Res = waveOutOpen(&WaveHandle, WAVE_MAPPER, &WaveFormat, MAKELONG(Handle, 0), 0, CALLBACK_WINDOW);

int Res = waveOutOpen(&WaveHandle, WAVE_MAPPER, &WaveFormat, 0,0, WAVE_FORMAT_QUERY);

Zamiast &WaveFormat ma byc &format ;)
Przynajmniej tak mi to dziala ;)

Mam tylko małą uwagę. Pliki *.wav są plikami binarnymi dlatego w tej linii

FILE *plik = fopen(FileName,"r");

jest bląd, ponieważ domyślnie plik będzie odczytany jako tekstowy. Powinno być tak: FILE *plik = fopen(FileName,"rb");

 i wszystko działa:D W ogóle to bardzo dobry artykuł!