unit UAudioInput_Portaudio;
interface
{$IFDEF FPC}
{$MODE Delphi}
{$ENDIF}
{$I ../switches.inc}
uses
Classes,
SysUtils,
UMusic;
implementation
uses
{$IFDEF UsePortmixer}
portmixer,
{$ENDIF}
portaudio,
UAudioCore_Portaudio,
URecord,
UIni,
ULog,
UMain;
type
TAudioInput_Portaudio = class(TAudioInputBase)
public
function GetName: String; override;
function InitializeRecord: boolean; override;
destructor Destroy; override;
end;
TPortaudioInputDevice = class(TAudioInputDevice)
public
RecordStream: PPaStream;
PaDeviceIndex: TPaDeviceIndex;
function Start(): boolean; override;
procedure Stop(); override;
end;
function MicrophoneCallback(input: Pointer; output: Pointer; frameCount: Longword;
timeInfo: PPaStreamCallbackTimeInfo; statusFlags: TPaStreamCallbackFlags;
inputDevice: Pointer): Integer; cdecl; forward;
function MicrophoneTestCallback(input: Pointer; output: Pointer; frameCount: Longword;
timeInfo: PPaStreamCallbackTimeInfo; statusFlags: TPaStreamCallbackFlags;
inputDevice: Pointer): Integer; cdecl; forward;
var
singleton_AudioInputPortaudio : IAudioInput;
{ TPortaudioInputDevice }
function TPortaudioInputDevice.Start(): boolean;
var
Error: TPaError;
ErrorMsg: string;
inputParams: TPaStreamParameters;
deviceInfo: PPaDeviceInfo;
begin
Result := false;
// get input latency info
deviceInfo := Pa_GetDeviceInfo(PaDeviceIndex);
// set input stream parameters
with inputParams do
begin
device := PaDeviceIndex;
channelCount := AudioFormat.Channels;
sampleFormat := paInt16;
suggestedLatency := deviceInfo^.defaultLowInputLatency;
hostApiSpecificStreamInfo := nil;
end;
//Log.LogStatus(deviceInfo^.name, 'Portaudio');
//Log.LogStatus(floattostr(deviceInfo^.defaultLowInputLatency), 'Portaudio');
// open input stream
Error := Pa_OpenStream(RecordStream, @inputParams, nil,
AudioFormat.SampleRate,
paFramesPerBufferUnspecified, paNoFlag,
@MicrophoneCallback, Pointer(Self));
if(Error <> paNoError) then
begin
ErrorMsg := Pa_GetErrorText(Error);
Log.LogError('Error opening stream: ' + ErrorMsg, 'TPortaudioInputDevice.Start');
Exit;
end;
// start capture
Error := Pa_StartStream(RecordStream);
if(Error <> paNoError) then
begin
Pa_CloseStream(RecordStream);
ErrorMsg := Pa_GetErrorText(Error);
Log.LogError('Error starting stream: ' + ErrorMsg, 'TPortaudioInputDevice.Start');
Exit;
end;
Result := true;
end;
procedure TPortaudioInputDevice.Stop();
begin
if assigned(RecordStream) then
begin
// Note: do NOT call Pa_StopStream here!
// It gets stuck on devices with non-working callback as Pa_StopStream
// waits until all buffers have been handled (which never occurs in that
// case).
// Pa_CloseStream internally calls Pa_AbortStream which works as expected.
Pa_CloseStream(RecordStream);
end;
end;
{ TAudioInput_Portaudio }
function TAudioInput_Portaudio.GetName: String;
begin
result := 'Portaudio';
end;
function TAudioInput_Portaudio.InitializeRecord(): boolean;
var
i: integer;
paApiIndex: TPaHostApiIndex;
paApiInfo: PPaHostApiInfo;
deviceName: string;
deviceIndex: TPaDeviceIndex;
deviceInfo: PPaDeviceInfo;
sourceCnt: integer;
sourceIndex: integer;
sourceName: string;
channelCnt: integer;
SC: integer; // soundcard
err: TPaError;
errMsg: string;
paDevice: TPortaudioInputDevice;
inputParams: TPaStreamParameters;
stream: PPaStream;
streamInfo: PPaStreamInfo;
sampleRate: integer;
latency: TPaTime;
{$IFDEF UsePortmixer}
mixer: PPxMixer;
{$ENDIF}
cbPolls: integer;
cbWorks: boolean;
begin
result := false;
// initialize portaudio
err := Pa_Initialize();
if(err <> paNoError) then
begin
Log.LogError(Pa_GetErrorText(err), 'TAudioInput_Portaudio.InitializeRecord');
Exit;
end;
// choose the best available Audio-API
paApiIndex := TAudioCore_Portaudio.GetPreferredApiIndex();
if(paApiIndex = -1) then
begin
Log.LogError('No working Audio-API found', 'TAudioInput_Portaudio.InitializeRecord');
Exit;
end;
paApiInfo := Pa_GetHostApiInfo(paApiIndex);
SC := 0;
// init array-size to max. input-devices count
SetLength(AudioInputProcessor.Device, paApiInfo^.deviceCount);
for i:= 0 to High(AudioInputProcessor.Device) do
begin
// convert API-specific device-index to global index
deviceIndex := Pa_HostApiDeviceIndexToDeviceIndex(paApiIndex, i);
deviceInfo := Pa_GetDeviceInfo(deviceIndex);
channelCnt := deviceInfo^.maxInputChannels;
// current device is no input device -> skip
if (channelCnt <= 0) then
continue;
// portaudio returns a channel-count of 128 for some devices
// (e.g. the "default"-device), so we have to detect those
// fantasy channel counts.
if (channelCnt > 8) then
channelCnt := 2;
paDevice := TPortaudioInputDevice.Create();
AudioInputProcessor.Device[SC] := paDevice;
// retrieve device-name
deviceName := deviceInfo^.name;
paDevice.Description := deviceName;
paDevice.PaDeviceIndex := deviceIndex;
if (deviceInfo^.defaultSampleRate > 0) then
sampleRate := Trunc(deviceInfo^.defaultSampleRate)
else
sampleRate := 44100;
// on vista and xp the defaultLowInputLatency may be set to 0 but it works.
// TODO: correct too low latencies (what is a too low latency, maybe < 10ms?)
latency := deviceInfo^.defaultLowInputLatency;
// setup desired input parameters
with inputParams do
begin
device := deviceIndex;
channelCount := channelCnt;
sampleFormat := paInt16;
suggestedLatency := latency;
hostApiSpecificStreamInfo := nil;
end;
// check if device supports our input-format
// TODO: retry with input-latency set to 20ms (defaultLowInputLatency might
// not be set correctly in OSS)
err := Pa_IsFormatSupported(@inputParams, nil, sampleRate);
if(err <> 0) then
begin
// format not supported -> skip
errMsg := Pa_GetErrorText(err);
Log.LogError('Device error: "'+ deviceName +'" ('+ errMsg +')',
'TAudioInput_Portaudio.InitializeRecord');
paDevice.Free();
continue;
end;
// check if the device really works
err := Pa_OpenStream(stream, @inputParams, nil, sampleRate,
paFramesPerBufferUnspecified, paNoFlag, @MicrophoneTestCallback, nil);
if(err <> paNoError) then
begin
// unable to open device -> skip
errMsg := Pa_GetErrorText(err);
Log.LogError('Device error: "'+ deviceName +'" ('+ errMsg +')',
'TAudioInput_Portaudio.InitializeRecord');
paDevice.Free();
continue;
end;
// check if mic-callback works (might not be called on some devices)
// start the callback
Pa_StartStream(stream);
cbWorks := false;
// check if the callback was called (poll for max. 200ms)
for cbPolls := 1 to 20 do
begin
// if the test-callback was called it should be aborted now
if (Pa_IsStreamActive(stream) = 0) then
begin
cbWorks := true;
break;
end;
// not yet aborted, wait and try (poll) again
Pa_Sleep(10);
end;
// finally abort the stream (Note: Pa_StopStream might hang here)
Pa_AbortStream(stream);
// ignore device if callback did not work
if (not cbWorks) then
begin
Log.LogError('Device "'+paDevice.Description+'" does not respond',
'TAudioInput_Portaudio.InitializeRecord');
Pa_CloseStream(stream);
paDevice.Free();
continue;
end;
// adjust sample-rate (might be changed by portaudio)
streamInfo := Pa_GetStreamInfo(stream);
if (streamInfo <> nil) then
begin
if (sampleRate <> Trunc(streamInfo^.sampleRate)) then
begin
Log.LogStatus('Portaudio changed Samplerate from ' + IntToStr(sampleRate) +
' to ' + FloatToStr(streamInfo^.sampleRate),
'TAudioInput_Portaudio.InitializeRecord');
sampleRate := Trunc(streamInfo^.sampleRate);
end;
end;
// create audio-format info and resize capture-buffer array
paDevice.AudioFormat := TAudioFormatInfo.Create(
channelCnt,
sampleRate,
asfS16
);
SetLength(paDevice.CaptureChannel, paDevice.AudioFormat.Channels);
Log.LogStatus('InputDevice "'+paDevice.Description+'"@' +
IntToStr(paDevice.AudioFormat.Channels)+'x'+
IntToStr(paDevice.AudioFormat.SampleRate)+'Hz ('+
FloatTostr(inputParams.suggestedLatency)+'sec)' ,
'Portaudio.InitializeRecord');
{$IFDEF UsePortmixer}
// use default mixer
mixer := Px_OpenMixer(stream, 0);
// get input count
sourceCnt := Px_GetNumInputSources(mixer);
SetLength(paDevice.Source, sourceCnt);
// get input names
for sourceIndex := 0 to sourceCnt-1 do
begin
sourceName := Px_GetInputSourceName(mixer, sourceIndex);
paDevice.Source[sourceIndex].Name := sourceName;
end;
Px_CloseMixer(mixer);
{$ELSE} // not UsePortmixer
// create a standard input source
SetLength(paDevice.Source, 1);
paDevice.Source[0].Name := 'Standard';
{$ENDIF}
// close test-stream
Pa_CloseStream(stream);
// use default input source
paDevice.SourceSelected := 0;
Inc(SC);
end;
// adjust size to actual input-device count
SetLength(AudioInputProcessor.Device, SC);
Log.LogStatus('#Soundcards: ' + inttostr(SC), 'Portaudio');
result := true;
end;
destructor TAudioInput_Portaudio.Destroy;
var
i: integer;
paSoundCard: TPortaudioInputDevice;
begin
Pa_Terminate();
for i := 0 to High(AudioInputProcessor.Device) do
begin
AudioInputProcessor.Device[i].Free();
end;
AudioInputProcessor.Device := nil;
inherited Destroy;
end;
{*
* Portaudio input capture callback.
*}
function MicrophoneCallback(input: Pointer; output: Pointer; frameCount: Longword;
timeInfo: PPaStreamCallbackTimeInfo; statusFlags: TPaStreamCallbackFlags;
inputDevice: Pointer): Integer; cdecl;
begin
AudioInputProcessor.HandleMicrophoneData(input, frameCount*4, inputDevice);
result := paContinue;
end;
{*
* Portaudio test capture callback.
*}
function MicrophoneTestCallback(input: Pointer; output: Pointer; frameCount: Longword;
timeInfo: PPaStreamCallbackTimeInfo; statusFlags: TPaStreamCallbackFlags;
inputDevice: Pointer): Integer; cdecl;
begin
// this callback is called only once
result := paAbort;
end;
initialization
singleton_AudioInputPortaudio := TAudioInput_Portaudio.create();
AudioManager.add( singleton_AudioInputPortaudio );
finalization
AudioManager.Remove( singleton_AudioInputPortaudio );
end.