chiark / gitweb /
Uniform audio command back end now rate limited.
authorRichard Kettlewell <rjk@greenend.org.uk>
Sun, 1 Mar 2009 19:01:36 +0000 (19:01 +0000)
committerRichard Kettlewell <rjk@greenend.org.uk>
Sun, 1 Mar 2009 19:01:36 +0000 (19:01 +0000)
The algorithm is the same as for the RTP backend, and is therefore
split out into a separate file, uaudio-schedule.c.

lib/Makefile.am
lib/uaudio-command.c
lib/uaudio-rtp.c
lib/uaudio-schedule.c [new file with mode: 0644]
lib/uaudio.h

index 6a6596f..7ef6039 100644 (file)
@@ -85,7 +85,7 @@ libdisorder_a_SOURCES=charset.c charset.h             \
        uaudio.c uaudio-thread.c uaudio.h               \
        uaudio-oss.c uaudio-alsa.c                      \
        uaudio-coreaudio.c                              \
-       uaudio-rtp.c uaudio-command.c                   \
+       uaudio-rtp.c uaudio-command.c uaudio-schedule.c \
        url.h url.c                                     \
        user.h user.c                                   \
        unicode.h unicode.c                             \
index 6b7ef66..b9a1abb 100644 (file)
  * along with this program.  If not, see <http://www.gnu.org/licenses/>.
  */
 /** @file lib/uaudio-command.c
- * @brief Support for commmand backend */
+ * @brief Support for commmand backend
+ *
+ * We use the code in @ref lib/uaudio-schedule.c to ensure that we write at
+ * approximately the 'real' rate.  For disorder-playrtp this isn't very useful
+ * (thought it might reduce the size of various buffers downstream of us) but
+ * when run from the speaker it means that pausing stands a chance of working.
+ */
 #include "common.h"
 
 #include <errno.h>
