Odtwarzanie wave z WinAPI

JacekH

Odtwarzanie plików wave WinAPI

Dla początkujących... ;-)

Może nic odkrywczego, ale mam nadzieję, że komuś się przyda. Sam nie tak dawno potrzebowałem odtworzyć plik wave nieco ?poważniej? niż tylko dzięki klasie TMediaPlayer, czy funkcji PlaySound, o komendach MCI nie wspominając. Zdaję sobie sprawę z wielu uproszczeń, którymi się posłużyłem i ?amatorszczyzny?, ale jestem właśnie amatorem w Delphi i próbuję czegoś się nauczyć, a wiedzą podzielić. Przepraszam więc z góry tych zaawansowanych i chętnie posłucham sugestii, co zmienić lub poprawić w moim artykule.

Za funkcje multimedialne odpowiedzialny jest moduł MMSystem, który należy dołączyć do sekcji interface. Pliki audio (wave) to typowe pliki RIFF (Resource Interchange File Format). Czyli ich budowa jest ściśle określona i dosyć prosta. Tak zbudowane są pliki MIDI (SMF), AVI i inne multimedialne. Pliki RIFF można korzystać z gotowych funkcji zawartych w WinAPI (mmio) lub zrobić ?po swojemu? :-) Wybrałem własny sposób.
Podstawowe struktury danych:

ID nagłówka bloku:
TChunkID = array [0..3] of Char;

TRIFFHeader = packed record
    RIFFID: TChunkID;
    RIFFSize: Cardinal;
    RIFFIDType: TChunkID;
  end;

Najpierw należy rozpoznać typ pliku. RIFFID musi zawierać wartość ?RIFF?. RIFFIDType określa typ pliku RIFF. W naszym przypadku musi to być ?WAVE?. RIFFSize to wielkość całego pliku pomniejszona o identyfikator (RIFFID) i RIFFSize, zatem będzie to długość pliku ? 8 bajtów. I od tego miejsca budowa pliku wave jest już stała: ID nagłówka, wielkość danych, dane. Nagłówek:

TChunkHeader = packed record
  ChunkID: TChunkID; 
  ChunkSize: Cardinal;
end;
TFormatHeader = TChunkHeader;

Po zidentyfikowaniu pliku WAVE obowiązkowym elementem jest blok określający format audio, czyli ChunkID = ?fmt ? Struktura danych formatu wygląda następująco:

tWAVEFORMAT = packed record
    wFormatTag: Word;
    nChannels: Word;
    nSamplesPerSec: DWORD;
    nAvgBytesPerSec: DWORD; 
    nBlockAlign: Word;
    wBitsPerSample: Word;
  end;

wFormatTag określa format WAVE. Zdefiniowane formaty:

0 - 'Unknown';
1 - 'PCM/uncompressed';
2 - 'Microsoft ADPCM';
3 - 'IEEE float';
5 - 'IBM CVSD';
6 - 'ITU G.711 a-law';
7 - 'ITU G.711 ?-law';
10 - 'OKI ADPCM';
15 - 'DIGISTD';
16 - 'DIGIFIX';
17 - 'IMA ADPCM';
20 - 'ITU G.723 ADPCM (Yamaha)';
49 - 'GSM 6.10';
64 - 'ITU G.721 ADPCM';
80 - 'MPEG';
65534 - ' Extensible';
65536 - 'Experimental';

Jeżeli format pliku jest inny niż 1 (PCM/uncompressed ? stała: WAVE_FORMAT_PCM) to należy odczytać kolejne dwa bajty zawiera wielkość dodatkowych bajtów definiujących format. nChannels określa ilość kanałów audio wymaganych do odtworzenia danych. Częstotliwość próbkowania (nSamplesPerSec) podaje się w Hz (np. 44100 ? czyli 44.1 kHz), wBitsPerSample to rozdzielczość (np. 16 bit). nBlockAlign określa ilość bajtów na próbkę: nBlockAlign = (wBitsPerSample div 8) * nChannels. nAvgBytesPerSec określa ilość bajtów jaką należy zapewnić urządzeniu odtwarzającemu w ciągu sekundy: nAvgBytesPerSec = nSamplesPerSec * nBlockAlign. Te informacje będą potrzebne podczas otwierania portu waveOut.

