view src/madplug/plugin.c @ 2341:59addab003d7

- reworked replaygain to use individual pre-gain for the files with RG info and the rest. - default pre-gain have been changed to +6dB for with RG, +0dB for the rest. - new clipping prevention feature using track peak information has been implemented. - reworked preferences dialog. widgets have been categorized by function and all changes will take effect immediately. and also, cancel button can reverts all changes have been done in the current session. - some keys in preferences have been changed.
author Yoshiki Yazawa <yaz@cc.rim.or.jp>
date Thu, 31 Jan 2008 15:22:15 +0900
parents 47d7a45b26a0
children fd8271f07747
line wrap: on
line source

/*
 * mad plugin for audacious
 * Copyright (C) 2005-2007 William Pitcock, Yoshiki Yazawa, Eugene Zagidullin
 *
 * Portions derived from xmms-mad:
 * Copyright (C) 2001-2002 Sam Clegg - See COPYING
 *
 * 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; under version 2 of the License.
 *
 * 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., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
 */

/* #define AUD_DEBUG 1 */

#include "config.h"
#include "plugin.h"
#include "input.h"

#include <math.h>

#include <gtk/gtk.h>
#include <audacious/util.h>
#include <audacious/configdb.h>
#include <stdarg.h>
#include <fcntl.h>
#include <audacious/vfs.h>
#include <sys/stat.h>
#include "SFMT.h"
#include "tuple.h"

/*
 * Global variables
 */
audmad_config_t *audmad_config;   /**< global configuration */
GMutex *mad_mutex;
GMutex *pb_mutex;
GCond *mad_cond;

/*
 * static variables
 */
static GThread *decode_thread; /**< the single decoder thread */
static struct mad_info_t info;   /**< info for current track */

#ifndef NOGUI
static GtkWidget *error_dialog = 0;
#endif

extern gboolean scan_file(struct mad_info_t *info, gboolean fast);

static gint mp3_bitrate_table[5][16] = {
  { 0, 32, 64, 96, 128, 160, 192, 224, 256, 288, 320, 352, 384, 416, 448, -1 }, /* MPEG1 L1 */
  { 0, 32, 48, 56,  64,  80,  96, 112, 128, 160, 192, 224, 256, 320, 384, -1 }, /* MPEG1 L2 */
  { 0, 32, 40, 48,  56,  64,  80,  96, 112, 128, 160, 192, 224, 256, 320, -1 }, /* MPEG1 L3 */
  { 0, 32, 48, 56,  64,  80,  96, 112, 128, 144, 160, 176, 192, 224, 256, -1 }, /* MPEG2(.5) L1 */
  { 0,  8, 16, 24,  32,  40,  48,  56,  64,  80,  96, 112, 128, 144, 160, -1 }  /* MPEG2(.5) L2,L3 */
};

static gint mp3_samplerate_table[4][4] = {
  { 11025, 12000, 8000, -1 },   /* MPEG2.5 */
  { -1, -1, -1, -1 },           /* Reserved */
  { 22050, 24000, 16000, -1 },  /* MPEG2 */
  { 44100, 48000, 32000, -1 }   /* MPEG1 */
};

/*
 * Function extname (filename)
 *
 *    Return pointer within filename to its extenstion, or NULL if
 *    filename has no extension.
 *
 */
static gchar *
extname(const char *filename)
{
    gchar *ext = strrchr(filename, '.');

    if (ext != NULL)
        ++ext;

    return ext;
}


void
audmad_config_compute(audmad_config_t *config)
{
    /* set some config parameters by parsing text fields
       (RG default gain, etc..)
     */
    const gchar *text;
    gdouble x;

    text = config->replaygain.preamp0_db;
    if ( text != NULL )
      x = g_strtod(text, NULL);
    else
      x = 0;
    config->replaygain.preamp0_scale = (x != 0) ? pow(10.0, x / 20) : 1;
    AUDDBG("RG.preamp0=[%s] -> %g  -> %g\n", text, x, config->preamp0_scale);

    text = config->replaygain.preamp1_db;
    if ( text != NULL )
      x = g_strtod(text, NULL);
    else
      x = 0;
    config->replaygain.preamp1_scale = (x != 0) ? pow(10.0, x / 20) : 1;
    AUDDBG("RG.preamp1=[%s] -> %g  -> %g\n", text, x,
              config->replaygain.preamp1_scale);

    text = config->replaygain.preamp2_db;
    if ( text != NULL )
      x = g_strtod(text, NULL);
    else
      x = 0;
    config->replaygain.preamp2_scale = (x != 0) ? pow(10.0, x / 20) : 1;
    AUDDBG("RG.preamp2=[%s] -> %g  -> %g\n", text, x,
              config->replaygain.preamp2_scale);
}

