Search code examples
delphiwav

Playing PCM Wav File in Delphi


I have written a simple code that reads the header of a Wav File and then starts playing it. this is my code:

unit Unit1;

interface

uses
  Winapi.Windows, Winapi.Messages, System.SysUtils, System.Variants, System.Classes, Vcl.Graphics,
  Vcl.Controls, Vcl.Forms, Vcl.Dialogs, Vcl.StdCtrls, Generics.collections,
  Vcl.ExtCtrls, MMSystem;

type
  TForm1 = class(TForm)
    Button1: TButton;
    OpenDialog1: TOpenDialog;
    Label1: TLabel;
    Label2: TLabel;
    Shape1: TShape;
    Image1: TImage;
    procedure Button1Click(Sender: TObject);
    procedure FormCreate(Sender: TObject);
    procedure FormClose(Sender: TObject; var Action: TCloseAction);
  private
    { Private declarations }
  public
    { Public declarations }
  end;

type
  TWaveformSample = integer; // signed 32-bit; -2147483648..2147483647
  TWaveformSamples = packed array of TWaveformSample; // one channel

var
  Form1: TForm1;

  myWavFile: file;
  DataBlock: array[0..3] of byte;
  Count: integer;
  NumOfChannels: integer;
  SampleRate: integer;
  BytesPerSecond: integer;
  ByesPerSample: integer;
  BitsPerSample: integer;
  CompressionCode: integer;
  CompressionDesc: string;
  BlockAlign: integer;
  ExtraFormatBytes: integer;

  CompressionCodes: TDictionary<integer, string>;

  BytesRead: integer;

  Samples: TWaveformSamples;
  fmt: TWaveFormatEx;

  PacketIsPlaying: Boolean;

implementation

{$R *.dfm}

procedure InitAudioSys;
begin
  with fmt do
  begin
    wFormatTag := WAVE_FORMAT_PCM;
    nChannels := NumOfChannels;
    nSamplesPerSec := SampleRate;
    wBitsPerSample := BitsPerSample;
    nAvgBytesPerSec := nChannels * nSamplesPerSec * wBitsPerSample div 8;
    nBlockAlign := nChannels * wBitsPerSample div 8;
    cbSize := 0;
  end;
end;


procedure PlaySound;
var
  wo: integer;
  hdr: TWaveHdr;
begin

  if Length(samples) = 0 then
  begin
    Writeln('Error: No audio has been created yet.');
    Exit;
  end;

  if waveOutOpen(@wo, WAVE_MAPPER, @fmt, 0, 0, CALLBACK_NULL) = MMSYSERR_NOERROR then
    try
      PacketIsPlaying := True;
      ZeroMemory(@hdr, sizeof(hdr));
      with hdr do
      begin
        lpData := @samples[0];
        dwBufferLength := fmt.nChannels * Length(Samples) * sizeof(TWaveformSample);
        dwFlags := 0;
      end;

      waveOutPrepareHeader(wo, @hdr, sizeof(hdr));
      waveOutWrite(wo, @hdr, sizeof(hdr));
      //sleep(450);

      //while waveOutUnprepareHeader(wo, @hdr, sizeof(hdr)) = WAVERR_STILLPLAYING do
        //sleep(100);

    finally
      waveOutClose(wo);
      PacketIsPlaying := False;
    end;


end;

function ReadDataBlock(Size: integer): Boolean;
begin
  try
    BlockRead(myWavFile, DataBlock, Size, Count);
    INC(BytesRead, Size);
    Result := True;
  except
    Result := False;
  end;
end;

function OpenWav(FileName: string): Boolean;
begin
  try
    Assignfile(myWavFile, filename);
    Reset(myWavFile, 1);
    Result := True;
  except
    Result := False;
  end;
end;

function CloseWav: Boolean;
begin
  try
    CloseFile(myWavFile);
    Result := True;
  except
    Result := False;
  end;
end;

