☰
noise-suppression-for-voice 中的 JUCE LV2_SDK:lilv 与其依赖库的嵌入式集成与改造
2026/10/15 10:43:28 网站建设 项目流程
  • 音频处理
  • 音频

【免费下载链接】noise-suppression-for-voice

Noise suppression plugin based on Xiph's RNNoise

项目地址:https://gitcode.com/gh_mirrors/no/noise-suppression-for-voice
点击查看免费下载

本文面向对 JUCE 音频插件工程内部实现感兴趣的开发者,围绕当前仓库external/JUCE/modules/juce_audio_processors/format_types/LV2_SDK/README.md这份说明文档,解读 JUCE 是如何将 lilv 及其依赖库(serd、sord、sratom、lv2、zix)以源码方式嵌入自身模块,并通过三处关键修改实现免系统依赖、跨平台可移植的 LV2 宿主能力。读完本文,你将掌握:LV2_SDK 目录的组成结构与改造清单、手工配置头文件juce_lv2_config.h的平台适配逻辑、JUCE 在运行时把嵌入的 LV2 数据包解压到临时目录再交给 lilv 加载的完整机制,以及这些能力在 RNNoise 降噪插件工程中的实际应用。

LV2_SDK 是什么:一份"嵌入式 lilv 发行版"

在 JUCE 的juce_audio_processors模块中,LV2 插件格式的宿主支持(JUCE_PLUGINHOST_LV2)依赖一个完整的 LV2 生态工具链。正常情况下,lilv、serd、sord 等库需要由系统包管理器安装(例如 Linux 发行版中的liblilv-dev、libserd-dev),并且 lilv 通过pkg-config/configure生成平台相关的 config 头文件。

而external/JUCE/modules/juce_audio_processors/format_types/LV2_SDK/目录(下文简称LV2_SDK)则把这条外部依赖链路整体拷贝进 JUCE 源码树,使其变成一个"自带电池"的嵌入式发行版。目录结构如下:

  • lilv/:lilv 库本体(C 头文件 lilv.h 与 C++ 绑定lilvmm.hpp),其src/下含 collections、filesystem、instance、lib、node、plugin、port、query、scalepoint、state、ui、util、world 等实现文件,以及内嵌的zix/tree.c红黑树容器;
  • lv2/:LV2 官方规范头文件与 Turtle(.ttl)数据,覆盖 core、atom、midi、ui、state、patch、presets、worker、urid 等二十余个扩展;
  • serd/:轻量级 RDF/ Turtle 序列化器(reader.c、writer.c、n3.c、uri.c等);
  • sord/:内存 RDF 模型/存储库,lilv 用它构建并查询插件元数据图谱;
  • sratom/:Atom 与 RDF 之间互转的辅助库;
  • juce_lv2_config.h、lilv_config.h、serd_config.h、sord_config.h:手工编写的配置头文件;
  • generate_lv2_bundle_sources.py:把lv2/下所有.ttl数据打包进 C++ 源码的生成脚本。

这份说明文档(README.md)的核心信息正是:这段源码并非原样照搬,而是经过三处针对性修改。

三处关键修改:从"系统库"到"嵌入式库"的改造清单

原文档列出的修改可以总结为一个目标:让 lilv 不再依赖构建时生成的头文件和系统级动态库,从而能被平滑地编进 JUCE 这样的大型 C++ 工程。

修改一:剔除构建非必需文件,包括生成的配置头文件

原 lilv 发行版中,configure/CMake 会生成config.h、lilv_config.h、serd_config.h、sord_config.h等平台相关头文件,同时还包含大量测试、文档与构建脚本。LV2_SDK 只保留编译 lilv 库本身严格需要的源文件与头文件,所有由构建系统生成的配置头文件一律移除,由后续的手写配置头文件取而代之。

从当前目录看,lilv/src/、serd/src/、sord/src/下均为纯净的.c/.h源码,sord侧的sord_config.h同样是 JUCE 手写的(见下文),没有任何 Autotools/CMake 生成的痕迹。

修改二:手工编写配置头文件

由于删除了生成的头文件,JUCE 在LV2_SDK中提供了三个手写配置头文件,而它们并没有各自定义宏,而是统一转发到唯一的事实来源 juce_lv2_config.h:

  • lilv_config.h;
  • serd_config.h;
  • sord_config.h。

三个文件内容一致:#pragma once后直接#include "juce_lv2_config.h",注释明确说明"不再为每个 LV2 相关库单独提供配置头,而是把所有配置集中在一个文件中"。

