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 @ 2:dd889fff8423

History | View | Annotate | Download (10.4 KB)

1
#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

    
359