]> git.tuebingen.mpg.de Git - paraslash.git/commitdiff
Merge branch 'refs/heads/t/poll'
authorAndre Noll <maan@tuebingen.mpg.de>
Sun, 11 Sep 2022 14:34:12 +0000 (16:34 +0200)
committerAndre Noll <maan@tuebingen.mpg.de>
Sun, 11 Sep 2022 14:35:30 +0000 (16:35 +0200)
This series modifies all calls to select(2) to use poll(2) instead in
order to avoid the known shortcomings of the select API, in particular
its limit of at most 1024 file descriptors and the fact that fds above
1023 cannot be monitored with select(2) even if fewer than 1024 fds
are open.

The first patches of the series prepare this switch, converting the
easy cases, hiding select specific data structures such as fd sets,
and adjusting function names and documentation. The crucial commit
is the last one. See its rather verbose log message for details.

* refs/heads/t/poll:
  Switch from select(2) to poll(2).
  Rename ->{pre,post}_select methods to ->{pre,post}_monitor.
  Misc documentation cleanups related to select().
  stdin/stdout: Streamline documentation of {pre,post}_select().
  Consolidate receiver/filter/writer {pre,post}_select() docs.
  Hide implementation of para_fd_set().
  send: Avoid select-specific arguments in {pre,post}_select().
  sched: Introduce sched_{read,write}_ok().
  audiod: Rename handle_connect().
  net: Drop fd_set parameter from para_accept().
  fd: Drop fd_set parameter from read_nonblock() and friends.
  interactive: Avoid select(2) in input_available().
  fd.c: Prefer poll(2) over select(2) for write_ok().
  sched: Use integer value for select timeout.

.gitignore
NEWS.md
aac_afh.c
autogen.sh
configure.ac
error.h
mp4.c [new file with mode: 0644]
mp4.h [new file with mode: 0644]

index bd5e04801c4bec2714dd515810296f1bc89cbab0..8f8d0af736cee205b638502171eea526d613cc22 100644 (file)
@@ -24,3 +24,4 @@ confdefs.h
 conftest
 conftest.c
 git-version.h
+*-local*
diff --git a/NEWS.md b/NEWS.md
index e9713d9826ff74e25aa01ceaad9a9db13f3113a5..0f2eec0d5e232f5ff11a74bd11860e77ea0e7e56 100644 (file)
--- a/NEWS.md
+++ b/NEWS.md
@@ -5,6 +5,17 @@ NEWS
 0.7.1 (to be announced) "digital spindrift"
 -------------------------------------------
 
+- The autogen.sh script now only creates the autoconf specific files
+  but no longer runs configure, make and the test suite.
+
+- A stripped down copy of the discontinued libmp4ff library has become
+  part of the paraslash code base. As a result it is no longer necessary
+  to install faad from source to get support for aac/m4a files. The
+  faad decoder package must still be installed.
+
+- All calls to select(2) have been replaced by calls to poll(2)
+  to avoid known shortcomings of the select API.
+
 [tarball](./releases/paraslash-git.tar.xz)
 
 ----------------------------------
index 5c1225b62b2e179026dadf7638d4f44f041345cb..79fa30ddc2f794f5c586c28b2823c7fce26dd9b8 100644 (file)
--- a/aac_afh.c
+++ b/aac_afh.c
 #include <neaacdec.h>
 
 #include "para.h"
-
-/* To get the mp4ff_tag_t and mp4ff_metadata_t typedefs. */
-#define USE_TAGGING
-#include <mp4ff.h>
-
+#include "mp4.h"
 #include "error.h"
 #include "portable_io.h"
 #include "afh.h"
@@ -26,13 +22,11 @@ struct aac_afh_context {
        const void *map;
        size_t mapsize;
        size_t fpos;
-       int32_t track;
-       mp4ff_t *mp4ff;
-       mp4AudioSpecificConfig masc;
-       mp4ff_callback_t cb;
+       struct mp4 *mp4;
+       struct mp4_callback cb;
 };
 
-static uint32_t aac_afh_read_cb(void *user_data, void *dest, uint32_t want)
+static ssize_t aac_afh_read_cb(void *user_data, void *dest, size_t want)
 {
        struct aac_afh_context *c = user_data;
        size_t have, rv;
@@ -40,39 +34,26 @@ static uint32_t aac_afh_read_cb(void *user_data, void *dest, uint32_t want)
        if (want == 0 || c->fpos >= c->mapsize)
                return 0;
        have = c->mapsize - c->fpos;
-       rv = PARA_MIN(have, (size_t)want);
+       rv = PARA_MIN(have, want);
        PARA_DEBUG_LOG("reading %zu bytes @%zu\n", rv, c->fpos);
        memcpy(dest, c->map + c->fpos, rv);
        c->fpos += rv;
        return rv;
 }
 
-static uint32_t aac_afh_seek_cb(void *user_data, uint64_t pos)
+static off_t aac_afh_seek_cb(void *user_data, off_t offset, int whence)
 {
        struct aac_afh_context *c = user_data;
-       c->fpos = pos;
-       return 0;
-}
 
-static int32_t aac_afh_get_track(mp4ff_t *mp4ff, mp4AudioSpecificConfig *masc)
-{
-       int32_t i, rc, num_tracks = mp4ff_total_tracks(mp4ff);
-
-       assert(num_tracks >= 0);
-       for (i = 0; i < num_tracks; i++) {
-               unsigned char *buf = NULL;
-               unsigned buf_size = 0;
-
-               mp4ff_get_decoder_config(mp4ff, i, &buf, &buf_size);
-               if (buf) {
-                       rc = NeAACDecAudioSpecificConfig(buf, buf_size, masc);
-                       free(buf);
-                       if (rc < 0)
-                               continue;
-                       return i;
-               }
-       }
-       return -1; /* no audio track */
+       if (whence == SEEK_SET)
+               c->fpos = offset;
+       else if (whence == SEEK_CUR)
+               c->fpos += offset;
+       else if (whence == SEEK_END)
+               c->fpos = c->mapsize + offset;
+       else
+               assert(false);
+       return c->fpos;
 }
 
 static int aac_afh_open(const void *map, size_t mapsize, void **afh_context)
@@ -87,18 +68,11 @@ static int aac_afh_open(const void *map, size_t mapsize, void **afh_context)
        c->cb.seek = aac_afh_seek_cb;
        c->cb.user_data = c;
 
-       ret = -E_MP4FF_OPEN;
-       c->mp4ff = mp4ff_open_read(&c->cb);
-       if (!c->mp4ff)
+       ret = mp4_open(&c->cb, &c->mp4);
+       if (ret < 0)
                goto free_ctx;
-       c->track = aac_afh_get_track(c->mp4ff, &c->masc);
-       ret = -E_MP4FF_TRACK;
-       if (c->track < 0)
-               goto close_mp4ff;
        *afh_context = c;
        return 0;
-close_mp4ff:
-       mp4ff_close(c->mp4ff);
 free_ctx:
        free(c);
        *afh_context = NULL;
@@ -108,51 +82,39 @@ free_ctx:
 static void aac_afh_close(void *afh_context)
 {
        struct aac_afh_context *c = afh_context;
-       mp4ff_close(c->mp4ff);
+       mp4_close(c->mp4);
        free(c);
 }
 
-/**
- * Libmp4ff function to reposition the file to the given sample.
- *
- * \param f The opaque handle returned by mp4ff_open_read().
- * \param track The number of the (audio) track.
- * \param sample Destination.
- *
- * We need this function to obtain the offset of the sample within the audio
- * file. Unfortunately, it is not exposed in the mp4ff header.
- *
- * \return This function always returns 0.
- */
-int32_t mp4ff_set_sample_position(mp4ff_t *f, const int32_t track, const int32_t sample);
-
 static int aac_afh_get_chunk(uint32_t chunk_num, void *afh_context,
                const char **buf, uint32_t *len)
 {
        struct aac_afh_context *c = afh_context;
-       int32_t ss;
+       uint32_t ss;
        size_t offset;
+       int ret;
 
-       assert(chunk_num <= INT_MAX);
-       /* this function always returns zero */
-       mp4ff_set_sample_position(c->mp4ff, c->track, chunk_num);
+       ret = mp4_set_sample_position(c->mp4, chunk_num);
+       if (ret < 0)
+               return ret;
        offset = c->fpos;
-       ss = mp4ff_read_sample_getsize(c->mp4ff, c->track, chunk_num);
-       if (ss <= 0)
-               return -E_MP4FF_BAD_SAMPLE;
-       assert(ss + offset <= c->mapsize);
+       ret = mp4_get_sample_size(c->mp4, chunk_num, &ss);
+       if (ret < 0)
+               return ret;
+       if (ss + offset > c->mapsize) /* file got truncated?! */
+               return -E_MP4_CORRUPT;
        *buf = c->map + offset;
        *len = ss;
        return 1;
 }
 
