Best way to trigger a sample to play

I’m trying to make a sample play every time I tap my foot. It actually works, but there’s an issue. If the trigger isn’t long enough, the sample stops before it’s complete. And if I trigger is long enough that I tap my foot twice it won’t play the sample the second time.

To add to the complexity, I also want to have a loop mode, where it plays the sample continuously, but restarts the sample if I tap my foot. But perhaps one step at a time :slight_smile:

Here’s a small sample that demonstrates the problem.

  SynthDef("simpleTrig", {
  		arg buf;
		var trigger = Trig.ar(Impulse.ar(1), 1.6);
		var snd=PlayBuf.ar(2, buf, BufRateScale.kr(buf), trigger: trigger);
		Out.ar(0, snd);
  }).send(s);

So, with this example I want the sample to play once a second, but since the trigger is 1.6s long, it only plays every two seconds. If the sample is 1.2 secs long, and I set the trigger to .9s, it will play every second, but the sample gets cut off.

Maybe there’s another method I should focus on, but PlayBuf seemed like the right direction. Any advice?

Thanks,
Paul

I’d do it something like this…

SynthDef(\playSample, {
	var snd = PlayBuf.ar(2, \buf.kr(-1), loop: 2);
	var env = Env.asr(0.01, 1, 0.2).ar(doneAction: 2, gate: \gate.kr(1));
	Out.ar(\out.kr(0), snd * env);
}).add;

~previousSampler = nil;
~sendFootTrigger = {
	~previousSampler !? {|p| p.set(\gate, 0) };   // remove old if exists
	~previousSampler = Synth(\playSample, [...]); // fill with your args
};

~sendFootTrigger.(); // call in midi func, or whatever the footswitch is connected with

This way, you are making a new synth each time you touch the switch and removing the old one with a fade out.
Each synth plays the sample independent of the others.


I haven’t tested this code, but it should work…

Thanks @jordan I think that makes sense. I was wondering if making a new Synth each time was better. But I’m not familiar with the syntax you used to call create the synth. In may case the trigger comes from a control bus. Should I create another synthdef which calls the function when it receives the trigger from the control bus. I will continue researching, but the solution didn’t seem obvious.

This is my trigger code which works, but with the original limitation, in case that helps. It also handles the loop scenario, and that actually works because it keeps playing until I retrigger.

  SynthDef("sh",{ 
		arg buf = 0, rate = 1.0, loop = 0, scale = 1.0, vol = 1.0, t_reset=0, wetBus = 0, deviceId = 0, trigBus;

		var snd;

		var button = Trig.ar(In.ar(trigBus), BufDur.kr(buf));
		
		var latch = SetResetFF.ar(button, t_reset) ;
		
		var volmultraw = ((1.0 - scale) + (button*scale));
				
		snd=PlayBuf.ar(2, buf, BufRateScale.kr(buf) * rate, trigger: button, loop: loop * latch, startPos: 0, doneAction: 0) * volmultraw * vol;
		//Synth.new(\playAndFree, [\buf: buf]);
		Out.ar(wetBus, snd);
		
  }).send(s);

Maybe some important points:

  • The control bus signal is the velocity so it should be used to control the volume of the PlayBuf (0.0 - 1.0)
  • I’m using OSC to communicate, so I know I can create SynthDefs with /new command. From your example, I’m thinking maybe some stuff is client side, but not confident with sclang yet.

Research a little more and I have questions on your code

  • What is \gate, \buf, \out? I’m not familiar with this syntax.
  • Maybe I could use SendTrig and OSCFunc to run the ~sendFootTrigger function, but first I would have to receive the velocity as well as the buffer. Also would this idea increase latency since the msg goes from the server to client back to server? Doesn’t seem like the right approach to me.

And maybe also important is the sample will be dynamic, currently passed into the Synth.new as an arg.

I did get SentTrig to send the velocity value and play the sound, but I can only send one param AFAIK. The buffer sample is fixed in my current attempt. Again, I have to believe there is some solution that doesn’t require a call going back and forth to the client.

Still poking around

Where is the trigger control bus value coming from?

\xyz.kr(defaultValue) is a shortcut syntax for NamedControl.kr(\xyz, defaultValue). Some prefer this as a way to embed SynthDef inputs into the DSP logic – a matter of preference, not of functionality at play time (though there are some subtle differences which haven’t yet arisen in this use case). If that synth were written as follows, its behavior from the user’s point of view would be no different – so, write it whichever way is more comfortable for you:

SynthDef(\playSample, {
	arg buf = -1, gate = 1, out = 0;
	var snd = PlayBuf.ar(2, buf, loop: 2);
	var env = Env.asr(0.01, 1, 0.2).ar(doneAction: 2, gate: gate);
	Out.ar(out, snd * env);
}).add;

hjh

Thanks for the \out explanation. Seems to be many different ways to do things in sclang.

The control bus is coming from the synthdef defined in this post: Using a boolean value to determine if a ctlbus value is set - #25 by paulelong

