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
|