Parallel Routines, or how I keep losing the ability to interact with the Server

I’ve been exploring @nathan’s tracker technique recently, also as documented recently in this live stream. I’ve got things working smoothly with only one voice playing back at a time.

What I’d like to do, though is have multiple voices, with independent play and stop control, going in parallel, and sync up the starting points of the loops. I haven’t yet been able to get this to work reliably, though I’ve gotten two voices simultaneously, though within the same Routine, and even that has been kind of flaky.

I seem to keep causing the server to become unresponsive, losing the ability to interact with it, hear sound, actually get Synths to play back etc. It fails silently though, because I can’t kill all nodes with cmd + . and client side calculations continue to work as if there were nothing wrong, but no error messages, no unresponsiveness from the interface or locking up of anything…

I’ve read through the a bunch of help files, tutorials related to Routines and Streams, and various threads on here, but have seen no examples of several independent Routines running simultaneously, and no mention of doing it, or not doing it. This leads me to believe that it’s super obvious that it can’t be done without doing something else.

In reading, also I think I understand that a Routine becomes a Stream, and that there’s only one Stream in SuperCollider.This means that I can’t evaluate one block of code that’s a looping Routine, and then after evaluate another.

So far the best I’ve been able to do is use

a = s.makeBundle(nil, firstRoutine.play(quant: 1));
b = s.makeBundle(nil, secondRoutine.play(quant: 1), a);

to mostly work, however, while I’m able to quantize to the clock, I’m not able to actually sync the start of the loops reliably. I’m hoping maybe to stick some sort of quantization in place of (one of those) nils, but haven’t seen anything explicitly documented. Though, at this point, this is less of a concern – I haven’t had a chance to attempt to solve for that in earnest yet, as I keep haven’t to restart the software.

Also adding c = s.makeBundle(nil, thirdRoutine(quant: 1), b) to this seems to cause trouble. Causing the unresponsiveness, though not always immediately, but definitely eventually. Sometimes it’s on playback, other times it’s after xRoutine.stop, and reinitializing all the code again…

Is there a more reliable way to do this?

When losing the ability to interact with the Server like this, is there a way to get it back? I’ve tried s.reboot, which just seems to hang, I’ve tried "killall scsynth".unixCmd, which also doesn’t fix it, or allow me to reconnect. So I’ve been having to quit and restart the IDE.

And generally, everything seems brittle, for lack of a better word. Starting and stopping things a few times seems sort of bound to lock up the Server like this, though while that’s fairly predictable, exactly when isn’t. (Or of course, for me, why).

Is there a way to run multiple Routines like this? Or Tasks? I’m using the two classes somewhat interchangeably in this post. Eg, if it can be done with one but not the other, I’d be fine with that.

I’m hoping for the ability to not have to preplan so much, so unless there’s a simple way to add additional data into an already running Routine, it doesn’t seem practical for me to run all these parallel voices within one Routine capable of handling the entire parallel load. … It’s be great to keep playing without having to stop the sound.

Thanks!

Code below:

(
// initialization:
SynthDef(\kik, { |freq = 57, amp = 0.5|
	var sig, env;
	env = Env.perc(0.001, 0.5).ar(2);
	sig = SinOsc.ar(XLine.kr(freq * 3.03, freq, 0.03));
	sig = sig + (Decay.ar(Impulse.ar(0), 1) * 0.5).tanh;
	sig = sig * env * amp;
	OffsetOut.ar(0, sig ! 2);
}).add;

SynthDef(\snr, { |freq = 177, amp = 0.5|
	var sig, env;
	env = Env.perc(0.007, 0.25).ar(2);
	sig = LFTri.ar(XLine.kr(305, freq, 0.02), 2) * 0.2;
	sig = sig * Env.perc(0.001, 0.2).kr;
	sig = sig + (HPF.ar(PinkNoise.ar, freq * 0.5) * 0.7);
	sig = sig * env * amp;
	OffsetOut.ar(0, sig ! 2);
}).add;

SynthDef(\hat, { |freq = 1400, amp = 0.5|
	var sig, env;
	env = Env.perc(0.01, 0.4).ar(2);
	sig = LPF.ar(HPF.ar(WhiteNoise.ar, freq), freq * 2);
	sig = sig * env * amp;
	OffsetOut.ar(0, sig ! 2);
}).add;

~map = IdentityDictionary[$k -> \kik, $s -> \snr, $h -> \hat];
~beat = 0.5;

// simple testers
~rhythm1 = "k.";
~rhythm2 = "h.";
~rhythm3 = ".s";


a = Routine({
	loop({
		~rhythm1.do({ |char|
			char.postln;
			if(~map[char].notNil) {
				Synth(~map[char]);
				~beat.wait;
			} {
				~beat.wait;
			}
		})
	})
});

b = Routine({
	loop({
		~rhythm2.do({ |char|
			char.postln;
			if(~map[char].notNil) {
				Synth(~map[char]);
				~beat.wait;
			} {
				~beat.wait;

			}
		})
	})
});

c = Routine({
	loop({
		s.makeBundle(s.latency, {
			~rhythm3.do({ |char|
				char.postln;
				if(~map[char].notNil) {
					Synth(~map[char]);
					~beat.wait;
			} {
					~beat.wait;

			}
			})
		});
	})
});
)

