|
| 1 | +/* |
| 2 | + ============================================================================== |
| 3 | +
|
| 4 | + This file is part of the YUP library. |
| 5 | + Copyright (c) 2026 - kunitoki@gmail.com |
| 6 | +
|
| 7 | + YUP is an open source library subject to open-source licensing. |
| 8 | +
|
| 9 | + The code included in this file is provided under the terms of the ISC license |
| 10 | + http://www.isc.org/downloads/software-support-policy/isc-license. Permission |
| 11 | + to use, copy, modify, and/or distribute this software for any purpose with or |
| 12 | + without fee is hereby granted provided that the above copyright notice and |
| 13 | + this permission notice appear in all copies. |
| 14 | +
|
| 15 | + YUP IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER |
| 16 | + EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE |
| 17 | + DISCLAIMED. |
| 18 | +
|
| 19 | + ============================================================================== |
| 20 | +*/ |
| 21 | + |
| 22 | +#pragma once |
| 23 | + |
| 24 | +#include <array> |
| 25 | +#include <atomic> |
| 26 | +#include <utility> |
| 27 | + |
| 28 | +#include <yup_dsp/yup_dsp.h> |
| 29 | + |
| 30 | +#include "NodeViewHelpers.h" |
| 31 | + |
| 32 | +//============================================================================== |
| 33 | +class FractionalDelayProcessor final : public yup::AudioProcessor |
| 34 | +{ |
| 35 | +public: |
| 36 | + FractionalDelayProcessor() |
| 37 | + : AudioProcessor ("Fractional Delay", |
| 38 | + yup::AudioBusLayout ({ yup::AudioBus ("Main", yup::AudioBus::Audio, yup::AudioBus::Input, 2) }, |
| 39 | + { yup::AudioBus ("Main", yup::AudioBus::Audio, yup::AudioBus::Output, 2) })) |
| 40 | + { |
| 41 | + } |
| 42 | + |
| 43 | + void prepareToPlay (float newSampleRate, int) override |
| 44 | + { |
| 45 | + sampleRate.store (yup::jmax (1.0f, newSampleRate), std::memory_order_relaxed); |
| 46 | + |
| 47 | + const int maxDelaySamples = yup::roundToInt (delayMillisecondsToSamples (maximumDelayMilliseconds)); |
| 48 | + for (auto& delayLine : delayLines) |
| 49 | + { |
| 50 | + delayLine.setMaxDelaySamples (maxDelaySamples); |
| 51 | + delayLine.reset(); |
| 52 | + } |
| 53 | + |
| 54 | + feedbackState = {}; |
| 55 | + } |
| 56 | + |
| 57 | + void releaseResources() override {} |
| 58 | + |
| 59 | + void flush() override |
| 60 | + { |
| 61 | + for (auto& delayLine : delayLines) |
| 62 | + delayLine.reset(); |
| 63 | + |
| 64 | + feedbackState = {}; |
| 65 | + } |
| 66 | + |
| 67 | + void processBlock (yup::AudioBuffer<float>& audioBuffer, yup::MidiBuffer&) override |
| 68 | + { |
| 69 | + if (delayLines[0].getBufferSize() == 0 || delayLines[1].getBufferSize() == 0) |
| 70 | + return; |
| 71 | + |
| 72 | + const auto currentDelayLeftSamples = delayMillisecondsToSamples (getDelayLeftMilliseconds()); |
| 73 | + const auto currentDelayRightSamples = delayMillisecondsToSamples (getDelayRightMilliseconds()); |
| 74 | + |
| 75 | + delayLines[0].setDelaySamples (currentDelayLeftSamples); |
| 76 | + delayLines[1].setDelaySamples (currentDelayRightSamples); |
| 77 | + |
| 78 | + const auto currentFeedback = feedback.load (std::memory_order_relaxed); |
| 79 | + const auto currentDryWet = dryWet.load (std::memory_order_relaxed); |
| 80 | + const auto dryGain = 1.0f - currentDryWet; |
| 81 | + |
| 82 | + const int channels = yup::jmin (audioBuffer.getNumChannels(), static_cast<int> (delayLines.size())); |
| 83 | + |
| 84 | + for (int channel = 0; channel < channels; ++channel) |
| 85 | + { |
| 86 | + auto* channelData = audioBuffer.getWritePointer (channel); |
| 87 | + auto& delayLine = delayLines[static_cast<size_t> (channel)]; |
| 88 | + auto& channelFeedback = feedbackState[static_cast<size_t> (channel)]; |
| 89 | + |
| 90 | + for (int sample = 0; sample < audioBuffer.getNumSamples(); ++sample) |
| 91 | + { |
| 92 | + const auto input = channelData[sample]; |
| 93 | + const auto delayed = delayLine.processSample (input + channelFeedback * currentFeedback); |
| 94 | + |
| 95 | + channelFeedback = delayed; |
| 96 | + channelData[sample] = dryGain * input + currentDryWet * delayed; |
| 97 | + } |
| 98 | + } |
| 99 | + } |
| 100 | + |
| 101 | + int getCurrentPreset() const noexcept override { return 0; } |
| 102 | + |
| 103 | + void setCurrentPreset (int) noexcept override {} |
| 104 | + |
| 105 | + int getNumPresets() const override { return 0; } |
| 106 | + |
| 107 | + yup::String getPresetName (int) const override { return {}; } |
| 108 | + |
| 109 | + void setPresetName (int, yup::StringRef) override {} |
| 110 | + |
| 111 | + yup::Result loadStateFromMemory (const yup::MemoryBlock& data) override |
| 112 | + { |
| 113 | + if (data.isEmpty()) |
| 114 | + return yup::Result::ok(); |
| 115 | + |
| 116 | + yup::MemoryInputStream stream (data, false); |
| 117 | + |
| 118 | + const int version = stream.readInt(); |
| 119 | + if (version != 1) |
| 120 | + return yup::Result::fail ("Unsupported fractional delay node state version"); |
| 121 | + |
| 122 | + setDelayLeftMilliseconds (stream.readFloat()); |
| 123 | + setDelayRightMilliseconds (stream.readFloat()); |
| 124 | + setFeedback (stream.readFloat()); |
| 125 | + setDryWet (stream.readFloat()); |
| 126 | + |
| 127 | + return yup::Result::ok(); |
| 128 | + } |
| 129 | + |
| 130 | + yup::Result saveStateIntoMemory (yup::MemoryBlock& data) override |
| 131 | + { |
| 132 | + yup::MemoryOutputStream stream (data, false); |
| 133 | + stream.writeInt (1); |
| 134 | + stream.writeFloat (getDelayLeftMilliseconds()); |
| 135 | + stream.writeFloat (getDelayRightMilliseconds()); |
| 136 | + stream.writeFloat (getFeedback()); |
| 137 | + stream.writeFloat (getDryWet()); |
| 138 | + stream.flush(); |
| 139 | + |
| 140 | + return yup::Result::ok(); |
| 141 | + } |
| 142 | + |
| 143 | + bool hasEditor() const override { return false; } |
| 144 | + |
| 145 | + yup::AudioProcessorEditor* createEditor() override { return nullptr; } |
| 146 | + |
| 147 | + float getDelayLeftMilliseconds() const noexcept |
| 148 | + { |
| 149 | + return delayLeftMilliseconds.load (std::memory_order_relaxed); |
| 150 | + } |
| 151 | + |
| 152 | + float getDelayRightMilliseconds() const noexcept |
| 153 | + { |
| 154 | + return delayRightMilliseconds.load (std::memory_order_relaxed); |
| 155 | + } |
| 156 | + |
| 157 | + float getFeedback() const noexcept |
| 158 | + { |
| 159 | + return feedback.load (std::memory_order_relaxed); |
| 160 | + } |
| 161 | + |
| 162 | + float getDryWet() const noexcept |
| 163 | + { |
| 164 | + return dryWet.load (std::memory_order_relaxed); |
| 165 | + } |
| 166 | + |
| 167 | + void setDelayLeftMilliseconds (float newDelayMilliseconds) noexcept |
| 168 | + { |
| 169 | + delayLeftMilliseconds.store (limitDelayMilliseconds (newDelayMilliseconds), std::memory_order_relaxed); |
| 170 | + } |
| 171 | + |
| 172 | + void setDelayRightMilliseconds (float newDelayMilliseconds) noexcept |
| 173 | + { |
| 174 | + delayRightMilliseconds.store (limitDelayMilliseconds (newDelayMilliseconds), std::memory_order_relaxed); |
| 175 | + } |
| 176 | + |
| 177 | + void setFeedback (float newFeedback) noexcept |
| 178 | + { |
| 179 | + feedback.store (yup::jlimit (0.0f, maximumFeedback, newFeedback), std::memory_order_relaxed); |
| 180 | + } |
| 181 | + |
| 182 | + void setDryWet (float newDryWet) noexcept |
| 183 | + { |
| 184 | + dryWet.store (yup::jlimit (0.0f, 1.0f, newDryWet), std::memory_order_relaxed); |
| 185 | + } |
| 186 | + |
| 187 | +private: |
| 188 | + static constexpr float defaultDelayLeftMilliseconds = 250.0f; |
| 189 | + static constexpr float defaultDelayRightMilliseconds = 375.0f; |
| 190 | + static constexpr float maximumDelayMilliseconds = 2000.0f; |
| 191 | + static constexpr float maximumFeedback = 0.95f; |
| 192 | + |
| 193 | + static float limitDelayMilliseconds (float milliseconds) noexcept |
| 194 | + { |
| 195 | + return yup::jlimit (1.0f, maximumDelayMilliseconds, milliseconds); |
| 196 | + } |
| 197 | + |
| 198 | + double delayMillisecondsToSamples (float milliseconds) const noexcept |
| 199 | + { |
| 200 | + const auto sr = sampleRate.load (std::memory_order_relaxed); |
| 201 | + return (static_cast<double> (milliseconds) * static_cast<double> (sr)) / 1000.0; |
| 202 | + } |
| 203 | + |
| 204 | + std::atomic<float> sampleRate { 44100.0f }; |
| 205 | + std::atomic<float> delayLeftMilliseconds { defaultDelayLeftMilliseconds }; |
| 206 | + std::atomic<float> delayRightMilliseconds { defaultDelayRightMilliseconds }; |
| 207 | + std::atomic<float> feedback { 0.35f }; |
| 208 | + std::atomic<float> dryWet { 0.5f }; |
| 209 | + std::array<yup::FractionallyAddressedDelayFloat, 2> delayLines; |
| 210 | + std::array<float, 2> feedbackState {}; |
| 211 | +}; |
| 212 | + |
| 213 | +//============================================================================== |
| 214 | +class FractionalDelayNodeView final : public yup::AudioGraphNodeView |
| 215 | +{ |
| 216 | +public: |
| 217 | + FractionalDelayNodeView (yup::AudioGraphNodeID nodeID, FractionalDelayProcessor& processorIn) |
| 218 | + : AudioGraphNodeView (nodeID) |
| 219 | + , processor (processorIn) |
| 220 | + , delayLeftSlider (yup::Slider::LinearBarHorizontal) |
| 221 | + , delayRightSlider (yup::Slider::LinearBarHorizontal) |
| 222 | + , feedbackSlider (yup::Slider::LinearBarHorizontal) |
| 223 | + , dryWetSlider (yup::Slider::LinearBarHorizontal) |
| 224 | + { |
| 225 | + configureDelaySlider (delayLeftSlider, processor.getDelayLeftMilliseconds(), [this] (double value) |
| 226 | + { |
| 227 | + processor.setDelayLeftMilliseconds (static_cast<float> (value)); |
| 228 | + repaint(); |
| 229 | + }); |
| 230 | + |
| 231 | + configureDelaySlider (delayRightSlider, processor.getDelayRightMilliseconds(), [this] (double value) |
| 232 | + { |
| 233 | + processor.setDelayRightMilliseconds (static_cast<float> (value)); |
| 234 | + repaint(); |
| 235 | + }); |
| 236 | + |
| 237 | + configureUnitSlider (feedbackSlider, processor.getFeedback(), maximumFeedback, [this] (double value) |
| 238 | + { |
| 239 | + processor.setFeedback (static_cast<float> (value)); |
| 240 | + repaint(); |
| 241 | + }); |
| 242 | + |
| 243 | + configureUnitSlider (dryWetSlider, processor.getDryWet(), 1.0, [this] (double value) |
| 244 | + { |
| 245 | + processor.setDryWet (static_cast<float> (value)); |
| 246 | + repaint(); |
| 247 | + }); |
| 248 | + |
| 249 | + addAndMakeVisible (delayLeftSlider); |
| 250 | + addAndMakeVisible (delayRightSlider); |
| 251 | + addAndMakeVisible (feedbackSlider); |
| 252 | + addAndMakeVisible (dryWetSlider); |
| 253 | + } |
| 254 | + |
| 255 | + yup::String getNodeTitle() const override { return "FAD"; } |
| 256 | + |
| 257 | + int getNumInputPorts() const override { return 1; } |
| 258 | + |
| 259 | + int getNumOutputPorts() const override { return 1; } |
| 260 | + |
| 261 | + int getPreferredWidth() const override { return 260; } |
| 262 | + |
| 263 | + yup::Color getNodeColor() const override { return yup::Color (0xfff97316); } |
| 264 | + |
| 265 | + yup::String getNodeSubtitle() const override |
| 266 | + { |
| 267 | + return yup::String ("L ") + yup::String (processor.getDelayLeftMilliseconds(), 0) |
| 268 | + + " / R " + yup::String (processor.getDelayRightMilliseconds(), 0) + " ms"; |
| 269 | + } |
| 270 | + |
| 271 | + PortInfo getInputPortInfo (int) const override { return { "audio", getPortKindColor (PortKind::audio), PortKind::audio }; } |
| 272 | + |
| 273 | + PortInfo getOutputPortInfo (int) const override { return { "audio", getPortKindColor (PortKind::audio), PortKind::audio }; } |
| 274 | + |
| 275 | + int getNumParameterRows() const override { return 4; } |
| 276 | + |
| 277 | + ParameterInfo getParameterInfo (int parameterIndex) const override |
| 278 | + { |
| 279 | + switch (parameterIndex) |
| 280 | + { |
| 281 | + case 0: |
| 282 | + return { "L Time", millisecondsString (processor.getDelayLeftMilliseconds()), getPortKindColor (PortKind::parameter), delayNormalized (processor.getDelayLeftMilliseconds()), PortKind::parameter }; |
| 283 | + |
| 284 | + case 1: |
| 285 | + return { "R Time", millisecondsString (processor.getDelayRightMilliseconds()), getPortKindColor (PortKind::parameter), delayNormalized (processor.getDelayRightMilliseconds()), PortKind::parameter }; |
| 286 | + |
| 287 | + case 2: |
| 288 | + return { "Feedback", percentageString (processor.getFeedback()), getPortKindColor (PortKind::parameter), processor.getFeedback() / maximumFeedback, PortKind::parameter }; |
| 289 | + |
| 290 | + case 3: |
| 291 | + return { "Dry/Wet", percentageString (processor.getDryWet()), getPortKindColor (PortKind::parameter), processor.getDryWet(), PortKind::parameter }; |
| 292 | + |
| 293 | + default: |
| 294 | + return {}; |
| 295 | + } |
| 296 | + } |
| 297 | + |
| 298 | + void resized() override |
| 299 | + { |
| 300 | + delayLeftSlider.setBounds (NodeViewHelpers::getInlineSliderBounds (*this, getPreferredWidth(), 0)); |
| 301 | + delayRightSlider.setBounds (NodeViewHelpers::getInlineSliderBounds (*this, getPreferredWidth(), 1)); |
| 302 | + feedbackSlider.setBounds (NodeViewHelpers::getInlineSliderBounds (*this, getPreferredWidth(), 2)); |
| 303 | + dryWetSlider.setBounds (NodeViewHelpers::getInlineSliderBounds (*this, getPreferredWidth(), 3)); |
| 304 | + } |
| 305 | + |
| 306 | +private: |
| 307 | + static constexpr float maximumFeedback = 0.95f; |
| 308 | + |
| 309 | + template <typename Callback> |
| 310 | + void configureDelaySlider (yup::Slider& slider, float value, Callback&& callback) |
| 311 | + { |
| 312 | + NodeViewHelpers::configureParameterSlider (slider, getPortKindColor (PortKind::parameter)); |
| 313 | + slider.setRange (1.0, 2000.0, 1.0); |
| 314 | + slider.setSkewFactorFromMidpoint (250.0); |
| 315 | + slider.setValue (value, yup::dontSendNotification); |
| 316 | + slider.onValueChanged = std::forward<Callback> (callback); |
| 317 | + } |
| 318 | + |
| 319 | + template <typename Callback> |
| 320 | + void configureUnitSlider (yup::Slider& slider, float value, double maximum, Callback&& callback) |
| 321 | + { |
| 322 | + NodeViewHelpers::configureParameterSlider (slider, getPortKindColor (PortKind::parameter)); |
| 323 | + slider.setRange (0.0, maximum, 0.01); |
| 324 | + slider.setValue (value, yup::dontSendNotification); |
| 325 | + slider.onValueChanged = std::forward<Callback> (callback); |
| 326 | + } |
| 327 | + |
| 328 | + static float delayNormalized (float milliseconds) noexcept |
| 329 | + { |
| 330 | + return yup::jlimit (0.0f, 1.0f, (milliseconds - 1.0f) / 1999.0f); |
| 331 | + } |
| 332 | + |
| 333 | + static yup::String millisecondsString (float milliseconds) |
| 334 | + { |
| 335 | + return yup::String (milliseconds, 0) + " ms"; |
| 336 | + } |
| 337 | + |
| 338 | + static yup::String percentageString (float value) |
| 339 | + { |
| 340 | + return yup::String (value * 100.0f, 0) + "%"; |
| 341 | + } |
| 342 | + |
| 343 | + FractionalDelayProcessor& processor; |
| 344 | + yup::Slider delayLeftSlider; |
| 345 | + yup::Slider delayRightSlider; |
| 346 | + yup::Slider feedbackSlider; |
| 347 | + yup::Slider dryWetSlider; |
| 348 | +}; |
0 commit comments