f@0
|
1 #pragma once
|
f@0
|
2
|
f@0
|
3 #include <array>
|
f@0
|
4 #include <type_traits>
|
f@0
|
5
|
f@0
|
6 #include "EnvASR.h"
|
f@0
|
7
|
f@0
|
8
|
f@0
|
9 namespace collidoscope {
|
f@0
|
10
|
f@0
|
11 using std::size_t;
|
f@0
|
12
|
f@3
|
13 /**
|
f@3
|
14 * The very core of the Collidoscope audio engine: the granular synthesizer.
|
f@3
|
15 * Based on SuperCollider's TGrains and Ross Becina's "Implementing Real-Time Granular Synthesis"
|
f@3
|
16 *
|
f@3
|
17 * It implements Collidoscope's selection-based approach to granular synthesis.
|
f@3
|
18 * A grain is basically a selection of a recorded sample of audio.
|
f@3
|
19 * Grains are played in a loop: they are retriggered each time they reach the end of the selection.
|
f@3
|
20 * However, if the duration coefficient is greater than one, a new grain is re-triggered before the previous one is done.
|
f@3
|
21 * The grains start to overlap with each other and create the typical eerie sound of grnular synthesis.
|
f@3
|
22 * Also every time a new grain is triggered, it is offset of a few samples from the initial position to make the timbre more interesting.
|
f@3
|
23 *
|
f@3
|
24 *
|
f@3
|
25 * PGranular uses a linear ASR envelope with 10 milliseconds attack and 50 milliseconds release.
|
f@3
|
26 *
|
f@3
|
27 * Note that PGranular is header based and only depends on std library and on "EnvASR.h" (also header based).
|
f@3
|
28 * This means you can embedd it in two your project just by copying these two files over.
|
f@3
|
29 *
|
f@3
|
30 * Template arguments:
|
f@3
|
31 * T: type of the audio samples (normally float or double)
|
f@3
|
32 * RandOffsetFunc: type of the callable passed as argument to the contructor
|
f@3
|
33 * TriggerCallbackFunc: type of the callable passed as argument to the contructor
|
f@3
|
34 *
|
f@3
|
35 */
|
f@0
|
36 template <typename T, typename RandOffsetFunc, typename TriggerCallbackFunc>
|
f@0
|
37 class PGranular
|
f@0
|
38 {
|
f@0
|
39
|
f@0
|
40 public:
|
f@0
|
41 static const size_t kMaxGrains = 32;
|
f@0
|
42 static const size_t kMinGrainsDuration = 640;
|
f@0
|
43
|
f@0
|
44 static inline T interpolateLin( double xn, double xn_1, double decimal )
|
f@0
|
45 {
|
f@0
|
46 /* weighted sum interpolation */
|
f@0
|
47 return static_cast<T> ((1 - decimal) * xn + decimal * xn_1);
|
f@0
|
48 }
|
f@0
|
49
|
f@3
|
50 /**
|
f@3
|
51 * A single grain of the granular synthesis
|
f@3
|
52 */
|
f@0
|
53 struct PGrain
|
f@0
|
54 {
|
f@0
|
55 double phase; // read pointer to mBuffer of this grain
|
f@0
|
56 double rate; // rate of the grain. e.g. rate = 2 the grain will play twice as fast
|
f@0
|
57 bool alive; // whether this grain is alive. Not alive means it has been processed and can be replanced by another grain
|
f@0
|
58 size_t age; // age of this grain in samples
|
f@0
|
59 size_t duration; // duration of this grain in samples. minimum = 4
|
f@0
|
60
|
f@3
|
61 double b1; // hann envelope from Ross Becina's "Implementing real time Granular Synthesis"
|
f@0
|
62 double y1;
|
f@0
|
63 double y2;
|
f@0
|
64 };
|
f@0
|
65
|
f@0
|
66
|
f@0
|
67
|
f@3
|
68 /**
|
f@3
|
69 * Constructor.
|
f@3
|
70 *
|
f@3
|
71 * \param buffer a pointer to an array of T that contains the original sample that will be granulized
|
f@3
|
72 * \param bufferLen length of buffer in samples
|
f@3
|
73 * \rand function returning of type size_t ()(void) that is called back each time a new grain is generated. The returned value is used
|
f@3
|
74 * to offset the starting sample of the grain. This adds more colour to the sound especially with small selections.
|
f@3
|
75 * \triggerCallback function of type void ()(char, int) that is called back each time a new grain is triggered.
|
f@3
|
76 * The function is passed the character 't' as first parameter when a new grain is triggered and the characted 't' when the synths becomes idle.
|
f@3
|
77 * \ID id of this PGrain is passed to the triggerCallback function as second parameter to identify this PGranular as the caller.
|
f@3
|
78 */
|
f@0
|
79 PGranular( const T* buffer, size_t bufferLen, size_t sampleRate, RandOffsetFunc & rand, TriggerCallbackFunc & triggerCallback, int ID ) :
|
f@0
|
80 mBuffer( buffer ),
|
f@0
|
81 mBufferLen( bufferLen ),
|
f@0
|
82 mNumAliveGrains( 0 ),
|
f@0
|
83 mGrainsRate( 1.0 ),
|
f@0
|
84 mTrigger( 0 ),
|
f@0
|
85 mTriggerRate( 0 ), // start silent
|
f@0
|
86 mGrainsStart( 0 ),
|
f@0
|
87 mGrainsDuration( kMinGrainsDuration ),
|
f@0
|
88 mGrainsDurationCoeff( 1 ),
|
f@0
|
89 mRand( rand ),
|
f@0
|
90 mTriggerCallback( triggerCallback ),
|
f@0
|
91 mEnvASR( 1.0f, 0.01f, 0.05f, sampleRate ),
|
f@0
|
92 mAttenuation( T(0.25118864315096) ),
|
f@0
|
93 mID( ID )
|
f@0
|
94 {
|
f@0
|
95 static_assert(std::is_pod<PGrain>::value, "PGrain must be POD");
|
f@0
|
96 #ifdef _WINDOW
|
f@0
|
97 static_assert(std::is_same<std::result_of<RandOffsetFunc()>::type, size_t>::value, "Rand must return a size_t");
|
f@0
|
98 #endif
|
f@0
|
99 /* init the grains */
|
f@0
|
100 for ( size_t grainIdx = 0; grainIdx < kMaxGrains; grainIdx++ ){
|
f@0
|
101 mGrains[grainIdx].phase = 0;
|
f@0
|
102 mGrains[grainIdx].rate = 1;
|
f@0
|
103 mGrains[grainIdx].alive = false;
|
f@0
|
104 mGrains[grainIdx].age = 0;
|
f@0
|
105 mGrains[grainIdx].duration = 1;
|
f@0
|
106 }
|
f@0
|
107 }
|
f@0
|
108
|
f@0
|
109 ~PGranular(){}
|
f@0
|
110
|
f@3
|
111 /** Sets multiplier of duration of grains in seconds */
|
f@0
|
112 void setGrainsDurationCoeff( double coeff )
|
f@0
|
113 {
|
f@0
|
114 mGrainsDurationCoeff = coeff;
|
f@0
|
115
|
f@3
|
116 mGrainsDuration = std::lround( mTriggerRate * coeff );
|
f@0
|
117
|
f@0
|
118 if ( mGrainsDuration < kMinGrainsDuration )
|
f@0
|
119 mGrainsDuration = kMinGrainsDuration;
|
f@0
|
120 }
|
f@0
|
121
|
f@3
|
122 /** Sets rate of grains. e.g rate = 2 means one octave higer */
|
f@0
|
123 void setGrainsRate( double rate )
|
f@0
|
124 {
|
f@0
|
125 mGrainsRate = rate;
|
f@0
|
126 }
|
f@0
|
127
|
f@3
|
128 /** sets the selection start in samples */
|
f@0
|
129 void setSelectionStart( size_t start )
|
f@0
|
130 {
|
f@0
|
131 mGrainsStart = start;
|
f@0
|
132 }
|
f@0
|
133
|
f@3
|
134 /** Sets the selection size ( and therefore the trigger rate) in samples */
|
f@0
|
135 void setSelectionSize( size_t size )
|
f@0
|
136 {
|
f@0
|
137
|
f@0
|
138 if ( size < kMinGrainsDuration )
|
f@0
|
139 size = kMinGrainsDuration;
|
f@0
|
140
|
f@0
|
141 mTriggerRate = size;
|
f@0
|
142
|
f@0
|
143 mGrainsDuration = std::lround( size * mGrainsDurationCoeff );
|
f@0
|
144
|
f@0
|
145
|
f@0
|
146 }
|
f@0
|
147
|
f@3
|
148 /** Sets the attenuation of the grains with respect to the level of the recorded sample
|
f@3
|
149 * attenuation is in amp value and defaule value is 0.25118864315096 (-12dB) */
|
f@0
|
150 void setAttenuation( T attenuation )
|
f@0
|
151 {
|
f@0
|
152 mAttenuation = attenuation;
|
f@0
|
153 }
|
f@0
|
154
|
f@3
|
155 /** Starts the synthesis engine */
|
f@0
|
156 void noteOn( double rate )
|
f@0
|
157 {
|
f@0
|
158 if ( mEnvASR.getState() == EnvASR<T>::State::eIdle ){
|
f@0
|
159 // note on sets triggering top the min value
|
f@0
|
160 if ( mTriggerRate < kMinGrainsDuration ){
|
f@0
|
161 mTriggerRate = kMinGrainsDuration;
|
f@0
|
162 }
|
f@0
|
163
|
f@0
|
164 setGrainsRate( rate );
|
f@0
|
165 mEnvASR.setState( EnvASR<T>::State::eAttack );
|
f@0
|
166 }
|
f@0
|
167 }
|
f@0
|
168
|
f@3
|
169 /** Stops the synthesis engine */
|
f@0
|
170 void noteOff()
|
f@0
|
171 {
|
f@0
|
172 if ( mEnvASR.getState() != EnvASR<T>::State::eIdle ){
|
f@0
|
173 mEnvASR.setState( EnvASR<T>::State::eRelease );
|
f@0
|
174 }
|
f@0
|
175 }
|
f@0
|
176
|
f@3
|
177 /** Whether the synthesis engine is active or not. After noteOff is called the synth stays active until the envelope decays to 0 */
|
f@0
|
178 bool isIdle()
|
f@0
|
179 {
|
f@0
|
180 return mEnvASR.getState() == EnvASR<T>::State::eIdle;
|
f@0
|
181 }
|
f@0
|
182
|
f@3
|
183 /**
|
f@3
|
184 * Runs the granular engine and stores the output in \a audioOut
|
f@3
|
185 *
|
f@3
|
186 * \param pointer to an array of T. This will be filled with the output of PGranular. It needs to be at least \a numSamples lond
|
f@3
|
187 * \param tempBuffer a temporary buffer used to store the envelope value. It needs to be at leas \a numSamples long
|
f@3
|
188 * \param numSamples number of samples to be processed
|
f@3
|
189 */
|
f@0
|
190 void process( T* audioOut, T* tempBuffer, size_t numSamples )
|
f@0
|
191 {
|
f@0
|
192
|
f@0
|
193 // num samples worth of sound ( due to envelope possibly finishing )
|
f@0
|
194 size_t envSamples = 0;
|
f@0
|
195 bool becameIdle = false;
|
f@0
|
196
|
f@3
|
197 // process the envelope first and store it in the tempBuffer
|
f@0
|
198 for ( size_t i = 0; i < numSamples; i++ ){
|
f@0
|
199 tempBuffer[i] = mEnvASR.tick();
|
f@0
|
200 envSamples++;
|
f@0
|
201
|
f@0
|
202 if ( isIdle() ){
|
f@0
|
203 // means that the envelope has stopped
|
f@0
|
204 becameIdle = true;
|
f@0
|
205 break;
|
f@0
|
206 }
|
f@0
|
207 }
|
f@0
|
208
|
f@3
|
209 // does the actual grains processing
|
f@0
|
210 processGrains( audioOut, tempBuffer, envSamples );
|
f@0
|
211
|
f@3
|
212 // becomes idle if the envelope goes to idle state
|
f@0
|
213 if ( becameIdle ){
|
f@0
|
214 mTriggerCallback( 'e', mID );
|
f@0
|
215 reset();
|
f@0
|
216 }
|
f@0
|
217 }
|
f@0
|
218
|
f@0
|
219 private:
|
f@0
|
220
|
f@0
|
221 void processGrains( T* audioOut, T* envelopeValues, size_t numSamples )
|
f@0
|
222 {
|
f@0
|
223
|
f@0
|
224 /* process all existing alive grains */
|
f@0
|
225 for ( size_t grainIdx = 0; grainIdx < mNumAliveGrains; ){
|
f@0
|
226 synthesizeGrain( mGrains[grainIdx], audioOut, envelopeValues, numSamples );
|
f@0
|
227
|
f@0
|
228 if ( !mGrains[grainIdx].alive ){
|
f@3
|
229 // this grain is dead so copy the last of the active grains here
|
f@0
|
230 // so as to keep all active grains at the beginning of the array
|
f@0
|
231 // don't increment grainIdx so the last active grain is processed next cycle
|
f@0
|
232 // if this grain is the last active grain then mNumAliveGrains is decremented
|
f@0
|
233 // and grainIdx = mNumAliveGrains so the loop stops
|
f@0
|
234 copyGrain( mNumAliveGrains - 1, grainIdx );
|
f@0
|
235 mNumAliveGrains--;
|
f@0
|
236 }
|
f@0
|
237 else{
|
f@0
|
238 // go to next grain
|
f@0
|
239 grainIdx++;
|
f@0
|
240 }
|
f@0
|
241 }
|
f@0
|
242
|
f@0
|
243 if ( mTriggerRate == 0 ){
|
f@0
|
244 return;
|
f@0
|
245 }
|
f@0
|
246
|
f@0
|
247 size_t randOffset = mRand();
|
f@0
|
248 bool newGrainWasTriggered = false;
|
f@0
|
249
|
f@0
|
250 // trigger new grain and synthesize them as well
|
f@0
|
251 while ( mTrigger < numSamples ){
|
f@0
|
252
|
f@0
|
253 // if there is room to accommodate new grains
|
f@0
|
254 if ( mNumAliveGrains < kMaxGrains ){
|
f@0
|
255 // get next grain will be placed at the end of the alive ones
|
f@0
|
256 size_t grainIdx = mNumAliveGrains;
|
f@0
|
257 mNumAliveGrains++;
|
f@0
|
258
|
f@0
|
259 // initialize and synthesise the grain
|
f@0
|
260 PGrain &grain = mGrains[grainIdx];
|
f@0
|
261
|
f@0
|
262 double phase = mGrainsStart + double( randOffset );
|
f@0
|
263 if ( phase >= mBufferLen )
|
f@0
|
264 phase -= mBufferLen;
|
f@0
|
265
|
f@0
|
266 grain.phase = phase;
|
f@0
|
267 grain.rate = mGrainsRate;
|
f@0
|
268 grain.alive = true;
|
f@0
|
269 grain.age = 0;
|
f@0
|
270 grain.duration = mGrainsDuration;
|
f@0
|
271
|
f@0
|
272 const double w = 3.14159265358979323846 / mGrainsDuration;
|
f@0
|
273 grain.b1 = 2.0 * std::cos( w );
|
f@0
|
274 grain.y1 = std::sin( w );
|
f@0
|
275 grain.y2 = 0.0;
|
f@0
|
276
|
f@0
|
277 synthesizeGrain( grain, audioOut + mTrigger, envelopeValues + mTrigger, numSamples - mTrigger );
|
f@0
|
278
|
f@0
|
279 if ( grain.alive == false ) {
|
f@0
|
280 mNumAliveGrains--;
|
f@0
|
281 }
|
f@0
|
282
|
f@0
|
283 newGrainWasTriggered = true;
|
f@0
|
284 }
|
f@0
|
285
|
f@0
|
286 // update trigger even if no new grain was started
|
f@0
|
287 mTrigger += mTriggerRate;
|
f@0
|
288 }
|
f@0
|
289
|
f@0
|
290 // prepare trigger for next cycle: init mTrigger with the reminder of the samples from this cycle
|
f@0
|
291 mTrigger -= numSamples;
|
f@0
|
292
|
f@0
|
293 if ( newGrainWasTriggered ){
|
f@0
|
294 mTriggerCallback( 't', mID );
|
f@0
|
295 }
|
f@0
|
296 }
|
f@0
|
297
|
f@3
|
298 // synthesize a single grain
|
f@0
|
299 // audioOut = pointer to audio block to fill
|
f@0
|
300 // numSamples = numpber of samples to process for this block
|
f@0
|
301 void synthesizeGrain( PGrain &grain, T* audioOut, T* envelopeValues, size_t numSamples )
|
f@0
|
302 {
|
f@0
|
303
|
f@0
|
304 // copy all grain data into local variable for faster porcessing
|
f@0
|
305 const auto rate = grain.rate;
|
f@0
|
306 auto phase = grain.phase;
|
f@0
|
307 auto age = grain.age;
|
f@0
|
308 auto duration = grain.duration;
|
f@0
|
309
|
f@0
|
310
|
f@0
|
311 auto b1 = grain.b1;
|
f@0
|
312 auto y1 = grain.y1;
|
f@0
|
313 auto y2 = grain.y2;
|
f@0
|
314
|
f@0
|
315 // only process minimum between samples of this block and time left to leave for this grain
|
f@0
|
316 auto numSamplesToOut = std::min( numSamples, duration - age );
|
f@0
|
317
|
f@0
|
318 for ( size_t sampleIdx = 0; sampleIdx < numSamplesToOut; sampleIdx++ ){
|
f@0
|
319
|
f@0
|
320 const size_t readIndex = (size_t)phase;
|
f@0
|
321 const size_t nextReadIndex = (readIndex == mBufferLen - 1) ? 0 : readIndex + 1; // wrap on the read buffer if needed
|
f@0
|
322
|
f@0
|
323 const double decimal = phase - readIndex;
|
f@0
|
324
|
f@0
|
325 T out = interpolateLin( mBuffer[readIndex], mBuffer[nextReadIndex], decimal );
|
f@0
|
326
|
f@0
|
327 // apply raised cosine bell envelope
|
f@0
|
328 auto y0 = b1 * y1 - y2;
|
f@0
|
329 y2 = y1;
|
f@0
|
330 y1 = y0;
|
f@0
|
331 out *= T(y0);
|
f@0
|
332
|
f@0
|
333 audioOut[sampleIdx] += out * envelopeValues[sampleIdx] * mAttenuation;
|
f@0
|
334
|
f@0
|
335 // increment age one sample
|
f@0
|
336 age++;
|
f@0
|
337 // increment the phase according to the rate of this grain
|
f@0
|
338 phase += rate;
|
f@0
|
339
|
f@0
|
340 if ( phase >= mBufferLen ){ // wrap the phase if needed
|
f@0
|
341 phase -= mBufferLen;
|
f@0
|
342 }
|
f@0
|
343 }
|
f@0
|
344
|
f@0
|
345 if ( age == duration ){
|
f@0
|
346 // if it porocessed all the samples left to leave ( numSamplesToOut = duration-age)
|
f@0
|
347 // then the grain is had finished
|
f@0
|
348 grain.alive = false;
|
f@0
|
349 }
|
f@0
|
350 else{
|
f@0
|
351 grain.phase = phase;
|
f@0
|
352 grain.age = age;
|
f@0
|
353 grain.y1 = y1;
|
f@0
|
354 grain.y2 = y2;
|
f@0
|
355 }
|
f@0
|
356 }
|
f@0
|
357
|
f@0
|
358 void copyGrain( size_t from, size_t to)
|
f@0
|
359 {
|
f@0
|
360 mGrains[to] = mGrains[from];
|
f@0
|
361 }
|
f@0
|
362
|
f@0
|
363 void reset()
|
f@0
|
364 {
|
f@0
|
365 mTrigger = 0;
|
f@0
|
366 for ( size_t i = 0; i < mNumAliveGrains; i++ ){
|
f@0
|
367 mGrains[i].alive = false;
|
f@0
|
368 }
|
f@0
|
369
|
f@0
|
370 mNumAliveGrains = 0;
|
f@0
|
371 }
|
f@0
|
372
|
f@0
|
373 int mID;
|
f@0
|
374
|
f@0
|
375 // pointer to (mono) buffer, where the underlying sample is recorder
|
f@0
|
376 const T* mBuffer;
|
f@0
|
377 // length of mBuffer in samples
|
f@0
|
378 const size_t mBufferLen;
|
f@0
|
379
|
f@0
|
380 // offset in the buffer where the grains start. a.k.a. seleciton start
|
f@0
|
381 size_t mGrainsStart;
|
f@0
|
382
|
f@0
|
383 // attenuates signal prevents clipping of grains
|
f@0
|
384 T mAttenuation;
|
f@0
|
385
|
f@0
|
386 // grain duration in samples
|
f@0
|
387 double mGrainsDurationCoeff;
|
f@0
|
388 // duration of grains is selcection size * duration coeff
|
f@0
|
389 size_t mGrainsDuration;
|
f@0
|
390 // rate of grain, affects pitch
|
f@0
|
391 double mGrainsRate;
|
f@0
|
392
|
f@0
|
393 size_t mTrigger; // next onset
|
f@0
|
394 size_t mTriggerRate; // inter onset
|
f@0
|
395
|
f@0
|
396 // the array of grains
|
f@0
|
397 std::array<PGrain, kMaxGrains> mGrains;
|
f@0
|
398 // number of alive grains
|
f@0
|
399 size_t mNumAliveGrains;
|
f@0
|
400
|
f@0
|
401 RandOffsetFunc &mRand;
|
f@0
|
402 TriggerCallbackFunc &mTriggerCallback;
|
f@0
|
403
|
f@0
|
404 EnvASR<T> mEnvASR;
|
f@0
|
405 };
|
f@0
|
406
|
f@0
|
407
|
f@0
|
408
|
f@0
|
409
|
f@0
|
410 } // namespace collidoscope
|
f@0
|
411
|
f@0
|
412
|