view src/xmms-sid.c @ 688:ba851baac5a0

Etc.
author Matti Hamalainen <ccr@tnsp.org>
date Thu, 22 May 2008 07:26:26 +0300
parents f22a708d29fd
children 4b0189df58ee
line wrap: on
line source

/*
   XMMS-SID - SIDPlay input plugin for X MultiMedia System (XMMS)

   Main source file

   Programmed and designed by Matti 'ccr' Hamalainen <ccr@tnsp.org>
   (C) Copyright 1999-2007 Tecnic Software productions (TNSP)

   This program is free software; you can redistribute it and/or modify
   it under the terms of the GNU General Public License as published by
   the Free Software Foundation; either version 2 of the License, or
   (at your option) any later version.

   This program is distributed in the hope that it will be useful,
   but WITHOUT ANY WARRANTY; without even the implied warranty of
   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
   GNU General Public License for more details.

   You should have received a copy of the GNU General Public License along along
   with this program; if not, write to the Free Software Foundation, Inc.,
   51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
*/
#include "xmms-sid.h"

#ifdef HAVE_STDLIB_H
#include <stdlib.h>
#endif

#include <stdarg.h>
#include <gdk/gdkkeysyms.h>
#include <gtk/gtk.h>

#include "xs_config.h"
#include "xs_length.h"
#include "xs_stil.h"
#include "xs_title.h"
#include "xs_filter.h"
#include "xs_fileinfo.h"
#include "xs_interface.h"
#include "xs_glade.h"
#include "xs_player.h"
#include "xs_slsup.h"


/*
 * Include player engines
 */
#ifdef HAVE_SIDPLAY1
#include "xs_sidplay1.h"
#endif
#ifdef HAVE_SIDPLAY2
#include "xs_sidplay2.h"
#endif


/*
 * List of players and links to their functions
 */
static const xs_player_t xs_playerlist[] = {
#ifdef HAVE_SIDPLAY1
    {XS_ENG_SIDPLAY1,
     xs_sidplay1_probe,
     xs_sidplay1_init, xs_sidplay1_close,
     xs_sidplay1_initsong, xs_sidplay1_fillbuffer,
     xs_sidplay1_load, xs_sidplay1_delete,
     xs_sidplay1_getinfo, xs_sidplay1_updateinfo,
     NULL
    },
#endif
#ifdef HAVE_SIDPLAY2
    {XS_ENG_SIDPLAY2,
     xs_sidplay2_probe,
     xs_sidplay2_init, xs_sidplay2_close,
     xs_sidplay2_initsong, xs_sidplay2_fillbuffer,
     xs_sidplay2_load, xs_sidplay2_delete,
     xs_sidplay2_getinfo, xs_sidplay2_updateinfo,
     xs_sidplay2_flush
    },
#endif
};

const gint xs_nplayerlist = (sizeof(xs_playerlist) / sizeof(xs_playerlist[0]));


/*
 * Global variables
 */
xs_status_t xs_status;
XS_MUTEX(xs_status);
static XS_THREAD_T xs_decode_thread;

static GtkWidget *xs_subctrl = NULL;
static GtkObject *xs_subctrl_adj = NULL;
XS_MUTEX(xs_subctrl);

void        xs_subctrl_close(void);
void        xs_subctrl_update(void);


/*
 * Error messages
 */
void xs_error(const char *fmt, ...)
{
    va_list ap;
    fprintf(stderr, "XMMS-SID: ");
    va_start(ap, fmt);
    vfprintf(stderr, fmt, ap);
    va_end(ap);
}

#ifndef DEBUG_NP
void XSDEBUG(const char *fmt, ...)
{
#ifdef DEBUG
    va_list ap;
    fprintf(stderr, "XSDEBUG: ");
    va_start(ap, fmt);
    vfprintf(stderr, fmt, ap);
    va_end(ap);
#endif
}
#endif


/*
 * Initialization functions
 */