static void
audmad_init()
{
    ConfigDb *db = NULL;

    audmad_config = g_malloc0(sizeof(audmad_config_t));

    audmad_config->dither = TRUE;
    audmad_config->force_reopen_audio = FALSE;
    audmad_config->fast_play_time_calc = TRUE;
    audmad_config->use_xing = TRUE;
    audmad_config->sjis = FALSE;
    audmad_config->show_avg_vbr_bitrate = TRUE;
    audmad_config->replaygain.enable = TRUE;
    audmad_config->replaygain.track_mode = FALSE;
    audmad_config->replaygain.anti_clip = FALSE;
    audmad_config->replaygain.hard_limit = FALSE;
    audmad_config->title_override = FALSE;


    db = aud_cfg_db_open();
    if (db) {
        //audio
        aud_cfg_db_get_bool(db, "MAD", "dither", &audmad_config->dither);
        aud_cfg_db_get_bool(db, "MAD", "force_reopen_audio",
                            &audmad_config->force_reopen_audio);

        //metadata
        aud_cfg_db_get_bool(db, "MAD", "fast_play_time_calc",
                            &audmad_config->fast_play_time_calc);
        aud_cfg_db_get_bool(db, "MAD", "use_xing",
                            &audmad_config->use_xing);
        aud_cfg_db_get_bool(db, "MAD", "sjis", &audmad_config->sjis);

        //misc
        aud_cfg_db_get_bool(db, "MAD", "show_avg_vbr_bitrate",
                            &audmad_config->show_avg_vbr_bitrate);

        //gain control
        aud_cfg_db_get_string(db, "MAD", "RG.preamp0_db",
                              &audmad_config->replaygain.preamp0_db);
        aud_cfg_db_get_bool(db, "MAD", "RG.enable",
                            &audmad_config->replaygain.enable);
        aud_cfg_db_get_bool(db, "MAD", "RG.track_mode",
                            &audmad_config->replaygain.track_mode);
        aud_cfg_db_get_string(db, "MAD", "RG.preamp1_db",
                              &audmad_config->replaygain.preamp1_db);
        aud_cfg_db_get_string(db, "MAD", "RG.preamp2_db",
                              &audmad_config->replaygain.preamp2_db);
        aud_cfg_db_get_bool(db, "MAD", "RG.anti_clip",
                            &audmad_config->replaygain.anti_clip);
        aud_cfg_db_get_bool(db, "MAD", "RG.hard_limit",
                            &audmad_config->replaygain.hard_limit);

        //text
        aud_cfg_db_get_bool(db, "MAD", "title_override",
                            &audmad_config->title_override);
        aud_cfg_db_get_string(db, "MAD", "id3_format",
                              &audmad_config->id3_format);

        aud_cfg_db_close(db);
    }

    mad_mutex = g_mutex_new();
    pb_mutex = g_mutex_new();
    mad_cond = g_cond_new();
    audmad_config_compute(audmad_config);

    if (!audmad_config->replaygain.preamp0_db)
        audmad_config->replaygain.preamp0_db = g_strdup("+0.00");

    if (!audmad_config->replaygain.preamp1_db)
        audmad_config->replaygain.preamp1_db = g_strdup("+6.00");
    if (!audmad_config->replaygain.preamp2_db)
        audmad_config->replaygain.preamp2_db = g_strdup("+0.00");

    if (!audmad_config->id3_format)
        audmad_config->id3_format = g_strdup("(none)");

    init_gen_rand(4357);

    aud_mime_set_plugin("audio/mpeg", mad_plugin);
}

