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