slouken@3099
|
1 |
/*
|
slouken@5535
|
2 |
Simple DirectMedia Layer
|
slouken@10737
|
3 |
Copyright (C) 1997-2017 Sam Lantinga <slouken@libsdl.org>
|
slouken@3099
|
4 |
|
slouken@5535
|
5 |
This software is provided 'as-is', without any express or implied
|
slouken@5535
|
6 |
warranty. In no event will the authors be held liable for any damages
|
slouken@5535
|
7 |
arising from the use of this software.
|
slouken@3099
|
8 |
|
slouken@5535
|
9 |
Permission is granted to anyone to use this software for any purpose,
|
slouken@5535
|
10 |
including commercial applications, and to alter it and redistribute it
|
slouken@5535
|
11 |
freely, subject to the following restrictions:
|
slouken@3099
|
12 |
|
slouken@5535
|
13 |
1. The origin of this software must not be misrepresented; you must not
|
slouken@5535
|
14 |
claim that you wrote the original software. If you use this software
|
slouken@5535
|
15 |
in a product, an acknowledgment in the product documentation would be
|
slouken@5535
|
16 |
appreciated but is not required.
|
slouken@5535
|
17 |
2. Altered source versions must be plainly marked as such, and must not be
|
slouken@5535
|
18 |
misrepresented as being the original software.
|
slouken@5535
|
19 |
3. This notice may not be removed or altered from any source distribution.
|
slouken@3099
|
20 |
*/
|
slouken@3099
|
21 |
|
icculus@9394
|
22 |
/*
|
icculus@9394
|
23 |
* !!! FIXME: streamline this a little by removing all the
|
icculus@9394
|
24 |
* !!! FIXME: if (capture) {} else {} sections that are identical
|
icculus@9394
|
25 |
* !!! FIXME: except for one flag.
|
icculus@9394
|
26 |
*/
|
icculus@9394
|
27 |
|
icculus@9394
|
28 |
/* !!! FIXME: can this target support hotplugging? */
|
icculus@9394
|
29 |
/* !!! FIXME: ...does SDL2 even support QNX? */
|
icculus@9394
|
30 |
|
icculus@8093
|
31 |
#include "../../SDL_internal.h"
|
slouken@3099
|
32 |
|
slouken@6044
|
33 |
#if SDL_AUDIO_DRIVER_QSA
|
slouken@6044
|
34 |
|
slouken@3099
|
35 |
#include <errno.h>
|
slouken@3099
|
36 |
#include <unistd.h>
|
slouken@3099
|
37 |
#include <fcntl.h>
|
slouken@3099
|
38 |
#include <signal.h>
|
slouken@3099
|
39 |
#include <sys/types.h>
|
slouken@3099
|
40 |
#include <sys/time.h>
|
slouken@3099
|
41 |
#include <sched.h>
|
slouken@3099
|
42 |
#include <sys/select.h>
|
slouken@3099
|
43 |
#include <sys/neutrino.h>
|
slouken@3099
|
44 |
#include <sys/asoundlib.h>
|
slouken@3099
|
45 |
|
slouken@3099
|
46 |
#include "SDL_timer.h"
|
slouken@3099
|
47 |
#include "SDL_audio.h"
|
slouken@3099
|
48 |
#include "../SDL_audio_c.h"
|
slouken@3099
|
49 |
#include "SDL_qsa_audio.h"
|
slouken@3099
|
50 |
|
slouken@3099
|
51 |
/* default channel communication parameters */
|
slouken@3099
|
52 |
#define DEFAULT_CPARAMS_RATE 44100
|
slouken@3099
|
53 |
#define DEFAULT_CPARAMS_VOICES 1
|
slouken@3099
|
54 |
|
slouken@3099
|
55 |
#define DEFAULT_CPARAMS_FRAG_SIZE 4096
|
slouken@3099
|
56 |
#define DEFAULT_CPARAMS_FRAGS_MIN 1
|
slouken@3099
|
57 |
#define DEFAULT_CPARAMS_FRAGS_MAX 1
|
slouken@3099
|
58 |
|
slouken@3099
|
59 |
#define QSA_NO_WORKAROUNDS 0x00000000
|
slouken@3099
|
60 |
#define QSA_MMAP_WORKAROUND 0x00000001
|
slouken@3099
|
61 |
|
slouken@3099
|
62 |
struct BuggyCards
|
slouken@3099
|
63 |
{
|
slouken@3139
|
64 |
char *cardname;
|
slouken@3099
|
65 |
unsigned long bugtype;
|
slouken@3099
|
66 |
};
|
slouken@3099
|
67 |
|
slouken@3099
|
68 |
#define QSA_WA_CARDS 3
|
slouken@3099
|
69 |
#define QSA_MAX_CARD_NAME_LENGTH 33
|
slouken@3099
|
70 |
|
slouken@3139
|
71 |
struct BuggyCards buggycards[QSA_WA_CARDS] = {
|
slouken@3139
|
72 |
{"Sound Blaster Live!", QSA_MMAP_WORKAROUND},
|
slouken@3139
|
73 |
{"Vortex 8820", QSA_MMAP_WORKAROUND},
|
slouken@3139
|
74 |
{"Vortex 8830", QSA_MMAP_WORKAROUND},
|
slouken@3099
|
75 |
};
|
slouken@3099
|
76 |
|
slouken@3099
|
77 |
/* List of found devices */
|
slouken@3099
|
78 |
#define QSA_MAX_DEVICES 32
|
slouken@3139
|
79 |
#define QSA_MAX_NAME_LENGTH 81+16 /* Hardcoded in QSA, can't be changed */
|
slouken@3099
|
80 |
|
slouken@3099
|
81 |
typedef struct _QSA_Device
|
slouken@3099
|
82 |
{
|
slouken@3139
|
83 |
char name[QSA_MAX_NAME_LENGTH]; /* Long audio device name for SDL */
|
slouken@3139
|
84 |
int cardno;
|
slouken@3139
|
85 |
int deviceno;
|
slouken@3099
|
86 |
} QSA_Device;
|
slouken@3099
|
87 |
|
slouken@3099
|
88 |
QSA_Device qsa_playback_device[QSA_MAX_DEVICES];
|
slouken@3139
|
89 |
uint32_t qsa_playback_devices;
|
slouken@3099
|
90 |
|
slouken@3099
|
91 |
QSA_Device qsa_capture_device[QSA_MAX_DEVICES];
|
slouken@3139
|
92 |
uint32_t qsa_capture_devices;
|
slouken@3099
|
93 |
|
slouken@7860
|
94 |
static SDL_INLINE int
|
slouken@3139
|
95 |
QSA_SetError(const char *fn, int status)
|
slouken@3099
|
96 |
{
|
icculus@7038
|
97 |
return SDL_SetError("QSA: %s() failed: %s", fn, snd_strerror(status));
|
slouken@3099
|
98 |
}
|
slouken@3099
|
99 |
|
slouken@3099
|
100 |
/* card names check to apply the workarounds */
|
slouken@3139
|
101 |
static int
|
slouken@3139
|
102 |
QSA_CheckBuggyCards(_THIS, unsigned long checkfor)
|
slouken@3099
|
103 |
{
|
slouken@3139
|
104 |
char scardname[QSA_MAX_CARD_NAME_LENGTH];
|
slouken@3139
|
105 |
int it;
|
slouken@3099
|
106 |
|
slouken@3139
|
107 |
if (snd_card_get_name
|
slouken@3139
|
108 |
(this->hidden->cardno, scardname, QSA_MAX_CARD_NAME_LENGTH - 1) < 0) {
|
slouken@3139
|
109 |
return 0;
|
slouken@3139
|
110 |
}
|
slouken@3099
|
111 |
|
slouken@3139
|
112 |
for (it = 0; it < QSA_WA_CARDS; it++) {
|
slouken@3139
|
113 |
if (SDL_strcmp(buggycards[it].cardname, scardname) == 0) {
|
slouken@3139
|
114 |
if (buggycards[it].bugtype == checkfor) {
|
slouken@3139
|
115 |
return 1;
|
slouken@3139
|
116 |
}
|
slouken@3139
|
117 |
}
|
slouken@3139
|
118 |
}
|
slouken@3139
|
119 |
|
slouken@3139
|
120 |
return 0;
|
slouken@3099
|
121 |
}
|
slouken@3099
|
122 |
|
icculus@5590
|
123 |
/* !!! FIXME: does this need to be here? Does the SDL version not work? */
|
slouken@3139
|
124 |
static void
|
slouken@3139
|
125 |
QSA_ThreadInit(_THIS)
|
slouken@3099
|
126 |
{
|
slouken@3139
|
127 |
struct sched_param param;
|
slouken@3139
|
128 |
int status;
|
slouken@3099
|
129 |
|
slouken@3139
|
130 |
/* Increase default 10 priority to 25 to avoid jerky sound */
|
slouken@3139
|
131 |
status = SchedGet(0, 0, ¶m);
|
slouken@3139
|
132 |
param.sched_priority = param.sched_curpriority + 15;
|
slouken@3139
|
133 |
status = SchedSet(0, 0, SCHED_NOCHANGE, ¶m);
|
slouken@3099
|
134 |
}
|
slouken@3099
|
135 |
|
slouken@3099
|
136 |
/* PCM channel parameters initialize function */
|
slouken@3139
|
137 |
static void
|
slouken@3139
|
138 |
QSA_InitAudioParams(snd_pcm_channel_params_t * cpars)
|
slouken@3099
|
139 |
{
|
icculus@10257
|
140 |
SDL_zerop(cpars);
|
slouken@3139
|
141 |
cpars->channel = SND_PCM_CHANNEL_PLAYBACK;
|
slouken@3139
|
142 |
cpars->mode = SND_PCM_MODE_BLOCK;
|
slouken@3139
|
143 |
cpars->start_mode = SND_PCM_START_DATA;
|
slouken@3139
|
144 |
cpars->stop_mode = SND_PCM_STOP_STOP;
|
slouken@3139
|
145 |
cpars->format.format = SND_PCM_SFMT_S16_LE;
|
slouken@3139
|
146 |
cpars->format.interleave = 1;
|
slouken@3139
|
147 |
cpars->format.rate = DEFAULT_CPARAMS_RATE;
|
slouken@3139
|
148 |
cpars->format.voices = DEFAULT_CPARAMS_VOICES;
|
slouken@3139
|
149 |
cpars->buf.block.frag_size = DEFAULT_CPARAMS_FRAG_SIZE;
|
slouken@3139
|
150 |
cpars->buf.block.frags_min = DEFAULT_CPARAMS_FRAGS_MIN;
|
slouken@3139
|
151 |
cpars->buf.block.frags_max = DEFAULT_CPARAMS_FRAGS_MAX;
|
slouken@3099
|
152 |
}
|
slouken@3099
|
153 |
|
slouken@3099
|
154 |
/* This function waits until it is possible to write a full sound buffer */
|
slouken@3139
|
155 |
static void
|
slouken@3139
|
156 |
QSA_WaitDevice(_THIS)
|
slouken@3099
|
157 |
{
|
slouken@3139
|
158 |
fd_set wfds;
|
slouken@3139
|
159 |
fd_set rfds;
|
slouken@3139
|
160 |
int selectret;
|
slouken@3139
|
161 |
struct timeval timeout;
|
slouken@3099
|
162 |
|
slouken@3139
|
163 |
if (!this->hidden->iscapture) {
|
slouken@3139
|
164 |
FD_ZERO(&wfds);
|
slouken@3139
|
165 |
FD_SET(this->hidden->audio_fd, &wfds);
|
slouken@3139
|
166 |
} else {
|
slouken@3139
|
167 |
FD_ZERO(&rfds);
|
slouken@3139
|
168 |
FD_SET(this->hidden->audio_fd, &rfds);
|
slouken@3139
|
169 |
}
|
slouken@3099
|
170 |
|
slouken@3139
|
171 |
do {
|
slouken@3139
|
172 |
/* Setup timeout for playing one fragment equal to 2 seconds */
|
slouken@3139
|
173 |
/* If timeout occured than something wrong with hardware or driver */
|
slouken@3139
|
174 |
/* For example, Vortex 8820 audio driver stucks on second DAC because */
|
slouken@3139
|
175 |
/* it doesn't exist ! */
|
slouken@3139
|
176 |
timeout.tv_sec = 2;
|
slouken@3139
|
177 |
timeout.tv_usec = 0;
|
slouken@3139
|
178 |
this->hidden->timeout_on_wait = 0;
|
slouken@3099
|
179 |
|
slouken@3139
|
180 |
if (!this->hidden->iscapture) {
|
slouken@3139
|
181 |
selectret =
|
slouken@3139
|
182 |
select(this->hidden->audio_fd + 1, NULL, &wfds, NULL,
|
slouken@3139
|
183 |
&timeout);
|
slouken@3139
|
184 |
} else {
|
slouken@3139
|
185 |
selectret =
|
slouken@3139
|
186 |
select(this->hidden->audio_fd + 1, &rfds, NULL, NULL,
|
slouken@3139
|
187 |
&timeout);
|
slouken@3139
|
188 |
}
|
slouken@3139
|
189 |
|
slouken@3139
|
190 |
switch (selectret) {
|
slouken@3139
|
191 |
case -1:
|
slouken@3139
|
192 |
{
|
icculus@7038
|
193 |
SDL_SetError("QSA: select() failed: %s", strerror(errno));
|
slouken@3139
|
194 |
return;
|
slouken@3139
|
195 |
}
|
slouken@3139
|
196 |
break;
|
slouken@3139
|
197 |
case 0:
|
slouken@3139
|
198 |
{
|
icculus@7038
|
199 |
SDL_SetError("QSA: timeout on buffer waiting occured");
|
slouken@3139
|
200 |
this->hidden->timeout_on_wait = 1;
|
slouken@3139
|
201 |
return;
|
slouken@3139
|
202 |
}
|
slouken@3139
|
203 |
break;
|
slouken@3139
|
204 |
default:
|
slouken@3139
|
205 |
{
|
slouken@3139
|
206 |
if (!this->hidden->iscapture) {
|
slouken@3139
|
207 |
if (FD_ISSET(this->hidden->audio_fd, &wfds)) {
|
slouken@3139
|
208 |
return;
|
slouken@3099
|
209 |
}
|
slouken@3139
|
210 |
} else {
|
slouken@3139
|
211 |
if (FD_ISSET(this->hidden->audio_fd, &rfds)) {
|
slouken@3139
|
212 |
return;
|
slouken@3099
|
213 |
}
|
slouken@3139
|
214 |
}
|
slouken@3139
|
215 |
}
|
slouken@3139
|
216 |
break;
|
slouken@3139
|
217 |
}
|
slouken@3139
|
218 |
} while (1);
|
slouken@3099
|
219 |
}
|
slouken@3099
|
220 |
|
slouken@3139
|
221 |
static void
|
slouken@3139
|
222 |
QSA_PlayDevice(_THIS)
|
slouken@3099
|
223 |
{
|
slouken@3139
|
224 |
snd_pcm_channel_status_t cstatus;
|
slouken@3139
|
225 |
int written;
|
slouken@3139
|
226 |
int status;
|
slouken@3139
|
227 |
int towrite;
|
slouken@3139
|
228 |
void *pcmbuffer;
|
slouken@3099
|
229 |
|
icculus@10238
|
230 |
if (!SDL_AtomicGet(&this->enabled) || !this->hidden) {
|
slouken@3139
|
231 |
return;
|
slouken@3139
|
232 |
}
|
slouken@3099
|
233 |
|
slouken@3139
|
234 |
towrite = this->spec.size;
|
slouken@3139
|
235 |
pcmbuffer = this->hidden->pcm_buf;
|
slouken@3099
|
236 |
|
slouken@3139
|
237 |
/* Write the audio data, checking for EAGAIN (buffer full) and underrun */
|
slouken@3139
|
238 |
do {
|
slouken@3139
|
239 |
written =
|
slouken@3139
|
240 |
snd_pcm_plugin_write(this->hidden->audio_handle, pcmbuffer,
|
slouken@3139
|
241 |
towrite);
|
slouken@3139
|
242 |
if (written != towrite) {
|
slouken@3139
|
243 |
/* Check if samples playback got stuck somewhere in hardware or in */
|
slouken@3139
|
244 |
/* the audio device driver */
|
slouken@3139
|
245 |
if ((errno == EAGAIN) && (written == 0)) {
|
slouken@3139
|
246 |
if (this->hidden->timeout_on_wait != 0) {
|
icculus@7038
|
247 |
SDL_SetError("QSA: buffer playback timeout");
|
slouken@3139
|
248 |
return;
|
slouken@3139
|
249 |
}
|
slouken@3099
|
250 |
}
|
slouken@3099
|
251 |
|
slouken@3139
|
252 |
/* Check for errors or conditions */
|
slouken@3139
|
253 |
if ((errno == EAGAIN) || (errno == EWOULDBLOCK)) {
|
slouken@3139
|
254 |
/* Let a little CPU time go by and try to write again */
|
slouken@3139
|
255 |
SDL_Delay(1);
|
slouken@3099
|
256 |
|
slouken@3139
|
257 |
/* if we wrote some data */
|
slouken@3139
|
258 |
towrite -= written;
|
slouken@3139
|
259 |
pcmbuffer += written * this->spec.channels;
|
slouken@3139
|
260 |
continue;
|
slouken@3139
|
261 |
} else {
|
slouken@3139
|
262 |
if ((errno == EINVAL) || (errno == EIO)) {
|
icculus@10257
|
263 |
SDL_zero(cstatus);
|
slouken@3139
|
264 |
if (!this->hidden->iscapture) {
|
slouken@3139
|
265 |
cstatus.channel = SND_PCM_CHANNEL_PLAYBACK;
|
slouken@3139
|
266 |
} else {
|
slouken@3139
|
267 |
cstatus.channel = SND_PCM_CHANNEL_CAPTURE;
|
slouken@3139
|
268 |
}
|
slouken@3099
|
269 |
|
slouken@3139
|
270 |
status =
|
slouken@3139
|
271 |
snd_pcm_plugin_status(this->hidden->audio_handle,
|
slouken@3139
|
272 |
&cstatus);
|
slouken@3139
|
273 |
if (status < 0) {
|
slouken@3139
|
274 |
QSA_SetError("snd_pcm_plugin_status", status);
|
slouken@3139
|
275 |
return;
|
slouken@3139
|
276 |
}
|
slouken@3099
|
277 |
|
slouken@3139
|
278 |
if ((cstatus.status == SND_PCM_STATUS_UNDERRUN) ||
|
slouken@3139
|
279 |
(cstatus.status == SND_PCM_STATUS_READY)) {
|
slouken@3139
|
280 |
if (!this->hidden->iscapture) {
|
slouken@3139
|
281 |
status =
|
slouken@3139
|
282 |
snd_pcm_plugin_prepare(this->hidden->
|
slouken@3139
|
283 |
audio_handle,
|
slouken@3139
|
284 |
SND_PCM_CHANNEL_PLAYBACK);
|
slouken@3139
|
285 |
} else {
|
slouken@3139
|
286 |
status =
|
slouken@3139
|
287 |
snd_pcm_plugin_prepare(this->hidden->
|
slouken@3139
|
288 |
audio_handle,
|
slouken@3139
|
289 |
SND_PCM_CHANNEL_CAPTURE);
|
slouken@3139
|
290 |
}
|
slouken@3139
|
291 |
if (status < 0) {
|
slouken@3139
|
292 |
QSA_SetError("snd_pcm_plugin_prepare", status);
|
slouken@3139
|
293 |
return;
|
slouken@3139
|
294 |
}
|
slouken@3139
|
295 |
}
|
slouken@3139
|
296 |
continue;
|
slouken@3139
|
297 |
} else {
|
slouken@3139
|
298 |
return;
|
slouken@3139
|
299 |
}
|
slouken@3099
|
300 |
}
|
slouken@3139
|
301 |
} else {
|
slouken@3139
|
302 |
/* we wrote all remaining data */
|
slouken@3139
|
303 |
towrite -= written;
|
slouken@3139
|
304 |
pcmbuffer += written * this->spec.channels;
|
slouken@3139
|
305 |
}
|
icculus@10238
|
306 |
} while ((towrite > 0) && SDL_AtomicGet(&this->enabled));
|
slouken@3099
|
307 |
|
slouken@3139
|
308 |
/* If we couldn't write, assume fatal error for now */
|
slouken@3139
|
309 |
if (towrite != 0) {
|
icculus@9394
|
310 |
SDL_OpenedAudioDeviceDisconnected(this);
|
slouken@3139
|
311 |
}
|
slouken@3099
|
312 |
}
|
slouken@3099
|
313 |
|
slouken@3139
|
314 |
static Uint8 *
|
slouken@3139
|
315 |
QSA_GetDeviceBuf(_THIS)
|
slouken@3099
|
316 |
{
|
slouken@3139
|
317 |
return this->hidden->pcm_buf;
|
slouken@3099
|
318 |
}
|
slouken@3099
|
319 |
|
slouken@3139
|
320 |
static void
|
slouken@3139
|
321 |
QSA_CloseDevice(_THIS)
|
slouken@3099
|
322 |
{
|
icculus@10255
|
323 |
if (this->hidden->audio_handle != NULL) {
|
icculus@10255
|
324 |
if (!this->hidden->iscapture) {
|
icculus@10255
|
325 |
/* Finish playing available samples */
|
icculus@10255
|
326 |
snd_pcm_plugin_flush(this->hidden->audio_handle,
|
icculus@10255
|
327 |
SND_PCM_CHANNEL_PLAYBACK);
|
icculus@10255
|
328 |
} else {
|
icculus@10255
|
329 |
/* Cancel unread samples during capture */
|
icculus@10255
|
330 |
snd_pcm_plugin_flush(this->hidden->audio_handle,
|
icculus@10255
|
331 |
SND_PCM_CHANNEL_CAPTURE);
|
slouken@3139
|
332 |
}
|
icculus@10255
|
333 |
snd_pcm_close(this->hidden->audio_handle);
|
icculus@10255
|
334 |
}
|
slouken@3099
|
335 |
|
icculus@10256
|
336 |
SDL_free(this->hidden->pcm_buf);
|
icculus@10255
|
337 |
SDL_free(this->hidden);
|
slouken@3099
|
338 |
}
|
slouken@3099
|
339 |
|
slouken@3139
|
340 |
static int
|
icculus@9394
|
341 |
QSA_OpenDevice(_THIS, void *handle, const char *devname, int iscapture)
|
slouken@3099
|
342 |
{
|
icculus@9394
|
343 |
const QSA_Device *device = (const QSA_Device *) handle;
|
slouken@3139
|
344 |
int status = 0;
|
slouken@3139
|
345 |
int format = 0;
|
slouken@3139
|
346 |
SDL_AudioFormat test_format = 0;
|
slouken@3139
|
347 |
int found = 0;
|
slouken@3139
|
348 |
snd_pcm_channel_setup_t csetup;
|
slouken@3139
|
349 |
snd_pcm_channel_params_t cparams;
|
slouken@3099
|
350 |
|
slouken@3139
|
351 |
/* Initialize all variables that we clean on shutdown */
|
slouken@3139
|
352 |
this->hidden =
|
slouken@3139
|
353 |
(struct SDL_PrivateAudioData *) SDL_calloc(1,
|
slouken@3139
|
354 |
(sizeof
|
slouken@3139
|
355 |
(struct
|
slouken@3139
|
356 |
SDL_PrivateAudioData)));
|
slouken@3139
|
357 |
if (this->hidden == NULL) {
|
icculus@7038
|
358 |
return SDL_OutOfMemory();
|
slouken@3139
|
359 |
}
|
icculus@10257
|
360 |
SDL_zerop(this->hidden);
|
slouken@3099
|
361 |
|
slouken@3139
|
362 |
/* Initialize channel transfer parameters to default */
|
slouken@3139
|
363 |
QSA_InitAudioParams(&cparams);
|
slouken@3099
|
364 |
|
slouken@3139
|
365 |
/* Initialize channel direction: capture or playback */
|
icculus@10235
|
366 |
this->hidden->iscapture = iscapture ? SDL_TRUE : SDL_FALSE;
|
slouken@3099
|
367 |
|
icculus@9394
|
368 |
if (device != NULL) {
|
icculus@9394
|
369 |
/* Open requested audio device */
|
icculus@9394
|
370 |
this->hidden->deviceno = device->deviceno;
|
icculus@9394
|
371 |
this->hidden->cardno = device->cardno;
|
icculus@9394
|
372 |
status = snd_pcm_open(&this->hidden->audio_handle,
|
icculus@9394
|
373 |
device->cardno, device->deviceno,
|
icculus@9394
|
374 |
iscapture ? SND_PCM_OPEN_PLAYBACK : SND_PCM_OPEN_CAPTURE);
|
icculus@9394
|
375 |
} else {
|
slouken@3139
|
376 |
/* Open system default audio device */
|
icculus@9394
|
377 |
status = snd_pcm_open_preferred(&this->hidden->audio_handle,
|
icculus@9394
|
378 |
&this->hidden->cardno,
|
icculus@9394
|
379 |
&this->hidden->deviceno,
|
icculus@9394
|
380 |
iscapture ? SND_PCM_OPEN_PLAYBACK : SND_PCM_OPEN_CAPTURE);
|
slouken@3139
|
381 |
}
|
slouken@3099
|
382 |
|
slouken@3139
|
383 |
/* Check if requested device is opened */
|
slouken@3139
|
384 |
if (status < 0) {
|
slouken@3139
|
385 |
this->hidden->audio_handle = NULL;
|
icculus@7038
|
386 |
return QSA_SetError("snd_pcm_open", status);
|
slouken@3139
|
387 |
}
|
slouken@3099
|
388 |
|
slouken@3139
|
389 |
if (!QSA_CheckBuggyCards(this, QSA_MMAP_WORKAROUND)) {
|
slouken@3139
|
390 |
/* Disable QSA MMAP plugin for buggy audio drivers */
|
slouken@3139
|
391 |
status =
|
slouken@3139
|
392 |
snd_pcm_plugin_set_disable(this->hidden->audio_handle,
|
slouken@3139
|
393 |
PLUGIN_DISABLE_MMAP);
|
slouken@3139
|
394 |
if (status < 0) {
|
icculus@7038
|
395 |
return QSA_SetError("snd_pcm_plugin_set_disable", status);
|
slouken@3139
|
396 |
}
|
slouken@3139
|
397 |
}
|
slouken@3099
|
398 |
|
slouken@3139
|
399 |
/* Try for a closest match on audio format */
|
slouken@3139
|
400 |
format = 0;
|
slouken@3139
|
401 |
/* can't use format as SND_PCM_SFMT_U8 = 0 in qsa */
|
slouken@3139
|
402 |
found = 0;
|
slouken@3099
|
403 |
|
slouken@3139
|
404 |
for (test_format = SDL_FirstAudioFormat(this->spec.format); !found;) {
|
slouken@3139
|
405 |
/* if match found set format to equivalent QSA format */
|
slouken@3139
|
406 |
switch (test_format) {
|
slouken@3139
|
407 |
case AUDIO_U8:
|
slouken@3139
|
408 |
{
|
slouken@3139
|
409 |
format = SND_PCM_SFMT_U8;
|
slouken@3139
|
410 |
found = 1;
|
slouken@3139
|
411 |
}
|
slouken@3139
|
412 |
break;
|
slouken@3139
|
413 |
case AUDIO_S8:
|
slouken@3139
|
414 |
{
|
slouken@3139
|
415 |
format = SND_PCM_SFMT_S8;
|
slouken@3139
|
416 |
found = 1;
|
slouken@3139
|
417 |
}
|
slouken@3139
|
418 |
break;
|
slouken@3139
|
419 |
case AUDIO_S16LSB:
|
slouken@3139
|
420 |
{
|
slouken@3139
|
421 |
format = SND_PCM_SFMT_S16_LE;
|
slouken@3139
|
422 |
found = 1;
|
slouken@3139
|
423 |
}
|
slouken@3139
|
424 |
break;
|
slouken@3139
|
425 |
case AUDIO_S16MSB:
|
slouken@3139
|
426 |
{
|
slouken@3139
|
427 |
format = SND_PCM_SFMT_S16_BE;
|
slouken@3139
|
428 |
found = 1;
|
slouken@3139
|
429 |
}
|
slouken@3139
|
430 |
break;
|
slouken@3139
|
431 |
case AUDIO_U16LSB:
|
slouken@3139
|
432 |
{
|
slouken@3139
|
433 |
format = SND_PCM_SFMT_U16_LE;
|
slouken@3139
|
434 |
found = 1;
|
slouken@3139
|
435 |
}
|
slouken@3139
|
436 |
break;
|
slouken@3139
|
437 |
case AUDIO_U16MSB:
|
slouken@3139
|
438 |
{
|
slouken@3139
|
439 |
format = SND_PCM_SFMT_U16_BE;
|
slouken@3139
|
440 |
found = 1;
|
slouken@3139
|
441 |
}
|
slouken@3139
|
442 |
break;
|
slouken@3139
|
443 |
case AUDIO_S32LSB:
|
slouken@3139
|
444 |
{
|
slouken@3139
|
445 |
format = SND_PCM_SFMT_S32_LE;
|
slouken@3139
|
446 |
found = 1;
|
slouken@3139
|
447 |
}
|
slouken@3139
|
448 |
break;
|
slouken@3139
|
449 |
case AUDIO_S32MSB:
|
slouken@3139
|
450 |
{
|
slouken@3139
|
451 |
format = SND_PCM_SFMT_S32_BE;
|
slouken@3139
|
452 |
found = 1;
|
slouken@3139
|
453 |
}
|
slouken@3139
|
454 |
break;
|
slouken@3139
|
455 |
case AUDIO_F32LSB:
|
slouken@3139
|
456 |
{
|
slouken@3139
|
457 |
format = SND_PCM_SFMT_FLOAT_LE;
|
slouken@3139
|
458 |
found = 1;
|
slouken@3139
|
459 |
}
|
slouken@3139
|
460 |
break;
|
slouken@3139
|
461 |
case AUDIO_F32MSB:
|
slouken@3139
|
462 |
{
|
slouken@3139
|
463 |
format = SND_PCM_SFMT_FLOAT_BE;
|
slouken@3139
|
464 |
found = 1;
|
slouken@3139
|
465 |
}
|
slouken@3139
|
466 |
break;
|
slouken@3139
|
467 |
default:
|
slouken@3139
|
468 |
{
|
slouken@3139
|
469 |
break;
|
slouken@3139
|
470 |
}
|
slouken@3139
|
471 |
}
|
slouken@3099
|
472 |
|
slouken@3139
|
473 |
if (!found) {
|
slouken@3139
|
474 |
test_format = SDL_NextAudioFormat();
|
slouken@3139
|
475 |
}
|
slouken@3139
|
476 |
}
|
slouken@3099
|
477 |
|
slouken@3139
|
478 |
/* assumes test_format not 0 on success */
|
slouken@3139
|
479 |
if (test_format == 0) {
|
icculus@7038
|
480 |
return SDL_SetError("QSA: Couldn't find any hardware audio formats");
|
slouken@3139
|
481 |
}
|
slouken@3099
|
482 |
|
slouken@3139
|
483 |
this->spec.format = test_format;
|
slouken@3099
|
484 |
|
slouken@3139
|
485 |
/* Set the audio format */
|
slouken@3139
|
486 |
cparams.format.format = format;
|
slouken@3099
|
487 |
|
slouken@3139
|
488 |
/* Set mono/stereo/4ch/6ch/8ch audio */
|
slouken@3139
|
489 |
cparams.format.voices = this->spec.channels;
|
slouken@3099
|
490 |
|
slouken@3139
|
491 |
/* Set rate */
|
slouken@3139
|
492 |
cparams.format.rate = this->spec.freq;
|
slouken@3099
|
493 |
|
slouken@3139
|
494 |
/* Setup the transfer parameters according to cparams */
|
slouken@3139
|
495 |
status = snd_pcm_plugin_params(this->hidden->audio_handle, &cparams);
|
slouken@3139
|
496 |
if (status < 0) {
|
icculus@7038
|
497 |
return QSA_SetError("snd_pcm_channel_params", status);
|
slouken@3139
|
498 |
}
|
slouken@3099
|
499 |
|
slouken@3139
|
500 |
/* Make sure channel is setup right one last time */
|
icculus@10257
|
501 |
SDL_zero(csetup);
|
slouken@3139
|
502 |
if (!this->hidden->iscapture) {
|
slouken@3139
|
503 |
csetup.channel = SND_PCM_CHANNEL_PLAYBACK;
|
slouken@3139
|
504 |
} else {
|
slouken@3139
|
505 |
csetup.channel = SND_PCM_CHANNEL_CAPTURE;
|
slouken@3139
|
506 |
}
|
slouken@3099
|
507 |
|
slouken@3139
|
508 |
/* Setup an audio channel */
|
slouken@3139
|
509 |
if (snd_pcm_plugin_setup(this->hidden->audio_handle, &csetup) < 0) {
|
icculus@7038
|
510 |
return SDL_SetError("QSA: Unable to setup channel");
|
slouken@3139
|
511 |
}
|
slouken@3099
|
512 |
|
slouken@3139
|
513 |
/* Calculate the final parameters for this audio specification */
|
slouken@3139
|
514 |
SDL_CalculateAudioSpec(&this->spec);
|
slouken@3099
|
515 |
|
slouken@3139
|
516 |
this->hidden->pcm_len = this->spec.size;
|
slouken@3099
|
517 |
|
slouken@3139
|
518 |
if (this->hidden->pcm_len == 0) {
|
slouken@3139
|
519 |
this->hidden->pcm_len =
|
slouken@3139
|
520 |
csetup.buf.block.frag_size * this->spec.channels *
|
slouken@3139
|
521 |
(snd_pcm_format_width(format) / 8);
|
slouken@3139
|
522 |
}
|
slouken@3099
|
523 |
|
slouken@3139
|
524 |
/*
|
slouken@3139
|
525 |
* Allocate memory to the audio buffer and initialize with silence
|
slouken@3139
|
526 |
* (Note that buffer size must be a multiple of fragment size, so find
|
slouken@3139
|
527 |
* closest multiple)
|
slouken@3139
|
528 |
*/
|
slouken@3139
|
529 |
this->hidden->pcm_buf =
|
icculus@10256
|
530 |
(Uint8 *) SDL_malloc(this->hidden->pcm_len);
|
slouken@3139
|
531 |
if (this->hidden->pcm_buf == NULL) {
|
icculus@7038
|
532 |
return SDL_OutOfMemory();
|
slouken@3139
|
533 |
}
|
slouken@3139
|
534 |
SDL_memset(this->hidden->pcm_buf, this->spec.silence,
|
slouken@3139
|
535 |
this->hidden->pcm_len);
|
slouken@3099
|
536 |
|
slouken@3139
|
537 |
/* get the file descriptor */
|
slouken@3139
|
538 |
if (!this->hidden->iscapture) {
|
slouken@3139
|
539 |
this->hidden->audio_fd =
|
slouken@3139
|
540 |
snd_pcm_file_descriptor(this->hidden->audio_handle,
|
slouken@3139
|
541 |
SND_PCM_CHANNEL_PLAYBACK);
|
slouken@3139
|
542 |
} else {
|
slouken@3139
|
543 |
this->hidden->audio_fd =
|
slouken@3139
|
544 |
snd_pcm_file_descriptor(this->hidden->audio_handle,
|
slouken@3139
|
545 |
SND_PCM_CHANNEL_CAPTURE);
|
slouken@3139
|
546 |
}
|
slouken@3099
|
547 |
|
slouken@3139
|
548 |
if (this->hidden->audio_fd < 0) {
|
icculus@7038
|
549 |
return QSA_SetError("snd_pcm_file_descriptor", status);
|
slouken@3139
|
550 |
}
|
slouken@3099
|
551 |
|
slouken@3139
|
552 |
/* Prepare an audio channel */
|
slouken@3139
|
553 |
if (!this->hidden->iscapture) {
|
slouken@3139
|
554 |
/* Prepare audio playback */
|
slouken@3139
|
555 |
status =
|
slouken@3139
|
556 |
snd_pcm_plugin_prepare(this->hidden->audio_handle,
|
slouken@3139
|
557 |
SND_PCM_CHANNEL_PLAYBACK);
|
slouken@3139
|
558 |
} else {
|
slouken@3139
|
559 |
/* Prepare audio capture */
|
slouken@3139
|
560 |
status =
|
slouken@3139
|
561 |
snd_pcm_plugin_prepare(this->hidden->audio_handle,
|
slouken@3139
|
562 |
SND_PCM_CHANNEL_CAPTURE);
|
slouken@3139
|
563 |
}
|
slouken@3099
|
564 |
|
slouken@3139
|
565 |
if (status < 0) {
|
icculus@7038
|
566 |
return QSA_SetError("snd_pcm_plugin_prepare", status);
|
slouken@3139
|
567 |
}
|
slouken@3099
|
568 |
|
slouken@3139
|
569 |
/* We're really ready to rock and roll. :-) */
|
icculus@7038
|
570 |
return 0;
|
slouken@3099
|
571 |
}
|
slouken@3099
|
572 |
|
icculus@5593
|
573 |
static void
|
icculus@9394
|
574 |
QSA_DetectDevices(void)
|
slouken@3099
|
575 |
{
|
slouken@3139
|
576 |
uint32_t it;
|
slouken@3139
|
577 |
uint32_t cards;
|
slouken@3139
|
578 |
uint32_t devices;
|
slouken@3139
|
579 |
int32_t status;
|
slouken@3139
|
580 |
|
slouken@3139
|
581 |
/* Detect amount of available devices */
|
slouken@3139
|
582 |
/* this value can be changed in the runtime */
|
slouken@3139
|
583 |
cards = snd_cards();
|
slouken@3099
|
584 |
|
slouken@3139
|
585 |
/* If io-audio manager is not running we will get 0 as number */
|
slouken@3139
|
586 |
/* of available audio devices */
|
slouken@3139
|
587 |
if (cards == 0) {
|
slouken@3139
|
588 |
/* We have no any available audio devices */
|
icculus@5593
|
589 |
return;
|
slouken@3139
|
590 |
}
|
slouken@3099
|
591 |
|
icculus@9394
|
592 |
/* !!! FIXME: code duplication */
|
slouken@3139
|
593 |
/* Find requested devices by type */
|
icculus@9394
|
594 |
{ /* output devices */
|
slouken@3139
|
595 |
/* Playback devices enumeration requested */
|
slouken@3139
|
596 |
for (it = 0; it < cards; it++) {
|
slouken@3139
|
597 |
devices = 0;
|
slouken@3139
|
598 |
do {
|
slouken@3139
|
599 |
status =
|
slouken@3139
|
600 |
snd_card_get_longname(it,
|
slouken@3139
|
601 |
qsa_playback_device
|
slouken@3139
|
602 |
[qsa_playback_devices].name,
|
slouken@3139
|
603 |
QSA_MAX_NAME_LENGTH);
|
slouken@3139
|
604 |
if (status == EOK) {
|
slouken@3139
|
605 |
snd_pcm_t *handle;
|
slouken@3099
|
606 |
|
slouken@3139
|
607 |
/* Add device number to device name */
|
slouken@3139
|
608 |
sprintf(qsa_playback_device[qsa_playback_devices].name +
|
slouken@3139
|
609 |
SDL_strlen(qsa_playback_device
|
slouken@3139
|
610 |
[qsa_playback_devices].name), " d%d",
|
slouken@3139
|
611 |
devices);
|
slouken@3099
|
612 |
|
slouken@3139
|
613 |
/* Store associated card number id */
|
slouken@3139
|
614 |
qsa_playback_device[qsa_playback_devices].cardno = it;
|
slouken@3099
|
615 |
|
slouken@3139
|
616 |
/* Check if this device id could play anything */
|
slouken@3139
|
617 |
status =
|
slouken@3139
|
618 |
snd_pcm_open(&handle, it, devices,
|
slouken@3139
|
619 |
SND_PCM_OPEN_PLAYBACK);
|
slouken@3139
|
620 |
if (status == EOK) {
|
slouken@3139
|
621 |
qsa_playback_device[qsa_playback_devices].deviceno =
|
slouken@3139
|
622 |
devices;
|
slouken@3139
|
623 |
status = snd_pcm_close(handle);
|
slouken@3139
|
624 |
if (status == EOK) {
|
icculus@9394
|
625 |
SDL_AddAudioDevice(SDL_FALSE, qsa_playback_device[qsa_playback_devices].name, &qsa_playback_device[qsa_playback_devices]);
|
slouken@3139
|
626 |
qsa_playback_devices++;
|
slouken@3139
|
627 |
}
|
slouken@3139
|
628 |
} else {
|
slouken@3139
|
629 |
/* Check if we got end of devices list */
|
slouken@3139
|
630 |
if (status == -ENOENT) {
|
slouken@3139
|
631 |
break;
|
slouken@3139
|
632 |
}
|
slouken@3139
|
633 |
}
|
slouken@3139
|
634 |
} else {
|
slouken@3139
|
635 |
break;
|
slouken@3139
|
636 |
}
|
slouken@3139
|
637 |
|
slouken@3139
|
638 |
/* Check if we reached maximum devices count */
|
slouken@3139
|
639 |
if (qsa_playback_devices >= QSA_MAX_DEVICES) {
|
slouken@3139
|
640 |
break;
|
slouken@3139
|
641 |
}
|
slouken@3139
|
642 |
devices++;
|
slouken@3139
|
643 |
} while (1);
|
slouken@3099
|
644 |
|
slouken@3099
|
645 |
/* Check if we reached maximum devices count */
|
slouken@3139
|
646 |
if (qsa_playback_devices >= QSA_MAX_DEVICES) {
|
slouken@3139
|
647 |
break;
|
slouken@3099
|
648 |
}
|
slouken@3139
|
649 |
}
|
icculus@9394
|
650 |
}
|
icculus@9394
|
651 |
|
icculus@9394
|
652 |
{ /* capture devices */
|
slouken@3139
|
653 |
/* Capture devices enumeration requested */
|
slouken@3139
|
654 |
for (it = 0; it < cards; it++) {
|
slouken@3139
|
655 |
devices = 0;
|
slouken@3139
|
656 |
do {
|
slouken@3139
|
657 |
status =
|
slouken@3139
|
658 |
snd_card_get_longname(it,
|
slouken@3139
|
659 |
qsa_capture_device
|
slouken@3139
|
660 |
[qsa_capture_devices].name,
|
slouken@3139
|
661 |
QSA_MAX_NAME_LENGTH);
|
slouken@3139
|
662 |
if (status == EOK) {
|
slouken@3139
|
663 |
snd_pcm_t *handle;
|
slouken@3099
|
664 |
|
slouken@3139
|
665 |
/* Add device number to device name */
|
slouken@3139
|
666 |
sprintf(qsa_capture_device[qsa_capture_devices].name +
|
slouken@3139
|
667 |
SDL_strlen(qsa_capture_device
|
slouken@3139
|
668 |
[qsa_capture_devices].name), " d%d",
|
slouken@3139
|
669 |
devices);
|
slouken@3099
|
670 |
|
slouken@3139
|
671 |
/* Store associated card number id */
|
slouken@3139
|
672 |
qsa_capture_device[qsa_capture_devices].cardno = it;
|
slouken@3099
|
673 |
|
slouken@3139
|
674 |
/* Check if this device id could play anything */
|
slouken@3139
|
675 |
status =
|
slouken@3139
|
676 |
snd_pcm_open(&handle, it, devices,
|
slouken@3139
|
677 |
SND_PCM_OPEN_CAPTURE);
|
slouken@3139
|
678 |
if (status == EOK) {
|
slouken@3139
|
679 |
qsa_capture_device[qsa_capture_devices].deviceno =
|
slouken@3139
|
680 |
devices;
|
slouken@3139
|
681 |
status = snd_pcm_close(handle);
|
slouken@3139
|
682 |
if (status == EOK) {
|
icculus@9394
|
683 |
SDL_AddAudioDevice(SDL_TRUE, qsa_capture_device[qsa_capture_devices].name, &qsa_capture_device[qsa_capture_devices]);
|
slouken@3139
|
684 |
qsa_capture_devices++;
|
slouken@3139
|
685 |
}
|
slouken@3139
|
686 |
} else {
|
slouken@3139
|
687 |
/* Check if we got end of devices list */
|
slouken@3139
|
688 |
if (status == -ENOENT) {
|
slouken@3139
|
689 |
break;
|
slouken@3139
|
690 |
}
|
slouken@3139
|
691 |
}
|
slouken@3099
|
692 |
|
slouken@3139
|
693 |
/* Check if we reached maximum devices count */
|
slouken@3139
|
694 |
if (qsa_capture_devices >= QSA_MAX_DEVICES) {
|
slouken@3139
|
695 |
break;
|
slouken@3139
|
696 |
}
|
slouken@3139
|
697 |
} else {
|
slouken@3139
|
698 |
break;
|
slouken@3139
|
699 |
}
|
slouken@3139
|
700 |
devices++;
|
slouken@3139
|
701 |
} while (1);
|
slouken@3099
|
702 |
|
slouken@3139
|
703 |
/* Check if we reached maximum devices count */
|
slouken@3139
|
704 |
if (qsa_capture_devices >= QSA_MAX_DEVICES) {
|
slouken@3139
|
705 |
break;
|
slouken@3139
|
706 |
}
|
slouken@3139
|
707 |
}
|
slouken@3139
|
708 |
}
|
slouken@3099
|
709 |
}
|
slouken@3099
|
710 |
|
icculus@5584
|
711 |
static void
|
slouken@3139
|
712 |
QSA_Deinitialize(void)
|
slouken@3139
|
713 |
{
|
slouken@3139
|
714 |
/* Clear devices array on shutdown */
|
icculus@10257
|
715 |
/* !!! FIXME: we zero these on init...any reason to do it here? */
|
icculus@10257
|
716 |
SDL_zero(qsa_playback_device);
|
icculus@10257
|
717 |
SDL_zero(qsa_capture_device);
|
slouken@3139
|
718 |
qsa_playback_devices = 0;
|
slouken@3139
|
719 |
qsa_capture_devices = 0;
|
slouken@3099
|
720 |
}
|
slouken@3099
|
721 |
|
slouken@3139
|
722 |
static int
|
slouken@3139
|
723 |
QSA_Init(SDL_AudioDriverImpl * impl)
|
slouken@3099
|
724 |
{
|
slouken@3139
|
725 |
snd_pcm_t *handle = NULL;
|
slouken@3139
|
726 |
int32_t status = 0;
|
slouken@3139
|
727 |
|
slouken@3139
|
728 |
/* Clear devices array */
|
icculus@10257
|
729 |
SDL_zero(qsa_playback_device);
|
icculus@10257
|
730 |
SDL_zero(qsa_capture_device);
|
slouken@3139
|
731 |
qsa_playback_devices = 0;
|
slouken@3139
|
732 |
qsa_capture_devices = 0;
|
slouken@3139
|
733 |
|
slouken@3139
|
734 |
/* Set function pointers */
|
slouken@3139
|
735 |
/* DeviceLock and DeviceUnlock functions are used default, */
|
slouken@3139
|
736 |
/* provided by SDL, which uses pthread_mutex for lock/unlock */
|
slouken@3139
|
737 |
impl->DetectDevices = QSA_DetectDevices;
|
slouken@3139
|
738 |
impl->OpenDevice = QSA_OpenDevice;
|
slouken@3139
|
739 |
impl->ThreadInit = QSA_ThreadInit;
|
slouken@3139
|
740 |
impl->WaitDevice = QSA_WaitDevice;
|
slouken@3139
|
741 |
impl->PlayDevice = QSA_PlayDevice;
|
slouken@3139
|
742 |
impl->GetDeviceBuf = QSA_GetDeviceBuf;
|
slouken@3139
|
743 |
impl->CloseDevice = QSA_CloseDevice;
|
slouken@3139
|
744 |
impl->Deinitialize = QSA_Deinitialize;
|
slouken@3139
|
745 |
impl->LockDevice = NULL;
|
slouken@3139
|
746 |
impl->UnlockDevice = NULL;
|
slouken@3139
|
747 |
|
slouken@3139
|
748 |
impl->OnlyHasDefaultOutputDevice = 0;
|
slouken@3139
|
749 |
impl->ProvidesOwnCallbackThread = 0;
|
slouken@3139
|
750 |
impl->SkipMixerLock = 0;
|
slouken@3139
|
751 |
impl->HasCaptureSupport = 1;
|
slouken@3139
|
752 |
impl->OnlyHasDefaultOutputDevice = 0;
|
icculus@10258
|
753 |
impl->OnlyHasDefaultCaptureDevice = 0;
|
slouken@3139
|
754 |
|
slouken@3139
|
755 |
/* Check if io-audio manager is running or not */
|
slouken@3139
|
756 |
status = snd_cards();
|
slouken@3139
|
757 |
if (status == 0) {
|
slouken@3139
|
758 |
/* if no, return immediately */
|
slouken@3139
|
759 |
return 1;
|
slouken@3139
|
760 |
}
|
slouken@3139
|
761 |
|
icculus@3699
|
762 |
return 1; /* this audio target is available. */
|
slouken@3099
|
763 |
}
|
slouken@3099
|
764 |
|
slouken@3139
|
765 |
AudioBootStrap QSAAUDIO_bootstrap = {
|
icculus@5594
|
766 |
"qsa", "QNX QSA Audio", QSA_Init, 0
|
slouken@3099
|
767 |
};
|
slouken@3099
|
768 |
|
slouken@6044
|
769 |
#endif /* SDL_AUDIO_DRIVER_QSA */
|
slouken@6044
|
770 |
|
slouken@3099
|
771 |
/* vi: set ts=4 sw=4 expandtab: */
|