Multibuffer-player synth - implementation support

Hello everyone,
I would like to ask for support for a synth on which I am having a little trouble.

My goal is to design a synthesiser that can play consecutive monophonic percussive sounds, randomly selected from a collection of pre-loaded buffers provided as an argument.
Collaterally I would like each of these samples to have independent amplitude, rate and envelope.

To achieve this, I am using PulseDivider as a starting point (taking inspiration from what I learned here).

In addition, this case study also involves a topic that I’ve always struggled a bit to keep in mind, perhaps because I don’t use it that often, namely the passing of lists as arguments to a synth (which can be solved with a NamedControl anyway, see also this discussion on the subject).

In the end, I managed to write the following code for the synth

(
SynthDef(\test, {
	|out=0,level=1.0, pan=0.0, pulseFreq=4,
	atk=0.0, rel=0.1|
	var number_of_copies = 10;
	var bufs = \buf.kr(999!6);
	var trig  = Impulse.ar( pulseFreq );
	var trigs = PulseDivider.ar(trig, number_of_copies, (0..(number_of_copies-1)));

	var sig = DC.ar(0.0)!number_of_copies;
	sig = sig.collect({
		|channel, i|
		var buf  = Demand.ar(trigs[i], 1, Drand(bufs, inf) );
		var rate = Demand.ar(trigs[i], 1, Dwhite(1.0, 2.0, inf) );
		var amp  = Demand.ar(trigs[i], 1, Dwhite(0.15, 0.35, inf) );

		var phase = Phasor.ar(trigs[i], BufRateScale.kr(buf)*rate, start:0.0, end:BufFrames.kr(buf)*100);
		var env  = EnvGen.ar(Env.perc(atk, rel), trigs[i] );
		BufRd.ar(1, buf, phase, loop:0) * env * amp ;
	});
	sig = sig * level;

	sig = Splay.ar(sig);
	Out.ar(out, sig);
}).add;
);

What I don’t understand is why, upon instantiation of the synth,

~my_samples_list; // <-- load some monophonic sample inside this list.

(
x = Synth(\test, [
	\buf, ~my_samples_list,
	\amp, 1.0,
	\pulseFreq, 6,
	\atk, 0.0,
	\rel, 1
]);
)

I get these messages in the post window, although afterwards, the synth operation seems correct.

Buffer UGen channel mismatch: expected 1, yet buffer has 3 channels
Buffer UGen channel mismatch: expected 1, yet buffer has 3 channels
Buffer UGen channel mismatch: expected 1, yet buffer has 3 channels
Buffer UGen channel mismatch: expected 1, yet buffer has 3 channels
Buffer UGen channel mismatch: expected 1, yet buffer has 3 channels
Buffer UGen channel mismatch: expected 1, yet buffer has 3 channels
Buffer UGen channel mismatch: expected 1, yet buffer has 3 channels
Buffer UGen channel mismatch: expected 1, yet buffer has 3 channels
Buffer UGen channel mismatch: expected 1, yet buffer has 3 channels
Buffer UGen channel mismatch: expected 1, yet buffer has 3 channels

How can I correct this behaviour?
what is wrong with my implementation that could be better rewritten/refactored?


Instead, here I would like to go into the details of the implementation a bit below:

  1. I’m using the namedControl \buf to allow the synth to accept a list of values as input (in our case the bufnumbers of the buffers we want to use). I don’t know if what I’m about to say makes sense or not, but what I think I’ve implemented here is the initialisation of this list to 6 copies of the number 999 (which I would assume to be a non-existent bufnum and which will then be immediately replaced when the synth is actually instantiated).

  2. does this number of copies used in the namedControl have to exactly correspond to the amount of elements inside the list I will pass as an argument?
    Let me explain: if I use !6 in the namedControl, can I then pass a list with a different size?

  3. the number_of_copies is a variable I use internally in the synth to change the structure of its UGen graph, in particular to make more or less “divisions” of the impulse on different “channels”.
    This value is intended to be fine tuned according to the sound you want to obtain. This mainly depends on the length of the samples you want the synth to playback. If those samples are relatively short, you can use a relatively small number here.

  4. my intention is not to rely too much on the inherent multichannel expansion that a certain type of code syntax would entail. Rather, I prefer to write things more clearly (at least for me) and make explicit how each audio channel the synth is generating works.

That is why I use this function:

sig = sig.collect({
	|channel, i|
	// code using index 'i' for its main logic
});

What I am wondering here is whether there is a more stringent syntax to achieve the same kind of result, especially with regard to the line just before

var sig = DC.ar(0.0)!number_of_copies;

where, in my mind, a prior creation of a multi-channel dummy signal with a corresponding number of channels was necessary.
Perhaps there is a more compact way to rewrite this section?

  1. in the line
var phase = Phasor.ar(trigs[i], BufRateScale.kr(buf)*rate, start:0.0, end:BufFrames.kr(buf)*100);

I’m using an (exaggerated) *100 multiplication for the end parameter of the Phasor in order to allow the ramp to increase its value well beyond the duration in frames of the sample I want to play, so I won’t have the sample to loop.
It will start playing again when the new trigger on its own channel occurs.

Please let me know if anything is unclear.
Thank you very much for your support

The error is coming from BufRd, a buffer has three channel but you are only reading one. See the help doc on it as there is a note. One solution: if you only want the left channel, then use Buffer.reachChannels to just get that channel.


A few other things.

Since you aren’t using the DC, you should just do var sig = 10.collect{|i| ... }.

var bufs = \buf.kr(999!6);

I believe, but could be wrong, it is better to do \bufs.kr(-1!6), this way you definitely won’t get any sound as the buf nums are positive only.

Does inf work here? I am not sure?

No. Well, I think you can pass less values, but never more. I don’t know if, when passing less values, the old values will stay the same, or be set the default. It is possible to have another control saying how many buffers you’d like to read from, but you’d have to set a maximum value. Honestly, I think its easier just to wrap the synthdef in a function have it is create a new synthdef for each size.

1 Like

Thank you so much @jordan for your support.

It also definitely seems to me that the problem is BufRd-related. However, I don’t understand why because the samples I am loading are all monophonic.

To be on the safe side, I also tried loading them explicitly in mono using the method you suggested but the problem persists.

I’m loading them with the function I usually use:

(
~my_samples_list = [];
({
	var path = PathName("/path/to/my/samples/folder/");
	path.entries.do({
		| entry |
		if( (entry.isFile).and(entry.extension == "wav"), {
			~my_samples_list = ~my_samples_list.add(
				Buffer.readChannel(s, entry.fullPath, channels:0);
			);

		});
	});
}.value);
);

and I can check that each buffer is indeed monophonic with a printout of this type (I get a series of 1s, which is what I expect).

~my_samples_list.do({ |b| b.numChannels.postln; });

You need to start the buf index on a real buffer, right now it just reads 999 (and apparently a nonexistent buffer has two channels!).

var trigs = PulseDivider.ar(trig, number_of_copies, (0..(number_of_copies-1))) 
+ Impulse.ar(0);

Adding Impulse.ar(0) will force all of the demand rate stuff to trigger initially.

This will still produce an error however, but only once!

I can’t think of a solution to this.

I’m pretty certain it has something to do with initialisation order, if it was possible to give a default value to the demand rate it might work…

See how the control and initial values don’t match.

[BufChannels.ir(buf), BufChannels.kr(buf)].poll();

I actually think this is a bug, and I know there was some work on initialisation order recently, perhaps someone else might know more!