The clock of s.waitForBoot and s.doWhenBooted

In order to include my suggestion in the help documentation, the following part would have to be changed, but it would be a breaking change:

}.play(SystemClock)

Would that be OK?

Hm, to be honest, I don’t see any benefit in doing this (but it would cause concrete problems, by breaking user code that has GUI operations in a waitForBoot function – I think my live-show setup sequence does this! So please don’t break my live-show setup :laughing: ).

The key words are “time-sensitive.” If you are trying to sync GUI work with time-sensitive operations (such as musical sequencing), then I’d recommend the approach of running the time-sensitive control flow on TempoClock / SystemClock, and deferring only the bits that have to be deferred.

But the server boot process is not time-sensitive. There is no compelling reason to run it in a real-time thread.

hjh

1 Like

Sure!

However, codes like this

(
//s.doWhenBooted {
s.waitForBoot {
	Window.closeAll;
	fork {
		600.do { |i|
			{ w = Window().background_(Color.rand).front }.defer;
			i = if (i % 4 == 0) { 12 } { 0 }.postln;
			x = { SinOsc.ar((81 + i).midicps) * Env.perc.ar(Done.freeSelf) * [0.1, -0.1] }.play;
			1.wait;
			{ w.close }.defer
		}
	}
}
)

could be written as follows:

(
//s.doWhenBooted {
s.waitForBoot {
	{ Window.closeAll }.defer;
	600.do { |i|
		{ w = Window().background_(Color.rand).front }.defer;
		i = if (i % 4 == 0) { 12 } { 0 }.postln;
		x = { SinOsc.ar((81 + i).midicps) * Env.perc.ar(Done.freeSelf) * [0.1, -0.1] }.play;
		1.wait;
		{ w.close }.defer
	}
}
)

My perspective could be from my lazy programming habit…
Such a construction is unnecessary if one uses Routine or Task outside of s.waitForBoot like this

( 
//s.doWhenBooted {
s.waitForBoot {
	Window.closeAll
};
fork {
	600.do { |i|
		{ w = Window().background_(Color.rand).front }.defer;
		i = if (i % 4 == 0) { 12 } { 0 }.postln;
		x = { SinOsc.ar((81 + i).midicps) * Env.perc.ar(Done.freeSelf) * [0.1, -0.1] }.play;
		// Until the local server is booted, the following warning will be displayed:
		// WARNING: server 'localhost' not running. 
		1.wait;
		{ w.close }.defer
	}
}
)

or if one uses Pbind like this:

(
//s.doWhenBooted {
s.waitForBoot {
	Window.closeAll; 
	~clockSound = Pbind(
		\instrument, \default,
		\midinote, Pseq([93, 81!3].flat, inf),
		\amp, 0.05,
		\dur, 1,
		\legato, 0.7,
		\callback, { 
			Routine {
				q = Window().background_(Color.rand).front; 
				0.99.wait; 
				q.close.postln
			}.play(AppClock)
		}
	).play
}
)

The advantage would be a somewhat simplified code structure for beginners and some of intermediate users. I do not think it is unimportant.
For advanced users who use SuperCollider in vscode for example, modifying these parts is not difficult. Finding and replacing text is a tedious process, but regular expression find and replace in a folder and its subfolders makes the process easier.

I would like to point out the method .plot. In most cases, users do not need to care about plotter. I think that defer{ ... } and AppClock could be in a similar relationship to .plot and Plotter if we change the clock of ServerStatusWatcher:doWhenBooted from AppClock to SystemClock.

There is an unknown amount of legacy user code that depends on waitForBoot running in AppClock – unknown meaning, potentially large.

There’s not a bug here, just confusion stemming from incomplete documentation. The fact that it’s not thoroughly documented is IMO not sufficient grounds to break existing user code.

With apologies, but I am 100% against this suggestion let me rephrase… while I appreciate the thought that’s gone into it, breaking changes really need careful consideration* and IMO this one doesn’t make the cut.

*Recalling Pamela Z’s ICMC '23 keynote, where she admitted she hasn’t upgraded past Max 5 because her live-show patch is broken in later versions. That’s the hidden (or not-so-hidden) cost of breaking changes.

hjh

1 Like

Yes, this may not be appropriate in SuperCollider 3.x.x.
But could it be considered for SuperCollider 4?

[EDITED]
Or we can give the fourth argument clock and set its default value to AppClock.

// Server
.waitForBoot(onComplete, limit: 100, onFailure)
.doWhenBooted(onComplete, limit: 100, onFailure)

// ServerStatusWatcher
.doWhenBooted(onComplete, limit: 100, onFailure)

Then, it will not be a breaking change!

I wonder if there might be a way that GUI calls could be automatically redirected rather than erroring when they are scheduled in a Thread?

(sorry for reposting due to typos)

I am not sure if I understand your question, but I don’t think there are such a way without modifying the method definition in Server.sc and ServerStatus.sc.

