Calling Supercollider script from Windows batch file?

Is it possible to call a long-running Supercollider script (including booting the server) from a Windows batch file, say, by calling sclang.exe with the Supercollider script as a command-line argument? If so, what is the preferred method to do so? Are there any differences in executing such a script from a batch file (e.g., syntax, formatting, performance) as opposed to running it interactively within the IDE? Any other gotchas to watch out for (e.g., working directory vs. location of Supercollider executables)?

Is it feasible to start the server and then attach sclang.exe to the running process? Would that be preferable?

Can you pass run-time parameters to the Supercollider script from the Windows batch file, for example from environmental variables or text files?

Thanks.

Absolutely, not a problem.

If you can run the script as one single block, and everything initializes automatically, then you can run it as a script.

There should be no extra code in the file – it will run the whole file.

If you need to run a block, then run a second block interactively, this can’t be done as-is in a script. You would have to put it in a thread with waiting signals so that it runs as one block. SC is quite articulate for this use case; I have a couple of fairly complex scripts.

I wouldn’t bother with this.

http://doc.sccode.org/Classes/Main.html#-argv

hjh

James,

Thanks for the quick response.

Sorry, but I’m feeling a little dense. Pardon my ignorance, as I’m a Supercollider newbie. I’ve added s.boot to my startup.scd file, so I don’t need to explicitly boot the server whenever I start a session. The server starts properly when I call Supercollider with the IDE. Likewise, if I call sclang.exe with no script, the server also starts up. However, the process just sits there, as you can’t enter commands to sclang.exe interactively from the command line.

I created the following Windows batch file to test these concepts:

– begin code –
test2.bat:
H:\MAK Drums\Training\One part>type test2.bat
F:
cd “\Program Files\SuperCollider-3.11.0”
sclang.exe “H:\MAK Drums\Training\One part\bootit.sc” “arg 1” “arg 2” “arg 3”
H:
– end code –

The Supercollider script is as follows:

– begin code –
bootit.sc:
(
~a = thisProcess.argv;
~a.postln;
“in bootit.sc”.postln;
" ".postln;
“------------------”.postln;
" ".postln;
VSTPlugin.search(s,[“F:/VstPlugins (x64)”,“H:/VSTPlugins (x64)”],false);
)
– end code –

When I call the batch file, I get the following:

– begin output –
H:\MAK Drums\Training\One part>test2

H:\MAK Drums\Training\One part>F:

F:\Program Files\SuperCollider-3.11.0>cd “\Program Files\SuperCollider-3.11.0”

F:\Program Files\SuperCollider-3.11.0>sclang.exe “H:\MAK Drums\Training\One part\bootit.sc” “arg 1” “arg 2” “arg 3”
compiling class library…
Found 848 primitives.
Compiling directory ‘F:\Program Files\SuperCollider-3.11.0\SCClassLibrary’
Compiling directory ‘C:\ProgramData\SuperCollider\Extensions’
Compiling directory ‘C:\Users\micha\AppData\Local\SuperCollider\Extensions’
Compiling directory ‘C:\Users\micha\AppData\Local\SuperCollider\downloaded-quarks\wslib’
Compiling directory ‘C:\Users\micha\AppData\Local\SuperCollider\downloaded-quarks\XML’
ERROR: Class extension for nonexistent class ‘Document’
In file:’’
ERROR: Class extension for nonexistent class ‘Document’
In file:’’
numentries = 1486214 / 27217810 = 0.055
7297 method selectors, 3730 classes
method table size 26783200 bytes, big table size 217742480
Number of Symbols 18609
Byte Code Size 665125
compiled 799 files in 1.21 seconds

Info: 2 methods are currently overwritten by extensions. To see which, execute:
MethodOverride.printAll

compile done
localhost : setting clientID to 0.
internal : setting clientID to 0.
Class tree inited in 0.03 seconds
In startup.scd

*** Welcome to SuperCollider 3.11.0. *** For help press F1.
[ arg 1, arg 2, arg 3 ]
in bootit.sc


Booting server ‘localhost’ on address 127.0.0.1:57110.
VSTPlugin 0.4.1
read cache file C:\Users\micha.VSTPlugin/cache.ini