// run this:
d = s.makeBundle(nil, a.play(quant: 1))
// then this:
e = s.makeBundle(nil, b.play(quant: 1), d)
// for me that works, other than reliably being able to sync both loops

// running the below usually causes things to go haywire:
f = s.makeBundle(nil, c.play(~clock, 1), e)

//////// 
// or

// works on its own
a.play;
// but adding either of these screws things up...
// b.play;
// c.play;

I was there for this live stream as well. I have been working on the same things as you. I can confirm on Windows 10 with SC 3.11.2, the same issue happens. The server becomes unresponsive all the time. I have just gotten used to terminating scsynth and sclang, then recompiling the class library frequently. I have not been able to isolate what’s doing it. I am also trying to learn how to run routines at the same time, etc.

I’m not sure exactly either, but I do know that you shouldn’t use routine-play inside makeBundle. They are very different concepts.

There’s absolutely no such restriction. You run concurrent routines by just starting them at the times when you want them to start.

You’re not finding any special sauce to do this because there isn’t any.

hjh

So, background on threading.

The SC interpreter is single-threaded, which means it can do only one thing at a time.

But this includes, for instance, on beat 100, doing 20 different things, very rapidly, one after another, so that it looks like it did 20 things at once – just like the way that a CPU core can context-switch so rapidly that it looks to us like concurrency.

var beat = TempoClock.beats + 1;  // a time in the future

TempoClock.schedAbs(beat, { "woke up first task".postln });
TempoClock.schedAbs(beat, { "woke up second task".postln });

This is scheduling two tasks for the same time. The interpreter can’t do them literally concurrently – but, these are very short tasks, so, when they run in sequence, we can’t tell. (Note that you could delete the variable and just write TempoClock.sched(1, ...) as well.)

Routines do the same, except they remember where they left off.

If you play two routines right now, then:

  • The first one runs up to its first wait or yield point.
    • If it yields a number, it will be rescheduled for a future time.
  • Then the second one runs up to its first wait point.
  • … and so on

It is still one thing at a time, but musical sequencing is usually a short burst of work (probably less than a ms) followed by a long wait time until the next event. When activity completes quickly and pauses much much longer, sequences of actions look like concurrency.

So there’s really nothing extra to do. It’s already handled transparently.

hjh

Sorry to keep bombing this thread… also I just found that sending a malformed bundle can cause weird behavior in the server:

s.boot;

().play;  // OK

s.numSynthDefs;  // 138 on my system; should be nonzero

s.sendBundle(nil, [ [ ] ]);

s.numSynthDefs;  // 0  :-O no no no, that's not right

().play;
FAILURE IN SERVER /s_new SynthDef not found
FAILURE IN SERVER /n_set Node 1001 not found

So … turns out that there is a direct connection between a mistaken makeBundle and unpredictable/buggy server behavior.

hjh

I’m glad to hear you say that! I was thinking that before I started down this path, and have vague memory of running parallel Routines before, but this behavior has been so consistent I’ve been thinking that it’s actually not the case. I’ve definitely reliably run several Pdefs or Pbinds like this before.

The unresponsiveness has actually been happening before I added the -makeBundle to the code. That was something I tried yesterday evening, just before starting this thread.

<<< edit >>>

Doh! I just realized there was a mistake in my code… After screwing around, some I isolated the problem to ~rhythm3’s Routine. I seem to have stuck a -makeBundle into it at some point, and forgot.

So sorry about that!

