2#include <ossia/detail/config.hpp>
4#if defined(OSSIA_ENABLE_PORTAUDIO)
5#if __has_include(<portaudio.h>)
6#include <ossia/audio/audio_engine.hpp>
8#include <ossia/detail/thread.hpp>
11#if defined(__linux__) && __has_include(<alsa/asoundlib.h>)
12#include <ossia/audio/libasound.hpp>
13#define OSSIA_PORTAUDIO_ALSA_PERIOD 1
24#define OSSIA_AUDIO_PORTAUDIO 1
28class portaudio_engine final :
public audio_engine
32 std::string name, std::string card_in, std::string card_out,
int inputs,
33 int outputs,
int rate,
int bs, PaHostApiTypeId hostApi)
35 if(
auto ec = Pa_Initialize(); ec != paNoError)
36 throw std::runtime_error(
37 std::string(
"PortAudio: cannot initialize: ") + Pa_GetErrorText(ec));
40 const int hostApiIndex
41 = hostApi == paInDevelopment ? -1 : Pa_HostApiTypeIdToHostApiIndex(hostApi);
42 const int card_in_idx = find_device(card_in, hostApiIndex,
true);
43 const int card_out_idx = find_device(card_out, hostApiIndex,
false);
45 auto devInInfo = Pa_GetDeviceInfo(card_in_idx);
46 inputs = devInInfo ? std::min(inputs, devInInfo->maxInputChannels) : 0;
47 auto devOutInfo = Pa_GetDeviceInfo(card_out_idx);
48 outputs = devOutInfo ? std::min(outputs, devOutInfo->maxOutputChannels) : 0;
50 if(inputs <= 0 && outputs <= 0)
53 throw std::runtime_error(
54 "PortAudio: device not found: "
55 + (card_out.empty() ? card_in : card_out));
58 auto latency = [&](
const PaDeviceInfo* dev,
bool input,
int channels) {
59 if(!dev || Pa_GetHostApiInfo(dev->hostApi)->type != paALSA)
67 int period = std::max(bs, alsa_min_period(dev->name, input, rate, channels));
68 period = (period + bs - 1) / bs * bs;
69 const int periods = period * 1000 < 2 * rate ? 4 : 3;
70 return double(periods - 1) * period / rate;
73 PaStreamParameters inputParameters;
74 inputParameters.device = card_in_idx;
75 inputParameters.channelCount = inputs;
76 inputParameters.sampleFormat = paFloat32 | paNonInterleaved;
77 inputParameters.suggestedLatency = latency(devInInfo,
true, inputs);
78 inputParameters.hostApiSpecificStreamInfo =
nullptr;
80 PaStreamParameters outputParameters;
81 outputParameters.device = card_out_idx;
82 outputParameters.channelCount = outputs;
83 outputParameters.sampleFormat = paFloat32 | paNonInterleaved;
84 outputParameters.suggestedLatency = latency(devOutInfo,
false, outputs);
85 outputParameters.hostApiSpecificStreamInfo =
nullptr;
87 PaStreamParameters* actualInput = inputs > 0 ? &inputParameters :
nullptr;
88 PaStreamParameters* actualOutput = outputs > 0 ? &outputParameters :
nullptr;
91 auto ec = Pa_OpenStream(
92 &stream, actualInput, actualOutput, rate, bs, paNoFlag, &PortAudioCallback,
96 auto err = error_text(
"cannot open", ec, card_in, card_out);
98 throw std::runtime_error(err);
102 auto info = Pa_GetStreamInfo(stream);
103 this->effective_sample_rate = info->sampleRate;
104 this->effective_buffer_size = bs;
105 this->effective_inputs = inputs;
106 this->effective_outputs = outputs;
107 this->effective_input_latency = int(std::lround(info->inputLatency * info->sampleRate));
108 this->effective_output_latency
109 = int(std::lround(info->outputLatency * info->sampleRate));
110 m_stall_timeout = std::max(
111 std::chrono::milliseconds(1000),
112 std::chrono::milliseconds(int64_t(20) * 1000 * bs / std::max(rate, 1)));
116 m_stream.store(stream);
118 if(ec = Pa_StartStream(stream); ec != paNoError)
120 auto err = error_text(
"cannot start", ec, card_in, card_out);
121 Pa_CloseStream(stream);
122 m_stream.store(
nullptr);
124 throw std::runtime_error(err);
129 bool running()
const override
131 auto s = m_stream.load();
132 return s && Pa_IsStreamActive(s) == 1;
139 bool failed() const noexcept
override
141 auto s = m_stream.load();
142 if(!s || stop_processing)
144 if(Pa_IsStreamActive(s) == 0)
146 const auto now = std::chrono::steady_clock::now();
147 const auto calls = m_callbacks.load(std::memory_order_relaxed);
148 if(calls != m_seenCallbacks || m_seenAt == std::chrono::steady_clock::time_point{})
150 m_seenCallbacks = calls;
154 return now - m_seenAt > m_stall_timeout;
159 audio_engine::stop();
165 if(
auto stream = m_stream.exchange(
nullptr))
167 if(
auto ec = Pa_AbortStream(stream); ec != paNoError)
168 ossia::logger().warn(
"PortAudio: stopping the stream: {}", Pa_GetErrorText(ec));
169 if(
auto ec = Pa_CloseStream(stream); ec != paNoError)
170 ossia::logger().warn(
"PortAudio: closing the stream: {}", Pa_GetErrorText(ec));
174 ~portaudio_engine()
override
183 static int alsa_min_period(std::string_view name,
bool input,
int rate,
int channels)
185#if OSSIA_PORTAUDIO_ALSA_PERIOD
187 auto pos = name.rfind(
" (hw:");
188 if(pos == std::string_view::npos || !name.ends_with(
')'))
190 const std::string hw{name.substr(pos + 2, name.size() - pos - 3)};
193 auto& snd = ossia::libasound::instance();
196 &pcm, hw.c_str(), input ? SND_PCM_STREAM_CAPTURE : SND_PCM_STREAM_PLAYBACK,
200 snd_pcm_hw_params_t* params{};
201 snd_alloca(¶ms, snd, pcm_hw_params);
202 snd_pcm_uframes_t
min{};
204 unsigned r = rate, c = std::max(channels, 1);
205 if(snd.pcm_hw_params_any(pcm, params) >= 0
206 && snd.pcm_hw_params_set_rate_near(pcm, params, &r,
nullptr) >= 0
207 && snd.pcm_hw_params_set_channels_near(pcm, params, &c) >= 0)
208 snd.pcm_hw_params_get_period_size_min(params, &min, &dir);
224 static int find_device(
const std::string& name,
int hostApiIndex,
bool input)
229 auto without_index = [](std::string_view n) {
230 for(
auto suffix : {
" (hw:",
" (plughw:"})
231 if(
auto pos = n.rfind(suffix); pos != std::string_view::npos && n.ends_with(
')'))
232 return n.substr(0, pos);
236 int fallback = paNoDevice;
237 for(
int i = 0; i < Pa_GetDeviceCount(); i++)
239 auto info = Pa_GetDeviceInfo(i);
240 if(hostApiIndex >= 0 && info->hostApi != hostApiIndex)
242 if((input ? info->maxInputChannels : info->maxOutputChannels) <= 0)
244 if(info->name == name)
246 if(fallback == paNoDevice && without_index(info->name) == without_index(name))
252 static std::string error_text(
253 const char* what, PaError ec,
const std::string& card_in,
254 const std::string& card_out)
256 std::string err = std::string(
"PortAudio: ") + what +
" ";
257 err += card_out.empty() ? card_in : card_out;
258 if(!card_in.empty() && !card_out.empty() && card_in != card_out)
259 err +=
" / " + card_in;
261 err += Pa_GetErrorText(ec);
262 if(ec == paUnanticipatedHostError)
263 if(
auto host = Pa_GetLastHostErrorInfo(); host && host->errorText)
264 err += std::string(
" (") + host->errorText +
")";
268 static int clearBuffers(
float** float_output,
unsigned long nframes,
int outs)
270 for(
int i = 0; i < outs; i++)
272 auto chan = float_output[i];
273 for(std::size_t j = 0; j < nframes; j++)
280 static int PortAudioCallback(
281 const void* input,
void* output,
unsigned long nframes,
282 const PaStreamCallbackTimeInfo* timeInfo, PaStreamCallbackFlags statusFlags,
286 static const thread_local auto _
288 ossia::set_thread_name(
"ossia audio 0");
289 ossia::set_thread_pinned(thread_type::Audio, 0);
290#if defined(__linux__)
296 auto& self = *
static_cast<portaudio_engine*
>(userData);
297 self.m_callbacks.fetch_add(1, std::memory_order_relaxed);
301 & (paInputUnderflow | paInputOverflow | paOutputUnderflow | paOutputOverflow))
303 if(timeInfo->currentTime == 0. || timeInfo->currentTime != self.m_lastXrunTime)
304 self.xruns.fetch_add(1, std::memory_order_relaxed);
305 self.m_lastXrunTime = timeInfo->currentTime;
309 auto clt = self.m_stream.load();
311 if(self.stop_processing || !clt)
314 return clearBuffers(((
float**)output), nframes, self.effective_outputs);
317 auto float_input = ((
float*
const*)input);
318 auto float_output = ((
float**)output);
320 ossia::audio_tick_state ts{
321 float_input, float_output, self.effective_inputs, self.effective_outputs,
322 nframes, timeInfo->currentTime};
329 std::atomic<PaStream*> m_stream{};
330 std::atomic<uint64_t> m_callbacks{};
331 double m_lastXrunTime{-1.};
332 std::chrono::milliseconds m_stall_timeout{1000};
333 mutable uint64_t m_seenCallbacks{};
334 mutable std::chrono::steady_clock::time_point m_seenAt{};
OSSIA_INLINE constexpr auto min(const T a, const U b) noexcept -> typename std::conditional<(sizeof(T) > sizeof(U)), T, U >::type
min function tailored for values
Definition math.hpp:125
spdlog::logger & logger() noexcept
Where the errors will be logged. Default is stderr.
Definition context.cpp:120
bool set_current_thread_realtime(int prio, bool algo_fifo, bool log)
For threads created by a library, e.g.
Definition thread.cpp:197