Demand rate tutorial?

Hiya dev/docs gang (and everyone else of course)!

I was surfing the helpfiles recently (for fun, obviously) and I couldn’t find any explicit description of demand rate or the DUgens in general. Tim Blechmann has a single sentence about dRate in his Supernova dissertation, but that seems to be the best I can find…

Thinking from the perspective of a new user, do we consider demand rate to be self-explanatory/understood through examples? While many (if not all, haven’t checked) of the classes have examples in their respective helpfiles, I wonder if this should be clarified/explained in one of the existing Guides or Overviews? In 2023 @fmiramar suggested improving the helpfiles for Duty, TDuty, and Demand, which could also be a solution. Thoughts?

4 Likes

A tutorial/overview would be great - there are some trickinesses!

One thought is that some of the Patterns docs might also link to dRate equivalents…

1 Like

A guide with use cases would be good!

1 Like

Is there already something written about it? a blog post or a chapter in the sc book (I forget).

Many of helpfiles for the DUgens include a sentence like:

See Pser for structurally related equivalent.

…but the Pattern classes don’t link in the other direction. As there are many existing Pattern guides, I agree that comparing the D* to analogous P* classes could be useful!

Such as: server-side sequencing, single-sample feedback, granular techniques…anything else I’m forgetting that would be important to include?

I’m away from my copy of the SC book at the moment, so I can’t say for sure…there is a new edition of the book coming out this spring, perhaps it’s discussed there? Maybe @dkmayer has some insights? Ideally the docs shouldn’t have to rely on external resources, right?

We have documentation, forum threads, blog posts, etc. about .ar vs. .kr vs. .tr vs .ir, but demand rate is kind of a different beast, and I’m not sure a new user would intuitively know to look for these classes without an introduction. Later this month I can work on a demand rate guide/overview, but I’d definitely appreciate some suggestions about areas to cover: use cases, gotchas (@semiquaver which trickinessess did you have in mind?), multichannel expansion examples, maybe an example with UnpackFFT? Anything else?

1 Like

Hello Mike,

excellent initiative, I agree that more docs on this would be very helpful. I’m posting a chapter of my synthesis course on demand ugens. It’s not very polished and with German text. I’ve no time to translate atm, sorry, but feel free to take ideas and examples from there. Some examples are focussing on the analogies between Patterns and drate UGens, some are producing even an an identically sounding output.
I find the application case TGrains (bottom of file) is a good argument for their usage. miSCellaneous_lib’s Buffer Granulation tutorial also contains examples that use drate UGens (1a, 1b, 1c, 1d, 1e, 1f, 3d), though some are a bit more complicated.

best

Daniel

////////////
DEMAND UGENS
////////////


Demand Ugens sind Objekte, die Sequenzierung innerhalb des SC-Servers erlauben,
analog zu Patterns/Streams in der SC-Language.

Sinnvoll sind demand-rate Ugens vor allem dann, wenn die Sequenzierung
innerhalb sehr kurzer Zeitintervalle stattfinden soll,
da keine OSC-Messages verschickt werden muessen.

Generell sind Setups mit demand-rate Ugens beim Debugging schwieriger
als Setups mit Patterns (Debugging-Daten muessen an die Sprache
zurueckgeschickt werden). Komplizierte Verschachtelungen von
demand-rate Ugens sind problematisch, da schnell unuebersichtlich.


s.boot;

Einfaches Beispiel:

(
SynthDef(\test, { |out = 0, freq = 440, amp = 0.1|
	Out.ar(out, SinOsc.ar(freq, 0, amp)!2)
}).add
)

Wh: bei Pmono wird nur ein synth am Server gestartet,
Pmono (bzw. eigentlich der EventStreamPlayer) veranlasst dann
die Setzung der entsprechenden Parameter (hier midinote/freq)
ueber OSC-Messages.