void xs_reinit(void)
{
    gint player;
    gboolean initialized;

    /* Stop playing, if we are */
    XS_MUTEX_LOCK(xs_status);
    if (xs_status.isPlaying) {
        XS_MUTEX_UNLOCK(xs_status);
        xs_stop();
    } else {
        XS_MUTEX_UNLOCK(xs_status);
    }

    XS_MUTEX_LOCK(xs_status);
    XS_MUTEX_LOCK(xs_cfg);

    /* Initialize status and sanitize configuration */
    memset(&xs_status, 0, sizeof(xs_status));

    if (xs_cfg.audioFrequency < 8000)
        xs_cfg.audioFrequency = 8000;

    if (xs_cfg.oversampleFactor < XS_MIN_OVERSAMPLE)
        xs_cfg.oversampleFactor = XS_MIN_OVERSAMPLE;
    else if (xs_cfg.oversampleFactor > XS_MAX_OVERSAMPLE)
        xs_cfg.oversampleFactor = XS_MAX_OVERSAMPLE;

    if (xs_cfg.audioChannels != XS_CHN_MONO)
        xs_cfg.oversampleEnable = FALSE;

    xs_status.audioFrequency = xs_cfg.audioFrequency;
    xs_status.audioBitsPerSample = xs_cfg.audioBitsPerSample;
    xs_status.audioChannels = xs_cfg.audioChannels;
    xs_status.audioFormat = -1;
    xs_status.oversampleEnable = xs_cfg.oversampleEnable;
    xs_status.oversampleFactor = xs_cfg.oversampleFactor;

    /* Try to initialize emulator engine */
    XSDEBUG("initializing emulator engine #%i...\n", xs_cfg.playerEngine);

    player = 0;
    initialized = FALSE;
    while ((player < xs_nplayerlist) && !initialized) {
        if (xs_playerlist[player].plrIdent == xs_cfg.playerEngine) {
            if (xs_playerlist[player].plrInit(&xs_status)) {
                initialized = TRUE;
                xs_status.sidPlayer = (xs_player_t *) & xs_playerlist[player];
            }
        }
        player++;
    }

    XSDEBUG("init#1: %s, %i\n", (initialized) ? "OK" : "FAILED", player);

    player = 0;
    while ((player < xs_nplayerlist) && !initialized) {
        if (xs_playerlist[player].plrInit(&xs_status)) {
            initialized = TRUE;
            xs_status.sidPlayer = (xs_player_t *) & xs_playerlist[player];
            xs_cfg.playerEngine = xs_playerlist[player].plrIdent;
        } else
            player++;
    }

    XSDEBUG("init#2: %s, %i\n", (initialized) ? "OK" : "FAILED", player);


    /* Get settings back, in case the chosen emulator backend changed them */
    xs_cfg.audioFrequency = xs_status.audioFrequency;
    xs_cfg.audioBitsPerSample = xs_status.audioBitsPerSample;
    xs_cfg.audioChannels = xs_status.audioChannels;
    xs_cfg.oversampleEnable = xs_status.oversampleEnable;

    XS_MUTEX_UNLOCK(xs_status);
    XS_MUTEX_UNLOCK(xs_cfg);

    /* Initialize song-length database */
    xs_songlen_close();
    if (xs_cfg.songlenDBEnable && (xs_songlen_init() != 0)) {
        xs_error("Error initializing song-length database!\n");
    }

    /* Initialize STIL database */
    xs_stil_close();
    if (xs_cfg.stilDBEnable && (xs_stil_init() != 0)) {
        xs_error("Error initializing STIL database!\n");
    }

}


/*
 * Initialize XMMS-SID
 */
void xs_init(void)
{
    XSDEBUG("xs_init()\n");

    /* Initialize and get configuration */
    xs_init_configuration();
    xs_read_configuration();

    /* Initialize subsystems */
    xs_reinit();

    XSDEBUG("OK\n");
}


/*
 * Shut down XMMS-SID
 */
void xs_close(void)
{
    XSDEBUG("xs_close(): shutting down...\n");

    /* Stop playing, free structures */
    xs_stop();

    xs_tuneinfo_free(xs_status.tuneInfo);
    xs_status.tuneInfo = NULL;
    xs_status.sidPlayer->plrDeleteSID(&xs_status);
    xs_status.sidPlayer->plrClose(&xs_status);

    xs_songlen_close();
    xs_stil_close();

    XSDEBUG("shutdown finished.\n");
}


/*
 * Check whether the given file is handled by this plugin
 */
