annotate CollidoscopeApp/include/PGranular.h @ 18:f1ff1a81be20 tip

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