LCOV - code coverage report
Current view: top level - foo/src/media/audio/pulseaudio - audiostream.cpp (source / functions) Hit Total Coverage
Test: jami-coverage-filtered.info Lines: 0 128 0.0 %
Date: 2025-12-18 10:07:43 Functions: 0 26 0.0 %

          Line data    Source code
       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 "audiostream.h"
      19             : #include "pulselayer.h"
      20             : #include "logger.h"
      21             : #include "compiler_intrinsics.h"
      22             : 
      23             : #include <string_view>
      24             : #include <stdexcept>
      25             : 
      26             : using namespace std::literals;
      27             : 
      28             : namespace jami {
      29             : 
      30           0 : AudioStream::AudioStream(pa_context* c,
      31             :                          pa_threaded_mainloop* m,
      32             :                          const char* desc,
      33             :                          AudioDeviceType type,
      34             :                          unsigned samplrate,
      35             :                          pa_sample_format_t format,
      36             :                          const PaDeviceInfos& infos,
      37             :                          bool ec,
      38             :                          OnReady onReady,
      39           0 :                          OnData onData)
      40           0 :     : onReady_(std::move(onReady))
      41           0 :     , onData_(std::move(onData))
      42           0 :     , audiostream_(nullptr)
      43           0 :     , mainloop_(m)
      44           0 :     , audioType_(type)
      45             : {
      46           0 :     pa_sample_spec sample_spec = {format, samplrate, infos.channel_map.channels};
      47             : 
      48           0 :     JAMI_DEBUG("{}: Creating stream with device {} ({}, {}Hz, {} channels)",
      49             :                desc,
      50             :                infos.name,
      51             :                pa_sample_format_to_string(sample_spec.format),
      52             :                samplrate,
      53             :                infos.channel_map.channels);
      54             : 
      55           0 :     assert(pa_sample_spec_valid(&sample_spec));
      56           0 :     assert(pa_channel_map_valid(&infos.channel_map));
      57             : 
      58           0 :     std::unique_ptr<pa_proplist, decltype(pa_proplist_free)&> pl(pa_proplist_new(), pa_proplist_free);
      59           0 :     pa_proplist_sets(pl.get(), PA_PROP_FILTER_WANT, "echo-cancel");
      60           0 :     pa_proplist_sets(pl.get(),
      61             :                      "filter.apply.echo-cancel.parameters", // needs pulseaudio >= 11.0
      62             :                      "use_volume_sharing=0"                 // share volume with master sink/source
      63             :                      " use_master_format=1"                 // use format/rate/channels from master sink/source
      64             :                      " aec_args=\""
      65             :                      "digital_gain_control=1"
      66             :                      " analog_gain_control=0"
      67             :                      " experimental_agc=1"
      68             :                      "\"");
      69             : 
      70           0 :     audiostream_ = pa_stream_new_with_proplist(c, desc, &sample_spec, &infos.channel_map, ec ? pl.get() : nullptr);
      71           0 :     if (!audiostream_) {
      72           0 :         JAMI_ERR("%s: pa_stream_new() failed : %s", desc, pa_strerror(pa_context_errno(c)));
      73           0 :         throw std::runtime_error("Unable to create stream\n");
      74             :     }
      75             : 
      76             :     pa_buffer_attr attributes;
      77           0 :     attributes.maxlength = pa_usec_to_bytes(160 * PA_USEC_PER_MSEC, &sample_spec);
      78           0 :     attributes.tlength = pa_usec_to_bytes(80 * PA_USEC_PER_MSEC, &sample_spec);
      79           0 :     attributes.prebuf = 0;
      80           0 :     attributes.fragsize = pa_usec_to_bytes(80 * PA_USEC_PER_MSEC, &sample_spec);
      81           0 :     attributes.minreq = (uint32_t) -1;
      82             : 
      83           0 :     pa_stream_set_state_callback(
      84             :         audiostream_,
      85           0 :         [](pa_stream* s, void* user_data) { static_cast<AudioStream*>(user_data)->stateChanged(s); },
      86             :         this);
      87           0 :     pa_stream_set_moved_callback(
      88           0 :         audiostream_, [](pa_stream* s, void* user_data) { static_cast<AudioStream*>(user_data)->moved(s); }, this);
      89             : 
      90           0 :     constexpr pa_stream_flags_t flags = static_cast<pa_stream_flags_t>(
      91             :         PA_STREAM_ADJUST_LATENCY | PA_STREAM_AUTO_TIMING_UPDATE | PA_STREAM_START_CORKED);
      92             : 
      93           0 :     if (type == AudioDeviceType::PLAYBACK || type == AudioDeviceType::RINGTONE) {
      94           0 :         pa_stream_set_write_callback(
      95             :             audiostream_,
      96           0 :             [](pa_stream* /*s*/, size_t bytes, void* userdata) { static_cast<AudioStream*>(userdata)->onData_(bytes); },
      97             :             this);
      98             : 
      99           0 :         pa_stream_connect_playback(audiostream_,
     100           0 :                                    infos.name.empty() ? nullptr : infos.name.c_str(),
     101             :                                    &attributes,
     102             :                                    flags,
     103             :                                    nullptr,
     104             :                                    nullptr);
     105           0 :     } else if (type == AudioDeviceType::CAPTURE) {
     106           0 :         pa_stream_set_read_callback(
     107             :             audiostream_,
     108           0 :             [](pa_stream* /*s*/, size_t bytes, void* userdata) { static_cast<AudioStream*>(userdata)->onData_(bytes); },
     109             :             this);
     110             : 
     111           0 :         pa_stream_connect_record(audiostream_, infos.name.empty() ? nullptr : infos.name.c_str(), &attributes, flags);
     112             :     }
     113           0 : }
     114             : 
     115             : void
     116           0 : disconnectStream(pa_stream* s)
     117             : {
     118             :     // make sure we don't get any further callback
     119           0 :     pa_stream_set_write_callback(s, nullptr, nullptr);
     120           0 :     pa_stream_set_read_callback(s, nullptr, nullptr);
     121           0 :     pa_stream_set_moved_callback(s, nullptr, nullptr);
     122           0 :     pa_stream_set_underflow_callback(s, nullptr, nullptr);
     123           0 :     pa_stream_set_overflow_callback(s, nullptr, nullptr);
     124           0 :     pa_stream_set_suspended_callback(s, nullptr, nullptr);
     125           0 :     pa_stream_set_started_callback(s, nullptr, nullptr);
     126           0 : }
     127             : 
     128             : void
     129           0 : destroyStream(pa_stream* s)
     130             : {
     131           0 :     pa_stream_disconnect(s);
     132           0 :     pa_stream_set_state_callback(s, nullptr, nullptr);
     133           0 :     disconnectStream(s);
     134           0 :     pa_stream_unref(s);
     135           0 : }
     136             : 
     137           0 : AudioStream::~AudioStream()
     138             : {
     139           0 :     stop();
     140           0 : }
     141             : 
     142             : void
     143           0 : AudioStream::start()
     144             : {
     145           0 :     pa_stream_cork(audiostream_, 0, nullptr, nullptr);
     146             : 
     147             :     // trigger echo cancel check
     148           0 :     moved(audiostream_);
     149           0 : }
     150             : 
     151             : void
     152           0 : AudioStream::stop()
     153             : {
     154           0 :     if (not audiostream_)
     155           0 :         return;
     156           0 :     JAMI_DBG("Destroying stream with device %s", pa_stream_get_device_name(audiostream_));
     157           0 :     if (pa_stream_get_state(audiostream_) == PA_STREAM_CREATING) {
     158           0 :         disconnectStream(audiostream_);
     159           0 :         pa_stream_set_state_callback(audiostream_, [](pa_stream* s, void*) { destroyStream(s); }, nullptr);
     160             :     } else {
     161           0 :         destroyStream(audiostream_);
     162             :     }
     163           0 :     audiostream_ = nullptr;
     164             : 
     165           0 :     std::unique_lock lock(mutex_);
     166           0 :     for (auto op : ongoing_ops)
     167           0 :         pa_operation_cancel(op);
     168             :     // wait for all operations to end
     169           0 :     cond_.wait(lock, [this] { return ongoing_ops.empty(); });
     170           0 : }
     171             : 
     172             : void
     173           0 : AudioStream::moved(pa_stream* s)
     174             : {
     175           0 :     audiostream_ = s;
     176           0 :     JAMI_LOG("[audiostream] Stream moved: {:d}, {:s}", pa_stream_get_index(s), pa_stream_get_device_name(s));
     177             : 
     178           0 :     if (audioType_ == AudioDeviceType::CAPTURE) {
     179             :         // check for echo cancel
     180           0 :         const char* name = pa_stream_get_device_name(s);
     181           0 :         if (!name) {
     182           0 :             JAMI_ERR("[audiostream] moved() unable to get audio stream device");
     183           0 :             return;
     184             :         }
     185             : 
     186           0 :         auto* op = pa_context_get_source_info_by_name(
     187             :             pa_stream_get_context(s),
     188             :             name,
     189           0 :             [](pa_context* /*c*/, const pa_source_info* i, int /*eol*/, void* userdata) {
     190           0 :                 AudioStream* thisPtr = (AudioStream*) userdata;
     191             :                 // this closure gets called twice by pulse for some reason
     192             :                 // the 2nd time, i is invalid
     193           0 :                 if (!i) {
     194             :                     // JAMI_ERROR("[audiostream] source info not found");
     195           0 :                     return;
     196             :                 }
     197           0 :                 if (!thisPtr) {
     198           0 :                     JAMI_ERROR("[audiostream] AudioStream pointer became invalid during pa_source_info_cb_t callback!");
     199           0 :                     return;
     200             :                 }
     201             : 
     202             :                 // string compare
     203           0 :                 bool usingEchoCancel = std::string_view(i->driver) == "module-echo-cancel.c"sv;
     204           0 :                 JAMI_WARNING("[audiostream] capture stream using pulse echo cancel module? {} ({})",
     205             :                              usingEchoCancel ? "yes" : "no",
     206             :                              i->name);
     207           0 :                 thisPtr->echoCancelCb(usingEchoCancel);
     208             :             },
     209           0 :             this);
     210             : 
     211           0 :         std::lock_guard lock(mutex_);
     212           0 :         pa_operation_set_state_callback(
     213           0 :             op, [](pa_operation* op, void* userdata) { static_cast<AudioStream*>(userdata)->opEnded(op); }, this);
     214           0 :         ongoing_ops.emplace(op);
     215           0 :     }
     216             : }
     217             : 
     218             : void
     219           0 : AudioStream::opEnded(pa_operation* op)
     220             : {
     221           0 :     std::lock_guard lock(mutex_);
     222           0 :     ongoing_ops.erase(op);
     223           0 :     pa_operation_unref(op);
     224           0 :     cond_.notify_all();
     225           0 : }
     226             : 
     227             : void
     228           0 : AudioStream::stateChanged(pa_stream* s)
     229             : {
     230             :     // UNUSED char str[PA_SAMPLE_SPEC_SNPRINT_MAX];
     231             : 
     232           0 :     switch (pa_stream_get_state(s)) {
     233           0 :     case PA_STREAM_CREATING:
     234           0 :         JAMI_DBG("Stream is creating…");
     235           0 :         break;
     236             : 
     237           0 :     case PA_STREAM_TERMINATED:
     238           0 :         JAMI_DBG("Stream is terminating…");
     239           0 :         break;
     240             : 
     241           0 :     case PA_STREAM_READY:
     242           0 :         JAMI_DBG("Stream successfully created, connected to %s", pa_stream_get_device_name(s));
     243             :         // JAMI_DBG("maxlength %u", pa_stream_get_buffer_attr(s)->maxlength);
     244             :         // JAMI_DBG("tlength %u", pa_stream_get_buffer_attr(s)->tlength);
     245             :         // JAMI_DBG("prebuf %u", pa_stream_get_buffer_attr(s)->prebuf);
     246             :         // JAMI_DBG("minreq %u", pa_stream_get_buffer_attr(s)->minreq);
     247             :         // JAMI_DBG("fragsize %u", pa_stream_get_buffer_attr(s)->fragsize);
     248             :         // JAMI_DBG("samplespec %s", pa_sample_spec_snprint(str, sizeof(str), pa_stream_get_sample_spec(s)));
     249           0 :         onReady_();
     250           0 :         break;
     251             : 
     252           0 :     case PA_STREAM_UNCONNECTED:
     253           0 :         JAMI_DBG("Stream unconnected");
     254           0 :         break;
     255             : 
     256           0 :     case PA_STREAM_FAILED:
     257             :     default:
     258           0 :         JAMI_ERR("Stream failure: %s", pa_strerror(pa_context_errno(pa_stream_get_context(s))));
     259           0 :         break;
     260             :     }
     261           0 : }
     262             : 
     263             : bool
     264           0 : AudioStream::isReady()
     265             : {
     266           0 :     if (!audiostream_)
     267           0 :         return false;
     268             : 
     269           0 :     return pa_stream_get_state(audiostream_) == PA_STREAM_READY;
     270             : }
     271             : 
     272             : } // namespace jami

Generated by: LCOV version 1.14