Pulsar synthesis

Hi folks,

Do you have some code examples of Pulsar synthesis ? I looked everywhere online and I didn’t find anything. I am gone order the Curtis Roads books but in the meantime I would love to find something as a starting point. The nuPG of Marcin Pietruszewski seems really efficient but it’s not published yet.
Thanks!

You can look at the SC book code examples of Alberto de Campo, Chapter 16:

Further you might check the Buffer Granulation and Live Granulation tutorials from the miSCellaneous_lib quark. They contain a lot of examples that can be turned into pulsar synthesis with short grain durations / low overlap params.

Here some recent examples which grew out of a discussion about correspondence of data between currently active Pbind-generated synths. Accidentially I took pulsar synthesis in a variant inspired by Curtis Roads, who points to the interesting possibilities of layering several pulsar streams. This is done by a SynthDef which defines granular events and event patterns sequencing synths of that instrument.

s.boot;

(
// load standard buffer
~buf = Buffer.read(s, Platform.resourceDir +/+ "sounds/a11wlk01-44_1.aiff");

// plain granulator
SynthDef(\gran, { |out, gate = 1, bufNum, pos = 0.5, trigRate = 100, trigRateOsc = 100,
	trigRateDev = 0, overlap = 0.3,
	att = 0.005, rel = 0.005, // attack and release of overall envelope, not grain envelope
	susLevel = 1, amp = 0.1|
	var env, sig, trig = Impulse.ar(
		trigRate * LFDNoise3.ar(trigRateOsc).range(1 / (1 + trigRateDev), (1 + trigRateDev))
	);
	sig = TGrains.ar(
		numChannels: 2,
		trigger: trig,
		bufnum: bufNum,
		rate: 1,
		centerPos: pos * BufDur.kr(bufNum),
		dur: overlap / trigRate,
		pan: Dseq([-1, 1], inf)
	);
	// env = EnvGen.ar(Env.asr(att, 1, rel), gate, doneAction: 2);
	env = EnvGen.ar(Env.adsr(att, att, susLevel, rel, 1), gate, doneAction: 2);
	OffsetOut.ar(out, sig * env * amp)
}).add;
)

s.scope

s.freqscope



// some single gran Synths

x = Synth(\gran, [bufNum: ~buf, overlap: 0.55, trigRate: 150, pos: 0.5, amp: 0.5])

x.release

//

x = Synth(\gran, [bufNum: ~buf, overlap: 0.15, trigRate: 500, pos: 0.5, trigRateDev: 0.1, amp: 0.5])

x.release

//

x = Synth(\gran, [bufNum: ~buf, overlap: 0.5, trigRate: 200, pos: 0.3, trigRateDev: 0.7, amp: 0.5])

x.release


//

x = Synth(\gran, [bufNum: ~buf, overlap: 0.2, trigRate: 300, pos: 0.3, trigRateDev: 5, amp: 0.5])

x.release



// run long overlapping pulsar streams with Pattern
// No correspondence between current Events

(
x = Pbind(
	\instrument, \gran,
	\bufNum, ~buf,
	\trigRate, Pexprand(50, 3000),
	\trigRateDev, 0.1,
	// overlap of grains in synth itself < 1: pulsar synthesis
	\overlap, Pexprand(0.1, 1),
	\dur, 1,
	\att, 5,
	\rel, 5,
	\legato, Pexprand(1, 7.0),   // overlaps up to 7 gran synths
	\pos, Pwhite(0.2, 0.8),
	\amp, 0.5
).play
)

// takes some time to stop current long synths

x.stop


// run short overlapping pulsar streams with Pattern
// No correspondence between current Events

(
x = Pbind(
	\instrument, \gran,
	\trigRate, Pexprand(50, 500),
	\overlap, Pexprand(0.1, 1), // overlap of grains in synth itself
	\dur, 0.01,
	\legato, Pexprand(1, 7.0),   // overlaps up to 7 gran synths 
	\pos, Pwhite(0.2, 0.8),
	\amp, 0.5
).play
)

x.stop



// correspondence between current Events


(
// storage of current synth ids / Events

~currentIds = List.new;
~currentEvents = Event.new;   // container for pbind-generated Events of active synths, they are linked with ids

// Function to be used in Pattern to update running ids and corresponding Events
// Notification must be on

~updater = { |event|
	var ids = ~currentIds;
	var evs = ~currentEvents;
	thisThread.clock.sched(0, {
		event[\id].do { |id|
			OSCFunc({ ids.add(id); evs.put(id, event) },
				'/n_go', s.addr, nil, [id]).oneShot;
			OSCFunc({ ids.remove(id); evs.put(id, nil) },
				'/n_end', s.addr, nil, [id]).oneShot;
		};
	})
};


// Function to determine average trigRate of currently running synths

~getAverageTrigrate = { |defaultLo = 200, defaultHi = 300|
	var sum = 0, size = { ~currentEvents.size }.value;
	~currentEvents.do { |ev| sum = sum + ev[\trigRate] };
	(size == 0).if {
		rrand(defaultLo, defaultHi);
	}{
		sum / size;
	}
};


x = Pbind(
	\instrument, \gran,
	\count, Pseries.new, // counter
	// core: depending on a changing probability take average trigRate (~freq) or take a new value
	\avrgProb, Pseg(Pseq([1, 0], inf), 10, \sine), // Pseg is a meta pattern for LFO-like changes
	\trigRate, Pfunc { |ev|
		var sign, avrg;
		(ev[\count] == 0).if {
			rrand(200, 600);
		}{
			avrg = ~getAverageTrigrate.value.clip(50, 700);
			ev[\avrgProb].coin.if { avrg }{ avrg * rrand(0.7, 1.3) }
		};
	},

	\overlap, Pexprand(0.1, 0.2), // overlap of grains in synth itself
	\dur, 0.2,
	\legato, Pwrand((1..7), (7..1).normalizeSum, inf),  // overlaps up to 7 gran synths
	\pos, 0.6,
	\susLevel, 0.5,
	\amp, 0.6,
	\do, Pfunc(~updater)
).trace.play
)

x.stop
1 Like