view src/madplug/plugin.c @ 972:cf7021ca4e7b trunk

[svn] Add lastfm:// transport, an abstract VFS class which derives from curl to provide lastfm radio support. Written by majeru with some cleanups by me. Most last.fm metadata support isn't yet implemented, however, and will need to be done by majeru. ;)
author nenolod
date Sun, 22 Apr 2007 04:16:08 -0700
parents 7e14701aef54
children bdf6ccf7bf53
line wrap: on
line source

/*
 * mad plugin for audacious
 * Copyright (C) 2005-2007 William Pitcock, Yoshiki Yazawa
 *
 * 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
 */

#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"

/*
 * Global variables
 */
struct audmad_config_t audmad_config;   /**< global configuration */
InputPlugin *mad_plugin = NULL;
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(struct audmad_config_t *config)
{
    /* set some config parameters by parsing text fields 
       (RG default gain, etc..)
     */
    const gchar *text;
    gdouble x;

    text = config->pregain_db;
    if ( text != NULL )
      x = g_strtod(text, NULL);
    else
      x = 0;
    config->pregain_scale = (x != 0) ? pow(10.0, x / 20) : 1;
#ifdef DEBUG
    g_message("pregain=[%s] -> %g  -> %g", text, x, config->pregain_scale);
#endif
    text = config->replaygain.default_db;
    if ( text != NULL )
      x = g_strtod(text, NULL);
    else
      x = 0;
    config->replaygain.default_scale = (x != 0) ? pow(10.0, x / 20) : 1;
#ifdef DEBUG
    g_message("RG.default=[%s] -> %g  -> %g", text, x,
              config->replaygain.default_scale);
#endif
}

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

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

    db = bmp_cfg_db_open();
    if (db) {
        bmp_cfg_db_get_bool(db, "MAD", "fast_play_time_calc",
                            &audmad_config.fast_play_time_calc);
        bmp_cfg_db_get_bool(db, "MAD", "use_xing",
                            &audmad_config.use_xing);
        bmp_cfg_db_get_bool(db, "MAD", "dither", &audmad_config.dither);
        bmp_cfg_db_get_bool(db, "MAD", "sjis", &audmad_config.sjis);
        bmp_cfg_db_get_bool(db, "MAD", "hard_limit",
                            &audmad_config.hard_limit);
        bmp_cfg_db_get_string(db, "MAD", "pregain_db",
                              &audmad_config.pregain_db);
        bmp_cfg_db_get_bool(db, "MAD", "RG.enable",
                            &audmad_config.replaygain.enable);
        bmp_cfg_db_get_bool(db, "MAD", "RG.track_mode",
                            &audmad_config.replaygain.track_mode);
        bmp_cfg_db_get_string(db, "MAD", "RG.default_db",
                              &audmad_config.replaygain.default_db);
        bmp_cfg_db_get_bool(db, "MAD", "title_override",
                            &audmad_config.title_override);
        bmp_cfg_db_get_string(db, "MAD", "id3_format",
                              &audmad_config.id3_format);
        bmp_cfg_db_get_bool(db, "MAD", "show_avg_vbr_bitrate",
                            &audmad_config.show_avg_vbr_bitrate);
        bmp_cfg_db_get_bool(db, "MAD", "force_reopen_audio",
                            &audmad_config.force_reopen_audio);

        bmp_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.pregain_db)
        audmad_config.pregain_db = g_strdup("+0.00");

    if (!audmad_config.replaygain.default_db)
        audmad_config.replaygain.default_db = g_strdup("-9.00");

    if (!audmad_config.id3_format)
        audmad_config.id3_format = g_strdup("");

    init_gen_rand(4357);

}

static void audmad_cleanup()
{
    g_free(audmad_config.pregain_db);
    g_free(audmad_config.replaygain.default_db);
    g_free(audmad_config.id3_format);

    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];
}

gboolean audmad_is_remote(gchar *url)
{
    if (!strncasecmp("http://", url, 7) || !strncasecmp("https://", url, 8))
        return TRUE;
    else
        return FALSE;
}

// 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 = FALSE;

    if(audmad_is_remote(filename))
        info.remote = TRUE;

    /* 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(vfs_fread(buf, 1, 4, fin) == 0) {
        gchar *tmp = g_filename_to_utf8(filename, -1, NULL, NULL, NULL);
        g_message("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)
    {
        vfs_fseek(fin, 4, SEEK_CUR);
        if(vfs_fread(buf, 1, 4, fin) == 0) {
            gchar *tmp = g_filename_to_utf8(filename, -1, NULL, NULL, NULL);            
            g_message("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 = vfs_fread(tmp, 1, chksize, fin)) == 0){
            gchar *tmp = g_filename_to_utf8(filename, -1, NULL, NULL, NULL);
            g_message("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;
                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 = vfs_fopen(filename, "rb");

    if (fin == NULL)
        return 0;

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

    return rtn;
}

static void audmad_stop(InputPlayback *playback)
{
#ifdef DEBUG
    g_message("f: audmad_stop");
#endif
    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);

#ifdef DEBUG
        g_message("waiting for thread");
#endif
        g_thread_join(decode_thread);
#ifdef DEBUG
        g_message("thread done");
#endif
        input_term(&info);
        decode_thread = NULL;

    }
#ifdef DEBUG
    g_message("e: audmad_stop");
#endif
}

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

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

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

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

    if (rtn == FALSE) {
        g_message("error reading input info");
        return;
    }
    g_mutex_lock(pb_mutex);
    info.playback = playback;
    info.playback->playing = 1;
    g_mutex_unlock(pb_mutex);

    decode_thread = g_thread_create(decode_loop, (void *) &info, TRUE, NULL);
}

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 DEBUG
    gchar *tmp = g_filename_to_utf8(url, -1, NULL, NULL, NULL);
    g_message("f: audmad_get_song_info: %s", tmp);
    g_free(tmp);
#endif                          /* DEBUG */

    if (input_init(&myinfo, url) == FALSE) {
#ifdef DEBUG
        g_message("error initialising input");
#endif
        return;
    }

    if (input_get_info(&myinfo, info.remote ? TRUE : audmad_config.fast_play_time_calc) == TRUE) {
        if(myinfo.tuple->track_name)
            *title = strdup(myinfo.tuple->track_name);
        else
            *title = strdup(url);
        if(myinfo.tuple->length == -1)
            *length = mad_timer_count(myinfo.duration, MAD_UNITS_MILLISECONDS);
        else
            *length = myinfo.tuple->length;
    }
    else {
        *title = strdup(url);
        *length = -1;
    }
    input_term(&myinfo);
#ifdef DEBUG
    g_message("e: audmad_get_song_info");
