Route Audio to Large Array of Outputs Using For Loop Gen~ Codebox

Emmett Palaima's icon

Emmett Palaima

7月 03 2024 | 4:54 午後

Hi, I have a project where I need to make a custom panner to route audio between a large number of outputs. I'd like to do this in the Gen~ codebox object for the sake of flexibility and simplicity.

Is there a way to route audio conditionally to a large number of outputs using a for loop in the codebox? Something like this:

for (let i = 0; i < 64; i++) {
  out[i] = in1 * vol[i];
}

Trying to avoid something like having to type out out1, out2, out3, etc...

As a bonus, is there any way to make the output of a gen or a codebox a multichannel (mc) patch cable? If not these outputs will be going directly into an mc.pack object with 64 patch cables, extra manual labor I'd like to avoid if possible.

Roman Thilenius's icon

Roman Thilenius

7月 03 2024 | 11:18 午後

mc.gen~ ?

___________

Graham Wakefield's icon

Graham Wakefield

7月 04 2024 | 2:59 午後

Yes, for this purpose an mc.gen~ @chans 64 is probably the most sensible option. This basically makes 64 copies of the gen~ patch running inside it. Assuming that vol is a buffer~ with each of the audio channel weights, the gen~ patcher inside the mc.gen~ just needs to use the mc_channel operator fed into a peek vol operator to index it, and multiply the result with in 1 to send to out 1.

The only way to make a multichannel "mc" cable from gen~ is to use mc.gen~ or mcs.gen~. With mc.gen~ you get N copies of the gen~ patcher and as many input and output mc signals as you have in and out operators, where each in and out operator in the gen~ patcher reads from or writes to the a single channel of the mc signals. With mcs.gen~ you get just one gen~ patcher, with one input and one output mc signal, and each in and out in the gen~ patcher reads from or writes to one channel of the mc signal.

Emmett Palaima's icon

Emmett Palaima

7月 04 2024 | 7:13 午後

Not sure mc.gen~ will be ideal for what I am trying to do. Can the copies reference what copy number they have?

I have also read that gen~ codebox doesn't have array capability, which is kind of an extreme limitation in this case (was a little confused since the documentation for rnbo codebox looks really similar and not always the clearest which people are talking about). Are you saying that referencing a buffer could be a way for codebox to "fake" having array capability?

Here is exactly what I am trying to accomplish in c style sudocode. The really basic version is setting 64 outputs to high or low based on the amplitude of an input signal:

void process_basic(){
	int input_scaled = fabs(in1) * 64; //scaled input to 0-64
	for(int i = 0; i < 64; ++i){
		out[i] = input_scaled > i; //turn on variable number of outputs based on amplitude
	}
}

The more complex version is randomizing the outputs, such that each new output that turns on when the signal increases in amplitude is randomly selected:

int out_sel[64] = { -1 };
int num_outputs = 0;

void process_random_outputs(){

	int input_scaled = fabs(in1) * 64; //scale input as 0-64 int

	//if the amplitude has increased
	//select a new random output to turn on that has not been selected yet
	while(input_scaled > num_outputs){ 
		out_sel[num_outputs] = -1;
		while(out_sel[num_outputs] = -1){
			out_sel[num_outputs] = random(64);
			for(int i = 0; i < num_outputs ++i){
				if(out_sel[num_outputs] == out_sel[i]){
					out_sel[num_outputs] = -1;
				}
			}
		}
		++num_outputs;
	}

	//turn off outputs if amplitude has decreased 
	while(num_outputs < input_scaled){
		--num_outputs;
		out_sel[num_outputs] = -1;
	}
	
	//zero outputs by default
	for(int i = 0; i < 64; ++i){
		out[i] = 0; //zero outputs by default
	}

	//turn outputs on if they've been selected
	for(int i = 0; i < 64; ++i){
		if(num_outputs[i] != -1){
			out[num_outputs[i]] = 1;
		}
	}
}

What is the best way to accomplish this in max?

Source Audio's icon

Source Audio

7月 05 2024 | 8:36 午前

you can get away with a single matrix~ object

Emmett Palaima's icon

Emmett Palaima

7月 05 2024 | 5:30 午後

To clarify I need the outputs to update at sample rate based on the amplitude of the control signal, I don't think matrix~ does this.

This is the solution I came up with in gen~ codebox, which seems to be working well for randomized output behavior as described above:

Data out_val(64);
Data out_list(64);
History len(0);

amp = floor(in1 * 64);

while(len < amp){
	new_val = -1;
	while(new_val == -1){
		new_val = floor(abs(noise()) * 63.99);
		for(i = 0; i < len; i += 1){
			if(new_val == peek(out_list, i)){
				new_val = -1;
			}
		}
	}
	poke(out_list, new_val, len);
	len += 1; 
}

if(len > amp){
	len = amp;
}

for(i = 0; i < 64; i += 1){
	poke(out_val, 0, i);
}

for(i = 0; i < len; i += 1){
	index = peek(out_list, i);
	poke(out_val, 1, index);
}


