Ring Daemon 16.0.0
Loading...
Searching...
No Matches
alsalayer.cpp
Go to the documentation of this file.
1/*
2 * Copyright (C) 2004-2025 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/ring_signal.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
34namespace jami {
35
38 , indexIn_(pref.getAlsaCardin())
39 , indexOut_(pref.getAlsaCardout())
40 , indexRing_(pref.getAlsaCardRingtone())
41 , audioPlugin_(pref.getAlsaPlugin())
42{
43 setHasNativeAEC(false);
44 setHasNativeNS(false);
45}
46
48{
50 stopThread();
51
52 /* Then close the audio devices */
53 closeCaptureStream();
54 closePlaybackStream();
55 closeRingtoneStream();
56}
57
61void
63{
64 if (playbackHandle_)
65 playbackChanged(true);
66 if (captureHandle_)
67 recordChanged(true);
68
69 while (status_ == Status::Started and running_) {
70 playback();
71 ringtone();
72 capture();
73 }
74
75 playbackChanged(false);
76 recordChanged(false);
77}
78
79// Retry approach taken from pa_linux_alsa.c, part of PortAudio
80bool
81AlsaLayer::openDevice(snd_pcm_t** pcm,
82 const std::string& dev,
83 snd_pcm_stream_t stream,
84 AudioFormat& format)
85{
86 JAMI_DBG("Alsa: Opening %s device '%s'",
87 (stream == SND_PCM_STREAM_CAPTURE) ? "capture" : "playback",
88 dev.c_str());
89
90 static const int MAX_RETRIES = 10; // times of 100ms
91 int err, tries = 0;
92 do {
93 err = snd_pcm_open(pcm, dev.c_str(), stream, 0);
94 // Retry if busy, since dmix plugin may not have released the device yet
95 if (err == -EBUSY) {
96 // We're called in audioThread_ context, so if exit is requested
97 // force return now
98 std::this_thread::sleep_for(std::chrono::milliseconds(100));
99 }
100 } while (err == -EBUSY and ++tries <= MAX_RETRIES);
101
102 if (err < 0) {
103 JAMI_ERR("Alsa: Unable to open %s device %s : %s",
104 (stream == SND_PCM_STREAM_CAPTURE) ? "capture"
105 : (stream == SND_PCM_STREAM_PLAYBACK) ? "playback"
106 : "ringtone",
107 dev.c_str(),
109 return false;
110 }
111
112 if (!alsa_set_params(*pcm, format)) {
114 return false;
115 }
116
117 return true;
118}
119
120void
122{
123 std::unique_lock lk(mutex_);
125 stopThread();
126
127 bool dsnop = audioPlugin_ == PCM_DMIX_DSNOOP;
128
129 if (type == AudioDeviceType::PLAYBACK and not is_playback_open_) {
130 is_playback_open_ = openDevice(&playbackHandle_,
131 buildDeviceTopo(dsnop ? PCM_DMIX : audioPlugin_, indexOut_),
134 if (not is_playback_open_)
136
138 startPlaybackStream();
139 }
140
142 and not ringtoneHandle_) {
143 if (!openDevice(&ringtoneHandle_,
144 buildDeviceTopo(dsnop ? PCM_DMIX : audioPlugin_, indexRing_),
148 }
149
150 if (type == AudioDeviceType::CAPTURE and not is_capture_open_) {
151 is_capture_open_ = openDevice(&captureHandle_,
152 buildDeviceTopo(dsnop ? PCM_DSNOOP : audioPlugin_, indexIn_),
155
156 if (not is_capture_open_)
158 prepareCaptureStream();
159 startCaptureStream();
160 }
161
163 startThread();
164}
165
166void
168{
169 std::unique_lock lk(mutex_);
170 stopThread();
171
172 if (stream == AudioDeviceType::CAPTURE && is_capture_open_) {
173 closeCaptureStream();
174 }
175
176 if (stream == AudioDeviceType::PLAYBACK && is_playback_open_) {
177 closePlaybackStream();
178 flushUrgent();
179 flushMain();
180 }
181
182 if (stream == AudioDeviceType::RINGTONE and ringtoneHandle_) {
183 closeRingtoneStream();
184 }
185
186 if (is_capture_open_ or is_playback_open_ or ringtoneHandle_) {
187 startThread();
188 } else {
190 }
191}
192
193void
194AlsaLayer::startThread()
195{
196 running_ = true;
197 audioThread_ = std::thread(&AlsaLayer::run, this);
198}
199
200void
201AlsaLayer::stopThread()
202{
203 running_ = false;
204 if (audioThread_.joinable())
205 audioThread_.join();
206}
207
208/*
209 * GCC extension : statement expression
210 *
211 * ALSA_CALL(function_call, error_string) will:
212 * call the function
213 * display an error if the function failed
214 * return the function return value
215 */
216#define ALSA_CALL(call, error) \
217 ({ \
218 int err_code = call; \
219 if (err_code < 0) \
220 JAMI_ERR(error ": %s", snd_strerror(err_code)); \
221 err_code; \
222 })
223
224void
225AlsaLayer::stopCaptureStream()
226{
227 if (captureHandle_ && ALSA_CALL(snd_pcm_drop(captureHandle_), "Unable to stop capture") >= 0) {
228 is_capture_running_ = false;
229 is_capture_prepared_ = false;
230 }
231}
232
233void
234AlsaLayer::closeCaptureStream()
235{
236 if (is_capture_prepared_ and is_capture_running_)
237 stopCaptureStream();
238
239 JAMI_DBG("Alsa: Closing capture stream");
240 if (is_capture_open_
241 && ALSA_CALL(snd_pcm_close(captureHandle_), "Unable to close capture") >= 0) {
242 is_capture_open_ = false;
243 captureHandle_ = nullptr;
244 }
245}
246
247void
248AlsaLayer::startCaptureStream()
249{
250 if (captureHandle_ and not is_capture_running_)
251 if (ALSA_CALL(snd_pcm_start(captureHandle_), "Unable to start capture") >= 0)
252 is_capture_running_ = true;
253}
254
255void
256AlsaLayer::stopPlaybackStream()
257{
258 if (playbackHandle_ and is_playback_running_) {
259 if (ALSA_CALL(snd_pcm_drop(playbackHandle_), "Unable to stop playback") >= 0) {
260 is_playback_running_ = false;
261 }
262 }
263}
264
265void
266AlsaLayer::closePlaybackStream()
267{
268 if (is_playback_running_)
269 stopPlaybackStream();
270
271 if (is_playback_open_) {
272 JAMI_DBG("Alsa: Closing playback stream");
273 if (ALSA_CALL(snd_pcm_close(playbackHandle_), "Coulnd't close playback") >= 0)
274 is_playback_open_ = false;
275 playbackHandle_ = nullptr;
276 }
277}
278
279void
280AlsaLayer::closeRingtoneStream()
281{
282 if (ringtoneHandle_) {
283 ALSA_CALL(snd_pcm_drop(ringtoneHandle_), "Unable to stop ringtone");
284 ALSA_CALL(snd_pcm_close(ringtoneHandle_), "Unable to close ringtone");
285 ringtoneHandle_ = nullptr;
286 }
287}
288
289void
290AlsaLayer::startPlaybackStream()
291{
292 is_playback_running_ = true;
293}
294
295void
296AlsaLayer::prepareCaptureStream()
297{
298 if (is_capture_open_ and not is_capture_prepared_)
299 if (ALSA_CALL(snd_pcm_prepare(captureHandle_), "Unable to prepare capture") >= 0)
300 is_capture_prepared_ = true;
301}
302
303bool
304AlsaLayer::alsa_set_params(snd_pcm_t* pcm_handle, AudioFormat& format)
305{
306#define TRY(call, error) \
307 do { \
308 if (ALSA_CALL(call, error) < 0) \
309 return false; \
310 } while (0)
311
314
315 const unsigned RING_ALSA_PERIOD_SIZE = 160;
316 const unsigned RING_ALSA_NB_PERIOD = 8;
318
321 unsigned int periods = RING_ALSA_NB_PERIOD;
322
327
328#define HW pcm_handle, hwparams /* hardware parameters */
329 TRY(snd_pcm_hw_params_any(HW), "hwparams init");
330
333
335 "hardware sample rate"); /* prevent software resampling */
336 TRY(snd_pcm_hw_params_set_rate_near(HW, &format.sample_rate, nullptr), "sample rate");
337
338 // TODO: use snd_pcm_query_chmaps or similar to get hardware channel num
340 format.nb_channels = 2;
341 TRY(snd_pcm_hw_params_set_channels_near(HW, &format.nb_channels), "channel count");
342
347 JAMI_DBG("Buffer size range from %lu to %lu", buffer_size_min, buffer_size_max);
348 JAMI_DBG("Period size range from %lu to %lu", period_size_min, period_size_max);
353
355 "Unable to set buffer size for playback");
357 "Unable to set period size for playback");
359 "Unable to set number of periods for playback");
360 TRY(snd_pcm_hw_params(HW), "hwparams");
361
364 snd_pcm_hw_params_get_rate(hwparams, &format.sample_rate, nullptr);
365 snd_pcm_hw_params_get_channels(hwparams, &format.nb_channels);
366 JAMI_DBG("Was set period_size = %lu", period_size);
367 JAMI_DBG("Was set buffer_size = %lu", buffer_size);
368
369 if (2 * period_size > buffer_size) {
370 JAMI_ERR("buffer to small, unable to use");
371 return false;
372 }
373
374#undef HW
375
376 JAMI_DBG("%s using format %s",
377 (snd_pcm_stream(pcm_handle) == SND_PCM_STREAM_PLAYBACK) ? "playback" : "capture",
378 format.toString().c_str());
379
380 snd_pcm_sw_params_t* swparams = nullptr;
382
383#define SW pcm_handle, swparams /* software parameters */
386 TRY(snd_pcm_sw_params(SW), "sw parameters");
387#undef SW
388
389 return true;
390
391#undef TRY
392}
393
394// TODO first frame causes broken pipe (underrun) because not enough data is sent
395// we should wait until the handle is ready
396void
397AlsaLayer::write(const AudioFrame& buffer, snd_pcm_t* handle)
398{
399 int err = snd_pcm_writei(handle,
400 (const void*) buffer.pointer()->data[0],
401 buffer.pointer()->nb_samples);
402
403 if (err < 0)
404 snd_pcm_recover(handle, err, 0);
405
406 if (err >= 0)
407 return;
408
409 switch (err) {
410 case -EPIPE:
411 case -ESTRPIPE:
412 case -EIO: {
413 snd_pcm_status_t* status;
414 snd_pcm_status_alloca(&status);
415
416 if (ALSA_CALL(snd_pcm_status(handle, status), "Unable to get playback handle status") >= 0)
418 stopPlaybackStream();
419 startPlaybackStream();
420 }
421
423 (const void*) buffer.pointer()->data[0],
424 buffer.pointer()->nb_samples),
425 "XRUN handling failed");
426 break;
427 }
428
429 case -EBADFD: {
430 snd_pcm_status_t* status;
431 snd_pcm_status_alloca(&status);
432
433 if (ALSA_CALL(snd_pcm_status(handle, status), "Unable to get playback handle status") >= 0) {
435 JAMI_ERR("Writing in state SND_PCM_STATE_SETUP, should be "
436 "SND_PCM_STATE_PREPARED or SND_PCM_STATE_RUNNING");
437 int error = snd_pcm_prepare(handle);
438
439 if (error < 0) {
440 JAMI_ERR("Failed to prepare handle: %s", snd_strerror(error));
441 stopPlaybackStream();
442 }
443 }
444 }
445
446 break;
447 }
448
449 default:
450 JAMI_ERR("Unknown write error, dropping frames: %s", snd_strerror(err));
451 stopPlaybackStream();
452 break;
453 }
454}
455
456std::unique_ptr<AudioFrame>
457AlsaLayer::read(unsigned frames)
458{
459 if (snd_pcm_state(captureHandle_) == SND_PCM_STATE_XRUN) {
460 prepareCaptureStream();
461 startCaptureStream();
462 }
463
464 auto ret = std::make_unique<AudioFrame>(audioInputFormat_, frames);
465 int err = snd_pcm_readi(captureHandle_, ret->pointer()->data[0], frames);
466
467 if (err >= 0) {
468 ret->pointer()->nb_samples = err;
469 return ret;
470 }
471
472 switch (err) {
473 case -EPIPE:
474 case -ESTRPIPE:
475 case -EIO: {
476 snd_pcm_status_t* status;
477 snd_pcm_status_alloca(&status);
478
479 if (ALSA_CALL(snd_pcm_status(captureHandle_, status), "Get status failed") >= 0)
481 stopCaptureStream();
482 prepareCaptureStream();
483 startCaptureStream();
484 }
485
486 JAMI_ERR("XRUN capture ignored (%s)", snd_strerror(err));
487 break;
488 }
489
490 case -EPERM:
491 JAMI_ERR("Unable to capture, EPERM (%s)", snd_strerror(err));
492 prepareCaptureStream();
493 startCaptureStream();
494 break;
495 }
496
497 return {};
498}
499
500std::string
501AlsaLayer::buildDeviceTopo(const std::string& plugin, int card)
502{
503 if (plugin == PCM_DEFAULT)
504 return plugin;
505
506 return fmt::format("{}:{}", plugin, card);
507}
508
509static bool
510safeUpdate(snd_pcm_t* handle, long& samples)
511{
512 samples = snd_pcm_avail_update(handle);
513
514 if (samples < 0) {
515 samples = snd_pcm_recover(handle, samples, 0);
516
517 if (samples < 0) {
518 JAMI_ERR("Got unrecoverable error from snd_pcm_avail_update: %s", snd_strerror(samples));
519 return false;
520 }
521 }
522
523 return true;
524}
525
526static std::vector<std::string>
527getValues(const std::vector<HwIDPair>& deviceMap)
528{
529 std::vector<std::string> audioDeviceList;
530 audioDeviceList.reserve(deviceMap.size());
531
532 for (const auto& dev : deviceMap)
533 audioDeviceList.push_back(dev.second);
534
535 return audioDeviceList;
536}
537
538std::vector<std::string>
540{
541 return getValues(getAudioDeviceIndexMap(true));
542}
543
544std::vector<std::string>
546{
547 return getValues(getAudioDeviceIndexMap(false));
548}
549
550std::vector<HwIDPair>
551AlsaLayer::getAudioDeviceIndexMap(bool getCapture) const
552{
553 snd_ctl_t* handle;
558
559 int numCard = -1;
560
561 std::vector<HwIDPair> audioDevice;
562
563 if (snd_card_next(&numCard) < 0 || numCard < 0)
564 return audioDevice;
565
566 do {
567 std::string name = fmt::format("hw:{}", numCard);
568
569 if (snd_ctl_open(&handle, name.c_str(), 0) == 0) {
570 if (snd_ctl_card_info(handle, info) == 0) {
575
576 int err;
577 if ((err = snd_ctl_pcm_info(handle, pcminfo)) < 0) {
578 JAMI_WARN("Unable to get info for %s %s: %s",
579 getCapture ? "capture device" : "playback device",
580 name.c_str(),
582 } else {
583 JAMI_DBG("card %i : %s [%s]",
584 numCard,
587 std::string description = snd_ctl_card_info_get_name(info);
588 description.append(" - ");
589 description.append(snd_pcm_info_get_name(pcminfo));
590
591 // The number of the sound card is associated with a string description
592 audioDevice.emplace_back(numCard, std::move(description));
593 }
594 }
595
596 snd_ctl_close(handle);
597 }
598 } while (snd_card_next(&numCard) >= 0 && numCard >= 0);
599
600 return audioDevice;
601}
602
603bool
605{
606 std::string name = fmt::format("hw:{}", card);
607
608 snd_ctl_t* handle;
609 if (snd_ctl_open(&handle, name.c_str(), 0) != 0)
610 return false;
611
617 bool ret = snd_ctl_pcm_info(handle, pcminfo) >= 0;
618 snd_ctl_close(handle);
619 return ret;
620}
621
622int
623AlsaLayer::getAudioDeviceIndex(const std::string& description, AudioDeviceType type) const
624{
625 std::vector<HwIDPair> devices = getAudioDeviceIndexMap(type == AudioDeviceType::CAPTURE);
626
627 for (const auto& dev : devices)
628 if (dev.second == description)
629 return dev.first;
630
631 // else return the default one
632 return 0;
633}
634
635std::string
637{
638 // a bit ugly and wrong.. i do not know how to implement it better in alsalayer.
639 // in addition, for now it is used in pulselayer only due to alsa and pulse layers api
640 // differences. but after some tweaking in alsalayer, it could be used in it too.
641 switch (type) {
644 return getPlaybackDeviceList().at(index);
645
647 return getCaptureDeviceList().at(index);
648 default:
649 // Should never happen
650 JAMI_ERR("Unexpected type");
651 return "";
652 }
653}
654
655void
657{
658 if (!captureHandle_ or !is_capture_running_)
659 return;
660
661 snd_pcm_wait(captureHandle_, 10);
662
663 int toGetFrames = snd_pcm_avail_update(captureHandle_);
664 if (toGetFrames < 0)
665 JAMI_ERR("Audio: Mic error: %s", snd_strerror(toGetFrames));
666 if (toGetFrames <= 0)
667 return;
668
669 const int framesPerBufferAlsa = 2048;
671 if (auto r = read(toGetFrames)) {
672 putRecorded(std::move(r));
673 } else
674 JAMI_ERR("ALSA MIC : Unable to read!");
675}
676
677void
679{
680 if (!playbackHandle_)
681 return;
682
683 snd_pcm_wait(playbackHandle_, 20);
684
685 long maxFrames = 0;
686 if (not safeUpdate(playbackHandle_, maxFrames))
687 return;
688
689 if (auto toPlay = getToPlay(audioFormat_, maxFrames)) {
690 write(*toPlay, playbackHandle_);
691 }
692}
693
694void
696{
697 if (!ringtoneHandle_)
698 return;
699
700 long ringtoneAvailFrames = 0;
701 if (not safeUpdate(ringtoneHandle_, ringtoneAvailFrames))
702 return;
703
705 write(*toRing, ringtoneHandle_);
706 }
707}
708
709void
710AlsaLayer::updatePreference(AudioPreference& preference, int index, AudioDeviceType type)
711{
712 switch (type) {
714 preference.setAlsaCardout(index);
715 break;
716
718 preference.setAlsaCardin(index);
719 break;
720
722 preference.setAlsaCardRingtone(index);
723 break;
724
725 default:
726 break;
727 }
728}
729
730} // namespace jami
#define SW
#define HW
#define ALSA_CALL(call, error)
#define TRY(call, error)
#define ALSA_CAPTURE_DEVICE
Definition alsalayer.h:30
#define ALSA_PLAYBACK_DEVICE
Definition alsalayer.h:31
#define PCM_DMIX
Definition alsalayer.h:27
#define PCM_DMIX_DSNOOP
Definition audiolayer.h:51
#define PCM_DEFAULT
Definition audiolayer.h:49
#define PCM_DSNOOP
Definition audiolayer.h:50
Loop on a sound file.
virtual int getIndexPlayback() const
Get the index of the audio card for playback.
Definition alsalayer.h:126
std::string getAudioDeviceName(int index, AudioDeviceType type) const
AlsaLayer(const AudioPreference &pref)
Constructor.
Definition alsalayer.cpp:36
~AlsaLayer()
Destructor.
Definition alsalayer.cpp:47
std::string buildDeviceTopo(const std::string &plugin, int card)
Concatenate two strings.
int getAudioDeviceIndex(const std::string &description, AudioDeviceType type) const
An index is associated with its string description.
virtual std::vector< std::string > getCaptureDeviceList() const
Scan the sound card available on the system.
virtual std::vector< std::string > getPlaybackDeviceList() const
virtual int getIndexRingtone() const
Get the index of the audio card for ringtone (could be differnet from playback)
Definition alsalayer.h:133
void run()
Reimplementation of run()
Definition alsalayer.cpp:62
static bool soundCardIndexExists(int card, AudioDeviceType stream)
Check if the given index corresponds to an existing sound card and supports the specified streaming m...
virtual void startStream(AudioDeviceType stream=AudioDeviceType::ALL)
Start the capture stream and prepare the playback stream.
virtual void stopStream(AudioDeviceType stream=AudioDeviceType::ALL)
Stop the playback and capture streams.
std::mutex mutex_
Lock for the entire audio layer.
Definition audiolayer.h:283
std::atomic< Status > status_
Whether or not the audio layer's playback stream is started.
Definition audiolayer.h:260
void playbackChanged(bool started)
void setHasNativeNS(bool hasNS)
void hardwareFormatAvailable(AudioFormat playback, size_t bufSize=0)
Callback to be called by derived classes when the audio output is opened.
std::shared_ptr< AudioFrame > getToRing(AudioFormat format, size_t writableSamples)
void flushUrgent()
Flush urgent buffer.
void setHasNativeAEC(bool hasEAC)
std::shared_ptr< AudioFrame > getToPlay(AudioFormat format, size_t writableSamples)
void recordChanged(bool started)
AudioFormat audioFormat_
Sample Rate that should be sent to the sound card.
Definition audiolayer.h:266
void flushMain()
Flush main buffer.
AudioFormat audioInputFormat_
Sample Rate for input.
Definition audiolayer.h:271
void putRecorded(std::shared_ptr< AudioFrame > &&frame)
AudioFormat getFormat() const
Get the audio format of the layer (sample rate & channel number).
Definition audiolayer.h:178
#define JAMI_ERR(...)
Definition logger.h:218
#define JAMI_DBG(...)
Definition logger.h:216
#define JAMI_WARN(...)
Definition logger.h:217
Definition Address.h:25
static constexpr std::string_view toString(AuthDecodingState state)
void emitSignal(Args... args)
Definition ring_signal.h:64
static bool safeUpdate(snd_pcm_t *handle, long &samples)
libjami::AudioFrame AudioFrame
static std::vector< std::string > getValues(const std::vector< HwIDPair > &deviceMap)
AudioDeviceType
Definition audiolayer.h:58
Simple macro to hide class' copy constructor and assignment operator.
Structure to hold sample rate and channel number associated with audio data.