src/audio/xaudio2/SDL_xaudio2.c
author David Ludwig
Thu, 22 Nov 2012 22:34:50 -0500
changeset 8353 82447809a27e
parent 6352 a9bcd26e7105
child 8361 eefad2ec4f76
permissions -rw-r--r--
WinRT: got the XAudio2 backend compiling (but not running, yet)
     1 /*
     2   Simple DirectMedia Layer
     3   Copyright (C) 1997-2012 Sam Lantinga <slouken@libsdl.org>
     4 
     5   This software is provided 'as-is', without any express or implied
     6   warranty.  In no event will the authors be held liable for any damages
     7   arising from the use of this software.
     8 
     9   Permission is granted to anyone to use this software for any purpose,
    10   including commercial applications, and to alter it and redistribute it
    11   freely, subject to the following restrictions:
    12 
    13   1. The origin of this software must not be misrepresented; you must not
    14      claim that you wrote the original software. If you use this software
    15      in a product, an acknowledgment in the product documentation would be
    16      appreciated but is not required.
    17   2. Altered source versions must be plainly marked as such, and must not be
    18      misrepresented as being the original software.
    19   3. This notice may not be removed or altered from any source distribution.
    20 */
    21 
    22 /* WinRT NOTICE:
    23 
    24    A number of changes were warranted to SDL's XAudio2 backend in order to
    25    get it compiling for Windows RT.
    26 
    27    When compiling for WinRT, XAudio2.h requires that it be compiled in a C++
    28    file, and not a straight C file.  Trying to compile it as C leads to lots
    29    of errors, at least with MSVC 2012 and Windows SDK 8.0, as of Nov 22, 2012.
    30    To address this specific issue, a few changes were made to SDL_xaudio2.c:
    31    
    32    1. SDL_xaudio2.c is compiled as a C++ file in WinRT builds.  Exported
    33       symbols, namely XAUDIO2_bootstrap, uses 'extern "C"' to make sure the
    34       rest of SDL can access it.  Non-WinRT builds continue to compile
    35       SDL_xaudio2.c as a C file.
    36    2. A macro redefines variables named 'this' to '_this', to prevent compiler
    37       errors (C2355 in Visual C++) related to 'this' being a reserverd keyword.
    38       This hack may need to be altered in the future, particularly if C++'s
    39       'this' keyword needs to be used (within SDL_xaudio2.c).  At the time
    40       WinRT support was initially added to SDL's XAudio2 backend, this
    41       capability was not needed.
    42    3. The C-style macros to invoke XAudio2's COM-based methods were
    43       rewritten to be C++-friendly.  These are provided in the file,
    44       SDL_xaudio2_winrthelpers.h.
    45    4. IXAudio2::CreateSourceVoice, when used in C++, requires its callbacks to
    46       be specified via a C++ class.  SDL's XAudio2 backend was written with
    47       C-style callbacks.  A class to bridge these two interfaces,
    48       SDL_XAudio2VoiceCallback, was written to make XAudio2 happy.  Its methods
    49       just call SDL's existing, C-style callbacks.
    50    5. Multiple checks for the __cplusplus macro were made, in appropriate
    51       places.  
    52 
    53 
    54    A few additional changes to SDL's XAudio2 backend were warranted by API
    55    changes to Windows.  Many, but not all of these are documented by Microsoft
    56    at:
    57    http://blogs.msdn.com/b/chuckw/archive/2012/04/02/xaudio2-and-windows-8-consumer-preview.aspx
    58 
    59    1. Windows' thread synchronization function, CreateSemaphore, was removed
    60       from Windows RT.  SDL's semaphore API was substituted instead.
    61    2. The method calls, IXAudio2::GetDeviceCount and IXAudio2::GetDeviceDetails
    62       were removed from the XAudio2 API.  Microsoft is telling developers to
    63       use APIs in Windows::Foundation instead.
    64       For SDL, the missing methods were reimplemented using the APIs Microsoft
    65       said to use.
    66    3. CoInitialize and CoUninitialize are not available in Windows RT.
    67       These calls were removed, as COM will have been initialized earlier,
    68       at least by the call to the WinRT app's main function
    69       (aka 'int main(Platform::Array<Platform::String^>^)).  (DLudwig:
    70       This was my understanding of how WinRT: the 'main' function uses
    71       a tag of [MTAThread], which should initialize COM.  My understanding
    72       of COM is somewhat limited, and I may be incorrect here.)
    73    4. IXAudio2::CreateMasteringVoice changed its integer-based 'DeviceIndex'
    74       argument to a string-based one, 'szDeviceId'.  In Windows RT, the
    75       string-based argument will be used.
    76 */
    77 
    78 #include "SDL_config.h"
    79 
    80 #if SDL_AUDIO_DRIVER_XAUDIO2
    81 
    82 #ifdef __cplusplus
    83 extern "C" {
    84 #endif
    85 #include "../../core/windows/SDL_windows.h"
    86 #include "SDL_audio.h"
    87 #include "../SDL_audio_c.h"
    88 #include "../SDL_sysaudio.h"
    89 #include "SDL_assert.h"
    90 #ifdef __cplusplus
    91 }
    92 #endif
    93 
    94 #if defined(__WINRT__)
    95 #  define SDL_XAUDIO2_HAS_SDK 1
    96 #endif
    97 #if defined(__WIN32__)
    98 #include <dxsdkver.h> /* XAudio2 exists as of the March 2008 DirectX SDK */
    99 #if (!defined(_DXSDK_BUILD_MAJOR) || (_DXSDK_BUILD_MAJOR < 1284))
   100 #  pragma message("Your DirectX SDK is too old. Disabling XAudio2 support.")
   101 #else
   102 #  define SDL_XAUDIO2_HAS_SDK 1
   103 #endif
   104 #endif
   105 
   106 #ifdef SDL_XAUDIO2_HAS_SDK
   107 
   108 #define INITGUID 1
   109 #include <XAudio2.h>
   110 
   111 /* Hidden "this" pointer for the audio functions */
   112 #define _THIS	SDL_AudioDevice *this
   113 
   114 #ifdef __cplusplus
   115 #define this _this
   116 #include "SDL_xaudio2_winrthelpers.h"
   117 #endif
   118 
   119 struct SDL_PrivateAudioData
   120 {
   121     IXAudio2 *ixa2;
   122     IXAudio2SourceVoice *source;
   123     IXAudio2MasteringVoice *mastering;
   124     SDL_sem * semaphore;
   125     Uint8 *mixbuf;
   126     int mixlen;
   127     Uint8 *nextbuf;
   128 };
   129 
   130 
   131 static __inline__ char *
   132 utf16_to_utf8(const WCHAR *S)
   133 {
   134     /* !!! FIXME: this should be UTF-16, not UCS-2! */
   135     return SDL_iconv_string("UTF-8", "UCS-2", (char *)(S),
   136                             (SDL_wcslen(S)+1)*sizeof(WCHAR));
   137 }
   138 
   139 static void
   140 XAUDIO2_DetectDevices(int iscapture, SDL_AddAudioDevice addfn)
   141 {
   142     IXAudio2 *ixa2 = NULL;
   143     UINT32 devcount = 0;
   144     UINT32 i = 0;
   145     void *ptr = NULL;
   146 
   147     if (iscapture) {
   148         SDL_SetError("XAudio2: capture devices unsupported.");
   149         return;
   150     } else if (XAudio2Create(&ixa2, 0, XAUDIO2_DEFAULT_PROCESSOR) != S_OK) {
   151         SDL_SetError("XAudio2: XAudio2Create() failed.");
   152         return;
   153     } else if (IXAudio2_GetDeviceCount(ixa2, &devcount) != S_OK) {
   154         SDL_SetError("XAudio2: IXAudio2::GetDeviceCount() failed.");
   155         IXAudio2_Release(ixa2);
   156         return;
   157     }
   158 
   159     for (i = 0; i < devcount; i++) {
   160         XAUDIO2_DEVICE_DETAILS details;
   161         if (IXAudio2_GetDeviceDetails(ixa2, i, &details) == S_OK) {
   162             char *str = utf16_to_utf8(details.DisplayName);
   163             if (str != NULL) {
   164                 addfn(str);
   165                 SDL_free(str);  /* addfn() made a copy of the string. */
   166             }
   167         }
   168     }
   169 
   170     IXAudio2_Release(ixa2);
   171 }
   172 
   173 static void STDMETHODCALLTYPE
   174 VoiceCBOnBufferEnd(THIS_ void *data)
   175 {
   176     /* Just signal the SDL audio thread and get out of XAudio2's way. */
   177     SDL_AudioDevice *this = (SDL_AudioDevice *) data;
   178     SDL_SemPost(this->hidden->semaphore);
   179 }
   180 
   181 static void STDMETHODCALLTYPE
   182 VoiceCBOnVoiceError(THIS_ void *data, HRESULT Error)
   183 {
   184     /* !!! FIXME: attempt to recover, or mark device disconnected. */
   185     SDL_assert(0 && "write me!");
   186 }
   187 
   188 /* no-op callbacks... */
   189 static void STDMETHODCALLTYPE VoiceCBOnStreamEnd(THIS) {}
   190 static void STDMETHODCALLTYPE VoiceCBOnVoiceProcessPassStart(THIS_ UINT32 b) {}
   191 static void STDMETHODCALLTYPE VoiceCBOnVoiceProcessPassEnd(THIS) {}
   192 static void STDMETHODCALLTYPE VoiceCBOnBufferStart(THIS_ void *data) {}
   193 static void STDMETHODCALLTYPE VoiceCBOnLoopEnd(THIS_ void *data) {}
   194 
   195 #if defined(__cplusplus)
   196 class SDL_XAudio2VoiceCallback : public IXAudio2VoiceCallback
   197 {
   198 public:
   199     STDMETHOD_(void, OnBufferEnd)(void *pBufferContext) {
   200         VoiceCBOnBufferEnd(pBufferContext);
   201     }
   202     STDMETHOD_(void, OnBufferStart)(void *pBufferContext) {
   203         VoiceCBOnBufferEnd(pBufferContext);
   204     }
   205     STDMETHOD_(void, OnLoopEnd)(void *pBufferContext) {
   206         VoiceCBOnLoopEnd(pBufferContext);
   207     }
   208     STDMETHOD_(void, OnStreamEnd)() {
   209         VoiceCBOnStreamEnd();
   210     }
   211     STDMETHOD_(void, OnVoiceError)(void *pBufferContext, HRESULT Error) {
   212         VoiceCBOnVoiceError(pBufferContext, Error);
   213     }
   214     STDMETHOD_(void, OnVoiceProcessingPassEnd)() {
   215         VoiceCBOnVoiceProcessPassEnd();
   216     }
   217     STDMETHOD_(void, OnVoiceProcessingPassStart)(UINT32 BytesRequired) {
   218         VoiceCBOnVoiceProcessPassStart(BytesRequired);
   219     }
   220 };
   221 #endif
   222 
   223 static Uint8 *
   224 XAUDIO2_GetDeviceBuf(_THIS)
   225 {
   226     return this->hidden->nextbuf;
   227 }
   228 
   229 static void
   230 XAUDIO2_PlayDevice(_THIS)
   231 {
   232     XAUDIO2_BUFFER buffer;
   233     Uint8 *mixbuf = this->hidden->mixbuf;
   234     Uint8 *nextbuf = this->hidden->nextbuf;
   235     const int mixlen = this->hidden->mixlen;
   236     IXAudio2SourceVoice *source = this->hidden->source;
   237     HRESULT result = S_OK;
   238 
   239     if (!this->enabled) { /* shutting down? */
   240         return;
   241     }
   242 
   243     /* Submit the next filled buffer */
   244     SDL_zero(buffer);
   245     buffer.AudioBytes = mixlen;
   246     buffer.pAudioData = nextbuf;
   247     buffer.pContext = this;
   248 
   249     if (nextbuf == mixbuf) {
   250         nextbuf += mixlen;
   251     } else {
   252         nextbuf = mixbuf;
   253     }
   254     this->hidden->nextbuf = nextbuf;
   255 
   256     result = IXAudio2SourceVoice_SubmitSourceBuffer(source, &buffer, NULL);
   257     if (result == XAUDIO2_E_DEVICE_INVALIDATED) {
   258         /* !!! FIXME: possibly disconnected or temporary lost. Recover? */
   259     }
   260 
   261     if (result != S_OK) {  /* uhoh, panic! */
   262         IXAudio2SourceVoice_FlushSourceBuffers(source);
   263         this->enabled = 0;
   264     }
   265 }
   266 
   267 static void
   268 XAUDIO2_WaitDevice(_THIS)
   269 {
   270     if (this->enabled) {
   271         SDL_SemWait(this->hidden->semaphore);
   272     }
   273 }
   274 
   275 static void
   276 XAUDIO2_WaitDone(_THIS)
   277 {
   278     IXAudio2SourceVoice *source = this->hidden->source;
   279     XAUDIO2_VOICE_STATE state;
   280     SDL_assert(!this->enabled);  /* flag that stops playing. */
   281     IXAudio2SourceVoice_Discontinuity(source);
   282     IXAudio2SourceVoice_GetState(source, &state);
   283     while (state.BuffersQueued > 0) {
   284         SDL_SemWait(this->hidden->semaphore);
   285         IXAudio2SourceVoice_GetState(source, &state);
   286     }
   287 }
   288 
   289 
   290 static void
   291 XAUDIO2_CloseDevice(_THIS)
   292 {
   293     if (this->hidden != NULL) {
   294         IXAudio2 *ixa2 = this->hidden->ixa2;
   295         IXAudio2SourceVoice *source = this->hidden->source;
   296         IXAudio2MasteringVoice *mastering = this->hidden->mastering;
   297 
   298         if (source != NULL) {
   299             IXAudio2SourceVoice_Stop(source, 0, XAUDIO2_COMMIT_NOW);
   300             IXAudio2SourceVoice_FlushSourceBuffers(source);
   301             IXAudio2SourceVoice_DestroyVoice(source);
   302         }
   303         if (ixa2 != NULL) {
   304             IXAudio2_StopEngine(ixa2);
   305         }
   306         if (mastering != NULL) {
   307             IXAudio2MasteringVoice_DestroyVoice(mastering);
   308         }
   309         if (ixa2 != NULL) {
   310             IXAudio2_Release(ixa2);
   311         }
   312         if (this->hidden->mixbuf != NULL) {
   313             SDL_free(this->hidden->mixbuf);
   314         }
   315         if (this->hidden->semaphore != NULL) {
   316             CloseHandle(this->hidden->semaphore);
   317         }
   318 
   319         SDL_free(this->hidden);
   320         this->hidden = NULL;
   321     }
   322 }
   323 
   324 static int
   325 XAUDIO2_OpenDevice(_THIS, const char *devname, int iscapture)
   326 {
   327     HRESULT result = S_OK;
   328     WAVEFORMATEX waveformat;
   329     int valid_format = 0;
   330     SDL_AudioFormat test_format = SDL_FirstAudioFormat(this->spec.format);
   331     IXAudio2 *ixa2 = NULL;
   332     IXAudio2SourceVoice *source = NULL;
   333 #if defined(__WINRT__)
   334     WCHAR devId[256];
   335 #else
   336     UINT32 devId = 0;  /* 0 == system default device. */
   337 #endif
   338 
   339 #if defined(__cplusplus)
   340     static SDL_XAudio2VoiceCallback callbacks;
   341 #else
   342 	static IXAudio2VoiceCallbackVtbl callbacks_vtable = {
   343 	    VoiceCBOnVoiceProcessPassStart,
   344         VoiceCBOnVoiceProcessPassEnd,
   345         VoiceCBOnStreamEnd,
   346         VoiceCBOnBufferStart,
   347         VoiceCBOnBufferEnd,
   348         VoiceCBOnLoopEnd,
   349         VoiceCBOnVoiceError
   350 	};
   351 
   352 	static IXAudio2VoiceCallback callbacks = { &callbacks_vtable };
   353 #endif // ! defined(__cplusplus)
   354 
   355 #if defined(__WINRT__)
   356     SDL_zero(devId);
   357 #endif
   358 
   359     if (iscapture) {
   360         SDL_SetError("XAudio2: capture devices unsupported.");
   361         return 0;
   362     } else if (XAudio2Create(&ixa2, 0, XAUDIO2_DEFAULT_PROCESSOR) != S_OK) {
   363         SDL_SetError("XAudio2: XAudio2Create() failed.");
   364         return 0;
   365     }
   366 
   367     if (devname != NULL) {
   368         UINT32 devcount = 0;
   369         UINT32 i = 0;
   370 
   371         if (IXAudio2_GetDeviceCount(ixa2, &devcount) != S_OK) {
   372             IXAudio2_Release(ixa2);
   373             SDL_SetError("XAudio2: IXAudio2_GetDeviceCount() failed.");
   374             return 0;
   375         }
   376         for (i = 0; i < devcount; i++) {
   377             XAUDIO2_DEVICE_DETAILS details;
   378             if (IXAudio2_GetDeviceDetails(ixa2, i, &details) == S_OK) {
   379                 char *str = utf16_to_utf8(details.DisplayName);
   380                 if (str != NULL) {
   381                     const int match = (SDL_strcmp(str, devname) == 0);
   382                     SDL_free(str);
   383                     if (match) {
   384 #if defined(__WINRT__)
   385                         wcsncpy_s(devId, ARRAYSIZE(devId), details.DeviceID, _TRUNCATE);
   386 #else
   387                         devId = i;
   388 #endif
   389                         break;
   390                     }
   391                 }
   392             }
   393         }
   394 
   395         if (i == devcount) {
   396             IXAudio2_Release(ixa2);
   397             SDL_SetError("XAudio2: Requested device not found.");
   398             return 0;
   399         }
   400     }
   401 
   402     /* Initialize all variables that we clean on shutdown */
   403     this->hidden = (struct SDL_PrivateAudioData *)
   404         SDL_malloc((sizeof *this->hidden));
   405     if (this->hidden == NULL) {
   406         IXAudio2_Release(ixa2);
   407         SDL_OutOfMemory();
   408         return 0;
   409     }
   410     SDL_memset(this->hidden, 0, (sizeof *this->hidden));
   411 
   412     this->hidden->ixa2 = ixa2;
   413     this->hidden->semaphore = SDL_CreateSemaphore(1);
   414     if (this->hidden->semaphore == NULL) {
   415         XAUDIO2_CloseDevice(this);
   416         SDL_SetError("XAudio2: CreateSemaphore() failed!");
   417         return 0;
   418     }
   419 
   420     while ((!valid_format) && (test_format)) {
   421         switch (test_format) {
   422         case AUDIO_U8:
   423         case AUDIO_S16:
   424         case AUDIO_S32:
   425         case AUDIO_F32:
   426             this->spec.format = test_format;
   427             valid_format = 1;
   428             break;
   429         }
   430         test_format = SDL_NextAudioFormat();
   431     }
   432 
   433     if (!valid_format) {
   434         XAUDIO2_CloseDevice(this);
   435         SDL_SetError("XAudio2: Unsupported audio format");
   436         return 0;
   437     }
   438 
   439     /* Update the fragment size as size in bytes */
   440     SDL_CalculateAudioSpec(&this->spec);
   441 
   442     /* We feed a Source, it feeds the Mastering, which feeds the device. */
   443     this->hidden->mixlen = this->spec.size;
   444     this->hidden->mixbuf = (Uint8 *) SDL_malloc(2 * this->hidden->mixlen);
   445     if (this->hidden->mixbuf == NULL) {
   446         XAUDIO2_CloseDevice(this);
   447         SDL_OutOfMemory();
   448         return 0;
   449     }
   450     this->hidden->nextbuf = this->hidden->mixbuf;
   451     SDL_memset(this->hidden->mixbuf, 0, 2 * this->hidden->mixlen);
   452 
   453     /* We use XAUDIO2_DEFAULT_CHANNELS instead of this->spec.channels. On
   454        Xbox360, this means 5.1 output, but on Windows, it means "figure out
   455        what the system has." It might be preferable to let XAudio2 blast
   456        stereo output to appropriate surround sound configurations
   457        instead of clamping to 2 channels, even though we'll configure the
   458        Source Voice for whatever number of channels you supply. */
   459     result = IXAudio2_CreateMasteringVoice(ixa2, &this->hidden->mastering,
   460                                            XAUDIO2_DEFAULT_CHANNELS,
   461                                            this->spec.freq, 0, devId, NULL);
   462     if (result != S_OK) {
   463         XAUDIO2_CloseDevice(this);
   464         SDL_SetError("XAudio2: Couldn't create mastering voice");
   465         return 0;
   466     }
   467 
   468     SDL_zero(waveformat);
   469     if (SDL_AUDIO_ISFLOAT(this->spec.format)) {
   470         waveformat.wFormatTag = WAVE_FORMAT_IEEE_FLOAT;
   471     } else {
   472         waveformat.wFormatTag = WAVE_FORMAT_PCM;
   473     }
   474     waveformat.wBitsPerSample = SDL_AUDIO_BITSIZE(this->spec.format);
   475     waveformat.nChannels = this->spec.channels;
   476     waveformat.nSamplesPerSec = this->spec.freq;
   477     waveformat.nBlockAlign =
   478         waveformat.nChannels * (waveformat.wBitsPerSample / 8);
   479     waveformat.nAvgBytesPerSec =
   480         waveformat.nSamplesPerSec * waveformat.nBlockAlign;
   481 
   482     result = IXAudio2_CreateSourceVoice(ixa2, &source, &waveformat,
   483                                         XAUDIO2_VOICE_NOSRC |
   484                                         XAUDIO2_VOICE_NOPITCH,
   485                                         1.0f, &callbacks, NULL, NULL);
   486     if (result != S_OK) {
   487         XAUDIO2_CloseDevice(this);
   488         SDL_SetError("XAudio2: Couldn't create source voice");
   489         return 0;
   490     }
   491     this->hidden->source = source;
   492 
   493     /* Start everything playing! */
   494     result = IXAudio2_StartEngine(ixa2);
   495     if (result != S_OK) {
   496         XAUDIO2_CloseDevice(this);
   497         SDL_SetError("XAudio2: Couldn't start engine");
   498         return 0;
   499     }
   500 
   501     result = IXAudio2SourceVoice_Start(source, 0, XAUDIO2_COMMIT_NOW);
   502     if (result != S_OK) {
   503         XAUDIO2_CloseDevice(this);
   504         SDL_SetError("XAudio2: Couldn't start source voice");
   505         return 0;
   506     }
   507 
   508     return 1; /* good to go. */
   509 }
   510 
   511 static void
   512 XAUDIO2_Deinitialize(void)
   513 {
   514 #if defined(__WIN32__)
   515     WIN_CoUninitialize();
   516 #endif
   517 }
   518 
   519 #endif  /* SDL_XAUDIO2_HAS_SDK */
   520 
   521 
   522 static int
   523 XAUDIO2_Init(SDL_AudioDriverImpl * impl)
   524 {
   525 #ifndef SDL_XAUDIO2_HAS_SDK
   526     SDL_SetError("XAudio2: SDL was built without XAudio2 support (old DirectX SDK).");
   527     return 0;  /* no XAudio2 support, ever. Update your SDK! */
   528 #else
   529     /* XAudio2Create() is a macro that uses COM; we don't load the .dll */
   530     IXAudio2 *ixa2 = NULL;
   531 #if defined(__WIN32__)
   532     if (FAILED(WIN_CoInitialize())) {
   533         SDL_SetError("XAudio2: CoInitialize() failed");
   534         return 0;
   535     }
   536 #endif
   537 
   538     if (XAudio2Create(&ixa2, 0, XAUDIO2_DEFAULT_PROCESSOR) != S_OK) {
   539 #if defined(__WIN32__)
   540         WIN_CoUninitialize();
   541 #endif
   542         SDL_SetError("XAudio2: XAudio2Create() failed");
   543         return 0;  /* not available. */
   544     }
   545     IXAudio2_Release(ixa2);
   546 
   547     /* Set the function pointers */
   548     impl->DetectDevices = XAUDIO2_DetectDevices;
   549     impl->OpenDevice = XAUDIO2_OpenDevice;
   550     impl->PlayDevice = XAUDIO2_PlayDevice;
   551     impl->WaitDevice = XAUDIO2_WaitDevice;
   552     impl->WaitDone = XAUDIO2_WaitDone;
   553     impl->GetDeviceBuf = XAUDIO2_GetDeviceBuf;
   554     impl->CloseDevice = XAUDIO2_CloseDevice;
   555     impl->Deinitialize = XAUDIO2_Deinitialize;
   556 
   557     return 1;   /* this audio target is available. */
   558 #endif
   559 }
   560 
   561 #if defined(__cplusplus)
   562 extern "C"
   563 #endif
   564 AudioBootStrap XAUDIO2_bootstrap = {
   565     "xaudio2", "XAudio2", XAUDIO2_Init, 0
   566 };
   567 
   568 #endif  /* SDL_AUDIO_DRIVER_XAUDIO2 */
   569 
   570 /* vi: set ts=4 sw=4 expandtab: */