| @@ -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 | |||||
| @@ -0,0 +1,137 @@ | |||||
| #include <math.h> | |||||
| #include <stdlib.h> | |||||
| #include <SDL/SDL.h> | |||||
| #include <SDL/SDL_mixer.h> | |||||
| #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: <mp3-file>\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; | |||||
| } | |||||
| @@ -0,0 +1,337 @@ | |||||
| #include <python2.7/Python.h> | |||||
| #include <SDL/SDL.h> | |||||
| /* 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; | |||||
| } | |||||
| @@ -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 | |||||