// Copyright (c) 2022 Robin 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 "pch.h" #include "PiPedalAlsa.hpp" #include "alsa/asoundlib.h" #include "Lv2Log.hpp" #include using namespace pipedal; static uint32_t RATES[] = { 22050, 24000, 44100, 48000, 44100 * 2, 48000 * 2, 44100 * 4, 48000 * 4}; std::mutex alsaMutex; bool PiPedalAlsaDevices::getCachedDevice(const std::string &name, AlsaDeviceInfo *pResult) { auto it = cachedDevices.find(name); if (it != cachedDevices.end()) { *pResult = it->second; return true; } return false; } void PiPedalAlsaDevices::cacheDevice(const std::string &name, const AlsaDeviceInfo &deviceInfo) { cachedDevices[name] = deviceInfo; } std::vector PiPedalAlsaDevices::GetAlsaDevices() { std::lock_guard guard{alsaMutex}; std::vector result; int cardNum = -1; // Start with first card int err; for (;;) { if ((err = snd_card_next(&cardNum)) < 0) { Lv2Log::error("Unexpected error enumerating ALSA devices."); break; } if (cardNum < 0) // No more cards break; { std::stringstream ss; ss << "hw:" << cardNum; std::string cardId = ss.str(); snd_ctl_t *hDevice = nullptr; if ((err = snd_ctl_open(&hDevice, cardId.c_str(), 0)) < 0) { continue; } snd_ctl_card_info_t *alsaInfo; if (snd_ctl_card_info_malloc(&alsaInfo) != 0) { Lv2Log::error("Failed to allocate ALSA card info"); snd_ctl_close(hDevice); continue; } err = snd_ctl_card_info(hDevice, alsaInfo); if (err == 0) { AlsaDeviceInfo info; info.cardId_ = cardNum; info.id_ = std::string("hw:") + snd_ctl_card_info_get_id(alsaInfo); const char *driver = snd_ctl_card_info_get_driver(alsaInfo); info.name_ = snd_ctl_card_info_get_name(alsaInfo); info.longName_ = snd_ctl_card_info_get_longname(alsaInfo); snd_pcm_t *hDevice = nullptr; // must support capture AND playback err = snd_pcm_open(&hDevice, cardId.c_str(), SND_PCM_STREAM_CAPTURE, 0); if (err == 0) { snd_pcm_close(hDevice); } if (err == 0) { err = snd_pcm_open(&hDevice, cardId.c_str(), SND_PCM_STREAM_PLAYBACK, 0); } if (err == 0) { snd_pcm_hw_params_t *params = nullptr; err = snd_pcm_hw_params_malloc(¶ms); if (err == 0) { err = snd_pcm_hw_params_any(hDevice, params); if (err == 0) { unsigned int minRate = 0, maxRate = 0; snd_pcm_uframes_t minBufferSize = 0, maxBufferSize = 0; int dir; err = snd_pcm_hw_params_get_rate_min(params, &minRate, &dir); if (err == 0) { err = snd_pcm_hw_params_get_rate_max(params, &maxRate, &dir); } if (err == 0) { err = snd_pcm_hw_params_get_buffer_size_min(params, &minBufferSize); } if (err == 0) { err = snd_pcm_hw_params_get_buffer_size_max(params, &maxBufferSize); } if (err == 0) { for (size_t i = 0; i < sizeof(RATES) / sizeof(RATES[0]); ++i) { uint32_t rate = RATES[i]; if (rate >= minRate && rate <= maxRate) { info.sampleRates_.push_back(rate); } } if (minBufferSize < 16) { minBufferSize = 16; } info.minBufferSize_ = (uint32_t)minBufferSize; info.maxBufferSize_ = (uint32_t)maxBufferSize; cacheDevice(info.name_, info); result.push_back(info); } } } if (params != nullptr) snd_pcm_hw_params_free(params); snd_pcm_close(hDevice); } else { if (getCachedDevice(info.name_, &info)) { result.push_back(info); } } } snd_ctl_card_info_free(alsaInfo); snd_ctl_close(hDevice); } } snd_config_update_free_global(); Lv2Log::debug("GetAlsaDevices --"); for (auto &device : result) { Lv2Log::debug(SS(" " << device.name_ << " " << device.longName_ << " " << device.cardId_)); } return result; } static void AddMidiCardDevicesToList(snd_ctl_t *ctl, int card, int device, AlsaMidiDeviceInfo::Direction direction, std::vector *result) { snd_rawmidi_info_t *info = nullptr; const char *name = nullptr; const char *sub_name = nullptr; int subs = 0, subs_in = 0, subs_out = 0; int sub = 0; int err = 0; ; snd_rawmidi_info_alloca(&info); snd_rawmidi_info_set_device(info, device); snd_rawmidi_info_set_stream(info, SND_RAWMIDI_STREAM_INPUT); err = snd_ctl_rawmidi_info(ctl, info); if (err >= 0) subs_in = snd_rawmidi_info_get_subdevices_count(info); else subs_in = 0; snd_rawmidi_info_set_stream(info, SND_RAWMIDI_STREAM_OUTPUT); err = snd_ctl_rawmidi_info(ctl, info); if (err >= 0) subs_out = snd_rawmidi_info_get_subdevices_count(info); else subs_out = 0; subs = subs_in > subs_out ? subs_in : subs_out; if (!subs) return; switch (direction) { case AlsaMidiDeviceInfo::In: if (subs_out == 0) // out for the device, in for us. { return; // no input devices to add } break; case AlsaMidiDeviceInfo::Out: // in for the device, out for us. if (subs_in == 0) { return; // no output devices to add } break; case AlsaMidiDeviceInfo::InOut: if (subs_in == 0 || subs_out == 0) { return; // no input or output devices to add } break; case AlsaMidiDeviceInfo::None: return; // no devices to add } for (sub = 0; sub < subs; ++sub) { snd_rawmidi_info_set_stream(info, direction == AlsaMidiDeviceInfo::Direction::Out ? SND_RAWMIDI_STREAM_INPUT : SND_RAWMIDI_STREAM_OUTPUT); snd_rawmidi_info_set_subdevice(info, sub); err = snd_ctl_rawmidi_info(ctl, info); if (err < 0) { throw std::runtime_error( SS("snd_ctl_rawmidi_info failed for card " << card << ", device " << device << ", subdevice " << sub << ": " << snd_strerror(err))); } name = snd_rawmidi_info_get_name(info); sub_name = snd_rawmidi_info_get_subdevice_name(info); // get card name. std::string cardName; snd_ctl_card_info_t *card_info = nullptr; snd_ctl_card_info_malloc(&card_info); if (snd_ctl_card_info(ctl, card_info) == 0) { cardName = snd_ctl_card_info_get_name(card_info); } snd_ctl_card_info_free(card_info); if (cardName.length() == 0) { return; } if (sub == 0 && sub_name[0] == '\0') { AlsaMidiDeviceInfo info; info.name_ = SS("hw:CARD=" << cardName << ",DEV=" << device); info.description_ = SS("Virtual MIDI " << card << "-" << device); info.card_ = card; info.device_ = device; info.subdevice_ = 0; info.isVirtual_ = true; info.subDevices_ = subs; if (subs_in > 0 && subs_out > 0) { info.direction_ = AlsaMidiDeviceInfo::InOut; } else if (subs_in > 0) { info.direction_ = AlsaMidiDeviceInfo::In; } else if (subs_out > 0) { info.direction_ = AlsaMidiDeviceInfo::Out; } else { info.direction_ = AlsaMidiDeviceInfo::None; } result->push_back(info); break; } else { AlsaMidiDeviceInfo info; info.name_ = SS("hw:CARD=" << cardName << ",DEV=" << device); if (sub != 0) { info.name_ = SS(info.name_ << "," << sub); } info.description_ = sub_name; info.card_ = card; info.device_ = device; info.subdevice_ = sub; info.isVirtual_ = false; info.subDevices_ = 1; result->push_back(info); } } } static void AddMidiCardToList(int card, std::vector *result, AlsaMidiDeviceInfo::Direction direction) { snd_ctl_t *ctl = nullptr; char name[32]; int device; int err; sprintf(name, "hw:%d", card); if ((err = snd_ctl_open(&ctl, name, 0)) < 0) { throw std::runtime_error(SS("cannot open control for card " << card << ": " << snd_strerror(err))); } device = -1; for (;;) { if ((err = snd_ctl_rawmidi_next_device(ctl, &device)) < 0) { snd_ctl_close(ctl); throw std::runtime_error(SS("cannot get next rawmidi device for card " << card << ": " << snd_strerror(err))); } if (device < 0) break; AddMidiCardDevicesToList(ctl, card, device, direction, result); } snd_ctl_close(ctl); } static std::vector GetAlsaDevices(const char *devname, const char *direction_) { std::vector result; int card, err; AlsaMidiDeviceInfo::Direction direction; if (strcmp(direction_, "Input") == 0) { direction = AlsaMidiDeviceInfo::In; } else if (strcmp(direction_, "Output") == 0) { direction = AlsaMidiDeviceInfo::Out; } else if (strcmp(direction_, "InOut") == 0) { direction = AlsaMidiDeviceInfo::InOut; } else { direction = AlsaMidiDeviceInfo::None; return result; } card = -1; if ((err = snd_card_next(&card)) < 0) { throw std::runtime_error(SS("snd_card_next failed.: " << snd_strerror(err))); } if (card < 0) { // no devices! return result; } do { AddMidiCardToList(card, &result, direction); if ((err = snd_card_next(&card)) < 0) { throw std::runtime_error(SS("snd_card_next failed: " << snd_strerror(err))); } } while (card >= 0); return result; } static std::vector OldGetAlsaDevices(const char *devname, const char *direction) { std::vector result; { char **hints; int err; char **n; char *name; char *desc; char *ioid; /* Enumerate sound devices */ err = snd_device_name_hint(-1, devname, (void ***)&hints); if (err != 0) { return result; } n = hints; while (*n != NULL) { name = snd_device_name_get_hint(*n, "NAME"); desc = snd_device_name_get_hint(*n, "DESC"); ioid = snd_device_name_get_hint(*n, "IOID"); if (desc != nullptr) // skip virtual device { if (ioid == nullptr || strcmp(ioid, direction) == 0) { result.push_back(AlsaMidiDeviceInfo(name, desc)); } } // translate rawmidi device name to device number. if (name && strcmp("null", name) != 0) free(name); if (desc && strcmp("null", desc) != 0) free(desc); if (ioid && strcmp("null", ioid) != 0) free(ioid); n++; } // Free hint buffer too snd_device_name_free_hint((void **)hints); } return result; } std::vector pipedal::LegacyGetAlsaMidiInputDevices() { return GetAlsaDevices("rawmidi", "Input"); } std::vector pipedal::LegacyGetAlsaMidiOutputDevices() { return GetAlsaDevices("rawmidi", "Output"); } AlsaMidiDeviceInfo::AlsaMidiDeviceInfo(const char *name, const char *description) : name_(name) { // extract just the display name from description. // undocumented but e.g.: M2, M2\nM2 Raw Midi const char *p = description; const char *pEnd = p; // undocumented but e.g.: M2, M2\nM2 Raw Midi while (pEnd != nullptr && *pEnd != 0 && *pEnd != ',' && *pEnd != '\n') { ++pEnd; } if (p != pEnd) { description_ = std::string(p, pEnd); } else { description = name; } } AlsaMidiDeviceInfo::AlsaMidiDeviceInfo(const char *name, const char *description, int card, int device, int subdevice) : AlsaMidiDeviceInfo(name, description) { this->card_ = card; this->device_ = device; this->subdevice_ = subdevice; } JSON_MAP_BEGIN(AlsaDeviceInfo) JSON_MAP_REFERENCE(AlsaDeviceInfo, cardId) JSON_MAP_REFERENCE(AlsaDeviceInfo, id) JSON_MAP_REFERENCE(AlsaDeviceInfo, name) JSON_MAP_REFERENCE(AlsaDeviceInfo, longName) JSON_MAP_REFERENCE(AlsaDeviceInfo, sampleRates) JSON_MAP_REFERENCE(AlsaDeviceInfo, minBufferSize) JSON_MAP_REFERENCE(AlsaDeviceInfo, maxBufferSize) JSON_MAP_END() JSON_MAP_BEGIN(AlsaMidiDeviceInfo) JSON_MAP_REFERENCE(AlsaMidiDeviceInfo, name) JSON_MAP_REFERENCE(AlsaMidiDeviceInfo, description) JSON_MAP_END()