However - and now anecdotal - I was consistently losing connection between client and server in this odd silent way before I started adding -makeBundle to things.

The code below, while not too rigorously tested, does seem to work reliably. Only difference from above is that all -makeBundles have been removed.

(
// initialization:
SynthDef(\kik, { |freq = 57, amp = 0.5|
	var sig, env;
	env = Env.perc(0.001, 0.5).ar(2);
	sig = SinOsc.ar(XLine.kr(freq * 3.03, freq, 0.03));
	sig = sig + (Decay.ar(Impulse.ar(0), 1) * 0.5).tanh;
	sig = sig * env * amp;
	OffsetOut.ar(0, sig ! 2);
}).add;

SynthDef(\snr, { |freq = 177, amp = 0.5|
	var sig, env;
	env = Env.perc(0.007, 0.25).ar(2);
	sig = LFTri.ar(XLine.kr(305, freq, 0.02), 2) * 0.2;
	sig = sig * Env.perc(0.001, 0.2).kr;
	sig = sig + (HPF.ar(PinkNoise.ar, freq * 0.5) * 0.7);
	sig = sig * env * amp;
	OffsetOut.ar(0, sig ! 2);
}).add;

SynthDef(\hat, { |freq = 1400, amp = 0.5|
	var sig, env;
	env = Env.perc(0.01, 0.4).ar(2);
	sig = LPF.ar(HPF.ar(WhiteNoise.ar, freq), freq * 2);
	sig = sig * env * amp;
	OffsetOut.ar(0, sig ! 2);
}).add;

~map = IdentityDictionary[$k -> \kik, $s -> \snr, $h -> \hat];
~beat = 0.5;

// simple testers
~rhythm1 = "k.";
~rhythm2 = "h..";
~rhythm3 = ".s";


a = Routine({
	loop({
		~rhythm1.do({ |char|
			char.postln;
			if(~map[char].notNil) {
				Synth(~map[char]);
				~beat.wait;
			} {
				~beat.wait;
			}
		})
	})
});

b = Routine({
	loop({
		~rhythm2.do({ |char|
			char.postln;
			if(~map[char].notNil) {
				Synth(~map[char]);
				~beat.wait;
			} {
				~beat.wait;

			}
		})
	})
});

c = Routine({
	loop({
			~rhythm3.do({ |char|
				char.postln;
				if(~map[char].notNil) {
					Synth(~map[char]);
					~beat.wait;
			} {
					~beat.wait;

			}
			})
	})
});
)

a.play(quant: ~beat)
a.stop
a.reset
a.play(quant: ~beat)

b.play(quant: ~beat)
c.play(quant: ~beat)

b.stop
c.stop

b.reset
c.reset

@poison0ak does this code run ok for you?

What might be the case is that between too many -forks and/or nested Routines, or loops in Routines, I was screwing something up, causing these issues, and in attempting to declutter and simplify, I stuck a -makeBundle in places, initially inside of the Routines, for just calling the Synths, and then that continued to perpetuate the behavior.

@jamshark70, when the client and server lose track of each other like this, are there ways to reinitiate communication without restarting the entire program?

So Routines essentially perform some sort of version, or parallel, of TempoClock.schedAbs when calling .play(clock: beat, quant: beat) (keys written for readability here)?

Or should I line things up in some sort of other way?

You can read the code for play – no hidden magic. It uses quant to calculate the time when it should start playing, and then it schedules the task for that time. (Though you’ll be disappointed if you supply a beat number for the clock parameter.)

Perhaps review the scheduling chapters of the Getting Started tutorials…? It’s really less complex than you’re making it out to be, maybe because of overlooking some core concepts from those documents.

hjh

Yes this works for me.

I’ve been going through my code, and I THINK my problems occured when I do inThanksdeed have malformed makeBundles. However, I am not able to reproduce the issue at this time. If and when it happens again I will post the code. Thanks @jamshark70

Btw: the reason for the malformed bundles goes like this:

makeBundle collects OSC messages from methods like Synth.new or node.set by:

  1. Temporarily replacing the server’s NetAddr object with a BundleNetAddr. NetAddr sends messages over the network; BundleNetAddr doesn’t send, but instead saves messages into an array.
  2. Running the function, which is expected to do things that will try to send messages.
  3. Restoring the original NetAddr.
  4. Either returning or sending the bundle.

Step 2 assumes that all the message-producing operations will occur right now, within the scope of the function evaluation.

