view src/cue/cuesheet.c @ 2284:d19b53359b24

cleaned up the sndfile wav plugin, currently limiting it ONLY TO WAV PLAYBACK. if somebody is more experienced with it and wants to restore the other formats, go ahead (maybe change the name of the plugin too?).
author mf0102 <0102@gmx.at>
date Wed, 09 Jan 2008 15:41:22 +0100
parents c4b97a9c1f6b
children 62391135da44
line wrap: on
line source

/* Audacious: An advanced media player.
 * cuesheet.c: Support cuesheets as a media container.
 *
 * Copyright (C) 2006 William Pitcock <nenolod -at- nenolod.net>.
 *                    Jonathan Schleifer <js-audacious@webkeks.org> (few fixes)
 *
 * Copyright (C) 2007 Yoshiki Yazawa <yaz@cc.rim.or.jp> (millisecond
 * seek and multithreading)
 *
 * This file was hacked out of of xmms-cueinfo,
 * Copyright (C) 2003  Oskar Liljeblad
 *
 * This software is copyrighted work licensed under the terms of the
 * GNU General Public License. Please consult the file "COPYING" for
 * details.
 */

#include "config.h"

/* #define DEBUG 1 */

#include <string.h>
#include <stdlib.h>
#include <stdio.h>
#include <ctype.h>
#include <audacious/plugin.h>
#include <audacious/output.h>
#include <audacious/playlist.h>
#include <audacious/util.h>
#include <audacious/strings.h>
#include <audacious/main.h>

#define MAX_CUE_LINE_LENGTH 1000
#define MAX_CUE_TRACKS 1000

static void cache_cue_file(gchar *f);
static void free_cue_info(void);
static void fix_cue_argument(char *line);
static gboolean is_our_file(gchar *filespec);
static void play(InputPlayback *data);
static void play_cue_uri(InputPlayback *data, gchar *uri);
static void seek(InputPlayback *data, gint time);
static void stop(InputPlayback *data);
static void cue_pause(InputPlayback *data, short);
static Tuple *get_tuple(gchar *uri);
static Tuple *get_aud_tuple_uri(gchar *uri);
static void cue_init(void);
static void cue_cleanup(void);
static gpointer watchdog_func(gpointer data);

static GThread *watchdog_thread = NULL;
static GThread *play_thread = NULL;
static GThread *real_play_thread = NULL;

static GMutex *cue_mutex;
static GCond *cue_cond;
static enum {
    STOP,
    RUN,
    EXIT,
} watchdog_state;

static GMutex *cue_block_mutex;
static GCond *cue_block_cond;

static InputPlayback *caller_ip = NULL;

static gchar *cue_file = NULL;
static gchar *cue_title = NULL;
static gchar *cue_performer = NULL;
static gchar *cue_genre = NULL;
static gchar *cue_year = NULL;
static gchar *cue_track = NULL;

static gint last_cue_track = 0;
static gint cur_cue_track = 0;
static gint target_time = 0;
static GMutex *cue_target_time_mutex;

static struct {
	gchar *title;
	gchar *performer;
	gint index;
} cue_tracks[MAX_CUE_TRACKS];

static gint finetune_seek = 0;

static InputPlayback *real_ip = NULL;

InputPlugin cue_ip =
{
	.description = "Cuesheet Plugin",	/* description */
	.init = cue_init,	       	/* init */
	.is_our_file = is_our_file,
	.play_file = play,
	.stop = stop,
	.pause = cue_pause,
	.seek = seek,
	.cleanup = cue_cleanup,		/* cleanup */
	.get_song_tuple = get_tuple,
};

InputPlugin *cue_iplist[] = { &cue_ip, NULL };

DECLARE_PLUGIN(cue, NULL, NULL, cue_iplist, NULL, NULL, NULL, NULL, NULL);

