Rewrite jpverb for multichannel output

Hello new forum! Running this on Arch Linux/Qutebrowser and it seems to work just fine.

I’ll give it a spin and see how well it reacts :wink:

I am working on a multi-channel setup for a performance with 2-6 channels output, depending on the space I am performing in. So far I have been using the excellent jpverb for my verb needs, and made it work by panning one output channel to 0 and the other to 1 in PanAz.ar. However, I imagine there must be a better/more elegant solution to this problem, I just don’t have (yet) the programming chops to implement this. How difficult would it be to scale jpverb up to an arbitrary number of channels? Am I likely to break stuff badly by trying to rewrite the original UGen? What should I take into consideration? Which learning resources are good for this kind of thing?

it’s best to post your code so people can understand what you are doing better.

I am assuming that you do not want to merely spread the stereo image over n channels using something like SplayAz ?

have you had a look inside JPverb.sc ?

// author: Julian Parker
// license: GPL2+
// year: 2013

JPverb {
	*ar { | in, t60(1.0), damp(0.0), size(1.0), earlyDiff(0.707), modDepth(0.1), modFreq(2.0), low(1.0), mid(1.0), high(1.0), lowcut(500.0), highcut(2000.0)|
		in = in.asArray;
		
		^JPverbRaw.ar(in.first, in.last, damp, earlyDiff, highcut, high, lowcut, low, modDepth, modFreq, mid, size, t60)
	}
}

JPverbRaw : MultiOutUGen
{
  *ar { | in1, in2, damp(0.0), earlydiff(0.707), highband(2000.0), highx(1.0), lowband(500.0), lowx(1.0), mdepth(0.1), mfreq(2.0), midx(1.0), size(1.0), t60(1.0) |
      ^this.multiNew('audio', in1, in2, damp, earlydiff, highband, highx, lowband, lowx, mdepth, mfreq, midx, size, t60)
  }

  *kr { | in1, in2, damp(0.0), earlydiff(0.707), highband(2000.0), highx(1.0), lowband(500.0), lowx(1.0), mdepth(0.1), mfreq(2.0), midx(1.0), size(1.0), t60(1.0) |
      ^this.multiNew('control', in1, in2, damp, earlydiff, highband, highx, lowband, lowx, mdepth, mfreq, midx, size, t60)
  } 

  checkInputs {
    if (rate == 'audio', {
      2.do({|i|
        if (inputs.at(i).rate != 'audio', {
          ^(" input at index " + i + "(" + inputs.at(i) + 
            ") is not audio rate");
        });
      });
    });
    ^this.checkValidInputs
  }

  init { | ... theInputs |
      inputs = theInputs
      ^this.initOutputs(2, rate)
  }

  name { ^"JPverbRaw" }
}

Hi Capogreco,

Sorry for the sloppiness, guess that’s what happens when trying to do everything in between bouts with my 1 and a half year old kid running around the house…

This is my implementation of the jpverb:

~numSpeakers = 6;
SynthDef(\jpverb, {
arg in=16, out, amp=1, attack=0.02, rel=0.1, gate=1, da=2,
revtime=1, damp=0, size=1, early=0.707;
var source, sig, sigL, sigR, env;
env = EnvGen.kr(Env.asr(attack, 1, rel), gate: gate, doneAction: da);
source = In.ar(in, 2);
sig = JPverb.ar(source, revtime, damp, size, early);
sigL = SplayAz.ar(~numSpeakers, sig[0], spread: 0.2, width:~numSpeakers, center: 0);
sigR = SplayAz.ar(~numSpeakers, sig[1], spread: 0.8, width:~numSpeakers, center: 0);
sig = sigL + sigR;
sig = sig * amp * env;
Out.ar(out, sig);
}).add;

And now that I look at it, I already see that there is potential stupdity lurking in there: not quite sure why I decided to sum the left and right channels there towards the end. Need to take a deeper look into that, I guess…
Thanks for posting the underlying code! Will take a look at it tomorrow when I have a bit more fresh brain cells ready. In the meantime, if any great ideas on improving the code come up, I am very happy to get them.

a neat way might be to use multichannel expansion to wrap your stereo image to the speaker array.

~numSpeakers = 6;
SynthDef(\jpverb, {
	arg in = 16, out = 0, amp = 1, attack = 0.02, rel = 0.1, gate = 1, da = 2,
	revtime = 1, damp = 0, size = 1, early = 0.707;
	var source, sig, env;
	env = EnvGen.kr (Env.asr (attack, amp, rel), gate: gate, doneAction: da);
	source = In.ar (in, 2) * env;
	sig = 0!~numSpeakers + JPverb.ar (source, revtime, damp, size, early);
	Out.ar (out, sig);
}).add;

I’ve condensed things somewhat, and applied the env before the reverb (to let out the reverb tail), but the multichannel work is happening on the line where the JPverb ugen is being assigned to sig.

because 0!~numSpeakers is an array with arbitrarily more elements than the two-element array that is returned by JPverb, SuperCollider’s multi-channel expansion loops back to the start of the smaller array to make them equivalent, in order to apply the + operator.

so an array [ L, R ], as returned by JPverb.ar, would expand to [ L, R, L, R, L .. ], alternating between the L and R signals until it has ~numSpeakers number of elements.

I haven’t tested the code above, but the technique should represent at least one way to address the problem of fitting a stereo image to a speaker array of size n.

1 Like

I like that implementation! Never encountered this way of using the ! operator before, and I can’t find it anywhere in the docs. Where can I find more examples and documentation about it? I would love to learn more about how to use it, seems like it would be directly applicable to lots of other multichannel expansions in my setup as well…

if you write Object, and then cmd + d on the text, you’ll be taken to the help documentation for that class.

while there, cmd + f will focus the search field, where searching for dup will take you to the explanation of the .dup method.

essentially x!n is shorthand for x.dup (n), which duplicates x, n number of times in an array.

edit: you can find out more about ! and other operators in the SuperCollider help docs under Syntax Shortcuts and Symbolic Notatons.

I realized I hadn’t understood well enough how SplayAz works, and this seems to work for my purposes, which is basically to create a bit of a smear across the space. I am not too concerned with realistic reverberation for this particular piece, and this will hopefully do the trick. Still need to test it on a multi-channel setup, though…

~numSpeakers = 6;
SynthDef(\jpverb, {
        arg in=16, out, amp=1, attack=0.02, rel=0.1, gate=1, da=2, 
        revtime=1, damp=0, size=1, early=0.707;
        var source, sig, env;
        env = EnvGen.kr(Env.asr(attack, amp, rel), gate: gate, doneAction: da);
        source = In.ar(in, ~numSpeakers); // necessary, as the expected input is from multichannel UGens
        source = Mix.ar(source) * env; // but then mixed down to a mono signal for reverb processing. Thanks for pointing out the issue with the envelope!
        sig = JPverb.ar(source, revtime, damp, size, early);
        sig = SplayAz.ar(~numSpeakers, sig, spread: 0.5, width:~numSpeakers); // This seems to create the necessary spread of the sound across the speakers
        Out.ar(out, sig);
}).add;

Thanks a lot for the help!

1 Like