Bläddra i källkod

MP3 support and rudimentary song playback.

master
Bas Weelinck 13 år sedan
förälder
incheckning
40adebbe13
5 ändrade filer med 489 tillägg och 9 borttagningar
  1. +2
    -4
      Makefile
  2. +17
    -2
      hoi2.py
  3. +34
    -0
      hoi3.py
  4. +319
    -1
      pymod.c
  5. +117
    -2
      ysynth.py

+ 2
- 4
Makefile Visa fil

@@ -3,16 +3,14 @@ PYTHON_VERSION:=python-2.7


CFLAGS += $(shell pkg-config --cflags $(PYTHON_VERSION)) CFLAGS += $(shell pkg-config --cflags $(PYTHON_VERSION))
CFLAGS += -Wall -pedantic -std=c99 -g CFLAGS += -Wall -pedantic -std=c99 -g
LFLAGS += $(shell pkg-config --libs $(PYTHON_VERSION))
LFLAGS += -lSDL -lSDL_mixer

LFLAGS += $(shell pkg-config --libs $(PYTHON_VERSION) sdl SDL_mixer libmpg123)


.PHONY: default clean .PHONY: default clean


default: flanger default: flanger


clean: clean:
rm -f flanger *.o
rm -f flanger *.o pymodmodule.so


flanger: hoi.c flanger: hoi.c
gcc -o flanger hoi.c $(LFLAGS) $(CFLAGS) gcc -o flanger hoi.c $(LFLAGS) $(CFLAGS)


+ 17
- 2
hoi2.py Visa fil

@@ -1,5 +1,6 @@


from ysynth import * from ysynth import *
import sys


synth = YSynth() synth = YSynth()
#synth.set_graph(Sin(440 + 110 * Sin(1))) #synth.set_graph(Sin(440 + 110 * Sin(1)))
@@ -13,8 +14,8 @@ synth = YSynth()
#synth.set_graph(WhiteNoise() * 1.0) #synth.set_graph(WhiteNoise() * 1.0)


# Should sound vagely similar to a flanged whitenoise signal # Should sound vagely similar to a flanged whitenoise signal
noise = WhiteNoise()
synth.set_graph((noise + noise[50 + 100 * (Sin(0.1) + 1)]) * 0.5 * (RevSaw(2) + 1) * 0.5)
#noise = WhiteNoise()
#synth.set_graph((noise + noise[50 + 100 * (Sin(0.1) + 1)]) * 0.5 * (RevSaw(2) + 1) * 0.5)
#synth.set_graph(Sin(440) * RevSaw(5) * 0.2) #synth.set_graph(Sin(440) * RevSaw(5) * 0.2)
#synth.set_graph((Sin(440) * 0.5, Sin(220) * 0.5)) #synth.set_graph((Sin(440) * 0.5, Sin(220) * 0.5))
#synth.set_graph(Sin((440, 220)) * 0.5) #synth.set_graph(Sin((440, 220)) * 0.5)
@@ -36,3 +37,17 @@ def harmonics(osc, freq, count):
(osc(freq * i) for i in xrange(1, count + 2))) \ (osc(freq * i) for i in xrange(1, count + 2))) \
/ (count + 1) / (count + 1)


# MP3 playback
#synth.set_graph(MP3Stream('hoi.mp3'))
# Crossfade between 2 songs
#fade = (Sin(0.5) + 1) * 0.5
#synth.set_graph(MP3Stream(sys.argv[1]) * fade + MP3Stream(sys.argv[2]) * (1 - fade))

# The following code causes more buggy behaviour.
#x = Sin(440)
#for i in xrange(1, 15):
# x += Sin(440 + x)
#synth.set_graph(x / 41 * 0.5)
user = UserSignal(440)
synth.set_graph(Sin(user) * 0.2)


+ 34
- 0
hoi3.py Visa fil

@@ -0,0 +1,34 @@

import pymod
import sys
import numpy as np

mp3 = pymod._ysynth_mp3_open(sys.argv[1])
samplerate, channels = pymod._ysynth_mp3_get_format(mp3)
print "Samplerate:", samplerate, "Channels:", channels

x = pymod._ysynth_init(samplerate, channels)
first_time = True

