浏览代码

YSynth initial commit.

- Synthesizer works but uses a lot of CPU.
master
Bas Weelinck 13 年前
当前提交
bbe8646987
共有 4 个文件被更改,包括 845 次插入0 次删除
  1. +17
    -0
      Makefile
  2. +137
    -0
      hoi.c
  3. +337
    -0
      pymod.c
  4. +354
    -0
      ysynth.py

+ 17
- 0
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


+ 137
- 0
hoi.c 查看文件

@@ -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;
}


+ 337
- 0
pymod.c 查看文件

@@ -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;
}


+ 354
- 0
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


正在加载...
取消
保存