static void cue_init(void)
{
    cue_mutex = g_mutex_new();
    cue_cond = g_cond_new();
    cue_block_mutex = g_mutex_new();
    cue_block_cond = g_cond_new();
    cue_target_time_mutex = g_mutex_new();

    /* create watchdog thread */
    g_mutex_lock(cue_mutex);
    watchdog_state = STOP;
    g_mutex_unlock(cue_mutex);
    watchdog_thread = g_thread_create(watchdog_func, NULL, TRUE, NULL);
#ifdef DEBUG
    g_print("watchdog_thread = %p\n", watchdog_thread);
#endif
    aud_uri_set_plugin("cue://", &cue_ip);
}

static void cue_cleanup(void)
{
    g_mutex_lock(cue_mutex);
    watchdog_state = EXIT;
    g_mutex_unlock(cue_mutex);
    g_cond_broadcast(cue_cond);

    g_thread_join(watchdog_thread);

    g_cond_free(cue_cond);
    g_mutex_free(cue_mutex);
    g_cond_free(cue_block_cond);
    g_mutex_free(cue_block_mutex);
    g_mutex_free(cue_target_time_mutex);
}

static int is_our_file(gchar *filename)
{
	gchar *ext;

	/* is it a cue:// URI? */
	if (!strncasecmp(filename, "cue://", 6))
		return TRUE;

	ext = strrchr(filename, '.');

	if(!ext)
		return FALSE;
	
	if (!strncasecmp(ext, ".cue", 4))
	{
		gint i;

		/* add the files, build cue urls, etc. */
		cache_cue_file(filename); //filename should be uri.

		for (i = 0; i < last_cue_track; i++)
		{
			gchar _buf[65535];

			g_snprintf(_buf, 65535, "cue://%s?%d", filename, i);
			aud_playlist_add_url(aud_playlist_get_active(), _buf);
		}

		free_cue_info();

		return -1;
	}

	return FALSE;
}

static void play(InputPlayback *data)
{
    gchar *uri = g_strdup(data->filename);

#ifdef DEBUG
    g_print("play: playback = %p\n", data);
    g_print("play: uri = %s\n", uri);
#endif

    caller_ip = data;
	/* this isn't a cue:// uri? */
	if (strncasecmp("cue://", uri, 6))
	{
		gchar *tmp = g_strdup_printf("cue://%s?0", uri);
		g_free(uri);
		uri = tmp;
	}
	play_thread = g_thread_self();
	data->set_pb_ready(data); // it should be done in real input plugin? --yaz
	play_cue_uri(data, uri);
	g_free(uri); uri = NULL;
}

static Tuple *get_tuple(gchar *uri)
{
	Tuple *ret;

	/* this isn't a cue:// uri? */
	if (strncasecmp("cue://", uri, 6))
	{
		gchar *tmp = g_strdup_printf("cue://%s?0", uri);
		ret = get_aud_tuple_uri(tmp);
		g_free(tmp);
		return ret;
	}

	return get_aud_tuple_uri(uri);
}

static void _aud_tuple_copy_field(Tuple *tuple, Tuple *tuple2, const gint nfield, const gchar *field)
{
    const gchar *str = aud_tuple_get_string(tuple, nfield, field);
    aud_tuple_disassociate(tuple2, nfield, field);
    aud_tuple_associate_string(tuple2, nfield, field, str);
}

