From 6bcdcffef00ff117eecf1b5447b9e849698d02e5 Mon Sep 17 00:00:00 2001 From: Andre Noll Date: Sun, 15 Dec 2013 21:01:57 +0100 Subject: [PATCH] The ogg/opus tagger. This adds support for modifying meta tags of ogg/opus files. The heart of this patch is ogg_rewrite_tags(), a codec-independent function which replaces the meta packet of an ogg stream. This function will also be used in subsequent patches which add support for ogg/vorbis and ogg/speex files. In order to create identically sized ogg pages for the output, the new ogg_rewrite_tags() function calls ogg_stream_flush_fill() if it is available. This function was introduced in libogg version 1.3.0. So this commit adds a configure check and makes ogg_rewrite_tags() fall back to ogg_stream_flush() on systems where libogg lacks ogg_stream_flush_fill(). --- configure.ac | 2 + error.h | 6 +- ogg_afh_common.c | 146 ++++++++++++++++++++++++++++++++++++++++++++++- ogg_afh_common.h | 2 + opus_afh.c | 96 ++++++++++++++++++++++++++++++- 5 files changed, 248 insertions(+), 4 deletions(-) diff --git a/configure.ac b/configure.ac index 186e3817..3c031df0 100644 --- a/configure.ac +++ b/configure.ac @@ -272,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 diff --git a/error.h b/error.h index 31426b04..eb8d659b 100644 --- a/error.h +++ b/error.h @@ -105,9 +105,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"), \ diff --git a/ogg_afh_common.c b/ogg_afh_common.c index 61ded4dc..b8b0006d 100644 --- a/ogg_afh_common.c +++ b/ogg_afh_common.c @@ -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; +} diff --git a/ogg_afh_common.h b/ogg_afh_common.h index 47e133bf..7b9d1313 100644 --- a/ogg_afh_common.h +++ b/ogg_afh_common.h @@ -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); diff --git a/opus_afh.c b/opus_afh.c index 62d9e08e..ace83008 100644 --- a/opus_afh.c +++ b/opus_afh.c @@ -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; } -- 2.39.2