/** * Copyright © 2017-2026 Wellington Wallace * * This file is part of Easy Effects. * * Easy Effects 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 3 of the License, or * (at your option) any later version. * * Easy Effects 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 Easy Effects. If not, see . */ #include "convolver_kernel_manager.hpp" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include "db_manager.hpp" #include "easyeffects_db_convolver.h" #include "pipeline_type.hpp" #include "resampler.hpp" #include "tags_app.hpp" #include "util.hpp" ConvolverKernelManager::ConvolverKernelManager(DbConvolver* settings, const PipelineType& pipeline_type) : settings(settings), pipeline_type(pipeline_type), app_data_dir(QStandardPaths::writableLocation(QStandardPaths::AppDataLocation).toStdString()), local_dir_irs(app_data_dir + "/irs") { /** * Flatpak specific path (.flatpak-info always present for apps running in * the flatpak sandbox) */ if (std::filesystem::is_regular_file(tags::app::flatpak_info_file)) { system_data_dir_irs.emplace_back("/app/extensions/Presets/irs"); } // Regular paths. for (auto& dir : QStandardPaths::standardLocations(QStandardPaths::AppDataLocation)) { dir += dir.endsWith("/") ? "" : "/"; system_data_dir_irs.push_back(dir.toStdString() + "irs"); } } auto ConvolverKernelManager::KernelData::isValid() const -> bool { return rate > 0 && !channel_L.empty() && !channel_R.empty() && channel_L.size() == channel_R.size(); } auto ConvolverKernelManager::KernelData::duration() const -> double { if (rate == 0 || channel_L.empty()) { return 0.0; } return static_cast(channel_L.size() - 1) / static_cast(rate); } auto ConvolverKernelManager::KernelData::sampleCount() const -> size_t { return channel_L.size(); } auto ConvolverKernelManager::loadKernel(const std::string& name) -> KernelData { if (name.empty()) { util::warning("Kernel name is empty"); return KernelData{}; } const auto file_path = searchKernelPath(name); if (file_path.empty()) { util::warning(std::format("Kernel '{}' not found", name)); return KernelData{}; } KernelData kernel_data; const auto extension = getFileExtension(file_path); if (extension == sofa_ext) { kernel_data = readSofaKernelFile(file_path); } else { kernel_data = readKernelFile(file_path); } kernel_data.name = QString::fromStdString(name); kernel_data.file_path = QString::fromStdString(file_path); if (!validateKernel(kernel_data)) { util::warning(std::format("Kernel '{}' is invalid", name)); return KernelData{}; } util::debug(std::format("Loaded kernel '{}': {} Hz, {} samples, {:.3f}s", name, kernel_data.rate, kernel_data.sampleCount(), kernel_data.duration())); return kernel_data; } auto ConvolverKernelManager::combineKernels(const std::string& kernel1_name, const std::string& kernel2_name, const std::string& output_name) -> bool { if (output_name.empty()) { util::warning("Output kernel name is empty"); return false; } const auto kernel1 = loadKernel(kernel1_name); const auto kernel2 = loadKernel(kernel2_name); if (!kernel1.isValid() || !kernel2.isValid()) { util::warning("One or both kernels are invalid for combination"); return false; } if (kernel1.channels != kernel2.channels) { util::warning("The kernels must have the same number of channel to be combined!"); return false; } const auto target_rate = std::max(kernel1.rate, kernel2.rate); const auto resampled_kernel1 = (kernel1.rate != target_rate) ? resampleKernel(kernel1, target_rate) : kernel1; const auto resampled_kernel2 = (kernel2.rate != target_rate) ? resampleKernel(kernel2, target_rate) : kernel2; auto combined_kernel_L = directConvolution(resampled_kernel1.channel_L, resampled_kernel2.channel_L); auto combined_kernel_R = directConvolution(resampled_kernel1.channel_R, resampled_kernel2.channel_R); KernelData combined_kernel; combined_kernel.rate = target_rate; combined_kernel.channels = resampled_kernel1.channels; combined_kernel.channel_L = std::move(combined_kernel_L); combined_kernel.channel_R = std::move(combined_kernel_R); if (combined_kernel.channels == 4) { combined_kernel.channel_LR = directConvolution(resampled_kernel1.channel_LR, resampled_kernel2.channel_LR); combined_kernel.channel_RL = directConvolution(resampled_kernel1.channel_RL, resampled_kernel2.channel_RL); } combined_kernel.name = QString::fromStdString(output_name); // Save the combined kernel const auto success = saveKernel(combined_kernel, output_name); if (success) { util::debug( std::format("Successfully combined kernels '{}' and '{}' into '{}'", kernel1_name, kernel2_name, output_name)); } return success; } auto ConvolverKernelManager::searchKernelPath(const std::string& name) -> std::string { // Given the irs name without extension, search the full path on the filesystem. const std::vector extensions = {irs_ext, sofa_ext}; const auto community_package = (pipeline_type == PipelineType::input) ? DbMain::lastLoadedInputCommunityPackage() : DbMain::lastLoadedOutputCommunityPackage(); std::string kernel_full_path; if (community_package.isEmpty()) { for (const auto& ext : extensions) { std::string path = local_dir_irs; path.append("/"); path.append(name); path.append(ext); auto local_kernel_file = std::filesystem::path{path}; if (std::filesystem::exists(local_kernel_file)) { kernel_full_path = local_kernel_file.string(); break; } } } else { // Search kernel file in community package paths. for (const auto& ext : extensions) { auto kernel_filename = name; kernel_filename.append(ext); for (const auto& xdg_irs_dir : system_data_dir_irs) { if (util::search_filename(std::filesystem::path{xdg_irs_dir + "/" + community_package.toStdString()}, kernel_filename, kernel_full_path, 3U)) { break; } } if (!kernel_full_path.empty()) { break; } } } return kernel_full_path; } auto ConvolverKernelManager::resampleKernel(const KernelData& kernel, const uint& target_rate) -> KernelData { if (target_rate == 0) { util::debug(std::format("Target rate value is zero. Aborting resampling of kernel {}", kernel.name.toStdString())); return kernel; } if (kernel.rate == target_rate || !kernel.isValid()) { util::debug(std::format("The kernel '{}' does not need resampling", kernel.name.toStdString())); return kernel; } KernelData resampled_kernel = kernel; resampled_kernel.rate = target_rate; util::debug( std::format("Resampling kernel '{}' from {} Hz to {} Hz", kernel.name.toStdString(), kernel.rate, target_rate)); // Resample left channel auto resampler = std::make_unique(kernel.rate, target_rate); resampled_kernel.channel_L = resampler->process(kernel.channel_L); // Resample right channel resampler = std::make_unique(kernel.rate, target_rate); resampled_kernel.channel_R = resampler->process(kernel.channel_R); if (kernel.channels == 4) { // Resample LR channel resampler = std::make_unique(kernel.rate, target_rate); resampled_kernel.channel_LR = resampler->process(kernel.channel_LR); // Resample right channel resampler = std::make_unique(kernel.rate, target_rate); resampled_kernel.channel_RL = resampler->process(kernel.channel_RL); } return resampled_kernel; } void ConvolverKernelManager::normalizeKernel(KernelData& kernel) { if (!kernel.isValid()) { return; } // Find the maximum absolute value across both channels const auto max_abs_L = std::ranges::max(kernel.channel_L, [](const auto& a, const auto& b) { return std::fabs(a) < std::fabs(b); }); const auto max_abs_R = std::ranges::max(kernel.channel_R, [](const auto& a, const auto& b) { return std::fabs(a) < std::fabs(b); }); auto peak = std::max(std::fabs(max_abs_L), std::fabs(max_abs_R)); if (kernel.channels == 4) { const auto max_abs_LR = std::ranges::max(kernel.channel_LR, [](const auto& a, const auto& b) { return std::fabs(a) < std::fabs(b); }); const auto max_abs_RL = std::ranges::max(kernel.channel_RL, [](const auto& a, const auto& b) { return std::fabs(a) < std::fabs(b); }); peak = std::max(peak, std::fabs(max_abs_LR)); peak = std::max(peak, std::fabs(max_abs_RL)); } if (peak <= 1e-6) { return; } // Normalize both channels const auto normalize_lambda = [peak](auto& sample) { sample /= peak; }; std::ranges::for_each(kernel.channel_L, normalize_lambda); std::ranges::for_each(kernel.channel_R, normalize_lambda); if (kernel.channels == 4) { std::ranges::for_each(kernel.channel_LR, normalize_lambda); std::ranges::for_each(kernel.channel_RL, normalize_lambda); } } auto ConvolverKernelManager::saveKernel(const KernelData& kernel, const std::string& file_name) -> bool { if (!kernel.isValid() || file_name.empty()) { return false; } const auto file_path = std::filesystem::path{local_dir_irs} / (file_name + irs_ext); try { // Ensure directory exists std::filesystem::create_directories(file_path.parent_path()); // Prepare interleaved buffer std::vector buffer(kernel.sampleCount() * kernel.channels); for (size_t i = 0; i < kernel.sampleCount(); ++i) { if (kernel.channels == 2) { buffer[2 * i] = kernel.channel_L[i]; buffer[(2 * i) + 1] = kernel.channel_R[i]; } else if (kernel.channels == 4) { buffer[4 * i] = kernel.channel_L[i]; buffer[(4 * i) + 1] = kernel.channel_LR[i]; buffer[(4 * i) + 2] = kernel.channel_RL[i]; buffer[(4 * i) + 3] = kernel.channel_R[i]; } } // Write to file const auto mode = SFM_WRITE; const auto format = SF_FORMAT_WAV | SF_FORMAT_PCM_32; const auto channels = kernel.channels; SndfileHandle sndfile(file_path.string(), mode, format, channels, kernel.rate); if (sndfile.error() != 0) { util::warning(std::format("Failed to create kernel file: {}", sndfile.strError())); return false; } const auto frames_written = sndfile.writef(buffer.data(), static_cast(kernel.sampleCount())); if (frames_written != static_cast(kernel.sampleCount())) { util::warning(std::format("Failed to write all kernel data: {} of {} frames written", frames_written, kernel.sampleCount())); return false; } util::debug(std::format("Saved kernel '{}' to {}", file_name, file_path.string())); return true; } catch (const std::exception& e) { util::warning(std::format("Exception while saving kernel: {}", e.what())); return false; } } auto ConvolverKernelManager::readKernelFile(const std::string& file_path) -> KernelData { KernelData kernel_data; try { SndfileHandle sndfile(file_path); if (sndfile.error() != 0) { util::warning(std::format("Failed to open kernel file: {} - {}", file_path, sndfile.strError())); return kernel_data; } if (sndfile.channels() != 1 && sndfile.channels() != 2 && sndfile.channels() != 4 && sndfile.frames() == 0) { util::warning(std::format("Only mono, stereo and true stereo impulse files are supported: {}", file_path)); return kernel_data; } // Read interleaved data std::vector buffer(sndfile.frames() * sndfile.channels()); const auto frames_read = sndfile.readf(buffer.data(), sndfile.frames()); if (frames_read != sndfile.frames()) { util::warning( std::format("Failed to read complete kernel file: {} of {} frames read", frames_read, sndfile.frames())); return kernel_data; } // Deinterleave channels kernel_data.rate = sndfile.samplerate(); kernel_data.original_rate = sndfile.samplerate(); kernel_data.channels = sndfile.channels() == 1 ? 2 : sndfile.channels(); kernel_data.channel_L.resize(sndfile.frames()); kernel_data.channel_R.resize(sndfile.frames()); if (kernel_data.channels == 4) { kernel_data.channel_LR.resize(sndfile.frames()); kernel_data.channel_RL.resize(sndfile.frames()); } for (size_t i = 0; i < static_cast(sndfile.frames()); ++i) { if (sndfile.channels() == 1) { kernel_data.channel_L[i] = buffer[i]; kernel_data.channel_R[i] = buffer[i]; } else if (sndfile.channels() == 2) { kernel_data.channel_L[i] = buffer[2 * i]; kernel_data.channel_R[i] = buffer[(2 * i) + 1]; } else if (sndfile.channels() == 4) { kernel_data.channel_L[i] = buffer[4 * i]; kernel_data.channel_LR[i] = buffer[(4 * i) + 1]; kernel_data.channel_RL[i] = buffer[(4 * i) + 2]; kernel_data.channel_R[i] = buffer[(4 * i) + 3]; } } } catch (const std::exception& e) { util::warning(std::format("Exception while reading kernel file {}: {}", file_path, e.what())); } return kernel_data; } auto ConvolverKernelManager::validateKernel(const KernelData& kernel) -> bool { if (kernel.rate <= 0) { util::warning("Kernel has invalid sample rate"); return false; } if (kernel.channel_L.empty() || kernel.channel_R.empty()) { util::warning("Kernel channels are empty"); return false; } if ((kernel.channels == 4) & (kernel.channel_LR.empty() || kernel.channel_RL.empty())) { util::warning("Kernel channels are empty"); return false; } if (kernel.channel_L.size() != kernel.channel_R.size()) { util::warning("Kernel channels have different sizes"); return false; } if ((kernel.channels == 4) & (kernel.channel_L.size() != kernel.channel_R.size())) { util::warning("Kernel LR and RL channels have different sizes"); return false; } const auto has_invalid_values = [](const std::vector& channel) { return std::ranges::any_of(channel, [](const auto& value) { return std::isnan(value) || std::isinf(value); }); }; if (has_invalid_values(kernel.channel_L) || has_invalid_values(kernel.channel_R)) { util::warning("Kernel contains NaN or infinite values"); return false; } if ((kernel.channels == 4) & has_invalid_values(kernel.channel_LR) || has_invalid_values(kernel.channel_RL)) { util::warning("Kernel contains NaN or infinite values"); return false; } return true; } auto ConvolverKernelManager::findKernelInDirectory(const std::filesystem::path& directory, const std::string& kernel_name) -> std::string { if (!std::filesystem::exists(directory)) { return ""; } const auto kernel_filename = kernel_name + irs_ext; try { for (const auto& entry : std::filesystem::recursive_directory_iterator(directory)) { if (entry.is_regular_file() && entry.path().filename() == kernel_filename) { return entry.path().string(); } } } catch (const std::exception& e) { util::warning(std::format("Error searching for kernel in {}: {}", directory.string(), e.what())); } return ""; } auto ConvolverKernelManager::directConvolution(const std::vector& a, const std::vector& b) -> std::vector { if (a.empty() || b.empty()) { return {}; } const auto output_size = a.size() + b.size() - 1; std::vector result(output_size, 0.0F); std::vector indices(output_size); std::iota(indices.begin(), indices.end(), 0U); auto each = [&](const int n) { result[n] = 0.0F; const int a_size = static_cast(a.size()); const int b_size = static_cast(b.size()); for (int m = 0; m < b_size; m++) { if (const auto z = n - m; z >= 0 && z < a_size - 1) { result[n] += b[m] * a[z]; } } }; #if defined(ENABLE_LIBCPP_WORKAROUNDS) || defined(_LIBCPP_HAS_NO_INCOMPLETE_PSTL) || \ (defined(_LIBCPP_VERSION) && _LIBCPP_VERSION < 170000) std::for_each(indices.begin(), indices.end(), each); #else std::for_each(std::execution::par_unseq, indices.begin(), indices.end(), each); #endif return result; } auto ConvolverKernelManager::getFileExtension(const std::string& file_path) -> std::string { std::filesystem::path path(file_path); std::string ext = path.extension().string(); // Convert to lowercase for case-insensitive comparison std::ranges::transform(ext, ext.begin(), [](unsigned char c) { return std::tolower(c); }); return ext; } // https://www.sofaconventions.org/mediawiki/index.php/SOFA_specifications auto ConvolverKernelManager::readSofaKernelFile(const std::string& file_path) -> KernelData { KernelData kernel_data; try { int error = 0; struct MYSOFA_HRTF* hrtf = mysofa_load(file_path.c_str(), &error); if (error != MYSOFA_OK) { util::warning(std::format("Error while trying to load the sofa file: {}", util::mysofa_error_to_string(error))); } if (!hrtf) { util::warning(std::format("Failed to load SOFA file: {} - Error: {}", file_path, error)); return kernel_data; } // Validate the HRTF structure if (mysofa_check(hrtf) != MYSOFA_OK) { util::warning(std::format("SOFA file validation failed: {}", file_path)); } kernel_data.rate = 48000; // Default fallback kernel_data.original_rate = kernel_data.rate; if (hrtf->DataSamplingRate.elements > 0 && hrtf->DataSamplingRate.values) { kernel_data.rate = static_cast(hrtf->DataSamplingRate.values[0]); kernel_data.original_rate = kernel_data.rate; util::debug(std::format("Found SOFA sampling rate: {} Hz", kernel_data.rate)); } // Get dimensions int M = hrtf->M; // Number of measurements (source positions) int R = hrtf->R; // Number of receivers (ears, usually 2) int N = hrtf->N; // Filter length (samples per IR) int E = hrtf->E; // Number of emitters util::debug(std::format("SOFA file measurements: {}", M)); util::debug(std::format("SOFA file receivers: {}", R)); util::debug(std::format("SOFA file filter length: {}", N)); util::debug(std::format("SOFA file emitters: {}", E)); // Set SOFA metadata kernel_data.is_sofa = true; kernel_data.sofaMetadata.measurements = M; kernel_data.sofaMetadata.database = mysofa_getAttribute(hrtf->attributes, const_cast("DatabaseName")); util::debug(std::format("Database: {}", kernel_data.sofaMetadata.database.toStdString())); if (M <= 0 || R < 1 || N <= 0) { util::warning(std::format("Invalid SOFA file structure: M={}, R={}, N={}", M, R, N)); mysofa_free(hrtf); return kernel_data; } int m = 0; mysofa_tocartesian(hrtf); struct MYSOFA_LOOKUP* lookup = mysofa_lookup_init(hrtf); if (!lookup) { util::warning("Failed to create SOFA lookup structure."); } else { util::debug(std::format("Theta min: {}, max: {}", lookup->theta_min, lookup->theta_max)); util::debug(std::format("Phi min: {}, max: {}", lookup->phi_min, lookup->phi_max)); util::debug(std::format("Radius min: {}, max: {}", lookup->radius_min, lookup->radius_max)); kernel_data.sofaMetadata.min_azimuth = lookup->phi_min; kernel_data.sofaMetadata.max_azimuth = lookup->phi_max; kernel_data.sofaMetadata.min_elevation = lookup->theta_min; kernel_data.sofaMetadata.max_elevation = lookup->theta_max; kernel_data.sofaMetadata.min_radius = lookup->radius_min; kernel_data.sofaMetadata.max_radius = lookup->radius_max; float coords[3] = {static_cast(settings->targetSofaAzimuth()), static_cast(settings->targetSofaElevation()), static_cast(settings->targetSofaRadius())}; mysofa_s2c(coords); m = mysofa_lookup(lookup, coords); kernel_data.sofaMetadata.index = m; util::debug(std::format( "For the desired azimuth = {}, elevation = {} and radius = {} the nearest SOFA measurement index is {}", settings->targetSofaAzimuth(), settings->targetSofaElevation(), settings->targetSofaRadius(), m)); mysofa_lookup_free(lookup); mysofa_tospherical(hrtf); float selected_azimuth = hrtf->SourcePosition.values[(m * 3) + 0]; float selected_elevation = hrtf->SourcePosition.values[(m * 3) + 1]; float selected_radius = hrtf->SourcePosition.values[(m * 3) + 2]; kernel_data.sofaMetadata.azimuth = selected_azimuth; kernel_data.sofaMetadata.elevation = selected_elevation; kernel_data.sofaMetadata.radius = selected_radius; util::debug(std::format("For measurement {} we have azimuth = {}, elevation = {} and radius = {}", m, selected_azimuth, selected_elevation, selected_radius)); } const int RxE_times_N = R * E * N; if (E == 1) { kernel_data.channel_L.resize(N); kernel_data.channel_R.resize(N); kernel_data.channels = 2; // Left ear (receiver 0) for (int n = 0; n < N; n++) { kernel_data.channel_L[n] = hrtf->DataIR.values[(m * RxE_times_N) + (0 * N) + n]; } // Right ear (receiver 1) if (R > 1) { for (int n = 0; n < N; n++) { kernel_data.channel_R[n] = hrtf->DataIR.values[(m * RxE_times_N) + (1 * N) + n]; } } else { kernel_data.channel_R = kernel_data.channel_L; } } if (R == 2 && E == 2) { // Assuming it is True Stereo HRTF: 4 channels kernel_data.channels = 4; kernel_data.channel_L.resize(N); // LL kernel_data.channel_LR.resize(N); // LR kernel_data.channel_RL.resize(N); // RL kernel_data.channel_R.resize(N); // RR // Left Channel (LL: Emitter 0 to Receiver 0) for (int n = 0; n < N; n++) { kernel_data.channel_L[n] = hrtf->DataIR.values[(m * RxE_times_N) + (0 * E * N) + (0 * N) + n]; } // Left-to-Right Crosstalk (LR: Emitter 0 to Receiver 1) for (int n = 0; n < N; n++) { kernel_data.channel_LR[n] = hrtf->DataIR.values[(m * RxE_times_N) + (1 * E * N) + (0 * N) + n]; } // Right-to-Left Crosstalk (RL: Emitter 1 to Receiver 0) for (int n = 0; n < N; n++) { kernel_data.channel_RL[n] = hrtf->DataIR.values[(m * RxE_times_N) + (0 * E * N) + (1 * N) + n]; } // Right Channel (RR: Emitter 1 to Receiver 1) for (int n = 0; n < N; n++) { kernel_data.channel_R[n] = hrtf->DataIR.values[(m * RxE_times_N) + (1 * E * N) + (1 * N) + n]; } } mysofa_free(hrtf); util::debug(std::format("Successfully loaded SOFA file '{}': {} Hz, {} samples", file_path, kernel_data.rate, N)); } catch (const std::exception& e) { util::warning(std::format("Exception while reading SOFA file {}: {}", file_path, e.what())); } return kernel_data; }