static Tuple *get_aud_tuple_uri(gchar *uri)
{
    gchar *path2 = g_strdup(uri + 6);
    gchar *_path = strchr(path2, '?');
    gint track = 0;

	ProbeResult *pr;
	InputPlugin *dec;

	Tuple *phys_tuple, *out;

        if (_path != NULL && *_path == '?')
        {
                *_path = '\0';
                _path++;
                track = atoi(_path);
        }	

	cache_cue_file(path2); //path2 should be uri.

	if (cue_file == NULL)
		return NULL;
#ifdef DEBUG    
	g_print("cue_file = %s\n", cue_file);
#endif
	pr = aud_input_check_file(cue_file, FALSE);
	if (pr == NULL)
		return NULL;
	dec = pr->ip;
	if (dec == NULL)
		return NULL;

	if (dec->get_song_tuple)
		phys_tuple = dec->get_song_tuple(cue_file);

    if(!phys_tuple)
        return NULL;

    out = aud_tuple_new();

    _aud_tuple_copy_field(phys_tuple, out, FIELD_FILE_PATH, NULL);
    _aud_tuple_copy_field(phys_tuple, out, FIELD_FILE_NAME, NULL);
    _aud_tuple_copy_field(phys_tuple, out, FIELD_FILE_EXT, NULL);
    _aud_tuple_copy_field(phys_tuple, out, FIELD_CODEC, NULL);
    _aud_tuple_copy_field(phys_tuple, out, FIELD_QUALITY, NULL);
    _aud_tuple_copy_field(phys_tuple, out, FIELD_COPYRIGHT, NULL);
    _aud_tuple_copy_field(phys_tuple, out, FIELD_COMMENT, NULL);

    aud_tuple_associate_int(out, FIELD_LENGTH, NULL, aud_tuple_get_int(phys_tuple, FIELD_LENGTH, NULL));

    aud_tuple_free(phys_tuple);

    aud_tuple_associate_string(out, FIELD_TITLE, NULL, cue_tracks[track].title);
    aud_tuple_associate_string(out, FIELD_ARTIST, NULL, cue_tracks[track].performer ?
				  cue_tracks[track].performer : cue_performer);
    aud_tuple_associate_string(out, FIELD_ALBUM, NULL, cue_title);
    aud_tuple_associate_string(out, FIELD_GENRE, NULL, cue_genre);
    if(cue_year)
        aud_tuple_associate_int(out, FIELD_YEAR, NULL, atoi(cue_year));
    aud_tuple_associate_int(out, FIELD_TRACK_NUMBER, NULL, track + 1);

    return out;
}

static void seek(InputPlayback * data, gint time)
{
    g_mutex_lock(cue_target_time_mutex);
    target_time = time * 1000;
    g_mutex_unlock(cue_target_time_mutex);

#ifdef DEBUG
    g_print("seek: playback = %p\n", data);
    g_print("cue: seek: target_time = %d\n", target_time);
#endif

	if (real_ip != NULL) {
		real_ip->plugin->seek(real_ip, time);
    }
}

static void stop(InputPlayback * data)
{
#ifdef DEBUG
    g_print("f: stop: playback = %p\n", data);
#endif

    if(play_thread) {
        if(real_play_thread) {
            g_cond_signal(cue_block_cond); /* kick play_cue_uri */

            if (real_ip != NULL)
                real_ip->plugin->stop(real_ip);
#ifdef DEBUG
            g_print("i: stop(real_ip) finished\n");
#endif
            real_play_thread = NULL;

            if (data != NULL)
                data->playing = 0;
            if (caller_ip != NULL)
                caller_ip->playing = 0;

            g_mutex_lock(cue_mutex);
            watchdog_state = STOP;
            g_mutex_unlock(cue_mutex);
            g_cond_signal(cue_cond);

            free_cue_info();

            if (real_ip != NULL) {
                real_ip->plugin->set_info = cue_ip.set_info;
                real_ip->plugin->output = NULL;
                g_free(real_ip);
                real_ip = NULL;
            }
        } /* real_play_thread */

        g_thread_join(play_thread);
        play_thread = NULL;

    } /*play_thread*/


#ifdef DEBUG
    g_print("e: stop\n");
#endif
}

static gboolean do_stop(gpointer data)
{
#ifdef DEBUG
    g_print("f: do_stop\n");
#endif

    audacious_drct_stop();

#ifdef DEBUG
    g_print("e: do_stop\n");
#endif
    return FALSE; //one-shot
}

static gboolean do_setpos(gpointer data)
{
    Playlist *playlist = aud_playlist_get_active();
    gint pos = aud_playlist_get_position_nolock(playlist);
    gint incr = *(gint *)data;

    pos = pos + incr;
    if(pos < 0)
        pos = 0;

#ifdef DEBUG
    g_print("do_setpos: pos = %d\n\n", pos);
#endif

    if (!playlist)
        return FALSE;

    /* being done from the main loop thread, does not require locks */
    aud_playlist_set_position(playlist, (guint)pos);

    return FALSE; //one-shot
}

