From: Andre Noll Date: Sun, 27 Sep 2015 12:35:05 +0000 (+0000) Subject: Merge branch 'refs/heads/t/taggers' X-Git-Tag: v0.5.6~100 X-Git-Url: http://git.tuebingen.mpg.de/?a=commitdiff_plain;h=486314426fcd25e5271fd65a982f8b321585e195;hp=6a22b3923f75436aaf84135bd397ab82f22bc09f;p=paraslash.git Merge branch 'refs/heads/t/taggers' 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. --- diff --git a/Makefile.in b/Makefile.in index bad4d872..a8e2a8b9 100644 --- a/Makefile.in +++ b/Makefile.in @@ -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 diff --git a/Makefile.real b/Makefile.real index 1cfcfa9f..00175bea 100644 --- a/Makefile.real +++ b/Makefile.real @@ -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 d98930c7..f1940d3d 100644 --- 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" -------------------------------------- diff --git a/aac_afh.c b/aac_afh.c index 3458af95..5b2e9fba 100644 --- a/aac_afh.c +++ b/aac_afh.c @@ -11,12 +11,14 @@ /** \file aac_afh.c para_server's aac audio format handler. */ #include +#include #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 f3c25a26..195b3788 100644 --- 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 48307298..62e38c02 100644 --- 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); diff --git a/afh_common.c b/afh_common.c index eb0813df..b1f8b25d 100644 --- a/afh_common.c +++ b/afh_common.c @@ -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); +} diff --git a/configure.ac b/configure.ac index a30b0c20..2b2f2966 100644 --- a/configure.ac +++ b/configure.ac @@ -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 + ],[ + 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 57f581cf..3818b110 100644 --- 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 ceff71f5..6a26ce5e 100644 --- a/fd.c +++ b/fd.c @@ -16,6 +16,30 @@ #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 89de8533..29f38798 100644 --- 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 *); diff --git a/flac_afh.c b/flac_afh.c index d72eb83b..51c3c314 100644 --- a/flac_afh.c +++ b/flac_afh.c @@ -14,9 +14,10 @@ #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; } diff --git a/m4/gengetopt/afh.m4 b/m4/gengetopt/afh.m4 index 0df2fad8..9b8a6503 100644 --- a/m4/gengetopt/afh.m4 +++ b/m4/gengetopt/afh.m4 @@ -6,6 +6,11 @@ include(header.m4) include(loglevel.m4) + +################################### +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 + diff --git a/mp3_afh.c b/mp3_afh.c index ccd28dad..484172ab 100644 --- 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 */ } diff --git a/ogg_afh.c b/ogg_afh.c index 9dfb028d..32f8bc1b 100644 --- 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; } 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; } diff --git a/spx_afh.c b/spx_afh.c index e8353679..108cfaa4 100644 --- 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; } diff --git a/web/about.in.html b/web/about.in.html index 5936f932..2c72f281 100644 --- a/web/about.in.html +++ b/web/about.in.html @@ -8,7 +8,7 @@ systems. It is written in C and released under the GPLv2.
  • Runs on Linux, Mac OS, FreeBSD, NetBSD
  • Mp3, ogg/vorbis, ogg/speex, aac (m4a), wma, flac and ogg/opus support
  • http, dccp and udp network streaming
  • -
  • Stand-alone decoder/player
  • +
  • Stand-alone decoder, player, tagger
  • Curses-based gui (screenshot)
  • Integrated volume normalizer, fader, alarm clock
  • Sophisticated audio file selector
  • diff --git a/web/manual.m4 b/web/manual.m4 index 8c00c99b..0963306e 100644 --- a/web/manual.m4 +++ b/web/manual.m4 @@ -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, diff --git a/wma_afh.c b/wma_afh.c index f1edacf0..0b6081cf 100644 --- a/wma_afh.c +++ b/wma_afh.c @@ -8,6 +8,7 @@ #include #include +#include #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; }