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 @ 5:75b744078d66

History | View | Annotate | Download (14.4 KB)

1 5:75b744078d66 f
/*
2

3
 Copyright (C) 2016  Queen Mary University of London
4
 Author: Fiore Martin
5

6
 This file is part of Collidoscope.
7

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

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

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