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