aboutsummaryrefslogblamecommitdiffstats
path: root/Game/Code/Classes/UAudioInput_Portaudio.pas
blob: 8073a7f3c0658bc8a6cfda09f47572ee42efb83b (plain) (tree)








































































































































































                                                                                     

                                                                                     










                                                                   
                                                     
















































































                                                                                  
                                               
















                                                             

                                                     







                                                         
                                                                 


                                
                                                                   


































                                                                                  
                                                                             


















                                                                                                     
                                                                          

















































                                                                                                                
                                                                          















                                                                  
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(AudioInputProcessor.SoundCard, apiInfo^.deviceCount); // fix deviceCountL
  for i:= 0 to High(AudioInputProcessor.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();
    AudioInputProcessor.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(AudioInputProcessor.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(AudioInputProcessor.Sound) do
    AudioInputProcessor.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 := AudioInputProcessor.Sound[PlayerLeft]
      else
        CaptureSoundLeft := nil;
      if (PlayerRight > -1) then
        CaptureSoundRight := AudioInputProcessor.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
  AudioInputProcessor.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(AudioInputProcessor.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(AudioInputProcessor.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.