diff options
Diffstat (limited to 'Game/Code/Classes/UAudioInput_Portaudio.pas')
-rw-r--r-- | Game/Code/Classes/UAudioInput_Portaudio.pas | 307 |
1 files changed, 177 insertions, 130 deletions
diff --git a/Game/Code/Classes/UAudioInput_Portaudio.pas b/Game/Code/Classes/UAudioInput_Portaudio.pas index 665f1972..74c8d98f 100644 --- a/Game/Code/Classes/UAudioInput_Portaudio.pas +++ b/Game/Code/Classes/UAudioInput_Portaudio.pas @@ -17,19 +17,18 @@ uses implementation uses - URecord, - UIni, - ULog, - UMain, {$IFDEF UsePortmixer} portmixer, {$ENDIF} - portaudio; + portaudio, + UAudioCore_Portaudio, + URecord, + UIni, + ULog, + UMain; type TAudioInput_Portaudio = class(TAudioInputBase) - private - function GetPreferredApiIndex(): TPaHostApiIndex; public function GetName: String; override; function InitializeRecord: boolean; override; @@ -41,7 +40,7 @@ type RecordStream: PPaStream; PaDeviceIndex: TPaDeviceIndex; - procedure Start(); override; + function Start(): boolean; override; procedure Stop(); override; end; @@ -49,88 +48,75 @@ function MicrophoneCallback(input: Pointer; output: Pointer; frameCount: Longwor 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; -{* 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; -const - 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(); +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 + with inputParams do + begin device := PaDeviceIndex; - channelCount := 2; + channelCount := AudioFormat.Channels; sampleFormat := paInt16; suggestedLatency := deviceInfo^.defaultLowInputLatency; hostApiSpecificStreamInfo := nil; end; - Log.LogStatus(inttostr(PaDeviceIndex), 'Portaudio'); - Log.LogStatus(floattostr(deviceInfo^.defaultLowInputLatency), 'Portaudio'); + //Log.LogStatus(deviceInfo^.name, 'Portaudio'); + //Log.LogStatus(floattostr(deviceInfo^.defaultLowInputLatency), 'Portaudio'); // open input stream - Error := Pa_OpenStream(RecordStream, @inputParams, nil, SampleRate, + Error := Pa_OpenStream(RecordStream, @inputParams, nil, + AudioFormat.SampleRate, paFramesPerBufferUnspecified, paNoFlag, @MicrophoneCallback, Pointer(Self)); - if(Error <> paNoError) then begin + if(Error <> paNoError) then + begin ErrorMsg := Pa_GetErrorText(Error); - Log.CriticalError('TPortaudioInputDevice.Start(): Error opening stream: ' + ErrorMsg); - //Halt; + Log.LogError('Error opening stream: ' + ErrorMsg, 'TPortaudioInputDevice.Start'); + Exit; end; // start capture Error := Pa_StartStream(RecordStream); - if(Error <> paNoError) then begin + if(Error <> paNoError) then + begin Pa_CloseStream(RecordStream); ErrorMsg := Pa_GetErrorText(Error); - Log.CriticalError('TPortaudioInputDevice.Start(): Error starting stream: ' + ErrorMsg); - //Halt; + Log.LogError('Error starting stream: ' + ErrorMsg, 'TPortaudioInputDevice.Start'); + Exit; end; + + Result := true; end; procedure TPortaudioInputDevice.Stop(); begin - if assigned(RecordStream) then begin - Pa_StopStream(RecordStream); + 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; @@ -143,151 +129,204 @@ 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; + paApiIndex: TPaHostApiIndex; + paApiInfo: PPaHostApiInfo; deviceName: string; deviceIndex: TPaDeviceIndex; deviceInfo: PPaDeviceInfo; sourceCnt: integer; + sourceIndex: integer; sourceName: string; + channelCnt: integer; SC: integer; // soundcard - SCI: integer; // soundcard source err: TPaError; errMsg: string; paDevice: TPortaudioInputDevice; inputParams: TPaStreamParameters; stream: PPaStream; + streamInfo: PPaStreamInfo; + sampleRate: integer; {$IFDEF UsePortmixer} mixer: PPxMixer; {$ENDIF} -const - captureFreq = 44100; + cbPolls: integer; + cbWorks: boolean; begin result := false; + // initialize portaudio err := Pa_Initialize(); - if(err <> paNoError) then begin - Log.LogError('Portaudio.InitializeRecord: ' + Pa_GetErrorText(err)); + if(err <> paNoError) then + begin + Log.LogError(Pa_GetErrorText(err), 'TAudioInput_Portaudio.InitializeRecord'); Exit; end; - apiIndex := GetPreferredApiIndex(); - apiInfo := Pa_GetHostApiInfo(apiIndex); + + // 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, apiInfo^.deviceCount); + 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(apiIndex, i); + deviceIndex := Pa_HostApiDeviceIndexToDeviceIndex(paApiIndex, i); deviceInfo := Pa_GetDeviceInfo(deviceIndex); + channelCnt := deviceInfo^.maxInputChannels; + // current device is no input device -> skip - if(deviceInfo^.maxInputChannels <= 0) then + 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; + if (deviceInfo^.defaultSampleRate > 0) then + sampleRate := Trunc(deviceInfo^.defaultSampleRate) + else + sampleRate := 44100; + // setup desired input parameters - with inputParams do begin + with inputParams do + begin device := deviceIndex; - channelCount := 2; + channelCount := channelCnt; 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 + // TODO: retry with input-latency set to 20ms (defaultLowInputLatency might + // not be set correctly in OSS) + 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 +')'); + Log.LogError('Device error: "'+ deviceName +'" ('+ errMsg +')', + 'TAudioInput_Portaudio.InitializeRecord'); 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 + // check if the device really works + 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('Portaudio.InitializeRecord, device: "'+ deviceName +'" ' - + '('+ errMsg +')'); + Log.LogError('Device error: "'+ deviceName +'" ('+ errMsg +')', + 'TAudioInput_Portaudio.InitializeRecord'); paDevice.Free(); continue; end; - {$IFDEF UsePortmixer} - - // use default mixer - mixer := Px_OpenMixer(stream, 0); + // check if mic-callback works (might not be called on some devices) - // get input count - sourceCnt := Px_GetNumInputSources(mixer); - SetLength(paDevice.Source, sourceCnt); - - // get input names - for SCI := 0 to sourceCnt-1 do + // start the callback + Pa_StartStream(stream); + + cbWorks := false; + // check if the callback was called (poll for max. 200ms) + for cbPolls := 1 to 20 do begin - sourceName := Px_GetInputSourceName(mixer, SCI); - paDevice.Source[SCI].Name := sourceName; + // if the test-callback was called it should be aborted now + if (Pa_IsStreamActive(stream) = 0) then + begin + cbWorks := true; + break; + end; + // not yet aborted, wait and try (poll) again + Pa_Sleep(10); end; - Px_CloseMixer(mixer); - - {$ELSE} // !UsePortmixer + // finally abort the stream (Note: Pa_StopStream might hang here) + Pa_AbortStream(stream); - //Pa_StartStream(stream); - // TODO: check if callback was called (this problem may occur on some devices) - //Pa_StopStream(stream); + // ignore device if callback did not work + if (not cbWorks) then + begin + Log.LogError('Device "'+paDevice.Description+'" does not respond', + 'TAudioInput_Portaudio.InitializeRecord'); + Pa_CloseStream(stream); + paDevice.Free(); + continue; + end; - // create a standard input source - SetLength(paDevice.Source, 1); - paDevice.Source[0].Name := 'Standard'; + // adjust sample-rate (might be changed by portaudio) + streamInfo := Pa_GetStreamInfo(stream); + if (streamInfo <> nil) then + begin + if (sampleRate <> Trunc(streamInfo^.sampleRate)) then + begin + Log.LogStatus('Portaudio changed Samplerate from ' + IntToStr(sampleRate) + + ' to ' + FloatToStr(streamInfo^.sampleRate), + 'TAudioInput_Portaudio.InitializeRecord'); + sampleRate := Trunc(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'+ + IntToStr(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 @@ -304,9 +343,6 @@ begin Log.LogStatus('#Soundcards: ' + inttostr(SC), 'Portaudio'); - { - SoundCard[SC].InputSelected := Mic[Device]; - } result := true; end; @@ -336,6 +372,17 @@ begin 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(); |