Work In Progress - true stereo to quad convolution reverberation

Dear all

As I am working on a performance patch my old Max reflexes and needs are coming back. I’ll share the finished synth don’t worry :slight_smile: It’ll have some cool modulation of the input as I usually do to avoid the static sound as well.

Anyway, for now, I want to know how to streamline this code. I can obviously save the prep’d buffers once I have the final setup. Where I am most worried is CPU spikes as there are 8 convo that are sync’d (i.e. doing their work at the same time)

Comments welcome. IRs included for fun.

// server needs tweaking
s.options.numOutputBusChannels = 4 //quad out
s.options.memSize = 2**15
s.reboot

// load my usual room
b = 4.collect{|i|Buffer.readChannel(s, "TownHall-L.wav".resolveRelative, channels: [i])};
c = 4.collect{|i|Buffer.readChannel(s, "TownHall-R.wav".resolveRelative, channels: [i])};
d = b ++ c;

e = PartConv.calcBufSize(4096, d[0])

f = Buffer.allocConsecutive(8,s,e);

f.do{|buf, i|buf.preparePartConv(d[i], 4096);};

// stereo source
(
a = {
	var pulse = Impulse.ar(1);
	Out.ar(ToggleFF.ar(pulse),LFTri.ar(111,mul: Decay.ar(TDelay.ar(pulse,0.01),0.5,0.1)));
}.play;
)

// mono I/O test
(
g = {
	var input = In.ar(0,2);
	[PartConv.ar(input[0],4096,f[0],0.2),0]
}.play(a,addAction: \addAfter)
)
g.free

// mono I quad O test
(
g = {
	var input = In.ar(0,2);
	4.collect{|i| PartConv.ar(input[0],4096,f[i],0.2)};
}.play(a,addAction: \addAfter)
)
g.free


// stereo to quad test
(
g = {
	var input = In.ar(0,2);
	4.collect{|i| PartConv.ar(input[0],4096,f[i],0.2) + PartConv.ar(input[1],4096,f[i+4],0.2)};
}.play(a,addAction: \addAfter)
)
g.free

the IRs, compliment of the HISS tools :slight_smile:

1 Like

Have you tried using an ambisonic IR as there are only three channels in 2d?

That way you can turn everything into ambisonics before you send it to the effect, therefore you only need one synth for all possible configurations, at the expense of some accuracy if you just want a specific speaker?

I did. Between us, I much prefer the sound of spaced omni for reverberation, and first order is way too imprecise as well. I’ve successfully done stereo to quad to third order in a few pieces, using the fluffy of ambisonic to my advantage over discreet sources to avoid phantom on sharp stuff, and it works really well. in this case, 8 IRs to 16 channel TOA.

My code review question is more in line with this thread and with my old C_polluted_with_Cpp habits of syntax. So the code review bit is more about peak of FFT transforms (and maybe I should have a bus and run 8 synths that I offset as in the post) or with my SClang syntax (which might be improved by some tricks I am unaware of)

I’ll post a new version with my favourite modulation at the input in a few hours, I’m going to the multichannel studio now.

Aside from doing less convolution I don’t know that you will find a less CPU intensive solution as the bottleneck will be in C++.

now with fun modulation, and with a ITU routing

// with modulation
(
g = {
	var input = DelayC.ar(In.ar(0,2),0.002,SinOsc.ar([1,2.938012],mul: SinOsc.ar([4.090824,2.37663],mul: 0.0005),add: 0.0007));
	var rev = 4.collect{|i| PartConv.ar(input[0],4096,f[i],0.2) + PartConv.ar(input[1],4096,f[i+4],0.2)};
	Out.ar(0, rev[[0,1]]);
	Out.ar(4, rev[[2,3]]);
}.play(a,addAction: \addAfter)
)
g.free

Okay, I spent 45 minutes to try and do a refactor of this code. This represents a bunch of design patterns I use for things like this - this is really not to be taken as advice or direct guidance, but only for potentially sparking some ideas related to organizing this kind of code. Happy to answer questions on any of it.

Super happy to to answer question about any of it. A few of the things happening:

  1. Slightly more organized server boot and initialization details (this is always my biggest headache)
  2. Generation of SynthDefs for multiple different input/output configurations.
  3. A prototype pattern (Pdef) to handle the setup for “convolution reverb operating on a bus inside a group”, such that I can just feed it what I want to play and don’t have to worry about allocating buses and stuff. I usually create a prototype Pdef for any complex synth or complex multi-synth setup, so I can set reasonable default values, create required buses etc., so when I re-use them later I don’t have to remember all that setup stuff. If I’m going to re-use something, I try to keep it so that I can run it later on with one line / no parameters - always a nice place to start when revisiting code.
( // INIT

// All of the initialization things in one block
~fftSize = 4096;

~server = Server.default;
~server.options.numOutputBusChannels = 4; //quad out
~server.options.memSize = 2**15;
~server.reboot;

~server.doWhenBooted {
    ~impulses = (
        4.collect{
            |i|
            Buffer.readChannel(s, "~/Downloads/TownHall-L.wav".standardizePath, channels: [i])
        } ++ 4.collect{
            |i|
            Buffer.readChannel(s, "~/Downloads/TownHall-R.wav".standardizePath, channels: [i])
        }           
    );
    
    ~server.sync;
    
    ~convBufferSize = PartConv.calcBufSize(~fftSize, ~impulses[0]);
    ~convBuffers = Buffer.allocConsecutive(8, ~server, ~convBufferSize);
    
    ~server.sync;
    
    ~convBuffers.do {
        |convBuffer, i|
        convBuffer.preparePartConv(~impulses[i], ~fftSize)
    };
    
    // Lets put these in separate lists of left and right
    ~convBuffers = ~convBuffers.clump(4);
};
)