Od tego miejsca do końca pliku mogą występować w dowolnej kolejności nagłówki i ich dane. Odczyt powinien wyglądać następująco:

repeat
  ilość_faktycznie_odczytanych_bajtów := (CzytajPlik do zmiennej typu TChunkHeader, ilość danych do wczytania to SizeOf(TChunkHeader);
  jeżeli ChunkID jest znany i potrzebne są te dane ? czytaj dane o rozmiarze ChunkSize
  else przesuń wskaźnik o ChunkSize dopełniony do parzystej liczby bajtów
until koniec pliku lub ChunkID to jest to, czego szukamy*

Jeśli podany rozmiar danych jest liczbą nieparzystą to blok powinien zawierać dodatkowy bajt z wartością 0, tzw. padded byte.
ChunkID może być praktycznie dowolnie zdefiniowanym identyfikatorem. Można znaleźć mniej lub bardziej pełne opisy różnych identyfikatorów. Problem w tym, że nie są określone jakimś standardem. Program powinien zignorować nieznany identyfikator i przejść do następnego bloku. Wśród tych najczęściej używanych można spotkać:
?fact? ? informacje na temat kompresji
?data?, ?wavl?, ?slnt? ? o tym za chwilę
?bext? ? określa tzw. BWF, czy Broadcast Wave File ? dane zawierają informacje na temat pozycji startu wave?a względem kodu czasowego, informacje o użytym oprogramowaniu, dane nagrywającego itp.

Najprostszy plik wave zawiera tylko blok jeden ?data?. Jeżeli w pliku występuje ?wavl? to określa on miejsce występowania w pliku i rozmiar bloków ?data? i ?slnt?. Nas interesuje teraz najprostsza forma ? czyli jeden blok ?data?. Zatem wracając do procedury wyszukiwania nagłówków ? szukamy, aż znajdziemy blok ?data?*. Zawiera on faktyczne dane do odtworzenia. Są one poukładane w ten sposób, że na każdą pozycję czasową zapisane są wartości dla każdego z kanałów, potem dane dla kolejnej pozycji itd. Wygląda to mniej więcej tak w przypadku audio 8-bitowego stereo: LR, LR, LR itd. Dla danych 16 bitowych (po dwa bajty na jedną próbkę) wyglądałoby to tak: LL, RR, LL, RR itd., gdzie kolejne litery oznaczają bajty. Warto zapamiętać pozycję pliku, od której zaczyna blok z danymi ? umożliwi to łatwe zapętlanie utworu, poprzez przesunięcie wskaźnika pliku do tej pozycji.

Samo odtwarzanie można zrealizować na kilka sposobów ? zależnie od potrzeb. Można wczytać do bufora całą zawartość bloku ?data? i wysłać ją do urządzenia audio, lub ?podsyłać? dane w mniejszych porcjach. Inny sposób to czytanie sekwencyjne danych z pliku. Odczyt danych do pamięci ma tę zaletę, że w przypadku wolniejszych dysków nie występuje problem ?czkania?, kiedy system nie nadąża czytać dane. Z kolei będzie to niepraktyczne przy odgrywaniu długich plików ? np. 74 minutowy koncert to 650 MB... :) Niektóre systemu (jak Creamware) wymagają większej liczby buforów ? zaczynają odtwarzać dopiero po otrzymaniu np. 3 buforów. Zatem punktem wyjścia może być 8 buforów po 8192 bajty.
Po sprawdzeniu poprawności danych można otworzyć port audio. Podczas otwierania należy wskazać sposób komunikacji z driverem. Driver rozsyła trzy komunikaty: MM_WOM_OPEN ? po otwarciu portu; MM_WOM_CLOSE ? po jego zamknięciu, MM_WOM_DONE ? po odegraniu danych z bufora. Komunikaty mogą być odbierane przez funkcję lub okno. Posłużę się przykładem CALLBACK_WINDOW.
waveOutOpen(AdresUchwytuPortu, Urządzenie, AdresStrukturyZawierającejFormat, UchwytOknaOdbierającegoKomunikaty, DaneUżytkownika CALLBACK_WINDOW)