I got this working using SendReply, and it works, but maybe there are some intermittent delays. Not sure it’s due to the back-and-forth, but it is seems pretty responsive. I need to do more testing, but if there’s a better way I’d like to know.

Here’s the complete code as it right now using SendReply. The actual code continaus other things, but I just chopped that out, I’m hoping correctly.

	var digitalPins = [ 0, 1, 2, 3, 4, 5, 6, 7];
	var triggerCount = 1;

	SynthDef(\trigDetector, {
		arg ctlBus, deviceId;

		var all = Array.fill(digitalPins.size, { |i|
		    DigitalIn.ar(digitalPins[i]);
		});
		
		// Convert all the bit array to a binary value
		var allId = all.reverse.reduce({ arg total, bit; total * 2 + bit }, 0);
		
		// Make sure they are set by delaying a little bit
		var delay_allId = TDelay.ar(in: allId, dur: 0.001);

		// Create bitmasks.  
		//  velMask is how many bit used for velocity
		//  devMask used for devices, of which there are only 2
		var velMask = pow(2, digitalPins.size - triggerCount) -1;
		var devMask = pow(2, triggerCount) - 1;

		// Use the masks to get the velocity and device that was activated
		var velocity = allId.bitAnd(velMask);
		var device = (allId >> (digitalPins.size - triggerCount)).bitAnd(devMask);

		// scale button between 0 and 1 based on velMask
		var button = velocity / velMask;

		// Combined this line to make it work
		var signal = Trig.ar( Select.ar(abs(device - deviceId) < 0.1, [K2A.ar(0), delay_allId * button]), 0.1);
		
		Out.ar(ctlBus, signal);
		Out.kr(ctlBus, signal);
	}).send(s);

	SynthDef("sh",{ 
		arg buf = 0, rate = 1.0, loop = 0, scale = 1.0, vol = 1.0, t_reset=0, wetBus = 0, deviceId = 0, trigBus;

		SendReply.ar(In.ar(trigBus) > 0.01, "/trg", [40, In.ar(trigBus) , buf]);
  }).send(s);

	SynthDef(\playSample, { |buf, vol = 1 |
		var snd = PlayBuf.ar(2, buf) * vol;
		var env = Env.asr(0.01, 1, 0.2).ar(doneAction: 2, gate: \gate.kr(1));
		Out.ar(\out.kr(0), snd * env);
	}).add;

	~previousSampler = nil;
	~sendFootTrigger = { | k, v |
		~previousSampler !? {|p| p.set(\gate, 0) };   // remove old if exists
		~previousSampler = Synth(\playSample, [\buf: k, \vol: v]); // fill with your args
	};

	o = OSCFunc({ 
		arg msg, time;
		
		// [time, msg].postln; 
		if(msg[2] == -1 && msg[3] == 40, { ~sendFootTrigger.value(msg[5], msg[4]) });
  	},'/trg');

On a cursory glance, looks reasonable.

The round-trip server → client → server delay should be related to the “max output latency” reported during server startup, which is determined by the hardware buffer size. If this is about 10 ms, I’d expect the round trip to be basically undetectable to the ear. 20 ms is possibly audible but not catastrophically. 40+ ms is pushing the upper limit. Lowering hardware buffer size would raise CPU usage but get a closer-to-instantaneous response.

If the round trip latency is acceptable, this approach will be a lot simpler than trying to manage it all in the server.

hjh

Hello,
Do you use Bela? Which device is the foot switcher?

I have automated test that animates the digital inputs on the Bela as a test once a second. I increments by 8, so I hear the sound get louder before it goes back to zero, but after some time, it fails and it seems the server is not reachable. It also gets a little erradict, playing multiple sound, and then it finally stops. Could there be a memory leak in this code?

I dumped out some of the server stats. It mostly shows the first line, until it stops working, then the last two

UnitGens=290 NumSynths=32 defs=113, loadedSynths=113

UnitGens=340 NumSynths=37 defs=113, loadedSynths=113

UnitGens=5 NumSynths=1058087492 defs=5178443, loadedSynths=5178443

Not sure if that last value is real. After a while, 5-6 minutes, it stops and my code to query the server fails.

@prko the footpedal is a ESP8622 which talks to an ESP32. The ESP32 is connected to the digital ins on the bela.

1 Like

I don’t know if you can configure

  • a toggle switch: send 1 when pressed (and held) and send 0 when not pressed (and released)
  • a bang switch: send 1 only when pressed for a moment

Anyway, DigitalIn is a UGen, so you seem to be able to do all this in a SynthDef.
I am very sorry that I cannot find my Bela at the moment and I do not have a device to connect to the DigitalIn.

So I simulated the following code using MouseButton.kr:

(
SynthDef(\simpleTrig, { |buf|
	var trigger,snd;
	trigger = MouseButton.kr(0, 1, 0).poll;
	snd = PlayBuf.ar(1, buf, BufRateScale.kr(buf), trigger: trigger) ! 2; // * see below
	Out.ar(0, snd);
}).add // .send(s) // <- if Bela does not need .send(s).
)

x = Synth(\simpleTrig, [buf: b]) // * see below.
x.free; b.free

