aboutsummaryrefslogtreecommitdiffstats
path: root/Game/Code/Classes/UAudioInput_Portaudio.pas
diff options
context:
space:
mode:
Diffstat (limited to 'Game/Code/Classes/UAudioInput_Portaudio.pas')
-rw-r--r--Game/Code/Classes/UAudioInput_Portaudio.pas420
1 files changed, 420 insertions, 0 deletions
diff --git a/Game/Code/Classes/UAudioInput_Portaudio.pas b/Game/Code/Classes/UAudioInput_Portaudio.pas
new file mode 100644
index 00000000..853fc35b
--- /dev/null
+++ b/Game/Code/Classes/UAudioInput_Portaudio.pas
@@ -0,0 +1,420 @@
+unit UAudioInput_Portaudio;
+
+interface
+
+{$IFDEF FPC}
+ {$MODE Delphi}
+{$ENDIF}
+
+{$I switches.inc}
+
+
+uses Classes,
+ SysUtils,
+ 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
+ TAudioInput_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_AudioInputPortaudio : 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 TAudioInput_Portaudio.GetName: String;
+begin
+ result := 'Portaudio';
+end;
+
+function TAudioInput_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 TAudioInput_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 TAudioInput_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 TAudioInput_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 TAudioInput_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 TAudioInput_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_AudioInputPortaudio := TAudioInput_Portaudio.create();
+ AudioManager.add( singleton_AudioInputPortaudio );
+
+finalization
+ AudioManager.Remove( singleton_AudioInputPortaudio );
+
+end.