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