view src/xs_length.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 3c2efa18c422
children 5e0a05c84694
line wrap: on
line source

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

   Get song length from SLDB for PSID/RSID files
   
   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
   with this program; if not, write to the Free Software Foundation, Inc.,
   51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
*/
#include "xs_length.h"
#include "xs_support.h"


/* Free memory allocated for given SLDB node
 */
static void xs_sldb_node_free(XSSLDBNode *node)
{
    if (node)
    {
        g_free(node->lengths);
        g_free(node);
    }
}


/* Insert given node to db linked list
 */
static void xs_sldb_node_insert(XSSLDB *db, XSSLDBNode *node)
{
    assert(db != NULL);

    if (db->nodes)
    {
        node->prev = db->nodes->prev;
        db->nodes->prev->next = node;
        db->nodes->prev = node;
    }
    else
    {
        db->nodes = node;
        node->prev = node;
    }
    node->next = NULL;
}


/* Parse a time-entry in SLDB format
 */
static gint xs_sldb_gettime(gchar *str, size_t *pos)
{
    gint result, tmp;

    /* Check if it starts with a digit */
    if (isdigit(str[*pos]))
    {
        /* Get minutes-field */
        result = 0;
        while (isdigit(str[*pos]))
            result = (result * 10) + (str[(*pos)++] - '0');

        result *= 60;

        /* Check the field separator char */
        if (str[*pos] == ':')
        {
            /* Get seconds-field */
            (*pos)++;
            tmp = 0;
            while (isdigit(str[*pos]))
                tmp = (tmp * 10) + (str[(*pos)++] - '0');

            result += tmp;
        }
        else
            result = -2;
    }
    else
        result = -1;

    /* Ignore and skip the possible attributes */
    while (str[*pos] && !isspace(str[*pos]))
        (*pos)++;

    return result;
}


/* Parse one SLDB definition line, return SLDB node
 */
XSSLDBNode * xs_sldb_read_entry(gchar *inLine)
{
    XSSLDBNode *tmnode = NULL;
    size_t linePos, tmpLen, savePos;
    gboolean isOK;
    gint i;

    /* Allocate new node */
    tmnode = (XSSLDBNode *) g_malloc0(sizeof(XSSLDBNode));
    if (tmnode == NULL)
    {
        xs_error("Error allocating new node. Fatal error.\n");
        return NULL;
    }

    /* Get hash value */
    linePos = 0;
    for (i = 0; i < XS_MD5HASH_LENGTH; i++, linePos += 2)
    {
        gint tmpu;
        sscanf(&inLine[linePos], "%2x", &tmpu);
        tmnode->md5Hash[i] = tmpu;
    }
        
    /* Get playtimes */
    xs_findnext(inLine, &linePos);
    if (inLine[linePos] != '=')
    {
        xs_error("'=' expected on column #%d.\n", linePos);
        goto error;
    }

    /* First playtime is after '=' */
    savePos = ++linePos;
    tmpLen = strlen(inLine);

    /* Get number of sub-tune lengths */
    isOK = TRUE;
    while (linePos < tmpLen && isOK)
    {
        xs_findnext(inLine, &linePos);

        if (xs_sldb_gettime(inLine, &linePos) >= 0)
            tmnode->nlengths++;
        else
            isOK = FALSE;
    }

    /* Allocate memory for lengths */
    if (tmnode->nlengths == 0)
        goto error;

    tmnode->lengths = (gint *) g_malloc0(tmnode->nlengths * sizeof(gint));
    if (tmnode->lengths == NULL)
    {
        xs_error("Could not allocate memory for node.\n");
        goto error;
    }

    /* Read lengths in */
    for (i = 0, linePos = savePos, isOK = TRUE; 
        linePos < tmpLen && i < tmnode->nlengths && isOK; i++)
    {
        gint l;
        xs_findnext(inLine, &linePos);

        l = xs_sldb_gettime(inLine, &linePos);
        if (l >= 0)
            tmnode->lengths[i] = l;
        else
            isOK = FALSE;
    }

    return tmnode;

error:
    xs_sldb_node_free(tmnode);
    return NULL;
}


/* Read database to memory
 */
