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) private AudioCore: TAudioCore_Portaudio; function EnumDevices(): boolean; public function GetName: String; override; function InitializeRecord: boolean; override; function FinalizeRecord: boolean; override; end; TPortaudioInputDevice = class(TAudioInputDevice) private RecordStream: PPaStream; {$IFDEF UsePortmixer} Mixer: PPxMixer; {$ENDIF} PaDeviceIndex: TPaDeviceIndex; public function Open(): boolean; function Close(): boolean; function Start(): boolean; override; function Stop(): boolean; override; function GetVolume(): integer; override; procedure SetVolume(Volume: integer); 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.Open(): boolean; var Error: TPaError; inputParams: TPaStreamParameters; deviceInfo: PPaDeviceInfo; SourceIndex: integer; 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 Log.LogError('Error opening stream: ' + Pa_GetErrorText(Error), 'TPortaudioInputDevice.Open'); Exit; end; {$IFDEF UsePortmixer} // open default mixer Mixer := Px_OpenMixer(RecordStream, 0); if (Mixer = nil) then begin Log.LogError('Error opening mixer: ' + Pa_GetErrorText(Error), 'TPortaudioInputDevice.Open'); end else begin // save current source selection and select new source SourceIndex := Ini.InputDeviceConfig[CfgIndex].Input-1; if (SourceIndex = -1) then begin // nothing to do if default source is used SourceRestore := -1; end else begin // store current source-index and select new source SourceRestore := Px_GetCurrentInputSource(Mixer); // -1 in error case Px_SetCurrentInputSource(Mixer, SourceIndex); end; end; {$ENDIF} Result := true; end; function TPortaudioInputDevice.Start(): boolean; var Error: TPaError; begin Result := false; // recording already started -> stop first if (RecordStream <> nil) then Stop(); // TODO: Do not open the device here (takes too much time). if (not Open()) then Exit; // start capture Error := Pa_StartStream(RecordStream); if(Error <> paNoError) then begin Log.LogError('Error starting stream: ' + Pa_GetErrorText(Error), 'TPortaudioInputDevice.Start'); Close(); RecordStream := nil; Exit; end; Result := true; end; function TPortaudioInputDevice.Stop(): boolean; var Error: TPaError; begin Result := false; if (RecordStream = nil) then Exit; // 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). Error := Pa_AbortStream(RecordStream); if (Error <> paNoError) then begin Log.LogError('Pa_AbortStream: ' + Pa_GetErrorText(Error), 'TPortaudioInputDevice.Stop'); end; Result := Close(); end; function TPortaudioInputDevice.Close(): boolean; var Error: TPaError; begin {$IFDEF UsePortmixer} if (Mixer <> nil) then begin // restore source selection if (SourceRestore >= 0) then begin Px_SetCurrentInputSource(Mixer, SourceRestore); end; // close mixer Px_CloseMixer(Mixer); Mixer := nil; end; {$ENDIF} Error := Pa_CloseStream(RecordStream); if (Error <> paNoError) then begin Log.LogError('Pa_CloseStream: ' + Pa_GetErrorText(Error), 'TPortaudioInputDevice.Close'); Result := false; end else begin Result := true; end; RecordStream := nil; end; function TPortaudioInputDevice.GetVolume(): integer; begin Result := 0; {$IFDEF UsePortmixer} if (Mixer <> nil) then begin Result := Round(Px_GetInputVolume(Mixer) * 100); // clip to valid range if (Result > 100) then Result := 100 else if (Result < 0) then Result := 0; end; {$ENDIF} end; procedure TPortaudioInputDevice.SetVolume(Volume: integer); begin {$IFDEF UsePortmixer} if (Mixer <> nil) then begin // clip to valid range if (Volume > 100) then Volume := 100 else if (Volume < 0) then Volume := 0; Px_SetInputVolume(Mixer, Volume / 100); end; {$ENDIF} end; { TAudioInput_Portaudio } function TAudioInput_Portaudio.GetName: String; begin result := 'Portaudio'; end; function TAudioInput_Portaudio.EnumDevices(): 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} mixer: PPxMixer; sourceCnt: integer; sourceIndex: integer; sourceName: string; {$ENDIF} cbPolls: integer; cbWorks: boolean; begin Result := false; // choose the best available Audio-API paApiIndex := AudioCore.GetPreferredApiIndex(); if(paApiIndex = -1) then begin Log.LogError('No working Audio-API found', 'TAudioInput_Portaudio.EnumDevices'); Exit; end; paApiInfo := Pa_GetHostApiInfo(paApiIndex); SC := 0; // init array-size to max. input-devices count SetLength(AudioInputProcessor.DeviceList, paApiInfo^.deviceCount); for i:= 0 to High(AudioInputProcessor.DeviceList) 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.DeviceList[SC] := paDevice; // retrieve device-name deviceName := deviceInfo^.name; paDevice.Name := 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 // TODO: retry with input-latency set to 20ms (defaultLowInputLatency might // not be set correctly in OSS) with inputParams do begin device := deviceIndex; channelCount := channelCnt; sampleFormat := paInt16; suggestedLatency := latency; hostApiSpecificStreamInfo := nil; end; // check souncard and adjust sample-rate if (not AudioCore.TestDevice(@inputParams, nil, sampleRate)) then begin // ignore device if it does not work Log.LogError('Device "'+paDevice.Name+'" 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.Name+'"@' + IntToStr(paDevice.AudioFormat.Channels)+'x'+ FloatToStr(paDevice.AudioFormat.SampleRate)+'Hz ('+ FloatTostr(inputParams.suggestedLatency)+'sec)' , 'Portaudio.EnumDevices'); // portaudio does not provide a source-type check paDevice.MicSource := -1; paDevice.SourceRestore := -1; // add a virtual default source (will not change mixer-settings) SetLength(paDevice.Source, 1); paDevice.Source[0].Name := DEFAULT_SOURCE_NAME; {$IFDEF UsePortmixer} // use default mixer mixer := Px_OpenMixer(stream, 0); // get input count sourceCnt := Px_GetNumInputSources(mixer); SetLength(paDevice.Source, sourceCnt+1); // get input names for sourceIndex := 1 to sourceCnt do begin sourceName := Px_GetInputSourceName(mixer, sourceIndex-1); paDevice.Source[sourceIndex].Name := sourceName; end; Px_CloseMixer(mixer); {$ENDIF} // close test-stream Pa_CloseStream(stream); Inc(SC); end; // adjust size to actual input-device count SetLength(AudioInputProcessor.DeviceList, SC); Log.LogStatus('#Input-Devices: ' + inttostr(SC), 'Portaudio'); Result := true; end; function TAudioInput_Portaudio.InitializeRecord(): boolean; var err: TPaError; begin AudioCore := TAudioCore_Portaudio.GetInstance(); // initialize portaudio err := Pa_Initialize(); if(err <> paNoError) then begin Log.LogError(Pa_GetErrorText(err), 'TAudioInput_Portaudio.InitializeRecord'); Result := false; Exit; end; Result := EnumDevices(); end; function TAudioInput_Portaudio.FinalizeRecord: boolean; begin CaptureStop; Pa_Terminate(); Result := inherited FinalizeRecord(); 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.