From 4246d03cf84849570bfe5dc1ac24e2410d766776 Mon Sep 17 00:00:00 2001 From: Tobias Blomberg Date: Mon, 1 Jan 2024 12:01:47 +0100 Subject: [PATCH] Add support for LADSPA audio processing LADSPA is a framework for sharing pluggable audio components and there are numerous plugins to choose from. SvxLink now support using LADSPA plugins to shape receiver audio. --- src/CMakeLists.txt | 16 + src/async/ChangeLog | 3 + src/async/audio/AsyncAudioLADSPAPlugin.cpp | 713 ++++++++++++++++++ src/async/audio/AsyncAudioLADSPAPlugin.h | 382 ++++++++++ src/async/audio/CMakeLists.txt | 5 + .../demo/AsyncAudioLADSPAPlugin_demo.cpp | 47 ++ src/async/demo/CMakeLists.txt | 4 + src/cmake/Modules/FindLADSPA.cmake | 84 +++ src/doc/man/svxlink.conf.5 | 29 + src/svxlink/ChangeLog | 4 + src/svxlink/svxlink/svxlink.conf.in | 1 + src/svxlink/trx/LocalRxBase.cpp | 59 +- src/svxlink/trx/Rx.cpp | 12 +- src/svxlink/trx/Rx.h | 18 +- src/svxlink/trx/SquelchCombine.cpp | 2 +- src/versions | 4 +- 16 files changed, 1368 insertions(+), 15 deletions(-) create mode 100644 src/async/audio/AsyncAudioLADSPAPlugin.cpp create mode 100644 src/async/audio/AsyncAudioLADSPAPlugin.h create mode 100644 src/async/demo/AsyncAudioLADSPAPlugin_demo.cpp create mode 100644 src/cmake/Modules/FindLADSPA.cmake diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 5bc381690..7cf6e7367 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -295,6 +295,22 @@ add_definitions(${SIGC2_DEFINITIONS}) set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} ${SIGC2_CXX_FLAGS}") set(LIBS ${LIBS} ${SIGC2_LIBRARIES}) +find_package(LADSPA) +if(LADSPA_FOUND) + if(DEFINED LADSPA_VERSION_MAJOR) + include_directories(${LADSPA_INCLUDE_DIRS}) + add_definitions(${LADSPA_DEFINITIONS}) + else() + message(WARNING + "Found LADSPA but version could not be resolved. " + "Will proceed without LADSPA.") + endif() +else(LADSPA_FOUND) + message("-- LADSPA is an optional dependency. The build will complete") + message("-- without it but support for loading LADSPA plugins will") + message("-- be unavailable.") +endif(LADSPA_FOUND) + # Find the chown utility include(FindCHOWN) diff --git a/src/async/ChangeLog b/src/async/ChangeLog index 38446f847..7d53402b7 100644 --- a/src/async/ChangeLog +++ b/src/async/ChangeLog @@ -67,6 +67,9 @@ * Bugfix Async::HttpServerConnection: EOL handling failed with newer compilers. +* New class Async::AudioLADSPAPlugin which enable the use of LADSPA plugins to + process audio. + 1.6.0 -- 01 Sep 2019 diff --git a/src/async/audio/AsyncAudioLADSPAPlugin.cpp b/src/async/audio/AsyncAudioLADSPAPlugin.cpp new file mode 100644 index 000000000..672de561e --- /dev/null +++ b/src/async/audio/AsyncAudioLADSPAPlugin.cpp @@ -0,0 +1,713 @@ +/** +@file AsyncAudioLADSPAPlugin.cpp +@brief A_brief_description_for_this_file +@author Tobias Blomberg / SM0SVX +@date 2023-12-09 + +\verbatim +Async - A library for programming event driven applications +Copyright (C) 2003-2024 Tobias Blomberg / SM0SVX + +This program is free software; you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation; either version 2 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program; if not, write to the Free Software +Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA +\endverbatim +*/ + +/**************************************************************************** + * + * System Includes + * + ****************************************************************************/ + +//#define _DEFAULT_SOURCE +#include +#include +#include + +#include +#include +#include +#include + + +/**************************************************************************** + * + * Project Includes + * + ****************************************************************************/ + + + +/**************************************************************************** + * + * Local Includes + * + ****************************************************************************/ + +#include "AsyncAudioLADSPAPlugin.h" + + +/**************************************************************************** + * + * Namespaces to use + * + ****************************************************************************/ + +using namespace Async; + + +/**************************************************************************** + * + * Defines & typedefs + * + ****************************************************************************/ + + + +/**************************************************************************** + * + * Static class variables + * + ****************************************************************************/ + + + +/**************************************************************************** + * + * Local class definitions + * + ****************************************************************************/ + +namespace { + + +/**************************************************************************** + * + * Local functions + * + ****************************************************************************/ + + + +}; /* End of anonymous namespace */ + +/**************************************************************************** + * + * Public member functions + * + ****************************************************************************/ + +bool AudioLADSPAPlugin::findPluginsInDir(std::string dir) +{ + if (dir.empty()) + { + return false; + } + if (dir[dir.length()-1] != '/') + { + dir += "/"; + } + //std::cout << "### dir=" << dir << std::endl; + struct dirent **namelist; + int n = scandir(dir.c_str(), &namelist, + [](const struct dirent* de) -> int + { + auto len = strlen(de->d_name); + return (len > 3) && (strcmp(de->d_name+len-3, ".so") == 0); + }, + alphasort); + if (n == -1) + { + perror(std::string("scandir(" + dir + ")").c_str()); + return false; + } + + while (n--) + { + //std::cout << "### " << namelist[n]->d_name << std::endl; + PluginIndex i = 0; + for (;;) + { + AudioLADSPAPlugin p(dir + namelist[n]->d_name, i++); + if (p.ladspaDescriptor() == nullptr) + { + break; + } + auto inst = std::make_shared(); + inst->m_path = p.path(); + inst->m_index = i-1; + inst->m_unique_id = p.uniqueId(); + inst->m_label = p.label(); + labelMap()[inst->m_label] = inst; + idMap()[inst->m_unique_id] = inst; + } + free(namelist[n]); + } + free(namelist); + return true; +} /* AudioLADSPAPlugin::findPluginsInDir */ + + +bool AudioLADSPAPlugin::findPlugins(void) +{ + std::string ladspa_path(LADSPA_PLUGIN_DIRS); + const char* env_ladspa_path = getenv("LADSPA_PATH"); + if (env_ladspa_path != nullptr) + { + ladspa_path = env_ladspa_path; + } + std::string::size_type begin = 0, end = 0; + do + { + end = ladspa_path.find(':', begin); + findPluginsInDir(ladspa_path.substr(begin, end)); + begin = end + 1; + } while (end != std::string::npos); + return true; +} /* AudioLADSPAPlugin::findPlugins */ + + +AudioLADSPAPlugin::AudioLADSPAPlugin(AudioLADSPAPlugin::UniqueID id) +{ + if (idMap().empty()) + { + findPlugins(); + } + auto instit = idMap().find(id); + if (instit == idMap().end()) + { + return; + } + auto inst_info = instit->second; + m_path = inst_info->m_path; + m_index = inst_info->m_index; +} /* AudioLADSPAPlugin::AudioLADSPAPlugin */ + + +AudioLADSPAPlugin::AudioLADSPAPlugin(const std::string& label) +{ + if (labelMap().empty()) + { + findPlugins(); + } + auto instit = labelMap().find(label); + if (instit == labelMap().end()) + { + return; + } + auto inst_info = instit->second; + m_path = inst_info->m_path; + m_index = inst_info->m_index; +} /* AudioLADSPAPlugin::AudioLADSPAPlugin */ + + +AudioLADSPAPlugin::~AudioLADSPAPlugin(void) +{ + deactivate(); + + delete [] m_ctrl_buf; + m_ctrl_buf = nullptr; + + if ((m_desc != nullptr) && (m_desc->cleanup != nullptr) && + (m_inst_handle != nullptr)) + { + m_desc->cleanup(m_inst_handle); + } + m_inst_handle = nullptr; + m_desc = nullptr; + + if (m_handle != nullptr) + { + if (dlclose(m_handle) != 0) + { + std::cerr << "*** ERROR: Failed to unload plugin " + << m_path << ": " << dlerror() << std::endl; + } + } + m_handle = nullptr; +} /* AudioLADSPAPlugin::~AudioLADSPAPlugin */ + + +bool AudioLADSPAPlugin::initialize(void) +{ + if (m_path.empty()) + { + //std::cerr << "*** ERROR: Empty LADSPA plugin path" << std::endl; + return false; + } + + if (ladspaDescriptor() == nullptr) + { + std::cerr << "*** ERROR: Could not find LADSPA instance for index " + << m_index << " in plugin " << m_path << std::endl; + return false; + } + + m_inst_handle = m_desc->instantiate(m_desc, INTERNAL_SAMPLE_RATE); + if (m_inst_handle == nullptr) + { + std::cerr << "*** ERROR: Could not instantiate LADSPA instance for index " + << m_index << " in plugin " << m_path << std::endl; + return false; + } + + m_ctrl_buf = new LADSPA_Data[m_desc->PortCount]; + + for (PortNumber i=0; iPortCount; ++i) + { + const LADSPA_PortDescriptor& port_desc = m_desc->PortDescriptors[i]; + + if (!LADSPA_IS_PORT_INPUT(port_desc) && !LADSPA_IS_PORT_OUTPUT(port_desc)) + { + std::cerr << "*** ERROR: Invalid LADSPA plugin " << m_path + << " with index " << m_index << ". Port " << i + << " is neither input nor output." << std::endl; + return false; + } + if (!LADSPA_IS_PORT_CONTROL(port_desc) && !LADSPA_IS_PORT_AUDIO(port_desc)) + { + std::cerr << "*** ERROR: Invalid LADSPA plugin " << m_path + << " with index " << m_index << ". Port " << i + << " is neither type control nor audio." << std::endl; + return false; + } + + if (LADSPA_IS_PORT_CONTROL(port_desc)) + { + if (!setDefault(i)) + { + std::cerr << "*** ERROR: Illegal default handling in LADSPA instance " + "for index " << m_index << " in plugin " << m_path + << std::endl; + return false; + } + m_desc->connect_port(m_inst_handle, i, &m_ctrl_buf[i]); + } + if (LADSPA_IS_PORT_AUDIO(port_desc)) + { + if (LADSPA_IS_PORT_INPUT(port_desc)) + { + if (m_sample_input_port != NOPORT) + { + std::cerr << "*** ERROR: Only single audio input port LADSPA " + "instances supported but index " << m_index + << " in plugin " << m_path + << " has multiple audio input ports" + << std::endl; + return false; + } + m_sample_input_port = i; + } + else + { + if (m_sample_output_port != NOPORT) + { + std::cerr << "*** ERROR: Only single audio output port LADSPA " + "instances supported but index " << m_index + << " in plugin " << m_path + << " has multiple audio output ports" + << std::endl; + return false; + } + m_sample_output_port = i; + } + } + } + + if ((m_sample_input_port == NOPORT) || (m_sample_output_port == NOPORT)) + { + std::cerr << "*** ERROR: LADSPA instances must have exactly one input " + "port and one output port but index " << m_index + << " in plugin " << m_path + << " is missing an input or output port" << std::endl; + return false; + } + + activate(); + + return true; + +} /* AudioLADSPAPlugin::initialize */ + + +bool AudioLADSPAPlugin::setControl(PortNumber portno, LADSPA_Data val) +{ + assert(m_desc != nullptr); + assert(m_ctrl_buf != nullptr); + if (portno >= m_desc->PortCount) + { + return false; + } + const LADSPA_PortDescriptor& port_desc = m_desc->PortDescriptors[portno]; + if (!LADSPA_IS_PORT_CONTROL(port_desc) || !LADSPA_IS_PORT_INPUT(port_desc)) + { + return false; + } + + float (*conv)(float) = [](float x) { return x; }; + const auto& port_range_hint = m_desc->PortRangeHints[portno]; + const auto& hint_desc = port_range_hint.HintDescriptor; + LADSPA_Data mult = 1.0f; + if (LADSPA_IS_HINT_SAMPLE_RATE(hint_desc)) + { + mult = INTERNAL_SAMPLE_RATE; + } + auto lower_bound = port_range_hint.LowerBound * mult; + auto upper_bound = port_range_hint.UpperBound * mult; + if (LADSPA_IS_HINT_INTEGER(hint_desc)) + { + conv = roundf; + } + if (LADSPA_IS_HINT_BOUNDED_BELOW(hint_desc) && (val < lower_bound)) + { + val = conv(lower_bound); + } + if (LADSPA_IS_HINT_BOUNDED_ABOVE(hint_desc) && (val > upper_bound)) + { + val = conv(upper_bound); + } + + m_ctrl_buf[portno] = val; + + return true; +} /* AudioLADSPAPlugin::setControl */ + + +void AudioLADSPAPlugin::activate(void) +{ + assert((m_desc != nullptr) && (m_inst_handle != nullptr)); + if ((m_desc->activate != nullptr) && !m_is_active) + { + m_desc->activate(m_inst_handle); + } + m_is_active = false; +} /* AudioLADSPAPlugin::activate */ + + +void AudioLADSPAPlugin::deactivate(void) +{ + if ((m_desc != nullptr) && (m_desc->deactivate != nullptr) && + (m_inst_handle != nullptr) && m_is_active) + { + m_desc->deactivate(m_inst_handle); + } + m_is_active = false; +} /* AudioLADSPAPlugin::deactivate */ + + +bool AudioLADSPAPlugin::portIsControl(PortNumber portno) const +{ + assert(m_desc != nullptr); + if (portno >= m_desc->PortCount) + { + return false; + } + const LADSPA_PortDescriptor& port_desc = m_desc->PortDescriptors[portno]; + return LADSPA_IS_PORT_CONTROL(port_desc); +} /* AudioLADSPAPlugin::portIsControl */ + + +bool AudioLADSPAPlugin::portIsAudio(PortNumber portno) const +{ + assert(m_desc != nullptr); + if (portno >= m_desc->PortCount) + { + return false; + } + const LADSPA_PortDescriptor& port_desc = m_desc->PortDescriptors[portno]; + return LADSPA_IS_PORT_AUDIO(port_desc); +} /* AudioLADSPAPlugin::portIsAudio */ + + +bool AudioLADSPAPlugin::portIsInput(PortNumber portno) const +{ + assert(m_desc != nullptr); + if (portno >= m_desc->PortCount) + { + return false; + } + const LADSPA_PortDescriptor& port_desc = m_desc->PortDescriptors[portno]; + return LADSPA_IS_PORT_INPUT(port_desc); +} /* AudioLADSPAPlugin::portIsInput */ + + +bool AudioLADSPAPlugin::portIsOutput(PortNumber portno) const +{ + assert(m_desc != nullptr); + if (portno >= m_desc->PortCount) + { + return false; + } + const LADSPA_PortDescriptor& port_desc = m_desc->PortDescriptors[portno]; + return LADSPA_IS_PORT_OUTPUT(port_desc); +} /* AudioLADSPAPlugin::portIsOutput */ + + +void AudioLADSPAPlugin::print(const std::string& prefix) +{ + assert(m_desc != nullptr); + + std::cout << prefix << "\"" << m_desc->Name << "\"" + << " (" << m_desc->Label << ")" + << " by \"" << m_desc->Maker << "\"" + << " (C) " << m_desc->Copyright + << std::endl; + std::cout << prefix << " Path: " << m_path << std::endl; + + for (PortNumber i=0; iPortCount; ++i) + { + LADSPA_PortDescriptor port_desc = m_desc->PortDescriptors[i]; + + if (LADSPA_IS_PORT_AUDIO(port_desc)) + { + continue; + } + + std::cout << prefix << " " + << (LADSPA_IS_PORT_INPUT(port_desc) ? "In " : "Out") + << ": \"" << m_desc->PortNames[i] << "\" "; + + //if (LADSPA_IS_PORT_CONTROL(port_desc)) + //{ + // std::cout << "control "; + //} + + float (*conv)(float) = [](float x) { return x; }; + const auto& port_range_hint = m_desc->PortRangeHints[i]; + const auto& hint_desc = port_range_hint.HintDescriptor; + if (LADSPA_IS_HINT_INTEGER(hint_desc)) + { + //std::cout << "int:"; + conv = roundf; + } + //else if (LADSPA_IS_HINT_TOGGLED(hint_desc)) + //{ + // std::cout << "bool:"; + //} + //else + //{ + // std::cout << "float:"; + //} + + bool bounded_below = LADSPA_IS_HINT_BOUNDED_BELOW(hint_desc); + bool bounded_above = LADSPA_IS_HINT_BOUNDED_ABOVE(hint_desc); + if (bounded_below || bounded_above) + { + LADSPA_Data samp_rate = 1.0f; + if (LADSPA_IS_HINT_SAMPLE_RATE(hint_desc)) + { + samp_rate = INTERNAL_SAMPLE_RATE; + } + std::cout << "["; + if (bounded_below) + { + std::cout << conv(samp_rate * port_range_hint.LowerBound); + } + std::cout << ","; + if (bounded_above) + { + std::cout << conv(samp_rate * port_range_hint.UpperBound); + } + std::cout << "]"; + } + + if (LADSPA_IS_HINT_LOGARITHMIC(hint_desc)) + { + std::cout << " (log)"; + } + + std::cout << " = " << m_ctrl_buf[i]; + + std::cout << std::endl; + } +} /* AudioLADSPAPlugin::print */ + + +/**************************************************************************** + * + * Protected member functions + * + ****************************************************************************/ + +void AudioLADSPAPlugin::processSamples(float *dest, const float *src, + int count) +{ + assert(dest != nullptr); + assert(src != nullptr); + assert(m_sample_input_port != NOPORT); + assert(m_sample_output_port != NOPORT); + if (count <= 0) + { + return; + } + m_desc->connect_port(m_inst_handle, m_sample_input_port, + const_cast(src)); + m_desc->connect_port(m_inst_handle, m_sample_output_port, dest); + m_desc->run(m_inst_handle, count); +} /* AudioLADSPAPlugin::processSamples */ + + +/**************************************************************************** + * + * Private member functions + * + ****************************************************************************/ + +const LADSPA_Descriptor* AudioLADSPAPlugin::ladspaDescriptor(void) +{ + m_handle = dlopen(m_path.c_str(), RTLD_NOW); + if (m_handle == nullptr) + { + std::cerr << "*** ERROR: Failed to load plugin " + << m_path << ": " << dlerror() << std::endl; + return nullptr; + } + + using ConstructFunc = LADSPA_Descriptor_Function; + ConstructFunc construct = (ConstructFunc)dlsym(m_handle, "ladspa_descriptor"); + if (construct == nullptr) + { + std::cerr << "*** ERROR: Could not find LADSPA descriptor function for " + "plugin " << m_path << ": " << dlerror() << std::endl; + return nullptr; + } + + m_desc = construct(m_index); + return m_desc; +} /* AudioLADSPAPlugin::ladspaDescriptor */ + + +bool AudioLADSPAPlugin::setDefault(PortNumber portno) +{ + assert(m_desc != nullptr); + + LADSPA_Data& def = m_ctrl_buf[portno]; + const auto& port_range_hint = m_desc->PortRangeHints[portno]; + const auto& hint_desc = port_range_hint.HintDescriptor; + + if (!LADSPA_IS_HINT_HAS_DEFAULT(hint_desc)) + { + def = 0.0f; + return true; + } + + LADSPA_Data mult = 1.0f; + if (LADSPA_IS_HINT_SAMPLE_RATE(hint_desc)) + { + mult = INTERNAL_SAMPLE_RATE; + } + auto lower_bound = port_range_hint.LowerBound * mult; + auto upper_bound = port_range_hint.UpperBound * mult; + + if (LADSPA_IS_HINT_DEFAULT_MINIMUM(hint_desc)) + { + if (!LADSPA_IS_HINT_BOUNDED_BELOW(hint_desc)) + { + return false; + } + def = lower_bound; + } + + if (LADSPA_IS_HINT_DEFAULT_LOW(hint_desc)) + { + if (!LADSPA_IS_HINT_BOUNDED_BELOW(hint_desc) || + !LADSPA_IS_HINT_BOUNDED_ABOVE(hint_desc)) + { + return false; + } + if (LADSPA_IS_HINT_LOGARITHMIC(hint_desc)) + { + def = expf(logf(lower_bound)*0.75 + logf(upper_bound)*0.25); + } + else + { + def = lower_bound*0.75 + upper_bound*0.25; + } + } + + if (LADSPA_IS_HINT_DEFAULT_MIDDLE(hint_desc)) + { + if (!LADSPA_IS_HINT_BOUNDED_BELOW(hint_desc) || + !LADSPA_IS_HINT_BOUNDED_ABOVE(hint_desc)) + { + return false; + } + if (LADSPA_IS_HINT_LOGARITHMIC(hint_desc)) + { + def = expf(logf(lower_bound)*0.5 + logf(upper_bound)*0.5); + } + else + { + def = lower_bound*0.5 + upper_bound*0.5; + } + } + + if (LADSPA_IS_HINT_DEFAULT_HIGH(hint_desc)) + { + if (!LADSPA_IS_HINT_BOUNDED_BELOW(hint_desc) || + !LADSPA_IS_HINT_BOUNDED_ABOVE(hint_desc)) + { + return false; + } + if (LADSPA_IS_HINT_LOGARITHMIC(hint_desc)) + { + def = expf(logf(lower_bound)*0.25 + logf(upper_bound)*0.75); + } + else + { + def = lower_bound*0.25 + upper_bound*0.75; + } + } + + if (LADSPA_IS_HINT_DEFAULT_MAXIMUM(hint_desc)) + { + if (!LADSPA_IS_HINT_BOUNDED_ABOVE(hint_desc)) + { + return false; + } + def = upper_bound; + } + + if (LADSPA_IS_HINT_DEFAULT_0(hint_desc)) + { + def = 0.0f; + } + + if (LADSPA_IS_HINT_DEFAULT_1(hint_desc)) + { + def = 1.0f; + } + + if (LADSPA_IS_HINT_DEFAULT_100(hint_desc)) + { + def = 100.0f; + } + + if (LADSPA_IS_HINT_DEFAULT_440(hint_desc)) + { + def = 440.0f; + } + + if (LADSPA_IS_HINT_INTEGER(hint_desc)) + { + def = roundf(def); + } + + return true; +} /* AudioLADSPAPlugin::setDefault */ + + +/* + * This file has not been truncated + */ diff --git a/src/async/audio/AsyncAudioLADSPAPlugin.h b/src/async/audio/AsyncAudioLADSPAPlugin.h new file mode 100644 index 000000000..fe4624314 --- /dev/null +++ b/src/async/audio/AsyncAudioLADSPAPlugin.h @@ -0,0 +1,382 @@ +/** +@file AsyncAudioLADSPAPlugin.h +@brief A class for using a LADSPA plugin as an audio processor +@author Tobias Blomberg / SM0SVX +@date 2023-12-09 + +\verbatim +Async - A library for programming event driven applications +Copyright (C) 2003-2024 Tobias Blomberg / SM0SVX + +This program is free software; you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation; either version 2 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program; if not, write to the Free Software +Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA +\endverbatim +*/ + +/** @example AsyncAudioLADSPAPlugin_demo.cpp +An example of how to use the AudioLADSPAPlugin class +*/ + +#ifndef ASYNC_AUDIO_LADSPA_PLUGIN_INCLUDED +#define ASYNC_AUDIO_LADSPA_PLUGIN_INCLUDED + + +/**************************************************************************** + * + * System Includes + * + ****************************************************************************/ + +extern "C" { + #include +}; + +#include +#include +#include + + +/**************************************************************************** + * + * Project Includes + * + ****************************************************************************/ + +#include + + +/**************************************************************************** + * + * Local Includes + * + ****************************************************************************/ + + + +/**************************************************************************** + * + * Forward declarations + * + ****************************************************************************/ + + + +/**************************************************************************** + * + * Namespace + * + ****************************************************************************/ + +namespace Async +{ + + +/**************************************************************************** + * + * Forward declarations of classes inside of the declared namespace + * + ****************************************************************************/ + + + +/**************************************************************************** + * + * Defines & typedefs + * + ****************************************************************************/ + + + +/**************************************************************************** + * + * Exported Global Variables + * + ****************************************************************************/ + + + +/**************************************************************************** + * + * Class definitions + * + ****************************************************************************/ + +/** +@brief Use a LADSPA plugin as an audio processor +@author Tobias Blomberg / SM0SVX +@date 2023-12-09 + +This class is used to load a LADSPA plugin and use it as an audio processor. + +\include AsyncAudioLADSPAPlugin_demo.cpp +*/ +class AudioLADSPAPlugin : public AudioProcessor +{ + public: + using PortNumber = decltype(LADSPA_Descriptor::PortCount); + using PluginIndex = unsigned long; + using UniqueID = unsigned long; + + /** + * @brief Find any LADSPA plugins in the given subdirectory + * @param dir The path to the directory to look in + * + * This function will go through all files in the given subdirectory, + * reading any *.so files it can find and load them as LADSPA plugins. The + * plugins are not fully initialized in this process but rather just the + * necessary calls are made to extract enough information to determine if + * this is a plugin that is compatible with this class. Any plugin found to + * be usable is put in an index so that we later can find plugins using + * their label. + */ + static bool findPluginsInDir(std::string dir); + + /** + * @brief Find LADSPA plugins in standard subdirectories + * + * LADSPA plugins are typically installed in /usr/lib64/ladspa (on a 64 bit + * x86 system). This default value is adapted automatically to match the + * target system during compilation of the software. + * + * If the LADSPA_PATH environment variable is set it will override the + * default path. + * + * This function is called by the "label variant" of the constructor if the + * plugin index is empty. + */ + static bool findPlugins(void); + + /** + * @brief Constructor + * @param path The full path of the plugin to load + * @param index The index of the plugin to instantiate + * + * This constructor is normally not used directly unless there is a special + * requirement to load a specific LADSPA plugin file with a known plugin + * index. + */ + AudioLADSPAPlugin(const std::string& path, PluginIndex index) + : m_path(path), m_index(index) {} + + /** + * @brief Constructor + * @param id The plugin unique id to look for + * + * Use this constructor for creating a LADSPA plugin instance if you know + * its unique ID. The easiest way normally is to use the label to find the + * plugin. + */ + AudioLADSPAPlugin(UniqueID id); + + /** + * @brief Constructor + * @param label The plugin label to look for + * + * This is the main constructor for creating a LADSPA plugin instance. + */ + AudioLADSPAPlugin(const std::string& label); + + /** + * @brief Disallow copy construction + */ + AudioLADSPAPlugin(const AudioLADSPAPlugin&) = delete; + + /** + * @brief Disallow copy assignment + */ + AudioLADSPAPlugin& operator=(const AudioLADSPAPlugin&) = delete; + + /** + * @brief Destructor + */ + ~AudioLADSPAPlugin(void); + + /** + * @brief Initialize the plugin + * @return Return \em true on success or else \em false is returned + * + * All loading, instantiation and initialization of the plugin is done in + * this function. The LADSPA activate call is called at the end of the + * function so it is directly ready to process audio when this function + * returns \em true. If the function returns \em false, it's not allowed to + * call any other functions so the object should be deleted as soon as + * possible. + */ + bool initialize(void); + + /** + * @brief Set a control input to the given value + * @param portno The port number to set + * @param val The value to set + */ + bool setControl(PortNumber portno, LADSPA_Data val); + + /** + * @brief Activate the plugin + * + * Use this function to activate this plugin. Activation is done in the + * initialize function so manual activation is normally not needed. Read + * more about plugin activation in the LADSPA documentation. + */ + void activate(void); + + /** + * @brief Deactivate the plugin + * + * Use this function to deactivate this plugin. Read more about plugin + * activation in the LADSPA documentation. + */ + void deactivate(void); + + /** + * @brief Get the path to the plugin + * @returns Returns the path to the plugin + */ + std::string path(void) const { return m_path; } + + /** + * @brief Get the unique ID for the plugin + * @returns Returns the unique ID of the plugin + * + * All plugins have a unique ID which can be used to find it. + */ + UniqueID uniqueId(void) const { return m_desc->UniqueID; } + + /** + * @brief Get the unique label for the plugin + * @returns Returns the unique label for the plugin + * + * The label is what most often is used to find a specific plugin. + */ + std::string label(void) const { return m_desc->Label; } + + /** + * @brief Get the name of the plugin + * @returns Returns the name of the plugin + * + * This function return the free text name/display name for the plugin. + */ + std::string name(void) const { return m_desc->Name; } + + /** + * @brief Get information on the maker of the plugin + * @returns Returns a string describing the plugin author. + */ + std::string maker(void) const { return m_desc->Maker; } + + /** + * @brief Get the copyright information for the plugin + * @returns Returns a string describing the copyright + */ + std::string copyright(void) const { return m_desc->Copyright; } + + /** + * @brief Get the number of ports for the plugin + * @returns Returns the total number of control/audio input/output ports + */ + PortNumber portCount(void) const { return m_desc->PortCount; } + + /** + * @brief Check if a port is a control port + * @returns Returns \em true if this is a control port + */ + bool portIsControl(PortNumber portno) const; + + /** + * @brief Check if a port is an audio port + * @returns Returns \em true if this is an audio port + */ + bool portIsAudio(PortNumber portno) const; + + /** + * @brief Check if a port is an input port + * @returns Returns \em true if this is an input port + */ + bool portIsInput(PortNumber portno) const; + + /** + * @brief Check if a port is an output port + * @returns Returns \em true if this is an output port + */ + bool portIsOutput(PortNumber portno) const; + + /** + * @brief Print some useful information for the plugin + */ + void print(const std::string& prefix=""); + + protected: + /** + * @brief Process incoming samples and put them into the output buffer + * @param dest Destination buffer + * @param src Source buffer + * @param count Number of samples in the source buffer + * + * This function should be reimplemented by the inheriting class to + * do the actual processing of the incoming samples. All samples must + * be processed, otherwise they are lost and the output buffer will + * contain garbage. + */ + virtual void processSamples(float *dest, const float *src, + int count) override; + + private: + struct InstanceInfo + { + std::string m_path; + PluginIndex m_index; + UniqueID m_unique_id; + std::string m_label; + }; + using InstanceInfoP = std::shared_ptr; + using LabelMap = std::map; + using IDMap = std::map; + + static const PortNumber NOPORT = 9999UL; + + static IDMap& idMap(void) + { + static IDMap id_map; + return id_map; + } + + static LabelMap& labelMap(void) + { + static LabelMap label_map; + return label_map; + } + + const LADSPA_Descriptor* ladspaDescriptor(void); + bool setDefault(PortNumber portno); + + std::string m_path; + void* m_handle = nullptr; + PluginIndex m_index = 0; + const LADSPA_Descriptor* m_desc = nullptr; + LADSPA_Handle m_inst_handle = nullptr; + bool m_is_active = false; + LADSPA_Data* m_ctrl_buf = nullptr; + PortNumber m_sample_input_port = NOPORT; + PortNumber m_sample_output_port = NOPORT; + +}; /* class AudioLADSPAPlugin */ + + +} /* namespace Async */ + +#endif /* ASYNC_AUDIO_LADSPA_PLUGIN_INCLUDED */ + +/* + * This file has not been truncated + */ diff --git a/src/async/audio/CMakeLists.txt b/src/async/audio/CMakeLists.txt index 504416a52..6e9fcafb4 100644 --- a/src/async/audio/CMakeLists.txt +++ b/src/async/audio/CMakeLists.txt @@ -110,6 +110,11 @@ if(OGG_FOUND AND Opus_FOUND) set(LIBSRC ${LIBSRC} AsyncAudioContainerOpus.cpp) endif(OGG_FOUND AND Opus_FOUND) +if(LADSPA_FOUND) + set(EXPINC ${EXPINC} AsyncAudioLADSPAPlugin.h) + set(LIBSRC ${LIBSRC} AsyncAudioLADSPAPlugin.cpp) +endif(LADSPA_FOUND) + if(USE_ALSA) set(LIBSRC ${LIBSRC} AsyncAudioDeviceAlsa.cpp) find_package(ALSA REQUIRED QUIET) diff --git a/src/async/demo/AsyncAudioLADSPAPlugin_demo.cpp b/src/async/demo/AsyncAudioLADSPAPlugin_demo.cpp new file mode 100644 index 000000000..733d04c21 --- /dev/null +++ b/src/async/demo/AsyncAudioLADSPAPlugin_demo.cpp @@ -0,0 +1,47 @@ +#include +#include +#include +#include +#include + +int main(void) +{ + Async::CppApplication app; + + Async::AudioIO audio_io("alsa:default", 0); + audio_io.open(Async::AudioIO::MODE_RDWR); + Async::AudioSource* prev_src = &audio_io; + + std::string label("tap_pitch"); + Async::AudioLADSPAPlugin p1(label); + if (!p1.initialize()) + { + std::cout << "*** ERROR: Could not instantiate LADSPA plugin instance " + "with label " << label << std::endl; + exit(1); + } + p1.setControl(0, 4); + p1.print(); + prev_src->registerSink(&p1); + prev_src = &p1; + + label = "tap_vibrato"; + Async::AudioLADSPAPlugin p2(label); + if (!p2.initialize()) + { + std::cout << "*** ERROR: Could not instantiate LADSPA plugin instance " + "with label " << label << std::endl; + exit(1); + } + p2.setControl(0, 10); + p2.setControl(1, 10); + p2.print(); + prev_src->registerSink(&p2); + prev_src = &p2; + + prev_src->registerSink(&audio_io); + + app.exec(); + + return 0; +} diff --git a/src/async/demo/CMakeLists.txt b/src/async/demo/CMakeLists.txt index 231c8fbe4..2394aad1d 100644 --- a/src/async/demo/CMakeLists.txt +++ b/src/async/demo/CMakeLists.txt @@ -11,6 +11,10 @@ set(CPPPROGS AsyncAudioIO_demo AsyncDnsLookup_demo AsyncFdWatch_demo set(QTPROGS AsyncQtApplication_demo) +if(LADSPA_FOUND) + set(CPPPROGS ${CPPPROGS} AsyncAudioLADSPAPlugin_demo) +endif(LADSPA_FOUND) + # Build all demo applications foreach(prog ${CPPPROGS}) diff --git a/src/cmake/Modules/FindLADSPA.cmake b/src/cmake/Modules/FindLADSPA.cmake new file mode 100644 index 000000000..f4382a1ad --- /dev/null +++ b/src/cmake/Modules/FindLADSPA.cmake @@ -0,0 +1,84 @@ +#.rst: +# FindLADSPA +# -------- +# Find the LADSPA include file +# +# LADSPA_FOUND - Set to true if the ladspa include file is found +# LADSPA_INCLUDE_DIRS - The directory where ladspa.h can be found +# LADSPA_PLUGIN_DIRS - The directories where LADSPA plugins can be found +# LADSPA_VERSION - Full version string (if available) +# LADSPA_VERSION_MAJOR - Major version (if available) +# LADSPA_VERSION_MINOR - Minor version (if available) + +#============================================================================= +# Copyright (C) 2003-2024 Tobias Blomberg / SM0SVX +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA +#============================================================================= + +if(CMAKE_MINIMUM_REQUIRED_VERSION VERSION_LESS 2.8.12) + message(AUTHOR_WARNING + "Your project should require at least CMake 2.6 to use FindLADSPA.cmake") +endif() + +# Try to find the directory where the ladspa.h header file is located +find_path(LADSPA_INCLUDE_DIR + NAMES ladspa.h + DOC "LADSPA include directory" +) + +# Set up version variables if the include file was found +if (LADSPA_INCLUDE_DIR) + file(READ "${LADSPA_INCLUDE_DIR}/ladspa.h" ladspa_h) + string(REGEX MATCH "#define LADSPA_VERSION \"([^ ]+)\"" _ ${ladspa_h}) + set(LADSPA_VERSION ${CMAKE_MATCH_1}) + string(REGEX MATCH "#define LADSPA_VERSION_MAJOR ([0-9]+)" _ ${ladspa_h}) + set(LADSPA_VERSION_MAJOR ${CMAKE_MATCH_1}) + string(REGEX MATCH "#define LADSPA_VERSION_MINOR ([0-9]+)" _ ${ladspa_h}) + set(LADSPA_VERSION_MINOR ${CMAKE_MATCH_1}) +endif (LADSPA_INCLUDE_DIR) + +# Find the LADSPA plugin directory +include(GNUInstallDirs) +find_file(LADSPA_PLUGIN_DIR + NAMES ladspa + DOC "LADSPA plugin directory" + PATHS /usr/${CMAKE_INSTALL_LIBDIR} /usr/lib + NO_DEFAULT_PATH +) + +# Handle the version, QUIETLY and REQUIRED arguments and set LADSPA_FOUND to +# TRUE if all required variables are available +include(FindPackageHandleStandardArgs) +find_package_handle_standard_args(LADSPA + FOUND_VAR LADSPA_FOUND + REQUIRED_VARS LADSPA_INCLUDE_DIR LADSPA_PLUGIN_DIR + VERSION_VAR LADSPA_VERSION +) + +# Set up other standard variables if LADSPA was found +if(LADSPA_FOUND) + set(LADSPA_INCLUDE_DIRS ${LADSPA_INCLUDE_DIR}) + set(LADSPA_PLUGIN_DIRS "${LADSPA_PLUGIN_DIR}") + set(LADSPA_DEFINITIONS + -DLADSPA_PLUGIN_DIRS="${LADSPA_PLUGIN_DIRS}" + -DLADSPA_VERSION=${LADSPA_VERSION} + -DLADSPA_VERSION_MAJOR=${LADSPA_VERSION_MAJOR} + -DLADSPA_VERSION_MINOR=${LADSPA_VERSION_MINOR}) +endif(LADSPA_FOUND) + +# Hide these variables for normal usage +mark_as_advanced(LADSPA_INCLUDE_DIR LADSPA_PLUGIN_DIR) + diff --git a/src/doc/man/svxlink.conf.5 b/src/doc/man/svxlink.conf.5 index 6bc8ef4ef..1bfac933e 100644 --- a/src/doc/man/svxlink.conf.5 +++ b/src/doc/man/svxlink.conf.5 @@ -1754,6 +1754,35 @@ be mFM;. It is possible to specify the same PTY for multiple functions (e.g. squelch, ptt etc) in both TX and RX configurations. This may be good if there is one script handling all functions. +.TP +.B LADSPA_PLUGINS +Used to set up one or more LADSPA plugins to process the received audio. The +processing chain is applied just before the final stages of the receiver audio +processing and will only affect how the audio sounds. Any digital signal +processing done on the audio before that, like DTMF, CTCSS etc, is unaffected. + +LADSPA is a framework for sharing pluggable audio components and there are +numerous plugins to choose from. Each plugin has a name ("label") and zero or +more configuration parameters ("control input ports"). To find out which +plugins are installed on your system, use the "listplugins" command. If you +are missing some plugin it may be available to install using your package +system (rpm/yum/dnf, deb/apt etc). To find out more information about a +plugin, use the "analyseplugin" command (e.g. analyseplugin filter hpf). Learn +more about the LADSPA framework in their official documentation. + +In SvxLink only plugins that have exactly one audio input and one audio +output can be used, so all stereo plugins are excluded for example. The same +thing is valid for input-only or output-only plugins. + +The analyseplugin utility will, among other things, list the ports. All ports +that are typed as "input, control" can be used to configure the plugin. Valid +range and a default value may also be listed. The format for the +LADSPA_PLUGINS configuration variable in SvxLink is +"label1:port1:port2:...:portN,label2:port1:port2:...:portN,...". Note that +only control input ports are counted here so when applying the parameters any +intermingled ports of other types are skipped. + +Example: LADSPA_PLUGINS=hpf:1000,tap_dynamic_m:4:500:15:15:13 . .SS Ddr Receiver Section . diff --git a/src/svxlink/ChangeLog b/src/svxlink/ChangeLog index 3ef960e08..1aacba37a 100644 --- a/src/svxlink/ChangeLog +++ b/src/svxlink/ChangeLog @@ -281,6 +281,10 @@ * Bugfix for the 1750_MUTING configuration variable. Setting it to 0 did not disable the feature. It had to be commented out. +* It's now possible to use LADSPA plugins to shape received audio. Fore more + information have a look at the documentation for the LADSPA_PLUGINS + configuration variable. + 1.7.0 -- 01 Sep 2019 diff --git a/src/svxlink/svxlink/svxlink.conf.in b/src/svxlink/svxlink/svxlink.conf.in index f04dba329..98929f518 100644 --- a/src/svxlink/svxlink/svxlink.conf.in +++ b/src/svxlink/svxlink/svxlink.conf.in @@ -252,6 +252,7 @@ DTMF_SERIAL=/dev/ttyS0 #1750_MUTING=1 #SEL5_DEC_TYPE=INTERNAL #SEL5_TYPE=ZVEI1 +#LADSPA_PLUGINS=hpf:1000 #FQ=433475000 #MODULATION=FM #WBRX=WbRx1 diff --git a/src/svxlink/trx/LocalRxBase.cpp b/src/svxlink/trx/LocalRxBase.cpp index 8bb1801ba..e354673ca 100644 --- a/src/svxlink/trx/LocalRxBase.cpp +++ b/src/svxlink/trx/LocalRxBase.cpp @@ -64,6 +64,9 @@ Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA #include #include #include +#ifdef LADSPA_VERSION +#include +#endif /**************************************************************************** @@ -598,11 +601,65 @@ bool LocalRxBase::initialize(void) // elimination), create it if (delay_line_len > 0) { + std::cout << name() << ": Delay line (for DTMF muting etc) set to " + << delay_line_len << " ms" << std::endl; delay = new AudioDelayLine(delay_line_len); prev_src->registerSink(delay, true); prev_src = delay; } +#ifdef LADSPA_VERSION + std::vector ladspa_plugin_cfg; + if (cfg().getValue(name(), "LADSPA_PLUGINS", ladspa_plugin_cfg)) + { + for (const auto& pcfg : ladspa_plugin_cfg) + { + std::istringstream is(pcfg); + std::string label; + std::getline(is, label, ':'); + //std::cout << "### pcfg=" << pcfg << " label=" << label << std::endl; + auto plug = new Async::AudioLADSPAPlugin(label); + if (!plug->initialize()) + { + std::cout << "*** ERROR: Could not instantiate LADSPA plugin instance " + "with label \"" << label << "\"" << std::endl; + return false; + } + unsigned long portno = 0; + LADSPA_Data val; + while (is >> val) + { + while ((portno < plug->portCount()) && + !(plug->portIsControl(portno) && plug->portIsInput(portno))) + { + ++portno; + } + if (portno >= plug->portCount()) + { + std::cerr << "*** ERROR: Too many parameters specified for LADSPA " + "plugin \"" << plug->label() + << "\" in configuration variable " << name() + << "/LADSPA_PLUGINS." << std::endl; + return false; + } + plug->setControl(portno++, val); + char colon = 0; + if ((is >> colon) && (colon != ':')) + { + std::cerr << "*** ERROR: Illegal format for " << name() + << "/LADSPA_PLUGINS configuration variable" << std::endl; + return false; + } + } + + plug->print(name() + ": "); + + prev_src->registerSink(plug, true); + prev_src = plug; + } + } +#endif + // Add a limiter to smoothly limit the audio before hard clipping it double limiter_thresh = DEFAULT_LIMITER_THRESH; cfg().getValue(name(), "LIMITER_THRESH", limiter_thresh); @@ -635,7 +692,7 @@ bool LocalRxBase::initialize(void) // Set the previous audio pipe object to handle audio distribution for // the LocalRxBase class - setHandler(prev_src); + setAudioSourceHandler(prev_src); cfg().getValue(name(), "AUDIO_DEV_KEEP_OPEN", audio_dev_keep_open); diff --git a/src/svxlink/trx/Rx.cpp b/src/svxlink/trx/Rx.cpp index 742764671..498bef18e 100644 --- a/src/svxlink/trx/Rx.cpp +++ b/src/svxlink/trx/Rx.cpp @@ -6,7 +6,7 @@ \verbatim SvxLink - A Multi Purpose Voice Services System for Ham Radio Use -Copyright (C) 2003-2008 Tobias Blomberg / SM0SVX +Copyright (C) 2003-2024 Tobias Blomberg / SM0SVX This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by @@ -224,6 +224,7 @@ Rx::Rx(Config &cfg, const string& name) Rx::~Rx(void) { delete m_sql_tmo_timer; + m_sql_tmo_timer = nullptr; } /* Rx::~Rx */ @@ -242,9 +243,9 @@ bool Rx::initialize(void) } } */ - + return true; - + } /* Rx::initialize */ @@ -341,6 +342,11 @@ void Rx::setSquelchState(bool is_open, const std::string& info) } /* Rx::setSquelchState */ +void Rx::setAudioSourceHandler(Async::AudioSource* src) +{ + setHandler(src); +} /* Rx::setAudioSourceHandler */ + /**************************************************************************** * diff --git a/src/svxlink/trx/Rx.h b/src/svxlink/trx/Rx.h index dbaa3faf8..24de8d019 100644 --- a/src/svxlink/trx/Rx.h +++ b/src/svxlink/trx/Rx.h @@ -6,7 +6,7 @@ \verbatim SvxLink - A Multi Purpose Voice Services System for Ham Radio Use -Copyright (C) 2003-2018 Tobias Blomberg / SM0SVX +Copyright (C) 2003-2024 Tobias Blomberg / SM0SVX This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by @@ -323,14 +323,16 @@ class Rx : public sigc::trackable, public Async::AudioSource */ void setSquelchState(bool is_open, const std::string& info=""); + void setAudioSourceHandler(Async::AudioSource* src); + private: - std::string m_name; - bool m_verbose; - bool m_sql_open; - Async::Config& m_cfg; - Async::Timer* m_sql_tmo_timer; - std::string m_sql_info; - MuteState m_mute_state; + std::string m_name; + bool m_verbose; + bool m_sql_open; + Async::Config& m_cfg; + Async::Timer* m_sql_tmo_timer; + std::string m_sql_info; + MuteState m_mute_state; void sqlTimeout(Async::Timer *t); diff --git a/src/svxlink/trx/SquelchCombine.cpp b/src/svxlink/trx/SquelchCombine.cpp index 614470f81..ed631bd23 100644 --- a/src/svxlink/trx/SquelchCombine.cpp +++ b/src/svxlink/trx/SquelchCombine.cpp @@ -423,7 +423,7 @@ bool SquelchCombine::initialize(Async::Config& cfg, return false; } - std::cout << rx_name << " combined squelch structure: "; + std::cout << rx_name << ": Combined squelch structure is "; m_comb->print(std::cout); std::cout << std::endl; diff --git a/src/versions b/src/versions index 61a2353eb..dc1669b6d 100644 --- a/src/versions +++ b/src/versions @@ -8,10 +8,10 @@ QTEL=1.2.4.99.6 LIBECHOLIB=1.3.3.99.3 # Version for the Async library -LIBASYNC=1.6.99.27 +LIBASYNC=1.6.99.28 # SvxLink versions -SVXLINK=1.7.99.94 +SVXLINK=1.7.99.95 MODULE_HELP=1.0.0 MODULE_PARROT=1.1.1 MODULE_ECHO_LINK=1.5.99.5