1 /* This program is licensed under the GNU General Public License, version 2,
2  * a copy of which is included with this program.
3  *
4  * (c) 2000-2001 Michael Smith <msmith@xiph.org>
5  * (c) 2008 Michael Gold <mgold@ncf.ca>
6  *
7  *
8  * Simple application to cut an ogg at a specified frame, and produce two
9  * output files.
10  *
11  * last modified: $Id$
12  */
13 
14 #ifdef HAVE_CONFIG_H
15 #include <config.h>
16 #endif
17 
18 #include <assert.h>
19 #include <stdio.h>
20 #include <stdlib.h>
21 #include <sys/types.h>
22 #include <ogg/ogg.h>
23 #include <vorbis/codec.h>
24 #include <time.h>
25 #include <errno.h>
26 #include <string.h>
27 
28 #ifndef _MSC_VER
29 #include <unistd.h>
30 #endif
31 
32 #include "vcut.h"
33 
34 #include <locale.h>
35 #include "i18n.h"
36 
37 #include <inttypes.h>
38 
clear_packet(vcut_packet * p)39 static void clear_packet(vcut_packet *p)
40 {
41 	if(p->packet)
42 		free(p->packet);
43 	p->packet = NULL;
44 }
45 
46 /* Returns 0 for success, or -1 on failure. */
vcut_malloc(size_t size)47 static void *vcut_malloc(size_t size)
48 {
49 	void *ret = malloc(size);
50 	/* FIXME: libogg will probably crash if one of its malloc calls fails,
51 	 *        so we can't always catch an OOM error. */
52 	if(!ret)
53 		fprintf(stderr, _("Out of memory\n"));
54 	return ret;
55 }
56 
57 /* Returns 0 for success, or -1 on failure. */
save_packet(ogg_packet * packet,vcut_packet * p)58 static int save_packet(ogg_packet *packet, vcut_packet *p)
59 {
60 	clear_packet(p);
61 	p->length = packet->bytes;
62 	p->packet = vcut_malloc(p->length);
63 	if(!p->packet)
64 		return -1;
65 
66 	memcpy(p->packet, packet->packet, p->length);
67 	return 0;
68 }
69 
get_blocksize(vcut_vorbis_stream * vs,ogg_packet * op)70 static long get_blocksize(vcut_vorbis_stream *vs, ogg_packet *op)
71 {
72 	int this = vorbis_packet_blocksize(&vs->vi, op);
73 	int ret = (this+vs->prevW)/4;
74 
75 	vs->prevW = this;
76 	return ret;
77 }
78 
update_sync(vcut_state * s)79 static int update_sync(vcut_state *s)
80 {
81 	char *buffer = ogg_sync_buffer(&s->sync_in, 4096);
82 	int bytes = fread(buffer, 1, 4096, s->in);
83 	ogg_sync_wrote(&s->sync_in, bytes);
84 	return bytes;
85 }
86 
87 /* Writes pages to the given file, or discards them if file is NULL.
88  * Returns 0 for success, or -1 on failure. */
write_pages_to_file(ogg_stream_state * stream,FILE * file,int flush)89 static int write_pages_to_file(ogg_stream_state *stream,
90 		FILE *file, int flush)
91 {
92 	ogg_page page;
93 
94 	if(flush)
95 	{
96 		while(ogg_stream_flush(stream, &page))
97 		{
98 			if(!file) continue;
99 			if(fwrite(page.header,1,page.header_len, file) != page.header_len)
100 				return -1;
101 			if(fwrite(page.body,1,page.body_len, file) != page.body_len)
102 				return -1;
103 		}
104 	}
105 	else
106 	{
107 		while(ogg_stream_pageout(stream, &page))
108 		{
109 			if(!file) continue;
110 			if(fwrite(page.header,1,page.header_len, file) != page.header_len)
111 				return -1;
112 			if(fwrite(page.body,1,page.body_len, file) != page.body_len)
113 				return -1;
114 		}
115 	}
116 
117 	return 0;
118 }
119 
120 
121 /* Flushes and closes the output stream, leaving the file open.
122  * Returns 0 for success, or -1 on failure. */
close_output_stream(vcut_state * s)123 static int close_output_stream(vcut_state *s)
124 {
125 	assert(s->output_stream_open);
126 
127 	if(write_pages_to_file(&s->stream_out, s->out, 1) != 0)
128 	{
129 		fprintf(stderr, _("Couldn't flush output stream\n"));
130 		return -1;
131 	}
132 
133 	ogg_stream_clear(&s->stream_out);
134 	s->output_stream_open = 0;
135 	return 0;
136 }
137 
138 /* Closes the output file and stream.
139  * Returns 0 for success, or -1 on failure. */
close_output_file(vcut_state * s)140 static int close_output_file(vcut_state *s)
141 {
142 	FILE *out = s->out;
143 	if(s->output_stream_open && (close_output_stream(s) != 0))
144 		return -1;
145 
146 	s->out = NULL;
147 	if(out && fclose(out) != 0)
148 	{
149 		fprintf(stderr, _("Couldn't close output file\n"));
150 		return -1;
151 	}
152 
153 	s->output_filename = NULL;
154 	s->drop_output = 0;
155 	return 0;
156 }
157 
158 /* Write out the header packets and reference audio packet. */
submit_headers_to_stream(vcut_state * s)159 static int submit_headers_to_stream(vcut_state *s)
160 {
161 	vcut_vorbis_stream *vs = &s->vorbis_stream;
162 	int i;
163 	for(i=0;i<4;i++)
164 	{
165 		ogg_packet p;
166 		if(i < 3)  /* a header packet */
167 		{
168 			p.bytes = vs->headers[i].length;
169 			p.packet = vs->headers[i].packet;
170 		}
171 		else  /* the reference audio packet */
172 		{
173 			if (!vs->last_packet.packet) break;
174 			p.bytes = vs->last_packet.length;
175 			p.packet = vs->last_packet.packet;
176 		}
177 
178 		assert(p.packet);
179 		p.b_o_s = ((i==0)?1:0);
180 		p.e_o_s = 0;
181 		p.granulepos = 0;
182 		p.packetno = i;
183 
184 		if (write_packet(s, &p) != 0)
185 			return -1;
186 	}
187 	return 0;
188 }
189 
190 /* Opens the given output file; or sets s->drop_output if the filename is ".".
191  * Returns 0 for success, or -1 on failure. */
open_output_file(vcut_state * s,char * filename)192 static int open_output_file(vcut_state *s, char *filename)
193 {
194 	assert(s->out == NULL);
195 
196 	if(strcmp(filename, ".") == 0)
197 	{
198 		s->out = NULL;
199 		s->drop_output = 1;
200 	}
201 
202 	else
203 	{
204                 if(strcmp(filename, "-") == 0)
205                         s->out = fdopen(1, "wb");
206                 else
207                         s->out = fopen(filename, "wb");
208 		s->drop_output = 0;
209 		if(!s->out) {
210 			fprintf(stderr, _("Couldn't open %s for writing\n"), filename);
211 			return -1;
212 		}
213 	}
214 	return 0;
215 }
216 
217 /* Opens an output stream; if necessary, opens the next output file first.
218  * Returns 0 for success, or -1 on failure. */
open_output_stream(vcut_state * s)219 static int open_output_stream(vcut_state *s)
220 {
221         int rv;
222 	if(!s->out && !s->drop_output)
223 	{
224 		if(open_output_file(s, s->output_filename)!=0)
225 			return -1;
226 	}
227 
228 	/* ogg_stream_init should only fail if stream_out is null */
229 	rv = ogg_stream_init(&s->stream_out, ++s->serial_out);
230 	assert(rv == 0);
231 
232 	s->output_stream_open = 1;
233 	return submit_headers_to_stream(s);
234 }
235 
236 
main(int argc,char ** argv)237 int main(int argc, char **argv)
238 {
239 	int ret=0;
240 	vcut_state state;
241 	vcut_segment *seg;
242 	memset(&state, 0, sizeof(state));
243 
244 	setlocale(LC_ALL, "");
245 	bindtextdomain(PACKAGE, LOCALEDIR);
246 	textdomain(PACKAGE);
247 
248 	if(argc<5)
249 	{
250 		printf(_("Usage: vcut infile.ogg outfile1.ogg"
251 				" outfile2.ogg [cutpoint | +cuttime]\n"));
252 		printf(_("To avoid creating an output file,"
253 				" specify \".\" as its name.\n"));
254 		exit(1);
255 	}
256 
257 	state.in = fopen(argv[1], "rb");
258         if(strcmp(argv[1], "-") == 0)
259                 state.in = fdopen(0, "rb");
260         else
261                 state.in = fopen(argv[1], "rb");
262 	if(!state.in) {
263 		fprintf(stderr, _("Couldn't open %s for reading\n"), argv[1]);
264 		exit(1);
265 	}
266 
267 	state.output_filename = argv[2];
268 	seg = vcut_malloc(sizeof(vcut_segment));
269 	if(!seg)
270 		exit(1);
271 	seg->cuttime = -1;
272 	seg->filename = argv[3];
273 	seg->next = NULL;
274 	state.next_segment = seg;
275 
276 	if(strchr(argv[4], '+') != NULL) {
277 	  if(sscanf(argv[4], "%lf", &seg->cuttime) != 1) {
278 	    fprintf(stderr, _("Couldn't parse cutpoint \"%s\"\n"), argv[4]);
279             exit(1);
280 	  }
281 	} else if(sscanf(argv[4], "%" PRId64, &seg->cutpoint) != 1) {
282 	    fprintf(stderr, _("Couldn't parse cutpoint \"%s\"\n"), argv[4]);
283             exit(1);
284 	}
285 
286 	if(seg->cuttime >= 0) {
287 		printf(_("Processing: Cutting at %lf seconds\n"), seg->cuttime);
288 	} else {
289 		printf(_("Processing: Cutting at %lld samples\n"),
290 				(long long)seg->cutpoint);
291 	}
292 
293 	/* include the PID in the random seed so two instances of vcut that
294 	 * run in the same second will use different serial numbers */
295 	srand(time(NULL) ^ getpid());
296 	state.serial_out = rand();
297 
298 	if(vcut_process(&state) != 0)
299 	{
300 		fprintf(stderr, _("Processing failed\n"));
301 		ret = 1;
302 	}
303 
304 	vcut_clear(&state);
305 	fclose(state.in);
306 
307 	return ret;
308 }
309 
310 /* Returns 0 for success, or -1 on failure. */
process_audio_packet(vcut_state * s,vcut_vorbis_stream * vs,ogg_packet * packet)311 int process_audio_packet(vcut_state *s,
312 		vcut_vorbis_stream *vs, ogg_packet *packet)
313 {
314 	int bs = get_blocksize(vs, packet);
315 	long cut_on_eos = 0;
316 	int packet_done = 0;
317 	ogg_int64_t packet_start_granpos = vs->granulepos;
318 	ogg_int64_t gp_to_global_sample_adj;
319 
320 	if(packet->granulepos >= 0)
321 	{
322 		/* If this is the second audio packet, and our calculated
323 		 * granule position is greater than the packet's, this means
324 		 * some audio samples must be discarded from the beginning
325 		 * when decoding (in this case, the Vorbis I spec. requires
326 		 * that this be the last packet on its page, so its granulepos
327 		 * will be available). Likewise, if the last packet's
328 		 * granulepos is less than expected, samples should be
329 		 * discarded from the end. */
330 
331 		if(vs->granulepos == 0 && packet->granulepos != bs)
332 		{
333 			/* this stream starts at a non-zero granulepos */
334 			vs->initial_granpos = packet->granulepos - bs;
335 			if(vs->initial_granpos < 0)
336 				vs->initial_granpos = 0;
337 		}
338 		else if(packet->granulepos != (vs->granulepos + bs)
339 				&& vs->granulepos > 0 && !packet->e_o_s)
340 		{
341 			fprintf(stderr, _("WARNING: unexpected granulepos "
342 					"%" PRId64 " (expected " "%" PRId64 ")\n"),
343 					packet->granulepos, (vs->granulepos + bs));
344 		}
345 		vs->granulepos = packet->granulepos;
346 	}
347 	else if(vs->granulepos == -1)
348 	{
349 		/* This is the first non-header packet. The next packet
350 		 * will start at granulepos 0, or will be the last packet
351 		 * on its page (as mentioned above). */
352 		vs->granulepos = 0;
353 
354 		/* Don't look for a cutpoint in this packet. */
355 		packet_done = 1;
356 	}
357 	else
358 	{
359 		vs->granulepos += bs;
360 	}
361 
362 	gp_to_global_sample_adj = s->prevstream_samples - vs->initial_granpos;
363 	while(!packet_done)
364 	{
365 		ogg_int64_t rel_cutpoint, rel_sample;
366 		vcut_segment *segment = s->next_segment;
367 		if(!segment) break;
368 
369 		if(segment->cuttime >= 0)
370 		{
371 			/* convert cuttime to cutpoint (a sample number) */
372 			rel_cutpoint = vs->vi.rate * (s->next_segment->cuttime
373 					- s->prevstream_time);
374 		}
375 		else
376 		{
377 			rel_cutpoint = (segment->cutpoint - s->prevstream_samples);
378 		}
379 
380 		rel_sample = vs->granulepos - vs->initial_granpos;
381 		if(rel_sample < rel_cutpoint)
382 			break;
383 
384 		/* reached the cutpoint */
385 
386 		if(rel_sample - bs > rel_cutpoint)
387 		{
388 			/* We passed the cutpoint without finding it. This could mean
389 			 * that the granpos values are discontinuous (in which case
390 			 * we'd have shown an "Unexpected granulepos" error), or that
391 			 * the cutpoints are not sorted correctly. */
392 			fprintf(stderr, _("Cutpoint not found\n"));
393 			return -1;
394 		}
395 
396 		if(rel_sample < bs && !s->drop_output)
397 		{
398 			fprintf(stderr, _("Can't produce a file starting"
399 					" and ending between sample positions " "%" PRId64
400 					" and " "%" PRId64 "\n"),
401 					packet_start_granpos + gp_to_global_sample_adj - 1,
402 					vs->granulepos + gp_to_global_sample_adj);
403 			return -1;
404 		}
405 
406 		/* Set it! This 'truncates' the final packet, as needed. */
407 		packet->granulepos = rel_cutpoint;
408 		cut_on_eos = packet->e_o_s;
409 		packet->e_o_s = 1;
410 		if(rel_cutpoint > 0)
411 		{
412 			if(write_packet(s, packet) != 0)
413 				return -1;
414 		}
415 		if(close_output_file(s) != 0)
416 			return -1;
417 
418 		vs->samples_cut = rel_cutpoint;
419 		packet->e_o_s = cut_on_eos;
420 
421 		s->output_filename = segment->filename;
422 		s->next_segment = segment->next;
423 		free(segment);
424 		segment = NULL;
425 
426 		if(rel_cutpoint == rel_sample)
427 		{
428 			/* There's no unwritten data left in this packet. */
429 			packet_done = 1;
430 		}
431 	}
432 
433 	/* Write the audio packet to the output stream, unless it's the
434 	 * reference packet or we cut it at the last sample. */
435 	if(!packet_done)
436 	{
437 		packet->granulepos = vs->granulepos
438 				- vs->initial_granpos - vs->samples_cut;
439 		if(packet->granulepos < bs && cut_on_eos
440 				&& strcmp(s->output_filename, ".") != 0)
441 		{
442 			fprintf(stderr, _("Can't produce a file starting between sample"
443 					" positions " "%" PRId64 " and " "%" PRId64 ".\n"),
444 					packet_start_granpos + gp_to_global_sample_adj - 1,
445 					vs->granulepos + gp_to_global_sample_adj);
446 			fprintf(stderr, _("Specify \".\" as the second output file"
447 					" to suppress this error.\n"));
448 			return -1;
449 		}
450 		if(write_packet(s, packet) != 0)
451 			return -1;
452 	}
453 
454 	/* We need to save the last packet in the first
455 	 * stream - but we don't know when we're going
456 	 * to get there. So we have to keep every packet
457 	 * just in case. */
458 	if(save_packet(packet, &vs->last_packet) != 0)
459 		return -1;
460 
461 	return 0;
462 }
463 
464 /* Writes a packet, opening an output stream/file if necessary.
465  * Returns 0 for success, or -1 on failure. */
write_packet(vcut_state * s,ogg_packet * packet)466 int write_packet(vcut_state *s, ogg_packet *packet)
467 {
468 	int flush;
469 	if(!s->output_stream_open && open_output_stream(s) != 0)
470 		return -1;
471 
472 	/* According to the Vorbis I spec, we need to flush the stream after:
473 	 *  - the first (BOS) header packet
474 	 *  - the last header packet (packet #2)
475 	 *  - the second audio packet (packet #4), if the stream starts at
476 	 *    a non-zero granulepos */
477 	flush = (s->stream_out.packetno == 2)
478 			|| (s->stream_out.packetno == 4 && packet->granulepos != -1)
479 			|| packet->b_o_s || packet->e_o_s;
480 	ogg_stream_packetin(&s->stream_out, packet);
481 
482 	if(write_pages_to_file(&s->stream_out, s->out, flush) != 0)
483 	{
484 		fprintf(stderr, _("Couldn't write packet to output file\n"));
485 		return -1;
486 	}
487 
488 	if(packet->e_o_s && close_output_stream(s) != 0)
489 		return -1;
490 
491 	return 0;
492 }
493 
494 /* Returns 0 for success, or -1 on failure. */
process_page(vcut_state * s,ogg_page * page)495 int process_page(vcut_state *s, ogg_page *page)
496 {
497 	int headercount;
498 	int result;
499 	vcut_vorbis_stream *vs = &s->vorbis_stream;
500 
501 	if(!s->vorbis_init)
502 	{
503 		if(!ogg_page_bos(page))
504 		{
505 			fprintf(stderr, _("BOS not set on first page of stream\n"));
506 			return -1;
507 		}
508 
509 		memset(vs, 0, sizeof(*vs));
510 		vs->serial = ogg_page_serialno(page);
511 		vs->granulepos = -1;
512 		vs->initial_granpos = 0;
513 		ogg_stream_init(&vs->stream_in, vs->serial);
514 		vorbis_info_init(&vs->vi);
515 		vorbis_comment_init(&vs->vc);
516 		s->vorbis_init = 1;
517 	}
518 	else if(vs->serial != ogg_page_serialno(page))
519 	{
520 		fprintf(stderr, _("Multiplexed bitstreams are not supported\n"));
521 		return -1;
522 	}
523 
524 	/* count the headers */
525 	for(headercount = 0; headercount < 3; ++headercount)
526 		if(!vs->headers[headercount].packet) break;
527 
528 	result = ogg_stream_pagein(&vs->stream_in, page);
529 	if(result)
530 	{
531 		fprintf(stderr, _("Internal stream parsing error\n"));
532 		return -1;
533 	}
534 
535 	while(1)
536 	{
537 		ogg_packet packet;
538 		result = ogg_stream_packetout(&vs->stream_in, &packet);
539 
540 		if(result==0) break;
541 		else if(result==-1)
542 		{
543 			if(headercount < 3)
544 			{
545 				fprintf(stderr, _("Header packet corrupt\n"));
546 				return -1;
547 			}
548 			else
549 			{
550 				if(!s->input_corrupt)
551 					fprintf(stderr, _("Bitstream error, continuing\n"));
552 				s->input_corrupt = 1;
553 				continue;
554 			}
555 		}
556 
557 		if(headercount < 3)  /* this is a header packet */
558 		{
559 			if(vorbis_synthesis_headerin(&vs->vi, &vs->vc, &packet)<0)
560 			{
561 				fprintf(stderr, _("Error in header: not vorbis?\n"));
562 				return -1;
563 			}
564 
565 			if(save_packet(&packet, &vs->headers[headercount]) != 0)
566 				return -1;
567 
568 			++headercount;
569 		}
570 		else  /* this is an audio (non-header) packet */
571 		{
572 			result = process_audio_packet(s, vs, &packet);
573 			if(result != 0)
574 				return result;
575 		}
576 	}
577 
578 	if(ogg_page_eos(page))
579 	{
580 		if(vs->granulepos >= 0)
581 		{
582 			ogg_int64_t samples = vs->granulepos - vs->initial_granpos;
583 			s->prevstream_samples += samples;
584 			s->prevstream_time += (double)samples / vs->vi.rate;
585 		}
586 		vcut_vorbis_clear(vs);
587 		s->vorbis_init = 0;
588 	}
589 
590 	return 0;
591 }
592 
593 /* Returns 0 for success, or -1 on failure. */
vcut_process(vcut_state * s)594 int vcut_process(vcut_state *s)
595 {
596 	int first_page = 1;
597 	do
598 	{
599 		ogg_page page;
600 		int result;
601 
602 		while((result = ogg_sync_pageout(&s->sync_in, &page)) == 1)
603 		{
604 			if(process_page(s, &page) != 0)
605 				return -1;
606 		}
607 
608 		if(result < 0 && !s->input_corrupt)
609 		{
610 			if(first_page)
611 			{
612 				fprintf(stderr, _("Input not ogg.\n"));
613 				return -1;
614 			}
615 
616 			fprintf(stderr, _("Page error, continuing\n"));
617 			/* continue, but don't print this error again */
618 			s->input_corrupt = 1;
619 		}
620 		first_page = 0;
621 	}
622 	while(update_sync(s) > 0);
623 
624 	if(s->vorbis_init)
625 	{
626 		fprintf(stderr, _("WARNING: input file ended unexpectedly\n"));
627 	}
628 	else if(s->next_segment)
629 	{
630 		fprintf(stderr, _("WARNING: found EOS before cutpoint\n"));
631 	}
632 	return close_output_file(s);
633 }
634 
vcut_vorbis_clear(vcut_vorbis_stream * vs)635 void vcut_vorbis_clear(vcut_vorbis_stream *vs)
636 {
637 	int i;
638 	clear_packet(&vs->last_packet);
639 
640 	for(i=0; i < 3; i++)
641 		clear_packet(&vs->headers[i]);
642 
643 	vorbis_block_clear(&vs->vb);
644 	vorbis_dsp_clear(&vs->vd);
645 	vorbis_info_clear(&vs->vi);
646 	ogg_stream_clear(&vs->stream_in);
647 }
648 
649 /* Full cleanup of internal state and vorbis/ogg structures */
vcut_clear(vcut_state * s)650 void vcut_clear(vcut_state *s)
651 {
652 	if(s->vorbis_init)
653 	{
654 		vcut_vorbis_clear(&s->vorbis_stream);
655 		s->vorbis_init = 0;
656 	}
657 	ogg_sync_clear(&s->sync_in);
658 }
659