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_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
192static void
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
216static void
217_ogg_process_null(ogg_stream_processor *stream, ogg_page *page, struct song_metadata *psong)
218{
219	// invalid stream
220}
221
222static void
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
228static void
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
248static int
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
263static void
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
271static void
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
282static void
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
299static ogg_stream_processor *
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
391static int
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
420static int
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\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