gint xs_is_our_file(gchar *filename)
{
    gchar *ext;
    assert(xs_status.sidPlayer);

    /* Check the filename */
    if (filename == NULL)
        return FALSE;

    /* Try to detect via detection routine, if required */
    if (xs_cfg.detectMagic) {
        xs_file_t *f;
        if ((f = xs_fopen(filename, "rb")) != NULL) {
            if (xs_status.sidPlayer->plrProbe(f))
                return TRUE;
            xs_fclose(f);
        }
    }

    /* Detect just by checking filename extension */
    ext = xs_strrchr(filename, '.');
    if (ext) {
        ext++;
        switch (xs_cfg.playerEngine) {
        case XS_ENG_SIDPLAY1:
        case XS_ENG_SIDPLAY2:
            if (!g_strcasecmp(ext, "psid"))
                return TRUE;
            if (!g_strcasecmp(ext, "sid"))
                return TRUE;
            if (!g_strcasecmp(ext, "dat"))
                return TRUE;
            if (!g_strcasecmp(ext, "inf"))
                return TRUE;
            if (!g_strcasecmp(ext, "info"))
                return TRUE;
            break;
        }
    }

    return FALSE;
}


/*
 * Main playing thread loop
 */
void *xs_playthread(void *argPointer)
{
    xs_status_t myStatus;
    xs_tuneinfo_t *myTune;
    gboolean audioOpen = FALSE, doPlay = FALSE, isFound = FALSE;
    gint audioGot, songLength, i;
    gchar *audioBuffer = NULL, *oversampleBuffer = NULL, *tmpTitle;

    (void) argPointer;

    /* Initialize */
    XSDEBUG("entering player thread\n");
    XS_MUTEX_LOCK(xs_status);
    memcpy(&myStatus, &xs_status, sizeof(xs_status_t));
    myTune = xs_status.tuneInfo;
    for (i = 0; i <= myTune->nsubTunes; i++)
        myTune->subTunes[i].tunePlayed = FALSE;
    XS_MUTEX_UNLOCK(xs_status);

    /* Allocate audio buffer */
    audioBuffer = (gchar *) g_malloc(XS_AUDIOBUF_SIZE);
    if (audioBuffer == NULL) {
        xs_error("Couldn't allocate memory for audio data buffer!\n");
        goto xs_err_exit;
    }

    if (myStatus.oversampleEnable) {
        oversampleBuffer = (gchar *) g_malloc(XS_AUDIOBUF_SIZE * myStatus.oversampleFactor);
        if (oversampleBuffer == NULL) {
            xs_error("Couldn't allocate memory for audio oversampling buffer!\n");
            goto xs_err_exit;
        }
    }

    /*
     * Main player loop: while not stopped, loop here - play subtunes
     */
    audioOpen = FALSE;
    doPlay = TRUE;
    while (xs_status.isPlaying && doPlay) {
        /* Automatic sub-tune change logic */
        XS_MUTEX_LOCK(xs_cfg);
        XS_MUTEX_LOCK(xs_status);
        myStatus.isPlaying = TRUE;
        
        if (xs_status.currSong < 1 || myStatus.currSong < 1) {
            XS_MUTEX_UNLOCK(xs_status);
            XS_MUTEX_UNLOCK(xs_cfg);
            goto xs_err_exit;
        }
        
        if (xs_cfg.subAutoEnable && (myStatus.currSong == xs_status.currSong)) {
            /* Check if currently selected sub-tune has been played already */
            if (myTune->subTunes[myStatus.currSong-1].tunePlayed) {
                /* Find a tune that has not been played */
                XSDEBUG("tune #%i already played, finding next match ...\n", myStatus.currSong);
                isFound = FALSE;
                i = 0;
                while (!isFound && (++i <= myTune->nsubTunes)) {
                    if (xs_cfg.subAutoMinOnly) {
                        /* A tune with minimum length must be found */
                        if (!myTune->subTunes[i-1].tunePlayed &&
                            myTune->subTunes[i-1].tuneLength >= xs_cfg.subAutoMinTime)
                            isFound = TRUE;
                    } else {
                        /* Any unplayed tune is okay */
                        if (!myTune->subTunes[i-1].tunePlayed)
                            isFound = TRUE;
                    }
                }

                if (isFound) {
                    /* Set the new sub-tune */
                    XSDEBUG("found #%i\n", i);
                    xs_status.currSong = i;
                } else
                    /* This is the end */
                    doPlay = FALSE;

                XS_MUTEX_UNLOCK(xs_status);
                XS_MUTEX_UNLOCK(xs_cfg);
                continue;    /* This is ugly, but ... */
            }
        }

        /* Tell that we are initializing, update sub-tune controls */
        myStatus.currSong = xs_status.currSong;
        myTune->subTunes[myStatus.currSong-1].tunePlayed = TRUE;
        XS_MUTEX_UNLOCK(xs_status);
        XS_MUTEX_UNLOCK(xs_cfg);

        XSDEBUG("subtune #%i selected, initializing...\n", myStatus.currSong);

        GDK_THREADS_ENTER();
        xs_subctrl_update();
        GDK_THREADS_LEAVE();

        /* Check minimum playtime */
        songLength = myTune->subTunes[myStatus.currSong-1].tuneLength;
        if (xs_cfg.playMinTimeEnable && (songLength >= 0)) {
            if (songLength < xs_cfg.playMinTime)
                songLength = xs_cfg.playMinTime;
        }

        /* Initialize song */
        if (!myStatus.sidPlayer->plrInitSong(&myStatus)) {
            xs_error("Couldn't initialize SID-tune '%s' (sub-tune #%i)!\n",
                  myTune->sidFilename, myStatus.currSong);
            goto xs_err_exit;
        }
        
        /* Open the audio output */
        XSDEBUG("open audio output (%d, %d, %d)\n",
            myStatus.audioFormat, myStatus.audioFrequency, myStatus.audioChannels);
        
        if (!xs_plugin_ip.output->
            open_audio(myStatus.audioFormat, myStatus.audioFrequency, myStatus.audioChannels)) {
            xs_error("Couldn't open XMMS audio output (fmt=%x, freq=%i, nchan=%i)!\n",
                myStatus.audioFormat,
                myStatus.audioFrequency,
                myStatus.audioChannels);

            XS_MUTEX_LOCK(xs_status);
            xs_status.isError = TRUE;
            XS_MUTEX_UNLOCK(xs_status);
            goto xs_err_exit;
        }

        audioOpen = TRUE;

        /* Set song information for current subtune */
        XSDEBUG("set tune info\n");
        myStatus.sidPlayer->plrUpdateSIDInfo(&myStatus);        
        tmpTitle = xs_make_titlestring(myTune, myStatus.currSong);
        
        xs_plugin_ip.set_info(
            tmpTitle,
            (songLength > 0) ? (songLength * 1000) : 0,
            -1,
            myStatus.audioFrequency,
            myStatus.audioChannels);
        
        g_free(tmpTitle);
        
        XSDEBUG("playing\n");

        /*
         * Play the subtune
         */
        while (xs_status.isPlaying && myStatus.isPlaying && (xs_status.currSong == myStatus.currSong)) {
            /* Render audio data */
            if (myStatus.oversampleEnable) {
                /* Perform oversampled rendering */
                audioGot = myStatus.sidPlayer->plrFillBuffer(
                    &myStatus,
                    oversampleBuffer,
                    (XS_AUDIOBUF_SIZE * myStatus.oversampleFactor));

                audioGot /= myStatus.oversampleFactor;

                /* Execute rate-conversion with filtering */
                if (xs_filter_rateconv(audioBuffer, oversampleBuffer,
                    myStatus.audioFormat, myStatus.oversampleFactor, audioGot) < 0) {
                    xs_error("Oversampling rate-conversion pass failed.\n");
                    XS_MUTEX_LOCK(xs_status);
                    xs_status.isError = TRUE;
                    XS_MUTEX_UNLOCK(xs_status);
                    goto xs_err_exit;
                }
            } else {
                audioGot = myStatus.sidPlayer->plrFillBuffer(
                    &myStatus, audioBuffer, XS_AUDIOBUF_SIZE);
            }

            /* I <3 visualice/haujobb */
            xs_plugin_ip.add_vis_pcm(
                xs_plugin_ip.output->written_time(),
                myStatus.audioFormat, myStatus.audioChannels,
                audioGot, audioBuffer);

            /* Wait a little */
            while (xs_status.isPlaying &&
                (xs_status.currSong == myStatus.currSong) &&
                (xs_plugin_ip.output->buffer_free() < audioGot))
                xmms_usleep(500);

            /* Output audio */
            if (xs_status.isPlaying && (xs_status.currSong == myStatus.currSong))
                xs_plugin_ip.output->write_audio(audioBuffer, audioGot);

            /* Check if we have played enough */
            if (xs_cfg.playMaxTimeEnable) {
                if (xs_cfg.playMaxTimeUnknown) {
                    if ((songLength < 0) &&
                        (xs_plugin_ip.output->output_time() >= (xs_cfg.playMaxTime * 1000)))
                        myStatus.isPlaying = FALSE;
                } else {
                    if (xs_plugin_ip.output->output_time() >= (xs_cfg.playMaxTime * 1000))
                        myStatus.isPlaying = FALSE;
                }
            }

            if (songLength >= 0) {
                if (xs_plugin_ip.output->output_time() >= (songLength * 1000))
                    myStatus.isPlaying = FALSE;
            }
        }

        XSDEBUG("subtune ended/stopped\n");

        /* Close audio output plugin */
        if (audioOpen) {
            XSDEBUG("close audio #1\n");
            xs_plugin_ip.output->close_audio();
            audioOpen = FALSE;
            XSDEBUG("closed\n");
        }

        /* Now determine if we continue by selecting other subtune or something */
        if (!myStatus.isPlaying && !xs_cfg.subAutoEnable)
            doPlay = FALSE;
    }

xs_err_exit:
    XSDEBUG("out of playing loop\n");
    
    /* Close audio output plugin */
    if (audioOpen) {
        XSDEBUG("close audio #2\n");
        xs_plugin_ip.output->close_audio();
        XSDEBUG("closed\n");
    }

    g_free(audioBuffer);
    g_free(oversampleBuffer);

    /* Set playing status to false (stopped), thus when
     * XMMS next calls xs_get_time(), it can return appropriate
     * value "not playing" status and XMMS knows to move to
     * next entry in the playlist .. or whatever it wishes.
     */
    XS_MUTEX_LOCK(xs_status);
    xs_status.isPlaying = FALSE;
    XS_MUTEX_UNLOCK(xs_status);

    /* Exit the playing thread */
    XSDEBUG("exiting thread, bye.\n");
    XS_THREAD_EXIT(NULL);
}