If the function schedules any operations for the future, they will occur after step 3, when it’s no longer collecting messages. Generating messages for collection after you’ve stopped collecting is not exactly sensible. So that’s wrong usage: it’s incorrect to schedule operations within makeBundle.

Playing a routine is scheduling.

In s.makeBundle(nil, a.play), the only action is to schedule. So there is nothing happening right now and thus no messages to collect. But nil means to send the bundle – which is empty – and it turns out that server behavior is unpredictable if you tell it “hey I’m gonna tell you to do something” and then there’s no real instruction.

For the purpose of improving the documentation, it might help to understand what was the thought process that led to trying to play a routine in makeBundle. Because the help should probably warn against that.

hjh

Definitely - well, there was nothing specifically about routines that pointed me toward makeBundle. I think what initially turned me around was some Routine / loop / fork nesting issues. I’ve since overwritten that part of the code in trying to work through it, but I think in somewhat overcomplicating that aspect to cause server issues, I reached the incorrect conclusion of ‘one routine at a time’, especially do to the fickle nature of the server communication loss- like you said, fairly unpredictable. Likely, but unpredictable.

So I looked at makeBundle, which was about scheduling things to happen at specific, or same times (like s.bind), and what appealed to me was especially the third argument in makeBundle. I figured that makeBundles schedule a function - I wanted a looping function, thought that I could only have one at a time… unless. And that third argument seemed to be my unless.

My use of it is likely an edge case. The help docs don’t link to one another specifically, and all documented makeBundles and binds are always inside of routines and as you wrote, for Synth.new or node.set.

FWIW I did read through the tutorial you mentioned, and Understanding Streams, Patterns and Events, and a bunch of relevant help files, including function.play. I think the conversion of function > routine / task > stream is what tripped me up, especially coupled with the odd behavior.

I would say the part that’s still quite fuzzy for me, and I think it’s ultimately that started the troubles is the nesting of Routines or Tasks, and loops and forks. Like in Nick Collins’s sequencing and scheduling tutorials, there is an outer function which gets forked, that contains do, which contains another function that also gets forked.

Forking converts a function into a routine, and it’s unclear to me why say the outer most shell needs to be a routine.

Code here for ease of reference (I didn’t copy over the actual SynthDefs that get called in Collins’s example):

( 
var barlengths= [4.0,3.5,5.0];  
var t= TempoClock(2.5);

{ 	
	inf.do{ |i|  
		var barnow= barlengths.wrapAt(i); 
		"new bar".postln;
		{ 		
			var whichsound; 
			whichsound= [\sound1,\sound2].choose;	
			
			((barnow/0.25)-2).floor.do{ |j| 			
				Synth(whichsound,[\freq, 300+(100*j),\amp,0.3]);  
				0.25.wait; 
			};  	
			
		}.fork(t); 
		barnow.wait; 	
	} 	
}.fork(t) 
)

I think I’m getting that the outer most fork is for running the inner -do methods on the server (but why not just for the inf.do directly?)

And then the inner forked function? Why fork it at all since if inf.do is already wrapped in a Routine?

So yeah, a lot of the code that I’ve looked at nests routines like this, so I think at the start I just instinctively started nesting and “routining” everything.

Thanks for your explanation of how makeBundle works as well, and why routines shouldn’t be stuck inside of them. This is all very interesting stuff for me. Especially in grappling how to have events unfold over time, how to sequence, but control, and manipulate, and improvise with and/or over content.

Personally I’d love to see something as thorough as your pattern tutorials for routines and tasks. The tutorial you mention is fairly brief in its coverage, and Nick Collins’s documentation covers some ground, but it seems like it was for a class or something, and so a lot of the why and how descriptions are missing, assumably explained in person. Watching Nathan use them is very interesting too, but in the livestream videos that I watched he specifically had everything nested within one routine, and monophonic, with stopping and reevaluating in between changes.

Ah, OK… makeBundle help is definitely unclear. At minimum, it assumes that you already know what is an OSC bundle, and how the bundle gets scheduled on the receiving side based on a timestamp.

Here, note that the bundle is sent immediately, and the OSCFunc responds to it immediately, and the timestamp is decoded and passed in.

(
o = OSCFunc({ |msg, time|
	"Received message %. Time is now %. Bundle time is %\n".postf(msg, SystemClock.seconds, time);
	SystemClock.schedAbs(time, {
		"Message % would be evaluated now\n".postf(msg);
	});
}, '/test').oneShot;

NetAddr.localAddr.sendBundle(1.0, ['/test', 123]);
)

