Line data Source code
1 : /*
2 : * Copyright (C) 2004-2026 Savoir-faire Linux Inc.
3 : *
4 : * This program is free software: you can redistribute it and/or modify
5 : * it under the terms of the GNU General Public License as published by
6 : * the Free Software Foundation, either version 3 of the License, or
7 : * (at your option) any later version.
8 : *
9 : * This program is distributed in the hope that it will be useful,
10 : * but WITHOUT ANY WARRANTY; without even the implied warranty of
11 : * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 : * GNU General Public License for more details.
13 : *
14 : * You should have received a copy of the GNU General Public License
15 : * along with this program. If not, see <https://www.gnu.org/licenses/>.
16 : */
17 :
18 : #include "alsalayer.h"
19 : #include "logger.h"
20 : #include "manager.h"
21 : #include "noncopyable.h"
22 : #include "client/jami_signal.h"
23 : #include "audio/ringbufferpool.h"
24 : #include "audio/ringbuffer.h"
25 : #include "audio/audioloop.h"
26 : #include "libav_utils.h"
27 :
28 : #include <fmt/core.h>
29 :
30 : #include <thread>
31 : #include <atomic>
32 : #include <chrono>
33 :
34 : namespace jami {
35 :
36 32 : AlsaLayer::AlsaLayer(const AudioPreference& pref)
37 : : AudioLayer(pref)
38 64 : , indexIn_(pref.getAlsaCardin())
39 32 : , indexOut_(pref.getAlsaCardout())
40 32 : , indexRing_(pref.getAlsaCardRingtone())
41 64 : , audioPlugin_(pref.getAlsaPlugin())
42 : {
43 32 : setHasNativeAEC(false);
44 32 : setHasNativeNS(false);
45 32 : }
46 :
47 64 : AlsaLayer::~AlsaLayer()
48 : {
49 32 : status_ = Status::Idle;
50 32 : stopThread();
51 :
52 : /* Then close the audio devices */
53 32 : closeCaptureStream();
54 32 : closePlaybackStream();
55 32 : closeRingtoneStream();
56 64 : }
57 :
58 : /**
59 : * Reimplementation of run()
60 : */
61 : void
62 215 : AlsaLayer::run()
63 : {
64 215 : if (playbackHandle_)
65 0 : playbackChanged(true);
66 215 : if (captureHandle_)
67 0 : recordChanged(true);
68 :
69 15813347815 : while (status_ == Status::Started and running_) {
70 15813347600 : playback();
71 15813347600 : ringtone();
72 15813347600 : capture();
73 : }
74 :
75 215 : playbackChanged(false);
76 215 : recordChanged(false);
77 215 : }
78 :
79 : // Retry approach taken from pa_linux_alsa.c, part of PortAudio
80 : bool
81 160 : AlsaLayer::openDevice(snd_pcm_t** pcm, const std::string& dev, snd_pcm_stream_t stream, AudioFormat& format)
82 : {
83 640 : JAMI_DEBUG("Alsa: Opening {} device '{}' with format {}",
84 : (stream == SND_PCM_STREAM_CAPTURE) ? "capture" : "playback",
85 : dev,
86 : format.toString());
87 :
88 : static const int MAX_RETRIES = 10; // times of 100ms
89 160 : int err, tries = 0;
90 : do {
91 160 : err = snd_pcm_open(pcm, dev.c_str(), stream, 0);
92 : // Retry if busy, since dmix plugin may not have released the device yet
93 160 : if (err == -EBUSY) {
94 : // We're called in audioThread_ context, so if exit is requested
95 : // force return now
96 0 : std::this_thread::sleep_for(std::chrono::milliseconds(100));
97 : }
98 160 : } while (err == -EBUSY and ++tries <= MAX_RETRIES);
99 :
100 160 : if (err < 0) {
101 640 : JAMI_ERROR("Alsa: Unable to open {} device {} : {}",
102 : (stream == SND_PCM_STREAM_CAPTURE) ? "capture"
103 : : (stream == SND_PCM_STREAM_PLAYBACK) ? "playback"
104 : : "ringtone",
105 : dev,
106 : snd_strerror(err));
107 160 : return false;
108 : }
109 :
110 0 : if (!alsa_set_params(*pcm, format)) {
111 0 : snd_pcm_close(*pcm);
112 0 : return false;
113 : }
114 :
115 0 : return true;
116 : }
117 :
118 : void
119 215 : AlsaLayer::startStream(AudioDeviceType type)
120 : {
121 215 : std::unique_lock lk(mutex_);
122 215 : status_ = Status::Starting;
123 215 : stopThread();
124 :
125 215 : bool dsnop = audioPlugin_ == PCM_DMIX_DSNOOP;
126 :
127 215 : if (type == AudioDeviceType::PLAYBACK and not is_playback_open_) {
128 101 : is_playback_open_ = openDevice(&playbackHandle_,
129 202 : buildDeviceTopo(dsnop ? PCM_DMIX : audioPlugin_, indexOut_),
130 : SND_PCM_STREAM_PLAYBACK,
131 101 : audioFormat_);
132 101 : if (not is_playback_open_)
133 101 : emitSignal<libjami::ConfigurationSignal::Error>(ALSA_PLAYBACK_DEVICE);
134 :
135 101 : hardwareFormatAvailable(getFormat());
136 101 : startPlaybackStream();
137 : }
138 :
139 215 : if (type == AudioDeviceType::RINGTONE and getIndexPlayback() != getIndexRingtone() and not ringtoneHandle_) {
140 0 : if (!openDevice(&ringtoneHandle_,
141 0 : buildDeviceTopo(dsnop ? PCM_DMIX : audioPlugin_, indexRing_),
142 : SND_PCM_STREAM_PLAYBACK,
143 0 : audioFormat_))
144 0 : emitSignal<libjami::ConfigurationSignal::Error>(ALSA_PLAYBACK_DEVICE);
145 : }
146 :
147 215 : if (type == AudioDeviceType::CAPTURE and not is_capture_open_) {
148 59 : is_capture_open_ = openDevice(&captureHandle_,
149 118 : buildDeviceTopo(dsnop ? PCM_DSNOOP : audioPlugin_, indexIn_),
150 : SND_PCM_STREAM_CAPTURE,
151 59 : audioInputFormat_);
152 :
153 59 : if (not is_capture_open_)
154 59 : emitSignal<libjami::ConfigurationSignal::Error>(ALSA_CAPTURE_DEVICE);
155 59 : prepareCaptureStream();
156 59 : startCaptureStream();
157 : }
158 :
159 215 : status_ = Status::Started;
160 215 : startThread();
161 215 : }
162 :
163 : void
164 215 : AlsaLayer::stopStream(AudioDeviceType stream)
165 : {
166 215 : std::unique_lock lk(mutex_);
167 215 : stopThread();
168 :
169 215 : if (stream == AudioDeviceType::CAPTURE && is_capture_open_) {
170 0 : closeCaptureStream();
171 : }
172 :
173 215 : if (stream == AudioDeviceType::PLAYBACK && is_playback_open_) {
174 0 : closePlaybackStream();
175 0 : flushUrgent();
176 0 : flushMain();
177 : }
178 :
179 215 : if (stream == AudioDeviceType::RINGTONE and ringtoneHandle_) {
180 0 : closeRingtoneStream();
181 : }
182 :
183 215 : if (is_capture_open_ or is_playback_open_ or ringtoneHandle_) {
184 0 : startThread();
185 : } else {
186 215 : status_ = Status::Idle;
187 : }
188 215 : }
189 :
190 : void
191 215 : AlsaLayer::startThread()
192 : {
193 215 : running_ = true;
194 215 : audioThread_ = std::thread(&AlsaLayer::run, this);
195 215 : }
196 :
197 : void
198 462 : AlsaLayer::stopThread()
199 : {
200 462 : running_ = false;
201 462 : if (audioThread_.joinable())
202 215 : audioThread_.join();
203 462 : }
204 :
205 : /*
206 : * GCC extension : statement expression
207 : *
208 : * ALSA_CALL(function_call, error_string) will:
209 : * call the function
210 : * display an error if the function failed
211 : * return the function return value
212 : */
213 : #define ALSA_CALL(call, error) \
214 : ({ \
215 : int err_code = call; \
216 : if (err_code < 0) \
217 : JAMI_ERROR(error ": {}", snd_strerror(err_code)); \
218 : err_code; \
219 : })
220 :
221 : void
222 0 : AlsaLayer::stopCaptureStream()
223 : {
224 0 : if (captureHandle_ && ALSA_CALL(snd_pcm_drop(captureHandle_), "Unable to stop capture") >= 0) {
225 0 : is_capture_running_ = false;
226 0 : is_capture_prepared_ = false;
227 : }
228 0 : }
229 :
230 : void
231 32 : AlsaLayer::closeCaptureStream()
232 : {
233 32 : if (is_capture_prepared_ and is_capture_running_)
234 0 : stopCaptureStream();
235 :
236 128 : JAMI_LOG("Alsa: Closing capture stream");
237 32 : if (is_capture_open_ && ALSA_CALL(snd_pcm_close(captureHandle_), "Unable to close capture") >= 0) {
238 0 : is_capture_open_ = false;
239 0 : captureHandle_ = nullptr;
240 : }
241 32 : }
242 :
243 : void
244 59 : AlsaLayer::startCaptureStream()
245 : {
246 59 : if (captureHandle_ and not is_capture_running_)
247 0 : if (ALSA_CALL(snd_pcm_start(captureHandle_), "Unable to start capture") >= 0)
248 0 : is_capture_running_ = true;
249 59 : }
250 :
251 : void
252 11 : AlsaLayer::stopPlaybackStream()
253 : {
254 11 : if (playbackHandle_ and is_playback_running_) {
255 0 : if (ALSA_CALL(snd_pcm_drop(playbackHandle_), "Unable to stop playback") >= 0) {
256 0 : is_playback_running_ = false;
257 : }
258 : }
259 11 : }
260 :
261 : void
262 32 : AlsaLayer::closePlaybackStream()
263 : {
264 32 : if (is_playback_running_)
265 11 : stopPlaybackStream();
266 :
267 32 : if (is_playback_open_) {
268 0 : JAMI_LOG("Alsa: Closing playback stream");
269 0 : if (ALSA_CALL(snd_pcm_close(playbackHandle_), "Coulnd't close playback") >= 0)
270 0 : is_playback_open_ = false;
271 0 : playbackHandle_ = nullptr;
272 : }
273 32 : }
274 :
275 : void
276 32 : AlsaLayer::closeRingtoneStream()
277 : {
278 32 : if (ringtoneHandle_) {
279 0 : ALSA_CALL(snd_pcm_drop(ringtoneHandle_), "Unable to stop ringtone");
280 0 : ALSA_CALL(snd_pcm_close(ringtoneHandle_), "Unable to close ringtone");
281 0 : ringtoneHandle_ = nullptr;
282 : }
283 32 : }
284 :
285 : void
286 101 : AlsaLayer::startPlaybackStream()
287 : {
288 101 : is_playback_running_ = true;
289 101 : }
290 :
291 : void
292 59 : AlsaLayer::prepareCaptureStream()
293 : {
294 59 : if (is_capture_open_ and not is_capture_prepared_)
295 0 : if (ALSA_CALL(snd_pcm_prepare(captureHandle_), "Unable to prepare capture") >= 0)
296 0 : is_capture_prepared_ = true;
297 59 : }
298 :
299 : bool
300 0 : AlsaLayer::alsa_set_params(snd_pcm_t* pcm_handle, AudioFormat& format)
301 : {
302 : #define TRY(call, error) \
303 : do { \
304 : if (ALSA_CALL(call, error) < 0) \
305 : return false; \
306 : } while (0)
307 :
308 : snd_pcm_hw_params_t* hwparams;
309 0 : snd_pcm_hw_params_alloca(&hwparams);
310 :
311 0 : constexpr unsigned ALSA_PERIOD_SIZE = 160;
312 0 : constexpr unsigned ALSA_NB_PERIOD = 8;
313 0 : constexpr unsigned ALSA_BUFFER_SIZE = ALSA_PERIOD_SIZE * ALSA_NB_PERIOD;
314 :
315 0 : snd_pcm_uframes_t period_size = ALSA_PERIOD_SIZE;
316 0 : snd_pcm_uframes_t buffer_size = ALSA_BUFFER_SIZE;
317 0 : unsigned int periods = ALSA_NB_PERIOD;
318 :
319 0 : snd_pcm_uframes_t period_size_min = 0;
320 0 : snd_pcm_uframes_t period_size_max = 0;
321 0 : snd_pcm_uframes_t buffer_size_min = 0;
322 0 : snd_pcm_uframes_t buffer_size_max = 0;
323 :
324 : #define HW pcm_handle, hwparams /* hardware parameters */
325 0 : TRY(snd_pcm_hw_params_any(HW), "hwparams init");
326 :
327 0 : TRY(snd_pcm_hw_params_set_access(HW, SND_PCM_ACCESS_RW_INTERLEAVED), "access type");
328 :
329 0 : if (snd_pcm_hw_params_test_format(HW, SND_PCM_FORMAT_FLOAT_LE) == 0) {
330 0 : TRY(snd_pcm_hw_params_set_format(HW, SND_PCM_FORMAT_FLOAT_LE), "sample format");
331 0 : format = format.withSampleFormat(AV_SAMPLE_FMT_FLT);
332 : } else {
333 0 : TRY(snd_pcm_hw_params_set_format(HW, SND_PCM_FORMAT_S16_LE), "sample format");
334 0 : format = format.withSampleFormat(AV_SAMPLE_FMT_S16);
335 : }
336 :
337 0 : TRY(snd_pcm_hw_params_set_rate_resample(HW, 0), "hardware sample rate"); /* prevent software resampling */
338 0 : TRY(snd_pcm_hw_params_set_rate_near(HW, &format.sample_rate, nullptr), "sample rate");
339 :
340 : // Query the max number of channels supported by the hardware
341 0 : unsigned int max_channels = 0;
342 0 : if (snd_pcm_hw_params_get_channels_max(hwparams, &max_channels) < 0) {
343 0 : JAMI_WARNING("Unable to query hardware channel number, defaulting to 2");
344 0 : max_channels = 2;
345 : }
346 :
347 0 : format.nb_channels = std::min(max_channels, 2u);
348 0 : if (format.nb_channels == 0)
349 0 : format.nb_channels = 2;
350 :
351 0 : TRY(snd_pcm_hw_params_set_channels_near(HW, &format.nb_channels), "channel count");
352 :
353 0 : snd_pcm_hw_params_get_buffer_size_min(hwparams, &buffer_size_min);
354 0 : snd_pcm_hw_params_get_buffer_size_max(hwparams, &buffer_size_max);
355 0 : snd_pcm_hw_params_get_period_size_min(hwparams, &period_size_min, nullptr);
356 0 : snd_pcm_hw_params_get_period_size_max(hwparams, &period_size_max, nullptr);
357 0 : JAMI_DEBUG("Buffer size range from {} to {}", buffer_size_min, buffer_size_max);
358 0 : JAMI_DEBUG("Period size range from {} to {}", period_size_min, period_size_max);
359 0 : buffer_size = buffer_size > buffer_size_max ? buffer_size_max : buffer_size;
360 0 : buffer_size = buffer_size < buffer_size_min ? buffer_size_min : buffer_size;
361 0 : period_size = period_size > period_size_max ? period_size_max : period_size;
362 0 : period_size = period_size < period_size_min ? period_size_min : period_size;
363 :
364 0 : TRY(snd_pcm_hw_params_set_buffer_size_near(HW, &buffer_size), "Unable to set buffer size for playback");
365 0 : TRY(snd_pcm_hw_params_set_period_size_near(HW, &period_size, nullptr), "Unable to set period size for playback");
366 0 : TRY(snd_pcm_hw_params_set_periods_near(HW, &periods, nullptr), "Unable to set number of periods for playback");
367 0 : TRY(snd_pcm_hw_params(HW), "hwparams");
368 :
369 0 : snd_pcm_hw_params_get_buffer_size(hwparams, &buffer_size);
370 0 : snd_pcm_hw_params_get_period_size(hwparams, &period_size, nullptr);
371 0 : snd_pcm_hw_params_get_rate(hwparams, &format.sample_rate, nullptr);
372 0 : snd_pcm_hw_params_get_channels(hwparams, &format.nb_channels);
373 0 : JAMI_DEBUG("ALSA period_size = {}", period_size);
374 0 : JAMI_DEBUG("ALSA buffer_size = {}", buffer_size);
375 :
376 0 : if (2 * period_size > buffer_size) {
377 0 : JAMI_ERROR("buffer to small, unable to use");
378 0 : return false;
379 : }
380 :
381 : #undef HW
382 :
383 0 : JAMI_DEBUG("{} using format {}",
384 : (snd_pcm_stream(pcm_handle) == SND_PCM_STREAM_PLAYBACK) ? "playback" : "capture",
385 : format.toString());
386 :
387 0 : snd_pcm_sw_params_t* swparams = nullptr;
388 0 : snd_pcm_sw_params_alloca(&swparams);
389 :
390 : #define SW pcm_handle, swparams /* software parameters */
391 0 : snd_pcm_sw_params_current(SW);
392 0 : TRY(snd_pcm_sw_params_set_start_threshold(SW, period_size * 2), "start threshold");
393 0 : TRY(snd_pcm_sw_params(SW), "sw parameters");
394 : #undef SW
395 :
396 0 : return true;
397 :
398 : #undef TRY
399 : }
400 :
401 : // TODO first frame causes broken pipe (underrun) because not enough data is sent
402 : // we should wait until the handle is ready
403 : void
404 0 : AlsaLayer::write(const AudioFrame& buffer, snd_pcm_t* handle)
405 : {
406 0 : int err = snd_pcm_writei(handle, (const void*) buffer.pointer()->data[0], buffer.pointer()->nb_samples);
407 :
408 0 : if (err < 0)
409 0 : snd_pcm_recover(handle, err, 0);
410 :
411 0 : if (err >= 0)
412 0 : return;
413 :
414 0 : switch (err) {
415 0 : case -EPIPE:
416 : case -ESTRPIPE:
417 : case -EIO: {
418 : snd_pcm_status_t* status;
419 0 : snd_pcm_status_alloca(&status);
420 :
421 0 : if (ALSA_CALL(snd_pcm_status(handle, status), "Unable to get playback handle status") >= 0)
422 0 : if (snd_pcm_status_get_state(status) == SND_PCM_STATE_XRUN) {
423 0 : stopPlaybackStream();
424 0 : startPlaybackStream();
425 : }
426 :
427 0 : ALSA_CALL(snd_pcm_writei(handle, (const void*) buffer.pointer()->data[0], buffer.pointer()->nb_samples),
428 : "XRUN handling failed");
429 0 : break;
430 : }
431 :
432 0 : case -EBADFD: {
433 : snd_pcm_status_t* status;
434 0 : snd_pcm_status_alloca(&status);
435 :
436 0 : if (ALSA_CALL(snd_pcm_status(handle, status), "Unable to get playback handle status") >= 0) {
437 0 : if (snd_pcm_status_get_state(status) == SND_PCM_STATE_SETUP) {
438 0 : JAMI_ERROR(
439 : "Writing in state SND_PCM_STATE_SETUP, should be SND_PCM_STATE_PREPARED or SND_PCM_STATE_RUNNING");
440 0 : int error = snd_pcm_prepare(handle);
441 :
442 0 : if (error < 0) {
443 0 : JAMI_ERROR("Failed to prepare handle: {}", snd_strerror(error));
444 0 : stopPlaybackStream();
445 : }
446 : }
447 : }
448 :
449 0 : break;
450 : }
451 :
452 0 : default:
453 0 : JAMI_ERROR("Unknown write error, dropping frames: {}", snd_strerror(err));
454 0 : stopPlaybackStream();
455 0 : break;
456 : }
457 : }
458 :
459 : std::unique_ptr<AudioFrame>
460 0 : AlsaLayer::read(unsigned frames)
461 : {
462 0 : if (snd_pcm_state(captureHandle_) == SND_PCM_STATE_XRUN) {
463 0 : prepareCaptureStream();
464 0 : startCaptureStream();
465 : }
466 :
467 0 : auto ret = std::make_unique<AudioFrame>(audioInputFormat_, frames);
468 0 : int err = snd_pcm_readi(captureHandle_, ret->pointer()->data[0], frames);
469 :
470 0 : if (err >= 0) {
471 0 : ret->pointer()->nb_samples = err;
472 0 : return ret;
473 : }
474 :
475 0 : switch (err) {
476 0 : case -EPIPE:
477 : case -ESTRPIPE:
478 : case -EIO: {
479 : snd_pcm_status_t* status;
480 0 : snd_pcm_status_alloca(&status);
481 :
482 0 : if (ALSA_CALL(snd_pcm_status(captureHandle_, status), "Get status failed") >= 0)
483 0 : if (snd_pcm_status_get_state(status) == SND_PCM_STATE_XRUN) {
484 0 : stopCaptureStream();
485 0 : prepareCaptureStream();
486 0 : startCaptureStream();
487 : }
488 :
489 0 : JAMI_ERROR("XRUN capture ignored ({})", snd_strerror(err));
490 0 : break;
491 : }
492 :
493 0 : case -EPERM:
494 0 : JAMI_ERROR("Unable to capture, EPERM ({})", snd_strerror(err));
495 0 : prepareCaptureStream();
496 0 : startCaptureStream();
497 0 : break;
498 : }
499 :
500 0 : return {};
501 0 : }
502 :
503 : std::string
504 160 : AlsaLayer::buildDeviceTopo(const std::string& plugin, int card)
505 : {
506 160 : if (plugin == PCM_DEFAULT)
507 320 : return fmt::format("plughw:{},0", card);
508 :
509 0 : return fmt::format("{}:{}", plugin, card);
510 : }
511 :
512 : static bool
513 0 : safeUpdate(snd_pcm_t* handle, long& samples)
514 : {
515 0 : samples = snd_pcm_avail_update(handle);
516 :
517 0 : if (samples < 0) {
518 0 : samples = snd_pcm_recover(handle, samples, 0);
519 :
520 0 : if (samples < 0) {
521 0 : JAMI_ERROR("Got unrecoverable error from snd_pcm_avail_update: {}", snd_strerror(samples));
522 0 : return false;
523 : }
524 : }
525 :
526 0 : return true;
527 : }
528 :
529 : static std::vector<std::string>
530 0 : getValues(const std::vector<HwIDPair>& deviceMap)
531 : {
532 0 : std::vector<std::string> audioDeviceList;
533 0 : audioDeviceList.reserve(deviceMap.size());
534 :
535 0 : for (const auto& dev : deviceMap)
536 0 : audioDeviceList.push_back(dev.second);
537 :
538 0 : return audioDeviceList;
539 0 : }
540 :
541 : std::vector<std::string>
542 0 : AlsaLayer::getCaptureDeviceList() const
543 : {
544 0 : return getValues(getAudioDeviceIndexMap(true));
545 : }
546 :
547 : std::vector<std::string>
548 0 : AlsaLayer::getPlaybackDeviceList() const
549 : {
550 0 : return getValues(getAudioDeviceIndexMap(false));
551 : }
552 :
553 : std::vector<HwIDPair>
554 110 : AlsaLayer::getAudioDeviceIndexMap(bool getCapture) const
555 : {
556 : snd_ctl_t* handle;
557 : snd_ctl_card_info_t* info;
558 : snd_pcm_info_t* pcminfo;
559 110 : snd_ctl_card_info_alloca(&info);
560 110 : snd_pcm_info_alloca(&pcminfo);
561 :
562 110 : int numCard = -1;
563 :
564 110 : std::vector<HwIDPair> audioDevice;
565 :
566 110 : if (snd_card_next(&numCard) < 0 || numCard < 0)
567 110 : return audioDevice;
568 :
569 : do {
570 0 : std::string name = fmt::format("hw:{}", numCard);
571 :
572 0 : if (snd_ctl_open(&handle, name.c_str(), 0) == 0) {
573 0 : if (snd_ctl_card_info(handle, info) == 0) {
574 0 : snd_pcm_info_set_device(pcminfo, 0);
575 0 : snd_pcm_info_set_stream(pcminfo, getCapture ? SND_PCM_STREAM_CAPTURE : SND_PCM_STREAM_PLAYBACK);
576 :
577 : int err;
578 0 : if ((err = snd_ctl_pcm_info(handle, pcminfo)) < 0) {
579 0 : JAMI_WARNING("Unable to get info for {} device {}: {}",
580 : getCapture ? "capture" : "playback",
581 : name,
582 : snd_strerror(err));
583 : } else {
584 : auto description = fmt::format("{} - {}",
585 0 : snd_ctl_card_info_get_name(info),
586 0 : snd_pcm_info_get_name(pcminfo));
587 0 : JAMI_DEBUG("{} device {} : {} [{}]",
588 : getCapture ? "capture" : "playback",
589 : snd_ctl_card_info_get_id(info),
590 : name,
591 : description);
592 :
593 : // The number of the sound card is associated with a string description
594 0 : audioDevice.emplace_back(numCard, std::move(description));
595 0 : }
596 : }
597 :
598 0 : snd_ctl_close(handle);
599 : }
600 0 : } while (snd_card_next(&numCard) >= 0 && numCard >= 0);
601 :
602 0 : return audioDevice;
603 0 : }
604 :
605 : bool
606 96 : AlsaLayer::soundCardIndexExists(int card, AudioDeviceType stream)
607 : {
608 96 : std::string name = fmt::format("hw:{}", card);
609 :
610 : snd_ctl_t* handle;
611 96 : if (snd_ctl_open(&handle, name.c_str(), 0) != 0)
612 96 : return false;
613 :
614 : snd_pcm_info_t* pcminfo;
615 0 : snd_pcm_info_alloca(&pcminfo);
616 0 : snd_pcm_info_set_stream(pcminfo,
617 0 : stream == AudioDeviceType::PLAYBACK ? SND_PCM_STREAM_PLAYBACK : SND_PCM_STREAM_CAPTURE);
618 0 : bool ret = snd_ctl_pcm_info(handle, pcminfo) >= 0;
619 0 : snd_ctl_close(handle);
620 0 : return ret;
621 96 : }
622 :
623 : int
624 0 : AlsaLayer::getAudioDeviceIndex(const std::string& description, AudioDeviceType type) const
625 : {
626 0 : std::vector<HwIDPair> devices = getAudioDeviceIndexMap(type == AudioDeviceType::CAPTURE);
627 :
628 0 : for (const auto& dev : devices)
629 0 : if (dev.second == description)
630 0 : return dev.first;
631 :
632 : // else return the default one
633 0 : return 0;
634 0 : }
635 :
636 : std::string
637 0 : AlsaLayer::getAudioDeviceName(int index, AudioDeviceType type) const
638 : {
639 : // a bit ugly and wrong.. i do not know how to implement it better in alsalayer.
640 : // in addition, for now it is used in pulselayer only due to alsa and pulse layers api
641 : // differences. but after some tweaking in alsalayer, it could be used in it too.
642 0 : switch (type) {
643 0 : case AudioDeviceType::PLAYBACK:
644 : case AudioDeviceType::RINGTONE:
645 0 : return getPlaybackDeviceList().at(index);
646 :
647 0 : case AudioDeviceType::CAPTURE:
648 0 : return getCaptureDeviceList().at(index);
649 0 : default:
650 : // Should never happen
651 0 : JAMI_ERROR("Unexpected type");
652 0 : return "";
653 : }
654 : }
655 :
656 : void
657 15813347600 : AlsaLayer::capture()
658 : {
659 15813347600 : if (!captureHandle_ or !is_capture_running_)
660 15813347600 : return;
661 :
662 0 : snd_pcm_wait(captureHandle_, 10);
663 :
664 0 : int toGetFrames = snd_pcm_avail_update(captureHandle_);
665 0 : if (toGetFrames < 0)
666 0 : JAMI_ERROR("ALSA: Mic error: {}", snd_strerror(toGetFrames));
667 0 : if (toGetFrames <= 0)
668 0 : return;
669 :
670 0 : const int framesPerBufferAlsa = 2048;
671 0 : toGetFrames = std::min(framesPerBufferAlsa, toGetFrames);
672 0 : if (auto r = read(toGetFrames)) {
673 0 : putRecorded(std::move(r));
674 : } else
675 0 : JAMI_ERROR("ALSA MIC : Unable to read!");
676 : }
677 :
678 : void
679 15813347600 : AlsaLayer::playback()
680 : {
681 15813347600 : if (!playbackHandle_)
682 15813347600 : return;
683 :
684 0 : snd_pcm_wait(playbackHandle_, 20);
685 :
686 0 : long maxFrames = 0;
687 0 : if (not safeUpdate(playbackHandle_, maxFrames))
688 0 : return;
689 :
690 0 : if (auto toPlay = getToPlay(audioFormat_, maxFrames)) {
691 0 : write(*toPlay, playbackHandle_);
692 0 : }
693 : }
694 :
695 : void
696 15813347600 : AlsaLayer::ringtone()
697 : {
698 15813347600 : if (!ringtoneHandle_)
699 15813347600 : return;
700 :
701 0 : long ringtoneAvailFrames = 0;
702 0 : if (not safeUpdate(ringtoneHandle_, ringtoneAvailFrames))
703 0 : return;
704 :
705 0 : if (auto toRing = getToRing(audioFormat_, ringtoneAvailFrames)) {
706 0 : write(*toRing, ringtoneHandle_);
707 0 : }
708 : }
709 :
710 : // Translate an ALSA card number back to its 0-based position in the device list.
711 : static int
712 110 : cardToListIndex(const std::vector<HwIDPair>& cards, int cardNum)
713 : {
714 110 : for (int i = 0; i < static_cast<int>(cards.size()); ++i)
715 0 : if (cards[i].first == cardNum)
716 0 : return i;
717 110 : return 0;
718 : }
719 :
720 : int
721 0 : AlsaLayer::getIndexCapture() const
722 : {
723 0 : return cardToListIndex(getAudioDeviceIndexMap(true), indexIn_);
724 : }
725 :
726 : int
727 55 : AlsaLayer::getIndexPlayback() const
728 : {
729 55 : return cardToListIndex(getAudioDeviceIndexMap(false), indexOut_);
730 : }
731 :
732 : int
733 55 : AlsaLayer::getIndexRingtone() const
734 : {
735 55 : return cardToListIndex(getAudioDeviceIndexMap(false), indexRing_);
736 : }
737 :
738 : void
739 0 : AlsaLayer::updatePreference(AudioPreference& preference, int index, AudioDeviceType type)
740 : {
741 : // 'index' is a list position in getCaptureDeviceList() / getPlaybackDeviceList(),
742 : // but the preference stores the actual ALSA card number. Translate here.
743 0 : auto toCardNum = [&](bool capture) -> int {
744 0 : auto cards = getAudioDeviceIndexMap(capture);
745 0 : if (index >= 0 && index < static_cast<int>(cards.size()))
746 0 : return cards[index].first;
747 0 : return 0;
748 0 : };
749 :
750 0 : switch (type) {
751 0 : case AudioDeviceType::PLAYBACK:
752 0 : preference.setAlsaCardout(toCardNum(false));
753 0 : break;
754 :
755 0 : case AudioDeviceType::CAPTURE:
756 0 : preference.setAlsaCardin(toCardNum(true));
757 0 : break;
758 :
759 0 : case AudioDeviceType::RINGTONE:
760 0 : preference.setAlsaCardRingtone(toCardNum(false));
761 0 : break;
762 :
763 0 : default:
764 0 : break;
765 : }
766 0 : }
767 :
768 : } // namespace jami
|