/*
 * Start playing the given file
 * Here we load the tune and initialize the playing thread.
 * Usually you would also initialize the output-plugin, but
 * this is XMMS-SID and we do it on the player thread instead.
 */
void xs_play_file(gchar *filename)
{
    assert(xs_status.sidPlayer);

    XSDEBUG("play '%s'\n", filename);

    /* Get tune information */
    if ((xs_status.tuneInfo = xs_status.sidPlayer->plrGetSIDInfo(filename)) == NULL)
        return;

    /* Initialize the tune */
    if (!xs_status.sidPlayer->plrLoadSID(&xs_status, filename)) {
        xs_tuneinfo_free(xs_status.tuneInfo);
        xs_status.tuneInfo = NULL;
        return;
    }

    XSDEBUG("load ok\n");

    /* Set general status information */
    xs_status.isPlaying = TRUE;
    xs_status.isError = FALSE;
    xs_status.currSong = xs_status.tuneInfo->startTune;

    /* Start the playing thread! */
    if (pthread_create(&xs_decode_thread, NULL, xs_playthread, NULL) < 0) {
        xs_error("Couldn't create playing thread!\n");
        xs_tuneinfo_free(xs_status.tuneInfo);
        xs_status.tuneInfo = NULL;
        xs_status.sidPlayer->plrDeleteSID(&xs_status);
    }

    /* Okay, here the playing thread has started up and we
     * return from here to XMMS. Rest is up to XMMS's GUI
     * and playing thread.
     */
    XSDEBUG("systems should be up?\n");
}


