Big change to new stream configuration. Possibility to include output channels back to input

This commit is contained in:
Anne de Jong 2020-08-03 20:17:52 +02:00
parent b17178c4a7
commit 287b0cfe83
12 changed files with 640 additions and 328 deletions

View File

@ -76,7 +76,8 @@ else()
set(win32 false) set(win32 false)
set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -fPIC -std=c11 \ set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -fPIC -std=c11 \
-Werror=incompatible-pointer-types") -Werror=incompatible-pointer-types")
include_directories(/usr/local/include/rtaudio)
link_directories(/usr/local/lib)
endif(CMAKE_SYSTEM_NAME STREQUAL "Windows") endif(CMAKE_SYSTEM_NAME STREQUAL "Windows")

View File

@ -6,3 +6,4 @@ from .lasp_octavefilter import *
from .lasp_slm import * from .lasp_slm import *
from .lasp_weighcal import * from .lasp_weighcal import *
from .wrappers import * from .wrappers import *
from .device import AvType

76
lasp/c/lasp_pyarray.h Normal file
View File

@ -0,0 +1,76 @@
// ascee_python.h
//
// Author: J.A. de Jong - ASCEE
//
// Description:
// Routine to generate a Numpy array from an arbitrary buffer. Careful, this
// code should both be C and C++ compatible!!
//////////////////////////////////////////////////////////////////////
#pragma once
#ifndef LASP_PYARRAY_H
#define LASP_PYARRAY_H
#define TRACERPLUS (-10)
#include <numpy/ndarrayobject.h>
#ifdef LASP_DOUBLE_PRECISION
#define LASP_NUMPY_FLOAT_TYPE NPY_FLOAT64
#define LASP_NUMPY_COMPLEX_TYPE NPY_COMPLEX128
#else
#define LASP_NUMPY_FLOAT_TYPE NPY_FLOAT32
#endif
#ifdef MS_WIN64
/**
* Function passed to Python to use for cleanup of
* foreignly obtained data.
**/
static inline void capsule_cleanup(void *capsule) {
void *memory = PyCapsule_GetPointer(capsule, NULL);
free(memory);
}
#endif
static inline PyObject *data_to_ndarray(void *data, int n_rows, int n_cols,
int typenum, bool transfer_ownership,
bool F_contiguous) {
/* fprintf(stderr, "Enter data_to_ndarray\n"); */
assert(data);
import_array();
npy_intp dims[2] = {n_rows, n_cols};
assert(n_rows > 0);
assert(n_cols > 0);
PyArrayObject *arr =
(PyArrayObject *)PyArray_SimpleNewFromData(2, dims, typenum, data);
if (!arr) {
return NULL;
}
if (F_contiguous) {
PyArray_ENABLEFLAGS(arr, NPY_ARRAY_F_CONTIGUOUS);
}
if (transfer_ownership == true) {
#ifdef MS_WIN64
// The default destructor of Python cannot free the data, as it is allocated
// with malloc. Therefore, with this code, we tell Numpy/Python to use
// the capsule_cleanup constructor. See:
// https://stackoverflow.com/questions/54269956/crash-of-jupyter-due-to-the-use-of-pyarray-enableflags/54278170#54278170
// Note that in general it was disadvised to build all C code with MinGW on
// Windows. We do it anyway, see if we find any problems on the way.
void *capsule = PyCapsule_New(mat->_data, NULL, capsule_cleanup);
PyArray_SetBaseObject(arr, capsule);
#endif
/* fprintf(stderr, "============Ownership transfer================\n"); */
PyArray_ENABLEFLAGS(arr, NPY_OWNDATA);
}
/* fprintf(stderr, "Exit data_to_ndarray\n"); */
return (PyObject *) arr;
}
#endif // LASP_PYARRAY_H
//////////////////////////////////////////////////////////////////////

View File

@ -9,25 +9,8 @@
#ifndef LASP_PYTHON_H #ifndef LASP_PYTHON_H
#define LASP_PYTHON_H #define LASP_PYTHON_H
#define TRACERPLUS (-10) #define TRACERPLUS (-10)
#include <numpy/ndarrayobject.h> #include "lasp_pyarray.h"
#ifdef LASP_DOUBLE_PRECISION
#define LASP_NUMPY_FLOAT_TYPE NPY_FLOAT64
#define LASP_NUMPY_COMPLEX_TYPE NPY_COMPLEX128
#else
#define LASP_NUMPY_FLOAT_TYPE NPY_FLOAT32
#endif
#ifdef MS_WIN64
/**
* Function passed to Python to use for cleanup of
* foreignly obtained data.
**/
static inline void capsule_cleanup(void* capsule) {
void *memory = PyCapsule_GetPointer(capsule, NULL);
free(memory);
}
#endif
/** /**
* Create a numpy array from an existing dmat. * Create a numpy array from an existing dmat.
* *
@ -38,50 +21,31 @@ static inline void capsule_cleanup(void* capsule) {
* @return Numpy array * @return Numpy array
*/ */
static inline PyObject* dmat_to_ndarray(dmat* mat,bool transfer_ownership) { static inline PyObject* dmat_to_ndarray(dmat* mat,bool transfer_ownership) {
fsTRACE(15);
dbgassert(mat,NULLPTRDEREF); dbgassert(mat,NULLPTRDEREF);
dbgassert(mat->_data,NULLPTRDEREF);
/* fprintf(stderr, "Enter dmat_to_ndarray\n"); */
import_array();
// Dimensions given in wrong order, as mat is // Dimensions given in wrong order, as mat is
// Fortran-contiguous. Later on we transpose the result. This is // Fortran-contiguous. Later on we transpose the result. This is
// more easy than using the PyArray_New syntax. // more easy than using the PyArray_New syntax.
npy_intp dims[] = {mat->n_cols,mat->n_rows}; PyObject* arr = data_to_ndarray(mat->_data,
PyObject* arr_t = PyArray_SimpleNewFromData(2,dims, mat->n_rows,
LASP_NUMPY_FLOAT_TYPE, mat->n_cols,
mat->_data); LASP_NUMPY_FLOAT_TYPE,
if(!arr_t) { transfer_ownership,
true); // Fortran-contiguous
if(transfer_ownership) {
mat->_foreign_data = true;
}
if(!arr) {
WARN("Array creation failure"); WARN("Array creation failure");
feTRACE(15); feTRACE(15);
return NULL; return NULL;
} }
/* fprintf(stderr, "Exit dmat_to_ndarray\n"); */
if(transfer_ownership) {
mat->_foreign_data = true;
#ifdef MS_WIN64
// The default destructor of Python cannot free the data, as it is allocated
// with malloc. Therefore, with this code, we tell Numpy/Python to use
// the capsule_cleanup constructor. See:
// https://stackoverflow.com/questions/54269956/crash-of-jupyter-due-to-the-use-of-pyarray-enableflags/54278170#54278170
// Note that in general it was disadvised to build all C code with MinGW on Windows.
// We do it anyway, see if we find any problems on the way.
void* capsule = PyCapsule_New(mat->_data, NULL, capsule_cleanup);
PyArray_SetBaseObject( arr_t, capsule);
#else
PyArray_ENABLEFLAGS(arr_t, NPY_OWNDATA);
#endif
}
// Transpose the array
PyObject* arr = PyArray_Transpose((PyArrayObject*) arr_t,NULL);
if(!arr) {
WARN("Array transpose failure");
feTRACE(15);
return NULL;
}
Py_DECREF(arr_t);
feTRACE(15);
return arr; return arr;
} }

