// src/trig_interpolation.cpp #include "trig_interpolation.h" #include "complex_block.h" // For the Hilbert Transform #include #include #include #include #include #include // For std::function #ifndef M_PI #define M_PI 3.14159265358979323846 #endif // Forward declaration for the new helper function static std::vector create_match_curve( const std::vector& smooth_source, const std::vector& smooth_target, double strength ); // --- FIX: Add new static helper function to correctly process magnitude spectrums --- static std::vector> get_cepstrum_from_magnitude_spectrum( const std::vector& mag_spectrum, BlockHilbert& hilbert_processor, std::function>&, std::vector>&)> ifft_func ) { if (mag_spectrum.empty()) return {}; // The input mag_spectrum is a half-spectrum of size (N/2 + 1). size_t half_n = mag_spectrum.size(); size_t n = (half_n > 1) ? (half_n - 1) * 2 : 0; if (n == 0) return {}; // 1. Take the natural logarithm of the magnitude spectrum. std::vector> log_mag_full(n, {0.0, 0.0}); for (size_t i = 0; i < half_n; ++i) { log_mag_full[i] = { log(std::max(1e-20, mag_spectrum[i])), 0.0 }; } // Create the conjugate symmetric part for the IFFT. for (size_t i = 1; i < half_n - 1; ++i) { log_mag_full[n - i] = log_mag_full[i]; } // 2. IFFT to get the real cepstrum. std::vector> cepstrum(n); ifft_func(log_mag_full, cepstrum); // 3. Perform Hilbert transform substitution on the real part to get the analytic cepstrum. std::vector real_part(n); for(size_t i = 0; i < n; ++i) real_part[i] = cepstrum[i].real(); auto substituted_pair = hilbert_processor.hilbertTransform(real_part, real_part); return substituted_pair.first; } // --- Main Public Method --- auto TrigInterpolation::process_and_generate_fir( const std::vector& source_L_in, const std::vector& source_R_in, const std::vector& target_L_in, const std::vector& target_R_in, int granularity, int detail_level, double strength, int fir_size, double lr_link, PhaseMode phase_mode ) -> std::tuple< std::vector, std::vector, // FIR Taps (L/R) std::vector, // Frequency Axis std::vector, std::vector, std::vector, // L(src,tgt,match) std::vector, std::vector, std::vector // R(src,tgt,match) > { // --- FIX: Call the new helper function instead of get_idealized_cepstrum --- // The lambda captures `this` to allow calling the member function `ifft`. auto ifft_lambda = [this](const auto& in, auto& out) { this->ifft(in, out); }; auto cepstrum_src_L = get_cepstrum_from_magnitude_spectrum(source_L_in, m_block_hilbert, ifft_lambda); auto cepstrum_src_R = get_cepstrum_from_magnitude_spectrum(source_R_in, m_block_hilbert, ifft_lambda); auto cepstrum_tgt_L = get_cepstrum_from_magnitude_spectrum(target_L_in, m_block_hilbert, ifft_lambda); auto cepstrum_tgt_R = get_cepstrum_from_magnitude_spectrum(target_R_in, m_block_hilbert, ifft_lambda); // 2. Independently Smooth Each Curve auto smooth_src_L = idealize_curve(cepstrum_src_L, granularity, detail_level); auto smooth_src_R = idealize_curve(cepstrum_src_R, granularity, detail_level); auto smooth_tgt_L = idealize_curve(cepstrum_tgt_L, granularity, detail_level); auto smooth_tgt_R = idealize_curve(cepstrum_tgt_R, granularity, detail_level); // 3. Perform the second trigonometric interpolation to get the match curves auto pre_link_match_L = create_match_curve(smooth_src_L, smooth_tgt_L, strength); auto pre_link_match_R = create_match_curve(smooth_src_R, smooth_tgt_R, strength); std::vector match_curve_L(pre_link_match_L.size()); std::vector match_curve_R(pre_link_match_R.size()); // 4. Apply Stereo Link to the final match curves for (size_t i = 0; i < pre_link_match_L.size(); ++i) { double val_L = pre_link_match_L[i]; double val_R = pre_link_match_R[i]; match_curve_L[i] = val_L * (1.0 - lr_link) + (val_L + val_R) * 0.5 * lr_link; match_curve_R[i] = val_R * (1.0 - lr_link) + (val_L + val_R) * 0.5 * lr_link; } // 5. Generate FIR Filter from Match Curve auto fir_coeffs_L = create_fir_from_curve(match_curve_L, fir_size, phase_mode); auto fir_coeffs_R = create_fir_from_curve(match_curve_R, fir_size, phase_mode); // 6. Prepare Plot Data std::vector frequency_axis, source_L_db, target_L_db, matched_L_db, source_R_db, target_R_db, matched_R_db; // FIX: Cast size_t to int for warning suppression if needed, though here it's just logic size_t num_plot_points = match_curve_L.size() > 1 ? match_curve_L.size() / 2 : 0; if (num_plot_points == 0) { // Return empty tuple if no data return { {}, {}, {}, {}, {}, {}, {}, {}, {} }; } // The frequency axis should correspond to the original half-spectrum size size_t freq_axis_size = source_L_in.size(); frequency_axis.resize(freq_axis_size); std::iota(frequency_axis.begin(), frequency_axis.end(), 0.0); auto to_db = [](double val) { return 20.0 * log10(std::max(1e-9, val)); }; source_L_db.resize(freq_axis_size); target_L_db.resize(freq_axis_size); matched_L_db.resize(freq_axis_size); source_R_db.resize(freq_axis_size); target_R_db.resize(freq_axis_size); matched_R_db.resize(freq_axis_size); for(size_t i = 0; i < freq_axis_size; ++i) { source_L_db[i] = to_db(smooth_src_L[i]); target_L_db[i] = to_db(smooth_tgt_L[i]); matched_L_db[i] = to_db(match_curve_L[i]); source_R_db[i] = to_db(smooth_src_R[i]); target_R_db[i] = to_db(smooth_tgt_R[i]); matched_R_db[i] = to_db(match_curve_R[i]); } return { fir_coeffs_L, fir_coeffs_R, frequency_axis, source_L_db, target_L_db, matched_L_db, source_R_db, target_R_db, matched_R_db }; } // --- New Function for Match Curve Interpolation --- static std::vector create_match_curve( const std::vector& smooth_source, const std::vector& smooth_target, double strength) { if (smooth_source.empty()) return {}; size_t n = smooth_source.size(); // 1. Find extrema in the source curve to use as anchors std::vector> source_extrema; if (n > 2) { for (size_t i = 1; i < n - 1; ++i) { if ((smooth_source[i] > smooth_source[i-1] && smooth_source[i] > smooth_source[i+1]) || (smooth_source[i] < smooth_source[i-1] && smooth_source[i] < smooth_source[i+1])) { source_extrema.push_back({i, smooth_source[i]}); } } } if (source_extrema.empty()) return smooth_source; // Return original if no features found // 2. Create a new set of "match" extrema by interpolating towards the target std::vector> match_extrema; for (const auto& extremum : source_extrema) { size_t index = extremum.first; double source_val = extremum.second; double target_val = smooth_target[index]; double match_val = source_val + (target_val - source_val) * strength; match_extrema.push_back({index, match_val}); } // 3. Reconstruct the final curve from the new match extrema using cosine bell interpolation std::vector final_match_curve(n); for (size_t i = 0; i < n; ++i) { double total_weight = 0.0; double weighted_sum = 0.0; for (size_t j = 0; j < match_extrema.size(); ++j) { size_t extremum_idx = match_extrema[j].first; double dist = static_cast(i) - extremum_idx; // Define the lobe width based on distance to adjacent extrema size_t prev_idx = (j > 0) ? match_extrema[j-1].first : 0; size_t next_idx = (j < match_extrema.size() - 1) ? match_extrema[j+1].first : n - 1; double lobe_width = (static_cast(next_idx - prev_idx)) / 2.0; if (lobe_width == 0) lobe_width = n / (2.0 * match_extrema.size()); // Fallback if (std::abs(dist) < lobe_width) { double weight = (cos(dist / lobe_width * M_PI) + 1.0) / 2.0; // Hann window weighted_sum += match_extrema[j].second * weight; total_weight += weight; } } if (total_weight > 0) { final_match_curve[i] = weighted_sum / total_weight; } else { // If a point is not influenced by any lobe, interpolate from nearest extrema auto it = std::lower_bound(match_extrema.begin(), match_extrema.end(), std::make_pair(i, 0.0)); if (it == match_extrema.begin()) final_match_curve[i] = it->second; else if (it == match_extrema.end()) final_match_curve[i] = (it-1)->second; else { auto prev = it - 1; double progress = static_cast(i - prev->first) / (it->first - prev->first); final_match_curve[i] = prev->second + (it->second - prev->second) * progress; } } } return final_match_curve; } // --- Private Helper Implementations --- std::vector TrigInterpolation::create_fir_from_curve(const std::vector& match_curve, int fir_size, PhaseMode phase_mode) { if (match_curve.empty() || fir_size <= 0) return {}; size_t n = match_curve.size(); std::vector> spectrum(n, {0.0, 0.0}); // Convert magnitude to complex spectrum (reconstructing phase) if (phase_mode == PhaseMode::Hilbert) { // For minimum phase via Hilbert, we take log, IFFT, causal part, FFT, exp std::vector> log_mag(n); for(size_t i = 0; i < n; ++i) log_mag[i] = {log(std::max(1e-20, match_curve[i])), 0.0}; std::vector> cepstrum(n); ifft(log_mag, cepstrum); // Causal part (zero out negative time) for(size_t i = n / 2; i < n; ++i) cepstrum[i] = {0.0, 0.0}; cepstrum[0] *= 1.0; // DC for(size_t i = 1; i < n / 2; ++i) cepstrum[i] *= 2.0; // Positive freqs std::vector> min_phase_spectrum(n); fft(cepstrum, min_phase_spectrum); for(size_t i = 0; i < n; ++i) spectrum[i] = std::exp(min_phase_spectrum[i]); } else { // Standard Cepstral (results in linear phase) for(size_t i = 0; i < n; ++i) spectrum[i] = {match_curve[i], 0.0}; } // IFFT to get impulse response std::vector> impulse_response(n); ifft(spectrum, impulse_response); // Window and extract FIR taps std::vector fir_taps(fir_size); int center = fir_size / 2; for (int i = 0; i < fir_size; ++i) { double hann_window = 0.5 * (1.0 - cos(2.0 * M_PI * i / (fir_size - 1))); int impulse_idx = (i - center + static_cast(n)) % static_cast(n); fir_taps[i] = static_cast(impulse_response[impulse_idx].real() * hann_window); } // --- FIX: Normalize the filter taps to prevent volume drop --- double tap_sum = 0.0; for (float tap : fir_taps) { tap_sum += tap; } if (std::abs(tap_sum) > 1e-9) { for (float& tap : fir_taps) { tap /= tap_sum; } } return fir_taps; } std::vector> TrigInterpolation::get_idealized_cepstrum(const std::vector& signal) { if (signal.empty()) return {}; size_t n = signal.size(); // 1. Hilbert Transform to gain complex input BlockHilbert hilbert; // Use the signal for both L/R as we process mono here auto analytic_pair = hilbert.hilbertTransform(signal, signal); std::vector> analytic_signal = analytic_pair.first; // 2. Take the FFT std::vector> spectrum(n); fft(analytic_signal, spectrum); // 3. Take the complex natural logarithm for (auto& val : spectrum) { if (std::abs(val) > 1e-9) { val = std::log(val); } else { val = {0.0, 0.0}; } } // 4. Unwrap the phase std::vector unwrapped = unwrap_phase(spectrum); for(size_t i = 0; i < n; ++i) { spectrum[i].imag(unwrapped[i]); } // 5. Perform an IFFT to get complex cepstrum std::vector> cepstrum(n); ifft(spectrum, cepstrum); // 6. Perform the Hilbert transform substitution std::vector real_part(n); for(size_t i = 0; i < n; ++i) real_part[i] = cepstrum[i].real(); auto substituted_pair = hilbert.hilbertTransform(real_part, real_part); return substituted_pair.first; // This is the final analytic cepstrum } // --- Full Implementation of the Idealize Curve Logic --- std::vector TrigInterpolation::idealize_curve(const std::vector>& analytic_cepstrum, int granularity, int detail_level) { if (analytic_cepstrum.empty()) return {}; size_t n = analytic_cepstrum.size(); std::vector current_curve(n); for(size_t i = 0; i < n; ++i) { current_curve[i] = std::abs(analytic_cepstrum[i]); } // Map slider values to algorithm parameters int num_bins = 4 + static_cast(granularity / 100.0 * 60.0); int iterations = 1 + static_cast(detail_level / 100.0 * 4.0); for (int iter = 0; iter < iterations; ++iter) { std::vector next_curve(n, 0.0); std::vector is_point_set(n, false); // 1. Binning std::vector bin_boundaries; double log_n = log((double)n); for (int i = 0; i <= num_bins; ++i) { double ratio = static_cast(i) / num_bins; size_t boundary = static_cast(exp(ratio * log_n)); boundary = std::max((size_t)1, std::min(n - 1, boundary)); bin_boundaries.push_back(boundary); } bin_boundaries[0] = 0; // 2. Find extrema and process within bins for (int i = 0; i < num_bins; ++i) { size_t bin_start = bin_boundaries[i]; size_t bin_end = bin_boundaries[i+1]; if (bin_start >= bin_end -1) continue; std::vector> extrema; for (size_t j = bin_start + 1; j < bin_end; ++j) { if ((current_curve[j] > current_curve[j-1] && current_curve[j] > current_curve[j+1]) || (current_curve[j] < current_curve[j-1] && current_curve[j] < current_curve[j+1])) { extrema.push_back({j, current_curve[j]}); } } if (extrema.empty()) continue; // 3. Weaving/Smoothing via cosine bell interpolation for (size_t j = bin_start; j <= bin_end; ++j) { double total_weight = 0.0; double weighted_sum = 0.0; for (const auto& extremum : extrema) { double dist = static_cast(j) - extremum.first; double lobe_width = (bin_end - bin_start) / 2.0; // Approximation if (std::abs(dist) < lobe_width) { double weight = (cos(dist / lobe_width * M_PI) + 1.0) / 2.0; // Hann window weighted_sum += extremum.second * weight; total_weight += weight; } } if (total_weight > 0) { next_curve[j] = weighted_sum / total_weight; is_point_set[j] = true; } } } // 4. Gap Filling (linear interpolation) size_t last_set_point = 0; if(is_point_set[0]) last_set_point = 0; else { // Find first set point for(size_t i=0; i last_set_point + 1) { // Gap detected double start_val = next_curve[last_set_point]; double end_val = next_curve[i]; for (size_t j = last_set_point + 1; j < i; ++j) { double progress = static_cast(j - last_set_point) / (i - last_set_point); next_curve[j] = start_val + (end_val - start_val) * progress; } } last_set_point = i; } } current_curve = next_curve; } return current_curve; } std::vector TrigInterpolation::unwrap_phase(const std::vector>& c_vec) { std::vector phases(c_vec.size()); for(size_t i=0; i < c_vec.size(); ++i) { phases[i] = std::arg(c_vec[i]); } double phase_correction = 0.0; for (size_t i = 1; i < phases.size(); ++i) { double diff = phases[i] - phases[i - 1]; if (diff > M_PI) { phase_correction -= 2.0 * M_PI; } else if (diff < -M_PI) { phase_correction += 2.0 * M_PI; } phases[i] += phase_correction; } return phases; } // --- FFTW Helpers (similar to complex_block.cpp but for complex-to-complex) --- void TrigInterpolation::fft(const std::vector>& input, std::vector>& output) { size_t n = input.size(); // FIX: Cast size_t to int for FFTW int n_int = static_cast(n); fftw_complex* in = (fftw_complex*)fftw_malloc(sizeof(fftw_complex) * n); fftw_complex* out = (fftw_complex*)fftw_malloc(sizeof(fftw_complex) * n); for(size_t i=0; i>& input, std::vector>& output) { size_t n = input.size(); // FIX: Cast size_t to int for FFTW int n_int = static_cast(n); fftw_complex* in = (fftw_complex*)fftw_malloc(sizeof(fftw_complex) * n); fftw_complex* out = (fftw_complex*)fftw_malloc(sizeof(fftw_complex) * n); for(size_t i=0; i