view src/uk/ac/qmul/eecs/depic/daw/beads/SamplePlayer.java @ 2:c0412c81d274

Added documentation
author Fiore Martin <f.martin@qmul.ac.uk>
date Thu, 18 Feb 2016 18:35:26 +0000
parents 3074a84ef81e
children
line wrap: on
line source
/*  
 Cross-Modal DAW Prototype - Prototype of a simple Cross-Modal Digital Audio Workstation.

 Copyright (C) 2015  Queen Mary University of London (http://depic.eecs.qmul.ac.uk/)
	
 This program is free software: you can redistribute it and/or modify
 it under the terms of the GNU General Public License as published by
 the Free Software Foundation, either version 3 of the License, or
 (at your option) any later version.

 This program is distributed in the hope that it will be useful,
 but WITHOUT ANY WARRANTY; without even the implied warranty of
 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
 GNU General Public License for more details.

 You should have received a copy of the GNU General Public License
 along with this program.  If not, see <http://www.gnu.org/licenses/>.
*/
package uk.ac.qmul.eecs.depic.daw.beads;

/*
 * This file is part of Beads. See http://www.beadsproject.net for all information.
 */

import java.util.Collections;
import java.util.List;

import net.beadsproject.beads.core.AudioContext;
import net.beadsproject.beads.core.Bead;
import net.beadsproject.beads.core.UGen;
import net.beadsproject.beads.ugens.Static;
import uk.ac.qmul.eecs.depic.daw.Clip;
import uk.ac.qmul.eecs.depic.daw.ClipList;
import uk.ac.qmul.eecs.depic.daw.SoundWaveEvent;
import uk.ac.qmul.eecs.depic.daw.SoundWaveListener;
import uk.ac.qmul.eecs.depic.patterns.Range;

/**
 * This is actually responsible for play the audio file in a track when the play button is pressed. 
 * It implements a {@code SoundWaveListener} because it updates its lenght when the sound wave changes 
 * 
 * 
 */
public class SamplePlayer extends UGen implements SoundWaveListener {
	
	public static final int NUM_CHANNELS = 2;
	public static final float ADAPTIVE_INTERP_LOW_THRESH = 0.5f;
	public static final float ADAPTIVE_INTERP_HIGH_THRESH = 2.5f;
	

	/**
	 * Used to determine which kind of loop the sample player will use.
	 */
	public static enum LoopType {
		/** Play forwards without looping. */
		NO_LOOP, 
		/** Play forwards with loop. */
		LOOP, 
	};
	

	/** The position in milliseconds. */
	protected double position;                

	/** The rate envelope. */
	protected UGen rateEnvelope;            

	/** The millisecond position increment per sample. Calculated from the ratio of the {@link AudioContext}'s sample rate and the Sample's sample rate. */
	protected double positionIncrement;           


	/** The loop type. */
	protected LoopType loopType;

	/** Flag to determine whether playback starts at the beginning of the sample or at the beginning of the loop. */
	protected boolean startLoop;


	/** The rate. Calculated and used internally from the rate envelope. */
	protected float rate;

	/** The loop start. Calculated and used internally from the loop start envelope. */
	protected float loopStart;

	/** The loop end. Calculated and used internally from the loop end envelope. */
	protected float loopEnd;
	
	/** Bead responding to sample at end (only applies when not in loop mode). */
	private Bead endListener;

	/* lenght of the sample player */
	private float length;
	
	private ClipList clips;
	
	private List<Clip> clipsAtPosition;
	private boolean loopEnabled;
	

	/**
	 * Instantiates a new SamplePlayer with given number of outputs.
	 * 
	 * @param context the AudioContext.
	 * @param outs the number of outputs.
	 */
	public SamplePlayer(AudioContext context, ClipList clips) {
		super(context, NUM_CHANNELS);
		this.clips = clips;
		clipsAtPosition = Collections.emptyList();
		clips.addSoundWaveListener(this);
		rateEnvelope = new Static(context, 1.0f);
		loopType = LoopType.NO_LOOP;
		loopStart = 0.0f;
		loopEnd = clips.getLength();
		length = 0.0f;
		positionIncrement = context.samplesToMs(1);
	}


