Best way to trigger a sample to play

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

Then:

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

(
c = Bus.control(s, 1);

a = { |gate = 1, amp = 0.1|
    var main_eg = EnvGen.kr(Env.asr(0.01, 1, 0.1), gate, doneAction: 2);
    var trig = NamedControl.tr(\trig, 0);
    var toggle = ToggleFF.kr(trig);
    var gates = [toggle, toggle <= 0];
    var egs = EnvGen.kr(Env.asr(0.01, 1, 0.01), gates);
    var players = PlayBuf.ar(1, b, BufRateScale.kr(b),
        gates,
        startPos: TRand.kr(0, BufFrames.kr(b) * 0.5, gates)
    );
    Out.ar(0, ((players * egs).sum * (main_eg * amp)).dup)
}.play(args: [trig: c.asMap]);

// automated trigger for testing: any kr trigger-y signal will work
// (which you already have)
t = {
    var trigFreq = SinOsc.kr(0.1).exprange(1, 12);
    Impulse.kr(trigFreq)
}.play(outbus: c);
)

hjh

1 Like

@jamshark70 what does the trig: c.asMap do. And are var trig and /trig the same? Guess I don’t understand NamedControl either. Staring at the documentation now, but not yet sinking in.

You can ignore that, because you already have a trigger signal coming from the bela pins.

See Node:map help for info on synth control bus mapping. But you don’t strictly speaking need bus mapping for your final version. This was just a convenient way to provide a trigger signal without requiring user interaction.

No. Alternately:

a = { |gate = 1, amp = 0.1|
    var main_eg = EnvGen.kr(Env.asr(0.01, 1, 0.1), gate, doneAction: 2);
    var trigVariableToProvideAccessToTheSynthControlInput = NamedControl.tr(\trigNameOfTheSynthControlInput, 0);
    var toggle = ToggleFF.kr(trigVariableToProvideAccessToTheSynthControlInput);
    var gates = [toggle, toggle <= 0];
    var egs = EnvGen.kr(Env.asr(0.01, 1, 0.01), gates);
    var players = PlayBuf.ar(1, b, BufRateScale.kr(b),
        gates,
        startPos: TRand.kr(0, BufFrames.kr(b) * 0.5, gates)
    );
    Out.ar(0, ((players * egs).sum * (main_eg * amp)).dup)
}.play(args: [trigNameOfTheSynthControlInput: c.asMap]);

Within a SynthDef function, we can use variables to refer to parts of the DSP graph. (Arguments are, practically speaking, variables – the only difference is that arguments receive values from outside when the function is called, while variables are completely local, 100% under control of the scope in which they are defined.)

When a Synth runs in the server, you can get control information into it using buses (In.kr) or control inputs. It’s inconvenient to allocate buses for everything, so SynthDefs have named control inputs. We use control input a lot, and buses somewhat less. (Buses are good for control values that are shared across many nodes; control inputs are good for per-synth values.)

SynthDef function arguments are converted into control inputs, and control input channels are passed into the function at SynthDef build time (which is how UGen inputs get connected to these control inputs). So there are already two elements to gate = 1, for instance: the control input named gate with default = 1, and the argument/variable gate which receives a handle to that control input.

For demo purposes, I wanted to be sure that the trigger signal is a true trigger (1 only momentarily). For control inputs, this can be done by defining it as a trigger. This can be done with argument names, but it’s a bit icky. It’s more clear to define the control explicitly as “trigger rate”: .tr. .tr exists basically only for the NamedControl class, where it produces a TrigControl control input. (Note that \someName.kr(...) is just a shortcut for NamedControl – and that the shortcut syntax also confused you earlier in this thread.)

But doing it this way exposes the two elements, which, in an argument list, are collapsed into one expression. The NamedControl needs… well… a name, which is provided as a symbol \trig. And to access the control elsewhere, it needs a variable, which is declared: var trig. But the name by which the control is exposed to the outside world, and the variable name used locally to access the control, are conceptually not the same thing.

(It’s also possible to dispense with the var and just write ToggleFF.kr(\trig.tr(0)). This is a matter of taste. Personally, I find it bothersome to have to scan through the entire function to find control input definitions, which may then be anywhere in a line, not only near the left. Others very much prefer not to be bothered with variable declarations. But, the problem in that case is that you have to be sure every time \trig.tr is written in the same SynthDef that it has the same default value. I’ve actually argued elsewhere that neither arguments nor \name.kr(default) syntax are good solutions to the problem, and that what’s needed is a bespoke syntax. I prototyped such a thing, but it hasn’t come into wide use. At the end of the day, it’s largely a matter of preference. For some, SynthDef function arguments are more transparent and intuitive at least for “normal” kr single-valued controls, but others feel that there is unpleasant magic lurking behind them and they prefer writing inline \symbol.kr style. I know which way I like better, but I’m not going to tell you what you should do.)

hjh