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

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