juce_lv2_config.h本身是理解整个改造的钥匙,其关键内容分为四组:

1. 静态编译宏

#define LILV_DYN_MANIFEST #define LILV_STATIC #define LV2_STATIC #define SERD_STATIC #define SORD_STATIC #define SRATOM_STATIC #define ZIX_STATIC

*_STATIC系列宏让 lilv、serd、sord、sratom、zix 都以静态编译形态进入 JUCE 二进制,避免了对外部.so/.dll的运行时依赖。LILV_DYN_MANIFEST保留 LV2 动态 manifest(dynmanifest)支持——即插件 bundle 的 manifest 可以在运行时由共享库动态生成,而不是预先生成.ttl文件。

2. 库版本号

#define LILV_VERSION "0.24.12" #define SERD_VERSION "0.30.10" #define SORD_VERSION "0.16.9"

这三个宏会被 lilv 的lilv_world_get_library_version()等版本查询接口使用。原文档特别强调:升级库版本时务必同步更新这里的版本号,否则宿主程序报告给用户的库版本信息将与实际代码不符。

3. C++ 启用宏

#define LILV_CXX 1

LILV_CXX让 lilv 的头文件启用 C++ 相关声明路径,配合 lilvmm.hpp 为 JUCE 的 C++ 宿主代码提供面向对象的 lilv 封装。

4. 平台相关的路径分隔符与默认 LV2 搜索路径

#if JUCE_WINDOWS #define LILV_DIR_SEP "\\" #define LILV_PATH_SEP ";" #else #define LILV_DIR_SEP "/" #define LILV_PATH_SEP ":" #endif

LILV_DIR_SEP与LILV_PATH_SEP直接决定 lilv 如何拼接路径和解析LV2_PATH环境变量。默认搜索路径LILV_DEFAULT_LV2_PATH则按平台区分:

平台默认 LV2 搜索路径
macOS / iOS~/Library/Audio/Plug-Ins/LV2、~/.lv2、/usr/local/lib/lv2、/usr/lib/lv2、/Library/Audio/Plug-Ins/LV2
Windows%APPDATA%\LV2、%COMMONPROGRAMFILES%\LV2
Linux / Android~/.lv2、/usr/lib/lv2、/usr/local/lib/lv2

与原生 lilv 不同,这里没有使用pkg-config探测,而是直接借助 JUCE 自身的平台宏(JUCE_WINDOWS、JUCE_MAC、JUCE_IOS、JUCE_LINUX、JUCE_ANDROID)在编译期选定正确取值,这也是手写配置头能够"一处定义、处处生效"的根本原因。

修改三:移除 world.c 中的 dlfcn.h 包含

第三处修改指向 lilv/src/world.c:原版world.c顶部直接#include <dlfcn.h>,而dlfcn.h是 POSIX 平台的动态库加载头,在 Windows/MSVC 上并不存在,会直接导致编译失败。

LV2_SDK 中删除了该包含。动态库加载相关的宏与函数被集中到 lilv/src/lilv_internal.h:

#ifdef _WIN32 # include <direct.h> # include <stdio.h> # include <windows.h> # define dlopen(path, flags) LoadLibrary(path) # define dlclose(lib) FreeLibrary((HMODULE)lib) ... static inline const char* dlerror(void) { return "Unknown error"; } #else # include <dlfcn.h> #endif

这样在 Windows 上dlopen/dlclose/dlerror被映射为LoadLibrary/FreeLibrary及固定错误字符串,在 POSIX 平台则沿用原生<dlfcn.h>,符号查找统一走lilv_dlfunc()(Windows 上为GetProcAddress)。world.c中的动态 manifest 加载代码(dlopen(lib_path, RTLD_LAZY)打开 manifest 生成库,再通过lv2_dyn_manifest_open/lv2_dyn_manifest_get_subjects等入口获取插件清单)因此得以跨平台编译。

源码级验证:lilv 如何进入 JUCE 构建

LV2_SDK 并非被静态地"放着",而是被 JUCE 的构建系统显式纳入编译。入口在 juce_audio_processors_lv2_libs.cpp:

#if JUCE_PLUGINHOST_LV2 #define _CRT_SECURE_NO_WARNINGS #include <juce_core/system/juce_CompilerWarnings.h> #include <juce_core/system/juce_TargetPlatform.h> #include "format_types/juce_LV2SupportLibs.cpp" #endif

