1aee14c25ed1fcf4cd0b0ba2fb2d53edd903b3fa
[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         bool opened;
47         long serial;
48         unsigned char *mainbuf;
49         unsigned char *bookbuf;
50         int     mainlen;
51         int     booklen;
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 vorbis_comment *
124 vcedit_comments (vcedit_state *state)
125 {
126         return state->opened ? state->vc : NULL;
127 }
128
129 static void
130 vcedit_clear_internals (vcedit_state *state)
131 {
132         ogg_stream_clear (state->os);
133         ogg_sync_clear (state->oy);
134
135         vorbis_info_clear (state->vi);
136         vorbis_comment_clear (state->vc);
137
138         free (state->vendor);
139         state->vendor = NULL;
140
141         free (state->mainbuf);
142         state->mainbuf = NULL;
143         state->mainlen = 0;
144
145         free (state->bookbuf);
146         state->bookbuf = NULL;
147         state->booklen = 0;
148
149         state->serial = 0;
150         state->opened = false;
151 }
152
153 void
154 vcedit_state_ref (vcedit_state *state)
155 {
156         state->refcount++;
157 }
158
159 void
160 vcedit_state_unref (vcedit_state *state)
161 {
162         if (--state->refcount)
163                 return;
164
165         if (state->opened)
166                 vcedit_clear_internals (state);
167
168         vcedit_state_free (state);
169 }
170
171 /* Next two functions pulled straight from libvorbis, apart from one change
172  * - we don't want to overwrite the vendor string.
173  */
174 static void
175 _v_writestring (oggpack_buffer *o, char *s, int len)
176 {
177         while (len--) {
178                 oggpack_write (o, *s++, 8);
179         }
180 }
181
182 static int
183 _commentheader_out (vorbis_comment *vc, char *vendor, ogg_packet *op)
184 {
185         int i;
186
187         oggpack_buffer opb;
188
189         oggpack_writeinit (&opb);
190
191         /* preamble */
192         oggpack_write (&opb, 0x03, 8);
193         _v_writestring (&opb, "vorbis", 6);
194
195         /* vendor */
196         oggpack_write (&opb, strlen (vendor), 32);
197         _v_writestring (&opb, vendor, strlen (vendor));
198
199         /* comments */
200         oggpack_write (&opb, vc->comments, 32);
201
202         for (i = 0; i < vc->comments; i++) {
203                 if (!vc->user_comments[i])
204                         oggpack_write (&opb, 0, 32);
205                 else {
206                         oggpack_write (&opb, vc->comment_lengths[i], 32);
207                         _v_writestring (&opb, vc->user_comments[i],
208                                         vc->comment_lengths[i]);
209                 }
210         }
211
212         oggpack_write (&opb, 1, 1);
213
214         op->packet = _ogg_malloc (oggpack_bytes (&opb));
215         memcpy (op->packet, opb.buffer, oggpack_bytes (&opb));
216
217         op->bytes = oggpack_bytes (&opb);
218         op->b_o_s = 0;
219         op->e_o_s = 0;
220         op->granulepos = 0;
221
222         oggpack_writeclear (&opb);
223
224         return 0;
225 }
226
227 static int
228 _blocksize (vcedit_state *s, ogg_packet *p)
229 {
230         int this, ret = 0;
231
232         this = vorbis_packet_blocksize (s->vi, p);
233
234         if (s->prevW)
235                 ret = (this + s->prevW) / 4;
236
237         s->prevW = this;
238
239         return ret;
240 }
241
242 static int
243 _fetch_next_packet (vcedit_state *s, ogg_packet *p, ogg_page *page)
244 {
245         char *buffer;
246         int result, bytes;
247
248         result = ogg_stream_packetout (s->os, p);
249
250         if (result > 0)
251                 return 1;
252
253         if (s->eosin)
254                 return 0;
255
256         while (ogg_sync_pageout (s->oy, page) <= 0) {
257                 buffer = ogg_sync_buffer (s->oy, CHUNKSIZE);
258                 bytes = fread (buffer, 1, CHUNKSIZE, s->in);
259                 ogg_sync_wrote (s->oy, bytes);
260
261                 if (!bytes)
262                         return 0;
263         }
264
265         if (ogg_page_eos (page))
266                 s->eosin = 1;
267         else if (ogg_page_serialno (page) != s->serial) {
268                 s->eosin = 1;
269                 s->extrapage = 1;
270                 return 0;
271         }
272
273         ogg_stream_pagein (s->os, page);
274
275         return _fetch_next_packet (s, p, page);
276 }
277
278 vcedit_error
279 vcedit_open (vcedit_state *state)
280 {
281         vcedit_error ret;
282         char *buffer;
283         int bytes, i;
284         int chunks = 0;
285         ogg_packet *header;
286         ogg_packet header_main, header_comments, header_codebooks;
287         ogg_page og;
288
289         state->in = fopen (state->filename, "rb");
290         if (!state->in)
291                 return VCEDIT_ERR_OPEN;
292
293         ogg_sync_init (state->oy);
294
295         while (1) {
296                 buffer = ogg_sync_buffer (state->oy, CHUNKSIZE);
297                 bytes = fread (buffer, 1, CHUNKSIZE, state->in);
298
299                 ogg_sync_wrote (state->oy, bytes);
300
301                 if (ogg_sync_pageout (state->oy, &og) == 1)
302                         break;
303
304                 /* Bail if we don't find data in the first 40 kB */
305                 if (chunks++ >= 10) {
306                         ogg_sync_clear (state->oy);
307
308                         return VCEDIT_ERR_INVAL;
309                 }
310         }
311
312         state->serial = ogg_page_serialno (&og);
313
314         ogg_stream_init (state->os, state->serial);
315         vorbis_info_init (state->vi);
316         vorbis_comment_init (state->vc);
317
318         if (ogg_stream_pagein (state->os, &og) < 0) {
319                 ret = VCEDIT_ERR_INVAL;
320                 goto err;
321         }
322
323         if (ogg_stream_packetout (state->os, &header_main) != 1) {
324                 ret = VCEDIT_ERR_INVAL;
325                 goto err;
326         }
327
328         if (vorbis_synthesis_headerin (state->vi, state->vc, &header_main) < 0) {
329                 ret = VCEDIT_ERR_INVAL;
330                 goto err;
331         }
332
333         state->mainlen = header_main.bytes;
334         state->mainbuf = malloc (state->mainlen);
335         memcpy (state->mainbuf, header_main.packet, header_main.bytes);
336
337         i = 0;
338         header = &header_comments;
339
340         while (i < 2) {
341                 while (i < 2) {
342                         int result = ogg_sync_pageout (state->oy, &og);
343
344                         if (!result)
345                                 break; /* Too little data so far */
346
347                         if (result == 1) {
348                                 ogg_stream_pagein (state->os, &og);
349
350                                 while (i < 2) {
351                                         result = ogg_stream_packetout (state->os, header);
352
353                                         if (!result)
354                                                 break;
355
356                                         if (result == -1) {
357                                                 ret = VCEDIT_ERR_INVAL;
358                                                 goto err;
359                                         }
360
361                                         vorbis_synthesis_headerin (state->vi, state->vc, header);
362
363                                         if (i == 1) {
364                                                 state->booklen = header->bytes;
365                                                 state->bookbuf = malloc (state->booklen);
366                                                 memcpy (state->bookbuf, header->packet, header->bytes);
367                                         }
368
369                                         i++;
370                                         header = &header_codebooks;
371                                 }
372                         }
373                 }
374
375                 buffer = ogg_sync_buffer (state->oy, CHUNKSIZE);
376                 bytes = fread (buffer, 1, CHUNKSIZE, state->in);
377
378                 if (bytes == 0 && i < 2) {
379                         ret = VCEDIT_ERR_INVAL;
380                         goto err;
381                 }
382
383                 ogg_sync_wrote (state->oy, bytes);
384         }
385
386         /* Copy the vendor tag */
387         state->vendor = strdup (state->vc->vendor);
388
389         /* Headers are done! */
390         state->opened = true;
391
392         return VCEDIT_ERR_SUCCESS;
393
394 err:
395         vcedit_clear_internals (state);
396
397         return ret;
398 }
399
400 vcedit_error
401 vcedit_write (vcedit_state *state)
402 {
403         ogg_stream_state streamout;
404         ogg_packet header_main, header_comments, header_codebooks, op;
405         ogg_page ogout, ogin;
406         ogg_int64_t granpos = 0;
407         FILE *out;
408         char *buffer, tmpfile[PATH_MAX];
409         int s, result, bytes, needflush = 0, needout = 0;
410         size_t tmp;
411
412         if (!state->opened)
413                 return VCEDIT_ERR_INVAL;
414
415         strcpy (tmpfile, state->filename);
416         strcat (tmpfile, ".XXXXXX");
417
418         s = mkstemp (tmpfile);
419         if (s == -1)
420                 return VCEDIT_ERR_TMPFILE;
421
422         out = fdopen (s, "wb");
423         if (!out) {
424                 unlink (tmpfile);
425                 close (s);
426
427                 return VCEDIT_ERR_TMPFILE;
428         }
429
430         state->prevW = state->extrapage = state->eosin = 0;
431
432         header_main.bytes = state->mainlen;
433         header_main.packet = state->mainbuf;
434         header_main.b_o_s = 1;
435         header_main.e_o_s = 0;
436         header_main.granulepos = 0;
437
438         header_codebooks.bytes = state->booklen;
439         header_codebooks.packet = state->bookbuf;
440         header_codebooks.b_o_s = 0;
441         header_codebooks.e_o_s = 0;
442         header_codebooks.granulepos = 0;
443
444         ogg_stream_init (&streamout, state->serial);
445
446         _commentheader_out (state->vc, state->vendor, &header_comments);
447
448         ogg_stream_packetin (&streamout, &header_main);
449         ogg_stream_packetin (&streamout, &header_comments);
450         ogg_stream_packetin (&streamout, &header_codebooks);
451
452         while ((result = ogg_stream_flush (&streamout, &ogout))) {
453                 tmp = fwrite (ogout.header, 1, ogout.header_len, out);
454                 if (tmp != (size_t) ogout.header_len)
455                         goto cleanup;
456
457                 tmp = fwrite (ogout.body, 1, ogout.body_len, out);
458                 if (tmp != (size_t) ogout.body_len)
459                         goto cleanup;
460         }
461
462         while (_fetch_next_packet (state, &op, &ogin)) {
463                 int size;
464
465                 size = _blocksize (state, &op);
466                 granpos += size;
467
468                 if (needflush) {
469                         if (ogg_stream_flush (&streamout, &ogout)) {
470                                 tmp = fwrite (ogout.header, 1, ogout.header_len, out);
471                                 if (tmp != (size_t) ogout.header_len)
472                                         goto cleanup;
473
474                                 tmp = fwrite (ogout.body, 1, ogout.body_len, out);
475                                 if (tmp != (size_t) ogout.body_len)
476                                         goto cleanup;
477                         }
478                 } else if (needout) {
479                         if (ogg_stream_pageout (&streamout, &ogout)) {
480                                 tmp = fwrite (ogout.header, 1, ogout.header_len, out);
481                                 if (tmp != (size_t) ogout.header_len)
482                                         goto cleanup;
483
484                                 tmp = fwrite (ogout.body, 1, ogout.body_len, out);
485                                 if (tmp != (size_t) ogout.body_len)
486                                         goto cleanup;
487                         }
488                 }
489
490                 needflush = needout = 0;
491
492                 if (op.granulepos == -1) {
493                         op.granulepos = granpos;
494                         ogg_stream_packetin (&streamout, &op);
495                 } else {
496                         /* granulepos is set, validly. Use it, and force a flush to
497                          * account for shortened blocks (vcut) when appropriate
498                          */
499                         if (granpos > op.granulepos) {
500                                 granpos = op.granulepos;
501                                 ogg_stream_packetin (&streamout, &op);
502                                 needflush = 1;
503                         } else {
504                                 ogg_stream_packetin (&streamout, &op);
505                                 needout = 1;
506                         }
507                 }
508         }
509
510         streamout.e_o_s = 1;
511
512         while (ogg_stream_flush (&streamout, &ogout)) {
513                 tmp = fwrite (ogout.header, 1, ogout.header_len, out);
514                 if (tmp != (size_t) ogout.header_len)
515                         goto cleanup;
516
517                 tmp = fwrite (ogout.body, 1, ogout.body_len, out);
518                 if (tmp != (size_t) ogout.body_len)
519                         goto cleanup;
520         }
521
522         if (state->extrapage) {
523                 tmp = fwrite (ogin.header, 1, ogin.header_len, out);
524                 if (tmp != (size_t) ogin.header_len)
525                         goto cleanup;
526
527                 tmp = fwrite (ogin.body, 1, ogin.body_len, out);
528                 if (tmp != (size_t) ogin.body_len)
529                         goto cleanup;
530         }
531
532         /* clear it, because not all paths to here do */
533         state->eosin = 0;
534
535         while (!state->eosin) { /* We reached eos, not eof */
536                 /* We copy the rest of the stream (other logical streams)
537                  * through, a page at a time.
538                  */
539                 while (1) {
540                         result = ogg_sync_pageout (state->oy, &ogout);
541
542                         if (!result)
543                 break;
544
545                         if (result >= 0) {
546                                 /* Don't bother going through the rest, we can just
547                                  * write the page out now
548                                  */
549                                 tmp = fwrite (ogout.header, 1, ogout.header_len, out);
550                                 if (tmp != (size_t) ogout.header_len)
551                                         goto cleanup;
552
553                                 tmp = fwrite (ogout.body, 1, ogout.body_len, out);
554                                 if (tmp != (size_t) ogout.body_len)
555                                         goto cleanup;
556                         }
557                 }
558
559                 buffer = ogg_sync_buffer (state->oy, CHUNKSIZE);
560                 bytes = fread (buffer, 1, CHUNKSIZE, state->in);
561                 ogg_sync_wrote (state->oy, bytes);
562
563                 if (!bytes) {
564                         state->eosin = 1;
565                         break;
566                 }
567         }
568
569         fclose (out);
570         fclose (state->in);
571
572         unlink (state->filename);
573         rename (tmpfile, state->filename);
574
575 cleanup:
576         ogg_stream_clear (&streamout);
577
578     /* We don't ogg_packet_clear() this, because the memory was
579          * allocated in _commentheader_out(), so we mirror that here
580          */
581     _ogg_free (header_comments.packet);
582
583         free (state->mainbuf);
584         free (state->bookbuf);
585
586     state->mainbuf = state->bookbuf = NULL;
587
588         if (!state->eosin)
589                 return VCEDIT_ERR_INVAL;
590
591         vcedit_clear_internals (state);
592
593         return (vcedit_open (state) == VCEDIT_ERR_SUCCESS) ?
594                VCEDIT_ERR_SUCCESS : VCEDIT_ERR_REOPEN;
595 }