OSSIA
Open Scenario System for Interactive Application
Loading...
Searching...
No Matches
alsa_protocol.hpp
1#pragma once
2#if __has_include( \
3 <alsa/asoundlib.h>) && __has_include(<zita-alsa-pcmi-ardour.h>) && !defined(__EMSCRIPTEN__)
4
5#include <ossia/audio/audio_engine.hpp>
6#include <ossia/audio/libasound.hpp>
8#include <ossia/detail/pod_vector.hpp>
9#include <ossia/detail/thread.hpp>
10
11#include <alsa/asoundlib.h>
12
13#include <atomic>
14#include <chrono>
15#include <cstring>
16#include <future>
17#include <memory>
18#include <stdexcept>
19#include <string>
20#include <thread>
21
22#include <zita-alsa-pcmi-ardour.h>
23
24#define OSSIA_AUDIO_ALSA 1
25
26namespace ossia
27{
28class alsa_engine final : public audio_engine
29{
30public:
32 alsa_engine(
33 std::string card_in, std::string card_out, int inputs, int outputs, int rate,
34 int bs, int periods = 0)
35 {
36 // A direction without channels is not opened at all: a capture device
37 // that is busy or missing would otherwise fail the whole engine. One of
38 // the two stays open, as it is the one that clocks the engine: playback
39 // when neither has channels.
40 const bool capture = inputs > 0 && !card_in.empty();
41 const bool playback = (outputs > 0 && !card_out.empty()) || !capture;
42 if(playback && card_out.empty())
43 throw std::runtime_error("ALSA: no device selected");
44
45 // Linked streams run on one clock and are fine with 2 periods. Streams
46 // that cannot be linked (dmix with dsnoop) drift in phase and need 3.
47 // Linking two cards succeeds but only starts them together: their clocks
48 // still differ, which the check below handles. Some devices need more
49 // than the period they accept: full-speed USB (1 ms packets) under-runs
50 // continuously with two periods shorter than 2 ms. With periods == 0, a
51 // small period that keeps under-running is retried with one more period,
52 // and a count the device refuses falls back to the one that ran.
53 bool use_capture = capture;
54 std::string error;
55 auto attempt = [&](unsigned nfrag) {
56 m_client.reset();
57 m_client = std::make_unique<Alsa_pcmi>(
58 playback ? card_out.c_str() : nullptr,
59 use_capture ? card_in.c_str() : nullptr, nullptr, rate, bs, nfrag, nfrag,
60 Alsa_pcmi::DEBUG_INIT, playback ? std::max(outputs, 0) : 0,
61 use_capture ? inputs : 0);
62 auto& client = *m_client;
63 // Playback clocks the engine and is what the user hears: without it the
64 // engine would run, silently. Capture only brings the inputs.
65 if(client.state() == 0 && (!playback || client.play_handle()))
66 return true;
67 error = describe_error(client, card_in, card_out, playback, use_capture, rate, bs);
68 m_client.reset();
69 return false;
70 };
71 auto configure = [&](unsigned nfrag) {
72 if(attempt(nfrag))
73 return true;
74 if(!(use_capture && playback))
75 return false;
76 ossia::logger().warn("{}; continuing without inputs", error);
77 use_capture = false;
78 return attempt(nfrag);
79 };
80 auto run = [&] {
81 try
82 {
83 start(inputs, outputs, playback ? card_out : card_in);
84 return true;
85 }
86 catch(std::exception& e)
87 {
88 error = e.what();
89 return false;
90 }
91 };
92
93 unsigned nfrag = periods > 0 ? periods : 2;
94 if(!configure(nfrag))
95 throw std::runtime_error(error);
96 if(periods <= 0 && m_client->play_handle() && m_client->capt_handle()
97 && !m_client->synced())
98 {
99 if(attempt(3))
100 nfrag = 3;
101 else if(!attempt(nfrag))
102 throw std::runtime_error(error);
103 }
104 if(!run())
105 throw std::runtime_error(error);
106
107 while(periods <= 0 && nfrag < 4 && keeps_underrunning())
108 {
109 ossia::logger().info(
110 "ALSA: {} under-runs with {} periods, trying {}",
111 playback ? card_out : card_in, nfrag, nfrag + 1);
112 shutdown();
113 if(attempt(nfrag + 1) && run())
114 {
115 nfrag++;
116 continue;
117 }
118 // The device does not take more periods: keep the count that ran.
119 if(!attempt(nfrag) || !run())
120 throw std::runtime_error(error);
121 break;
122 }
123
124 if(capture && !m_client->capt_handle())
125 ossia::logger().warn(
126 "ALSA: cannot open {} for capture ({}), continuing without inputs", card_in,
127 ossia::libasound::instance().strerror(m_client->capt_open_error()));
128 if(m_fsize != bs)
129 ossia::logger().info(
130 "ALSA: {} uses a period of {} frames instead of {}",
131 playback ? card_out : card_in, m_fsize, bs);
132
133 m_activated = true;
134 }
135
136 void stop() override
137 {
138 audio_engine::stop();
139 shutdown();
140 m_activated = false;
141 }
142
143 ~alsa_engine() override { stop(); }
144
145 bool running() const override
146 {
147 if(!m_client)
148 return false;
149 return m_activated && !m_failed.load(std::memory_order_relaxed);
150 }
151
152 bool failed() const noexcept override
153 {
154 return m_failed.load(std::memory_order_relaxed);
155 }
156
157private:
158 void start(int inputs, int outputs, const std::string& name)
159 {
160 auto& client = *m_client;
161 m_fsize = client.fsize();
162 effective_sample_rate = client.fsamp();
163 effective_buffer_size = m_fsize;
164 effective_inputs = std::min(inputs, (int)client.ncapt());
165 effective_outputs = std::min(outputs, (int)client.nplay());
166 effective_input_latency = client.capt_handle() ? m_fsize : 0;
167 effective_output_latency
168 = client.play_handle() ? m_fsize * (int)client.real_play_nfrag() : 0;
169
170 m_capture.assign(m_fsize * std::max(effective_inputs, 1), 0.f);
171 m_playback.assign(m_fsize * std::max(effective_outputs, 1), 0.f);
172 m_capture_ptrs.clear();
173 m_playback_ptrs.clear();
174 for(int i = 0; i < effective_inputs; i++)
175 m_capture_ptrs.push_back(m_capture.data() + i * m_fsize);
176 for(int i = 0; i < effective_outputs; i++)
177 m_playback_ptrs.push_back(m_playback.data() + i * m_fsize);
178
179 xruns = 0;
180 m_failed = false;
181 m_stop_token = true;
182
183 // The thread owns the promise: the future returns as soon as the value
184 // is set, possibly while set_value() is still running.
185 std::promise<int> started;
186 auto start_result = started.get_future();
187 m_thread = ossia::thread{[this, started = std::move(started)]() mutable {
188 init_thread();
189 const int err = m_client->pcm_start();
190 started.set_value(err);
191 if(err == 0)
192 thread_duplex();
193 }};
194 set_thread_realtime(m_thread);
195
196 if(start_result.get() != 0)
197 {
198 m_thread.join();
199 m_client.reset();
200 throw std::runtime_error("ALSA: " + name + " could not be started");
201 }
202 }
203
209 bool keeps_underrunning()
210 {
211 const int period_us = int(int64_t(1'000'000) * m_fsize / std::max(effective_sample_rate, 1));
212 if(period_us > 5000)
213 return false;
214 using namespace std::chrono;
215 std::this_thread::sleep_for(std::max<microseconds>(30ms, microseconds(4 * period_us)));
216 const auto before = xruns.load();
217 std::this_thread::sleep_for(std::max<microseconds>(60ms, microseconds(20 * period_us)));
218 return xruns.load() - before >= 2 || m_failed.load();
219 }
220
221 void shutdown()
222 {
223 if(m_client)
224 {
225 m_stop_token.store(false, std::memory_order_relaxed);
226 if(m_thread.joinable())
227 m_thread.join();
228 m_client.reset();
229 }
230 }
231
232 void thread_duplex()
233 {
234 auto& client = *this->m_client;
235 const int n = m_fsize;
236 int failures = 0;
237 int idle_xruns = 0;
238 // On USB the hardware pointer is where the driver submitted the frames,
239 // not where they are heard: the latency is measured once it runs, as the
240 // mean over periods 8 to 15 after the start.
241 int delay_period = 0;
242 int64_t delay_sum = 0;
243 int delay_count = 0;
244
245 while(m_stop_token.load(std::memory_order_relaxed))
246 {
247 long k = client.pcm_wait();
248
249 // pcm_wait reports 1 after recovering from an xrun, and below 0 when
250 // poll() timed out (1 s) or the recovery failed.
251 if(client.state() < 0)
252 {
253 if(++failures >= 3)
254 {
255 ossia::logger().error("ALSA: the device stopped responding");
256 m_failed = true;
257 break;
258 }
259 continue;
260 }
261 failures = 0;
262 if(client.state() > 0)
263 {
264 xruns.fetch_add(1, std::memory_order_relaxed);
265 if(++idle_xruns >= 200)
266 {
267 ossia::logger().error(
268 "ALSA: the device under-runs continuously; try a larger buffer size");
269 m_failed = true;
270 break;
271 }
272 }
273 if(k >= n)
274 idle_xruns = 0;
275
276 while(k >= n)
277 {
278 // Both return the frames that can be accessed in one piece: always n
279 // here, as the buffers are whole periods, unless the device failed.
280 if(client.capt_init(n) != n && client.capt_handle())
281 break;
282 for(int c = 0; c < effective_inputs; c++)
283 client.capt_chan(c, m_capture_ptrs[c], n, 1);
284 client.capt_done(n);
285
286 std::fill(m_playback.begin(), m_playback.end(), 0.f);
287 process(m_capture_ptrs.data(), m_playback_ptrs.data(), n);
288
289 if(client.play_init(n) != n && client.play_handle())
290 break;
291 for(int c = 0; c < effective_outputs; c++)
292 client.play_chan(c, m_playback_ptrs[c], n, 1);
293 for(int c = effective_outputs; c < (int)client.nplay(); c++)
294 client.clear_chan(c, n);
295 client.play_done(n);
296 if(delay_period < 16 && ++delay_period >= 8 && client.play_handle())
297 {
298 snd_pcm_sframes_t delay{};
299 if(ossia::libasound::instance().pcm_delay(client.play_handle(), &delay) == 0
300 && delay > n)
301 {
302 delay_sum += delay - n;
303 delay_count++;
304 }
305 if(delay_period == 16 && delay_count > 0)
306 effective_output_latency.store(
307 int(delay_sum / delay_count), std::memory_order_relaxed);
308 }
309
310 k -= n;
311 }
312 }
313
314 client.pcm_stop();
315 }
316
317 static std::string describe_error(
318 const Alsa_pcmi& client, const std::string& card_in, const std::string& card_out,
319 bool playback, bool capture, int rate, int bs)
320 {
321 if(playback && !client.play_handle() && client.play_open_error() < 0)
322 return "ALSA: cannot open " + card_out + " for playback: "
323 + ossia::libasound::instance().strerror(client.play_open_error());
324 // Nothing opened: a capture-only engine whose device could not be opened.
325 if(!client.play_handle() && capture && !client.capt_handle()
326 && client.capt_open_error() < 0)
327 return "ALSA: cannot open " + card_in + " for capture: "
328 + ossia::libasound::instance().strerror(client.capt_open_error());
329
330 const char* stream = client.error_stream() ? client.error_stream() : "the device";
331 const std::string& name = (client.error_stream()
332 && std::strcmp(client.error_stream(), "capture") == 0)
333 ? card_in
334 : card_out;
335 switch(client.state())
336 {
337 case -3:
338 return "ALSA: " + name + " does not support " + std::to_string(rate)
339 + " Hz for " + stream;
340 case -4:
341 return "ALSA: " + name + " does not support a buffer of " + std::to_string(bs)
342 + " frames for " + stream;
343 case -5:
344 return "ALSA: " + name + " does not support this number of periods for "
345 + stream;
346 case -8:
347 return "ALSA: " + card_out + " cannot use the period " + card_in
348 + " gave for capture (" + std::to_string(bs)
349 + " frames requested): choose another buffer size";
350 case -6:
351 return "ALSA: " + name + " uses a sample format score cannot handle for "
352 + stream;
353 default:
354 return "ALSA: " + name + " rejected the configuration for " + stream + " ("
355 + std::to_string(rate) + " Hz, " + std::to_string(bs) + " frames)";
356 }
357 }
358
359 void init_thread()
360 {
361 ossia::set_thread_name("ossia audio 0");
362 ossia::set_thread_pinned(thread_type::Audio, 0);
363 }
364
365 void process(float** float_input, float** float_output, uint64_t nframes)
366 {
367 tick_start();
368
369 if(stop_processing)
370 {
371 tick_clear();
372 return;
373 }
374
375 using namespace std::chrono;
376 const auto now = clk::now();
377 if(m_start_time == clk::time_point{})
378 m_start_time = now;
379 ossia::audio_tick_state ts{
380 float_input,
381 float_output,
382 effective_inputs,
383 effective_outputs,
384 nframes,
385 duration_cast<nanoseconds>(now - m_start_time).count() / 1e9};
386 audio_tick(ts);
387
388 tick_end();
389 }
390
391 std::unique_ptr<Alsa_pcmi> m_client;
392 ossia::thread m_thread;
393 using clk = std::chrono::steady_clock;
394 clk::time_point m_start_time{};
395 ossia::float_vector m_capture;
396 ossia::float_vector m_playback;
397 ossia::pod_vector<float*> m_capture_ptrs;
398 ossia::pod_vector<float*> m_playback_ptrs;
399 int m_fsize{};
400 std::atomic_bool m_stop_token{true};
401 std::atomic_bool m_activated{};
402 std::atomic_bool m_failed{};
403};
404}
405
406#endif
Definition git_info.h:7
spdlog::logger & logger() noexcept
Where the errors will be logged. Default is stderr.
Definition context.cpp:120
bool set_thread_realtime(std::thread &t, int prio, bool algo_fifo)
Requests a real-time policy for the thread.
Definition thread.cpp:188