// this prints *immediately*
Received message [ /test, 123 ]. Time is now 1183.976016362. Bundle time is 1184.9757600878

// this prints one second later
Message [ /test, 123 ] would be evaluated now

So makeBundle’s time applies only to outgoing messages, and to nothing else. The receiver handles the timing. But you’re interested in scheduling routines, not messages, and on the sending side rather than the receiving side.

Also, makeBundle’s third argument is expected to be a bundle. What is a bundle? It’s an array of messages. So the third argument should be an array, or nothing. Putting in another routine is definitely a nope.

It might be helpful to try to visualize it.

The baseline for scheduling is something like this.

For every one thing that is scheduled, the cycle goes like that. The scheduled thing should, at any time, be in the clock’s queue only once. (That simplifies things. You don’t have to think about every time that the routine will do something – only about the one next time.)

What does fork do?

	fork { arg clock, quant, stackSize;
		^Routine(this, stackSize).play(clock, quant);
	}

This, and only this, nothing more. What does play do? Stream:play redirects through the clock and ends up with:

	play { arg task, quant = 1;
		this.schedAbs(quant.nextTimeOnGrid(this), task)
	}

– calculate an onset time nextTimeOnGrid, and then schedule for that time. At the point of scheduling, then, it’s exactly the scenario pictured above. There isn’t anything different about fork.

func.fork means only and exactly theClock.schedAbs(quant.nextTimeOnGrid(theClock), Routine(func)).

So what about nested fork? The inner fork follows exactly the same rule, just with (assuming) a shorter wait time. But these shorter cycles are independent of the outer cycle. The outer cycle continues on its own time.

I think what Nick might be suggesting is a modular style of thinking. You can work first on a routine that will play a gesture, or a bar. Then, take exactly this code, without having to modify it, and put it inside a larger scale timing structure that will play that gesture/bar many times.

Significantly, the total time of the inner loop does not have to match the wait time of the outer loop.

  • Inner time == outer time: A continuous sequence.
  • Inner time < outer time: Gestures with pauses in between (like a burst generator in a modular rig).
  • Inner time > outer time: Overlapping gestures.

Illustrating the burst-generator idea:

(
r = fork {
	loop {
		var timeToNext = exprand(2.5, 6.0);
		var burstDur = timeToNext * rrand(0.15, 0.3);
		
		// one burst
		fork {
			var startTime = thisThread.beats;
			
			while { thisThread.beats - startTime < burstDur } {
				(freq: exprand(200, 800), sustain: 2.5, amp: 0.05).play;
				exprand(0.03, 0.08).wait;
			};
		};

		// the nested structure means outer-level wait is very simple
		// it isn't necessary to do anything like
		// (timeToNext - innerLoopElapsed).wait
		// by keeping the math simpler, you can have fewer bugs
		timeToNext.wait;
	};
};
)

But if you think “Why fork it at all…?” then you have only one creative option: monody.

Sure… I’m afraid somebody else would have to write it, since I almost never use routines/tasks directly.

hjh

2 Likes

Oh! Thank you James. This is super helpful. I think I’m getting it now.

I’ve been thinking about the inner nested loops taking over the stream until complete, and not allowing the outer block to continue onward until after – i.e the code runs linearly as written. Starting to get that this is not the case.

I wrote a short a bit of code this morning to confirm this. Pitches aren’t exactly pleasing, just meant to be very explicit:

(
SynthDef(\hping, { |freq  = 1000, amp = 0.1, atk = 0.1, dur = 1, pan = 0|
	var sig, env;
	env = Env.perc(atk, dur).kr(2);
	sig = LFTri.ar(freq) * env * amp;
	sig = Pan2.ar(sig, pan);
	Out.ar(0, sig)
}).add;

SynthDef(\lping, { |freq  = 200, amp = 0.1, atk = 0.3, dur = 1, pan = 0|
	var sig, env;
	env = Env.perc(atk, dur).kr(2);
	sig = LFTri.ar(freq) * env * amp;
	sig = Pan2.ar(sig, pan);
	Out.ar(0, sig);
}).add;
)

