From 6b8abb878acd84c02a83d2872912f619cb5103af Mon Sep 17 00:00:00 2001 From: "J.A. de Jong - Redu-Sone B.V., ASCEE V.O.F" Date: Sat, 17 Oct 2020 19:14:24 +0200 Subject: [PATCH] RtAudio in a theoretically working state. Now its time for some testing. --- lasp/c/CMakeLists.txt | 2 +- lasp/device/CMakeLists.txt | 14 +- lasp/device/lasp_common_decls.pxd | 97 +++++++---- lasp/device/lasp_cppdaq.cpp | 68 ++++++-- lasp/device/lasp_cppdaq.h | 193 +++++++++++---------- lasp/device/lasp_cpprtaudio.cpp | 267 +++++++++++++++++++++++++++++- lasp/device/lasp_daq.pxd | 8 + lasp/device/lasp_daq.pyx | 212 ++++++++++-------------- lasp/device/lasp_daqconfig.pxd | 5 + lasp/device/lasp_daqconfig.pyx | 148 +++++++++-------- lasp/lasp_measurement.py | 4 +- 11 files changed, 680 insertions(+), 338 deletions(-) diff --git a/lasp/c/CMakeLists.txt b/lasp/c/CMakeLists.txt index 7eb9403..a68d4ef 100644 --- a/lasp/c/CMakeLists.txt +++ b/lasp/c/CMakeLists.txt @@ -27,4 +27,4 @@ add_library(lasp_lib -target_link_libraries(lasp_lib ${LASP_FFT_LIBRARY} openblas) +target_link_libraries(lasp_lib ${LASP_FFT_LIBRARY} openblas pthread) diff --git a/lasp/device/CMakeLists.txt b/lasp/device/CMakeLists.txt index 3085508..320d528 100644 --- a/lasp/device/CMakeLists.txt +++ b/lasp/device/CMakeLists.txt @@ -1,21 +1,21 @@ set(cpp_daq_files lasp_cppdaq.cpp) -set(mycpp_daq_linklibs pthread) +set(cpp_daq_linklibs pthread) if(LASP_RTAUDIO) include_directories(/usr/include/rtaudio) list(APPEND cpp_daq_files lasp_cpprtaudio.cpp) - list(PREPEND mycpp_daq_linklibs rtaudio) + list(PREPEND cpp_daq_linklibs rtaudio) endif() if(LASP_ULDAQ) list(APPEND cpp_daq_files lasp_cppuldaq.cpp) - list(PREPEND mycpp_daq_linklibs uldaq) + list(PREPEND cpp_daq_linklibs uldaq) endif() if(win32) - list(APPEND mycpp_daq_linklibs python) + list(APPEND cpp_daq_linklibs python) endif(win32) add_library(cpp_daq ${cpp_daq_files}) -target_link_libraries(cpp_daq ${mycpp_daq_linklibs}) +target_link_libraries(cpp_daq ${cpp_daq_linklibs}) foreach(cython_file lasp_daq lasp_deviceinfo lasp_daqconfig) @@ -27,7 +27,7 @@ foreach(cython_file lasp_daq lasp_deviceinfo lasp_daqconfig) cython_add_module(${cython_file} ${cython_file}.pyx) - target_link_libraries(${cython_file} ${mycpp_daq_linklibs}) + target_link_libraries(${cython_file} cpp_daq ${cpp_daq_linklibs}) endforeach() @@ -36,4 +36,4 @@ endforeach() # also used in the testing directory. But better to already link cpp_daq with # linklibs. -# set(cpp_daq_linklibs "${mycpp_daq_linklibs}" CACHE INTERNAL "cpp_daq_linklibs") +# set(cpp_daq_linklibs "${cpp_daq_linklibs}" CACHE INTERNAL "cpp_daq_linklibs") diff --git a/lasp/device/lasp_common_decls.pxd b/lasp/device/lasp_common_decls.pxd index 2c06fa2..1332d70 100644 --- a/lasp/device/lasp_common_decls.pxd +++ b/lasp/device/lasp_common_decls.pxd @@ -49,55 +49,96 @@ cdef extern from "lasp_cppdaq.h" nogil: unsigned apicode unsigned api_specific_subcode + @staticmethod + vector[DaqApi] getAvailableApis(); + cdef cppclass DataType: string name unsigned sw bool is_floating DataType dtype_fl64 + bool operator==(const DataType&) + + DataType dtype_fl64 + DataType dtype_fl32 + DataType dtype_int8 + DataType dtype_int16 + DataType dtype_int32 + DataType dtype_invalid cdef cppclass cppDeviceInfo "DeviceInfo": DaqApi api - string device_name - unsigned devindex + string device_name + + int api_specific_devindex + vector[DataType] availableDataTypes + int prefDataTypeIndex + vector[double] availableSampleRates + int prefSampleRateIndex + vector[us] availableFramesPerBlock + unsigned prefFramesPerBlockIndex - int prefSampleRateIndex - int prefInputRangeIndex - int prefFramesPerBlockIndex - unsigned ninchannels - unsigned noutchannels - bool hasInputIEPE - bool hasInputACCouplingSwitch - bool hasInputTrigger - vector[double] availableInputRanges + dvec availableInputRanges + int prefInputRangeIndex - cdef cppclass DaqConfiguration: - boolvec eninchannels - boolvec enoutchannels - vector[string] channel_names - vector[double] channel_sensitivities - unsigned sampleRateIndex - DataType datatype - bool monitorOutput - unsigned nFramesPerBlock; + unsigned ninchannels + unsigned noutchannels - boolvec inputIEPEEnabled; - boolvec inputACCouplingMode; - usvec inputRangeIndices; + bool hasInputIEPE + bool hasInputACCouplingSwitch + bool hasInputTrigger + + cdef cppclass cppDaqConfiguration "DaqConfiguration": + DaqApi api + string device_name + + boolvec eninchannels + boolvec enoutchannels + + vector[double] inchannel_sensitivities + vector[string] inchannel_names + + vector[string] outchannel_names + + us sampleRateIndex + + us dataTypeIndex + + us framesPerBlockIndex + + bool monitorOutput + vector[us] input_qty_idx + + boolvec inputIEPEEnabled + boolvec inputACCouplingMode + + usvec inputRangeIndices + + DaqConfiguration() + + int getHighestInChannel() + int getHighestOutChannel() + + int getLowestInChannel() + int getLowestOutChannel() cdef cppclass cppDaq "Daq": void start(SafeQueue[void*] *inQueue, SafeQueue[void*] *outQueue) except + - void stop() + void stop() except + double samplerate() - us neninchannels() + us neninchannels(bool include_monitorchannel=False) us nenoutchannels() - DataType getDataType() + DataType dataType() + us framesPerBlock() + bool isRunning() + bool duplexMode() @staticmethod - cppDaq* createDaqDevice(cppDeviceInfo&, DaqConfiguration&) + cppDaq* createDaq(cppDeviceInfo&, cppDaqConfiguration&) except + @staticmethod - vector[cppDeviceInfo] getDeviceInfo() + vector[cppDeviceInfo] getDeviceInfo() except + diff --git a/lasp/device/lasp_cppdaq.cpp b/lasp/device/lasp_cppdaq.cpp index 00787de..4b01812 100644 --- a/lasp/device/lasp_cppdaq.cpp +++ b/lasp/device/lasp_cppdaq.cpp @@ -24,6 +24,24 @@ vector Daq::getDeviceInfo() { return devs; } +vector DaqApi::getAvailableApis() { + + vector apis; + apis.resize(6); +#ifdef HAS_ULDAQ_API + apis[uldaqapi.apicode] = uldaqapi; +#endif +#ifdef HAS_RTAUDIO_API + apis[rtaudioAlsaApi.apicode] = rtaudioAlsaApi; + apis[rtaudioPulseaudioApi.apicode] = rtaudioPulseaudioApi; + apis[rtaudioWasapiApi.apicode] = rtaudioWasapiApi; + apis[rtaudioDsApi.apicode] = rtaudioDsApi; + apis[rtaudioAsioApi.apicode] = rtaudioAsioApi; +#endif + return apis; +} + + DaqConfiguration::DaqConfiguration(const DeviceInfo &device) { api = device.api; @@ -61,19 +79,37 @@ bool DaqConfiguration::match(const DeviceInfo& dev) const { return (dev.device_name == device_name && dev.api == api); } -Daq *Daq::createDevice(const DaqConfiguration &config, - const vector &devinfos) { - - bool match; - DeviceInfo devinfo; - for (auto cur_devinfo : devinfos) { - if ((match = config.match(cur_devinfo))) { - devinfo = cur_devinfo; - break; - } +int DaqConfiguration::getHighestInChannel() const { + for(int i=eninchannels.size()-1; i>-1;i--) { + if(eninchannels[i]) return i; } - if (!match) { - return NULL; + return -1; +} + +int DaqConfiguration::getHighestOutChannel() const { + for(us i=enoutchannels.size()-1; i>=0;i--) { + if(enoutchannels[i]) return i; + } + return -1; +} +int DaqConfiguration::getLowestInChannel() const { + for(us i=0; i #include +#include #include using std::cerr; @@ -30,14 +30,17 @@ typedef vector usvec; typedef std::lock_guard mutexlock; class DataType { - public: - string name; - unsigned sw; - bool is_floating; +public: + string name; + unsigned sw; + bool is_floating; - DataType(const char *name, unsigned sw, bool is_floating) + DataType(const char *name, unsigned sw, bool is_floating) : name(name), sw(sw), is_floating(is_floating) {} - DataType() : name("invalid data type"), sw(0), is_floating(false) {} + DataType() : name("invalid data type"), sw(0), is_floating(false) {} + bool operator==(const DataType &o) { + return (name == o.name && sw == o.sw && is_floating == o.is_floating); + } }; const DataType dtype_invalid; @@ -48,27 +51,25 @@ const DataType dtype_int16("16-bits integers", 2, false); const DataType dtype_int32("32-bits integers", 4, false); const std::vector dataTypes = { - dtype_int8, - dtype_int16, - dtype_int32, - dtype_fl32, - dtype_fl64, + dtype_int8, dtype_int16, dtype_int32, dtype_fl32, dtype_fl64, }; class DaqApi { - public: - string apiname = ""; - unsigned apicode = 0; - unsigned api_specific_subcode = 0; +public: + string apiname = "Invalid API"; + int apicode = -1; + unsigned api_specific_subcode = 0; - DaqApi(string apiname, unsigned apicode, unsigned api_specific_subcode = 0) + DaqApi(string apiname, unsigned apicode, unsigned api_specific_subcode = 0) : apiname(apiname), apicode(apicode), - api_specific_subcode(api_specific_subcode) {} - DaqApi() {} - bool operator==(const DaqApi &other) const { - return (apiname == other.apiname && apicode == other.apicode && - api_specific_subcode == other.api_specific_subcode); - } + api_specific_subcode(api_specific_subcode) {} + DaqApi() {} + bool operator==(const DaqApi &other) const { + return (apiname == other.apiname && apicode == other.apicode && + api_specific_subcode == other.api_specific_subcode); + } + + static vector getAvailableApis(); }; #ifdef HAS_ULDAQ_API @@ -76,127 +77,143 @@ const DaqApi uldaqapi("UlDaq", 0); #endif #ifdef HAS_RTAUDIO_API const DaqApi rtaudioAlsaApi("RtAudio Linux ALSA", 1, RtAudio::Api::LINUX_ALSA); -const DaqApi rtaudioPulseaudioApi("RtAudio Linux Pulseaudio", 2, RtAudio::Api::LINUX_PULSE); -const DaqApi rtaudioWasapiApi("RtAudio Windows Wasapi", 3, RtAudio::Api::WINDOWS_WASAPI); -const DaqApi rtaudioDsApi("RtAudio Windows DirectSound", 4, RtAudio::Api::WINDOWS_DS); -const DaqApi rtaudioAsioApi("RtAudio Windows ASIO", 4, RtAudio::Api::WINDOWS_ASIO); +const DaqApi rtaudioPulseaudioApi("RtAudio Linux Pulseaudio", 2, + RtAudio::Api::LINUX_PULSE); +const DaqApi rtaudioWasapiApi("RtAudio Windows Wasapi", 3, + RtAudio::Api::WINDOWS_WASAPI); +const DaqApi rtaudioDsApi("RtAudio Windows DirectSound", 4, + RtAudio::Api::WINDOWS_DS); +const DaqApi rtaudioAsioApi("RtAudio Windows ASIO", 5, + RtAudio::Api::WINDOWS_ASIO); #endif - // Structure containing device info parameters class DeviceInfo { - public: - DaqApi api; - string device_name = ""; +public: + DaqApi api; + string device_name = ""; - int api_specific_devindex = -1; + int api_specific_devindex = -1; - vector availableDataTypes; - int prefDataTypeIndex = 0; + vector availableDataTypes; + int prefDataTypeIndex = 0; - vector availableSampleRates; - int prefSampleRateIndex = -1; + vector availableSampleRates; + int prefSampleRateIndex = -1; - vector availableFramesPerBlock; - unsigned prefFramesPerBlockIndex = 0; + vector availableFramesPerBlock; + unsigned prefFramesPerBlockIndex = 0; - dvec availableInputRanges; - int prefInputRangeIndex = 0; + dvec availableInputRanges; + int prefInputRangeIndex = 0; - unsigned ninchannels = 0; - unsigned noutchannels = 0; + unsigned ninchannels = 0; + unsigned noutchannels = 0; - bool hasInputIEPE = false; - bool hasInputACCouplingSwitch = false; - bool hasInputTrigger = false; + bool hasInputIEPE = false; + bool hasInputACCouplingSwitch = false; + bool hasInputTrigger = false; - /* DeviceInfo(): */ - /* datatype(dtype_invalid) { } */ + /* DeviceInfo(): */ + /* datatype(dtype_invalid) { } */ - double prefSampleRate() const { - if (((us) prefSampleRateIndex < availableSampleRates.size()) && - (prefSampleRateIndex >= 0)) { - return availableSampleRates[prefSampleRateIndex]; - } else { - throw std::runtime_error("No prefered sample rate available"); - } + double prefSampleRate() const { + if (((us)prefSampleRateIndex < availableSampleRates.size()) && + (prefSampleRateIndex >= 0)) { + return availableSampleRates[prefSampleRateIndex]; + } else { + throw std::runtime_error("No prefered sample rate available"); } + } - operator string() const { - std::stringstream str; - str << api.apiname + " " << api_specific_devindex + operator string() const { + std::stringstream str; + str << api.apiname + " " << api_specific_devindex << " number of input channels: " << ninchannels << " number of output channels: " << noutchannels; - return str.str(); - } + return str.str(); + } }; // Device configuration parameters class DaqConfiguration { - public: - DaqApi api; - string device_name; +public: + DaqApi api; + string device_name; - boolvec eninchannels; // Enabled input channels - boolvec enoutchannels; // Enabled output channels + boolvec eninchannels; // Enabled input channelsvice(const DeviceInfo& devinfo, + boolvec enoutchannels; // Enabled output channels - vector inchannel_sensitivities; - vector inchannel_names; + vector inchannel_sensitivities; + vector inchannel_names; + vector inchannel_qtys; - // This is not necessary at the moment - /* vector outchannel_sensitivities; */ - vector outchannel_names; + // This is not necessary at the moment + /* vector outchannel_sensitivities; */ + vector outchannel_names; - us sampleRateIndex = 0; // Index in list of sample rates + us sampleRateIndex = 0; // Index in list of sample rates - us dataTypeIndex = 0; // Required datatype for output, should be - // present in the list + us dataTypeIndex = 0; // Required datatype for output, should be + // present in the list - us framesPerBlockIndex = 0; + us framesPerBlockIndex = 0; - bool monitorOutput = false; // Whether the first output channel should be replicated - // to the input as the first channel + bool monitorOutput = false; + boolvec inputIEPEEnabled; + boolvec inputACCouplingMode; - boolvec inputIEPEEnabled; - boolvec inputACCouplingMode; + usvec inputRangeIndices; - usvec inputRangeIndices; + // Create a default configuration, with all channels disabled on both + // input and output, and default channel names + DaqConfiguration(const DeviceInfo &device); + DaqConfiguration() {} - // Create a default configuration, with all channels disabled on both - // input and output, and default channel names - DaqConfiguration(const DeviceInfo &device); + bool match(const DeviceInfo &devinfo) const; - bool match(const DeviceInfo &devinfo) const; + int getHighestInChannel() const; + int getHighestOutChannel() const; + + int getLowestInChannel() const; + int getLowestOutChannel() const; }; class Daq; -class Daq : public DaqConfiguration,public DeviceInfo { +class Daq : public DaqConfiguration, public DeviceInfo { mutable std::mutex mutex; - public: +public: static vector getDeviceInfo(); - static Daq *createDevice(const DaqConfiguration &config, - const std::vector &devinfos); + static Daq *createDaq(const DeviceInfo &, const DaqConfiguration &config); Daq(const DeviceInfo &devinfo, const DaqConfiguration &config); virtual void start(SafeQueue *inqueue, - SafeQueue *outqueue) = 0; + SafeQueue *outqueue) = 0; virtual void stop() = 0; virtual bool isRunning() const = 0; virtual ~Daq(){}; - us neninchannels() const; + us neninchannels(bool include_monitorchannel = true) const; us nenoutchannels() const; double samplerate() const; double inputRangeForChannel(us ch) const; DataType dataType() const; + + us framesPerBlock() const { + mutexlock lock(mutex); + return availableFramesPerBlock[framesPerBlockIndex]; + } + bool duplexMode() const { + return (neninchannels(false) > 0 && nenoutchannels() > 0); + } }; #endif // LASP_CPPDAQ_H diff --git a/lasp/device/lasp_cpprtaudio.cpp b/lasp/device/lasp_cpprtaudio.cpp index 705c19e..b3cc7e8 100644 --- a/lasp/device/lasp_cpprtaudio.cpp +++ b/lasp/device/lasp_cpprtaudio.cpp @@ -2,6 +2,9 @@ #include #include #include +#include +#include + using std::atomic; void fillRtAudioDeviceInfo(vector &devinfolist) { @@ -72,26 +75,272 @@ void fillRtAudioDeviceInfo(vector &devinfolist) { d.prefDataTypeIndex = d.availableDataTypes.size() - 1; devinfolist.push_back(d); - + } } } +int mycallback(void *outputBuffer, void *inputBuffer, + unsigned int nFrames, + double streamTime, + RtAudioStreamStatus status, + void *userData); + +void myerrorcallback(RtAudioError::Type,const string& errorText); + class AudioDaq: public Daq { - atomic stopThread; + SafeQueue *inqueue = NULL; + SafeQueue *outqueue = NULL; + SafeQueue *outDelayqueue = NULL; - std::thread* thread = NULL; + RtAudio* rtaudio = NULL; + RtAudio::StreamParameters* instreamparams = nullptr; + RtAudio::StreamParameters* outstreamparams = nullptr; - SafeQueue *inqueue = NULL; - SafeQueue *outqueue = NULL; + us nFramesPerBlock; - void* inbuffer = NULL; - void* outbuffer = NULL; + public: + AudioDaq(const DeviceInfo& devinfo, + const DaqConfiguration& config): + Daq(devinfo, config) { - public: - friend void threadfcn(AudioDaq*); + nFramesPerBlock = this->framesPerBlock(); + + + if(neninchannels(false) > 0) { + instreamparams = new RtAudio::StreamParameters(); + instreamparams->nChannels = getHighestInChannel(); + instreamparams->firstChannel = 0; + instreamparams->deviceId = devinfo.api_specific_devindex; + } + + if(nenoutchannels() > 0) { + outstreamparams = new RtAudio::StreamParameters(); + outstreamparams->nChannels = getHighestOutChannel(); + outstreamparams->firstChannel = 0; + outstreamparams->deviceId = devinfo.api_specific_devindex; + } + + RtAudio::StreamOptions streamoptions; + streamoptions.flags = RTAUDIO_NONINTERLEAVED | RTAUDIO_HOG_DEVICE; + + streamoptions.numberOfBuffers = 2; + streamoptions.streamName = "RtAudio stream"; + streamoptions.priority = 0; + + RtAudioFormat format; + DataType dtype = dataType(); + if(dtype == dtype_fl32) { + format = RTAUDIO_FLOAT32; + } else if(dtype == dtype_fl64) { + format = RTAUDIO_FLOAT64; + } else if(dtype == dtype_int8) { + format = RTAUDIO_SINT8; + } else if(dtype == dtype_int16) { + format = RTAUDIO_SINT16; + } else if(dtype == dtype_int32) { + format = RTAUDIO_SINT32; + } else { + throw runtime_error("Invalid data type"); + } + + try { + rtaudio = new RtAudio((RtAudio::Api) devinfo.api_specific_devindex); + if(!rtaudio) { + throw runtime_error("RtAudio allocation failed"); + } + rtaudio->openStream( + outstreamparams, + instreamparams, + format, + (us) samplerate(), + &nFramesPerBlock, + &mycallback, + (void*) this, + &streamoptions, + &myerrorcallback + ); + } catch(...) { + if(rtaudio) delete rtaudio; + throw; + } + if(monitorOutput) { + outDelayqueue = new SafeQueue(); + } + + } + + friend int mycallback(void *outputBuffer, void *inputBuffer, + unsigned int nFrames, + double streamTime, + RtAudioStreamStatus status, + void *userData); + + + void start(SafeQueue *inqueue, SafeQueue *outqueue) { + if(isRunning()){ + throw runtime_error("Stream already running"); + } + + if(neninchannels(false) > 0 && !inqueue) { + throw runtime_error("inqueue argument not given"); + } + if(nenoutchannels() > 0 && !outqueue) { + throw runtime_error("outqueue argument not given"); + } + assert(rtaudio); + rtaudio->startStream(); + + } + + void stop() { + if(!isRunning()) { + cerr << "Stream is already stopped" << endl; + } + else { + assert(rtaudio); + rtaudio->stopStream(); + } + + } + bool isRunning() const {return (rtaudio && rtaudio->isStreamRunning());} + + ~AudioDaq() { + assert(rtaudio); + if(rtaudio->isStreamRunning()) { + stop(); + } + if(rtaudio->isStreamOpen()) { + rtaudio->closeStream(); + } + + if(rtaudio) delete rtaudio; + if(outDelayqueue) delete outDelayqueue; + if(instreamparams) delete instreamparams; + if(outstreamparams) delete outstreamparams; + + } }; + +Daq* createRtAudioDevice(const DeviceInfo& devinfo, + const DaqConfiguration& config) { + + AudioDaq *daq = NULL; + + try { + daq = new AudioDaq(devinfo, config); + + } catch (runtime_error &e) { + if (daq) + delete daq; + throw; + } + return daq; +} + + +int mycallback( + void *outputBuffervoid, + void *inputBuffervoid, + unsigned int nFrames, + double streamTime, + RtAudioStreamStatus status, + void *userData) { + + u_int8_t* inputBuffer = (u_int8_t*) inputBuffervoid; + u_int8_t* outputBuffer = (u_int8_t*) outputBuffervoid; + + AudioDaq* daq = (AudioDaq*) userData; + DataType dtype = daq->dataType(); + /* us neninchannels = daq->neninchannels(); */ + us neninchannels_inc_mon = daq->neninchannels(); + us nenoutchannels = daq->nenoutchannels(); + + bool monitorOutput = daq->monitorOutput; + us bytesperchan = dtype.sw*nFrames; + us monitorOffset = ((us) monitorOutput)*bytesperchan; + + SafeQueue *inqueue = daq->inqueue; + SafeQueue *outqueue = daq->outqueue; + SafeQueue *outDelayqueue = daq->outDelayqueue; + + const boolvec& eninchannels = daq->eninchannels; + const boolvec& enoutchannels = daq->enoutchannels; + + if(inputBuffer || monitorOutput) { + + u_int8_t *inbuffercpy = (u_int8_t*) malloc(bytesperchan*neninchannels_inc_mon); + if(inputBuffer) { + us j=0; // OUR buffer channel counter + us i=0; // RtAudio channel counter + for(us ch=daq->getLowestInChannel();ch<=daq->getHighestInChannel();ch++) { + if(eninchannels[ch]) { + memcpy( + &(inbuffercpy[monitorOffset+j*bytesperchan]), + &(inputBuffer[i*bytesperchan]), + bytesperchan); + j++; + } + i++; + } + } + if(monitorOutput) { + assert(outDelayqueue); + + if(!daq->outDelayqueue->empty()) { + void* dat = daq->outDelayqueue->dequeue(); + memcpy((void*) inbuffercpy, dat, bytesperchan); + free(dat); + } else { + cerr << "Warning: output delay queue appears empty!" << endl; + memset(inbuffercpy, 0, bytesperchan); + } + } + assert(inqueue); + inqueue->enqueue(inbuffercpy); + + } + + if(outputBuffer) { + assert(outqueue); + if(!outqueue->empty()) { + u_int8_t* outbuffercpy = (u_int8_t*) outqueue->dequeue(); + us j=0; // OUR buffer channel counter + us i=0; // RtAudio channel counter + for(us ch=0;ch<=daq->getHighestOutChannel();ch++) { + if(enoutchannels[ch]) { + memcpy( + &(outputBuffer[i*bytesperchan]), + &(outbuffercpy[j*bytesperchan]), + bytesperchan); + j++; + } + else { + memset( + &(outputBuffer[i*bytesperchan]),0,bytesperchan); + } + i++; + } + if(!monitorOutput) { + free(outbuffercpy); + } else { + assert(outDelayqueue); + outDelayqueue->enqueue((void*) outbuffercpy); + } + } + else { + cerr << "Stream output buffer underflow, zero-ing buffer... " << endl; + } + + + } + + return 0; +} +void myerrorcallback(RtAudioError::Type,const string& errorText) { + cerr << errorText << endl; +} diff --git a/lasp/device/lasp_daq.pxd b/lasp/device/lasp_daq.pxd index deb6cdc..f97682d 100644 --- a/lasp/device/lasp_daq.pxd +++ b/lasp/device/lasp_daq.pxd @@ -1,2 +1,10 @@ include "lasp_common_decls.pxd" +ctypedef struct PyStreamData + +cdef class Daq: + cdef: + PyStreamData *sd + cppDaq* daq_device + + cdef cleanupStream(self, PyStreamData* sd) diff --git a/lasp/device/lasp_daq.pyx b/lasp/device/lasp_daq.pyx index 08e747a..e0eeead 100644 --- a/lasp/device/lasp_daq.pyx +++ b/lasp/device/lasp_daq.pyx @@ -1,11 +1,27 @@ cimport cython from .lasp_deviceinfo cimport DeviceInfo +from .lasp_daqconfig cimport DaqConfiguration from cpython.ref cimport PyObject,Py_INCREF, Py_DECREF from .lasp_device_common import AvType __all__ = ['Daq'] +cdef cnp.NPY_TYPES getNumpyDataType(DataType& dt): + if(dt == dtype_fl32): + return cnp.NPY_FLOAT32 + elif(dt == dtype_fl64): + return cnp.NPY_FLOAT64 + elif(dt == dtype_int8): + return cnp.NPY_INT8 + elif(dt == dtype_int16): + return cnp.NPY_INT16 + elif(dt == dtype_int32): + return cnp.NPY_INT32 + else: + raise ValueError('Unknown data type') + + ctypedef struct PyStreamData: PyObject* pyCallback @@ -24,6 +40,8 @@ ctypedef struct PyStreamData: double samplerate + cnp.NPY_TYPES npy_format + # If either of these queue pointers are NULL, it means the stream does not have an # input, or output. SafeQueue[void*] *inQueue @@ -33,7 +51,6 @@ ctypedef struct PyStreamData: cdef void audioCallbackPythonThreadFunction(void* voidsd) nogil: cdef: PyStreamData* sd = voidsd - cnp.NPY_TYPES npy_format double* inbuffer = NULL double* outbuffer = NULL @@ -43,8 +60,6 @@ cdef void audioCallbackPythonThreadFunction(void* voidsd) nogil: unsigned nBytesPerChan= sd.nBytesPerChan unsigned nFramesPerBlock= sd.nFramesPerBlock - unsigned sw = sizeof(double) - double sleeptime = ( sd.nFramesPerBlock)/(4*sd.samplerate); us sleeptime_us = (sleeptime*1e6); @@ -64,11 +79,10 @@ cdef void audioCallbackPythonThreadFunction(void* voidsd) nogil: outbuffer, nFramesPerBlock, noutchannels, - npy_format, + sd.npy_format, False, # Do not transfer ownership True) # F-contiguous try: - rval = callback(None, npy_output, nFramesPerBlock, @@ -93,10 +107,9 @@ cdef void audioCallbackPythonThreadFunction(void* voidsd) nogil: inbuffer, nFramesPerBlock, ninchannels, - npy_format, + sd.npy_format, True, # Do transfer ownership True) # F-contiguous is True: data is Fortran-cont. - rval = callback(npy_input, None, nFramesPerBlock, @@ -118,9 +131,6 @@ cdef void audioCallbackPythonThreadFunction(void* voidsd) nogil: fprintf(stderr, 'Exiting python thread...\n') cdef class Daq: - cdef: - PyStreamData *sd - cppDaq* daq_device def __cinit__(self): @@ -132,9 +142,9 @@ cdef class Daq: self.sd = NULL def __dealloc__(self): - fprintf(stderr, "UlDaq.__dealloc__\n") + # fprintf(stderr, "UlDaq.__dealloc__\n") if self.sd is not NULL: - fprintf(stderr, "UlDaq.__dealloc__, stopping stream.\n") + fprintf(stderr, "UlDaq.__dealloc__: stopping stream.\n") self.stop() def isRunning(self): @@ -144,143 +154,101 @@ cdef class Daq: def getDeviceInfo(): cdef: vector[cppDeviceInfo] devinfos = cppDaq.getDeviceInfo() - pydevinfo = [] + pydevinfo = {} for i in range(devinfos.size()): d = DeviceInfo() d.devinfo = devinfos[i] - pydevinfo.append(d) + if d.api not in pydevinfo.keys(): + pydevinfo[d.api] = [d] + else: + pydevinfo[d.api].append(d) return pydevinfo - # @cython.nonecheck(True) - # def start(self, avstream): - # """ - # Opens a stream with specified parameters + @cython.nonecheck(True) + def start(self, avstream): + """ + Opens a stream with specified parameters - # Args: - # avstream: AvStream instance + Args: + avstream: AvStream instance - # Returns: None - # """ + Returns: None + """ + cdef: + DaqConfiguration pydaqconfig + DeviceInfo pydevinfo + cppDaqConfiguration* daqconfig + cppDeviceInfo* devinfo + vector[cppDeviceInfo] devinfos - # if self.sd is not NULL: - # assert self.daq_device is not NULL - # raise RuntimeError('Stream is already opened.') + if self.sd is not NULL: + assert self.daq_device is not NULL + raise RuntimeError('Stream is already opened.') - # daqconfig = avstream.daqconfig - # avtype = avstream.avtype - # device = avstream.device + pydaqconfig = avstream.daqconfig + avtype = avstream.avtype + pydevinfo = avstream.deviceinfo - # cdef: - # bint duplex_mode = daqconfig.duplex_mode - # bint monitorOutput = daqconfig.monitor_gen + daqconfig = &(pydaqconfig.config) + devinfo = &(pydevinfo.devinfo) - # unsigned int nFramesPerBlock = daqconfig.nFramesPerBlock - # unsigned int samplerate + self.daq_device = cppDaq.createDaq(devinfo[0], daqconfig[0]) + cdef: + cppDaq* daq = self.daq_device - # int i - # bint in_stream=False - # bint out_stream=False + cdef: + unsigned int nFramesPerBlock = daq.framesPerBlock() + double samplerate = daq.samplerate() + DataType dtype = daq.dataType() - # cppDeviceInfo devinfo - # DaqConfiguration cppconfig - - # cppDaq* daq_device - - # if daqconfig.nFramesPerBlock > 8192 or daqconfig.nFramesPerBlock < 512: - # raise ValueError('Invalid number of nFramesPerBlock') - - - - # # if daqconfig.outputDelayBlocks != 0: - # # print('WARNING: OutputDelayBlocks not supported by API') - - # # Determine sample rate and sample format, determine whether we are an - # # in or an output stream, or both - # # print(f'AvType: {avtype}') - # # print(f'Dup: {duplex_mode}') - - # 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) - # in_stream = True - # if duplex_mode: - # fprintf(stderr, 'Duplex mode enabled\n') - # out_stream = True - # elif avtype == AvType.audio_output: - # sampleformat = daqconfig.en_output_sample_format - # samplerate = int(daqconfig.en_output_rate) - # out_stream = True - # else: - # raise ValueError(f'Invalid stream type {avtype}') - - # if out_stream and daqconfig.firstEnabledOutputChannelNumber() == -1: - # raise RuntimeError('No output channels enabled') - - # if in_stream and daqconfig.firstEnabledInputChannelNumber() == -1: - # raise RuntimeError('No input channels enabled') + if nFramesPerBlock > 8192 or nFramesPerBlock < 512: + del self.daq_device + raise ValueError('Invalid number of nFramesPerBlock') - # # All set, allocate the stream! - # self.sd = malloc(sizeof(PyStreamData)) - # if self.sd == NULL: - # raise MemoryError('Could not allocate stream: memory error.') + # All set, allocate the stream! + self.sd = malloc(sizeof(PyStreamData)) + if self.sd == NULL: + del daq + raise MemoryError('Could not allocate stream: memory error.') - # self.sd.stopThread.store(False) - # self.sd.inQueue = NULL - # self.sd.outQueue = NULL + self.sd.stopThread.store(False) + self.sd.inQueue = NULL + self.sd.outQueue = NULL - # self.sd.thread = NULL - # self.sd.samplerate = samplerate + self.sd.thread = NULL + self.sd.samplerate = samplerate - # self.sd.ninchannels = 0 - # self.sd.noutchannels = 0 - # self.sd.nBytesPerChan = daqconfig.nFramesPerBlock*sizeof(double) - # self.sd.nFramesPerBlock = daqconfig.nFramesPerBlock + self.sd.ninchannels = daq.neninchannels() + self.sd.noutchannels = daq.nenoutchannels() + self.sd.nBytesPerChan = nFramesPerBlock*dtype.sw + self.sd.nFramesPerBlock = nFramesPerBlock + self.sd.npy_format = getNumpyDataType(dtype) + if daq.neninchannels() > 0: + self.sd.inQueue = new SafeQueue[void*]() + if daq.nenoutchannels() > 0: + self.sd.inQueue = new SafeQueue[void*]() - # # Create channel maps for in channels, set in stream - # # parameters - # inch_enabled = 4*[False] - # if in_stream: - # inch_enabled = [True if ch.channel_enabled else False for ch in - # daqconfig.getInputChannels()] + self.sd.pyCallback = avstream._audioCallback - # self.sd.inQueue = new SafeQueue[void*]() + # Increase reference count to the callback + Py_INCREF( avstream._audioCallback) - # # Create channel maps for output channels - # outch_enabled = 1*[False] - # if out_stream: - # outch_enabled = [True if ch.channel_enabled else False for ch in - # daqconfig.getOutputChannels()] + with nogil: + self.sd.thread = new CPPThread[void*, void (*)(void*)](audioCallbackPythonThreadFunction, + self.sd) - # self.sd.outQueue = new SafeQueue[void*]() + # Allow stream stome time to start + CPPsleep_ms(500) - # daq_device = createDaqDevice(devinfo, cppconfig) + self.daq_device.start( + self.sd.inQueue, + self.sd.outQueue) - # self.sd.pyCallback = avstream._audioCallback - # self.sd.ninchannels = daq_device.neninchannels() - # self.sd.noutchannels = daq_device.nenoutchannels() - - # self.daq_device = daq_device - - # # Increase reference count to the callback - # Py_INCREF( avstream._audioCallback) - - # with nogil: - # self.sd.thread = new CPPThread[void*, void (*)(void*)](audioCallbackPythonThreadFunction, - # self.sd) - - # # Allow it to start - # CPPsleep_ms(500) - - # self.daq_device.start( - # self.sd.inQueue, - # self.sd.outQueue) - - # return nFramesPerBlock, self.daq_device.samplerate() + return nFramesPerBlock, self.daq_device.samplerate() def stop(self): if self.sd is NULL: diff --git a/lasp/device/lasp_daqconfig.pxd b/lasp/device/lasp_daqconfig.pxd index e69de29..83ed5bd 100644 --- a/lasp/device/lasp_daqconfig.pxd +++ b/lasp/device/lasp_daqconfig.pxd @@ -0,0 +1,5 @@ +include "lasp_common_decls.pxd" + +cdef class DaqConfiguration: + cdef: + cppDaqConfiguration config diff --git a/lasp/device/lasp_daqconfig.pyx b/lasp/device/lasp_daqconfig.pyx index 6e2047d..d11029c 100644 --- a/lasp/device/lasp_daqconfig.pyx +++ b/lasp/device/lasp_daqconfig.pyx @@ -13,6 +13,7 @@ from typing import List from dataclasses_json import dataclass_json from ..lasp_common import lasp_shelve, SIQtys, Qty +import json class CouplingMode: @@ -25,111 +26,126 @@ class Range: tenV = '+/- 10 V' undefined = 'Undefined' + @dataclass_json @dataclass -class DAQChannel: +class DaqChannel: channel_enabled: bool channel_name: str = 'Unnamed channel' sensitivity: float = 1.0 - qty: Qty = SIQtys.default - range_: str = 'Undefined' + range_index: int = 0 ACCoupling_enabled: bool = False IEPE_enabled: bool = False - -cdef class DAQConfiguration: +cdef class DaqConfiguration: """ Initialize a device descriptor """ - # DaqConfiguration config - def getInputChannels(self): - return self.input_channel_configs + @staticmethod + def from_json(self, jsonstring): + config_dict = json.loads(jsonstring) + return DaqConfiguration.from_dict(config_dict) - def getOutputChannels(self): - return self.output_channel_configs + @staticmethod + def from_dict(pydict): + cdef: + cppDaqConfiguration config + vector[DaqApi] apis = DaqApi.getAvailableApis() + config.api = apis[pydict['apicode']] + config.device_name = pydict['device_name'].encode('utf-8') + config.eninchannels = pydict['eninchannels'] + config.enoutchannels = pydict['enoutchannels'] + config.inchannel_names = pydict['inchannel_names'] + config.outchannel_names = pydict['outchannel_names'] + config.sampleRateIndex = pydict['sampleRateIndex'] + config.framesPerBlockIndex = pydict['framesPerBlockIndex'] + config.dataTypeIndex = pydict['dataTypeIndex'] + config.monitorOutput = pydict['monitorOutput'] + config.inputIEPEEnabled = pydict['inputIEPEEnabled'] + config.inputACCouplingMode = pydict['inputACCouplingMode'] + config.inputRangeIndices = pydict['inputRangeIndices'] - 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 + pydaqcfg = DaqConfiguration() + pydaqcfg.config = config - def lastEnabledInputChannelNumber(self): - last = -1 - for i, ch in enumerate(self.input_channel_configs): - if ch.channel_enabled: - last = i - return last + return pydaqcfg - 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 to_json(self): + return json.dumps(dict( + apicode = self.api.apicode, + device_name = self.config.device_name.decode('utf-8'), - def getEnabledInputChannels(self): - en_channels = [] - for chan in self.input_channel_configs: - if chan.channel_enabled: - en_channels.append(chan) - return en_channels + eninchannels = self.eninchannels(), + enoutchannels = self.enoutchannels(), - def getEnabledInputChannelNames(self): - return [ch.channel_name for ch in self.getEnabledInputChannels()] + inchannel_names = [name.decode('utf-8') for name in + self.config.inchannel_names], + outchannel_names = [name.decode('utf-8') for name in + self.config.outchannel_names], + sampleRateIndex = self.config.sampleRateIndex, + dataTypeIndex = self.config.dataTypeIndex, + nFramesPerBlockIndex = self.config.framesPerBlockIndex, + monitorOutput = self.config.monitorOutput, - def getEnabledInputChannelQtys(self): - return [ch.qty for ch in self.getEnabledInputChannels()] + inputIEPEEnabled = self.config.inputIEPEEnabled, + inputACCouplingMode = self.config.inputACCouplingMode, + inputRangeIndices = self.config.inputRangeIndices, + )) - def getEnabledInputChannelSensitivities(self): - return [ - float(channel.sensitivity) - for channel in self.getEnabledInputChannels() - ] + def getInChannel(self, i:int): + return DaqChannel( + channel_enabled=self.config.eninchannels[i], + channel_name=self.config.inchannel_names[i].decode('utf-8'), + sensitivity=self.config.inchannel_sensitivities[i], + range_index=self.config.inputRangeIndices[i], + ACCoupling_enabled=self.config.inputACCouplingMode[i], + IEPE_enabled=self.config.inputIEPEEnabled[i] + ) + def getOutChannel(self, i:int): + return DaqChannel( + channel_enabled=self.config.enoutchannels[i], + channel_name=self.config.outchannel_names[i].decode('utf-8'), + ) + def setInChannel(self, i:int, daqchannel: DaqChannel): + self.config.eninchannels[i] = daqchannel.channel_enabled + self.config.inchannel_names[i] = daqchannel.channel_name + self.config.inchannel_sensitivities[i] = daqchannel.sensitivity + self.config.inputRangeIndices[i] = daqchannel.range_index + self.config.inputACCouplingMode[i] = daqchannel.ACCoupling_enabled + self.config.inputIEPEEnabled[i] = daqchannel.IEPE_enabled - def getEnabledOutputChannels(self): - en_channels = [] - for chan in self.output_channel_configs: - if chan.channel_enabled: - en_channels.append(chan) - return en_channels + def setOutChannel(self, i:int, daqchannel: DaqChannel): + self.config.enoutchannels[i] = daqchannel.channel_enabled + self.config.outchannel_names[i] = daqchannel.channel_name - def getEnabledOutputChannelQtys(self): - return [ch.qty for ch in self.getEnabledOutputChannels()] + def eninchannels(self): + return self.config.eninchannels - def getEnabledOutputChannelSensitivities(self): - return [ - float(channel.sensitivity) - for channel in self.getEnabledOutputChannels() - ] + def enoutchannels(self): + return self.config.enoutchannels @staticmethod def loadConfigsJSON(): with lasp_shelve() as sh: configs_json = sh.load('daqconfigs', {}) - return configs_json + return configs_json @staticmethod def loadConfigs(): """ Returns a list of currently available configurations """ - configs_json = DAQConfiguration.loadConfigsJSON() + configs_json = DaqConfiguration.loadConfigsJSON() configs = {} for name, val in configs_json.items(): - configs[name] = DAQConfiguration.from_json(val) - return configs + configs[name] = DaqConfiguration.from_json(val) + return configs def saveConfig(self, name): - configs_json = DAQConfiguration.loadConfigsJSON() + configs_json = DaqConfiguration.loadConfigsJSON() with lasp_shelve() as sh: configs_json[name] = self.to_json() @@ -138,6 +154,6 @@ cdef class DAQConfiguration: @staticmethod def deleteConfig(name): with lasp_shelve() as sh: - cur_configs = DAQConfiguration.loadConfigs() + cur_configs = DaqConfiguration.loadConfigs() del cur_configs[name] sh.store('daqconfigs', cur_configs) diff --git a/lasp/lasp_measurement.py b/lasp/lasp_measurement.py index 9699bf4..42a4955 100644 --- a/lasp/lasp_measurement.py +++ b/lasp/lasp_measurement.py @@ -48,7 +48,7 @@ import os import time import wave from .lasp_common import SIQtys, Qty -from .device import DAQChannel +from .device import DaqChannel import logging @@ -217,7 +217,7 @@ class Measurement: @property def channelConfig(self): - return [DAQChannel(channel_enabled=True, + return [DaqChannel(channel_enabled=True, channel_name=chname, sensitivity=sens, qty=qty)