Do uchwytu portu (HWAVEOUT) będziemy odwoływać się przy wszystkich pozostałych funkcjach waveOut.
AdresStrukturyZawierającejFormat ? tWAVEFORMATEX. Dane odczytaliśmy ze strumienia.
UchwytOknaOdbierającegoKomunikaty tu będą zwracane bufory
DaneUżytkownika ? w moim przykładzie alokuję uchwyt dla klasy TWaveOut i tu podaję go jako parametr
CALLBACK_WINDOW ? sposób komunikacji z driverem.

Po otwarciu portu należy przygotować nagłówki (WAVEHDR). Służy do tego funkcja:
waveOutPrepareHeader(hwo, AdresNagłówka, SizeOf(WAVEHDR))

Struktura WAVEHDR:

lpData: PChar;
dwBufferLength: DWORD; 
dwBytesRecorded: DWORD; 
dwUser: DWORD;
dwFlags: DWORD;
dwLoops: DWORD;
lpNext: PWaveHdr;
reserved: DWORD;

lpData zawiera wskaźnik do bloku danych, dwBufferLength to ich wielkość, dwBytesRecorded ? podczas nagrywania tu zostaje wpisana wielkość zarejestrowanych danych, dwUser ? dane użytkownika, wpiszę tu numer bufora, dwFlags ? flagi dotyczące stanu bufora, dwLoops ? kontrola pętli, lpNext wskaźnik na kolejną strukturę danych.

Po przygotowaniu nagłówków można rozpocząć proces odtwarzania. Wczytujemy dane do bufora (lpData), następnie wysyłamy go do urządzenia audio funkcją: waveOutWrite(hwo, AdresNagłówka, SizeOf(WAVEHDR)). Po odegraniu danych z bufora procedura otrzymuje wiadomość MM_WOM_DONE. Teraz bufor należy zapełnić nowymi danymi i ponownie wysłać do urządzenia. Po wysłaniu wszystkich danych należy sprawdzić, czy flagi (dwFlags) buforów zostały ustawione na WHDR_DONE ? zostały odegrane. Przed zamknięciem portu należy go zresetować ? waveOutReset(hwo). Funkcja zwraca niezwrócone bufory z flagą WHDR_DONE. Teraz można wyczyścić nagłówki buforów: waveOutUnprepareHeader(hwo, AdresNagłówka SizeOf(WAVEHDR)) i zamknąć port waveOutClose(hwo).
Wszystkie funkcje waveOut... zwracają wartość MM_RESULT, która jeżeli jest różna od MMSYSERR_NOERROR oznacza, że wystąpił błąd. Komunikat błędu można odczytać posługując się funkcją waveOutGetErrorText.

Ponieważ interesowało mnie odtwarzanie wielokanałowe ? czyli wiele plików jednocześnie ? każdy do innego portu audio, po sprawdzeniu poprawności strumieni wczytuję początkowe dane do buforów, ale nie rozpoczynam odtwarzania. Po załadowaniu danych startuje jednocześnie kilka ?odtwarzaczy?, które dzięki temu grają idealnie równo. Sprawdziłem to odtwarzając ten sam plik w dwóch wersjach, gdzie jedna to kopia z odwróconą fazą. Przy parzystej liczbie odtwarzaczy i odtworzeniu tych plików miałem ciszę w głośnikach ? po zsumowaniu wyjść, co oznacza, że wszystkie zagrały z dokładnością co do sampla. I o to mi chodziło ;-) Poniżej jest przykład modułu odtwarzacza oraz załącznik do banalnej "grającej" aplikacji.

No, to teraz możecie sobie po mnie pojeździć... :D

Jacek Hajnrych

