tcl-snack/SnackMpg.c

811 lines
22 KiB
C
Executable File

/*
* 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 "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);
}