Ring Daemon
Loading...
Searching...
No Matches
alsalayer.cpp
Go to the documentation of this file.
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"
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, const std::string& dev, snd_pcm_stream_t stream, AudioFormat& format)
82{
83 JAMI_DBG("Alsa: Opening %s device '%s'", (stream == SND_PCM_STREAM_CAPTURE) ? "capture" : "playback", dev.c_str());
84
85 static const int MAX_RETRIES = 10; // times of 100ms
86 int err, tries = 0;
87 do {
88 err = snd_pcm_open(pcm, dev.c_str(), stream, 0);
89 // Retry if busy, since dmix plugin may not have released the device yet
90 if (err == -EBUSY) {
91 // We're called in audioThread_ context, so if exit is requested
92 // force return now
93 std::this_thread::sleep_for(std::chrono::milliseconds(100));
94 }
95 } while (err == -EBUSY and ++tries <= MAX_RETRIES);
96
97 if (err < 0) {
98 JAMI_ERR("Alsa: Unable to open %s device %s : %s",
99 (stream == SND_PCM_STREAM_CAPTURE) ? "capture"
100 : (stream == SND_PCM_STREAM_PLAYBACK) ? "playback"
101 : "ringtone",
102 dev.c_str(),
104 return false;
105 }
106
107 if (!alsa_set_params(*pcm, format)) {
109 return false;
110 }
111
112 return true;
113}
114
115void
117{
118 std::unique_lock lk(mutex_);
120 stopThread();
121
122 bool dsnop = audioPlugin_ == PCM_DMIX_DSNOOP;
123
124 if (type == AudioDeviceType::PLAYBACK and not is_playback_open_) {
125 is_playback_open_ = openDevice(&playbackHandle_,
126 buildDeviceTopo(dsnop ? PCM_DMIX : audioPlugin_, indexOut_),
129 if (not is_playback_open_)
131
133 startPlaybackStream();
134 }
135
136 if (type == AudioDeviceType::RINGTONE and getIndexPlayback() != getIndexRingtone() and not ringtoneHandle_) {
137 if (!openDevice(&ringtoneHandle_,
138 buildDeviceTopo(dsnop ? PCM_DMIX : audioPlugin_, indexRing_),
142 }
143
144 if (type == AudioDeviceType::CAPTURE and not is_capture_open_) {
145 is_capture_open_ = openDevice(&captureHandle_,
146 buildDeviceTopo(dsnop ? PCM_DSNOOP : audioPlugin_, indexIn_),
149
150 if (not is_capture_open_)
152 prepareCaptureStream();
153 startCaptureStream();
154 }
155
157 startThread();
158}
159
160void
162{
163 std::unique_lock lk(mutex_);
164 stopThread();
165
166 if (stream == AudioDeviceType::CAPTURE && is_capture_open_) {
167 closeCaptureStream();
168 }
169
170 if (stream == AudioDeviceType::PLAYBACK && is_playback_open_) {
171 closePlaybackStream();
172 flushUrgent();
173 flushMain();
174 }
175
176 if (stream == AudioDeviceType::RINGTONE and ringtoneHandle_) {
177 closeRingtoneStream();
178 }
179
180 if (is_capture_open_ or is_playback_open_ or ringtoneHandle_) {
181 startThread();
182 } else {
184 }
185}
186
187void
188AlsaLayer::startThread()
189{
190 running_ = true;
191 audioThread_ = std::thread(&AlsaLayer::run, this);
192}
193
194void
195AlsaLayer::stopThread()
196{
197 running_ = false;
198 if (audioThread_.joinable())
199 audioThread_.join();
200}
201
202/*
203 * GCC extension : statement expression
204 *
205 * ALSA_CALL(function_call, error_string) will:
206 * call the function
207 * display an error if the function failed
208 * return the function return value
209 */
210#define ALSA_CALL(call, error) \
211 ({ \
212 int err_code = call; \
213 if (err_code < 0) \
214 JAMI_ERR(error ": %s", snd_strerror(err_code)); \
215 err_code; \
216 })
217
218void
219AlsaLayer::stopCaptureStream()
220{
221 if (captureHandle_ && ALSA_CALL(snd_pcm_drop(captureHandle_), "Unable to stop capture") >= 0) {
222 is_capture_running_ = false;
223 is_capture_prepared_ = false;
224 }
225}
226
227void
228AlsaLayer::closeCaptureStream()
229{
230 if (is_capture_prepared_ and is_capture_running_)
231 stopCaptureStream();
232
233 JAMI_DBG("Alsa: Closing capture stream");
234 if (is_capture_open_ && ALSA_CALL(snd_pcm_close(captureHandle_), "Unable to close capture") >= 0) {
235 is_capture_open_ = false;
236 captureHandle_ = nullptr;
237 }
238}
239
240void
241AlsaLayer::startCaptureStream()
242{
243 if (captureHandle_ and not is_capture_running_)
244 if (ALSA_CALL(snd_pcm_start(captureHandle_), "Unable to start capture") >= 0)
245 is_capture_running_ = true;
246}
247
248void
249AlsaLayer::stopPlaybackStream()
250{
251 if (playbackHandle_ and is_playback_running_) {
252 if (ALSA_CALL(snd_pcm_drop(playbackHandle_), "Unable to stop playback") >= 0) {
253 is_playback_running_ = false;
254 }
255 }
256}
257
258void
259AlsaLayer::closePlaybackStream()
260{
261 if (is_playback_running_)
262 stopPlaybackStream();
263
264 if (is_playback_open_) {
265 JAMI_DBG("Alsa: Closing playback stream");
266 if (ALSA_CALL(snd_pcm_close(playbackHandle_), "Coulnd't close playback") >= 0)
267 is_playback_open_ = false;
268 playbackHandle_ = nullptr;
269 }
270}
271
272void
273AlsaLayer::closeRingtoneStream()
274{
275 if (ringtoneHandle_) {
276 ALSA_CALL(snd_pcm_drop(ringtoneHandle_), "Unable to stop ringtone");
277 ALSA_CALL(snd_pcm_close(ringtoneHandle_), "Unable to close ringtone");
278 ringtoneHandle_ = nullptr;
279 }
280}
281
282void
283AlsaLayer::startPlaybackStream()
284{
285 is_playback_running_ = true;
286}
287
288void
289AlsaLayer::prepareCaptureStream()
290{
291 if (is_capture_open_ and not is_capture_prepared_)
292 if (ALSA_CALL(snd_pcm_prepare(captureHandle_), "Unable to prepare capture") >= 0)
293 is_capture_prepared_ = true;
294}
295
296bool
297AlsaLayer::alsa_set_params(snd_pcm_t* pcm_handle, AudioFormat& format)
298{
299#define TRY(call, error) \
300 do { \
301 if (ALSA_CALL(call, error) < 0) \
302 return false; \
303 } while (0)
304
307
308 const unsigned RING_ALSA_PERIOD_SIZE = 160;
309 const unsigned RING_ALSA_NB_PERIOD = 8;
311
314 unsigned int periods = RING_ALSA_NB_PERIOD;
315
320
321#define HW pcm_handle, hwparams /* hardware parameters */
322 TRY(snd_pcm_hw_params_any(HW), "hwparams init");
323
325
328 format = format.withSampleFormat(AV_SAMPLE_FMT_FLT);
329 } else {
331 format = format.withSampleFormat(AV_SAMPLE_FMT_S16);
332 }
333
334 TRY(snd_pcm_hw_params_set_rate_resample(HW, 0), "hardware sample rate"); /* prevent software resampling */
335 TRY(snd_pcm_hw_params_set_rate_near(HW, &format.sample_rate, nullptr), "sample rate");
336
337 // Query the max number of channels supported by the hardware
338 unsigned int max_channels = 0;
340 JAMI_WARN("Unable to query hardware channel number, defaulting to 2");
341 max_channels = 2;
342 }
343
344 format.nb_channels = std::min(max_channels, 2u);
345 if (format.nb_channels == 0)
346 format.nb_channels = 2;
347
348 TRY(snd_pcm_hw_params_set_channels_near(HW, &format.nb_channels), "channel count");
349
354 JAMI_DBG("Buffer size range from %lu to %lu", buffer_size_min, buffer_size_max);
355 JAMI_DBG("Period size range from %lu to %lu", period_size_min, period_size_max);
360
361 TRY(snd_pcm_hw_params_set_buffer_size_near(HW, &buffer_size), "Unable to set buffer size for playback");
362 TRY(snd_pcm_hw_params_set_period_size_near(HW, &period_size, nullptr), "Unable to set period size for playback");
363 TRY(snd_pcm_hw_params_set_periods_near(HW, &periods, nullptr), "Unable to set number of periods for playback");
364 TRY(snd_pcm_hw_params(HW), "hwparams");
365
368 snd_pcm_hw_params_get_rate(hwparams, &format.sample_rate, nullptr);
369 snd_pcm_hw_params_get_channels(hwparams, &format.nb_channels);
370 JAMI_DBG("Was set period_size = %lu", period_size);
371 JAMI_DBG("Was set buffer_size = %lu", buffer_size);
372
373 if (2 * period_size > buffer_size) {
374 JAMI_ERR("buffer to small, unable to use");
375 return false;
376 }
377
378#undef HW
379
380 JAMI_DBG("%s using format %s",
381 (snd_pcm_stream(pcm_handle) == SND_PCM_STREAM_PLAYBACK) ? "playback" : "capture",
382 format.toString().c_str());
383
384 snd_pcm_sw_params_t* swparams = nullptr;
386
387#define SW pcm_handle, swparams /* software parameters */
390 TRY(snd_pcm_sw_params(SW), "sw parameters");
391#undef SW
392
393 return true;
394
395#undef TRY
396}
397
398// TODO first frame causes broken pipe (underrun) because not enough data is sent
399// we should wait until the handle is ready
400void
401AlsaLayer::write(const AudioFrame& buffer, snd_pcm_t* handle)
402{
403 int err = snd_pcm_writei(handle, (const void*) buffer.pointer()->data[0], buffer.pointer()->nb_samples);
404
405 if (err < 0)
406 snd_pcm_recover(handle, err, 0);
407
408 if (err >= 0)
409 return;
410
411 switch (err) {
412 case -EPIPE:
413 case -ESTRPIPE:
414 case -EIO: {
415 snd_pcm_status_t* status;
416 snd_pcm_status_alloca(&status);
417
418 if (ALSA_CALL(snd_pcm_status(handle, status), "Unable to get playback handle status") >= 0)
420 stopPlaybackStream();
421 startPlaybackStream();
422 }
423
424 ALSA_CALL(snd_pcm_writei(handle, (const void*) buffer.pointer()->data[0], 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) {
573
574 int err;
575 if ((err = snd_ctl_pcm_info(handle, pcminfo)) < 0) {
576 JAMI_WARN("Unable to get info for %s %s: %s",
577 getCapture ? "capture device" : "playback device",
578 name.c_str(),
580 } else {
581 JAMI_DBG("card %i : %s [%s]",
582 numCard,
585 std::string description = snd_ctl_card_info_get_name(info);
586 description.append(" - ");
587 description.append(snd_pcm_info_get_name(pcminfo));
588
589 // The number of the sound card is associated with a string description
590 audioDevice.emplace_back(numCard, std::move(description));
591 }
592 }
593
594 snd_ctl_close(handle);
595 }
596 } while (snd_card_next(&numCard) >= 0 && numCard >= 0);
597
598 return audioDevice;
599}
600
601bool
603{
604 std::string name = fmt::format("hw:{}", card);
605
606 snd_ctl_t* handle;
607 if (snd_ctl_open(&handle, name.c_str(), 0) != 0)
608 return false;
609
614 bool ret = snd_ctl_pcm_info(handle, pcminfo) >= 0;
615 snd_ctl_close(handle);
616 return ret;
617}
618
619int
620AlsaLayer::getAudioDeviceIndex(const std::string& description, AudioDeviceType type) const
621{
622 std::vector<HwIDPair> devices = getAudioDeviceIndexMap(type == AudioDeviceType::CAPTURE);
623
624 for (const auto& dev : devices)
625 if (dev.second == description)
626 return dev.first;
627
628 // else return the default one
629 return 0;
630}
631
632std::string
634{
635 // a bit ugly and wrong.. i do not know how to implement it better in alsalayer.
636 // in addition, for now it is used in pulselayer only due to alsa and pulse layers api
637 // differences. but after some tweaking in alsalayer, it could be used in it too.
638 switch (type) {
641 return getPlaybackDeviceList().at(index);
642
644 return getCaptureDeviceList().at(index);
645 default:
646 // Should never happen
647 JAMI_ERR("Unexpected type");
648 return "";
649 }
650}
651
652void
654{
655 if (!captureHandle_ or !is_capture_running_)
656 return;
657
658 snd_pcm_wait(captureHandle_, 10);
659
660 int toGetFrames = snd_pcm_avail_update(captureHandle_);
661 if (toGetFrames < 0)
662 JAMI_ERR("Audio: Mic error: %s", snd_strerror(toGetFrames));
663 if (toGetFrames <= 0)
664 return;
665
666 const int framesPerBufferAlsa = 2048;
668 if (auto r = read(toGetFrames)) {
669 putRecorded(std::move(r));
670 } else
671 JAMI_ERR("ALSA MIC : Unable to read!");
672}
673
674void
676{
677 if (!playbackHandle_)
678 return;
679
680 snd_pcm_wait(playbackHandle_, 20);
681
682 long maxFrames = 0;
683 if (not safeUpdate(playbackHandle_, maxFrames))
684 return;
685
686 if (auto toPlay = getToPlay(audioFormat_, maxFrames)) {
687 write(*toPlay, playbackHandle_);
688 }
689}
690
691void
693{
694 if (!ringtoneHandle_)
695 return;
696
697 long ringtoneAvailFrames = 0;
698 if (not safeUpdate(ringtoneHandle_, ringtoneAvailFrames))
699 return;
700
702 write(*toRing, ringtoneHandle_);
703 }
704}
705
706void
707AlsaLayer::updatePreference(AudioPreference& preference, int index, AudioDeviceType type)
708{
709 switch (type) {
711 preference.setAlsaCardout(index);
712 break;
713
715 preference.setAlsaCardin(index);
716 break;
717
719 preference.setAlsaCardRingtone(index);
720 break;
721
722 default:
723 break;
724 }
725}
726
727} // 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:50
#define PCM_DEFAULT
Definition audiolayer.h:48
#define PCM_DSNOOP
Definition audiolayer.h:49
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 stopStream(AudioDeviceType stream=AudioDeviceType::ALL)
Stop the playback and capture streams.
virtual void startStream(AudioDeviceType stream)
Start the capture stream and prepare the playback stream.
std::mutex mutex_
Lock for the entire audio layer.
Definition audiolayer.h:296
std::atomic< Status > status_
Whether or not the audio layer's playback stream is started.
Definition audiolayer.h:273
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:279
void flushMain()
Flush main buffer.
AudioFormat audioInputFormat_
Sample Rate for input.
Definition audiolayer.h:284
void putRecorded(std::shared_ptr< AudioFrame > &&frame)
AudioFormat getFormat() const
Get the audio format of the layer (sample rate & channel number).
Definition audiolayer.h:191
#define JAMI_ERR(...)
Definition logger.h:230
#define JAMI_DBG(...)
Definition logger.h:228
#define JAMI_WARN(...)
Definition logger.h:229
Definition Address.h:25
static constexpr std::string_view toString(AuthDecodingState state)
void emitSignal(Args... args)
Definition jami_signal.h:64
static bool safeUpdate(snd_pcm_t *handle, long &samples)
AudioDeviceType
Definition audiolayer.h:57
libjami::AudioFrame AudioFrame
static std::vector< std::string > getValues(const std::vector< HwIDPair > &deviceMap)
Simple macro to hide class' copy constructor and assignment operator.
Structure to hold sample rate and channel number associated with audio data.