OSSIA
Open Scenario System for Interactive Application
Loading...
Searching...
No Matches
portaudio_protocol.hpp
1#pragma once
2#include <ossia/detail/config.hpp>
3
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>
9
10#include <portaudio.h>
11#if defined(__linux__) && __has_include(<alsa/asoundlib.h>)
12#include <ossia/audio/libasound.hpp>
13#define OSSIA_PORTAUDIO_ALSA_PERIOD 1
14#endif
15
16#include <algorithm>
17#include <atomic>
18#include <chrono>
19#include <cmath>
20#include <stdexcept>
21#include <string>
22#include <string_view>
23
24#define OSSIA_AUDIO_PORTAUDIO 1
25
26namespace ossia
27{
28class portaudio_engine final : public audio_engine
29{
30public:
31 portaudio_engine(
32 std::string name, std::string card_in, std::string card_out, int inputs,
33 int outputs, int rate, int bs, PaHostApiTypeId hostApi)
34 {
35 if(auto ec = Pa_Initialize(); ec != paNoError)
36 throw std::runtime_error(
37 std::string("PortAudio: cannot initialize: ") + Pa_GetErrorText(ec));
38
39 // paInDevelopment: any host API.
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);
44
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;
49
50 if(inputs <= 0 && outputs <= 0)
51 {
52 Pa_Terminate();
53 throw std::runtime_error(
54 "PortAudio: device not found: "
55 + (card_out.empty() ? card_in : card_out));
56 }
57
58 auto latency = [&](const PaDeviceInfo* dev, bool input, int channels) {
59 if(!dev || Pa_GetHostApiInfo(dev->hostApi)->type != paALSA)
60 return 0.01;
61 // The ALSA host (pa_linux_alsa.c) asks for a period of (bs + latency) / 4
62 // rounded up to a multiple of bs, at least the device's minimum, and a
63 // buffer of latency + period. With latency = (n - 1) periods: n periods
64 // of max(bs, device minimum) rounded up to a multiple of bs. Unlike the
65 // ALSA engine it cannot retry with more periods, so periods shorter
66 // than 2 ms get 4 (an HDA codec at 0.67 ms under-runs with 3 here).
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;
71 };
72
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;
79
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;
86
87 PaStreamParameters* actualInput = inputs > 0 ? &inputParameters : nullptr;
88 PaStreamParameters* actualOutput = outputs > 0 ? &outputParameters : nullptr;
89
90 PaStream* stream{};
91 auto ec = Pa_OpenStream(
92 &stream, actualInput, actualOutput, rate, bs, paNoFlag, &PortAudioCallback,
93 this);
94 if(ec != paNoError)
95 {
96 auto err = error_text("cannot open", ec, card_in, card_out);
97 Pa_Terminate();
98 throw std::runtime_error(err);
99 }
100
101 // Before the stream starts: the callback reads them.
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)));
113
114 // PortAudio's own real-time mode (PaAlsa_EnableRealtimeScheduling) runs
115 // the callback at SCHED_FIFO 1: the callback sets the priority instead.
116 m_stream.store(stream);
117
118 if(ec = Pa_StartStream(stream); ec != paNoError)
119 {
120 auto err = error_text("cannot start", ec, card_in, card_out);
121 Pa_CloseStream(stream);
122 m_stream.store(nullptr);
123 Pa_Terminate();
124 throw std::runtime_error(err);
125 }
126
127 }
128
129 bool running() const override
130 {
131 auto s = m_stream.load();
132 return s && Pa_IsStreamActive(s) == 1;
133 }
134
139 bool failed() const noexcept override
140 {
141 auto s = m_stream.load();
142 if(!s || stop_processing)
143 return false;
144 if(Pa_IsStreamActive(s) == 0)
145 return true;
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{})
149 {
150 m_seenCallbacks = calls;
151 m_seenAt = now;
152 return false;
153 }
154 return now - m_seenAt > m_stall_timeout;
155 }
156
157 void stop() override
158 {
159 audio_engine::stop();
160
161 // A stream that is stopped but not closed keeps the device open: opening
162 // it again, e.g. when the settings change, fails until it is closed.
163 // Aborting: stopping waits for the host's poll loop, seconds on a stalled
164 // device, and the ALSA host drops the buffer either way.
165 if(auto stream = m_stream.exchange(nullptr))
166 {
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));
171 }
172 }
173
174 ~portaudio_engine() override
175 {
176 stop();
177 Pa_Terminate();
178 }
179
180private:
183 static int alsa_min_period(std::string_view name, bool input, int rate, int channels)
184 {
185#if OSSIA_PORTAUDIO_ALSA_PERIOD
186 // "Card: device (hw:N,M)"
187 auto pos = name.rfind(" (hw:");
188 if(pos == std::string_view::npos || !name.ends_with(')'))
189 return 0;
190 const std::string hw{name.substr(pos + 2, name.size() - pos - 3)};
191 try
192 {
193 auto& snd = ossia::libasound::instance();
194 snd_pcm_t* pcm{};
195 if(snd.pcm_open(
196 &pcm, hw.c_str(), input ? SND_PCM_STREAM_CAPTURE : SND_PCM_STREAM_PLAYBACK,
197 SND_PCM_NONBLOCK)
198 < 0)
199 return 0;
200 snd_pcm_hw_params_t* params{};
201 snd_alloca(&params, snd, pcm_hw_params);
202 snd_pcm_uframes_t min{};
203 int dir{};
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);
209 snd.pcm_close(pcm);
210 return int(min);
211 }
212 catch(...)
213 {
214 return 0;
215 }
216#else
217 return 0;
218#endif
219 }
220
224 static int find_device(const std::string& name, int hostApiIndex, bool input)
225 {
226 if(name.empty())
227 return paNoDevice;
228
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);
233 return n;
234 };
235
236 int fallback = paNoDevice;
237 for(int i = 0; i < Pa_GetDeviceCount(); i++)
238 {
239 auto info = Pa_GetDeviceInfo(i);
240 if(hostApiIndex >= 0 && info->hostApi != hostApiIndex)
241 continue;
242 if((input ? info->maxInputChannels : info->maxOutputChannels) <= 0)
243 continue;
244 if(info->name == name)
245 return i;
246 if(fallback == paNoDevice && without_index(info->name) == without_index(name))
247 fallback = i;
248 }
249 return fallback;
250 }
251
252 static std::string error_text(
253 const char* what, PaError ec, const std::string& card_in,
254 const std::string& card_out)
255 {
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;
260 err += ": ";
261 err += Pa_GetErrorText(ec);
262 if(ec == paUnanticipatedHostError)
263 if(auto host = Pa_GetLastHostErrorInfo(); host && host->errorText)
264 err += std::string(" (") + host->errorText + ")";
265 return err;
266 }
267
268 static int clearBuffers(float** float_output, unsigned long nframes, int outs)
269 {
270 for(int i = 0; i < outs; i++)
271 {
272 auto chan = float_output[i];
273 for(std::size_t j = 0; j < nframes; j++)
274 chan[j] = 0.f;
275 }
276
277 return paContinue;
278 }
279
280 static int PortAudioCallback(
281 const void* input, void* output, unsigned long nframes,
282 const PaStreamCallbackTimeInfo* timeInfo, PaStreamCallbackFlags statusFlags,
283 void* userData)
284 {
285 [[maybe_unused]]
286 static const thread_local auto _
287 = [] {
288 ossia::set_thread_name("ossia audio 0");
289 ossia::set_thread_pinned(thread_type::Audio, 0);
290#if defined(__linux__)
291 ossia::set_current_thread_realtime(95, true, false);
292#endif
293 return 0;
294 }();
295
296 auto& self = *static_cast<portaudio_engine*>(userData);
297 self.m_callbacks.fetch_add(1, std::memory_order_relaxed);
298 // The flags of one host period reach every callback that period is split
299 // into; they share its time stamp.
300 if(statusFlags
301 & (paInputUnderflow | paInputOverflow | paOutputUnderflow | paOutputOverflow))
302 {
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;
306 }
307
308 self.tick_start();
309 auto clt = self.m_stream.load();
310
311 if(self.stop_processing || !clt)
312 {
313 self.tick_clear();
314 return clearBuffers(((float**)output), nframes, self.effective_outputs);
315 }
316
317 auto float_input = ((float* const*)input);
318 auto float_output = ((float**)output);
319
320 ossia::audio_tick_state ts{
321 float_input, float_output, self.effective_inputs, self.effective_outputs,
322 nframes, timeInfo->currentTime};
323 self.audio_tick(ts);
324
325 self.tick_end();
326 return paContinue;
327 }
328
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{};
335};
336}
337
338#endif
339#endif
Definition git_info.h:7
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