Merge branch 'refs/heads/t/taggers'
authorAndre Noll <maan@tuebingen.mpg.de>
Sun, 27 Sep 2015 12:35:05 +0000 (12:35 +0000)
committerAndre Noll <maan@tuebingen.mpg.de>
Sun, 27 Sep 2015 12:40:18 +0000 (12:40 +0000)
Cooking in next since 2015-04-26.

* refs/heads/t/taggers:
  aac: Fix compilation without libmp4v2.
  The mp4 tagger.
  The mp3 tagger.
  The flac tagger.
  The ogg/speex tagger.
  The ogg/vorbis tagger.
  The ogg/opus tagger.
  The wma tagger.

22 files changed:
Makefile.in
Makefile.real
NEWS
aac_afh.c
afh.c
afh.h
afh_common.c
configure.ac
error.h
fd.c
fd.h
flac_afh.c
m4/gengetopt/afh.m4
mp3_afh.c
ogg_afh.c
ogg_afh_common.c
ogg_afh_common.h
opus_afh.c
spx_afh.c
web/about.in.html
web/manual.m4
wma_afh.c

index bad4d87..a8e2a8b 100644 (file)
@@ -51,6 +51,7 @@ samplerate_cppflags := @samplerate_cppflags@
 readline_cppflags := @readline_cppflags@
 alsa_cppflags := @alsa_cppflags@
 oss_cppflags := @oss_cppflags@
+mp4v2_cppflags := @mp4v2_cppflags@
 
 clock_gettime_ldflags := @clock_gettime_ldflags@
 id3tag_ldflags := @id3tag_ldflags@
@@ -73,5 +74,7 @@ nsl_ldflags := @nsl_ldflags@
 curses_ldflags := @curses_ldflags@
 core_audio_ldflags := @core_audio_ldflags@
 crypto_ldflags := @crypto_ldflags@
+iconv_ldflags := @iconv_ldflags@
+mp4v2_ldflags := @mp4v2_ldflags@
 
 include Makefile.real
index 1cfcfa9..00175be 100644 (file)
@@ -197,6 +197,8 @@ $(object_dir)/flac%.o $(dep_dir)/flac%.d: CPPFLAGS += $(flac_cppflags)
 $(object_dir)/mp3_afh.o $(dep_dir)/mp3_afh.d: CPPFLAGS += $(id3tag_cppflags)
 $(object_dir)/crypt.o $(dep_dir)/crypt.d: CPPFLAGS += $(openssl_cppflags)
 $(object_dir)/gcrypt.o $(dep_dir)/gcrypt.d: CPPFLAGS += $(gcrypt_cppflags)
+$(object_dir)/ao_write.o $(dep_dir)/ao_write.d: CPPFLAGS += $(ao_cppflags)
+$(object_dir)/aac_afh.o $(dep_dir)/aac_afh.d: CPPFLAGS += $(mp4v2_cppflags)
 $(object_dir)/alsa%.o $(dep_dir)/alsa%.d: CPPFLAGS += $(alsa_cppflags)
 
 $(object_dir)/interactive.o $(dep_dir)/interactive.d \
@@ -287,6 +289,7 @@ para_filter \
 para_play \
 : LDFLAGS += \
        $(mad_ldflags) \
+       $(faad_ldflags) \
        $(samplerate_ldflags) \
        -lm
 
@@ -312,6 +315,13 @@ para_recv \
        $(faad_ldflags) \
        $(flac_ldflags)
 
+para_server \
+para_play \
+para_afh \
+para_recv \
+: LDFLAGS += \
+       $(mp4v2_ldflags)
+
 para_server \
 para_client \
 para_audioc \
@@ -320,6 +330,8 @@ para_recv \
 : LDFLAGS += \
        $(socket_ldflags) $(nsl_ldflags)
 
+para_afh para_recv para_server para_play: LDFLAGS += $(iconv_ldflags)
+
 $(foreach exe,$(executables),$(eval para_$(exe): $$($(exe)_objs)))
 $(prefixed_executables):
        @[ -z "$(Q)" ] || echo 'LD $@'
diff --git a/NEWS b/NEWS
index d98930c..f1940d3 100644 (file)
--- a/NEWS
+++ b/NEWS
@@ -1,6 +1,13 @@
 NEWS
 ====
 
+------------------------------------------
+current master branch "cascading gradient"
+------------------------------------------
+
+       - para_afh learned to modify meta tags of mp3 wma ogg spx
+         opus flac aac files.
+
 --------------------------------------
 0.5.5 (2015-09-20) "magnetic momentum"
 --------------------------------------
index 3458af9..5b2e9fb 100644 (file)
--- a/aac_afh.c
+++ b/aac_afh.c
 /** \file aac_afh.c para_server's aac audio format handler. */
 
 #include <regex.h>
+#include <mp4v2/mp4v2.h>
 
 #include "para.h"
 #include "error.h"
 #include "afh.h"
 #include "string.h"
 #include "aac.h"
+#include "fd.h"
 
 static int aac_find_stsz(unsigned char *buf, size_t buflen, off_t *skip)
 {
@@ -263,6 +265,68 @@ out:
        return ret;
 }
 
+static int aac_rewrite_tags(const char *map, size_t mapsize,
+               struct taginfo *tags, int fd, const char *filename)
+{
+       MP4FileHandle h;
+       const MP4Tags *mdata;
+       int ret = write_all(fd, map, mapsize);
+
+       if (ret < 0)
+               return ret;
+       lseek(fd, 0, SEEK_SET);
+       h = MP4Modify(filename, 0);
+       if (!h) {
+               PARA_ERROR_LOG("MP4Modify() failed, fd = %d\n", fd);
+               return -E_MP4V2;
+       }
+       mdata = MP4TagsAlloc();
+       assert(mdata);
+       if (!MP4TagsFetch(mdata, h)) {
+               PARA_ERROR_LOG("MP4Tags_Fetch() failed\n");
+               ret = -E_MP4V2;
+               goto close;
+       }
+
+       if (!MP4TagsSetAlbum(mdata, tags->album)) {
+               PARA_ERROR_LOG("Could not set album\n");
+               ret = -E_MP4V2;
+               goto tags_free;
+       }
+       if (!MP4TagsSetArtist(mdata, tags->artist)) {
+               PARA_ERROR_LOG("Could not set album\n");
+               ret = -E_MP4V2;
+               goto tags_free;
+       }
+       if (!MP4TagsSetComments(mdata, tags->comment)) {
+               PARA_ERROR_LOG("Could not set comment\n");
+               ret = -E_MP4V2;
+               goto tags_free;
+       }
+       if (!MP4TagsSetName(mdata, tags->title)) {
+               PARA_ERROR_LOG("Could not set title\n");
+               ret = -E_MP4V2;
+               goto tags_free;
+       }
+       if (!MP4TagsSetReleaseDate(mdata, tags->year)) {
+               PARA_ERROR_LOG("Could not set release date\n");
+               ret = -E_MP4V2;
+               goto tags_free;
+       }
+
+       if (!MP4TagsStore(mdata, h)) {
+               PARA_ERROR_LOG("Could not store tags\n");
+               ret = -E_MP4V2;
+               goto tags_free;
+       }
+       ret = 1;
+tags_free:
+       MP4TagsFree(mdata);
+close:
+       MP4Close(h, 0);
+       return ret;
+}
+
 static const char* aac_suffixes[] = {"m4a", "mp4", NULL};
 /**
  * the init function of the aac audio format handler
@@ -273,4 +337,5 @@ void aac_afh_init(struct audio_format_handler *afh)
 {
        afh->get_file_info = aac_get_file_info,
        afh->suffixes = aac_suffixes;
+       afh->rewrite_tags = aac_rewrite_tags;
 }
diff --git a/afh.c b/afh.c
index f3c25a2..195b378 100644 (file)
--- a/afh.c
+++ b/afh.c
@@ -23,6 +23,97 @@ INIT_AFH_ERRLISTS;
 static int loglevel;
 INIT_STDERR_LOGGING(loglevel)
 
+static inline bool tag_needs_update(bool given, const char *tag,
+               const char *arg)
+{
+       return given && (!tag || strcmp(tag, arg) != 0);
+}
+
+static int rewrite_tags(const char *name, int input_fd, void *map,
+               size_t map_size, int audio_format_id, struct afh_info *afhi)
+{
+       struct taginfo *tags = &afhi->tags;
+       bool modified = false;
+       char *tmp_name;
+       int output_fd = -1, ret;
+       struct stat sb;
+
+       if (tag_needs_update(conf.year_given, tags->year, conf.year_arg)) {
+               free(tags->year);
+               tags->year = para_strdup(conf.year_arg);
+               modified = true;
+       }
+       if (tag_needs_update(conf.title_given, tags->title, conf.title_arg)) {
+               free(tags->title);
+               tags->title = para_strdup(conf.title_arg);
+               modified = true;
+       }
+       if (tag_needs_update(conf.artist_given, tags->artist,
+                       conf.artist_arg)) {
+               free(tags->artist);
+               tags->artist = para_strdup(conf.artist_arg);
+               modified = true;
+       }
+       if (tag_needs_update(conf.album_given, tags->album, conf.album_arg)) {
+               free(tags->album);
+               tags->album = para_strdup(conf.album_arg);
+               modified = true;
+       }
+       if (tag_needs_update(conf.comment_given, tags->comment,
+                       conf.comment_arg)) {
+               free(tags->comment);
+               tags->comment = para_strdup(conf.comment_arg);
+               modified = true;
+       }
+       if (!modified) {
+               PARA_WARNING_LOG("no modifications necessary\n");
+               return 0;
+       }
+       /*
+        * mkstmp() creates the temporary file with permissions 0600, but we
+        * like it to have the same permissions as the original file, so we
+        * have to get this information.
+        */
+       if (fstat(input_fd, &sb) < 0) {
+               ret = -ERRNO_TO_PARA_ERROR(errno);
+               PARA_ERROR_LOG("failed to fstat fd %d (%s)\n", input_fd, name);
+               return ret;
+       }
+       tmp_name = make_message("%s.XXXXXX", name);
+       ret = mkstemp(tmp_name);
+       if (ret < 0) {
+               ret = -ERRNO_TO_PARA_ERROR(errno);
+               PARA_ERROR_LOG("could not create temporary file\n");
+               goto out;
+       }
+       output_fd = ret;
+       if (fchmod(output_fd, sb.st_mode) < 0) {
+               ret = -ERRNO_TO_PARA_ERROR(errno);
+               PARA_ERROR_LOG("failed to fchmod fd %d (%s)\n", output_fd,
+                       tmp_name);
+               goto out;
+       }
+       ret = afh_rewrite_tags(audio_format_id, map, map_size, tags, output_fd,
+               tmp_name);
+       if (ret < 0)
+               goto out;
+       if (conf.backup_given) {
+               char *backup_name = make_message("%s~", name);
+               ret = xrename(name, backup_name);
+               free(backup_name);
+               if (ret < 0)
+                       goto out;
+       }
+       ret = xrename(tmp_name, name);
+out:
+       if (ret < 0 && output_fd >= 0)
+               unlink(tmp_name); /* ignore errors */
+       free(tmp_name);
+       if (output_fd >= 0)
+               close(output_fd);
+       return ret;
+}
+
 static void print_info(int audio_format_num, struct afh_info *afhi)
 {
        char *msg;
@@ -104,11 +195,16 @@ int main(int argc, char **argv)
                        fd, &afhi);
                if (ret >= 0) {
                        audio_format_num = ret;
-                       printf("File %d: %s\n", i + 1, conf.inputs[i]);
-                       print_info(audio_format_num, &afhi);
-                       if (conf.chunk_table_given)
-                               print_chunk_table(&afhi);
-                       printf("\n");
+                       if (conf.modify_given) {
+                               ret = rewrite_tags(conf.inputs[i], fd, audio_file_data,
+                                       audio_file_size, audio_format_num, &afhi);
+                       } else {
+                               printf("File %d: %s\n", i + 1, conf.inputs[i]);
+                               print_info(audio_format_num, &afhi);
+                               if (conf.chunk_table_given)
+                                       print_chunk_table(&afhi);
+                               printf("\n");
+                       }
                        clear_afhi(&afhi);
                }
                close(fd);