Please ignore that PlayBuf plays the buffer when evaluating this line. (I am not sure, but this could be a bug because the mouse button is not pressed when the line is evaluated.) If you want to avoid this, you can use the following code as a template:

(
SynthDef(\simpleTrig, { |buf|
	var trigger, env, snd;
	trigger = MouseButton.kr(0, 1, 0).poll;
	env = Env.asr(0.005, 1, 0.005).kr(gate: trigger); // 0.005 can be changed.
	snd = PlayBuf.ar(1, buf, BufRateScale.kr(buf), trigger: trigger) ! 2 * env;
	Out.ar(0, snd);
}).add 
)

x = Synth(\simpleTrig, [buf: b])
x.free; b.free

You could modify your last code in this way, I think.
The MouseButton.kr part should be written in more lines starting with DigitalIn.ar.

I am not sure if this is helpful. If not, sorry!

As for my current issue/memory leak, it could be due to how I’m handling the SendReply/SendTrig messages. Seems adding a SendTrig in my working code also causes issue. I’ll look into that next.

@prko thanks for your suggestion. The problem that I run into is that when the buf I’m playing is long and I try to retrigger it, it doesn’t play. I’m not sure how MouseButton works as a trigger, but I wonder, if you use a long sample, can you quickly click the mouse and restart the trigger?

Um. There are a few things I need to check to get your answer right (Or I can help you):
Points to check:

  1. What do you mean by a long buffer? How long is it?
  2. How fast do you retrigger if you retrigger the fastest you can?
  3. (Optional) Do you use only one buffer or more than one?
  4. Are the amplitudes of the buffers normalised (standardised was wrong. sorry)? If so, to which?

Questions 3 to 4 are to check that there is no clipping when two or more buffers are played together.

Just one buffer which I load from a file. In my case if I have a sample that is 1 sec long, I have to wait till it’s complete before I can play it a second time. For that example, clicking the mouse more than once a 1 sec, would fail to make it play again.

I’m not sure what you mean by standardized, but you can use a wav file as an example.

Sorry, it was a mistyped word of normalised.

Which of the following do you want to happen with a 1-second sound file when you press the footswitch 0.5 seconds after the previous buffer has started playing?

  • The previous playback should stop, and then the new playback should start immediately.
  • The previous playback should continue, and the new playback should start. So the first half of the new playback will be played with the last half of the previous playback.

It isn’t necessary to hold the trigger open for x seconds, so if that’s the issue, you can drop the Trig1.

But the reason why the Synth approach was proposed is: upon retrigger, to be formally correct, the old player should fade out (briefly) and the new player should concurrently crossfade in. This is possible to do all on the server side (minimum, a trigger hits a ToggleFF, and then you’d have two players, each with its own envelope, where one envelope’s gate is the toggle and the other’s is 1 - toggle, and these gates also drive the PlayBuf objects), but the new-synth approach is simpler. So in that sense, I agree with Jordan’s suggestion.

ServerOptions should be setting a limit on the number of synths – ServerOptions | SuperCollider 3.12.2 Help, “The default is 1024” – plus, if these are in response to triggers, you could have a maximum of 22000+ triggers per second, meaning 1058087492 / 22000 = 48094.886 seconds or over half a day, no way this can happen suddenly.

I think one of the first steps to working with any interactively derived signal is to scope it, print it, be sure you understand its behavior. Is the trigger signal doing something weird before the server goes haywire? Or test the new synth logic with an automated routine, not using the trigger… if the problem happens, or doesn’t happen, then either way it narrows down where to look.

hjh

Hi James, slight off topic…
do you happen to know if it has ever been proposed (or is indeed possible) to create a synth from inside another synth? Since it is all just osc messages, this seems like it might be possible - ugen adds command to ring buffer which is gathered by the server’s osc message processor. There would probably be some limitations (perhaps with args), but might be reasonable here, particularly if latency was an issue?

Maybe the syntax would be:

{
   CreateSynthOnTrigger.kr(\mySynth, \groupID.kr, Impulse.kr(1));
}.play

Thanks @jamshark70 getting of the trigger solves my problem though without a nice fade-out. But it seems to be working perfectly now, and I can tap my foot as fast as I need to.

I think the crazy NumSynths value is due to a bug in my code. I’m sending/receiving OSC from my code through sockets, but in a very rudimentary way that only works with the simple commands to setup synths. But the extra stress of sending continuous trigger OSC commands most likely caused intermixing of commands and responses. My solution for that is to have a thread store socket responses in a circular queue, but that will probably take some time to implement. It was always a TODO, but not important until now. In the end @jordan solution is nicer because, as you mention, the sound can fade out because it cuts it off with this current solution you mentioned of getting rid of the trigger. But perhaps having a server side way to call a synthdef as @jordon mentioned might also be useful in the future.

Your advice about using scopes and simple automated routines is precisely what I learned, so very important for any beginners. It seems that trying to print values isn’t a reliable way to understand what is happening when everything is a signal.

Thanks so much for everybody’s help!
-Paul

1 Like