static void
audmad_cleanup()
{
    g_free(audmad_config->replaygain.preamp0_db);
    g_free(audmad_config->replaygain.preamp1_db);
    g_free(audmad_config->replaygain.preamp2_db);
    g_free(audmad_config->id3_format);
    g_free(audmad_config);
    
    g_cond_free(mad_cond);
    g_mutex_free(mad_mutex);
    g_mutex_free(pb_mutex);
}

static gboolean
mp3_head_check(guint32 head, gint *frameSize)
{
    gint version, layer, bitIndex, bitRate, sampleIndex, sampleRate, padding;

    /* http://www.mp3-tech.org/programmer/frame_header.html
     * Bits 21-31 must be set (frame sync)
     */
    if ((head & 0xffe00000) != 0xffe00000)
        return FALSE;

    /* check if layer bits (17-18) are good */
    layer = (head >> 17) & 0x3;
    if (!layer)
        return FALSE; /* 00 = reserved */
    layer = 4 - layer;

    /* check if bitrate index bits (12-15) are acceptable */
    bitIndex = (head >> 12) & 0xf;

    /* 1111 and 0000 are reserved values for all layers */
    if (bitIndex == 0xf || bitIndex == 0)
        return FALSE;

    /* check samplerate index bits (10-11) */
    sampleIndex = (head >> 10) & 0x3;
    if (sampleIndex == 0x3)
        return FALSE;

    /* check version bits (19-20) and get bitRate */
    version = (head >> 19) & 0x03;
    switch (version) {
        case 0: /* 00 = MPEG Version 2.5 */
        case 2: /* 10 = MPEG Version 2 */
            if (layer == 1)
                bitRate = mp3_bitrate_table[3][bitIndex];
            else
                bitRate = mp3_bitrate_table[4][bitIndex];
            break;

        case 1: /* 01 = reserved */
            return FALSE;

        case 3: /* 11 = MPEG Version 1 */
            bitRate = mp3_bitrate_table[layer][bitIndex];
            break;

        default:
            return FALSE;
    }

    /* check layer II restrictions vs. bitrate */
    if (layer == 2) {
        gint chanMode = (head >> 6) & 0x3;

        if (chanMode == 0x3) {
            /* single channel with bitrate > 192 */
            if (bitRate > 192)
                return FALSE;
        } else {
            /* any other mode with bitrates 32-56 and 80.
             * NOTICE! this check is not entirely correct, but I think
             * it is sufficient in most cases.
             */
            if (((bitRate >= 32 && bitRate <= 56) || bitRate == 80))
                return FALSE;
        }
    }

    /* calculate approx. frame size */
    padding = (head >> 9) & 1;
    sampleRate = mp3_samplerate_table[version][sampleIndex];
    if (layer == 1)
        *frameSize = ((12 * bitRate * 1000 / sampleRate) + padding) * 4;
    else
        *frameSize = (144 * bitRate * 1000) / (sampleRate + padding);

    /* check if bits 16 - 19 are all set (MPEG 1 Layer I, not protected?) */
    if (((head >> 19) & 1) == 1 &&
        ((head >> 17) & 3) == 3 && ((head >> 16) & 1) == 1)
        return FALSE;

    /* not sure why we check this, but ok! */
    if ((head & 0xffff0000) == 0xfffe0000)
        return FALSE;

    return TRUE;
}

static int
mp3_head_convert(const guchar * hbuf)
{
    return ((unsigned long) hbuf[0] << 24) |
        ((unsigned long) hbuf[1] << 16) |
        ((unsigned long) hbuf[2] << 8) | (unsigned long) hbuf[3];
}

