annotate CollidoscopeApp/include/PGranular.h @ 2:dd889fff8423

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