View File

@ -1,6 +1,3 @@
#!/usr/bin/python3 #!/usr/bin/python3
__all__ = ['DAQConfiguration'] from .lasp_rtaudio import *
from .lasp_daqconfig import DAQConfiguration, DAQInputChannel, DeviceInfo from .lasp_daqconfig import *
from .lasp_rtaudio import (RtAudio,
get_numpy_dtype_from_format_string,
get_sampwidth_from_format_string)

View File

@ -9,11 +9,14 @@ Data Acquistiion (DAQ) device descriptors, and the DAQ devices themselves
""" """
from dataclasses import dataclass, field from dataclasses import dataclass, field
from dataclasses_json import dataclass_json
import numpy as np import numpy as np
from ..lasp_common import lasp_shelve, Qty
@dataclass @dataclass
class DeviceInfo: class DeviceInfo:
api: int
index: int index: int
probed: bool probed: bool
name: str name: str
@ -25,17 +28,17 @@ class DeviceInfo:
prefsamplerate: int prefsamplerate: int
from ..lasp_common import lasp_shelve
@dataclass_json
@dataclass @dataclass
class DAQInputChannel: class DAQChannel:
channel_enabled: bool channel_enabled: bool
channel_name: str channel_name: str
sensitivity: float sensitivity: float
unit: Qty
@dataclass_json
@dataclass @dataclass
class DAQConfiguration: class DAQConfiguration:
""" """
@ -44,6 +47,9 @@ class DAQConfiguration:
Args: Args:
duplex_mode: Set device to duplex mode, if possible duplex_mode: Set device to duplex mode, if possible
monitor_gen: If set to true, add monitor channel to recording. monitor_gen: If set to true, add monitor channel to recording.
outputDelayBlocks: number of blocks to delay output stream when added
to input for monitoring the output synchronously with input.
input_device_name: ASCII name with which to open the device when connected input_device_name: ASCII name with which to open the device when connected
outut_device_name: ASCII name with which to open the device when connected outut_device_name: ASCII name with which to open the device when connected
@ -66,6 +72,7 @@ class DAQConfiguration:
""" """
api: int
duplex_mode: bool duplex_mode: bool
input_device_name: str input_device_name: str
@ -76,28 +83,12 @@ class DAQConfiguration:
en_input_rate: int en_input_rate: int
en_output_rate: int en_output_rate: int
input_channel_configs: list = None input_channel_configs: list
monitor_gen: bool = False output_channel_configs: list
monitor_gen: bool
outputDelayBlocks: int
def __post_init__(self): nFramesPerBlock: int
"""
We do a check here to see whether the list of enabled channels is
contiguous. Non-contiguous is not yet implemented in RtAudio backend.
"""
en_input = self.input_channel_configs
first_ch_enabled_found = False
ch_disabled_found_after = False
for ch in en_input:
if ch.channel_enabled:
first_ch_enabled_found = True
if ch_disabled_found_after:
raise ValueError('Error: non-contiguous array of channels'
' found. This is not yet implemented in'
' backend')
else:
if first_ch_enabled_found:
ch_disabled_found_after = True
def firstEnabledInputChannelNumber(self): def firstEnabledInputChannelNumber(self):
""" """
@ -109,18 +100,55 @@ class DAQConfiguration:
return i return i
return -1 return -1
def firstEnabledOutputChannelNumber(self):
"""
Returns the channel number of the first enabled output channel. Returns -1 if
no channels are enabled.
"""
for i, ch in enumerate(self.output_channel_configs):
if ch.channel_enabled:
return i
return -1
def getEnabledChannels(self): def lastEnabledInputChannelNumber(self):
last = -1
for i, ch in enumerate(self.input_channel_configs):
if ch.channel_enabled:
last = i
return last
def lastEnabledOutputChannelNumber(self):
last = -1
for i, ch in enumerate(self.output_channel_configs):
print(ch)
if ch.channel_enabled:
last = i
return last
def getEnabledInputChannels(self):
en_channels = [] en_channels = []
for chan in self.input_channel_configs: for chan in self.input_channel_configs:
if chan.channel_enabled: if chan.channel_enabled:
en_channels.append(chan) en_channels.append(chan)
return en_channels return en_channels
def getEnabledChannelSensitivities(self): def getEnabledInputChannelNames(self):
return np.array( return [ch.channel_name for ch in self.getEnabledInputChannels()]
[float(channel.sensitivity) for channel in
self.getEnabledChannels()]) def getEnabledInputChannelSensitivities(self):
return [float(channel.sensitivity) for channel in
self.getEnabledInputChannels()]
def getEnabledOutputChannels(self):
en_channels = []
for chan in self.output_channel_configs:
if chan.channel_enabled:
en_channels.append(chan)
return en_channels
def getEnabledOutputChannelSensitivities(self):
return [float(channel.sensitivity) for channel in
self.getEnabledOutputChannels()]
@staticmethod @staticmethod
def loadConfigs(): def loadConfigs():

View File

