1 //=========================================================================
2 // FILENAME : tagutils-ogg.c
3 // DESCRIPTION : Ogg metadata reader
4 //=========================================================================
5 // Copyright (c) 2008- NETGEAR, Inc. All Rights Reserved.
6 //=========================================================================
7
8 /*
9 * This program is free software; you can redistribute it and/or modify
10 * it under the terms of the GNU General Public License as published by
11 * the Free Software Foundation; either version 2 of the License, or
12 * (at your option) any later version.
13 *
14 * This program is distributed in the hope that it will be useful,
15 * but WITHOUT ANY WARRANTY; without even the implied warranty of
16 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
17 * GNU General Public License for more details.
18 *
19 * You should have received a copy of the GNU General Public License
20 * along with this program. If not, see <http://www.gnu.org/licenses/>.
21 */
22
23 /*
24 * This file is derived from mt-daap project.
25 */
26
27 typedef struct _ogg_stream_processor {
28 void (*process_page)(struct _ogg_stream_processor *, ogg_page *, struct song_metadata *);
29 void (*process_end)(struct _ogg_stream_processor *, struct song_metadata *);
30 int isillegal;
31 int constraint_violated;
32 int shownillegal;
33 int isnew;
34 long seqno;
35 int lostseq;
36
37 int start;
38 int end;
39
40 int num;
41 char *type;
42
43 ogg_uint32_t serial;
44 ogg_stream_state os;
45 void *data;
46 } ogg_stream_processor;
47
48 typedef struct {
49 ogg_stream_processor *streams;
50 int allocated;
51 int used;
52
53 int in_headers;
54 } ogg_stream_set;
55
56 typedef struct {
57 vorbis_info vi;
58 vorbis_comment vc;
59
60 ogg_int64_t bytes;
61 ogg_int64_t lastgranulepos;
62 ogg_int64_t firstgranulepos;
63
64 int doneheaders;
65 } ogg_misc_vorbis_info;
66
67 #define CONSTRAINT_PAGE_AFTER_EOS 1
68 #define CONSTRAINT_MUXING_VIOLATED 2
69
70 static ogg_stream_set *
_ogg_create_stream_set(void)71 _ogg_create_stream_set(void)
72 {
73 ogg_stream_set *set = calloc(1, sizeof(ogg_stream_set));
74
75 set->streams = calloc(5, sizeof(ogg_stream_processor));
76 set->allocated = 5;
77 set->used = 0;
78
79 return set;
80 }
81
82 static void
_ogg_vorbis_process(ogg_stream_processor * stream,ogg_page * page,struct song_metadata * psong)83 _ogg_vorbis_process(ogg_stream_processor *stream, ogg_page *page,
84 struct song_metadata *psong)
85 {
86 ogg_packet packet;
87 ogg_misc_vorbis_info *inf = stream->data;
88 int i, header = 0;
89
90 ogg_stream_pagein(&stream->os, page);
91 if(inf->doneheaders < 3)
92 header = 1;
93
94 while(ogg_stream_packetout(&stream->os, &packet) > 0)
95 {
96 if(inf->doneheaders < 3)
97 {
98 if(vorbis_synthesis_headerin(&inf->vi, &inf->vc, &packet) < 0)
99 {
100 DPRINTF(E_WARN, L_SCANNER, "Could not decode vorbis header "
101 "packet - invalid vorbis stream (%d)\n", stream->num);
102 continue;
103 }
104 inf->doneheaders++;
105 if(inf->doneheaders == 3)
106 {
107 if(ogg_page_granulepos(page) != 0 || ogg_stream_packetpeek(&stream->os, NULL) == 1)
108 DPRINTF(E_WARN, L_SCANNER, "No header in vorbis stream %d\n", stream->num);
109 DPRINTF(E_MAXDEBUG, L_SCANNER, "Vorbis headers parsed for stream %d, "
110 "information follows...\n", stream->num);
111 DPRINTF(E_MAXDEBUG, L_SCANNER, "Channels: %d\n", inf->vi.channels);
112 DPRINTF(E_MAXDEBUG, L_SCANNER, "Rate: %ld\n\n", inf->vi.rate);
113
114 psong->samplerate = inf->vi.rate;
115 psong->channels = inf->vi.channels;
116
117 if(inf->vi.bitrate_nominal > 0)
118 {
119 DPRINTF(E_MAXDEBUG, L_SCANNER, "Nominal bitrate: %f kb/s\n",
120 (double)inf->vi.bitrate_nominal / 1000.0);
121 psong->bitrate = inf->vi.bitrate_nominal / 1000;
122 }
123 else
124 {
125 int upper_rate, lower_rate;
126
127 DPRINTF(E_MAXDEBUG, L_SCANNER, "Nominal bitrate not set\n");
128
129 //
130 upper_rate = 0;
131 lower_rate = 0;
132
133 if(inf->vi.bitrate_upper > 0)
134 {
135 DPRINTF(E_MAXDEBUG, L_SCANNER, "Upper bitrate: %f kb/s\n",
136 (double)inf->vi.bitrate_upper / 1000.0);
137 upper_rate = inf->vi.bitrate_upper;
138 }
139 else
140 {
141 DPRINTF(E_MAXDEBUG, L_SCANNER, "Upper bitrate not set\n");
142 }
143
144 if(inf->vi.bitrate_lower > 0)
145 {
146 DPRINTF(E_MAXDEBUG, L_SCANNER, "Lower bitrate: %f kb/s\n",
147 (double)inf->vi.bitrate_lower / 1000.0);
148 lower_rate = inf->vi.bitrate_lower;;
149 }
150 else
151 {
152 DPRINTF(E_MAXDEBUG, L_SCANNER, "Lower bitrate not set\n");
153 }
154
155 if(upper_rate && lower_rate)
156 {
157 psong->bitrate = (upper_rate + lower_rate) / 2;
158 }
159 else
160 {
161 psong->bitrate = upper_rate + lower_rate;
162 }
163 }
164
165 if(inf->vc.comments > 0)
166 DPRINTF(E_MAXDEBUG, L_SCANNER,
167 "User comments section follows...\n");
168
169 for(i = 0; i < inf->vc.comments; i++)
170 {
171 vc_scan(psong, inf->vc.user_comments[i], inf->vc.comment_lengths[i]);
172 }
173 }
174 }
175 }
176
177 if(!header)
178 {
179 ogg_int64_t gp = ogg_page_granulepos(page);
180 if(gp > 0)
181 {
182 inf->lastgranulepos = gp;
183 }
184 else
185 {
186 DPRINTF(E_WARN, L_SCANNER, "Malformed vorbis strem.\n");
187 }
188 inf->bytes += page->header_len + page->body_len;
189 }
190 }
191
192 static void
_ogg_vorbis_end(ogg_stream_processor * stream,struct song_metadata * psong)193 _ogg_vorbis_end(ogg_stream_processor *stream, struct song_metadata *psong)
194 {
195 ogg_misc_vorbis_info *inf = stream->data;
196 double bitrate, time;
197
198 time = (double)inf->lastgranulepos / inf->vi.rate;
199 bitrate = inf->bytes * 8 / time / 1000;
200
201 if(psong != NULL)
202 {
203 if(psong->bitrate <= 0)
204 {
205 psong->bitrate = bitrate * 1000;
206 }
207 psong->song_length = time * 1000;
208 }
209
210 vorbis_comment_clear(&inf->vc);
211 vorbis_info_clear(&inf->vi);
212
213 free(stream->data);
214 }
215
216 static void
_ogg_process_null(ogg_stream_processor * stream,ogg_page * page,struct song_metadata * psong)217 _ogg_process_null(ogg_stream_processor *stream, ogg_page *page, struct song_metadata *psong)
218 {
219 // invalid stream
220 }
221
222 static void
_ogg_process_other(ogg_stream_processor * stream,ogg_page * page,struct song_metadata * psong)223 _ogg_process_other(ogg_stream_processor *stream, ogg_page *page, struct song_metadata *psong)
224 {
225 ogg_stream_pagein(&stream->os, page);
226 }
227
228 static void
_ogg_free_stream_set(ogg_stream_set * set)229 _ogg_free_stream_set(ogg_stream_set *set)
230 {
231 int i;
232
233 for(i = 0; i < set->used; i++)
234 {
235 if(!set->streams[i].end)
236 {
237 // no EOS
238 if(set->streams[i].process_end)
239 set->streams[i].process_end(&set->streams[i], NULL);
240 }
241 ogg_stream_clear(&set->streams[i].os);
242 }
243
244 free(set->streams);
245 free(set);
246 }
247
248 static int
_ogg_streams_open(ogg_stream_set * set)249 _ogg_streams_open(ogg_stream_set *set)
250 {
251 int i;
252 int res = 0;
253
254 for(i = 0; i < set->used; i++)
255 {
256 if(!set->streams[i].end)
257 res++;
258 }
259
260 return res;
261 }
262
263 static void
_ogg_null_start(ogg_stream_processor * stream)264 _ogg_null_start(ogg_stream_processor *stream)
265 {
266 stream->process_end = NULL;
267 stream->type = "invalid";
268 stream->process_page = _ogg_process_null;
269 }
270
271 static void
_ogg_other_start(ogg_stream_processor * stream,char * type)272 _ogg_other_start(ogg_stream_processor *stream, char *type)
273 {
274 if(type)
275 stream->type = type;
276 else
277 stream->type = "unknown";
278 stream->process_page = _ogg_process_other;
279 stream->process_end = NULL;
280 }
281
282 static void
_ogg_vorbis_start(ogg_stream_processor * stream)283 _ogg_vorbis_start(ogg_stream_processor *stream)
284 {
285 ogg_misc_vorbis_info *info;
286
287 stream->type = "vorbis";
288 stream->process_page = _ogg_vorbis_process;
289 stream->process_end = _ogg_vorbis_end;
290
291 stream->data = calloc(1, sizeof(ogg_misc_vorbis_info));
292
293 info = stream->data;
294
295 vorbis_comment_init(&info->vc);
296 vorbis_info_init(&info->vi);
297 }
298
299 static ogg_stream_processor *
_ogg_find_stream_processor(ogg_stream_set * set,ogg_page * page)300 _ogg_find_stream_processor(ogg_stream_set *set, ogg_page *page)
301 {
302 ogg_uint32_t serial = ogg_page_serialno(page);
303 int i;
304 int invalid = 0;
305 int constraint = 0;
306 ogg_stream_processor *stream;
307
308 for(i = 0; i < set->used; i++)
309 {
310 if(serial == set->streams[i].serial)
311 {
312 stream = &(set->streams[i]);
313
314 set->in_headers = 0;
315
316 if(stream->end)
317 {
318 stream->isillegal = 1;
319 stream->constraint_violated = CONSTRAINT_PAGE_AFTER_EOS;
320 return stream;
321 }
322
323 stream->isnew = 0;
324 stream->start = ogg_page_bos(page);
325 stream->end = ogg_page_eos(page);
326 stream->serial = serial;
327 return stream;
328 }
329 }
330 if(_ogg_streams_open(set) && !set->in_headers)
331 {
332 constraint = CONSTRAINT_MUXING_VIOLATED;
333 invalid = 1;
334 }
335
336 set->in_headers = 1;
337
338 if(set->allocated < set->used)
339 stream = &set->streams[set->used];
340 else
341 {
342 set->allocated += 5;
343 set->streams = realloc(set->streams, sizeof(ogg_stream_processor) * set->allocated);
344 stream = &set->streams[set->used];
345 }
346 set->used++;
347 stream->num = set->used; // count from 1
348
349 stream->isnew = 1;
350 stream->isillegal = invalid;
351 stream->constraint_violated = constraint;
352
353 {
354 int res;
355 ogg_packet packet;
356
357 ogg_stream_init(&stream->os, serial);
358 ogg_stream_pagein(&stream->os, page);
359 res = ogg_stream_packetout(&stream->os, &packet);
360 if(res <= 0)
361 {
362 DPRINTF(E_WARN, L_SCANNER, "Invalid header page, no packet found\n");
363 _ogg_null_start(stream);
364 }
365 else if(packet.bytes >= 7 && memcmp(packet.packet, "\001vorbis", 7) == 0)
366 _ogg_vorbis_start(stream);
367 else if(packet.bytes >= 8 && memcmp(packet.packet, "OggMIDI\0", 8) == 0)
368 _ogg_other_start(stream, "MIDI");
369 else
370 _ogg_other_start(stream, NULL);
371
372 res = ogg_stream_packetout(&stream->os, &packet);
373 if(res > 0)
374 {
375 DPRINTF(E_WARN, L_SCANNER, "Invalid header page in stream %d, "
376 "contains multiple packets\n", stream->num);
377 }
378
379 /* re-init, ready for processing */
380 ogg_stream_clear(&stream->os);
381 ogg_stream_init(&stream->os, serial);
382 }
383
384 stream->start = ogg_page_bos(page);
385 stream->end = ogg_page_eos(page);
386 stream->serial = serial;
387
388 return stream;
389 }
390
391 static int
_ogg_get_next_page(FILE * f,ogg_sync_state * sync,ogg_page * page,ogg_int64_t * written)392 _ogg_get_next_page(FILE *f, ogg_sync_state *sync, ogg_page *page,
393 ogg_int64_t *written)
394 {
395 int ret;
396 char *buffer;
397 int bytes;
398
399 while((ret = ogg_sync_pageout(sync, page)) <= 0)
400 {
401 if(ret < 0)
402 DPRINTF(E_WARN, L_SCANNER, "Hole in data found at approximate offset %lld bytes. Corrupted ogg.\n",
403 (long long)*written);
404
405 buffer = ogg_sync_buffer(sync, 4500); // chunk=4500
406 bytes = fread(buffer, 1, 4500, f);
407 if(bytes <= 0)
408 {
409 ogg_sync_wrote(sync, 0);
410 return 0;
411 }
412 ogg_sync_wrote(sync, bytes);
413 *written += bytes;
414 }
415
416 return 1;
417 }
418
419
420 static int
_get_oggfileinfo(char * filename,struct song_metadata * psong)421 _get_oggfileinfo(char *filename, struct song_metadata *psong)
422 {
423 FILE *file = fopen(filename, "rb");
424 ogg_sync_state sync;
425 ogg_page page;
426 ogg_stream_set *processors = _ogg_create_stream_set();
427 int gotpage = 0;
428 ogg_int64_t written = 0;
429
430 if(!file)
431 {
432 DPRINTF(E_FATAL, L_SCANNER,
433 "Error opening input file \"%s\": %s\n", filename, strerror(errno));
434 _ogg_free_stream_set(processors);
435 return -1;
436 }
437
438 DPRINTF(E_MAXDEBUG, L_SCANNER, "Processing file \"%s\"...\n", filename);
439
440 ogg_sync_init(&sync);
441
442 while(_ogg_get_next_page(file, &sync, &page, &written))
443 {
444 ogg_stream_processor *p = _ogg_find_stream_processor(processors, &page);
445 gotpage = 1;
446
447 if(!p)
448 {
449 DPRINTF(E_FATAL, L_SCANNER, "Could not find a processor for stream, bailing\n");
450 _ogg_free_stream_set(processors);
451 fclose(file);
452 return -1;
453 }
454
455 if(p->isillegal && !p->shownillegal)
456 {
457 char *constraint;
458 switch(p->constraint_violated)
459 {
460 case CONSTRAINT_PAGE_AFTER_EOS:
461 constraint = "Page found for stream after EOS flag";
462 break;
463 case CONSTRAINT_MUXING_VIOLATED:
464 constraint = "Ogg muxing constraints violated, new "
465 "stream before EOS of all previous streams";
466 break;
467 default:
468 constraint = "Error unknown.";
469 }
470
471 DPRINTF(E_WARN, L_SCANNER,
472 "Warning: illegally placed page(s) for logical stream %d\n"
473 "This indicates a corrupt ogg file: %s.\n",
474 p->num, constraint);
475 p->shownillegal = 1;
476
477 if(!p->isnew)
478 continue;
479 }
480
481 if(p->isnew)
482 {
483 DPRINTF(E_MAXDEBUG, L_SCANNER, "New logical stream (#%d, serial: %08x): type %s\n",
484 p->num, p->serial, p->type);
485 if(!p->start)
486 DPRINTF(E_WARN, L_SCANNER,
487 "stream start flag not set on stream %d\n",
488 p->num);
489 }
490 else if(p->start)
491 DPRINTF(E_WARN, L_SCANNER, "stream start flag found in mid-stream "
492 "on stream %d\n", p->num);
493
494 if(p->seqno++ != ogg_page_pageno(&page))
495 {
496 if(!p->lostseq)
497 DPRINTF(E_WARN, L_SCANNER,
498 "sequence number gap in stream %d. Got page %ld "
499 "when expecting page %ld. Indicates missing data.\n",
500 p->num, ogg_page_pageno(&page), p->seqno - 1);
501 p->seqno = ogg_page_pageno(&page);
502 p->lostseq = 1;
503 }
504 else
505 p->lostseq = 0;
506
507 if(!p->isillegal)
508 {
509 p->process_page(p, &page, psong);
510
511 if(p->end)
512 {
513 if(p->process_end)
514 p->process_end(p, psong);
515 DPRINTF(E_MAXDEBUG, L_SCANNER, "Logical stream %d ended\n", p->num);
516 p->isillegal = 1;
517 p->constraint_violated = CONSTRAINT_PAGE_AFTER_EOS;
518 }
519 }
520 }
521
522 _ogg_free_stream_set(processors);
523
524 ogg_sync_clear(&sync);
525
526 fclose(file);
527
528 if(!gotpage)
529 {
530 DPRINTF(E_ERROR, L_SCANNER, "No ogg data found in file \"%s\".\n", filename);
531 return -1;
532 }
533
534 return 0;
535 }
536