Real-Time Attack and Decay Control of grain envelope for Granular Synthesis

Hello everyone!
I hope this wasn’t already somehow answered in the forum or mailing list.
I’m currently working on a project focused on concatenative synthesis or mosaicking, using the Flucoma library. My patch is set up successfully and works as intended. My question is about the granulation implementation, but first, let me provide a brief overview of my current workflow:

  • Analyzing a buffer for the corpus, getting all the slices along with their respective MFCC data, and storing it in a dataset.

  • Using kdtree to perform nearest neighbor searches in 13 dimensions (MFCCs).

  • Implementing a granular playback SynthDef (TGrain) that also captures audio from a microphone, conducts FluidOnsetSlice to generate triggers from it, and analyzes the MFCCs of the incoming audio.

  • Whenever an onset from the incoming audio is detected, a nearest neighbor search is performed to determine the position in the corpus buffer for the TGrain playback.

    Currently, my patch has a constant trigger rate for the Grains, and playback works well. However, I’d like to achieve real-time control over the attack and decay time of each grain envelope. I initially tried using TGrain3, but it seemed too buggy and didn’t deliver the desired result.

    In search of alternative methods, I looked into Buffer Granulation tutorials of Daniel, which primarily suggest a Pbind/Pattern solution. However, I’m uncertain whether this is the only useful approach.

  • I would appreciate any guidance on how to accomplish real-time control of attack and decay times for each grain within my current setup or how I could improve it to work with a different approach.

    Thank you in advance!

You’ll need to use TGrains2 or TGrains3 - these are the only one that have attack/decay control, and in general are the only granular UGens that I have found to work pretty flexibly for complex use-cases like this. What were the problems you were seeing?

A language-based approach (e.g. Pbind) is going to drastically reduce the throughput of any granulation you’re doing - this might be fine for you if you’re only triggering at a low rate (e.g. 10hz) and with a low number of simultaneous grains (e.g. 50) but larger than that and you’ll be much better off with TGrains.

1 Like

Another way might be to use GrainBuf.ar. This uses an envelope stored in a buffer for the grain env.
Then you could either change the env live by writing to the buffer. Or else fill multiple buffers with different envelopes and change the env buffer number as required.
Best,
Paul

Edit: Thinking about this more, the 2nd option is probably safer, in case you get a glitch if you write to the buffer while a grain is playing.

1 Like

Thank you for the replies! Will give TGrains3 another try and also try the GrainBuf option :slight_smile:

Hi,

Patterns are one, but certainly not the only approach. As always, much depends on personal taste. I think that Patterns are most flexible for granular synthesis, so it might be that a certain bias from my side comes across in miSCellaneous_lib’s Buffer Granulation tutorial, although it contains alternative server-side and hybrid approaches as well.
With Pattern-based granulation, you can do granulation with some hundred grains triggered per second. But indeed, as @scztt points to, you are limited to the overlap of some dozen grains or so. From my experience, not a huge restriction. Instead you gain an enourmous flexibilty in sequencing all parameters of the granulation, including the envelope. This flexibility, in my view, outweighs the “stacking power” of TGrains, GrainBuf and alike. Having said that, I still use both strategies.

GrainBuf with using a number of predefined envelopes, as @TXMod suggests, is also a possibilty, I tried it at some point in the past.

Other options with server-side granulation are there: Buffer Granulation tutorial Ex. 1f uses DXEnvGen – a multichannel envelope generator – for granulation. The DX suite help files contain further examples. There are many ways to define, control, or sequence envelope times and shapes with DX ugens.

miSCellaneous_lib’s Live Granulation tutorial Ex. 1b contains a granulation with server-driven enveloping. This could also be extended to overlapping grain streams.

Some time ago, I sketched a server-side live granulation setup with overlapping that I find quite interesting. It’s not (yet) contained in the live granulation tutorial. In the example below, the envelopes are sine-shaped and dependent on grain duration and overlap restriction (the multichannel size has to be defined beforehand). Obviously, the envelope times could be controlled otherwise.


// boot with extended resources

(
s.options.numWireBufs = 64 * 16;
s.options.memSize = 8192 * 64;
s.reboot;
)

(
~maxNumChannels  = 30;
~inBus = Bus.audio(s, ~maxNumChannels);
~maxDelay = 0.2;
)


