File: PG_Cookbook05_Using_Samples.schelp

package info (click to toggle)
supercollider 1%3A3.13.0%2Brepack-3
  • links: PTS, VCS
  • area: main
  • in suites: trixie
  • size: 80,296 kB
  • sloc: cpp: 476,363; lisp: 84,680; ansic: 77,685; sh: 25,509; python: 7,909; makefile: 3,440; perl: 1,964; javascript: 974; xml: 826; java: 677; yacc: 314; lex: 175; objc: 152; ruby: 136
file content (324 lines) | stat: -rw-r--r-- 11,447 bytes parent folder | download | duplicates (4)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
title:: Pattern Guide Cookbook 05: Using Samples
summary:: Using samples
related:: Tutorials/A-Practical-Guide/PG_Cookbook04_Sending_MIDI, Tutorials/A-Practical-Guide/PG_Cookbook06_Phrase_Network
categories:: Streams-Patterns-Events>A-Practical-Guide

section::Using samples

subsection::Playing a pattern in time with a sampled loop

A deceptively complex requirement.

To synchronize patterns with a sampled loop, the basic procedure is:

numberedlist::
## Determine the loop boundaries.
## Adjust tempo and/or playback rate.
## Sequence individual loop segments alongside other patterns.
::

strong::1. Determine the loop boundaries::

Use an external audio editor to identify a segment of the source file that loops in a musically sensible way. For this example, we will use "a11wlk01.wav" because it's readily available. Empirically, we can find that the segment from 0.404561 to 3.185917 seconds produces a rhythm that can be parsed as one bar of 4/4 time.

The segment beginning (0.404561) and ending (3.185917) are important. We will use them below.

Choose these values carefully. If the loop boundaries are wrong, then the musical result will not make sense.

strong::2. Adjust tempo and/or playback rate::

To match the loop tempo with sequencing tempo, we need to know both:

list::
## the loop's original tempo, and
## the desired playback tempo.
::

strong::Original tempo: :: The duration of the segment chosen in part 1 is 3.185917 - 0.404561 = 2.781356 seconds. This spans one bar = 4 beats, so the duration of one beat is 2.781356 / 4 = 0.695339 seconds/beat. SuperCollider specifies tempo as beats per second, so we need the reciprocal: 1 / 0.695339 = 1.4381474359988 beats/second (86.289 bpm).

code::
((end - start) / numBeats).reciprocal

// or, algebraically
(end - start).reciprocal * numBeats

// which equals
numBeats / (end - start)
::

strong::Playback tempo: :: In principle, you can choose any tempo you like. The loop-segment player should provide a code::rate:: parameter, where the rate is code::desiredTempo / originalTempo::. If the original tempo is, as above, 86.289 bpm and you want to play at 72 bpm, you have to scale the sample's rate down by a factor of 72 / 86.289 = 0.83440531238049.

strong::3. Sequence individual loop segments alongside other patterns::

It might be tempting to loop a link::Classes/PlayBuf:: so that the loop runs automatically on the server, but it can easily drift out of sync with the client (because of slight deviations in the actual sample rate). Instead, it is better to define a SynthDef that plays exactly one repetition of the loop, and repeatedly triggers it once per bar.

The primary bell pattern accents the downbeat and follows with a randomly generated rhythm. The catch is that we have no assurance that the link::Classes/Pwrand:: code::\dur:: pattern will add up to exactly 4 beats. The link::Classes/Pfindur:: ("finite duration") pattern cuts off the inner Pbind after 4 beats. This would stop the pattern, except link::Classes/Pn:: repeats the Pfindur infinitely, placing the accent in the right place every time.

The loop actually starts with a half-beat anacrusis, so link::Classes/Ptpar:: delays the bell patterns by 0.5 beats.

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

// one loop segment
SynthDef(\oneLoop, { |out, bufnum, start, time, amp, rate = 1|
	var sig = PlayBuf.ar(1, bufnum,
		rate: rate * BufRateScale.kr(bufnum),
		startPos: start, loop: 0
	),
	env = EnvGen.kr(Env.linen(0.01, time, 0.05, level: amp),
		doneAction: Done.freeSelf);
	Out.ar(out, (sig * env).dup);
}).add;

