diff --git a/examples/audiograph/source/nodes/DistortionNodes.h b/examples/audiograph/source/nodes/DistortionNodes.h new file mode 100644 index 000000000..42e21f8f9 --- /dev/null +++ b/examples/audiograph/source/nodes/DistortionNodes.h @@ -0,0 +1,541 @@ +/* + ============================================================================== + + 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 + +#include +#include +#include + +#include + +#include "NodeViewHelpers.h" + +//============================================================================== +class TanhDistortionProcessor final : public yup::AudioProcessor +{ +public: + TanhDistortionProcessor() + : AudioProcessor ("Tanh Distortion", + yup::AudioBusLayout ({ yup::AudioBus ("Main", yup::AudioBus::Audio, yup::AudioBus::Input, 2) }, + { yup::AudioBus ("Main", yup::AudioBus::Audio, yup::AudioBus::Output, 2) })) + { + updateLatency(); + } + + void prepareToPlay (float newSampleRate, int maxBlockSize) override + { + const auto sampleRate = yup::jmax (1.0f, newSampleRate); + const auto blockSize = yup::jmax (1, maxBlockSize); + + oversampler2x.prepare (sampleRate, maximumOversampledChannels, blockSize); + oversampler4x.prepare (sampleRate, maximumOversampledChannels, blockSize); + oversampler8x.prepare (sampleRate, maximumOversampledChannels, blockSize); + oversamplersPrepared = true; + + updateLatency(); + } + + void releaseResources() override {} + + void flush() override + { + oversampler2x.reset(); + oversampler4x.reset(); + oversampler8x.reset(); + } + + void processBlock (yup::AudioProcessContext& context) override + { + auto& audioBuffer = context.audio; + + const int numChannels = audioBuffer.getNumChannels(); + const int numSamples = audioBuffer.getNumSamples(); + + if (numChannels <= 0 || numSamples <= 0) + return; + + const auto currentDrive = drive.load (std::memory_order_relaxed); + const auto currentOversamplingIndex = oversamplingIndex.load (std::memory_order_relaxed); + + if (! oversamplersPrepared || currentOversamplingIndex == 0) + { + processChannelsInPlace (audioBuffer, 0, numChannels, currentDrive); + return; + } + + const int oversampledChannels = yup::jmin (numChannels, maximumOversampledChannels); + + switch (currentOversamplingIndex) + { + case 1: + processOversampled (oversampler2x, audioBuffer, oversampledChannels, numSamples, currentDrive); + break; + + case 2: + processOversampled (oversampler4x, audioBuffer, oversampledChannels, numSamples, currentDrive); + break; + + case 3: + processOversampled (oversampler8x, audioBuffer, oversampledChannels, numSamples, currentDrive); + break; + + default: + break; + } + + processChannelsInPlace (audioBuffer, oversampledChannels, numChannels, currentDrive); + } + + int getCurrentPreset() const noexcept override { return 0; } + + void setCurrentPreset (int) noexcept override {} + + int getNumPresets() const override { return 0; } + + yup::String getPresetName (int) const override { return {}; } + + void setPresetName (int, yup::StringRef) override {} + + yup::Result loadStateFromMemory (const yup::MemoryBlock& data) override + { + if (data.isEmpty()) + return yup::Result::ok(); + + yup::MemoryInputStream stream (data, false); + + const int version = stream.readInt(); + if (version != 1) + return yup::Result::fail ("Unsupported tanh distortion node state version"); + + setDrive (stream.readFloat()); + setOversamplingIndex (stream.readInt()); + + return yup::Result::ok(); + } + + yup::Result saveStateIntoMemory (yup::MemoryBlock& data) override + { + yup::MemoryOutputStream stream (data, false); + stream.writeInt (1); + stream.writeFloat (getDrive()); + stream.writeInt (getOversamplingIndex()); + stream.flush(); + + return yup::Result::ok(); + } + + bool hasEditor() const override { return false; } + + yup::AudioProcessorEditor* createEditor() override { return nullptr; } + + float getDrive() const noexcept + { + return drive.load (std::memory_order_relaxed); + } + + int getOversamplingIndex() const noexcept + { + return oversamplingIndex.load (std::memory_order_relaxed); + } + + void setDrive (float newDrive) noexcept + { + drive.store (yup::jlimit (1.0f, 24.0f, newDrive), std::memory_order_relaxed); + } + + void setOversamplingIndex (int newOversamplingIndex) noexcept + { + oversamplingIndex.store (yup::jlimit (0, 3, newOversamplingIndex), std::memory_order_relaxed); + updateLatency(); + } + + yup::String getOversamplingText() const + { + switch (getOversamplingIndex()) + { + case 1: + return "2x"; + case 2: + return "4x"; + case 3: + return "8x"; + default: + return "1x"; + } + } + +private: + static constexpr int maximumOversampledChannels = 2; + + static float processSample (float input, float currentDrive) noexcept + { + return std::tanh (input * currentDrive); + } + + void processChannelsInPlace (yup::AudioBuffer& audioBuffer, int startChannel, int endChannel, float currentDrive) noexcept + { + for (int channel = startChannel; channel < endChannel; ++channel) + { + auto* channelData = audioBuffer.getWritePointer (channel); + + for (int sample = 0; sample < audioBuffer.getNumSamples(); ++sample) + channelData[sample] = processSample (channelData[sample], currentDrive); + } + } + + template + void processOversampled (OversamplerType& oversampler, + yup::AudioBuffer& audioBuffer, + int numChannels, + int numSamples, + float currentDrive) noexcept + { + oversampler.upsample (audioBuffer.getArrayOfReadPointers(), numChannels, numSamples); + + oversampler.processOversampledBlock ([&] (auto& oversampledBuffer) + { + const int oversampledSamples = oversampler.getOversampledNumSamples(); + + for (int channel = 0; channel < numChannels; ++channel) + { + auto* channelData = oversampledBuffer.getWritePointer (channel); + + for (int sample = 0; sample < oversampledSamples; ++sample) + *channelData++ = processSample (*channelData, currentDrive); + } + }); + + oversampler.downsample (audioBuffer.getArrayOfWritePointers(), numChannels, numSamples); + } + + void updateLatency() + { + int latencySamples = 0; + + switch (getOversamplingIndex()) + { + case 1: + latencySamples = oversampler2x.getLatencyInSamples(); + break; + case 2: + latencySamples = oversampler4x.getLatencyInSamples(); + break; + case 3: + latencySamples = oversampler8x.getLatencyInSamples(); + break; + default: + break; + } + + setLatencySamples (latencySamples); + } + + std::atomic drive { 4.0f }; + std::atomic oversamplingIndex { 2 }; + yup::Oversampler2xFloat oversampler2x; + yup::Oversampler4xFloat oversampler4x; + yup::Oversampler8xFloat oversampler8x; + bool oversamplersPrepared = false; +}; + +//============================================================================== +class BlunterSoftClipperProcessor final : public yup::AudioProcessor +{ +public: + BlunterSoftClipperProcessor() + : AudioProcessor ("Blunter Soft Clip", + yup::AudioBusLayout ({ yup::AudioBus ("Main", yup::AudioBus::Audio, yup::AudioBus::Input, 2) }, + { yup::AudioBus ("Main", yup::AudioBus::Audio, yup::AudioBus::Output, 2) })) + { + } + + void prepareToPlay (float newSampleRate, int maxBlockSize) override + { + for (auto& clipper : clippers) + clipper.prepare (newSampleRate, maxBlockSize); + } + + void releaseResources() override {} + + void flush() override + { + for (auto& clipper : clippers) + clipper.reset(); + } + + void processBlock (yup::AudioProcessContext& context) override + { + auto& audioBuffer = context.audio; + + const auto currentDrive = drive.load (std::memory_order_relaxed); + const auto currentOutput = output.load (std::memory_order_relaxed); + const int clipperChannels = static_cast (clippers.size()); + + for (auto& clipper : clippers) + clipper.setParameters (currentDrive, currentOutput); + + for (int channel = 0; channel < audioBuffer.getNumChannels(); ++channel) + { + auto* channelData = audioBuffer.getWritePointer (channel); + auto& clipper = clippers[static_cast (yup::jmin (channel, clipperChannels - 1))]; + + clipper.processInPlace (channelData, audioBuffer.getNumSamples()); + } + } + + int getCurrentPreset() const noexcept override { return 0; } + + void setCurrentPreset (int) noexcept override {} + + int getNumPresets() const override { return 0; } + + yup::String getPresetName (int) const override { return {}; } + + void setPresetName (int, yup::StringRef) override {} + + yup::Result loadStateFromMemory (const yup::MemoryBlock& data) override + { + if (data.isEmpty()) + return yup::Result::ok(); + + yup::MemoryInputStream stream (data, false); + + const int version = stream.readInt(); + if (version != 1) + return yup::Result::fail ("Unsupported blunter soft clipper node state version"); + + setDrive (stream.readFloat()); + setOutput (stream.readFloat()); + + return yup::Result::ok(); + } + + yup::Result saveStateIntoMemory (yup::MemoryBlock& data) override + { + yup::MemoryOutputStream stream (data, false); + stream.writeInt (1); + stream.writeFloat (getDrive()); + stream.writeFloat (getOutput()); + stream.flush(); + + return yup::Result::ok(); + } + + bool hasEditor() const override { return false; } + + yup::AudioProcessorEditor* createEditor() override { return nullptr; } + + float getDrive() const noexcept + { + return drive.load (std::memory_order_relaxed); + } + + float getOutput() const noexcept + { + return output.load (std::memory_order_relaxed); + } + + void setDrive (float newDrive) noexcept + { + drive.store (yup::jlimit (0.1f, 24.0f, newDrive), std::memory_order_relaxed); + } + + void setOutput (float newOutput) noexcept + { + output.store (yup::jlimit (0.0f, 1.5f, newOutput), std::memory_order_relaxed); + } + +private: + std::atomic drive { 1.0f }; + std::atomic output { 0.5f }; + std::array clippers; +}; + +//============================================================================== +class TanhDistortionNodeView final : public yup::AudioGraphNodeView +{ +public: + TanhDistortionNodeView (yup::AudioGraphNodeID nodeID, TanhDistortionProcessor& processorIn) + : AudioGraphNodeView (nodeID) + , processor (processorIn) + , driveSlider (yup::Slider::LinearBarHorizontal) + , oversamplingSlider (yup::Slider::LinearBarHorizontal) + { + NodeViewHelpers::configureParameterSlider (driveSlider, getPortKindColor (PortKind::parameter)); + driveSlider.setRange (1.0, 24.0, 0.1); + driveSlider.setSkewFactorFromMidpoint (4.0); + driveSlider.setValue (processor.getDrive(), yup::dontSendNotification); + driveSlider.onValueChanged = [this] (double value) + { + processor.setDrive (static_cast (value)); + repaint(); + }; + addAndMakeVisible (driveSlider); + + NodeViewHelpers::configureParameterSlider (oversamplingSlider, getPortKindColor (PortKind::parameter)); + oversamplingSlider.setRange (0.0, 3.0, 1.0); + oversamplingSlider.setValue (processor.getOversamplingIndex(), yup::dontSendNotification); + oversamplingSlider.onValueChanged = [this] (double value) + { + processor.setOversamplingIndex (yup::roundToInt (value)); + repaint(); + }; + addAndMakeVisible (oversamplingSlider); + } + + yup::String getNodeTitle() const override { return "TANH"; } + + int getNumInputPorts() const override { return 1; } + + int getNumOutputPorts() const override { return 1; } + + int getPreferredWidth() const override { return 240; } + + yup::Color getNodeColor() const override { return yup::Color (0xffef4444); } + + yup::String getNodeSubtitle() const override + { + return yup::String ("x ") + yup::String (processor.getDrive(), 1) + " / " + processor.getOversamplingText(); + } + + PortInfo getInputPortInfo (int) const override { return { "audio", getPortKindColor (PortKind::audio), PortKind::audio }; } + + PortInfo getOutputPortInfo (int) const override { return { "audio", getPortKindColor (PortKind::audio), PortKind::audio }; } + + int getNumParameterRows() const override { return 2; } + + ParameterInfo getParameterInfo (int parameterIndex) const override + { + switch (parameterIndex) + { + case 0: + return { "Drive", yup::String (processor.getDrive(), 1), getPortKindColor (PortKind::parameter), normalizedDrive (processor.getDrive()), PortKind::parameter }; + + case 1: + return { "Oversampling", processor.getOversamplingText(), getPortKindColor (PortKind::parameter), static_cast (processor.getOversamplingIndex()) / 3.0f, PortKind::parameter }; + + default: + return {}; + } + } + + void resized() override + { + driveSlider.setBounds (NodeViewHelpers::getInlineSliderBounds (*this, getPreferredWidth(), 0)); + oversamplingSlider.setBounds (NodeViewHelpers::getInlineSliderBounds (*this, getPreferredWidth(), 1)); + } + +private: + static float normalizedDrive (float value) noexcept + { + return yup::jlimit (0.0f, 1.0f, (value - 1.0f) / 23.0f); + } + + TanhDistortionProcessor& processor; + yup::Slider driveSlider; + yup::Slider oversamplingSlider; +}; + +//============================================================================== +class BlunterSoftClipperNodeView final : public yup::AudioGraphNodeView +{ +public: + BlunterSoftClipperNodeView (yup::AudioGraphNodeID nodeID, BlunterSoftClipperProcessor& processorIn) + : AudioGraphNodeView (nodeID) + , processor (processorIn) + , driveSlider (yup::Slider::LinearBarHorizontal) + , outputSlider (yup::Slider::LinearBarHorizontal) + { + NodeViewHelpers::configureParameterSlider (driveSlider, getPortKindColor (PortKind::parameter)); + driveSlider.setRange (0.1, 24.0, 0.1); + driveSlider.setSkewFactorFromMidpoint (2.0); + driveSlider.setValue (processor.getDrive(), yup::dontSendNotification); + driveSlider.onValueChanged = [this] (double value) + { + processor.setDrive (static_cast (value)); + repaint(); + }; + addAndMakeVisible (driveSlider); + + NodeViewHelpers::configureParameterSlider (outputSlider, getPortKindColor (PortKind::parameter)); + outputSlider.setRange (0.0, 1.5, 0.01); + outputSlider.setValue (processor.getOutput(), yup::dontSendNotification); + outputSlider.onValueChanged = [this] (double value) + { + processor.setOutput (static_cast (value)); + repaint(); + }; + addAndMakeVisible (outputSlider); + } + + yup::String getNodeTitle() const override { return "BLUNTER"; } + + int getNumInputPorts() const override { return 1; } + + int getNumOutputPorts() const override { return 1; } + + int getPreferredWidth() const override { return 240; } + + yup::Color getNodeColor() const override { return yup::Color (0xfff97316); } + + yup::String getNodeSubtitle() const override + { + return yup::String ("x ") + yup::String (processor.getDrive(), 1) + " -> " + yup::String (processor.getOutput(), 2); + } + + PortInfo getInputPortInfo (int) const override { return { "audio", getPortKindColor (PortKind::audio), PortKind::audio }; } + + PortInfo getOutputPortInfo (int) const override { return { "audio", getPortKindColor (PortKind::audio), PortKind::audio }; } + + int getNumParameterRows() const override { return 2; } + + ParameterInfo getParameterInfo (int parameterIndex) const override + { + switch (parameterIndex) + { + case 0: + return { "Drive", yup::String (processor.getDrive(), 1), getPortKindColor (PortKind::parameter), normalizedDrive (processor.getDrive()), PortKind::parameter }; + + case 1: + return { "Output", yup::String (processor.getOutput(), 2), getPortKindColor (PortKind::parameter), processor.getOutput() / 1.5f, PortKind::parameter }; + + default: + return {}; + } + } + + void resized() override + { + driveSlider.setBounds (NodeViewHelpers::getInlineSliderBounds (*this, getPreferredWidth(), 0)); + outputSlider.setBounds (NodeViewHelpers::getInlineSliderBounds (*this, getPreferredWidth(), 1)); + } + +private: + static float normalizedDrive (float value) noexcept + { + return yup::jlimit (0.0f, 1.0f, (value - 0.1f) / 23.9f); + } + + BlunterSoftClipperProcessor& processor; + yup::Slider driveSlider; + yup::Slider outputSlider; +}; diff --git a/examples/audiograph/source/nodes/NodeRegistry.h b/examples/audiograph/source/nodes/NodeRegistry.h index ac03d1507..9428e39b6 100644 --- a/examples/audiograph/source/nodes/NodeRegistry.h +++ b/examples/audiograph/source/nodes/NodeRegistry.h @@ -26,6 +26,7 @@ #include #include +#include "DistortionNodes.h" #include "GainNode.h" #include "LatencyNode.h" #include "LowPassFilterNode.h" @@ -59,6 +60,12 @@ class NodeRegistry /** Stable factory key for the built-in low-pass filter node. */ static constexpr const char* lpfIdentifier = "internal.lpf"; + /** Stable factory key for the built-in tanh distortion node. */ + static constexpr const char* tanhDistortionIdentifier = "internal.tanhDistortion"; + + /** Stable factory key for the built-in Blunter soft clipper node. */ + static constexpr const char* blunterSoftClipperIdentifier = "internal.blunterSoftClipper"; + /** Stable factory key for the built-in looping sample player node. */ static constexpr const char* samplePlayerIdentifier = "internal.samplePlayer"; @@ -160,6 +167,36 @@ class NodeRegistry } }; + entries[tanhDistortionIdentifier] = { + [] (const yup::AudioGraphNodeProperties&) -> yup::ResultValue> + { + return yup::makeResultValueOk (std::make_unique()); + }, + [] (yup::AudioGraphNodeID nodeID, yup::AudioProcessor* proc, yup::AudioGraphProcessor*) -> std::unique_ptr + { + auto* distortion = dynamic_cast (proc); + if (distortion == nullptr) + return nullptr; + + return std::make_unique (nodeID, *distortion); + } + }; + + entries[blunterSoftClipperIdentifier] = { + [] (const yup::AudioGraphNodeProperties&) -> yup::ResultValue> + { + return yup::makeResultValueOk (std::make_unique()); + }, + [] (yup::AudioGraphNodeID nodeID, yup::AudioProcessor* proc, yup::AudioGraphProcessor*) -> std::unique_ptr + { + auto* clipper = dynamic_cast (proc); + if (clipper == nullptr) + return nullptr; + + return std::make_unique (nodeID, *clipper); + } + }; + entries[samplePlayerIdentifier] = { [] (const yup::AudioGraphNodeProperties&) -> yup::ResultValue> { @@ -392,6 +429,8 @@ class NodeRegistry oscillatorIdentifier, gainIdentifier, lpfIdentifier, + tanhDistortionIdentifier, + blunterSoftClipperIdentifier, latencyIdentifier, samplePlayerIdentifier, subgraphIdentifier @@ -420,6 +459,12 @@ class NodeRegistry if (id == lpfIdentifier) return "Low Pass Filter"; + if (id == tanhDistortionIdentifier) + return "Tanh Distortion"; + + if (id == blunterSoftClipperIdentifier) + return "Blunter Soft Clip"; + if (id == samplePlayerIdentifier) return "Sample Player"; if (id == subgraphIdentifier) diff --git a/examples/audiograph/source/nodes/NodeViewHelpers.h b/examples/audiograph/source/nodes/NodeViewHelpers.h index 2159b5362..86b98de0f 100644 --- a/examples/audiograph/source/nodes/NodeViewHelpers.h +++ b/examples/audiograph/source/nodes/NodeViewHelpers.h @@ -35,14 +35,19 @@ inline void configureParameterSlider (yup::Slider& slider, yup::Color accent) slider.setColor (yup::Slider::Style::thumbDownColorId, accent.darker (0.15f)); } -inline yup::Rectangle getInlineSliderBounds (const yup::Component& component, int preferredWidth) +inline yup::Rectangle getInlineSliderBounds (const yup::Component& component, int preferredWidth, int rowIndex) { const auto bounds = component.getLocalBounds(); const auto scale = bounds.getWidth() / static_cast (preferredWidth); return { 62.0f * scale, - 49.0f * scale, + (49.0f + 25.0f * static_cast (rowIndex)) * scale, yup::jmax (42.0f * scale, bounds.getWidth() - (150.0f * scale)), 20.0f * scale }; } +inline yup::Rectangle getInlineSliderBounds (const yup::Component& component, int preferredWidth) +{ + return getInlineSliderBounds (component, preferredWidth, 0); +} + } // namespace NodeViewHelpers diff --git a/modules/yup_audio_basics/sources/yup_BufferingAudioSource.cpp b/modules/yup_audio_basics/sources/yup_BufferingAudioSource.cpp index cdeb97d57..7b2a312db 100644 --- a/modules/yup_audio_basics/sources/yup_BufferingAudioSource.cpp +++ b/modules/yup_audio_basics/sources/yup_BufferingAudioSource.cpp @@ -210,6 +210,26 @@ int64 BufferingAudioSource::getNextReadPosition() const : pos; } +void BufferingAudioSource::setLooping (bool shouldLoop) +{ + { + const ScopedLock sl (bufferRangeLock); + + source->setLooping (shouldLoop); + + const auto isSourceLooping = source->isLooping(); + + if (wasSourceLooping != isSourceLooping) + { + wasSourceLooping = isSourceLooping; + bufferValidStart = 0; + bufferValidEnd = 0; + } + } + + backgroundThread.moveToFrontOfQueue (this); +} + void BufferingAudioSource::setNextReadPosition (int64 newPosition) { const ScopedLock sl (bufferRangeLock); diff --git a/modules/yup_audio_basics/sources/yup_BufferingAudioSource.h b/modules/yup_audio_basics/sources/yup_BufferingAudioSource.h index 8589358a8..773fef2c5 100644 --- a/modules/yup_audio_basics/sources/yup_BufferingAudioSource.h +++ b/modules/yup_audio_basics/sources/yup_BufferingAudioSource.h @@ -108,6 +108,9 @@ class YUP_API BufferingAudioSource : public PositionableAudioSource /** Implements the PositionableAudioSource method. */ bool isLooping() const override { return source->isLooping(); } + /** Implements the PositionableAudioSource method. */ + void setLooping (bool shouldLoop) override; + /** A useful function to block until the next the buffer info can be filled. This is useful for offline rendering. diff --git a/modules/yup_dsp/dynamics/yup_BlunterClipper.h b/modules/yup_dsp/dynamics/yup_BlunterClipper.h new file mode 100644 index 000000000..49a35c5b9 --- /dev/null +++ b/modules/yup_dsp/dynamics/yup_BlunterClipper.h @@ -0,0 +1,214 @@ +/* + ============================================================================== + + 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 +{ + +//============================================================================== +/** + Blunter soft clipper based on a quadratic bounded clipping curve. + + Based on the research by Lorenzo Fiestas ("Quantifying Clipping Softness"), + the Blunter was experimentally found to minimize peak second derivative + (maximize softness) among the generated symmetric bounded clippers for the + normalization range studied in the paper. + + The canonical Blunter formula on the unit domain: + + @code + B(x) = x * (2 - |x|) for |x| <= 1 + B(x) = sign(x) for |x| > 1 + @endcode + + This gives a constant |B″(x)| = 2 across the entire knee region, spreading + the transition evenly compared to tanh or hyperbolic clippers that + concentrate distortion near the clip point. + + The processSample function computes: B(x * inputGain) * outputGain + + - inputGain (A_in) controls the amount of distortion (overdrive). Values + above 1.0 push the signal further into the non-linear region. + - outputGain (A_out) compensates the volume after clipping. + + Note: B′(0) = 2, so signals well below the clip point are amplified by 2×. + Set outputGain = 0.5 for unity gain at the origin if needed. + + Gain setters are not synchronized. Do not call them concurrently with + processSample() or processBlock(); update parameters between processing + passes, or wrap this class in a thread-safe/smoothed parameter layer. + + @tparam SampleType The type of audio samples (float or double) + @tparam CoeffType The type used for internal calculations (defaults to double) +*/ +template +class BlunterClipper +{ +public: + //============================================================================== + /** Constructs a BlunterClipper with the given input and output gains. + + @param newInputGain Scales the input before clipping (default: 1.0). + Higher values push more signal into the non-linear + region and produce more harmonic distortion. + @param newOutputGain Scales the output after clipping (default: 1.0). + Use 0.5 to restore unity gain at the origin + (since B′(0) = 2). + */ + BlunterClipper (CoeffType newInputGain = static_cast (1), + CoeffType newOutputGain = static_cast (1)) noexcept + : inputGain (sanitizeInputGain (newInputGain)) + , outputGain (sanitizeOutputGain (newOutputGain)) + { + } + + //============================================================================== + /** Sets the input gain (A_in), which controls distortion amount. + + @param newInputGain The new input gain. Values less than or equal to + zero are clamped to the smallest positive value + representable by CoeffType. + */ + void setInputGain (CoeffType newInputGain) noexcept + { + inputGain = sanitizeInputGain (newInputGain); + } + + /** Returns the current input gain. */ + CoeffType getInputGain() const noexcept + { + return inputGain; + } + + /** Sets the output gain (A_out), which compensates the volume after clipping. + + @param newOutputGain The new output gain. Negative values are clamped + to zero to preserve monotonicity. + */ + void setOutputGain (CoeffType newOutputGain) noexcept + { + outputGain = sanitizeOutputGain (newOutputGain); + } + + /** Returns the current output gain. */ + CoeffType getOutputGain() const noexcept + { + return outputGain; + } + + /** Sets both gains at once. + + @param newInputGain The new input gain. + @param newOutputGain The new output gain. + */ + void setParameters (CoeffType newInputGain, CoeffType newOutputGain) noexcept + { + inputGain = sanitizeInputGain (newInputGain); + outputGain = sanitizeOutputGain (newOutputGain); + } + + //============================================================================== + /** No-op: this processor is stateless. */ + void reset() noexcept {} + + /** No-op: this processor is stateless. + + @param sampleRate Unused. + @param maximumBlockSize Unused. + */ + void prepare (double /*sampleRate*/, int /*maximumBlockSize*/) noexcept {} + + //============================================================================== + /** Processes a single sample through the Blunter clipper. + + Computes: B(inputSample * inputGain) * outputGain + + @param inputSample The input sample to process. + @returns The soft-clipped output sample. + */ + SampleType processSample (SampleType inputSample) noexcept + { + const auto x = static_cast (inputSample) * inputGain; + return static_cast (blunter (x) * outputGain); + } + + /** Processes a block of samples. + + @param inputBuffer Pointer to the input samples. + @param outputBuffer Pointer to the output buffer. + @param numSamples Number of samples to process. + */ + void processBlock (const SampleType* inputBuffer, SampleType* outputBuffer, int numSamples) noexcept + { + for (int i = 0; i < numSamples; ++i) + outputBuffer[i] = processSample (inputBuffer[i]); + } + + /** Processes a block of samples in-place. + + @param buffer Pointer to the buffer to process in-place. + @param numSamples Number of samples to process. + */ + void processInPlace (SampleType* buffer, int numSamples) noexcept + { + processBlock (buffer, buffer, numSamples); + } + +private: + //============================================================================== + /** Applies the canonical Blunter function: B(x) = x*(2-|x|), clamped to [-1, 1]. */ + static CoeffType blunter (CoeffType x) noexcept + { + if (x >= static_cast (1)) + return static_cast (1); + + if (x <= static_cast (-1)) + return static_cast (-1); + + return x * (static_cast (2) - std::abs (x)); + } + + static CoeffType sanitizeInputGain (CoeffType value) noexcept + { + const auto minimumGain = std::numeric_limits::epsilon(); + return value > minimumGain ? value : minimumGain; + } + + static CoeffType sanitizeOutputGain (CoeffType value) noexcept + { + return value > static_cast (0) ? value : static_cast (0); + } + + //============================================================================== + CoeffType inputGain = static_cast (1); + CoeffType outputGain = static_cast (1); + + //============================================================================== + YUP_LEAK_DETECTOR (BlunterClipper) +}; + +//============================================================================== +/** Type aliases for convenience. */ +using BlunterClipperFloat = BlunterClipper; +using BlunterClipperDouble = BlunterClipper; + +} // namespace yup diff --git a/modules/yup_dsp/resampling/yup_Oversampler.h b/modules/yup_dsp/resampling/yup_Oversampler.h index 59fd89f90..2656c2990 100644 --- a/modules/yup_dsp/resampling/yup_Oversampler.h +++ b/modules/yup_dsp/resampling/yup_Oversampler.h @@ -92,9 +92,15 @@ class Oversampler decimBeginBufs.assign (maxChannels, CircularBuffer {}); decimEndBufs.assign (maxChannels, CircularBuffer {}); - xInterp.assign (maxChannels, std::vector (static_cast (maxBlockSize + SincRadius), SampleType {})); - xDecim.assign (maxChannels, std::vector (static_cast (maxInterpolated + SincRadius * OversampleFactor), SampleType {})); - oversampledBuffer.assign (maxChannels, std::vector (static_cast (maxInterpolated), SampleType {})); + xInterp.setSize (maxChannels, maxBlockSize + SincRadius); + xInterp.clear(); + + xDecim.setSize (maxChannels, maxInterpolated + SincRadius * OversampleFactor); + xDecim.clear(); + + oversampledBuffer.setSize (maxChannels, maxInterpolated); + oversampledBuffer.clear(); + currentOversampledSize = 0; currentNumChannels = 0; } @@ -110,19 +116,20 @@ class Oversampler { for (auto& b : interpolBeginBufs) b.clear(); + for (auto& b : interpolEndBufs) b.clear(); + for (auto& b : decimBeginBufs) b.clear(); + for (auto& b : decimEndBufs) b.clear(); - for (auto& ch : xInterp) - std::fill (ch.begin(), ch.end(), SampleType {}); - for (auto& ch : xDecim) - std::fill (ch.begin(), ch.end(), SampleType {}); - for (auto& ch : oversampledBuffer) - std::fill (ch.begin(), ch.end(), SampleType {}); + xInterp.clear(); + xDecim.clear(); + oversampledBuffer.clear(); + currentOversampledSize = 0; currentNumChannels = 0; } @@ -142,20 +149,18 @@ class Oversampler void upsample (const SampleType* const* input, int numChannels, int numSamples) noexcept { jassert (numChannels > 0 && numSamples > 0); - jassert (numChannels <= static_cast (xInterp.size())); - jassert (numSamples + SincRadius <= static_cast (xInterp[0].size())); + jassert (numChannels <= xInterp.getNumChannels()); + jassert (numSamples + SincRadius <= xInterp.getNumSamples()); for (int ch = 0; ch < numChannels; ++ch) { - auto& xBuf = xInterp[static_cast (ch)]; + const auto* inputData = input[ch]; + + auto* xBuf = xInterp.getWritePointer (ch); auto& endBuf = interpolEndBufs[static_cast (ch)]; for (int i = 0; i < numSamples + SincRadius; ++i) - { - xBuf[static_cast (i)] = (i >= SincRadius) - ? input[ch][i - SincRadius] - : endBuf[i]; - } + *xBuf++ = (i >= SincRadius) ? inputData[i - SincRadius] : endBuf[i]; } currentOversampledSize = numSamples * OversampleFactor; @@ -163,12 +168,12 @@ class Oversampler for (int ch = 0; ch < numChannels; ++ch) { - auto& xBuf = xInterp[static_cast (ch)]; - auto& outBuf = oversampledBuffer[static_cast (ch)]; + auto* xBuf = xInterp.getReadPointer (ch); auto& beginBuf = interpolBeginBufs[static_cast (ch)]; auto& endBuf = interpolEndBufs[static_cast (ch)]; - outBuf[0] = xBuf[0]; + auto* outBuf = oversampledBuffer.getWritePointer (ch); + *outBuf++ = *xBuf; for (int k = 1; k < currentOversampledSize; ++k) { @@ -180,18 +185,16 @@ class Oversampler CoeffType acc = CoeffType (0); for (int n = -SincRadius; n <= 0; ++n) - acc += sincTable (n, delta) - * static_cast (xBuf[static_cast (index - n)]); + acc += sincTable (n, delta) * static_cast (xBuf[static_cast (index - n)]); for (int n = 1; n <= SincRadius; ++n) - acc += sincTable (n, delta) - * static_cast (beginBuf[SincRadius - n]); + acc += sincTable (n, delta) * static_cast (beginBuf[SincRadius - n]); - outBuf[static_cast (k)] = static_cast (acc); + *outBuf++ = static_cast (acc); } else { - outBuf[static_cast (k)] = xBuf[static_cast (index)]; + *outBuf++ = xBuf[static_cast (index)]; beginBuf.push (xBuf[static_cast (index - 1)]); } } @@ -218,28 +221,31 @@ class Oversampler void downsample (SampleType* const* output, int numChannels, int numSamples) noexcept { jassert (numChannels > 0 && numSamples > 0); - jassert (numChannels <= static_cast (xDecim.size())); + jassert (numChannels <= xDecim.getNumChannels()); jassert (currentOversampledSize > 0); const int interpolatedSize = currentOversampledSize; for (int ch = 0; ch < numChannels; ++ch) { - auto& xBuf = xDecim[static_cast (ch)]; - auto& inBuf = oversampledBuffer[static_cast (ch)]; + auto* inBuf = oversampledBuffer.getReadPointer (ch); + + auto* xBuf = xDecim.getWritePointer (ch); auto& dEndBuf = decimEndBufs[static_cast (ch)]; for (int i = 0; i < interpolatedSize + SincRadius * OversampleFactor; ++i) { - xBuf[static_cast (i)] = (i >= SincRadius * OversampleFactor) - ? inBuf[static_cast (i - SincRadius * OversampleFactor)] - : dEndBuf[i]; + auto* currentBuf = inBuf + static_cast (i - SincRadius * OversampleFactor); + + *xBuf++ = (i >= SincRadius * OversampleFactor) ? *currentBuf : dEndBuf[i]; } } for (int ch = 0; ch < numChannels; ++ch) { - auto& xBuf = xDecim[static_cast (ch)]; + auto* outputData = output[ch]; + + auto* xBuf = xDecim.getReadPointer (ch); auto& beginBuf = decimBeginBufs[static_cast (ch)]; auto& dEndBuf = decimEndBufs[static_cast (ch)]; @@ -249,17 +255,15 @@ class Oversampler CoeffType acc = CoeffType (0); for (int n = 1; n <= SincRadius * OversampleFactor; ++n) - acc += sincTable[n] - * static_cast (beginBuf[SincRadius * OversampleFactor - n]); + acc += sincTable[n] * static_cast (beginBuf[SincRadius * OversampleFactor - n]); for (int n = 0; n >= -(SincRadius * OversampleFactor); --n) - acc += sincTable[n] - * static_cast (xBuf[static_cast (index - n)]); + acc += sincTable[n] * static_cast (xBuf[static_cast (index - n)]); for (int i = 0; i < OversampleFactor; ++i) beginBuf.push (xBuf[static_cast (index + i)]); - output[ch][k] = static_cast (acc / static_cast (OversampleFactor)); + outputData[k] = static_cast (acc / static_cast (OversampleFactor)); } for (int i = 0; i < SincRadius * OversampleFactor; ++i) @@ -271,13 +275,11 @@ class Oversampler /** Invokes a callback with the internal oversampled multi-channel buffer. - The callback receives a reference to the internal - `std::vector>`, where each inner vector has - getOversampledNumSamples() elements. Use this to apply processing at the - elevated sample rate. + The callback receives a reference to the internal `AudioBuffer`, where each inner + vector has getOversampledNumSamples() elements. Use this to apply processing at the elevated + sample rate. - @param callback Callable with signature - `void(std::vector>&)`. + @param callback Callable with signature `void(AudioBuffer&)`. */ template void processOversampledBlock (Callable&& callback) @@ -300,7 +302,7 @@ class Oversampler if (channel < 0 || channel >= currentNumChannels) return nullptr; - return oversampledBuffer[static_cast (channel)].data(); + return oversampledBuffer.getWritePointer (channel); } /** @@ -317,7 +319,7 @@ class Oversampler if (channel < 0 || channel >= currentNumChannels) return nullptr; - return oversampledBuffer[static_cast (channel)].data(); + return oversampledBuffer.getReadPointer (channel); } /** @@ -351,9 +353,9 @@ class Oversampler std::vector> decimBeginBufs; std::vector> decimEndBufs; - std::vector> xInterp; - std::vector> xDecim; - std::vector> oversampledBuffer; + AudioBuffer xInterp; + AudioBuffer xDecim; + AudioBuffer oversampledBuffer; int currentOversampledSize = 0; int currentNumChannels = 0; diff --git a/modules/yup_dsp/yup_dsp.h b/modules/yup_dsp/yup_dsp.h index 558db5f3f..7c5243012 100644 --- a/modules/yup_dsp/yup_dsp.h +++ b/modules/yup_dsp/yup_dsp.h @@ -176,6 +176,7 @@ // Dynamics processors #include "dynamics/yup_SoftClipper.h" +#include "dynamics/yup_BlunterClipper.h" // Convolution processors #include "convolution/yup_PartitionedConvolver.h" diff --git a/tests/yup_audio_basics/yup_BufferingAudioSource.cpp b/tests/yup_audio_basics/yup_BufferingAudioSource.cpp index 9f8a4b85f..aa8e71516 100644 --- a/tests/yup_audio_basics/yup_BufferingAudioSource.cpp +++ b/tests/yup_audio_basics/yup_BufferingAudioSource.cpp @@ -498,7 +498,7 @@ TEST_F (BufferingAudioSourceTests, GetNextReadPosition) TEST_F (BufferingAudioSourceTests, GetNextReadPositionWithLooping) { - mockSource->setLooping (true); + buffering->setLooping (true); buffering->prepareToPlay (512, 44100.0); Thread::sleep (50); @@ -608,11 +608,11 @@ TEST_F (BufferingAudioSourceTests, ReadNextBufferChunkLoopingChange) mockSource->getNextAudioBlockCalled.store (false); // Change looping state to trigger buffer reset (line 245-250) - mockSource->setLooping (true); + buffering->setLooping (true); EXPECT_TRUE (waitForFlag (mockSource->getNextAudioBlockCalled)); mockSource->getNextAudioBlockCalled.store (false); - mockSource->setLooping (false); + buffering->setLooping (false); EXPECT_TRUE (waitForFlag (mockSource->getNextAudioBlockCalled)); } diff --git a/tests/yup_dsp.cpp b/tests/yup_dsp.cpp index 94b04f724..eb9308e02 100644 --- a/tests/yup_dsp.cpp +++ b/tests/yup_dsp.cpp @@ -40,6 +40,7 @@ #include "yup_dsp/yup_Resampler.cpp" #include "yup_dsp/yup_SincTable.cpp" #include "yup_dsp/yup_SoftClipper.cpp" +#include "yup_dsp/yup_BlunterClipper.cpp" #include "yup_dsp/yup_SpectrumAnalyzerState.cpp" #include "yup_dsp/yup_StateVariableFilter.cpp" #include "yup_dsp/yup_WindowFunctions.cpp" diff --git a/tests/yup_dsp/yup_BlunterClipper.cpp b/tests/yup_dsp/yup_BlunterClipper.cpp new file mode 100644 index 000000000..4da25d9bd --- /dev/null +++ b/tests/yup_dsp/yup_BlunterClipper.cpp @@ -0,0 +1,276 @@ +/* + ============================================================================== + + 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 "yup_core/yup_core.h" +#include "yup_dsp/yup_dsp.h" + +#include + +template +class BlunterClipperTests : public ::testing::Test +{ +public: + using Clipper = yup::BlunterClipper; + + static FloatType blunterRef (FloatType x) + { + if (x >= FloatType (1)) + return FloatType (1); + if (x <= FloatType (-1)) + return FloatType (-1); + return x * (FloatType (2) - std::abs (x)); + } + + void testDefaultConstruction() + { + Clipper clipper; + EXPECT_NEAR (clipper.getInputGain(), FloatType (1), FloatType (1e-7)); + EXPECT_NEAR (clipper.getOutputGain(), FloatType (1), FloatType (1e-7)); + } + + void testParameterizedConstruction() + { + Clipper clipper (FloatType (0.6), FloatType (0.5)); + EXPECT_NEAR (clipper.getInputGain(), FloatType (0.6), FloatType (1e-7)); + EXPECT_NEAR (clipper.getOutputGain(), FloatType (0.5), FloatType (1e-7)); + } + + void testSetParameters() + { + Clipper clipper; + + clipper.setInputGain (FloatType (2)); + EXPECT_NEAR (clipper.getInputGain(), FloatType (2), FloatType (1e-7)); + + clipper.setOutputGain (FloatType (0.25)); + EXPECT_NEAR (clipper.getOutputGain(), FloatType (0.25), FloatType (1e-7)); + + clipper.setParameters (FloatType (1.5), FloatType (0.75)); + EXPECT_NEAR (clipper.getInputGain(), FloatType (1.5), FloatType (1e-7)); + EXPECT_NEAR (clipper.getOutputGain(), FloatType (0.75), FloatType (1e-7)); + } + + void testSanitizesGainParameters() + { + Clipper clipper (FloatType (-1), FloatType (-1)); + + EXPECT_GT (clipper.getInputGain(), FloatType (0)); + EXPECT_NEAR (clipper.getOutputGain(), FloatType (0), FloatType (1e-7)); + + clipper.setInputGain (FloatType (0)); + EXPECT_GT (clipper.getInputGain(), FloatType (0)); + + clipper.setOutputGain (FloatType (-0.25)); + EXPECT_NEAR (clipper.getOutputGain(), FloatType (0), FloatType (1e-7)); + + clipper.setParameters (FloatType (-2), FloatType (-0.5)); + EXPECT_GT (clipper.getInputGain(), FloatType (0)); + EXPECT_NEAR (clipper.getOutputGain(), FloatType (0), FloatType (1e-7)); + } + + void testCanonicalFormulaUnityGains() + { + Clipper clipper; + + for (FloatType x : { FloatType (-1), FloatType (-0.5), FloatType (0), FloatType (0.25), FloatType (0.5), FloatType (1) }) + { + EXPECT_NEAR (clipper.processSample (x), blunterRef (x), FloatType (1e-6)); + } + } + + void testBoundedOutput() + { + Clipper clipper; + + for (FloatType x : { FloatType (-10), FloatType (-1.001), FloatType (1.001), FloatType (10) }) + { + FloatType y = clipper.processSample (x); + EXPECT_LE (y, FloatType (1)); + EXPECT_GE (y, FloatType (-1)); + } + } + + void testMonotonicity() + { + Clipper clipper; + + FloatType prev = clipper.processSample (FloatType (-3)); + for (int i = -29; i <= 30; ++i) + { + FloatType x = FloatType (i) * FloatType (0.1); + FloatType curr = clipper.processSample (x); + EXPECT_GE (curr, prev - FloatType (1e-7)); + prev = curr; + } + } + + void testSymmetry() + { + Clipper clipper; + + for (FloatType x : { FloatType (0.1), FloatType (0.5), FloatType (0.9), FloatType (1.0), FloatType (1.5), FloatType (5.0) }) + { + EXPECT_NEAR (clipper.processSample (-x), -clipper.processSample (x), FloatType (1e-6)); + } + } + + void testZeroInput() + { + Clipper clipper; + EXPECT_NEAR (clipper.processSample (FloatType (0)), FloatType (0), FloatType (1e-7)); + } + + void testSlopeAtOriginIsTwo() + { + Clipper clipper; + const FloatType eps = FloatType (1e-5); + const FloatType slope = clipper.processSample (eps) / eps; + EXPECT_NEAR (slope, FloatType (2), FloatType (1e-4)); + } + + void testInputGainScalesDistortion() + { + Clipper low (FloatType (0.5), FloatType (1)); + Clipper high (FloatType (2.0), FloatType (1)); + + const FloatType x = FloatType (0.6); + + FloatType outLow = low.processSample (x); + FloatType outHigh = high.processSample (x); + + EXPECT_GT (outHigh, outLow); + EXPECT_NEAR (outHigh, FloatType (1), FloatType (1e-6)); + } + + void testOutputGainScalesResult() + { + Clipper half (FloatType (1), FloatType (0.5)); + Clipper full (FloatType (1), FloatType (1)); + + const FloatType x = FloatType (0.5); + EXPECT_NEAR (half.processSample (x), full.processSample (x) * FloatType (0.5), FloatType (1e-6)); + } + + void testUnityGainAtOriginWithHalfOutputGain() + { + Clipper clipper (FloatType (1), FloatType (0.5)); + const FloatType eps = FloatType (1e-5); + const FloatType slope = clipper.processSample (eps) / eps; + EXPECT_NEAR (slope, FloatType (1), FloatType (1e-4)); + } + + void testBlockProcessing() + { + Clipper clipper; + const int numSamples = 16; + FloatType input[numSamples]; + FloatType output[numSamples]; + + for (int i = 0; i < numSamples; ++i) + input[i] = FloatType (i - 8) * FloatType (0.2); + + clipper.processBlock (input, output, numSamples); + + for (int i = 0; i < numSamples; ++i) + EXPECT_NEAR (output[i], clipper.processSample (input[i]), FloatType (1e-6)); + } + + void testInPlaceProcessing() + { + Clipper clipper; + const int numSamples = 16; + FloatType data[numSamples]; + FloatType reference[numSamples]; + + for (int i = 0; i < numSamples; ++i) + { + data[i] = FloatType (i - 8) * FloatType (0.2); + reference[i] = data[i]; + } + + clipper.processInPlace (data, numSamples); + + for (int i = 0; i < numSamples; ++i) + EXPECT_NEAR (data[i], clipper.processSample (reference[i]), FloatType (1e-6)); + } + + void testResetAndPrepareAreNoOps() + { + Clipper clipper (FloatType (0.6), FloatType (0.5)); + clipper.reset(); + clipper.prepare (44100.0, 512); + + EXPECT_NEAR (clipper.getInputGain(), FloatType (0.6), FloatType (1e-7)); + EXPECT_NEAR (clipper.getOutputGain(), FloatType (0.5), FloatType (1e-7)); + } + + void testSecondDerivativeIsConstantInKnee() + { + Clipper clipper; + const FloatType h = FloatType (1e-2); + + for (FloatType x : { FloatType (-0.8), FloatType (-0.5), FloatType (-0.2), FloatType (0.2), FloatType (0.5), FloatType (0.8) }) + { + const FloatType f_plus = clipper.processSample (x + h); + const FloatType f_zero = clipper.processSample (x); + const FloatType f_minus = clipper.processSample (x - h); + const FloatType d2 = (f_plus - FloatType (2) * f_zero + f_minus) / (h * h); + EXPECT_NEAR (std::abs (d2), FloatType (2), FloatType (0.01)); + } + } +}; + +using TestTypes = ::testing::Types; +TYPED_TEST_SUITE (BlunterClipperTests, TestTypes); + +TYPED_TEST (BlunterClipperTests, DefaultConstruction) { this->testDefaultConstruction(); } + +TYPED_TEST (BlunterClipperTests, ParameterizedConstruction) { this->testParameterizedConstruction(); } + +TYPED_TEST (BlunterClipperTests, SetParameters) { this->testSetParameters(); } + +TYPED_TEST (BlunterClipperTests, SanitizesGainParameters) { this->testSanitizesGainParameters(); } + +TYPED_TEST (BlunterClipperTests, CanonicalFormulaUnityGains) { this->testCanonicalFormulaUnityGains(); } + +TYPED_TEST (BlunterClipperTests, BoundedOutput) { this->testBoundedOutput(); } + +TYPED_TEST (BlunterClipperTests, Monotonicity) { this->testMonotonicity(); } + +TYPED_TEST (BlunterClipperTests, Symmetry) { this->testSymmetry(); } + +TYPED_TEST (BlunterClipperTests, ZeroInput) { this->testZeroInput(); } + +TYPED_TEST (BlunterClipperTests, SlopeAtOriginIsTwo) { this->testSlopeAtOriginIsTwo(); } + +TYPED_TEST (BlunterClipperTests, InputGainScalesDistortion) { this->testInputGainScalesDistortion(); } + +TYPED_TEST (BlunterClipperTests, OutputGainScalesResult) { this->testOutputGainScalesResult(); } + +TYPED_TEST (BlunterClipperTests, UnityGainAtOriginWithHalfOutputGain) { this->testUnityGainAtOriginWithHalfOutputGain(); } + +TYPED_TEST (BlunterClipperTests, BlockProcessing) { this->testBlockProcessing(); } + +TYPED_TEST (BlunterClipperTests, InPlaceProcessing) { this->testInPlaceProcessing(); } + +TYPED_TEST (BlunterClipperTests, ResetAndPrepareAreNoOps) { this->testResetAndPrepareAreNoOps(); } + +TYPED_TEST (BlunterClipperTests, SecondDerivativeIsConstantInKnee) { this->testSecondDerivativeIsConstantInKnee(); } diff --git a/tests/yup_dsp/yup_Oversampler.cpp b/tests/yup_dsp/yup_Oversampler.cpp index dccfddf0e..53699a77a 100644 --- a/tests/yup_dsp/yup_Oversampler.cpp +++ b/tests/yup_dsp/yup_Oversampler.cpp @@ -136,9 +136,8 @@ TEST_F (OversamplerTest, ProcessOversampledBlockCallbackReceivesCorrectSize) int callbackSamples = 0; os2x.processOversampledBlock ([&] (auto& buf) { - callbackChannels = static_cast (buf.size()); - if (! buf.empty()) - callbackSamples = static_cast (buf[0].size()); + callbackChannels = buf.getNumChannels(); + callbackSamples = buf.getNumSamples(); }); EXPECT_EQ (callbackChannels, maxChannels); diff --git a/thirdparty/hmp3_library/hmp3_library_3.c b/thirdparty/hmp3_library/hmp3_library_3.c index 4a26227a7..1dd23a8c9 100644 --- a/thirdparty/hmp3_library/hmp3_library_3.c +++ b/thirdparty/hmp3_library/hmp3_library_3.c @@ -32,6 +32,10 @@ #pragma GCC diagnostic ignored "-Wpointer-to-int-cast" #pragma GCC diagnostic ignored "-Wint-to-pointer-cast" #pragma GCC diagnostic ignored "-Wincompatible-pointer-types" +#elif _MSC_VER +#pragma warning (push) +#pragma warning (disable : 4311) +#pragma warning (disable : 4312) #endif #define mdct_init hmp3_mdct_init @@ -44,4 +48,6 @@ #pragma clang diagnostic pop #elif __GNUC__ #pragma GCC diagnostic pop +#elif _MSC_VER +#pragma warning (pop) #endif