Environment Variables in SuperDirt

Looking at the SuperDirt code I always see a lof of global variables (for example in the sound module). I am wondering how that is working, they are grouped in a separate environment I guess?

I don't think there are global variables (these would be the instance variables of classes), but do you mean environment variables, like ~dirt etc.?

Yes sorry, I mean environment variables not global.

Ok, yes - good question. An expression like ~something = 9; will use the environment you are currently in and set the key 'something' to 9. You could e.g. make an environment e = Environment.new and then write e.use { ~something = 9 }. Then e` would carry this value.

There is one (now really) global variable in the interpreter that is called currentEnvironment. It is where stuff is kept by default when you use statements like ~something = 9; or ~dirt = .... You can switch it out, e.g. like this: p = currentEnvironment; e = Environment.new; currentEnvironment = e, and restore it later. But mostly it works like a shared environment which you can work in.

2 Likes

Thanks!
So coming back at my original question, where are these variables stored for example? (~buffer, ~sustain, ~freq....)

I can't find any of them in the currentEnvironment.
When I first started studying SuperDirt code I founded puzzling that there was such a use of environment variables cause it looks something that could break any moment (according to my understanding at least). So I am sure there is something I am missing in how this variables are behaving, where they are stored, and from where you can access them.

Ah now I see, this is a good question. The expressions like ~sustain ... are references to the current environment, which here is the specific event which is just assembled. So in this particular case, we are in the method playSynths in DirtEvent, where the module functions are called. If you would call this method "by hand", it would look for these values in your current environment. But playSynths is called in DirtEvent's play method. It looks like this:


	play {
		event.parent = orbit.defaultParentEvent;
		event.use {
			// s and n stand for synth/sample and note/number
			~s ?? { this.splitName };
			// unless orbit wide diversion returns something, we proceed
			~diversion.(this) ?? {
				this.mergeSoundEvent;
				server = ~server.value; // as server is used a lot, make lookup more efficient
				this.orderTimeSpan;
				this.calcTimeSpan; // ~sustain is called here
				this.finaliseParameters;
				// unless event diversion returns something, we proceed
				~play.(this) ?? {
					if(~sustain >= orbit.minSustain) { this.playSynths }; // otherwise drop it.
				}
			}
		}
	}

So you can see that it wraps the function calls in an event.use { ... }, where an event is just a special kind of environment. And in the line above you see that the event in question gets specified with default values from orbit.defaultParentEvent. Every orbit has a prototype. Currently the event itself is made in OSC responder that receives the message from tidal (see in SuperDirt the connect method), but as I explain this here, I notice that it would be better to move that part to the orbit.

The important thing is that environment variables implement dynamic scope so that their value depends on the execution context (that is where you call that function). This needs to be handled with care of course, but it is a good way to be very general (Lisp and java script have been using this a lot). It is useful e.g. when you want to have many different functions contribute to one object whose values need not be predefined: anyone could add a module with new parameters, and it would just work.

1 Like

This is super interesting!
I'm still struggling a bit to connect all the classes and methods in action, but I understand the logic and it's very fascinating.
I attach a mini example that isolates the concept that could be helpful for future reference.

({
e = (ciao: 12, pizza: 13); // an event
	e.use { ~ciao + ~pizza}.value.postln;
	
b = (); b.parent = e; // setting e as the parent of b makes b inherit things stored in e
	b.use {~pizza - ~ciao}.value;
}.value)

~pizza //this is empty
e.pizza // this holds the value

great!
You just have to take care that the pseudo method pizza is fine (like in e.pizza), but if you have a method that already exists in Event (or the long chain of superclasses), like e.g. play, then this won't override that method. So for the demonstration it is safer to write e[\pizza] instead.

1 Like

yes, don't worry – it is super dirt after all :slight_smile:

1 Like

So coming back at my original question, where are these variables stored for example?

This is likely already clear from Julian's answers, but below are some further links:

(Apologies separate message, there is a link counter and I can only write two...)

Anyways, thanks for the nice discussion! (It prompted me to add an 'Environment' section to the 'Terse Guide', which was missing one: https://gitlab.com/rd--/stsc3/-/blob/master/help/terse/terse.scd#L307)

1 Like