{
  Klasa: TWaveOut wersja 1.0
  Autor: Jacek Hajnrych
  Kontakt: [email protected]
  Kielce, 25 października 2005
  Freeware - jeżeli korzystasz z tego modułu dodaj w swojej aplikacji informację
  o autorze

  WaveOut korzysta z strumieniowego odczytu audio w formacie PCMWAVEFORMAT. Po
  utworzeniu zmiennej typu TWaveOut należy przypisać obsługę zdarzeń:
  OnReady: wywoływane po załadowaniu do pamięciu danych początkowych
  OnFinished: wywoływane po zakończeniu odtwarzania
  OnNextLoop: wywoływane po osiągnięciu końca danych podczas odwtarzania w pętli
  OnRead: wywoływane po załadowaniu do bufora kolejnej porcji danych
  Jeżeli powiedzie się przygotowanie danych:
  function PrepareMemoryStream(WaveStream: TMemoryStream): Boolean;
  function PrepareFileStream(FileName: TFileName): Boolean;
  otwierany jest port audio (Device) i wywoływane zdarzenie OnReady.
  Start i Pause służą jak nazwa wskazuje do uruchamiania i wstrzymywania
  odtwarzania. Stop kończy odtwarzanie i zamyka port.

  Klasa stanowi prostą wersję odtwarzacza, nie jest szczytem wydajności.  Została
  stworzona w celach edukacyjnych. To wcześniejsze przygotowanie danych do
  odtwarzania pozwala na synchroniczne (co do sampla) odtwarzanie wielu plików
  na kartach wielokanałowych.

}
unit uWaveOut;

interface

uses
  Windows, Messages, SysUtils, Classes, MMSystem, ComCtrls, Controls, Dialogs;

const

  MAX_BUFFERS_COUNT = 64; // Maksymalna ilość buforów
  MAX_BUFFER_SIZE = 32768; // Maksymalny rozmiar bufora
  // Komunikaty wysyłane przez funkcję CALLBACK
  WM_AFTEROPEN = WM_USER + $200;
  WM_AFTERCLOSE = WM_USER + $201;
  WM_ONDONE = WM_USER + $202;

type
  EWaveOutErrors = Exception;
  TWaveData = PChar;
  TOnRead = procedure(Sender: TObject; Buffer: TWaveData; Size: DWORD) of object;
  TChunkID = array [0..3] of Char;
  TChunkHeader = packed record
    ChunkID: TChunkID;
    ChunkSize: Cardinal;
  end;
  TRIFFHeader = packed record
    RIFFID: TChunkID;
    RIFFSize: Cardinal;
    RIFFIDType: TChunkID;
  end;
  TFormatHeader = TChunkHeader;
  TWaveFileHeader = packed record
    RIFFHeader: TRIFFHeader;
    FormatHeader: TFormatHeader;
    FormatData: tWAVEFORMATEX;
    ChunkHeader: TChunkHeader;
  end;

  TWaveOut = class
  private
    hwo: HWAVEOUT;
    FActive: Boolean;
    FBuffer: array[0..MAX_BUFFERS_COUNT - 1] of WAVEHDR;
    FBuffersCount: Byte;
    FBufferSize: Int64;
    FDataStart: Int64;
    FFinished: Boolean;

    FLooped: Boolean;
    FDevice: Integer;
    FPaused: Boolean;
    FWaveFileHeader: TWaveFileHeader;
    FWaveStream: TStream;

    FOnReady: TNotifyEvent;
    FOnFinished: TNotifyEvent;
    FOnNextLoop: TNotifyEvent;
    FOnRead: TOnRead;
    FHandle: THandle;

    function MMR(Error: MMResult): Boolean;
    function OpenPort: Boolean; overload;
    function ClosePort: Boolean;
    procedure Init;
    procedure LoadToBuffer(i: Integer);
    function Prepare: Boolean;
    procedure SetDevice(iDevice: Integer);

    procedure WndProc(var Msg: TMessage);
  protected
  public
    constructor Create(AOwner: TComponent);
    destructor Destroy;
    procedure SetBuffer(iBuffers: Byte; iBufferSize: Int64);
    function PrepareMemoryStream(WaveStream: TMemoryStream): Boolean;
    function PrepareFileStream(FileName: TFileName): Boolean;
    function Start: Boolean;
    function Pause: Boolean;
    procedure Stop;

  published
    property Active: Boolean read FActive;
    property BuffersCount: Byte read FBuffersCount;
    property BufferSize: Int64 read FBufferSize;
    property Channels: Word read FWaveFileHeader.FormatData.nChannels;
    property SamplesPerSec: DWord read FWaveFileHeader.FormatData.nSamplesPerSec;
    property BitsPerSample: Word read FWaveFileHeader.FormatData.wBitsPerSample;
    property Device: Integer read FDevice write SetDevice;
    property Looped: Boolean read FLooped write FLooped;
    property Paused: Boolean read FPaused;

    property OnReady: TNotifyEvent read FOnReady write FOnReady;
    property OnFinished: TNotifyEvent read FOnFinished write FOnFinished;
    property OnNextLoop: TNotifyEvent read FOnNextLoop write FOnNextLoop;
    property OnRead: TOnRead read FOnRead write FOnRead;
  end;

