/* * 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 #include #include "snack.h" #include #include #include "mpg123.h" #if defined(__WIN32__) # include # include # define WIN32_LEAN_AND_MEAN # include # 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 /* #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 = (len0) { 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); }