Rudymentary portaudio support. A stream callback is running.

This commit is contained in:
Anne de Jong 2023-06-14 21:23:53 +02:00
parent 303e15e2d6
commit 88624764e7
12 changed files with 429 additions and 5 deletions

3
.gitmodules vendored
View File

@ -31,3 +31,6 @@
[submodule "third_party/uldaq"] [submodule "third_party/uldaq"]
path = third_party/uldaq path = third_party/uldaq
url = https://github.com/asceenl/uldaq url = https://github.com/asceenl/uldaq
[submodule "third_party/portaudio"]
path = third_party/portaudio
url = https://github.com/PortAudio/portaudio

View File

@ -6,7 +6,11 @@ set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED) set(CMAKE_CXX_STANDARD_REQUIRED)
option(LASP_DOUBLE_PRECISION "Compile as double precision floating point" ON) 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_HAS_ULDAQ "Compile with UlDaq backend" ON)
option(LASP_BUILD_TUNED "Tune build for current machine (Experimental / untested)" OFF) 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) 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 # ####################################### End of user-adjustable variables section
include(OSSpecific) include(OSSpecific)
include(rtaudio) include(rtaudio)
include(portaudio)
include(uldaq) include(uldaq)
# #
add_definitions(-Dgsl_CONFIG_DEFAULTS_VERSION=1)
add_subdirectory(src/lasp) add_subdirectory(src/lasp)
if(LASP_BUILD_CPP_TESTS) if(LASP_BUILD_CPP_TESTS)
add_subdirectory(test) add_subdirectory(test)

14
cmake/portaudio.cmake Normal file
View File

@ -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()

View File

@ -1,5 +1,6 @@
# src/lasp/device/CMakeLists.txt # src/lasp/device/CMakeLists.txt
include_directories(uldaq) include_directories(uldaq)
include_directories(portaudio)
add_library(lasp_device_lib OBJECT add_library(lasp_device_lib OBJECT
lasp_daq.cpp lasp_daq.cpp
@ -13,6 +14,7 @@ add_library(lasp_device_lib OBJECT
uldaq/lasp_uldaq_impl.cpp uldaq/lasp_uldaq_impl.cpp
uldaq/lasp_uldaq_bufhandler.cpp uldaq/lasp_uldaq_bufhandler.cpp
uldaq/lasp_uldaq_common.cpp uldaq/lasp_uldaq_common.cpp
portaudio/lasp_portaudiodaq.cpp
) )
# Callback requires certain arguments that are not used by code. This disables # Callback requires certain arguments that are not used by code. This disables
@ -31,6 +33,13 @@ endif()
if(LASP_HAS_RTAUDIO) if(LASP_HAS_RTAUDIO)
target_link_libraries(lasp_device_lib rtaudio) target_link_libraries(lasp_device_lib rtaudio)
endif() 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) target_link_libraries(lasp_device_lib lasp_dsp_lib)

View File

@ -2,6 +2,7 @@
#include "debugtrace.hpp" #include "debugtrace.hpp"
#include "lasp_daqconfig.h" #include "lasp_daqconfig.h"
#include "lasp_config.h"
#include "lasp_daq.h" #include "lasp_daq.h"
#if LASP_HAS_ULDAQ == 1 #if LASP_HAS_ULDAQ == 1
#include "lasp_uldaq.h" #include "lasp_uldaq.h"
@ -9,6 +10,9 @@
#if LASP_HAS_RTAUDIO == 1 #if LASP_HAS_RTAUDIO == 1
#include "lasp_rtaudiodaq.h" #include "lasp_rtaudiodaq.h"
#endif #endif
#if LASP_HAS_PORTAUDIO == 1
#include "lasp_portaudiodaq.h"
#endif
using rte = std::runtime_error; using rte = std::runtime_error;
Daq::~Daq() { DEBUGTRACE_ENTER; } Daq::~Daq() { DEBUGTRACE_ENTER; }
@ -27,6 +31,11 @@ std::unique_ptr<Daq> Daq::createDaq(const DeviceInfo &devinfo,
if (devinfo.api.apicode == LASP_RTAUDIO_APICODE) { if (devinfo.api.apicode == LASP_RTAUDIO_APICODE) {
return createRtAudioDevice(devinfo, config); return createRtAudioDevice(devinfo, config);
} }
#endif
#if LASP_HAS_PORTAUDIO == 1
if (devinfo.api.apicode == LASP_PORTAUDIO_APICODE) {
return createPortAudioDevice(devinfo, config);
}
#endif #endif
throw rte(string("Unable to match Device API: ") + devinfo.api.apiname); throw rte(string("Unable to match Device API: ") + devinfo.api.apiname);
} }

View File