implementation

{ TWaveOut }
constructor TWaveOut.Create(AOwner: TComponent);
begin
  inherited Create;
  Init;
  // Klasa musi posiadać uchwyt, aby mogła odbierać komunikaty
  FHandle := AllocateHWnd(WndProc);
end;

procedure TWaveOut.Init;
{
  Inicjalizacja zmiennych
}
begin
  hwo := 0;
  FActive := False;
  FBuffersCount := 3;
  FBufferSize := 8192;
  FFinished := False;
  FLooped := False;
  FPaused := False;
  FWaveStream := nil;
  with FWaveFileHeader.FormatData do
  begin
    nChannels := 2;
    nSamplesPerSec := 44100;
    wBitsPerSample := 16;
  end;
end;

destructor TWaveOut.Destroy;
var
  i: Integer;
begin
  if FActive then ClosePort;
  DeallocateHWnd(FHandle);
  inherited;
end;

procedure TWaveOut.SetDevice(iDevice: Integer);
{
  Domyślnym urządzeniem jest WAVE_MAPPER (-1), przed rozpoczęciem odtwarzania
  można wybrać dowolne inne urządzenie
}
begin
  if FActive then Exit;
  if (iDevice >= -1) and  (iDevice < waveOutGetNumDevs ) then
    FDevice := iDevice
  else
    EWaveOutErrors.Create(Format('Numer portu musi być liczbą z zakresu -1..%d!', [waveOutGetNumDevs -1]));
end;

procedure TWaveOut.SetBuffer(iBuffers: Byte; iBufferSize: Int64);
{
  Zmiana domyślnych parametrów bufora: ilości buforów i rozmiaru
}
begin
  if FActive then Exit;
  if (iBuffers > 0) and (iBuffers <= MAX_BUFFERS_COUNT) then
    FBuffersCount := iBuffers else
  begin
    EWaveOutErrors.Create(Format('Liczba buforów musi być liczbą z zakresu 1..%d!', [MAX_BUFFERS_COUNT]));
    Exit;
  end;
  if (iBufferSize > 0) and  (iBufferSize <= MAX_BUFFER_SIZE) then
    FBufferSize := iBufferSize else
  begin
    EWaveOutErrors.Create(Format('Wielkość bufora musi być liczbą z zakresu 1..%d!', [MAX_BUFFER_SIZE]));
    Exit;
  end;
end;

function TWaveOut.PrepareMemoryStream(WaveStream: TMemoryStream): Boolean;
{
  Inicjacja strumienia wave - tu dla strumienia w pamięci
}
begin
  try
    Init;
    FWaveStream := WaveStream;
    Result := Prepare;
  except
    Result := False;
  end;
end;

function TWaveOut.PrepareFileStream(FileName: TFileName): Boolean;
{
  Inicjacja strumienia wave - tu dla pliku
}
begin
  Result := False;
  if FActive or (not FileExists(Filename)) then Exit;
  try
    Init;
    FWaveStream := TFileStream.Create(Filename, fmOpenRead or fmShareDenyWrite);
    Result := Prepare;
  except
    Result := False;
  end;
