All Downloads are FREE. Search and download functionalities are using the official Maven repository.

be.tarsos.dsp.AudioDispatcher Maven / Gradle / Ivy

There is a newer version: 2.4-1
Show newest version
/*
*      _______                       _____   _____ _____  
*     |__   __|                     |  __ \ / ____|  __ \ 
*        | | __ _ _ __ ___  ___  ___| |  | | (___ | |__) |
*        | |/ _` | '__/ __|/ _ \/ __| |  | |\___ \|  ___/ 
*        | | (_| | |  \__ \ (_) \__ \ |__| |____) | |     
*        |_|\__,_|_|  |___/\___/|___/_____/|_____/|_|     
*                                                         
* -------------------------------------------------------------
*
* TarsosDSP is developed by Joren Six at IPEM, University Ghent
*  
* -------------------------------------------------------------
*
*  Info: http://0110.be/tag/TarsosDSP
*  Github: https://github.com/JorenSix/TarsosDSP
*  Releases: http://0110.be/releases/TarsosDSP/
*  
*  TarsosDSP includes modified source code by various authors,
*  for credits and info, see README.
* 
*/


package be.tarsos.dsp;

import java.io.IOException;
import java.util.List;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.logging.Level;
import java.util.logging.Logger;

import be.tarsos.dsp.io.TarsosDSPAudioFloatConverter;
import be.tarsos.dsp.io.TarsosDSPAudioFormat;
import be.tarsos.dsp.io.TarsosDSPAudioInputStream;


/**
 * This class plays a file and sends float arrays to registered AudioProcessor
 * implementors. This class can be used to feed FFT's, pitch detectors, audio players, ...
 * Using a (blocking) audio player it is even possible to synchronize execution of
 * AudioProcessors and sound. This behavior can be used for visualization.
 * @author Joren Six
 */
public class AudioDispatcher implements Runnable {


	/**
	 * Log messages.
	 */
	private static final Logger LOG = Logger.getLogger(AudioDispatcher.class.getName());

	/**
	 * The audio stream (in bytes), conversion to float happens at the last
	 * moment.
	 */
	private final TarsosDSPAudioInputStream audioInputStream;

	/**
	 * This buffer is reused again and again to store audio data using the float
	 * data type.
	 */
	private float[] audioFloatBuffer;

	/**
	 * This buffer is reused again and again to store audio data using the byte
	 * data type.
	 */
	private byte[] audioByteBuffer;

	/**
	 * A list of registered audio processors. The audio processors are
	 * responsible for actually doing the digital signal processing
	 */
	private final List audioProcessors;

	/**
	 * Converter converts an array of floats to an array of bytes (and vice
	 * versa).
	 */
	private final TarsosDSPAudioFloatConverter converter;
	
	private final TarsosDSPAudioFormat format;

	/**
	 * The floatOverlap: the number of elements that are copied in the buffer
	 * from the previous buffer. Overlap should be smaller (strict) than the
	 * buffer size and can be zero. Defined in number of samples.
	 */
	private int floatOverlap, floatStepSize;

	/**
	 * The overlap and stepsize defined not in samples but in bytes. So it
	 * depends on the bit depth. Since the int datatype is used only 8,16,24,...
	 * bits or 1,2,3,... bytes are supported.
	 */
	private int byteOverlap, byteStepSize;
	
	
	/**
	 * The number of bytes to skip before processing starts.
	 */
	private long bytesToSkip;
	
	/**
	 * Position in the stream in bytes. e.g. if 44100 bytes are processed and 16
	 * bits per frame are used then you are 0.5 seconds into the stream.
	 */
	private long bytesProcessed;
	
	
	/**
	 * The audio event that is send through the processing chain.
	 */
	private AudioEvent audioEvent;
	
	/**
	 * If true the dispatcher stops dispatching audio.
	 */
	private boolean stopped;
	
	/**
	 * If true then the first buffer is only filled up to buffer size - hop size
	 * E.g. if the buffer is 2048 and the hop size is 48 then you get 2000 times
	 * zero 0 and 48 actual audio samples. During the next iteration you get
	 * mostly zeros and 96 samples.
	 */
	private boolean zeroPadFirstBuffer;
	
	/**
	 * If true then the last buffer is zero padded. Otherwise the buffer is
	 * shortened to the remaining number of samples. If false then the audio
	 * processors must be prepared to handle shorter audio buffers.
	 */
	private boolean zeroPadLastBuffer;

