chiark / gitweb /
Merge from disorder.dev.
[disorder] / server / speaker.c
index 6a212dd..892e33c 100644 (file)
  * obvious way.  If the callback finds itself required to play when there is no
  * playing track it returns dead air.
  *
+ * To implement gapless playback, the server is notified that a track has
+ * finished slightly early.  @ref SM_PLAY is therefore allowed to arrive while
+ * the previous track is still playing provided an early @ref SM_FINISHED has
+ * been sent for it.
+ *
  * @b Encodings.  The encodings supported depend entirely on the uaudio backend
  * chosen.  See @ref uaudio.h, etc.
  *
@@ -79,6 +84,8 @@
 #include <sys/un.h>
 #include <sys/stat.h>
 #include <pthread.h>
+#include <sys/resource.h>
+#include <gcrypt.h>
 
 #include "configuration.h"
 #include "syscalls.h"
 /** @brief Maximum number of FDs to poll for */
 #define NFDS 1024
 
+/** @brief Number of bytes before end of track to send SM_FINISHED
+ *
+ * Generally set to 1 second.
+ */
+static size_t early_finish;
+
 /** @brief Track structure
  *
  * Known tracks are kept in a linked list.  Usually there will be at most two
@@ -131,6 +144,14 @@ struct track {
    * out life not playable.
    */
   int playable;
+
+  /** @brief Set when finished
+   *
+   * This is set when we've notified the server that the track is finished.
+   * Once this has happened (typically very late in the track's lifetime) the
+   * track cannot be paused or cancelled.
+   */
+  int finished;
   
   /** @brief Input buffer
    *
@@ -142,9 +163,13 @@ struct track {
 /** @brief Lock protecting data structures
  *
  * This lock protects values shared between the main thread and the callback.
- * It is needed e.g. if changing @ref playing or if modifying buffer pointers.
- * It is not needed to add a new track, to read values only modified in the
- * same thread, etc.
+ *
+ * It is held 'all' the time by the main thread, the exceptions being when
+ * called activate/deactivate callbacks and when calling (potentially) slow
+ * system calls (in particular poll(), where in fact the main thread will spend
+ * most of its time blocked).
+ *
+ * The callback holds it when it's running.
  */
 static pthread_mutex_t lock = PTHREAD_MUTEX_INITIALIZER;
 
@@ -153,11 +178,17 @@ static struct track *tracks;
 
 /** @brief Playing track, or NULL
  *
- * This means the DESIRED playing track.  It does not reflect any other state
- * (e.g. activation of uaudio backend).
+ * This means the track the speaker process intends to play.  It does not
+ * reflect any other state (e.g. activation of uaudio backend).
  */
 static struct track *playing;
 
+/** @brief Pending playing track, or NULL
+ *
+ * This means the track the server wants the speaker to play.
+ */
+static struct track *pending_playing;
+
 /** @brief Array of file descriptors for poll() */
 static struct pollfd fds[NFDS];
 
@@ -272,7 +303,6 @@ static int speaker_fill(struct track *t) {
      t->id, t->eof, t->used));
   if(t->eof)
     return -1;
-  pthread_mutex_lock(&lock);
   if(t->used < sizeof t->buffer) {
     /* there is room left in the buffer */
     where = (t->start + t->used) % sizeof t->buffer;
@@ -307,7 +337,6 @@ static int speaker_fill(struct track *t) {
       rc = 0;
     }
   }
-  pthread_mutex_unlock(&lock);
   return rc;
 }
 
@@ -315,10 +344,15 @@ static int speaker_fill(struct track *t) {
  *
  * We want to play audio if there is a current track; and it is not paused; and
  * it is playable according to the rules for @ref track::playable.
+ *
+ * We don't allow tracks to be paused if we've already told the server we've
+ * finished them; that would cause such tracks to survive much longer than the
+ * few samples they're supposed to, with report() remaining silent for the
+ * duration.
  */
 static int playable(void) {
   return playing
-         && !paused
+         && (!paused || playing->finished)
          && playing->playable;
 }
 
@@ -327,15 +361,17 @@ static void report(void) {
   struct speaker_message sm;
 
   if(playing) {
+    /* Had better not send a report for a track that the server thinks has
+     * finished, that would be confusing. */
+    if(playing->finished)
+      return;
     memset(&sm, 0, sizeof sm);
     sm.type = paused ? SM_PAUSED : SM_PLAYING;
     strcpy(sm.id, playing->id);
-    pthread_mutex_lock(&lock);
     sm.data = playing->played / (uaudio_rate * uaudio_channels);
-    pthread_mutex_unlock(&lock);
     speaker_send(1, &sm);
+    xtime(&last_report);
   }
-  time(&last_report);
 }
 
 /** @brief Add a file descriptor to the set to poll() for
@@ -390,8 +426,10 @@ static size_t speaker_callback(void *buffer,
       if(playing->start == sizeof playing->buffer)
         playing->start = 0;
       /* See if we've reached the end of the track */
-      if(playing->used == 0 && playing->eof)
-        write(sigpipe[1], "", 1);
+      if(playing->used == 0 && playing->eof) {
+        int ignored = write(sigpipe[1], "", 1);
+        (void) ignored;
+      }
       provided_samples = bytes / uaudio_sample_size;
       playing->played += provided_samples;
     }