static void cue_pause(InputPlayback * data, short p)
{
	if (real_ip != NULL)
		real_ip->plugin->pause(real_ip, p);
}

static void set_info_override(gchar * unused, gint length, gint rate, gint freq, gint nch)
{
	gchar *title;
	Playlist *playlist = aud_playlist_get_active();

	g_return_if_fail(playlist != NULL);

	/* annoying. */
	if (playlist->position->tuple == NULL)
	{
		gint pos = aud_playlist_get_position(playlist);
		aud_playlist_get_tuple(playlist, pos);
	}

	title = g_strdup(playlist->position->title);

	cue_ip.set_info(title, length, rate, freq, nch);
}

static void play_cue_uri(InputPlayback * data, gchar *uri)
{
    gchar *path2 = g_strdup(uri + 6); // "cue://" is stripped.
    gchar *_path = strchr(path2, '?');
	gint track = 0;
	ProbeResult *pr;
	InputPlugin *real_ip_plugin;
    Tuple *tuple = NULL;

#ifdef DEBUG
    g_print("f: play_cue_uri\n");
    g_print("play_cue_uri: playback = %p\n", data);
    g_print("play_cue_uri: path2 = %s\n", path2);
#endif

    /* stop watchdog thread */
    g_mutex_lock(cue_mutex);
    watchdog_state = STOP;
    g_mutex_unlock(cue_mutex);
    g_cond_signal(cue_cond);

    if (_path != NULL && *_path == '?')
    {
        *_path = '\0';
        _path++;
        track = atoi(_path);
    }	
	cur_cue_track = track;
	cache_cue_file(path2); //path2 should be uri.

    if (cue_file == NULL || !aud_vfs_file_test(cue_file, G_FILE_TEST_EXISTS))
        return;

	pr = aud_input_check_file(cue_file, FALSE);
	if (pr == NULL)
		return;

	real_ip_plugin = pr->ip;

	if (real_ip_plugin != NULL)
	{
		if (real_ip)
			g_free(real_ip);

		/* duplicate original playback and modify */
		real_ip = (InputPlayback *)g_memdup(data, sizeof(InputPlayback));
		real_ip->plugin = real_ip_plugin;
		real_ip->plugin->set_info = set_info_override;
		real_ip->plugin->output = cue_ip.output;
		real_ip->filename = cue_file;

		data->playing = 1;

		real_play_thread = g_thread_create((GThreadFunc)(real_ip->plugin->play_file), (gpointer)real_ip, TRUE, NULL);
		g_usleep(50000); // wait for 50msec while real input plugin is initializing.

		if(real_ip->plugin->mseek) {
#ifdef DEBUG
            g_print("mseek\n");
#endif
			real_ip->plugin->mseek(real_ip, finetune_seek ? finetune_seek : cue_tracks[track].index);
		}
		else
			real_ip->plugin->seek(real_ip, finetune_seek ? finetune_seek / 1000 : cue_tracks[track].index / 1000 + 1);

        g_mutex_lock(cue_target_time_mutex);
        target_time = finetune_seek ? finetune_seek : cue_tracks[track].index;
        g_mutex_unlock(cue_target_time_mutex);
#ifdef DEBUG
        g_print("cue: play_cue_uri: target_time = %d\n", target_time);
#endif

        tuple = real_ip->plugin->get_song_tuple(cue_file);
        if(tuple) {
            cue_tracks[last_cue_track].index = aud_tuple_get_int(tuple, FIELD_LENGTH, NULL);
            aud_tuple_free(tuple); tuple = NULL;
        }

        /* kick watchdog thread */
        g_mutex_lock(cue_mutex);
        watchdog_state = RUN;
        g_mutex_unlock(cue_mutex);
        g_cond_signal(cue_cond);
#ifdef DEBUG
        g_print("watchdog activated\n");
#endif
        finetune_seek = 0;
        if(real_play_thread) {
            g_mutex_lock(cue_block_mutex);
            g_cond_wait(cue_block_cond, cue_block_mutex); // block until stop() is called.
            g_mutex_unlock(cue_block_mutex);            
        }
	}

#ifdef DEBUG
    g_print("e: play_cue_uri\n");
#endif
}

