/////////////////////////////////////////////////////////////////////////////////// // Copyright (C) 2026 Jon Beniston, M7RCE // // Some code by AI // // // // 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 as version 3 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 V3 for more details. // // // // You should have received a copy of the GNU General Public License // // along with this program. If not, see . // /////////////////////////////////////////////////////////////////////////////////// #include #include #include #include #include #include #include #ifdef Q_OS_WIN #define WIN32_LEAN_AND_MEAN #ifndef NOMINMAX #define NOMINMAX #endif #include #endif #include "nvidiaaudioeffects.h" namespace { bool findInstallation(QString& libraryPath, QString& modelPath, QString& error) { #if defined(Q_OS_WIN) || defined(Q_OS_LINUX) QString sdkDir = qEnvironmentVariable("NVAFX_SDK_DIR"); #ifdef Q_OS_WIN if (sdkDir.isEmpty()) { sdkDir = QDir(qEnvironmentVariable("ProgramFiles")).filePath("NVIDIA Corporation/NVIDIA Audio Effects SDK"); } libraryPath = QDir(sdkDir).filePath("NVAudioEffects.dll"); modelPath = QDir(sdkDir).filePath("models/denoiser_48k.trtpkg"); #else if (sdkDir.isEmpty()) { sdkDir = qEnvironmentVariable("AFX_SDK_ROOT"); } if (sdkDir.isEmpty()) { error = QStringLiteral("Set NVAFX_SDK_DIR to the Linux Audio Effects SDK root"); return false; } libraryPath = QDir(sdkDir).filePath("nvafx/lib/libnv_audiofx.so"); modelPath = qEnvironmentVariable("NVAFX_MODEL_PATH"); if (modelPath.isEmpty()) { // Packages contain GPU-specific models. Pick one only if unambiguous. QStringList models; for (const QString& feature : {QStringLiteral("denoiser"), QStringLiteral("nvafxdenoiser")}) { const QDir modelDir(QDir(sdkDir).filePath(QStringLiteral("features/%1/models").arg(feature))); for (const QString& gpuDir : modelDir.entryList(QDir::Dirs | QDir::NoDotAndDotDot)) { const QString path = modelDir.filePath(gpuDir + QStringLiteral("/denoiser_48k.trtpkg")); if (QFileInfo(path).isFile()) { models.append(path); } } } if (models.size() == 1) { modelPath = models.first(); } else { error = QStringLiteral("Set NVAFX_MODEL_PATH to this GPU's denoiser_48k.trtpkg (found %1 models)").arg(models.size()); return false; } } #endif if (!QFileInfo(libraryPath).isFile() || !QFileInfo(modelPath).isFile()) { error = QStringLiteral("NVIDIA Audio Effects library or 48 kHz model was not found: %1, %2").arg(libraryPath, modelPath); return false; } return true; #else Q_UNUSED(libraryPath) Q_UNUSED(modelPath) error = QStringLiteral("NVIDIA Audio Effects is available only on Windows and Linux"); return false; #endif } } #if defined(Q_OS_WIN) || defined(Q_OS_LINUX) struct NvidiaAudioEffects::Impl { using CreateEffect = int (*)(const char *, void **); using DestroyEffect = int (*)(void *); using SetString = int (*)(void *, const char *, const char *); using SetU32 = int (*)(void *, const char *, unsigned int); using SetFloat = int (*)(void *, const char *, float); using GetU32 = int (*)(void *, const char *, unsigned int *); using Load = int (*)(void *); using Run = int (*)(void *, const float **, float **, unsigned int, unsigned int); QLibrary library; void *effect = nullptr; QByteArray modelPath; Qt::HANDLE threadId = nullptr; DestroyEffect destroyEffect = nullptr; Run run = nullptr; }; #else struct NvidiaAudioEffects::Impl {}; #endif NvidiaAudioEffects::NvidiaAudioEffects() : m_impl(nullptr) { } NvidiaAudioEffects::~NvidiaAudioEffects() { shutdown(); } bool NvidiaAudioEffects::isAvailable(QString& error) { QString libraryPath; QString modelPath; return findInstallation(libraryPath, modelPath, error); } bool NvidiaAudioEffects::initialize(QString& error, float intensityRatio, bool enableVad) { shutdown(); #if defined(Q_OS_WIN) || defined(Q_OS_LINUX) QString libraryPath; QString modelPath; if (!findInstallation(libraryPath, modelPath, error)) { return false; } m_impl = new Impl; m_impl->modelPath = QFile::encodeName(QDir::toNativeSeparators(modelPath)); m_impl->library.setFileName(QFileInfo(libraryPath).absoluteFilePath()); #ifdef Q_OS_WIN // QLibrary's ordinary load cannot find this SDK's adjacent DLL dependencies. // Load once with the SDK directory in the dependency search path, then let // QLibrary acquire and own its own reference to the already loaded DLL. HMODULE bootstrap = LoadLibraryExW(reinterpret_cast(QDir::toNativeSeparators(libraryPath).utf16()), nullptr, LOAD_LIBRARY_SEARCH_DLL_LOAD_DIR | LOAD_LIBRARY_SEARCH_DEFAULT_DIRS); if (!bootstrap) { error = QStringLiteral("Could not load NVIDIA Audio Effects DLL (Windows error %1)").arg(GetLastError()); shutdown(); return false; } #endif const bool loaded = m_impl->library.load(); #ifdef Q_OS_WIN FreeLibrary(bootstrap); #endif if (!loaded) { error = QStringLiteral("Could not load NVIDIA Audio Effects library: %1").arg(m_impl->library.errorString()); shutdown(); return false; } m_impl->threadId = QThread::currentThreadId(); const auto resolve = [this](const char *name) { return m_impl->library.resolve(name); }; const auto createEffect = reinterpret_cast(resolve("NvAFX_CreateEffect")); m_impl->destroyEffect = reinterpret_cast(resolve("NvAFX_DestroyEffect")); const auto setString = reinterpret_cast(resolve("NvAFX_SetString")); const auto setU32 = reinterpret_cast(resolve("NvAFX_SetU32")); const auto setFloat = reinterpret_cast(resolve("NvAFX_SetFloat")); const auto getU32 = reinterpret_cast(resolve("NvAFX_GetU32")); const auto load = reinterpret_cast(resolve("NvAFX_Load")); m_impl->run = reinterpret_cast(resolve("NvAFX_Run")); if (!createEffect || !m_impl->destroyEffect || !setString || !setU32 || !setFloat || !getU32 || !load || !m_impl->run) { error = QStringLiteral("NVIDIA Audio Effects library is missing required functions"); shutdown(); return false; } int status = createEffect("denoiser", &m_impl->effect); if (status == 0) { status = setU32(m_impl->effect, "input_sample_rate", 48000); } if (status == 0) { status = setFloat(m_impl->effect, "intensity_ratio", qBound(0.0f, intensityRatio, 1.0f)); } if (status == 0) { status = setU32(m_impl->effect, "enable_vad", enableVad ? 1U : 0U); } #ifdef Q_OS_LINUX if (status == 0) { status = setU32(m_impl->effect, "num_streams", 1); } if (status == 0) { status = setU32(m_impl->effect, "num_samples_per_input_frame", 480); } #endif if (status == 0) { status = setString(m_impl->effect, "model_path", m_impl->modelPath.constData()); } if (status == 0) { status = load(m_impl->effect); } unsigned int frameSize = 0; unsigned int outputFrameSize = 0; unsigned int inputChannels = 0; unsigned int outputChannels = 0; unsigned int outputSampleRate = 0; #ifdef Q_OS_LINUX const char *inputFrameSelector = "num_samples_per_input_frame"; const char *outputFrameSelector = "num_samples_per_output_frame"; #else const char *inputFrameSelector = "num_input_samples_per_frame"; const char *outputFrameSelector = "num_output_samples_per_frame"; #endif if (status == 0) { status = getU32(m_impl->effect, inputFrameSelector, &frameSize); } if (status == 0) { status = getU32(m_impl->effect, outputFrameSelector, &outputFrameSize); } if (status == 0) { status = getU32(m_impl->effect, "num_input_channels", &inputChannels); } if (status == 0) { status = getU32(m_impl->effect, "num_output_channels", &outputChannels); } if (status == 0) { status = getU32(m_impl->effect, "output_sample_rate", &outputSampleRate); } if (status != 0 || frameSize != 480 || outputFrameSize != 480 || inputChannels != 1 || outputChannels != 1 || outputSampleRate != 48000) { error = QStringLiteral("NVIDIA denoiser initialization failed (status %1, frames %2/%3, channels %4/%5, output rate %6)") .arg(status).arg(frameSize).arg(outputFrameSize).arg(inputChannels).arg(outputChannels).arg(outputSampleRate); shutdown(); return false; } return true; #else error = QStringLiteral("NVIDIA Audio Effects is available only on Windows and Linux"); return false; #endif } void NvidiaAudioEffects::shutdown() { if (!m_impl) { return; } #if defined(Q_OS_WIN) || defined(Q_OS_LINUX) if (m_impl->effect && m_impl->destroyEffect) { m_impl->destroyEffect(m_impl->effect); } if (m_impl->library.isLoaded()) { m_impl->library.unload(); } #endif delete m_impl; m_impl = nullptr; } bool NvidiaAudioEffects::process(const float *input, float *output, QString& error) { #if defined(Q_OS_WIN) || defined(Q_OS_LINUX) if (!m_impl || !m_impl->effect) { error = QStringLiteral("NVIDIA denoiser is not initialized"); return false; } if (QThread::currentThreadId() != m_impl->threadId) { error = QStringLiteral("NVIDIA denoiser was called from a different thread than it was initialized on"); return false; } // Keep the caller's input intact for failure passthrough. std::memcpy(output, input, 480 * sizeof(float)); #ifdef Q_OS_WIN // The installed Windows runtime processes the buffer in place, as in OBS. const float *inputs[] = {output}; #else const float *inputs[] = {input}; #endif float *outputs[] = {output}; const int status = m_impl->run(m_impl->effect, inputs, outputs, 480, 1); if (status != 0) { error = QStringLiteral("NVIDIA denoiser processing failed (status %1)").arg(status); shutdown(); return false; } return true; #else Q_UNUSED(input) Q_UNUSED(output) error = QStringLiteral("NVIDIA Audio Effects is available only on Windows and Linux"); return false; #endif }