( // SYNTHDEFS

SynthDef(\stereoTest, {
    var pulse = Impulse.ar(4);
    Out.ar(
        \out.ir(0) + ToggleFF.ar(pulse).poll,
        LFTri.ar(111) * Decay.ar(TDelay.ar(pulse, 0.01), 0.5, 0.1)
    );
}).add;

// Roll up my different channel configurations rather than 
// manually defining each one.
[
    (ins: 1, outs: 1),
    (ins: 1, outs: 4),
    (ins: 1, outs: 2),
    (ins: 2, outs: 2),
    (ins: 2, outs: 4),
].do {
    |settings|
    var ins = settings[\ins];
    var outs = settings[\outs];
    var name = "convolution_in_%_out_%".format(ins, outs).asSymbol;
    
    SynthDef(name, {
        var input, sig;
        
        input = In.ar(\in.kr, ins).asArray;
        
        sig = outs.collect {
            |index|
            input.collect({
                |in, channel|
                PartConv.ar(
                    in,
                    ~fftSize,
                    ~convBuffers[channel][index]
                )                
            }).sum
        };
        sig = \amp.kr(0.2) * sig;
        
        Out.ar(\out.ir(0), sig);
    }).add;
};

// Make a little prototype pattern for playing things back in my reverb.
// What do I need to play this back?
//  1. A bus
//  2. A group
//  3. A running convolution synth
//  4. A pattern to play though it.
//
// Values from Pdef:envir are passed in to the arguments of my Pdef function,
// which in this case is `innerFunction` and an ins / outs count. I can use this like
// a little factory to pass in a pattern from the outside, and assemble all the pieces I need
// to play it back correctly.
// A Ppar because I'm playing two things, a Pmono for my convolution since I want one synth 
// playing continuously.
Pdef(\convPlayer, {
    |innerPattern, ins, outs|
    [innerPattern, ins, outs].postln; 
    Pproto(
        {
            ~bus = (type: \audioBus, channels: ins).yield;
            ~group = (type: \group).yield;
        },
        Ppar([
            // My inner pattern - set the right out and addAction. 
            // ~group is set automatically because of Pproto.
            Pbind(
                \out, { ~bus },
                \addAction, \addToHead
            ) <> innerPattern,
            
            // My reverb
            Pmono(
                "convolution_in_%_out_%".format(ins, outs).asSymbol,
                \addAction, \addToTail,
                \in, { ~bus },
                \out, 0
            )
        ])
    )
});
);

(
// 2 in, 4 out
Pdef(\convPlayer).envir_((
    ins: 2, outs: 4,
    innerPattern: Pmono(
        \stereoTest
    )
));
Pdef(\convPlayer).play;
)

(
// 1 in, 4 out
Pdef(\convPlayer).envir_((
    ins: 1, outs: 4,
    innerPattern: Pmono(
        \stereoTest
    )
));
Pdef(\convPlayer).play;
)


(
// 1 in, 2 out
Pdef(\convPlayer).envir_((
    ins: 1, outs: 2,
    innerPattern: Pmono(
        \stereoTest
    )
));
Pdef(\convPlayer).play;
)
2 Likes

thanks so much - reading other people’s code is always a precious learning experience.

I presume that I could bundle the load, with timings, to do the proposed FFT load offseting in the other thread. I will give it a spin.

thanks again! questions might follow although you have been clear and commented it all!!!

p

I normally use a class for calculating convolution, I will add it to the bottom.

s.waitForBoot{
   ~york_ir = Conv(PathName("york_ir_stereo.wav"));

   { ~york_ir.ar(Dust.ar(2!2)) }.play
}
Classes
Conv {
	var <name;
	var <ir;
	var <fftSize;
	var <numChannels;
	*new{
		|path, fft_size=4096|
		^super.new.init(path, fft_size);
	}
	init {
		|path, fft_size|
		this.prInit(path, fft_size);
		^this;
	}
	prInit{
		|path, fft_size|
		var s = Server.default;
		var sync = { |f| var r = f.(); s.sync; r };
		var channel_count = {
			var audio_temp = sync.({Buffer.read(s, path.fullPath)});
			var c = audio_temp.numChannels;
			audio_temp.free;
			c
		}.();
		var ir_files = sync.({
			channel_count.collect{|n| Buffer.readChannel(s, path.fullPath, channels:[n]) }
		});
		var size = sync.({
			PartConv.calcBufSize(fft_size, ir_files[0])
		});
		var ir_bufs = sync.({ channel_count.collect{|n| Buffer.alloc(s, size, 1)} });

		ir = sync.({
			ir_bufs.collect{|buf, n| buf.preparePartConv(ir_files[n], fft_size)}
		});

		ir_files.do{|f| f.free };

		name = path.fileNameWithoutExtension.stripWhiteSpace;
		fftSize = fft_size;
		numChannels = channel_count;
		^this;
	}
	err {
		|signal|
		"Conv:".error;
		format("\tInput signal (%) with % channels, ", signal, if(signal.size() == 0 , 1, signal.size())).error;
		"\tmust have the same number of channels as the impulse responses, ".error;
		format("\twhich is %.", numChannels).error;
	}

	ar {
		|signal|
		if(numChannels < signal.size(),
			{ this.err(signal); ^Silent.ar(signal.size) },
			{ ^signal.size.collect{ |n|
				PartConv.ar(signal[n], fftSize, ir[n])
			}}
		)
	}
}
1 Like

a quick question: looking at your beautiful class code, I wonder: why load the buffer in prInit ? Is it because SoundFile wouldn’t be available at that time (which would allow to get your channel count by just reading the header)

Nope, its because I’m dumb and wrote it a while ago… and I don’t think I’ve used SoundFile before - might change it now, thanks for the suggestion!

1 Like