Blob Blame History Raw
/*
 * A Snack MP3 format handler using libmpg123.
 *
 * BSD Copyright 2009 - Peter MacDonald
 *
 * Implements mp3 as a loadable module using libmpg123 (which is LGPL).
 * Replaces snacks builtin MP3 driver which:
 *
 *   - has noise major artifacts when used with -file on 48000 sound cards.
 *   - fails on small mp3 files (under 20k?).
 *   - has a restrictive (non-commercial only) licence
 *   - Can't be easily removed from snack (to avoid patent issue)
 * 
 * TODO:
 *   - Check if file changed on multiple opens (for header).
 *   - Check return codes.
 *   - Add encoding support option (ie. use lame library?).
 *
 */
#include <math.h>
#include <tcl.h>
#include "snack.h"
#include <stdlib.h>
#include <time.h>
#include <ctype.h>
#include "mpg123.h"

#if defined(__WIN32__)
#  include <io.h>
#  include <fcntl.h>
#  define WIN32_LEAN_AND_MEAN
#  include <windows.h>
#  undef WIN32_LEAN_AND_MEAN
#  define EXPORT(a,b) __declspec(dllexport) a b
BOOL APIENTRY
DllMain(HINSTANCE hInst, DWORD reason, LPVOID reserved)
{
    return TRUE;
}
#else
#  define EXPORT(a,b) a b
#endif

#ifdef __cplusplus
extern "C"
{
#endif /* __cplusplus */

#include <stdio.h>

/* #define MPG_NODIRECT_FILES 1 */
/* If above is defined we never let libmpg123 use native files directly. */

#define MPG123_STRING "MPG"
#define SNACK_MPG123_INT 21
#define DECODEBUFSIZE (10*BUFSIZ)
#define READBUFSIZE 8500

typedef struct Mpg123_File {
    mpg123_handle *m;
    int maxbitrate;
    int minbitrate;
    int nombitrate;
    double quality;
    long rate;
    int channels, enc;
    mpg123_id3v1 *v1;
    mpg123_id3v2 *v2;
    Tcl_Obj *fname, *nfname;
    struct mpg123_frameinfo fi;
    int ref;
    size_t savepos[10];
    int lastret;
    Tcl_Channel datasource;
    long ttllen;
    int isFile;
    int noFiles;
    char *chanType;
    int started;
    int opened;
    int gotformat;
    unsigned char *pcmbuf;
    int buffer_size;
    off_t current_frame;
    off_t frames_left;
    double current_seconds;
    double seconds_left;
    int seeksync;
} Mpg123_File;

#ifdef __cplusplus
}
#endif /* __cplusplus */

static int mpgIsInit = 0;


Mpg123_File * AllocMpg(Sound *s) {
    Mpg123_File *of;
    of = (Mpg123_File*) ckalloc(sizeof(Mpg123_File));
    memset(of, 0, sizeof(Mpg123_File));
    s->extHead2 = (char *) of;
    s->extHead2Type = SNACK_MPG123_INT;
    of->nombitrate = 128000;
    of->maxbitrate = -1;
    of->minbitrate = -1;
    of->quality = -1.0;
    of->seeksync = 5000;
#ifdef MPG_NODIRECT_FILES
    of->noFiles = 1;
#endif
    return of;
}

Mpg123_File *MpgObj(Sound *s) {
    Mpg123_File *of = (Mpg123_File *)s->extHead2;
    if (of == NULL) {
        of = AllocMpg(s);
    }
    return of;
}

static int guessByMagic = 1;
/* If above is 1 GuessMpg123File() looks only at header magic alone. */
/* Meaning the header bits start with 0xFFF or the string ID3 or RIFF. */
/* This avoids the more expensive decoding first chunk to see if we have mp3. */