/*
 * Stop playing
 * Here we set the playing status to stop and wait for playing
 * thread to shut down. In any "correctly" done plugin, this is
 * also the function where you close the output-plugin, but since
 * XMMS-SID has special behaviour (audio opened/closed in the
 * playing thread), we don't do that here.
 *
 * Finally tune and other memory allocations are free'd.
 */
void xs_stop(void)
{
    XSDEBUG("stop requested\n");

    /* Close the sub-tune control window, if any */
    xs_subctrl_close();

    /* Lock xs_status and stop playing thread */
    XS_MUTEX_LOCK(xs_status);
    if (xs_status.isPlaying) {
        XSDEBUG("stopping...\n");
        xs_status.isPlaying = FALSE;
        XS_MUTEX_UNLOCK(xs_status);
        XS_THREAD_JOIN(xs_decode_thread);
    } else {
        XS_MUTEX_UNLOCK(xs_status);
    }

    XSDEBUG("done, updating status\n");
    
    /* Status is now stopped, update the sub-tune
     * controller in fileinfo window (if open)
     */
    xs_fileinfo_update();

    /* Free tune information */
    XS_MUTEX_LOCK(xs_status);
    xs_status.sidPlayer->plrDeleteSID(&xs_status);
    xs_tuneinfo_free(xs_status.tuneInfo);
    xs_status.tuneInfo = NULL;
    XS_MUTEX_UNLOCK(xs_status);
    XSDEBUG("ok\n");
}