end;

function TWaveOut.Prepare: Boolean;
{
  Funkcja sprawdza poprawność strumienia wave. Sprawdzenie polega na odczycie
  nagłówków - 'RIFF' i 'WAVE', następnie 'fmt ' i odszukaniu bloku danych ('data').
  Jeżeli dane są poprawne funkcja zwraca wartość true jeżeli otworzono port wave Out.
}
begin
  Result := False;
  try
    if FActive or (FWavestream = nil) then Exit;
    // RIFF Header
    FDataStart := 0;
    FWavestream.Position := 0;
    if FWaveStream.Read(FWaveFileHeader.RIFFHeader, SizeOf(TRIFFHeader)) <> SizeOf(TRIFFHeader) then
      Abort; //
    FDataStart := FWaveStream.Position;
    if not ((FWaveFileHeader.RIFFHeader.RIFFID = 'RIFF') and (FWaveFileHeader.RIFFHeader.RIFFIDType = 'WAVE')) then
      Abort;
    // 'fmt '
    if FWaveStream.Read(FWaveFileHeader.FormatHeader, SizeOf(TFormatHeader)) <> SizeOf(TFormatHeader) then
      Abort;
    FDataStart := FWaveStream.Position;
    if not (FWaveFileHeader.FormatHeader.ChunkID = 'fmt ') then
      Abort;
    if FWaveStream.Read(FWaveFileHeader.FormatData, SizeOf(tWAVEFORMATEX)) <> SizeOf(tWAVEFORMATEX) then
      Abort;
    if FWaveFileHeader.FormatData.wFormatTag = WAVE_FORMAT_PCM then
      FWaveStream.Position := FWaveStream.Position - 2
    else
      FWaveStream.Position := FWaveStream.Position + FWaveFileHeader.FormatData.cbSize;
    repeat
      if FWaveStream.Read(FWaveFileHeader.ChunkHeader, SizeOf(TChunkHeader)) <> SizeOf(TChunkHeader) then
        Abort;
      if (FWaveFileHeader.ChunkHeader.ChunkID <> 'data') then
        FWaveStream.Position := FWaveStream.Position
                              + FWaveFileHeader.ChunkHeader.ChunkSize
                             + (FWaveFileHeader.ChunkHeader.ChunkSize mod 2);
    until (FWaveStream.Position = FWaveStream.Size) or (FWaveFileHeader.ChunkHeader.ChunkID = 'data');
    if (FWaveFileHeader.ChunkHeader.ChunkID <> 'data') then
      Abort;
    FDataStart := FWaveStream.Position;
    Result := OpenPort;
  except
    FWaveStream.Free;
  end;
end;

function TWaveOut.OpenPort: Boolean;
{
  Otwarcie portu wave Out
}
begin
  //  if not MMR(waveOutOpen(@hwo, FDevice, @FWaveFileHeader.FormatData, DWORD(@waveOutProc), DWORD(Self), CALLBACK_FUNCTION or WAVE_ALLOWSYNC)) then
  if not MMR(waveOutOpen(@hwo, FDevice, @FWaveFileHeader.FormatData, DWORD(Self.FHandle), DWORD(Self), CALLBACK_WINDOW or WAVE_ALLOWSYNC)) then
  begin
    FreeAndNil(FWaveStream);
    if Assigned(FOnFinished) then
      FOnFinished(Self);
  end;
end;

procedure TWaveOut.WndProc(var Msg: TMessage);
{
  Procedura odbierająca komunikaty
}
var
  i: Integer;
  BuffersLeft: Word;
  Hdr: PWAVEHDR;
  MMResult: Boolean;
