aboutsummaryrefslogblamecommitdiffstats
path: root/Game/Code/Classes/UAudioInput_Portaudio.pas
blob: 753c69f6d992e80ea80462bd0016df3e76880a0a (plain) (tree)
1
2
3
4
5
6
7
8
9







                           
                    

 



           



              








                       
    
                                                


                                                       


                                                   

      






                                                  








                                                                                  





























                                                                                  























































                                                                                           



























                                                                       
                                                           






                               

                       
                                    
                                           

                        
                                     




                                   

                      
     

                  

                                 
                                                                        

         





                                                

                                                              








                                                                   

                                               


                                   

                                          









                                                             

                                       
                                                
                                                                        




                                                                            
                      





                                                                               
                                          
 
                                                                        





                                                                            
                      









                                     

                                              

                      
                                  
         

                                                      









                                                                                  
                                     

                                          


            


                           
                               
                                 




                                             
                                            





                                                             
                 

    
                                         
   

                                     
     



                                                   
      
                                    
 
                    








                                                                                  
                                                                             


                       








                                                                  
unit UAudioInput_Portaudio;

interface

{$IFDEF FPC}
  {$MODE Delphi}
{$ENDIF}

{$I ../switches.inc}


uses
  Classes,
  SysUtils,
  UMusic;

implementation

uses
  URecord,
  UIni,
  ULog,
  UMain,
  {$IFDEF UsePortmixer}
  portmixer,
  {$ENDIF}
  portaudio;

type
  TAudioInput_Portaudio = class(TAudioInputBase)
    private
      function GetPreferredApiIndex(): TPaHostApiIndex;
    public
      function GetName: String; override;
      function InitializeRecord: boolean; override;
      destructor Destroy; override;
  end;

  TPortaudioInputDevice = class(TAudioInputDevice)
    public
      RecordStream:   PPaStream;
      PaDeviceIndex:  TPaDeviceIndex;

      procedure Start(); override;
      procedure Stop();  override;
  end;

function MicrophoneCallback(input: Pointer; output: Pointer; frameCount: Longword;
      timeInfo: PPaStreamCallbackTimeInfo; statusFlags: TPaStreamCallbackFlags;
      inputDevice: Pointer): Integer; cdecl; forward;

var
  singleton_AudioInputPortaudio : IAudioInput;

{* 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}


{ TPortaudioInputDevice }

procedure TPortaudioInputDevice.Start();
var
  Error:       TPaError;
  ErrorMsg:    string;
  inputParams: TPaStreamParameters;
  deviceInfo:  PPaDeviceInfo;
begin
  // get input latency info
  deviceInfo := Pa_GetDeviceInfo(PaDeviceIndex);

  // set input stream parameters
  with inputParams do begin
    device := PaDeviceIndex;
    channelCount := 2;
    sampleFormat := paInt16;
    suggestedLatency := deviceInfo^.defaultLowInputLatency;
    hostApiSpecificStreamInfo := nil;
  end;

  Log.LogStatus(inttostr(PaDeviceIndex), 'Portaudio');
  Log.LogStatus(floattostr(deviceInfo^.defaultLowInputLatency), 'Portaudio');

  // open input stream
  Error := Pa_OpenStream(RecordStream, @inputParams, nil, SampleRate,
      paFramesPerBufferUnspecified, paNoFlag,
      @MicrophoneCallback, Pointer(Self));
  if(Error <> paNoError) then begin
    ErrorMsg := Pa_GetErrorText(Error);
    Log.CriticalError('TPortaudioInputDevice.Start(): Error opening stream: ' + ErrorMsg);
    //Halt;
  end;

  // start capture
  Error := Pa_StartStream(RecordStream);
  if(Error <> paNoError) then begin
    Pa_CloseStream(RecordStream);
    ErrorMsg := Pa_GetErrorText(Error);
    Log.CriticalError('TPortaudioInputDevice.Start(): Error starting stream: ' + ErrorMsg);
    //Halt;
  end;
end;

procedure TPortaudioInputDevice.Stop();
begin
  if assigned(RecordStream) then begin
    Pa_StopStream(RecordStream);
    Pa_CloseStream(RecordStream);
  end;
end;


{ TAudioInput_Portaudio }

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;

function TAudioInput_Portaudio.InitializeRecord(): boolean;
var
  i:           integer;
  apiIndex:    TPaHostApiIndex;
  apiInfo:     PPaHostApiInfo;
  deviceName:  string;
  deviceIndex: TPaDeviceIndex;
  deviceInfo:  PPaDeviceInfo;
  sourceCnt:   integer;
  sourceName:  string;
  SC:          integer; // soundcard
  SCI:         integer; // soundcard source
  err:         TPaError;
  errMsg:      string;
  paDevice:    TPortaudioInputDevice;
  inputParams: TPaStreamParameters;
  stream:      PPaStream;
  {$IFDEF UsePortmixer}
  mixer:       PPxMixer;
  {$ENDIF}
const
  captureFreq = 44100;
begin
  result := false;

  err := Pa_Initialize();
  if(err <> paNoError) then begin
    Log.LogError('Portaudio.InitializeRecord: ' + Pa_GetErrorText(err));
    Exit;
  end;
  apiIndex := GetPreferredApiIndex();
  apiInfo := Pa_GetHostApiInfo(apiIndex);

  SC := 0;

  // init array-size to max. input-devices count
  SetLength(AudioInputProcessor.Device, apiInfo^.deviceCount);
  for i:= 0 to High(AudioInputProcessor.Device) 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;

    paDevice := TPortaudioInputDevice.Create();
    AudioInputProcessor.Device[SC] := paDevice;
    
    // retrieve device-name
    deviceName := deviceInfo^.name;
    paDevice.Description := deviceName;
    paDevice.PaDeviceIndex := deviceIndex;

    // setup desired input parameters
    with inputParams do begin
      device := deviceIndex;
      channelCount := 2;
      sampleFormat := paInt16;
      suggestedLatency := deviceInfo^.defaultLowInputLatency;
      hostApiSpecificStreamInfo := nil;
    end;

    paDevice.SampleRate := captureFreq;

    // check if device supports our input-format
    err := Pa_IsFormatSupported(@inputParams, nil, paDevice.SampleRate);
    if(err <> 0) then begin
      // format not supported -> skip
      errMsg := Pa_GetErrorText(err);
      Log.LogError('Portaudio.InitializeRecord, device: "'+ deviceName +'" '
                 + '('+ errMsg +')');
      paDevice.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)
    // use TPaDeviceInfo.defaultSampleRate

    err := Pa_OpenStream(stream, @inputParams, nil, paDevice.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 +')');
      paDevice.Free();
      continue;
    end;


    {$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 SCI := 0 to sourceCnt-1 do
    begin
      sourceName := Px_GetInputSourceName(mixer, SCI);
      paDevice.Source[SCI].Name := sourceName;
    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);

    // 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');

  {
    SoundCard[SC].InputSelected := Mic[Device];
  }
  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;


initialization
  singleton_AudioInputPortaudio := TAudioInput_Portaudio.create();
  AudioManager.add( singleton_AudioInputPortaudio );

finalization
  AudioManager.Remove( singleton_AudioInputPortaudio );

end.