digego / extempore

A cyber-physical programming environment
1.41k stars 127 forks source link

Question: dsp swap with longer cross-fade? #406

Open avdrd opened 2 years ago

avdrd commented 2 years ago

The supported way to swap a dsp in extempore is to bind-func it again. I could not find a way to do this with a longer cross-fade though.

In some other systems like SuperCollider it's pretty easy to have a Ndef cross-fade to another when its source is changed; there's even a \fadeTime parameter in SC (well in JITLib, but it ships with SC) that controls that cross-fade and one can even supply their own fading envelope with a bit more work. The default one is S-shaped for audio, i.e. sine a sine from 0..pi/2 for the fade-in source and its mirror, i.e. pi/2..pi, for the source being faded-out. For control-rate sources, a linear envelope is automagically used instead. As you can see, a fair bit of thinking went into designing that in SC, so it mostly works as expected "out of the box".

So how can I get an equivalent functionality in extempore, at least in terms of controlling a basic \fadeTime length when doing a bind-func for a dsp that's already bound?

digego commented 2 years ago

Hi,

There's no builtin way to do this directly with a bind-func . Indirectly this is possible but you're comparing apples and oranges here. In Extempore when you recompile a bind-func you are literally switching the executing code atomically - it's not a "high level audio node" in an audio signal chain that hangs around and can be cross-faded. Now, having said that, you could certainly build such a thing - but this would be running at a higher level of abstraction, it's not an inherent property of XTLang's general purpose functions.

Cheers, Andrew.

On Sat, Jan 1, 2022 at 10:16 PM avdrd @.***> wrote:

The supported way to swap a dsp in extempore is to bind-func it again. I could not find a way to do this with a longer cross-fade though.

In some other systems like SuperCollider it's pretty easy to have a Ndef cross-fade to another when its source is changed; there's even a \fadeTime parameter in SC (well in JITLib, but it ships with SC) that controls that cross-fade and one can even supply their own fading envelope with a bit more work (the default one is S-shaped for audio, i.e. sine a sine from 0..pi/2 for the fade-in source and its mirror, i.e. pi/2..pi, for the source being faded-out. For control-rate sources, a linear envelope is automagically used instead. As you can see, a fair bit of thinkinging went into designing that in SC, so it mostly work as expected "out of the box")

So how can I get an equivalent functionality in extempore, at least in terms of controlling a basic \fadeTime length when doing a bind-func for dsp that's already bound?

— Reply to this email directly, view it on GitHub https://github.com/digego/extempore/issues/406, or unsubscribe https://github.com/notifications/unsubscribe-auth/AAEHPKJENN7I5LQTZVET5WDUT3PAVANCNFSM5LCMLV3Q . Triage notifications on the go with GitHub Mobile for iOS https://apps.apple.com/app/apple-store/id1477376905?ct=notification-email&mt=8&pt=524675 or Android https://play.google.com/store/apps/details?id=com.github.android&referrer=utm_campaign%3Dnotification-email%26utm_medium%3Demail%26utm_source%3Dgithub.

You are receiving this because you are subscribed to this thread.Message ID: @.***>

avdrd commented 2 years ago

Fair enough. Even in SuperCollider such layering exists. The basic scsynth server doesn't quite have those notions of replacement with cross-fade, but only of node replacement, although it does have an XOut ugen that allows multiple "dsps" to effectively give the impression of running on the same "dsp slot", or rather on the same (share) bus in scsynth's case. I see Extempore doesn't seem to have this notion of shared busses at the lowest layer. I'm guessing it's not not hard to add such a shared-bus notion that's basically an array of functions all piping through the same dsp. I'm just a bit surprised that nobody using Extempore need this yet, i.e. I'd have expected you to point me to some Extempore library that does this. 🧀

monkey-w1n5t0n commented 2 years ago

I agree with Andrew; this comparison is not just unfair, it's impractical and unhelpful. bind-func is XTLang's low-level way of (re)compiling a function and binding it to a name (any function, not just DSP ones), while SuperCollider's JITlib is an entire library built on top of the fixed scsynth architecture that doesn't need to recompile anything (at least not to machine code, it compiles a SynthDef to a bytecode format that is then interpreted).

That being said, the crossfade functionality you're talking about is certainly both useful and possible; here's one way it could perhaps work:

Let's assume we have a (bind-func foo ...) which makes a sound. If we want to change its definition, recompile, and have the two sounds crossfade before the old is fully replaced by the new, then we need a few things:

  1. A "mixer" to handle the actual crossfade by mixing the two sounds. This will of course need to be another function, which will need to know when compilation of the new version has finished and it's ready to be called, and should also be able to free the old function after the crossfade has completed (otherwise we end up with an ever-growing "graveyard" of old functions in memory that we don't use anymore).
  2. Therefore, the mixer needs both versions of foo, old and new, to be available for the full duration of the crossfade. This means that it would need to keep some kind of distinct reference to both, otherwise it wouldn't be able to distinguish between them while they're both around. Perhaps they could be compiled anonymously as lambdas and stored as closure pointers inside current and new parameters of the mixer closure?
  3. But if the functions themselves replace one another, then how do other functions that are using foo for their own sound know that there has been a new version and that they should start listening to that instead? Well then, maybe foo should have been the name of the mixer all along, not the function(s). This way, other functions never have to be "updated" about any change, because from their perspective there isn't any: they only ever listen to the mixer's output. When there is no crossfade, the mixer simply returns the values from the latest lambda.
  4. But then the mixer does need to be updated: it needs to be provided the new lambda and be told to compile it and start the crossfade, with an optional argument as to how long that crossfade should be. So bind-func won't work for us here, because that would just replace the mixer object with a brand-new mixer.

We therefore need a new macro, which could be something like (bind-macro (bind-node name fade-time lambda) ...). When called, it would first check to see whether a mixer already exists under that name. If it doesn't (i.e. this is the first time we've introduced a node by the name foo), then it would create the mixer using bind-func and initialise it with the lambda passed in. If it does (i.e. we're re-defining the function that produces foo's sound), then it would just pass it the new lambda and ask it to start crossfading.

This sounds like a fun little project so I may take a stab at it soon. One thing I'm not sure about, is the call to bind-func synchronous? Also, is there a way to check whether a function exists under a certain name, e.g. something like (is-bound-func 'foo)?