diff --git a/examples/graphics/source/examples/FilterDemo.h b/examples/graphics/source/examples/FilterDemo.h index 2eedbaf51..d806b4633 100644 --- a/examples/graphics/source/examples/FilterDemo.h +++ b/examples/graphics/source/examples/FilterDemo.h @@ -510,13 +510,11 @@ class FrequencyResponsePlot : public yup::Component { sampleRate = newSampleRate; maxFreq = sampleRate * 0.45; // Nyquist - some margin - updateResponseData(); } void setFilter (std::shared_ptr> newFilter) { filter = newFilter; - updateResponseData(); } const std::vector>& getPhaseData() const { return phaseData; } @@ -533,19 +531,62 @@ class FrequencyResponsePlot : public yup::Component return; } - const int numPoints = 512; + const int numPoints = isCombFilter() ? 4096 : 512; + + minDb = isCombFilter() ? -80.0 : -60.0; + maxDb = isCombFilter() ? 40.0 : 20.0; responseData.clear(); responseData.resize (numPoints); - yup::calculateFilterMagnitudeResponse (*filter, yup::Span (responseData), minFreq, maxFreq); phaseData.clear(); phaseData.resize (numPoints); - yup::calculateFilterPhaseResponse (*filter, yup::Span (phaseData), minFreq, maxFreq); groupDelayData.clear(); groupDelayData.resize (numPoints); - yup::calculateFilterGroupDelay (*filter, yup::Span (groupDelayData), minFreq, maxFreq, sampleRate); + + std::vector phaseRadians; + phaseRadians.resize (numPoints); + + for (int i = 0; i < numPoints; ++i) + { + const double ratio = static_cast (i) / static_cast (numPoints - 1); + const double freq = minFreq * std::pow (maxFreq / minFreq, ratio); + const auto response = filter->getComplexResponse (freq); + const double magnitudeDb = 20.0 * std::log10 (yup::jmax (std::abs (response), 1.0e-12)); + double phase = std::arg (response); + const double displayPhase = phase; + + if (i > 0) + { + while (phase - phaseRadians[static_cast (i - 1)] > yup::MathConstants::pi) + phase -= yup::MathConstants::twoPi; + while (phase - phaseRadians[static_cast (i - 1)] < -yup::MathConstants::pi) + phase += yup::MathConstants::twoPi; + } + + phaseRadians[static_cast (i)] = phase; + responseData[static_cast (i)] = { static_cast (freq), static_cast (magnitudeDb) }; + phaseData[static_cast (i)] = { static_cast (freq), static_cast (displayPhase * 180.0 / yup::MathConstants::pi) }; + } + + for (int i = 1; i < numPoints - 1; ++i) + { + const auto previousFrequency = static_cast (std::real (phaseData[static_cast (i - 1)])); + const auto nextFrequency = static_cast (std::real (phaseData[static_cast (i + 1)])); + const auto previousOmega = yup::MathConstants::twoPi * previousFrequency / sampleRate; + const auto nextOmega = yup::MathConstants::twoPi * nextFrequency / sampleRate; + const auto delay = -(phaseRadians[static_cast (i + 1)] - phaseRadians[static_cast (i - 1)]) + / (nextOmega - previousOmega); + + groupDelayData[static_cast (i)] = { std::real (phaseData[static_cast (i)]), static_cast (delay) }; + } + + if (numPoints > 1) + { + groupDelayData.front() = { std::real (phaseData.front()), std::imag (groupDelayData[1]) }; + groupDelayData.back() = { std::real (phaseData.back()), std::imag (groupDelayData[static_cast (numPoints - 2)]) }; + } stepResponseData.clear(); stepResponseData.resize (100); @@ -591,7 +632,9 @@ class FrequencyResponsePlot : public yup::Component } // Horizontal dB lines - for (double db = -60.0; db <= 20.0; db += 20.0) + const auto gridStepDb = isCombFilter() ? 40.0 : 20.0; + + for (double db = minDb; db <= maxDb; db += gridStepDb) { float y = dbToY (db, bounds); g.strokeLine ({ bounds.getX(), y }, { bounds.getRight(), y }); @@ -662,7 +705,9 @@ class FrequencyResponsePlot : public yup::Component } // dB labels - for (double db = -60.0; db <= 20.0; db += 20.0) + const auto gridStepDb = isCombFilter() ? 40.0 : 20.0; + + for (double db = minDb; db <= maxDb; db += gridStepDb) { float y = dbToY (db, bounds); yup::String label = yup::String (db, 0) + " dB"; @@ -682,6 +727,11 @@ class FrequencyResponsePlot : public yup::Component return static_cast (bounds.getBottom() - ratio * bounds.getHeight()); } + bool isCombFilter() const + { + return dynamic_cast*> (filter.get()) != nullptr; + } + std::shared_ptr> filter; std::vector> responseData; std::vector> phaseData; @@ -830,6 +880,9 @@ class FilterDemo if (oscilloscope.isVisible()) oscilloscope.repaint(); + + if (analysisUpdatePending.exchange (false)) + updateAnalysisDisplays(); } void visibilityChanged() override @@ -952,6 +1005,12 @@ class FilterDemo filterTypeCombo->addItem ("First Order", 4); filterTypeCombo->addItem ("Butterworth", 5); filterTypeCombo->addItem ("FIR Filter", 6); + filterTypeCombo->addItem ("Analog Two Pole", 7); + filterTypeCombo->addItem ("Analog Vowel", 8); + filterTypeCombo->addItem ("Analog Korg35", 9); + filterTypeCombo->addItem ("Analog Moog Ladder", 10); + filterTypeCombo->addItem ("Analog Roland Diode", 11); + filterTypeCombo->addItem ("Comb", 12); filterTypeCombo->setSelectedId (1); filterTypeCombo->onSelectedItemChanged = [this] { @@ -961,15 +1020,7 @@ class FilterDemo // Response type selector responseTypeCombo = std::make_unique ("ResponseType"); - responseTypeCombo->addItem ("Lowpass", 1); - responseTypeCombo->addItem ("Highpass", 2); - responseTypeCombo->addItem ("Bandpass CSG", 3); - responseTypeCombo->addItem ("Bandpass CPG", 4); - responseTypeCombo->addItem ("Bandstop", 5); - responseTypeCombo->addItem ("Peak", 6); - responseTypeCombo->addItem ("Low Shelf", 7); - responseTypeCombo->addItem ("High Shelf", 8); - responseTypeCombo->addItem ("Allpass", 9); + addFullResponseTypes(); responseTypeCombo->setSelectedId (1); responseTypeCombo->onSelectedItemChanged = [this] { @@ -983,7 +1034,7 @@ class FilterDemo firCoefficientsSlider->setValue (64.0); firCoefficientsSlider->onValueChanged = [this] (double value) { - updateAnalysisDisplays(); + requestAnalysisUpdate(); }; addAndMakeVisible (*firCoefficientsSlider); @@ -998,7 +1049,7 @@ class FilterDemo firWindowCombo->onSelectedItemChanged = [this] { updateWindowParameterRange(); - updateAnalysisDisplays(); + requestAnalysisUpdate(); }; addAndMakeVisible (*firWindowCombo); @@ -1009,7 +1060,7 @@ class FilterDemo firWindowParameterSlider->setValue (1.0); firWindowParameterSlider->onValueChanged = [this] (double value) { - updateAnalysisDisplays(); + requestAnalysisUpdate(); }; addAndMakeVisible (*firWindowParameterSlider); @@ -1021,7 +1072,7 @@ class FilterDemo frequencySlider->onValueChanged = [this] (double value) { smoothedFrequency.setTargetValue ((float) value); - updateAnalysisDisplays(); + requestAnalysisUpdate(); }; addAndMakeVisible (*frequencySlider); @@ -1032,7 +1083,7 @@ class FilterDemo frequency2Slider->onValueChanged = [this] (double value) { smoothedFrequency2.setTargetValue ((float) value); - updateAnalysisDisplays(); + requestAnalysisUpdate(); }; addAndMakeVisible (*frequency2Slider); @@ -1043,7 +1094,7 @@ class FilterDemo qSlider->onValueChanged = [this] (double value) { smoothedQ.setTargetValue ((float) value); - updateAnalysisDisplays(); + requestAnalysisUpdate(); }; addAndMakeVisible (*qSlider); @@ -1054,7 +1105,7 @@ class FilterDemo gainSlider->onValueChanged = [this] (double value) { smoothedGain.setTargetValue ((float) value); - updateAnalysisDisplays(); + requestAnalysisUpdate(); }; addAndMakeVisible (*gainSlider); @@ -1064,7 +1115,7 @@ class FilterDemo orderSlider->onValueChanged = [this] (double value) { smoothedOrder.setTargetValue ((float) value); - updateAnalysisDisplays(); + requestAnalysisUpdate(); }; addAndMakeVisible (*orderSlider); @@ -1155,6 +1206,12 @@ class FilterDemo audioFirstOrder = std::make_shared>(); audioButterworthFilter = std::make_shared>(); audioDirectFIR = std::make_shared>(); + audioAnalogTwoPole = std::make_shared>(); + audioAnalogVowel = std::make_shared>(); + audioAnalogKorg35 = std::make_shared>(); + audioAnalogMoogLadder = std::make_shared>(); + audioAnalogRolandDiode = std::make_shared>(); + audioCombFilter = std::make_shared>(); // Create instances of all filter types for UI thread uiRbj = std::make_shared>(); @@ -1163,14 +1220,20 @@ class FilterDemo uiFirstOrder = std::make_shared>(); uiButterworthFilter = std::make_shared>(); uiDirectFIR = std::make_shared>(); + uiAnalogTwoPole = std::make_shared>(); + uiAnalogVowel = std::make_shared>(); + uiAnalogKorg35 = std::make_shared>(); + uiAnalogMoogLadder = std::make_shared>(); + uiAnalogRolandDiode = std::make_shared>(); + uiCombFilter = std::make_shared>(); // Store in arrays for easy management allAudioFilters = { - audioRbj, audioZoelzer, audioSvf, audioFirstOrder, audioButterworthFilter, audioDirectFIR + audioRbj, audioZoelzer, audioSvf, audioFirstOrder, audioButterworthFilter, audioDirectFIR, audioAnalogTwoPole, audioAnalogVowel, audioAnalogKorg35, audioAnalogMoogLadder, audioAnalogRolandDiode, audioCombFilter }; allUIFilters = { - uiRbj, uiZoelzer, uiSvf, uiFirstOrder, uiButterworthFilter, uiDirectFIR + uiRbj, uiZoelzer, uiSvf, uiFirstOrder, uiButterworthFilter, uiDirectFIR, uiAnalogTwoPole, uiAnalogVowel, uiAnalogKorg35, uiAnalogMoogLadder, uiAnalogRolandDiode, uiCombFilter }; // Set default filters @@ -1218,11 +1281,33 @@ class FilterDemo case 6: currentUIFilter = uiDirectFIR; break; + case 7: + currentUIFilter = uiAnalogTwoPole; + break; + case 8: + currentUIFilter = uiAnalogVowel; + break; + case 9: + currentUIFilter = uiAnalogKorg35; + break; + case 10: + currentUIFilter = uiAnalogMoogLadder; + break; + case 11: + currentUIFilter = uiAnalogRolandDiode; + break; + case 12: + currentUIFilter = uiCombFilter; + break; default: currentUIFilter = uiRbj; break; } + // Adjust available response modes and parameter ranges before reading values. + updateControlVisibility(); + currentResponseTypeId = responseTypeCombo->getSelectedId(); + // Synchronize smoothed values with current UI values when switching filters smoothedFrequency.setCurrentAndTargetValue (static_cast (frequencySlider->getValue())); smoothedFrequency2.setCurrentAndTargetValue (static_cast (frequency2Slider->getValue())); @@ -1236,13 +1321,9 @@ class FilterDemo // Update UI filter with current parameters updateUIFilterParameters(); - // Update control visibility based on filter type - updateControlVisibility(); - // Update displays using UI filter frequencyResponsePlot.setFilter (currentUIFilter); - frequencyResponsePlot.updateResponseData(); - updateAnalysisDisplays(); + requestAnalysisUpdate(); } void updateAudioFilterParameters() @@ -1300,6 +1381,30 @@ class FilterDemo { updateFIRFilterParameters (fir, coefficients, freq, freq2); } + else if (auto analogTwoPole = dynamic_cast*> (filter)) + { + analogTwoPole->setParameters (getFilterMode (currentResponseTypeId), freq, q, gain, currentSampleRate); + } + else if (auto analogVowel = dynamic_cast*> (filter)) + { + analogVowel->setParameters (freq, q, gain, currentSampleRate); + } + else if (auto analogKorg35 = dynamic_cast*> (filter)) + { + analogKorg35->setParameters (getFilterMode (currentResponseTypeId), freq, q, gain, currentSampleRate); + } + else if (auto analogMoogLadder = dynamic_cast*> (filter)) + { + analogMoogLadder->setParameters (getMoogLadderMode (currentResponseTypeId), freq, q, gain, currentSampleRate); + } + else if (auto analogRolandDiode = dynamic_cast*> (filter)) + { + analogRolandDiode->setParameters (freq, q, gain, currentSampleRate); + } + else if (auto combFilter = dynamic_cast*> (filter)) + { + combFilter->setParameters (freq, q, gain, currentSampleRate); + } } void updateCurrentAudioFilter() @@ -1325,6 +1430,24 @@ class FilterDemo case 6: currentAudioFilter = audioDirectFIR; break; + case 7: + currentAudioFilter = audioAnalogTwoPole; + break; + case 8: + currentAudioFilter = audioAnalogVowel; + break; + case 9: + currentAudioFilter = audioAnalogKorg35; + break; + case 10: + currentAudioFilter = audioAnalogMoogLadder; + break; + case 11: + currentAudioFilter = audioAnalogRolandDiode; + break; + case 12: + currentAudioFilter = audioCombFilter; + break; default: currentAudioFilter = audioRbj; break; @@ -1354,22 +1477,25 @@ class FilterDemo frequencyResponsePlot.updateResponseData(); // Update phase response - auto phaseData = frequencyResponsePlot.getPhaseData(); + const auto& phaseData = frequencyResponsePlot.getPhaseData(); std::vector> phaseDataDouble; + phaseDataDouble.reserve (phaseData.size()); for (const auto& data : phaseData) phaseDataDouble.push_back ({ static_cast (std::real (data)), static_cast (std::imag (data)) }); phaseResponseDisplay.updateResponse (phaseDataDouble); // Update group delay - auto groupDelayData = frequencyResponsePlot.getGroupDelayData(); + const auto& groupDelayData = frequencyResponsePlot.getGroupDelayData(); std::vector> groupDelayDataDouble; + groupDelayDataDouble.reserve (groupDelayData.size()); for (const auto& data : groupDelayData) groupDelayDataDouble.push_back ({ static_cast (std::real (data)), static_cast (std::imag (data)) }); groupDelayDisplay.updateResponse (groupDelayDataDouble); // Update step response - auto stepData = frequencyResponsePlot.getStepResponseData(); + const auto& stepData = frequencyResponsePlot.getStepResponseData(); std::vector> stepDataDouble; + stepDataDouble.reserve (stepData.size()); for (const auto& data : stepData) stepDataDouble.push_back ({ static_cast (std::real (data)), static_cast (std::imag (data)) }); stepResponseDisplay.updateResponse (stepDataDouble); @@ -1378,6 +1504,11 @@ class FilterDemo updatePolesZerosDisplay(); } + void requestAnalysisUpdate() + { + analysisUpdatePending = true; + } + void updateDisplayParameters() { if (! currentUIFilter) @@ -1386,8 +1517,7 @@ class FilterDemo // Update UI filter parameters and displays updateUIFilterParameters(); frequencyResponsePlot.setFilter (currentUIFilter); - frequencyResponsePlot.updateResponseData(); - updateAnalysisDisplays(); + requestAnalysisUpdate(); } void updatePolesZerosDisplay() @@ -1401,9 +1531,164 @@ class FilterDemo polesZerosDisplay.updatePolesZeros (poles, zeros); } + void addFullResponseTypes() + { + responseTypeCombo->addItem ("Lowpass", 1); + responseTypeCombo->addItem ("Highpass", 2); + responseTypeCombo->addItem ("Bandpass CSG", 3); + responseTypeCombo->addItem ("Bandpass CPG", 4); + responseTypeCombo->addItem ("Bandstop", 5); + responseTypeCombo->addItem ("Peak", 6); + responseTypeCombo->addItem ("Low Shelf", 7); + responseTypeCombo->addItem ("High Shelf", 8); + responseTypeCombo->addItem ("Allpass", 9); + } + + void addFIRResponseTypes() + { + responseTypeCombo->addItem ("Lowpass", 1); + responseTypeCombo->addItem ("Highpass", 2); + responseTypeCombo->addItem ("Bandpass", 3); + responseTypeCombo->addItem ("Bandstop", 5); + } + + void addAnalogTwoPoleResponseTypes() + { + responseTypeCombo->addItem ("Lowpass", 1); + responseTypeCombo->addItem ("Highpass", 2); + responseTypeCombo->addItem ("Bandpass CSG", 3); + responseTypeCombo->addItem ("Bandpass CPG", 4); + responseTypeCombo->addItem ("Bandstop", 5); + responseTypeCombo->addItem ("Peak", 6); + } + + void addKorg35ResponseTypes() + { + responseTypeCombo->addItem ("Lowpass", 1); + responseTypeCombo->addItem ("Highpass", 2); + responseTypeCombo->addItem ("Bandpass", 3); + } + + void addMoogLadderResponseTypes() + { + responseTypeCombo->addItem ("Lowpass 24 dB", 10); + responseTypeCombo->addItem ("Highpass 24 dB", 11); + responseTypeCombo->addItem ("Lowpass 18 dB", 12); + responseTypeCombo->addItem ("Highpass 18 dB", 13); + responseTypeCombo->addItem ("Lowpass 12 dB", 14); + responseTypeCombo->addItem ("Highpass 12 dB", 15); + responseTypeCombo->addItem ("Lowpass 6 dB", 16); + responseTypeCombo->addItem ("Highpass 6 dB", 17); + responseTypeCombo->addItem ("Bandpass 12 dB", 18); + responseTypeCombo->addItem ("Bandpass 6 dB", 19); + } + + void updateResponseTypeList() + { + const int filterType = currentFilterTypeId; + const int currentResponse = responseTypeCombo->getSelectedId(); + + responseTypeCombo->clear(); + + switch (filterType) + { + case 6: + addFIRResponseTypes(); + break; + + case 7: + addAnalogTwoPoleResponseTypes(); + break; + + case 8: + responseTypeCombo->addItem ("Vowel Formants", 6); + break; + + case 9: + addKorg35ResponseTypes(); + break; + + case 10: + addMoogLadderResponseTypes(); + break; + + case 11: + responseTypeCombo->addItem ("Lowpass", 1); + break; + + case 12: + responseTypeCombo->addItem ("Comb", 20); + break; + + default: + addFullResponseTypes(); + break; + } + + if (isResponseTypeSupported (filterType, currentResponse)) + responseTypeCombo->setSelectedId (currentResponse, yup::dontSendNotification); + else + responseTypeCombo->setSelectedId (getDefaultResponseType (filterType), yup::dontSendNotification); + } + + static bool isAnalogFilterType (int filterTypeId) + { + return filterTypeId >= 7 && filterTypeId <= 12; + } + + static bool isResponseTypeSupported (int filterTypeId, int responseTypeId) + { + switch (filterTypeId) + { + case 6: + return responseTypeId == 1 || responseTypeId == 2 || responseTypeId == 3 || responseTypeId == 5; + + case 7: + return responseTypeId >= 1 && responseTypeId <= 6; + + case 8: + return responseTypeId == 6; + + case 9: + return responseTypeId == 1 || responseTypeId == 2 || responseTypeId == 3; + + case 10: + return responseTypeId >= 10 && responseTypeId <= 19; + + case 11: + return responseTypeId == 1; + + case 12: + return responseTypeId == 20; + + default: + return responseTypeId >= 1 && responseTypeId <= 9; + } + } + + static int getDefaultResponseType (int filterTypeId) + { + switch (filterTypeId) + { + case 8: + return 6; + + case 10: + return 10; + + case 12: + return 20; + + default: + return 1; + } + } + void updateControlVisibility() { bool isFIRFilter = (currentFilterTypeId == 6); + bool isAnalogFilter = isAnalogFilterType (currentFilterTypeId); + bool isVowelFilter = (currentFilterTypeId == 8); // Show/hide FIR-specific controls firCoefficientsSlider->setVisible (isFIRFilter); @@ -1419,54 +1704,54 @@ class FilterDemo // Show/hide standard filter controls qSlider->setVisible (! isFIRFilter); gainSlider->setVisible (! isFIRFilter); - orderSlider->setVisible (! isFIRFilter || currentFilterTypeId == 5); // Show for Butterworth and FIR - parameterLabels[4]->setVisible (! isFIRFilter); // Q label - parameterLabels[5]->setVisible (! isFIRFilter); // Gain label - parameterLabels[6]->setVisible (! isFIRFilter || currentFilterTypeId == 5); // Order label + orderSlider->setVisible (currentFilterTypeId == 5); // Show for Butterworth + parameterLabels[4]->setVisible (! isFIRFilter); // Q label + parameterLabels[5]->setVisible (! isFIRFilter); // Gain label + parameterLabels[6]->setVisible (currentFilterTypeId == 5); // Order label - // Frequency 2 is only visible for bandpass/bandstop filters - bool needsFreq2 = (currentResponseTypeId >= 3 && currentResponseTypeId <= 5); - frequency2Slider->setVisible (needsFreq2); - parameterLabels[3]->setVisible (needsFreq2); // Frequency 2 label + parameterLabels[2]->setText (isVowelFilter ? "Vowel:" : "Frequency:"); + parameterLabels[4]->setText (isAnalogFilter ? "Resonance:" : "Q/Resonance:"); + parameterLabels[5]->setText (isAnalogFilter ? "Saturation:" : "Gain (dB):"); - // Update restricted response types for FIR - if (isFIRFilter) + if (isVowelFilter) { - // Save current selection - int currentResponse = responseTypeCombo->getSelectedId(); + frequencySlider->setRange ({ 0.0, 1.0 }); + frequencySlider->setSkewFactorFromMidpoint (0.5); - // Clear and repopulate with FIR-compatible responses - responseTypeCombo->clear(); - responseTypeCombo->addItem ("Lowpass", 1); - responseTypeCombo->addItem ("Highpass", 2); - responseTypeCombo->addItem ("Bandpass", 3); - responseTypeCombo->addItem ("Bandstop", 5); + if (frequencySlider->getValue() > 1.0) + frequencySlider->setValue (0.5, yup::dontSendNotification); + } + else + { + frequencySlider->setRange ({ 20.0, isAnalogFilter ? getAnalogFilterMaxFrequency() : 20000.0 }); + frequencySlider->setSkewFactorFromMidpoint (1000.0); - // Restore selection if compatible, otherwise default to lowpass - if (currentResponse == 1 || currentResponse == 2 || currentResponse == 3 || currentResponse == 5) - responseTypeCombo->setSelectedId (currentResponse, yup::dontSendNotification); - else - responseTypeCombo->setSelectedId (1, yup::dontSendNotification); + if (frequencySlider->getValue() <= 1.0) + frequencySlider->setValue (1000.0, yup::dontSendNotification); + } + + if (isAnalogFilter) + { + gainSlider->setRange ({ 0.0, 1.0 }); + gainSlider->setSkewFactorFromMidpoint (0.3); + + if (gainSlider->getValue() < 0.0 || gainSlider->getValue() > 1.0) + gainSlider->setValue (0.2, yup::dontSendNotification); } else { - // Restore full response type list for IIR filters - int currentResponse = responseTypeCombo->getSelectedId(); - responseTypeCombo->clear(); - responseTypeCombo->addItem ("Lowpass", 1); - responseTypeCombo->addItem ("Highpass", 2); - responseTypeCombo->addItem ("Bandpass CSG", 3); - responseTypeCombo->addItem ("Bandpass CPG", 4); - responseTypeCombo->addItem ("Bandstop", 5); - responseTypeCombo->addItem ("Peak", 6); - responseTypeCombo->addItem ("Low Shelf", 7); - responseTypeCombo->addItem ("High Shelf", 8); - responseTypeCombo->addItem ("Allpass", 9); - - // Restore selection - responseTypeCombo->setSelectedId (currentResponse, yup::dontSendNotification); + gainSlider->setRange ({ -48.0, 20.0 }); + gainSlider->setSkewFactorFromMidpoint (0.0); } + updateResponseTypeList(); + currentResponseTypeId = responseTypeCombo->getSelectedId(); + + // Frequency 2 is only visible for bandpass/bandstop filters + bool needsFreq2 = isFIRFilter && (currentResponseTypeId == 3 || currentResponseTypeId == 5); + frequency2Slider->setVisible (needsFreq2); + parameterLabels[3]->setVisible (needsFreq2); // Frequency 2 label + repaint(); } @@ -1498,6 +1783,11 @@ class FilterDemo updateControlVisibility(); } + double getAnalogFilterMaxFrequency() const + { + return yup::jmax (20.0, (currentSampleRate * 0.5) / (22050.0 / 18000.0)); + } + void updateFIRFilterParameters (yup::DirectFIR* fir, std::vector& coeffs, double freq, double freq2) { int numCoeffs = static_cast (firCoefficientsSlider->getValue()); @@ -1564,11 +1854,52 @@ class FilterDemo return yup::FilterMode::highshelf; case 9: return yup::FilterMode::allpass; + case 10: + case 12: + case 14: + case 16: + return yup::FilterMode::lowpass; + case 11: + case 13: + case 15: + case 17: + return yup::FilterMode::highpass; + case 18: + case 19: + return yup::FilterMode::bandpassCsg; default: return yup::FilterMode::lowpass; } } + yup::AnalogMoogLadderMode getMoogLadderMode (int responseTypeId) + { + switch (responseTypeId) + { + case 11: + return yup::AnalogMoogLadderMode::highpass24; + case 12: + return yup::AnalogMoogLadderMode::lowpass18; + case 13: + return yup::AnalogMoogLadderMode::highpass18; + case 14: + return yup::AnalogMoogLadderMode::lowpass12; + case 15: + return yup::AnalogMoogLadderMode::highpass12; + case 16: + return yup::AnalogMoogLadderMode::lowpass6; + case 17: + return yup::AnalogMoogLadderMode::highpass6; + case 18: + return yup::AnalogMoogLadderMode::bandpass12; + case 19: + return yup::AnalogMoogLadderMode::bandpass6; + case 10: + default: + return yup::AnalogMoogLadderMode::lowpass24; + } + } + // Audio components yup::AudioDeviceManager deviceManager; yup::SmoothedValue outputGain { 0.5f }; @@ -1603,6 +1934,12 @@ class FilterDemo std::shared_ptr> audioFirstOrder; std::shared_ptr> audioButterworthFilter; std::shared_ptr> audioDirectFIR; + std::shared_ptr> audioAnalogTwoPole; + std::shared_ptr> audioAnalogVowel; + std::shared_ptr> audioAnalogKorg35; + std::shared_ptr> audioAnalogMoogLadder; + std::shared_ptr> audioAnalogRolandDiode; + std::shared_ptr> audioCombFilter; // UI thread filter instances std::shared_ptr> uiRbj; @@ -1611,6 +1948,12 @@ class FilterDemo std::shared_ptr> uiFirstOrder; std::shared_ptr> uiButterworthFilter; std::shared_ptr> uiDirectFIR; + std::shared_ptr> uiAnalogTwoPole; + std::shared_ptr> uiAnalogVowel; + std::shared_ptr> uiAnalogKorg35; + std::shared_ptr> uiAnalogMoogLadder; + std::shared_ptr> uiAnalogRolandDiode; + std::shared_ptr> uiCombFilter; std::vector>> allAudioFilters; std::vector>> allUIFilters; @@ -1646,4 +1989,5 @@ class FilterDemo std::vector renderData; yup::CriticalSection renderMutex; std::atomic_int readPos { 0 }; + std::atomic_bool analysisUpdatePending { false }; }; diff --git a/modules/yup_dsp/base/yup_AnalogFilterCoefficients.h b/modules/yup_dsp/base/yup_AnalogFilterCoefficients.h new file mode 100644 index 000000000..8250e64d0 --- /dev/null +++ b/modules/yup_dsp/base/yup_AnalogFilterCoefficients.h @@ -0,0 +1,157 @@ +/* + ============================================================================== + + This file is part of the YUP library. + Copyright (c) 2026 - kunitoki@gmail.com + + YUP is an open source library subject to open-source licensing. + + The code included in this file is provided under the terms of the ISC license + http://www.isc.org/downloads/software-support-policy/isc-license. Permission + to use, copy, modify, and/or distribute this software for any purpose with or + without fee is hereby granted provided that the above copyright notice and + this permission notice appear in all copies. + + YUP IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER + EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE + DISCLAIMED. + + ============================================================================== +*/ + +#pragma once + +namespace yup +{ + +//============================================================================== +/** + Coefficients for the saturator used by analog-model filters. + + The coefficients describe a symmetric atan waveshaper. A drive of zero keeps + the processor transparent. +*/ +template +struct AnalogSaturatorCoefficients +{ + CoeffType drive = static_cast (0); + CoeffType preScale = static_cast (1); + CoeffType postScale = static_cast (1); +}; + +//============================================================================== +/** + Coefficients for a trapezoidal-integrator two-pole state-variable filter. + + The output gains select low-pass, high-pass, band-pass, peak, or notch + responses without changing the integrator state update. +*/ +template +struct AnalogTwoPoleCoefficients +{ + CoeffType g = static_cast (0); + CoeffType h = static_cast (1); + CoeffType r2 = static_cast (0); + CoeffType gainCorrection = static_cast (1); + CoeffType lowOut = static_cast (1); + CoeffType bandOut = static_cast (0); + CoeffType highOut = static_cast (0); +}; + +//============================================================================== +/** + Coefficients for one trapezoidal one-pole section used inside ladder models. +*/ +template +struct AnalogOnePoleCoefficients +{ + CoeffType alpha = static_cast (0); + CoeffType beta = static_cast (1); +}; + +//============================================================================== +/** + Coefficients for a Korg35-inspired analog-model filter. +*/ +template +struct AnalogKorg35Coefficients +{ + std::array, 3> poles; + CoeffType alpha0 = static_cast (1); + CoeffType feedback = static_cast (0); + CoeffType gainCorrection = static_cast (1); +}; + +//============================================================================== +/** + Moog ladder output mode. +*/ +enum class AnalogMoogLadderMode +{ + lowpass24, + highpass24, + lowpass18, + highpass18, + lowpass12, + highpass12, + lowpass6, + highpass6, + bandpass12, + bandpass6 +}; + +//============================================================================== +/** + Coefficients for a four-pole Moog ladder-style analog-model filter. +*/ +template +struct AnalogMoogLadderCoefficients +{ + std::array, 4> poles; + std::array outputs { + static_cast (0), + static_cast (0), + static_cast (0), + static_cast (0), + static_cast (1) + }; + CoeffType alpha0 = static_cast (1); + CoeffType feedback = static_cast (0); + CoeffType gainCorrection = static_cast (1); +}; + +//============================================================================== +/** + Coefficients for a Roland diode-ladder-inspired low-pass filter. +*/ +template +struct AnalogRolandDiodeCoefficients +{ + CoeffType cutoff = static_cast (0); + CoeffType feedback = static_cast (0); + CoeffType gainCorrection = static_cast (1); + CoeffType a = static_cast (0); + CoeffType a2 = static_cast (0); + CoeffType aInv = static_cast (1); + CoeffType b = static_cast (1); + CoeffType b2 = static_cast (1); + CoeffType c = static_cast (1); + CoeffType g0 = static_cast (0); + CoeffType g = static_cast (0); + CoeffType fg = static_cast (1); + CoeffType highpassA = static_cast (0); + CoeffType highpassB = static_cast (1); +}; + +//============================================================================== +/** + Coefficients for the vowel/formant analog-model filter. +*/ +template +struct AnalogVowelCoefficients +{ + std::array, 3> formants; + CoeffType gainCompensation = static_cast (1); +}; + +} // namespace yup diff --git a/modules/yup_dsp/base/yup_AnalogPoles.h b/modules/yup_dsp/base/yup_AnalogPoles.h new file mode 100644 index 000000000..c9f081d96 --- /dev/null +++ b/modules/yup_dsp/base/yup_AnalogPoles.h @@ -0,0 +1,209 @@ +/* + ============================================================================== + + This file is part of the YUP library. + Copyright (c) 2026 - kunitoki@gmail.com + + YUP is an open source library subject to open-source licensing. + + The code included in this file is provided under the terms of the ISC license + http://www.isc.org/downloads/software-support-policy/isc-license. Permission + to use, copy, modify, and/or distribute this software for any purpose with or + without fee is hereby granted provided that the above copyright notice and + this permission notice appear in all copies. + + YUP IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER + EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE + DISCLAIMED. + + ============================================================================== +*/ + +#pragma once + +namespace yup +{ + +//============================================================================== +/** State for a one-pole analog-model filter. + + The state is designed to be used with the coefficients defined in AnalogOnePoleCoefficients. +*/ +template +struct AnalogOnePoleState +{ + CoeffType z1 = static_cast (0); + + void reset() noexcept + { + z1 = static_cast (0); + } + + CoeffType processLowPass (CoeffType input, const AnalogOnePoleCoefficients& coefficients) noexcept + { + const auto v = (input - z1) * coefficients.alpha; + const auto output = v + z1; + z1 = output + v; + + return output; + } + + CoeffType processHighPass (CoeffType input, const AnalogOnePoleCoefficients& coefficients) noexcept + { + return input - processLowPass (input, coefficients); + } + + CoeffType getFeedbackOutput (const AnalogOnePoleCoefficients& coefficients) const noexcept + { + return coefficients.beta * z1; + } +}; + +//============================================================================== +/** State for a two-pole analog-model filter. + + The state is designed to be used with the coefficients defined in AnalogTwoPoleCoefficients. +*/ +template +struct AnalogTwoPoleState +{ + CoeffType s1 = static_cast (0); + CoeffType s2 = static_cast (0); + + void reset() noexcept + { + s1 = static_cast (0); + s2 = static_cast (0); + } +}; + +//============================================================================== +/** Computes the low-pass output of a one-pole analog-model filter. + + @param input The input sample to process + @param state The current state of the filter + @param coefficients The coefficients defining the filter behavior + + @return The low-pass output sample +*/ +template +CoeffType getAnalogOnePoleLowPassOutput (CoeffType input, CoeffType state, const AnalogOnePoleCoefficients& coefficients) noexcept +{ + return coefficients.alpha * input + (static_cast (1) - coefficients.alpha) * state; +} + +/** Computes the high-pass output of a one-pole analog-model filter. + + @param input The input sample to process + @param state The current state of the filter + @param coefficients The coefficients defining the filter behavior + + @return The high-pass output sample +*/ +template +CoeffType getAnalogOnePoleHighPassOutput (CoeffType input, CoeffType state, const AnalogOnePoleCoefficients& coefficients) noexcept +{ + return (static_cast (1) - coefficients.alpha) * (input - state); +} + +/** Computes the next state of a one-pole analog-model filter. + + @param input The input sample to process + @param state The current state of the filter + @param coefficients The coefficients defining the filter behavior + + @return The next state of the filter +*/ +template +CoeffType getAnalogOnePoleNextState (CoeffType input, CoeffType state, const AnalogOnePoleCoefficients& coefficients) noexcept +{ + return static_cast (2) * coefficients.alpha * input + + (static_cast (1) - static_cast (2) * coefficients.alpha) * state; +} + +//============================================================================== +/** Processes a single sample through a two-pole analog-model filter. + + This function implements the processing for a trapezoidal-integrator two-pole state-variable filter. + The output is a combination of low-pass, high-pass, and band-pass responses based on the coefficients provided. + + @param input The input sample to process + @param coefficients The coefficients defining the filter behavior + @param state The state of the filter, which will be updated during processing + + @return The processed output sample + */ +template +SampleType processAnalogTwoPole ( + SampleType input, + const AnalogTwoPoleCoefficients& coefficients, + AnalogTwoPoleState& state) noexcept +{ + const auto inputValue = static_cast (input); + const auto highpass = (inputValue - coefficients.r2 * state.s1 - coefficients.g * state.s1 - state.s2) * coefficients.h; + const auto bandpass = coefficients.g * highpass + state.s1; + const auto bandpassFeedback = clipAnalogResonance (bandpass); + const auto lowpass = coefficients.g * bandpass + state.s2; + + state.s1 = coefficients.g * highpass + bandpassFeedback; + state.s2 = coefficients.g * bandpassFeedback + lowpass; + + const auto output = (coefficients.lowOut * lowpass + coefficients.bandOut * bandpass + coefficients.highOut * highpass) + * coefficients.gainCorrection; + + return static_cast (output); +} + +//============================================================================== +/** Computes the complex frequency response of a two-pole analog-model filter. + + @param coefficients The coefficients defining the filter behavior + @param frequency The frequency at which to evaluate the response + @param sampleRate The sample rate of the system + + @return The complex frequency response + */ +template +Complex getAnalogTwoPoleComplexResponse ( + const AnalogTwoPoleCoefficients& coefficients, + CoeffType frequency, + double sampleRate) noexcept +{ + using ComplexType = Complex; + + const auto highpassState1 = -coefficients.h * (coefficients.r2 + coefficients.g); + const auto highpassState2 = -coefficients.h; + const auto highpassInput = coefficients.h; + + const auto bandpassState1 = coefficients.g * highpassState1 + static_cast (1); + const auto bandpassState2 = coefficients.g * highpassState2; + const auto bandpassInput = coefficients.g * highpassInput; + + const auto lowpassState1 = coefficients.g * bandpassState1; + const auto lowpassState2 = coefficients.g * bandpassState2 + static_cast (1); + const auto lowpassInput = coefficients.g * bandpassInput; + + const auto state11 = coefficients.g * highpassState1 + bandpassState1; + const auto state12 = coefficients.g * highpassState2 + bandpassState2; + const auto state1Input = coefficients.g * highpassInput + bandpassInput; + + const auto state21 = coefficients.g * bandpassState1 + lowpassState1; + const auto state22 = coefficients.g * bandpassState2 + lowpassState2; + const auto state2Input = coefficients.g * bandpassInput + lowpassInput; + + const auto outputState1 = (coefficients.lowOut * lowpassState1 + coefficients.bandOut * bandpassState1 + coefficients.highOut * highpassState1) + * coefficients.gainCorrection; + const auto outputState2 = (coefficients.lowOut * lowpassState2 + coefficients.bandOut * bandpassState2 + coefficients.highOut * highpassState2) + * coefficients.gainCorrection; + const auto outputInput = (coefficients.lowOut * lowpassInput + coefficients.bandOut * bandpassInput + coefficients.highOut * highpassInput) + * coefficients.gainCorrection; + + const auto z = polar (static_cast (1), frequencyToAngular (frequency, static_cast (sampleRate))); + const auto determinant = (z - state11) * (z - state22) - state12 * state21; + const auto responseState1 = ((z - state22) * state1Input + state12 * state2Input) / determinant; + const auto responseState2 = (state21 * state1Input + (z - state11) * state2Input) / determinant; + + return ComplexType (outputInput) + outputState1 * responseState1 + outputState2 * responseState2; +} + +} // namespace yup diff --git a/modules/yup_dsp/base/yup_AnalogSaturator.h b/modules/yup_dsp/base/yup_AnalogSaturator.h new file mode 100644 index 000000000..2467a1612 --- /dev/null +++ b/modules/yup_dsp/base/yup_AnalogSaturator.h @@ -0,0 +1,70 @@ +/* + ============================================================================== + + This file is part of the YUP library. + Copyright (c) 2026 - kunitoki@gmail.com + + YUP is an open source library subject to open-source licensing. + + The code included in this file is provided under the terms of the ISC license + http://www.isc.org/downloads/software-support-policy/isc-license. Permission + to use, copy, modify, and/or distribute this software for any purpose with or + without fee is hereby granted provided that the above copyright notice and + this permission notice appear in all copies. + + YUP IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER + EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE + DISCLAIMED. + + ============================================================================== +*/ + +#pragma once + +namespace yup +{ + +//============================================================================== +/** Clips the input value to the range [-4, 4] using a hyperbolic tangent function. + + This function is used to limit the feedback in analog-model filters to prevent + instability while preserving the character of the resonance. + + @param input The input value to clip + + @return The clipped output value +*/ +template +CoeffType clipAnalogResonance (CoeffType input) noexcept +{ + return std::tanh (input * static_cast (0.25)) * static_cast (4); +} + +/** Analog saturator structure. + + This structure implements a simple symmetric atan-based waveshaper for analog + saturation effects. The drive parameter controls the amount of saturation, while + preScale and postScale allow for adjusting the input and output levels to + achieve the desired tonal characteristics. +*/ +template +struct AnalogSaturator +{ + void setCoefficients (const AnalogSaturatorCoefficients& newCoefficients) noexcept + { + coefficients = newCoefficients; + } + + SampleType process (SampleType input) const noexcept + { + if (coefficients.drive <= static_cast (0)) + return input; + + return static_cast ( + std::atan (static_cast (input) * coefficients.preScale) * coefficients.postScale); + } + + AnalogSaturatorCoefficients coefficients; +}; + +} // namespace yup diff --git a/modules/yup_dsp/designers/yup_AnalogFilterDesigner.cpp b/modules/yup_dsp/designers/yup_AnalogFilterDesigner.cpp new file mode 100644 index 000000000..b7e5742ea --- /dev/null +++ b/modules/yup_dsp/designers/yup_AnalogFilterDesigner.cpp @@ -0,0 +1,395 @@ +/* + ============================================================================== + + This file is part of the YUP library. + Copyright (c) 2026 - kunitoki@gmail.com + + YUP is an open source library subject to open-source licensing. + + The code included in this file is provided under the terms of the ISC license + http://www.isc.org/downloads/software-support-policy/isc-license. Permission + to use, copy, modify, and/or distribute this software for any purpose with or + without fee is hereby granted provided that the above copyright notice and + this permission notice appear in all copies. + + YUP IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER + EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE + DISCLAIMED. + + ============================================================================== +*/ + +namespace yup +{ + +//============================================================================== + +template +AnalogSaturatorCoefficients AnalogFilterDesigner::designSaturator ( + CoeffType drive) noexcept +{ + drive = sanitizeNormalized (drive); + + AnalogSaturatorCoefficients coefficients; + coefficients.drive = drive; + + if (drive > static_cast (0)) + { + coefficients.preScale = square (drive * static_cast (5)); + coefficients.postScale = static_cast (1) / std::atan (coefficients.preScale) + * std::pow (static_cast (4), -drive); + } + + return coefficients; +} + +//============================================================================== + +template +AnalogTwoPoleCoefficients AnalogFilterDesigner::designTwoPole ( + FilterModeType mode, + CoeffType frequency, + double sampleRate, + CoeffType normalizedResonance) noexcept +{ + const auto fs = sanitizeSampleRate (sampleRate); + frequency = sanitizeFrequency (frequency, fs); + normalizedResonance = sanitizeNormalized (normalizedResonance); + + constexpr auto maxQ = static_cast (16); + + AnalogTwoPoleCoefficients coefficients; + coefficients.g = std::tan (MathConstants::pi * frequency / fs); + + if (mode.test (FilterMode::bandpassCpg)) + { + const auto k = static_cast (0.075) + + square (static_cast (1) - normalizedResonance) + * (maxQ / static_cast (4) - static_cast (0.075)); + const auto lowerFrequency = frequency * std::pow (static_cast (2), -k / static_cast (2)); + const auto lowerG = std::tan (MathConstants::pi * lowerFrequency / fs); + const auto ratio = lowerG / coefficients.g; + const auto ratioTerm = (static_cast (1) - ratio * ratio) + * (static_cast (1) - ratio * ratio) + / (static_cast (4) * ratio * ratio); + + coefficients.r2 = static_cast (2) * std::sqrt (jmax (static_cast (0), ratioTerm)); + coefficients.h = static_cast (1) + / (static_cast (1) + coefficients.r2 * coefficients.g + coefficients.g * coefficients.g); + coefficients.lowOut = static_cast (0); + coefficients.bandOut = coefficients.r2; + coefficients.highOut = static_cast (0); + coefficients.gainCorrection = static_cast (1); + return coefficients; + } + + if (mode.test (FilterMode::bandstop)) + { + const auto k = static_cast (0.075) + + square (static_cast (1) - normalizedResonance) + * (maxQ / static_cast (4) - static_cast (0.075)); + + coefficients.r2 = static_cast (1) / k; + coefficients.h = static_cast (1) + / (static_cast (1) + coefficients.r2 * coefficients.g + coefficients.g * coefficients.g); + coefficients.lowOut = static_cast (1); + coefficients.bandOut = static_cast (0); + coefficients.highOut = static_cast (1); + coefficients.gainCorrection = static_cast (1); + return coefficients; + } + + if (mode.test (FilterMode::peak)) + { + const auto k = static_cast (1) + + square (normalizedResonance) * (static_cast (2) * maxQ - static_cast (1)); + + coefficients.r2 = static_cast (1) / k; + coefficients.h = static_cast (1) + / (static_cast (1) + coefficients.r2 * coefficients.g + coefficients.g * coefficients.g); + coefficients.lowOut = static_cast (1); + coefficients.bandOut = static_cast (1); + coefficients.highOut = static_cast (1); + coefficients.gainCorrection = static_cast (1) + / (static_cast (1) + (k - static_cast (1)) / static_cast (8)); + return coefficients; + } + + const auto k = static_cast (0.35) + + square (normalizedResonance) * (maxQ - static_cast (0.35)); + + coefficients.r2 = static_cast (1) / k; + coefficients.h = static_cast (1) + / (static_cast (1) + coefficients.r2 * coefficients.g + coefficients.g * coefficients.g); + coefficients.lowOut = mode.test (FilterMode::lowpass) ? static_cast (1) : static_cast (0); + coefficients.bandOut = mode.test (FilterMode::bandpassCsg) ? static_cast (1) : static_cast (0); + coefficients.highOut = mode.test (FilterMode::highpass) ? static_cast (1) : static_cast (0); + + if (mode.test (FilterMode::bandpassCsg)) + coefficients.gainCorrection = static_cast (2) + / (static_cast (1) + (k - static_cast (0.7)) / static_cast (8)); + else + coefficients.gainCorrection = static_cast (1) + / (static_cast (1) + (k - static_cast (0.7)) / static_cast (8)); + + return coefficients; +} + +//============================================================================== + +template +AnalogVowelCoefficients AnalogFilterDesigner::designVowel ( + CoeffType vowel, + double sampleRate, + CoeffType normalizedResonance) noexcept +{ + static constexpr std::array, 10> vowelFrequencies { { { { static_cast (570), static_cast (840), static_cast (2410) } }, + { { static_cast (300), static_cast (870), static_cast (2240) } }, + { { static_cast (440), static_cast (1020), static_cast (2240) } }, + { { static_cast (730), static_cast (1090), static_cast (2440) } }, + { { static_cast (520), static_cast (1190), static_cast (2390) } }, + { { static_cast (490), static_cast (1350), static_cast (1690) } }, + { { static_cast (660), static_cast (1720), static_cast (2410) } }, + { { static_cast (530), static_cast (1840), static_cast (2480) } }, + { { static_cast (390), static_cast (1990), static_cast (2550) } }, + { { static_cast (270), static_cast (2290), static_cast (3010) } } } }; + + vowel = sanitizeNormalized (vowel); + normalizedResonance = sanitizeNormalized (normalizedResonance); + + const auto scaledVowel = vowel * static_cast (vowelFrequencies.size() - 1); + const auto baseIndex = jmin (static_cast (scaledVowel), static_cast (vowelFrequencies.size() - 1)); + const auto nextIndex = jmin (baseIndex + 1, static_cast (vowelFrequencies.size() - 1)); + const auto fraction = scaledVowel - static_cast (baseIndex); + const auto currentWeight = square (square (static_cast (1) - fraction)); + const auto nextWeight = static_cast (1) - currentWeight; + + AnalogVowelCoefficients coefficients; + + for (std::size_t i = 0; i < coefficients.formants.size(); ++i) + { + const auto formantFrequency = vowelFrequencies[static_cast (baseIndex)][i] * currentWeight + + vowelFrequencies[static_cast (nextIndex)][i] * nextWeight; + coefficients.formants[i] = designTwoPole (FilterMode::peak, formantFrequency, sampleRate, normalizedResonance); + } + + coefficients.gainCompensation = static_cast (1) + + square (square (normalizedResonance)) * static_cast (18); + + return coefficients; +} + +//============================================================================== + +template +AnalogKorg35Coefficients AnalogFilterDesigner::designKorg35 ( + FilterModeType mode, + CoeffType frequency, + double sampleRate, + CoeffType resonance, + CoeffType saturation) noexcept +{ + const auto fs = sanitizeSampleRate (sampleRate); + frequency = sanitizeFrequency (frequency, fs); + resonance = sanitizeNormalized (resonance); + saturation = sanitizeNormalized (saturation); + mode = resolveFilterMode (mode, FilterMode::lowpass | FilterMode::bandpassCsg | FilterMode::highpass); + + const auto t2 = static_cast (0.5) / fs; + const auto wa = static_cast (2) * fs * std::tan (MathConstants::twoPi * frequency * t2); + const auto g = wa * t2; + const auto gI = static_cast (1) / (static_cast (1) + g); + const auto G = g * gI; + + AnalogKorg35Coefficients coefficients; + coefficients.poles[0].alpha = G; + coefficients.poles[1].alpha = G; + coefficients.poles[2].alpha = G; + coefficients.feedback = static_cast (0.01) + + resonance * static_cast (1.99) + - (static_cast (1) - saturation) * static_cast (0.001); + + coefficients.poles[0].beta = static_cast (1); + + if (mode.test (FilterMode::lowpass)) + { + coefficients.poles[1].beta = (coefficients.feedback - coefficients.feedback * G) * gI; + coefficients.poles[2].beta = -gI; + coefficients.gainCorrection = (static_cast (1) / coefficients.feedback) + / (static_cast (1) + square (resonance) * static_cast (1.5)); + } + else if (mode.test (FilterMode::bandpass)) + { + coefficients.poles[1].beta = gI; + coefficients.poles[2].beta = -G * gI; + coefficients.gainCorrection = (static_cast (2.2) / coefficients.feedback) + / (static_cast (1) + square (resonance)); + } + else + { + coefficients.poles[1].beta = gI; + coefficients.poles[2].beta = -G * gI; + coefficients.gainCorrection = (static_cast (1) / coefficients.feedback) + / (static_cast (1) + square (resonance)); + } + + coefficients.alpha0 = static_cast (1) + / (static_cast (1) - coefficients.feedback * G + coefficients.feedback * G * G); + + return coefficients; +} + +//============================================================================== + +template +AnalogMoogLadderCoefficients AnalogFilterDesigner::designMoogLadder ( + AnalogMoogLadderMode mode, + CoeffType frequency, + double sampleRate, + CoeffType resonance, + CoeffType saturation) noexcept +{ + static constexpr std::array, 10> outputGains { { { { static_cast (0), static_cast (0), static_cast (0), static_cast (0), static_cast (1) } }, + { { static_cast (1), static_cast (-4), static_cast (6), static_cast (-4), static_cast (1) } }, + { { static_cast (0), static_cast (0), static_cast (0), static_cast (1), static_cast (0) } }, + { { static_cast (1), static_cast (-3), static_cast (3), static_cast (-1), static_cast (0) } }, + { { static_cast (0), static_cast (0), static_cast (1), static_cast (0), static_cast (0) } }, + { { static_cast (1), static_cast (-2), static_cast (1), static_cast (0), static_cast (0) } }, + { { static_cast (0), static_cast (1), static_cast (0), static_cast (0), static_cast (0) } }, + { { static_cast (1), static_cast (-1), static_cast (0), static_cast (0), static_cast (0) } }, + { { static_cast (0), static_cast (0), static_cast (1), static_cast (-2), static_cast (1) } }, + { { static_cast (0), static_cast (1), static_cast (-1), static_cast (0), static_cast (0) } } } }; + + const auto fs = sanitizeSampleRate (sampleRate); + frequency = sanitizeFrequency (frequency, fs); + resonance = sanitizeNormalized (resonance); + saturation = sanitizeNormalized (saturation); + + const auto t2 = static_cast (0.5) / fs; + const auto wa = static_cast (2) * fs * std::tan (MathConstants::twoPi * frequency * t2); + const auto g = wa * t2; + const auto gI = static_cast (1) / (static_cast (1) + g); + const auto G = g * gI; + + AnalogMoogLadderCoefficients coefficients; + + for (auto& pole : coefficients.poles) + pole.alpha = G; + + coefficients.poles[0].beta = G * G * G * gI; + coefficients.poles[1].beta = G * G * gI; + coefficients.poles[2].beta = G * gI; + coefficients.poles[3].beta = gI; + + coefficients.outputs = outputGains[static_cast (mode)]; + coefficients.feedback = resonance * (static_cast (4) - static_cast (0.02) * (static_cast (1) - saturation * static_cast (0.98))); + coefficients.alpha0 = static_cast (1) + / (static_cast (1) + coefficients.feedback * G * G * G * G); + + switch (mode) + { + case AnalogMoogLadderMode::lowpass24: + case AnalogMoogLadderMode::lowpass18: + case AnalogMoogLadderMode::lowpass12: + case AnalogMoogLadderMode::lowpass6: + coefficients.gainCorrection = std::pow (static_cast (1) + resonance * static_cast (4), static_cast (0.45)); + break; + + case AnalogMoogLadderMode::highpass24: + case AnalogMoogLadderMode::highpass18: + case AnalogMoogLadderMode::highpass12: + case AnalogMoogLadderMode::highpass6: + coefficients.gainCorrection = static_cast (1) - square (resonance * static_cast (0.6)); + break; + + case AnalogMoogLadderMode::bandpass12: + coefficients.gainCorrection = static_cast (4); + break; + + case AnalogMoogLadderMode::bandpass6: + coefficients.gainCorrection = static_cast (2); + break; + } + + return coefficients; +} + +//============================================================================== + +template +AnalogRolandDiodeCoefficients AnalogFilterDesigner::designRolandDiode ( + CoeffType frequency, + double sampleRate, + CoeffType resonance, + CoeffType saturation) noexcept +{ + const auto fs = sanitizeSampleRate (sampleRate); + frequency = sanitizeFrequency (frequency, fs); + resonance = sanitizeNormalized (resonance); + saturation = sanitizeNormalized (saturation); + + AnalogRolandDiodeCoefficients coefficients; + coefficients.cutoff = frequency / (static_cast (2) * fs) * MathConstants::sqrt2; + coefficients.feedback = resonance * (static_cast (17.1) - static_cast (0.1) * (static_cast (1) - saturation * static_cast (0.99))); + coefficients.gainCorrection = static_cast (1.05) + static_cast (0.8) * coefficients.feedback - square (resonance) * static_cast (7); + + const auto highpassFc = static_cast (50) / (static_cast (2) * fs); + const auto kh = highpassFc * MathConstants::pi; + const auto khp2Inv = static_cast (1) / (kh + static_cast (2)); + coefficients.highpassA = (kh - static_cast (2)) * khp2Inv; + coefficients.highpassB = static_cast (2) * khp2Inv; + + coefficients.a = MathConstants::pi * coefficients.cutoff; + coefficients.a2 = coefficients.a * coefficients.a; + coefficients.aInv = static_cast (1) / coefficients.a; + coefficients.b = static_cast (2) * coefficients.a + static_cast (1); + coefficients.b2 = coefficients.b * coefficients.b; + coefficients.c = static_cast (1) + / (static_cast (2) * coefficients.a2 * coefficients.a2 + - static_cast (4) * coefficients.a2 * coefficients.b2 + + coefficients.b2 * coefficients.b2); + coefficients.g0 = static_cast (2) * coefficients.a2 * coefficients.a2 * coefficients.c; + coefficients.g = coefficients.g0 * coefficients.highpassB; + coefficients.fg = static_cast (1) / (static_cast (1) + coefficients.g * coefficients.feedback); + + return coefficients; +} + +//============================================================================== + +template +CoeffType AnalogFilterDesigner::sanitizeFrequency (CoeffType frequency, double sampleRate) noexcept +{ + const auto fs = sanitizeSampleRate (sampleRate); + const auto maxFrequency = jmax (static_cast (20), fs * static_cast (0.5) / (static_cast (22050) / static_cast (18000))); + + if (! std::isfinite (frequency)) + return static_cast (20); + + return jlimit (static_cast (20), maxFrequency, frequency); +} + +template +CoeffType AnalogFilterDesigner::sanitizeSampleRate (double sampleRate) noexcept +{ + if (! std::isfinite (sampleRate)) + return static_cast (44100); + + return jmax (static_cast (11025), static_cast (sampleRate)); +} + +template +CoeffType AnalogFilterDesigner::sanitizeNormalized (CoeffType value) noexcept +{ + if (! std::isfinite (value)) + return static_cast (0); + + return jlimit (static_cast (0), static_cast (1), value); +} + +//============================================================================== + +template class AnalogFilterDesigner; +template class AnalogFilterDesigner; + +} // namespace yup diff --git a/modules/yup_dsp/designers/yup_AnalogFilterDesigner.h b/modules/yup_dsp/designers/yup_AnalogFilterDesigner.h new file mode 100644 index 000000000..6f0ec8af8 --- /dev/null +++ b/modules/yup_dsp/designers/yup_AnalogFilterDesigner.h @@ -0,0 +1,125 @@ +/* + ============================================================================== + + This file is part of the YUP library. + Copyright (c) 2026 - kunitoki@gmail.com + + YUP is an open source library subject to open-source licensing. + + The code included in this file is provided under the terms of the ISC license + http://www.isc.org/downloads/software-support-policy/isc-license. Permission + to use, copy, modify, and/or distribute this software for any purpose with or + without fee is hereby granted provided that the above copyright notice and + this permission notice appear in all copies. + + YUP IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER + EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE + DISCLAIMED. + + ============================================================================== +*/ + +#pragma once + +namespace yup +{ + +//============================================================================== +/** + Designs coefficients for nonlinear analog-model filters. + + These designs are intended for topology-preserving filters and ladder models + where the coefficient set describes a filter topology rather than a plain + transfer-function biquad. +*/ +template +class AnalogFilterDesigner +{ +public: + /** + Designs coefficients for the shared atan saturator. + + @param drive Normalized drive amount in the range 0..1 + */ + static AnalogSaturatorCoefficients designSaturator (CoeffType drive) noexcept; + + /** + Designs a two-pole topology-preserving state-variable filter. + + @param mode Output mode. Supported modes are low-pass, + high-pass, band-pass, peak, and band-stop. + @param frequency Cutoff or center frequency in Hz + @param sampleRate Sample rate in Hz + @param normalizedResonance Normalized resonance amount in the range 0..1 + */ + static AnalogTwoPoleCoefficients designTwoPole ( + FilterModeType mode, + CoeffType frequency, + double sampleRate, + CoeffType normalizedResonance) noexcept; + + /** + Designs a vowel/formant filter as three cascaded two-pole peak filters. + + @param vowel Normalized vowel position in the range 0..1 + @param sampleRate Sample rate in Hz + @param normalizedResonance Normalized formant resonance in the range 0..1 + */ + static AnalogVowelCoefficients designVowel ( + CoeffType vowel, + double sampleRate, + CoeffType normalizedResonance) noexcept; + + /** + Designs coefficients for a Korg35-inspired filter. + + @param mode Supported modes are low-pass, band-pass, and high-pass + @param frequency Cutoff frequency in Hz + @param sampleRate Sample rate in Hz + @param resonance Normalized resonance amount in the range 0..1 + @param saturation Normalized saturation amount in the range 0..1 + */ + static AnalogKorg35Coefficients designKorg35 ( + FilterModeType mode, + CoeffType frequency, + double sampleRate, + CoeffType resonance, + CoeffType saturation) noexcept; + + /** + Designs coefficients for a four-pole Moog ladder-style filter. + + @param mode Ladder output tap mix + @param frequency Cutoff frequency in Hz + @param sampleRate Sample rate in Hz + @param resonance Normalized resonance amount in the range 0..1 + @param saturation Normalized saturation amount in the range 0..1 + */ + static AnalogMoogLadderCoefficients designMoogLadder ( + AnalogMoogLadderMode mode, + CoeffType frequency, + double sampleRate, + CoeffType resonance, + CoeffType saturation) noexcept; + + /** + Designs coefficients for a Roland diode-ladder-inspired low-pass filter. + + @param frequency Cutoff frequency in Hz + @param sampleRate Sample rate in Hz + @param resonance Normalized resonance amount in the range 0..1 + @param saturation Normalized saturation amount in the range 0..1 + */ + static AnalogRolandDiodeCoefficients designRolandDiode ( + CoeffType frequency, + double sampleRate, + CoeffType resonance, + CoeffType saturation) noexcept; + +private: + static CoeffType sanitizeFrequency (CoeffType frequency, double sampleRate) noexcept; + static CoeffType sanitizeSampleRate (double sampleRate) noexcept; + static CoeffType sanitizeNormalized (CoeffType value) noexcept; +}; + +} // namespace yup diff --git a/modules/yup_dsp/designers/yup_FilterDesigner.cpp b/modules/yup_dsp/designers/yup_FilterDesigner.cpp index 2972eb0c0..35e58d6a1 100644 --- a/modules/yup_dsp/designers/yup_FilterDesigner.cpp +++ b/modules/yup_dsp/designers/yup_FilterDesigner.cpp @@ -387,12 +387,12 @@ BiquadCoefficients FilterDesigner::designZoelzer ( template int FilterDesigner::designButterworth ( + std::vector>& coefficients, FilterModeType filterMode, int order, CoeffType frequency, CoeffType frequency2, - double sampleRate, - std::vector>& coefficients) noexcept + double sampleRate) noexcept { // Validate inputs jassert (order >= 2 && order <= 16); @@ -540,11 +540,11 @@ int FilterDesigner::designButterworth ( template int FilterDesigner::designLinkwitzRiley ( + std::vector>& lowCoeffs, + std::vector>& highCoeffs, int order, CoeffType crossoverFreq, - double sampleRate, - std::vector>& lowCoeffs, - std::vector>& highCoeffs) noexcept + double sampleRate) noexcept { jassert (order >= 2 && order <= 16); jassert ((order & 1) == 0); // Must be even diff --git a/modules/yup_dsp/designers/yup_FilterDesigner.h b/modules/yup_dsp/designers/yup_FilterDesigner.h index f74b7097d..0fb8cfb1c 100644 --- a/modules/yup_dsp/designers/yup_FilterDesigner.h +++ b/modules/yup_dsp/designers/yup_FilterDesigner.h @@ -432,115 +432,110 @@ class FilterDesigner /** Butterworth implementation with mode selection */ static int designButterworth ( + std::vector>& coefficients, FilterModeType filterMode, int order, CoeffType frequency, CoeffType frequency2, - double sampleRate, - std::vector>& coefficients) noexcept; + double sampleRate) noexcept; /** Designs Butterworth lowpass filter coefficients. + @param coefficients Output vector for biquad coefficients @param order The filter order (2, 4, 8, 16, 32) @param frequency The cutoff frequency in Hz @param sampleRate The sample rate in Hz - @param workspace Pre-allocated workspace to avoid allocations - @param coefficients Output vector for biquad coefficients @returns Number of biquad sections created */ static int designButterworthLowpass ( + std::vector>& coefficients, int order, CoeffType frequency, - double sampleRate, - std::vector>& coefficients) noexcept + double sampleRate) noexcept { - return designButterworth (FilterMode::lowpass, order, frequency, static_cast (0.0), sampleRate, coefficients); + return designButterworth (coefficients, FilterMode::lowpass, order, frequency, static_cast (0.0), sampleRate); } /** Designs Butterworth highpass filter coefficients. + @param coefficients Output vector for biquad coefficients @param order The filter order (2, 4, 8, 16, 32) @param frequency The cutoff frequency in Hz @param sampleRate The sample rate in Hz - @param workspace Pre-allocated workspace to avoid allocations - @param coefficients Output vector for biquad coefficients @returns Number of biquad sections created */ static int designButterworthHighpass ( + std::vector>& coefficients, int order, CoeffType frequency, - double sampleRate, - std::vector>& coefficients) noexcept + double sampleRate) noexcept { - return designButterworth (FilterMode::highpass, order, frequency, static_cast (0.0), sampleRate, coefficients); + return designButterworth (coefficients, FilterMode::highpass, order, frequency, static_cast (0.0), sampleRate); } /** Designs Butterworth bandpass filter coefficients. + @param coefficients Output vector for biquad coefficients @param order The filter order (2, 4, 8, 16, 32) @param lowFreq The lower cutoff frequency in Hz @param highFreq The upper cutoff frequency in Hz @param sampleRate The sample rate in Hz - @param workspace Pre-allocated workspace to avoid allocations - @param coefficients Output vector for biquad coefficients @returns Number of biquad sections created */ static int designButterworthBandpass ( + std::vector>& coefficients, int order, CoeffType lowFreq, CoeffType highFreq, - double sampleRate, - std::vector>& coefficients) noexcept + double sampleRate) noexcept { - return designButterworth (FilterMode::bandpass, order, lowFreq, highFreq, sampleRate, coefficients); + return designButterworth (coefficients, FilterMode::bandpass, order, lowFreq, highFreq, sampleRate); } /** Designs Butterworth bandstop filter coefficients. + @param coefficients Output vector for biquad coefficients @param order The filter order (2, 4, 8, 16, 32) @param lowFreq The lower cutoff frequency in Hz @param highFreq The upper cutoff frequency in Hz @param sampleRate The sample rate in Hz - @param workspace Pre-allocated workspace to avoid allocations - @param coefficients Output vector for biquad coefficients @returns Number of biquad sections created */ static int designButterworthBandstop ( + std::vector>& coefficients, int order, CoeffType lowFreq, CoeffType highFreq, - double sampleRate, - std::vector>& coefficients) noexcept + double sampleRate) noexcept { - return designButterworth (FilterMode::bandstop, order, lowFreq, highFreq, sampleRate, coefficients); + return designButterworth (coefficients, FilterMode::bandstop, order, lowFreq, highFreq, sampleRate); } /** Designs Butterworth allpass filter coefficients. + @param coefficients Output vector for biquad coefficients @param order The filter order (2, 4, 8, 16, 32) @param frequency The characteristic frequency in Hz @param sampleRate The sample rate in Hz - @param workspace Pre-allocated workspace to avoid allocations - @param coefficients Output vector for biquad coefficients @returns Number of biquad sections created */ static int designButterworthAllpass ( + std::vector>& coefficients, int order, CoeffType frequency, - double sampleRate, - std::vector>& coefficients) noexcept + double sampleRate) noexcept { - return designButterworth (FilterMode::allpass, order, frequency, static_cast (0.0), sampleRate, coefficients); + return designButterworth (coefficients, FilterMode::allpass, order, frequency, static_cast (0.0), sampleRate); } //============================================================================== @@ -550,20 +545,20 @@ class FilterDesigner /** General Linkwitz-Riley crossover designer with order specification. + @param lowCoeffs Output vector for lowpass biquad coefficients + @param highCoeffs Output vector for highpass biquad coefficients @param order The filter order (2, 4, 8, 16) @param crossoverFreq The crossover frequency in Hz @param sampleRate The sample rate in Hz - @param lowCoeffs Output vector for lowpass biquad coefficients - @param highCoeffs Output vector for highpass biquad coefficients @returns Number of biquad sections created */ static int designLinkwitzRiley ( + std::vector>& lowCoeffs, + std::vector>& highCoeffs, int order, CoeffType crossoverFreq, - double sampleRate, - std::vector>& lowCoeffs, - std::vector>& highCoeffs) noexcept; + double sampleRate) noexcept; /** Designs Linkwitz-Riley (LR2) 2nd order crossover coefficients. @@ -572,58 +567,58 @@ class FilterDesigner filters, resulting in complementary magnitude responses that sum to unity gain with phase alignment at the crossover frequency. - @param crossoverFreq The crossover frequency in Hz - @param sampleRate The sample rate in Hz @param lowCoeffs Output coefficients for lowpass section @param highCoeffs Output coefficients for highpass section + @param crossoverFreq The crossover frequency in Hz + @param sampleRate The sample rate in Hz @returns True if coefficients were successfully calculated */ static bool designLinkwitzRiley2 ( - CoeffType crossoverFreq, - double sampleRate, std::vector>& lowCoeffs, - std::vector>& highCoeffs) noexcept + std::vector>& highCoeffs, + CoeffType crossoverFreq, + double sampleRate) noexcept { - return designLinkwitzRiley (2, crossoverFreq, sampleRate, lowCoeffs, highCoeffs); + return designLinkwitzRiley (lowCoeffs, highCoeffs, 2, crossoverFreq, sampleRate); } /** Designs Linkwitz-Riley 4th order crossover coefficients. - @param crossoverFreq The crossover frequency in Hz - @param sampleRate The sample rate in Hz @param lowCoeffs Output vector for lowpass biquad coefficients @param highCoeffs Output vector for highpass biquad coefficients + @param crossoverFreq The crossover frequency in Hz + @param sampleRate The sample rate in Hz @returns Number of biquad sections created (2 for LR4) */ static int designLinkwitzRiley4 ( - CoeffType crossoverFreq, - double sampleRate, std::vector>& lowCoeffs, - std::vector>& highCoeffs) noexcept + std::vector>& highCoeffs, + CoeffType crossoverFreq, + double sampleRate) noexcept { - return designLinkwitzRiley (4, crossoverFreq, sampleRate, lowCoeffs, highCoeffs); + return designLinkwitzRiley (lowCoeffs, highCoeffs, 4, crossoverFreq, sampleRate); } /** Designs Linkwitz-Riley 8th order crossover coefficients. - @param crossoverFreq The crossover frequency in Hz - @param sampleRate The sample rate in Hz @param lowCoeffs Output vector for lowpass biquad coefficients @param highCoeffs Output vector for highpass biquad coefficients + @param crossoverFreq The crossover frequency in Hz + @param sampleRate The sample rate in Hz @returns Number of biquad sections created (4 for LR8) */ static int designLinkwitzRiley8 ( - CoeffType crossoverFreq, - double sampleRate, std::vector>& lowCoeffs, - std::vector>& highCoeffs) noexcept + std::vector>& highCoeffs, + CoeffType crossoverFreq, + double sampleRate) noexcept { - return designLinkwitzRiley (8, crossoverFreq, sampleRate, lowCoeffs, highCoeffs); + return designLinkwitzRiley (lowCoeffs, highCoeffs, 8, crossoverFreq, sampleRate); } //============================================================================== diff --git a/modules/yup_dsp/filters/yup_AnalogFilters.h b/modules/yup_dsp/filters/yup_AnalogFilters.h new file mode 100644 index 000000000..3baf777cc --- /dev/null +++ b/modules/yup_dsp/filters/yup_AnalogFilters.h @@ -0,0 +1,721 @@ +/* + ============================================================================== + + This file is part of the YUP library. + Copyright (c) 2026 - kunitoki@gmail.com + + YUP is an open source library subject to open-source licensing. + + The code included in this file is provided under the terms of the ISC license + http://www.isc.org/downloads/software-support-policy/isc-license. Permission + to use, copy, modify, and/or distribute this software for any purpose with or + without fee is hereby granted provided that the above copyright notice and + this permission notice appear in all copies. + + YUP IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER + EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE + DISCLAIMED. + + ============================================================================== +*/ + +#pragma once + +namespace yup +{ + +//============================================================================== +/** + Two-pole topology-preserving analog-model filter. + + The filter supports low-pass, high-pass, two band-pass variants, peak, and + band-stop modes. Resonance is normalized to 0..1 to match the analog-model + designs. +*/ +template +class AnalogTwoPoleFilter : public FilterBase +{ +public: + AnalogTwoPoleFilter() + { + setParameters (FilterMode::lowpass, static_cast (1000), static_cast (0), static_cast (0), this->sampleRate); + } + + explicit AnalogTwoPoleFilter (FilterModeType mode) + { + setParameters (mode, static_cast (1000), static_cast (0), static_cast (0), this->sampleRate); + } + + /** Sets all filter parameters. */ + void setParameters ( + FilterModeType mode, + CoeffType frequency, + CoeffType normalizedResonance, + CoeffType saturation, + double sampleRate) noexcept + { + mode = resolveFilterMode (mode, getSupportedModes()); + + if (filterMode != mode + || ! approximatelyEqual (centerFreq, frequency) + || ! approximatelyEqual (resonance, normalizedResonance) + || ! approximatelyEqual (drive, saturation) + || ! approximatelyEqual (this->sampleRate, sampleRate)) + { + filterMode = mode; + centerFreq = frequency; + resonance = normalizedResonance; + drive = saturation; + this->sampleRate = sampleRate; + updateCoefficients(); + } + } + + FilterModeType getSupportedModes() const noexcept override + { + return FilterMode::lowpass | FilterMode::highpass | FilterMode::bandpassCsg | FilterMode::bandpassCpg | FilterMode::bandstop | FilterMode::peak; + } + + void setSignalRange (CoeffType range) noexcept + { + jassert (range > static_cast (0)); + + signalRange = jmax (range, std::numeric_limits::min()); + inverseSignalRange = static_cast (1) / signalRange; + } + + CoeffType getSignalRange() const noexcept + { + return signalRange; + } + + void reset() noexcept override + { + state.reset(); + } + + void prepare (double sampleRate, int maximumBlockSize) override + { + this->sampleRate = sampleRate; + this->maximumBlockSize = maximumBlockSize; + updateCoefficients(); + reset(); + } + + SampleType processSample (SampleType inputSample) noexcept override + { + auto input = static_cast (static_cast (inputSample) * inverseSignalRange); + input = saturator.process (input); + + auto output = processAnalogTwoPole (input, coefficients, state); + output = saturator.process (output); + + return static_cast (static_cast (output) * signalRange); + } + + void processBlock (const SampleType* inputBuffer, SampleType* outputBuffer, int numSamples) noexcept override + { + for (int i = 0; i < numSamples; ++i) + outputBuffer[i] = processSample (inputBuffer[i]); + } + + Complex getComplexResponse (CoeffType frequency) const override + { + return getAnalogTwoPoleComplexResponse (coefficients, frequency, this->sampleRate); + } + +private: + void updateCoefficients() noexcept + { + coefficients = AnalogFilterDesigner::designTwoPole (filterMode, centerFreq, this->sampleRate, resonance); + saturator.setCoefficients (AnalogFilterDesigner::designSaturator (drive)); + } + + FilterModeType filterMode = FilterMode::lowpass; + CoeffType centerFreq = static_cast (1000); + CoeffType resonance = static_cast (0); + CoeffType drive = static_cast (0); + CoeffType signalRange = static_cast (1); + CoeffType inverseSignalRange = static_cast (1); + AnalogTwoPoleCoefficients coefficients; + AnalogTwoPoleState state; + AnalogSaturator saturator; + + YUP_LEAK_DETECTOR (AnalogTwoPoleFilter) +}; + +//============================================================================== +/** + Three-formant vowel filter built from analog two-pole peak sections. +*/ +template +class AnalogVowelFilter : public FilterBase +{ +public: + AnalogVowelFilter() + { + setParameters (static_cast (0), static_cast (0), static_cast (0), this->sampleRate); + } + + /** Sets vowel, resonance, saturation, and sample-rate parameters. */ + void setParameters (CoeffType vowelPosition, CoeffType normalizedResonance, CoeffType saturation, double sampleRate) noexcept + { + if (! approximatelyEqual (vowel, vowelPosition) + || ! approximatelyEqual (resonance, normalizedResonance) + || ! approximatelyEqual (drive, saturation) + || ! approximatelyEqual (this->sampleRate, sampleRate)) + { + vowel = vowelPosition; + resonance = normalizedResonance; + drive = saturation; + this->sampleRate = sampleRate; + updateCoefficients(); + } + } + + FilterModeType getSupportedModes() const noexcept override + { + return FilterMode::peak; + } + + void setSignalRange (CoeffType range) noexcept + { + jassert (range > static_cast (0)); + + signalRange = jmax (range, std::numeric_limits::min()); + inverseSignalRange = static_cast (1) / signalRange; + } + + CoeffType getSignalRange() const noexcept + { + return signalRange; + } + + void reset() noexcept override + { + for (auto& formantState : states) + formantState.reset(); + } + + void prepare (double sampleRate, int maximumBlockSize) override + { + this->sampleRate = sampleRate; + this->maximumBlockSize = maximumBlockSize; + updateCoefficients(); + reset(); + } + + SampleType processSample (SampleType inputSample) noexcept override + { + auto output = saturator.process (static_cast (static_cast (inputSample) * inverseSignalRange)); + + for (std::size_t i = 0; i < states.size(); ++i) + output = processAnalogTwoPole (output, coefficients.formants[i], states[i]); + + output = static_cast (static_cast (output) * coefficients.gainCompensation); + output = saturator.process (output); + + return static_cast (static_cast (output) * signalRange); + } + + void processBlock (const SampleType* inputBuffer, SampleType* outputBuffer, int numSamples) noexcept override + { + for (int i = 0; i < numSamples; ++i) + outputBuffer[i] = processSample (inputBuffer[i]); + } + + Complex getComplexResponse (CoeffType frequency) const override + { + auto response = Complex (coefficients.gainCompensation); + + for (const auto& formant : coefficients.formants) + response *= getAnalogTwoPoleComplexResponse (formant, frequency, this->sampleRate); + + return response; + } + +private: + void updateCoefficients() noexcept + { + coefficients = AnalogFilterDesigner::designVowel (vowel, this->sampleRate, resonance); + saturator.setCoefficients (AnalogFilterDesigner::designSaturator (drive)); + } + + CoeffType vowel = static_cast (0); + CoeffType resonance = static_cast (0); + CoeffType drive = static_cast (0); + CoeffType signalRange = static_cast (1); + CoeffType inverseSignalRange = static_cast (1); + AnalogVowelCoefficients coefficients; + std::array, 3> states; + AnalogSaturator saturator; + + YUP_LEAK_DETECTOR (AnalogVowelFilter) +}; + +//============================================================================== +/** + Korg35-inspired analog-model filter. +*/ +template +class AnalogKorg35Filter : public FilterBase +{ +public: + AnalogKorg35Filter() + { + setParameters (FilterMode::lowpass, static_cast (1000), static_cast (0), static_cast (0), this->sampleRate); + } + + explicit AnalogKorg35Filter (FilterModeType mode) + { + setParameters (mode, static_cast (1000), static_cast (0), static_cast (0), this->sampleRate); + } + + /** Sets all filter parameters. */ + void setParameters (FilterModeType mode, CoeffType frequency, CoeffType normalizedResonance, CoeffType saturation, double sampleRate) noexcept + { + mode = resolveFilterMode (mode, getSupportedModes()); + + if (filterMode != mode + || ! approximatelyEqual (centerFreq, frequency) + || ! approximatelyEqual (resonance, normalizedResonance) + || ! approximatelyEqual (drive, saturation) + || ! approximatelyEqual (this->sampleRate, sampleRate)) + { + filterMode = mode; + centerFreq = frequency; + resonance = normalizedResonance; + drive = saturation; + this->sampleRate = sampleRate; + updateCoefficients(); + } + } + + FilterModeType getSupportedModes() const noexcept override + { + return FilterMode::lowpass | FilterMode::bandpassCsg | FilterMode::highpass; + } + + void setSignalRange (CoeffType range) noexcept + { + jassert (range > static_cast (0)); + + signalRange = jmax (range, std::numeric_limits::min()); + inverseSignalRange = static_cast (1) / signalRange; + } + + void reset() noexcept override + { + for (auto& pole : poles) + pole.reset(); + } + + void prepare (double sampleRate, int maximumBlockSize) override + { + this->sampleRate = sampleRate; + this->maximumBlockSize = maximumBlockSize; + updateCoefficients(); + reset(); + } + + SampleType processSample (SampleType inputSample) noexcept override + { + auto input = saturator.process (static_cast (static_cast (inputSample) * inverseSignalRange)); + const auto inputValue = static_cast (input); + const auto firstPole = filterMode.test (FilterMode::highpass) + ? poles[0].processHighPass (inputValue, coefficients.poles[0]) + : poles[0].processLowPass (inputValue, coefficients.poles[0]); + const auto feedback = poles[2].getFeedbackOutput (coefficients.poles[2]) + + poles[1].getFeedbackOutput (coefficients.poles[1]); + auto u = coefficients.alpha0 * (firstPole + feedback); + u = clipAnalogResonance (u); + + auto output = coefficients.feedback * u; + + if (filterMode.test (FilterMode::lowpass)) + { + output = coefficients.feedback * poles[1].processLowPass (u, coefficients.poles[1]); + poles[2].processHighPass (output, coefficients.poles[2]); + } + else if (filterMode.test (FilterMode::bandpass)) + { + output = poles[1].processLowPass (poles[2].processHighPass (output, coefficients.poles[2]), coefficients.poles[1]); + } + else + { + poles[1].processLowPass (poles[2].processHighPass (output, coefficients.poles[2]), coefficients.poles[1]); + } + + output *= coefficients.gainCorrection; + + return static_cast (saturator.process (static_cast (output)) * signalRange); + } + + void processBlock (const SampleType* inputBuffer, SampleType* outputBuffer, int numSamples) noexcept override + { + for (int i = 0; i < numSamples; ++i) + outputBuffer[i] = processSample (inputBuffer[i]); + } + + Complex getComplexResponse (CoeffType frequency) const override + { + return getLinearizedComplexResponse<3, CoeffType> ( + [this] (CoeffType input, std::array state) + { + LinearStepResult<3, CoeffType> result; + + const auto firstPole = filterMode.test (FilterMode::highpass) + ? getAnalogOnePoleHighPassOutput (input, state[0], coefficients.poles[0]) + : getAnalogOnePoleLowPassOutput (input, state[0], coefficients.poles[0]); + const auto feedback = coefficients.poles[2].beta * state[2] + coefficients.poles[1].beta * state[1]; + const auto u = coefficients.alpha0 * (firstPole + feedback); + const auto feedbackInput = coefficients.feedback * u; + + result.state[0] = getAnalogOnePoleNextState (input, state[0], coefficients.poles[0]); + + if (filterMode.test (FilterMode::lowpass)) + { + const auto pole2Output = getAnalogOnePoleLowPassOutput (u, state[1], coefficients.poles[1]); + const auto output = coefficients.feedback * pole2Output; + + result.state[1] = getAnalogOnePoleNextState (u, state[1], coefficients.poles[1]); + result.state[2] = getAnalogOnePoleNextState (output, state[2], coefficients.poles[2]); + result.output = output * coefficients.gainCorrection; + } + else + { + const auto pole3Output = getAnalogOnePoleHighPassOutput (feedbackInput, state[2], coefficients.poles[2]); + const auto pole2Output = getAnalogOnePoleLowPassOutput (pole3Output, state[1], coefficients.poles[1]); + + result.state[1] = getAnalogOnePoleNextState (pole3Output, state[1], coefficients.poles[1]); + result.state[2] = getAnalogOnePoleNextState (feedbackInput, state[2], coefficients.poles[2]); + result.output = (filterMode.test (FilterMode::bandpass) ? pole2Output : feedbackInput) * coefficients.gainCorrection; + } + + return result; + }, + frequency, + this->sampleRate); + } + +private: + void updateCoefficients() noexcept + { + coefficients = AnalogFilterDesigner::designKorg35 (filterMode, centerFreq, this->sampleRate, resonance, drive); + saturator.setCoefficients (AnalogFilterDesigner::designSaturator (drive)); + } + + FilterModeType filterMode = FilterMode::lowpass; + CoeffType centerFreq = static_cast (1000); + CoeffType resonance = static_cast (0); + CoeffType drive = static_cast (0); + CoeffType signalRange = static_cast (1); + CoeffType inverseSignalRange = static_cast (1); + AnalogKorg35Coefficients coefficients; + std::array, 3> poles; + AnalogSaturator saturator; + + YUP_LEAK_DETECTOR (AnalogKorg35Filter) +}; + +//============================================================================== +/** + Four-pole Moog ladder-style analog-model filter. +*/ +template +class AnalogMoogLadderFilter : public FilterBase +{ +public: + AnalogMoogLadderFilter() + { + setParameters (AnalogMoogLadderMode::lowpass24, static_cast (1000), static_cast (0), static_cast (0), this->sampleRate); + } + + /** Sets all filter parameters. */ + void setParameters (AnalogMoogLadderMode newMode, CoeffType frequency, CoeffType normalizedResonance, CoeffType saturation, double sampleRate) noexcept + { + if (mode != newMode + || ! approximatelyEqual (centerFreq, frequency) + || ! approximatelyEqual (resonance, normalizedResonance) + || ! approximatelyEqual (drive, saturation) + || ! approximatelyEqual (this->sampleRate, sampleRate)) + { + mode = newMode; + centerFreq = frequency; + resonance = normalizedResonance; + drive = saturation; + this->sampleRate = sampleRate; + updateCoefficients(); + } + } + + FilterModeType getSupportedModes() const noexcept override + { + return FilterMode::lowpass | FilterMode::highpass | FilterMode::bandpassCsg; + } + + void setSignalRange (CoeffType range) noexcept + { + jassert (range > static_cast (0)); + + signalRange = jmax (range, std::numeric_limits::min()); + inverseSignalRange = static_cast (1) / signalRange; + } + + void reset() noexcept override + { + for (auto& pole : poles) + pole.reset(); + } + + void prepare (double sampleRate, int maximumBlockSize) override + { + this->sampleRate = sampleRate; + this->maximumBlockSize = maximumBlockSize; + updateCoefficients(); + reset(); + } + + SampleType processSample (SampleType inputSample) noexcept override + { + auto input = saturator.process (static_cast (static_cast (inputSample) * inverseSignalRange)); + const auto feedback = poles[0].getFeedbackOutput (coefficients.poles[0]) + + poles[1].getFeedbackOutput (coefficients.poles[1]) + + poles[2].getFeedbackOutput (coefficients.poles[2]) + + poles[3].getFeedbackOutput (coefficients.poles[3]); + auto u = (static_cast (input) - coefficients.feedback * feedback) * coefficients.alpha0; + u = clipAnalogResonance (u); + + const auto y1 = poles[0].processLowPass (u, coefficients.poles[0]); + const auto y2 = poles[1].processLowPass (y1, coefficients.poles[1]); + const auto y3 = poles[2].processLowPass (y2, coefficients.poles[2]); + const auto y4 = poles[3].processLowPass (y3, coefficients.poles[3]); + const auto output = (coefficients.outputs[0] * u + + coefficients.outputs[1] * y1 + + coefficients.outputs[2] * y2 + + coefficients.outputs[3] * y3 + + coefficients.outputs[4] * y4) + * coefficients.gainCorrection; + + return static_cast (saturator.process (static_cast (output)) * signalRange); + } + + void processBlock (const SampleType* inputBuffer, SampleType* outputBuffer, int numSamples) noexcept override + { + for (int i = 0; i < numSamples; ++i) + outputBuffer[i] = processSample (inputBuffer[i]); + } + + Complex getComplexResponse (CoeffType frequency) const override + { + return getLinearizedComplexResponse<4, CoeffType> ( + [this] (CoeffType input, std::array state) noexcept + { + LinearStepResult<4, CoeffType> result; + + const auto feedback = coefficients.poles[0].beta * state[0] + + coefficients.poles[1].beta * state[1] + + coefficients.poles[2].beta * state[2] + + coefficients.poles[3].beta * state[3]; + const auto u = (input - coefficients.feedback * feedback) * coefficients.alpha0; + const auto y1 = getAnalogOnePoleLowPassOutput (u, state[0], coefficients.poles[0]); + const auto y2 = getAnalogOnePoleLowPassOutput (y1, state[1], coefficients.poles[1]); + const auto y3 = getAnalogOnePoleLowPassOutput (y2, state[2], coefficients.poles[2]); + const auto y4 = getAnalogOnePoleLowPassOutput (y3, state[3], coefficients.poles[3]); + + result.state[0] = getAnalogOnePoleNextState (u, state[0], coefficients.poles[0]); + result.state[1] = getAnalogOnePoleNextState (y1, state[1], coefficients.poles[1]); + result.state[2] = getAnalogOnePoleNextState (y2, state[2], coefficients.poles[2]); + result.state[3] = getAnalogOnePoleNextState (y3, state[3], coefficients.poles[3]); + result.output = (coefficients.outputs[0] * u + + coefficients.outputs[1] * y1 + + coefficients.outputs[2] * y2 + + coefficients.outputs[3] * y3 + + coefficients.outputs[4] * y4) + * coefficients.gainCorrection; + + return result; + }, + frequency, + this->sampleRate); + } + +private: + void updateCoefficients() noexcept + { + coefficients = AnalogFilterDesigner::designMoogLadder (mode, centerFreq, this->sampleRate, resonance, drive); + saturator.setCoefficients (AnalogFilterDesigner::designSaturator (drive)); + } + + AnalogMoogLadderMode mode = AnalogMoogLadderMode::lowpass24; + CoeffType centerFreq = static_cast (1000); + CoeffType resonance = static_cast (0); + CoeffType drive = static_cast (0); + CoeffType signalRange = static_cast (1); + CoeffType inverseSignalRange = static_cast (1); + AnalogMoogLadderCoefficients coefficients; + std::array, 4> poles; + AnalogSaturator saturator; + + YUP_LEAK_DETECTOR (AnalogMoogLadderFilter) +}; + +//============================================================================== +/** + Roland diode-ladder-inspired four-pole low-pass filter. +*/ +template +class AnalogRolandDiodeFilter : public FilterBase +{ +public: + AnalogRolandDiodeFilter() + { + setParameters (static_cast (1000), static_cast (0), static_cast (0), this->sampleRate); + } + + /** Sets all filter parameters. */ + void setParameters (CoeffType frequency, CoeffType normalizedResonance, CoeffType saturation, double sampleRate) noexcept + { + if (! approximatelyEqual (centerFreq, frequency) + || ! approximatelyEqual (resonance, normalizedResonance) + || ! approximatelyEqual (drive, saturation) + || ! approximatelyEqual (this->sampleRate, sampleRate)) + { + centerFreq = frequency; + resonance = normalizedResonance; + drive = saturation; + this->sampleRate = sampleRate; + updateCoefficients(); + } + } + + FilterModeType getSupportedModes() const noexcept override + { + return FilterMode::lowpass; + } + + void setSignalRange (CoeffType range) noexcept + { + jassert (range > static_cast (0)); + + signalRange = jmax (range, std::numeric_limits::min()); + inverseSignalRange = static_cast (1) / signalRange; + } + + void reset() noexcept override + { + z.fill (static_cast (0)); + } + + void prepare (double sampleRate, int maximumBlockSize) override + { + this->sampleRate = sampleRate; + this->maximumBlockSize = maximumBlockSize; + updateCoefficients(); + reset(); + } + + SampleType processSample (SampleType inputSample) noexcept override + { + auto input = saturator.process (static_cast (static_cast (inputSample) * inverseSignalRange)); + const auto x = static_cast (input); + auto s0 = (z[0] * coefficients.a2 * coefficients.a + + z[1] * coefficients.a2 * coefficients.b + + z[2] * (coefficients.b2 - static_cast (2) * coefficients.a2) * coefficients.a + + z[3] * (coefficients.b2 - static_cast (3) * coefficients.a2) * coefficients.b) + * coefficients.c; + const auto s = coefficients.highpassB * s0 - z[4]; + + auto y5 = (coefficients.g * x + s) * coefficients.fg; + const auto y0 = clipAnalogResonance ((x - coefficients.feedback * y5) * static_cast (2)) * static_cast (0.5); + y5 = coefficients.g * y0 + s; + + const auto y4 = coefficients.g0 * y0 + s0; + const auto y3 = (coefficients.b * y4 - z[3]) * coefficients.aInv; + const auto y2 = (coefficients.b * y3 - coefficients.a * y4 - z[2]) * coefficients.aInv; + const auto y1 = (coefficients.b * y2 - coefficients.a * y3 - z[1]) * coefficients.aInv; + + z[0] += static_cast (4) * coefficients.a * (y0 - y1 + y2); + z[1] += static_cast (2) * coefficients.a * (y1 - static_cast (2) * y2 + y3); + z[2] += static_cast (2) * coefficients.a * (y2 - static_cast (2) * y3 + y4); + z[3] += static_cast (2) * coefficients.a * (y3 - static_cast (2) * y4); + z[4] = coefficients.highpassB * y4 + coefficients.highpassA * y5; + + const auto output = y4 * coefficients.gainCorrection; + + return static_cast (saturator.process (static_cast (output)) * signalRange); + } + + void processBlock (const SampleType* inputBuffer, SampleType* outputBuffer, int numSamples) noexcept override + { + for (int i = 0; i < numSamples; ++i) + outputBuffer[i] = processSample (inputBuffer[i]); + } + + Complex getComplexResponse (CoeffType frequency) const override + { + return getLinearizedComplexResponse<5, CoeffType> ( + [this] (CoeffType input, std::array state) noexcept + { + LinearStepResult<5, CoeffType> result; + + const auto s0 = (state[0] * coefficients.a2 * coefficients.a + + state[1] * coefficients.a2 * coefficients.b + + state[2] * (coefficients.b2 - static_cast (2) * coefficients.a2) * coefficients.a + + state[3] * (coefficients.b2 - static_cast (3) * coefficients.a2) * coefficients.b) + * coefficients.c; + const auto s = coefficients.highpassB * s0 - state[4]; + auto y5 = (coefficients.g * input + s) * coefficients.fg; + const auto y0 = input - coefficients.feedback * y5; + y5 = coefficients.g * y0 + s; + + const auto y4 = coefficients.g0 * y0 + s0; + const auto y3 = (coefficients.b * y4 - state[3]) * coefficients.aInv; + const auto y2 = (coefficients.b * y3 - coefficients.a * y4 - state[2]) * coefficients.aInv; + const auto y1 = (coefficients.b * y2 - coefficients.a * y3 - state[1]) * coefficients.aInv; + + result.state[0] = state[0] + static_cast (4) * coefficients.a * (y0 - y1 + y2); + result.state[1] = state[1] + static_cast (2) * coefficients.a * (y1 - static_cast (2) * y2 + y3); + result.state[2] = state[2] + static_cast (2) * coefficients.a * (y2 - static_cast (2) * y3 + y4); + result.state[3] = state[3] + static_cast (2) * coefficients.a * (y3 - static_cast (2) * y4); + result.state[4] = coefficients.highpassB * y4 + coefficients.highpassA * y5; + result.output = y4 * coefficients.gainCorrection; + + return result; + }, + frequency, + this->sampleRate); + } + +private: + void updateCoefficients() noexcept + { + coefficients = AnalogFilterDesigner::designRolandDiode (centerFreq, this->sampleRate, resonance, drive); + saturator.setCoefficients (AnalogFilterDesigner::designSaturator (drive)); + } + + CoeffType centerFreq = static_cast (1000); + CoeffType resonance = static_cast (0); + CoeffType drive = static_cast (0); + CoeffType signalRange = static_cast (1); + CoeffType inverseSignalRange = static_cast (1); + AnalogRolandDiodeCoefficients coefficients; + std::array z {}; + AnalogSaturator saturator; + + YUP_LEAK_DETECTOR (AnalogRolandDiodeFilter) +}; + +//============================================================================== +using AnalogTwoPoleFilterFloat = AnalogTwoPoleFilter; +using AnalogTwoPoleFilterDouble = AnalogTwoPoleFilter; +using AnalogVowelFilterFloat = AnalogVowelFilter; +using AnalogVowelFilterDouble = AnalogVowelFilter; +using AnalogKorg35FilterFloat = AnalogKorg35Filter; +using AnalogKorg35FilterDouble = AnalogKorg35Filter; +using AnalogMoogLadderFilterFloat = AnalogMoogLadderFilter; +using AnalogMoogLadderFilterDouble = AnalogMoogLadderFilter; +using AnalogRolandDiodeFilterFloat = AnalogRolandDiodeFilter; +using AnalogRolandDiodeFilterDouble = AnalogRolandDiodeFilter; + +} // namespace yup diff --git a/modules/yup_dsp/filters/yup_ButterworthFilter.h b/modules/yup_dsp/filters/yup_ButterworthFilter.h index 5ae150bda..5cc2cc8ca 100644 --- a/modules/yup_dsp/filters/yup_ButterworthFilter.h +++ b/modules/yup_dsp/filters/yup_ButterworthFilter.h @@ -240,12 +240,12 @@ class ButterworthFilter : public BiquadCascade // Use FilterDesigner to calculate coefficients const auto numSections = FilterDesigner::designButterworth ( + coefficients, filterMode, order, frequency, frequency2, - this->sampleRate, - coefficients); + this->sampleRate); // Update the biquad cascade if (numSections > 0) diff --git a/modules/yup_dsp/filters/yup_CombFilter.h b/modules/yup_dsp/filters/yup_CombFilter.h new file mode 100644 index 000000000..14b2e4148 --- /dev/null +++ b/modules/yup_dsp/filters/yup_CombFilter.h @@ -0,0 +1,406 @@ +/* + ============================================================================== + + This file is part of the YUP library. + Copyright (c) 2026 - kunitoki@gmail.com + + YUP is an open source library subject to open-source licensing. + + The code included in this file is provided under the terms of the ISC license + http://www.isc.org/downloads/software-support-policy/isc-license. Permission + to use, copy, modify, and/or distribute this software for any purpose with or + without fee is hereby granted provided that the above copyright notice and + this permission notice appear in all copies. + + YUP IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER + EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE + DISCLAIMED. + + ============================================================================== +*/ + +#pragma once + +namespace yup +{ + +//============================================================================== +/** + Fractional-delay feedback comb filter. + + The filter uses a power-of-two circular delay line, cubic Hermite interpolation + for fractional delay reads, clipped feedback resonance, and optional atan output + saturation. The delay can be controlled directly in Hertz or from a MIDI note. + + Parameter ranges are normalized for feedback and saturation: + - frequency: fundamental frequency in Hz + - feedback: 0..1, internally mapped to a stable feedback gain + - saturation: 0..1, applied at the filter output + + @tparam SampleType Type for audio samples (float or double) + @tparam CoeffType Type for internal coefficients +*/ +template +class CombFilter : public FilterBase +{ +public: + //============================================================================== + static constexpr std::size_t defaultDelayLineSize = 16384; + + //============================================================================== + /** Creates a comb filter with the default delay-line size. */ + CombFilter() + { + initialiseDelayLine(); + setParameters (static_cast (440), static_cast (0), static_cast (0), this->sampleRate); + } + + /** Creates a comb filter with a custom power-of-two delay-line size. */ + explicit CombFilter (std::size_t delayLineSize) + : delayLineSizeInSamples (nextPowerOfTwo (std::max (4, delayLineSize))) + , delayLineMask (delayLineSizeInSamples - 1) + { + initialiseDelayLine(); + setParameters (static_cast (440), static_cast (0), static_cast (0), this->sampleRate); + } + + //============================================================================== + /** + Sets all comb filter parameters. + + @param frequencyHz Fundamental frequency in Hz + @param feedback Normalized feedback amount in the range 0..1 + @param saturation Normalized output saturation amount in the range 0..1 + @param sampleRate Sample rate in Hz + */ + void setParameters ( + CoeffType frequencyHz, + CoeffType feedback, + CoeffType saturation, + double sampleRate) noexcept + { + const auto normalizedFeedback = sanitizeNormalized (feedback); + const auto normalizedSaturation = sanitizeNormalized (saturation); + const auto sanitizedSampleRate = sanitizeSampleRate (sampleRate); + const auto sanitizedFrequency = sanitizeFrequency (frequencyHz, sanitizedSampleRate); + + if (! approximatelyEqual (frequency, sanitizedFrequency) + || ! approximatelyEqual (feedbackAmount, normalizedFeedback) + || ! approximatelyEqual (saturationAmount, normalizedSaturation) + || ! approximatelyEqual (this->sampleRate, static_cast (sanitizedSampleRate))) + { + frequency = sanitizedFrequency; + feedbackAmount = normalizedFeedback; + saturationAmount = normalizedSaturation; + this->sampleRate = sanitizedSampleRate; + + updateDerivedParameters(); + } + } + + /** + Sets the comb delay from a MIDI note number. + + The conversion uses the equal-tempered A440 mapping. + */ + void setParametersFromNote ( + CoeffType midiNote, + CoeffType feedback, + CoeffType saturation, + double sampleRate) noexcept + { + const auto frequencyHz = static_cast (440) + * std::pow (static_cast (2), (midiNote - static_cast (69)) / static_cast (12)); + setParameters (frequencyHz, feedback, saturation, sampleRate); + } + + /** Sets the expected signal range used before and after saturation. */ + void setSignalRange (CoeffType range) noexcept + { + jassert (range > static_cast (0)); + + signalRange = jmax (range, std::numeric_limits::min()); + inverseSignalRange = static_cast (1) / signalRange; + } + + /** Returns the expected signal range. */ + CoeffType getSignalRange() const noexcept + { + return signalRange; + } + + /** Returns the comb fundamental frequency in Hz. */ + CoeffType getFrequency() const noexcept + { + return frequency; + } + + /** Returns the normalized feedback amount. */ + CoeffType getFeedback() const noexcept + { + return feedbackAmount; + } + + /** Returns the normalized saturation amount. */ + CoeffType getSaturation() const noexcept + { + return saturationAmount; + } + + /** Returns the current target delay in samples. */ + CoeffType getDelayInSamples() const noexcept + { + return targetDelaySamples; + } + + //============================================================================== + FilterModeType getSupportedModes() const noexcept override + { + return FilterMode::peak; + } + + void reset() noexcept override + { + std::fill (delayLine.begin(), delayLine.end(), static_cast (0)); + writeIndex = 0; + currentDelaySamples = targetDelaySamples; + } + + void prepare (double sampleRate, int maximumBlockSize) override + { + this->sampleRate = sanitizeSampleRate (sampleRate); + this->maximumBlockSize = maximumBlockSize; + + initialiseDelayLine(); + updateDerivedParameters(); + reset(); + } + + SampleType processSample (SampleType inputSample) noexcept override + { + rampDelay(); + + const auto input = static_cast (inputSample) * inverseSignalRange; + const auto delayedSignal = readDelayedSample (currentDelaySamples); + const auto delayLineInput = input + clipResonance (delayLineFeedback * delayedSignal); + + delayLine[writeIndex] = static_cast (delayLineInput); + writeIndex = (writeIndex + 1) & delayLineMask; + + const auto output = saturateOutput (input + delayedSignal * static_cast (0.5)); + + return static_cast (output * signalRange); + } + + void processBlock (const SampleType* inputBuffer, SampleType* outputBuffer, int numSamples) noexcept override + { + if (inputBuffer == nullptr || outputBuffer == nullptr) + return; + + for (int i = 0; i < numSamples; ++i) + outputBuffer[i] = processSample (inputBuffer[i]); + } + + /** + Returns the linearized comb response at the given frequency. + + Saturation and feedback clipping are nonlinear and intentionally omitted + from this transfer-function estimate. + */ + Complex getComplexResponse (CoeffType responseFrequency) const override + { + const auto delayed = getFractionalDelayResponse (responseFrequency); + const auto denominator = Complex (static_cast (1)) - delayLineFeedback * delayed; + + if (std::abs (denominator) <= std::numeric_limits::epsilon()) + return Complex (static_cast (1)) + + (static_cast (0.5) * delayed) + / (denominator + Complex (std::numeric_limits::epsilon())); + + return Complex (static_cast (1)) + + (static_cast (0.5) * delayed) / denominator; + } + +private: + //============================================================================== + static constexpr CoeffType delayRampStep = static_cast (4); + + static std::size_t nextPowerOfTwo (std::size_t value) noexcept + { + --value; + + for (std::size_t i = 1; i < sizeof (std::size_t) * 8; i <<= 1) + value |= value >> i; + + return value + 1; + } + + static CoeffType sanitizeSampleRate (double sampleRate) noexcept + { + return jmax (static_cast (11025), static_cast (sampleRate)); + } + + static CoeffType sanitizeNormalized (CoeffType value) noexcept + { + return jlimit (static_cast (0), static_cast (1), value); + } + + CoeffType sanitizeFrequency (CoeffType frequencyHz, CoeffType sampleRate) const noexcept + { + const auto minFrequency = sampleRate / static_cast (delayLineSizeInSamples - 1); + const auto maxFrequency = sampleRate * static_cast (0.45); + + return jlimit (minFrequency, maxFrequency, frequencyHz); + } + + static CoeffType cubicHermite ( + CoeffType y0, + CoeffType y1, + CoeffType y2, + CoeffType y3, + CoeffType fraction) noexcept + { + const auto c0 = y1; + const auto c1 = static_cast (0.5) * (y2 - y0); + const auto c2 = y0 - static_cast (2.5) * y1 + static_cast (2) * y2 - static_cast (0.5) * y3; + const auto c3 = static_cast (0.5) * (y3 - y0) + static_cast (1.5) * (y1 - y2); + + return ((c3 * fraction + c2) * fraction + c1) * fraction + c0; + } + + static CoeffType fastAtan (CoeffType input) noexcept + { + static constexpr auto b = static_cast (0.596227); + + const auto sign = input < static_cast (0) ? static_cast (-1) : static_cast (1); + const auto bx = std::abs (b * input); + const auto numerator = bx + input * input; + const auto atanFirstQuadrant = numerator / (static_cast (1) + bx + numerator); + + return sign * atanFirstQuadrant; + } + + static CoeffType clipResonance (CoeffType input) noexcept + { + return std::tanh (input * static_cast (0.25)) * static_cast (4); + } + + void initialiseDelayLine() + { + if (delayLine.size() != delayLineSizeInSamples) + delayLine.assign (delayLineSizeInSamples, static_cast (0)); + } + + void updateDerivedParameters() noexcept + { + targetDelaySamples = jlimit ( + static_cast (1), + static_cast (delayLineSizeInSamples - 1), + static_cast (this->sampleRate) / frequency); + delayLineFeedback = jmin (static_cast (0.985), std::sqrt (feedbackAmount)); + + if (saturationAmount > static_cast (0)) + { + saturationPreScale = square (saturationAmount * static_cast (5)); + saturationPostScale = static_cast (1) / fastAtan (saturationPreScale) + * std::pow (static_cast (4), -saturationAmount); + } + else + { + saturationPreScale = static_cast (1); + saturationPostScale = static_cast (1); + } + + if (currentDelaySamples <= static_cast (0)) + currentDelaySamples = targetDelaySamples; + } + + void rampDelay() noexcept + { + const auto delta = targetDelaySamples - currentDelaySamples; + + if (std::abs (delta) <= delayRampStep) + currentDelaySamples = targetDelaySamples; + else + currentDelaySamples += delta > static_cast (0) ? delayRampStep : -delayRampStep; + } + + CoeffType readDelayedSample (CoeffType delaySamples) const noexcept + { + const auto readPosition = static_cast (writeIndex) + - delaySamples + + static_cast (delayLineSizeInSamples); + const auto readIndex = static_cast (std::floor (readPosition)) & delayLineMask; + const auto fraction = readPosition - std::floor (readPosition); + + const auto y0 = static_cast (delayLine[(readIndex - 1) & delayLineMask]); + const auto y1 = static_cast (delayLine[readIndex]); + const auto y2 = static_cast (delayLine[(readIndex + 1) & delayLineMask]); + const auto y3 = static_cast (delayLine[(readIndex + 2) & delayLineMask]); + + return cubicHermite (y0, y1, y2, y3, fraction); + } + + CoeffType saturateOutput (CoeffType input) const noexcept + { + if (saturationAmount <= static_cast (0)) + return input; + + return fastAtan (saturationPreScale * input) * saturationPostScale; + } + + CoeffType getDelayForResponse() const noexcept + { + return targetDelaySamples > static_cast (0) + ? targetDelaySamples + : jlimit ( + static_cast (1), + static_cast (delayLineSizeInSamples - 1), + static_cast (this->sampleRate) / frequency); + } + + Complex getFractionalDelayResponse (CoeffType responseFrequency) const noexcept + { + const auto delay = getDelayForResponse(); + const auto readDelay = static_cast (std::ceil (delay)); + const auto fraction = readDelay - delay; + const auto fraction2 = fraction * fraction; + const auto fraction3 = fraction2 * fraction; + + const auto weight0 = static_cast (-0.5) * fraction + fraction2 - static_cast (0.5) * fraction3; + const auto weight1 = static_cast (1) - static_cast (2.5) * fraction2 + static_cast (1.5) * fraction3; + const auto weight2 = static_cast (0.5) * fraction + static_cast (2) * fraction2 - static_cast (1.5) * fraction3; + const auto weight3 = static_cast (-0.5) * fraction2 + static_cast (0.5) * fraction3; + const auto omega = frequencyToAngular (responseFrequency, static_cast (this->sampleRate)); + + return weight0 * polar (static_cast (1), -omega * (readDelay + static_cast (1))) + + weight1 * polar (static_cast (1), -omega * readDelay) + + weight2 * polar (static_cast (1), -omega * (readDelay - static_cast (1))) + + weight3 * polar (static_cast (1), -omega * (readDelay - static_cast (2))); + } + + //============================================================================== + std::size_t delayLineSizeInSamples = defaultDelayLineSize; + std::size_t delayLineMask = defaultDelayLineSize - 1; + std::vector delayLine; + std::size_t writeIndex = 0; + + CoeffType frequency = static_cast (440); + CoeffType feedbackAmount = static_cast (0); + CoeffType saturationAmount = static_cast (0); + CoeffType signalRange = static_cast (1); + CoeffType inverseSignalRange = static_cast (1); + CoeffType delayLineFeedback = static_cast (0); + CoeffType currentDelaySamples = static_cast (0); + CoeffType targetDelaySamples = static_cast (0); + CoeffType saturationPreScale = static_cast (1); + CoeffType saturationPostScale = static_cast (1); + + YUP_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (CombFilter) +}; + +//============================================================================== +using CombFilterFloat = CombFilter; +using CombFilterDouble = CombFilter; + +} // namespace yup diff --git a/modules/yup_dsp/filters/yup_DirectFIR.h b/modules/yup_dsp/filters/yup_DirectFIR.h index b637714c8..9ea85f314 100644 --- a/modules/yup_dsp/filters/yup_DirectFIR.h +++ b/modules/yup_dsp/filters/yup_DirectFIR.h @@ -45,14 +45,15 @@ namespace yup DirectFIR fir; // Set filter coefficients (e.g., lowpass filter) - auto coeffs = FilterDesigner::designFIRLowpass(64, 1000.0f, 44100.0); - fir.setCoefficients(coeffs); + std::vector coeffs; + FilterDesigner::designFIRLowpass (coeffs, 64, 1000.0f, 44100.0); + fir.setCoefficients (coeffs); // Prepare for processing - fir.prepare(44100.0, 512); + fir.prepare (44100.0, 512); // In audio callback: - fir.processBlock(inputBuffer, outputBuffer, numSamples); + fir.processBlock (inputBuffer, outputBuffer, numSamples); @endcode @tparam SampleType Type for audio samples (float or double) diff --git a/modules/yup_dsp/filters/yup_LinkwitzRileyFilter.h b/modules/yup_dsp/filters/yup_LinkwitzRileyFilter.h index 4e408d239..e3e77e0e9 100644 --- a/modules/yup_dsp/filters/yup_LinkwitzRileyFilter.h +++ b/modules/yup_dsp/filters/yup_LinkwitzRileyFilter.h @@ -269,7 +269,7 @@ class LinkwitzRileyFilter return; // Use FilterDesigner to calculate Linkwitz-Riley coefficients - const int numSections = FilterDesigner::designLinkwitzRiley (Order, frequency, sampleRate, lowCoeffs, highCoeffs); + const int numSections = FilterDesigner::designLinkwitzRiley (lowCoeffs, highCoeffs, Order, frequency, sampleRate); if (numSections != numStages * 2) return; diff --git a/modules/yup_dsp/utilities/yup_DspMath.h b/modules/yup_dsp/utilities/yup_DspMath.h index c058bbc2c..27dce5e93 100644 --- a/modules/yup_dsp/utilities/yup_DspMath.h +++ b/modules/yup_dsp/utilities/yup_DspMath.h @@ -170,9 +170,6 @@ void extractPolesZerosFromSecondOrderBiquad (FloatType b0, FloatType b1, FloatTy { const auto epsilon = static_cast (1e-12); - // Calculate poles from denominator: 1 + a1*z^-1 + a2*z^-2 = 0 - // Multiplying by z^2: z^2 + a1*z + a2 = 0 - // Using quadratic formula: z = (-a1 ± √(a1² - 4*a2)) / 2 if (std::abs (a2) > epsilon) { auto discriminant = a1 * a1 - 4 * a2; @@ -194,26 +191,20 @@ void extractPolesZerosFromSecondOrderBiquad (FloatType b0, FloatType b1, FloatTy } else if (std::abs (a1) > epsilon) { - // First-order: 1 + a1*z^-1 = 0 -> z = -1/a1 poles.push_back (Complex (-1 / a1, 0)); } - // Calculate zeros from numerator: b0 + b1*z^-1 + b2*z^-2 = 0 - // Multiplying by z^2: b0*z^2 + b1*z + b2 = 0 - // Using quadratic formula: z = (-b1 ± √(b1² - 4*b0*b2)) / (2*b0) if (std::abs (b0) > epsilon && std::abs (b2) > epsilon) { auto discriminant = b1 * b1 - 4 * b0 * b2; if (discriminant >= 0) { - // Real zeros auto sqrtDisc = std::sqrt (discriminant); zeros.push_back (Complex ((-b1 + sqrtDisc) / (2 * b0), 0)); zeros.push_back (Complex ((-b1 - sqrtDisc) / (2 * b0), 0)); } else { - // Complex conjugate zeros auto real = -b1 / (2 * b0); auto imag = std::sqrt (-discriminant) / (2 * b0); zeros.push_back (Complex (real, imag)); @@ -222,49 +213,31 @@ void extractPolesZerosFromSecondOrderBiquad (FloatType b0, FloatType b1, FloatTy } else if (std::abs (b1) > epsilon && std::abs (b0) > epsilon) { - // First-order: b0 + b1*z^-1 = 0 -> z = -b0/b1 zeros.push_back (Complex (-b0 / b1, 0)); } else if (std::abs (b2) > epsilon) { - // Zero at origin (b0 = 0): b1*z^-1 + b2*z^-2 = 0 -> z*(b1 + b2*z^-1) = 0 - // One zero at z = 0, another at z = -b1/b2 zeros.push_back (Complex (0, 0)); if (std::abs (b1) > epsilon) zeros.push_back (Complex (-b1 / b2, 0)); } } +//============================================================================== + /** Extract poles and zeros from fourth-order section coefficients */ template void extractPolesZerosFromFourthOrderBiquad (FloatType b0, FloatType b1, FloatType b2, FloatType b3, FloatType b4, FloatType a0, FloatType a1, FloatType a2, FloatType a3, FloatType a4, ComplexVector& poles, ComplexVector& zeros) { - // For fourth-order polynomials, we can try to factor them into quadratic pairs - // This is a simplified approach - for full accuracy, a robust polynomial root finder would be needed - - // First, try to factor the denominator polynomial (poles) - // a4*z^4 + a3*z^3 + a2*z^2 + a1*z + a0 = 0 - - // For Butterworth filters designed using our method, we can often decompose this way: - // Split into two biquads with shared characteristics - - // Simple approach: assume it can be factored as (z^2 + p1*z + q1)(z^2 + p2*z + q2) - const auto epsilon = static_cast (1e-12); if (std::abs (a4) > epsilon) { - // Attempt to find characteristic polynomial roots - // This is a simplified extraction - in practice, you'd want a full polynomial solver - - // Try to extract first biquad-like section auto a1_norm = a1 / a4; auto a2_norm = a2 / a4; auto a3_norm = a3 / a4; auto a0_norm = a0 / a4; - // Use approximation method for 4th order Butterworth characteristics - // Extract two approximate biquad sections auto q1 = std::sqrt (std::abs (a0_norm)); auto p1 = a1_norm / 2; @@ -286,7 +259,6 @@ void extractPolesZerosFromFourthOrderBiquad (FloatType b0, FloatType b1, FloatTy } } - // Second pair (approximation) auto p2 = a3_norm / 2; auto q2 = a2_norm - q1; @@ -309,7 +281,6 @@ void extractPolesZerosFromFourthOrderBiquad (FloatType b0, FloatType b1, FloatTy } } - // Similar approach for zeros (numerator polynomial) if (std::abs (b4) > epsilon) { auto b1_norm = b1 / b4; @@ -361,4 +332,114 @@ void extractPolesZerosFromFourthOrderBiquad (FloatType b0, FloatType b1, FloatTy } } +//============================================================================== + +template +struct LinearStepResult +{ + CoeffType output = static_cast (0); + std::array state {}; +}; + +template +Complex getLinearizedComplexResponse ( + StepFunction&& stepFunction, + CoeffType frequency, + double sampleRate) noexcept +{ + using ComplexType = Complex; + using State = std::array; + + const auto zeroStep = stepFunction (static_cast (0), State {}); + const auto inputStep = stepFunction (static_cast (1), State {}); + + std::array, numStates> stateMatrix {}; + std::array inputVector {}; + std::array outputVector {}; + + for (std::size_t row = 0; row < numStates; ++row) + inputVector[row] = inputStep.state[row] - zeroStep.state[row]; + + const auto directGain = inputStep.output - zeroStep.output; + + for (std::size_t column = 0; column < numStates; ++column) + { + State basis {}; + basis[column] = static_cast (1); + + const auto basisStep = stepFunction (static_cast (0), basis); + + for (std::size_t row = 0; row < numStates; ++row) + stateMatrix[row][column] = basisStep.state[row] - zeroStep.state[row]; + + outputVector[column] = basisStep.output - zeroStep.output; + } + + const auto z = polar (static_cast (1), frequencyToAngular (frequency, static_cast (sampleRate))); + std::array, numStates> matrix {}; + std::array vector {}; + + for (std::size_t row = 0; row < numStates; ++row) + { + for (std::size_t column = 0; column < numStates; ++column) + matrix[row][column] = (row == column ? z : ComplexType {}) - stateMatrix[row][column]; + + vector[row] = inputVector[row]; + } + + for (std::size_t pivot = 0; pivot < numStates; ++pivot) + { + auto pivotRow = pivot; + auto pivotMagnitude = std::abs (matrix[pivot][pivot]); + + for (std::size_t row = pivot + 1; row < numStates; ++row) + { + const auto rowMagnitude = std::abs (matrix[row][pivot]); + if (rowMagnitude > pivotMagnitude) + { + pivotMagnitude = rowMagnitude; + pivotRow = row; + } + } + + if (pivotRow != pivot) + { + std::swap (matrix[pivot], matrix[pivotRow]); + std::swap (vector[pivot], vector[pivotRow]); + } + + if (std::abs (matrix[pivot][pivot]) <= std::numeric_limits::epsilon()) + matrix[pivot][pivot] += ComplexType (std::numeric_limits::epsilon()); + + const auto pivotValue = matrix[pivot][pivot]; + + for (std::size_t column = pivot; column < numStates; ++column) + matrix[pivot][column] /= pivotValue; + + vector[pivot] /= pivotValue; + + for (std::size_t row = 0; row < numStates; ++row) + { + if (row == pivot) + continue; + + const auto scale = matrix[row][pivot]; + if (std::abs (scale) <= std::numeric_limits::epsilon()) + continue; + + for (std::size_t column = pivot; column < numStates; ++column) + matrix[row][column] -= scale * matrix[pivot][column]; + + vector[row] -= scale * vector[pivot]; + } + } + + auto response = ComplexType (directGain); + + for (std::size_t column = 0; column < numStates; ++column) + response += outputVector[column] * vector[column]; + + return response; +} + } // namespace yup diff --git a/modules/yup_dsp/yup_dsp.cpp b/modules/yup_dsp/yup_dsp.cpp index 7a3968d74..a888e7c3a 100644 --- a/modules/yup_dsp/yup_dsp.cpp +++ b/modules/yup_dsp/yup_dsp.cpp @@ -85,6 +85,7 @@ #include "metering/yup_LoudnessFilter.cpp" #include "metering/yup_KMeterState.cpp" #include "designers/yup_FilterDesigner.cpp" +#include "designers/yup_AnalogFilterDesigner.cpp" #include "convolution/yup_PartitionedConvolver.cpp" #include "stretching/yup_TimeStretchProcessorEngine.h" #include "stretching/yup_TimeStretchTimeDomainBackend.h" diff --git a/modules/yup_dsp/yup_dsp.h b/modules/yup_dsp/yup_dsp.h index 1ccd00033..558db5f3f 100644 --- a/modules/yup_dsp/yup_dsp.h +++ b/modules/yup_dsp/yup_dsp.h @@ -144,11 +144,14 @@ #include "base/yup_FilterBase.h" #include "base/yup_FilterCharacteristics.h" #include "base/yup_FirstOrderCoefficients.h" -#include "base/yup_BiquadCoefficients.h" -#include "base/yup_StateVariableCoefficients.h" #include "base/yup_FirstOrder.h" +#include "base/yup_BiquadCoefficients.h" #include "base/yup_Biquad.h" #include "base/yup_BiquadCascade.h" +#include "base/yup_AnalogFilterCoefficients.h" +#include "base/yup_AnalogSaturator.h" +#include "base/yup_AnalogPoles.h" +#include "base/yup_StateVariableCoefficients.h" // Metering and level measurement (after Biquad definition) #include "metering/yup_LevelProcessor.h" @@ -157,6 +160,7 @@ // Filter designers and coefficient calculators #include "designers/yup_FilterDesigner.h" +#include "designers/yup_AnalogFilterDesigner.h" // Filter implementations #include "filters/yup_FirstOrderFilter.h" @@ -167,6 +171,8 @@ #include "filters/yup_ButterworthFilter.h" #include "filters/yup_LinkwitzRileyFilter.h" #include "filters/yup_DirectFIR.h" +#include "filters/yup_AnalogFilters.h" +#include "filters/yup_CombFilter.h" // Dynamics processors #include "dynamics/yup_SoftClipper.h" diff --git a/tests/yup_dsp.cpp b/tests/yup_dsp.cpp index 4bcd9b402..94b04f724 100644 --- a/tests/yup_dsp.cpp +++ b/tests/yup_dsp.cpp @@ -20,8 +20,10 @@ */ #include "yup_dsp/yup_BiquadCascade.cpp" +#include "yup_dsp/yup_AnalogFilters.cpp" #include "yup_dsp/yup_BiquadFilter.cpp" #include "yup_dsp/yup_ButterworthFilter.cpp" +#include "yup_dsp/yup_CombFilter.cpp" #include "yup_dsp/yup_CircularBuffer.cpp" #include "yup_dsp/yup_DirectFIR.cpp" #include "yup_dsp/yup_FFTProcessor.cpp" diff --git a/tests/yup_dsp/yup_AnalogFilters.cpp b/tests/yup_dsp/yup_AnalogFilters.cpp new file mode 100644 index 000000000..420ff218a --- /dev/null +++ b/tests/yup_dsp/yup_AnalogFilters.cpp @@ -0,0 +1,333 @@ +/* + ============================================================================== + + This file is part of the YUP library. + Copyright (c) 2026 - kunitoki@gmail.com + + YUP is an open source library subject to open-source licensing. + + The code included in this file is provided under the terms of the ISC license + http://www.isc.org/downloads/software-support-policy/isc-license. Permission + to use, copy, modify, and/or distribute this software for any purpose with or + without fee is hereby granted provided that the above copyright notice and + this permission notice appear in all copies. + + YUP IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER + EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE + DISCLAIMED. + + ============================================================================== +*/ + +#include + +#include + +#include + +using namespace yup; + +namespace +{ +constexpr double analogSampleRate = 44100.0; +constexpr int analogBlockSize = 128; +constexpr std::array analogResponseFrequencies { 0.0, 100.0, 1000.0, 10000.0 }; + +template +std::vector makeAnalogInput() +{ + std::vector input (analogBlockSize); + + for (int i = 0; i < analogBlockSize; ++i) + input[static_cast (i)] = static_cast ((i % 17) / 17.0 - 0.5); + + return input; +} + +template +void expectFiniteAnalogBuffer (const std::vector& buffer) +{ + for (auto sample : buffer) + EXPECT_TRUE (std::isfinite (sample)); +} + +template +void expectFiniteAnalogResponse (const Complex& response) +{ + EXPECT_TRUE (std::isfinite (response.real())); + EXPECT_TRUE (std::isfinite (response.imag())); + EXPECT_TRUE (std::isfinite (std::abs (response))); +} + +template +void expectFiniteAnalogResponses (const FilterType& filter) +{ + for (auto frequency : analogResponseFrequencies) + expectFiniteAnalogResponse (filter.getComplexResponse (frequency)); +} +} // namespace + +//============================================================================== + +TEST (AnalogFilterDesignerTests, DesignsFiniteTwoPoleCoefficients) +{ + const auto lowpass = AnalogFilterDesigner::designTwoPole (FilterMode::lowpass, 1000.0, analogSampleRate, 0.5); + const auto bandpass = AnalogFilterDesigner::designTwoPole (FilterMode::bandpassCpg, 1000.0, analogSampleRate, 0.5); + const auto peak = AnalogFilterDesigner::designTwoPole (FilterMode::peak, 1000.0, analogSampleRate, 0.5); + + EXPECT_TRUE (std::isfinite (lowpass.g)); + EXPECT_TRUE (std::isfinite (lowpass.h)); + EXPECT_TRUE (std::isfinite (bandpass.r2)); + EXPECT_TRUE (std::isfinite (peak.gainCorrection)); +} + +TEST (AnalogFilterDesignerTests, DesignsFiniteLadderCoefficients) +{ + const auto korg = AnalogFilterDesigner::designKorg35 (FilterMode::lowpass, 1200.0, analogSampleRate, 0.7, 0.25); + const auto moog = AnalogFilterDesigner::designMoogLadder (AnalogMoogLadderMode::lowpass24, 1200.0, analogSampleRate, 0.7, 0.25); + const auto diode = AnalogFilterDesigner::designRolandDiode (1200.0, analogSampleRate, 0.7, 0.25); + + EXPECT_TRUE (std::isfinite (korg.alpha0)); + EXPECT_TRUE (std::isfinite (korg.feedback)); + EXPECT_TRUE (std::isfinite (moog.alpha0)); + EXPECT_TRUE (std::isfinite (moog.feedback)); + EXPECT_TRUE (std::isfinite (diode.fg)); + EXPECT_TRUE (std::isfinite (diode.g0)); +} + +TEST (AnalogFilterDesignerTests, DesignsVowelFormants) +{ + const auto vowel = AnalogFilterDesigner::designVowel (0.35, analogSampleRate, 0.8); + + EXPECT_TRUE (std::isfinite (vowel.gainCompensation)); + + for (const auto& formant : vowel.formants) + { + EXPECT_TRUE (std::isfinite (formant.g)); + EXPECT_TRUE (std::isfinite (formant.h)); + EXPECT_GT (formant.gainCorrection, 0.0); + } +} + +//============================================================================== + +TEST (AnalogFilterTests, TwoPoleProcessesFiniteOutput) +{ + AnalogTwoPoleFilter filter; + filter.prepare (analogSampleRate, analogBlockSize); + filter.setParameters (FilterMode::bandpassCpg, 1000.0f, 0.75f, 0.2f, analogSampleRate); + + const auto input = makeAnalogInput(); + std::vector output (analogBlockSize); + + filter.processBlock (input.data(), output.data(), analogBlockSize); + + expectFiniteAnalogBuffer (output); +} + +TEST (AnalogFilterTests, TwoPoleComplexResponseCoversSupportedModes) +{ + for (auto mode : { FilterMode::lowpass, FilterMode::highpass, FilterMode::bandpassCsg, FilterMode::bandpassCpg, FilterMode::bandstop, FilterMode::peak }) + { + AnalogTwoPoleFilter filter; + filter.prepare (analogSampleRate, analogBlockSize); + filter.setParameters (mode, 1000.0, 0.65, 0.0, analogSampleRate); + + expectFiniteAnalogResponses (filter); + } +} + +TEST (AnalogFilterTests, TwoPoleComplexResponseMatchesModeShape) +{ + AnalogTwoPoleFilter lowpass; + lowpass.prepare (analogSampleRate, analogBlockSize); + lowpass.setParameters (FilterMode::lowpass, 1000.0, 0.35, 0.0, analogSampleRate); + EXPECT_GT (std::abs (lowpass.getComplexResponse (100.0)), std::abs (lowpass.getComplexResponse (10000.0))); + + AnalogTwoPoleFilter highpass; + highpass.prepare (analogSampleRate, analogBlockSize); + highpass.setParameters (FilterMode::highpass, 1000.0, 0.35, 0.0, analogSampleRate); + EXPECT_LT (std::abs (highpass.getComplexResponse (100.0)), std::abs (highpass.getComplexResponse (10000.0))); + + AnalogTwoPoleFilter bandpass; + bandpass.prepare (analogSampleRate, analogBlockSize); + bandpass.setParameters (FilterMode::bandpassCpg, 1000.0, 0.75, 0.0, analogSampleRate); + + const auto centerResponse = std::abs (bandpass.getComplexResponse (1000.0)); + EXPECT_GT (centerResponse, std::abs (bandpass.getComplexResponse (100.0))); + EXPECT_GT (centerResponse, std::abs (bandpass.getComplexResponse (10000.0))); +} + +TEST (AnalogFilterTests, VowelProcessesFiniteOutput) +{ + AnalogVowelFilter filter; + filter.prepare (analogSampleRate, analogBlockSize); + filter.setParameters (0.5f, 0.75f, 0.2f, analogSampleRate); + + const auto input = makeAnalogInput(); + std::vector output (analogBlockSize); + + filter.processBlock (input.data(), output.data(), analogBlockSize); + + expectFiniteAnalogBuffer (output); +} + +TEST (AnalogFilterTests, VowelComplexResponseIsFinite) +{ + AnalogVowelFilter filter; + filter.prepare (analogSampleRate, analogBlockSize); + filter.setParameters (0.5, 0.75, 0.0, analogSampleRate); + + expectFiniteAnalogResponses (filter); +} + +TEST (AnalogFilterTests, Korg35ProcessesAllModes) +{ + const auto input = makeAnalogInput(); + + for (auto mode : { FilterMode::lowpass, FilterMode::bandpassCsg, FilterMode::highpass }) + { + AnalogKorg35Filter filter; + filter.prepare (analogSampleRate, analogBlockSize); + filter.setParameters (mode, 1000.0f, 0.6f, 0.2f, analogSampleRate); + + std::vector output (analogBlockSize); + filter.processBlock (input.data(), output.data(), analogBlockSize); + + expectFiniteAnalogBuffer (output); + } +} + +TEST (AnalogFilterTests, Korg35ComplexResponseCoversAllModes) +{ + for (auto mode : { FilterMode::lowpass, FilterMode::bandpassCsg, FilterMode::highpass }) + { + AnalogKorg35Filter filter; + filter.prepare (analogSampleRate, analogBlockSize); + filter.setParameters (mode, 1000.0, 0.55, 0.0, analogSampleRate); + + expectFiniteAnalogResponses (filter); + } +} + +TEST (AnalogFilterTests, Korg35ComplexResponseMatchesModeShape) +{ + AnalogKorg35Filter lowpass; + lowpass.prepare (analogSampleRate, analogBlockSize); + lowpass.setParameters (FilterMode::lowpass, 1000.0, 0.4, 0.0, analogSampleRate); + EXPECT_GT (std::abs (lowpass.getComplexResponse (100.0)), std::abs (lowpass.getComplexResponse (10000.0))); + + AnalogKorg35Filter highpass; + highpass.prepare (analogSampleRate, analogBlockSize); + highpass.setParameters (FilterMode::highpass, 1000.0, 0.4, 0.0, analogSampleRate); + EXPECT_LT (std::abs (highpass.getComplexResponse (100.0)), std::abs (highpass.getComplexResponse (10000.0))); + + AnalogKorg35Filter bandpass; + bandpass.prepare (analogSampleRate, analogBlockSize); + bandpass.setParameters (FilterMode::bandpassCsg, 1000.0, 0.55, 0.0, analogSampleRate); + + const auto centerResponse = std::abs (bandpass.getComplexResponse (1000.0)); + EXPECT_GT (centerResponse, std::abs (bandpass.getComplexResponse (100.0))); + EXPECT_GT (centerResponse, std::abs (bandpass.getComplexResponse (10000.0))); +} + +TEST (AnalogFilterTests, MoogLadderProcessesRepresentativeModes) +{ + const auto input = makeAnalogInput(); + + for (auto mode : { AnalogMoogLadderMode::lowpass24, AnalogMoogLadderMode::highpass12, AnalogMoogLadderMode::bandpass6 }) + { + AnalogMoogLadderFilter filter; + filter.prepare (analogSampleRate, analogBlockSize); + filter.setParameters (mode, 1000.0f, 0.6f, 0.2f, analogSampleRate); + + std::vector output (analogBlockSize); + filter.processBlock (input.data(), output.data(), analogBlockSize); + + expectFiniteAnalogBuffer (output); + } +} + +TEST (AnalogFilterTests, MoogLadderComplexResponseCoversAllModes) +{ + for (auto mode : { AnalogMoogLadderMode::lowpass24, + AnalogMoogLadderMode::highpass24, + AnalogMoogLadderMode::lowpass18, + AnalogMoogLadderMode::highpass18, + AnalogMoogLadderMode::lowpass12, + AnalogMoogLadderMode::highpass12, + AnalogMoogLadderMode::lowpass6, + AnalogMoogLadderMode::highpass6, + AnalogMoogLadderMode::bandpass12, + AnalogMoogLadderMode::bandpass6 }) + { + AnalogMoogLadderFilter filter; + filter.prepare (analogSampleRate, analogBlockSize); + filter.setParameters (mode, 1000.0, 0.55, 0.0, analogSampleRate); + + expectFiniteAnalogResponses (filter); + } +} + +TEST (AnalogFilterTests, MoogLadderComplexResponseMatchesModeShape) +{ + AnalogMoogLadderFilter lowpass; + lowpass.prepare (analogSampleRate, analogBlockSize); + lowpass.setParameters (AnalogMoogLadderMode::lowpass24, 1000.0, 0.4, 0.0, analogSampleRate); + EXPECT_GT (std::abs (lowpass.getComplexResponse (100.0)), std::abs (lowpass.getComplexResponse (10000.0))); + + AnalogMoogLadderFilter highpass; + highpass.prepare (analogSampleRate, analogBlockSize); + highpass.setParameters (AnalogMoogLadderMode::highpass24, 1000.0, 0.4, 0.0, analogSampleRate); + EXPECT_LT (std::abs (highpass.getComplexResponse (100.0)), std::abs (highpass.getComplexResponse (10000.0))); + + AnalogMoogLadderFilter bandpass; + bandpass.prepare (analogSampleRate, analogBlockSize); + bandpass.setParameters (AnalogMoogLadderMode::bandpass12, 1000.0, 0.55, 0.0, analogSampleRate); + + const auto centerResponse = std::abs (bandpass.getComplexResponse (1000.0)); + EXPECT_GT (centerResponse, std::abs (bandpass.getComplexResponse (100.0))); + EXPECT_GT (centerResponse, std::abs (bandpass.getComplexResponse (10000.0))); +} + +TEST (AnalogFilterTests, RolandDiodeProcessesFiniteOutput) +{ + AnalogRolandDiodeFilter filter; + filter.prepare (analogSampleRate, analogBlockSize); + filter.setParameters (1000.0f, 0.6f, 0.2f, analogSampleRate); + + const auto input = makeAnalogInput(); + std::vector output (analogBlockSize); + + filter.processBlock (input.data(), output.data(), analogBlockSize); + + expectFiniteAnalogBuffer (output); +} + +TEST (AnalogFilterTests, RolandDiodeComplexResponseIsFiniteAndLowpass) +{ + AnalogRolandDiodeFilter filter; + filter.prepare (analogSampleRate, analogBlockSize); + filter.setParameters (1000.0, 0.45, 0.0, analogSampleRate); + + expectFiniteAnalogResponses (filter); + EXPECT_GT (std::abs (filter.getComplexResponse (100.0)), std::abs (filter.getComplexResponse (10000.0))); +} + +TEST (AnalogFilterTests, ResetRestoresDeterministicState) +{ + AnalogMoogLadderFilter filter; + filter.prepare (analogSampleRate, analogBlockSize); + filter.setParameters (AnalogMoogLadderMode::lowpass24, 1000.0f, 0.5f, 0.1f, analogSampleRate); + + const auto input = makeAnalogInput(); + std::vector firstOutput (analogBlockSize); + std::vector secondOutput (analogBlockSize); + + filter.processBlock (input.data(), firstOutput.data(), analogBlockSize); + filter.reset(); + filter.processBlock (input.data(), secondOutput.data(), analogBlockSize); + + for (int i = 0; i < analogBlockSize; ++i) + EXPECT_FLOAT_EQ (firstOutput[static_cast (i)], secondOutput[static_cast (i)]); +} diff --git a/tests/yup_dsp/yup_CombFilter.cpp b/tests/yup_dsp/yup_CombFilter.cpp new file mode 100644 index 000000000..32586dbb9 --- /dev/null +++ b/tests/yup_dsp/yup_CombFilter.cpp @@ -0,0 +1,145 @@ +/* + ============================================================================== + + This file is part of the YUP library. + Copyright (c) 2026 - kunitoki@gmail.com + + YUP is an open source library subject to open-source licensing. + + The code included in this file is provided under the terms of the ISC license + http://www.isc.org/downloads/software-support-policy/isc-license. Permission + to use, copy, modify, and/or distribute this software for any purpose with or + without fee is hereby granted provided that the above copyright notice and + this permission notice appear in all copies. + + YUP IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER + EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE + DISCLAIMED. + + ============================================================================== +*/ + +#include + +#include + +using namespace yup; + +namespace +{ +constexpr double combSampleRate = 48000.0; +constexpr int combBlockSize = 128; + +template +void expectFiniteCombBuffer (const std::vector& buffer) +{ + for (auto sample : buffer) + EXPECT_TRUE (std::isfinite (sample)); +} +} // namespace + +//============================================================================== + +TEST (CombFilterTests, DefaultConstructorInitializes) +{ + CombFilter filter; + + EXPECT_NO_THROW (filter.prepare (combSampleRate, combBlockSize)); + EXPECT_GT (filter.getFrequency(), 0.0f); + EXPECT_GE (filter.getDelayInSamples(), 1.0f); +} + +TEST (CombFilterTests, ProducesExpectedFeedForwardDelayTap) +{ + CombFilter filter; + filter.prepare (combSampleRate, combBlockSize); + filter.setParameters (static_cast (combSampleRate / 8.0), 0.0f, 0.0f, combSampleRate); + filter.reset(); + + std::vector input (combBlockSize, 0.0f); + std::vector output (combBlockSize, 0.0f); + input[0] = 1.0f; + + filter.processBlock (input.data(), output.data(), combBlockSize); + + EXPECT_FLOAT_EQ (output[0], 1.0f); + EXPECT_NEAR (output[8], 0.5f, 1e-5f); + expectFiniteCombBuffer (output); +} + +TEST (CombFilterTests, FeedbackAndSaturationRemainFinite) +{ + CombFilter filter; + filter.prepare (combSampleRate, combBlockSize); + filter.setParameters (440.0f, 1.0f, 1.0f, combSampleRate); + filter.setSignalRange (2.0f); + + std::vector input (combBlockSize, 0.25f); + std::vector output (combBlockSize, 0.0f); + + filter.processBlock (input.data(), output.data(), combBlockSize); + + expectFiniteCombBuffer (output); +} + +TEST (CombFilterTests, SetParametersFromNoteUpdatesDelay) +{ + CombFilter filter; + filter.prepare (combSampleRate, combBlockSize); + filter.setParametersFromNote (69.0, 0.5, 0.0, combSampleRate); + + EXPECT_NEAR (filter.getFrequency(), 440.0, 1e-9); + EXPECT_NEAR (filter.getDelayInSamples(), combSampleRate / 440.0, 1e-9); +} + +TEST (CombFilterTests, ResetRestoresDeterministicState) +{ + CombFilter filter; + filter.prepare (combSampleRate, combBlockSize); + filter.setParameters (660.0f, 0.5f, 0.2f, combSampleRate); + + std::vector input (combBlockSize, 0.0f); + std::vector firstOutput (combBlockSize, 0.0f); + std::vector secondOutput (combBlockSize, 0.0f); + + for (int i = 0; i < combBlockSize; ++i) + input[static_cast (i)] = (i % 5 == 0) ? 0.5f : -0.125f; + + filter.processBlock (input.data(), firstOutput.data(), combBlockSize); + filter.reset(); + filter.processBlock (input.data(), secondOutput.data(), combBlockSize); + + for (int i = 0; i < combBlockSize; ++i) + EXPECT_FLOAT_EQ (firstOutput[static_cast (i)], secondOutput[static_cast (i)]); +} + +TEST (CombFilterTests, ComplexResponseIsFinite) +{ + CombFilter filter; + filter.prepare (combSampleRate, combBlockSize); + filter.setParameters (440.0, 0.7, 0.3, combSampleRate); + + const auto response = filter.getComplexResponse (1000.0); + + EXPECT_TRUE (std::isfinite (response.real())); + EXPECT_TRUE (std::isfinite (response.imag())); +} + +TEST (CombFilterTests, ComplexResponseMatchesIntegerDelayFeedForwardComb) +{ + CombFilter filter; + filter.prepare (combSampleRate, combBlockSize); + filter.setParameters (combSampleRate / 8.0, 0.0, 0.0, combSampleRate); + + EXPECT_NEAR (std::abs (filter.getComplexResponse (0.0)), 1.5, 1e-9); + EXPECT_NEAR (std::abs (filter.getComplexResponse (combSampleRate / 16.0)), 0.5, 1e-9); +} + +TEST (CombFilterTests, ComplexResponseIncludesFeedbackResonance) +{ + CombFilter filter; + filter.prepare (combSampleRate, combBlockSize); + filter.setParameters (combSampleRate / 8.0, 0.81, 0.0, combSampleRate); + + EXPECT_NEAR (std::abs (filter.getComplexResponse (combSampleRate / 8.0)), 6.0, 1e-9); +} diff --git a/tests/yup_dsp/yup_FilterDesigner.cpp b/tests/yup_dsp/yup_FilterDesigner.cpp index 5ddc72b6d..40cb687ae 100644 --- a/tests/yup_dsp/yup_FilterDesigner.cpp +++ b/tests/yup_dsp/yup_FilterDesigner.cpp @@ -361,6 +361,48 @@ TEST_F (FilterDesignerTests, FloatPrecisionConsistency) EXPECT_NEAR (doubleCoeffs.a2, static_cast (floatCoeffs.a2), toleranceF); } +//============================================================================== +// Butterworth Filter Design Tests +//============================================================================== + +TEST_F (FilterDesignerTests, ButterworthDesignersUseCoefficientVectorFirst) +{ + std::vector> coeffs; + + auto sections = FilterDesigner::designButterworth (coeffs, FilterMode::lowpass, 4, frequency, 0.0, sampleRate); + EXPECT_EQ (coeffs.size(), static_cast (sections)); + EXPECT_GT (sections, 0); + + sections = FilterDesigner::designButterworthLowpass (coeffs, 4, frequency, sampleRate); + EXPECT_EQ (coeffs.size(), static_cast (sections)); + EXPECT_GT (sections, 0); + + sections = FilterDesigner::designButterworthHighpass (coeffs, 4, frequency, sampleRate); + EXPECT_EQ (coeffs.size(), static_cast (sections)); + EXPECT_GT (sections, 0); + + sections = FilterDesigner::designButterworthBandpass (coeffs, 4, 800.0, 1200.0, sampleRate); + EXPECT_EQ (coeffs.size(), static_cast (sections)); + EXPECT_GT (sections, 0); + + sections = FilterDesigner::designButterworthBandstop (coeffs, 4, 800.0, 1200.0, sampleRate); + EXPECT_EQ (coeffs.size(), static_cast (sections)); + EXPECT_GT (sections, 0); + + sections = FilterDesigner::designButterworthAllpass (coeffs, 4, frequency, sampleRate); + EXPECT_EQ (coeffs.size(), static_cast (sections)); + EXPECT_GT (sections, 0); + + for (const auto& coeff : coeffs) + { + EXPECT_TRUE (std::isfinite (coeff.b0)); + EXPECT_TRUE (std::isfinite (coeff.b1)); + EXPECT_TRUE (std::isfinite (coeff.b2)); + EXPECT_TRUE (std::isfinite (coeff.a1)); + EXPECT_TRUE (std::isfinite (coeff.a2)); + } +} + //============================================================================== // FIR Filter Design Tests //============================================================================== diff --git a/tests/yup_dsp/yup_LinkwitzRileyFilter.cpp b/tests/yup_dsp/yup_LinkwitzRileyFilter.cpp index 6b85f0053..1800cd64f 100644 --- a/tests/yup_dsp/yup_LinkwitzRileyFilter.cpp +++ b/tests/yup_dsp/yup_LinkwitzRileyFilter.cpp @@ -245,7 +245,7 @@ TEST (FilterDesignerLinkwitzRileyTests, DesignLR2ReturnsValidCoefficients) { std::vector> lowCoeffs, highCoeffs; - int sections = FilterDesigner::designLinkwitzRiley2 (1000.0, 44100.0, lowCoeffs, highCoeffs); + int sections = FilterDesigner::designLinkwitzRiley2 (lowCoeffs, highCoeffs, 1000.0, 44100.0); //EXPECT_EQ (sections, 2); EXPECT_EQ (lowCoeffs.size(), 2); @@ -270,7 +270,7 @@ TEST (FilterDesignerLinkwitzRileyTests, DesignLR4ReturnsCorrectNumberOfSections) { std::vector> lowCoeffs, highCoeffs; - int sections = FilterDesigner::designLinkwitzRiley4 (1000.0, 48000.0, lowCoeffs, highCoeffs); + int sections = FilterDesigner::designLinkwitzRiley4 (lowCoeffs, highCoeffs, 1000.0, 48000.0); EXPECT_EQ (sections, 4); // LR4 should create 4 biquad sections EXPECT_EQ (lowCoeffs.size(), 4); @@ -281,7 +281,7 @@ TEST (FilterDesignerLinkwitzRileyTests, DesignLR8ReturnsCorrectNumberOfSections) { std::vector> lowCoeffs, highCoeffs; - int sections = FilterDesigner::designLinkwitzRiley8 (1000.0, 48000.0, lowCoeffs, highCoeffs); + int sections = FilterDesigner::designLinkwitzRiley8 (lowCoeffs, highCoeffs, 1000.0, 48000.0); EXPECT_EQ (sections, 8); // LR8 should create 8 biquad sections EXPECT_EQ (lowCoeffs.size(), 8); @@ -293,14 +293,14 @@ TEST (FilterDesignerLinkwitzRileyTests, GeneralDesignerHandlesVariousOrders) std::vector> lowCoeffs, highCoeffs; // Test LR2 - int sections2 = FilterDesigner::designLinkwitzRiley (2, 1000.0, 48000.0, lowCoeffs, highCoeffs); + int sections2 = FilterDesigner::designLinkwitzRiley (lowCoeffs, highCoeffs, 2, 1000.0, 48000.0); EXPECT_EQ (sections2, 2); // Test LR4 - int sections4 = FilterDesigner::designLinkwitzRiley (4, 1000.0, 48000.0, lowCoeffs, highCoeffs); + int sections4 = FilterDesigner::designLinkwitzRiley (lowCoeffs, highCoeffs, 4, 1000.0, 48000.0); EXPECT_EQ (sections4, 4); // Test LR8 - int sections8 = FilterDesigner::designLinkwitzRiley (8, 1000.0, 48000.0, lowCoeffs, highCoeffs); + int sections8 = FilterDesigner::designLinkwitzRiley (lowCoeffs, highCoeffs, 8, 1000.0, 48000.0); EXPECT_EQ (sections8, 8); }