(
x = Pmono(\test,
	\dur, 0.2,
	\midinote, Pwhite(60.0, 90)  // was bewirkt hier 60.0 statt 60 ?
).play
)


Wh: da die Werte sprunghaft veraendert werden, hoert man Klicks.
Das kann durch Lag ("Verschmierung" oder "Abrundung" des Signals)
verhindert werden.

(
SynthDef(\test_2, { |out = 0, freq = 440, amp = 0.1|
	Out.ar(out, SinOsc.ar(freq.lag(0.03), 0, amp)!2)
}).add
)


(
x = Pmono(\test_2,
	\dur, 0.2,
	\midinote, Pwhite(60, 90)
).play
)




Umschreibung desselben Beispiels mit demand-rate Ugen:
Die wesentlichen Bestandteile des obigen Pmono / EventStreamPlayers
sind erstens die Sequenz von Dauern und zweitens die Sequenz,
von der zu jedem durch die erste Sequenz definierten Einsatzzeitpunkt
ein Wert der Frequenz "gezogen" oder "gefordert" wird (fordern = demand).

Im Server ist das Schema nun folgendes: es gibt einen UGen, der
fuer die zeitlich definierte "Forderung" von Daten verantwortlich ist
(Demand) und einen fuer die Sequenz der geforderten Frequenzdaten
(in diesem Fall Dwhite).

Die Definition der Zeiten innerhalb von Demand wird von einem
Trigger-Ugen (hier Impulse) uebernommen.

Die Umrechnung der Tonhoehendaten vom MIDI-Raster in Hz
wird separat vorgenommen und ist unabhaengig von demand-rate UGens.



// Das zweite Argument von Demand ist ein Reset-Trigger,
// er wird hier nicht benoetigt (= 0)

// da hier ein Impulse mit geringer Frequenz verwendet wird,
// reicht Demand.kr und Impulse.kr

(
SynthDef(\test_2b, { |out = 0, amp = 0.1|
	var freq = Demand.kr(Impulse.kr(5), 0, Dwhite(60, 90)).midicps;
	Out.ar(out, SinOsc.ar(freq.lag(0.03), 0, amp)!2)
}).add
)


// Nun Sequenzierung im synth selbst !

x = Synth(\test_2b);

x.free;



Mit demand-rate Ugens koennen die Dauern sehr kurz sein, bis zu Samplelaenge
(Achtung: Impulse und Demand sind nun ar).
1/1000 sec ware bei Steuerung mit Pmono (1000 Osc-Messages pro Sekunde)
schon problematisch.

(
SynthDef(\test_2c, { |out = 0, dur = 0.2, lag = 0.001, lo = 60, hi = 90, amp = 0.1|
	var freq = Demand.ar(Impulse.ar(1/dur), 0, Dwhite(lo, hi)).midicps;
	Out.ar(out, SinOsc.ar(freq.lag(lag), 0, amp)!2)
}).add
)

s.scope

x = Synth(\test_2c)

x.set(\dur, 1/100)


// geht immer mehr Richtung Rauschen

x.set(\dur, 1/1000)

x.set(\lo, 40)

x.set(\hi, 90)


// lag hat auch Einfluss (laengeres lag: hoehere Frequenzen werden gefiltert)

s.freqscope

x.set(\lag, 0.0001)


x.free



///////////////////////////////////////////



demand-rate Ugens, die in aehnlicher Weise operieren wie
gleichnamige Patterns:

Dseq, Dser, Dshuf, Drand, Dxrand, Dwrand

Dswitch, Dswitch1, Dwhite, Dbrown, Ddup

Die Unterscheidung zwischen Dwhite und Diwhite (fuer Integer-Werte)
bzw Dbrown und Dibrown existiert, weil der Server an sich im Gegensatz
zur Sprache keine Integers kennt (Integers sind ja gerade
in der SC-Sprache definiert), daher braucht es eigene Objekte,
die innerhalb des Servers Integers generieren (der Server ist in C++
programmiert und diese Sprache hat natuerlich Integer-Objekte).