/******************************************************* watchdog */

/*
 * This is fairly hard to explain.
 *
 * Basically we loop until we have reached the correct track.
 * Then we set a finetune adjustment to make sure we stay in the
 * right place.
 *
 * I used to recurse here (it was prettier), but that didn't work
 * as well as I was hoping.
 *
 * Anyhow, yeah. The logic here isn't great, but it works, so I'm
 * cool with it.
 *
 *     - nenolod
 */
static gpointer watchdog_func(gpointer data)
{
    gint time = 0;
    Playlist *playlist = NULL;
    GTimeVal sleep_time;

#ifdef DEBUG
    g_print("f: watchdog\n");
#endif

    while(1) {
#if 0
#if DEBUG
        g_print("time = %d cur = %d cidx = %d nidx = %d last = %d\n",
                time, cur_cue_track,
                cue_tracks[cur_cue_track].index,
                cue_tracks[cur_cue_track+1].index, last_cue_track);
#endif
#endif
        g_get_current_time(&sleep_time);
        g_time_val_add(&sleep_time, 10000); // interval is 10msec.

        g_mutex_lock(cue_mutex);
        switch(watchdog_state) {
        case EXIT:
#ifdef DEBUG
            g_print("e: watchdog exit\n");
#endif
            g_mutex_unlock(cue_mutex); // stop() will lock cue_mutex.
            stop(real_ip); // need not to care about real_ip != NULL here.
            g_thread_exit(NULL);
            break;
        case RUN:
            if(!playlist)
                playlist = aud_playlist_get_active();
            g_cond_timed_wait(cue_cond, cue_mutex, &sleep_time);
            break;
        case STOP:
#ifdef DEBUG
            g_print("watchdog deactivated\n");
#endif
            g_cond_wait(cue_cond, cue_mutex);
            playlist = aud_playlist_get_active();
            break;
        }
        g_mutex_unlock(cue_mutex);

        if(watchdog_state != RUN)
            continue;

        time = audacious_drct_get_output_time();
#if 0
#ifdef DEBUG
        g_print("time = %d target_time = %d\n", time, target_time);
#endif
#endif
        if(time == 0 || time <= target_time)
            continue;

        // prev track
        if (time < cue_tracks[cur_cue_track].index)
        {
            static gint incr = 0;
            gint oldpos = cur_cue_track;
#ifdef DEBUG
            g_print("i: watchdog prev\n");
            g_print("time = %d cur = %d cidx = %d nidx = %d\n", time, cur_cue_track,
                    cue_tracks[cur_cue_track].index,
                    cue_tracks[cur_cue_track+1].index);
#endif
            while(time < cue_tracks[cur_cue_track].index) {
                cur_cue_track--;
                incr = cur_cue_track - oldpos; // relative position
                if (time >= cue_tracks[cur_cue_track].index)
                    finetune_seek = time;
#ifdef DEBUG
                g_print("cue: prev_track: time = %d cue_tracks[cur_cue_track].index = %d\n",
                       time, cue_tracks[cur_cue_track].index);
                g_print("cue: prev_track: finetune_seek = %d\n", finetune_seek);
#endif
            }

            g_mutex_lock(cue_target_time_mutex);
            target_time = finetune_seek ? finetune_seek : cue_tracks[cur_cue_track].index;
            g_mutex_unlock(cue_target_time_mutex);
#ifdef DEBUG
            g_print("cue: prev_track: target_time = %d\n", target_time);
#endif
            g_idle_add_full(G_PRIORITY_HIGH , do_setpos, &incr, NULL);
            continue;
        }

        // next track
        if (cur_cue_track + 1 < last_cue_track && time > cue_tracks[cur_cue_track + 1].index)
        {
            static gint incr = 0;
            gint oldpos = cur_cue_track;
#ifdef DEBUG
            g_print("i: watchdog next\n");
            g_print("time = %d cur = %d cidx = %d nidx = %d last = %d lidx = %d\n", time, cur_cue_track,
                    cue_tracks[cur_cue_track].index,
                    cue_tracks[cur_cue_track+1].index,
                    last_cue_track, cue_tracks[last_cue_track].index);
#endif
            while(time > cue_tracks[cur_cue_track + 1].index) {
                cur_cue_track++;
                incr = cur_cue_track - oldpos; // relative position
                if (time >= cue_tracks[cur_cue_track].index)
                    finetune_seek = time;
#ifdef DEBUG
                g_print("cue: next_track: time = %d cue_tracks[cur_cue_track].index = %d\n",
                       time, cue_tracks[cur_cue_track].index);
                g_print("cue: next_track: finetune_seek = %d\n", finetune_seek);
#endif
            }

            g_mutex_lock(cue_target_time_mutex);
            target_time = finetune_seek ? finetune_seek : cue_tracks[cur_cue_track].index;
            g_mutex_unlock(cue_target_time_mutex);
#ifdef DEBUG
            g_print("cue: next_track: target_time = %d\n", target_time);
#endif
            if(aud_cfg->stopaftersong) {
                g_idle_add_full(G_PRIORITY_HIGH, do_stop, (void *)real_ip, NULL);
                continue;
            }
            else {
                g_idle_add_full(G_PRIORITY_HIGH , do_setpos, &incr, NULL);
                continue;
            }
        }

        // last track
        if (cur_cue_track + 1 == last_cue_track &&
            (cue_tracks[last_cue_track].index - time < 500 ||
             time > cue_tracks[last_cue_track].index) ){ // may not happen. for safety.
            if(!real_ip->output->buffer_playing()) {
                gint pos = aud_playlist_get_position(playlist);
                if (pos + 1 == aud_playlist_get_length(playlist)) {
#ifdef DEBUG
                    g_print("i: watchdog eof reached\n\n");
#endif
                    if(aud_cfg->repeat) {
                        static gint incr = 0;
                        incr = -pos;
                        g_idle_add_full(G_PRIORITY_HIGH , do_setpos, &incr, NULL);
                        continue;
                    }
                    else {
                        g_idle_add_full(G_PRIORITY_HIGH, do_stop, (void *)real_ip, NULL);
                        continue;
                    }
                }
                else {
                    if(aud_cfg->stopaftersong) {
                        g_idle_add_full(G_PRIORITY_HIGH, do_stop, (void *)real_ip, NULL);
                        continue;
                    }
#ifdef DEBUG
                    g_print("i: watchdog end of cue, advance in playlist\n\n");
#endif
                    static gint incr = 1;
                    g_idle_add_full(G_PRIORITY_HIGH , do_setpos, &incr, NULL);
                    continue;
                }
            }
        }
    }
#ifdef DEBUG
    g_print("e: watchdog\n");
#endif
    return NULL; // dummy.
}

