afh: Make ->suffixes array const.
[paraslash.git] / flac_afh.c
index 4ac68cd..2e2842b 100644 (file)
@@ -1,5 +1,5 @@
 /*
- * Copyright (C) 2011 Andre Noll <maan@systemlinux.org>
+ * Copyright (C) 2011 Andre Noll <maan@tuebingen.mpg.de>
  *
  * Licensed under the GPL v2. For licencing details see COPYING.
  */
 /** \file flac_afh.c Audio format handler for flac files. */
 
 #include <regex.h>
+#include <FLAC/stream_decoder.h>
+#include <FLAC/metadata.h>
 
 #include "para.h"
 #include "error.h"
 #include "afh.h"
+#include "string.h"
+#include "fd.h"
 
-static int flac_get_file_info(char *map, size_t numbytes, __a_unused int fd,
-               struct afh_info *afhi)
+struct private_flac_afh_data {
+       const char *map;
+       size_t map_bytes;
+       size_t fpos;
+       struct afh_info *afhi;
+       unsigned blocksize;
+};
+
+static size_t copy_data(struct private_flac_afh_data *pfad, void *buf,
+               size_t want)
+{
+       size_t copy, have = pfad->map_bytes - pfad->fpos;
+
+       if (have == 0)
+               return 0;
+       copy = have < want? have : want;
+       memcpy(buf, pfad->map + pfad->fpos, copy);
+       pfad->fpos += copy;
+       return copy;
+}
+
+static size_t meta_read_cb(void *ptr, size_t size, size_t nmemb,
+               FLAC__IOHandle handle)
+{
+       struct private_flac_afh_data *pfad = handle;
+       return copy_data(pfad, ptr, nmemb * size);
+}
+
+static int meta_seek_cb(FLAC__IOHandle handle, FLAC__int64 offset, int whence)
+{
+       struct private_flac_afh_data *pfad = handle;
+
+       if (offset < 0)
+               return -1;
+
+       switch (whence) {
+       case SEEK_SET:
+               if (offset >= pfad->map_bytes)
+                       return -1;
+               pfad->fpos = offset;
+               return 0;
+       case SEEK_CUR:
+               if (pfad->fpos + offset >= pfad->map_bytes)
+                       return -1;
+               pfad->fpos += offset;
+               return 0;
+       case SEEK_END:
+               if (offset >= pfad->map_bytes)
+                       return -1;
+               pfad->fpos = offset;
+               return 0;
+       default:
+               return -1;
+       }
+}
+
+static FLAC__int64 meta_tell_cb(FLAC__IOHandle handle)
+{
+       struct private_flac_afh_data *pfad = handle;
+       return pfad->fpos;
+}
+
+static int meta_eof_cb(FLAC__IOHandle handle)
+{
+       struct private_flac_afh_data *pfad = handle;
+       return pfad->fpos == pfad->map_bytes;
+}
+
+static int meta_close_cb(FLAC__IOHandle __a_unused handle)
 {
        return 0;
 }
 
