aboutsummaryrefslogtreecommitdiffstats
path: root/Game/Code/Classes/UAudioCore_Portaudio.pas
blob: bcc8a00168abf9f006d5e4babd6e35e717ea762c (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
unit UAudioCore_Portaudio;

interface

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

{$I ../switches.inc}


uses
  Classes,
  SysUtils,
  portaudio;

type
  TAudioCore_Portaudio = class
    public
      constructor Create();
      class function GetInstance(): TAudioCore_Portaudio;
      function GetPreferredApiIndex(): TPaHostApiIndex;
      function TestDevice(inParams, outParams: PPaStreamParameters; var sampleRate: Double): boolean;
  end;

implementation

uses
  ULog;

{*
 * The default API used by Portaudio is the least common denominator
 * and might lack efficiency. In addition it might not even work.
 * We use an array named ApiPreferenceOrder with which we define the order of
 * preferred APIs to use. The first API-type in the list is tried first.
 * If it is not available the next one is tried and so on ...
 * If none of the preferred APIs was found the default API (detected by
 * portaudio) is used.
 *
 * Pascal does not permit zero-length static arrays, so you must use paDefaultApi
 * as an array's only member if you do not have any preferences.
 * You can also append paDefaultApi to a non-zero length preferences array but
 * this is optional because the default API is always used as a fallback.
 *}
const
  paDefaultApi = -1;
const
  ApiPreferenceOrder:
{$IF Defined(MSWINDOWS)}
    // Note1: Portmixer has no mixer support for paASIO and paWASAPI at the moment
    // Note2: Windows Default-API is MME, but DirectSound is faster
    array[0..0] of TPaHostApiTypeId = ( paDirectSound );
{$ELSEIF Defined(LINUX)}
    // Note: Portmixer has no mixer support for JACK at the moment
    array[0..2] of TPaHostApiTypeId = ( paALSA, paJACK, paOSS );
{$ELSEIF Defined(DARWIN)}
    array[0..0] of TPaHostApiTypeId = ( paDefaultApi ); // paCoreAudio
{$ELSE}
    array[0..0] of TPaHostApiTypeId = ( paDefaultApi );
{$IFEND}


{ TAudioInput_Portaudio }

var
  Instance: TAudioCore_Portaudio;

constructor TAudioCore_Portaudio.Create();
begin
  inherited;
end;

class function TAudioCore_Portaudio.GetInstance(): TAudioCore_Portaudio;
begin
  if not assigned(Instance) then
    Instance := TAudioCore_Portaudio.Create();
  Result := Instance;
end;

function TAudioCore_Portaudio.GetPreferredApiIndex(): TPaHostApiIndex;
var
  i: integer;
  apiIndex: TPaHostApiIndex;
  apiInfo:  PPaHostApiInfo;
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
      apiIndex := Pa_HostApiTypeIdToHostApiIndex(ApiPreferenceOrder[i]);
      if(apiIndex >= 0) then
      begin
        // we found an API but we must check if it works
        // (on linux portaudio might detect OSS but does not provide
        // any devices if ALSA is enabled)
        apiInfo := Pa_GetHostApiInfo(apiIndex);
        if (apiInfo^.deviceCount > 0) then
        begin
          Result := apiIndex;
          break;
        end;
      end;
    end;
  end;

  // None of the preferred APIs is available -> use default
  if(result < 0) then
  begin
    result := Pa_GetDefaultHostApi();
  end;
end;

{*
 * Portaudio test callback used by TestDevice().
 *}
function TestCallback(input: Pointer; output: Pointer; frameCount: Longword;
      timeInfo: PPaStreamCallbackTimeInfo; statusFlags: TPaStreamCallbackFlags;
      inputDevice: Pointer): Integer; cdecl;
begin
  // this callback is called only once
  result := paAbort;
end;

(*
 * Tests if the callback works. Some devices can be opened without
 * an error but the callback is never called. Calling Pa_StopStream() on such
 * a stream freezes USDX then. Probably because the callback-thread is deadlocked
 * due to some bug in portaudio. The blocking Pa_ReadStream() and Pa_WriteStream()
 * block forever too and though can't be used for testing.
 *
 * To avoid freezing Pa_AbortStream (or Pa_CloseStream which calls Pa_AbortStream)
 * can be used to force the stream to stop. But for some reason this stops debugging
 * in gdb with a "no process found" message.
 *
 * Because freezing devices are non-working devices we test the devices here to
 * be able to exclude them from the device-selection list.
 *
 * Portaudio does not provide any test to check this error case (probably because
 * it should not even occur). So we have to open the device, start the stream and
 * check if the callback is called (the stream is stopped if the callback is called
 * for the first time, so we can poll until the stream is stopped).
 *
 * Another error that occurs is that some devices (even the default device) might
 * work at the beginning but stop after a few calls (maybe 50) of the callback.
 * For me this problem occurs with the default output-device. The "dmix" or "front"
 * device must be selected instead. Another problem is that (due to a bug in
 * portaudio or ALSA) the "front" device is not detected every time portaudio
 * is started. Sometimes it needs two or more restarts.
 *
 * There is no reasonable way to test for these errors. For the first error-case
 * we could test if the callback is called 50 times but this can take a second
 * for each device and it can fail in the 51st or even 100th callback call then.
 *
 * The second error-case cannot be tested at all. How should we now that one
 * device is missing if portaudio is not even able to detect it.
 * We could start and terminate Portaudio for several times and see if the device
 * count changes but this is ugly.
 *
 * Conclusion: We are not able to autodetect a working device with
 *   portaudio (at least not with the newest v19_20071207) at the moment.
 *   So we have to provide the possibility to manually select an output device
 *   in the UltraStar options if we want to use portaudio instead of SDL.
 *)
function TAudioCore_Portaudio.TestDevice(inParams, outParams: PPaStreamParameters; var sampleRate: Double): boolean;
var
  stream: PPaStream;
  err: TPaError;
  cbWorks: boolean;
  cbPolls: integer;
  i: integer;
const
  altSampleRates: array[0..1] of Double = (44100, 48000); // alternative sample-rates
begin
  Result := false;

  if (sampleRate <= 0) then
    sampleRate := 44100;

  // check if device supports our input-format
  err := Pa_IsFormatSupported(inParams, outParams, sampleRate);
  if(err <> paNoError) then
  begin
    // we cannot fix the error -> exit
    if (err <> paInvalidSampleRate) then
      Exit;

    // try alternative sample-rates to the detected one
    sampleRate := 0;
    for i := 0 to High(altSampleRates) do
    begin
      // do not check the detected sample-rate twice
      if (altSampleRates[i] = sampleRate) then
        continue;
      // check alternative
      err := Pa_IsFormatSupported(inParams, outParams, altSampleRates[i]);
      if (err = paNoError) then
      begin
        // sample-rate works
        sampleRate := altSampleRates[i];
        break;
      end;
    end;
    // no working sample-rate found
    if (sampleRate = 0) then
      Exit;
  end;

  // FIXME: for some reason gdb stops after a call of Pa_AbortStream()
  // which is implicitely called by Pa_CloseStream().
  // gdb's stops with the message: "ptrace: no process found".
  // Probably because the callback-thread is killed what confuses gdb.
  {$IF Defined(Debug) and Defined(Linux)}
  cbWorks := true;
  {$ELSE}
  // open device for testing
  err := Pa_OpenStream(stream, inParams, outParams, sampleRate,
          paFramesPerBufferUnspecified,
          paNoFlag, @TestCallback, nil);
  if(err <> paNoError) then
  begin
    exit;
  end;

  // start the callback
  err := Pa_StartStream(stream);
  if(err <> paNoError) then
  begin
    Pa_CloseStream(stream);
    exit;
  end;

  cbWorks := false;
  // check if the callback was called (poll for max. 200ms)
  for cbPolls := 1 to 20 do
  begin
    // 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;

  // finally abort the stream
  Pa_CloseStream(stream);
  {$IFEND}
  
  Result := cbWorks;
end;

end.