function ValidateWav: Boolean;
const
  RIFF: array[0..3] of byte = (82, 73, 70, 70);
  WAVE: array[0..3] of byte = (87, 65, 86, 69);
  _FMT: array[0..3] of byte = (102, 109, 116, 32);
  FACT: array[0..3] of byte = (102, 97, 99, 116);
  DATA: array[0..3] of byte = (100, 97, 116, 97);
  _DATA: array[0..3] of byte = (64, 61, 74, 61);
var
  RiffChunkSize, FmtChunkSize, FactChunkSize, DataChunkSize, i, j, tmp, Freq: integer;

  omega,
  dt, t: double;
  vol: double;
begin

  BytesRead := 0;

  //Check "RIFF"
  ReadDataBlock(4);
  if not CompareMem(@DataBlock, @RIFF, SizeOf(DataBlock)) then
    begin
      Result := False;
      Exit;
    end;

  //Get "RIFF" Chunk Data Size
  ReadDataBlock(4);
  Move(DataBlock, RiffChunkSize, 4);

  //Check "WAVE"
  ReadDataBlock(4);
  if not CompareMem(@DataBlock, @WAVE, SizeOf(DataBlock)) then
    begin
      Result := False;
      Exit;
    end;

  {FMT ---------------------------------------------------------------------}

  //Check "FMT"
  ReadDataBlock(4);
  if not CompareMem(@DataBlock, @_FMT, SizeOf(DataBlock)) then
    begin
      Result := False;
      Exit;
    end;

  //Get "FMT" Chunk Data Size
  ReadDataBlock(4);
  Move(DataBlock, FmtChunkSize, 4);

  BytesRead := 0;

  //Get Wav Compression Code
  ReadDataBlock(2);
  Move(DataBlock, CompressionCode, 2);
  if not CompressionCodes.TryGetValue(CompressionCode, CompressionDesc) then
    CompressionDesc := 'File Error!';

  //Get Number of Channels
  ReadDataBlock(2);
  Move(DataBlock, NumOfChannels, 2);

  //Get Sample Rate
  ReadDataBlock(4);
  Move(DataBlock, SampleRate, 4);

  //Get Average Bytes Per Second
  ReadDataBlock(4);
  Move(DataBlock, BytesPerSecond, 4);

  //Get Block Align
  ReadDataBlock(2);
  Move(DataBlock, BlockAlign, 2);

  //Get Bits Per Sample
  ReadDataBlock(2);
  Move(DataBlock, BitsPerSample, 2);

  //Extra Format Bytes
  if BytesRead <= FmtChunkSize - 2 then
    begin
      ReadDataBlock(2);
      Move(DataBlock, ExtraFormatBytes, 2);
    end;

  //If it's not Uncompressed/PCM File, then we have Extra Format Bytes
  if CompressionCode <> 1 then
    begin
      //Skip Compression Data
      for i := 0 to FmtChunkSize - BytesRead - 1 do
        ReadDataBlock(1);

      Result := False;
      Exit;
    end;

  {FACT --------------------------------------------------------------------}

  {FactChunkSize := 0;
  //Check "FACT"
  ReadDataBlock(4);
  if CompareMem(@DataBlock, @FACT, SizeOf(DataBlock)) then
    begin
      //Get "FMT" Chunk Data Size
      ReadDataBlock(4);
      Move(DataBlock, FactChunkSize, 4);

      BytesRead := 0;
      for i := 0 to FactChunkSize - BytesRead - 1 do
        ReadDataBlock(1);
    end;   }

    {DATA ------------------------------------------------------------------}

    while BytesRead < FmtChunkSize do
      ReadDataBlock(1);

    BytesRead := 0;

    //Skip bytes until "data" shows up
    while (not CompareMem(@DataBlock, @DATA, SizeOf(DataBlock))) and (not CompareMem(@DataBlock, @_DATA, SizeOf(DataBlock))) do
    begin
      ReadDataBlock(4);
    end;

    ReadDataBlock(4);
    Move(DataBlock, DataChunkSize, 4);




      Form1.Label1.Caption := 'Compression Code: ' + IntToStr(CompressionCode) + #10#13 +
                        'Compression Description: ' + CompressionDesc + #10#13 +
                        'Number of Channels: ' + IntToStr(NumOfChannels) + #10#13 +
                        'Sample Rate: ' + IntToStr(SampleRate) + #10#13 +
                        'Byes per Sample: ' + IntToStr(ByesPerSample) + #10#13 +
                        'Byes per Second: ' + IntToStr(BytesPerSecond) + #10#13 +
                        'Bits per Second: ' + IntToStr(BitsPerSample);




    tmp := FileSize(myWavFile) - DataChunkSize;

   { j := 0;
    Form1.Image1.Canvas.Rectangle(0, 0, Form1.Image1.Width, Form1.Image1.Height);
    for i := 0 to (DataChunkSize div 20) do
      begin
        //BlockRead(myWavFile, DataBlock, 76, Count);
        tmp := tmp + 76;
        Seek(myWavFile, tmp);

        ReadDataBlock(4);

        Move(DataBlock, Freq, 4);

        if i mod ((DataChunkSize div 80) div Form1.Image1.Width) = 0 then
        begin
          INC(J);
          Form1.Image1.Canvas.MoveTo(j, 121 div 2);
          Form1.Image1.Canvas.LineTo(j, (121 div 2) - Trunc((Freq / High(Integer)) * (121 div 2)));
        end;

        Application.ProcessMessages;
      end;

    Seek(myWavFile, FileSize(myWavFile) - DataChunkSize); }

    InitAudioSys;
    PacketIsPlaying := False;

    SetLength(Samples, fmt.nSamplesPerSec);

    while PacketIsPlaying = false do
      begin
        for i := 0 to fmt.nSamplesPerSec do
          begin
            ReadDataBlock(4);
            Move(DataBlock, Freq, 4);

            Samples[i] := Freq;
          end;

        PlaySound;
        Sleep(2000);
        Application.ProcessMessages;
      end;




  Result := True;