@@ -401,6 +439,10 @@ static size_t speaker_callback(void *buffer,
   if(!provided_samples) {
     memset(buffer, 0, max_bytes);
     provided_samples = max_samples;
+    if(playing)
+      info("%zu samples silence, playing->used=%zu", provided_samples, playing->used);
+    else
+      info("%zu samples silence, playing=NULL", provided_samples);
   }
   pthread_mutex_unlock(&lock);
   return provided_samples;
@@ -413,13 +455,14 @@ static void mainloop(void) {
   int n, fd, stdin_slot, timeout, listen_slot, sigpipe_slot;
 
   /* Keep going while our parent process is alive */
+  pthread_mutex_lock(&lock);
   while(getppid() != 1) {
     int force_report = 0;
 
     fdno = 0;
-    /* By default we will wait up to a second before thinking about current
-     * state. */
-    timeout = 1000;
+    /* By default we will wait up to half a second before thinking about
+     * current state. */
+    timeout = 500;
     /* Always ready for commands from the main server. */
     stdin_slot = addfd(0, POLLIN);
     /* Also always ready for inbound connections */
@@ -445,9 +488,11 @@ static void mainloop(void) {
         } else
           t->slot = -1;
       }
-    sigpipe_slot = addfd(sigpipe[1], POLLIN);
+    sigpipe_slot = addfd(sigpipe[0], POLLIN);
     /* Wait for something interesting to happen */
+    pthread_mutex_unlock(&lock);
     n = poll(fds, fdno, timeout);
+    pthread_mutex_lock(&lock);
     if(n < 0) {
       if(errno == EINTR) continue;
       fatal(errno, "error calling poll");
@@ -493,18 +538,40 @@ static void mainloop(void) {
        * this won't be the case, so we don't bother looping around to pick them
        * all up. */ 
       n = speaker_recv(0, &sm);
-      /* TODO */
       if(n > 0)
+        /* As a rule we don't send success replies to most commands - we just
+         * force the regular status update to be sent immediately rather than
+         * on schedule. */
        switch(sm.type) {
        case SM_PLAY:
-          if(playing)
-            fatal(0, "got SM_PLAY but already playing something");
+          /* SM_PLAY is only allowed if the server reasonably believes that
+           * nothing is playing */
+          if(playing) {
+            /* If finished isn't set then the server can't believe that this
+             * track has finished */
+            if(!playing->finished)
+              fatal(0, "got SM_PLAY but already playing something");
+            /* If pending_playing is set then the server must believe that that
+             * is playing */
+            if(pending_playing)
+              fatal(0, "got SM_PLAY but have a pending playing track");
+          }
          t = findtrack(sm.id, 1);
           D(("SM_PLAY %s fd %d", t->id, t->fd));
           if(t->fd == -1)
             error(0, "cannot play track because no connection arrived");
-          playing = t;
-          force_report = 1;
+          /* TODO as things stand we often report this error message but then
+           * appear to proceed successfully.  Understanding why requires a look
+           * at play.c: we call prepare() which makes the connection in a child
+           * process, and then sends the SM_PLAY in the parent process.  The
+           * latter may well be faster.  As it happens this is harmless; we'll
+           * just sit around sending silence until the decoder connects and
+           * starts sending some sample data.  But is is annoying and ought to
+           * be fixed. */
+          pending_playing = t;
+          /* If nothing is currently playing then we'll switch to the pending
+           * track below so there's no point distinguishing the situations
+           * here. */
          break;
        case SM_PAUSE:
           D(("SM_PAUSE"));
@@ -520,11 +587,15 @@ static void mainloop(void) {
           D(("SM_CANCEL %s", sm.id));
          t = removetrack(sm.id);
          if(t) {
-            pthread_mutex_lock(&lock);
-           if(t == playing) {
-              /* scratching the playing track */
+           if(t == playing || t == pending_playing) {
+              /* Scratching the track that the server believes is playing,
+               * which might either be the actual playing track or a pending
+               * playing track */
               sm.type = SM_FINISHED;
-             playing = 0;
+              if(t == playing)
+                playing = 0;
+              else
+                pending_playing = 0;
             } else {
               /* Could be scratching the playing track before it's quite got
                * going, or could be just removing a track from the queue.  We
@@ -536,7 +607,6 @@ static void mainloop(void) {
             }
             strcpy(sm.id, t->id);
            destroy(t);
-            pthread_mutex_unlock(&lock);
          } else {
             /* Probably scratching the playing track well before it's got
              * going, but could indicate a bug, so we log this as an error. */
@@ -548,7 +618,7 @@ static void mainloop(void) {
          break;
        case SM_RELOAD:
           D(("SM_RELOAD"));
-         if(config_read(1))
+         if(config_read(1, NULL))
             error(0, "cannot read configuration");
           info("reloaded configuration");
          break;
@@ -566,36 +636,55 @@ static void mainloop(void) {
      * interrupted poll(). */
     if(fds[sigpipe_slot].revents & POLLIN) {
       char buffer[64];
+      int ignored; (void)ignored;
 
-      read(sigpipe[0], buffer, sizeof buffer);
+      ignored = read(sigpipe[0], buffer, sizeof buffer);
     }
-    if(playing && playing->used == 0 && playing->eof) {
-      /* The playing track is done.  Tell the server, and destroy it. */
+    /* Send SM_FINISHED when we're near the end of the track.
+     *
+     * This is how we implement gapless play; we hope that the SM_PLAY from the
+     * server arrives before the remaining bytes of the track play out.
+     */
+    if(playing
+       && playing->eof
+       && !playing->finished
+       && playing->used <= early_finish) {
       memset(&sm, 0, sizeof sm);
       sm.type = SM_FINISHED;
       strcpy(sm.id, playing->id);
       speaker_send(1, &sm);
+      playing->finished = 1;
+    }
+    /* When the track is actually finished, deconfigure it */
+    if(playing && playing->eof && !playing->used) {
       removetrack(playing->id);
-      pthread_mutex_lock(&lock);
       destroy(playing);
       playing = 0;
-      pthread_mutex_unlock(&lock);
-      /* The server will presumalby send as an SM_PLAY by return */
+    }
+    /* Act on the pending SM_PLAY */
+    if(!playing && pending_playing) {
+      playing = pending_playing;
+      pending_playing = 0;
+      force_report = 1;
     }
     /* Impose any state change required by the above */
     if(playable()) {
       if(!activated) {
         activated = 1;
+        pthread_mutex_unlock(&lock);
         backend->activate();
+        pthread_mutex_lock(&lock);
       }
     } else {
       if(activated) {
         activated = 0;
+        pthread_mutex_unlock(&lock);
         backend->deactivate();
+        pthread_mutex_lock(&lock);
       }
     }
     /* If we've not reported our state for a second do so now. */
-    if(force_report || time(0) > last_report)
+    if(force_report || xtime(0) > last_report)
       report();
   }
 }
@@ -629,7 +718,7 @@ int main(int argc, char **argv) {
     log_default = &log_syslog;
   }
   config_uaudio_apis = uaudio_apis;
-  if(config_read(1)) fatal(0, "cannot read configuration");
+  if(config_read(1, NULL)) fatal(0, "cannot read configuration");
   /* ignore SIGPIPE */
   signal(SIGPIPE, SIG_IGN);
   /* set nice value */
@@ -651,6 +740,11 @@ int main(int argc, char **argv) {
     info("set RLIM_NOFILE to %lu", (unsigned long)rl->rlim_cur);
   } else
     info("RLIM_NOFILE is %lu", (unsigned long)rl->rlim_cur);
+  /* gcrypt initialization */
+  if(!gcry_check_version(NULL))
+    disorder_fatal(0, "gcry_check_version failed");
+  gcry_control(GCRYCTL_INIT_SECMEM, 0);
+  gcry_control (GCRYCTL_INITIALIZATION_FINISHED, 0);
   /* create a pipe between the backend callback and the poll() loop */
   xpipe(sigpipe);
   nonblock(sigpipe[0]);
@@ -659,9 +753,12 @@ int main(int argc, char **argv) {
                     config->sample_format.channels,
                     config->sample_format.bits,
                     config->sample_format.bits != 8);
+  early_finish = uaudio_sample_size * uaudio_channels * uaudio_rate;
   /* TODO other parameters! */
   backend = uaudio_find(config->api);
   /* backend-specific initialization */
+  if(backend->configure)
+    backend->configure();
   backend->start(speaker_callback, NULL);
   /* create the socket directory */
   byte_xasprintf(&dir, "%s/speaker", config->home);