// audacious vfs fast version
static int
audmad_is_our_fd(char *filename, VFSFile *fin)
{
    guint32 check;
    gchar *ext = extname(filename);
    gint cyc = 0, chkcount = 0, chksize = 4096;
    guchar buf[4];
    guchar tmp[4096];
    gint ret, i, frameSize;

    info.remote = aud_vfs_is_remote(filename);

    /* I've seen some flac files beginning with id3 frames..
       so let's exclude known non-mp3 filename extensions */
    if ((ext != NULL) &&
        (!strcasecmp("flac", ext) || !strcasecmp("mpc", ext) ||
         !strcasecmp("tta", ext)  || !strcasecmp("ogg", ext) ||
         !strcasecmp("wma", ext) )
        )
        return 0;

    if (fin == NULL) {
        g_message("fin = NULL");
        return 0;
    }

    if(aud_vfs_fread(buf, 1, 4, fin) == 0) {
        gchar *tmp = g_filename_to_utf8(filename, -1, NULL, NULL, NULL);
        g_message("aud_vfs_fread failed @1 %s", tmp);
        g_free(tmp);
        return 0;
    }

    check = mp3_head_convert(buf);

    if (memcmp(buf, "ID3", 3) == 0)
        return 1;
    else if (memcmp(buf, "OggS", 4) == 0)
        return 0;
    else if (memcmp(buf, "RIFF", 4) == 0)
    {
        aud_vfs_fseek(fin, 4, SEEK_CUR);
        if(aud_vfs_fread(buf, 1, 4, fin) == 0) {
            gchar *tmp = g_filename_to_utf8(filename, -1, NULL, NULL, NULL);
            g_message("aud_vfs_fread failed @2 %s", tmp);
            g_free(tmp);
            return 0;
        }

        if (memcmp(buf, "RMP3", 4) == 0)
            return 1;
    }

    // check data for frame header
    while (!mp3_head_check(check, &frameSize))
    {
        if((ret = aud_vfs_fread(tmp, 1, chksize, fin)) == 0){
            gchar *tmp = g_filename_to_utf8(filename, -1, NULL, NULL, NULL);
            g_message("aud_vfs_fread failed @3 %s", tmp);
            g_free(tmp);
            return 0;
        }
        for (i = 0; i < ret; i++)
        {
            check <<= 8;
            check |= tmp[i];

            if (mp3_head_check(check, &frameSize)) {
                /* when the first matching frame header is found, we check for
                 * another frame by seeking to the approximate start of the
                 * next header ... also reduce the check size.
                 */
                if (++chkcount >= 3) return 1;
                aud_vfs_fseek(fin, frameSize-4, SEEK_CUR);
                check = 0;
                chksize = 8;
            }
        }

        if (++cyc > 32)
            return 0;
    }

    return 1;
}

// audacious vfs version
static int
audmad_is_our_file(char *filename)
{
    VFSFile *fin = NULL;
    gint rtn;

    fin = aud_vfs_fopen(filename, "rb");

    if (fin == NULL)
        return 0;

    rtn = audmad_is_our_fd(filename, fin);
    aud_vfs_fclose(fin);

    return rtn;
}

static void
audmad_stop(InputPlayback *playback)
{
    AUDDBG("f: audmad_stop\n");
    g_mutex_lock(mad_mutex);
    info.playback = playback;
    g_mutex_unlock(mad_mutex);

    if (decode_thread) {

        g_mutex_lock(mad_mutex);
        info.playback->playing = 0;
        g_mutex_unlock(mad_mutex);
        g_cond_signal(mad_cond);

        AUDDBG("waiting for thread\n");
        g_thread_join(decode_thread);
        AUDDBG("thread done\n");

        input_term(&info);
        decode_thread = NULL;

    }
    AUDDBG("e: audmad_stop\n");
}

static void
audmad_play_file(InputPlayback *playback)
{
    gboolean rtn;
    gchar *url = playback->filename;

#ifdef AUD_DEBUG
    {
        gchar *tmp = g_filename_to_utf8(url, -1, NULL, NULL, NULL);
        AUDDBG("playing %s\n", tmp);
        g_free(tmp);
    }
#endif                          /* DEBUG */

    if (input_init(&info, url, NULL) == FALSE) {
        g_message("error initialising input");
        return;
    }

    // remote access must use fast scan.
    rtn = input_get_info(&info, aud_vfs_is_remote(url) ? TRUE : audmad_config->fast_play_time_calc);

    if (rtn == FALSE) {
        g_message("error reading input info");
        /*
         * return;
         * commenting this return seems to be a hacky fix for the damn lastfm plugin playback
         * that used to work only for nenolod because of his fsck-ing lastfm subscription :p
        */
    }
    g_mutex_lock(pb_mutex);
    info.playback = playback;
    info.playback->playing = 1;
    g_mutex_unlock(pb_mutex);

    decode_thread = g_thread_self();
    playback->set_pb_ready(playback);
    decode_loop(&info);
}

