/* RawSignalSampleSource.java created 2008-01-29 * */ package org.signalml.domain.signal.raw; import java.io.File; import java.io.IOException; import java.io.RandomAccessFile; import java.nio.ByteBuffer; import java.nio.DoubleBuffer; import java.nio.FloatBuffer; import java.nio.IntBuffer; import java.nio.ShortBuffer; import java.util.Arrays; import org.apache.log4j.Logger; import org.signalml.domain.signal.samplesource.AbstractMultichannelSampleSource; import org.signalml.domain.signal.samplesource.OriginalMultichannelSampleSource; import org.signalml.exception.SanityCheckException; import org.signalml.plugin.export.SignalMLException; import org.signalml.plugin.export.change.listeners.PluginSignalChangeListener; /** * This class represents the source of samples for the raw signal. * Reads samples from file and uses buffering if possible. * Contains information about the file with the signal, the number of channels, the sampling * frequency, the {@link RawSignalSampleType type of samples} and * the {@link RawSignalByteOrder byte order}. * * @author Michal Dobaczewski © 2007-2008 CC Otwarte Systemy Komputerowe Sp. z o.o. */ public class RawSignalSampleSource extends AbstractMultichannelSampleSource implements OriginalMultichannelSampleSource { protected static final Logger logger = Logger.getLogger(RawSignalSampleSource.class); /** * the file with the signal */ private File file; /** * the file opened for random access reading */ private RandomAccessFile randomAccessFile; /** * the number of signal channels */ private int channelCount; /** * the number of samples per seconds (in one channel) */ private float samplingFrequency; /** * the {@link RawSignalSampleType type} of samples in the signal */ private RawSignalSampleType sampleType; /** * the {@link RawSignalByteOrder order} of bytes in the file with signal */ private RawSignalByteOrder byteOrder; /** * The calibration gain for the signal - the value by which each sample * value is multiplied. */ private float[] calibrationGain; /** * The calibration offset for the signal - the value which is added * to each sample value. */ private float[] calibrationOffset; /** * an array of labels of channels */ private String[] labels; /** * the buffer of read samples */ private byte[] byteBuffer; /** * the index of the first buffered sample */ private int minBufferedSample; /** * the index of the last buffered sample */ private int maxBufferedSample; /** * the wrapper for the buffer of read samples */ private ByteBuffer bBuffer; /** * the size of the sample in bytes */ private int sampleByteWidth; /** * the number of samples in the single channel */ private int sampleCount; /** * The timestamp of the first sample in the signal. */ private double firstSampleTimestamp; /** * Constructor. Creates the source of samples for the multichannel raw * signal based on the file with that signal. * @param file the file with the signal * @param channelCount number of channels in the signal * @param samplingFrequency number of samples per second * @param sampleType the {@link RawSignalSampleType type} of signal * samples in the file * @param byteOrder the {@link RawSignalByteOrder order} of bytes * in the signal file * @throws IOException if there is an error while reading samples from * file */ public RawSignalSampleSource(File file, int channelCount, float samplingFrequency, RawSignalSampleType sampleType, RawSignalByteOrder byteOrder) throws IOException { this.file = file; this.channelCount = channelCount; this.samplingFrequency = samplingFrequency; this.sampleType = sampleType; this.byteOrder = byteOrder; randomAccessFile = new RandomAccessFile(file, "r"); sampleByteWidth = sampleType.getByteWidth(); sampleCount = (int)(file.length() / (channelCount * sampleByteWidth)); } /** * Closes the file with the signal. */ public void close() { if (randomAccessFile != null) { try { randomAccessFile.close(); } catch (IOException ex) { // ignore } finally { randomAccessFile = null; } } } /** * Creates the copy of this sample source. * @return the copy of this sample source. * @throws SignalMLException if there is an error while reading from * file */ @Override public OriginalMultichannelSampleSource duplicate() throws SignalMLException { RawSignalSampleSource newSource; try { newSource = new RawSignalSampleSource(file, channelCount, samplingFrequency, sampleType, byteOrder); } catch (IOException ex) { throw new SignalMLException(ex); } newSource.calibrationGain = Arrays.copyOf(calibrationGain, calibrationGain.length); newSource.calibrationOffset = Arrays.copyOf(calibrationOffset, calibrationOffset.length); if (labels != null) { newSource.labels = Arrays.copyOf(labels, labels.length); } return newSource; } /** * Returns the file with the signal. * @return the file with the signal */ public File getFile() { return file; } /** * Returns the number of samples in the single channel. * @return the number of samples in the single channel */ public int getSampleCount() { return sampleCount; } /** * Returns the {@link RawSignalSampleType type} of the signal sample * @return the type of the signal sample */ public RawSignalSampleType getSampleType() { return sampleType; } /** * Returns the {@link RawSignalByteOrder order} of bytes in the file * with signal * @return the order of bytes in the file with signal */ public RawSignalByteOrder getByteOrder() { return byteOrder; } /** * Returns if the implementation is capable of returning a calibration * @return true because the implementation is capable of returning a * calibration */ @Override public boolean isCalibrationCapable() { return true; } /** * Returns if the implementation is capable of returning a channel count * @return true because the implementation is capable of returning a channel * count */ @Override public boolean isChannelCountCapable() { return true; } /** * Returns if the implementation is capable of returning a * sampling frequency * @return true because the implementation is capable of returning a * sampling frequency */ @Override public boolean isSamplingFrequencyCapable() { return true; } @Override public float[] getCalibrationGain() { return calibrationGain; } @Override public void setCalibrationGain(float[] calibration) { if (!Arrays.equals(this.calibrationGain, calibration)) { float[] oldCalibration = this.calibrationGain; this.calibrationGain = calibration; pcSupport.firePropertyChange(CALIBRATION_PROPERTY, oldCalibration, calibration); } } @Override public int getChannelCount() { return channelCount; } @Override public void setChannelCount(int channelCount) { throw new SanityCheckException("Changing channel count not allowed"); } @Override public float getSamplingFrequency() { return samplingFrequency; } @Override public void setSamplingFrequency(float samplingFrequency) { if (this.samplingFrequency != samplingFrequency) { float oldSamplingFrequency = this.samplingFrequency; this.samplingFrequency = samplingFrequency; pcSupport.firePropertyChange(SAMPLING_FREQUENCY_PROPERTY, oldSamplingFrequency, samplingFrequency); } } /** * Returns an array of labels of channels. * @return an array of labels of channels */ public String[] getLabels() { return labels; } /** * Sets the labels of channels to given values * @param labels an array with labels to be set */ public void setLabels(String[] labels) { if (this.labels != labels) { String[] oldLabels = this.labels; this.labels = labels; pcSupport.firePropertyChange(LABEL_PROPERTY, oldLabels, labels); } } /** * Returns the number of the channel. The same value as given * @param channel the number of a channel * @return the number of the channel, the same value as given */ @Override public int getDocumentChannelIndex(int channel) { return channel; } @Override public String getLabel(int channel) { if (labels != null && channel < labels.length) { return labels[channel]; } return "L" + (channel+1); } @Override public int getSampleCount(int channel) { return sampleCount; } /** * Returns the given number of samples for a given channel starting * from a given position in time. * If it is possible uses buffer, if not (or only partially) reads the * data from file (random access). * @param channel the number of channel * @param target the array to which results will be written starting * from position <code>arrayOffset</code> * @param signalOffset the position (in time) in the signal starting * from which samples will be returned * @param count the number of samples to be returned * @param arrayOffset the offset in <code>target</code> array starting * from which samples will be written * @throws IndexOutOfBoundsException if bad channel number is given * or samples of requested indexes are not in the signal * or the requested part of the signal doesn't fit in the * <code>target<\code> array */ @Override public void getSamples(int channel, double[] target, int signalOffset, int count, int arrayOffset) { synchronized (this) { if (channel < 0 || channel >= channelCount) { throw new IndexOutOfBoundsException("Bad channel number [" + channel + "]"); } if ((signalOffset < 0) || ((signalOffset + count) > sampleCount)) { throw new IndexOutOfBoundsException("Signal range [" + signalOffset + ":" + count + "] doesn't fit in the signal"); } if ((arrayOffset < 0) || ((arrayOffset + count) > target.length)) { throw new IndexOutOfBoundsException("Target range [" + arrayOffset + ":" + count + "] doesn't fit in the target array"); } int targetOffset; int sampleSize = channelCount * sampleByteWidth; // try to use existing mutiplexing buffer if (byteBuffer != null && minBufferedSample <= signalOffset && maxBufferedSample >= (signalOffset+count-1)) { targetOffset = (signalOffset-minBufferedSample) * channelCount; } else { byteBuffer = new byte[count * sampleSize]; minBufferedSample = signalOffset; maxBufferedSample = signalOffset + count - 1; try { long seekOffset = (long) signalOffset * (long) sampleSize; randomAccessFile.seek(seekOffset); randomAccessFile.readFully(byteBuffer); } catch (IOException ex) { byteBuffer = null; logger.error("Failed to read samples, filling the array with zero and exiting", ex); for (int i=0; i<count; i++) { target[arrayOffset+i] = 0.0F; } return; } bBuffer = ByteBuffer.wrap(byteBuffer).order(byteOrder.getByteOrder()); targetOffset = 0; } int sample = channel; int i; switch (sampleType) { case DOUBLE : DoubleBuffer doubleBuffer = bBuffer.asDoubleBuffer(); for (i=0; i<count; i++) { target[arrayOffset+i] = performCalibration(channel, doubleBuffer.get(targetOffset+sample)); sample += channelCount; } break; case FLOAT : FloatBuffer floatBuffer = bBuffer.asFloatBuffer(); for (i=0; i<count; i++) { target[arrayOffset+i] = performCalibration(channel, floatBuffer.get(targetOffset+sample)); sample += channelCount; } break; case INT : IntBuffer intBuffer = bBuffer.asIntBuffer(); for (i=0; i<count; i++) { target[arrayOffset+i] = performCalibration(channel, intBuffer.get(targetOffset+sample)); sample += channelCount; } break; case SHORT : ShortBuffer shortBuffer = bBuffer.asShortBuffer(); for (i=0; i<count; i++) { target[arrayOffset+i] = performCalibration(channel, shortBuffer.get(targetOffset+sample)); sample += channelCount; } break; } } } protected double performCalibration(int channelNumber, double sample) { if (calibrationGain != null && calibrationOffset != null) { Float gain = calibrationGain[channelNumber]; Float offset = calibrationOffset[channelNumber]; return sample * gain + offset; } else return sample; } /** * Destroys this sample source. Closes the file with signal. */ @Override public void destroy() { close(); } @Override public boolean areIndividualChannelsCalibrationCapable() { return true; } @Override public void setCalibrationGain(float calibration) { for (int i = 0; i < calibrationGain.length; i++) calibrationGain[i] = calibration; pcSupport.firePropertyChange(CALIBRATION_PROPERTY, null, calibrationGain); } @Override public float getSingleCalibrationGain() { return calibrationGain[0]; } @Override public float[] getCalibrationOffset() { return calibrationOffset; } @Override public void setCalibrationOffset(float calibrationOffset) { Arrays.fill(this.calibrationOffset, calibrationOffset); pcSupport.firePropertyChange(CALIBRATION_PROPERTY, null, calibrationOffset); } @Override public void setCalibrationOffset(float[] calibrationOffset) { if (!Arrays.equals(this.calibrationOffset, calibrationOffset)) { float[] oldCalibration = this.calibrationOffset; this.calibrationOffset = calibrationOffset; pcSupport.firePropertyChange(CALIBRATION_PROPERTY, oldCalibration, calibrationOffset); } } @Override public float getSingleCalibrationOffset() { return calibrationOffset[0]; } /** * Return the timestamp of the first sample in the signal. * @return the timestamp of the first sample in the signal */ public double getFirstSampleTimestamp() { return firstSampleTimestamp; } /** * Sets the timestamp of the first sample in the signal. * @param value the new value of the first sample in the signal */ public void setFirstSampleTimestamp(double value) { this.firstSampleTimestamp = value; } @Override public void addSignalChangeListener(PluginSignalChangeListener listener) { //this sample source doesn't support signalchangelisteners. } }