// Was passiert hier ?

(
SynthDef(\test_3, { |out = 0, dur = 0.2, lag = 0.001, amp = 0.1|
	var freq = Demand.ar(
		Impulse.ar(1/dur),
		0,
		Dwrand([0, 12], [0.8, 0.2], inf) + Dseq([60, 65, 67], inf)
	).midicps;
	Out.ar(out, SinOsc.ar(freq.lag(lag), 0, amp)!2)
}).add
)


x = Synth(\test_3)

x.set(\dur, 1/40)

x.free

///////////////////////////////////////////

Setzung eines Arrays waehrend demand-rate Ugen laeuft.
Wh.: Array Argumente !

Operationen von demand-rate Ugens sind in aehnlicher
Weise wie bei Patterns moeglich (+, -, *  ... )

(
SynthDef(\test_3b, { |out = 0, dur = 0.2, lag = 0.001,
	midi = #[60, 65, 67], amp = 0.1|
	var freq = Demand.ar(
		Impulse.ar(1/dur),
		0,
		Dwrand([0, 12], [0.8, 0.2], inf) + Dseq(midi, inf)
	).midicps;
	Out.ar(out, SinOsc.ar(freq.lag(lag), 0, amp)!2)
}).add
)

x = Synth(\test_3b)


x.set(\midi, [61, 62, 68])

x.free




///////////////////////////////////////////

Ebenso wie bei Patterns sind auch Schachtelungen
im Prinzip in beliebiger Tiefe moeglich


(
SynthDef(\test_3c, { |out = 0, dur = 0.2, lag = 0.001,
	midi1 = #[60, 65, 67], midi2 = #[73, 76, 80],
	repeats1 = 3, repeats2 = 3, amp = 0.1|
	var freq = Demand.ar(
		Impulse.ar(1/dur),
		0,
		Dseq([Dxrand(midi1, repeats1), Dxrand(midi2, repeats2)], inf)
	).midicps;
	Out.ar(out, SinOsc.ar(freq.lag(lag), 0, amp)!2)
}).add
)

x = Synth(\test_3c)


x.set(\midi2, [81, 82, 95])

x.set(\repeats2, 1)

x.set(\lag, 0)

Bei diesem Tempo geht die "melodische Linie"
in eine Klangfarbe mit starker rauschartiger Charakteristik ueber.

x.set(\dur, 1/500)


Wie viele Wellenlaengen gehen sich bei dieser Dauer aus ?

1/500 / (1/[81, 82, 95].midicps)

s.makeGui



// Vergleich von Recordings

x.set(\dur, 1/2000)



Diese Klangcharakteristik (das Spektrum) kann durch die Parameter der Sequenz
beeinflusst werden

x.set(\repeats1, 4)
x.set(\repeats2, 3)

x.set(\repeats1, 12)
x.set(\repeats2, 27)



x.set(\midi1, [53, 59, 81])

x.set(\midi2, [45, 55, 90])


Das geht im Resultat in Richtung der "stochastischen Synthese" (Iannis Xenakis),
bei der verschiedene elementare Wellenformen zusammengesetzt werden (hier nur Sinus).
Das Ergebnis ist ein im Allgemeinen sehr energiereiches Signal.


//////////////////////////////////////////////////

Mit einem Demand-Objekt koennen auch von mehreren demand-rate Ugens
parallel Werte gezogen werden:

Ein Trigger, zwei demand rate Ugens

(
SynthDef(\test_3d, { |out = 0, dur = 0.2, lag = 0.001,
	midi1 = #[60, 65, 67], midi2 = #[73, 76, 80], amp = 0.1|
	var freq = Demand.ar(
		Impulse.ar(1/dur),
		0,
		[Dxrand(midi1, inf), Dxrand(midi2, inf)]
	).midicps;
	// ohne dup bereits stereo wegen multichannel expansion !
	Out.ar(out, SinOsc.ar(freq.lag(lag), 0, amp))
}).add
)


