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