	/**
	 * Sets the playback position to the end of the Sample.
	 */
	public void setToEnd() {
		position = length;
	}

	/**
	 * Determines whether the playback position is within the loop points.
	 * 
	 * @return true if the playback position is within the loop points.
	 */
	public boolean inLoop() {
		return position < Math.max(loopStart, loopEnd) && position > Math.min(loopStart, loopEnd);
	}

	/**
	 * Sets the playback position to the loop start point.
	 */
	public void setToLoopStart() {
		position = Math.min(loopStart, loopEnd);
	}

	/**
	 * Starts the sample at the given position.
	 * 
	 * @param msPosition the position in milliseconds.
	 */
	public void start(float msPosition) {
		position = msPosition;
		start();
	}

	/**
	 * Resets the position to the start of the Sample.
	 */
	public void reset() {
		position = 0f;
	}

	/**
	 * Gets the playback position.
	 * 
	 * @return the position in milliseconds.
	 */
	public double getPosition() {
		return position;
	}

	/**
	 * Sets the playback position. This will not work if the position envelope is not null.
	 * 
	 * @param position the new position in milliseconds.
	 */
	public void setPosition(double position) {
		this.position = position;
	}


	/**
	 * Gets the rate UGen.
	 * 
	 * @return the rate UGen.
	 */
	public UGen getRateUGen() {
		return rateEnvelope;
	}
	
	
	/**
	 * Sets the rate to a UGen.
	 * 
	 * @param rateUGen the new rate UGen.
	 */
	public void setRate(UGen rateUGen) {
		this.rateEnvelope = rateUGen;
	}
	
	/**
	 * Gets the rate UGen (this method is provided so that SamplePlayer and GranularSamplePlayer can 
	 * be used interchangeably).
	 * 
	 * @return the rate envelope.
	 */
	public UGen getPitchUGen() {
		return rateEnvelope;
	}

	
	/**
	 * Sets the rate UGen (this method is provided so that SamplePlayer and GranularSamplePlayer can 
	 * be used interchangeably).
	 * 
	 * @param rateUGen the new rate UGen.
	 */
	public void setPitch(UGen rateUGen) {
		this.rateEnvelope = rateUGen;
	}

	

	/**
	 * Sets both loop points to static values as fractions of the Sample length, 
	 * overriding any UGens that were controlling the loop points.
	 * 
	 * @param start the start value, as fraction of the Sample length.
	 * @param end the end value, as fraction of the Sample length.
	 */
	public void setLoopPointsFraction(float start, float end) {
		float l = getLenght();
		loopStart = start * l;
		loopEnd = end * l;
	}
	
	public Range<Float> getLoop(){
		return new Range<Float>(loopStart,loopEnd);
	}

	/**
	 * Gets the loop type.
	 * 
	 * @return the loop type.
	 */
	public LoopType getLoopType() {
		return loopType;
	}

	/**
	 * Sets the loop type.
	 * 
	 * @param loopType the new loop type.
	 */
	public void setLoopType(LoopType loopType) {
		if(loopEnabled){
			this.loopType = loopType;
		}
	}

	
	public void setLoopEnabled(boolean enabled){
		loopEnabled = enabled;
		this.loopType = loopEnabled ? LoopType.LOOP : LoopType.NO_LOOP;
	}
	
