{var delay = [2,3,5,7,11,13,17,19,23,29,31,37,41,43,47,53] ** 1.135 / 1000; var sig = SinOsc.ar(Line.kr(0,48000,1)); var sc = DelayC.ar(sig, K2A.ar(2), K2A.ar(delay)); RunningSum.rms(sc)}.plot(1.13)

{hhr, 200508}


If you take linear interpolation, then you would linearly blend two adjacent samples in the buffer with a weight determined by the delay time in sample frames modulus 1.0. So if you have a delay time that corresponds to an integer number of sample frames, then the weight for the two neighbouring buffer samples would be 1.0 and 0.0, and therefore "interpolation" has no audible effect. But if you take for example `2.pow(0.735) / 1000 * 48000 = 79,89` sample frames delay, then you have a non-integer frame offset and that explains indeed a kind of "low pass" filter. `DelayN` in this case would just round to an integer number of samples, so 79 or 80.

DelayN.ar(sig, 2, delay);

interpolation experiments

in non-standard FDN

0 Hz

DelayN.ar(sig, 2, delay);

                 interpolation and frequency response


Since the network is completely deterministic, meaning that there's no random generator involved, these different behaviors are due to the inteprolation algorithm. Still it wasn't clear to me why interpolation should matter if there's no time modulation involved. I found this discussion by Miller Puckette that explains the issue. Essentially:


Since they are in effect doing wavetable lookup, variable delay lines introduce distortion to the signals they operate on. Moreover, a subtler problem can come up even when the delay line is not changing in length: the frequency response, in real situations, is never perfectly flat for a delay line whose length is not an integer.

 


So, even if lengths don't change, it might still be useful to interpolate to achieve a better frequency response in this kind of situation. In my case here I'm not really interested in "good" or "bad" frequency responses, but more in understanding how a different frequency response affects the time development of the network. So I was curious to visualise the frequency response of DelayN and DelayC, for the exact lengths I'm using.

               no / linear / cubic


When translating network 2 into Faust I noticed in my SuperCollider code I'm using DelayC (delay lines with cubic interpolation). I thought this was a left over coming from another (older) version of the network, in which I was modulating the delay times and therefore interpolation was needed. Since in network 2 I'm not modulating the delays, I tried to switch from DelayC to DelayN (delay lines with no interpolation). But to my surprise, this radically modifies the behaviour of the network. Out of curiosity I also tried DelayL (linear interpolation), getting also another result (more similar to DelayC, but tuned lower and with shraper transients).

poz / 200501

 

in this page I'm collecting some experiments on how different interpolation algorithms might affect sound forms in feedback networks in which interpolated delay lines are part of the loop. The page functions also as a sort of "diary" for me not to loose track of the process.

 

The starting point is one of the four networks (I'll call it network 2) I'm planning to use in the Kunsthaus installation. The core of network 2 consists of 16 delay lines whose delay times are prime numbers, and which are then combined using a 16x16 hadamard matrix. This is a quite common technique in FDN reverberation to create rich, plausible reverbs. Nevertheless my intent here is not to write a good reverb, but rather to tweak this model in order to understand what affordances are contained in it, that might be useful for composing processes that exceed the scope of reverberation.

DelayN.ar(loc, 2, lengths);

 

DelayL.ar(loc, 2, lengths);

DelayC.ar(loc, 2, lengths);

nyquist

Even though the response of no interpolation might appear "more linear" if we zoom we see that the response of cubic interpolation is much more regular around Nyquist


So next I generated the Supercollider Ugen, and hoped that this naive implementation would do the trick. Unfortunately, this cubic interpolation is much more brutal than the one used in sc plugins.

  (naive) cubic interpolation in Faust


These slopes play a central role in both the tuning and time behaviour of the network. Therefore, if I want to maintain these characteristics in my Faust porting, I shall be sure to replicate the interpolation used in the sc plugin. In the same page I cited above I found a straightforward implementation for cubic interpolation, using the last four samples:


FaustDelayC.ar(loc, 2, lengths);

DelayC.ar(loc, 2, lengths);

DelayC.ar(sig, 2, delay);

FaustDelayC.ar(loc, 2, lengths);

48 kHz

I'm still puzzled by the slight discrepancy. So now I went to check whether this is visible looking at the individual samples. By polling and confronting values coming from DelayC and FaustDelayC I noticed values are exactly the same, but FaustDelayC has a /slight/ delay. Looking at the plot it is even more evident

sound results are also getting closer to the original behaviour

DelayC.ar(loc, 2, lengths);

The sound outcome, when substituting DelayC with FaustDelayC in the network, is once again different. With the faust naive interpolation the sound is a bit smoother, transitions a bit harsher, timbre less rich

The next thing I'll try is to implement in Faust the same cubic interpolation used in DelayC, namely "simplified spline cubic interpolation with fixed amt"

poz / 200502

 

found a very useful document about Polynomial Interpolators for High-Quality Resampling of Oversampled Audio.



                supercollider cubicinterp()


looking at the source code of DelayC, it is not using spline but 4-point, 3rd-order Hermite, whose implementation is defined in SC_SndBuf.h.

audacity confirms there is a 2 samples delay between DelayC and FaustDelayC. I wonder where this comes from, but I suspect this very small temporal offset is immensely amplified by the FDN, becoming audible in terms of different timbre and time development

Next, I'll try to trace back the origin of this delay. even tho I'm not sure it will be possible at all.. I also want to experiment with other interpolation algorithms, and maybe try to do a sort of chart that shows how different frequency responses affect the timbre and temporal behaviour of network 2

2

3

5

7

11

13

17

19

23

29

31

37

41

43

47

53

3


5


7


11

and generate the sc UGen for testing. Now the frequency response of my porting is almost identical to the original DelayC

0 Hz

DelayC.ar(sig, 2, delay);

let's do it faust

nyquist

{
    var delay = [2,3,5,7,11,13,17,19,23,29,31,37,41,43,47,53] ** 0.735 / 1000;
    var sig = SinOsc.ar(Line.kr(0,48000,1));
    var sc = DelayC.ar(sig, 2, delay);
    RunningSum.rms(sc)
}.plot(1.13)


{kind: code}

r = {
    arg decay = 211.9;
    var local = LocalIn.ar(16) + Impulse.ar(0);
    var loc = local+local.reverse;
    var room = [0.735];
    var lengths = [2,3,5,7,11,13,17,19,23,29,31,37,41,43,47,53]**room/1000;
    var delay = DelayN.ar(loc, 2, lengths);
    var walsh = delay;
    walsh = walsh * (decay*0.95);
    walsh = ~walsh.value(walsh);
    walsh = Compander.ar(walsh, walsh, 0.47, 1.0, LPF.ar(delay[1].abs, 2)/100, 0.0000015, 3.9);
    walsh = walsh.tanh;
    LocalOut.ar(LPF.ar(walsh, 20000));
    walsh = LeakDC.ar(walsh);
    Out.ar(0, Splay.ar(walsh));
}.play;



{kind: code}

FaustDelayC.ar(sig, 2, delay);

Translated into Faust code, this looks like:


DelayC.ar(sig, 2, delay);

48 kHz

{hhr, 200508}


You can implement a "non-causal" filter, i.e. an FIR that needs to look "two samples into the future" as a causal one by simply delaying the signal. My guess is one case (SC vs Faust) it's implemented with two samples delay, in the other case not (assuming zeroes for the initial two past values).

x is the fractional part of the delay in samples, and can be calculated like this:

{
    var delay = 2 * 0.735 / 1000;
    var sig = SinOsc.ar(Line.kr(0,48000,1));
    var sc = DelayC.ar(sig, 2, delay);
    var faust = FaustDelayC.ar(sig, K2A.ar(2), K2A.ar(delay));
    RunningSum.rms([sc, faust])
}.plot(1.13)



{kind: code}

import("stdfaust.lib");

sr = 48000;
phasor(length) = %(max(length,1)) ~_ +(1);

delayN(sig, length, del) = rwtable(sLength, 0.0, write, sig, read)
with{
    sLength = int(5*sr);
    write = int(phasor(sLength));
    read = int((sLength+phasor(sLength)-(del*sr))%sLength);
};

cubic(sig) = sig <: (-sig + 9*sig@1 + 9*sig@2 - sig@3) / 8;

process(sig, length, del) = delayN(sig, length, del) : cubic(_);



{kind: code}

---
meta: true
artwork: ThroughSegments

author: poz

keywords: [test, prototype, sound, rendering, experiment]
---

FaustDelayC.ar(sig, 2, delay);

DelayC.ar(sig, 2, delay);

{hhr, 200508}


What you get then is a 4-tap FIR filter with low-pass characteristics. The reason you are seeing a different frequency response than from DelayC, is that you did the interpolation for exactly 0.5 samples offset, while your delay time might have an offset of say 0.89. So the weights in the formula change depending on the fractional offset.