static void
audmad_pause(InputPlayback *playback, short paused)
{
    g_mutex_lock(pb_mutex);
    info.playback = playback;
    g_mutex_unlock(pb_mutex);
    playback->output->pause(paused);
}

static void
audmad_mseek(InputPlayback *playback, gulong millisecond)
{
    g_mutex_lock(pb_mutex);
    info.playback = playback;
    info.seek = millisecond;
    g_mutex_unlock(pb_mutex);
}

static void
audmad_seek(InputPlayback *playback, gint time)
{
    audmad_mseek(playback, time * 1000);
}

/**
 * Scan the given file or URL.
 * Fills in the title string and the track length in milliseconds.
 */
static void
audmad_get_song_info(char *url, char **title, int *length)
{
    struct mad_info_t myinfo;
#ifdef AUD_DEBUG
    gchar *tmp = g_filename_to_utf8(url, -1, NULL, NULL, NULL);
    AUDDBG("f: audmad_get_song_info: %s\n", tmp);
    g_free(tmp);
#endif                          /* DEBUG */

    if (input_init(&myinfo, url, NULL) == FALSE) {
        AUDDBG("error initialising input\n");
        return;
    }

    if (input_get_info(&myinfo, info.remote ? TRUE : audmad_config->fast_play_time_calc) == TRUE) {
        if(aud_tuple_get_string(myinfo.tuple, -1, "track-name"))
            *title = g_strdup(aud_tuple_get_string(myinfo.tuple, -1, "track-name"));
        else
            *title = g_strdup(url);

        *length = aud_tuple_get_int(myinfo.tuple, FIELD_LENGTH, NULL);
        if(*length == -1)
            *length = mad_timer_count(myinfo.duration, MAD_UNITS_MILLISECONDS);
    }
    else {
        *title = g_strdup(url);
        *length = -1;
    }
    input_term(&myinfo);
    AUDDBG("e: audmad_get_song_info\n");
}

static gboolean
audmad_fill_info(struct mad_info_t *info, VFSFile *fd)
{
    if (fd == NULL || info == NULL) return FALSE;
    AUDDBG("f: audmad_fill_info(): %s\n", fd->uri);

    if (input_init(info, fd->uri, fd) == FALSE) {
        AUDDBG("audmad_fill_info(): error initialising input\n");
        return FALSE;
    }
    
    info->fileinfo_request = FALSE; /* we don't need to read tuple again */
    return input_get_info(info, aud_vfs_is_remote(fd->uri) ? TRUE : audmad_config->fast_play_time_calc);
}

static void
audmad_about()
{
    static GtkWidget *aboutbox;
    gchar *scratch;

    if (aboutbox != NULL)
        return;

    scratch = g_strdup_printf(
    _("Audacious MPEG Audio Plugin\n"
    "\n"
    "Compiled against libMAD version: %d.%d.%d%s\n"
    "\n"
    "Written by:\n"
    "    William Pitcock <nenolod@sacredspiral.co.uk>\n"
    "    Yoshiki Yazawa <yaz@cc.rim.or.jp>\n"
    "\n"
    "Portions derived from XMMS-MAD by:\n"
    "    Sam Clegg\n"
    "\n"
    "ReplayGain support by:\n"
    "    Samuel Krempp"),
    MAD_VERSION_MAJOR, MAD_VERSION_MINOR, MAD_VERSION_PATCH,
    MAD_VERSION_EXTRA);

    aboutbox = audacious_info_dialog(_("About MPEG Audio Plugin"),
                                 scratch,
                                 _("Ok"), FALSE, NULL, NULL);

    g_free(scratch);

    g_signal_connect(G_OBJECT(aboutbox), "destroy",
                     G_CALLBACK(gtk_widget_destroyed), &aboutbox);
}

/**
 * Display a GTK box containing the given error message.
 * Taken from mpg123 plugin.
 */
void
audmad_error(char *error, ...)
{
#ifndef NOGUI
    if (!error_dialog) {
        va_list args;
        char string[256];
        va_start(args, error);
        vsnprintf(string, 256, error, args);
        va_end(args);
        GDK_THREADS_ENTER();
        error_dialog =
            audacious_info_dialog(_("Error"), string, _("Ok"), FALSE, 0, 0);
        gtk_signal_connect(GTK_OBJECT(error_dialog), "destroy",
                           GTK_SIGNAL_FUNC(gtk_widget_destroyed),
                           &error_dialog);
        GDK_THREADS_LEAVE();
    }
#endif                          /* !NOGUI */
}

