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 27typedef 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 48typedef struct { 49 ogg_stream_processor *streams; 50 int allocated; 51 int used; 52 53 int in_headers; 54} ogg_stream_set; 55 56typedef 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 70static ogg_stream_set * 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 82static void 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_DEBUG, L_SCANNER, "Vorbis headers parsed for stream %d, " 110 "information follows...\n", stream->num); 111 DPRINTF(E_DEBUG, L_SCANNER, "Channels: %d\n", inf->vi.channels); 112 DPRINTF(E_DEBUG, 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_DEBUG, 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_DEBUG, 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_DEBUG, 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_DEBUG, L_SCANNER, "Upper bitrate not set\n"); 142 } 143 144 if(inf->vi.bitrate_lower > 0) 145 { 146 DPRINTF(E_DEBUG, 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_DEBUG, 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_DEBUG, 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 if(gp < inf->lastgranulepos) 183 DPRINTF(E_WARN, L_SCANNER, "granulepos in stream %d decreases from %lld to %lld", 184 stream->num, inf->lastgranulepos, gp); 185 inf->lastgranulepos = gp; 186 } 187 else 188 { 189 DPRINTF(E_WARN, L_SCANNER, "Malformed vorbis strem.\n"); 190 } 191 inf->bytes += page->header_len + page->body_len; 192 } 193} 194 195static void 196_ogg_vorbis_end(ogg_stream_processor *stream, struct song_metadata *psong) 197{ 198 ogg_misc_vorbis_info *inf = stream->data; 199 double bitrate, time; 200 201 time = (double)inf->lastgranulepos / inf->vi.rate; 202 bitrate = inf->bytes * 8 / time / 1000; 203 204 if(psong != NULL) 205 { 206 if(psong->bitrate <= 0) 207 { 208 psong->bitrate = bitrate * 1000; 209 } 210 psong->song_length = time * 1000; 211 } 212 213 vorbis_comment_clear(&inf->vc); 214 vorbis_info_clear(&inf->vi); 215 216 free(stream->data); 217} 218 219static void 220_ogg_process_null(ogg_stream_processor *stream, ogg_page *page, struct song_metadata *psong) 221{ 222 // invalid stream 223} 224 225static void 226_ogg_process_other(ogg_stream_processor *stream, ogg_page *page, struct song_metadata *psong) 227{ 228 ogg_stream_pagein(&stream->os, page); 229} 230 231static void 232_ogg_free_stream_set(ogg_stream_set *set) 233{ 234 int i; 235 236 for(i = 0; i < set->used; i++) 237 { 238 if(!set->streams[i].end) 239 { 240 // no EOS 241 if(set->streams[i].process_end) 242 set->streams[i].process_end(&set->streams[i], NULL); 243 } 244 ogg_stream_clear(&set->streams[i].os); 245 } 246 247 free(set->streams); 248 free(set); 249} 250 251static int 252_ogg_streams_open(ogg_stream_set *set) 253{ 254 int i; 255 int res = 0; 256 257 for(i = 0; i < set->used; i++) 258 { 259 if(!set->streams[i].end) 260 res++; 261 } 262 263 return res; 264} 265 266static void 267_ogg_null_start(ogg_stream_processor *stream) 268{ 269 stream->process_end = NULL; 270 stream->type = "invalid"; 271 stream->process_page = _ogg_process_null; 272} 273 274static void 275_ogg_other_start(ogg_stream_processor *stream, char *type) 276{ 277 if(type) 278 stream->type = type; 279 else 280 stream->type = "unknown"; 281 stream->process_page = _ogg_process_other; 282 stream->process_end = NULL; 283} 284 285static void 286_ogg_vorbis_start(ogg_stream_processor *stream) 287{ 288 ogg_misc_vorbis_info *info; 289 290 stream->type = "vorbis"; 291 stream->process_page = _ogg_vorbis_process; 292 stream->process_end = _ogg_vorbis_end; 293 294 stream->data = calloc(1, sizeof(ogg_misc_vorbis_info)); 295 296 info = stream->data; 297 298 vorbis_comment_init(&info->vc); 299 vorbis_info_init(&info->vi); 300} 301 302static ogg_stream_processor * 303_ogg_find_stream_processor(ogg_stream_set *set, ogg_page *page) 304{ 305 ogg_uint32_t serial = ogg_page_serialno(page); 306 int i; 307 int invalid = 0; 308 int constraint = 0; 309 ogg_stream_processor *stream; 310 311 for(i = 0; i < set->used; i++) 312 { 313 if(serial == set->streams[i].serial) 314 { 315 stream = &(set->streams[i]); 316 317 set->in_headers = 0; 318 319 if(stream->end) 320 { 321 stream->isillegal = 1; 322 stream->constraint_violated = CONSTRAINT_PAGE_AFTER_EOS; 323 return stream; 324 } 325 326 stream->isnew = 0; 327 stream->start = ogg_page_bos(page); 328 stream->end = ogg_page_eos(page); 329 stream->serial = serial; 330 return stream; 331 } 332 } 333 if(_ogg_streams_open(set) && !set->in_headers) 334 { 335 constraint = CONSTRAINT_MUXING_VIOLATED; 336 invalid = 1; 337 } 338 339 set->in_headers = 1; 340 341 if(set->allocated < set->used) 342 stream = &set->streams[set->used]; 343 else 344 { 345 set->allocated += 5; 346 set->streams = realloc(set->streams, sizeof(ogg_stream_processor) * set->allocated); 347 stream = &set->streams[set->used]; 348 } 349 set->used++; 350 stream->num = set->used; // count from 1 351 352 stream->isnew = 1; 353 stream->isillegal = invalid; 354 stream->constraint_violated = constraint; 355 356 { 357 int res; 358 ogg_packet packet; 359 360 ogg_stream_init(&stream->os, serial); 361 ogg_stream_pagein(&stream->os, page); 362 res = ogg_stream_packetout(&stream->os, &packet); 363 if(res <= 0) 364 { 365 DPRINTF(E_WARN, L_SCANNER, "Invalid header page, no packet found\n"); 366 _ogg_null_start(stream); 367 } 368 else if(packet.bytes >= 7 && memcmp(packet.packet, "\001vorbis", 7) == 0) 369 _ogg_vorbis_start(stream); 370 else if(packet.bytes >= 8 && memcmp(packet.packet, "OggMIDI\0", 8) == 0) 371 _ogg_other_start(stream, "MIDI"); 372 else 373 _ogg_other_start(stream, NULL); 374 375 res = ogg_stream_packetout(&stream->os, &packet); 376 if(res > 0) 377 { 378 DPRINTF(E_WARN, L_SCANNER, "Invalid header page in stream %d, " 379 "contains multiple packets\n", stream->num); 380 } 381 382 /* re-init, ready for processing */ 383 ogg_stream_clear(&stream->os); 384 ogg_stream_init(&stream->os, serial); 385 } 386 387 stream->start = ogg_page_bos(page); 388 stream->end = ogg_page_eos(page); 389 stream->serial = serial; 390 391 return stream; 392} 393 394static int 395_ogg_get_next_page(FILE *f, ogg_sync_state *sync, ogg_page *page, 396 ogg_int64_t *written) 397{ 398 int ret; 399 char *buffer; 400 int bytes; 401 402 while((ret = ogg_sync_pageout(sync, page)) <= 0) 403 { 404 if(ret < 0) 405 DPRINTF(E_WARN, L_SCANNER, "Hole in data found at approximate offset %lld bytes. Corrupted ogg.\n", *written); 406 407 buffer = ogg_sync_buffer(sync, 4500); // chunk=4500 408 bytes = fread(buffer, 1, 4500, f); 409 if(bytes <= 0) 410 { 411 ogg_sync_wrote(sync, 0); 412 return 0; 413 } 414 ogg_sync_wrote(sync, bytes); 415 *written += bytes; 416 } 417 418 return 1; 419} 420 421 422static int 423_get_oggfileinfo(char *filename, struct song_metadata *psong) 424{ 425 FILE *file = fopen(filename, "rb"); 426 ogg_sync_state sync; 427 ogg_page page; 428 ogg_stream_set *processors = _ogg_create_stream_set(); 429 int gotpage = 0; 430 ogg_int64_t written = 0; 431 432 if(!file) 433 { 434 DPRINTF(E_FATAL, L_SCANNER, 435 "Error opening input file \"%s\": %s\n", filename, strerror(errno)); 436 _ogg_free_stream_set(processors); 437 return -1; 438 } 439 440 DPRINTF(E_INFO, L_SCANNER, "Processing file \"%s\"...\n\n", filename); 441 442 ogg_sync_init(&sync); 443 444 while(_ogg_get_next_page(file, &sync, &page, &written)) 445 { 446 ogg_stream_processor *p = _ogg_find_stream_processor(processors, &page); 447 gotpage = 1; 448 449 if(!p) 450 { 451 DPRINTF(E_FATAL, L_SCANNER, "Could not find a processor for stream, bailing\n"); 452 _ogg_free_stream_set(processors); 453 fclose(file); 454 return -1; 455 } 456 457 if(p->isillegal && !p->shownillegal) 458 { 459 char *constraint; 460 switch(p->constraint_violated) 461 { 462 case CONSTRAINT_PAGE_AFTER_EOS: 463 constraint = "Page found for stream after EOS flag"; 464 break; 465 case CONSTRAINT_MUXING_VIOLATED: 466 constraint = "Ogg muxing constraints violated, new " 467 "stream before EOS of all previous streams"; 468 break; 469 default: 470 constraint = "Error unknown."; 471 } 472 473 DPRINTF(E_WARN, L_SCANNER, 474 "Warning: illegally placed page(s) for logical stream %d\n" 475 "This indicates a corrupt ogg file: %s.\n", 476 p->num, constraint); 477 p->shownillegal = 1; 478 479 if(!p->isnew) 480 continue; 481 } 482 483 if(p->isnew) 484 { 485 DPRINTF(E_DEBUG, L_SCANNER, "New logical stream (#%d, serial: %08x): type %s\n", 486 p->num, p->serial, p->type); 487 if(!p->start) 488 DPRINTF(E_WARN, L_SCANNER, 489 "stream start flag not set on stream %d\n", 490 p->num); 491 } 492 else if(p->start) 493 DPRINTF(E_WARN, L_SCANNER, "stream start flag found in mid-stream " 494 "on stream %d\n", p->num); 495 496 if(p->seqno++ != ogg_page_pageno(&page)) 497 { 498 if(!p->lostseq) 499 DPRINTF(E_WARN, L_SCANNER, 500 "sequence number gap in stream %d. Got page %ld " 501 "when expecting page %ld. Indicates missing data.\n", 502 p->num, ogg_page_pageno(&page), p->seqno - 1); 503 p->seqno = ogg_page_pageno(&page); 504 p->lostseq = 1; 505 } 506 else 507 p->lostseq = 0; 508 509 if(!p->isillegal) 510 { 511 p->process_page(p, &page, psong); 512 513 if(p->end) 514 { 515 if(p->process_end) 516 p->process_end(p, psong); 517 DPRINTF(E_DEBUG, L_SCANNER, "Logical stream %d ended\n", p->num); 518 p->isillegal = 1; 519 p->constraint_violated = CONSTRAINT_PAGE_AFTER_EOS; 520 } 521 } 522 } 523 524 _ogg_free_stream_set(processors); 525 526 ogg_sync_clear(&sync); 527 528 fclose(file); 529 530 if(!gotpage) 531 { 532 DPRINTF(E_ERROR, L_SCANNER, "No ogg data found in file \"%s\".\n", filename); 533 return -1; 534 } 535 536 return 0; 537} 538