view src/xmms-sid.c @ 957:0e60e5d56fdd

Change how the backend emulator library is initialized for libSIDPlay2 and FP, as it seems the engine configuration has some persistence despite reconfiguration between loaded files if same engine object is retained. This caused, for example, 2SID stereo tunes being played "mono" if played after a normal 1-SID tune. Duh.
author Matti Hamalainen <ccr@tnsp.org>
date Tue, 20 Nov 2012 22:13:48 +0200
parents 04dc44e87e03
children ede5374e9294
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-2009 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 <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_fileinfo.h"
#include "xs_interface.h"
#include "xs_glade.h"
#include "xs_backend.h"
#include "xs_slsup.h"


/* Global variables
 */
XSEngineState 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);


/*
 * Initialization functions
 */
void xs_reinit(void)
{
    /* Stop playing, if we are */
    XS_MUTEX_LOCK(xs_status);
    if (xs_status.playing)
    {
        XS_MUTEX_UNLOCK(xs_status);
        xs_stop();
    }
    else
    {
        XS_MUTEX_UNLOCK(xs_status);
    }

    XS_MUTEX_LOCK(xs_status);
    XS_MUTEX_LOCK(xs_cfg);

    // Close player stuff
    xs_tuneinfo_free(xs_status.tuneInfo);
    xs_status.tuneInfo = NULL;
    if (xs_status.engine != NULL)
    {
        xs_status.engine->plrDeleteSID(&xs_status);
        xs_status.engine->plrClose(&xs_status);
    }

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

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

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

    /* Try to initialize emulator engine */
    xs_init_emu_backend(&xs_status, &xs_cfg.playerEngine);

    /* 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_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");
    }
}


gboolean xs_configure_pester(gpointer data)
{
    xs_configure();

    xs_messagebox(_("Attention!"),
    _("It seems you haven't yet configured XMMS-SID plugin, "
    "or the configuration is from another version. "
    "Opening the configuration dialog for it now."));
    return FALSE;
}


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

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

    if (!xs_read_configuration())
    {
        g_timeout_add(1500, xs_configure_pester, NULL);
    }

    /* 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;
    if (xs_status.engine != NULL)
    {
        xs_status.engine->plrDeleteSID(&xs_status);
        xs_status.engine->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;
    if (xs_status.engine == NULL)
        return FALSE;

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

    /* Try to detect via detection routine, if required */
    if (xs_cfg.detectMagic && xs_status.engine->plrProbe != NULL)
    {
        gboolean res = FALSE;
        XSFile *f;
        if ((f = xs_fopen(filename, "rb")) != NULL)
        {
            res = xs_status.engine->plrProbe(f);
            xs_fclose(f);
        }
        return res;
    }

    /* Detect just by checking filename extension */
    ext = strrchr(filename, '.');
    if (ext)
    {
        ext++;
        switch (xs_cfg.playerEngine)
        {
            case XS_ENG_SIDPLAY1:
                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;

            case XS_ENG_SIDPLAY2:
            case XS_ENG_SIDPLAYFP:
                if (!g_strcasecmp(ext, "sid"))
                    return TRUE;
                break;
        }
    }

    return FALSE;
}


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

    (void) argPointer;

    /* Initialize */
    XSDEBUG("entering player thread\n");
    XS_MUTEX_LOCK(xs_status);
    memcpy(&myStatus, &xs_status, sizeof(XSEngineState));
    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;
    }

    /*
     * Main player loop: while not stopped, loop here - play subtunes
     */
    audioOpen = FALSE;
    doPlay = TRUE;
    while (xs_status.playing && doPlay)
    {
        /* Automatic sub-tune change logic */
        XS_MUTEX_LOCK(xs_cfg);
        XS_MUTEX_LOCK(xs_status);
        myStatus.playing = 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.engine->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.error = TRUE;
            XS_MUTEX_UNLOCK(xs_status);
            goto xs_err_exit;
        }

        audioOpen = TRUE;

        /* Set song information for current subtune */
        XSDEBUG("set tune info\n");
        myStatus.engine->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.playing && myStatus.playing && (xs_status.currSong == myStatus.currSong))
        {
            audioGot = myStatus.engine->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.playing &&
                (xs_status.currSong == myStatus.currSong) &&
                (xs_plugin_ip.output->buffer_free() < audioGot))
                xmms_usleep(500);

            /* Output audio */
            if (xs_status.playing && 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.playing = FALSE;
                }
                else
                {
                    if (xs_plugin_ip.output->output_time() >= xs_cfg.playMaxTime * 1000)
                        myStatus.playing = FALSE;
                }
            }

            if (songLength >= 0)
            {
                if (xs_plugin_ip.output->output_time() >= songLength * 1000)
                    myStatus.playing = 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.playing && !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);

    /* 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.playing = 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)
{
    if (xs_status.engine == NULL || filename == NULL)
        return;

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

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

    /* Initialize the tune */
    if (!xs_status.engine->plrLoadSID(&xs_status, filename))
    {
        XSDEBUG("backend could not load '%s'\n", filename);
        xs_tuneinfo_free(xs_status.tuneInfo);
        xs_status.tuneInfo = NULL;
        return;
    }

    XSDEBUG("load ok\n");

    /* Set general status information */
    xs_status.playing = TRUE;
    xs_status.error = 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.engine->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.playing)
    {
        XSDEBUG("stopping...\n");
        xs_status.playing = 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);
    if (xs_status.engine != NULL)
        xs_status.engine->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);
    xs_status.paused = pause;
    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.paused && xs_status.tuneInfo && xs_status.playing)
    {
        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.paused && xs_status.tuneInfo && xs_status.playing)
    {
        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.paused && xs_status.tuneInfo && xs_status.playing)
    {
        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.paused && xs_status.tuneInfo && xs_status.playing)
        {
            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.paused || !xs_status.tuneInfo || !xs_status.playing ||
        xs_subctrl || xs_status.tuneInfo->nsubTunes <= 1)
        goto out;

    /* 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);

out:
    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)
{
    XS_MUTEX_LOCK(xs_status);
    if (!xs_status.tuneInfo || !xs_status.playing)
        goto out;

    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
    }

out:
    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)
{
    int err;

    /* If errorflag is set, return -2 to signal it to XMMS's idle callback */
    XS_MUTEX_LOCK(xs_status);
    if (xs_status.error)
    {
        err = -2;
        goto error;
    }

    /* If there is no tune or tune has ended, return -1 */
    if (!xs_status.tuneInfo || !xs_status.playing)
    {
        err = -1;
        goto error;
    }

    /* 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();

error:
    XS_MUTEX_UNLOCK(xs_status);
    return err;
}


/* 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)
{
    XSTuneInfo *info;
    
    XS_MUTEX_LOCK(xs_status);

    if (xs_status.engine == NULL)
        goto out;

    /* Get tune information from emulation engine */
    info = xs_status.engine->plrGetSIDInfo(songFilename);
    if (!info)
        goto out;

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

        (*songTitle) = xs_make_titlestring(info, info->startTune);

        length = info->subTunes[info->startTune-1].tuneLength;
        (*songLength) = length < 0 ? -1 : length * 1000;
    }

    /* Free tune information */
    xs_tuneinfo_free(info);

out:
    XS_MUTEX_UNLOCK(xs_status);
}