	/**
	 * Gets the sample rate.
	 * 
	 * @return the sample rate, in samples per second.
	 */
	public float getSampleRate() {
		return rate;
	}

	
	@Override
	public void calculateBuffer(){
		//major speed up possible here if these envelopes are all either null or paused 
		//(can we pause Envelope when it is not doing anything?).
		//if this holds true we can tell buffer to just grab the whole frame at the given rate
		//and then update the position all at once.
		rateEnvelope.update();
		
		for (int i = 0; i < bufferSize; i++) {
			/* calculate the samples */
			
			/* initialize to silence */
			for (int j = 0; j < NUM_CHANNELS; j++) {
				bufOut[j][i] = 0.0f;
			}

			if(!clipsAtPosition.isEmpty()){ /* if no current sample, then output silence */
				for(Clip s : clipsAtPosition){
					float [] frame = new float[s.getSample().getNumChannels()];
					/* Always use adaptive Interpolation */
					if(rate > ADAPTIVE_INTERP_HIGH_THRESH) {
						s.getSample().getFrameNoInterp(s.getSampleStartMs()+(position-s.getStartTimeMs()), frame);
					} else if(rate > ADAPTIVE_INTERP_LOW_THRESH) {
						s.getSample().getFrameLinear(s.getSampleStartMs()+(position-s.getStartTimeMs()), frame);
					} else {
						s.getSample().getFrameCubic(s.getSampleStartMs()+(position-s.getStartTimeMs()), frame);
					}
					
					/* if the sample is mono, fill the two channels with the same frame */
					for (int j = 0; j < NUM_CHANNELS; j++) {
						bufOut[j][i] += frame[j % frame.length];
						/* clip the sound */
						if(bufOut[j][i] > 1.0f )
							bufOut[j][i] = 1.0f;
						else if(bufOut[j][i] < -1.0f){
							bufOut[j][i] = -1.0f;
						}
					}
				}
			}
					
			//update the position, loop state, direction
			calculateNextPosition(i);
		}
	}

	public float getLenght(){
		return Math.max(length,clips.getLengthMs());
	}
	
	public void setLength(float length){
		this.length = length;
	}
	
	/**
	 * Used at each sample in the perform routine to determine the next playback position.
	 * 
	 * @param i the index within the buffer loop.
	 */
	protected void calculateNextPosition(int i) {
		rate = rateEnvelope.getValue(0, i);
		switch(loopType) {
		case NO_LOOP:
			position += positionIncrement * rate;
			if(position > clips.getLengthMs() || position < 0.0f) 
				atEnd();
			break;
		case LOOP:
			position += positionIncrement * rate;
			if(rate > 0 && position > Math.max(loopStart, loopEnd)) {
				position = Math.min(loopStart, loopEnd);
			} else if(rate < 0 && position < Math.min(loopStart, loopEnd)) {
				position = Math.max(loopStart, loopEnd);
			}
			break;
		}
		clipsAtPosition = clips.getClipsAtTime((float)position);		
	}

	/**
	 * Called when at the end of the Sample, assuming the loop mode is non-looping, or beginning, if the SamplePlayer is playing backwards..
	 */
	private void atEnd() {
		if(endListener != null) {
			endListener.message(this);
		}
		reTrigger();
	}
	
	/**
	 * Sets a {@link Bead} that will be triggered when this SamplePlayer gets to the end. This occurs when the SamplePlayer's
	 * position reaches then end when playing forwards in a non-looping mode, or reaches the the beginning when playing backwards in a 
	 * non-looping mode. It is never triggered in a looping mode. As an alternative, you can use the method {@link Bead.#setKillListener(Bead)}
	 * as long as {@link #setKillOnEnd(boolean)} is set to true. In other words, you set this SamplePlayer to kill itself when it
	 * reaches the end of the sample, and then use the functionality of {@link Bead}, which allows you to create a trigger
	 * whenever a Bead is killed. Set to null to remove the current listener.
	 * 
	 * @param endListener the {@link Bead} that responds to this SamplePlayer reaching its end.
	 */
	public void setEndListener(Bead endListener) {
		this.endListener = endListener;
	}
	
	/**
	 * Gets the current endListener. 
	 * @see {#setEndListener(Bead)}.
	 * @return the current endListener.
	 */
	public Bead getEndListener() {
		return endListener;
	}

	/**
	 * Re trigger the SamplePlayer from the beginning.
	 */
	public void reTrigger() {
		reset();
		this.pause(false);
	}
	
	/**
	 * Updates the lenght when the sound wave changes 
	 */
	@Override
	public void update(SoundWaveEvent evt) {
		length = clips.getLengthMs();		
	}

}