diff --git a/.vscode/launch.json b/.vscode/launch.json index 89fa735..4064db1 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -297,7 +297,7 @@ //"--get-current-port" // "--uninstall" //"--enable-p2p" , "CA", "PiPedalTest","12345678","14" - "--list-p2p-channels" + "--alsa-devices" ], "stopAtEntry": false, "cwd": "${workspaceFolder}", @@ -326,6 +326,33 @@ } ] }, + { + "name": "(gdb) pipedal_alsa_check", + "type": "cppdbg", + "request": "launch", + // Resolved by CMake Tools: + "program": "${command:cmake.launchTargetPath}", + "args": [ + //"hw:M2" + ], + "stopAtEntry": false, + "cwd": "${workspaceFolder}", + "environment": [ + { + "name": "PATH", + "value": "$PATH:${command:cmake.launchTargetDirectory}" + } + ], + "externalConsole": false, + "MIMode": "gdb", + "setupCommands": [ + { + "description": "Enable pretty-printing for gdb", + "text": "-enable-pretty-printing", + "ignoreFailures": true + } + ] + }, { "name": "(gdb) pipedal_p2pd", diff --git a/src/AlsaDriver.cpp b/src/AlsaDriver.cpp index 11b2889..f47e51f 100644 --- a/src/AlsaDriver.cpp +++ b/src/AlsaDriver.cpp @@ -1,7 +1,7 @@ /* * MIT License * - * Copyright (c) 2022 Robin E. R. Davies + * Copyright (c) 2024 Robin E. R. Davies * * Permission is hereby granted, free of charge, to any person obtaining a copy of * this software and associated documentation files (the "Software"), to deal in @@ -366,11 +366,38 @@ namespace pipedal { /*if not user-specified, try to find the maximum * number of channels */ - unsigned int channels_max; + unsigned int channels_max = 0; + unsigned int channels_min = 0; err = snd_pcm_hw_params_get_channels_max(hwParams, &channels_max); + if (err < 0) { + AlsaError(SS("Can't get channels_max.")); + } + + err = snd_pcm_hw_params_get_channels_min(hwParams, + &channels_min); + if (err < 0) { + AlsaError(SS("Can't get channels_min.")); + } + + *channels = channels_max; + if (channels_max > 2 && channels_min <= 2 && channels_min > 0) { + unsigned int bestChannelConfig = 2; + + snd_pcm_hw_params_t* test_params; + snd_pcm_hw_params_alloca(&test_params); + snd_pcm_hw_params_copy(test_params, hwParams); + + if ((err = snd_pcm_hw_params_set_channels(handle, test_params, + bestChannelConfig)) >= 0) + { + *channels = bestChannelConfig; + } + + } + if (*channels > 1024) { // The default PCM device has unlimited channels. @@ -2109,11 +2136,47 @@ namespace pipedal { throw PiPedalLogicException("No outut channels."); } + unsigned int channelsMin; + err = snd_pcm_hw_params_get_channels_min(playbackHwParams, &channelsMin); + if (err < 0) + { + throw PiPedalLogicException("No outut channels."); + } + if (playbackChannels > 2 && channelsMin <=2 && channelsMin > 0) + { + snd_pcm_hw_params_t* test_params; + snd_pcm_hw_params_alloca(&test_params); + snd_pcm_hw_params_copy(test_params, playbackHwParams); + + + if (snd_pcm_hw_params_set_channels(playbackHandle,test_params,(unsigned int)2) >= 0) + { + playbackChannels = 2; + } + } + err = snd_pcm_hw_params_get_channels_max(captureHwParams, &captureChannels); if (err < 0) { throw PiPedalLogicException("No input channels."); } + err = snd_pcm_hw_params_get_channels_min(captureHwParams,&channelsMin); + if (err >= 0) + { + if (captureChannels > 2 && channelsMin <= 2 && channelsMin > 0) + { + snd_pcm_hw_params_t* test_params; + snd_pcm_hw_params_alloca(&test_params); + snd_pcm_hw_params_copy(test_params, captureHwParams); + + + if (snd_pcm_hw_params_set_channels(captureHandle,test_params,(unsigned int)2) >= 0) + { + captureChannels = 2; + } + + } + } inputAudioPorts.clear(); for (unsigned int i = 0; i < captureChannels; ++i) diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 6fdd18d..8f45399 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -638,6 +638,8 @@ add_executable(pipedalconfig CommandLineParser.hpp PiPedalException.hpp ConfigMain.cpp + alsaCheck.cpp + alsaCheck.hpp ModFileTypes.cpp ModFileTypes.hpp PiPedalConfiguration.hpp PiPedalConfiguration.cpp JackServerSettings.hpp JackServerSettings.cpp @@ -647,7 +649,7 @@ add_executable(pipedalconfig ) -target_link_libraries(pipedalconfig PRIVATE PiPedalCommon pthread atomic uuid stdc++fs +target_link_libraries(pipedalconfig PRIVATE PiPedalCommon pthread atomic uuid stdc++fs asound ) add_executable(pipedal_latency_test @@ -668,6 +670,12 @@ target_link_libraries(pipedal_latency_test PRIVATE pthread asound PiPedalCommon ) +add_executable(pipedal_alsa_info + alsaCheck.cpp alsaCheck.hpp + alsaCheckMain.cpp +) +target_link_libraries(pipedal_alsa_info PRIVATE asound) + add_executable(capturepresets CapturePresetsMain.cpp Storage.cpp diff --git a/src/ConfigMain.cpp b/src/ConfigMain.cpp index 4738203..84e8834 100644 --- a/src/ConfigMain.cpp +++ b/src/ConfigMain.cpp @@ -24,6 +24,7 @@ #include "CommandLineParser.hpp" #include "SystemConfigFile.hpp" #include "ModFileTypes.hpp" +#include "alsaCheck.hpp" #include #include @@ -92,18 +93,19 @@ namespace fs = std::filesystem; #define REMOVE_OLD_SERVICE 0 // Grandfathering: whether to remove the old shutdown service (now pipedaladmind) #define OLD_SHUTDOWN_SERVICE "pipedalshutdownd" - - -void changeUserShell(const char* username, const char* newShell) { - struct passwd* pw; +void changeUserShell(const char *username, const char *newShell) +{ + struct passwd *pw; struct passwd p; char buf[1024]; pw = getpwnam(username); - if (pw == nullptr) { + if (pw == nullptr) + { throw std::runtime_error("User not found"); } - if (strcmp(pw->pw_shell,newShell) == 0) { + if (strcmp(pw->pw_shell, newShell) == 0) + { return; } std::string args = SS("/usr/sbin/usermod -s " << newShell << " " << username); @@ -114,7 +116,6 @@ void changeUserShell(const char* username, const char* newShell) { } } - fs::path GetServiceFileName(const std::string &serviceName) { return fs::path(SERVICE_PATH) / (serviceName + ".service"); @@ -218,7 +219,7 @@ bool disableAvahiConfigLine(SystemConfigFile &avahi, const std::string §ion, ++line; } } - avahi.UndoableAddLine(line,lineStart+"no"); + avahi.UndoableAddLine(line, lineStart + "no"); changed = true; } } @@ -242,7 +243,7 @@ static void AvahiInstall() changed |= disableAvahiConfigLine(avahi, "[server]", "use-ipv6="); changed |= disableAvahiConfigLine(avahi, "[publish]", "publish-aaaa-on-ipv4="); - + if (changed) { avahi.Save(); @@ -1035,12 +1036,13 @@ void Install(const fs::path &programPrefix, const std::string endpointAddress) // add to netdev group sysExec(USERMOD_BIN " -a -G " NETDEV_GROUP_NAME " " SERVICE_ACCOUNT_NAME); - try { + try + { changeUserShell(SERVICE_ACCOUNT_NAME, "/usr/sbin/nologon"); - } catch (const std::exception&e) + } + catch (const std::exception &e) { cout << "Error: Can't set user shell for pipedal_d. " << e.what() << std::endl; - } // create and configure /var directory. @@ -1162,7 +1164,6 @@ void Install(const fs::path &programPrefix, const std::string endpointAddress) FixPermissions(); ModFileTypes::CreateDefaultDirectories("/var/pipedal/audio_uploads"); - StopService(false); AvahiInstall(); InstallPgpKey(); @@ -1281,6 +1282,10 @@ static void PrintHelp() << HangingIndent() << " --disable-hotspot\tDisabled the Wi-Fi hotspot." << "\n\n" + << HangingIndent() << " --alsa-devices\tList available ALSA hw devices." + << "\n\n" + << HangingIndent() << " --alsa-device [device_id]\tPrint info for the specified ALSA device." + << "\n\n" << HangingIndent() << " --list-wifi-channels [] \tList valid Wifi channels for the current/specified country." << "\n\n" @@ -1364,6 +1369,8 @@ int main(int argc, char **argv) bool fix_permissions = false; bool nosudo = false; bool excludeShutdownService = false; + bool alsaDevices = false; + std::string alsaDevice; std::string prefixOption; std::string portOption; std::string homeNetwork; @@ -1387,12 +1394,14 @@ int main(int argc, char **argv) parser.AddOption("--home-network", &homeNetwork); parser.AddOption("--no-ethernet", &noEthernet); parser.AddOption("--no-wifi", &noWifi); + parser.AddOption("--alsa-devices", &alsaDevices); + parser.AddOption("--alsa-device", &alsaDevice); try { parser.Parse(argc, (const char **)argv); int actionCount = - help + get_current_port + install + uninstall + stop + start + enable + disable + enable_hotspot + disable_hotspot + restart + enable_p2p + disable_p2p + list_p2p_channels + fix_permissions; + (!alsaDevice.empty()) + alsaDevices + help + get_current_port + install + uninstall + stop + start + enable + disable + enable_hotspot + disable_hotspot + restart + enable_p2p + disable_p2p + list_p2p_channels + fix_permissions; if (actionCount > 1) { throw std::runtime_error("Please provide only one action."); @@ -1454,6 +1463,32 @@ int main(int argc, char **argv) std::cout << "current port: " << GetCurrentWebServicePort() << std::endl; return EXIT_SUCCESS; } + if (alsaDevices) + { + try + { + list_alsa_devices(); + } + catch (const std::exception &e) + { + cerr << "Error: " << e.what() << endl; + return EXIT_FAILURE; + } + return EXIT_SUCCESS; + } + if (alsaDevice.length() != 0) + { + try + { + check_alsa_channel_configs(alsaDevice.c_str()); + } + catch (const std::exception &e) + { + cerr << "Error: " << e.what() << endl; + return EXIT_FAILURE; + } + return EXIT_SUCCESS; + } if (list_p2p_channels) { return ListP2PChannels(parser.Arguments()); diff --git a/src/alsaCheck.cpp b/src/alsaCheck.cpp new file mode 100644 index 0000000..e19670a --- /dev/null +++ b/src/alsaCheck.cpp @@ -0,0 +1,209 @@ +/* + * MIT License + * + * Copyright (c) 2024 Robin E. R. Davies + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to + * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies + * of the Software, and to permit persons to whom the Software is furnished to do + * so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +#include "alsaCheck.hpp" + +#include +#include +#include +#include +#include "ss.hpp" +#include +#include "Finally.hpp" + + + +void pipedal::check_alsa_channel_configs(const char* device_name) { + snd_pcm_t* handle; + int err; + + // Open PCM device for playback + if ((err = snd_pcm_open(&handle, device_name, SND_PCM_STREAM_PLAYBACK, 0)) < 0) { + throw std::runtime_error(SS("Cannot open audio device " << device_name << ": " << snd_strerror(err))); + return; + } + + Finally ff([handle] { + snd_pcm_close(handle); + }); + // Get hardware parameters + snd_pcm_hw_params_t* hw_params; + snd_pcm_hw_params_alloca(&hw_params); + + if ((err = snd_pcm_hw_params_any(handle, hw_params)) < 0) { + const std::string errorMessage = snd_strerror(err); + throw std::runtime_error(SS("Cannot initialize hardware parameter structure: " << errorMessage)); + } + + // Get channel count range + unsigned int min_channels, max_channels; + if ((err = snd_pcm_hw_params_get_channels_min(hw_params, &min_channels)) < 0) { + const std::string errorMessage = snd_strerror(err); + throw std::runtime_error(SS("Cannot get minimum channels: " << errorMessage)); + } + + if ((err = snd_pcm_hw_params_get_channels_max(hw_params, &max_channels)) < 0) { + std::cerr << "Cannot get maximum channels: " << snd_strerror(err) << std::endl; + return; + } + + std::cout << "Device: " << device_name << std::endl; + std::cout << "Channel range: " << min_channels << " to " << max_channels << std::endl; + std::cout << "\nSupported channel configurations:" << std::endl; + + // Test specific channel counts + std::vector common_configs = {1, 2, 4, 6, 8}; + for (unsigned int channels : common_configs) { + if (channels >= min_channels && channels <= max_channels) { + snd_pcm_hw_params_t* test_params; + snd_pcm_hw_params_alloca(&test_params); + snd_pcm_hw_params_copy(test_params, hw_params); + + if (snd_pcm_hw_params_set_channels(handle, test_params, channels) == 0) { + std::string config_name; + switch (channels) { + case 1: config_name = "Mono"; break; + case 2: config_name = "Stereo"; break; + case 4: config_name = "Quadraphonic"; break; + case 6: config_name = "5.1 Surround"; break; + case 8: config_name = "7.1 Surround"; break; + default: config_name = std::to_string(channels) + " channels"; + } + std::cout << " " << channels << " channels (" << config_name << ")" << std::endl; + } + } + } + +} + + +static void print_device_info(snd_ctl_t* handle, int card, int device) { + snd_pcm_info_t* pcminfo; + snd_pcm_info_alloca(&pcminfo); + snd_pcm_info_set_device(pcminfo, device); + snd_pcm_info_set_subdevice(pcminfo, 0); + + // Check playback + snd_pcm_info_set_stream(pcminfo, SND_PCM_STREAM_PLAYBACK); + bool has_playback = snd_ctl_pcm_info(handle, pcminfo) >= 0; + + // Check capture + snd_pcm_info_set_stream(pcminfo, SND_PCM_STREAM_CAPTURE); + bool has_capture = snd_ctl_pcm_info(handle, pcminfo) >= 0; + + if (has_playback || has_capture) { + char name[32]; + snprintf(name, sizeof(name), "hw:%d,%d", card, device); + + // Get device name + snd_pcm_t* pcm; + std::string device_name = "Unknown"; + if (snd_pcm_open(&pcm, name, SND_PCM_STREAM_PLAYBACK, SND_PCM_NONBLOCK) >= 0) { + device_name = snd_pcm_name(pcm); + snd_pcm_close(pcm); + } + + // Print basic info + std::cout << "\n Subdevice: " << name << " - " << device_name << std::endl; + std::cout << " ID: " << snd_pcm_info_get_id(pcminfo) << std::endl; + std::cout << " Capabilities:"; + if (has_playback) std::cout << " PLAYBACK"; + if (has_capture) std::cout << " CAPTURE"; + std::cout << std::endl; + + // Get hardware parameters for more detailed info + if (has_playback) { + snd_pcm_t* pcm_handle; + if (snd_pcm_open(&pcm_handle, name, SND_PCM_STREAM_PLAYBACK, 0) >= 0) { + snd_pcm_hw_params_t* hw_params; + snd_pcm_hw_params_alloca(&hw_params); + + if (snd_pcm_hw_params_any(pcm_handle, hw_params) >= 0) { + unsigned int min_channels, max_channels; + unsigned int min_rate, max_rate; + + snd_pcm_hw_params_get_channels_min(hw_params, &min_channels); + snd_pcm_hw_params_get_channels_max(hw_params, &max_channels); + snd_pcm_hw_params_get_rate_min(hw_params, &min_rate, 0); + snd_pcm_hw_params_get_rate_max(hw_params, &max_rate, 0); + + std::cout << " Channels: " << min_channels << " - " << max_channels << std::endl; + std::cout << " Sample rates: " << min_rate << " - " << max_rate << " Hz" << std::endl; + + // Get supported formats + std::cout << " Supported formats:"; + for (snd_pcm_format_t fmt = SND_PCM_FORMAT_S8; fmt <= SND_PCM_FORMAT_LAST; fmt = snd_pcm_format_t(fmt + 1)) { + if (snd_pcm_hw_params_test_format(pcm_handle, hw_params, fmt) == 0) { + std::cout << " " << snd_pcm_format_name(fmt); + } + } + std::cout << std::endl; + } + snd_pcm_close(pcm_handle); + } else { + std::cout << " (device in use)" << std::endl; + } + } + } +} + +void pipedal::list_alsa_devices() { + int card = -1; + int err; + + std::cout << "=== ALSA Hardware Devices ===" << std::endl; + + // Iterate through cards + while ((err = snd_card_next(&card)) >= 0 && card >= 0) { + snd_ctl_t* handle; + char name[32]; + snprintf(name, sizeof(name), "hw:%d", card); + + if ((err = snd_ctl_open(&handle, name, 0)) < 0) { + std::cerr << "Cannot open control for card " << card << ": " << snd_strerror(err) << std::endl; + continue; + } + + // Get card info + snd_ctl_card_info_t* info; + snd_ctl_card_info_alloca(&info); + if ((err = snd_ctl_card_info(handle, info)) < 0) { + std::cerr << "Cannot get card info: " << snd_strerror(err) << std::endl; + snd_ctl_close(handle); + continue; + } + + std::cout << "\nSound Card hw:" << snd_ctl_card_info_get_id(info) << std::endl; + std::cout << " Name: " << snd_ctl_card_info_get_name(info) << std::endl; + std::cout << " Driver: " << snd_ctl_card_info_get_driver(info) << std::endl; + + // Iterate through devices on this card + int device = -1; + while ((err = snd_ctl_pcm_next_device(handle, &device)) >= 0 && device >= 0) { + print_device_info(handle, card, device); + } + + snd_ctl_close(handle); + } +} diff --git a/src/alsaCheck.hpp b/src/alsaCheck.hpp new file mode 100644 index 0000000..90efff5 --- /dev/null +++ b/src/alsaCheck.hpp @@ -0,0 +1,29 @@ +/* + * MIT License + * + * Copyright (c) 2024 Robin E. R. Davies + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to + * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies + * of the Software, and to permit persons to whom the Software is furnished to do + * so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ +#pragma once + +namespace pipedal { + void check_alsa_channel_configs(const char* device_name = "default"); + void list_alsa_devices(); +} diff --git a/src/alsaCheckMain.cpp b/src/alsaCheckMain.cpp new file mode 100644 index 0000000..a682863 --- /dev/null +++ b/src/alsaCheckMain.cpp @@ -0,0 +1,39 @@ +#include +#include +#include +#include +#include "CommandLineParser.hpp" +#include "alsaCheck.hpp" + +using namespace std; +using namespace pipedal; + + +int main(int argc, char* argv[]) { + + bool listDevices = false; + + try { + CommandLineParser commandLineParser; + + std::string device = ""; + + commandLineParser.AddOption("--list",&listDevices); + + commandLineParser.Parse(argc,(const char**)argv); + + if (commandLineParser.Arguments().size() >= 1) { + device = commandLineParser.Arguments()[0]; + } + if (listDevices || device.empty()) { + list_alsa_devices(); + } else { + check_alsa_channel_configs(device.c_str()); + } + return EXIT_SUCCESS; + } catch (const std::exception &e) + { + cerr << "Error: " << e.what() << endl; + return EXIT_FAILURE; + } +} \ No newline at end of file