x = Synth(\test_3d)

x.free;




//////////////////////////////////////////////////

Zwei Trigger, ein demand rate Ugen

Achtung, dies ist eine haeufige Fehlerursache !
Demand-rate Ugens sind viel eher mit Streams als mit Patterns
zu vergleichen: wenn mehrfach von ihnen Werte gezogen werden,
aendert sich jedesmal ihr Status.

In diesem Beispiel soll ein Effekt erzielt werden,
der beim Klavier als "nachschlagende" Oktaven bezeichnet wird:
ein Ton wird mit Verzoegerung und Oktavversetzung wiederholt.
Dazu bekommt der zweite Impulse einen Phasenoffset
und der Dseq wird in einem Ddup verpackt, damit die Werte
zweimal ausgegeben werden.


(
SynthDef(\test_3e, { |out = 0, dur = 1, lag = 0.001,
	midi1 = #[60, 65, 67], amp = 0.1|
	var freq = Demand.ar(
		// wie koennte man diese Zeile eleganter schreiben ?
		[Impulse.ar(1/dur), Impulse.ar(1/dur, 0.5)],
		0,
		Ddup(2, Dseq(midi1, inf)) + Dseq([0, 12], inf)
	).midicps;
	Out.ar(out, SinOsc.ar(freq.lag(lag), 0, amp))
}).add
)

x = Synth(\test_3e)


x.set(\dur, 1/20)

// Stereo-Effekt
x.set(\dur, 1/100)

x.set(\dur, 1/2000)

x.free;


Dieses Beispiel funktioniert nicht so wie gedacht
jeder Impulse zieht von jedem Dseq!

(
SynthDef(\test_3f, { |out = 0, dur = 1, lag = 0.001,
	midi1 = #[60, 65, 67], amp = 0.1|
	var freq = Demand.ar(
		[Impulse.ar(1/dur), Impulse.ar(1/dur, 0.5)],
		0,
		[Dseq(midi1, inf), Dseq(midi1, inf) + 12]
	).midicps;
	Out.ar(out, SinOsc.ar(freq.lag(lag), 0, amp))
}).add
)

x = Synth(\test_3f)



Umweg, zwei Demands mit Funktion definieren:

(
SynthDef(\test_3g, { |out = 0, dur = 1, lag = 0.001,
	midi1 = #[60, 65, 67], amp = 0.1|
	var freq;

	freq = ({ |i|
		Demand.ar(
			Impulse.ar(1/dur, i/2),
			0,
			Dseq(midi1, inf) + (i * 12)
		) }!2
	).midicps;
	Out.ar(out, SinOsc.ar(freq.lag(lag), 0, amp))
}).add
)

x = Synth(\test_3g)


//////////////////////////////////////////////////

Definition von Rhythmen mit demand rate ugens: Duty

(
SynthDef(\test_3h, { |out = 0, durs = #[3, 3, 2], durFactor = 0.1,
	lag = 0.003, midi = #[60, 65, 67], amp = 0.1|
	var freq = Duty.ar(
		Dseq(durs, inf) * durFactor,
		0,
		Dwrand([0, 12], [0.8, 0.2], inf) + Dseq(midi, inf)
	).midicps;
	Out.ar(out, SinOsc.ar(freq.lag(lag), 0, amp)!2)
}).add
)

x = Synth(\test_3h)

x.set(\durFactor, 0.07)

