2014-11-13 129 views
1

我已经写了一个简单的代码来读取Wav文件的头文件,然后开始播放它。这是我的代码:在Delphi中播放PCM Wav文件

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. 

的代码需要的TLabel,一个TButton和打开文件对话框的形式。

我有文件播放问题。目前,我创建了长度为SamplesPerSecond的样本阵列,并在2000年的延迟中一个接一个地播放它们(延迟少于2000毫秒会引发错误)。 我现在想要的是我怎样才能顺利阅读样品并连续播放,毫不拖延。另外,我希望能够在文件正在播放时将图形中的每个样本都可视化。

回答

10

有趣的是,当你这样做时发布,因为我刚刚昨天写了一个使用微软的waveOut... API工作的WAV播放器。

您没有通过RIFF块有效/正确地阅读。我强烈建议您使用微软的多媒体功能(mmioOpen(),mmioDescend(),mmioAscend()mmioRead()),而不是使用AssignFile()BlockRead()。 WAV文件比您想象的更复杂,您所显示的代码不够灵活,无法处理可能遇到的所有问题。例如,FMT并不总是WAV文件中的第一个块,并且可能还有其他块出现在DATA块之前,而您并未跳过该块。

使用waveOutOpen(),你应该通过原WAVEFORMATEX从文件读取,而不是创建你解释值填充新WAVEFORMATEX。使用MMIO函数,可以将WAVEFORMATEX变量mmioDescend()声明为FMT块,mmioRead()整个块直接放入该变量中,然后将该变量原样传递给waveOutOpen()

使用waveOutWrite(),你应该使用多个音频缓冲,你可以通过循环(你可以用waveOutPrepareHeader()预先做好准备在你开始阅读的音频采样数据之前,所以你只准备他们一次)。如果您一次只向一个缓冲区提供波形设备,则很可能会听起来波涛汹涌(这听起来就像是你)。最好是使用至少3个缓冲区(我的播放器使用20,但我可以敲那回来以后):

  1. 填充2个缓冲器与样本数据并传递给waveOutWrite()向右走,并填写第三缓冲而他们正在玩。
  2. 当您的waveOutOpen()回调表示第一个缓冲区已完成播放时,将第三个缓冲区传递到waveOutWrite()并用新数据填充第一个缓冲区。
  3. 当回调表示第二个缓冲区已完成播放时,将第一个缓冲区传递到waveOutWrite()并用新数据填充第二个缓冲区。
  4. 当回调表示第三个缓冲区已完成播放时,将第二个缓冲区传递到waveOutWrite()并用新数据填充第三个缓冲区。
  5. 依此类推,继续此循环法逻辑,直至达到DATA块的末尾。

波形设备在任何给定的时间应始终至少有2个活动音频缓冲区,以避免播放时出现空隙。让回调告诉你每个缓冲区何时完成,以便您可以提供下一个缓冲区。

我根据大卫·奥弗顿的教程,它具有的信息有很多我的播放器代码和代码示例:使用waveout的接口

在Windows播放音频
http://www.et.hs-wismar.de/~litschke/TMS/Audioprogrammierung.pdf
http://www.planet-source-code.com/vb/scripts/ShowCode.asp?txtCodeId=4422&lngWId=3

唯一的调整我使教程的代码是:

  1. 使用文件I/O的MMIO函数。
  2. 使用RTL的内存管理功能而不是OS内存功能。
  3. 更改了音频缓冲区的大小。大卫使用8KB缓冲区,我发现在几秒钟​​后垃圾回放,因为我的WAV文件(这是GSM编码,而不是PCM,因此他们有较小的样本大小)的波形设备没有足够快的音频采样。我将缓冲区大小更改为由FMT组块报告的nAvgBytesPerSec值,然后音频一路播放干净。
  4. 错误处理。

试一下这个(从我的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; 

关于样本的可视化,你是最好关闭使用一个第三方组件(和你应该无论如何都要使用第三方WAV播放器,而不是手动编写API代码),例如Mitov Software's AudioLab组件。

+1

+1这是一个非常棒的答案,值得许多upvotes。 –

+0

感谢您的详细解答。正如你所提到的,我尝试了在我的代码中使用3个缓冲区,它听起来非常好。这是我第一次编写处理音频文件的代码,这只是一个开始。我还有很长的路要走。我正在处理您现在提供的代码。 – Agha