package org.signalml.app.worker.monitor; import java.io.File; import java.io.FileNotFoundException; import java.io.FileOutputStream; import java.io.IOException; import java.io.OutputStream; import java.nio.ByteBuffer; import java.nio.FloatBuffer; import java.util.ArrayList; import java.util.List; import org.apache.log4j.Logger; import org.apache.log4j.Level; import org.signalml.app.model.document.opensignal.ExperimentDescriptor; import org.signalml.app.model.document.opensignal.elements.SignalParameters; import org.signalml.domain.signal.raw.RawSignalByteOrder; import org.signalml.domain.signal.raw.RawSignalDescriptor; import org.signalml.domain.signal.raw.RawSignalSampleType; import org.signalml.domain.signal.raw.RawSignalDescriptor.SourceSignalType; import org.signalml.domain.signal.raw.RawSignalDescriptorWriter; /** * SignalRecorderWorker */ public class SignalRecorderWorker { /** * Logger. */ protected static final Logger logger = Logger.getLogger(SignalRecorderWorker.class); /** * Array to store received samples in. */ private List<float[]> sampleList; /** * The stream to save signal to. */ private OutputStream outputStream; /** * Path to data file. */ private String dataFilePath; /** * Path to metadata file. */ private String metadataFilePath; /** * Object describing the signal. */ private ExperimentDescriptor monitorDescriptor; /** * How many samples have been received. */ private int savedSampleCount; /** * How often a backup should be saved. */ private float backupFrequencyInMiliseconds; /** * The timestamp of the first recorded sample. */ private double firstSampleTimestamp; /** * Whether the worker is finished. */ private volatile boolean finished; /** * Timestamp of last offer. */ private long lastOffer; /** * Sum of time elapsed between offers; */ private long timeElapsed; /** * Tags recorder related to current recording. * This object calls the backup method, so it is synchronized. */ private TagRecorder tagRecorder; /** * Descriptor for recorded samples. */ private RawSignalDescriptor rawSignalDescriptor = new RawSignalDescriptor(); /** * Default constructor. * @param dataPath path to output file * @param experimentDescriptor object describing the signal * @throws FileNotFoundException when output stream cannot be initialized */ public SignalRecorderWorker(String dataPath, ExperimentDescriptor experimentDescriptor) throws FileNotFoundException { String metadataPath; if (dataPath.endsWith(".raw")) { metadataPath = dataPath.substring(0, dataPath.length() - 4) + ".xml"; } else { metadataPath = dataPath + ".xml"; dataPath = dataPath + ".raw"; } this.dataFilePath = dataPath; this.metadataFilePath = metadataPath; this.sampleList = new ArrayList<float[]>(); this.monitorDescriptor = experimentDescriptor; this.backupFrequencyInMiliseconds = experimentDescriptor.getBackupFrequency() * 1000; this.lastOffer = System.currentTimeMillis(); this.timeElapsed = 0; this.finished = false; this.savedSampleCount = 0; this.firstSampleTimestamp = Double.NaN; this.tagRecorder = null; rawSignalDescriptor.setSampleType(RawSignalSampleType.FLOAT); rawSignalDescriptor.setByteOrder(RawSignalByteOrder.LITTLE_ENDIAN); logger.setLevel((Level) Level.INFO); } /** * Adds samples to the list, and if it is time for a backup - saves them. * @param samples samples to be recorded */ public void offerChunk(float[] samples) { synchronized (this) { if (!finished) { sampleList.add(samples); long currentTimestamp = System.currentTimeMillis(); timeElapsed += currentTimestamp - lastOffer; lastOffer = currentTimestamp; if (timeElapsed > backupFrequencyInMiliseconds) { timeElapsed = 0; doSave(true); // it's time for a backup - let the TagRecorder know if (tagRecorder != null) tagRecorder.doBackup(); } } } } /** * Saves all remaining samples. */ public void save() { synchronized (this) { doSave(false); finished = true; } } /** * Saves all data to the disk. * @param isBackup whether the save is a backup */ private void doSave(boolean isBackup) { try { flushSamples(); saveMetadata(isBackup); } catch (IOException ex) { } } /** * Saves all samples from the list to the output. */ private void flushSamples() throws IOException { if (outputStream == null) outputStream = new FileOutputStream(dataFilePath); int chunkCount = sampleList.size(); if (chunkCount == 0) return; int sampleSize = rawSignalDescriptor.getSampleType().getByteWidth(); int chunkSize = sampleList.get(0).length * sampleSize; byte[] toSave = new byte[chunkSize * chunkCount]; int position = 0; for (int i = 0; i < chunkCount; i++) { float[] chunk = sampleList.get(i); byte[] processedChunk = processChunk(chunk); for (int j = 0; j < processedChunk.length; j++) { toSave[position++] = processedChunk[j]; } } outputStream.write(toSave, 0, toSave.length); outputStream.flush(); savedSampleCount += chunkCount; sampleList.clear(); } /** * Processes a chunk to be saved. * @param chunk chunk to be saved * @return processed chunk */ private byte[] processChunk(float[] chunk) { byte[] byteBuffer = new byte[chunk.length * rawSignalDescriptor.getSampleType().getByteWidth()]; ByteBuffer bBuffer = ByteBuffer.wrap(byteBuffer).order(rawSignalDescriptor.getByteOrder().getByteOrder()); FloatBuffer buf = bBuffer.asFloatBuffer(); buf.clear(); buf.put(chunk, 0, chunk.length); return byteBuffer; } /** * Saves metadata. * @param isBackup whether this save is a backup or a normal save * @throws IOException when file cannot be used */ private void saveMetadata(boolean isBackup) throws IOException { File metadataFile = new File(metadataFilePath); if (metadataFile.exists()) metadataFile.delete(); metadataFile.createNewFile(); RawSignalDescriptor rsd = new RawSignalDescriptor(); rsd.setExportFileName(dataFilePath); rsd.setBlocksPerPage(1); SignalParameters signalParameters = monitorDescriptor.getSignalParameters(); rsd.setByteOrder(rawSignalDescriptor.getByteOrder()); rsd.setCalibrationGain(signalParameters.getCalibrationGain()); rsd.setCalibrationOffset(signalParameters.getCalibrationOffset()); rsd.setChannelCount(signalParameters.getChannelCount()); rsd.setChannelLabels(monitorDescriptor.getAmplifier().getSelectedChannelsLabels()); rsd.setPageSize(signalParameters.getPageSize()); rsd.setSampleCount(savedSampleCount); rsd.setSampleType(rawSignalDescriptor.getSampleType()); rsd.setSamplingFrequency(signalParameters.getSamplingFrequency()); rsd.setSourceSignalType(SourceSignalType.RAW); rsd.setFirstSampleTimestamp(firstSampleTimestamp); rsd.setIsBackup(isBackup); if (monitorDescriptor.getEegSystem() != null) rsd.setEegSystemName(monitorDescriptor.getEegSystem().getEegSystemName()); RawSignalDescriptorWriter descrWriter = new RawSignalDescriptorWriter(); descrWriter.writeDocument(rsd, metadataFile); } /** * Sets the timestamp of the first sample for this signal. * @param value new value of the timestamp */ public void setFirstSampleTimestamp(double value) { this.firstSampleTimestamp = value; } /** * Returns if the firstSampleTimestamp was set using {@link SignalRecorderWorker#setFirstSampleTimestamp(double)}. * @return true if the firstSampleTimestamp was set, false otherwise. */ public boolean isFirstSampleTimestampSet() { if (Double.isNaN(firstSampleTimestamp)) { return false; } return true; } /** * Sets {@link #tagRecorder}. * @param tagRecorder {@link #tagRecorder} */ public void setTagRecorder(TagRecorder tagRecorder) { this.tagRecorder = tagRecorder; } }