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; channelCnt: integer; SC: integer; // soundcard err: TPaError; errMsg: string; paDevice: TPortaudioInputDevice; inputParams: TPaStreamParameters; stream: PPaStream; streamInfo: PPaStreamInfo; sampleRate: double; latency: TPaTime; {$IFDEF UsePortmixer} sourceIndex: integer; mixer: PPxMixer; sourceCnt: integer; sourceName: string; {$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; sampleRate := deviceInfo^.defaultSampleRate; // 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 souncard and adjust sample-rate if (not TAudioCore_Portaudio.TestDevice(@inputParams, nil, sampleRate)) then begin // ignore device if it does not work Log.LogError('Device "'+paDevice.Description+'" does not work', 'TAudioInput_Portaudio.EnumDevices'); paDevice.Free(); continue; end; // open device for further info 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.EnumDevices'); paDevice.Free(); continue; end; // adjust sample-rate (might be changed by portaudio) streamInfo := Pa_GetStreamInfo(stream); if (streamInfo <> nil) then begin if (sampleRate <> streamInfo^.sampleRate) then begin Log.LogStatus('Portaudio changed Samplerate from ' + FloatToStr(sampleRate) + ' to ' + FloatToStr(streamInfo^.sampleRate), 'TAudioInput_Portaudio.InitializeRecord'); sampleRate := 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'+ FloatToStr(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.