	/**
	 * Create a new dispatcher from a stream.
	 * 
	 * @param stream
	 *            The stream to read data from.
	 * @param audioBufferSize
	 *            The size of the buffer defines how much samples are processed
	 *            in one step. Common values are 1024,2048.
	 * @param bufferOverlap
	 *            How much consecutive buffers overlap (in samples). Half of the
	 *            AudioBufferSize is common (512, 1024) for an FFT.
	 */
	public AudioDispatcher(final TarsosDSPAudioInputStream stream, final int audioBufferSize, final int bufferOverlap){
		// The copy on write list allows concurrent modification of the list while
		// it is iterated. A nice feature to have when adding AudioProcessors while
		// the AudioDispatcher is running.
		audioProcessors = new CopyOnWriteArrayList();
		audioInputStream = stream;

		format = audioInputStream.getFormat();
		
			
		setStepSizeAndOverlap(audioBufferSize, bufferOverlap);
		
		audioEvent = new AudioEvent(format);
		audioEvent.setFloatBuffer(audioFloatBuffer);
		audioEvent.setOverlap(bufferOverlap);
		
		converter = TarsosDSPAudioFloatConverter.getConverter(format);
		
		stopped = false;
		
		bytesToSkip = 0;
		
		zeroPadLastBuffer = true;
	}	
	
	/**
	 * Skip a number of seconds before processing the stream.
	 * @param seconds
	 */
	public void skip(double seconds){
		bytesToSkip = Math.round(seconds * format.getSampleRate()) * format.getFrameSize(); 
	}
	
	/**
	 * Set a new step size and overlap size. Both in number of samples. Watch
	 * out with this method: it should be called after a batch of samples is
	 * processed, not during.
	 * 
	 * @param audioBufferSize
	 *            The size of the buffer defines how much samples are processed
	 *            in one step. Common values are 1024,2048.
	 * @param bufferOverlap
	 *            How much consecutive buffers overlap (in samples). Half of the
	 *            AudioBufferSize is common (512, 1024) for an FFT.
	 */
	public void setStepSizeAndOverlap(final int audioBufferSize, final int bufferOverlap){
		audioFloatBuffer = new float[audioBufferSize];
		floatOverlap = bufferOverlap;
		floatStepSize = audioFloatBuffer.length - floatOverlap;

		audioByteBuffer = new byte[audioFloatBuffer.length * format.getFrameSize()];
		byteOverlap = floatOverlap * format.getFrameSize();
		byteStepSize = floatStepSize * format.getFrameSize();
	}
	
	/**
	 * if zero pad is true then the first buffer is only filled up to  buffer size - hop size
	 * E.g. if the buffer is 2048 and the hop size is 48 then you get 2000x0 and 48 filled audio samples
	 * @param zeroPadFirstBuffer true if the buffer should be zeroPadFirstBuffer, false otherwise.
	 */
	public void setZeroPadFirstBuffer(boolean zeroPadFirstBuffer){
		this.zeroPadFirstBuffer = zeroPadFirstBuffer;		
	}
	
	/**
	 * If zero pad last buffer is true then the last buffer is filled with zeros until the normal amount
	 * of elements are present in the buffer. Otherwise the buffer only contains the last elements and no zeros.
	 * By default it is set to true.
	 * 
	 * @param zeroPadLastBuffer
	 */
	public void setZeroPadLastBuffer(boolean zeroPadLastBuffer) {
		this.zeroPadLastBuffer = zeroPadLastBuffer;
	}
	

	/**
	 * Adds an AudioProcessor to the chain of processors.
	 * 
	 * @param audioProcessor
	 *            The AudioProcessor to add.
	 */
	public void addAudioProcessor(final AudioProcessor audioProcessor) {
		audioProcessors.add(audioProcessor);
		LOG.fine("Added an audioprocessor to the list of processors: " + audioProcessor.toString());
	}
	
	/**
	 * Removes an AudioProcessor to the chain of processors and calls its processingFinished method.
	 * 
	 * @param audioProcessor
	 *            The AudioProcessor to remove.
	 */
	public void removeAudioProcessor(final AudioProcessor audioProcessor) {
		audioProcessors.remove(audioProcessor);
		audioProcessor.processingFinished();
		LOG.fine("Remove an audioprocessor to the list of processors: " + audioProcessor.toString());
	}