end;

procedure TForm1.Button1Click(Sender: TObject);
var
  f: file;
  b: array[0..3] of byte;
  count: integer;
begin

  with opendialog1 do
  if execute then
    begin
      Form1.Image1.Canvas.Rectangle(0, 0, Form1.Image1.Width, Form1.Image1.Height);
      Label1.Font.Color := clBlack;

      OpenWav(FileName);

      if ValidateWav = False then
        begin
          Label1.Caption := 'Invalid File Data!';
          Label1.Font.Color := clRed;
          Exit;
        end;



      CloseWav;
    end;

end;

procedure TForm1.FormClose(Sender: TObject; var Action: TCloseAction);
begin
  CompressionCodes.Destroy;
end;

procedure TForm1.FormCreate(Sender: TObject);
begin
  Image1.Canvas.Rectangle(0, 0, Image1.Width, Image1.Height);

  CompressionCodes := TDictionary<integer, string>.Create;

  CompressionCodes.Add(0, 'Unknown');
  CompressionCodes.Add(1, 'PCM/Uncompressed');
  CompressionCodes.Add(2, 'Microsoft ADPCM');
  CompressionCodes.Add(6, 'ITU G.711 a-law');
  CompressionCodes.Add(7, 'ITU G.711 µ-law');
  CompressionCodes.Add(17, 'IMA ADPCM');
  CompressionCodes.Add(20, 'ITU G.723 ADPCM (Yamaha)');
  CompressionCodes.Add(49, 'GSM 6.10');
  CompressionCodes.Add(64, 'ITU G.721 ADPCM');
  CompressionCodes.Add(80, 'MPEG');
  CompressionCodes.Add(85, 'ISO/MPEG');
  CompressionCodes.Add(65536, 'Experimental');


end;

end.

The Code needs a TLabel, a Tbutton and an OpenFileDialog on the form.

I have problem with the File Playback. currently I create arrays of samples with the length of SamplesPerSecond and play them one after another with the delay of 2000 (delays less than 2000ms will raise error). What I want now is how can I Read samples and play them one after another smoothly and without delay. and Also I want to be able to visualize every few samples on a graph as the file is being played.