begin
  case Msg.Msg of
    // Komunikat otrzymany po otwarciu portu
    MM_WOM_OPEN   :
      begin
        FActive := True;
        FFinished := False;
        if Pause then
        begin
        // Zerowanie buforów
          for i := 0 to FBuffersCount - 1 do
          begin
          FBuffer[i].lpData := AllocMem(FBufferSize);
          ZeroMemory(FBuffer[i].lpData, FBufferSize);
          // Dla danych 8-bitowych "zero" to wartość 128
          if FWaveFileHeader.FormatData.wBitsPerSample = 8 then
            FillMemory(FBuffer[i].lpData, FBufferSize, $80);
          FBuffer[i].dwUser := i;
          FBuffer[i].dwFlags := 0;
          MMResult := MMR(waveOutPrepareHeader(hwo, @FBuffer[i], SizeOf(WAVEHDR)));
          if MMResult then
          begin
            // Wczytanie do buforów danych początkowych
            LoadToBuffer(i);
            if Assigned(FOnRead) then
              FOnRead(Self, @FBuffer[i].lpData, FBuffer[i].dwBufferLength);
          end else
            Break;
          end;
          if not MMResult then
            ClosePort
          else if Assigned(FOnReady) then
            FOnReady(Self);
        end;
      end;
    // Komunikat otrzymany po odegraniu danych z bufora
    MM_WOM_DONE   :
      begin
        HDR := PWAVEHDR(Msg.LParam);
        // Wczytanie nowych danych
        LoadToBuffer(HDR^.dwUser);
        // Modyfikacja, prezentacja etc.
        if Assigned(FOnRead) then
          FOnRead(Self, HDR.lpData, HDR.dwBufferLength);
        if (HDR^.dwBufferLength = 0) and (not FFinished) then
        // Długość zero oznacza koniec pliku, należy sprawdzić, czy wszystkie
        // bufory zostały odegrane
        begin
          BuffersLeft := 0;
          for i := 0 to FBuffersCount - 1 do
            BuffersLeft := BuffersLeft + (HDR^.dwFlags and WHDR_INQUEUE);
          if BuffersLeft = 0 then Closeport;
        end;
      end;
    // Komunikat otrzymany po zamknięciu portu
    MM_WOM_CLOSE  :
      begin
        // Zwalnianie pamięci
        for i := 0 to FBuffersCount - 1 do
          FreeMem(FBuffer[i].lpData);
        FreeAndNil(FWaveStream);
        if Assigned(FOnFinished) then
          FOnFinished(Self);
      end;
  end;
  inherited;
end;

procedure TWaveOut.LoadToBuffer(i: Integer);
{
  Procedura wczytuje blok danych do bufora numer "i"
}
begin
  if not FFinished then
  begin
    // Odczyt danych
    FBuffer[i].dwBufferLength := FWaveStream.Read(FBuffer[i].lpData^, FBufferSize);
    if (FBuffer[i].dwBufferLength > 0) then
    begin
      if MMR(waveOutWrite(hwo, @FBuffer[i], SizeOf(WAVEHDR))) then
      begin
        if FBuffer[i].dwBufferLength < FBufferSize then
          if FLooped then
          // Jeżeli aktywna jest pętla to powrót na początek bloku danych
          begin
            FWaveStream.Position := FDataStart;
            // Informacja o rozpoczęciu odtwarzania kolejnej pętli
            if Assigned(FOnNextLoop) then
              FOnNextLoop(Self);
          end;
      end else
        Closeport;
    end;
  end else
    FBuffer[i].dwBufferLength := 0;
end;

function TWaveOut.Pause: Boolean;
{
  Wstrzymanie odtwarzania
}
begin
  Result := False;
  if not FActive then Exit;
  Result := MMR(waveOutPause(hwo));
  if Result then FPaused := True;
end;

function TWaveOut.Start: Boolean;
{
  Kontynuacja odtwarzania
}
begin
  Result := False;
  if not FActive then Exit;
  Result := MMR(waveOutRestart(hwo));
  if Result then FPaused := False;
end;

procedure TWaveOut.Stop;
{
  Zamknięcie portu
}
begin
  ClosePort;
end;

function TWaveOut.ClosePort: Boolean;
{
  Funkcja resetuje a nastepnie zamyka aktywny port
}
var
  i: Integer;
  Tick: DWORD;
  Msg: TMsg;
