Add autoconf stuff to git.
[rtaudio-cdist.git] / RtAudio.cpp
index d1920d5a12a3b2c295624fcc98d37a961caaf15e..64b15774c8642423fffb6492eac264469b930d60 100644 (file)
@@ -47,6 +47,7 @@
 #include <climits>
 #include <cmath>
 #include <algorithm>
+#include <cmath>
 
 // Static variable definitions.
 const unsigned int RtApi::MAX_SAMPLE_RATES = 14;
@@ -98,39 +99,95 @@ std::string RtAudio :: getVersion( void )
   return RTAUDIO_VERSION;
 }
 
-void RtAudio :: getCompiledApi( std::vector<RtAudio::Api> &apis )
-{
-  apis.clear();
+// Define API names and display names.
+// Must be in same order as API enum.
+extern "C" {
+const char* rtaudio_api_names[][2] = {
+  { "unspecified" , "Unknown" },
+  { "alsa"        , "ALSA" },
+  { "pulse"       , "Pulse" },
+  { "oss"         , "OpenSoundSystem" },
+  { "jack"        , "Jack" },
+  { "core"        , "CoreAudio" },
+  { "wasapi"      , "WASAPI" },
+  { "asio"        , "ASIO" },
+  { "ds"          , "DirectSound" },
+  { "dummy"       , "Dummy" },
+};
+const unsigned int rtaudio_num_api_names = 
+  sizeof(rtaudio_api_names)/sizeof(rtaudio_api_names[0]);
 
-  // The order here will control the order of RtAudio's API search in
-  // the constructor.
+// The order here will control the order of RtAudio's API search in
+// the constructor.
+extern "C" const RtAudio::Api rtaudio_compiled_apis[] = {
 #if defined(__UNIX_JACK__)
-  apis.push_back( UNIX_JACK );
+  RtAudio::UNIX_JACK,
 #endif
 #if defined(__LINUX_PULSE__)
-  apis.push_back( LINUX_PULSE );
+  RtAudio::LINUX_PULSE,
 #endif
 #if defined(__LINUX_ALSA__)
-  apis.push_back( LINUX_ALSA );
+  RtAudio::LINUX_ALSA,
 #endif
 #if defined(__LINUX_OSS__)
-  apis.push_back( LINUX_OSS );
+  RtAudio::LINUX_OSS,
 #endif
 #if defined(__WINDOWS_ASIO__)
-  apis.push_back( WINDOWS_ASIO );
+  RtAudio::WINDOWS_ASIO,
 #endif
 #if defined(__WINDOWS_WASAPI__)
-  apis.push_back( WINDOWS_WASAPI );
+  RtAudio::WINDOWS_WASAPI,
 #endif
 #if defined(__WINDOWS_DS__)
-  apis.push_back( WINDOWS_DS );
+  RtAudio::WINDOWS_DS,
 #endif
 #if defined(__MACOSX_CORE__)
-  apis.push_back( MACOSX_CORE );
+  RtAudio::MACOSX_CORE,
 #endif
 #if defined(__RTAUDIO_DUMMY__)
-  apis.push_back( RTAUDIO_DUMMY );
+  RtAudio::RTAUDIO_DUMMY,
 #endif
+  RtAudio::UNSPECIFIED,
+};
+extern "C" const unsigned int rtaudio_num_compiled_apis =
+  sizeof(rtaudio_compiled_apis)/sizeof(rtaudio_compiled_apis[0])-1;
+}
+
+// This is a compile-time check that rtaudio_num_api_names == RtAudio::NUM_APIS.
+// If the build breaks here, check that they match.
+template<bool b> class StaticAssert { private: StaticAssert() {} };
+template<> class StaticAssert<true>{ public: StaticAssert() {} };
+class StaticAssertions { StaticAssertions() {
+  StaticAssert<rtaudio_num_api_names == RtAudio::NUM_APIS>();
+}};
+
+void RtAudio :: getCompiledApi( std::vector<RtAudio::Api> &apis )
+{
+  apis = std::vector<RtAudio::Api>(rtaudio_compiled_apis,
+                                   rtaudio_compiled_apis + rtaudio_num_compiled_apis);
+}
+
+std::string RtAudio :: getApiName( RtAudio::Api api )
+{
+  if (api < 0 || api >= RtAudio::NUM_APIS)
+    return "";
+  return rtaudio_api_names[api][0];
+}
+
+std::string RtAudio :: getApiDisplayName( RtAudio::Api api )
+{
+  if (api < 0 || api >= RtAudio::NUM_APIS)
+    return "Unknown";
+  return rtaudio_api_names[api][1];
+}
+
+RtAudio::Api RtAudio :: getCompiledApiByName( const std::string &name )
+{
+  unsigned int i=0;
+  for (i = 0; i < rtaudio_num_compiled_apis; ++i)
+    if (name == rtaudio_api_names[rtaudio_compiled_apis[i]][0])
+      return rtaudio_compiled_apis[i];
+  return RtAudio::UNSPECIFIED;
 }
 
 void RtAudio :: openRtApi( RtAudio::Api api )
@@ -409,7 +466,7 @@ double RtApi :: getStreamTime( void )
   struct timeval then;
   struct timeval now;
 
-  if ( stream_.state != STREAM_RUNNING || stream_.streamTime == 0.0 )
+  if ( stream_.state != STREAM_RUNNING || (stream_.lastTickTimestamp.tv_sec == 0 && stream_.lastTickTimestamp.tv_usec == 0) )
     return stream_.streamTime;
 
   gettimeofday( &now, NULL );
@@ -440,6 +497,14 @@ unsigned int RtApi :: getStreamSampleRate( void )
  return stream_.sampleRate;
 }
 
+void RtApi :: startStream( void )
+{
+#if defined( HAVE_GETTIMEOFDAY )
+  stream_.lastTickTimestamp.tv_sec = 0;
+  stream_.lastTickTimestamp.tv_usec = 0;
+#endif
+}
+
 
 // *************************************************** //
 //
