/*
* @(#)AudioSamples.java 1.02 April 27, 2005.
*
* Cory McKay
* McGill Univarsity
*/
package org.vocvark.jAudioTools;
import javax.sound.sampled.AudioFileFormat;
import javax.sound.sampled.AudioFormat;
import javax.sound.sampled.AudioInputStream;
import javax.sound.sampled.AudioSystem;
import javax.sound.sampled.UnsupportedAudioFileException;
import java.io.File;
import java.io.IOException;
/**
* A class for holding audio samples and associated audio formatting information.
* Samples are stored as arrays of doubles, generally for use in analysis and
* signal processing. Samples are stored maintaining any channel segregation as
* well as mixed down to a single channel. Values can vary between -1 and +1.
* <p/>
* <p>Each object of this class is assigned a string at instantiation that
* can be used externally as a reference. Although not used internally by the
* methods of this class, it will generally be best to assign a unique value
* to this string, such as a filename.
* <p/>
* <p>Includes constructors for generating and storing the samples from
* AudioInputStreams, audio files or existing arrays of sample values.
* <p/>
* <p>Includes methods for accessing samples as a whole or as segments, and also
* for returning the samples after having been broken into equally sized windows
* of specified sizes. A method is also present for saving the samples to a File.
* Methods are also available for checking if any sample values fall out of the
* allowable ranges (i.e. between -1 and +1) and for normalizing samples so that
* the maximum amplitude will have an absolute value of 1. Methods are also
* available for getting copies of objects of this class as well as for getting
* formatted information about them. The samples can also be changed externally.
*
* @author Cory McKay
*/
public class AudioSamples {
/* FIELDS ******************************************************************/
/**
* A unique identifier that external objects can use to identify each individual
* object of this class. Not used internally by objects of this class.
*/
protected String unique_ID;
/**
* Audio samles, with a minimum value of -1 and a maximum value of +1. If
* audio is multi-channel, all channels are mixed down into this one channel.
*/
protected double[] samples;
/**
* Audio samles, with a minimum value of -1 and a maximum value of +1. Is
* set to null if only one channel of audio is present. First indice
* corresponds to channel and second indice corresponds to sample number.
*/
protected double[][] channel_samples;
/**
* The AudioFormat used to encode the samples field. Will always involve
* big-endian signed linear PCM encoding and a bit depth of eith 8 or 16 bits.
*/
protected AudioFormat audio_format;
/* CONSTRUCTORS ************************************************************/
/**
* Store the given audio file as samples and the corresponding
* AudioFormat.
* <p/>
* <p><b>IMPORTANT:</b> Note that, regardless of the AudioFormat in the given file,
* it will be converted and stored using big-endian signed linear PCM encoding.
* Sampling rate and number of channels is maintained, but bit depth will be
* changed to 16 bits if it is not either 8 or 16 bits.
*
* @param audio_file A reference to an audio file from which to extract
* and store samples as double values.
* @param unique_identifier The string that will be used by external
* objects to uniquely identify the instantiated
* AudioSamples object.
* @param normalize_if_clipped If set to true, then normalizes audio so the
* absolute value of the highest amplitude sample is
* 1. Does this if and only if one or more of the samples
* is outside the allowable range of sample values
* (-1 to 1). If set to false, then does not
* normalize, regardless of sample values.
* @throws Exception Throws an informative exception if the samples
* cannot be extracted from the file.
*/
public AudioSamples(File audio_file,
String unique_identifier,
boolean normalize_if_clipped)
throws Exception {
if (!audio_file.exists())
throw new Exception("File " + audio_file.getName() + " does not exist.");
if (audio_file.isDirectory())
throw new Exception("File " + audio_file.getName() + " is a directory.");
AudioInputStream audio_input_stream = null;
try {
audio_input_stream = AudioSystem.getAudioInputStream(audio_file);
} catch (UnsupportedAudioFileException ex) {
throw new Exception("File " + audio_file.getName() + " has an unsupported audio format.");
} catch (IOException ex) {
throw new Exception("File " + audio_file.getName() + " is not readable.");
}
AudioInputStream converted_audio = org.vocvark.jAudioTools.AudioMethods.getConvertedAudioStream(audio_input_stream);
channel_samples = org.vocvark.jAudioTools.AudioMethods.extractSampleValues(converted_audio);
samples = DSPMethods.getSamplesMixedDownIntoOneChannel(channel_samples);
if (channel_samples.length == 1)
channel_samples = null;
audio_format = converted_audio.getFormat();
unique_ID = unique_identifier;
if (normalize_if_clipped)
normalizeIfClipped();
converted_audio.close();
}
/**
* Store the given AudioInputStream as samples and the corresponding
* AudioFormat.
* <p/>
* <p><b>IMPORTANT:</b> Note that the AudioFormat in the AudioInputStream will
* be converted and stored as big-endian signed linear PCM encoding with. Sampling
* rate and number of channels is maintained, but bit depth will be changed
* to 16 bits if it is not either 8 or 16 bits.
*
* @param audio_input_stream An AudioInputStream from which to extract
* and store samples as double values.
* @param unique_identifier The string that will be used by external
* objects to uniquely identify the instantiated
* AudioSamples object.
* @param normalize_if_clipped If set to true, then normalizes audio so the
* absolute value of the highest amplitude sample is
* 1. Does this if and only if one or more of the samples
* is outside the allowable range of sample values
* (-1 to 1). If set to false, then does not
* normalize, regardless of sample values.
* @throws Exception Throws an informative exception if the samples
* cannot be extracted from the AudioInputStream.
*/
public AudioSamples(AudioInputStream audio_input_stream,
String unique_identifier,
boolean normalize_if_clipped)
throws Exception {
if (audio_input_stream == null)
throw new Exception("Given AudioInputStream is empty.");
unique_ID = unique_identifier;
AudioInputStream converted_audio = org.vocvark.jAudioTools.AudioMethods.getConvertedAudioStream(audio_input_stream);
channel_samples = org.vocvark.jAudioTools.AudioMethods.extractSampleValues(converted_audio);
samples = DSPMethods.getSamplesMixedDownIntoOneChannel(channel_samples);
if (channel_samples.length == 1)
channel_samples = null;
audio_format = converted_audio.getFormat();
if (normalize_if_clipped)
normalizeIfClipped();
converted_audio.close();
}
/**
* Store the given samples with the associated AudioFormat.
* <p/>
* <p><b>IMPORTANT:</b> Note that, regardless of the given AudioFormat, it will be
* converted and samples will be stored as big-endian signed linear PCM encoding.
* Sampling rate and number of channels is maintained, but bit depth will be changed
* to 16 bits if it is not either 8 or 16 bits.
*
* @param audio_samples Audio samles to store, with a minimum value of
* -1 and a maximum value of +1. The first indice
* corresponds to the channel and the second indice
* corresponds to the sample number.
* @param audio_format The AudioFormat to use for interpereting the
* given samples.
* @param unique_identifier The string that will be used by external
* objects to uniquely identify the instantiated
* AudioSamples object.
* @param normalize_if_clipped If set to true, then normalizes audio so the
* absolute value of the highest amplitude sample is
* 1. Does this if and only if one or more of the samples
* is outside the allowable range of sample values
* (-1 to 1). If set to false, then does not
* normalize, regardless of sample values.
* @throws Exception Throws an informative exception if the samples
* cannot be extracted from the audio_samples.
*/
public AudioSamples(double[][] audio_samples,
AudioFormat audio_format,
String unique_identifier,
boolean normalize_if_clipped)
throws Exception {
if (audio_samples == null)
throw new Exception("Given audio samples array is empty.");
for (int chan = 0; chan < audio_samples.length; chan++)
if (audio_samples[chan] == null)
throw new Exception("One or more channels of given audio samples array is empty.");
int number_samples = audio_samples[0].length;
for (int chan = 0; chan < audio_samples.length; chan++)
if (audio_samples[chan].length != number_samples)
throw new Exception("Different channels of given audio samples array have a\n" +
"different number of samples.");
if (audio_format == null)
throw new Exception("Null audio format specified for samples.");
if (audio_format.getChannels() != audio_samples.length)
throw new Exception("The specified samples have " + audio_samples.length + " channels but\n" +
"the specified audio format has " + audio_format.getChannels() + " channels.\n" +
"These must be the same.");
unique_ID = unique_identifier;
samples = DSPMethods.getSamplesMixedDownIntoOneChannel(audio_samples);
if (audio_samples.length == 1)
channel_samples = null;
else
channel_samples = DSPMethods.getCopyOfSamples(audio_samples);
this.audio_format = org.vocvark.jAudioTools.AudioMethods.getConvertedAudioFormat(audio_format);
if (normalize_if_clipped)
normalizeIfClipped();
}
/**
* Store the given samples with the associated sampling rate.
* A default AudioFormat is generated.
*
* @param audio_samples Audio samles to store, with a minimum value of
* -1 and a maximum value of +1. The first indice
* corresponds to the channel and the second indice
* corresponds to the sample number.
* @param sampling_rate The sampling rate to associate with the given
* samples.
* @param unique_identifier The string that will be used by external
* objects to uniquely identify the instantiated
* AudioSamples object.
* @param normalize_if_clipped If set to true, then normalizes audio so the
* absolute value of the highest amplitude sample is
* 1. Does this if and only if one or more of the samples
* is outside the allowable range of sample values
* (-1 to 1). If set to false, then does not
* normalize, regardless of sample values.
* @throws Exception Throws an informative exception if the samples
* cannot be extracted from the audio_samples.
*/
public AudioSamples(double[][] audio_samples,
float sampling_rate,
String unique_identifier,
boolean normalize_if_clipped)
throws Exception {
if (audio_samples == null)
throw new Exception("Given audio samples array is empty.");
for (int chan = 0; chan < audio_samples.length; chan++)
if (audio_samples[chan] == null)
throw new Exception("One or more channels of given audio samples array is empty.");
int number_samples = audio_samples[0].length;
for (int chan = 0; chan < audio_samples.length; chan++)
if (audio_samples[chan].length != number_samples)
throw new Exception("Different channels of given audio samples array have a\n" +
"different number of samples.");
unique_ID = unique_identifier;
samples = DSPMethods.getSamplesMixedDownIntoOneChannel(audio_samples);
if (audio_samples.length == 1)
channel_samples = null;
else
channel_samples = DSPMethods.getCopyOfSamples(audio_samples);
audio_format = getDefaultAudioFormat(sampling_rate);
if (normalize_if_clipped)
normalizeIfClipped();
}
/* PUBLIC METHODS ***********************************************************/
/**
* Returns a copy of this AudioSamples object. All fields are copies, not
* references to the original fields, so no changes made to the copies
* will change the original
*
* @return A copy of this object.
* @throws Exception Throws an informative exception if the copy cannot
* be made.
*/
public AudioSamples getCopyOfAudioSamples()
throws Exception {
String new_unique_ID = null;
if (new_unique_ID != null)
new_unique_ID = new String(unique_ID);
double[][] new_channel_samples = null;
if (channel_samples != null) {
new_channel_samples = new double[channel_samples.length][];
for (int i = 0; i < new_channel_samples.length; i++) {
new_channel_samples[i] = new double[channel_samples[i].length];
for (int j = 0; j < new_channel_samples[i].length; j++)
new_channel_samples[i][j] = channel_samples[i][j];
}
} else {
new_channel_samples = new double[1][samples.length];
for (int i = 0; i < samples.length; i++)
new_channel_samples[0][i] = samples[i];
}
AudioFormat new_audio_format = null;
if (audio_format != null) {
new_audio_format = new AudioFormat(audio_format.getEncoding(),
audio_format.getSampleRate(),
audio_format.getSampleSizeInBits(),
audio_format.getChannels(),
audio_format.getFrameSize(),
audio_format.getFrameRate(),
audio_format.isBigEndian());
}
return new AudioSamples(new_channel_samples, new_audio_format, new_unique_ID, false);
}
/**
* Returns a formatted description of the AudioFormat of the samples
* as well as the number of samples per channel, the duration in
* seconds of the recording and the maximum sample amplitude.
*
* @return A formatted description of the recording.
*/
public String getRecordingInfo() {
String return_string = org.vocvark.jAudioTools.AudioMethods.getAudioFormatData(audio_format);
String number_samples = getNumberSamplesPerChannel() + " samples\n";
String duration = getDuration() + " seconds\n";
String max_sample_value = getMaximumAmplitude() + "\n";
return_string += new String("SAMPLES PER CHANNEL: " + number_samples);
return_string += new String("DURATION: " + duration);
return_string += new String("MAX SIGNAL AMPLITUDE: " + max_sample_value);
return return_string;
}
/**
* Returns the identifier assigned to an object of this class at instantiation.
*
* @return The identifier of this object.
*/
public String getUniqueIdentifier() {
return unique_ID;
}
/**
* Returns the AudioFormat associated with the stored samples.
*
* @return The AudioFormat associated with the stored samples.
*/
public AudioFormat getAudioFormat() {
return audio_format;
}
/**
* Returns the sampling rate that is associated with the stored samples.
*
* @return The sampling rate associated with the stored samples in the
* form of a float.
*/
public float getSamplingRate() {
return audio_format.getSampleRate();
}
/**
* Returns the sampling rate that is associated with the stored samples.
*
* @return The sampling rate associated with the stored samples in the
* form of a double.
*/
public double getSamplingRateAsDouble() {
return (new Float(audio_format.getSampleRate())).doubleValue();
}
/**
* Returns the total number of samples per channel in the stored audio.
*
* @return The total number of samples per channel in the stored audio.
*/
public int getNumberSamplesPerChannel() {
return samples.length;
}
/**
* Returns the total length of the stored audio in seconds.
*
* @return The length of the stored audio in seconds.
*/
public double getDuration() {
return convertSampleIndexToTime(samples.length - 1);
}
/**
* Returns the number of channels of stored audio.
*
* @return The number of channels of stored audio.
*/
public int getNumberChannels() {
if (channel_samples == null)
return 1;
else
return channel_samples.length;
}
/**
* Returns the stored audio samples. If the audio data originally consisted
* of multiple channels, then the returned samples represent the audio
* after mixing down into a single channel.
*
* @return The audio samples stored in this object. These have a minimum
* value of -1 and a maximum value of +1.
*/
public double[] getSamplesMixedDown() {
return samples;
}
/**
* Returns the stored audio samples between the given sample indices.
* If the audio data originally consisted of multiple channels, then
* the returned samples represent the audio after mixing down into a
* single channel.
*
* @param start_sample The sample indice of the first sample to return.
* Must be 0 or higher and must be less than
* end_sample.
* @param end_sample The sample indice of the last sample to return.
* Must be less than the total number of samples.
* @return The audio samples stored in this object. These
* have a minimum value of -1 and a maximum value
* of +1. Only the samples within (and including)
* the given sample indices are returned. Only one
* (potentially combined) channel of audio is
* returned.
* @throws Exception Throws an informative exception if the given
* start sample or end sample are outside acceptable
* ranges.
*/
public double[] getSamplesMixedDown(int start_sample, int end_sample)
throws Exception {
if (start_sample < 0)
throw new Exception("Requested audio starting at sample " + start_sample +
"\nStart sample indice must be 0 or greater.");
if (end_sample >= samples.length)
throw new Exception("Requested audio ending at sample " + end_sample +
"\nA total of " + samples.length + "samples are present." +
"\nRequested ending sample indice must be less than this.");
if (start_sample >= end_sample)
throw new Exception("Requested audio starting at sample " + start_sample +
" and ending at sample " + end_sample + ".\n" +
"Requested start sample indice must be less than requested" +
"\nend sample indice.");
int number_samples = end_sample - start_sample;
double[] sample_segment = new double[number_samples];
for (int samp = start_sample; samp <= end_sample; samp++)
sample_segment[samp - start_sample] = samples[samp];
return sample_segment;
}
/**
* Returns the stored audio samples between the given times.
* If the audio data originally consisted of multiple channels, then
* the returned samples represent the audio after mixing down into a
* single channel.
*
* @param start_time The time, in seconds, of the first sample to
* return. Must be 0 or higher and must be less than
* end_time.
* @param end_time The time, in seconds, of the last sample to return.
* return. Must be less than or equal to the total
* duration.
* @return The audio samples stored in this object. These
* have a minimum value of -1 and a maximum value
* of +1. Only the samples within (and including)
* the given times are returned. Only one (potentially
* combined) channel of audio is returned.
* @throws Exception Throws an informative exception if the given
* times are outside acceptable ranges.
*/
public double[] getSamplesMixedDown(double start_time, double end_time)
throws Exception {
int start_sample = convertTimeToSampleIndex(start_time);
int end_sample = convertTimeToSampleIndex(end_time);
return getSamplesMixedDown(start_sample, end_sample);
}
/**
* Returns the stored audio samples divided into windows of equal lengths.
* The last window is extended past the length of the stored samples if
* necessary and is zero padded. If the audio data originally consisted of
* multiple channels, then the returned samples represent the audio after
* mixing down into a single channel.
*
* @param window_size The length in samples of the windows that the
* samples are to divided into.
* @return The audio samples stored in this object after
* being broken into equally sized windows of the
* specified size. The last window is zero-padded
* if necessary. The samples have a minimum value
* of -1 and a maximum value of +1. Only one
* (potentially combined) channel of audio is
* returned. The first indice specifies the window
* and the second indice specifies the sample index
* within the window.
* @throws Exception Throws an exception if a negative or 0 window size
* is specified.
*/
public double[][] getSampleWindowsMixedDown(int window_size)
throws Exception {
if (window_size < 1)
throw new Exception("Window size of " + window_size + " specified.\n" +
"This value must be above 0.");
int number_windows = samples.length / window_size;
if (samples.length % window_size != 0)
number_windows++;
double[][] windowed_samples = new double[number_windows][window_size];
for (int win = 0; win < number_windows; win++) {
if (win != number_windows - 1) {
for (int samp = 0; samp < window_size; samp++)
windowed_samples[win][samp] = samples[win * window_size + samp];
} else {
for (int samp = 0; samp < window_size; samp++) {
if (win * window_size + samp < samples.length)
windowed_samples[win][samp] = samples[win * window_size + samp];
else
windowed_samples[win][samp] = 0;
}
}
}
return windowed_samples;
}
/**
* Returns the stored audio samples divided into windows of equal lengths.
* The last window is extended past the length of the stored samples if
* necessary and is zero padded. If the audio data originally consisted of
* multiple channels, then the returned samples represent the audio after
* mixing down into a single channel.
*
* @param window_duration The duration in seconds of the windows that the
* samples are divided into.
* @return The audio samples stored in this object after
* being broken into equally sized windows of the
* specified size. The last window is zero-padded
* if necessary. The samples have a minimum value
* of -1 and a maximum value of +1. Only one
* (potentially combined) channel of audio is
* returned. The first indice specifies the window
* and the second indice specifies the sample index
* within the window.
* @throws Exception Throws an exception if a negative or 0 window size
* is specified.
*/
public double[][] getSampleWindowsMixedDown(double window_duration)
throws Exception {
int window_size = convertTimeToSampleIndex(window_duration);
return getSampleWindowsMixedDown(window_size);
}
/**
* Returns the stored audio samples in the form of an AudioInputStream.
* If the audio data originally consisted of multiple channels, then the
* returned samples represent the audio after mixing down into a single
* channel.
*
* @return The entire set of samples mixed down into one channel.
* Returned in the form of an AudioInputStream.
* @throws Exception Throws an exception if a a problem occurs
* during conversion.
*/
public AudioInputStream getAudioInputStreamMixedDown()
throws Exception {
// Specify that only one channel is used
AudioFormat mixed_down_audio_format = new AudioFormat(audio_format.getSampleRate(),
audio_format.getSampleSizeInBits(),
1,
true,
audio_format.isBigEndian());
// Convert samples to 2-D array
double[][] samples_to_convert = new double[1][];
samples_to_convert[0] = samples;
// Convert to an AudioInputStream
AudioInputStream audio_input_stream
= org.vocvark.jAudioTools.AudioMethods.convertToAudioInputStream(samples_to_convert, mixed_down_audio_format);
// Return the resuls
return audio_input_stream;
}
/**
* Returns the stored audio samples between the given samples in the form of an
* AudioInputStream. If the audio data originally consisted of multiple channels,
* then the returned samples represent the audio after mixing down into a single
* channel.
*
* @param start_sample The sample indice of the first sample to return.
* Must be 0 or higher and must be less than
* end_sample.
* @param end_sample The sample indice of the last sample to return.
* Must be less than the total number of samples.
* @return The specified set of samples mixed down into one channel.
* Returned in the form of an AudioInputStream.
* @throws Exception Throws an informative exception if the given
* start sample or end sample are outside acceptable
* ranges or if error occurs during conversion.
*/
public AudioInputStream getAudioInputStreamMixedDown(int start_sample, int end_sample)
throws Exception {
// Specify that only one channel is used
AudioFormat mixed_down_audio_format = new AudioFormat(audio_format.getSampleRate(),
audio_format.getSampleSizeInBits(),
1,
true,
audio_format.isBigEndian());
// Find the particular samples to convert
double[] sample_portion = getSamplesMixedDown(start_sample, end_sample);
// Convert the samples to 2-D array
double[][] samples_to_convert = new double[1][];
samples_to_convert[0] = sample_portion;
// Convert to an AudioInputStream
AudioInputStream audio_input_stream
= org.vocvark.jAudioTools.AudioMethods.convertToAudioInputStream(samples_to_convert, mixed_down_audio_format);
// Return the resuls
return audio_input_stream;
}
/**
* Returns the stored audio samples between the given times in the form of an
* AudioInputStream. If the audio data originally consisted of multiple channels,
* then the returned samples represent the audio after mixing down into a single
* channel.
*
* @param start_time The time, in seconds, of the first sample to
* return. Must be 0 or higher and must be less than
* end_time.
* @param end_time The time, in seconds, of the last sample to return.
* return. Must be less than or equal to the total
* duration.
* @return The specified set of samples mixed down into one channel.
* Returned in the form of an AudioInputStream.
* @throws Exception Throws an informative exception if the given
* start time or end time are outside acceptable
* ranges or if error occurs during conversion.
*/
public AudioInputStream getAudioInputStreamMixedDown(double start_time, double end_time)
throws Exception {
// Specify that only one channel is used
AudioFormat mixed_down_audio_format = new AudioFormat(audio_format.getSampleRate(),
audio_format.getSampleSizeInBits(),
1,
true,
audio_format.isBigEndian());
// Find the particular samples to convert
double[] sample_portion = getSamplesMixedDown(start_time, end_time);
// Convert the samples to 2-D array
double[][] samples_to_convert = new double[1][];
samples_to_convert[0] = sample_portion;
// Convert to an AudioInputStream
AudioInputStream audio_input_stream
= org.vocvark.jAudioTools.AudioMethods.convertToAudioInputStream(samples_to_convert, mixed_down_audio_format);
// Return the resuls
return audio_input_stream;
}
/**
* Returns the stored audio samples.
*
* @return The audio samples stored in this object. These have a minimum
* value of -1 and a maximum value of +1. The first indice corresponds
* to the channel and the second indice corresponds to the sample
* number.
*/
public double[][] getSamplesChannelSegregated() {
if (channel_samples == null) {
double[][] formatted_samples = new double[1][];
formatted_samples[0] = samples;
return formatted_samples;
} else
return channel_samples;
}
/**
* Returns the stored audio samples between the given sample indices.
*
* @param start_sample The sample indice of the first sample to return.
* Must be 0 or higher and must be less than
* end_sample.
* @param end_sample The sample indice of the last sample to return.
* Must be less than the total number of samples.
* @return The audio samples stored in this object. These
* have a minimum value of -1 and a maximum value
* of +1. The first indice corresponds to the channel
* and the second indice corresponds to the sample
* number.
* @throws Exception Throws an informative exception if the given
* start sample or end sample are outside acceptable
* ranges.
*/
public double[][] getSamplesChannelSegregated(int start_sample, int end_sample)
throws Exception {
if (start_sample < 0)
throw new Exception("Requested audio starting at sample " + start_sample +
"\nStart sample indice must be 0 or greater.");
if (end_sample >= samples.length)
throw new Exception("Requested audio ending at sample " + end_sample +
"\nA total of " + samples.length + "samples are present." +
"\nRequested ending sample indice must be less than this.");
if (start_sample >= end_sample)
throw new Exception("Requested audio starting at sample " + start_sample +
" and ending at sample " + end_sample + ".\n" +
"Requested start sample indice must be less than requested" +
"\nend sample indice.");
int number_samples = end_sample - start_sample + 1;
// Case where only one channel of audio is present
if (channel_samples == null) {
double[][] sample_segment = new double[1][number_samples];
for (int samp = start_sample; samp <= end_sample; samp++)
sample_segment[0][samp - start_sample] = samples[samp];
return sample_segment;
}
// Case where multiple channels of audio are present
else {
double[][] sample_segment = new double[channel_samples.length][number_samples];
for (int chan = 0; chan < channel_samples.length; chan++)
for (int samp = start_sample; samp <= end_sample; samp++)
sample_segment[chan][samp - start_sample] = channel_samples[chan][samp];
return sample_segment;
}
}
/**
* Returns the stored audio samples between the given times in secondes.
*
* @param start_time The time, in seconds, of the first sample to
* return. Must be 0 or higher and must be less than
* end_time.
* @param end_time The time, in seconds, of the last sample to return.
* return. Must be less than or equal to the total
* duration.
* @return The audio samples stored in this object. These
* have a minimum value of -1 and a maximum value
* of +1. The first indice corresponds to the channel
* and the second indice corresponds to the sample
* number. Only the samples within (and including)
* the given times are returned.
* @throws Exception Throws an informative exception if the given
* times are outside acceptable ranges.
*/
public double[][] getSamplesChannelSegregated(double start_time, double end_time)
throws Exception {
int start_sample = convertTimeToSampleIndex(start_time);
int end_sample = convertTimeToSampleIndex(end_time);
return getSamplesChannelSegregated(start_sample, end_sample);
}
/**
* Returns the stored audio samples divided into windows of equal lengths.
* The last window is extended past the length of the stored samples if
* necessary and is zero padded.
*
* @param window_size The length in samples of the windows that the
* samples are to divided into.
* @return The audio samples stored in this object after
* being broken into equally sized windows of the
* specified size. The last window is zero-padded
* if necessary. The samples have a minimum value
* of -1 and a maximum value of +1. The first indice
* specifies the channel, the second indice specifies
* the window and the third indice specifies the
* sample index within the window.
* @throws Exception Throws an exception if a negative or 0 window size
* is specified.
*/
public double[][][] getSampleWindowsChannelSegregated(int window_size)
throws Exception {
if (channel_samples == null) {
double[][][] windowed_samples = new double[1][][];
windowed_samples[0] = getSampleWindowsMixedDown(window_size);
return windowed_samples;
}
if (window_size < 1)
throw new Exception("Window size of " + window_size + " specified.\n" +
"This value must be above 0.");
int number_windows = samples.length / window_size;
if (samples.length % window_size != 0)
number_windows++;
double[][][] windowed_samples = new double[channel_samples.length][number_windows][window_size];
for (int chan = 0; chan < channel_samples.length; chan++)
for (int win = 0; win < number_windows; win++) {
if (win != number_windows - 1) {
for (int samp = 0; samp < window_size; samp++)
windowed_samples[chan][win][samp] = channel_samples[chan][win * window_size + samp];
} else {
for (int samp = 0; samp < window_size; samp++) {
if (win * window_size + samp < samples.length)
windowed_samples[chan][win][samp] = channel_samples[chan][win * window_size + samp];
else
windowed_samples[chan][win][samp] = 0;
}
}
}
return windowed_samples;
}
/**
* Returns the stored audio samples divided into windows of equal lengths.
* The last window is extended past the length of the stored samples if
* necessary and is zero padded.
*
* @param window_duration The duration in seconds of the windows that the
* samples are divided into.
* @return The audio samples stored in this object after
* being broken into equally sized windows of the
* specified size. The last window is zero-padded
* if necessary. The samples have a minimum value
* of -1 and a maximum value of +1. The first indice
* specifies the channel, the second indice specifies
* the window and the third indice specifies the
* sample index within the window.
* @throws Exception Throws an exception if a negative or 0 window size
* is specified.
*/
public double[][][] getSampleWindowsChannelSegregated(double window_duration)
throws Exception {
int window_size = convertTimeToSampleIndex(window_duration);
return getSampleWindowsChannelSegregated(window_size);
}
/**
* Returns the stored audio samples in the form of an AudioInputStream.
*
* @return The entire set of samples returned in the
* form of an AudioInputStream.
* @throws Exception Throws an exception if a a problem occurs
* during conversion.
*/
public AudioInputStream getAudioInputStreamChannelSegregated()
throws Exception {
// Extract samples from samples field if only one channel present
double[][] samples_to_convert = getSamplesChannelSegregated();
// Convert to an AudioInputStream
AudioInputStream audio_input_stream
= org.vocvark.jAudioTools.AudioMethods.convertToAudioInputStream(samples_to_convert, audio_format);
// Return the resuls
return audio_input_stream;
}
/**
* Returns the stored audio samples between the given samples in the form of
* an AudioInputStream.
*
* @param start_sample The sample indice of the first sample to return.
* Must be 0 or higher and must be less than
* end_sample.
* @param end_sample The sample indice of the last sample to return.
* Must be less than the total number of samples.
* @return The specified set of samples returned in the form of an
* of an AudioInputStream.
* @throws Exception Throws an informative exception if the given
* start sample or end sample are outside acceptable
* ranges or if error occurs during conversion.
*/
public AudioInputStream getAudioInputStreamChannelSegregated(int start_sample, int end_sample)
throws Exception {
// Extract samples from samples field if only one channel present
double[][] samples_to_convert = getSamplesChannelSegregated(start_sample, end_sample);
// Convert to an AudioInputStream
AudioInputStream audio_input_stream
= org.vocvark.jAudioTools.AudioMethods.convertToAudioInputStream(samples_to_convert, audio_format);
// Return the resuls
return audio_input_stream;
}
/**
* Returns the stored audio samples between the given times in the form of
* an AudioInputStream.
*
* @param start_time The time, in seconds, of the first sample to
* return. Must be 0 or higher and must be less than
* end_time.
* @param end_time The time, in seconds, of the last sample to return.
* return. Must be less than or equal to the total
* duration.
* @return The specified set of samples returned in the form of an
* of an AudioInputStream.
* @throws Exception Throws an informative exception if the given
* start time or end time are outside acceptable
* ranges or if error occurs during conversion.
*/
public AudioInputStream getAudioInputStreamChannelSegregated(double start_time, double end_time)
throws Exception {
// Extract samples from samples field if only one channel present
double[][] samples_to_convert = getSamplesChannelSegregated(start_time, end_time);
// Convert to an AudioInputStream
AudioInputStream audio_input_stream
= org.vocvark.jAudioTools.AudioMethods.convertToAudioInputStream(samples_to_convert, audio_format);
// Return the resuls
return audio_input_stream;
}
/**
* Saves the currently stored samples to the specified file.
* <p/>
* <p><b>WARNING:</b> Will automatically overwrite given file if it already exists.
*
* @param save_file The File to save the audio samples to.
* @param multi_channel If this is true, then any separate channels are
* saved on separate channels. If this is false,
* then saved file has only one channel, onto which
* all samples have been mixed down to.
* @param save_file_type The AudioFileFormat.Type to use for saving the audio.
* A default value is used if this is null.
* @param normalize_if_clipped If set to true, then normalizes audio so the
* absolute value of the highest amplitude sample is 1.
* Does this if and only if one or more of the samples to
* save is outside the allowable range of sample values
* (-1 to 1). If set to false, then does not
* normalize, regardless of sample values. <b>WARNING:</b>
* This will normalize the samples stored in memory as well.
* @throws Exception Throws an informative exception if the samples cannot
* be succesfully saved to the file.
*/
public void saveAudio(File save_file,
boolean multi_channel,
AudioFileFormat.Type save_file_type,
boolean normalize_if_clipped)
throws Exception {
// Verify that a file is specified
if (save_file == null)
throw new Exception("No file provided to save to.");
// Normalize the file if requested and if necessary
if (normalize_if_clipped)
normalizeIfClipped();
// Convert samples to an AudioInputStream
AudioInputStream audio_input_stream = null;
if (multi_channel)
audio_input_stream = getAudioInputStreamChannelSegregated();
else
audio_input_stream = getAudioInputStreamMixedDown();
// Save as a wav file if file type not specified
if (save_file_type == null)
save_file_type = AudioFileFormat.Type.WAVE;
// Delete any pre-existing file
if (save_file.exists())
save_file.delete();
// Write the samples to the file
AudioSystem.write(audio_input_stream, save_file_type, save_file);
}
/**
* Checks the mixed down and channel segregated samples separately to see if
* either one has values outside of the permitted range (-1 to +1). If so,
* normalizes the appropriate samples so that the absolute value of the
* highest amplitude sample is 1.
*/
public void normalizeIfClipped() {
if (checkMixedDownSamplesForClipping() > 0.0)
normalizeMixedDownSamples();
if (checkChannelSegregatedSamplesForClipping() > 0.0)
normalizeChannelSegretatedSamples();
}
/**
* Returns the maximum amplitude value in all of the channels.
*
* @return The maximum amplitude (absolute) value in any one channel.
*/
public double getMaximumAmplitude() {
double max_amplitude = 0.0;
if (channel_samples != null) {
for (int chan = 0; chan < channel_samples.length; chan++)
for (int samp = 0; samp < channel_samples[chan].length; samp++)
if (Math.abs(channel_samples[chan][samp]) > max_amplitude)
max_amplitude = Math.abs(channel_samples[chan][samp]);
} else {
for (int samp = 0; samp < samples.length; samp++)
if (Math.abs(samples[samp]) > max_amplitude)
max_amplitude = Math.abs(samples[samp]);
}
return max_amplitude;
}
/**
* Returns the maximum deviation in the stored mixed down sample values outside
* the permissible range of -1 to +1. Returns -1.0 if all samples fall withing
* the permissible range.
*
* @return The maximum deviation from the permissible sample values. -1 if
* all sample values fall within the allowable range.
*/
public double checkMixedDownSamplesForClipping() {
double max_difference = -1.0;
for (int samp = 0; samp < samples.length; samp++)
if (Math.abs(samples[samp]) > 1.0) {
double difference = Math.abs(samples[samp]) - 1.0;
if (difference > max_difference)
max_difference = difference;
}
return max_difference;
}
/**
* Returns the maximum deviation in the stored sample values outside the
* permissible range of -1 to +1. Returns -1.0 if all samples fall withing
* the permissible range. Does this dependantly over all channels, but not
* over the mixed down channels (unless only one channel is present).
*
* @return The maximum deviation from the permissible sample values. -1 if
* all sample values fall within the allowable range.
*/
public double checkChannelSegregatedSamplesForClipping() {
double max_difference = -1.0;
if (channel_samples != null) {
for (int chan = 0; chan < channel_samples.length; chan++)
for (int samp = 0; samp < channel_samples[chan].length; samp++)
if (Math.abs(channel_samples[chan][samp]) > 1.0) {
double difference = Math.abs(channel_samples[chan][samp]) - 1.0;
if (difference > max_difference)
max_difference = difference;
}
} else
max_difference = checkMixedDownSamplesForClipping();
return max_difference;
}
/**
* Normalizes the samples mixed down into one channel so that the absolute value
* of the highest sample amplitude is 1. Does nothing if all samples are 0.
*/
public void normalizeMixedDownSamples() {
samples = DSPMethods.normalizeSamples(samples);
}
/**
* Normalizes the channel segregated samples so that the absolute value
* of the highest sample amplitude is 1. Does nothing if all samples are 0.
*/
public void normalizeChannelSegretatedSamples() {
if (channel_samples != null)
channel_samples = DSPMethods.normalizeSamples(channel_samples);
else
normalizeMixedDownSamples();
}
/**
* Normalizes both the channel segregated samples and the samples mixed
* down into one channel so that the absolute value of the highest sample
* amplitude is 1. Does nothing if all samples are 0.
*/
public void normalize() {
normalizeChannelSegretatedSamples();
if (channel_samples != null)
normalizeMixedDownSamples();
}
/**
* Updates the samples stored in an object of this class. The given new
* samples must have the same number of channels as the original data.
* The stored samples are a copy of the given samples, so changes to
* the passed array will not affect the information stored here.
*
* @param new_samples Audio samles to store, usually with a minimum value
* of -1 and a maximum value of +1. The first indice
* corresponds to the channel and the second indice
* corresponds to the sample number.
* @throws Exception Throws an exception if an invalid parameter is
* passed (null entries, non-matching number of channels
* or samples per channel.
*/
public void setSamples(double[][] new_samples)
throws Exception {
// Verify that the given samples are valid
if (new_samples == null)
throw new Exception("An empty set of samples provided.");
int number_samples = -1;
for (int chan = 0; chan < new_samples.length; chan++) {
if (new_samples[chan] == null)
throw new Exception("Channel " + chan + " of the given samples is empty.");
if (number_samples != -1)
if (number_samples != new_samples[chan].length)
throw new Exception("Different channels have different numbers of samples.");
number_samples = new_samples[chan].length;
}
// Update the samples and channel_samples fields
if (channel_samples == null) {
if (new_samples.length != 1) {
throw new Exception("Given samples have " + new_samples.length + " channels.\n" +
"Only one channel should be present.");
}
samples = new double[number_samples];
for (int samp = 0; samp < samples.length; samp++)
samples[samp] = new_samples[0][samp];
} else {
if (new_samples.length != channel_samples.length) {
throw new Exception("Given samples have " + new_samples.length + " channels.\n" +
channel_samples.length + " channel should be present.");
}
channel_samples = new double[new_samples.length][number_samples];
for (int chan = 0; chan < channel_samples.length; chan++)
for (int samp = 0; samp < channel_samples[chan].length; samp++)
channel_samples[chan][samp] = new_samples[chan][samp];
samples = DSPMethods.getSamplesMixedDownIntoOneChannel(channel_samples);
}
}
/* PRIVATE METHODS *********************************************************/
/**
* Returns a new <code>AudioFormat</code> with the given sampling rate. The
* number of channels is automatically set based on the channel_samples field.
* <p/>
* Big-endian signed linear PCM encoding is used by default, and the default
* bit-depth is set to 16 bits.
*
* @param sampling_rate The sampling rate, in Hz, to use for interpereting
* samples.
* @return The default AudioFormat with the specified sampling
* rate.
*/
private AudioFormat getDefaultAudioFormat(float sampling_rate) {
int bit_depth = 16;
boolean signed = true;
boolean big_endian = true;
int channels = 1;
if (channel_samples == null)
channels = channel_samples.length;
return new AudioFormat(sampling_rate, bit_depth, channels, signed, big_endian);
}
/**
* Returns the given sample index converted into a time in seconds based
* on the sampling rate of the stored audio samples. If the given sample
* index is negative, then time 0 is returned. If the given sample index
* is greate thean the length of the audio, then the time of the last index
* is returned.
*
* @param sample_index The sample index to convert to a time value.
* @return The time in seconds corresponding to the given
* sample index, or the nearest time if the
* given sample index is outside the duration
* of the audio.
*/
private double convertSampleIndexToTime(int sample_index) {
if (sample_index < 0)
sample_index = 0;
else if (sample_index >= samples.length)
sample_index = samples.length - 1;
float time = samples.length / audio_format.getSampleRate();
return (new Float(time)).doubleValue();
}
/**
* Returns the given time in seconds converted into a sample index based
* on the sampling rate of the stored audio samples. If the given time
* would correspond to a negative sample, then the index of the first
* sample is returned. If the given time would correspond to a sample
* greater than the length of the audio, then the last index is returned.
*
* @param time The time in seconds to convert to a sample.
* @return The sample corresponding to the given time, or the
* nearest sample if the given time is outside the duration
* of the audio.
*/
private int convertTimeToSampleIndex(double time) {
int sample_index = (int) (time * audio_format.getSampleRate());
if (sample_index < 0)
return 0;
else if (sample_index >= samples.length)
return samples.length - 1;
return sample_index;
}
}