/* #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; // We store the name in d.device_name d._paDevInfo.name = nullptr; 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.prefInputRangeIndex = 0; // Constructor-defined d.availableOutputRanges = {1.0}; // d.prefOutputRangeIndex = 0; // Constructor-defined 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; std::atomic _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 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++) { // 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 instreamParams; std::unique_ptr outstreamParams; if (neninchannels() > 0) { instreamParams = std::make_unique( PaStreamParameters({.device = devindex, .channelCount = (int)getHighestEnabledInChannel() + 1, .sampleFormat = format, .suggestedLatency = framesPerBlock() / samplerate(), .hostApiSpecificStreamInfo = nullptr})); } if (nenoutchannels() > 0) { outstreamParams = std::make_unique( 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); } void PortAudioDaq::start(InDaqCallback inCallback, OutDaqCallback outCallback) { DEBUGTRACE_ENTER; assert(_stream); 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); } 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; // Copy over atomic flag. status.errorType = _streamError; // Check if stream is still running. 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; 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; } #endif