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