From a58be3ab873c1e6d49a57c7e2b29381d8959571d Mon Sep 17 00:00:00 2001 From: "J.A. de Jong - Redu-Sone B.V., ASCEE V.O.F" Date: Sun, 11 Jun 2023 14:44:15 +0200 Subject: [PATCH 1/6] Simple input tests script --- examples/test_input.py | 49 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 49 insertions(+) create mode 100755 examples/test_input.py diff --git a/examples/test_input.py b/examples/test_input.py new file mode 100755 index 0000000..c248836 --- /dev/null +++ b/examples/test_input.py @@ -0,0 +1,49 @@ +#!/usr/bin/env python3 +import lasp +# Get handle to stream manager +mgr = lasp.StreamMgr.getInstance() +import time +time.sleep(1) + +ds = mgr.getDeviceInfo() +# Search for a device +for i, d in enumerate(ds): + print(f'{i}: ' + d.device_name) + +d = ds[0] # Create a configuration and enable some input channels +config = lasp.DaqConfiguration(d) +config.inchannel_config[0].enabled = True +config.inchannel_config[1].enabled = True +# Choose a different number of frames per block +config.framesPerBlockIndex = 2 + +# Start a stream with a configuration +mgr.startStream(config) + +def reset_cb(daq): + print('Reset called') + +def cb(data): + # Print something on callback + print(data.shape) + return True + + +# Attach the indata handler to the stream +#i = lasp.InDataHandler(mgr, cb, reset_cb) + +ppm = lasp.PPMHandler(mgr) +#del ppm +del mgr + +#del i +try: + while True: + val, clip = ppm.getCurrentValue() + print(val) + time.sleep(0.1) + + #print(f'{val[0]} {val[1]}', end='') +except KeyboardInterrupt: + pass +# mgr.stopStream(lasp.StreamMgr.StreamType.input) From 303e15e2d620b5635b5bde0cdb52acdb6368853a Mon Sep 17 00:00:00 2001 From: "J.A. de Jong - Redu-Sone B.V., ASCEE V.O.F" Date: Mon, 12 Jun 2023 09:11:08 +0200 Subject: [PATCH 2/6] Renamed ThreadSafeThreadPool to GlobalThreadPool. As of https://github.com/bshoshany/thread-pool/issues/112, the thread pool itself is thread-safe, so we removed the (extra, unnecessary) mutexes around it. --- src/lasp/device/lasp_streammgr.h | 2 +- src/lasp/dsp/lasp_biquadbank.h | 2 +- src/lasp/dsp/lasp_slm.h | 2 +- src/lasp/dsp/lasp_thread.cpp | 7 ++++--- src/lasp/dsp/lasp_thread.h | 22 ++++------------------ src/lasp/dsp/lasp_threadedindatahandler.h | 2 +- 6 files changed, 12 insertions(+), 25 deletions(-) diff --git a/src/lasp/device/lasp_streammgr.h b/src/lasp/device/lasp_streammgr.h index 8955cc6..d5e1e4c 100644 --- a/src/lasp/device/lasp_streammgr.h +++ b/src/lasp/device/lasp_streammgr.h @@ -31,7 +31,7 @@ class StreamMgr { */ std::unique_ptr _inputStream, _outputStream; - ThreadSafeThreadPool _pool; + GlobalThreadPool _pool; /** * @brief All indata handlers are called when input data is available. Note diff --git a/src/lasp/dsp/lasp_biquadbank.h b/src/lasp/dsp/lasp_biquadbank.h index 0328cea..cc4833d 100644 --- a/src/lasp/dsp/lasp_biquadbank.h +++ b/src/lasp/dsp/lasp_biquadbank.h @@ -61,7 +61,7 @@ public: class BiquadBank : public Filter { std::vector _filters; vd _gains; - ThreadSafeThreadPool _pool; + GlobalThreadPool _pool; mutable std::mutex _mtx; public: diff --git a/src/lasp/dsp/lasp_slm.h b/src/lasp/dsp/lasp_slm.h index 1c2d871..297ff8f 100644 --- a/src/lasp/dsp/lasp_slm.h +++ b/src/lasp/dsp/lasp_slm.h @@ -15,7 +15,7 @@ * channel. A channel is the result of a filtered signal */ class SLM { - ThreadSafeThreadPool _pool; + GlobalThreadPool _pool; /** * @brief A, C or Z weighting, depending on the pre-filter installed. */ diff --git a/src/lasp/dsp/lasp_thread.cpp b/src/lasp/dsp/lasp_thread.cpp index a04e051..af3cde9 100644 --- a/src/lasp/dsp/lasp_thread.cpp +++ b/src/lasp/dsp/lasp_thread.cpp @@ -12,14 +12,15 @@ std::weak_ptr _global_weak_pool; /** - * @brief Static storage for the mutex. + * @brief Global mutex, used to restrict the pool creation to a single thread + * at once. */ -std::mutex ThreadSafeThreadPool::_mtx; +std::mutex _mtx; using Lck = std::scoped_lock; using rte = std::runtime_error; -ThreadSafeThreadPool::ThreadSafeThreadPool() { +GlobalThreadPool::GlobalThreadPool() { DEBUGTRACE_ENTER; Lck lck(_mtx); /// See if we can get it from the global ptr. If not, time to allocate it. diff --git a/src/lasp/dsp/lasp_thread.h b/src/lasp/dsp/lasp_thread.h index c28805e..396f1b2 100644 --- a/src/lasp/dsp/lasp_thread.h +++ b/src/lasp/dsp/lasp_thread.h @@ -7,30 +7,21 @@ * safely spawn threads also from other threads. Only wraps a submit() and * push_task for now. */ -class ThreadSafeThreadPool { +class GlobalThreadPool { /** * @brief Shared access to the thread pool. */ std::shared_ptr _pool; - /** - * @brief Global mutex, used to restrict pool access to a single thread at - * once. - */ - static std::mutex _mtx; - - using Lck = std::scoped_lock; - ThreadSafeThreadPool(const ThreadSafeThreadPool&) = delete; - ThreadSafeThreadPool & - operator=(const ThreadSafeThreadPool&) = delete; public: /** * @brief Instantiate handle to the thread pool. */ - ThreadSafeThreadPool(); + GlobalThreadPool(); + GlobalThreadPool(const GlobalThreadPool &) = default; + GlobalThreadPool &operator=(const GlobalThreadPool &) = default; - /** * @brief Wrapper around BS::thread_pool::submit(...) */ @@ -38,8 +29,6 @@ public: typename F, typename... A, typename R = std::invoke_result_t, std::decay_t...>> [[nodiscard]] std::future submit(F &&task, A &&...args) { - /// Lock access to pool - Lck lck(_mtx); return _pool->submit(task, args...); } @@ -47,9 +36,6 @@ public: * @brief Wrapper around BS::thread_pool::push_task(...) */ template void push_task(F &&task, A &&...args) { - /// Lock access to pool - Lck lck(_mtx); _pool->push_task(task, args...); } }; - diff --git a/src/lasp/dsp/lasp_threadedindatahandler.h b/src/lasp/dsp/lasp_threadedindatahandler.h index b769ad1..0d7f030 100644 --- a/src/lasp/dsp/lasp_threadedindatahandler.h +++ b/src/lasp/dsp/lasp_threadedindatahandler.h @@ -37,7 +37,7 @@ class ThreadedInDataHandlerBase { std::atomic _thread_running{false}; std::atomic _thread_can_safely_run{false}; - ThreadSafeThreadPool _pool; + GlobalThreadPool _pool; /** * @brief Function pointer that is called when new DaqData arrives. From 88624764e7bc534358fd87c9ad52f82b7174bdd1 Mon Sep 17 00:00:00 2001 From: "J.A. de Jong - Redu-Sone B.V., ASCEE V.O.F" Date: Wed, 14 Jun 2023 21:23:53 +0200 Subject: [PATCH 3/6] Rudymentary portaudio support. A stream callback is running. --- .gitmodules | 3 + CMakeLists.txt | 8 +- cmake/portaudio.cmake | 14 + src/lasp/device/CMakeLists.txt | 9 + src/lasp/device/lasp_daq.cpp | 9 + src/lasp/device/lasp_daqconfig.h | 12 +- src/lasp/device/lasp_deviceinfo.cpp | 6 + .../device/portaudio/lasp_portaudiodaq.cpp | 335 ++++++++++++++++++ src/lasp/device/portaudio/lasp_portaudiodaq.h | 35 ++ src/lasp/lasp_config.h.in | 1 + src/lasp/pybind11/lasp_deviceinfo.cpp | 1 + third_party/portaudio | 1 + 12 files changed, 429 insertions(+), 5 deletions(-) create mode 100644 cmake/portaudio.cmake create mode 100644 src/lasp/device/portaudio/lasp_portaudiodaq.cpp create mode 100644 src/lasp/device/portaudio/lasp_portaudiodaq.h create mode 160000 third_party/portaudio diff --git a/.gitmodules b/.gitmodules index ac8821d..6924d53 100644 --- a/.gitmodules +++ b/.gitmodules @@ -31,3 +31,6 @@ [submodule "third_party/uldaq"] path = third_party/uldaq url = https://github.com/asceenl/uldaq +[submodule "third_party/portaudio"] + path = third_party/portaudio + url = https://github.com/PortAudio/portaudio diff --git a/CMakeLists.txt b/CMakeLists.txt index a9e339e..b3f827a 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -6,7 +6,11 @@ set(CMAKE_CXX_STANDARD 17) set(CMAKE_CXX_STANDARD_REQUIRED) option(LASP_DOUBLE_PRECISION "Compile as double precision floating point" ON) -option(LASP_HAS_RTAUDIO "Compile with RtAudio Daq backend" ON) +option(LASP_HAS_RTAUDIO "Compile with RtAudio Daq backend" OFF) +option(LASP_HAS_PORTAUDIO "Compile with PortAudio Daq backend" ON) +if(LASP_HAS_PORTAUDIO AND LASP_HAS_RTAUDIO) + message(FATAL_ERROR "Either PortAudio or RtAudio can be selected as audio backend") +endif() option(LASP_HAS_ULDAQ "Compile with UlDaq backend" ON) option(LASP_BUILD_TUNED "Tune build for current machine (Experimental / untested)" OFF) option(LASP_WITH_OPENMP "Use OpenMP parallelization (Experimental: crashes SHOULD BE EXPECTED)" OFF) @@ -99,8 +103,10 @@ include_directories(/usr/lib/python3.10/site-packages/numpy/core/include) # ####################################### End of user-adjustable variables section include(OSSpecific) include(rtaudio) +include(portaudio) include(uldaq) # +add_definitions(-Dgsl_CONFIG_DEFAULTS_VERSION=1) add_subdirectory(src/lasp) if(LASP_BUILD_CPP_TESTS) add_subdirectory(test) diff --git a/cmake/portaudio.cmake b/cmake/portaudio.cmake new file mode 100644 index 0000000..053f950 --- /dev/null +++ b/cmake/portaudio.cmake @@ -0,0 +1,14 @@ +# ###################################### RtAudio +if(LASP_HAS_PORTAUDIO) + message("Building with Portaudio backend") + if(WIN32) + else() + set(PA_USE_ALSA TRUE CACHE BOOL "Build PortAudio with ALSA backend") + set(PA_USE_JACK TRUE CACHE BOOL "Build PortAudio with Jack backend") + # set(PA_ALSA_DYNAMIC FALSE CACHE BOOL "Build static library of ALSA") + set(PA_BUILD_SHARED_LIBS FALSE CACHE BOOL "Build static library") + endif() + add_subdirectory(third_party/portaudio) + include_directories(third_party/portaudio/include) + link_directories(third_party/portaudio) +endif() diff --git a/src/lasp/device/CMakeLists.txt b/src/lasp/device/CMakeLists.txt index 133c788..481fb24 100644 --- a/src/lasp/device/CMakeLists.txt +++ b/src/lasp/device/CMakeLists.txt @@ -1,5 +1,6 @@ # src/lasp/device/CMakeLists.txt include_directories(uldaq) +include_directories(portaudio) add_library(lasp_device_lib OBJECT lasp_daq.cpp @@ -13,6 +14,7 @@ add_library(lasp_device_lib OBJECT uldaq/lasp_uldaq_impl.cpp uldaq/lasp_uldaq_bufhandler.cpp uldaq/lasp_uldaq_common.cpp + portaudio/lasp_portaudiodaq.cpp ) # Callback requires certain arguments that are not used by code. This disables @@ -31,6 +33,13 @@ endif() if(LASP_HAS_RTAUDIO) target_link_libraries(lasp_device_lib rtaudio) endif() +if(LASP_HAS_PORTAUDIO) + target_link_libraries(lasp_device_lib portaudio) + if(WIN32) + else() + target_link_libraries(lasp_device_lib asound) + endif() +endif() target_link_libraries(lasp_device_lib lasp_dsp_lib) diff --git a/src/lasp/device/lasp_daq.cpp b/src/lasp/device/lasp_daq.cpp index 2c39627..24374a2 100644 --- a/src/lasp/device/lasp_daq.cpp +++ b/src/lasp/device/lasp_daq.cpp @@ -2,6 +2,7 @@ #include "debugtrace.hpp" #include "lasp_daqconfig.h" +#include "lasp_config.h" #include "lasp_daq.h" #if LASP_HAS_ULDAQ == 1 #include "lasp_uldaq.h" @@ -9,6 +10,9 @@ #if LASP_HAS_RTAUDIO == 1 #include "lasp_rtaudiodaq.h" #endif +#if LASP_HAS_PORTAUDIO == 1 +#include "lasp_portaudiodaq.h" +#endif using rte = std::runtime_error; Daq::~Daq() { DEBUGTRACE_ENTER; } @@ -27,6 +31,11 @@ std::unique_ptr Daq::createDaq(const DeviceInfo &devinfo, if (devinfo.api.apicode == LASP_RTAUDIO_APICODE) { return createRtAudioDevice(devinfo, config); } +#endif +#if LASP_HAS_PORTAUDIO == 1 + if (devinfo.api.apicode == LASP_PORTAUDIO_APICODE) { + return createPortAudioDevice(devinfo, config); + } #endif throw rte(string("Unable to match Device API: ") + devinfo.api.apiname); } diff --git a/src/lasp/device/lasp_daqconfig.h b/src/lasp/device/lasp_daqconfig.h index dfd9d2f..80eb5cb 100644 --- a/src/lasp/device/lasp_daqconfig.h +++ b/src/lasp/device/lasp_daqconfig.h @@ -137,15 +137,19 @@ const DaqApi uldaqapi("UlDaq", 0); #include "RtAudio.h" const us LASP_RTAUDIO_APICODE = 1; const DaqApi rtaudioAlsaApi("RtAudio Linux ALSA", 1, RtAudio::Api::LINUX_ALSA); -const DaqApi rtaudioPulseaudioApi("RtAudio Linux Pulseaudio", 1, +const DaqApi rtaudioPulseaudioApi("RtAudio Linux Pulseaudio", LASP_RTAUDIO_APICODE, RtAudio::Api::LINUX_PULSE); -const DaqApi rtaudioWasapiApi("RtAudio Windows Wasapi", 1, +const DaqApi rtaudioWasapiApi("RtAudio Windows Wasapi", LASP_RTAUDIO_APICODE, RtAudio::Api::WINDOWS_WASAPI); -const DaqApi rtaudioDsApi("RtAudio Windows DirectSound", 1, +const DaqApi rtaudioDsApi("RtAudio Windows DirectSound", LASP_RTAUDIO_APICODE, RtAudio::Api::WINDOWS_DS); -const DaqApi rtaudioAsioApi("RtAudio Windows ASIO", 1, +const DaqApi rtaudioAsioApi("RtAudio Windows ASIO", LASP_RTAUDIO_APICODE, RtAudio::Api::WINDOWS_ASIO); #endif +#if LASP_HAS_PORTAUDIO == 1 +const us LASP_PORTAUDIO_APICODE = 2; +const DaqApi portaudioApi("PortAudio Linux ALSA", LASP_PORTAUDIO_APICODE, 0); +#endif class DeviceInfo; diff --git a/src/lasp/device/lasp_deviceinfo.cpp b/src/lasp/device/lasp_deviceinfo.cpp index f90e34e..fd8189e 100644 --- a/src/lasp/device/lasp_deviceinfo.cpp +++ b/src/lasp/device/lasp_deviceinfo.cpp @@ -10,6 +10,9 @@ #if LASP_HAS_RTAUDIO == 1 #include "lasp_rtaudiodaq.h" #endif +#if LASP_HAS_PORTAUDIO == 1 +#include "lasp_portaudiodaq.h" +#endif DeviceInfoList DeviceInfo::getDeviceInfo() { @@ -21,6 +24,9 @@ DeviceInfoList DeviceInfo::getDeviceInfo() { #if LASP_HAS_RTAUDIO == 1 fillRtAudioDeviceInfo(devs); #endif +#if LASP_HAS_PORTAUDIO == 1 + fillPortAudioDeviceInfo(devs); +#endif return devs; } diff --git a/src/lasp/device/portaudio/lasp_portaudiodaq.cpp b/src/lasp/device/portaudio/lasp_portaudiodaq.cpp new file mode 100644 index 0000000..f85e971 --- /dev/null +++ b/src/lasp/device/portaudio/lasp_portaudiodaq.cpp @@ -0,0 +1,335 @@ +#define DEBUGTRACE_ENABLED +#include "debugtrace.hpp" +#include "lasp_config.h" + +#if LASP_HAS_PORTAUDIO == 1 +#include "lasp_portaudiodaq.h" +#include "portaudio.h" +#include +#include +#include + +using rte = std::runtime_error; +using std::cerr; +using std::endl; +using std::string; +using std::to_string; + +inline void throwIfError(PaError e) { + DEBUGTRACE_ENTER; + if (e != paNoError) { + throw rte(string("PortAudio backend error: ") + Pa_GetErrorText(e)); + } +} + +/** + * @brief Device info, plus PortAudio stuff + */ +class OurPaDeviceInfo : public DeviceInfo { +public: + /** + * @brief Store instance to PaDeviceInfo. + */ + PaDeviceInfo _paDevInfo; + + virtual std::unique_ptr clone() const override final { + return std::make_unique(*this); + } +}; + +void fillPortAudioDeviceInfo(DeviceInfoList &devinfolist) { + DEBUGTRACE_ENTER; + try { + + PaError err = Pa_Initialize(); + /// PortAudio says that Pa_Terminate() should not be called whenever there + /// is an error in Pa_Initialize(). This is opposite to what most examples + /// of PortAudio show. + throwIfError(err); + + auto fin = gsl::finally([&err] { + DEBUGTRACE_PRINT("Terminating PortAudio instance"); + err = Pa_Terminate(); + if (err != paNoError) { + cerr << "Error terminating PortAudio. Do not know what to do." << endl; + } + }); + + /* const PaDeviceInfo *deviceInfo; */ + const int numDevices = Pa_GetDeviceCount(); + if (numDevices < 0) { + throw rte("PortAudio could not find any devices"); + } + for (us i = 0; i < (us)numDevices; i++) { + /* DEBUGTRACE_PRINT(i); */ + + const PaDeviceInfo *deviceInfo = Pa_GetDeviceInfo(i); + if (!deviceInfo) { + throw rte("No device info struct returned"); + } + OurPaDeviceInfo d; + d._paDevInfo = *deviceInfo; + d.api = portaudioApi; + d.device_name = deviceInfo->name; + + d.availableDataTypes = {DataTypeDescriptor::DataType::dtype_int16, + DataTypeDescriptor::DataType::dtype_int32, + DataTypeDescriptor::DataType::dtype_fl32}; + + d.prefDataTypeIndex = 2; + + d.availableSampleRates = {8000.0, 9600.0, 11025.0, 12000.0, 16000.0, + 22050.0, 24000.0, 32000.0, 44100.0, 48000.0, + 88200.0, 96000.0, 192000.0}; + d.prefSampleRateIndex = 9; + + d.availableFramesPerBlock = {512, 1024, 2048, 4096, 8192}; + d.prefFramesPerBlockIndex = 2; + + d.availableInputRanges = {1.0}; + + d.ninchannels = deviceInfo->maxInputChannels; + d.noutchannels = deviceInfo->maxOutputChannels; + + devinfolist.push_back(std::make_unique(d)); + } + } + + catch (rte &e) { + cerr << "PortAudio backend error: " << e.what() << std::endl; + return; + } +} + +/** + * @brief Forward declaration of raw callback. Calls into + * PortAudioDaq->memberPaCallback. Undocumented parameters are specified + * in memberPaCallback + * + * @param inputBuffer + * @param outputBuffer + * @param framesPerBuffer + * @param timeInfo + * @param statusFlags + * @param userData Pointer to PortAudioDaq* instance. + * + * @return + */ +static int rawPaCallback(const void *inputBuffer, void *outputBuffer, + unsigned long framesPerBuffer, + const PaStreamCallbackTimeInfo *timeInfo, + PaStreamCallbackFlags statusFlags, void *userData); + +class PortAudioDaq : public Daq { + bool _shouldPaTerminate = false; + PaStream *_stream = nullptr; + +public: + PortAudioDaq(const OurPaDeviceInfo &devinfo_gen, + const DaqConfiguration &config); + + void start(InDaqCallback inCallback, + OutDaqCallback outCallback) override final; + void stop() override final; + + StreamStatus getStreamStatus() const override final; + + /** + * @brief Member va + * + * @param inputBuffer + * @param outputBuffer + * @param framesPerBuffer + * @param timeInfo + * @param statusFlags + * + * @return + */ + int memberPaCallback(const void *inputBuffer, void *outputBuffer, + unsigned long framesPerBuffer, + const PaStreamCallbackTimeInfo *timeInfo, + PaStreamCallbackFlags statusFlags); + ~PortAudioDaq(); +}; + +std::unique_ptr createPortAudioDevice(const DeviceInfo &devinfo, + const DaqConfiguration &config) { + + const OurPaDeviceInfo *_info = + dynamic_cast(&devinfo); + if (_info == nullptr) { + throw rte("BUG: Could not cast DeviceInfo to OurPaDeviceInfo"); + } + return std::make_unique(*_info, config); +} + +static int rawPaCallback(const void *inputBuffer, void *outputBuffer, + unsigned long framesPerBuffer, + const PaStreamCallbackTimeInfo *timeInfo, + PaStreamCallbackFlags statusFlags, void *userData) { + return static_cast(userData)->memberPaCallback( + inputBuffer, outputBuffer, framesPerBuffer, timeInfo, statusFlags); +} + +PortAudioDaq::PortAudioDaq(const OurPaDeviceInfo &devinfo_gen, + const DaqConfiguration &config) + : Daq(devinfo_gen, config) { + + DEBUGTRACE_ENTER; + PaError err = Pa_Initialize(); + /// PortAudio says that Pa_Terminate() should not be called whenever there + /// is an error in Pa_Initialize(). This is opposite to what most examples + /// of PortAudio show. + throwIfError(err); + + // OK, Pa_Initialize successfully finished, it means we have to clean up with + // Pa_Terminate in the destructor. + _shouldPaTerminate = true; + + // Going to find the device in the list. If its there, we have to retrieve + // the index, as this is required in the PaStreamParameters struct + int devindex = -1; + for (int i = 0; i < Pa_GetDeviceCount(); i++) { + bool ok = true; + const PaDeviceInfo *info = Pa_GetDeviceInfo(i); + if (!info) { + throw rte("No device structure returned from PortAudio"); + } + ok &= string(info->name) == devinfo_gen._paDevInfo.name; + ok &= info->hostApi == devinfo_gen._paDevInfo.hostApi; + ok &= info->maxInputChannels == devinfo_gen._paDevInfo.maxInputChannels; + ok &= info->maxOutputChannels == devinfo_gen._paDevInfo.maxOutputChannels; + ok &= info->defaultSampleRate == devinfo_gen._paDevInfo.defaultSampleRate; + + if (ok) { + devindex = i; + } + } + if (devindex < 0) { + throw rte(string("Device not found: ") + string(devinfo_gen.device_name)); + } + + using Dtype = DataTypeDescriptor::DataType; + const Dtype dtype = dataType(); + PaSampleFormat format; + switch (dtype) { + case Dtype::dtype_fl32: + DEBUGTRACE_PRINT("Datatype float32"); + format = paFloat32; + break; + case Dtype::dtype_fl64: + DEBUGTRACE_PRINT("Datatype float64"); + throw rte("Invalid data type specified for DAQ stream."); + break; + case Dtype::dtype_int8: + DEBUGTRACE_PRINT("Datatype int8"); + format = paInt8; + break; + case Dtype::dtype_int16: + DEBUGTRACE_PRINT("Datatype int16"); + format = paInt16; + break; + case Dtype::dtype_int32: + DEBUGTRACE_PRINT("Datatype int32"); + format = paInt32; + break; + default: + throw rte("Invalid data type specified for DAQ stream."); + break; + } + + std::unique_ptr instreamParams; + std::unique_ptr outstreamParams; + + if (neninchannels() > 0) { + instreamParams = std::make_unique( + PaStreamParameters({.device = devindex, + .channelCount = (int)neninchannels(), + .sampleFormat = format, + .suggestedLatency = framesPerBlock() / samplerate(), + .hostApiSpecificStreamInfo = nullptr})); + } + if (nenoutchannels() > 0) { + instreamParams = std::make_unique( + PaStreamParameters({.device = devindex, + .channelCount = (int)nenoutchannels(), + .sampleFormat = format, + .suggestedLatency = framesPerBlock() / samplerate(), + .hostApiSpecificStreamInfo = nullptr})); + } + + // Next step: check whether we are OK + err = Pa_IsFormatSupported(instreamParams.get(), outstreamParams.get(), + samplerate()); + throwIfError(err); + + err = Pa_OpenStream(&_stream, // stream + instreamParams.get(), // inputParameters + outstreamParams.get(), // outputParameters + samplerate(), // yeah, + framesPerBlock(), // framesPerBuffer + paNoFlag, // streamFlags + rawPaCallback, this); + throwIfError(err); +} + +void PortAudioDaq::start(InDaqCallback inCallback, OutDaqCallback outCallback) { + DEBUGTRACE_ENTER; + assert(_stream); + if (Pa_IsStreamActive(_stream)) { + throw rte("Stream is already running"); + } + PaError err = Pa_StartStream(_stream); + throwIfError(err); +} +void PortAudioDaq::stop() { + DEBUGTRACE_ENTER; + assert(_stream); + if (Pa_IsStreamStopped(_stream)) { + throw rte("Stream is already stopped"); + } + PaError err = Pa_StopStream(_stream); + throwIfError(err); +} +Daq::StreamStatus PortAudioDaq::getStreamStatus() const { + Daq::StreamStatus status; + if (_stream) { + if (Pa_IsStreamActive(_stream)) { + status.isRunning = true; + } + } + return status; +} + +PortAudioDaq::~PortAudioDaq() { + PaError err; + if (_stream) { + if (Pa_IsStreamActive(_stream)) { + stop(); + } + + err = Pa_CloseStream(_stream); + _stream = nullptr; + if (err != paNoError) { + cerr << "Error closing PortAudio stream. Do not know what to do." << endl; + } + assert(_shouldPaTerminate); + } + + if (_shouldPaTerminate) { + err = Pa_Terminate(); + if (err != paNoError) { + cerr << "Error terminating PortAudio. Do not know what to do." << endl; + } + } +} +int PortAudioDaq::memberPaCallback(const void *inputBuffer, void *outputBuffer, + unsigned long framesPerBuffer, + const PaStreamCallbackTimeInfo *timeInfo, + PaStreamCallbackFlags statusFlags) { + + DEBUGTRACE_ENTER; + + return paContinue; +} +#endif diff --git a/src/lasp/device/portaudio/lasp_portaudiodaq.h b/src/lasp/device/portaudio/lasp_portaudiodaq.h new file mode 100644 index 0000000..9aeff92 --- /dev/null +++ b/src/lasp/device/portaudio/lasp_portaudiodaq.h @@ -0,0 +1,35 @@ +#pragma once +#include "lasp_daq.h" +#include + +/** \addtogroup device + * @{ + * \defgroup portaudio PortAudio backend + * This code is used to interface with the PortAudio cross-platform audio + * interface. + * + * \addtogroup portaudio + * @{ + */ + + +/** + * @brief Method called from Daq::createDaq. + * + * @param devinfo Device info + * @param config DAQ Configuration settings + * + * @return Pointer to Daq instance. Throws Runtime errors on error. + */ +std::unique_ptr createPortAudioDevice(const DeviceInfo& devinfo, + const DaqConfiguration& config); + +/** + * @brief Append PortAudio backend devices to the list + * + * @param devinfolist List to append to + */ +void fillPortAudioDeviceInfo(DeviceInfoList &devinfolist); + +/** @} */ +/** @} */ diff --git a/src/lasp/lasp_config.h.in b/src/lasp/lasp_config.h.in index 5a0f28e..c71b8e9 100644 --- a/src/lasp/lasp_config.h.in +++ b/src/lasp/lasp_config.h.in @@ -31,6 +31,7 @@ const int LASP_VERSION_MINOR = @CMAKE_PROJECT_VERSION_MINOR@; #cmakedefine LASP_FFT_BACKEND @LASP_FFT_BACKEND@ #cmakedefine01 LASP_HAS_RTAUDIO +#cmakedefine01 LASP_HAS_PORTAUDIO #cmakedefine01 LASP_HAS_ULDAQ #cmakedefine01 LASP_DOUBLE_PRECISION #cmakedefine01 LASP_USE_BLAS diff --git a/src/lasp/pybind11/lasp_deviceinfo.cpp b/src/lasp/pybind11/lasp_deviceinfo.cpp index ad3b9de..6034fcc 100644 --- a/src/lasp/pybind11/lasp_deviceinfo.cpp +++ b/src/lasp/pybind11/lasp_deviceinfo.cpp @@ -1,5 +1,6 @@ #include #include +#include #include "lasp_deviceinfo.h" using std::cerr; diff --git a/third_party/portaudio b/third_party/portaudio new file mode 160000 index 0000000..cb8d3dc --- /dev/null +++ b/third_party/portaudio @@ -0,0 +1 @@ +Subproject commit cb8d3dcbc6fa74c67f3e236be89b12d5630da141 From 77b1848bb4b9e3553a5ae23f4dd4385273cc1b8e Mon Sep 17 00:00:00 2001 From: "J.A. de Jong - Redu-Sone B.V., ASCEE V.O.F" Date: Wed, 14 Jun 2023 21:24:29 +0200 Subject: [PATCH 4/6] Made functions without correct return type compile time error --- CMakeLists.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index b3f827a..ae28946 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -94,7 +94,7 @@ endif() # ###################################### Compilation flags set(CMAKE_C_FLAGS_RELEASE "-O3 -flto -mfpmath=sse -march=x86-64 -mtune=native \ -fdata-sections -ffunction-sections -fomit-frame-pointer -finline-functions") -set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -Wall -Wextra -Wno-type-limits") +set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -Wall -Wextra -Wno-type-limits -Werror=return-type") # ############################# End compilation flags From 64a268e277cbe39119ef3d44c6c157244f7338aa Mon Sep 17 00:00:00 2001 From: "J.A. de Jong - Redu-Sone B.V., ASCEE V.O.F" Date: Thu, 15 Jun 2023 09:48:45 +0200 Subject: [PATCH 5/6] Portaudio backend seems to be working. No extensive checks performed yet. --- src/lasp/device/lasp_rtaudiodaq.cpp | 6 +- .../device/portaudio/lasp_portaudiodaq.cpp | 124 +++++++++++++++++- 2 files changed, 120 insertions(+), 10 deletions(-) diff --git a/src/lasp/device/lasp_rtaudiodaq.cpp b/src/lasp/device/lasp_rtaudiodaq.cpp index b10d54c..467fa63 100644 --- a/src/lasp/device/lasp_rtaudiodaq.cpp +++ b/src/lasp/device/lasp_rtaudiodaq.cpp @@ -337,9 +337,9 @@ public: const auto &dtype_descr = dtypeDescr(); const auto dtype = dataType(); - us neninchannels = this->neninchannels(); - us nenoutchannels = this->nenoutchannels(); - us sw = dtype_descr.sw; + const us neninchannels = this->neninchannels(); + const us nenoutchannels = this->nenoutchannels(); + const us sw = dtype_descr.sw; if (nFrames != nFramesPerBlock) { cerr << "RtAudio backend error: nFrames does not match block size!" << endl; diff --git a/src/lasp/device/portaudio/lasp_portaudiodaq.cpp b/src/lasp/device/portaudio/lasp_portaudiodaq.cpp index f85e971..4715828 100644 --- a/src/lasp/device/portaudio/lasp_portaudiodaq.cpp +++ b/src/lasp/device/portaudio/lasp_portaudiodaq.cpp @@ -1,4 +1,4 @@ -#define DEBUGTRACE_ENABLED +/* #define DEBUGTRACE_ENABLED */ #include "debugtrace.hpp" #include "lasp_config.h" @@ -123,6 +123,10 @@ static int rawPaCallback(const void *inputBuffer, void *outputBuffer, class PortAudioDaq : public Daq { bool _shouldPaTerminate = false; PaStream *_stream = nullptr; + std::atomic _streamError = + StreamStatus::StreamError::noError; + InDaqCallback _incallback; + OutDaqCallback _outcallback; public: PortAudioDaq(const OurPaDeviceInfo &devinfo_gen, @@ -211,11 +215,12 @@ PortAudioDaq::PortAudioDaq(const OurPaDeviceInfo &devinfo_gen, using Dtype = DataTypeDescriptor::DataType; const Dtype dtype = dataType(); - PaSampleFormat format; + // Sample format is bit flag + PaSampleFormat format = paNonInterleaved; switch (dtype) { case Dtype::dtype_fl32: DEBUGTRACE_PRINT("Datatype float32"); - format = paFloat32; + format |= paFloat32; break; case Dtype::dtype_fl64: DEBUGTRACE_PRINT("Datatype float64"); @@ -223,15 +228,15 @@ PortAudioDaq::PortAudioDaq(const OurPaDeviceInfo &devinfo_gen, break; case Dtype::dtype_int8: DEBUGTRACE_PRINT("Datatype int8"); - format = paInt8; + format |= paInt8; break; case Dtype::dtype_int16: DEBUGTRACE_PRINT("Datatype int16"); - format = paInt16; + format |= paInt16; break; case Dtype::dtype_int32: DEBUGTRACE_PRINT("Datatype int32"); - format = paInt32; + format |= paInt32; break; default: throw rte("Invalid data type specified for DAQ stream."); @@ -250,7 +255,7 @@ PortAudioDaq::PortAudioDaq(const OurPaDeviceInfo &devinfo_gen, .hostApiSpecificStreamInfo = nullptr})); } if (nenoutchannels() > 0) { - instreamParams = std::make_unique( + outstreamParams = std::make_unique( PaStreamParameters({.device = devindex, .channelCount = (int)nenoutchannels(), .sampleFormat = format, @@ -279,6 +284,30 @@ void PortAudioDaq::start(InDaqCallback inCallback, OutDaqCallback outCallback) { if (Pa_IsStreamActive(_stream)) { throw rte("Stream is already running"); } + + // Logical XOR + if (inCallback && outCallback) { + throw rte("Either input or output stream possible for RtAudio. " + "Stream duplex mode not provided."); + } + + if (neninchannels() > 0) { + if (!inCallback) { + throw rte( + + "Input callback given, but stream does not provide input data"); + } + + _incallback = inCallback; + } + if (nenoutchannels() > 0) { + if (!outCallback) { + throw rte( + "Output callback given, but stream does not provide output data"); + } + _outcallback = outCallback; + } + PaError err = Pa_StartStream(_stream); throwIfError(err); } @@ -293,6 +322,9 @@ void PortAudioDaq::stop() { } Daq::StreamStatus PortAudioDaq::getStreamStatus() const { Daq::StreamStatus status; + // Copy over atomic flag. + status.errorType = _streamError; + // Check if stream is still running. if (_stream) { if (Pa_IsStreamActive(_stream)) { status.isRunning = true; @@ -329,6 +361,84 @@ int PortAudioDaq::memberPaCallback(const void *inputBuffer, void *outputBuffer, PaStreamCallbackFlags statusFlags) { DEBUGTRACE_ENTER; + typedef Daq::StreamStatus::StreamError se; + if (statusFlags & paPrimingOutput) { + // Initial output buffers generated. So nothing with input yet + return paContinue; + } + if ((statusFlags & paInputUnderflow) || (statusFlags & paInputOverflow)) { + _streamError = se::inputXRun; + return paAbort; + } + if ((statusFlags & paOutputUnderflow) || (statusFlags & paOutputOverflow)) { + _streamError = se::outputXRun; + return paAbort; + } + if (framesPerBuffer != framesPerBlock()) { + cerr << "Logic error: expected a block size of: " << framesPerBlock() + << endl; + _streamError = se::logicError; + return paAbort; + } + + const us neninchannels = this->neninchannels(); + const us nenoutchannels = this->nenoutchannels(); + const auto &dtype_descr = dtypeDescr(); + const auto dtype = dataType(); + const us sw = dtype_descr.sw; + if (inputBuffer) { + assert(_incallback); + std::vector ptrs; + ptrs.reserve(neninchannels); + + const us ch_min = getLowestEnabledInChannel(); + const us ch_max = getHighestEnabledInChannel(); + assert(ch_min < ninchannels); + assert(ch_max < ninchannels); + + /// Only pass on the pointers of the channels we want. inputBuffer is + /// noninterleaved, as specified in PortAudioDaq constructor. + for (us ch = ch_min; ch <= ch_max; ch++) { + if (inchannel_config.at(ch).enabled) { + byte_t *ch_ptr = + reinterpret_cast(const_cast(inputBuffer))[ch]; + ptrs.push_back(ch_ptr); + } + } + DaqData d{framesPerBuffer, neninchannels, dtype}; + d.copyInFromRaw(ptrs); + + _incallback(d); + } + + if (outputBuffer) { + assert(_outcallback); + std::vector ptrs; + ptrs.reserve(nenoutchannels); + + /* outCallback */ + + const us ch_min = getLowestEnabledOutChannel(); + const us ch_max = getHighestEnabledOutChannel(); + assert(ch_min < noutchannels); + assert(ch_max < noutchannels); + /// Only pass on the pointers of the channels we want + for (us ch = ch_min; ch <= ch_max; ch++) { + if (outchannel_config.at(ch).enabled) { + byte_t *ch_ptr = reinterpret_cast(outputBuffer)[ch]; + ptrs.push_back(ch_ptr); + } + } + DaqData d{framesPerBuffer, nenoutchannels, dtype}; + + _outcallback(d); + // Copy over the buffer + us j = 0; + for (auto ptr : ptrs) { + d.copyToRaw(j, ptr); + j++; + } + } return paContinue; } From d9a3cfd627f91230e5348d79fc7d11149ecdbe9c Mon Sep 17 00:00:00 2001 From: "J.A. de Jong - Redu-Sone B.V., ASCEE V.O.F" Date: Thu, 15 Jun 2023 09:57:06 +0200 Subject: [PATCH 6/6] Made default preset for compiled in DAQ backends in CMakeLists --- CMakeLists.txt | 20 +++++++++++++++++--- 1 file changed, 17 insertions(+), 3 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index ae28946..382904d 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -6,12 +6,26 @@ set(CMAKE_CXX_STANDARD 17) set(CMAKE_CXX_STANDARD_REQUIRED) option(LASP_DOUBLE_PRECISION "Compile as double precision floating point" ON) -option(LASP_HAS_RTAUDIO "Compile with RtAudio Daq backend" OFF) -option(LASP_HAS_PORTAUDIO "Compile with PortAudio Daq backend" ON) + +# Setting defaults for PortAudio and RtAudio backend, depending on Linux / +# Windows. +if(WIN32) + set(DEFAULT_RTAUDIO OFF) + set(DEFAULT_PORTAUDIO ON) + set(DEFAULT_ULDAQ OFF) +else() + set(DEFAULT_RTAUDIO ON) + set(DEFAULT_PORTAUDIO OFF) + set(DEFAULT_ULDAQ ON) +endif() + + +option(LASP_HAS_RTAUDIO "Compile with RtAudio Daq backend" ${DEFAULT_RTAUDIO}) +option(LASP_HAS_PORTAUDIO "Compile with PortAudio Daq backend" ${DEFAULT_PORTAUDIO}) if(LASP_HAS_PORTAUDIO AND LASP_HAS_RTAUDIO) message(FATAL_ERROR "Either PortAudio or RtAudio can be selected as audio backend") endif() -option(LASP_HAS_ULDAQ "Compile with UlDaq backend" ON) +option(LASP_HAS_ULDAQ "Compile with UlDaq backend" ${DEFAULT_ULDAQ}) option(LASP_BUILD_TUNED "Tune build for current machine (Experimental / untested)" OFF) option(LASP_WITH_OPENMP "Use OpenMP parallelization (Experimental: crashes SHOULD BE EXPECTED)" OFF) set(LASP_MAX_NFFT "33554432" CACHE STRING "Max FFT size")