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.
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@
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
$(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 \
para_play \
: LDFLAGS += \
$(mad_ldflags) \
+ $(faad_ldflags) \
$(samplerate_ldflags) \
-lm
$(faad_ldflags) \
$(flac_ldflags)
+para_server \
+para_play \
+para_afh \
+para_recv \
+: LDFLAGS += \
+ $(mp4v2_ldflags)
+
para_server \
para_client \
para_audioc \
: 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 $@'
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"
--------------------------------------
/** \file aac_afh.c para_server's aac audio format handler. */
#include <regex.h>
+#include <mp4v2/mp4v2.h>
#include "para.h"
#include "error.h"
#include "afh.h"
#include "string.h"
#include "aac.h"
+#include "fd.h"
static int aac_find_stsz(unsigned char *buf, size_t buflen, off_t *skip)
{
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
{
afh->get_file_info = aac_get_file_info,
afh->suffixes = aac_suffixes;
+ afh->rewrite_tags = aac_rewrite_tags;
}
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;
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);
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);
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);
},
{
.name = "aac",
-#ifdef HAVE_FAAD
+#if defined(HAVE_MP4V2)
.init = aac_afh_init,
#endif
},
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);
+}
[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=],
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
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"
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,
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"
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
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"
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
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 \
#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"), \
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"), \
#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"), \
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"), \
#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.
*
/** \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 *);
#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;
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);
}
}
-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;
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);
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);
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};
/**
{
afh->get_file_info = flac_get_file_info,
afh->suffixes = flac_suffixes;
+ afh->rewrite_tags = flac_rewrite_tags;
}
include(loglevel.m4)
<qu>
+
+###################################
+section "printing meta information"
+###################################
+
option "chunk-table" c
#~~~~~~~~~~~~~~~~~~~~~
"print also the chunk table"
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>
#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
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 */
/*
{
afh->get_file_info = mp3_get_file_info;
afh->suffixes = mp3_suffixes;
+#ifdef HAVE_LIBID3TAG
+ afh->rewrite_tags = mp3_rewrite_tags;
+#endif /* HAVE_LIBID3TAG */
}
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.
*
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;
}
#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,
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;
+}
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);
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)
{
/* 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);
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.
*
{
afh->get_file_info = opus_get_file_info,
afh->suffixes = opus_suffixes;
+ afh->rewrite_tags = opus_rewrite_tags;
}
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.
*
{
afh->get_file_info = spx_get_file_info,
afh->suffixes = speex_suffixes;
+ afh->rewrite_tags = spx_rewrite_tags;
}
<li> Runs on Linux, Mac OS, FreeBSD, NetBSD </li>
<li> Mp3, ogg/vorbis, ogg/speex, aac (m4a), wma, flac and ogg/opus support </li>
<li> http, dccp and udp network streaming </li>
- <li> Stand-alone decoder/player </li>
+ <li> Stand-alone decoder, player, tagger </li>
<li> Curses-based gui (<a href="gui.png">screenshot</a>) </li>
<li> Integrated volume normalizer, fader, alarm clock </li>
<li> Sophisticated audio file selector </li>
- 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,
#include <sys/types.h>
#include <regex.h>
+#include <iconv.h>
#include "para.h"
#include "error.h"
#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); \
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};
/**
{
afh->get_file_info = wma_get_file_info;
afh->suffixes = wma_suffixes;
+ afh->rewrite_tags = wma_rewrite_tags;
}