Line data Source code
1 : #include "audio/backend/portaudio_backend.h"
2 :
3 : #include <algorithm>
4 : #include <cctype>
5 : #include <cstring>
6 : #include <iostream>
7 : #include <vector>
8 :
9 : #include "audio/backend/audio_backend_portaudio_helpers.h"
10 : #include "audio/engine/i_audio_engine.h"
11 : #ifdef _WIN32
12 : #include <pa_win_wasapi.h>
13 : #endif
14 :
15 : namespace Amplitron {
16 :
17 : // -----------------------------------------------------------------------------
18 : // Helper functions — promoted to non-static for use across TUs
19 : // -----------------------------------------------------------------------------
20 :
21 791 : bool is_usb_device_name(const std::string& name) {
22 791 : std::string lower = name;
23 791 : std::transform(lower.begin(), lower.end(), lower.begin(),
24 18035 : [](unsigned char c) { return std::tolower(c); });
25 :
26 12 : static const char* usb_keywords[] = {
27 : "usb", "guitar", "guitar link", "irig", "scarlett",
28 : "behringer", "focusrite", "presonus", "steinberg", "audio interface",
29 : "line 6", "rocksmith", "umc", "um2", "uphoria",
30 : "podcast", "xenyx"};
31 :
32 14076 : for (const auto* keyword : usb_keywords) {
33 13295 : if (lower.find(keyword) != std::string::npos) {
34 5 : return true;
35 : }
36 : }
37 774 : return false;
38 791 : }
39 :
40 1745 : int get_host_api_priority(int host_api_type) {
41 1745 : auto type = static_cast<PaHostApiTypeId>(host_api_type);
42 : #if defined(__linux__)
43 484 : switch (type) {
44 0 : case paJACK:
45 0 : return 100;
46 242 : case paALSA:
47 242 : return 70;
48 242 : default:
49 242 : return 10;
50 : }
51 : #elif defined(_WIN32)
52 5 : switch (type) {
53 : case paASIO:
54 : return 100;
55 : case paWASAPI:
56 : return 90;
57 : case paDirectSound:
58 : return 40;
59 : case paMME:
60 : return 10;
61 : default:
62 : return 20;
63 : }
64 : #elif defined(__APPLE__)
65 257 : switch (type) {
66 : case paCoreAudio:
67 253 : return 100;
68 : default:
69 4 : return 30;
70 : }
71 : #else
72 : (void)type;
73 : return 30;
74 : #endif
75 257 : }
76 :
77 763 : bool is_projector_or_hdmi(const std::string& name) {
78 763 : std::string lower = name;
79 763 : std::transform(lower.begin(), lower.end(), lower.begin(),
80 17401 : [](unsigned char c) { return std::tolower(c); });
81 770 : return lower.find("epson") != std::string::npos ||
82 768 : lower.find("projector") != std::string::npos ||
83 775 : lower.find("hdmi") != std::string::npos ||
84 767 : lower.find("displayport") != std::string::npos;
85 763 : }
86 :
87 2 : bool devices_share_host_api(int input_dev, int output_dev) {
88 2 : const PaDeviceInfo* in_info = Pa_GetDeviceInfo(input_dev);
89 2 : const PaDeviceInfo* out_info = Pa_GetDeviceInfo(output_dev);
90 2 : if (!in_info || !out_info) return false;
91 0 : return in_info->hostApi == out_info->hostApi;
92 1 : }
93 :
94 : // -----------------------------------------------------------------------------
95 :
96 : // Real callback implementation
97 1190 : int pa_audio_callback(const void* input, void* output, unsigned long frame_count,
98 : const PaStreamCallbackTimeInfo* /*time_info*/,
99 : PaStreamCallbackFlags /*status_flags*/, void* user_data) {
100 1190 : auto* engine = static_cast<IAudioEngine*>(user_data);
101 1190 : const auto* in = static_cast<const float*>(input);
102 1190 : auto* out = static_cast<float*>(output);
103 :
104 1190 : if (!in || !out) {
105 4 : if (out) std::memset(out, 0, frame_count * 2 * sizeof(float));
106 4 : return paContinue;
107 : }
108 :
109 1186 : engine->process_audio(in, out, static_cast<int>(frame_count));
110 1186 : return paContinue;
111 1187 : }
112 :
113 1090 : PortAudioBackend::PortAudioBackend() = default;
114 :
115 1629 : PortAudioBackend::~PortAudioBackend() { shutdown(); }
116 :
117 756 : bool PortAudioBackend::initialize(IAudioEngine* engine) {
118 756 : if (initialized_) return true;
119 756 : engine_ = engine;
120 :
121 756 : PaError err = Pa_Initialize();
122 756 : if (err != paNoError) {
123 2 : std::cerr << "PortAudio init failed: " << Pa_GetErrorText(err) << std::endl;
124 2 : return false;
125 : }
126 754 : initialized_ = true;
127 :
128 754 : auto_detect_devices();
129 754 : return true;
130 257 : }
131 :
132 2354 : void PortAudioBackend::shutdown() {
133 2354 : stop();
134 2354 : if (initialized_) {
135 754 : Pa_Terminate();
136 754 : initialized_ = false;
137 256 : }
138 2354 : }
139 :
140 39 : bool PortAudioBackend::start() {
141 39 : if (!initialized_ || running_) return false;
142 :
143 39 : int buffer_size = engine_->get_buffer_size();
144 39 : int sample_rate = engine_->get_sample_rate();
145 39 : double desired_latency = static_cast<double>(buffer_size) / sample_rate;
146 :
147 17 : PaStreamParameters input_params;
148 39 : input_params.device = input_device_;
149 39 : input_params.channelCount = 1;
150 39 : input_params.sampleFormat = paFloat32;
151 39 : input_params.suggestedLatency = desired_latency;
152 39 : input_params.hostApiSpecificStreamInfo = nullptr;
153 :
154 17 : PaStreamParameters output_params;
155 39 : output_params.device = output_device_;
156 39 : output_params.channelCount = 2;
157 39 : output_params.sampleFormat = paFloat32;
158 39 : output_params.suggestedLatency = desired_latency;
159 39 : output_params.hostApiSpecificStreamInfo = nullptr;
160 :
161 : #ifdef _WIN32
162 17 : PaWasapiStreamInfo wasapi_in_info = {};
163 17 : PaWasapiStreamInfo wasapi_out_info = {};
164 17 : const PaDeviceInfo* in_dev = Pa_GetDeviceInfo(input_device_);
165 17 : if (in_dev) {
166 11 : const PaHostApiInfo* api = Pa_GetHostApiInfo(in_dev->hostApi);
167 11 : if (api && api->type == paWASAPI) {
168 0 : wasapi_in_info.size = sizeof(PaWasapiStreamInfo);
169 0 : wasapi_in_info.hostApiType = paWASAPI;
170 0 : wasapi_in_info.version = 1;
171 0 : wasapi_in_info.flags = paWinWasapiExclusive;
172 0 : input_params.hostApiSpecificStreamInfo = &wasapi_in_info;
173 :
174 0 : wasapi_out_info.size = sizeof(PaWasapiStreamInfo);
175 0 : wasapi_out_info.hostApiType = paWASAPI;
176 0 : wasapi_out_info.version = 1;
177 0 : wasapi_out_info.flags = paWinWasapiExclusive;
178 0 : output_params.hostApiSpecificStreamInfo = &wasapi_out_info;
179 :
180 0 : std::cout << " Using WASAPI Exclusive Mode" << std::endl;
181 : }
182 : }
183 : #endif
184 :
185 39 : unsigned long frames = static_cast<unsigned long>(buffer_size);
186 :
187 78 : PaError err = Pa_OpenStream(&stream_, &input_params, &output_params, sample_rate, frames,
188 39 : paClipOff | paDitherOff, pa_audio_callback, engine_);
189 :
190 39 : if (err != paNoError) {
191 12 : std::cerr << "Failed to open PortAudio stream: " << Pa_GetErrorText(err) << std::endl;
192 :
193 : // Adjust parameters before retrying: disable WASAPI exclusive and reset latency suggestions
194 12 : input_params.hostApiSpecificStreamInfo = nullptr;
195 12 : output_params.hostApiSpecificStreamInfo = nullptr;
196 :
197 12 : const PaDeviceInfo* in_info = Pa_GetDeviceInfo(input_device_);
198 12 : if (in_info) {
199 4 : input_params.suggestedLatency = in_info->defaultLowInputLatency;
200 2 : } else {
201 8 : input_params.suggestedLatency = 0.0;
202 : }
203 :
204 12 : const PaDeviceInfo* out_info = Pa_GetDeviceInfo(output_device_);
205 12 : if (out_info) {
206 4 : output_params.suggestedLatency = out_info->defaultLowOutputLatency;
207 2 : } else {
208 8 : output_params.suggestedLatency = 0.0;
209 : }
210 :
211 : // Retry
212 24 : err = Pa_OpenStream(&stream_, &input_params, &output_params, sample_rate, buffer_size,
213 12 : paClipOff | paDitherOff, pa_audio_callback, engine_);
214 12 : if (err != paNoError) {
215 12 : std::cerr << "PortAudio open stream retry failed: " << Pa_GetErrorText(err)
216 12 : << std::endl;
217 12 : return false;
218 : }
219 0 : }
220 :
221 27 : err = Pa_StartStream(stream_);
222 27 : if (err != paNoError) {
223 2 : std::cerr << "Failed to start PortAudio stream: " << Pa_GetErrorText(err) << std::endl;
224 2 : Pa_CloseStream(stream_);
225 2 : stream_ = nullptr;
226 2 : return false;
227 : }
228 :
229 25 : running_ = true;
230 25 : return true;
231 20 : }
232 :
233 3923 : void PortAudioBackend::stop() {
234 3923 : if (stream_) {
235 25 : if (running_) {
236 25 : Pa_StopStream(stream_);
237 25 : running_ = false;
238 16 : }
239 25 : Pa_CloseStream(stream_);
240 25 : stream_ = nullptr;
241 16 : }
242 3923 : }
243 :
244 14 : std::vector<AudioDeviceInfo> PortAudioBackend::get_input_devices() const {
245 14 : std::vector<AudioDeviceInfo> devices;
246 14 : if (!initialized_) return devices;
247 14 : int count = Pa_GetDeviceCount();
248 35 : for (int i = 0; i < count; ++i) {
249 21 : const PaDeviceInfo* info = Pa_GetDeviceInfo(i);
250 21 : if (info && info->maxInputChannels > 0) {
251 23 : devices.push_back({i, info->name, info->maxInputChannels, info->maxOutputChannels,
252 24 : info->defaultSampleRate, is_usb_device_name(info->name)});
253 11 : }
254 18 : }
255 8 : return devices;
256 8 : }
257 :
258 14 : std::vector<AudioDeviceInfo> PortAudioBackend::get_output_devices() const {
259 14 : std::vector<AudioDeviceInfo> devices;
260 14 : if (!initialized_) return devices;
261 14 : int count = Pa_GetDeviceCount();
262 35 : for (int i = 0; i < count; ++i) {
263 21 : const PaDeviceInfo* info = Pa_GetDeviceInfo(i);
264 21 : if (info && info->maxOutputChannels > 0) {
265 23 : devices.push_back({i, info->name, info->maxInputChannels, info->maxOutputChannels,
266 24 : info->defaultSampleRate, is_usb_device_name(info->name)});
267 11 : }
268 18 : }
269 8 : return devices;
270 8 : }
271 :
272 19 : bool PortAudioBackend::set_input_device(int device_index) {
273 19 : const PaDeviceInfo* info = Pa_GetDeviceInfo(device_index);
274 19 : if (!info || info->maxInputChannels < 1) {
275 5 : return false;
276 : }
277 12 : input_device_ = device_index;
278 12 : return true;
279 11 : }
280 :
281 19 : bool PortAudioBackend::set_output_device(int device_index) {
282 19 : const PaDeviceInfo* info = Pa_GetDeviceInfo(device_index);
283 19 : if (!info || info->maxOutputChannels < 1) {
284 4 : return false;
285 : }
286 13 : output_device_ = device_index;
287 13 : return true;
288 11 : }
289 :
290 19 : std::string PortAudioBackend::get_input_device_name() const {
291 19 : if (input_device_ >= 0) {
292 3 : const PaDeviceInfo* info = Pa_GetDeviceInfo(input_device_);
293 3 : if (info) return info->name;
294 0 : }
295 21 : return "None";
296 7 : }
297 :
298 19 : std::string PortAudioBackend::get_output_device_name() const {
299 19 : if (output_device_ >= 0) {
300 3 : const PaDeviceInfo* info = Pa_GetDeviceInfo(output_device_);
301 3 : if (info) return info->name;
302 0 : }
303 21 : return "None";
304 7 : }
305 :
306 25 : int PortAudioBackend::get_sample_rate() const {
307 25 : if (stream_) {
308 25 : const PaStreamInfo* si = Pa_GetStreamInfo(stream_);
309 25 : if (si && si->sampleRate > 0.0) {
310 25 : return static_cast<int>(si->sampleRate + 0.5);
311 : }
312 0 : }
313 0 : return engine_ ? engine_->get_sample_rate() : 48000;
314 16 : }
315 :
316 25 : int PortAudioBackend::get_buffer_size() const { return engine_ ? engine_->get_buffer_size() : 512; }
317 :
318 754 : void PortAudioBackend::auto_detect_devices() {
319 754 : int device_count = Pa_GetDeviceCount();
320 754 : int num_apis = Pa_GetHostApiCount();
321 :
322 3492 : struct ApiCandidate {
323 : int api_index;
324 : int priority;
325 : int usb_input;
326 : int best_output;
327 : std::string api_name;
328 : };
329 :
330 754 : std::vector<ApiCandidate> candidates;
331 2489 : for (int a = 0; a < num_apis; ++a) {
332 1735 : const PaHostApiInfo* api = Pa_GetHostApiInfo(a);
333 1735 : if (!api) continue;
334 :
335 1735 : ApiCandidate c;
336 1735 : c.api_index = a;
337 1735 : c.priority = get_host_api_priority(api->type);
338 1735 : c.usb_input = -1;
339 1735 : c.best_output = -1;
340 1735 : c.api_name = api->name;
341 :
342 2500 : for (int d = 0; d < api->deviceCount; ++d) {
343 765 : int dev_idx = Pa_HostApiDeviceIndexToDeviceIndex(a, d);
344 765 : const PaDeviceInfo* info = Pa_GetDeviceInfo(dev_idx);
345 765 : if (!info) continue;
346 :
347 757 : bool is_usb = is_usb_device_name(info->name);
348 :
349 757 : if (is_usb && info->maxInputChannels > 0 && c.usb_input < 0) {
350 2 : c.usb_input = dev_idx;
351 1 : }
352 1261 : if (!is_usb && !is_projector_or_hdmi(info->name) && info->maxOutputChannels > 0 &&
353 502 : c.best_output < 0) {
354 253 : c.best_output = dev_idx;
355 251 : }
356 752 : }
357 :
358 1735 : if (c.best_output < 0) {
359 1488 : for (int d = 0; d < api->deviceCount; ++d) {
360 6 : int dev_idx = Pa_HostApiDeviceIndexToDeviceIndex(a, d);
361 6 : const PaDeviceInfo* info = Pa_GetDeviceInfo(dev_idx);
362 6 : if (!info) continue;
363 0 : if (!is_usb_device_name(info->name) && info->maxOutputChannels > 0) {
364 0 : c.best_output = dev_idx;
365 0 : break;
366 : }
367 0 : }
368 1 : }
369 :
370 1735 : candidates.push_back(c);
371 1735 : }
372 :
373 754 : std::sort(candidates.begin(), candidates.end(),
374 1729 : [](const ApiCandidate& a, const ApiCandidate& b) { return a.priority > b.priority; });
375 :
376 754 : bool found_pair = false;
377 2487 : for (auto& c : candidates) {
378 1735 : if (c.usb_input >= 0 && c.best_output >= 0) {
379 2 : input_device_ = c.usb_input;
380 2 : output_device_ = c.best_output;
381 2 : found_pair = true;
382 2 : break;
383 : }
384 : }
385 :
386 499 : if (!found_pair) {
387 2234 : for (auto& c : candidates) {
388 1733 : if (c.best_output >= 0) {
389 251 : const PaHostApiInfo* api = Pa_GetHostApiInfo(c.api_index);
390 502 : for (int d = 0; d < api->deviceCount; ++d) {
391 502 : int dev_idx = Pa_HostApiDeviceIndexToDeviceIndex(c.api_index, d);
392 502 : const PaDeviceInfo* info = Pa_GetDeviceInfo(dev_idx);
393 502 : if (info && info->maxInputChannels > 0) {
394 251 : input_device_ = dev_idx;
395 251 : output_device_ = c.best_output;
396 251 : found_pair = true;
397 251 : break;
398 : }
399 250 : }
400 251 : if (found_pair) break;
401 0 : }
402 : }
403 255 : }
404 :
405 754 : if (!found_pair) {
406 501 : input_device_ = Pa_GetDefaultInputDevice();
407 501 : output_device_ = Pa_GetDefaultOutputDevice();
408 5 : }
409 754 : }
410 :
411 : } // namespace Amplitron
|