plectrum

Plectrum: instrument tuner for Android
Log | Files | Refs | README | LICENSE

Granulator.java (9722B)


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