@@ -85,6 +91,7 @@ static void command_open(void) {
 
 /** @brief Send audio data to subprocess */
 static size_t command_play(void *buffer, size_t nsamples) {
+  uaudio_schedule_synchronize();
   const size_t bytes = nsamples * uaudio_sample_size;
   int written = write(command_fd, buffer, bytes);
   if(written < 0) {
@@ -100,12 +107,15 @@ static size_t command_play(void *buffer, size_t nsamples) {
       fatal(errno, "error writing to audio command subprocess");
     }
   }
-  return written / uaudio_sample_size;
+  const size_t written_samples = written / uaudio_sample_size;
+  uaudio_schedule_update(written_samples);
+  return written_samples;
 }
 
 static void command_start(uaudio_callback *callback,
                       void *userdata) {
   command_open();
+  uaudio_schedule_init();
   uaudio_thread_start(callback,
                       userdata,
                       command_play,
@@ -119,6 +129,7 @@ static void command_stop(void) {
 }
 
 static void command_activate(void) {
+  uaudio_schedule_reactivated = 1;
   uaudio_thread_activate();
 }
 
index 9980ba0..ea1d1ca 100644 (file)
@@ -59,28 +59,6 @@ static uint32_t rtp_id;
 /** @brief RTP sequence number */
 static uint16_t rtp_sequence;
 
-/** @brief RTP timestamp
- *
- * This is the timestamp that will be used on the next outbound packet.
- *
- * The timestamp in the packet header is only 32 bits wide.  With 44100Hz
- * stereo, that only gives about half a day before wrapping, which is not
- * particularly convenient for certain debugging purposes.  Therefore the
- * timestamp is maintained as a 64-bit integer, giving around six million years
- * before wrapping, and truncated to 32 bits when transmitting.
- */
-static uint64_t rtp_timestamp;
-
-/** @brief Actual time corresponding to @ref rtp_timestamp
- *
- * This is the time, on this machine, at which the sample at @ref rtp_timestamp
- * ought to be sent, interpreted as the time the last packet was sent plus the
- * time length of the packet. */
-static struct timeval rtp_timeval;
-
-/** @brief Set when we (re-)activate, to provoke timestamp resync */
-static int rtp_reactivated;
-
 /** @brief Network error count
  *
  * If too many errors occur in too short a time, we give up.
@@ -100,21 +78,20 @@ static const char *const rtp_options[] = {
   "rtp-source-port",
   "multicast-ttl",
   "multicast-loop",
-  "rtp-delay-threshold",
+  "delay-threshold",
   NULL
 };
 
 static size_t rtp_play(void *buffer, size_t nsamples) {
   struct rtp_header header;
   struct iovec vec[2];
-  struct timeval now;
   
   /* We do as much work as possible before checking what time it is */
   /* Fill out header */
   header.vpxcc = 2 << 6;              /* V=2, P=0, X=0, CC=0 */
   header.seq = htons(rtp_sequence++);
   header.ssrc = rtp_id;
-  header.mpt = (rtp_reactivated ? 0x80 : 0x00) | rtp_payload;
+  header.mpt = (uaudio_schedule_reactivated ? 0x80 : 0x00) | rtp_payload;
 #if !WORDS_BIGENDIAN
   /* Convert samples to network byte order */
   uint16_t *u = buffer, *const limit = u + nsamples;
@@ -127,61 +104,8 @@ static size_t rtp_play(void *buffer, size_t nsamples) {
   vec[0].iov_len = sizeof header;
   vec[1].iov_base = buffer;
   vec[1].iov_len = nsamples * uaudio_sample_size;
-retry:
-  xgettimeofday(&now, NULL);
-  if(rtp_reactivated) {
-    /* We've been deactivated for some unknown interval.  We need to advance
-     * rtp_timestamp to account for the dead air. */
-    /* On the first run through we'll set the start time. */
-    if(!rtp_timeval.tv_sec)
-      rtp_timeval = now;
-    /* See how much time we missed.
-     *
-     * This will be 0 on the first run through, in which case we'll not modify
-     * anything.
-     *
-     * It'll be negative in the (rare) situation where the deactivation
-     * interval is shorter than the last packet we sent.  In this case we wait
-     * for that much time and then return having sent no samples, which will
-     * cause uaudio_play_thread_fn() to retry.
-     *
-     * In the normal case it will be positive.
-     */
-    const int64_t delay = tvsub_us(now, rtp_timeval); /* microseconds */
-    if(delay < 0) {
-      usleep(-delay);
-      goto retry;
-    }
-    /* Advance the RTP timestamp to the present.  With 44.1KHz stereo this will
-     * overflow the intermediate value with a delay of a bit over 6 years.
-     * This seems acceptable. */
-    uint64_t update = (delay * uaudio_rate * uaudio_channels) / 1000000;
-    /* Don't throw off channel synchronization */
-    update -= update % uaudio_channels;
-    /* We log nontrivial changes */
-    if(update)
-      info("advancing rtp_time by %"PRIu64" samples", update);
-      rtp_timestamp += update;
-    rtp_timeval = now;
-    rtp_reactivated = 0;
-  } else {
-    /* Chances are we've been called right on the heels of the previous packet.
-     * If we just sent packets as fast as we got audio data we'd get way ahead
-     * of the player and some buffer somewhere would fill (or at least become
-     * unreasonably large).
-     *
-     * First find out how far ahead of the target time we are.
-     */
-    const int64_t ahead = tvsub_us(now, rtp_timeval); /* microseconds */
-    /* Only delay at all if we are nontrivially ahead. */
-    if(ahead > rtp_delay_threshold) {
-      /* Don't delay by the full amount */
-      usleep(ahead - rtp_delay_threshold / 2);
-      /* Refetch time (so we don't get out of step with reality) */
-      xgettimeofday(&now, NULL);
-    }
-  }
-  header.timestamp = htonl((uint32_t)rtp_timestamp);
+  uaudio_schedule_synchronize();
+  header.timestamp = htonl((uint32_t)uaudio_schedule_timestamp);
   int written_bytes;
   do {
     written_bytes = writev(rtp_fd, vec, 2);
@@ -195,17 +119,8 @@ retry:
   } else
     rtp_errors /= 2;                    /* gradual decay */
   written_bytes -= sizeof (struct rtp_header);
-  size_t written_samples = written_bytes / uaudio_sample_size;
-  /* rtp_timestamp and rtp_timestamp are supposed to refer to the first sample
-   * of the next packet */
-  rtp_timestamp += written_samples;
-  const unsigned usec = (rtp_timeval.tv_usec
-                         + 1000000 * written_samples / (uaudio_rate
-                                                            * uaudio_channels));
-  /* ...will only overflow 32 bits if one packet is more than about half an
-   * hour long, which is not plausible. */
-  rtp_timeval.tv_sec += usec / 1000000;
-  rtp_timeval.tv_usec = usec % 1000000;
+  const size_t written_samples = written_bytes / uaudio_sample_size;
+  uaudio_schedule_update(written_samples);
   return written_samples;
 }
 
@@ -340,14 +255,6 @@ static void rtp_open(void) {
     fatal(errno, "error binding broadcast socket to %s", ssockname);
   if(connect(rtp_fd, res->ai_addr, res->ai_addrlen) < 0)
     fatal(errno, "error connecting broadcast socket to %s", sockname);
-  /* Various fields are required to have random initial values by RFC3550.  The
-   * packet contents are highly public so there's no point asking for very
-   * strong randomness. */
-  gcry_create_nonce(&rtp_id, sizeof rtp_id);
-  gcry_create_nonce(&rtp_sequence, sizeof rtp_sequence);
-  gcry_create_nonce(&rtp_timestamp, sizeof rtp_timestamp);
-  /* rtp_play() will spot this and choose an initial value */
-  rtp_timeval.tv_sec = 0;
 }
 
 static void rtp_start(uaudio_callback *callback,
@@ -364,7 +271,13 @@ static void rtp_start(uaudio_callback *callback,
   else
     fatal(0, "asked for %d/%d/%d 16/44100/1 and 16/44100/2",
           uaudio_bits, uaudio_rate, uaudio_channels); 
+  /* Various fields are required to have random initial values by RFC3550.  The
+   * packet contents are highly public so there's no point asking for very
+   * strong randomness. */
+  gcry_create_nonce(&rtp_id, sizeof rtp_id);
+  gcry_create_nonce(&rtp_sequence, sizeof rtp_sequence);
   rtp_open();
+  uaudio_schedule_init();
   uaudio_thread_start(callback,
                       userdata,
                       rtp_play,
@@ -380,7 +293,7 @@ static void rtp_stop(void) {
 }
 
 static void rtp_activate(void) {
-  rtp_reactivated = 1;
+  uaudio_schedule_reactivated = 1;
   uaudio_thread_activate();
 }
 
diff --git a/lib/uaudio-schedule.c b/lib/uaudio-schedule.c
new file mode 100644 (file)
index 0000000..95fa0c0
--- /dev/null
@@ -0,0 +1,180 @@
+/*
+ * This file is part of DisOrder.
+ * Copyright (C) 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
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ * 
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+/** @file lib/uaudio-schedule.c
+ * @brief Scheduler for RTP and command backends
+ *
+ * These functions ensure that audio is only written at approximately the rate
+ * it should play at, allowing pause to function properly.
+ *
+ * OSS and ALSA we expect to be essentially synchronous (though we could use
+ * this code if they don't play nicely).  Core Audio sorts out its own timing
+ * issues itself.
+ *
+ * The sequence numbers are intended for RTP's use but it's more convenient to
+ * maintain them here.
+ */
+
+#include "common.h"
+
+#include <unistd.h>
+#include <gcrypt.h>
+
+#include "uaudio.h"
+#include "mem.h"
+#include "log.h"
+#include "syscalls.h"
+#include "timeval.h"
+
+/** @brief Sample timestamp
+ *
+ * This is the timestamp that will be used on the next outbound packet.
+ *
+ * The timestamp in an RTP packet header is only 32 bits wide.  With 44100Hz
+ * stereo, that only gives about half a day before wrapping, which is not
+ * particularly convenient for certain debugging purposes.  Therefore the
+ * timestamp is maintained as a 64-bit integer, giving around six million years
+ * before wrapping, and truncated to 32 bits when transmitting.
+ */
+uint64_t uaudio_schedule_timestamp;
+
+/** @brief Actual time corresponding to @ref uaudio_schedule_timestamp
+ *
+ * This is the time, on this machine, at which the sample at @ref
+ * uaudio_schedule_timestamp ought to be sent, interpreted as the time the last
+ * packet was sent plus the time length of the packet. */
+static struct timeval uaudio_schedule_timeval;
+
+/** @brief Set when we (re-)activate, to provoke timestamp resync */
+int uaudio_schedule_reactivated;
+
+/** @brief Delay threshold in microseconds
+ *
+ * uaudio_schedule_play() never attempts to introduce a delay shorter than this.
+ */
+static int64_t uaudio_schedule_delay_threshold;
+
+/** @brief Time for current packet */
+static struct timeval uaudio_schedule_now;
+
+/** @brief Synchronize playback operations against real time
+ *
+ * This function sleeps as necessary to rate-limit playback operations to match
+ * the actual playback rate.  It also maintains @ref uaudio_schedule_timestamp
+ * as an arbitrarily-based sample counter, for use by RTP.
+ *
+ * You should call this in your API's @ref uaudio_playcallback before writing
+ * and call uaudio_schedule_update() afterwards.
+ */
+void uaudio_schedule_synchronize(void) {
+retry:
+  xgettimeofday(&uaudio_schedule_now, NULL);
+  if(uaudio_schedule_reactivated) {
+    /* We've been deactivated for some unknown interval.  We need to advance
+     * rtp_timestamp to account for the dead air. */
+    /* On the first run through we'll set the start time. */
+    if(!uaudio_schedule_timeval.tv_sec)
+      uaudio_schedule_timeval = uaudio_schedule_now;
+    /* See how much time we missed.
+     *
+     * This will be 0 on the first run through, in which case we'll not modify
+     * anything.
+     *
+     * It'll be negative in the (rare) situation where the deactivation
+     * interval is shorter than the last packet we sent.  In this case we wait
+     * for that much time and then return having sent no samples, which will
+     * cause uaudio_play_thread_fn() to retry.
+     *
+     * In the normal case it will be positive.
+     */
+    const int64_t delay = tvsub_us(uaudio_schedule_now,
+                                   uaudio_schedule_timeval); /* microseconds */
+    if(delay < 0) {
+      usleep(-delay);
+      goto retry;
+    }
+    /* Advance the RTP timestamp to the present.  With 44.1KHz stereo this will
+     * overflow the intermediate value with a delay of a bit over 6 years.
+     * This seems acceptable. */
+    uint64_t update = (delay * uaudio_rate * uaudio_channels) / 1000000;
+    /* Don't throw off channel synchronization */
+    update -= update % uaudio_channels;
+    /* We log nontrivial changes */
+    if(update)
+      info("advancing uaudio_schedule_timeval by %"PRIu64" samples", update);
+    uaudio_schedule_timestamp += update;
+    uaudio_schedule_timeval = uaudio_schedule_now;
+    uaudio_schedule_reactivated = 0;
+  } else {
+    /* Chances are we've been called right on the heels of the previous packet.
+     * If we just sent packets as fast as we got audio data we'd get way ahead
+     * of the player and some buffer somewhere would fill (or at least become
+     * unreasonably large).
+     *
+     * First find out how far ahead of the target time we are.
+     */
+    const int64_t ahead = tvsub_us(uaudio_schedule_timeval,
+                                   uaudio_schedule_now); /* microseconds */
+    /* Only delay at all if we are nontrivially ahead. */
+    if(ahead > uaudio_schedule_delay_threshold) {
+      /* Don't delay by the full amount */
+      usleep(ahead - uaudio_schedule_delay_threshold / 2);
+      /* Refetch time (so we don't get out of step with reality) */
+      xgettimeofday(&uaudio_schedule_now, NULL);
+    }
+  }
+}
+
+/** @brief Update schedule after writing
+ *
+ * Called by your API's @ref uaudio_playcallback after sending audio data (to a
+ * subprocess or network or whatever).  A separate function so that the caller
+ * doesn't have to know how many samples they're going to write until they've
+ * done so.
+ */
+void uaudio_schedule_update(size_t written_samples) {
+  /* uaudio_schedule_timestamp and uaudio_schedule_timestamp are supposed to
+   * refer to the first sample of the next packet */
+  uaudio_schedule_timestamp += written_samples;
+  const unsigned usec = (uaudio_schedule_timeval.tv_usec
+                         + 1000000 * written_samples / (uaudio_rate
+                                                            * uaudio_channels));
+  /* ...will only overflow 32 bits if one packet is more than about half an
+   * hour long, which is not plausible. */
+  uaudio_schedule_timeval.tv_sec += usec / 1000000;
+  uaudio_schedule_timeval.tv_usec = usec % 1000000;
+}
+
+/** @brief Initialize audio scheduling
+ *
+ * Should be called from your API's @c start callback.
+ */
+void uaudio_schedule_init(void) {
+  gcry_create_nonce(&uaudio_schedule_timestamp,
+                    sizeof uaudio_schedule_timestamp);
+  /* uaudio_schedule_play() will spot this and choose an initial value */
+  uaudio_schedule_timeval.tv_sec = 0;
+}
+
+/*
+Local Variables:
+c-basic-offset:2
+comment-column:40
+fill-column:79
+indent-tabs-mode:nil
+End:
+*/
index 3e62aad..be396df 100644 (file)
@@ -103,6 +103,12 @@ void uaudio_thread_start(uaudio_callback *callback,
 void uaudio_thread_stop(void);
 void uaudio_thread_activate(void);
 void uaudio_thread_deactivate(void);
+void uaudio_schedule_synchronize(void);
+void uaudio_schedule_update(size_t written_samples);
+void uaudio_schedule_init(void);
+
+extern uint64_t uaudio_schedule_timestamp;
+extern int uaudio_schedule_reactivated;
 
 #if HAVE_COREAUDIO_AUDIOHARDWARE_H
 extern const struct uaudio uaudio_coreaudio;