Fast repeating PlayBuf


#1

Hello,
I’m trying to playing very fast notes with PlayBuf, (maybe it could be considered as a special kind of granulator), and to avoid too much latency, I tried the server sequencing way, but I have very little experience with the Deman Ugens and all that stuff.

// This is what I want to achieve, it works, but it has clicks:

SynthDef(\playBufTest1, {| out = 0, bufnum = 0 |
var trig, sig, env;

trig = TDuty.ar(Drand([0.25, 0.5,  1, 1.33, 1.75, 2.3, 3,  4,  6,  8], inf) / 20.0);

sig = PlayBuf.ar(2, bufnum, BufRateScale.kr(bufnum), trig, 0);
Out.ar(out, sig);

}).play(s, [\out, 0, \bufnum, b]);
)

// So I trIed to put en envelope, but it doesn’t work as expected :
//The sound drops very quickly

(
SynthDef(\playBufTest2, {| out = 0, bufnum = 0 |
var trig, sig, env;

trig = TDuty.ar(Drand([0.25, 0.5,  1, 1.33, 1.75, 2.3, 3,  4,  6,  8], inf) / 20.0);

env = EnvGen.kr(Env.new([0,1,0], [0.01, 0.5]), trig);
sig = PlayBuf.ar(2, bufnum, BufRateScale.kr(bufnum), trig, 0);
Out.ar(out, sig * env);

}).play(s, [\out, 0, \bufnum, b]);
)

// With a basic trigger, the envelope works, but there are still clicks

(
SynthDef(\playBufTest3, {| out = 0, bufnum = 0 |
var trig, sig, env;

trig = Dust.kr(5);

env = EnvGen.kr(Env.new([0,1,0], [0.01, 0.5]), trig);
sig = PlayBuf.ar(2, bufnum, BufRateScale.kr(bufnum), trig, 0);
Out.ar(out, sig * env);

}).play(s, [\out, 0, \bufnum, b]);
)

Any idea ?


#2

I’m not sure what effect you’re looking for with the envelope, but if you want very fast sounds/notes, maybe you should use another type. For example the .perc envelope, or if you still want attack and release even, a .linen could be a better choice.


#3

The only thing I want to do is to avoid the clicks.


#4

I assume that the click is cause by the sudden cut of the PlayBuf without a release. You could have a look on the behaviour of EnvGen with a sustain envelope. From the EnvGen help file:

This triggers the envelope and holds it open while > 0. If the Env is fixed-length (e.g. Env.linen, Env.perc), the gate argument is used as a simple trigger. If it is an sustaining envelope (e.g. Env.adsr, Env.asr), the envelope is held open until the gate becomes 0, at which point is released.
If gate < 0, force release with time -1.0 - gate. See Forced release below.

You would then need to delay the trigger happening on the PlayBuf for the duration of the release portion.

I haven’t tried any of that so these are just some quick thoughts. To be honest I think you should look at different UGens. Maybe GrainBuf / GrainBufJ will be more suitable for what you are trying to achieve. You should also have a look at Patterns using buffers (check the Pattern Guide series) for a way to trigger events from the language side.

Hope that is of some help.

Dionysis


#5

The wslib quark has a ugen called PlayBufCF which will cross-fade between 2 playbufs which should help with the clicking


#6

The usual way to avoid clicks that come from sudden jumps from/to other buffer positions/waveforms is crossfading. Either you define it yourself (e.g. toggle between parallel PlayBufs, but can be clumsy) or take a UGens that does it for you. I suppose the recommendation of @droptableuser - PlayBufCF is most practical in your use case.


#7

I would recommend writing one SynthDef that plays exactly one buffer segment, with an envelope, and then sequence that using patterns.

Managing sequencing and crossfading within a big SynthDef is a good way to give yourself a headache.

hjh


#8

I haven’t tested your code (BTW, you can enclose your code between three backticks like PlayBuf.ar so that it displays nicely), but you should pay attention to the values of your envelope.

Reading Env documentation

Close attention must be paid when retriggering envelopes. Starting from their value at the moment of retrigger, envelopes will cycle through all of their nodes, with the exception of the first. The first node is an envelope’s initial value and is only output prior to the initial trigger.

I would then try the following so that the ramp between 0 and 1 in 0.01s is repeated at each trigger.

env = EnvGen.kr(Env.new([0, 0, 1,0], [0, 0.01, 0.5]), trig);

#9

Thank you for all your answers.