char *
GuessMpg123File(char *buf, int len)
{
    long rate;
    int channels, enc;
    int fnd = 0, ret, done;
    mpg123_handle *m;
    unsigned char *ubuf = buf;
    unsigned char pcmout[4*sizeof(short)*20000];
    int decsiz = 4*sizeof(short)*20000;

    if (len < 4) return(QUE_STRING);
    if ((ubuf[0] == 0xff && (ubuf[1]&0xf0) == 0xf0)) {
        return MPG123_STRING;
    }
    if (buf[0] == 'I' && buf[1] == 'D' && buf[2] == '3') {
        return MPG123_STRING;
    }
    if (len > 20 && buf[20] == 0x55 &&
        toupper(buf[0]) == 'R' && toupper(buf[0]) == 'I' &&
        toupper(buf[0]) == 'F' && toupper(buf[0]) == 'F') {
        return(MP3_STRING);
    }
    if (guessByMagic) {
        return NULL;
    }
    if (!mpgIsInit) {
        mpgIsInit = 1;
        mpg123_init();
    }
    m = mpg123_new(NULL, &ret);
    if(m == NULL)
    {
        fprintf(stderr, "mp3 fail\n" );
        return NULL;
    }
    mpg123_open_feed(m);
    ret = mpg123_decode(m, buf, len, pcmout, decsiz, &done);
    if (ret != MPG123_ERR ) {
        ret = mpg123_getformat(m, &rate, &channels, &enc);
        if (channels<=0) {
            ret = MPG123_ERR;
        }
    }
    mpg123_delete(m);
    if (ret != MPG123_ERR) {
        return(MPG123_STRING);
    }
    return NULL;
}

char *
ExtMpg123File(char *s)
{
    int l1 = strlen(".mp3");
    int l2 = strlen(s);

    if (strncasecmp(".mp3", &s[l2 - l1], l1) == 0) {
        return(MPG123_STRING);
    }
    return(NULL);
}


static int
Mpg123Setup(Sound *s, Tcl_Interp *interp, Tcl_Channel ch)
{
    mpg123_handle *m;
    Mpg123_File *of;
    int ret, fd, rc;
    long mlen;
    Tcl_ChannelType *cType;

    of = MpgObj(s);
    of->isFile = 0;
    Tcl_SetChannelOption(interp, ch, "-translation", "binary");
#ifdef TCL_81_API
    Tcl_SetChannelOption(interp, ch, "-encoding", "binary");
#endif
    cType = Tcl_GetChannelType(ch);
    if (of->noFiles == 0 && of->opened) {
        of->isFile = !strcmp(cType->typeName, "file");
    }
    if (s->debug)
        fprintf(stderr, "CHANTYPE(%d,%d): %s, BUF=%d\n", of->isFile, of->noFiles, cType->typeName, DECODEBUFSIZE);

    if (!mpgIsInit) {
        mpgIsInit = 1;
        mpg123_init();
    }
    m = of->m;
    /* TODO: check file name didn't change */
    if (m != NULL) {
        /* If used with */
        if (of->ref<10) {
            if (of->isFile){
                of->savepos[of->ref] = mpg123_tell(m);
            } else {
            }
        }
        of->ref++;
    }
    if (of->isFile){
        of->fname = Tcl_NewStringObj(s->fcname, -1);
        Tcl_IncrRefCount(of->fname);
        of->nfname = Tcl_FSGetNormalizedPath(interp, of->fname);
    } else {
        of->lastret = MPG123_NEED_MORE;
    }
    of->datasource = ch;
    m = mpg123_new(NULL, &ret);
    if(m == NULL) {
        Tcl_AppendResult(interp, "Unable to create mpg123 handle: ", mpg123_plain_strerror(ret), 0);
        return TCL_ERROR;
    }
    of->m = m;
    if (of->isFile){
        if (mpg123_open(m, Tcl_GetString(of->nfname)) != MPG123_OK) {
            Tcl_AppendResult(interp, "Open mpg123 failed: ", mpg123_plain_strerror(ret), 0);
            return TCL_ERROR;
        }
        if (s->debug) mpg123_param(m, MPG123_VERBOSE, 2, 0);
        if (s->debug == 0 ) mpg123_param(m, MPG123_ADD_FLAGS, MPG123_QUIET, 0);
        /*mpg123_param(m, MPG123_ADD_FLAGS, MPG123_SEEKBUFFER, 0);
        mpg123_param(m, MPG123_ADD_FLAGS, MPG123_FUZZY, 0);
        mpg123_param(m, MPG123_REMOVE_FLAGS, MPG123_GAPLESS, 0);*/
    } else {
        mpg123_open_feed(m);
    }
    if (of->pcmbuf)  ckfree( of->pcmbuf );
    of->buffer_size = mpg123_outblock( m );
    of->pcmbuf = ckalloc( of->buffer_size );
    mlen = (long)mpg123_length(m);
    if (mlen<=0) {
        return TCL_OK;
    }
    of->gotformat = 1;
    Snack_SetLength(s, mlen);
    mpg123_info(of->m, &of->fi);
    mpg123_getformat(of->m, &of->rate, &of->channels, &of->enc);
    if (s->debug) fprintf(stderr, "MPG FORMAT: channels=%d, rate=%ld enc=0x%x\n", of->channels, of->rate, of->enc);
    Snack_SetSampleRate(s, of->rate);
    Snack_SetNumChannels(s, of->channels);
    Snack_SetSampleEncoding(s, LIN16);
    of->nombitrate = of->rate;

    rc = mpg123_id3(of->m, &of->v1, &of->v2);

    Snack_SetBytesPerSample(s, 2);
    Snack_SetHeaderSize(s, 0);
    return TCL_OK;
}

static int
OpenMpg123File(Sound *s, Tcl_Interp *interp, Tcl_Channel *ch, char *mode)
{
    mpg123_handle *m;
    Mpg123_File *of;
    int ret, fd, rc;
    long mlen;
    Tcl_ChannelType *cType;

    if (s->debug) fprintf(stderr, "MPG Open: %p : %s\n", s, s->fcname);
    *ch = Tcl_OpenFileChannel(interp, s->fcname, mode, 420);
    if (*ch == NULL) {
        Tcl_AppendResult(interp, "Mpg123: unable to open file: ",
            Snack_GetSoundFilename(s), NULL);
            return TCL_ERROR;
    }
    of = MpgObj(s);
    of->opened = 1;
    return Mpg123Setup(s, interp, *ch);
    
}

static int
FreeRes(Mpg123_File *of)
{
    if (of->fname) {
        Tcl_DecrRefCount(of->fname);
    }
    of->fname = NULL;
    of->nfname = NULL;
    of->v1 = NULL;
    of->v2 = NULL;
    if (of->m) {
        mpg123_delete(of->m);
    }
    if (of->pcmbuf) {
        ckfree(of->pcmbuf);
    }
    of->pcmbuf = NULL;
    of->m = NULL;
}

static int
CloseMpg123File(Sound *s, Tcl_Interp *interp, Tcl_Channel *ch)
{
    Mpg123_File *of;

    of = MpgObj(s);

    if (s->debug) fprintf(stderr, "MPG Close: %p\n", s);
    if (of->ref > 0 && of->m) {
        of->ref--;
        if (of->ref<10) {
            if (of->isFile){
                mpg123_seek(of->m, of->savepos[of->ref], SEEK_SET);
            }
        }
        return;
    }

    FreeRes(of);
    if (of->started == 0) {
        *ch = NULL;
    } else {
        of->started = 0;
    }
    if (ch != NULL) {
        Tcl_Close(interp, *ch);
    }
    *ch = NULL;

    return TCL_OK;
}

static int
ReadMpg123Samples(Sound *s, Tcl_Interp *interp, Tcl_Channel ch, char *ibuf,
          float *obuf, int len)
{
    Mpg123_File *of;
    int i, iread, rc, cnt, bigendian = Snack_PlatformIsLittleEndian() ? 0 : 1;
    float *f  = obuf;
    size_t done = 0, rlen, nread = 0;
    short *r;
    long bytes;
    char buffer[READBUFSIZE];

    of = MpgObj(s);
    of->started = 1;
    memset(obuf, 0, len*sizeof(float));
    /*rlen = (len<DECODEBUFSIZE?len:DECODEBUFSIZE); */
    while (len>0) {
        rlen = (len * sizeof(short));
        if (rlen>of->buffer_size) rlen = of->buffer_size;
        if (of->isFile) {
            rc = mpg123_read(of->m, of->pcmbuf, rlen, &done);
        } else {
            if (of->lastret == MPG123_NEED_MORE) {
                bytes=Tcl_Read(of->datasource, buffer, READBUFSIZE); 
                if (bytes <= 0) {
                    if (s->debug) fprintf(stderr, "MPG ERR\n");
                    return 0;
                }
                rc = mpg123_decode(of->m, buffer, bytes, of->pcmbuf, rlen, &done);
            } else {
                rc = mpg123_decode(of->m, NULL, 0, of->pcmbuf, rlen, &done);
            }
        }
        of->lastret = rc;
        if (rc == MPG123_NEW_FORMAT || !of->gotformat) {
            of->gotformat = 1;
            mpg123_getformat(of->m, &of->rate, &of->channels, &of->enc);
            if (s->debug) fprintf(stderr, "MPG FORMAT: channels=%d, rate=%ld enc=0x%x\n", of->channels, of->rate, of->enc);
            Snack_SetSampleRate(s, of->rate);
            Snack_SetNumChannels(s, of->channels);
        }
        if (rc == MPG123_DONE) {
            if (s->debug) fprintf(stderr, "MPG DONE: %d\n", nread);
            return nread;
        }
        if (rc == MPG123_ERR) {
            if (s->debug) fprintf(stderr, "MPG ERROR: %d\n", nread);
            return 0;
        }
        r = (short *) of->pcmbuf;
        cnt = (done / sizeof(short));
        for (i = 0; i < cnt; i++) {
            float fv = (float)*r;
            *f++ = fv;
            r++;
        }
        nread += cnt;
        if (s->debug) fprintf(stderr, "MPG READ (%d of %d): %d\n", nread, len, rc);
        if (cnt >= len) break;
        len -= cnt;
    }

    if (nread>0) {
        of->ttllen += nread;
        if (of->isFile == 0) {
            /* Don't know length for channels so we lie. */
            Snack_SetLength(s, of->ttllen+1);
        }
    }
    if (done < 0) {
        return 0;
    }
    if (of->isFile) {
        mpg123_position(of->m, 0, nread * sizeof(short),
            &of->current_frame, &of->frames_left, &of->current_seconds,
            &of->seconds_left);
    }

    iread = (int)nread;
    if (iread < 0)
    iread = 1;
    if (s->debug) fprintf(stderr, "MPG READ RET: %d\n", nread);
    return iread;
}

/* SeekMpg123File:
 *
 * Seek to sound-sample position.
 * This happens a lot when global rate does not equal decode rate.
 * We work around quanticize bug (resyncing?) 
 * by seeking back an extra amount, then read forward again by that amount. 
 * TODO: check return codes.
 */
static int
SeekMpg123File(Sound *s, Tcl_Interp *interp, Tcl_Channel ch, int pos)
{
    Mpg123_File *of;
    int opos;

    of = MpgObj(s);
    if (s->debug) fprintf(stderr, "MPG SEEK: %d\n", pos);
    if (of->started  == 0 && pos == 0) {
        if (s->debug) fprintf(stderr, "MPG SEEK SKIPPED\n");
        return pos;
    }
    opos = mpg123_tell(of->m);
    if (pos == opos) {
        if (s->debug) fprintf(stderr, "MPG SEEK NOMOVE: %d\n", opos, pos);
    }
    opos = pos;

    if (of->datasource) {
        int extra = (pos>of->seeksync?of->seeksync:pos);
        size_t done;
        
        if (of->isFile) {
            if (of->seeksync > 0 && extra > 0) {
                mpg123_seek(of->m, pos-extra, SEEK_SET);
                mpg123_read(of->m, of->pcmbuf, extra, &done);
            } else {
                mpg123_seek(of->m, pos, SEEK_SET);
            }
        } else {
            off_t ioffs;
            if (of->seeksync > 0 && extra > 0) {
                mpg123_feedseek(of->m, pos-extra, SEEK_SET, &ioffs);
                Tcl_Seek(of->datasource, ioffs, SEEK_SET);
                Tcl_Read(of->datasource, of->pcmbuf, extra);
                mpg123_decode(of->m, of->pcmbuf, extra, NULL, 0, &done);
                mpg123_decode(of->m, NULL, 0, of->pcmbuf, extra, &done);
            } else {
                mpg123_feedseek(of->m, pos, SEEK_SET, &ioffs);
                Tcl_Seek(of->datasource, ioffs, SEEK_SET);
            }
        }
    }
    pos = mpg123_tell(of->m);
    if (s->debug) fprintf(stderr, "MPG SEEKPOS: %d -> %d\n", opos, pos);
    if (pos<0) {
        return(-1);
    } else {
        return pos;
    }
}

static int
GetMpg123Header(Sound *s, Tcl_Interp *interp, Tcl_Channel ch, Tcl_Obj *obj,
         char *buf)
{
    Mpg123_File *of;
    long mlen;
    size_t done;
    int i, ret, rc;
    mpg123_id3v1 *v1; mpg123_id3v2 *v2;
    
    of = MpgObj(s);
    if (!of->opened) {
        return Mpg123Setup(s, interp, ch);
    }

    if (s->debug) fprintf(stderr, "MPG Header\n");

    /* For the case when Tcl_Open has been done somewhere else */

    if (s->extHead2 != NULL && s->extHead2Type != SNACK_MPG123_INT) {
        Snack_FileFormat *ff;
    
        for (ff = Snack_GetFileFormats(); ff != NULL; ff = ff->nextPtr) {
            if (strcmp(s->fileType, ff->name) == 0) {
                if (ff->freeHeaderProc != NULL) {
                    (ff->freeHeaderProc)(s);
                }
            }
        }
    }
    of = MpgObj(s);
    of->started = 1;
    
    mlen = (long)mpg123_length(of->m);
    if (mlen<=0) {
        return TCL_OK;
        return TCL_ERROR;
    }
    Snack_SetLength(s, mlen);
    mpg123_info(of->m, &of->fi);
    mpg123_getformat(of->m, &of->rate, &of->channels, &of->enc);
    if (s->debug) fprintf(stderr, "MPG FORMAT: channels=%d, rate=%ld enc=0x%x\n", of->channels, of->rate, of->enc);
    Snack_SetSampleRate(s, of->rate);
    Snack_SetNumChannels(s, of->channels);
    Snack_SetSampleEncoding(s, LIN16);
    of->nombitrate = of->rate;

    rc = mpg123_id3(of->m, &of->v1, &of->v2);

    Snack_SetBytesPerSample(s, 2);
    Snack_SetHeaderSize(s, 0);

    return TCL_OK;
}


void
FreeMpg123Header(Sound *s)
{
    Mpg123_File *of = (Mpg123_File *)s->extHead2;

    if (s->extHead2 != NULL) {
        FreeRes(of);
        ckfree((char *)s->extHead2);
        s->extHead2 = NULL;
        s->extHead2Type = 0;
    }
}

