diff options
author | Ian Nartowicz <iann@crunchbang> | 2014-06-04 22:45:30 +0100 |
---|---|---|
committer | Ian Nartowicz <iann@crunchbang> | 2014-06-04 22:45:30 +0100 |
commit | 823a4064ebcd2662f8e480f99326ac75dd6404a7 (patch) | |
tree | 420f6df26d390479f5573acb4f922d62d458ae8c /plugins | |
parent | 923bdcd04d441869dc0fc9350c5bfb856f6e5677 (diff) |
Full release of oggedit
Diffstat (limited to 'plugins')
-rw-r--r-- | plugins/flac/Makefile.am | 2 | ||||
-rw-r--r-- | plugins/flac/flac.c | 119 | ||||
-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 | 857 |
16 files changed, 1953 insertions, 2044 deletions
diff --git a/plugins/flac/Makefile.am b/plugins/flac/Makefile.am index 02e594d3..c87ca53d 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 flac_la_LIBADD = $(LDADD) $(FLAC_LIBS) ../liboggedit/liboggedit.a AM_CFLAGS = $(CFLAGS) $(FLAC_CFLAGS) -std=c99 diff --git a/plugins/flac/flac.c b/plugins/flac/flac.c index 9a823b9e..e48f30a4 100644 --- a/plugins/flac/flac.c +++ b/plugins/flac/flac.c @@ -1,49 +1,52 @@ /* - DeaDBeeF - ultimate music player for GNU/Linux systems with X11 - Copyright (C) 2009-2013 Alexey Yakovenko <waker@users.sourceforge.net> - - Redistribution and use in source and binary forms, with or without - modification, are permitted provided that the following conditions - are met: - - - Redistributions of source code must retain the above copyright - notice, this list of conditions and the following disclaimer. - - - Redistributions in binary form must reproduce the above copyright - notice, this list of conditions and the following disclaimer in the - documentation and/or other materials provided with the distribution. - - - Neither the name of the DeaDBeeF Player nor the names of its - contributors may be used to endorse or promote products derived from - this software without specific prior written permission. - - THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS - ``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT - LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR - A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE FOUNDATION OR - CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, - EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, - PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR - PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF - LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING - NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS - SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + DeaDBeeF - ultimate music player for GNU/Linux systems with X11 + Copyright (C) 2009-2013 Alexey Yakovenko <waker@users.sourceforge.net> + + Redistribution and use in source and binary forms, with or without + modification, are permitted provided that the following conditions + are met: + + - Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + + - Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. + + - Neither the name of the DeaDBeeF Player nor the names of its + contributors may be used to endorse or promote products derived from + this software without specific prior written permission. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + ``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE FOUNDATION OR + CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, + EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, + PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR + PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF + LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING + 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; static DB_artwork_plugin_t *coverart_plugin = NULL; -//#define trace(...) { fprintf(stderr, __VA_ARGS__); } +// #define trace(...) { fprintf(stderr, __VA_ARGS__); } #define trace(fmt,...) #define min(x,y) ((x)<(y)?(x):(y)) @@ -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); } @@ -871,8 +874,11 @@ cflac_read_metadata (DB_playItem_t *it) { FLAC__bool res = FLAC__metadata_chain_read (chain, deadbeef->pl_find_meta (it, ":URI")); deadbeef->pl_unlock (); if (!res) { - trace ("cflac_read_metadata: FLAC__metadata_chain_read failed\n"); - goto error; + FLAC__bool res = FLAC__metadata_chain_read_ogg (chain, deadbeef->pl_find_meta (it, ":URI")); + if (!res) { + trace ("cflac_read_metadata: FLAC__metadata_chain_read failed\n"); + goto error; + } } FLAC__metadata_chain_merge_padding (chain); @@ -924,6 +930,27 @@ error: } 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; +} + +int cflac_write_metadata (DB_playItem_t *it) { int err = -1; FLAC__Metadata_Chain *chain = NULL; @@ -937,8 +964,13 @@ cflac_write_metadata (DB_playItem_t *it) { deadbeef->pl_lock (); FLAC__bool res = FLAC__metadata_chain_read (chain, deadbeef->pl_find_meta (it, ":URI")); deadbeef->pl_unlock (); + FLAC__bool isogg = false; + 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")); + } if (!res) { - trace ("cflac_write_metadata: FLAC__metadata_chain_read failed\n"); + trace ("cflac_write_metadata: FLAC__metadata_chain_read(_ogg) failed\n"); goto error; } FLAC__metadata_chain_merge_padding (chain); @@ -1128,10 +1160,15 @@ error2: } #endif - if (!FLAC__metadata_chain_write (chain, 1, 0)) { - trace ("cflac_write_metadata: FLAC__metadata_chain_write failed\n"); + if (isogg) + res = cflac_write_metadata_ogg(it, &data->data.vorbis_comment); + else + res = FLAC__metadata_chain_write (chain, 1, 0); + 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,9 +1191,9 @@ 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 libFLAC (C) Copyright (C) 2000,2001,2002,2003,2004,2005,2006,2007 Josh Coalson\n" "Uses libogg Copyright (c) 2002, Xiph.org Foundation\n" "\n" "Redistribution and use in source and binary forms, with or without\n" @@ -1177,7 +1214,7 @@ static DB_decoder_t plugin = { "THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS\n" "``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT\n" "LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR\n" - "A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE FOUNDATION OR\n" + "A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE FOUNDATION OR\n" "CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,\n" "EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,\n" "PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR\n" diff --git a/plugins/liboggedit/Makefile.am b/plugins/liboggedit/Makefile.am index cdf59124..2134718f 100644 --- a/plugins/liboggedit/Makefile.am +++ b/plugins/liboggedit/Makefile.am @@ -1,5 +1,6 @@ if HAVE_VORBIS 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 = $(VORBIS_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..00577268 100644 --- a/plugins/vorbis/vorbis.c +++ b/plugins/vorbis/vorbis.c @@ -3,7 +3,7 @@ Copyright (C) 2009-2014 Alexey Yakovenko and other contributors This software is provided 'as-is', without any express or implied - warranty. In no event will the authors be held liable for any damages + 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, @@ -11,12 +11,12 @@ 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. + 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. + misrepresented as being the original software. 3. This notice may not be removed or altered from any source distribution. */ @@ -24,24 +24,28 @@ #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)) -//#define trace(...) { fprintf (stderr, __VA_ARGS__); } +// #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; } @@ -833,61 +778,61 @@ static DB_decoder_t plugin = { .plugin.name = "OggVorbis decoder", .plugin.descr = "OggVorbis decoder using standard xiph.org libraries", .plugin.copyright = - "OggVorbis plugin for DeaDBeeF\n" - "Copyright (C) 2009-2014 Alexey Yakovenko et al.\n" - "\n" - "vcedit.c\n" - "Ogg Vorbis plugin Ogg edit functions\n" - "\n" - "Copyright (C) 2014 Ian Nartowicz <deadbeef@nartowicz.co.uk>\n" - "This software is provided 'as-is', without any express or implied\n" - "warranty. In no event will the authors be held liable for any damages\n" - "arising from the use of this software.\n" - "\n" - "Permission is granted to anyone to use this software for any purpose,\n" - "including commercial applications, and to alter it and redistribute it\n" - "freely, subject to the following restrictions:\n" - "\n" - "1. The origin of this software must not be misrepresented; you must not\n" - " claim that you wrote the original software. If you use this software\n" - " in a product, an acknowledgment in the product documentation would be\n" - " appreciated but is not required.\n" - "\n" - "2. Altered source versions must be plainly marked as such, and must not be\n" - " misrepresented as being the original software.\n" - "\n" - "3. This notice may not be removed or altered from any source distribution.\n" - "\n" - "\n" - "\n" - "Uses libogg,libvorbis Copyright (c) 2002, Xiph.org Foundation\n" - "\n" - "Redistribution and use in source and binary forms, with or without\n" - "modification, are permitted provided that the following conditions\n" - "are met:\n" - "\n" - "- Redistributions of source code must retain the above copyright\n" - "notice, this list of conditions and the following disclaimer.\n" - "\n" - "- Redistributions in binary form must reproduce the above copyright\n" - "notice, this list of conditions and the following disclaimer in the\n" - "documentation and/or other materials provided with the distribution.\n" - "\n" - "- Neither the name of the DeaDBeeF Player nor the names of its\n" - "contributors may be used to endorse or promote products derived from\n" - "this software without specific prior written permission.\n" - "\n" - "THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS\n" - "``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT\n" - "LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR\n" - "A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE FOUNDATION OR\n" - "CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,\n" - "EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,\n" - "PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR\n" - "PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF\n" - "LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING\n" - "NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS\n" - "SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.\n" + "OggVorbis plugin for DeaDBeeF\n" + "Copyright (C) 2009-2014 Alexey Yakovenko et al.\n" + "\n" + "vcedit.c\n" + "Ogg Vorbis plugin Ogg edit functions\n" + "\n" + "Copyright (C) 2014 Ian Nartowicz <deadbeef@nartowicz.co.uk>\n" + "This software is provided 'as-is', without any express or implied\n" + "warranty. In no event will the authors be held liable for any damages\n" + "arising from the use of this software.\n" + "\n" + "Permission is granted to anyone to use this software for any purpose,\n" + "including commercial applications, and to alter it and redistribute it\n" + "freely, subject to the following restrictions:\n" + "\n" + "1. The origin of this software must not be misrepresented; you must not\n" + " claim that you wrote the original software. If you use this software\n" + " in a product, an acknowledgment in the product documentation would be\n" + " appreciated but is not required.\n" + "\n" + "2. Altered source versions must be plainly marked as such, and must not be\n" + " misrepresented as being the original software.\n" + "\n" + "3. This notice may not be removed or altered from any source distribution.\n" + "\n" + "\n" + "\n" + "Uses libogg,libvorbis Copyright (c) 2002, Xiph.org Foundation\n" + "\n" + "Redistribution and use in source and binary forms, with or without\n" + "modification, are permitted provided that the following conditions\n" + "are met:\n" + "\n" + "- Redistributions of source code must retain the above copyright\n" + "notice, this list of conditions and the following disclaimer.\n" + "\n" + "- Redistributions in binary form must reproduce the above copyright\n" + "notice, this list of conditions and the following disclaimer in the\n" + "documentation and/or other materials provided with the distribution.\n" + "\n" + "- Neither the name of the DeaDBeeF Player nor the names of its\n" + "contributors may be used to endorse or promote products derived from\n" + "this software without specific prior written permission.\n" + "\n" + "THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS\n" + "``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT\n" + "LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR\n" + "A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE FOUNDATION OR\n" + "CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,\n" + "EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,\n" + "PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR\n" + "PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF\n" + "LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING\n" + "NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS\n" + "SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.\n" , .plugin.website = "http://deadbeef.sf.net", .plugin.start = vorbis_start, |