// for each level, all code blocks occur simultaneously
// each respective the inner level's code is reevaluated after its outer level's block wait time
//
(
// bc the level 2 loop is currently set for 4 iterations @ 1 sec/ea, value here of < 4 means the inner levels will begin to overlap with previous iterations:
var mainLoopRestartTime = 4;

r = {
	// level 1
	inf.do({ |i|

		// level 2
		q = {
			var dur2 = 1;
			(1..4).do({ |ii|

				// level 3
				p = {
					var dur3 = 0.25;
					(1..4).do({ |iii|
						Synth(\lping, [amp: 0.1 * iii, atk: 0.2 * iii, pan: 1 - ((iii - 1) * 0.3), freq: 200 + (200 * iii * 0.25), dur: dur3]);
						dur3.wait;
					});
				}.fork;

				Synth(\hping, [amp: 0.1 * ii, atk: 0.2 * ii, pan: (1 - ((ii - 1) * 0.3)).neg, freq: 1000 + (1000 * ii * 0.25), dur: dur2]);
				dur2.wait;
			});

		}.fork;

		// runs in parallel with the above function (q):
		// p = {
			// (1..4).do({ |ii|
			// Synth(\lping, [amp: 0.1 * ii, atk: 0.2 * ii, pan: 1 - ((ii -1) * 0.3), freq: 200 + (100 * (ii - 1)), dur: 0.3]);
			// 0.375.wait;
			// });
		// }.fork;

		mainLoopRestartTime.wait;
	});
}.fork
)

Interesting also for me is that after stopping the main level 1 loop, I can -reset any of the forks and play them without having to maintain the same structure (though they play whatever’s inside of them, eg q.play happens to contain p, so it will be evaluated.

Thanks again!

Boris

Just had to say I really appreciate the depth of this post James. Amazingly helpful for me.

1 Like

Just able to get back in SC today after a hiatus…

James, I’m curious:

while { thisThread.beats - startTime < burstDur }

Isn’t that just 0 < burstDur? trying to figure out the reasoning.

No – think about: What is the operator precedence rule in SC?

hjh

From “A Gentle Introduction to Supercollider” by Bruno Ruviaro, it’s:

“SuperCollider follows a left to right order of precedence, regardless of operation.”

??

The next line in the book also explains what happens when you combine binary operations and methods – methods take precedence.

EDIT: I had not looked at the specific example when I posted the clarification above, which turns out not to be relevant to the question. In any case, thisThread.beats is always going up in value, whereas startTime is fixed at the original value from when it was assigned. So thisThread.beats inside the while will always be a bigger number then startTime.

Ahhhh… My mistake.

What I was thinking was… If the Boolean > is being assumed to be always false, then perhaps mentally you were grouping the fixed value startTime with the comparison operator, to be done first. This reading doesn’t make any sense, in hindsight. Apologies for wasting a bit of time.

Bruno’s explanation is correct: .beats is updating after every wait, while startTime doesn’t.

var startTime = thisThread.beats;

Variable assignment in SC, as in BASIC, Fortran, Pascal, C, Java, Python, Lua etc etc etc, means:

  1. Evaluate the right hand expression – fully evaluate it.
  2. Change the variable’s value to be the result of the expression.

thisThread.beats returns a number. There is nothing special about the number just because it came from thisThread.beats. It is only a number.

What happens to startTime when thisThread.beats changes? … Nothing. Because startTime is only a number, grabbed once at the beginning of the forked routine.

Lately I’m observing that some users come to SC with the assumption from algebra that a variable assignment creates a relationship between an identifier and an expression (so that, if the expression’s value changes, so will the variable). That’s not how it works. The variable takes on the value of the expression at that moment, and only that value. The variable doesn’t know where the value came from. There is no persistent connection to the source expression.

(In Haskell, variables do work more like algebra. So perhaps Haskell would be a more comfortable language for this subset of users.)

hjh

@jamshark70 @Bruno Thank you both, I understand now.

Edit: Also @jamshark70 I did start programming music with tidalCycles, and tried to learn Haskell as a first language. I can tell you I am NOT comfortable with it. I do find functional programming beautiful from a conceptual POV, but after getting more into tidal I discovered that I am more into composition, not live coding, therefore sclang is the way to go, hands down. In SC all the problems I face are always from not understanding the language mechanics though. I’m still wondering if learning SmallTalk at the same time, since it has documentation more sutied to learning a programming language from the ground up, would be more beneficial than just sticking with sclang by itself. God this pararaph is horrible, sorry about that.