[xiph-cvs] cvs commit: vorbis-tools/vcut vcut.c vcut.h
Michael Smith
msmith at xiph.org
Mon Jul 16 06:09:58 PDT 2001
msmith 01/07/16 06:09:57
Modified: vcut vcut.c vcut.h
Log:
Major restructuring so that it's more usable, more reliable, and less likely
to just randomly collapse. This version is ready for more serious testing.
Revision Changes Path
1.3 +302 -180 vorbis-tools/vcut/vcut.c
Index: vcut.c
===================================================================
RCS file: /usr/local/cvsroot/vorbis-tools/vcut/vcut.c,v
retrieving revision 1.2
retrieving revision 1.3
diff -u -r1.2 -r1.3
--- vcut.c 2001/07/04 14:48:55 1.2
+++ vcut.c 2001/07/16 13:09:57 1.3
@@ -7,7 +7,7 @@
* Simple application to cut an ogg at a specified frame, and produce two
* output files.
*
- * last modified: $Id: vcut.c,v 1.2 2001/07/04 14:48:55 msmith Exp $
+ * last modified: $Id: vcut.c,v 1.3 2001/07/16 13:09:57 msmith Exp $
*/
#include <stdio.h>
@@ -20,131 +20,91 @@
#include "vcut.h"
-int main(int argc, char **argv)
+static vcut_packet *save_packet(ogg_packet *packet)
{
- ogg_int64_t cutpoint;
- FILE *in,*out1,*out2;
- int ret=0;
-
- if(argc<5)
- {
- fprintf(stderr,
- "Usage: vcut infile.ogg outfile1.ogg outfile2.ogg cutpoint\n");
- exit(1);
- }
-
- fprintf(stderr, "WARNING: vcut is still experimental code.\n"
- "Check that the output files are correct before deleting sources.\n\n");
+ vcut_packet *p = malloc(sizeof(vcut_packet));
- in = fopen(argv[1], "rb");
- if(!in) {
- fprintf(stderr, "Couldn't open %s for reading\n", argv[1]);
- exit(1);
- }
- out1 = fopen(argv[2], "wb");
- if(!out1) {
- fprintf(stderr, "Couldn't open %s for writing\n", argv[2]);
- exit(1);
- }
- out2 = fopen(argv[3], "wb");
- if(!out2) {
- fprintf(stderr, "Couldn't open %s for writing\n", argv[3]);
- exit(1);
- }
+ p->length = packet->bytes;
+ p->packet = malloc(p->length);
+ memcpy(p->packet, packet->packet, p->length);
- sscanf(argv[4], "%Ld", &cutpoint);
+ return p;
+}
- if(vcut_process(in,out1,out2,cutpoint))
+static void free_packet(vcut_packet *p)
+{
+ if(p)
{
- fprintf(stderr, "Processing failed\n");
- ret = 1;
+ if(p->packet)
+ free(p->packet);
+ free(p);
}
+}
- fclose(in);
- fclose(out1);
- fclose(out2);
+static long get_blocksize(vcut_state *s, vorbis_info *vi, ogg_packet *op)
+{
+ int this = vorbis_packet_blocksize(vi, op);
+ int ret = (this+s->prevW)/4;
+ s->prevW = this;
return ret;
}
-int vcut_process(FILE *in, FILE *first, FILE *second, ogg_int64_t cutpoint)
+static int update_sync(vcut_state *s, FILE *f)
{
- ogg_stream_state stream_out_first;
- ogg_stream_state stream_out_second;
- vcut_state *s;
-
- s = vcut_new_state();
- fprintf(stderr, "Processing: Cutting at %lld\n", cutpoint);
- s->cutpoint = cutpoint;
-
- /* Read headers in, and save them */
- if(vcut_process_headers(s, in))
- {
- fprintf(stderr, "Error reading headers\n");
- //cleanup();
- }
-
- /* ok, headers are all in, and saved */
- vorbis_synthesis_init(s->vd,s->vi);
- vorbis_block_init(s->vd,s->vb);
-
- ogg_stream_init(&stream_out_first,s->serial); /* first file gets original */
- srand(time(NULL));
- ogg_stream_init(&stream_out_second, rand()); /* second gets random */
-
- vcut_submit_headers_to_stream(&stream_out_first, s);
- vcut_flush_pages_to_file(&stream_out_first, first);
+ unsigned char *buffer = ogg_sync_buffer(s->sync_in, 4096);
+ int bytes = fread(buffer,1,4096,f);
+ ogg_sync_wrote(s->sync_in, bytes);
+ return bytes;
+}
- vcut_submit_headers_to_stream(&stream_out_second, s);
- vcut_flush_pages_to_file(&stream_out_second, second);
+/* Returns 0 for success, or -1 on failure. */
+static int write_pages_to_file(ogg_stream_state *stream,
+ FILE *file, int flush)
+{
+ ogg_page page;
-
- if(vcut_process_first_stream(s, &stream_out_first, in, first))
+ if(flush)
{
- fprintf(stderr, "Error writing first output file\n");
- //cleanup();
+ while(ogg_stream_flush(stream, &page))
+ {
+ if(fwrite(page.header,1,page.header_len, file) != page.header_len)
+ return -1;
+ if(fwrite(page.body,1,page.body_len, file) != page.body_len)
+ return -1;
+ }
}
-
- ogg_stream_clear(&stream_out_first);
-
- if(vcut_process_second_stream(s, &stream_out_second, in, second))
+ else
{
- fprintf(stderr, "Error writing second output file\n");
- //cleanup();
+ while(ogg_stream_pageout(stream, &page))
+ {
+ if(fwrite(page.header,1,page.header_len, file) != page.header_len)
+ return -1;
+ if(fwrite(page.body,1,page.body_len, file) != page.body_len)
+ return -1;
+ }
}
- ogg_stream_clear(&stream_out_second);
- /* Free some memory! */
- vorbis_block_clear(s->vb);
- vorbis_dsp_clear(s->vd);
- vorbis_info_clear(s->vi);
-
- ogg_sync_clear(s->sync_in);
-
return 0;
}
-vcut_state *vcut_new_state()
-{
- vcut_state *s = malloc(sizeof(vcut_state));
- memset(s,0,sizeof(vcut_state));
-
- s->sync_in = malloc(sizeof(ogg_sync_state));
- s->stream_in = malloc(sizeof(ogg_stream_state));
- s->vd = malloc(sizeof(vorbis_dsp_state));
- s->vi = malloc(sizeof(vorbis_info));
- s->vb = malloc(sizeof(vorbis_block));
-
- s->headers = malloc(sizeof(vcut_packet)*3);
- memset(s->headers, 0, sizeof(vcut_packet)*3);
- s->packets = malloc(sizeof(vcut_packet)*2);
- memset(s->packets, 0, sizeof(vcut_packet)*2);
-
- return s;
-}
-
-int vcut_process_first_stream(vcut_state *s, ogg_stream_state *stream,
+/* Write the first stream to output file until we get to the appropriate
+ * cut point.
+ *
+ * We need to do the following:
+ * - Adjust the end of the stream to note the new end of stream.
+ * - Change the final granulepos to be the cutpoint value, so that we don't
+ * decode the extra data past this.
+ * - Save the final two packets in the stream to temporary buffers.
+ * These two packets then become the first two packets in the 2nd stream
+ * (we need two packets because of the overlap-add nature of vorbis).
+ * - For each packet, buffer it (it could be the 2nd last packet, we don't
+ * know yet (but we could optimise this decision based on known maximum
+ * block sizes, and call get_blocksize(), because this updates internal
+ * state needed for sample-accurate block size calculations.
+ */
+static int process_first_stream(vcut_state *s, ogg_stream_state *stream,
FILE *in, FILE *f)
{
int eos=0;
@@ -159,7 +119,7 @@
{
int result = ogg_sync_pageout(s->sync_in, &page);
if(result==0) break;
- else if(result<0) fprintf(stderr, "Page error. May not work\n");
+ else if(result<0) fprintf(stderr, "Page error. Corrupt input.\n");
else
{
granpos = ogg_page_granulepos(&page);
@@ -171,39 +131,44 @@
{
result=ogg_stream_packetout(s->stream_in, &packet);
- /* throw away result */
- vcut_get_blocksize(s,s->vi,&packet);
+ /* throw away result, but update state */
+ get_blocksize(s,s->vi,&packet);
if(result==0) break;
else if(result==-1)
fprintf(stderr, "Bitstream error, continuing\n");
else
{
- /* Free these somewhere: FIXME!!! */
- s->packets[0] = vcut_save_packet(&packet);
+ /* We need to save the last packet in the first
+ * stream - but we don't know when we're going
+ * to get there. So we have to keep every packet
+ * just in case.
+ */
+ if(s->packets[0])
+ free_packet(s->packets[0]);
+ s->packets[0] = save_packet(&packet);
+
ogg_stream_packetin(stream, &packet);
- vcut_write_pages_to_file(stream, f);
+ if(write_pages_to_file(stream, f,0))
+ return -1;
}
}
prevgranpos = granpos;
}
else
- {
- fprintf(stderr, "DEBUG: Page passes cutpoint\n");
- eos=1; /* This way we break out */
- }
-
+ eos=1; /* First stream ends somewhere in this page.
+ We break of out this loop here. */
if(ogg_page_eos(&page))
{
- fprintf(stderr, "Found EOS page\n");
+ fprintf(stderr, "Found EOS before cut point.\n");
eos=1;
}
}
}
if(!eos)
{
- if(vcut_update_sync(s,in)==0)
+ if(update_sync(s,in)==0)
{
fprintf(stderr, "Setting eos: update sync returned 0\n");
eos=1;
@@ -216,7 +181,8 @@
{
fprintf(stderr,
"Cutpoint not within stream. Second file will be empty\n");
- vcut_write_pages_to_file(stream, f);
+ write_pages_to_file(stream, f,0);
+
return -1;
}
@@ -224,54 +190,66 @@
{
int bs;
- bs = vcut_get_blocksize(s, s->vi, &packet);
+ bs = get_blocksize(s, s->vi, &packet);
prevgranpos += bs;
if(prevgranpos > s->cutpoint)
{
- s->packets[1] = vcut_save_packet(&packet);
- packet.granulepos = s->cutpoint; /* Set it! */
+ s->packets[1] = save_packet(&packet);
+ packet.granulepos = s->cutpoint; /* Set it! This 'truncates' the
+ * final packet, as needed. */
packet.e_o_s = 1;
ogg_stream_packetin(stream, &packet);
break;
}
- /* Free these? */
- s->packets[0] = vcut_save_packet(&packet);
+ if(s->packets[0])
+ free_packet(s->packets[0]);
+ s->packets[0] = save_packet(&packet);
ogg_stream_packetin(stream, &packet);
- vcut_write_pages_to_file(stream,f);
+ if(write_pages_to_file(stream,f, 0))
+ return -1;
}
- /* Check that we got at least two packets here */
+ /* Check that we got at least two packets here, which we need later */
if(!s->packets[0] || !s->packets[1])
{
fprintf(stderr, "Unhandled special case: first file too short?\n");
return -1;
}
- vcut_write_pages_to_file(stream,f);
+ if(write_pages_to_file(stream,f, 0))
+ return -1;
/* Remaining samples in first packet */
s->initialgranpos = prevgranpos - s->cutpoint;
- fprintf(stderr, "DEBUG: Trimming %lld samples from end of first stream (placing on second stream\n", s->initialgranpos);
return 0;
}
-int vcut_process_second_stream(vcut_state *s, ogg_stream_state *stream, FILE *in, FILE *f)
+/* Process second stream.
+ *
+ * We need to do more packet manipulation here, because we need to calculate
+ * a new granulepos for every packet, since the old ones are now invalid.
+ * Start by placing the modified first and second packets into the stream.
+ * Then just proceed through the stream modifying packno and granulepos for
+ * each packet, using the granulepos which we track block-by-block.
+ */
+static int process_second_stream(vcut_state *s, ogg_stream_state *stream,
+ FILE *in, FILE *f)
{
ogg_packet packet;
ogg_page page;
int eos=0;
int result;
ogg_int64_t page_granpos, current_granpos=s->initialgranpos;
- ogg_int64_t packetnum=0; /* Do we even care? */
+ ogg_int64_t packetnum=0; /* Should this start from 0 or 3 ? */
packet.bytes = s->packets[0]->length;
packet.packet = s->packets[0]->packet;
packet.b_o_s = 0;
packet.e_o_s = 0;
packet.granulepos = 0;
- packet.packetno = packetnum++; /* 0 or 3 for the first audio packet? */
+ packet.packetno = packetnum++;
ogg_stream_packetin(stream,&packet);
packet.bytes = s->packets[1]->length;
@@ -282,11 +260,20 @@
packet.packetno = packetnum++;
ogg_stream_packetin(stream,&packet);
+ if(ogg_stream_flush(stream, &page)!=0)
+ {
+ fwrite(page.header,1,page.header_len,f);
+ fwrite(page.body,1,page.body_len,f);
+ }
+
while(ogg_stream_flush(stream, &page)!=0)
{
- /* What happens if this _IS_ more than one page? (e.g. really big
- * packets - high bitrate modes or whatever) */
- fprintf(stderr, "DEBUG: Flushing (should only happen _once_)\n");
+ /* Might this happen for _really_ high bitrate modes, if we're
+ * spectacularly unlucky? Doubt it, but let's check for it just
+ * in case.
+ */
+ fprintf(stderr, "ERROR: First two audio packets did not fit into one\n"
+ " ogg page. File may not decode correctly.\n");
fwrite(page.header,1,page.header_len,f);
fwrite(page.body,1,page.body_len,f);
}
@@ -311,7 +298,7 @@
else if(result==-1) fprintf(stderr, "Bitstream error\n");
else
{
- int bs = vcut_get_blocksize(s, s->vi, &packet);
+ int bs = get_blocksize(s, s->vi, &packet);
current_granpos += bs;
if(current_granpos > page_granpos)
{
@@ -321,14 +308,15 @@
packet.granulepos = current_granpos;
packet.packetno = packetnum++;
ogg_stream_packetin(stream, &packet);
- vcut_write_pages_to_file(stream,f);
+ if(write_pages_to_file(stream,f, 0))
+ return -1;
}
}
}
}
if(!eos)
{
- if(vcut_update_sync(s, in)==0)
+ if(update_sync(s, in)==0)
{
fprintf(stderr, "Update sync returned 0, setting eos\n");
eos=1;
@@ -338,30 +326,8 @@
return 0;
}
-
-/* Do we want to move this functionality into core libvorbis? Then we
- * wouldn't need the internal headers to be included. That would be good.
- */
-long vcut_get_blocksize(vcut_state *s, vorbis_info *vi, ogg_packet *op)
-{
- int this = vorbis_packet_blocksize(vi, op);
- int ret = (this+s->prevW)/4;
-
- s->prevW = this;
- return ret;
-}
-
-int vcut_update_sync(vcut_state *s, FILE *f)
-{
- unsigned char *buffer = ogg_sync_buffer(s->sync_in, 4096);
- int bytes = fread(buffer,1,4096,f);
- ogg_sync_wrote(s->sync_in, bytes);
- return bytes;
-}
-
-
-void vcut_submit_headers_to_stream(ogg_stream_state *stream, vcut_state *s)
+static void submit_headers_to_stream(ogg_stream_state *stream, vcut_state *s)
{
int i;
for(i=0;i<3;i++)
@@ -377,8 +343,9 @@
}
}
-
-int vcut_process_headers(vcut_state *s, FILE *in)
+/* Pull out and save the 3 header packets from the input file.
+ */
+static int process_headers(vcut_state *s)
{
vorbis_comment vc;
ogg_page page;
@@ -393,7 +360,7 @@
vorbis_comment_init(&vc);
buffer = ogg_sync_buffer(s->sync_in, 4096);
- bytes = fread(buffer, 1, 4096, in);
+ bytes = fread(buffer, 1, 4096, s->in);
ogg_sync_wrote(s->sync_in, bytes);
if(ogg_sync_pageout(s->sync_in, &page)!=1){
@@ -422,7 +389,7 @@
return -1;
}
- s->headers[0] = vcut_save_packet(&packet);
+ s->headers[0] = save_packet(&packet);
i=0;
while(i<2)
@@ -441,14 +408,14 @@
fprintf(stderr, "Secondary header corrupt\n");
return -1;
}
- s->headers[i+1] = vcut_save_packet(&packet);
+ s->headers[i+1] = save_packet(&packet);
vorbis_synthesis_headerin(s->vi,&vc,&packet);
i++;
}
}
}
buffer=ogg_sync_buffer(s->sync_in, 4096);
- bytes=fread(buffer,1,4096,in);
+ bytes=fread(buffer,1,4096,s->in);
if(bytes==0 && i<2)
{
fprintf(stderr, "EOF in headers\n");
@@ -461,40 +428,195 @@
return 0;
}
+
-vcut_packet *vcut_save_packet(ogg_packet *packet)
+int main(int argc, char **argv)
{
- vcut_packet *p = malloc(sizeof(vcut_packet));
+ ogg_int64_t cutpoint;
+ FILE *in,*out1,*out2;
+ int ret=0;
+ vcut_state *state;
- p->length = packet->bytes;
- p->packet = malloc(p->length);
- memcpy(p->packet, packet->packet, p->length);
+ if(argc<5)
+ {
+ fprintf(stderr,
+ "Usage: vcut infile.ogg outfile1.ogg outfile2.ogg cutpoint\n");
+ exit(1);
+ }
- return p;
+ fprintf(stderr, "WARNING: vcut is still experimental code.\n"
+ "Check that the output files are correct before deleting sources.\n\n");
+
+ in = fopen(argv[1], "rb");
+ if(!in) {
+ fprintf(stderr, "Couldn't open %s for reading\n", argv[1]);
+ exit(1);
+ }
+ out1 = fopen(argv[2], "wb");
+ if(!out1) {
+ fprintf(stderr, "Couldn't open %s for writing\n", argv[2]);
+ exit(1);
+ }
+ out2 = fopen(argv[3], "wb");
+ if(!out2) {
+ fprintf(stderr, "Couldn't open %s for writing\n", argv[3]);
+ exit(1);
+ }
+
+ sscanf(argv[4], "%Ld", &cutpoint);
+
+ fprintf(stderr, "Processing: Cutting at %lld\n", cutpoint);
+
+ state = vcut_new();
+
+ vcut_set_files(state, in,out1,out2);
+ vcut_set_cutpoint(state, cutpoint);
+
+ if(vcut_process(state))
+ {
+ fprintf(stderr, "Processing failed\n");
+ ret = 1;
+ }
+
+ vcut_free(state);
+
+ fclose(in);
+ fclose(out1);
+ fclose(out2);
+
+ return ret;
}
-void vcut_write_pages_to_file(ogg_stream_state *stream, FILE *file)
+int vcut_process(vcut_state *s)
{
- ogg_page page;
+ ogg_stream_state stream_out_first;
+ ogg_stream_state stream_out_second;
- while(ogg_stream_pageout(stream, &page))
+ /* Read headers in, and save them */
+ if(process_headers(s))
{
- fwrite(page.header,1,page.header_len, file);
- fwrite(page.body,1,page.body_len, file);
+ fprintf(stderr, "Error reading headers\n");
+ return -1;
}
+
+ /* ok, headers are all in, and saved */
+ vorbis_synthesis_init(s->vd,s->vi);
+ vorbis_block_init(s->vd,s->vb);
+
+ ogg_stream_init(&stream_out_first,s->serial); /* first file gets original */
+ srand(time(NULL));
+ ogg_stream_init(&stream_out_second, rand()); /* second gets random */
+
+ submit_headers_to_stream(&stream_out_first, s);
+ if(write_pages_to_file(&stream_out_first, s->out1, 1))
+ return -1;
+
+ submit_headers_to_stream(&stream_out_second, s);
+ if(write_pages_to_file(&stream_out_second, s->out2, 1))
+ return -1;
+
+
+ if(process_first_stream(s, &stream_out_first, s->in, s->out1))
+ {
+ fprintf(stderr, "Error writing first output file\n");
+ return -1;
+ }
+
+ ogg_stream_clear(&stream_out_first);
+
+ if(process_second_stream(s, &stream_out_second, s->in, s->out2))
+ {
+ fprintf(stderr, "Error writing second output file\n");
+ return -1;
+ }
+ ogg_stream_clear(&stream_out_second);
+
+ return 0;
}
-void vcut_flush_pages_to_file(ogg_stream_state *stream, FILE *file)
+vcut_state *vcut_new(void)
{
- ogg_page page;
+ vcut_state *s = malloc(sizeof(vcut_state));
+ memset(s,0,sizeof(vcut_state));
+
+ s->sync_in = malloc(sizeof(ogg_sync_state));
+ s->stream_in = malloc(sizeof(ogg_stream_state));
+ s->vd = malloc(sizeof(vorbis_dsp_state));
+ s->vi = malloc(sizeof(vorbis_info));
+ s->vb = malloc(sizeof(vorbis_block));
+
+ s->headers = malloc(sizeof(vcut_packet)*3);
+ memset(s->headers, 0, sizeof(vcut_packet)*3);
+ s->packets = malloc(sizeof(vcut_packet)*2);
+ memset(s->packets, 0, sizeof(vcut_packet)*2);
+
+ return s;
+}
- while(ogg_stream_flush(stream, &page))
+/* Full cleanup of internal state and vorbis/ogg structures */
+void vcut_free(vcut_state *s)
+{
+ if(s)
{
- fwrite(page.header,1,page.header_len, file);
- fwrite(page.body,1,page.body_len, file);
+ if(s->packets)
+ {
+ if(s->packets[0])
+ free_packet(s->packets[0]);
+ if(s->packets[1])
+ free_packet(s->packets[1]);
+ free(s->packets);
+ }
+
+ if(s->headers)
+ {
+ int i;
+ for(i=0; i < 3; i++)
+ if(s->headers[i])
+ free_packet(s->headers[i]);
+ free(s->headers);
+ }
+
+ if(s->vb)
+ {
+ vorbis_block_clear(s->vb);
+ free(s->vb);
+ }
+ if(s->vd)
+ {
+ vorbis_dsp_clear(s->vd);
+ free(s->vd);
+ }
+ if(s->vi)
+ {
+ vorbis_info_clear(s->vi);
+ free(s->vi);
+ }
+ if(s->stream_in)
+ {
+ ogg_stream_clear(s->stream_in);
+ free(s->stream_in);
+ }
+ if(s->sync_in)
+ {
+ ogg_sync_clear(s->sync_in);
+ free(s->sync_in);
+ }
+
+ free(s);
}
}
+void vcut_set_files(vcut_state *s, FILE *in, FILE *out1, FILE *out2)
+{
+ s->in = in;
+ s->out1 = out1;
+ s->out2 = out2;
+}
+
+void vcut_set_cutpoint(vcut_state *s, ogg_int64_t cutpoint)
+{
+ s->cutpoint = cutpoint;
+}
1.3 +7 -13 vorbis-tools/vcut/vcut.h
Index: vcut.h
===================================================================
RCS file: /usr/local/cvsroot/vorbis-tools/vcut/vcut.h,v
retrieving revision 1.2
retrieving revision 1.3
diff -u -r1.2 -r1.3
--- vcut.h 2001/07/04 14:48:55 1.2
+++ vcut.h 2001/07/16 13:09:57 1.3
@@ -22,20 +22,14 @@
unsigned int serial;
vcut_packet **headers; //3
vcut_packet **packets; //2
-} vcut_state;
-
-int vcut_process(FILE *in, FILE *first, FILE *second, ogg_int64_t cutpoint);
-vcut_packet *vcut_save_packet(ogg_packet *packet);
+ FILE *in,*out1,*out2;
+} vcut_state;
-void vcut_submit_headers_to_stream(ogg_stream_state *stream, vcut_state *s);
-int vcut_process_headers(vcut_state *s, FILE *in);
-void vcut_write_pages_to_file(ogg_stream_state *stream, FILE *file);
-void vcut_flush_pages_to_file(ogg_stream_state *stream, FILE *file);
-int vcut_update_sync(vcut_state *s, FILE *f);
-vcut_state *vcut_new_state(void);
-int vcut_process_first_stream(vcut_state *s, ogg_stream_state *stream, FILE *in, FILE *f);
-int vcut_process_second_stream(vcut_state *s, ogg_stream_state *stream, FILE *in,FILE *f);
-long vcut_get_blocksize(vcut_state *s, vorbis_info *vb, ogg_packet *p);
+int vcut_process(vcut_state *state);
+void vcut_set_files(vcut_state *s, FILE *in, FILE *out1, FILE *out2);
+void vcut_set_cutpoint(vcut_state *s, ogg_int64_t cutpoint);
+vcut_state *vcut_new(void);
+void vcut_free(vcut_state *state);
#endif /* __VCUT_H */
--- >8 ----
List archives: http://www.xiph.org/archives/
Ogg project homepage: http://www.xiph.org/ogg/
To unsubscribe from this list, send a message to 'cvs-request at xiph.org'
containing only the word 'unsubscribe' in the body. No subject is needed.
Unsubscribe messages sent to the list will be ignored/filtered.
More information about the commits
mailing list