diff options
author | Alexey Yakovenko <waker@users.sourceforge.net> | 2014-06-06 19:58:25 +0200 |
---|---|---|
committer | Alexey Yakovenko <waker@users.sourceforge.net> | 2014-06-06 19:58:25 +0200 |
commit | ecebdd0c0603bcad2b306a0bb660b1028c78bed8 (patch) | |
tree | 6f0b058f576c03e176741a498de2ec8710fd9ec0 /plugins | |
parent | 1a857fc4e64e4ad9d7ceae0797e8d9f260ad7398 (diff) | |
parent | 1cd417fc09faec13357268141767b24b8033eea7 (diff) |
Merge branch 'master' of https://github.com/Lithopsian/deadbeef into Lithopsian-master
Conflicts:
plugins/liboggedit/Makefile.am
Diffstat (limited to 'plugins')
-rw-r--r-- | plugins/flac/Makefile.am | 2 | ||||
-rw-r--r-- | plugins/flac/flac.c | 59 | ||||
-rw-r--r-- | plugins/liboggedit/Makefile.am | 3 | ||||
-rw-r--r-- | plugins/liboggedit/oggedit.c | 1212 | ||||
-rw-r--r-- | plugins/liboggedit/oggedit.h | 18 | ||||
-rw-r--r-- | plugins/liboggedit/oggedit_art.c | 172 | ||||
-rw-r--r-- | plugins/liboggedit/oggedit_flac.c | 213 | ||||
-rw-r--r-- | plugins/liboggedit/oggedit_internal.c | 524 | ||||
-rw-r--r-- | plugins/liboggedit/oggedit_internal.h | 59 | ||||
-rw-r--r-- | plugins/liboggedit/oggedit_opus.c | 197 | ||||
-rw-r--r-- | plugins/liboggedit/oggedit_utils.c | 125 | ||||
-rw-r--r-- | plugins/liboggedit/oggedit_vorbis.c | 168 | ||||
-rw-r--r-- | plugins/vorbis/Makefile.am | 4 | ||||
-rw-r--r-- | plugins/vorbis/vcedit.c | 289 | ||||
-rw-r--r-- | plugins/vorbis/vcedit.h | 35 | ||||
-rw-r--r-- | plugins/vorbis/vorbis.c | 735 |
16 files changed, 1864 insertions, 1951 deletions
diff --git a/plugins/flac/Makefile.am b/plugins/flac/Makefile.am index cc5ecf28..40023a16 100644 --- a/plugins/flac/Makefile.am +++ b/plugins/flac/Makefile.am @@ -2,7 +2,7 @@ if HAVE_FLAC flacdir = $(libdir)/$(PACKAGE) pkglib_LTLIBRARIES = flac.la flac_la_SOURCES = flac.c -flac_la_LDFLAGS = -module -avoid-version +flac_la_LDFLAGS = -module -avoid-version -export-symbols-regex flac_load if HAVE_OGG oggedit_def = -DUSE_OGGEDIT=1 diff --git a/plugins/flac/flac.c b/plugins/flac/flac.c index 9a823b9e..016a3c6a 100644 --- a/plugins/flac/flac.c +++ b/plugins/flac/flac.c @@ -29,14 +29,17 @@ NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ + #include <string.h> #include <stdio.h> #include <stdlib.h> +#include <stdbool.h> +#include <math.h> #include <FLAC/stream_decoder.h> #include <FLAC/metadata.h> -#include <math.h> #include "../../deadbeef.h" #include "../artwork/artwork.h" +#include "../liboggedit/oggedit.h" static DB_decoder_t plugin; static DB_functions_t *deadbeef; @@ -653,9 +656,9 @@ cflac_insert_with_embedded_cue (ddb_playlist_t *plt, DB_playItem_t *after, DB_pl deadbeef->pl_item_unref (it); } deadbeef->pl_item_ref (after); - + DB_playItem_t *first = deadbeef->pl_get_next (ins, PL_MAIN); - + if (!first) { first = deadbeef->plt_get_first (plt, PL_MAIN); } @@ -869,9 +872,12 @@ cflac_read_metadata (DB_playItem_t *it) { } deadbeef->pl_lock (); FLAC__bool res = FLAC__metadata_chain_read (chain, deadbeef->pl_find_meta (it, ":URI")); + if (!res && FLAC__metadata_chain_status(chain) == FLAC__METADATA_SIMPLE_ITERATOR_STATUS_NOT_A_FLAC_FILE) { + res = FLAC__metadata_chain_read_ogg (chain, deadbeef->pl_find_meta (it, ":URI")); + } deadbeef->pl_unlock (); if (!res) { - trace ("cflac_read_metadata: FLAC__metadata_chain_read failed\n"); + trace ("cflac_read_metadata: FLAC__metadata_chain_read(_ogg) failed\n"); goto error; } FLAC__metadata_chain_merge_padding (chain); @@ -922,7 +928,28 @@ error: return err; } +#if USE_OGGEDIT +int +cflac_write_metadata_ogg (DB_playItem_t *it, FLAC__StreamMetadata_VorbisComment *vc) +{ + char fname[PATH_MAX]; + deadbeef->pl_get_meta (it, ":URI", fname, sizeof (fname)); + + size_t num_tags = vc->num_comments; + char **tags = calloc(num_tags+1, sizeof(char **)); + for (size_t i = 0; i < num_tags; i++) + tags[i] = vc->comments[i].entry; + const off_t file_size = oggedit_write_flac_metadata (deadbeef->fopen(fname), fname, 0, num_tags, tags); + if (file_size <= 0) { + trace ("cflac_write_metadata_ogg: oggedit_write_flac_metadata failed: code %d\n", file_size); + return -1; + } + free(tags); + + return 0; +} +#endif int cflac_write_metadata (DB_playItem_t *it) { int err = -1; @@ -936,9 +963,16 @@ cflac_write_metadata (DB_playItem_t *it) { } deadbeef->pl_lock (); FLAC__bool res = FLAC__metadata_chain_read (chain, deadbeef->pl_find_meta (it, ":URI")); + FLAC__bool isogg = false; +#if USE_OGGEDIT + if (!res && FLAC__metadata_chain_status(chain) == FLAC__METADATA_SIMPLE_ITERATOR_STATUS_NOT_A_FLAC_FILE) { + isogg = true; + res = FLAC__metadata_chain_read_ogg (chain, deadbeef->pl_find_meta (it, ":URI")); + } +#endif deadbeef->pl_unlock (); if (!res) { - trace ("cflac_write_metadata: FLAC__metadata_chain_read failed\n"); + trace ("cflac_write_metadata: FLAC__metadata_chain_read(_ogg) failed - code %d\n", res); goto error; } FLAC__metadata_chain_merge_padding (chain); @@ -978,7 +1012,7 @@ cflac_write_metadata (DB_playItem_t *it) { } else { // create new and add to chain - data = FLAC__metadata_object_new(FLAC__METADATA_TYPE_VORBIS_COMMENT); + data = FLAC__metadata_object_new(FLAC__METADATA_TYPE_VORBIS_COMMENT); if (!data) { fprintf (stderr, "flac: failed to allocate new vorbis comment block\n"); goto error; @@ -1128,10 +1162,17 @@ error2: } #endif - if (!FLAC__metadata_chain_write (chain, 1, 0)) { - trace ("cflac_write_metadata: FLAC__metadata_chain_write failed\n"); + if (!isogg) + res = FLAC__metadata_chain_write (chain, 1, 0); +#if USE_OGGEDIT + else + res = cflac_write_metadata_ogg(it, &data->data.vorbis_comment); +#endif + if (res) { + trace ("cflac_write_metadata: failed to write tags: code %d\n", res); goto error; } + err = 0; error: FLAC__metadata_iterator_delete (iter); @@ -1154,7 +1195,7 @@ static DB_decoder_t plugin = { .plugin.id = "stdflac", .plugin.name = "FLAC decoder", .plugin.descr = "FLAC decoder using libFLAC", - .plugin.copyright = + .plugin.copyright = "Copyright (C) 2009-2013 Alexey Yakovenko et al.\n" "Uses libFLAC (C) Copyright (C) 2000,2001,2002,2003,2004,2005,2006,2007 Josh Coalson\n" "Uses libogg Copyright (c) 2002, Xiph.org Foundation\n" diff --git a/plugins/liboggedit/Makefile.am b/plugins/liboggedit/Makefile.am index c6d8032a..651f9f44 100644 --- a/plugins/liboggedit/Makefile.am +++ b/plugins/liboggedit/Makefile.am @@ -1,5 +1,6 @@ if HAVE_OGG noinst_LIBRARIES = liboggedit.a -liboggedit_a_SOURCES = oggedit.c +liboggedit_a_SOURCES = oggedit_internal.h oggedit.h \ + oggedit_internal.c oggedit_utils.c oggedit_art.c oggedit_opus.c oggedit_vorbis.c oggedit_flac.c AM_CFLAGS = $(OGG_CFLAGS) -fPIC -std=c99 endif diff --git a/plugins/liboggedit/oggedit.c b/plugins/liboggedit/oggedit.c deleted file mode 100644 index 66c4a358..00000000 --- a/plugins/liboggedit/oggedit.c +++ /dev/null @@ -1,1212 +0,0 @@ -/* - This file is part of Deadbeef Player source code - http://deadbeef.sourceforge.net - - DeaDBeeF Ogg Edit library - - Copyright (C) 2014 Ian Nartowicz <deadbeef@nartowicz.co.uk> - - This software is provided 'as-is', without any express or implied - warranty. In no event will the authors be held liable for any damages - arising from the use of this software. - - Permission is granted to anyone to use this software for any purpose, - including commercial applications, and to alter it and redistribute it - freely, subject to the following restrictions: - - 1. The origin of this software must not be misrepresented; you must not - claim that you wrote the original software. If you use this software - in a product, an acknowledgment in the product documentation would be - appreciated but is not required. - 2. Altered source versions must be plainly marked as such, and must not be - misrepresented as being the original software. - 3. This notice may not be removed or altered from any source distribution. - -*/ - -#ifdef HAVE_CONFIG_H -#include <config.h> -#endif -#include <stdio.h> -#include <stdlib.h> -#include <string.h> -#include <unistd.h> -#include <stdbool.h> -#include <limits.h> -#include <ctype.h> -#include <libgen.h> -#include <errno.h> -#include <sys/stat.h> -#include <ogg/ogg.h> -#include "../../deadbeef.h" -#include "oggedit.h" - -#define CHUNKSIZE 4096 -#define MAXPAGE 65536 -#define MAXPAYLOAD 65025 - -/**************************** - * Ogg - ****************************/ -uint8_t *oggedit_vorbis_channel_map(const int channel_count) -{ - size_t map_size = channel_count * sizeof(uint8_t); - uint8_t *map = malloc(map_size); - if (!map) - return NULL; - switch(channel_count) { - case 3: - return memcpy(map, &(uint8_t[]){0,2,1}, map_size); - case 5: - return memcpy(map, &(uint8_t[]){0,2,1,3,4}, map_size); - case 6: - return memcpy(map, &(uint8_t[]){0,2,1,4,5,3}, map_size); - case 7: - return memcpy(map, &(uint8_t[]){0,2,1,4,5,6,3}, map_size); - case 8: - return memcpy(map, &(uint8_t[]){0,2,1,6,7,4,5,3}, map_size); - default: - free(map); - return NULL; - } -} - -const char *oggedit_map_tag(char *key, const char *in_or_out) -{ - typedef struct { - const char *tag; - const char *meta; - } key_t; - const key_t keys[] = { - /* Permanent named tags in DeaDBeef */ -// {.tag = "ARTIST", .meta = "artist"}, -// {.tag = "TITLE", .meta = "title"}, -// {.tag = "ALBUM", .meta = "album"}, - {.tag = "DATE", .meta = "year"}, - {.tag = "TRACKNUMBER", .meta = "track"}, - {.tag = "TRACKTOTAL", .meta = "numtracks"}, -// {.tag = "GENRE", .meta = "genre"}, -// {.tag = "COMPOSER", .meta = "composer"}, - {.tag = "DISCNUMBER", .meta = "disc"}, -// {.tag = "COMMENT", .meta = "comment"}, - /* Vorbis standard tags */ -// {.tag = "ARRANGER", .meta = "Arranger"}, -// {.tag = "AUTHOR", .meta = "Author"}, -// {.tag = "CONDUCTOR", .meta = "Conductor"}, -// {.tag = "ENSEMBLE", .meta = "Ensemble"}, -// {.tag = "LYRICIST", .meta = "Lyricist"}, -// {.tag = "PERFORMER", .meta = "Performer"}, -// {.tag = "PUBLISHER", .meta = "Publisher"}, -// {.tag = "DISCTOTAL", .meta = "Disctotal"}, -// {.tag = "OPUS", .meta = "Opus"}, -// {.tag = "PART", .meta = "Part"}, -// {.tag = "PARTNUMBER", .meta = "Partnumber"}, -// {.tag = "VERSION", .meta = "Version"}, -// {.tag = "DESCRIPTION", .meta = "Description"}, -// {.tag = "COPYRIGHT", .meta = "Copyright"}, -// {.tag = "LICENSE", .meta = "License"}, -// {.tag = "CONTACT", .meta = "Contact"}, -// {.tag = "ORGANIZATION", .meta = "Organization"}, -// {.tag = "LOCATION", .meta = "Location"}, -// {.tag = "EAN/UPN", .meta = "EAN/UPN"}, -// {.tag = "ISRC", .meta = "ISRC"}, -// {.tag = "LABEL", .meta = "Label"}, -// {.tag = "LABELNO", .meta = "Labelno"}, -// {.tag = "ENCODER", .meta = "Encoder"}, -// {.tag = "ENCODED-BY", .meta = "Encoded-by"}, -// {.tag = "ENCODING", .meta = "Encoding"}, - /* Other tags */ -// {.tag = "ALBUMARTIST", .meta = "Albumartist"}, -// {.tag = "ALBUM ARTIST", .meta = "Album artist"}, -// {.tag = "BAND", .meta = "Band"}, -// {.tag = "COMPILATION", .meta = "Compilation"}, -// {.tag = "TOTALTRACKS", .meta = "Totaltracks"}, -// {.tag = "ENCODED_BY", .meta = "Encoded_by"}, -// {.tag = "ENCODER_OPTIONS",.meta = "Encoder_options"}, - {.tag = NULL} - }; - - /* Mapping for special Deadbeef internal metadata */ - for (const key_t *match = keys; match->tag; match++) - if (!strcasecmp(*in_or_out == 't' ? match->tag : match->meta, key)) - return *in_or_out == 't' ? match->meta : match->tag; - - /* Upper-case all Vorbis Comment tag names */ - if (*in_or_out == 'm') - for (size_t i = 0; key[i]; i++) - key[i] = toupper(key[i]); - - return key; -} - -const char *oggedit_album_art_type(const int type) -{ - switch (type) { - case 1: - return "32x32 pixels file icon"; - case 2: - return "other file icon"; - case 3: - return "front cover"; - case 4: - return "back cover"; - case 5: - return "leaflet page"; - case 6: - return "media"; - case 7: - return "lead artist/lead performer/soloist"; - case 8: - return "artist/performer"; - case 9: - return "conductor"; - case 10: - return "band/orchestra"; - case 11: - return "composer"; - case 12: - return "lyricist/text writer"; - case 13: - return "recording location"; - case 14: - return "during recording"; - case 15: - return "during performance"; - case 16: - return "movie/video screen capture"; - case 17: - return "bright coloured fish"; - case 18: - return "illustration"; - case 19: - return "band/artist logotype"; - case 20: - return "publisher/studio logotype"; - default: - return "other"; - } -} - -static char *cat_string(char *dest, const char *src, const char *sep) -{ - char *more = realloc(dest, strlen(dest) + strlen(src) + strlen(sep) + 1); - if (!more) { - free(dest); - return NULL; - } - return strcat(strcat(more, sep), src); -} - -static const char *codec_name(ogg_page *og) -{ - typedef struct { - const size_t length; - const char *codec; - const char *magic; - } codec_t; - const codec_t codecs[] = { - {.codec = OPUSNAME, .length = 19, .magic = "OpusHead"}, - {.codec = VORBISNAME, .length = 30, .magic = "\1vorbis"}, - {.codec = FLACNAME, .length = 47, .magic = "\177FLAC"}, - {.codec = "Speex", .length = 80, .magic = "Speex "}, - {.codec = "Celt", .length = 80, .magic = "CELT"}, // obsolete - {.codec = "MIDI", .length = 13, .magic = "OggMIDI\0"}, - {.codec = "PCM", .length = 28, .magic = "PCM "}, - {.codec = "Theora", .length = 42, .magic = "\200theora"}, - {.codec = "Daala", .length = 38, .magic = "\200daala"}, - {.codec = "Dirac", .length = 5, .magic = "BBCD\0"}, - {.codec = "Skeleton", .length = 80, .magic = "fishead\0"}, - {.codec = "Kate", .length = 64, .magic = "\200kate\0\0\0"}, - {.codec = "CMML", .length = 29, .magic = "CMML\0\0\0\0"}, - {.codec = "YUV4MPEG", .length = 8, .magic = "YUV4Mpeg"}, - {.codec = "UVS", .length = 48, .magic = "UVS "}, - {.codec = "YUV", .length = 32, .magic = "\0YUV"}, - {.codec = "RGB", .length = 24, .magic = "\0RGB"}, - {.codec = "JNG", .length = 48, .magic = "\213JNG\r\n\032\n"}, - {.codec = "MNG", .length = 48, .magic = "\212MNG\r\n\032\n"}, - {.codec = "PNG", .length = 48, .magic = "\211PNG\r\n\032\n"}, - {.codec = "Spots", .length = 52, .magic = "SPOTS\0\0\0"}, - {.codec = NULL} - }; - - for (const codec_t *match = codecs; match->codec; match++) - if ((size_t)og->body_len >= match->length && !memcmp(og->body, match->magic, strlen(match->codec))) - return match->codec; - - return "unknown"; -} - -static char *btoa(const unsigned char *binary, const size_t binary_length) -{ - const char b64[64] = { - 'A','B','C','D','E','F','G','H','I','J','K','L','M','N','O','P','Q','R','S','T','U','V','W','X','Y','Z', - 'a','b','c','d','e','f','g','h','i','j','k','l','m','n','o','p','q','r','s','t','u','v','w','x','y','z', - '0','1','2','3','4','5','6','7','8','9','+','/' - }; - - char *ascii_base64 = malloc((binary_length-1)/3 * 4 + 5); - if (!ascii_base64) - return NULL; - - char *p = ascii_base64; - const unsigned char *q = binary; - const unsigned char *binary_end = binary + binary_length; - while (q+2 < binary_end) { - uint32_t chunk = q[0]<<16 | q[1]<<8 | q[2]; - p[3] = b64[chunk & 0x3f]; - p[2] = b64[chunk>>6 & 0x3f]; - p[1] = b64[chunk>>12 & 0x3f]; - p[0] = b64[chunk>>18 & 0x3f]; - p+=4, q+=3; - } - - if (q < binary_end) { - uint16_t remainder = q[0]<<8 | (q+1 == binary_end ? '\0' : q[1]); - p[3] = '='; - p[2] = q+1 == binary_end ? '=' : b64[remainder<<2 & 0x3f]; - p[1] = b64[remainder>>4 & 0x3f]; - p[0] = b64[remainder>>10 & 0x3f]; - p+=4; - } - *p = '\0'; - - return ascii_base64; -} - -static void _oggpack_chars(oggpack_buffer *opb, const char *s, size_t length) -{ -// oggpack_writecopy(opb, s, length * 8); currently broken - while (length--) - oggpack_write(opb, *s++, 8); -} - -static void _oggpack_string(oggpack_buffer *opb, const char *s) -{ - oggpack_write(opb, strlen(s), 32); - _oggpack_chars(opb, s, strlen(s)); -} - -static void _oggpackB_string(oggpack_buffer *opb, const char *s) -{ - oggpackB_write(opb, strlen(s), 32); - _oggpack_chars(opb, s, strlen(s)); -} - -char *oggedit_album_art_tag(DB_FILE *fp, int *res) -{ - if (!fp) { - *res = OGGEDIT_FILE_NOT_OPEN; - return NULL; - } - - const int64_t data_length = fp->vfs->getlength(fp); - if (data_length < 50 || data_length > 10000000) { - fp->vfs->close(fp); - *res = OGGEDIT_BAD_FILE_LENGTH; - return NULL; - } - - char *data = malloc(data_length); - if (!data) { - fp->vfs->close(fp); - *res = OGGEDIT_ALLOCATION_FAILURE; - return NULL; - } - - const size_t data_read = fp->vfs->read(data, 1, data_length, fp); - fp->vfs->close(fp); - if (data_read != data_length) { - free(data); - *res = OGGEDIT_CANT_READ_IMAGE_FILE; - return NULL; - } - - oggpack_buffer opb; - oggpackB_writeinit(&opb); - oggpackB_write(&opb, 3, 32); - _oggpackB_string(&opb, memcmp(data, "\x89PNG\x0D\x0A\x1A\x0A", 8) ? "image/jpeg" : "image/png"); - _oggpackB_string(&opb, "Album art added from DeaDBeeF"); - oggpackB_write(&opb, 0, 32); - oggpackB_write(&opb, 0, 32); - oggpackB_write(&opb, 0, 32); - oggpackB_write(&opb, 0, 32); - oggpackB_write(&opb, data_length, 32); - _oggpack_chars(&opb, data, data_length); - free(data); - if (oggpack_writecheck(&opb)) { - *res = OGGEDIT_ALLOCATION_FAILURE; - return NULL; - } - - char *tag = btoa(oggpackB_get_buffer(&opb), oggpackB_bytes(&opb)); - oggpackB_writeclear(&opb); - return tag; -} - -static bool ensure_directory(const char *path) -{ - struct stat stat_struct; - if (!stat(path, &stat_struct)) - return !S_ISDIR(stat_struct.st_mode); - - if (errno != ENOENT) - return false; - - char* dir = strdup(path); - if (!dir) - return false; - - const int bad_dir = ensure_directory(dirname(dir)); - free(dir); - - return !bad_dir && !mkdir(path, 0777); -} - -static int finish_temp_file(const char *tempname, const char *fname) -{ - return rename(tempname, fname) ? OGGEDIT_RENAME_FAILED : 0; -} - -static int open_temp_file(const char *fname, char *tempname, FILE **out) -{ - snprintf(tempname, PATH_MAX, "%s.temp", fname); - unlink(tempname); - if (!(*out = freopen(tempname, "abx", *out))) - return OGGEDIT_CANNOT_OPEN_TEMPORARY_FILE; - - struct stat stat_struct; - if (!stat(fname, &stat_struct)) - chmod(tempname, stat_struct.st_mode); - - return 0; -} - -static FILE *open_new_file(const char *outname) -{ - char outpath[PATH_MAX]; - strcpy(outpath, outname); - if (!ensure_directory(dirname(outpath))) - return NULL; - - unlink(outname); - return fopen(outname, "wbx"); -} - -static off_t file_size(const char *fname) -{ - struct stat sb; - if (stat(fname, &sb)) - return OGGEDIT_STAT_FAILED; - - return sb.st_size; -} - -static void cleanup(DB_FILE *in, FILE *out, ogg_sync_state *oy, void *buffer) -{ - if (in) - in->vfs->close(in); - - if (out) - fclose(out); - - ogg_sync_clear(oy); - - if (buffer) - free(buffer); -} - -static int get_page(DB_FILE *in, ogg_sync_state *oy, ogg_page *og) -{ - uint16_t chunks_left = MAXPAGE / CHUNKSIZE; - while (ogg_sync_pageout(oy, og) != 1) { - char *buffer = ogg_sync_buffer(oy, CHUNKSIZE); - if (!in || !buffer || !chunks_left--) - return OGGEDIT_CANT_FIND_STREAM; - - const size_t bytes = in->vfs->read(buffer, 1, CHUNKSIZE, in); - if (!bytes) - return OGGEDIT_EOF; - - ogg_sync_wrote(oy, bytes); - } - - return ogg_page_serialno(og); -} - -static int skip_to_bos(DB_FILE *in, ogg_sync_state *oy, ogg_page *og, const off_t offset) -{ - if (!in) - return OGGEDIT_FILE_NOT_OPEN; - - if (in->vfs->seek(in, offset, SEEK_SET)) - return OGGEDIT_SEEK_FAILED; - - ogg_sync_reset(oy); - int serial; - do - serial = get_page(in, oy, og); - while (serial > OGGEDIT_EOF && !ogg_page_bos(og)); - - return serial; -} - -static int skip_to_codec(DB_FILE *in, ogg_sync_state *oy, ogg_page *og, const off_t offset, const char *codec) -{ - int serial = skip_to_bos(in, oy, og, offset); - while (serial > OGGEDIT_EOF && strcmp(codec_name(og), codec)) - serial = get_page(in, oy, og); - - return serial; -} - -static int skip_to_header(DB_FILE *in, ogg_sync_state *oy, ogg_page *og, int serial, const int codec_serial) -{ - while (serial > OGGEDIT_EOF && (ogg_page_bos(og) || serial != codec_serial)) - serial = get_page(in, oy, og); - - return serial; -} - -static off_t sync_tell(DB_FILE *in, ogg_sync_state *oy, ogg_page *og) -{ - return in->vfs->tell(in) - oy->fill + oy->returned - og->header_len - og->body_len; -} - -static bool write_page(FILE *out, ogg_page *og) -{ - if (fwrite(og->header, 1, og->header_len, out) != (size_t)og->header_len) - return false; - - if (fwrite(og->body, 1, og->body_len, out) != (size_t)og->body_len) - return false; - - return true; -} - -static int write_page_and_get_next(DB_FILE *in, FILE *out, ogg_sync_state *oy, ogg_page *og) -{ - if (!write_page(out, og)) - return OGGEDIT_WRITE_ERROR; - - return get_page(in, oy, og); -} - -static int copy_up_to_codec(DB_FILE *in, FILE *out, ogg_sync_state *oy, ogg_page *og, const off_t start_offset, const off_t link_offset, const char *codec) -{ - int serial = skip_to_bos(in, oy, og, start_offset); - - if (fseek(out, sync_tell(in, oy, og), SEEK_SET)) - return OGGEDIT_SEEK_FAILED; - - while (serial > OGGEDIT_EOF && (sync_tell(in, oy, og) < link_offset || !ogg_page_bos(og) || strcmp(codec_name(og), codec))) - serial = write_page_and_get_next(in, out, oy, og); - - return serial; -} - -static int copy_up_to_header(DB_FILE *in, FILE *out, ogg_sync_state *oy, ogg_page *og, const int codec_serial) -{ - int serial; - do - serial = write_page_and_get_next(in, out, oy, og); - while (serial > OGGEDIT_EOF && serial != codec_serial); - return serial; -} - -static long flush_stream(FILE *out, ogg_stream_state *os) -{ - ogg_page og; - while (ogg_stream_flush_fill(os, &og, MAXPAYLOAD)) - if (!write_page(out, &og)) - return OGGEDIT_WRITE_ERROR; - const long pageno = ogg_stream_check(os) ? OGGEDIT_FLUSH_FAILED : ogg_page_pageno(&og); - ogg_stream_clear(os); - return pageno; -} - -static char *codec_names(DB_FILE *in, ogg_sync_state *oy, const off_t link_offset, int *res) -{ - ogg_page og; - *res = skip_to_bos(in, oy, &og, link_offset); - char *codecs = strdup("Ogg"); - while (codecs && *res > OGGEDIT_EOF && ogg_page_bos(&og)) { - codecs = cat_string(codecs, codec_name(&og), strcmp(codecs, "Ogg") ? "/" : " "); - *res = get_page(in, oy, &og); - } - if (!*codecs) { - *res = OGGEDIT_ALLOCATION_FAILURE; - return NULL; - } - if (*res <= OGGEDIT_EOF) { - free(codecs); - return NULL; - } - - *res = OGGEDIT_OK; - return codecs; -} - -static off_t codec_stream_size(DB_FILE *in, ogg_sync_state *oy, const off_t start_offset, const off_t end_offset, const char *codec) -{ - /* Find codec serial and any other codecs */ - bool multiplex = false; - ogg_page og; - int codec_serial = -1; - int serial = skip_to_bos(in, oy, &og, start_offset); - while (serial > OGGEDIT_EOF && ogg_page_bos(&og)) { - if (strcmp(codec_name(&og), codec)) - multiplex = true; - else - codec_serial = serial; - serial = get_page(in, oy, &og); - } - - /* Skip to the first codec data page */ - while (serial > OGGEDIT_EOF && !(ogg_page_granulepos(&og) > 0 && serial == codec_serial)) - if ((serial = get_page(in, oy, &og)) <= OGGEDIT_EOF) - return serial; - - off_t stream_size = 0; - if (multiplex) { - /* Add up all the codec stream pages until EOF or a new link */ - while (serial > OGGEDIT_EOF && !ogg_page_bos(&og)) { - if (serial == codec_serial) - stream_size += og.header_len + og.body_len; - serial = get_page(in, oy, &og); - } - } - else { - /* Find the exact offsets of the start and end of the audio */ - stream_size -= sync_tell(in, oy, &og); - if (in->vfs->seek(in, end_offset, end_offset == 0 ? SEEK_END : SEEK_SET)) - return OGGEDIT_SEEK_FAILED; - stream_size += in->vfs->tell(in); - ogg_sync_reset(oy); - while ((serial = get_page(in, oy, &og)) > OGGEDIT_EOF && !ogg_page_bos(&og)) - stream_size += og.header_len + og.body_len; - } - if (serial < OGGEDIT_EOF) - return serial; - - return stream_size; -} - -static char *parse_vendor(const ogg_packet *op, const size_t magic_length) -{ - if (op->bytes < magic_length + 4) - return NULL; - - const uint8_t *p = op->packet + magic_length; - const uint32_t vendor_length = *p | *(p+1)<<8 | *(p+2)<<16 | *(p+3)<<24; - if (op->bytes < magic_length + 4 + vendor_length) - return NULL; - - char *vendor = calloc(vendor_length+1, 1); - if (vendor) - memcpy(vendor, p+4, vendor_length); - return vendor; -} - -static int init_read_stream(DB_FILE *in, ogg_sync_state *oy, ogg_stream_state *os, ogg_page *og, const off_t offset, const char *codec) -{ - int serial = skip_to_codec(in, oy, og, offset, codec); - serial = skip_to_header(in, oy, og, serial, serial); - if (serial <= OGGEDIT_EOF) - return serial; - - if (ogg_stream_init(os, serial)) - return OGGEDIT_FAILED_TO_INIT_STREAM; - - os->b_o_s = 1; - ogg_stream_pagein(os, og); - - return OGGEDIT_OK; -} - -static int read_packet(DB_FILE *in, ogg_sync_state *oy, ogg_stream_state *os, ogg_page *og, ogg_packet *header, int pages) -{ - ogg_packet op; - do { - while (ogg_stream_packetpeek(os, NULL) == 0) { - const int serial = get_page(in, oy, og); - if (serial <= OGGEDIT_EOF) { - return serial; - } - if (os->serialno == serial) { - pages++; - ogg_stream_pagein(os, og); - } - } - if (ogg_stream_check(os)) - return OGGEDIT_FAILED_TO_STREAM_PAGE_FOR_PACKET; - } while (ogg_stream_packetout(os, &op) != 1); - - memset(header, '\0', sizeof(*header)); - if (!header || !(header->packet = malloc(op.bytes))) { - free(header); - return OGGEDIT_ALLOCATION_FAILURE; - } - - header->bytes = op.bytes; - memcpy(header->packet, op.packet, op.bytes); - return pages; -} - -static ogg_packet *fill_vc_packet(const char *magic, const size_t magic_length, const char *vendor, const size_t num_tags, char **tags, const bool framing, const size_t padding, ogg_packet *op) -{ - oggpack_buffer opb; - oggpack_writeinit(&opb); - _oggpack_chars(&opb, magic, magic_length); - _oggpack_string(&opb, vendor); - oggpack_write(&opb, num_tags, 32); - for (size_t i = 0; i < num_tags; i++) - _oggpack_string(&opb, tags[i]); - if (framing) { - oggpack_write(&opb, 1, 1); - oggpack_writealign(&opb); - } - for (size_t i = 0; i < padding; i++) - oggpack_write(&opb, 0, 8); // use oggpack_writecopy when it is fixed - if (oggpack_writecheck(&opb)) - return NULL; - - if (op) { - memset(op, '\0', sizeof(*op)); - op->bytes = oggpack_bytes(&opb); - if (op->packet = malloc(op->bytes)) - memcpy(op->packet, oggpack_get_buffer(&opb), op->bytes); - } - oggpack_writeclear(&opb); - - if (!op->packet) - return NULL; - - return op; -} - -static size_t vc_size(const char *vendor, size_t num_tags, char **tags) -{ - size_t metadata_size = 4 + strlen(vendor) + 4; - for (size_t i = 0; i < num_tags; i++) - metadata_size += strlen(tags[i]) + 4; - return metadata_size; -} - -static int copy_remaining_pages(DB_FILE *in, FILE *out, ogg_sync_state *oy, const int codec_serial, uint32_t pageno) -{ - /* Skip past the codec header packets */ - ogg_page og; - int serial; - do - serial = get_page(in, oy, &og); - while (serial > OGGEDIT_EOF && serial == codec_serial && ogg_page_granulepos(&og) <= 0); - - /* Renumber the rest of this link */ - while (serial > OGGEDIT_EOF && !ogg_page_bos(&og)) { - if (serial == codec_serial && ogg_page_pageno(&og) != ++pageno) { - og.header[18] = pageno & 0xFF; - og.header[19] = pageno >> 8 & 0xFF; - og.header[20] = pageno >> 16 & 0xFF; - og.header[21] = pageno >> 24 & 0xFF; - ogg_page_checksum_set(&og); - } - serial = write_page_and_get_next(in, out, oy, &og); - } - - /* Blindly copy remaining links */ - while (serial > OGGEDIT_EOF) - serial = write_page_and_get_next(in, out, oy, &og); - - return serial; -} - -static int write_all_streams(DB_FILE *in, FILE *out, ogg_sync_state *oy, const off_t offset) -{ - /* Copy BOS page(s) */ - ogg_page og; - int serial = skip_to_bos(in, oy, &og, offset); - while (serial > OGGEDIT_EOF && ogg_page_bos(&og)) - serial = write_page_and_get_next(in, out, oy, &og); - if (serial <= OGGEDIT_EOF) - return serial; - - /* Copy all pages until EOF or next link */ - while (serial > OGGEDIT_EOF && !ogg_page_bos(&og)) - if ((serial = write_page_and_get_next(in, out, oy, &og)) < OGGEDIT_EOF) - return serial; - - return OGGEDIT_OK; -} - -static int write_one_stream(DB_FILE *in, FILE *out, ogg_sync_state *oy, const off_t offset, const char *codec) -{ - /* Find codec BOS page */ - ogg_page og; - const int codec_serial = skip_to_codec(in, oy, &og, offset, codec); - if (codec_serial <= OGGEDIT_EOF) - return codec_serial; - - /* Write it and skip the other BOS pages */ - int serial = write_page_and_get_next(in, out, oy, &og); - if ((serial = skip_to_header(in, oy, &og, serial, codec_serial)) <= OGGEDIT_EOF) - return serial; - - /* Copy all codec pages until EOF or next link */ - while (serial > OGGEDIT_EOF && !ogg_page_bos(&og)) { - if (serial == codec_serial && !write_page(out, &og)) - return OGGEDIT_WRITE_ERROR; - serial = get_page(in, oy, &og); - } - if (serial < OGGEDIT_EOF) - return serial; - - return OGGEDIT_OK; -} - -int oggedit_write_file(DB_FILE *in, const char *outname, const off_t offset, const char *codec) -{ - FILE *out = open_new_file(outname); - if (!out) - return OGGEDIT_CANNOT_OPEN_OUTPUT_FILE; - - ogg_sync_state oy; - ogg_sync_init(&oy); - - int res; - if (codec) - res = write_one_stream(in, out, &oy, offset, codec); - else - res = write_all_streams(in, out, &oy, offset); - - cleanup(in, out, &oy, NULL); - - if (res <= OGGEDIT_EOF) - unlink(outname); - - return res; -} - -off_t oggedit_stream_info(DB_FILE *in, const off_t start_offset, const off_t end_offset, const char *codec, char **codecs) -{ - int res; - ogg_sync_state oy; - ogg_sync_init(&oy); - *codecs = codec_names(in, &oy, start_offset, &res); - const off_t stream_size = codec_stream_size(in, &oy, start_offset, end_offset, codec); - cleanup(in, NULL, &oy, NULL); - return stream_size; -} - -/**************************** - * FLAC - ****************************/ -#define PADTYPE 0x01 -#define VCTYPE 0x04 -#define FLACLAST 0x80 - -static void clear_header_list(ogg_packet **headers) -{ - if (headers) { - for (ogg_packet **header = headers; *header; header++) { - ogg_packet_clear(*header); - free(*header); - } - free(headers); - } -} - -static ogg_packet **flac_headers_alloc(ogg_packet **headers, const size_t packets) -{ - ogg_packet **new_headers = realloc(headers, (packets+2) * sizeof(headers)); - if (!new_headers) { - clear_header_list(headers); - return NULL; - } - - if (!(new_headers[packets] = malloc(sizeof(ogg_packet)))) { - clear_header_list(new_headers); - return NULL; - } - - new_headers[packets + 1] = NULL; - return new_headers; -} - -static ogg_packet **metadata_block_packets(DB_FILE *in, ogg_sync_state *oy, const off_t offset, char **vendor, int *res) -{ - ogg_stream_state os; - ogg_page og; - if ((*res = init_read_stream(in, oy, &os, &og, offset, FLACNAME)) <= OGGEDIT_EOF) - return NULL; - - int pages = 1; - size_t packets = 0; - ogg_packet **headers = NULL; - while ((headers = flac_headers_alloc(headers, packets)) && - (pages = read_packet(in, oy, &os, &og, headers[packets], pages)) > OGGEDIT_EOF && - (headers[packets++]->packet[0] & FLACLAST) != FLACLAST); - - ogg_stream_clear(&os); - - if (!headers) - *res = OGGEDIT_ALLOCATION_FAILURE; - else if (!packets || headers[0]->packet[0] & 0x3F != VCTYPE) - *res = OGGEDIT_CANNOT_PARSE_HEADERS; - else - *res = pages; - if (*res <= OGGEDIT_EOF) { - clear_header_list(headers); - return NULL; - } - - *vendor = parse_vendor(headers[0], 4); - size_t bytes = 0; - for (size_t i = 0; i < packets; i++) - bytes += headers[i]->bytes; - if (bytes < MAXPAYLOAD * (pages-1)) - headers[0]->bytes = 4; - - *res = OGGEDIT_OK; - return headers; -} - -static long write_metadata_block_packets(FILE *out, const int serial, const char *vendor, const size_t num_tags, char **tags, size_t padding, ogg_packet **metadata) -{ - const size_t header_length = vc_size(vendor, num_tags, tags); - if (header_length > (2<<24)) - return OGGEDIT_ALLOCATION_FAILURE; - - char magic[4]; - magic[0] = VCTYPE; - magic[1] = header_length >> 16 & 0xFF; - magic[2] = header_length >> 8 & 0xFF; - magic[3] = header_length & 0xFF; - ogg_packet_clear(metadata[0]); - if (!fill_vc_packet(magic, 4, vendor, num_tags, tags, false, padding, metadata[0])) - return OGGEDIT_ALLOCATION_FAILURE; - - ogg_stream_state os; - if (ogg_stream_init(&os, serial)) - return OGGEDIT_FAILED_TO_INIT_STREAM; - os.b_o_s = 1; - os.pageno = 1; - - for (int i = 0; metadata[i]; i++) { - if (!metadata[i+1]) - metadata[i]->packet[0] |= FLACLAST; - ogg_stream_packetin(&os, metadata[i]); - } - - return flush_stream(out, &os); -} - -off_t oggedit_write_flac_metadata(DB_FILE *in, const char *fname, const off_t offset, const int num_tags, char **tags) -{ - off_t res; - char tempname[PATH_MAX] = ""; - ogg_packet **headers = NULL; - char *vendor = NULL; - ogg_sync_state oy; - ogg_sync_init(&oy); - - /* Original file must be writable whichever way we update it */ - FILE *out = fopen(fname, "r+b"); - if (!out) { - res = OGGEDIT_CANNOT_UPDATE_FILE; - goto cleanup; - } - - /* See if we can write the tags packet directly into the existing file ... */ - if (!(headers = metadata_block_packets(in, &oy, offset, &vendor, (int *)&res))) - goto cleanup; - const off_t stream_size_k = in->vfs->getlength(in) / 1000; // use file size for now - const size_t metadata_size = 4 + vc_size(vendor, num_tags, tags); - ptrdiff_t padding = headers[0]->bytes - metadata_size; - if (stream_size_k < 1000 || padding < 0 || headers[1] && padding > 0 || padding > stream_size_k+metadata_size) - if (res = open_temp_file(fname, tempname, &out)) - goto cleanup; - - /* Re-pad if writing the whole file */ - if (*tempname) { - size_t i = 0, j = 0; - while (headers[i]) { - headers[j++] = headers[i]; - while (headers[++i] && (headers[i]->packet[0] & 0x3F) == PADTYPE) { - ogg_packet_clear(headers[i]); - free(headers[i]); - } - } - headers[j] = NULL; - padding = headers[1] || stream_size_k < 900 ? 0 : stream_size_k < 10000 ? 1024 : stream_size_k < 100000 ? 8192 : 65536; - } - - /* Write pages until we reach the correct comment header */ - ogg_page og; - const int flac_serial = copy_up_to_codec(in, out, &oy, &og, *tempname ? 0 : offset, offset, FLACNAME); - if (flac_serial <= OGGEDIT_EOF) { - res = flac_serial; - goto cleanup; - } - if ((res = copy_up_to_header(in, out, &oy, &og, flac_serial)) <= OGGEDIT_EOF) - goto cleanup; - const long pageno = write_metadata_block_packets(out, flac_serial, vendor, num_tags, tags, padding, headers); - if (pageno < OGGEDIT_EOF) { - res = pageno; - goto cleanup; - } - - /* If we have tempfile, copy the remaining pages */ - if (*tempname) - if ((res = copy_remaining_pages(in, out, &oy, flac_serial, pageno)) < OGGEDIT_EOF || (res = finish_temp_file(tempname, fname))) - goto cleanup; - - res = file_size(fname); - -cleanup: - clear_header_list(headers); - cleanup(in, out, &oy, vendor); - if (res < OGGEDIT_OK) - unlink(tempname); - return res; -} - -/**************************** - * Vorbis - ****************************/ -#define VCMAGIC "\3vorbis" -#define CODEMAGIC "\5vorbis" - -static ptrdiff_t check_vorbis_headers(DB_FILE *in, ogg_sync_state *oy, const off_t offset, char **vendor, ogg_packet *codebooks) -{ - ogg_stream_state os; - ogg_page og; - const int serial = init_read_stream(in, oy, &os, &og, offset, VORBISNAME); - if (serial <= OGGEDIT_EOF) - return serial; - - ogg_packet vc; - int pages = read_packet(in, oy, &os, &og, &vc, 1); - if (pages > OGGEDIT_EOF) - pages = read_packet(in, oy, &os, &og, codebooks, pages); - ogg_stream_clear(&os); - if (pages <= OGGEDIT_EOF) - return pages; - - if (vc.bytes > strlen(VCMAGIC) && !memcmp(vc.packet, VCMAGIC, strlen(VCMAGIC)) && - codebooks->bytes > strlen(CODEMAGIC) && !memcmp(codebooks->packet, CODEMAGIC, strlen(CODEMAGIC))) - *vendor = parse_vendor(&vc, strlen(VCMAGIC)); - free(vc.packet); - if (!*vendor) - return OGGEDIT_CANNOT_PARSE_HEADERS; - - if ((vc.bytes + codebooks->bytes) < MAXPAYLOAD * (pages-1)) - return 4; // prevent in-place write if the packets are split over too many pages - - return vc.bytes; -} - -static long write_vorbis_tags(FILE *out, const int serial, const char *vendor, const size_t num_tags, char **tags, const size_t padding, ogg_packet *codebooks) -{ - ogg_packet op; - if (!fill_vc_packet(VCMAGIC, strlen(VCMAGIC), vendor, num_tags, tags, true, padding, &op)) - return OGGEDIT_ALLOCATION_FAILURE; - - ogg_stream_state os; - if (ogg_stream_init(&os, serial)) - return OGGEDIT_FAILED_TO_INIT_STREAM; - os.b_o_s = 1; - os.pageno = 1; - ogg_stream_packetin(&os, &op); - ogg_stream_packetin(&os, codebooks); - - ogg_packet_clear(&op); - return flush_stream(out, &os); -} - -off_t oggedit_write_vorbis_metadata(DB_FILE *in, const char *fname, const off_t offset, const size_t stream_size, const int num_tags, char **tags) -{ - off_t res; - char tempname[PATH_MAX] = ""; - char *vendor = NULL; - ogg_packet codebooks; - memset(&codebooks, '\0', sizeof(codebooks)); - ogg_sync_state oy; - ogg_sync_init(&oy); - - /* Original file must be writable whichever way we update it */ - FILE *out = fopen(fname, "r+b"); - if (!out) { - res = OGGEDIT_CANNOT_UPDATE_FILE; - goto cleanup; - } - - /* See if we can write the tags packet directly into the existing file ... */ - const ptrdiff_t tags_packet_size = check_vorbis_headers(in, &oy, offset, &vendor, &codebooks); - if (tags_packet_size <= OGGEDIT_EOF) { - res = tags_packet_size; - goto cleanup; - } - const size_t metadata_size = strlen(VCMAGIC) + vc_size(vendor, num_tags, tags) + 1; - ptrdiff_t padding = tags_packet_size - metadata_size; - const off_t file_size_k = in->vfs->getlength(in) / 1000; - const size_t stream_size_k = stream_size ? stream_size / 1000 : file_size_k; - if (file_size_k < 100 || padding < 0 || padding > file_size_k/10+stream_size_k+metadata_size) - if (res = open_temp_file(fname, tempname, &out)) - goto cleanup; - - /* Re-pad if writing the whole file */ - if (*tempname) - padding = stream_size_k < 90 ? 0 : stream_size_k < 1000 ? 128 : stream_size_k < 10000 ? 1024 : 8192; - - /* Write pages until the correct comment header */ - ogg_page og; - const int vorbis_serial = copy_up_to_codec(in, out, &oy, &og, *tempname ? 0 : offset, offset, VORBISNAME); - if (vorbis_serial <= OGGEDIT_EOF) { - res = vorbis_serial; - goto cleanup; - } - if ((res = copy_up_to_header(in, out, &oy, &og, vorbis_serial)) <= OGGEDIT_EOF) - goto cleanup; - const long pageno = write_vorbis_tags(out, vorbis_serial, vendor, num_tags, tags, padding, &codebooks); - if (pageno < OGGEDIT_EOF) { - res = pageno; - goto cleanup; - } - - /* If we have tempfile, copy the remaining pages */ - if (*tempname) - if ((res = copy_remaining_pages(in, out, &oy, vorbis_serial, pageno)) < OGGEDIT_EOF || (res = finish_temp_file(tempname, fname))) - goto cleanup; - - res = file_size(fname); - -cleanup: - ogg_packet_clear(&codebooks); - cleanup(in, out, &oy, vendor); - if (res < OGGEDIT_OK) - unlink(tempname); - return res; -} - -/**************************** - * Opus - ****************************/ -#define TAGMAGIC "OpusTags" - -static ptrdiff_t check_opus_header(DB_FILE *in, ogg_sync_state *oy, const off_t offset, char **vendor) -{ - ogg_stream_state os; - ogg_page og; - const int serial = init_read_stream(in, oy, &os, &og, offset, OPUSNAME); - if (serial <= OGGEDIT_EOF) - return serial; - - ogg_packet op; - const int pages = read_packet(in, oy, &os, &og, &op, 1); - ogg_stream_clear(&os); - if (pages <= OGGEDIT_EOF) - return pages; - - if (op.bytes > strlen(TAGMAGIC) && !memcmp(op.packet, TAGMAGIC, strlen(TAGMAGIC))) - *vendor = parse_vendor(&op, strlen(TAGMAGIC)); - free(op.packet); - if (!*vendor) - return OGGEDIT_CANNOT_PARSE_HEADERS; - - if (op.bytes < MAXPAYLOAD * (pages-1)) - return 4; // prevent in-place write if the packet is weirdly split into too many pages - - return op.bytes; -} - -static long write_opus_tags(FILE *out, const int serial, const char *vendor, const size_t num_tags, char **tags, const size_t padding) -{ - ogg_packet op; - if (!fill_vc_packet(TAGMAGIC, strlen(TAGMAGIC), vendor, num_tags, tags, false, padding, &op)) - return OGGEDIT_ALLOCATION_FAILURE; - - ogg_stream_state os; - if (ogg_stream_init(&os, serial)) - return OGGEDIT_FAILED_TO_INIT_STREAM; - os.b_o_s = 1; - os.pageno = 1; - ogg_stream_packetin(&os, &op); - - ogg_packet_clear(&op); - return flush_stream(out, &os); -} - -off_t oggedit_write_opus_metadata(DB_FILE *in, const char *fname, const off_t offset, const size_t stream_size, const int output_gain, const int num_tags, char **tags) -{ - off_t res; - char tempname[PATH_MAX] = ""; - char *vendor = NULL; - ogg_sync_state oy; - ogg_sync_init(&oy); - - /* Original file must be writable whichever way we update it */ - FILE *out = fopen(fname, "r+b"); - if (!out) { - res = OGGEDIT_CANNOT_UPDATE_FILE; - goto cleanup; - } - - /* Should we write the tags packet directly into the existing file ... */ - const ptrdiff_t tags_packet_size = check_opus_header(in, &oy, offset, &vendor); - if (tags_packet_size <= OGGEDIT_EOF) { - res = tags_packet_size; - goto cleanup; - } - const size_t metadata_size = strlen(TAGMAGIC) + vc_size(vendor, num_tags, tags); - ptrdiff_t padding = tags_packet_size - metadata_size; - const off_t file_size_k = in->vfs->getlength(in) / 1000; - const size_t stream_size_k = stream_size ? stream_size / 1000 : file_size_k; - if (file_size_k < 100 || padding < 0 || padding > file_size_k/10+stream_size_k+metadata_size) - if (res = open_temp_file(fname, tempname, &out)) - goto cleanup; - - /* Re-pad if writing the whole file */ - if (*tempname) - padding = stream_size_k < 90 ? 0 : stream_size_k < 1000 ? 128 : stream_size_k < 10000 ? 1024 : 8192; - - /* Write pages until we reach the correct OpusHead, then write OpusTags */ - ogg_page og; - const int opus_serial = copy_up_to_codec(in, out, &oy, &og, *tempname ? 0 : offset, offset, OPUSNAME); - if (opus_serial <= OGGEDIT_EOF) { - res = opus_serial; - goto cleanup; - } - if (output_gain > INT_MIN) { - og.body[16] = output_gain & 0xFF; - og.body[17] = output_gain >> 8 & 0xFF; - ogg_page_checksum_set(&og); - } - if ((res = copy_up_to_header(in, out, &oy, &og, opus_serial)) <= OGGEDIT_EOF) - goto cleanup; - const long pageno = write_opus_tags(out, opus_serial, vendor, num_tags, tags, (size_t)padding); - if (pageno < OGGEDIT_EOF) { - res = pageno; - goto cleanup; - } - - /* If we have tempfile, copy the remaining pages */ - if (*tempname) - if ((res = copy_remaining_pages(in, out, &oy, opus_serial, pageno)) < OGGEDIT_EOF || (res = finish_temp_file(tempname, fname))) - goto cleanup; - - res = file_size(fname); - -cleanup: - cleanup(in, out, &oy, vendor); - if (res < OGGEDIT_OK) - unlink(tempname); - return res; -} -/* -struct timeval timeval; -gettimeofday(&timeval, NULL); -int usecs = timeval.tv_sec* 1000000 + timeval.tv_usec; -gettimeofday(&timeval, NULL); -usecs = timeval.tv_sec* 1000000 + timeval.tv_usec - usecs; -fprintf(stderr, "%d micro-seconds\n", usecs); -*/ diff --git a/plugins/liboggedit/oggedit.h b/plugins/liboggedit/oggedit.h index 29ca7d13..b0a4d9b0 100644 --- a/plugins/liboggedit/oggedit.h +++ b/plugins/liboggedit/oggedit.h @@ -27,10 +27,6 @@ #ifndef __OGGEDIT_H #define __OGGEDIT_H -#define OPUSNAME "Opus" -#define VORBISNAME "Vorbis" -#define FLACNAME "Flac" - #define ALBUM_ART_KEY "METADATA_BLOCK_PICTURE" #define ALBUM_ART_META "metadata_block_picture" @@ -74,16 +70,24 @@ /* Cannot read data from image file */ #define OGGEDIT_CANT_READ_IMAGE_FILE -101 +/* oggedit_utils.c */ uint8_t *oggedit_vorbis_channel_map(const int channel_count); const char *oggedit_map_tag(char *key, const char *in_or_out); + +/* oggedit_art.c */ const char *oggedit_album_art_type(const int type); char *oggedit_album_art_tag(DB_FILE *fp, int *res); -int oggedit_write_file(DB_FILE *in, const char *outname, const off_t offset, const char *codec); -off_t oggedit_stream_info(DB_FILE *in, const off_t start_offset, const off_t end_offset, const char *codec, char **codecs); - +/* oggedit_flac.c */ off_t oggedit_write_flac_metadata(DB_FILE *in, const char *fname, const off_t offset, const int num_tags, char **tags); + +/* oggedit_vorbis.c */ +off_t oggedit_vorbis_stream_info(DB_FILE *in, const off_t start_offset, const off_t end_offset, char **codecs); off_t oggedit_write_vorbis_metadata(DB_FILE *in, const char *fname, const off_t offset, const size_t stream_size, const int num_tags, char **tags); + +/* oggedit_opus.c */ +int oggedit_write_opus_file(DB_FILE *in, const char *outname, const off_t offset, const bool all_streams); +off_t oggedit_opus_stream_info(DB_FILE *in, const off_t start_offset, const off_t end_offset, char **codecs); off_t oggedit_write_opus_metadata(DB_FILE *in, const char *fname, const off_t offset, const size_t stream_size, const int output_gain, const int num_tags, char **tags); #endif /* __OGGEDIT_H */ diff --git a/plugins/liboggedit/oggedit_art.c b/plugins/liboggedit/oggedit_art.c new file mode 100644 index 00000000..44e1c015 --- /dev/null +++ b/plugins/liboggedit/oggedit_art.c @@ -0,0 +1,172 @@ +/* + This file is part of Deadbeef Player source code + http://deadbeef.sourceforge.net + + DeaDBeeF Ogg Edit library album art functions + + Copyright (C) 2014 Ian Nartowicz <deadbeef@nartowicz.co.uk> + + This software is provided 'as-is', without any express or implied + warranty. In no event will the authors be held liable for any damages + arising from the use of this software. + + Permission is granted to anyone to use this software for any purpose, + including commercial applications, and to alter it and redistribute it + freely, subject to the following restrictions: + + 1. The origin of this software must not be misrepresented; you must not + claim that you wrote the original software. If you use this software + in a product, an acknowledgment in the product documentation would be + appreciated but is not required. + 2. Altered source versions must be plainly marked as such, and must not be + misrepresented as being the original software. + 3. This notice may not be removed or altered from any source distribution. + +*/ + +#ifdef HAVE_CONFIG_H +#include <config.h> +#endif +#include <stdlib.h> +#include <string.h> +#include <stdbool.h> +#include <ogg/ogg.h> +#include <deadbeef/deadbeef.h> +#include "oggedit_internal.h" +#include "oggedit.h" + +const char *oggedit_album_art_type(const int type) +{ + switch (type) { + case 1: + return "32x32 pixels file icon"; + case 2: + return "other file icon"; + case 3: + return "front cover"; + case 4: + return "back cover"; + case 5: + return "leaflet page"; + case 6: + return "media"; + case 7: + return "lead artist/lead performer/soloist"; + case 8: + return "artist/performer"; + case 9: + return "conductor"; + case 10: + return "band/orchestra"; + case 11: + return "composer"; + case 12: + return "lyricist/text writer"; + case 13: + return "recording location"; + case 14: + return "during recording"; + case 15: + return "during performance"; + case 16: + return "movie/video screen capture"; + case 17: + return "bright coloured fish"; + case 18: + return "illustration"; + case 19: + return "band/artist logotype"; + case 20: + return "publisher/studio logotype"; + default: + return "other"; + } +} + +static char *btoa(const unsigned char *binary, const size_t binary_length) +{ + const char b64[64] = { + 'A','B','C','D','E','F','G','H','I','J','K','L','M','N','O','P','Q','R','S','T','U','V','W','X','Y','Z', + 'a','b','c','d','e','f','g','h','i','j','k','l','m','n','o','p','q','r','s','t','u','v','w','x','y','z', + '0','1','2','3','4','5','6','7','8','9','+','/' + }; + + char *ascii_base64 = malloc((binary_length-1)/3 * 4 + 5); + if (!ascii_base64) + return NULL; + + char *p = ascii_base64; + const unsigned char *q = binary; + const unsigned char *binary_end = binary + binary_length; + while (q+2 < binary_end) { + uint32_t chunk = q[0]<<16 | q[1]<<8 | q[2]; + p[3] = b64[chunk & 0x3f]; + p[2] = b64[chunk>>6 & 0x3f]; + p[1] = b64[chunk>>12 & 0x3f]; + p[0] = b64[chunk>>18 & 0x3f]; + p+=4, q+=3; + } + + if (q < binary_end) { + uint16_t remainder = q[0]<<8 | (q+1 == binary_end ? '\0' : q[1]); + p[3] = '='; + p[2] = q+1 == binary_end ? '=' : b64[remainder<<2 & 0x3f]; + p[1] = b64[remainder>>4 & 0x3f]; + p[0] = b64[remainder>>10 & 0x3f]; + p+=4; + } + *p = '\0'; + + return ascii_base64; +} + +char *oggedit_album_art_tag(DB_FILE *fp, int *res) +{ + if (!fp) { + *res = OGGEDIT_FILE_NOT_OPEN; + return NULL; + } + + const int64_t data_length = fp->vfs->getlength(fp); + if (data_length < 50 || data_length > 10000000) { + fp->vfs->close(fp); + *res = OGGEDIT_BAD_FILE_LENGTH; + return NULL; + } + + char *data = malloc(data_length); + if (!data) { + fp->vfs->close(fp); + *res = OGGEDIT_ALLOCATION_FAILURE; + return NULL; + } + + const size_t data_read = fp->vfs->read(data, 1, data_length, fp); + fp->vfs->close(fp); + if (data_read != data_length) { + free(data); + *res = OGGEDIT_CANT_READ_IMAGE_FILE; + return NULL; + } + + oggpack_buffer opb; + oggpackB_writeinit(&opb); + oggpackB_write(&opb, 3, 32); + _oggpackB_string(&opb, memcmp(data, "\x89PNG\x0D\x0A\x1A\x0A", 8) ? "image/jpeg" : "image/png"); + _oggpackB_string(&opb, "Album art added from DeaDBeeF"); + oggpackB_write(&opb, 0, 32); + oggpackB_write(&opb, 0, 32); + oggpackB_write(&opb, 0, 32); + oggpackB_write(&opb, 0, 32); + oggpackB_write(&opb, data_length, 32); + _oggpack_chars(&opb, data, data_length); + free(data); + if (oggpack_writecheck(&opb)) { + *res = OGGEDIT_ALLOCATION_FAILURE; + return NULL; + } + + char *tag = btoa(oggpackB_get_buffer(&opb), oggpackB_bytes(&opb)); + oggpackB_writeclear(&opb); + return tag; +} diff --git a/plugins/liboggedit/oggedit_flac.c b/plugins/liboggedit/oggedit_flac.c new file mode 100644 index 00000000..6eec6ac9 --- /dev/null +++ b/plugins/liboggedit/oggedit_flac.c @@ -0,0 +1,213 @@ +/* + This file is part of Deadbeef Player source code + http://deadbeef.sourceforge.net + + DeaDBeeF Ogg Edit library Flac functions + + Copyright (C) 2014 Ian Nartowicz <deadbeef@nartowicz.co.uk> + + This software is provided 'as-is', without any express or implied + warranty. In no event will the authors be held liable for any damages + arising from the use of this software. + + Permission is granted to anyone to use this software for any purpose, + including commercial applications, and to alter it and redistribute it + freely, subject to the following restrictions: + + 1. The origin of this software must not be misrepresented; you must not + claim that you wrote the original software. If you use this software + in a product, an acknowledgment in the product documentation would be + appreciated but is not required. + 2. Altered source versions must be plainly marked as such, and must not be + misrepresented as being the original software. + 3. This notice may not be removed or altered from any source distribution. + +*/ + +#ifdef HAVE_CONFIG_H +#include <config.h> +#endif +#include <stdlib.h> +#include <string.h> +#include <unistd.h> +#include <stdbool.h> +#include <ogg/ogg.h> +#include <deadbeef/deadbeef.h> +#include "oggedit_internal.h" +#include "oggedit.h" + +#define PADTYPE 0x01 +#define VCTYPE 0x04 +#define LASTBLOCK 0x80 + +static void clear_header_list(ogg_packet **headers) +{ + if (headers) { + for (ogg_packet **header = headers; *header; header++) { + ogg_packet_clear(*header); + free(*header); + } + free(headers); + } +} + +static ogg_packet **headers_alloc(ogg_packet **headers, const size_t packets) +{ + ogg_packet **new_headers = realloc(headers, (packets+2) * sizeof(headers)); + if (!new_headers) { + clear_header_list(headers); + return NULL; + } + + if (!(new_headers[packets] = malloc(sizeof(ogg_packet)))) { + clear_header_list(new_headers); + return NULL; + } + + new_headers[packets + 1] = NULL; + return new_headers; +} + +static ogg_packet **metadata_block_packets(DB_FILE *in, ogg_sync_state *oy, const off_t offset, char **vendor, int *res) +{ + ogg_stream_state os; + ogg_page og; + if ((*res = init_read_stream(in, oy, &os, &og, offset, FLACNAME)) <= OGGEDIT_EOF) + return NULL; + + int pages = 1; + size_t packets = 0; + ogg_packet **headers = NULL; + while ((headers = headers_alloc(headers, packets)) && + (pages = read_packet(in, oy, &os, &og, headers[packets], pages)) > OGGEDIT_EOF && + (headers[packets++]->packet[0] & LASTBLOCK) != LASTBLOCK); + + ogg_stream_clear(&os); + + if (!headers) + *res = OGGEDIT_ALLOCATION_FAILURE; + else if (!packets || headers[0]->packet[0] & 0x3F != VCTYPE) + *res = OGGEDIT_CANNOT_PARSE_HEADERS; + else + *res = pages; + if (*res <= OGGEDIT_EOF) { + clear_header_list(headers); + return NULL; + } + + *vendor = parse_vendor(headers[0], 4); + size_t bytes = 0; + for (size_t i = 0; i < packets; i++) + bytes += headers[i]->bytes; + if (bytes < MAXPAYLOAD * (pages-1)) + headers[0]->bytes = 4; + + *res = OGGEDIT_OK; + return headers; +} + +static long write_metadata_block_packets(FILE *out, const int serial, const char *vendor, const size_t num_tags, char **tags, size_t padding, ogg_packet **metadata) +{ + const size_t header_length = vc_size(vendor, num_tags, tags); + if (header_length > (2<<24)) + return OGGEDIT_ALLOCATION_FAILURE; + + char magic[4]; + magic[0] = VCTYPE; + magic[1] = header_length >> 16 & 0xFF; + magic[2] = header_length >> 8 & 0xFF; + magic[3] = header_length & 0xFF; + ogg_packet_clear(metadata[0]); + if (!fill_vc_packet(magic, 4, vendor, num_tags, tags, false, padding, metadata[0])) + return OGGEDIT_ALLOCATION_FAILURE; + + ogg_stream_state os; + if (ogg_stream_init(&os, serial)) + return OGGEDIT_FAILED_TO_INIT_STREAM; + os.b_o_s = 1; + os.pageno = 1; + + for (int i = 0; metadata[i]; i++) { + if (!metadata[i+1]) + metadata[i]->packet[0] |= LASTBLOCK; + ogg_stream_packetin(&os, metadata[i]); + } + + return flush_stream(out, &os); +} + +off_t oggedit_write_flac_metadata(DB_FILE *in, const char *fname, const off_t offset, const int num_tags, char **tags) +{ + off_t res; + char tempname[PATH_MAX] = ""; + ogg_packet **headers = NULL; + char *vendor = NULL; + ogg_sync_state oy; + ogg_sync_init(&oy); + + /* Original file must be writable whichever way we update it */ + FILE *out = fopen(fname, "r+b"); + if (!out) { + res = OGGEDIT_CANNOT_UPDATE_FILE; + goto cleanup; + } + + /* See if we can write the tags packet directly into the existing file ... */ + if (!(headers = metadata_block_packets(in, &oy, offset, &vendor, (int *)&res))) + goto cleanup; + const off_t stream_size_k = in->vfs->getlength(in) / 1000; // use file size for now + const size_t metadata_size = 4 + vc_size(vendor, num_tags, tags); + ptrdiff_t padding = headers[0]->bytes - metadata_size; + if (stream_size_k < 1000 || padding < 0 || headers[1] && padding > 0 || padding > stream_size_k+metadata_size) + if (res = open_temp_file(fname, tempname, &out)) + goto cleanup; + + /* Re-pad if writing the whole file */ + if (*tempname) { + size_t i = 0, j = 0; + while (headers[i]) { + headers[j++] = headers[i]; + while (headers[++i] && (headers[i]->packet[0] & 0x3F) == PADTYPE) { + ogg_packet_clear(headers[i]); + free(headers[i]); + } + } + headers[j] = NULL; + padding = headers[1] || stream_size_k < 900 ? 0 : stream_size_k < 10000 ? 1024 : stream_size_k < 100000 ? 8192 : 65536; + } + + /* Write pages until we reach the correct comment header */ + ogg_page og; + const int flac_serial = copy_up_to_codec(in, out, &oy, &og, *tempname ? 0 : offset, offset, FLACNAME); + if (flac_serial <= OGGEDIT_EOF) { + res = flac_serial; + goto cleanup; + } + if ((res = copy_up_to_header(in, out, &oy, &og, flac_serial)) <= OGGEDIT_EOF) + goto cleanup; + const long pageno = write_metadata_block_packets(out, flac_serial, vendor, num_tags, tags, padding, headers); + if (pageno < OGGEDIT_EOF) { + res = pageno; + goto cleanup; + } + + /* If we have tempfile, copy the remaining pages */ + if (*tempname) { + if ((res = copy_remaining_pages(in, out, &oy, flac_serial, pageno)) < OGGEDIT_EOF) + goto cleanup; + if (rename(tempname, fname)) { + res = OGGEDIT_RENAME_FAILED; + goto cleanup; + } + } + + res = file_size(fname); + +cleanup: + clear_header_list(headers); + cleanup(in, out, &oy, vendor); + if (res < OGGEDIT_OK) + unlink(tempname); + return res; +} + diff --git a/plugins/liboggedit/oggedit_internal.c b/plugins/liboggedit/oggedit_internal.c new file mode 100644 index 00000000..a95fa1f8 --- /dev/null +++ b/plugins/liboggedit/oggedit_internal.c @@ -0,0 +1,524 @@ +/* + This file is part of Deadbeef Player source code + http://deadbeef.sourceforge.net + + DeaDBeeF Ogg Edit library internal functions + + Copyright (C) 2014 Ian Nartowicz <deadbeef@nartowicz.co.uk> + + This software is provided 'as-is', without any express or implied + warranty. In no event will the authors be held liable for any damages + arising from the use of this software. + + Permission is granted to anyone to use this software for any purpose, + including commercial applications, and to alter it and redistribute it + freely, subject to the following restrictions: + + 1. The origin of this software must not be misrepresented; you must not + claim that you wrote the original software. If you use this software + in a product, an acknowledgment in the product documentation would be + appreciated but is not required. + 2. Altered source versions must be plainly marked as such, and must not be + misrepresented as being the original software. + 3. This notice may not be removed or altered from any source distribution. + +*/ + +#ifdef HAVE_CONFIG_H +#include <config.h> +#endif +#include <stdio.h> +#include <stdlib.h> +#include <string.h> +#include <unistd.h> +#include <stdbool.h> +#include <libgen.h> +#include <errno.h> +#include <sys/stat.h> +#include <ogg/ogg.h> +#include <deadbeef/deadbeef.h> +#include "oggedit.h" +#include "oggedit_internal.h" + +static char *cat_string(char *dest, const char *src, const char *sep) +{ + char *more = realloc(dest, strlen(dest) + strlen(src) + strlen(sep) + 1); + if (!more) { + free(dest); + return NULL; + } + return strcat(strcat(more, sep), src); +} + +static const char *codec_name(ogg_page *og) +{ + typedef struct { + const size_t length; + const char *codec; + const char *magic; + } codec_t; + const codec_t codecs[] = { + {.codec = OPUSNAME, .length = 19, .magic = "OpusHead"}, + {.codec = VORBISNAME, .length = 30, .magic = "\1vorbis"}, + {.codec = FLACNAME, .length = 47, .magic = "\177FLAC"}, + {.codec = "Speex", .length = 80, .magic = "Speex "}, + {.codec = "Celt", .length = 80, .magic = "CELT"}, // obsolete + {.codec = "MIDI", .length = 13, .magic = "OggMIDI\0"}, + {.codec = "PCM", .length = 28, .magic = "PCM "}, + {.codec = "Theora", .length = 42, .magic = "\200theora"}, + {.codec = "Daala", .length = 38, .magic = "\200daala"}, + {.codec = "Dirac", .length = 5, .magic = "BBCD\0"}, + {.codec = "Skeleton", .length = 80, .magic = "fishead\0"}, + {.codec = "Kate", .length = 64, .magic = "\200kate\0\0\0"}, + {.codec = "CMML", .length = 29, .magic = "CMML\0\0\0\0"}, + {.codec = "YUV4MPEG", .length = 8, .magic = "YUV4Mpeg"}, + {.codec = "UVS", .length = 48, .magic = "UVS "}, + {.codec = "YUV", .length = 32, .magic = "\0YUV"}, + {.codec = "RGB", .length = 24, .magic = "\0RGB"}, + {.codec = "JNG", .length = 48, .magic = "\213JNG\r\n\032\n"}, + {.codec = "MNG", .length = 48, .magic = "\212MNG\r\n\032\n"}, + {.codec = "PNG", .length = 48, .magic = "\211PNG\r\n\032\n"}, + {.codec = "Spots", .length = 52, .magic = "SPOTS\0\0\0"}, + {.codec = NULL} + }; + + for (const codec_t *match = codecs; match->codec; match++) + if ((size_t)og->body_len >= match->length && !memcmp(og->body, match->magic, strlen(match->codec))) + return match->codec; + + return "unknown"; +} + +void _oggpack_chars(oggpack_buffer *opb, const char *s, size_t length) +{ +// oggpack_writecopy(opb, s, length * 8); currently broken + while (length--) + oggpack_write(opb, *s++, 8); +} + +void _oggpack_string(oggpack_buffer *opb, const char *s) +{ + oggpack_write(opb, strlen(s), 32); + _oggpack_chars(opb, s, strlen(s)); +} + +void _oggpackB_string(oggpack_buffer *opb, const char *s) +{ + oggpackB_write(opb, strlen(s), 32); + _oggpack_chars(opb, s, strlen(s)); +} + +static bool ensure_directory(const char *path) +{ + struct stat stat_struct; + if (!stat(path, &stat_struct)) + return !S_ISDIR(stat_struct.st_mode); + + if (errno != ENOENT) + return false; + + char* dir = strdup(path); + if (!dir) + return false; + + const int bad_dir = ensure_directory(dirname(dir)); + free(dir); + + return !bad_dir && !mkdir(path, 0777); +} + +FILE *open_new_file(const char *outname) +{ + char outpath[PATH_MAX]; + strcpy(outpath, outname); + if (!ensure_directory(dirname(outpath))) + return NULL; + + unlink(outname); + return fopen(outname, "wbx"); +} + +int open_temp_file(const char *fname, char *tempname, FILE **out) +{ + snprintf(tempname, PATH_MAX, "%s.temp", fname); + unlink(tempname); + if (!(*out = freopen(tempname, "abx", *out))) + return OGGEDIT_CANNOT_OPEN_TEMPORARY_FILE; + + struct stat stat_struct; + if (!stat(fname, &stat_struct)) + chmod(tempname, stat_struct.st_mode); + + return 0; +} + +off_t file_size(const char *fname) +{ + struct stat sb; + if (stat(fname, &sb)) + return OGGEDIT_STAT_FAILED; + + return sb.st_size; +} + +void cleanup(DB_FILE *in, FILE *out, ogg_sync_state *oy, void *buffer) +{ + if (in) + in->vfs->close(in); + + if (out) + fclose(out); + + ogg_sync_clear(oy); + + if (buffer) + free(buffer); +} + +static int get_page(DB_FILE *in, ogg_sync_state *oy, ogg_page *og) +{ + uint16_t chunks_left = MAXPAGE / CHUNKSIZE; + while (ogg_sync_pageout(oy, og) != 1) { + char *buffer = ogg_sync_buffer(oy, CHUNKSIZE); + if (!in || !buffer || !chunks_left--) + return OGGEDIT_CANT_FIND_STREAM; + + const size_t bytes = in->vfs->read(buffer, 1, CHUNKSIZE, in); + if (!bytes) + return OGGEDIT_EOF; + + ogg_sync_wrote(oy, bytes); + } + + return ogg_page_serialno(og); +} + +static int skip_to_bos(DB_FILE *in, ogg_sync_state *oy, ogg_page *og, const off_t offset) +{ + if (!in) + return OGGEDIT_FILE_NOT_OPEN; + + if (in->vfs->seek(in, offset, SEEK_SET)) + return OGGEDIT_SEEK_FAILED; + + ogg_sync_reset(oy); + int serial; + do + serial = get_page(in, oy, og); + while (serial > OGGEDIT_EOF && !ogg_page_bos(og)); + + return serial; +} + +static int skip_to_codec(DB_FILE *in, ogg_sync_state *oy, ogg_page *og, const off_t offset, const char *codec) +{ + int serial = skip_to_bos(in, oy, og, offset); + while (serial > OGGEDIT_EOF && strcmp(codec_name(og), codec)) + serial = get_page(in, oy, og); + + return serial; +} + +static int skip_to_header(DB_FILE *in, ogg_sync_state *oy, ogg_page *og, int serial, const int codec_serial) +{ + while (serial > OGGEDIT_EOF && (ogg_page_bos(og) || serial != codec_serial)) + serial = get_page(in, oy, og); + + return serial; +} + +static off_t sync_tell(DB_FILE *in, ogg_sync_state *oy, ogg_page *og) +{ + return in->vfs->tell(in) - oy->fill + oy->returned - og->header_len - og->body_len; +} + +static bool write_page(FILE *out, ogg_page *og) +{ + if (fwrite(og->header, 1, og->header_len, out) != (size_t)og->header_len) + return false; + + if (fwrite(og->body, 1, og->body_len, out) != (size_t)og->body_len) + return false; + + return true; +} + +static int write_page_and_get_next(DB_FILE *in, FILE *out, ogg_sync_state *oy, ogg_page *og) +{ + if (!write_page(out, og)) + return OGGEDIT_WRITE_ERROR; + + return get_page(in, oy, og); +} + +int copy_up_to_codec(DB_FILE *in, FILE *out, ogg_sync_state *oy, ogg_page *og, const off_t start_offset, const off_t link_offset, const char *codec) +{ + int serial = skip_to_bos(in, oy, og, start_offset); + + if (fseek(out, sync_tell(in, oy, og), SEEK_SET)) + return OGGEDIT_SEEK_FAILED; + + while (serial > OGGEDIT_EOF && (sync_tell(in, oy, og) < link_offset || !ogg_page_bos(og) || strcmp(codec_name(og), codec))) + serial = write_page_and_get_next(in, out, oy, og); + + return serial; +} + +int copy_up_to_header(DB_FILE *in, FILE *out, ogg_sync_state *oy, ogg_page *og, const int codec_serial) +{ + int serial; + do + serial = write_page_and_get_next(in, out, oy, og); + while (serial > OGGEDIT_EOF && serial != codec_serial); + return serial; +} + +long flush_stream(FILE *out, ogg_stream_state *os) +{ + ogg_page og; + while (ogg_stream_flush_fill(os, &og, MAXPAYLOAD)) + if (!write_page(out, &og)) + return OGGEDIT_WRITE_ERROR; + const long pageno = ogg_stream_check(os) ? OGGEDIT_FLUSH_FAILED : ogg_page_pageno(&og); + ogg_stream_clear(os); + return pageno; +} + +char *codec_names(DB_FILE *in, ogg_sync_state *oy, const off_t link_offset, int *res) +{ + ogg_page og; + *res = skip_to_bos(in, oy, &og, link_offset); + char *codecs = strdup("Ogg"); + while (codecs && *res > OGGEDIT_EOF && ogg_page_bos(&og)) { + codecs = cat_string(codecs, codec_name(&og), strcmp(codecs, "Ogg") ? "/" : " "); + *res = get_page(in, oy, &og); + } + if (!*codecs) { + *res = OGGEDIT_ALLOCATION_FAILURE; + return NULL; + } + if (*res <= OGGEDIT_EOF) { + free(codecs); + return NULL; + } + + *res = OGGEDIT_OK; + return codecs; +} + +off_t codec_stream_size(DB_FILE *in, ogg_sync_state *oy, const off_t start_offset, const off_t end_offset, const char *codec) +{ + /* Find codec serial and any other codecs */ + bool multiplex = false; + ogg_page og; + int codec_serial = -1; + int serial = skip_to_bos(in, oy, &og, start_offset); + while (serial > OGGEDIT_EOF && ogg_page_bos(&og)) { + if (strcmp(codec_name(&og), codec)) + multiplex = true; + else + codec_serial = serial; + serial = get_page(in, oy, &og); + } + + /* Skip to the first codec data page */ + while (serial > OGGEDIT_EOF && !(ogg_page_granulepos(&og) > 0 && serial == codec_serial)) + if ((serial = get_page(in, oy, &og)) <= OGGEDIT_EOF) + return serial; + + off_t stream_size = 0; + if (multiplex) { + /* Add up all the codec stream pages until EOF or a new link */ + while (serial > OGGEDIT_EOF && !ogg_page_bos(&og)) { + if (serial == codec_serial) + stream_size += og.header_len + og.body_len; + serial = get_page(in, oy, &og); + } + } + else { + /* Find the exact offsets of the start and end of the audio */ + stream_size -= sync_tell(in, oy, &og); + if (in->vfs->seek(in, end_offset, end_offset == 0 ? SEEK_END : SEEK_SET)) + return OGGEDIT_SEEK_FAILED; + stream_size += in->vfs->tell(in); + ogg_sync_reset(oy); + while ((serial = get_page(in, oy, &og)) > OGGEDIT_EOF && !ogg_page_bos(&og)) + stream_size += og.header_len + og.body_len; + } + if (serial < OGGEDIT_EOF) + return serial; + + return stream_size; +} + +char *parse_vendor(const ogg_packet *op, const size_t magic_length) +{ + if (op->bytes < magic_length + 4) + return NULL; + + const uint8_t *p = op->packet + magic_length; + const uint32_t vendor_length = *p | *(p+1)<<8 | *(p+2)<<16 | *(p+3)<<24; + if (op->bytes < magic_length + 4 + vendor_length) + return NULL; + + char *vendor = calloc(vendor_length+1, 1); + if (vendor) + memcpy(vendor, p+4, vendor_length); + return vendor; +} + +int init_read_stream(DB_FILE *in, ogg_sync_state *oy, ogg_stream_state *os, ogg_page *og, const off_t offset, const char *codec) +{ + int serial = skip_to_codec(in, oy, og, offset, codec); + serial = skip_to_header(in, oy, og, serial, serial); + if (serial <= OGGEDIT_EOF) + return serial; + + if (ogg_stream_init(os, serial)) + return OGGEDIT_FAILED_TO_INIT_STREAM; + + os->b_o_s = 1; + ogg_stream_pagein(os, og); + + return OGGEDIT_OK; +} + +int read_packet(DB_FILE *in, ogg_sync_state *oy, ogg_stream_state *os, ogg_page *og, ogg_packet *header, int pages) +{ + ogg_packet op; + do { + while (ogg_stream_packetpeek(os, NULL) == 0) { + const int serial = get_page(in, oy, og); + if (serial <= OGGEDIT_EOF) { + return serial; + } + if (os->serialno == serial) { + pages++; + ogg_stream_pagein(os, og); + } + } + if (ogg_stream_check(os)) + return OGGEDIT_FAILED_TO_STREAM_PAGE_FOR_PACKET; + } while (ogg_stream_packetout(os, &op) != 1); + + memset(header, '\0', sizeof(*header)); + if (!header || !(header->packet = malloc(op.bytes))) { + free(header); + return OGGEDIT_ALLOCATION_FAILURE; + } + + header->bytes = op.bytes; + memcpy(header->packet, op.packet, op.bytes); + return pages; +} + +ogg_packet *fill_vc_packet(const char *magic, const size_t magic_length, const char *vendor, const size_t num_tags, char **tags, const bool framing, const size_t padding, ogg_packet *op) +{ + oggpack_buffer opb; + oggpack_writeinit(&opb); + _oggpack_chars(&opb, magic, magic_length); + _oggpack_string(&opb, vendor); + oggpack_write(&opb, num_tags, 32); + for (size_t i = 0; i < num_tags; i++) + _oggpack_string(&opb, tags[i]); + if (framing) { + oggpack_write(&opb, 1, 1); + oggpack_writealign(&opb); + } + for (size_t i = 0; i < padding; i++) + oggpack_write(&opb, 0, 8); // use oggpack_writecopy when it is fixed + if (oggpack_writecheck(&opb)) + return NULL; + + if (op) { + memset(op, '\0', sizeof(*op)); + op->bytes = oggpack_bytes(&opb); + if (op->packet = malloc(op->bytes)) + memcpy(op->packet, oggpack_get_buffer(&opb), op->bytes); + } + oggpack_writeclear(&opb); + + if (!op->packet) + return NULL; + + return op; +} + +size_t vc_size(const char *vendor, size_t num_tags, char **tags) +{ + size_t metadata_size = 4 + strlen(vendor) + 4; + for (size_t i = 0; i < num_tags; i++) + metadata_size += strlen(tags[i]) + 4; + return metadata_size; +} + +int copy_remaining_pages(DB_FILE *in, FILE *out, ogg_sync_state *oy, const int codec_serial, uint32_t pageno) +{ + /* Skip past the codec header packets */ + ogg_page og; + int serial; + do + serial = get_page(in, oy, &og); + while (serial > OGGEDIT_EOF && serial == codec_serial && ogg_page_granulepos(&og) <= 0); + + /* Renumber the rest of this link */ + while (serial > OGGEDIT_EOF && !ogg_page_bos(&og)) { + if (serial == codec_serial && ogg_page_pageno(&og) != ++pageno) { + og.header[18] = pageno & 0xFF; + og.header[19] = pageno >> 8 & 0xFF; + og.header[20] = pageno >> 16 & 0xFF; + og.header[21] = pageno >> 24 & 0xFF; + ogg_page_checksum_set(&og); + } + serial = write_page_and_get_next(in, out, oy, &og); + } + + /* Blindly copy remaining links */ + while (serial > OGGEDIT_EOF) + serial = write_page_and_get_next(in, out, oy, &og); + + return serial; +} + +int write_all_streams(DB_FILE *in, FILE *out, ogg_sync_state *oy, const off_t offset) +{ + /* Copy BOS page(s) */ + ogg_page og; + int serial = skip_to_bos(in, oy, &og, offset); + while (serial > OGGEDIT_EOF && ogg_page_bos(&og)) + serial = write_page_and_get_next(in, out, oy, &og); + if (serial <= OGGEDIT_EOF) + return serial; + + /* Copy all pages until EOF or next link */ + while (serial > OGGEDIT_EOF && !ogg_page_bos(&og)) + if ((serial = write_page_and_get_next(in, out, oy, &og)) < OGGEDIT_EOF) + return serial; + + return OGGEDIT_OK; +} + +int write_one_stream(DB_FILE *in, FILE *out, ogg_sync_state *oy, const off_t offset, const char *codec) +{ + /* Find codec BOS page */ + ogg_page og; + const int codec_serial = skip_to_codec(in, oy, &og, offset, codec); + if (codec_serial <= OGGEDIT_EOF) + return codec_serial; + + /* Write it and skip the other BOS pages */ + int serial = write_page_and_get_next(in, out, oy, &og); + if ((serial = skip_to_header(in, oy, &og, serial, codec_serial)) <= OGGEDIT_EOF) + return serial; + + /* Copy all codec pages until EOF or next link */ + while (serial > OGGEDIT_EOF && !ogg_page_bos(&og)) { + if (serial == codec_serial && !write_page(out, &og)) + return OGGEDIT_WRITE_ERROR; + serial = get_page(in, oy, &og); + } + if (serial < OGGEDIT_EOF) + return serial; + + return OGGEDIT_OK; +} diff --git a/plugins/liboggedit/oggedit_internal.h b/plugins/liboggedit/oggedit_internal.h new file mode 100644 index 00000000..46a84345 --- /dev/null +++ b/plugins/liboggedit/oggedit_internal.h @@ -0,0 +1,59 @@ +/* + This file is part of Deadbeef Player source code + http://deadbeef.sourceforge.net + + DeaDBeeF Ogg Edit library internal headers + + Copyright (C) 2014 Ian Nartowicz <deadbeef@nartowicz.co.uk> + + This software is provided 'as-is', without any express or implied + warranty. In no event will the authors be held liable for any damages + arising from the use of this software. + + Permission is granted to anyone to use this software for any purpose, + including commercial applications, and to alter it and redistribute it + freely, subject to the following restrictions: + + 1. The origin of this software must not be misrepresented; you must not + claim that you wrote the original software. If you use this software + in a product, an acknowledgment in the product documentation would be + appreciated but is not required. + 2. Altered source versions must be plainly marked as such, and must not be + misrepresented as being the original software. + 3. This notice may not be removed or altered from any source distribution. + +*/ + +#ifndef __OGGEDIT_INT_H +#define __OGGEDIT_INT_H + +#define OPUSNAME "Opus" +#define VORBISNAME "Vorbis" +#define FLACNAME "Flac" + +#define CHUNKSIZE 4096 +#define MAXPAGE 65536 +#define MAXPAYLOAD 65025 + +void _oggpack_chars(oggpack_buffer *opb, const char *s, size_t length); +void _oggpack_string(oggpack_buffer *opb, const char *s); +void _oggpackB_string(oggpack_buffer *opb, const char *s); +int open_temp_file(const char *fname, char *tempname, FILE **out); +FILE *open_new_file(const char *outname); +off_t file_size(const char *fname); +void cleanup(DB_FILE *in, FILE *out, ogg_sync_state *oy, void *buffer); +int copy_up_to_codec(DB_FILE *in, FILE *out, ogg_sync_state *oy, ogg_page *og, const off_t start_offset, const off_t link_offset, const char *codec); +int copy_up_to_header(DB_FILE *in, FILE *out, ogg_sync_state *oy, ogg_page *og, const int codec_serial); +long flush_stream(FILE *out, ogg_stream_state *os); +char *codec_names(DB_FILE *in, ogg_sync_state *oy, const off_t link_offset, int *res); +off_t codec_stream_size(DB_FILE *in, ogg_sync_state *oy, const off_t start_offset, const off_t end_offset, const char *codec); +char *parse_vendor(const ogg_packet *op, const size_t magic_length); +int init_read_stream(DB_FILE *in, ogg_sync_state *oy, ogg_stream_state *os, ogg_page *og, const off_t offset, const char *codec); +int read_packet(DB_FILE *in, ogg_sync_state *oy, ogg_stream_state *os, ogg_page *og, ogg_packet *header, int pages); +ogg_packet *fill_vc_packet(const char *magic, const size_t magic_length, const char *vendor, const size_t num_tags, char **tags, const bool framing, const size_t padding, ogg_packet *op); +size_t vc_size(const char *vendor, size_t num_tags, char **tags); +int copy_remaining_pages(DB_FILE *in, FILE *out, ogg_sync_state *oy, const int codec_serial, uint32_t pageno); +int write_all_streams(DB_FILE *in, FILE *out, ogg_sync_state *oy, const off_t offset); +int write_one_stream(DB_FILE *in, FILE *out, ogg_sync_state *oy, const off_t offset, const char *codec); + +#endif /* __OGGEDIT_INT_H */ diff --git a/plugins/liboggedit/oggedit_opus.c b/plugins/liboggedit/oggedit_opus.c new file mode 100644 index 00000000..cb52480d --- /dev/null +++ b/plugins/liboggedit/oggedit_opus.c @@ -0,0 +1,197 @@ +/* + This file is part of Deadbeef Player source code + http://deadbeef.sourceforge.net + + DeaDBeeF Ogg Edit library Opus functions + + Copyright (C) 2014 Ian Nartowicz <deadbeef@nartowicz.co.uk> + + This software is provided 'as-is', without any express or implied + warranty. In no event will the authors be held liable for any damages + arising from the use of this software. + + Permission is granted to anyone to use this software for any purpose, + including commercial applications, and to alter it and redistribute it + freely, subject to the following restrictions: + + 1. The origin of this software must not be misrepresented; you must not + claim that you wrote the original software. If you use this software + in a product, an acknowledgment in the product documentation would be + appreciated but is not required. + 2. Altered source versions must be plainly marked as such, and must not be + misrepresented as being the original software. + 3. This notice may not be removed or altered from any source distribution. + +*/ + +#ifdef HAVE_CONFIG_H +#include <config.h> +#endif +#include <stdlib.h> +#include <string.h> +#include <unistd.h> +#include <stdbool.h> +#include <limits.h> +#include <ogg/ogg.h> +#include <deadbeef/deadbeef.h> +#include "oggedit_internal.h" +#include "oggedit.h" + +#define TAGMAGIC "OpusTags" + +int oggedit_write_opus_file(DB_FILE *in, const char *outname, const off_t offset, const bool all_streams) +{ + FILE *out = open_new_file(outname); + if (!out) + return OGGEDIT_CANNOT_OPEN_OUTPUT_FILE; + + ogg_sync_state oy; + ogg_sync_init(&oy); + + int res; + if (all_streams) + res = write_one_stream(in, out, &oy, offset, OPUSNAME); + else + res = write_all_streams(in, out, &oy, offset); + + cleanup(in, out, &oy, NULL); + + if (res <= OGGEDIT_EOF) + unlink(outname); + + return res; +} + +off_t oggedit_opus_stream_info(DB_FILE *in, const off_t start_offset, const off_t end_offset, char **codecs) +{ + int res; + ogg_sync_state oy; + ogg_sync_init(&oy); + *codecs = codec_names(in, &oy, start_offset, &res); + const off_t stream_size = codec_stream_size(in, &oy, start_offset, end_offset, OPUSNAME); + cleanup(in, NULL, &oy, NULL); + return stream_size; +} + +static ptrdiff_t check_opus_header(DB_FILE *in, ogg_sync_state *oy, const off_t offset, char **vendor) +{ + ogg_stream_state os; + ogg_page og; + const int serial = init_read_stream(in, oy, &os, &og, offset, OPUSNAME); + if (serial <= OGGEDIT_EOF) + return serial; + + ogg_packet op; + const int pages = read_packet(in, oy, &os, &og, &op, 1); + ogg_stream_clear(&os); + if (pages <= OGGEDIT_EOF) + return pages; + + if (op.bytes > strlen(TAGMAGIC) && !memcmp(op.packet, TAGMAGIC, strlen(TAGMAGIC))) + *vendor = parse_vendor(&op, strlen(TAGMAGIC)); + free(op.packet); + if (!*vendor) + return OGGEDIT_CANNOT_PARSE_HEADERS; + + if (op.bytes < MAXPAYLOAD * (pages-1)) + return 4; // prevent in-place write if the packet is weirdly split into too many pages + + return op.bytes; +} + +static long write_opus_tags(FILE *out, const int serial, const char *vendor, const size_t num_tags, char **tags, const size_t padding) +{ + ogg_packet op; + if (!fill_vc_packet(TAGMAGIC, strlen(TAGMAGIC), vendor, num_tags, tags, false, padding, &op)) + return OGGEDIT_ALLOCATION_FAILURE; + + ogg_stream_state os; + if (ogg_stream_init(&os, serial)) + return OGGEDIT_FAILED_TO_INIT_STREAM; + os.b_o_s = 1; + os.pageno = 1; + ogg_stream_packetin(&os, &op); + + ogg_packet_clear(&op); + return flush_stream(out, &os); +} + +off_t oggedit_write_opus_metadata(DB_FILE *in, const char *fname, const off_t offset, const size_t stream_size, const int output_gain, const int num_tags, char **tags) +{ + off_t res; + char tempname[PATH_MAX] = ""; + char *vendor = NULL; + ogg_sync_state oy; + ogg_sync_init(&oy); + + /* Original file must be writable whichever way we update it */ + FILE *out = fopen(fname, "r+b"); + if (!out) { + res = OGGEDIT_CANNOT_UPDATE_FILE; + goto cleanup; + } + + /* Should we write the tags packet directly into the existing file ... */ + const ptrdiff_t tags_packet_size = check_opus_header(in, &oy, offset, &vendor); + if (tags_packet_size <= OGGEDIT_EOF) { + res = tags_packet_size; + goto cleanup; + } + const size_t metadata_size = strlen(TAGMAGIC) + vc_size(vendor, num_tags, tags); + ptrdiff_t padding = tags_packet_size - metadata_size; + const off_t file_size_k = in->vfs->getlength(in) / 1000; + const size_t stream_size_k = stream_size ? stream_size / 1000 : file_size_k; + if (file_size_k < 100 || padding < 0 || padding > file_size_k/10+stream_size_k+metadata_size) + if (res = open_temp_file(fname, tempname, &out)) + goto cleanup; + + /* Re-pad if writing the whole file */ + if (*tempname) + padding = stream_size_k < 90 ? 0 : stream_size_k < 1000 ? 128 : stream_size_k < 10000 ? 1024 : 8192; + + /* Write pages until we reach the correct OpusHead, then write OpusTags */ + ogg_page og; + const int opus_serial = copy_up_to_codec(in, out, &oy, &og, *tempname ? 0 : offset, offset, OPUSNAME); + if (opus_serial <= OGGEDIT_EOF) { + res = opus_serial; + goto cleanup; + } + if (output_gain > INT_MIN) { + og.body[16] = output_gain & 0xFF; + og.body[17] = output_gain >> 8 & 0xFF; + ogg_page_checksum_set(&og); + } + if ((res = copy_up_to_header(in, out, &oy, &og, opus_serial)) <= OGGEDIT_EOF) + goto cleanup; + const long pageno = write_opus_tags(out, opus_serial, vendor, num_tags, tags, (size_t)padding); + if (pageno < OGGEDIT_EOF) { + res = pageno; + goto cleanup; + } + + /* If we have tempfile, copy the remaining pages */ + if (*tempname) { + if ((res = copy_remaining_pages(in, out, &oy, opus_serial, pageno)) < OGGEDIT_EOF) + goto cleanup; + if (rename(tempname, fname)) { + res = OGGEDIT_RENAME_FAILED; + goto cleanup; + } + } + + res = file_size(fname); + +cleanup: + cleanup(in, out, &oy, vendor); + if (res < OGGEDIT_OK) + unlink(tempname); + return res; +} +/* +struct timeval timeval; +gettimeofday(&timeval, NULL); +int usecs = timeval.tv_sec* 1000000 + timeval.tv_usec; +gettimeofday(&timeval, NULL); +usecs = timeval.tv_sec* 1000000 + timeval.tv_usec - usecs; +fprintf(stderr, "%d micro-seconds\n", usecs); +*/ diff --git a/plugins/liboggedit/oggedit_utils.c b/plugins/liboggedit/oggedit_utils.c new file mode 100644 index 00000000..03e753a8 --- /dev/null +++ b/plugins/liboggedit/oggedit_utils.c @@ -0,0 +1,125 @@ +/* + This file is part of Deadbeef Player source code + http://deadbeef.sourceforge.net + + DeaDBeeF Ogg Edit library miscellaneous functions + + Copyright (C) 2014 Ian Nartowicz <deadbeef@nartowicz.co.uk> + + This software is provided 'as-is', without any express or implied + warranty. In no event will the authors be held liable for any damages + arising from the use of this software. + + Permission is granted to anyone to use this software for any purpose, + including commercial applications, and to alter it and redistribute it + freely, subject to the following restrictions: + + 1. The origin of this software must not be misrepresented; you must not + claim that you wrote the original software. If you use this software + in a product, an acknowledgment in the product documentation would be + appreciated but is not required. + 2. Altered source versions must be plainly marked as such, and must not be + misrepresented as being the original software. + 3. This notice may not be removed or altered from any source distribution. + +*/ + +#ifdef HAVE_CONFIG_H +#include <config.h> +#endif +#include <stdlib.h> +#include <stdint.h> +#include <string.h> +#include <stdbool.h> +#include <ctype.h> + +uint8_t *oggedit_vorbis_channel_map(const int channel_count) +{ + size_t map_size = channel_count * sizeof(uint8_t); + uint8_t *map = malloc(map_size); + if (!map) + return NULL; + switch(channel_count) { + case 3: + return memcpy(map, &(uint8_t[]){0,2,1}, map_size); + case 5: + return memcpy(map, &(uint8_t[]){0,2,1,3,4}, map_size); + case 6: + return memcpy(map, &(uint8_t[]){0,2,1,4,5,3}, map_size); + case 7: + return memcpy(map, &(uint8_t[]){0,2,1,4,5,6,3}, map_size); + case 8: + return memcpy(map, &(uint8_t[]){0,2,1,6,7,4,5,3}, map_size); + default: + free(map); + return NULL; + } +} + +const char *oggedit_map_tag(char *key, const char *in_or_out) +{ + typedef struct { + const char *tag; + const char *meta; + } key_t; + const key_t keys[] = { + /* Permanent named tags in DeaDBeef */ +// {.tag = "ARTIST", .meta = "artist"}, +// {.tag = "TITLE", .meta = "title"}, +// {.tag = "ALBUM", .meta = "album"}, + {.tag = "DATE", .meta = "year"}, + {.tag = "TRACKNUMBER", .meta = "track"}, + {.tag = "TRACKTOTAL", .meta = "numtracks"}, +// {.tag = "GENRE", .meta = "genre"}, +// {.tag = "COMPOSER", .meta = "composer"}, + {.tag = "DISCNUMBER", .meta = "disc"}, +// {.tag = "COMMENT", .meta = "comment"}, + /* Vorbis standard tags */ +// {.tag = "ARRANGER", .meta = "Arranger"}, +// {.tag = "AUTHOR", .meta = "Author"}, +// {.tag = "CONDUCTOR", .meta = "Conductor"}, +// {.tag = "ENSEMBLE", .meta = "Ensemble"}, +// {.tag = "LYRICIST", .meta = "Lyricist"}, +// {.tag = "PERFORMER", .meta = "Performer"}, +// {.tag = "PUBLISHER", .meta = "Publisher"}, +// {.tag = "DISCTOTAL", .meta = "Disctotal"}, +// {.tag = "OPUS", .meta = "Opus"}, +// {.tag = "PART", .meta = "Part"}, +// {.tag = "PARTNUMBER", .meta = "Partnumber"}, +// {.tag = "VERSION", .meta = "Version"}, +// {.tag = "DESCRIPTION", .meta = "Description"}, +// {.tag = "COPYRIGHT", .meta = "Copyright"}, +// {.tag = "LICENSE", .meta = "License"}, +// {.tag = "CONTACT", .meta = "Contact"}, +// {.tag = "ORGANIZATION", .meta = "Organization"}, +// {.tag = "LOCATION", .meta = "Location"}, +// {.tag = "EAN/UPN", .meta = "EAN/UPN"}, +// {.tag = "ISRC", .meta = "ISRC"}, +// {.tag = "LABEL", .meta = "Label"}, +// {.tag = "LABELNO", .meta = "Labelno"}, +// {.tag = "ENCODER", .meta = "Encoder"}, +// {.tag = "ENCODED-BY", .meta = "Encoded-by"}, +// {.tag = "ENCODING", .meta = "Encoding"}, + /* Other tags */ +// {.tag = "ALBUMARTIST", .meta = "Albumartist"}, +// {.tag = "ALBUM ARTIST", .meta = "Album artist"}, +// {.tag = "BAND", .meta = "Band"}, +// {.tag = "COMPILATION", .meta = "Compilation"}, +// {.tag = "TOTALTRACKS", .meta = "Totaltracks"}, +// {.tag = "ENCODED_BY", .meta = "Encoded_by"}, +// {.tag = "ENCODER_OPTIONS",.meta = "Encoder_options"}, + {.tag = NULL} + }; + + /* Mapping for special Deadbeef internal metadata */ + for (const key_t *match = keys; match->tag; match++) + if (!strcasecmp(*in_or_out == 't' ? match->tag : match->meta, key)) + return *in_or_out == 't' ? match->meta : match->tag; + + /* Upper-case all Vorbis Comment tag names */ + if (*in_or_out == 'm') + for (size_t i = 0; key[i]; i++) + key[i] = toupper(key[i]); + + return key; +} diff --git a/plugins/liboggedit/oggedit_vorbis.c b/plugins/liboggedit/oggedit_vorbis.c new file mode 100644 index 00000000..2ff3fc09 --- /dev/null +++ b/plugins/liboggedit/oggedit_vorbis.c @@ -0,0 +1,168 @@ +/* + This file is part of Deadbeef Player source code + http://deadbeef.sourceforge.net + + DeaDBeeF Ogg Edit library Vorbis functions + + Copyright (C) 2014 Ian Nartowicz <deadbeef@nartowicz.co.uk> + + This software is provided 'as-is', without any express or implied + warranty. In no event will the authors be held liable for any damages + arising from the use of this software. + + Permission is granted to anyone to use this software for any purpose, + including commercial applications, and to alter it and redistribute it + freely, subject to the following restrictions: + + 1. The origin of this software must not be misrepresented; you must not + claim that you wrote the original software. If you use this software + in a product, an acknowledgment in the product documentation would be + appreciated but is not required. + 2. Altered source versions must be plainly marked as such, and must not be + misrepresented as being the original software. + 3. This notice may not be removed or altered from any source distribution. + +*/ + +#ifdef HAVE_CONFIG_H +#include <config.h> +#endif +#include <stdlib.h> +#include <string.h> +#include <unistd.h> +#include <stdbool.h> +#include <ogg/ogg.h> +#include <deadbeef/deadbeef.h> +#include "oggedit_internal.h" +#include "oggedit.h" + +#define VCMAGIC "\3vorbis" +#define CODEMAGIC "\5vorbis" + +off_t oggedit_vorbis_stream_info(DB_FILE *in, const off_t start_offset, const off_t end_offset, char **codecs) +{ + int res; + ogg_sync_state oy; + ogg_sync_init(&oy); + *codecs = codec_names(in, &oy, start_offset, &res); + const off_t stream_size = codec_stream_size(in, &oy, start_offset, end_offset, VORBISNAME); + cleanup(in, NULL, &oy, NULL); + return stream_size; +} + +static ptrdiff_t check_vorbis_headers(DB_FILE *in, ogg_sync_state *oy, const off_t offset, char **vendor, ogg_packet *codebooks) +{ + ogg_stream_state os; + ogg_page og; + const int serial = init_read_stream(in, oy, &os, &og, offset, VORBISNAME); + if (serial <= OGGEDIT_EOF) + return serial; + + ogg_packet vc; + int pages = read_packet(in, oy, &os, &og, &vc, 1); + if (pages > OGGEDIT_EOF) + pages = read_packet(in, oy, &os, &og, codebooks, pages); + ogg_stream_clear(&os); + if (pages <= OGGEDIT_EOF) + return pages; + + if (vc.bytes > strlen(VCMAGIC) && !memcmp(vc.packet, VCMAGIC, strlen(VCMAGIC)) && + codebooks->bytes > strlen(CODEMAGIC) && !memcmp(codebooks->packet, CODEMAGIC, strlen(CODEMAGIC))) + *vendor = parse_vendor(&vc, strlen(VCMAGIC)); + free(vc.packet); + if (!*vendor) + return OGGEDIT_CANNOT_PARSE_HEADERS; + + if ((vc.bytes + codebooks->bytes) < MAXPAYLOAD * (pages-1)) + return 4; // prevent in-place write if the packets are split over too many pages + + return vc.bytes; +} + +static long write_vorbis_tags(FILE *out, const int serial, const char *vendor, const size_t num_tags, char **tags, const size_t padding, ogg_packet *codebooks) +{ + ogg_packet op; + if (!fill_vc_packet(VCMAGIC, strlen(VCMAGIC), vendor, num_tags, tags, true, padding, &op)) + return OGGEDIT_ALLOCATION_FAILURE; + + ogg_stream_state os; + if (ogg_stream_init(&os, serial)) + return OGGEDIT_FAILED_TO_INIT_STREAM; + os.b_o_s = 1; + os.pageno = 1; + ogg_stream_packetin(&os, &op); + ogg_stream_packetin(&os, codebooks); + + ogg_packet_clear(&op); + return flush_stream(out, &os); +} + +off_t oggedit_write_vorbis_metadata(DB_FILE *in, const char *fname, const off_t offset, const size_t stream_size, const int num_tags, char **tags) +{ + off_t res; + char tempname[PATH_MAX] = ""; + char *vendor = NULL; + ogg_packet codebooks; + memset(&codebooks, '\0', sizeof(codebooks)); + ogg_sync_state oy; + ogg_sync_init(&oy); + + /* Original file must be writable whichever way we update it */ + FILE *out = fopen(fname, "r+b"); + if (!out) { + res = OGGEDIT_CANNOT_UPDATE_FILE; + goto cleanup; + } + + /* See if we can write the tags packet directly into the existing file ... */ + const ptrdiff_t tags_packet_size = check_vorbis_headers(in, &oy, offset, &vendor, &codebooks); + if (tags_packet_size <= OGGEDIT_EOF) { + res = tags_packet_size; + goto cleanup; + } + const size_t metadata_size = strlen(VCMAGIC) + vc_size(vendor, num_tags, tags) + 1; + ptrdiff_t padding = tags_packet_size - metadata_size; + const off_t file_size_k = in->vfs->getlength(in) / 1000; + const size_t stream_size_k = stream_size ? stream_size / 1000 : file_size_k; + if (file_size_k < 100 || padding < 0 || padding > file_size_k/10+stream_size_k+metadata_size) + if (res = open_temp_file(fname, tempname, &out)) + goto cleanup; + + /* Re-pad if writing the whole file */ + if (*tempname) + padding = stream_size_k < 90 ? 0 : stream_size_k < 1000 ? 128 : stream_size_k < 10000 ? 1024 : 8192; + + /* Write pages until the correct comment header */ + ogg_page og; + const int vorbis_serial = copy_up_to_codec(in, out, &oy, &og, *tempname ? 0 : offset, offset, VORBISNAME); + if (vorbis_serial <= OGGEDIT_EOF) { + res = vorbis_serial; + goto cleanup; + } + if ((res = copy_up_to_header(in, out, &oy, &og, vorbis_serial)) <= OGGEDIT_EOF) + goto cleanup; + const long pageno = write_vorbis_tags(out, vorbis_serial, vendor, num_tags, tags, padding, &codebooks); + if (pageno < OGGEDIT_EOF) { + res = pageno; + goto cleanup; + } + + /* If we have tempfile, copy the remaining pages */ + if (*tempname) { + if ((res = copy_remaining_pages(in, out, &oy, vorbis_serial, pageno)) < OGGEDIT_EOF) + goto cleanup; + if (rename(tempname, fname)) { + res = OGGEDIT_RENAME_FAILED; + goto cleanup; + } + } + + res = file_size(fname); + +cleanup: + ogg_packet_clear(&codebooks); + cleanup(in, out, &oy, vendor); + if (res < OGGEDIT_OK) + unlink(tempname); + return res; +} diff --git a/plugins/vorbis/Makefile.am b/plugins/vorbis/Makefile.am index 859d1af0..8b09c687 100644 --- a/plugins/vorbis/Makefile.am +++ b/plugins/vorbis/Makefile.am @@ -1,8 +1,8 @@ if HAVE_VORBIS vorbisdir = $(libdir)/$(PACKAGE) pkglib_LTLIBRARIES = vorbis.la -vorbis_la_SOURCES = vorbis.c vcedit.c vcedit.h vceditaux.h i18n.h -vorbis_la_LDFLAGS = -module -avoid-version -lm +vorbis_la_SOURCES = vorbis.c +vorbis_la_LDFLAGS = -module -avoid-version -lm -export-symbols-regex vorbis_load vorbis_la_LIBADD = $(LDADD) $(VORBIS_LIBS) ../liboggedit/liboggedit.a AM_CFLAGS = $(CFLAGS) $(VORBIS_CFLAGS) -std=c99 diff --git a/plugins/vorbis/vcedit.c b/plugins/vorbis/vcedit.c deleted file mode 100644 index 2f30aeb6..00000000 --- a/plugins/vorbis/vcedit.c +++ /dev/null @@ -1,289 +0,0 @@ -/* - This file is part of Deadbeef Player source code - http://deadbeef.sourceforge.net - - Ogg Vorbis plugin Ogg edit functions - - Copyright (C) 2014 Ian Nartowicz <deadbeef@nartowicz.co.uk> - - This software is provided 'as-is', without any express or implied - warranty. In no event will the authors be held liable for any damages - arising from the use of this software. - - Permission is granted to anyone to use this software for any purpose, - including commercial applications, and to alter it and redistribute it - freely, subject to the following restrictions: - - 1. The origin of this software must not be misrepresented; you must not - claim that you wrote the original software. If you use this software - in a product, an acknowledgment in the product documentation would be - appreciated but is not required. - 2. Altered source versions must be plainly marked as such, and must not be - misrepresented as being the original software. - 3. This notice may not be removed or altered from any source distribution. - -*/ - -#ifdef HAVE_CONFIG_H -#include <config.h> -#endif -#include <stdio.h> -#include <stdlib.h> -#include <string.h> -#include <limits.h> -#include <unistd.h> -#include <stdbool.h> -#include <sys/stat.h> -#include <ogg/ogg.h> -#include <deadbeef.h> -#include "vcedit.h" - -#define VORBISNAME "Vorbis" -#define TAGMAGIC "\3vorbis" -#define CODEMAGIC "\5vorbis" - -#define CHUNKSIZE 4096 -#define MAXOGGPAGE 65536 - -static const char *ogg_codec(ogg_page *og) -{ - typedef struct { - const char *magic; - const size_t length; - const char *codec; - } codec_t; - const codec_t codecs[] = { - {.codec = "Opus", .length = 19, .magic = "OpusHead"}, - {.codec = "Vorbis", .length = 30, .magic = "\1vorbis"}, - {.codec = "Flac", .length = 47, .magic = "\177FLAC"}, - {.codec = "Speex", .length = 80, .magic = "Speex "}, - {.codec = "Celt", .length = 80, .magic = "CELT"}, // obsolete - {.codec = "MIDI", .length = 13, .magic = "OggMIDI\0"}, - {.codec = "PCM", .length = 28, .magic = "PCM "}, - {.codec = "Theora", .length = 42, .magic = "\200theora"}, - {.codec = "Daala", .length = 38, .magic = "\200daala"}, - {.codec = "Dirac", .length = 5, .magic = "BBCD\0"}, - {.codec = "Skeleton", .length = 80, .magic = "fishead\0"}, - {.codec = "Kate", .length = 64, .magic = "\200kate\0\0\0"}, - {.codec = "CMML", .length = 29, .magic = "CMML\0\0\0\0"}, - {.codec = "YUV4MPEG", .length = 8, .magic = "YUV4Mpeg"}, - {.codec = "UVS", .length = 48, .magic = "UVS "}, - {.codec = "YUV", .length = 32, .magic = "\0YUV"}, - {.codec = "RGB", .length = 24, .magic = "\0RGB"}, - {.codec = "JNG", .length = 48, .magic = "\213JNG\r\n\032\n"}, - {.codec = "MNG", .length = 48, .magic = "\212MNG\r\n\032\n"}, - {.codec = "PNG", .length = 48, .magic = "\211PNG\r\n\032\n"}, - {.codec = "Spots", .length = 52, .magic = "SPOTS\0\0\0"}, - {.codec = NULL} - }; - - for (const codec_t *match = codecs; match->codec; match++) - if ((size_t)og->body_len >= match->length && !memcmp(og->body, match->magic, strlen(match->codec))) - return match->codec; - - return "unknown"; -} - -static void _oggpack_chars(oggpack_buffer *opb, const char *s, size_t length) -{ - while (length--) - oggpack_write(opb, *s++, 8); -} - -static void _oggpack_string(oggpack_buffer *opb, const char *s) -{ - oggpack_write(opb, strlen(s), 32); - _oggpack_chars(opb, s, strlen(s)); -} - -static int get_page(DB_FILE *in, ogg_sync_state *oy, ogg_page *og, char **lasterror) -{ - uint16_t chunks_left = MAXOGGPAGE / CHUNKSIZE; - while (ogg_sync_pageout(oy, og) != 1) { - char *buffer = ogg_sync_buffer(oy, CHUNKSIZE); - if (!in || !buffer || !chunks_left--) { - *lasterror = "can't find Ogg bitstream."; - return -1; - } - - const size_t bytes = in->vfs->read(buffer, 1, CHUNKSIZE, in); - if (!bytes) { - *lasterror = "unexpected EOF."; - return 0; - } - - ogg_sync_wrote(oy, bytes); - } - - return ogg_page_serialno(og); -} - -static bool write_page(FILE *out, ogg_page *og) -{ - return fwrite(og->header, 1, og->header_len, out) != (size_t)og->header_len || - fwrite(og->body, 1, og->body_len, out) != (size_t)og->body_len; -} - -static int write_page_and_get_next(DB_FILE *in, FILE *out, ogg_sync_state *oy, ogg_page *og, char **lasterror) -{ - if (write_page(out, og)) - return -1; - - return get_page(in, oy, og, lasterror); -} - -static bool write_vorbis_tags(FILE *out, const int serial, const char *vendor, const size_t num_tags, char **tags, ogg_packet *codebooks) -{ - oggpack_buffer opb; - oggpack_writeinit(&opb); - _oggpack_chars(&opb, TAGMAGIC, strlen(TAGMAGIC)); - _oggpack_string(&opb, vendor); - oggpack_write(&opb, num_tags, 32); - for (size_t i = 0; i < num_tags; i++) - _oggpack_string(&opb, tags[i]); - oggpack_write(&opb, 1, 1); - oggpack_writealign(&opb); - - - ogg_stream_state os; - ogg_stream_init(&os, serial); - os.b_o_s = 1; - os.pageno = 1; - ogg_packet op; - memset(&op, '\0', sizeof(op)); - op.packet = oggpack_get_buffer(&opb); - op.bytes = oggpack_bytes(&opb); - ogg_stream_packetin(&os, &op); - oggpack_writeclear(&opb); - ogg_stream_packetin(&os, codebooks); - - ogg_page og; - while (ogg_stream_flush(&os, &og)) - if (write_page(out, &og)) - return true; - - return ogg_stream_check(&os) || ogg_stream_clear(&os); -} - -static int extract_codebooks(DB_FILE *in, ogg_sync_state *oy, ogg_page *og, int serial, ogg_packet *codebooks, char **lasterror) -{ - memset(codebooks, '\0', sizeof(codebooks)); - ogg_stream_state os; - if (ogg_stream_init(&os, serial)) { - *lasterror = "cannot init Ogg stream."; - return -1; - } - os.pageno = 1; - os.b_o_s = 1; - - ogg_packet op; - ogg_stream_pagein(&os, og); - do { - while (ogg_stream_packetpeek(&os, NULL) == 0) { - if ((serial = get_page(in, oy, og, lasterror)) <= 0) - goto cleanup; - fprintf(stderr, "Serial: %d, %ld bytes (%s)\n", serial, og->header_len + og->body_len, og->body); - if (ogg_stream_pagein(&os, og)) { - *lasterror = "failed to stream page for codebooks packet."; - goto cleanup; - } - } - } while (ogg_stream_packetout(&os, &op) != 1 || op.bytes < strlen(CODEMAGIC) || memcmp(op.packet, CODEMAGIC, strlen(CODEMAGIC))); - - if (!(codebooks->packet = malloc(op.bytes))) { - *lasterror = "cannot allocate codebooks packet."; - goto cleanup; - } - codebooks->bytes = op.bytes; - memcpy(codebooks->packet, op.packet, op.bytes); - -cleanup: - ogg_stream_clear(&os); - - return codebooks->packet ? serial : -1; -} - -off_t vcedit_write_metadata(DB_FILE *in, const char *fname, int link, const char *vendor, const int num_tags, char **tags, char **lasterror) -{ - off_t file_size = 0; - ogg_page og; - ogg_sync_state oy; - ogg_sync_init(&oy); - - char outname[PATH_MAX]; - snprintf(outname, PATH_MAX, "%s.temp", fname); - FILE *out = fopen(outname, "w+b"); - if (!out) { - *lasterror = "unable to open temporary file for writing."; - goto cleanup; - } - if (!in) { - *lasterror = "file not opened for reading."; - goto cleanup; - } - struct stat stat_struct; - if (!stat(fname, &stat_struct)) - chmod(outname, stat_struct.st_mode); - - /* Copy through pages until we reach the right info header */ - int codec_serial = 0; - int serial = get_page(in, &oy, &og, lasterror); - while (serial > 0 && !codec_serial) { - while (serial > 0 && !ogg_page_bos(&og)) - serial = write_page_and_get_next(in, out, &oy, &og, lasterror); - - while (serial > 0 && ogg_page_bos(&og)) { - if (link < 1 && !strcmp(ogg_codec(&og), VORBISNAME)) { - codec_serial = serial; - } - serial = write_page_and_get_next(in, out, &oy, &og, lasterror); - } - link--; - } - - /* Copy additional pages up to our comment header */ - while (serial > 0 && serial != codec_serial) - serial = write_page_and_get_next(in, out, &oy, &og, lasterror); - if (serial <= 0) - goto cleanup; - - /* Add the codebook packet to our comment header and save it */ - ogg_packet codebooks; - if ((serial = extract_codebooks(in, &oy, &og, codec_serial, &codebooks, lasterror)) <= 0) - goto cleanup; - const bool write_tags = write_vorbis_tags(out, codec_serial, vendor, num_tags, tags, &codebooks); - ogg_packet_clear(&codebooks); - if (write_tags) { - *lasterror = "internal error writing Vorbis comment header packet."; - goto cleanup; - } - - /* Blindly copy through the remaining pages */ - serial = get_page(in, &oy, &og, lasterror); - while (serial > 0) - serial = write_page_and_get_next(in, out, &oy, &og, lasterror); - if (serial < 0) - goto cleanup; - - fseeko(out, 0, SEEK_END); - file_size = ftello(out); - -cleanup: - if (in) - in->vfs->close(in); - - if (out) - fclose(out); - - ogg_sync_clear(&oy); - - if (file_size <= 0) { - unlink(outname); - if (!*lasterror) - *lasterror = "error writing new file, changes backed out."; - return -1; - } - - rename(outname, fname); - return file_size; -} diff --git a/plugins/vorbis/vcedit.h b/plugins/vorbis/vcedit.h deleted file mode 100644 index 7eabe877..00000000 --- a/plugins/vorbis/vcedit.h +++ /dev/null @@ -1,35 +0,0 @@ -/* - This file is part of Deadbeef Player source code - http://deadbeef.sourceforge.net - - Ogg Vorbis plugin Ogg edit functions header - - Copyright (C) 2014 Ian Nartowicz <deadbeef@nartowicz.co.uk> - - This software is provided 'as-is', without any express or implied - warranty. In no event will the authors be held liable for any damages - arising from the use of this software. - - Permission is granted to anyone to use this software for any purpose, - including commercial applications, and to alter it and redistribute it - freely, subject to the following restrictions: - - 1. The origin of this software must not be misrepresented; you must not - claim that you wrote the original software. If you use this software - in a product, an acknowledgment in the product documentation would be - appreciated but is not required. - 2. Altered source versions must be plainly marked as such, and must not be - misrepresented as being the original software. - 3. This notice may not be removed or altered from any source distribution. - -*/ - -#ifndef __VCEDIT_H -#define __VCEDIT_H - -#define ALBUM_ART_KEY "METADATA_BLOCK_PICTURE" -#define ALBUM_ART_META "metadata_block_picture" - -off_t vcedit_write_metadata(DB_FILE *in, const char *fname, int link, const char *vendor, const int num_tags, char **tags, char **lasterror); - -#endif /* __VCEDIT_H */ diff --git a/plugins/vorbis/vorbis.c b/plugins/vorbis/vorbis.c index 1bd4a382..3fb6bef2 100644 --- a/plugins/vorbis/vorbis.c +++ b/plugins/vorbis/vorbis.c @@ -24,17 +24,15 @@ #ifdef HAVE_CONFIG_H # include <config.h> #endif -#include <vorbis/codec.h> #include <vorbis/vorbisfile.h> #include <string.h> #include <stdlib.h> -#include <assert.h> #include <limits.h> #include <unistd.h> #include <math.h> #include <stdbool.h> #include "../../deadbeef.h" -#include "vcedit.h" +#include "../liboggedit/oggedit.h" #define min(x,y) ((x)<(y)?(x):(y)) #define max(x,y) ((x)>(y)?(x):(y)) @@ -42,6 +40,12 @@ //#define trace(...) { fprintf (stderr, __VA_ARGS__); } #define trace(fmt,...) +#if WORDS_BIGENDIAN +#define CVORBIS_ENDIANNESS 1 +#else +#define CVORBIS_ENDIANNESS 0 +#endif + #define RG_REFERENCE_LOUDNESS -1 #define DELIMITER "\n - \n" @@ -51,13 +55,11 @@ static DB_functions_t *deadbeef; typedef struct { DB_fileinfo_t info; OggVorbis_File vorbis_file; - vorbis_info *vi; int cur_bit_stream; - int startsample; - int endsample; - int currentsample; - int last_comment_update; - DB_playItem_t *ptrack; + float next_update; + DB_playItem_t *it; + const DB_playItem_t *new_track; + uint8_t *channel_map; } ogg_info_t; static size_t @@ -88,18 +90,18 @@ static const char *gain_tag_name(const int tag_enum) { switch(tag_enum) { - case DDB_REPLAYGAIN_ALBUMGAIN: - return "REPLAYGAIN_ALBUM_GAIN"; - case DDB_REPLAYGAIN_ALBUMPEAK: - return "REPLAYGAIN_ALBUM_PEAK"; - case DDB_REPLAYGAIN_TRACKGAIN: - return "REPLAYGAIN_TRACK_GAIN"; - case DDB_REPLAYGAIN_TRACKPEAK: - return "REPLAYGAIN_TRACK_PEAK"; - case RG_REFERENCE_LOUDNESS: - return "REPLAYGAIN_REFERENCE_LOUDNESS"; - default: - return NULL; + case DDB_REPLAYGAIN_ALBUMGAIN: + return "REPLAYGAIN_ALBUM_GAIN"; + case DDB_REPLAYGAIN_ALBUMPEAK: + return "REPLAYGAIN_ALBUM_PEAK"; + case DDB_REPLAYGAIN_TRACKGAIN: + return "REPLAYGAIN_TRACK_GAIN"; + case DDB_REPLAYGAIN_TRACKPEAK: + return "REPLAYGAIN_TRACK_PEAK"; + case RG_REFERENCE_LOUDNESS: + return "REPLAYGAIN_REFERENCE_LOUDNESS"; + default: + return NULL; } } @@ -107,18 +109,18 @@ static const char *gain_meta_key(const int tag_enum) { switch(tag_enum) { - case DDB_REPLAYGAIN_ALBUMGAIN: - return ":REPLAYGAIN_ALBUMGAIN"; - case DDB_REPLAYGAIN_ALBUMPEAK: - return ":REPLAYGAIN_ALBUMPEAK"; - case DDB_REPLAYGAIN_TRACKGAIN: - return ":REPLAYGAIN_TRACKGAIN"; - case DDB_REPLAYGAIN_TRACKPEAK: - return ":REPLAYGAIN_TRACKPEAK"; - case RG_REFERENCE_LOUDNESS: - return ":REPLAYGAIN_REFERENCE_LOUDNESS"; - default: - return NULL; + case DDB_REPLAYGAIN_ALBUMGAIN: + return ":REPLAYGAIN_ALBUMGAIN"; + case DDB_REPLAYGAIN_ALBUMPEAK: + return ":REPLAYGAIN_ALBUMPEAK"; + case DDB_REPLAYGAIN_TRACKGAIN: + return ":REPLAYGAIN_TRACKGAIN"; + case DDB_REPLAYGAIN_TRACKPEAK: + return ":REPLAYGAIN_TRACKPEAK"; + case RG_REFERENCE_LOUDNESS: + return ":REPLAYGAIN_REFERENCE_LOUDNESS"; + default: + return NULL; } } @@ -131,95 +133,62 @@ is_special_tag(const char *tag) { !strcasecmp(tag, gain_tag_name(RG_REFERENCE_LOUDNESS)); } -static const char *metainfo[] = { - "ARTIST", "artist", - "TITLE", "title", - "ALBUM", "album", - "TRACKNUMBER", "track", - "DATE", "year", - "GENRE", "genre", - "COMMENT", "comment", - "PERFORMER", "performer", - "COMPOSER", "composer", - "DISCNUMBER", "disc", - "COPYRIGHT", "copyright", - "TOTALTRACKS", "numtracks", - "TRACKTOTAL", "numtracks", - NULL -}; +static bool +add_meta(DB_playItem_t *it, const char *key, const char *value) +{ + const char *old_value = deadbeef->pl_find_meta(it, key); + if (old_value) { + char *new_value = malloc(strlen(old_value) + strlen(DELIMITER) + strlen(value) + 1); + if (new_value) { + sprintf(new_value, "%s"DELIMITER"%s", old_value, value); + deadbeef->pl_replace_meta(it, key, new_value); + free(new_value); + return true; + } + } + else { + deadbeef->pl_add_meta(it, key, value); + return true; + } +} + +static bool +replaygain_tag(DB_playItem_t *it, const int tag_enum, const char *tag, const char *value) +{ + if (strcasecmp(gain_tag_name(tag_enum), tag)) + return false; + + deadbeef->pl_set_item_replaygain (it, tag_enum, atof(value)); + return true; +} -// refresh_playlist == 1 means "send playlistchanged event if metadata had been changed" -// refresh_playlist == 2 means "don't change memory, just check for changes" static int -update_vorbis_comments (DB_playItem_t *it, vorbis_comment *vc, int refresh_playlist) { - if (refresh_playlist == 1) { - if (!update_vorbis_comments (it, vc, 2)) { - return 0; - } +update_vorbis_comments (DB_playItem_t *it, OggVorbis_File *vorbis_file, const int tracknum) { + const vorbis_comment *vc = ov_comment(vorbis_file, tracknum); + if (!vc) { + trace("update_vorbis_comments: ov_comment failed\n"); + return -1; } - if (vc) { - if (refresh_playlist != 2) { - deadbeef->pl_delete_all_meta (it); - } - for (int i = 0; i < vc->comments; i++) { - char *s = vc->user_comments[i]; - int m; - for (m = 0; metainfo[m]; m += 2) { - int l = strlen (metainfo[m]); - if (vc->comment_lengths[i] > l && !strncasecmp (metainfo[m], s, l) && s[l] == '=') { - if (refresh_playlist == 2) { - deadbeef->pl_lock (); - const char *val = deadbeef->pl_find_meta (it, metainfo[m+1]); - if (!val || strcmp (val, s+l+1)) { - deadbeef->pl_unlock (); - return 1; - } - deadbeef->pl_unlock (); - } - else { - deadbeef->pl_append_meta (it, metainfo[m+1], s + l + 1); - break; - } - } - } - if (!metainfo[m] && refresh_playlist != 2) { - if (!strncasecmp (s, "cuesheet=", 9)) { - deadbeef->pl_add_meta (it, "cuesheet", s + 9); - } - else if (!strncasecmp (s, "replaygain_album_gain=", 22)) { - deadbeef->pl_set_item_replaygain (it, DDB_REPLAYGAIN_ALBUMGAIN, atof (s+22)); - } - else if (!strncasecmp (s, "replaygain_album_peak=", 22)) { - deadbeef->pl_set_item_replaygain (it, DDB_REPLAYGAIN_ALBUMPEAK, atof (s+22)); - } - else if (!strncasecmp (s, "replaygain_track_gain=", 22)) { - deadbeef->pl_set_item_replaygain (it, DDB_REPLAYGAIN_TRACKGAIN, atof (s+22)); - } - else if (!strncasecmp (s, "replaygain_track_peak=", 22)) { - deadbeef->pl_set_item_replaygain (it, DDB_REPLAYGAIN_TRACKPEAK, atof (s+22)); + deadbeef->pl_delete_all_meta (it); + for (int i = 0; i < vc->comments; i++) { + char *tag = strdup(vc->user_comments[i]); + char *value; + if (tag && (value = strchr(tag, '='))) { + *value++ = '\0'; + if (!replaygain_tag(it, DDB_REPLAYGAIN_ALBUMGAIN, tag, value) && + !replaygain_tag(it, DDB_REPLAYGAIN_ALBUMPEAK, tag, value) && + !replaygain_tag(it, DDB_REPLAYGAIN_TRACKGAIN, tag, value) && + !replaygain_tag(it, DDB_REPLAYGAIN_TRACKPEAK, tag, value)) { + if (!strcasecmp(tag, gain_tag_name(RG_REFERENCE_LOUDNESS))) + deadbeef->pl_replace_meta(it, gain_meta_key(RG_REFERENCE_LOUDNESS), value); + else + add_meta(it, oggedit_map_tag(tag, "tag2meta"), value); } - else if (!strncasecmp (s, "replaygain_reference_loudness=", 30)) { - deadbeef->pl_replace_meta(it, gain_meta_key(RG_REFERENCE_LOUDNESS), s+30); - } - else { - const char *p = s; - while (*p && *p != '=') { - p++; - } - if (*p == '=') { - char key[p-s+1]; - memcpy (key, s, p-s); - key[p-s] = 0; - deadbeef->pl_add_meta (it, key, p+1); - } - } - } + free(tag); } } - if (refresh_playlist == 2) { - return 0; - } + deadbeef->pl_add_meta (it, "title", NULL); uint32_t f = deadbeef->pl_get_item_flags (it); f &= ~DDB_TAG_MASK; @@ -230,35 +199,59 @@ update_vorbis_comments (DB_playItem_t *it, vorbis_comment *vc, int refresh_playl deadbeef->plt_modified (plt); deadbeef->plt_unref (plt); } + deadbeef->sendmessage (DB_EV_PLAYLISTCHANGED, 0, 0, 0); + + return 0; +} - if (refresh_playlist) { - deadbeef->sendmessage (DB_EV_PLAYLISTCHANGED, 0, 0, 0); +static void +set_meta_ll(DB_playItem_t *it, const char *key, const int64_t value) +{ + char string[11]; + sprintf(string, "%lld", value); + deadbeef->pl_replace_meta(it, key, string); +} + +static int +cvorbis_seek_sample (DB_fileinfo_t *_info, int sample) { + ogg_info_t *info = (ogg_info_t *)_info; + if (sample < 0) { + trace ("vorbis: negative seek sample - ignored, but it is a bug!\n"); + return -1; + } + if (!info->info.file) { + trace ("vorbis: file is NULL on seek\n"); + return -1; } + sample += info->it->startsample; + trace ("vorbis: seek to sample %d\n", sample); + int res = ov_pcm_seek (&info->vorbis_file, sample); + if (res != 0 && res != OV_ENOSEEK) { + trace ("vorbis: error %x seeking to sample %d\n", res, sample); + return -1; + } + int tell = ov_pcm_tell (&info->vorbis_file); + if (tell != sample) { + trace ("vorbis: failed to do sample-accurate seek (%d->%d)\n", sample, tell); + } + trace ("vorbis: seek successful\n") + _info->readpos = (float)(sample - info->it->startsample)/_info->fmt.samplerate; + info->next_update = -2; return 0; } static DB_fileinfo_t * cvorbis_open (uint32_t hints) { - DB_fileinfo_t *_info = malloc (sizeof (ogg_info_t)); - ogg_info_t *info = (ogg_info_t *)_info; - memset (info, 0, sizeof (ogg_info_t)); - return _info; + return calloc(1, sizeof (ogg_info_t)); } static int cvorbis_init (DB_fileinfo_t *_info, DB_playItem_t *it) { + trace("cvorbis_init\n"); ogg_info_t *info = (ogg_info_t *)_info; - info->info.file = NULL; - info->vi = NULL; - if (it->endsample > 0) { - info->cur_bit_stream = -1; - } - else { - int tracknum = deadbeef->pl_find_meta_int (it, ":TRACKNUM", -1); - info->cur_bit_stream = tracknum; - } - info->ptrack = it; + info->new_track = info->it = it; deadbeef->pl_item_ref (it); + deadbeef->pl_replace_meta (it, "!FILETYPE", "OggVorbis"); deadbeef->pl_lock (); info->info.file = deadbeef->fopen (deadbeef->pl_find_meta (it, ":URI")); @@ -267,8 +260,7 @@ cvorbis_init (DB_fileinfo_t *_info, DB_playItem_t *it) { trace ("ogg: failed to open file %s\n", deadbeef->pl_find_meta (it, ":URI")); return -1; } - int ln = deadbeef->fgetlength (info->info.file); - if (info->info.file->vfs->is_streaming () && ln == -1) { + if (info->info.file->vfs->is_streaming () && deadbeef->fgetlength (info->info.file) == -1) { ov_callbacks ovcb = { .read_func = cvorbis_fread, .seek_func = NULL, @@ -304,61 +296,55 @@ cvorbis_init (DB_fileinfo_t *_info, DB_playItem_t *it) { trace ("ov_open_callbacks returned %d\n", err); return -1; } -// deadbeef->pl_set_item_duration (it, ov_time_total (&vorbis_file, -1)); } - info->vi = ov_info (&info->vorbis_file, info->cur_bit_stream); - if (!info->vi) { // not a vorbis stream +// _info->readpos = 0; + if (info->info.file->vfs->is_streaming ()) { + info->it->startsample = 0; + if (deadbeef->pl_get_item_duration (it) < 0) { + it->endsample = -1; + } + else { + it->endsample = ov_pcm_total (&info->vorbis_file, -1)-1; + } + if (update_vorbis_comments (it, &info->vorbis_file, -1)) + return -1; + deadbeef->pl_set_meta_int(info->it, ":TRACKNUM", 0); + } + else { + cvorbis_seek_sample (_info, 1); // vorbisfile bug + } + + vorbis_info *vi = ov_info (&info->vorbis_file, -1); + if (!vi) { trace ("not a vorbis stream\n"); return -1; } - if (info->vi->rate <= 0) { + if (vi->rate <= 0) { trace ("vorbis: bad samplerate\n"); return -1; } _info->plugin = &plugin; _info->fmt.bps = 16; - //_info->dataSize = ov_pcm_total (&vorbis_file, -1) * vi->channels * 2; - _info->fmt.channels = info->vi->channels; - _info->fmt.samplerate = info->vi->rate; - + _info->fmt.samplerate = vi->rate; + _info->fmt.channels = vi->channels; + info->channel_map = oggedit_vorbis_channel_map(vi->channels); for (int i = 0; i < _info->fmt.channels; i++) { _info->fmt.channelmask |= 1 << i; } - _info->readpos = 0; - info->currentsample = 0; - if (!info->info.file->vfs->is_streaming ()) { - if (it->endsample > 0) { - info->startsample = it->startsample; - info->endsample = it->endsample; - plugin.seek_sample (_info, 0); - } - else { - info->startsample = 0; - info->endsample = ov_pcm_total (&info->vorbis_file, -1)-1; - } - } - else { - info->startsample = 0; - if (deadbeef->pl_get_item_duration (it) < 0) { - info->endsample = -1; - } - else { - info->endsample = ov_pcm_total (&info->vorbis_file, -1)-1; - } - vorbis_comment *vc = ov_comment (&info->vorbis_file, -1); - update_vorbis_comments (it, vc, 1); - } return 0; } static void cvorbis_free (DB_fileinfo_t *_info) { + trace("cvorbis_free\n"); ogg_info_t *info = (ogg_info_t *)_info; if (info) { - if (info->ptrack) { - deadbeef->pl_item_unref (info->ptrack); + if (info->it) { + deadbeef->pl_item_unref (info->it); } + free(info->channel_map); + info->channel_map = NULL; if (info->info.file) { if (info->vorbis_file.datasource) { ov_clear (&info->vorbis_file); @@ -371,165 +357,137 @@ cvorbis_free (DB_fileinfo_t *_info) { } } -static int -cvorbis_read (DB_fileinfo_t *_info, char *bytes, int size) { - ogg_info_t *info = (ogg_info_t *)_info; -// trace ("cvorbis_read %d bytes\n", size); +static void send_event(DB_playItem_t *it, const int event_enum) +{ + ddb_event_track_t *event = (ddb_event_track_t *)deadbeef->event_alloc(event_enum); + if (event->track = it) + deadbeef->pl_item_ref(event->track); + deadbeef->event_send((ddb_event_t *)event, 0, 0); +} - _info->fmt.channels = info->vi->channels; - _info->fmt.samplerate = info->vi->rate; +static bool +new_streaming_link(ogg_info_t *info, const int new_link) +{ + if (info->cur_bit_stream == 0 && new_link != 1) + return false; + + trace("Streaming link changed from %d to %d\n", info->cur_bit_stream, new_link); + deadbeef->pl_set_meta_int(info->it, ":TRACKNUM", new_link); + update_vorbis_comments(info->it, &info->vorbis_file, new_link); + send_event(info->it, DB_EV_SONGSTARTED); + send_event(info->it, DB_EV_TRACKINFOCHANGED); + deadbeef->sendmessage(DB_EV_PLAYLISTCHANGED, 0, 0, 0); + info->cur_bit_stream = new_link; + + vorbis_info *vi = ov_info (&info->vorbis_file, new_link); + if (vi && info->info.fmt.channels != vi->channels || info->info.fmt.samplerate != vi->rate) { + // Streamer can't do this, so re-init the stream + trace("Stream channel count changed from %d to %d\n", info->info.fmt.channels, vi->channels); + trace("Stream channel count changed from %d to %d\n", info->info.fmt.samplerate, vi->rate); + deadbeef->sendmessage(DB_EV_PAUSE, 0, 0, 0); + deadbeef->sendmessage(DB_EV_TOGGLE_PAUSE, 0, 0, 0); + return true; + } + + return false; +} - int samplesize = _info->fmt.channels * _info->fmt.bps / 8; +static void +map_channels(int16_t *dest, const int16_t *src, const int16_t *src_end, const uint8_t *map, const int channel_count) +{ + while (src < src_end) { + for (uint8_t *map_ptr = (uint8_t *)map; map_ptr < map + channel_count; map_ptr++, src++) + dest[*map_ptr] = *src; + dest += channel_count; + } +} - if (!info->info.file->vfs->is_streaming ()) { - if (info->currentsample + size / samplesize > info->endsample) { - size = (info->endsample - info->currentsample + 1) * samplesize; - trace ("size truncated to %d bytes, cursample=%d, info->endsample=%d, totalsamples=%lld\n", size, info->currentsample, info->endsample, ov_pcm_total (&info->vorbis_file, -1)); - if (size <= 0) { - return 0; - } - } +static bool +is_playing_track(const DB_playItem_t *it) +{ + DB_playItem_t *track = deadbeef->streamer_get_playing_track(); + if (track) + deadbeef->pl_item_unref(track); + return track == it; +} + +static int +cvorbis_read (DB_fileinfo_t *_info, char *buffer, int bytes_to_read) { + ogg_info_t *info = (ogg_info_t *)_info; + + /* Work round some streamer limitations and infobar issue #22 */ + if (info->new_track && is_playing_track(info->new_track)) { + info->new_track = NULL; + send_event(info->it, DB_EV_TRACKINFOCHANGED); + info->next_update = -2; } - else { - if (info->ptrack && info->currentsample - info->last_comment_update > 5 * _info->fmt.samplerate) { - if (info->ptrack) { - info->last_comment_update = info->currentsample; - vorbis_comment *vc = ov_comment (&info->vorbis_file, -1); - update_vorbis_comments (info->ptrack, vc, 1); - ddb_event_track_t *ev = (ddb_event_track_t *)deadbeef->event_alloc (DB_EV_TRACKINFOCHANGED); - ev->track = info->ptrack; - if (ev->track) { - deadbeef->pl_item_ref (ev->track); - } - deadbeef->event_send ((ddb_event_t *)ev, 0, 0); - } - else { - info->ptrack = NULL; - } - } + + /* Don't read past the end of a sub-track */ + if (deadbeef->pl_get_item_flags(info->it) & DDB_IS_SUBTRACK) { + const ogg_int64_t bytes_left = (info->it->endsample - ov_pcm_tell(&info->vorbis_file)) * 2 * _info->fmt.channels; + if (bytes_left < bytes_to_read) + bytes_to_read = bytes_left; } -// trace ("cvorbis_read %d bytes[2]\n", size); - int initsize = size; - int64_t ret; - for (;;) - { - // read ogg - int endianess = 0; -#if WORDS_BIGENDIAN - endianess = 1; -#endif - if (_info->fmt.channels <= 2 || _info->fmt.channels == 4) { - ret = (int64_t)ov_read (&info->vorbis_file, bytes, size, endianess, 2, 1, &info->cur_bit_stream); - } - else { - int16_t temp[size/2]; - ret=ov_read (&info->vorbis_file, (char *)temp, size, endianess, 2, 1, &info->cur_bit_stream); - if (ret > 0) { - // remap channels to wav format - int idx = _info->fmt.channels - 3; - static int remap[6][8] = { - {0,2,1}, - {0,1,2,3}, // should not be used - {0,2,1,3,4}, - {0,2,1,4,5,3}, - {0,2,1,4,5,6,3}, - {0,2,1,6,7,4,5,3} - }; - - if (_info->fmt.channels > 8) { - fprintf (stderr, "vorbis plugin doesn't support more than 8 channels\n"); - return -1; - } + /* Read until we have enough bytes to satisfy streamer, or there are none left */ + char map_buffer[info->channel_map ? bytes_to_read : 0]; + char *ptr = info->channel_map ? map_buffer : buffer; + int ret = OV_HOLE; + int bytes_read = 0; + while ((ret > 0 || ret == OV_HOLE) && bytes_read < bytes_to_read) + { + int new_link = -1; + ret=ov_read (&info->vorbis_file, ptr+bytes_read, bytes_to_read-bytes_read, CVORBIS_ENDIANNESS, 2, 1, &new_link); - int i, j; - int16_t *src = temp; - int n = ret / samplesize; - for (i = 0; i < n; i++) { - for (j = 0; j < _info->fmt.channels; j++) { - ((int16_t *)(bytes + samplesize * i))[remap[idx][j]] = src[j]; - } - src += _info->fmt.channels; - } - } - } - if (ret <= 0) - { - if (ret < 0) { - trace ("ov_read returned %lld\n", ret); - switch (ret) { - case OV_HOLE: - trace ("OV_HOLE\n"); - break; - case OV_EBADLINK: - trace ("OV_EBADLINK\n"); - break; - case OV_EINVAL: - trace ("OV_EINVAL\n"); - break; - } - } - if (ret == OV_HOLE) { - continue; - } - // error or eof - break; + if (ret < 0) { + trace("cvorbis_read: ov_read returned %d\n", ret); } - else if (ret < size) - { - info->currentsample += ret / samplesize; - size -= ret; - bytes += ret; + else if (new_link != info->cur_bit_stream && !ov_seekable(&info->vorbis_file) && new_streaming_link(info, new_link)) { + bytes_read = bytes_to_read; } else { - info->currentsample += ret / samplesize; - size = 0; - break; + bytes_read += ret; } - } - _info->readpos = (float)(ov_pcm_tell(&info->vorbis_file)-info->startsample)/info->vi->rate; - //trace ("cvorbis_read got %d bytes, readpos %f, info->currentsample %d, ret %d\n", initsize-size, _info->readpos, info->currentsample, ret); - deadbeef->streamer_set_bitrate (ov_bitrate (&info->vorbis_file, info->cur_bit_stream)/1000); - return initsize - size; -} -static int -cvorbis_seek_sample (DB_fileinfo_t *_info, int sample) { - ogg_info_t *info = (ogg_info_t *)_info; - if (sample < 0) { - trace ("vorbis: negative seek sample - ignored, but it is a bug!\n"); - return -1; - } - if (!info->info.file) { - trace ("vorbis: file is NULL on seek\n"); - return -1; - } - trace ("vorbis: seek to sample %d\n", sample); - sample += info->startsample; - int res = ov_pcm_seek (&info->vorbis_file, sample); - if (res != 0 && res != OV_ENOSEEK) { - trace ("vorbis: error %x seeking to sample %d\n", res, sample); - return -1; +// trace("cvorbis_read got %d bytes towards %d bytes (%d bytes still required)\n", ret, bytes_to_read, bytes_to_read-bytes_read); } - int tell = ov_pcm_tell (&info->vorbis_file); - if (tell != sample) { - trace ("oggvorbis: failed to do sample-accurate seek (%d->%d)\n", sample, tell); + + if (info->channel_map) + map_channels((int16_t *)buffer, (int16_t *)map_buffer, (int16_t *)(ptr+bytes_read), info->channel_map, _info->fmt.channels); + + _info->readpos = (float)(ov_pcm_tell(&info->vorbis_file) - info->it->startsample) / _info->fmt.samplerate; + if (_info->readpos > info->next_update) { + const int rate = ov_bitrate_instant(&info->vorbis_file) / 1000; + if (rate > 0) { + deadbeef->streamer_set_bitrate(rate); + info->next_update = info->next_update <= 0 ? info->next_update + 1 : _info->readpos + 5; + } } - trace ("vorbis: seek successful\n") - info->currentsample = sample; - _info->readpos = (float)(ov_pcm_tell(&info->vorbis_file) - info->startsample)/info->vi->rate; - return 0; +// trace("cvorbis_read returning %d bytes out of %d\n", bytes_read, bytes_to_read); + return bytes_read; } static int cvorbis_seek (DB_fileinfo_t *_info, float time) { ogg_info_t *info = (ogg_info_t *)_info; - return cvorbis_seek_sample (_info, time * info->vi->rate); + return cvorbis_seek_sample (_info, time * info->info.fmt.samplerate); +} + +static off_t sample_offset(OggVorbis_File *vorbis_file, const ogg_int64_t sample) +{ + if (sample <= 0 || sample == ov_pcm_total(vorbis_file, -1)) + return 0; + + if (ov_pcm_seek(vorbis_file, sample)) { + trace("ov_pcm_seek failed to find sample %lld: %d\n", sample, ov_pcm_seek(vorbis_file, sample)); + return -1; + } + + return ov_raw_tell(vorbis_file); } static DB_playItem_t * cvorbis_insert (ddb_playlist_t *plt, DB_playItem_t *after, const char *fname) { - // check for validity DB_FILE *fp = deadbeef->fopen (fname); if (!fp) { trace ("vorbis: failed to fopen %s\n", fname); @@ -538,7 +496,6 @@ cvorbis_insert (ddb_playlist_t *plt, DB_playItem_t *after, const char *fname) { int64_t fsize = deadbeef->fgetlength (fp); if (fp->vfs->is_streaming ()) { DB_playItem_t *it = deadbeef->pl_item_alloc_init (fname, plugin.plugin.id); - deadbeef->pl_add_meta (it, "!FILETYPE", "OggVorbis"); deadbeef->plt_set_item_duration (plt, it, -1); deadbeef->pl_add_meta (it, "title", NULL); after = deadbeef->plt_insert_item (plt, after, it); @@ -553,7 +510,6 @@ cvorbis_insert (ddb_playlist_t *plt, DB_playItem_t *after, const char *fname) { .tell_func = cvorbis_ftell }; OggVorbis_File vorbis_file; - vorbis_info *vi; int err = ov_open_callbacks (fp, &vorbis_file, NULL, 0, ovcb); if (err != 0) { trace ("ov_open_callbacks returned %d\n", err); @@ -565,8 +521,8 @@ cvorbis_insert (ddb_playlist_t *plt, DB_playItem_t *after, const char *fname) { long nstreams = ov_streams (&vorbis_file); int currentsample = 0; for (int stream = 0; stream < nstreams; stream++) { - vi = ov_info (&vorbis_file, stream); - if (!vi) { // not a vorbis stream + const vorbis_info *vi = ov_info (&vorbis_file, stream); + if (!vi) { trace ("vorbis: ov_info failed for file %s stream %d\n", fname, stream); continue; } @@ -574,32 +530,35 @@ cvorbis_insert (ddb_playlist_t *plt, DB_playItem_t *after, const char *fname) { int totalsamples = ov_pcm_total (&vorbis_file, stream); DB_playItem_t *it = deadbeef->pl_item_alloc_init (fname, plugin.plugin.id); - deadbeef->pl_add_meta (it, ":FILETYPE", "OggVorbis"); deadbeef->pl_set_meta_int (it, ":TRACKNUM", stream); deadbeef->plt_set_item_duration (plt, it, duration); if (nstreams > 1) { it->startsample = currentsample; - it->endsample = currentsample + totalsamples; + it->endsample = currentsample + totalsamples - 1; deadbeef->pl_set_item_flags (it, DDB_IS_SUBTRACK); } - // metainfo - vorbis_comment *vc = ov_comment (&vorbis_file, stream); - update_vorbis_comments (it, vc, 0); + if (update_vorbis_comments (it, &vorbis_file, stream)) + continue; int samplerate = vi->rate; - char s[100]; - snprintf (s, sizeof (s), "%lld", fsize); - deadbeef->pl_add_meta (it, ":FILE_SIZE", s); + const off_t start_offset = sample_offset(&vorbis_file, it->startsample-1); + const off_t end_offset = sample_offset(&vorbis_file, it->endsample); + char *filetype = NULL; + const off_t stream_size = oggedit_vorbis_stream_info(deadbeef->fopen(fname), start_offset, end_offset, &filetype); + if (filetype) { + deadbeef->pl_replace_meta(it, ":FILETYPE", filetype); + free(filetype); + } + if (stream_size > 0) { + set_meta_ll(it, ":STREAM SIZE", stream_size); + deadbeef->pl_set_meta_int(it, ":BITRATE", 8.f * samplerate * stream_size / totalsamples / 1000); + } + set_meta_ll (it, ":FILE_SIZE", fsize); deadbeef->pl_add_meta (it, ":BPS", "16"); - snprintf (s, sizeof (s), "%d", vi->channels); - deadbeef->pl_add_meta (it, ":CHANNELS", s); - snprintf (s, sizeof (s), "%d", samplerate); - deadbeef->pl_add_meta (it, ":SAMPLERATE", s); - int br = ov_bitrate (&vorbis_file, stream)/1000; - snprintf (s, sizeof (s), "%d", br); - deadbeef->pl_add_meta (it, ":BITRATE", s); - + deadbeef->pl_set_meta_int (it, ":CHANNELS", vi->channels); + deadbeef->pl_set_meta_int (it, ":SAMPLERATE", samplerate); +// deadbeef->pl_set_meta_int (it, ":BITRATE", ov_bitrate (&vorbis_file, stream)/1000); if (nstreams == 1) { DB_playItem_t *cue = deadbeef->plt_insert_cue (plt, after, it, totalsamples, samplerate); @@ -610,10 +569,12 @@ cvorbis_insert (ddb_playlist_t *plt, DB_playItem_t *after, const char *fname) { return cue; } - // embedded cue deadbeef->pl_lock (); - const char *cuesheet = deadbeef->pl_find_meta (it, "cuesheet"); - if (cuesheet) { + const char *cuesheet_meta = deadbeef->pl_find_meta (it, "cuesheet"); + if (cuesheet_meta) { + trace("Embedded cuesheet found for %s\n", fname); + const char *last_sheet = strstr(cuesheet_meta, DELIMITER); + const char *cuesheet = last_sheet ? last_sheet + strlen(DELIMITER) : cuesheet_meta; cue = deadbeef->plt_insert_cue_from_buffer (plt, after, it, cuesheet, strlen (cuesheet), totalsamples, samplerate); if (cue) { deadbeef->pl_unlock (); @@ -647,7 +608,6 @@ vorbis_stop (void) { } int cvorbis_read_metadata (DB_playItem_t *it) { - int err = -1; DB_FILE *fp = NULL; OggVorbis_File vorbis_file; vorbis_info *vi = NULL; @@ -661,7 +621,7 @@ cvorbis_read_metadata (DB_playItem_t *it) { } if (fp->vfs->is_streaming ()) { trace ("cvorbis_read_metadata: failed to fopen %s\n", deadbeef->pl_find_meta (it, ":URI")); - goto error; + return -1; } ov_callbacks ovcb = { .read_func = cvorbis_fread, @@ -672,28 +632,23 @@ cvorbis_read_metadata (DB_playItem_t *it) { int res = ov_open_callbacks (fp, &vorbis_file, NULL, 0, ovcb); if (res != 0) { trace ("cvorbis_read_metadata: ov_open_callbacks returned %d\n", res); - goto error; + return -1; } int tracknum = deadbeef->pl_find_meta_int (it, ":TRACKNUM", -1); vi = ov_info (&vorbis_file, tracknum); - if (!vi) { // not a vorbis stream + if (!vi) { trace ("cvorbis_read_metadata: failed to ov_open %s\n", deadbeef->pl_find_meta (it, ":URI")); - goto error; - } - - // metainfo - vorbis_comment *vc = ov_comment (&vorbis_file, tracknum); - if (vc) { - update_vorbis_comments (it, vc, 0); + ov_clear (&vorbis_file); + return -1; } - err = 0; -error: - if (fp) { + if (update_vorbis_comments (it, &vorbis_file, tracknum)) { ov_clear (&vorbis_file); + return -1; } - return err; + ov_clear (&vorbis_file); + return 0; } static void @@ -733,50 +688,16 @@ merge_gain_tag(DB_playItem_t *it, vorbis_comment *vc, vorbis_comment *tags, cons } } -static const char -*map_tag(const char *key) -{ - for (int i = 0; metainfo[i]; i += 2) - if (!strcasecmp (metainfo[i+1], key)) - return metainfo[i]; - return key; -} - static vorbis_comment -*create_tags_list(DB_playItem_t *it, const char *fname, vorbis_comment *tags) +*tags_list(DB_playItem_t *it, OggVorbis_File *vorbis_file) { - DB_FILE *fp = deadbeef->fopen (fname); - if (!fp) { - trace ("vorbis: failed to fopen %s\n", fname); - return NULL; - } - ov_callbacks ovcb = { - .read_func = cvorbis_fread, - .seek_func = cvorbis_fseek, - .close_func = cvorbis_fclose, - .tell_func = cvorbis_ftell - }; - OggVorbis_File vorbis_file; - int err = ov_test_callbacks (fp, &vorbis_file, NULL, 0, ovcb); - if (err != 0) { - trace ("ov_test_callbacks returned %d\n", err); - deadbeef->fclose (fp); + vorbis_comment *vc = ov_comment (vorbis_file, -1); + if (!vc) return NULL; - } - vorbis_comment *vc = ov_comment (&vorbis_file, -1); - if (!vc) { - trace ("ov_comment failed\n"); - ov_clear (&vorbis_file); + vorbis_comment *tags = calloc(1, sizeof(vorbis_comment)); + if (!tags) return NULL; - } - - vorbis_comment_init(tags); - if (!(tags->vendor = strdup(vc->vendor))) { - ov_clear (&vorbis_file); - trace("create_tags_list: cannot allocate tags list\n"); - return NULL; - } deadbeef->pl_lock (); merge_gain_tag(it, vc, tags, DDB_REPLAYGAIN_ALBUMGAIN, "%0.2f dB", -100, 100); @@ -784,15 +705,15 @@ static vorbis_comment merge_gain_tag(it, vc, tags, DDB_REPLAYGAIN_TRACKGAIN, "%0.2f dB", -100, 100); merge_gain_tag(it, vc, tags, DDB_REPLAYGAIN_TRACKPEAK, "%0.8f", 0, 2); merge_gain_tag(it, vc, tags, RG_REFERENCE_LOUDNESS, "%0.1f dB", 0, 128); - DB_metaInfo_t *m = deadbeef->pl_get_metadata_head(it); - while (m) { - if (m->key[0] != ':' && m->key[0] != '!' && !is_special_tag(m->key)) - split_tag(tags, map_tag(m->key), (char *)m->value); - m = m->next; + for (DB_metaInfo_t *m = deadbeef->pl_get_metadata_head(it); m; m = m->next) { + char *key = strdup(m->key); + if (key && key[0] != ':' && key[0] != '!' && !is_special_tag(key)) { + split_tag(tags, oggedit_map_tag(key, "meta2tag"), m->value); + free(key); + } } deadbeef->pl_unlock (); - ov_clear (&vorbis_file); return tags; } @@ -801,22 +722,46 @@ cvorbis_write_metadata (DB_playItem_t *it) { char fname[PATH_MAX]; deadbeef->pl_get_meta (it, ":URI", fname, sizeof (fname)); - vorbis_comment tags; - if (!create_tags_list(it, fname, &tags)) + DB_FILE *fp = deadbeef->fopen (fname); + if (!fp) { + trace ("cvorbis_write_metadata: failed to fopen %s\n", fname); return -1; + } + ov_callbacks ovcb = { + .read_func = cvorbis_fread, + .seek_func = cvorbis_fseek, + .close_func = cvorbis_fclose, + .tell_func = cvorbis_ftell + }; + OggVorbis_File vorbis_file; + int err = ov_test_callbacks (fp, &vorbis_file, NULL, 0, ovcb); + if (err != 0) { + trace ("ov_test_callbacks returned %d\n", err); + deadbeef->fclose (fp); + return -1; + } + + vorbis_comment *tags = tags_list(it, &vorbis_file); + ov_clear(&vorbis_file); + if (!tags) { + trace("cvorbis_write_metadata: tags list allocation failed\n"); + return -1; + } - char *vorbis_error = NULL; - const off_t file_size = vcedit_write_metadata (deadbeef->fopen(fname), fname, -1, tags.vendor, tags.comments, tags.user_comments, &vorbis_error); - vorbis_comment_clear(&tags); + deadbeef->pl_lock(); + const char *stream_size_string = deadbeef->pl_find_meta(it, ":STREAM SIZE"); + const size_t stream_size = stream_size_string ? (off_t)atoll(stream_size_string) : 0; + deadbeef->pl_unlock(); + const off_t file_size = oggedit_write_vorbis_metadata (deadbeef->fopen(fname), fname, 0, stream_size, tags->comments, tags->user_comments); + vorbis_comment_clear(tags); + free(tags); if (file_size <= 0) { - trace ("cvorbis_write_metadata: failed to write tags to %s, error: %s\n", fname, vorbis_error); + trace ("cvorbis_write_metadata: failed to write tags to %s, code %d\n", fname, file_size); return -1; } - deadbeef->pl_set_meta_int(it, ":FILE_SIZE", file_size); + set_meta_ll(it, ":FILE_SIZE", (int64_t)file_size); return cvorbis_read_metadata(it); - - return 0; } |