LCOV - code coverage report
Current view: top level - src/audio/backend - audio_backend_portaudio_lifecycle.cpp (source / functions) Coverage Total Hit
Test: merged.info Lines: 89.4 % 235 210
Test Date: 2026-06-01 11:15:25 Functions: 100.0 % 7 7

            Line data    Source code
       1              : // =============================================================================
       2              : // PortAudio backend — lifecycle management
       3              : // =============================================================================
       4              : 
       5              : #include "audio/engine/audio_engine.h"
       6              : #include "audio/backend/audio_backend.h"
       7              : #include "audio/backend/audio_backend_portaudio_helpers.h"
       8              : #include "audio/backend/audio_backend_portaudio_internal.h"
       9              : #include <portaudio.h>
      10              : #ifdef _WIN32
      11              : #include <pa_win_wasapi.h>
      12              : #endif
      13              : #include <cstring>
      14              : #include <iostream>
      15              : #include <vector>
      16              : #include <algorithm>
      17              : 
      18              : namespace Amplitron {
      19              : 
      20              : // PortAudio callback
      21         1328 : int pa_audio_callback(const void* input, void* output,
      22              :                               unsigned long frame_count,
      23              :                               const PaStreamCallbackTimeInfo* /*time_info*/,
      24              :                               PaStreamCallbackFlags /*status_flags*/,
      25              :                               void* user_data) {
      26         1328 :     auto* engine = static_cast<AudioEngine*>(user_data);
      27         1328 :     const auto* in = static_cast<const float*>(input);
      28         1328 :     auto* out = static_cast<float*>(output);
      29              : 
      30         1328 :     if (!in || !out) {
      31            4 :         if (out) std::memset(out, 0, frame_count * 2 * sizeof(float));
      32            4 :         return paContinue;
      33              :     }
      34              : 
      35         1324 :     engine->process_audio(in, out, static_cast<int>(frame_count));
      36         1324 :     return paContinue;
      37         1325 : }
      38              : 
      39              : // Device auto-detection
      40          512 : static void auto_detect_devices(int& input_device, int& output_device, int& sample_rate) {
      41          512 :     int device_count = Pa_GetDeviceCount();
      42          512 :     int num_apis = Pa_GetHostApiCount();
      43              : 
      44              :     // Phase 1: Print all devices
      45          512 :     std::cout << "\nDetected audio devices:" << std::endl;
      46         1269 :     for (int i = 0; i < device_count; ++i) {
      47          757 :         const PaDeviceInfo* info = Pa_GetDeviceInfo(i);
      48          757 :         if (!info) continue;
      49          755 :         bool is_usb = is_usb_device_name(info->name);
      50          755 :         const PaHostApiInfo* api = Pa_GetHostApiInfo(info->hostApi);
      51          755 :         const char* api_name = api ? api->name : "Unknown";
      52          755 :         if (info->maxInputChannels > 0) {
      53          504 :             std::cout << "  [IN]  " << info->name
      54          500 :                       << " (" << api_name << ")"
      55          503 :                       << (is_usb ? " [USB]" : "") << std::endl;
      56          500 :         }
      57          755 :         if (info->maxOutputChannels > 0) {
      58          504 :             std::cout << "  [OUT] " << info->name
      59          500 :                       << " (" << api_name << ")"
      60          507 :                       << (is_usb ? " [USB]" : "") << std::endl;
      61          500 :         }
      62          751 :     }
      63              : 
      64              :     // Phase 2: For each host API (ranked by priority), find the best
      65              :     // USB input + non-USB/non-projector output PAIR on the SAME API.
      66         3492 :     struct ApiCandidate {
      67              :         int api_index;
      68              :         int priority;
      69              :         int usb_input;
      70              :         int best_output;
      71              :         std::string api_name;
      72              :     };
      73              : 
      74          512 :     std::vector<ApiCandidate> candidates;
      75         1763 :     for (int a = 0; a < num_apis; ++a) {
      76         1251 :         const PaHostApiInfo* api = Pa_GetHostApiInfo(a);
      77         1251 :         if (!api) continue;
      78              : 
      79         1251 :         ApiCandidate c;
      80         1251 :         c.api_index = a;
      81         1251 :         c.priority = get_host_api_priority(api->type);
      82         1251 :         c.usb_input = -1;
      83         1251 :         c.best_output = -1;
      84         1251 :         c.api_name = api->name;
      85              : 
      86         2016 :         for (int d = 0; d < api->deviceCount; ++d) {
      87          765 :             int dev_idx = Pa_HostApiDeviceIndexToDeviceIndex(a, d);
      88          765 :             const PaDeviceInfo* info = Pa_GetDeviceInfo(dev_idx);
      89          765 :             if (!info) continue;
      90              : 
      91          757 :             bool is_usb = is_usb_device_name(info->name);
      92              : 
      93          757 :             if (is_usb && info->maxInputChannels > 0 && c.usb_input < 0) {
      94            2 :                 c.usb_input = dev_idx;
      95            1 :             }
      96         1512 :             if (!is_usb && !is_projector_or_hdmi(info->name)
      97          755 :                 && info->maxOutputChannels > 0 && c.best_output < 0) {
      98          253 :                 c.best_output = dev_idx;
      99          251 :             }
     100          752 :         }
     101              : 
     102         1251 :         if (c.best_output < 0) {
     103         1004 :             for (int d = 0; d < api->deviceCount; ++d) {
     104            6 :                 int dev_idx = Pa_HostApiDeviceIndexToDeviceIndex(a, d);
     105            6 :                 const PaDeviceInfo* info = Pa_GetDeviceInfo(dev_idx);
     106            6 :                 if (!info) continue;
     107            0 :                 if (!is_usb_device_name(info->name) && info->maxOutputChannels > 0) {
     108            0 :                     c.best_output = dev_idx;
     109            0 :                     break;
     110              :                 }
     111            0 :             }
     112            1 :         }
     113              : 
     114         1251 :         candidates.push_back(c);
     115         1251 :     }
     116              : 
     117          512 :     std::sort(candidates.begin(), candidates.end(),
     118         1245 :               [](const ApiCandidate& a, const ApiCandidate& b) {
     119         1245 :                   return a.priority > b.priority;
     120              :               });
     121              : 
     122              :     // Phase 3: Pick the best API that has BOTH a USB input AND an output
     123          512 :     bool found_pair = false;
     124         1761 :     for (auto& c : candidates) {
     125         1251 :         if (c.usb_input >= 0 && c.best_output >= 0) {
     126            2 :             input_device = c.usb_input;
     127            2 :             output_device = c.best_output;
     128              : 
     129            2 :             const PaDeviceInfo* in_info = Pa_GetDeviceInfo(input_device);
     130            2 :             const PaDeviceInfo* out_info = Pa_GetDeviceInfo(output_device);
     131              : 
     132            3 :             std::cout << "\n>> Auto-selected (same API: " << c.api_name << "):" << std::endl;
     133            2 :             std::cout << "   INPUT:  " << in_info->name << " [USB Guitar Cable]" << std::endl;
     134            2 :             std::cout << "   OUTPUT: " << out_info->name << " [Speakers]" << std::endl;
     135              : 
     136            2 :             if (in_info->defaultSampleRate > 0) {
     137            2 :                 sample_rate = static_cast<int>(in_info->defaultSampleRate);
     138            2 :                 std::cout << "   Rate:   " << sample_rate << " Hz (device native)" << std::endl;
     139            1 :             }
     140              : 
     141            1 :             found_pair = true;
     142            1 :             break;
     143              :         }
     144              :     }
     145              : 
     146              :     // Fallback: no USB input found
     147          256 :     if (!found_pair) {
     148         1508 :         for (auto& c : candidates) {
     149         1249 :             if (c.best_output >= 0) {
     150          251 :                 const PaHostApiInfo* api = Pa_GetHostApiInfo(c.api_index);
     151          253 :                 for (int d = 0; d < api->deviceCount; ++d) {
     152          253 :                     int dev_idx = Pa_HostApiDeviceIndexToDeviceIndex(c.api_index, d);
     153          253 :                     const PaDeviceInfo* info = Pa_GetDeviceInfo(dev_idx);
     154          253 :                     if (info && info->maxInputChannels > 0) {
     155          251 :                         input_device = dev_idx;
     156          251 :                         output_device = c.best_output;
     157              : 
     158          251 :                         std::cout << "\n>> No USB guitar cable detected." << std::endl;
     159          252 :                         std::cout << "   Using " << c.api_name << " defaults:" << std::endl;
     160          251 :                         std::cout << "   INPUT:  " << info->name << std::endl;
     161         1248 :                         std::cout << "   OUTPUT: " << Pa_GetDeviceInfo(output_device)->name << std::endl;
     162              : 
     163          250 :                         found_pair = true;
     164          250 :                         break;
     165              :                     }
     166            1 :                 }
     167          250 :                 if (found_pair) break;
     168            0 :             }
     169              :         }
     170          255 :     }
     171              : 
     172              :     // Last resort: system defaults
     173          511 :     if (!found_pair) {
     174          259 :         input_device = Pa_GetDefaultInputDevice();
     175          259 :         output_device = Pa_GetDefaultOutputDevice();
     176          259 :         std::cout << "\n>> Using system default input/output devices." << std::endl;
     177            5 :     }
     178          512 : }
     179              : 
     180          514 : bool AudioEngine::initialize() {
     181          514 :     PaError err = Pa_Initialize();
     182          514 :     if (err != paNoError) {
     183            2 :         std::cerr << "PortAudio init failed: " << Pa_GetErrorText(err) << std::endl;
     184            2 :         return false;
     185              :     }
     186          512 :     initialized_ = true;
     187              : 
     188          512 :     auto_detect_devices(input_device_, output_device_, sample_rate_);
     189              : 
     190          512 :     return true;
     191          257 : }
     192              : 
     193         1037 : void AudioEngine::shutdown() {
     194         1037 :     stop();
     195         1037 :     if (initialized_) {
     196          512 :         Pa_Terminate();
     197          512 :         initialized_ = false;
     198          256 :     }
     199         1037 : }
     200              : 
     201              : 
     202              : 
     203           43 : bool AudioEngine::start() {
     204           43 :     if (!initialized_ || running_) return false;
     205              : 
     206           37 :     const PaDeviceInfo* in_dev = Pa_GetDeviceInfo(input_device_);
     207           37 :     const PaDeviceInfo* out_dev = Pa_GetDeviceInfo(output_device_);
     208           17 :     (void)in_dev; (void)out_dev;
     209              : 
     210           37 :     double desired_latency = static_cast<double>(buffer_size_) / sample_rate_;
     211              : 
     212           17 :     PaStreamParameters input_params;
     213           37 :     input_params.device = input_device_;
     214           37 :     input_params.channelCount = 1;
     215           37 :     input_params.sampleFormat = paFloat32;
     216           37 :     input_params.suggestedLatency = desired_latency;
     217           37 :     input_params.hostApiSpecificStreamInfo = nullptr;
     218              : 
     219           17 :     PaStreamParameters output_params;
     220           37 :     output_params.device = output_device_;
     221           37 :     output_params.channelCount = 2;
     222           37 :     output_params.sampleFormat = paFloat32;
     223           37 :     output_params.suggestedLatency = desired_latency;
     224           37 :     output_params.hostApiSpecificStreamInfo = nullptr;
     225              : 
     226              : #ifdef _WIN32
     227           17 :     PaWasapiStreamInfo wasapi_in_info = {};
     228           17 :     PaWasapiStreamInfo wasapi_out_info = {};
     229           17 :     if (in_dev) {
     230           11 :         const PaHostApiInfo* api = Pa_GetHostApiInfo(in_dev->hostApi);
     231           11 :         if (api && api->type == paWASAPI) {
     232            0 :             wasapi_in_info.size = sizeof(PaWasapiStreamInfo);
     233            0 :             wasapi_in_info.hostApiType = paWASAPI;
     234            0 :             wasapi_in_info.version = 1;
     235            0 :             wasapi_in_info.flags = paWinWasapiExclusive;
     236            0 :             input_params.hostApiSpecificStreamInfo = &wasapi_in_info;
     237              : 
     238            0 :             wasapi_out_info.size = sizeof(PaWasapiStreamInfo);
     239            0 :             wasapi_out_info.hostApiType = paWASAPI;
     240            0 :             wasapi_out_info.version = 1;
     241            0 :             wasapi_out_info.flags = paWinWasapiExclusive;
     242            0 :             output_params.hostApiSpecificStreamInfo = &wasapi_out_info;
     243              : 
     244            0 :             std::cout << "  Using WASAPI Exclusive Mode" << std::endl;
     245              :         }
     246              :     }
     247              : #endif
     248              : 
     249           37 :     unsigned long frames = static_cast<unsigned long>(buffer_size_);
     250              : 
     251           54 :     PaError err = Pa_OpenStream(
     252           37 :         &backend_->stream,
     253              :         &input_params,
     254              :         &output_params,
     255           37 :         sample_rate_,
     256           20 :         frames,
     257              :         paClipOff | paDitherOff,
     258              :         pa_audio_callback,
     259              :         this
     260              :     );
     261              : 
     262           37 :     if (err != paNoError) {
     263           10 :         std::cerr << "Failed to open stream: " << Pa_GetErrorText(err) << std::endl;
     264           10 :         std::cerr << "Retrying with buffer size " << buffer_size_ << "..." << std::endl;
     265           17 :         err = Pa_OpenStream(
     266           10 :             &backend_->stream,
     267              :             &input_params,
     268              :             &output_params,
     269           10 :             sample_rate_,
     270           10 :             buffer_size_,
     271              :             paClipOff | paDitherOff,
     272              :             pa_audio_callback,
     273              :             this
     274              :         );
     275           10 :         if (err != paNoError) {
     276           10 :             std::cerr << "Failed to open stream: " << Pa_GetErrorText(err) << std::endl;
     277           10 :             return false;
     278              :         }
     279            0 :     }
     280              : 
     281           27 :     const PaStreamInfo* si = Pa_GetStreamInfo(backend_->stream);
     282           27 :     if (si && si->sampleRate > 0.0) {
     283           25 :         const int actual_rate = static_cast<int>(si->sampleRate + 0.5);
     284           25 :         if (actual_rate != sample_rate_) {
     285            6 :             sample_rate_ = actual_rate;
     286            6 :             update_metronome_timing();
     287            6 :             std::lock_guard<std::mutex> lock(effect_mutex_);
     288            6 :             for (const auto& node : main_graph_.get_nodes()) {
     289            0 :                 if (node.pedal) {
     290            0 :                     node.pedal->set_sample_rate(sample_rate_);
     291            0 :                     node.pedal->reset();
     292            0 :                 }
     293              :             }
     294            6 :             if (tuner_tap_) {
     295            0 :                 tuner_tap_->set_sample_rate(sample_rate_);
     296            0 :                 tuner_tap_->reset();
     297            0 :             }
     298            6 :         }
     299           16 :     }
     300              : 
     301           27 :     err = Pa_StartStream(backend_->stream);
     302           27 :     if (err != paNoError) {
     303            2 :         std::cerr << "Failed to start stream: " << Pa_GetErrorText(err) << std::endl;
     304            2 :         Pa_CloseStream(backend_->stream);
     305            2 :         backend_->stream = nullptr;
     306            2 :         return false;
     307              :     }
     308              : 
     309           25 :     running_ = true;
     310           25 :     si = Pa_GetStreamInfo(backend_->stream);
     311           25 :     const PaDeviceInfo* in_info = Pa_GetDeviceInfo(input_device_);
     312           25 :     const PaDeviceInfo* out_info = Pa_GetDeviceInfo(output_device_);
     313           25 :     std::cout << "Audio stream started:" << std::endl;
     314           25 :     std::cout << "  Input:   " << (in_info ? in_info->name : "Unknown") << std::endl;
     315           25 :     std::cout << "  Output:  " << (out_info ? out_info->name : "Unknown") << std::endl;
     316           34 :     std::cout << "  Rate:    " << (si ? si->sampleRate : sample_rate_) << " Hz" << std::endl;
     317           25 :     if (si) {
     318           25 :         std::cout << "  Latency: in=" << (si->inputLatency * 1000.0) << " ms"
     319           43 :                   << "  out=" << (si->outputLatency * 1000.0) << " ms" << std::endl;
     320           16 :     }
     321           16 :     return true;
     322           25 : }
     323              : 
     324         1064 : void AudioEngine::stop() {
     325         1064 :     if (backend_->stream) {
     326           25 :         if (running_) {
     327           25 :             Pa_StopStream(backend_->stream);
     328           25 :             running_ = false;
     329           16 :         }
     330           25 :         Pa_CloseStream(backend_->stream);
     331           25 :         backend_->stream = nullptr;
     332           16 :     }
     333         1064 : }
     334              : 
     335            4 : bool AudioEngine::restart() {
     336            4 :     stop();
     337            4 :     bool ok = start();
     338            4 :     if (!ok) {
     339            2 :         last_error_ = "Failed to restart audio stream. Check device settings.";
     340            4 :         std::cerr << "[Amplitron] " << last_error_ << std::endl;
     341            0 :     } else {
     342            2 :         last_error_.clear();
     343              :     }
     344            4 :     return ok;
     345              : }
     346              : 
     347              : } // namespace Amplitron
        

Generated by: LCOV version 2.0-1