Discontinuous Multichannel Out

Hello everyone,

In two weeks I’ll be performing on a 16 speakers’ acousmonium with Supercollider, however two of the outs on the sound card are not working, so we’ll be skipping them going from 1 to 9, and 12 to 18.

Is there a way to tell Supercollider to ignore these two channels and have the usual multichannel expansion behaviour on the other ones ?

My setup consists of a ~numChannel variable in the startup file, which is then used by many SynthDefs and Ndefs in PanAz and SplayAz Ugens.

Many thanks for your help :slight_smile:

1 Like

If you can use a Linux system, there is a very easy solution. Just change the JACK connections, and I believe if you skip the two physical bad outputs, it will work with the same code.

Or, maybe you can create a node at the end of the chain, like SafetyNet, but replace some of the outputs? You will just need a ReplaceOut.ar, and change the output bus numbers. A minimal SynthDef does the job.

this is how I do it:

let’s say

~outputs = (0..5);

is an array of which outputs you want to use (in your case (0..8)++(11..17) and

~outbus

is the Bus.audio that has your master out (6 in my case, and in yours it would be 16 channels) then

var input = In.ar(~outbus,6); ~outputs.collect{|j, i| Out.ar(j,input[i])};

will map one into the other.

Did I understand right?

One idea could be to create an intermediate out as a Bus - all emitters need to send to this Bus instead of 0 directly. Then we can use a Ndef (or something similar) to simply playback this meta bus, but now implementing the remapping and shifting of the channels to the hardware.

Using the jitlib dialect this could be written like this

// establish a 16ch bus
~out = Bus.audio(server: s, numChannels: 16);

(
Ndef(\out, {
	// get signals of outbus - use InFeedback to not care about order within graph
	// but introduces a delay of the length of the server block size
	var sig = InFeedback.ar(~out, numChannels: 16);
	// split signal by splicing a Silent.ar in between
	// adjust to liking
	sig[0..7] ++ Silent.ar.dup(2) ++ sig[7..15];
// play this on the soundcard, starting from output 0
}).play(out: 0);
)


(
// some 16ch sig
Ndef(\foo, {
	SinOsc.ar(16.collect({|i| 200 + (i**2)})) * LFDNoise3.kr(0.5!16).exprange(-40.dbamp, -10.dbamp);
// output needs to be the ~out bus
}).play(out: ~out).scope;
)

(
// example how to apply to patterns to out
Pdef(\foo, Pbind(
	\dur, 0.5,
	\offset, Pwhite(0, 15),
	\out, ~out.index + Pkey(\offset),
	\pan, -1,
)).play;
)

Some relevant documentation to consider is

If you have a soundcard by RME you can use TotalMix to do this routing which is favorable because in this case you don’t have to modify your SuperCollider patch - MOTU has also some equivalent to TotalMix - it is similar to what @smoge suggested.

Enjoy your time on an acousmonium!

1 Like

If you just want to enable/disable specific outputs on your audio device, try ServerOptions:outputStreamsEnabled - this allows you to select which outputs are visible to SuperCollider. Then you can use normal contiguous output from your Synths.

2 Likes

I think this is Mac only, and probably not down to channel level.

hjh

Ah, you’re right - this is only groups of channels, not individual channels.

Thanks so much for all these responses ! It’s really appreciated.

@smoge That would be incredibly nice, unfortunately i’m on mac and they’ve decided to serve a ready meal, make only home cinema surround systems available for tweaking, which is unbelievably annoying.The node option sounds good though if it could run in the background and resist command-period.

@tremblap I think you might have understood perfectly, but i’m not sure i understand well myself. How would I go about using this in a Ndef or SynthDef ? The only idea that came to mind after a while was to try this but that doesn’t seem to be it:

~outputs = (0..8)++(11..17);

~input = In.ar(~outbus,16); ~outputs.collect{|j, i| Out.ar(j,~input[i])};

(
t = Ndef(\test, {
	var snd;
	snd = SinOsc.ar(100, 0, Env.perc().kr(2));
	snd = SplayAz.ar(~numChannels, snd, 1, 1, ~numChannels);
}).mold(~numChannels);

t.play(out: ~input);
)

@dscheiba thanks ! That seems like a good solution that i can wrap my head around. However I’m running into a problem, which i’ve had in many other contexts the past few days. This is what i get when i evaluate the Ndef(\out) code:

ERROR: Meta_Bus:audio: failed to get an audio bus allocated. numChannels: 19 server: localhost
ERROR: BusPlug:setBus: bus can't be set to nil.
CALL STACK:
	Exception:reportError
		arg this = <instance of Error>
	Nil:handleError
		arg this = nil
		arg error = <instance of Error>
	Thread:handleError
		arg this = <instance of Thread>
		arg error = <instance of Error>
	Object:throw
		arg this = <instance of Error>
	BusPlug:setBus
		arg this = <instance of Ndef>
		arg inBus = nil
	NodeProxy:setBus
		arg this = <instance of Ndef>
		arg argBus = nil
	BusPlug:defineBus
		arg this = <instance of Ndef>
		arg rate = 'audio'
		arg numChannels = 19
	BusPlug:initBus
		arg this = <instance of Ndef>
		arg rate = 'audio'
		arg numChannels = 19
	SynthDefControl:build
		arg this = <instance of SynthDefControl>
		arg proxy = <instance of Ndef>
		arg orderIndex = 0
		var ok = nil
		var rate = 'audio'
		var numChannels = 19
		var outerDefControl = nil
		var outerBuildProxy = <instance of Ndef>
		var controlNames = nil
	NodeProxy:put
		arg this = <instance of Ndef>
		arg index = nil
		arg obj = <instance of Function>
		arg channelOffset = 0
		arg extraArgs = nil
		arg now = true
		var container = <instance of SynthDefControl>
		var bundle = <instance of MixedBundle>
		var oldBus = nil
	NodeProxy:source_
		arg this = <instance of Ndef>
		arg obj = <instance of Function>
	Meta_Ndef:new
		arg this = <instance of Meta_Ndef>
		arg key = 'out'
		arg object = <instance of Function>
		var res = <instance of Ndef>
		var server = <instance of Server>
		var dict = <instance of ProxySpace>
	< closed FunctionDef >  (no arguments or variables)
	Interpreter:interpretPrintCmdLine
		arg this = <instance of Interpreter>
		var res = nil
		var func = <instance of Function>
		var code = "(
Ndef(\out, {
	// get signa..."
		var doc = nil
		var ideClass = <instance of Meta_ScIDE>
	Process:interpretPrintCmdLine
		arg this = <instance of Main>
^^ The preceding error dump is for ERROR: BusPlug:setBus: bus can't be set to nil.

I’ve also had this error message thrown about a lot when i set the server’s numOutputBusChannels to 18 channels instead of 16. The exact same Ndef might work… or not, without any changes other than setting numOutputBusChannels to a higher number at startup.

I’m also wondering how would i make sure that the Ndef(\out) keeps running in the background ? Say if i hit Command-period for instance.

Overall my main concern with this approach is the above error message, and that i’ve noticed higher CPU with a higher number of outputs channels. I didn’t realise it would affect the audio and code that much to have a higher number of server outputs. Which means I was hoping for a solution that would be a bit lower level i guess, like a hack to fully ignore certain hardware outs at startup and remain at 16 channels out in the server. (Although I don’t know if that makes much sense from a computering point of view :/)

Thanks again for all these suggestions.

Regarding the SafetyNet quark, note that there’s always a Synth at the end of the Graph:

The good news is that you can change the number of channels and the SynthDef. This way, the quark would just rearrange the signals at the very end of your graph.

And you would not need to change anything in your original code. That would be a big plus!

If I understood your setup correctly, the SynthDef can be quite simple:

Safety(s).numChannels_(18);

Safety.addSynthDefFunc(\remapOutputs, { 
    ReplaceOut.ar(12, In.ar(11, 6));
});

This will just work, probably the simplest solution without changing your project code.

It will persist Cmd-.

What is your s.options.numAudioBusChannels?

If it’s too small, and you’re allocating large segments of contiguous bus numbers, then this error becomes likely.

Part of the solution is to set a large number here, like, 512 or 1024. 1024 / 19 = 53.something, which pushes the error further into the future.

The other part of the solution is to make sure that you release buses when you’re finished using them, by calling .free. If you do ~out = Bus.audio(server: s, numChannels: 16); and then do it again, now you’re using 32 channels. Keep doing that repeatedly and you’ll run out of buses.

hjh

Would be a handy feature though!

That sounds really promising ! I just had a go at the code though and i can see from scope that all channels are still coming through. Let me know if you have any more ideas or how i may investigate this further. Reading the help docs hasn’t got me to a solution yet.

I’ve been testing it out with the following synthdef and ndef btw:

(
SynthDef(\test, {|out 0|
	var snd;
	snd = SinOsc.ar(100, 0, Env.perc().kr(2));
	Out.ar(out, snd);
}).play;
)

(
t = Ndef(\test, {
	var snd;
	snd = SinOsc.ar(100, 0, Env.perc().kr(2));
	snd = SplayAz.ar(~numChannels, snd, 1, 1, ~numChannels);
}).mold(~numChannels);

t.play;
)

Yes that seems to help ! I was already at 1024 but doubling it does the job for the few multichannel Ndefs i’m planning to use in Supercollider. I’m guessing the high number of buses has to do with the fact that i’m also booting Superdirt for Tidal Cycles.

Recently I made an installation for which I needed to skip and reorder channels. I did this, maybe you can tweak it for your piece:



s.tree = {
	"\n\nLServer routes % channels to % output channels (well, approximately).\n"
	.format(~internalNumChannels, ~externalNumChannels).postln;
	{
		var allChannels = In.ar(0, ~internalNumChannels);
		var outputChannels = allChannels;
		var soundcardChannels, monoMix, mapping;

		if(~externalNumChannels != ~internalNumChannels) {
			//outputChannels = SplayAz.ar(~externalNumChannels, allChannels)
			outputChannels = allChannels.clump(~externalNumChannels).sum;
			if(~externalNumChannels > ~internalNumChannels) {
				outputChannels = outputChannels.extend(~internalNumChannels, Silent.ar); // silence the rest
			}
		};

		monoMix = allChannels.sum * Ndef.kr(\control_monoMixForSub);
		mapping = { |channelNumber|
			[4, 3, 6, 10, 13, 16, 11, 15, \sub, nil, nil, nil, 12, 9, 14, 7, 8, 5, 1, 2]
			.indexOf(channelNumber + 1)
		};



		//outputChannels = outputChannels * Ndef.kr(\control_global);

		soundcardChannels = Silent.ar.dup(s.options.numOutputBusChannels);

		allChannels.do { |channel, i|
			var outIndex = mapping.(i);
			if(outIndex.isNil) {
				Error("channel missing, there is no % in the mapping".format(i)).throw
			};
			"mapping supercollider channel % to soundcard channel %\n".postf(i, outIndex);
			soundcardChannels[outIndex] = channel;
		};

		soundcardChannels[8] = monoMix;


		ReplaceOut.ar(0, soundcardChannels)
	}.play(addAction: \addAfter);
};

It doesn’t silence the broken channels, I thought it would not be necessary. But you can add a ReplaceOut outputting silence maybe?

(Check if that fits your setup, I’m not sure I remember)

Safety(s).numChannels_(18);

Safety.addSynthDefFunc(\remapOutputs, { 
    ReplaceOut.ar(9, Silent.ar(2));
    ReplaceOut.ar(12, In.ar(11, 6));
});

Thanks a lot for sharing this. I found an external solution in the meanwhile routing outputs to Ableton via Blackhole, but this might come in handy later down the line.

Oh i see, don’t quite understand how this works then. I tried the new alternative just now but sound is still coming out from adjacent channels 0 - 17.

I found another solution in the meanwhile routing audio to Ableton via Blackhole and remapping outputs via sends from there. I’ll stick to this solution for now as i’m having issues with anything above 16 channels in SC and I’m running out of time. Also it seems surprisingly stable.

Thanks for your suggestions though, they’ll come in handy at some point. Did some research and most sound cards for multichannel have S/PDIFs or Headphones out right in the middle, which isn’t your preferred way out to speakers for concerts. Hopefully with another tweak this would do for a nice way out. i’m surprised it’s such a biggie.

Check the helpfile. You need Safety.defaultDefName =

This is surprisingly tricky to get right. Might be nice to have a prefab solution.

1 Like

Yes, it is. What do you think should the interface look like?