diff --git a/Builds/VisualStudio2013/Plugins/BinaryWriter/BinaryWriter.vcxproj b/Builds/VisualStudio2013/Plugins/BinaryWriter/BinaryWriter.vcxproj index aaa3f13e9f26588f0f187b739da1769f1ac90178..e2f9f274aebabcff702d5ba3507f66fb15b77987 100644 --- a/Builds/VisualStudio2013/Plugins/BinaryWriter/BinaryWriter.vcxproj +++ b/Builds/VisualStudio2013/Plugins/BinaryWriter/BinaryWriter.vcxproj @@ -117,10 +117,12 @@ <ItemGroup> <ClInclude Include="..\..\..\..\Source\Plugins\BinaryWriter\BinaryRecording.h" /> <ClInclude Include="..\..\..\..\Source\Plugins\BinaryWriter\FileMemoryBlock.h" /> + <ClInclude Include="..\..\..\..\Source\Plugins\BinaryWriter\NpyFile.h" /> <ClInclude Include="..\..\..\..\Source\Plugins\BinaryWriter\SequentialBlockFile.h" /> </ItemGroup> <ItemGroup> <ClCompile Include="..\..\..\..\Source\Plugins\BinaryWriter\BinaryRecording.cpp" /> + <ClCompile Include="..\..\..\..\Source\Plugins\BinaryWriter\NpyFile.cpp" /> <ClCompile Include="..\..\..\..\Source\Plugins\BinaryWriter\OpenEphysLib.cpp" /> <ClCompile Include="..\..\..\..\Source\Plugins\BinaryWriter\SequentialBlockFile.cpp" /> </ItemGroup> diff --git a/Builds/VisualStudio2013/Plugins/BinaryWriter/BinaryWriter.vcxproj.filters b/Builds/VisualStudio2013/Plugins/BinaryWriter/BinaryWriter.vcxproj.filters index cdb841320585eb08d8283cdbc2af0d06cdf4f9db..bc74dc84ad1170ee3966b405cfdc2d03cd8d1438 100644 --- a/Builds/VisualStudio2013/Plugins/BinaryWriter/BinaryWriter.vcxproj.filters +++ b/Builds/VisualStudio2013/Plugins/BinaryWriter/BinaryWriter.vcxproj.filters @@ -24,6 +24,9 @@ <ClInclude Include="..\..\..\..\Source\Plugins\BinaryWriter\BinaryRecording.h"> <Filter>Source Files</Filter> </ClInclude> + <ClInclude Include="..\..\..\..\Source\Plugins\BinaryWriter\NpyFile.h"> + <Filter>Source Files</Filter> + </ClInclude> </ItemGroup> <ItemGroup> <ClCompile Include="..\..\..\..\Source\Plugins\BinaryWriter\SequentialBlockFile.cpp"> @@ -35,5 +38,8 @@ <ClCompile Include="..\..\..\..\Source\Plugins\BinaryWriter\OpenEphysLib.cpp"> <Filter>Source Files</Filter> </ClCompile> + <ClCompile Include="..\..\..\..\Source\Plugins\BinaryWriter\NpyFile.cpp"> + <Filter>Source Files</Filter> + </ClCompile> </ItemGroup> </Project> \ No newline at end of file diff --git a/Source/Plugins/BinaryWriter/BinaryRecording.cpp b/Source/Plugins/BinaryWriter/BinaryRecording.cpp index 9542f84937c0176e1d19b3e099fdfa19f8cb0c6d..b13ee7123bd6925ccf8da8a50c6e527ac44d8de2 100644 --- a/Source/Plugins/BinaryWriter/BinaryRecording.cpp +++ b/Source/Plugins/BinaryWriter/BinaryRecording.cpp @@ -31,6 +31,7 @@ BinaryRecording::BinaryRecording() { m_scaledBuffer.malloc(MAX_BUFFER_SIZE); m_intBuffer.malloc(MAX_BUFFER_SIZE); + m_tsBuffer.malloc(MAX_BUFFER_SIZE); } BinaryRecording::~BinaryRecording() @@ -45,13 +46,20 @@ String BinaryRecording::getEngineID() const void BinaryRecording::openFiles(File rootFolder, int experimentNumber, int recordingNumber) { - String basepath = rootFolder.getFullPathName() + rootFolder.separatorString + "experiment" + String(experimentNumber); + String basepath = rootFolder.getFullPathName() + rootFolder.separatorString + "experiment" + String(experimentNumber) + + File::separatorString + "recording" + String(recordingNumber + 1) + File::separatorString; + String contPath = basepath + "continuous" + File::separatorString; //Open channel files int nProcessors = getNumRecordedProcessors(); m_channelIndexes.insertMultiple(0, 0, getNumRecordedChannels()); m_fileIndexes.insertMultiple(0, 0, getNumRecordedChannels()); + Array<const DataChannel*> indexedDataChannels; + Array<unsigned int> indexedChannelCount; + Array<var> jsonContinuousfiles; + Array<var> jsonChannels; + StringArray continuousFileNames; int lastId = 0; for (int proc = 0; proc < nProcessors; proc++) { @@ -65,94 +73,421 @@ void BinaryRecording::openFiles(File rootFolder, int experimentNumber, int recor const DataChannel* channelInfo = getDataChannel(realChan); int sourceId = channelInfo->getSourceNodeID(); int sourceSubIdx = channelInfo->getSubProcessorIdx(); - int nInfoArrays = m_dataChannels.size(); + int nInfoArrays = indexedDataChannels.size(); bool found = false; + DynamicObject::Ptr jsonChan = new DynamicObject(); + jsonChan->setProperty("name", channelInfo->getName()); + jsonChan->setProperty("description", channelInfo->getDescription()); + jsonChan->setProperty("identifier", channelInfo->getIdentifier()); + jsonChan->setProperty("history", channelInfo->getHistoricString()); + jsonChan->setProperty("bit_volts", channelInfo->getBitVolts()); + jsonChan->setProperty("units", channelInfo->getDataUnits()); + jsonChan->setProperty("source_processor_index", channelInfo->getSourceIndex()); + jsonChan->setProperty("recorded_processor_index", channelInfo->getCurrentNodeChannelIdx()); + createChannelMetaData(channelInfo, jsonChan); for (int i = lastId; i < nInfoArrays; i++) { - if (sourceId == m_dataChannels.getReference(i)[0]->getSourceNodeID() && sourceSubIdx == m_dataChannels.getReference(i)[0]->getSubProcessorIdx()) + if (sourceId == indexedDataChannels[i]->getSourceNodeID() && sourceSubIdx == indexedDataChannels[i]->getSubProcessorIdx()) { - m_channelIndexes.set(recordedChan, m_dataChannels.getReference(i).size()); + unsigned int count = indexedChannelCount[i]; + m_channelIndexes.set(recordedChan, count); m_fileIndexes.set(recordedChan, i); - m_dataChannels.getReference(i).add(getDataChannel(realChan)); + indexedChannelCount.set(i, count + 1); + jsonChannels.getReference(i).append(var(jsonChan)); found = true; break; } } if (!found) { - File datFile(basepath + "_" + String(pInfo.processorId) + "_" + String(sourceId) + "." + String(sourceSubIdx) + "_" + String(recordingNumber) + ".dat"); - ScopedPointer<SequentialBlockFile> bFile = new SequentialBlockFile(pInfo.recordedChannels.size(), samplesPerBlock); - if (bFile->openFile(datFile)) - m_DataFiles.add(bFile.release()); - else - m_DataFiles.add(nullptr); + String datFileName(channelInfo->getCurrentNodeName() + "_(" + String(channelInfo->getCurrentNodeID()) + ")" + File::separatorString + channelInfo->getSourceName() + "_(" + String(sourceId) + "." + String(sourceSubIdx) + ")"); + continuousFileNames.add(contPath + datFileName + ".dat"); + + Array<NpyType> tstypes; + tstypes.add(NpyType("Timestamp", BaseType::INT64, 1)); + + ScopedPointer<NpyFile> tFile = new NpyFile(contPath + datFileName + "_timestamps.npy", tstypes); + m_dataTimestampFiles.add(tFile.release()); - ContinuousGroup newGroup; - newGroup.add(getDataChannel(realChan)); - m_dataChannels.add(newGroup); m_fileIndexes.set(recordedChan, nInfoArrays); m_channelIndexes.set(recordedChan, 0); - + indexedChannelCount.add(1); + indexedDataChannels.add(channelInfo); + + Array<var> jsonChanArray; + jsonChanArray.add(var(jsonChan)); + jsonChannels.add(var(jsonChanArray)); + DynamicObject::Ptr jsonFile = new DynamicObject(); + jsonFile->setProperty("name", datFileName); + jsonFile->setProperty("sample_rate", channelInfo->getSampleRate()); + jsonFile->setProperty("source_processor_name", channelInfo->getSourceName()); + jsonFile->setProperty("source_processor_id", channelInfo->getSourceNodeID()); + jsonFile->setProperty("source_processor_sub_idx", channelInfo->getSubProcessorIdx()); + jsonFile->setProperty("recorded_processor", channelInfo->getCurrentNodeName()); + jsonFile->setProperty("recorded_processor_id", channelInfo->getCurrentNodeID()); + jsonContinuousfiles.add(var(jsonFile)); } } + lastId = indexedDataChannels.size(); } + int nFiles = continuousFileNames.size(); + for (int i = 0; i < nFiles; i++) + { + int numChannels = jsonChannels.getReference(i).size(); + ScopedPointer<SequentialBlockFile> bFile = new SequentialBlockFile(numChannels, samplesPerBlock); + if (bFile->openFile(continuousFileNames[i])) + m_DataFiles.add(bFile.release()); + else + m_DataFiles.add(nullptr); + DynamicObject::Ptr jsonFile = jsonContinuousfiles.getReference(i).getDynamicObject(); + jsonFile->setProperty("num_channels", numChannels); + jsonFile->setProperty("channels", jsonChannels.getReference(i)); + } + int nChans = getNumRecordedChannels(); - //Origin Timestamp + //Timestamps + Array<uint32> procIDs; for (int i = 0; i < nChans; i++) { m_startTS.add(getTimestamp(i)); } - //Other files, using OriginalRecording code - openEventFile(basepath, recordingNumber); - openMessageFile(basepath, recordingNumber); - for (int i = 0; i < spikeFileArray.size(); i++) + int nEvents = getNumRecordedEvents(); + String eventPath(basepath + "events" + File::separatorString); + int binCount = 0, ttlCount = 0, textCount = 0; + Array<var> jsonEventFiles; + + for (int ev = 0; ev < nEvents; ev++) + { + const EventChannel* chan = getEventChannel(ev); + String eventName; + Array<NpyType> types; + String typeName; + + switch (chan->getChannelType()) + { + case EventChannel::TEXT: + textCount++; + eventName += "TEXT" + String(textCount); + types.add(NpyType("message", BaseType::CHAR, chan->getLength())); + typeName = "text_message"; + break; + case EventChannel::TTL: + ttlCount++; + eventName += "TTL" + String(ttlCount); + types.add(NpyType("TTL_Channel", BaseType::INT16, 1)); + typeName = "ttl"; + break; + default: + binCount++; + eventName += "BIN" + String(ttlCount); + types.add(NpyType("Data", chan->getEquivalentMetaDataType(), chan->getLength())); + typeName = jsonTypeValue(chan->getEquivalentMetaDataType()); + break; + } + eventName += "_" + chan->getSourceName() + "(" + String(chan->getSourceNodeID()) + "." + String(chan->getSubProcessorIdx()) + ")"; + String fName = eventPath + eventName; + ScopedPointer<EventRecording> rec = new EventRecording(); + Array<NpyType> tsType; + tsType.add(NpyType("Timestamp", BaseType::INT64, 1)); + //TTL channels behave a bit different + if (chan->getChannelType() == EventChannel::TTL) + { + if (m_TTLMode == TTLMode::JointWord) + { + types.add(NpyType("TTL_Word", BaseType::UINT8, chan->getDataSize())); + } + else if (m_TTLMode == TTLMode::SeparateWord) + { + Array<NpyType> wordType; + wordType.add(NpyType("TTL_Word", BaseType::UINT8, chan->getDataSize())); + rec->extraFile = new NpyFile(fName + "_TTLWord.npy", wordType); + } + //since the main TTL file already contins channel numbers, it would be redundant to store them on the timestamp file + } + else + { + if (m_eventMode == EventMode::SeparateChannel) + { + Array<NpyType> chanType; + chanType.add(NpyType("Channel", BaseType::UINT16, 1)); + rec->channelFile = new NpyFile(fName + "_channel.npy", chanType); + } + else + tsType.add(NpyType("Channel", BaseType::UINT16, 1)); + } + rec->mainFile = new NpyFile(fName + ".npy", types); + rec->timestampFile = new NpyFile(fName + "_timestamps.npy", tsType); + DynamicObject::Ptr jsonChannel = new DynamicObject(); + jsonChannel->setProperty("name", chan->getName()); + jsonChannel->setProperty("description", chan->getDescription()); + jsonChannel->setProperty("identifier", chan->getIdentifier()); + jsonChannel->setProperty("sample_rate", chan->getSampleRate()); + jsonChannel->setProperty("type", typeName); + jsonChannel->setProperty("num_channels", (int)chan->getNumChannels()); + jsonChannel->setProperty("source_processor", chan->getSourceName()); + createChannelMetaData(chan, jsonChannel); + + rec->metaDataFile = createEventMetadataFile(chan, fName + "_metadata.npy", jsonChannel); + m_eventFiles.add(rec.release()); + jsonEventFiles.add(var(jsonChannel)); + } + + int nSpikes = getNumRecordedSpikes(); + Array<const SpikeChannel*> indexedSpikes; + Array<uint16> indexedChannels; + m_spikeFileIndexes.insertMultiple(0, 0, nSpikes); + m_spikeChannelIndexes.insertMultiple(0, 0, nSpikes); + String spikePath(basepath + "spikes" + File::separatorString); + Array<var> jsonSpikeFiles; + Array<var> jsonSpikeChannels; + for (int sp = 0; sp < nSpikes; sp++) + { + const SpikeChannel* ch = getSpikeChannel(sp); + DynamicObject::Ptr jsonChannel = new DynamicObject(); + unsigned int numSpikeChannels = ch->getNumChannels(); + jsonChannel->setProperty("name", ch->getName()); + jsonChannel->setProperty("description", ch->getDescription()); + jsonChannel->setProperty("identifier", ch->getIdentifier()); + Array<var> jsonChannelInfo; + for (int i = 0; i < numSpikeChannels; i++) + { + SourceChannelInfo sourceInfo = ch->getSourceChannelInfo()[i]; + DynamicObject::Ptr jsonSpikeChInfo = new DynamicObject(); + jsonSpikeChInfo->setProperty("source_processor_id", sourceInfo.processorID); + jsonSpikeChInfo->setProperty("source_processor_sub_idx", sourceInfo.subProcessorID); + jsonSpikeChInfo->setProperty("source_processor_channel", sourceInfo.channelIDX); + jsonChannelInfo.add(var(jsonSpikeChInfo)); + } + jsonChannel->setProperty("source_channel_info", jsonChannelInfo); + createChannelMetaData(ch, jsonChannel); + + int nIndexed = indexedSpikes.size(); + bool found = false; + for (int i = 0; i < nIndexed; i++) + { + const SpikeChannel* ich = indexedSpikes[i]; + //identical channels (same data and metadata) from the same processor go to the same file + if (ch->getSourceNodeID() == ich->getSourceNodeID() && ch->getSubProcessorIdx() == ich->getSubProcessorIdx() && *ch == *ich) + { + found = true; + m_spikeFileIndexes.set(sp, i); + unsigned int numChans = indexedChannels[i]; + indexedChannels.set(i, numChans); + m_spikeChannelIndexes.set(sp, numChans + 1); + jsonSpikeChannels.getReference(i).append(var(jsonChannel)); + break; + } + } + + if (!found) + { + int fileIndex = m_spikeFiles.size(); + m_spikeFileIndexes.set(sp, fileIndex); + indexedSpikes.add(ch); + m_spikeChannelIndexes.set(sp, 0); + indexedChannels.add(1); + ScopedPointer<EventRecording> rec = new EventRecording(); + Array<NpyType> spTypes; + for (int c = 0; c < ch->getNumChannels(); c++) + { + spTypes.add(NpyType("channel" + String(c + 1), BaseType::INT16, ch->getTotalSamples())); + } + String spikeName("spike_group_" + String(fileIndex + 1)); + String fName(spikePath + spikeName); + rec->mainFile = new NpyFile(fName + ".npy", spTypes); + Array<NpyType> tsTypes; + tsTypes.add(NpyType("timestamp", BaseType::INT64, 1)); + if (m_spikeMode == SpikeMode::AllInOne) + { + tsTypes.add(NpyType("electrode_index", BaseType::UINT16, 1)); + tsTypes.add(NpyType("sorted_id", BaseType::UINT16, 1)); + } + else + { + Array<NpyType> indexType; + indexType.add(NpyType("electrode_index", BaseType::UINT16, 1)); + if (m_spikeMode == SpikeMode::AllSeparated) + { + Array<NpyType> sortedType; + sortedType.add(NpyType("sorted_id", BaseType::UINT16, 1)); + rec->extraFile = new NpyFile(fName + "_sortedID.npy", sortedType); + } + else + { + indexType.add(NpyType("sorted_id", BaseType::UINT16, 1)); + } + rec->channelFile = new NpyFile(fName + "indexes.npy", indexType); + } + rec->timestampFile = new NpyFile(fName + "_timestamps.npy", tsTypes); + Array<var> jsonChanArray; + jsonChanArray.add(var(jsonChannel)); + jsonSpikeChannels.add(var(jsonChanArray)); + DynamicObject::Ptr jsonFile = new DynamicObject(); + + jsonFile->setProperty("name", spikeName); + jsonFile->setProperty("sample_rate", ch->getSampleRate()); + jsonFile->setProperty("source_processor", ch->getSourceName()); + jsonFile->setProperty("num_channels", (int)numSpikeChannels); + jsonFile->setProperty("pre_peak_samples", (int)ch->getPrePeakSamples()); + jsonFile->setProperty("post_peak_samples", (int)ch->getPostPeakSamples()); + + rec->metaDataFile = createEventMetadataFile(ch, fName + "_metadata.npy", jsonFile); + m_spikeFiles.add(rec.release()); + jsonSpikeFiles.add(var(jsonFile)); + } + } + int nSpikeFiles = jsonSpikeFiles.size(); + for (int i = 0; i < nSpikeFiles; i++) { - openSpikeFile(basepath, i, recordingNumber); + int size = jsonSpikeChannels.getReference(i).size(); + DynamicObject::Ptr jsonFile = jsonSpikeFiles.getReference(i).getDynamicObject(); + jsonFile->setProperty("num_channels", size); + jsonFile->setProperty("channels", jsonSpikeChannels.getReference(i)); } + + Array<NpyType> msgType; + msgType.add(NpyType("sync_text", BaseType::CHAR, 256)); + m_syncTextFile = new NpyFile(basepath + "sync_text.npy", msgType); m_recordingNum = recordingNumber; + + DynamicObject::Ptr jsonSettingsFile = new DynamicObject(); + jsonSettingsFile->setProperty("GUI version", CoreServices::getGUIVersion()); + jsonSettingsFile->setProperty("continuous", jsonContinuousfiles); + jsonSettingsFile->setProperty("events", jsonEventFiles); + jsonSettingsFile->setProperty("spikes", jsonSpikeFiles); + FileOutputStream settingsFileStream(File(basepath + "structure.oebin")); + + jsonSettingsFile->writeAsJSON(settingsFileStream, 2, false); } -void BinaryRecording::closeFiles() +NpyFile* BinaryRecording::createEventMetadataFile(const MetaDataEventObject* channel, String filename, DynamicObject* jsonFile) { - m_DataFiles.clear(); - for (int i = 0; i < spikeFileArray.size(); i++) + int nMetaData = channel->getEventMetaDataCount(); + if (nMetaData < 1) return nullptr; + + Array<NpyType> types; + Array<var> jsonMetaData; + for (int i = 0; i < nMetaData; i++) { - if (spikeFileArray[i] != nullptr) - { - diskWriteLock.enter(); - fclose(spikeFileArray[i]); - spikeFileArray.set(i, nullptr); - diskWriteLock.exit(); - } + const MetaDataDescriptor* md = channel->getEventMetaDataDescriptor(i); + types.add(NpyType(md->getName(), md->getType(), md->getLength())); + DynamicObject::Ptr jsonValues = new DynamicObject(); + jsonValues->setProperty("name", md->getName()); + jsonValues->setProperty("description", md->getDescription()); + jsonValues->setProperty("identifier", md->getIdentifier()); + jsonValues->setProperty("type", jsonTypeValue(md->getType())); + jsonValues->setProperty("length", (int)md->getLength()); + jsonMetaData.add(var(jsonValues)); } - if (eventFile != nullptr) + if (jsonFile) + jsonFile->setProperty("event_metadata", jsonMetaData); + return new NpyFile(filename, types); +} + +template <typename TO, typename FROM> +void dataToVar(var& dataTo, const void* dataFrom, int length) +{ + const FROM* buffer = reinterpret_cast<const FROM*>(dataFrom); + for (int i = 0; i < length; i++) { - diskWriteLock.enter(); - fclose(eventFile); - eventFile = nullptr; - diskWriteLock.exit(); + dataTo.append(static_cast<TO>(*(buffer + i))); } - if (messageFile != nullptr) +} + +void BinaryRecording::createChannelMetaData(const MetaDataInfoObject* channel, DynamicObject* jsonFile) +{ + int nMetaData = channel->getMetaDataCount(); + if (nMetaData < 1) return; + + Array<var> jsonMetaData; + for (int i = 0; i < nMetaData; i++) { - diskWriteLock.enter(); - fclose(messageFile); - messageFile = nullptr; - diskWriteLock.exit(); + const MetaDataDescriptor* md = channel->getMetaDataDescriptor(i); + const MetaDataValue* mv = channel->getMetaDataValue(i); + DynamicObject::Ptr jsonValues = new DynamicObject(); + MetaDataDescriptor::MetaDataTypes type = md->getType(); + unsigned int length = md->getLength(); + jsonValues->setProperty("name", md->getName()); + jsonValues->setProperty("description", md->getDescription()); + jsonValues->setProperty("identifier", md->getIdentifier()); + jsonValues->setProperty("type", jsonTypeValue(type)); + jsonValues->setProperty("length", (int)length); + var val; + if (type == MetaDataDescriptor::CHAR) + { + String tmp; + mv->getValue(tmp); + val = tmp; + } + else + { + const void* buf = mv->getRawValuePointer(); + switch (type) + { + case MetaDataDescriptor::INT8: + dataToVar<int, int8>(val, buf, length); + break; + case MetaDataDescriptor::UINT8: + dataToVar<int, uint8>(val, buf, length); + break; + case MetaDataDescriptor::INT16: + dataToVar<int, int16>(val, buf, length); + break; + case MetaDataDescriptor::UINT16: + dataToVar<int, uint16>(val, buf, length); + break; + case MetaDataDescriptor::INT32: + dataToVar<int, int32>(val, buf, length); + break; + //A full uint32 doesn't fit in a regular int, so we increase size + case MetaDataDescriptor::UINT32: + dataToVar<int64, uint8>(val, buf, length); + break; + case MetaDataDescriptor::INT64: + dataToVar<int64, int64>(val, buf, length); + break; + //This might overrun and end negative if the uint64 is really big, but there is no way to store a full uint64 in a var + case MetaDataDescriptor::UINT64: + dataToVar<int64, uint64>(val, buf, length); + break; + case MetaDataDescriptor::FLOAT: + dataToVar<float, float>(val, buf, length); + break; + case MetaDataDescriptor::DOUBLE: + dataToVar<double, double>(val, buf, length); + break; + default: + val = "invalid"; + } + } + jsonValues->setProperty("value", val); + jsonMetaData.add(var(jsonValues)); } - m_scaledBuffer.malloc(MAX_BUFFER_SIZE); - m_intBuffer.malloc(MAX_BUFFER_SIZE); - m_bufferSize = MAX_BUFFER_SIZE; - m_startTS.clear(); + jsonFile->setProperty("channel_metadata", jsonMetaData); +} + +void BinaryRecording::closeFiles() +{ + resetChannels(); } void BinaryRecording::resetChannels() { + m_DataFiles.clear(); + m_channelIndexes.clear(); + m_fileIndexes.clear(); + m_dataTimestampFiles.clear(); + m_eventFiles.clear(); + m_spikeChannelIndexes.clear(); + m_spikeFileIndexes.clear(); + m_spikeFiles.clear(); + m_syncTextFile = nullptr; + m_scaledBuffer.malloc(MAX_BUFFER_SIZE); m_intBuffer.malloc(MAX_BUFFER_SIZE); + m_tsBuffer.malloc(MAX_BUFFER_SIZE); m_bufferSize = MAX_BUFFER_SIZE; - m_DataFiles.clear(); - spikeFileArray.clear(); m_startTS.clear(); } @@ -164,336 +499,187 @@ void BinaryRecording::writeData(int writeChannel, int realChannel, const float* m_bufferSize = size; m_scaledBuffer.malloc(size); m_intBuffer.malloc(size); + m_tsBuffer.malloc(size); } double multFactor = 1 / (float(0x7fff) * getDataChannel(realChannel)->getBitVolts()); FloatVectorOperations::copyWithMultiply(m_scaledBuffer.getData(), buffer, multFactor, size); AudioDataConverters::convertFloatToInt16LE(m_scaledBuffer.getData(), m_intBuffer.getData(), size); m_DataFiles[m_fileIndexes[writeChannel]]->writeChannel(getTimestamp(writeChannel)-m_startTS[writeChannel],m_channelIndexes[writeChannel],m_intBuffer.getData(),size); + + if (m_channelIndexes[writeChannel] == 0) + { + int64 baseTS = getTimestamp(writeChannel); + //Let's hope that the compiler is smart enough to vectorize this. + for (int i = 0; i < size; i++) + { + m_tsBuffer[i] = (baseTS + i); + } + m_dataTimestampFiles[m_fileIndexes[writeChannel]]->writeData(m_tsBuffer, size*sizeof(int64)); + } } -//Code below is copied from OriginalRecording, so it's not as clean as newer one void BinaryRecording::addSpikeElectrode(int index, const SpikeChannel* elec) { - spikeFileArray.add(nullptr); } -void BinaryRecording::openEventFile(String basepath, int recordingNumber) +void BinaryRecording::writeEventMetaData(const MetaDataEvent* event, NpyFile* file) { - FILE* chFile; - String fullPath = basepath + "_all_channels_" + String(recordingNumber) + ".events"; - - - - std::cout << "OPENING FILE: " << fullPath << std::endl; - - File f = File(fullPath); - - bool fileExists = f.exists(); - - diskWriteLock.enter(); - - chFile = fopen(fullPath.toUTF8(), "ab"); - - if (!fileExists) - { - // create and write header - std::cout << "Writing header." << std::endl; - String header = generateEventHeader(); - //std::cout << header << std::endl; - std::cout << "File ID: " << chFile << ", number of bytes: " << header.getNumBytesAsUTF8() << std::endl; - - - fwrite(header.toUTF8(), 1, header.getNumBytesAsUTF8(), chFile); - - std::cout << "Wrote header." << std::endl; - - // std::cout << "Block index: " << blockIndex << std::endl; - - } - else + if (!file || !event) return; + int nMetaData = event->getMetadataValueCount(); + for (int i = 0; i < nMetaData; i++) { - std::cout << "File already exists, just opening." << std::endl; - fseek(chFile, 0, SEEK_END); + const MetaDataValue* val = event->getMetaDataValue(i); + file->writeData(val->getRawValuePointer(), val->getDataSize()); } - - eventFile = chFile; - - diskWriteLock.exit(); - } -void BinaryRecording::openSpikeFile(String basePath, int spikeIndex, int recordingNumber) +void BinaryRecording::writeEvent(int eventIndex, const MidiMessage& event) { - const SpikeChannel* elec = getSpikeChannel(spikeIndex); - FILE* spFile; - String fullPath = basePath + "_" + elec->getName().removeCharacters(" ") + "_" + String(recordingNumber) + ".spikes"; - - std::cout << "OPENING FILE: " << fullPath << std::endl; - - File f = File(fullPath); - - bool fileExists = f.exists(); - - diskWriteLock.enter(); - - spFile = fopen(fullPath.toUTF8(), "ab"); - - if (!fileExists) + EventPtr ev = Event::deserializeFromMessage(event, getEventChannel(eventIndex)); + EventRecording* rec = m_eventFiles[eventIndex]; + if (!rec) return; + const EventChannel* info = getEventChannel(eventIndex); + int64 ts = ev->getTimestamp(); + rec->timestampFile->writeData(&ts, sizeof(int64)); + if (ev->getEventType() == EventChannel::TTL) { - String header = generateSpikeHeader(elec); - fwrite(header.toUTF8(), 1, header.getNumBytesAsUTF8(), spFile); + TTLEvent* ttl = static_cast<TTLEvent*>(ev.get()); + int16 data = ttl->getChannel() * (ttl->getState() ? 1 : -1); + rec->mainFile->writeData(&data, sizeof(int16)); + NpyFile* wordFile = nullptr; + if (m_TTLMode == TTLMode::JointWord) + { + wordFile = rec->mainFile; + } + else if (m_TTLMode == TTLMode::SeparateWord) + { + wordFile = rec->extraFile; + } + if (wordFile) + wordFile->writeData(ttl->getTTLWordPointer(), info->getDataSize()); } - diskWriteLock.exit(); - spikeFileArray.set(spikeIndex, spFile); - -} - -void BinaryRecording::openMessageFile(String basepath, int recordNumber) -{ - FILE* mFile; - String fullPath = basepath + "_messages_" + String(recordNumber) + ".events"; - - fullPath += "messages"; - - - - std::cout << "OPENING FILE: " << fullPath << std::endl; - - File f = File(fullPath); - - //bool fileExists = f.exists(); - - diskWriteLock.enter(); - - mFile = fopen(fullPath.toUTF8(), "ab"); - - //If this file needs a header, it goes here - - diskWriteLock.exit(); - messageFile = mFile; - -} - -String BinaryRecording::generateEventHeader() -{ - - String header = "header.format = 'Open Ephys Data Format'; \n"; - - header += "header.version = " + String(VERSION_STRING) + "; \n"; - header += "header.header_bytes = "; - header += String(HEADER_SIZE); - header += ";\n"; - - header += "header.description = 'each record contains one 64-bit timestamp, one 16-bit sample position, one uint8 event type, one uint8 processor ID, one uint8 event ID, one uint8 event channel, and one uint16 recordingNumber'; \n"; - - - header += "header.date_created = '"; - header += generateDateString(); - header += "';\n"; - - header += "header.channel = '"; - header += "Events"; - header += "';\n"; - - header += "header.channelType = 'Event';\n"; - - - header += "header.sampleRate = "; - // all channels need to have the same sample rate under the current scheme - header += String(getEventChannel(0)->getSampleRate()); - header += ";\n"; - header += "header.blockLength = "; - header += BLOCK_LENGTH; - header += ";\n"; - header += "header.bufferSize = "; - header += "1024"; - header += ";\n"; - header += "header.bitVolts = "; - header += "1"; - header += ";\n"; - - header = header.paddedRight(' ', HEADER_SIZE); - - //std::cout << header << std::endl; - - return header; - -} - -String BinaryRecording::generateSpikeHeader(const SpikeChannel* elec) -{ - String header = "header.format = 'Open Ephys Data Format'; \n"; - header += "header.version = " + String(VERSION_STRING) + "; \n"; - header += "header.header_bytes = "; - header += String(HEADER_SIZE); - header += ";\n"; - - header += "header.description = 'Each record contains 1 uint8 eventType, 1 int64 timestamp, 1 int64 software timestamp, " - "1 uint16 sourceID, 1 uint16 numChannels (n), 1 uint16 numSamples (m), 1 uint16 sortedID, 1 uint16 electrodeID, " - "1 uint16 channel, 3 uint8 color codes, 2 float32 component projections, n*m uint16 samples, n float32 channelGains, n uint16 thresholds, and 1 uint16 recordingNumber'; \n"; - - header += "header.date_created = '"; - header += generateDateString(); - header += "';\n"; - - header += "header.electrode = '"; - header += elec->getName(); - header += "';\n"; - - header += "header.num_channels = "; - header += String(elec->getNumChannels()); - header += ";\n"; - - header += "header.sampleRate = "; - header += String(elec->getSampleRate()); - header += ";\n"; - - header = header.paddedRight(' ', HEADER_SIZE); - - //std::cout << header << std::endl; - - return header; -} - -void BinaryRecording::writeEvent(int eventIndex, const MidiMessage& event) -{ - writeTTLEvent(eventIndex, event); - if (Event::getEventType(event) == EventChannel::TEXT) + else { - TextEventPtr ev = TextEvent::deserializeFromMessage(event, getEventChannel(eventIndex)); - if (ev == nullptr) return; - writeMessage(ev->getText(), ev->getSourceID(), ev->getChannel(), ev->getTimestamp()); + rec->mainFile->writeData(ev->getRawDataPointer(), info->getDataSize()); + NpyFile* chanFile = nullptr; + if (m_eventMode == EventMode::SeparateChannel) + { + chanFile = rec->channelFile; + } + else + { + chanFile = rec->timestampFile; + } + uint16 chan = ev->getChannel(); + chanFile->writeData(&chan, sizeof(uint16)); } + writeEventMetaData(ev.get(), rec->metaDataFile); } void BinaryRecording::writeTimestampSyncText(uint16 sourceID, uint16 sourceIdx, int64 timestamp, float, String text) { - writeMessage(text, sourceID, 255, timestamp); -} - -void BinaryRecording::writeMessage(String message, uint16 processorID, uint16 channel, int64 timestamp) -{ - if (messageFile == nullptr) - return; - - int msgLength = message.getNumBytesAsUTF8(); - - String timestampText(timestamp); - - diskWriteLock.enter(); - fwrite(timestampText.toUTF8(), 1, timestampText.length(), messageFile); - fwrite(" ", 1, 1, messageFile); - fwrite(message.toUTF8(), 1, msgLength, messageFile); - fwrite("\n", 1, 1, messageFile); - diskWriteLock.exit(); - + text.paddedRight(' ', 256); + m_syncTextFile->writeData(text.toUTF8(), 256); } -void BinaryRecording::writeTTLEvent(int eventIndex, const MidiMessage& event) -{ - // find file and write samples to disk - // std::cout << "Received event!" << std::endl; - - if (eventFile == nullptr) - return; - - uint8 data[16]; - //With the new external recording thread, this field has no sense. - int16 samplePos = 0; - - EventPtr ev = Event::deserializeFromMessage(event, getEventChannel(eventIndex)); - if (!ev) return; - *reinterpret_cast<int64*>(data) = ev->getTimestamp(); - *reinterpret_cast<int16*>(data + 8) = samplePos; - *(data + 10) = static_cast<uint8>(ev->getEventType()); - *(data + 11) = static_cast<uint8>(ev->getSourceID()); - *(data + 12) = (ev->getEventType() == EventChannel::TTL) ? (dynamic_cast<TTLEvent*>(ev.get())->getState() ? 1 : 0) : 0; - *(data + 13) = static_cast<uint8>(ev->getChannel()); - *reinterpret_cast<uint16*>(data + 14) = static_cast<uint16>(m_recordingNum); - - - diskWriteLock.enter(); - - fwrite(&data, // ptr - sizeof(uint8), // size of each element - 16, // count - eventFile); // ptr to FILE object - - diskWriteLock.exit(); -} void BinaryRecording::writeSpike(int electrodeIndex, const SpikeEvent* spike) { - if (spikeFileArray[electrodeIndex] == nullptr) - return; - - HeapBlock<char> spikeBuffer; const SpikeChannel* channel = getSpikeChannel(electrodeIndex); + EventRecording* rec = m_spikeFiles[m_spikeFileIndexes[electrodeIndex]]; + uint16 spikeChannel = m_spikeChannelIndexes[electrodeIndex]; int totalSamples = channel->getTotalSamples() * channel->getNumChannels(); - int numChannels = channel->getNumChannels(); - int chanSamples = channel->getTotalSamples(); - - int totalBytes = totalSamples * 2 + // account for samples - numChannels * 4 + // acount for gain - numChannels * 2 + // account for thresholds - 42; // 42, from SpikeObject.h - spikeBuffer.malloc(totalBytes); - *(spikeBuffer.getData()) = static_cast<char>(channel->getChannelType()); - *reinterpret_cast<int64*>(spikeBuffer.getData() + 1) = spike->getTimestamp(); - *reinterpret_cast<int64*>(spikeBuffer.getData() + 9) = 0; //Legacy unused value - *reinterpret_cast<uint16*>(spikeBuffer.getData() + 17) = spike->getSourceID(); - *reinterpret_cast<uint16*>(spikeBuffer.getData() + 19) = numChannels; - *reinterpret_cast<uint16*>(spikeBuffer.getData() + 21) = chanSamples; - *reinterpret_cast<uint16*>(spikeBuffer.getData() + 23) = spike->getSortedID(); - *reinterpret_cast<uint16*>(spikeBuffer.getData() + 25) = electrodeIndex; //Legacy value - *reinterpret_cast<uint16*>(spikeBuffer.getData() + 27) = 0; //Legacy unused value - zeromem(spikeBuffer.getData() + 29, 3 * sizeof(uint8)); - zeromem(spikeBuffer.getData() + 32, 2 * sizeof(float)); - *reinterpret_cast<uint16*>(spikeBuffer.getData() + 40) = channel->getSampleRate(); - - int ptrIdx = 0; - uint16* dataIntPtr = reinterpret_cast<uint16*>(spikeBuffer.getData() + 42); - const float* spikeDataPtr = spike->getDataPointer(); - for (int i = 0; i < numChannels; i++) + + + if (totalSamples > m_bufferSize) //Shouldn't happen, and if it happens it'll be slow, but better this than crashing. Will be reset on file close and reset. { - const float bitVolts = channel->getChannelBitVolts(i); - for (int j = 0; j < chanSamples; j++) - { - *(dataIntPtr + ptrIdx) = uint16(*(spikeDataPtr + ptrIdx) / bitVolts + 32768); - ptrIdx++; - } + std::cerr << "(spike) Write buffer overrun, resizing to" << totalSamples << std::endl; + m_bufferSize = totalSamples; + m_scaledBuffer.malloc(totalSamples); + m_intBuffer.malloc(totalSamples); } - ptrIdx = totalSamples * 2 + 42; - for (int i = 0; i < numChannels; i++) + double multFactor = 1 / (float(0x7fff) * channel->getChannelBitVolts(0)); + FloatVectorOperations::copyWithMultiply(m_scaledBuffer.getData(), spike->getDataPointer(), multFactor, totalSamples); + AudioDataConverters::convertFloatToInt16LE(m_scaledBuffer.getData(), m_intBuffer.getData(), totalSamples); + rec->mainFile->writeData(m_intBuffer.getData(), totalSamples*sizeof(int16)); + int64 ts = spike->getTimestamp(); + rec->timestampFile->writeData(&ts, sizeof(int64)); + NpyFile* indexFile; + NpyFile* sortedFile; + if (m_spikeMode == SpikeMode::AllInOne) { - //To get the same value as the original version - *reinterpret_cast<float*>(spikeBuffer.getData() + ptrIdx) = (int)(1.0f / channel->getChannelBitVolts(i)) * 1000; - ptrIdx += sizeof(float); + indexFile = rec->timestampFile; + sortedFile = rec->timestampFile; } - for (int i = 0; i < numChannels; i++) + else if (m_spikeMode == SpikeMode::SeparateTimestamps) { - *reinterpret_cast<int16*>(spikeBuffer.getData() + ptrIdx) = spike->getThreshold(i); - ptrIdx += sizeof(int16); + indexFile = rec->channelFile; + sortedFile = rec->channelFile; } + else + { + indexFile = rec->channelFile; + sortedFile = rec->extraFile; + } + indexFile->writeData(&spikeChannel, sizeof(uint16)); - diskWriteLock.enter(); - - fwrite(spikeBuffer, 1, totalBytes, spikeFileArray[electrodeIndex]); - - fwrite(&m_recordingNum, // ptr - 2, // size of each element - 1, // count - spikeFileArray[electrodeIndex]); // ptr to FILE object + uint16 sortedID = spike->getSortedID(); + sortedFile->writeData(&sortedID, sizeof(uint16)); - diskWriteLock.exit(); + writeEventMetaData(spike, rec->metaDataFile); } RecordEngineManager* BinaryRecording::getEngineManager() { RecordEngineManager* man = new RecordEngineManager("RAWBINARY", "Binary", &(engineFactory<BinaryRecording>)); + EngineParameter* param; + param = new EngineParameter(EngineParameter::MULTI, 0, "Spike TS/chan/sortedID File Mode|All in one|Separate timestamps|All Separated", 0); + man->addParameter(param); + param = new EngineParameter(EngineParameter::MULTI, 1, "TTL Event word file|In main file|Separated|Do not save ttl word", 0); + man->addParameter(param); + param = new EngineParameter(EngineParameter::MULTI, 2, "Other event channel file|With timestamp|Separate", 0); + man->addParameter(param); return man; } + +void BinaryRecording::setParameter(EngineParameter& parameter) +{ + multiParameter(0, reinterpret_cast<int&>(m_spikeMode)); + multiParameter(1, reinterpret_cast<int&>(m_TTLMode)); + multiParameter(2, reinterpret_cast<int&>(m_eventMode)); +} + +String BinaryRecording::jsonTypeValue(BaseType type) +{ + switch (type) + { + case BaseType::CHAR: + return "string"; + case BaseType::INT8: + return "int8"; + case BaseType::UINT8: + return "uint8"; + case BaseType::INT16: + return "int16"; + case BaseType::UINT16: + return "uint16"; + case BaseType::INT32: + return "int32"; + case BaseType::UINT32: + return "uint32"; + case BaseType::INT64: + return "int64"; + case BaseType::UINT64: + return "uint64"; + case BaseType::FLOAT: + return "float"; + case BaseType::DOUBLE: + return "double"; + default: + return String::empty; + } +} \ No newline at end of file diff --git a/Source/Plugins/BinaryWriter/BinaryRecording.h b/Source/Plugins/BinaryWriter/BinaryRecording.h index d710dd4dfa9bd8334c2a2e89b3d709694df93a66..e0493fac8926f7fff26d2a5e95ecf37fe586fcef 100644 --- a/Source/Plugins/BinaryWriter/BinaryRecording.h +++ b/Source/Plugins/BinaryWriter/BinaryRecording.h @@ -25,14 +25,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>. #include <RecordingLib.h> #include "SequentialBlockFile.h" - -//Defines for the old-stype original recording spikes and event files -#define HEADER_SIZE 1024 -#define BLOCK_LENGTH 1024 -#define VERSION 0.4 -#define VSTR(s) #s -#define VSTR2(s) VSTR(s) -#define VERSION_STRING VSTR2(VERSION) +#include "NpyFile.h" namespace BinaryRecordingEngine { @@ -52,36 +45,71 @@ namespace BinaryRecordingEngine void addSpikeElectrode(int index, const SpikeChannel* elec) override; void writeSpike(int electrodeIndex, const SpikeEvent* spike) override; void writeTimestampSyncText(uint16 sourceID, uint16 sourceIdx, int64 timestamp, float, String text) override; + void setParameter(EngineParameter& parameter) override; static RecordEngineManager* getEngineManager(); private: - typedef Array<const DataChannel*> ContinuousGroup; - void openSpikeFile(String basepath, int spikeIndex, int recordingNumber); - String generateSpikeHeader(const SpikeChannel* elec); - String generateEventHeader(); - void openMessageFile(String basepath, int recordingNumber); - void openEventFile(String basepath, int recordingNumber); - void writeTTLEvent(int eventIndex, const MidiMessage& event); - void writeMessage(String message, uint16 processorID, uint16 channel, int64 timestamp); + class EventRecording + { + public: + ScopedPointer<NpyFile> mainFile; + ScopedPointer<NpyFile> timestampFile; + ScopedPointer<NpyFile> metaDataFile; + ScopedPointer<NpyFile> channelFile; + ScopedPointer<NpyFile> extraFile; + }; + + enum SpikeMode + { + AllInOne = 0, + SeparateTimestamps = 1, + AllSeparated = 2 + }; + + enum TTLMode + { + JointWord = 0, + SeparateWord = 1, + NoWord = 2 + }; + + enum EventMode + { + JointChannel = 0, + SeparateChannel = 1 + }; + + NpyFile* createEventMetadataFile(const MetaDataEventObject* channel, String fileName, DynamicObject* jsonObject); + void createChannelMetaData(const MetaDataInfoObject* channel, DynamicObject* jsonObject); + void writeEventMetaData(const MetaDataEvent* event, NpyFile* file); + static String jsonTypeValue(BaseType type); + + SpikeMode m_spikeMode; + TTLMode m_TTLMode; + EventMode m_eventMode; + HeapBlock<float> m_scaledBuffer; HeapBlock<int16> m_intBuffer; + HeapBlock<int64> m_tsBuffer; int m_bufferSize; OwnedArray<SequentialBlockFile> m_DataFiles; Array<unsigned int> m_channelIndexes; Array<unsigned int> m_fileIndexes; - Array<ContinuousGroup> m_dataChannels; + OwnedArray<EventRecording> m_eventFiles; + OwnedArray<EventRecording> m_spikeFiles; + OwnedArray<NpyFile> m_dataTimestampFiles; + ScopedPointer<NpyFile> m_syncTextFile; + + Array<unsigned int> m_spikeFileIndexes; + Array<uint16> m_spikeChannelIndexes; - FILE* eventFile; - FILE* messageFile; - Array<FILE*> spikeFileArray; int m_recordingNum; Array<int64> m_startTS; - CriticalSection diskWriteLock; //Compile-time constants const int samplesPerBlock{ 4096 }; diff --git a/Source/Plugins/BinaryWriter/SequentialBlockFile.cpp b/Source/Plugins/BinaryWriter/SequentialBlockFile.cpp index 5b5c256ce8cd97202ecbbfc0b6b3609bc96ff6f5..15e75d057457035e27299522844a68b12ca44daf 100644 --- a/Source/Plugins/BinaryWriter/SequentialBlockFile.cpp +++ b/Source/Plugins/BinaryWriter/SequentialBlockFile.cpp @@ -44,8 +44,15 @@ SequentialBlockFile::~SequentialBlockFile() m_memBlocks.remove(0); } -bool SequentialBlockFile::openFile(File file) +bool SequentialBlockFile::openFile(String filename) { + File file(filename); + Result res = file.create(); + if (res.failed()) + { + std::cerr << "Error creating file " << filename << ":" << res.getErrorMessage() << std::endl; + return false; + } m_file = file.createOutputStream(streamBufferSize); if (!m_file) return false; diff --git a/Source/Plugins/BinaryWriter/SequentialBlockFile.h b/Source/Plugins/BinaryWriter/SequentialBlockFile.h index 31ed0b5f0ce5994f616a31cb05c34bde136a7d17..b5fd350aad449cad3b1687abe8b1abf390f84f15 100644 --- a/Source/Plugins/BinaryWriter/SequentialBlockFile.h +++ b/Source/Plugins/BinaryWriter/SequentialBlockFile.h @@ -37,7 +37,7 @@ namespace BinaryRecordingEngine SequentialBlockFile(int nChannels, int samplesPerBlock); ~SequentialBlockFile(); - bool openFile(File file); + bool openFile(String filename); bool writeChannel(uint64 startPos, int channel, int16* data, int nSamples); private: diff --git a/Source/Plugins/NWBFormat/NWBRecording.cpp b/Source/Plugins/NWBFormat/NWBRecording.cpp index 081a4ff9b487fff244ebcaed34403e83a86dfc4d..e729934bbff4de16a8c9345cb4ee97d507d44630 100644 --- a/Source/Plugins/NWBFormat/NWBRecording.cpp +++ b/Source/Plugins/NWBFormat/NWBRecording.cpp @@ -98,6 +98,10 @@ for (int i = 0; i < nEvents; i++) eventChannels.add(getEventChannel(i)); + int nSpikes = getNumRecordedSpikes(); + for (int i = 0; i < nSpikes; i++) + spikeChannels.add(getSpikeChannel(i)); + //open the file recordFile->open(getNumRecordedChannels() + continuousChannels.size() + eventChannels.size() + spikeChannels.size()); //total channels + timestamp arrays, to create a big enough buffer @@ -113,19 +117,14 @@ recordFile->stopRecording(); recordFile->close(); recordFile = nullptr; - resetChannels(false); + resetChannels(); } - void NWBRecordEngine::resetChannels() - { - resetChannels(true); - } - void NWBRecordEngine::resetChannels(bool resetSpikes) + void NWBRecordEngine::resetChannels() { - if (resetSpikes) - spikeChannels.clear(); + spikeChannels.clear(); eventChannels.clear(); continuousChannels.clear(); datasetIndexes.clear(); @@ -171,7 +170,6 @@ void NWBRecordEngine::writeTimestampSyncText(uint16 sourceID, uint16 sourceIdx, void NWBRecordEngine::addSpikeElectrode(int index,const SpikeChannel* elec) { - spikeChannels.add(elec); } void NWBRecordEngine::writeSpike(int electrodeIndex, const SpikeEvent* spike) diff --git a/Source/Plugins/NWBFormat/NWBRecording.h b/Source/Plugins/NWBFormat/NWBRecording.h index e521a34c35dbb2a4627a318bce94c7bd6d33d534..e74219d6c4f8d675574f9d6e74d6f8d45cd7453a 100644 --- a/Source/Plugins/NWBFormat/NWBRecording.h +++ b/Source/Plugins/NWBFormat/NWBRecording.h @@ -48,7 +48,6 @@ static RecordEngineManager* getEngineManager(); private: - void resetChannels(bool resetSpikes); ScopedPointer<NWBFile> recordFile; Array<int> datasetIndexes; Array<int> writeChannelIndexes; diff --git a/Source/Processors/Channel/InfoObjects.cpp b/Source/Processors/Channel/InfoObjects.cpp index c49d4b8923f15e113ef2c3cd05f69599bdd73f2c..cb847a6b0468b46e0b8d14e62230fd050c0cadae 100644 --- a/Source/Processors/Channel/InfoObjects.cpp +++ b/Source/Processors/Channel/InfoObjects.cpp @@ -30,18 +30,23 @@ HistoryObject::HistoryObject() {} NamedInfoObject::NamedInfoObject() {} //NodeInfoBase -NodeInfoBase::NodeInfoBase(uint16 id) : - m_nodeID(id) +NodeInfoBase::NodeInfoBase(uint16 id, uint16 idx) : +m_nodeID(id), m_nodeIdx(idx) {} NodeInfoBase::~NodeInfoBase() {} -unsigned int NodeInfoBase::getCurrentNodeID() const +uint16 NodeInfoBase::getCurrentNodeID() const { return m_nodeID; } +uint16 NodeInfoBase::getCurrentNodeChannelIdx() const +{ + return m_nodeIdx; +} + String NodeInfoBase::getCurrentNodeType() const { return m_currentNodeType; @@ -56,7 +61,7 @@ String NodeInfoBase::getCurrentNodeName() const HistoryObject::~HistoryObject() {} -String HistoryObject::getHistoricString() +String HistoryObject::getHistoricString() const { return m_historicString; } @@ -143,7 +148,7 @@ String NamedInfoObject::getDescription() const //InfoObjectCommon InfoObjectCommon::InfoObjectCommon(uint16 idx, uint16 typeidx, float sampleRate, const GenericProcessor* source, uint16 subproc) - : NodeInfoBase(source->getNodeId()), + : NodeInfoBase(source->getNodeId(), idx), SourceProcessorInfo(source, subproc), m_sourceIndex(idx), m_sourceTypeIndex(typeidx), @@ -170,6 +175,29 @@ uint16 InfoObjectCommon::getSourceTypeIndex() const return m_sourceTypeIndex; } +bool InfoObjectCommon::operator==(const InfoObjectCommon& other) const +{ + return isEqual(other); +} + +bool InfoObjectCommon::isEqual(const InfoObjectCommon& other) const +{ + return isEqual(other, false); +} + +bool InfoObjectCommon::isSimilar(const InfoObjectCommon& other) const +{ + return isEqual(other, true); +} + +bool InfoObjectCommon::isEqual(const InfoObjectCommon& other, bool similar) const +{ + if (getInfoObjectType() != other.getInfoObjectType()) return false; + if (m_sampleRate != other.m_sampleRate) return false; + if (!similar && getIdentifier().trim() != other.getIdentifier().trim()) return false; + return checkEqual(other, similar); +} + //DataChannel DataChannel::DataChannel(DataChannelTypes type, float sampleRate, GenericProcessor* source, uint16 subproc) : @@ -297,6 +325,16 @@ void DataChannel::setDefaultNameAndDescription() setIdentifier("genericdata.continuous"); } +bool DataChannel::checkEqual(const InfoObjectCommon& other, bool similar) const +{ + const DataChannel& o = dynamic_cast<const DataChannel&>(other); + if (m_bitVolts != o.m_bitVolts) return false; + if (!similar && m_unitName != o.m_unitName) return false; + if (similar) + return hasSimilarMetadata(o); + else return hasSameMetadata(o); +} + //EventChannel EventChannel::EventChannel(EventChannelTypes type, unsigned int nChannels, unsigned int dataLength, float sampleRate, GenericProcessor* source, uint16 subproc) : InfoObjectCommon(source->eventChannelCount++, source->eventChannelTypeCount[type]++, sampleRate, source, subproc), @@ -418,6 +456,56 @@ void EventChannel::setDefaultNameAndDescription() setIdentifier("genericevent"); } + +BaseType EventChannel::getEquivalentMetaDataType(const EventChannel& ev) +{ + switch (ev.getChannelType()) + { + case EventChannel::TEXT: + return MetaDataDescriptor::CHAR; + case EventChannel::INT8_ARRAY: + return MetaDataDescriptor::INT8; + case EventChannel::UINT8_ARRAY: + return MetaDataDescriptor::UINT8; + case EventChannel::INT16_ARRAY: + return MetaDataDescriptor::INT16; + case EventChannel::UINT16_ARRAY: + return MetaDataDescriptor::UINT16; + case EventChannel::INT32_ARRAY: + return MetaDataDescriptor::INT32; + case EventChannel::UINT32_ARRAY: + return MetaDataDescriptor::UINT32; + case EventChannel::INT64_ARRAY: + return MetaDataDescriptor::INT64; + case EventChannel::UINT64_ARRAY: + return MetaDataDescriptor::UINT64; + case EventChannel::FLOAT_ARRAY: + return MetaDataDescriptor::FLOAT; + case EventChannel::DOUBLE_ARRAY: + return MetaDataDescriptor::DOUBLE; + default: + return MetaDataDescriptor::UINT8; + } +} + +BaseType EventChannel::getEquivalentMetaDataType() const +{ + return getEquivalentMetaDataType(*this); +} + +bool EventChannel::checkEqual(const InfoObjectCommon& other, bool similar) const +{ + const EventChannel& o = dynamic_cast<const EventChannel&>(other); + if (m_type != o.m_type) return false; + if (m_numChannels != o.m_numChannels) return false; + if (m_length != o.m_length) return false; + if (similar && !hasSimilarMetadata(o)) return false; + if (!similar && !hasSameMetadata(o)) return false; + if (similar && !hasSimilarEventMetadata(o)) return false; + if (!similar && !hasSameEventMetadata(o)) return false; + return true; +} + //SpikeChannel SpikeChannel::SpikeChannel(ElectrodeTypes type, GenericProcessor* source, const Array<const DataChannel*>& sourceChannels, uint16 subproc) @@ -428,7 +516,7 @@ SpikeChannel::SpikeChannel(ElectrodeTypes type, GenericProcessor* source, const jassert(n == getNumChannels(type)); for (int i = 0; i < n; i++) { - sourceChannelInfo info; + SourceChannelInfo info; const DataChannel* chan = sourceChannels[i]; info.processorID = chan->getSourceNodeID(); info.subProcessorID = chan->getSubProcessorIdx(); @@ -452,7 +540,7 @@ SpikeChannel::ElectrodeTypes SpikeChannel::getChannelType() const return m_type; } -Array<sourceChannelInfo> SpikeChannel::getSourceChannelInfo() const +Array<SourceChannelInfo> SpikeChannel::getSourceChannelInfo() const { return m_sourceInfo; } @@ -549,6 +637,27 @@ void SpikeChannel::setDefaultNameAndDescription() setIdentifier("spikesource"); } +bool SpikeChannel::checkEqual(const InfoObjectCommon& other, bool similar) const +{ + const SpikeChannel& o = dynamic_cast<const SpikeChannel&>(other); + if (m_type != o.m_type) return false; + if (m_numPostSamples != o.m_numPostSamples) return false; + if (m_numPreSamples != o.m_numPreSamples) return false; + + int nChans = m_channelBitVolts.size(); + if (nChans != o.m_channelBitVolts.size()) return false; + for (int i = 0; i < nChans; i++) + { + if (m_channelBitVolts[i] != o.m_channelBitVolts[i]) return false; + } + + if (similar && !hasSimilarMetadata(o)) return false; + if (!similar && !hasSameMetadata(o)) return false; + if (similar && !hasSimilarEventMetadata(o)) return false; + if (!similar && !hasSameEventMetadata(o)) return false; + return true; +} + //ConfigurationObject ConfigurationObject::ConfigurationObject(String identifier, GenericProcessor* source, uint16 subproc) : SourceProcessorInfo(source, subproc) diff --git a/Source/Processors/Channel/InfoObjects.h b/Source/Processors/Channel/InfoObjects.h index a3e1679b52941ab288494aa98d7b47c58ca6248b..cdda5769ef0e0546a494e5c4617b5ae97ab67666 100644 --- a/Source/Processors/Channel/InfoObjects.h +++ b/Source/Processors/Channel/InfoObjects.h @@ -35,7 +35,7 @@ class GenericProcessor; /** Structure with the basic info that identifies a channel */ -struct sourceChannelInfo +struct SourceChannelInfo { uint16 processorID; uint16 subProcessorID; @@ -49,15 +49,19 @@ class PLUGIN_API NodeInfoBase public: virtual ~NodeInfoBase(); /** Gets the ID of the processor which currently owns this copy of the info object */ - unsigned int getCurrentNodeID() const; + uint16 getCurrentNodeID() const; + /** Gets the index of this channel in the processor which currently owns this copy of the info object */ + uint16 getCurrentNodeChannelIdx() const; /** Gets the type of the processor which currently owns this copy of the info object */ String getCurrentNodeType() const; /** Gets the name of the processor which currently owns this copy of the info object */ String getCurrentNodeName() const; protected: NodeInfoBase() = delete; - NodeInfoBase(uint16 id); + NodeInfoBase(uint16 id, uint16 idx); +private: uint16 m_nodeID{ 0 }; + uint16 m_nodeIdx{ 0 }; String m_currentNodeType; String m_currentNodeName; }; @@ -71,7 +75,7 @@ protected: public: virtual ~HistoryObject(); /** Returns the historic string */ - String getHistoricString(); + String getHistoricString() const; /** Adds a new entry in the historic string*/ void addToHistoricString(String entry); @@ -171,7 +175,13 @@ public: virtual InfoObjectType getInfoObjectType() const = 0; + bool isEqual(const InfoObjectCommon& other) const; + bool isSimilar(const InfoObjectCommon& other) const; + bool operator==(const InfoObjectCommon& other) const; + private: + bool isEqual(const InfoObjectCommon& other, bool similar) const; + virtual bool checkEqual(const InfoObjectCommon& other, bool similar) const = 0; /** Index of the object in the source processor */ const uint16 m_sourceIndex; /** Index of this particular subtype in the source processor */ @@ -251,6 +261,7 @@ public: InfoObjectType getInfoObjectType() const override; void setDefaultNameAndDescription() override; private: + bool checkEqual(const InfoObjectCommon& other, bool similar) const override; const DataChannelTypes m_type; float m_bitVolts{ 1.0f }; bool m_isEnabled{ true }; @@ -341,9 +352,16 @@ public: /** Gets the size in bytes of an element depending of the type*/ static size_t getTypeByteSize(EventChannelTypes type); + /** Handy method to get an equivalente metadata value type for the main event data*/ + static BaseType getEquivalentMetaDataType(const EventChannel& ev); + + /** Handy method to get an equivalente metadata value type for the main event data*/ + BaseType getEquivalentMetaDataType() const; + InfoObjectType getInfoObjectType() const override; void setDefaultNameAndDescription() override; private: + bool checkEqual(const InfoObjectCommon& other, bool similar) const override; const EventChannelTypes m_type; unsigned int m_numChannels{ 1 }; size_t m_dataSize{ 1 }; @@ -378,7 +396,7 @@ public: ElectrodeTypes getChannelType() const; /** Returns an array with info about the channels from which the spikes originate */ - Array<sourceChannelInfo> getSourceChannelInfo() const; + Array<SourceChannelInfo> getSourceChannelInfo() const; /** Sets the number of samples, pre and post peak */ void setNumSamples(unsigned int preSamples, unsigned int postSamples); @@ -413,8 +431,9 @@ public: InfoObjectType getInfoObjectType() const override; void setDefaultNameAndDescription() override; private: + bool checkEqual(const InfoObjectCommon& other, bool similar) const override; const ElectrodeTypes m_type; - Array<sourceChannelInfo> m_sourceInfo; + Array<SourceChannelInfo> m_sourceInfo; unsigned int m_numPreSamples{ 8 }; unsigned int m_numPostSamples{ 32 }; Array<float> m_channelBitVolts; diff --git a/Source/Processors/Channel/MetaData.cpp b/Source/Processors/Channel/MetaData.cpp index 5dca331398785c7bcdfb1643bddcd620919b5858..8c6711b16d9c5775ed510c9e130fa29d3b436a46 100644 --- a/Source/Processors/Channel/MetaData.cpp +++ b/Source/Processors/Channel/MetaData.cpp @@ -76,7 +76,7 @@ String MetaDataDescriptor::getName() const { return m_name; } String MetaDataDescriptor::getDescription() const { return m_description; } String MetaDataDescriptor::getIdentifier() const { return m_identifier; } -bool MetaDataDescriptor::isEqual(const MetaDataDescriptor& other) const +bool MetaDataDescriptor::isSimilar(const MetaDataDescriptor& other) const { if ((m_type == other.m_type) && (m_length == other.m_length)) return true; @@ -84,6 +84,14 @@ bool MetaDataDescriptor::isEqual(const MetaDataDescriptor& other) const return false; } +bool MetaDataDescriptor::isEqual(const MetaDataDescriptor& other) const +{ + if ((m_type == other.m_type) && (m_length == other.m_length) && m_identifier.trim() == other.m_identifier.trim()) + return true; + else + return false; +} + bool MetaDataDescriptor::operator==(const MetaDataDescriptor& other) const { return isEqual(other); @@ -209,7 +217,7 @@ void MetaDataValue::setValue(const String& data) void MetaDataValue::getValue(String& data) const { jassert(m_type == MetaDataDescriptor::CHAR); - data.createStringFromData(m_data.getData(), m_length); + data = String::createStringFromData(m_data.getData(), m_length); } template <typename T> @@ -335,6 +343,36 @@ int MetaDataInfoObject::findMetaData(MetaDataDescriptor::MetaDataTypes type, uns return -1; } +bool MetaDataInfoObject::hasSameMetadata(const MetaDataInfoObject& other) const +{ + return checkMetaDataCoincidence(other, false); +} + +bool MetaDataInfoObject::hasSimilarMetadata(const MetaDataInfoObject& other) const +{ + return checkMetaDataCoincidence(other, true); +} + +bool MetaDataInfoObject::checkMetaDataCoincidence(const MetaDataInfoObject& other, bool similar) const +{ + int nMetaData = m_metaDataDescriptorArray.size(); + if (nMetaData != other.m_metaDataDescriptorArray.size()) return false; + for (int i = 0; i < nMetaData; i++) + { + MetaDataDescriptorPtr md = m_metaDataDescriptorArray[i]; + MetaDataDescriptorPtr mdo = other.m_metaDataDescriptorArray[i]; + if (similar) + { + if (!md->isSimilar(*mdo)) return false; + } + else + { + if (!md->isEqual(*mdo)) return false; + } + } + return true; +} + //MetaDataEventObject MetaDataEventObject::MetaDataEventObject() {} @@ -398,6 +436,36 @@ int MetaDataEventObject::findEventMetaData(MetaDataDescriptor::MetaDataTypes typ return -1; } +bool MetaDataEventObject::hasSameEventMetadata(const MetaDataEventObject& other) const +{ + return checkMetaDataCoincidence(other, false); +} + +bool MetaDataEventObject::hasSimilarEventMetadata(const MetaDataEventObject& other) const +{ + return checkMetaDataCoincidence(other, true); +} + +bool MetaDataEventObject::checkMetaDataCoincidence(const MetaDataEventObject& other, bool similar) const +{ + int nMetaData = m_eventMetaDataDescriptorArray.size(); + if (nMetaData != other.m_eventMetaDataDescriptorArray.size()) return false; + for (int i = 0; i < nMetaData; i++) + { + MetaDataDescriptorPtr md = m_eventMetaDataDescriptorArray[i]; + MetaDataDescriptorPtr mdo = other.m_eventMetaDataDescriptorArray[i]; + if (similar) + { + if (!md->isSimilar(*mdo)) return false; + } + else + { + if (!md->isEqual(*mdo)) return false; + } + } + return true; +} + size_t MetaDataEventObject::getMaxEventMetaDataSize() const { return m_maxSize; diff --git a/Source/Processors/Channel/MetaData.h b/Source/Processors/Channel/MetaData.h index 7ecbb0b164667dd86e86ef57eebf310a3cea54f8..88f316715f1a624a4b106f4d20dd78ed8c868486 100644 --- a/Source/Processors/Channel/MetaData.h +++ b/Source/Processors/Channel/MetaData.h @@ -85,8 +85,12 @@ public: /** Gets the machine-readable identifier for this field */ String getIdentifier() const; + /** Returns true if both descriptors have the same type, length and identifier */ bool isEqual(const MetaDataDescriptor& other) const; + /** Returns true if both descriptors have the same type, length and identifier */ bool operator==(const MetaDataDescriptor& other) const; + /** Returns true if both descriptors have the same type and length, regardless of the identifier */ + bool isSimilar(const MetaDataDescriptor& other) const; static size_t getTypeSize(MetaDataTypes type); private: @@ -184,9 +188,13 @@ public: const MetaDataValue* getMetaDataValue(int index) const; int findMetaData(MetaDataDescriptor::MetaDataTypes type, unsigned int length, String identifier = String::empty) const; const int getMetaDataCount() const; + bool hasSameMetadata(const MetaDataInfoObject& other) const; + bool hasSimilarMetadata(const MetaDataInfoObject& other) const; protected: MetaDataDescriptorArray m_metaDataDescriptorArray; MetaDataValueArray m_metaDataValueArray; +private: + bool checkMetaDataCoincidence(const MetaDataInfoObject& other, bool similar) const; }; class PLUGIN_API MetaDataEventLock @@ -215,11 +223,15 @@ public: int getEventMetaDataCount() const; //gets the largest metadata size, which can be useful to reserve buffers in advance size_t getMaxEventMetaDataSize() const; + bool hasSameEventMetadata(const MetaDataEventObject& other) const; + bool hasSimilarEventMetadata(const MetaDataEventObject& other) const; protected: MetaDataDescriptorArray m_eventMetaDataDescriptorArray; MetaDataEventObject(); size_t m_totalSize{ 0 }; size_t m_maxSize{ 0 }; +private: + bool checkMetaDataCoincidence(const MetaDataEventObject& other, bool similar) const; }; //And the base from which event objects can hold their metadata before serializing @@ -239,4 +251,6 @@ protected: //Helper function to compare identifier strings bool compareIdentifierStrings(const String& identifier, const String& compareWith); +typedef MetaDataDescriptor::MetaDataTypes BaseType; + #endif \ No newline at end of file diff --git a/Source/Processors/Events/Events.cpp b/Source/Processors/Events/Events.cpp index b2b11a2115621e22a8f0547b628d71ce5f141c76..dbf122acb86a0d8e8cbf9ee93218156d3c81aff4 100644 --- a/Source/Processors/Events/Events.cpp +++ b/Source/Processors/Events/Events.cpp @@ -218,6 +218,14 @@ String SystemEvent::getSyncText(const MidiMessage& msg) } //Event +Event::Event(const Event& other) + : Event(other) +{ + size_t size = other.m_channelInfo->getDataSize(); + m_data.malloc(size); + memcpy(m_data.getData(), other.m_data.getData(), size); +} + EventChannel::EventChannelTypes Event::getEventType() const { return m_eventType; @@ -300,6 +308,11 @@ bool Event::createChecks(const EventChannel* channelInfo, EventChannel::EventCha return true; } +const void* Event::getRawDataPointer() const +{ + return m_data.getData(); +} + //TTLEvent TTLEvent::TTLEvent(const EventChannel* channelInfo, int64 timestamp, uint16 channel, const void* eventData) @@ -452,21 +465,22 @@ TTLEventPtr TTLEvent::deserializeFromMessage(const MidiMessage& msg, const Event //TextEvent TextEvent::TextEvent(const EventChannel* channelInfo, int64 timestamp, uint16 channel, const String& text) - : Event(channelInfo, timestamp, channel), - m_text(text) + : Event(channelInfo, timestamp, channel) { + m_data.calloc(channelInfo->getDataSize()); + text.copyToUTF8(m_data.getData(), channelInfo->getDataSize()); } TextEvent::~TextEvent() {} TextEvent::TextEvent(const TextEvent& other) - : Event(other), - m_text(other.m_text) -{} + : Event(other) +{ +} String TextEvent::getText() const { - return m_text; + return String(m_data.getData(), m_channelInfo->getLength()); } void TextEvent::serialize(void* dstBuffer, size_t dstSize) const @@ -477,10 +491,10 @@ void TextEvent::serialize(void* dstBuffer, size_t dstSize) const size_t dataSize = m_channelInfo->getDataSize(); size_t eventSize = dataSize + EVENT_BASE_SIZE; - size_t stringSize = m_text.getNumBytesAsUTF8(); - memcpy((buffer + EVENT_BASE_SIZE), m_text.toUTF8(), stringSize); - if ((dataSize - stringSize) > 0) - zeromem((buffer + EVENT_BASE_SIZE + stringSize), dataSize - stringSize); + //size_t stringSize = m_text.getNumBytesAsUTF8(); + memcpy((buffer + EVENT_BASE_SIZE), m_data, dataSize); +// if ((dataSize - stringSize) > 0) +// zeromem((buffer + EVENT_BASE_SIZE + stringSize), dataSize - stringSize); serializeMetaData(buffer + eventSize); } @@ -492,7 +506,7 @@ TextEventPtr TextEvent::createTextEvent(const EventChannel* channelInfo, int64 t return nullptr; } - if (text.length() > channelInfo->getLength()) + if (text.getNumBytesAsUTF8() > channelInfo->getDataSize()) { jassertfalse; return nullptr; @@ -509,7 +523,7 @@ TextEventPtr TextEvent::createTextEvent(const EventChannel* channelInfo, int64 t return nullptr; } - if (text.length() > channelInfo->getLength()) + if (text.getNumBytesAsUTF8() > channelInfo->getDataSize()) { jassertfalse; return nullptr; @@ -600,9 +614,6 @@ BinaryEvent::BinaryEvent(const BinaryEvent& other) :Event(other), m_type(other.m_type) { - size_t size = other.m_channelInfo->getDataSize(); - m_data.malloc(size); - memcpy(m_data.getData(), other.m_data.getData(), size); } BinaryEvent::~BinaryEvent() {} diff --git a/Source/Processors/Events/Events.h b/Source/Processors/Events/Events.h index 2a5076e2b84ac63265ac038851039783381ee6f5..45a5fadbe2b4442b98be06ee820f120473099a8e 100644 --- a/Source/Processors/Events/Events.h +++ b/Source/Processors/Events/Events.h @@ -134,12 +134,18 @@ class PLUGIN_API Event public: virtual ~Event(); virtual void serialize(void* dstBuffer, size_t dstSize) const override = 0; + Event(const Event& other); + Event& operator=(const Event&) = delete; + EventChannel::EventChannelTypes getEventType() const; const EventChannel* getChannelInfo() const; /** Gets the channel that triggered the event */ uint16 getChannel() const; + /* Gets the raw data payload */ + const void* getRawDataPointer() const; + static EventChannel::EventChannelTypes getEventType(const MidiMessage& msg); static EventPtr deserializeFromMessage(const MidiMessage& msg, const EventChannel* channelInfo); @@ -154,6 +160,8 @@ protected: const EventChannel* m_channelInfo; const EventChannel::EventChannelTypes m_eventType; + HeapBlock<char> m_data; + }; typedef ScopedPointer<TTLEvent> TTLEventPtr; @@ -179,7 +187,6 @@ private: TTLEvent() = delete; TTLEvent(const EventChannel* channelInfo, int64 timestamp, uint16 channel, const void* eventData); - HeapBlock<char> m_data; JUCE_LEAK_DETECTOR(TTLEvent); }; @@ -202,7 +209,6 @@ private: TextEvent() = delete; TextEvent(const EventChannel* channelInfo, int64 timestamp, uint16 channel, const String& text); - const String m_text; JUCE_LEAK_DETECTOR(TextEvent); }; @@ -235,7 +241,6 @@ private: template<typename T> static EventChannel::EventChannelTypes getType(); - HeapBlock<char> m_data; const EventChannel::EventChannelTypes m_type; JUCE_LEAK_DETECTOR(BinaryEvent); }; diff --git a/Source/Processors/GenericProcessor/GenericProcessor.cpp b/Source/Processors/GenericProcessor/GenericProcessor.cpp index b0f11fd1ea3f1ac4f200b4f4cdad0a055f86f7ec..7597465ddbcb21140cc89120722b3ccd4356ed18 100755 --- a/Source/Processors/GenericProcessor/GenericProcessor.cpp +++ b/Source/Processors/GenericProcessor/GenericProcessor.cpp @@ -412,6 +412,7 @@ void GenericProcessor::updateChannelIndexes(bool updateNodeID) if (updateNodeID) { channel->m_nodeID = nodeId; + channel->m_nodeIdx = i; channel->m_currentNodeName = getName(); channel->m_currentNodeType = getName(); //Fix when the ability to name individual processors is implemented } @@ -424,6 +425,7 @@ void GenericProcessor::updateChannelIndexes(bool updateNodeID) if (updateNodeID) { channel->m_nodeID = nodeId; + channel->m_nodeIdx = i; channel->m_currentNodeName = getName(); channel->m_currentNodeType = getName(); //Fix when the ability to name individual processors is implemented } @@ -436,6 +438,7 @@ void GenericProcessor::updateChannelIndexes(bool updateNodeID) if (updateNodeID) { channel->m_nodeID = nodeId; + channel->m_nodeIdx = i; channel->m_currentNodeName = getName(); channel->m_currentNodeType = getName(); //Fix when the ability to name individual processors is implemented } diff --git a/Source/Processors/RecordNode/EngineConfigWindow.cpp b/Source/Processors/RecordNode/EngineConfigWindow.cpp index 6fb06d7c12ae92d68ca98f87c3f2c4e7285efa9d..ffd7e00540c6f6a6801b643f406c41f75154ba71 100644 --- a/Source/Processors/RecordNode/EngineConfigWindow.cpp +++ b/Source/Processors/RecordNode/EngineConfigWindow.cpp @@ -33,11 +33,26 @@ EngineParameterComponent::EngineParameterComponent(EngineParameter& param) but->setBounds(120,0,100,20); addAndMakeVisible(but); control = but; + name = param.name; } + else if (param.type == EngineParameter::MULTI) + { + ComboBox* box = new ComboBox(); + box->setBounds(120, 0, 100, 20); + StringArray options = StringArray::fromTokens(param.name, "|", "\""); + name = options[0]; + options.remove(0); + box->addItemList(options, 1); + box->setSelectedId(param.multiParam.value + 1, dontSendNotification); + box->setEditableText(false); + addAndMakeVisible(box); + control = box; + } else { Label* lab = new Label(); lab->setFont(Font("Small Text",10,Font::plain)); + name = param.name; switch (param.type) { case EngineParameter::BOOL: @@ -63,7 +78,7 @@ EngineParameterComponent::EngineParameterComponent(EngineParameter& param) addAndMakeVisible(lab); control = lab; } - this->setTooltip(param.name); + this->setTooltip(name); } EngineParameterComponent::~EngineParameterComponent() @@ -74,7 +89,7 @@ void EngineParameterComponent::paint(Graphics& g) { g.setColour(Colours::black); g.setFont(13); - g.drawText(parameter.name+":",0,0,100,30,Justification::left,true); + g.drawText(name+":",0,0,100,30,Justification::left,true); } void EngineParameterComponent::labelTextChanged(Label* l) @@ -104,25 +119,28 @@ void EngineParameterComponent::saveValue() switch (parameter.type) { case EngineParameter::BOOL: - parameter.boolParam.value = ((ToggleButton*)control.get())->getToggleState(); + parameter.boolParam.value = static_cast<ToggleButton*>(control.get())->getToggleState(); break; case EngineParameter::INT: - parameter.intParam.value = ((Label*)control.get())->getText().getIntValue(); + parameter.intParam.value = static_cast<Label*>(control.get())->getText().getIntValue(); if (parameter.intParam.value < parameter.intParam.min) parameter.intParam.value = parameter.intParam.min; if (parameter.intParam.value > parameter.intParam.max) parameter.intParam.value = parameter.intParam.max; break; case EngineParameter::FLOAT: - parameter.floatParam.value = ((Label*)control.get())->getText().getFloatValue(); + parameter.floatParam.value = static_cast<Label*>(control.get())->getText().getFloatValue(); if (parameter.floatParam.value < parameter.floatParam.min) parameter.floatParam.value = parameter.floatParam.min; if (parameter.floatParam.value > parameter.floatParam.max) parameter.floatParam.value = parameter.floatParam.max; break; case EngineParameter::STR: - parameter.strParam.value = ((Label*)control.get())->getText(); + parameter.strParam.value = static_cast<Label*>(control.get())->getText(); break; + case EngineParameter::MULTI: + parameter.multiParam.value = static_cast<ComboBox*>(control.get())->getSelectedId() - 1; + break; } } @@ -135,7 +153,7 @@ EngineConfigComponent::EngineConfigComponent(RecordEngineManager* man, int heigh for (int i = 0; i < man->getNumParameters(); i++) { EngineParameterComponent* par = new EngineParameterComponent(man->getParameter(i)); - if (man->getParameter(i).type == EngineParameter::STR) + if (man->getParameter(i).type == EngineParameter::STR || man->getParameter(i).type == EngineParameter::MULTI) hasString=true; par->setBounds(10,10+40*i,300,30); addAndMakeVisible(par); diff --git a/Source/Processors/RecordNode/EngineConfigWindow.h b/Source/Processors/RecordNode/EngineConfigWindow.h index d57975b7bf8fed8dad5eb7a7f8fff0ddfbe0551f..0e3e1925bb1e1714bea8cdf213d903d6d5749b5f 100644 --- a/Source/Processors/RecordNode/EngineConfigWindow.h +++ b/Source/Processors/RecordNode/EngineConfigWindow.h @@ -43,6 +43,7 @@ private: ScopedPointer<Component> control; EngineParameter::EngineParameterType type; EngineParameter& parameter; + String name; JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR(EngineParameterComponent); }; diff --git a/Source/Processors/RecordNode/OriginalRecording.h b/Source/Processors/RecordNode/OriginalRecording.h index 3730992ec2d91d1e0aef513f8eedc5b6fb7849aa..849e479f1dac1fe796fc2064effb8be7c68b607e 100644 --- a/Source/Processors/RecordNode/OriginalRecording.h +++ b/Source/Processors/RecordNode/OriginalRecording.h @@ -44,7 +44,7 @@ public: OriginalRecording(); ~OriginalRecording(); - void setParameter(EngineParameter& parameter); + void setParameter(EngineParameter& parameter) override; String getEngineID() const override; void openFiles(File rootFolder, int experimentNumber, int recordingNumber) override; void closeFiles() override; diff --git a/Source/Processors/RecordNode/RecordEngine.cpp b/Source/Processors/RecordNode/RecordEngine.cpp index 6aaef305fd4b5b3db8118d256a01200f92536645..c627fd54b49c5c48ed9af8ae17ea8c52cc93a638 100644 --- a/Source/Processors/RecordNode/RecordEngine.cpp +++ b/Source/Processors/RecordNode/RecordEngine.cpp @@ -108,6 +108,11 @@ int RecordEngine::getNumRecordedEvents() const return AccessClass::getProcessorGraph()->getRecordNode()->getTotalEventChannels(); } +int RecordEngine::getNumRecordedSpikes() const +{ + return AccessClass::getProcessorGraph()->getRecordNode()->getTotalSpikeChannels(); +} + void RecordEngine::registerSpikeSource (const GenericProcessor* processor) {} int RecordEngine::getNumRecordedProcessors() const @@ -186,6 +191,10 @@ EngineParameter::EngineParameter (EngineParameter::EngineParameterType paramType { strParam.value = defaultValue; } + else if (paramType == MULTI) + { + multiParam.value = defaultValue; + } } @@ -209,6 +218,9 @@ void EngineParameter::restoreDefault() strParam.value = def; break; + case MULTI: + multiParam.value = def; + default: break; } @@ -329,6 +341,10 @@ void RecordEngineManager::saveParametersToXml (XmlElement* xml) param->setAttribute ("value", parameters[i]->strParam.value); break; + case EngineParameter::MULTI: + param->setAttribute("type", "multi"); + param->setAttribute("value", parameters[i]->multiParam.value); + default: break; } @@ -363,6 +379,11 @@ void RecordEngineManager::loadParametersFromXml (XmlElement* xml) { parameters[i]->strParam.value = xmlNode->getStringAttribute ("value"); } + else if ((xmlNode->getStringAttribute("type") == "multi") + && (parameters[i]->type == EngineParameter::MULTI)) + { + parameters[i]->multiParam.value = xmlNode->getIntAttribute("value"); + } } } } diff --git a/Source/Processors/RecordNode/RecordEngine.h b/Source/Processors/RecordNode/RecordEngine.h index f6864ec06021f644be3c7c78aaa81274926f5f7b..d6234c07dc506af898501cd0002e0f1e2e4f4785 100644 --- a/Source/Processors/RecordNode/RecordEngine.h +++ b/Source/Processors/RecordNode/RecordEngine.h @@ -38,6 +38,8 @@ v = parameter.floatParam.value #define strParameter(i,v) if ((parameter.id == i) && (parameter.type == EngineParameter::STR)) \ v = parameter.strParam.value +#define multiParameter(i,v) if ((parameter.id == i) && (parameter.type == EngineParameter::MULTI)) \ + v = parameter.multiParam.value struct RecordProcessorInfo { @@ -165,7 +167,7 @@ protected: /** Generate a Matlab-compatible datestring */ String generateDateString() const; - /** Gets the current block's first timestamp for a given channel */ + /** Gets the current block's first timestamp for a given recorded channel */ int64 getTimestamp (int channel) const; /** Gets the actual channel number from a recorded channel index */ @@ -178,8 +180,9 @@ protected: (right now all channels are recorded) */ int getNumRecordedEvents() const; - /** TODO: to fill when the probe system is implemented*/ - //int getNumRecordedSpikes() const; + /** Gets the number of recorded spike channels + (right now all channels are recorded) */ + int getNumRecordedSpikes() const; /** Gets the number of processors being recorded */ @@ -219,7 +222,7 @@ typedef RecordEngine* (*EngineCreator)(); struct PLUGIN_API EngineParameter { public: - enum EngineParameterType { STR, INT, FLOAT, BOOL }; + enum EngineParameterType { STR, INT, FLOAT, BOOL, MULTI }; EngineParameter (EngineParameterType paramType, int paramId, @@ -250,6 +253,11 @@ public: { bool value; } boolParam; + + struct + { + int value; + } multiParam; }; //Strings can't be inside an union. This means wasting a bit of memory, but adds more safety than using char*