More detailed error message.
[rtaudio-cdist.git] / RtAudio.cpp
index 7c87572fd79964819720eab69ccbce4cbaf1dd45..18d1fd0af29b264fe835aa78649b09e9af5324fa 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;
@@ -113,7 +114,7 @@ const char* rtaudio_api_names[][2] = {
   { "ds"          , "DirectSound" },
   { "dummy"       , "Dummy" },
 };
-const unsigned int rtaudio_num_api_names = 
+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
@@ -465,14 +466,14 @@ 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 );
   then = stream_.lastTickTimestamp;
   return stream_.streamTime +
     ((now.tv_sec + 0.000001 * now.tv_usec) -
-     (then.tv_sec + 0.000001 * then.tv_usec));     
+     (then.tv_sec + 0.000001 * then.tv_usec));
 #else
   return stream_.streamTime;
 #endif
@@ -496,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
+}
+
 
 // *************************************************** //
 //
@@ -1535,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 ) {
@@ -1889,7 +1903,7 @@ bool RtApiCore :: callbackEvent( AudioDeviceID deviceId,
           channelsLeft -= streamChannels;
         }
       }
-      
+
       if ( stream_.doConvertBuffer[1] ) { // convert from our internal "device" buffer
         convertBuffer( stream_.userBuffer[1],
                        stream_.deviceBuffer,
@@ -2493,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 ) {
@@ -2782,7 +2801,7 @@ RtApiAsio :: RtApiAsio()
   // CoInitialize beforehand, but it must be for appartment threading
   // (in which case, CoInitilialize will return S_FALSE here).
   coInitialized_ = false;
-  HRESULT hr = CoInitialize( NULL ); 
+  HRESULT hr = CoInitialize( NULL );
   if ( FAILED(hr) ) {
     errorText_ = "RtApiAsio::ASIO requires a single-threaded appartment. Call CoInitializeEx(0,COINIT_APARTMENTTHREADED)";
     error( RtAudioError::WARNING );
@@ -3233,7 +3252,7 @@ bool RtApiAsio :: probeDeviceOpen( unsigned int device, StreamMode mode, unsigne
     errorText_ = errorStream_.str();
     goto error;
   }
-  buffersAllocated = true;  
+  buffersAllocated = true;
   stream_.state = STREAM_STOPPED;
 
   // Set flags for buffer conversion.
@@ -3372,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 ) {
@@ -3707,13 +3731,13 @@ static long asioMessages( long selector, long value, void* /*message*/, double*
 
 static const char* getAsioErrorString( ASIOError result )
 {
-  struct Messages 
+  struct Messages
   {
     ASIOError value;
     const char*message;
   };
 
-  static const Messages m[] = 
+  static const Messages m[] =
     {
       {   ASE_NotPresent,    "Hardware input or output is not present or available." },
       {   ASE_HWMalfunction,  "Hardware is malfunctioning." },
@@ -3829,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
     }
 
@@ -3890,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
     }
 
@@ -3946,7 +3972,7 @@ private:
 
 // In order to satisfy WASAPI's buffer requirements, we need a means of converting sample rate
 // between HW and the user. The WasapiResampler class is used to perform this conversion between
-// HwIn->UserIn and UserOut->HwOut during the stream callback loop.
+// HwIn->UserIn and UserO ut->HwOut during the stream callback loop.
 class WasapiResampler
 {
 public:
@@ -4388,7 +4414,9 @@ RtAudio::DeviceInfo RtApiWasapi::getDeviceInfo( unsigned int device )
 
   hr = audioClient->GetMixFormat( &deviceFormat );
   if ( FAILED( hr ) ) {
-    errorText_ = "RtApiWasapi::getDeviceInfo: Unable to retrieve device mix format.";
+    char error[256];
+    snprintf(error, sizeof(error), "RtApiWasapi::getDeviceInfo: Unable to retrieve device mix format (%d)", hr);
+    errorText_ = error;
     goto Exit;
   }
 
@@ -4544,6 +4572,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.";
@@ -4551,6 +4580,10 @@ void RtApiWasapi::startStream( void )
     return;
   }
 
+  #if defined( HAVE_GETTIMEOFDAY )
+  gettimeofday( &stream_.lastTickTimestamp, NULL );
+  #endif
+
   // update stream state
   stream_.state = STREAM_RUNNING;
 
@@ -4590,26 +4623,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.";
@@ -4640,26 +4653,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.";
@@ -4860,8 +4853,7 @@ bool RtApiWasapi::probeDeviceOpen( unsigned int device, StreamMode mode, unsigne
   stream_.doConvertBuffer[mode] = false;
   if ( stream_.userFormat != stream_.deviceFormat[mode] ||
        stream_.nUserChannels[0] != stream_.nDeviceChannels[0] ||
-       stream_.nUserChannels[1] != stream_.nDeviceChannels[1] ||
-       stream_.userInterleaved )
+       stream_.nUserChannels[1] != stream_.nDeviceChannels[1] )
     stream_.doConvertBuffer[mode] = true;
   else if ( stream_.userInterleaved != stream_.deviceInterleaved[mode] &&
             stream_.nUserChannels[mode] > 1 )
@@ -4982,7 +4974,8 @@ void RtApiWasapi::wasapiThread()
   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 );
   }
@@ -5042,6 +5035,20 @@ void RtApiWasapi::wasapiThread()
       }
 
       ( ( WasapiHandle* ) stream_.apiHandle )->captureClient = captureClient;
+
+      // 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;
+      }
     }
 
     unsigned int inBufferSize = 0;
