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
258
|
unit UAudioCore_Portaudio;
interface
{$IFDEF FPC}
{$MODE Delphi}
{$ENDIF}
{$I ../switches.inc}
uses
Classes,
SysUtils,
portaudio;
type
TAudioCore_Portaudio = class
private
constructor Create();
public
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.
|