浏览代码

MP3 support and rudimentary song playback.

master
Bas Weelinck 13 年前
父节点
当前提交
40adebbe13
共有 5 个文件被更改,包括 489 次插入9 次删除
  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 查看文件

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

CFLAGS += $(shell pkg-config --cflags $(PYTHON_VERSION))
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

default: flanger

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

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


+ 17
- 2
hoi2.py 查看文件

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

from ysynth import *
import sys

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

# 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) * 0.5, Sin(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))) \
/ (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 查看文件

@@ -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 查看文件

@@ -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");


+ 117
- 2
ysynth.py 查看文件

@@ -2,13 +2,17 @@
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 itertools import izip
import numpy as np
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):
"""
@@ -465,6 +469,64 @@ class ChannelJoiner(YAudioGraphNode):
for chunks in l:
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
# XXX: I am probably not White Noise, fix me
# up at a later time.
@@ -526,6 +588,59 @@ class Pulse(YOscillator):
pos, zi = lfilter([-1, 1], [1], pos, zi=zi)
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
# FIXME: Incomplete class
class Flanger(YAudioGraphNode):


正在加载...
取消
保存