Solution

  • Funny you post this when you did, because I just yesterday wrote a working WAV player using Microsoft's waveOut... API.

    You are not reading through the RIFF chunks effectively/correctly. I strongly suggest you use Microsoft's Multimedia functions (mmioOpen(), mmioDescend(), mmioAscend() and mmioRead()) instead of using AssignFile() and BlockRead(). WAV files are more complicated than you think, the code you have shown is not flexible enough to handle everything it may encounter. For instance, FMT is not always the first chunk in a WAV file, and there may be other chunks present before the DATA chunk, which you are not skipping.

    When using waveOutOpen(), you should pass the original WAVEFORMATEX as read from the file, rather than creating a new WAVEFORMATEX that you populate with interpreted values. Using MMIO functions, you can declare a WAVEFORMATEX variable, mmioDescend() into the FMT chunk, mmioRead() the entire chunk directly into the variable, and then pass the variable as-is to waveOutOpen().

    When using waveOutWrite(), you should use multiple audio buffers that you loop through (you can pre-prepare them with waveOutPrepareHeader() before you start reading the audio sample data, so you are only preparing them once). If you supply the wave device with only one buffer at a time, you are likely to get choppy audio playback (which it sounds like you are). It is best to use at least 3 buffers (my player uses 20, but I may knock that back later):

    1. Fill 2 buffers with sample data and pass them to waveOutWrite() right away, and fill the 3rd buffer while they are playing.
    2. When your waveOutOpen() callback says the 1st buffer is done playing, pass the 3rd buffer to waveOutWrite() and fill the 1st buffer with new data.
    3. When the callback says the 2nd buffer is done playing, pass the 1st buffer to waveOutWrite() and fill the 2nd buffer with new data.
    4. When the callback says the 3rd buffer is done playing, pass the 2nd buffer to waveOutWrite() and fill the 3rd buffer with new data.
    5. And so on, continuing this round-robin logic until the end of the DATA chunk is reached.

    The wave device should always have at least 2 active audio buffers playing at any given time to avoid gaps in the playback. Let the callback tell you when each buffer is done so you can provide the next buffer.

    I based my player code on David Overton's tutorial, which has a LOT of information, and code examples:

    Playing Audio in Windows using waveOut Interface
    http://www.et.hs-wismar.de/~litschke/TMS/Audioprogrammierung.pdf
    http://www.planet-source-code.com/vb/scripts/ShowCode.asp?txtCodeId=4422&lngWId=3

    The only tweaks I made to the tutorial's code was to:

    1. using MMIO functions for file I/O.
    2. using the RTL's memory management functions instead of OS memory functions.
    3. changed the size of the audio buffers. David uses 8KB buffers, which I found caused garbage playback after a few seconds as the wave device was not being fed audio samples fast enough for my WAV files (which are GSM encoded, not PCM, so they have smaller sample sizes). I changed the buffer size to the nAvgBytesPerSec value reported by the FMT chunk, and then the audio played cleanly all the way through.
    4. error handling.

    Try this (translated to Delphi from my real code written in C++):

    {
    The following is based on code written by David Overton:
    
    Playing Audio in Windows using waveOut Interface
    http://www.planet-source-code.com/vb/scripts/ShowCode.asp?txtCodeId=4422&lngWId=3
    https://www.et.hs-wismar.de/~litschke/TMS/Audioprogrammierung.pdf
    
    But with some custom tweaks.
    }
    
    uses
      ..., Winapi.Windows, Winapi.MMSystem;
    
    const
      BLOCK_COUNT = 20;
    
    procedure waveOutProc(hWaveOut: HWAVEOUT; uMsg: UINT; dwInstance, dwParam1, dwParam2: DWORD_PTR): stdcall; forward;
    function writeAudio(hWaveOut: HWAVEOUT; data: PByte; size: Integer): Boolean; forward;
    
    var
      waveCriticalSection: CRITICAL_SECTION;
      waveBlocks: PWaveHdr;
      waveFreeBlockCount: Integer;
      waveCurrentBlock: Integer;
      buffer: array[0..1023] of Byte;
      mmckinfoParent: MMCKINFO;
      mmckinfoSubchunk: MMCKINFO;
      dwFmtSize: DWORD;
      dwDataSize: DWORD;
      dwSizeToRead: DWORD;
      hmmio: HMMIO;
      wfxBuffer: array of Byte;
      wfx: PWaveFormatEx;
      hWaveOut: HWAVEOUT;
      blockBuffer: array of Byte;
      pBlockData: PByte;
      i: Integer;
      readBytes: LONG;
    begin
      ...
      hmmio := mmioOpen(PChar(FileName), nil, MMIO_READ or MMIO_DENYWRITE);
      if hmmio = 0 then
        raise Exception.Create('Unable to open WAV file');
    
      try
        mmckinfoParent.fccType := mmioStringToFOURCC('WAVE', 0);
        if mmioDescend(hmmio, @mmckinfoParent, nil, MMIO_FINDRIFF) <> MMSYSERR_NOERROR then
          raise Exception.CreateFmt('%s is not a WAVE file', [FileName]);
    
        mmckinfoSubchunk.ckid := mmioStringToFOURCC('fmt', 0);
        if mmioDescend(hmmio, @mmckinfoSubchunk, @mmckinfoParent, MMIO_FINDCHUNK) <> MMSYSERR_NOERROR then
          raise Exception.Create('File has no FMT chunk');
    
        dwFmtSize := mmckinfoSubchunk.cksize;
        if dwFmtSize = 0 then
          raise Exception.Create('File FMT chunk is empty');
    
        SetLength(wfxBuffer, dwFmtSize);
        wfx := PWaveFormatEx(Pointer(wfxBuffer));
    
        if mmioRead(hmmio, PAnsiChar(wfx), dwFmtSize) <> dwFmtSize then
          raise Exception.Create('Failed to read FMT chunk');
    
        if mmioAscend(hmmio, @mmckinfoSubchunk, 0) <> MMSYSERR_NOERROR then
          raise Exception.Create('Failed to ascend into RIFF chunk');
    
        mmckinfoSubchunk.ckid := mmioStringToFOURCC('data', 0);
        if mmioDescend(hmmio, @mmckinfoSubchunk, @mmckinfoParent, MMIO_FINDCHUNK) <> MMSYSERR_NOERROR then
          raise Exception.Create('File has no DATA chunk');
    
        dwDataSize := mmckinfoSubchunk.cksize;
        if dwDataSize <> 0 then
        begin
          hWaveOut := 0;
          if waveOutOpen(@hWaveOut, WAVE_MAPPER, wfx, DWORD_PTR(@waveOutProc), 0, CALLBACK_FUNCTION) <> MMSYSERR_NOERROR then
            raise Exception.Create('Unable to open wave mapper device');
    
          try
            SetLength(blockBuffer, (sizeof(WAVEHDR) + wfx.nAvgBytesPerSec) * BLOCK_COUNT);
            pBlockData := PByte(blockBuffer);
    
            waveBlocks := PWaveHdr(pBlockData);
            Inc(pBlockData, sizeof(WAVEHDR) * BLOCK_COUNT);
            for i := 0 to BLOCK_COUNT-1 do
            begin
              ZeroMemory(@waveBlocks[i], sizeof(WAVEHDR));
              waveBlocks[i].dwBufferLength := wfx.nAvgBytesPerSec;
              waveBlocks[i].lpData := pBlockData;
    
              if waveOutPrepareHeader(hWaveOut, @waveBlocks[i], sizeof(WAVEHDR)) <> MMSYSERR_NOERROR then
                raise Exception.Create('Failed to prepare a WAV audio header');
    
              Inc(pBlockData, wfx.nAvgBytesPerSec);
            end;
    
            waveFreeBlockCount := BLOCK_COUNT;
            waveCurrentBlock := 0;
    
            InitializeCriticalSection(@waveCriticalSection);
            try
              repeat
                dwSizeToRead := Min(dwDataSize, sizeof(buffer));
    
                readBytes := mmioRead(hmmio, PAnsiChar(buffer), dwSizeToRead);
                if readBytes <= 0 then Break;
    
                if readBytes < sizeof(buffer) then
                  ZeroMemory(@buffer[readBytes], sizeof(buffer) - readBytes);
    
                writeAudio(hWaveOut, buffer, sizeof(buffer));
    
                Dec(dwDataSize, readBytes);
              until dwDataSize = 0;
    
              writeAudio(hWaveOut, nil, 0);
    
              while waveFreeBlockCount < BLOCK_COUNT do
                Sleep(10);
    
              for i := 0 to BLOCK_COUNT-1 do
              begin
                if (waveBlocks[i].dwFlags and WHDR_PREPARED) <> 0 then
                  waveOutUnprepareHeader(hWaveOut, @waveBlocks[i], sizeof(WAVEHDR));
              end;
            finally
              DeleteCriticalSection(@waveCriticalSection);
            end;
          finally
            waveOutClose(hWaveOut);
          end;
        end;
      finally
        mmioClose(hmmio, 0);
      end;
    end;
    
    procedure waveOutProc(hWaveOut: HWAVEOUT; uMsg: UINT; dwInstance, dwParam1, dwParam2: DWORD_PTR); stdcall;
    begin
      if uMsg = WOM_DONE then
      begin
        EnterCriticalSection(&waveCriticalSection);
        Inc(waveFreeBlockCount);
        LeaveCriticalSection(&waveCriticalSection);
      end;
    end;
    
    procedure writeAudio(hWaveOut: HWAVEOUT; data: PByte; size: Integer);
    var
      current: PWaveHdr;
      remaining: Integer;
    begin
      current := @waveBlocks[waveCurrentBlock];
    
      if data = nil then
      begin
        if current.dwUser <> 0 then
        begin
          if current.dwUser < current.dwBufferLength then
          begin
            remaining := Integer(current.dwBufferLength - current.dwUser);
            ZeroMemory(current.lpData + current.dwUser, remaining);
            Inc(current.dwUser, remainint);
          end;
    
          EnterCriticalSection(&waveCriticalSection);
          Dec(waveFreeBlockCount);
          LeaveCriticalSection(&waveCriticalSection);
    
          if waveOutWrite(hWaveOut, current, sizeof(WAVEHDR)) <> MMSYSERR_NOERROR then
            raise Exception.Create('Failed to write a WAV audio header');
        end;
      end else
      begin
        while size > 0 do
        begin
          remaining := Integer(current.dwBufferLength - current.dwUser);
          if size < remaining then
          begin
            Move(data^, (current.lpData + current.dwUser)^, size);
            Inc(current.dwUser, size);
            Break;
          end;
    
          Move(data^, (current.lpData + current.dwUser)^, remaining);
          Inc(current.dwUser, remaining);
    
          Inc(data, remaining);
          Dec(size, remaining);
    
          EnterCriticalSection(&waveCriticalSection);
          Dec(waveFreeBlockCount);
          LeaveCriticalSection(&waveCriticalSection);
    
          if waveOutWrite(hWaveOut, current, sizeof(WAVEHDR)) <> MMSYSERR_NOERROR then
             raise Exception.Create('Failed to write a WAV audio header');
    
          while waveFreeBlockCount = 0 do
            Sleep(10);
    
          Inc(waveCurrentBlock);
          waveCurrentBlock := waveCurrentBlock mod BLOCK_COUNT;
          current := @waveBlocks[waveCurrentBlock];
          current.dwUser := 0;
        end;
      end;
    end;
    

    Regarding visualization of the samples, you are best off using a 3rd party component for that (and you probably should be using a 3rd party WAV player anyway, instead of writing API code manually), such as Mitov Software's AudioLab components.