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
|