# Mapping Multichannel Buses: do not seem to expand properly..?

Hello,

I’m attempting to map a multichannel Bus to an argument in a SynthDef. My bus is allocated for 2 channels, and my SynthDef already has multichannel expansion, so I don’t believe that what I’m doing should be coercing an array after the definition has been added to the server, but perhaps that’s exactly what’s happening?

I have a simple two-channel effect, that allows for each channel’s amplitude to be controlled separately. My goal is to use a two-channel LFO (one LFO 180-degrees out of phase with the other) is to create a panning/sweep effect. This is a simplified version of what I’m working on.

When I use an In UGen to read from the Bus, everything behaves as expected. But when I use map or asMap messages, it doesn’t work correctly- I believe that it’s only using the first channel’s data, because the stereo effect doesn’t work, and both channels oscillate at the same rate and phase.

I feel like I am missing something basic in my implementation. Any help would be greatly appreciated!

Notes:
There are three SynthDefs: \panNo, \pan, \mod.
\panNo is the one that isn’t working correctly, while \pan uses an In UGen to achieve the correct results.

In \panNo, I’m using NamedControl to define my args, modIn = \modIn.kr... is the arg of consequence here, and I tried to instantiate it both without setting any values and also by creating it as an array. The Synth behaves differently in both instances, but neither are correct… I either get a duplicated signal in both channels (when I don’t definite \modIn.kr as an array), or the first channel is modulated, and the second channel is not when I do define it as array.

Even using a.set(1, 0) to set static values for the Bus doesn’t work, and I also tried calling it explicitly as an array, but that too didn’t work.

(
SynthDef(\panNo, { // this version does not multichannel expand
var out = \out.kr, amp = \amp.kr(0.1), depth = \depth.kr(1);
var modIn = \modIn.kr([0, 0]);
// var modIn = \modIn.kr; // <- I also tried it like this
var lfo, env, sig;

sig = LFPulse.ar(330) ! 2;

// lfo = In.kr(mod, 2).range(0, 1);  // <- only difference from \pan
lfo = modIn; //.range(0, 1);

sig = (sig * lfo * depth) + (sig * (1 - depth));

Out.ar(out, sig);

SynthDef(\pan, { // this version works as expected
var out = \out.kr, amp = \amp.kr(0.1), depth = \depth.kr(1);
var modIn = \modIn.kr;
var lfo, env, sig;

sig = LFPulse.ar(330) ! 2;

lfo = In.kr(modIn, 2).range(0, 1);
// lfo = modIn.range(0, 1); // <- only difference from \panNo

sig = (sig * lfo * depth) + (sig * (1 - depth));

Out.ar(out, sig);

SynthDef(\mod, {
var out = \out.kr;
var sig = [SinOsc.kr(0.5), SinOsc.kr(0.5) * -1];
Out.kr(out, sig)

a = Bus.control(s, 2);
)

a.numChannels; // returns 2, as expected
x = Synth.tail(s, \panNo, [\modIn, a.asMap]);
a.set(1, 0); // this doesn't work, nor does explicitly using an array: a.set([1, 0])

w = Synth.head(s, \mod, [\out, a]); // this doesn't work with \panNo, neither
x.free;

// don't free w yet:
y = Synth.tail(s, \pan, [\modIn, a]); // this functions correctly


This is a tricky point, and (AFAICS) not immediately well supported in the class library.

If the synth input is an array, then set-ting it needs to supply a value for every array element.

asMap gives you a mapping string for the first array element only, but none of the others.

b = Bus.audio(s, 2);
b.asMap
-> a4


But what you really need is \modIn, ['a4', 'a5'].

I can’t find any method in the main class library that will automatically produce the array of mapping strings. That’s… an oversight.

As a short-term solution, you could 1/ paste the following into an empty code window, 2/ File → Save as extension, something like ext-Bus-asMapAll.sc, 3/ recompile the class library.

+ Bus {
asMapAll {
var char;
if(index.isNil) { MethodError("bus not allocated.", this).throw };
char = if(rate == \control) { "c" } { "a" };
^Array.fill(numChannels, { |i| (char ++ (index + i)).asSymbol })
}
}


Then:

x = Synth.tail(s, \pan, [\modIn, a.asMapAll]);


The fact that we don’t have an easy way to do this deserves at least a feature request. You’re quite right to be confused by it.

hjh

2 Likes

Thank you James!

This works perfectly for my needs! Even just learning that b.asMap returns the bus number, which I hadn’t thought to try, is helpful on its own, and the class you wrote has everything happily panning away!

The Bus help file, in the examples at the bottom, actually mentions multichannel mapping. Though it’s commented out, and in the context of a single channel bus, so it might have been intended just to show correct formatting, not necessarily meant to be a testable example…

Anyway, thanks so much for your help on this!

ComputatIonally, this uses one less UGen than using In, and I think allows for far greater flexibilIty for modularity, but in a big patch, is this method more efficient in the use of CPU or less or negligible? Does the server process the data differently than it would from a direct input in the Synth?

And as far as feature requests go, should I submit one on github and reference this thread and your code? Theoretically this code could be added directly to the .asMap method, right?

Boris

I’m pretty sure that In.ar(bus, 2) produces one In UGen with two outputs. \in.ar([0, 0]) produces one Control UGen with two outputs. Performance should be just about identical.

On github, yes. But, updating asMap would break compatibility with existing code, which wouldn’t be done without a very very good reason. There should be another way.

hjh

Hi!

Very useful discussion, thank you both!
Has a feature update been requested in the end?

Soma

This hasn’t been added to releases of SuperCollider, but jamshark’s “short-term solution” works great!

I did upon a feature request ticket on Github though: Mapping Multichannel Buses to Arguments Does Not Currently Work · Issue #5484 · supercollider/supercollider · GitHub

1 Like

Thank you very much!
This is such a basic functionality, I hope it will find its way to a future release.