My test is successful:

  1. Modifications
    1.1. waitForBoot and doWhenBooted in Server.sc
    	waitForBoot { |onComplete, limit = 100, onFailure, clock = \AppClock|
    		// onFailure.true: why is this necessary?
    		// this.boot also calls doWhenBooted.
    		// doWhenBooted prints the normal boot failure message.
    		// if the server fails to boot, the failure error gets posted TWICE.
    		// So, we suppress one of them.
    		if(this.serverRunning.not) { this.boot(onFailure: true) };
    		this.doWhenBooted(onComplete, limit, onFailure, clock);
    	}
    
    	doWhenBooted { |onComplete, limit=100, onFailure, clock = \AppClock|
    		statusWatcher.doWhenBooted(onComplete, limit, onFailure, clock)
    	}
    
    1.2. doWhenBooted in ServerStatus.sc
    	doWhenBooted { |onComplete, limit = 100, onFailure, clock = \AppClock|
    		var mBootNotifyFirst = bootNotifyFirst, postError = true;
    		bootNotifyFirst = false;
    
    		^Routine {
    			while {
    				server.serverRunning.not
    				/*
    				// this is not yet implemented.
    				or: { serverBooting and: mBootNotifyFirst.not }
    				and: { (limit = limit - 1) > 0 }
    				and: { server.applicationRunning.not }
    				*/
    
    			} {
    				0.2.wait;
    			};
    
    			if(server.serverRunning.not, {
    				if(onFailure.notNil) {
    					postError = (onFailure.value(server) == false);
    				};
    				if(postError) {
    					"Server '%' on failed to start. You may need to kill all servers".format(server.name).error;
    				};
    				serverBooting = false;
    				server.changed(\serverRunning);
    			}, {
    				// make sure the server process finishes all pending tasks from Server.tree before running onComplete
    				server.sync;
    				onComplete.value;
    			});
    
    		}.play(clock.asClass)
    	}
    
  2. test code
    2.1. with default AppClock:
    (
    //s.doWhenBooted {
    s.waitForBoot {
    	Window.closeAll;
    	fork {
    		600.do { |i|
    			{ w = Window().background_(Color.rand).front }.defer;
    			i = if (i % 4 == 0) { 12 } { 0 }.postln;
    			x = { SinOsc.ar((81 + i).midicps) * Env.perc.ar(Done.freeSelf) * [0.1, -0.1] }.play;
    			1.wait;
    			{ w.close }.defer
    		}
    	}
    }
    )
    
    or
    (
    //s.doWhenBooted({
    s.waitForBoot({
    	Window.closeAll;
    	fork {
    		600.do { |i|
    			{ w = Window().background_(Color.rand).front }.defer;
    			i = if (i % 4 == 0) { 12 } { 0 }.postln;
    			x = { SinOsc.ar((81 + i).midicps) * Env.perc.ar(Done.freeSelf) * [0.1, -0.1] }.play;
    			1.wait;
    			{ w.close }.defer
    		}
    	}
    })
    )
    
    2.2. with SystemClock
    (
    //s.doWhenBooted({
    s.waitForBoot({
    	{ Window.closeAll }.defer;
    	600.do { |i|
    		{ w = Window().background_(Color.rand).front }.defer;
    		i = if (i % 4 == 0) { 12 } { 0 }.postln;
    		x = { SinOsc.ar((81 + i).midicps) * Env.perc.ar(Done.freeSelf) * [0.1, -0.1] }.play;
    		1.wait;
    		{ w.close }.defer
    	}
    }, clock: SystemClock)
    )
    
    2.3. with TempoClock
    (
    ~tempo = TempoClock;
    ~tempo.tempo = 1;
    //s.doWhenBooted({
    s.waitForBoot({
    	{ Window.closeAll }.defer;
    	600.do { |i|
    		{ w = Window().background_(Color.rand).front }.defer;
    		i = if (i % 4 == 0) { 12 } { 0 }.postln;
    		x = { SinOsc.ar((81 + i).midicps) * Env.perc.ar(Done.freeSelf) * [0.1, -0.1] }.play;
    		1.wait;
    		{ w.close }.defer
    	}
    }, clock:~tempo)
    )
    ~tempo.tempo = 30/60
    

Could it be acceptable? This

  • is not a breaking change,
  • has a simpler code structure,
  • is easy for newbies to use, and
  • makes users (at least me) less likely to make mistakes.
1 Like

If AppClock is the default, this does not imply any problems with the existing code.
I have some projects for stage performance use that don’t use any GUI, and some people in some situations might be glad to start the entire project with one main file being loaded with just those methods.

It could be somewhat educative too to provide options.

But it’s not a big deal! I’m just giving a perspective.

I closed the initial PR (Topic/mention default clock for s.waitForBoot and s.doWhenBooted by prko ¡ Pull Request #6193 ¡ supercollider/supercollider ¡ GitHub) and made a new one: