Supercollider tips, Q/A


#81

i had a similar ambition - wanting to package that work a little better for inclusion in plug-and-play instruments. not wanting to take a lot of time or dig into the guts too much.

here’s my fork - i wrapped the script in a class, which works basically fine - but the linked line is the next thing that really needs to change; currently it loads a data file from disk and steps through it line by line - on every note - this data really needs to be a (byte array) classvar in a separate class, or something.

[ https://github.com/catfact/DX7-Supercollider/blob/master/DX7Clone.sc#L785 ]


#82

haha so basically needs a bit of tlc but the sounds are so cool. Just sat there auditioning percussive elements for like half an hour using pd-kria as a trigger… rather magical! Better stop this in case it’s actually physically accessing disk on those notes & grinding my ssd to death.


#83

This is interesting, I took a closer look: the delays of the affected zombie synths kind of points in the direction of the issue of setting gate to a different gate value within the same block:

(
s.waitForBoot { Routine {

	var interval, offset;

	SynthDef.new(\gated_sine, { arg out=0, hz=110, amp=0.5, gate=1, pan=0;
		var ampenv, snd, done;
		ampenv =  EnvGen.ar(Env.asr(), gate, doneAction:2);
		snd = ampenv * SinOsc.ar(hz) * amp;
		Out.ar(out, Pan2.ar(snd, pan));
	}).send(s);

	s.sync;

	/// make a bunch of notes and sustain them for random times
	// each is on a different thread
	interval = Array.fill(1 + 4.rand, { 2 + 8.rand });
	interval.postln;
	offset = 30 + 20.rand;
	200.do {
		Routine {
			var syn, delay;
			0.4.rand.wait;
			syn = Synth.new(\gated_sine, [
				\hz, (offset + (10.rand * interval.choose)).midicps,
				\amp, 0.02, \pan, 1.0.rand2
			]);
			delay = 1.0.rand;	
			delay.wait;
			syn.set(\gate, 0);
			~info = ~info.add( (syn: syn, delay: delay) );
		}.play;
	}

}.play; }
)

s.queryAllNodes;

// when a zombie synth appears, check below
~infoSortedByDelay = ~info.sortBy(\delay);
~synthSmallestDelay=~infoSortedByDelay[0];
~synthSmallestDelay[\delay] < (s.options.blockSize/44100); // true
~synthSmallestDelay[\syn].get(\gate, { |gate| ("gate is" + gate).debug }); // it's possible to check gate, a non-zombie synth would not reply with an answer

~synthSmallestDelay[\syn].set(\gate, 1); // this will trigger the synth for the first time
~synthSmallestDelay[\syn].set(\gate, 0); // this will release synth and invoke doneAction

#84

that was pretty much the conclusion i came too (with a very similar test to yours) for me, a wait of around 0.01-0.02 will result in the synth not freeing, above and its fine… and indeed you can retrig it with a gate=1, and then gate 0 will free it.
the only thing that surprised me, was my osc messages from another process were getting in so quickly (note-on then note-off) , but on reflection, its probably just they happen to be delivered in the same packet, so they would be very close together.


#85

oh wow - i could have sworn i put a minimum lower bound on that delay time but i guess i didn’t. ugh. so dumb

you’re absolutely right - can’t get the zombies if there is a delay of more than the blocksize between synth creation and setting gate = 0. (e.g. delay = (s.options.blockSize * 1.5)/ s.sampleRate;)

so this is the same bug - or rather, known limitation.

good news is there’s a simple, known server-side workaround - it’s weird-looking but ensures that gate=1 on synth creation, before the \set messages are processed:

	gate = gate + Impulse.kr(0);

#86

Yeah, more tests of my own showed that bundling the messages (makeBundle or bind) actually does not fix the problem either: If delta of timestamps of synth creation and release is below s.options.blockSize/samplerate synths are likely not to get triggered due to the issue.

A lower bound (1-2 ms for 64 samples @ 44100 hz) needs to be ensured between synth creation and release (unless you other workarounds like Impulse.kr in the SynthDef). That said, I’ve never included code to guarantee this and never had issues with zombie synths.

But then I typically don’t run a SuperCollider app for more than 30 minutes. I constantly tinker with code and often recompile. :slight_smile:


#87

Oh dear, seems I officially got sucked into DX7-Supercollider! Figured out how to spray the preset file into memory on init, untangled some of the preset logic & added ‘channel’ to the noteParser so you can can independently play the same note with different preset (by putting on a different ‘channel’). https://github.com/boqs/DX7-Supercollider

Anyway I very nearly have this working well enough to use now but still getting very occasional weird ‘transposition jumps’. Basically if you push the thing hard enough to start x-run-ing the synth is prone to (permanently) jumping in frequency. It intermittently does the same thing every 20 minutes or so even when I’m just putting together looped grooves with 3/4 parts. The bug is always accompanied by:
> FAILURE IN SERVER /n_free Node 17185 not found

My guess is if there’s a realtime violation, some bit of ‘global frequency state’ is getting mutated in there & not put back at the end of the block. Anyone know how to detect this error so I can reboot the dx7 object? @zebra, @cannc any other insights/suggestions?

Janky stuff about my setup that needs mentioning:

- my jack blocks are 128 samples
- I am hitting dx7 with fast noteon/noteoff pairs like this: https://github.com/boqs/DX7-Supercollider/blob/master/DX7Clone_test.scd#L93-L97
- Added DC blocker to the synth free-ing method (seems uncorrelated with the bug)

EDIT:
Discovered the above weird/confusing behaviour is specific to the lisp implementation I was running my sequencer on. Moved over from sbcl (very fast lisp) to ccl (slightly less fast lisp) & the problem can’t be reproduced. So probably I was overloading supercollider with midi messages, nothing to do with the DX7 code. Gonna clean up & PR my DX7Clone changes to catfact…


#88

To add to the initial list of questions that started this thread, here’s mine.

How can I create "“patchable” arguments in SC?

For context, I got OSC control working with SC and it was surprisingly simple. This opened up a lot of possibilities for physical control. I have a basic understanding of how a SynthDef and the UGen graph function can map parameters to OSC controllers.

But what if I want to dynamically “patch” the output of a UGen to the input of an argument after the Synth is running that graph function?

Is this what Busses are for?

P.S. I noticed that Supercollider 3.9 was released yesterday.


#89

Yeah, busses are used to route audio and control signals between synths in the node tree.

You should also plan the order of execution of synths in the node tree, especially if you intend to route audio signals.

http://doc.sccode.org/Guides/Order-of-execution.html


#90

Is this what Busses are for?

yep

for control: you can use In.kr explicitly or supply synthdef agument names to .map

s.waitForBoot {
	// synth with argument
	~snd = { arg hz=110;
		var hzlag = Lag.ar(K2A.ar(hz), 0.005);
		(SinOsc.ar(hzlag) * 0.25).dup
	}.play;

	// .kr bus
	~hzb = Bus.control(s, 1);

	// modulator
	~lfnoise = { arg hz=12.0, out=~hzb.index;
		var cv = LFNoise0.kr(hz, 20, 40).midicps;
		cv.poll;
		Out.kr(out, cv)
	}.play;

	// fn to connect them by mapping kr bus to arg
	~connect = { ~snd.map(\hz, ~hzb); };

	// click the button to connect
	{
		Button(Window("patch").front).action_({
			~connect.value;
		}).states_(["connect"]);
	}.defer;
};

for audio you need to use In.ar explicitly, and the nodes have to be in the correct execution order. (or `InFeedback.ar to insert one block of delay and not care about OOE.)

with audio, not as straightforward to use defaults or set control values from other threads (UI).


#91

I found out recently it’s even possible to map() audio busses, as long as the ugen graph function argument the bus is mapped to is “audio rate”.

Example with audio rate bus mapping (I hope I got this right):

(
s.waitForBoot {
	// synth defs
	SynthDef('snd', { arg hz=110;
		var hzlag = Lag.ar(hz, 0.005);
		Out.ar(0, (SinOsc.ar(hzlag) * 0.25).dup);
	}, [\ar]).add; // <-- \ar creates audio rate control
	
	SynthDef('lfnoise', { arg hz=12.0, out;
		var cv = LFNoise0.ar(hz, 20, 40).midicps;
		Out.ar(out, cv)
	}).add;

	0.1.wait;
	
	// synth with argument
	~snd = Synth('snd');

	// .ar bus
	~hzb = Bus.audio(s, 1);

	// modulator
	~lfnoise = Synth('lfnoise', [\out, ~hzb.index]);

	// fn to connect them by mapping kr bus to arg
	~connect = { ~snd.map(\hz, ~hzb); };

	// click the button to connect
	{
		Button(Window("patch").front).action_({
			~connect.value;
		}).states_(["connect"]);
	}.defer;
};
)

~lfnoise synth writes to audio bus ~hzb using Out.ar().
~snd audio rate hz argument is mapped to ~hzb.

I believe the map() here boils down to the n_mapa server command.


#92

This changes everything. This’ll be fun…

I also forgot about the GUI drawing classes in SC. That’s a whole other subject.


#93

As an aside.

I assumed the audio rate bus mapping exemplified above would be dependent on order of execution. But that does not seem to be the case: Even if ~snd is instantiated after ~lfnoise in the example above, causing it to be added before ~lfnoise in the node tree, ~lfnoise still modulates ~snd hz argument:

(
s.waitForBoot {
	// synth defs
	SynthDef('snd', { arg hz=110;
		var hzlag = Lag.ar(hz, 0.005);
		Out.ar(0, (SinOsc.ar(hzlag) * 0.25).dup);
	}, [\ar]).add; // <-- \ar creates audio rate control
	
	SynthDef('lfnoise', { arg hz=12.0, out;
		var cv = LFNoise0.ar(hz, 20, 40).midicps;
		Out.ar(out, cv)
	}).add;

	0.1.wait;
	
	~hzb = Bus.audio(s, 1); // .ar bus
	~lfnoise = Synth('lfnoise', [\out, ~hzb.index]); // modulator
	~snd = Synth('snd'); // add synth with argument after modulator
	~snd.map(\hz, ~hzb); // mapping ar bus to arg

	0.1.wait;

	s.queryAllNodes(true); // ~snd is executed before ~lfnoise but ~lfnoise modulates ~snd
};
)

The modulation will not work if ~snd explicitly reads from ~hzb using In.ar, when it is executed before ~lfnoise.

(
s.waitForBoot {
	// synth defs
	SynthDef('snd', { arg hz_bus;
		var hzlag = Lag.ar(In.ar(hz_bus), 0.005);
		Out.ar(0, (SinOsc.ar(hzlag) * 0.25).dup);
	}, [\ar]).add; // <-- \ar creates audio rate control
	
	SynthDef('lfnoise', { arg hz=12.0, out;
		var cv = LFNoise0.ar(hz, 20, 40).midicps;
		Out.ar(out, cv)
	}).add;

	0.1.wait;
	
	~hzb = Bus.audio(s, 1); // .ar bus
	~snd = Synth('snd', [\hz_bus, ~hzb.index]);
	~lfnoise = Synth('lfnoise', [\out, ~hzb.index]);

	0.1.wait;
	
	s.queryAllNodes(true); // ~snd is executed after ~lfnoise, so ~lfnoise can modulate ~snd
};
)

The modulation will work if ~snd explicitly reads from ~hzb using In.ar and is executed after ~lfnoise.

(
~snd.free;
~lfnoise.free;
~lfnoise = Synth('lfnoise', [\out, ~hzb.index]);
~snd = Synth('snd', [\hz_bus, ~hzb.index]);
)

s.queryAllNodes(true); // as expected, when ~snd is executed before ~lfnoise, ~lfnoise does not modulate ~snd

To me this is inconsistent, but perhaps not an issue in practice.

SuperCollider is fun but quirky. :slight_smile:


#94

ohho, that’s great! I didn’t know about n_mapa. I guess it causes samples to be read from previous block or current block, like InFeedback


#95

What is ~connect.value; doing in this context? It seems like it’s evaluating the function to map the connections. Is there an unmap?


#96

Also you can check out the JITlib guides/tutorials under browse > libraries in the documentation. That has examples of being able to patch UGens into each other in a more informal way.


#97

I looked into that a bit more because it broke my assumptions about Supercollider. I’m pretty sure this is what’s happening.

A Bus is a block of memory that you can write samples to - during each process of the Node tree, the memory in those blocks can be read or written to.

The memory in a Bus isn’t explicitly set back to zero at the end of a cycle (that’s the counter-intuitive bit)

So in your case, even though the order of execution is

  1. ~snd reads data from ~hz_bus and writes to the hardware output Buses
  2. ~lfnoise writes to ~hz_bus

The data in ~hz_bus during step 1 is whatever ~lfnoise wrote to it in step 2 during the last cycle.

To demonstrate this, we can define a Synth that will just clear out the bus

SynthDef('silence_bus', {arg bus; ReplaceOut.ar(bus, Silent.ar())}).add;

And add them in the order ~snd, ~lfnoise, ~silence

~hzb = Bus.audio(s, 1); // .ar bus
~lfnoise = Synth('lfnoise', [\out, ~hzb.index]); // modulator	
~snd = Synth.before(~lfnoise, 'snd'); // add synth before modulator
~snd.map(\hz, ~hzb); // mapping ar bus to arg
	
~silence = Synth.after(~lfnoise, 'silence_bus', [\bus, ~hzb.index]); //clear out the contents of the bus after the modulator

We hear silence because the sine wave is now reading 0 as its frequency.


#98

Also want to mention - I’ve been working on a graphical patching environment for SuperCollider on and off (mostly off) for a while.

The project is in a totally unusable state (I can’t even get it to compile tbh after some node js/clojurescript version changes).

But my idea was that I’d run the application on a Raspberry Pi (or ideally a Bela for low latency) and connect to its graphical web interface with a tablet.

Then I could patch together objects just by tapping and dragging stuff around the screen. I could place the tablet on a stand, and mess around with it while playing guitar. Hook up a couple of MIDI controllers to the Raspberry Pi, and I could experiment with different perfomance setups on the fly.

The web interface looks like this:

It works, at least in the POC sense that you can patch some stuff together and change controls.

Here’s a link to the project https://github.com/brianfay/Triggerfish although I haven’t messed around with it in a few months, and it might be a while before I get back to it.


#99

Dude, I’m doing the same thing with a DIY hardware controller that has, at the moment a 4x4 matrix of banana jacks and speaks OSC.


#100

Yeah. Audio busses are timestamped, so to speak, by Out-ugens to the cycle they were last written to and In/InFeedback coordinate reads from Busses based on these timestamps. AFAIK In will read content from a bus updated within the same cycle, otherwise silence/zeroes, InFeedback will read content from a bus updated within the previous cycle, otherwise silence/zeroes. AFAIK Out will also use the timestamps to coordinate writes - busses updated within the same cycle will be added to, otherwise content will be replaced.

This makes sense. For consistency I assumed the logic for audio busses mapped to arguments with n_mapa would work the same way. They work differently, or there are/were bugs. Here’s something fixed for 3.9:

The node tree approach with routing via global busses is very simple. I recommend the following talk by James McCartney, in which he discusses scsynth as based on the idea of making an low level audio virtual machine (registers and what not) which other programs makes use of:

https://medias.ircam.fr/xb090dd_supercollider-and-time

I recently hacked together a stripped down toy non-realtime Ruby implementation of node tree with groups/synths (monolithic SynthDefs, not ugen graph func based) and routing via global busses. It was a lot of fun.