@@ -1479,12 +1544,17 @@ void RtApiCore :: closeStream( void )
 void RtApiCore :: startStream( void )
 {
   verifyStream();
+  RtApi::startStream();
   if ( stream_.state == STREAM_RUNNING ) {
     errorText_ = "RtApiCore::startStream(): the stream is already running!";
     error( RtAudioError::WARNING );
     return;
   }
 
+  #if defined( HAVE_GETTIMEOFDAY )
+  gettimeofday( &stream_.lastTickTimestamp, NULL );
+  #endif
+
   OSStatus result = noErr;
   CoreHandle *handle = (CoreHandle *) stream_.apiHandle;
   if ( stream_.mode == OUTPUT || stream_.mode == DUPLEX ) {
@@ -2437,12 +2507,17 @@ void RtApiJack :: closeStream( void )
 void RtApiJack :: startStream( void )
 {
   verifyStream();
+  RtApi::startStream();
   if ( stream_.state == STREAM_RUNNING ) {
     errorText_ = "RtApiJack::startStream(): the stream is already running!";
     error( RtAudioError::WARNING );
     return;
   }
 
+  #if defined( HAVE_GETTIMEOFDAY )
+  gettimeofday( &stream_.lastTickTimestamp, NULL );
+  #endif
+
   JackHandle *handle = (JackHandle *) stream_.apiHandle;
   int result = jack_activate( handle->client );
   if ( result ) {
@@ -3165,8 +3240,8 @@ bool RtApiAsio :: probeDeviceOpen( unsigned int device, StreamMode mode, unsigne
   result = ASIOCreateBuffers( handle->bufferInfos, nChannels, stream_.bufferSize, &asioCallbacks );
   if ( result != ASE_OK ) {
     // Standard method failed. This can happen with strict/misbehaving drivers that return valid buffer size ranges
-    // but only accept the preferred buffer size as parameter for ASIOCreateBuffers. eg. Creatives ASIO driver
-    // in that case, let's be naïve and try that instead
+    // but only accept the preferred buffer size as parameter for ASIOCreateBuffers (e.g. Creative's ASIO driver).
+    // In that case, let's be naïve and try that instead.
     *bufferSize = preferSize;
     stream_.bufferSize = *bufferSize;
     result = ASIOCreateBuffers( handle->bufferInfos, nChannels, stream_.bufferSize, &asioCallbacks );
@@ -3316,12 +3391,17 @@ bool stopThreadCalled = false;
 void RtApiAsio :: startStream()
 {
   verifyStream();
+  RtApi::startStream();
   if ( stream_.state == STREAM_RUNNING ) {
     errorText_ = "RtApiAsio::startStream(): the stream is already running!";
     error( RtAudioError::WARNING );
     return;
   }
 
+  #if defined( HAVE_GETTIMEOFDAY )
+  gettimeofday( &stream_.lastTickTimestamp, NULL );
+  #endif
+
   AsioHandle *handle = (AsioHandle *) stream_.apiHandle;
   ASIOError result = ASIOStart();
   if ( result != ASE_OK ) {
@@ -3699,7 +3779,15 @@ static const char* getAsioErrorString( ASIOError result )
 #include <audioclient.h>
 #include <avrt.h>
 #include <mmdeviceapi.h>
-#include <functiondiscoverykeys_devpkey.h>
+#include <FunctionDiscoveryKeys_devpkey.h>
+
+#ifndef MF_E_TRANSFORM_NEED_MORE_INPUT
+  #define MF_E_TRANSFORM_NEED_MORE_INPUT _HRESULT_TYPEDEF_(0xc00d6d72)
+#endif
+
+#ifndef MFSTARTUP_NOSOCKET
+  #define MFSTARTUP_NOSOCKET 0x1
+#endif
 
 #ifdef _MSC_VER
   #pragma comment( lib, "ksuser" )
@@ -3765,8 +3853,9 @@ public:
       relOutIndex += bufferSize_;
     }
 
-    // "in" index can end on the "out" index but cannot begin at it
-    if ( inIndex_ <= relOutIndex && inIndexEnd > relOutIndex ) {
+    // the "IN" index CAN BEGIN at the "OUT" index
+    // the "IN" index CANNOT END at the "OUT" index
+    if ( inIndex_ < relOutIndex && inIndexEnd >= relOutIndex ) {
       return false; // not enough space between "in" index and "out" index
     }
 
@@ -3826,8 +3915,9 @@ public:
       relInIndex += bufferSize_;
     }
 
-    // "out" index can begin at and end on the "in" index
-    if ( outIndex_ < relInIndex && outIndexEnd > relInIndex ) {
+    // the "OUT" index CANNOT BEGIN at the "IN" index
+    // the "OUT" index CAN END at the "IN" index
+    if ( outIndex_ <= relInIndex && outIndexEnd > relInIndex ) {
       return false; // not enough space between "out" index and "in" index
     }
 
@@ -3944,17 +4034,17 @@ public:
 
     // 4. Send stream start messages to Resampler
 
-    _transform->ProcessMessage( MFT_MESSAGE_COMMAND_FLUSH, NULL );
-    _transform->ProcessMessage( MFT_MESSAGE_NOTIFY_BEGIN_STREAMING, NULL );
-    _transform->ProcessMessage( MFT_MESSAGE_NOTIFY_START_OF_STREAM, NULL );
+    _transform->ProcessMessage( MFT_MESSAGE_COMMAND_FLUSH, 0 );
+    _transform->ProcessMessage( MFT_MESSAGE_NOTIFY_BEGIN_STREAMING, 0 );
+    _transform->ProcessMessage( MFT_MESSAGE_NOTIFY_START_OF_STREAM, 0 );
   }
 
   ~WasapiResampler()
   {
     // 8. Send stream stop messages to Resampler
 
-    _transform->ProcessMessage( MFT_MESSAGE_NOTIFY_END_OF_STREAM, NULL );
-    _transform->ProcessMessage( MFT_MESSAGE_NOTIFY_END_STREAMING, NULL );
+    _transform->ProcessMessage( MFT_MESSAGE_NOTIFY_END_OF_STREAM, 0 );
+    _transform->ProcessMessage( MFT_MESSAGE_NOTIFY_END_STREAMING, 0 );
 
     // 9. Cleanup
 
@@ -4106,10 +4196,9 @@ RtApiWasapi::RtApiWasapi()
                          CLSCTX_ALL, __uuidof( IMMDeviceEnumerator ),
                          ( void** ) &deviceEnumerator_ );
 
-  if ( FAILED( hr ) ) {
-    errorText_ = "RtApiWasapi::RtApiWasapi: Unable to instantiate device enumerator";
-    error( RtAudioError::DRIVER_ERROR );
-  }
+  // If this runs on an old Windows, it will fail. Ignore and proceed.
+  if ( FAILED( hr ) )
+    deviceEnumerator_ = NULL;
 }
 
 //-----------------------------------------------------------------------------
@@ -4136,6 +4225,9 @@ unsigned int RtApiWasapi::getDeviceCount( void )
   IMMDeviceCollection* captureDevices = NULL;
   IMMDeviceCollection* renderDevices = NULL;
 
+  if ( !deviceEnumerator_ )
+    return 0;
+
   // Count capture devices
   errorText_.clear();
   HRESULT hr = deviceEnumerator_->EnumAudioEndpoints( eCapture, DEVICE_STATE_ACTIVE, &captureDevices );
@@ -4478,6 +4570,7 @@ void RtApiWasapi::closeStream( void )
 void RtApiWasapi::startStream( void )
 {
   verifyStream();
+  RtApi::startStream();
 
   if ( stream_.state == STREAM_RUNNING ) {
     errorText_ = "RtApiWasapi::startStream: The stream is already running.";
@@ -4485,6 +4578,10 @@ void RtApiWasapi::startStream( void )
     return;
   }
 
+  #if defined( HAVE_GETTIMEOFDAY )
+  gettimeofday( &stream_.lastTickTimestamp, NULL );
+  #endif
+
   // update stream state
   stream_.state = STREAM_RUNNING;
 
@@ -4524,26 +4621,6 @@ void RtApiWasapi::stopStream( void )
   // Wait for the last buffer to play before stopping.
   Sleep( 1000 * stream_.bufferSize / stream_.sampleRate );
 
-  // stop capture client if applicable
-  if ( ( ( WasapiHandle* ) stream_.apiHandle )->captureAudioClient ) {
-    HRESULT hr = ( ( WasapiHandle* ) stream_.apiHandle )->captureAudioClient->Stop();
-    if ( FAILED( hr ) ) {
-      errorText_ = "RtApiWasapi::stopStream: Unable to stop capture stream.";
-      error( RtAudioError::DRIVER_ERROR );
-      return;
-    }
-  }
-
-  // stop render client if applicable
-  if ( ( ( WasapiHandle* ) stream_.apiHandle )->renderAudioClient ) {
-    HRESULT hr = ( ( WasapiHandle* ) stream_.apiHandle )->renderAudioClient->Stop();
-    if ( FAILED( hr ) ) {
-      errorText_ = "RtApiWasapi::stopStream: Unable to stop render stream.";
-      error( RtAudioError::DRIVER_ERROR );
-      return;
-    }
-  }
-
   // close thread handle
   if ( stream_.callbackInfo.thread && !CloseHandle( ( void* ) stream_.callbackInfo.thread ) ) {
     errorText_ = "RtApiWasapi::stopStream: Unable to close callback thread.";
@@ -4574,26 +4651,6 @@ void RtApiWasapi::abortStream( void )
     Sleep( 1 );
   }
 
-  // stop capture client if applicable
-  if ( ( ( WasapiHandle* ) stream_.apiHandle )->captureAudioClient ) {
-    HRESULT hr = ( ( WasapiHandle* ) stream_.apiHandle )->captureAudioClient->Stop();
-    if ( FAILED( hr ) ) {
-      errorText_ = "RtApiWasapi::abortStream: Unable to stop capture stream.";
-      error( RtAudioError::DRIVER_ERROR );
-      return;
-    }
-  }
-
-  // stop render client if applicable
-  if ( ( ( WasapiHandle* ) stream_.apiHandle )->renderAudioClient ) {
-    HRESULT hr = ( ( WasapiHandle* ) stream_.apiHandle )->renderAudioClient->Stop();
-    if ( FAILED( hr ) ) {
-      errorText_ = "RtApiWasapi::abortStream: Unable to stop render stream.";
-      error( RtAudioError::DRIVER_ERROR );
-      return;
-    }
-  }
-
   // close thread handle
   if ( stream_.callbackInfo.thread && !CloseHandle( ( void* ) stream_.callbackInfo.thread ) ) {
     errorText_ = "RtApiWasapi::abortStream: Unable to close callback thread.";
@@ -4661,7 +4718,7 @@ bool RtApiWasapi::probeDeviceOpen( unsigned int device, StreamMode mode, unsigne
     goto Exit;
   }
 
-  // determine whether index falls within capture or render devices
+  // if device index falls within capture devices
   if ( device >= renderDeviceCount ) {
     if ( mode != INPUT ) {
       errorType = RtAudioError::INVALID_USE;
@@ -4681,28 +4738,66 @@ bool RtApiWasapi::probeDeviceOpen( unsigned int device, StreamMode mode, unsigne
     hr = devicePtr->Activate( __uuidof( IAudioClient ), CLSCTX_ALL,
                               NULL, ( void** ) &captureAudioClient );
     if ( FAILED( hr ) ) {
-      errorText_ = "RtApiWasapi::probeDeviceOpen: Unable to retrieve device audio client.";
+      errorText_ = "RtApiWasapi::probeDeviceOpen: Unable to retrieve capture device audio client.";
       goto Exit;
     }
 
     hr = captureAudioClient->GetMixFormat( &deviceFormat );
     if ( FAILED( hr ) ) {
-      errorText_ = "RtApiWasapi::probeDeviceOpen: Unable to retrieve device mix format.";
+      errorText_ = "RtApiWasapi::probeDeviceOpen: Unable to retrieve capture device mix format.";
       goto Exit;
     }
 
     stream_.nDeviceChannels[mode] = deviceFormat->nChannels;
     captureAudioClient->GetStreamLatency( ( long long* ) &stream_.latency[mode] );
   }
-  else {
-    if ( mode != OUTPUT ) {
-      errorType = RtAudioError::INVALID_USE;
-      errorText_ = "RtApiWasapi::probeDeviceOpen: Render device selected as input device.";
+
+  // if device index falls within render devices and is configured for loopback
+  if ( device < renderDeviceCount && mode == INPUT )
+  {
+    // if renderAudioClient is not initialised, initialise it now
+    IAudioClient*& renderAudioClient = ( ( WasapiHandle* ) stream_.apiHandle )->renderAudioClient;
+    if ( !renderAudioClient )
+    {
+      probeDeviceOpen( device, OUTPUT, channels, firstChannel, sampleRate, format, bufferSize, options );
+    }
+
+    // retrieve captureAudioClient from devicePtr
+    IAudioClient*& captureAudioClient = ( ( WasapiHandle* ) stream_.apiHandle )->captureAudioClient;
+
+    hr = renderDevices->Item( device, &devicePtr );
+    if ( FAILED( hr ) ) {
+      errorText_ = "RtApiWasapi::probeDeviceOpen: Unable to retrieve render device handle.";
       goto Exit;
     }
 
-    // retrieve renderAudioClient from devicePtr
+    hr = devicePtr->Activate( __uuidof( IAudioClient ), CLSCTX_ALL,
+                              NULL, ( void** ) &captureAudioClient );
+    if ( FAILED( hr ) ) {
+      errorText_ = "RtApiWasapi::probeDeviceOpen: Unable to retrieve render device audio client.";
+      goto Exit;
+    }
+
+    hr = captureAudioClient->GetMixFormat( &deviceFormat );
+    if ( FAILED( hr ) ) {
+      errorText_ = "RtApiWasapi::probeDeviceOpen: Unable to retrieve render device mix format.";
+      goto Exit;
+    }
+
+    stream_.nDeviceChannels[mode] = deviceFormat->nChannels;
+    captureAudioClient->GetStreamLatency( ( long long* ) &stream_.latency[mode] );
+  }
+
+  // if device index falls within render devices and is configured for output
+  if ( device < renderDeviceCount && mode == OUTPUT )
+  {
+    // if renderAudioClient is already initialised, don't initialise it again
     IAudioClient*& renderAudioClient = ( ( WasapiHandle* ) stream_.apiHandle )->renderAudioClient;
+    if ( renderAudioClient )
+    {
+      methodResult = SUCCESS;
+      goto Exit;
+    }
 
     hr = renderDevices->Item( device, &devicePtr );
     if ( FAILED( hr ) ) {
@@ -4713,13 +4808,13 @@ bool RtApiWasapi::probeDeviceOpen( unsigned int device, StreamMode mode, unsigne
     hr = devicePtr->Activate( __uuidof( IAudioClient ), CLSCTX_ALL,
                               NULL, ( void** ) &renderAudioClient );
     if ( FAILED( hr ) ) {
-      errorText_ = "RtApiWasapi::probeDeviceOpen: Unable to retrieve device audio client.";
+      errorText_ = "RtApiWasapi::probeDeviceOpen: Unable to retrieve render device audio client.";
       goto Exit;
     }
 
     hr = renderAudioClient->GetMixFormat( &deviceFormat );
     if ( FAILED( hr ) ) {
-      errorText_ = "RtApiWasapi::probeDeviceOpen: Unable to retrieve device mix format.";
+      errorText_ = "RtApiWasapi::probeDeviceOpen: Unable to retrieve render device mix format.";
       goto Exit;
     }
 
@@ -4859,6 +4954,7 @@ void RtApiWasapi::wasapiThread()
   unsigned int bufferFrameCount = 0;
   unsigned int numFramesPadding = 0;
   unsigned int convBufferSize = 0;
+  bool loopbackEnabled = stream_.device[INPUT] == stream_.device[OUTPUT];
   bool callbackPushed = true;
   bool callbackPulled = false;
   bool callbackStopped = false;
@@ -4869,14 +4965,15 @@ void RtApiWasapi::wasapiThread()
   unsigned int convBuffSize = 0;
   unsigned int deviceBuffSize = 0;
 
-  errorText_.clear();
+  std::string errorText;
   RtAudioError::Type errorType = RtAudioError::DRIVER_ERROR;
 
   // Attempt to assign "Pro Audio" characteristic to thread
   HMODULE AvrtDll = LoadLibrary( (LPCTSTR) "AVRT.dll" );
   if ( AvrtDll ) {
     DWORD taskIndex = 0;
-    TAvSetMmThreadCharacteristicsPtr AvSetMmThreadCharacteristicsPtr = ( TAvSetMmThreadCharacteristicsPtr ) GetProcAddress( AvrtDll, "AvSetMmThreadCharacteristicsW" );
+    TAvSetMmThreadCharacteristicsPtr AvSetMmThreadCharacteristicsPtr =
+      ( TAvSetMmThreadCharacteristicsPtr ) (void(*)()) GetProcAddress( AvrtDll, "AvSetMmThreadCharacteristicsW" );
     AvSetMmThreadCharacteristicsPtr( L"Pro Audio", &taskIndex );
     FreeLibrary( AvrtDll );
   }
@@ -4885,7 +4982,7 @@ void RtApiWasapi::wasapiThread()
   if ( captureAudioClient ) {
     hr = captureAudioClient->GetMixFormat( &captureFormat );
     if ( FAILED( hr ) ) {
-      errorText_ = "RtApiWasapi::wasapiThread: Unable to retrieve device mix format.";
+      errorText = "RtApiWasapi::wasapiThread: Unable to retrieve device mix format.";
       goto Exit;
     }
 
@@ -4896,51 +4993,66 @@ void RtApiWasapi::wasapiThread()
 
     captureSrRatio = ( ( float ) captureFormat->nSamplesPerSec / stream_.sampleRate );
 
-    // initialize capture stream according to desire buffer size
-    float desiredBufferSize = stream_.bufferSize * captureSrRatio;
-    REFERENCE_TIME desiredBufferPeriod = ( REFERENCE_TIME ) ( ( float ) desiredBufferSize * 10000000 / captureFormat->nSamplesPerSec );
-
     if ( !captureClient ) {
       hr = captureAudioClient->Initialize( AUDCLNT_SHAREMODE_SHARED,
-                                           AUDCLNT_STREAMFLAGS_EVENTCALLBACK,
-                                           desiredBufferPeriod,
-                                           desiredBufferPeriod,
+                                           loopbackEnabled ? AUDCLNT_STREAMFLAGS_LOOPBACK : AUDCLNT_STREAMFLAGS_EVENTCALLBACK,
+                                           0,
+                                           0,
                                            captureFormat,
                                            NULL );
       if ( FAILED( hr ) ) {
-        errorText_ = "RtApiWasapi::wasapiThread: Unable to initialize capture audio client.";
+        errorText = "RtApiWasapi::wasapiThread: Unable to initialize capture audio client.";
         goto Exit;
       }
 
       hr = captureAudioClient->GetService( __uuidof( IAudioCaptureClient ),
                                            ( void** ) &captureClient );
       if ( FAILED( hr ) ) {
-        errorText_ = "RtApiWasapi::wasapiThread: Unable to retrieve capture client handle.";
+        errorText = "RtApiWasapi::wasapiThread: Unable to retrieve capture client handle.";
         goto Exit;
       }
 
-      // configure captureEvent to trigger on every available capture buffer
-      captureEvent = CreateEvent( NULL, FALSE, FALSE, NULL );
-      if ( !captureEvent ) {
-        errorType = RtAudioError::SYSTEM_ERROR;
-        errorText_ = "RtApiWasapi::wasapiThread: Unable to create capture event.";
-        goto Exit;
+      // don't configure captureEvent if in loopback mode
+      if ( !loopbackEnabled )
+      {
+        // configure captureEvent to trigger on every available capture buffer
+        captureEvent = CreateEvent( NULL, FALSE, FALSE, NULL );
+        if ( !captureEvent ) {
+          errorType = RtAudioError::SYSTEM_ERROR;
+          errorText = "RtApiWasapi::wasapiThread: Unable to create capture event.";
+          goto Exit;
+        }
+
+        hr = captureAudioClient->SetEventHandle( captureEvent );
+        if ( FAILED( hr ) ) {
+          errorText = "RtApiWasapi::wasapiThread: Unable to set capture event handle.";
+          goto Exit;
+        }
+
+        ( ( WasapiHandle* ) stream_.apiHandle )->captureEvent = captureEvent;
       }
 
-      hr = captureAudioClient->SetEventHandle( captureEvent );
+      ( ( WasapiHandle* ) stream_.apiHandle )->captureClient = captureClient;
+
+      // reset the capture stream
+      hr = captureAudioClient->Reset();
       if ( FAILED( hr ) ) {
-        errorText_ = "RtApiWasapi::wasapiThread: Unable to set capture event handle.";
+        errorText = "RtApiWasapi::wasapiThread: Unable to reset capture stream.";
         goto Exit;
       }
 
-      ( ( WasapiHandle* ) stream_.apiHandle )->captureClient = captureClient;
-      ( ( WasapiHandle* ) stream_.apiHandle )->captureEvent = captureEvent;
+      // start the capture stream
+      hr = captureAudioClient->Start();
+      if ( FAILED( hr ) ) {
+        errorText = "RtApiWasapi::wasapiThread: Unable to start capture stream.";
+        goto Exit;
+      }
     }
 
     unsigned int inBufferSize = 0;
     hr = captureAudioClient->GetBufferSize( &inBufferSize );
     if ( FAILED( hr ) ) {
-      errorText_ = "RtApiWasapi::wasapiThread: Unable to get capture buffer size.";
+      errorText = "RtApiWasapi::wasapiThread: Unable to get capture buffer size.";
       goto Exit;
     }
 
@@ -4950,27 +5062,13 @@ void RtApiWasapi::wasapiThread()
 
     // set captureBuffer size
     captureBuffer.setBufferSize( inBufferSize + outBufferSize, formatBytes( stream_.deviceFormat[INPUT] ) );
-
-    // reset the capture stream
-    hr = captureAudioClient->Reset();
-    if ( FAILED( hr ) ) {
-      errorText_ = "RtApiWasapi::wasapiThread: Unable to reset capture stream.";
-      goto Exit;
-    }
-
-    // start the capture stream
-    hr = captureAudioClient->Start();
-    if ( FAILED( hr ) ) {
-      errorText_ = "RtApiWasapi::wasapiThread: Unable to start capture stream.";
-      goto Exit;
-    }
   }
 
   // start render stream if applicable
   if ( renderAudioClient ) {
     hr = renderAudioClient->GetMixFormat( &renderFormat );
     if ( FAILED( hr ) ) {
-      errorText_ = "RtApiWasapi::wasapiThread: Unable to retrieve device mix format.";
+      errorText = "RtApiWasapi::wasapiThread: Unable to retrieve device mix format.";
       goto Exit;
     }
 
@@ -4981,26 +5079,22 @@ void RtApiWasapi::wasapiThread()
 
     renderSrRatio = ( ( float ) renderFormat->nSamplesPerSec / stream_.sampleRate );
 
-    // initialize render stream according to desire buffer size
-    float desiredBufferSize = stream_.bufferSize * renderSrRatio;
-    REFERENCE_TIME desiredBufferPeriod = ( REFERENCE_TIME ) ( ( float ) desiredBufferSize * 10000000 / renderFormat->nSamplesPerSec );
-
     if ( !renderClient ) {
       hr = renderAudioClient->Initialize( AUDCLNT_SHAREMODE_SHARED,
                                           AUDCLNT_STREAMFLAGS_EVENTCALLBACK,
-                                          desiredBufferPeriod,
-                                          desiredBufferPeriod,
+                                          0,
+                                          0,
                                           renderFormat,
                                           NULL );
       if ( FAILED( hr ) ) {
-        errorText_ = "RtApiWasapi::wasapiThread: Unable to initialize render audio client.";
+        errorText = "RtApiWasapi::wasapiThread: Unable to initialize render audio client.";
         goto Exit;
       }
 
       hr = renderAudioClient->GetService( __uuidof( IAudioRenderClient ),
                                           ( void** ) &renderClient );
       if ( FAILED( hr ) ) {
-        errorText_ = "RtApiWasapi::wasapiThread: Unable to retrieve render client handle.";
+        errorText = "RtApiWasapi::wasapiThread: Unable to retrieve render client handle.";
         goto Exit;
       }
 
@@ -5008,24 +5102,38 @@ void RtApiWasapi::wasapiThread()
       renderEvent = CreateEvent( NULL, FALSE, FALSE, NULL );
       if ( !renderEvent ) {
         errorType = RtAudioError::SYSTEM_ERROR;
-        errorText_ = "RtApiWasapi::wasapiThread: Unable to create render event.";
+        errorText = "RtApiWasapi::wasapiThread: Unable to create render event.";
         goto Exit;
       }
 
       hr = renderAudioClient->SetEventHandle( renderEvent );
       if ( FAILED( hr ) ) {
-        errorText_ = "RtApiWasapi::wasapiThread: Unable to set render event handle.";
+        errorText = "RtApiWasapi::wasapiThread: Unable to set render event handle.";
         goto Exit;
       }
 
       ( ( WasapiHandle* ) stream_.apiHandle )->renderClient = renderClient;
       ( ( WasapiHandle* ) stream_.apiHandle )->renderEvent = renderEvent;
+
+      // reset the render stream
+      hr = renderAudioClient->Reset();
+      if ( FAILED( hr ) ) {
+        errorText = "RtApiWasapi::wasapiThread: Unable to reset render stream.";
+        goto Exit;
+      }
+
+      // start the render stream
+      hr = renderAudioClient->Start();
+      if ( FAILED( hr ) ) {
+        errorText = "RtApiWasapi::wasapiThread: Unable to start render stream.";
+        goto Exit;
+      }
     }
 
     unsigned int outBufferSize = 0;
     hr = renderAudioClient->GetBufferSize( &outBufferSize );
     if ( FAILED( hr ) ) {
-      errorText_ = "RtApiWasapi::wasapiThread: Unable to get render buffer size.";
+      errorText = "RtApiWasapi::wasapiThread: Unable to get render buffer size.";
       goto Exit;
     }
 
@@ -5035,20 +5143,6 @@ void RtApiWasapi::wasapiThread()
 
     // set renderBuffer size
     renderBuffer.setBufferSize( inBufferSize + outBufferSize, formatBytes( stream_.deviceFormat[OUTPUT] ) );
-
-    // reset the render stream
-    hr = renderAudioClient->Reset();
-    if ( FAILED( hr ) ) {
-      errorText_ = "RtApiWasapi::wasapiThread: Unable to reset render stream.";
-      goto Exit;
-    }
-
-    // start the render stream
-    hr = renderAudioClient->Start();
-    if ( FAILED( hr ) ) {
-      errorText_ = "RtApiWasapi::wasapiThread: Unable to start render stream.";
-      goto Exit;
-    }
   }
 
   // malloc buffer memory
@@ -5072,11 +5166,11 @@ void RtApiWasapi::wasapiThread()
   }
 
   convBuffSize *= 2; // allow overflow for *SrRatio remainders
-  convBuffer = ( char* ) malloc( convBuffSize );
-  stream_.deviceBuffer = ( char* ) malloc( deviceBuffSize );
+  convBuffer = ( char* ) calloc( convBuffSize, 1 );
+  stream_.deviceBuffer = ( char* ) calloc( deviceBuffSize, 1 );
   if ( !convBuffer || !stream_.deviceBuffer ) {
     errorType = RtAudioError::MEMORY_ERROR;
-    errorText_ = "RtApiWasapi::wasapiThread: Error allocating device buffer memory.";
+    errorText = "RtApiWasapi::wasapiThread: Error allocating device buffer memory.";
     goto Exit;
   }
 
@@ -5160,18 +5254,21 @@ void RtApiWasapi::wasapiThread()
                                    captureFlags & AUDCLNT_BUFFERFLAGS_DATA_DISCONTINUITY ? RTAUDIO_INPUT_OVERFLOW : 0,
                                    stream_.callbackInfo.userData );
 
+        // tick stream time
+        RtApi::tickStreamTime();
+
         // Handle return value from callback
         if ( callbackResult == 1 ) {
           // instantiate a thread to stop this thread
           HANDLE threadHandle = CreateThread( NULL, 0, stopWasapiThread, this, 0, NULL );
           if ( !threadHandle ) {
             errorType = RtAudioError::THREAD_ERROR;
-            errorText_ = "RtApiWasapi::wasapiThread: Unable to instantiate stream stop thread.";
+            errorText = "RtApiWasapi::wasapiThread: Unable to instantiate stream stop thread.";
             goto Exit;
           }
           else if ( !CloseHandle( threadHandle ) ) {
             errorType = RtAudioError::THREAD_ERROR;
-            errorText_ = "RtApiWasapi::wasapiThread: Unable to close stream stop thread handle.";
+            errorText = "RtApiWasapi::wasapiThread: Unable to close stream stop thread handle.";
             goto Exit;
           }
 
@@ -5182,12 +5279,12 @@ void RtApiWasapi::wasapiThread()
           HANDLE threadHandle = CreateThread( NULL, 0, abortWasapiThread, this, 0, NULL );
           if ( !threadHandle ) {
             errorType = RtAudioError::THREAD_ERROR;
-            errorText_ = "RtApiWasapi::wasapiThread: Unable to instantiate stream abort thread.";
+            errorText = "RtApiWasapi::wasapiThread: Unable to instantiate stream abort thread.";
             goto Exit;
           }
           else if ( !CloseHandle( threadHandle ) ) {
             errorType = RtAudioError::THREAD_ERROR;
-            errorText_ = "RtApiWasapi::wasapiThread: Unable to close stream abort thread handle.";
+            errorText = "RtApiWasapi::wasapiThread: Unable to close stream abort thread handle.";
             goto Exit;
           }
 
@@ -5215,6 +5312,12 @@ void RtApiWasapi::wasapiThread()
                          stream_.convertInfo[OUTPUT] );
 
         }
+        else {
+          // no further conversion, simple copy userBuffer to deviceBuffer
+          memcpy( stream_.deviceBuffer,
+                  stream_.userBuffer[OUTPUT],
+                  stream_.bufferSize * stream_.nUserChannels[OUTPUT] * formatBytes( stream_.userFormat ) );
+        }
 
         // Convert callback buffer to stream sample rate
         renderResampler->Convert( convBuffer,
@@ -5242,7 +5345,7 @@ void RtApiWasapi::wasapiThread()
     if ( captureAudioClient ) {
       // if the callback input buffer was not pulled from captureBuffer, wait for next capture event
       if ( !callbackPulled ) {
-        WaitForSingleObject( captureEvent, INFINITE );
+        WaitForSingleObject( loopbackEnabled ? renderEvent : captureEvent, INFINITE );
       }
 
       // Get capture buffer from stream
@@ -5250,7 +5353,7 @@ void RtApiWasapi::wasapiThread()
                                      &bufferFrameCount,
                                      &captureFlags, NULL, NULL );
       if ( FAILED( hr ) ) {
-        errorText_ = "RtApiWasapi::wasapiThread: Unable to retrieve capture buffer.";
+        errorText = "RtApiWasapi::wasapiThread: Unable to retrieve capture buffer.";
         goto Exit;
       }
 
@@ -5263,7 +5366,7 @@ void RtApiWasapi::wasapiThread()
           // Release capture buffer
           hr = captureClient->ReleaseBuffer( bufferFrameCount );
           if ( FAILED( hr ) ) {
-            errorText_ = "RtApiWasapi::wasapiThread: Unable to release capture buffer.";
+            errorText = "RtApiWasapi::wasapiThread: Unable to release capture buffer.";
             goto Exit;
           }
         }
@@ -5272,7 +5375,7 @@ void RtApiWasapi::wasapiThread()
           // Inform WASAPI that capture was unsuccessful
           hr = captureClient->ReleaseBuffer( 0 );
           if ( FAILED( hr ) ) {
-            errorText_ = "RtApiWasapi::wasapiThread: Unable to release capture buffer.";
+            errorText = "RtApiWasapi::wasapiThread: Unable to release capture buffer.";
             goto Exit;
           }
         }
@@ -5282,7 +5385,7 @@ void RtApiWasapi::wasapiThread()
         // Inform WASAPI that capture was unsuccessful
         hr = captureClient->ReleaseBuffer( 0 );
         if ( FAILED( hr ) ) {
-          errorText_ = "RtApiWasapi::wasapiThread: Unable to release capture buffer.";
+          errorText = "RtApiWasapi::wasapiThread: Unable to release capture buffer.";
           goto Exit;
         }
       }
@@ -5304,13 +5407,13 @@ void RtApiWasapi::wasapiThread()
       // Get render buffer from stream
       hr = renderAudioClient->GetBufferSize( &bufferFrameCount );
       if ( FAILED( hr ) ) {
-        errorText_ = "RtApiWasapi::wasapiThread: Unable to retrieve render buffer size.";
+        errorText = "RtApiWasapi::wasapiThread: Unable to retrieve render buffer size.";
         goto Exit;
       }
 
       hr = renderAudioClient->GetCurrentPadding( &numFramesPadding );
       if ( FAILED( hr ) ) {
-        errorText_ = "RtApiWasapi::wasapiThread: Unable to retrieve render buffer padding.";
+        errorText = "RtApiWasapi::wasapiThread: Unable to retrieve render buffer padding.";
         goto Exit;
       }
 
@@ -5319,7 +5422,7 @@ void RtApiWasapi::wasapiThread()
       if ( bufferFrameCount != 0 ) {
         hr = renderClient->GetBuffer( bufferFrameCount, &streamBuffer );
         if ( FAILED( hr ) ) {
-          errorText_ = "RtApiWasapi::wasapiThread: Unable to retrieve render buffer.";
+          errorText = "RtApiWasapi::wasapiThread: Unable to retrieve render buffer.";
           goto Exit;
         }
 
@@ -5332,7 +5435,7 @@ void RtApiWasapi::wasapiThread()
           // Release render buffer
           hr = renderClient->ReleaseBuffer( bufferFrameCount, 0 );
           if ( FAILED( hr ) ) {
-            errorText_ = "RtApiWasapi::wasapiThread: Unable to release render buffer.";
+            errorText = "RtApiWasapi::wasapiThread: Unable to release render buffer.";
             goto Exit;
           }
         }
@@ -5341,7 +5444,7 @@ void RtApiWasapi::wasapiThread()
           // Inform WASAPI that render was unsuccessful
           hr = renderClient->ReleaseBuffer( 0, 0 );
           if ( FAILED( hr ) ) {
-            errorText_ = "RtApiWasapi::wasapiThread: Unable to release render buffer.";
+            errorText = "RtApiWasapi::wasapiThread: Unable to release render buffer.";
             goto Exit;
           }
         }
@@ -5351,7 +5454,7 @@ void RtApiWasapi::wasapiThread()
         // Inform WASAPI that render was unsuccessful
         hr = renderClient->ReleaseBuffer( 0, 0 );
         if ( FAILED( hr ) ) {
-          errorText_ = "RtApiWasapi::wasapiThread: Unable to release render buffer.";
+          errorText = "RtApiWasapi::wasapiThread: Unable to release render buffer.";
           goto Exit;
         }
       }
@@ -5362,9 +5465,6 @@ void RtApiWasapi::wasapiThread()
       // unsetting the callbackPulled flag lets the stream know that
       // the audio device is ready for another callback output buffer.
       callbackPulled = false;
-
-      // tick stream time
-      RtApi::tickStreamTime();
     }
 
   }
@@ -5380,11 +5480,14 @@ Exit:
 
   CoUninitialize();
 
-  if ( !errorText_.empty() )
-    error( errorType );
-
   // update stream state
   stream_.state = STREAM_STOPPED;
+
+  if ( !errorText.empty() )
+  {
+    errorText_ = errorText;
+    error( errorType );
+  }
 }
 
 //******************** End of __WINDOWS_WASAPI__ *********************//
@@ -6275,12 +6378,17 @@ void RtApiDs :: closeStream()
 void RtApiDs :: startStream()
 {
   verifyStream();
+  RtApi::startStream();
   if ( stream_.state == STREAM_RUNNING ) {
     errorText_ = "RtApiDs::startStream(): the stream is already running!";
     error( RtAudioError::WARNING );
     return;
   }
 
+  #if defined( HAVE_GETTIMEOFDAY )
+  gettimeofday( &stream_.lastTickTimestamp, NULL );
+  #endif
+
   DsHandle *handle = (DsHandle *) stream_.apiHandle;
 
   // Increase scheduler frequency on lesser windows (a side-effect of
@@ -6331,6 +6439,7 @@ void RtApiDs :: startStream()
 void RtApiDs :: stopStream()
 {
   verifyStream();
+  RtApi::startStream();
   if ( stream_.state == STREAM_STOPPED ) {
     errorText_ = "RtApiDs::stopStream(): the stream is already stopped!";
     error( RtAudioError::WARNING );
@@ -7048,7 +7157,7 @@ unsigned int RtApiAlsa :: getDeviceCount( void )
   unsigned nDevices = 0;
   int result, subdevice, card;
   char name[64];
-  snd_ctl_t *handle;
+  snd_ctl_t *handle = 0;
 
   // Count cards and devices
   card = -1;
@@ -7057,6 +7166,7 @@ unsigned int RtApiAlsa :: getDeviceCount( void )
     sprintf( name, "hw:%d", card );
     result = snd_ctl_open( &handle, name, 0 );
     if ( result < 0 ) {
+      handle = 0;
       errorStream_ << "RtApiAlsa::getDeviceCount: control open, card = " << card << ", " << snd_strerror( result ) << ".";
       errorText_ = errorStream_.str();
       error( RtAudioError::WARNING );
@@ -7076,7 +7186,8 @@ unsigned int RtApiAlsa :: getDeviceCount( void )
       nDevices++;
     }
   nextcard:
-    snd_ctl_close( handle );
+    if ( handle )
+        snd_ctl_close( handle );
     snd_card_next( &card );
   }
 
@@ -7097,7 +7208,7 @@ RtAudio::DeviceInfo RtApiAlsa :: getDeviceInfo( unsigned int device )
   unsigned nDevices = 0;
   int result, subdevice, card;
   char name[64];
-  snd_ctl_t *chandle;
+  snd_ctl_t *chandle = 0;
 
   // Count cards and devices
   card = -1;
@@ -7107,6 +7218,7 @@ RtAudio::DeviceInfo RtApiAlsa :: getDeviceInfo( unsigned int device )
     sprintf( name, "hw:%d", card );
     result = snd_ctl_open( &chandle, name, SND_CTL_NONBLOCK );
     if ( result < 0 ) {
+      chandle = 0;
       errorStream_ << "RtApiAlsa::getDeviceInfo: control open, card = " << card << ", " << snd_strerror( result ) << ".";
       errorText_ = errorStream_.str();
       error( RtAudioError::WARNING );
@@ -7129,7 +7241,8 @@ RtAudio::DeviceInfo RtApiAlsa :: getDeviceInfo( unsigned int device )
       nDevices++;
     }
   nextcard:
-    snd_ctl_close( chandle );
+    if ( chandle )
+        snd_ctl_close( chandle );
     snd_card_next( &card );
   }
 
@@ -7438,10 +7551,12 @@ bool RtApiAlsa :: probeDeviceOpen( unsigned int device, StreamMode mode, unsigne
     if ( result == 0 ) {
       if ( nDevices == device ) {
         strcpy( name, "default" );
+        snd_ctl_close( chandle );
         goto foundDevice;
       }
       nDevices++;
     }
+    snd_ctl_close( chandle );
 
     if ( nDevices == 0 ) {
       // This should not happen because a check is made before this function is called.
@@ -7841,7 +7956,7 @@ bool RtApiAlsa :: probeDeviceOpen( unsigned int device, StreamMode mode, unsigne
     pthread_attr_t attr;
     pthread_attr_init( &attr );
     pthread_attr_setdetachstate( &attr, PTHREAD_CREATE_JOINABLE );
-#ifdef SCHED_RR // Undefined with some OSes (eg: NetBSD 1.6.x with GNU Pthread)
+#ifdef SCHED_RR // Undefined with some OSes (e.g. NetBSD 1.6.x with GNU Pthread)
     if ( options && options->flags & RTAUDIO_SCHEDULE_REALTIME ) {
       stream_.callbackInfo.doRealtime = true;
       struct sched_param param;
@@ -7963,6 +8078,7 @@ void RtApiAlsa :: startStream()
   // This method calls snd_pcm_prepare if the device isn't already in that state.
 
   verifyStream();
+  RtApi::startStream();
   if ( stream_.state == STREAM_RUNNING ) {
     errorText_ = "RtApiAlsa::startStream(): the stream is already running!";
     error( RtAudioError::WARNING );
@@ -7971,6 +8087,10 @@ void RtApiAlsa :: startStream()
 
   MUTEX_LOCK( &stream_.mutex );
 
+  #if defined( HAVE_GETTIMEOFDAY )
+  gettimeofday( &stream_.lastTickTimestamp, NULL );
+  #endif
+
   int result = 0;
   snd_pcm_state_t state;
   AlsaHandle *apiInfo = (AlsaHandle *) stream_.apiHandle;
@@ -8290,7 +8410,7 @@ static void *alsaCallbackHandler( void *ptr )
   RtApiAlsa *object = (RtApiAlsa *) info->object;
   bool *isRunning = &info->isRunning;
 
-#ifdef SCHED_RR // Undefined with some OSes (eg: NetBSD 1.6.x with GNU Pthread)
+#ifdef SCHED_RR // Undefined with some OSes (e.g. NetBSD 1.6.x with GNU Pthread)
   if ( info->doRealtime ) {
     std::cerr << "RtAudio alsa: " << 
              (sched_getscheduler(0) == SCHED_RR ? "" : "_NOT_ ") << 
@@ -8316,6 +8436,7 @@ static void *alsaCallbackHandler( void *ptr )
 
 #include <pulse/error.h>
 #include <pulse/simple.h>
+#include <pulse/pulseaudio.h>
 #include <cstdio>
 
 static const unsigned int SUPPORTED_SAMPLERATES[] = { 8000, 16000, 22050, 32000,
@@ -8352,8 +8473,33 @@ unsigned int RtApiPulse::getDeviceCount( void )
   return 1;
 }
 
+void RtApiPulse::sinkInfoCallback(pa_context*, const pa_sink_info* info, int, void* arg)
+{
+  RtApiPulse* api = (RtApiPulse *) arg;
+  if (info) {
+    api->channels_ = info->sample_spec.channels;
+  }
+  pa_threaded_mainloop_signal(api->mainloop_, 0);
+}
+
+void RtApiPulse::contextStateCallback(pa_context* c, void* arg)
+{
+  pa_threaded_mainloop* mainloop = (pa_threaded_mainloop*) arg;
+
+  switch (pa_context_get_state(c)) {
+  case PA_CONTEXT_READY:
+  case PA_CONTEXT_TERMINATED:
+  case PA_CONTEXT_FAILED:
+    pa_threaded_mainloop_signal(mainloop, 0);
+    break;
+  default:
+    break;
+  }
+}
+
 RtAudio::DeviceInfo RtApiPulse::getDeviceInfo( unsigned int /*device*/ )
 {
+  /* Set up some defaults in case we crash and burn */
   RtAudio::DeviceInfo info;
   info.probed = true;
   info.name = "PulseAudio";
@@ -8369,6 +8515,72 @@ RtAudio::DeviceInfo RtApiPulse::getDeviceInfo( unsigned int /*device*/ )
   info.preferredSampleRate = 48000;
   info.nativeFormats = RTAUDIO_SINT16 | RTAUDIO_SINT32 | RTAUDIO_FLOAT32;
 
+  /* Get the number of output channels from pulseaudio.  A simple task, you say?
+     "What is your mainloop?" */
+  mainloop_ = pa_threaded_mainloop_new();
+  if (!mainloop_) {
+    return info;
+  }
+
+  pa_threaded_mainloop_start(mainloop_);
+  pa_threaded_mainloop_lock(mainloop_);
+
+  /* "And what is your context?" */
+  pa_context* context = pa_context_new(pa_threaded_mainloop_get_api(mainloop_), "RtAudio");
+  if (!context) {
+    pa_threaded_mainloop_unlock(mainloop_);
+    pa_threaded_mainloop_stop(mainloop_);
+    pa_threaded_mainloop_free(mainloop_);
+    mainloop_ = 0;
+    return info;
+  }
+
+  pa_context_set_state_callback(context, contextStateCallback, mainloop_);
+
+  pa_context_connect(context, 0, (pa_context_flags_t) 0, 0);
+
+  /* "And what is your favourite colour?" */
+  int connected = 0;
+  pa_context_state_t state = pa_context_get_state(context);
+  for (; !connected; state = pa_context_get_state(context)) {
+    switch (state) {
+    case PA_CONTEXT_READY:
+      connected = 1;
+      continue;
+    case PA_CONTEXT_FAILED:
+    case PA_CONTEXT_TERMINATED:
+      /* Blue! No, I mean red! */
+      pa_threaded_mainloop_unlock(mainloop_);
+      pa_context_disconnect(context);
+      pa_context_unref(context);
+      pa_threaded_mainloop_stop(mainloop_);
+      pa_threaded_mainloop_free(mainloop_);
+      mainloop_ = 0;
+      return info;
+    default:
+      pa_threaded_mainloop_wait(mainloop_);
+      break;
+    }
+  }
+
+  pa_operation* op = pa_context_get_sink_info_by_index(context, 0, sinkInfoCallback, this);
+
+  if (op) {
+    pa_operation_unref(op);
+  }
+
+  pa_threaded_mainloop_wait(mainloop_);
+  pa_threaded_mainloop_unlock(mainloop_);
+
+  pa_context_disconnect(context);
+  pa_context_unref(context);
+
+  pa_threaded_mainloop_stop(mainloop_);
+  pa_threaded_mainloop_free(mainloop_);
+  mainloop_ = 0;
+
+  info.outputChannels = channels_;
+  
   return info;
 }
 
@@ -8378,7 +8590,7 @@ static void *pulseaudio_callback( void * user )
   RtApiPulse *context = static_cast<RtApiPulse *>( cbi->object );
   volatile bool *isRunning = &cbi->isRunning;
   
-#ifdef SCHED_RR // Undefined with some OSes (eg: NetBSD 1.6.x with GNU Pthread)
+#ifdef SCHED_RR // Undefined with some OSes (e.g. NetBSD 1.6.x with GNU Pthread)
   if (cbi->doRealtime) {
     std::cerr << "RtAudio pulse: " << 
              (sched_getscheduler(0) == SCHED_RR ? "" : "_NOT_ ") << 
@@ -8521,12 +8733,21 @@ void RtApiPulse::callbackEvent( void )
   MUTEX_UNLOCK( &stream_.mutex );
   RtApi::tickStreamTime();
 
+  if (pah->s_play) {
+    int e = 0;
+    pa_usec_t const lat = pa_simple_get_latency(pah->s_play, &e);
+    if (e == 0) {
+      stream_.latency[0] = lat * stream_.sampleRate / 1000000;
+    }
+  }
+
   if ( doStopStream == 1 )
     stopStream();
 }
 
 void RtApiPulse::startStream( void )
 {
+  RtApi::startStream();
   PulseAudioHandle *pah = static_cast<PulseAudioHandle *>( stream_.apiHandle );
 
   if ( stream_.state == STREAM_CLOSED ) {
@@ -8542,6 +8763,10 @@ void RtApiPulse::startStream( void )
 
   MUTEX_LOCK( &stream_.mutex );
 
+  #if defined( HAVE_GETTIMEOFDAY )
+  gettimeofday( &stream_.lastTickTimestamp, NULL );
+  #endif
+
   stream_.state = STREAM_RUNNING;
 
   pah->runnable = true;
@@ -8565,6 +8790,7 @@ void RtApiPulse::stopStream( void )
   }
 
   stream_.state = STREAM_STOPPED;
+  pah->runnable = false;
   MUTEX_LOCK( &stream_.mutex );
 
   if ( pah && pah->s_play ) {
@@ -8599,6 +8825,7 @@ void RtApiPulse::abortStream( void )
   }
 
   stream_.state = STREAM_STOPPED;
+  pah->runnable = false;
   MUTEX_LOCK( &stream_.mutex );
 
   if ( pah && pah->s_play ) {
@@ -8628,10 +8855,6 @@ bool RtApiPulse::probeDeviceOpen( unsigned int device, StreamMode mode,
 
   if ( device != 0 ) return false;
   if ( mode != INPUT && mode != OUTPUT ) return false;
-  if ( channels != 1 && channels != 2 ) {
-    errorText_ = "RtApiPulse::probeDeviceOpen: unsupported number of channels.";
-    return false;
-  }
   ss.channels = channels;
 
   if ( firstChannel != 0 ) return false;
@@ -8751,7 +8974,38 @@ bool RtApiPulse::probeDeviceOpen( unsigned int device, StreamMode mode,
     }
     break;
   case OUTPUT:
-    pah->s_play = pa_simple_new( NULL, streamName.c_str(), PA_STREAM_PLAYBACK, NULL, "Playback", &ss, NULL, NULL, &error );
+    /* XXX: hard-coded for DCP-o-matic */
+    pa_channel_map map;
+    pa_channel_map_init(&map);
+    /* XXX: need to check 7.1 */
+    map.channels = channels;
+
+    if (channels > 0) {
+      map.map[0] = PA_CHANNEL_POSITION_FRONT_LEFT;
+    }
+    if (channels > 1) {
+      map.map[1] = PA_CHANNEL_POSITION_FRONT_RIGHT;
+    }
+    if (channels > 2) {
+      map.map[2] = PA_CHANNEL_POSITION_FRONT_CENTER;
+    }
+    if (channels > 3) {
+      map.map[3] = PA_CHANNEL_POSITION_LFE;
+    }
+    if (channels > 4) {
+      map.map[4] = PA_CHANNEL_POSITION_REAR_LEFT;
+    }
+    if (channels > 5) {
+      map.map[5] = PA_CHANNEL_POSITION_REAR_RIGHT;
+    }
+    if (channels > 6) {
+      map.map[6] = PA_CHANNEL_POSITION_SIDE_LEFT;
+    }
+    if (channels > 7) {
+      map.map[7] = PA_CHANNEL_POSITION_SIDE_RIGHT;
+    }
+
+    pah->s_play = pa_simple_new( NULL, streamName.c_str(), PA_STREAM_PLAYBACK, NULL, "Playback", &ss, &map, NULL, &error );
     if ( !pah->s_play ) {
       errorText_ = "RtApiPulse::probeDeviceOpen: error connecting output to PulseAudio server.";
       goto error;
@@ -8781,7 +9035,7 @@ bool RtApiPulse::probeDeviceOpen( unsigned int device, StreamMode mode,
     pthread_attr_t attr;
     pthread_attr_init( &attr );
     pthread_attr_setdetachstate( &attr, PTHREAD_CREATE_JOINABLE );
-#ifdef SCHED_RR // Undefined with some OSes (eg: NetBSD 1.6.x with GNU Pthread)
+#ifdef SCHED_RR // Undefined with some OSes (e.g. NetBSD 1.6.x with GNU Pthread)
     if ( options && options->flags & RTAUDIO_SCHEDULE_REALTIME ) {
       stream_.callbackInfo.doRealtime = true;
       struct sched_param param;
@@ -9402,7 +9656,7 @@ bool RtApiOss :: probeDeviceOpen( unsigned int device, StreamMode mode, unsigned
     pthread_attr_t attr;
     pthread_attr_init( &attr );
     pthread_attr_setdetachstate( &attr, PTHREAD_CREATE_JOINABLE );
-#ifdef SCHED_RR // Undefined with some OSes (eg: NetBSD 1.6.x with GNU Pthread)
+#ifdef SCHED_RR // Undefined with some OSes (e.g. NetBSD 1.6.x with GNU Pthread)
     if ( options && options->flags & RTAUDIO_SCHEDULE_REALTIME ) {
       stream_.callbackInfo.doRealtime = true;
       struct sched_param param;
@@ -9518,6 +9772,7 @@ void RtApiOss :: closeStream()
 void RtApiOss :: startStream()
 {
   verifyStream();
+  RtApi::startStream();
   if ( stream_.state == STREAM_RUNNING ) {
     errorText_ = "RtApiOss::startStream(): the stream is already running!";
     error( RtAudioError::WARNING );
@@ -9526,6 +9781,10 @@ void RtApiOss :: startStream()
 
   MUTEX_LOCK( &stream_.mutex );
 
+  #if defined( HAVE_GETTIMEOFDAY )
+  gettimeofday( &stream_.lastTickTimestamp, NULL );
+  #endif
+
   stream_.state = STREAM_RUNNING;
 
   // No need to do anything else here ... OSS automatically starts
@@ -9793,7 +10052,7 @@ static void *ossCallbackHandler( void *ptr )
   RtApiOss *object = (RtApiOss *) info->object;
   bool *isRunning = &info->isRunning;
 
-#ifdef SCHED_RR // Undefined with some OSes (eg: NetBSD 1.6.x with GNU Pthread)
+#ifdef SCHED_RR // Undefined with some OSes (e.g. NetBSD 1.6.x with GNU Pthread)
   if (info->doRealtime) {
     std::cerr << "RtAudio oss: " << 
              (sched_getscheduler(0) == SCHED_RR ? "" : "_NOT_ ") <<