gint xs_sldb_read(XSSLDB *db, const gchar *dbFilename)
{
    FILE *inFile;
    gchar inLine[XS_BUF2_SIZE];
    size_t lineNum;
    XSSLDBNode *tmnode;
    assert(db);

    /* Try to open the file */
    if ((inFile = fopen(dbFilename, "ra")) == NULL)
    {
        xs_error("Could not open SongLengthDB '%s'\n", dbFilename);
        return -1;
    }

    /* Read and parse the data */
    lineNum = 0;

    while (fgets(inLine, XS_BUF2_SIZE, inFile) != NULL)
    {
        size_t linePos = 0;
        lineNum++;
        
        xs_findnext(inLine, &linePos);

        /* Check if it is datafield */
        if (isxdigit(inLine[linePos]))
        {
            /* Check the length of the hash */
            gint hashLen;
            for (hashLen = 0; inLine[linePos] && isxdigit(inLine[linePos]); hashLen++, linePos++);

            if (hashLen != XS_MD5HASH_LENGTH_CH)
            {
                xs_error("Invalid MD5-hash in SongLengthDB file '%s' line #%d:\n%s\n",
                    dbFilename, lineNum, inLine);
            }
            else
            {
                /* Parse and add node to db */
                if ((tmnode = xs_sldb_read_entry(inLine)) != NULL)
                {
                    xs_sldb_node_insert(db, tmnode);
                }
                else
                {
                    xs_error("Invalid entry in SongLengthDB file '%s' line #%d:\n%s\n",
                        dbFilename, lineNum, inLine);
                }
            }
        }
        else
        if (inLine[linePos] != ';' && inLine[linePos] != '[' && inLine[linePos] != 0)
        {
            xs_error("Invalid line in SongLengthDB file '%s' line #%d:\n%s\n",
                dbFilename, lineNum, inLine);
        }
    }

    fclose(inFile);
    return 0;
}


/* Compare two given MD5-hashes.
 * Return: 0 if equal
 *         negative if testHash1 < testHash2
 *         positive if testHash1 > testHash2
 */
static gint xs_sldb_cmphash(xs_md5hash_t testHash1, xs_md5hash_t testHash2)
{
    gint i, d;

    /* Compute difference of hashes */
    for (i = 0, d = 0; (i < XS_MD5HASH_LENGTH) && !d; i++)
        d = (testHash1[i] - testHash2[i]);

    return d;
}


/* Compare two nodes
 */
static gint xs_sldb_cmp(const void *node1, const void *node2)
{
    /* We assume here that we never ever get NULL-pointers or similar */
    return xs_sldb_cmphash(
        (*(XSSLDBNode **) node1)->md5Hash,
        (*(XSSLDBNode **) node2)->md5Hash);
}


/* (Re)create index
 */
gint xs_sldb_index(XSSLDB * db)
{
    XSSLDBNode *node;
    size_t i;
    assert(db);

    /* Free old index */
    g_free(db->pindex);
    db->pindex = NULL;

    /* Get size of db */
    for (node = db->nodes, db->n = 0; node != NULL; node = node->next)
        db->n++;

    /* Check number of nodes */
    if (db->n > 0)
    {
        /* Allocate memory for index-table */
        db->pindex = (XSSLDBNode **) g_malloc(sizeof(XSSLDBNode *) * db->n);
        if (!db->pindex)
            return -1;

        /* Get node-pointers to table */
        for (i = 0, node = db->nodes; node && i < db->n; node = node->next)
            db->pindex[i++] = node;

        /* Sort the indexes */
        qsort(db->pindex, db->n, sizeof(XSSLDBNode *), xs_sldb_cmp);
    }

    return 0;
}


/* Free a given song-length database
 */
void xs_sldb_free(XSSLDB * db)
{
    XSSLDBNode *node, *next;

    if (!db)
        return;

    /* Free the memory allocated for nodes */
    node = db->nodes;
    while (node != NULL)
    {
        next = node->next;
        xs_sldb_node_free(node);
        node = next;
    }

    db->nodes = NULL;

    /* Free memory allocated for index */
    g_free(db->pindex);
    db->pindex = NULL;

    /* Free structure */
    db->n = 0;
    g_free(db);
}


/* Compute md5hash of given SID-file
 */
typedef struct
{
    gchar magicID[4];    /* "PSID" / "RSID" magic identifier */
    guint16 version,     /* Version number */
        dataOffset,      /* Start of actual c64 data in file */
        loadAddress,     /* Loading address */
        initAddress,     /* Initialization address */
        playAddress,     /* Play one frame */
        nSongs,          /* Number of subsongs */
        startSong;       /* Default starting song */
    guint32 speed;       /* Speed */
    gchar sidName[32];   /* Descriptive text-fields, ASCIIZ */
    gchar sidAuthor[32];
    gchar sidCopyright[32];
} psidv1_header_t;


typedef struct
{
    guint16 flags;        /* Flags */
    guint8 startPage, pageLength;
    guint16 reserved;
} psidv2_header_t;