/*
 * Pause/unpause the playing
 */
void xs_pause(short pause)
{
    XS_MUTEX_LOCK(xs_status);
    /* FIXME FIX ME todo: pause should disable sub-tune controls */
    XS_MUTEX_UNLOCK(xs_status);

    xs_subctrl_close();
    xs_fileinfo_update();
    xs_plugin_ip.output->pause(pause);
}


/*
 * Pop-up subtune selector
 */
void xs_subctrl_setsong(void)
{
    gint n;

    XS_MUTEX_LOCK(xs_status);
    XS_MUTEX_LOCK(xs_subctrl);

    if (xs_status.tuneInfo && xs_status.isPlaying) {
        n = (gint) GTK_ADJUSTMENT(xs_subctrl_adj)->value;
        if ((n >= 1) && (n <= xs_status.tuneInfo->nsubTunes))
            xs_status.currSong = n;
    }

    XS_MUTEX_UNLOCK(xs_subctrl);
    XS_MUTEX_UNLOCK(xs_status);
}


void xs_subctrl_prevsong(void)
{
    XS_MUTEX_LOCK(xs_status);

    if (xs_status.tuneInfo && xs_status.isPlaying) {
        if (xs_status.currSong > 1)
            xs_status.currSong--;
    }

    XS_MUTEX_UNLOCK(xs_status);

    xs_subctrl_update();
}


void xs_subctrl_nextsong(void)
{
    XS_MUTEX_LOCK(xs_status);

    if (xs_status.tuneInfo && xs_status.isPlaying) {
        if (xs_status.currSong < xs_status.tuneInfo->nsubTunes)
            xs_status.currSong++;
    }

    XS_MUTEX_UNLOCK(xs_status);

    xs_subctrl_update();
}


void xs_subctrl_update(void)
{
    GtkAdjustment *tmpAdj;

    XS_MUTEX_LOCK(xs_status);
    XS_MUTEX_LOCK(xs_subctrl);

    /* Check if control window exists, we are currently playing and have a tune */
    if (xs_subctrl) {
        if (xs_status.tuneInfo && xs_status.isPlaying) {
            tmpAdj = GTK_ADJUSTMENT(xs_subctrl_adj);

            tmpAdj->value = xs_status.currSong;
            tmpAdj->lower = 1;
            tmpAdj->upper = xs_status.tuneInfo->nsubTunes;
            XS_MUTEX_UNLOCK(xs_status);
            XS_MUTEX_UNLOCK(xs_subctrl);
            gtk_adjustment_value_changed(tmpAdj);
        } else {
            XS_MUTEX_UNLOCK(xs_status);
            XS_MUTEX_UNLOCK(xs_subctrl);
            xs_subctrl_close();
        }
    } else {
        XS_MUTEX_UNLOCK(xs_subctrl);
        XS_MUTEX_UNLOCK(xs_status);
    }

    xs_fileinfo_update();
}