begin
  Result := False;
  if FActive then
  begin
    FFinished := True;
    MMR(waveOutReset(hwo));
    Tick := timeGetTime();
    while timeGetTime - Tick < 300 do
      while (PeekMessage(Msg, 0, 0 ,0, PM_REMOVE)) do
      begin
        TranslateMessage(Msg);
        DispatchMessage(Msg);
     end;
    for i := 0 to FBuffersCount - 1 do
      Result := MMR(waveOutUnprepareHeader(hwo, @FBuffer[i], SizeOf(WAVEHDR)));
    FActive := False;
    Result := MMR(waveOutClose(hwo));
    hwo := 0;
  end;
end;

function TWaveOut.MMR(Error: MMResult): Boolean;
{
  Obsługa błędów
}
var
  ErrTxt: array[0..MAXERRORLENGTH] of Char;
begin
  Result := (Error = MMSYSERR_NOERROR);
  if Result then Exit;
  if (waveOutGetErrorText(Error, @ErrTxt, MAXERRORLENGTH) = MMSYSERR_NOERROR) then
    raise EWaveOutErrors.Create(StrPas(ErrTxt));
end;

end.

7 komentarzy

weź kod źródłowy w tagi < delphi > i </ delphi > oczywiście bez spacji

Co do waveOut... w Callback. "Applications should not call any system-defined functions from inside a callback function [...] Calling other wave functions will cause deadlock."

To akurat wiem tylko szkoda, że w dokumentacji nie wyjaśnili dokładnie dlaczego nie powinno używać się funkcji waveXX. Gdzieś wyczytałem, że chodzi (między innymi) o sekcje krytyczne (te wewnątrz driver'a).

WaveOutWrite w Callbacku działa jedynie z moją banalną kartą onboard, natomiast system RME HDSP (pro) niestety wisi. Zatem chyba nie zgodzę się z tym, co napisałeś.

Być może RME HDSP działa w trybie synchronicznym i stąd ten zawias. Próbowałeś bez flagi WAVE_ALLOWSYNC??? Bo ja jej nie daje.

0x666 :
OK, z tymi "dużymi buforami" :D już poprawiłem. Takie info mam w mądrej książce.
Co do waveOut... w Callback. "Applications should not call any system-defined functions from inside a callback function, except for EnterCriticalSection, LeaveCriticalSection, midiOutLongMsg, midiOutShortMsg, OutputDebugString, PostMessage, PostThreadMessage, SetEvent, timeGetSystemTime, timeGetTime, timeKillEvent, and timeSetEvent. Calling other wave functions will cause deadlock." - to z helpa. I WaveOutWrite w Callbacku działa jedynie z moją banalną kartą onboard, natomiast system RME HDSP (pro) niestety wisi. Zatem chyba nie zgodzę się z tym, co napisałeś.

Po co CALLBACK_FUNCTION i rozsyłane komunikaty? Chciałem pokazać jak działają obie "wersje" - może niezbyt jasno wyraziłem się w opisie. Faktycznie może to być mylące, zmieniłem opis i przykład - jest tylko CALLBACK_WINDOW. JHWavePlay1.zip zawiera prostą aplikację, nie mogę usunąć starszego załącznika...

Dzięki za uwagi.

Bufor ? jeden duży bufor [...] ma tę wadę, że nie sposób przerwać takie odtwarzanie zanim bufor nie zostanie odegrany i zwrócony do aplikacji ? napotkamy stosowny komunikat o błędzie.

Nieprawda ;)

CALLBACK - zgodnie z MS SDK z funkcji CALBACK nie mogą być wywoływane żadne funkcje waveOut..., ponieważ mogą spowodować pętlę i zawiesić aplikację. Zatem funkcja rozsyła jedynie komunikaty

Nieprawda ;) waveOutWrite i waveInAddBuffer mogą być wywoływane.

Po co ten callback jeżeli korzystasz z komunikatów okienkowych??? Nie prościej przy otwieraniu portu dać CALLBACK_WINDOW?

nie wiem czy dziala - ale wyglada niezle :)

myślę podobnie: wygląda nieźle ;-)

Artur - dziękuję, faktycznie po poprawce zjadło mi ">" w tagu otwierającym :)