extern void audmad_configure();

static void
__set_and_free(Tuple *tuple, gint nfield, gchar *name, gchar *value)
{
    aud_tuple_associate_string(tuple, nfield, name, value);
    g_free(value);
}

// tuple stuff
static Tuple *
__audmad_get_song_tuple(char *filename, VFSFile *fd)
{
    Tuple *tuple = NULL;
    gchar *string = NULL;

    struct id3_file *id3file = NULL;
    struct id3_tag *tag = NULL;

    struct mad_info_t myinfo;

    gboolean local_fd = FALSE;
    int length;

#ifdef AUD_DEBUG
    string = aud_str_to_utf8(filename);
    AUDDBG("f: mad: audmad_get_song_tuple: %s\n", string);
    g_free(string);
    string = NULL;
#endif

    /* isn't is obfuscated? --eugene */

    if(info.remote && mad_timer_count(info.duration, MAD_UNITS_SECONDS) <= 0){
        if((fd && aud_vfs_is_streaming(fd)) || (info.playback && info.playback->playing)) {
            gchar *tmp = NULL;
            tuple = aud_tuple_new_from_filename(filename);

#ifdef AUD_DEBUG
            if(info.playback)
                AUDDBG("info.playback->playing = %d\n",info.playback->playing);
#endif
            tmp = aud_vfs_get_metadata(info.infile ? info.infile : fd, "track-name");
            if(tmp){
                gchar *scratch;

                scratch = aud_str_to_utf8(tmp);
                aud_tuple_associate_string(tuple, FIELD_TITLE, NULL, scratch);
                g_free(tmp);
                g_free(scratch);

                tmp = NULL;
            }
            tmp = aud_vfs_get_metadata(info.infile ? info.infile : fd, "stream-name");
            if(tmp){
                gchar *scratch;

                scratch = aud_str_to_utf8(tmp);
                aud_tuple_associate_string(tuple, FIELD_TITLE, NULL, scratch);
                g_free(tmp);
                g_free(scratch);

                tmp = NULL;
            }

            AUDDBG("audmad_get_song_tuple: track_name = %s\n", aud_tuple_get_string(tuple, -1, "track-name"));
            AUDDBG("audmad_get_song_tuple: stream_name = %s\n", aud_tuple_get_string(tuple, -1, "stream-name"));
            aud_tuple_associate_int(tuple, FIELD_LENGTH, NULL, -1);
            aud_tuple_associate_int(tuple, FIELD_MTIME, NULL, 0); // this indicates streaming
            AUDDBG("get_song_tuple: remote: tuple\n");
            return tuple;
        }
        AUDDBG("get_song_tuple: remote: NULL\n");
    } /* info.remote  */

    // if !fd, pre-open the file with aud_vfs_fopen() and reuse fd.
    if(!fd) {
        fd = aud_vfs_fopen(filename, "rb");
        if(!fd)
            return NULL;
        local_fd = TRUE;
    }

    if (!audmad_fill_info(&myinfo, fd)) {
        AUDDBG("get_song_tuple: error obtaining info\n");
        if (local_fd) aud_vfs_fclose(fd);
        return NULL;
    }

    tuple = aud_tuple_new();
    aud_tuple_associate_int(tuple, FIELD_LENGTH, NULL, -1);

    id3file = id3_file_vfsopen(fd, ID3_FILE_MODE_READONLY);

    if (id3file) {

        tag = id3_file_tag(id3file);
        if (tag) {
            __set_and_free(tuple, FIELD_ARTIST, NULL, input_id3_get_string(tag, ID3_FRAME_ARTIST));
            __set_and_free(tuple, FIELD_ALBUM, NULL, input_id3_get_string(tag, ID3_FRAME_ALBUM));
            __set_and_free(tuple, FIELD_TITLE, NULL, input_id3_get_string(tag, ID3_FRAME_TITLE));

            // year
            string = NULL;
            string = input_id3_get_string(tag, ID3_FRAME_YEAR); //TDRC
            if (!string)
                string = input_id3_get_string(tag, "TYER");

            if (string) {
                aud_tuple_associate_int(tuple, FIELD_YEAR, NULL, atoi(string));
                g_free(string);
                string = NULL;
            }

            __set_and_free(tuple, FIELD_FILE_NAME, NULL, aud_uri_to_display_basename(filename));
            __set_and_free(tuple, FIELD_FILE_PATH, NULL, aud_uri_to_display_dirname(filename));
            aud_tuple_associate_string(tuple, FIELD_FILE_EXT, NULL, extname(filename));

            // length
            length = mad_timer_count(myinfo.duration, MAD_UNITS_MILLISECONDS);
            aud_tuple_associate_int(tuple, FIELD_LENGTH, NULL, length);

            // track number
            string = input_id3_get_string(tag, ID3_FRAME_TRACK);
            if (string) {
                aud_tuple_associate_int(tuple, FIELD_TRACK_NUMBER, NULL, atoi(string));
                g_free(string);
                string = NULL;
            }
            // genre
            __set_and_free(tuple, FIELD_GENRE, NULL, input_id3_get_string(tag, ID3_FRAME_GENRE));
            __set_and_free(tuple, FIELD_COMMENT, NULL, input_id3_get_string(tag, ID3_FRAME_COMMENT));
            AUDDBG("genre = %s\n", aud_tuple_get_string(tuple, FIELD_GENRE, NULL));
        }
        id3_file_close(id3file);
    } // id3file
    else { // no id3tag
        __set_and_free(tuple, FIELD_FILE_NAME, NULL, aud_uri_to_display_basename(filename));
        __set_and_free(tuple, FIELD_FILE_PATH, NULL, aud_uri_to_display_dirname(filename));
        aud_tuple_associate_string(tuple, FIELD_FILE_EXT, NULL, extname(filename));
        // length
        length = mad_timer_count(myinfo.duration, MAD_UNITS_MILLISECONDS);
        aud_tuple_associate_int(tuple, FIELD_LENGTH, NULL, length);
    }

    aud_tuple_associate_string(tuple, FIELD_QUALITY, NULL, "lossy");
    aud_tuple_associate_int(tuple, FIELD_BITRATE, NULL, myinfo.bitrate / 1000);

    string = g_strdup_printf("MPEG-1 Audio Layer %d", myinfo.mpeg_layer);
    aud_tuple_associate_string(tuple, FIELD_CODEC, NULL, string);
    g_free(string);

    aud_tuple_associate_string(tuple, FIELD_MIMETYPE, NULL, "audio/mpeg");
    
    input_term(&myinfo);

    if(local_fd)
        aud_vfs_fclose(fd);

    AUDDBG("e: mad: audmad_get_song_tuple\n");
    return tuple;
}

