source: trunk/src/winmm/dwaveout.cpp@ 588

Last change on this file since 588 was 588, checked in by phaller, 26 years ago

Add: added ODINWRAP support for WINMM

File size: 19.2 KB
Line 
1/* $Id: dwaveout.cpp,v 1.5 1999-08-19 18:46:04 phaller Exp $ */
2
3/*
4 * Wave playback class
5 *
6 * Copyright 1998 Sander van Leeuwen (sandervl@xs4all.nl)
7 *
8 *
9 * Project Odin Software License can be found in LICENSE.TXT
10 *
11 */
12
13
14/****************************************************************************
15 * Includes *
16 ****************************************************************************/
17
18#define INCL_BASE
19#define INCL_OS2MM
20#include <os2wrap.h> //Odin32 OS/2 api wrappers
21#include <os2me.h>
22#include <stdlib.h>
23#include <string.h>
24
25#define OS2_ONLY
26#include "win32type.h"
27
28#include "misc.h"
29#include "dwaveout.h"
30
31#ifndef min
32#define min(a, b) ((a > b) ? b : a)
33#endif
34
35LONG APIENTRY WaveOutHandler(ULONG ulStatus, PMCI_MIX_BUFFER pBuffer, ULONG ulFlags);
36
37//TODO: mulaw, alaw & adpcm
38/******************************************************************************/
39/******************************************************************************/
40DartWaveOut::DartWaveOut(LPWAVEFORMATEX pwfx)
41{
42 Init(pwfx);
43}
44/******************************************************************************/
45/******************************************************************************/
46DartWaveOut::DartWaveOut(LPWAVEFORMATEX pwfx, ULONG nCallback, ULONG dwInstance)
47{
48 Init(pwfx);
49
50 callback = (LPDRVCALLBACK)nCallback;
51 this->dwInstance = dwInstance;
52
53 callback((ULONG)this, WOM_OPEN, dwInstance, 0, 0);
54}
55/******************************************************************************/
56/******************************************************************************/
57DartWaveOut::DartWaveOut(LPWAVEFORMATEX pwfx, HWND hwndCallback)
58{
59 Init(pwfx);
60
61 this->hwndCallback = hwndCallback;
62
63 WinPostMsg(hwndCallback, WOM_OPEN, 0, 0);
64}
65/******************************************************************************/
66/******************************************************************************/
67void DartWaveOut::Init(LPWAVEFORMATEX pwfx)
68{
69 MCI_GENERIC_PARMS GenericParms;
70 MCI_AMP_OPEN_PARMS AmpOpenParms;
71 APIRET rc;
72
73 curPlayBuf = curFillBuf = curFillPos = curPlayPos = 0;
74
75 fMixerSetup = FALSE;
76 next = NULL;
77 wavehdr = NULL;
78 curhdr = NULL;
79 callback = NULL;
80 hwndCallback = 0;
81 dwInstance = 0;
82 ulError = 0;
83 State = STATE_STOPPED;
84
85 MixBuffer = (MCI_MIX_BUFFER *)malloc(PREFILLBUF_DART*sizeof(MCI_MIX_BUFFER));
86 MixSetupParms = (MCI_MIXSETUP_PARMS *)malloc(sizeof(MCI_MIXSETUP_PARMS));
87 BufferParms = (MCI_BUFFER_PARMS *)malloc(sizeof(MCI_BUFFER_PARMS));
88
89 switch(pwfx->nBlockAlign) {
90 case 1://8 bits mono
91 BitsPerSample = 8;
92 break;
93 case 2://16 bits mono or 8 bits stereo
94 if(nChannels == 1)
95 BitsPerSample = 16;
96 else BitsPerSample = 8;
97 break;
98 case 4://16 bits stereo
99 BitsPerSample = 16;
100 break;
101 }
102 SampleRate = pwfx->nSamplesPerSec;
103 this->nChannels = pwfx->nChannels;
104 ulBufSize = DART_BUFSIZE;
105
106 // Setup the open structure, pass the playlist and tell MCI_OPEN to use it
107 memset(&AmpOpenParms,0,sizeof(AmpOpenParms));
108
109 AmpOpenParms.usDeviceID = ( USHORT ) 0;
110 AmpOpenParms.pszDeviceType = ( PSZ ) MCI_DEVTYPE_AUDIO_AMPMIX;
111
112 rc = mciSendCommand(0, MCI_OPEN,
113 MCI_WAIT | MCI_OPEN_TYPE_ID | MCI_OPEN_SHAREABLE,
114 (PVOID) &AmpOpenParms,
115 0);
116 DeviceId = AmpOpenParms.usDeviceID;
117 if(rc) {
118#ifdef DEBUG
119 WriteLog("MCI_OPEN failed\n");
120#endif
121 mciError(rc);
122 ulError = MMSYSERR_NOTENABLED;
123 }
124 if(rc == 0) {
125 //Grab exclusive rights to device instance (NOT entire device)
126 GenericParms.hwndCallback = 0; //Not needed, so set to 0
127 rc = mciSendCommand(DeviceId, MCI_ACQUIREDEVICE, MCI_EXCLUSIVE_INSTANCE,
128 (PVOID)&GenericParms, 0);
129 if(rc) {
130#ifdef DEBUG
131 WriteLog("MCI_ACQUIREDEVICE failed\n");
132#endif
133 mciError(rc);
134 ulError = MMSYSERR_NOTENABLED;
135 }
136 }
137 State = STATE_STOPPED;
138
139 wmutex = new VMutex();
140 if(wmutex == NULL) {
141 ulError = MMSYSERR_NOTSUPPORTED;
142 }
143 if(wmutex)
144 wmutex->enter(VMUTEX_WAIT_FOREVER);
145
146 if(waveout == NULL) {
147 waveout = this;
148 }
149 else {
150 DartWaveOut *dwave = waveout;
151
152 while(dwave->next) {
153 dwave = dwave->next;
154 }
155 dwave->next = this;
156 }
157
158 if(wmutex)
159 wmutex->leave();
160}
161/******************************************************************************/
162/******************************************************************************/
163DartWaveOut::~DartWaveOut()
164{
165 MCI_GENERIC_PARMS GenericParms;
166
167 // Generic parameters
168 GenericParms.hwndCallback = 0; //hwndFrame
169
170 // Stop the playback.
171 mciSendCommand(DeviceId, MCI_STOP,MCI_WAIT, (PVOID)&GenericParms,0);
172
173 mciSendCommand(DeviceId,
174 MCI_BUFFER,
175 MCI_WAIT | MCI_DEALLOCATE_MEMORY,
176 (PVOID)&BufferParms,
177 0);
178
179 // Generic parameters
180 GenericParms.hwndCallback = 0; //hwndFrame
181
182 // Close the device
183 mciSendCommand(DeviceId, MCI_CLOSE, MCI_WAIT, (PVOID)&GenericParms, 0);
184
185 if(wmutex)
186 wmutex->enter(VMUTEX_WAIT_FOREVER);
187
188 State = STATE_STOPPED;
189
190 if(waveout == this) {
191 waveout = this->next;
192 }
193 else {
194 DartWaveOut *dwave = waveout;
195
196 while(dwave->next != this) {
197 dwave = dwave->next;
198 }
199 dwave->next = this->next;
200 }
201 if(wmutex)
202 wmutex->leave();
203
204 if(callback) {
205 callback((ULONG)this, WOM_CLOSE, dwInstance, 0, 0);
206 }
207 else
208 if(hwndCallback)
209 WinPostMsg(hwndCallback, WOM_CLOSE, 0, 0);
210
211 if(wmutex)
212 delete wmutex;
213
214 if(MixBuffer)
215 free(MixBuffer);
216 if(MixSetupParms)
217 free(MixSetupParms);
218 if(BufferParms)
219 free(BufferParms);
220}
221/******************************************************************************/
222/******************************************************************************/
223MMRESULT DartWaveOut::getError()
224{
225 return(ulError);
226}
227/******************************************************************************/
228/******************************************************************************/
229MMRESULT DartWaveOut::write(LPWAVEHDR pwh, UINT cbwh)
230{
231 MCI_GENERIC_PARMS GenericParms = {0};
232 APIRET rc;
233 int i, buflength;
234
235 if(fMixerSetup == FALSE) {
236#ifdef DEBUG
237 WriteLog("device acquired\n");
238#endif
239 /* Set the MixSetupParms data structure to match the loaded file.
240 * This is a global that is used to setup the mixer.
241 */
242 memset(MixSetupParms, 0, sizeof( MCI_MIXSETUP_PARMS ) );
243
244 MixSetupParms->ulBitsPerSample = BitsPerSample;
245 MixSetupParms->ulSamplesPerSec = SampleRate;
246 MixSetupParms->ulFormatTag = MCI_WAVE_FORMAT_PCM;
247 MixSetupParms->ulChannels = nChannels;
248
249#ifdef DEBUG
250 WriteLog("bps %d, sps %d chan %d\n", BitsPerSample, SampleRate, nChannels);
251#endif
252
253 /* Setup the mixer for playback of wave data
254 */
255 MixSetupParms->ulFormatMode = MCI_PLAY;
256 MixSetupParms->ulDeviceType = MCI_DEVTYPE_WAVEFORM_AUDIO;
257 MixSetupParms->pmixEvent = WaveOutHandler;
258
259 rc = mciSendCommand(DeviceId,
260 MCI_MIXSETUP,
261 MCI_WAIT | MCI_MIXSETUP_INIT,
262 (PVOID)MixSetupParms,
263 0);
264
265 if ( rc != MCIERR_SUCCESS ) {
266 mciError(rc);
267 mciSendCommand(DeviceId, MCI_RELEASEDEVICE, MCI_WAIT,
268 (PVOID)&GenericParms, 0);
269 return(MMSYSERR_NOTSUPPORTED);
270 }
271
272 /*
273 * Set up the BufferParms data structure and allocate
274 * device buffers from the Amp-Mixer
275 */
276#ifdef DEBUG
277 WriteLog("mix setup %d, %d\n", pwh->dwBufferLength, pwh->dwBufferLength);
278#endif
279#if 1
280 ulBufSize = pwh->dwBufferLength/2;
281#else
282 if(pwh->dwBufferLength >= 512 && pwh->dwBufferLength <= 1024)
283 ulBufSize = pwh->dwBufferLength;
284 else ulBufSize = 1024;
285#endif
286
287 MixSetupParms->ulBufferSize = ulBufSize;
288
289 BufferParms->ulNumBuffers = PREFILLBUF_DART;
290 BufferParms->ulBufferSize = MixSetupParms->ulBufferSize;
291 BufferParms->pBufList = MixBuffer;
292
293 for(i=0;i<PREFILLBUF_DART;i++) {
294 MixBuffer[i].ulUserParm = (ULONG)this;
295 }
296
297 rc = mciSendCommand(DeviceId,
298 MCI_BUFFER,
299 MCI_WAIT | MCI_ALLOCATE_MEMORY,
300 (PVOID)BufferParms,
301 0);
302
303 if(ULONG_LOWD(rc) != MCIERR_SUCCESS) {
304 mciError(rc);
305 mciSendCommand(DeviceId, MCI_RELEASEDEVICE, MCI_WAIT,
306 (PVOID)&GenericParms, 0);
307 return(MMSYSERR_NOTSUPPORTED);
308 }
309
310 wmutex->enter(VMUTEX_WAIT_FOREVER);
311 fMixerSetup = TRUE;
312
313 curPlayBuf = curFillBuf = curFillPos = curPlayPos = 0;
314
315 for(i=0;i<PREFILLBUF_DART;i++) {
316 memset(MixBuffer[i].pBuffer, 0, MixBuffer[i].ulBufferLength);
317 }
318#ifdef DEBUG
319 WriteLog("Dart opened, bufsize = %d\n", MixBuffer[i].ulBufferLength);
320#endif
321
322 wavehdr = pwh;
323 curhdr = pwh;
324 pwh->lpNext = NULL;
325
326 while(TRUE) {
327 buflength = min((ULONG)MixBuffer[curFillBuf].ulBufferLength - curPlayPos,
328 (ULONG)wavehdr->dwBufferLength - curFillPos);
329#ifdef DEBUG
330 WriteLog("Copying %d data; curPlayPos = %d curFillPos = %d\n", buflength, curPlayPos, curFillPos);
331#endif
332 memcpy((char *)MixBuffer[curFillBuf].pBuffer + curPlayPos,
333 wavehdr->lpData + curFillPos,
334 buflength);
335
336 curPlayPos += buflength;
337 curFillPos += buflength;
338 if(curFillPos == wavehdr->dwBufferLength) {
339#ifdef DEBUG
340 WriteLog("Processed first win32 buffer\n");
341#endif
342 curFillPos = 0;
343 wavehdr->dwFlags |= WHDR_DONE;
344 curhdr = NULL;
345 }
346 if(curPlayPos == MixBuffer[curPlayBuf].ulBufferLength) {
347 if(++curPlayBuf == PREFILLBUF_DART) {
348 curPlayBuf = 0;
349 break;
350 }
351 curPlayPos = 0;
352 }
353 if(curFillPos == 0)
354 break;
355 }
356#ifdef DEBUG
357 WriteLog("MixSetupParms = %X\n", MixSetupParms);
358#endif
359 State = STATE_PLAYING;
360 wmutex->leave();
361
362 MixSetupParms->pmixWrite(MixSetupParms->ulMixHandle,
363 MixBuffer,
364 PREFILLBUF_DART);
365#ifdef DEBUG
366 WriteLog("Dart playing\n");
367#endif
368 }
369 else {
370 wmutex->enter(VMUTEX_WAIT_FOREVER);
371 pwh->lpNext = NULL;
372 if(wavehdr) {
373 WAVEHDR *chdr = wavehdr;
374 while(chdr->lpNext) {
375 chdr = chdr->lpNext;
376 }
377 chdr->lpNext = pwh;
378 }
379 else wavehdr = pwh;
380 wmutex->leave();
381 if(State != STATE_PLAYING) {//continue playback
382 restart();
383 }
384 }
385
386 return(MMSYSERR_NOERROR);
387}
388/******************************************************************************/
389/******************************************************************************/
390MMRESULT DartWaveOut::pause()
391{
392 MCI_GENERIC_PARMS Params;
393
394 if(State != STATE_PLAYING)
395 return(MMSYSERR_HANDLEBUSY);
396
397 wmutex->enter(VMUTEX_WAIT_FOREVER);
398 State = STATE_PAUSED;
399 wmutex->leave();
400
401 memset(&Params, 0, sizeof(Params));
402
403 // Stop the playback.
404 mciSendCommand(DeviceId, MCI_PAUSE, MCI_WAIT, (PVOID)&Params, 0);
405
406 return(MMSYSERR_NOERROR);
407}
408/******************************************************************************/
409/******************************************************************************/
410MMRESULT DartWaveOut::reset()
411{
412 MCI_GENERIC_PARMS Params;
413
414 dprintf(("DartWaveOut::reset %s", (State == STATE_PLAYING) ? "playing" : "stopped"));
415 if(State != STATE_PLAYING)
416 return(MMSYSERR_HANDLEBUSY);
417
418 memset(&Params, 0, sizeof(Params));
419
420 // Stop the playback.
421 mciSendCommand(DeviceId, MCI_STOP, MCI_WAIT, (PVOID)&Params, 0);
422
423#ifdef DEBUG
424 WriteLog("Nr of threads blocked on mutex = %d\n", wmutex->getNrBlocked());
425#endif
426
427 wmutex->enter(VMUTEX_WAIT_FOREVER);
428 while(wavehdr) {
429 wavehdr->dwFlags |= WHDR_DONE;
430 wmutex->leave();
431 if(callback) {
432 callback((ULONG)this, WOM_DONE, dwInstance, wavehdr->dwUser, (ULONG)wavehdr);
433 }
434 else
435 if(hwndCallback)
436 WinPostMsg(hwndCallback, WOM_DONE, (MPARAM)wavehdr->dwUser, (MPARAM)wavehdr);
437
438 wmutex->enter(VMUTEX_WAIT_FOREVER);
439 wavehdr = wavehdr->lpNext;
440 }
441 wavehdr = NULL;
442 State = STATE_STOPPED;
443
444 wmutex->leave();
445 return(MMSYSERR_NOERROR);
446}
447/******************************************************************************/
448/******************************************************************************/
449MMRESULT DartWaveOut::restart()
450{
451 dprintf(("DartWaveOut::restart"));
452 wmutex->enter(VMUTEX_WAIT_FOREVER);
453 State = STATE_PLAYING;
454 wmutex->leave();
455 MixSetupParms->pmixWrite(MixSetupParms->ulMixHandle,
456 &MixBuffer[curPlayBuf],
457 PREFILLBUF_DART);
458 return(MMSYSERR_NOERROR);
459}
460/******************************************************************************/
461/******************************************************************************/
462BOOL DartWaveOut::queryFormat(ULONG formatTag, ULONG nChannels,
463 ULONG nSamplesPerSec, ULONG sampleSize)
464{
465 MCI_WAVE_GETDEVCAPS_PARMS mciAudioCaps;
466 MCI_GENERIC_PARMS GenericParms;
467 MCI_OPEN_PARMS mciOpenParms; /* open parms for MCI_OPEN */
468 int i, freqbits = 0;
469 ULONG rc, DeviceId;
470 BOOL winrc;
471
472 memset(&mciOpenParms, /* Object to fill with zeros. */
473 0, /* Value to place into the object. */
474 sizeof( mciOpenParms ) ); /* How many zero's to use. */
475
476 mciOpenParms.pszDeviceType = (PSZ)MCI_DEVTYPE_WAVEFORM_AUDIO;
477
478 rc = mciSendCommand( (USHORT) 0,
479 MCI_OPEN,
480 MCI_WAIT | MCI_OPEN_TYPE_ID,
481 (PVOID) &mciOpenParms,
482 0);
483 if (rc != 0) {
484 return(FALSE);
485 }
486 DeviceId = mciOpenParms.usDeviceID;
487
488 memset( &mciAudioCaps , 0, sizeof(MCI_WAVE_GETDEVCAPS_PARMS));
489
490 switch(sampleSize) {
491 case 1:
492 mciAudioCaps.ulBitsPerSample = 8;
493 break;
494 case 2:
495 if(nChannels == 1)
496 mciAudioCaps.ulBitsPerSample = 16;
497 else mciAudioCaps.ulBitsPerSample = 8;
498 break;
499 case 4:
500 mciAudioCaps.ulBitsPerSample = 16;
501 break;
502 }
503 mciAudioCaps.ulFormatTag = DATATYPE_WAVEFORM;
504 mciAudioCaps.ulSamplesPerSec = nSamplesPerSec;
505 mciAudioCaps.ulChannels = nChannels;
506 mciAudioCaps.ulFormatMode = MCI_PLAY;
507 mciAudioCaps.ulItem = MCI_GETDEVCAPS_WAVE_FORMAT;
508
509 rc = mciSendCommand(DeviceId, /* Device ID */
510 MCI_GETDEVCAPS,
511 MCI_WAIT | MCI_GETDEVCAPS_EXTENDED | MCI_GETDEVCAPS_ITEM,
512 (PVOID) &mciAudioCaps,
513 0);
514 if((rc & 0xFFFF) != MCIERR_SUCCESS) {
515 mciError(rc);
516 winrc = FALSE;
517 }
518 // Close the device
519 mciSendCommand(DeviceId,MCI_CLOSE,MCI_WAIT,(PVOID)&GenericParms,0);
520 return(winrc);
521}
522/******************************************************************************/
523/******************************************************************************/
524void DartWaveOut::mciError(ULONG ulError)
525{
526#ifdef DEBUG
527 char szError[256] = "";
528
529 mciGetErrorString(ulError, szError, sizeof(szError));
530 WriteLog("WINMM: DartWaveOut: %s\n", szError);
531#endif
532}
533//******************************************************************************
534//******************************************************************************
535BOOL DartWaveOut::find(DartWaveOut *dwave)
536{
537 DartWaveOut *curwave = waveout;
538
539 while(curwave) {
540 if(dwave == curwave) {
541 return(TRUE);
542 }
543 curwave = curwave->next;
544 }
545
546#ifdef DEBUG
547 WriteLog("WINMM:DartWaveOut not found!\n");
548#endif
549 return(FALSE);
550}
551/******************************************************************************/
552/******************************************************************************/
553void DartWaveOut::handler(ULONG ulStatus, PMCI_MIX_BUFFER pBuffer, ULONG ulFlags)
554{
555 ULONG buflength;
556 WAVEHDR *whdr = wavehdr, *prevhdr = NULL;
557
558#ifdef DEBUG1
559 WriteLog("WINMM: handler %d\n", curPlayBuf);
560#endif
561 if(ulFlags == MIX_STREAM_ERROR) {
562 if(ulStatus == ERROR_DEVICE_UNDERRUN) {
563 dprintf(("WINMM: WaveOut handler UNDERRUN!\n"));
564 pause(); //out of buffers, so pause playback
565 return;
566 }
567 dprintf(("WINMM: WaveOut handler, Unknown error %X\n", ulStatus));
568 return;
569 }
570 wmutex->enter(VMUTEX_WAIT_FOREVER);
571
572 while(whdr) {
573 if(whdr->dwFlags & WHDR_DONE) {
574#ifdef DEBUG1
575 WriteLog("WINMM: handler buf %X done\n", whdr);
576#endif
577 whdr->dwFlags &= ~WHDR_INQUEUE;
578
579 if(prevhdr == NULL)
580 wavehdr = whdr->lpNext;
581 else prevhdr->lpNext = whdr->lpNext;
582
583 whdr->lpNext = NULL;
584 wmutex->leave();
585
586 if(callback) {
587 callback((ULONG)this, WOM_DONE, dwInstance, whdr->dwUser, (ULONG)whdr);
588 }
589 else
590 if(hwndCallback)
591 WinPostMsg(hwndCallback, WOM_DONE, (MPARAM)whdr->dwUser, (MPARAM)whdr);
592
593 wmutex->enter(VMUTEX_WAIT_FOREVER);
594 }
595 prevhdr = whdr;
596 whdr = whdr->lpNext;
597 }
598
599 if(curhdr == NULL)
600 curhdr = wavehdr;
601
602#ifdef DEBUG1
603 WriteLog("WINMM: handler cur (%d,%d), fill (%d,%d)\n", curPlayBuf, curPlayPos, curFillBuf, curFillPos);
604#endif
605
606 while(curhdr) {
607 buflength = min((ULONG)MixBuffer[curFillBuf].ulBufferLength - curPlayPos,
608 (ULONG)curhdr->dwBufferLength - curFillPos);
609 memcpy((char *)MixBuffer[curFillBuf].pBuffer + curPlayPos,
610 curhdr->lpData + curFillPos,
611 buflength);
612 curPlayPos += buflength;
613 curFillPos += buflength;
614#ifdef DEBUG1
615 WriteLog("WINMM: copied %d bytes, cufFillPos = %d, dwBufferLength = %d\n", buflength, curFillPos, curhdr->dwBufferLength);
616#endif
617 if(curFillPos == curhdr->dwBufferLength) {
618#ifdef DEBUG1
619 WriteLog("Buffer %d done\n", curFillBuf);
620#endif
621 curFillPos = 0;
622 curhdr->dwFlags |= WHDR_DONE;
623 //search for next unprocessed buffer
624 while(curhdr && curhdr->dwFlags & WHDR_DONE)
625 curhdr = curhdr->lpNext;
626 }
627 if(curPlayPos == MixBuffer[curFillBuf].ulBufferLength) {
628 curPlayPos = 0;
629 if(++curFillBuf == PREFILLBUF_DART) {
630 curFillBuf = 0;
631 }
632 if(curFillBuf == curPlayBuf)
633 break; //no more room left
634 }
635 }
636
637 if(curPlayBuf == PREFILLBUF_DART-1)
638 curPlayBuf = 0;
639 else curPlayBuf++;
640
641 wmutex->leave();
642 //Transfer buffer to DART
643 MixSetupParms->pmixWrite(MixSetupParms->ulMixHandle, &MixBuffer[curPlayBuf], 1);
644}
645/******************************************************************************/
646/******************************************************************************/
647LONG APIENTRY WaveOutHandler(ULONG ulStatus, PMCI_MIX_BUFFER pBuffer,
648 ULONG ulFlags)
649{
650 DartWaveOut *dwave;
651 PTIB ptib;
652 PPIB ppib;
653
654 DosGetInfoBlocks(&ptib, &ppib);
655// dprintf(("WaveOutHandler: thread %d prio %X", ptib->tib_ptib2->tib2_ultid, ptib->tib_ptib2->tib2_ulpri));
656 if(pBuffer && pBuffer->ulUserParm) {
657 dwave = (DartWaveOut *)pBuffer->ulUserParm;
658 dwave->handler(ulStatus, pBuffer, ulFlags);
659 }
660 return(TRUE);
661}
662/******************************************************************************/
663/******************************************************************************/
664DartWaveOut *DartWaveOut::waveout = NULL;
665
Note: See TracBrowser for help on using the repository browser.