1 /* ogg.c
2  * Copyright (C) 2004, 2005 Sylvain Cresto <scresto@gmail.com>
3  *
4  * This file is part of graveman!
5  *
6  * graveman! is free software; you can redistribute it and/or modify
7  * it under the terms of the GNU General Public License as published by
8  * the Free Software Foundation; either version 2, or
9  * (at your option) any later version.
10  *
11  * graveman! is distributed in the hope that it will be useful,
12  * but WITHOUT ANY WARRANTY; without even the implied warranty of
13  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
14  * General Public License for more details.
15  *
16  * You should have received a copy of the GNU General Public License
17  * along with program; see the file COPYING. If not, write to the
18  * Free Software Foundation, Inc., 59 Temple Place - Suite 330, Boston,
19  * MA 02111-1307, USA.
20  *
21  * URL: http://www.nongnu.org/graveman/
22  *
23  */
24 
25 #include "graveman.h"
26 
27 #ifdef ENABLE_OGG
28 #include <vorbis/codec.h>
29 
30 /* ce code est bas� sur ogginfo2.c, livr� avec les vorbis-tools-1.0
31  * en telechargement sur http://www.vorbis.com/download.psp */
32 
33 
34 
35 /* Ogginfo
36  *
37  * A tool to describe ogg file contents and metadata.
38  *
39  * Copyright 2002 Michael Smith <msmith@layrinth.net.au>
40  * Licensed under the GNU GPL, distributed with this program.
41  */
42 #define OGG_TITLE "TITLE"
43 #define OGG_ALBUM "ALBUM"
44 #define OGG_ARTIST "ARTIST"
45 
46 #define CHUNK 4500
47 
48 /* TODO:
49  *
50  * - detect decreasing granulepos
51  * - detect violations of muxing constraints
52  * - better EOS detection (when EOS not explicitly set)
53  * - detect granulepos 'gaps' (possibly vorbis-specific).
54  * - check for serial number == (unsigned)-1 (will break some tools?)
55  * - more options (e.g. less or more verbose)
56  */
57 
58 typedef struct _stream_processor {
59   void (*process_page)(struct _stream_processor *, ogg_page *);
60   void (*process_end)(struct _stream_processor *);
61   gint isillegal;
62   gint shownillegal;
63   gint isnew;
64   glong seqno;
65   gint lostseq;
66 
67   gint start;
68   gint end;
69 
70   gint num;
71   gchar *type;
72 
73   ogg_uint32_t serial; /* must be 32 bit unsigned */
74   ogg_stream_state os;
75   void *data;
76 } stream_processor;
77 
78 typedef struct {
79   stream_processor *streams;
80   gint allocated;
81   gint used;
82 
83   gint in_headers;
84 } stream_set;
85 
86 typedef struct {
87   glong bytes;
88   vorbis_comment vc;
89   vorbis_info vi;
90   ogg_int64_t lastgranulepos;
91 
92   gint doneheaders;
93 
94   gchar *title, *artist, *album; /* les informations qui nous interessent */
95 /*  glong minutes, seconds; */
96   guint32 length;
97 } misc_vorbis_info;
98 
create_stream_set(void)99 static stream_set *create_stream_set(void) {
100   stream_set *set = g_malloc0(sizeof(stream_set));
101 
102   set->streams = g_new0(stream_processor, 5);
103   set->allocated = 5;
104   set->used = 0;
105 
106   return set;
107 }
108 
vorbis_process(stream_processor * stream,ogg_page * page)109 static void vorbis_process(stream_processor *stream, ogg_page *page )
110 {
111   ogg_packet packet;
112   misc_vorbis_info *inf = stream->data;
113   gint i, header=0;
114 
115   ogg_stream_pagein(&stream->os, page);
116 
117   while(ogg_stream_packetout(&stream->os, &packet) > 0) {
118     if(inf->doneheaders < 3) {
119       if(vorbis_synthesis_headerin(&inf->vi, &inf->vc, &packet) < 0) {
120         g_warning(_("Warning: Could not decode vorbis header packet - invalid vorbis stream (%d)"), stream->num);
121         continue;
122       }
123       header = 1;
124       inf->doneheaders++;
125 
126       if(inf->doneheaders == 3) {
127         for(i=0; i < inf->vc.comments; i++) {
128           gchar *sep = strchr(inf->vc.user_comments[i], '=');
129 
130           /* extraction des commentaires */
131           if(sep == NULL) continue;
132 
133           *(sep++) = 0;
134           if (!g_ascii_strcasecmp(inf->vc.user_comments[i], OGG_TITLE)) {
135             inf->title = sc_realloc_cat(" ", inf->title, sep);
136           } else if (!g_ascii_strcasecmp(inf->vc.user_comments[i], OGG_ARTIST)) {
137             inf->artist = sc_realloc_cat(" ", inf->artist, sep);
138           } else if (!g_ascii_strcasecmp(inf->vc.user_comments[i], OGG_ALBUM)) {
139             inf->album = sc_realloc_cat(" ", inf->album, sep);
140           }
141         }
142       }
143     }
144   }
145 
146   if(!header) {
147     ogg_int64_t gp = ogg_page_granulepos(page);
148     if(gp > 0) {
149       inf->lastgranulepos = gp;
150     }
151     inf->bytes += page->header_len + page->body_len;
152   }
153 }
154 
vorbis_end(stream_processor * stream)155 static void vorbis_end(stream_processor *stream)
156 {
157   misc_vorbis_info *inf = stream->data;
158 
159   inf->length = (guint32)inf->lastgranulepos / inf->vi.rate;
160 
161   vorbis_comment_clear(&inf->vc);
162   vorbis_info_clear(&inf->vi);
163 }
164 
process_null(stream_processor * stream,ogg_page * page)165 static void process_null(stream_processor *stream, ogg_page *page)
166 {
167   /* This is for invalid streams. */
168 }
169 
free_stream_set(stream_set * set)170 static void free_stream_set(stream_set *set)
171 {
172   gint i;
173   for(i=0; i < set->used; i++) {
174     if(!set->streams[i].end) {
175       g_warning(_("Warning: EOS not set on stream %d"), set->streams[i].num);
176       if(set->streams[i].process_end)
177         set->streams[i].process_end(&set->streams[i]);
178     }
179     ogg_stream_clear(&set->streams[i].os);
180   }
181 
182   free(set->streams);
183   free(set);
184 }
185 
streams_open(stream_set * set)186 static int streams_open(stream_set *set)
187 {
188   gint i;
189   gint res=0;
190   for(i=0; i < set->used; i++) {
191     if(!set->streams[i].end)
192     res++;
193   }
194 
195   return res;
196 }
197 
null_start(stream_processor * stream)198 static void null_start(stream_processor *stream)
199 {
200   stream->process_end = NULL;
201   stream->type = "invalid";
202   stream->process_page = process_null;
203 }
204 
vorbis_start(stream_processor * stream,misc_vorbis_info * inf)205 static void vorbis_start(stream_processor *stream, misc_vorbis_info *inf)
206 {
207   stream->type = "vorbis";
208   stream->process_page = vorbis_process;
209   stream->process_end = vorbis_end;
210 
211   stream->data = inf;
212 
213   inf = stream->data;
214 
215   vorbis_comment_init(&inf->vc);
216   vorbis_info_init(&inf->vi);
217 }
218 
find_stream_processor(stream_set * set,ogg_page * page,misc_vorbis_info * inf)219 static stream_processor *find_stream_processor(stream_set *set, ogg_page *page, misc_vorbis_info *inf)
220 {
221   ogg_uint32_t serial = ogg_page_serialno(page);
222   gint i, found = 0;
223   gint invalid = 0;
224   stream_processor *stream;
225 
226   for(i=0; i < set->used; i++) {
227     if(serial == set->streams[i].serial) {
228       /* We have a match! */
229       found = 1;
230       stream = &(set->streams[i]);
231 
232       set->in_headers = 0;
233       /* if we have detected EOS, then this can't occur here. */
234       if(stream->end) {
235         stream->isillegal = 1;
236         return stream;
237       }
238 
239       stream->isnew = 0;
240       stream->start = ogg_page_bos(page);
241       stream->end = ogg_page_eos(page);
242       stream->serial = serial;
243       return stream;
244     }
245   }
246 
247   /* If there are streams open, and we've reached the end of the
248    * headers, then we can't be starting a new stream.
249    * XXX: might this sometimes catch ok streams if EOS flag is missing,
250    * but the stream is otherwise ok?
251    */
252   if(streams_open(set) && !set->in_headers)
253     invalid = 1;
254 
255   set->in_headers = 1;
256 
257   if(set->allocated < set->used) stream = &set->streams[set->used];
258   else {
259     set->allocated += 5;
260     set->streams = realloc(set->streams, sizeof(stream_processor)* set->allocated);
261     stream = &set->streams[set->used];
262   }
263   set->used++;
264   stream->num = set->used; /* We count from 1 */
265 
266   stream->isnew = 1;
267   stream->isillegal = invalid;
268 
269   {
270     gint res;
271     ogg_packet packet;
272 
273     /* We end up processing the header page twice, but that's ok. */
274     ogg_stream_init(&stream->os, serial);
275     ogg_stream_pagein(&stream->os, page);
276     res = ogg_stream_packetout(&stream->os, &packet);
277     if(res > 0 && packet.bytes >= 7 && memcmp(packet.packet, "\001vorbis", 7)==0) {
278       vorbis_start(stream, inf);
279     } else {
280       g_warning(_("Warning: no vorbis"));
281       null_start(stream);
282     }
283 
284     res = ogg_stream_packetout(&stream->os, &packet);
285     if(res > 0) {
286       g_warning(_("Warning: Invalid header page in stream %d, contains multiple packets"), stream->num);
287     }
288 
289     /* re-init, ready for processing */
290     ogg_stream_clear(&stream->os);
291     ogg_stream_init(&stream->os, serial);
292   }
293 
294   stream->start = ogg_page_bos(page);
295   stream->end = ogg_page_eos(page);
296   stream->serial = serial;
297 
298   return stream;
299 }
300 
get_next_page(FILE * f,ogg_sync_state * sync,ogg_page * page,ogg_int64_t * written)301 static gint get_next_page(FILE *f, ogg_sync_state *sync, ogg_page *page,
302         ogg_int64_t *written)
303 {
304   gint ret;
305   gchar *buffer;
306   gint bytes;
307 
308   while((ret = ogg_sync_pageout(sync, page)) <= 0) {
309     buffer = ogg_sync_buffer(sync, CHUNK);
310     bytes = fread(buffer, 1, CHUNK, f);
311     if(bytes <= 0) {
312       ogg_sync_wrote(sync, 0);
313       return 0;
314     }
315     ogg_sync_wrote(sync, bytes);
316     *written += bytes;
317   }
318 
319   return 1;
320 }
321 
getOggInfo(gchar * Apath,gchar ** Atitle,gchar ** Aalbum,gchar ** Aartist,guint32 * Alength,GError ** Aerror)322 gboolean getOggInfo(gchar *Apath, gchar **Atitle, gchar **Aalbum, gchar **Aartist, guint32 *Alength, GError **Aerror) {
323   FILE *Lfile = fopen(Apath, "rb");
324   ogg_sync_state sync;
325   ogg_page page;
326   stream_set *processors = create_stream_set();
327   gint gotpage = 0;
328   ogg_int64_t written = 0;
329   misc_vorbis_info inf;
330 
331   *(Atitle) = *(Aalbum) = *(Aartist) = NULL;
332   *(Alength) = 0;
333 
334   bzero(&inf, sizeof(misc_vorbis_info));
335 
336   if(!Lfile) {
337      g_set_error(Aerror, G_FILE_ERROR, g_file_error_from_errno(errno), "%s:%s\n%s",
338          _("Cannot read file"), Apath, strerror(errno));
339     return FALSE;
340   }
341 
342   ogg_sync_init(&sync);
343 
344   while(get_next_page(Lfile, &sync, &page, &written)) {
345     stream_processor *p = find_stream_processor(processors, &page, &inf);
346     gotpage = 1;
347     if(!p) {
348       g_set_error(Aerror, GRAVEMAN_ERROR, _ERR_INAPPROPRIATE_DATA, _("%s is not a valid .ogg file !"), Apath);
349       return FALSE;
350     }
351 
352     if(p->isillegal && !p->shownillegal) {
353       g_warning(_("Warning: illegally placed page(s) for logical stream %d\n"
354                   "This indicates a corrupt ogg file."), p->num);
355       p->shownillegal = 1;
356       continue;
357     }
358 
359     if(p->seqno++ != ogg_page_pageno(&page)) {
360       if(!p->lostseq)
361         g_warning(_("Warning: sequence number gap in stream %d. Got "
362                     "page %ld when expecting page %ld. Indicates missing data."),
363                     p->num, ogg_page_pageno(&page), p->seqno - 1);
364       p->seqno = ogg_page_pageno(&page);
365       p->lostseq = 1;
366     } else p->lostseq = 0;
367 
368     if(!p->isillegal) {
369       p->process_page(p, &page);
370       if(p->end) {
371         if(p->process_end) p->process_end(p);
372         p->isillegal = 1;
373       }
374     }
375   }
376 
377   free_stream_set(processors);
378 
379   ogg_sync_clear(&sync);
380 
381   fclose(Lfile);
382 
383   if (gotpage) {
384     *Atitle = g_strdup(inf.title ? inf.title : "");
385     *Aalbum = g_strdup(inf.album ? inf.album : "");
386     *Aartist = g_strdup(inf.artist ? inf.artist : "");
387 /*    *Alength = g_strdup_printf("%02ld:%02ld", inf.minutes, inf.seconds); */
388     *(Alength) = inf.length;
389   } else {
390     g_set_error(Aerror, GRAVEMAN_ERROR, _ERR_INAPPROPRIATE_DATA, _("%s is not a valid .ogg file !"), Apath);
391   }
392 
393   if (inf.title) g_free(inf.title);
394   if (inf.album) g_free(inf.album);
395   if (inf.artist) g_free(inf.artist);
396 
397   return gotpage ? TRUE : FALSE;
398 }
399 
400 #endif  /* ifdef ENABLE_OGG */
401 
402 /*
403  * vim:et:ts=8:sts=2:sw=2
404  */
405