The wma tagger.
authorAndre Noll <maan@systemlinux.org>
Sun, 27 Oct 2013 17:50:14 +0000 (18:50 +0100)
committerAndre Noll <maan@tuebingen.mpg.de>
Sun, 26 Apr 2015 12:13:34 +0000 (14:13 +0200)
This adds infrastructure to support meta tag editing. If the new
--modify option is given to para_afh, the arguments to --title,
--artist, --album and --comment are used to alter the meta information
of the audio file. Only the wma audio format handler is extended
to support the new feature. Patches for other audio format handlers
follow.

As for the implementation, this commit adds the function pointer
->rewrite_tags to struct audio_format_handler.  This function takes
a file descriptor to a newly opened temporary file. The individual
audio format handlers are supposed to write the altered contents to
this file descriptor. On success, the temporary file is renamed on
top of the original file unless --backup is given.

Since meta tags in wma files are encoded in UTF-16 we need primitives
to convert from UTF8 to UTF16 and vice versa. These are provided by
libiconv, so we check for this library and deactivate the new features
on systems that lack libiconv.

Unfortunately the signatures of iconv() are different between Linux
and FreeBSD. To deal with this incompatibility this patch adds a
configure check to determine if the cast is necessary.

FEATURES
Makefile.in
Makefile.real
afh.c
afh.h
afh_common.c
configure.ac
fd.c
fd.h
m4/gengetopt/afh.m4
wma_afh.c

index 395d0f1..9b7311b 100644 (file)
--- a/FEATURES
+++ b/FEATURES
@@ -13,8 +13,7 @@ Features
        * Forward error correction allows receivers to recover from packet losses
        * Volume normalizer
        * Stream grabbing at any point in the filter chain
-       * Stand-alone command line receiver/decoder/normalizer/player
-       * Stand-alone audio format handler utility
+       * Stand-alone command line receiver/decoder/normalizer/player/tagger
        * Sophisticated audio file selector
        * Small memory footprint
        * Command line interface for easy scripting in high-level languages
index 14015c9..6d86ee7 100644 (file)
@@ -73,5 +73,6 @@ nsl_ldflags := @nsl_ldflags@
 curses_ldflags := @curses_ldflags@
 core_audio_ldflags := @core_audio_ldflags@
 crypto_ldflags := @crypto_ldflags@
+iconv_ldflags := @iconv_ldflags@
 
 include Makefile.real
index c5d6405..fa8a915 100644 (file)
@@ -319,6 +319,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/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 08bcde9..7947a66 100644 (file)
@@ -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 8095d33..186e381 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=],
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 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 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;
 }