1 /*
2 * recording.cxx
3 *
4 * OPAL call recording
5 *
6 * Open Phone Abstraction Library (OPAL)
7 *
8 * Copyright (C) 2009 Post Increment
9 *
10 * The contents of this file are subject to the Mozilla Public License
11 * Version 1.0 (the "License"); you may not use this file except in
12 * compliance with the License. You may obtain a copy of the License at
13 * http://www.mozilla.org/MPL/
14 *
15 * Software distributed under the License is distributed on an "AS IS"
16 * basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See
17 * the License for the specific language governing rights and limitations
18 * under the License.
19 *
20 * The Original Code is Open Phone Abstraction Library.
21 *
22 * The Initial Developer of the Original Code is Post Increment
23 *
24 * Contributor(s): ______________________________________.
25 *
26 * $Revision: 27141 $
27 * $Author: rjongbloed $
28 * $Date: 2012-03-06 21:56:21 -0600 (Tue, 06 Mar 2012) $
29 */
30
31
32 #include <ptlib.h>
33
34 #include <opal/buildopts.h>
35
36 #if OPAL_HAS_MIXER
37
38 #include <opal/opalmixer.h>
39 #include <opal/recording.h>
40 #include <codec/opalwavfile.h>
41
42
43 //////////////////////////////////////////////////////////////////////////////
44
45 /** This class manages the recording of OPAL calls to WAV files.
46 */
47 class OpalWAVRecordManager : public OpalRecordManager
48 {
49 public:
50 OpalWAVRecordManager();
51 ~OpalWAVRecordManager();
52
53 virtual bool OpenFile(const PFilePath & fn);
54 virtual bool IsOpen() const;
55 virtual bool Close();
56 virtual bool OpenStream(const PString & strmId, const OpalMediaFormat & format);
57 virtual bool CloseStream(const PString & strmId);
58 virtual bool WriteAudio(const PString & strmId, const RTP_DataFrame & rtp);
59 virtual bool WriteVideo(const PString & strmId, const RTP_DataFrame & rtp);
60
61 protected:
62 struct Mixer : public OpalAudioMixer {
MixerOpalWAVRecordManager::Mixer63 Mixer() { }
~MixerOpalWAVRecordManager::Mixer64 ~Mixer() { StopPushThread(); }
65
66 bool Open(const PFilePath & fn, const Options & options);
67 virtual bool OnMixed(RTP_DataFrame * & output);
68
69 OpalWAVFile m_file;
70 } * m_mixer;
71
72 PMutex m_mutex;
73 };
74
75 PFACTORY_CREATE(OpalRecordManager::Factory, OpalWAVRecordManager, ".wav", false);
76
77
OpalWAVRecordManager()78 OpalWAVRecordManager::OpalWAVRecordManager()
79 : m_mixer(NULL)
80 {
81 }
82
83
~OpalWAVRecordManager()84 OpalWAVRecordManager::~OpalWAVRecordManager()
85 {
86 Close();
87 }
88
89
OpenFile(const PFilePath & fn)90 bool OpalWAVRecordManager::OpenFile(const PFilePath & fn)
91 {
92 if (m_options.m_audioFormat.IsEmpty())
93 m_options.m_audioFormat = OpalPCM16.GetName();
94
95 PWaitAndSignal mutex(m_mutex);
96
97 if (IsOpen()) {
98 PTRACE(2, "OpalRecord\tCannot open mixer after it has started.");
99 return false;
100 }
101
102 m_mixer = new Mixer();
103 if (m_mixer->Open(fn, m_options))
104 return true;
105
106 delete m_mixer;
107 m_mixer = NULL;
108 return false;
109 }
110
111
IsOpen() const112 bool OpalWAVRecordManager::IsOpen() const
113 {
114 PWaitAndSignal mutex(m_mutex);
115 return m_mixer != NULL && m_mixer->m_file.IsOpen();
116 }
117
118
Close()119 bool OpalWAVRecordManager::Close()
120 {
121 m_mutex.Wait();
122
123 delete m_mixer;
124 m_mixer = NULL;
125
126 m_mutex.Signal();
127
128 return true;
129 }
130
131
OpenStream(const PString & strmId,const OpalMediaFormat & format)132 bool OpalWAVRecordManager::OpenStream(const PString & strmId, const OpalMediaFormat & format)
133 {
134 PWaitAndSignal mutex(m_mutex);
135
136 if (m_mixer == NULL || format.GetMediaType() != OpalMediaType::Audio())
137 return false;
138
139 m_mixer->m_file.SetSampleRate(format.GetClockRate());
140 return m_mixer->SetSampleRate(format.GetClockRate()) &&
141 m_mixer->AddStream(strmId);
142 }
143
144
CloseStream(const PString & streamId)145 bool OpalWAVRecordManager::CloseStream(const PString & streamId)
146 {
147 m_mutex.Wait();
148 if (m_mixer != NULL)
149 m_mixer->RemoveStream(streamId);
150 m_mutex.Signal();
151
152 PTRACE(4, "OpalRecord\tClosed stream " << streamId);
153 return true;
154 }
155
156
WriteAudio(const PString & strm,const RTP_DataFrame & rtp)157 bool OpalWAVRecordManager::WriteAudio(const PString & strm, const RTP_DataFrame & rtp)
158 {
159 PWaitAndSignal mutex(m_mutex);
160 return m_mixer != NULL && m_mixer->WriteStream(strm, rtp);
161 }
162
163
WriteVideo(const PString &,const RTP_DataFrame &)164 bool OpalWAVRecordManager::WriteVideo(const PString &, const RTP_DataFrame &)
165 {
166 return false;
167 }
168
169
Open(const PFilePath & fn,const Options & options)170 bool OpalWAVRecordManager::Mixer::Open(const PFilePath & fn, const Options & options)
171 {
172 if (!m_file.SetFormat(options.m_audioFormat)) {
173 PTRACE(2, "OpalRecord\tWAV file recording does not support format " << options.m_audioFormat);
174 return false;
175 }
176
177 if (!m_file.Open(fn, PFile::ReadWrite, PFile::Create|PFile::Truncate)) {
178 PTRACE(2, "OpalRecord\tCould not open file \"" << fn << '"');
179 return false;
180 }
181
182 if (options.m_stereo) {
183 m_file.SetChannels(2);
184 if (m_file.GetChannels() == 2)
185 m_stereo = true;
186 }
187
188 PTRACE(4, "OpalRecord\t" << (m_stereo ? "Stereo" : "Mono") << " mixer opened for file \"" << fn << '"');
189 return true;
190 }
191
192
OnMixed(RTP_DataFrame * & output)193 bool OpalWAVRecordManager::Mixer::OnMixed(RTP_DataFrame * & output)
194 {
195 if (!m_file.IsOpen())
196 return false;
197
198 if (m_file.Write(output->GetPayloadPtr(), output->GetPayloadSize()))
199 return true;
200
201 PTRACE(1, "OpalRecord\tError writing WAV file " << m_file.GetFilePath());
202 return false;
203 }
204
205
206 /////////////////////////////////////////////////////////////////////////////
207
208 #if OPAL_VIDEO && P_VFW_CAPTURE
209
210 #include <ptlib/vconvert.h>
211
212 #include <vfw.h>
213 #pragma comment(lib, "vfw32.lib")
214
215
216 /** This class manages the recording of OPAL calls to AVI files.
217 */
218 class OpalAVIRecordManager : public OpalRecordManager
219 {
220 protected:
221 struct AudioMixer : public OpalAudioMixer
222 {
AudioMixerOpalAVIRecordManager::AudioMixer223 AudioMixer(OpalAVIRecordManager & manager, bool stereo)
224 : OpalAudioMixer(stereo), m_manager(manager) { }
~AudioMixerOpalAVIRecordManager::AudioMixer225 ~AudioMixer() { StopPushThread(); }
OnMixedOpalAVIRecordManager::AudioMixer226 virtual bool OnMixed(RTP_DataFrame * & output) { return m_manager.OnMixedAudio(*output); }
227 OpalAVIRecordManager & m_manager;
228 } * m_audioMixer;
229
230 struct VideoMixer : public OpalVideoMixer
231 {
VideoMixerOpalAVIRecordManager::VideoMixer232 VideoMixer(OpalAVIRecordManager & manager, OpalVideoMixer::Styles style, unsigned width, unsigned height)
233 : OpalVideoMixer(style, width, height), m_manager(manager) { }
~VideoMixerOpalAVIRecordManager::VideoMixer234 ~VideoMixer() { StopPushThread(); }
OnMixedOpalAVIRecordManager::VideoMixer235 virtual bool OnMixed(RTP_DataFrame * & output) { return m_manager.OnMixedVideo(*output); }
236 OpalAVIRecordManager & m_manager;
237 } * m_videoMixer;
238
239 public:
240 OpalAVIRecordManager();
241 ~OpalAVIRecordManager();
242
243 virtual bool OpenFile(const PFilePath & fn);
244 virtual bool IsOpen() const;
245 virtual bool Close();
246 virtual bool OpenStream(const PString & strmId, const OpalMediaFormat & format);
247 virtual bool CloseStream(const PString & strmId);
248 virtual bool WriteAudio(const PString & strmId, const RTP_DataFrame & rtp);
249 virtual bool WriteVideo(const PString & strmId, const RTP_DataFrame & rtp);
250
251 protected:
252 // Callback from OpalAudioMixer
253 virtual bool OpenAudio(const PString & strmId, const OpalMediaFormat & format);
254 virtual bool OnMixedAudio(const RTP_DataFrame & frame);
255
256 // Callback from OpalVideoMixer
257 virtual bool OpenVideo(const PString & strmId, const OpalMediaFormat & format);
258 virtual bool OnMixedVideo(const RTP_DataFrame & frame);
259
260 #if PTRACING
IsResultError(HRESULT result,const char * msg)261 bool IsResultError(HRESULT result, const char * msg)
262 {
263 if (result == AVIERR_OK)
264 return false;
265
266 if (!PTrace::CanTrace(2))
267 return true;
268
269 ostream & strm = PTrace::Begin(2, __FILE__, __LINE__);
270 strm << "OpalRecord\tError " << msg << ": ";
271 switch (result) {
272 case AVIERR_UNSUPPORTED :
273 strm << "Unsupported compressor '" << m_options.m_videoFormat << '\'';
274 break;
275
276 case AVIERR_NOCOMPRESSOR :
277 strm << "No compressor '" << m_options.m_videoFormat << '\'';
278 break;
279
280 default :
281 strm << "Error=0x" << hex << result;
282 }
283 strm << PTrace::End;
284
285 return true;
286 }
287
288 #define IS_RESULT_ERROR(result, msg) IsResultError(result, msg)
289
290 #else
291
292 #define IS_RESULT_ERROR(result, msg) ((result) != AVIERR_OK)
293
294 #endif
295
296
297 PMutex m_mutex;
298 PAVIFILE m_file;
299 PAVISTREAM m_audioStream;
300 DWORD m_audioSampleCount;
301 DWORD m_audioSampleSize;
302 PAVISTREAM m_videoStream;
303 PAVISTREAM m_videoCompressor;
304 DWORD m_VideoFrameCount;
305 PBYTEArray m_videoBuffer;
306 PColourConverter * m_videoConverter;
307 };
308
309 PFACTORY_CREATE(OpalRecordManager::Factory, OpalAVIRecordManager, ".avi", false);
310
311
312 static char const * const VideoModeNames[OpalRecordManager::NumVideoMixingModes] = {
313 "Side by Side Letterbox Video",
314 "Side by Side Scaled Video",
315 "Stacked Pillarbox Video",
316 "Stacked Scaled Video",
317 "Separate Video Stream"
318 };
319
320
OpalAVIRecordManager()321 OpalAVIRecordManager::OpalAVIRecordManager()
322 : m_audioMixer(NULL)
323 , m_videoMixer(NULL)
324 , m_file(NULL)
325 , m_audioStream(NULL)
326 , m_audioSampleCount(0)
327 , m_audioSampleSize(sizeof(short))
328 , m_videoStream(NULL)
329 , m_videoCompressor(NULL)
330 , m_VideoFrameCount(0)
331 , m_videoConverter(NULL)
332 {
333 AVIFileInit();
334 }
335
336
~OpalAVIRecordManager()337 OpalAVIRecordManager::~OpalAVIRecordManager()
338 {
339 Close();
340 AVIFileExit();
341 }
342
343
OpenFile(const PFilePath & fn)344 bool OpalAVIRecordManager::OpenFile(const PFilePath & fn)
345 {
346 if (m_options.m_audioFormat.IsEmpty())
347 m_options.m_audioFormat = OpalPCM16.GetName();
348 else if (m_options.m_audioFormat != OpalPCM16) {
349 PTRACE(2, "OpalRecord\tAVI file recording does not (yet) support format " << m_options.m_audioFormat);
350 return false;
351 }
352
353 if (m_options.m_videoFormat.IsEmpty())
354 m_options.m_videoFormat = "MSVC"; // Default to Microsoft Video 1, every system has that!
355 else if (m_options.m_videoFormat.GetLength() != 4) {
356 PTRACE(2, "OpalRecord\tAVI file recording does not (yet) support format " << m_options.m_videoFormat);
357 return false;
358 }
359
360 PWaitAndSignal mutex(m_mutex);
361
362 if (m_file != NULL) {
363 PTRACE(2, "OpalRecord\tCannot open mixer after it has started.");
364 return false;
365 }
366
367 if (IS_RESULT_ERROR(AVIFileOpen(&m_file, fn, OF_WRITE|OF_CREATE, NULL), "creating AVI file"))
368 return false;
369
370 m_audioMixer = new AudioMixer(*this, m_options.m_stereo);
371
372 OpalVideoMixer::Styles style;
373 switch (m_options.m_videoMixing) {
374 case eSideBySideScaled :
375 m_options.m_videoWidth *= 2;
376 style = OpalVideoMixer::eSideBySideScaled;
377 break;
378
379 case eSideBySideLetterbox :
380 style = OpalVideoMixer::eSideBySideLetterbox;
381 break;
382
383 case eStackedScaled :
384 m_options.m_videoHeight *= 2;
385 style = OpalVideoMixer::eStackedScaled;
386 break;
387
388 case eStackedPillarbox :
389 style = OpalVideoMixer::eStackedPillarbox;
390 break;
391
392 default :
393 PAssertAlways(PInvalidParameter);
394 return false;
395 }
396
397 m_videoMixer = new VideoMixer(*this, style, m_options.m_videoWidth, m_options.m_videoHeight);
398
399 PTRACE(4, "OpalRecord\t" << (m_options.m_stereo ? "Stereo" : "Mono") << "-PCM/"
400 << m_options.m_videoFormat << "-Video mixers opened for file \"" << fn << '"');
401 return true;
402 }
403
404
IsOpen() const405 bool OpalAVIRecordManager::IsOpen() const
406 {
407 return m_file != NULL;
408 }
409
410
Close()411 bool OpalAVIRecordManager::Close()
412 {
413 m_mutex.Wait();
414
415 delete m_audioMixer;
416 m_audioMixer = NULL;
417
418 delete m_videoMixer;
419 m_videoMixer = NULL;
420
421 delete m_videoConverter;
422 m_videoConverter = NULL;
423
424 if (m_videoCompressor != NULL) {
425 AVIStreamRelease(m_videoCompressor);
426 m_videoCompressor = NULL;
427 }
428
429 if (m_videoStream != NULL) {
430 AVIStreamRelease(m_videoStream);
431 m_videoStream = NULL;
432 }
433
434 if (m_audioStream != NULL) {
435 AVIStreamRelease(m_audioStream);
436 m_audioStream = NULL;
437 }
438
439 if (m_file != NULL) {
440 AVIFileRelease(m_file);
441 m_file = NULL;
442 }
443
444 m_mutex.Signal();
445
446 return true;
447 }
448
449
OpenStream(const PString & strmId,const OpalMediaFormat & format)450 bool OpalAVIRecordManager::OpenStream(const PString & strmId, const OpalMediaFormat & format)
451 {
452 PWaitAndSignal mutex(m_mutex);
453
454 if (format.GetMediaType() == OpalMediaType::Audio())
455 return OpenAudio(strmId, format);
456
457 if (format.GetMediaType() == OpalMediaType::Video())
458 return OpenVideo(strmId, format);
459
460 return false;
461 }
462
463
CloseStream(const PString & streamId)464 bool OpalAVIRecordManager::CloseStream(const PString & streamId)
465 {
466 m_mutex.Wait();
467
468 if (m_audioMixer != NULL)
469 m_audioMixer->RemoveStream(streamId);
470
471 if (m_videoMixer != NULL)
472 m_videoMixer->RemoveStream(streamId);
473
474 m_mutex.Signal();
475
476 PTRACE(4, "OpalRecord\tClosed stream " << streamId);
477 return true;
478 }
479
480
WriteAudio(const PString & strmId,const RTP_DataFrame & rtp)481 bool OpalAVIRecordManager::WriteAudio(const PString & strmId, const RTP_DataFrame & rtp)
482 {
483 PWaitAndSignal mutex(m_mutex);
484 return m_audioMixer != NULL && m_audioMixer->WriteStream(strmId, rtp);
485 }
486
487
WriteVideo(const PString & strmId,const RTP_DataFrame & rtp)488 bool OpalAVIRecordManager::WriteVideo(const PString & strmId, const RTP_DataFrame & rtp)
489 {
490 PWaitAndSignal mutex(m_mutex);
491 return m_videoMixer != NULL && m_videoMixer->WriteStream(strmId, rtp);
492 }
493
494
OpenAudio(const PString & strmId,const OpalMediaFormat & format)495 bool OpalAVIRecordManager::OpenAudio(const PString & strmId, const OpalMediaFormat & format)
496 {
497 if (m_audioMixer == NULL)
498 return false;
499
500 if (!m_audioMixer->SetSampleRate(format.GetClockRate()))
501 return false;
502
503 if (m_audioStream != NULL)
504 return m_audioMixer->AddStream(strmId);
505
506 PTRACE(4, "OpalRecord\tCreating AVI stream for audio format '" << m_options.m_audioFormat << '\'');
507
508 WAVEFORMATEX fmt;
509 fmt.wFormatTag = WAVE_FORMAT_PCM;
510 fmt.wBitsPerSample = 16;
511 fmt.nChannels = m_audioMixer->IsStereo() ? 2 : 1;
512 fmt.nSamplesPerSec = m_audioMixer->GetSampleRate();
513 fmt.nBlockAlign = (fmt.nChannels*fmt.wBitsPerSample+7)/8;
514 fmt.nAvgBytesPerSec = fmt.nSamplesPerSec*fmt.nBlockAlign;
515 fmt.cbSize = 0;
516
517 m_audioSampleSize = fmt.nBlockAlign;
518
519 AVISTREAMINFO info;
520 memset(&info, 0, sizeof(info));
521 info.fccType = streamtypeAUDIO;
522 info.dwScale = fmt.nBlockAlign;
523 info.dwRate = fmt.nAvgBytesPerSec;
524 info.dwSampleSize = fmt.nBlockAlign;
525 info.dwQuality = (DWORD)-1;
526 strcpy(info.szName, fmt.nChannels == 2 ? "Stereo Audio" : "Mixed Audio");
527
528 if (IS_RESULT_ERROR(AVIFileCreateStream(m_file, &m_audioStream, &info), "creating AVI audio stream"))
529 return false;
530
531 if (IS_RESULT_ERROR(AVIStreamSetFormat(m_audioStream, 0, &fmt, sizeof(fmt)), "setting format of AVI audio stream"))
532 return false;
533
534 return m_audioMixer->AddStream(strmId);
535 }
536
537
OnMixedAudio(const RTP_DataFrame & frame)538 bool OpalAVIRecordManager::OnMixedAudio(const RTP_DataFrame & frame)
539 {
540 if (!IsOpen() || PAssertNULL(m_audioStream) == NULL)
541 return false;
542
543 DWORD samples = frame.GetPayloadSize()/m_audioSampleSize;
544 if (IS_RESULT_ERROR(AVIStreamWrite(m_audioStream,
545 m_audioSampleCount, samples,
546 frame.GetPayloadPtr(), frame.GetPayloadSize(),
547 0, NULL, NULL), "writing AVI audio stream"))
548 return false;
549
550 m_audioSampleCount += samples;
551 return true;
552 }
553
554
OpenVideo(const PString & strmId,const OpalMediaFormat &)555 bool OpalAVIRecordManager::OpenVideo(const PString & strmId, const OpalMediaFormat & /*format*/)
556 {
557 if (m_videoMixer == NULL)
558 return false;
559
560 if (m_videoStream != NULL)
561 return m_videoMixer->AddStream(strmId);
562
563 PTRACE(4, "OpalRecord\tCreating AVI stream for video format '" << m_options.m_videoFormat << '\'');
564
565 PVideoFrameInfo yuv(m_options.m_videoWidth, m_options.m_videoHeight, "YUV420P");
566 PVideoFrameInfo rgb(m_options.m_videoWidth, m_options.m_videoHeight, "BGR24");
567 m_videoConverter = PColourConverter::Create(yuv, rgb);
568 if (m_videoConverter == NULL)
569 return false;
570 m_videoConverter->SetVFlipState(true);
571
572 AVISTREAMINFO info;
573 memset(&info, 0, sizeof(info));
574 info.fccType = streamtypeVIDEO;
575 info.dwRate = 1000;
576 info.dwScale = info.dwRate/m_options.m_videoRate;
577 info.rcFrame.right = m_options.m_videoWidth;
578 info.rcFrame.bottom = m_options.m_videoHeight;
579 info.dwSuggestedBufferSize = m_options.m_videoWidth*m_options.m_videoHeight*3/2;
580 info.dwQuality = (DWORD)-1;
581 strcpy(info.szName, VideoModeNames[m_options.m_videoMixing]);
582
583 if (IS_RESULT_ERROR(AVIFileCreateStream(m_file, &m_videoStream, &info), "creating AVI video stream"))
584 return false;
585
586 AVICOMPRESSOPTIONS opts;
587 memset(&opts, 0, sizeof(opts));
588 opts.fccType = streamtypeVIDEO;
589 opts.fccHandler = mmioFOURCC(m_options.m_videoFormat[0],
590 m_options.m_videoFormat[1],
591 m_options.m_videoFormat[2],
592 m_options.m_videoFormat[3]);
593 opts.dwQuality = (DWORD)-1;
594 if (IS_RESULT_ERROR(AVIMakeCompressedStream(&m_videoCompressor, m_videoStream, &opts, NULL), "creating AVI video compressor"))
595 return false;
596
597 BITMAPINFOHEADER fmt;
598 memset(&fmt, 0, sizeof(fmt));
599 fmt.biSize = sizeof(fmt);
600 fmt.biCompression = BI_RGB;
601 fmt.biWidth = m_options.m_videoWidth;
602 fmt.biHeight = m_options.m_videoHeight;
603 fmt.biBitCount = 24;
604 fmt.biPlanes = 1;
605 fmt.biSizeImage = rgb.CalculateFrameBytes();
606
607 if (IS_RESULT_ERROR(AVIStreamSetFormat(m_videoCompressor, 0, &fmt, sizeof(fmt)), "setting format of AVI video compressor"))
608 return false;
609
610 PTRACE(4, "OpalRecord\tAllocating video buffer " << fmt.biSizeImage << " bytes");
611 return m_videoBuffer.SetSize(fmt.biSizeImage) &&
612 m_videoMixer->AddStream(strmId);
613 }
614
615
OnMixedVideo(const RTP_DataFrame & frame)616 bool OpalAVIRecordManager::OnMixedVideo(const RTP_DataFrame & frame)
617 {
618 if (!IsOpen() || PAssertNULL(m_videoStream) == NULL || PAssertNULL(m_videoConverter) == NULL)
619 return false;
620
621 PluginCodec_Video_FrameHeader * header = (PluginCodec_Video_FrameHeader *)frame.GetPayloadPtr();
622 if (header->x != 0 || header->y != 0 || header->width != m_options.m_videoWidth || header->height != m_options.m_videoHeight) {
623 PTRACE(2, "OpalRecord\tUnexpected change of video frame size!");
624 return false;
625 }
626
627 PINDEX bytesReturned = 0;
628 if (!m_videoConverter->Convert(OPAL_VIDEO_FRAME_DATA_PTR(header), m_videoBuffer.GetPointer(), &bytesReturned)) {
629 PTRACE(2, "OpalRecord\tConversion of YUV420P to RGB24 failed!");
630 return false;
631 }
632
633 if (IS_RESULT_ERROR(AVIStreamWrite(m_videoCompressor,
634 m_VideoFrameCount, 1,
635 m_videoBuffer.GetPointer(), bytesReturned,
636 AVIIF_KEYFRAME, NULL, NULL), "writing AVI video stream"))
637 return false;
638
639 ++m_VideoFrameCount;
640 return true;
641 }
642
643
644 #endif // _WIN32
645
646 #endif // OPAL_HAS_MIXER
647
648
649 /////////////////////////////////////////////////////////////////////////////
650