#endif                          /* DEBUG */
}

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 = xmms_show_message("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 =
            xmms_show_message("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_get_file_info(char *filename);
extern void audmad_configure();


// tuple stuff
static TitleInput *audmad_get_song_tuple(char *filename)
{
    TitleInput *tuple = NULL;
    gchar *string = NULL;

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

#ifdef DEBUG
    string = str_to_utf8(filename);
    g_message("f: mad: audmad_get_song_tuple: %s", string);
    g_free(string);
    string = NULL;
#endif

    if(info.remote && mad_timer_count(info.duration, MAD_UNITS_SECONDS) <= 0){
        if(info.playback && info.playback->playing) {
            gchar *tmp = NULL;
            tuple = bmp_title_input_new();
#ifdef DEBUG
            g_message("info.playback->playing = %d",info.playback->playing);
#endif
            tmp = vfs_get_metadata(info.infile, "track-name");
            if(tmp){
                tuple->track_name = str_to_utf8(tmp);
                g_free(tmp);
                tmp = NULL;
            }
            tmp = vfs_get_metadata(info.infile, "stream-name");
            if(tmp){
                tuple->album_name = str_to_utf8(tmp);
                g_free(tmp);
                tmp = NULL;
            }
#ifdef DEBUG
            g_message("audmad_get_song_tuple: track_name = %s", tuple->track_name);
            g_message("audmad_get_song_tuple: stream_name = %s", tuple->album_name);
#endif
            tuple->file_name = g_path_get_basename(filename);
            tuple->file_path = g_path_get_dirname(filename);
            tuple->file_ext = extname(filename);
            tuple->length = -1;
            tuple->mtime = 0; // this indicates streaming
#ifdef DEBUG
            g_message("get_song_tuple: remote: tuple");
#endif
            return tuple;
        }
#ifdef DEBUG
        g_message("get_song_tuple: remote: NULL");
#endif
        return NULL;
    }

    tuple = bmp_title_input_new();

    id3file = id3_file_open(filename, ID3_FILE_MODE_READONLY);
    if (id3file) {

        tag = id3_file_tag(id3file);
        if (tag) {
            tuple->performer =
                input_id3_get_string(tag, ID3_FRAME_ARTIST);
            tuple->album_name =
                input_id3_get_string(tag, ID3_FRAME_ALBUM);
            tuple->track_name =
                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) {
                tuple->year = atoi(string);
                g_free(string);
                string = NULL;
            }

            tuple->file_name = g_path_get_basename(filename);
            tuple->file_path = g_path_get_dirname(filename);
            tuple->file_ext = extname(filename);

            // length
            tuple->length = -1;
            string = input_id3_get_string(tag, "TLEN");
            if (string) {
                tuple->length = atoi(string);
#ifdef DEBUG
                g_message("get_song_tuple: TLEN = %d", tuple->length);
#endif	
                g_free(string);
                string = NULL;
            }
            else {
                char *dummy = NULL;
                int length = 0;
                audmad_get_song_info(filename, &dummy, &length);
                tuple->length = length;
                g_free(dummy);
            }

            // track number
            string = input_id3_get_string(tag, ID3_FRAME_TRACK);
            if (string) {
                tuple->track_number = atoi(string);
                g_free(string);
                string = NULL;
            }
            // genre
            tuple->genre = input_id3_get_string(tag, ID3_FRAME_GENRE);
#ifdef DEBUG
            g_message("genre = %s", tuple->genre);
#endif
            // comment
            tuple->comment =
                input_id3_get_string(tag, ID3_FRAME_COMMENT);

        }
        id3_file_close(id3file);
    }
    else { // no id3tag
        tuple->file_name = g_path_get_basename(filename);
        tuple->file_path = g_path_get_dirname(filename);
        tuple->file_ext = extname(filename);
        // length
        {
            char *dummy = NULL;
            int length = 0;
            if(tuple->length == -1) {
                audmad_get_song_info(filename, &dummy, &length);
                tuple->length = length;
            }
            g_free(dummy);
        }
    }
#ifdef DEBUG
    g_message("e: mad: audmad_get_song_tuple");
#endif
    return tuple;
}


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

InputPlugin *get_iplugin_info(void)
{
    if (mad_plugin != NULL)
        return mad_plugin;

    mad_plugin = g_new0(InputPlugin, 1);
    mad_plugin->description = g_strdup(_("MPEG Audio Plugin"));
    mad_plugin->init = audmad_init;
    mad_plugin->about = audmad_about;
    mad_plugin->configure = audmad_configure;
    mad_plugin->is_our_file = audmad_is_our_file;
    mad_plugin->play_file = audmad_play_file;
    mad_plugin->stop = audmad_stop;
    mad_plugin->pause = audmad_pause;
    mad_plugin->seek = audmad_seek;
    mad_plugin->cleanup = audmad_cleanup;
    mad_plugin->get_song_info = audmad_get_song_info;
    mad_plugin->file_info_box = audmad_get_file_info;
    mad_plugin->get_song_tuple = audmad_get_song_tuple;
    mad_plugin->is_our_file_from_vfs = audmad_is_our_fd;
    mad_plugin->vfs_extensions = fmts;
    mad_plugin->mseek = audmad_mseek;

    return mad_plugin;
}