@ -137,15 +137,19 @@ const DaqApi uldaqapi("UlDaq", 0);
#include "RtAudio.h" #include "RtAudio.h"
const us LASP_RTAUDIO_APICODE = 1; const us LASP_RTAUDIO_APICODE = 1;
const DaqApi rtaudioAlsaApi("RtAudio Linux ALSA", 1, RtAudio::Api::LINUX_ALSA); 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); RtAudio::Api::LINUX_PULSE);
const DaqApi rtaudioWasapiApi("RtAudio Windows Wasapi", 1, const DaqApi rtaudioWasapiApi("RtAudio Windows Wasapi", LASP_RTAUDIO_APICODE,
RtAudio::Api::WINDOWS_WASAPI); RtAudio::Api::WINDOWS_WASAPI);
const DaqApi rtaudioDsApi("RtAudio Windows DirectSound", 1, const DaqApi rtaudioDsApi("RtAudio Windows DirectSound", LASP_RTAUDIO_APICODE,
RtAudio::Api::WINDOWS_DS); RtAudio::Api::WINDOWS_DS);
const DaqApi rtaudioAsioApi("RtAudio Windows ASIO", 1, const DaqApi rtaudioAsioApi("RtAudio Windows ASIO", LASP_RTAUDIO_APICODE,
RtAudio::Api::WINDOWS_ASIO); RtAudio::Api::WINDOWS_ASIO);
#endif #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; class DeviceInfo;

View File

@ -10,6 +10,9 @@
#if LASP_HAS_RTAUDIO == 1 #if LASP_HAS_RTAUDIO == 1
#include "lasp_rtaudiodaq.h" #include "lasp_rtaudiodaq.h"
#endif #endif
#if LASP_HAS_PORTAUDIO == 1
#include "lasp_portaudiodaq.h"
#endif
DeviceInfoList DeviceInfo::getDeviceInfo() { DeviceInfoList DeviceInfo::getDeviceInfo() {
@ -21,6 +24,9 @@ DeviceInfoList DeviceInfo::getDeviceInfo() {
#if LASP_HAS_RTAUDIO == 1 #if LASP_HAS_RTAUDIO == 1
fillRtAudioDeviceInfo(devs); fillRtAudioDeviceInfo(devs);
#endif #endif
#if LASP_HAS_PORTAUDIO == 1
fillPortAudioDeviceInfo(devs);
#endif
return devs; return devs;
} }

View File

@ -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 <gsl-lite/gsl-lite.hpp>
#include <mutex>
#include <string>
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<DeviceInfo> clone() const override final {
return std::make_unique<OurPaDeviceInfo>(*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<OurPaDeviceInfo>(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<Daq> createPortAudioDevice(const DeviceInfo &devinfo,
const DaqConfiguration &config) {
const OurPaDeviceInfo *_info =
dynamic_cast<const OurPaDeviceInfo *>(&devinfo);
if (_info == nullptr) {
throw rte("BUG: Could not cast DeviceInfo to OurPaDeviceInfo");
}
return std::make_unique<PortAudioDaq>(*_info, config);
}
static int rawPaCallback(const void *inputBuffer, void *outputBuffer,
unsigned long framesPerBuffer,
const PaStreamCallbackTimeInfo *timeInfo,
PaStreamCallbackFlags statusFlags, void *userData) {
return static_cast<PortAudioDaq *>(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<PaStreamParameters> instreamParams;
std::unique_ptr<PaStreamParameters> outstreamParams;
if (neninchannels() > 0) {
instreamParams = std::make_unique<PaStreamParameters>(
PaStreamParameters({.device = devindex,
.channelCount = (int)neninchannels(),
.sampleFormat = format,
.suggestedLatency = framesPerBlock() / samplerate(),
.hostApiSpecificStreamInfo = nullptr}));
}
if (nenoutchannels() > 0) {
instreamParams = std::make_unique<PaStreamParameters>(
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

View File

@ -0,0 +1,35 @@
#pragma once
#include "lasp_daq.h"
#include <memory>
/** \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<Daq> 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);
/** @} */
/** @} */

View File

@ -31,6 +31,7 @@ const int LASP_VERSION_MINOR = @CMAKE_PROJECT_VERSION_MINOR@;
#cmakedefine LASP_FFT_BACKEND @LASP_FFT_BACKEND@ #cmakedefine LASP_FFT_BACKEND @LASP_FFT_BACKEND@
#cmakedefine01 LASP_HAS_RTAUDIO #cmakedefine01 LASP_HAS_RTAUDIO
#cmakedefine01 LASP_HAS_PORTAUDIO
#cmakedefine01 LASP_HAS_ULDAQ #cmakedefine01 LASP_HAS_ULDAQ
#cmakedefine01 LASP_DOUBLE_PRECISION #cmakedefine01 LASP_DOUBLE_PRECISION
#cmakedefine01 LASP_USE_BLAS #cmakedefine01 LASP_USE_BLAS

View File

@ -1,5 +1,6 @@
#include <pybind11/stl.h> #include <pybind11/stl.h>
#include <stdint.h> #include <stdint.h>
#include <iostream>
#include "lasp_deviceinfo.h" #include "lasp_deviceinfo.h"
using std::cerr; using std::cerr;

1
third_party/portaudio vendored Submodule

@ -0,0 +1 @@
Subproject commit cb8d3dcbc6fa74c67f3e236be89b12d5630da141