static gint xs_get_sid_hash(const gchar *filename, xs_md5hash_t hash)
{
    XSFile *inFile = NULL;
    xs_md5state_t inState;
    psidv1_header_t psidH;
    psidv2_header_t psidH2;
    guint8 *songData = NULL;
    guint8 ib8[2], i8;
    gint index, result;

    /* Try to open the file */
    if ((inFile = xs_fopen(filename, "rb")) == NULL)
        goto error;

    /* Read PSID header in */
    if (!xs_fread_str(inFile, psidH.magicID, sizeof(psidH.magicID)) ||
        !xs_fread_be16(inFile, &psidH.version) ||
        !xs_fread_be16(inFile, &psidH.dataOffset) ||
        !xs_fread_be16(inFile, &psidH.loadAddress) ||
        !xs_fread_be16(inFile, &psidH.initAddress) ||
        !xs_fread_be16(inFile, &psidH.playAddress) ||
        !xs_fread_be16(inFile, &psidH.nSongs) ||
        !xs_fread_be16(inFile, &psidH.startSong) ||
        !xs_fread_be32(inFile, &psidH.speed))
    {
        xs_error("Could not read PSID/RSID header from '%s'\n", filename);
        goto error;
    }

    if ((strncmp(psidH.magicID, "PSID", 4) &&
        strncmp(psidH.magicID, "RSID", 4)) ||
        psidH.version < 1 || psidH.version > 3)
    {
        xs_error("Not a supported PSID or RSID file '%s'\n", filename);
        goto error;
    }
        
    if (!xs_fread_str(inFile, psidH.sidName, sizeof(psidH.sidName)) ||
        !xs_fread_str(inFile, psidH.sidAuthor, sizeof(psidH.sidAuthor)) ||
        !xs_fread_str(inFile, psidH.sidCopyright, sizeof(psidH.sidCopyright)))
    {
        xs_error("Error reading SID file header from '%s'\n", filename);
        goto error;
    }
    
    /* Check if we need to load PSIDv2NG header ... */
    psidH2.flags = 0;    /* Just silence a stupid gcc warning */
    
    if (psidH.version == 2 || psidH.version == 3)
    {
        /* Yes, we need to */
        if (!xs_fread_be16(inFile, &psidH2.flags) ||
            !xs_fread_byte(inFile, &psidH2.startPage) ||
            !xs_fread_byte(inFile, &psidH2.startPage) ||
            !xs_fread_be16(inFile, &psidH2.reserved))
        {
            xs_error("Error reading PSID/RSID v2+ extra header data from '%s'\n",
                filename);
            goto error;
        }
    }

    /* Allocate buffer */
    if ((songData = (guint8 *) g_malloc(XS_SIDBUF_SIZE)) == NULL)
    {
        xs_error("Error allocating temp data buffer for file '%s'\n", filename);
        goto error;
    }

    /* Read data to buffer */
    result = xs_fread(songData, sizeof(guint8), XS_SIDBUF_SIZE, inFile);
    xs_fclose(inFile);

    /* Initialize and start MD5-hash calculation */
    xs_md5_init(&inState);

    if (psidH.loadAddress == 0)
    {
        /* Strip load address (2 first bytes) */
        xs_md5_append(&inState, &songData[2], result - 2);
    }
    else
    {
        /* Append "as is" */
        xs_md5_append(&inState, songData, result);
    }

    /* Free buffer */
    g_free(songData);

    /* Append header data to hash */
#define XSADDHASH(QDATAB) do {			\
    ib8[0] = (QDATAB & 0xff);			\
    ib8[1] = (QDATAB >> 8);			\
    xs_md5_append(&inState, (guint8 *) &ib8, sizeof(ib8));    \
    } while (0)

    XSADDHASH(psidH.initAddress);
    XSADDHASH(psidH.playAddress);
    XSADDHASH(psidH.nSongs);
#undef XSADDHASH

    /* Append song speed data to hash */
    i8 = 0;
    for (index = 0; index < psidH.nSongs && index < 32; index++)
    {
        i8 = (psidH.speed & (1 << index)) ? 60 : 0;
        xs_md5_append(&inState, &i8, sizeof(i8));
    }

    /* Rest of songs (more than 32) */
    for (index = 32; index < psidH.nSongs; index++)
        xs_md5_append(&inState, &i8, sizeof(i8));

    /* PSIDv2NG specific */
    if (psidH.version == 2 || psidH.version == 3)
    {
        /* SEE SIDPLAY HEADERS FOR INFO */
        i8 = (psidH2.flags >> 2) & 3;
        if (i8 == 2)
            xs_md5_append(&inState, &i8, sizeof(i8));
    }

    /* Calculate the hash */
    xs_md5_finish(&inState, hash);

    return 0;

error:
    if (inFile != NULL)
        xs_fclose(inFile);
    g_free(songData);
    return -1;
}


/* Get node from db index via binary search
 */
XSSLDBNode *xs_sldb_get(XSSLDB *db, const gchar *filename)
{
    XSSLDBNode keyItem, *key, **item;

    /* Check the database pointers */
    if (!db || !db->nodes || !db->pindex)
        return NULL;

    /* Get the hash and then look up from db */
    if (xs_get_sid_hash(filename, keyItem.md5Hash) == 0)
    {
        key = &keyItem;
        item = bsearch(&key, db->pindex, db->n,
            sizeof(db->pindex[0]), xs_sldb_cmp);
        
        if (item == NULL)
        {
            gint i;
            xs_error("No matching hash in SLDB: %s\n", filename);
            for (i = 0; i < XS_MD5HASH_LENGTH; i++)
                fprintf(stderr, "%02x", keyItem.md5Hash[i]);
            fprintf(stderr, "\n");
        }
        return (item != NULL) ? *item : NULL;
    }
    else
        return NULL;
}