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 @ 0:02467299402e

History | View | Annotate | Download (10.4 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
template <typename T, typename RandOffsetFunc, typename TriggerCallbackFunc>
14
class PGranular
15
{
16
17
public:
18
    static const size_t kMaxGrains = 32;
19
    static const size_t kMinGrainsDuration = 640;
20
21
    static inline T interpolateLin( double xn, double xn_1, double decimal )
22
    {
23
        /* weighted sum interpolation */
24
        return static_cast<T> ((1 - decimal) * xn + decimal * xn_1);
25
    }
26
27
    struct PGrain
28
    {
29
        double phase;    // read pointer to mBuffer of this grain
30
        double rate;     // rate of the grain. e.g. rate = 2 the grain will play twice as fast
31
        bool alive;      // whether this grain is alive. Not alive means it has been processed and can be replanced by another grain
32
        size_t age;      // age of this grain in samples
33
        size_t duration; // duration of this grain in samples. minimum = 4
34
35
        double b1;       // hann envelope from Ross Becina "Implementing real time Granular Synthesis"
36
        double y1;
37
        double y2;
38
    };
39
40
41
42
    PGranular( const T* buffer, size_t bufferLen, size_t sampleRate, RandOffsetFunc & rand, TriggerCallbackFunc & triggerCallback, int ID ) :
43
        mBuffer( buffer ),
44
        mBufferLen( bufferLen ),
45
        mNumAliveGrains( 0 ),
46
        mGrainsRate( 1.0 ),
47
        mTrigger( 0 ),
48
        mTriggerRate( 0 ), // start silent
49
        mGrainsStart( 0 ),
50
        mGrainsDuration( kMinGrainsDuration ),
51
        mGrainsDurationCoeff( 1 ),
52
        mRand( rand ),
53
        mTriggerCallback( triggerCallback ),
54
        mEnvASR( 1.0f, 0.01f, 0.05f, sampleRate ),
55
        mAttenuation( T(0.25118864315096) ),
56
        mID( ID )
57
    {
58
        static_assert(std::is_pod<PGrain>::value, "PGrain must be POD");
59
#ifdef _WINDOW
60
        static_assert(std::is_same<std::result_of<RandOffsetFunc()>::type, size_t>::value, "Rand must return a size_t");
61
#endif
62
        /* init the grains */
63
        for ( size_t grainIdx = 0; grainIdx < kMaxGrains; grainIdx++ ){
64
            mGrains[grainIdx].phase = 0;
65
            mGrains[grainIdx].rate = 1;
66
            mGrains[grainIdx].alive = false;
67
            mGrains[grainIdx].age = 0;
68
            mGrains[grainIdx].duration = 1;
69
        }
70
    }
71
72
    ~PGranular(){}
73
74
    /* sets multiplier of duration of grains in seconds */
75
    void setGrainsDurationCoeff( double coeff )
76
    {
77
        mGrainsDurationCoeff = coeff;
78
79
        mGrainsDuration = std::lround( mTriggerRate * coeff ); // FIXME check if right rounding
80
81
        if ( mGrainsDuration < kMinGrainsDuration )
82
            mGrainsDuration = kMinGrainsDuration;
83
    }
84
85
    /* sets rate of grains. e.g rate = 2 means one octave higer */
86
    void setGrainsRate( double rate )
87
    {
88
        mGrainsRate = rate;
89
    }
90
91
    // sets trigger rate in samples
92
    void setSelectionStart( size_t start )
93
    {
94
        mGrainsStart = start;
95
    }
96
97
    void setSelectionSize( size_t size )
98
    {
99
100
        if ( size < kMinGrainsDuration )
101
            size = kMinGrainsDuration;
102
103
        mTriggerRate = size;
104
105
        mGrainsDuration = std::lround( size * mGrainsDurationCoeff );
106
107
108
    }
109
110
    void setAttenuation( T attenuation )
111
    {
112
        mAttenuation = attenuation;
113
    }
114
115
    void noteOn( double rate )
116
    {
117
        if ( mEnvASR.getState() == EnvASR<T>::State::eIdle ){
118
            // note on sets triggering top the min value
119
            if ( mTriggerRate < kMinGrainsDuration ){
120
                mTriggerRate = kMinGrainsDuration;
121
            }
122
123
            setGrainsRate( rate );
124
            mEnvASR.setState( EnvASR<T>::State::eAttack );
125
        }
126
    }
127
128
    void noteOff()
129
    {
130
        if ( mEnvASR.getState() != EnvASR<T>::State::eIdle ){
131
            mEnvASR.setState( EnvASR<T>::State::eRelease );
132
        }
133
    }
134
135
    bool isIdle()
136
    {
137
        return mEnvASR.getState() == EnvASR<T>::State::eIdle;
138
    }
139
140
    void process( T* audioOut, T* tempBuffer, size_t numSamples )
141
    {
142
143
        // num samples worth of sound ( due to envelope possibly finishing )
144
        size_t envSamples = 0;
145
        bool becameIdle = false;
146
147
        // do the envelope first and store it in the tempBuffer
148
        for ( size_t i = 0; i < numSamples; i++ ){
149
            tempBuffer[i] = mEnvASR.tick();
150
            envSamples++;
151
152
            if ( isIdle() ){
153
                // means that the envelope has stopped
154
                becameIdle = true;
155
                break;
156
            }
157
        }
158
159
        processGrains( audioOut, tempBuffer, envSamples );
160
161
        if ( becameIdle ){
162
            mTriggerCallback( 'e', mID );
163
            reset();
164
        }
165
    }
166
167
private:
168
169
    void processGrains( T* audioOut, T* envelopeValues, size_t numSamples )
170
    {
171
172
        /* process all existing alive grains */
173
        for ( size_t grainIdx = 0; grainIdx < mNumAliveGrains;  ){
174
            synthesizeGrain( mGrains[grainIdx], audioOut, envelopeValues, numSamples );
175
176
            if ( !mGrains[grainIdx].alive ){
177
                // this grain is dead so copyu the last of the active grains here
178
                // so as to keep all active grains at the beginning of the array
179
                // don't increment grainIdx so the last active grain is processed next cycle
180
                // if this grain is the last active grain then mNumAliveGrains is decremented
181
                // and grainIdx = mNumAliveGrains so the loop stops
182
                copyGrain( mNumAliveGrains - 1, grainIdx );
183
                mNumAliveGrains--;
184
            }
185
            else{
186
                // go to next grain
187
                grainIdx++;
188
            }
189
        }
190
191
        if ( mTriggerRate == 0 ){
192
            return;
193
        }
194
195
        size_t randOffset =  mRand();
196
        bool newGrainWasTriggered = false;
197
198
        // trigger new grain and synthesize them as well
199
        while ( mTrigger < numSamples ){
200
201
            // if there is room to accommodate new grains
202
            if ( mNumAliveGrains < kMaxGrains ){
203
                // get next grain will be placed at the end of the alive ones
204
                size_t grainIdx = mNumAliveGrains;
205
                mNumAliveGrains++;
206
207
                // initialize and synthesise the grain
208
                PGrain &grain = mGrains[grainIdx];
209
210
                double phase = mGrainsStart + double( randOffset );
211
                if ( phase >= mBufferLen )
212
                    phase -= mBufferLen;
213
214
                grain.phase = phase;
215
                grain.rate = mGrainsRate;
216
                grain.alive = true;
217
                grain.age = 0;
218
                grain.duration = mGrainsDuration;
219
220
                const double w = 3.14159265358979323846 / mGrainsDuration;
221
                grain.b1 = 2.0 * std::cos( w );
222
                grain.y1 = std::sin( w );
223
                grain.y2 = 0.0;
224
225
                synthesizeGrain( grain, audioOut + mTrigger, envelopeValues + mTrigger, numSamples - mTrigger );
226
227
                if ( grain.alive == false ) {
228
                    mNumAliveGrains--;
229
                }
230
231
                newGrainWasTriggered = true;
232
            }
233
234
            // update trigger even if no new grain was started
235
            mTrigger += mTriggerRate;
236
        }
237
238
        // prepare trigger for next cycle: init mTrigger with the reminder of the samples from this cycle
239
        mTrigger -= numSamples;
240
241
        if ( newGrainWasTriggered ){
242
            mTriggerCallback( 't', mID );
243
        }
244
    }
245
246
    // audioOut = pointer to audio block to fill
247
    // numSamples = numpber of samples to process for this block
248
    void synthesizeGrain( PGrain &grain, T* audioOut, T* envelopeValues, size_t numSamples )
249
    {
250
251
        // copy all grain data into local variable for faster porcessing
252
        const auto rate = grain.rate;
253
        auto phase = grain.phase;
254
        auto age = grain.age;
255
        auto duration = grain.duration;
256
257
258
        auto b1 = grain.b1;
259
        auto y1 = grain.y1;
260
        auto y2 = grain.y2;
261
262
        // only process minimum between samples of this block and time left to leave for this grain
263
        auto numSamplesToOut = std::min( numSamples, duration - age );
264
265
        for ( size_t sampleIdx = 0; sampleIdx < numSamplesToOut; sampleIdx++ ){
266
267
            const size_t readIndex = (size_t)phase;
268
            const size_t nextReadIndex = (readIndex == mBufferLen - 1) ? 0 : readIndex + 1; // wrap on the read buffer if needed
269
270
            const double decimal = phase - readIndex;
271
272
            T out = interpolateLin( mBuffer[readIndex], mBuffer[nextReadIndex], decimal );
273
274
            // apply raised cosine bell envelope
275
            auto y0 = b1 * y1 - y2;
276
            y2 = y1;
277
            y1 = y0;
278
            out *= T(y0);
279
280
            audioOut[sampleIdx] += out * envelopeValues[sampleIdx] * mAttenuation;
281
282
            // increment age one sample
283
            age++;
284
            // increment the phase according to the rate of this grain
285
            phase += rate;
286
287
            if ( phase >= mBufferLen ){   // wrap the phase if needed
288
                phase -= mBufferLen;
289
            }
290
        }
291
292
        if ( age == duration ){
293
            // if it porocessed all the samples left to leave ( numSamplesToOut = duration-age)
294
            // then the grain is had finished
295
            grain.alive = false;
296
        }
297
        else{
298
            grain.phase = phase;
299
            grain.age = age;
300
            grain.y1 = y1;
301
            grain.y2 = y2;
302
        }
303
    }
304
305
    void copyGrain( size_t from, size_t to)
306
    {
307
        mGrains[to] = mGrains[from];
308
    }
309
310
    void reset()
311
    {
312
        mTrigger = 0;
313
        for ( size_t i = 0; i < mNumAliveGrains; i++ ){
314
            mGrains[i].alive = false;
315
        }
316
317
        mNumAliveGrains = 0;
318
    }
319
320
    int mID;
321
322
    // pointer to (mono) buffer, where the underlying sample is recorder
323
    const T* mBuffer;
324
    // length of mBuffer in samples
325
    const size_t mBufferLen;
326
327
    // offset in the buffer where the grains start. a.k.a. seleciton start
328
    size_t mGrainsStart;
329
330
    // attenuates signal prevents clipping of grains
331
    T mAttenuation;
332
333
    // grain duration in samples
334
    double mGrainsDurationCoeff;
335
    // duration of grains is selcection size * duration coeff
336
    size_t mGrainsDuration;
337
    // rate of grain, affects pitch
338
    double mGrainsRate;
339
340
    size_t mTrigger;       // next onset
341
    size_t mTriggerRate;   // inter onset
342
343
    // the array of grains
344
    std::array<PGrain, kMaxGrains> mGrains;
345
    // number of alive grains
346
    size_t mNumAliveGrains;
347
348
    RandOffsetFunc &mRand;
349
    TriggerCallbackFunc &mTriggerCallback;
350
351
    EnvASR<T> mEnvASR;
352
};
353
354
355
356
357
} // namespace collidoscope
358