Skip to content

Instantly share code, notes, and snippets.

@samueljohn
Last active March 4, 2017 21:41
Show Gist options
  • Star 5 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save samueljohn/4740180 to your computer and use it in GitHub Desktop.
Save samueljohn/4740180 to your computer and use it in GitHub Desktop.
Hacking a FIFOPlayer into Pyo. Yay. Pumping numpy arrays to the soundcard (or do some post-processing or mixing first)! Pyo is a fantastic sound processing library. To test this, copy these files into a freshly checked out pyo repository into pyo/externals. Then build pyo from source. In the long(er) run, I'd love to provide this to upstream. Cr…

We (Samuel John and Thomas Hermann) added a FIFOPlayer to Pyo, allowing you to directly (and almost copy-free) play numpy Arrays. See help(pyo.FIFOPlayer)! Output should be scaled [-1.0,1.0] (roughly).

See fifo_demo.py how to use a thread to generate audio and fill the queue.

# minimal imports.
# _core contains PyoObject, Mix, Dummy and some utility functions.
# _maps contains all SLMap objects used to set up slider controls.
from pyolib._core import *
from pyolib._maps import *
import Queue
class FIFOPlayer(PyoObject):
# Objects generating vectors of samples must inherit from PyoObject.
# Always provide a __doc__ string with the syntax below.
"""
Directly feed a FIFO buffer with audio samples (from NumPy arrays).
You can call put(x) to add to the buffer, which is implemented as a Python
stdlib queue. FIFOPlayer will accept any objects supporting the Python
Buffer protocol as long as the type of the elements is matching either
float (32Bit) or double (64Bit) depending on the pyo variant
(import pyo or pyo64). The number of elements in the array does not matter.
FIFOPlayer is thread-save.
Parentclass: PyoObject
Parameters:
maxsize : int
The maximum number of objects in the queue until the next call to
put(x) blocks. The size of each array does not matter. (Default 100)
copy_free : Boolean
If set to True (Default:False), the FIFOPlayer attemps to avoid memcpy
and just sets the pointer to point into the memory of the array in the
queue. This is slightly faster (however, getting an object from the
queue is the bigger overhead). Even if copy_free is True, sometimes
memcpy cannot be avoided if the size of an array is *not* a multiple
of the buffer size of the server. Remaining samples have to be copied.
Beware, `mul` and `add` (and any other) postprocessing will be ignored!
Methods:
put(x) : Feed x into the queue. The dtype of x must be correct but the
size does not matter. put() actually adds a reference.
Therefore, you should not change the contents of x, after putting
it into the FIFOPlayer.
If you use `import pyo` (the 32Bit version), make sure to
use dtype=numpy.float32 or the numpy methond `.astype(np.float32)`
on the numpy array.
If the FIFOPlayer has multiple streams, you may use the keyword
stream=i to put into the specific stream (this feature is
experimental).
Examples:
>>> import pyo # or pyo64 but then use np.float64 as the type of `a`
>>> import threading # for the sound generation thread
>>>
>>> s = pyo.Server()
>>> s.boot()
>>> fifo = pyo.FIFOPlayer().out()
>>>
>>> def proc(fifo, stopevent):
>>> import numpy as np
>>> freq = 500
>>> inc = 10
>>> while not stopevent.wait(0):
>>> a = np.cos(np.linspace(0,2*np.pi*freq,num=256*100))
>>> fifo.put(a.astype(np.float32))
>>> freq += inc
>>> if freq > 700:
>>> inc = -10
>>> elif freq < 500:
>>> inc = 10
>>>
>>> stopevent = threading.Event()
>>> producer = threading.Thread(name="Compute audio signal", target=proc, args=[fifo, stopevent])
>>> producer.start()
>>>
>>> s.gui(locals())
>>> # Remember, the producer may be blocked at fifo.put, because the queue is full,
>>> # so we set the stopevent and additionally, we have to remove one item from
>>> # the queue, so that the last fifo.put() call returns.
>>> stopevent.set()
>>> fifo._base_objs[0]._queue.get_nowait()
>>>
"""
# Do not forget "mul" and "add" attributes.
def __init__(self, maxsize=100, mul=1, add=0, always_memcpy=True):
PyoObject.__init__(self)
self._mul = mul
self._add = add
# Converts every arguments to lists (for multi-channel expansion).
mul, add, always_memcpy, lmax = convertArgsToLists(mul, add, always_memcpy)
# self._base_objs contains the list of XXX_base objects. Use "wrap" function to prevent "out of range" errors.
self._base_objs = [FIFOPlayer_base(wrap(mul,i), wrap(add,i), wrap(always_memcpy,i)) for i in range(lmax)]
for bo in self._base_objs:
# One queue for each channel
bo._queue = Queue.Queue(maxsize=maxsize)
def put(self, x, stream=0):
self._base_objs[stream].put(x)
/**************************************************************************
* A FIFOPlayer. Sending numpy arrays to the sound server *
* Basically any object supporting the Python buffer protocol can be *
* put into the _queue object (a stdlib Queue) *
* *
* Copyright 2013 Samuel John and Thomas Hermann *
* *
* *
* This file is based on the Template from Olivier under GPL --> *
* Copyright 2010 Olivier Belanger *
* *
* This file is part of pyo, a python module to help digital signal *
* processing script creation. *
* *
* pyo is free software: you can redistribute it and/or modify *
* it under the terms of the GNU General Public License as published by *
* the Free Software Foundation, either version 3 of the License, or *
* (at your option) any later version. *
* *
* pyo is distributed in the hope that it will be useful, *
* but WITHOUT ANY WARRANTY; without even the implied warranty of *
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
* GNU General Public License for more details. *
* *
* You should have received a copy of the GNU General Public License *
* along with pyo. If not, see <http://www.gnu.org/licenses/>. *
*************************************************************************/
#include <Python.h>
#include "structmember.h"
#include <math.h>
#include <stdio.h> // for memcopy
#include "pyomodule.h"
#include "streammodule.h"
#include "servermodule.h"
#include "dummymodule.h"
// set DEBUG to 1 if you want to see printf output of process_i
// for production, uncomment the next line ->
// #define DEBUG 1
#ifdef DEBUG
#include <time.h>
unsigned int GetTimeStamp() {
struct timeval tv;
gettimeofday(&tv, NULL);
return tv.tv_sec * (unsigned int)1000000 + tv.tv_usec;
}
#endif
/*****************************************************
FIFOPlayer struct. Here comes every attributes required
for the proper operation of the object.
*****************************************************/
typedef struct {
/* Mandatory macro intializing common properties of PyoObjects */
pyo_audio_HEAD
/* modebuffer keeps trace of the type of attributes that can
be float or PyoObject. Processing function is selected to
optimize operations. Need at least 2 slots for mul & add. */
int modebuffer[3];
/* Object's attributes. Attribute that can be float or PyoObject
must be defined as PyObject and must be coupled with a Stream
object (to hold the vector of samples of the audio signal). */
// we have none
PyObject *_queue; // a thread safe queue object (from Python's std-lib)
Py_buffer current_buffer; // The current buffer (already taken out of the queue)
int current_buffer_idx; // Track position in current_buffer
int data_idx; // Track position in data
MYFLT last_sample; // Value of the last sample to repeat it, if buffer runs empty
MYFLT *backup_data; // When changing data pointer, back it up to here
PyObject *python_str_get; // "get" string as Python object (we need it often)
PyObject *python_str_put;
int always_memcpy;
/* Floating-point values must always use the MYFLT macro which
handles float vs double builds. */
} FIFOPlayer;
/**********************************************************************
Processing functions. The last letter(s) of the name indicates the type
of object assigned to PyObject attribute, in this case "db" attribute.
"i" is for floating-point value and "a" is for audio signal.
**********************************************************************/
static void
FIFOPlayer_process_i(FIFOPlayer *self) {
int total; // number of samples in the self->current_buffer
int available; // number of samples not yet processed
int needed = self->bufsize - self->data_idx; // number of items needed
#ifdef DEBUG
unsigned int t_start = GetTimeStamp(), t_delta;
#endif
// Do we have a current_buffer?
if (self->current_buffer.obj != NULL) {
assert(self->current_buffer.itemsize == sizeof(MYFLT));
total = (int)self->current_buffer.len / (int)self->current_buffer.itemsize;
available = total - self->current_buffer_idx;
#ifdef DEBUG
printf("--> Current_buffer: total=%i available=%i needed=%i.\n", total, available, needed);
#endif
// First, we let self->stream->data point to the chunk of mem that Pyo
// allocated for us in the first place.
if (self->backup_data != NULL) {
self->stream->data = self->backup_data;
self->backup_data = NULL;
}
if (available == 0) {
// Clean-up if we are finished with the self->current_buffer
#ifdef DEBUG
unsigned int t_start_clean = GetTimeStamp(), t_delta_clean;
#endif
// PyBuffer_Release will also DECREF the object we got from the
// self->_queue. We didn't store a reference to that object, but
// the Py_Buffer->obj stores a reference and calling
// PyBuffer_Release will Py_DECREF it.
// Doc: http://docs.python.org/3.3/c-api/buffer.html
PyBuffer_Release(&(self->current_buffer));
self->current_buffer_idx = 0;
total = 0;
available = 0;
// Note, don't (re)set self->data_idx here!! It's value is needed.
#ifdef DEBUG
t_delta_clean = GetTimeStamp() - t_start_clean;
printf( " * Clean up: PyBuffer_Release(). Now total=%i available=%i needed=%i (%u µs)\n",
total, available, needed, t_delta_clean);
#endif
} else if (self->always_memcpy == 0 && available >= self->bufsize && needed == self->bufsize) {
// self->data buffer is empty and we have enough samples available
// in the current_buffer so we can basically just let the
// self->data pointer point into the right position in the
// current_buffer.
#ifdef DEBUG
unsigned int t_start_pointer = GetTimeStamp(), t_delta_pointer;
#endif
// But first, we store the current self->data into
// self->backup_data for later reuse.
if (self->backup_data == NULL) {
self->backup_data = self->data;
}
// Copy-free pointer reassignment. You may consider this a hack.
// For some reason self->data does not work, because the Server uses
// self->stream->data. So we have to change the self->stream->data pointer.
self->stream->data = (MYFLT*)(self->current_buffer.buf) + self->current_buffer_idx;
// Update some counters and last_sample
self->current_buffer_idx += self->bufsize;
available -= self->bufsize;
needed -= self->bufsize;
self->last_sample = self->data[self->bufsize-1];
#ifdef DEBUG
t_delta_pointer = GetTimeStamp() - t_start_pointer;
printf( " * Let self->data point to <%u>: total=%i available=%i needed=%i (%u µs)\n",
(unsigned int)self->stream->data, total, available, needed, t_delta_pointer);
#endif
} else {
// Fall back to memcpy!
#ifdef DEBUG
unsigned int t_start_memcpy = GetTimeStamp(), t_delta_memcpy;
#endif
// Copy what is available but not more than what is needed.
// memcpy operates on bytes!
int n = (available < needed) ? available : needed;
memcpy( self->data + self->data_idx, /* dest (pointer has right type) */
(MYFLT*)(self->current_buffer.buf) + self->current_buffer_idx, /* source */
n * sizeof(MYFLT));
self->current_buffer_idx += n;
self->data_idx += n;
available -= n;
needed -= n;
self->last_sample = self->data[self->data_idx-1];
#ifdef DEBUG
t_delta_memcpy = GetTimeStamp() - t_start_memcpy;
printf( " * memcpy (%lu bytes (%i MYFLTs) to data_idx=%i): total=%i available=%i needed=%i (%u µs)\n",
n * sizeof(MYFLT), n, self->data_idx, total, available, needed, t_delta_memcpy);
#endif
}
#ifdef DEBUG
// available should not be negative!
assert(available >= 0);
// if there is more available, it should have been used up and needed == 0 !
if (available > 0) { assert(needed == 0); }
#endif
} // end: if (self->current_buffer.obj != NULL)
if (needed > 0 && self->current_buffer.obj == NULL) {
// We have to get the next buffer from the queue (in non-blocking style)
// because more samples are needed to be put into self->data ...
#ifdef DEBUG
unsigned int t_start_get = GetTimeStamp(), t_delta_get;
#endif
PyObject *ret = PyObject_CallMethodObjArgs( self->_queue,
self->python_str_get,
Py_False, /*non-blocking*/
NULL ); // NULL as last arg to flag the end
if (ret) {
// Python Buffer structure, so we don't need numpy's include files
if (PyObject_GetBuffer(ret, &(self->current_buffer), PyBUF_CONTIG_RO ) != 0) {
printf("ERROR: FIFOPlayer: Could not get C-continuous buffer from _queue.\n");
PyErr_Print();
exit(-3);
}
if (PyErr_Occurred()) {
// Exporter may raise an err if it does not support PyBUF_CONTIG_RO
printf("ERROR: FIFOPlayer exception during PyObject_GetBuffer...\n");
PyErr_Print();
exit(-1);
}
// The buffer's ndim (number of dimensions) must be equal to one
if (self->current_buffer.ndim != 1) {
printf("ERROR: FIFOPlayer: Given buffer has to be one-dimensional for audio!\n");
exit(-4);
}
// Alright, we got a new self->current_buffer, so in a few lines
// down we recursively call FIFOPlayer_process_i and we don't need
// to update any of the vars, as they will be updated right after
// the beginning of FIFOPlayer_process_i!
}
#ifdef DEBUG
t_delta_get = GetTimeStamp() - t_start_get;
if (self->current_buffer.obj != NULL) {
printf( " * Getting new Py_Buffer from queue: itemsize=%u, len=%u, ndim=%u (%u µs)\n",
(unsigned int)self->current_buffer.itemsize,
(unsigned int)(self->current_buffer.len/self->current_buffer.itemsize),
(unsigned int)self->current_buffer.ndim,
t_delta_get );
}
#endif
} // enf of getting next buffer from queue
if (needed > 0) {
if (self->current_buffer.obj == NULL) {
// The queue was empty, so we resort to play the last sample (constant output; no sound!)
#ifdef DEBUG
unsigned int t_start_const = GetTimeStamp(), t_delta_const;
#endif
int i;
for (i = self->data_idx; i < self->bufsize; i++) {
self->data[i] = self->last_sample;
}
#ifdef DEBUG
t_delta_const = GetTimeStamp() - t_start_const;
if (self->data_idx > 0 ) {
// Don't print this on empty (idle) loop
printf(" * Buffer empty. Const filling with last sample=%f (%u µs)\n", (double)self->last_sample, t_delta_const);
}
#endif
self->data_idx = 0;
} else {
// recursive call to FIFOPlayer_process_i
#ifdef DEBUG
printf("--> Recursive call to FIFOPlayer_process_i to fill %i remaining samples...\n", needed);
#endif
// self->data_idx will be used to determine where to continue.
FIFOPlayer_process_i(self);
}
} else {
// The self->data buffer has been filled. Nothing more needed.
#ifdef DEBUG
t_delta = GetTimeStamp() - t_start;
if (self->data_idx > 0) {
// Don't print this on empty (idle) loop, so we check data_idx
printf("==> Finished. The self->data buffer has been filled (%u µs).\n\n", t_delta);
}
// should not be negative!
assert(needed >= 0);
#endif
self->data_idx = 0; // reset for next call
}
}
/**********************************************************************
End of processing function definitions.
**********************************************************************/
/**********************************************************************
Post-Processing functions. These are functions where are applied "mul"
and "add" attributes. Macros are defined in pyomodule.h. Just keep them
and change the object's name.
**********************************************************************/
static void FIFOPlayer_postprocessing_ii(FIFOPlayer *self) { POST_PROCESSING_II };
static void FIFOPlayer_postprocessing_ai(FIFOPlayer *self) { POST_PROCESSING_AI };
static void FIFOPlayer_postprocessing_ia(FIFOPlayer *self) { POST_PROCESSING_IA };
static void FIFOPlayer_postprocessing_aa(FIFOPlayer *self) { POST_PROCESSING_AA };
static void FIFOPlayer_postprocessing_ireva(FIFOPlayer *self) { POST_PROCESSING_IREVA };
static void FIFOPlayer_postprocessing_areva(FIFOPlayer *self) { POST_PROCESSING_AREVA };
static void FIFOPlayer_postprocessing_revai(FIFOPlayer *self) { POST_PROCESSING_REVAI };
static void FIFOPlayer_postprocessing_revaa(FIFOPlayer *self) { POST_PROCESSING_REVAA };
static void FIFOPlayer_postprocessing_revareva(FIFOPlayer *self) { POST_PROCESSING_REVAREVA };
/**********************************************************************
setProcMode is called everytime a new value is assigned to one of the
object's attributes. Here are specified pointers to processing and
post-processing functions according to attribute types.
**********************************************************************/
static void
FIFOPlayer_setProcMode(FIFOPlayer *self)
{
// FIFOPlayer only defines process_i
self->proc_func_ptr = FIFOPlayer_process_i;
int muladdmode;
/* "muladdmode" swith statement should be left as is. */
muladdmode = self->modebuffer[0] + self->modebuffer[1] * 10;
switch (muladdmode) {
case 0:
self->muladd_func_ptr = FIFOPlayer_postprocessing_ii;
break;
case 1:
self->muladd_func_ptr = FIFOPlayer_postprocessing_ai;
break;
case 2:
self->muladd_func_ptr = FIFOPlayer_postprocessing_revai;
break;
case 10:
self->muladd_func_ptr = FIFOPlayer_postprocessing_ia;
break;
case 11:
self->muladd_func_ptr = FIFOPlayer_postprocessing_aa;
break;
case 12:
self->muladd_func_ptr = FIFOPlayer_postprocessing_revaa;
break;
case 20:
self->muladd_func_ptr = FIFOPlayer_postprocessing_ireva;
break;
case 21:
self->muladd_func_ptr = FIFOPlayer_postprocessing_areva;
break;
case 22:
self->muladd_func_ptr = FIFOPlayer_postprocessing_revareva;
break;
}
}
/**********************************************************************
"compute_next_data_frame" is the function called by the server on each
processing loop. Processing functions are executed first and then, the
post-processing function is called.
**********************************************************************/
static void
FIFOPlayer_compute_next_data_frame(FIFOPlayer *self)
{
(*self->proc_func_ptr)(self);
(*self->muladd_func_ptr)(self);
}
/**********************************************************************
Garbage-collector. Every PyObject and Stream objects must be added to
"traverse" and "clear" functions to allow the interpreter to clean memory
when ref count drop to zero. These functions must be registered as
"tp_traverse" and "tp_clear" in the object's PyTypeObject structure below.
Py_TPFLAGS_HAVE_GC flag must be added to the class flags (tp_flags).
**********************************************************************/
static int
FIFOPlayer_traverse(FIFOPlayer *self, visitproc visit, void *arg)
{
pyo_VISIT
Py_VISIT(self->python_str_put);
Py_VISIT(self->python_str_get);
Py_VISIT(self->_queue);
return 0;
}
static int
FIFOPlayer_clear(FIFOPlayer *self)
{
pyo_CLEAR
Py_CLEAR(self->python_str_get);
Py_CLEAR(self->python_str_put);
Py_CLEAR(self->_queue);
return 0;
}
/**********************************************************************
Deallocation function, registered as "tp_dealloc". Here must be freed
every malloc'ed and realloc'ed variables of the object.
**********************************************************************/
static void
FIFOPlayer_dealloc(FIFOPlayer* self)
{
pyo_DEALLOC
FIFOPlayer_clear(self);
// in FIFOPlayer_new, we increased Py_False and Py_True (the global objects)
// to pass them to _queue.get(False) and _queue.put(...,True) instead
// of INCREFing and DECREFing them after each call.
Py_DECREF(Py_False);
Py_DECREF(Py_True);
self->ob_type->tp_free((PyObject*)self);
}
/**********************************************************************
Function called at the object creation, registered as "tp_new".
**********************************************************************/
static PyObject *
FIFOPlayer_new(PyTypeObject *type, PyObject *args, PyObject *kwds)
{
int i;
PyObject *multmp=NULL, *addtmp=NULL, *always_memcpy=NULL;
/* Object's allocation */
FIFOPlayer *self;
self = (FIFOPlayer *)type->tp_alloc(type, 0);
/* Initialization of object's attributes */
self->modebuffer[0] = 0;
self->modebuffer[1] = 0;
self->modebuffer[2] = 0; // so setProcMode will set self->proc_func_ptr to FIFOPlayer_process_i
/* Initialization of common properties of PyoObject */
INIT_OBJECT_COMMON
/* Assign the stream's pointer to the object's processing callback. */
/* The stream struct is what is registered in the server. */
Stream_setFunctionPtr(self->stream, FIFOPlayer_compute_next_data_frame);
/* Assign setProcMode to the common mode_func_ptr pointer */
self->mode_func_ptr = FIFOPlayer_setProcMode;
/* Object's keyword list. These are arguments to the object's creation */
static char *kwlist[] = { "mul", "add", "copy_free", NULL};
/* Argument parsing. If there is float values in the type list ("|OO"),
a macro must be given to handle float vs double argument. See pyomodule.h
for the list of macros already available. */
if (! PyArg_ParseTupleAndKeywords(args, kwds, "|OOO", kwlist, &multmp, &addtmp, &always_memcpy))
Py_RETURN_NONE;
self->backup_data = NULL;
self->current_buffer_idx = 0;
self->data_idx = 0;
self->last_sample = (MYFLT)0.0;
self->python_str_get = PyString_FromString("get");
self->python_str_put = PyString_FromString("put");
self->always_memcpy = !PyObject_IsTrue(always_memcpy);
// these two are used globally to pass as an argument,
Py_INCREF(Py_False);
Py_INCREF(Py_True);
if (multmp) {
PyObject_CallMethod((PyObject *)self, "setMul", "O", multmp);
}
if (addtmp) {
PyObject_CallMethod((PyObject *)self, "setAdd", "O", addtmp);
}
/* Add the object's stream struct to the server registry */
PyObject_CallMethod(self->server, "addStream", "O", self->stream);
/* Call setProcMode to be sure pointers are correctly assigned */
(*self->mode_func_ptr)(self);
return (PyObject *)self;
}
/**********************************************************************
Functions common to almost all PyoObjects. The macros are defined in
pyomodule.h. Sometime the "out" function is removed to prevent sending
non-normalized signal (like the fft analysis of FFT object) to the soundcard.
Object dependant initializations can be added before PLAY, OUT and STOP macros.
**********************************************************************/
static PyObject * FIFOPlayer_getServer(FIFOPlayer *self) { GET_SERVER };
static PyObject * FIFOPlayer_getStream(FIFOPlayer *self) { GET_STREAM };
static PyObject * FIFOPlayer_setMul(FIFOPlayer *self, PyObject *arg) { SET_MUL };
static PyObject * FIFOPlayer_setAdd(FIFOPlayer *self, PyObject *arg) { SET_ADD };
static PyObject * FIFOPlayer_setSub(FIFOPlayer *self, PyObject *arg) { SET_SUB };
static PyObject * FIFOPlayer_setDiv(FIFOPlayer *self, PyObject *arg) { SET_DIV };
static PyObject * FIFOPlayer_play(FIFOPlayer *self, PyObject *args, PyObject *kwds) { PLAY };
static PyObject * FIFOPlayer_out(FIFOPlayer *self, PyObject *args, PyObject *kwds) { OUT };
static PyObject * FIFOPlayer_stop(FIFOPlayer *self) { STOP };
static PyObject * FIFOPlayer_multiply(FIFOPlayer *self, PyObject *arg) { MULTIPLY };
static PyObject * FIFOPlayer_inplace_multiply(FIFOPlayer *self, PyObject *arg) { INPLACE_MULTIPLY };
static PyObject * FIFOPlayer_add(FIFOPlayer *self, PyObject *arg) { ADD };
static PyObject * FIFOPlayer_inplace_add(FIFOPlayer *self, PyObject *arg) { INPLACE_ADD };
static PyObject * FIFOPlayer_sub(FIFOPlayer *self, PyObject *arg) { SUB };
static PyObject * FIFOPlayer_inplace_sub(FIFOPlayer *self, PyObject *arg) { INPLACE_SUB };
static PyObject * FIFOPlayer_div(FIFOPlayer *self, PyObject *arg) { DIV };
static PyObject * FIFOPlayer_inplace_div(FIFOPlayer *self, PyObject *arg) { INPLACE_DIV };
static PyObject *
FIFOPlayer_put(FIFOPlayer *self, PyObject *arg)
{
if (arg == NULL) {
Py_INCREF(Py_None);
return Py_None;
}
// Does the given arg support the buffer protocol at all?
if (!PyObject_CheckBuffer(arg)) {
PyErr_SetString(PyExc_TypeError, "FIFOPlayer.put: Object with buffer protocol expected (e.g. str, numpy.array, ...)");
return NULL;
}
// we pass True as second arg, so it will block until the queue has a free slot...
PyObject* ret;
Py_INCREF(Py_True);
ret = PyObject_CallMethodObjArgs(self->_queue, self->python_str_put, arg, Py_True, NULL); // NULL as last arg to flag the end
Py_DECREF(Py_True);
if (!ret) {
// todo: raise err or what? I dunno. queue.put(arg,True) should not fail
PyErr_SetString(PyExc_TypeError, "FIFOPlayer.put: Object with buffer protocol expected (e.g. str, numpy.array, ...)");
return NULL;
}
Py_DECREF(ret);
Py_INCREF(Py_None);
return Py_None;
}
/**********************************************************************
Object's members descriptors. Here should appear the server and stream
refs and also every PyObjects declared in the object's struct.
Registered as "tp_members".
**********************************************************************/
static PyMemberDef FIFOPlayer_members[] = {
{"server", T_OBJECT_EX, offsetof(FIFOPlayer, server), 0, "Pyo server."},
{"stream", T_OBJECT_EX, offsetof(FIFOPlayer, stream), 0, "Stream object."},
{"mul", T_OBJECT_EX, offsetof(FIFOPlayer, mul), 0, "Mul factor."},
{"add", T_OBJECT_EX, offsetof(FIFOPlayer, add), 0, "Add factor."},
{"_queue", T_OBJECT_EX, offsetof(FIFOPlayer, _queue), 0, "The Python Queue (from stdlib) for this channel."},
{NULL} /* Sentinel */
};
/**********************************************************************
Object's method descriptors. Here should appear every methods needed to
be exposed to the python interpreter, casted to PyCFunction.
Registered as "tp_methods".
**********************************************************************/
static PyMethodDef FIFOPlayer_methods[] = {
{"getServer", (PyCFunction)FIFOPlayer_getServer, METH_NOARGS, "Returns server object."},
{"_getStream", (PyCFunction)FIFOPlayer_getStream, METH_NOARGS, "Returns stream object."},
{"play", (PyCFunction)FIFOPlayer_play, METH_VARARGS|METH_KEYWORDS, "Starts dbuting without sending sound to soundcard."},
{"out", (PyCFunction)FIFOPlayer_out, METH_VARARGS|METH_KEYWORDS, "Starts dbuting and sends sound to soundcard channel speficied by argument."},
{"stop", (PyCFunction)FIFOPlayer_stop, METH_NOARGS, "Stops dbuting."},
{"setMul", (PyCFunction)FIFOPlayer_setMul, METH_O, "Sets oscillator mul factor."},
{"setAdd", (PyCFunction)FIFOPlayer_setAdd, METH_O, "Sets oscillator add factor."},
{"setSub", (PyCFunction)FIFOPlayer_setSub, METH_O, "Sets inverse add factor."},
{"setDiv", (PyCFunction)FIFOPlayer_setDiv, METH_O, "Sets inverse mul factor."},
{"put", (PyCFunction)FIFOPlayer_put, METH_O, "Put a numpy array into the FIFO buffer."},
{NULL} /* Sentinel */
};
/**********************************************************************
Number protocol struct. This allow to override the behaviour of mathematical
operations on the object. At the moment of the writing, only +, -, * and /,
and the corresponding "inplace" operations (a *= 1) are implemented. More
to comme in the future. Registered as "tp_as_number".
**********************************************************************/
static PyNumberMethods FIFOPlayer_as_number = {
(binaryfunc)FIFOPlayer_add, /*nb_add*/
(binaryfunc)FIFOPlayer_sub, /*nb_subtract*/
(binaryfunc)FIFOPlayer_multiply, /*nb_multiply*/
(binaryfunc)FIFOPlayer_div, /*nb_divide*/
0, /*nb_remainder*/
0, /*nb_divmod*/
0, /*nb_power*/
0, /*nb_neg*/
0, /*nb_pos*/
0, /*(unaryfunc)array_abs,*/
0, /*nb_nonzero*/
0, /*nb_invert*/
0, /*nb_lshift*/
0, /*nb_rshift*/
0, /*nb_and*/
0, /*nb_xor*/
0, /*nb_or*/
0, /*nb_coerce*/
0, /*nb_int*/
0, /*nb_long*/
0, /*nb_float*/
0, /*nb_oct*/
0, /*nb_hex*/
(binaryfunc)FIFOPlayer_inplace_add, /*inplace_add*/
(binaryfunc)FIFOPlayer_inplace_sub, /*inplace_subtract*/
(binaryfunc)FIFOPlayer_inplace_multiply, /*inplace_multiply*/
(binaryfunc)FIFOPlayer_inplace_div, /*inplace_divide*/
0, /*inplace_remainder*/
0, /*inplace_power*/
0, /*inplace_lshift*/
0, /*inplace_rshift*/
0, /*inplace_and*/
0, /*inplace_xor*/
0, /*inplace_or*/
0, /*nb_floor_divide*/
0, /*nb_true_divide*/
0, /*nb_inplace_floor_divide*/
0, /*nb_inplace_true_divide*/
0, /* nb_index */
};
/**************************************************************
Object's type declaration. The type's name should be "XXXType",
where XXX is replaced by the name of the object.
Fields in PyTypeObject that are not used should be 0.
**************************************************************/
PyTypeObject FIFOPlayerType = {
PyObject_HEAD_INIT(NULL)
0, /*ob_size*/
/* How the object will be exposed to the
python interpreter. The name of the C component
of a PyoObject should be "XXX_base", where XXX
is replaced by the name of the object. These
objects are actually never directly created
by the user. They are used inside the object's
Python class to handle multi-channel expansion.*/
"_pyo.FIFOPlayer_base", /*tp_name*/
sizeof(FIFOPlayer), /*tp_basicsize*/
0, /*tp_itemsize*/
(destructor)FIFOPlayer_dealloc, /*tp_dealloc*/
0, /*tp_print*/
0, /*tp_getattr*/
0, /*tp_setattr*/
0, /*tp_dbare*/
0, /*tp_repr*/
&FIFOPlayer_as_number, /*tp_as_number*/
0, /*tp_as_sequence*/
0, /*tp_as_mapping*/
0, /*tp_hash */
0, /*tp_call*/
0, /*tp_str*/
0, /*tp_getattro*/
0, /*tp_setattro*/
0, /*tp_as_buffer*/
Py_TPFLAGS_DEFAULT | Py_TPFLAGS_BASETYPE | Py_TPFLAGS_HAVE_GC | Py_TPFLAGS_CHECKTYPES, /*tp_flags*/
"FIFOPlayer C-implementation. Plays audio data as raw samples from numpy arrays.\n"
"Other objects that support the Python buffer protocol, are valid, too, as long as the element type matches.\n"
"For pyo", /* tp_doc */
(traverseproc)FIFOPlayer_traverse, /* tp_traverse */
(inquiry)FIFOPlayer_clear, /* tp_clear */
0, /* tp_richdbare */
0, /* tp_weaklistoffset */
0, /* tp_iter */
0, /* tp_iternext */
FIFOPlayer_methods, /* tp_methods */
FIFOPlayer_members, /* tp_members */
0, /* tp_getset */
0, /* tp_base */
0, /* tp_dict */
0, /* tp_descr_get */
0, /* tp_descr_set */
0, /* tp_dictoffset */
0, /* tp_init */
0, /* tp_alloc */
FIFOPlayer_new, /* tp_new */
};
/**************************************************
Declare each external object type as "extern" to be sure
that the pyomodule will be aware of their existence.
**************************************************/
extern PyTypeObject FIFOPlayerType;
/**********************************************************
This macro is called at runtime to include external objects
in "_pyo" module. Add a line calling "module_add_object"
with your object's name and type for each external object.
**********************************************************/
#define EXTERNAL_OBJECTS \
module_add_object(m, "FIFOPlayer_base", &FIFOPlayerType); \
import pyo64 as pyo
import threading # for the sound generation thread
s = pyo.Server()
s.boot()
fifo = pyo.FIFOPlayer(maxsize=100, mul=.3)
fifo.out()
# play around with n to hear the same sound with different buffer sizes
# Should not be larger than 256*100, because then a single float would be
# in every buffer we put into the fifo. On my machine, I can increase n to
# around 2560 which is insane since each numpy array only contains 10 floats.
# If you can affort it, make the arrays you put into the fifo a multiple of
# s.getBufferSize()
n = 100
def proc(fifo, n, stopevent):
import numpy as np
freq = 500
inc = 10
while not stopevent.wait(0):
# np.sin results in click sounds, so use np.cos (zero-crossings)
a = np.cos( np.linspace( 0, 2*np.pi*freq, num=256*100 ) ).astype(
np.float64 if pyo_use_double else np.float32)
# We could just put the array `a` into fifo.put(a), which would be
# faster but for testing and debugging, we want to cut `a` into
# smaller arrays and pump them into the fifo. The sound should be the
# same.
for i in xrange(n):
if stopevent.wait(0):
break
else:
fifo.put( (a[i*(a.size//n):(i+1)*(a.size//n)]) )
freq += inc
if freq > 700:
inc = -10
elif freq < 500:
inc = 10
print("qsize: {}".format(fifo._base_objs[0]._queue.qsize()))
# Don't use Pattern, since it will be called by the server in the same
# thread as the server and block if the queue gets over maxsize.
#pat = pyo.Pattern(function=proc, time=0.2).play()
# Instead we just generate the data as fast as we can in a second thread
# and let it wait (i.e. block) when the queue reaches maxsize. The thread
# will continue when the server has consumed an item from the queue,
stopevent = threading.Event()
producer = threading.Thread(name="Compute audio signal", target=proc, args=[fifo, n, stopevent])
producer.start()
s.gui(locals())
# Remember, the producer may be blocked at fifo.put, because the queue is full,
# so we set the stopevent and additionally, we have to remove one item from
# the queue, so that the last fifo.put() call returns.
stopevent.set()
fifo._base_objs[0]._queue.get_nowait()
@samueljohn
Copy link
Author

This is all GPL licensed since pyo is GPL :/

@MathieuDuponchelle
Copy link

Great !

@samueljohn
Copy link
Author

Thanks. I updated with a fifo_demo.py that uses a thread (to avoid blocking).

@samueljohn
Copy link
Author

Update: Hack to make the demo (thread) correctly stop.

TODO: I should make fifo.stop() that does what the demo is now doing manually to shut down the thread.

@2147483647
Copy link

This is great, thanks for sharing !

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment