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