@ -9,6 +9,15 @@ from libc.stdio cimport printf, fprintf, stderr
from libc.string cimport memcpy, memset from libc.string cimport memcpy, memset
from cpython.ref cimport PyObject,Py_INCREF, Py_DECREF from cpython.ref cimport PyObject,Py_INCREF, Py_DECREF
__all__ = ['AvType', 'RtAudio', 'get_numpy_dtype_from_format_string',
'get_sampwidth_from_format_string']
class AvType:
"""Specificying the type of data, for adding and removing callbacks from
the stream."""
audio_input = 1
audio_output = 2
video = 4
cdef extern from "RtAudio.h" nogil: cdef extern from "RtAudio.h" nogil:
ctypedef unsigned long RtAudioStreamStatus ctypedef unsigned long RtAudioStreamStatus
@ -47,7 +56,7 @@ cdef extern from "RtAudio.h" nogil:
ctypedef int (*RtAudioCallback)(void* outputBuffer, ctypedef int (*RtAudioCallback)(void* outputBuffer,
void* inputBuffer, void* inputBuffer,
unsigned int nFrames, unsigned int nFramesPerBlock,
double streamTime, double streamTime,
RtAudioStreamStatus status, RtAudioStreamStatus status,
void* userData) void* userData)
@ -56,6 +65,15 @@ cdef extern from "RtAudio.h" nogil:
const string& errortxt) const string& errortxt)
cdef cppclass cppRtAudio "RtAudio": cdef cppclass cppRtAudio "RtAudio":
enum Api:
UNSPECIFIED
LINUX_ALSA
LINUX_PULSE
MACOSX_CORE
WINDOWS_WASAPI
WINDOWS_ASIO
WINDOWS_DS
cppclass DeviceInfo: cppclass DeviceInfo:
bool probed bool probed
string name string name
@ -69,17 +87,35 @@ cdef extern from "RtAudio.h" nogil:
RtAudioFormat nativeFormats RtAudioFormat nativeFormats
cppclass StreamOptions: cppclass StreamOptions:
StreamOptions()
RtAudioStreamFlags flags RtAudioStreamFlags flags
unsigned int numberOfBuffers unsigned int numberOfBuffers
string streamName string streamName
int priority int priority
cppclass StreamParameters: cppclass StreamParameters:
StreamParameters()
unsigned int deviceId unsigned int deviceId
unsigned int nChannels unsigned int nChannels
unsigned int firstChannel unsigned int firstChannel
RtAudio() except + @staticmethod
void getCompiledApi(vector[cppRtAudio.Api]& apis)
@staticmethod
cppRtAudio.Api getCompiledApiByName(string& name)
@staticmethod
string getApiDisplayName(cppRtAudio.Api api)
@staticmethod
string getApiName(cppRtAudio.Api api)
# RtAudio() except +
cppRtAudio() except +
cppRtAudio(cppRtAudio.Api api) except +
# ~RtAudio() Destructors should not be listed # ~RtAudio() Destructors should not be listed
unsigned int getDeviceCount() unsigned int getDeviceCount()
DeviceInfo getDeviceInfo(unsigned int device) DeviceInfo getDeviceInfo(unsigned int device)
unsigned int getDefaultOutputDevice() unsigned int getDefaultOutputDevice()
@ -126,6 +162,13 @@ cdef extern from "atomic" namespace "std" nogil:
T load() T load()
void store(T) void store(T)
cdef extern from "lasp_pyarray.h":
PyObject* data_to_ndarray(void* data,
int n_rows,int n_cols,
int typenum,
bool transfer_ownership,
bool F_contiguous)
_formats_strkey = { _formats_strkey = {
'8-bit integers': (RTAUDIO_SINT8, 1, np.int8), '8-bit integers': (RTAUDIO_SINT8, 1, np.int8),
'16-bit integers': (RTAUDIO_SINT16, 2, np.int16), '16-bit integers': (RTAUDIO_SINT16, 2, np.int16),
@ -135,10 +178,10 @@ _formats_strkey = {
'64-bit floats': (RTAUDIO_FLOAT64, 8, np.float64), '64-bit floats': (RTAUDIO_FLOAT64, 8, np.float64),
} }
_formats_rtkey = { _formats_rtkey = {
RTAUDIO_SINT8: ('8-bit integers', 1, cnp.NPY_INT8), RTAUDIO_SINT8: ('8-bit integers', 1, cnp.NPY_INT8),
RTAUDIO_SINT16: ('16-bit integers',2, cnp.NPY_INT16), RTAUDIO_SINT16: ('16-bit integers', 2, cnp.NPY_INT16),
RTAUDIO_SINT24: ('24-bit integers',3), RTAUDIO_SINT24: ('24-bit integers', 3),
RTAUDIO_SINT32: ('32-bit integers',4, cnp.NPY_INT32), RTAUDIO_SINT32: ('32-bit integers', 4, cnp.NPY_INT32),
RTAUDIO_FLOAT32: ('32-bit floats', 4, cnp.NPY_FLOAT32), RTAUDIO_FLOAT32: ('32-bit floats', 4, cnp.NPY_FLOAT32),
RTAUDIO_FLOAT64: ('64-bit floats', 8, cnp.NPY_FLOAT64), RTAUDIO_FLOAT64: ('64-bit floats', 8, cnp.NPY_FLOAT64),
} }
@ -153,52 +196,60 @@ def get_sampwidth_from_format_string(format_string):
# It took me quite a long time to fully understand Cython's idiosyncrasies # It took me quite a long time to fully understand Cython's idiosyncrasies
# concerning C(++) callbacks, the GIL and passing Python objects as pointers # concerning C(++) callbacks, the GIL and passing Python objects as pointers
# into C(++) functions. But finally, here it is! # into C(++) functions. But finally, here it is!
cdef object fromBufferToNPYNoCopy(
cnp.NPY_TYPES buffer_format_type,
void* buf,
size_t nchannels,
size_t nframes):
cdef cnp.npy_intp[2] dims = [nframes, nchannels]
# Interleaved data is C-style contiguous. Therefore, we can directly use
# SimpleNewFromData()
array = cnp.PyArray_SimpleNewFromData(2, &dims[0], buffer_format_type,
buf)
return array # cdef void fromNPYToBuffer(cnp.ndarray arr,
# void* buf):
# """
# Copy a Python numpy array over to a buffer
# No checks, just memcpy! Careful!
# """
# memcpy(buf, arr.data, arr.size*arr.itemsize)
cdef void copyChannel(void* to, void* from_,
unsigned bytesperchan,
unsigned toindex,
unsigned fromindex) nogil:
memcpy(<void*> &((<char*> to)[bytesperchan*toindex]),
<void*> &((<char*> from_)[bytesperchan*fromindex]),
bytesperchan)
cdef void fromNPYToBuffer(cnp.ndarray arr,
void* buf):
"""
Copy a Python numpy array over to a buffer
No checks, just memcpy! Careful!
"""
memcpy(buf, arr.data, arr.size*arr.itemsize)
ctypedef struct _Stream: ctypedef struct _Stream:
PyObject* pyCallback PyObject* pyCallback
RtAudioFormat sampleformat RtAudioFormat sampleformat
# Flag used to pass the stopThread.
atomic[bool] stopThread atomic[bool] stopThread
unsigned int nFrames # Number of frames per block
unsigned nFramesPerBlock
# Number of bytes per channel
unsigned int nBytesPerChan
# Number of blocks to delay the output before adding to the input
unsigned int outputDelayBlocks
# The structures as used by RtAudio
cppRtAudio.StreamParameters inputParams cppRtAudio.StreamParameters inputParams
cppRtAudio.StreamParameters outputParams cppRtAudio.StreamParameters outputParams
bool* inputChannelsEnabled
bool* outputChannelsEnabled
unsigned ninputchannels_forwarded
unsigned noutputchannels_forwarded
# If these queue pointers are NULL, it means the stream does not have an # If these queue pointers are NULL, it means the stream does not have an
# input, or output. # input, or output.
SafeQueue[void*] *outputDelayQueue
SafeQueue[void*] *inputQueue SafeQueue[void*] *inputQueue
SafeQueue[void*] *outputQueue SafeQueue[void*] *outputQueue
size_t inputbuffersize # Full size of the output buffer, in BYTES
size_t outputbuffersize # Full size of the output buffer, in BYTES
CPPThread[void*, void (*)(void*)] *thread CPPThread[void*, void (*)(void*)] *thread
cdef int audioCallback(void* outputbuffer, cdef int audioCallback(void* outputbuffer,
void* inputbuffer, void* inputbuffer,
unsigned int nFrames, unsigned int nFramesPerBlock,
double streamTime, double streamTime,
RtAudioStreamStatus status, RtAudioStreamStatus status,
void* userData) nogil: void* userData) nogil:
@ -211,8 +262,22 @@ cdef int audioCallback(void* outputbuffer,
_Stream* stream _Stream* stream
void* outputbuffercpy = NULL void* outputbuffercpy = NULL
void* inputbuffercpy = NULL void* inputbuffercpy = NULL
unsigned j, i
unsigned bytesperchan
unsigned noutputchannels_forwarded, ninputchannels_forwarded
bint ch_en
stream = <_Stream*>(userData) stream = <_Stream*>(userData)
bytesperchan = stream.nBytesPerChan
ninputchannels_forwarded = stream.ninputchannels_forwarded
noutputchannels_forwarded = stream.noutputchannels_forwarded
# with gil:
# print(f'bytesperchan: {bytesperchan}')
# print(f'ninputchannels_forwarded:: {ninputchannels_forwarded}')
# print(f'noutputchannels_forwarded:: {noutputchannels_forwarded}')
# fprintf(stderr, "Stream heartbeat...\n")
# Returning 2 means aborting the stream immediately # Returning 2 means aborting the stream immediately
if status == RTAUDIO_INPUT_OVERFLOW: if status == RTAUDIO_INPUT_OVERFLOW:
@ -224,35 +289,78 @@ cdef int audioCallback(void* outputbuffer,
# stream.stopThread.store(True) # stream.stopThread.store(True)
return 0 return 0
if nFrames != stream.nFrames: if nFramesPerBlock != stream.nFramesPerBlock:
printf('Number of frames mismath in callback data!\n') printf('Number of frames mismath in callback data!\n')
stream.stopThread.store(True) stream.stopThread.store(True)
return 2 return 2
if inputbuffer: if inputbuffer:
# assert stream.inputQueue is not NULL # fprintf(stderr, "enter copying input buffer code\n")
inputbuffercpy = malloc(stream.inputbuffersize) # with gil:
memcpy(inputbuffercpy, inputbuffer, # assert stream.inputQueue is not NULL
stream.inputbuffersize) inputbuffercpy = malloc(bytesperchan*ninputchannels_forwarded)
if not inputbuffercpy:
fprintf(stderr, "Error allocating buffer\n")
return 2
if stream.outputDelayQueue:
if stream.outputDelayQueue.size() > stream.outputDelayBlocks:
outputbuffercpy = stream.outputDelayQueue.dequeue()
memcpy(inputbuffercpy, outputbuffercpy,
bytesperchan*noutputchannels_forwarded)
# Cleanup buffer
free(outputbuffercpy)
outputbuffercpy = NULL
else:
memset(inputbuffercpy, 0,
bytesperchan*noutputchannels_forwarded)
# Starting channel for copying input channels according to the channel
# map
j = stream.noutputchannels_forwarded
else:
j = 0
i = 0
for i in range(stream.inputParams.nChannels):
ch_en = stream.inputChannelsEnabled[i]
if ch_en:
copyChannel(inputbuffercpy, inputbuffer, bytesperchan, j, i)
j+=1
i+=1
stream.inputQueue.enqueue(inputbuffercpy) stream.inputQueue.enqueue(inputbuffercpy)
if outputbuffer: if outputbuffer:
# assert stream.outputQueue is not NULL # with gil:
# assert stream.outputQueue
# fprintf(stderr, "enter copying output buffer code\n")
if stream.outputQueue.empty(): if stream.outputQueue.empty():
fprintf(stderr, 'Stream output buffer underflow, zero-ing buffer...\n') fprintf(stderr, 'Stream output buffer underflow, zero-ing buffer...\n')
# Pre-stack three empty output buffers memset(outputbuffer, 0, stream.outputParams.nChannels*bytesperchan)
# printf('Pre-stacking\n') else:
# outputbuffer = malloc(stream.outputbuffersize) outputbuffercpy = stream.outputQueue.dequeue()
memset(outputbuffer, 0, stream.outputbuffersize) # fprintf(stderr, 'Copying data to stream output buffer...\n')
if stream.inputQueue: j = 0
stream.inputQueue.enqueue(NULL) i = 0
return 0 for i in range(stream.outputParams.nChannels):
ch_en = stream.outputChannelsEnabled[i]
outputbuffercpy = stream.outputQueue.dequeue() if ch_en:
memcpy(outputbuffer, outputbuffercpy, copyChannel(outputbuffer, outputbuffercpy, bytesperchan, i, j)
stream.outputbuffersize) j+=1
free(outputbuffercpy) else:
# If channel is not enabled, we set the data to zero
memset(<void*> &((<char*> outputbuffer)[bytesperchan*i]), 0, bytesperchan)
pass
i+=1
if stream.outputDelayQueue:
# fprintf(stderr, "Adding to delay queue\n")
stream.outputDelayQueue.enqueue(outputbuffercpy)
else:
free(outputbuffercpy)
outputbuffercpy = NULL
return 0 return 0
@ -263,69 +371,104 @@ cdef void audioCallbackPythonThreadFunction(void* voidstream) nogil:
cnp.NPY_TYPES npy_format cnp.NPY_TYPES npy_format
void* inputbuffer = NULL void* inputbuffer = NULL
void* outputbuffer = NULL void* outputbuffer = NULL
unsigned noutputchannels_forwarded
unsigned ninputchannels_forwarded
unsigned nBytesPerChan
unsigned nFramesPerBlock
void* _TESTDATA
stream = <_Stream*> voidstream stream = <_Stream*> voidstream
printf('Thread started\n') ninputchannels_forwarded = stream.ninputchannels_forwarded
noutputchannels_forwarded = stream.noutputchannels_forwarded
nBytesPerChan = stream.nBytesPerChan
nFramesPerBlock = stream.nFramesPerBlock
with gil: with gil:
npy_format = _formats_rtkey[stream.sampleformat][2] npy_format = _formats_rtkey[stream.sampleformat][2]
callback = <object> stream.pyCallback callback = <object> stream.pyCallback
print(f'noutputchannels_forwarded: {noutputchannels_forwarded}')
print(f'ninputchannels_forwarded: {ninputchannels_forwarded}')
print(f'nBytesPerChan: {nBytesPerChan}')
print(f'nFramesPerBlock: {nFramesPerBlock}')
while True: while True:
if stream.stopThread.load() == True: if stream.stopThread.load() == True:
printf('Stopping thread...\n') printf('Stopping thread...\n')
return return
if stream.inputQueue: if stream.inputQueue:
# fprintf(stderr, "Waiting on input queue\n")
inputbuffer = stream.inputQueue.dequeue() inputbuffer = stream.inputQueue.dequeue()
# if inputbuffer == NULL: if not inputbuffer:
# continue printf('Stopping thread...\n')
return
if stream.outputQueue: if stream.outputQueue:
outputbuffer = malloc(stream.outputbuffersize) # fprintf(stderr, 'Allocating output buffer...\n')
outputbuffer = malloc(nBytesPerChan*noutputchannels_forwarded)
# memset(outputbuffer, 0, nBytesPerChan*noutputchannels_forwarded)
with gil: with gil:
# Obtain stream information # Obtain stream information
npy_input = None npy_input = None
npy_output = None npy_output = None
if stream.inputQueue and inputbuffer: if stream.inputQueue:
# assert(inputbuffer)
try: try:
npy_input = fromBufferToNPYNoCopy( # print(f'========ninputchannels_forwarded: {ninputchannels_forwarded}')
npy_format, # print(f'========nFramesPerBlock: {nFramesPerBlock}')
inputbuffer, # print(f'========npy_format: {npy_format}')
stream.inputParams.nChannels,
stream.nFrames) npy_input = <object> data_to_ndarray(
inputbuffer,
nFramesPerBlock,
ninputchannels_forwarded,
npy_format,
True, # Do transfer ownership
True) # F-contiguous is True: data is Fortran-cont.
# fprintf(stderr, "Copying array...\n")
# fprintf(stderr, "End Copying array...\n")
except Exception as e: except Exception as e:
print('exception in cython callback for audio input: ', str(e)) print('exception in cython callback for audio input: ', str(e))
return return
if stream.outputQueue: if stream.outputQueue:
# fprintf(stderr, 'Copying output buffer to Numpy...\n')
try: try:
assert outputbuffer != NULL npy_output = <object> data_to_ndarray(
npy_output = fromBufferToNPYNoCopy( outputbuffer,
npy_format, nFramesPerBlock,
outputbuffer, noutputchannels_forwarded,
stream.outputParams.nChannels, npy_format,
stream.nFrames) False, # Do not transfer ownership
True) # F-contiguous
except Exception as e: except Exception as e:
print('exception in Cython callback for audio output: ', str(e)) print('exception in Cython callback for audio output: ', str(e))
return return
try: try:
# fprintf(stderr, "Python callback...\n")
rval = callback(npy_input, rval = callback(npy_input,
npy_output, npy_output,
stream.nFrames, nFramesPerBlock,
) )
# fprintf(stderr, "Return from Python callback...\n")
except Exception as e: except Exception as e:
print('Exception in Cython callback: ', str(e)) print('Exception in Cython callback: ', str(e))
return return
if stream.outputQueue: if stream.outputQueue:
# fprintf(stderr, 'Enqueuing output buffer...\n')
stream.outputQueue.enqueue(outputbuffer) stream.outputQueue.enqueue(outputbuffer)
if not stream.inputQueue: if not stream.inputQueue:
# fprintf(stderr, 'No input queue!\n')
while stream.outputQueue.size() > 10 and not stream.stopThread.load(): while stream.outputQueue.size() > 10 and not stream.stopThread.load():
# printf('Sleeping...\n') # printf('Sleeping...\n')
# No input queue to wait on, so we relax a bit here. # No input queue to wait on, so we relax a bit here.
@ -334,30 +477,50 @@ cdef void audioCallbackPythonThreadFunction(void* voidstream) nogil:
# Outputbuffer is free'ed by the audiothread, so should not be touched # Outputbuffer is free'ed by the audiothread, so should not be touched
# here. # here.
outputbuffer = NULL outputbuffer = NULL
# Inputbuffer memory is owned by Numpy, so should not be free'ed # Inputbuffer memory is owned by Numpy, so should not be free'ed
inputbuffer = NULL inputbuffer = NULL
cdef void errorCallback(RtAudioError.Type _type,const string& errortxt) nogil: cdef void errorCallback(RtAudioError.Type _type,const string& errortxt) nogil:
printf('RtAudio error callback called: ') fprintf(stderr, 'RtAudio error callback called: ')
printf(errortxt.c_str()) fprintf(stderr, errortxt.c_str())
printf('\n') fprintf(stderr, '\n')
cdef class RtAudio: cdef class RtAudio:
cdef: cdef:
cppRtAudio _rtaudio cppRtAudio* _rtaudio
_Stream* _stream _Stream* _stream
int api
def __cinit__(self): def __cinit__(self, unsigned int iapi):
cdef:
cppRtAudio.Api api = <cppRtAudio.Api> iapi
self._rtaudio = new cppRtAudio(api)
self._stream = NULL self._stream = NULL
self._rtaudio.showWarnings(True) self._rtaudio.showWarnings(True)
self.api = api
def __dealloc__(self): def __dealloc__(self):
if self._stream is not NULL: if self._stream is not NULL:
fprintf(stderr, 'Force closing stream...') # fprintf(stderr, 'Force closing stream...')
self._rtaudio.closeStream() self._rtaudio.closeStream()
self.cleanupStream(self._stream) self.cleanupStream(self._stream)
del self._rtaudio
@staticmethod
def getApi():
cdef:
vector[cppRtAudio.Api] apis
cppRtAudio.getCompiledApi(apis)
apidict = {}
for api in apis:
apidict[<int> api] = {
'displayname': cppRtAudio.getApiDisplayName(api).decode('utf-8'),
'name': cppRtAudio.getApiName(api).decode('utf-8')
}
return apidict
cpdef unsigned int getDeviceCount(self): cpdef unsigned int getDeviceCount(self):
return self._rtaudio.getDeviceCount() return self._rtaudio.getDeviceCount()
@ -372,14 +535,17 @@ cdef class RtAudio:
""" """
Return device information of the current device Return device information of the current device
""" """
cdef cppRtAudio.DeviceInfo devinfo = self._rtaudio.getDeviceInfo(device)
sampleformats = [] sampleformats = []
cdef:
cppRtAudio.DeviceInfo devinfo
devinfo = self._rtaudio.getDeviceInfo(device)
nf = devinfo.nativeFormats nf = devinfo.nativeFormats
for format_ in [ RTAUDIO_SINT8, RTAUDIO_SINT16, RTAUDIO_SINT24, for format_ in [ RTAUDIO_SINT8, RTAUDIO_SINT16, RTAUDIO_SINT24,
RTAUDIO_SINT32, RTAUDIO_FLOAT32, RTAUDIO_FLOAT64]: RTAUDIO_SINT32, RTAUDIO_FLOAT32, RTAUDIO_FLOAT64]:
if nf & format_: if nf & format_:
sampleformats.append(_formats_rtkey[format_][0]) sampleformats.append(_formats_rtkey[format_][0])
return DeviceInfo( return DeviceInfo(
api = self.api,
index = device, index = device,
probed = devinfo.probed, probed = devinfo.probed,
name = devinfo.name.decode('utf-8'), name = devinfo.name.decode('utf-8'),
@ -391,109 +557,204 @@ cdef class RtAudio:
prefsamplerate = devinfo.preferredSampleRate) prefsamplerate = devinfo.preferredSampleRate)
@cython.nonecheck(True) @cython.nonecheck(True)
def openStream(self,object outputParams, def openStream(self,
object inputParams, avstream
str sampleformat, ):
unsigned int sampleRate,
unsigned int bufferFrames,
object pyCallback,
object options = None,
object pyErrorCallback = None):
""" """
Opening a stream with specified parameters Opening a stream with specified parameters
Args: Args:
outputParams: dictionary of stream outputParameters, set to None avstream: AvStream instance
if no outputPararms are specified
inputParams: dictionary of stream inputParameters, set to None
if no inputPararms are specified
sampleRate: desired sample rate.
bufferFrames: the amount of frames in a callback buffer
callback: callback to call. Note: this callback is called on a
different thread!
options: A dictionary of optional additional stream options
errorCallback: client-defined function that will be invoked when an
error has occured.
Returns: None Returns: None
""" """
if self._stream is not NULL: if self._stream is not NULL:
raise RuntimeError('Stream is already opened.') raise RuntimeError('Stream is already opened.')
daqconfig = avstream.daqconfig
avtype = avstream.avtype
device = avstream.device
cdef: cdef:
bint duplex_mode = daqconfig.duplex_mode
bint monitorOutput = daqconfig.monitor_gen
unsigned int outputDelayBlocks = daqconfig.outputDelayBlocks
cppRtAudio.StreamParameters *rtOutputParams_ptr = NULL cppRtAudio.StreamParameters *rtOutputParams_ptr = NULL
cppRtAudio.StreamParameters *rtInputParams_ptr = NULL cppRtAudio.StreamParameters *rtInputParams_ptr = NULL
cppRtAudio.StreamOptions streamoptions cppRtAudio.StreamOptions streamoptions
size_t bytespersample size_t sw
unsigned int bufferFrames_local unsigned int nFramesPerBlock = int(daqconfig.nFramesPerBlock)
int firstinputchannel, firstoutputchannel
int lastinputchannel, lastoutputchannel
unsigned int ninputchannels_forwarded=0
unsigned int ninputchannels_rtaudio=0
unsigned int noutputchannels_rtaudio=0
unsigned int noutputchannels_forwarded=0
unsigned int samplerate
int i
bint input_stream=False, output_stream=False
bool* inputChannelsEnabled
bool* outputChannelsEnabled
streamoptions.flags = RTAUDIO_HOG_DEVICE if daqconfig.nFramesPerBlock > 8192 or daqconfig.nFramesPerBlock < 512:
streamoptions.numberOfBuffers = 4 raise ValueError('Invalid number of nFramesPerBlock')
bufferFrames_local = bufferFrames
self._stream = <_Stream*> malloc(sizeof(_Stream)) if daqconfig.outputDelayBlocks < 0 or daqconfig.outputDelayBlocks > 10:
if self._stream == NULL: raise ValueError('Invalid number of outputDelayBlocks')
raise MemoryError()
self._stream.pyCallback = <PyObject*> pyCallback
Py_INCREF(pyCallback)
self._stream.sampleformat = _formats_strkey[sampleformat][0]
self._stream.inputQueue = NULL
self._stream.outputQueue = NULL
self._stream.outputbuffersize = 0
self._stream.inputbuffersize = 0
self._stream.stopThread.store(False)
self._stream.thread = NULL
bytespersample = get_sampwidth_from_format_string(sampleformat)
if outputParams is not None:
rtOutputParams_ptr = &self._stream.outputParams
rtOutputParams_ptr.deviceId = outputParams['deviceid']
rtOutputParams_ptr.nChannels = outputParams['nchannels']
rtOutputParams_ptr.firstChannel = outputParams['firstchannel']
self._stream.outputQueue = new SafeQueue[void*]()
if inputParams is not None:
rtInputParams_ptr = &self._stream.inputParams
rtInputParams_ptr.deviceId = inputParams['deviceid']
rtInputParams_ptr.nChannels = inputParams['nchannels']
rtInputParams_ptr.firstChannel = inputParams['firstchannel']
self._stream.inputQueue = new SafeQueue[void*]()
try: try:
# Determine sample rate and sample format, determine whether we are an
# input or an output stream, or both
if avtype == AvType.audio_input or duplex_mode:
# Here, we override the sample format in case of duplex mode.
sampleformat = daqconfig.en_input_sample_format
samplerate = int(daqconfig.en_input_rate)
input_stream = True
if duplex_mode:
output_stream = True
else:
sampleformat = daqconfig.en_output_sample_format
samplerate = int(daqconfig.en_output_rate)
output_stream = True
print(f'Is input stream: {input_stream}')
print(f'Is output stream: {output_stream}')
sw = get_sampwidth_from_format_string(sampleformat)
print(f'samplewidth: {sw}')
# All set, allocate the stream!
self._stream = <_Stream*> malloc(sizeof(_Stream))
if self._stream == NULL:
raise MemoryError('Could not allocate stream: memory error.')
self._stream.pyCallback = <PyObject*> avstream._audioCallback
# Increase reference count to the callback
Py_INCREF(<object> avstream._audioCallback)
self._stream.sampleformat = _formats_strkey[sampleformat][0]
self._stream.stopThread.store(False)
self._stream.inputQueue = NULL
self._stream.outputQueue = NULL
self._stream.outputDelayQueue = NULL
self._stream.thread = NULL
self._stream.outputDelayBlocks = outputDelayBlocks
self._stream.ninputchannels_forwarded = 0
self._stream.noutputchannels_forwarded = 0
self._stream.inputChannelsEnabled = NULL
self._stream.outputChannelsEnabled = NULL
# Create channel maps for input channels, set RtAudio input stream
# parameters
if input_stream:
firstinputchannel = daqconfig.firstEnabledInputChannelNumber()
lastinputchannel = daqconfig.lastEnabledInputChannelNumber()
ninputchannels_rtaudio = lastinputchannel-firstinputchannel+1
# print(firstinputchannel)
# print(lastinputchannel)
# print(ninputchannels_rtaudio)
if lastinputchannel < 0 or ninputchannels_rtaudio < 1:
raise ValueError('Not enough input channels selected')
input_ch = daqconfig.input_channel_configs
inputChannelsEnabled = <bool*> malloc(sizeof(bool)*ninputchannels_rtaudio)
self._stream.inputChannelsEnabled = inputChannelsEnabled
for i in range(firstinputchannel, lastinputchannel+1):
ch_en = input_ch[i].channel_enabled
if ch_en:
ninputchannels_forwarded += 1
inputChannelsEnabled[i] = ch_en
rtInputParams_ptr = &self._stream.inputParams
rtInputParams_ptr.deviceId = device.index
rtInputParams_ptr.nChannels = ninputchannels_rtaudio
rtInputParams_ptr.firstChannel = firstinputchannel
self._stream.inputQueue = new SafeQueue[void*]()
self._stream.ninputchannels_forwarded = ninputchannels_forwarded
# Create channel maps for output channels
if output_stream:
firstoutputchannel = daqconfig.firstEnabledOutputChannelNumber()
lastoutputchannel = daqconfig.lastEnabledOutputChannelNumber()
noutputchannels_rtaudio = lastoutputchannel-firstoutputchannel+1
# print(firstoutputchannel)
# print(lastoutputchannel)
# print(noutputchannels_rtaudio)
if lastoutputchannel < 0 or noutputchannels_rtaudio < 1:
raise ValueError('Not enough output channels selected')
output_ch = daqconfig.output_channel_configs
outputChannelsEnabled = <bool*> malloc(sizeof(bool)*noutputchannels_rtaudio)
self._stream.outputChannelsEnabled = outputChannelsEnabled
for i in range(firstoutputchannel, lastoutputchannel+1):
ch_en = output_ch[i].channel_enabled
if ch_en:
noutputchannels_forwarded += 1
outputChannelsEnabled[i] = ch_en
rtOutputParams_ptr = &self._stream.outputParams
rtOutputParams_ptr.deviceId = device.index
rtOutputParams_ptr.nChannels = noutputchannels_rtaudio
rtOutputParams_ptr.firstChannel = firstoutputchannel
self._stream.outputQueue = new SafeQueue[void*]()
self._stream.noutputchannels_forwarded = noutputchannels_forwarded
if monitorOutput and duplex_mode:
self._stream.outputDelayQueue = new SafeQueue[void*]()
self._stream.ninputchannels_forwarded += noutputchannels_forwarded
streamoptions.flags = RTAUDIO_HOG_DEVICE
streamoptions.flags |= RTAUDIO_NONINTERLEAVED
streamoptions.numberOfBuffers = 4
streamoptions.streamName = "LASP Audio stream".encode('utf-8')
streamoptions.priority = 1
self._rtaudio.openStream(rtOutputParams_ptr, self._rtaudio.openStream(rtOutputParams_ptr,
rtInputParams_ptr, rtInputParams_ptr,
_formats_strkey[sampleformat][0], _formats_strkey[sampleformat][0],
sampleRate, samplerate,
&bufferFrames_local, &nFramesPerBlock,
audioCallback, audioCallback,
<void*> self._stream, <void*> self._stream,
&streamoptions, # Stream options &streamoptions, # Stream options
errorCallback # Error callback errorCallback # Error callback
) )
self._stream.nFrames = bufferFrames_local
self._stream.nBytesPerChan = nFramesPerBlock*sw
self._stream.nFramesPerBlock = nFramesPerBlock
except Exception as e: except Exception as e:
print('Exception occured in stream opening: ', str(e)) print('Exception occured in stream opening: ', e)
self.cleanupStream(self._stream) self.cleanupStream(self._stream)
self._stream = NULL self._stream = NULL
raise raise e
if inputParams is not None:
self._stream.inputbuffersize = bufferFrames_local*bytespersample*inputParams['nchannels']
if outputParams is not None:
self._stream.outputbuffersize = bufferFrames_local*bytespersample*outputParams['nchannels']
with nogil: with nogil:
self._stream.thread = new CPPThread[void*, void (*)(void*)](audioCallbackPythonThreadFunction, self._stream.thread = new CPPThread[void*, void (*)(void*)](audioCallbackPythonThreadFunction,
<void*> self._stream) <void*> self._stream)
# Allow it to start
CPPsleep(500) CPPsleep(500)
pass
return bufferFrames_local return nFramesPerBlock
cdef cleanupStream(self, _Stream* stream): cdef cleanupStream(self, _Stream* stream):
# printf('Entrance function cleanupStream...\n') # printf('Entrance function cleanupStream...\n')
cdef:
void* ptr
if stream == NULL: if stream == NULL:
return return
@ -509,17 +770,32 @@ cdef class RtAudio:
stream.thread.join() stream.thread.join()
# printf('Thread joined!\n') # printf('Thread joined!\n')
del stream.thread del stream.thread
stream.thread = NULL
if stream.inputChannelsEnabled:
free(stream.inputChannelsEnabled)
if stream.outputChannelsEnabled:
free(stream.outputChannelsEnabled)
if stream.outputQueue: if stream.outputQueue:
while not stream.outputQueue.empty():
free(stream.outputQueue.dequeue())
del stream.outputQueue del stream.outputQueue
if stream.inputQueue: if stream.inputQueue:
while not stream.inputQueue.empty():
free(stream.inputQueue.dequeue())
del stream.inputQueue del stream.inputQueue
if stream.outputDelayQueue:
while not stream.outputDelayQueue.empty():
free(stream.outputDelayQueue.dequeue())
del stream.outputDelayQueue
fprintf(stderr, "End cleanup stream queues...\n")
if stream.pyCallback: if stream.pyCallback:
Py_DECREF(<object> stream.pyCallback) Py_DECREF(<object> stream.pyCallback)
stream.pyCallback = NULL
# fprintf(stderr, "End cleanup callback...\n")
free(stream) free(stream)
# printf('Cleanup of stream is done\n')
def startStream(self): def startStream(self):
self._rtaudio.startStream() self._rtaudio.startStream()

View File

@ -11,20 +11,13 @@ import numpy as np
import time import time
from .device import (RtAudio, DeviceInfo, DAQConfiguration, from .device import (RtAudio, DeviceInfo, DAQConfiguration,
get_numpy_dtype_from_format_string, get_numpy_dtype_from_format_string,
get_sampwidth_from_format_string) get_sampwidth_from_format_string, AvType)
__all__ = ['AvType', 'AvStream'] __all__ = ['AvStream']
video_x, video_y = 640, 480 video_x, video_y = 640, 480
class AvType:
"""Specificying the type of data, for adding and removing callbacks from
the stream."""
audio_input = 1
audio_output = 2
video = 4
class AvStream: class AvStream:
"""Audio and video data stream, to which callbacks can be added for """Audio and video data stream, to which callbacks can be added for
@ -49,59 +42,28 @@ class AvStream:
""" """
self.daqconfig = daqconfig self.daqconfig = daqconfig
self._device = device self.device = device
self.avtype = avtype self.avtype = avtype
self.duplex_mode = daqconfig.duplex_mode self.duplex_mode = daqconfig.duplex_mode
self.monitor_gen = daqconfig.monitor_gen
# Determine highest input channel number self.output_channel_names = [ch.channel_name for ch in daqconfig.getEnabledOutputChannels()]
channelconfigs = daqconfig.input_channel_configs
firstchannel = daqconfig.firstEnabledInputChannelNumber()
if firstchannel < 0:
raise ValueError('No input channels enabled')
self.channel_names = []
self.sensitivity = self.daqconfig.getEnabledChannelSensitivities()
if daqconfig.monitor_gen: if daqconfig.monitor_gen:
assert self.duplex_mode self.input_channel_names = self.output_channel_names
self.channel_names.append('Generated signal') self.input_sensitivity = daqconfig.getEnabledOutputChannelSensitivities()
self.sensitivity = np.concatenate([np.array([1.]), else:
self.sensitivity]) self.input_channel_names = []
self.input_sensitivity = []
self.input_sensitivity += daqconfig.getEnabledInputChannelSensitivities()
self.input_sensitivity = np.asarray(self.input_sensitivity)
rtaudio_inputparams = None self.input_channel_names += [
rtaudio_outputparams = None ch.channel_name for ch in daqconfig.getEnabledInputChannels()]
self.nframes_per_block = 1024 self.input_samplerate = float(daqconfig.en_input_rate)
self.output_samplerate = float(daqconfig.en_output_rate)
if self.duplex_mode or avtype == AvType.audio_output:
rtaudio_outputparams = {'deviceid': device.index,
# TODO: Add option to specify the number of output channels to use
'nchannels': 1, # device.outputchannels,
'firstchannel': 0}
self.sampleformat = daqconfig.en_output_sample_format
self.samplerate = int(daqconfig.en_output_rate)
if avtype == AvType.audio_input or self.duplex_mode:
for i, channelconfig in enumerate(channelconfigs):
if channelconfig.channel_enabled:
self.nchannels = i+1
self.channel_names.append(channelconfig.channel_name)
rtaudio_inputparams = {'deviceid': device.index,
'nchannels': self.nchannels,
'firstchannel': firstchannel}
# Here, we override the sample format in case of duplex mode.
self.sampleformat = daqconfig.en_input_sample_format
self.samplerate = int(daqconfig.en_input_rate)
# Fill in numpy data type, and sample width
self.numpy_dtype = get_numpy_dtype_from_format_string(
self.sampleformat)
self.sampwidth = get_sampwidth_from_format_string(
self.sampleformat)
# Counters for the number of frames that have been coming in # Counters for the number of frames that have been coming in
self._aframectr = Atomic(0) self._aframectr = Atomic(0)
@ -125,19 +87,19 @@ class AvStream:
# Possible, but long not tested: store video # Possible, but long not tested: store video
self._videothread = None self._videothread = None
self._rtaudio = RtAudio(daqconfig.api)
self.blocksize = self._rtaudio.openStream(self)
try: # Fill in numpy data type, and sample width
self._rtaudio = RtAudio() self.input_numpy_dtype = get_numpy_dtype_from_format_string(
self.blocksize = self._rtaudio.openStream( daqconfig.en_input_sample_format)
rtaudio_outputparams, # Outputparams self.output_numpy_dtype = get_numpy_dtype_from_format_string(
rtaudio_inputparams, # Inputparams daqconfig.en_output_sample_format)
self.sampleformat, # Sampleformat
self.samplerate,
self.nframes_per_block, # Buffer size in frames
self._audioCallback)
except Exception as e: self.input_sampwidth = get_sampwidth_from_format_string(
raise RuntimeError(f'Could not initialize DAQ device: {str(e)}') daqconfig.en_input_sample_format)
self.output_sampwidth = get_sampwidth_from_format_string(
daqconfig.en_output_sample_format)
def close(self): def close(self):
self._rtaudio.closeStream() self._rtaudio.closeStream()
@ -212,29 +174,30 @@ class AvStream:
"""This is called (from a separate thread) for each audio block.""" """This is called (from a separate thread) for each audio block."""
self._aframectr += nframes self._aframectr += nframes
with self._callbacklock: with self._callbacklock:
# Count the number of output callbacks. If no output callbacks are # Count the number of output callbacks. If no output callbacks are
# present, and there should be output callbacks, we explicitly set # present, and there should be output callbacks, we explicitly set
# the output buffer to zero # the output buffer to zero
noutput_cb = len(self._callbacks[AvType.audio_output]) noutput_cb = len(self._callbacks[AvType.audio_output])
shouldhaveoutput = (self.avtype == AvType.audio_output or shouldhaveoutput = (self.avtype == AvType.audio_output or
self.duplex_mode) self.daqconfig.duplex_mode)
if noutput_cb == 0 and shouldhaveoutput: if noutput_cb == 0 and shouldhaveoutput and outdata is not None:
outdata[:, :] = 0 outdata[:, :] = 0
# Loop over callbacks # Loop over callbacks
for cb in self._callbacks[AvType.audio_output]: if outdata is not None:
try: for cb in self._callbacks[AvType.audio_output]:
cb(indata, outdata, self._aframectr()) try:
except Exception as e: cb(indata, outdata, self._aframectr())
print(e) except Exception as e:
return 1 print(e)
for cb in self._callbacks[AvType.audio_input]: return 2
try: if indata is not None:
cb(indata, outdata, self._aframectr()) for cb in self._callbacks[AvType.audio_input]:
except Exception as e: try:
print(e) cb(indata, outdata, self._aframectr())
return 1 except Exception as e:
print(e)
return 1
return 0 if self._running else 1 return 0 if self._running else 1

View File

@ -9,6 +9,8 @@ import numpy as np
from .wrappers import Window as wWindow from .wrappers import Window as wWindow
from collections import namedtuple from collections import namedtuple
from dataclasses import dataclass
from dataclasses_json import dataclass_json
""" """
Common definitions used throughout the code. Common definitions used throughout the code.
@ -39,9 +41,24 @@ U_REF = 5e-8 # 50 nano meter / s
# hence this is the reference level as specified below. # hence this is the reference level as specified below.
dBFS_REF = 0.5*2**0.5 # Which level would be -3.01 dBFS dBFS_REF = 0.5*2**0.5 # Which level would be -3.01 dBFS
Qty = namedtuple('Qty', 'name unit_name unit_symb level_unit level_ref_name level_ref_value') @dataclass_json
@dataclass
class Qty:
name: str
unit_name: str
unit_symb: str
level_unit: str
level_ref_name: str
level_ref_value: str
class SIQtys: class SIQtys:
N = Qty(name='Number',
unit_name='No unit / full scale',
unit_symb=('-'),
level_unit=('dBFS',),
level_ref_name=('Full scale sine wave',),
level_ref_value=(dBFS_REF,)
)
AP = Qty(name='Acoustic Pressure', AP = Qty(name='Acoustic Pressure',
unit_name='Pascal', unit_name='Pascal',
unit_symb=('Pa', 'muPa'), unit_symb=('Pa', 'muPa'),
@ -51,21 +68,14 @@ class SIQtys:
) )
V = Qty(name='Voltage', V = Qty(name='Voltage',
unit_name='volt', unit_name='Volt',
unit_symb='V', unit_symb='V',
level_unit=('dBV',), # dBV level_unit=('dBV',), # dBV
level_ref_name=('1V',), level_ref_name=('1V',),
level_ref_value=(1.0,), level_ref_value=(1.0,),
) )
N = Qty(name='No unit',
unit_name='',
unit_symb='[-]',
level_unit=('dBFS',),
level_ref_name=('Full scale sine wave',),
level_ref_value=(dBFS_REF,)
)
types = (AP, V, N) types = (AP, V, N)
default = AP default = N
default_index = 0 default_index = 0
@staticmethod @staticmethod
@ -78,7 +88,7 @@ class SIQtys:
""" """
cb.clear() cb.clear()
for ty in SIQtys.types: for ty in SIQtys.types:
cb.addItem(f'{ty.name} [{ty.unit_symb[0]}') cb.addItem(f'{ty.unit_name}')
cb.setCurrentIndex(SIQtys.default_index) cb.setCurrentIndex(SIQtys.default_index)
@staticmethod @staticmethod

View File

@ -9,6 +9,8 @@ The ASCEE hdf5 measurement file format contains the following fields:
- Attributes: - Attributes:
'version': If not given, version 1 is assumed. For version 1, measurement data
is assumed to be acoustic data.
'samplerate': The audio data sample rate in Hz. 'samplerate': The audio data sample rate in Hz.
'nchannels': The number of audio channels in the file 'nchannels': The number of audio channels in the file
'sensitivity': (Optionally) the stored sensitivity of the record channels. 'sensitivity': (Optionally) the stored sensitivity of the record channels.
@ -42,6 +44,7 @@ from .lasp_config import LASP_NUMPY_FLOAT_TYPE
from scipy.io import wavfile from scipy.io import wavfile
import os import os
import time import time
import wave
class BlockIter: class BlockIter:

View File

@ -78,10 +78,6 @@ class Recording:
stream = self._stream stream = self._stream
f = self._f f = self._f
nchannels = stream.nchannels nchannels = stream.nchannels
if stream.monitor_gen:
nchannels += 1
self.monitor_gen = stream.monitor_gen
self._ad = f.create_dataset('audio', self._ad = f.create_dataset('audio',
(1, stream.blocksize, nchannels), (1, stream.blocksize, nchannels),
@ -173,11 +169,7 @@ class Recording:
return return
self._ad.resize(self._ablockno()+1, axis=0) self._ad.resize(self._ablockno()+1, axis=0)
if self.monitor_gen: self._ad[self._ablockno(), :, :] = indata
self._ad[self._ablockno(), :, 0] = outdata[:, 0]
self._ad[self._ablockno(), :, 1:] = indata
else:
self._ad[self._ablockno(), :, :] = indata
self._ablockno += 1 self._ablockno += 1
def _vCallback(self, frame, framectr): def _vCallback(self, frame, framectr):

View File

@ -32,6 +32,7 @@ setup(
author_email="j.a.dejong@ascee.nl", author_email="j.a.dejong@ascee.nl",
install_requires=['matplotlib>=1.0', install_requires=['matplotlib>=1.0',
'scipy>=1.0', 'numpy>=1.0', 'h5py', 'scipy>=1.0', 'numpy>=1.0', 'h5py',
'dataclasses_json',
], ],
license='MIT', license='MIT',
description="Library for Acoustic Signal Processing", description="Library for Acoustic Signal Processing",