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