I am playing with BASS from http://www.un4seen.com/.
I need to create a flac file(16bits) or flac stream from user speaking on Microphone.
I have seen this demo in BASS source code.
There is a bassenc_flac.dll as well with these functions:
function BASS_Encode_FLAC_Start(handle:DWORD; options:PChar; flags:DWORD; proc:ENCODEPROCEX; user:Pointer): HENCODE; {$IFDEF MSWINDOWS}stdcall{$ELSE}cdecl{$ENDIF}; external bassencflacdll;
function BASS_Encode_FLAC_StartFile(handle:DWORD; options:PChar; flags:DWORD; filename:PChar): HENCODE; {$IFDEF MSWINDOWS}stdcall{$ELSE}cdecl{$ENDIF}; external bassencflacdll;
How could I change the next code to encode the audio to flac file or stream?
From RecordTest BASS demo
(* This is called while recording audio *)
function RecordingCallback(Handle: HRECORD; buffer: Pointer; length: DWORD; user: Pointer): boolean; stdcall;
var level:dword;
begin
level:=BASS_ChannelGetLevel(Handle);
// Copy new buffer contents to the memory buffer
Form1.WaveStream.Write(buffer^, length);
// Allow recording to continue
Result := True;
end;
(* Start recording to memory *)
procedure TForm1.StartRecording;
begin
if ComboBox1.ItemIndex < 0 then Exit;
if WaveStream.Size > 0 then
begin // free old recording
BASS_StreamFree(chan);
WaveStream.Clear;
end;
// generate header for WAV file
with WaveHdr do
begin
riff := 'RIFF';
len := 36;
cWavFmt := 'WAVEfmt ';
dwHdrLen := 16;
wFormat := 1;
wNumChannels := 2;
dwSampleRate := 44100;
wBlockAlign := 4;
dwBytesPerSec := 176400;
wBitsPerSample := 16;
cData := 'data';
dwDataLen := 0;
end;
WaveStream.Write(WaveHdr, SizeOf(WAVHDR));
// start recording # 44100hz 16-bit stereo
rchan := BASS_RecordStart(44100, 2, 0, #RecordingCallback, nil);
if rchan = 0 then
begin
MessageDlg('Couldn''t start recording!', mtError, [mbOk], 0);
WaveStream.Clear;
end
else
begin
bRecord.Caption := 'Stop';
bPlay.Enabled := False;
bSave.Enabled := False;
end;
end;
(* Stop recording *)
procedure TForm1.StopRecording;
var
i: integer;
he:BassEnc.HENCODE;
begin
BASS_ChannelStop(rchan);
bRecord.Caption := 'Record';
// complete the WAV header
WaveStream.Position := 4;
i := WaveStream.Size - 8;
WaveStream.Write(i, 4);
i := i - $24;
WaveStream.Position := 40;
WaveStream.Write(i, 4);
WaveStream.Position := 0;
// create a stream from the recorded data
chan := BASS_StreamCreateFile(True, WaveStream.Memory, 0, WaveStream.Size, 0);
if chan <> 0 then
begin
// enable "Play" & "Save" buttons
bPlay.Enabled := True;
bSave.Enabled := True;
end
else
MessageDlg('Error creating stream from recorded data!', mtError, [mbOk], 0);
if SaveDialog.Execute then
WaveStream.SaveToFile(SaveDialog.FileName);
end;
I have updated code because of comments that show incorrect work of previous encoder version. And I am totally agree with these comments.
In order to create an encoder to FLAC we should go to un4seen web-site and download the next files:
BASS audio library 2.4
BASSFLAC 2.4.4
BASSenc 2.4.14
BASSenc_FLAC 2.4.1.1
Go through these folders and look for the next files:
bass.pas
bassenc.pas
bassenc_flac.pas
Now place these pas-files into one folder and add it to Library via Delphi's options.
After this step create new project, save it in separate folder.
Then go through BASS_XXX folders and look for *.dll files.
Combine them together in the folder where you have saved your project!
Now let's write some code.
Add to the uses clause bass.pas, bassenc.pas and bassenc_flac.pas. Then copy the code shown below.
uses ..., BASS, BASSEnc, BASSEnc_FLAC;
...
TForm1 = class(TForm)
ProgressBar1: TProgressBar;
public
{ Public declarations }
procedure StartEncode(SourceFileName, OutputFileName: String);
procedure StopEncode;
end;
...
procedure TForm1.StartEncode(SourceFileName, OutputFileName: String);
var
PercentDone: Cardinal;
Buffer: array [0..1024] of Byte;
begin
Channel := BASS_StreamCreateFile(false, PChar(SourceFileName), 0, 0, BASS_MUSIC_DECODE or BASS_UNICODE);
BASSEnc_FLAC.BASS_Encode_FLAC_StartFile(Channel, 0, BASS_ENCODE_FP_AUTO or BASS_UNICODE, PChar(OutputFileName));
while BASS_ChannelIsActive(Channel) > 0 do
begin
BASS_ChannelGetData(Channel, #Buffer, 1024);
PercentDone := Trunc(100 * (BASS_ChannelGetPosition(Channel, BASS_POS_BYTE) / BASS_ChannelGetLength(Channel, BASS_POS_BYTE)));
ProgressBar1.Position := PercentDone;
end;
StopEncode;
end;
procedure TForm1.StopEncode;
begin
BASS_Encode_Stop(Channel);
BASS_StreamFree(Channel);
end;
procedure TForm1.Button1Click(Sender: TObject);
begin
BASS_Init(-1, 44100, 0, Application.Handle, nil);
try
// Set name of file to convert it to FLAC and save it with output name
StartEncode('SourceFileName', 'OutputFileName');
finally
BASS.BASS_Free;
end;
end;
One notice:
Indeed, file encoded with previous version of the code had incorrect header (I could see it when opened file in Notepad.exe). After code has been updated I can see valid header (in Notepad, of course, because I have no professional instruments for work with audio-files).
Now you even have no need to add plugin to BASS as I did earlier.
Since this I think that the encoder works as it was expected.
I have a call to GetDIBits that works perfectly in 32-bit, but fails on 64-bit. Despite the different values for the handles the content of the bitmapinfo structure are the same.
Here is the smallest (at least slightly structured) code example I could come up with to reproduce the error. I tested with Delphi 10 Seattle Update 1, but the error seems to occur even with other Delphi versions.
program Project1;
{$APPTYPE CONSOLE}
{$R *.res}
uses
Winapi.Windows,
System.SysUtils,
Vcl.Graphics;
type
TRGBALine = array[Word] of TRGBQuad;
PRGBALine = ^TRGBALine;
type
{ same structure as TBitmapInfo, but adds space for two more entries in bmiColors }
TMyBitmapInfo = record
bmiHeader: TBitmapInfoHeader;
bmiColors: array[0..2] of TRGBQuad;
public
constructor Create(AWidth, AHeight: Integer);
end;
constructor TMyBitmapInfo.Create(AWidth, AHeight: Integer);
begin
FillChar(bmiHeader, Sizeof(bmiHeader), 0);
bmiHeader.biSize := SizeOf(bmiHeader);
bmiHeader.biWidth := AWidth;
bmiHeader.biHeight := -AHeight; //Otherwise the image is upside down.
bmiHeader.biPlanes := 1;
bmiHeader.biBitCount := 32;
bmiHeader.biCompression := BI_BITFIELDS;
bmiHeader.biSizeImage := 4*AWidth*AHeight; // 4 = 32 Bits/Pixel div 8 Bits/Byte
bmiColors[0].rgbRed := 255;
bmiColors[1].rgbGreen := 255;
bmiColors[2].rgbBlue := 255;
end;
procedure Main;
var
bitmap: TBitmap;
res: Cardinal;
Bits: PRGBALine;
buffer: TMyBitmapInfo;
BitmapInfo: TBitmapInfo absolute buffer;
BitsSize: Cardinal;
icon: TIcon;
IconInfo: TIconInfo;
begin
bitmap := TBitmap.Create;
try
icon := TIcon.Create;
try
icon.LoadFromResourceID(0, Integer(IDI_WINLOGO));
if not GetIconInfo(icon.Handle, IconInfo) then begin
Writeln('Error GetIconInfo: ', GetLastError);
Exit;
end;
bitmap.PixelFormat := pf32bit;
bitmap.Handle := IconInfo.hbmColor;
BitsSize := BytesPerScanline(bitmap.Width, 32, 32) * bitmap.Height;
Bits := AllocMem(BitsSize);
try
ZeroMemory(Bits, BitsSize);
buffer := TMyBitmapInfo.Create(bitmap.Width, bitmap.Height);
res := GetDIBits(bitmap.Canvas.Handle, bitmap.Handle, 0, bitmap.Height, Bits, BitmapInfo, DIB_RGB_COLORS);
if res = 0 then begin
Writeln('Error GetDIBits: ', GetLastError);
Exit;
end;
Writeln('Succeed');
finally
FreeMem(Bits);
end;
finally
icon.Free;
end;
finally
bitmap.Free;
end;
end;
begin
try
Main;
except
on E: Exception do
Writeln(E.ClassName, ': ', E.Message);
end;
Readln;
end.
Update A comment to this answer points the way as to why your code is failing. The order of evaluation of bitmap.Handle and bitmap.Canvas.Handle matters. Since parameter evaluation order is undefined, your program has undefined behaviour. And that explains why the x86 and x64 programs differ in behaviour.
So you could resolve the issue by assigning the bitmap handle and device context to local variables in the appropriate order, and then passing these as the arguments to GetDIBits. But I still think that the code is far better to avoid the VCL TBitmap class and use GDI calls directly, as in the code below.
I believe that your mistake is to pass the bitmap handle, and its canvas handle. Instead you should pass, for example, a device context obtained by calling CreateCompatibleDC(0). Or pass IconInfo.hbmColor to GetDIBits. But don't pass the handle of the TBitmap and the handle of its canvas.
I also cannot see any purpose to the TBitmap that you create. All you do with it is obtain the width and height of IconInfo.hbmColor. You don't need to create TBitmap to do that.
So if I were you I would remove the TBitmap, and use CreateCompatibleDC(0) to obtain the device context. This should greatly simplify the code.
You will also need to delete the bitmaps returned by the call to GetIconInfo, but I guess that you know this already and removed that code from the question for simplicity.
Frankly, the VCL objects are just getting in the way here. It's actually much simpler to call the GDI functions directly. Perhaps something like this:
procedure Main;
var
res: Cardinal;
Bits: PRGBALine;
bitmap: Winapi.Windows.TBitmap;
DC: HDC;
buffer: TMyBitmapInfo;
BitmapInfo: TBitmapInfo absolute buffer;
BitsSize: Cardinal;
IconInfo: TIconInfo;
begin
if not GetIconInfo(LoadIcon(0, IDI_WINLOGO), IconInfo) then begin
Writeln('Error GetIconInfo: ', GetLastError);
Exit;
end;
try
if GetObject(IconInfo.hbmColor, SizeOf(bitmap), #bitmap) = 0 then begin
Writeln('Error GetObject');
Exit;
end;
BitsSize := BytesPerScanline(bitmap.bmWidth, 32, 32) * abs(bitmap.bmHeight);
Bits := AllocMem(BitsSize);
try
buffer := TMyBitmapInfo.Create(bitmap.bmWidth, abs(bitmap.bmHeight));
DC := CreateCompatibleDC(0);
res := GetDIBits(DC, IconInfo.hbmColor, 0, abs(bitmap.bmHeight), Bits, BitmapInfo,
DIB_RGB_COLORS);
DeleteDC(DC);
if res = 0 then begin
Writeln('Error GetDIBits: ', GetLastError);
Exit;
end;
Writeln('Succeed');
finally
FreeMem(Bits);
end;
finally
DeleteObject(IconInfo.hbmMask);
DeleteObject(IconInfo.hbmColor);
end;
end;
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.
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):
Fill 2 buffers with sample data and pass them to waveOutWrite() right away, and fill the 3rd buffer while they are playing.
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.
When the callback says the 2nd buffer is done playing, pass the 1st buffer to waveOutWrite() and fill the 2nd buffer with new data.
When the callback says the 3rd buffer is done playing, pass the 2nd buffer to waveOutWrite() and fill the 3rd buffer with new data.
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:
using MMIO functions for file I/O.
using the RTL's memory management functions instead of OS memory functions.
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.
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.
UINT is an unsigned 32 bit integer which is not used in Delphi. Change UINT to "cardinal", which is Delphi's 32 bit unsigned integer.
Ian
Is there a real-time microphone Delphi component?
I'm looking for one that provides the results without a buffer.
You'll want an audio card that has ASIO drivers, such as the higher end Sound Blaster cards. ASIO has very low latency, as it provides a minimal layer between the software and hardware. Then you can use this:
Delphi ASIO & VST Project
Other alternatives include using the FFMPEG Delphi port and, of course, Direct X from Jedi.
There's AudioLab from Mitov Software, which claims to do things you describe. I haven't tried it.
Real time get microphone data in one array :
unit WaveSound;
interface
uses
SysUtils, MMSystem;
const
NUMSAMPLES = 1024; // Number of Samples
type
TIndata = array[0 .. NUMSAMPLES - 1] of Integer;
PIndata = ^TIndata;
TFrec= record
Fx, dx :Integer;
end;
function SNDInitWaveIn: Cardinal;
procedure SNDProcWaveIn(var Indata : TIndata);
procedure SNDStopWave;
implementation
var
DevHandle : Integer;
WAVEFORMAT1 : TWAVEFORMATEX;
Wave : WAVEHDR;
function SNDInitWaveIn: Cardinal;
begin
with WAVEFORMAT1 do begin
wFormatTag := WAVE_FORMAT_PCM;
nChannels := 1;
nSamplesPerSec := 44100;// 11025; //11khz
wBitsPerSample := 16;
nBlockAlign := (nChannels * wBitsPerSample) div 8;
nAvgBytesPerSec := nBlockAlign * nSamplesPerSec;
cbSize := 0;
end;
Result:= waveInOpen(#DevHandle, cardinal(-1),#WAVEFORMAT1, cardinal(0), cardinal(0), cardinal(0));
If not(DevHandle = 0) Then waveInStart(DevHandle);
end;
procedure SNDProcWaveIn(var Indata : TIndata);
begin
//lpdata requires the address of an array to fill up data with
Wave.lpData := #Indata;
//the buffer length
Wave.dwBufferLength := NUMSAMPLES;
Wave.dwFlags := 0;
//prepare device for input
waveInPrepareHeader(DevHandle, #Wave, sizeof(Wave));
waveInAddBuffer(DevHandle, #Wave, sizeof(Wave));
// if the following statement is removed, the vis. will be a lot faster (avs style)
// but uses up 100% of cpu!
// this is why i hate avs
Sleep(10); // give device a breather
// the following loop is quite useless, but anyway...
repeat
//Just wait for the blocks to be done or the device to close
until (((Wave.dwFlags and WHDR_DONE)= WHDR_DONE) or (DevHandle = 0));
If (DevHandle = 0) Then Exit; //Cut out if the device is closed
waveInUnprepareHeader(DevHandle, #Wave, sizeof(Wave));
end;
procedure SNDStopWave;
begin
waveInReset(DevHandle);
waveInClose(DevHandle);
DevHandle := 0;
end;
/////////// [sample of use : ] /////////////////
var
ind :TIndata;
procedure TForm1.FormCreate(Sender: TObject);
begin
SNDInitWaveIn;
Timer1.Enabled:= true;
end;
procedure TForm1.Timer1Timer(Sender: TObject);
begin
SNDProcWaveIn(ind);
//code for proc data in "ind" buffer
end;
procedure TForm1.FormClose(Sender: TObject; var Action: TCloseAction);
begin
SNDStopWave;
end;