Startup.scd, .waitForBoot, sclang / scsynth shutdown

I run a scd file from the terminal with sclang and I use .waitForBoot. I assumed it would join the running sc server if it was already booted, but I get:

*** ERROR: failed to open UDP socket: address in use. This could be because another instance of scsynth is already using it.

A related question, how to let sclang kill scsynth if it receives a SIGTERM signal?

To join a running server, 1/ the server had to be booted with a high enough s.options.maxLogins. The default is 1, which will be used by the client that booted the server. So then no other client can connect unless you prepared for it in advance.

And 2/:

Server.default = s = Server.remote(\remote, NetAddr("127.0.0.1", 57110);
s.startAliveThread;

I’m not aware of any built-in method to boot if needed, otherwise join.

Probably better is to kill stray servers before booting.

My understanding is that this should happen automatically, since scsynth is a subordinate process.

hjh

.waitForBoot(onComplete, limit: 100, onFailure)

“Evaluate “onComplete” as soon as the server has booted. This method will boot the server for you if it is not already running or booting. If the server is already running, “onComplete” is executed immediately.”

Maybe I miss something. I assumed I could run this piece of code on the same server multiple times, or at least, run it when the server is already running.

Server.default.waitForBoot {
    SynthDef(\sine, {
        var snd;
        snd = SinOsc.ar(\freq.kr(440));
        snd = snd * Env.perc(0.01, 0.3).ar(Done.freeSelf);
        Out.ar(0, snd);
    }).add;
    Server.default.sync;
    Routine({
        10.do {
            Synth(\sine, [freq: exprand(100, 8000), amp: 0.1]);
            rrand(0.1, 0.5).yield;
        };
        0.exit;
    }).play;
};


This code is taken from:

That post is also stating:
“You have to write 0.exit manually at the completion of your piece. sclang doesn’t do the node.js thing where it exits automatically when there are no more outstanding asynchronous tasks.”

But what if that code is running a infinite loop? I thought I’d use SIGTERM but that doesn’t kill scsynth.

Killing a parent process does not automatically kill the child process on any OS! There are various solutions but they are highly OS specific and some of them are not very reliable.

The solution I typically use is to pass the parent PID to the child and let the child process periodically poll its parent. Here’s a simplified example from VSTPlugin’s plugin bridge:

void PluginServer::checkIfParentAlive(){
#if VST_HOST_SYSTEM == VST_WINDOWS
    bool alive = WaitForSingleObject(parent_, 0) == WAIT_TIMEOUT;
#else
    bool alive = getppid() == parent_;
#endif
    if (!alive) {
        quit();
    }
}

This is pretty reliable and almost cross-platform. The downside is that you need to modify the client program; in particular, you’d need to add a command line argument for the parent PID.

1 Like

Yes, it should, if the server was already started in the same sclang session.

Usually we assume there’s just one sclang session. That is, baseline usage is: start the IDE – IDE starts sclang – sclang starts scsynth. These are 1:1:1 in normal usage (you could run multiple editors, each with its own sclang, but normally we don’t; a single sclang can run multiple servers but normally we don’t).

If you’re getting the port error, it means either 1/ scsynth isn’t shutting down properly when you end a session, or 2/ you’re doing something funky with multiple sessions. I guess #1 is much more likely. In that case, just go to the Server menu and run “Kill all servers,” then try the waitForBoot block again.

That’s getting way ahead of yourself. For normal interactive usage (via IDE or another editor), this is not necessary.

Thanks for the correction – I had heard something somewhere about subordinate processes but I must have misunderstood.

hjh

1 Like

Multiple sclangs:

The same sclang session, ah ok.

What I basically want to do is running sclang from the terminal, so that it more or less runs as standalone application and I want to be able to run multiple of them.

I understand that it is not the main use case, but my naive feeling says that it shouldn’t be considered ‘funky’ at all. A OSC server with multiple clients communicating a bidirectional way, is where OSC is made for.

Sigterm:

In ‘normal’ programming languages it is possible to catch the SIGTERM signal and handle it the way you like with a function. Is this possible with sclang? When I kill sclang it doesn’t kill scsynth, which is probably a good thing, but can I define a function which handles signals, so that if it receives a SIGTERM signal, it sends /quit to scsynth?

I don’t see any functionality in sclang to handle Unix signals, but it could be quite useful!

Keep in mind that Windows has no notion of signals on the system level. It only supports signals that are required by the C standard ( SIGINT, SIGABRT, SIGTERM, SIGSEGV, SIGILL, and SIGFPE), but with the important restriction that SIGABRT and SIGTERM can only be generated in the same process. To handle keyboard interrupts and termination requests, you must install a console handler instead (SetConsoleCtrlHandler function - Windows Console | Microsoft Learn).

It is not trivial to wrap this in an intuitive cross-platform manner. Python, for example, raises a KeyboardInterrupt exception on Control+C, but AFAICT it does not provide a simple cross-platform way of handling termination requests. To give you a taste of the whole mess: python - How to handle a signal.SIGINT on a Windows OS machine? - Stack Overflow

Yes, I think it is.

Sure, Python is a bad example. Better look at Rust, Go and such probably. signal package - os/signal - Go Packages

The Go API looks pretty good indeed. CONTROL_C_EVENT and CTRL_BREAK_EVENT is mapped to SIGINT; CTRL_CLOSE_EVENT, CTRL_LOGOFF_EVENT and CTRL_SHUTDOWN_EVENT is mapped to SIGTERM. Makes sense.

1 Like

Ok – I think in a traditional server, there are concrete benefits to a central server handling requests from a large number of clients (e.g., in databases, table locking would be an incredible mess if distributed across many servers). For real-time audio, those benefits are mainly irrelevant.

In SC, one client doesn’t know what the others are doing, so there’s no meaningful way for one to interact with another’s server resources (unless you build an OSC protocol for it). So SC’s solution is to isolate server users into their own ID spaces, with an up-to-5-bit mask distinguishing them. But the server resources are allocated globally at boot time. So if you allow 1024 audio buses and then think, I don’t know how many users I’ll need, so I’ll set s.options.maxLogins = 32, then each user can use 1024/32 = 32 audio buses, and what looked like abundance is suddenly not so great.

If you run one server and multiple clients, you might in theory gain a little bit by having less overhead (for one audio callback rather than several), but you’d also get reduced addressing spaces for buses and buffers, and also, in scsynth, all users’ audio will calculate in one thread. Despite the single server process, clients would be hard pressed to share resources.

If you run one server per client, each server would have a small, negligible overhead, but each client gets the full id address space to itself, and also, the OS can schedule different scsynth instances on different cores (parallel processing for free, with less finagling than in supernova).

I may be missing something that you’re trying to do, but multiple clients on the same server looks like a net loss in SC. It’s definitely supported (see the Server.remote method I mentioned above), but it isn’t often done because scsynth isn’t a database or file server. The concerns and the optimal use case(s) are different.

Perhaps in Windows, you’re running into older audio protocols’ limitation of one process “owning” the soundcard. That’s easily remedied; scsynth does work with JACK For Windows, for instance – it’s easy to run multiple scsynthses and let JACK sort them out.

hjh

1 Like

So multiple servers instead, ok.

But how do I use a different Server when running sclang from the command line?

Server.New(\test, NetAddr("127.0.0.1", 77110)).waitForBoot {
     // my code here

};

Running this in a scd file from the command line with sclang, doesn’t work. The synthdefs are send to the default server instead?

How do I make this command line approach work with a other server?

Fortunately, that limitation is mostly gone. I think with MME, DirectSound or WASAPI that has never been an issue. In the past, ASIO drivers only supported a single client, but most modern ASIO drivers are multi-client. One exception I know is ASIO4ALL, which is still single-client.

scsynth does work with JACK For Windows

Only via the Jack ASIO driver, right? (Unless you compile SC with the Jack backend.) If your ASIO driver is already multi-client, the Jack ASIO driver wouldn’t buy you anything, assuming you don’t need inter-app routing.

There should be no difference in the server boot behavior between IDE and commandline.

The behavior of sending SynthDefs on server bootup is defined in SynthDescLib *initClass:

	*initClass {
		Class.initClassTree(Server);
		all = IdentityDictionary.new;
		global = this.new(\global);

		ServerBoot.add { |server|
			this.send(server, false)
		}
	}

ServerBoot.add can add an action for a specific server, or for all servers. Here, no server is specified, so this action is for all servers.

In my test, the SynthDefs were definitely sent to the NetAddr object belonging to the server that I created, not to the default. So I can’t reproduce the behavior you’re reporting. It works fine at the command line.

hjh

Although this is a typo – method names in SC are never capitalized.

hjh

Ok hm, I can’t get the example of Nathan running on a different server from the command line. Not sure what’s going on. Not urgent atm though.

The first problem here is that port numbers are 16 bits, unsigned – maximum 65535. (Not a limitation in SC – every app is subject to this.)

If you try to boot the server with port number 77110, sclang will be unable to communicate with it. So your script will hang, waiting forever for boot.

Here, I think your troubleshooting efforts might have been flummoxed by jumping immediately to commandline use. If you had tested your block of code in the IDE first, you would have seen that the server status indicator goes yellow and freezes – valuable information!

Try port 62110 or such.

The second problem is that, by default, indeed SynthDef:add sends only to the default server. (I was thinking of something else last night… my mistake.)

There are two ways to deal with that. One is documented: “A server can be added by SynthDescLib.global.addServer(server).”

(
~myServer = Server(\myServer, NetAddr("127.0.0.1", 62110));
SynthDescLib.global.addServer(~myServer);

~myServer.waitForBoot {
	SynthDef(\sine, {
		var snd;
		snd = SinOsc.ar(\freq.kr(440));
		snd = snd * Env.perc(0.01, 0.3).ar(Done.freeSelf);

		// btw I'm on headphones now, so the example as written
		// will blast full-scale audio into my left ear only.
		// (the Synth call specifies amp: 0.1 but there's no amp arg!)
		// so I'm gonna fix that here: proper amp scaling + L-R dup

		Out.ar(0, (snd * \amp.kr(0.1)).dup);
	}).add;
	~myServer.sync;
	Routine({
		10.do {
			Synth(\sine, [freq: exprand(100, 8000), amp: 0.1], ~myServer);
			rrand(0.1, 0.5).yield;
		};
		0.exit;
	}).play;
};
)

The other is… if you aren’t using the original default server for anything, you can simply make your new server the default… and then don’t worry about it. I think this is easier because, in the above case, you have to remember to tell the Synth() to go to ~myServer – you will forget this sometimes.

// this, at the beginning of Nathan's script
Server.default = s = Server(\myServer, NetAddr("127.0.0.1", 62110));

Or, just change the addr of the default server:

Server.default.addr = NetAddr("127.0.0.1", 62110);

hjh

1 Like

Small steps when coding / debugging… fair point!
I think SC could have helped me here though and just reported a error telling that my port number was to high and quit.

Is the Server.default client specific, can every client have it’s own Server.default?

Thanks!

Each sclang client is its own process with its own memory space, its own object pool, its own state. It’s literally impossible to share the same identical Server object across multiple clients. It would be possible for one client to use OSC to communicate a server address to other clients, but those would be distinct Server objects that happen to be pointing to the same server.

I do understand – it’s extremely difficult to anticipate every possible mistake a user could make, so there are places where potential errors aren’t caught. We’ve fixed many of these to print more descriptive error messages, often in response to a situation like this. You’d be welcome to file a bug report (or a fix, if you feel comfortable with that) on the github repository.

hjh

1 Like
$ sclang -h
Usage:
   sclang [options] [file..] [-]

Options:
   -v                             Print supercollider version and exit
   -d <path>                      Set runtime directory
   -D                             Enter daemon mode (no input)
   -g <memory-growth>[km]         Set heap growth (default 256k)
   -h                             Display this message and exit
   -l <path>                      Set library configuration file
   -m <memory-space>[km]          Set initial heap size (default 2m)
   -r                             Call Main.run on startup
   -s                             Call Main.stop on shutdown
   -u <network-port-number>       Set UDP listening port (default 57120)
   -i <ide-name>                  Specify IDE name (for enabling IDE-specific class code, default "none")
   -a                             Standalone mode (exclude SCClassLibrary and user and system Extensions folders from search path)

It’s only possible to pass the -u flag for port. Shouldn’t it be possible to pass the hostname / ip address to sclang?

This is the port to which the client socket should be bound. In this situation, you would only provide an IP address if you want to restrict the incoming traffic to a particular network interface. Is this really what you are looking for?