Set default of maxdelaytime = nil for delay ugens

Currently, one has to set both the maxdelaytime and the delaytime in delay ugens, even if the delay won’t ever change. Because of that, I suggest changing the default maxdelaytime in all delay ugens to nil. And then, and adding a check when the ugen is created, so that if the value of that parameter is nil, it takes the value of delaytime.
All previous code will remain probably remain unaffected. Only if the max delay time wasn’t specified, the delay time was smaller than 0.2 at initialization and then it increased to a bigger number (but yet smaller than 0.2) that would be affected. This case isn’t very probable though; users would want to specify the maximum delay time when the delaytime is changing for clarity, and even the order of the arguments kind of enforces that.

If the delay time is not a constant, should this throw an error?

hjh

Yes, it is a good idea.

Agree that typing both maxdelaytime and delaytme for a fixed delay time is an annoyance.

The change would only makes sense in DelayN since DelayL and DelayC are only ever used when delay times modulate.

The annoyance in the proposed fix is still having to tab past the ‘nil’ value for ‘maxdelaytime’ or go to keyword args at delaytime:. And it is unclear what that nil means if you haven’t read the docs.

A better design would have been to express delaytime as a proportion of maxdelaytime and have delaytime default to 1. As it is now, when you want to scale a modulating delay you need to scale both args which is annoying.

But maybe the simplest fix would be to add a Delay UGen for fixed delay values?

That’s not guaranteed to be true.

That’s a large breaking change. Also, I’m not sure I understand what you mean by modulating maxdelaytime, since that’s not modulatable by definition.

hjh

1 Like

Also AllpassN, CombN

Too bad that when the functions were created maxdelaytime was put before delaytime… I personally don’t mind though as in this particular case I like having the arguments typed out for clarity. If delaytime had been the second argument I wouldn’t probably use the keyword.

That sounds unusual to me, I don’t know of any audio app interface where the delay time is specified as a fraction of the max delay time.

Another option (taking a bit of a cue from Design Patterns) is to create an adapter or wrapper around object construction: translate your preferred way of expressing it into the way that the class expects.

For example:

(
~makeDelay = { |delayClass, sig, delayTime, decayTime, maxDelayTime|
	var rateSelector = UGen.methodSelectorForRate(sig.rate);
	var metaclass = ("Meta_" ++ delayClass.name.asString).asSymbol.asClass;
	var method = metaclass.findRespondingMethodFor(rateSelector);
	
	if(maxDelayTime.isNil) {
		if(delayTime.asArray.every(_.isNumber)) {
			maxDelayTime = delayTime
		} {
			Error("Must specify maxDelayTime for a modulating delay time").throw;
		};
	};
	
	if(method.argNames.includes(\decaytime)) {
		delayClass.perform(
			rateSelector,
			sig, maxDelayTime, delayTime, decayTime
		)
	} {
		delayClass.perform(
			rateSelector,
			sig, maxDelayTime, delayTime
		)
	}
};
)

a = {
	var trig = Dust.kr(8);
	var eg = Decay2.kr(trig, 0.01, 0.2);
	var sig = (eg * 0.1) * SinOsc.ar(TExpRand.kr(200, 800, trig));

	// didn't write the maxdelaytime *wink*
	var dly = ~makeDelay.(CombC, sig, [0.75, 1.25], 7);

	sig + (0.7 * dly)
}.play;

a.free;

In my ddwChucklib quark, I have a storage class for arbitrary functions, so I would write { ... that whole function ... } => Func(\makeDelay), and then I wouldn’t need to worry about environment variable access in the synthesis function: just \makeDelay.eval( ... args ... ).

Or you could create a class for it.

It occurred to me that the dynamic in this thread, and in a couple of other threads recently, is: “Here is something I don’t like. Let’s talk about how to change it for everybody.” If implemented, this would of course have the advantage of improving the interface going forward. Disadvantages may include broken backward compatibility (here, if the argument order doesn’t change, it wouldn’t break compatibility), and also that you don’t get the benefit unless you can build consensus and get the change put in (whereas a “factory” function, you can do immediately without anyone’s permission).

