10746706f71c97fbd0c620b4ca2d95f4d8d8be9b
[ruby-vorbistagger.git] / ext / vcedit.c
1 /*
2  * Copyright (C) 2000-2001 Michael Smith (msmith at xiph org)
3  *
4  * This library is free software; you can redistribute it and/or
5  * modify it under the terms of the GNU Lesser General Public
6  * License as published by the Free Software Foundation, version 2.
7  *
8  * This library is distributed in the hope that it will be useful,
9  * but WITHOUT ANY WARRANTY; without even the implied warranty of
10  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
11  * Lesser General Public License for more details.
12  *
13  * You should have received a copy of the GNU Lesser General Public
14  * License along with this library; if not, write to the Free Software
15  * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
16  * MA 02110-1301 USA
17  */
18
19 #include <stdio.h>
20 #include <stdbool.h>
21 #include <stdlib.h>
22 #include <string.h>
23 #include <errno.h>
24 #include <limits.h>
25 #include <unistd.h>
26 #include <ogg/ogg.h>
27 #include <vorbis/codec.h>
28 #include <assert.h>
29
30 #include "vcedit.h"
31
32 #define CHUNKSIZE 4096
33
34 struct vcedit_state_St {
35         int refcount;
36
37         ogg_sync_state *oy;
38         ogg_stream_state *os;
39
40         vorbis_comment *vc;
41         vorbis_info *vi;
42
43         char filename[PATH_MAX];
44
45         FILE *in;
46         long serial;
47         unsigned char *mainbuf;
48         unsigned char *bookbuf;
49         int     mainlen;
50         int     booklen;
51         const char *lasterror;
52         char *vendor;
53         int prevW;
54         int extrapage;
55         int eosin;
56 };
57
58 static void
59 vcedit_state_free (vcedit_state *state)
60 {
61         free (state->oy);
62         free (state->os);
63         free (state->vc);
64         free (state->vi);
65         free (state->mainbuf);
66         free (state->bookbuf);
67         free (state->vendor);
68
69         if (state->in) {
70                 fclose (state->in);
71                 state->in = NULL;
72         }
73
74         free (state);
75 }
76
77 static bool
78 vcedit_state_init (vcedit_state *state)
79 {
80         state->refcount = 1;
81
82         state->oy = malloc (sizeof (ogg_sync_state));
83         if (!state->oy)
84                 return false;
85
86         state->os = malloc (sizeof (ogg_stream_state));
87         if (!state->os)
88                 return false;
89
90         state->vc = malloc (sizeof (vorbis_comment));
91         if (!state->vc)
92                 return false;
93
94         state->vi = malloc (sizeof (vorbis_info));
95         if (!state->vi)
96                 return false;
97
98         return true;
99 }
100
101 vcedit_state *
102 vcedit_state_new (const char *filename)
103 {
104         vcedit_state *state;
105
106         state = malloc (sizeof (vcedit_state));
107         if (!state)
108                 return NULL;
109
110         memset (state, 0, sizeof (vcedit_state));
111
112         if (!vcedit_state_init (state)) {
113                 vcedit_state_free (state);
114                 return NULL;
115         }
116
117         snprintf (state->filename, sizeof (state->filename),
118                   "%s", filename);
119
120         return state;
121 }
122
123 const char *
124 vcedit_error (vcedit_state *state)
125 {
126         return state->lasterror;
127 }
128
129 vorbis_comment *
130 vcedit_comments (vcedit_state *state)
131 {
132         return state->vc;
133 }
134
135 static void
136 vcedit_clear_internals (vcedit_state *state)
137 {
138         ogg_stream_clear (state->os);
139         ogg_sync_clear (state->oy);
140
141         vorbis_info_clear (state->vi);
142         vorbis_comment_clear (state->vc);
143
144         free (state->vendor);
145         state->vendor = NULL;
146
147         free (state->mainbuf);
148         state->mainbuf = NULL;
149         state->mainlen = 0;
150
151         free (state->bookbuf);
152         state->bookbuf = NULL;
153         state->booklen = 0;
154
155         state->serial = 0;
156 }
157
158 void
159 vcedit_state_ref (vcedit_state *state)
160 {
161         state->refcount++;
162 }
163
164 void
165 vcedit_state_unref (vcedit_state *state)
166 {
167         if (--state->refcount)
168                 return;
169
170         vcedit_clear_internals (state);
171         vcedit_state_free (state);
172 }
173
174 /* Next two functions pulled straight from libvorbis, apart from one change
175  * - we don't want to overwrite the vendor string.
176  */
177 static void
178 _v_writestring (oggpack_buffer *o, char *s, int len)
179 {
180         while (len--) {
181                 oggpack_write (o, *s++, 8);
182         }
183 }
184
185 static int
186 _commentheader_out (vorbis_comment *vc, char *vendor, ogg_packet *op)
187 {
188         int i;
189
190         oggpack_buffer opb;
191
192         oggpack_writeinit (&opb);
193
194         /* preamble */
195         oggpack_write (&opb, 0x03, 8);
196         _v_writestring (&opb, "vorbis", 6);
197
198         /* vendor */
199         oggpack_write (&opb, strlen (vendor), 32);
200         _v_writestring (&opb, vendor, strlen (vendor));
201
202         /* comments */
203         oggpack_write (&opb, vc->comments, 32);
204
205         for (i = 0; i < vc->comments; i++) {
206                 if (!vc->user_comments[i])
207                         oggpack_write (&opb, 0, 32);
208                 else {
209                         oggpack_write (&opb, vc->comment_lengths[i], 32);
210                         _v_writestring (&opb, vc->user_comments[i],
211                                         vc->comment_lengths[i]);
212                 }
213         }
214
215         oggpack_write (&opb, 1, 1);
216
217         op->packet = _ogg_malloc (oggpack_bytes (&opb));
218         memcpy (op->packet, opb.buffer, oggpack_bytes (&opb));
219
220         op->bytes = oggpack_bytes (&opb);
221         op->b_o_s = 0;
222         op->e_o_s = 0;
223         op->granulepos = 0;
224
225         oggpack_writeclear (&opb);
226
227         return 0;
228 }
229
230 static int
231 _blocksize (vcedit_state *s, ogg_packet *p)
232 {
233         int this, ret = 0;
234
235         this = vorbis_packet_blocksize (s->vi, p);
236
237         if (s->prevW)
238                 ret = (this + s->prevW) / 4;
239
240         s->prevW = this;
241
242         return ret;
243 }
244
245 static int
246 _fetch_next_packet (vcedit_state *s, ogg_packet *p, ogg_page *page)
247 {
248         char *buffer;
249         int result, bytes;
250
251         result = ogg_stream_packetout (s->os, p);
252
253         if (result > 0)
254                 return 1;
255
256         if (s->eosin)
257                 return 0;
258
259         while (ogg_sync_pageout (s->oy, page) <= 0) {
260                 buffer = ogg_sync_buffer (s->oy, CHUNKSIZE);
261                 bytes = fread (buffer, 1, CHUNKSIZE, s->in);
262                 ogg_sync_wrote (s->oy, bytes);
263
264                 if (!bytes)
265                         return 0;
266         }
267
268         if (ogg_page_eos (page))
269                 s->eosin = 1;
270         else if (ogg_page_serialno (page) != s->serial) {
271                 s->eosin = 1;
272                 s->extrapage = 1;
273                 return 0;
274         }
275
276         ogg_stream_pagein (s->os, page);
277
278         return _fetch_next_packet (s, p, page);
279 }
280
281 int
282 vcedit_open (vcedit_state *state)
283 {
284         char *buffer;
285         int bytes, i;
286         int chunks = 0;
287         ogg_packet *header;
288         ogg_packet header_main, header_comments, header_codebooks;
289         ogg_page og;
290
291         state->in = fopen (state->filename, "rb");
292         if (!state->in) {
293                 state->lasterror = "Cannot open file.";
294                 return -1;
295         }
296
297         ogg_sync_init (state->oy);
298
299         while (1) {
300                 buffer = ogg_sync_buffer (state->oy, CHUNKSIZE);
301                 bytes = fread (buffer, 1, CHUNKSIZE, state->in);
302
303                 ogg_sync_wrote (state->oy, bytes);
304
305                 if (ogg_sync_pageout (state->oy, &og) == 1)
306                         break;
307
308                 /* Bail if we don't find data in the first 40 kB */
309                 if (chunks++ >= 10) {
310                         if (bytes < CHUNKSIZE)
311                                 state->lasterror = "Input truncated or empty.";
312                         else
313                                 state->lasterror = "Input is not an Ogg bitstream.";
314
315                         goto err;
316                 }
317         }
318
319         state->serial = ogg_page_serialno (&og);
320
321         ogg_stream_init (state->os, state->serial);
322         vorbis_info_init (state->vi);
323         vorbis_comment_init (state->vc);
324
325         if (ogg_stream_pagein (state->os, &og) < 0) {
326                 state->lasterror = "Error reading first page of Ogg bitstream.";
327                 goto err;
328         }
329
330         if (ogg_stream_packetout (state->os, &header_main) != 1) {
331                 state->lasterror = "Error reading initial header packet.";
332                 goto err;
333         }
334
335         if (vorbis_synthesis_headerin (state->vi, state->vc, &header_main) < 0) {
336                 state->lasterror = "Ogg bitstream does not contain vorbis data.";
337                 goto err;
338         }
339
340         state->mainlen = header_main.bytes;
341         state->mainbuf = malloc (state->mainlen);
342         memcpy (state->mainbuf, header_main.packet, header_main.bytes);
343
344         i = 0;
345         header = &header_comments;
346
347         while (i < 2) {
348                 while (i < 2) {
349                         int result = ogg_sync_pageout (state->oy, &og);
350
351                         if (!result)
352                                 break; /* Too little data so far */
353
354                         if (result == 1) {
355                                 ogg_stream_pagein (state->os, &og);
356
357                                 while (i < 2) {
358                                         result = ogg_stream_packetout (state->os, header);
359
360                                         if (!result)
361                                                 break;
362
363                                         if (result == -1) {
364                                                 state->lasterror = "Corrupt secondary header.";
365                                                 goto err;
366                                         }
367
368                                         vorbis_synthesis_headerin (state->vi, state->vc, header);
369
370                                         if (i == 1) {
371                                                 state->booklen = header->bytes;
372                                                 state->bookbuf = malloc (state->booklen);
373                                                 memcpy (state->bookbuf, header->packet, header->bytes);
374                                         }
375
376                                         i++;
377                                         header = &header_codebooks;
378                                 }
379                         }
380                 }
381
382                 buffer = ogg_sync_buffer (state->oy, CHUNKSIZE);
383                 bytes = fread (buffer, 1, CHUNKSIZE, state->in);
384
385                 if (bytes == 0 && i < 2) {
386                         state->lasterror = "EOF before end of vorbis headers.";
387                         goto err;
388                 }
389
390                 ogg_sync_wrote (state->oy, bytes);
391         }
392
393         /* Copy the vendor tag */
394         state->vendor = strdup (state->vc->vendor);
395
396         /* Headers are done! */
397         return 0;
398
399 err:
400         vcedit_clear_internals (state);
401
402         return -1;
403 }
404
405 int
406 vcedit_write (vcedit_state *state)
407 {
408         ogg_stream_state streamout;
409         ogg_packet header_main, header_comments, header_codebooks, op;
410         ogg_page ogout, ogin;
411         ogg_int64_t granpos = 0;
412         FILE *out;
413         char *buffer, tmpfile[PATH_MAX];
414         int s, result, bytes, needflush = 0, needout = 0;
415         size_t tmp;
416
417         strcpy (tmpfile, state->filename);
418         strcat (tmpfile, ".XXXXXX");
419
420         s = mkstemp (tmpfile);
421         if (s == -1) {
422                 state->lasterror = "Error writing stream to output. "
423                                    "Cannot open temporary file.";
424                 return -1;
425         }
426
427         out = fdopen (s, "wb");
428         if (!out) {
429                 unlink (tmpfile);
430                 close (s);
431                 state->lasterror = "Error writing stream to output. "
432                                    "Cannot open temporary file.";
433                 return -1;
434         }
435
436         state->prevW = state->extrapage = state->eosin = 0;
437
438         header_main.bytes = state->mainlen;
439         header_main.packet = state->mainbuf;
440         header_main.b_o_s = 1;
441         header_main.e_o_s = 0;
442         header_main.granulepos = 0;
443
444         header_codebooks.bytes = state->booklen;
445         header_codebooks.packet = state->bookbuf;
446         header_codebooks.b_o_s = 0;
447         header_codebooks.e_o_s = 0;
448         header_codebooks.granulepos = 0;
449
450         ogg_stream_init (&streamout, state->serial);
451
452         _commentheader_out (state->vc, state->vendor, &header_comments);
453
454         ogg_stream_packetin (&streamout, &header_main);
455         ogg_stream_packetin (&streamout, &header_comments);
456         ogg_stream_packetin (&streamout, &header_codebooks);
457
458         while ((result = ogg_stream_flush (&streamout, &ogout))) {
459                 tmp = fwrite (ogout.header, 1, ogout.header_len, out);
460                 if (tmp != (size_t) ogout.header_len)
461                         goto cleanup;
462
463                 tmp = fwrite (ogout.body, 1, ogout.body_len, out);
464                 if (tmp != (size_t) ogout.body_len)
465                         goto cleanup;
466         }
467
468         while (_fetch_next_packet (state, &op, &ogin)) {
469                 int size;
470
471                 size = _blocksize (state, &op);
472                 granpos += size;
473
474                 if (needflush) {
475                         if (ogg_stream_flush (&streamout, &ogout)) {
476                                 tmp = fwrite (ogout.header, 1, ogout.header_len, out);
477                                 if (tmp != (size_t) ogout.header_len)
478                                         goto cleanup;
479
480                                 tmp = fwrite (ogout.body, 1, ogout.body_len, out);
481                                 if (tmp != (size_t) ogout.body_len)
482                                         goto cleanup;
483                         }
484                 } else if (needout) {
485                         if (ogg_stream_pageout (&streamout, &ogout)) {
486                                 tmp = fwrite (ogout.header, 1, ogout.header_len, out);
487                                 if (tmp != (size_t) ogout.header_len)
488                                         goto cleanup;
489
490                                 tmp = fwrite (ogout.body, 1, ogout.body_len, out);
491                                 if (tmp != (size_t) ogout.body_len)
492                                         goto cleanup;
493                         }
494                 }
495
496                 needflush = needout = 0;
497
498                 if (op.granulepos == -1) {
499                         op.granulepos = granpos;
500                         ogg_stream_packetin (&streamout, &op);
501                 } else {
502                         /* granulepos is set, validly. Use it, and force a flush to
503                          * account for shortened blocks (vcut) when appropriate
504                          */
505                         if (granpos > op.granulepos) {
506                                 granpos = op.granulepos;
507                                 ogg_stream_packetin (&streamout, &op);
508                                 needflush = 1;
509                         } else {
510                                 ogg_stream_packetin (&streamout, &op);
511                                 needout = 1;
512                         }
513                 }
514         }
515
516         streamout.e_o_s = 1;
517
518         while (ogg_stream_flush (&streamout, &ogout)) {
519                 tmp = fwrite (ogout.header, 1, ogout.header_len, out);
520                 if (tmp != (size_t) ogout.header_len)
521                         goto cleanup;
522
523                 tmp = fwrite (ogout.body, 1, ogout.body_len, out);
524                 if (tmp != (size_t) ogout.body_len)
525                         goto cleanup;
526         }
527
528         if (state->extrapage) {
529                 tmp = fwrite (ogin.header, 1, ogin.header_len, out);
530                 if (tmp != (size_t) ogin.header_len)
531                         goto cleanup;
532
533                 tmp = fwrite (ogin.body, 1, ogin.body_len, out);
534                 if (tmp != (size_t) ogin.body_len)
535                         goto cleanup;
536         }
537
538         /* clear it, because not all paths to here do */
539         state->eosin = 0;
540
541         while (!state->eosin) { /* We reached eos, not eof */
542                 /* We copy the rest of the stream (other logical streams)
543                  * through, a page at a time.
544                  */
545                 while (1) {
546                         result = ogg_sync_pageout (state->oy, &ogout);
547
548                         if (!result)
549                 break;
550
551                         if (result < 0)
552                                 state->lasterror = "Corrupt or missing data, continuing...";
553                         else {
554                                 /* Don't bother going through the rest, we can just
555                                  * write the page out now
556                                  */
557                                 tmp = fwrite (ogout.header, 1, ogout.header_len, out);
558                                 if (tmp != (size_t) ogout.header_len)
559                                         goto cleanup;
560
561                                 tmp = fwrite (ogout.body, 1, ogout.body_len, out);
562                                 if (tmp != (size_t) ogout.body_len)
563                                         goto cleanup;
564                         }
565                 }
566
567                 buffer = ogg_sync_buffer (state->oy, CHUNKSIZE);
568                 bytes = fread (buffer, 1, CHUNKSIZE, state->in);
569                 ogg_sync_wrote (state->oy, bytes);
570
571                 if (!bytes) {
572                         state->eosin = 1;
573                         break;
574                 }
575         }
576
577         fclose (out);
578         fclose (state->in);
579
580         unlink (state->filename);
581         rename (tmpfile, state->filename);
582
583 cleanup:
584         ogg_stream_clear (&streamout);
585
586     /* We don't ogg_packet_clear() this, because the memory was
587          * allocated in _commentheader_out(), so we mirror that here
588          */
589     _ogg_free (header_comments.packet);
590
591         free (state->mainbuf);
592         free (state->bookbuf);
593
594     state->mainbuf = state->bookbuf = NULL;
595
596         if (!state->eosin) {
597                 state->lasterror = "Error writing stream to output. "
598                                    "Output stream may be corrupted or truncated.";
599                 return -1;
600         }
601
602         vcedit_clear_internals (state);
603         vcedit_open (state);
604
605         return 0;
606 }