annotate CollidoscopeApp/include/PGranular.h @ 9:20bb004a36de

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