def play_mp3(channels):
"""
Play contents of MP3 file.
"""
global first_time

if first_time:
print "First time callback occurred"
print "Array shape is: %s" % str(channels.shape)
first_time = False

next_chunk = pymod._ysynth_mp3_read(mp3, channels.shape[0])
next_chunk = np.minimum(np.maximum(next_chunk, -1.0), 1.0)
# Using 32767.5 as a factor causes overflow, we need to fix this
# in the most uniformly distributed way possible
np.round(next_chunk * 32767, 0, out=channels)

return

pymod._ysynth_set_callback(x, play_mp3)


+ 319
- 1
pymod.c Visa fil

@@ -1,10 +1,17 @@
#include <Python.h> #include <Python.h>
#include <SDL/SDL.h> #include <SDL/SDL.h>
#include <numpy/arrayobject.h> #include <numpy/arrayobject.h>
#include <mpg123.h>


/* SDL error handling */ /* SDL error handling */
PyObject *SDLError; PyObject *SDLError;


/* MPG123 error handling */
PyObject *MP3Error;

/* The synthesis context, used mainly
* for storing audio settings and state.
*/
typedef struct { typedef struct {
PyObject_HEAD PyObject_HEAD
int int
@@ -234,6 +241,283 @@ static PyObject *_ysynth_get_callback(PyObject *self, PyObject *args)
return r; 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 */ /* Exported methods */
static PyMethodDef pymod_methods[] = { static PyMethodDef pymod_methods[] = {
{"_ysynth_init", (PyCFunction)_ysynth_init, {"_ysynth_init", (PyCFunction)_ysynth_init,
@@ -244,17 +528,30 @@ static PyMethodDef pymod_methods[] = {
METH_VARARGS, _ysynth_set_callback_doc}, METH_VARARGS, _ysynth_set_callback_doc},
{"_ysynth_get_callback", (PyCFunction)_ysynth_get_callback, {"_ysynth_get_callback", (PyCFunction)_ysynth_get_callback,
METH_VARARGS, _ysynth_get_callback_doc}, 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, {"hello", (PyCFunction)pymod_hello,
METH_VARARGS, hello_doc}, METH_VARARGS, hello_doc},
{NULL, NULL} /* sentinel */ {NULL, NULL} /* sentinel */
}; };


/* Shutdown SDL */
/* Shutdown SDL/MPG123 */
void exitpymod(void) void exitpymod(void)
{ {
mpg123_exit();
/* XXX: Should we check for successful initialisation of SDL? */
SDL_Quit(); SDL_Quit();
} }



PyDoc_STRVAR(module_doc, "Simple test module"); PyDoc_STRVAR(module_doc, "Simple test module");


/* Module initialisation, called by Python on import */ /* Module initialisation, called by Python on import */
@@ -262,11 +559,21 @@ PyMODINIT_FUNC
initpymod(void) initpymod(void)
{ {
PyObject *m; PyObject *m;
int err;


/* Setup SDL audio without signal handlers */ /* Setup SDL audio without signal handlers */
if (SDL_Init(SDL_INIT_AUDIO | SDL_INIT_NOPARACHUTE) < 0) if (SDL_Init(SDL_INIT_AUDIO | SDL_INIT_NOPARACHUTE) < 0)
return; 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 /* Register exit handler
* XXX: Later on try to use Python's atexit module? * XXX: Later on try to use Python's atexit module?
*/ */
@@ -282,6 +589,17 @@ initpymod(void)
Py_INCREF(SDLError); Py_INCREF(SDLError);
PyModule_AddObject(m, "SDLError", 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 */ /* Our module requires the GIL to be available */
if (!PyEval_ThreadsInitialized()) { if (!PyEval_ThreadsInitialized()) {
puts("Initialising multithreading for Python"); puts("Initialising multithreading for Python");


+ 117
- 2
ysynth.py Visa fil

@@ -2,13 +2,17 @@
Python audio synthesis framework. Python audio synthesis framework.
""" """


from pymod import _ysynth_init, _ysynth_init, _ysynth_set_callback
from pymod import _ysynth_init, _ysynth_init, _ysynth_set_callback, \
_ysynth_mp3_open, _ysynth_mp3_get_format, _ysynth_mp3_read, \
_ysynth_mp3_close

from math import cos, sin, pi, modf from math import cos, sin, pi, modf
from itertools import izip from itertools import izip
import numpy as np import numpy as np
from scipy.signal import lfilter from scipy.signal import lfilter


__all__ = ['YSynth', 'Sin', 'Cos', 'Saw', 'RevSaw', 'Square', 'Pulse', 'WhiteNoise']
__all__ = ['YSynth', 'Sin', 'Cos', 'Saw', 'RevSaw', 'Square', 'Pulse', 'WhiteNoise',
'UserSignal', 'MP3Stream']


class YSynth(object): class YSynth(object):
""" """
@@ -465,6 +469,64 @@ class ChannelJoiner(YAudioGraphNode):
for chunks in l: for chunks in l:
yield np.hstack(np.reshape(chunk, (-1, 1)) for chunk in chunks) yield np.hstack(np.reshape(chunk, (-1, 1)) for chunk in chunks)


# User programmable signal
class UserSignal(YAudioGraphNode):
"""
Simple programmable output signal.
"""

def __init__(self, out=0.0):
YAudioGraphNode.__init__(self)
self.cur_out = out
self.ps_list = []

def __call__(self, l):
while True:
x = np.empty((self.synth.chunk_size,))
if len(self.ps_list):
pos = 0
cur_out = self.cur_out

# FIXME: lrn2python queue
while len(self.ps_list):
change, delay = self.ps_list.pop(0)
if delay + pos > self.synth.chunk_size:
x[pos:] = cur_out
delay -= self.synth.chunk_size - pos
self.ps_list.insert(0, (change, delay))
pos = self.synth.chunk_size
break
else:
x[pos:pos + delay] = cur_out
pos += delay
cur_out = change

x[pos:] = cur_out
self.cur_out = cur_out

else:
x.fill(self.cur_out)

yield x

def add_signal_change(self, *changes):
"""
Program a sequence of signal changes in the
output signal.
"""
self.ps_list.extend(changes)

def pulse(self, delay, high=1.0, low=0.0):
"""
Add a pulse change to the output signal.
This is a convenience function that calls
add_signal_change with the appropriate arguments.
"""
self.add_signal_change((high, delay), (low, 1))

def set_output(self, out):
self.cur_out = out

# Noise signals # Noise signals
# XXX: I am probably not White Noise, fix me # XXX: I am probably not White Noise, fix me
# up at a later time. # up at a later time.
@@ -526,6 +588,59 @@ class Pulse(YOscillator):
pos, zi = lfilter([-1, 1], [1], pos, zi=zi) pos, zi = lfilter([-1, 1], [1], pos, zi=zi)
yield np.array(pos > 0, dtype=float) yield np.array(pos > 0, dtype=float)


# MP3 playback
class MP3Stream(YAudioGraphNode):
def __init__(self, filename):
YAudioGraphNode.__init__(self)

# Open MP3 file
self.filename = filename
self.mp3_handle = _ysynth_mp3_open(filename)
self.samplerate, self.channels = _ysynth_mp3_get_format(self.mp3_handle)

def __call__(self, l):
"""
Return next chunk of mp3 audio.
"""

while True:
yield _ysynth_mp3_read(self.mp3_handle, self.synth.chunk_size)

def __del__(self):
"""
Close MP3 file and 'free' handle
"""

_ysynth_mp3_close(self.mp3_handle)
self.mp3_handle = None

# Resamplers
class ResampleNN(YAudioGraphNode):
"""
Resamplers have their own sample clock which
distinguishes them from the usual components, obviously
necessary because they change the samplerate of the input
signal.
"""

def __init__(self, in_audio, in_rate=None, out_rate=None):
self.synth = self

if in_rate is None:
in_rate = 44100

if out_rate is None:
out_rate = 44100

YAudioGraphNode.__init__(self, in_audio, in_rate, out_rate)

def __call__(self, l):
for in_audio, in_rate, out_rate in l:
pass

def set_synth(self, synth):
self.real_synth = synth

# Flanger effect # Flanger effect
# FIXME: Incomplete class # FIXME: Incomplete class
class Flanger(YAudioGraphNode): class Flanger(YAudioGraphNode):


Laddar…
Avbryt
Spara