diff options
author | Alexey Yakovenko <wakeroid@gmail.com> | 2010-04-05 15:21:51 +0200 |
---|---|---|
committer | Alexey Yakovenko <wakeroid@gmail.com> | 2010-04-05 15:24:16 +0200 |
commit | 3f2e3471d49abd1db57709a119a341c2a8a64204 (patch) | |
tree | e779e7967aaae0af3661733ec72949a05f1b3839 /plugins/vorbis | |
parent | 1dfd6d2399b20a72aae401fa8b4893ee1558dd03 (diff) |
added ogg vorbis tag editing
Diffstat (limited to 'plugins/vorbis')
-rw-r--r-- | plugins/vorbis/Makefile.am | 2 | ||||
-rw-r--r-- | plugins/vorbis/i18n.h | 18 | ||||
-rw-r--r-- | plugins/vorbis/vcedit.c | 880 | ||||
-rw-r--r-- | plugins/vorbis/vcedit.h | 69 | ||||
-rw-r--r-- | plugins/vorbis/vceditaux.h | 9 | ||||
-rw-r--r-- | plugins/vorbis/vorbis.c | 221 |
6 files changed, 1176 insertions, 23 deletions
diff --git a/plugins/vorbis/Makefile.am b/plugins/vorbis/Makefile.am index 0526b8f7..a316d3b3 100644 --- a/plugins/vorbis/Makefile.am +++ b/plugins/vorbis/Makefile.am @@ -2,7 +2,7 @@ if HAVE_VORBISFILE if HAVE_VORBIS vorbisdir = $(libdir)/$(PACKAGE) pkglib_LTLIBRARIES = vorbis.la -vorbis_la_SOURCES = vorbis.c +vorbis_la_SOURCES = vorbis.c vcedit.c vcedit.h vceditaux.h i18n.h vorbis_la_LDFLAGS = -module vorbis_la_LIBADD = $(LDADD) $(VORBIS_LIBS) -lc diff --git a/plugins/vorbis/i18n.h b/plugins/vorbis/i18n.h new file mode 100644 index 00000000..86248307 --- /dev/null +++ b/plugins/vorbis/i18n.h @@ -0,0 +1,18 @@ +#ifndef VORBIS_TOOLS_I18N_H +#define VORBIS_TOOLS_I18N_H + +#ifdef ENABLE_NLS +#include <libintl.h> +#define _(X) gettext(X) +#else +#define _(X) (X) +#define textdomain(X) +#define bindtextdomain(X, Y) +#endif +#ifdef gettext_noop +#define N_(X) gettext_noop(X) +#else +#define N_(X) (X) +#endif + +#endif diff --git a/plugins/vorbis/vcedit.c b/plugins/vorbis/vcedit.c new file mode 100644 index 00000000..2300b3da --- /dev/null +++ b/plugins/vorbis/vcedit.c @@ -0,0 +1,880 @@ +/* This program is licensed under the GNU Library General Public License, version 2, + * a copy of which is included with this program (LICENCE.LGPL). + * + * (c) 2000-2001 Michael Smith <msmith@xiph.org> + * + * + * Comment editing backend, suitable for use by nice frontend interfaces. + * + * last modified: $Id: vcedit.c 16826 2010-01-27 04:16:24Z xiphmont $ + */ + +/* Handle muxed streams and the Vorbis renormalization without having + * to understand remuxing: + * Linked list of buffers (buffer_chain). Start a link and whenever + * you encounter an unknown page from the current stream (ie we found + * its bos in the bos section) push it onto the current buffer. Whenever + * you encounter the stream being renormalized create a new link in the + * chain. + * On writing, write the contents of the first link before every Vorbis + * page written, and move to the next link. Assuming the Vorbis pages + * in match vorbis pages out, the order of pages from different logical + * streams will be unchanged. + * Special case: header. After writing the vorbis headers, and before + * starting renormalization, flush accumulated links (takes care of + * situations where number of secondary vorbis header pages changes due + * to remuxing. Similarly flush links at the end of renormalization + * and before the start of the next chain is written. + * + */ + +#ifdef HAVE_CONFIG_H +#include <config.h> +#endif + +#include <stdio.h> +#include <stdlib.h> +#include <string.h> +#include <errno.h> +#include <ogg/ogg.h> +#include <vorbis/codec.h> + +#include "vcedit.h" +#include "vceditaux.h" +#include "i18n.h" + +#define CHUNKSIZE 4096 +#define BUFFERCHUNK CHUNKSIZE + +/* Helper function, shouldn't need to call directly */ +static int page_buffer_push(vcedit_buffer_chain *bufferlink, ogg_page *og) { + int result=0; + char *tmp; + vcedit_page_buffer *buffer; + + buffer = &bufferlink->buffer; + tmp = realloc(buffer->data, + buffer->data_len + og->header_len + og->body_len); + if(tmp) { + buffer->data = tmp; + memcpy(buffer->data + buffer->data_len, og->header, + og->header_len); + buffer->data_len += og->header_len; + memcpy(buffer->data + buffer->data_len, og->body, + og->body_len); + result = 1; + buffer->data_len += og->body_len; + } else { + result = -1; + } + + return result; +} + +/* Write and free the first link using callbacks */ +static int buffer_chain_writelink(vcedit_state *state, void *out) { + int result = 0; + vcedit_buffer_chain *tmpchain; + vcedit_page_buffer *tmpbuffer; + + tmpchain = state->sidebuf; + tmpbuffer = &tmpchain->buffer; + if(tmpbuffer->data_len) + { + if(state->write(tmpbuffer->data,1,tmpbuffer->data_len, out) != + (size_t) tmpbuffer->data_len) + result = -1; + else + result = 1; + } + + free(tmpbuffer->data); + state->sidebuf = tmpchain->next; + free(tmpchain); + return result; +} + + +static int buffer_chain_newlink(vcedit_state *state) { + int result = 1; + vcedit_buffer_chain *bufferlink; + + if(!state->sidebuf) { + state->sidebuf = malloc (sizeof *state->sidebuf); + if(state->sidebuf) { + bufferlink = state->sidebuf; + } else { + result = -1; + } + } else { + bufferlink=state->sidebuf; + while(bufferlink->next) { + bufferlink = bufferlink->next; + } + bufferlink->next = malloc (sizeof *bufferlink->next); + if(bufferlink->next) { + bufferlink = bufferlink->next; + } else { + result = -1; + } + } + + if(result > 0 ) { + bufferlink->next = 0; + bufferlink->buffer.data = 0; + bufferlink->buffer.data_len = 0; + } + else + state->lasterror = + _("Couldn't get enough memory for input buffering."); + + return result; +} + + +/* Push page onto the end of the buffer chain */ +static int buffer_chain_push(vcedit_state *state, ogg_page *og) { + /* If there is no sidebuffer yet we need to create one, otherwise + * traverse to the last buffer and push the new page onto it. */ + int result=1; + vcedit_buffer_chain *bufferlink; + if(!state->sidebuf) { + result = buffer_chain_newlink(state); + } + + if(result > 0) { + bufferlink = state->sidebuf; + while(bufferlink->next) { + bufferlink = bufferlink->next; + } + result = page_buffer_push(bufferlink, og); + } + + if(result < 0) + state->lasterror = + _("Couldn't get enough memory for input buffering."); + + return result; +} + + + +static int vcedit_supported_stream(vcedit_state *state, ogg_page *og) { + ogg_stream_state os; + vorbis_info vi; + vorbis_comment vc; + ogg_packet header; + int result = 0; + + ogg_stream_init(&os, ogg_page_serialno(og)); + vorbis_info_init(&vi); + vorbis_comment_init(&vc); + + if( !ogg_page_bos(og) ) + result = -1; + + if(result >= 0 && ogg_stream_pagein(&os, og) < 0) + { + state->lasterror = + _("Error reading first page of Ogg bitstream."); + result = -1; + } + + if(result >= 0 && ogg_stream_packetout(&os, &header) != 1) + { + state->lasterror = _("Error reading initial header packet."); + result = -1; + } + + if(result >= 0 && vorbis_synthesis_headerin(&vi, &vc, &header) >= 0) + { + result = 1; + } else { + /* Not vorbis, may eventually become a chain of checks (Speex, + * Theora), but for the moment return 0, bos scan will push + * the current page onto the buffer. + */ + } + + ogg_stream_clear(&os); + vorbis_info_clear(&vi); + vorbis_comment_clear(&vc); + return result; +} + + +static int vcedit_contains_serial (vcedit_state *state, int serialno) { + int result = 0; + size_t count; + for( count=0; count < state->serials.streams_len; count++ ) { + if ( *(state->serials.streams + count ) == serialno ) + result = 1; + } + + return result; +} + + +static int vcedit_add_serial (vcedit_state *state, long serial) { + int result = 0; + long *tmp; + + + if( vcedit_contains_serial(state, serial) ) + { + result = 1; + } else { + tmp = realloc(state->serials.streams, + (state->serials.streams_len + 1) * sizeof *tmp); + if(tmp) { + state->serials.streams = tmp; + *(state->serials.streams + + state->serials.streams_len) = serial; + state->serials.streams_len += 1; + result = 1; + } else { + state->lasterror = + _("Couldn't get enough memory to register new stream serial number."); + result = -1; + } + } + return result; +} + + +/* For the benefit of the secondary header read only. Quietly creates + * newlinks and pushes pages onto the buffer in the right way */ +static int vcedit_target_pageout (vcedit_state *state, ogg_page *og) { + int result = 0; + int pageout_result; + pageout_result = ogg_sync_pageout(state->oy, og); + if(pageout_result > 0) + { + if(state->serial == ogg_page_serialno(og)) + result = buffer_chain_newlink(state); + else + result = buffer_chain_push(state, og); + } else if (pageout_result < 0) { + /* Vorbis comment traditionally ignores the not-synced + * error from pageout, so give it a different code. */ + result = -2; + } + return result; +} + + +/* (I'm paranoid about memset(x,0,len) not giving null pointers */ +vcedit_state *vcedit_new_state(void) { + vcedit_state *state = malloc(sizeof(vcedit_state)); + if(state) { + memset(state, 0, sizeof(vcedit_state)); + state->sidebuf = 0; + state->serials.streams = 0; + state->serials.streams_len = 0; + } + return state; +} + +char *vcedit_error(vcedit_state *state) { + return state->lasterror; +} + +vorbis_comment *vcedit_comments(vcedit_state *state) { + return state->vc; +} + +static void vcedit_clear_internals(vcedit_state *state) { + char *tmp; + if(state->vc) { + vorbis_comment_clear(state->vc); + free(state->vc); + } + if(state->os) { + ogg_stream_clear(state->os); + free(state->os); + } + if(state->oy) { + ogg_sync_clear(state->oy); + free(state->oy); + } + if(state->serials.streams_len) { + free(state->serials.streams); + state->serials.streams_len = 0; + state->serials.streams = 0; + } + while(state->sidebuf) { + vcedit_buffer_chain *tmpbuffer; + tmpbuffer = state->sidebuf; + state->sidebuf = tmpbuffer->next; + free(tmpbuffer->buffer.data); + free(tmpbuffer); + } + if(state->vendor) + free(state->vendor); + if(state->mainbuf) + free(state->mainbuf); + if(state->bookbuf) + free(state->bookbuf); + if(state->vi) { + vorbis_info_clear(state->vi); + free(state->vi); + } + + tmp = state->lasterror; + memset(state, 0, sizeof(*state)); + state->lasterror = tmp; +} + +void vcedit_clear(vcedit_state *state) +{ + if(state) + { + vcedit_clear_internals(state); + free(state); + } +} + +/* Next two functions pulled straight from libvorbis, apart from one change + * - we don't want to overwrite the vendor string. + */ +static void _v_writestring(oggpack_buffer *o,char *s, int len) +{ + while(len--) + { + oggpack_write(o,*s++,8); + } +} + +static int _commentheader_out(vorbis_comment *vc, char *vendor, ogg_packet *op) +{ + oggpack_buffer opb; + + oggpack_writeinit(&opb); + + /* preamble */ + oggpack_write(&opb,0x03,8); + _v_writestring(&opb,"vorbis", 6); + + /* vendor */ + oggpack_write(&opb,strlen(vendor),32); + _v_writestring(&opb,vendor, strlen(vendor)); + + /* comments */ + oggpack_write(&opb,vc->comments,32); + if(vc->comments){ + int i; + for(i=0;i<vc->comments;i++){ + if(vc->user_comments[i]){ + oggpack_write(&opb,vc->comment_lengths[i],32); + _v_writestring(&opb,vc->user_comments[i], + vc->comment_lengths[i]); + }else{ + oggpack_write(&opb,0,32); + } + } + } + oggpack_write(&opb,1,1); + + op->packet = malloc(oggpack_bytes(&opb)); + memcpy(op->packet, opb.buffer, oggpack_bytes(&opb)); + + op->bytes=oggpack_bytes(&opb); + op->b_o_s=0; + op->e_o_s=0; + op->granulepos=0; + + oggpack_writeclear(&opb); + return 0; +} + +static int _blocksize(vcedit_state *s, ogg_packet *p) +{ + int this = vorbis_packet_blocksize(s->vi, p); + int ret = (this + s->prevW)/4; + + if(!s->prevW) + { + s->prevW = this; + return 0; + } + + s->prevW = this; + return ret; +} + +static int _fetch_next_packet(vcedit_state *s, ogg_packet *p, ogg_page *page) +{ + int result; + char *buffer; + int bytes; + int serialno; + + result = ogg_stream_packetout(s->os, p); + + if(result > 0) + return 1; + else { + while(1) { + if(s->eosin) + return 0; + + while(ogg_sync_pageout(s->oy, page) <= 0) + { + buffer = ogg_sync_buffer(s->oy, CHUNKSIZE); + bytes = s->read(buffer,1, CHUNKSIZE, s->in); + ogg_sync_wrote(s->oy, bytes); + if(bytes == 0) + return 0; + } + + serialno = ogg_page_serialno(page); + if(ogg_page_serialno(page) != s->serial) + { + if(vcedit_contains_serial(s, serialno)) { + result = buffer_chain_push(s, page); + if(result < 0) + return result; + } + else + { + s->eosin = 1; + s->extrapage = 1; + return 0; + } + } + else + { + ogg_stream_pagein(s->os, page); + result = buffer_chain_newlink(s); + if (result < 0) + return result; + + if(ogg_page_eos(page)) + s->eosin = 1; + } + result = ogg_stream_packetout(s->os, p); + if(result > 0) + return 1; + } + /* Here == trouble */ + return 0; + } +} + +int vcedit_open(vcedit_state *state, FILE *in) +{ + return vcedit_open_callbacks(state, (void *)in, + (vcedit_read_func)fread, (vcedit_write_func)fwrite); +} + +int vcedit_open_callbacks(vcedit_state *state, void *in, + vcedit_read_func read_func, vcedit_write_func write_func) +{ + + char *buffer; + int bytes,i; + int chunks = 0; + int read_bos, test_supported, page_pending; + int have_vorbis; + ogg_packet *header; + ogg_packet header_main; + ogg_packet header_comments; + ogg_packet header_codebooks; + ogg_page og; + + state->in = in; + state->read = read_func; + state->write = write_func; + + state->oy = malloc(sizeof(ogg_sync_state)); + ogg_sync_init(state->oy); + + while(1) + { + buffer = ogg_sync_buffer(state->oy, CHUNKSIZE); + bytes = state->read(buffer, 1, CHUNKSIZE, state->in); + + ogg_sync_wrote(state->oy, bytes); + + if(ogg_sync_pageout(state->oy, &og) == 1) + break; + + if(chunks++ >= 10) /* Bail if we don't find data in the first 40 kB */ + { + if(bytes<CHUNKSIZE) + state->lasterror = _("Input truncated or empty."); + else + state->lasterror = _("Input is not an Ogg bitstream."); + goto err; + } + } + + /* BOS loop, starting with a loaded ogg page. */ + if(buffer_chain_newlink(state) < 0) + goto err; + + for( read_bos = 1, have_vorbis = 0 ; read_bos; ) + { + test_supported = vcedit_supported_stream(state, &og); + if(test_supported < 0) + { + goto err; + } + else if (test_supported == 0 || have_vorbis ) + { + if(vcedit_add_serial ( state, ogg_page_serialno(&og)) < 0) + goto err; + if( buffer_chain_push(state, &og) < 0) + goto err; + } + else if (test_supported > 0) + { + if(buffer_chain_newlink(state) < 0) + goto err; + state->serial = ogg_page_serialno(&og); + if(vcedit_add_serial ( state, ogg_page_serialno(&og)) < 0) + goto err; + + state->os = malloc(sizeof(ogg_stream_state)); + ogg_stream_init(state->os, state->serial); + + state->vi = malloc(sizeof(vorbis_info)); + vorbis_info_init(state->vi); + + state->vc = malloc(sizeof(vorbis_comment)); + vorbis_comment_init(state->vc); + + if(ogg_stream_pagein(state->os, &og) < 0) + { + state->lasterror = + _("Error reading first page of Ogg bitstream."); + goto err; + } + + if(ogg_stream_packetout(state->os, &header_main) != 1) + { + state->lasterror = + _("Error reading initial header packet."); + goto err; + } + + if(vorbis_synthesis_headerin(state->vi, state->vc, + &header_main) < 0) + { + state->lasterror = + _("Ogg bitstream does not contain Vorbis data."); + goto err; + } + have_vorbis = 1; + } + while(1) + { + buffer = ogg_sync_buffer(state->oy, CHUNKSIZE); + bytes = state->read(buffer, 1, CHUNKSIZE, state->in); + + if(bytes == 0) + { + state->lasterror = + _("EOF before recognised stream."); + goto err; + } + + ogg_sync_wrote(state->oy, bytes); + + if(ogg_sync_pageout(state->oy, &og) == 1) + break; + } + if(!ogg_page_bos(&og)) { + read_bos = 0; + page_pending = 1; + } + } + + if(!state->os) { + state->lasterror = _("Ogg bitstream does not contain a supported data-type."); + goto err; + } + + state->mainlen = header_main.bytes; + state->mainbuf = malloc(state->mainlen); + memcpy(state->mainbuf, header_main.packet, header_main.bytes); + + if(ogg_page_serialno(&og) == state->serial) + { + if(buffer_chain_newlink(state) < 0) + goto err; + } + + else + { + if(buffer_chain_push(state, &og) < 0) + goto err; + page_pending = 0; + } + + i = 0; + header = &header_comments; + while(i<2) { + while(i<2) { + int result; + if(!page_pending) + result = vcedit_target_pageout(state, &og); + else + { + result = 1; + page_pending = 0; + } + if(result == 0 || result == -2) break; /* Too little data so far */ + else if(result == -1) goto err; + else if(result == 1) + { + ogg_stream_pagein(state->os, &og); + while(i<2) + { + result = ogg_stream_packetout(state->os, header); + if(result == 0) break; + if(result == -1) + { + state->lasterror = _("Corrupt secondary header."); + goto err; + } + vorbis_synthesis_headerin(state->vi, state->vc, header); + if(i==1) + { + state->booklen = header->bytes; + state->bookbuf = malloc(state->booklen); + memcpy(state->bookbuf, header->packet, + header->bytes); + } + i++; + header = &header_codebooks; + } + } + } + + buffer = ogg_sync_buffer(state->oy, CHUNKSIZE); + bytes = state->read(buffer, 1, CHUNKSIZE, state->in); + if(bytes == 0 && i < 2) + { + state->lasterror = _("EOF before end of Vorbis headers."); + goto err; + } + ogg_sync_wrote(state->oy, bytes); + } + + /* Copy the vendor tag */ + state->vendor = malloc(strlen(state->vc->vendor) +1); + strcpy(state->vendor, state->vc->vendor); + + /* Headers are done! */ + return 0; + +err: + vcedit_clear_internals(state); + return -1; +} + +int vcedit_write(vcedit_state *state, void *out) +{ + ogg_stream_state streamout; + ogg_packet header_main; + ogg_packet header_comments; + ogg_packet header_codebooks; + + ogg_page ogout, ogin; + ogg_packet op; + ogg_int64_t granpos = 0; + int result; + char *buffer; + int bytes; + int needflush=0, needout=0; + + state->eosin = 0; + state->extrapage = 0; + + header_main.bytes = state->mainlen; + header_main.packet = state->mainbuf; + header_main.b_o_s = 1; + header_main.e_o_s = 0; + header_main.granulepos = 0; + + header_codebooks.bytes = state->booklen; + header_codebooks.packet = state->bookbuf; + header_codebooks.b_o_s = 0; + header_codebooks.e_o_s = 0; + header_codebooks.granulepos = 0; + + ogg_stream_init(&streamout, state->serial); + + _commentheader_out(state->vc, state->vendor, &header_comments); + + ogg_stream_packetin(&streamout, &header_main); + ogg_stream_packetin(&streamout, &header_comments); + ogg_stream_packetin(&streamout, &header_codebooks); + + while((result = ogg_stream_flush(&streamout, &ogout))) + { + if(state->sidebuf && buffer_chain_writelink(state, out) < 0) + goto cleanup; + if(state->write(ogout.header,1,ogout.header_len, out) != + (size_t) ogout.header_len) + goto cleanup; + if(state->write(ogout.body,1,ogout.body_len, out) != + (size_t) ogout.body_len) + goto cleanup; + } + + while(state->sidebuf) { + if(buffer_chain_writelink(state, out) < 0) + goto cleanup; + } + if(buffer_chain_newlink(state) < 0) + goto cleanup; + + while(_fetch_next_packet(state, &op, &ogin)) + { + int size; + size = _blocksize(state, &op); + granpos += size; + + if(needflush) + { + if(ogg_stream_flush(&streamout, &ogout)) + { + if(state->sidebuf && + buffer_chain_writelink(state, out) < 0) + goto cleanup; + if(state->write(ogout.header,1,ogout.header_len, + out) != (size_t) ogout.header_len) + goto cleanup; + if(state->write(ogout.body,1,ogout.body_len, + out) != (size_t) ogout.body_len) + goto cleanup; + } + } + else if(needout) + { + if(ogg_stream_pageout(&streamout, &ogout)) + { + if(state->sidebuf && + buffer_chain_writelink(state, out) < 0) + goto cleanup; + if(state->write(ogout.header,1,ogout.header_len, + out) != (size_t) ogout.header_len) + goto cleanup; + if(state->write(ogout.body,1,ogout.body_len, + out) != (size_t) ogout.body_len) + goto cleanup; + } + } + + needflush=needout=0; + + if(op.granulepos == -1) + { + op.granulepos = granpos; + ogg_stream_packetin(&streamout, &op); + } + else /* granulepos is set, validly. Use it, and force a flush to + account for shortened blocks (vcut) when appropriate */ + { + if(granpos > op.granulepos) + { + granpos = op.granulepos; + ogg_stream_packetin(&streamout, &op); + needflush=1; + } + else + { + ogg_stream_packetin(&streamout, &op); + needout=1; + } + } + } + + streamout.e_o_s = 1; + while(ogg_stream_flush(&streamout, &ogout)) + { + if(state->sidebuf && buffer_chain_writelink(state, out) < 0) + goto cleanup; + if(state->write(ogout.header,1,ogout.header_len, + out) != (size_t) ogout.header_len) + goto cleanup; + if(state->write(ogout.body,1,ogout.body_len, + out) != (size_t) ogout.body_len) + goto cleanup; + } + + if (state->extrapage) + { + /* This is the first page of a new chain, get rid of the + * sidebuffer */ + while(state->sidebuf) + if(buffer_chain_writelink(state, out) < 0) + goto cleanup; + if(state->write(ogin.header,1,ogin.header_len, + out) != (size_t) ogin.header_len) + goto cleanup; + if (state->write(ogin.body,1,ogin.body_len, out) != + (size_t) ogin.body_len) + goto cleanup; + } + + state->eosin=0; /* clear it, because not all paths to here do */ + while(!state->eosin) /* We reached eos, not eof */ + { + /* We copy the rest of the stream (other logical streams) + * through, a page at a time. */ + while(1) + { + result = ogg_sync_pageout(state->oy, &ogout); + if(result==0) + break; + if(result<0) + state->lasterror = _("Corrupt or missing data, continuing..."); + else + { + /* Don't bother going through the rest, we can just + * write the page out now */ + if(state->write(ogout.header,1,ogout.header_len, + out) != (size_t) ogout.header_len) { + goto cleanup; + } + if(state->write(ogout.body,1,ogout.body_len, out) != + (size_t) ogout.body_len) { + goto cleanup; + } + } + } + buffer = ogg_sync_buffer(state->oy, CHUNKSIZE); + bytes = state->read(buffer,1, CHUNKSIZE, state->in); + ogg_sync_wrote(state->oy, bytes); + if(bytes == 0) + { + state->eosin = 1; + break; + } + } + + +cleanup: + ogg_stream_clear(&streamout); + + /* We don't ogg_packet_clear() this, because the memory was allocated in + _commentheader_out(), so we mirror that here */ + _ogg_free(header_comments.packet); + + free(state->mainbuf); + free(state->bookbuf); + state->mainbuf = state->bookbuf = NULL; + + if(!state->eosin) + { + state->lasterror = + _("Error writing stream to output. " + "Output stream may be corrupted or truncated."); + return -1; + } + + return 0; +} diff --git a/plugins/vorbis/vcedit.h b/plugins/vorbis/vcedit.h new file mode 100644 index 00000000..173876f3 --- /dev/null +++ b/plugins/vorbis/vcedit.h @@ -0,0 +1,69 @@ +/* This program is licensed under the GNU Library General Public License, version 2, + * a copy of which is included with this program (with filename LICENSE.LGPL). + * + * (c) 2000-2001 Michael Smith <msmith@xiph.org> + * + * VCEdit header. + * + * last modified: $ID:$ + */ + +#ifndef __VCEDIT_H +#define __VCEDIT_H + +#ifdef __cplusplus +extern "C" { +#endif + +#include <stdio.h> +#include <ogg/ogg.h> +#include <vorbis/codec.h> + +typedef size_t (*vcedit_read_func)(void *, size_t, size_t, void *); +typedef size_t (*vcedit_write_func)(const void *, size_t, size_t, void *); + +typedef struct { + long *streams; + size_t streams_len; +} vcedit_serial_nos; + +typedef struct { + ogg_sync_state *oy; + ogg_stream_state *os; + + vorbis_comment *vc; + vorbis_info *vi; + + vcedit_read_func read; + vcedit_write_func write; + + void *in; + int serial; + vcedit_serial_nos serials; + unsigned char *mainbuf; + unsigned char *bookbuf; + int mainlen; + int booklen; + char *lasterror; + char *vendor; + int prevW; + int extrapage; + int eosin; + struct vcedit_buffer_chain *sidebuf; +} vcedit_state; + +extern vcedit_state * vcedit_new_state(void); +extern void vcedit_clear(vcedit_state *state); +extern vorbis_comment * vcedit_comments(vcedit_state *state); +extern int vcedit_open(vcedit_state *state, FILE *in); +extern int vcedit_open_callbacks(vcedit_state *state, void *in, + vcedit_read_func read_func, vcedit_write_func write_func); +extern int vcedit_write(vcedit_state *state, void *out); +extern char * vcedit_error(vcedit_state *state); + +#ifdef __cplusplus +} +#endif + +#endif /* __VCEDIT_H */ + diff --git a/plugins/vorbis/vceditaux.h b/plugins/vorbis/vceditaux.h new file mode 100644 index 00000000..bb40eaeb --- /dev/null +++ b/plugins/vorbis/vceditaux.h @@ -0,0 +1,9 @@ +typedef struct vcedit_page_buffer { + char *data; + size_t data_len; +} vcedit_page_buffer; + +typedef struct vcedit_buffer_chain { + struct vcedit_buffer_chain *next; + struct vcedit_page_buffer buffer; +} vcedit_buffer_chain; diff --git a/plugins/vorbis/vorbis.c b/plugins/vorbis/vorbis.c index 4532eef9..45533651 100644 --- a/plugins/vorbis/vorbis.c +++ b/plugins/vorbis/vorbis.c @@ -20,16 +20,19 @@ #include <string.h> #include <stdlib.h> #include <assert.h> +#include <limits.h> +#include <unistd.h> #ifdef HAVE_CONFIG_H # include <config.h> #endif #include "../../deadbeef.h" +#include "vcedit.h" #define min(x,y) ((x)<(y)?(x):(y)) #define max(x,y) ((x)>(y)?(x):(y)) -//#define trace(...) { fprintf (stderr, __VA_ARGS__); } -#define trace(fmt,...) +#define trace(...) { fprintf (stderr, __VA_ARGS__); } +//#define trace(fmt,...) static DB_decoder_t plugin; static DB_functions_t *deadbeef; @@ -50,7 +53,7 @@ typedef struct { static size_t cvorbis_fread (void *ptr, size_t size, size_t nmemb, void *datasource) { size_t ret = deadbeef->fread (ptr, size, nmemb, datasource); - trace ("cvorbis_fread %d %d %d\n", size, nmemb, ret); +// trace ("cvorbis_fread %d %d %d\n", size, nmemb, ret); return ret; } @@ -77,19 +80,22 @@ update_vorbis_comments (DB_playItem_t *it, vorbis_comment *vc) { deadbeef->pl_delete_all_meta (it); deadbeef->pl_add_meta (it, "vendor", vc->vendor); for (int i = 0; i < vc->comments; i++) { - if (!strncasecmp (vc->user_comments[i], "artist=", 7)) { + if (!strncasecmp (vc->user_comments[i], "ARTIST=", 7)) { deadbeef->pl_add_meta (it, "artist", vc->user_comments[i] + 7); } - else if (!strncasecmp (vc->user_comments[i], "album=", 6)) { + else if (!strncasecmp (vc->user_comments[i], "ALBUM=", 6)) { deadbeef->pl_add_meta (it, "album", vc->user_comments[i] + 6); } - else if (!strncasecmp (vc->user_comments[i], "title=", 6)) { + else if (!strncasecmp (vc->user_comments[i], "TITLE=", 6)) { deadbeef->pl_add_meta (it, "title", vc->user_comments[i] + 6); } - else if (!strncasecmp (vc->user_comments[i], "tracknumber=", 12)) { + else if (!strncasecmp (vc->user_comments[i], "TRACKNUMBER=", 12)) { deadbeef->pl_add_meta (it, "track", vc->user_comments[i] + 12); } - else if (!strncasecmp (vc->user_comments[i], "date=", 5)) { + else if (!strncasecmp (vc->user_comments[i], "TRACKTOTAL=", 11)) { + deadbeef->pl_add_meta (it, "numtracks", vc->user_comments[i] + 11); + } + else if (!strncasecmp (vc->user_comments[i], "DATE=", 5)) { deadbeef->pl_add_meta (it, "year", vc->user_comments[i] + 5); } else if (!strncasecmp (vc->user_comments[i], "COMMENT=", 8)) { @@ -254,7 +260,7 @@ cvorbis_read (DB_fileinfo_t *_info, char *bytes, int size) { if (!info->file->vfs->streaming) { if (info->currentsample + size / (2 * _info->channels) > info->endsample) { size = (info->endsample - info->currentsample + 1) * 2 * _info->channels; - trace ("size truncated to %d bytes, cursample=%d, info->endsample=%d, totalsamples=%d\n", size, info->currentsample, info->endsample, ov_pcm_total (&vorbis_file, -1)); + trace ("size truncated to %d bytes, cursample=%d, info->endsample=%d, totalsamples=%d\n", size, info->currentsample, info->endsample, ov_pcm_total (&info->vorbis_file, -1)); if (size <= 0) { return 0; } @@ -320,7 +326,7 @@ cvorbis_read (DB_fileinfo_t *_info, char *bytes, int size) { } } _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); + //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_instant (&info->vorbis_file)/1000); return initsize - size; } @@ -431,28 +437,197 @@ cvorbis_insert (DB_playItem_t *after, const char *fname) { return after; } -// that won't be needed with refcounting - -//static int -//vorbis_trackdeleted (DB_event_track_t *ev, uintptr_t data) { -// if (ev->track == info->ptrack) { -// info->ptrack = NULL; -// } -// return 0; -//} - static int vorbis_start (void) { -// deadbeef->ev_subscribe (DB_PLUGIN (&plugin), DB_EV_TRACKDELETED, DB_CALLBACK (vorbis_trackdeleted), 0); return 0; } static int vorbis_stop (void) { -// deadbeef->ev_unsubscribe (DB_PLUGIN (&plugin), DB_EV_TRACKDELETED, DB_CALLBACK (vorbis_trackdeleted), 0); return 0; } +static const char *metainfo[] = { + "ARTIST", "artist", + "TITLE", "title", + "ALBUM", "album", + "TRACKNUMBER", "track", + "DATE", "year", + "GENRE", "genre", + "COMMENT", "comment", + "PERFORMER", "performer", + "ENSEMBLE", "band", + "COMPOSER", "composer", + "ENCODED-BY", "vendor", + "DISCNUMBER", "disc", + "COPYRIGHT", "copyright", + "TRACKTOTAL", "numtracks", + NULL +}; + +int +cvorbis_read_metadata (DB_playItem_t *it) { + int err = -1; + DB_FILE *fp = NULL; + OggVorbis_File vorbis_file; + vorbis_info *vi = NULL; + + fp = deadbeef->fopen (it->fname); + if (!fp) { + trace ("cvorbis_read_metadata: failed to fopen %s\n", it->fname); + return -1; + } + if (fp->vfs->streaming) { + trace ("cvorbis_read_metadata: failed to fopen %s\n", it->fname); + goto error; + } + ov_callbacks ovcb = { + .read_func = cvorbis_fread, + .seek_func = cvorbis_fseek, + .close_func = cvorbis_fclose, + .tell_func = cvorbis_ftell + }; + 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; + } + vi = ov_info (&vorbis_file, -1); + if (!vi) { // not a vorbis stream + trace ("cvorbis_read_metadata: failed to ov_open %s\n", it->fname); + goto error; + } + + // metainfo + vorbis_comment *vc = ov_comment (&vorbis_file, -1); + if (vc) { + update_vorbis_comments (it, vc); + } + + err = 0; +error: + if (fp) { + ov_clear (&vorbis_file); + } + return err; + +} + +int +cvorbis_write_metadata (DB_playItem_t *it) { + vcedit_state *state = NULL; + vorbis_comment *vc = NULL; + FILE *fp = NULL; + FILE *out = NULL; + int err = -1; + char outname[PATH_MAX] = ""; + + struct field { + struct field *next; + int size; + uint8_t data[0]; + }; + + struct field *preserved_fields = NULL; + + state = vcedit_new_state (); + if (!state) { + trace ("cvorbis_write_metadata: vcedit_new_state failed\n"); + return -1; + } + fp = fopen (it->fname, "rb"); + if (!fp) { + trace ("cvorbis_write_metadata: failed to read metadata from %s\n", it->fname); + goto error; + } + if (vcedit_open (state, fp) != 0) { + trace ("cvorbis_write_metadata: vcedit_open failed, error: %s\n", vcedit_error (state)); + goto error; + } + + vc = vcedit_comments (state); + if (!vc) { + trace ("cvorbis_write_metadata: vcedit_comments failed, error: %s\n", vcedit_error (state)); + goto error; + } + + // copy all unknown fields to separate buffer + for (int i = 0; i < vc->comments; i++) { + int m; + for (m = 0; metainfo[m]; m += 2) { + int l = strlen (metainfo[m]); + if (l+1 <= vc->comment_lengths[i] && !strncasecmp (vc->user_comments[i], metainfo[m], vc->comment_lengths[i]) && vc->user_comments[i][l] == '=') { + break; + } + } + if (!metainfo[m]) { + // unknown field + struct field *f = malloc (sizeof (struct field) + vc->comment_lengths[i]); + memset (f, 0, sizeof (struct field)); + memcpy (f->data, vc->user_comments[i], vc->comment_lengths[i]); + f->size = vc->comment_lengths[i]; + f->next = preserved_fields; + preserved_fields = f; + } + } + + vorbis_comment_clear(vc); + vorbis_comment_init(vc); + + // add known fields + for (int m = 0; metainfo[m]; m += 2) { + const char *val = deadbeef->pl_find_meta (it, metainfo[m+1]); + if (val && *val) { + char s[1024]; + snprintf (s, sizeof (s), "%s=%s", metainfo[m], val); + vorbis_comment_add (vc, s); + } + } + + // add preserved fields + for (struct field *f = preserved_fields; f; f = f->next) { + vorbis_comment_add (vc, f->data); + } + + snprintf (outname, sizeof (outname), "%s.temp.ogg", it->fname); + + out = fopen (outname, "w+b"); + if (!fp) { + trace ("cvorbis_write_metadata: failed to open %s for writing\n", it->fname); + goto error; + } + + if (vcedit_write (state, out) < 0) { + trace ("cvorbis_write_metadata: failed to write tags to %s, error: %s\n", it->fname, vcedit_error (state)); + goto error; + } + + err = 0; +error: + if (out) { + fclose (fp); + } + if (state) { + vcedit_clear (state); + } + + while (preserved_fields) { + struct field *next = preserved_fields->next; + free (preserved_fields); + preserved_fields = next; + } + + if (!err) { + rename (outname, it->fname); + } + else if (*outname) { + unlink (outname); + } + + return err; +} + + static const char * exts[] = { "ogg", "ogx", NULL }; static const char *filetypes[] = { "OggVorbis", NULL }; @@ -478,6 +653,8 @@ static DB_decoder_t plugin = { .seek = cvorbis_seek, .seek_sample = cvorbis_seek_sample, .insert = cvorbis_insert, + .read_metadata = cvorbis_read_metadata, + .write_metadata = cvorbis_write_metadata, .exts = exts, .filetypes = filetypes }; |