/*
- * 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: %u\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.
{
afh->get_file_info = flac_get_file_info,
afh->suffixes = flac_suffixes;
+ afh->rewrite_tags = flac_rewrite_tags;
}