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 @ 16:4dad0b810f18

History | View | Annotate | Download (14.5 KB)

1 5:75b744078d66 f
/*
2

3 16:4dad0b810f18 f
 Copyright (C) 2002 James McCartney.
4 5:75b744078d66 f
 Copyright (C) 2016  Queen Mary University of London
5 16:4dad0b810f18 f
 Author: Fiore Martin, based on Supercollider's (http://supercollider.github.io) TGrains code and Ross Bencina's "Implementing Real-Time Granular Synthesis"
6 5:75b744078d66 f

7
 This file is part of Collidoscope.
8

9
 Collidoscope is free software: you can redistribute it and/or modify
10
 it under the terms of the GNU General Public License as published by
11
 the Free Software Foundation, either version 3 of the License, or
12
 (at your option) any later version.
13

14
 This program is distributed in the hope that it will be useful,
15
 but WITHOUT ANY WARRANTY; without even the implied warranty of
16
 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
17
 GNU General Public License for more details.
18

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