chiark / gitweb /
Shun time(), since on Linux it is not monotonic with gettimeofday().
[disorder] / clients / playrtp.c
index a5542dbd51fbc3ce5d5f09a9264a3cd20be24137..7eed9eb3529f75f58b4ca76f34ee0c1012bee766 100644 (file)
@@ -1,6 +1,6 @@
 /*
  * This file is part of DisOrder.
- * Copyright (C) 2007, 2008 Richard Kettlewell
+ * Copyright (C) 2007-2009 Richard Kettlewell
  *
  * This program is free software: you can redistribute it and/or modify
  * it under the terms of the GNU General Public License as published by
@@ -64,6 +64,7 @@
 #include <unistd.h>
 #include <sys/mman.h>
 #include <fcntl.h>
+#include <math.h>
 
 #include "log.h"
 #include "mem.h"
@@ -94,14 +95,13 @@ static FILE *logfp;
 
 /** @brief Output device */
 
-/** @brief Minimum low watermark
- *
- * We'll stop playing if there's only this many samples in the buffer. */
-unsigned minbuffer = 2 * 44100 / 10;  /* 0.2 seconds */
+/** @brief Buffer low watermark in samples */
+unsigned minbuffer = 4 * (2 * 44100) / 10;  /* 0.4 seconds */
 
-/** @brief Maximum buffer size
+/** @brief Maximum buffer size in samples
  *
- * We'll stop reading from the network if we have this many samples. */
+ * We'll stop reading from the network if we have this many samples.
+ */
 static unsigned maxbuffer;
 
 /** @brief Received packets
@@ -212,6 +212,7 @@ static const struct option options[] = {
   { "pause-mode", required_argument, 0, 'P' },
   { "socket", required_argument, 0, 's' },
   { "config", required_argument, 0, 'C' },
+  { "monitor", no_argument, 0, 'M' },
   { 0, 0, 0, 0 }
 };
 
@@ -323,6 +324,9 @@ static void *queue_thread(void attribute((unused)) *arg) {
     pthread_cond_broadcast(&cond);
     pthread_mutex_unlock(&lock);
   }
+#if HAVE_STUPID_GCC44
+  return NULL;
+#endif
 }
 
 /** @brief Background thread collecting samples
@@ -401,6 +405,14 @@ static void *listen_thread(void attribute((unused)) *arg) {
       fatal(0, "unsupported RTP payload type %d",
             header.mpt & 0x7F);
     }
+    /* See if packet is silent */
+    const uint16_t *s = p->samples_raw;
+    n = p->nsamples;
+    for(; n > 0; --n)
+      if(*s++)
+        break;
+    if(!n)
+      p->flags |= SILENT;
     if(logfp)
       fprintf(logfp, "sequence %u timestamp %"PRIx32" length %"PRIx32" end %"PRIx32"\n",
               seq, timestamp, p->nsamples, timestamp + p->nsamples);