out1 = peek(out_val, 0);
out2 = peek(out_val, 1);
out3 = peek(out_val, 2);
out4 = peek(out_val, 3);
out5 = peek(out_val, 4);
out6 = peek(out_val, 5);
out7 = peek(out_val, 6);
out8 = peek(out_val, 7);
out9 = peek(out_val, 8);
out10 = peek(out_val, 9);
out11 = peek(out_val, 10);
out12 = peek(out_val, 11);
out13 = peek(out_val, 12);
out14 = peek(out_val, 13);
out15 = peek(out_val, 14);
out16 = peek(out_val, 15);
out17 = peek(out_val, 16);
out18 = peek(out_val, 17);
out19 = peek(out_val, 18);
out20 = peek(out_val, 19);
out21 = peek(out_val, 20);
out22 = peek(out_val, 21);
out23 = peek(out_val, 22);
out24 = peek(out_val, 23);
out25 = peek(out_val, 24);
out26 = peek(out_val, 25);
out27 = peek(out_val, 26);
out28 = peek(out_val, 27);
out29 = peek(out_val, 28);
out30 = peek(out_val, 29);
out31 = peek(out_val, 30);
out32 = peek(out_val, 31);
out33 = peek(out_val, 32);
out34 = peek(out_val, 33);
out35 = peek(out_val, 34);
out36 = peek(out_val, 35);
out37 = peek(out_val, 36);
out38 = peek(out_val, 37);
out39 = peek(out_val, 38);
out40 = peek(out_val, 39);
out41 = peek(out_val, 40);
out42 = peek(out_val, 41);
out43 = peek(out_val, 42);
out44 = peek(out_val, 43);
out45 = peek(out_val, 44);
out46 = peek(out_val, 45);
out47 = peek(out_val, 46);
out48 = peek(out_val, 47);
out49 = peek(out_val, 48);
out50 = peek(out_val, 49);
out51 = peek(out_val, 50);
out52 = peek(out_val, 51);
out53 = peek(out_val, 52);
out54 = peek(out_val, 53);
out55 = peek(out_val, 54);
out56 = peek(out_val, 55);
out57 = peek(out_val, 56);
out58 = peek(out_val, 57);
out59 = peek(out_val, 58);
out60 = peek(out_val, 59);
out61 = peek(out_val, 60);
out62 = peek(out_val, 61);
out63 = peek(out_val, 62);
out64 = peek(out_val, 63);

Emmett Palaima's icon

Emmett Palaima

7月 05 2024 | 5:32 午後

Still having issues with codebox syntax tho as I try to adjust behavior further. Variables that I clearly declared exactly as stated in the documentation are not being recognized by the compiler.

Made a post about it here if anyone is able to help:

Graham Wakefield's icon

Graham Wakefield

7月 05 2024 | 5:54 午後

It seems like you are writing code to dynamically switch routing between sources and outputs, i.e. a matrix router.

How frequently will the routing assignments change -- is it up to audio rate? Do they need to be sample accurate? If neither of the answers are "yes", then there's really no need to use gen~ for this, you can perfectly well use matrix~, or perhaps even better, mcs.matrix~. Channel assignment logic is then just about generating the messages to send to matrix~. (That said, you could reproduce what matrix~ does as a gen~ patcher, see patch below).

Max Patch
Copy patch and select New From Clipboard in Max.

To answer your question "Not sure mc.gen~ will be ideal for what I am trying to do. Can the copies reference what copy number they have?" -- yes, that's what the mc_channel operator tells you.

Graham Wakefield's icon

Graham Wakefield

7月 05 2024 | 6:02 午後

... and if you really do need to do this at audio rate / with sample accuracy (and therefore using gen~), then I'd probably recommend storing the routing matrix in a buffer~. Exactly how you represent the matrix depends on what constraints the matrix has. E.g. if an input can only ever be routed to one output (or each output can only ever read from one input), then you can store these routings in each sample of a buffer~, where the value stored identifies which channel is routed.

But if many inputs can route to many outputs, you need an NxM matrix. You could do this in a buffer~ using e.g. buffer~ matrix @samps 10 16 for a 10x16 matrix, where each sample frame represents an output, and each channel represents an input, where the value stored represents the connection gain (e.g. 1 means connected, 0 means not connected, anything in between is an attenuated connection). For each output you can peek matrix @channels 10 at the appropriate index, and it will give you the 10 input gain levels. To update the matrix you write into the buffer~ at the appropriate index and channel (e.g. using poke matrix).

Anyway, I'm not sure that you really need this, I'm just posting it here in case it becomes useful for someone in the future browsing the forum!

Graham Wakefield's icon

Graham Wakefield

7月 05 2024 | 6:14 午後

Oh wait -- I think I may have overcomplicated things. Now I look at your code, you're just selectively muting our unmuting outputs, right? It's not a matrix at all.

In that case:

Max Patch
Copy patch and select New From Clipboard in Max.

The actual level calculations don't need to be in gen~, you could do them in JS, or gen (no tilde), or with Max objects, or node.script, or whatever.