void xs_subctrl_close(void)
{
    XS_MUTEX_LOCK(xs_subctrl);

    if (xs_subctrl) {
        gtk_widget_destroy(xs_subctrl);
        xs_subctrl = NULL;
    }

    XS_MUTEX_UNLOCK(xs_subctrl);
}


gboolean xs_subctrl_keypress(GtkWidget * win, GdkEventKey * ev)
{
    (void) win;

    if (ev->keyval == GDK_Escape)
        xs_subctrl_close();

    return FALSE;
}


void xs_subctrl_open(void)
{
    GtkWidget *frame25, *hbox15, *subctrl_prev, *subctrl_current, *subctrl_next;

    XS_MUTEX_LOCK(xs_subctrl);
    if (!xs_status.tuneInfo || !xs_status.isPlaying ||
        xs_subctrl || (xs_status.tuneInfo->nsubTunes <= 1)) {
        XS_MUTEX_UNLOCK(xs_subctrl);
        return;
    }

    /* Create the pop-up window */
    xs_subctrl = gtk_window_new(GTK_WINDOW_DIALOG);
    gtk_widget_set_name(xs_subctrl, "xs_subctrl");
    gtk_object_set_data(GTK_OBJECT(xs_subctrl), "xs_subctrl", xs_subctrl);

    gtk_window_set_title(GTK_WINDOW(xs_subctrl), _("Subtune Control"));
    gtk_window_set_position(GTK_WINDOW(xs_subctrl), GTK_WIN_POS_MOUSE);
    gtk_container_set_border_width(GTK_CONTAINER(xs_subctrl), 0);
    gtk_window_set_policy(GTK_WINDOW(xs_subctrl), FALSE, FALSE, FALSE);

    XS_SIGNAL_CONNECT(xs_subctrl, "destroy", gtk_widget_destroyed, &xs_subctrl);
    XS_SIGNAL_CONNECT(xs_subctrl, "focus_out_event", xs_subctrl_close, NULL);

    gtk_widget_realize(xs_subctrl);
    gdk_window_set_decorations(xs_subctrl->window, (GdkWMDecoration) 0);


    /* Create the control widgets */
    frame25 = gtk_frame_new(NULL);
    gtk_container_add(GTK_CONTAINER(xs_subctrl), frame25);
    gtk_container_set_border_width(GTK_CONTAINER(frame25), 2);
    gtk_frame_set_shadow_type(GTK_FRAME(frame25), GTK_SHADOW_OUT);

    hbox15 = gtk_hbox_new(FALSE, 4);
    gtk_container_add(GTK_CONTAINER(frame25), hbox15);

    subctrl_prev = gtk_button_new_with_label(" < ");
    gtk_widget_set_name(subctrl_prev, "subctrl_prev");
    gtk_box_pack_start(GTK_BOX(hbox15), subctrl_prev, FALSE, FALSE, 0);

    xs_subctrl_adj = gtk_adjustment_new(xs_status.currSong, 1, xs_status.tuneInfo->nsubTunes, 1, 1, 0);
    XS_SIGNAL_CONNECT(xs_subctrl_adj, "value_changed", xs_subctrl_setsong, NULL);

    subctrl_current = gtk_hscale_new(GTK_ADJUSTMENT(xs_subctrl_adj));
    gtk_widget_set_name(subctrl_current, "subctrl_current");
    gtk_box_pack_start(GTK_BOX(hbox15), subctrl_current, FALSE, TRUE, 0);
    gtk_scale_set_digits(GTK_SCALE(subctrl_current), 0);
    gtk_range_set_update_policy(GTK_RANGE(subctrl_current), GTK_UPDATE_DELAYED);
    gtk_widget_grab_focus(subctrl_current);

    subctrl_next = gtk_button_new_with_label(" > ");
    gtk_widget_set_name(subctrl_next, "subctrl_next");
    gtk_box_pack_start(GTK_BOX(hbox15), subctrl_next, FALSE, FALSE, 0);

    XS_SIGNAL_CONNECT(subctrl_prev, "clicked", xs_subctrl_prevsong, NULL);
    XS_SIGNAL_CONNECT(subctrl_next, "clicked", xs_subctrl_nextsong, NULL);
    XS_SIGNAL_CONNECT(xs_subctrl, "key_press_event", xs_subctrl_keypress, NULL);

    gtk_widget_show_all(xs_subctrl);

    XS_MUTEX_UNLOCK(xs_subctrl);
}