@@ -433,11 +445,14 @@ static void *listen_thread(void attribute((unused)) *arg) {
  */
 void playrtp_fill_buffer(void) {
   /* Discard current buffer contents */
-  while(nsamples)
+  while(nsamples) {
+    //fprintf(stderr, "%8u/%u (%u) DROPPING\n", nsamples, maxbuffer, minbuffer);
     drop_first_packet();
+  }
   info("Buffering...");
   /* Wait until there's at least minbuffer samples available */
   while(nsamples < minbuffer) {
+    //fprintf(stderr, "%8u/%u (%u) FILLING\n", nsamples, maxbuffer, minbuffer);
     pthread_cond_wait(&cond, &lock);
   }
   /* Start from whatever is earliest */
@@ -501,6 +516,7 @@ static size_t playrtp_callback(void *buffer,
                                size_t max_samples,
                                void attribute((unused)) *userdata) {
   size_t samples;
+  int silent = 0;
 
   pthread_mutex_lock(&lock);
   /* Get the next packet, junking any that are now in the past */
@@ -531,8 +547,7 @@ static size_t playrtp_callback(void *buffer,
       *bufptr++ = (int16_t)ntohs(*ptr++);
       --i;
     }
-    /* We don't junk the packet here; a subsequent call to
-     * playrtp_next_packet() will dispose of it (if it's actually done with). */
+    silent = !!(p->flags & SILENT);
   } else {
     /* There is no suitable packet.  We introduce 0s up to the next packet, or
      * to fill the buffer if there's no next packet or that's too many.  The
@@ -543,6 +558,7 @@ static size_t playrtp_callback(void *buffer,
       samples = max_samples;
     //info("infill by %zu", samples);
     memset(buffer, 0, samples * uaudio_sample_size);
+    silent = 1;
   }
   /* Debug dump */
   if(dump_buffer) {
@@ -553,6 +569,47 @@ static size_t playrtp_callback(void *buffer,
   }
   /* Advance timestamp */
   next_timestamp += samples;
+  /* If we're getting behind then try to drop just silent packets
+   *
+   * In theory this shouldn't be necessary.  The server is supposed to send
+   * packets at the right rate and compares the number of samples sent with the
+   * time in order to ensure this.
+   *
+   * However, various things could throw this off:
+   *
+   * - the server's clock could advance at the wrong rate.  This would cause it
+   *   to mis-estimate the right number of samples to have sent and
+   *   inappropriately throttle or speed up.
+   *
+   * - playback could happen at the wrong rate.  If the playback host's sound
+   *   card has a slightly incorrect clock then eventually it will get out
+   *   of step.
+   *
+   * So if we play back slightly slower than the server sends for either of
+   * these reasons then eventually our buffer, and the socket's buffer, will
+   * fill, and the kernel will start dropping packets.  The result is audible
+   * and not very nice.
+   *
+   * Therefore if we're getting behind, we pre-emptively drop silent packets,
+   * since a change in the duration of a silence is less noticeable than a
+   * dropped packet from the middle of continuous music.
+   *
+   * (If things go wrong the other way then eventually we run out of packets to
+   * play and are forced to play silence.  This doesn't seem to happen in
+   * practice but if it does then in the same way we can artificially extend
+   * silent packets to compensate.)
+   *
+   * Dropped packets are always logged; use 'disorder-playrtp --monitor' to
+   * track how close to target buffer occupancy we are on a once-a-minute
+   * basis.
+   */
+  if(nsamples > minbuffer && silent) {
+    info("dropping %zu samples (%"PRIu32" > %"PRIu32")",
+         samples, nsamples, minbuffer);
+    samples = 0;
+  }
+  /* Junk obsolete packets */
+  playrtp_next_packet();
   pthread_mutex_unlock(&lock);
   return samples;
 }
@@ -577,6 +634,7 @@ int main(int argc, char **argv) {
   union any_sockaddr mgroup;
   const char *dumpfile = 0;
   pthread_t ltid;
+  int monitor = 0;
   static const int one = 1;
 
   static const struct addrinfo prefs = {
@@ -592,7 +650,7 @@ int main(int argc, char **argv) {
   mem_init();
   if(!setlocale(LC_CTYPE, "")) fatal(errno, "error calling setlocale");
   backend = uaudio_apis[0];
-  while((n = getopt_long(argc, argv, "hVdD:m:x:L:R:M:aocC:re:P:", options, 0)) >= 0) {
+  while((n = getopt_long(argc, argv, "hVdD:m:x:L:R:aocC:re:P:M", options, 0)) >= 0) {
     switch(n) {
     case 'h': help();
     case 'V': version("disorder-playrtp");
@@ -616,10 +674,11 @@ int main(int argc, char **argv) {
     case 'r': dumpfile = optarg; break;
     case 'e': backend = &uaudio_command; uaudio_set("command", optarg); break;
     case 'P': uaudio_set("pause-mode", optarg); break;
+    case 'M': monitor = 1; break;
     default: fatal(0, "invalid option");
     }
   }
-  if(config_read(0)) fatal(0, "cannot read configuration");
+  if(config_read(0, NULL)) fatal(0, "cannot read configuration");
   if(!maxbuffer)
     maxbuffer = 2 * minbuffer;
   argc -= optind;
@@ -703,9 +762,6 @@ int main(int argc, char **argv) {
       struct sockaddr_in *in = (struct sockaddr_in *)res->ai_addr;
       
       memset(&in->sin_addr, 0, sizeof (struct in_addr));
-      if(bind(rtpfd, res->ai_addr, res->ai_addrlen) < 0)
-        fatal(errno, "error binding socket to 0.0.0.0 port %d",
-              ntohs(in->sin_port));
       break;
     }
     case AF_INET6: {
@@ -736,6 +792,7 @@ int main(int argc, char **argv) {
            rcvbuf, target_rcvbuf);
   } else
     info("default socket receive buffer %d", rcvbuf);
+  //info("minbuffer %u maxbuffer %u", minbuffer, maxbuffer);
   if(logfp)
     info("WARNING: -L option can impact performance");
   if(control_socket) {
@@ -777,6 +834,7 @@ int main(int argc, char **argv) {
   if((err = pthread_create(&ltid, 0, queue_thread, 0)))
     fatal(err, "pthread_create queue_thread");
   pthread_mutex_lock(&lock);
+  time_t lastlog = 0;
   for(;;) {
     /* Wait for the buffer to fill up a bit */
     playrtp_fill_buffer();
@@ -799,8 +857,30 @@ int main(int argc, char **argv) {
     while(nsamples >= minbuffer
          || (nsamples > 0
              && contains(pheap_first(&packets), next_timestamp))) {
+      if(monitor) {
+        time_t now = xtime(0);
+
+        if(now >= lastlog + 60) {
+          int offset = nsamples - minbuffer;
+          double offtime = (double)offset / (uaudio_rate * uaudio_channels);
+          info("%+d samples off (%d.%02ds, %d bytes)",
+               offset,
+               (int)fabs(offtime) * (offtime < 0 ? -1 : 1),
+               (int)(fabs(offtime) * 100) % 100,
+               offset * uaudio_bits / CHAR_BIT);
+          lastlog = now;
+        }
+      }
+      //fprintf(stderr, "%8u/%u (%u) PLAYING\n", nsamples, maxbuffer, minbuffer);
       pthread_cond_wait(&cond, &lock);
     }
+#if 0
+    if(nsamples) {
+      struct packet *p = pheap_first(&packets);
+      fprintf(stderr, "nsamples=%u (%u) next_timestamp=%"PRIx32", first packet is [%"PRIx32",%"PRIx32")\n",
+              nsamples, minbuffer, next_timestamp,p->timestamp,p->timestamp+p->nsamples);
+    }
+#endif
     /* Stop playing for a bit until the buffer re-fills */
     pthread_mutex_unlock(&lock);
     backend->deactivate();