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