@@ -5057,20 +5064,6 @@ 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
@@ -5123,6 +5116,20 @@ void RtApiWasapi::wasapiThread()
 
       ( ( 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;
@@ -5138,20 +5145,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
@@ -5175,8 +5168,8 @@ 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.";
@@ -5263,6 +5256,9 @@ 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
@@ -5318,6 +5314,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,
@@ -5465,9 +5467,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();
     }
 
   }
@@ -5500,7 +5499,7 @@ Exit:
 #if defined(__WINDOWS_DS__) // Windows DirectSound API
 
 // Modified by Robin Davies, October 2005
-// - Improvements to DirectX pointer chasing. 
+// - Improvements to DirectX pointer chasing.
 // - Bug fix for non-power-of-two Asio granularity used by Edirol PCR-A30.
 // - Auto-call CoInitialize for DSOUND and ASIO platforms.
 // Various revisions for RtAudio 4.0 by Gary Scavone, April 2007
@@ -5544,7 +5543,7 @@ struct DsHandle {
   void *id[2];
   void *buffer[2];
   bool xrun[2];
-  UINT bufferPointer[2];  
+  UINT bufferPointer[2];
   DWORD dsBufferSize[2];
   DWORD dsPointerLeadTime[2]; // the number of bytes ahead of the safe pointer to lead by.
   HANDLE condition;
@@ -6381,18 +6380,23 @@ 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
   // increasing timer accuracy).  On greater windows (Win2K or later),
   // this is already in effect.
-  timeBeginPeriod( 1 ); 
+  timeBeginPeriod( 1 );
 
   buffersRolling = false;
   duplexPrerollBytes = 0;
@@ -6437,6 +6441,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 );
@@ -6713,7 +6718,7 @@ void RtApiDs :: callbackEvent()
   }
 
   if ( stream_.mode == OUTPUT || stream_.mode == DUPLEX ) {
-    
+
     LPDIRECTSOUNDBUFFER dsBuffer = (LPDIRECTSOUNDBUFFER) handle->buffer[0];
 
     if ( handle->drainCounter > 1 ) { // write zeros to the output stream
@@ -6780,7 +6785,7 @@ void RtApiDs :: callbackEvent()
     }
 
     if ( dsPointerBetween( nextWritePointer, safeWritePointer, currentWritePointer, dsBufferSize )
-         || dsPointerBetween( endWrite, safeWritePointer, currentWritePointer, dsBufferSize ) ) { 
+         || dsPointerBetween( endWrite, safeWritePointer, currentWritePointer, dsBufferSize ) ) {
       // We've strayed into the forbidden zone ... resync the read pointer.
       handle->xrun[0] = true;
       nextWritePointer = safeWritePointer + handle->dsPointerLeadTime[0] - bufferBytes;
@@ -6854,14 +6859,14 @@ void RtApiDs :: callbackEvent()
     if ( safeReadPointer < (DWORD)nextReadPointer ) safeReadPointer += dsBufferSize; // unwrap offset
     DWORD endRead = nextReadPointer + bufferBytes;
 
-    // Handling depends on whether we are INPUT or DUPLEX. 
+    // Handling depends on whether we are INPUT or DUPLEX.
     // If we're in INPUT mode then waiting is a good thing. If we're in DUPLEX mode,
     // then a wait here will drag the write pointers into the forbidden zone.
-    // 
-    // In DUPLEX mode, rather than wait, we will back off the read pointer until 
-    // it's in a safe position. This causes dropouts, but it seems to be the only 
-    // practical way to sync up the read and write pointers reliably, given the 
-    // the very complex relationship between phase and increment of the read and write 
+    //
+    // In DUPLEX mode, rather than wait, we will back off the read pointer until
+    // it's in a safe position. This causes dropouts, but it seems to be the only
+    // practical way to sync up the read and write pointers reliably, given the
+    // the very complex relationship between phase and increment of the read and write
     // pointers.
     //
     // In order to minimize audible dropouts in DUPLEX mode, we will
@@ -6912,7 +6917,7 @@ void RtApiDs :: callbackEvent()
           error( RtAudioError::SYSTEM_ERROR );
           return;
         }
-      
+
         if ( safeReadPointer < (DWORD)nextReadPointer ) safeReadPointer += dsBufferSize; // unwrap offset
       }
     }