/******************************************************** cuefile */

static void free_cue_info(void)
{
	g_free(cue_file);	cue_file = NULL;
	g_free(cue_title);	cue_title = NULL;
	g_free(cue_performer);	cue_performer = NULL;
	g_free(cue_genre);	cue_genre = NULL;
	g_free(cue_year); 	cue_year = NULL;
	g_free(cue_track);	cue_track = NULL;

	for (; last_cue_track > 0; last_cue_track--) {
		g_free(cue_tracks[last_cue_track-1].performer);
		cue_tracks[last_cue_track-1].performer = NULL;
		g_free(cue_tracks[last_cue_track-1].title);
		cue_tracks[last_cue_track-1].title = NULL;
	}
#ifdef DEBUG
	g_print("free_cue_info: last_cue_track = %d\n", last_cue_track);
#endif
	last_cue_track = 0;
}

static void cache_cue_file(char *f)
{
	VFSFile *file = aud_vfs_fopen(f, "rb");
	gchar line[MAX_CUE_LINE_LENGTH+1];

	if(!file)
		return;
	
	while (TRUE) {
		gint p;
		gint q;

		if (aud_vfs_fgets(line, MAX_CUE_LINE_LENGTH+1, file) == NULL) {
			aud_vfs_fclose(file);
			return;
        }

		for (p = 0; line[p] && isspace((int) line[p]); p++);
		if (!line[p])
			continue;
		for (q = p; line[q] && !isspace((int) line[q]); q++);
		if (!line[q])
			continue;
		line[q] = '\0';
		for (q++; line[q] && isspace((int) line[q]); q++);
		if (strcasecmp(line+p, "REM") == 0) {
			for (p = q; line[p] && !isspace((int) line[p]); p++);
			if (!line[p])
				continue;
			line[p] = '\0';
			for (p++; line[p] && isspace((int) line[p]); p++);
			if(strcasecmp(line+q, "GENRE") == 0) {
				fix_cue_argument(line+p);
				if (last_cue_track == 0)
					cue_genre = aud_str_to_utf8(line + p);
			}
			if(strcasecmp(line+q, "DATE") == 0) {
				gchar *tmp;
				fix_cue_argument(line+p);
				if (last_cue_track == 0) {
					tmp = g_strdup(line + p);
					if (tmp) {
						cue_year = strtok(tmp, "/"); // XXX: always in the same format?
					}
				}
			}
		}
		else if (strcasecmp(line+p, "PERFORMER") == 0) {
			fix_cue_argument(line+q);

			if (last_cue_track == 0)
				cue_performer = aud_str_to_utf8(line + q);
			else
				cue_tracks[last_cue_track - 1].performer = aud_str_to_utf8(line + q);
		}
		else if (strcasecmp(line+p, "FILE") == 0) {
			gchar *tmp = g_path_get_dirname(f);
			fix_cue_argument(line+q);
			cue_file = g_strdup_printf("%s/%s", tmp, line+q); //XXX need to check encoding?
			g_free(tmp);
		}
		else if (strcasecmp(line+p, "TITLE") == 0) {
			fix_cue_argument(line+q);
			if (last_cue_track == 0)
				cue_title = aud_str_to_utf8(line + q);
			else
				cue_tracks[last_cue_track-1].title = aud_str_to_utf8(line + q);
		}
		else if (strcasecmp(line+p, "TRACK") == 0) {
			gint track;

			fix_cue_argument(line+q);
			for (p = q; line[p] && isdigit((int) line[p]); p++);
			line[p] = '\0';
			for (; line[q] && line[q] == '0'; q++);
			if (!line[q])
				continue;
			track = atoi(line+q);
			if (track >= MAX_CUE_TRACKS)
				continue;
			last_cue_track = track;
			cue_tracks[last_cue_track-1].index = 0;
			cue_tracks[last_cue_track-1].performer = NULL;
			cue_tracks[last_cue_track-1].title = NULL;
		}
		else if (strcasecmp(line+p, "INDEX") == 0) {
            gint min, sec, frac;
			for (p = q; line[p] && !isspace((int) line[p]); p++);
			if (!line[p])
				continue;
			for (p++; line[p] && isspace((int) line[p]); p++);
			for (q = p; line[q] && !isspace((int) line[q]); q++);

            if(sscanf(line+p, "%d:%d:%d", &min, &sec, &frac) == 3) {
//                printf("%3d:%02d:%02d\n", min, sec, frac);
                cue_tracks[last_cue_track-1].index = min * 60000 + sec * 1000 + frac * 10;
            }
        }
    }

	aud_vfs_fclose(file);
}

static void fix_cue_argument(char *line)
{
	if (line[0] == '"') {
		gchar *l2;
		for (l2 = line+1; *l2 && *l2 != '"'; l2++)
				*(l2-1) = *l2;
			*(l2-1) = *l2;
		for (; *line && *line != '"'; line++) {
			if (*line == '\\' && *(line+1)) {
				for (l2 = line+1; *l2 && *l2 != '"'; l2++)
					*(l2-1) = *l2;
				*(l2-1) = *l2;
			}
		}
		*line = '\0';
	}
	else {
		for (; *line && *line != '\r' && *line != '\n'; line++);
		*line = '\0';
	}
}