unit UAudio_Portaudio;
interface
{$IFDEF FPC}
{$MODE Delphi}
{$ENDIF}
{$I switches.inc}
uses Classes,
{$IFDEF win32}
windows,
{$ENDIF}
Messages,
SysUtils,
{$IFNDEF FPC}
Forms,
{$ENDIF}
portaudio,
{$IFDEF UsePortmixer}
portmixer,
{$ENDIF}
ULog,
UMusic;
implementation
uses
{$IFDEF LAZARUS}
lclintf,
{$ENDIF}
URecord,
UIni,
UMain,
UCommon,
UThemes;
{
type
TPaHostApiIndex = PaHostApiIndex;
TPaDeviceIndex = PaDeviceIndex;
PPaStream = ^PaStreamPtr;
PPaStreamCallbackTimeInfo = ^PaStreamCallbackTimeInfo;
TPaStreamCallbackFlags = PaStreamCallbackFlags;
TPaHostApiTypeId = PaHostApiTypeId;
PPaHostApiInfo = ^PaHostApiInfo;
PPaDeviceInfo = ^PaDeviceInfo;
TPaError = PaError;
TPaStreamParameters = PaStreamParameters;
}
type
TAudio_Portaudio = class( TInterfacedObject, IAudioInput )
private
function GetPreferredApiIndex(): TPaHostApiIndex;
public
function GetName: String;
procedure InitializeRecord;
procedure CaptureStart;
procedure CaptureStop;
procedure CaptureCard(Card: byte; CaptureSoundLeft, CaptureSoundRight: TSound);
procedure StopCard(Card: byte);
end;
TPortaudioSoundCard = class(TGenericSoundCard)
RecordStream: PPaStream;
DeviceIndex: TPaDeviceIndex;
end;
function MicrophoneCallback(input: Pointer; output: Pointer; frameCount: Longword;
timeInfo: PPaStreamCallbackTimeInfo; statusFlags: TPaStreamCallbackFlags;
inputDevice: Pointer): Integer; cdecl; forward;
var
singleton_MusicPortaudio : IAudioInput;
const
sampleRate: Double = 44100.;
{* the default API used by Portaudio is the least common denominator
* and might lack efficiency. ApiPreferenceOrder defines the order of
* preferred APIs to use. The first API-type in the list is tried first. If it's
* not available the next is tried, ...
* If none of the preferred APIs was found the default API is used.
* Pascal doesn't permit zero-length static arrays, so you can use paDefaultApi
* as an array's only member if you do not have any preferences.
* paDefaultApi also terminates a preferences list but this is optional.
*}
const
paDefaultApi = -1;
var
ApiPreferenceOrder:
{$IF Defined(WIN32)}
// Note1: Portmixer has no mixer support for paASIO and paWASAPI at the moment
// Note2: Windows Default-API is MME
//array[0..0] of TPaHostApiTypeId = ( paDirectSound, paMME );
array[0..0] of TPaHostApiTypeId = ( paDirectSound );
{$ELSEIF Defined(LINUX)}
// Note1: Portmixer has no mixer support for paJACK at the moment
// Note2: Not tested, but ALSA might be better than OSS.
array[0..1] of TPaHostApiTypeId = ( paALSA, paOSS );
{$ELSEIF Defined(DARWIN)}
// Note: Not tested.
//array[0..0] of TPaHostApiTypeId = ( paCoreAudio );
array[0..0] of TPaHostApiTypeId = ( paDefaultApi );
{$ELSE}
array[0..0] of TPaHostApiTypeId = ( paDefaultApi );
{$IFEND}
function TAudio_Portaudio.GetName: String;
begin
result := 'Portaudio';
end;
function TAudio_Portaudio.GetPreferredApiIndex(): TPaHostApiIndex;
var
i: integer;
begin
result := -1;
// select preferred sound-API
for i:= 0 to High(ApiPreferenceOrder) do
begin
if(ApiPreferenceOrder[i] <> paDefaultApi) then begin
// check if API is available
result := Pa_HostApiTypeIdToHostApiIndex(ApiPreferenceOrder[i]);
if(result >= 0) then
break;
end;
end;
// None of the preferred APIs is available -> use default
if(result < 0) then begin
result := Pa_GetDefaultHostApi();
end;
end;
// TODO: should be a function with boolean return type
procedure TAudio_Portaudio.InitializeRecord;
var
i: integer;
apiIndex: TPaHostApiIndex;
apiInfo: PPaHostApiInfo;
deviceName: string;
deviceIndex: TPaDeviceIndex;
deviceInfo: PPaDeviceInfo;
inputCnt: integer;
inputName: string;
SC: integer; // soundcard
SCI: integer; // soundcard input
err: TPaError;
errMsg: string;
paSoundCard: TPortaudioSoundCard;
inputParams: TPaStreamParameters;
stream: PPaStream;
{$IFDEF UsePortmixer}
mixer: PPxMixer;
{$ENDIF}
begin
// TODO: call Pa_Terminate() on termination
err := Pa_Initialize();
if(err <> paNoError) then begin
Log.CriticalError('Portaudio.InitializeRecord: ' + Pa_GetErrorText(err));
//Log.LogError('Portaudio.InitializeRecord: ' + Pa_GetErrorText(err));
// result := false;
Exit;
end;
apiIndex := GetPreferredApiIndex();
apiInfo := Pa_GetHostApiInfo(apiIndex);
SC := 0;
// init array-size to max. input-devices count
SetLength(Recording.SoundCard, apiInfo^.deviceCount); // fix deviceCountL
for i:= 0 to High(Recording.SoundCard) do
begin
// convert API-specific device-index to global index
deviceIndex := Pa_HostApiDeviceIndexToDeviceIndex(apiIndex, i);
deviceInfo := Pa_GetDeviceInfo(deviceIndex);
// current device is no input device -> skip
if(deviceInfo^.maxInputChannels <= 0) then
continue;
// TODO: free object on termination
paSoundCard := TPortaudioSoundCard.Create();
Recording.SoundCard[SC] := paSoundCard;
// retrieve device-name
deviceName := deviceInfo^.name;
paSoundCard.Description := deviceName;
paSoundCard.DeviceIndex := deviceIndex;
// setup desired input parameters
with inputParams do begin
device := deviceIndex;
channelCount := 2;
sampleFormat := paInt16;
suggestedLatency := deviceInfo^.defaultLowInputLatency;
hostApiSpecificStreamInfo := nil;
end;
// check if device supports our input-format
err := Pa_IsFormatSupported(@inputParams, nil, sampleRate);
if(err <> 0) then begin
// format not supported -> skip
errMsg := Pa_GetErrorText(err);
Log.LogError('Portaudio.InitializeRecord, device: "'+ deviceName +'" '
+ '('+ errMsg +')');
paSoundCard.Free();
continue;
end;
// TODO: retry with mono if stereo is not supported
// TODO: retry with input-latency set to 20ms (defaultLowInputLatency might
// not be set correctly in OSS)
err := Pa_OpenStream(stream, @inputParams, nil, sampleRate,
paFramesPerBufferUnspecified, paNoFlag, @MicrophoneCallback, nil);
if(err <> paNoError) then begin
// unable to open device -> skip
errMsg := Pa_GetErrorText(err);
Log.LogError('Portaudio.InitializeRecord, device: "'+ deviceName +'" '
+ '('+ errMsg +')');
paSoundCard.Free();
continue;
end;
{$IFDEF UsePortmixer}
// use default mixer
mixer := Px_OpenMixer(stream, 0);
// get input count
inputCnt := Px_GetNumInputSources(mixer);
SetLength(paSoundCard.Input, inputCnt);
// get input names
for SCI := 0 to inputCnt-1 do
begin
inputName := Px_GetInputSourceName(mixer, SCI);
paSoundCard.Input[SCI].Name := inputName;
end;
Px_CloseMixer(mixer);
{$ELSE} // !UsePortmixer
//Pa_StartStream(stream);
// TODO: check if callback was called (this problem may occur on some devices)
//Pa_StopStream(stream);
Pa_CloseStream(stream);
// create a standard input source
SetLength(paSoundCard.Input, 1);
paSoundCard.Input[0].Name := 'Standard';
{$ENDIF}
// use default input source
paSoundCard.InputSelected := 0;
Inc(SC);
end;
// adjust size to actual input-device count
SetLength(Recording.SoundCard, SC);
Log.LogStatus('#Soundcards: ' + inttostr(SC), 'Portaudio');
{
SoundCard[SC].InputSelected := Mic[Device];
}
end;
// TODO: code is used by all IAudioInput implementors
// -> move to a common superclass (TAudioInput_Generic?)
procedure TAudio_Portaudio.CaptureStart;
var
S: integer;
SC: integer;
PlayerLeft, PlayerRight: integer;
CaptureSoundLeft, CaptureSoundRight: TSound;
begin
for S := 0 to High(Recording.Sound) do
Recording.Sound[S].BufferLong[0].Clear;
for SC := 0 to High(Ini.CardList) do begin
PlayerLeft := Ini.CardList[SC].ChannelL-1;
PlayerRight := Ini.CardList[SC].ChannelR-1;
if PlayerLeft >= PlayersPlay then PlayerLeft := -1;
if PlayerRight >= PlayersPlay then PlayerRight := -1;
if (PlayerLeft > -1) or (PlayerRight > -1) then begin
if (PlayerLeft > -1) then
CaptureSoundLeft := Recording.Sound[PlayerLeft]
else
CaptureSoundLeft := nil;
if (PlayerRight > -1) then
CaptureSoundRight := Recording.Sound[PlayerRight]
else
CaptureSoundRight := nil;
CaptureCard(SC, CaptureSoundLeft, CaptureSoundRight);
end;
end;
end;
// TODO: code is used by all IAudioInput implementors
// -> move to a common superclass (TAudioInput_Generic?)
procedure TAudio_Portaudio.CaptureStop;
var
SC: integer;
PlayerLeft: integer;
PlayerRight: integer;
begin
for SC := 0 to High(Ini.CardList) do begin
PlayerLeft := Ini.CardList[SC].ChannelL-1;
PlayerRight := Ini.CardList[SC].ChannelR-1;
if PlayerLeft >= PlayersPlay then PlayerLeft := -1;
if PlayerRight >= PlayersPlay then PlayerRight := -1;
if (PlayerLeft > -1) or (PlayerRight > -1) then
StopCard(SC);
end;
end;
{*
* Portaudio input capture callback.
*}
function MicrophoneCallback(input: Pointer; output: Pointer; frameCount: Longword;
timeInfo: PPaStreamCallbackTimeInfo; statusFlags: TPaStreamCallbackFlags;
inputDevice: Pointer): Integer; cdecl;
begin
Recording.HandleMicrophoneData(input, frameCount*4, inputDevice);
result := paContinue;
end;
{*
* Start input-capturing on Soundcard specified by Card.
* Params:
* Card - soundcard index in Recording.SoundCard array
* CaptureSoundLeft - sound(-buffer) used for left channel capture data
* CaptureSoundRight - sound(-buffer) used for right channel capture data
*}
procedure TAudio_Portaudio.CaptureCard(Card: byte; CaptureSoundLeft, CaptureSoundRight: TSound);
var
Error: TPaError;
ErrorMsg: string;
inputParams: TPaStreamParameters;
deviceInfo: PPaDeviceInfo;
stream: PPaStream;
paSoundCard: TPortaudioSoundCard;
begin
paSoundCard := TPortaudioSoundCard(Recording.SoundCard[Card]);
paSoundCard.CaptureSoundLeft := CaptureSoundLeft;
paSoundCard.CaptureSoundRight := CaptureSoundRight;
// get input latency info
deviceInfo := Pa_GetDeviceInfo(paSoundCard.DeviceIndex);
// set input stream parameters
with inputParams do begin
device := paSoundCard.DeviceIndex;
channelCount := 2;
sampleFormat := paInt16;
suggestedLatency := deviceInfo^.defaultLowInputLatency;
hostApiSpecificStreamInfo := nil;
end;
Log.LogStatus(inttostr(paSoundCard.DeviceIndex), 'Portaudio');
Log.LogStatus(floattostr(deviceInfo^.defaultLowInputLatency), 'Portaudio');
// open input stream
Error := Pa_OpenStream(stream, @inputParams, nil, sampleRate,
paFramesPerBufferUnspecified, paNoFlag,
@MicrophoneCallback, Pointer(paSoundCard));
if(Error <> paNoError) then begin
ErrorMsg := Pa_GetErrorText(Error);
Log.CriticalError('TAudio_Portaudio.CaptureCard('+ IntToStr(Card) +'): Error opening stream: ' + ErrorMsg);
//Halt;
end;
paSoundCard.RecordStream := stream;
// start capture
Error := Pa_StartStream(stream);
if(Error <> paNoError) then begin
Pa_CloseStream(stream);
ErrorMsg := Pa_GetErrorText(Error);
Log.CriticalError('TAudio_Portaudio.CaptureCard('+ IntToStr(Card) +'): Error starting stream: ' + ErrorMsg);
//Halt;
end;
end;
{*
* Stop input-capturing on Soundcard specified by Card.
* Params:
* Card - soundcard index in Recording.SoundCard array
*}
procedure TAudio_Portaudio.StopCard(Card: byte);
var
stream: PPaStream;
paSoundCard: TPortaudioSoundCard;
begin
paSoundCard := TPortaudioSoundCard(Recording.SoundCard[Card]);
stream := paSoundCard.RecordStream;
if(stream <> nil) then begin
Pa_StopStream(stream);
Pa_CloseStream(stream);
end;
end;
initialization
singleton_MusicPortaudio := TAudio_Portaudio.create();
AudioManager.add( singleton_MusicPortaudio );
finalization
AudioManager.Remove( singleton_MusicPortaudio );
end.