static Tuple *
audmad_get_song_tuple(char *filename)
{
    return __audmad_get_song_tuple(filename, NULL);
}

static Tuple *
audmad_probe_for_tuple(char *filename, VFSFile *fd)
{
    if (!audmad_is_our_fd(filename, fd))
        return NULL;

    aud_vfs_rewind(fd);

    return __audmad_get_song_tuple(filename, fd);
}

static gchar *fmts[] = { "mp3", "mp2", "mpg", "bmu", NULL };

InputPlugin mad_ip = {
    .description = "MPEG Audio Plugin",
    .init = audmad_init,
    .about = audmad_about,
    .configure = audmad_configure,
    .is_our_file = audmad_is_our_file,
    .play_file = audmad_play_file,
    .stop = audmad_stop,
    .pause = audmad_pause,
    .seek = audmad_seek,
    .cleanup = audmad_cleanup,
    .get_song_info = audmad_get_song_info,
    .get_song_tuple = audmad_get_song_tuple,
    .is_our_file_from_vfs = audmad_is_our_fd,
    .vfs_extensions = fmts,
    .mseek = audmad_mseek,
    .probe_for_tuple = audmad_probe_for_tuple,
    .update_song_tuple = audmad_update_song_tuple,
};

InputPlugin *madplug_iplist[] = { &mad_ip, NULL };

SIMPLE_INPUT_PLUGIN(madplug, madplug_iplist);

InputPlugin *mad_plugin = &mad_ip;