Factories, adapters, wrappers etc. are probably under-used in SC.

hjh

1 Like

I think, in this case, there is an opportunity for a optimized UGen (in theory much faster, in practice, only god knows). Not only reduces computations and branching to manage the internal buffer indices and positions, but I think (experts could confirm this ? please?) SIMD optimizations would be more effective without irregular memory access.

more or less like this?

void FixedDelay_next(FixedDelay* unit, int inNumSamples) {
    float* out = OUT(0);
    const float* in = IN(0);
    float* buf = unit->buffer;
    int bufSize = unit->bufferSize;
    int writePos = unit->writePos;
    int delaySamples = unit->delaySamples;

    int i = 0;
    // Process samples in chunks of 4 
    for (; i <= inNumSamples - 4; i += 4) {
        int readPos = (writePos - delaySamples + bufSize) % bufSize;
        __m128 bufVec = _mm_loadu_ps(buf + readPos);
        _mm_storeu_ps(out + i, bufVec);
        __m128 inVec = _mm_loadu_ps(in + i);
        _mm_storeu_ps(buf + writePos, inVec);
        writePos = (writePos + 4) % bufSize;
    }

  /// .... 

    unit->writePos = writePos;
}

Thanks for all that code! :slight_smile:

All the topics I have opened for now have been indeed like that :v

And that’s good, right?

I know backward compatibility is very important. Because of that, I am only making suggestions that, for what I know about SuperCollider (and I am a newcomer here) don’t break anything, and I back off if a more knowledgeable person tells me they do.

Yes, that’s true. Trying to get changes done in the core program consumes much more time and energy than just making the changes for my environment. I do think it is worth the effort for me if it’s going to improve the consistency and/or the ease of use of the language without breaking anything.

1 Like

I do this… Because I know the default, if there’s no need to explicitly set it I don’t. This change would certainly cause problems in my old code

For maximum compatibility and to ensure nothing breaks, the ‘maxdelaytime’, when it is ‘nil’, could be set to the maximum between 0.2 and the value ‘delaytime’. That will allow to use the function with modulating delay time under 0.2.
Then, if the user tries to modulate:

  • if the new ‘delaytime’ is greater than 0.2, an error is thrown.
  • if it’s smaller, throw a warning (does supercollider allow for that?). I think that for delays with modulating time, specifying the ‘maxdelaytime’ should be encouraged; the opposite makes for not-so-clear code, as 0.2 is an arbitrary value).

@julian recently pointed out to me that argument names can be used when specifying argument defaults as expressions in the argument list.

So an interesting solution might look something like:

DelayN : PureUGen {

	*ar { arg in = 0.0, maxdelaytime = 0.2, delaytime = (maxdelaytime), mul = 1.0, add = 0.0;
		^this.multiNew('audio', in.asAudioRateInput, maxdelaytime, delaytime).madd(mul, add)
	}
...

This perfectly addresses the case at hand:

You can just then write

DelayC.ar(in, 0.123)

because delaytime defaults to maxdelaytime. This default structure means that in the case of a fixed delay, these arguments “interchangeable”.

I haven’t looked further into how the UGen implementation would respond to a modulating input to maxdelaytime in this case, but I assume it takes the signal’s initial value (Edit: yep). The argument order unfortunately makes this a “gotcha” for beginners.

But, when arguments are properly specified, I think this satisfies all use cases, including a modulating delaytime (behavior is same as currently), and even this one:

A related optimization would be to catch an i-rate delaytime , force maxdelaytime to match it, so only the required memory is allocated.

One minor downside is that when specifying arg defaults with expressions is that they don’t show up in help docs or auto-complete popups. But that’s an issue with those utils, and shouldn’t prevent using the feature, especially if it’s an improvement on the current setting :slight_smile: