|
|
|
@@ -1,10 +1,17 @@ |
|
|
|
#include <Python.h> |
|
|
|
#include <SDL/SDL.h> |
|
|
|
#include <numpy/arrayobject.h> |
|
|
|
#include <mpg123.h> |
|
|
|
|
|
|
|
/* SDL error handling */ |
|
|
|
PyObject *SDLError; |
|
|
|
|
|
|
|
/* MPG123 error handling */ |
|
|
|
PyObject *MP3Error; |
|
|
|
|
|
|
|
/* The synthesis context, used mainly |
|
|
|
* for storing audio settings and state. |
|
|
|
*/ |
|
|
|
typedef struct { |
|
|
|
PyObject_HEAD |
|
|
|
int |
|
|
|
@@ -234,6 +241,283 @@ static PyObject *_ysynth_get_callback(PyObject *self, PyObject *args) |
|
|
|
return r; |
|
|
|
} |
|
|
|
|
|
|
|
/* MP3 Support */ |
|
|
|
typedef struct { |
|
|
|
PyObject_HEAD |
|
|
|
mpg123_handle *mh; |
|
|
|
int open, eof; |
|
|
|
long rate; |
|
|
|
int |
|
|
|
channels, |
|
|
|
encoding; |
|
|
|
} PyYMP3Context; |
|
|
|
|
|
|
|
void PyYMP3Context_Dealloc(PyYMP3Context *ctx) |
|
|
|
{ |
|
|
|
puts("_ysynth: Dealloc mp3 context"); |
|
|
|
|
|
|
|
/* Close if necessary */ |
|
|
|
if (ctx->open) |
|
|
|
mpg123_close(ctx->mh); |
|
|
|
|
|
|
|
/* Finally free the MP3 decoder handle */ |
|
|
|
if (ctx->mh) { |
|
|
|
mpg123_delete(ctx->mh); |
|
|
|
ctx->mh = NULL; |
|
|
|
} |
|
|
|
|
|
|
|
return; |
|
|
|
} |
|
|
|
|
|
|
|
PyTypeObject PyYMP3Context_Type; |
|
|
|
|
|
|
|
PyTypeObject PyYMP3Context_Type = { |
|
|
|
PyVarObject_HEAD_INIT(NULL, 0) |
|
|
|
"_ysynth.ymp3 context", /*tp_name*/ |
|
|
|
sizeof(PyYMP3Context), /*tp_basicsize*/ |
|
|
|
0, /*tp_itemsize*/ |
|
|
|
/* methods */ |
|
|
|
(destructor)PyYMP3Context_Dealloc, /*tp_dealloc*/ |
|
|
|
0, /*tp_print*/ |
|
|
|
0, /*tp_getattr*/ |
|
|
|
0, /*tp_setattr*/ |
|
|
|
0, /*tp_compare*/ |
|
|
|
0, /*tp_repr*/ |
|
|
|
0, /*tp_as_number*/ |
|
|
|
0, /*tp_as_sequence*/ |
|
|
|
0, /*tp_as_mapping*/ |
|
|
|
0, /*tp_hash*/ |
|
|
|
}; |
|
|
|
|
|
|
|
PyDoc_STRVAR(_ysynth_mp3_open_doc, "Open MP3 file and return context," |
|
|
|
" raises exception if file does not exists\n" |
|
|
|
"Args:\n" |
|
|
|
" filename: Path string to file\n" |
|
|
|
"Returns:\n" |
|
|
|
" MP3 context"); |
|
|
|
|
|
|
|
/* Open MP3 file */ |
|
|
|
static PyObject *_ysynth_mp3_open(PyObject *self, PyObject *args) |
|
|
|
{ |
|
|
|
PyObject *r = NULL; |
|
|
|
char *filename; |
|
|
|
PyYMP3Context *ctx; |
|
|
|
mpg123_handle *mh; |
|
|
|
int err; |
|
|
|
|
|
|
|
if (PyArg_ParseTuple(args, "s;filename", &filename)) { |
|
|
|
/* Try to open MP3 */ |
|
|
|
mh = mpg123_new(NULL, &err); |
|
|
|
if (!mh) { |
|
|
|
PyErr_SetString(MP3Error, mpg123_plain_strerror(err)); |
|
|
|
return NULL; |
|
|
|
} |
|
|
|
|
|
|
|
/* Setup float samples */ |
|
|
|
mpg123_param(mh, MPG123_ADD_FLAGS, MPG123_FORCE_FLOAT, 0.); |
|
|
|
|
|
|
|
/* Open the actual MP3 file */ |
|
|
|
if (mpg123_open(mh, filename) != MPG123_OK) { |
|
|
|
PyErr_SetString(MP3Error, mpg123_strerror(mh)); |
|
|
|
mpg123_delete(mh); |
|
|
|
return NULL; |
|
|
|
} |
|
|
|
|
|
|
|
/* Create object */ |
|
|
|
ctx = PyObject_NEW(PyYMP3Context, &PyYMP3Context_Type); |
|
|
|
ctx->open = 1; |
|
|
|
ctx->eof = 0; |
|
|
|
ctx->mh = mh; |
|
|
|
r = (PyObject*)ctx; |
|
|
|
|
|
|
|
if (!ctx) { |
|
|
|
mpg123_close(mh); |
|
|
|
mpg123_delete(mh); |
|
|
|
return NULL; |
|
|
|
} |
|
|
|
|
|
|
|
/* Check-out file stream format */ |
|
|
|
mpg123_getformat(mh, &ctx->rate, &ctx->channels, &ctx->encoding); |
|
|
|
|
|
|
|
if (ctx->encoding != MPG123_ENC_FLOAT_32) { |
|
|
|
PyErr_SetString(MP3Error, |
|
|
|
"Got non-float encoder while forcing float"); |
|
|
|
Py_DECREF(ctx); |
|
|
|
return NULL; |
|
|
|
} |
|
|
|
|
|
|
|
/* Now don't allow the format to change. |
|
|
|
* |
|
|
|
* FIXME: Later on we probably do want to change this, as to |
|
|
|
* allow YSynth to handle any changes in samplerate instead of |
|
|
|
* allowing MPG123's crude resampler to do the job. |
|
|
|
*/ |
|
|
|
mpg123_format_none(mh); |
|
|
|
mpg123_format(mh, ctx->rate, ctx->channels, ctx->encoding); |
|
|
|
} |
|
|
|
|
|
|
|
return r; |
|
|
|
} |
|
|
|
|
|
|
|
PyDoc_STRVAR(_ysynth_mp3_close_doc, "Close MP3 file context\n" |
|
|
|
"Args:\n" |
|
|
|
" context: MP3 context\n" |
|
|
|
"Returns:\n" |
|
|
|
" None, but raises an MP3Error if the file has already been closed"); |
|
|
|
|
|
|
|
/* Close MP3 file */ |
|
|
|
static PyObject *_ysynth_mp3_close(PyObject *self, PyObject *args) |
|
|
|
{ |
|
|
|
PyObject *r = NULL; |
|
|
|
PyYMP3Context *ctx; |
|
|
|
|
|
|
|
if (PyArg_ParseTuple(args, "O!", &PyYMP3Context_Type, (PyObject**)&ctx)) { |
|
|
|
if (ctx->open) { |
|
|
|
ctx->open = 0; |
|
|
|
mpg123_close(ctx->mh); |
|
|
|
r = Py_None; |
|
|
|
Py_INCREF(r); |
|
|
|
|
|
|
|
} else { |
|
|
|
PyErr_SetString(MP3Error, "File already closed"); |
|
|
|
r = NULL; |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
return r; |
|
|
|
} |
|
|
|
|
|
|
|
PyDoc_STRVAR(_ysynth_mp3_get_format_doc, "Get MP3 format configuration\n" |
|
|
|
"Args: None\n" |
|
|
|
"Returns:\n" |
|
|
|
" Tuple containing: (samplerate, channels)"); |
|
|
|
|
|
|
|
/* Read MP3 data */ |
|
|
|
static PyObject *_ysynth_mp3_get_format(PyObject *self, PyObject *args) |
|
|
|
{ |
|
|
|
PyObject *r = NULL; |
|
|
|
PyYMP3Context *ctx; |
|
|
|
|
|
|
|
if (PyArg_ParseTuple(args, "O!", &PyYMP3Context_Type, (PyObject**)&ctx)) { |
|
|
|
if (ctx->open) { |
|
|
|
r = Py_BuildValue("(ii)", ctx->rate, ctx->channels); |
|
|
|
} else { |
|
|
|
PyErr_SetString(MP3Error, "File has been closed"); |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
return r; |
|
|
|
} |
|
|
|
|
|
|
|
PyDoc_STRVAR(_ysynth_mp3_read_doc, "Read MP3 data\n" |
|
|
|
"Args:\n" |
|
|
|
" context: Open MP3 context\n" |
|
|
|
" samples: Number of samples to decode\n" |
|
|
|
"Returns:\n" |
|
|
|
" A NumPy array of shape (samples, channels)\n" |
|
|
|
" This function raises an MP3Error on decode failure.\n" |
|
|
|
" It shall output silence on EOF"); |
|
|
|
|
|
|
|
/* Read MP3 data */ |
|
|
|
static PyObject *_ysynth_mp3_read(PyObject *self, PyObject *args) |
|
|
|
{ |
|
|
|
PyObject *r = NULL; |
|
|
|
PyYMP3Context *ctx; |
|
|
|
int samples, bytes_read; |
|
|
|
int err; |
|
|
|
npy_int dims[2]; |
|
|
|
NPY_AO *stream; |
|
|
|
|
|
|
|
if (PyArg_ParseTuple(args, "O!i", &PyYMP3Context_Type, (PyObject**)&ctx, &samples)) { |
|
|
|
if (ctx->open) { |
|
|
|
/* Allocate output array */ |
|
|
|
dims[0] = samples; |
|
|
|
dims[1] = ctx->channels; |
|
|
|
stream = PyArray_New(&PyArray_Type, 2, dims, NPY_FLOAT32, NULL, NULL, 4, 0, NULL); |
|
|
|
|
|
|
|
if (!stream) |
|
|
|
return NULL; |
|
|
|
r = (PyObject*)stream; |
|
|
|
|
|
|
|
/* EOF Generates a silence stream */ |
|
|
|
if (ctx->eof) { |
|
|
|
|
|
|
|
memset(PyArray_DATA(stream), 0x0, PyArray_NBYTES(stream)); |
|
|
|
} else { |
|
|
|
|
|
|
|
/* Read the next set of MP3 samples */ |
|
|
|
err = mpg123_read(ctx->mh, PyArray_DATA(stream), |
|
|
|
PyArray_NBYTES(stream), &bytes_read); |
|
|
|
|
|
|
|
/* Handle any weird errors */ |
|
|
|
if (err != MPG123_OK) { |
|
|
|
if (err == MPG123_DONE) { |
|
|
|
ctx->eof = 1; |
|
|
|
} else { |
|
|
|
Py_DECREF(stream); |
|
|
|
PyErr_SetString(MP3Error, mpg123_strerror(ctx->mh)); |
|
|
|
return NULL; |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
/* Fill any missing data with silence, and of course complain |
|
|
|
* whenever necessary |
|
|
|
*/ |
|
|
|
if (bytes_read != PyArray_NBYTES(stream)) { |
|
|
|
if (!ctx->eof) |
|
|
|
fprintf(stderr, "warning: MPG123 did not return" |
|
|
|
" enough data but did not set MPG123_DONE" |
|
|
|
" either..\n"); |
|
|
|
memset(PyArray_BYTES(stream) + bytes_read, 0x0, |
|
|
|
PyArray_NBYTES(stream) - bytes_read); |
|
|
|
} |
|
|
|
} |
|
|
|
} else { |
|
|
|
PyErr_SetString(MP3Error, "File has been closed"); |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
return r; |
|
|
|
} |
|
|
|
|
|
|
|
/* TODO: Let's use the same strategy NumPy uses for its built-ins, provide |
|
|
|
* the function signature in the docstrings |
|
|
|
*/ |
|
|
|
|
|
|
|
PyDoc_STRVAR(_ysynth_mp3_seek_doc, "Seek to position in MP3\n" |
|
|
|
"Args:\n" |
|
|
|
" context: Open MP3 context, providing somehting else\n" |
|
|
|
" will result in an exception\n" |
|
|
|
" position: Integer position in samples to seek to\n" |
|
|
|
"Returns:\n" |
|
|
|
" None, this functions raises an MP3Error when errors are encountered"); |
|
|
|
|
|
|
|
/* Seek MP3 data */ |
|
|
|
static PyObject *_ysynth_mp3_seek(PyObject *self, PyObject *args) |
|
|
|
{ |
|
|
|
PyObject *r = NULL; |
|
|
|
PyYMP3Context *ctx; |
|
|
|
int sample; |
|
|
|
int err; |
|
|
|
|
|
|
|
if (PyArg_ParseTuple(args, "O!i;context, position", &PyYMP3Context_Type, |
|
|
|
(PyObject**)&ctx, &sample)) { |
|
|
|
if (ctx->open) { |
|
|
|
err = mpg123_seek(ctx->mh, sample, SEEK_SET); |
|
|
|
if (err < 0) { |
|
|
|
PyErr_SetString(MP3Error, mpg123_strerror(ctx->mh)); |
|
|
|
} else { |
|
|
|
r = Py_None; |
|
|
|
Py_INCREF(r); |
|
|
|
} |
|
|
|
} else { |
|
|
|
PyErr_SetString(MP3Error, "File has been closed"); |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
return r; |
|
|
|
} |
|
|
|
|
|
|
|
/* Exported methods */ |
|
|
|
static PyMethodDef pymod_methods[] = { |
|
|
|
{"_ysynth_init", (PyCFunction)_ysynth_init, |
|
|
|
@@ -244,17 +528,30 @@ static PyMethodDef pymod_methods[] = { |
|
|
|
METH_VARARGS, _ysynth_set_callback_doc}, |
|
|
|
{"_ysynth_get_callback", (PyCFunction)_ysynth_get_callback, |
|
|
|
METH_VARARGS, _ysynth_get_callback_doc}, |
|
|
|
{"_ysynth_mp3_open", (PyCFunction)_ysynth_mp3_open, |
|
|
|
METH_VARARGS, _ysynth_mp3_open_doc}, |
|
|
|
{"_ysynth_mp3_close", (PyCFunction)_ysynth_mp3_close, |
|
|
|
METH_VARARGS, _ysynth_mp3_close_doc}, |
|
|
|
{"_ysynth_mp3_get_format", (PyCFunction)_ysynth_mp3_get_format, |
|
|
|
METH_VARARGS, _ysynth_mp3_get_format_doc}, |
|
|
|
{"_ysynth_mp3_read", (PyCFunction)_ysynth_mp3_read, |
|
|
|
METH_VARARGS, _ysynth_mp3_read_doc}, |
|
|
|
{"_ysynth_mp3_seek", (PyCFunction)_ysynth_mp3_seek, |
|
|
|
METH_VARARGS, _ysynth_mp3_seek_doc}, |
|
|
|
{"hello", (PyCFunction)pymod_hello, |
|
|
|
METH_VARARGS, hello_doc}, |
|
|
|
{NULL, NULL} /* sentinel */ |
|
|
|
}; |
|
|
|
|
|
|
|
/* Shutdown SDL */ |
|
|
|
/* Shutdown SDL/MPG123 */ |
|
|
|
void exitpymod(void) |
|
|
|
{ |
|
|
|
mpg123_exit(); |
|
|
|
/* XXX: Should we check for successful initialisation of SDL? */ |
|
|
|
SDL_Quit(); |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
PyDoc_STRVAR(module_doc, "Simple test module"); |
|
|
|
|
|
|
|
/* Module initialisation, called by Python on import */ |
|
|
|
@@ -262,11 +559,21 @@ PyMODINIT_FUNC |
|
|
|
initpymod(void) |
|
|
|
{ |
|
|
|
PyObject *m; |
|
|
|
int err; |
|
|
|
|
|
|
|
/* Setup SDL audio without signal handlers */ |
|
|
|
if (SDL_Init(SDL_INIT_AUDIO | SDL_INIT_NOPARACHUTE) < 0) |
|
|
|
return; |
|
|
|
|
|
|
|
#if 1 |
|
|
|
/* Setup MPG123 */ |
|
|
|
if ((err = mpg123_init()) != MPG123_OK) { |
|
|
|
fprintf(stderr, "Failure while initialising mgp123: %s\n", |
|
|
|
mpg123_plain_strerror(err)); |
|
|
|
return; |
|
|
|
} |
|
|
|
#endif |
|
|
|
|
|
|
|
/* Register exit handler |
|
|
|
* XXX: Later on try to use Python's atexit module? |
|
|
|
*/ |
|
|
|
@@ -282,6 +589,17 @@ initpymod(void) |
|
|
|
Py_INCREF(SDLError); |
|
|
|
PyModule_AddObject(m, "SDLError", SDLError); |
|
|
|
|
|
|
|
MP3Error = PyErr_NewException("_ysynth.MP3Error", NULL, NULL); |
|
|
|
Py_INCREF(MP3Error); |
|
|
|
PyModule_AddObject(m, "MP3Error", MP3Error); |
|
|
|
|
|
|
|
/* Setup types */ |
|
|
|
if (PyType_Ready(&PyYSynthContext_Type) < 0) |
|
|
|
return; |
|
|
|
|
|
|
|
if (PyType_Ready(&PyYMP3Context_Type) < 0) |
|
|
|
return; |
|
|
|
|
|
|
|
/* Our module requires the GIL to be available */ |
|
|
|
if (!PyEval_ThreadsInitialized()) { |
|
|
|
puts("Initialising multithreading for Python"); |
|
|
|
|