Skip to content

Commit b4d33c5

Browse files
committed
Add the delay in the audio graph
1 parent 0940791 commit b4d33c5

7 files changed

Lines changed: 449 additions & 12 deletions

File tree

Lines changed: 348 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,348 @@
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+
};

examples/audiograph/source/nodes/GainNode.h

Lines changed: 25 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -55,9 +55,31 @@ class GainProcessor final : public yup::AudioProcessor
5555

5656
void setPresetName (int, yup::StringRef) override {}
5757

58-
yup::Result loadStateFromMemory (const yup::MemoryBlock&) override { return yup::Result::ok(); }
58+
yup::Result loadStateFromMemory (const yup::MemoryBlock& data) override
59+
{
60+
if (data.isEmpty())
61+
return yup::Result::ok();
62+
63+
yup::MemoryInputStream stream (data, false);
64+
65+
const int version = stream.readInt();
66+
if (version != 1)
67+
return yup::Result::fail ("Unsupported gain node state version");
5968

60-
yup::Result saveStateIntoMemory (yup::MemoryBlock&) override { return yup::Result::ok(); }
69+
setGain (stream.readFloat());
70+
71+
return yup::Result::ok();
72+
}
73+
74+
yup::Result saveStateIntoMemory (yup::MemoryBlock& data) override
75+
{
76+
yup::MemoryOutputStream stream (data, false);
77+
stream.writeInt (1);
78+
stream.writeFloat (gain.load (std::memory_order_relaxed));
79+
stream.flush();
80+
81+
return yup::Result::ok();
82+
}
6183

6284
bool hasEditor() const override { return false; }
6385

@@ -119,7 +141,7 @@ class GainNodeView final : public yup::AudioGraphNodeView
119141

120142
void resized() override
121143
{
122-
gainSlider.setBounds (NodeViewHelpers::getInlineSliderBounds (*this, getPreferredWidth()));
144+
gainSlider.setBounds (NodeViewHelpers::getInlineSliderBounds (*this, getPreferredWidth(), 0));
123145
}
124146

125147
private:

0 commit comments

Comments
 (0)