SynthDef(\bell, { |out, accent = 0, amp = 0.1, decayScale = 1|
	var exc = PinkNoise.ar(amp)
	* Decay2.kr(Impulse.kr(0), 0.01, 0.05),
	sig = Klank.ar(`[
		{ ExpRand(400, 1600) } ! 4,
		1 ! 4,
		{ ExpRand(0.1, 0.4) } ! 4
	], exc, freqscale: accent + 1, decayscale: decayScale);
	DetectSilence.ar(sig, doneAction: Done.freeSelf);
	Out.ar(out, sig.dup)
}).add;
)

(
var start = 0.404561, end = 3.185917,
beatsInLoop = 4,
originalTempo = beatsInLoop / (end - start);

TempoClock.tempo = originalTempo;

p = Ptpar([
	0, Pbind(
		\instrument, \oneLoop,
		\bufnum, b,
		\amp, 0.4,
		\start, start * b.sampleRate,
		\dur, beatsInLoop,
		\time, Pkey(\dur) / Pfunc { thisThread.clock.tempo },
		\rate, Pfunc { thisThread.clock.tempo / originalTempo }
	),
	0.5, Pn(
		Pfindur(4,
			Pbind(
				\instrument, \bell,
				\accent, Pseq([2, Pn(0, inf)], 1),
				\amp, Pseq([0.3, Pn(0.1, inf)], 1),
				\decayScale, Pseq([6, Pn(1, inf)], 1),
				\dur, Pwrand(#[0.25, 0.5, 0.75, 1], #[2, 3, 1, 1].normalizeSum, inf)
			)
		),
	inf),
	0.5, Pbind(
		\instrument, \bell,
		\accent, -0.6,
		\amp, 0.2,
		\decayScale, 0.1,
		\dur, 1
	)
], 1).play;
)

// for fun, change tempo
// resyncs on next bar
TempoClock.tempo = 104/60;

p.stop;
::

The use of Ptpar above means that you could stop or start only the whole ball of wax at once, with no control over the three layers. It's no more difficult to play the layers in the independent event stream players, using the quant argument to ensure the proper synchronization. See the link::Classes/Quant:: help file for details on specifying the onset time of a pattern.

code::
(
var start = 0.404561, end = 3.185917,
beatsInLoop = 4,
originalTempo = beatsInLoop / (end - start);

TempoClock.tempo = originalTempo;

p = Pbind(
	\instrument, \oneLoop,
	\bufnum, b,
	\amp, 0.4,
	\start, start * b.sampleRate,
	\dur, beatsInLoop,
	\time, Pkey(\dur) / Pfunc { thisThread.clock.tempo },
	\rate, Pfunc { thisThread.clock.tempo / originalTempo }
).play(quant: [4, 3.5]);

q = Pn(
	Pfindur(4,
		Pbind(
			\instrument, \bell,
			\accent, Pseq([2, Pn(0, inf)], 1),
			\amp, Pseq([0.3, Pn(0.1, inf)], 1),
			\decayScale, Pseq([6, Pn(1, inf)], 1),
			\dur, Pwrand(#[0.25, 0.5, 0.75, 1], #[2, 3, 1, 1].normalizeSum, inf)
		)
	),
inf).play(quant: [4, 4]);

r = Pbind(
	\instrument, \bell,
	\accent, -0.6,
	\amp, 0.2,
	\decayScale, 0.1,
	\dur, 1
).play(quant: [4, 4]);
)

[p, q, r].do(_.stop);

b.free;
::

subsection::Using audio samples to play pitched material

To use an instrument sample in a pattern, you need a SynthDef that plays the sample at a given rate. Here we will use link::Classes/PlayBuf::, which doesn't allow looping over a specific region. For that, link::Classes/Phasor:: and link::Classes/BufRd:: are probably the best choice. ( strong::Third-party extension alert:: : LoopBuf by Lance Putnam is an alternative - find it in the strong::sc3-plugins:: package.)

Frequency is controlled by the rate parameter. The sample plays at a given frequency at normal rate, so to play a specific frequency, code::frequency / baseFrequency:: gives you the required rate.

The first example makes a custom protoEvent that calculates rate, as code::\freq::, based on the base frequency. It uses one sample, so it would be best for patterns that will play in a narrow range. Since there isn't an instrument sample in the SuperCollider distribution, we will record a frequency-modulation sample into a buffer before running the pattern.

code::
// make a sound sample
(
var recorder;
fork {
	b = Buffer.alloc(s, 44100 * 2, 1);
	s.sync;
	recorder = { |freq = 440|
		var initPulse = Impulse.kr(0),
		mod = SinOsc.ar(freq) * Decay2.kr(initPulse, 0.01, 3) * 5,
		car = SinOsc.ar(freq + (mod*freq)) * Decay2.kr(initPulse, 0.01, 2.0);
		RecordBuf.ar(car, b, loop: 0, doneAction: Done.freeSelf);
		car ! 2
	}.play;
	o = OSCFunc({ |msg|
		if(msg[1] == recorder.nodeID, {
			"done recording".postln;
			o.free;
		});
	}, '/n_end', s.addr);
};
SynthDef(\sampler, { |out, bufnum, freq = 1, amp = 1|
	var sig = PlayBuf.ar(1, bufnum, rate: freq, doneAction: Done.freeSelf) * amp;
	Out.ar(out, sig ! 2)
}).add;
)

(
// WAIT for "done recording" message before doing this
var samplerEvent = Event.default.put(\freq, { ~midinote.midicps / ~sampleBaseFreq });

TempoClock.default.tempo = 1;
p = Pbind(
	\degree, Pwhite(0, 12, inf),
	\dur, Pwrand([0.25, Pn(0.125, 2)], #[0.8, 0.2], inf),
	\amp, Pexprand(0.1, 0.5, inf),
	\sampleBaseFreq, 440,
	\instrument, \sampler,
	\bufnum, b
).play(protoEvent: samplerEvent);
)

p.stop;
b.free;
::

subsection::Multi-sampled instruments

To extend the sampler's range using multiple samples and ensure smooth transitions between frequency ranges, the SynthDef should crossfade between adjacent buffers. A hybrid approach is used here, where Pbind calculates the lower buffer number to use and the SynthDef calculates the crossfade strength. (The calculations could be structured differently, either putting more of them into the SynthDef for convenience in the pattern, or loading them into the pattern and keeping the SynthDef as lean as possible.)

MIDI note numbers are used for these calculations because it's a linear frequency scale and linear interpolation is easier than the exponential interpolation that would be required when using Hz. Assuming a sorted array, indexInBetween gives the fractional index using linear interpolation. If you need to use frequency in Hz, use this function in place of indexInBetween.

code::
f = { |val, array|
	var a, b, div;
	var i = array.indexOfGreaterThan(val);
	if(i.isNil) { array.size - 1 } {
		if(i == 0) { i } {
			a = array[i-1]; b = array[i];
			div = b / a;
			if(div == 1) { i } {
					// log() / log() == log(val/a) at base (b/a)
					// which is the inverse of exponential interpolation
				log(val / a) / log(div) + i - 1
			}
		}
	};
};
::

But that function isn't needed for this example:

code::
(
var bufCount;
~midinotes = (39, 46 .. 88);
bufCount = ~midinotes.size;

fork {
	// record the samples at different frequencies
	b = Buffer.allocConsecutive(~midinotes.size, s, 44100 * 2, 1);
	SynthDef(\sampleSource, { |freq = 440, bufnum|
		var initPulse = Impulse.kr(0),
		mod = SinOsc.ar(freq) * Decay2.kr(initPulse, 0.01, 3) * 5,
		car = SinOsc.ar(freq + (mod*freq)) * Decay2.kr(initPulse, 0.01, 2.0);
		RecordBuf.ar(car, bufnum, loop: 0, doneAction: Done.freeSelf);
	}).send(s);
	s.sync;
	// record all 8 buffers concurrently
	b.do({ |buf, i|
		Synth(\sampleSource, [freq: ~midinotes[i].midicps, bufnum: buf]);
	});
};
o = OSCFunc({ |msg|
	bufCount = bufCount - 1;
	if(bufCount == 0) {
		"done recording".postln;
		o.free;
	};
}, '/n_end', s.addr);

SynthDef(\multiSampler, { |out, bufnum, bufBase, baseFreqBuf, freq = 440, amp = 1|
	var buf1 = bufnum.floor,
	buf2 = buf1 + 1,
	xfade = (bufnum - buf1).madd(2, -1),
	basefreqs = Index.kr(baseFreqBuf, [buf1, buf2]),
	playbufs = PlayBuf.ar(1, bufBase + [buf1, buf2], freq / basefreqs, loop: 0,
		doneAction: Done.freeSelf),
	sig = XFade2.ar(playbufs[0], playbufs[1], xfade, amp);
	Out.ar(out, sig ! 2)
}).add;

~baseBuf = Buffer.alloc(s, ~midinotes.size, 1, { |buf| buf.setnMsg(0, ~midinotes.midicps) });
)

(
TempoClock.default.tempo = 1;
p = Pbind(
	\instrument, \multiSampler,
	\bufBase, b.first,
	\baseFreqBuf, ~baseBuf,
	\degree, Pseries(0, Prand(#[-2, -1, 1, 2], inf), inf).fold(-11, 11),
	\dur, Pwrand([0.25, Pn(0.125, 2)], #[0.8, 0.2], inf),
	\amp, Pexprand(0.1, 0.5, inf),
	// some important conversions
	// identify the buffer numbers to read
	\freq, Pfunc { |ev| ev.use(ev[\freq]) },
	\bufnum, Pfunc({ |ev| ~midinotes.indexInBetween(ev[\freq].cpsmidi) })
	.clip(0, ~midinotes.size - 1.001)
).play;
)

p.stop;
b.do(_.free); ~baseBuf.free;
::

Previous:	link::Tutorials/A-Practical-Guide/PG_Cookbook04_Sending_MIDI::

Next:		link::Tutorials/A-Practical-Guide/PG_Cookbook06_Phrase_Network::