532 lines
16 KiB
C++
532 lines
16 KiB
C++
// #define DEBUGTRACE_ENABLED
|
|
#include "debugtrace.hpp"
|
|
#include "lasp_config.h"
|
|
|
|
#if LASP_HAS_PORTAUDIO == 1
|
|
#include <gsl-lite/gsl-lite.hpp>
|
|
#include <mutex>
|
|
#include <string>
|
|
|
|
#include "lasp_portaudiodaq.h"
|
|
#include "portaudio.h"
|
|
|
|
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);
|
|
}
|
|
OurPaDeviceInfo &operator=(const OurPaDeviceInfo &) = delete;
|
|
OurPaDeviceInfo(const OurPaDeviceInfo &) = default;
|
|
OurPaDeviceInfo(const PaDeviceInfo &o) : DeviceInfo(), _paDevInfo(o) {}
|
|
};
|
|
|
|
void fillPortAudioDeviceInfo(DeviceInfoList &devinfolist) {
|
|
DEBUGTRACE_ENTER;
|
|
bool shouldPaTerminate = false;
|
|
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);
|
|
shouldPaTerminate = true;
|
|
|
|
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 PaHostApiIndex apicount = Pa_GetHostApiCount();
|
|
if (apicount < 0) {
|
|
return;
|
|
}
|
|
/* 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); */
|
|
bool hasDuplexMode = false;
|
|
|
|
const PaDeviceInfo *deviceInfo = Pa_GetDeviceInfo(i);
|
|
if (!deviceInfo) {
|
|
throw rte("No device info struct returned");
|
|
}
|
|
OurPaDeviceInfo d(*deviceInfo);
|
|
// We store the name in d.device_name
|
|
d._paDevInfo.name = nullptr;
|
|
d.device_name = deviceInfo->name;
|
|
|
|
const PaHostApiInfo *hostapiinfo = Pa_GetHostApiInfo(deviceInfo->hostApi);
|
|
if (hostapiinfo == nullptr) {
|
|
throw std::runtime_error("Hostapi nullptr!");
|
|
}
|
|
switch (hostapiinfo->type) {
|
|
case paALSA:
|
|
// Duplex mode for alsa
|
|
hasDuplexMode = true;
|
|
d.api = portaudioALSAApi;
|
|
break;
|
|
case paASIO:
|
|
d.api = portaudioASIOApi;
|
|
break;
|
|
case paDirectSound:
|
|
d.api = portaudioDirectSoundApi;
|
|
break;
|
|
case paMME:
|
|
d.api = portaudioWMMEApi;
|
|
break;
|
|
case paWDMKS:
|
|
d.api = portaudioWDMKS;
|
|
break;
|
|
case paWASAPI:
|
|
d.api = portaudioWASAPIApi;
|
|
break;
|
|
case paPulseAudio:
|
|
d.api = portaudioPulseApi;
|
|
break;
|
|
default:
|
|
throw rte("Unimplemented portaudio API!");
|
|
break;
|
|
}
|
|
|
|
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.prefInputRangeIndex = 0; // Constructor-defined
|
|
d.availableOutputRanges = {1.0};
|
|
// d.prefOutputRangeIndex = 0; // Constructor-defined
|
|
|
|
d.ninchannels = deviceInfo->maxInputChannels;
|
|
d.noutchannels = deviceInfo->maxOutputChannels;
|
|
|
|
// Duplex mode, only for ALSA devices
|
|
d.hasDuplexMode = hasDuplexMode;
|
|
|
|
devinfolist.push_back(std::make_unique<OurPaDeviceInfo>(d));
|
|
}
|
|
}
|
|
|
|
catch (rte &e) {
|
|
if (shouldPaTerminate) {
|
|
PaError err = Pa_Terminate();
|
|
if (err != paNoError) {
|
|
cerr << "Error terminating PortAudio. Do not know what to do." << endl;
|
|
}
|
|
}
|
|
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 {
|
|
PaStream *_stream = nullptr;
|
|
std::atomic<StreamStatus::StreamError> _streamError =
|
|
StreamStatus::StreamError::noError;
|
|
InDaqCallback _incallback;
|
|
OutDaqCallback _outcallback;
|
|
|
|
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) {
|
|
DEBUGTRACE_ENTER;
|
|
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;
|
|
bool shouldPaTerminate = false;
|
|
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);
|
|
|
|
// 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++) {
|
|
// DEBUGTRACE_PRINT(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.device_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();
|
|
// Sample format is bit flag
|
|
PaSampleFormat format = paNonInterleaved;
|
|
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)getHighestEnabledInChannel() + 1,
|
|
.sampleFormat = format,
|
|
.suggestedLatency = framesPerBlock() / samplerate(),
|
|
.hostApiSpecificStreamInfo = nullptr}));
|
|
}
|
|
if (nenoutchannels() > 0) {
|
|
outstreamParams = std::make_unique<PaStreamParameters>(PaStreamParameters(
|
|
{.device = devindex,
|
|
.channelCount = (int)getHighestEnabledOutChannel() + 1,
|
|
.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);
|
|
assert(_stream);
|
|
} catch (rte &e) {
|
|
if (shouldPaTerminate) {
|
|
PaError err = Pa_Terminate();
|
|
if (err != paNoError) {
|
|
cerr << "Error terminating PortAudio. Do not know what to do." << endl;
|
|
}
|
|
}
|
|
throw;
|
|
}
|
|
}
|
|
|
|
void PortAudioDaq::start(InDaqCallback inCallback, OutDaqCallback outCallback) {
|
|
DEBUGTRACE_ENTER;
|
|
assert(_stream);
|
|
|
|
if (Pa_IsStreamActive(_stream)) {
|
|
throw rte("Stream is already running");
|
|
}
|
|
|
|
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);
|
|
}
|
|
void PortAudioDaq::stop() {
|
|
DEBUGTRACE_ENTER;
|
|
assert(_stream);
|
|
if (Pa_IsStreamStopped(_stream) > 1) {
|
|
throw rte("Stream is already stopped");
|
|
}
|
|
PaError err = Pa_StopStream(_stream);
|
|
throwIfError(err);
|
|
}
|
|
Daq::StreamStatus PortAudioDaq::getStreamStatus() const {
|
|
DEBUGTRACE_ENTER;
|
|
// Stores an error type and whether the
|
|
Daq::StreamStatus status;
|
|
using StreamError = Daq::StreamStatus::StreamError;
|
|
Daq::StreamStatus::StreamError errortype = _streamError.load();
|
|
|
|
PaError err = Pa_IsStreamStopped(_stream);
|
|
if (err > 1) {
|
|
// Stream is stopped due to an error in the callback. The exact error type
|
|
// is filled in in the if-statement above
|
|
return status;
|
|
} else if (err == 0) {
|
|
// Still running
|
|
status.isRunning = true;
|
|
} else if (err < 0) {
|
|
// Stream encountered an error.
|
|
switch (err) {
|
|
case paInternalError:
|
|
errortype = StreamError::driverError;
|
|
break;
|
|
case paDeviceUnavailable:
|
|
errortype = StreamError::driverError;
|
|
break;
|
|
case paInputOverflowed:
|
|
errortype = StreamError::inputXRun;
|
|
break;
|
|
case paOutputUnderflowed:
|
|
errortype = StreamError::outputXRun;
|
|
break;
|
|
default:
|
|
errortype = StreamError::driverError;
|
|
cerr << "Portaudio backend error:" << Pa_GetErrorText(err) << endl;
|
|
break;
|
|
}
|
|
}
|
|
status.errorType = errortype;
|
|
return status;
|
|
}
|
|
|
|
PortAudioDaq::~PortAudioDaq() {
|
|
DEBUGTRACE_ENTER;
|
|
PaError err;
|
|
assert(_stream);
|
|
if (Pa_IsStreamActive(_stream)) {
|
|
// Stop the stream first
|
|
stop();
|
|
}
|
|
|
|
err = Pa_CloseStream(_stream);
|
|
_stream = nullptr;
|
|
if (err != paNoError) {
|
|
cerr << "Error closing PortAudio stream. Do not know what to do." << endl;
|
|
}
|
|
|
|
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;
|
|
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<byte_t *> 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<byte_t **>(const_cast<void *>(inputBuffer))[ch];
|
|
ptrs.push_back(ch_ptr);
|
|
}
|
|
}
|
|
DaqData d{framesPerBuffer, neninchannels, dtype};
|
|
d.copyInFromRaw(ptrs);
|
|
|
|
_incallback(d);
|
|
}
|
|
|
|
if (outputBuffer) {
|
|
assert(_outcallback);
|
|
std::vector<byte_t *> 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<byte_t **>(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;
|
|
}
|
|
#endif
|