(
SynthDef(\granulator_1, {
    arg outBus = 0, overallAtt = 0.01, overallRel = 0.1, grainDur = 0.1,
		pulseFreq = 10, numChannels = 10, channelOffset = 0,
		pos = 0, posDev = 0, delayRange = 5,
		gate = 1, preAmp = 1, limitAmp = 0.1;

	var maxGrainDur = ~maxNumChannels / pulseFreq - 0.001;
	var maxDelay = 20;
	var grainEnv = Env.sine(min(maxGrainDur, grainDur));

    var trig = Impulse.ar(pulseFreq);

	// multichannel trigger
    var trigs = { |i|
		PulseDivider.ar(trig, ~maxNumChannels, ~maxNumChannels - 1 - i)
	} ! ~maxNumChannels;

	// multichannel envelope for Grains (multichannel expansion because of trigs)
    var grainEnvGens = EnvGen.ar(grainEnv, trigs);

	// global env
	var envGen = EnvGen.ar(Env.asr(overallAtt, 1, overallRel), 1, doneAction: 2);

	// multichannel in signal is wrapped if numChannels < maxNumChannels
	var in = { |i|
		Select.ar(i % numChannels + channelOffset % ~maxNumChannels, In.ar(~inBus, ~maxNumChannels))
	} ! ~maxNumChannels;
	var grains = in * grainEnvGens * envGen * preAmp;
	var sig;

	// core workhorse, each grain gets own position
	// array of stereo signals
	sig = { |i|
		var localPos = (pos.lag(1) + TRand.ar(posDev.neg, posDev, trigs[i])).clip(-1, 1);
		var localDelay = LFDNoise3.ar(0.1).range(0, delayRange.lag(1) * 0.001);
		Pan2.ar(DelayL.ar(grains[i], ~maxDelay, localDelay), localPos)
	} ! ~maxNumChannels;

	// mix to stereo
	Out.ar(0, Limiter.ar(Mix(sig), limitAmp))

}, metadata: (
	specs: (
		grainDur: [0.001, 0.2, 3, 0, 0.015],
		pulseFreq: [1, 500, 3, 0, 10],
		pos: [-1, 1, \lin, 0, 0],
		posDev: [0, 0.2, \lin, 0, 0.25],
		delayRange: [0, 10, 2, 0, 1],
		numChannels: [1, ~maxNumChannels, \lin, 1, ~maxNumChannels],
		channelOffset: [0, ~maxNumChannels - 1, \lin, 1, 0],
		preAmp: [0, 3, \lin, 0, 0.5],
		limitAmp: [0, 0.5, 2, 0, 0.1]
	)
)
).add;
)

(
SynthDescLib.global[\granulator_1].makeGui;
s.scope;
s.freqscope;
)

// start gui


// only white noise

x = { Out.ar(~inBus, WhiteNoise.ar(0.1 ! ~maxNumChannels)) }.play



// granulator can continue

x.free



// 2 synths play on sub-busses

(
~n = ~maxNumChannels / 2;
x = { Out.ar(~inBus.subBus(0, ~n), WhiteNoise.ar(0.1 ! ~n)) }.play
)

y = { Out.ar(~inBus.subBus(~n, ~n), Saw.ar({ exprand(150, 1500) } ! ~n)) }.play

x.free

y.free



// saw with fixed frequencies

x = { Out.ar(~inBus, Saw.ar({ exprand(100, 3000) } ! ~maxNumChannels, 0.1)) }.play

x.free


// glisson synthesis

(
x = {
	Out.ar(
		~inBus,
		Saw.ar(
			LFDNoise3.ar({ rrand(0.1, 5) } ! ~maxNumChannels).exprange(150, 5000),
			mul: 0.1
		)
	)
}.play
)

x.free



// mixed noise

(
x = {
	var n = ~maxNumChannels.div(3);
	Out.ar(
		~inBus,
		[
			WhiteNoise.ar(0.1 ! n),
			Impulse.ar({ rrand(1000, 3000) } ! n),
			ClipNoise.ar(0.1 ! n)
		].flop.flat
	)
}.play
)

x.free


// mixed sources

(
x = {
	var n = ~maxNumChannels.div(5);
	Out.ar(
		~inBus,
		[
			SinOsc.ar(LFDNoise3.ar(0.3 ! n).range(300, 1500), 0, 0.3),
			WhiteNoise.ar(0.1 ! n),
			Saw.ar(LFDNoise3.ar(0.2 ! n).range(300, 1500), 0.2),
			Crackle.ar(1.9 ! n),
			Pulse.ar(LFDNoise3.ar(0.4 ! n).range(300, 1500), 0.5, 0.2)
		].flop.flat
	)
}.play
)

x.free



// mixed sources, omit channels of multichannel bus

(
x = {
	// only 10 of 30
	var n = 2;
	Out.ar(
		~inBus,
		[
			WhiteNoise.ar(0.1 ! n),
			SinOsc.ar(LFDNoise0.ar(2.1 ! n).range(300, 3000)),
			ClipNoise.ar(0.1 ! n),
			Saw.ar(LFDNoise0.ar(3 ! n).range(300, 3000)),
			Crackle.ar(1.9 ! n),
		].flop.flat
	)
}.play
)

x.free


// read a soundfile

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


// multichannel playback, shifted playback rates

(
x = {
	Out.ar(
		~inBus,
		PlayBuf.ar(
			1,
			b,
			((1..~maxNumChannels) / (~maxNumChannels * 10) + 1) * BufRateScale.kr(b),
			loop: 1
		)
	)
}.play
)

x.free

// stop granulator in GUI





Cheers

Daniel

5 Likes

That’s really cool - thanks for sharing.