-static void _aac_afh_get_taginfo(const mp4ff_t *mp4ff, struct taginfo *tags)
+static void aac_afh_get_taginfo(const struct mp4 *mp4, struct taginfo *tags)
 {
-       mp4ff_meta_get_artist(mp4ff, &tags->artist);
-       mp4ff_meta_get_title(mp4ff, &tags->title);
-       mp4ff_meta_get_date(mp4ff, &tags->year);
-       mp4ff_meta_get_album(mp4ff, &tags->album);
-       mp4ff_meta_get_comment(mp4ff, &tags->comment);
+       tags->artist = mp4_get_tag_value(mp4, "artist");
+       tags->title = mp4_get_tag_value(mp4, "title");
+       tags->year = mp4_get_tag_value(mp4, "date");
+       tags->album = mp4_get_tag_value(mp4, "album");
+       tags->comment = mp4_get_tag_value(mp4, "comment");
 }
 
 /*
@@ -162,9 +124,8 @@ static int aac_get_file_info(char *map, size_t numbytes, __a_unused int fd,
                struct afh_info *afhi)
 {
        int ret;
-       int32_t rv;
        struct aac_afh_context *c;
-       int64_t tmp;
+       uint64_t milliseconds;
        const char *buf;
        uint32_t n, len;
 
@@ -172,157 +133,116 @@ static int aac_get_file_info(char *map, size_t numbytes, __a_unused int fd,
        if (ret < 0)
                return ret;
 
-       ret = -E_MP4FF_BAD_SAMPLERATE;
-       rv = mp4ff_get_sample_rate(c->mp4ff, c->track);
-       if (rv <= 0)
-               goto close;
-       afhi->frequency = rv;
-
-       ret = -E_MP4FF_BAD_CHANNEL_COUNT;
-       rv = mp4ff_get_channel_count(c->mp4ff, c->track);
-       if (rv <= 0)
-               goto close;
-       afhi->channels = rv;
-
-       ret = -E_MP4FF_BAD_SAMPLE_COUNT;
-       rv = mp4ff_num_samples(c->mp4ff, c->track);
-       if (rv <= 0)
-               goto close;
-       afhi->chunks_total = rv;
+       afhi->frequency = mp4_get_sample_rate(c->mp4);
+       assert(afhi->frequency > 0);
+       afhi->channels = mp4_get_channel_count(c->mp4);
+       assert(afhi->channels > 0);
+       afhi->chunks_total = mp4_num_samples(c->mp4);
+       assert(afhi->chunks_total > 0);
+
        afhi->max_chunk_size = 0;
        for (n = 0; n < afhi->chunks_total; n++) {
-               if (aac_afh_get_chunk(n, c, &buf, &len) < 0)
-                       break;
+               ret = aac_afh_get_chunk(n, c, &buf, &len);
+               if (ret < 0)
+                       goto out;
                afhi->max_chunk_size = PARA_MAX(afhi->max_chunk_size, len);
        }
-
-       tmp = c->masc.sbr_present_flag == 1? 2048 : 1024;
-       afhi->seconds_total = tmp * afhi->chunks_total / afhi->frequency;
-       ms2tv(1000 * tmp / afhi->frequency, &afhi->chunk_tv);
-
-       if (aac_afh_get_chunk(0, c, &buf, &len) >= 0)
-               numbytes -= buf - map;
+       milliseconds = mp4_get_duration(c->mp4);
+       afhi->seconds_total = milliseconds / 1000;
+       ms2tv(milliseconds / afhi->chunks_total, &afhi->chunk_tv);
+       if (aac_afh_get_chunk(0, c, &buf, &len) < 0)
+               goto out;
+       numbytes -= buf - map;
        afhi->bitrate = 8 * numbytes / afhi->seconds_total / 1000;
-       _aac_afh_get_taginfo(c->mp4ff, &afhi->tags);
+       aac_afh_get_taginfo(c->mp4, &afhi->tags);
        ret = 1;
-close:
+out:
        aac_afh_close(c);
        return ret;
 }
 
-static uint32_t aac_afh_meta_read_cb(void *user_data, void *dest, uint32_t want)
+static ssize_t aac_afh_meta_read_cb(void *user_data, void *dest, size_t want)
 {
        int fd = *(int *)user_data;
        return read(fd, dest, want);
 }
 
-static uint32_t aac_afh_meta_seek_cb(void *user_data, uint64_t pos)
+static off_t aac_afh_meta_seek_cb(void *user_data, off_t offset, int whence)
 {
        int fd = *(int *)user_data;
-       return lseek(fd, pos, SEEK_SET);
+       off_t ret = lseek(fd, offset, whence);
+
+       assert(ret != (off_t)-1);
+       return ret;
 }
 
-static uint32_t aac_afh_meta_write_cb(void *user_data, void *dest, uint32_t want)
+static ssize_t aac_afh_meta_write_cb(void *user_data, void *dest, size_t count)
 {
        int fd = *(int *)user_data;
-       return write(fd, dest, want);
+       return write(fd, dest, count);
 }
 
-static uint32_t aac_afh_meta_truncate_cb(void *user_data)
+static int aac_afh_meta_truncate_cb(void *user_data)
 {
        int fd = *(int *)user_data;
        off_t offset = lseek(fd, 0, SEEK_CUR);
        return ftruncate(fd, offset);
 }
 
-static void replace_tag(mp4ff_tag_t *tag, const char *new_val, bool *found)
-{
-       free(tag->value);
-       tag->value = para_strdup(new_val);
-       *found = true;
-}
-
-static void add_tag(mp4ff_metadata_t *md, const char *item, const char *value)
+static void replace_or_add_tag(const char *item, const char *value,
+               struct mp4_metadata *meta)
 {
-       md->tags[md->count].item = para_strdup(item);
-       md->tags[md->count].value = para_strdup(value);
-       md->count++;
+       uint32_t n;
+       struct mp4_tag *t;
+
+       for (n = 0; n < meta->count; n++) {
+               t = meta->tags + n;
+               if (strcasecmp(t->item, item))
+                       continue;
+               free(t->value);
+               t->value = para_strdup(value);
+               return;
+       }
+       /* item not found, add new tag */
+       meta->tags = para_realloc(meta->tags, (meta->count + 1)
+               * sizeof(struct mp4_tag));
+       t = meta->tags + meta->count;
+       t->item = para_strdup(item);
+       t->value = para_strdup(value);
+       meta->count++;
 }
 
 static int aac_afh_rewrite_tags(const char *map, size_t mapsize,
                struct taginfo *tags, int fd, __a_unused const char *filename)
 {
-       int ret, i;
-       int32_t rv;
-       mp4ff_metadata_t metadata;
-       mp4ff_t *mp4ff;
-       mp4ff_callback_t cb = {
+       int ret;
+       struct mp4_metadata *metadata;
+       struct mp4 *mp4;
+       struct mp4_callback cb = {
                .read = aac_afh_meta_read_cb,
                .seek = aac_afh_meta_seek_cb,
                .write = aac_afh_meta_write_cb,
                .truncate = aac_afh_meta_truncate_cb,
                .user_data = &fd
        };
-       bool found_artist = false, found_title = false, found_album = false,
-               found_year = false, found_comment = false;
 
        ret = write_all(fd, map, mapsize);
        if (ret < 0)
                return ret;
        lseek(fd, 0, SEEK_SET);
 
-       mp4ff = mp4ff_open_read_metaonly(&cb);
-       if (!mp4ff)
-               return -E_MP4FF_OPEN;
-
-       ret = -E_MP4FF_META_READ;
-       rv = mp4ff_meta_get_num_items(mp4ff);
-       if (rv < 0)
-               goto close;
-       metadata.count = rv;
-       PARA_NOTICE_LOG("%d metadata item(s) found\n", rv);
-
-       metadata.tags = para_malloc((metadata.count + 5) * sizeof(mp4ff_tag_t));
-       for (i = 0; i < metadata.count; i++) {
-               mp4ff_tag_t *tag = metadata.tags + i;
-
-               ret = -E_MP4FF_META_READ;
-               if (!mp4ff_meta_get_by_index(mp4ff, i, &tag->item, &tag->value))
-                       goto free_tags;
-               PARA_INFO_LOG("found: %s: %s\n", tag->item, tag->value);
-               if (!strcmp(tag->item, "artist"))
-                       replace_tag(tag, tags->artist, &found_artist);
-               else if (!strcmp(tag->item, "title"))
-                       replace_tag(tag, tags->title, &found_title);
-               else if (!strcmp(tag->item, "album"))
-                       replace_tag(tag, tags->album, &found_album);
-               else if (!strcmp(tag->item, "date"))
-                       replace_tag(tag, tags->year, &found_year);
-               else if (!strcmp(tag->item, "comment"))
-                       replace_tag(tag, tags->comment, &found_comment);
-       }
-       if (!found_artist)
-               add_tag(&metadata, "artist", tags->artist);
-       if (!found_title)
-               add_tag(&metadata, "title", tags->title);
-       if (!found_album)
-               add_tag(&metadata, "album", tags->album);
-       if (!found_year)
-               add_tag(&metadata, "date", tags->year);
-       if (!found_comment)
-               add_tag(&metadata, "comment", tags->comment);
-       ret = -E_MP4FF_META_WRITE;
-       if (!mp4ff_meta_update(&cb, &metadata))
-               goto free_tags;
-       ret = 1;
-free_tags:
-       for (; i > 0; i--) {
-               free(metadata.tags[i - 1].item);
-               free(metadata.tags[i - 1].value);
-       }
-       free(metadata.tags);
-close:
-       mp4ff_close(mp4ff);
+       ret = mp4_open_meta(&cb, &mp4);
+       if (ret < 0)
+               return ret;
+       metadata = mp4_get_meta(mp4);
+       PARA_NOTICE_LOG("%u metadata item(s) found\n", metadata->count);
+       replace_or_add_tag("artist", tags->artist, metadata);
+       replace_or_add_tag("title", tags->title, metadata);
+       replace_or_add_tag("album", tags->album, metadata);
+       replace_or_add_tag("date", tags->year, metadata);
+       replace_or_add_tag("comment", tags->comment, metadata);
+       ret = mp4_update_meta(mp4);
+       mp4_close(mp4);
        return ret;
 }
 
index caf1401d8bf02e2d299bdcdf58ae902cfb260092..cbfea7fdfb2ea5e2b8ad84c8faba815b67b8a966 100755 (executable)
@@ -1,28 +1,8 @@
 #!/bin/sh
-# check if we have multiple processors/cores
-n=$(nproc 2>/dev/null)
-if [ -z "$n" ]; then
-       n=$(grep ^processor /proc/cpuinfo 2>/dev/null | wc -l)
-       [ $n -eq 0 ] && n=1
-fi
-# If we are compiling with distcc, try to guess a reasonable number
-# based on (a) the number of cores on this machine and (b) the number
-# of words in the DISTCC_HOSTS variable.
-d="$(echo $DISTCC_HOSTS | wc -w)"
-n=$(($n + 2 * $n * $d))
-echo preparing, parallel=$n...
-if test -f Makefile; then
-       make maintainer-clean > /dev/null 2>&1
-fi
 autom4te \
        --language=autoconf \
        --output=configure \
        --no-cache \
        --warnings=all \
-       configure.ac
+       configure.ac &&
 autoheader
-echo configuring...
-./configure $@ > /dev/null
-echo compiling...
-make clean > /dev/null 2>&1
-make -j $n > /dev/null && make check
index 0e37e39ef412521b611fe38353865c21f6e315d8..c72581118326924a25b5ca9b020316439135d60b 100644 (file)
@@ -291,12 +291,10 @@ AC_DEFUN([NEED_FLAC_OBJECTS], [{
 }])
 ########################################################################### faad
 STASH_FLAGS
-LIB_ARG_WITH([faad], [-lfaad -lmp4ff])
+LIB_ARG_WITH([faad], [-lfaad])
 HAVE_FAAD=yes
 AC_CHECK_HEADER(neaacdec.h, [], HAVE_FAAD=no)
-AC_CHECK_HEADER(mp4ff.h, [], HAVE_FAAD=no)
 AC_CHECK_LIB([faad], [NeAACDecOpen], [], HAVE_FAAD=no)
-AC_CHECK_LIB([mp4ff], [mp4ff_meta_get_artist], [], HAVE_FAAD=no)
 LIB_SUBST_FLAGS(faad)
 UNSTASH_FLAGS
 ########################################################################### mad
@@ -434,7 +432,7 @@ if test -n "$CRYPTOLIB" && test $HAVE_OSL = yes && test -n "$BISON" && \
        NEED_OPUS_OBJECTS() && server_errlist_objs="$server_errlist_objs opus_afh opus_common"
        NEED_FLAC_OBJECTS && server_errlist_objs="$server_errlist_objs flac_afh"
        if test $HAVE_FAAD = yes; then
-               server_errlist_objs="$server_errlist_objs aac_afh"
+               server_errlist_objs="$server_errlist_objs aac_afh mp4"
        fi
        server_objs="$server_errlist_objs"
        AC_SUBST(server_objs, add_dot_o($server_objs))
@@ -693,7 +691,7 @@ NEED_OPUS_OBJECTS && recv_errlist_objs="$recv_errlist_objs opus_afh opus_common"
 NEED_FLAC_OBJECTS && recv_errlist_objs="$recv_errlist_objs flac_afh"
 
 if test $HAVE_FAAD = yes; then
-       recv_errlist_objs="$recv_errlist_objs aac_afh"
+       recv_errlist_objs="$recv_errlist_objs aac_afh mp4"
 fi
 recv_objs="$recv_errlist_objs"
 AC_SUBST(recv_objs, add_dot_o($recv_objs))
@@ -728,7 +726,7 @@ NEED_FLAC_OBJECTS && {
        audio_format_handlers="$audio_format_handlers flac"
 }
 if test $HAVE_FAAD = yes; then
-       afh_errlist_objs="$afh_errlist_objs aac_afh"
+       afh_errlist_objs="$afh_errlist_objs aac_afh mp4"
        audio_format_handlers="$audio_format_handlers aac"
 fi
 
@@ -786,7 +784,7 @@ NEED_FLAC_OBJECTS && {
        play_errlist_objs="$play_errlist_objs flacdec_filter flac_afh"
 }
 if test $HAVE_FAAD = yes; then
-       play_errlist_objs="$play_errlist_objs aac_afh aacdec_filter"
+       play_errlist_objs="$play_errlist_objs aac_afh aacdec_filter mp4"
 fi
 if test $HAVE_MAD = yes; then
        play_errlist_objs="$play_errlist_objs mp3dec_filter"
diff --git a/error.h b/error.h
index 60c23294d1270c9aa8b4f56d69a1def2f1fac9b8..40081b6b3551712f051ed3ecc4d1ca40ebf2d4f8 100644 (file)
--- a/error.h
+++ b/error.h
        PARA_ERROR(MP3DEC_CORRUPT, "too many corrupt frames"), \
        PARA_ERROR(MP3DEC_EOF, "mp3dec: end of file"), \
        PARA_ERROR(MP3_INFO, "could not read mp3 info"), \
-       PARA_ERROR(MP4FF_BAD_CHANNEL_COUNT, "mp4ff: invalid number of channels"), \
-       PARA_ERROR(MP4FF_BAD_SAMPLE, "mp4ff: invalid sample number"), \
-       PARA_ERROR(MP4FF_BAD_SAMPLERATE, "mp4ff: invalid sample rate"), \
-       PARA_ERROR(MP4FF_BAD_SAMPLE_COUNT, "mp4ff: invalid number of samples"), \
-       PARA_ERROR(MP4FF_META_READ, "mp4ff: could not read mp4 metadata"), \
-       PARA_ERROR(MP4FF_META_WRITE, "mp4ff: could not update mp4 metadata"), \
-       PARA_ERROR(MP4FF_OPEN, "mp4ff: open failed"), \
-       PARA_ERROR(MP4FF_TRACK, "mp4ff: no audio track"), \
+       PARA_ERROR(MP4_READ, "mp4: read error or unexpected end of file"), \
+       PARA_ERROR(MP4_CORRUPT, "invalid/corrupt mp4 file"), \
+       PARA_ERROR(MP4_BAD_SAMPLE, "mp4: invalid sample number"), \
+       PARA_ERROR(MP4_BAD_SAMPLERATE, "mp4: invalid sample rate"), \
+       PARA_ERROR(MP4_BAD_SAMPLE_COUNT, "mp4: invalid number of samples"), \
+       PARA_ERROR(MP4_TRACK, "mp4: no audio track"), \
+       PARA_ERROR(MP4_MISSING_ATOM, "mp4: essential atom not found"), \
        PARA_ERROR(MPI_SCAN, "could not scan multi-precision integer"), \
        PARA_ERROR(NAME_TOO_LONG, "name too long for struct sockaddr_un"), \
        PARA_ERROR(NO_AFHI, "audio format handler info required"), \
diff --git a/mp4.c b/mp4.c
new file mode 100644 (file)
index 0000000..4b8607b
--- /dev/null
+++ b/mp4.c
@@ -0,0 +1,1055 @@
+/*
+ * Copyright (C) 2003-2005 M. Bakker, Nero AG, http://www.nero.com
+ * FAAD2 - Freeware Advanced Audio (AAC) Decoder including SBR decoding
+ *
+ * See file COPYING.
+ */
+
+/** \file mp4.c Paraslash's internal mp4 parser. */
+
+/*
+ * This is a stripped down version of the former mp4ff library which used to be
+ * part of the faad decoder project but was removed from the faad code base in
+ * 2017. The original code has been cleaned up substantially and the public API
+ * has been documented. See the git commit log for details.
+ */
+
+#include <regex.h>
+
+#include "para.h"
+#include "error.h"
+#include "portable_io.h"
+#include "string.h"
+#include "mp4.h"
+
+/**
+ * The three states of the mp4 parser. The parser only loads the audio specific
+ * values and tables when it is in the second state.
+ */
+enum audio_track_state {
+       /** We haven't encountered an mp4a atom so far. */
+       ATS_INITIAL,
+       /** We have seen an mp4a atom but no subsequent trak atom yet. */
+       ATS_SEEN_MP4A,
+       /** A trak atom was seen *after* the mp4a atom. */
+       ATS_TRACK_CHANGE,
+};
+
+struct mp4_track {
+       /* determines which atoms we still need to parse. */
+       enum audio_track_state state;
+
+       /* mp4a */
+       uint16_t channel_count;
+       uint16_t sample_rate;
+
+       /* stsz */
+       uint32_t stsz_sample_size;
+       uint32_t stsz_sample_count;
+       uint32_t *stsz_table;
+
+       /* stts */
+       uint32_t stts_entry_count;
+       uint32_t *stts_sample_count;
+
+       /* stsc */
+       uint32_t stsc_entry_count;
+       uint32_t *stsc_first_chunk;
+       uint32_t *stsc_samples_per_chunk;
+
+       /* stsc */
+       uint32_t stco_entry_count;
+       uint32_t *stco_chunk_offset;
+
+       /* mdhd */
+       uint32_t time_scale;
+       uint64_t duration;
+};
+
+struct mp4 {
+       const struct mp4_callback *cb;
+
+       uint64_t moov_offset;
+       uint64_t moov_size;
+       uint64_t meta_offset;
+       uint32_t meta_size;
+       uint64_t ilst_offset;
+       uint32_t ilst_size;
+       uint64_t udta_offset;
+       uint32_t udta_size;
+
+       uint8_t last_atom;
+       struct mp4_track track;
+       struct mp4_metadata meta;
+};
+
+/*
+ * Returns -E_MP4_READ, 0, or 1 on errors/EOF/success. Partial reads followed
+ * by EOF or read errors are treated as errors.
+ */
+static int read_data(struct mp4 *f, void *data, size_t size)
+{
+       while (size > 0) {
+               ssize_t ret = f->cb->read(f->cb->user_data, data, size);
+               if (ret < 0 && errno == EINTR)
+                       continue;
+               /* regard EAGAIN as an error as reads should be blocking. */
+               if (ret <= 0)
+                       return ret < 0? -E_MP4_READ : 0;
+               size -= ret;
+       }
+       return 1;
+}
+
+static int read_int64(struct mp4 *f, uint64_t *result)
+{
+       uint8_t data[8];
+       int ret = read_data(f, data, 8);
+
+       if (ret > 0)
+               *result = read_u64_be(data);
+       return ret;
+}
+
+static int read_int32(struct mp4 *f, uint32_t *result)
+{
+       uint8_t data[4];
+       int ret = read_data(f, data, 4);
+
+       if (ret > 0)
+               *result = read_u32_be(data);
+       return ret;
+}
+
+static int read_int16(struct mp4 *f, uint16_t *result)
+{
+       uint8_t data[2];
+       int ret = read_data(f, data, 2);
+
+       if (ret > 0)
+               *result = read_u16_be(data);
+       return ret;
+}
+
+/** A macro defining the atoms we care about. It gets expanded twice. */
+#define ATOM_ITEMS \
+       ATOM_ITEM(MOOV, 'm', 'o', 'o', 'v') /* movie (top-level container) */ \
+       ATOM_ITEM(TRAK, 't', 'r', 'a', 'k') /* container for a single track */ \
+       ATOM_ITEM(MDIA, 'm', 'd', 'i', 'a') /* media information */ \
+       ATOM_ITEM(MINF, 'm', 'i', 'n', 'f') /* extends mdia */ \
+       ATOM_ITEM(STBL, 's', 't', 'b', 'l') /* sample table container */ \
+       ATOM_ITEM(UDTA, 'u', 'd', 't', 'a') /* user data */ \
+       ATOM_ITEM(ILST, 'i', 'l', 's', 't') /* iTunes Metadata list */ \
+       ATOM_ITEM(ARTIST, 0xa9, 'A', 'R', 'T') /* artist */ \
+       ATOM_ITEM(TITLE, 0xa9, 'n', 'a', 'm') /* title */ \
+       ATOM_ITEM(ALBUM, 0xa9, 'a', 'l', 'b') /* album */ \
+       ATOM_ITEM(DATE, 0xa9, 'd', 'a', 'y') /* date */ \
+       ATOM_ITEM(COMMENT, 0xa9, 'c', 'm', 't') /* comment */ \
+       ATOM_ITEM(MDHD, 'm', 'd', 'h', 'd') /* track header */ \
+       ATOM_ITEM(STSD, 's', 't', 's', 'd') /* sample description box */ \
+       ATOM_ITEM(STTS, 's', 't', 't', 's') /* time to sample box */ \
+       ATOM_ITEM(STSZ, 's', 't', 's', 'z') /* sample size box */ \
+       ATOM_ITEM(STCO, 's', 't', 'c', 'o') /* chunk offset box */ \
+       ATOM_ITEM(STSC, 's', 't', 's', 'c') /* sample to chunk box */ \
+       ATOM_ITEM(MP4A, 'm', 'p', '4', 'a') /* mp4 audio */ \
+       ATOM_ITEM(META, 'm', 'e', 't', 'a') /* iTunes Metadata box */ \
+       ATOM_ITEM(DATA, 'd', 'a', 't', 'a') /* iTunes Metadata data box */ \
+
+/** For the C enumeration we concatenate ATOM_ with the first argument. */
+#define ATOM_ITEM(_name, a, b, c, d) ATOM_ ## _name,
+/** The enumeration of interesting atoms. */
+enum atom {ATOM_ITEMS};
+#undef ATOM_ITEM
+
+/** A cpp version of read_u32_be(). */
+#define ATOM_VALUE(a, b, c, d) ((a << 24) + (b << 16) + (c << 8) + d)
+
+static uint8_t atom_name_to_type(uint8_t *p)
+{
+       /** Expands to an instance of the following unnamed structure. */
+       #define ATOM_ITEM(_name, a, b, c, d) \
+               {.name = # _name, .val = ATOM_VALUE(a, b, c, d)},
+       static const struct {
+               const char *name;
+               uint32_t val;
+       } atom_table[] = {ATOM_ITEMS};
+       #undef ATOM_ITEM
+       uint32_t val = read_u32_be(p);
+
+       for (uint8_t n = 0; n < ARRAY_SIZE(atom_table); n++)
+               if (val == atom_table[n].val)
+                       return n;
+       return 255;
+}
+
+/* read atom header, atom size is returned with header included. */
+static int atom_read_header(struct mp4 *f, uint8_t *atom_type,
+               uint8_t *header_size, uint64_t *atom_size)
+{
+       uint32_t size;
+       int ret;
+       uint8_t atom_header[8];
+
+       ret = read_data(f, atom_header, 8);
+       if (ret <= 0)
+               return ret;
+       size = read_u32_be(atom_header);
+       if (size == 1) { /* 64 bit atom size */
+               if (header_size)
+                       *header_size = 16;
+               ret = read_int64(f, atom_size);
+               if (ret <= 0)
+                       return ret;
+       } else {
+               if (header_size)
+                       *header_size = 8;
+               *atom_size = size;
+       }
+       *atom_type = atom_name_to_type(atom_header + 4);
+       return 1;
+}
+
+static off_t get_position(const struct mp4 *f)
+{
+       return f->cb->seek(f->cb->user_data, 0, SEEK_CUR);
+}
+
+static void set_position(struct mp4 *f, off_t position)
+{
+       f->cb->seek(f->cb->user_data, position, SEEK_SET);
+}
+
+static void skip_bytes(struct mp4 *f, off_t num_skip)
+{
+       f->cb->seek(f->cb->user_data, num_skip, SEEK_CUR);
+}
+
+static int read_stsz(struct mp4 *f)
+{
+       int ret;
+       struct mp4_track *t = &f->track;
+
+       if (t->state != ATS_SEEN_MP4A || t->stsz_table)
+               return 1;
+       skip_bytes(f, 4); /* version (1), flags (3) */
+       ret = read_int32(f, &t->stsz_sample_size);
+       if (ret <= 0)
+               return ret;
+       ret = read_int32(f, &t->stsz_sample_count);
+       if (ret <= 0)
+               return ret;
+       if (t->stsz_sample_size != 0)
+               return 1;
+       t->stsz_table = para_malloc(t->stsz_sample_count * sizeof(int32_t));
+       for (uint32_t n = 0; n < t->stsz_sample_count; n++) {
+               ret = read_int32(f, &t->stsz_table[n]);
+               if (ret <= 0)
+                       return ret;
+       }
+       return 1;
+}
+
+static int read_stts(struct mp4 *f)
+{
+       int ret;
+       struct mp4_track *t = &f->track;
+
+       if (t->state != ATS_SEEN_MP4A || t->stts_sample_count)
+               return 1;
+       skip_bytes(f, 4); /* version (1), flags (3) */
+       ret = read_int32(f, &t->stts_entry_count);
+       if (ret <= 0)
+               return ret;
+       t->stts_sample_count = para_malloc(t->stts_entry_count
+               * sizeof(int32_t));
+       for (uint32_t n = 0; n < t->stts_entry_count; n++) {
+               ret = read_int32(f, &t->stts_sample_count[n]);
+               if (ret <= 0)
+                       return ret;
+               skip_bytes(f, 4); /* sample delta */
+       }
+       return 1;
+}
+
+static int read_stsc(struct mp4 *f)
+{
+       int ret;
+       struct mp4_track *t = &f->track;
+
+       if (t->state != ATS_SEEN_MP4A)
+               return 1;
+       if (t->stsc_first_chunk || t->stsc_samples_per_chunk)
+               return 1;
+       skip_bytes(f, 4); /* version (1), flags (3) */
+       ret = read_int32(f, &t->stsc_entry_count);
+       if (ret <= 0)
+               return ret;
+       t->stsc_first_chunk = para_malloc(t->stsc_entry_count * sizeof(int32_t));
+       t->stsc_samples_per_chunk = para_malloc(t->stsc_entry_count
+               * sizeof (int32_t));
+       for (uint32_t n = 0; n < t->stsc_entry_count; n++) {
+               ret = read_int32(f, &t->stsc_first_chunk[n]);
+               if (ret <= 0)
+                       return ret;
+               ret = read_int32(f, &t->stsc_samples_per_chunk[n]);
+               if (ret <= 0)
+                       return ret;
+               skip_bytes(f, 4); /* sample desc index */
+       }
+       return 1;
+}
+
+static int read_stco(struct mp4 *f)
+{
+       int ret;
+       struct mp4_track *t = &f->track;
+
+       if (t->state != ATS_SEEN_MP4A || t->stco_chunk_offset)
+               return 1;
+       skip_bytes(f, 4); /* version (1), flags (3) */
+       ret = read_int32(f, &t->stco_entry_count);
+       if (ret <= 0)
+               return ret;
+       t->stco_chunk_offset = para_malloc(t->stco_entry_count
+               * sizeof(int32_t));
+       for (uint32_t n = 0; n < t->stco_entry_count; n++) {
+               ret = read_int32(f, &t->stco_chunk_offset[n]);
+               if (ret <= 0)
+                       return ret;
+       }
+       return 1;
+}
+
+static int read_stsd(struct mp4 *f)
+{
+       int ret;
+       uint32_t entry_count;
+
+       if (f->track.state != ATS_INITIAL)
+               return 1;
+       skip_bytes(f, 4); /* version (1), flags (3) */
+       ret = read_int32(f, &entry_count);
+       if (ret <= 0)
+               return ret;
+       for (uint32_t n = 0; n < entry_count; n++) {
+               uint64_t skip = get_position(f);
+               uint64_t size;
+               uint8_t atom_type = 0;
+               ret = atom_read_header(f, &atom_type, NULL, &size);
+               if (ret <= 0)
+                       return ret;
+               skip += size;
+               if (atom_type == ATOM_MP4A) {
+                       f->track.state = ATS_SEEN_MP4A;
+                       /* reserved (6), data reference index (2), reserved (8) */
+                       skip_bytes(f, 16);
+                       ret = read_int16(f, &f->track.channel_count);
+                       if (ret <= 0)
+                               return ret;
+                       skip_bytes(f, 6);
+                       ret = read_int16(f, &f->track.sample_rate);
+                       if (ret <= 0)
+                               return ret;
+               }
+               set_position(f, skip);
+       }
+       return 1;
+}
+
+static const char *get_metadata_name(uint8_t atom_type)
+{
+       switch (atom_type) {
+       case ATOM_TITLE: return "title";
+       case ATOM_ARTIST: return "artist";
+       case ATOM_ALBUM: return "album";
+       case ATOM_DATE: return "date";
+       case ATOM_COMMENT: return "comment";
+       default: return "unknown";
+       }
+}
+
+static int parse_tag(struct mp4 *f, uint8_t parent, int32_t size)
+{
+       int ret;
+       uint64_t subsize, sumsize;
+       char *value = NULL;
+       uint32_t len = 0;
+       uint64_t destpos;
+       struct mp4_tag *tag;
+
+       for (
+               sumsize = 0;
+               sumsize < size;
+               set_position(f, destpos), sumsize += subsize
+       ) {
+               uint8_t atom_type;
+               uint8_t header_size = 0;
+               ret = atom_read_header(f, &atom_type, &header_size, &subsize);
+               if (ret <= 0)
+                       goto fail;
+               destpos = get_position(f) + subsize - header_size;
+               if (atom_type != ATOM_DATA)
+                       continue;
+               skip_bytes(f, 8); /* version (1), flags (3), reserved (4) */
+               ret = -E_MP4_CORRUPT;
+               if (subsize < header_size + 8 || subsize > UINT_MAX)
+                       goto fail;
+               len = subsize - (header_size + 8);
+               free(value);
+               value = para_malloc(len + 1);
+               ret = read_data(f, value, len);
+               if (ret <= 0)
+                       goto fail;
+               value[len] = '\0';
+       }
+       if (!value)
+               return -E_MP4_CORRUPT;
+       f->meta.tags = para_realloc(f->meta.tags, (f->meta.count + 1)
+               * sizeof(struct mp4_tag));
+       tag = f->meta.tags + f->meta.count;
+       tag->item = para_strdup(get_metadata_name(parent));
+       tag->value = value;
+       f->meta.count++;
+       return 1;
+fail:
+       free(value);
+       return ret;
+}
+
+static int read_mdhd(struct mp4 *f)
+{
+       int ret;
+       uint32_t version;
+       struct mp4_track *t = &f->track;
+
+       if (t->state != ATS_INITIAL)
+               return 1;
+       ret = read_int32(f, &version);
+       if (ret <= 0)
+               return ret;
+       if (version == 1) {
+               skip_bytes(f, 16); /* creation time (8), modification time (8) */
+               ret = read_int32(f, &t->time_scale);
+               if (ret <= 0)
+                       return ret;
+               ret = read_int64(f, &t->duration);
+               if (ret <= 0)
+                       return ret;
+       } else { /* version == 0 */
+               uint32_t temp;
+
+               skip_bytes(f, 8); /* creation time (4), modification time (4) */
+               ret = read_int32(f, &t->time_scale);
+               if (ret <= 0)
+                       return ret;
+               ret = read_int32(f, &temp);
+               if (ret <= 0)
+                       return ret;
+               t->duration = (temp == (uint32_t) (-1))?
+                       (uint64_t) (-1) : (uint64_t) (temp);
+       }
+       skip_bytes(f, 4);
+       return 1;
+}
+
+static int read_ilst(struct mp4 *f, int32_t size)
+{
+       int ret;
+       uint64_t sumsize = 0;
+
+       while (sumsize < size) {
+               uint8_t atom_type;
+               uint64_t subsize, destpos;
+               uint8_t header_size = 0;
+               ret = atom_read_header(f, &atom_type, &header_size, &subsize);
+               if (ret <= 0)
+                       return ret;
+               destpos = get_position(f) + subsize - header_size;
+               switch (atom_type) {
+               case ATOM_ARTIST:
+               case ATOM_TITLE:
+               case ATOM_ALBUM:
+               case ATOM_COMMENT:
+               case ATOM_DATE:
+                       ret = parse_tag(f, atom_type, subsize - header_size);
+                       if (ret <= 0)
+                               return ret;
+               }
+               set_position(f, destpos);
+               sumsize += subsize;
+       }
+       return 1;
+}
+
+static int read_meta(struct mp4 *f, uint64_t size)
+{
+       int ret;
+       uint64_t subsize, sumsize = 0;
+       uint8_t atom_type;
+       uint8_t header_size = 0;
+
+       skip_bytes(f, 4); /* version (1), flags (3) */
+       while (sumsize < (size - (header_size + 4))) {
+               ret = atom_read_header(f, &atom_type, &header_size, &subsize);
+               if (ret <= 0)
+                       return ret;
+               if (subsize <= header_size + 4)
+                       return 1;
+               if (atom_type == ATOM_ILST) {
+                       f->ilst_offset = get_position(f) - header_size;
+                       f->ilst_size = subsize;
+                       ret = read_ilst(f, subsize - (header_size + 4));
+                       if (ret <= 0)
+                               return ret;
+               } else
+                       set_position(f, get_position(f) + subsize - header_size);
+               sumsize += subsize;
+       }
+       return 1;
+}
+
+static bool need_atom(uint8_t atom_type, bool meta_only)
+{
+       /* these are needed in any case */
+       switch (atom_type) {
+       case ATOM_STSD:
+       case ATOM_META:
+       case ATOM_TRAK:
+       case ATOM_MDIA:
+       case ATOM_MINF:
+       case ATOM_STBL:
+       case ATOM_UDTA:
+               return true;
+       }
+       /* meta-only opens don't need anything else */
+       if (meta_only)
+               return false;
+       /* these are only required for regular opens */
+       switch (atom_type) {
+       case ATOM_STTS:
+       case ATOM_STSZ:
+       case ATOM_STCO:
+       case ATOM_STSC:
+       case ATOM_MDHD:
+               return true;
+       }
+       return false;
+}
+
+/* parse atoms that are sub atoms of other atoms */
+static int parse_sub_atoms(struct mp4 *f, uint64_t total_size, bool meta_only)
+{
+       int ret;
+       uint64_t dest, size, end = get_position(f) + total_size;
+
+       for (dest = get_position(f); dest < end; set_position(f, dest)) {
+               uint8_t header_size, atom_type;
+               ret = atom_read_header(f, &atom_type, &header_size, &size);
+               if (ret <= 0)
+                       return ret;
+               if (size == 0)
+                       return -E_MP4_CORRUPT;
+               dest = get_position(f) + size - header_size;
+               if (atom_type == ATOM_TRAK && f->track.state == ATS_SEEN_MP4A) {
+                       f->track.state = ATS_TRACK_CHANGE;
+                       continue;
+               }
+               if (atom_type == ATOM_UDTA) {
+                       f->udta_offset = get_position(f) - header_size;
+                       f->udta_size = size;
+               }
+               if (!need_atom(atom_type, meta_only))
+                       continue;
+               switch (atom_type) {
+               case ATOM_STSZ: ret = read_stsz(f); break;
+               case ATOM_STTS: ret = read_stts(f); break;
+               case ATOM_STSC: ret = read_stsc(f); break;
+               case ATOM_STCO: ret = read_stco(f); break;
+               case ATOM_STSD: ret = read_stsd(f); break;
+               case ATOM_MDHD: ret = read_mdhd(f); break;
+               case ATOM_META:
+                       f->meta_offset = get_position(f) - header_size;
+                       f->meta_size = size;
+                       ret = read_meta(f, size);
+                       break;
+               default:
+                       ret = parse_sub_atoms(f, size - header_size, meta_only);
+               }
+               if (ret <= 0)
+                       return ret;
+       }
+       return 1;
+}
+
+/**
+ * Deallocate all resources associated with an mp4 file handle.
+ *
+ * \param f File handle returned by \ref mp4_open() or \ref mp4_open_meta().
+ *
+ * This frees the metadata items and various tables which were allocated when
+ * the file was opened. The given file handle must not be NULL.
+ */
+void mp4_close(struct mp4 *f)
+{
+       free(f->track.stsz_table);
+       free(f->track.stts_sample_count);
+       free(f->track.stsc_first_chunk);
+       free(f->track.stsc_samples_per_chunk);
+       free(f->track.stco_chunk_offset);
+       for (uint32_t n = 0; n < f->meta.count; n++) {
+               free(f->meta.tags[n].item);
+               free(f->meta.tags[n].value);
+       }
+       free(f->meta.tags);
+       free(f);
+}
+
+static int open_file(const struct mp4_callback *cb, bool meta_only, struct mp4 **result)
+{
+       int ret;
+       uint64_t size;
+       uint8_t atom_type, header_size;
+       struct mp4 *f = para_calloc(sizeof(*f));
+
+       f->cb = cb;
+       while ((ret = atom_read_header(f, &atom_type, &header_size, &size)) > 0) {
+               f->last_atom = atom_type;
+               if (atom_type != ATOM_MOOV || size <= header_size) { /* skip */
+                       set_position(f, get_position(f) + size - header_size);
+                       continue;
+               }
+               f->moov_offset = get_position(f) - header_size;
+               f->moov_size = size;
+               ret = parse_sub_atoms(f, size - header_size, meta_only);
+               if (ret <= 0)
+                       break;
+       }
+       if (ret < 0)
+               goto fail;
+       ret = -E_MP4_TRACK;
+       if (f->track.channel_count == 0)
+               goto fail;
+       ret = -E_MP4_BAD_SAMPLERATE;
+       if (f->track.sample_rate == 0)
+               goto fail;
+       ret = -E_MP4_MISSING_ATOM;
+       if (f->udta_size == 0 || f->meta_size == 0 || f->ilst_size == 0)
+               goto fail;
+       *result = f;
+       return 1;
+fail:
+       *result = NULL;
+       mp4_close(f);
+       return ret;
+}
+
+/**
+ * Read the audio track and the metadata of an mp4 file.
+ *
+ * \param cb Only the ->read() and ->seek() methods need to be supplied.
+ * \param result Initialized to a non-NULL pointer iff the function succeeds.
+ *
+ * This detects and parses the first audio track and the metadata information
+ * of the mp4 file. Various error checks are performed after the mp4 atoms have
+ * been parsed successfully.
+ *
+ * This function does not modify the file. However, if the caller intents to
+ * update the metadata later, the ->write() and ->truncate() methods must be
+ * supplied in the callback structure.
+ *
+ * \return Standard. Several errors are possible.
+ *
+ * \sa \ref mp4_open_meta().
+ */
+int mp4_open(const struct mp4_callback *cb, struct mp4 **result)
+{
+       struct mp4 *f;
+       int ret;
+
+       *result = NULL;
+       ret = open_file(cb, false, &f);
+       if (ret < 0)
+               return ret;
+       ret = -E_MP4_BAD_SAMPLE_COUNT;
+       if (f->track.stsz_sample_count == 0)
+               goto fail;
+       ret = -E_MP4_CORRUPT;
+       if (f->track.time_scale == 0)
+               goto fail;
+       *result = f;
+       return 1;
+fail:
+       mp4_close(f);
+       return ret;
+}
+
+static int32_t chunk_of_sample(const struct mp4 *f, int32_t sample,
+               int32_t *chunk)
+{
+       const struct mp4_track *t = &f->track;
+       uint32_t *fc = t->stsc_first_chunk, *spc = t->stsc_samples_per_chunk;
+       uint32_t chunk1, chunk1samples, n, total, k;
+
+       for (k = 1, total = 0; k < t->stsc_entry_count; k++, total += n) {
+               n = (fc[k] - fc[k - 1]) * spc[k - 1]; /* number of samples */
+               if (sample < total + n)
+                       break;
+       }
+       chunk1 = fc[k - 1];
+       chunk1samples = spc[k - 1];
+       if (chunk1samples != 0)
+               *chunk = (sample - total) / chunk1samples + chunk1;
+       else
+               *chunk = 1;
+       return total + (*chunk - chunk1) * chunk1samples;
+}
+
+/**
+ * Compute the duration of an mp4 file.
+ *
+ * \param f See \ref mp4_close().
+ *
+ * \return The number of milliseconds of the audio track. This function never
+ * fails.
+ */
+uint64_t mp4_get_duration(const struct mp4 *f)
+{
+       const struct mp4_track *t = &f->track;
+
+       return t->duration * 1000 / t->time_scale;
+}
+
+/**
+ * Reposition the read/write file offset.
+ *
+ * \param f See \ref mp4_close().
+ * \param sample The number of the sample to reposition to.
+ *
+ * The given sample number must be within range, i.e., strictly less than the
+ * value returned by \ref mp4_num_samples().
+ *
+ * \return Standard. The only possible error is an invalid sample number.
+ */
+int mp4_set_sample_position(struct mp4 *f, uint32_t sample)
+{
+       const struct mp4_track *t = &f->track;
+       int32_t offset, chunk, chunk_sample;
+       uint32_t n, srs; /* sample range size */
+
+       if (sample >= t->stsz_sample_count)
+               return -ERRNO_TO_PARA_ERROR(EINVAL);
+       chunk_sample = chunk_of_sample(f, sample, &chunk);
+       if (t->stsz_sample_size > 0)
+               srs = (sample - chunk_sample) * t->stsz_sample_size;
+       else {
+               for (srs = 0, n = chunk_sample; n < sample; n++)
+                       srs += t->stsz_table[n];
+       }
+       if (t->stco_entry_count > 0 && chunk > t->stco_entry_count)
+               offset = t->stco_chunk_offset[t->stco_entry_count - 1];
+       else if (t->stco_entry_count > 0)
+               offset = t->stco_chunk_offset[chunk - 1];
+       else
+               offset = 8;
+       set_position(f, offset + srs);
+       return 1;
+}
+
+/**
+ * Look up and return the size of the given sample in the stsz table.
+ *
+ * \param f See \ref mp4_close().
+ * \param sample The sample number of interest.
+ * \param result Sample size is returned here.
+ *
+ * For the sample argument the restriction mentioned in the documentation of
+ * \ref mp4_set_sample_position() applies as well.
+ *
+ * \return Standard. Like for \ref mp4_set_sample_position(), EINVAL is the
+ * only possible error.
+ */
+int mp4_get_sample_size(const struct mp4 *f, uint32_t sample, uint32_t *result)
+{
+       const struct mp4_track *t = &f->track;
+
+       if (sample >= t->stsz_sample_count)
+               return -ERRNO_TO_PARA_ERROR(EINVAL);
+       if (t->stsz_sample_size != 0)
+               *result = t->stsz_sample_size;
+       else
+               *result = t->stsz_table[sample];
+       return 1;
+}
+
+/**
+ * Return the sample rate stored in the stsd atom.
+ *
+ * \param f See \ref mp4_close().
+ *
+ * The sample rate is a property of the audio track of the mp4 file and is thus
+ * independent of the sample number.
+ *
+ * \return The function always returns a positive value because the open
+ * operation fails if the sample rate happens to be zero. A typical value is
+ * 44100.
+ */
+uint16_t mp4_get_sample_rate(const struct mp4 *f)
+{
+       return f->track.sample_rate;
+}
+
+/**
+ * Return the number of channels of the audio track.
+ *
+ * \param f See \ref mp4_close().
+ *
+ * \return The returned channel count is guaranteed to be positive because the
+ * open operation fails if the mp4a atom is missing or contains a zero channel
+ * count.
+ */
+uint16_t mp4_get_channel_count(const struct mp4 *f)
+{
+       return f->track.channel_count;
+}
+
+/**
+ * Return the number of samples of the audio track.
+ *
+ * \param f See \ref mp4_close().
+ *
+ * \return The sample count is read from the stsz atom during open.
+ */
+uint32_t mp4_num_samples(const struct mp4 *f)
+{
+       return f->track.stsz_sample_count;
+}
+
+/**
+ * Open an mp4 file in metadata-only mode.
+ *
+ * \param cb See \ref mp4_open().
+ * \param result See \ref mp4_open().
+ *
+ * This is similar to \ref mp4_open() but is cheaper because it only parses the
+ * metadata of the mp4 file. The only functions that can subsequently be called
+ * with the file handle returned here are \ref mp4_get_meta() and \ref
+ * mp4_update_meta().
+ *
+ * \return Standard.
+ *
+ * \sa \ref mp4_open(). The comment about ->write() and ->truncate() applies to
+ * this function as well.
+ */
+int mp4_open_meta(const struct mp4_callback *cb, struct mp4 **result)
+{
+       struct mp4 *f;
+       int ret = open_file(cb, true, &f);
+
+       if (ret < 0)
+               return ret;
+       *result = f;
+       return 1;
+}
+
+/**
+ * Return the metadata of an mp4 file.
+ *
+ * \param f See \ref mp4_close().
+ *
+ * The caller is allowed to add, delete or modify the entries of the returned
+ * structure with the intention to pass the modified version to \ref
+ * mp4_update_meta().
+ *
+ * \return This never returns NULL, even if the file contains no metadata tag
+ * items. However, the meta count will be zero and the ->tags pointer NULL in
+ * this case.
+ */
+struct mp4_metadata *mp4_get_meta(struct mp4 *f)
+{
+       return &f->meta;
+}
+
+/** Total length of an on-disk metadata tag. */
+#define TAG_LEN(_len) (24 + (_len))
+static void create_ilst(const struct mp4_metadata *meta, uint8_t *out)
+{
+       for (unsigned n = 0; n < meta->count; n++) {
+               struct mp4_tag *tag = meta->tags + n;
+               unsigned len = strlen(tag->value);
+               const char *atom_name;
+
+               if (!strcasecmp(tag->item, "title"))
+                       atom_name = "\xA9" "nam";
+               else if (!strcasecmp(tag->item, "artist"))
+                       atom_name = "\xA9" "ART";
+               else if (!strcasecmp(tag->item, "album"))
+                       atom_name = "\xA9" "alb";
+               else if (!strcasecmp(tag->item, "date"))
+                       atom_name = "\xA9" "day";
+               else if (!strcasecmp(tag->item, "comment"))
+                       atom_name = "\xA9" "cmt";
+               else
+                       assert(false);
+               write_u32_be(out, TAG_LEN(len));
+               memcpy(out + 4, atom_name, 4);
+               write_u32_be(out + 8, 8 /* data atom header */
+                       + 8 /* flags + reserved */
+                       + len);
+               memcpy(out + 12, "data", 4);
+               write_u32_be(out + 16, 1); /* flags */
+               write_u32_be(out + 20, 0); /* reserved */
+               memcpy(out + 24, tag->value, len);
+               out += TAG_LEN(len);
+       }
+}
+
+static void *modify_moov(struct mp4 *f, uint32_t *out_size)
+{
+       int ret;
+       uint64_t total_base = f->moov_offset + 8;
+       uint32_t total_size = f->moov_size - 8;
+       uint32_t new_ilst_size = 0;
+       void *out_buffer;
+       uint8_t *p_out;
+       int32_t size_delta;
+       uint32_t tmp;
+
+       for (unsigned n = 0; n < f->meta.count; n++)
+               new_ilst_size += TAG_LEN(strlen(f->meta.tags[n].value));
+       size_delta = new_ilst_size - (f->ilst_size - 8);
+       *out_size = total_size + size_delta;
+       out_buffer = para_malloc(*out_size);
+       p_out = out_buffer;
+       set_position(f, total_base);
+       ret = read_data(f, p_out, f->udta_offset - total_base);
+       if (ret <= 0)
+               return NULL;
+       p_out += f->udta_offset - total_base;
+       ret = read_int32(f, &tmp);
+       if (ret <= 0)
+               return NULL;
+       write_u32_be(p_out, tmp + size_delta);
+       p_out += 4;
+       ret = read_data(f, p_out, 4);
+       if (ret <= 0)
+               return NULL;
+       p_out += 4;
+       ret = read_data(f, p_out, f->meta_offset - f->udta_offset - 8);
+       if (ret <= 0)
+               return NULL;
+       p_out += f->meta_offset - f->udta_offset - 8;
+       ret = read_int32(f, &tmp);
+       if (ret <= 0)
+               return NULL;
+       write_u32_be(p_out, tmp + size_delta);
+       p_out += 4;
+       ret = read_data(f, p_out, 4);
+       if (ret <= 0)
+               return NULL;
+       p_out += 4;
+       ret = read_data(f, p_out, f->ilst_offset - f->meta_offset - 8);
+       if (ret <= 0)
+               return NULL;
+       p_out += f->ilst_offset - f->meta_offset - 8;
+       ret = read_int32(f, &tmp);
+       if (ret <= 0)
+               return NULL;
+       write_u32_be(p_out, tmp + size_delta);
+       p_out += 4;
+       ret = read_data(f, p_out, 4);
+       if (ret <= 0)
+               return NULL;
+       p_out += 4;
+       create_ilst(&f->meta, p_out);
+       p_out += new_ilst_size;
+       set_position(f, f->ilst_offset + f->ilst_size);
+       ret = read_data(f, p_out, total_size - (f->ilst_offset - total_base)
+               - f->ilst_size);
+       if (ret <= 0)
+               return NULL;
+       return out_buffer;
+}
+
+static int write_data(struct mp4 *f, void *data, size_t size)
+{
+       while (size > 0) {
+               ssize_t ret = f->cb->write(f->cb->user_data, data, size);
+               if (ret < 0) {
+                       if (errno == EINTR)
+                               continue;
+                       return -ERRNO_TO_PARA_ERROR(errno);
+               }
+               size -= ret;
+       }
+       return 1;
+}
+
+/**
+ * Write back the modified metadata items to the mp4 file.
+ *
+ * This is the only public function which modifies the contents of an mp4 file.
+ * This is achieved by calling the ->write() and ->truncate() methods of the
+ * callback structure passed to \ref mp4_open() or \ref mp4_open_meta().
+ *
+ * \param f See \ref mp4_close().
+ *
+ * The modified metadata structure does not need to be supplied to this
+ * function because it is part of the mp4 structure.
+ *
+ * \return Standard.
+ */
+int mp4_update_meta(struct mp4 *f)
+{
+       void *new_moov_data;
+       uint32_t new_moov_size;
+       uint8_t buf[8] = "----moov";
+       int ret;
+
+       set_position(f, 0);
+       new_moov_data = modify_moov(f, &new_moov_size);
+       if (!new_moov_data ) {
+               mp4_close(f);
+               return 0;
+       }
+       if (f->last_atom != ATOM_MOOV) {
+               set_position(f, f->moov_offset + 4);
+               ret = write_data(f, "free", 4); /* rename old moov to free */
+               if (ret < 0)
+                       goto free_moov;
+               /* write new moov atom at EOF */
+               f->cb->seek(f->cb->user_data, 0, SEEK_END);
+       } else /* overwrite old moov atom */
+               set_position(f, f->moov_offset);
+       write_u32_be(buf, new_moov_size + 8);
+       ret = write_data(f, buf, sizeof(buf));
+       if (ret < 0)
+               goto free_moov;
+       ret = write_data(f, new_moov_data, new_moov_size);
+       if (ret < 0)
+               goto free_moov;
+       ret = f->cb->truncate(f->cb->user_data);
+       if (ret < 0)
+               ret = -ERRNO_TO_PARA_ERROR(errno);
+free_moov:
+       free(new_moov_data);
+       return ret;
+}
+
+/**
+ * Return the value of the given tag item.
+ *
+ * \param f See \ref mp4_close().
+ * \param item "artist", "title", "album", "comment", or "date".
+ *
+ * \return The function returns NULL if the given item is not in the above
+ * list. Otherwise, if the file does not contain a tag for the given item, the
+ * function also returns NULL. Otherwise a copy of the tag value is returned
+ * and the caller should free this memory when it is no longer needed.
+ */
+char *mp4_get_tag_value(const struct mp4 *f, const char *item)
+{
+       for (unsigned n = 0; n < f->meta.count; n++)
+               if (!strcasecmp(f->meta.tags[n].item, item))
+                       return para_strdup(f->meta.tags[n].value);
+       return NULL;
+}
diff --git a/mp4.h b/mp4.h
new file mode 100644 (file)
index 0000000..1618aa3
--- /dev/null
+++ b/mp4.h
@@ -0,0 +1,87 @@
+/** \file mp4.h Public API of the mp4 parser. */
+
+/**
+ * Callbacks provided by the user of the mp4 parsing API.
+ *
+ * A pointer to this structure is passed to the two public open functions. If
+ * the file is opened in read-only mode, the ->write() and ->truncate() methods
+ * won't be called and may thus be NULL. The ->read() and ->seek() methods
+ * must be supplied for either open type.
+ *
+ * All methods are supposed to work like their corresponding system calls.
+ * That is, they should return non-negative for success and -1 on failure. In
+ * the error case errno is expected to be set accordingly.
+ *
+ * \sa \ref mp4_open(), \ref mp4_open_meta().
+ */
+struct mp4_callback {
+       /** This pointer is propagated to each call of all methods. */
+       void *user_data;
+       /**
+        * This should return the number of bytes read on success. Short reads
+        * are OK: the function may return less than length.
+        */
+       ssize_t (*read)(void *user_data, void *buffer, size_t length);
+       /**
+        * This method is assumed to succeed. The implementation should simply
+        * abort on errors. Note that offsets beyond EOF must not be regarded
+        * as invalid arguments.
+        */
+       off_t (*seek)(void *user_data, off_t offset, int whence);
+       /**
+        * Like the write() system call, this should return the number of bytes
+        * written. Short writes are OK: the function may return less than
+        * count.
+        */
+       ssize_t (*write)(void *user_data, void *buffer, size_t count);
+       /**
+        * Unlike the truncate system call, this function does not receive an
+        * offset. The method is expected to truncate the file to the offset
+        * given by the current file position instead.
+        */
+       int (*truncate)(void *user_data);
+};
+
+/** Specifies one metadata tag. Both fields are 0-terminated strings. */
+struct mp4_tag {
+       /** The item name: "artist", "title", "album", "comment", or "date". */
+       char *item;
+       /** An arbitrary string value. */
+       char *value;
+};
+
+/**
+ * An array of name/value pairs.
+ *
+ * This structure is initialized when the mp4 file is opened in either mode.
+ * If the file contains metadata items other than the standard five, those
+ * non-standard items are not included in the array. After a successful open, a
+ * pointer to the metadata structure can be obtained via \ref mp4_get_meta().
+ */
+struct mp4_metadata {
+       /** It's OK to change this, for example by calling realloc(). */
+       struct mp4_tag *tags;
+       /** The number of entries of the array. */
+       unsigned count;
+};
+
+/**
+ * The mp4 file handle.
+ *
+ * A pointer to this opaque structure is returned by the two open functions.
+ * All other functions of the mp4 API receive a pointer of this type.
+ */
+struct mp4;
+
+int mp4_set_sample_position(struct mp4 *f, uint32_t sample);
+int mp4_open(const struct mp4_callback *cb, struct mp4 **result);
+void mp4_close(struct mp4 *f);
+int mp4_get_sample_size(const struct mp4 *f, uint32_t sample, uint32_t *result);
+uint16_t mp4_get_sample_rate(const struct mp4 *f);
+uint16_t mp4_get_channel_count(const struct mp4 *f);
+uint32_t mp4_num_samples(const struct mp4 *f);
+uint64_t mp4_get_duration(const struct mp4 *f);
+int mp4_open_meta(const struct mp4_callback *cb, struct mp4 **result);
+struct mp4_metadata *mp4_get_meta(struct mp4 *f);
+int mp4_update_meta(struct mp4 *f);
+char *mp4_get_tag_value(const struct mp4 *f, const char *item);