You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
I don't know if you've already have this idea ? Long time ago, I've work on my own implementation of a waveguide (bi-directional, / multi-dimensionnal) that is an extension of the Julis Orion Smith historical implementation. It's not very modern but it's efficient and extensible. The code is not framework ready but that can be a starting point if you want to integration this kind of feature in your framework.
The main class is waveguide.hpp and consist of a multi-dimensionnal bi-directional two-rails (delaylines) with random access and two terminations :
#pragma once
#include<algorithm>
#include<vector>
#include<delay.hpp>
#include<lagrange.hpp>namespacedwg {
template <typename T, size_t dims = 1, typename interpolation = details::lagrange<T, 5>>
classwaveguide
{
public:waveguide(double size)
: upper(size * 0.5)
, lower(size * 0.5)
{}
constexprsize_tsize()
{
return upper.size();
}
constexprvoidset(double position, T value, size_t dim = 0)
{
auto up = upper.size(dim) * position;
auto lp = lower.size(dim) - up;
upper.set(up, value * 0.5, dim);
lower.set(lp, value * 0.5, dim);
}
constexpr T get(double position, size_t dim = 0)
{
auto up = upper.size(dim) * position;
auto lp = lower.size(dim) - up;
returndc(upper.get(up, dim)
+ lower.get(lp, dim));
}
constexprvoidfill(std::vector<T> values, size_t dim = 0)
{
auto normalized = std::transform(values.begin(), values.end(), [](auto& x) { return x * 0.5; });
upper.fill(normalized, dim);
std::reverse(values.begin(), values.end());
lower.fill(normalized, dim);
}
constexprvoidmove()
{
#pragma simd
for (auto dim = 0; dim < dims; ++dim)
{
auto up = upper.out(dim);
auto lo = lower.out(dim);
upper.in( LT ( lo, dim ), dim);
lower.in( RT ( up, dim ), dim);
}
}
protected:
details::delay<T, dims, interpolation, details::forward> upper; // fractional delay
details::delay<T, dims, interpolation, details::backward> lower; // fractional delayprivate:structdcremover
{
inline T operator()(T value)
{
static T n = 1;
static T mean = 0;
mean += value;
return value - (mean / n++);
}
};
dcremover dc;
private:virtual T LT (T value, size_t dim) = 0; // left terminationvirtual T RT (T value, size_t dim) = 0; // right termination
};
} // namespace dwg
the delay.hpp is a simple old-school shared aligned memory with moving-head-pointer, the code include the well-know crossfade-trick that allow fast & click-free resize of the delay as experimental feature (fixed period / samplerate in constructor).
#pragma once
#include<crossfade.hpp>
#include<lagrange.hpp>
#include<memory.hpp>
#include<pointer.hpp>namespacedwg {
namespacedetails {
template <typename T, size_t dims, typename interpolation = lagrange<T, 5>, size_t direction = forward>
classdelay : publicinterpolation, publiccrossfade<T, dims>
{
public:usingPtr = pointer<T, interpolation, direction>; // read/write head (oldschool moving-pointer trick)public:delay(double size, size_t max_size = 1024)
: crossfade<T>(std::round(44.1 * 28)) // crossfade of 28 ms @ 44100 hz
{
for (auto dim = 0; dim < dims; ++dim)
{
data[dim] = head[dim] = (T*)mem::aligned_alloc(1024, max_size * sizeof(T));
std::memset(data[dim], 0, max_size * sizeof(T));
cur_ptr[dim] = newPtr(data[dim], size, max_size, order);
nxt_ptr[dim] = newPtr(data[dim], size, max_size, order);
}
}
~delay()
{
for (auto dim = 0; dim < dims; ++dim)
{
delete cur_ptr[dim];
delete nxt_ptr[dim];
mem::free(data[dim]);
}
}
constexprsize_tsize(size_t dim = 0)
{
return cur_ptr[dim]->size;
}
constexprvoidresize(double new_size, size_t dim = 0, bool smooth = true)
{
if (smooth)
{
nxt_ptr[dim]->resize(new_size);
crossfade<T>::reset(dim);
}
else
cur_ptr[dim]->resize(new_size);
}
constexprvoidin(T value, size_t dim = 0)
{
if (crossfade<T>::running())
{
interpolation::deinterpolate(cur_ptr[dim], head[dim], 0, value);
interpolation::deinterpolate(nxt_ptr[dim], head[dim], 0, value);
}
else
{
interpolation::deinterpolate(cur_ptr[dim], head[dim], 0, value);
}
}
constexpr T out(size_t dim = 0)
{
head[dim] = cur_ptr[dim]->move(head[dim]);
return (crossfade<T>::running())
? crossfade<T>::process
(interpolation::interpolate(cur_ptr[dim], head[dim], cur_ptr[dim]->size),
interpolation::interpolate(nxt_ptr[dim], head[dim], nxt_ptr[dim]->size))
: interpolation::interpolate(cur_ptr[dim], head[dim], cur_ptr[dim]->size);
}
constexprvoidset(double position, T value, size_t dim = 0)
{
if (crossfade<T>::running())
{
interpolation::deinterpolate(cur_ptr[dim], head[dim], cur_ptr[dim]->size * position, value);
interpolation::deinterpolate(nxt_ptr[dim], head[dim], nxt_ptr[dim]->size * position, value);
}
else
{
interpolation::deinterpolate(cur_ptr[dim], head[dim], cur_ptr[dim]->size * position, value);
}
}
constexpr T get(double position, size_t dim = 0)
{
return (crossfade<T>::running(dim))
? crossfade<T>::process
(interpolation::interpolate(cur_ptr[dim], head[dim], cur_ptr[dim]->size * position),
interpolation::interpolate(nxt_ptr[dim], head[dim], nxt_ptr[dim]->size * position), dim)
: interpolation::interpolate(cur_ptr[dim], head[dim], cur_ptr[dim]->size * position);
}
constexprvoidfill(std::vector<T> const& values, size_t dim = 0)
{
int size = values.size();
for (auto i = 0; i < size; ++i)
cur_ptr[dim]->set(head[dim], i, values[i]);
}
private:
T* data[dims]; // shared aligned memory
T* head[dims]; // shared pointer// Legato trick (smooth resize)Ptr* cur_ptr[dims]; // current notePtr* nxt_ptr[dims]; // next note to crossfade// alternate crossfade trick (see. crossfade.hpp)voidcomplete(size_t dim = 0) override
{
// just swap pointersauto temp = cur_ptr[dim];
cur_ptr[dim] = nxt_ptr[dim];
nxt_ptr[dim] = temp;
}
constexprvoidmove(int dim = -1)
{
ifconstexpr (dim >= 0)
{
head[dim] = cur_ptr[dim]->move(head[dim]);
}
else
{
#pragma simd
for (auto d = 0; d < dims; ++d)
head[d] = cur_ptr[d]->move(head[d]);
}
}
};
} // namespace details
} // namespace dwg
the lagrange.hpp is a underrated technique of interpolation/deinterpolation that allow the random access over the delay line at any fractional point. This is a N-order Lagrange Interpolation based on faust code with some trick from some scientific litterature. Default is 5th order initialized by the template parameters.
#pragma once
#include<pointer.hpp>namespacedwg {
namespacedetails {
template <typename T, size_t N>
classlagrange
{
public:usingPtr = pointer<T, lagrange<T, N>>;
public:staticinline T fractional(T size)
{
auto o = (double(N) - 1.00001) * 0.5; // ~ center FIR interpolatorauto dmo = size - o; // assumed >=0 [size > (N-1)/2]return o + (dmo - std::floor(dmo)); // fractional part
}
staticinlinevoidcoefficients(double d, std::vector<T>& h)
{
h.resize(N+1);
std::fill(h.begin(), h.end(), 1);
std::vector<size_t> n(N);
std::iota(n.begin(), n.end(), 0);
for (auto k = 0; k < N+1; ++k)
{
auto idx = std::find_if(n.begin(), n.end(), [k](auto x) { return x != k; });
for (auto* i : idx)
h[i] = h[i] * (d - k) / (n[i] - k);
}
}
staticinline T interpolate(Ptr* ptr, T* head, int index)
{
auto out = T(0);
for (auto i = 0; i < N+1; ++i)
out += ptr->get(head, index-i) * ptr->fir(i);
return out;
}
staticinlinevoiddeinterpolate(Ptr* ptr, T* head, int index, T value)
{
for (auto i = 0; i < N+1; ++i)
ptr->add(head, index-(N-i), value * ptr->fir(i));
}
};
} // namespace details
} // namespace dwg
the pointer.hpp is a naive implementation of a bidirectional moving pointer allowing to selected the 'true' propagation direction. This is the most efficient way to simulate signal move inside a delay-line and this is a special case (bidirectional) for me to keep the code logical and coherent with the theory.
the crossfade.hpp is using to state-crossfader moving around a lookup-table of a S-shape curve (sigmoid) and is used for the 'legato' trick.
#pragma once
#include<vector>namespacedwg {
namespacedetails {
template <typename T, size_t dims = 1>
classcrossfade
{
public:crossfade(size_t period)
: counter(0)
{
auto sigmoid = [](T x, T a = 0.787) -> T
{
constexpr T epsilon = 0.0001;
constexpr T min_param_a = 0.0 + epsilon;
constexpr T max_param_a = 1.0 - epsilon;
a = std::max(min_param_a, std::min(max_param_a, a));
a = (T(1) / (T(1) - a) - T(1));
// "Logistic" Sigmoid function
T A = 1.0 / (1.0 + exp(0 - ((x - 0.5)*a*2.0)));
T B = 1.0 / (1.0 + exp(a));
T C = 1.0 / (1.0 + exp(0 - a));
T y = (A - B) / (C - B);
return y;
};
// optimized S-shaped curve
curve.resize(period+1);
std::fill(curve.begin(), curve.end(), 1);
auto ratio = 1.0 / period;
for (auto i = 0; i < period; ++i)
curve[i] = sigmoid(ratio * i);
}
inlinevoidreset(size_t dim = 0)
{
counter[dim] = curve.size()-1;
}
inlineboolrunning(size_t dim = 0)
{
return counter[dim] > 0;
}
inline T process(T A, T B, size_t dim = 0)
{
auto ratio = curve[step(dim)];
return (A * ratio)
+ (B * (1.0 - ratio));
}
virtualvoidcomplete(size_t dim = 0) = 0;
private:
std::vector<T> curve; // S-shaped curveint counter[dims]; // crossfade evolution counterinlinesize_tstep(size_t dim = 0)
{
if (--counter[dim] < 0)
complete();
return (counter[dim] < 0) ? 0 : counter[dim];
}
};
} // namespace details
} // namespace dwg
To finish a memory.hpp helpers is here to workaround the aligned_memory for microsoft visual studio compiler. Don't know if it's always pertinent since latest VS2019 version includes the std::aligned_memory method.
#pragma once
#include<memory>
#include<type_traits>// Since Visual Studio C++17 doesn't implement std::aligned_alloc// just hack the std namespace for the Microsoft equivalent methods
#ifdef _MSC_VER
namespacemem
{
inlinevoid* aligned_alloc(std::size_t alignment, std::size_t size)
{
return_aligned_malloc(size, alignment);
}
inlinevoidfree(void* ptr)
{
_aligned_free(ptr);
}
}
#elsenamespacemem
{
inlinevoid* aligned_alloc(std::size_t alignment, std::size_t size)
{
returnstd::aligned_malloc(alignment, size);
}
inlinevoidfree(void* ptr)
{
std::free(ptr);
}
}
#endif// MSVC
Now, how to use the classes ? The most simples example is the JOS digital waveguide example (string.hpp) that is a simple bidirectional waveguide with a simple one-pole lowpass at right termination.
And this is a very basic code to use it (main.cpp):
#include<iostream>
#include<string.hpp>template <typename T>
structquadratic
{
quadratic(T x1, T y1, T x2, T y2, T x3, T y3)
: x1(x1), y1(y1)
, x2(x2), y2(y2)
, x3(x3), y3(y3)
{}
inline std::tuple<T, T> operator()(T perc)
{
auto xa = pt(x1, x2, perc);
auto ya = pt(y1, y2, perc);
auto xb = pt(x2, x3, perc);
auto yb = pt(y2, y3, perc);
auto x = pt(xa, xb, perc);
auto y = pt(ya, yb, perc);
return { x, y };
}
private:
T x1; T y1; // P0
T x2; T y2; // P1
T x3; T y3; // P2inline T pt(T n1, T n2, T perc)
{
auto diff = n2 - n1;
return n1 + (diff * perc);
}
};
template <typename T>
static std::vector<T> get_initial_displacement(size_t rail_length, double pick, double amplitude = 1.0)
{
auto quad = quadratic<T>(0.f, 0.f, pick, 2.f, 1.f, 0.f);
float ratio = 1.f / rail_length;
std::vector<T> initial_shape(rail_length);
for (auto i = 0; i < rail_length; ++i)
initial_shape[i] = std::get<1>(quad(ratio * i) * amplitude);
return initial_shape;
}
inlineintsamples_for_ms(double ms, double fs = 44100)
{
return (fs/ 1000) * ms;
}
static std::vector<double> test_string(size_t num_samples, double position)
{
// theorical dual-polarization string
dwg::string<double, 2> str (330.5409);
// just generate a initial displacement centred at 28% left of the string sizeauto buffer = get_initial_displacement<double>(str.size(), position);
// TODO: change the vertical polarization to be effective
str.fill(buffer, 0);
str.fill(buffer, 1);
// main process
std::vector<double> buffer;
for (auto i = 0; i < num_samples; ++i)
{
auto hor = str.get(position, 0);
auto ver = str.get(position, 1);
str.move();
buffer.push_back(hor + ver);
}
return buffer;
}
intmain()
{
auto samples = test_string(samples_for_ms(5000), 0.28);
return0;
}
OK, this is a old code that take dust in my hard-drive since long date. I hope that will inspire you, at least for future plugins, at best for a rewrite and an inclusion in your framework. In any case, it's shared and it makes me happy.
Sincerely,
Max
The text was updated successfully, but these errors were encountered:
Thanks for sharing, yeah this looks super useful! I'd love to integrate the delay line and interpolation code into what I've already got in this repo, so it may take a little time to work out exactly how to integrate everything, but I do plan to do it.
I don't know if you've already have this idea ? Long time ago, I've work on my own implementation of a waveguide (bi-directional, / multi-dimensionnal) that is an extension of the Julis Orion Smith historical implementation. It's not very modern but it's efficient and extensible. The code is not framework ready but that can be a starting point if you want to integration this kind of feature in your framework.
The main class is
waveguide.hpp
and consist of a multi-dimensionnal bi-directional two-rails (delaylines) with random access and two terminations :the
delay.hpp
is a simple old-school shared aligned memory with moving-head-pointer, the code include the well-know crossfade-trick that allow fast & click-free resize of the delay as experimental feature (fixed period / samplerate in constructor).the
lagrange.hpp
is a underrated technique of interpolation/deinterpolation that allow the random access over the delay line at any fractional point. This is a N-order Lagrange Interpolation based on faust code with some trick from some scientific litterature. Default is 5th order initialized by the template parameters.the
pointer.hpp
is a naive implementation of a bidirectional moving pointer allowing to selected the 'true' propagation direction. This is the most efficient way to simulate signal move inside a delay-line and this is a special case (bidirectional) for me to keep the code logical and coherent with the theory.the
crossfade.hpp
is using to state-crossfader moving around a lookup-table of a S-shape curve (sigmoid) and is used for the 'legato' trick.To finish a
memory.hpp
helpers is here to workaround the aligned_memory for microsoft visual studio compiler. Don't know if it's always pertinent since latest VS2019 version includes the std::aligned_memory method.Now, how to use the classes ? The most simples example is the JOS digital waveguide example (
string.hpp
) that is a simple bidirectional waveguide with a simple one-pole lowpass at right termination.And this is a very basic code to use it (
main.cpp
):OK, this is a old code that take dust in my hard-drive since long date. I hope that will inspire you, at least for future plugins, at best for a rewrite and an inclusion in your framework. In any case, it's shared and it makes me happy.
Sincerely,
Max
The text was updated successfully, but these errors were encountered: