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