package ddf.minim.ugens;
import java.util.Random;
/**
* Wavetable wraps a float array of any size and lets you sample the array using
* a normalized value [0,1]. This means that if you have an array that is 2048
* samples long, then value(0.5) will give you the 1024th sample. You will most
* often use Wavetables as the Waveform in an Oscil, but other uses are also
* possible. Additionally, Wavetable provides a set of methods for transforming
* the samples it contains.
*
* @example Synthesis/WavetableMethods
*
* @related Waveform
* @related Waves
* @related WavetableGenerator
*
* @author Mark Godfrey <mark.godfrey@gatech.edu>
*/
public class Wavetable implements Waveform
{
private float[] waveform;
// precalculate this since we use it alot
private float lengthForValue;
/**
* Construct a Wavetable that contains <code>size</code> entries.
*
* @param size
* int: the number of samples the Wavetable should contain
*
* @related Wavetable
*/
public Wavetable(int size)
{
waveform = new float[size];
lengthForValue = size - 1;
}
/**
* Construct a Wavetable that will use <code>waveform</code> as the float
* array to sample from. This <em>will not</em> copy <code>waveform</code>,
* it will use it directly.
*
* @param waveform
* float[]: the float array this Wavetable will sample
*
* @related Wavetable
*/
public Wavetable(float[] waveform)
{
this.waveform = waveform;
lengthForValue = waveform.length - 1;
}
/**
* Make a new Wavetable that has the same waveform values as
* <code>wavetable</code>. This will <em>copy</em> the values from the
* provided Wavetable into this Wavetable's waveform.
*
* @param wavetable
* Wavetable: the Wavetable to copy
*
* @related Wavetable
*/
public Wavetable(Wavetable wavetable)
{
waveform = new float[wavetable.waveform.length];
System.arraycopy( wavetable.waveform, 0, waveform, 0, waveform.length );
lengthForValue = waveform.length - 1;
}
/**
* Sets this Wavetable's waveform to the one provided. This
* <em>will not</em> copy the values from the provided waveform, it will use
* the waveform directly.
*
* @param waveform
* float[]: the new sample data
*
* @related Wavetable
*/
public void setWaveform(float[] waveform)
{
this.waveform = waveform;
lengthForValue = waveform.length - 1;
}
/**
* Returns the value of the i<sup>th</sup> entry in this Wavetable's
* waveform. This is equivalent to getWaveform()[i].
*
* @shortdesc Returns the value of the i<sup>th</sup> entry in this Wavetable's
* waveform.
*
* @param i
* int: the index of the sample to return
*
* @return float: the value of the sample at i
*
* @related Wavetable
*/
public float get(int i)
{
return waveform[i];
}
/**
* Sample the Wavetable using a value in the range [0,1]. For instance, if
* the Wavetable has 1024 values in its float array, then calling value(0.5)
* will return the 512th value in the array. If the result is that it needs
* say the 456.65th value, this will interpolate between the surrounding
* values.
*
* @shortdesc Sample the Wavetable using a value in the range [0,1].
*
* @example Synthesis/WavetableMethods
*
* @param at
* float: a value in the range [0, 1]
*
* @return float: this Wavetable sampled at the requested interval
*
* @related Wavetable
*/
public float value(float at)
{
float whichSample = lengthForValue * at;
// linearly interpolate between the two samples we want.
int lowSamp = (int)whichSample;
int hiSamp = lowSamp + 1;
// lowSamp might be the last sample in the waveform
// we need to make sure we wrap.
if ( hiSamp >= waveform.length )
{
hiSamp -= waveform.length;
}
float rem = whichSample - lowSamp;
return waveform[lowSamp] + rem
* ( waveform[hiSamp] - waveform[lowSamp] );
// This was here for testing.
// Causes non-interpolation, but adds max # of oscillators
// return get(lowSamp);
}
/**
* Returns the underlying waveform, <em>not</em> a copy of it.
*
* @return float[]: the float array managed by this Wavetable
*
* @related Wavetable
*/
public float[] getWaveform()
{
return waveform;
}
/**
* Sets the i<sup>th</sup> entry of the underlying waveform to
* <code>value</code>. This is equivalent to:
* <p>
* <code>getWaveform()[i] = value;</code>
*
* @param i
* int: the index of the sample to set
* @param value
* float: the new sample value
*
* @related Wavetable
*/
public void set(int i, float value)
{
waveform[i] = value;
}
/**
* Returns the length of the underlying waveform. This is equivalent to:
* <p>
* <code>getWaveform().length</code>
*
* @return int: the length of the underlying float array
*
* @related Wavetable
*/
public int size()
{
return waveform.length;
}
/**
* Multiplies each value of the underlying waveform by <code>scale</code>.
*
* @param scale
* float: the amount to scale the Wavetable with
*
* @related Wavetable
*/
public void scale(float scale)
{
for ( int i = 0; i < waveform.length; i++ )
{
waveform[i] *= scale;
}
}
/**
* Apply a DC offset to this Wavetable. In other words, add
* <code>amount</code> to every sample.
*
* @param amount
* float: the amount to add to every sample in the table
*
* @related Wavetable
*/
public void offset(float amount)
{
for ( int i = 0; i < waveform.length; ++i )
{
waveform[i] += amount;
}
}
/**
* Normalizes the Wavetable by finding the largest amplitude in the table
* and scaling the table by the inverse of that amount. The result is that
* the largest value in the table will now have an amplitude of 1 and
* everything else is scaled proportionally.
*
* @example Synthesis/WavetableMethods
*
* @related Wavetable
*/
public void normalize()
{
float max = Float.MIN_VALUE;
for ( int i = 0; i < waveform.length; i++ )
{
if ( Math.abs( waveform[i] ) > max )
max = Math.abs( waveform[i] );
}
scale( 1 / max );
}
/**
* Flips the table around 0. Equivalent to <code>flip(0)</code>.
*
* @see #flip(float)
* @related flip ( )
* @related Wavetable
*/
public void invert()
{
flip( 0 );
}
/**
* Flip the values in the table around a particular value. For example, if
* you flip around 2, values greater than 2 will become less than two by the
* same amount and values less than 2 will become greater than 2 by the same
* amount. 3 -> 1, 0 -> 4, etc.
*
* @shortdesc Flip the values in the table around a particular value.
*
* @example Synthesis/WavetableMethods
*
* @param in
* float: the value to flip the table around
*
* @related Wavetable
*/
public void flip(float in)
{
for ( int i = 0; i < waveform.length; i++ )
{
if ( waveform[i] > in )
waveform[i] = in - ( waveform[i] - in );
else
waveform[i] = in + ( in - waveform[i] );
}
}
/**
* Adds Gaussian noise to the waveform.
*
* @example Synthesis/WavetableMethods
*
* @param sigma
* float: the amount to scale the random values by, in effect how
* "loud" the added noise will be.
*
* @related Wavetable
*/
public void addNoise(float sigma)
{
Random rgen = new Random();
for ( int i = 0; i < waveform.length; i++ )
{
waveform[i] += ( (float)rgen.nextGaussian() ) * sigma;
}
}
/**
* Inverts all values in the table that are less than zero. -1 -> 1, -0.2 -> 0.2, etc.
*
* @example Synthesis/WavetableMethods
*
* @related Wavetable
*/
public void rectify()
{
for ( int i = 0; i < waveform.length; i++ )
{
if ( waveform[i] < 0 )
waveform[i] *= -1;
}
}
/**
* Smooth out the values in the table by using a moving average window.
*
* @example Synthesis/WavetableMethods
*
* @param windowLength
* int: how many samples large the window should be
*
* @related Wavetable
*/
public void smooth(int windowLength)
{
if ( windowLength < 1 )
return;
float[] temp = (float[])waveform.clone();
for ( int i = windowLength; i < waveform.length; i++ )
{
float avg = 0;
for ( int j = i - windowLength; j <= i; j++ )
{
avg += temp[j] / windowLength;
}
waveform[i] = avg;
}
}
/**
* Warping works by choosing a point in the waveform, the warpPoint, and
* then specifying where it should move to, the warpTarget. Both values
* should be normalized (i.e. in the range [0,1]). What will happen is that
* the waveform data in front of and behind the warpPoint will be squashed
* or stretch to fill the space defined by where the warpTarget is. For
* instance, if you took Waves.SQUARE and called warp( 0.5, 0.2 ), you would
* wind up with a square wave with a 20 percent duty cycle, the same as
* using Waves.square( 0.2 ). This is because the crossover point of a
* square wave is halfway through and warping it such that the crossover is
* moved to 20% through the waveform is equivalent to changing the duty
* cycle. Or course, much more interesting things happen when warping a more
* complex waveform, such as one returned by the Waves.randomNHarms method,
* especially if it is warped more than once.
*
* @shortdesc Warping works by choosing a point in the waveform, the
* warpPoint, and then specifying where it should move to, the
* warpTarget.
*
* @example Synthesis/WavetableMethods
*
* @param warpPoint
* float: the point in the wave for to be moved, expressed as a
* normalized value.
* @param warpTarget
* float: the point in the wave to move the warpPoint to,
* expressed as a normalized value.
*
* @related Wavetable
*/
public void warp(float warpPoint, float warpTarget)
{
float[] newWave = new float[waveform.length];
for ( int s = 0; s < newWave.length; ++s )
{
float lookup = (float)s / newWave.length;
if ( lookup <= warpTarget )
{
// normalize look up to [0,warpTarget], expand to [0,warpPoint]
lookup = ( lookup / warpTarget ) * warpPoint;
}
else
{
// map (warpTarget,1] to (warpPoint,1]
lookup = warpPoint + ( 1 - ( 1 - lookup ) / ( 1 - warpTarget ) ) * ( 1 - warpPoint );
}
newWave[s] = value( lookup );
}
waveform = newWave;
}
}