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