/* SampleBuffer.java created 2007-09-24 * */ package org.signalml.domain.signal; import java.beans.PropertyChangeEvent; import java.util.Arrays; import static java.lang.String.format; import org.apache.log4j.Logger; import org.signalml.domain.signal.samplesource.MultichannelSampleSource; /** * This class represents a multichannel source of signal samples with the * ability to buffer samples (and return buffered if possible). * * @author Michal Dobaczewski © 2007-2008 CC Otwarte Systemy Komputerowe Sp. z o.o. */ public class MultichannelSampleBuffer extends MultichannelSampleProcessor { public static final int INITIAL_BUFFER_SIZE = 128*60*3; // buffer 3 minutes of signal @ 128Hz protected static final Logger logger = Logger.getLogger(MultichannelSampleBuffer.class); /** * the length of the buffer (number of samples) */ private int bufferLength; /** * an array containing for each channel the in-signal index of the * first sample in the buffer (in the range from 0 to signal length-1) */ private int minSample[]; /** * an array containing for each channel the in-signal index of the * last sample in the buffer (in the range from 0 to signal length-1) */ private int maxSample[]; /** * an array containing for each <code>channel</code> the index in the * buffer where sample <code>minSample</code> is located * (in the range from 0 to <code>bufferLength-1</code>) * @see #minSample */ private int boundary[]; private double[][] buffer; /** * Constructor. Creates a buffer of a given length for a given source. * @param source the actual {@link MultichannelSampleSource source} * of samples * @param bufferLength the length of a buffer (a number of samples) */ public MultichannelSampleBuffer(MultichannelSampleSource source, int bufferLength) { super(source); this.bufferLength = bufferLength; reinitializeBuffers(); } /** * Returns the length of a buffer. * @return the length of a buffer (number of samples) */ public int getBufferLength() { return bufferLength; } /** * Sets the length of a buffer. * @param bufferLength the length of a buffer */ public void setBufferLength(int bufferLength) { this.bufferLength = bufferLength; reinitializeBuffers(); } private static int _buffer_too_small_count = 0; /** * Returns the given number of samples for a given channel starting * from a given position in time. * Uses buffering if possible and updates the buffer. * @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 samples) 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 */ @Override public void getSamples(int channel, double[] target, int signalOffset, int count, int arrayOffset) { if (count > bufferLength) { logger.warn(String.format("Unable to use buffer - buffer too small: count=%d bufferLength=%d", count, bufferLength)); if (this._buffer_too_small_count++ == 0) logger.debug("Buffer too small here, writing traceback once", new Throwable()); source.getSamples(channel, target, signalOffset, count, arrayOffset); return; } int maxSignalOffset = signalOffset + (count - 1); int bufferOffset; if (minSample[channel] == 0 && maxSample[channel] == 0) { // buffer fresh, rebuffer; bufferOffset = -1; } else if ((signalOffset >= minSample[channel]) && (maxSignalOffset < maxSample[channel])) { // the whole requested range is buffered bufferOffset = (boundary[channel] + (signalOffset-minSample[channel])) % bufferLength; } else if ((signalOffset > (maxSample[channel])) || (maxSignalOffset < (minSample[channel]-1))) { // the requested range is not adjacent to the buffered range - rebuffer whole bufferOffset = -1; } else { // partial buffer use is possible, but something is missing int cacheLength = (maxSample[channel]-minSample[channel]); int missingLength; int lostCnt; if (signalOffset < minSample[channel]) { // the beginning of the signal needs to be buffered missingLength = minSample[channel] - signalOffset; if (boundary[channel] - missingLength >= 0) { // this can be read in one bit source.getSamples(channel, buffer[channel], signalOffset, missingLength, boundary[channel]-missingLength); boundary[channel] -= missingLength; } else { int remainder = missingLength - boundary[channel]; // the number of floats to put at the end // read the bit at the beginning of buffer (count of places == boundary) if (missingLength > remainder) { source.getSamples(channel, buffer[channel], signalOffset+remainder, missingLength-remainder, 0); } boundary[channel] = bufferLength - remainder; source.getSamples(channel, buffer[channel], signalOffset, remainder, boundary[channel]); } minSample[channel] = signalOffset; lostCnt = (cacheLength + missingLength) - bufferLength; if (lostCnt > 0) { maxSample[channel] -= lostCnt; } cacheLength = (maxSample[channel]-minSample[channel]); } if (maxSignalOffset >= maxSample[channel]) { int endBoundary = (boundary[channel] + cacheLength) % bufferLength; // the index of the first place following the data // the end of the signal needs to be buffered missingLength = 1 + maxSignalOffset - maxSample[channel]; if (endBoundary + missingLength < bufferLength) { // this can be read in one bit source.getSamples(channel, buffer[channel], maxSample[channel], missingLength, endBoundary); } else { int remainder = missingLength - (bufferLength-endBoundary); if (missingLength > remainder) { source.getSamples(channel, buffer[channel], maxSample[channel], missingLength-remainder, endBoundary); } source.getSamples(channel, buffer[channel], maxSample[channel] + (missingLength-remainder), remainder, 0); } maxSample[channel] = maxSignalOffset + 1; lostCnt = (cacheLength + missingLength) - bufferLength; if (lostCnt > 0) { minSample[channel] += lostCnt; boundary[channel] = (boundary[channel] + lostCnt) % bufferLength; } } bufferOffset = (boundary[channel] + (signalOffset-minSample[channel])) % bufferLength; } if (bufferOffset < 0) { // needs to rebuffer boundary[channel] = 0; minSample[channel] = signalOffset; maxSample[channel] = maxSignalOffset+1; try { source.getSamples(channel, buffer[channel], signalOffset, count, 0); } catch(RuntimeException e) { logger.error(format("failed to read %d samples at %d", count, signalOffset)); throw e; } bufferOffset = 0; } // hopefully everything is now buffered int copyEnd = (bufferOffset + count); if (copyEnd > bufferLength) { // copy in two parts int second = copyEnd - bufferLength; int first = count - second; arrCopy(buffer[channel], target, bufferOffset, arrayOffset, first); arrCopy(buffer[channel], target, 0, arrayOffset+first, second); } else { // copy in one part arrCopy(buffer[channel], target, bufferOffset, arrayOffset, count); } } /** * Clears the buffer (actually creates a new one). */ public void clear() { reinitializeBuffers(); } /** * Returns the index in the buffer where sample <code>minSample</code> * for a given channel is located. * (in the range from 0 to <code>bufferLength-1</code>) * @param channel the index of a channel * @return the index in the buffer where sample <code>minSample</code> * for a given channel is located */ public int getBoundary(int channel) { return boundary[channel]; } /** * Returns for a given channel the in-signal index of the * last sample in the buffer (in the range from 0 to signal length-1) * @param channel the index of a channel * @return the in-signal index of the last sample in the buffer * (in the range from 0 to signal length-1) */ public int getMaxSample(int channel) { return maxSample[channel]; } /** * Returns for a given channel the in-signal index of the * first sample in the buffer (in the range from 0 to signal length-1) * @param channel the index of a channel * @return the in-signal index of the first sample in the buffer * (in the range from 0 to signal length-1) */ public int getMinSample(int channel) { return minSample[channel]; } /** * Returns a copy of a buffer for a given channel. * @param channel the index of a channel * @return a copy of a buffer for a given channel */ public double[] getBufferCopy(int channel) { return Arrays.copyOf(buffer[channel], bufferLength); } /** * Returns a copy of a part of a buffer for a given channel. * @param channel the index of a channel * @param offset the position in the buffer from which coping should * start * @param length the number of elements to be copied * @return the created copy */ public double[] getBufferCopy(int channel, int offset, int length) { return Arrays.copyOfRange(buffer[channel], offset, offset+length); } /** * Copies <code>count</code> elements from one array to another. * In the source array starts from <code>srcInx</code>, in the * destination array from <code>dstInx</code>. * @param src the source array * @param dst the destination array * @param srcIdx the index in the source array from which coping will * start * @param dstIdx the index in the destination array from starting from * which elements will be written to the destination array * @param count the number of elements to be copied */ private void arrCopy(double[] src, double[] dst, int srcIdx, int dstIdx, int count) { int i,e; int srcEnd = srcIdx + count; e = dstIdx; for (i=srcIdx; i<srcEnd; i++) { dst[e] = src[i]; e++; } } /** * Creates a new buffer and replaces the old one. */ private void reinitializeBuffers() { int cnt = source.getChannelCount(); minSample = new int[cnt]; maxSample = new int[cnt]; boundary = new int[cnt]; buffer = new double[cnt][bufferLength]; } /** * Fires listeners that the property has changed and reinitialises * the buffer * @param evt an event describing the change */ @Override public void propertyChange(PropertyChangeEvent evt) { reinitializeBuffers(); super.propertyChange(evt); } }