OptimizedGranulator.java (9939B)
1 package be.tarsos.dsp.granulator; 2 3 4 import java.util.ArrayList; 5 import java.util.Arrays; 6 7 import be.tarsos.dsp.AudioEvent; 8 import be.tarsos.dsp.AudioProcessor; 9 10 /** 11 * Granulator plays back samples using granular synthesis. 12 * Methods can be used to control playback rate, pitch, grain size, 13 * grain interval and grain randomness and position (this last case assumes that the playback rate is zero). 14 * 15 * 16 * 17 * @author ollie 18 * @author Joren 19 */ 20 public class OptimizedGranulator implements AudioProcessor { 21 22 public static final float ADAPTIVE_INTERP_LOW_THRESH = 0.5f; 23 public static final float ADAPTIVE_INTERP_HIGH_THRESH = 2.5f; 24 25 /** The position in milliseconds. */ 26 protected double position; 27 28 /** 29 * The millisecond position increment per sample. Calculated from the ratio 30 * of the sample rate 31 */ 32 private double audioSampleLength; 33 34 35 private float grainInterval; 36 private float grainSize; 37 private float grainRandomness; 38 39 /** The time in milliseconds since the last grain was activated. */ 40 private float timeSinceLastGrain; 41 42 43 /** The pitch, bound to the pitch envelope. */ 44 private float pitchFactor; 45 46 /** The pitch, bound to the pitch envelope. */ 47 private float timeStretchFactor; 48 49 /** The list of current grains. */ 50 private Grain[] grains; 51 52 /** The interpolation type. */ 53 //protected InterpolationType interpolationType; 54 55 /** The window used by grains. */ 56 private final float[] window; 57 58 59 private final float[] audioBuffer; 60 private int audioBufferWatermark; 61 62 private final float[] outputBuffer; 63 64 65 /** 66 * Instantiates a new GranularSamplePlayer. 67 * 68 * @param sampleRate the sample rate. 69 * @param bufferSize the size of an output buffer. 70 */ 71 public OptimizedGranulator(float sampleRate,int bufferSize) { 72 grains = new Grain[50]; 73 for(int i = 0 ; i < grains.length ; i++){ 74 grains[i] = new Grain(); 75 } 76 77 audioBuffer = new float[4800*2];//max 0.2s 78 audioBufferWatermark = 0; 79 80 pitchFactor = 1.0f; 81 82 grainInterval = 40.0f; 83 grainSize = 100.0f; 84 grainRandomness = 0.1f; 85 86 window = new be.tarsos.dsp.util.fft.CosineWindow().generateCurve(512); 87 outputBuffer = new float[bufferSize]; 88 89 audioSampleLength = 1000.0f/sampleRate; 90 } 91 92 93 public void start() { 94 timeSinceLastGrain = 0; 95 } 96 97 98 /** Flag to indicate special case for the first grain. */ 99 private boolean firstGrain = true; 100 101 /** Special case method for playing first grain. */ 102 private void firstGrain() { 103 if(firstGrain) { 104 Grain g = grains[0]; 105 g.position = position; 106 g.age = grainSize / 4f; 107 g.grainSize = grainSize; 108 109 firstGrain = false; 110 timeSinceLastGrain = grainInterval / 2f; 111 } 112 } 113 114 @Override 115 public boolean process(AudioEvent audioEvent) { 116 117 int bufferSize = audioEvent.getBufferSize(); 118 for (int i = 0; i < bufferSize; i++) { 119 audioBuffer[audioBufferWatermark] = audioEvent.getFloatBuffer()[i]; 120 audioBufferWatermark++; 121 if(audioBufferWatermark==audioBuffer.length){ 122 audioBufferWatermark=0; 123 124 } 125 } 126 127 System.out.println("Buffer water mark:" + audioBufferWatermark); 128 129 // grains.clear(); 130 // position = audioEvent.getTimeStamp()*1000 - 5000; 131 132 // reset output 133 Arrays.fill(outputBuffer, 0); 134 135 firstGrain(); 136 137 int activeGrains = 0; 138 for(int j = 0 ; j < grains.length ; j++){ 139 if(grains[j].active){ 140 activeGrains++; 141 } 142 } 143 System.out.println("Active grains = " + activeGrains); 144 145 // now loop through the buffer 146 for (int i = 0; i < bufferSize; i++) { 147 // determine if we need a new grain 148 if (timeSinceLastGrain > grainInterval) { 149 Grain firstInactiveGrain = null; 150 for(int j = 0 ; j < grains.length ; j++){ 151 if(!grains[j].active){ 152 firstInactiveGrain = grains[j]; 153 firstInactiveGrain.reset(grainSize, grainRandomness, position,timeStretchFactor,pitchFactor); 154 timeSinceLastGrain = 0f; 155 break; 156 } 157 } 158 //System.out.println(grains.size()); 159 } 160 161 // gather the output from each grain 162 for (int gi = 0; gi < grains.length; gi++) { 163 Grain g = grains[gi]; 164 if(g.active){ 165 // calculate value of grain window 166 float windowScale = getValueFraction((float) (g.age / g.grainSize)); 167 // get position in sample for this grain 168 // get the frame for this grain 169 170 double sampleValue; 171 //if (pitchFactor > ADAPTIVE_INTERP_HIGH_THRESH) { 172 sampleValue = getFrameNoInterp(g.position); 173 //} else if (pitchFactor > ADAPTIVE_INTERP_LOW_THRESH) { 174 // sampleValue = getFrameLinear(g.position); 175 //} else { 176 // sampleValue = getFrameCubic(g.position); 177 //} 178 sampleValue = sampleValue * windowScale; 179 outputBuffer[i] += (float) sampleValue; 180 } 181 } 182 // increment time 183 position += audioSampleLength * timeStretchFactor; 184 185 for (int gi = 0; gi < grains.length; gi++) { 186 Grain g = grains[gi]; 187 if(g.active){ 188 calculateNextGrainPosition(g); 189 190 if (g.age > g.grainSize) { 191 g.active = false; 192 } 193 } 194 } 195 196 timeSinceLastGrain += audioSampleLength; 197 } 198 199 for (int i = 0; i < bufferSize; i++) { 200 outputBuffer[i] = outputBuffer[i]/(float) 5.0f; 201 } 202 203 audioEvent.setFloatBuffer(outputBuffer); 204 205 return true; 206 } 207 208 209 /** 210 * Retrieves a frame of audio using linear interpolation. If the frame is 211 * not in the sample range then zeros are returned. 212 * 213 * @param posInMS 214 * The frame to read -- can be fractional (e.g., 4.4). 215 * @param result 216 * The framedata to fill. 217 */ 218 public double getFrameLinear(double posInMS) { 219 double result = 0.0; 220 double sampleNumber = msToSamples(posInMS); 221 int sampleNumberFloor = (int) Math.floor(sampleNumber); 222 if (sampleNumberFloor > 0 && sampleNumberFloor < audioBufferWatermark) { 223 double sampleNumberFraction = sampleNumber - sampleNumberFloor; 224 if (sampleNumberFloor == audioBufferWatermark - 1) { 225 result = audioBuffer[sampleNumberFloor]; 226 } else { 227 // linear interpolation 228 double current = audioBuffer[sampleNumberFloor]; 229 double next = audioBuffer[sampleNumberFloor]; 230 result = (float) ((1 - sampleNumberFraction) * current + sampleNumberFraction * next); 231 } 232 } 233 return result; 234 } 235 236 /** 237 * Retrieves a frame of audio using no interpolation. If the frame is not in 238 * the sample range then zeros are returned. 239 * 240 * @param posInMS 241 * The frame to read -- will take the last frame before this one. 242 * 243 */ 244 public float getFrameNoInterp(double posInMS) { 245 double frame = msToSamples(posInMS); 246 247 int frame_floor = (int) Math.floor(frame); 248 249 //int diff = audioBufferWatermark - frame_floor; 250 //if( diff < 4800 || diff > ) 251 252 253 return audioBuffer[frame_floor]; 254 } 255 256 /** 257 * Retrieves a frame of audio using cubic interpolation. If the frame is not 258 * in the sample range then zeros are returned. 259 * 260 * @param posInMS 261 * The frame to read -- can be fractional (e.g., 4.4). 262 */ 263 public float getFrameCubic(double posInMS) { 264 float frame = (float) msToSamples(posInMS); 265 float result = 0.0f; 266 float a0, a1, a2, a3, mu2; 267 float ym1, y0, y1, y2; 268 269 int realCurrentSample = (int) Math.floor(frame); 270 float fractionOffset = (float) (frame - realCurrentSample); 271 272 if (realCurrentSample >= 0 && realCurrentSample < (audioBufferWatermark - 1)) { 273 realCurrentSample--; 274 if (realCurrentSample < 0) { 275 ym1 = audioBuffer[0]; 276 realCurrentSample = 0; 277 } else { 278 ym1 = audioBuffer[realCurrentSample++]; 279 } 280 y0 = audioBuffer[realCurrentSample++]; 281 if (realCurrentSample >= audioBufferWatermark) { 282 y1 = audioBuffer[audioBufferWatermark-1]; // ?? 283 } else { 284 y1 = audioBuffer[realCurrentSample++]; 285 } 286 if (realCurrentSample >= audioBufferWatermark) { 287 y2 = audioBuffer[audioBufferWatermark-1]; 288 } else { 289 y2 = audioBuffer[realCurrentSample++]; 290 } 291 mu2 = fractionOffset * fractionOffset; 292 a0 = y2 - y1 - ym1 + y0; 293 a1 = ym1 - y0 - a0; 294 a2 = y1 - ym1; 295 a3 = y0; 296 result = a0 * fractionOffset * mu2 + a1 * mu2 + a2 * fractionOffset + a3; 297 } 298 return result; 299 } 300 301 302 private double msToSamples(double posInMs){ 303 double positionInSamples = posInMs / audioSampleLength; 304 if(positionInSamples < 0){ 305 positionInSamples = 0; 306 }else{ 307 int bufferNumber = (int) (positionInSamples/audioBuffer.length); 308 positionInSamples = positionInSamples - bufferNumber * audioBuffer.length; 309 } 310 return positionInSamples; 311 } 312 313 @Override 314 public void processingFinished() { 315 316 } 317 318 /** 319 * Returns the value of the buffer at the given fraction along its length (0 = start, 1 = end). Uses linear interpolation. 320 * 321 * @param fraction the point along the buffer to inspect. 322 * 323 * @return the value at that point. 324 */ 325 public float getValueFraction(float fraction) { 326 float posInBuf = fraction * window.length; 327 if(fraction >= 1.0f){ 328 posInBuf -= 1.0f; 329 } 330 int lowerIndex = (int) posInBuf; 331 float offset = posInBuf - lowerIndex; 332 int upperIndex = (lowerIndex + 1) % window.length; 333 return (1 - offset) * window[lowerIndex] + offset * window[upperIndex]; 334 } 335 336 /** 337 * Calculate next position for the given Grain. 338 * 339 * @param g the Grain. 340 */ 341 private void calculateNextGrainPosition(Grain g) { 342 int direction = timeStretchFactor >= 0 ? 1 : -1; //this is a bit odd in the case when controlling grain from positionEnvelope 343 g.age += audioSampleLength; 344 g.position += direction * audioSampleLength * pitchFactor; 345 } 346 347 public void setTimestretchFactor(float currentFactor) { 348 timeStretchFactor = currentFactor; 349 } 350 351 public void setPitchShiftFactor(float currentFactor) { 352 pitchFactor = currentFactor; 353 } 354 355 356 357 public void setGrainInterval(int grainInterval) { 358 this.grainInterval = grainInterval; 359 } 360 361 362 363 public void setGrainSize(int grainSize) { 364 this.grainSize = grainSize; 365 366 } 367 368 public void setGrainRandomness(float grainRandomness) { 369 this.grainRandomness = grainRandomness; 370 } 371 372 373 374 /** 375 * @param position in seconds 376 */ 377 public void setPosition(float position) { 378 this.position = position * 1000; 379 } 380 } 381