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