x.set(\durs, #[2, 1, 1])

x.set(\midi, #[50, 70, 90])

x.set(\midi, #[70, 71, 72])


x.set(\durFactor, 0.01)

x.set(\durFactor, 0.003)

x.free


//////////////////////////////////////////////////

TDuty ist eine Variante von Duty, bei der ein Trigger ausgegeben wird


//////////////////////////////////////////////////


Weitere Anwendung von demand rate Ugens: Granulation (TGrains)


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


Demand Ugens als Input f. TGrains-Argumente: 'rate' und 'pan'

(
x = {
    var trate, dur, rate;
    trate = 10;
    dur = 0.5 / trate;
    rate = Dseq([1.5, 1, 1], inf);
    TGrains.ar(2, Impulse.ar(trate), b, rate, MouseX.kr(0, BufDur.kr(b) * 0.1 + 0.5), dur, Dseq([-1, 1], inf), 0.1, 2);
}.scope;
)

x.free


(Poly-)Rhythmus mit TDuty als Trigger

(
x = {
    var trig, dur, rate;
	trig = TDuty.ar(Dseq([2, 1, 1, 1] / 50, inf));
    dur = 0.05;
    rate = Dseq([1.5, 1, 1], inf);
    TGrains.ar(2, trig, b, rate, MouseX.kr(0, BufDur.kr(b) * 0.1 + 0.5), dur, Dseq([-1, 1], inf), 0.1, 2);
}.scope;
)

x.free






5 Likes

@Mike_McCormick well for one Dunique is tricky… also isolation and multi-channel expansion

{
	x =  {{Dseq([300, 500, 900, 450], inf)}}; //single pair of braces won't do it here 
	SinOsc.ar(Demand.ar(Impulse.ar([1, 1.3]), 0,  x) * [1, 2])
	/10
}.play
1 Like

There is a pretty cool example of Paul stretch using demand rate somewhere on the internet!

1 Like

this one maybe? Paulstretch for SuperCollider

for multichannel expansion of demand ugens im either using:

(
{
	var triggers = Impulse.ar([1, 1.3]);
	var demand = Ddup(2, Dseq([300, 500, 900, 450], inf));
	var freqs = triggers.collect{ |localTrig|
		Demand.ar(localTrig, DC.ar(0), demand);
	};
	var sigs = SinOsc.ar(freqs * [1, 2]);
	sigs * 0.1;
}.play;
)

or if the triggers are offset by PulseDivider you can use Latch with the multichannel trigger on a single Demand Ugen with a single trigger.

1 Like

Thanks for the tips/examples folks ! I’ll try to work on a PR after the Symposium. :slight_smile:

1 Like

Here is one example with my go to Duty accumulator for granulation, demonstrating the sample and hold of a single Demand sequence with a multichannel trigger offset by PulseDivider for multichannel expansion (note the overlapping grains with different grain frequencies dervived from a single channel Demand sequence by a multichannel sample and hold):

(
var rampToTrig = { |phase|
	var history = Delay1.ar(phase);
	var delta = (phase - history);
	var sum = (phase + history);
	var trig = (delta / sum).abs > 0.5;
	Trig1.ar(trig, SampleDur.ir);
};

var rampToSlope = { |phase|
	var history = Delay1.ar(phase);
	var delta = (phase - history);
	delta.wrap(-0.5, 0.5);
};

var getSubSampleOffset = { |phase, slope, trig|
	var sampleCount = phase - (slope < 0) / slope;
	Latch.ar(sampleCount, trig);
};

var accumulatorSubSample = { |trig, subSampleOffset|
	var hasTriggered = PulseCount.ar(trig) > 0;
	var accum = Duty.ar(SampleDur.ir, trig, Dseries(0, 1)) * hasTriggered;
	accum + subSampleOffset;
};

var multiChannelAccumulator = { |triggers, subSampleOffsets|
	triggers.collect{ |localTrig, i|
		accumulatorSubSample.(localTrig, subSampleOffsets[i]);
	};
};

var multiChannelTrigger = { |numChannels, trig|
	numChannels.collect{ |chan|
		PulseDivider.ar(trig, numChannels, numChannels - 1 - chan);
	};
};

var hanningWindow = { |phase|
	1 - cos(phase * 2pi) * 0.5;
};

{
	var numChannels = 2;
	
	var stepPhase, stepTrigger, stepSlope;
	var triggers, subSampleOffsets, accumulator;
	var windowSlopes, windowPhases, grainWindows;
	var grainFreq, grainSlopes, grainPhases, grains;
	
	stepPhase = Phasor.ar(DC.ar(0), 100 * SampleDur.ir);
	stepTrigger = rampToTrig.(stepPhase);
	stepSlope = rampToSlope.(stepPhase);
	
	// distribute triggers round-robin across the channels
	triggers = multiChannelTrigger.(numChannels, stepTrigger);

	// calculate sub-sample offset per multichannel trigger
	subSampleOffsets = getSubSampleOffset.(stepPhase, stepSlope, triggers);

	// create a multichannel accumulator with sub-sample accuracy
	accumulator = multiChannelAccumulator.(triggers, subSampleOffsets);
	
	// create overlapping windows by scaling the slope of the scheduling phasor
	windowSlopes = Latch.ar(stepSlope, triggers) / max(0.001, \overlap.kr(2));
	windowPhases = (windowSlopes * accumulator).clip(0, 1);
	grainWindows = hanningWindow.(windowPhases);
	
	// create a single channel Demand sequence
	grainFreq = Demand.ar(stepTrigger, DC.ar(0), Dseq([440, 880, 220], inf));
	// sample and hold it with the multichannel trigger for multichannel expansion
	grainSlopes = Latch.ar(grainFreq * SampleDur.ir, triggers);
	// accumulate a multichannel grain phase
	grainPhases = (grainSlopes * accumulator).wrap(0, 1);
	grains = sin(grainPhases * 2pi);
	
	// multiply your carrier waveforms with the grain windows
	grains * grainWindows;
		
}.plot(0.05);
)

After spending some time with the paulStretch approach, i came up with this version. Its using PulseDivider and Playbuf and no additional windows, i think it works quite nicely. But maybe i have overseen something critical. Let me know what you think.

(
var multiChannelTrigger = { |numChannels, trig|
	numChannels.collect{ |chan|
		PulseDivider.ar(trig, numChannels, numChannels - 1 - chan);
	};
};

SynthDef(\paulStretch, { |sndBuf|

	var numChannels = 2;

	var fftSize, fftDuration, tFreq, trig;
	var triggers, positions;
	var sigs, sig, chain;

	fftSize = 8192;
	fftDuration = fftSize * SampleDur.ir;
	tFreq = 1 / fftDuration * numChannels;
	trig = Impulse.ar(tFreq);

	triggers = multiChannelTrigger.(numChannels, trig);

	positions = triggers.collect{ |localTrig|
		var localCount = Demand.ar(localTrig, DC.ar(0), Dseries(0, 1));
		localCount / (tFreq * \stretch.kr(50) * BufDur.kr(sndBuf));
	};

	sigs = triggers.collect{ |localTrig, i|
		var hasTriggered = PulseCount.ar(localTrig) > 0;
		var localSig = PlayBuf.ar(
			numChannels: 1,
			bufnum: sndBuf,
			rate: 1,
			trigger: localTrig,
			startPos: positions[i] * BufFrames.kr(sndBuf),
			loop: 1
		);
		localSig * hasTriggered;
	};

	sigs = sigs.collect{ |localSig, i|
		chain = FFT(LocalBuf(fftSize), localSig, hop: 1.0, winsize: fftSize / 2);
		chain = PV_Diffuser(chain, 1 - triggers[i]);
		IFFT(chain);
	};
	sig = sigs.sum;

	sig = sig * \amp.kr(-15).dbamp;
	
	sig = Pan2.ar(sig, \pan.kr(0));
	
	Out.ar(\out.kr(0), sig);
}).add;
)

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

Synth(\paulStretch, [\sndBuf, ~sndBuf]);