From bbe86469875af93d2d1609de8128be0b97193f17 Mon Sep 17 00:00:00 2001 From: Bas Weelinck Date: Tue, 24 Apr 2012 14:08:22 +0200 Subject: [PATCH] YSynth initial commit. - Synthesizer works but uses a lot of CPU. --- Makefile | 17 +++ hoi.c | 137 +++++++++++++++++++++ pymod.c | 337 +++++++++++++++++++++++++++++++++++++++++++++++++++ ysynth.py | 354 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 845 insertions(+) create mode 100644 Makefile create mode 100644 hoi.c create mode 100644 pymod.c create mode 100644 ysynth.py diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..3c1205c --- /dev/null +++ b/Makefile @@ -0,0 +1,17 @@ + +CFLAGS += -Wall -Werror -pedantic -g +LFLAGS += -lSDL -lSDL_mixer + +.PHONY: default clean + +default: flanger + +clean: + rm -f flanger *.o + +flanger: hoi.c + gcc -o flanger hoi.c $(LFLAGS) $(CFLAGS) + +pymodmodule.so: pymod.c + gcc -o pymodmodule.so pymod.c -fPIC $(CFLAGS) -shared -lpython2.7 -lSDL + diff --git a/hoi.c b/hoi.c new file mode 100644 index 0000000..45f7bb9 --- /dev/null +++ b/hoi.c @@ -0,0 +1,137 @@ +#include +#include +#include +#include + +#define CHUNK_SIZE 4096 +#if 1 +#define FLANGER_BUFFER_SIZE 88200 +#else +#define FLANGER_BUFFER_SIZE 2000 +#endif + +void handle_exit(void) +{ + Mix_Quit(); + SDL_Quit(); + puts("Shutdown complete."); +} + +static struct flanger { + int samples; + int frequency; + int + start_ms, + stop_ms; + int cur_delay; + int sweep_dir; + int sweep_speed; + Sint16 buf[FLANGER_BUFFER_SIZE]; +} flange_chan; + +/* Perform flanging effect */ +void flanger_func(int chan, void *stream, int len, void *udata) +{ + int i; + Sint16 *sbuf = stream; + struct flanger *flanger = udata; + int local_len = len / sizeof(Sint16); + + /*printf("Samples: %d, Local Len: %d, Len: %d\n", flanger->samples, local_len, len);*/ + for (i = 0; i < local_len; i++) { + flanger->buf[(flanger->samples + i) % + FLANGER_BUFFER_SIZE] = sbuf[i]; + sbuf[i] = sbuf[i] / 2 + flanger->buf[(flanger->samples + + (FLANGER_BUFFER_SIZE - flanger->cur_delay / 1000 * 2) + i) % + FLANGER_BUFFER_SIZE] / 2; + if (!(i%2)) { + if (flanger->cur_delay >= flanger->frequency / + (1000 / flanger->stop_ms) * 1000) { + flanger->sweep_dir = -flanger->sweep_speed; + } else if (flanger->cur_delay <= flanger->frequency / + (1000 / flanger->start_ms) * 1000) { + flanger->sweep_dir = flanger->sweep_speed; + } + flanger->cur_delay += flanger->sweep_dir; + } + /*sbuf[i] = 0;*/ + } + + flanger->samples = (flanger->samples + local_len) % FLANGER_BUFFER_SIZE; + + return; +} + +int main(int argc, char *argv[]) +{ + char *mp3_file; + Mix_Music *mp3_stream; + + /* Audio settings */ + int frequency = 44100; + Uint16 format = AUDIO_S16; + int channels = 2; + + if (argc != 2) { + printf("Usage %s: \n", argv[0]); + return EXIT_SUCCESS; + } + + mp3_file = argv[1]; + + /* Setup shutdown sequence */ + atexit(handle_exit); + + /* Setup SDL & friends */ + SDL_Init(SDL_INIT_AUDIO); + Mix_Init(MIX_INIT_MP3); + + /* Open audio */ + if (Mix_OpenAudio(frequency, format, channels, CHUNK_SIZE)) { + printf("Mix_OpenAudio failed: %s\n", Mix_GetError()); + /* Mix_FreeMusic(mp3_stream); */ + return EXIT_FAILURE; + } + + /* Verify audio compatibility */ + Mix_QuerySpec(&frequency, &format, &channels); + printf("Device settings are: %d, %hd, %d\n", frequency, format, channels); + if (format != AUDIO_S16) { + puts("Selected device format incompatible :-(\n"); + return EXIT_FAILURE; + } + + /* Load MP3 file */ + mp3_stream = Mix_LoadMUS(mp3_file); + if (!mp3_stream) { + printf("Mix_LoadMUS failed: %s\n", Mix_GetError()); + return EXIT_FAILURE; + } + + /* Setup flangers */ + memset(flange_chan.buf, 0x0, sizeof(short) * FLANGER_BUFFER_SIZE); + flange_chan.frequency = frequency; + flange_chan.samples = 0; + flange_chan.start_ms = 1; + flange_chan.stop_ms = 20; + flange_chan.sweep_speed = 5; + flange_chan.cur_delay = 0; + flange_chan.sweep_dir = 1; + if (!Mix_RegisterEffect(MIX_CHANNEL_POST, flanger_func, NULL, &flange_chan)) { + printf("Mix_RegisterEffect failed: %s\n", Mix_GetError()); + return EXIT_FAILURE; + } + + /* Start playback */ + if (Mix_PlayMusic(mp3_stream, 0) < 0) { + printf("Mix_PlayMusic failed: %s\n", Mix_GetError()); + Mix_FreeMusic(mp3_stream); + return EXIT_FAILURE; + } + + while (Mix_PlayingMusic()) + SDL_Delay(100); + + return EXIT_SUCCESS; +} + diff --git a/pymod.c b/pymod.c new file mode 100644 index 0000000..0b84b12 --- /dev/null +++ b/pymod.c @@ -0,0 +1,337 @@ +#include +#include + +/* SDL error handling */ +PyObject *SDLError; + +typedef struct { + PyObject_HEAD + int + samplerate, + channels; + int active; + PyObject *callback; +} PyYSynthContext; + +void PyYSynthContext_Dealloc(PyYSynthContext *self) +{ + puts("_ysynth: Dealloc context"); + if (self->active) { + SDL_CloseAudio(); + self->active = 0; + } + + Py_XDECREF(self->callback); + return; +} + +PyTypeObject PyYSynthContext_Type; + +PyTypeObject PyYSynthContext_Type = { + PyVarObject_HEAD_INIT(NULL, 0) + "_ysynth.ysynth context", /*tp_name*/ + sizeof(PyYSynthContext), /*tp_basicsize*/ + 0, /*tp_itemsize*/ + /* methods */ + (destructor)PyYSynthContext_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(hello_doc, "Print hello message from Python module"); + +/* SDL Audio callback */ +void _sdl_audio_callback(void *user, Uint8 *stream, int len) +{ + PyYSynthContext *ctx = (PyYSynthContext*)user; + PyObject + *channels, + *chan, + *py_sample, + *left, + *right, + *r = NULL; + PyGILState_STATE gstate; + int i, j; + int sample; + int local_len = len / sizeof(Sint16) / 2; + Sint16 *local_stream = (Sint16*)stream;; + + /* This function is called from SDL's own thread, so we need to + * register it with the Python interpreter before calling any + * Python functions. + */ + gstate = PyGILState_Ensure(); + + do { + if (ctx->callback) { + /* Build list of doubles */ + channels = PyTuple_New(ctx->channels); + if (!channels) + break; + + /* Fill tuple with channel lists */ + for (i = 0; i < ctx->channels; i++) { + /* Allocate new channel list */ + chan = PyList_New(local_len); + if (!chan) { + Py_DECREF(channels); + break; + } + PyTuple_SET_ITEM(channels, i, chan); + + /* Fill channel with 0.0 floats */ + for (j = 0; j < local_len; j++) { + py_sample = PyFloat_FromDouble(0.0); + if (!py_sample) + break; + PyList_SET_ITEM(chan, j, py_sample); + } + if (j != local_len) + break; + } + if (i != ctx->channels) + break; + + /* Finally execute the callback */ + r = PyObject_CallObject(ctx->callback, channels); + } + } while (0); + + /* Print stacktrace if necessary */ + if (!r) { + if (PyErr_Occurred()) + PyErr_Print(); + } else { + left = PyTuple_GetItem(channels, 0); + right = PyTuple_GetItem(channels, 1); + + /* Make sure we won't exceed list length.. the callee shouldn't change + * the list length though */ + local_len = local_len > PyList_Size(left) ? + PyList_Size(left) : local_len; + local_len = local_len > PyList_Size(right) ? + PyList_Size(right) : local_len; + + /* Try to convert both lists to audio stream */ + for (i = 0; i < local_len; i++) { + + /* With the courtesy of microsynth, float to sample conversion ;-) */ + sample = (int)(32767.5 * PyFloat_AsDouble(PyList_GetItem(left, i))); + + /* Clip samples */ + if (sample > 32767) sample = 32767; + if (sample < -32768) sample = -32768; + + local_stream[i * 2] = (Sint16)sample; + + sample = (int)(32767.5 * PyFloat_AsDouble(PyList_GetItem(right, + i))); + + /* Clip samples */ + if (sample > 32767) sample = 32767; + if (sample < -32768) sample = -32768; + + local_stream[i * 2 + 1] = (Sint16)sample; + } + + /* Free the call's return value, probably Py_None */ + Py_DECREF(r); + } + + /* Free the channels */ + Py_XDECREF(channels); + + /* And leave the GIL */ + PyGILState_Release(gstate); + return; +} + +/* Output hello message */ +static PyObject *pymod_hello(PyObject *self, PyObject *args) +{ + PyObject *r = NULL;; + + if (PyArg_ParseTuple(args, "")) { + printf("Hello from Python module\n"); + + Py_INCREF(Py_None); + r = Py_None; + } + + return r; +} + +PyDoc_STRVAR(_ysynth_init_doc, "Initialise SDL audio and return context"); + +/* Initialise synthesizer */ +static PyObject *_ysynth_init(PyObject *self, PyObject *args) +{ + PyObject *r = NULL; + PyYSynthContext *ctx; + int + samplerate, + channels; + SDL_AudioSpec fmt, fmt_obtained; + + if (PyArg_ParseTuple(args, "ii;samplerate, channels", &samplerate, &channels)) { + ctx = PyObject_NEW(PyYSynthContext, &PyYSynthContext_Type); + r = (PyObject*)ctx; + + ctx->callback = NULL; + + /* Setup SDL Audio device */ + fmt.freq = samplerate; + fmt.format = AUDIO_S16; + fmt.channels = channels; + fmt.samples = 512; + fmt.callback = _sdl_audio_callback; + fmt.userdata = r; + + /* Handle SDL error */ + if (SDL_OpenAudio(&fmt, &fmt_obtained) < 0) { + PyErr_SetString(SDLError, SDL_GetError()); + Py_DECREF(r); + return NULL; + } + SDL_PauseAudio(0); + + ctx->samplerate = fmt_obtained.freq; + ctx->channels = fmt_obtained.channels; + ctx->active = 1; + + } + + return r; +} + +PyDoc_STRVAR(_ysynth_shutdown_doc, "Shutdown SDL audio device"); + +/* Shutdown SDL audio */ +static PyObject *_ysynth_shutdown(PyObject *self, PyObject *args) +{ + PyObject *r = NULL; + PyYSynthContext *ctx; + + if (!PyArg_ParseTuple(args, "O!", &PyYSynthContext_Type, (PyObject**)&ctx)) + { + ctx->active = 0; + SDL_CloseAudio(); + Py_XDECREF(ctx->callback); + r = Py_None; + Py_INCREF(r); + } + + return r; +} + +PyDoc_STRVAR(_ysynth_set_callback_doc, "Set audio callback"); + +/* Set Python audio callback */ +static PyObject *_ysynth_set_callback(PyObject *self, PyObject *args) +{ + PyObject *r = NULL; + PyYSynthContext *ctx; + PyObject *callback; + + if (PyArg_ParseTuple(args, "O!O", &PyYSynthContext_Type, (PyObject**)&ctx, + &callback)) { + /* We don't have to lock SDL audio because Python's GIL will + * have locked out the callback for us + */ + Py_XDECREF(ctx->callback); + ctx->callback = callback; + Py_INCREF(callback); + + r = Py_None; + Py_INCREF(r); + } + + return r; +} + +PyDoc_STRVAR(_ysynth_get_callback_doc, "Get audio callback"); + +/* Get Python audio callback */ +static PyObject *_ysynth_get_callback(PyObject *self, PyObject *args) +{ + PyObject *r = NULL; + PyYSynthContext *ctx; + + if (PyArg_ParseTuple(args, "O!", PyYSynthContext_Type, (PyObject**)&ctx)) { + if (ctx->callback) { + Py_INCREF(ctx->callback); + r = ctx->callback; + } else { + r = Py_None; + Py_INCREF(r); + } + } + + return r; +} + +/* Exported methods */ +static PyMethodDef pymod_methods[] = { + {"_ysynth_init", (PyCFunction)_ysynth_init, + METH_VARARGS, _ysynth_init_doc}, + {"_ysynth_shutdown", (PyCFunction)_ysynth_shutdown, + METH_VARARGS, _ysynth_shutdown_doc}, + {"_ysynth_set_callback", (PyCFunction)_ysynth_set_callback, + METH_VARARGS, _ysynth_set_callback_doc}, + {"_ysynth_get_callback", (PyCFunction)_ysynth_get_callback, + METH_VARARGS, _ysynth_get_callback_doc}, + {"hello", (PyCFunction)pymod_hello, + METH_VARARGS, hello_doc}, + {NULL, NULL} /* sentinel */ +}; + +PyDoc_STRVAR(module_doc, "Simple test module"); + +/* Shutdown SDL */ +void exitpymod(void) +{ + SDL_Quit(); +} + +/* Module initialisation, called by Python on import */ +PyMODINIT_FUNC +initpymod(void) +{ + PyObject *m; + + /* Setup SDL audio without signal handlers */ + if (SDL_Init(SDL_INIT_AUDIO | SDL_INIT_NOPARACHUTE) < 0) + return; + + /* Register exit handler + * XXX: Later on try to use Python's atexit module? + */ + atexit(exitpymod); + + puts("Hoi, module pymod loaded."); + m = Py_InitModule3("pymod", pymod_methods, module_doc); + if (m == NULL) + return; + + /* Setup exceptions */ + SDLError = PyErr_NewException("_ysynth.SDLError", NULL, NULL); + Py_INCREF(SDLError); + PyModule_AddObject(m, "SDLError", SDLError); + + /* Our module requires the GIL to be available */ + if (!PyEval_ThreadsInitialized()) { + puts("Initialising multithreading for Python"); + PyEval_InitThreads(); + } + + return; +} + diff --git a/ysynth.py b/ysynth.py new file mode 100644 index 0000000..0cfc3e8 --- /dev/null +++ b/ysynth.py @@ -0,0 +1,354 @@ +""" + Python audio synthesis framework. +""" + +from pymod import _ysynth_init, _ysynth_init, _ysynth_set_callback +from math import cos, sin, pi, modf +from itertools import izip + +__all__ = ['YSynth', 'Sin', 'Cos', 'Saw', 'RevSaw', 'Square', 'Pulse'] + +class YSynth(object): + """ + YSynth synthesis and audio context. + """ + + def __init__(self, samplerate=44100, channels=2): + """ + Setup a simple synthesizer. + + samplerate: Samples/second + channels: Number of output channels. + e.g. 1 for mono, 2 for stereo + """ + self.context = _ysynth_init(samplerate, channels) + self.samples = 0 + self.volume = 0.75 + + # XXX need to fetch 'acquired' values from ysynth context + self.samplerate = samplerate + self.channels = channels + self.graph = None + self.set_callback(self.default_callback) + + def deinit(self): + """ + Shutdown synthesizer, afterwards this object becomes useless. + """ + if self.context: + _ysynth_shutdown(self.context) + self.context = None + + def default_callback(self, *channels): + """ + Default synthesis callback. + """ + + # Outputs silence if no graph available + if not self.graph: + return + + # Process audio graph + buf_len = len(channels[0]) + for i in xrange(buf_len): + next_sample = next(self.graph) + for j in xrange(len(channels)): + channels[j][i] = next_sample + + # Advance sampleclock + self.samples += 1 + + def set_graph(self, graph): + graph.set_synth(self) + self.graph = iter(graph) + + def get_graph(self): + return self.graph + + def __del__(self): + """ + Deinitialise synth before being removed from mem + """ + print "Deinitialising" + self.deinit() + + def set_callback(self, func): + """ + Set audio output function. + + Without any callback the synthesizer will simply output silence. + + The callback should adhere the following signature: + def callback(channels): + + channels shall contain a list of lists and each list + contains a large block of 0.0 floats describing the next set of samples. + Those floats should be set to the chunk of audio. + """ + + _ysynth_set_callback(self.context, func) + + def get_callback(self): + return _ysynth_get_callback(self.context) + +class YConstant(object): + """ + Unchanging signal output. + """ + + def __init__(self, const): + self.const = float(const) + + def __iter__(self): + return iter(self()) + + def __call__(self): + while True: + yield self.const + + def set_synth(self, synth): + pass + +class YAudioGraphNode(object): + """ + Base audio graph node + + This base class provides YSynth's DSL behaviours such as + adding, substracting and the sample iteration protocol. + """ + + def __init__(self, *inputs): + self.inputs = [] + lens = 0 + + for stream in inputs: + if not isinstance(stream, YAudioGraphNode): + stream = YConstant(stream) + self.inputs.append(stream) + + self.inputs = tuple(self.inputs) + #for stream in inputs: + # if isinstance(stream, YAudioGraphNode): + # pass + # # Make sure all multi-channels share the same size + # if len(stream) != 1: + # if not lens: + # lens = len + # elif lens != len(stream): + # # TODO: Expand error info to contain sizes + # raise ValueError("Cannot combine different sized " + # "multi-channel streams") + + #self.channels = lens + + def __iter__(self): + """ + Initialise graph for synthesis, link to sampleclock. + """ + + # XXX This function is not graph cycle safe + + # FIXME: I need to unpack the channels from the input streams + # multiplex any mono-streams if there are multi-channel streams + # available, and then initialise every set of streams' component + # generator function by calling self with the correct arguments. + + # Setup input streams + for stream in self.inputs: + stream.set_synth(self.synth) + + # Setup self + + # The self.samples variable is used for protecting against + # multiple next() calls, next will only evaluate the next + # set of input samples if the synth's sampleclock has changed + self.samples = self.synth.samples - 1 + + # XXX self.last_sample is currently not initialised on purpose + # maybe this must be changed at a later time. + + # Connect the actual generator components + sample_iter = iter(self(izip(*self.inputs))) + + # Build the sample protection function + def sample_func(): + """ + Make sure multiple next() calls during the same + clock cycle yield the same sample value. + """ + while True: + if self.samples != self.synth.samples: + self.last_sample = next(sample_iter) + self.samples = self.synth.samples + + yield self.last_sample + + return sample_func() + + def set_synth(self, synth): + """ + Set this component's synthesizer. + + This is mainly useful for reading the synth's sampleclock. However every + active component requires a valid synthesizer. + """ + self.synth = synth + + def get_synth(self, synth): + """ + Return this component's synthesizer. + """ + return self.synth + + def __add__(self, other): + return Adder(self, other) + + def __radd__(self, other): + return Adder(other, self) + + def __sub__(self, other): + return Subtractor(self, other) + + def __rsub__(self, other): + return Subtractor(other, self) + + def __mul__(self, other): + return Multiplier(self, other) + + def __rmul__(self, other): + return Multiplier(other, self) + + def __div__(self, other): + return Divisor(self, other) + + def __rdiv__(self, other): + return Divisor(other, self) + + def __getitem__(self, delay): + """ + Return a delayed version of the output signal. + """ + return Delay(self, delay) + + def __next__(self): + """ + Process next sample. + """ + + def __call__(self): + raise NotImplementedError("You need to inherit this class") + + def process(self, *streams): + raise NotImplementedError("You need to inherit this class") + +# Basic signal arithmetic +class Adder(YAudioGraphNode): + def __call__(self, l): + for a, b in l: + yield a + b + +class Subtractor(YAudioGraphNode): + def __call__(self, l): + for a, b in l: + yield a - b + +class Multiplier(YAudioGraphNode): + def __call__(self, l): + for a, b in l: + yield a * b + +class Divisor(YAudioGraphNode): + def __call__(self, l): + for a, b in l: + yield a / b + +# Sample delay +class Delay(YAudioGraphNode): + def __call__(self, l): + buf = [0.0] * 4096 + samples = 0 + for sig, delay in l: + buf[samples] = sig + yield buf[(samples - delay) % 4096] + samples = (samples + 1) % 4096 + +# Base oscillator class +class YOscillator(YAudioGraphNode): + def __call__(self, l): + def cycle_gen(): + """ + This function generates the oscillation cycle + upon which all basic decoupled oscillators base their output + signal. + A decoupled oscillator is an oscillator + with its own cycle generator. These oscillators respond well to + incoming frequency changes, but due to the limitations of floating + point are at risk of drifting out of phase, coupled oscillators + are always in phase with each other. + The generated cycle ranges from 0.0 to 1.0 exclusive. + """ + + cycle = 0.0 + samples = 0 + + for freq, in l: + cycle = freq * samples / float(self.synth.samplerate) + yield modf(cycle)[0] + samples = (samples + 1) % self.synth.samplerate + + return self.oscillate(cycle_gen()) + + def oscillate(self, l): + raise NotImplementedError("Inherit this class") + +# Basic oscillators +class Sin(YOscillator): + """ + Sine wave oscillator + """ + def oscillate(self, l): + for pos in l: + yield sin(pos * 2 * pi) + +class Cos(YOscillator): + """ + Cosine wave oscillator + """ + def oscillate(self, l): + for pos in l: + yield cos(pos * 2 * pi) + +class Saw(YOscillator): + """ + Saw wave oscillator + """ + def oscillate(self, l): + for pos in l: + yield pos * 2.0 - 1.0 + +class RevSaw(YOscillator): + """ + Reverse Saw wave oscillator + """ + def oscillate(self, l): + for pos in l: + yield -(pos * 2.0 - 1.0) + +class Square(YOscillator): + """ + Square wave oscillator + """ + def oscillate(self, l): + for pos in l: + yield 1.0 if pos < 0.5 else -1.0 + +class Pulse(YOscillator): + """ + Pulse oscillator + """ + def oscillate(self, l): + prev_pos = 1.0 + for pos in l: + yield 1.0 if pos < prev_pos else 0.0 + prev_pos = pos +