LCOV - code coverage report
Current view: top level - src/audio/backend - portaudio_backend.cpp (source / functions) Coverage Total Hit
Test: merged.info Lines: 91.4 % 278 254
Test Date: 2026-06-07 15:51:50 Functions: 91.7 % 24 22

            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
        

Generated by: LCOV version 2.0-1