diff --git a/afh.h b/afh.h
index 4830729..62e38c0 100644 (file)
--- a/afh.h
+++ b/afh.h
@@ -104,6 +104,14 @@ struct audio_format_handler {
                struct afh_info *afi);
        /** Optional, used for header-rewriting. See \ref afh_get_header(). */
        void (*get_header)(void *map, size_t mapsize, char **buf, size_t *len);
+       /**
+        * Write audio file with altered tags, optional.
+        *
+        * The output file descriptor has been opened by the caller and must not
+        * be closed in this function.
+        */
+       int (*rewrite_tags)(const char *map, size_t mapsize, struct taginfo *tags,
+               int output_fd, const char *filename);
 };
 
 void afh_init(void);
@@ -120,3 +128,5 @@ void afh_get_header(struct afh_info *afhi, uint8_t audio_format_id,
 void afh_free_header(char *header_buf, uint8_t audio_format_id);
 void clear_afhi(struct afh_info *afhi);
 unsigned afh_get_afhi_txt(int audio_format_num, struct afh_info *afhi, char **result);
+int afh_rewrite_tags(int audio_format_id, void *map, size_t mapsize,
+               struct taginfo *tags, int output_fd, const char *filename);
index eb0813d..b1f8b25 100644 (file)
@@ -48,7 +48,7 @@ static struct audio_format_handler afl[] = {
        },
        {
                .name = "aac",
-#ifdef HAVE_FAAD
+#if defined(HAVE_MP4V2)
                .init = aac_afh_init,
 #endif
        },
@@ -392,3 +392,31 @@ unsigned afh_get_afhi_txt(int audio_format_num, struct afh_info *afhi, char **re
                status_item_list[SI_COMMENT], afhi->tags.comment? afhi->tags.comment : ""
        );
 }
+
+/**
+ * Create a copy of the given file with altered meta tags.
+ *
+ * \param audio_format_id Specifies the audio format.
+ * \param map The (read-only) memory map of the input file.
+ * \param mapsize The size of the input file in bytes.
+ * \param tags The new tags.
+ * \param output_fd Altered file is created using this file descriptor.
+ * \param filename The name of the temporary output file.
+ *
+ * This calls the ->rewrite_tags method of the audio format handler associated
+ * with \a audio_format_id to create a copy of the memory-mapped file given
+ * by \a map and \a mapsize, but with altered tags according to \a tags. If
+ * the audio format handler for \a audio_format_id lacks this optional method,
+ * the function returns (the paraslash error code of) \p ENOTSUP.
+ *
+ * \return Standard.
+ */
+int afh_rewrite_tags(int audio_format_id, void *map, size_t mapsize,
+               struct taginfo *tags, int output_fd, const char *filename)
+{
+       struct audio_format_handler *afh = afl + audio_format_id;
+
+       if (!afh->rewrite_tags)
+               return -ERRNO_TO_PARA_ERROR(ENOTSUP);
+       return afh->rewrite_tags(map, mapsize, tags, output_fd, filename);
+}
index a30b0c2..2b2f296 100644 (file)
@@ -174,6 +174,30 @@ AC_CHECK_LIB([c], [socket],
        [socket_ldflags="-lsocket"]
 )
 AC_SUBST(socket_ldflags)
