|
MythTV
0.26-pre
|
00001 #include <iostream> 00002 #include <cmath> 00003 00004 using namespace std; 00005 00006 #include "mythlogging.h" 00007 #include "audiooutputdx.h" 00008 00009 #include <windows.h> 00010 #include <mmsystem.h> 00011 #include <dsound.h> 00012 #include <unistd.h> 00013 00014 #define LOC QString("AODX: ") 00015 00016 #include <initguid.h> 00017 DEFINE_GUID(IID_IDirectSoundNotify, 0xb0210783, 0x89cd, 0x11d0, 00018 0xaf, 0x8, 0x0, 0xa0, 0xc9, 0x25, 0xcd, 0x16); 00019 00020 #ifndef WAVE_FORMAT_DOLBY_AC3_SPDIF 00021 #define WAVE_FORMAT_DOLBY_AC3_SPDIF 0x0092 00022 #endif 00023 00024 #ifndef WAVE_FORMAT_IEEE_FLOAT 00025 #define WAVE_FORMAT_IEEE_FLOAT 0x0003 00026 #endif 00027 00028 #ifndef WAVE_FORMAT_EXTENSIBLE 00029 #define WAVE_FORMAT_EXTENSIBLE 0xFFFE 00030 #endif 00031 00032 #ifndef _WAVEFORMATEXTENSIBLE_ 00033 typedef struct { 00034 WAVEFORMATEX Format; 00035 union { 00036 WORD wValidBitsPerSample; // bits of precision 00037 WORD wSamplesPerBlock; // valid if wBitsPerSample==0 00038 WORD wReserved; // If neither applies, set to zero 00039 } Samples; 00040 DWORD dwChannelMask; // which channels are present in stream 00041 GUID SubFormat; 00042 } WAVEFORMATEXTENSIBLE, *PWAVEFORMATEXTENSIBLE; 00043 #endif 00044 00045 DEFINE_GUID(_KSDATAFORMAT_SUBTYPE_IEEE_FLOAT, WAVE_FORMAT_IEEE_FLOAT, 00046 0x0000, 0x0010, 0x80, 0x00, 0x00, 0xaa, 0x00, 0x38, 0x9b, 0x71); 00047 DEFINE_GUID(_KSDATAFORMAT_SUBTYPE_PCM, WAVE_FORMAT_PCM, 00048 0x0000, 0x0010, 0x80, 0x00, 0x00, 0xaa, 0x00, 0x38, 0x9b, 0x71); 00049 DEFINE_GUID(_KSDATAFORMAT_SUBTYPE_DOLBY_AC3_SPDIF, WAVE_FORMAT_DOLBY_AC3_SPDIF, 00050 0x0000, 0x0010, 0x80, 0x00, 0x00, 0xaa, 0x00, 0x38, 0x9b, 0x71); 00051 00052 class AudioOutputDXPrivate 00053 { 00054 public: 00055 AudioOutputDXPrivate(AudioOutputDX *in_parent) : 00056 parent(in_parent), 00057 dsound_dll(NULL), 00058 dsobject(NULL), 00059 dsbuffer(NULL), 00060 playStarted(false), 00061 writeCursor(0), 00062 chosenGUID(NULL), 00063 device_count(0), 00064 device_num(0) 00065 { 00066 } 00067 00068 ~AudioOutputDXPrivate() 00069 { 00070 DestroyDSBuffer(); 00071 00072 if (dsobject) 00073 IDirectSound_Release(dsobject); 00074 00075 if (dsound_dll) 00076 FreeLibrary(dsound_dll); 00077 } 00078 00079 int InitDirectSound(bool passthrough = false); 00080 void ResetDirectSound(void); 00081 void DestroyDSBuffer(void); 00082 void FillBuffer(unsigned char *buffer, int size); 00083 bool StartPlayback(void); 00084 static int CALLBACK DSEnumCallback(LPGUID lpGuid, 00085 LPCSTR lpcstrDesc, 00086 LPCSTR lpcstrModule, 00087 LPVOID lpContext); 00088 00089 public: 00090 AudioOutputDX *parent; 00091 HINSTANCE dsound_dll; 00092 LPDIRECTSOUND dsobject; 00093 LPDIRECTSOUNDBUFFER dsbuffer; 00094 bool playStarted; 00095 DWORD writeCursor; 00096 GUID deviceGUID, *chosenGUID; 00097 int device_count, device_num; 00098 QString device_name; 00099 QMap<int, QString> device_list; 00100 }; 00101 00102 00103 AudioOutputDX::AudioOutputDX(const AudioSettings &settings) : 00104 AudioOutputBase(settings), 00105 m_priv(new AudioOutputDXPrivate(this)), 00106 m_UseSPDIF(settings.use_passthru) 00107 { 00108 timeBeginPeriod(1); 00109 InitSettings(settings); 00110 if (passthru_device == "auto" || passthru_device.toLower() == "default") 00111 passthru_device = main_device; 00112 else 00113 m_discretedigital = true; 00114 if (settings.init) 00115 Reconfigure(settings); 00116 } 00117 00118 AudioOutputDX::~AudioOutputDX() 00119 { 00120 KillAudio(); 00121 if (m_priv) 00122 { 00123 delete m_priv; 00124 m_priv = NULL; 00125 } 00126 timeEndPeriod(1); 00127 } 00128 00129 typedef HRESULT (WINAPI *LPFNDSC) (LPGUID, LPDIRECTSOUND *, LPUNKNOWN); 00130 typedef HRESULT (WINAPI *LPFNDSE) (LPDSENUMCALLBACK, LPVOID); 00131 00132 int CALLBACK AudioOutputDXPrivate::DSEnumCallback(LPGUID lpGuid, 00133 LPCSTR lpcstrDesc, 00134 LPCSTR lpcstrModule, 00135 LPVOID lpContext) 00136 { 00137 const QString enum_desc = lpcstrDesc; 00138 AudioOutputDXPrivate *context = static_cast<AudioOutputDXPrivate*>(lpContext); 00139 const QString cfg_desc = context->device_name; 00140 const int device_num = context->device_num; 00141 const int device_count = context->device_count; 00142 00143 VBAUDIO(QString("Device %1:" + enum_desc).arg(device_count)); 00144 00145 if ((device_num == device_count || 00146 (device_num == 0 && !cfg_desc.isEmpty() && 00147 enum_desc.startsWith(cfg_desc, Qt::CaseInsensitive))) && lpGuid) 00148 { 00149 context->deviceGUID = *lpGuid; 00150 context->chosenGUID = 00151 &(context->deviceGUID); 00152 context->device_name = enum_desc; 00153 context->device_num = device_count; 00154 } 00155 00156 context->device_list.insert(device_count, enum_desc); 00157 context->device_count++; 00158 return 1; 00159 } 00160 00161 void AudioOutputDXPrivate::ResetDirectSound(void) 00162 { 00163 DestroyDSBuffer(); 00164 00165 if (dsobject) 00166 { 00167 IDirectSound_Release(dsobject); 00168 dsobject = NULL; 00169 } 00170 00171 if (dsound_dll) 00172 { 00173 FreeLibrary(dsound_dll); 00174 dsound_dll = NULL; 00175 } 00176 00177 chosenGUID = NULL; 00178 device_count = 0; 00179 device_num = 0; 00180 device_list.clear(); 00181 } 00182 00183 int AudioOutputDXPrivate::InitDirectSound(bool passthrough) 00184 { 00185 LPFNDSC OurDirectSoundCreate; 00186 LPFNDSE OurDirectSoundEnumerate; 00187 bool ok; 00188 00189 ResetDirectSound(); 00190 00191 dsound_dll = LoadLibrary("DSOUND.DLL"); 00192 if (dsound_dll == NULL) 00193 { 00194 VBERROR("Cannot open DSOUND.DLL"); 00195 goto error; 00196 } 00197 00198 if (parent) // parent can be NULL only when called from GetDXDevices() 00199 device_name = passthrough ? 00200 parent->passthru_device : parent->main_device; 00201 device_name = device_name.section(':', 1); 00202 device_num = device_name.toInt(&ok, 10); 00203 00204 VBAUDIO(QString("Looking for device num:%1 or name:%2") 00205 .arg(device_num).arg(device_name)); 00206 00207 OurDirectSoundEnumerate = 00208 (LPFNDSE)GetProcAddress(dsound_dll, "DirectSoundEnumerateA"); 00209 00210 if(OurDirectSoundEnumerate) 00211 if(FAILED(OurDirectSoundEnumerate(DSEnumCallback, this))) 00212 VBERROR("DirectSoundEnumerate FAILED"); 00213 00214 if (!chosenGUID) 00215 { 00216 device_num = 0; 00217 device_name = "Primary Sound Driver"; 00218 } 00219 00220 VBAUDIO(QString("Using device %1:%2").arg(device_num).arg(device_name)); 00221 00222 OurDirectSoundCreate = 00223 (LPFNDSC)GetProcAddress(dsound_dll, "DirectSoundCreate"); 00224 00225 if (OurDirectSoundCreate == NULL) 00226 { 00227 VBERROR("GetProcAddress FAILED"); 00228 goto error; 00229 } 00230 00231 if (FAILED(OurDirectSoundCreate(chosenGUID, &dsobject, NULL))) 00232 { 00233 VBERROR("Cannot create a direct sound device"); 00234 goto error; 00235 } 00236 00237 /* Set DirectSound Cooperative level, ie what control we want over Windows 00238 * sound device. In our case, DSSCL_EXCLUSIVE means that we can modify the 00239 * settings of the primary buffer, but also that only the sound of our 00240 * application will be hearable when it will have the focus. 00241 * !!! (this is not really working as intended yet because to set the 00242 * cooperative level you need the window handle of your application, and 00243 * I don't know of any easy way to get it. Especially since we might play 00244 * sound without any video, and so what window handle should we use ??? 00245 * The hack for now is to use the Desktop window handle - it seems to be 00246 * working */ 00247 if (FAILED(IDirectSound_SetCooperativeLevel(dsobject, GetDesktopWindow(), 00248 DSSCL_EXCLUSIVE))) 00249 VBERROR("Cannot set DS cooperative level"); 00250 00251 VBAUDIO("Initialised DirectSound"); 00252 00253 return 0; 00254 00255 error: 00256 dsobject = NULL; 00257 if (dsound_dll) 00258 { 00259 FreeLibrary(dsound_dll); 00260 dsound_dll = NULL; 00261 } 00262 return -1; 00263 } 00264 00265 void AudioOutputDXPrivate::DestroyDSBuffer(void) 00266 { 00267 if (!dsbuffer) 00268 return; 00269 00270 VBAUDIO("Destroying DirectSound buffer"); 00271 IDirectSoundBuffer_Stop(dsbuffer); 00272 writeCursor = 0; 00273 IDirectSoundBuffer_SetCurrentPosition(dsbuffer, writeCursor); 00274 playStarted = false; 00275 IDirectSoundBuffer_Release(dsbuffer); 00276 dsbuffer = NULL; 00277 } 00278 00279 void AudioOutputDXPrivate::FillBuffer(unsigned char *buffer, int size) 00280 { 00281 void *p_write_position, *p_wrap_around; 00282 DWORD l_bytes1, l_bytes2, play_pos, write_pos; 00283 HRESULT dsresult; 00284 00285 if (!dsbuffer) 00286 return; 00287 00288 while (true) 00289 { 00290 00291 dsresult = IDirectSoundBuffer_GetCurrentPosition(dsbuffer, 00292 &play_pos, &write_pos); 00293 if (dsresult != DS_OK) 00294 { 00295 VBERROR("Cannot get current buffer position"); 00296 return; 00297 } 00298 00299 VBAUDIOTS(QString("play: %1 write: %2 wcursor: %3") 00300 .arg(play_pos).arg(write_pos).arg(writeCursor)); 00301 00302 while ((writeCursor < write_pos && 00303 ((writeCursor >= play_pos && write_pos >= play_pos) || 00304 (writeCursor < play_pos && write_pos < play_pos))) || 00305 (writeCursor >= play_pos && write_pos < play_pos)) 00306 { 00307 VBERROR("buffer underrun"); 00308 writeCursor += size; 00309 while (writeCursor >= (DWORD)parent->soundcard_buffer_size) 00310 writeCursor -= parent->soundcard_buffer_size; 00311 } 00312 00313 if ((writeCursor < play_pos && writeCursor + size >= play_pos) || 00314 (writeCursor >= play_pos && 00315 writeCursor + size >= play_pos + parent->soundcard_buffer_size)) 00316 { 00317 usleep(50000); 00318 continue; 00319 } 00320 00321 break; 00322 } 00323 00324 dsresult = IDirectSoundBuffer_Lock( 00325 dsbuffer, /* DS buffer */ 00326 writeCursor, /* Start offset */ 00327 size, /* Number of bytes */ 00328 &p_write_position, /* Address of lock start */ 00329 &l_bytes1, /* Bytes locked before wrap */ 00330 &p_wrap_around, /* Buffer address (if wrap around) */ 00331 &l_bytes2, /* Count of bytes after wrap around */ 00332 0); /* Flags */ 00333 00334 if (dsresult == DSERR_BUFFERLOST) 00335 { 00336 IDirectSoundBuffer_Restore(dsbuffer); 00337 dsresult = IDirectSoundBuffer_Lock(dsbuffer, writeCursor, size, 00338 &p_write_position, &l_bytes1, 00339 &p_wrap_around, &l_bytes2, 0); 00340 } 00341 00342 if (dsresult != DS_OK) 00343 { 00344 VBERROR("Cannot lock buffer, audio dropped"); 00345 return; 00346 } 00347 00348 memcpy(p_write_position, buffer, l_bytes1); 00349 if (p_wrap_around) 00350 memcpy(p_wrap_around, buffer + l_bytes1, l_bytes2); 00351 00352 writeCursor += l_bytes1 + l_bytes2; 00353 00354 while (writeCursor >= (DWORD)parent->soundcard_buffer_size) 00355 writeCursor -= parent->soundcard_buffer_size; 00356 00357 IDirectSoundBuffer_Unlock(dsbuffer, p_write_position, l_bytes1, 00358 p_wrap_around, l_bytes2); 00359 } 00360 00361 bool AudioOutputDXPrivate::StartPlayback(void) 00362 { 00363 HRESULT dsresult; 00364 00365 dsresult = IDirectSoundBuffer_Play(dsbuffer, 0, 0, DSBPLAY_LOOPING); 00366 if (dsresult == DSERR_BUFFERLOST) 00367 { 00368 IDirectSoundBuffer_Restore(dsbuffer); 00369 dsresult = IDirectSoundBuffer_Play(dsbuffer, 0, 0, DSBPLAY_LOOPING); 00370 } 00371 if (dsresult != DS_OK) 00372 { 00373 VBERROR("Cannot start playing buffer"); 00374 playStarted = false; 00375 return false; 00376 } 00377 00378 playStarted=true; 00379 return true; 00380 } 00381 00382 AudioOutputSettings* AudioOutputDX::GetOutputSettings(bool passthrough) 00383 { 00384 AudioOutputSettings *settings = new AudioOutputSettings(); 00385 DSCAPS devcaps; 00386 devcaps.dwSize = sizeof(DSCAPS); 00387 00388 m_priv->InitDirectSound(passthrough); 00389 if ((!m_priv->dsobject || !m_priv->dsound_dll) || 00390 FAILED(IDirectSound_GetCaps(m_priv->dsobject, &devcaps)) ) 00391 { 00392 delete settings; 00393 return NULL; 00394 } 00395 00396 VBAUDIO(QString("GetCaps sample rate min: %1 max: %2") 00397 .arg(devcaps.dwMinSecondarySampleRate) 00398 .arg(devcaps.dwMaxSecondarySampleRate)); 00399 00400 /* We shouldn't assume we can do everything between min and max but 00401 there's no way to test individual rates, so we'll have to */ 00402 while (DWORD rate = (DWORD)settings->GetNextRate()) 00403 if((rate >= devcaps.dwMinSecondarySampleRate) || 00404 (rate <= devcaps.dwMaxSecondarySampleRate)) 00405 settings->AddSupportedRate(rate); 00406 00407 /* We can only test for 8 and 16 bit support, DS uses float internally 00408 Guess that we can support S24 and S32 too */ 00409 if (devcaps.dwFlags & DSCAPS_PRIMARY8BIT) 00410 settings->AddSupportedFormat(FORMAT_U8); 00411 if (devcaps.dwFlags & DSCAPS_PRIMARY16BIT) 00412 settings->AddSupportedFormat(FORMAT_S16); 00413 settings->AddSupportedFormat(FORMAT_S24); 00414 settings->AddSupportedFormat(FORMAT_S32); 00415 settings->AddSupportedFormat(FORMAT_FLT); 00416 00417 /* No way to test anything other than mono or stereo, guess that we can do 00418 up to 5.1 */ 00419 for (uint i = 2; i < 7; i++) 00420 settings->AddSupportedChannels(i); 00421 00422 settings->setPassthrough(0); // Maybe passthrough 00423 00424 return settings; 00425 } 00426 00427 bool AudioOutputDX::OpenDevice(void) 00428 { 00429 WAVEFORMATEXTENSIBLE wf; 00430 DSBUFFERDESC dsbdesc; 00431 00432 CloseDevice(); 00433 00434 m_UseSPDIF = passthru || enc; 00435 m_priv->InitDirectSound(m_UseSPDIF); 00436 if (!m_priv->dsobject || !m_priv->dsound_dll) 00437 { 00438 Error("DirectSound initialization failed"); 00439 return false; 00440 } 00441 00442 // fragments are 50ms worth of samples 00443 fragment_size = 50 * output_bytes_per_frame * samplerate / 1000; 00444 // DirectSound buffer holds 4 fragments = 200ms worth of samples 00445 soundcard_buffer_size = fragment_size << 2; 00446 00447 VBAUDIO(QString("DirectSound buffer size: %1").arg(soundcard_buffer_size)); 00448 00449 wf.Format.nChannels = channels; 00450 wf.Format.nSamplesPerSec = samplerate; 00451 wf.Format.nBlockAlign = output_bytes_per_frame; 00452 wf.Format.nAvgBytesPerSec = samplerate * output_bytes_per_frame; 00453 wf.Format.wBitsPerSample = (output_bytes_per_frame << 3) / channels; 00454 wf.Samples.wValidBitsPerSample = 00455 AudioOutputSettings::FormatToBits(output_format); 00456 00457 if (m_UseSPDIF) 00458 { 00459 wf.Format.wFormatTag = WAVE_FORMAT_DOLBY_AC3_SPDIF; 00460 wf.SubFormat = _KSDATAFORMAT_SUBTYPE_DOLBY_AC3_SPDIF; 00461 } 00462 else if (output_format == FORMAT_FLT) 00463 { 00464 wf.Format.wFormatTag = WAVE_FORMAT_IEEE_FLOAT; 00465 wf.SubFormat = _KSDATAFORMAT_SUBTYPE_IEEE_FLOAT; 00466 } 00467 else 00468 { 00469 wf.Format.wFormatTag = WAVE_FORMAT_PCM; 00470 wf.SubFormat = _KSDATAFORMAT_SUBTYPE_PCM; 00471 } 00472 00473 VBAUDIO(QString("New format: %1bits %2ch %3Hz %4") 00474 .arg(wf.Samples.wValidBitsPerSample).arg(channels) 00475 .arg(samplerate).arg(m_UseSPDIF ? "data" : "PCM")); 00476 00477 if (channels <= 2) 00478 wf.Format.cbSize = 0; 00479 else 00480 { 00481 wf.Format.wFormatTag = WAVE_FORMAT_EXTENSIBLE; 00482 wf.dwChannelMask = 0x003F; // 0x003F = 5.1 channels 00483 wf.Format.cbSize = sizeof(WAVEFORMATEXTENSIBLE) - sizeof(WAVEFORMATEX); 00484 } 00485 00486 memset(&dsbdesc, 0, sizeof(DSBUFFERDESC)); 00487 dsbdesc.dwSize = sizeof(DSBUFFERDESC); 00488 dsbdesc.dwFlags = DSBCAPS_GETCURRENTPOSITION2 // Better position accuracy 00489 | DSBCAPS_GLOBALFOCUS // Allows background playing 00490 | DSBCAPS_LOCHARDWARE; // Needed for 5.1 on emu101k 00491 00492 if (!m_UseSPDIF) 00493 dsbdesc.dwFlags |= DSBCAPS_CTRLVOLUME; // Allow volume control 00494 00495 dsbdesc.dwBufferBytes = soundcard_buffer_size; // buffer size 00496 dsbdesc.lpwfxFormat = (WAVEFORMATEX *)&wf; 00497 00498 if (FAILED(IDirectSound_CreateSoundBuffer(m_priv->dsobject, &dsbdesc, 00499 &m_priv->dsbuffer, NULL))) 00500 { 00501 /* Vista does not support hardware mixing 00502 try without DSBCAPS_LOCHARDWARE */ 00503 dsbdesc.dwFlags &= ~DSBCAPS_LOCHARDWARE; 00504 HRESULT dsresult = 00505 IDirectSound_CreateSoundBuffer(m_priv->dsobject, &dsbdesc, 00506 &m_priv->dsbuffer, NULL); 00507 if (FAILED(dsresult)) 00508 { 00509 if (dsresult == DSERR_UNSUPPORTED) 00510 Error(QString("Unsupported format for device %1:%2") 00511 .arg(m_priv->device_num).arg(m_priv->device_name)); 00512 else 00513 Error(QString("Failed to create DS buffer 0x%1") 00514 .arg((DWORD)dsresult, 0, 16)); 00515 return false; 00516 } 00517 VBAUDIO("Using software mixer"); 00518 } 00519 VBAUDIO("Created DirectSound buffer"); 00520 00521 return true; 00522 } 00523 00524 void AudioOutputDX::CloseDevice(void) 00525 { 00526 if (m_priv->dsbuffer) 00527 m_priv->DestroyDSBuffer(); 00528 } 00529 00530 void AudioOutputDX::WriteAudio(unsigned char * buffer, int size) 00531 { 00532 if (size == 0) 00533 return; 00534 00535 m_priv->FillBuffer(buffer, size); 00536 if (!m_priv->playStarted) 00537 m_priv->StartPlayback(); 00538 } 00539 00540 int AudioOutputDX::GetBufferedOnSoundcard(void) const 00541 { 00542 if (!m_priv->playStarted) 00543 return 0; 00544 00545 HRESULT dsresult; 00546 DWORD play_pos, write_pos; 00547 int buffered; 00548 00549 dsresult = IDirectSoundBuffer_GetCurrentPosition(m_priv->dsbuffer, 00550 &play_pos, &write_pos); 00551 if (dsresult != DS_OK) 00552 { 00553 VBERROR("Cannot get current buffer position"); 00554 return 0; 00555 } 00556 00557 buffered = (int)m_priv->writeCursor - (int)play_pos; 00558 00559 if (buffered <= 0) 00560 buffered += soundcard_buffer_size; 00561 00562 return buffered; 00563 } 00564 00565 int AudioOutputDX::GetVolumeChannel(int channel) const 00566 { 00567 HRESULT dsresult; 00568 long dxVolume = 0; 00569 int volume; 00570 00571 if (m_UseSPDIF) 00572 return 100; 00573 00574 dsresult = IDirectSoundBuffer_GetVolume(m_priv->dsbuffer, &dxVolume); 00575 volume = (int)(pow(10,(float)dxVolume/20)*100); 00576 00577 if (dsresult != DS_OK) 00578 { 00579 VBERROR(QString("Failed to get volume %1").arg(dxVolume)); 00580 return volume; 00581 } 00582 00583 VBAUDIO(QString("Got volume %1").arg(volume)); 00584 return volume; 00585 } 00586 00587 void AudioOutputDX::SetVolumeChannel(int channel, int volume) 00588 { 00589 HRESULT dsresult; 00590 float dbAtten = 20 * log10((float)volume/100); 00591 long dxVolume = (volume == 0) ? DSBVOLUME_MIN : (long)(100.0f * dbAtten); 00592 00593 if (m_UseSPDIF) 00594 return; 00595 00596 // dxVolume is attenuation in 100ths of a decibel 00597 dsresult = IDirectSoundBuffer_SetVolume(m_priv->dsbuffer, dxVolume); 00598 00599 if (dsresult != DS_OK) 00600 { 00601 VBERROR(QString("Failed to set volume %1").arg(dxVolume)); 00602 return; 00603 } 00604 00605 VBAUDIO(QString("Set volume %1").arg(dxVolume)); 00606 } 00607 00608 QMap<int, QString> *AudioOutputDX::GetDXDevices(void) 00609 { 00610 AudioOutputDXPrivate *tmp_priv = new AudioOutputDXPrivate(NULL); 00611 tmp_priv->InitDirectSound(false); 00612 QMap<int, QString> *dxdevs = new QMap<int, QString>(tmp_priv->device_list); 00613 delete tmp_priv; 00614 return dxdevs; 00615 } 00616 00617 /* vim: set expandtab tabstop=4 shiftwidth=4: */
1.7.6.1