@@ -7154,7 +7159,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;
@@ -7163,6 +7168,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 );
@@ -7182,7 +7188,8 @@ unsigned int RtApiAlsa :: getDeviceCount( void )
       nDevices++;
     }
   nextcard:
-    snd_ctl_close( handle );
+    if ( handle )
+        snd_ctl_close( handle );
     snd_card_next( &card );
   }
 
@@ -7203,7 +7210,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;
@@ -7213,6 +7220,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 );
@@ -7235,7 +7243,8 @@ RtAudio::DeviceInfo RtApiAlsa :: getDeviceInfo( unsigned int device )
       nDevices++;
     }
   nextcard:
-    snd_ctl_close( chandle );
+    if ( chandle )
+        snd_ctl_close( chandle );
     snd_card_next( &card );
   }
 
@@ -8071,6 +8080,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 );
@@ -8079,6 +8089,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;
@@ -8135,7 +8149,7 @@ void RtApiAlsa :: stopStream()
   AlsaHandle *apiInfo = (AlsaHandle *) stream_.apiHandle;
   snd_pcm_t **handle = (snd_pcm_t **) apiInfo->handles;
   if ( stream_.mode == OUTPUT || stream_.mode == DUPLEX ) {
-    if ( apiInfo->synchronized ) 
+    if ( apiInfo->synchronized )
       result = snd_pcm_drop( handle[0] );
     else
       result = snd_pcm_drain( handle[0] );
@@ -8400,8 +8414,8 @@ static void *alsaCallbackHandler( void *ptr )
 
 #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_ ") << 
+    std::cerr << "RtAudio alsa: " <<
+             (sched_getscheduler(0) == SCHED_RR ? "" : "_NOT_ ") <<
              "running realtime scheduling" << std::endl;
   }
 #endif
@@ -8424,6 +8438,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,
@@ -8460,8 +8475,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";
@@ -8477,6 +8517,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;
 }
 
@@ -8485,15 +8591,15 @@ static void *pulseaudio_callback( void * user )
   CallbackInfo *cbi = static_cast<CallbackInfo *>( user );
   RtApiPulse *context = static_cast<RtApiPulse *>( cbi->object );
   volatile bool *isRunning = &cbi->isRunning;
