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