{* UltraStar Deluxe - Karaoke Game
*
* UltraStar Deluxe is the legal property of its developers, whose names
* are too numerous to list here. Please refer to the COPYRIGHT
* file distributed with this source distribution.
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU General Public License
* as published by the Free Software Foundation; either version 2
* of the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; see the file COPYING. If not, write to
* the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor,
* Boston, MA 02110-1301, USA.
*
* $URL$
* $Id$
*}
unit UAudioPlayback_Portaudio;
interface
{$IFDEF FPC}
{$MODE Delphi}
{$ENDIF}
{$I switches.inc}
uses
Classes,
SysUtils,
UMusic;
implementation
uses
portaudio,
UAudioCore_Portaudio,
UAudioPlayback_SoftMixer,
ULog,
UIni,
UMain;
type
TAudioPlayback_Portaudio = class(TAudioPlayback_SoftMixer)
private
paStream: PPaStream;
AudioCore: TAudioCore_Portaudio;
Latency: double;
function OpenDevice(deviceIndex: TPaDeviceIndex): boolean;
function EnumDevices(): boolean;
protected
function InitializeAudioPlaybackEngine(): boolean; override;
function StartAudioPlaybackEngine(): boolean; override;
procedure StopAudioPlaybackEngine(); override;
function FinalizeAudioPlaybackEngine(): boolean; override;
function GetLatency(): double; override;
public
function GetName: String; override;
end;
TPortaudioOutputDevice = class(TAudioOutputDevice)
private
PaDeviceIndex: TPaDeviceIndex;
end;
{ TAudioPlayback_Portaudio }
function PortaudioAudioCallback(input: Pointer; output: Pointer; frameCount: Longword;
timeInfo: PPaStreamCallbackTimeInfo; statusFlags: TPaStreamCallbackFlags;
userData: Pointer): Integer; cdecl;
var
Engine: TAudioPlayback_Portaudio;
begin
Engine := TAudioPlayback_Portaudio(userData);
// update latency
Engine.Latency := timeInfo.outputBufferDacTime - timeInfo.currentTime;
// call superclass callback
Engine.AudioCallback(output, frameCount * Engine.FormatInfo.FrameSize);
Result := paContinue;
end;
function TAudioPlayback_Portaudio.GetName: String;
begin
Result := 'Portaudio_Playback';
end;
function TAudioPlayback_Portaudio.OpenDevice(deviceIndex: TPaDeviceIndex): boolean;
var
DeviceInfo : PPaDeviceInfo;
SampleRate : double;
OutParams : TPaStreamParameters;
StreamInfo : PPaStreamInfo;
err : TPaError;
begin
Result := false;
DeviceInfo := Pa_GetDeviceInfo(deviceIndex);
Log.LogInfo('Audio-Output Device: ' + DeviceInfo^.name, 'TAudioPlayback_Portaudio.OpenDevice');
SampleRate := DeviceInfo^.defaultSampleRate;
with OutParams do
begin
device := deviceIndex;
channelCount := 2;
sampleFormat := paInt16;
suggestedLatency := DeviceInfo^.defaultLowOutputLatency;
hostApiSpecificStreamInfo := nil;
end;
// check souncard and adjust sample-rate
if not AudioCore.TestDevice(nil, @OutParams, SampleRate) then
begin
Log.LogStatus('TestDevice failed!', 'TAudioPlayback_Portaudio.OpenDevice');
Exit;
end;
// open output stream
err := Pa_OpenStream(paStream, nil, @OutParams, SampleRate,
paFramesPerBufferUnspecified,
paNoFlag, @PortaudioAudioCallback, Self);
if(err <> paNoError) then
begin
Log.LogStatus(Pa_GetErrorText(err), 'TAudioPlayback_Portaudio.OpenDevice');
paStream := nil;
Exit;
end;
// get estimated latency (will be updated with real latency in the callback)
StreamInfo := Pa_GetStreamInfo(paStream);
if (StreamInfo <> nil) then
Latency := StreamInfo^.outputLatency
else
Latency := 0;
FormatInfo := TAudioFormatInfo.Create(
OutParams.channelCount,
SampleRate,
asfS16 // FIXME: is paInt16 system-dependant or -independant?
);
Result := true;
end;
function TAudioPlayback_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: TPortaudioOutputDevice;
outputParams: TPaStreamParameters;
stream: PPaStream;
streamInfo: PPaStreamInfo;
sampleRate: double;
latency: TPaTime;
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', 'TAudioPlayback_Portaudio.EnumDevices');
Exit;
end;
paApiInfo := Pa_GetHostApiInfo(paApiIndex);
SC := 0;
// init array-size to max. output-devices count
SetLength(OutputDeviceList, paApiInfo^.deviceCount);
for i:= 0 to High(OutputDeviceList) do
begin
// convert API-specific device-index to global index
deviceIndex := Pa_HostApiDeviceIndexToDeviceIndex(paApiIndex, i);
deviceInfo := Pa_GetDeviceInfo(deviceIndex);
channelCnt := deviceInfo^.maxOutputChannels;
// current device is no output 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 := TPortaudioOutputDevice.Create();
OutputDeviceList[SC] := paDevice;
// retrieve device-name
deviceName := deviceInfo^.name;
paDevice.Name := deviceName;
paDevice.PaDeviceIndex := deviceIndex;
if (deviceInfo^.defaultSampleRate > 0) then
sampleRate := deviceInfo^.defaultSampleRate
else
sampleRate := 44100;
// 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 output parameters
// TODO: retry with input-latency set to 20ms (defaultLowOutputLatency might
// not be set correctly in OSS)
with outputParams do
begin
device := deviceIndex;
channelCount := channelCnt;
sampleFormat := paInt16;
suggestedLatency := latency;
hostApiSpecificStreamInfo := nil;
end;
// check if mic-callback works (might not be called on some devices)
if (not TAudioCore_Portaudio.TestDevice(nil, @outputParams, sampleRate)) then
begin
// ignore device if callback did not work
Log.LogError('Device "'+paDevice.Name+'" does not respond',
'TAudioPlayback_Portaudio.InitializeRecord');
paDevice.Free();
continue;
end;
// open device for further info
err := Pa_OpenStream(stream, nil, @outputParams, 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 +')',
'TAudioPlayback_Portaudio.InitializeRecord');
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('OutputDevice "'+paDevice.Name+'"@' +
IntToStr(paDevice.AudioFormat.Channels)+'x'+
FloatToStr(paDevice.AudioFormat.SampleRate)+'Hz ('+
FloatTostr(outputParams.suggestedLatency)+'sec)' ,
'TAudioInput_Portaudio.InitializeRecord');
// close test-stream
Pa_CloseStream(stream);
Inc(SC);
end;
// adjust size to actual input-device count
SetLength(OutputDeviceList, SC);
Log.LogStatus('#Output-Devices: ' + inttostr(SC), 'Portaudio');
*)
Result := true;
end;
function TAudioPlayback_Portaudio.InitializeAudioPlaybackEngine(): boolean;
var
paApiIndex : TPaHostApiIndex;
paApiInfo : PPaHostApiInfo;
paOutDevice : TPaDeviceIndex;
begin
Result := false;
AudioCore := TAudioCore_Portaudio.GetInstance();
// initialize portaudio
if (not AudioCore.Initialize()) then
Exit;
paApiIndex := AudioCore.GetPreferredApiIndex();
if (paApiIndex = -1) then
begin
Log.LogError('No working Audio-API found', 'TAudioPlayback_Portaudio.InitializeAudioPlaybackEngine');
Exit;
end;
EnumDevices();
paApiInfo := Pa_GetHostApiInfo(paApiIndex);
Log.LogInfo('Audio-Output API-Type: ' + paApiInfo^.name, 'TAudioPlayback_Portaudio.OpenDevice');
paOutDevice := paApiInfo^.defaultOutputDevice;
if (not OpenDevice(paOutDevice)) then
begin
Exit;
end;
Result := true;
end;
function TAudioPlayback_Portaudio.StartAudioPlaybackEngine(): boolean;
var
err: TPaError;
begin
Result := false;
if (paStream = nil) then
Exit;
err := Pa_StartStream(paStream);
if(err <> paNoError) then
begin
Log.LogStatus('Pa_StartStream: '+Pa_GetErrorText(err), 'UAudioPlayback_Portaudio');
Exit;
end;
Result := true;
end;
procedure TAudioPlayback_Portaudio.StopAudioPlaybackEngine();
begin
if (paStream <> nil) then
begin
Pa_CloseStream(paStream);
// wait until stream is closed, otherwise Terminate() might cause a segfault
while (Pa_IsStreamActive(paStream) = 1) do
;
paStream := nil;
end;
end;
function TAudioPlayback_Portaudio.FinalizeAudioPlaybackEngine(): boolean;
begin
StopAudioPlaybackEngine();
Result := AudioCore.Terminate();
end;
function TAudioPlayback_Portaudio.GetLatency(): double;
begin
Result := Latency;
end;
initialization
MediaManager.Add(TAudioPlayback_Portaudio.Create);
end.