-static const char* flac_suffixes[] = {"flac", NULL};
+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);
+       freep(&tags->title);
+       freep(&tags->album);
+       freep(&tags->year);
+       freep(&tags->comment);
+}
+
+static bool copy_if_tag_type(const char *tag, int taglen, const char *type,
+               char **p)
+{
+       char *q = key_value_copy(tag, taglen, type);
+       if (!q)
+               return false;
+       free(*p);
+       *p = q;
+       return true;
+}
+
+static void flac_read_vorbis_comments(FLAC__StreamMetadata_VorbisComment *vc,
+               struct taginfo *tags)
+{
+       int i;
+       FLAC__StreamMetadata_VorbisComment_Entry *comment = vc->comments;
+
+       PARA_INFO_LOG("found %u vorbis comments\n", vc->num_comments);
+       for (i = 0; i < vc->num_comments; i++) {
+               char *e = (char *)comment[i].entry;
+               int len = comment[i].length;
+               if (copy_if_tag_type(e, len, "artist", &tags->artist))
+                       continue;
+               if (copy_if_tag_type(e, len, "title", &tags->title))
+                       continue;
+               if (copy_if_tag_type(e, len, "album", &tags->album))
+                       continue;
+               if (copy_if_tag_type(e, len, "year", &tags->year))
+                       continue;
+               if (copy_if_tag_type(e, len, "comment", &tags->comment))
+                       continue;
+       }
+}
+
+/*
+ * 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;
+
+       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;
+
+       *chainp = NULL;
+       *iterp = NULL;
+       chain = FLAC__metadata_chain_new();
+       if (!chain)
+               return -E_FLAC_CHAIN_ALLOC;
+       ret = -E_FLAC_CHAIN_READ;
+       ok = FLAC__metadata_chain_read_with_callbacks(chain, pfad,
+               meta_callbacks);
+       if (!ok)
+               goto free_chain;
+       ret = -E_FLAC_ITER_ALLOC;
+       iter = FLAC__metadata_iterator_new();
+       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);
+               if (!b)
+                       break;
+               if (b->type == FLAC__METADATA_TYPE_STREAMINFO) {
+                       info = &b->data.stream_info;
+                       ret = -E_FLAC_VARBLOCK;
+                       if (info->min_blocksize != info->max_blocksize)
+                               goto free_iter;
+                       pfad->afhi->frequency = info->sample_rate;
+                       pfad->afhi->channels = info->channels;
+                       pfad->blocksize = info->min_blocksize;
+               }
+               if (b->type == FLAC__METADATA_TYPE_VORBIS_COMMENT)
+                       flac_read_vorbis_comments(&b->data.vorbis_comment,
+                               &pfad->afhi->tags);
+               ok = FLAC__metadata_iterator_next(iter);
+               if (!ok)
+                       break;
+       }
+       ret = info? 0: -E_FLAC_STREAMINFO;
+free_iter:
+       FLAC__metadata_iterator_delete(iter);
+       FLAC__metadata_chain_delete(chain);
+       if (ret < 0)
+               free_tags(&pfad->afhi->tags);
+       return ret;
+}
+
+static FLAC__StreamDecoderReadStatus read_cb(
+               __a_unused const FLAC__StreamDecoder *decoder,
+               FLAC__byte buffer[], size_t *bytes, void *client_data)
+{
+       struct private_flac_afh_data *pfad = client_data;
+
+       assert(*bytes > 0);
+       *bytes = copy_data(pfad, buffer, *bytes);
+       if (*bytes == 0)
+               return FLAC__STREAM_DECODER_READ_STATUS_END_OF_STREAM;
+       else
+               return FLAC__STREAM_DECODER_READ_STATUS_CONTINUE;
+}
+
+static FLAC__StreamDecoderTellStatus tell_cb(
+               __a_unused const FLAC__StreamDecoder *decoder,
+               FLAC__uint64 *absolute_byte_offset, void *client_data)
+{
+       struct private_flac_afh_data *pfad = client_data;
+
+       *absolute_byte_offset = pfad->fpos;
+       return FLAC__STREAM_DECODER_TELL_STATUS_OK;
+}
+
+/* libflac insists on this callback being present. */
+static FLAC__StreamDecoderWriteStatus write_cb(
+               __a_unused const FLAC__StreamDecoder *decoder,
+               __a_unused const FLAC__Frame *frame,
+               __a_unused const FLAC__int32 *const buffer[],
+               __a_unused void *client_data)
+{
+       return FLAC__STREAM_DECODER_WRITE_STATUS_CONTINUE;
+}
+
+static void error_cb(
+               __a_unused const FLAC__StreamDecoder *decoder,
+               FLAC__StreamDecoderErrorStatus status,
+               __a_unused void *client_data)
+{
+       PARA_ERROR_LOG("%s\n", FLAC__StreamDecoderErrorStatusString[status]);
+}
+
+static int flac_afh_read_chunks(struct private_flac_afh_data *pfad)
+{
+       FLAC__StreamDecoder *decoder;
+       FLAC__StreamDecoderInitStatus init_status;
+       FLAC__bool ok;
+       FLAC__uint64 c;
+       unsigned chunk_table_size = 0;
+       int ret;
+       struct afh_info *afhi = pfad->afhi;
+
+       PARA_INFO_LOG("reading chunk table\n");
+       afhi->chunk_table = NULL;
+       decoder = FLAC__stream_decoder_new();
+       if (!decoder)
+               return -E_FLAC_AFH_DECODER_ALLOC;
+       ret = -E_FLAC_AFH_DECODER_INIT;
+       init_status = FLAC__stream_decoder_init_stream(
+               decoder,
+               read_cb,
+               NULL, /* seek */
+               tell_cb,
+               NULL, /* length */
+               NULL, /* eof */
+               write_cb,
+               NULL,
+               error_cb,
+               pfad
+       );
+       if (init_status != FLAC__STREAM_DECODER_INIT_STATUS_OK)
+               goto free_decoder;
+       ret = -E_FLAC_SKIP_META;
+       ok = FLAC__stream_decoder_process_until_end_of_metadata(decoder);
+       if (!ok)
+               goto free_decoder;
+       for (c = 0;; c++) {
+               FLAC__uint64 pos;
+               FLAC__StreamDecoderState state;
+
+               ret = -E_FLAC_DECODE_POS;
+               ok = FLAC__stream_decoder_get_decode_position(decoder, &pos);
+               if (!ok)
+                       goto free_decoder;
+               if (c >= chunk_table_size) {
+                       chunk_table_size = 2 * chunk_table_size + 100;
+                       afhi->chunk_table = para_realloc(afhi->chunk_table,
+                               chunk_table_size * sizeof(uint32_t));
+               }
+               afhi->chunk_table[c] = pos;
+
+               ok = FLAC__stream_decoder_skip_single_frame(decoder);
+               if (!ok)
+                       break;
+               state = FLAC__stream_decoder_get_state(decoder);
+               if (state == FLAC__STREAM_DECODER_END_OF_STREAM)
+                       break;
+       }
+       afhi->chunks_total = c;
+       ret = 1;
+free_decoder:
+       FLAC__stream_decoder_finish(decoder);
+       FLAC__stream_decoder_delete(decoder);
+       if (ret < 0)
+               freep(&afhi->chunk_table);
+       return ret;
+}
+
+static int flac_get_file_info(char *map, size_t map_bytes, __a_unused int fd,
+               struct afh_info *afhi)
+{
+       struct private_flac_afh_data pfad_struct = {
+               .map = map,
+               .map_bytes = map_bytes,
+               .afhi = afhi
+       }, *pfad = &pfad_struct;
+       int ret;
+       double chunk_time;
+
+       afhi->header_len = 0;
+       ret = flac_read_meta(pfad);
+       if (ret < 0)
+               return ret;
+       pfad->fpos = 0;
+       ret = flac_afh_read_chunks(pfad);
+       if (ret < 0) {
+               free_tags(&afhi->tags);
+               return ret;
+       }
+       afhi->techinfo = make_message("blocksize: %u", pfad->blocksize);
+       afhi->seconds_total = DIV_ROUND_UP(afhi->chunks_total * pfad->blocksize,
+               afhi->frequency);
+       afhi->bitrate = pfad->map_bytes * 8 / afhi->seconds_total / 1024;
+       chunk_time = (double)pfad->blocksize / afhi->frequency;
+       afhi->chunk_tv.tv_sec = chunk_time;
+       chunk_time *= 1000 * 1000;
+       chunk_time -= afhi->chunk_tv.tv_sec * 1000 * 1000;
+       afhi->chunk_tv.tv_usec = chunk_time;
+       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 * const flac_suffixes[] = {"flac", NULL};
 
 /**
  * The init function of the flac audio format handler.
@@ -29,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;
 }