int
ConfigMpg123(Sound *s, Tcl_Interp *interp, int objc, Tcl_Obj *CONST objv[])
{
    Mpg123_File *of;
    int arg, index;
    static CONST char *optionStrings[] = {
        "-comment", "-album", "-seeksync",
        "-artist", "-year", "-tag", "-title", "-genre",
        "-maxbitrate", "-minbitrate", "-nominalbitrate",
        "-quality", "-nofiles", "-magiconly",  "-played", "-remain", NULL
    };
    enum options {
        COMMENT, ALBUM, SEEKSYNC, ARTIST, YEAR, TAG, TITLE, GENRE, MAX, MIN, NOMINAL, QUALITY, NOFILES, USEMAGIC, SECONDS, REMAIN
    };
  
    of = MpgObj(s);

    if (s->extHead2 != NULL && s->extHead2Type != SNACK_MPG123_INT) {
        Snack_FileFormat *ff;
    
        for (ff = Snack_GetFileFormats(); ff != NULL; ff = ff->nextPtr) {
            if (strcmp(s->fileType, ff->name) == 0) {
                if (ff->freeHeaderProc != NULL) {
                    (ff->freeHeaderProc)(s);
                }
            }
        }
    }
  
    if (objc < 3) return 0;


    if (objc == 3) { 
        /* get option */
        if (Tcl_GetIndexFromObj(interp, objv[2], optionStrings, "option", 0,
            &index) != TCL_OK) {
                Tcl_AppendResult(interp, ", or\n", NULL);
                return 0;
        }

#define NSO(str) Tcl_NewStringObj((of->v1 && of->v1->str)?(of->v1->str):"",-1)

        switch ((enum options) index) {
            case COMMENT:
            {
                Tcl_SetObjResult(interp, NSO(comment));
                break;
            }
            case ALBUM:
            {
                Tcl_SetObjResult(interp, NSO(album));
                break;
            }
            case TITLE:
            {
                Tcl_SetObjResult(interp, NSO(title));
                break;
            }
            case TAG:
            {
                Tcl_SetObjResult(interp, NSO(tag));
                break;
            }
            case YEAR:
            {
                Tcl_SetObjResult(interp, NSO(year));
                break;
            }
            case GENRE:
            {
                if (of->v1)
                Tcl_SetObjResult(interp, Tcl_NewIntObj(of->v1?of->v1->genre:-1));
                break;
            }
            case NOFILES:
            {
                Tcl_SetObjResult(interp, Tcl_NewIntObj(of->noFiles));
                break;
            }
            case MAX:
            {
                Tcl_SetObjResult(interp, Tcl_NewIntObj(of->maxbitrate));
                break;
            }
            case MIN:
            {
                Tcl_SetObjResult(interp, Tcl_NewIntObj(of->minbitrate));
                break;
            }
            case NOMINAL:
            {
                Tcl_SetObjResult(interp, Tcl_NewIntObj(of->nombitrate));
                break;
            }
            case SEEKSYNC:
            {
                Tcl_SetObjResult(interp, Tcl_NewIntObj(of->seeksync));
                break;
            }
            case REMAIN:
            {
                Tcl_SetObjResult(interp, Tcl_NewIntObj(of->seconds_left));
                break;
            }
            case SECONDS:
            {
                Tcl_SetObjResult(interp, Tcl_NewIntObj(of->current_seconds));
                break;
            }
            case QUALITY:
            {
                Tcl_SetObjResult(interp, Tcl_NewDoubleObj(of->quality));
                break;
            }
            case USEMAGIC:
            {
                Tcl_SetObjResult(interp, Tcl_NewIntObj(guessByMagic));
                break;
            }
        }
    } else { 
        /* set option */
        for (arg = 2; arg < objc; arg+=2) {
            int index;
      
            if (Tcl_GetIndexFromObj(interp, objv[arg], optionStrings, "option", 0,
                &index) != TCL_OK) {
                    return 0;
            }
      
            if (arg + 1 == objc) {
                Tcl_AppendResult(interp, "No argument given for ",
                    optionStrings[index], " option\n", (char *) NULL);
                    return 0;
            }
      
            switch ((enum options) index) {
                case NOFILES:
                {
#ifndef MPG_NODIRECT_FILES
                    if (Tcl_GetIntFromObj(interp,objv[arg+1], &of->noFiles) != TCL_OK)
#endif
                    return 0;
                    break;
                }
                case COMMENT:
                {
                    int i, n;
                    break;
                }
                case MAX:
                {
                    if (Tcl_GetIntFromObj(interp,objv[arg+1], &of->maxbitrate) != TCL_OK)
                    return 0;
                    break;
                }
                case MIN:
                {
                    if (Tcl_GetIntFromObj(interp,objv[arg+1], &of->minbitrate) != TCL_OK)
                    return 0;
                    break;
                }
                case NOMINAL:
                {
                    if (Tcl_GetIntFromObj(interp,objv[arg+1], &of->nombitrate) != TCL_OK)
                    return 0;
                    break;
                }
                case SEEKSYNC:
                {
                    if (Tcl_GetIntFromObj(interp,objv[arg+1], &of->seeksync) != TCL_OK)
                    return 0;
                    break;
                }
                case USEMAGIC:
                {
                    if (Tcl_GetIntFromObj(interp,objv[arg+1], &guessByMagic) != TCL_OK)
                    return 0;
                    break;
                }
                case QUALITY:
                {
                    if (Tcl_GetDoubleFromObj(interp, objv[arg+1], &of->quality) !=TCL_OK)
                    return 0;
                    break;
                }
            }
        }
    }

    return 1;
}

#define MPG123FILE_VERSION "1.3"

Snack_FileFormat snackMpg123Format = {
    MPG123_STRING,
        GuessMpg123File,
        GetMpg123Header,
        ExtMpg123File,
        NULL, /* PutMpg123Header, */
        OpenMpg123File,
        CloseMpg123File,
        ReadMpg123Samples,
        NULL, /* WriteMpg123Samples, */
        SeekMpg123File,
        FreeMpg123Header,
        ConfigMpg123,
        (Snack_FileFormat *) NULL
};

/* Called by "load libsnackmpg" */
EXPORT(int, Snackmpg_Init) _ANSI_ARGS_((Tcl_Interp *interp))
{
    int res;
  
#ifdef USE_TCL_STUBS
    if (Tcl_InitStubs(interp, "8", 0) == NULL) {
        return TCL_ERROR;
    }
#endif
  
#ifdef USE_SNACK_STUBS
    if (Snack_InitStubs(interp, "2", 0) == NULL) {
        return TCL_ERROR;
    }
#endif
  
    res = Tcl_PkgProvide(interp, "snackmpg", MPG123FILE_VERSION);
  
    if (res != TCL_OK) return res;

    Tcl_SetVar(interp, "snack::snackmpg", MPG123FILE_VERSION, TCL_GLOBAL_ONLY);

    Snack_CreateFileFormat(&snackMpg123Format);

    return TCL_OK;
}

EXPORT(int, Snackmpg_SafeInit)(Tcl_Interp *interp)
{
    return Snackmpg_Init(interp);
}