source: trunk/src/multimedia/audio/qaudiooutput_win32_p.cpp

Last change on this file was 846, checked in by Dmitry A. Kuminov, 14 years ago

trunk: Merged in qt 4.7.2 sources from branches/vendor/nokia/qt.

  • Property svn:eol-style set to native
File size: 20.3 KB
Line 
1/****************************************************************************
2**
3** Copyright (C) 2011 Nokia Corporation and/or its subsidiary(-ies).
4** All rights reserved.
5** Contact: Nokia Corporation (qt-info@nokia.com)
6**
7** This file is part of the QtMultimedia module of the Qt Toolkit.
8**
9** $QT_BEGIN_LICENSE:LGPL$
10** Commercial Usage
11** Licensees holding valid Qt Commercial licenses may use this file in
12** accordance with the Qt Commercial License Agreement provided with the
13** Software or, alternatively, in accordance with the terms contained in
14** a written agreement between you and Nokia.
15**
16** GNU Lesser General Public License Usage
17** Alternatively, this file may be used under the terms of the GNU Lesser
18** General Public License version 2.1 as published by the Free Software
19** Foundation and appearing in the file LICENSE.LGPL included in the
20** packaging of this file. Please review the following information to
21** ensure the GNU Lesser General Public License version 2.1 requirements
22** will be met: http://www.gnu.org/licenses/old-licenses/lgpl-2.1.html.
23**
24** In addition, as a special exception, Nokia gives you certain additional
25** rights. These rights are described in the Nokia Qt LGPL Exception
26** version 1.1, included in the file LGPL_EXCEPTION.txt in this package.
27**
28** GNU General Public License Usage
29** Alternatively, this file may be used under the terms of the GNU
30** General Public License version 3.0 as published by the Free Software
31** Foundation and appearing in the file LICENSE.GPL included in the
32** packaging of this file. Please review the following information to
33** ensure the GNU General Public License version 3.0 requirements will be
34** met: http://www.gnu.org/copyleft/gpl.html.
35**
36** If you have questions regarding the use of this file, please contact
37** Nokia at qt-info@nokia.com.
38** $QT_END_LICENSE$
39**
40****************************************************************************/
41
42//
43// W A R N I N G
44// -------------
45//
46// This file is not part of the Qt API. It exists for the convenience
47// of other Qt classes. This header file may change from version to
48// version without notice, or even be removed.
49//
50// We mean it.
51//
52
53#include "qaudiooutput_win32_p.h"
54
55#ifndef SPEAKER_FRONT_LEFT
56 #define SPEAKER_FRONT_LEFT 0x00000001
57 #define SPEAKER_FRONT_RIGHT 0x00000002
58 #define SPEAKER_FRONT_CENTER 0x00000004
59 #define SPEAKER_LOW_FREQUENCY 0x00000008
60 #define SPEAKER_BACK_LEFT 0x00000010
61 #define SPEAKER_BACK_RIGHT 0x00000020
62 #define SPEAKER_FRONT_LEFT_OF_CENTER 0x00000040
63 #define SPEAKER_FRONT_RIGHT_OF_CENTER 0x00000080
64 #define SPEAKER_BACK_CENTER 0x00000100
65 #define SPEAKER_SIDE_LEFT 0x00000200
66 #define SPEAKER_SIDE_RIGHT 0x00000400
67 #define SPEAKER_TOP_CENTER 0x00000800
68 #define SPEAKER_TOP_FRONT_LEFT 0x00001000
69 #define SPEAKER_TOP_FRONT_CENTER 0x00002000
70 #define SPEAKER_TOP_FRONT_RIGHT 0x00004000
71 #define SPEAKER_TOP_BACK_LEFT 0x00008000
72 #define SPEAKER_TOP_BACK_CENTER 0x00010000
73 #define SPEAKER_TOP_BACK_RIGHT 0x00020000
74 #define SPEAKER_RESERVED 0x7FFC0000
75 #define SPEAKER_ALL 0x80000000
76#endif
77
78#ifndef _WAVEFORMATEXTENSIBLE_
79
80 #define _WAVEFORMATEXTENSIBLE_
81 typedef struct
82 {
83 WAVEFORMATEX Format; // Base WAVEFORMATEX data
84 union
85 {
86 WORD wValidBitsPerSample; // Valid bits in each sample container
87 WORD wSamplesPerBlock; // Samples per block of audio data; valid
88 // if wBitsPerSample=0 (but rarely used).
89 WORD wReserved; // Zero if neither case above applies.
90 } Samples;
91 DWORD dwChannelMask; // Positions of the audio channels
92 GUID SubFormat; // Format identifier GUID
93 } WAVEFORMATEXTENSIBLE, *PWAVEFORMATEXTENSIBLE, *LPPWAVEFORMATEXTENSIBLE;
94 typedef const WAVEFORMATEXTENSIBLE* LPCWAVEFORMATEXTENSIBLE;
95
96#endif
97
98#if !defined(WAVE_FORMAT_EXTENSIBLE)
99#define WAVE_FORMAT_EXTENSIBLE 0xFFFE
100#endif
101
102//#define DEBUG_AUDIO 1
103
104QT_BEGIN_NAMESPACE
105
106QAudioOutputPrivate::QAudioOutputPrivate(const QByteArray &device, const QAudioFormat& audioFormat):
107 settings(audioFormat)
108{
109 bytesAvailable = 0;
110 buffer_size = 0;
111 period_size = 0;
112 m_device = device;
113 totalTimeValue = 0;
114 intervalTime = 1000;
115 audioBuffer = 0;
116 errorState = QAudio::NoError;
117 deviceState = QAudio::StoppedState;
118 audioSource = 0;
119 pullMode = true;
120 finished = false;
121}
122
123QAudioOutputPrivate::~QAudioOutputPrivate()
124{
125 mutex.lock();
126 finished = true;
127 mutex.unlock();
128
129 close();
130}
131
132void CALLBACK QAudioOutputPrivate::waveOutProc( HWAVEOUT hWaveOut, UINT uMsg,
133 DWORD dwInstance, DWORD dwParam1, DWORD dwParam2 )
134{
135 Q_UNUSED(dwParam1)
136 Q_UNUSED(dwParam2)
137 Q_UNUSED(hWaveOut)
138
139 QAudioOutputPrivate* qAudio;
140 qAudio = (QAudioOutputPrivate*)(dwInstance);
141 if(!qAudio)
142 return;
143
144 QMutexLocker(&qAudio->mutex);
145
146 switch(uMsg) {
147 case WOM_OPEN:
148 qAudio->feedback();
149 break;
150 case WOM_CLOSE:
151 return;
152 case WOM_DONE:
153 if(qAudio->finished || qAudio->buffer_size == 0 || qAudio->period_size == 0) {
154 return;
155 }
156 qAudio->waveFreeBlockCount++;
157 if(qAudio->waveFreeBlockCount >= qAudio->buffer_size/qAudio->period_size)
158 qAudio->waveFreeBlockCount = qAudio->buffer_size/qAudio->period_size;
159 qAudio->feedback();
160 break;
161 default:
162 return;
163 }
164}
165
166WAVEHDR* QAudioOutputPrivate::allocateBlocks(int size, int count)
167{
168 int i;
169 unsigned char* buffer;
170 WAVEHDR* blocks;
171 DWORD totalBufferSize = (size + sizeof(WAVEHDR))*count;
172
173 if((buffer=(unsigned char*)HeapAlloc(GetProcessHeap(),HEAP_ZERO_MEMORY,
174 totalBufferSize)) == 0) {
175 qWarning("QAudioOutput: Memory allocation error");
176 return 0;
177 }
178 blocks = (WAVEHDR*)buffer;
179 buffer += sizeof(WAVEHDR)*count;
180 for(i = 0; i < count; i++) {
181 blocks[i].dwBufferLength = size;
182 blocks[i].lpData = (LPSTR)buffer;
183 buffer += size;
184 }
185 return blocks;
186}
187
188void QAudioOutputPrivate::freeBlocks(WAVEHDR* blockArray)
189{
190 WAVEHDR* blocks = blockArray;
191
192 int count = buffer_size/period_size;
193
194 for(int i = 0; i < count; i++) {
195 waveOutUnprepareHeader(hWaveOut,blocks, sizeof(WAVEHDR));
196 blocks++;
197 }
198 HeapFree(GetProcessHeap(), 0, blockArray);
199}
200
201QAudioFormat QAudioOutputPrivate::format() const
202{
203 return settings;
204}
205
206QIODevice* QAudioOutputPrivate::start(QIODevice* device)
207{
208 if(deviceState != QAudio::StoppedState)
209 close();
210
211 if(!pullMode && audioSource) {
212 delete audioSource;
213 }
214
215 if(device) {
216 //set to pull mode
217 pullMode = true;
218 audioSource = device;
219 deviceState = QAudio::ActiveState;
220 } else {
221 //set to push mode
222 pullMode = false;
223 audioSource = new OutputPrivate(this);
224 audioSource->open(QIODevice::WriteOnly|QIODevice::Unbuffered);
225 deviceState = QAudio::IdleState;
226 }
227
228 if( !open() )
229 return 0;
230
231 emit stateChanged(deviceState);
232
233 return audioSource;
234}
235
236void QAudioOutputPrivate::stop()
237{
238 if(deviceState == QAudio::StoppedState)
239 return;
240 close();
241 if(!pullMode && audioSource) {
242 delete audioSource;
243 audioSource = 0;
244 }
245 emit stateChanged(deviceState);
246}
247
248bool QAudioOutputPrivate::open()
249{
250#ifdef DEBUG_AUDIO
251 QTime now(QTime::currentTime());
252 qDebug()<<now.second()<<"s "<<now.msec()<<"ms :open()";
253#endif
254
255 period_size = 0;
256
257 if (!settings.isValid()) {
258 qWarning("QAudioOutput: open error, invalid format.");
259 } else if (settings.channels() <= 0) {
260 qWarning("QAudioOutput: open error, invalid number of channels (%d).",
261 settings.channels());
262 } else if (settings.sampleSize() <= 0) {
263 qWarning("QAudioOutput: open error, invalid sample size (%d).",
264 settings.sampleSize());
265 } else if (settings.frequency() < 8000 || settings.frequency() > 48000) {
266 qWarning("QAudioOutput: open error, frequency out of range (%d).", settings.frequency());
267 } else if (buffer_size == 0) {
268 // Default buffer size, 200ms, default period size is 40ms
269 buffer_size
270 = (settings.frequency()
271 * settings.channels()
272 * settings.sampleSize()
273 + 39) / 40;
274 period_size = buffer_size / 5;
275 } else {
276 period_size = buffer_size / 5;
277 }
278
279 if (period_size == 0) {
280 errorState = QAudio::OpenError;
281 deviceState = QAudio::StoppedState;
282 emit stateChanged(deviceState);
283 return false;
284 }
285
286 waveBlocks = allocateBlocks(period_size, buffer_size/period_size);
287
288 mutex.lock();
289 waveFreeBlockCount = buffer_size/period_size;
290 mutex.unlock();
291
292 waveCurrentBlock = 0;
293
294 if(audioBuffer == 0)
295 audioBuffer = new char[buffer_size];
296
297 timeStamp.restart();
298 elapsedTimeOffset = 0;
299
300 wfx.nSamplesPerSec = settings.frequency();
301 wfx.wBitsPerSample = settings.sampleSize();
302 wfx.nChannels = settings.channels();
303 wfx.cbSize = 0;
304
305 wfx.wFormatTag = WAVE_FORMAT_PCM;
306 wfx.nBlockAlign = (wfx.wBitsPerSample >> 3) * wfx.nChannels;
307 wfx.nAvgBytesPerSec = wfx.nBlockAlign * wfx.nSamplesPerSec;
308
309 UINT_PTR devId = WAVE_MAPPER;
310
311 WAVEOUTCAPS woc;
312 unsigned long iNumDevs,ii;
313 iNumDevs = waveOutGetNumDevs();
314 for(ii=0;ii<iNumDevs;ii++) {
315 if(waveOutGetDevCaps(ii, &woc, sizeof(WAVEOUTCAPS))
316 == MMSYSERR_NOERROR) {
317 QString tmp;
318 tmp = QString((const QChar *)woc.szPname);
319 if(tmp.compare(QLatin1String(m_device)) == 0) {
320 devId = ii;
321 break;
322 }
323 }
324 }
325
326 if ( settings.channels() <= 2) {
327 if(waveOutOpen(&hWaveOut, devId, &wfx,
328 (DWORD_PTR)&waveOutProc,
329 (DWORD_PTR) this,
330 CALLBACK_FUNCTION) != MMSYSERR_NOERROR) {
331 errorState = QAudio::OpenError;
332 deviceState = QAudio::StoppedState;
333 emit stateChanged(deviceState);
334 qWarning("QAudioOutput: open error");
335 return false;
336 }
337 } else {
338 WAVEFORMATEXTENSIBLE wfex;
339 wfex.Format.wFormatTag = WAVE_FORMAT_EXTENSIBLE;
340 wfex.Format.nChannels = settings.channels();
341 wfex.Format.wBitsPerSample = settings.sampleSize();
342 wfex.Format.nSamplesPerSec = settings.frequency();
343 wfex.Format.nBlockAlign = wfex.Format.nChannels*wfex.Format.wBitsPerSample/8;
344 wfex.Format.nAvgBytesPerSec=wfex.Format.nSamplesPerSec*wfex.Format.nBlockAlign;
345 wfex.Samples.wValidBitsPerSample=wfex.Format.wBitsPerSample;
346 static const GUID _KSDATAFORMAT_SUBTYPE_PCM = {
347 0x00000001, 0x0000, 0x0010, {0x80, 0x00, 0x00, 0xaa, 0x00, 0x38, 0x9b, 0x71}};
348 wfex.SubFormat=_KSDATAFORMAT_SUBTYPE_PCM;
349 wfex.Format.cbSize=22;
350
351 wfex.dwChannelMask = SPEAKER_FRONT_LEFT | SPEAKER_FRONT_RIGHT;
352 if (settings.channels() >= 4)
353 wfex.dwChannelMask |= SPEAKER_BACK_LEFT | SPEAKER_BACK_RIGHT;
354 if (settings.channels() >= 6)
355 wfex.dwChannelMask |= SPEAKER_FRONT_CENTER | SPEAKER_LOW_FREQUENCY;
356 if (settings.channels() == 8)
357 wfex.dwChannelMask |= SPEAKER_SIDE_LEFT | SPEAKER_SIDE_RIGHT;
358
359 if(waveOutOpen(&hWaveOut, devId, &wfex.Format,
360 (DWORD_PTR)&waveOutProc,
361 (DWORD_PTR) this,
362 CALLBACK_FUNCTION) != MMSYSERR_NOERROR) {
363 errorState = QAudio::OpenError;
364 deviceState = QAudio::StoppedState;
365 emit stateChanged(deviceState);
366 qWarning("QAudioOutput: open error");
367 return false;
368 }
369 }
370
371 totalTimeValue = 0;
372 timeStampOpened.restart();
373 elapsedTimeOffset = 0;
374
375 errorState = QAudio::NoError;
376 if(pullMode) {
377 deviceState = QAudio::ActiveState;
378 QTimer::singleShot(10, this, SLOT(feedback()));
379 } else
380 deviceState = QAudio::IdleState;
381
382 return true;
383}
384
385void QAudioOutputPrivate::close()
386{
387 if(deviceState == QAudio::StoppedState)
388 return;
389
390 deviceState = QAudio::StoppedState;
391 errorState = QAudio::NoError;
392 int delay = (buffer_size-bytesFree())*1000/(settings.frequency()
393 *settings.channels()*(settings.sampleSize()/8));
394 waveOutReset(hWaveOut);
395 Sleep(delay+10);
396
397 freeBlocks(waveBlocks);
398 waveOutClose(hWaveOut);
399 delete [] audioBuffer;
400 audioBuffer = 0;
401 buffer_size = 0;
402}
403
404int QAudioOutputPrivate::bytesFree() const
405{
406 int buf;
407 buf = waveFreeBlockCount*period_size;
408
409 return buf;
410}
411
412int QAudioOutputPrivate::periodSize() const
413{
414 return period_size;
415}
416
417void QAudioOutputPrivate::setBufferSize(int value)
418{
419 if(deviceState == QAudio::StoppedState)
420 buffer_size = value;
421}
422
423int QAudioOutputPrivate::bufferSize() const
424{
425 return buffer_size;
426}
427
428void QAudioOutputPrivate::setNotifyInterval(int ms)
429{
430 intervalTime = qMax(0, ms);
431}
432
433int QAudioOutputPrivate::notifyInterval() const
434{
435 return intervalTime;
436}
437
438qint64 QAudioOutputPrivate::processedUSecs() const
439{
440 if (deviceState == QAudio::StoppedState)
441 return 0;
442 qint64 result = qint64(1000000) * totalTimeValue /
443 (settings.channels()*(settings.sampleSize()/8)) /
444 settings.frequency();
445
446 return result;
447}
448
449qint64 QAudioOutputPrivate::write( const char *data, qint64 len )
450{
451 // Write out some audio data
452 if (deviceState != QAudio::ActiveState && deviceState != QAudio::IdleState)
453 return 0;
454
455 char* p = (char*)data;
456 int l = (int)len;
457
458 WAVEHDR* current;
459 int remain;
460 current = &waveBlocks[waveCurrentBlock];
461 while(l > 0) {
462 mutex.lock();
463 if(waveFreeBlockCount==0) {
464 mutex.unlock();
465 break;
466 }
467 mutex.unlock();
468
469 if(current->dwFlags & WHDR_PREPARED)
470 waveOutUnprepareHeader(hWaveOut, current, sizeof(WAVEHDR));
471
472 if(l < period_size)
473 remain = l;
474 else
475 remain = period_size;
476 memcpy(current->lpData, p, remain);
477
478 l -= remain;
479 p += remain;
480 current->dwBufferLength = remain;
481 waveOutPrepareHeader(hWaveOut, current, sizeof(WAVEHDR));
482 waveOutWrite(hWaveOut, current, sizeof(WAVEHDR));
483
484 mutex.lock();
485 waveFreeBlockCount--;
486#ifdef DEBUG_AUDIO
487 qDebug("write out l=%d, waveFreeBlockCount=%d",
488 current->dwBufferLength,waveFreeBlockCount);
489#endif
490 mutex.unlock();
491 totalTimeValue += current->dwBufferLength;
492 waveCurrentBlock++;
493 waveCurrentBlock %= buffer_size/period_size;
494 current = &waveBlocks[waveCurrentBlock];
495 current->dwUser = 0;
496 errorState = QAudio::NoError;
497 if (deviceState != QAudio::ActiveState) {
498 deviceState = QAudio::ActiveState;
499 emit stateChanged(deviceState);
500 }
501 }
502 return (len-l);
503}
504
505void QAudioOutputPrivate::resume()
506{
507 if(deviceState == QAudio::SuspendedState) {
508 deviceState = QAudio::ActiveState;
509 errorState = QAudio::NoError;
510 waveOutRestart(hWaveOut);
511 QTimer::singleShot(10, this, SLOT(feedback()));
512 emit stateChanged(deviceState);
513 }
514}
515
516void QAudioOutputPrivate::suspend()
517{
518 if(deviceState == QAudio::ActiveState || deviceState == QAudio::IdleState) {
519 int delay = (buffer_size-bytesFree())*1000/(settings.frequency()
520 *settings.channels()*(settings.sampleSize()/8));
521 waveOutPause(hWaveOut);
522 Sleep(delay+10);
523 deviceState = QAudio::SuspendedState;
524 errorState = QAudio::NoError;
525 emit stateChanged(deviceState);
526 }
527}
528
529void QAudioOutputPrivate::feedback()
530{
531#ifdef DEBUG_AUDIO
532 QTime now(QTime::currentTime());
533 qDebug()<<now.second()<<"s "<<now.msec()<<"ms :feedback()";
534#endif
535 bytesAvailable = bytesFree();
536
537 if(!(deviceState==QAudio::StoppedState||deviceState==QAudio::SuspendedState)) {
538 if(bytesAvailable >= period_size)
539 QMetaObject::invokeMethod(this, "deviceReady", Qt::QueuedConnection);
540 }
541}
542
543bool QAudioOutputPrivate::deviceReady()
544{
545 if(deviceState == QAudio::StoppedState || deviceState == QAudio::SuspendedState)
546 return false;
547
548 if(pullMode) {
549 int chunks = bytesAvailable/period_size;
550#ifdef DEBUG_AUDIO
551 qDebug()<<"deviceReady() avail="<<bytesAvailable<<" bytes, period size="<<period_size<<" bytes";
552 qDebug()<<"deviceReady() no. of chunks that can fit ="<<chunks<<", chunks in bytes ="<<chunks*period_size;
553#endif
554 bool startup = false;
555 if(totalTimeValue == 0)
556 startup = true;
557
558 bool full=false;
559
560 mutex.lock();
561 if(waveFreeBlockCount==0) full = true;
562 mutex.unlock();
563
564 if (full){
565#ifdef DEBUG_AUDIO
566 qDebug() << "Skipping data as unable to write";
567#endif
568 if(intervalTime && (timeStamp.elapsed() + elapsedTimeOffset) > intervalTime ) {
569 emit notify();
570 elapsedTimeOffset = timeStamp.elapsed() + elapsedTimeOffset - intervalTime;
571 timeStamp.restart();
572 }
573 return true;
574 }
575
576 if(startup)
577 waveOutPause(hWaveOut);
578 int input = period_size*chunks;
579 int l = audioSource->read(audioBuffer,input);
580 if(l > 0) {
581 int out= write(audioBuffer,l);
582 if(out > 0) {
583 if (deviceState != QAudio::ActiveState) {
584 deviceState = QAudio::ActiveState;
585 emit stateChanged(deviceState);
586 }
587 }
588 if ( out < l) {
589 // Didn't write all data
590 audioSource->seek(audioSource->pos()-(l-out));
591 }
592 if(startup)
593 waveOutRestart(hWaveOut);
594 } else if(l == 0) {
595 bytesAvailable = bytesFree();
596
597 int check = 0;
598
599 mutex.lock();
600 check = waveFreeBlockCount;
601 mutex.unlock();
602
603 if(check == buffer_size/period_size) {
604 if (deviceState != QAudio::IdleState) {
605 errorState = QAudio::UnderrunError;
606 deviceState = QAudio::IdleState;
607 emit stateChanged(deviceState);
608 }
609 }
610
611 } else if(l < 0) {
612 bytesAvailable = bytesFree();
613 errorState = QAudio::IOError;
614 }
615 } else {
616 int buffered;
617
618 mutex.lock();
619 buffered = waveFreeBlockCount;
620 mutex.unlock();
621
622 if (buffered >= buffer_size/period_size && deviceState == QAudio::ActiveState) {
623 if (deviceState != QAudio::IdleState) {
624 errorState = QAudio::UnderrunError;
625 deviceState = QAudio::IdleState;
626 emit stateChanged(deviceState);
627 }
628 }
629 }
630 if(deviceState != QAudio::ActiveState && deviceState != QAudio::IdleState)
631 return true;
632
633 if(intervalTime && (timeStamp.elapsed() + elapsedTimeOffset) > intervalTime) {
634 emit notify();
635 elapsedTimeOffset = timeStamp.elapsed() + elapsedTimeOffset - intervalTime;
636 timeStamp.restart();
637 }
638
639 return true;
640}
641
642qint64 QAudioOutputPrivate::elapsedUSecs() const
643{
644 if (deviceState == QAudio::StoppedState)
645 return 0;
646
647 return timeStampOpened.elapsed()*1000;
648}
649
650QAudio::Error QAudioOutputPrivate::error() const
651{
652 return errorState;
653}
654
655QAudio::State QAudioOutputPrivate::state() const
656{
657 return deviceState;
658}
659
660void QAudioOutputPrivate::reset()
661{
662 close();
663}
664
665OutputPrivate::OutputPrivate(QAudioOutputPrivate* audio)
666{
667 audioDevice = qobject_cast<QAudioOutputPrivate*>(audio);
668}
669
670OutputPrivate::~OutputPrivate() {}
671
672qint64 OutputPrivate::readData( char* data, qint64 len)
673{
674 Q_UNUSED(data)
675 Q_UNUSED(len)
676
677 return 0;
678}
679
680qint64 OutputPrivate::writeData(const char* data, qint64 len)
681{
682 int retry = 0;
683 qint64 written = 0;
684
685 if((audioDevice->deviceState == QAudio::ActiveState)
686 ||(audioDevice->deviceState == QAudio::IdleState)) {
687 qint64 l = len;
688 while(written < l) {
689 int chunk = audioDevice->write(data+written,(l-written));
690 if(chunk <= 0)
691 retry++;
692 else
693 written+=chunk;
694
695 if(retry > 10)
696 return written;
697 }
698 audioDevice->deviceState = QAudio::ActiveState;
699 }
700 return written;
701}
702
703QT_END_NAMESPACE
Note: See TracBrowser for help on using the repository browser.