* @brief
*
* ogg file parser.
+ *
+ * Reference:
+ * http://xiph.org/ogg/doc/libogg/decoding.html
+ * http://xiph.org/vorbis/doc/libvorbis/overview.html
*/
#include <lightmediascanner_plugin.h>
#include <lightmediascanner_utils.h>
#include <shared/util.h>
+#include <assert.h>
+#include <errno.h>
#include <stdlib.h>
#include <string.h>
#include "lms_ogg_private.h"
+#ifndef CHUNKSIZE
+int CHUNKSIZE = 4096;
+#endif
+
+#define MAX_CHUNKS_PER_PAGE 10
+
+struct stream {
+ struct lms_stream base;
+ int serial;
+ int remain_headers;
+ ogg_stream_state *os;
+ union {
+ struct {
+ vorbis_comment vc;
+ vorbis_info vi;
+ } audio;
+ };
+};
+
+struct ogg_info {
+ struct lms_string_size title;
+ struct lms_string_size artist;
+ struct lms_string_size album;
+ struct lms_string_size genre;
+ enum lms_stream_type type;
+ unsigned char trackno;
+ unsigned int channels;
+ unsigned int sampling_rate;
+ unsigned int bitrate;
+
+ struct stream *streams;
+};
+
static const struct lms_string_size _container = LMS_STATIC_STRING_SIZE("ogg");
-static const struct lms_string_size _codec = LMS_STATIC_STRING_SIZE("vorbis");
+static const struct lms_string_size _audio_codec =
+ LMS_STATIC_STRING_SIZE("vorbis");
static long int
_id3_tag_size(FILE *file)
return 0L;
}
-
-static int
-_get_vorbis_headers(FILE *file, vorbis_info *vi, vorbis_comment *vc)
+static void
+_set_lms_info(struct lms_string_size *info, const char *tag)
{
- lms_ogg_buffer_t buffer = NULL;
- int bytes = 0, i = 0, chunks = 0;
- ogg_packet header;
+ int size;
- ogg_page og;
- ogg_sync_state *osync = NULL;
- ogg_stream_state *os = NULL;
+ if (!info || !tag)
+ return;
- int serial = 0;
-#ifndef CHUNKSIZE
- int CHUNKSIZE = 4096;
-#endif
- int nheaders = 0;
- int ret = -1;
+ size = strlen(tag);
- /* Initialize stuff */
- memset(&header, 0, sizeof(ogg_packet));
- memset(&og, 0, sizeof(ogg_page));
+ if (!size)
+ return;
- osync = lms_create_ogg_sync();
+ info->len = size;
+ info->str = malloc(size * sizeof(char));
+ memcpy(info->str, tag, size);
+ lms_string_size_strip_and_free(info);
+}
+
+static bool _ogg_read_page(FILE *fp, ogg_sync_state *osync, ogg_page *page)
+{
+ int i;
- vorbis_info_init(vi);
- vorbis_comment_init(vc);
+ for (i = 0; i < MAX_CHUNKS_PER_PAGE && ogg_sync_pageout(osync, page) != 1;
+ i++) {
+ lms_ogg_buffer_t buffer = lms_get_ogg_sync_buffer(osync, CHUNKSIZE);
+ int bytes = fread(buffer, 1, CHUNKSIZE, fp);
- while (1) {
- buffer = lms_get_ogg_sync_buffer(osync, CHUNKSIZE);
- bytes = fread(buffer, 1, CHUNKSIZE, file);
+ /* EOF */
+ if (bytes == 0)
+ return false;
ogg_sync_wrote(osync, bytes);
+ }
- if (ogg_sync_pageout(osync, &og) == 1)
- break;
+ if (i > MAX_CHUNKS_PER_PAGE)
+ return false;
- if (chunks++ >= 10)
- goto end;
- }
+ return true;
+}
- serial = ogg_page_serialno(&og);
- os = lms_create_ogg_stream(serial);
-
- if (ogg_stream_pagein(os, &og) < 0 ||
- ogg_stream_packetout(os, &header) != 1 ||
- vorbis_synthesis_headerin(vi, vc, &header) != 0)
- goto end;
-
- i = 1;
- nheaders = 3;
- while (i < nheaders) {
- while (i < nheaders) {
- int result = ogg_sync_pageout(osync, &og);
- if (result == 0)
- break;
- else if (result == 1) {
- ogg_stream_pagein(os, &og);
- while (i < nheaders) {
- result = ogg_stream_packetout(os, &header);
- if (result == 0)
- break;
- if (result == -1)
- goto end;
-
- vorbis_synthesis_headerin(vi, vc, &header);
- i++;
- }
- }
- }
+static struct stream *_stream_new(int serial, int id)
+{
+ struct stream *s;
- buffer = lms_get_ogg_sync_buffer(osync, CHUNKSIZE);
- bytes = fread(buffer, 1, CHUNKSIZE, file);
+ s = calloc(1, sizeof(*s));
+ if (!s)
+ return NULL;
- if (bytes == 0 && i < 2)
- goto end;
+ s->serial = serial;
+ s->os = lms_create_ogg_stream(serial);
- ogg_sync_wrote(osync, bytes);
- }
+ s->base.type = LMS_STREAM_TYPE_UNKNOWN;
+ s->base.stream_id = id;
- ret = 0;
+ return s;
+}
-end:
- if (ret) {
- vorbis_comment_clear(vc);
- vorbis_info_clear(vi);
+static void _stream_free(struct stream *s)
+{
+ switch (s->base.type) {
+ case LMS_STREAM_TYPE_UNKNOWN:
+ case LMS_STREAM_TYPE_VIDEO:
+ case LMS_STREAM_TYPE_SUBTITLE:
+ break;
+ case LMS_STREAM_TYPE_AUDIO:
+ vorbis_comment_clear(&s->audio.vc);
+ vorbis_info_clear(&s->audio.vi);
+ break;
}
- if (os)
- lms_destroy_ogg_stream(os);
+ lms_destroy_ogg_stream(s->os);
+ free(s);
+}
+
+static struct stream *_info_find_stream(struct ogg_info *info, int serial)
+{
+ struct stream *s;
- if (osync)
- lms_destroy_ogg_sync(osync);
+ for (s = info->streams; s; s = (struct stream *) s->base.next) {
+ if (s->serial == serial)
+ return s;
+ }
- return ret;
+ return NULL;
}
-static void
-_set_lms_info(struct lms_string_size *info, const char *tag)
+static struct stream *_info_prepend_stream(struct ogg_info *info, int serial,
+ int id)
{
- int size;
+ struct stream *s = _stream_new(serial, id);
+ if (!s)
+ return NULL;
+ s->base.next = (struct lms_stream *) info->streams;
+ info->streams = s;
+ return s;
+}
- if (!info || !tag)
- return;
+static int _stream_handle_page(struct stream *s, ogg_page *page)
+{
+ ogg_packet packet;
+ int r;
- size = strlen(tag);
+ if (!s->os)
+ s->os = lms_create_ogg_stream(s->serial);
- if (!size)
- return;
+ if (ogg_stream_pagein(s->os, page) < 0)
+ return -1;
- info->len = size;
- info->str = malloc(size * sizeof(char));
- memcpy(info->str, tag, size);
- lms_string_size_strip_and_free(info);
+ do {
+ r = ogg_stream_packetout(s->os, &packet);
+ if (r == 0)
+ return 0;
+ if (r == -1)
+ return -1;
+
+ switch (s->base.type) {
+ case LMS_STREAM_TYPE_UNKNOWN:
+ vorbis_info_init(&s->audio.vi);
+ vorbis_comment_init(&s->audio.vc);
+ r = vorbis_synthesis_headerin(&s->audio.vi, &s->audio.vc, &packet);
+ if (r != 0)
+ return -1;
+
+ s->base.type = LMS_STREAM_TYPE_AUDIO;
+ s->remain_headers = 2;
+ break;
+ case LMS_STREAM_TYPE_AUDIO:
+ assert(s->remain_headers > 0);
+ r = vorbis_synthesis_headerin(&s->audio.vi, &s->audio.vc, &packet);
+ if (r != 0)
+ return -1;
+
+ s->remain_headers--;
+ if (!s->remain_headers)
+ return 0;
+ break;
+ default:
+ return 0;
+ }
+ } while (1);
+
+ return 0;
}
-static int
-_parse_ogg(const char *filename, struct lms_audio_info *info)
+static void _parse_vorbis_stream(struct ogg_info *info, struct stream *s)
{
- vorbis_comment vc;
- vorbis_info vi;
- FILE *file;
const char *tag;
+ info->channels = s->audio.vi.channels;
+ info->sampling_rate = s->audio.vi.rate;
+ info->bitrate = s->audio.vi.bitrate_nominal;
+
+ tag = vorbis_comment_query(&s->audio.vc, "TITLE", 0);
+ _set_lms_info(&info->title, tag);
+
+ tag = vorbis_comment_query(&s->audio.vc, "ARTIST", 0);
+ _set_lms_info(&info->artist, tag);
+
+ tag = vorbis_comment_query(&s->audio.vc, "ALBUM", 0);
+ _set_lms_info(&info->album, tag);
+
+ tag = vorbis_comment_query(&s->audio.vc, "GENRE", 0);
+ _set_lms_info(&info->genre, tag);
+
+ tag = vorbis_comment_query(&s->audio.vc, "TRACKNUMBER", 0);
+ if (tag)
+ info->trackno = atoi(tag);
+}
+
+static int _parse_ogg(const char *filename, struct ogg_info *info)
+{
+ FILE *fp;
+ ogg_page page;
+ ogg_sync_state *osync;
+ int r;
+ /* no numeration in the protocol, start arbitrarily from 1 */
+ int id = 0;
+ /* the 1st audio stream, the one used if audio */
+ struct stream *s, *audio_stream = NULL;
+
if (!filename)
return -1;
- file = fopen(filename, "rb");
- if (file == NULL)
+ fp = fopen(filename, "rb");
+ if (fp == NULL)
return -1;
- fseek(file, _id3_tag_size(file), SEEK_SET);
+ /* Skip ID3 on the beginning */
+ fseek(fp, _id3_tag_size(fp), SEEK_SET);
- if (_get_vorbis_headers(file, &vi, &vc) != 0)
- return -1;
+ osync = lms_create_ogg_sync();
+ while (_ogg_read_page(fp, osync, &page)) {
+ int serial = ogg_page_serialno(&page);
- tag = vorbis_comment_query(&vc, "TITLE", 0);
- _set_lms_info(&info->title, tag);
+ s = _info_find_stream(info, serial);
- tag = vorbis_comment_query(&vc, "ARTIST", 0);
- _set_lms_info(&info->artist, tag);
+ /* A new page for a stream that has all the headers already: stop
+ * parsing */
+ if (s && s->remain_headers == 0)
+ break;
- tag = vorbis_comment_query(&vc, "ALBUM", 0);
- _set_lms_info(&info->album, tag);
+ if (!s) {
+ /* We didn't find the stream, but we are not at its start page
+ * neither: it's an unknown stream, go to the next one */
+ if (!ogg_page_bos(&page))
+ continue;
- tag = vorbis_comment_query(&vc, "TRACKNUMBER", 0);
- if (tag)
- info->trackno = atoi(tag);
+ s = _info_prepend_stream(info, serial, ++id);
+ if (!s) {
+ r = -ENOMEM;
+ goto done;
+ }
+ }
- tag = vorbis_comment_query(&vc, "GENRE", 0);
- _set_lms_info(&info->genre, tag);
+ r = _stream_handle_page(s, &page);
+ if (r < 0)
+ goto done;
+
+ if (s->remain_headers == 0 && !audio_stream &&
+ s->base.type == LMS_STREAM_TYPE_AUDIO) {
+ info->type = s->base.type;
+ audio_stream = s;
+ }
+ }
- info->container = _container;
- info->codec = _codec;
+ if (audio_stream)
+ _parse_vorbis_stream(info, audio_stream);
- info->channels = vi.channels;
- info->sampling_rate = vi.rate;
- info->bitrate = vi.bitrate_nominal;
+ while (info->streams) {
+ s = info->streams;
+ info->streams = (struct stream *) s->base.next;
+ _stream_free(s);
+ }
- fclose(file);
- vorbis_comment_clear(&vc);
- vorbis_info_clear(&vi);
+done:
+ lms_destroy_ogg_sync(osync);
+ fclose(fp);
- return 0;
+ return r;
}
}
static int
-_parse(struct plugin *plugin, struct lms_context *ctxt, const struct lms_file_info *finfo, void *match)
+_parse(struct plugin *plugin, struct lms_context *ctxt,
+ const struct lms_file_info *finfo, void *match)
{
- struct lms_audio_info info = { };
+ struct ogg_info info = { .type = LMS_STREAM_TYPE_UNKNOWN };
int r;
r = _parse_ogg(finfo->path, &info);
&_exts[((long) match) - 1],
NULL);
if (info.title.str)
- lms_charset_conv(ctxt->cs_conv, &info.title.str, &info.title.len);
+ lms_charset_conv(ctxt->cs_conv, &info.title.str, &info.title.len);
if (info.artist.str)
- lms_charset_conv(ctxt->cs_conv, &info.artist.str, &info.artist.len);
- if (info.album.str)
- lms_charset_conv(ctxt->cs_conv, &info.album.str, &info.album.len);
- if (info.genre.str)
- lms_charset_conv(ctxt->cs_conv, &info.genre.str, &info.genre.len);
-
- info.id = finfo->id;
- r = lms_db_audio_add(plugin->audio_db, &info);
+ lms_charset_conv(ctxt->cs_conv, &info.artist.str, &info.artist.len);
+
+ if (info.type == LMS_STREAM_TYPE_AUDIO) {
+ struct lms_audio_info audio_info = { };
+
+ if (info.album.str)
+ lms_charset_conv(ctxt->cs_conv, &info.album.str, &info.album.len);
+ if (info.genre.str)
+ lms_charset_conv(ctxt->cs_conv, &info.genre.str, &info.genre.len);
+
+ audio_info.id = finfo->id;
+ audio_info.title = info.title;
+ audio_info.artist = info.artist;
+ audio_info.album = info.album;
+ audio_info.genre = info.genre;
+
+ audio_info.trackno = info.trackno;
+ audio_info.container = _container;
+ audio_info.codec = _audio_codec;
+ audio_info.channels = info.channels;
+ audio_info.sampling_rate = info.sampling_rate;
+ audio_info.bitrate = info.bitrate;
+
+ r = lms_db_audio_add(plugin->audio_db, &audio_info);
+ }
- done:
+done:
free(info.title.str);
free(info.artist.str);
free(info.album.str);