diff --git a/.gitignore b/.gitignore index b176678ddf..4407802532 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,7 @@ # Files and folders to ignore +Builds_Backup_Ubuntu_18.04_2022-06-29/ + # 1. intermediate files *.o *.d diff --git a/Builds/Linux/Makefile b/Builds/Linux/Makefile index 4cb2fc7ab8..39bf09f6f3 100644 --- a/Builds/Linux/Makefile +++ b/Builds/Linux/Makefile @@ -4,6 +4,8 @@ # (this disables dependency generation if multiple architectures are set) DEPFLAGS := $(if $(word 2, $(TARGET_ARCH)), , -MMD) +CONFIG=Release + ifndef CONFIG CONFIG=Debug endif @@ -23,7 +25,7 @@ ifeq ($(CONFIG),Debug) CXXFLAGS += $(CFLAGS) -std=c++11 LDFLAGS += $(TARGET_ARCH) -L$(BINDIR) -L$(LIBDIR) -L/usr/X11R6/lib/ -L/usr/local/include -lGL -lX11 -lXext -lXinerama -lasound -ldl -lfreetype -lpthread -lrt -ldl -lXext -lGLU -rdynamic -fPIC -Wl,-rpath,'$$ORIGIN' - TARGET := open-ephys + TARGET := open-ephys-debug BLDCMD = $(CXX) -o $(OUTDIR)/$(TARGET) $(OBJECTS) $(LDFLAGS) $(RESOURCES) $(TARGET_ARCH) CLEANCMD = rm -rf $(OUTDIR)/$(TARGET) $(OBJDIR) endif @@ -43,7 +45,7 @@ ifeq ($(CONFIG),Release) CXXFLAGS += $(CFLAGS) -std=c++11 LDFLAGS += $(TARGET_ARCH) -L$(BINDIR) -L$(LIBDIR) -fvisibility=hidden -L/usr/X11R6/lib/ -lGL -lX11 -lXext -lXinerama -lasound -ldl -lfreetype -lpthread -lrt -ldl -lXext -lGLU -rdynamic -fPIC -Wl,-rpath,'$$ORIGIN' - TARGET := open-ephys-release + TARGET := open-ephys BLDCMD = $(CXX) -o $(OUTDIR)/$(TARGET) $(OBJECTS) $(LDFLAGS) $(RESOURCES) $(TARGET_ARCH) CLEANCMD = rm -rf $(OUTDIR)/$(TARGET) $(OBJDIR) endif diff --git a/Builds/Linux/Makefile.plugins b/Builds/Linux/Makefile.plugins index d02d1bd67d..0a1a08cd69 100644 --- a/Builds/Linux/Makefile.plugins +++ b/Builds/Linux/Makefile.plugins @@ -7,6 +7,8 @@ DEPFLAGS := $(if $(word 2, $(TARGET_ARCH)), , -MMD) PLUGIN_DIR := ../../Source/Plugins COMMON_DIR := ../../Source/Plugins/CommonLibs +CONFIG=Release + ifndef CONFIG CONFIG=Debug endif diff --git a/JuceLibraryCode/modules/juce_audio_formats/codecs/flac/libFLAC/lpc_flac.c b/JuceLibraryCode/modules/juce_audio_formats/codecs/flac/libFLAC/lpc_flac.c index 87e2321e67..a1ca961418 100644 --- a/JuceLibraryCode/modules/juce_audio_formats/codecs/flac/libFLAC/lpc_flac.c +++ b/JuceLibraryCode/modules/juce_audio_formats/codecs/flac/libFLAC/lpc_flac.c @@ -57,7 +57,7 @@ #elif defined(__GNUC__) #define copysign __builtin_copysign #endif -static inline long int lround(double x) { +static inline long int jlround(double x) { return (long)(x + copysign (0.5, x)); } /* If this fails, we are in the presence of a mid 90's compiler, move along... */ @@ -210,7 +210,7 @@ int FLAC__lpc_quantize_coefficients(const FLAC__real lp_coeff[], unsigned order, FLAC__int32 q; for(i = 0; i < order; i++) { error += lp_coeff[i] * (1 << *shift); - q = lround(error); + q = jlround(error); #ifdef FLAC__OVERFLOW_DETECT if(q > qmax+1) /* we expect q==qmax+1 occasionally due to rounding */ @@ -239,7 +239,7 @@ int FLAC__lpc_quantize_coefficients(const FLAC__real lp_coeff[], unsigned order, #endif for(i = 0; i < order; i++) { error += lp_coeff[i] / (1 << nshift); - q = lround(error); + q = jlround(error); #ifdef FLAC__OVERFLOW_DETECT if(q > qmax+1) /* we expect q==qmax+1 occasionally due to rounding */ fprintf(stderr,"FLAC__lpc_quantize_coefficients: quantizer overflow: q>qmax %d>%d shift=%d cmax=%f precision=%u lpc[%u]=%f\n",q,qmax,*shift,cmax,precision+1,i,lp_coeff[i]); diff --git a/JuceLibraryCode/modules/juce_core/files/juce_File.cpp b/JuceLibraryCode/modules/juce_core/files/juce_File.cpp index 4463a04fdf..9d292e35b7 100644 --- a/JuceLibraryCode/modules/juce_core/files/juce_File.cpp +++ b/JuceLibraryCode/modules/juce_core/files/juce_File.cpp @@ -735,7 +735,7 @@ bool File::appendText (const String& text, if (out.failedToOpen()) return false; - out.writeText (text, asUnicode, writeUnicodeHeaderBytes); + out.writeText (text, asUnicode, writeUnicodeHeaderBytes, nullptr); return true; } diff --git a/JuceLibraryCode/modules/juce_core/streams/juce_OutputStream.cpp b/JuceLibraryCode/modules/juce_core/streams/juce_OutputStream.cpp index 8b5585f3ae..e2e9511676 100644 --- a/JuceLibraryCode/modules/juce_core/streams/juce_OutputStream.cpp +++ b/JuceLibraryCode/modules/juce_core/streams/juce_OutputStream.cpp @@ -186,28 +186,40 @@ bool OutputStream::writeString (const String& text) #endif } -bool OutputStream::writeText (const String& text, const bool asUTF16, - const bool writeUTF16ByteOrderMark) +bool OutputStream::writeText (const String& text, bool asUTF16, bool writeUTF16ByteOrderMark, const char* lf) { + bool replaceLineFeedWithUnix = lf != nullptr && lf[0] == '\n' && lf[1] == 0; + bool replaceLineFeedWithWindows = lf != nullptr && lf[0] == '\r' && lf[1] == '\n' && lf[2] == 0; + + // The line-feed passed in must be either nullptr, or "\n" or "\r\n" + jassert (lf == nullptr || replaceLineFeedWithWindows || replaceLineFeedWithUnix); + if (asUTF16) { if (writeUTF16ByteOrderMark) write ("\x0ff\x0fe", 2); - String::CharPointerType src (text.getCharPointer()); + auto src = text.getCharPointer(); bool lastCharWasReturn = false; for (;;) { - const juce_wchar c = src.getAndAdvance(); + auto c = src.getAndAdvance(); if (c == 0) break; - if (c == '\n' && ! lastCharWasReturn) - writeShort ((short) '\r'); + if (replaceLineFeedWithWindows) + { + if (c == '\n' && ! lastCharWasReturn) + writeShort ((short) '\r'); - lastCharWasReturn = (c == L'\r'); + lastCharWasReturn = (c == L'\r'); + } + else if (replaceLineFeedWithUnix && c == '\r') + { + continue; + } if (! writeShort ((short) c)) return false; @@ -215,37 +227,57 @@ bool OutputStream::writeText (const String& text, const bool asUTF16, } else { - const char* src = text.toUTF8(); - const char* t = src; + const char* src = text.toRawUTF8(); - for (;;) + if (replaceLineFeedWithWindows) { - if (*t == '\n') + for (auto t = src;;) { - if (t > src) - if (! write (src, (size_t) (t - src))) - return false; + if (*t == '\n') + { + if (t > src) + if (! write (src, (size_t) (t - src))) + return false; - if (! write ("\r\n", 2)) - return false; + if (! write ("\r\n", 2)) + return false; - src = t + 1; - } - else if (*t == '\r') - { - if (t[1] == '\n') - ++t; + src = t + 1; + } + else if (*t == '\r') + { + if (t[1] == '\n') + ++t; + } + else if (*t == 0) + { + if (t > src) + if (! write (src, (size_t) (t - src))) + return false; + + break; + } + + ++t; } - else if (*t == 0) + } + else if (replaceLineFeedWithUnix) + { + for (;;) { - if (t > src) - if (! write (src, (size_t) (t - src))) - return false; + auto c = *src++; - break; - } + if (c == 0) + break; - ++t; + if (c != '\r') + if (! writeByte (c)) + return false; + } + } + else + { + return write (src, text.getNumBytesAsUTF8()); } } diff --git a/JuceLibraryCode/modules/juce_core/streams/juce_OutputStream.h b/JuceLibraryCode/modules/juce_core/streams/juce_OutputStream.h index 407d34218f..af6411d405 100644 --- a/JuceLibraryCode/modules/juce_core/streams/juce_OutputStream.h +++ b/JuceLibraryCode/modules/juce_core/streams/juce_OutputStream.h @@ -206,12 +206,15 @@ class JUCE_API OutputStream bytes (0xff, 0xfe) to indicate the endianness (these should only be used at the start of a file). - The method also replaces '\\n' characters in the text with '\\r\\n'. + If lineEndings is nullptr, then line endings in the text won't be modified. If you + pass "\\n" or "\\r\\n" then this function will replace any existing line feeds. + @returns false if the write operation fails for some reason */ virtual bool writeText (const String& text, bool asUTF16, - bool writeUTF16ByteOrderMark); + bool writeUTF16ByteOrderMark, + const char* lineEndings); /** Reads data from an input stream and writes it to this stream. diff --git a/PluginGenerator/JuceLibraryCode/BinaryData.cpp b/PluginGenerator/JuceLibraryCode/BinaryData.cpp index ea15500858..ee2ac20b88 100644 --- a/PluginGenerator/JuceLibraryCode/BinaryData.cpp +++ b/PluginGenerator/JuceLibraryCode/BinaryData.cpp @@ -1902,7 +1902,7 @@ static const unsigned char temp_binary_data_18[] = "}\r\n" "\r\n" "\r\n" -"void PROCESSORCLASSNAME::openFiles (File rootFolder, int experimentNumber, int recordingNumber)\r\n" +"void PROCESSORCLASSNAME::openFiles (File rootFolder, String baseName, int recordingNumber)\r\n" "{\r\n" "}\r\n" "\r\n" @@ -2003,7 +2003,7 @@ static const unsigned char temp_binary_data_19[] = "\r\n" " String getEngineID() const override;\r\n" "\r\n" -" void openFiles (File rootFolder, int experimentNumber, int recordingNumber) override;\r\n" +" void openFiles (File rootFolder, String baseName, int recordingNumber) override;\r\n" " void closeFiles() override;\r\n" "\r\n" " void writeData (int writeChannel, int realChannel, const float* buffer, int size) override;\r\n" diff --git a/Resources/ChanMaps/A1x32__Adpt_A32_OM32_RHD2132.chanmap b/Resources/ChanMaps/A1x32__Adpt_A32_OM32_RHD2132.chanmap new file mode 100644 index 0000000000..8f9e5a021c --- /dev/null +++ b/Resources/ChanMaps/A1x32__Adpt_A32_OM32_RHD2132.chanmap @@ -0,0 +1,20 @@ +{ + "chanmap": { + "mapping": [1, 17, 16, 32, 3, 19, 14, 30, 9, 25, 10, 20, 8, 24, 2, 29, 7, 26, 15, 21, 11, 23, 12, 28, 6, 18, 13, 22, 5, 27, 4, 31 + ], + "labels": [17, 16, 18, 15, 19, 14, 20, 13, 21, 12, 22, 11, 23, 10, 24, 9, 25, 8, 26, 7, 27, 6, 28, 5, 29, 4, 30, 3, 31, 2, 32, 1 + ], + "reference": [-1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1 + ], + "enabled": [true, true, true, true, true, true, true, true, true, true, true, true, true, true, true, true, true, true, true, true, true, true, true, true, true, true, true, true, true, true, true, true + ] + }, + "refs": { + "channels": [ + ] + }, + "recording": { + "channels": [false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false + ] + } + } diff --git a/Resources/ChanMaps/A1x32__Adpt_CM32_RHD2132.chanmap b/Resources/ChanMaps/A1x32__Adpt_CM32_RHD2132.chanmap new file mode 100644 index 0000000000..9cf006f5ae --- /dev/null +++ b/Resources/ChanMaps/A1x32__Adpt_CM32_RHD2132.chanmap @@ -0,0 +1,20 @@ +{ + "chanmap": { + "mapping": [1, 32, 2, 31, 16, 17, 15, 18, 9, 24, 10, 23, 8, 25, 11, 22, 7, 26, 12, 21, 6, 27, 13, 20, 5, 28, 14, 19, 4, 29, 3, 30 + ], + "labels": [17, 16, 18, 15, 19, 14, 20, 13, 21, 12, 22, 11, 23, 10, 24, 9, 25, 8, 26, 7, 27, 6, 28, 5, 29, 4, 30, 3, 31, 2, 32, 1 + ], + "reference": [-1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1 + ], + "enabled": [true, true, true, true, true, true, true, true, true, true, true, true, true, true, true, true, true, true, true, true, true, true, true, true, true, true, true, true, true, true, true, true + ] + }, + "refs": { + "channels": [ + ] + }, + "recording": { + "channels": [false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false + ] + } + } diff --git a/Resources/ChanMaps/A1x32_edge__Adpt_CM32_RHD2132.chanmap b/Resources/ChanMaps/A1x32_edge__Adpt_CM32_RHD2132.chanmap new file mode 100644 index 0000000000..ca5534e33f --- /dev/null +++ b/Resources/ChanMaps/A1x32_edge__Adpt_CM32_RHD2132.chanmap @@ -0,0 +1,122 @@ +{ + "chanmap": { + "mapping": [ + 3, 4, 14, 5, 13, 6, 12, 7, 11, 8, 10, 9, 15, 16, 2, 1, 32, 31, 17, 18, 24, 23, 25, 22, 26, 21, 27, 20, 28, 19, 29, 30 + ], + "labels": [ + 32, 31, 30, 29, 28, 27, 26, 25, 24, 23, 22, 21, 20, 19, 18, 17, 16, 15, 14, 13, 12, 11, 10, 9, 8, 7, 6, 5, 4, 3, 2, 1 + ], + "reference": [ + -1, + -1, + -1, + -1, + -1, + -1, + -1, + -1, + -1, + -1, + -1, + -1, + -1, + -1, + -1, + -1, + -1, + -1, + -1, + -1, + -1, + -1, + -1, + -1, + -1, + -1, + -1, + -1, + -1, + -1, + -1, + -1 + ], + "enabled": [ + true, + true, + true, + true, + true, + true, + true, + true, + true, + true, + true, + true, + true, + true, + true, + true, + true, + true, + true, + true, + true, + true, + true, + true, + true, + true, + true, + true, + true, + true, + true, + true + ] + }, + "refs": { + "channels": [ + 31, + -1, + -1, + -1 + ] + }, + "recording": { + "channels": [ + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false + ] + } + } diff --git a/Resources/ChanMaps/A1x64__Adpt_A64_OM32x2_sm_RHD2164.chanmap b/Resources/ChanMaps/A1x64__Adpt_A64_OM32x2_sm_RHD2164.chanmap new file mode 100644 index 0000000000..f1ae86e9e8 --- /dev/null +++ b/Resources/ChanMaps/A1x64__Adpt_A64_OM32x2_sm_RHD2164.chanmap @@ -0,0 +1,20 @@ +{ + "chanmap": { + "mapping": [64, 4, 59, 2, 57, 5, 63, 7, 55, 1, 61, 9, 53, 3, 58, 11, 51, 8, 56, 13, 49, 10, 54, 15, 34, 12, 52, 32, 36, 14, 50, 30, 37, 16, 33, 27, 42, 31, 35, 24, 46, 29, 38, 20, 39, 28, 40, 25, 43, 26, 44, 21, 48, 22, 62, 18, 60, 6, 45, 19, 47, 17, 41, 23 + ], + "labels": [27, 37, 26, 38, 25, 39, 24, 40, 23, 41, 22, 42, 21, 43, 20, 44, 19, 45, 18, 46, 17, 47, 16, 48, 15, 49, 14, 50, 13, 51, 12, 52, 11, 53, 10, 54, 9, 55, 8, 56, 7, 57, 6, 58, 5, 59, 4, 60, 3, 61, 2, 62, 1, 63, 28, 64, 29, 36, 30, 35, 31, 34, 32, 33 + ], + "reference": [-1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1 + ], + "enabled": [true, true, true, true, true, true, true, true, true, true, true, true, true, true, true, true, true, true, true, true, true, true, true, true, true, true, true, true, true, true, true, true, true, true, true, true, true, true, true, true, true, true, true, true, true, true, true, true, true, true, true, true, true, true, true, true, true, true, true, true, true, true, true, true + ] + }, + "refs": { + "channels": [ + ] + }, + "recording": { + "channels": [false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false + ] + } + } diff --git a/Resources/ChanMaps/README b/Resources/ChanMaps/README new file mode 100644 index 0000000000..b3fd83cedb --- /dev/null +++ b/Resources/ChanMaps/README @@ -0,0 +1,13 @@ +.chanmap files are just like open-ephys' normal .prb files plus a "labels" field to allow +renaming of AD chans to probe chans. + +Channel map descriptions: + +A1x32__Adpt_CM32_RHD2132 + - NeuroNexus A1x32 single shank probe + Adpt_CM32_RHD2132 "adapter" + +A1x32__Adpt_A32_OM32_RHD2132 + - NeuroNexus A1x32 single shank probe + Adpt_A32_OM32_RHD2132 adapter + +A1x64__Adpt_A64_OM32x2_sm_RHD2164 + - NeuroNexus A1x64 single shank probe + Adpt_A64_OM32x2_sm_RHD2164 adapter diff --git a/Resources/ChanMaps/generate_channel_mappings.py b/Resources/ChanMaps/generate_channel_mappings.py new file mode 100644 index 0000000000..6af29630c3 --- /dev/null +++ b/Resources/ChanMaps/generate_channel_mappings.py @@ -0,0 +1,28 @@ +"""Generates ADchan mappings that describes how to rearrange AD chans such that they are +displayed in vertical spatial order of their corresponding probe channels, taking both the +probe layout and the adapter mapping into account. Also generates corresponding probe channel +numbers for renaming AD chans to probe chans""" + +from spyke import probes +import numpy as np + +probenames = ['A1x32', 'A1x32_edge', 'A1x32', 'A1x64' ] +adapternames = ['Adpt_CM32_RHD2132', 'Adpt_CM32_RHD2132', 'Adpt_A32_OM32_RHD2132', 'Adpt_A64_OM32x2_sm_RHD2164'] + +for probename, adaptername in zip(probenames, adapternames): + + # instantiate probe and adapter objects: + probe = probes.getprobe(probename) + adapter = probes.getadapter(adaptername) + + # get probe chans sorted by spatial order: + spatiallysortedprobechans = probe.chans_lrtb # left to right, top to bottom + + # get AD chans in spatiallysortedprobechan order: + mapping = [ adapter.probe2AD[probechan] for probechan in spatiallysortedprobechans ] + + print(probename + '__' + adaptername) + print('mapping:') + print(mapping) + print('labels:') + print(list(spatiallysortedprobechans)) diff --git a/Source/CoreServices.cpp b/Source/CoreServices.cpp index edecf4e1fa..2108902959 100644 --- a/Source/CoreServices.cpp +++ b/Source/CoreServices.cpp @@ -98,6 +98,20 @@ float getSoftwareSampleRate() return getProcessorGraph()->getGlobalSampleRate(true); } +var getChannelMapNames() +{ + Array procs = getProcessorGraph()->getListOfProcessors(); + int nprocs = procs.size(); + var chanmapnames; + for (int proci=0; proci < nprocs; proci++) + { + GenericProcessor* proc = procs[proci]; + if (proc->getName() == "Channel Map") + chanmapnames.append(proc->getEditor()->getDisplayName()); + } + return chanmapnames; +} + void setRecordingDirectory(String dir) { getControlPanel()->setRecordingDirectory(dir); @@ -108,16 +122,6 @@ void createNewRecordingDir() getControlPanel()->labelTextChanged(NULL); } -void setPrependTextToRecordingDir(String text) -{ - getControlPanel()->setPrependText(text); -} - -void setAppendTextToRecordingDir(String text) -{ - getControlPanel()->setAppendText(text); -} - String getSelectedRecordEngineId() { return getControlPanel()->getSelectedRecordEngineId(); @@ -140,14 +144,14 @@ File getRecordingPath() return getProcessorGraph()->getRecordNode()->getDataDirectory(); } -int getRecordingNumber() +String getBaseName() { - return getProcessorGraph()->getRecordNode()->getRecordingNumber(); + return getProcessorGraph()->getRecordNode()->getBaseName(); } -int getExperimentNumber() +int getRecordingNumber() { - return getProcessorGraph()->getRecordNode()->getExperimentNumber(); + return getProcessorGraph()->getRecordNode()->getRecordingNumber(); } void writeSpike(const SpikeEvent* spike, const SpikeChannel* chan) @@ -191,4 +195,4 @@ String getGUIVersion() return STR_DEF(JUCE_APP_VERSION); } -}; \ No newline at end of file +}; diff --git a/Source/CoreServices.h b/Source/CoreServices.h index 24f593e1d2..8eb946fbdf 100644 --- a/Source/CoreServices.h +++ b/Source/CoreServices.h @@ -71,6 +71,9 @@ PLUGIN_API float getGlobalSampleRate(); /** Gets the software timestamp based on a high resolution timer aligned to the start of each processing block */ PLUGIN_API int64 getSoftwareTimestamp(); +/** Returns a list of display names of all channel map editors in the signal chain **/ +PLUGIN_API var getChannelMapNames(); + /** Gets the ticker frequency of the software timestamp clock*/ PLUGIN_API float getSoftwareSampleRate(); @@ -99,10 +102,10 @@ namespace RecordNode /** Forces creation of new directory on recording */ PLUGIN_API void createNewrecordingDir(); -/** Gets the current recording directories and status information */ +/** Gets the current recording path, base name, and number */ PLUGIN_API File getRecordingPath(); +PLUGIN_API String getBaseName(); PLUGIN_API int getRecordingNumber(); -PLUGIN_API int getExperimentNumber(); /* Spike related methods. See record engine documentation */ diff --git a/Source/Plugins/BasicSpikeDisplay/SpikeDetector/SpikeDetector.cpp b/Source/Plugins/BasicSpikeDisplay/SpikeDetector/SpikeDetector.cpp index c44681df4f..da7342b234 100644 --- a/Source/Plugins/BasicSpikeDisplay/SpikeDetector/SpikeDetector.cpp +++ b/Source/Plugins/BasicSpikeDisplay/SpikeDetector/SpikeDetector.cpp @@ -37,8 +37,6 @@ SpikeDetector::SpikeDetector() //// the standard form: electrodeTypes.add ("single electrode"); - electrodeTypes.add ("stereotrode"); - electrodeTypes.add ("tetrode"); //// the technically correct form (Greek cardinal prefixes): // electrodeTypes.add("hentrode"); @@ -118,7 +116,7 @@ void SpikeDetector::updateSettings() bool SpikeDetector::addElectrode (int nChans, int electrodeID) { - std::cout << "Adding electrode with " << nChans << " channels." << std::endl; + //std::cout << "Adding electrode with " << nChans << " channels." << std::endl; int firstChan; @@ -220,7 +218,7 @@ void SpikeDetector::resetElectrode (SimpleElectrode* e) bool SpikeDetector::removeElectrode (int index) { - // std::cout << "Spike detector removing electrode" << std::endl; + //std::cout << "Spike detector removing electrode" << std::endl; if (index > electrodes.size() || index < 0) return false; @@ -238,8 +236,8 @@ void SpikeDetector::setElectrodeName (int index, String newName) void SpikeDetector::setChannel (int electrodeIndex, int channelNum, int newChannel) { - std::cout << "Setting electrode " << electrodeIndex << " channel " << channelNum - << " to " << newChannel << std::endl; + //std::cout << "Setting electrode " << electrodeIndex << " channel " << channelNum + // << " to " << newChannel << std::endl; *(electrodes[electrodeIndex]->channels + channelNum) = newChannel; } @@ -289,7 +287,7 @@ void SpikeDetector::setChannelActive (int electrodeIndex, int subChannel, bool a currentElectrode = electrodeIndex; currentChannelIndex = subChannel; - std::cout << "Setting channel active to " << active << std::endl; + //std::cout << "Setting channel active to " << active << std::endl; if (active) setParameter (98, 1); @@ -309,7 +307,7 @@ void SpikeDetector::setChannelThreshold (int electrodeNum, int channelNum, float currentElectrode = electrodeNum; currentChannelIndex = channelNum; - std::cout << "Setting electrode " << electrodeNum << " channel threshold " << channelNum << " to " << thresh << std::endl; + //std::cout << "Setting electrode " << electrodeNum << " channel threshold " << channelNum << " to " << thresh << std::endl; setParameter (99, thresh); } @@ -376,7 +374,6 @@ void SpikeDetector::addWaveformToSpikeObject (SpikeEvent::SpikeBuffer& s, if (isChannelActive (electrodeNumber, currentChannel)) { - for (int sample = 0; sample < spikeLength; ++sample) { s.set(currentChannel,sample, getNextSample (*(electrodes[electrodeNumber]->channels+currentChannel))); @@ -390,7 +387,7 @@ void SpikeDetector::addWaveformToSpikeObject (SpikeEvent::SpikeBuffer& s, for (int sample = 0; sample < spikeLength; ++sample) { // insert a blank spike if the - s.set(currentChannel, sample, 0); + s.set(currentChannel, sample, 0); ++sampleIndex; //std::cout << currentIndex << std::endl; } @@ -428,7 +425,7 @@ void SpikeDetector::process (AudioSampleBuffer& buffer) // cycle through channels for (int chan = 0; chan < electrode->numChannels; ++chan) { - // std::cout << " channel " << chan << std::endl; + //std::cout << " channel " << chan << std::endl; if (*(electrode->isActive + chan)) { int currentChannel = *(electrode->channels + chan); @@ -597,7 +594,7 @@ void SpikeDetector::loadCustomParametersFromXml() { ++electrodeIndex; - std::cout << "ELECTRODE>>>" << std::endl; + //std::cout << "ELECTRODE>>>" << std::endl; const int channelsPerElectrode = xmlNode->getIntAttribute ("numChannels"); const int electrodeID = xmlNode->getIntAttribute ("electrodeID"); @@ -615,7 +612,7 @@ void SpikeDetector::loadCustomParametersFromXml() { ++channelIndex; - std::cout << "Subchannel " << channelIndex << std::endl; + //std::cout << "Subchannel " << channelIndex << std::endl; setChannel (electrodeIndex, channelIndex, channelNode->getIntAttribute ("ch")); setChannelThreshold (electrodeIndex, channelIndex, channelNode->getDoubleAttribute ("thresh")); diff --git a/Source/Plugins/BasicSpikeDisplay/SpikeDetector/SpikeDetector.h b/Source/Plugins/BasicSpikeDisplay/SpikeDetector/SpikeDetector.h index 7f1656bfb2..0b14530a3f 100644 --- a/Source/Plugins/BasicSpikeDisplay/SpikeDetector/SpikeDetector.h +++ b/Source/Plugins/BasicSpikeDisplay/SpikeDetector/SpikeDetector.h @@ -135,7 +135,7 @@ class SpikeDetector : public GenericProcessor /** Sets the current electrode index */ SimpleElectrode* setCurrentElectrodeIndex (int); - /** Returns a list of possible electrode types (e.g., stereotrode, tetrode). */ + /** Returns a list of possible electrode types (e.g., stereotrode, tetrode, etc.). */ StringArray electrodeTypes; // ===================================================================== diff --git a/Source/Plugins/BasicSpikeDisplay/SpikeDetector/SpikeDetectorEditor.cpp b/Source/Plugins/BasicSpikeDisplay/SpikeDetector/SpikeDetectorEditor.cpp index 50100748ae..eb2cf9ff10 100644 --- a/Source/Plugins/BasicSpikeDisplay/SpikeDetector/SpikeDetectorEditor.cpp +++ b/Source/Plugins/BasicSpikeDisplay/SpikeDetector/SpikeDetectorEditor.cpp @@ -46,7 +46,7 @@ SpikeDetectorEditor::SpikeDetectorEditor(GenericProcessor* parentNode, bool useD for (int i = 0; i < processor->electrodeTypes.size(); i++) { String type = processor->electrodeTypes[i]; - electrodeTypes->addItem(type += "s", i+1); + electrodeTypes->addItem(type, i+1); } electrodeTypes->setEditableText(false); @@ -511,7 +511,7 @@ void SpikeDetectorEditor::comboBoxChanged(ComboBox* comboBox) { int ID = comboBox->getSelectedId(); - std::cout << "ID: " << ID << std::endl; + //std::cout << "ID: " << ID << std::endl; if (ID == 0) { diff --git a/Source/Plugins/BasicSpikeDisplay/SpikeDetector/SpikeDetectorEditor.h b/Source/Plugins/BasicSpikeDisplay/SpikeDetector/SpikeDetectorEditor.h index 07162cd778..86a9ff4dc4 100644 --- a/Source/Plugins/BasicSpikeDisplay/SpikeDetector/SpikeDetectorEditor.h +++ b/Source/Plugins/BasicSpikeDisplay/SpikeDetector/SpikeDetectorEditor.h @@ -36,7 +36,7 @@ class UtilityButton; User interface for the SpikeDetector processor. - Allows the user to add single electrodes, stereotrodes, or tetrodes. + Allows the user to add single electrodes, stereotrodes, tetrodes, etc. Parameters of individual channels, such as channel mapping, threshold, and enabled state, can be edited. diff --git a/Source/Plugins/BasicSpikeDisplay/SpikeDisplayNode/SpikeDisplayCanvas.cpp b/Source/Plugins/BasicSpikeDisplay/SpikeDisplayNode/SpikeDisplayCanvas.cpp index 2fd17e7010..8948d2a67f 100644 --- a/Source/Plugins/BasicSpikeDisplay/SpikeDisplayNode/SpikeDisplayCanvas.cpp +++ b/Source/Plugins/BasicSpikeDisplay/SpikeDisplayNode/SpikeDisplayCanvas.cpp @@ -85,36 +85,48 @@ void SpikeDisplayCanvas::endAnimation() void SpikeDisplayCanvas::update() { - //std::cout << "Updating SpikeDisplayCanvas" << std::endl; - - int nPlots = processor->getNumElectrodes(); + int nplots = processor->getNumElectrodes(); + int ninputs = processor->getNumInputs(); + //std::cout << "SpikeDisplayCanvas nplots " << nplots << std::endl; + //std::cout << "SpikeDisplayCanvas ninputs " << ninputs << std::endl; processor->removeSpikePlots(); - - if (nPlots != spikeDisplay->getNumPlots()) + // this fails to update plot titles when chans are rearranged in chanmap: + // if (nplots != spikeDisplay->getNumPlots()) + if (true) { spikeDisplay->removePlots(); - - for (int i = 0; i < nPlots; i++) + for (int ploti = 0; ploti < nplots; ploti++) { - SpikePlot* sp = spikeDisplay->addSpikePlot(processor->getNumberOfChannelsForElectrode(i), i, - processor->getNameForElectrode(i)); - processor->addSpikePlotForElectrode(sp, i); + int nchans = processor->getNumberOfChannelsForElectrode(ploti); + if (nchans != 1) + { + std::cout << "NOTE: Code has been tailored to high chan count probes, and assumes " + << "only one chan per spike plot. Please use only single electrodes. " + << "Exiting..." + << std::endl; + JUCEApplication::quit(); + } + String dataChanName = processor->getDataChannel(ploti)->getName(); + // label each spike plot according to datachan name - this obeys preceding chanmaps + // and is simpler and more meaningful than processor->getNameForElectrode(i)): + SpikePlot* sp = spikeDisplay->addSpikePlot(nchans, ploti, dataChanName); + processor->addSpikePlotForElectrode(sp, ploti); } } else + // mspacek: is this more efficient? if so, not sure how to easily check for change + // in chanmap that doesn't involve change in num chans { - for (int i = 0; i < nPlots; i++) + for (int ploti = 0; ploti < nplots; ploti++) { - processor->addSpikePlotForElectrode(spikeDisplay->getSpikePlot(i), i); + processor->addSpikePlotForElectrode(spikeDisplay->getSpikePlot(ploti), ploti); } } - spikeDisplay->resized(); spikeDisplay->repaint(); } - void SpikeDisplayCanvas::refreshState() { // called when the component's tab becomes visible again @@ -123,22 +135,22 @@ void SpikeDisplayCanvas::refreshState() void SpikeDisplayCanvas::resized() { - viewport->setBounds(0,0,getWidth(),getHeight()-90); + viewport->setBounds(0, 0, getWidth(), getHeight()-30); // leave space at bottom for buttons spikeDisplay->setBounds(0,0,getWidth()-scrollBarThickness, spikeDisplay->getTotalHeight()); - clearButton->setBounds(10, getHeight()-40, 100,20); + clearButton->setBounds(10, getHeight()-25, 130, 20); - lockThresholdsButton->setBounds(130, getHeight()-40, 130,20); + lockThresholdsButton->setBounds(10+130+10, getHeight()-25, 130, 20); - invertSpikesButton->setBounds(270, getHeight()-40, 130,20); + invertSpikesButton->setBounds(10+130+10+130+10, getHeight()-25, 130, 20); } void SpikeDisplayCanvas::paint(Graphics& g) { - g.fillAll(Colours::darkgrey); + g.fillAll(Colours::black); } @@ -224,42 +236,29 @@ void SpikeDisplayCanvas::loadVisualizerParameters(XmlElement* xml) lockThresholdsButton->setToggleState(xmlNode->getBoolAttribute("LockThresholds"), sendNotification); int plotIndex = -1; - forEachXmlChildElement(*xmlNode, plotNode) { if (plotNode->hasTagName("PLOT")) { - plotIndex++; - - std::cout << "PLOT NUMBER " << plotIndex << std::endl; - + //std::cout << "PLOT NUMBER " << plotIndex << std::endl; int channelIndex = -1; - forEachXmlChildElement(*plotNode, channelNode) { - if (channelNode->hasTagName("AXIS")) { channelIndex++; - - std::cout << "CHANNEL NUMBER " << channelIndex << std::endl; - + //std::cout << "CHANNEL NUMBER " << channelIndex << std::endl; spikeDisplay->setThresholdForWaveAxis(plotIndex, channelIndex, channelNode->getDoubleAttribute("thresh")); - spikeDisplay->setRangeForWaveAxis(plotIndex, channelIndex, channelNode->getDoubleAttribute("range")); - } } - - } } - } } } @@ -301,7 +300,7 @@ void SpikeDisplay::removePlots() SpikePlot* SpikeDisplay::addSpikePlot(int numChannels, int electrodeNum, String name_) { - std::cout << "Adding new spike plot." << std::endl; + //std::cout << "Adding new spike plot." << std::endl; SpikePlot* spikePlot = new SpikePlot(canvas, electrodeNum, 1000 + numChannels, name_); spikePlots.add(spikePlot); @@ -339,12 +338,7 @@ void SpikeDisplay::resized() int numColumns = 1; int column, row; - int stereotrodeStart = 0; - int tetrodeStart = 0; - int singlePlotIndex = -1; - int stereotrodePlotIndex = -1; - int tetrodePlotIndex = -1; int index = -1; float width = 0; @@ -362,22 +356,6 @@ void SpikeDisplay::resized() numColumns = (int) jmax(w / spikePlots[i]->minWidth, 1.0f); width = jmin((float) w / (float) numColumns, (float) getWidth()); height = width * spikePlots[i]->aspectRatio; - - } - else if (spikePlots[i]->nChannels == 2) - { - index = ++stereotrodePlotIndex; - numColumns = (int) jmax(w / spikePlots[i]->minWidth, 1.0f); - width = jmin((float) w / (float) numColumns, (float) getWidth()); - height = width * spikePlots[i]->aspectRatio; - - } - else if (spikePlots[i]->nChannels == 4) - { - index = ++tetrodePlotIndex; - numColumns = (int) jmax(w / spikePlots[i]->minWidth, 1.0f); - width = jmin((float) w / (float) numColumns, (float) getWidth()); - height = width * spikePlots[i]->aspectRatio; } column = index % numColumns; @@ -388,42 +366,18 @@ void SpikeDisplay::resized() maxHeight = jmax(maxHeight, row*height + height); - if (spikePlots[i]->nChannels == 1) - { - stereotrodeStart = (int)(height*(float(row)+1)); - } - else if (spikePlots[i]->nChannels == 2) - { - tetrodeStart = (int)(height*(float(row)+1)); - } - } for (int i = 0; i < spikePlots.size(); i++) { - int x = spikePlots[i]->getX(); int y = spikePlots[i]->getY(); int w2 = spikePlots[i]->getWidth(); int h2 = spikePlots[i]->getHeight(); - - if (spikePlots[i]->nChannels == 2) - { - spikePlots[i]->setBounds(x, y+stereotrodeStart, w2, h2); - maxHeight = jmax(maxHeight, (float) y+stereotrodeStart+h2); - - } - else if (spikePlots[i]->nChannels == 4) - { - spikePlots[i]->setBounds(x, y+stereotrodeStart+tetrodeStart, w2, h2); - maxHeight = jmax(maxHeight, (float) y+stereotrodeStart+tetrodeStart+h2); - } - - } - totalHeight = (int) maxHeight + 50; + totalHeight = (int) maxHeight; // std::cout << "New height = " << totalHeight << std::endl; @@ -517,27 +471,6 @@ SpikePlot::SpikePlot(SpikeDisplayCanvas* sdc, int elecNum, int p, String name_) minWidth = 200; aspectRatio = 1.0f; break; - case STEREO_PLOT: - // std::cout<<"SpikePlot as STEREO_PLOT"<clear(); @@ -790,7 +702,7 @@ float SpikePlot::getDisplayThresholdForChannel(int i) void SpikePlot::setDisplayThresholdForChannel(int i, float thresh) { - std::cout << "Setting threshold to " << thresh << std::endl; + //std::cout << "Setting threshold to " << thresh << std::endl; wAxes[i]->setDisplayThreshold(thresh); } @@ -801,7 +713,7 @@ float SpikePlot::getRangeForChannel(int i) void SpikePlot::setRangeForChannel(int i, float range) { - std::cout << "Setting range to " << range << std::endl; + //std::cout << "Setting range to " << range << std::endl; wAxes[i]->setRange(range); rangeButtons[i]->setLabel(String(int(range))); } @@ -893,7 +805,7 @@ WaveAxes::WaveAxes(int channel) : GenericAxes(channel), void WaveAxes::setRange(float r) { - std::cout << "Setting range to " << r << std::endl; + //std::cout << "Setting range to " << r << std::endl; range = r; @@ -1538,4 +1450,4 @@ void SpikeThresholdCoordinator::thresholdChanged(float displayThreshold, float r registeredPlots[i]->setAllThresholds(displayThreshold,range); } } -} \ No newline at end of file +} diff --git a/Source/Plugins/BinaryWriter/BinaryRecording.cpp b/Source/Plugins/BinaryWriter/BinaryRecording.cpp deleted file mode 100644 index c75e5b8968..0000000000 --- a/Source/Plugins/BinaryWriter/BinaryRecording.cpp +++ /dev/null @@ -1,639 +0,0 @@ -/* ------------------------------------------------------------------- - -This file is part of the Open Ephys GUI -Copyright (C) 2013 Open Ephys - ------------------------------------------------------------------- - -This program is free software: you can redistribute it and/or modify -it under the terms of the GNU General Public License as published by -the Free Software Foundation, either version 3 of the License, or -(at your option) any later version. - -This program is distributed in the hope that it will be useful, -but WITHOUT ANY WARRANTY; without even the implied warranty of -MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -GNU General Public License for more details. - -You should have received a copy of the GNU General Public License -along with this program. If not, see . - -*/ - -#include "BinaryRecording.h" - -#define MAX_BUFFER_SIZE 40960 - -using namespace BinaryRecordingEngine; - -BinaryRecording::BinaryRecording() -{ - m_scaledBuffer.malloc(MAX_BUFFER_SIZE); - m_intBuffer.malloc(MAX_BUFFER_SIZE); - m_tsBuffer.malloc(MAX_BUFFER_SIZE); -} - -BinaryRecording::~BinaryRecording() -{ - -} - -String BinaryRecording::getEngineID() const -{ - return "RAWBINARY"; -} - -String BinaryRecording::getProcessorString(const InfoObjectCommon* channelInfo) -{ - String fName = (channelInfo->getCurrentNodeName().replaceCharacter(' ', '_') + "-" + String(channelInfo->getCurrentNodeID())); - if (channelInfo->getCurrentNodeID() == channelInfo->getSourceNodeID()) //it is the channel source - { - fName += "." + String(channelInfo->getSubProcessorIdx()); - } - else - { - fName += "_" + String(channelInfo->getSourceNodeID()) + "." + String(channelInfo->getSubProcessorIdx()); - } - fName += File::separatorString; - return fName; -} - -void BinaryRecording::openFiles(File rootFolder, int experimentNumber, int recordingNumber) -{ - 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 indexedDataChannels; - Array indexedChannelCount; - Array jsonContinuousfiles; - Array jsonChannels; - StringArray continuousFileNames; - int lastId = 0; - for (int proc = 0; proc < nProcessors; proc++) - { - const RecordProcessorInfo& pInfo = getProcessorInfo(proc); - int recChans = pInfo.recordedChannels.size(); - - for (int chan = 0; chan < recChans; chan++) - { - int recordedChan = pInfo.recordedChannels[chan]; - int realChan = getRealChannel(recordedChan); - const DataChannel* channelInfo = getDataChannel(realChan); - int sourceId = channelInfo->getSourceNodeID(); - int sourceSubIdx = channelInfo->getSubProcessorIdx(); - int nInfoArrays = indexedDataChannels.size(); - bool found = false; - DynamicObject::Ptr jsonChan = new DynamicObject(); - jsonChan->setProperty("channel_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 == indexedDataChannels[i]->getSourceNodeID() && sourceSubIdx == indexedDataChannels[i]->getSubProcessorIdx()) - { - unsigned int count = indexedChannelCount[i]; - m_channelIndexes.set(recordedChan, count); - m_fileIndexes.set(recordedChan, i); - indexedChannelCount.set(i, count + 1); - jsonChannels.getReference(i).append(var(jsonChan)); - found = true; - break; - } - } - if (!found) - { - String datPath = getProcessorString(channelInfo); - continuousFileNames.add(contPath + datPath + "continuous.dat"); - - ScopedPointer tFile = new NpyFile(contPath + datPath + "timestamps.npy", NpyType(BaseType::INT64,1)); - m_dataTimestampFiles.add(tFile.release()); - - m_fileIndexes.set(recordedChan, nInfoArrays); - m_channelIndexes.set(recordedChan, 0); - indexedChannelCount.add(1); - indexedDataChannels.add(channelInfo); - - Array jsonChanArray; - jsonChanArray.add(var(jsonChan)); - jsonChannels.add(var(jsonChanArray)); - DynamicObject::Ptr jsonFile = new DynamicObject(); - jsonFile->setProperty("folder_name", datPath.replace(File::separatorString, "/")); //to make it more system agnostic, replace separator with only one slash - 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 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(); - //Timestamps - Array procIDs; - for (int i = 0; i < nChans; i++) - { - m_startTS.add(getTimestamp(i)); - } - - int nEvents = getNumRecordedEvents(); - String eventPath(basepath + "events" + File::separatorString); - Array jsonEventFiles; - - for (int ev = 0; ev < nEvents; ev++) - { - const EventChannel* chan = getEventChannel(ev); - String eventName = getProcessorString(chan); - NpyType type; - String dataFileName; - - switch (chan->getChannelType()) - { - case EventChannel::TEXT: - eventName += "TEXT_group"; - type = NpyType(BaseType::CHAR, chan->getLength()); - dataFileName = "text"; - break; - case EventChannel::TTL: - eventName += "TTL"; - type = NpyType(BaseType::INT16, 1); - dataFileName = "channel_states"; - break; - default: - eventName += "BINARY_group"; - type = NpyType(chan->getEquivalentMetaDataType(), chan->getLength()); - dataFileName = "data_array"; - break; - } - eventName += "_" + String(chan->getSourceIndex() + 1) + File::separatorString; - ScopedPointer rec = new EventRecording(); - - rec->mainFile = new NpyFile(eventPath + eventName + dataFileName + ".npy", type); - rec->timestampFile = new NpyFile(eventPath + eventName + "timestamps.npy", NpyType(BaseType::INT64, 1)); - rec->channelFile = new NpyFile(eventPath + eventName + "channels.npy", NpyType(BaseType::UINT16, 1)); - if (chan->getChannelType() == EventChannel::TTL && m_saveTTLWords) - { - rec->extraFile = new NpyFile(eventPath + eventName + "full_words.npy", NpyType(BaseType::UINT8, chan->getDataSize())); - } - - DynamicObject::Ptr jsonChannel = new DynamicObject(); - jsonChannel->setProperty("folder_name", eventName.replace(File::separatorString, "/")); - jsonChannel->setProperty("channel_name", chan->getName()); - jsonChannel->setProperty("description", chan->getDescription()); - jsonChannel->setProperty("identifier", chan->getIdentifier()); - jsonChannel->setProperty("sample_rate", chan->getSampleRate()); - jsonChannel->setProperty("type", jsonTypeValue(type.getType())); - jsonChannel->setProperty("num_channels", (int)chan->getNumChannels()); - jsonChannel->setProperty("source_processor", chan->getSourceName()); - createChannelMetaData(chan, jsonChannel); - - rec->metaDataFile = createEventMetadataFile(chan, eventPath + eventName + "metadata.npy", jsonChannel); - m_eventFiles.add(rec.release()); - jsonEventFiles.add(var(jsonChannel)); - } - - int nSpikes = getNumRecordedSpikes(); - Array indexedSpikes; - Array indexedChannels; - m_spikeFileIndexes.insertMultiple(0, 0, nSpikes); - m_spikeChannelIndexes.insertMultiple(0, 0, nSpikes); - String spikePath(basepath + "spikes" + File::separatorString); - Array jsonSpikeFiles; - Array jsonSpikeChannels; - std::map groupMap; - for (int sp = 0; sp < nSpikes; sp++) - { - const SpikeChannel* ch = getSpikeChannel(sp); - DynamicObject::Ptr jsonChannel = new DynamicObject(); - unsigned int numSpikeChannels = ch->getNumChannels(); - jsonChannel->setProperty("channel_name", ch->getName()); - jsonChannel->setProperty("description", ch->getDescription()); - jsonChannel->setProperty("identifier", ch->getIdentifier()); - Array 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 rec = new EventRecording(); - - uint32 procID = GenericProcessor::getProcessorFullId(ch->getSourceNodeID(), ch->getSubProcessorIdx()); - int groupIndex = ++groupMap[procID]; - - String spikeName = getProcessorString(ch) + "spike_group_" + String(groupIndex) + File::separatorString; - - rec->mainFile = new NpyFile(spikePath + spikeName + "spike_waveforms.npy", NpyType(BaseType::INT16, ch->getTotalSamples()), ch->getNumChannels()); - rec->timestampFile = new NpyFile(spikePath + spikeName + "spike_times.npy", NpyType(BaseType::INT64, 1)); - rec->channelFile = new NpyFile(spikePath + spikeName + "spike_electrode_indices.npy", NpyType(BaseType::UINT16, 1)); - rec->extraFile = new NpyFile(spikePath + spikeName + "spike_clusters.npy", NpyType(BaseType::UINT16, 1)); - Array tsTypes; - - Array jsonChanArray; - jsonChanArray.add(var(jsonChannel)); - jsonSpikeChannels.add(var(jsonChanArray)); - DynamicObject::Ptr jsonFile = new DynamicObject(); - - jsonFile->setProperty("folder_name", spikeName.replace(File::separatorString,"/")); - 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, spikePath + spikeName + "metadata.npy", jsonFile); - m_spikeFiles.add(rec.release()); - jsonSpikeFiles.add(var(jsonFile)); - } - } - int nSpikeFiles = jsonSpikeFiles.size(); - for (int i = 0; i < nSpikeFiles; i++) - { - int size = jsonSpikeChannels.getReference(i).size(); - DynamicObject::Ptr jsonFile = jsonSpikeFiles.getReference(i).getDynamicObject(); - jsonFile->setProperty("num_channels", size); - jsonFile->setProperty("channels", jsonSpikeChannels.getReference(i)); - } - - File syncFile = File(basepath + "sync_messages.txt"); - Result res = syncFile.create(); - if (res.failed()) - { - std::cerr << "Error creating sync text file:" << res.getErrorMessage() << std::endl; - } - else - { - m_syncTextFile = syncFile.createOutputStream(); - } - - 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); -} - -NpyFile* BinaryRecording::createEventMetadataFile(const MetaDataEventObject* channel, String filename, DynamicObject* jsonFile) -{ - int nMetaData = channel->getEventMetaDataCount(); - if (nMetaData < 1) return nullptr; - - Array types; - Array jsonMetaData; - for (int i = 0; i < nMetaData; i++) - { - 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 (jsonFile) - jsonFile->setProperty("event_metadata", jsonMetaData); - return new NpyFile(filename, types); -} - -template -void dataToVar(var& dataTo, const void* dataFrom, int length) -{ - const FROM* buffer = reinterpret_cast(dataFrom); - for (int i = 0; i < length; i++) - { - dataTo.append(static_cast(*(buffer + i))); - } -} - -void BinaryRecording::createChannelMetaData(const MetaDataInfoObject* channel, DynamicObject* jsonFile) -{ - int nMetaData = channel->getMetaDataCount(); - if (nMetaData < 1) return; - - Array jsonMetaData; - for (int i = 0; i < nMetaData; i++) - { - 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(val, buf, length); - break; - case MetaDataDescriptor::UINT8: - dataToVar(val, buf, length); - break; - case MetaDataDescriptor::INT16: - dataToVar(val, buf, length); - break; - case MetaDataDescriptor::UINT16: - dataToVar(val, buf, length); - break; - case MetaDataDescriptor::INT32: - dataToVar(val, buf, length); - break; - //A full uint32 doesn't fit in a regular int, so we increase size - case MetaDataDescriptor::UINT32: - dataToVar(val, buf, length); - break; - case MetaDataDescriptor::INT64: - dataToVar(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(val, buf, length); - break; - case MetaDataDescriptor::FLOAT: - dataToVar(val, buf, length); - break; - case MetaDataDescriptor::DOUBLE: - dataToVar(val, buf, length); - break; - default: - val = "invalid"; - } - } - jsonValues->setProperty("value", val); - jsonMetaData.add(var(jsonValues)); - } - 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_startTS.clear(); -} - -void BinaryRecording::writeData(int writeChannel, int realChannel, const float* buffer, int size) -{ - if (size > 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. - { - std::cerr << "Write buffer overrun, resizing to" << size << std::endl; - 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); - int fileIndex = m_fileIndexes[writeChannel]; - m_DataFiles[fileIndex]->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[fileIndex]->writeData(m_tsBuffer, size*sizeof(int64)); - m_dataTimestampFiles[fileIndex]->increaseRecordCount(size); - } -} - - -void BinaryRecording::addSpikeElectrode(int index, const SpikeChannel* elec) -{ -} - -void BinaryRecording::writeEventMetaData(const MetaDataEvent* event, NpyFile* file) -{ - if (!file || !event) return; - int nMetaData = event->getMetadataValueCount(); - for (int i = 0; i < nMetaData; i++) - { - const MetaDataValue* val = event->getMetaDataValue(i); - file->writeData(val->getRawValuePointer(), val->getDataSize()); - } -} - -void BinaryRecording::writeEvent(int eventIndex, const MidiMessage& event) -{ - 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)); - - uint16 chan = ev->getChannel() +1; - rec->channelFile->writeData(&chan, sizeof(uint16)); - - if (ev->getEventType() == EventChannel::TTL) - { - TTLEvent* ttl = static_cast(ev.get()); - int16 data = (ttl->getChannel()+1) * (ttl->getState() ? 1 : -1); - rec->mainFile->writeData(&data, sizeof(int16)); - if (rec->extraFile) - rec->extraFile->writeData(ttl->getTTLWordPointer(), info->getDataSize()); - } - else - { - rec->mainFile->writeData(ev->getRawDataPointer(), info->getDataSize()); - } - - writeEventMetaData(ev.get(), rec->metaDataFile); - increaseEventCounts(rec); -} - -void BinaryRecording::writeTimestampSyncText(uint16 sourceID, uint16 sourceIdx, int64 timestamp, float, String text) -{ - if (!m_syncTextFile) - return; - m_syncTextFile->writeText(text + "\n", false, false); -} - - - -void BinaryRecording::writeSpike(int electrodeIndex, const SpikeEvent* spike) -{ - const SpikeChannel* channel = getSpikeChannel(electrodeIndex); - EventRecording* rec = m_spikeFiles[m_spikeFileIndexes[electrodeIndex]]; - uint16 spikeChannel = m_spikeChannelIndexes[electrodeIndex]; - - int totalSamples = channel->getTotalSamples() * channel->getNumChannels(); - - - 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. - { - std::cerr << "(spike) Write buffer overrun, resizing to" << totalSamples << std::endl; - m_bufferSize = totalSamples; - m_scaledBuffer.malloc(totalSamples); - m_intBuffer.malloc(totalSamples); - } - 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)); - - rec->channelFile->writeData(&spikeChannel, sizeof(uint16)); - - uint16 sortedID = spike->getSortedID(); - rec->extraFile->writeData(&sortedID, sizeof(uint16)); - writeEventMetaData(spike, rec->metaDataFile); - - increaseEventCounts(rec); -} - -void BinaryRecording::increaseEventCounts(EventRecording* rec) -{ - rec->mainFile->increaseRecordCount(); - rec->timestampFile->increaseRecordCount(); - if (rec->extraFile) rec->extraFile->increaseRecordCount(); - if (rec->channelFile) rec->channelFile->increaseRecordCount(); - if (rec->metaDataFile) rec->metaDataFile->increaseRecordCount(); -} - -RecordEngineManager* BinaryRecording::getEngineManager() -{ - RecordEngineManager* man = new RecordEngineManager("RAWBINARY", "Binary", &(engineFactory)); - EngineParameter* param; - param = new EngineParameter(EngineParameter::BOOL, 0, "Record TTL full words", true); - man->addParameter(param); - - return man; -} - -void BinaryRecording::setParameter(EngineParameter& parameter) -{ - boolParameter(0, m_saveTTLWords); -} - -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; - } -} diff --git a/Source/Plugins/BinaryWriter/BinaryRecording.h b/Source/Plugins/BinaryWriter/BinaryRecording.h deleted file mode 100644 index 98d76badc2..0000000000 --- a/Source/Plugins/BinaryWriter/BinaryRecording.h +++ /dev/null @@ -1,101 +0,0 @@ -/* ------------------------------------------------------------------- - -This file is part of the Open Ephys GUI -Copyright (C) 2013 Open Ephys - ------------------------------------------------------------------- - -This program is free software: you can redistribute it and/or modify -it under the terms of the GNU General Public License as published by -the Free Software Foundation, either version 3 of the License, or -(at your option) any later version. - -This program is distributed in the hope that it will be useful, -but WITHOUT ANY WARRANTY; without even the implied warranty of -MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -GNU General Public License for more details. - -You should have received a copy of the GNU General Public License -along with this program. If not, see . - -*/ -#ifndef BINARYRECORDING_H -#define BINARYRECORDING_H - -#include -#include "SequentialBlockFile.h" -#include "NpyFile.h" - -namespace BinaryRecordingEngine -{ - - class BinaryRecording : public RecordEngine - { - public: - BinaryRecording(); - ~BinaryRecording(); - - String getEngineID() const override; - void openFiles(File rootFolder, int experimentNumber, int recordingNumber) override; - void closeFiles() override; - void writeData(int writeChannel, int realChannel, const float* buffer, int size) override; - void writeEvent(int eventIndex, const MidiMessage& event) override; - void resetChannels() override; - 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: - - class EventRecording - { - public: - ScopedPointer mainFile; - ScopedPointer timestampFile; - ScopedPointer metaDataFile; - ScopedPointer channelFile; - ScopedPointer extraFile; - }; - - - NpyFile* createEventMetadataFile(const MetaDataEventObject* channel, String fileName, DynamicObject* jsonObject); - void createChannelMetaData(const MetaDataInfoObject* channel, DynamicObject* jsonObject); - void writeEventMetaData(const MetaDataEvent* event, NpyFile* file); - void increaseEventCounts(EventRecording* rec); - static String jsonTypeValue(BaseType type); - static String getProcessorString(const InfoObjectCommon* channelInfo); - - bool m_saveTTLWords{ true }; - - HeapBlock m_scaledBuffer; - HeapBlock m_intBuffer; - HeapBlock m_tsBuffer; - int m_bufferSize; - - OwnedArray m_DataFiles; - Array m_channelIndexes; - Array m_fileIndexes; - OwnedArray m_eventFiles; - OwnedArray m_spikeFiles; - OwnedArray m_dataTimestampFiles; - ScopedPointer m_syncTextFile; - - Array m_spikeFileIndexes; - Array m_spikeChannelIndexes; - - int m_recordingNum; - Array m_startTS; - - - //Compile-time constants - const int samplesPerBlock{ 4096 }; - - }; - -} - -#endif \ No newline at end of file diff --git a/Source/Plugins/BinaryWriter/FileMemoryBlock.h b/Source/Plugins/BinaryWriter/FileMemoryBlock.h deleted file mode 100644 index eb61a2fb7d..0000000000 --- a/Source/Plugins/BinaryWriter/FileMemoryBlock.h +++ /dev/null @@ -1,68 +0,0 @@ -/* ------------------------------------------------------------------- - -This file is part of the Open Ephys GUI -Copyright (C) 2013 Open Ephys - ------------------------------------------------------------------- - -This program is free software: you can redistribute it and/or modify -it under the terms of the GNU General Public License as published by -the Free Software Foundation, either version 3 of the License, or -(at your option) any later version. - -This program is distributed in the hope that it will be useful, -but WITHOUT ANY WARRANTY; without even the implied warranty of -MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -GNU General Public License for more details. - -You should have received a copy of the GNU General Public License -along with this program. If not, see . - -*/ - -#ifndef FILEMEMORYBLOCK_H -#define FILEMEMORYBLOCK_H - -#include - -namespace BinaryRecordingEngine -{ - - template - class FileMemoryBlock - { - public: - FileMemoryBlock(FileOutputStream* file, int blockSize, uint64 offset) : - m_data(blockSize, true), - m_file(file), - m_blockSize(blockSize), - m_offset(offset) - {}; - ~FileMemoryBlock() { - if (!m_flushed) - { - m_file->write(m_data, m_blockSize*sizeof(StorageType)); - } - }; - - inline uint64 getOffset() { return m_offset; } - inline StorageType* getData() { return m_data.getData(); } - void partialFlush(size_t size, bool markFlushed = true) - { - std::cout << "flushing last block " << size << std::endl; - m_file->write(m_data, size*sizeof(StorageType)); - if (markFlushed) - m_flushed = true; - } - - private: - HeapBlock m_data; - FileOutputStream* const m_file; - const int m_blockSize; - const uint64 m_offset; - bool m_flushed{ false }; - JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR(FileMemoryBlock); - }; -} -#endif \ No newline at end of file diff --git a/Source/Plugins/BinaryWriter/NpyFile.cpp b/Source/Plugins/BinaryWriter/NpyFile.cpp deleted file mode 100644 index 68b3f66d15..0000000000 --- a/Source/Plugins/BinaryWriter/NpyFile.cpp +++ /dev/null @@ -1,218 +0,0 @@ -/* ------------------------------------------------------------------- - -This file is part of the Open Ephys GUI -Copyright (C) 2017 Open Ephys - ------------------------------------------------------------------- - -This program is free software: you can redistribute it and/or modify -it under the terms of the GNU General Public License as published by -the Free Software Foundation, either version 3 of the License, or -(at your option) any later version. - -This program is distributed in the hope that it will be useful, -but WITHOUT ANY WARRANTY; without even the implied warranty of -MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -GNU General Public License for more details. - -You should have received a copy of the GNU General Public License -along with this program. If not, see . - -*/ - -#include "NpyFile.h" - -using namespace BinaryRecordingEngine; - -NpyFile::NpyFile(String path, const Array& typeList) -{ - - m_dim1 = 1; - m_dim2 = 1; - - /*If there is only one element on the list but is - an array, make this a multidimensional file. - */ - if (typeList.size() == 1) - { - NpyType type = typeList[0]; - if (type.getType() != BaseType::CHAR) //strings work different - m_dim1 = type.getTypeLength(); - } - - if (!openFile(path)) - return; - writeHeader(typeList); - -} - -NpyFile::NpyFile(String path, NpyType type, unsigned int dim) -{ - - if (!openFile(path)) - return; - - Array typeList; - typeList.add(type); - m_dim1 = dim; - m_dim2 = type.getTypeLength(); - writeHeader(typeList); - -} - -bool NpyFile::openFile(String path) -{ - File file(path); - Result res = file.create(); - if (res.failed()) - { - std::cerr << "Error creating file " << path << ":" << res.getErrorMessage() << std::endl; - return false; - } - m_file = file.createOutputStream(); - if (!m_file) - return false; - - m_okOpen = true; - return true; -} - -void NpyFile::writeHeader(const Array& typeList) -{ - bool multiValue = typeList.size() > 1; - String header = "{'descr': "; - header.preallocateBytes(100); - - if (multiValue) - header += "["; - - int nTypes = typeList.size(); - - for (int i = 0; i < nTypes; i++) - { - NpyType& type = typeList.getReference(i); - if (i > 0) header += ", "; - if (multiValue) - header += "('" + type.getName() + "', '" + type.getTypeString() + "', (" + String(type.getTypeLength()) + ",))"; - else - header += "'" + type.getTypeString() + "'"; - } - if (multiValue) - header += "]"; - header += ", 'fortran_order': False, 'shape': "; - - m_countPos = header.length() + 10; - header += "(1,), }"; - int padding = (int((header.length() + 30) / 16) + 1) * 16; - header = header.paddedRight(' ', padding); - header += '\n'; - - uint8 magicNum = 0x093; - m_file->write(&magicNum, sizeof(uint8)); - String magic = "NUMPY"; - uint16 len = header.length(); - m_file->write(magic.toUTF8(), magic.getNumBytesAsUTF8()); - uint16 ver = 0x0001; - m_file->write(&ver, sizeof(uint16)); - m_file->write(&len, sizeof(uint16)); - m_file->write(header.toUTF8(), len); -} - -NpyFile::~NpyFile() -{ - if (m_file->setPosition(m_countPos)) - { - String newShape = "("; - newShape.preallocateBytes(20); - newShape += String(m_recordCount) + ","; - if (m_dim1 > 1) - { - newShape += String(m_dim1) + ","; - } - if (m_dim2 > 1) - newShape += String(m_dim2); - newShape += "), }"; - m_file->write(newShape.toUTF8(), newShape.getNumBytesAsUTF8()); - } - else - { - std::cerr << "Error. Unable to seek to update header on file " << m_file->getFile().getFullPathName() << std::endl; - } -} - -void NpyFile::writeData(const void* data, size_t size) -{ - m_file->write(data, size); -} - -void NpyFile::increaseRecordCount(int count) -{ - m_recordCount += count; -} - - -NpyType::NpyType(String n, BaseType t, size_t l) - : name(n), type(t), length(l) -{ -} - -NpyType::NpyType(BaseType t, size_t l) - : name(String::empty), type(t), length(l) -{ -} - -NpyType::NpyType() - : name(String::empty), type(BaseType::INT8), length(1) -{ - -} - -String NpyType::getTypeString() const -{ - switch (type) - { - case BaseType::CHAR: - return "S" + String(length + 1); //account for the null separator - case BaseType::INT8: - return ". - -*/ - -#ifndef NPYFILE_H -#define NPYFILE_H - -#include - -namespace BinaryRecordingEngine -{ - - class NpyType - { - public: - NpyType(String, BaseType, size_t); - NpyType(BaseType, size_t); - NpyType(); - String getName() const; - String getTypeString() const; - int getTypeLength() const; - BaseType getType() const; - private: - String name; - BaseType type; - size_t length; - }; - - class NpyFile - { - public: - NpyFile(String path, const Array& typeList); - NpyFile(String path, NpyType type, unsigned int dim = 1); - ~NpyFile(); - void writeData(const void* data, size_t size); - void increaseRecordCount(int count = 1); - private: - bool openFile(String path); - void writeHeader(const Array& typeList); - ScopedPointer m_file; - bool m_okOpen{ false }; - int64 m_recordCount{ 0 }; - size_t m_countPos; - unsigned int m_dim1; - unsigned int m_dim2; - }; - -}; -#endif \ No newline at end of file diff --git a/Source/Plugins/BinaryWriter/SequentialBlockFile.cpp b/Source/Plugins/BinaryWriter/SequentialBlockFile.cpp deleted file mode 100644 index 334de359d2..0000000000 --- a/Source/Plugins/BinaryWriter/SequentialBlockFile.cpp +++ /dev/null @@ -1,161 +0,0 @@ -/* ------------------------------------------------------------------- - -This file is part of the Open Ephys GUI -Copyright (C) 2013 Open Ephys - ------------------------------------------------------------------- - -This program is free software: you can redistribute it and/or modify -it under the terms of the GNU General Public License as published by -the Free Software Foundation, either version 3 of the License, or -(at your option) any later version. - -This program is distributed in the hope that it will be useful, -but WITHOUT ANY WARRANTY; without even the implied warranty of -MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -GNU General Public License for more details. - -You should have received a copy of the GNU General Public License -along with this program. If not, see . - -*/ - -#include "SequentialBlockFile.h" - -using namespace BinaryRecordingEngine; - -SequentialBlockFile::SequentialBlockFile(int nChannels, int samplesPerBlock) : -m_file(nullptr), -m_nChannels(nChannels), -m_samplesPerBlock(samplesPerBlock), -m_blockSize(nChannels*samplesPerBlock), -m_lastBlockFill(0) -{ - m_memBlocks.ensureStorageAllocated(blockArrayInitSize); - for (int i = 0; i < nChannels; i++) - m_currentBlock.add(-1); -} - -SequentialBlockFile::~SequentialBlockFile() -{ - //Ensure that all remaining blocks are flushed in order. Keep the last one - int n = m_memBlocks.size(); - for (int i = 0; i < n - 1; i++) - { - m_memBlocks.remove(0); - } - - //manually flush the last one to avoid trailing zeroes - m_memBlocks[0]->partialFlush(m_lastBlockFill * m_nChannels); -} - -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; - - m_memBlocks.add(new FileBlock(m_file, m_blockSize, 0)); - return true; -} - -bool SequentialBlockFile::writeChannel(uint64 startPos, int channel, int16* data, int nSamples) -{ - if (!m_file) - return false; - - int bIndex = m_memBlocks.size() - 1; - if ((bIndex < 0) || (m_memBlocks[bIndex]->getOffset() + m_samplesPerBlock) < (startPos + nSamples)) - allocateBlocks(startPos, nSamples); - - for (bIndex = m_memBlocks.size() - 1; bIndex >= 0; bIndex--) - { - if (m_memBlocks[bIndex]->getOffset() <= startPos) - break; - } - if (bIndex < 0) - { - std::cerr << "BINARY WRITER: Memory block unloaded ahead of time for chan " << channel << " start " << startPos << " ns " << nSamples << " first " << m_memBlocks[0]->getOffset() <getOffset(); - int startMemPos = startIdx*m_nChannels; - int dataIdx = 0; - int lastBlockIdx = m_memBlocks.size() - 1; - while (writtenSamples < nSamples) - { - int16* blockPtr = m_memBlocks[bIndex]->getData(); - int samplesToWrite = jmin((nSamples - writtenSamples), (m_samplesPerBlock - startIdx)); - for (int i = 0; i < samplesToWrite; i++) - { - *(blockPtr + startMemPos + channel + i*m_nChannels) = *(data + dataIdx); - dataIdx++; - } - writtenSamples += samplesToWrite; - - //Update the last block fill index - size_t samplePos = startIdx + samplesToWrite; - if (bIndex == lastBlockIdx && samplePos > m_lastBlockFill) - { - m_lastBlockFill = samplePos; - } - - startIdx = 0; - startMemPos = 0; - bIndex++; - } - m_currentBlock.set(channel, bIndex - 1); //store the last block a channel was written in - return true; -} - -void SequentialBlockFile::allocateBlocks(uint64 startIndex, int numSamples) -{ - //First deallocate full blocks - //Search for the earliest unused block; - unsigned int minBlock = 0xFFFFFFFF; //large number; - for (int i = 0; i < m_nChannels; i++) - { - if (m_currentBlock[i] < minBlock) - minBlock = m_currentBlock[i]; - } - - //Update block indexes - for (int i = 0; i < m_nChannels; i++) - { - m_currentBlock.set(i, m_currentBlock[i] - minBlock); - } - - m_memBlocks.removeRange(0, minBlock); - - //for (int i = 0; i < minBlock; i++) - //{ - //Not the most efficient way, as it has to move back all the elements, but it's a simple array of pointers, so it's quick enough - // m_memBlocks.remove(0); - //} - - //Look for the last available position and calculate needed space - uint64 lastOffset = m_memBlocks.getLast()->getOffset(); - uint64 maxAddr = lastOffset + m_samplesPerBlock - 1; - uint64 newSpaceNeeded = numSamples - (maxAddr - startIndex); - int newBlocks = (newSpaceNeeded + m_samplesPerBlock - 1) / m_samplesPerBlock; //Fast ceiling division - - for (int i = 0; i < newBlocks; i++) - { - lastOffset += m_samplesPerBlock; - m_memBlocks.add(new FileBlock(m_file, m_blockSize, lastOffset)); - } - if (newBlocks > 0) - m_lastBlockFill = 0; //we've added some new blocks, so the last one will be empty -} - diff --git a/Source/Plugins/BusseLabBinaryWriter/BinaryRecording.cpp b/Source/Plugins/BusseLabBinaryWriter/BinaryRecording.cpp new file mode 100644 index 0000000000..089c011efb --- /dev/null +++ b/Source/Plugins/BusseLabBinaryWriter/BinaryRecording.cpp @@ -0,0 +1,465 @@ +/* +------------------------------------------------------------------ + +This file is part of the Open Ephys GUI +Copyright (C) 2013 Open Ephys + +------------------------------------------------------------------ + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program. If not, see . + +*/ + +#include "BinaryRecording.h" + +#define MAX_BUFFER_SIZE 40960 + +using namespace BinaryRecordingEngine; + +BinaryRecording::BinaryRecording() +{ + m_scaledBuffer.malloc(MAX_BUFFER_SIZE); + m_intBuffer.malloc(MAX_BUFFER_SIZE); +} + +BinaryRecording::~BinaryRecording() +{ + +} + +String BinaryRecording::getEngineID() const +{ + return "BUSSELABRAWBINARY"; +} + +String BinaryRecording::getProcessorString(const InfoObjectCommon* channelInfo) +{ + String fName = (channelInfo->getCurrentNodeName().replaceCharacter(' ', '_') + "-" + + String(channelInfo->getCurrentNodeID())); + if (channelInfo->getCurrentNodeID() == channelInfo->getSourceNodeID()) + // found the channel source + { + fName += "." + String(channelInfo->getSubProcessorIdx()); + } + else + { + fName += "_" + String(channelInfo->getSourceNodeID()) + "." + + String(channelInfo->getSubProcessorIdx()); + } + fName += File::separatorString; + return fName; +} + +String BinaryRecording::getRecordingNumberString(int recordingNumber) +{ + String s = ""; + if (recordingNumber > 0) + s = "_r" + String(recordingNumber).paddedLeft('0', 2); // pad with at most 1 leading 0 + return s; +} + +void BinaryRecording::openFiles(File rootFolder, String baseName, int recordingNumber) +{ + String basepath = rootFolder.getFullPathName() + rootFolder.separatorString + baseName; + + int nRecProcessors = getNumRecordedProcessors(); + if (nRecProcessors != 1) + std::cerr << "ERROR: BusseLabBinaryWriter plugin assumes only 1 recorded processor, " + << "found " << nRecProcessors << std::endl; + + // collect some channel parameters: + int nRecChans = getNumRecordedChannels(); // num recorded channels + int nHeadstageChans = getNumHeadstageChannels(); // number of headstage chans + std::cout << "getNumRecordedChannels: " << nRecChans << std::endl; + std::cout << "getNumHeadstageChannels: " << nHeadstageChans << std::endl; + m_channelIndexes.insertMultiple(0, 0, nRecChans); + m_fileIndexes.insertMultiple(0, 0, nRecChans); + + const RecordProcessorInfo& pInfo0 = getProcessorInfo(0); // info for processor 0 + int recordedChan0 = pInfo0.recordedChannels[0]; + int realChan0 = getRealChannel(recordedChan0); + const DataChannel* datachan0 = getDataChannel(realChan0); + + // compare each chan's sample rate and uV per AD to that of chani 0: + int sample_rate = datachan0->getSampleRate(); + double uV_per_AD = datachan0->getBitVolts(); + + // iterate over all chans enabled for recording in processor 0: + var chanis; + var chanNames; + for (int chani = 0; chani < nRecChans; chani++) + { + int recordedChan = pInfo0.recordedChannels[chani]; + /// TODO: what's the difference between recordedChan and realChan? + int realChan = getRealChannel(recordedChan); // does some kind of dereferencing? + const DataChannel* datachan = getDataChannel(realChan); + //chanis.append(recordedChan); + chanis.append(realChan); + chanNames.append(datachan->getName()); + + // some diagnostics: + //std::cout << "chan getName: " << datachan->getName() << std::endl; + //std::cout << "chan getSourceNodeID: " << datachan->getSourceNodeID() << std::endl; + //std::cout << "chan getSubProcessorIdx: " << datachan->getSubProcessorIdx() + //<< std::endl; + //std::cout << "chan getSourceIndex: " << datachan->getSourceIndex() << std::endl; + //std::cout << "chan getDescription: " << datachan->getDescription() << std::endl; + + // compare to chani 0: + if (datachan->getSampleRate() != sample_rate) + std::cerr << "ERROR: sample rate of chan " << realChan << " == " + << datachan->getSampleRate() << " != " << sample_rate << std::endl; + if (datachan->getBitVolts() != uV_per_AD) + std::cerr << "ERROR: uV_per_AD of chan " << realChan << " == " + << datachan->getBitVolts() << " != " << uV_per_AD << std::endl; + + // fill in m_channelIndexes and m_fileIndexes for use in writeData, though + // given the simplified setup assumed, these might not be necessary any more: + /// TODO: is this right? shouldn't the args be reversed?????????????? + m_channelIndexes.set(recordedChan, chani); // index, value + m_fileIndexes.set(recordedChan, 0); + } + std::cout << "Recording chanis: " << JSON::toString(chanis, true) << std::endl; + std::cout << "Recording chanNames: " << JSON::toString(chanNames, true) << std::endl; + + // open .dat file: + String datFileName = basepath; + datFileName += getRecordingNumberString(recordingNumber) + ".dat"; + ScopedPointer bFile = new SequentialBlockFile(nRecChans, + samplesPerBlock); + std::cout << "OPENING FILE: " << datFileName << std::endl; + if (bFile->openFile(datFileName)) + m_DataFiles.add(bFile.release()); + else + m_DataFiles.add(nullptr); + + // get start timestamps for all enabled channels - should be the same for all?: + int nsamples_offset = getTimestamp(0); + for (int i = 0; i < nRecChans; i++) + { + if (i == 0) + std::cout << "Start timestamp: " << nsamples_offset << std::endl; + jassert(getTimestamp(i) == nsamples_offset); + m_startTS.add(getTimestamp(i)); + } + Time now = Time::getCurrentTime(); + String datetime = now.toISO8601(true); + String tz = now.getUTCOffsetString(true); + datetime = datetime.upToLastOccurrenceOf(tz, false, false); // strip time zone + + // parse the chanmap to extract probe_name and adapter_name, separated by __: + String sep = "__"; + var chanmapnames = CoreServices::getChannelMapNames(); + if (chanmapnames.size() != 1) + { + std::cerr << "ERROR: Need exactly 1 channel map, found: " + << JSON::toString(chanmapnames, true) << std::endl; + JUCEApplication::quit(); + } + String chanmapname = chanmapnames[0]; + std::cout << "Extracting probe and adapter names from channel map name '" << chanmapname + << "'" << std::endl; + String probe_name = chanmapname.upToFirstOccurrenceOf(sep, false, false); + String adapter_name = chanmapname.fromFirstOccurrenceOf(sep, false, false); + std::cout << "Extracted probe_name: " << probe_name << std::endl; + std::cout << "Extracted adapter_name: " << adapter_name << std::endl; + + // collect chans array: + std::cout << "Assuming '" << probe_name << "' chans are 1-based" << std::endl; + int chanbase = 1; + var chans; + for (int i = 0; i < nRecChans; i++) + chans.append((int)chanis[i] + chanbase); // convert to chanbase-based chans + std::cout << "Saving headstage chans: " << JSON::toString(chans, true) << std::endl; + + // Handle auxchans here + //var auxchans; + + // collect .dat metadata in JSON data structure: + DynamicObject::Ptr json = new DynamicObject(); + //json->setProperty("dat_fname", datFileName); + json->setProperty("nchans", nRecChans); // number of enabled headstage (?) chans + json->setProperty("sample_rate", sample_rate); + json->setProperty("dtype", "int16"); + json->setProperty("uV_per_AD", uV_per_AD); + json->setProperty("probe_name", probe_name); + if (adapter_name != "") // add adapter_name field only if adapter is defined for this probe + json->setProperty("adapter_name", adapter_name); + // add chans field only if some chans have been disabled: + if (nRecChans != nHeadstageChans) + json->setProperty("chans", chans); + // normally don't have any analog input auxchans: + //if (auxchans) + //json->setProperty("auxchans", auxchans); + json->setProperty("nsamples_offset", nsamples_offset); + json->setProperty("datetime", datetime); + json->setProperty("author", "Open-Ephys, BusseLabBinaryWriter plugin"); + String version = CoreServices::getGUIVersion() + ", " + BusseLabBinaryWriterPluginVersion; + json->setProperty("version", version); + json->setProperty("notes", ""); + + std::cout << "METADATA:" << std::endl; + String jsonstr = JSON::toString(var(json), false); // JUCE 5.3.2 has maximumDecimalPlaces + std::cout << jsonstr << std::endl; + + // write .dat metadata to .dat.json file: + String jsonFileName = basepath; + jsonFileName += getRecordingNumberString(recordingNumber) + ".dat.json"; + File jsonf = File(jsonFileName); + Result res = jsonf.create(); + if (res.failed()) + std::cerr << "Error creating JSON file:" << res.getErrorMessage() << std::endl; + ScopedPointer jsonFile = jsonf.createOutputStream(); + std::cout << "WRITING FILE: " << jsonFileName << std::endl; + // this writeText() is from JUCE 5.3.2, see commit 06be1c2: + jsonFile->writeText(jsonstr, false, false, nullptr); + jsonFile->flush(); + + // open .msg.txt and .din.npy event files: + int nEventChans = getNumRecordedEventChannels(); + for (int evChani = 0; evChani < nEventChans; evChani++) + { + const EventChannel* chan = getEventChannel(evChani); + + switch (chan->getChannelType()) + { + case EventChannel::TEXT: + { + String msgFileName = basepath; + msgFileName += getRecordingNumberString(recordingNumber) + ".msg.txt"; + std::cout << "OPENING FILE: " << msgFileName << std::endl; + File msgf = File(msgFileName); + Result res = msgf.create(); + if (res.failed()) + std::cerr << "Error creating message text file:" << res.getErrorMessage() + << std::endl; + else + m_msgFile = msgf.createOutputStream(); // store file handle + // this writeText() is from JUCE 5.3.2, see commit 06be1c2: + m_msgFile->writeText(getMessageHeader(datetime), false, false, nullptr); + m_msgFile->flush(); + break; + } + case EventChannel::TTL: + { + if (!m_saveTTLWords) + break; + String dinFileName = basepath; + dinFileName += getRecordingNumberString(recordingNumber) + ".din.npy"; + std::cout << "OPENING FILE: " << dinFileName << std::endl; + ScopedPointer rec = new EventRecording(); + // 2D, each row is [timestamp, word]: + NpyType dindtype = NpyType(BaseType::INT64, 2); + rec->dataFile = new NpyFile(dinFileName, dindtype); + m_dinFile = rec.release(); // store pointer to rec object + break; + } + } + } + + // open .spikes.npy file: + String spikeFileName = basepath; + spikeFileName += getRecordingNumberString(recordingNumber) + ".spikes.npy"; + std::cout << "OPENING FILE: " << spikeFileName << std::endl; + ScopedPointer rec = new EventRecording(); + // 3D, each row is [timestamp, chani, clusteri]: + NpyType spikedtype = NpyType(BaseType::INT64, 3); + rec->dataFile = new NpyFile(spikeFileName, spikedtype); + m_spikeFile = rec.release(); // store pointer to rec object + + //m_recordingNum = recordingNumber; // don't really need to store this? +} + + +template +void dataToVar(var& dataTo, const void* dataFrom, int length) +{ + const FROM* buffer = reinterpret_cast(dataFrom); + for (int i = 0; i < length; i++) + { + dataTo.append(static_cast(*(buffer + i))); + } +} + + +void BinaryRecording::closeFiles() +{ + resetChannels(); +} + +void BinaryRecording::resetChannels() +{ + // clear all stored objects, including open file handles? + m_DataFiles.clear(); + m_channelIndexes.clear(); + m_fileIndexes.clear(); + m_dinFile = nullptr; + m_spikeFile = nullptr; + m_msgFile = nullptr; + + m_scaledBuffer.malloc(MAX_BUFFER_SIZE); + m_intBuffer.malloc(MAX_BUFFER_SIZE); + m_bufferSize = MAX_BUFFER_SIZE; + m_startTS.clear(); +} + +void BinaryRecording::writeData(int writeChannel, int realChannel, const float* buffer, + int size) +{ + if (size > m_bufferSize) + // shouldn't happen, and if it does it'll be slow, but better this than crashing + { + std::cerr << "Write buffer overrun, resizing to " << size << std::endl; + m_bufferSize = size; + m_scaledBuffer.malloc(size); + m_intBuffer.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); + int fileIndex = m_fileIndexes[writeChannel]; + m_DataFiles[fileIndex]->writeChannel(getTimestamp(writeChannel) - m_startTS[writeChannel], + m_channelIndexes[writeChannel], + m_intBuffer.getData(), size); +} + + +void BinaryRecording::addSpikeElectrode(int index, const SpikeChannel* elec) +{ +} + +void BinaryRecording::writeEvent(int eventIndex, const MidiMessage& event) +{ + EventPtr ev = Event::deserializeFromMessage(event, getEventChannel(eventIndex)); + EventRecording* rec = m_dinFile; + if (!rec) + return; + const EventChannel* info = getEventChannel(eventIndex); + int64 ts = ev->getTimestamp(); + if (ev->getEventType() == EventChannel::TEXT) + { + const String tsstr = String(ts); + //String msg = String((char*)ev->getRawDataPointer(), info->getDataSize()); + const String msg = String((char*)ev->getRawDataPointer()); + // this writeText() is from JUCE 5.3.2, see commit 06be1c2: + m_msgFile->writeText(tsstr + "\t" + msg + '\n', false, false, nullptr); + m_msgFile->flush(); + } + else if (ev->getEventType() == EventChannel::TTL) + { + TTLEvent* ttl = static_cast(ev.get()); + // cast void pointer to uint8 pointer, dereference, cast to int64: + int64 word = (int64)*(uint8*)(ttl->getTTLWordPointer()); + rec->dataFile->writeData(&ts, sizeof(int64)); // timestamp + rec->dataFile->writeData(&word, sizeof(int64)); // digital input word + increaseEventCounts(rec); + // if old and new words differ at the experiment bit, flush to disk so that online + // analysis can be performed on the dinFile: + int64 expBitChanged = (word ^ m_lastTTLWord) & m_experimentBit; + //std::cout << "expBitChanged: " << expBitChanged << std::endl; + if (expBitChanged) + { + std::cout << "Experiment bit change detected, flushing .din to disk" << std::endl; + rec->dataFile->updateHeader(); + } + m_lastTTLWord = word; // update + } + else + { + std::cerr << "Error. Don't know how to handle event type " << ev->getEventType() + << std::endl; + } +} + +String BinaryRecording::getMessageHeader(String datetime) +{ + String header = "## Generated by Open-Ephys " + CoreServices::getGUIVersion() + + ", BusseLabBinaryWriter plugin " + BusseLabBinaryWriterPluginVersion + + "\n"; + header += "## " + datetime + "\n"; + header += "## Processor start time is index of first sample in .dat file\n"; + header += "## Message log format: samplei message\n"; + header += "samplei\tmessage\n"; // column names for loading into Pandas DataFrame + return header; +} + +void BinaryRecording::writeTimestampSyncText(uint16 sourceID, uint16 sourceIdx, + int64 timestamp, float, String text) +{ + if (!m_msgFile) + return; + // this writeText() is from JUCE 5.3.2, see commit 06be1c2: + m_msgFile->writeText("## " + text + "\n", false, false, nullptr); + m_msgFile->flush(); +} + +void BinaryRecording::writeSpike(int electrodeIndex, const SpikeEvent* spike) +{ + int64 ts = spike->getTimestamp(); + /* + electrodeIndex is really just the plot index, which isn't relevant + (each electrode has exactly 1 plot, because only single electrodes are allowed now) + What we care about is the actual data channel that plot is plotting, so that's + what we'll write to file. + */ + const SpikeChannel* spikeChan = spike->getChannelInfo(); + String chanName = spikeChan->getName(); // should represent probe channel, not ADC channel + // "CH": ADC chan, for channel maps without "labels" field + // "PR": probe chan, for channel maps with "labels" field + // strip "PR" from start of chanName, use remaining string as numeric ID, convert to int64: + if (!chanName.startsWith("PR")) + { + std::cerr << "ERROR: data must be piped through a channel map with a 'labels'" + "field specifying probe channel labels, resulting in all chanNames " + "starting with 'PR'" << std::endl; + std::cerr << "Got chanName: " << chanName << std::endl; + JUCEApplication::quit(); + } + String chanIDstr = chanName.trimCharactersAtStart("PR"); + int64 chanID = chanIDstr.getLargeIntValue(); + int64 sortedID = (int64)(uint16)spike->getSortedID(); + EventRecording* rec = m_spikeFile; + rec->dataFile->writeData(&ts, sizeof(int64)); // timestamp + rec->dataFile->writeData(&chanID, sizeof(int64)); // spike channel + rec->dataFile->writeData(&sortedID, sizeof(int64)); // cluster ID + increaseEventCounts(rec); + //std::cout << "ts " << ts << std::endl; + //std::cout << "chanID " << chanID << std::endl; + //std::cout << "sortedID " << sortedID << std::endl; +} + +void BinaryRecording::increaseEventCounts(EventRecording* rec) +{ + rec->dataFile->increaseRecordCount(); + //if (rec->tsFile) rec->tsFile->increaseRecordCount(); + //if (rec->extraFile) rec->extraFile->increaseRecordCount(); + //if (rec->chanFile) rec->chanFile->increaseRecordCount(); +} + +RecordEngineManager* BinaryRecording::getEngineManager() +{ + RecordEngineManager* man = new RecordEngineManager("BUSSELABRAWBINARY", "Binary", + &(engineFactory)); + EngineParameter* param; + param = new EngineParameter(EngineParameter::BOOL, 0, "Record TTL full words", true); + man->addParameter(param); + return man; +} + +void BinaryRecording::setParameter(EngineParameter& parameter) +{ + boolParameter(0, m_saveTTLWords); +} diff --git a/Source/Plugins/BusseLabBinaryWriter/BinaryRecording.h b/Source/Plugins/BusseLabBinaryWriter/BinaryRecording.h new file mode 100644 index 0000000000..cbfa128bf4 --- /dev/null +++ b/Source/Plugins/BusseLabBinaryWriter/BinaryRecording.h @@ -0,0 +1,94 @@ +/* +------------------------------------------------------------------ + +This file is part of the Open Ephys GUI +Copyright (C) 2013 Open Ephys + +------------------------------------------------------------------ + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program. If not, see . + +*/ +#ifndef BINARYRECORDING_H +#define BINARYRECORDING_H + +#include +#include "SequentialBlockFile.h" +#include "NpyFile.h" + +namespace BinaryRecordingEngine +{ + + class BinaryRecording : public RecordEngine + { + public: + BinaryRecording(); + ~BinaryRecording(); + + String getEngineID() const override; + void openFiles(File rootFolder, String baseName, int recordingNumber) override; + void closeFiles() override; + void writeData(int writeChannel, int realChannel, const float* buffer, int size) override; + void writeEvent(int eventIndex, const MidiMessage& event) override; + void resetChannels() override; + 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; + String getMessageHeader(String datetime); + void setParameter(EngineParameter& parameter) override; + + static RecordEngineManager* getEngineManager(); + + private: + + class EventRecording + { + public: + ScopedPointer dataFile; + //ScopedPointer tsFile; + //ScopedPointer chanFile; + //ScopedPointer extraFile; + }; + + void increaseEventCounts(EventRecording* rec); + static String getProcessorString(const InfoObjectCommon* channelInfo); + String getRecordingNumberString(int recordingNumber); + + bool m_saveTTLWords{ true }; + int64 m_lastTTLWord{ 0 }; + int64 m_experimentBit{ 1 << 0 }; // first bit (1 bit shifted left 0 positions) + + HeapBlock m_scaledBuffer; + HeapBlock m_intBuffer; + int m_bufferSize; + + OwnedArray m_DataFiles; + Array m_channelIndexes; + Array m_fileIndexes; + ScopedPointer m_dinFile; + ScopedPointer m_spikeFile; + ScopedPointer m_msgFile; + + //int m_recordingNum; + Array m_startTS; + + //Compile-time constants + const int samplesPerBlock{ 4096 }; + const String BusseLabBinaryWriterPluginVersion = "0.5"; + + }; + +} + +#endif diff --git a/Source/Plugins/BusseLabBinaryWriter/FileMemoryBlock.h b/Source/Plugins/BusseLabBinaryWriter/FileMemoryBlock.h new file mode 100644 index 0000000000..5cd9d67c1a --- /dev/null +++ b/Source/Plugins/BusseLabBinaryWriter/FileMemoryBlock.h @@ -0,0 +1,68 @@ +/* +------------------------------------------------------------------ + +This file is part of the Open Ephys GUI +Copyright (C) 2013 Open Ephys + +------------------------------------------------------------------ + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program. If not, see . + +*/ + +#ifndef FILEMEMORYBLOCK_H +#define FILEMEMORYBLOCK_H + +#include + +namespace BinaryRecordingEngine +{ + + template + class FileMemoryBlock + { + public: + FileMemoryBlock(FileOutputStream* file, int blockSize, uint64 offset) : + m_data(blockSize, true), + m_file(file), + m_blockSize(blockSize), + m_offset(offset) + {}; + ~FileMemoryBlock() { + if (!m_flushed) + { + m_file->write(m_data, m_blockSize*sizeof(StorageType)); + } + }; + + inline uint64 getOffset() { return m_offset; } + inline StorageType* getData() { return m_data.getData(); } + void partialFlush(size_t size, bool markFlushed = true) + { + std::cout << "flushing last block " << size << std::endl; + m_file->write(m_data, size*sizeof(StorageType)); + if (markFlushed) + m_flushed = true; + } + + private: + HeapBlock m_data; + FileOutputStream* const m_file; + const int m_blockSize; + const uint64 m_offset; + bool m_flushed{ false }; + JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR(FileMemoryBlock); + }; +} +#endif diff --git a/Source/Plugins/BinaryWriter/Makefile b/Source/Plugins/BusseLabBinaryWriter/Makefile similarity index 100% rename from Source/Plugins/BinaryWriter/Makefile rename to Source/Plugins/BusseLabBinaryWriter/Makefile diff --git a/Source/Plugins/BusseLabBinaryWriter/NpyFile.cpp b/Source/Plugins/BusseLabBinaryWriter/NpyFile.cpp new file mode 100644 index 0000000000..981ac18e72 --- /dev/null +++ b/Source/Plugins/BusseLabBinaryWriter/NpyFile.cpp @@ -0,0 +1,262 @@ +/* +------------------------------------------------------------------ + +This file is part of the Open Ephys GUI +Copyright (C) 2017 Open Ephys + +------------------------------------------------------------------ + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program. If not, see . + +Specification of the .npy file format is at: + +http://www.numpy.org/neps/nep-0001-npy-format.html + +Python implementation is at: + +https://github.com/numpy/numpy/blob/master/numpy/lib/format.py + +*/ + +#include "NpyFile.h" + +using namespace BinaryRecordingEngine; + +NpyFile::NpyFile(String path, const Array& typeList) +{ + m_dim1 = 1; + m_dim2 = 1; + + /*If there is only one element on the list but is + an array, make this a multidimensional file. + */ + if (typeList.size() == 1) + { + NpyType type = typeList[0]; + if (type.getType() != BaseType::CHAR) //strings work different + m_dim1 = type.getTypeLength(); + } + + if (!openFile(path)) + return; + writeHeader(typeList); +} + +NpyFile::NpyFile(String path, NpyType type, unsigned int dim) +{ + if (!openFile(path)) + return; + + Array typeList; + typeList.add(type); + m_dim1 = dim; + m_dim2 = type.getTypeLength(); + writeHeader(typeList); +} + +bool NpyFile::openFile(String path) +{ + File file(path); + Result res = file.create(); + if (res.failed()) + { + std::cerr << "Error creating file " << path << ":" << res.getErrorMessage() + << std::endl; + return false; + } + file.deleteFile(); // overwrite, never append a new .npy file to end of an existing one + // output stream buffer size defaults to 32768 bytes, but is irrelevant because + // each updateHeader() call triggers a m_file->flush() to disk: + m_file = file.createOutputStream(); + if (!m_file) + return false; + + m_okOpen = true; + return true; +} + +String NpyFile::getShapeString() +{ + String shape; + shape.preallocateBytes(32); + shape = "("; + shape += String(m_recordCount) + ","; + if (m_dim1 > 1) + { + shape += " " + String(m_dim1) + ","; + } + if (m_dim2 > 1) + shape += " " + String(m_dim2); + shape += "), }"; + return shape; +} + +void NpyFile::writeHeader(const Array& typeList) +{ + uint8 magicNum = 0x93; + String magicStr = "NUMPY"; + uint16 ver = 0x0001; + // magic = magic number + magic string + magic version + int magicLen = sizeof(uint8) + magicStr.getNumBytesAsUTF8() + sizeof(uint16); + int nbytesAlign = 64; // header should use an integer multiple of this many bytes + + bool multiValue = typeList.size() > 1; + String strHeader; + strHeader.preallocateBytes(128); + strHeader = "{'descr': "; + + if (multiValue) + strHeader += "["; + + int nTypes = typeList.size(); + + for (int i = 0; i < nTypes; i++) + { + NpyType& type = typeList.getReference(i); + if (i > 0) strHeader += ", "; + if (multiValue) + strHeader += "('" + type.getName() + "', '" + type.getTypeString() + + "', (" + String(type.getTypeLength()) + ",))"; + else + strHeader += "'" + type.getTypeString() + "'"; + } + if (multiValue) + strHeader += "]"; + strHeader += ", 'fortran_order': False, 'shape': "; + + // save byte offset of shape field in .npy file + // magic + header length field + current string header length: + m_shapePos = magicLen + sizeof(uint16) + strHeader.length(); + strHeader += getShapeString(); // inits to 0 records, i.e. 1st dim has length 0 + int baseHeaderLen = magicLen + sizeof(uint16) + strHeader.length() + 1; // +1 for newline + int padlen = nbytesAlign - (baseHeaderLen % nbytesAlign); + strHeader = strHeader.paddedRight(' ', strHeader.length() + padlen); + strHeader += '\n'; + uint16 strHeaderLen = strHeader.length(); + + m_file->write(&magicNum, sizeof(uint8)); + m_file->write(magicStr.toUTF8(), magicStr.getNumBytesAsUTF8()); + m_file->write(&ver, sizeof(uint16)); + m_file->write(&strHeaderLen, sizeof(uint16)); + m_file->write(strHeader.toUTF8(), strHeaderLen); + m_headerLen = m_file->getPosition(); // total header length + m_file->flush(); +} + +void NpyFile::updateHeader() +{ + // overwrite the shape part of the header - even without explicitly calling + // m_file->flush(), overwriting seems to trigger a flush to disk, + // while appending to end of file does not + int64 currentPos = m_file->getPosition(); // returns int64, necessary for big files + if (m_file->setPosition(m_shapePos)) + { + String newShape = getShapeString(); + if (m_shapePos + newShape.getNumBytesAsUTF8() + 1 > m_headerLen) // +1 for newline + { + std::cerr << "Error. Header has grown too big to update in-place " << std::endl; + } + m_file->write(newShape.toUTF8(), newShape.getNumBytesAsUTF8()); + m_file->flush(); // not necessary, already flushed due to overwrite? do it anyway + m_file->setPosition(currentPos); // restore position to end of file + } + else + { + std::cerr << "Error. Unable to seek to update file header" + << m_file->getFile().getFullPathName() << std::endl; + } +} + +NpyFile::~NpyFile() +{ + updateHeader(); +} + +void NpyFile::writeData(const void* data, size_t size) +{ + m_file->write(data, size); +} + +void NpyFile::increaseRecordCount(int count) +{ + int64 old_recordCount = m_recordCount; + m_recordCount += count; + if ((old_recordCount / recordBufferSize) != (m_recordCount / recordBufferSize)) + updateHeader(); // crossed recordBufferSize threshold, update header +} + +NpyType::NpyType(String n, BaseType t, size_t l) + : name(n), type(t), length(l) +{ +} + +NpyType::NpyType(BaseType t, size_t l) + : name(String::empty), type(t), length(l) +{ +} + +NpyType::NpyType() + : name(String::empty), type(BaseType::INT8), length(1) +{ + +} + +String NpyType::getTypeString() const +{ + switch (type) + { + case BaseType::CHAR: + return "|S" + String(length + 1); // null-terminated bytes, account for null separator + case BaseType::INT8: + return "|i1"; + case BaseType::UINT8: + return "|u1"; + case BaseType::INT16: + return ". + +*/ + +#ifndef NPYFILE_H +#define NPYFILE_H + +#include + +namespace BinaryRecordingEngine +{ + + class NpyType + { + public: + NpyType(String, BaseType, size_t); + NpyType(BaseType, size_t); + NpyType(); + String getName() const; + String getTypeString() const; + int getTypeLength() const; + BaseType getType() const; + private: + String name; + BaseType type; + size_t length; + }; + + class NpyFile + { + public: + NpyFile(String path, const Array& typeList); + NpyFile(String path, NpyType type, unsigned int dim = 1); + ~NpyFile(); + void writeData(const void* data, size_t size); + void increaseRecordCount(int count = 1); + void updateHeader(); + private: + bool openFile(String path); + String getShapeString(); + void writeHeader(const Array& typeList); + ScopedPointer m_file; + int64 m_headerLen; // total header length + bool m_okOpen{ false }; + int64 m_recordCount{ 0 }; + size_t m_shapePos; + unsigned int m_dim1; + unsigned int m_dim2; + + // Compile-time constants + + // flush file buffer to disk and update the .npy header every this many records: + const int recordBufferSize{ 1024 }; + + }; + +}; +#endif diff --git a/Source/Plugins/BinaryWriter/OpenEphysLib.cpp b/Source/Plugins/BusseLabBinaryWriter/OpenEphysLib.cpp similarity index 70% rename from Source/Plugins/BinaryWriter/OpenEphysLib.cpp rename to Source/Plugins/BusseLabBinaryWriter/OpenEphysLib.cpp index 4f5679530f..3eec861e00 100644 --- a/Source/Plugins/BinaryWriter/OpenEphysLib.cpp +++ b/Source/Plugins/BusseLabBinaryWriter/OpenEphysLib.cpp @@ -37,34 +37,34 @@ using namespace Plugin; extern "C" EXPORT void getLibInfo(Plugin::LibraryInfo* info) { - info->apiVersion = PLUGIN_API_VER; - info->name = "Binary recording"; - info->libVersion = 1; - info->numPlugins = NUM_PLUGINS; + info->apiVersion = PLUGIN_API_VER; + info->name = "Busse Lab Binary recording"; + info->libVersion = 1; + info->numPlugins = NUM_PLUGINS; } extern "C" EXPORT int getPluginInfo(int index, Plugin::PluginInfo* info) { - switch (index) - { - case 0: - info->type = Plugin::PLUGIN_TYPE_RECORD_ENGINE; - info->recordEngine.name = "Binary"; - info->recordEngine.creator = &(Plugin::createRecordEngine); - break; - default: - return -1; - } + switch (index) + { + case 0: + info->type = Plugin::PLUGIN_TYPE_RECORD_ENGINE; + info->recordEngine.name = "Busse Lab Binary"; + info->recordEngine.creator = &(Plugin::createRecordEngine); + break; + default: + return -1; + } - return 0; + return 0; } #ifdef WIN32 BOOL WINAPI DllMain(IN HINSTANCE hDllHandle, - IN DWORD nReason, - IN LPVOID Reserved) + IN DWORD nReason, + IN LPVOID Reserved) { - return TRUE; + return TRUE; } -#endif \ No newline at end of file +#endif diff --git a/Source/Plugins/BusseLabBinaryWriter/SequentialBlockFile.cpp b/Source/Plugins/BusseLabBinaryWriter/SequentialBlockFile.cpp new file mode 100644 index 0000000000..83e4df36ba --- /dev/null +++ b/Source/Plugins/BusseLabBinaryWriter/SequentialBlockFile.cpp @@ -0,0 +1,167 @@ +/* +------------------------------------------------------------------ + +This file is part of the Open Ephys GUI +Copyright (C) 2013 Open Ephys + +------------------------------------------------------------------ + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program. If not, see . + +*/ + +#include "SequentialBlockFile.h" + +using namespace BinaryRecordingEngine; + +SequentialBlockFile::SequentialBlockFile(int nChannels, int samplesPerBlock) : +m_file(nullptr), +m_nChannels(nChannels), +m_samplesPerBlock(samplesPerBlock), +m_blockSize(nChannels*samplesPerBlock), +m_lastBlockFill(0) +{ + m_memBlocks.ensureStorageAllocated(blockArrayInitSize); + for (int i = 0; i < nChannels; i++) + m_currentBlock.add(-1); +} + +SequentialBlockFile::~SequentialBlockFile() +{ + //Ensure that all remaining blocks are flushed in order. Keep the last one + int n = m_memBlocks.size(); + for (int i = 0; i < n - 1; i++) + { + m_memBlocks.remove(0); + } + + //manually flush the last one to avoid trailing zeroes + m_memBlocks[0]->partialFlush(m_lastBlockFill * m_nChannels); +} + +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; + + m_memBlocks.add(new FileBlock(m_file, m_blockSize, 0)); + return true; +} + +bool SequentialBlockFile::writeChannel(uint64 startPos, int channel, int16* data, int nSamples) +{ + if (!m_file) + return false; + + int bIndex = m_memBlocks.size() - 1; + if ((bIndex < 0) || (m_memBlocks[bIndex]->getOffset() + m_samplesPerBlock) < (startPos + nSamples)) + allocateBlocks(startPos, nSamples); + + for (bIndex = m_memBlocks.size() - 1; bIndex >= 0; bIndex--) + { + if (m_memBlocks[bIndex]->getOffset() <= startPos) + break; + } + if (bIndex < 0) + { + std::cerr << "BINARY WRITER: Memory block unloaded ahead of time for chan " << channel << " start " << startPos << " ns " << nSamples << " first " << m_memBlocks[0]->getOffset() <getOffset(); + int startMemPos = startIdx*m_nChannels; + int dataIdx = 0; + int lastBlockIdx = m_memBlocks.size() - 1; + while (writtenSamples < nSamples) + { + int16* blockPtr = m_memBlocks[bIndex]->getData(); + int samplesToWrite = jmin((nSamples - writtenSamples), (m_samplesPerBlock - startIdx)); + for (int i = 0; i < samplesToWrite; i++) + { + //if (writtenSamples == 0 && *(data + dataIdx) == 0) + //{ + // std::cout << "Found a zero." << std::endl; + // break; + //} + + *(blockPtr + startMemPos + channel + i*m_nChannels) = *(data + dataIdx); + dataIdx++; + } + writtenSamples += samplesToWrite; + + //Update the last block fill index + size_t samplePos = startIdx + samplesToWrite; + if (bIndex == lastBlockIdx && samplePos > m_lastBlockFill) + { + m_lastBlockFill = samplePos; + } + + startIdx = 0; + startMemPos = 0; + bIndex++; + } + m_currentBlock.set(channel, bIndex - 1); //store the last block a channel was written in + return true; +} + +void SequentialBlockFile::allocateBlocks(uint64 startIndex, int numSamples) +{ + //First deallocate full blocks + //Search for the earliest unused block; + unsigned int minBlock = 0xFFFFFFFF; //large number; + for (int i = 0; i < m_nChannels; i++) + { + if (m_currentBlock[i] < minBlock) + minBlock = m_currentBlock[i]; + } + + //Update block indexes + for (int i = 0; i < m_nChannels; i++) + { + m_currentBlock.set(i, m_currentBlock[i] - minBlock); + } + + m_memBlocks.removeRange(0, minBlock); + + //for (int i = 0; i < minBlock; i++) + //{ + //Not the most efficient way, as it has to move back all the elements, but it's a simple array of pointers, so it's quick enough + // m_memBlocks.remove(0); + //} + + //Look for the last available position and calculate needed space + uint64 lastOffset = m_memBlocks.getLast()->getOffset(); + uint64 maxAddr = lastOffset + m_samplesPerBlock - 1; + uint64 newSpaceNeeded = numSamples - (maxAddr - startIndex); + int newBlocks = (newSpaceNeeded + m_samplesPerBlock - 1) / m_samplesPerBlock; //Fast ceiling division + + for (int i = 0; i < newBlocks; i++) + { + lastOffset += m_samplesPerBlock; + m_memBlocks.add(new FileBlock(m_file, m_blockSize, lastOffset)); + } + if (newBlocks > 0) + m_lastBlockFill = 0; //we've added some new blocks, so the last one will be empty +} + diff --git a/Source/Plugins/BinaryWriter/SequentialBlockFile.h b/Source/Plugins/BusseLabBinaryWriter/SequentialBlockFile.h similarity index 55% rename from Source/Plugins/BinaryWriter/SequentialBlockFile.h rename to Source/Plugins/BusseLabBinaryWriter/SequentialBlockFile.h index a5b6e7e095..61cd25a488 100644 --- a/Source/Plugins/BinaryWriter/SequentialBlockFile.h +++ b/Source/Plugins/BusseLabBinaryWriter/SequentialBlockFile.h @@ -29,35 +29,35 @@ along with this program. If not, see . namespace BinaryRecordingEngine { - typedef FileMemoryBlock FileBlock; + typedef FileMemoryBlock FileBlock; - class SequentialBlockFile - { - public: - SequentialBlockFile(int nChannels, int samplesPerBlock); - ~SequentialBlockFile(); + class SequentialBlockFile + { + public: + SequentialBlockFile(int nChannels, int samplesPerBlock); + ~SequentialBlockFile(); - bool openFile(String filename); - bool writeChannel(uint64 startPos, int channel, int16* data, int nSamples); + bool openFile(String filename); + bool writeChannel(uint64 startPos, int channel, int16* data, int nSamples); - private: - ScopedPointer m_file; - const int m_nChannels; - const int m_samplesPerBlock; - const int m_blockSize; - OwnedArray m_memBlocks; - Array m_currentBlock; - size_t m_lastBlockFill; + private: + ScopedPointer m_file; + const int m_nChannels; + const int m_samplesPerBlock; + const int m_blockSize; + OwnedArray m_memBlocks; + Array m_currentBlock; + size_t m_lastBlockFill; - void allocateBlocks(uint64 startIndex, int numSamples); + void allocateBlocks(uint64 startIndex, int numSamples); - //Compile-time parameters - const int streamBufferSize{ 0 }; - const int blockArrayInitSize{ 128 }; + //Compile-time parameters + const int streamBufferSize{ 0 }; + const int blockArrayInitSize{ 128 }; - }; + }; } -#endif \ No newline at end of file +#endif diff --git a/Source/Plugins/BusseLabBinaryWriter/TODO b/Source/Plugins/BusseLabBinaryWriter/TODO new file mode 100644 index 0000000000..870f993667 --- /dev/null +++ b/Source/Plugins/BusseLabBinaryWriter/TODO @@ -0,0 +1,30 @@ +TODO: + +- handle aux and ADC oe chans - need to add auxchans field to .dat.json + - what does the clock divider ratio do? should change the ADC chan sampling rate, but doesn't seem to? Maybe force it to always be 1 for now? + - getBitVolts() for ADC chans is different, and seems ~1000x off? + - yes: case DataChannel::AUX_CHANNEL: return "mV"; + case DataChannel::ADC_CHANNEL: return "V"; + default: return "uV"; + - from RHD2000Thread::getChannelUnits + - also, see RHD2000Thread::setDefaultChannelNames() for in.gain + - also see getAdcBitVolts +- push bandwidth, dsp, noiseslicer, clockdivider round-trip fixes and tweaks to dev branch +- push spike chan display labels and values written to disk to dev branch +- test audio monitor + - make clicking on LFP viewer chan or Spike Viewer chan change audio to that chan +- test CAR before spike detector +- test spike detection and saving + - add some kind of automatic threshold level setting? + - add spike ticks to LFP viewer chans? + - make spike detector editor use actual chan IDs instead of chanis (e.g. single electrode 1) +- make ENTER in message window save the message, so don't need to hit SAVE button +- check assumption that there's only one spike detector in the signal chain +- how does clustering work? does it fill the cluster id field in .spikes.npy properly? +- add git rev to .json/.msg.txt? +- get "Error in Rhd2000EvalBoard::readDataBlock: Incorrect header." randomly, won't exit +- why can't splitters be deleted? +- sometimes rearranging chans in the chanmap segfaults +- store last used file open path in settings.xml + + diff --git a/Source/Plugins/ChannelMappingNode/ChannelMappingEditor.cpp b/Source/Plugins/ChannelMappingNode/ChannelMappingEditor.cpp index ddcbcce467..23e3faa4d7 100755 --- a/Source/Plugins/ChannelMappingNode/ChannelMappingEditor.cpp +++ b/Source/Plugins/ChannelMappingNode/ChannelMappingEditor.cpp @@ -54,13 +54,13 @@ ChannelMappingEditor::ChannelMappingEditor(GenericProcessor* parentNode, bool us resetButton->setClickingTogglesState(false); resetButton->setEnabled(false); - + addAndMakeVisible(electrodeButtonViewport = new Viewport()); electrodeButtonViewport->setBounds(10,30,330,70); electrodeButtonViewport->setScrollBarsShown(true,false,true,true); electrodeButtonHolder = new Component(); electrodeButtonViewport->setViewedComponent(electrodeButtonHolder,false); - + loadButton = new LoadButton(); loadButton->addListener(this); @@ -121,13 +121,13 @@ void ChannelMappingEditor::updateSettings() createElectrodeButtons(getProcessor()->getNumInputs()); previousChannelCount = getProcessor()->getNumInputs(); } - channelCountArray.clearQuick(); - int size = channelArray.size(); - for (int i = 0; i < size; i++) - { - if (enabledChannelArray[channelArray[i]-1]) - channelCountArray.add(channelArray[i]-1); - } + channelCountArray.clearQuick(); + int size = channelArray.size(); + for (int i = 0; i < size; i++) + { + if (enabledChannelArray[channelArray[i]-1]) + channelCountArray.add(channelArray[i]-1); + } } void ChannelMappingEditor::createElectrodeButtons(int numNeeded, bool clearPrevious) @@ -138,8 +138,9 @@ void ChannelMappingEditor::createElectrodeButtons(int numNeeded, bool clearPrevi { electrodeButtons.clear(); - referenceArray.clear(); channelArray.clear(); + labelArray.clear(); + referenceArray.clear(); referenceChannels.clear(); enabledChannelArray.clear(); startButton=0; @@ -188,11 +189,13 @@ void ChannelMappingEditor::createElectrodeButtons(int numNeeded, bool clearPrevi referenceArray.add(-1); getProcessor()->setCurrentChannel(i); - getProcessor()->setParameter(0,i); // set channel mapping to standard channel - getProcessor()->setParameter(1,-1); // set reference to none - getProcessor()->setParameter(3,1); //enable channel + getProcessor()->setParameter(0, i); // set channel mapping to standard channel + getProcessor()->setParameter(5, i); // set channel label to standard channel + getProcessor()->setParameter(1, -1); // set reference to none + getProcessor()->setParameter(3, 1); // enable channel channelArray.add(i+1); + labelArray.add(i+1); enabledChannelArray.add(true); } @@ -203,7 +206,7 @@ void ChannelMappingEditor::createElectrodeButtons(int numNeeded, bool clearPrevi for (int i = 0; i < NUM_REFERENCES; i++) { - getProcessor()->setParameter(2,i); //Clear reference + getProcessor()->setParameter(2, i); // clear reference referenceChannels.add(-1); referenceButtons[i]->setEnabled(true); } @@ -226,15 +229,18 @@ void ChannelMappingEditor::refreshButtonLocations() { ElectrodeButton* button = electrodeButtons[i]; button->setBounds(column*width, row*height, width, height); - totalWidth = jmax(totalWidth, ++column*width); - - if (column % 16 == 0) + totalWidth = jmax(totalWidth, ++column*width); + if (column == 1) { - totalHeight = jmax(totalHeight, ++row*height); - column = 0; + totalHeight = jmax(totalHeight, (row + 1)*height); + } + else if (column == 16) // start a new row after 16 columns + { + row++; + column = 0; } } - electrodeButtonHolder->setSize(totalWidth,totalHeight); + electrodeButtonHolder->setSize(totalWidth, totalHeight); } void ChannelMappingEditor::collapsedStateChanged() @@ -257,24 +263,25 @@ void ChannelMappingEditor::buttonEvent(Button* button) } else if (button == resetButton) { - if (acquisitionIsActive) - { - CoreServices::sendStatusMessage("Cannot change channel order while acquiring"); - return; - } + if (acquisitionIsActive) + { + CoreServices::sendStatusMessage("Cannot change channel order while acquiring"); + return; + } createElectrodeButtons(getProcessor()->getNumInputs()); previousChannelCount = getProcessor()->getNumInputs(); setConfigured(false); - CoreServices::updateSignalChain(this); + setDisplayName(getProcessor()->getName()); + CoreServices::updateSignalChain(this); } else if (button == modifyButton) { - if (acquisitionIsActive) - { - CoreServices::sendStatusMessage("Cannot change channel order while acquiring"); - button->setToggleState(false,dontSendNotification); - return; - } + if (acquisitionIsActive) + { + CoreServices::sendStatusMessage("Cannot change channel order while acquiring"); + button->setToggleState(false,dontSendNotification); + return; + } if (reorderActive) { channelSelector->activateButtons(); @@ -289,15 +296,15 @@ void ChannelMappingEditor::buttonEvent(Button* button) if (referenceChannels[selectedReference] >= 0) { - if (referenceChannels[selectedReference] < channelSelector->getNumChannels()) - a.add(referenceChannels[selectedReference]); - else - { - a.add(channelSelector->getNumChannels() - 1); - getProcessor()->setCurrentChannel(channelSelector->getNumChannels() - 1); - getProcessor()->setParameter(2, selectedReference); - referenceChannels.set(selectedReference, channelSelector->getNumChannels() - 1); - } + if (referenceChannels[selectedReference] < channelSelector->getNumChannels()) + a.add(referenceChannels[selectedReference]); + else + { + a.add(channelSelector->getNumChannels() - 1); + getProcessor()->setCurrentChannel(channelSelector->getNumChannels() - 1); + getProcessor()->setParameter(2, selectedReference); + referenceChannels.set(selectedReference, channelSelector->getNumChannels() - 1); + } } channelSelector->setActiveChannels(a); @@ -520,21 +527,21 @@ void ChannelMappingEditor::buttonEvent(Button* button) if (!acquisitionIsActive) { FileChooser fc("Choose the file name...", - CoreServices::getDefaultUserSaveDirectory(), - "*", - true); + CoreServices::getDefaultUserSaveDirectory(), + "*", + true); if (fc.browseForFileToSave(true)) { File fileToSave = fc.getResult(); - std::cout << fileToSave.getFileName() << std::endl; - CoreServices::sendStatusMessage(writePrbFile(fileToSave)); + std::cout << "Saving channel map: " << fileToSave.getFileName() << std::endl; + CoreServices::sendStatusMessage(writeChanMapFile(fileToSave)); } } else { - CoreServices::sendStatusMessage("Stop acquisition before saving the channel map."); + CoreServices::sendStatusMessage("Stop acquisition before saving the channel map."); } - + } else if (button == loadButton) { @@ -543,20 +550,20 @@ void ChannelMappingEditor::buttonEvent(Button* button) if (!acquisitionIsActive) { FileChooser fc("Choose a file to load...", - CoreServices::getDefaultUserSaveDirectory(), - "*", - true); + CoreServices::getDefaultUserSaveDirectory(), + "*", + true); if (fc.browseForFileToOpen()) { if (reorderActive) modifyButton->setToggleState(false,sendNotificationSync); File fileToOpen = fc.getResult(); - std::cout << fileToOpen.getFileName() << std::endl; - CoreServices::sendStatusMessage(loadPrbFile(fileToOpen)); + std::cout << "Loading channel map: " << fileToOpen.getFileName() << std::endl; + CoreServices::sendStatusMessage(loadChanMapFile(fileToOpen)); } } else { - CoreServices::sendStatusMessage("Stop acquisition before saving the channel map."); + CoreServices::sendStatusMessage("Stop acquisition before saving the channel map."); } } } @@ -569,12 +576,12 @@ void ChannelMappingEditor::setChannelReference(ElectrodeButton* button) if (button->getToggleState()) { referenceArray.set(chan,selectedReference); - getProcessor()->setParameter(1,selectedReference); + getProcessor()->setParameter(1, selectedReference); } else { - referenceArray.set(chan,-1); - getProcessor()->setParameter(1,-1); + referenceArray.set(chan, -1); + getProcessor()->setParameter(1, -1); } } @@ -603,6 +610,7 @@ void ChannelMappingEditor::saveCustomParameters(XmlElement* xml) XmlElement* channelXml = xml->createNewChildElement("CHANNEL"); channelXml->setAttribute("Number", i); channelXml->setAttribute("Mapping", channelArray[i]); + channelXml->setAttribute("Labels", labelArray[i]); channelXml->setAttribute("Reference", referenceArray[channelArray[i]-1]); channelXml->setAttribute("Enabled",enabledChannelArray[channelArray[i]-1]); } @@ -618,7 +626,7 @@ void ChannelMappingEditor::saveCustomParameters(XmlElement* xml) void ChannelMappingEditor::loadCustomParameters(XmlElement* xml) { setConfigured(true); - forEachXmlChildElementWithTagName(*xml, settingXml, "SETTING") + forEachXmlChildElementWithTagName(*xml, settingXml, "SETTING") { if (settingXml->getStringAttribute("Type").equalsIgnoreCase("visibleChannels")) { @@ -637,14 +645,18 @@ void ChannelMappingEditor::loadCustomParameters(XmlElement* xml) { int mapping = channelXml->getIntAttribute("Mapping"); + int label = channelXml->getIntAttribute("Labels"); int reference = channelXml->getIntAttribute("Reference"); bool enabled = channelXml->getBoolAttribute("Enabled"); channelArray.set(i, mapping); + labelArray.set(i, label); referenceArray.set(mapping-1, reference); enabledChannelArray.set(mapping-1,enabled); electrodeButtons[i]->setChannelNum(mapping); + electrodeButtons[i]->setChannelLabel(label); + electrodeButtons[i]->setTooltip("PR"+String(label)); // PR means probe channel electrodeButtons[i]->setEnabled(enabled); electrodeButtons[i]->repaint(); @@ -653,11 +665,13 @@ void ChannelMappingEditor::loadCustomParameters(XmlElement* xml) getProcessor()->setParameter(0, mapping-1); // set mapping + getProcessor()->setParameter(5, label); // set label + getProcessor()->setCurrentChannel(mapping-1); getProcessor()->setParameter(1, reference); // set reference - getProcessor()->setParameter(3,enabled ? 1 : 0); //set enabled + getProcessor()->setParameter(3, enabled ? 1 : 0); // set enabled } } @@ -674,7 +688,7 @@ void ChannelMappingEditor::loadCustomParameters(XmlElement* xml) getProcessor()->setCurrentChannel(channel); - getProcessor()->setParameter(2,i); + getProcessor()->setParameter(2, i); // clear reference? } } @@ -730,6 +744,7 @@ void ChannelMappingEditor::mouseDrag(const MouseEvent& e) initialDraggedButton = electrodeButtons.indexOf(button); lastHoverButton = initialDraggedButton; draggingChannel = button->getChannelNum(); + draggingLabel = button->getChannelLabel(); } else if (isDragging) { @@ -737,10 +752,10 @@ void ChannelMappingEditor::mouseDrag(const MouseEvent& e) int mouseDownY = ev.getMouseDownY()-30; int mouseDownX = ev.getMouseDownX()-10; Point viewPosition =electrodeButtonViewport->getViewPosition(); - + int distanceY = ev.getDistanceFromDragStartY(); int distanceX = ev.getDistanceFromDragStartX(); - + int newPosY = viewPosition.getY()+ mouseDownY + distanceY; int newPosX = viewPosition.getX()+ mouseDownX + distanceX; if ( mouseDownY + distanceY > 70){ @@ -748,8 +763,8 @@ void ChannelMappingEditor::mouseDrag(const MouseEvent& e) }else if( mouseDownY + distanceY < 0 ){ electrodeButtonViewport->setViewPosition(viewPosition.getX(),newPosY); } - - + + int col = (newPosX / 19); if (col < 0) col = 0; else if (col > 16) col = 16; @@ -774,6 +789,9 @@ void ChannelMappingEditor::mouseDrag(const MouseEvent& e) for (int i = lastHoverButton; i > hoverButton; i--) { electrodeButtons[i]->setChannelNum(electrodeButtons[i-1]->getChannelNum()); + int label = electrodeButtons[i-1]->getChannelLabel(); + electrodeButtons[i]->setChannelLabel(label); + electrodeButtons[i]->setTooltip("PR"+String(label)); if (enabledChannelArray[electrodeButtons[i]->getChannelNum()-1]) //Could be more compact, but definitely less legible { electrodeButtons[i]->setToggleState(true, dontSendNotification); @@ -789,6 +807,9 @@ void ChannelMappingEditor::mouseDrag(const MouseEvent& e) for (int i = lastHoverButton; i < hoverButton; i++) { electrodeButtons[i]->setChannelNum(electrodeButtons[i+1]->getChannelNum()); + int label = electrodeButtons[i+1]->getChannelLabel(); + electrodeButtons[i]->setChannelLabel(label); + electrodeButtons[i]->setTooltip("PR"+String(label)); if (enabledChannelArray[electrodeButtons[i]->getChannelNum()-1]) { electrodeButtons[i]->setToggleState(true, dontSendNotification); @@ -800,6 +821,8 @@ void ChannelMappingEditor::mouseDrag(const MouseEvent& e) } } electrodeButtons[hoverButton]->setChannelNum(draggingChannel); + electrodeButtons[hoverButton]->setChannelLabel(draggingLabel); + electrodeButtons[hoverButton]->setTooltip("PR"+String(draggingLabel)); electrodeButtons[hoverButton]->setToggleState(enabledChannelArray[draggingChannel-1], dontSendNotification); lastHoverButton = hoverButton; @@ -835,18 +858,26 @@ void ChannelMappingEditor::mouseUp(const MouseEvent& e) for (int i=from; i <= to; i++) { - setChannelPosition(i,electrodeButtons[i]->getChannelNum()); + setChannelPosition(i, electrodeButtons[i]->getChannelNum()); + setChannelLabel(i, electrodeButtons[i]->getChannelLabel()); } setConfigured(true); - CoreServices::updateSignalChain(this); + CoreServices::updateSignalChain(this); } } void ChannelMappingEditor::setChannelPosition(int position, int channel) { getProcessor()->setCurrentChannel(position); - getProcessor()->setParameter(0,channel-1); - channelArray.set(position,channel); + getProcessor()->setParameter(0, channel-1); + channelArray.set(position, channel); +} + +void ChannelMappingEditor::setChannelLabel(int position, int label) +{ + getProcessor()->setCurrentChannel(position); + getProcessor()->setParameter(5, label); + labelArray.set(position, label); } void ChannelMappingEditor::mouseDoubleClick(const MouseEvent& e) @@ -860,16 +891,16 @@ void ChannelMappingEditor::mouseDoubleClick(const MouseEvent& e) button->setToggleState(false, dontSendNotification); enabledChannelArray.set(button->getChannelNum()-1,false); getProcessor()->setCurrentChannel(button->getChannelNum()-1); - getProcessor()->setParameter(3,0); + getProcessor()->setParameter(3, 0); } else { button->setToggleState(true, dontSendNotification); enabledChannelArray.set(button->getChannelNum()-1,true); getProcessor()->setCurrentChannel(button->getChannelNum()-1); - getProcessor()->setParameter(3,1); + getProcessor()->setParameter(3, 1); } - CoreServices::updateSignalChain(this); + CoreServices::updateSignalChain(this); } } @@ -899,31 +930,31 @@ void ChannelMappingEditor::setConfigured(bool state) isConfigured = state; resetButton->setEnabled(state); resetButton->setToggleState(!state, dontSendNotification); - getProcessor()->setParameter(4,state?1:0); + getProcessor()->setParameter(4, state?1:0); } void ChannelMappingEditor::startAcquisition() { - if (reorderActive) - modifyButton->setToggleState(false,sendNotificationSync); + if (reorderActive) + modifyButton->setToggleState(false,sendNotificationSync); } int ChannelMappingEditor::getChannelDisplayNumber(int chan) const { - if (channelCountArray.size() > chan) - { - return channelCountArray[chan]; - } - else - return chan; + if (channelCountArray.size() > chan) + { + return channelCountArray[chan]; + } + else + return chan; } -String ChannelMappingEditor::writePrbFile(File filename) +String ChannelMappingEditor::writeChanMapFile(File filename) { FileOutputStream outputStream(filename); - outputStream.setPosition(0); - outputStream.truncate(); + outputStream.setPosition(0); + outputStream.truncate(); //outputStream.writeString("channel_groups = "); info = new DynamicObject(); @@ -935,7 +966,14 @@ String ChannelMappingEditor::writePrbFile(File filename) arr.add(var(channelArray[i])); } nestedObj->setProperty("mapping", var(arr)); - + + Array labels; + for (int i = 0; i < labelArray.size(); i++) + { + labels.add(var(labelArray[i])); + } + nestedObj->setProperty("labels", var(labels)); + Array arr2; for (int i = 0; i < referenceArray.size(); i++) { @@ -950,7 +988,7 @@ String ChannelMappingEditor::writePrbFile(File filename) } nestedObj->setProperty("enabled", var(arr3)); - info->setProperty("0", nestedObj); + info->setProperty("chanmap", nestedObj); DynamicObject* nestedObj2 = new DynamicObject(); Array arr4; @@ -962,16 +1000,16 @@ String ChannelMappingEditor::writePrbFile(File filename) info->setProperty("refs", nestedObj2); - DynamicObject* nestedObj3 = new DynamicObject(); + DynamicObject* nestedObj3 = new DynamicObject(); - Array arr5; - for (int i=0; i < channelSelector->getNumChannels(); i++) - { - arr5.add(var(channelSelector->getRecordStatus(i))); - } - nestedObj3->setProperty("channels",var(arr5)); + Array arr5; + for (int i=0; i < channelSelector->getNumChannels(); i++) + { + arr5.add(var(channelSelector->getRecordStatus(i))); + } + nestedObj3->setProperty("channels",var(arr5)); - info->setProperty("recording",nestedObj3); + info->setProperty("recording",nestedObj3); info->writeAsJSON(outputStream, 2, false); @@ -979,7 +1017,7 @@ String ChannelMappingEditor::writePrbFile(File filename) } -String ChannelMappingEditor::loadPrbFile(File filename) +String ChannelMappingEditor::loadChanMapFile(File filename) { FileInputStream inputStream(filename); @@ -987,32 +1025,38 @@ String ChannelMappingEditor::loadPrbFile(File filename) var returnVal = -255; - var channelGroup = json.getProperty(Identifier("0"), returnVal); + var channelGroup = json.getProperty(Identifier("chanmap"), returnVal); if (channelGroup.equalsWithSameType(returnVal)) { - return "Not a valid .prb file."; + return "Not a valid .chanmap file (i.e. a .prb file with a 'labels' field)"; } var mapping = channelGroup[Identifier("mapping")]; Array* map = mapping.getArray(); + var labels = channelGroup[Identifier("labels")]; + Array* lbl = labels.getArray(); + var reference = channelGroup[Identifier("reference")]; Array* ref = reference.getArray(); var enabled = channelGroup[Identifier("enabled")]; Array* enbl = enabled.getArray(); - std::cout << "We found this many: " << map->size() << std::endl; + std::cout << "Found " << map->size() << " channels in channel map" << std::endl; - if (map->size() > previousChannelCount) - createElectrodeButtons(map->size(), false); + if (map->size() > previousChannelCount) + createElectrodeButtons(map->size(), false); for (int i = 0; i < map->size(); i++) { - int ch = map->getUnchecked(i); + int ch = map->getUnchecked(i); channelArray.set(i, ch); + int lb = lbl->getUnchecked(i); + labelArray.set(i, lb); + int rf = ref->getUnchecked(i); referenceArray.set(ch-1, rf); @@ -1020,15 +1064,18 @@ String ChannelMappingEditor::loadPrbFile(File filename) enabledChannelArray.set(ch-1, en); electrodeButtons[i]->setChannelNum(ch); + electrodeButtons[i]->setChannelLabel(lb); + electrodeButtons[i]->setTooltip("PR"+String(lb)); electrodeButtons[i]->setEnabled(en); - - getProcessor()->setCurrentChannel(i); - getProcessor()->setParameter(0,ch-1); - getProcessor()->setCurrentChannel(ch-1); - getProcessor()->setParameter(1, rf); - getProcessor()->setParameter(3, en ? 1 : 0); + + getProcessor()->setCurrentChannel(i); + getProcessor()->setParameter(0, ch-1); + getProcessor()->setParameter(5, lb); + getProcessor()->setCurrentChannel(ch-1); + getProcessor()->setParameter(1, rf); + getProcessor()->setParameter(3, en ? 1 : 0); } - checkUnusedChannels(); + checkUnusedChannels(); var refChans = json[Identifier("refs")]; var channels = refChans[Identifier("channels")]; @@ -1039,7 +1086,7 @@ String ChannelMappingEditor::loadPrbFile(File filename) int ch = chans->getUnchecked(i); referenceChannels.set(i,ch); getProcessor()->setCurrentChannel(ch); - getProcessor()->setParameter(2,i); + getProcessor()->setParameter(2, i); } referenceButtons[0]->setToggleState(true, sendNotificationSync); @@ -1056,19 +1103,20 @@ String ChannelMappingEditor::loadPrbFile(File filename) } } - setConfigured(true); - CoreServices::updateSignalChain(this); + setDisplayName(filename.getFileNameWithoutExtension()); + setConfigured(true); + CoreServices::updateSignalChain(this); - var recChans = json[Identifier("recording")]; - var recording = recChans[Identifier("channels")]; - Array* rec = recording.getArray(); + var recChans = json[Identifier("recording")]; + var recording = recChans[Identifier("channels")]; + Array* rec = recording.getArray(); - for (int i = 0; i < rec->size(); i++) - { - bool recEnabled = rec->getUnchecked(i); - channelSelector->setRecordStatus(i,recEnabled); - } + for (int i = 0; i < rec->size(); i++) + { + bool recEnabled = rec->getUnchecked(i); + channelSelector->setRecordStatus(i,recEnabled); + } - return "Loaded " + filename.getFileName(); + return "Loaded channel map: " + filename.getFileName(); } diff --git a/Source/Plugins/ChannelMappingNode/ChannelMappingEditor.h b/Source/Plugins/ChannelMappingNode/ChannelMappingEditor.h index 291f07f8d7..389cc36205 100755 --- a/Source/Plugins/ChannelMappingNode/ChannelMappingEditor.h +++ b/Source/Plugins/ChannelMappingNode/ChannelMappingEditor.h @@ -65,17 +65,18 @@ class ChannelMappingEditor : public GenericEditor, void collapsedStateChanged(); - void startAcquisition(); + void startAcquisition(); - int getChannelDisplayNumber(int chan) const override; + int getChannelDisplayNumber(int chan) const override; - String writePrbFile(File filename); - String loadPrbFile(File filename); + String writeChanMapFile(File filename); + String loadChanMapFile(File filename); private: - void setChannelReference(ElectrodeButton* button); void setChannelPosition(int position, int channel); + void setChannelLabel(int position, int label); + void setChannelReference(ElectrodeButton* button); void checkUnusedChannels(); void setConfigured(bool state); @@ -92,10 +93,11 @@ class ChannelMappingEditor : public GenericEditor, ScopedPointer electrodeButtonHolder; Array channelArray; + Array labelArray; Array referenceArray; Array referenceChannels; Array enabledChannelArray; - Array channelCountArray; + Array channelCountArray; int previousChannelCount; int selectedReference; @@ -107,6 +109,7 @@ class ChannelMappingEditor : public GenericEditor, bool isDragging; int initialDraggedButton; int draggingChannel; + int draggingLabel; int lastHoverButton; bool isConfigured; diff --git a/Source/Plugins/ChannelMappingNode/ChannelMappingNode.cpp b/Source/Plugins/ChannelMappingNode/ChannelMappingNode.cpp index c06b51de2c..9333d9b527 100644 --- a/Source/Plugins/ChannelMappingNode/ChannelMappingNode.cpp +++ b/Source/Plugins/ChannelMappingNode/ChannelMappingNode.cpp @@ -31,12 +31,14 @@ ChannelMappingNode::ChannelMappingNode() { setProcessorType (PROCESSOR_TYPE_FILTER); - referenceArray.resize (1024); // make room for 1024 channels - channelArray.resize (1024); + channelArray.resize (1024); // make room for 1024 channels + labelArray.resize (1024); + referenceArray.resize (1024); for (int i = 0; i < referenceArray.size(); ++i) { channelArray.set (i, i); + labelArray.set (i, i); referenceArray.set (i, -1); enabledChannelArray.set (i, true); } @@ -82,9 +84,10 @@ void ChannelMappingNode::updateSettings() if ( (enabledChannelArray[channelArray[i]]) && (channelArray[i] < oldChannels.size())) { - DataChannel* oldChan = oldChannels[channelArray[i]]; - oldChannels.set(channelArray[i], nullptr, false); - dataChannelArray.add (oldChan); + DataChannel* oldChan = oldChannels[channelArray[i]]; + oldChannels.set(channelArray[i], nullptr, false); + oldChan->setName("PR" + String(labelArray[i])); // PR means probe channel + dataChannelArray.add (oldChan); recordStates.add (oldChan->getRecordState()); settings.numOutputs++; } @@ -101,25 +104,33 @@ void ChannelMappingNode::updateSettings() void ChannelMappingNode::setParameter (int parameterIndex, float newValue) { - if (parameterIndex == 1) + if (parameterIndex == 0) // mapping + { + channelArray.set (currentChannel, (int) newValue); + } + else if (parameterIndex == 1) // set reference { referenceArray.set (currentChannel, (int) newValue); } - else if (parameterIndex == 2) + else if (parameterIndex == 2) // clear reference? { referenceChannels.set ((int)newValue, currentChannel); } - else if (parameterIndex == 3) + else if (parameterIndex == 3) // enabled { enabledChannelArray.set (currentChannel, (newValue != 0) ? true : false); } - else if (parameterIndex == 4) + else if (parameterIndex == 4) // reset? { editorIsConfigured = (newValue != 0) ? true : false; } + else if (parameterIndex == 5) // label + { + labelArray.set (currentChannel, (int) newValue); + } else { - channelArray.set (currentChannel, (int) newValue); + std::cerr << "ERROR: Unknown parameterIndex: " << String(parameterIndex) << std::endl; } } @@ -133,7 +144,7 @@ void ChannelMappingNode::process (AudioSampleBuffer& buffer) // use copy constructor to set the data to refer to channelBuffer = buffer; - // buffer.clear(); + // buffer.clear(); while (j < settings.numOutputs) { @@ -153,19 +164,16 @@ void ChannelMappingNode::process (AudioSampleBuffer& buffer) && (referenceChannels[referenceArray[realChan]] > -1) && (referenceChannels[referenceArray[realChan]] < channelBuffer.getNumChannels())) { - buffer.addFrom (j, // destChannel - 0, // destStartSample - channelBuffer, // source + buffer.addFrom (j, // destChannel + 0, // destStartSample + channelBuffer, // source channelArray[referenceChannels[referenceArray[realChan]]], // sourceChannel - 0, // sourceStartSample - getNumSamples (j), // numSamples + 0, // sourceStartSample + getNumSamples (j), // numSamples -1.0f); // gain to apply to source (negative for reference) } - ++j; } - ++i; } } - diff --git a/Source/Plugins/ChannelMappingNode/ChannelMappingNode.h b/Source/Plugins/ChannelMappingNode/ChannelMappingNode.h index 5b5b78b109..b2a59b60f3 100644 --- a/Source/Plugins/ChannelMappingNode/ChannelMappingNode.h +++ b/Source/Plugins/ChannelMappingNode/ChannelMappingNode.h @@ -53,9 +53,10 @@ class ChannelMappingNode : public GenericProcessor private: + Array channelArray; + Array labelArray; Array referenceArray; Array referenceChannels; - Array channelArray; Array enabledChannelArray; bool editorIsConfigured; diff --git a/Source/Plugins/KWIKFormat/FileSource/KwikFileSource.cpp b/Source/Plugins/KWIKFormat/FileSource/KwikFileSource.cpp deleted file mode 100644 index 4ca543b52d..0000000000 --- a/Source/Plugins/KWIKFormat/FileSource/KwikFileSource.cpp +++ /dev/null @@ -1,291 +0,0 @@ -/* - ------------------------------------------------------------------ - - This file is part of the Open Ephys GUI - Copyright (C) 2013 Open Ephys - - ------------------------------------------------------------------ - - This program is free software: you can redistribute it and/or modify - it under the terms of the GNU General Public License as published by - the Free Software Foundation, either version 3 of the License, or - (at your option) any later version. - - This program is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU General Public License for more details. - - You should have received a copy of the GNU General Public License - along with this program. If not, see . - -*/ -#include -#include "KwikFileSource.h" -#include - - -using namespace H5; - -#define PROCESS_ERROR std::cerr << "KwikFilesource exception: " << error.getCDetailMsg() << std::endl - -KWIKFileSource::KWIKFileSource() : samplePos(0), skipRecordEngineCheck(false) -{ -} - -KWIKFileSource::~KWIKFileSource() -{ -} - -bool KWIKFileSource::Open(File file) -{ - ScopedPointer tmpFile; - Attribute ver; - uint16 vernum; - try - { - tmpFile = new H5File(file.getFullPathName().toUTF8(),H5F_ACC_RDONLY); - if (!tmpFile->attrExists("kwik_version")) - { - return false; - } - - ver = tmpFile->openAttribute("kwik_version"); - ver.read(PredType::NATIVE_UINT16,&vernum); - if ((vernum < MIN_KWIK_VERSION) || (vernum > MAX_KWIK_VERSION)) - { - return false; - } - - sourceFile = tmpFile; - return true; - - } - catch (FileIException error) - { - PROCESS_ERROR; - return false; - } - catch (AttributeIException error) - { - PROCESS_ERROR; - return false; - } - - //Code should never reach here - return false; -} - -void KWIKFileSource::fillRecordInfo() -{ - Group recordings; - - try - { - recordings = sourceFile->openGroup("/recordings"); - int numObjs = (int) recordings.getNumObjs(); - - for (int i=0; i < numObjs; i++) - { - try - { - Group recordN; - DataSet data; - Attribute attr; - DataSpace dSpace; - float sampleRate; - float bitVolts; - hsize_t dims[3]; - RecordInfo info; - - recordN = recordings.openGroup(String(i).toUTF8()); - data = recordN.openDataSet("data"); - attr = recordN.openAttribute("sample_rate"); - attr.read(PredType::NATIVE_FLOAT,&sampleRate); - attr = recordN.openAttribute("bit_depth"); - attr.read(PredType::NATIVE_FLOAT,&bitVolts); - dSpace = data.getSpace(); - dSpace.getSimpleExtentDims(dims); - - info.name="Record "+String(i); - info.numSamples = dims[0]; - info.sampleRate = sampleRate; - - bool foundBitVoltArray = false; - HeapBlock bitVoltArray(dims[1]); - - try - { - recordN = recordings.openGroup((String(i) + "/application_data").toUTF8()); - try - { - DataSet bV = recordN.openDataSet("channel_bit_volts"); - bV.read(bitVoltArray.getData(), PredType::NATIVE_FLOAT); - foundBitVoltArray = true; - } - catch (GroupIException) - { } - catch (DataSetIException) - { } - if (!foundBitVoltArray) - { - attr = recordN.openAttribute("channel_bit_volts"); - attr.read(ArrayType(PredType::NATIVE_FLOAT, 1, &dims[1]), bitVoltArray); - foundBitVoltArray = true; - } - } catch (GroupIException) - { - } catch (AttributeIException) - { - } - - for (int j = 0; j < dims[1]; j++) - { - RecordedChannelInfo c; - c.name = "CH" + String(j); - - if (foundBitVoltArray) - c.bitVolts = bitVoltArray[j]; - else - c.bitVolts = bitVolts; - - info.channels.add(c); - } - infoArray.add(info); - availableDataSets.add(i); - numRecords++; - } - catch (GroupIException) - { - } - catch (DataSetIException) - { - } - catch (AttributeIException) - { - } - catch (DataSpaceIException error) - { - PROCESS_ERROR; - } - } - } - catch (FileIException error) - { - PROCESS_ERROR; - } - catch (GroupIException error) - { - PROCESS_ERROR; - } -} - -void KWIKFileSource::updateActiveRecord() -{ - samplePos=0; - try - { - String path = "/recordings/" + String(availableDataSets[activeRecord.get()]) + "/data"; - dataSet = new DataSet(sourceFile->openDataSet(path.toUTF8())); - } - catch (FileIException error) - { - PROCESS_ERROR; - } - catch (DataSetIException error) - { - PROCESS_ERROR; - } -} - -void KWIKFileSource::seekTo(int64 sample) -{ - samplePos = sample % getActiveNumSamples(); -} - -int KWIKFileSource::readData(int16* buffer, int nSamples) -{ - DataSpace fSpace,mSpace; - int samplesToRead; - int nChannels = getActiveNumChannels(); - hsize_t dim[3],offset[3]; - - if (samplePos + nSamples > getActiveNumSamples()) - { - samplesToRead = (int) getActiveNumSamples() - (int) samplePos; - } - else - { - samplesToRead = nSamples; - } - - try - { - fSpace = dataSet->getSpace(); - dim[0] = samplesToRead; - dim[1] = nChannels; - dim[2] = 1; - offset[0] = samplePos; - offset[1] = 0; - offset[2] = 0; - - fSpace.selectHyperslab(H5S_SELECT_SET,dim,offset); - mSpace = DataSpace(2,dim); - - dataSet->read(buffer,PredType::NATIVE_INT16,mSpace,fSpace); - samplePos += samplesToRead; - return samplesToRead; - - } - catch (DataSetIException error) - { - PROCESS_ERROR; - return 0; - } - catch (DataSpaceIException error) - { - PROCESS_ERROR; - return 0; - } - return 0; -} - -void KWIKFileSource::processChannelData(int16* inBuffer, float* outBuffer, int channel, int64 numSamples) -{ - int n = getActiveNumChannels(); - float bitVolts = getChannelInfo(channel).bitVolts; - - for (int i=0; i < numSamples; i++) - { - *(outBuffer+i) = *(inBuffer+(n*i)+channel) * bitVolts; - } - -} - -bool KWIKFileSource::isReady() -{ - //HDF5 is by default not thread-safe, so we must warn the user. - if ((!skipRecordEngineCheck) && (CoreServices::getSelectedRecordEngineId() == "KWIK")) - { - int res = AlertWindow::showYesNoCancelBox(AlertWindow::WarningIcon, "Record format conflict", - "Both the selected input file for the File Reader and the output file format for recording use the HDF5 library.\n" - "This library is, by default, not thread safe, so running both at the same time might cause unexpected crashes (chances increase with signal complexity and number of recorded channels).\n\n" - "If you have a custom-built hdf5 library with the thread safe features turned on, you can safely continue, but performance will be reduced.\n" - "More information on:\n" - "https://www.hdfgroup.org/HDF5/doc/TechNotes/ThreadSafeLibrary.html/n" - "https://www.hdfgroup.org/hdf5-quest.html/n/n" - "Do you want to continue acquisition?", "Yes", "Yes and don't ask again", "No"); - switch (res) - { - case 2: - skipRecordEngineCheck = true; - case 1: - return true; - break; - default: - return false; - } - } - else - return true; -} diff --git a/Source/Plugins/KWIKFormat/FileSource/KwikFileSource.h b/Source/Plugins/KWIKFormat/FileSource/KwikFileSource.h deleted file mode 100644 index 581462967d..0000000000 --- a/Source/Plugins/KWIKFormat/FileSource/KwikFileSource.h +++ /dev/null @@ -1,70 +0,0 @@ -/* - ------------------------------------------------------------------ - - This file is part of the Open Ephys GUI - Copyright (C) 2013 Open Ephys - - ------------------------------------------------------------------ - - This program is free software: you can redistribute it and/or modify - it under the terms of the GNU General Public License as published by - the Free Software Foundation, either version 3 of the License, or - (at your option) any later version. - - This program is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU General Public License for more details. - - You should have received a copy of the GNU General Public License - along with this program. If not, see . - -*/ - -#ifndef KWIKFILESOURCE_H_INCLUDED -#define KWIKFILESOURCE_H_INCLUDED - -#include - -#define MIN_KWIK_VERSION 2 -#define MAX_KWIK_VERSION 2 - -class HDF5RecordingData; -namespace H5 -{ -class DataSet; -class H5File; -class DataType; -} - -class KWIKFileSource : public FileSource -{ -public: - KWIKFileSource(); - ~KWIKFileSource(); - - int readData (int16* buffer, int nSamples) override; - - void seekTo (int64 sample) override; - - void processChannelData (int16* inBuffer, float* outBuffer, int channel, int64 numSamples) override; - - bool isReady() override; - - -private: - bool Open (File file) override; - void fillRecordInfo() override; - void updateActiveRecord() override; - - ScopedPointer sourceFile; - ScopedPointer dataSet; - - int64 samplePos; - Array availableDataSets; - bool skipRecordEngineCheck; -}; - - - -#endif // KWIKFILESOURCE_H_INCLUDED diff --git a/Source/Plugins/KWIKFormat/Makefile b/Source/Plugins/KWIKFormat/Makefile deleted file mode 100644 index 3b6cc54f68..0000000000 --- a/Source/Plugins/KWIKFormat/Makefile +++ /dev/null @@ -1,41 +0,0 @@ - -LIBNAME := $(notdir $(CURDIR)) -OBJDIR := $(OBJDIR)/$(LIBNAME) -TARGET := $(LIBNAME).so - -CXXFLAGS := $(CXXFLAGS) -I/usr/include/hdf5/serial -I/usr/local/hdf5/include -LDFLAGS := $(LDFLAGS) -L/usr/lib/x86_64-linux-gnu/hdf5/serial -L/usr/local/hdf5/lib -lhdf5 -lhdf5_cpp -l:OpenEphysHDF5Lib.so - -SRC_DIR := ${shell find ./ -type d -print} -VPATH := $(SOURCE_DIRS) - -SRC := $(foreach sdir,$(SRC_DIR),$(wildcard $(sdir)/*.cpp)) -OBJ := $(addprefix $(OBJDIR)/,$(notdir $(SRC:.cpp=.o))) - -BLDCMD := $(CXX) -shared -o $(OUTDIR)/$(TARGET) $(OBJ) $(LDFLAGS) $(RESOURCES) $(TARGET_ARCH) - -VPATH = $(SRC_DIR) - -.PHONY: objdir - -$(OUTDIR)/$(TARGET): objdir $(OBJ) - -@mkdir -p $(BINDIR) - -@mkdir -p $(LIBDIR) - -@mkdir -p $(OUTDIR) - @echo "Building $(TARGET)" - @$(BLDCMD) - -$(OBJDIR)/%.o : %.cpp - @echo "Compiling $<" - @$(CXX) $(CXXFLAGS) -o "$@" -c "$<" - - -objdir: - -@mkdir -p $(OBJDIR) - -clean: - @echo "Cleaning $(LIBNAME)" - -@rm -rf $(OBJDIR) - -@rm -f $(OUTDIR)/$(TARGET) - --include $(OBJ:%.o=%.d) diff --git a/Source/Plugins/KWIKFormat/OpenEphysLib.cpp b/Source/Plugins/KWIKFormat/OpenEphysLib.cpp deleted file mode 100644 index ed107bdc4c..0000000000 --- a/Source/Plugins/KWIKFormat/OpenEphysLib.cpp +++ /dev/null @@ -1,77 +0,0 @@ -/* ------------------------------------------------------------------- - -This file is part of the Open Ephys GUI -Copyright (C) 2013 Open Ephys - ------------------------------------------------------------------- - -This program is free software: you can redistribute it and/or modify -it under the terms of the GNU General Public License as published by -the Free Software Foundation, either version 3 of the License, or -(at your option) any later version. - -This program is distributed in the hope that it will be useful, -but WITHOUT ANY WARRANTY; without even the implied warranty of -MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -GNU General Public License for more details. - -You should have received a copy of the GNU General Public License -along with this program. If not, see . - -*/ - -#include -#include "RecordEngine/HDF5Recording.h" -#include "FileSource/KwikFileSource.h" -#include -#ifdef WIN32 -#include -#define EXPORT __declspec(dllexport) -#else -#define EXPORT __attribute__((visibility("default"))) -#endif - - -using namespace Plugin; -#define NUM_PLUGINS 2 - -extern "C" EXPORT void getLibInfo(Plugin::LibraryInfo* info) -{ - info->apiVersion = PLUGIN_API_VER; - info->name = "Kwik Format"; - info->libVersion = 1; - info->numPlugins = NUM_PLUGINS; -} - -extern "C" EXPORT int getPluginInfo(int index, Plugin::PluginInfo* info) -{ - switch (index) - { - case 0: - info->type = Plugin::PLUGIN_TYPE_RECORD_ENGINE; - info->recordEngine.name = "Kwik"; - info->recordEngine.creator = &(Plugin::createRecordEngine); - break; - case 1: - info->type = Plugin::PLUGIN_TYPE_FILE_SOURCE; - info->fileSource.name = "Kwd file"; - info->fileSource.extensions = "kwd"; - info->fileSource.creator = &(Plugin::createFileSource); - break; - default: - return -1; - } - - return 0; -} - -#ifdef WIN32 -BOOL WINAPI DllMain(IN HINSTANCE hDllHandle, - IN DWORD nReason, - IN LPVOID Reserved) -{ - return TRUE; -} - -#endif diff --git a/Source/Plugins/KWIKFormat/RecordEngine/.HDF5Recording.cpp.swo b/Source/Plugins/KWIKFormat/RecordEngine/.HDF5Recording.cpp.swo deleted file mode 100644 index d76ea40bbe..0000000000 Binary files a/Source/Plugins/KWIKFormat/RecordEngine/.HDF5Recording.cpp.swo and /dev/null differ diff --git a/Source/Plugins/KWIKFormat/RecordEngine/HDF5Recording.cpp b/Source/Plugins/KWIKFormat/RecordEngine/HDF5Recording.cpp deleted file mode 100644 index 185f885ed1..0000000000 --- a/Source/Plugins/KWIKFormat/RecordEngine/HDF5Recording.cpp +++ /dev/null @@ -1,293 +0,0 @@ -/* - ------------------------------------------------------------------ - - This file is part of the Open Ephys GUI - Copyright (C) 2014 Florian Franzen - - ------------------------------------------------------------------ - - This program is free software: you can redistribute it and/or modify - it under the terms of the GNU General Public License as published by - the Free Software Foundation, either version 3 of the License, or - (at your option) any later version. - - This program is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU General Public License for more details. - - You should have received a copy of the GNU General Public License - along with this program. If not, see . - - */ - -#include "HDF5Recording.h" -#define MAX_BUFFER_SIZE 40960 -#define CHANNEL_TIMESTAMP_PREALLOC_SIZE 128 -#define CHANNEL_TIMESTAMP_MIN_WRITE 32 -#define TIMESTAMP_EACH_NSAMPLES 1024 - -HDF5Recording::HDF5Recording() : processorIndex(-1), bufferSize(MAX_BUFFER_SIZE), hasAcquired(false) -{ - //timestamp = 0; - scaledBuffer.malloc(MAX_BUFFER_SIZE); - intBuffer.malloc(MAX_BUFFER_SIZE); -} - -HDF5Recording::~HDF5Recording() -{ -} - -String HDF5Recording::getEngineID() const -{ - return "KWIK"; -} - -// void HDF5Recording::updateTimeStamp(int64 timestamp) -// { -// this->timestamp = timestamp; -// } - -void HDF5Recording::registerProcessor(const GenericProcessor* proc) -{ - KWIKRecordingInfo* info = new KWIKRecordingInfo(); - info->sample_rate = proc->getSampleRate(); - info->bit_depth = 16; - info->multiSample = false; - infoArray.add (info); - - fileArray.add (new KWDFile()); - bitVoltsArray.add (new Array); - sampleRatesArray.add (new Array); - channelsPerProcessor.add (0); - processorIndex++; -} - -void HDF5Recording::resetChannels() -{ - scaledBuffer.malloc(MAX_BUFFER_SIZE); - intBuffer.malloc(MAX_BUFFER_SIZE); - bufferSize = MAX_BUFFER_SIZE; - processorIndex = -1; - fileArray.clear(); - channelsPerProcessor.clear(); - bitVoltsArray.clear(); - sampleRatesArray.clear(); - processorMap.clear(); - infoArray.clear(); - recordedChanToKWDChan.clear(); - channelLeftOverSamples.clear(); - channelTimestampArray.clear(); - if (spikesFile) - spikesFile->resetChannels(); -} - -void HDF5Recording::addDataChannel(int index,const DataChannel* chan) -{ - processorMap.add(processorIndex); -} - -void HDF5Recording::openFiles(File rootFolder, int experimentNumber, int recordingNumber) -{ - String basepath = rootFolder.getFullPathName() + rootFolder.separatorString + "experiment" + String(experimentNumber); - //KWE file - eventFile->initFile(basepath); - eventFile->open(); - - //KWX file - spikesFile->initFile(basepath); - spikesFile->open(); - spikesFile->startNewRecording(recordingNumber); - - //Let's just put the first processor (usually the source node) on the KWIK for now - infoArray[0]->name = String("Open Ephys Recording #") + String(recordingNumber); - - /* if (hasAcquired) - infoArray[0]->start_time = (*timestamps)[getChannel(0)->sourceNodeId]; //(*timestamps).begin()->first; - else - infoArray[0]->start_time = 0;*/ - infoArray[0]->start_time = getTimestamp(0); - - infoArray[0]->start_sample = 0; - eventFile->startNewRecording(recordingNumber,infoArray[0]); - - //KWD files - recordedChanToKWDChan.clear(); - Array processorRecPos; - processorRecPos.insertMultiple(0, 0, fileArray.size()); - for (int i = 0; i < getNumRecordedChannels(); i++) - { - int index = processorMap[getRealChannel(i)]; - if (!fileArray[index]->isOpen()) - { - fileArray[index]->initFile(getDataChannel(getRealChannel(i))->getCurrentNodeID(), basepath); - infoArray[index]->start_time = getTimestamp(i); - } - - channelsPerProcessor.set(index, channelsPerProcessor[index] + 1); - bitVoltsArray[index]->add(getDataChannel(getRealChannel(i))->getBitVolts()); - sampleRatesArray[index]->add(getDataChannel(getRealChannel(i))->getSampleRate()); - if (getDataChannel(getRealChannel(i))->getSampleRate() != infoArray[index]->sample_rate) - { - infoArray[index]->multiSample = true; - } - int procPos = processorRecPos[index]; - recordedChanToKWDChan.add(procPos); - processorRecPos.set(index, procPos+1); - channelTimestampArray.add(new Array); - channelTimestampArray.getLast()->ensureStorageAllocated(CHANNEL_TIMESTAMP_PREALLOC_SIZE); - channelLeftOverSamples.add(0); - } - - for (int i = 0; i < fileArray.size(); i++) - { - if ((!fileArray[i]->isOpen()) && (fileArray[i]->isReadyToOpen())) - { - fileArray[i]->open(channelsPerProcessor[i]); - } - if (fileArray[i]->isOpen()) - { - // File f(fileArray[i]->getFileName()); - // eventFile->addKwdFile(f.getFileName()); - - infoArray[i]->name = String("Open Ephys Recording #") + String(recordingNumber); - // infoArray[i]->start_time = timestamp; - infoArray[i]->start_sample = 0; - infoArray[i]->bitVolts.clear(); - infoArray[i]->bitVolts.addArray(*bitVoltsArray[i]); - infoArray[i]->channelSampleRates.clear(); - infoArray[i]->channelSampleRates.addArray(*sampleRatesArray[i]); - fileArray[i]->startNewRecording(recordingNumber,bitVoltsArray[i]->size(),infoArray[i]); - } - } - - hasAcquired = true; -} - -void HDF5Recording::closeFiles() -{ - eventFile->stopRecording(); - eventFile->close(); - spikesFile->stopRecording(); - spikesFile->close(); - for (int i = 0; i < fileArray.size(); i++) - { - if (fileArray[i]->isOpen()) - { - std::cout << "Closed file " << i << std::endl; - fileArray[i]->stopRecording(); - fileArray[i]->close(); - bitVoltsArray[i]->clear(); - sampleRatesArray[i]->clear(); - } - channelsPerProcessor.set(i, 0); - } - recordedChanToKWDChan.clear(); - channelTimestampArray.clear(); - channelLeftOverSamples.clear(); - scaledBuffer.malloc(MAX_BUFFER_SIZE); - intBuffer.malloc(MAX_BUFFER_SIZE); - bufferSize = MAX_BUFFER_SIZE; -} - -void HDF5Recording::writeData(int writeChannel, int realChannel, const float* buffer, int size) -{ - if (size > bufferSize) //Shouldn't happen, and if it happens it'll be slow, but better this than crashing. Will be reset on flie close and reset. - { - std::cerr << "Write buffer overrun, resizing to" << size << std::endl; - bufferSize = size; - scaledBuffer.malloc(size); - intBuffer.malloc(size); - } - double multFactor = 1 / (float(0x7fff) * getDataChannel(realChannel)->getBitVolts()); - int index = processorMap[realChannel]; //CHECK - FloatVectorOperations::copyWithMultiply(scaledBuffer.getData(), buffer, multFactor, size); - AudioDataConverters::convertFloatToInt16LE(scaledBuffer.getData(), intBuffer.getData(), size); - fileArray[index]->writeRowData(intBuffer.getData(), size, recordedChanToKWDChan[writeChannel]); - - int sampleOffset = channelLeftOverSamples[writeChannel]; - int blockStart = sampleOffset; - int64 currentTS = getTimestamp(writeChannel); - - if (sampleOffset > 0) - { - currentTS += TIMESTAMP_EACH_NSAMPLES - sampleOffset; - blockStart += TIMESTAMP_EACH_NSAMPLES - sampleOffset; - } - - for (int i = 0; i < size; i += TIMESTAMP_EACH_NSAMPLES) - { - if ((blockStart + i) < (sampleOffset + size)) - { - channelTimestampArray[writeChannel]->add(currentTS); - currentTS += TIMESTAMP_EACH_NSAMPLES; - } - } - channelLeftOverSamples.set(writeChannel, (size + sampleOffset) % TIMESTAMP_EACH_NSAMPLES); -} - -void HDF5Recording::endChannelBlock(bool lastBlock) -{ - int nCh = channelTimestampArray.size(); - for (int ch = 0; ch < nCh; ++ch) - { - int tsSize = channelTimestampArray[ch]->size(); - if ((tsSize > 0) && ((tsSize > CHANNEL_TIMESTAMP_MIN_WRITE) || lastBlock)) - { - int realChan = getRealChannel(ch); - int index = processorMap[realChan]; //CHECK - fileArray[index]->writeTimestamps(channelTimestampArray[ch]->getRawDataPointer(), tsSize, recordedChanToKWDChan[ch]); - channelTimestampArray[ch]->clearQuick(); - } - } -} - -void HDF5Recording::writeEvent(int eventChannel, const MidiMessage& event) -{ - if (Event::getEventType(event) == EventChannel::TTL) - { - TTLEventPtr ttl = TTLEvent::deserializeFromMessage(event, getEventChannel(eventChannel)); - if (ttl == nullptr) return; - uint8 channel = ttl->getChannel(); - eventFile->writeEvent(0, (ttl->getState() ? 1 : 0), ttl->getSourceID(), &channel, ttl->getTimestamp()); - } - else if (Event::getEventType(event) == EventChannel::TEXT) - { - TextEventPtr text = TextEvent::deserializeFromMessage(event, getEventChannel(eventChannel)); - if (text == nullptr) return; - String textMsg = text->getText(); - eventFile->writeEvent(1, 0, text->getSourceID(), textMsg.toUTF8().getAddress(), text->getTimestamp()); - } -} - -void HDF5Recording::writeTimestampSyncText(uint16 sourceID, uint16 sourceIdx, int64 timestamp, float, String text) -{ - eventFile->writeEvent(1, 0xFF, sourceID, text.toUTF8().getAddress(), timestamp); -} - -void HDF5Recording::addSpikeElectrode(int index, const SpikeChannel* elec) -{ - spikesFile->addChannelGroup(elec->getNumChannels()); -} -void HDF5Recording::writeSpike(int electrodeIndex, const SpikeEvent* spike) -{ - const SpikeChannel* spikeInfo = getSpikeChannel(electrodeIndex); - Array bitVolts; - for (int i = 0; i < spikeInfo->getNumChannels(); i++) - bitVolts.add(spikeInfo->getChannelBitVolts(i)); - spikesFile->writeSpike(electrodeIndex,spikeInfo->getTotalSamples(),spike->getDataPointer(), bitVolts, spike->getTimestamp()); -} - -void HDF5Recording::startAcquisition() -{ - eventFile = new KWEFile(); - eventFile->addEventType("TTL",HDF5FileBase::BaseDataType::U8,"event_channels"); - eventFile->addEventType("Messages", HDF5FileBase::BaseDataType::DSTR, "Text"); - spikesFile = new KWXFile(); -} - -RecordEngineManager* HDF5Recording::getEngineManager() -{ - RecordEngineManager* man = new RecordEngineManager("KWIK","Kwik",&(engineFactory)); - return man; -} diff --git a/Source/Plugins/KWIKFormat/RecordEngine/HDF5Recording.h b/Source/Plugins/KWIKFormat/RecordEngine/HDF5Recording.h deleted file mode 100644 index 7b9705032f..0000000000 --- a/Source/Plugins/KWIKFormat/RecordEngine/HDF5Recording.h +++ /dev/null @@ -1,77 +0,0 @@ -/* - ------------------------------------------------------------------ - - This file is part of the Open Ephys GUI - Copyright (C) 2014 Florian Franzen - - ------------------------------------------------------------------ - - This program is free software: you can redistribute it and/or modify - it under the terms of the GNU General Public License as published by - the Free Software Foundation, either version 3 of the License, or - (at your option) any later version. - - This program is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU General Public License for more details. - - You should have received a copy of the GNU General Public License - along with this program. If not, see . - - */ - -#ifndef HDF5RECORDING_H_INCLUDED -#define HDF5RECORDING_H_INCLUDED - -#include -#include "KWIKFormat.h" - -class HDF5Recording : public RecordEngine -{ -public: - HDF5Recording(); - ~HDF5Recording(); - String getEngineID() const override; - void openFiles(File rootFolder, int experimentNumber, int recordingNumber) override; - void closeFiles() override; - void writeData(int writeChannel, int realChannel, const float* buffer, int size) override; - void writeEvent(int eventType, const MidiMessage& event) override; - void writeTimestampSyncText(uint16 sourceID, uint16 sourceIdx, int64 timestamp, float, String text) override; - void addDataChannel(int index, const DataChannel* chan) override; - void addSpikeElectrode(int index,const SpikeChannel* elec) override; - void writeSpike(int electrodeIndex, const SpikeEvent* spike) override; - void registerProcessor(const GenericProcessor* processor) override; - void resetChannels() override; - void startAcquisition() override; - void endChannelBlock(bool lastBlock) override; - - static RecordEngineManager* getEngineManager(); -private: - - int processorIndex; - - Array processorMap; - Array channelsPerProcessor; - Array recordedChanToKWDChan; - OwnedArray> bitVoltsArray; - OwnedArray> sampleRatesArray; - OwnedArray> channelTimestampArray; - Array channelLeftOverSamples; - OwnedArray fileArray; - OwnedArray infoArray; - ScopedPointer eventFile; - ScopedPointer spikesFile; - HeapBlock scaledBuffer; - HeapBlock intBuffer; - int bufferSize; - //float* scaledBuffer; - //int16* intBuffer; - - bool hasAcquired; - - JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR(HDF5Recording); -}; - - -#endif // HDF5RECORDING_H_INCLUDED diff --git a/Source/Plugins/KWIKFormat/RecordEngine/KWIKFormat.cpp b/Source/Plugins/KWIKFormat/RecordEngine/KWIKFormat.cpp deleted file mode 100644 index 110d86cf3c..0000000000 --- a/Source/Plugins/KWIKFormat/RecordEngine/KWIKFormat.cpp +++ /dev/null @@ -1,419 +0,0 @@ -/* - ------------------------------------------------------------------ - - This file is part of the Open Ephys GUI - Copyright (C) 2014 Florian Franzen - - ------------------------------------------------------------------ - - This program is free software: you can redistribute it and/or modify - it under the terms of the GNU General Public License as published by - the Free Software Foundation, either version 3 of the License, or - (at your option) any later version. - - This program is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU General Public License for more details. - - You should have received a copy of the GNU General Public License - along with this program. If not, see . - - */ - - #include "KWIKFormat.h" - -#define CHUNK_XSIZE 2048 -#define EVENT_CHUNK_SIZE 8 -#define SPIKE_CHUNK_XSIZE 8 -#define SPIKE_CHUNK_YSIZE 40 -#define TIMESTAMP_CHUNK_SIZE 16 -#define MAX_TRANSFORM_SIZE 512 -#define MAX_STR_SIZE 256 - - //KWD File - -KWDFile::KWDFile(int processorNumber, String basename) : HDF5FileBase() -{ - initFile(processorNumber, basename); -} - -KWDFile::KWDFile() : HDF5FileBase() -{ -} - -KWDFile::~KWDFile() {} - -String KWDFile::getFileName() -{ - return filename; -} - -void KWDFile::initFile(int processorNumber, String basename) -{ - if (isOpen()) return; - filename = basename + "_" + String(processorNumber) + ".raw.kwd"; - readyToOpen=true; -} - -void KWDFile::startNewRecording(int recordingNumber, int nChannels, KWIKRecordingInfo* info) -{ - this->recordingNumber = recordingNumber; - this->nChannels = nChannels; - this->multiSample = info->multiSample; - uint8 mSample = info->multiSample ? 1 : 0; - - ScopedPointer bitVoltsSet; - ScopedPointer sampleRateSet; - - String recordPath = String("/recordings/")+String(recordingNumber); - CHECK_ERROR(createGroup(recordPath)); - CHECK_ERROR(setAttributeStr(info->name,recordPath,String("name"))); - CHECK_ERROR(setAttribute(BaseDataType::U64, &(info->start_time), recordPath, String("start_time"))); - CHECK_ERROR(setAttribute(BaseDataType::U32, &(info->start_sample), recordPath, String("start_sample"))); - CHECK_ERROR(setAttribute(BaseDataType::F32, &(info->sample_rate), recordPath, String("sample_rate"))); - CHECK_ERROR(setAttribute(BaseDataType::U32, &(info->bit_depth), recordPath, String("bit_depth"))); - CHECK_ERROR(createGroup(recordPath+"/application_data")); - // CHECK_ERROR(setAttributeArray(F32,info->bitVolts.getRawDataPointer(),info->bitVolts.size(),recordPath+"/application_data",String("channel_bit_volts"))); - bitVoltsSet = createDataSet(BaseDataType::F32, info->bitVolts.size(), 0, recordPath + "/application_data/channel_bit_volts"); - if (bitVoltsSet.get()) - bitVoltsSet->writeDataBlock(info->bitVolts.size(), BaseDataType::F32, info->bitVolts.getRawDataPointer()); - else - std::cerr << "Error creating bitvolts data set" << std::endl; - - CHECK_ERROR(setAttribute(BaseDataType::U8, &mSample, recordPath + "/application_data", String("is_multiSampleRate_data"))); - //CHECK_ERROR(setAttributeArray(F32,info->channelSampleRates.getRawDataPointer(),info->channelSampleRates.size(),recordPath+"/application_data",String("channel_sample_rates"))); - sampleRateSet = createDataSet(BaseDataType::F32, info->channelSampleRates.size(), 0, recordPath + "/application_data/channel_sample_rates"); - if (sampleRateSet.get()) - sampleRateSet->writeDataBlock(info->channelSampleRates.size(), BaseDataType::F32, info->channelSampleRates.getRawDataPointer()); - else - std::cerr << "Error creating sample rates data set" << std::endl; - - recdata = createDataSet(BaseDataType::I16, 0, nChannels, CHUNK_XSIZE, recordPath + "/data"); - if (!recdata.get()) - std::cerr << "Error creating data set" << std::endl; - - tsData = createDataSet(BaseDataType::I64, 0, nChannels, TIMESTAMP_CHUNK_SIZE, recordPath + "/application_data/timestamps"); - if (!tsData.get()) - std::cerr << "Error creating timestamps data set" << std::endl; - - curChan = nChannels; -} - -void KWDFile::stopRecording() -{ - Array samples; - String path = String("/recordings/")+String(recordingNumber)+String("/data"); - recdata->getRowXPositions(samples); - - CHECK_ERROR(setAttributeArray(BaseDataType::U32, samples.getRawDataPointer(), samples.size(), path, "valid_samples")); - //ScopedPointer does the deletion and destructors the closings - recdata = nullptr; - tsData = nullptr; -} - -int KWDFile::createFileStructure() -{ - const uint16 ver = 2; - if (createGroup("/recordings")) return -1; - if (setAttribute(BaseDataType::U16, (void*)&ver, "/", "kwik_version")) return -1; - return 0; -} - -void KWDFile::writeBlockData(int16* data, int nSamples) -{ - CHECK_ERROR(recdata->writeDataBlock(nSamples, BaseDataType::I16, data)); -} - -void KWDFile::writeRowData(int16* data, int nSamples) -{ - if (curChan >= nChannels) - { - curChan=0; - } - CHECK_ERROR(recdata->writeDataRow(curChan, nSamples, BaseDataType::I16, data)); - curChan++; -} - -void KWDFile::writeRowData(int16* data, int nSamples, int channel) -{ - if (channel >= 0 && channel < nChannels) - { - CHECK_ERROR(recdata->writeDataRow(channel, nSamples, BaseDataType::I16, data)); - curChan = channel; - } -} - -void KWDFile::writeTimestamps(int64* ts, int nTs, int channel) -{ - if (channel >= 0 && channel < nChannels) - { - CHECK_ERROR(tsData->writeDataRow(channel, nTs, BaseDataType::I64, ts)); - } -} - -//KWE File - -KWEFile::KWEFile(String basename) : HDF5FileBase() -{ - initFile(basename); -} - -KWEFile::KWEFile() : HDF5FileBase() -{ - -} - -KWEFile::~KWEFile() {} - -String KWEFile::getFileName() -{ - return filename; -} - -void KWEFile::initFile(String basename) -{ - if (isOpen()) return; - filename = basename + ".kwe"; - readyToOpen=true; -} - -int KWEFile::createFileStructure() -{ - const uint16 ver = 2; - if (createGroup("/recordings")) return -1; - if (createGroup("/event_types")) return -1; - for (int i=0; i < eventNames.size(); i++) - { - ScopedPointer dSet; - String path = "/event_types/" + eventNames[i]; - if (createGroup(path)) return -1; - path += "/events"; - if (createGroup(path)) return -1; - dSet = createDataSet(BaseDataType::U64, 0, EVENT_CHUNK_SIZE, path + "/time_samples"); - if (!dSet) return -1; - dSet = createDataSet(BaseDataType::U16, 0, EVENT_CHUNK_SIZE, path + "/recording"); - if (!dSet) return -1; - path += "/user_data"; - if (createGroup(path)) return -1; - dSet = createDataSet(BaseDataType::U8, 0, EVENT_CHUNK_SIZE, path + "/eventID"); - if (!dSet) return -1; - dSet = createDataSet(BaseDataType::U8, 0, EVENT_CHUNK_SIZE, path + "/nodeID"); - if (!dSet) return -1; - dSet = createDataSet(eventTypes[i],0,EVENT_CHUNK_SIZE,path + "/" + eventDataNames[i]); - if (!dSet) return -1; - } - if (setAttribute(BaseDataType::U16, (void*)&ver, "/", "kwik_version")) return -1; - - return 0; -} - -void KWEFile::startNewRecording(int recordingNumber, KWIKRecordingInfo* info) -{ - this->recordingNumber = recordingNumber; - kwdIndex=0; - String recordPath = String("/recordings/")+String(recordingNumber); - CHECK_ERROR(createGroup(recordPath)); - CHECK_ERROR(setAttributeStr(info->name,recordPath,String("name"))); - CHECK_ERROR(setAttribute(BaseDataType::U64, &(info->start_time), recordPath, String("start_time"))); - // CHECK_ERROR(setAttribute(U32,&(info->start_sample),recordPath,String("start_sample"))); - CHECK_ERROR(setAttribute(BaseDataType::F32, &(info->sample_rate), recordPath, String("sample_rate"))); - CHECK_ERROR(setAttribute(BaseDataType::U32, &(info->bit_depth), recordPath, String("bit_depth"))); - // CHECK_ERROR(createGroup(recordPath + "/raw")); - // CHECK_ERROR(createGroup(recordPath + "/raw/hdf5_paths")); - - for (int i = 0; i < eventNames.size(); i++) - { - HDF5RecordingData* dSet; - String path = "/event_types/" + eventNames[i] + "/events"; - dSet = getDataSet(path + "/time_samples"); - if (!dSet) - std::cerr << "Error loading event timestamps dataset for type " << i << std::endl; - timeStamps.add(dSet); - dSet = getDataSet(path + "/recording"); - if (!dSet) - std::cerr << "Error loading event recordings dataset for type " << i << std::endl; - recordings.add(dSet); - dSet = getDataSet(path + "/user_data/eventID"); - if (!dSet) - std::cerr << "Error loading event ID dataset for type " << i << std::endl; - eventID.add(dSet); - dSet = getDataSet(path + "/user_data/nodeID"); - if (!dSet) - std::cerr << "Error loading event node ID dataset for type " << i << std::endl; - nodeID.add(dSet); - dSet = getDataSet(path + "/user_data/" + eventDataNames[i]); - if (!dSet) - std::cerr << "Error loading event channel dataset for type " << i << std::endl; - eventData.add(dSet); - } -} - -void KWEFile::stopRecording() -{ - timeStamps.clear(); - recordings.clear(); - eventID.clear(); - nodeID.clear(); - eventData.clear(); -} - -void KWEFile::writeEvent(int type, uint8 id, uint8 processor, void* data, int64 timestamp) -{ - if (type > eventNames.size() || type < 0) - { - std::cerr << "HDF5::writeEvent Invalid event type " << type << std::endl; - return; - } - CHECK_ERROR(timeStamps[type]->writeDataBlock(1, BaseDataType::U64, ×tamp)); - CHECK_ERROR(recordings[type]->writeDataBlock(1, BaseDataType::I32, &recordingNumber)); - CHECK_ERROR(eventID[type]->writeDataBlock(1, BaseDataType::U8, &id)); - CHECK_ERROR(nodeID[type]->writeDataBlock(1, BaseDataType::U8, &processor)); - CHECK_ERROR(eventData[type]->writeDataBlock(1,eventTypes[type],data)); -} - -/*void KWEFile::addKwdFile(String filename) -{ - if (kwdIndex == 0) - { - CHECK_ERROR(setAttributeStr(filename + "/recordings/" + String(recordingNumber), "/recordings/" + String(recordingNumber) + - "/raw", "hdf5_path")); - } - CHECK_ERROR(setAttributeStr(filename + "/recordings/" + String(recordingNumber),"/recordings/" + String(recordingNumber) + - "/raw/hdf5_paths",String(kwdIndex))); - kwdIndex++; -}*/ - -void KWEFile::addEventType(String name, BaseDataType type, String dataName) -{ - eventNames.add(name); - eventTypes.add(type); - eventDataNames.add(dataName); -} - -//KWX File - -KWXFile::KWXFile(String basename) : HDF5FileBase() -{ - initFile(basename); - numElectrodes=0; - transformVector.malloc(MAX_TRANSFORM_SIZE); -} - -KWXFile::KWXFile() : HDF5FileBase() -{ - numElectrodes=0; - transformVector.malloc(MAX_TRANSFORM_SIZE); -} - -KWXFile::~KWXFile() -{ -} - -String KWXFile::getFileName() -{ - return filename; -} - -void KWXFile::initFile(String basename) -{ - if (isOpen()) return; - filename = basename + ".kwx"; - readyToOpen=true; -} - -int KWXFile::createFileStructure() -{ - const uint16 ver = 2; - if (createGroup("/channel_groups")) return -1; - if (setAttribute(BaseDataType::U16, (void*)&ver, "/", "kwik_version")) return -1; - for (int i=0; i < channelArray.size(); i++) - { - int res = createChannelGroup(i); - if (res) return -1; - } - return 0; -} - -void KWXFile::addChannelGroup(int nChannels) -{ - channelArray.add(nChannels); - numElectrodes++; -} - -int KWXFile::createChannelGroup(int index) -{ - ScopedPointer dSet; - int nChannels = channelArray[index]; - String path("/channel_groups/"+String(index)); - CHECK_ERROR(createGroup(path)); - dSet = createDataSet(BaseDataType::I16, 0, 0, nChannels, SPIKE_CHUNK_XSIZE, SPIKE_CHUNK_YSIZE, path + "/waveforms_filtered"); - if (!dSet) return -1; - dSet = createDataSet(BaseDataType::U64, 0, SPIKE_CHUNK_XSIZE, path + "/time_samples"); - if (!dSet) return -1; - dSet = createDataSet(BaseDataType::U16, 0, SPIKE_CHUNK_XSIZE, path + "/recordings"); - if (!dSet) return -1; - return 0; -} - -void KWXFile::startNewRecording(int recordingNumber) -{ - HDF5RecordingData* dSet; - String path; - this->recordingNumber = recordingNumber; - - for (int i=0; i < channelArray.size(); i++) - { - path = "/channel_groups/"+String(i); - dSet=getDataSet(path+"/waveforms_filtered"); - if (!dSet) - std::cerr << "Error loading spikes dataset for group " << i << std::endl; - spikeArray.add(dSet); - dSet=getDataSet(path+"/time_samples"); - if (!dSet) - std::cerr << "Error loading spike timestamp dataset for group " << i << std::endl; - timeStamps.add(dSet); - dSet=getDataSet(path+"/recordings"); - if (!dSet) - std::cerr << "Error loading spike recordings dataset for group " << i << std::endl; - recordingArray.add(dSet); - } -} - -void KWXFile::stopRecording() -{ - spikeArray.clear(); - timeStamps.clear(); - recordingArray.clear(); -} - -void KWXFile::resetChannels() -{ - stopRecording(); //Just in case - channelArray.clear(); -} - -void KWXFile::writeSpike(int groupIndex, int nSamples, const float* data, Array& bitVolts, int64 timestamp) -{ - if ((groupIndex < 0) || (groupIndex >= numElectrodes)) - { - std::cerr << "HDF5::writeSpike Electrode index out of bounds " << groupIndex << std::endl; - return; - } - int nChans= channelArray[groupIndex]; - int16* dst=transformVector; - - //Given the way we store spike data, we need to transpose it to store in - //N x NSAMPLES x NCHANNELS as well as convert from float to i16 - for (int i = 0; i < nSamples; i++) - { - for (int j = 0; j < nChans; j++) - { - *(dst++) = static_cast((*(data+j*nSamples+i))/bitVolts[j]); - } - } - - CHECK_ERROR(spikeArray[groupIndex]->writeDataBlock(1, nSamples, BaseDataType::I16, transformVector)); - CHECK_ERROR(recordingArray[groupIndex]->writeDataBlock(1, BaseDataType::I32, &recordingNumber)); - CHECK_ERROR(timeStamps[groupIndex]->writeDataBlock(1, BaseDataType::U64, ×tamp)); -} diff --git a/Source/Plugins/KWIKFormat/RecordEngine/KWIKFormat.h b/Source/Plugins/KWIKFormat/RecordEngine/KWIKFormat.h deleted file mode 100644 index 0ffefea2d0..0000000000 --- a/Source/Plugins/KWIKFormat/RecordEngine/KWIKFormat.h +++ /dev/null @@ -1,137 +0,0 @@ -/* - ------------------------------------------------------------------ - - This file is part of the Open Ephys GUI - Copyright (C) 2014 Florian Franzen - - ------------------------------------------------------------------ - - This program is free software: you can redistribute it and/or modify - it under the terms of the GNU General Public License as published by - the Free Software Foundation, either version 3 of the License, or - (at your option) any later version. - - This program is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU General Public License for more details. - - You should have received a copy of the GNU General Public License - along with this program. If not, see . - - */ - -#ifndef KWIKFORMAT_H_INCLUDED -#define KWIKFORMAT_H_INCLUDED - -#include -using namespace OpenEphysHDF5; - -struct KWIKRecordingInfo -{ - String name; - int64 start_time; - uint32 start_sample; - float sample_rate; - uint32 bit_depth; - Array bitVolts; - Array channelSampleRates; - bool multiSample; -}; - -class KWDFile : public HDF5FileBase -{ -public: - KWDFile(int processorNumber, String basename); - KWDFile(); - virtual ~KWDFile(); - void initFile(int processorNumber, String basename); - void startNewRecording(int recordingNumber, int nChannels, KWIKRecordingInfo* info); - void stopRecording(); - void writeBlockData(int16* data, int nSamples); - void writeRowData(int16* data, int nSamples); - void writeRowData(int16* data, int nSamples, int channel); - void writeTimestamps(int64* ts, int nTs, int channel); - String getFileName(); - -protected: - int createFileStructure(); - -private: - int recordingNumber; - int nChannels; - int curChan; - String filename; - bool multiSample; - ScopedPointer recdata; - ScopedPointer tsData; - - JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR(KWDFile); -}; - -class KWEFile : public HDF5FileBase -{ -public: - KWEFile(String basename); - KWEFile(); - virtual ~KWEFile(); - void initFile(String basename); - void startNewRecording(int recordingNumber, KWIKRecordingInfo* info); - void stopRecording(); - void writeEvent(int type, uint8 id, uint8 processor, void* data, int64 timestamp); - // void addKwdFile(String filename); - void addEventType(String name, BaseDataType type, String dataName); - String getFileName(); - -protected: - int createFileStructure(); - -private: - int recordingNumber; - String filename; - OwnedArray timeStamps; - OwnedArray recordings; - OwnedArray eventID; - OwnedArray nodeID; - OwnedArray eventData; - Array eventNames; - Array eventTypes; - Array eventDataNames; - int kwdIndex; - - JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR(KWEFile); -}; - -class KWXFile : public HDF5FileBase -{ -public: - KWXFile(String basename); - KWXFile(); - virtual ~KWXFile(); - void initFile(String basename); - void startNewRecording(int recordingNumber); - void stopRecording(); - void addChannelGroup(int nChannels); - void resetChannels(); - void writeSpike(int groupIndex, int nSamples, const float* data, Array& bitVolts, int64 timestamp); - String getFileName(); - -protected: - int createFileStructure(); - -private: - int createChannelGroup(int index); - int recordingNumber; - String filename; - OwnedArray spikeArray; - OwnedArray recordingArray; - OwnedArray timeStamps; - Array channelArray; - int numElectrodes; - HeapBlock transformVector; - //int16* transformVector; - - JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR(KWXFile); -}; - -#endif diff --git a/Source/Plugins/LfpDisplayNode/LfpDisplayCanvas.cpp b/Source/Plugins/LfpDisplayNode/LfpDisplayCanvas.cpp index c55418e78c..2643008c79 100644 --- a/Source/Plugins/LfpDisplayNode/LfpDisplayCanvas.cpp +++ b/Source/Plugins/LfpDisplayNode/LfpDisplayCanvas.cpp @@ -242,7 +242,7 @@ void LfpDisplayCanvas::update() sampleRate.add(30000); } - // std::cout << "Sample rate for ch " << i << " = " << sampleRate[i] << std::endl; + // std::cout << "Sample rate for ch " << i << " = " << sampleRate[i] << std::endl; displayBufferIndex.add(0); screenBufferIndex.add(0); lastScreenBufferIndex.add(0); @@ -250,7 +250,6 @@ void LfpDisplayCanvas::update() if (nChans != lfpDisplay->getNumChannels()) { - refreshScreenBuffer(); lfpDisplay->setNumChannels(nChans); // add an extra channel for events @@ -258,12 +257,9 @@ void LfpDisplayCanvas::update() // update channel names for (int i = 0; i < processor->getNumInputs(); i++) { - String chName = processor->getDataChannel(i)->getName(); - lfpDisplay->channelInfo[i]->setName(chName); lfpDisplay->setEnabledState(isChannelEnabled[i], i); - } if (nChans == 0) lfpDisplay->setBounds(0, 0, getWidth(), getHeight()); @@ -278,6 +274,8 @@ void LfpDisplayCanvas::update() { for (int i = 0; i < processor->getNumInputs(); i++) { + String chName = processor->getDataChannel(i)->getName(); + lfpDisplay->channelInfo[i]->setName(chName); lfpDisplay->channels[i]->updateType(); lfpDisplay->channelInfo[i]->updateType(); } @@ -2278,7 +2276,7 @@ void LfpDisplay::setNumChannels(int numChannels) setColors(); - std::cout << "TOTAL HEIGHT = " << totalHeight << std::endl; + //std::cout << "TOTAL HEIGHT = " << totalHeight << std::endl; } @@ -3069,6 +3067,7 @@ LfpChannelDisplay::LfpChannelDisplay(LfpDisplayCanvas* c, LfpDisplay* d, LfpDisp , options(o) , isSelected(false) , chan(channelNumber) + , name("") , drawableChan(channelNumber) , channelOverlap(300) , channelHeight(40) @@ -3504,6 +3503,11 @@ int LfpChannelDisplay::getChannelNumber() return chan; } +String LfpChannelDisplay::getName() +{ + return name; +} + int LfpChannelDisplay::getDrawableChannelNumber() { return drawableChan; @@ -3560,7 +3564,7 @@ LfpChannelDisplayInfo::LfpChannelDisplayInfo(LfpDisplayCanvas* canvas_, LfpDispl y = -1.0f; // enableButton = new UtilityButton(String(ch+1), Font("Small Text", 13, Font::plain)); - enableButton = new UtilityButton("*", Font("Small Text", 13, Font::plain)); + enableButton = new UtilityButton("", Font("Small Text", 13, Font::plain)); enableButton->setRadius(5.0f); enableButton->setEnabledState(true); @@ -3720,24 +3724,17 @@ void LfpChannelDisplayInfo::paint(Graphics& g) int center = getHeight()/2 - (isSingleChannel?(75):(0)); -// g.setColour(lineColour); - //if (chan > 98) - // g.fillRoundedRectangle(5,center-8,51,22,8.0f); - //else - -// g.fillRoundedRectangle(5,center-8,41,22,8.0f); - // Draw the channel numbers g.setColour(Colours::grey); - const String channelString = (isChannelNumberHidden() ? ("--") : String(getChannelNumber() + 1)); + const String channelString = (isChannelNumberHidden() ? ("--") : getName()); bool isCentered = !getEnabledButtonVisibility(); g.drawText(channelString, 2, center-4, - isCentered ? (getWidth()/2-4) : (getWidth()/4), + getWidth()/2, 10, - isCentered ? Justification::centred : Justification::centredRight, + isCentered ? Justification::centred : Justification::centredLeft, false); g.setColour(lineColour); @@ -3788,17 +3785,11 @@ void LfpChannelDisplayInfo::resized() { int center = getHeight()/2 - (isSingleChannel?(75):(0)); - - //if (chan > 98) - // enableButton->setBounds(8,center-5,45,16); - //else -// enableButton->setBounds(8,center-5,35,16); - setEnabledButtonVisibility(getHeight() >= 16); if (getEnabledButtonVisibility()) { - enableButton->setBounds(getWidth()/4 + 5, (center) - 7, 15, 15); + enableButton->setBounds(getWidth()/2 - 10, (center) - 5, 10, 10); } setChannelNumberIsHidden(getHeight() < 16 && (getDrawableChannelNumber() + 1) % 10 != 0); diff --git a/Source/Plugins/LfpDisplayNode/LfpDisplayCanvas.h b/Source/Plugins/LfpDisplayNode/LfpDisplayCanvas.h index b8886eeff5..54d1e03d62 100644 --- a/Source/Plugins/LfpDisplayNode/LfpDisplayCanvas.h +++ b/Source/Plugins/LfpDisplayNode/LfpDisplayCanvas.h @@ -148,7 +148,7 @@ class LfpDisplayCanvas : public Visualizer, //void scrollBarMoved(ScrollBar *scrollBarThatHasMoved, double newRangeStart); bool fullredraw; // used to indicate that a full redraw is required. is set false after each full redraw, there is a similar switch for each display; - static const int leftmargin=50; // left margin for lfp plots (so the ch number text doesnt overlap) + static const int leftmargin = 50; // left margin for lfp plots (so the ch number text doesnt overlap) Array isChannelEnabled; @@ -709,8 +709,11 @@ class LfpChannelDisplay : public Component void setChannelOverlap(int); int getChannelOverlap(); - /** Return the assigned channel number for this display */ + /** Return the assigned channel number */ int getChannelNumber(); + + /** Return the assigned channel name */ + String getName(); /** Returns the assigned channel number for this display, relative to the subset of channels being drawn to the canvas */ diff --git a/Source/Plugins/LfpDisplayNode/LfpDisplayNode.cpp b/Source/Plugins/LfpDisplayNode/LfpDisplayNode.cpp index bc38d05ae5..f4cd323e38 100644 --- a/Source/Plugins/LfpDisplayNode/LfpDisplayNode.cpp +++ b/Source/Plugins/LfpDisplayNode/LfpDisplayNode.cpp @@ -80,11 +80,11 @@ void LfpDisplayNode::updateSettings() numEventChannels = eventSourceNodes.size(); - std::cout << "Found " << numEventChannels << " event channels." << std::endl; + //std::cout << "Found " << numEventChannels << " event channels." << std::endl; for (int i = 0; i < eventSourceNodes.size(); ++i) { - std::cout << "Adding channel " << getNumInputs() + i << " for event source node " << eventSourceNodes[i] << std::endl; + //std::cout << "Adding channel " << getNumInputs() + i << " for event source node " << eventSourceNodes[i] << std::endl; channelForEventSource[eventSourceNodes[i]] = getNumInputs() + i; ttlState[eventSourceNodes[i]] = 0; diff --git a/Source/Plugins/NWBFormat/Makefile b/Source/Plugins/NWBFormat/Makefile deleted file mode 100644 index fbe5b93837..0000000000 --- a/Source/Plugins/NWBFormat/Makefile +++ /dev/null @@ -1,40 +0,0 @@ - -LIBNAME := $(notdir $(CURDIR)) -OBJDIR := $(OBJDIR)/$(LIBNAME) -TARGET := $(LIBNAME).so - -LDFLAGS += -l:OpenEphysHDF5Lib.so - -SRC_DIR := ${shell find ./ -type d -print} -VPATH := $(SOURCE_DIRS) - -SRC := $(foreach sdir,$(SRC_DIR),$(wildcard $(sdir)/*.cpp)) -OBJ := $(addprefix $(OBJDIR)/,$(notdir $(SRC:.cpp=.o))) - -BLDCMD := $(CXX) -shared -o $(OUTDIR)/$(TARGET) $(OBJ) $(LDFLAGS) $(RESOURCES) $(TARGET_ARCH) - -VPATH = $(SRC_DIR) - -.PHONY: objdir - -$(OUTDIR)/$(TARGET): objdir $(OBJ) - -@mkdir -p $(BINDIR) - -@mkdir -p $(LIBDIR) - -@mkdir -p $(OUTDIR) - @echo "Building $(TARGET)" - @$(BLDCMD) - -$(OBJDIR)/%.o : %.cpp - @echo "Compiling $<" - @$(CXX) $(CXXFLAGS) -o "$@" -c "$<" - - -objdir: - -@mkdir -p $(OBJDIR) - -clean: - @echo "Cleaning $(LIBNAME)" - -@rm -rf $(OBJDIR) - -@rm -f $(OUTDIR)/$(TARGET) - --include $(OBJ:%.o=%.d) diff --git a/Source/Plugins/NWBFormat/NWBFormat.cpp b/Source/Plugins/NWBFormat/NWBFormat.cpp deleted file mode 100644 index 30e3d3a08b..0000000000 --- a/Source/Plugins/NWBFormat/NWBFormat.cpp +++ /dev/null @@ -1,674 +0,0 @@ -/* - ------------------------------------------------------------------ - - This file is part of the Open Ephys GUI - Copyright (C) 2014 Open Ephys - - ------------------------------------------------------------------ - - This program is free software: you can redistribute it and/or modify - it under the terms of the GNU General Public License as published by - the Free Software Foundation, either version 3 of the License, or - (at your option) any later version. - - This program is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU General Public License for more details. - - You should have received a copy of the GNU General Public License - along with this program. If not, see . - - */ - - #include "NWBFormat.h" - using namespace NWBRecording; - -#ifndef EVENT_CHUNK_SIZE -#define EVENT_CHUNK_SIZE 8 -#endif - -#ifndef SPIKE_CHUNK_XSIZE -#define SPIKE_CHUNK_XSIZE 8 -#endif - -#ifndef SPIKE_CHUNK_YSIZE -#define SPIKE_CHUNK_YSIZE 40 -#endif - - #define MAX_BUFFER_SIZE 40960 - - NWBFile::NWBFile(String fName, String ver, String idText) : HDF5FileBase(), filename(fName), identifierText(idText), GUIVersion(ver) - { - //Init stuff - readyToOpen=true; //In KWIK this is in initFile, but the new recordEngine methods make it safe for it to be here - - scaledBuffer.malloc(MAX_BUFFER_SIZE); - intBuffer.malloc(MAX_BUFFER_SIZE); - bufferSize = MAX_BUFFER_SIZE; - } - - NWBFile::~NWBFile() - { - } - - - void NWBFile::setXmlText(const String& xmlText) - { - this->xmlText = &xmlText; - } - -//All int return values are 0 if succesful, other number otherwise -int NWBFile::createFileStructure() -{ - //This is called when the file is first created (not if it's opened but it already exists) - //Creates all the common structures, like file version and such - if (createGroup("/acquisition")) return -1; - if (createGroup("/analysis")) return -1; - if (createGroup("/epochs")) return -1; - if (createGroup("/general")) return -1; - if (createGroup("/processing")) return -1; - if (createGroup("/stimulus")) return -1; - - if (createGroup("/acquisition/timeseries")) return -1; - - - if (createGroup("/general/data_collection")) return -1; - - CHECK_ERROR(setAttributeStr(String("OpenEphys GUI v") + GUIVersion, "/general/data_collection", "software")); - CHECK_ERROR(setAttributeStr(*xmlText, "/general/data_collection", "configuration")); - - //TODO: Add default datasets - //Modify this one once we have JUCE4 to allow UTC time - String time = Time::getCurrentTime().formatted("%Y-%m-%dT%H:%M:%S"); - createTextDataSet("", "file_create_date", time); - createTextDataSet("", "identifier", identifierText); - createTextDataSet("", "nwb_version", "NWB-1.0.6"); - createTextDataSet("", "session_description", " "); - createTextDataSet("", "session_start_time", time); - - return 0; -} - -bool NWBFile::startNewRecording(int recordingNumber, const Array& continuousArray, - const Array& eventArray, const Array& electrodeArray) - { - //Created each time a new recording is started. Creates the specific file structures and attributes - //for that specific recording - String basePath; - StringArray ancestry; - String rootPath = "/acquisition/timeseries/recording" + String(recordingNumber + 1); - if (createGroup(rootPath)) return false; - if (createGroupIfDoesNotExist(rootPath + "/continuous")) return false; - if (createGroupIfDoesNotExist(rootPath + "/spikes")) return false; - if (createGroupIfDoesNotExist(rootPath + "/events")) return false; - - //just in case - continuousDataSets.clearQuick(true); - spikeDataSets.clearQuick(true); - eventDataSets.clearQuick(true); - - ScopedPointer tsStruct; - ScopedPointer dSet; - - int nCont; - nCont = continuousArray.size(); - for (int i = 0; i < nCont; i++) - { - //All channels in a group will share the same source information (any caller to this method MUST assure this happen - //so we just pick the first channel. - const DataChannel* info = continuousArray.getReference(i)[0]; - basePath = rootPath + "/continuous/processor" + String(info->getCurrentNodeID()) + "_" + String(info->getSourceNodeID()); - if (info->getSourceSubprocessorCount() > 1) basePath += "." + String(info->getSubProcessorIdx()); - String name = info->getCurrentNodeName() + " (" + String(info->getCurrentNodeID()) + ") From " + info->getSourceName() + " (" + String(info->getSourceNodeID()); - if (info->getSourceSubprocessorCount() > 1) name += "." + String(info->getSubProcessorIdx()); - name += ")"; - ancestry.clearQuick(); - ancestry.add("Timeseries"); - ancestry.add("ElectricalSeries"); - if (!createTimeSeriesBase(basePath, name, "Stores acquired voltage data from extracellular recordings", "", ancestry)) return false; - tsStruct = new TimeSeries(); - tsStruct->basePath = basePath; - dSet = createDataSet(BaseDataType::I16, 0, continuousArray.getReference(i).size(), CHUNK_XSIZE, basePath + "/data"); - if (dSet == nullptr) - { - std::cerr << "Error creating dataset for " << name << std::endl; - return false; - } - else - { - createDataAttributes(basePath, info->getBitVolts(), info->getBitVolts() / 65536, info->getDataUnits()); - } - tsStruct->baseDataSet = dSet; - - dSet = createTimestampDataSet(basePath, CHUNK_XSIZE); - if (dSet == nullptr) return false; - tsStruct->timestampDataSet = dSet; - - basePath = basePath + "/oe_extra_info"; - if (createGroup(basePath)) return false; - int nChans = continuousArray.getReference(i).size(); - for (int j = 0; j < nChans; j++) - { - String channelPath = basePath + "/channel" + String(j + 1); - const DataChannel* chan = continuousArray.getReference(i)[j]; - createExtraInfo(channelPath, chan->getName(), chan->getDescription(), chan->getIdentifier(), chan->getSourceIndex(), chan->getSourceTypeIndex()); - createChannelMetaDataSets(channelPath + "/channel_metadata", chan); - } - continuousDataSets.add(tsStruct.release()); - } - - nCont = electrodeArray.size(); - for (int i = 0; i < nCont; i++) - { - basePath = rootPath + "/spikes/electrode" + String(i + 1); - const SpikeChannel* info = electrodeArray[i]; - String sourceName = info->getSourceName() + "_" + String(info->getSourceNodeID()); - if (info->getSourceSubprocessorCount() > 1) sourceName = sourceName + "." + String(info->getSubProcessorIdx()); - ancestry.clearQuick(); - ancestry.add("Timeseries"); - ancestry.add("SpikeEventSeries"); - if (!createTimeSeriesBase(basePath, sourceName, "Snapshorts of spike events from data", info->getName(), ancestry)) return false; - - tsStruct = new TimeSeries(); - tsStruct->basePath = basePath; - - dSet = createDataSet(BaseDataType::I16, 0, info->getNumChannels(), info->getTotalSamples(), SPIKE_CHUNK_XSIZE, basePath + "/data"); - if (dSet == nullptr) - { - std::cerr << "Error creating dataset for electrode " << i << std::endl; - return false; - } - else - { - createDataAttributes(basePath, info->getChannelBitVolts(0), info->getChannelBitVolts(0) / 65536, "volt"); - } - tsStruct->baseDataSet = dSet; - dSet = createTimestampDataSet(basePath, SPIKE_CHUNK_XSIZE); - if (dSet == nullptr) return false; - tsStruct->timestampDataSet = dSet; - - basePath = basePath + "/oe_extra_info"; - createExtraInfo(basePath, info->getName(), info->getDescription(), info->getIdentifier(), info->getSourceIndex(), info->getSourceTypeIndex()); - createChannelMetaDataSets(basePath + "/channel_metadata", info); - createEventMetaDataSets(basePath + "/spike_metadata", tsStruct, info); - - spikeDataSets.add(tsStruct.release()); - - - } - - nCont = eventArray.size(); - int nTTL = 0; - int nTXT = 0; - int nBIN = 0; - for (int i = 0; i < nCont; i++) - { - basePath = rootPath + "/events"; - const EventChannel* info = eventArray[i]; - String sourceName = info->getSourceName() + "_" + String(info->getSourceNodeID()); - if (info->getSourceSubprocessorCount() > 1) sourceName = sourceName + "." + String(info->getSubProcessorIdx()); - ancestry.clearQuick(); - ancestry.add("Timeseries"); - - String helpText; - - switch (info->getChannelType()) - { - case EventChannel::TTL: - nTTL += 1; - basePath = basePath + "/ttl" + String(nTTL); - ancestry.add("IntervalSeries"); - ancestry.add("TTLSeries"); - helpText = "Stores the start and stop times for TTL events"; - break; - case EventChannel::TEXT: - nTXT += 1; - basePath = basePath + "/text" + String(nTXT); - ancestry.add("AnnotationSeries"); - helpText = "Time-stamped annotations about an experiment"; - break; - default: - nBIN += 1; - basePath = basePath + "/binary" + String(nBIN); - ancestry.add("BinarySeries"); - helpText = "Stores arbitrary binary data"; - break; - } - - if (!createTimeSeriesBase(basePath, sourceName, helpText, info->getDescription(), ancestry)) return false; - - tsStruct = new TimeSeries(); - tsStruct->basePath = basePath; - - if (info->getChannelType() >= EventChannel::BINARY_BASE_VALUE) //only binary events have length greater than 1 - { - dSet = createDataSet(getEventH5Type(info->getChannelType(), info->getLength()), 0, info->getLength(), EVENT_CHUNK_SIZE, basePath + "/data");; - } - else - { - dSet = createDataSet(getEventH5Type(info->getChannelType(), info->getLength()), 0, EVENT_CHUNK_SIZE, basePath + "/data"); - } - - if (dSet == nullptr) - { - std::cerr << "Error creating dataset for event " << info->getName() << std::endl; - return false; - } - else - { - createDataAttributes(basePath, NAN, NAN, "n/a"); - } - - - tsStruct->baseDataSet = dSet; - dSet = createTimestampDataSet(basePath, EVENT_CHUNK_SIZE); - if (dSet == nullptr) return false; - tsStruct->timestampDataSet = dSet; - - dSet = createDataSet(BaseDataType::U8, 0, EVENT_CHUNK_SIZE, basePath + "/control"); - if (dSet == nullptr) return false; - tsStruct->controlDataSet = dSet; - - if (info->getChannelType() == EventChannel::TTL) - { - dSet = createDataSet(BaseDataType::U8, 0, info->getDataSize(), EVENT_CHUNK_SIZE, basePath + "/full_word"); - if (dSet == nullptr) return false; - tsStruct->ttlWordDataSet = dSet; - } - - basePath = basePath + "/oe_extra_info"; - createExtraInfo(basePath, info->getName(), info->getDescription(), info->getIdentifier(), info->getSourceIndex(), info->getSourceTypeIndex()); - createChannelMetaDataSets(basePath + "/channel_metadata", info); - createEventMetaDataSets(basePath + "/event_metadata", tsStruct, info); - eventDataSets.add(tsStruct.release()); - - } - basePath = rootPath + "/events/sync_messages"; - ancestry.clearQuick(); - ancestry.add("Timeseries"); - ancestry.add("AnnotationSeries"); - String desc = "Stores recording start timestamps for each processor in text format"; - if (!createTimeSeriesBase(basePath, "Autogenerated messages", desc, desc, ancestry)) return false; - tsStruct = new TimeSeries(); - tsStruct->basePath = basePath; - dSet = createDataSet(BaseDataType::STR(100), 0, 1, basePath + "/data"); - if (dSet == nullptr) - { - std::cerr << "Error creating dataset for sync messages" << std::endl; - return false; - } - else - { - createDataAttributes(basePath, NAN, NAN, "n/a"); - } - tsStruct->baseDataSet = dSet; - dSet = createTimestampDataSet(basePath, 1); - if (dSet == nullptr) return false; - tsStruct->timestampDataSet = dSet; - - dSet = createDataSet(BaseDataType::U8, 0, 1, basePath + "/control"); - if (dSet == nullptr) return false; - tsStruct->controlDataSet = dSet; - syncMsgDataSet = tsStruct; - - return true; - } - - void NWBFile::stopRecording() - { - int nObjs = continuousDataSets.size(); - const TimeSeries* tsStruct; - for (int i = 0; i < nObjs; i++) - { - tsStruct = continuousDataSets[i]; - CHECK_ERROR(setAttribute(BaseDataType::U64, &(tsStruct->numSamples), tsStruct->basePath, "num_samples")); - } - nObjs = spikeDataSets.size(); - for (int i = 0; i < nObjs; i++) - { - tsStruct = spikeDataSets[i]; - CHECK_ERROR(setAttribute(BaseDataType::U64, &(tsStruct->numSamples), tsStruct->basePath, "num_samples")); - } - nObjs = eventDataSets.size(); - for (int i = 0; i < nObjs; i++) - { - tsStruct = eventDataSets[i]; - CHECK_ERROR(setAttribute(BaseDataType::U64, &(tsStruct->numSamples), tsStruct->basePath, "num_samples")); - } - - CHECK_ERROR(setAttribute(BaseDataType::U64, &(syncMsgDataSet->numSamples), syncMsgDataSet->basePath, "num_samples")); - - continuousDataSets.clear(); - spikeDataSets.clear(); - eventDataSets.clear(); - syncMsgDataSet = nullptr; - } - - void NWBFile::writeData(int datasetID, int channel, int nSamples, const float* data, float bitVolts) - { - if (!continuousDataSets[datasetID]) - return; - - if (nSamples > 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. - { - std::cerr << "Write buffer overrun, resizing to" << nSamples << std::endl; - bufferSize = nSamples; - scaledBuffer.malloc(nSamples); - intBuffer.malloc(nSamples); - } - - double multFactor = 1 / (float(0x7fff) * bitVolts); - FloatVectorOperations::copyWithMultiply(scaledBuffer.getData(), data, multFactor, nSamples); - AudioDataConverters::convertFloatToInt16LE(scaledBuffer.getData(), intBuffer.getData(), nSamples); - - CHECK_ERROR(continuousDataSets[datasetID]->baseDataSet->writeDataRow(channel, nSamples, BaseDataType::I16, intBuffer)); - - /* Since channels are filled asynchronouysly by the Record Thread, there is no guarantee - that at a any point in time all channels in a dataset have the same number of filled samples. - However, since each dataset is filled from a single source, all channels must have the - same number of samples at acquisition stop. To keep track of the written samples we must chose - an arbitrary channel, and at the end all channels will be the same. */ - - if (channel == 0) //there will always be a first channel or there wouldn't be dataset - continuousDataSets[datasetID]->numSamples += nSamples; - } - - void NWBFile::writeTimestamps(int datasetID, int nSamples, const double* data) - { - if (!continuousDataSets[datasetID]) - return; - - CHECK_ERROR(continuousDataSets[datasetID]->timestampDataSet->writeDataBlock(nSamples, BaseDataType::F64, data)); - } - - void NWBFile::writeSpike(int electrodeId, const SpikeChannel* channel, const SpikeEvent* event) - { - if (!spikeDataSets[electrodeId]) - return; - int nSamples = channel->getTotalSamples() * channel->getNumChannels(); - - if (nSamples > 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. - { - std::cerr << "Write buffer overrun, resizing to" << nSamples << std::endl; - bufferSize = nSamples; - scaledBuffer.malloc(nSamples); - intBuffer.malloc(nSamples); - } - - double multFactor = 1 / (float(0x7fff) * channel->getChannelBitVolts(0)); - FloatVectorOperations::copyWithMultiply(scaledBuffer.getData(), event->getDataPointer(), multFactor, nSamples); - AudioDataConverters::convertFloatToInt16LE(scaledBuffer.getData(), intBuffer.getData(), nSamples); - - double timestampSec = event->getTimestamp() / channel->getSampleRate(); - - CHECK_ERROR(spikeDataSets[electrodeId]->baseDataSet->writeDataBlock(1, BaseDataType::I16, intBuffer)); - CHECK_ERROR(spikeDataSets[electrodeId]->timestampDataSet->writeDataBlock(1, BaseDataType::F64, ×tampSec)); - writeEventMetaData(spikeDataSets[electrodeId], channel, event); - - spikeDataSets[electrodeId]->numSamples += 1; - - } - - void NWBFile::writeEvent(int eventID, const EventChannel* channel, const Event* event) - { - if (!eventDataSets[eventID]) - return; - - const void* dataSrc; - BaseDataType type; - int8 ttlVal; - String text; - - switch (event->getEventType()) - { - case EventChannel::TTL: - ttlVal = (static_cast(event)->getState() ? 1 : -1) * (event->getChannel() + 1); - dataSrc = &ttlVal; - type = BaseDataType::I8; - break; - case EventChannel::TEXT: - text = static_cast(event)->getText(); - dataSrc = text.toUTF8().getAddress(); - type = BaseDataType::STR(text.length()); - break; - default: - dataSrc = static_cast(event)->getBinaryDataPointer(); - type = getEventH5Type(event->getEventType()); - break; - } - CHECK_ERROR(eventDataSets[eventID]->baseDataSet->writeDataBlock(1, type, dataSrc)); - - double timeSec = event->getTimestamp() / channel->getSampleRate(); - - CHECK_ERROR(eventDataSets[eventID]->timestampDataSet->writeDataBlock(1, BaseDataType::F64, &timeSec)); - - uint8 controlValue = event->getChannel() + 1; - - CHECK_ERROR(eventDataSets[eventID]->controlDataSet->writeDataBlock(1, BaseDataType::U8, &controlValue)); - - if (event->getEventType() == EventChannel::TTL) - { - CHECK_ERROR(eventDataSets[eventID]->ttlWordDataSet->writeDataBlock(1, BaseDataType::U8, static_cast(event)->getTTLWordPointer())); - } - - eventDataSets[eventID]->numSamples += 1; - } - - void NWBFile::writeTimestampSyncText(uint16 sourceID, int64 timestamp, float sourceSampleRate, String text) - { - CHECK_ERROR(syncMsgDataSet->baseDataSet->writeDataBlock(1, BaseDataType::STR(text.length()), text.toUTF8())); - double timeSec = timestamp / sourceSampleRate; - CHECK_ERROR(syncMsgDataSet->timestampDataSet->writeDataBlock(1, BaseDataType::F64, &timeSec)); - CHECK_ERROR(syncMsgDataSet->controlDataSet->writeDataBlock(1, BaseDataType::U8, &sourceID)); - syncMsgDataSet->numSamples += 1; - } - - - String NWBFile::getFileName() - { - return filename; - } - - bool NWBFile::createTimeSeriesBase(String basePath, String source, String helpText, String description, StringArray ancestry) - { - if (createGroup(basePath)) return false; - CHECK_ERROR(setAttributeStrArray(ancestry, basePath, "ancestry")); - CHECK_ERROR(setAttributeStr(" ", basePath, "comments")); - CHECK_ERROR(setAttributeStr(description, basePath, "description")); - CHECK_ERROR(setAttributeStr("TimeSeries", basePath, "neurodata_type")); - CHECK_ERROR(setAttributeStr(source, basePath, "source")); - CHECK_ERROR(setAttributeStr(helpText, basePath, "help")); - return true; - } - - void NWBFile::createDataAttributes(String basePath, float conversion, float resolution, String unit) - { - CHECK_ERROR(setAttribute(BaseDataType::F32, &conversion, basePath + "/data", "conversion")); - CHECK_ERROR(setAttribute(BaseDataType::F32, &resolution, basePath + "/data", "resolution")); - CHECK_ERROR(setAttributeStr(unit, basePath + "/data", "unit")); - } - - HDF5RecordingData* NWBFile::createTimestampDataSet(String basePath, int chunk_size) - { - HDF5RecordingData* tsSet = createDataSet(BaseDataType::F64, 0, chunk_size, basePath + "/timestamps"); - if (!tsSet) - std::cerr << "Error creating timestamp dataset in " << basePath << std::endl; - else - { - const int32 one = 1; - CHECK_ERROR(setAttribute(BaseDataType::I32, &one, basePath + "/timestamps", "interval")); - CHECK_ERROR(setAttributeStr("seconds", basePath + "/timestamps", "unit")); - } - return tsSet; - } - - bool NWBFile::createExtraInfo(String basePath, String name, String desc, String id, uint16 index, uint16 typeIndex) - { - if (createGroup(basePath)) return false; - CHECK_ERROR(setAttributeStr("openephys:/", basePath, "schema_id")); - CHECK_ERROR(setAttributeStr(name, basePath, "name")); - CHECK_ERROR(setAttributeStr(desc, basePath, "description")); - CHECK_ERROR(setAttributeStr(id, basePath, "identifier")); - CHECK_ERROR(setAttribute(BaseDataType::U16, &index, basePath, "source_index")); - CHECK_ERROR(setAttribute(BaseDataType::U16, &typeIndex, basePath, "source_type_index")); - return true; - } - - bool NWBFile::createChannelMetaDataSets(String basePath, const MetaDataInfoObject* info) - { - if (!info) return false; - if (createGroup(basePath)) return false; - CHECK_ERROR(setAttributeStr("openephys:/", basePath, "schema_id")); - int nMetaData = info->getMetaDataCount(); - - for (int i = 0; i < nMetaData; i++) - { - const MetaDataDescriptor* desc = info->getMetaDataDescriptor(i); - String fieldName = "Field_" + String(i+1); - String name = desc->getName(); - String description = desc->getDescription(); - String identifier = desc->getIdentifier(); - BaseDataType type = getMetaDataH5Type(desc->getType(), desc->getLength()); //only string types use length, for others is always set to 1. If array types are implemented, change this - int length = desc->getType() == MetaDataDescriptor::CHAR ? 1 : desc->getLength(); //strings are a single element of length set in the type (see above) while other elements are saved a - HeapBlock data(desc->getDataSize()); - info->getMetaDataValue(i)->getValue(static_cast(data.getData())); - createBinaryDataSet(basePath, fieldName, type, length, data.getData()); - String fullPath = basePath + "/" + fieldName; - CHECK_ERROR(setAttributeStr("openephys:/", fullPath, "schema_id")); - CHECK_ERROR(setAttributeStr(name, fullPath, "name")); - CHECK_ERROR(setAttributeStr(description, fullPath, "description")); - CHECK_ERROR(setAttributeStr(identifier, fullPath, "identifier")); - } - return true; - } - - - bool NWBFile::createEventMetaDataSets(String basePath, TimeSeries* timeSeries, const MetaDataEventObject* info) - { - if (!info) return false; - if (createGroup(basePath)) return false; - CHECK_ERROR(setAttributeStr("openephys:/", basePath, "schema_id")); - int nMetaData = info->getEventMetaDataCount(); - - timeSeries->metaDataSet.clear(); //just in case - for (int i = 0; i < nMetaData; i++) - { - const MetaDataDescriptor* desc = info->getEventMetaDataDescriptor(i); - String fieldName = "Field_" + String(i+1); - String name = desc->getName(); - String description = desc->getDescription(); - String identifier = desc->getIdentifier(); - BaseDataType type = getMetaDataH5Type(desc->getType(), desc->getLength()); //only string types use length, for others is always set to 1. If array types are implemented, change this - int length = desc->getType() == MetaDataDescriptor::CHAR ? 1 : desc->getLength(); //strings are a single element of length set in the type (see above) while other elements are saved as arrays - String fullPath = basePath + "/" + fieldName; - HDF5RecordingData* dSet = createDataSet(type, 0, length, EVENT_CHUNK_SIZE, fullPath); - if (!dSet) return false; - timeSeries->metaDataSet.add(dSet); - - CHECK_ERROR(setAttributeStr("openephys:/", fullPath, "schema_id")); - CHECK_ERROR(setAttributeStr(name, fullPath, "name")); - CHECK_ERROR(setAttributeStr(description, fullPath, "description")); - CHECK_ERROR(setAttributeStr(identifier, fullPath, "identifier")); - } - return true; - } - - void NWBFile::writeEventMetaData(TimeSeries* timeSeries, const MetaDataEventObject* info, const MetaDataEvent* event) - { - jassert(timeSeries->metaDataSet.size() == event->getMetadataValueCount()); - jassert(info->getEventMetaDataCount() == event->getMetadataValueCount()); - int nMetaData = event->getMetadataValueCount(); - for (int i = 0; i < nMetaData; i++) - { - BaseDataType type = getMetaDataH5Type(info->getEventMetaDataDescriptor(i)->getType(), info->getEventMetaDataDescriptor(i)->getLength()); - timeSeries->metaDataSet[i]->writeDataBlock(1, type, event->getMetaDataValue(i)->getRawValuePointer()); - } - - } - - void NWBFile::createTextDataSet(String path, String name, String text) - { - ScopedPointer dSet; - - if (text.isEmpty()) text = " "; //to avoid 0-length strings, which cause errors - BaseDataType type = BaseDataType::STR(text.length()); - - dSet = createDataSet(type, 1, 0, path + "/" + name); - if (!dSet) return; - dSet->writeDataBlock(1, type, text.toUTF8()); - } - - void NWBFile::createBinaryDataSet(String path, String name, BaseDataType type, int length, void* data) - { - ScopedPointer dSet; - if ((length < 1) || !data) return; - - dSet = createDataSet(type, 1, length, 1, path + "/" + name); - if (!dSet) return; - dSet->writeDataBlock(1, type, data); - } - - - //These two methods whould be easy to adapt to support array types for all base types, for now - //length is only used for string types. - NWBFile::BaseDataType NWBFile::getEventH5Type(EventChannel::EventChannelTypes type, int length) - { - switch (type) - { - case EventChannel::INT8_ARRAY: - return BaseDataType::I8; - case EventChannel::UINT8_ARRAY: - return BaseDataType::U8; - case EventChannel::INT16_ARRAY: - return BaseDataType::I16; - case EventChannel::UINT16_ARRAY: - return BaseDataType::U16; - case EventChannel::INT32_ARRAY: - return BaseDataType::I32; - case EventChannel::UINT32_ARRAY: - return BaseDataType::U32; - case EventChannel::INT64_ARRAY: - return BaseDataType::I64; - case EventChannel::UINT64_ARRAY: - return BaseDataType::U64; - case EventChannel::FLOAT_ARRAY: - return BaseDataType::F32; - case EventChannel::DOUBLE_ARRAY: - return BaseDataType::F64; - case EventChannel::TEXT: - return BaseDataType::STR(length); - default: - return BaseDataType::I8; - } - } - NWBFile::BaseDataType NWBFile::getMetaDataH5Type(MetaDataDescriptor::MetaDataTypes type, int length) - { - switch (type) - { - case MetaDataDescriptor::INT8: - return BaseDataType::I8; - case MetaDataDescriptor::UINT8: - return BaseDataType::U8; - case MetaDataDescriptor::INT16: - return BaseDataType::I16; - case MetaDataDescriptor::UINT16: - return BaseDataType::U16; - case MetaDataDescriptor::INT32: - return BaseDataType::I32; - case MetaDataDescriptor::UINT32: - return BaseDataType::U32; - case MetaDataDescriptor::INT64: - return BaseDataType::I64; - case MetaDataDescriptor::UINT64: - return BaseDataType::U64; - case MetaDataDescriptor::FLOAT: - return BaseDataType::F32; - case MetaDataDescriptor::DOUBLE: - return BaseDataType::F64; - case MetaDataDescriptor::CHAR: - return BaseDataType::STR(length); - default: - return BaseDataType::I8; - } - } \ No newline at end of file diff --git a/Source/Plugins/NWBFormat/NWBFormat.h b/Source/Plugins/NWBFormat/NWBFormat.h deleted file mode 100644 index bf5fb79b26..0000000000 --- a/Source/Plugins/NWBFormat/NWBFormat.h +++ /dev/null @@ -1,103 +0,0 @@ -/* - ------------------------------------------------------------------ - - This file is part of the Open Ephys GUI - Copyright (C) 2014 Open Ephys - - ------------------------------------------------------------------ - - This program is free software: you can redistribute it and/or modify - it under the terms of the GNU General Public License as published by - the Free Software Foundation, either version 3 of the License, or - (at your option) any later version. - - This program is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU General Public License for more details. - - You should have received a copy of the GNU General Public License - along with this program. If not, see . - - */ - -#ifndef NWBFORMAT_H -#define NWBFORMAT_H - -#include -#include -using namespace OpenEphysHDF5; - -namespace NWBRecording -{ - typedef Array ContinuousGroup; - class TimeSeries - { - public: - ScopedPointer baseDataSet; - ScopedPointer timestampDataSet; - ScopedPointer controlDataSet; //for all but spikes - ScopedPointer ttlWordDataSet; //just for ttl events - OwnedArray metaDataSet; - String basePath; - uint64 numSamples{ 0 }; - }; - - class NWBFile : public HDF5FileBase - { - public: - NWBFile(String fName, String ver, String idText); //with whatever arguments it's necessary - ~NWBFile(); - bool startNewRecording(int recordingNumber, const Array& continuousArray, - const Array& eventArray, const Array& electrodeArray); - void stopRecording(); - void writeData(int datasetID, int channel, int nSamples, const float* data, float bitVolts); - void writeTimestamps(int datasetID, int nSamples, const double* data); - void writeSpike(int electrodeId, const SpikeChannel* channel, const SpikeEvent* event); - void writeEvent(int eventID, const EventChannel* channel, const Event* event); - void writeTimestampSyncText(uint16 sourceID, int64 timestamp, float sourceSampleRate, String text); - String getFileName() override; - void setXmlText(const String& xmlText); - - protected: - int createFileStructure() override; - - private: - - void createTextDataSet(String path, String name, String text); - void createBinaryDataSet(String path, String name, HDF5FileBase::BaseDataType type, int length, void* data); - static HDF5FileBase::BaseDataType getEventH5Type(EventChannel::EventChannelTypes type, int length = 1); - static HDF5FileBase::BaseDataType getMetaDataH5Type(MetaDataDescriptor::MetaDataTypes type, int length = 1); - - bool createTimeSeriesBase(String basePath, String source, String helpText, String description, StringArray ancestry); - bool createExtraInfo(String basePath, String name, String desc, String id, uint16 index, uint16 typeIndex); - HDF5RecordingData* createTimestampDataSet(String basePath, int chunk_size); - void createDataAttributes(String basePath, float conversion, float resolution, String unit); - bool createChannelMetaDataSets(String basePath, const MetaDataInfoObject* info); - bool createEventMetaDataSets(String basePath, TimeSeries* timeSeries, const MetaDataEventObject* info); - - void writeEventMetaData(TimeSeries* timeSeries, const MetaDataEventObject* info, const MetaDataEvent* event); - - - const String filename; - const String GUIVersion; - - OwnedArray continuousDataSets; - OwnedArray spikeDataSets; - OwnedArray eventDataSets; - ScopedPointer syncMsgDataSet; - - const String* xmlText; - const String identifierText; - - HeapBlock scaledBuffer; - HeapBlock intBuffer; - size_t bufferSize; - - JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR(NWBFile); - - }; - -} - -#endif diff --git a/Source/Plugins/NWBFormat/NWBRecording.cpp b/Source/Plugins/NWBFormat/NWBRecording.cpp deleted file mode 100644 index e729934bbf..0000000000 --- a/Source/Plugins/NWBFormat/NWBRecording.cpp +++ /dev/null @@ -1,196 +0,0 @@ -/* - ------------------------------------------------------------------ - - This file is part of the Open Ephys GUI - Copyright (C) 2014 Open Ephys - - ------------------------------------------------------------------ - - This program is free software: you can redistribute it and/or modify - it under the terms of the GNU General Public License as published by - the Free Software Foundation, either version 3 of the License, or - (at your option) any later version. - - This program is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU General Public License for more details. - - You should have received a copy of the GNU General Public License - along with this program. If not, see . - - */ - - #include "NWBRecording.h" -#define MAX_BUFFER_SIZE 40960 - - using namespace NWBRecording; - - NWBRecordEngine::NWBRecordEngine() - { - - tsBuffer.malloc(MAX_BUFFER_SIZE); - } - - NWBRecordEngine::~NWBRecordEngine() - { - } - - String NWBRecordEngine::getEngineID() const - { - return "NWB"; //a text identifier - } - - void NWBRecordEngine::openFiles(File rootFolder, int experimentNumber, int recordingNumber) - { - - //Called when acquisition starts, to open the files - String basepath = rootFolder.getFullPathName() + rootFolder.separatorString + "experiment_" + String(experimentNumber) + ".nwb"; - - recordFile = new NWBFile(basepath, CoreServices::getGUIVersion(), identifierText); - recordFile->setXmlText(getLatestSettingsXml()); - - int recProcs = getNumRecordedProcessors(); - - datasetIndexes.insertMultiple(0, 0, getNumRecordedChannels()); - writeChannelIndexes.insertMultiple(0, 0, getNumRecordedChannels()); - - //Generate the continuous datasets info array, seeking for different combinations of recorded processor and source processor - int lastId = 0; - for (int proc = 0; proc < recProcs; proc++) - { - const RecordProcessorInfo& procInfo = getProcessorInfo(proc); - int recChans = procInfo.recordedChannels.size(); - for (int chan = 0; chan < recChans; chan++) - { - int recordedChan = procInfo.recordedChannels[chan]; - int realChan = getRealChannel(recordedChan); - const DataChannel* channelInfo = getDataChannel(realChan); - int sourceId = channelInfo->getSourceNodeID(); - int sourceSubIdx = channelInfo->getSubProcessorIdx(); - int nInfoArrays = continuousChannels.size(); - bool found = false; - for (int i = lastId; i < nInfoArrays; i++) - { - if (sourceId == continuousChannels.getReference(i)[0]->getSourceNodeID() && sourceSubIdx == continuousChannels.getReference(i)[0]->getSubProcessorIdx()) - { - //A dataset for the current processor from the current source is already present - writeChannelIndexes.set(recordedChan, continuousChannels.getReference(i).size()); - continuousChannels.getReference(i).add(getDataChannel(realChan)); - datasetIndexes.set(recordedChan, i); - found = true; - break; - } - } - if (!found) //a new dataset must be created - { - ContinuousGroup newGroup; - newGroup.add(getDataChannel(realChan)); - continuousChannels.add(newGroup); - datasetIndexes.set(recordedChan, nInfoArrays); - writeChannelIndexes.set(recordedChan, 0); - } - - } - lastId = continuousChannels.size(); - } - int nEvents = getNumRecordedEvents(); - 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 - - //create the recording - recordFile->startNewRecording(recordingNumber, continuousChannels, eventChannels, spikeChannels); - - } - - - void NWBRecordEngine::closeFiles() - { - //Called when acquisition stops. Should close the files and leave the processor in a reset status - recordFile->stopRecording(); - recordFile->close(); - recordFile = nullptr; - resetChannels(); - } - - - - void NWBRecordEngine::resetChannels() - { - spikeChannels.clear(); - eventChannels.clear(); - continuousChannels.clear(); - datasetIndexes.clear(); - writeChannelIndexes.clear(); - tsBuffer.malloc(MAX_BUFFER_SIZE); - bufferSize = MAX_BUFFER_SIZE; - } - - void NWBRecordEngine::writeData(int writeChannel, int realChannel, const float* buffer, int size) - { - - recordFile->writeData(datasetIndexes[writeChannel], writeChannelIndexes[writeChannel], size, buffer, getDataChannel(realChannel)->getBitVolts()); - - /* All channels in a dataset have the same number of samples and share timestamps. But since this method is called - asynchronously, the timestamps might not be in sync during acquisition, so we chose a channel and write the - timestamps when writing that channel's data */ - if (writeChannelIndexes[writeChannel] == 0) - { - int64 baseTS = getTimestamp(writeChannel); - double fs = getDataChannel(realChannel)->getSampleRate(); - //Let's hope that the compiler is smart enough to vectorize this. - for (int i = 0; i < size; i++) - { - tsBuffer[i] = (baseTS + i) / fs; - } - recordFile->writeTimestamps(datasetIndexes[writeChannel], size, tsBuffer); - } - - } - -void NWBRecordEngine::writeEvent(int eventIndex, const MidiMessage& event) -{ - const EventChannel* channel = getEventChannel(eventIndex); - EventPtr eventStruct = Event::deserializeFromMessage(event, channel); - - recordFile->writeEvent(eventIndex, channel, eventStruct); -} - -void NWBRecordEngine::writeTimestampSyncText(uint16 sourceID, uint16 sourceIdx, int64 timestamp, float sourceSampleRate, String text) -{ - recordFile->writeTimestampSyncText(sourceID, timestamp, sourceSampleRate, text); -} - -void NWBRecordEngine::addSpikeElectrode(int index,const SpikeChannel* elec) -{ -} - -void NWBRecordEngine::writeSpike(int electrodeIndex, const SpikeEvent* spike) -{ - const SpikeChannel* channel = getSpikeChannel(electrodeIndex); - - recordFile->writeSpike(electrodeIndex, channel, spike); -} - -RecordEngineManager* NWBRecordEngine::getEngineManager() -{ - //static factory that instantiates the engine manager, which allows to configure recording options among other things. See OriginalRecording to see how to create options for a record engine - RecordEngineManager* man = new RecordEngineManager("NWB", "NWB", &(engineFactory)); - EngineParameter* param; - param = new EngineParameter(EngineParameter::STR, 0, "Identifier Text", String::empty); - man->addParameter(param); - return man; - -} - -void NWBRecordEngine::setParameter(EngineParameter& parameter) -{ - strParameter(0, identifierText); -} \ No newline at end of file diff --git a/Source/Plugins/NWBFormat/NWBRecording.h b/Source/Plugins/NWBFormat/NWBRecording.h deleted file mode 100644 index e74219d6c4..0000000000 --- a/Source/Plugins/NWBFormat/NWBRecording.h +++ /dev/null @@ -1,70 +0,0 @@ -/* - ------------------------------------------------------------------ - - This file is part of the Open Ephys GUI - Copyright (C) 2014 Open Ephys - - ------------------------------------------------------------------ - - This program is free software: you can redistribute it and/or modify - it under the terms of the GNU General Public License as published by - the Free Software Foundation, either version 3 of the License, or - (at your option) any later version. - - This program is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU General Public License for more details. - - You should have received a copy of the GNU General Public License - along with this program. If not, see . - - */ - - #ifndef NWBRECORDING_H - #define NWBRECORDING_H - - #include - #include "NWBFormat.h" - - namespace NWBRecording { - class NWBRecordEngine : public RecordEngine - { - public: - NWBRecordEngine(); - ~NWBRecordEngine(); - //Those are only the basic set of calls. Look at RecordEngine.cpp and RecordEngine.h for description on all possible hooks and the order they're called. - String getEngineID() const override; - void openFiles(File rootFolder, int experimentNumber, int recordingNumber) override; - void closeFiles() override; - void writeData(int writeChannel, int realChannel, const float* buffer, int size) override; - void writeEvent(int eventIndex, const MidiMessage& event) override; - 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 sourceSampleRate, String text) override; - void resetChannels() override; - void setParameter(EngineParameter& parameter) override; - - static RecordEngineManager* getEngineManager(); - - private: - ScopedPointer recordFile; - Array datasetIndexes; - Array writeChannelIndexes; - - Array continuousChannels; - Array eventChannels; - Array spikeChannels; - - HeapBlock tsBuffer; - size_t bufferSize; - - String identifierText; - - JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR(NWBRecordEngine); - - - }; - } - - #endif \ No newline at end of file diff --git a/Source/Plugins/NWBFormat/OpenEphysLib.cpp b/Source/Plugins/NWBFormat/OpenEphysLib.cpp deleted file mode 100644 index f079ebf3f9..0000000000 --- a/Source/Plugins/NWBFormat/OpenEphysLib.cpp +++ /dev/null @@ -1,70 +0,0 @@ -/* ------------------------------------------------------------------- - -This file is part of the Open Ephys GUI -Copyright (C) 2013 Open Ephys - ------------------------------------------------------------------- - -This program is free software: you can redistribute it and/or modify -it under the terms of the GNU General Public License as published by -the Free Software Foundation, either version 3 of the License, or -(at your option) any later version. - -This program is distributed in the hope that it will be useful, -but WITHOUT ANY WARRANTY; without even the implied warranty of -MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -GNU General Public License for more details. - -You should have received a copy of the GNU General Public License -along with this program. If not, see . - -*/ - -#include -#include "NWBRecording.h" -#include -#ifdef WIN32 -#include -#define EXPORT __declspec(dllexport) -#else -#define EXPORT __attribute__((visibility("default"))) -#endif - - -using namespace Plugin; -#define NUM_PLUGINS 1 - -extern "C" EXPORT void getLibInfo(Plugin::LibraryInfo* info) -{ - info->apiVersion = PLUGIN_API_VER; - info->name = "NWB format"; - info->libVersion = 1; - info->numPlugins = NUM_PLUGINS; -} - -extern "C" EXPORT int getPluginInfo(int index, Plugin::PluginInfo* info) -{ - switch (index) - { - case 0: - info->type = Plugin::PLUGIN_TYPE_RECORD_ENGINE; - info->recordEngine.name = "NWB"; - info->recordEngine.creator = &(Plugin::createRecordEngine); - break; - default: - return -1; - } - - return 0; -} - -#ifdef WIN32 -BOOL WINAPI DllMain(IN HINSTANCE hDllHandle, - IN DWORD nReason, - IN LPVOID Reserved) -{ - return TRUE; -} - -#endif diff --git a/Source/Plugins/NetworkEvents/NetworkEvents.cpp b/Source/Plugins/NetworkEvents/NetworkEvents.cpp index d61ddc987b..0c61fddb1b 100644 --- a/Source/Plugins/NetworkEvents/NetworkEvents.cpp +++ b/Source/Plugins/NetworkEvents/NetworkEvents.cpp @@ -514,16 +514,16 @@ String NetworkEvents::handleSpecialMessages (StringTS msg) String msg (file.getFullPathName()); return msg; } - else if (cmd.compareIgnoreCase ("GetRecordingNumber") == 0) + else if (cmd.compareIgnoreCase ("GetBaseName") == 0) { String status; - status += (CoreServices::RecordNode::getRecordingNumber() + 1); + status += CoreServices::RecordNode::getBaseName(); return status; } - else if (cmd.compareIgnoreCase ("GetExperimentNumber") == 0) + else if (cmd.compareIgnoreCase ("GetRecordingNumber") == 0) { String status; - status += CoreServices::RecordNode::getExperimentNumber(); + status += (CoreServices::RecordNode::getRecordingNumber() + 1); return status; } diff --git a/Source/Plugins/SpikeSorter/SpikeSorter.cpp b/Source/Plugins/SpikeSorter/SpikeSorter.cpp index 6be80a43f2..0ecbfbb2df 100644 --- a/Source/Plugins/SpikeSorter/SpikeSorter.cpp +++ b/Source/Plugins/SpikeSorter/SpikeSorter.cpp @@ -545,17 +545,13 @@ void SpikeSorter::setElectrodeName(int index, String newName) void SpikeSorter::setChannel(int electrodeIndex, int channelNum, int newChannel) { mut.enter(); - String log = "Setting electrode " + String(electrodeIndex) + " channel " + String(channelNum)+ - " to " + String(newChannel); - std::cout << log<< std::endl; + //String log = "Setting electrode " + String(electrodeIndex) + " channel " + String(channelNum)+ + // " to " + String(newChannel); + //std::cout << log<< std::endl; - - - String eventlog = "ChanelElectrodeChannel " + String(electrodes[electrodeIndex]->electrodeID) + " " + String(channelNum) + " " + String(newChannel); + //String eventlog = "ChanelElectrodeChannel " + String(electrodes[electrodeIndex]->electrodeID) + " " + String(channelNum) + " " + String(newChannel); //addNetworkEventToQueue(StringTS(eventlog)); - - // updateSinks(electrodes[electrodeIndex]->electrodeID, channelNum,newChannel); - + //updateSinks(electrodes[electrodeIndex]->electrodeID, channelNum,newChannel); *(electrodes[electrodeIndex]->channels+channelNum) = newChannel; mut.exit(); } @@ -1102,7 +1098,9 @@ bool SpikeSorter::samplesAvailable(int nSamples) } -void SpikeSorter::addProbes(String probeType,int numProbes, int nElectrodesPerProbe, int nChansPerElectrode, double firstContactOffset, double interelectrodeDistance) +void SpikeSorter::addProbes(String probeType, int numProbes, int nElectrodesPerProbe, + int nChansPerElectrode, double firstContactOffset, + double interelectrodeDistance) { for (int probeIter=0; probeIter 1) - name = probeType + " " + String(probeCounter) + " ["+String(electrodeIter+1)+"/"+String(nElectrodesPerProbe)+"]"; + name = probeType + " " + String(probeCounter) + " ["+String(electrodeIter+1) + + "/" + String(nElectrodesPerProbe) + "]"; else name = probeType + " " + String(probeCounter); diff --git a/Source/Plugins/SpikeSorter/SpikeSorterEditor.cpp b/Source/Plugins/SpikeSorter/SpikeSorterEditor.cpp index d9451e61a1..cffa690e18 100644 --- a/Source/Plugins/SpikeSorter/SpikeSorterEditor.cpp +++ b/Source/Plugins/SpikeSorter/SpikeSorterEditor.cpp @@ -357,15 +357,6 @@ void SpikeSorterEditor::buttonEvent(Button* button) //updateAdvancerList(); PopupMenu probeMenu; probeMenu.addItem(1,"Single Electrode"); - probeMenu.addItem(2,"Stereotrode"); - probeMenu.addItem(3,"Tetrode"); - PopupMenu depthprobeMenu; - depthprobeMenu.addItem(4,"8 ch, 125um"); - depthprobeMenu.addItem(5,"16 ch, 125um"); - depthprobeMenu.addItem(6,"24 ch, 125um"); - depthprobeMenu.addItem(7,"32 ch, 50um"); - depthprobeMenu.addItem(8,"32 ch, 25um"); - probeMenu.addSubMenu("Depth probe", depthprobeMenu,true); const int result = probeMenu.show(); int nChansPerElectrode = 0; @@ -385,56 +376,10 @@ void SpikeSorterEditor::buttonEvent(Button* button) nElectrodes = 1; firstElectrodeOffset=0; break; - case 2: - ProbeType = "Stereotrode"; - nChansPerElectrode = 2; - nElectrodes = 1; - firstElectrodeOffset = 0; - break; - case 3: - ProbeType = "Tetrode"; - nChansPerElectrode = 4; - nElectrodes = 1; - firstElectrodeOffset = 0; - break; - case 4: - ProbeType = "Depth Probe"; - nChansPerElectrode = 1; - nElectrodes = 8; - interelectrodeDistance = 0.125; - firstElectrodeOffset= -0.5; - break; - case 5: - ProbeType = "Depth Probe"; - nChansPerElectrode = 1; - nElectrodes = 16; - interelectrodeDistance = 0.125; - firstElectrodeOffset= -0.5; - break; - case 6: - ProbeType = "Depth Probe"; - nChansPerElectrode = 1; - nElectrodes = 24; - interelectrodeDistance = 0.125; - firstElectrodeOffset= -0.5; - break; - case 7: - ProbeType = "Depth Probe"; - nChansPerElectrode = 1; - nElectrodes = 32; - interelectrodeDistance = 0.050; - firstElectrodeOffset= -0.5; - break; - case 8: - ProbeType = "Depth Probe"; - nChansPerElectrode = 1; - nElectrodes = 32; - interelectrodeDistance = 0.025; - firstElectrodeOffset= -0.075; - break; } - processor->addProbes(ProbeType,numProbes, nElectrodes,nChansPerElectrode, firstElectrodeOffset,interelectrodeDistance); + processor->addProbes(ProbeType, numProbes, nElectrodes, nChansPerElectrode, + firstElectrodeOffset, interelectrodeDistance); refreshElectrodeList(); CoreServices::updateSignalChain(this); diff --git a/Source/Plugins/SpikeSorter/SpikeSorterEditor.h b/Source/Plugins/SpikeSorter/SpikeSorterEditor.h index f4e16b1cdb..3afff79e0c 100644 --- a/Source/Plugins/SpikeSorter/SpikeSorterEditor.h +++ b/Source/Plugins/SpikeSorter/SpikeSorterEditor.h @@ -32,7 +32,7 @@ class SpikeSorterCanvas; User interface for the SpikeSorter processor. - Allows the user to add single electrodes, stereotrodes, or tetrodes. + Allows the user to add single electrodes, stereotrodes, tetrodes, etc. Parameters of individual channels, such as channel mapping, threshold, and enabled state, can be edited. diff --git a/Source/Processors/Channel/InfoObjects.cpp b/Source/Processors/Channel/InfoObjects.cpp index 6f300da9e5..ede0967b40 100644 --- a/Source/Processors/Channel/InfoObjects.cpp +++ b/Source/Processors/Channel/InfoObjects.cpp @@ -303,7 +303,7 @@ void DataChannel::setDefaultNameAndDescription() break; case AUX_CHANNEL: name = "AUX "; - description = "Auxiliar"; + description = "Auxiliary"; break; case ADC_CHANNEL: name = "ADC "; @@ -524,7 +524,11 @@ SpikeChannel::SpikeChannel(ElectrodeTypes type, GenericProcessor* source, const m_sourceInfo.add(info); m_channelBitVolts.add(chan->getBitVolts()); } - setDefaultNameAndDescription(); + //setDefaultNameAndDescription(); + const DataChannel* datachan0 = sourceChannels[0]; + setName(datachan0->getName()); + setDescription("Single electrode spike data source"); + setIdentifier("spikesource"); } SpikeChannel::~SpikeChannel() @@ -576,8 +580,6 @@ unsigned int SpikeChannel::getNumChannels(SpikeChannel::ElectrodeTypes type) switch (type) { case SINGLE: return 1; - case STEREOTRODE: return 2; - case TETRODE: return 4; default: return 0; } } @@ -587,8 +589,6 @@ SpikeChannel::ElectrodeTypes SpikeChannel::typeFromNumChannels(unsigned int nCha switch (nChannels) { case 1: return SINGLE; - case 2: return STEREOTRODE; - case 4: return TETRODE; default: return INVALID; } } @@ -621,14 +621,6 @@ void SpikeChannel::setDefaultNameAndDescription() name = "SE "; description = "Single electrode"; break; - case STEREOTRODE: - name = "ST "; - description = "Stereotrode"; - break; - case TETRODE: - name = "TT "; - description = "Tetrode"; - break; default: name = "INVALID "; break; } name += String(" p") + String(getSourceNodeID()) + String(".") + String(getSubProcessorIdx()) + String(" n") + String(getSourceTypeIndex()); diff --git a/Source/Processors/Channel/InfoObjects.h b/Source/Processors/Channel/InfoObjects.h index 597fa4f307..d4a8fb7a46 100644 --- a/Source/Processors/Channel/InfoObjects.h +++ b/Source/Processors/Channel/InfoObjects.h @@ -378,13 +378,11 @@ class PLUGIN_API SpikeChannel : enum ElectrodeTypes { SINGLE, - STEREOTRODE, - TETRODE, INVALID = 100 }; /** Default constructor - @param type The type of electrode this channel represents (SINGLE, STEREOTRODE, TETRODE) + @param type The type of electrode this channel represents (SINGLE, STEREOTRODE, TETRODE, etc.) @param source A pointer to the source processor @param souceChannels An array containing const pointers to the channels that originate the data for this spike electrode @param subproc Optional. The source subprocessor index. @@ -474,4 +472,4 @@ class PLUGIN_API ConfigurationObject : -#endif \ No newline at end of file +#endif diff --git a/Source/Processors/DataThreads/RhythmNode/RHD2000Editor.cpp b/Source/Processors/DataThreads/RhythmNode/RHD2000Editor.cpp index 0ff09df895..5511ce9ec8 100644 --- a/Source/Processors/DataThreads/RhythmNode/RHD2000Editor.cpp +++ b/Source/Processors/DataThreads/RhythmNode/RHD2000Editor.cpp @@ -37,6 +37,12 @@ inline double round(double x) #endif #endif +String round10(double x) +// round to nearest 0.1 and return as String +{ + return String(round(x * 10.f) / 10.f); +} + FPGAchannelList::FPGAchannelList(GenericProcessor* proc_, Viewport* p, FPGAcanvas* c) : chainUpdate(false), viewport(p), canvas(c) { proc = (SourceNode*)proc_; @@ -120,8 +126,8 @@ void FPGAchannelList::buttonClicked(Button* btn) void FPGAchannelList::update() { - // const int columnWidth = 330; - const int columnWidth = 250; + // const int columnWidth = 330; + const int columnWidth = 250; // Query processor for number of channels, types, gains, etc... and update the UI channelComponents.clear(); staticLabels.clear(); @@ -131,7 +137,6 @@ void FPGAchannelList::update() // find out which streams are active. bool hsActive[MAX_NUM_HEADSTAGES+1]; - //bool adcActive = false; int numActiveHeadstages = 0; int hsColumn[MAX_NUM_HEADSTAGES + 1]; int numChannelsPerHeadstage[MAX_NUM_HEADSTAGES + 1]; @@ -156,7 +161,7 @@ void FPGAchannelList::update() if (thread->getNumDataOutputs(DataChannel::ADC_CHANNEL,0) > 0) { - numChannelsPerHeadstage[MAX_NUM_HEADSTAGES] = thread->getNumDataOutputs(DataChannel::ADC_CHANNEL, 0); + numChannelsPerHeadstage[MAX_NUM_HEADSTAGES] = thread->getNumDataOutputs(DataChannel::ADC_CHANNEL, 0); hsActive[MAX_NUM_HEADSTAGES] = true; hsColumn[MAX_NUM_HEADSTAGES] = numActiveHeadstages*columnWidth; numActiveHeadstages++; @@ -195,11 +200,14 @@ void FPGAchannelList::update() } - for (int k = 0; k < MAX_NUM_HEADSTAGES + 1; k++) + for (int k = 0; k < MAX_NUM_HEADSTAGES + 1; k++) // +1 is for the ADC "headstage" { if (hsActive[k]) { - for (int ch = 0; ch < numChannelsPerHeadstage[k]+ (k < MAX_NUM_HEADSTAGES ? 3 : 0); ch++) + int nchans = numChannelsPerHeadstage[k]; + if (k < MAX_NUM_HEADSTAGES && thread->isAuxEnabled()) + nchans += 3; + for (int ch = 0; ch < nchans; ch++) { int channelGainIndex = 1; int realChan = thread->getChannelFromHeadstage(k, ch); @@ -217,7 +225,7 @@ void FPGAchannelList::update() else type = DataChannel::ADC_CHANNEL; - FPGAchannelComponent* comp = new FPGAchannelComponent(this, realChan, channelGainIndex + 1, thread->getChannelName(realChan), gains,type); + FPGAchannelComponent* comp = new FPGAchannelComponent(this, realChan, channelGainIndex + 1, thread->getChannelName(realChan), gains, type); comp->setBounds(10 + hsColumn[k], 70 + ch * 22, columnWidth, 22); comp->setUserDefinedData(k); addAndMakeVisible(comp); @@ -239,11 +247,11 @@ void FPGAchannelList::update() channelComponents.add(comp); } - Label* lbl = new Label("TTL Events","TTL Events"); + Label* lbl = new Label("TTL Events", "TTL Events"); lbl->setEditable(false); - lbl->setBounds(numActiveHeadstages*columnWidth,40,columnWidth, 25); + lbl->setBounds(numActiveHeadstages*columnWidth, 40, columnWidth, 25); lbl->setJustificationType(juce::Justification::centred); - lbl->setColour(Label::textColourId,juce::Colours::white); + lbl->setColour(Label::textColourId, juce::Colours::white); staticLabels.add(lbl); addAndMakeVisible(lbl); @@ -256,10 +264,10 @@ void FPGAchannelList::disableAll() { channelComponents[k]->disableEdit(); } - impedanceButton->setEnabled(false); - saveImpedanceButton->setEnabled(false); - autoMeasureButton->setEnabled(false); - numberingScheme->setEnabled(false); + impedanceButton->setEnabled(false); + saveImpedanceButton->setEnabled(false); + autoMeasureButton->setEnabled(false); + numberingScheme->setEnabled(false); } void FPGAchannelList::enableAll() @@ -268,10 +276,10 @@ void FPGAchannelList::enableAll() { channelComponents[k]->enableEdit(); } - impedanceButton->setEnabled(true); - saveImpedanceButton->setEnabled(true); - autoMeasureButton->setEnabled(true); - numberingScheme->setEnabled(true); + impedanceButton->setEnabled(true); + saveImpedanceButton->setEnabled(true); + autoMeasureButton->setEnabled(true); + numberingScheme->setEnabled(true); } void FPGAchannelList::setNewGain(int channel, float gain) @@ -314,21 +322,21 @@ void FPGAchannelList::comboBoxChanged(ComboBox* b) void FPGAchannelList::updateImpedance(Array streams, Array channels, Array magnitude, Array phase) { - int i = 0; + int i = 0; for (int k = 0; k < streams.size(); k++) { - if (i >= channelComponents.size()) - break; //little safety - - if (channelComponents[i]->type != DataChannel::HEADSTAGE_CHANNEL) - { - k--; - } - else - { - channelComponents[i]->setImpedanceValues(magnitude[k], phase[k]); - } - i++; + if (i >= channelComponents.size()) + break; //little safety + + if (channelComponents[i]->type != DataChannel::HEADSTAGE_CHANNEL) + { + k--; + } + else + { + channelComponents[i]->setImpedanceValues(magnitude[k], phase[k]); + } + i++; } } @@ -459,7 +467,7 @@ void FPGAchannelComponent::resized() if (impedance != nullptr) { // impedance->setBounds(180,0,130,20); - impedance->setBounds(100, 0, 130, 20); + impedance->setBounds(100, 0, 130, 20); } } @@ -523,10 +531,10 @@ void FPGAcanvas::update() { // create channel buttons (name, gain, recording, impedance, ... ?) channelList->update(); - if (static_cast(processor->getThread())->isAcquisitionActive()) - { - channelList->disableAll(); - } + if (static_cast(processor->getThread())->isAcquisitionActive()) + { + channelList->disableAll(); + } } void FPGAcanvas::resized() @@ -564,10 +572,10 @@ RHD2000Editor::RHD2000Editor(GenericProcessor* parentNode, measureWhenRecording = false; saveImpedances = false; - impedanceData = new ImpedanceData(); - impedanceData->valid = false; + impedanceData = new ImpedanceData(); + impedanceData->valid = false; - // add headstage-specific controls (currently just an enable/disable button) + // add headstage-specific controls (currently just a toggle button) for (int i = 0; i < 4; i++) { HeadstageOptionsInterface* hsOptions = new HeadstageOptionsInterface(board, this, i); @@ -576,35 +584,52 @@ RHD2000Editor::RHD2000Editor(GenericProcessor* parentNode, hsOptions->setBounds(3, 28+i*20, 70, 18); } + // add rescan button + rescanButton = new UtilityButton("RESCAN", Font("Small Text", 13, Font::plain)); + rescanButton->setRadius(3.0f); + rescanButton->setBounds(6, 108, 65, 18); + rescanButton->addListener(this); + rescanButton->setTooltip("Check for connected headstages"); + addAndMakeVisible(rescanButton); + // add sample rate selection sampleRateInterface = new SampleRateInterface(board, this); addAndMakeVisible(sampleRateInterface); - sampleRateInterface->setBounds(80, 25, 110, 50); + sampleRateInterface->setBounds(80, 20, 80, 50); // add Bandwidth selection bandwidthInterface = new BandwidthInterface(board, this); addAndMakeVisible(bandwidthInterface); - bandwidthInterface->setBounds(80, 58, 80, 50); + bandwidthInterface->setBounds(80, 55, 80, 50); - // add DSP selection - // dspInterface = new DSPInterface(board, this); - // addAndMakeVisible(dspInterface); - // dspInterface->setBounds(80, 58, 80, 50); + auxButton = new UtilityButton("AUX", Font("Small Text", 13, Font::plain)); + auxButton->setRadius(3.0f); + auxButton->setBounds(80, 108, 32, 18); + auxButton->addListener(this); + auxButton->setClickingTogglesState(true); + auxButton->setTooltip("Toggle AUX channels (3 per headstage)"); + addAndMakeVisible(auxButton); - // add rescan button - rescanButton = new UtilityButton("RESCAN", Font("Small Text", 13, Font::plain)); - rescanButton->setRadius(3.0f); - rescanButton->setBounds(6, 108,65,18); - rescanButton->addListener(this); - rescanButton->setTooltip("Check for connected headstages"); - addAndMakeVisible(rescanButton); + adcButton = new UtilityButton("ADC", Font("Small Text", 13, Font::plain)); + adcButton->setRadius(3.0f); + adcButton->setBounds(80+32+1, 108, 32, 18); + adcButton->addListener(this); + adcButton->setClickingTogglesState(true); + adcButton->setTooltip("Toggle 8 external HDMI ADC channels"); + addAndMakeVisible(adcButton); + + audioLabel = new Label("audio label", "Audio out"); + audioLabel->setBounds(170, 20, 75, 15); + audioLabel->setFont(Font("Small Text", 10, Font::plain)); + audioLabel->setColour(Label::textColourId, Colours::darkgrey); + addAndMakeVisible(audioLabel); for (int i = 0; i < 2; i++) { ElectrodeButton* button = new ElectrodeButton(-1); electrodeButtons.add(button); - button->setBounds(200+i*25, 40, 25, 15); + button->setBounds(174+i*30, 35, 30, 15); button->setChannelNum(-1); button->setToggleState(false, dontSendNotification); button->setRadioGroupId(999); @@ -622,97 +647,81 @@ RHD2000Editor::RHD2000Editor(GenericProcessor* parentNode, } } - audioLabel = new Label("audio label", "Audio out"); - audioLabel->setBounds(190,25,75,15); - audioLabel->setFont(Font("Small Text", 10, Font::plain)); - audioLabel->setColour(Label::textColourId, Colours::darkgrey); - addAndMakeVisible(audioLabel); - // add HW audio parameter selection audioInterface = new AudioInterface(board, this); addAndMakeVisible(audioInterface); - audioInterface->setBounds(179, 58, 70, 50); + audioInterface->setBounds(174, 55, 80, 50); clockInterface = new ClockDivideInterface(board, this); addAndMakeVisible(clockInterface); - clockInterface->setBounds(179, 82, 70, 50); - - adcButton = new UtilityButton("ADC 1-8", Font("Small Text", 13, Font::plain)); - adcButton->setRadius(3.0f); - adcButton->setBounds(179,108,70,18); - adcButton->addListener(this); - adcButton->setClickingTogglesState(true); - adcButton->setTooltip("Enable/disable ADC channels"); - addAndMakeVisible(adcButton); - - ledButton = new UtilityButton("LED", Font("Very Small Text", 13, Font::plain)); - ledButton->setRadius(3.0f); - ledButton->setBounds(140, 108, 30, 18); - ledButton->addListener(this); - ledButton->setClickingTogglesState(true); - ledButton->setTooltip("Enable/disable board LEDs"); - addAndMakeVisible(ledButton); - ledButton->setToggleState(true, dontSendNotification); + clockInterface->setBounds(174, 80, 80, 50); // add DSP Offset Button - dspoffsetButton = new UtilityButton("DSP", Font("Very Small Text", 13, Font::plain)); + dspoffsetButton = new UtilityButton("DSP:", Font("Small Text", 13, Font::plain)); dspoffsetButton->setRadius(3.0f); // sets the radius of the button's corners - dspoffsetButton->setBounds(80, 108,30,18); // sets the x position, y position, width, and height of the button + dspoffsetButton->setBounds(174, 108, 32, 18); // sets the x position, y position, width, and height of the button dspoffsetButton->addListener(this); dspoffsetButton->setClickingTogglesState(true); // makes the button toggle its state when clicked - dspoffsetButton->setTooltip("Enable/disable DSP offset removal"); + dspoffsetButton->setTooltip("Toggle DSP offset removal"); addAndMakeVisible(dspoffsetButton); // makes the button a child component of the editor and makes it visible dspoffsetButton->setToggleState(true, dontSendNotification); // add DSP Frequency Selection field dspInterface = new DSPInterface(board, this); addAndMakeVisible(dspInterface); - dspInterface->setBounds(110, 108, 30, 50); - - ttlSettleLabel = new Label("TTL Settle","TTL Settle"); - ttlSettleLabel->setFont(Font("Small Text", 11, Font::plain)); - ttlSettleLabel->setBounds(255,80,70,20); - ttlSettleLabel->setColour(Label::textColourId, Colours::darkgrey); - addAndMakeVisible(ttlSettleLabel); - - - ttlSettleCombo = new ComboBox("FastSettleComboBox"); - ttlSettleCombo->setBounds(260,100,60,18); - ttlSettleCombo->addListener(this); - ttlSettleCombo->addItem("-",1); - for (int k=0; k<8; k++) - { - ttlSettleCombo->addItem("TTL"+String(1+k),2+k); - } - ttlSettleCombo->setSelectedId(1, sendNotification); - addAndMakeVisible(ttlSettleCombo); + dspInterface->setBounds(174+32, 108, 80-32, 50); dacTTLButton = new UtilityButton("DAC TTL", Font("Small Text", 13, Font::plain)); dacTTLButton->setRadius(3.0f); - dacTTLButton->setBounds(260,25,65,18); + dacTTLButton->setBounds(260, 25, 60, 18); dacTTLButton->addListener(this); dacTTLButton->setClickingTogglesState(true); - dacTTLButton->setTooltip("Enable/disable DAC Threshold TTL Output"); + dacTTLButton->setTooltip("Toggle DAC Threshold TTL Output"); addAndMakeVisible(dacTTLButton); - dacHPFlabel = new Label("DAC HPF","DAC HPF"); - dacHPFlabel->setFont(Font("Small Text", 11, Font::plain)); - dacHPFlabel->setBounds(260,42,65,20); + dacHPFlabel = new Label("DAC HPF", "DAC HPF"); + dacHPFlabel->setFont(Font("Small Text", 10, Font::plain)); + dacHPFlabel->setBounds(255, 40, 60, 20); dacHPFlabel->setColour(Label::textColourId, Colours::darkgrey); addAndMakeVisible(dacHPFlabel); dacHPFcombo = new ComboBox("dacHPFCombo"); - dacHPFcombo->setBounds(260,60,60,18); + dacHPFcombo->setBounds(260, 55, 60, 18); dacHPFcombo->addListener(this); - dacHPFcombo->addItem("OFF",1); - int HPFvalues[10] = {50,100,200,300,400,500,600,700,800,900}; + dacHPFcombo->addItem("OFF", 1); + int HPFvalues[10] = {50, 100, 200, 300, 400, 500, 600, 700, 800, 900}; for (int k=0; k<10; k++) { - dacHPFcombo->addItem(String(HPFvalues[k])+" Hz",2+k); + dacHPFcombo->addItem(String(HPFvalues[k])+" Hz", 2+k); } dacHPFcombo->setSelectedId(1, sendNotification); addAndMakeVisible(dacHPFcombo); + ttlSettleLabel = new Label("TTL Settle", "TTL Settle"); + ttlSettleLabel->setFont(Font("Small Text", 10, Font::plain)); + ttlSettleLabel->setBounds(255, 70, 70, 20); + ttlSettleLabel->setColour(Label::textColourId, Colours::darkgrey); + addAndMakeVisible(ttlSettleLabel); + + ttlSettleCombo = new ComboBox("FastSettleComboBox"); + ttlSettleCombo->setBounds(260, 85, 60, 18); + ttlSettleCombo->addListener(this); + ttlSettleCombo->addItem("-",1); + for (int k=0; k<8; k++) + { + ttlSettleCombo->addItem("TTL"+String(1+k), 2+k); + } + ttlSettleCombo->setSelectedId(1, sendNotification); + addAndMakeVisible(ttlSettleCombo); + + ledButton = new UtilityButton("LED", Font("Small Text", 13, Font::plain)); + ledButton->setRadius(3.0f); + ledButton->setBounds(288, 108, 32, 18); + ledButton->addListener(this); + ledButton->setClickingTogglesState(true); + ledButton->setTooltip("Toggle board LEDs"); + addAndMakeVisible(ledButton); + ledButton->setToggleState(true, dontSendNotification); } RHD2000Editor::~RHD2000Editor() @@ -727,23 +736,23 @@ void RHD2000Editor::scanPorts() void RHD2000Editor::measureImpedance() { - impedanceData->valid = false; - board->runImpedanceTest(impedanceData); + impedanceData->valid = false; + board->runImpedanceTest(impedanceData); } void RHD2000Editor::handleAsyncUpdate() { - if (!impedanceData->valid) - return; + if (!impedanceData->valid) + return; if (canvas == nullptr) VisualizerEditor::canvas = createNewCanvas(); // update components... - canvas->updateImpedance(impedanceData->streams, impedanceData->channels, impedanceData->magnitudes, impedanceData->phases); + canvas->updateImpedance(impedanceData->streams, impedanceData->channels, impedanceData->magnitudes, impedanceData->phases); if (saveImpedances) { - CoreServices::RecordNode::createNewrecordingDir(); + CoreServices::RecordNode::createNewrecordingDir(); - String path(CoreServices::RecordNode::getRecordingPath().getFullPathName() + String path(CoreServices::RecordNode::getRecordingPath().getFullPathName() + File::separatorString + "impedance_measurement.xml"); std::cout << "Saving impedance measurements in " << path << "\n"; File file(path); @@ -753,14 +762,14 @@ void RHD2000Editor::handleAsyncUpdate() XmlDocument doc(file); ScopedPointer xml = new XmlElement("CHANNEL_IMPEDANCES"); - for (int i = 0; i < impedanceData->channels.size(); i++) + for (int i = 0; i < impedanceData->channels.size(); i++) { XmlElement* chan = new XmlElement("CHANNEL"); - chan->setAttribute("name",board->getChannelName(i)); - chan->setAttribute("stream", impedanceData->streams[i]); - chan->setAttribute("channel_number", impedanceData->channels[i]); - chan->setAttribute("magnitude", impedanceData->magnitudes[i]); - chan->setAttribute("phase", impedanceData->phases[i]); + chan->setAttribute("name", board->getChannelName(i)); + chan->setAttribute("stream", impedanceData->streams[i]); + chan->setAttribute("channel_number", impedanceData->channels[i]); + chan->setAttribute("magnitude", impedanceData->magnitudes[i]); + chan->setAttribute("phase", impedanceData->phases[i]); xml->addChildElement(chan); } xml->writeToFile(file,String::empty); @@ -828,8 +837,7 @@ void RHD2000Editor::buttonEvent(Button* button) { headstageOptionsInterfaces[i]->checkEnabledState(); } - // board->updateChannelNames(); - CoreServices::updateSignalChain(this); + CoreServices::updateSignalChain(this); } else if (button == electrodeButtons[0]) { @@ -839,13 +847,18 @@ void RHD2000Editor::buttonEvent(Button* button) { channelSelector->setRadioStatus(true); } + else if (button == auxButton && !acquisitionIsActive) + { + board->enableAuxs(button->getToggleState()); + std::cout << "AUX Button toggled" << "\n"; + CoreServices::updateSignalChain(this); + } else if (button == adcButton && !acquisitionIsActive) { board->enableAdcs(button->getToggleState()); - // board->updateChannelNames(); std::cout << "ADC Button toggled" << "\n"; - CoreServices::updateSignalChain(this); - std::cout << "Editor visible." << "\n"; + CoreServices::updateSignalChain(this); + } else if (button == dacTTLButton) { @@ -856,15 +869,15 @@ void RHD2000Editor::buttonEvent(Button* button) std::cout << "DSP offset " << button->getToggleState() << "\n"; board->setDSPOffset(button->getToggleState()); } - else if (button == ledButton) - { - board->enableBoardLeds(button->getToggleState()); - } + else if (button == ledButton) + { + board->enableBoardLeds(button->getToggleState()); + } /* - else - { - VisualizerEditor::buttonEvent(button); - } + else + { + VisualizerEditor::buttonEvent(button); + } */ } @@ -891,12 +904,12 @@ void RHD2000Editor::startAcquisition() channelSelector->startAcquisition(); rescanButton->setEnabledState(false); + auxButton->setEnabledState(false); adcButton->setEnabledState(false); dspoffsetButton-> setEnabledState(false); acquisitionIsActive = true; - if (canvas != nullptr) - canvas->channelList->disableAll(); - //canvas->channelList->setEnabled(false); + if (canvas != nullptr) + canvas->channelList->disableAll(); } void RHD2000Editor::stopAcquisition() @@ -905,24 +918,25 @@ void RHD2000Editor::stopAcquisition() channelSelector->stopAcquisition(); rescanButton->setEnabledState(true); + auxButton->setEnabledState(true); adcButton->setEnabledState(true); dspoffsetButton-> setEnabledState(true); acquisitionIsActive = false; - if (canvas != nullptr) - canvas->channelList->enableAll(); - // canvas->channelList->setEnabled(true); + if (canvas != nullptr) + canvas->channelList->enableAll(); } void RHD2000Editor::saveCustomParameters(XmlElement* xml) { xml->setAttribute("SampleRate", sampleRateInterface->getSelectedId()); + xml->setAttribute("SampleRateString", sampleRateInterface->getText()); xml->setAttribute("LowCut", bandwidthInterface->getLowerBandwidth()); xml->setAttribute("HighCut", bandwidthInterface->getUpperBandwidth()); - xml->setAttribute("ADCsOn", adcButton->getToggleState()); - xml->setAttribute("SampleRate", sampleRateInterface->getSelectedId()); - xml->setAttribute("LowCut", bandwidthInterface->getLowerBandwidth()); - xml->setAttribute("HighCut", bandwidthInterface->getUpperBandwidth()); + // also save requested bandwidths: board values often diverge from requested ones: + xml->setAttribute("LowCutRequested", bandwidthInterface->getLowerBandwidthRequested()); + xml->setAttribute("HighCutRequested", bandwidthInterface->getUpperBandwidthRequested()); + xml->setAttribute("AUXsOn", auxButton->getToggleState()); xml->setAttribute("ADCsOn", adcButton->getToggleState()); xml->setAttribute("AudioOutputL", electrodeButtons[0]->getChannelNum()); xml->setAttribute("AudioOutputR", electrodeButtons[1]->getChannelNum()); @@ -934,16 +948,18 @@ void RHD2000Editor::saveCustomParameters(XmlElement* xml) xml->setAttribute("DSPCutoffFreq", dspInterface->getDspCutoffFreq()); xml->setAttribute("save_impedance_measurements",saveImpedances); xml->setAttribute("auto_measure_impedances",measureWhenRecording); - xml->setAttribute("LEDs", ledButton->getToggleState()); - xml->setAttribute("ClockDivideRatio", clockInterface->getClockDivideRatio()); + xml->setAttribute("LEDs", ledButton->getToggleState()); + xml->setAttribute("ClockDivideRatio", clockInterface->getClockDivideRatio()); } void RHD2000Editor::loadCustomParameters(XmlElement* xml) { sampleRateInterface->setSelectedId(xml->getIntAttribute("SampleRate")); - bandwidthInterface->setLowerBandwidth(xml->getDoubleAttribute("LowCut")); - bandwidthInterface->setUpperBandwidth(xml->getDoubleAttribute("HighCut")); + // request the same bandwidth values as before to get the same board values: + bandwidthInterface->setLowerBandwidth(xml->getDoubleAttribute("LowCutRequested")); + bandwidthInterface->setUpperBandwidth(xml->getDoubleAttribute("HighCutRequested")); + auxButton->setToggleState(xml->getBoolAttribute("AUXsOn"), sendNotification); adcButton->setToggleState(xml->getBoolAttribute("ADCsOn"), sendNotification); //electrodeButtons[0]->setChannelNum(xml->getIntAttribute("AudioOutputL")); //board->assignAudioOut(0, xml->getIntAttribute("AudioOutputL")); @@ -957,8 +973,8 @@ void RHD2000Editor::loadCustomParameters(XmlElement* xml) dspInterface->setDspCutoffFreq(xml->getDoubleAttribute("DSPCutoffFreq")); saveImpedances = xml->getBoolAttribute("save_impedance_measurements"); measureWhenRecording = xml->getBoolAttribute("auto_measure_impedances"); - ledButton->setToggleState(xml->getBoolAttribute("LEDs", true),sendNotification); - clockInterface->setClockDivideRatio(xml->getIntAttribute("ClockDivideRatio")); + ledButton->setToggleState(xml->getBoolAttribute("LEDs", true),sendNotification); + clockInterface->setClockDivideRatio(xml->getIntAttribute("ClockDivideRatio")); } @@ -977,33 +993,28 @@ BandwidthInterface::BandwidthInterface(RHD2000Thread* board_, RHD2000Editor* editor_) : board(board_), editor(editor_) { - name = "Bandwidth"; - lastHighCutString = "7500"; - lastLowCutString = "1"; + // init board to default values: + board->setUpperBandwidth(board->getDesiredUpperBandwidth()); + board->setLowerBandwidth(board->getDesiredLowerBandwidth()); - actualUpperBandwidth = 7500.0f; - actualLowerBandwidth = 1.0f; - - upperBandwidthSelection = new Label("UpperBandwidth",lastHighCutString); // this is currently set in RHD2000Thread, the cleaner would be to set it here again - upperBandwidthSelection->setEditable(true,false,false); + upperBandwidthSelection = new Label("UpperBandwidth", round10(board->getUpperBandwidth())); + upperBandwidthSelection->setTooltip("Upper bandwidth limit (Hz)"); + upperBandwidthSelection->setEditable(true, false, false); upperBandwidthSelection->addListener(this); - upperBandwidthSelection->setBounds(30,30,60,20); + upperBandwidthSelection->setBounds(30, 25, 60, 20); upperBandwidthSelection->setColour(Label::textColourId, Colours::darkgrey); addAndMakeVisible(upperBandwidthSelection); - - lowerBandwidthSelection = new Label("LowerBandwidth",lastLowCutString); - lowerBandwidthSelection->setEditable(true,false,false); + lowerBandwidthSelection = new Label("LowerBandwidth", round10(board->getLowerBandwidth())); + lowerBandwidthSelection->setTooltip("Lower bandwidth limit (Hz)"); + lowerBandwidthSelection->setEditable(true, false, false); lowerBandwidthSelection->addListener(this); - lowerBandwidthSelection->setBounds(25,10,60,20); + lowerBandwidthSelection->setBounds(30, 10, 60, 20); lowerBandwidthSelection->setColour(Label::textColourId, Colours::darkgrey); addAndMakeVisible(lowerBandwidthSelection); - - - } BandwidthInterface::~BandwidthInterface() @@ -1014,101 +1025,100 @@ BandwidthInterface::~BandwidthInterface() void BandwidthInterface::labelTextChanged(Label* label) { - if (!(editor->acquisitionIsActive) && board->foundInputSource()) { if (label == upperBandwidthSelection) { - Value val = label->getTextValue(); double requestedValue = double(val.getValue()); - if (requestedValue < 100.0 || requestedValue > 20000.0 || requestedValue < lastLowCutString.getFloatValue()) + if (requestedValue < 100.0 || requestedValue > 20000.0 || + requestedValue < board->getLowerBandwidth()) { CoreServices::sendStatusMessage("Value out of range."); - - label->setText(lastHighCutString, dontSendNotification); - + label->setText(round10(board->getUpperBandwidth()), dontSendNotification); return; } - actualUpperBandwidth = board->setUpperBandwidth(requestedValue); - - std::cout << "Setting Upper Bandwidth to " << requestedValue << "\n"; - std::cout << "Actual Upper Bandwidth: " << actualUpperBandwidth << "\n"; - label->setText(String(round(actualUpperBandwidth*10.f)/10.f), dontSendNotification); - + board->setUpperBandwidth(requestedValue); + std::cout << "Setting Upper Bandwidth to: " << requestedValue << endl; + std::cout << "Actual Upper Bandwidth: " << board->getUpperBandwidth() << endl; + label->setText(round10(board->getUpperBandwidth()), dontSendNotification); } else { - Value val = label->getTextValue(); double requestedValue = double(val.getValue()); - if (requestedValue < 0.1 || requestedValue > 500.0 || requestedValue > lastHighCutString.getFloatValue()) + if (requestedValue < 0.1 || requestedValue > 500.0 || + requestedValue > board->getUpperBandwidth()) { - CoreServices::sendStatusMessage("Value out of range."); - - label->setText(lastLowCutString, dontSendNotification); - + CoreServices::sendStatusMessage("Value out of range."); + label->setText(round10(board->getLowerBandwidth()), dontSendNotification); return; } - actualLowerBandwidth = board->setLowerBandwidth(requestedValue); - - std::cout << "Setting Lower Bandwidth to " << requestedValue << "\n"; - std::cout << "Actual Lower Bandwidth: " << actualLowerBandwidth << "\n"; - - label->setText(String(round(actualLowerBandwidth*10.f)/10.f), dontSendNotification); + board->setLowerBandwidth(requestedValue); + std::cout << "Setting Lower Bandwidth to: " << requestedValue << endl; + std::cout << "Actual Lower Bandwidth: " << board->getLowerBandwidth() << endl; + label->setText(round10(board->getLowerBandwidth()), dontSendNotification); } } else if (editor->acquisitionIsActive) { - CoreServices::sendStatusMessage("Can't change bandwidth while acquisition is active!"); + CoreServices::sendStatusMessage("Can't change bandwidth while acquisition is active!"); if (label == upperBandwidthSelection) - label->setText(lastHighCutString, dontSendNotification); + label->setText(round10(board->getUpperBandwidth()), dontSendNotification); else - label->setText(lastLowCutString, dontSendNotification); + label->setText(round10(board->getLowerBandwidth()), dontSendNotification); return; } - } void BandwidthInterface::setLowerBandwidth(double value) { - actualLowerBandwidth = board->setLowerBandwidth(value); - lowerBandwidthSelection->setText(String(round(actualLowerBandwidth*10.f)/10.f), dontSendNotification); + board->setLowerBandwidth(value); + lowerBandwidthSelection->setText(round10(board->getLowerBandwidth()), dontSendNotification); } void BandwidthInterface::setUpperBandwidth(double value) { - actualUpperBandwidth = board->setUpperBandwidth(value); - upperBandwidthSelection->setText(String(round(actualUpperBandwidth*10.f)/10.f), dontSendNotification); + board->setUpperBandwidth(value); + upperBandwidthSelection->setText(round10(board->getUpperBandwidth()), dontSendNotification); } double BandwidthInterface::getLowerBandwidth() { - return actualLowerBandwidth; + return board->getLowerBandwidth(); +} + +double BandwidthInterface::getLowerBandwidthRequested() +{ + return board->getDesiredLowerBandwidth(); } double BandwidthInterface::getUpperBandwidth() { - return actualUpperBandwidth; + return board->getUpperBandwidth(); } +double BandwidthInterface::getUpperBandwidthRequested() +{ + return board->getDesiredUpperBandwidth(); +} void BandwidthInterface::paint(Graphics& g) { g.setColour(Colours::darkgrey); - g.setFont(Font("Small Text",10,Font::plain)); + g.setFont(Font("Small Text", 10, Font::plain)); g.drawText(name, 0, 0, 200, 15, Justification::left, false); - g.drawText("Low: ", 0, 10, 200, 20, Justification::left, false); + g.drawText("Low:", 0, 11, 200, 20, Justification::left, false); - g.drawText("High: ", 0, 30, 200, 20, Justification::left, false); + g.drawText("High:", 0, 26, 200, 20, Justification::left, false); } @@ -1144,11 +1154,8 @@ SampleRateInterface::SampleRateInterface(RHD2000Thread* board_, rateSelection->addItemList(sampleRateOptions, 1); rateSelection->setSelectedId(17, dontSendNotification); rateSelection->addListener(this); - - rateSelection->setBounds(0,15,300,20); + rateSelection->setBounds(0, 12, 80, 20); addAndMakeVisible(rateSelection); - - } SampleRateInterface::~SampleRateInterface() @@ -1166,7 +1173,7 @@ void SampleRateInterface::comboBoxChanged(ComboBox* cb) std::cout << "Setting sample rate to index " << cb->getSelectedId()-1 << "\n"; - CoreServices::updateSignalChain(editor); + CoreServices::updateSignalChain(editor); } } } @@ -1181,15 +1188,20 @@ void SampleRateInterface::setSelectedId(int id) rateSelection->setSelectedId(id); } +String SampleRateInterface::getText() +{ + return rateSelection->getText(); +} + void SampleRateInterface::paint(Graphics& g) { g.setColour(Colours::darkgrey); - g.setFont(Font("Small Text",10,Font::plain)); + g.setFont(Font("Small Text", 10, Font::plain)); - g.drawText(name, 0, 0, 200, 15, Justification::left, false); + g.drawText(name, 0, 0, 80, 15, Justification::left, false); } @@ -1323,7 +1335,7 @@ void HeadstageOptionsInterface::buttonClicked(Button* button) editor->updateSettings(); } - CoreServices::updateSignalChain(editor); + CoreServices::updateSignalChain(editor); } } @@ -1340,7 +1352,7 @@ void HeadstageOptionsInterface::paint(Graphics& g) else g.setColour(Colours::grey); - g.setFont(Font("Small Text",15,Font::plain)); + g.setFont(Font("Small Text", 15, Font::plain)); g.drawText(name, 8, 2, 200, 15, Justification::left, false); @@ -1354,20 +1366,17 @@ AudioInterface::AudioInterface(RHD2000Thread* board_, board(board_), editor(editor_) { - name = "Noise Slicer"; + name = "Noise"; - lastNoiseSlicerString = "0"; + board->setNoiseSlicerLevel(board->getNoiseSlicerLevel()); // init board to default value - actualNoiseSlicerLevel = 0.0f; - - noiseSlicerLevelSelection = new Label("Noise Slicer",lastNoiseSlicerString); // this is currently set in RHD2000Thread, the cleaner would be to set it here again - noiseSlicerLevelSelection->setEditable(true,false,false); + noiseSlicerLevelSelection = new Label("Noise Slicer", String(board->getNoiseSlicerLevel())); + noiseSlicerLevelSelection->setTooltip("Audio noise slicer level (0-127) (* 16 uV)"); + noiseSlicerLevelSelection->setEditable(true, false, false); noiseSlicerLevelSelection->addListener(this); - noiseSlicerLevelSelection->setBounds(30,10,30,20); + noiseSlicerLevelSelection->setBounds(45, 6, 40, 20); noiseSlicerLevelSelection->setColour(Label::textColourId, Colours::darkgrey); addAndMakeVisible(noiseSlicerLevelSelection); - - } AudioInterface::~AudioInterface() @@ -1375,86 +1384,75 @@ AudioInterface::~AudioInterface() } - void AudioInterface::labelTextChanged(Label* label) { if (board->foundInputSource()) { if (label == noiseSlicerLevelSelection) { - Value val = label->getTextValue(); - int requestedValue = int(val.getValue()); // Note that it might be nice to translate to actual uV levels (16*value) + int requestedValue = roundToInt(val.getValue()); if (requestedValue < 0 || requestedValue > 127) { - CoreServices::sendStatusMessage("Value out of range."); - - label->setText(lastNoiseSlicerString, dontSendNotification); - + CoreServices::sendStatusMessage("Value must be between 1 and 127."); + label->setText(String(board->getNoiseSlicerLevel()), dontSendNotification); return; } - actualNoiseSlicerLevel = board->setNoiseSlicerLevel(requestedValue); - - std::cout << "Setting Noise Slicer Level to " << requestedValue << "\n"; - label->setText(String((roundFloatToInt)(actualNoiseSlicerLevel)), dontSendNotification); - - } - } - else - { - Value val = label->getTextValue(); - int requestedValue = int(val.getValue()); // Note that it might be nice to translate to actual uV levels (16*value) - if (requestedValue < 0 || requestedValue > 127) - { - CoreServices::sendStatusMessage("Value out of range."); - label->setText(lastNoiseSlicerString, dontSendNotification); - return; + board->setNoiseSlicerLevel(requestedValue); + std::cout << "Setting Noise Slicer Level to: " << board->getNoiseSlicerLevel() + << " (" << 16*(board->getNoiseSlicerLevel()) << " uV)" << endl; + label->setText(String((board->getNoiseSlicerLevel())), dontSendNotification); } } } void AudioInterface::setNoiseSlicerLevel(int value) { - actualNoiseSlicerLevel = board->setNoiseSlicerLevel(value); - noiseSlicerLevelSelection->setText(String(roundFloatToInt(actualNoiseSlicerLevel)), dontSendNotification); + board->setNoiseSlicerLevel(value); + noiseSlicerLevelSelection->setText(String(roundToInt(board->getNoiseSlicerLevel())), + dontSendNotification); } int AudioInterface::getNoiseSlicerLevel() { - return actualNoiseSlicerLevel; + return board->getNoiseSlicerLevel(); } - void AudioInterface::paint(Graphics& g) { - g.setColour(Colours::darkgrey); - g.setFont(Font("Small Text",9,Font::plain)); + g.setFont(Font("Small Text", 10, Font::plain)); g.drawText(name, 0, 0, 200, 15, Justification::left, false); - g.drawText("Level: ", 0, 10, 200, 20, Justification::left, false); + g.drawText("Slicer:", 0, 10, 200, 15, Justification::left, false); } // Clock Divider options ClockDivideInterface::ClockDivideInterface(RHD2000Thread* board_, RHD2000Editor* editor_) : - name("Clock Divider") - , lastDivideRatioString("1") - , board(board_) - , editor(editor_) - , actualDivideRatio(1) - -{ - divideRatioSelection = new Label("Clock Divide", lastDivideRatioString); - divideRatioSelection->setEditable(true,false,false); + board(board_), editor(editor_) +{ + + name = "Clock"; + + board->setClockDivideRatio(board->getClockDivideRatio()); // init board to default value + + divideRatioSelection = new Label("Clock Divider", String(board->getClockDivideRatio())); + divideRatioSelection->setTooltip("Clock divider (1-65534)"); + divideRatioSelection->setEditable(true, false, false); divideRatioSelection->addListener(this); - divideRatioSelection->setBounds(30,10,30,20); + divideRatioSelection->setBounds(45, 6, 40, 20); divideRatioSelection->setColour(Label::textColourId, Colours::darkgrey); addAndMakeVisible(divideRatioSelection); } +ClockDivideInterface::~ClockDivideInterface() +{ + +} + void ClockDivideInterface::labelTextChanged(Label* label) { if (board->foundInputSource()) @@ -1462,37 +1460,40 @@ void ClockDivideInterface::labelTextChanged(Label* label) if (label == divideRatioSelection) { Value val = label->getTextValue(); - int requestedValue = int(val.getValue()); + int requestedValue = roundToInt(val.getValue()); if (requestedValue < 1 || requestedValue > 65534) { - CoreServices::sendStatusMessage("Value must be between 1 and 65534."); - label->setText(lastDivideRatioString, dontSendNotification); + CoreServices::sendStatusMessage("Value must be between 1 and 65534."); + label->setText(String(board->getClockDivideRatio()), dontSendNotification); return; } - actualDivideRatio = board->setClockDivider(requestedValue); - lastDivideRatioString = String(actualDivideRatio); - - std::cout << "Setting clock divide ratio to " << actualDivideRatio << "\n"; - label->setText(lastDivideRatioString, dontSendNotification); + board->setClockDivideRatio(requestedValue); + std::cout << "Setting clock divide ratio to: " << board->getClockDivideRatio() << endl; + label->setText(String(board->getClockDivideRatio()), dontSendNotification); } } } +int ClockDivideInterface::getClockDivideRatio() const +{ + return board->getClockDivideRatio(); +} + void ClockDivideInterface::setClockDivideRatio(int value) { - actualDivideRatio = board->setClockDivider(value); - divideRatioSelection->setText(String(actualDivideRatio), dontSendNotification); + board->setClockDivideRatio(value); + divideRatioSelection->setText(String(board->getClockDivideRatio()), dontSendNotification); } void ClockDivideInterface::paint(Graphics& g) { g.setColour(Colours::darkgrey); - g.setFont(Font("Small Text",9,Font::plain)); + g.setFont(Font("Small Text", 10, Font::plain)); g.drawText(name, 0, 0, 200, 15, Justification::left, false); - g.drawText("Ratio: ", 0, 10, 200, 20, Justification::left, false); + g.drawText("Divider: ", 0, 10, 200, 15, Justification::left, false); } // DSP Options -------------------------------------------------------------------- @@ -1503,14 +1504,15 @@ DSPInterface::DSPInterface(RHD2000Thread* board_, { name = "DSP"; - dspOffsetSelection = new Label("DspOffsetSelection",String(round(board->getDspCutoffFreq()*10.f)/10.f)); - dspOffsetSelection->setEditable(true,false,false); + board->setDspCutoffFreq(board->getDesiredDspCutoffFreq()); // init board to default value + + dspOffsetSelection = new Label("DspOffsetSelection", round10(board->getDspCutoffFreq())); + dspOffsetSelection->setTooltip("DSP offset cutoff (Hz)"); + dspOffsetSelection->setEditable(true, false, false); dspOffsetSelection->addListener(this); - dspOffsetSelection->setBounds(0,0,30,20); + dspOffsetSelection->setBounds(0, 0, 50, 20); dspOffsetSelection->setColour(Label::textColourId, Colours::darkgrey); - addAndMakeVisible(dspOffsetSelection); - } DSPInterface::~DSPInterface() @@ -1518,50 +1520,40 @@ DSPInterface::~DSPInterface() } - void DSPInterface::labelTextChanged(Label* label) { - if (!(editor->acquisitionIsActive) && board->foundInputSource()) { if (label == dspOffsetSelection) { - Value val = label->getTextValue(); double requestedValue = double(val.getValue()); - - actualDspCutoffFreq = board->setDspCutoffFreq(requestedValue); - - std::cout << "Setting DSP Cutoff Freq to " << requestedValue << "\n"; - std::cout << "Actual DSP Cutoff Freq: " << actualDspCutoffFreq << "\n"; - label->setText(String(round(actualDspCutoffFreq*10.f)/10.f), dontSendNotification); - + board->setDspCutoffFreq(requestedValue); + std::cout << "Setting DSP Cutoff Freq to: " << requestedValue << endl; + std::cout << "Actual DSP Cutoff Freq: " << board->getDspCutoffFreq() << endl; + label->setText(round10(board->getDspCutoffFreq()), dontSendNotification); } } else if (editor->acquisitionIsActive) { - CoreServices::sendStatusMessage("Can't change DSP cutoff while acquisition is active!"); + CoreServices::sendStatusMessage("Can't change DSP cutoff while acquisition is active!"); + label->setText(round10(board->getDspCutoffFreq()), dontSendNotification); } - } void DSPInterface::setDspCutoffFreq(double value) { - actualDspCutoffFreq = board->setDspCutoffFreq(value); - dspOffsetSelection->setText(String(round(actualDspCutoffFreq*10.f)/10.f), dontSendNotification); + board->setDspCutoffFreq(value); + dspOffsetSelection->setText(round10(board->getDspCutoffFreq()), dontSendNotification); } - double DSPInterface::getDspCutoffFreq() { - return actualDspCutoffFreq; + return board->getDspCutoffFreq(); } void DSPInterface::paint(Graphics& g) { - g.setColour(Colours::darkgrey); - - g.setFont(Font("Small Text",10,Font::plain)); - + g.setFont(Font("Small Text", 10, Font::plain)); } diff --git a/Source/Processors/DataThreads/RhythmNode/RHD2000Editor.h b/Source/Processors/DataThreads/RhythmNode/RHD2000Editor.h index f88f6611e7..d2fe98c216 100644 --- a/Source/Processors/DataThreads/RhythmNode/RHD2000Editor.h +++ b/Source/Processors/DataThreads/RhythmNode/RHD2000Editor.h @@ -117,7 +117,7 @@ class FPGAchannelComponent : public Component, Button::Listener, public ComboBox void resized(); - const DataChannel::DataChannelTypes type; + const DataChannel::DataChannelTypes type; private: Array gains; FPGAchannelList* channelList; @@ -188,7 +188,7 @@ class RHD2000Editor : public VisualizerEditor, public ComboBox::Listener, public bool getSaveImpedance(); bool getAutoMeasureImpedance(); - void handleAsyncUpdate(); + void handleAsyncUpdate(); private: @@ -203,8 +203,9 @@ class RHD2000Editor : public VisualizerEditor, public ComboBox::Listener, public ScopedPointer clockInterface; ScopedPointer rescanButton,dacTTLButton; + ScopedPointer auxButton; ScopedPointer adcButton; - ScopedPointer ledButton; + ScopedPointer ledButton; ScopedPointer dspoffsetButton; ScopedPointer ttlSettleCombo,dacHPFcombo; @@ -217,7 +218,7 @@ class RHD2000Editor : public VisualizerEditor, public ComboBox::Listener, public RHD2000Thread* board; FPGAcanvas* canvas; - ScopedPointer impedanceData; + ScopedPointer impedanceData; JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR(RHD2000Editor); @@ -268,6 +269,8 @@ class BandwidthInterface : public Component, void setUpperBandwidth(double value); double getLowerBandwidth(); double getUpperBandwidth(); + double getLowerBandwidthRequested(); + double getUpperBandwidthRequested(); private: @@ -281,9 +284,6 @@ class BandwidthInterface : public Component, ScopedPointer