+########################################################################## iconv
+STASH_FLAGS
+LIBS=
+AC_SEARCH_LIBS([libiconv_open], [iconv],
+       [iconv_ldflags="$LIBS"],
+       []
+)
+AC_SUBST(iconv_ldflags)
+AC_MSG_CHECKING([whether iconv needs const char ** cast])
+AC_COMPILE_IFELSE([
+        AC_LANG_PROGRAM([
+                #include <iconv.h>
+        ],[
+                size_t iconv(iconv_t cd, const char **inbuf,
+                        size_t *inbytesleft, char **outbuf,
+                        size_t *outbytesleft);
+        ])
+],
+        [cast='(const char **)'; msg=yes],
+        [cast=; msg=no]
+)
+AC_DEFINE_UNQUOTED(ICONV_CAST, $cast, [cast for second arg to iconv()])
+AC_MSG_RESULT($msg)
+UNSTASH_FLAGS
 ########################################################################### libnsl
 AC_CHECK_LIB([c], [gethostbyname],
        [nsl_ldflags=],
@@ -248,6 +272,8 @@ LIB_ARG_WITH([ogg], [-logg])
 HAVE_OGG=yes
 AC_CHECK_HEADERS([ogg/ogg.h], [], [HAVE_OGG=no])
 AC_CHECK_LIB([ogg], [ogg_stream_init], [], [HAVE_OGG=no])
+AC_CHECK_LIB([ogg], [ogg_stream_flush_fill], [
+       AC_DEFINE(HAVE_OGG_STREAM_FLUSH_FILL, 1, [libogg >= 1.3.0])])
 LIB_SUBST_FLAGS(ogg)
 UNSTASH_FLAGS
 ######################################################################### vorbis
@@ -386,6 +412,14 @@ AC_CHECK_HEADER(samplerate.h, [], HAVE_SAMPLERATE=no)
 AC_CHECK_LIB([samplerate], [src_process], [], HAVE_SAMPLERATE=no)
 LIB_SUBST_FLAGS(samplerate)
 UNSTASH_FLAGS
+########################################################################## mp4v2
+STASH_FLAGS
+LIB_ARG_WITH([mp4v2], [-lmp4v2])
+HAVE_MP4V2=yes
+AC_CHECK_HEADER([mp4v2/mp4v2.h], [], [HAVE_MP4V2=no])
+AC_CHECK_LIB([mp4v2], [MP4Read], [], [HAVE_MP4V2=no])
+LIB_SUBST_FLAGS(mp4v2)
+UNSTASH_FLAGS
 ######################################################################### server
 if test -n "$CRYPTOLIB" && test $HAVE_OSL = yes; then
        build_server="yes"
@@ -440,7 +474,9 @@ if test -n "$CRYPTOLIB" && test $HAVE_OSL = yes; then
        NEED_SPEEX_OBJECTS() && server_errlist_objs="$server_errlist_objs spx_afh spx_common"
        NEED_OPUS_OBJECTS() && server_errlist_objs="$server_errlist_objs opus_afh opus_common"
        NEED_FLAC_OBJECTS && server_errlist_objs="$server_errlist_objs flac_afh"
-       test $HAVE_FAAD = yes && server_errlist_objs="$server_errlist_objs aac_afh aac_common"
+       if test $HAVE_FAAD = yes && test $HAVE_MP4V2 = yes; then
+               server_errlist_objs="$server_errlist_objs aac_afh aac_common"
+       fi
        server_objs="add_cmdline($server_cmdline_objs) $server_errlist_objs"
        AC_SUBST(server_objs, add_dot_o($server_objs))
        AC_DEFINE_UNQUOTED(INIT_SERVER_ERRLISTS,
@@ -797,7 +833,7 @@ NEED_SPEEX_OBJECTS && recv_errlist_objs="$recv_errlist_objs spx_afh spx_common"
 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
+if test $HAVE_FAAD = yes -a $HAVE_MP4V2 = yes; then
        recv_errlist_objs="$recv_errlist_objs aac_afh aac_common"
 fi
 recv_objs="add_cmdline($recv_cmdline_objs) $recv_errlist_objs"
@@ -837,8 +873,8 @@ NEED_FLAC_OBJECTS && {
        afh_errlist_objs="$afh_errlist_objs flac_afh"
        audio_format_handlers="$audio_format_handlers flac"
 }
-if test $HAVE_FAAD = yes; then
-       afh_errlist_objs="$afh_errlist_objs aac_common aac_afh"
+if test $HAVE_FAAD = yes -a $HAVE_MP4V2 = yes; then
+       afh_errlist_objs="$afh_errlist_objs aac_afh aac_common"
        audio_format_handlers="$audio_format_handlers aac"
 fi
 
@@ -914,7 +950,13 @@ 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 aacdec_filter aac_afh aac_common"
+       play_errlist_objs="$play_errlist_objs aacdec_filter"
+fi
+if test $HAVE_MP4V2 = yes; then
+       play_errlist_objs="$play_errlist_objs aac_afh"
+fi
+if test $HAVE_MP4V2 = yes || test $HAVE_FAAD = yes; then
+       play_errlist_objs="$play_errlist_objs aac_common"
 fi
 if test $HAVE_MAD = yes; then
        play_cmdline_objs="$play_cmdline_objs mp3dec_filter"
@@ -1107,9 +1149,12 @@ paraslash configuration:
 crypto lib: ${CRYPTOLIB:-[none]}
 unix socket credentials: $have_ucred
 readline (interactive CLIs): $HAVE_READLINE
-audio formats handlers: $audio_format_handlers
 id3 version 2 support: $HAVE_ID3TAG
-filters: $filters
+faad: $HAVE_FAAD
+mp4v2: $HAVE_MP4V2
+
+audio format handlers: $audio_format_handlers
+filters: $(echo $filters)
 writers: $writers
 
 para_fade: $build_fade
diff --git a/error.h b/error.h
index 57f581c..3818b11 100644 (file)
--- a/error.h
+++ b/error.h
@@ -98,6 +98,8 @@ extern const char **para_errlist[];
        PARA_ERROR(FLAC_SKIP_META, "could not skip metadata"), \
        PARA_ERROR(FLAC_DECODE_POS, "could not get decode position"), \
        PARA_ERROR(FLAC_STREAMINFO, "could not read stream info meta block"), \
+       PARA_ERROR(FLAC_REPLACE_COMMENT, "could not replace vorbis comment"), \
+       PARA_ERROR(FLAC_WRITE_CHAIN, "failed to write metadata chain"), \
 
 
 #define AFH_RECV_ERRORS \
@@ -105,9 +107,11 @@ extern const char **para_errlist[];
 
 
 #define OGG_AFH_COMMON_ERRORS \
-       PARA_ERROR(STREAM_PACKETOUT, "ogg stream packet-out error (first packet)"), \
+       PARA_ERROR(STREAM_PACKETOUT, "ogg stream packet-out error"), \
+       PARA_ERROR(STREAM_PACKETIN, "ogg stream packet-in error"), \
        PARA_ERROR(SYNC_PAGEOUT, "ogg sync page-out error (no ogg file?)"), \
-       PARA_ERROR(STREAM_PAGEIN, "ogg stream page-in error (first page)"), \
+       PARA_ERROR(STREAM_PAGEIN, "ogg stream page-in error"), \
+       PARA_ERROR(STREAM_PAGEOUT, "ogg stream page-out error"), \
        PARA_ERROR(OGG_SYNC, "internal ogg storage overflow"), \
        PARA_ERROR(OGG_EMPTY, "no ogg pages found"), \
 
@@ -403,13 +407,17 @@ extern const char **para_errlist[];
        PARA_ERROR(MP3_INFO, "could not read mp3 info"), \
        PARA_ERROR(HEADER_FREQ, "invalid header frequency"), \
        PARA_ERROR(HEADER_BITRATE, "invalid header bitrate"), \
-
+       PARA_ERROR(ID3_DETACH, "could not detach id3 frame"), \
+       PARA_ERROR(ID3_ATTACH, "could not atttach id3 frame"), \
+       PARA_ERROR(ID3_SETENCODING, "could not set id3 text encoding field"), \
+       PARA_ERROR(ID3_SETSTRING, "could not set id3 string field"), \
 
 #define AAC_AFH_ERRORS \
        PARA_ERROR(STSZ, "did not find stcz atom"), \
        PARA_ERROR(MP4ASC, "audio spec config error"), \
        PARA_ERROR(AAC_AFH_INIT, "failed to init aac decoder"), \
-
+       PARA_ERROR(MP4V2, "mp4v2 library error"), \
+       PARA_ERROR(NO_AUDIO_TRACK, "file contains no valid audio track"), \
 
 #define AAC_COMMON_ERRORS \
        PARA_ERROR(ESDS, "did not find esds atom"), \
@@ -418,6 +426,7 @@ extern const char **para_errlist[];
 
 #define OGG_AFH_ERRORS \
        PARA_ERROR(VORBIS, "vorbis synthesis header-in error (not vorbis?)"), \
+       PARA_ERROR(VORBIS_COMMENTHEADER, "could not create vorbis comment header"), \
        PARA_ERROR(OGG_PACKET_IN, "ogg_stream_packetin() failed"), \
        PARA_ERROR(OGG_STREAM_FLUSH, "ogg_stream_flush() failed"), \
 
@@ -495,7 +504,6 @@ extern const char **para_errlist[];
        PARA_ERROR(AACDEC_INIT, "failed to init aac decoder"), \
        PARA_ERROR(AAC_DECODE, "aac decode error"), \
 
-
 #define CHUNK_QUEUE_ERRORS \
        PARA_ERROR(QUEUE, "packet queue overrun"), \
 
diff --git a/fd.c b/fd.c
index ceff71f..6a26ce5 100644 (file)
--- a/fd.c
+++ b/fd.c
 #include "string.h"
 #include "fd.h"
 
+/**
+ * Change the name or location of a file.
+ *
+ * \param oldpath File to be moved.
+ * \param newpath Destination.
+ *
+ * This is just a simple wrapper for the rename(2) system call which returns a
+ * paraslash error code and prints an error message on failure.
+ *
+ * \return Standard.
+ *
+ * \sa rename(2).
+ */
+int xrename(const char *oldpath, const char *newpath)
+{
+       int ret = rename(oldpath, newpath);
+
+       if (ret >= 0)
+               return 1;
+       ret = -ERRNO_TO_PARA_ERROR(errno);
+       PARA_ERROR_LOG("failed to rename %s -> %s\n", oldpath, newpath);
+       return ret;
+}
+
 /**
  * Write an array of buffers to a file descriptor.
  *
diff --git a/fd.h b/fd.h
index 89de853..29f3879 100644 (file)
--- a/fd.h
+++ b/fd.h
@@ -6,6 +6,7 @@
 
 /** \file fd.h exported symbols from fd.c */
 
+int xrename(const char *oldpath, const char *newpath);
 int write_all(int fd, const char *buf, size_t len);
 __printf_2_3 int write_va_buffer(int fd, const char *fmt, ...);
 int file_exists(const char *);
index d72eb83..51c3c31 100644 (file)
 #include "error.h"
 #include "afh.h"
 #include "string.h"
+#include "fd.h"
 
 struct private_flac_afh_data {
-       char *map;
+       const char *map;
        size_t map_bytes;
        size_t fpos;
        struct afh_info *afhi;
@@ -88,6 +89,15 @@ static int meta_close_cb(FLAC__IOHandle __a_unused handle)
        return 0;
 }
 
+static const FLAC__IOCallbacks meta_callbacks = {
+       .read = meta_read_cb,
+       .write = NULL,
+       .seek = meta_seek_cb,
+       .tell = meta_tell_cb,
+       .eof = meta_eof_cb,
+       .close = meta_close_cb
+};
+
 static void free_tags(struct taginfo *tags)
 {
        freep(&tags->artist);
@@ -131,22 +141,95 @@ static void flac_read_vorbis_comments(FLAC__StreamMetadata_VorbisComment *vc,
        }
 }
 
-static int flac_read_meta(struct private_flac_afh_data *pfad)
+/*
+ * FLAC__metadata_object_vorbiscomment_replace_comment() is buggy in some
+ * libFLAC versions (see commit e95399c1 in the flac git repository). Hence we
+ * use delete and add as a workaround.
+ */
+static int flac_replace_vorbis_comment(FLAC__StreamMetadata *b,
+               const char *tag, const char* val)
 {
+       FLAC__bool ok;
+       FLAC__StreamMetadata_VorbisComment_Entry entry;
        int ret;
-       FLAC__IOCallbacks meta_callbacks = {
-               .read = meta_read_cb,
-               .write = NULL,
-               .seek = meta_seek_cb,
-               .tell = meta_tell_cb,
-               .eof = meta_eof_cb,
-               .close = meta_close_cb
-       };
+
+       PARA_INFO_LOG("replacing %s\n", tag);
+       ret = FLAC__metadata_object_vorbiscomment_remove_entries_matching(
+               b, tag);
+       if (ret < 0)
+               return -E_FLAC_REPLACE_COMMENT;
+       ok = FLAC__metadata_object_vorbiscomment_entry_from_name_value_pair(
+               &entry, tag, val? val : "");
+       if (!ok)
+               return -E_FLAC_REPLACE_COMMENT;
+       ok = FLAC__metadata_object_vorbiscomment_append_comment(b, entry,
+               false /* no copy */);
+       if (!ok) {
+               free(entry.entry);
+               return -E_FLAC_REPLACE_COMMENT;
+       }
+       return 1;
+}
+
+static int flac_replace_vorbis_comments(FLAC__Metadata_Chain *chain,
+               FLAC__StreamMetadata *b, struct taginfo *tags)
+{
+       FLAC__bool ok;
+       int ret;
+
+       ret = flac_replace_vorbis_comment(b, "artist", tags->artist);
+       if (ret < 0)
+               return ret;
+       ret = flac_replace_vorbis_comment(b, "title", tags->title);
+       if (ret < 0)
+               return ret;
+       ret = flac_replace_vorbis_comment(b, "album", tags->album);
+       if (ret < 0)
+               return ret;
+       ret = flac_replace_vorbis_comment(b, "year", tags->year);
+       if (ret < 0)
+               return ret;
+       ret = flac_replace_vorbis_comment(b, "comment", tags->comment);
+       if (ret < 0)
+               return ret;
+       /*
+        * Even if padding is disabled, libflac will try to modify the original
+        * file inplace if the metadata size has not changed. This won't work
+        * here though, because the original file is mapped read-only. Since
+        * there is no option to force the use of a temp file we work around
+        * this shortcoming by adding a dummy entry which increases the size of
+        * the meta data. If the entry already exists, we simply remove it.
+        */
+       ok = FLAC__metadata_chain_check_if_tempfile_needed(chain,
+               false /* no padding */);
+       if (!ok) {
+               PARA_INFO_LOG("adding/removing dummy comment\n");
+               ret = FLAC__metadata_object_vorbiscomment_remove_entries_matching(
+                       b, "comment2");
+               if (ret < 0)
+                       return -E_FLAC_REPLACE_COMMENT;
+               if (ret == 0) { /* nothing was removed */
+                       ret = flac_replace_vorbis_comment(b, "comment2",
+                               "avoid inplace write");
+                       if (ret < 0)
+                               return ret;
+               }
+               assert(FLAC__metadata_chain_check_if_tempfile_needed(chain,
+                       false /* no padding */));
+       }
+       return 1;
+}
+
+static int flac_init_meta(struct private_flac_afh_data *pfad,
+               FLAC__Metadata_Chain **chainp, FLAC__Metadata_Iterator **iterp)
+{
+       int ret;
+       FLAC__bool ok;
        FLAC__Metadata_Chain *chain;
        FLAC__Metadata_Iterator *iter;
-       FLAC__StreamMetadata_StreamInfo *info = NULL;
-       FLAC__bool ok;
 
+       *chainp = NULL;
+       *iterp = NULL;
        chain = FLAC__metadata_chain_new();
        if (!chain)
                return -E_FLAC_CHAIN_ALLOC;
@@ -160,6 +243,25 @@ static int flac_read_meta(struct private_flac_afh_data *pfad)
        if (!iter)
                goto free_chain;
        FLAC__metadata_iterator_init(iter, chain);
+       *iterp = iter;
+       *chainp = chain;
+       return 1;
+free_chain:
+       FLAC__metadata_chain_delete(chain);
+       return ret;
+}
+
+static int flac_read_meta(struct private_flac_afh_data *pfad)
+{
+       int ret;
+       FLAC__Metadata_Chain *chain;
+       FLAC__Metadata_Iterator *iter;
+       FLAC__StreamMetadata_StreamInfo *info = NULL;
+       FLAC__bool ok;
+
+       ret = flac_init_meta(pfad, &chain, &iter);
+       if (ret < 0)
+               return ret;
        for (;;) {
                FLAC__StreamMetadata *b;
                b = FLAC__metadata_iterator_get_block(iter);
@@ -184,7 +286,6 @@ static int flac_read_meta(struct private_flac_afh_data *pfad)
        ret = info? 0: -E_FLAC_STREAMINFO;
 free_iter:
        FLAC__metadata_iterator_delete(iter);
-free_chain:
        FLAC__metadata_chain_delete(chain);
        if (ret < 0)
                free_tags(&pfad->afhi->tags);
@@ -332,6 +433,88 @@ static int flac_get_file_info(char *map, size_t map_bytes, __a_unused int fd,
        return 1;
 }
 
+static size_t temp_write_cb(const void *ptr, size_t size, size_t nmemb,
+       FLAC__IOHandle handle)
+{
+       int ret, fd = *(int *)handle;
+       size_t n = size * nmemb; /* FIXME: possible overflow */
+
+       ret = write_all(fd, ptr, n);
+
+       /*
+        * libflac expects POSIX semantics: If an error occurs, or the end of
+        * the file is reached, the return value is a short item count or zero.
+        */
+       if (ret < 0) {
+               PARA_ERROR_LOG("%s\n", para_strerror(-ret));
+               return 0;
+       }
+       return nmemb;
+}
+
+/* only the write callback needs to be supplied for writing the temp file. */
+static const FLAC__IOCallbacks temp_callbacks = {
+       .write = temp_write_cb,
+};
+
+static int flac_write_chain(FLAC__Metadata_Chain *chain,
+               struct private_flac_afh_data *pfad, int fd)
+{
+       FLAC__bool ok;
+
+       ok = FLAC__metadata_chain_write_with_callbacks_and_tempfile(chain,
+               false /* no padding*/, pfad,
+               meta_callbacks, &fd, temp_callbacks);
+       if (!ok) {
+               FLAC__Metadata_ChainStatus st;
+               st = FLAC__metadata_chain_status(chain);
+               PARA_ERROR_LOG("chain status: %d\n", st);
+               if (st == FLAC__METADATA_CHAIN_STATUS_READ_ERROR)
+                       PARA_ERROR_LOG("read error\n");
+               return -E_FLAC_WRITE_CHAIN;
+       }
+       return 1;
+}
+
+static int flac_rewrite_tags(const char *map, size_t map_bytes,
+               struct taginfo *tags, int fd, __a_unused const char *filename)
+{
+       int ret;
+       FLAC__Metadata_Chain *chain;
+       FLAC__Metadata_Iterator *iter;
+       FLAC__StreamMetadata *b = NULL;
+       FLAC__bool ok;
+       struct private_flac_afh_data *pfad = para_calloc(sizeof(*pfad));
+
+       pfad->map = map;
+       pfad->map_bytes = map_bytes;
+       pfad->fpos = 0;
+
+       ret = flac_init_meta(pfad, &chain, &iter);
+       if (ret < 0)
+               goto free_pfad;
+       for (ok = true; ok; ok = FLAC__metadata_iterator_next(iter)) {
+               b = FLAC__metadata_iterator_get_block(iter);
+               assert(b);
+               if (b->type == FLAC__METADATA_TYPE_VORBIS_COMMENT)
+                       break;
+               b = NULL;
+       }
+       ret = -E_FLAC_REPLACE_COMMENT;
+       if (!b)
+               goto out;
+       ret = flac_replace_vorbis_comments(chain, b, tags);
+       if (ret < 0)
+               goto out;
+       ret = flac_write_chain(chain, pfad, fd);
+out:
+       FLAC__metadata_iterator_delete(iter);
+       FLAC__metadata_chain_delete(chain);
+free_pfad:
+       free(pfad);
+       return ret;
+}
+
 static const char* flac_suffixes[] = {"flac", NULL};
 
 /**
@@ -343,4 +526,5 @@ void flac_afh_init(struct audio_format_handler *afh)
 {
        afh->get_file_info = flac_get_file_info,
        afh->suffixes = flac_suffixes;
+       afh->rewrite_tags = flac_rewrite_tags;
 }
index 0df2fad..9b8a650 100644 (file)
@@ -6,6 +6,11 @@ include(header.m4)
 include(loglevel.m4)
 
 <qu>
+
+###################################
+section "printing meta information"
+###################################
+
 option "chunk-table" c
 #~~~~~~~~~~~~~~~~~~~~~
 "print also the chunk table"
@@ -34,4 +39,61 @@ details = "
        the duration and the size of each chunk. The parser-friendly
        output prints only the offsets, in one line.
 "
+
+#############################
+section "modifying meta tags"
+#############################
+
+option "modify" m
+#~~~~~~~~~~~~~~~~
+"modify (rather than print) tags"
+flag off
+details = "
+       When this option is given, para_afh creates the result file
+       as a temporary copy of the given file(s), but with meta
+       tags changed according to the options below. On errors,
+       the temporary file is removed, leaving the original file
+       unchanged. On success, if --backup is given, the original
+       file is moved away. Finally the temporary file is renamed to
+       the name of the original file.
+"
+
+option "backup" b
+"create backup of the original file"
+flag off
+details = "
+       The backup suffix is '~', i.e. a single tilde character is appended
+       to the given file name.
+"
+
+option "year" y
+#~~~~~~~~~~~~~~
+"set the year tag"
+string typestr="year"
+optional
+
+option "title" t
+#~~~~~~~~~~~~~~~
+"set the title tag"
+string typestr="title"
+optional
+
+option "artist" a
+#~~~~~~~~~~~~~~~~
+"set the artist/author tag"
+string typestr="artist"
+optional
+
+option "album" A
+#~~~~~~~~~~~~~~~
+"set the album tag"
+string typestr="album"
+optional
+
+option "comment" C
+#~~~~~~~~~~~~~~~~~
+"set the comment tag"
+string typestr="comment"
+optional
+
 </qu>
index ccd28da..484172a 100644 (file)
--- a/mp3_afh.c
+++ b/mp3_afh.c
@@ -22,6 +22,7 @@
 #include "error.h"
 #include "afh.h"
 #include "string.h"
+#include "fd.h"
 
 /*
  * MIN_CONSEC_GOOD_FRAMES defines how many consecutive valid MP3 frames we need
@@ -183,6 +184,201 @@ static int mp3_get_id3(unsigned char *map, size_t numbytes, __a_unused int fd,
        return ret;
 }
 
+/* These helpers are not mentioned in the libid3tag header file. */
+void id3_field_init(union id3_field *field, enum id3_field_type type);
+void id3_field_finish(union id3_field *field);
+
+/*
+ * Frames of type ID3_FRAME_COMMENT contain three fields of type language,
+ * string, and fullstring. The third field contains the actual comment.
+ */
+static int set_comment_fields(struct id3_frame *fr, id3_ucs4_t *ucs4_val)
+{
+       int ret;
+       union id3_field *field;
+
+       field = id3_frame_field(fr, 1);
+       id3_field_init(field, ID3_FIELD_TYPE_LANGUAGE);
+
+       field = id3_frame_field(fr, 2);
+       id3_field_init(field, ID3_FIELD_TYPE_STRING);
+
+       field = id3_frame_field(fr, 3);
+       id3_field_init(field, ID3_FIELD_TYPE_STRINGFULL);
+       ret = id3_field_setfullstring(field, ucs4_val);
+       if (ret < 0)
+               return -E_ID3_SETSTRING;
+       return 1;
+}
+
+/*
+ * UCS-4 stands for Universal Character Set where each character uses exactly
+ * 32 bits. It is also known as UTF-32, see ISO 10646.
+ *
+ * We have to use UCS-4 as an intermediate representation because the functions
+ * of libid3tag which set the tag content expect an array of UCS-4 characters.
+ * Fortunately, libid3tag contains conversion functions from and to UTF-8.
+ */
+static int replace_tag(char const *id, const char *val, struct id3_tag *id3_t,
+               int options)
+{
+       int ret;
+       struct id3_frame *fr;
+       union id3_field *field;
+       id3_ucs4_t *ucs4_val;
+
+       /* First, detach all frames that match the given id. */
+       while ((fr = id3_tag_findframe(id3_t, id, 0))) {
+               PARA_INFO_LOG("deleting %s frame\n", id);
+               ret = id3_tag_detachframe(id3_t, fr);
+               if (ret < 0)
+                       return -E_ID3_DETACH;
+               id3_frame_delete(fr);
+       }
+       if (!val || !*val)
+               return 0;
+       fr = id3_frame_new(id);
+       PARA_DEBUG_LOG("frame desc: %s, %d fields\n", fr->description, fr->nfields);
+
+       /* Frame 0 contains the encoding. We always use UTF-8. */
+       field = id3_frame_field(fr, 0);
+       id3_field_init(field, ID3_FIELD_TYPE_TEXTENCODING);
+       ret = id3_field_settextencoding(field, ID3_FIELD_TEXTENCODING_UTF_8);
+       if (ret < 0)
+               return -E_ID3_SETENCODING;
+       /* create UCS-4 representation */
+       ucs4_val = id3_utf8_ucs4duplicate((const id3_utf8_t *)val);
+
+       if (strcmp(id, ID3_FRAME_COMMENT) == 0)
+               ret = set_comment_fields(fr, ucs4_val);
+       else {
+               /* Non-comment frames contain the value in field 1. */
+               field = id3_frame_field(fr, 1);
+               if (options & ID3_TAG_OPTION_ID3V1) {
+                       id3_ucs4_t *ptr[] = {ucs4_val, NULL};
+                       id3_field_init(field, ID3_FIELD_TYPE_STRINGLIST);
+                       ret = id3_field_setstrings(field, 1, ptr);
+               } else {
+                       id3_field_init(field, ID3_FIELD_TYPE_STRINGFULL);
+                       ret = id3_field_setfullstring(field, ucs4_val);
+               }
+       }
+       free(ucs4_val);
+       if (ret < 0)
+               return -E_ID3_SETSTRING;
+
+       PARA_INFO_LOG("attaching %s frame, value: %s\n", id, val);
+       ret = id3_tag_attachframe(id3_t, fr);
+       if (ret < 0)
+               return -E_ID3_ATTACH;
+       return 1;
+}
+
+static int replace_tags(struct id3_tag *id3_t, struct taginfo *tags)
+{
+       int ret, options = id3_tag_options(id3_t, 0, 0);
+
+       ret = replace_tag(ID3_FRAME_ARTIST, tags->artist, id3_t, options);
+       if (ret < 0)
+               return ret;
+       ret = replace_tag(ID3_FRAME_TITLE, tags->title, id3_t, options);
+       if (ret < 0)
+               return ret;
+       ret = replace_tag(ID3_FRAME_ALBUM, tags->album, id3_t, options);
+       if (ret < 0)
+               return ret;
+       ret = replace_tag(ID3_FRAME_YEAR, tags->year, id3_t, options);
+       if (ret < 0)
+               return ret;
+       ret = replace_tag(ID3_FRAME_COMMENT, tags->comment, id3_t, options);
+       if (ret < 0)
+               return ret;
+       return 1;
+}
+
+/* id3_tag_delete() does not seem to work due to refcount issues. */
+static void free_tag(struct id3_tag *id3_t)
+{
+       int i, j;
+       for (i = 0; i < id3_t->nframes; i++) {
+               struct id3_frame *fr = id3_t->frames[i];
+               for (j = 0; j < fr->nfields; j++) {
+                       union id3_field *field = &fr->fields[j];
+                       id3_field_finish(field);
+               }
+               free(fr);
+       }
+       free(id3_t->frames);
+       free(id3_t);
+}
+
+static int mp3_rewrite_tags(const char *map, size_t mapsize,
+               struct taginfo *tags, int fd, __a_unused const char *filename)
+{
+       int ret;
+       id3_length_t old_v2size, new_v2size;
+       id3_byte_t v1_buffer[128], *v2_buffer;
+       size_t data_sz;
+       struct id3_tag *v1_tag = NULL, *v2_tag = NULL;
+
+       if (mapsize >= 128) {
+               v1_tag = id3_tag_parse((const id3_byte_t *)map + mapsize - 128,
+                       128);
+               if (v1_tag) {
+                       PARA_NOTICE_LOG("replacing id3v1 tag\n");
+                       ret = replace_tags(v1_tag, tags);
+                       if (ret < 0)
+                               goto out;
+                       id3_tag_render(v1_tag, v1_buffer);
+               }
+       }
+       old_v2size = 0;
+       v2_tag = id3_tag_parse((const id3_byte_t *)map, mapsize);
+       if (v2_tag) {
+               PARA_NOTICE_LOG("replacing id3v2 tag\n");
+               old_v2size = v2_tag->paddedsize;
+       } else if (!v1_tag) {
+               PARA_NOTICE_LOG("no id3 tags found, adding id3v2 tag\n");
+               v2_tag = id3_tag_new();
+               assert(v2_tag);
+       }
+       if (v2_tag) {
+               /*
+                * Turn off all options to avoid creating an extended header.
+                * id321 does not understand it.
+                */
+               id3_tag_options(v2_tag, ~0U, 0);
+               ret = replace_tags(v2_tag, tags);
+               if (ret < 0)
+                       goto out;
+               new_v2size = id3_tag_render(v2_tag, NULL);
+               v2_buffer = para_malloc(new_v2size);
+               id3_tag_render(v2_tag, v2_buffer);
+               PARA_INFO_LOG("writing v2 tag (%lu bytes)\n", new_v2size);
+               ret = write_all(fd, (char *)v2_buffer, new_v2size);
+               free(v2_buffer);
+               if (ret < 0)
+                       goto out;
+       }
+       data_sz = mapsize - old_v2size;
+       if (v1_tag && data_sz >= 128)
+               data_sz -= 128;
+       PARA_INFO_LOG("writing data part (%zu bytes)\n", data_sz);
+       ret = write_all(fd, map + old_v2size, data_sz);
+       if (ret < 0)
+               goto out;
+       if (v1_tag) {
+               PARA_INFO_LOG("writing v1 tag\n");
+               ret = write_all(fd, (char *)v1_buffer, 128);
+       }
+out:
+       if (v1_tag)
+               free_tag(v1_tag);
+       if (v2_tag)
+               free_tag(v2_tag);
+       return ret;
+}
+
 #else /* HAVE_ID3TAG */
 
 /*
@@ -499,4 +695,7 @@ void mp3_init(struct audio_format_handler *afh)
 {
        afh->get_file_info = mp3_get_file_info;
        afh->suffixes = mp3_suffixes;
+#ifdef HAVE_LIBID3TAG
+       afh->rewrite_tags = mp3_rewrite_tags;
+#endif /* HAVE_LIBID3TAG */
 }
index 9dfb028..32f8bc1 100644 (file)
--- a/ogg_afh.c
+++ b/ogg_afh.c
@@ -173,6 +173,39 @@ fail:
 
 static const char* ogg_suffixes[] = {"ogg", NULL};
 
+static int vorbis_make_meta_packet(struct taginfo *tags, ogg_packet *result)
+{
+       vorbis_comment vc;
+       int ret;
+
+       vorbis_comment_init(&vc);
+       vorbis_comment_add_tag(&vc, "artist", tags->artist);
+       vorbis_comment_add_tag(&vc, "title", tags->title);
+       vorbis_comment_add_tag(&vc, "album", tags->album);
+       vorbis_comment_add_tag(&vc, "year", tags->year);
+       vorbis_comment_add_tag(&vc, "comment", tags->comment);
+       ret = vorbis_commentheader_out(&vc, result);
+       vorbis_comment_clear(&vc);
+       if (ret != 0)
+               return -E_VORBIS_COMMENTHEADER;
+       return 1;
+}
+
+static int vorbis_rewrite_tags(const char *map, size_t mapsize,
+               struct taginfo *tags, int output_fd,
+               __a_unused const char *filename)
+{
+       int ret;
+       ogg_packet packet;
+
+       ret = vorbis_make_meta_packet(tags, &packet);
+       if (ret < 0)
+               return ret;
+       ret = ogg_rewrite_tags(map, mapsize, output_fd, (char *)packet.packet,
+               packet.bytes);
+       free(packet.packet);
+       return ret;
+}
 /**
  * The init function of the ogg vorbis audio format handler.
  *
@@ -183,4 +216,5 @@ void ogg_init(struct audio_format_handler *afh)
        afh->get_file_info = ogg_vorbis_get_file_info;
        afh->get_header = vorbis_get_header;
        afh->suffixes = ogg_suffixes;
+       afh->rewrite_tags = vorbis_rewrite_tags;
 }
index 61ded4d..b8b0006 100644 (file)
@@ -14,7 +14,7 @@
 #include "error.h"
 #include "string.h"
 #include "ogg_afh_common.h"
-
+#include "fd.h"
 
 /* Taken from decoder_example.c of libvorbis-1.2.3. */
 static int process_packets_2_and_3(ogg_sync_state *oss,
@@ -188,3 +188,147 @@ out:
        ogg_sync_clear(&oss);
        return ret;
 }
+
+static int write_ogg_page(int fd, const ogg_page *op)
+{
+       int ret;
+
+       PARA_DEBUG_LOG("header/body: %lu/%lu\n", op->header_len, op->body_len);
+       ret = xwrite(fd, (const char *)op->header, op->header_len);
+       if (ret < 0)
+               return ret;
+       return xwrite(fd, (const char *)op->body, op->body_len);
+}
+
+/**
+ * Change meta tags of ogg files.
+ *
+ * \param map The (read-only) memory map of the input file.
+ * \param map_sz The size of the input file in bytes.
+ * \param fd The output file descriptor.
+ * \param meta_packet Codec-specific packet containing modified tags.
+ * \param meta_sz Size of the metadata packet.
+ *
+ * This function writes a new ogg file content using file descriptor \a fd,
+ * which must correspond to a file which has been opened for writing.  The
+ * second packet is supposed to contain the metadata, and is replaced by \a
+ * meta_packet. This output file has to be closed by the caller.
+ *
+ * \return Standard.
+ */
+int ogg_rewrite_tags(const char *map, size_t map_sz, int fd,
+               char *meta_packet, size_t meta_sz)
+{
+       ogg_sync_state oss_in, oss_out;
+       ogg_stream_state stream_in, stream_out, *si = NULL, *so = NULL;
+       ogg_packet packet;
+       ogg_page op;
+       char *buf;
+       int serial, ret;
+       long len = map_sz;
+
+       ogg_sync_init(&oss_in);
+       ogg_sync_init(&oss_out);
+
+       ret = -E_OGG_SYNC;
+       buf = ogg_sync_buffer(&oss_in, len);
+       if (!buf)
+               goto out;
+       memcpy(buf, map, len);
+       ret = -E_OGG_SYNC;
+       if (ogg_sync_wrote(&oss_in, len) < 0)
+               goto out;
+       if (ogg_sync_pageout(&oss_in, &op) != 1)
+               goto out;
+       ret = ogg_page_serialno(&op);
+       serial = ret;
+
+       si = &stream_in;
+       ogg_stream_init(si, serial);
+       /* Packet #0 goes to an own page */
+       ret = -E_STREAM_PAGEIN;
+       if (ogg_stream_pagein(si, &op) < 0)
+               goto out;
+       ret = -E_STREAM_PACKETOUT;
+       if (ogg_stream_packetout(si, &packet) != 1)
+               goto out;
+       ret = -E_STREAM_PACKETIN;
+       so = &stream_out;
+       ogg_stream_init(so, serial);
+       if (ogg_stream_packetin(so, &packet) != 0)
+               goto out;
+       ret = ogg_stream_flush(so, &op);
+       assert(ret != 0);
+       /* packets have been flushed into the page. */
+       ret = write_ogg_page(fd, &op);
+       if (ret < 0)
+               goto out;
+       /*
+        * For all supported ogg/xxx audio formats the meta data packet is
+        * packet #1. Write out our modified version of this packet.
+        */
+       packet.packetno = 1;
+       packet.b_o_s = packet.e_o_s = 0;
+       packet.packet = (typeof(packet.packet))meta_packet;
+       packet.bytes = meta_sz;
+       ret = -E_STREAM_PACKETIN;
+       if (ogg_stream_packetin(so, &packet) != 0)
+               goto out;
+       /* Copy ogg packets, ignoring the meta data packet. */
+       for (;;) {
+               ret = ogg_stream_packetout(si, &packet);
+               if (ret == -1)
+                       break;
+               if (ret != 1) {
+                       ret = -E_STREAM_PAGEOUT;
+                       if (ogg_sync_pageout(&oss_in, &op) < 0)
+                               goto out;
+                       ret = -E_STREAM_PAGEIN;
+                       if (ogg_stream_pagein(si, &op))
+                               goto out;
+                       continue;
+               }
+               PARA_DEBUG_LOG("packet: bytes: %d, granule: %d, packetno: %u\n",
+                       (int)packet.bytes, (int)packet.granulepos,
+                       (int)packet.packetno);
+               /* ignore meta data packet which we replaced */
+               if (packet.packetno == 1)
+                       continue;
+               ret = -E_STREAM_PACKETIN;
+               if (ogg_stream_packetin(so, &packet) != 0)
+                       goto out;
+               /* only create a new ogg page if granulepos is valid */
+               if (packet.granulepos == -1)
+                       continue;
+               /* force remaining packets into a page */
+               for (;;) {
+#ifdef HAVE_OGG_STREAM_FLUSH_FILL
+                       ret = ogg_stream_flush_fill(so, &op, INT_MAX);
+#else
+                       ret = ogg_stream_flush(so, &op);
+#endif
+                       if (ret <= 0)
+                               break;
+                       PARA_DEBUG_LOG("writing page (%lu bytes)\n",
+                               op.header_len + op.body_len);
+                       ret = write_ogg_page(fd, &op);
+                       if (ret < 0)
+                               goto out;
+               }
+       }
+       if (ogg_stream_flush(so, &op)) {
+               /* write remaining data */
+               ret = write_ogg_page(fd, &op);
+               if (ret < 0)
+                       goto out;
+       }
+       ret = 1;
+out:
+       ogg_sync_clear(&oss_in);
+       ogg_sync_clear(&oss_out);
+       if (si)
+               ogg_stream_clear(si);
+       if (so)
+               ogg_stream_clear(so);
+       return ret;
+}
index 47e133b..7b9d131 100644 (file)
@@ -35,3 +35,5 @@ struct ogg_afh_callback_info {
 
 int ogg_get_file_info(char *map, size_t numbytes, struct afh_info *afhi,
                struct ogg_afh_callback_info *ci);
+int ogg_rewrite_tags(const char *map, size_t mapsize, int fd,
+               char *meta_packet, size_t meta_sz);
index 62d9e08..ace8300 100644 (file)
@@ -19,6 +19,8 @@
 
 static const char* opus_suffixes[] = {"opus", NULL};
 
+#define OPUS_COMMENT_HEADER "OpusTags"
+
 static bool copy_if_tag_type(const char *tag, int taglen, const char *type,
                char **p)
 {
@@ -40,7 +42,7 @@ static int opus_get_comments(char *comments, int length,
        /* min size of a opus header is 16 bytes */
        if (length < 16)
                return -E_OPUS_COMMENT;
-       if (memcmp(p, "OpusTags", 8) != 0)
+       if (memcmp(p, OPUS_COMMENT_HEADER, strlen(OPUS_COMMENT_HEADER)) != 0)
                return -E_OPUS_COMMENT;
        p += 8;
        val = read_u32(p);
@@ -133,6 +135,97 @@ static int opus_get_file_info(char *map, size_t numbytes, __a_unused int fd,
        return 1;
 }
 
+static size_t opus_make_meta_packet(struct taginfo *tags, char **result)
+{
+       size_t sz;
+       char *buf, *p;
+       size_t comment_len = strlen(tags->comment),
+               artist_len = strlen(tags->artist),
+               title_len = strlen(tags->title),
+               album_len = strlen(tags->album),
+               year_len = strlen(tags->year);
+       uint32_t comment_sz = comment_len,
+               artist_sz = artist_len + strlen("artist="),
+               title_sz = title_len + strlen("title="),
+               album_sz = album_len + strlen("album="),
+               year_sz = year_len + strlen("year=");
+       uint32_t num_tags;
+
+       sz = strlen(OPUS_COMMENT_HEADER)
+               + 4 /* comment length (always present) */
+               + comment_sz
+               + 4; /* number of tags */
+       num_tags = 0;
+       if (artist_len) {
+               num_tags++;
+               sz += 4 + artist_sz;
+       }
+       if (title_len) {
+               num_tags++;
+               sz += 4 + title_sz;
+       }
+       if (album_len) {
+               num_tags++;
+               sz += 4 + album_sz;
+       }
+       if (year_len) {
+               num_tags++;
+               sz += 4 + year_sz;
+       }
+       PARA_DEBUG_LOG("meta packet size: %zu bytes\n", sz);
+       /* terminating zero byte for the last sprintf() */
+       buf = p = para_malloc(sz + 1);
+       memcpy(p, OPUS_COMMENT_HEADER, strlen(OPUS_COMMENT_HEADER));
+       p += strlen(OPUS_COMMENT_HEADER);
+       write_u32(p, comment_sz);
+       p += 4;
+       strcpy(p, tags->comment);
+       p += comment_sz;
+       write_u32(p, num_tags);
+       p += 4;
+       if (artist_len) {
+               write_u32(p, artist_sz);
+               p += 4;
+               sprintf(p, "artist=%s", tags->artist);
+               p += artist_sz;
+       }
+       if (title_len) {
+               write_u32(p, title_sz);
+               p += 4;
+               sprintf(p, "title=%s", tags->title);
+               p += title_sz;
+       }
+       if (album_len) {
+               write_u32(p, album_sz);
+               p += 4;
+               sprintf(p, "album=%s", tags->album);
+               p += album_sz;
+       }
+       if (year_len) {
+               write_u32(p, year_sz);
+               p += 4;
+               sprintf(p, "year=%s", tags->year);
+               p += year_sz;
+       }
+       assert(p == buf + sz);
+       *result = buf;
+       return sz;
+}
+
+static int opus_rewrite_tags(const char *map, size_t mapsize,
+               struct taginfo *tags, int output_fd,
+               __a_unused const char *filename)
+{
+       char *meta_packet;
+       size_t meta_sz;
+       int ret;
+
+       meta_sz = opus_make_meta_packet(tags, &meta_packet);
+       ret = ogg_rewrite_tags(map, mapsize, output_fd, meta_packet, meta_sz);
+       free(meta_packet);
+       return ret;
+}
+
 /**
  * The init function of the ogg/opus audio format handler.
  *
@@ -142,4 +235,5 @@ void opus_afh_init(struct audio_format_handler *afh)
 {
        afh->get_file_info = opus_get_file_info,
        afh->suffixes = opus_suffixes;
+       afh->rewrite_tags = opus_rewrite_tags;
 }
index e835367..108cfaa 100644 (file)
--- a/spx_afh.c
+++ b/spx_afh.c
@@ -163,6 +163,94 @@ static int spx_get_file_info(char *map, size_t numbytes, __a_unused int fd,
        return ogg_get_file_info(map, numbytes, afhi, &spx_callback_info);
 }
 
+static size_t spx_make_meta_packet(struct taginfo *tags, char **result)
+{
+       size_t sz;
+       char *buf, *p;
+       size_t comment_len = strlen(tags->comment),
+               artist_len = strlen(tags->artist),
+               title_len = strlen(tags->title),
+               album_len = strlen(tags->album),
+               year_len = strlen(tags->year);
+       uint32_t comment_sz = comment_len,
+               artist_sz = artist_len + strlen("artist="),
+               title_sz = title_len + strlen("title="),
+               album_sz = album_len + strlen("album="),
+               year_sz = year_len + strlen("year=");
+       uint32_t num_tags;
+
+       sz = 4 /* comment length (always present) */
+               + comment_sz
+               + 4; /* number of tags */
+       num_tags = 0;
+       if (artist_len) {
+               num_tags++;
+               sz += 4 + artist_sz;
+       }
+       if (title_len) {
+               num_tags++;
+               sz += 4 + title_sz;
+       }
+       if (album_len) {
+               num_tags++;
+               sz += 4 + album_sz;
+       }
+       if (year_len) {
+               num_tags++;
+               sz += 4 + year_sz;
+       }
+       PARA_DEBUG_LOG("meta packet size: %zu bytes\n", sz);
+       /* terminating zero byte for the last sprintf() */
+       buf = p = para_malloc(sz + 1);
+       write_u32(p, comment_sz);
+       p += 4;
+       strcpy(p, tags->comment);
+       p += comment_sz;
+       write_u32(p, num_tags);
+       p += 4;
+       if (artist_len) {
+               write_u32(p, artist_sz);
+               p += 4;
+               sprintf(p, "artist=%s", tags->artist);
+               p += artist_sz;
+       }
+       if (title_len) {
+               write_u32(p, title_sz);
+               p += 4;
+               sprintf(p, "title=%s", tags->title);
+               p += title_sz;
+       }
+       if (album_len) {
+               write_u32(p, album_sz);
+               p += 4;
+               sprintf(p, "album=%s", tags->album);
+               p += album_sz;
+       }
+       if (year_len) {
+               write_u32(p, year_sz);
+               p += 4;
+               sprintf(p, "year=%s", tags->year);
+               p += year_sz;
+       }
+       assert(p == buf + sz);
+       *result = buf;
+       return sz;
+}
+
+static int spx_rewrite_tags(const char *map, size_t mapsize,
+               struct taginfo *tags, int output_fd,
+               __a_unused const char *filename)
+{
+       char *meta_packet;
+       size_t meta_sz;
+       int ret;
+
+       meta_sz = spx_make_meta_packet(tags, &meta_packet);
+       ret = ogg_rewrite_tags(map, mapsize, output_fd, meta_packet, meta_sz);
+       free(meta_packet);
+       return ret;
+}
+
 /**
  * The init function of the ogg/speex audio format handler.
  *
@@ -172,4 +260,5 @@ void spx_afh_init(struct audio_format_handler *afh)
 {
        afh->get_file_info = spx_get_file_info,
        afh->suffixes = speex_suffixes;
+       afh->rewrite_tags = spx_rewrite_tags;
 }
index 5936f93..2c72f28 100644 (file)
@@ -8,7 +8,7 @@ systems. It is written in C and released under the GPLv2.
        <li> Runs on Linux, Mac OS, FreeBSD, NetBSD </li>
        <li> Mp3, ogg/vorbis, ogg/speex, aac (m4a), wma, flac and ogg/opus support </li>
        <li> http, dccp and udp network streaming </li>
-       <li> Stand-alone decoder/player </li>
+       <li> Stand-alone decoder, player, tagger </li>
        <li> Curses-based gui (<a href="gui.png">screenshot</a>) </li>
        <li> Integrated volume normalizer, fader, alarm clock </li>
        <li> Sophisticated audio file selector </li>
index 8c00c99..0963306 100644 (file)
@@ -259,7 +259,8 @@ Optional:
        - XREFERENCE(http://www.underbit.com/products/mad/,
        libid3tag). For version-2 ID3 tag support, you'll need
        the libid3tag development package libid3tag0-dev. Without
-       libid3tag, only version one tags are recognized.
+       libid3tag, only version-1 tags are recognized. The mp3 tagger
+       also needs this library for modifying (id3v1 and id3v2) tags.
 
        - XREFERENCE(http://www.xiph.org/downloads/, ogg vorbis).
        For ogg vorbis streams you'll need libogg, libvorbis,
index f1edacf..0b6081c 100644 (file)
--- a/wma_afh.c
+++ b/wma_afh.c
@@ -8,6 +8,7 @@
 
 #include <sys/types.h>
 #include <regex.h>
+#include <iconv.h>
 
 #include "para.h"
 #include "error.h"
@@ -15,6 +16,7 @@
 #include "portable_io.h"
 #include "string.h"
 #include "wma.h"
+#include "fd.h"
 
 #define FOR_EACH_FRAME(_f, _buf, _size, _ba) for (_f = (_buf); \
        _f + (_ba) + WMA_FRAME_SKIP < (_buf) + (_size); \
@@ -265,6 +267,381 @@ static int wma_get_file_info(char *map, size_t numbytes, __a_unused int fd,
        return 0;
 }
 
+struct asf_object {
+       char *ptr;
+       uint64_t size;
+};
+
+struct tag_object_nums {
+       int content_descr_obj_num;
+       int extended_content_descr_obj_num;
+};
+
+struct afs_top_level_header_object {
+       uint64_t size;
+       uint32_t num_objects;
+       uint8_t reserved1, reserved2;
+       struct asf_object *objects;
+};
+
+#define CHECK_HEADER(_p, _h) (memcmp((_p), (_h), sizeof((_h))) == 0)
+
+static int read_asf_objects(const char *src, size_t size, uint32_t num_objects,
+               struct asf_object *objs, struct tag_object_nums *ton)
+{
+       int i;
+       const char *p;
+
+       for (i = 0, p = src; i < num_objects; p += objs[i++].size) {
+               if (p + 24 > src + size)
+                       return -E_NO_WMA;
+               objs[i].ptr = (char *)p;
+               objs[i].size = read_u64(p + 16);
+               if (p + objs[i].size > src + size)
+                       return -E_NO_WMA;
+
+               if (CHECK_HEADER(p, content_description_header))
+                       ton->content_descr_obj_num = i;
+               else if (CHECK_HEADER(p, extended_content_header))
+                       ton->extended_content_descr_obj_num = i;
+       }
+       return 1;
+}
+
+static const char top_level_header_object_guid[] = {
+       0x30, 0x26, 0xb2, 0x75, 0x8e, 0x66, 0xcf, 0x11,
+       0xa6, 0xd9, 0x00, 0xaa, 0x00, 0x62, 0xce, 0x6c
+};
+
+static int convert_utf8_to_utf16(char *src, char **dst)
+{
+       /*
+        * Without specifying LE (little endian), iconv includes a byte order
+        * mark (e.g. 0xFFFE) at the beginning.
+        */
+       iconv_t cd = iconv_open("UTF-16LE", "UTF-8");
+       size_t sz, inbytes, outbytes, inbytesleft, outbytesleft;
+       char *inbuf, *outbuf;
+       int ret;
+
+       if (!src || !*src) {
+               *dst = para_calloc(2);
+               ret = 0;
+               goto out;
+       }
+       if (cd == (iconv_t) -1)
+               return -ERRNO_TO_PARA_ERROR(errno);
+       inbuf = src;
+       /* even though src is in UTF-8, strlen() should DTRT */
+       inbytes = inbytesleft = strlen(src);
+       outbytes = outbytesleft = 4 * inbytes + 2; /* hope that's enough */
+       *dst = outbuf = para_malloc(outbytes);
+       sz = iconv(cd, ICONV_CAST &inbuf, &inbytesleft, &outbuf, &outbytesleft);
+       if (sz == (size_t)-1) {
+               ret = -ERRNO_TO_PARA_ERROR(errno);
+               goto out;
+       }
+       assert(outbytes >= outbytesleft);
+       assert(outbytes - outbytesleft < INT_MAX - 2);
+       ret = outbytes - outbytesleft;
+       outbuf = para_realloc(*dst, ret + 2);
+       outbuf[ret] = outbuf[ret + 1] = '\0';
+       ret += 2;
+       *dst = outbuf;
+       PARA_INFO_LOG("converted %s to %d UTF-16 bytes\n", src, ret);
+out:
+       if (ret < 0)
+               free(*dst);
+       if (iconv_close(cd) < 0)
+               PARA_WARNING_LOG("iconv_close: %s\n", strerror(errno));
+       return ret;
+}
+
+/* The content description object contains artist, title, comment. */
+static int make_cdo(struct taginfo *tags, const struct asf_object *cdo,
+               struct asf_object *result)
+{
+       const char *cr, *rating; /* orig data */
+       uint16_t orig_title_bytes, orig_artist_bytes, orig_cr_bytes,
+               orig_comment_bytes, orig_rating_bytes;
+       /* pointers to new UTF-16 tags */
+       char *artist = NULL, *title = NULL, *comment = NULL;
+       /* number of bytes in UTF-16 for the new tags */
+       int artist_bytes, title_bytes, comment_bytes, ret;
+       char *p, null[2] = "\0\0";
+
+       result->ptr = NULL;
+       result->size = 0;
+       ret = convert_utf8_to_utf16(tags->artist, &artist);
+       if (ret < 0)
+               return ret;
+       artist_bytes = ret;
+       ret = convert_utf8_to_utf16(tags->title, &title);
+       if (ret < 0)
+               goto out;
+       title_bytes = ret;
+       ret = convert_utf8_to_utf16(tags->comment, &comment);
+       if (ret < 0)
+               goto out;
+       comment_bytes = ret;
+
+       if (cdo) {
+               /*
+                * Sizes of the five fields (stored as 16-bit numbers) are
+                * located after the header (16 bytes) and the cdo size (8
+                * bytes).
+                */
+               orig_title_bytes = read_u16(cdo->ptr + 24);
+               orig_artist_bytes = read_u16(cdo->ptr + 26);
+               orig_cr_bytes = read_u16(cdo->ptr + 28);
+               orig_comment_bytes = read_u16(cdo->ptr + 30);
+               orig_rating_bytes = read_u16(cdo->ptr + 32);
+               cr = cdo->ptr + 34 + orig_title_bytes + orig_artist_bytes;
+               rating = cr + orig_cr_bytes + orig_comment_bytes;
+       } else {
+               orig_title_bytes = 2;
+               orig_artist_bytes = 2;
+               orig_cr_bytes = 2;
+               orig_comment_bytes = 2;
+               orig_rating_bytes = 2;
+               cr = null;
+               rating = null;
+       }
+
+       /* compute size of result cdo */
+       result->size = 16 + 8 + 5 * 2 + title_bytes + artist_bytes
+               + orig_cr_bytes + comment_bytes + orig_rating_bytes;
+       PARA_DEBUG_LOG("cdo is %zu bytes\n", (size_t)result->size);
+       p = result->ptr = para_malloc(result->size);
+       memcpy(p, content_description_header, 16);
+       p += 16;
+       write_u64(p, result->size);
+       p += 8;
+       write_u16(p, title_bytes);
+       p += 2;
+       write_u16(p, artist_bytes);
+       p += 2;
+       write_u16(p, orig_cr_bytes);
+       p += 2;
+       write_u16(p, comment_bytes);
+       p += 2;
+       write_u16(p, orig_rating_bytes);
+       p += 2;
+       memcpy(p, title, title_bytes);
+       p += title_bytes;
+       memcpy(p, artist, artist_bytes);
+       p += artist_bytes;
+       memcpy(p, cr, orig_cr_bytes);
+       p += orig_cr_bytes;
+       memcpy(p, comment, comment_bytes);
+       p += comment_bytes;
+       memcpy(p, rating, orig_rating_bytes);
+       p += orig_rating_bytes;
+       assert(p - result->ptr == result->size);
+       ret = 1;
+out:
+       free(artist);
+       free(title);
+       free(comment);
+       return ret;
+}
+
+/* The extended content description object contains album and year. */
+static int make_ecdo(struct taginfo *tags, struct asf_object *result)
+{
+       int ret;
+       char *p, *album = NULL, *year = NULL, null[2] = "\0\0";
+       int album_bytes, year_bytes;
+
+       result->ptr = NULL;
+       result->size = 0;
+       ret = convert_utf8_to_utf16(tags->album, &album);
+       if (ret < 0)
+               return ret;
+       album_bytes = ret;
+       ret = convert_utf8_to_utf16(tags->year, &year);
+       if (ret < 0)
+               goto out;
+       year_bytes = ret;
+       result->size = 16 + 8 + 2; /* GUID, size, count */
+       /* name_length + name + null + data type + val length + val */
+       result->size += 2 + sizeof(album_tag_header) + 2 + 2 + 2 + album_bytes;
+       result->size += 2 + sizeof(year_tag_header) + 2 + 2 + 2 + year_bytes;
+
+       p = result->ptr = para_malloc(result->size);
+       memcpy(p, extended_content_header, 16);
+       p += 16;
+       write_u64(p, result->size);
+       p += 8;
+       write_u16(p, 2); /* count */
+       p += 2;
+
+       /* album */
+       write_u16(p, sizeof(album_tag_header) + 2);
+       p += 2;
+       memcpy(p, album_tag_header, sizeof(album_tag_header));
+       p += sizeof(album_tag_header);
+       memcpy(p, null, 2);
+       p += 2;
+       write_u16(p, 0); /* data type (UTF-16) */
+       p += 2;
+       write_u16(p, album_bytes);
+       p += 2;
+       memcpy(p, album, album_bytes);
+       p += album_bytes;
+
+       /* year */
+       write_u16(p, sizeof(year_tag_header));
+       p += 2;
+       memcpy(p, year_tag_header, sizeof(year_tag_header));
+       p += sizeof(year_tag_header);
+       memcpy(p, null, 2);
+       p += 2;
+       write_u16(p, 0); /* data type (UTF-16) */
+       p += 2;
+       write_u16(p, year_bytes);
+       p += 2;
+       memcpy(p, year, year_bytes);
+       p += year_bytes;
+       assert(p - result->ptr == result->size);
+       ret = 1;
+out:
+       free(album);
+       free(year);
+       return ret;
+}
+
+static int write_output_file(int fd, const char *map, size_t mapsize,
+               struct afs_top_level_header_object *top, struct tag_object_nums *ton,
+               struct asf_object *cdo, struct asf_object *ecdo)
+{
+       int i, ret;
+       uint64_t sz; /* of the new header object */
+       uint32_t num_objects;
+       char tmp[8];
+
+       sz = 16 + 8 + 4 + 1 + 1; /* top-level header object */
+       for (i = 0; i < top->num_objects; i++) {
+               if (i == ton->content_descr_obj_num)
+                       continue;
+               if (i == ton->extended_content_descr_obj_num)
+                       continue;
+               sz += top->objects[i].size;
+       }
+       sz += cdo->size;
+       sz += ecdo->size;
+       num_objects = top->num_objects;
+       if (ton->content_descr_obj_num < 0)
+               num_objects++;
+       if (ton->extended_content_descr_obj_num < 0)
+               num_objects++;
+       ret = xwrite(fd, top_level_header_object_guid, 16);
+       if (ret < 0)
+               goto out;
+       write_u64(tmp, sz);
+       ret = xwrite(fd, tmp, 8);
+       if (ret < 0)
+               goto out;
+       write_u32(tmp, num_objects);
+       ret = xwrite(fd, tmp, 4);
+       if (ret < 0)
+               goto out;
+       write_u8(tmp, top->reserved1);
+       ret = xwrite(fd, tmp, 1);
+       if (ret < 0)
+               goto out;
+       write_u8(tmp, top->reserved2);
+       ret = xwrite(fd, tmp, 1);
+       if (ret < 0)
+               goto out;
+       /*
+        * Write cto and ecto as objects 0 and 1 if they did not exist in the
+        * original file.
+        */
+       if (ton->content_descr_obj_num < 0) {
+               ret = xwrite(fd, cdo->ptr, cdo->size);
+               if (ret < 0)
+                       goto out;
+       }
+       if (ton->extended_content_descr_obj_num < 0) {
+               ret = xwrite(fd, ecdo->ptr, ecdo->size);
+               if (ret < 0)
+                       goto out;
+       }
+
+       for (i = 0; i < top->num_objects; i++) {
+               char *buf = top->objects[i].ptr;
+               sz = top->objects[i].size;
+               if (i == ton->content_descr_obj_num) {
+                       buf = cdo->ptr;
+                       sz = cdo->size;
+               } else if (i == ton->extended_content_descr_obj_num) {
+                       buf = ecdo->ptr;
+                       sz = ecdo->size;
+               }
+               ret = xwrite(fd, buf, sz);
+               if (ret < 0)
+                       goto out;
+       }
+       ret = xwrite(fd, map + top->size, mapsize - top->size);
+out:
+       return ret;
+}
+
+static int wma_rewrite_tags(const char *map, size_t mapsize,
+               struct taginfo *tags, int fd,
+               __a_unused const char *filename)
+{
+       struct afs_top_level_header_object top;
+       struct tag_object_nums ton = {-1, -1};
+       const char *p = map;
+       /* (extended) content description object */
+       struct asf_object cdo = {.ptr = NULL}, ecdo = {.ptr = NULL};
+       int ret;
+
+       /* guid + size + num_objects + 2 * reserved */
+       if (mapsize < 16 + 8 + 4 + 1 + 1)
+               return -E_NO_WMA;
+       if (memcmp(map, top_level_header_object_guid, 16))
+               return -E_NO_WMA;
+       p += 16;
+       top.size = read_u64(p);
+       PARA_INFO_LOG("header_size: %lu\n", (long unsigned)top.size);
+       if (top.size >= mapsize)
+               return -E_NO_WMA;
+       p += 8;
+       top.num_objects = read_u32(p);
+       PARA_NOTICE_LOG("%u header objects\n", top.num_objects);
+       if (top.num_objects > top.size / 24)
+               return -E_NO_WMA;
+       p += 4;
+       top.reserved1 = read_u8(p);
+       p++;
+       top.reserved2 = read_u8(p);
+       if (top.reserved2 != 2)
+               return -E_NO_WMA;
+       p++; /* objects start at p */
+       top.objects = para_malloc(top.num_objects * sizeof(struct asf_object));
+       ret = read_asf_objects(p, top.size - (p - map), top.num_objects,
+               top.objects, &ton);
+       if (ret < 0)
+               goto out;
+       ret = make_cdo(tags, ton.content_descr_obj_num >= 0?
+               top.objects + ton.content_descr_obj_num : NULL, &cdo);
+       if (ret < 0)
+               goto out;
+       ret = make_ecdo(tags, &ecdo);
+       if (ret < 0)
+               goto out;
+       ret = write_output_file(fd, map, mapsize, &top, &ton, &cdo,
+               &ecdo);
+out:
+       free(cdo.ptr);
+       free(ecdo.ptr);
+       free(top.objects);
+       return ret;
+}
+
 static const char* wma_suffixes[] = {"wma", NULL};
 
 /**
@@ -276,4 +653,5 @@ void wma_afh_init(struct audio_format_handler *afh)
 {
        afh->get_file_info = wma_get_file_info;
        afh->suffixes = wma_suffixes;
+       afh->rewrite_tags = wma_rewrite_tags;
 }