Geoffroy: I tried your envelope. Of course, I missed an important point! It’s better, but still clicks, I guess it happens on the end, when the PlayBuf stops before jumping to the start.

joesh: the perc envelope works if it is smaller than the time between the minimum trigger time, but it sound now way too short.

dkmayer, droptableuser: PlayBufCF works and it sounds very nice. There is a small fluctuation in volume, but perhaps I should try to play with the 2 additional parameters.

The problem with my second example was a stupid typo: TDuty and EnvGen must of course have the same rate. (I’m just not sure if .ar is better sounding than .kr, or it is just my imagination).

Dionysis: I will also try to test th GrainBuf ugens, I just have to learn how they work. Of course, the language side sequencing was the very first thing that I tried, but for so small durations, the latency is a problem (and it should be played in sync with realtime audio).

In fact, I’m trying to convert a Max MSP patch that sounds very nice and I want to have the same quality. 1.45 ms for SR 44100 instead of 1 ms in Max is maybe not a problem, but the latency due to the OSC transmission could be.

Are there certain persons who think now that maybe the decision to separate the language from the server was not a so good idea ?

It would also be easier if it would be possible to start other synths for a synth! The “num” parameter from PlayBufCF simulated this in a certain way.


#10

You could check DX ugens from miSCellaneous_lib quark. They are for drate-triggered crossfading, especially you can select the crossfade type which can be responsible for audible fluctuations. I didn’t mention it at a first glance because PlayBufCF is probably more straight to use.


#11

I think that the transition from SC2 to SC3 - where this happended - is mainly regarded as a progress, especially in terms of stability and efficiency.


#12

That’s a legitimate concern, yes. If you’re streaming audio from a disk file and the buffer sequencing needs to stay in sync with that, the language-side clock is not guaranteed to do that. (This isn’t SC’s fault. Audio hardware’s sample rate is not 100% accurate. sclang thinks 1 sec = 44100 samples have elapsed, but it might be +/- 1 sample or a fraction.)

So then, yes, you would need to use triggers and envelopes.

FWIW I’d do it like:

s.boot;

// btw can someone fix the colorizer?
b = Buffer.read(s, Platform.resourceDir +/+ "sounds/a11wlk01.wav");

a = { |start = 60000|
	var rate = 12,
	trig = Impulse.ar(rate),
	splitTrigs = PulseDivider.ar(trig, 2, [0, 1]),
	egs = EnvGen.ar(Env([0, 1, 1, 0], [0.005, rate.reciprocal - 0.01, 0.005], [-2, 0, 2]), splitTrigs),
	playbufs = PlayBuf.ar(1, b, trigger: splitTrigs, startPos: start);
	sum(egs * playbufs).dup
}.play;

a.free;

The converse is also true: If you’re sequencing some things using language-side patterns, server-side sequences will drift out of sync.

Alternately, supernova has an option to measure OSC timestamps according to the system clock (default) or sample clock. My experience was that this was not usable for very long sessions, but for some minutes it should be OK. http://doc.sccode.org/Classes/ServerOptions.html#-useSystemClock

1.45 ms for SR 44100 instead of 1 ms in Max is maybe not a problem

See OffsetOut. scsynth can resolve OSC timestamps into the middle of a control block. This isn’t the same as sample accuracy, but you can get better than 1.45 ms resolution.

but the latency due to the OSC transmission could be.

s.latency is intended to correct precisely for this.

On my machine (I’ve been looking at this a lot lately), UDP packets are transmitted seemingly within 5-10 ms. s.latency by default is 200 ms, more than enough to compensate for localhost network transmission + audio hardware buffer size.

s.latency is used automatically in patterns (and can be applied manually in s.sendBundle) to take “now” (current logical time in seconds), add latency, and convert this into an OSC timestamp (a future timestamp). The server then performs the action at the requested time. If the language and server are keying off of the same clock, then everything stays in sync.

Logical time is stable, so the OSC timestamps are stable. It doesn’t matter, then, if a particular message takes 3 ms vs 8 ms to be delivered.

Are there certain persons who think now that maybe the decision to separate the language from the server was not a so good idea ?

That’s rather an overstatement of the case.

In fact, JMc acknowledged in the early paper on SC Server that this move was sacrificing the ability to trigger new synths instantaneously based on trigger signals.

In exchange, decoupling the synth opens the door to alternate client languages and types of network applications that would have been difficult otherwise.

hjh


#13

The durations that you posted are not extremely short - and I don’t understand the relation to latency here.


#14

Thank you for all these suggestions. I’m pretty busy right now, but I’ll test them all. Thank you !