在 juce_LV2SupportLibs.cpp 中,所有依赖库的.c文件以#include "..."的方式直接展开进同一个翻译单元,并包在extern "C"内:

extern "C" { #define is_windows_path serd_is_windows_path #include "serd/src/base64.c" #include "serd/src/byte_source.c" ... #include "sord/src/sord.c" #include "sord/src/syntax.c" #include "lilv/src/collections.c" ... #include "lilv/src/world.c" #include "lilv/src/zix/tree.c" ... #include "LV2_SDK/sratom/src/sratom.c" }

这里还体现了两个工程细节:

  • 文件顶部先#include "juce_lv2_config.h",保证所有依赖库在编译开始时即获得手写配置宏;
  • 通过JUCE_BEGIN_IGNORE_WARNINGS_GCC_LIKE/JUCE_BEGIN_IGNORE_WARNINGS_MSVC屏蔽第三方 C 代码的告警(如-Wc99-extensions、-Wcast-align、-Wconversion以及 MSVC 的 4100/4200/4244/4267 等),避免干扰 JUCE 自身的严格告警策略。

#define is_windows_path serd_is_windows_path、#define read_object sratom_read_object等宏再定义则用于消除不同库之间的符号命名冲突。正是这种"单一翻译单元 + 宏隔离"的做法,让原本各自独立的 C 库能够共存于同一个 C++ 工程中而不发生链接冲突。

值得注意的是,LV2 宿主能力由宏JUCE_PLUGINHOST_LV2控制(默认关闭,定义于 juce_audio_processors.h),Projucer 工程中通过isConfigFlagEnabled ("JUCE_PLUGINHOST_LV2", false)判断(见 jucer_Project.cpp),需要开发者按需显式开启。

运行时机制:嵌入的 LV2 数据如何变成可加载的 bundle

LV2 宿主要发现插件,必须在磁盘上存在真实的 bundle 目录(含manifest.ttl)。而 JUCE 不可能要求终端用户额外安装 LV2 规范数据文件,因此它把lv2/下的全部.ttl文件嵌入程序二进制,在运行时写出到临时目录再加载。

生成阶段:Turtle 数据 → C++ 源码

generate_lv2_bundle_sources.py 递归遍历 LV2 目录,筛出所有.ttl文件(filter_turtle),把每个文件内容用原始字符串字面量R"lv2ttl(...)lv2ttl"按 8000 字符分块打包,最终生成 juce_LV2Resources.h(文件头部明确标注"This file is auto-generated by generate_lv2_bundle_sources.py")。生成的数据结构为:

namespace juce::lv2 { struct BundleResource { const char* name; const char* contents; }; struct Bundle { const char* name; std::vector<BundleResource> contents; static std::vector<Bundle> getAllBundles(); }; }

每个 bundle 对应一个 LV2 扩展目录(如instance-access、midi、presets等),内含该目录下的manifest.ttl、*.ttl、*.meta.ttl等全部 Turtle 文件。

运行阶段:写临时目录 → lilv 加载

在 juce_LV2PluginFormat.cpp 的LV2PluginFormat::Pimpl构造函数中(对应第 5185–5210 行),加载流程如下:

world->loadAll(); // 加载系统 LV2_PATH 下的所有插件 const auto tempFile = lv2ResourceFolder.getFile(); if (tempFile.createDirectory()) { for (const auto& bundle : lv2::Bundle::getAllBundles()) { const auto pathToBundle = tempFile.getChildFile (bundle.name + String (".lv2")); if (! pathToBundle.createDirectory()) continue; for (const auto& resource : bundle.contents) pathToBundle.getChildFile (resource.name).replaceWithText (resource.contents); const auto pathString = File::addTrailingSeparator (pathToBundle.getFullPathName()); world->loadBundle (world->newFileUri (nullptr, pathString.toRawUTF8())); } }

逐行拆解:

  1. lv2ResourceFolder是TemporaryFile类型的成员(juce_LV2PluginFormat.cpp),构造时在系统临时目录创建一个唯一目录;
  2. 遍历lv2::Bundle::getAllBundles(),对每个 bundle 在其名下创建<名称>.lv2子目录;
  3. 用replaceWithText把嵌入的 Turtle 内容逐个写出为真实文件;
  4. 调用world->loadBundle()让 lilv 解析这些 bundle,把规范数据并入 RDF 模型;
  5. 析构函数中lv2ResourceFolder.getFile().deleteRecursively()在宿主退出时清理整个临时目录。

lilv 侧的数据支撑来自world.c的lilv_world_new():内部创建 sord 世界与 SPO 索引的 RDF 模型(sord_new(world->world, SORD_SPO | SORD_OPS, true)),后续所有插件查询(lilv_world_get_all_plugins、lilv_world_ask等,见 juce_LV2PluginFormat.cpp 的World封装)都基于该模型进行。

这套"编译期嵌入 + 运行时落盘"的设计,既免除了宿主开发者把规范数据打进安装包并安装到约定位置的负担,也保证了在隔离/只读环境中 LV2 宿主依然能发现内置 bundle。

在本仓库中的实际应用:RNNoise 降噪插件的 LV2 构建

本仓库noise-suppression-for-voice正是一个使用 JUCE 构建的实时降噪插件工程,LV2 是其主要输出格式之一。根 CMakeLists.txt 中默认开启BUILD_LV2_PLUGIN(值为ON),并声明了BUILD_VST_PLUGIN、BUILD_VST3_PLUGIN、BUILD_LADSPA_PLUGIN、BUILD_AU_PLUGIN、BUILD_AUV3_PLUGIN等可选开关。

src/juce_plugin/CMakeLists.txt 中通过juce_add_plugin按格式生成插件目标:

juce_add_plugin(${PLUGIN_NAME} ... FORMATS "${formats}" LV2URI "https://github.com/werman/noise-suppression-for-voice#${URI_SUFFIX}" ... LV2_COPY_DIR "${CMAKE_LIBRARY_OUTPUT_DIRECTORY}")

因为 LV2 需要为不同通道数提供独立库,工程对_mono(1 通道)与_stereo(2 通道)分别调用compile_plugins(见 src/juce_plugin/CMakeLists.txt),并在 Linux 安装阶段把rnnoise${suffix}.lv2目录安装到${CMAKE_INSTALL_LIBDIR}/lv2(第 91–101 行)。插件实例层面,RnNoiseAudioProcessor.cpp 在processBlock中把 VAD 阈值、VAD 宽限时间等参数(定义于 RnNoiseAudioProcessor.h,默认值分别对应构造器中的 0.6、20、0)传入 RNNoise 处理内核,再经由 lilv 描述的标准 LV2 端口暴露给宿主。

虽然本项目是 LV2插件而非 LV2宿主,但 JUCE 在编译 LV2 插件时同样会编译整个juce_audio_processors模块;LV2_SDK 的存在保证了模块在任何平台上都能无外部依赖地完成编译——这正是这份 README 改造清单在整个工程中的实际价值:它让 LV2 支持从"系统库依赖"变为"仓库内自洽"。

升级维护要点

原文档以"Remember to update the versions in the config headers if you ever update the library versions!"作结,这句话值得展开为一条明确的维护清单:

  1. 同步版本宏:替换 lilv/serd/sord 源码后,必须在 juce_lv2_config.h 中把LILV_VERSION、SERD_VERSION、SORD_VERSION更新为对应新版本号;
  2. 复核头文件依赖:确认新版本没有引入新的构建生成头文件或新的平台相关包含(尤其警惕<dlfcn.h>这类 POSIX 专属头,必要时按"修改三"的模式收敛到lilv_internal.h的宏适配层);
  3. 重生成资源数据:若 LV2 规范数据有更新,需重新运行generate_lv2_bundle_sources.py生成新的 juce_LV2Resources.h;
  4. 跨平台回归:由于静态链接与动态库宏(LILV_DYN_MANIFEST)同时生效,至少应在 Linux(dlopen 路径)与 Windows(LoadLibrary 路径)各做一次编译与插件发现验证。

小结

LV2_SDK 目录以极小篇幅的说明文档,记录了一次典型的第三方 C 库嵌入式改造:删生成物、换手写配置、收拢平台差异。配合 juce_LV2SupportLibs.cpp 的单一翻译单元编译、generate_lv2_bundle_sources.py 的数据嵌入以及Pimpl中的临时目录落盘机制,JUCE 得以在零系统依赖的前提下为各平台提供完整的 LV2 宿主/插件支持。对需要把 lilv 集成进自有 C++ 工程的开发者而言,这份 README 与其背后的源码,是一份可以直接参考的工程范本。

  • 音频处理
  • 音频

【免费下载链接】noise-suppression-for-voice

Noise suppression plugin based on Xiph's RNNoise

项目地址:https://gitcode.com/gh_mirrors/no/noise-suppression-for-voice
点击查看免费下载

相关推荐

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询