Device options:

  • MME : Microsoft Sound Mapper - Output (device #0 with 0 ins 2 outs)
  • MME : Speakers (Realtek High Definiti (device #1 with 0 ins 2 outs)
  • Windows DirectSound : Primary Sound Driver (device #2 with 0 ins 2 outs)
  • Windows DirectSound : Speakers (Realtek High Definition Audio(SST)) (device #3 with 0 ins 2 outs)
  • Windows WASAPI : Speakers (Realtek High Definition Audio(SST)) (device #4 with 0 ins 2 outs)
  • Windows WDM-KS : Microphone Array 1 () (device #5 with 2 ins 0 outs)
  • Windows WDM-KS : Microphone Array 2 () (device #6 with 1 ins 0 outs)
  • Windows WDM-KS : Speakers 1 (Realtek HD Audio output with SST) (device #7 with 0 ins 2 outs)
  • Windows WDM-KS : Speakers 2 (Realtek HD Audio output with SST) (device #8 with 0 ins 2 outs)
  • Windows WDM-KS : PC Speaker (Realtek HD Audio output with SST) (device #9 with 2 ins 0 outs)

Requested devices:
In (matching device NOT found):

  • MME
    Out (matching device found):
  • MME

Booting with:
Out: MME : Speakers (Realtek High Definiti
Sample rate: 48000.000
Latency (in/out): 0.000 / 0.107 sec
SC_AudioDriver: sample rate = 48000.000000, driver’s block size = 512
SuperCollider 3 server ready.
Requested notification messages from server ‘localhost’
localhost: server process’s maxLogins (1) matches with my options.
localhost: keeping clientID (0) as confirmed by server process.
Shared memory server interface initialized
– end code –

I inserted the debugging message, ‘in startup.scd’, to show when the startup file was being executed. After the Supercollider welcome banner, I dumped the sample command-line arguments to verify that functionality. Then, you can see the output from the server boot process. However, nothing else gets executed in the script. The VSTPlugin.search command is never called. The script hangs and does not exit, since sclang.exe does not accept interactive input from the command line.

What am I doing wrong? How do I wait for the server reboot to be completed before proceeding with the rest of the script? If I add 0.exit at the end of the block, the script terminates properly, but exits immediately, before the server is booted. How do I get anything meaningful done in the script block?

Do you have any simple examples of Windows batch files and Supercollider scripts that you use? Is there any documentation illustrating the proper use of batch files and Supercollider scripts beyond basic descriptions of the relevant classes and methods in the help files?

Thanks again.

I’m unsure of the most recent status, but it looks like command-line usage is not working in Windows: https://github.com/supercollider/supercollider/issues/2171

When you run sclang with a script file, no prompt will be presented. The only interaction you can do at that point is via GUI objects.

– begin code –

Please use triple backticks to delimit code. Code markup: Correct style

The easiest way is s.waitForBoot { /* in here, all of the code that should follow server boot */ }. “All of the code” may be a very long block. If the server is already booting, it won’t boot a second time (and waitForBoot will wait for the existing boot attempt to complete).

The function runs in the context of an AppClock thread, so it can wait or s.sync or use Condition for further waiting.

The 0.exit needs to be in a thread that will wait for the work to be done, or in a GUI object for the user to trigger.

Part of the issue here is that script usage really depends on understanding threads and scheduling. If you haven’t read the scheduling chapters of the tutorial, most of this may not make much sense.

It’s absolutely essential in a script to run things in a context where you can pause and wait for completion before moving on (unless all of the computations are synchronous, which is never the case when a server is involved). In SC, this is a Routine (or Task).

Note that waitForBoot implicitly creates a Routine based on the action function – so you’re halfway there with that. Then you need to understand how to wait for completion – see the Condition help file.

I can perhaps post an example a while later.

hjh

1 Like

Here’s a quick example of a script, demonstrating sync/pausing mechanisms.

  • User input: Pop up a GUI for a window with a text field. When the user completes the input (action), the callback function from the parent thread saves the input string and “unhangs” the thread.

    • Semantically, it reads a little strangely to have cond.unhang appear in the code before cond.hang, when unhang will happen later. But this is necessary: You have to put in place the thing that will reactivate the thread, before pausing the thread. If it’s not like this, it’s kind of like falling asleep first before setting your alarm.
  • Server boot with waitForBoot.

  • record involves some asynchronous initialization, so, s.sync.

  • Playing a pattern: This might not be especially well documented, but a SimpleController watching for a \stopped signal from the pattern’s EventStreamPlayer is the best way here.

I’ve tested this as a script by saving it into a file and running it with sclang path/to/thefile.scd. It loads the class library, pops up the input dialog, boots the server, plays a few notes, and by the time it exits, there’s a short WAV file in your recordings directory.

hjh

fork({
	var cond = Condition.new;
	var str;
	var path = Platform.recordingsDir +/+ "SC_" ++ Date.getDate.stamp ++ ".wav";
	var player;
	var watcher;

	~inputWindow.value({ |input|
		str = input;
		cond.unhang;
	});
	cond.hang;

	s.waitForBoot {
		s.record(path, numChannels: 2);
		s.sync;
		player = Pbind(
			\midinote, Pseq(str).collect(_.ascii),
			\dur, 0.25
		).play;
		watcher = SimpleController(player)
		.put(\stopped, {
			watcher.remove;
			cond.unhang;
		});
	};

	cond.hang;

	2.0.wait;  // allow a little time for notes to fade out
	s.stopRecording;

	if(Platform.ideName == "none") {
		0.exit;
	};
}, AppClock);

~inputWindow = { |action|
	var w = Window("Type something",
		Rect.aboutPoint(Window.screenBounds.center, 100, 40)).front;
	var tf;
	w.layout = VLayout(
		tf = TextField()
	);
	tf.action_({ |view|
		var str = view.value;
		w.close;
		action.value(str);
	});
};
2 Likes

James,

My apologies for trying to jump into the deep end without being fully prepared. I think I’m beginning to understand threads and scheduling in Supercollider. I had cross-posted this query on Reddit and did get a response, including another useful example script.

Thanks again.

James,

Sorry, I was unaware of the recommended approach for delimiting code in posts to this forum. I will use triple backticks going forward. How do you highlight code fragments within the text? Does the preformatted text button ("</>") work, (e.g., s.boot)?

Thanks.

– Michael

James,

I’m just about there. I inserted my code into your example framework, as follows:

fork({
   	var cond = Condition.new;
	var str;
	var player;
	var source_dir;
	var samples;
	var rt;
	var root;
	var path1;
	var m;
	var test_folder;
	var args;
	var drum;

        ~inputWindow.value({ |input|
                str = input;
                cond.unhang;
        });
        cond.hang;

        s.waitForBoot {
// get command-line arguments
		args=thisProcess.argv;
		drum=args[0];

// root folder
		root="H:/MAK Drums/Training/One-shot";

// load MIDI file
		path1=root+/+"Original MIDI"+/+"One-shot [C1 36].mid";
		m=SimpleMIDIFile.read(path1);
		m.init0( 120, "4/4" );	// init for type 0, 120bpm, 4/4 measures
		m.timeMode = \seconds;      // change from default to something useful

// set output folder
		test_folder=root+/+"WAV Files"+/+"Battery 4"+/+drum;

//collect (stereo) one-shot samples into buffer
		source_dir="F:/Documents/Battery 4 Factory Library/Samples/Drums"+/+drum+/+"*";
		samples = SoundFile.collectIntoBuffers(source_dir);

// synth for mono samples
		SynthDef( \play_mono, {
			arg amp = 1.0, buf, out;
			var sig;
			sig = 0.707 * 5 * amp * PlayBuf.ar(1, buf, doneAction: Done.freeSelf);
			Out.ar(out,sig!2);
		}).add;

// synth for stereo samples
		SynthDef( \play_stereo, {
			arg amp = 1.0, buf, out;
			var sig;
			sig = 0.707 * 5 * amp * PlayBuf.ar(2, buf, doneAction: Done.freeSelf);
			Out.ar(out,sig);
		}).add;

// process all samples in selected folder
		rt = Routine({
			samples.do({
				arg item;
				var player, p1, p2, q1, r1, samplefilename, outputfilename;
				p1=item.path;
				p2=PathName.new(p1);
				samplefilename=p2.fileNameWithoutExtension;
				outputfilename=test_folder+/+samplefilename++".wav";
				q1=item.numChannels;
				s.recHeaderFormat="WAV";
				s.recSampleFormat="int16";
				s.prepareForRecord(outputfilename,2);
				if
				    (
					q1 == 1,
					{
					player = Pbindf(m.p,\instrument,"play_mono",\buf,item,\legato, 1.0);
					player.record(outputfilename,"WAV","int16",2,nil,0.2,TempoClock.default,Event.default,Server.default,0,2);
					},
					{
					player = Pbindf(m.p,\instrument,"play_stereo",\buf,item,\legato, 1.0);
					player.record(outputfilename,"WAV","int16",2,nil,0.2,TempoClock.default,Event.default,Server.default,0,2);
					}
				    );

				1.0.wait;
				});
		    });

	rt.play;
	};

        cond.hang;

        2.0.wait;  // allow a little time for notes to fade out
        s.stopRecording;

        if(Platform.ideName == "none") {
                0.exit;
        };
}, AppClock);

~inputWindow = { |action|
        var w = Window("Type something",
                Rect.aboutPoint(Window.screenBounds.center, 100, 40)).front;
        var tf;
        w.layout = VLayout(
                tf = TextField()
        );
        tf.action_({ |view|
                var str = view.value;
                w.close;
                action.value(str);
        });
};

My version is slightly different from your example, as I’ve got a loop, iterating over a number of samples found in a particular folder. I wasn’t quite sure where to put the cond.unhang statements, though. So, while I do get all of the expected recordings, the process seems to hang after the loop has been completed and the script fails to terminate cleanly.

Any suggestions would be appreciated. Once again, please excuse my hopefully somewhat lessened ignorance.

On a slightly different topic, I get about 100ms of silence at the beginning of each recording. Booted as follows:

Booting with:
  Out: MME : Speakers (Realtek High Definiti
  Sample rate: 48000.000
  Latency (in/out): 0.000 / 0.107 sec
SC_AudioDriver: sample rate = 48000.000000, driver's block size = 512

Is that silence due to the latency? Dropping the buffer size down to 128, or even 16, doesn’t drop the latency much. The silence seems to remain unchanged regardless of the buffer size. Is that to be expected using MME? Or is there something I can do in the code to reduce such leading silence?

Thanks again.

I think the first thing is to think through, and be clear about, the process you want.

Your current example records the tracks concurrently. So you can’t assume that the recordings will finish in order. In that case, here’s an outline (a counter, used in a Condition function, and wait / signal):

(
s.waitForBoot {
	var samples;
	var completed = 0;
	var cond = Condition { completed >= samples.size };
	
	// for demo purposes, this is just a 'time to finish'
	samples = /*... get stuff...*/ Array.fill(5, { 10.0.rand });
	
	samples.do { |time, i|
		// simulate some process that will finish later
		thisThread.clock.sched(time, {
			"% finished\n".postf(i);
			completed = completed + 1;
			cond.signal;
		});
	};
	
	cond.wait;  // this waits for *all* of them
	
	"All done".postln;
};
)

That leaves the action function.

In my example, I watched for a signal from the EventStreamPlayer that the pattern was finished.

Because you’re using record for patterns, you don’t have access to the EventStreamPlayer. That’s probably why you couldn’t figure out where to unhang/signal. This is admittedly not easy to grasp at first – there are many kinds of processes in SC which finish in different ways.

What I would do here is to attach a cleanup to the pattern that you are recording. (Also beg pardon while I add spaces after commas – not required but IMO this is good for readability.)

player = Pbindf(m.p, \instrument, "play_mono", \buf, item, \legato, 1.0);

player = Pfset(nil, player, { cond.unhang /* or signal, for concurrent recording */ });

player.record(outputfilename, "WAV", "int16", 2, nil, 0.2, TempoClock.default, Event.default, Server.default, 0, 2);

Side note: You might be able to simplify a little by removing rt. A waitForBoot code block is already running in a routine. (There would be one reason to launch another routine. The waitForBoot routine is running on AppClock, where timing may lag. When you need musical timing, that would call for a thread on a TempoClock – but, patterns run on TempoClock by default – so there’s not a strict need to run the pattern-controller on TempoClock.)

Recording requires some initialization, and this is asynchronous. It allocates some memory from the OS. There is no way to predict how long that will take. So, it has to be outside of the audio thread, and this means that a record convenience method cannot guarantee that recording will start precisely in sync with audio.

The other thing: Patterns play with s.latency delay by default. (See the Server Timing help file.) But it takes some time to initialize, so I guess you’re seeing server latency minus recording setup time.

It is possible to start recording in sync, but you have to take the process apart. I’ll wrap it up into a function (much of it copied from Pattern:record) that you can call in place of the record lines.

(
~patternRec = { |pattern, path, headerFormat, sampleFormat, numChannels = 2, dur = nil, fadeTime = 0.2, clock(TempoClock.default), protoEvent(Event.default), server(Server.default), out = 0, outNumChannels|
	
	var buffer, nodeID, defname;
	
	pattern = if(dur.notNil) { Pfindur(dur, pattern) } { pattern };
	
	// recorder.recHeaderFormat = headerFormat;
	// recorder.recSampleFormat = sampleFormat;
	
	server.waitForBoot {
		var group, bus, free, monitor;
		
		// prepareForRecord
		buffer = Buffer.alloc(server, 32768, numChannels);
		buffer.write(path, headerFormat, sampleFormat, 0, leaveOpen: true);
		defname = SystemSynthDefs.generateTempName;
		SynthDef(defname, { |bufnum, bus|
			var sig = In.ar(bus, numChannels);
			DiskOut.ar(bufnum, sig);
		}).add;
		
		fadeTime = (fadeTime ? 0).roundUp(buffer.numFrames / server.sampleRate);
		
		bus = Bus.audio(server, numChannels);
		group = Group(server);
		Monitor.new.play(bus.index, bus.numChannels, out, outNumChannels ? numChannels, group);
		
		// important!
		server.sync;
		
		free = {
			(type: \off, id: nodeID, server: server, hasGate: false).play;
			{
				bus.free; group.free;
				buffer.free;
				server.sendMsg(\d_free, defname);
			}.defer(server.latency);
		};
		
		Pprotect(
			Pfset(nil,
				Pseq([
					// start recording synth exactly in time with pattern
					(
						type: \on, id: nodeID, instrument: defname,
						bus: bus, group: group, addAction: \addAfter,
						delta: 0,
						callback: { nodeID = ~id }
					),
					pattern <> (out: bus),
					(type: \rest, delta: fadeTime)
				], 1),
				free
			),
			free // on error
		).play(clock, protoEvent, quant: 0);
	}
};
)

Is that hard? … yeah… you’ve accidentally stumbled into a use case where you need some fairly advanced types of control. There’s no way a beginner would figure this out alone.

hjh

I was having trouble to do this, so here is a simple code for running supercollider from terminal (mac) in order to make scripted recordings (maybe it is possible to have a workaround on windows).

I am only running only sclang here because I don’t want to have the IDE being opened all the time.

I worth mention that is based on the main script for running SC on a raspberry pi as soon as it boots (you also need to start JACK before SC).

Basically, this is an rough alternative to NRT syntax, because I could not figure out a way to use NRT strictly using { }.play functions.

SuperCollider Code:

s.waitForBoot{
	s.record(duration:20);
	{SinOsc.ar(440 * SinOsc.kr(LFDNoise3.kr(Line.kr(0.1,40,20,doneAction: 2)).range(0,10)))}.play;
	Routine{
		20.wait; CmdPeriod.run;
		1.wait;thisProcess.shutdown;0.exit;
	}.play;
}

Bash code:

/Applications/SuperCollider/SuperCollider.app/Contents/MacOS/sclang 
/Users/yourusername/Desktop/yourSCfile.scd