package apes.models;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.util.Observable;
import apes.plugins.WaveFileFormat;
/**
* Describes the audio in a format suitable for internal representation in the
* program.
*
* @author Daniel Kvick (kvick@student.chalmers.se)
*/
public class InternalFormat extends Observable
{
/**
* Information about where the file were saved to or loaded from.
*/
private FileStatus fileStatus;
/**
* Information of all sound file tags.
*/
private Tags tags;
/**
* Amount of samples per second.
*/
private int sampleRate;
/**
* Amount of channels in file
*/
private int channels;
/**
* Memory handler taking care of swapping
*/
private MemoryHandler memoryHandler;
public final int bitsPerSample;
public final int bytesPerSample;
/**
* Length of each channel in samples.
*/
private int sampleAmount;
/**
* Constructor setting up the Internal Format according to the supplied data.
*
* @param tags Tag information of the audio file.
* @param samplerate Amount of samples per second.
* @param numChannels Number of channels.
*/
public InternalFormat(Tags tags, int samplerate, int numChannels, int bitsPerSample)
{
if(tags == null)
{
this.tags = new Tags();
}
else
{
this.tags = tags;
}
this.bitsPerSample = bitsPerSample;
this.bytesPerSample = bitsPerSample / 8;
this.sampleRate = samplerate;
channels = numChannels;
memoryHandler = new MemoryHandler();
sampleAmount = 0;
}
/**
* The tag object describing all the tags of the audio file.
*
* @return All tags of the audio file as a Tag object.
*/
public Tags getTags()
{
return tags;
}
/**
* Sets the tags object for this IF.
*
* @param tags The tags object.
*/
public void setTags(Tags tags)
{
this.tags = tags;
}
/**
* Returns the number of channels.
*
* @return The number of channels.
*/
public int getNumChannels()
{
return channels;
}
/**
* Returns the number of samples in each channel.
*
* @return <code>sampleAmount</code>.
*/
public int getSampleAmount()
{
return sampleAmount;
}
/**
* Returns the sample rate, that is, how many samples make up one second.
*
* @return Returns the sample rate.
*/
public int getSampleRate()
{
return sampleRate;
}
/**
* Returns a chunk of data in PCM format. The chunk contains
* <code>amount</code> samples from each channel starting from absolute index
* <code>index</code>.
*
* @param indexS The first sample to include in the chunk, as an absolute
* index.
* @param amountS Amount of samples in the chunk. If there are fewer than
* <code>amount</code> samples after <code>index</code>, all
* remaining samples are included.
* @return A byte array containing the requested data.
*/
public byte[] getChunk(long indexS, int amountS)
{
if(indexS + amountS > sampleAmount || indexS < 0 || amountS < 1)
return null;
try
{
return memoryHandler.read(samplesToBytes(indexS), (int)samplesToBytes(amountS));
}
catch(IOException e)
{
e.printStackTrace();
System.exit(1);
}
return null;
}
/**
* Returns an approximate average of the specified interval.
*
* @param channel What channel to perform average on.
* @param startS First sample to consider.
* @param lengthS Amount of samples to consider.
* @return
*/
public int getAverageAmplitude(int channel, int startS, int lengthS)
{
if(startS < 0 || channel >= channels || lengthS < 1 || startS + lengthS > sampleAmount)
return 0;
int c = 0;
int total = 0;
int step = lengthS <= 50 ? 1 : Math.round(lengthS * 0.1f);
final int IO_SIZE = 100000; // Amount in samples
ByteBuffer buffer;
int nToRead = IO_SIZE;
for(int iS = startS; iS < startS + lengthS; iS++)
{
if(iS + nToRead > startS + lengthS)
nToRead = startS + lengthS - iS;
buffer = ByteBuffer.wrap(getSamples(iS, iS + nToRead));
for(int i = 0; i < nToRead; i += step, c++)
{
switch (bytesPerSample)
{
case 2:
total += buffer.getShort((int)(samplesToBytes(i) + channel * bytesPerSample));
break;
case 4:
total += buffer.getInt((int)(samplesToBytes(i) + channel * bytesPerSample));
break;
default:
System.out.println("BAD BYTES PER SAMPLE IN INTERNAL FORMAT WHILE AVERAGING");
System.exit(1);
}
}
iS += nToRead;
}
return Math.round((float)total / c);
}
/**
* Saves the internal format to the specifed location with the specified name.
* TODO: add error handling, or some sort of response
*
* @param filePath The location the file should be saved to.
* @param fileName The name of the file to be stored.
*/
public void saveAs(String filePath, String fileName) throws IOException
{
WaveFileFormat wav = new WaveFileFormat();
wav.exportFile(this, filePath, fileName);
fileStatus.setFileName(fileName);
fileStatus.setFilePath(filePath);
fileStatus.setOpenedByInternal();
}
/**
* Save file.
* TODO: add error handling or some sort of response
*/
public void save() throws IOException
{
WaveFileFormat wav = new WaveFileFormat();
wav.exportFile(this, fileStatus.getFilepath(), fileStatus.getFileName());
fileStatus.setOpenedByInternal();
}
/**
* Closes all streams and cleans up the <code>InternalFormat</code>
*/
public void close()
{
memoryHandler.dispose();
this.channels = 0;
}
/**
* load an internal format
*
* @param filePath
* @param fileName
* @throws IOException
*/
public static InternalFormat load(String filePath, String fileName) throws IOException
{
WaveFileFormat wav = new WaveFileFormat();
InternalFormat internalFormat = wav.importFile(filePath, fileName);
internalFormat.fileStatus.setOpenedByInternal();
return internalFormat;
}
/**
* Get the <code>FileStatus</code>.
*
* @return The <code>FileStatus</code>.
*/
public FileStatus getFileStatus()
{
return fileStatus;
}
/**
* Set a <code>FileStatus</code>.
*
* @param fileStatus The new FileStatus.
*/
public void setFileStatus(FileStatus fileStatus)
{
this.fileStatus = fileStatus;
}
/**
* Returns a sample.
*
* @param channel
* @param indexS
* @return
*/
public int getSample(int channel, int indexS)
{
if(channel >= channels || indexS >= sampleAmount || indexS < 0)
return 0;
int amplitude = 0;
byte[] b = null;
try
{
b = memoryHandler.read(samplesToBytes(indexS) + channel * bytesPerSample, bytesPerSample);
}
catch(IOException e)
{
e.printStackTrace();
}
for(int i = 0; i < bytesPerSample; i++)
amplitude += b[i] << (i * 8);
return amplitude;
}
/**
* Returns an array of Samples[] containing the data copied from each channel.
*
* @param startS The first index to copy.
* @param stopS The last index to copy.
* @return All data copied.
*/
// FIXME: How large chunks can we get?
public byte[] getSamples(long startS, long stopS)
{
if(startS < 0 || startS > stopS || stopS >= sampleAmount)
return null;
try
{
return memoryHandler.read(samplesToBytes(startS), (int)samplesToBytes(stopS - startS + 1));
}
catch(IOException e)
{
e.printStackTrace();
return null;
}
}
/**
* Removes all samples
*
* @param startS
* @param stopS
*/
public void removeSamples(long startS, long stopS)
{
if(startS < 0 || startS > stopS || stopS >= sampleAmount)
return;
long lengthS = (stopS - startS + 1);
try
{
if(memoryHandler.free(samplesToBytes(startS), samplesToBytes(lengthS)))
{
sampleAmount -= lengthS;
}
}
catch(IOException e)
{
e.printStackTrace();
}
updated();
}
public void copy(int startS, int stopS, MemoryHandler mH)
{
long startB = samplesToBytes(startS);
long amountB = samplesToBytes(stopS - startS + 1);
mH.transfer(memoryHandler, startB, amountB, 0);
}
/**
* Removes the samples in the specified interval from all channel and returns
* them in a Samples[][] containing a Samples[] for each channel.
*
* @param startS First sample to cut away.
* @param stopS Last sample to cut away.
* @return An array containing arrays of Samples objects of the data removed
* from the channels.
*/
public void cutSamples(long startS, long stopS, MemoryHandler mH)
{
if(startS < 0 || startS > stopS || stopS >= sampleAmount)
return;
long startB = samplesToBytes(startS);
long amountB = samplesToBytes(stopS - startS + 1);
mH.transfer(memoryHandler, startB, amountB, 0L);
removeSamples(startS, stopS);
}
/**
* Sets all samples in the selected range to data taken from
* <code>values</code>
*
* @param startB First index to set as bytes.
* @param values An array of byte values to use for setting.
*/
// INDEX OK !!!
private void setSamples(long startB, byte[] values)
{
if(startB < 0 || startB + values.length > samplesToBytes(sampleAmount))
return;
try
{
memoryHandler.write(startB, values);
}
catch(IOException e)
{
e.printStackTrace();
}
}
/**
* Inserts the provided samples at the specified index.
*
* @param startB Index to insert at in bytes.
* @param samplesB Samples to insert at start.
* @return Index of the first sample after the inserted samples.
*/
public int insertSamples(int startB, byte[] samplesB)
{
if(samplesB == null || startB > samplesToBytes(sampleAmount))
return -1;
boolean alloc = false;
try
{
alloc = memoryHandler.malloc(startB, samplesB.length);
}
catch(IOException e)
{
e.printStackTrace();
}
if(!alloc)
return -1;
sampleAmount += bytesToSamples(samplesB.length);
setSamples(startB, samplesB);
updated();
return startB + samplesB.length;
}
/**
* TODO: Comment
*
* @param startS
* @param m
* @return
*/
public void pasteSamples(long startS, MemoryHandler m)
{
memoryHandler.transfer(m, 0, m.getUsedMemory(), samplesToBytes(startS));
sampleAmount += bytesToSamples(m.getUsedMemory());
updated();
}
/**
* Scales all samples in the internal format.
*
* @param startS The start sample.
* @param stopS The end sample.
* @param alpha The alpha value.
*/
public void scaleSamples(long startS, long stopS, float alpha)
{
final int IO_SIZE = 100000; // Amount in samples
ByteBuffer toWrite = null;
int nToWriteS = IO_SIZE;
if(alpha == 0)
{
toWrite = ByteBuffer.wrap(new byte[(int)samplesToBytes(nToWriteS)]);
toWrite.order(ByteOrder.LITTLE_ENDIAN);
}
for(long iS = startS; iS < stopS; iS++)
{
if((stopS - iS + 1) < IO_SIZE)
{
nToWriteS = (int)(stopS - iS + 1);
if(alpha == 0)
{
toWrite = ByteBuffer.wrap(new byte[(int)samplesToBytes(nToWriteS)]);
toWrite.order(ByteOrder.LITTLE_ENDIAN);
}
}
if(alpha != 0)
{
toWrite = ByteBuffer.wrap(getSamples(iS, iS + nToWriteS));
toWrite.order(ByteOrder.LITTLE_ENDIAN);
for(int index = 0; index < nToWriteS; ++index)
{
for(int C = 0; C < channels; C++)
{
switch (bytesPerSample)
{
case 2:
toWrite.putShort((int)samplesToBytes(index) + C*2, (short)Math.round(toWrite.getShort((int)samplesToBytes(index) + C*2) * alpha));
break;
case 4:
toWrite.putInt((int)samplesToBytes(index) + C*4, Math.round(toWrite.getInt((int)samplesToBytes(index) + C*4) * alpha));
break;
default:
System.out.println("BAD BYTES PER SAMPLE IN INTERNAL FORMAT WHILE SCALING");
System.exit(1);
}
}
}
}
setSamples(samplesToBytes(iS), toWrite.array());
iS += nToWriteS;
}
}
/**
* Notify observers that the internal format has changed.
*/
public void updated()
{
setChanged();
notifyObservers();
}
/**
* Returns <code>samples</code> in bytes.
*
* @param samples Number of samples.
* @return Number of bytes.
*/
public long samplesToBytes(long samples)
{
return samples * bytesPerSample * channels;
}
/**
* Returns <code>bytes</code> in samples.
*
* @param bytes Number of byes.
* @return Number of samples.
*/
public long bytesToSamples(long bytes)
{
return bytes / (bytesPerSample * channels);
}
}