	public void run() {
		
		int bytesRead = 0;
		
		if(bytesToSkip!=0){
			skipToStart();
		}
	
		//Read the first (and in some cases last) audio block.
		try {
			//needed to get correct time info when skipping first x seconds
			audioEvent.setBytesProcessed(bytesProcessed);
			bytesRead = readNextAudioBlock();
		} catch (IOException e) {
			String message="Error while reading audio input stream: " + e.getMessage();	
			LOG.warning(message);
			throw new Error(message);
		}

		// As long as the stream has not ended
		while (bytesRead != 0 && !stopped) {
			
			//Makes sure the right buffers are processed, they can be changed by audio processors.
			for (final AudioProcessor processor : audioProcessors) {
				if(!processor.process(audioEvent)){
					//skip to the next audio processors if false is returned.
					break;
				}	
			}
			
			if(!stopped){			
				//Update the number of bytes processed;
				bytesProcessed += bytesRead;
				audioEvent.setBytesProcessed(bytesProcessed);
					
				// Read, convert and process consecutive overlapping buffers.
				// Slide the buffer.
				try {
					bytesRead = readNextAudioBlock();
					audioEvent.setOverlap(floatOverlap);
				} catch (IOException e) {
					String message="Error while reading audio input stream: " + e.getMessage();	
					LOG.warning(message);
					throw new Error(message);
				}
			}
		}

		// Notify all processors that no more data is available. 
		// when stop() is called processingFinished is called explicitly, no need to do this again.
		// The explicit call is to prevent timing issues.
		if(!stopped){
			stop();
		}
	}
	
	
	private void skipToStart() {
		long skipped = 0l;
		try{
			skipped = audioInputStream.skip(bytesToSkip);
			if(skipped !=bytesToSkip){
				throw new IOException();
			}
			bytesProcessed += bytesToSkip;
		}catch(IOException e){
			String message=String.format("Did not skip the expected amount of bytes,  %d skipped, %d expected!", skipped,bytesToSkip);	
			LOG.warning(message);
			throw new Error(message);
		}
	}

	/**
	 * Stops dispatching audio data.
	 */
	public void stop() {
		stopped = true;
		for (final AudioProcessor processor : audioProcessors) {
			processor.processingFinished();
		}
		try {
			audioInputStream.close();
		} catch (IOException e) {
			LOG.log(Level.SEVERE, "Closing audio stream error.", e);
		}
	}

	/**
	 * Reads the next audio block. It tries to read the number of bytes defined
	 * by the audio buffer size minus the overlap. If the expected number of
	 * bytes could not be read either the end of the stream is reached or
	 * something went wrong.
	 * 
	 * The behavior for the first and last buffer is defined by their corresponding the zero pad settings. The method also handles the case if
	 * the first buffer is also the last.
	 * 
	 * @return The number of bytes read.
	 * @throws IOException
	 *             When something goes wrong while reading the stream. In
	 *             particular, an IOException is thrown if the input stream has
	 *             been closed.
	 */
	private int readNextAudioBlock() throws IOException {
		assert floatOverlap < audioFloatBuffer.length;
		
		// Is this the first buffer?
		boolean isFirstBuffer = (bytesProcessed ==0 || bytesProcessed == bytesToSkip);
		
		final int offsetInBytes;
		
		final int offsetInSamples;
		
		final int bytesToRead;
		//Determine the amount of bytes to read from the stream
		if(isFirstBuffer && !zeroPadFirstBuffer){
			//If this is the first buffer and we do not want to zero pad the
			//first buffer then read a full buffer
			bytesToRead =  audioByteBuffer.length;
			// With an offset in bytes of zero;
			offsetInBytes = 0;
			offsetInSamples=0;
		}else{
			//In all other cases read the amount of bytes defined by the step size
			bytesToRead = byteStepSize;
			offsetInBytes = byteOverlap;
			offsetInSamples = floatOverlap;
		}
		
		//Shift the audio information using array copy since it is probably faster than manually shifting it.
		// No need to do this on the first buffer
		if(!isFirstBuffer && audioFloatBuffer.length == floatOverlap + floatStepSize ){
			System.arraycopy(audioFloatBuffer,floatStepSize, audioFloatBuffer,0 ,floatOverlap);
			/*
			for(int i = floatStepSize ; i < floatStepSize+floatOverlap ; i++){
				audioFloatBuffer[i-floatStepSize] = audioFloatBuffer[i];
			}*/
		}
		
		// Total amount of bytes read
		int totalBytesRead = 0;
		
		// The amount of bytes read from the stream during one iteration.
		int bytesRead=0;
		
		// Is the end of the stream reached?
		boolean endOfStream = false;
				
		// Always try to read the 'bytesToRead' amount of bytes.
		// unless the stream is closed (stopped is true) or no bytes could be read during one iteration 
		while(!stopped && !endOfStream && totalBytesRead




© 2015 - 2024 Weber Informatics LLC | Privacy Policy