To check out this repository please hg clone the following URL, or open the URL using EasyMercurial or your preferred Mercurial client.

Statistics Download as Zip
| Branch: | Tag: | Revision:

root / CollidoscopeApp / include / PGranular.h @ 3:7fb593d53361

History | View | Annotate | Download (13.6 KB)

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