New audio subsystem.
[jog] / au.c
diff --git a/au.c b/au.c
new file mode 100644 (file)
index 0000000..80382b8
--- /dev/null
+++ b/au.c
@@ -0,0 +1,425 @@
+/* -*-c-*-
+ *
+ * $Id: au.c,v 1.1 2002/02/02 19:16:28 mdw Exp $
+ *
+ * High-level audio subsystem
+ *
+ * (c) 2002 Mark Wooding
+ */
+
+/*----- Licensing notice --------------------------------------------------* 
+ *
+ * This file is part of Jog: Programming for a jogging machine.
+ *
+ * Jog 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 2 of the License, or
+ * (at your option) any later version.
+ * 
+ * Jog 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 Jog; if not, write to the Free Software Foundation,
+ * Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
+ */
+
+/*----- Revision history --------------------------------------------------* 
+ *
+ * $Log: au.c,v $
+ * Revision 1.1  2002/02/02 19:16:28  mdw
+ * New audio subsystem.
+ *
+ */
+
+/*----- Header files ------------------------------------------------------*/
+
+#ifdef HAVE_CONFIG_H
+#  include "config.h"
+#endif
+
+#include <assert.h>
+#include <errno.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+
+#include <sys/types.h>
+#include <unistd.h>
+#include <sys/stat.h>
+#include <fcntl.h>
+
+#include <mLib/alloc.h>
+#include <mLib/dstr.h>
+#include <mLib/sub.h>
+#include <mLib/sym.h>
+#include <mLib/trace.h>
+
+#include "au.h"
+#include "ausys.h"
+#include "err.h"
+#include "jog.h"
+
+/*----- Static variables --------------------------------------------------*/
+
+static unsigned au_flags = 0;
+static sym_table au_tab;               /* Sample cache, by name */
+static const char *au_dir = AUDIODIR;  /* Directory containing samples */
+static struct { au_data *next, *prev; } au_spare; /* Lists for sample data */
+static size_t au_sz, au_max;           /* Size of cached samples */
+
+#define AU_SPARE ((au_data*)&au_spare)
+#define f_init 1u
+
+/*----- Utility functions -------------------------------------------------*/
+
+/* --- @filename@ --- *
+ *
+ * Arguments:  @const char *tag@ = sample tag string
+ *
+ * Returns:    A pointer to a filename for the sample.
+ *
+ * Use:                Converts tag strings to filenames.
+ */
+
+static const char *filename(const char *tag)
+{
+  static dstr d = DSTR_INIT;
+
+  DRESET(&d);
+  dstr_putf(&d, "%s/%s.%s", au_dir, tag, ausys_suffix);
+  T( trace(T_AU, "au: map tag `%s' -> `%s'", tag, d.buf); )
+  return (d.buf);
+}
+
+/* --- @prune@ --- *
+ *
+ * Arguments:  ---
+ *
+ * Returns:    ---
+ *
+ * Use:                Prunes the cache of old sample data.
+ */
+
+static void prune(void)
+{
+  T( trace(T_AU, "au: pruning cache (%lu/%lu)",
+          (unsigned long)au_sz, (unsigned long)au_max); )
+  while (au_sz > au_max && AU_SPARE->next != AU_SPARE) {
+    au_data *a = AU_SPARE->next;
+    au_sample *s = a->s;
+    assert(!a->ref);
+    AU_SPARE->next = a->next;
+    a->next->prev = AU_SPARE;
+    s->a = 0;
+    au_sz -= a->sz;
+    ausys_free(a);
+    T( trace(T_AU, "au: ... discarded `%s' (%lu/%lu)", SYM_NAME(s),
+            (unsigned long)au_sz, (unsigned long)au_max); )
+  }
+}
+
+/*----- Main code ---------------------------------------------------------*/
+
+/* --- @au_init@ --- *
+ *
+ * Arguments:  @const char *dir@ = samples directory, or null
+ *             @size_t max@ = maximum cache size
+ *
+ * Returns:    ---
+ *
+ * Use:                Initializes the audio subsystem.
+ */
+
+void au_init(const char *dir, size_t max)
+{
+  if (au_flags & f_init)
+    return;
+
+  /* --- Set up the sound directory --- */
+
+  if (!dir)
+    dir = getenv("JOG_AUDIR");
+  if (dir)
+    au_dir = dir;
+
+  /* --- Initialize the sample cache --- */
+
+  sym_create(&au_tab);
+  AU_SPARE->next = AU_SPARE->prev = AU_SPARE;
+  au_max = max;
+
+  /* --- Initialize the system-specific subsystem --- */
+
+  ausys_init();
+  T( trace(T_AU, "au: initialized ok (dir = `%s')", au_dir); )
+
+  /* --- Done --- */
+
+  au_flags |= f_init;
+}
+
+/* --- @au_shutdown@ --- *
+ *
+ * Arguments:  ---
+ *
+ * Returns:    ---
+ *
+ * Use:                Shuts down the audio subsystem.
+ */
+
+void au_shutdown(void)
+{
+  if (!(au_flags & f_init))
+    return;
+  ausys_shutdown();
+  T( trace(T_AU, "au: shutdown ok"); )
+}
+
+/* --- @au_find@ --- *
+ *
+ * Arguments:  @const char *tag@ = sample tag string
+ *
+ * Returns:    A pointer to the sample corresponding to the tag, or null if
+ *             the sample wasn't found.
+ *
+ * Use:                Looks up a sample by its name.
+ */
+
+au_sample *au_find(const char *tag)
+{
+  unsigned f;
+  au_sample *s = sym_find(&au_tab, tag, -1, sizeof(*s), &f);
+
+  if (!f) {
+    struct stat st;
+
+    s->f = 0;
+    s->a = 0;
+    if (stat(filename(tag), &st))
+      s->f |= AUF_NOMATCH;
+  }
+  if (s->f & AUF_NOMATCH) {
+    T( trace(T_AU, "au: sample `%s' not found%s", tag, f ? " (hit)": ""); )
+    return (0);
+  }
+  T( trace(T_AU, "au: sample `%s' found%s", tag, f ? " (hit)" : ""); )
+  return (s);
+}
+
+/* --- @au_fetch@ --- *
+ *
+ * Arguments:  @au_sample *s@ = sample pointer
+ *
+ * Returns:    A pointer to the audio data for the sample.
+ *
+ * Use:                Fetches a sample, and decodes it, if necessary.  The decoded
+ *             sample data is left with an outstanding reference to it, so
+ *             it won't be freed.  This is used internally by @au_queue@,
+ *             before passing the fetched sample data to the system-specific
+ *             layer for playback.  It can also be used to lock sample data
+ *             in memory (e.g., for the `abort' error message), or (by
+ *             freeing it immediately afterwards) to prefetch a sample which
+ *             will be used soon, to reduce the latency before the sample is
+ *             heard.
+ */
+
+au_data *au_fetch(au_sample *s)
+{
+  dstr d = DSTR_INIT;
+  struct stat st;
+  const char *fn;
+  size_t n;
+  ssize_t r;
+  au_data *a;
+  int fd;
+
+  /* --- If we already have the sample data --- *
+   *
+   * If it's currently languishing in the spare bin, rescue it.  If this
+   * doesn't work, we can release the audio lock, because nothing else messes
+   * with the spare list.
+   */
+
+  ausys_lock();
+  a = s->a;
+  if (a) {
+    if (!a->ref) {
+      assert(a->next);
+      a->prev->next = a->next;
+      a->next->prev = a->prev;
+      a->next = a->prev = 0;
+    }
+    a->ref++;
+    T( trace(T_AU, "au: reusing sample `%s'", SYM_NAME(s)); )
+    ausys_unlock();
+    return (a);
+  }
+  ausys_unlock();
+
+  /* --- Read the file --- *
+   *
+   * Buffered I/O will just involve more copying.
+   */
+
+  T( trace(T_AU, "au: fetching sample `%s'", SYM_NAME(s)); )
+  fn = filename(SYM_NAME(s));
+  if ((fd = open(fn, O_RDONLY)) < 0) {
+    err_report(ERR_AUDIO, ERRAU_OPEN, errno,
+              "couldn't open sample `%s': %s", fn, strerror(errno));
+    goto fail_0;
+  }
+  if (fstat(fd, &st)) {
+    err_report(ERR_AUDIO, ERRAU_OPEN, errno,
+              "couldn't fstat `%s': %s", fn, strerror(errno));
+    goto fail_1;
+  }
+  n = st.st_size + 1;
+  if (n < 4096)
+    n = 4096;
+  for (;;) {
+    dstr_ensure(&d, n);
+  again:
+    if ((r = read(fd, d.buf + d.len, n)) < 0) {
+      if (errno == EWOULDBLOCK || errno == EAGAIN || errno == EINTR)
+       goto again;
+      err_report(ERR_AUDIO, ERRAU_OPEN, errno,
+                "couldn't read `%s': %s", fn, strerror(errno));
+      goto fail_1;
+    }
+    if (!r)
+      break;
+    d.len += r;
+    n -= r;
+    if (!n)
+      n = d.len;
+  }
+  close(fd);
+
+  /* --- Convert it into internal form --- */
+
+  if ((a = ausys_decode(s, d.buf, d.len)) == 0)
+    goto fail_0;
+  au_sz += a->sz;
+  a->ref = 1;
+  a->next = a->prev = 0;
+  a->s = s;
+  s->a = a;
+
+  /* --- Done --- */
+
+  ausys_lock();
+  prune();
+  ausys_unlock();
+  goto done;
+done:
+  dstr_destroy(&d);
+  return (a);
+
+  /* --- Tidy up after botched file I/O --- */
+
+fail_1:
+  close(fd);
+fail_0:
+  dstr_destroy(&d);
+  return (0);
+}
+
+/* --- @au_queue@ --- *
+ *
+ * Arguments:  @au_sample *s@ = sample pointer
+ *
+ * Returns:    Zero on success, nonzero on failure.
+ *
+ * Use:                Queues a sample to be played.
+ */
+
+int au_queue(au_sample *s)
+{
+  au_data *a;
+
+  assert(s);
+  if ((a = au_fetch(s)) == 0)
+    return (-1);
+  T( trace(T_AU, "au: queuing sample `%s'", SYM_NAME(s)); )
+  ausys_queue(s->a);
+  return (0);
+}
+
+/* --- @au_free@, @au_free_unlocked@ --- *
+ *
+ * Arguments:  @au_data *a@ = pointer to audio data block
+ *
+ * Returns:    ---
+ *
+ * Use:                Frees a sample data block when it's no longer required.
+ */
+
+void au_free(au_data *a)
+{
+  ausys_lock();
+  au_free_unlocked(a);
+  ausys_unlock();
+}
+
+void au_free_unlocked(au_data *a)
+{
+  /* --- If the sample is unreferenced, throw it in the spare bin --- *
+   *
+   * This can be called from a background audio processing thread, so we need
+   * to acquire the lock.
+   */
+
+  assert(a->ref);
+  assert(!a->next);
+  assert(!a->prev);
+  a->ref--;
+  if (a->ref) {
+    T( trace(T_AU, "au: drop ref to `%s' (other refs remain)",
+            SYM_NAME(a->s)); )
+    ;
+  } else {
+    a->next = AU_SPARE;
+    a->prev = AU_SPARE->prev;
+    AU_SPARE->prev->next = a;
+    AU_SPARE->prev = a;
+    T( trace(T_AU, "au: drop last ref to `%s'", SYM_NAME(a->s)); )
+    prune();
+  }
+}
+
+/* --- @au_play@, @au_tryplay@ --- *
+ *
+ * Arguments:  @const char *tag@ = sample tag string
+ *
+ * Returns:    Zero on success, nonzero on failure.
+ *
+ * Use:                Convenience functions for queueing samples by tag.
+ *             If @au_tryplay@ cannot find the requested sample, it returns
+ *             a zero value; if @au_play@ cannot find the sample, it reports
+ *             an error.
+ */
+
+int au_tryplay(const char *tag)
+{
+  au_sample *s;
+
+  if (!(au_flags & f_init))
+    return (0);
+  if ((s = au_find(tag)) == 0 || au_queue(s))
+    return (-1);
+  return (0);
+}
+
+int au_play(const char *tag)
+{
+  int rc;
+
+  if ((rc = au_tryplay(tag)) != 0)
+    err_report(ERR_AUDIO, ERRAU_NOTFOUND, 0, "sample `%s' not found", tag);
+  return (rc);
+}
+
+/*----- That's all, folks -------------------------------------------------*/