-  
+
 #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_ ") << 
+    std::cerr << "RtAudio pulse: " <<
+             (sched_getscheduler(0) == SCHED_RR ? "" : "_NOT_ ") <<
              "running realtime scheduling" << std::endl;
   }
 #endif
-  
+
   while ( *isRunning ) {
     pthread_testcancel();
     context->callbackEvent();
@@ -8611,7 +8717,7 @@ void RtApiPulse::callbackEvent( void )
     else
       bytes = stream_.nUserChannels[INPUT] * stream_.bufferSize *
         formatBytes( stream_.userFormat );
-            
+
     if ( pa_simple_read( pah->s_rec, pulse_in, bytes, &pa_error ) < 0 ) {
       errorStream_ << "RtApiPulse::callbackEvent: audio read error, " <<
         pa_strerror( pa_error ) << ".";
@@ -8629,12 +8735,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 ) {
@@ -8650,6 +8765,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;
@@ -8673,6 +8792,7 @@ void RtApiPulse::stopStream( void )
   }
 
   stream_.state = STREAM_STOPPED;
+  pah->runnable = false;
   MUTEX_LOCK( &stream_.mutex );
 
   if ( pah && pah->s_play ) {
@@ -8707,6 +8827,7 @@ void RtApiPulse::abortStream( void )
   }
 
   stream_.state = STREAM_STOPPED;
+  pah->runnable = false;
   MUTEX_LOCK( &stream_.mutex );
 
   if ( pah && pah->s_play ) {
@@ -8736,10 +8857,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;
@@ -8859,7 +8976,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;
@@ -8878,7 +9026,7 @@ bool RtApiPulse::probeDeviceOpen( unsigned int device, StreamMode mode,
 
   if ( !stream_.callbackInfo.isRunning ) {
     stream_.callbackInfo.object = this;
-    
+
     stream_.state = STREAM_STOPPED;
     // Set the thread attributes for joinable and realtime scheduling
     // priority (optional).  The higher priority will only take affect
@@ -8899,7 +9047,7 @@ bool RtApiPulse::probeDeviceOpen( unsigned int device, StreamMode mode,
       if ( priority < min ) priority = min;
       else if ( priority > max ) priority = max;
       param.sched_priority = priority;
-      
+
       // Set the policy BEFORE the priority. Otherwise it fails.
       pthread_attr_setschedpolicy(&attr, SCHED_RR);
       pthread_attr_setscope (&attr, PTHREAD_SCOPE_SYSTEM);
@@ -8928,7 +9076,7 @@ bool RtApiPulse::probeDeviceOpen( unsigned int device, StreamMode mode,
   }
 
   return SUCCESS;
+
  error:
   if ( pah && stream_.callbackInfo.isRunning ) {
     pthread_cond_destroy( &pah->runnable_cv );
@@ -9520,7 +9668,7 @@ bool RtApiOss :: probeDeviceOpen( unsigned int device, StreamMode mode, unsigned
       if ( priority < min ) priority = min;
       else if ( priority > max ) priority = max;
       param.sched_priority = priority;
-      
+
       // Set the policy BEFORE the priority. Otherwise it fails.
       pthread_attr_setschedpolicy(&attr, SCHED_RR);
       pthread_attr_setscope (&attr, PTHREAD_SCOPE_SYSTEM);
@@ -9626,6 +9774,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 );
@@ -9634,6 +9783,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
@@ -9903,8 +10056,8 @@ static void *ossCallbackHandler( void *ptr )
 
 #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_ ") << 
+    std::cerr << "RtAudio oss: " <<
+             (sched_getscheduler(0) == SCHED_RR ? "" : "_NOT_ ") <<
              "running realtime scheduling" << std::endl;
   }
 #endif
@@ -10625,4 +10778,3 @@ void RtApi :: byteSwapBuffer( char *buffer, unsigned int samples, RtAudioFormat
   // End:
   //
   // vim: et sts=2 sw=2
-