/*
 * Set the time-seek position
 * The playing thread will do the "seeking", which means sub-tune
 * changing in XMMS-SID's case. time argument is time in seconds,
 * in contrast to milliseconds used in other occasions.
 *
 * This function is called whenever position slider is clicked or
 * other method of seeking is used (keyboard, etc.)
 */
void xs_seek(gint time)
{
    /* Check status */
    XS_MUTEX_LOCK(xs_status);
    if (!xs_status.tuneInfo || !xs_status.isPlaying) {
        XS_MUTEX_UNLOCK(xs_status);
        return;
    }

    /* Act according to settings */
    switch (xs_cfg.subsongControl) {
    case XS_SSC_SEEK:
        if (time < xs_status.lastTime) {
            if (xs_status.currSong > 1)
                xs_status.currSong--;
        } else if (time > xs_status.lastTime) {
            if (xs_status.currSong < xs_status.tuneInfo->nsubTunes)
                xs_status.currSong++;
        }
        break;

    case XS_SSC_POPUP:
        xs_subctrl_open();
        break;

        /* If we have song-position patch, check settings */
#ifdef HAVE_SONG_POSITION
    case XS_SSC_PATCH:
        if ((time > 0) && (time <= xs_status.tuneInfo->nsubTunes))
            xs_status.currSong = time;
        break;
#endif
    }

    XS_MUTEX_UNLOCK(xs_status);
}


/*
 * Return the playing "position/time"
 * Determine current position/time in song. Used by XMMS to update
 * the song clock and position slider and MOST importantly to determine
 * END OF SONG! Return value of -2 means error, XMMS opens an audio
 * error dialog. -1 means end of song (if one was playing currently).
 */
gint xs_get_time(void)
{
    /* If errorflag is set, return -2 to signal it to XMMS's idle callback */
    XS_MUTEX_LOCK(xs_status);
    if (xs_status.isError) {
        XS_MUTEX_UNLOCK(xs_status);
        return -2;
    }

    /* If there is no tune, return -1 */
    if (!xs_status.tuneInfo) {
        XS_MUTEX_UNLOCK(xs_status);
        return -1;
    }

    /* If tune has ended, return -1 */
    if (!xs_status.isPlaying) {
        XS_MUTEX_UNLOCK(xs_status);
        return -1;
    }

    /* Let's see what we do */
    switch (xs_cfg.subsongControl) {
    case XS_SSC_SEEK:
        xs_status.lastTime = (xs_plugin_ip.output->output_time() / 1000);
        break;

#ifdef HAVE_SONG_POSITION
    case XS_SSC_PATCH:
        set_song_position(xs_status.currSong, 1, xs_status.tuneInfo->nsubTunes);
        break;
#endif
    }

    XS_MUTEX_UNLOCK(xs_status);

    /* Return output time reported by audio output plugin */
    return xs_plugin_ip.output->output_time();
}


/* Return song information: called by XMMS when initially loading the playlist.
 * Subsequent changes to information are made by the player thread,
 * which uses xs_plugin_ip.set_info();
 */
void xs_get_song_info(gchar * songFilename, gchar ** songTitle, gint * songLength)
{
    xs_tuneinfo_t *info;
    
    XS_MUTEX_LOCK(xs_status);

    /* Get tune information from emulation engine */
    info = xs_status.sidPlayer->plrGetSIDInfo(songFilename);
    if (!info) {
        XS_MUTEX_UNLOCK(xs_status);
        return;
    }

    /* Get sub-tune information, if available */
    if ((info->startTune > 0) && (info->startTune <= info->nsubTunes)) {
        gint tmpInt;
        
        (*songTitle) = xs_make_titlestring(info, info->startTune);

        tmpInt = info->subTunes[info->startTune-1].tuneLength;
        if (tmpInt < 0)
            (*songLength) = -1;
        else
            (*songLength) = (tmpInt * 1000);
    }

    /* Free tune information */
    xs_tuneinfo_free(info);
    XS_MUTEX_UNLOCK(xs_status);
}