// #define DEBUGTRACE_ENABLED #include "debugtrace.hpp" #include "lasp_config.h" #if LASP_HAS_PORTAUDIO == 1 #include #include #include #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 clone() const override final { return std::make_unique(*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: hasDuplexMode = true; 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(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 _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) { DEBUGTRACE_ENTER; 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; 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 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); 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 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