package ddf.minim.ugens;
import java.util.Arrays;
import ddf.minim.Minim;
import ddf.minim.MultiChannelBuffer;
import ddf.minim.UGen;
/**
* Sampler is the UGen version of AudioSample and is
* the preferred method of triggering short audio files.
* You will also find Sampler much more flexible,
* since it provides ways to trigger only part of a sample, and
* to trigger a sample at different playback rates. Also, unlike AudioSample,
* a Sampler lets you specify how many voices (i.e. simultaneous
* playbacks of the sample) should have.
* <p>
* Sampler provides several inputs that allow you to control the properties
* of a triggered sample. When you call the trigger method, the values of these
* inputs are "snapshotted" and used to configure the new voice that will play
* the sample. So, changing the values does not effect already playing voices,
* except for <code>amplitude</code>, which controls the volume of the Sampler
* as a whole.
*
* @example Advanced/DrumMachine
*
* @related AudioSample
* @related UGen
*
* @author Damien Di Fede
*
*/
public class Sampler extends UGen
{
/**
* The sample number in the source sample
* the voice will start at when triggering this Sampler.
*/
public UGenInput begin;
/**
* The sample number in the source sample
* the voice will end at when triggering this Sampler.
*/
public UGenInput end;
/**
* The attack time, in seconds, when triggering
* this Sampler. Attack time is used to ramp up
* the amplitude of the voice. By default it
* is 0 seconds.
*/
public UGenInput attack;
/**
* The amplitude of this Sampler. This acts as an
* overall volume control. So changing the amplitude
* will effect all currently active voices.
*/
public UGenInput amplitude;
/**
* The playback rate used when triggering this Sampler.
*/
public UGenInput rate;
/**
* Whether triggered voices should loop or not.
*/
public boolean looping;
private MultiChannelBuffer sampleData;
// what's the sample rate of our sample data
private float sampleDataSampleRate;
// what's the baseline playback rate.
// this is set whenever sampleRateChanged is called
// and is used to scale the value of the rate input
// when starting a trigger. we need this so that,
// for example, 22k sample data will playback at
// the correct speed when played through a 44.1k
// UGen chain.
private float basePlaybackRate;
// Trigger class is defined at bottom of Sampler imp
private Trigger[] triggers;
private int nextTrigger;
/**
* Create a new Sampler for triggering the provided file.
*
* @param filename
* String: the file to load
* @param maxVoices
* int: the maximum number of voices for this Sampler
* @param system
* Minim: the instance of Minim to use for loading the file
*
*/
public Sampler( String filename, int maxVoices, Minim system )
{
triggers = new Trigger[maxVoices];
for( int i = 0; i < maxVoices; ++i )
{
triggers[i] = new Trigger();
}
sampleData = new MultiChannelBuffer(1,1);
sampleDataSampleRate = system.loadFileIntoBuffer( filename, sampleData );
createInputs();
}
/**
* Create a Sampler that will use the audio in the provided MultiChannelBuffer
* for its sample. It will make a copy of the data, so modifying the provided
* buffer after the fact will not change the audio in this Sampler.
* The original sample rate of the audio data must be provided
* so that the default playback rate of the Sampler can be set properly.
* Additionally, you must specify how many voices the Sampler should use,
* which will determine how many times the sound can overlap with itself
* when triggered.
*
* @param sampleData
* MultiChannelBuffer: the sample data this Sampler will use to generate sound
* @param sampleRate
* float: the sample rate of the sampleData
* @param maxVoices
* int: the maximum number of voices for this Sampler
*
* @related MultiChannelBuffer
*/
public Sampler( MultiChannelBuffer sampleData, float sampleRate, int maxVoices )
{
triggers = new Trigger[maxVoices];
for( int i = 0; i < maxVoices; ++i )
{
triggers[i] = new Trigger();
}
this.sampleData = new MultiChannelBuffer( sampleData.getChannelCount(), sampleData.getBufferSize() );
this.sampleData.set( sampleData );
sampleDataSampleRate = sampleRate;
createInputs();
}
private void createInputs()
{
begin = addControl(0);
end = addControl(sampleData.getBufferSize()-1);
attack = addControl();
amplitude = addControl(1);
rate = addControl(1);
}
/**
* Trigger this Sampler. If all of the Sampler's voices
* are currently in use, it will use the least recently
* triggered voice, which means whatever that voice is
* currently playing will get cut off. For this reason,
* choose the number of voices you want carefully.
*
* @shortdesc Trigger this Sampler.
*/
public void trigger()
{
triggers[nextTrigger].activate();
nextTrigger = (nextTrigger+1)%triggers.length;
}
/**
* Stop all active voices. In other words,
* immediately silence this Sampler.
*/
public void stop()
{
for( Trigger t : triggers )
{
t.stop();
}
}
/**
* Sets the sample data used by this Sampler by <em>copying</em> the
* contents of the provided MultiChannelBuffer into the internal buffer.
*
* @param newSampleData
* MultiChannelBuffer: the new sample data for this Sampler
* @param sampleRate
* float: the sample rate of the sample data
*
* @related MultiChannelBuffer
*/
public void setSample( MultiChannelBuffer newSampleData, float sampleRate )
{
sampleData.set( newSampleData );
sampleDataSampleRate = sampleRate;
basePlaybackRate = sampleRate / sampleRate();
}
@Override
protected void sampleRateChanged()
{
basePlaybackRate = sampleDataSampleRate / sampleRate();
}
@Override
protected void uGenerate(float[] channels)
{
Arrays.fill( channels, 0 );
for( Trigger t : triggers )
{
t.generate( channels );
}
}
private class Trigger
{
// begin and end sample numbers
float beginSample;
float endSample;
// playback rate
float playbackRate;
// what sample we are at in our trigger. expressed as a float to handle variable rate.
float sample;
// how many output samples we have generated, tracked for attack/release
float outSampleCount;
// attack time, in samples
int attackLength;
// current amplitude mod for attack
float attackAmp;
// how much to increase the attack amp each sample frame
float attackAmpStep;
// release time, in samples
int release;
// whether we are done playing our bit of the sample or not
boolean done;
// whether we should start triggering in the next call to generate
boolean triggering;
Trigger()
{
done = true;
}
// start this Trigger playing with the current settings of the Sampler
void activate()
{
triggering = true;
}
// stop this trigger
void stop()
{
done = true;
}
// generate one sample frame of data
void generate( float[] sampleFrame )
{
if ( triggering )
{
beginSample = (int)Math.min( begin.getLastValue(), sampleData.getBufferSize()-2);
endSample = (int)Math.min( end.getLastValue(), sampleData.getBufferSize()-1 );
playbackRate = rate.getLastValue();
attackLength = (int)Math.max( sampleRate() * attack.getLastValue(), 1.f );
attackAmp = 0;
attackAmpStep = 1.0f / attackLength;
release = 0;
sample = beginSample;
outSampleCount = 0;
done = false;
triggering = false;
}
if ( done ) return;
final float outAmp = amplitude.getLastValue() * attackAmp;
for( int c = 0; c < sampleFrame.length; ++c )
{
int sourceChannel = c < sampleData.getChannelCount() ? c : sampleData.getChannelCount() - 1;
sampleFrame[c] += outAmp * sampleData.getSample( sourceChannel, sample );
}
sample += playbackRate*basePlaybackRate;
if ( sample > endSample )
{
if ( looping )
{
sample -= endSample - beginSample;
}
else
{
done = true;
}
}
++outSampleCount;
if ( outSampleCount <= attackLength )
{
attackAmp += attackAmpStep;
}
}
}
}