Query Buffer channels and samplerate in a SynthDef?

Hi list,

I am writing a synthdef that play back from a Buffer. I want to query
the buffer’s samplerate from within that synth, to be able to set its
start position in seconds. My current solution is to use
Buffer(s, bufnum).sampleRate
to query it. Is there a better alternative, possibly without a hardcoded Server variable?

SynthDef(\mohn1, {arg out=0, bufnum=0, rate=1, start=0, dur, ampDB=0, fadIn=0.01, fadOut=0.01;
var env = Env.linen(attackTime: fadIn, sustainTime: dur, releaseTime: fadOut, level: ampDB.dbamp).ar(Done.freeSelf);
Out.ar(out, PlayBuf.ar(numChannels: 2, bufnum: bufnum, rate: rate, startPos: start * Buffer(s, bufnum).sampleRate, doneAction: 2) * env);

Furthermore (a classic question for sure) how can I best use the above single SynthDef with Buffers of varying channel numbers, eg. mono OR stereo buffers?



Considering that a SynthDef is a fixed arrangement of UGens (no way to add or remove on the fly), there’s no way to do this. (Well, there may be some way to do it, but it’s likely to be clumsy and inefficient.)

But it’s quite easy to make a SynthDef for one channel, and another one for two channels, and select them dynamically based on the buffer’s number of channels.


PS Please use code tags for code blocks (the </> button in the editor).

I will elaborate on what James said since I think it’s common for new users to think as you did when dealing with mono and stereo soundfiles.

Here is an example, modified from Fredriks RedSampler Quark code, of making more than one SynthDef in one go for channel expansion:

2.do({|i|		//do this two times
	SynthDef("sampler-"++(i+1), { // make "sampler-1" and "sampler-2"
		|i_out= 0, bufnum, amp= 0.7, pan= 0, offset= 0, speed= 1, loop= 0|
		var src= PlayBuf.ar(
		i+1,		// number of channels
		bufnum, // buffer number
		BufRateScale.ir(bufnum)*speed.lag(0.3), //playback rate
		1, // trigger (to jump to position in playback)
		BufFrames.ir(bufnum)*offset, // start position frame
		loop, // loop on / off
		2, // doneAction 2, free self when done

		if (i== 0, {
			Out.ar(i_out, Pan2.ar(src*amp, pan)); // if mono
			Out.ar(i_out, Balance2.ar(src[0]*amp, src[1]*amp, pan)); //if stereo

b= Buffer.read(s, Platform.resourceDir +/+ "sounds/a11wlk01.wav");
a= Synth("sampler-1", [\bufnum, b.bufnum]);

b= Buffer.read(s, stereoSoundfile.wav");
a= Synth("sampler-2", [\bufnum, b.bufnum]);

//mono or stereo
b= Buffer.read(s, "monoOrStereoSoundfile.wav");
a= Synth("sampler-"++b.numChannels, [\bufnum, b.bufnum]);
1 Like

Wow, thanks for sharing… this :


I didn’t know you could implicitly convert something into a string by simply concatenating it to a pre-existent string !

I always use : "sampler-"++(i+1).asString.

Note that beginning with an other type of variable, for example : 2++"string", yields an error.

Sorry for the off-topic.

That’s really interesting! My inelegant solution to this issue has been to read in all my samples, mono or stereo, then convert the mono ones to two identical channels. I’ll have a go this way.

I have another option. Although you could use BufSampleRate, you could actually keep with what you where doing and combine it with blindmanonacid suggestion…

This will make a new synthdef for every buffer you give it, but it will also memorise them so that you only have the latency the first time.

~sound_file_synths = nil;
~play_buf = {
	|buf, user_args, users_target, users_add_to|
	var defaultArgs = (out: 0, pan:0, rate: 1, start: 0, dur: 1, ampDB: 0, fadIn: 0.01, fadOut: 0.01);
	var args = (defaultArgs ++ (user_args ? ())).collect{|v,k| [k,v]}.asArray.flatten;
	var target = users_target ? s;
	var add_to = users_add_to ? \addToTail;
	var name = "sampler_for_" ++ buf.bufnum;
	~sound_file_synths = ~sound_file_synths ? [];
	if( ~sound_file_synths.includes(buf.bufnum), {
		'Reusing original'.postln;
		Synth(name.asSymbol,  args, target, add_to)
		'Making a new SynthDef'.postln;
		~sound_file_synths = ~sound_file_synths ++ buf.bufnum;
		SynthDef(name.asSymbol, {
			arg out=args.out, rate=args.rate, start=args.start, dur=args.dur, ampDB=args.ampDB, fadIn=args.fade.in, fadOut=args.fade.out, pan=args.pan;
			var env = Env.linen(attackTime: fadIn, sustainTime: dur, releaseTime: fadOut, level: ampDB.dbamp).ar(Done.freeSelf);
			var snd = PlayBuf.ar(numChannels: buf.numChannels, bufnum: buf, rate: rate, startPos: start * buf.sampleRate) * env;
			var panned = case
			{buf.numChannels == 1} {Pan2.ar(snd, pan)}
			{buf.numChannels == 2} {Balance2.ar(snd[0], snd[1], pan)}
			{buf.numChannels > 2} { var splay = Splay.ar(snd); Balance2.ar(splay[0], splay[1], pan); } ;
			Out.ar(out, panned);
		}).play(target, args, add_to)

Now to use it …

~b1 = Buffer.read(s, Platform.resourceDir +/+ "sounds/a11wlk01.wav");

~synth = ~play_buf.(~b1)
~synth = ~play_buf.(~b1, (\dur: 2, rate: 0.5));

~g = Group.new();
~synth = ~play_buf.(~b1, nil, ~g);

Yes, since the ++ operator relates to what is before it, as I think all operators do(?).
For example:

[1, 2]++"hello" // returns an Array
"hello"++[1, 2] // returns a String
([1, 2]++"hello").class
("hello"++[1, 2]).class

I think this is probably more common than we’d like to believe :slight_smile:

On this topic I also highly recommend Fredriks RedSampler quark for quickly throwing in soundfiles in code. It saves me a lot of time when starting something out that uses samples. I use it and my own forks of it in almost all my projects. All the bookkeeping, synthdefs, buffers etc neatly reduced to a couple lines of code.

Here are some examples of it:

a= RedSampler(s);
a.prepareForPlay(\sound1, Platform.resourceDir +/+ "sounds/a11wlk01.wav");
a.prepareForPlay(\sound2, "anotherSoundfile.wav");

a.set(\speed, 0.5);
a.free; // frees all synths on the sampler

//all available arguments to starting playback
a.play(\key, attack: 0, sustain: , release: 0, amp: 0.7, out: 0, group, loop: 0, pan: 0, speed: 1, offset: 0, startLoop: 0, endLoop: 1);

a= RedDiskInSampler(s); // same but using DiskIn

I also made a wrapper class called RedCombiSampler that makes an instance of both RedSampler and RedDiskInSampler and decides which one to use based on a threshold of the soundfile duration in seconds. I’ll attach it here if anyone finds it useful.

Usage is the same just with a threshold argument:

a= RedCombiSampler(s, 60); // soundfiles > 60 seconds use DiskIn, < 60 sec use PlayBuf
a.prepareForPlay(\sound1, "anysoundfile.wav");


RedCombiSampler {
	var keys, ramSampler, diskSampler;
	var server, thresh;
	*new{|server, thresh|
	init {|argServer, argthresh|
		keys= ();
		server= argServer ?? Server.default;
		thresh= argthresh ?? 30;
		keys= Dictionary.new;
		ramSampler= RedSampler(server);
		diskSampler= RedDiskInSamplerGiga(server);

	prepareForPlay{|key, path, startFrame= 0, numFrames|
		var f;

		f= SoundFile.new;
		if (f.duration > thresh, { keys[key]= \disk }, {keys[key]= \ram});
		{keys[key]==\ram} {ramSampler.prepareForPlay(key, path, startFrame, numFrames)}
		{keys[key]==\disk} {diskSampler.prepareForPlay(key, path, startFrame, numFrames)};
	channels {|key|
		{keys[key]==\ram} {^ramSampler.channels(key)}
		{keys[key]==\disk} {^diskSampler.channels(key)}
		{keys[key]== nil } {^nil};
	buffers {|key|
		{keys[key]==\ram} {^ramSampler.buffers(key)}
		{keys[key]==\disk} {^diskSampler.buffers(key)}
		{keys[key]== nil } {^nil};
	voicesLeft {|key|
		{keys[key]==\ram} {^ramSampler.voicesLeft(key)}
		{keys[key]==\disk} {^diskSampler.voicesLeft(key)}
		{keys[key]== nil } {^nil};
	isPlaying { |key|
		{keys[key]==\ram} {^ramSampler.isPlaying(key)}
		{keys[key]==\disk} {^diskSampler.isPlaying(key)}
		{keys[key]== nil } {^false};
	play { |key, attack= 0, sustain, release= 0, amp= 0.7, out= 0, group, loop= 0, pan= 0, speed= 1, offset= 0, startLoop= 0, endLoop= 1, id, loopId|
		{keys[key]==\ram} {ramSampler.play(key, attack, sustain, release, amp, out, group, loop, pan, speed, offset, startLoop, endLoop, id, loopId)}
		{keys[key]==\disk} {diskSampler.play(key, attack, sustain, release, amp, out, group, loop, pan, speed, id, loopId)};
	stop { arg key... args;
		{keys[key]==\ram} {ramSampler.stop(key, *args)}
		{keys[key]==\disk} {diskSampler.stop(key, *args)}
		{keys[key]== nil } {"WARNING:".postln; "RedCombiSampler: key not found"};
	length { arg key;
		{keys[key]==\ram} {^ramSampler.length(key)}
		{keys[key]==\disk} {^diskSampler.length(key)}
		{keys[key]== nil } {^nil};

	speed_{ arg val; 
	amp_{ arg val; 
	flush { arg release; 
	freeKey { arg key;
		{keys[key]==\ram} {ramSampler.freeKey(key)}
		{keys[key]==\disk} {diskSampler.freeKey(key)};
	free {
		keys= Dictionary.new;
	keys {
	loadedKeys {
	playingKeys {
	overlaps {
	numFrames {
	set {|key|
		case //only sets first voice, but that's usually all thats needed
		{keys[key]==\ram} {^ramSampler.keys[key][0]}
		{keys[key]==\disk} {^diskSampler.keys[key][0]}
		{keys[key]== nil } {"WARNING:".postln; "RedCombiSampler: key not found"};


That points to a weakness in documentation, doesn’t it? This probably should be added to the Buffers chapter in the getting started tutorial series. I guess the introductory tutorials would be a good place to provide alternatives to anti-patterns (“you’re probably thinking X is a good way to do this, but Y is better”).


1 Like