61
|
1 /* This program is licensed under the GNU Library General Public License, version 2,
|
|
2 * a copy of which is included with this program (LICENCE.LGPL).
|
|
3 *
|
|
4 * (c) 2000-2001 Michael Smith <msmith@labyrinth.net.au>
|
|
5 *
|
|
6 *
|
|
7 * Comment editing backend, suitable for use by nice frontend interfaces.
|
|
8 *
|
|
9 */
|
|
10 #include "config.h"
|
|
11
|
|
12 #include <glib.h>
|
|
13 #include <stdio.h>
|
|
14 #include <stdlib.h>
|
|
15 #include <string.h>
|
|
16 #include <ogg/ogg.h>
|
|
17 #include <vorbis/codec.h>
|
|
18
|
|
19 #include "vcedit.h"
|
|
20 #include <libaudacious/vfs.h>
|
|
21
|
|
22 #define CHUNKSIZE 4096
|
|
23
|
|
24 vcedit_state *
|
|
25 vcedit_new_state(void)
|
|
26 {
|
|
27 return g_new0(vcedit_state, 1);
|
|
28 }
|
|
29
|
|
30 char *
|
|
31 vcedit_error(vcedit_state * state)
|
|
32 {
|
|
33 return state->lasterror;
|
|
34 }
|
|
35
|
|
36 vorbis_comment *
|
|
37 vcedit_comments(vcedit_state * state)
|
|
38 {
|
|
39 return state->vc;
|
|
40 }
|
|
41
|
|
42 static void
|
|
43 vcedit_clear_internals(vcedit_state * state)
|
|
44 {
|
|
45 if (state->vc) {
|
|
46 vorbis_comment_clear(state->vc);
|
|
47 g_free(state->vc);
|
|
48 state->vc = NULL;
|
|
49 }
|
|
50 if (state->os) {
|
|
51 ogg_stream_clear(state->os);
|
|
52 g_free(state->os);
|
|
53 state->os = NULL;
|
|
54 }
|
|
55 if (state->oy) {
|
|
56 ogg_sync_clear(state->oy);
|
|
57 g_free(state->oy);
|
|
58 state->oy = NULL;
|
|
59 }
|
|
60 if (state->vendor) {
|
|
61 g_free(state->vendor);
|
|
62 state->vendor = NULL;
|
|
63 }
|
|
64 }
|
|
65
|
|
66 void
|
|
67 vcedit_clear(vcedit_state * state)
|
|
68 {
|
|
69 if (state) {
|
|
70 vcedit_clear_internals(state);
|
|
71 g_free(state);
|
|
72 }
|
|
73 }
|
|
74
|
|
75 /* Next two functions pulled straight from libvorbis, apart from one change
|
|
76 * - we don't want to overwrite the vendor string.
|
|
77 */
|
|
78 static void
|
|
79 _v_writestring(oggpack_buffer * o, char *s, int len)
|
|
80 {
|
|
81 while (len--) {
|
|
82 oggpack_write(o, *s++, 8);
|
|
83 }
|
|
84 }
|
|
85
|
|
86 static int
|
|
87 _commentheader_out(vorbis_comment * vc, char *vendor, ogg_packet * op)
|
|
88 {
|
|
89 oggpack_buffer opb;
|
|
90
|
|
91 oggpack_writeinit(&opb);
|
|
92
|
|
93 /* preamble */
|
|
94 oggpack_write(&opb, 0x03, 8);
|
|
95 _v_writestring(&opb, "vorbis", 6);
|
|
96
|
|
97 /* vendor */
|
|
98 oggpack_write(&opb, strlen(vendor), 32);
|
|
99 _v_writestring(&opb, vendor, strlen(vendor));
|
|
100
|
|
101 /* comments */
|
|
102 oggpack_write(&opb, vc->comments, 32);
|
|
103 if (vc->comments) {
|
|
104 int i;
|
|
105 for (i = 0; i < vc->comments; i++) {
|
|
106 if (vc->user_comments[i]) {
|
|
107 oggpack_write(&opb, vc->comment_lengths[i], 32);
|
|
108 _v_writestring(&opb, vc->user_comments[i],
|
|
109 vc->comment_lengths[i]);
|
|
110 }
|
|
111 else {
|
|
112 oggpack_write(&opb, 0, 32);
|
|
113 }
|
|
114 }
|
|
115 }
|
|
116 oggpack_write(&opb, 1, 1);
|
|
117
|
|
118 op->packet = _ogg_malloc(oggpack_bytes(&opb));
|
|
119 memcpy(op->packet, opb.buffer, oggpack_bytes(&opb));
|
|
120
|
|
121 op->bytes = oggpack_bytes(&opb);
|
|
122 op->b_o_s = 0;
|
|
123 op->e_o_s = 0;
|
|
124 op->granulepos = 0;
|
|
125
|
|
126 return 0;
|
|
127 }
|
|
128
|
|
129 static int
|
|
130 _blocksize(vcedit_state * s, ogg_packet * p)
|
|
131 {
|
|
132 int this = vorbis_packet_blocksize(&s->vi, p);
|
|
133 int ret = (this + s->prevW) / 4;
|
|
134
|
|
135 if (!s->prevW) {
|
|
136 s->prevW = this;
|
|
137 return 0;
|
|
138 }
|
|
139
|
|
140 s->prevW = this;
|
|
141 return ret;
|
|
142 }
|
|
143
|
|
144 static int
|
|
145 _fetch_next_packet(vcedit_state * s, ogg_packet * p, ogg_page * page)
|
|
146 {
|
|
147 int result;
|
|
148 char *buffer;
|
|
149 int bytes;
|
|
150
|
|
151 result = ogg_stream_packetout(s->os, p);
|
|
152
|
|
153 if (result > 0)
|
|
154 return 1;
|
|
155 else {
|
|
156 if (s->eosin)
|
|
157 return 0;
|
|
158 while (ogg_sync_pageout(s->oy, page) <= 0) {
|
|
159 buffer = ogg_sync_buffer(s->oy, CHUNKSIZE);
|
|
160 bytes = s->read(buffer, 1, CHUNKSIZE, s->in);
|
|
161 ogg_sync_wrote(s->oy, bytes);
|
|
162 if (bytes == 0)
|
|
163 return 0;
|
|
164 }
|
|
165 if (ogg_page_eos(page))
|
|
166 s->eosin = 1;
|
|
167 else if (ogg_page_serialno(page) != s->serial) {
|
|
168 s->eosin = 1;
|
|
169 s->extrapage = 1;
|
|
170 return 0;
|
|
171 }
|
|
172
|
|
173 ogg_stream_pagein(s->os, page);
|
|
174 return _fetch_next_packet(s, p, page);
|
|
175 }
|
|
176 }
|
|
177
|
|
178
|
|
179 int
|
|
180 vcedit_open(vcedit_state * state, VFSFile * in)
|
|
181 {
|
|
182 return vcedit_open_callbacks(state, (void *) in,
|
|
183 (vcedit_read_func) vfs_fread,
|
|
184 (vcedit_write_func) vfs_fwrite);
|
|
185 }
|
|
186
|
|
187 int
|
|
188 vcedit_open_callbacks(vcedit_state * state, void *in,
|
|
189 vcedit_read_func read_func,
|
|
190 vcedit_write_func write_func)
|
|
191 {
|
|
192 char *buffer;
|
|
193 int bytes, i;
|
|
194 ogg_packet *header;
|
|
195 ogg_packet header_main;
|
|
196 ogg_packet header_comments;
|
|
197 ogg_packet header_codebooks;
|
|
198 ogg_page og;
|
|
199
|
|
200 state->in = in;
|
|
201 state->read = read_func;
|
|
202 state->write = write_func;
|
|
203
|
|
204 state->oy = g_new(ogg_sync_state, 1);
|
|
205 ogg_sync_init(state->oy);
|
|
206
|
|
207 buffer = ogg_sync_buffer(state->oy, CHUNKSIZE);
|
|
208
|
|
209 bytes = state->read(buffer, 1, CHUNKSIZE, state->in);
|
|
210
|
|
211 ogg_sync_wrote(state->oy, bytes);
|
|
212
|
|
213 if (ogg_sync_pageout(state->oy, &og) != 1) {
|
|
214 if (bytes < CHUNKSIZE)
|
|
215 state->lasterror = "Input truncated or empty.";
|
|
216 else
|
|
217 state->lasterror = "Input is not an Ogg bitstream.";
|
|
218 goto err;
|
|
219 }
|
|
220
|
|
221 state->serial = ogg_page_serialno(&og);
|
|
222
|
|
223 state->os = g_new(ogg_stream_state, 1);
|
|
224 ogg_stream_init(state->os, state->serial);
|
|
225
|
|
226 vorbis_info_init(&state->vi);
|
|
227
|
|
228 state->vc = g_new(vorbis_comment, 1);
|
|
229 vorbis_comment_init(state->vc);
|
|
230
|
|
231 if (ogg_stream_pagein(state->os, &og) < 0) {
|
|
232 state->lasterror = "Error reading first page of Ogg bitstream.";
|
|
233 goto err;
|
|
234 }
|
|
235
|
|
236 if (ogg_stream_packetout(state->os, &header_main) != 1) {
|
|
237 state->lasterror = "Error reading initial header packet.";
|
|
238 goto err;
|
|
239 }
|
|
240
|
|
241 if (vorbis_synthesis_headerin(&state->vi, state->vc, &header_main) < 0) {
|
|
242 state->lasterror = "Ogg bitstream does not contain vorbis data.";
|
|
243 goto err;
|
|
244 }
|
|
245
|
|
246 state->mainlen = header_main.bytes;
|
|
247 state->mainbuf = g_malloc(state->mainlen);
|
|
248 memcpy(state->mainbuf, header_main.packet, header_main.bytes);
|
|
249
|
|
250 i = 0;
|
|
251 header = &header_comments;
|
|
252 while (i < 2) {
|
|
253 while (i < 2) {
|
|
254 int result = ogg_sync_pageout(state->oy, &og);
|
|
255 if (result == 0)
|
|
256 break; /* Too little data so far */
|
|
257 else if (result == 1) {
|
|
258 ogg_stream_pagein(state->os, &og);
|
|
259 while (i < 2) {
|
|
260 result = ogg_stream_packetout(state->os, header);
|
|
261 if (result == 0)
|
|
262 break;
|
|
263 if (result == -1) {
|
|
264 state->lasterror = "Corrupt secondary header.";
|
|
265 goto err;
|
|
266 }
|
|
267 vorbis_synthesis_headerin(&state->vi, state->vc, header);
|
|
268 if (i == 1) {
|
|
269 state->booklen = header->bytes;
|
|
270 state->bookbuf = g_malloc(state->booklen);
|
|
271 memcpy(state->bookbuf, header->packet, header->bytes);
|
|
272 }
|
|
273 i++;
|
|
274 header = &header_codebooks;
|
|
275 }
|
|
276 }
|
|
277 }
|
|
278
|
|
279 buffer = ogg_sync_buffer(state->oy, CHUNKSIZE);
|
|
280 bytes = state->read(buffer, 1, CHUNKSIZE, state->in);
|
|
281 if (bytes == 0 && i < 2) {
|
|
282 state->lasterror = "EOF before end of vorbis headers.";
|
|
283 goto err;
|
|
284 }
|
|
285 ogg_sync_wrote(state->oy, bytes);
|
|
286 }
|
|
287
|
|
288 /* Copy the vendor tag */
|
|
289 state->vendor = g_malloc(strlen(state->vc->vendor) + 1);
|
|
290 strcpy(state->vendor, state->vc->vendor);
|
|
291
|
|
292 /* Headers are done! */
|
|
293 return 0;
|
|
294
|
|
295 err:
|
|
296 vcedit_clear_internals(state);
|
|
297 return -1;
|
|
298 }
|
|
299
|
|
300 #if 0
|
|
301 static void
|
|
302 dump_state(vcedit_state * state)
|
|
303 {
|
|
304 }
|
|
305 #endif
|
|
306
|
|
307 int
|
|
308 vcedit_write(vcedit_state * state, void *out)
|
|
309 {
|
|
310
|
|
311 ogg_stream_state streamout;
|
|
312 ogg_packet header_main;
|
|
313 ogg_packet header_comments;
|
|
314 ogg_packet header_codebooks;
|
|
315
|
|
316 ogg_page ogout, ogin;
|
|
317 ogg_packet op;
|
|
318 ogg_int64_t granpos = 0;
|
|
319 int result;
|
|
320 char *buffer;
|
|
321 int bytes;
|
|
322 int needflush = 0, needout = 0;
|
|
323
|
|
324 state->eosin = 0;
|
|
325 state->extrapage = 0;
|
|
326
|
|
327 header_main.bytes = state->mainlen;
|
|
328 header_main.packet = state->mainbuf;
|
|
329 header_main.b_o_s = 1;
|
|
330 header_main.e_o_s = 0;
|
|
331 header_main.granulepos = 0;
|
|
332
|
|
333 header_codebooks.bytes = state->booklen;
|
|
334 header_codebooks.packet = state->bookbuf;
|
|
335 header_codebooks.b_o_s = 0;
|
|
336 header_codebooks.e_o_s = 0;
|
|
337 header_codebooks.granulepos = 0;
|
|
338
|
|
339 ogg_stream_init(&streamout, state->serial);
|
|
340
|
|
341 _commentheader_out(state->vc, state->vendor, &header_comments);
|
|
342
|
|
343 ogg_stream_packetin(&streamout, &header_main);
|
|
344 ogg_stream_packetin(&streamout, &header_comments);
|
|
345 ogg_stream_packetin(&streamout, &header_codebooks);
|
|
346
|
|
347 while ((result = ogg_stream_flush(&streamout, &ogout))) {
|
|
348 if (state->write(ogout.header, 1, ogout.header_len, out) !=
|
|
349 (size_t) ogout.header_len)
|
|
350 goto cleanup;
|
|
351 if (state->write(ogout.body, 1, ogout.body_len, out) !=
|
|
352 (size_t) ogout.body_len)
|
|
353 goto cleanup;
|
|
354 }
|
|
355
|
|
356 while (_fetch_next_packet(state, &op, &ogin)) {
|
|
357 int size;
|
|
358 size = _blocksize(state, &op);
|
|
359 granpos += size;
|
|
360
|
|
361 if (needflush) {
|
|
362 if (ogg_stream_flush(&streamout, &ogout)) {
|
|
363 if (state->write(ogout.header, 1, ogout.header_len,
|
|
364 out) != (size_t) ogout.header_len)
|
|
365 goto cleanup;
|
|
366 if (state->write(ogout.body, 1, ogout.body_len,
|
|
367 out) != (size_t) ogout.body_len)
|
|
368 goto cleanup;
|
|
369 }
|
|
370 }
|
|
371 else if (needout) {
|
|
372 if (ogg_stream_pageout(&streamout, &ogout)) {
|
|
373 if (state->write(ogout.header, 1, ogout.header_len,
|
|
374 out) != (size_t) ogout.header_len)
|
|
375 goto cleanup;
|
|
376 if (state->write(ogout.body, 1, ogout.body_len,
|
|
377 out) != (size_t) ogout.body_len)
|
|
378 goto cleanup;
|
|
379 }
|
|
380 }
|
|
381
|
|
382 needflush = needout = 0;
|
|
383
|
|
384 if (op.granulepos == -1) {
|
|
385 op.granulepos = granpos;
|
|
386 ogg_stream_packetin(&streamout, &op);
|
|
387 }
|
|
388 else { /* granulepos is set, validly. Use it, and force a flush to
|
|
389 account for shortened blocks (vcut) when appropriate */
|
|
390 if (granpos > op.granulepos) {
|
|
391 granpos = op.granulepos;
|
|
392 ogg_stream_packetin(&streamout, &op);
|
|
393 needflush = 1;
|
|
394 }
|
|
395 else {
|
|
396 ogg_stream_packetin(&streamout, &op);
|
|
397 needout = 1;
|
|
398 }
|
|
399 }
|
|
400 }
|
|
401
|
|
402 streamout.e_o_s = 1;
|
|
403 while (ogg_stream_flush(&streamout, &ogout)) {
|
|
404 if (state->write(ogout.header, 1, ogout.header_len,
|
|
405 out) != (size_t) ogout.header_len)
|
|
406 goto cleanup;
|
|
407 if (state->write(ogout.body, 1, ogout.body_len,
|
|
408 out) != (size_t) ogout.body_len)
|
|
409 goto cleanup;
|
|
410 }
|
|
411
|
|
412 /* FIXME: freeing this here probably leaks memory */
|
|
413 /* Done with this, now */
|
|
414 vorbis_info_clear(&state->vi);
|
|
415
|
|
416 if (state->extrapage) {
|
|
417 if (state->write(ogin.header, 1, ogin.header_len,
|
|
418 out) != (size_t) ogin.header_len)
|
|
419 goto cleanup;
|
|
420 if (state->write(ogin.body, 1, ogin.body_len, out) !=
|
|
421 (size_t) ogin.body_len)
|
|
422 goto cleanup;
|
|
423 }
|
|
424
|
|
425 state->eosin = 0; /* clear it, because not all paths to here do */
|
|
426 while (!state->eosin) { /* We reached eos, not eof */
|
|
427 /* We copy the rest of the stream (other logical streams)
|
|
428 * through, a page at a time. */
|
|
429 while (1) {
|
|
430 result = ogg_sync_pageout(state->oy, &ogout);
|
|
431 if (result == 0)
|
|
432 break;
|
|
433 if (result < 0)
|
|
434 state->lasterror = "Corrupt or missing data, continuing...";
|
|
435 else {
|
|
436 /* Don't bother going through the rest, we can just
|
|
437 * write the page out now */
|
|
438 if (state->write(ogout.header, 1, ogout.header_len,
|
|
439 out) != (size_t) ogout.header_len)
|
|
440 goto cleanup;
|
|
441 if (state->write(ogout.body, 1, ogout.body_len, out) !=
|
|
442 (size_t) ogout.body_len)
|
|
443 goto cleanup;
|
|
444 }
|
|
445 }
|
|
446 buffer = ogg_sync_buffer(state->oy, CHUNKSIZE);
|
|
447 bytes = state->read(buffer, 1, CHUNKSIZE, state->in);
|
|
448 ogg_sync_wrote(state->oy, bytes);
|
|
449 if (bytes == 0) {
|
|
450 state->eosin = 1;
|
|
451 break;
|
|
452 }
|
|
453 }
|
|
454
|
|
455
|
|
456 cleanup:
|
|
457 ogg_stream_clear(&streamout);
|
|
458 ogg_packet_clear(&header_comments);
|
|
459
|
|
460 g_free(state->mainbuf);
|
|
461 g_free(state->bookbuf);
|
|
462
|
|
463 vcedit_clear_internals(state);
|
|
464 if (!state->eosin) {
|
|
465 state->lasterror =
|
|
466 "Error writing stream to output. "
|
|
467 "Output stream may be corrupted or truncated.";
|
|
468 return -1;
|
|
469 }
|
|
470
|
|
471 return 0;
|
|
472 }
|