From 972f0a7228405e42c6d77a90efe4daf628f1812b Mon Sep 17 00:00:00 2001 From: Anjal Doshi Date: Fri, 15 Aug 2025 13:42:38 -0700 Subject: [PATCH 01/26] Ensure start/stop recording is triggered for editors on state changes --- Source/Processors/ProcessorGraph/ProcessorGraph.cpp | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/Source/Processors/ProcessorGraph/ProcessorGraph.cpp b/Source/Processors/ProcessorGraph/ProcessorGraph.cpp index 39b55310f..230d5bbc8 100644 --- a/Source/Processors/ProcessorGraph/ProcessorGraph.cpp +++ b/Source/Processors/ProcessorGraph/ProcessorGraph.cpp @@ -1824,6 +1824,14 @@ void ProcessorGraph::setRecordState (bool isRecording) p->startRecording(); else p->stopRecording(); + + if (auto editor = p->getEditor()) + { + if (isRecording) + editor->startRecording(); + else + editor->stopRecording(); + } } } } From dd6d72e744fb82955a6c464b20a8c6e8b5e14f8f Mon Sep 17 00:00:00 2001 From: MANUEL lab <65401298+MarinManuel@users.noreply.github.com> Date: Fri, 22 Aug 2025 00:47:19 -0400 Subject: [PATCH 02/26] Show the channel number in the tooltip, even if it is not shown on the left --- Plugins/LfpViewer/LfpChannelDisplayInfo.cpp | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/Plugins/LfpViewer/LfpChannelDisplayInfo.cpp b/Plugins/LfpViewer/LfpChannelDisplayInfo.cpp index e09617469..4ef015e5c 100644 --- a/Plugins/LfpViewer/LfpChannelDisplayInfo.cpp +++ b/Plugins/LfpViewer/LfpChannelDisplayInfo.cpp @@ -325,8 +325,7 @@ bool LfpChannelDisplayInfo::isChannelNumberHidden() String LfpChannelDisplayInfo::getTooltip() { const bool showChannelNumbers = options->getChannelNameState(); - const String channelString = (isChannelNumberHidden() ? ("--") : showChannelNumbers ? String (getChannelNumber() + 1) - : getName()); + const String channelString = showChannelNumbers ? String (getChannelNumber() + 1) : getName(); return channelString; -} \ No newline at end of file +} From dcfe5b1b40447a23cff37b105f9c0a6f06963cfb Mon Sep 17 00:00:00 2001 From: Pavel Kulik Date: Wed, 3 Sep 2025 10:16:08 -0700 Subject: [PATCH 03/26] Add unit and integration tests on PR --- .github/workflows/linux.yml | 10 -- .github/workflows/osx.yml | 10 -- .github/workflows/tests.yml | 129 ++++++++++++++++++ .github/workflows/windows.yml | 10 -- .../LfpViewer/Tests/LfpDisplayNodeTests.cpp | 8 +- Resources/Scripts/gha_unit_tests.patch | 15 ++ Resources/Scripts/run_unit_tests_linux.sh | 20 +++ Tests/Processors/DataThreadTests.cpp | 4 +- Tests/Processors/RecordNodeTests.cpp | 28 ++-- Tests/Processors/SourceNodeTests.cpp | 4 +- Tests/TestHelpers/include/TestFixtures.h | 8 +- 11 files changed, 190 insertions(+), 56 deletions(-) create mode 100644 .github/workflows/tests.yml create mode 100644 Resources/Scripts/gha_unit_tests.patch create mode 100644 Resources/Scripts/run_unit_tests_linux.sh diff --git a/.github/workflows/linux.yml b/.github/workflows/linux.yml index 05f7bc8f7..91e522106 100644 --- a/.github/workflows/linux.yml +++ b/.github/workflows/linux.yml @@ -11,16 +11,6 @@ on: - 'Source/**' - 'CMakeLists.txt' - 'HelperFunctions.cmake' - pull_request: - paths: - - '.github/workflows/**' - - 'JuceLibraryCode/**' - - 'PluginGenerator/**' - - 'Plugins/**' - - 'Resources/**' - - 'Source/**' - - 'CMakeLists.txt' - - 'HelperFunctions.cmake' jobs: build-ubuntu: diff --git a/.github/workflows/osx.yml b/.github/workflows/osx.yml index 0775dc65f..043a9fece 100644 --- a/.github/workflows/osx.yml +++ b/.github/workflows/osx.yml @@ -11,16 +11,6 @@ on: - 'Source/**' - 'CMakeLists.txt' - 'HelperFunctions.cmake' - pull_request: - paths: - - '.github/workflows/**' - - 'JuceLibraryCode/**' - - 'PluginGenerator/**' - - 'Plugins/**' - - 'Resources/**' - - 'Source/**' - - 'CMakeLists.txt' - - 'HelperFunctions.cmake' jobs: build-osx: diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml new file mode 100644 index 000000000..a3f981806 --- /dev/null +++ b/.github/workflows/tests.yml @@ -0,0 +1,129 @@ +name: Tests + +on: + pull_request: + paths: + - 'JuceLibraryCode/**' + - 'Plugins/**' + - 'Resources/**' + - 'Source/**' + - 'CMakeLists.txt' + - 'HelperFunctions.cmake' + branches: + - 'development' + - 'testing' + +jobs: + unit-tests: + name: Unit Tests + runs-on: ubuntu-22.04 + + steps: + - uses: actions/checkout@v4 + - name: build + env: + CC: gcc-10 + CXX: g++-10 + run: | + sudo apt update + sudo ./Resources/Scripts/install_linux_dependencies.sh + git apply Resources/Scripts/gha_unit_tests.patch + cd Build && cmake -G "Unix Makefiles" -DCMAKE_BUILD_TYPE=Release -DBUILD_TESTS=ON .. + make -j8 + - name: run tests + run: | + chmod +x ./Resources/Scripts/run_unit_tests_linux.sh + ./Resources/Scripts/run_unit_tests_linux.sh Build/TestBin + shell: bash + + integration-tests: + name: Integration Tests + runs-on: windows-2022 + + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + - name: Start Windows Audio Engine + run: net start audiosrv + - name: Install Scream + shell: powershell + run: | + Start-Service audio* + Invoke-WebRequest https://github.com/duncanthrax/scream/releases/download/3.6/Scream3.6.zip -OutFile C:\Scream3.6.zip + Expand-7ZipArchive -Path C:\Scream3.6.zip -DestinationPath C:\Scream + $cert = (Get-AuthenticodeSignature C:\Scream\Install\driver\Scream.sys).SignerCertificate + $store = [System.Security.Cryptography.X509Certificates.X509Store]::new("TrustedPublisher", "LocalMachine") + $store.Open("ReadWrite") + $store.Add($cert) + $store.Close() + cd C:\Scream\Install\driver + C:\Scream\Install\helpers\devcon install Scream.inf *Scream + - name: Show audio device + run: Get-CimInstance Win32_SoundDevice | fl * + - name: configure + run: | + cd Build + cmake -G "Visual Studio 17 2022" -A x64 .. + - name: Add msbuild to PATH + uses: microsoft/setup-msbuild@v1.0.2 + - name: build + run: | + cd Build + msbuild ALL_BUILD.vcxproj -p:Configuration=Release -p:Platform=x64 -m + - name: Install open-ephys-data-format + shell: powershell + run: | + New-Item -Path '..\OEPlugins' -ItemType Directory + git clone --branch main https://github.com/open-ephys-plugins/open-ephys-data-format.git ..\OEPlugins\open-ephys-data-format + cd ..\OEPlugins\open-ephys-data-format\Build + cmake -G "Visual Studio 17 2022" -A x64 .. + msbuild INSTALL.vcxproj -p:Configuration=Release -p:Platform=x64 + - name: Install OpenEphysHDF5Lib + shell: powershell + run: | + git clone --branch main https://github.com/open-ephys-plugins/OpenEphysHDF5Lib.git ..\OEPlugins\OpenEphysHDF5Lib + cd ..\OEPlugins\OpenEphysHDF5Lib\Build + cmake -G "Visual Studio 17 2022" -A x64 .. + msbuild INSTALL.vcxproj -p:Configuration=Release -p:Platform=x64 + - name: Install nwb-format + shell: powershell + run: | + git clone --branch main https://github.com/open-ephys-plugins/nwb-format.git ..\OEPlugins\nwb-format + cd ..\OEPlugins\nwb-format\Build + cmake -G "Visual Studio 17 2022" -A x64 .. + msbuild INSTALL.vcxproj -p:Configuration=Release -p:Platform=x64 + - name: Install test-suite + shell: powershell + run: | + git clone --branch juce8 https://github.com/open-ephys/open-ephys-python-tools.git C:\open-ephys-python-tools + cd C:\open-ephys-python-tools + pip install -e . + pip install psutil + - name: Run Tests + shell: powershell + run: | + New-Item -Path 'C:\open-ephys\data' -ItemType Directory + git clone --branch main https://github.com/open-ephys/open-ephys-test-suite.git C:\test-suite + cd C:\test-suite + $process = Start-Process -FilePath "Build\Release\open-ephys.exe" -ArgumentList "Build\Release\configs\file_reader_config.xml" -NoNewWindow -PassThru + Write-Host "Started open-ephys process with ID: $($process.Id)" + Start-Sleep -Seconds 10 + Write-Host "Starting Python script..." + python run_all.py 2>&1 | Tee-Object -FilePath "python_output.log" + Write-Host "Python script completed. Output saved to python_output.log" + Stop-Process -Id $process.Id -Force + env: + OE_WINDOWS_GITHUB_RECORD_PATH: C:\open-ephys\data + - name: Set timestamp + shell: powershell + id: timestamp + run: | + $timestamp = Get-Date -Format 'yyyy_MM_dd_HH_mm_ss' + "timestamp=$timestamp" >> $env:GITHUB_OUTPUT + - name: Upload test results + uses: actions/upload-artifact@v4 + with: + name: windows_${{ steps.timestamp.outputs.timestamp }}.log + path: python_output.log + retention-days: 7 \ No newline at end of file diff --git a/.github/workflows/windows.yml b/.github/workflows/windows.yml index 375f9b6b6..c1d711844 100644 --- a/.github/workflows/windows.yml +++ b/.github/workflows/windows.yml @@ -11,16 +11,6 @@ on: - 'Source/**' - 'CMakeLists.txt' - 'HelperFunctions.cmake' - pull_request: - paths: - - '.github/workflows/**' - - 'JuceLibraryCode/**' - - 'PluginGenerator/**' - - 'Plugins/**' - - 'Resources/**' - - 'Source/**' - - 'CMakeLists.txt' - - 'HelperFunctions.cmake' jobs: build-windows: diff --git a/Plugins/LfpViewer/Tests/LfpDisplayNodeTests.cpp b/Plugins/LfpViewer/Tests/LfpDisplayNodeTests.cpp index e83446778..ca1592e4e 100644 --- a/Plugins/LfpViewer/Tests/LfpDisplayNodeTests.cpp +++ b/Plugins/LfpViewer/Tests/LfpDisplayNodeTests.cpp @@ -319,7 +319,7 @@ TEST_F (LfpDisplayNodeTests, VisualIntegrityTest) Rectangle canvasSnapshot (x, y, width, height); ExpectedImage expected (numChannels, sampleRate * 2); //2 seconds to match canvas timebase - tester->startAcquisition (false); + processor->startAcquisition (); canvas->beginAnimation(); //Add 5 10Hz waves with +-125uV amplitude @@ -367,16 +367,16 @@ TEST_F (LfpDisplayNodeTests, VisualIntegrityTest) missCount = getImageDifferencePixelCount (expectedImage, canvasImage); EXPECT_LE (float (missCount) / float (width * height), errorThreshold); - tester->stopAcquisition(); + processor->stopAcquisition(); } TEST_F (LfpDisplayNodeTests, DataIntegrityTest) { int numSamples = 100; - tester->startAcquisition (false); + processor->startAcquisition (); auto inputBuffer = createBuffer (1000.0, 20.0, numChannels, numSamples); writeBlock (inputBuffer); - tester->stopAcquisition(); + processor->stopAcquisition(); } diff --git a/Resources/Scripts/gha_unit_tests.patch b/Resources/Scripts/gha_unit_tests.patch new file mode 100644 index 000000000..6b3c7c073 --- /dev/null +++ b/Resources/Scripts/gha_unit_tests.patch @@ -0,0 +1,15 @@ +diff --git a/Tests/Processors/CMakeLists.txt b/Tests/Processors/CMakeLists.txt +index a89fa4da4..ab53e8d89 100644 +--- a/Tests/Processors/CMakeLists.txt ++++ b/Tests/Processors/CMakeLists.txt +@@ -5,8 +5,8 @@ add_sources(${COMPONENT_NAME}_tests + DataBufferTests.cpp + PluginManagerTests.cpp + SourceNodeTests.cpp +- RecordNodeTests.cpp +- ProcessorGraphTests.cpp ++ #RecordNodeTests.cpp ++ #ProcessorGraphTests.cpp + EventTests.cpp + DataThreadTests.cpp + GenericProcessorTests.cpp diff --git a/Resources/Scripts/run_unit_tests_linux.sh b/Resources/Scripts/run_unit_tests_linux.sh new file mode 100644 index 000000000..087fd6900 --- /dev/null +++ b/Resources/Scripts/run_unit_tests_linux.sh @@ -0,0 +1,20 @@ +#!/bin/bash + +# Use first argument as TEST_DIR if provided, otherwise use default +TEST_DIR="${1:-../../Build/TestBin}" + +# Track overall exit code +EXIT_CODE=0 + +# Find all executable files that are not .so files +for test_exec in $(find "$TEST_DIR" -type f -executable ! -name "*.so"); do + echo "Running test: $test_exec" + "$test_exec" + TEST_RESULT=$? + if [ $TEST_RESULT -ne 0 ]; then + EXIT_CODE=1 + fi + echo "----------------------------------------" +done + +exit $EXIT_CODE \ No newline at end of file diff --git a/Tests/Processors/DataThreadTests.cpp b/Tests/Processors/DataThreadTests.cpp index c98c03c84..fde4406a6 100644 --- a/Tests/Processors/DataThreadTests.cpp +++ b/Tests/Processors/DataThreadTests.cpp @@ -115,11 +115,11 @@ class DataThreadTests : public testing::Test TEST_F(DataThreadTests, DataIntegrity) { - tester->startAcquisition(false); + processor->startAcquisition(); int numSamples = 100; auto inputBuffer = createBuffer(1000.0, 20.0, 5, numSamples); writeBlock(inputBuffer); - tester->stopAcquisition(); + processor->stopAcquisition(); } \ No newline at end of file diff --git a/Tests/Processors/RecordNodeTests.cpp b/Tests/Processors/RecordNodeTests.cpp index 786f9a7a5..9bf8d1a4d 100644 --- a/Tests/Processors/RecordNodeTests.cpp +++ b/Tests/Processors/RecordNodeTests.cpp @@ -220,14 +220,14 @@ class RecordNodeTests : public testing::Test { TEST_F(RecordNodeTests, TestInputOutput_Continuous_Single) { int numSamples = 100; - tester->startAcquisition(true); + processor->startAcquisition(); auto inputBuffer = createBuffer(1000.0, 20.0, numChannels, numSamples); writeBlock(inputBuffer); // The record node always flushes its pending writes when stopping acquisition, so we don't need to sleep before // stopping. - tester->stopAcquisition(); + processor->stopAcquisition(); std::vector persistedData; loadContinuousDatFile(&persistedData); @@ -245,7 +245,7 @@ TEST_F(RecordNodeTests, TestInputOutput_Continuous_Single) { } TEST_F(RecordNodeTests, TestInputOutput_Continuous_Multiple) { - tester->startAcquisition(true); + processor->startAcquisition(); int numSamplesPerBlock = 100; int numBlocks = 8; @@ -256,7 +256,7 @@ TEST_F(RecordNodeTests, TestInputOutput_Continuous_Multiple) { inputBuffers.push_back(inputBuffer); } - tester->stopAcquisition(); + processor->stopAcquisition(); std::vector persistedData; loadContinuousDatFile(&persistedData); @@ -277,8 +277,8 @@ TEST_F(RecordNodeTests, TestInputOutput_Continuous_Multiple) { } TEST_F(RecordNodeTests, TestEmpty) { - tester->startAcquisition(true); - tester->stopAcquisition(); + processor->startAcquisition(); + processor->stopAcquisition(); std::vector persistedData; loadContinuousDatFile(&persistedData); @@ -287,7 +287,7 @@ TEST_F(RecordNodeTests, TestEmpty) { TEST_F(RecordNodeTests, TestClipsProperly) { int numSamples = 100; - tester->startAcquisition(true); + processor->startAcquisition(); // The min value is actually -32767, not -32768 like the "true" min std::vector> inputBuffers; @@ -301,7 +301,7 @@ TEST_F(RecordNodeTests, TestClipsProperly) { writeBlock(inputBuffer); inputBuffers.push_back(inputBuffer); - tester->stopAcquisition(); + processor->stopAcquisition(); std::vector persistedData; loadContinuousDatFile(&persistedData); @@ -341,10 +341,10 @@ class CustomBitVolts_RecordNodeTests : public RecordNodeTests { TEST_F(CustomBitVolts_RecordNodeTests, Test_RespectsBitVolts) { int numSamples = 100; - tester->startAcquisition(true); + processor->startAcquisition(); auto inputBuffer = createBuffer(1000.0, 20.0, numChannels, numSamples); writeBlock(inputBuffer); - tester->stopAcquisition(); + processor->stopAcquisition(); std::vector persistedData; loadContinuousDatFile(&persistedData); @@ -370,7 +370,7 @@ TEST_F(CustomBitVolts_RecordNodeTests, Test_RespectsBitVolts) { } TEST_F(RecordNodeTests, Test_PersistsSampleNumbersAndTimestamps) { - tester->startAcquisition(true); + processor->startAcquisition(); int numSamples = 5; for (int i = 0; i < 3; i++) { @@ -417,7 +417,7 @@ TEST_F(RecordNodeTests, Test_PersistsSampleNumbersAndTimestamps) { } TEST_F(RecordNodeTests, Test_PersistsStructureOeBin) { - tester->startAcquisition(true); + processor->startAcquisition(); int numSamples = 5; for (int i = 0; i < 3; i++) { @@ -479,7 +479,7 @@ TEST_F(RecordNodeTests, Test_PersistsEvents) { processor->setRecordEvents(true); processor->updateSettings(); - tester->startAcquisition(true); + processor->startAcquisition(); int numSamples = 5; auto streamId = processor->getDataStreams()[0]->getStreamId(); @@ -492,7 +492,7 @@ TEST_F(RecordNodeTests, Test_PersistsEvents) { true); auto inputBuffer = createBuffer(1000.0, 20.0, numChannels, numSamples); writeBlock(inputBuffer, eventPtr.get()); - tester->stopAcquisition(); + processor->stopAcquisition(); std::filesystem::path sampleNumbersPath; ASSERT_TRUE(eventsPathFor("sample_numbers.npy", &sampleNumbersPath)); diff --git a/Tests/Processors/SourceNodeTests.cpp b/Tests/Processors/SourceNodeTests.cpp index af9a5f99c..67ea9ac57 100644 --- a/Tests/Processors/SourceNodeTests.cpp +++ b/Tests/Processors/SourceNodeTests.cpp @@ -127,11 +127,11 @@ This test verifies that given a Data Thread, the Source Node will perform this w */ TEST_F(SourceNodeTests, DataAcquisition) { - tester->startAcquisition(false); + tester->getSourceNode()->startAcquisition(); int numSamples = 100; auto inputBuffer = createBuffer(1000.0, 20.0, 5, numSamples); writeBlock(inputBuffer); - tester->stopAcquisition(); + tester->getSourceNode()->stopAcquisition(); } \ No newline at end of file diff --git a/Tests/TestHelpers/include/TestFixtures.h b/Tests/TestHelpers/include/TestFixtures.h index b244c24d6..77e1f9f81 100644 --- a/Tests/TestHelpers/include/TestFixtures.h +++ b/Tests/TestHelpers/include/TestFixtures.h @@ -79,9 +79,9 @@ class ProcessorTester LookAndFeel::setDefaultLookAndFeel (customLookAndFeel.get()); // All of these sets the global state in AccessClass in their constructors - audioComponent = std::make_unique(); + //audioComponent = std::make_unique(); processorGraph = std::make_unique (true); - controlPanel = std::make_unique (processorGraph.get(), audioComponent.get(), true); + //controlPanel = std::make_unique (processorGraph.get(), audioComponent.get(), true); SourceNode* snTemp = sourceNodeBuilder.buildSourceNode(); sourceNodeId = nextProcessorId++; @@ -97,12 +97,12 @@ class ProcessorTester sn->initialize (false); sn->setDestNode (nullptr); - controlPanel->updateRecordEngineList(); + //controlPanel->updateRecordEngineList(); // Refresh everything processorGraph->updateSettings (sn); - controlPanel->colourChanged(); + //controlPanel->colourChanged(); } virtual ~ProcessorTester() From c098d1614f3a8656c902a0c00160a51b4327e563 Mon Sep 17 00:00:00 2001 From: Pavel Kulik Date: Wed, 22 Oct 2025 12:56:12 -0700 Subject: [PATCH 04/26] Use main branch of python-tools for tests --- .github/workflows/tests.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index a3f981806..48bad8e80 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -96,7 +96,7 @@ jobs: - name: Install test-suite shell: powershell run: | - git clone --branch juce8 https://github.com/open-ephys/open-ephys-python-tools.git C:\open-ephys-python-tools + git clone --branch main https://github.com/open-ephys/open-ephys-python-tools.git C:\open-ephys-python-tools cd C:\open-ephys-python-tools pip install -e . pip install psutil From 38dfd75e3fc1e30db85b002dc8e9cbd8e0f5bba1 Mon Sep 17 00:00:00 2001 From: Anjal Doshi Date: Tue, 11 Nov 2025 17:08:39 -0800 Subject: [PATCH 05/26] Add input range handling and auto-scaling for AUX channels --- Plugins/LfpViewer/DisplayBuffer.cpp | 6 +- Plugins/LfpViewer/DisplayBuffer.h | 6 +- Plugins/LfpViewer/LfpDisplay.cpp | 86 +++++++++++++++++++-- Plugins/LfpViewer/LfpDisplay.h | 3 + Plugins/LfpViewer/LfpDisplayNode.cpp | 9 ++- Plugins/LfpViewer/LfpDisplayOptions.cpp | 73 ++++++++++++++--- Plugins/LfpViewer/LfpDisplayOptions.h | 3 + Source/Processors/FileReader/FileReader.cpp | 24 ++++++ 8 files changed, 189 insertions(+), 21 deletions(-) diff --git a/Plugins/LfpViewer/DisplayBuffer.cpp b/Plugins/LfpViewer/DisplayBuffer.cpp index 168ab5027..d0b52142f 100644 --- a/Plugins/LfpViewer/DisplayBuffer.cpp +++ b/Plugins/LfpViewer/DisplayBuffer.cpp @@ -66,7 +66,9 @@ void DisplayBuffer::addChannel ( int group, float ypos, String description, - String structure) + String structure, + float inputRangeMin, + float inputRangeMax) { ChannelMetadata metadata = ChannelMetadata(); metadata.name = name; @@ -77,6 +79,8 @@ void DisplayBuffer::addChannel ( metadata.type = type; metadata.isRecorded = isRecorded; metadata.description = description; + metadata.inputRangeMin = inputRangeMin; + metadata.inputRangeMax = inputRangeMax; channelMetadata.add (metadata); channelMap[channelNum] = numChannels; diff --git a/Plugins/LfpViewer/DisplayBuffer.h b/Plugins/LfpViewer/DisplayBuffer.h index 7464590a2..6912ce0d1 100644 --- a/Plugins/LfpViewer/DisplayBuffer.h +++ b/Plugins/LfpViewer/DisplayBuffer.h @@ -63,7 +63,9 @@ class TESTABLE DisplayBuffer : public AudioBuffer int group = 0, float ypos = 0, String description = "", - String structure = "None"); + String structure = "None", + float inputRangeMin = -5000.0f, + float inputRangeMax = +5000.0f); /** Initializes the event channel at the start of each buffer */ void initializeEventChannel (int nSamples); @@ -91,6 +93,8 @@ class TESTABLE DisplayBuffer : public AudioBuffer ContinuousChannel::Type type; bool isRecorded = false; String description = ""; + float inputRangeMin = -5000.0f; + float inputRangeMax = +5000.0f; }; Array channelMetadata; diff --git a/Plugins/LfpViewer/LfpDisplay.cpp b/Plugins/LfpViewer/LfpDisplay.cpp index 884c489ff..275f32cb4 100644 --- a/Plugins/LfpViewer/LfpDisplay.cpp +++ b/Plugins/LfpViewer/LfpDisplay.cpp @@ -162,8 +162,32 @@ ChannelColourScheme* LfpDisplay::getColourSchemePtr() void LfpDisplay::updateRange (int i) { - channels[i]->setRange (range[channels[i]->getType()]); - channelInfo[i]->setRange (range[channels[i]->getType()]); + // Check if this is an AUX channel with auto-scaling enabled + if (channels[i]->getType() == ContinuousChannel::Type::AUX && options->isAuxAutoScaleEnabled()) + { + // Apply individual auto-scaling based on this channel's InputRange + float rangeMin = canvasSplit->displayBuffer->channelMetadata[i].inputRangeMin; + float rangeMax = canvasSplit->displayBuffer->channelMetadata[i].inputRangeMax; + float autoRange = rangeMax - rangeMin; + + if (autoRange > 0.0f) + { + channels[i]->setRange (autoRange); + channelInfo[i]->setRange (autoRange); + } + else + { + // Fall back to the default range for this type + channels[i]->setRange (range[channels[i]->getType()]); + channelInfo[i]->setRange (range[channels[i]->getType()]); + } + } + else + { + // Use the standard range for the channel type + channels[i]->setRange (range[channels[i]->getType()]); + channelInfo[i]->setRange (range[channels[i]->getType()]); + } } void LfpDisplay::setNumChannels (int newChannelCount) @@ -602,7 +626,10 @@ void LfpDisplay::setRange (float r, ContinuousChannel::Type type) for (int i = 0; i < numChans; i++) { if (channels[i]->getType() == type) + { channels[i]->setRange (range[type]); + channelInfo[i]->setRange (range[type]); + } } if (displayIsPaused) @@ -615,6 +642,46 @@ void LfpDisplay::setRange (float r, ContinuousChannel::Type type) } } +void LfpDisplay::setAutoRangeForAuxChannels() +{ + if (canvasSplit->displayBuffer == nullptr || channels.size() == 0) + return; + + // Apply individual ranges to each AUX channel based on their InputRange + for (int i = 0; i < numChans; i++) + { + if (channels[i]->getType() == ContinuousChannel::Type::AUX) + { + // Get the InputRange from the channel metadata + if (i < canvasSplit->displayBuffer->channelMetadata.size()) + { + float rangeMin = canvasSplit->displayBuffer->channelMetadata[i].inputRangeMin; + float rangeMax = canvasSplit->displayBuffer->channelMetadata[i].inputRangeMax; + float autoRange = rangeMax - rangeMin; + + if (autoRange > 0.0f) + { + channels[i]->setRange (autoRange); + channelInfo[i]->setRange (autoRange); + } + else + { + // Fall back to the default range for this type + channels[i]->setRange (range[ContinuousChannel::Type::AUX]); + channelInfo[i]->setRange (range[ContinuousChannel::Type::AUX]); + } + } + } + } + + if (displayIsPaused) + { + timeOffsetChanged = true; + canRefresh = true; + refresh(); + } +} + int LfpDisplay::getRange() { return getRange (options->getSelectedType()); @@ -834,20 +901,25 @@ void LfpDisplay::mouseWheelMove (const MouseEvent& e, const MouseWheelDetails& w { if (e.mods.isAltDown()) // ALT + scroll wheel -> change channel range (was SHIFT but that clamps wheel.deltaY to 0 on OSX for some reason..) { + auto chanType = options->getSelectedType(); + + if (chanType == ContinuousChannel::AUX && options->isAuxAutoScaleEnabled()) + return; + int h = getRange(); - int step = options->getRangeStep (options->getSelectedType()); + int step = options->getRangeStep (chanType); // std::cout << wheel.deltaY << std::endl; if (wheel.deltaY > 0) { - setRange (h + step, options->getSelectedType()); + setRange (h + step, chanType); } else { if (h > step + 1) - setRange (h - step, options->getSelectedType()); + setRange (h - step, chanType); } options->setRangeSelection (h); // update combobox @@ -1174,6 +1246,7 @@ void LfpDisplay::mouseDown (const MouseEvent& event) int dist = 0; int mindist = 10000; int closest = 5; + float chanRange = getRange(); for (int n = 0; n < drawableChannels.size(); n++) // select closest instead of relying on eventComponent { @@ -1188,6 +1261,7 @@ void LfpDisplay::mouseDown (const MouseEvent& event) { mindist = dist - 1; closest = n; + chanRange = drawableChannels[n].channel->getRange(); } } @@ -1213,7 +1287,7 @@ void LfpDisplay::mouseDown (const MouseEvent& event) { drawableChannels[0].channelInfo->updateXY ( float (x) / getWidth() * canvasSplit->timebase, - (-(float (y) - viewport->getViewPositionY()) / viewport->getViewHeight() * float (getRange())) + float (getRange() / 2)); + (-(float (y) - viewport->getViewPositionY()) / viewport->getViewHeight() * float (chanRange)) + float (chanRange / 2)); } } diff --git a/Plugins/LfpViewer/LfpDisplay.h b/Plugins/LfpViewer/LfpDisplay.h index ad9331028..64b2be08f 100644 --- a/Plugins/LfpViewer/LfpDisplay.h +++ b/Plugins/LfpViewer/LfpDisplay.h @@ -92,6 +92,9 @@ class LfpDisplay : public Component, /** Sets the display range for a particular channel type*/ void setRange (float range, ContinuousChannel::Type type); + /** Applies auto-scaling to AUX channels based on their individual InputRange values */ + void setAutoRangeForAuxChannels(); + /** Returns the display range for the current channel type*/ int getRange(); diff --git a/Plugins/LfpViewer/LfpDisplayNode.cpp b/Plugins/LfpViewer/LfpDisplayNode.cpp index 8e79522f0..3e29f4ee3 100644 --- a/Plugins/LfpViewer/LfpDisplayNode.cpp +++ b/Plugins/LfpViewer/LfpDisplayNode.cpp @@ -85,14 +85,17 @@ void LfpDisplayNode::updateSettings() displayBufferMap[streamId]->sampleRate = channel->getSampleRate(); displayBufferMap[streamId]->name = name; } - // + displayBufferMap[streamId]->addChannel (channel->getName(), // name ch, // index channel->getChannelType(), // type channel->isRecorded, - 0, // group + channel->group.number, // group channel->position.y, // ypos - channel->getDescription()); + channel->getDescription(), + "None", // structure + channel->inputRange.min, // inputRangeMin + channel->inputRange.max); // inputRangeMax } Array toDelete; diff --git a/Plugins/LfpViewer/LfpDisplayOptions.cpp b/Plugins/LfpViewer/LfpDisplayOptions.cpp index b9d5cc325..ead0c3f61 100644 --- a/Plugins/LfpViewer/LfpDisplayOptions.cpp +++ b/Plugins/LfpViewer/LfpDisplayOptions.cpp @@ -154,6 +154,7 @@ LfpDisplayOptions::LfpDisplayOptions (LfpDisplayCanvas* canvas_, LfpDisplaySplit typeButtons.add (tbut); //Ranges for AUX/accelerometer data + voltageRanges[ContinuousChannel::Type::AUX].add ("Auto"); voltageRanges[ContinuousChannel::Type::AUX].add ("25"); voltageRanges[ContinuousChannel::Type::AUX].add ("50"); voltageRanges[ContinuousChannel::Type::AUX].add ("100"); @@ -163,7 +164,7 @@ LfpDisplayOptions::LfpDisplayOptions (LfpDisplayCanvas* canvas_, LfpDisplaySplit voltageRanges[ContinuousChannel::Type::AUX].add ("750"); voltageRanges[ContinuousChannel::Type::AUX].add ("1000"); voltageRanges[ContinuousChannel::Type::AUX].add ("2000"); - selectedVoltageRange[ContinuousChannel::Type::AUX] = 9; + selectedVoltageRange[ContinuousChannel::Type::AUX] = 1; // Default to Auto rangeGain[ContinuousChannel::Type::AUX] = 0.001f; //mV rangeSteps[ContinuousChannel::Type::AUX] = 10; rangeUnits.add ("mV"); @@ -549,11 +550,23 @@ LfpDisplayOptions::LfpDisplayOptions (LfpDisplayCanvas* canvas_, LfpDisplaySplit * rangeGain[ContinuousChannel::Type::ELECTRODE], ContinuousChannel::Type::ELECTRODE); lfpDisplay->setRange (voltageRanges[ContinuousChannel::Type::ADC][selectedVoltageRange[ContinuousChannel::Type::ADC] - 1].getFloatValue() - * rangeGain[ContinuousChannel::Type::AUX], + * rangeGain[ContinuousChannel::Type::ADC], ContinuousChannel::Type::ADC); - lfpDisplay->setRange (voltageRanges[ContinuousChannel::Type::AUX][selectedVoltageRange[ContinuousChannel::Type::AUX] - 1].getFloatValue() - * rangeGain[ContinuousChannel::Type::AUX], - ContinuousChannel::Type::AUX); + + // Handle Auto scaling for AUX channels during initialization + String auxRangeValue = voltageRanges[ContinuousChannel::Type::AUX][selectedVoltageRange[ContinuousChannel::Type::AUX] - 1]; + if (auxRangeValue.equalsIgnoreCase("Auto")) + { + // Set a default range value for the type (used as fallback) + lfpDisplay->setRange(2000.0f * rangeGain[ContinuousChannel::Type::AUX], ContinuousChannel::Type::AUX); + // Apply individual auto-scaling to each AUX channel based on their InputRange + lfpDisplay->setAutoRangeForAuxChannels(); + } + else + { + lfpDisplay->setRange (auxRangeValue.getFloatValue() * rangeGain[ContinuousChannel::Type::AUX], + ContinuousChannel::Type::AUX); + } } void LfpDisplayOptions::timerCallback() @@ -1198,17 +1211,39 @@ void LfpDisplayOptions::comboBoxChanged (ComboBox* cb) { if (cb->getSelectedId()) { - lfpDisplay->setRange (voltageRanges[selectedChannelType][cb->getSelectedId() - 1].getFloatValue() * rangeGain[selectedChannelType], selectedChannelType); + String selectedText = voltageRanges[selectedChannelType][cb->getSelectedId() - 1]; + + // Check if "Auto" is selected for AUX channels + if (selectedChannelType == ContinuousChannel::Type::AUX && selectedText.equalsIgnoreCase("Auto")) + { + // Set a default range value for the type (used as fallback) + lfpDisplay->setRange(2000.0f * rangeGain[selectedChannelType], selectedChannelType); + // Apply individual auto-scaling to each AUX channel based on their InputRange + lfpDisplay->setAutoRangeForAuxChannels(); + } + else + { + lfpDisplay->setRange (selectedText.getFloatValue() * rangeGain[selectedChannelType], selectedChannelType); + } } else { float vRange = cb->getText().getFloatValue(); if (vRange) { - if (vRange < voltageRanges[selectedChannelType][0].getFloatValue()) + // Check if we should skip the first item (Auto) when validating range + int firstRangeIndex = 0; + if (selectedChannelType == ContinuousChannel::Type::AUX && + voltageRanges[selectedChannelType][0].equalsIgnoreCase("Auto")) { - cb->setSelectedId (1, dontSendNotification); - vRange = voltageRanges[selectedChannelType][0].getFloatValue(); + firstRangeIndex = 1; + } + + if (firstRangeIndex < voltageRanges[selectedChannelType].size() && + vRange < voltageRanges[selectedChannelType][firstRangeIndex].getFloatValue()) + { + cb->setSelectedId (firstRangeIndex + 1, dontSendNotification); + vRange = voltageRanges[selectedChannelType][firstRangeIndex].getFloatValue(); } else if (vRange > voltageRanges[selectedChannelType][voltageRanges[selectedChannelType].size() - 1].getFloatValue()) { @@ -1396,6 +1431,11 @@ ContinuousChannel::Type LfpDisplayOptions::getSelectedType() return selectedChannelType; } +bool LfpDisplayOptions::isAuxAutoScaleEnabled() +{ + return selectedVoltageRangeValues[ContinuousChannel::Type::AUX].equalsIgnoreCase("Auto"); +} + void LfpDisplayOptions::setSelectedType (ContinuousChannel::Type type, bool toggleButton) { if (selectedChannelType == type) @@ -1547,7 +1587,20 @@ void LfpDisplayOptions::loadParameters (XmlElement* xml) selectedVoltageRange[2] = voltageRanges[2].indexOf (ranges[2]) + 1; rangeSelection->setText (ranges[selectedChannelType]); lfpDisplay->setRange (ranges[0].getFloatValue() * rangeGain[0], ContinuousChannel::Type::ELECTRODE); - lfpDisplay->setRange (ranges[1].getFloatValue() * rangeGain[1], ContinuousChannel::Type::AUX); + + // Handle Auto scaling for AUX channels + if (ranges[1].equalsIgnoreCase("Auto")) + { + // Set a default range value for the type (used as fallback) + lfpDisplay->setRange(2000.0f * rangeGain[ContinuousChannel::Type::AUX], ContinuousChannel::Type::AUX); + // Apply individual auto-scaling to each AUX channel based on their InputRange + lfpDisplay->setAutoRangeForAuxChannels(); + } + else + { + lfpDisplay->setRange (ranges[1].getFloatValue() * rangeGain[1], ContinuousChannel::Type::AUX); + } + lfpDisplay->setRange (ranges[2].getFloatValue() * rangeGain[2], ContinuousChannel::Type::ADC); // LOGD(" Set range in ", MS_FROM_START, " milliseconds"); diff --git a/Plugins/LfpViewer/LfpDisplayOptions.h b/Plugins/LfpViewer/LfpDisplayOptions.h index 7a1171a00..ef27f3b80 100644 --- a/Plugins/LfpViewer/LfpDisplayOptions.h +++ b/Plugins/LfpViewer/LfpDisplayOptions.h @@ -103,6 +103,9 @@ class LfpDisplayOptions : public Component, /** Returns the selected channel type for the range editor */ ContinuousChannel::Type getSelectedType(); + /** Returns true if AUX channels are set to auto-scale */ + bool isAuxAutoScaleEnabled(); + /** Returns the name for a given channel type (DATA, AUX, ADC) */ String getTypeName (ContinuousChannel::Type type); diff --git a/Source/Processors/FileReader/FileReader.cpp b/Source/Processors/FileReader/FileReader.cpp index 9816b1587..cb6c7a63f 100644 --- a/Source/Processors/FileReader/FileReader.cpp +++ b/Source/Processors/FileReader/FileReader.cpp @@ -542,6 +542,30 @@ void FileReader::updateSettings() }; continuousChannels.add (new ContinuousChannel (channelSettings)); + auto name = channelInfo[i].name; + + if (channelInfo[i].type == ContinuousChannel::Type::AUX) + { + if (name.equalsIgnoreCase("Eul-Y")) + continuousChannels.getLast()->inputRange = { -360.0f, 360.0f }; + else if (name.equalsIgnoreCase("Eul-R")) + continuousChannels.getLast()->inputRange = { -90.0f, 90.0f }; + else if (name.equalsIgnoreCase("Eul-P")) + continuousChannels.getLast()->inputRange = { -180.0f, 180.0f }; + else if (name.startsWithIgnoreCase("Quat")) + continuousChannels.getLast()->inputRange = { -1.0f, 1.0f }; + else if (name.startsWithIgnoreCase("Acc")) + continuousChannels.getLast()->inputRange = { -100.0f, 100.0f }; + else if (name.startsWithIgnoreCase("Grav")) + continuousChannels.getLast()->inputRange = { -10.0f, 10.0f }; + else if (name.equalsIgnoreCase("Temp")) + continuousChannels.getLast()->inputRange = { -100.0f, 100.0f }; + else if (name.startsWithIgnoreCase("Cal")) + continuousChannels.getLast()->inputRange = { -3.0f, 3.0f }; + else + continuousChannels.getLast()->inputRange = { -5000.0f, 5000.0f }; + } + continuousChannels.getLast()->addProcessor (this); } From 858628559807393f8e80b4c728b73bb8327e5d2f Mon Sep 17 00:00:00 2001 From: Anjal Doshi Date: Wed, 12 Nov 2025 11:15:22 -0800 Subject: [PATCH 06/26] Hide range units in options bar when AUX auto-scaling is enabled --- Plugins/LfpViewer/LfpDisplayOptions.cpp | 36 +++++++++++++------------ 1 file changed, 19 insertions(+), 17 deletions(-) diff --git a/Plugins/LfpViewer/LfpDisplayOptions.cpp b/Plugins/LfpViewer/LfpDisplayOptions.cpp index ead0c3f61..dc2fe01e0 100644 --- a/Plugins/LfpViewer/LfpDisplayOptions.cpp +++ b/Plugins/LfpViewer/LfpDisplayOptions.cpp @@ -552,13 +552,13 @@ LfpDisplayOptions::LfpDisplayOptions (LfpDisplayCanvas* canvas_, LfpDisplaySplit lfpDisplay->setRange (voltageRanges[ContinuousChannel::Type::ADC][selectedVoltageRange[ContinuousChannel::Type::ADC] - 1].getFloatValue() * rangeGain[ContinuousChannel::Type::ADC], ContinuousChannel::Type::ADC); - + // Handle Auto scaling for AUX channels during initialization String auxRangeValue = voltageRanges[ContinuousChannel::Type::AUX][selectedVoltageRange[ContinuousChannel::Type::AUX] - 1]; - if (auxRangeValue.equalsIgnoreCase("Auto")) + if (auxRangeValue.equalsIgnoreCase ("Auto")) { // Set a default range value for the type (used as fallback) - lfpDisplay->setRange(2000.0f * rangeGain[ContinuousChannel::Type::AUX], ContinuousChannel::Type::AUX); + lfpDisplay->setRange (2000.0f * rangeGain[ContinuousChannel::Type::AUX], ContinuousChannel::Type::AUX); // Apply individual auto-scaling to each AUX channel based on their InputRange lfpDisplay->setAutoRangeForAuxChannels(); } @@ -1212,12 +1212,12 @@ void LfpDisplayOptions::comboBoxChanged (ComboBox* cb) if (cb->getSelectedId()) { String selectedText = voltageRanges[selectedChannelType][cb->getSelectedId() - 1]; - + // Check if "Auto" is selected for AUX channels - if (selectedChannelType == ContinuousChannel::Type::AUX && selectedText.equalsIgnoreCase("Auto")) + if (selectedChannelType == ContinuousChannel::Type::AUX && selectedText.equalsIgnoreCase ("Auto")) { // Set a default range value for the type (used as fallback) - lfpDisplay->setRange(2000.0f * rangeGain[selectedChannelType], selectedChannelType); + lfpDisplay->setRange (2000.0f * rangeGain[selectedChannelType], selectedChannelType); // Apply individual auto-scaling to each AUX channel based on their InputRange lfpDisplay->setAutoRangeForAuxChannels(); } @@ -1233,14 +1233,12 @@ void LfpDisplayOptions::comboBoxChanged (ComboBox* cb) { // Check if we should skip the first item (Auto) when validating range int firstRangeIndex = 0; - if (selectedChannelType == ContinuousChannel::Type::AUX && - voltageRanges[selectedChannelType][0].equalsIgnoreCase("Auto")) + if (selectedChannelType == ContinuousChannel::Type::AUX && voltageRanges[selectedChannelType][0].equalsIgnoreCase ("Auto")) { firstRangeIndex = 1; } - - if (firstRangeIndex < voltageRanges[selectedChannelType].size() && - vRange < voltageRanges[selectedChannelType][firstRangeIndex].getFloatValue()) + + if (firstRangeIndex < voltageRanges[selectedChannelType].size() && vRange < voltageRanges[selectedChannelType][firstRangeIndex].getFloatValue()) { cb->setSelectedId (firstRangeIndex + 1, dontSendNotification); vRange = voltageRanges[selectedChannelType][firstRangeIndex].getFloatValue(); @@ -1433,7 +1431,7 @@ ContinuousChannel::Type LfpDisplayOptions::getSelectedType() bool LfpDisplayOptions::isAuxAutoScaleEnabled() { - return selectedVoltageRangeValues[ContinuousChannel::Type::AUX].equalsIgnoreCase("Auto"); + return selectedVoltageRangeValues[ContinuousChannel::Type::AUX].equalsIgnoreCase ("Auto"); } void LfpDisplayOptions::setSelectedType (ContinuousChannel::Type type, bool toggleButton) @@ -1452,7 +1450,11 @@ void LfpDisplayOptions::setSelectedType (ContinuousChannel::Type type, bool togg else rangeSelection->setText (selectedVoltageRangeValues[selectedChannelType], dontSendNotification); - rangeSelectionLabel->setText ("Range (" + rangeUnits[type] + ")", dontSendNotification); + // If AUX and 'Auto' is selected, do not show units + if (type == ContinuousChannel::Type::AUX && isAuxAutoScaleEnabled()) + rangeSelectionLabel->setText ("Range", dontSendNotification); + else + rangeSelectionLabel->setText ("Range (" + rangeUnits[type] + ")", dontSendNotification); repaint (5, getHeight() - 55, 300, 100); @@ -1587,12 +1589,12 @@ void LfpDisplayOptions::loadParameters (XmlElement* xml) selectedVoltageRange[2] = voltageRanges[2].indexOf (ranges[2]) + 1; rangeSelection->setText (ranges[selectedChannelType]); lfpDisplay->setRange (ranges[0].getFloatValue() * rangeGain[0], ContinuousChannel::Type::ELECTRODE); - + // Handle Auto scaling for AUX channels - if (ranges[1].equalsIgnoreCase("Auto")) + if (ranges[1].equalsIgnoreCase ("Auto")) { // Set a default range value for the type (used as fallback) - lfpDisplay->setRange(2000.0f * rangeGain[ContinuousChannel::Type::AUX], ContinuousChannel::Type::AUX); + lfpDisplay->setRange (2000.0f * rangeGain[ContinuousChannel::Type::AUX], ContinuousChannel::Type::AUX); // Apply individual auto-scaling to each AUX channel based on their InputRange lfpDisplay->setAutoRangeForAuxChannels(); } @@ -1600,7 +1602,7 @@ void LfpDisplayOptions::loadParameters (XmlElement* xml) { lfpDisplay->setRange (ranges[1].getFloatValue() * rangeGain[1], ContinuousChannel::Type::AUX); } - + lfpDisplay->setRange (ranges[2].getFloatValue() * rangeGain[2], ContinuousChannel::Type::ADC); // LOGD(" Set range in ", MS_FROM_START, " milliseconds"); From 1202fbbdcbd7131fa0446e495be1fcb6ffe98f52 Mon Sep 17 00:00:00 2001 From: Anjal Doshi Date: Wed, 12 Nov 2025 15:45:33 -0800 Subject: [PATCH 07/26] Remove test input range settings for AUX channels in FileReader --- Source/Processors/FileReader/FileReader.cpp | 24 --------------------- 1 file changed, 24 deletions(-) diff --git a/Source/Processors/FileReader/FileReader.cpp b/Source/Processors/FileReader/FileReader.cpp index cb6c7a63f..9816b1587 100644 --- a/Source/Processors/FileReader/FileReader.cpp +++ b/Source/Processors/FileReader/FileReader.cpp @@ -542,30 +542,6 @@ void FileReader::updateSettings() }; continuousChannels.add (new ContinuousChannel (channelSettings)); - auto name = channelInfo[i].name; - - if (channelInfo[i].type == ContinuousChannel::Type::AUX) - { - if (name.equalsIgnoreCase("Eul-Y")) - continuousChannels.getLast()->inputRange = { -360.0f, 360.0f }; - else if (name.equalsIgnoreCase("Eul-R")) - continuousChannels.getLast()->inputRange = { -90.0f, 90.0f }; - else if (name.equalsIgnoreCase("Eul-P")) - continuousChannels.getLast()->inputRange = { -180.0f, 180.0f }; - else if (name.startsWithIgnoreCase("Quat")) - continuousChannels.getLast()->inputRange = { -1.0f, 1.0f }; - else if (name.startsWithIgnoreCase("Acc")) - continuousChannels.getLast()->inputRange = { -100.0f, 100.0f }; - else if (name.startsWithIgnoreCase("Grav")) - continuousChannels.getLast()->inputRange = { -10.0f, 10.0f }; - else if (name.equalsIgnoreCase("Temp")) - continuousChannels.getLast()->inputRange = { -100.0f, 100.0f }; - else if (name.startsWithIgnoreCase("Cal")) - continuousChannels.getLast()->inputRange = { -3.0f, 3.0f }; - else - continuousChannels.getLast()->inputRange = { -5000.0f, 5000.0f }; - } - continuousChannels.getLast()->addProcessor (this); } From d2dda07772776ccc61c281c76f8e27715401488f Mon Sep 17 00:00:00 2001 From: Anjal Doshi Date: Wed, 12 Nov 2025 15:50:05 -0800 Subject: [PATCH 08/26] Display units alongside channel type in LFP Viewer --- Plugins/LfpViewer/DisplayBuffer.cpp | 4 +++- Plugins/LfpViewer/DisplayBuffer.h | 4 +++- Plugins/LfpViewer/LfpChannelDisplay.cpp | 10 +++++++++ Plugins/LfpViewer/LfpChannelDisplay.h | 8 +++++++ Plugins/LfpViewer/LfpChannelDisplayInfo.cpp | 25 ++++++++++++++++++++- Plugins/LfpViewer/LfpDisplayNode.cpp | 3 ++- 6 files changed, 50 insertions(+), 4 deletions(-) diff --git a/Plugins/LfpViewer/DisplayBuffer.cpp b/Plugins/LfpViewer/DisplayBuffer.cpp index d0b52142f..c471d4a02 100644 --- a/Plugins/LfpViewer/DisplayBuffer.cpp +++ b/Plugins/LfpViewer/DisplayBuffer.cpp @@ -68,7 +68,8 @@ void DisplayBuffer::addChannel ( String description, String structure, float inputRangeMin, - float inputRangeMax) + float inputRangeMax, + String units) { ChannelMetadata metadata = ChannelMetadata(); metadata.name = name; @@ -81,6 +82,7 @@ void DisplayBuffer::addChannel ( metadata.description = description; metadata.inputRangeMin = inputRangeMin; metadata.inputRangeMax = inputRangeMax; + metadata.units = units; channelMetadata.add (metadata); channelMap[channelNum] = numChannels; diff --git a/Plugins/LfpViewer/DisplayBuffer.h b/Plugins/LfpViewer/DisplayBuffer.h index 6912ce0d1..45ea5b7be 100644 --- a/Plugins/LfpViewer/DisplayBuffer.h +++ b/Plugins/LfpViewer/DisplayBuffer.h @@ -65,7 +65,8 @@ class TESTABLE DisplayBuffer : public AudioBuffer String description = "", String structure = "None", float inputRangeMin = -5000.0f, - float inputRangeMax = +5000.0f); + float inputRangeMax = +5000.0f, + String units = ""); /** Initializes the event channel at the start of each buffer */ void initializeEventChannel (int nSamples); @@ -95,6 +96,7 @@ class TESTABLE DisplayBuffer : public AudioBuffer String description = ""; float inputRangeMin = -5000.0f; float inputRangeMax = +5000.0f; + String units = ""; }; Array channelMetadata; diff --git a/Plugins/LfpViewer/LfpChannelDisplay.cpp b/Plugins/LfpViewer/LfpChannelDisplay.cpp index a1df71d80..27a8c572e 100644 --- a/Plugins/LfpViewer/LfpChannelDisplay.cpp +++ b/Plugins/LfpViewer/LfpChannelDisplay.cpp @@ -79,6 +79,16 @@ void LfpChannelDisplay::setType (ContinuousChannel::Type type_) typeStr = options->getTypeName (type); } +void LfpChannelDisplay::setUnits (const String& newUnits) +{ + units = newUnits; +} + +const String& LfpChannelDisplay::getUnits() const +{ + return units; +} + void LfpChannelDisplay::setEnabledState (bool state) { /*if (state) diff --git a/Plugins/LfpViewer/LfpChannelDisplay.h b/Plugins/LfpViewer/LfpChannelDisplay.h index f360c1a54..a90efe7ad 100644 --- a/Plugins/LfpViewer/LfpChannelDisplay.h +++ b/Plugins/LfpViewer/LfpChannelDisplay.h @@ -113,6 +113,12 @@ class LfpChannelDisplay : public Component /** Return the assigned channel name */ String getName(); + /** Set the units string used for display */ + void setUnits (const String& newUnits); + + /** Return the units string for this channel */ + const String& getUnits() const; + /** Returns the assigned channel number for this display, relative to the subset of channels being drawn to the canvas */ int getDrawableChannelNumber(); @@ -193,6 +199,8 @@ class LfpChannelDisplay : public Component float depth; bool isRecorded; + String units; + FontOptions channelFont; Colour lineColour; diff --git a/Plugins/LfpViewer/LfpChannelDisplayInfo.cpp b/Plugins/LfpViewer/LfpChannelDisplayInfo.cpp index 4ef015e5c..904ea56c4 100644 --- a/Plugins/LfpViewer/LfpChannelDisplayInfo.cpp +++ b/Plugins/LfpViewer/LfpChannelDisplayInfo.cpp @@ -239,8 +239,31 @@ void LfpChannelDisplayInfo::paint (Graphics& g) if (getChannelTypeStringVisibility()) { + constexpr int textHeight = 14; + constexpr int textStartX = 5; + const int textY = center + 10; + g.setFont (FontOptions (13.0f)); - g.drawText (typeStr, 5, center + 10, 50, 14, Justification::centred, false); + g.setColour (lineColour); + const auto currentFont = g.getCurrentFont(); + + const int typeWidth = currentFont.getStringWidth (typeStr); + const int typeBoundsWidth = typeWidth + 2; + g.drawText (typeStr, textStartX, textY, typeBoundsWidth, textHeight, Justification::centredLeft, false); + + const String& unitsText = getUnits(); + if (unitsText.isNotEmpty()) + { + const int unitsX = textStartX + typeWidth + 5; + const int unitsWidth = getWidth() - unitsX - 4; + + if (unitsWidth > 0) + { + g.setColour (Colours::grey.withAlpha (0.8f)); + g.setFont (FontOptions (12.0f)); + g.drawFittedText (unitsText, unitsX, textY, unitsWidth, textHeight, Justification::centredLeft, 1, 0.8f); + } + } } if (isSingleChannel) diff --git a/Plugins/LfpViewer/LfpDisplayNode.cpp b/Plugins/LfpViewer/LfpDisplayNode.cpp index 3e29f4ee3..752551cd4 100644 --- a/Plugins/LfpViewer/LfpDisplayNode.cpp +++ b/Plugins/LfpViewer/LfpDisplayNode.cpp @@ -95,7 +95,8 @@ void LfpDisplayNode::updateSettings() channel->getDescription(), "None", // structure channel->inputRange.min, // inputRangeMin - channel->inputRange.max); // inputRangeMax + channel->inputRange.max, // inputRangeMax + channel->getUnits()); // units } Array toDelete; From 279705d507f7a8b5d30e5b7e796559ece9565ba9 Mon Sep 17 00:00:00 2001 From: Anjal Doshi Date: Wed, 12 Nov 2025 15:51:51 -0800 Subject: [PATCH 09/26] Adjust left margin of LFP Display dynamically based on channel name width --- Plugins/LfpViewer/LfpDisplayCanvas.cpp | 28 ++++++++++++++++++++++++++ Plugins/LfpViewer/LfpDisplayCanvas.h | 9 +++++++-- 2 files changed, 35 insertions(+), 2 deletions(-) diff --git a/Plugins/LfpViewer/LfpDisplayCanvas.cpp b/Plugins/LfpViewer/LfpDisplayCanvas.cpp index ff1f7f48e..5d94cfcef 100644 --- a/Plugins/LfpViewer/LfpDisplayCanvas.cpp +++ b/Plugins/LfpViewer/LfpDisplayCanvas.cpp @@ -30,6 +30,8 @@ #include "ShowHideOptionsButton.h" #include +#include +#include #define MS_FROM_START Time::highResolutionTicksToSeconds (Time::getHighResolutionTicks() - start) * 1000 @@ -750,6 +752,27 @@ String LfpDisplaySplitter::getStreamKey() return stream->getKey(); } +void LfpDisplaySplitter::refreshLeftMargin() +{ + int newMargin = minimumLeftMargin; + + if (displayBuffer != nullptr) + { + const auto labelFont = Font (FontOptions (14.0f)); + constexpr int padding = 20; + + for (int i = 0; i < displayBuffer->channelMetadata.size(); ++i) + { + const auto& metadata = displayBuffer->channelMetadata.getReference (i); + const float textWidth = labelFont.getStringWidthFloat (metadata.name); + const int requiredWidth = static_cast (std::ceil (textWidth)) + padding; + newMargin = std::max (newMargin, requiredWidth); + } + } + + leftmargin = newMargin; +} + void LfpDisplaySplitter::resized() { const int timescaleHeight = 30; @@ -928,6 +951,7 @@ void LfpDisplaySplitter::updateSettings() sampleRate = 44100.0f; options->setEnabled (true); + leftmargin = minimumLeftMargin; } else { @@ -942,6 +966,8 @@ void LfpDisplaySplitter::updateSettings() options->setEnabled (true); channelOverlapFactor = options->selectedOverlapValue.getFloatValue(); + + refreshLeftMargin(); } if (eventDisplayBuffer == nullptr) // not yet initialized @@ -968,12 +994,14 @@ void LfpDisplaySplitter::updateSettings() lfpDisplay->channels[i]->setDepth (displayBuffer->channelMetadata[i].ypos); lfpDisplay->channels[i]->setRecorded (displayBuffer->channelMetadata[i].isRecorded); lfpDisplay->channels[i]->updateType (displayBuffer->channelMetadata[i].type); + lfpDisplay->channels[i]->setUnits (displayBuffer->channelMetadata[i].units); lfpDisplay->channelInfo[i]->setName (displayBuffer->channelMetadata[i].name); lfpDisplay->channelInfo[i]->setGroup (displayBuffer->channelMetadata[i].group); lfpDisplay->channelInfo[i]->setDepth (displayBuffer->channelMetadata[i].ypos); lfpDisplay->channelInfo[i]->setRecorded (displayBuffer->channelMetadata[i].isRecorded); lfpDisplay->channelInfo[i]->updateType (displayBuffer->channelMetadata[i].type); + lfpDisplay->channelInfo[i]->setUnits (displayBuffer->channelMetadata[i].units); lfpDisplay->updateRange (i); diff --git a/Plugins/LfpViewer/LfpDisplayCanvas.h b/Plugins/LfpViewer/LfpDisplayCanvas.h index adb90583e..2e9631b39 100644 --- a/Plugins/LfpViewer/LfpDisplayCanvas.h +++ b/Plugins/LfpViewer/LfpDisplayCanvas.h @@ -308,8 +308,11 @@ class LfpDisplaySplitter : public Component, */ bool fullredraw; - /** Left margin for lfp plots (so the ch number text doesnt overlap) */ - static const int leftmargin = 60; // + /** Minimum left margin for LFP plots (so the channel label has space) */ + static constexpr int minimumLeftMargin = 60; + + /** Left margin for LFP plots, adjusted to fit the widest channel label */ + int leftmargin = minimumLeftMargin; Array isChannelEnabled; @@ -359,6 +362,8 @@ class LfpDisplaySplitter : public Component, String getStreamKey(); private: + void refreshLeftMargin(); + bool isSelected; bool isUpdating; From ef83d54b47c91b695ab7238eef517c9a7517b6a0 Mon Sep 17 00:00:00 2001 From: Anjal Doshi Date: Thu, 20 Nov 2025 12:08:03 -0800 Subject: [PATCH 10/26] Add error logging for missing files in BinaryFileSource --- .../BinaryFileSource/BinaryFileSource.cpp | 20 ++++++++++++++++--- 1 file changed, 17 insertions(+), 3 deletions(-) diff --git a/Source/Processors/FileReader/BinaryFileSource/BinaryFileSource.cpp b/Source/Processors/FileReader/BinaryFileSource/BinaryFileSource.cpp index a095c1e3d..99f906526 100644 --- a/Source/Processors/FileReader/BinaryFileSource/BinaryFileSource.cpp +++ b/Source/Processors/FileReader/BinaryFileSource/BinaryFileSource.cpp @@ -77,7 +77,7 @@ void BinaryFileSource::fillRecordInfo() String sampleNumbersFilename; String channelStatesFilename; - int majorVersion = guiVersion.substring(0, 1).getIntValue(); + int majorVersion = guiVersion.substring (0, 1).getIntValue(); int minorVersion = guiVersion.substring (2, 3).getIntValue(); if (minorVersion < 6 && majorVersion == 0) @@ -125,7 +125,10 @@ void BinaryFileSource::fillRecordInfo() File dataFile = m_rootPath.getChildFile ("continuous").getChildFile (streamName).getChildFile ("continuous.dat"); if (! dataFile.existsAsFile()) + { + LOGE ("Continuous data file not found: ", dataFile.getFullPathName()); continue; + } int numChannels = record[idNumChannels]; int64 numSamples = (dataFile.getSize() / numChannels) / sizeof (int16); @@ -157,7 +160,7 @@ void BinaryFileSource::fillRecordInfo() cInfo.name = chan[idChannelName]; cInfo.bitVolts = chan[idBitVolts]; - cInfo.type = static_cast(int(chan[idType])); + cInfo.type = static_cast (int (chan[idType])); info.channels.add (cInfo); } @@ -210,6 +213,12 @@ void BinaryFileSource::fillRecordInfo() LOGD ("TTL found"); File channelStatesFile = m_rootPath.getChildFile ("events").getChildFile (streamName).getChildFile (channelStatesFilename); + + if (! channelStatesFile.existsAsFile()) + { + LOGE ("Channel states file not found: ", channelStatesFile.getFullPathName()); + continue; + } LOGD ("Channel States File: ", channelStatesFile.getFullPathName()); std::unique_ptr channelStatesFileMap (new MemoryMappedFile (channelStatesFile, MemoryMappedFile::readOnly)); jassert (channelStatesFileMap.get() != nullptr); @@ -233,6 +242,11 @@ void BinaryFileSource::fillRecordInfo() LOGD ("Message found"); File textFile = m_rootPath.getChildFile ("events").getChildFile (streamName).getChildFile ("text.npy"); + if (! textFile.existsAsFile()) + { + LOGE ("MessageCenter text file not found: ", textFile.getFullPathName()); + continue; + } juce::FileInputStream inputStream (textFile); inputStream.skipNextBytes (10); // \x93NUMPY \x01 \x00 @@ -338,7 +352,7 @@ int BinaryFileSource::readData (float* buffer, int nSamples) } m_samplePos += samplesToRead; - return int(samplesToRead); + return int (samplesToRead); } /* void BinaryFileSource::processChannelData (int16* inBuffer, float* outBuffer, int channel, int64 numSamples) From 90bc34d56bfc1bc2b12e106403b3ca591a723d2a Mon Sep 17 00:00:00 2001 From: Anjal Doshi Date: Wed, 26 Nov 2025 15:15:18 -0800 Subject: [PATCH 11/26] Validate selected and masked channels against channel count limit when copying parameter values --- .../Parameter/ParameterCollection.cpp | 39 ++++++++++++++++++- 1 file changed, 38 insertions(+), 1 deletion(-) diff --git a/Source/Processors/Parameter/ParameterCollection.cpp b/Source/Processors/Parameter/ParameterCollection.cpp index 017f70f87..1a1ee6fd9 100644 --- a/Source/Processors/Parameter/ParameterCollection.cpp +++ b/Source/Processors/Parameter/ParameterCollection.cpp @@ -87,7 +87,44 @@ void ParameterCollection::copyParameterValuesTo (ParameterOwner* pOwner) for (auto parameter : parameters) { if (pOwner->hasParameter (parameter->getName())) - pOwner->getParameter (parameter->getName())->currentValue = parameter->getValue(); + { + Parameter* targetParam = pOwner->getParameter (parameter->getName()); + + // For MaskChannelsParameter and SelectedChannelsParameter, filter the values + // to only include valid channel indices for the target parameter's channel count + if (parameter->getType() == Parameter::MASK_CHANNELS_PARAM) + { + MaskChannelsParameter* targetMaskParam = (MaskChannelsParameter*) targetParam; + int targetChannelCount = targetMaskParam->getChannelCount(); + + Array filteredValues; + for (int i = 0; i < parameter->getValue().getArray()->size(); i++) + { + int channelIndex = (int) parameter->getValue()[i]; + if (channelIndex >= 0 && channelIndex < targetChannelCount) + filteredValues.add (channelIndex); + } + targetParam->currentValue = filteredValues; + } + else if (parameter->getType() == Parameter::SELECTED_CHANNELS_PARAM) + { + SelectedChannelsParameter* targetSelectedParam = (SelectedChannelsParameter*) targetParam; + int targetChannelCount = targetSelectedParam->getChannelCount(); + + Array filteredValues; + for (int i = 0; i < parameter->getValue().getArray()->size(); i++) + { + int channelIndex = (int) parameter->getValue()[i]; + if (channelIndex >= 0 && channelIndex < targetChannelCount) + filteredValues.add (channelIndex); + } + targetParam->currentValue = filteredValues; + } + else + { + targetParam->currentValue = parameter->getValue(); + } + } } } From 2064ffc301c1b58828fdfad02de8e35a5c87dbf6 Mon Sep 17 00:00:00 2001 From: Anjal Doshi Date: Wed, 26 Nov 2025 17:48:56 -0800 Subject: [PATCH 12/26] Add file validation and error handling in BinaryRecording and RecordNode Fixes #679 --- .../BinaryFormat/BinaryRecording.cpp | 131 ++++++++++++++++-- .../RecordNode/BinaryFormat/BinaryRecording.h | 4 + .../RecordNode/BinaryFormat/NpyFile.cpp | 8 +- .../RecordNode/BinaryFormat/NpyFile.h | 3 + .../BinaryFormat/SequentialBlockFile.cpp | 7 +- Source/Processors/RecordNode/RecordNode.cpp | 22 ++- Source/UI/ControlPanel.cpp | 4 +- 7 files changed, 160 insertions(+), 19 deletions(-) diff --git a/Source/Processors/RecordNode/BinaryFormat/BinaryRecording.cpp b/Source/Processors/RecordNode/BinaryFormat/BinaryRecording.cpp index 7c6072093..06951f879 100644 --- a/Source/Processors/RecordNode/BinaryFormat/BinaryRecording.cpp +++ b/Source/Processors/RecordNode/BinaryFormat/BinaryRecording.cpp @@ -26,6 +26,7 @@ #include "../../Settings/DataStream.h" #include "../../Settings/InfoObject.h" +#include "../../../CoreServices.h" #include "../../Events/Spike.h" #define TIC std::chrono::high_resolution_clock::now() @@ -95,7 +96,7 @@ void BinaryRecording::openFiles (File rootFolder, int experimentNumber, int reco if (streamId != lastStreamId) { wroteFirstSampleNumber[streamId] = false; - + firstChannels.add (channelInfo); streamIndex++; @@ -120,7 +121,7 @@ void BinaryRecording::openFiles (File rootFolder, int experimentNumber, int reco singleChannelJSON->setProperty ("history", channelInfo->getHistoryString()); singleChannelJSON->setProperty ("bit_volts", channelInfo->getBitVolts()); singleChannelJSON->setProperty ("units", channelInfo->getUnits()); - singleChannelJSON->setProperty ("type", static_cast(channelInfo->getChannelType())); + singleChannelJSON->setProperty ("type", static_cast (channelInfo->getChannelType())); createChannelMetadata (channelInfo, singleChannelJSON); singleStreamJSON.add (var (singleChannelJSON)); @@ -140,11 +141,14 @@ void BinaryRecording::openFiles (File rootFolder, int experimentNumber, int reco String datPath = getProcessorString (ch); String filename = contPath + datPath + "continuous.dat"; - LOGD ("Creating file: ", contPath, datPath, "sample_numbers.npy"); - ScopedPointer tFile = new NpyFile (contPath + datPath + "sample_numbers.npy", NpyType (BaseType::INT64, 1)); + String samplesPath = contPath + datPath + "sample_numbers.npy"; + LOGD ("Creating file: ", samplesPath); + ScopedPointer tFile = new NpyFile (samplesPath, NpyType (BaseType::INT64, 1)); m_dataTimestampFiles.add (tFile.release()); - ScopedPointer syncTimestampFile = new NpyFile (contPath + datPath + "timestamps.npy", NpyType (BaseType::DOUBLE, 1)); + String syncTimestampPath = contPath + datPath + "timestamps.npy"; + LOGD ("Creating file: ", syncTimestampPath); + ScopedPointer syncTimestampFile = new NpyFile (syncTimestampPath, NpyType (BaseType::DOUBLE, 1)); m_dataSyncTimestampFiles.add (syncTimestampFile.release()); DynamicObject::Ptr fileJSON = new DynamicObject(); @@ -332,7 +336,113 @@ void BinaryRecording::openFiles (File rootFolder, int experimentNumber, int reco settingsJSON->writeAsJSON (settingsFileStream, JSON::FormatOptions {}.withIndentLevel (2).withSpacing (JSON::Spacing::multiLine).withMaxDecimalPlaces (10)); - + // Validate that all files were opened successfully + if (! validateOpenFiles()) + { + String errorMsg = "Recording stopped! Failed to open one or more recording files. Please check disk space and write permissions."; + + LOGE ("BinaryRecording::openFiles: ", errorMsg); + + // Stop recording and show error message on the message thread + CoreServices::setRecordingStatus (false); + MessageManager::callAsync ([] + { CoreServices::sendStatusMessage ("Unable to start recording. Please check the console for errors."); }); + } +} + +bool BinaryRecording::validateOpenFiles() const +{ + bool allFilesValid = true; + + // Check continuous data files (SequentialBlockFile) + for (int i = 0; i < m_continuousFiles.size(); i++) + { + if (m_continuousFiles[i] == nullptr) + { + allFilesValid = false; + } + } + + // Check timestamp files + for (int i = 0; i < m_dataTimestampFiles.size(); i++) + { + if (m_dataTimestampFiles[i] == nullptr || ! m_dataTimestampFiles[i]->isOpen()) + { + allFilesValid = false; + } + } + + // Check sync timestamp files + for (int i = 0; i < m_dataSyncTimestampFiles.size(); i++) + { + if (m_dataSyncTimestampFiles[i] == nullptr || ! m_dataSyncTimestampFiles[i]->isOpen()) + { + allFilesValid = false; + } + } + + // Check event files + for (int i = 0; i < m_eventFiles.size(); i++) + { + EventRecording* rec = m_eventFiles[i]; + if (rec != nullptr) + { + if (rec->data == nullptr || ! rec->data->isOpen()) + { + allFilesValid = false; + } + if (rec->samples == nullptr || ! rec->samples->isOpen()) + { + allFilesValid = false; + } + if (rec->timestamps == nullptr || ! rec->timestamps->isOpen()) + { + allFilesValid = false; + } + // extraFile is optional (only for TTL full words) + if (rec->extraFile != nullptr && ! rec->extraFile->isOpen()) + { + allFilesValid = false; + } + } + } + + // Check spike files + for (int i = 0; i < m_spikeFiles.size(); i++) + { + EventRecording* rec = m_spikeFiles[i]; + if (rec != nullptr) + { + if (rec->data == nullptr || ! rec->data->isOpen()) + { + allFilesValid = false; + } + if (rec->samples == nullptr || ! rec->samples->isOpen()) + { + allFilesValid = false; + } + if (rec->timestamps == nullptr || ! rec->timestamps->isOpen()) + { + allFilesValid = false; + } + if (rec->channels == nullptr || ! rec->channels->isOpen()) + { + allFilesValid = false; + } + if (rec->extraFile == nullptr || ! rec->extraFile->isOpen()) + { + allFilesValid = false; + } + } + } + + // Check sync text file + if (m_syncTextFile == nullptr) + { + allFilesValid = false; + } + + return allFilesValid; } std::unique_ptr BinaryRecording::createEventMetadataFile (const MetadataEventObject* channel, String filename, DynamicObject* jsonFile) @@ -549,6 +659,9 @@ void BinaryRecording::writeContinuousData (int writeChannel, /* Get the file index that belongs to the current recording channel */ int fileIndex = m_fileIndexes[writeChannel]; + if (! m_continuousFiles[fileIndex]) + return; + /* Write the data to that file */ m_continuousFiles[fileIndex]->writeChannel ( m_samplesWritten[writeChannel], @@ -565,7 +678,7 @@ void BinaryRecording::writeContinuousData (int writeChannel, uint32 streamId = getContinuousChannel (realChannel)->getStreamId(); - if (! wroteFirstSampleNumber[streamId] ) + if (! wroteFirstSampleNumber[streamId]) { firstSampleNumber[streamId] = baseSampleNumber; wroteFirstSampleNumber[streamId] = true; @@ -682,11 +795,11 @@ void BinaryRecording::writeTimestampSyncText (uint64 streamId, int64 sampleNumbe int64 fsn = firstSampleNumber[streamId]; - if(streamId > 0) + if (streamId > 0) jassert (fsn == sampleNumber); m_syncTextFile->writeText (syncString + "\r\n", false, false, nullptr); - + m_syncTextFile->flush(); } diff --git a/Source/Processors/RecordNode/BinaryFormat/BinaryRecording.h b/Source/Processors/RecordNode/BinaryFormat/BinaryRecording.h index a604dcd73..9932571b0 100644 --- a/Source/Processors/RecordNode/BinaryFormat/BinaryRecording.h +++ b/Source/Processors/RecordNode/BinaryFormat/BinaryRecording.h @@ -89,6 +89,10 @@ class BinaryRecording : public RecordEngine void createChannelMetadata (const MetadataObject* channel, DynamicObject* jsonObject); void writeEventMetadata (const MetadataEvent* event, NpyFile* file); void increaseEventCounts (EventRecording* rec); + + /** Validates that all recording files were opened successfully. + Returns true if all files are valid, false otherwise. */ + bool validateOpenFiles () const; bool m_saveTTLWords { true }; diff --git a/Source/Processors/RecordNode/BinaryFormat/NpyFile.cpp b/Source/Processors/RecordNode/BinaryFormat/NpyFile.cpp index d7226aa79..0bce821bf 100644 --- a/Source/Processors/RecordNode/BinaryFormat/NpyFile.cpp +++ b/Source/Processors/RecordNode/BinaryFormat/NpyFile.cpp @@ -70,7 +70,7 @@ bool NpyFile::openFile (String path) Result res = file.create(); if (res.failed()) { - std::cerr << "Error creating file " << path << ":" << res.getErrorMessage() << std::endl; + LOGD ("Error creating file ", path, ": ", res.getErrorMessage()); file.deleteFile(); Result res = file.create(); LOGD ("Re-creating file: ", path); @@ -108,7 +108,7 @@ void NpyFile::writeHeader (const Array& typeList) String magicStr = "NUMPY"; uint16 ver = 0x0001; // magic = magic number + magic string + magic version - int magicLen = int( sizeof (uint8) + magicStr.getNumBytesAsUTF8() + sizeof (uint16)); + int magicLen = int (sizeof (uint8) + magicStr.getNumBytesAsUTF8() + sizeof (uint16)); int nbytesAlign = 64; // header should use an integer multiple of this many bytes bool multiValue = typeList.size() > 1; @@ -157,7 +157,7 @@ void NpyFile::writeHeader (const Array& typeList) void NpyFile::updateHeader() { - if (true) + if (m_okOpen) // only update if file opened successfully { // overwrite the shape part of the header - even without explicitly calling // m_file->flush(), overwriting seems to trigger a flush to disk, @@ -251,7 +251,7 @@ int NpyType::getTypeLength() const if (type == BaseType::CHAR) return 1; else - return int(length); + return int (length); } String NpyType::getName() const diff --git a/Source/Processors/RecordNode/BinaryFormat/NpyFile.h b/Source/Processors/RecordNode/BinaryFormat/NpyFile.h index 4a5ae79e3..4ea0dbc48 100644 --- a/Source/Processors/RecordNode/BinaryFormat/NpyFile.h +++ b/Source/Processors/RecordNode/BinaryFormat/NpyFile.h @@ -99,6 +99,9 @@ class PLUGIN_API NpyFile /** Increases the count of the number of records in the file (must match the number of samples written) */ void increaseRecordCount (int count = 1); + /** Returns true if the file was opened successfully */ + bool isOpen() const { return m_okOpen; } + private: /** Opens the file at a specified path */ bool openFile (String path); diff --git a/Source/Processors/RecordNode/BinaryFormat/SequentialBlockFile.cpp b/Source/Processors/RecordNode/BinaryFormat/SequentialBlockFile.cpp index d283347ab..d603c0843 100644 --- a/Source/Processors/RecordNode/BinaryFormat/SequentialBlockFile.cpp +++ b/Source/Processors/RecordNode/BinaryFormat/SequentialBlockFile.cpp @@ -44,7 +44,8 @@ SequentialBlockFile::~SequentialBlockFile() } //manually flush the last one to avoid trailing zeroes - m_memBlocks[0]->partialFlush (m_lastBlockFill * m_nChannels); + if (m_memBlocks.size() > 0) + m_memBlocks[0]->partialFlush (m_lastBlockFill * m_nChannels); } bool SequentialBlockFile::openFile (String filename) @@ -53,7 +54,7 @@ bool SequentialBlockFile::openFile (String filename) Result res = file.create(); if (res.failed()) { - std::cerr << "Error creating file " << filename << ":" << res.getErrorMessage() << std::endl; + LOGD ("Error creating file ", filename, ": ", res.getErrorMessage()); file.deleteFile(); Result res = file.create(); LOGD ("Re-creating file: ", filename); @@ -104,7 +105,7 @@ bool SequentialBlockFile::writeChannel (uint64 startPos, int channel, int16* dat while (writtenSamples < nSamples) { int16* blockPtr = m_memBlocks[bIndex]->getData(); - int samplesToWrite = jmin ((nSamples - writtenSamples), (m_samplesPerBlock - int(startIdx))); + int samplesToWrite = jmin ((nSamples - writtenSamples), (m_samplesPerBlock - int (startIdx))); for (int i = 0; i < samplesToWrite; i++) { diff --git a/Source/Processors/RecordNode/RecordNode.cpp b/Source/Processors/RecordNode/RecordNode.cpp index a94e0fcaa..8d0de543e 100755 --- a/Source/Processors/RecordNode/RecordNode.cpp +++ b/Source/Processors/RecordNode/RecordNode.cpp @@ -865,7 +865,24 @@ void RecordNode::startRecording() if (! rootFolder.exists()) { - rootFolder.createDirectory(); + Result res = rootFolder.createDirectory(); + if (res.failed()) + { + LOGE ("Record Node " + String (getNodeId()) + ": Could not create directory: " + rootFolder.getFullPathName(), " -- ", res.getErrorMessage()); + + CoreServices::setRecordingStatus (false); + + if (! headlessMode) + { + AlertWindow::showMessageBoxAsync (AlertWindow::WarningIcon, + "Recording Error", + "Record Node " + String (getNodeId()) + " - Could not create recording directory:\n\n" + + rootFolder.getFullPathName() + "\n\n" + + res.getErrorMessage()); + } + + return; + } } recordThread->setFileComponents (rootFolder, experimentNumber, recordingNumber); @@ -889,6 +906,9 @@ void RecordNode::startRecording() // called by GenericProcessor::setRecording() and CoreServices::setRecordingStatus() void RecordNode::stopRecording() { + if (! isRecording) + return; + isRecording = false; hasRecorded = true; recordingNumber++; // increment recording number within this directory; should be zero for first recording diff --git a/Source/UI/ControlPanel.cpp b/Source/UI/ControlPanel.cpp index 269735889..d9ea87ab6 100755 --- a/Source/UI/ControlPanel.cpp +++ b/Source/UI/ControlPanel.cpp @@ -1048,10 +1048,10 @@ void ControlPanel::startRecording() filenameComponent->setEnabled (false); - graph->setRecordState (true); - LOGC ("Starting recording"); + graph->setRecordState (true); + repaint(); } From 64eb6bc4d998b39ccff94fb60b3bbfbb8a9380ee Mon Sep 17 00:00:00 2001 From: Pavel Kulik Date: Tue, 9 Dec 2025 22:35:05 -0800 Subject: [PATCH 13/26] Upgrade Python version --- .github/workflows/tests.yml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 48bad8e80..b6f49a99e 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -44,6 +44,10 @@ jobs: - uses: actions/checkout@v4 with: fetch-depth: 0 + - name: Setup Python + uses: actions/setup-python@v5 + with: + python-version: '3.13' - name: Start Windows Audio Engine run: net start audiosrv - name: Install Scream From 01cc22470b3855f214b4711ff0d1171b32f8418e Mon Sep 17 00:00:00 2001 From: Pavel Kulik Date: Fri, 12 Dec 2025 15:10:54 -0800 Subject: [PATCH 14/26] Restore cross-platform builds on PRs --- .github/workflows/linux.yml | 10 ++++++++++ .github/workflows/osx.yml | 10 ++++++++++ .github/workflows/windows.yml | 10 ++++++++++ 3 files changed, 30 insertions(+) diff --git a/.github/workflows/linux.yml b/.github/workflows/linux.yml index 91e522106..05f7bc8f7 100644 --- a/.github/workflows/linux.yml +++ b/.github/workflows/linux.yml @@ -11,6 +11,16 @@ on: - 'Source/**' - 'CMakeLists.txt' - 'HelperFunctions.cmake' + pull_request: + paths: + - '.github/workflows/**' + - 'JuceLibraryCode/**' + - 'PluginGenerator/**' + - 'Plugins/**' + - 'Resources/**' + - 'Source/**' + - 'CMakeLists.txt' + - 'HelperFunctions.cmake' jobs: build-ubuntu: diff --git a/.github/workflows/osx.yml b/.github/workflows/osx.yml index 043a9fece..0775dc65f 100644 --- a/.github/workflows/osx.yml +++ b/.github/workflows/osx.yml @@ -11,6 +11,16 @@ on: - 'Source/**' - 'CMakeLists.txt' - 'HelperFunctions.cmake' + pull_request: + paths: + - '.github/workflows/**' + - 'JuceLibraryCode/**' + - 'PluginGenerator/**' + - 'Plugins/**' + - 'Resources/**' + - 'Source/**' + - 'CMakeLists.txt' + - 'HelperFunctions.cmake' jobs: build-osx: diff --git a/.github/workflows/windows.yml b/.github/workflows/windows.yml index c1d711844..375f9b6b6 100644 --- a/.github/workflows/windows.yml +++ b/.github/workflows/windows.yml @@ -11,6 +11,16 @@ on: - 'Source/**' - 'CMakeLists.txt' - 'HelperFunctions.cmake' + pull_request: + paths: + - '.github/workflows/**' + - 'JuceLibraryCode/**' + - 'PluginGenerator/**' + - 'Plugins/**' + - 'Resources/**' + - 'Source/**' + - 'CMakeLists.txt' + - 'HelperFunctions.cmake' jobs: build-windows: From a96ba3d6f4956d1c28a02a41bf1a209fae9d849a Mon Sep 17 00:00:00 2001 From: Anjal Doshi Date: Fri, 12 Dec 2025 16:38:04 -0800 Subject: [PATCH 15/26] Add error handling for file loading in SelectFile action --- Source/Processors/FileReader/FileReaderActions.cpp | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/Source/Processors/FileReader/FileReaderActions.cpp b/Source/Processors/FileReader/FileReaderActions.cpp index ce59d18a6..1349a0289 100644 --- a/Source/Processors/FileReader/FileReaderActions.cpp +++ b/Source/Processors/FileReader/FileReaderActions.cpp @@ -48,7 +48,14 @@ bool SelectFile::perform() pathParam->setNextValue (newPath, false); // Load the new file - processor->setFile (newPath, false); + bool success = processor->setFile (newPath, false); + + if (!success) + { + // If loading the file failed, revert the path parameter to the original path + pathParam->setNextValue (originalPath, false); + return false; + } // Set the active stream to the first stream processor->setActiveStream (0, true); From a4e3f64c0df6598f737d2d5378d45288f1bc9c8b Mon Sep 17 00:00:00 2001 From: Anjal Doshi Date: Mon, 15 Dec 2025 15:19:16 -0800 Subject: [PATCH 16/26] Update LfpViewer to use new channel position and group metadata --- Plugins/LfpViewer/DisplayBuffer.cpp | 10 ++- Plugins/LfpViewer/DisplayBuffer.h | 10 ++- Plugins/LfpViewer/LfpChannelDisplay.cpp | 12 +++ Plugins/LfpViewer/LfpChannelDisplay.h | 18 ++++- Plugins/LfpViewer/LfpDisplay.cpp | 102 +++++++++++++++++++----- Plugins/LfpViewer/LfpDisplayCanvas.cpp | 10 ++- Plugins/LfpViewer/LfpDisplayNode.cpp | 26 +++++- 7 files changed, 162 insertions(+), 26 deletions(-) diff --git a/Plugins/LfpViewer/DisplayBuffer.cpp b/Plugins/LfpViewer/DisplayBuffer.cpp index c471d4a02..67b4049d3 100644 --- a/Plugins/LfpViewer/DisplayBuffer.cpp +++ b/Plugins/LfpViewer/DisplayBuffer.cpp @@ -64,17 +64,22 @@ void DisplayBuffer::addChannel ( ContinuousChannel::Type type, bool isRecorded, int group, + float xpos, float ypos, String description, String structure, float inputRangeMin, float inputRangeMax, - String units) + String units, + bool hasGroupMetadata, + bool hasYposMetadata, + bool hasXposMetadata) { ChannelMetadata metadata = ChannelMetadata(); metadata.name = name; metadata.type = type; metadata.group = group; + metadata.xpos = xpos; metadata.ypos = ypos; metadata.structure = structure; metadata.type = type; @@ -83,6 +88,9 @@ void DisplayBuffer::addChannel ( metadata.inputRangeMin = inputRangeMin; metadata.inputRangeMax = inputRangeMax; metadata.units = units; + metadata.hasGroupMetadata = hasGroupMetadata; + metadata.hasYposMetadata = hasYposMetadata; + metadata.hasXposMetadata = hasXposMetadata; channelMetadata.add (metadata); channelMap[channelNum] = numChannels; diff --git a/Plugins/LfpViewer/DisplayBuffer.h b/Plugins/LfpViewer/DisplayBuffer.h index 45ea5b7be..f882ceab9 100644 --- a/Plugins/LfpViewer/DisplayBuffer.h +++ b/Plugins/LfpViewer/DisplayBuffer.h @@ -61,12 +61,16 @@ class TESTABLE DisplayBuffer : public AudioBuffer ContinuousChannel::Type channelType, bool isRecorded, int group = 0, + float xpos = 0.0f, float ypos = 0, String description = "", String structure = "None", float inputRangeMin = -5000.0f, float inputRangeMax = +5000.0f, - String units = ""); + String units = "", + bool hasGroupMetadata = false, + bool hasYposMetadata = false, + bool hasXposMetadata = false); /** Initializes the event channel at the start of each buffer */ void initializeEventChannel (int nSamples); @@ -89,6 +93,7 @@ class TESTABLE DisplayBuffer : public AudioBuffer { String name = ""; int group = 0; + float xpos = 0.0f; float ypos = 0; String structure = "None"; ContinuousChannel::Type type; @@ -97,6 +102,9 @@ class TESTABLE DisplayBuffer : public AudioBuffer float inputRangeMin = -5000.0f; float inputRangeMax = +5000.0f; String units = ""; + bool hasGroupMetadata = false; + bool hasYposMetadata = false; + bool hasXposMetadata = false; }; Array channelMetadata; diff --git a/Plugins/LfpViewer/LfpChannelDisplay.cpp b/Plugins/LfpViewer/LfpChannelDisplay.cpp index 27a8c572e..085504e36 100644 --- a/Plugins/LfpViewer/LfpChannelDisplay.cpp +++ b/Plugins/LfpViewer/LfpChannelDisplay.cpp @@ -819,6 +819,18 @@ void LfpChannelDisplay::setDepth (float depth_) depth = depth_; } +void LfpChannelDisplay::setXpos (float xpos_) +{ + xpos = xpos_; +} + +void LfpChannelDisplay::setMetadataPresence (bool hasGroupMetadata_, bool hasYposMetadata_, bool hasXposMetadata_) +{ + groupMetadataAvailable = hasGroupMetadata_; + yposMetadataAvailable = hasYposMetadata_; + xposMetadataAvailable = hasXposMetadata_; +} + void LfpChannelDisplay::setRecorded (bool recorded_) { isRecorded = recorded_; diff --git a/Plugins/LfpViewer/LfpChannelDisplay.h b/Plugins/LfpViewer/LfpChannelDisplay.h index a90efe7ad..e4abbde6a 100644 --- a/Plugins/LfpViewer/LfpChannelDisplay.h +++ b/Plugins/LfpViewer/LfpChannelDisplay.h @@ -89,6 +89,12 @@ class LfpChannelDisplay : public Component /** Sets the channel depth*/ void setDepth (float); + /** Sets the channel x-position */ + void setXpos (float); + + /** Records which metadata fields were provided for this channel */ + void setMetadataPresence (bool hasGroupMetadata_, bool hasYposMetadata_, bool hasXposMetadata_); + /** Sets whether or not the channel is recorded by an upstream Record Node*/ void setRecorded (bool); @@ -176,6 +182,10 @@ class LfpChannelDisplay : public Component float getDepth() { return depth; } int getGroup() { return group; } + float getXpos() const { return xpos; } + bool hasGroupMetadata() const { return groupMetadataAvailable; } + bool hasYposMetadata() const { return yposMetadataAvailable; } + bool hasXposMetadata() const { return xposMetadataAvailable; } int ifrom, ito, ito_local, ifrom_local; @@ -195,8 +205,12 @@ class LfpChannelDisplay : public Component int drawableChan; String name; - int group; - float depth; + int group = 0; + float depth = 0.0f; + float xpos = 0.0f; + bool groupMetadataAvailable = false; + bool yposMetadataAvailable = false; + bool xposMetadataAvailable = false; bool isRecorded; String units; diff --git a/Plugins/LfpViewer/LfpDisplay.cpp b/Plugins/LfpViewer/LfpDisplay.cpp index 275f32cb4..de1ce60b3 100644 --- a/Plugins/LfpViewer/LfpDisplay.cpp +++ b/Plugins/LfpViewer/LfpDisplay.cpp @@ -47,8 +47,10 @@ #define MS_FROM_START Time::highResolutionTicksToSeconds (Time::getHighResolutionTicks() - start) * 1000 +#include #include #include +#include using namespace LfpViewer; @@ -258,17 +260,31 @@ void LfpDisplay::setColours() { if (colourGrouping.equalsIgnoreCase ("By Shank")) { - /* - depth ranges of electrodes for multishank configurations: - Shank 1: depth < 10000 - Shank 2: 10000 ≤ depth < 20000 - Shank 3: 20000 ≤ depth < 30000 - Shank 4: depth ≥ 30000 - */ - int depth = drawableChannels[i].channel->getDepth(); - int colourIdx = depth < 10000 ? 0 : depth < 20000 ? 2 : depth < 30000 ? 4 : 6; - drawableChannels[i].channel->setColour (getColourSchemePtr()->getColourForIndex (colourIdx)); - drawableChannels[i].channelInfo->setColour (getColourSchemePtr()->getColourForIndex (colourIdx)); + auto* channel = drawableChannels[i].channel; + auto* info = drawableChannels[i].channelInfo; + + if (channel->hasGroupMetadata()) + { + const int colourIdx = (channel->getGroup() * 2) % 8; + channel->setColour (getColourSchemePtr()->getColourForIndex (colourIdx)); + info->setColour (getColourSchemePtr()->getColourForIndex (colourIdx)); + } + else + { + /* + Legacy depth-based shank colouring ranges: + Shank 1: depth < 10000 + Shank 2: 10000 ≤ depth < 20000 + Shank 3: 20000 ≤ depth < 30000 + Shank 4: depth ≥ 30000 + */ + const int depth = channel->getDepth(); + const int colourIdx = depth < 10000 ? 0 : depth < 20000 ? 2 + : depth < 30000 ? 4 + : 6; + channel->setColour (getColourSchemePtr()->getColourForIndex (colourIdx)); + info->setColour (getColourSchemePtr()->getColourForIndex (colourIdx)); + } } else { @@ -281,10 +297,24 @@ void LfpDisplay::setColours() { if (colourGrouping.equalsIgnoreCase ("By Shank")) { - int depth = drawableChannels[0].channel->getDepth(); - int colourIdx = depth < 10000 ? 0 : depth < 20000 ? 2 : depth < 30000 ? 4 : 6; - drawableChannels[0].channel->setColour (getColourSchemePtr()->getColourForIndex (colourIdx)); - drawableChannels[0].channelInfo->setColour (getColourSchemePtr()->getColourForIndex (colourIdx)); + auto* channel = drawableChannels[0].channel; + auto* info = drawableChannels[0].channelInfo; + + if (channel->hasGroupMetadata()) + { + const int colourIdx = channel->getGroup(); + channel->setColour (getColourSchemePtr()->getColourForIndex (colourIdx)); + info->setColour (getColourSchemePtr()->getColourForIndex (colourIdx)); + } + else + { + const int depth = channel->getDepth(); + const int colourIdx = depth < 10000 ? 0 : depth < 20000 ? 2 + : depth < 30000 ? 4 + : 6; + channel->setColour (getColourSchemePtr()->getColourForIndex (colourIdx)); + info->setColour (getColourSchemePtr()->getColourForIndex (colourIdx)); + } } else { @@ -1092,23 +1122,43 @@ void LfpDisplay::rebuildDrawableChannelsList() const int numChannels = channelsToDraw.size(); std::vector depths (numChannels); + std::vector xposValues (numChannels); + std::vector hasYposMetadata (numChannels); + std::vector hasXposMetadata (numChannels); + std::vector groups (numChannels); + std::vector hasGroupMetadata (numChannels); bool allSame = true; + bool anyYposMetadata = false; + bool anyXposMetadata = false; + bool anyGroupMetadata = false; float last = channelsToDraw[0].channelInfo->getDepth(); for (int i = 0; i < channelsToDraw.size(); i++) { - float depth = channelsToDraw[i].channelInfo->getDepth(); + auto* info = channelsToDraw[i].channelInfo; + const float depth = info->getDepth(); if (depth != last) allSame = false; depths[i] = depth; - + xposValues[i] = info->getXpos(); + hasYposMetadata[i] = info->hasYposMetadata(); + hasXposMetadata[i] = info->hasXposMetadata(); + groups[i] = info->getGroup(); + hasGroupMetadata[i] = info->hasGroupMetadata(); + + anyYposMetadata = anyYposMetadata || hasYposMetadata[i]; + anyXposMetadata = anyXposMetadata || hasXposMetadata[i]; + anyGroupMetadata = anyGroupMetadata || hasGroupMetadata[i]; last = depth; } - if (allSame) + const bool positionMetadataAvailable = anyYposMetadata && anyXposMetadata; + const bool groupMetadataAvailable = anyGroupMetadata; + + if (! groupMetadataAvailable && allSame && ! anyYposMetadata) { LOGD ("No depth info found."); } @@ -1118,7 +1168,21 @@ void LfpDisplay::rebuildDrawableChannelsList() std::iota (V.begin(), V.end(), 0); //Initializing sort (V.begin(), V.end(), [&] (int i, int j) - { return depths[i] <= depths[j]; }); + { + const float depthDiff = depths[i] - depths[j]; + const float depthEpsilon = 1.0e-3f; + + if (groupMetadataAvailable && groups[i] != groups[j]) + return groups[i] < groups[j]; + + if (std::abs (depthDiff) >= depthEpsilon) + return depths[i] < depths[j]; + + if (positionMetadataAvailable) + return xposValues[i] < xposValues[j]; + + return i < j; // deterministic fallback + }); Array orderedDrawableChannels; diff --git a/Plugins/LfpViewer/LfpDisplayCanvas.cpp b/Plugins/LfpViewer/LfpDisplayCanvas.cpp index 5d94cfcef..e9d0f9a88 100644 --- a/Plugins/LfpViewer/LfpDisplayCanvas.cpp +++ b/Plugins/LfpViewer/LfpDisplayCanvas.cpp @@ -29,9 +29,9 @@ #include "LfpDisplayNode.h" #include "ShowHideOptionsButton.h" -#include #include #include +#include #define MS_FROM_START Time::highResolutionTicksToSeconds (Time::getHighResolutionTicks() - start) * 1000 @@ -992,6 +992,10 @@ void LfpDisplaySplitter::updateSettings() lfpDisplay->channels[i]->setName (displayBuffer->channelMetadata[i].name); lfpDisplay->channels[i]->setGroup (displayBuffer->channelMetadata[i].group); lfpDisplay->channels[i]->setDepth (displayBuffer->channelMetadata[i].ypos); + lfpDisplay->channels[i]->setXpos (displayBuffer->channelMetadata[i].xpos); + lfpDisplay->channels[i]->setMetadataPresence (displayBuffer->channelMetadata[i].hasGroupMetadata, + displayBuffer->channelMetadata[i].hasYposMetadata, + displayBuffer->channelMetadata[i].hasXposMetadata); lfpDisplay->channels[i]->setRecorded (displayBuffer->channelMetadata[i].isRecorded); lfpDisplay->channels[i]->updateType (displayBuffer->channelMetadata[i].type); lfpDisplay->channels[i]->setUnits (displayBuffer->channelMetadata[i].units); @@ -999,6 +1003,10 @@ void LfpDisplaySplitter::updateSettings() lfpDisplay->channelInfo[i]->setName (displayBuffer->channelMetadata[i].name); lfpDisplay->channelInfo[i]->setGroup (displayBuffer->channelMetadata[i].group); lfpDisplay->channelInfo[i]->setDepth (displayBuffer->channelMetadata[i].ypos); + lfpDisplay->channelInfo[i]->setXpos (displayBuffer->channelMetadata[i].xpos); + lfpDisplay->channelInfo[i]->setMetadataPresence (displayBuffer->channelMetadata[i].hasGroupMetadata, + displayBuffer->channelMetadata[i].hasYposMetadata, + displayBuffer->channelMetadata[i].hasXposMetadata); lfpDisplay->channelInfo[i]->setRecorded (displayBuffer->channelMetadata[i].isRecorded); lfpDisplay->channelInfo[i]->updateType (displayBuffer->channelMetadata[i].type); lfpDisplay->channelInfo[i]->setUnits (displayBuffer->channelMetadata[i].units); diff --git a/Plugins/LfpViewer/LfpDisplayNode.cpp b/Plugins/LfpViewer/LfpDisplayNode.cpp index 752551cd4..a0e2b528c 100644 --- a/Plugins/LfpViewer/LfpDisplayNode.cpp +++ b/Plugins/LfpViewer/LfpDisplayNode.cpp @@ -86,17 +86,39 @@ void LfpDisplayNode::updateSettings() displayBufferMap[streamId]->name = name; } + bool hasGroupMetadata = (! channel->group.name.equalsIgnoreCase ("default")); + + float ypos = channel->position.y; + float xpos = channel->position.x; + bool hasYposMetadata = false; + bool hasXposMetadata = false; + + const int yposMetadataIndex = channel->findMetadata (MetadataDescriptor::MetadataType::FLOAT, 1, "channel.ypos"); + if (yposMetadataIndex >= 0) + { + if (const auto* yposValue = channel->getMetadataValue (yposMetadataIndex)) + { + yposValue->getValue (ypos); + hasYposMetadata = true; + hasXposMetadata = true; + } + } + displayBufferMap[streamId]->addChannel (channel->getName(), // name ch, // index channel->getChannelType(), // type channel->isRecorded, channel->group.number, // group - channel->position.y, // ypos + xpos, + ypos, // ypos channel->getDescription(), "None", // structure channel->inputRange.min, // inputRangeMin channel->inputRange.max, // inputRangeMax - channel->getUnits()); // units + channel->getUnits(), // units + hasGroupMetadata, + hasYposMetadata, + hasXposMetadata); // metadata flags } Array toDelete; From 7321cd18721843c99df5746d174a33508f6b9b31 Mon Sep 17 00:00:00 2001 From: Anjal Doshi Date: Mon, 22 Dec 2025 17:28:24 -0800 Subject: [PATCH 17/26] Add error logging for missing event sample numbers file --- .../FileReader/BinaryFileSource/BinaryFileSource.cpp | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/Source/Processors/FileReader/BinaryFileSource/BinaryFileSource.cpp b/Source/Processors/FileReader/BinaryFileSource/BinaryFileSource.cpp index 99f906526..469b6d520 100644 --- a/Source/Processors/FileReader/BinaryFileSource/BinaryFileSource.cpp +++ b/Source/Processors/FileReader/BinaryFileSource/BinaryFileSource.cpp @@ -199,6 +199,11 @@ void BinaryFileSource::fillRecordInfo() streamName = streamName.trimCharactersAtEnd ("/"); File sampleNumbersFile = m_rootPath.getChildFile ("events").getChildFile (streamName).getChildFile (sampleNumbersFilename); + if (! sampleNumbersFile.existsAsFile()) + { + LOGE ("Sample numbers file not found: ", sampleNumbersFile.getFullPathName(), ". Unable to load events for this stream."); + continue; + } std::unique_ptr sampleNumbersMap (new MemoryMappedFile (sampleNumbersFile, MemoryMappedFile::readOnly)); if (sampleNumbersFile.getSize() == EVENT_HEADER_SIZE_IN_BYTES) From eafde66adae4319d2ba95c067087279c9c9d33cc Mon Sep 17 00:00:00 2001 From: Anjal Doshi Date: Mon, 22 Dec 2025 17:31:38 -0800 Subject: [PATCH 18/26] Add warning messages in FileReader --- Source/Processors/FileReader/FileReader.cpp | 19 ++++++++++++++++++- Source/Processors/FileReader/FileReader.h | 3 +++ .../FileReader/FileReaderActions.cpp | 7 +++---- 3 files changed, 24 insertions(+), 5 deletions(-) diff --git a/Source/Processors/FileReader/FileReader.cpp b/Source/Processors/FileReader/FileReader.cpp index 9816b1587..f025029c0 100644 --- a/Source/Processors/FileReader/FileReader.cpp +++ b/Source/Processors/FileReader/FileReader.cpp @@ -103,6 +103,7 @@ void FileReader::parameterValueChanged (Parameter* p) { if (p->getName() == "selected_file") { + LOGC ("FileReader::parameterValueChanged - selected_file changed to: ", p->getValue().toString()); setFile (p->getValue(), false); } else if (p->getName() == "active_stream") @@ -239,6 +240,8 @@ bool FileReader::setFile (String fullpath, bool shouldUpdateSignalChain) } } + LOGC ("[FileReader] set file: ", fullpath); + //Open file File file (fullpath); @@ -262,12 +265,14 @@ bool FileReader::setFile (String fullpath, bool shouldUpdateSignalChain) if (! input) { LOGE ("Error creating file source for extension ", ext); + showWarningAsync ("Failed to open file", "Error creating file source for extension " + ext); return false; } } else { input = nullptr; + showWarningAsync ("Failed to open file", "File type \"" + ext + "\" not supported"); CoreServices::sendStatusMessage ("File type not supported"); return false; } @@ -275,8 +280,8 @@ bool FileReader::setFile (String fullpath, bool shouldUpdateSignalChain) if (! input->openFile (file)) { input = nullptr; + showWarningAsync ("Invalid file", "The selected file is invalid. Please make sure the file is not corrupted and has valid format."); CoreServices::sendStatusMessage ("Invalid file"); - return false; } @@ -284,6 +289,7 @@ bool FileReader::setFile (String fullpath, bool shouldUpdateSignalChain) if (isEmptyFile) { input = nullptr; + showWarningAsync ("Failed to open file", "Continuous data file is missing or empty."); CoreServices::sendStatusMessage ("Empty file. Ignoring open operation"); return false; @@ -318,6 +324,17 @@ bool FileReader::setFile (String fullpath, bool shouldUpdateSignalChain) return true; } +void FileReader::showWarningAsync (const String& title, const String& message) const +{ + if (headlessMode) + return; + + MessageManager::callAsync ([title, message]() + { AlertWindow::showMessageBoxAsync (AlertWindow::WarningIcon, + title, + message); }); +} + void FileReader::setActiveStream (int index, bool reset) { //Resets the stream to the beginning if reset flag is true diff --git a/Source/Processors/FileReader/FileReader.h b/Source/Processors/FileReader/FileReader.h index 3cd8cfe26..4f66f8605 100644 --- a/Source/Processors/FileReader/FileReader.h +++ b/Source/Processors/FileReader/FileReader.h @@ -242,6 +242,9 @@ class FileReader : public GenericProcessor, /** Returns a new FileSource object for a given file source */ FileSource* createBuiltInFileSource (int index) const; + /** Shows a warning message asynchronously */ + void showWarningAsync (const String& title, const String& message) const; + /** Holds a path to the default file */ File defaultFile; diff --git a/Source/Processors/FileReader/FileReaderActions.cpp b/Source/Processors/FileReader/FileReaderActions.cpp index 1349a0289..971fb543c 100644 --- a/Source/Processors/FileReader/FileReaderActions.cpp +++ b/Source/Processors/FileReader/FileReaderActions.cpp @@ -47,10 +47,9 @@ bool SelectFile::perform() // Set the new path - this will trigger the linked parameter changes pathParam->setNextValue (newPath, false); - // Load the new file - bool success = processor->setFile (newPath, false); - - if (!success) + // Check if the file was loaded successfully + if (processor->getFile().isEmpty() + || ! processor->getFile().equalsIgnoreCase (newPath.toString())) { // If loading the file failed, revert the path parameter to the original path pathParam->setNextValue (originalPath, false); From 69648975059e0c7413f9d2f62fb112f23e6e5be4 Mon Sep 17 00:00:00 2001 From: Anjal Doshi Date: Tue, 23 Dec 2025 17:25:10 -0800 Subject: [PATCH 19/26] Add plugin update check and notification on startup --- Source/MainWindow.cpp | 7 ++ Source/Processors/FileReader/FileReader.cpp | 3 - Source/UI/PluginInstaller.cpp | 78 +++++++++++++++++++++ Source/UI/PluginInstaller.h | 4 ++ Source/UI/UIComponent.cpp | 27 +++++++ Source/UI/UIComponent.h | 3 + 6 files changed, 119 insertions(+), 3 deletions(-) diff --git a/Source/MainWindow.cpp b/Source/MainWindow.cpp index 493049641..53e6e81ba 100644 --- a/Source/MainWindow.cpp +++ b/Source/MainWindow.cpp @@ -244,6 +244,13 @@ MainWindow::MainWindow (const File& fileToLoad, bool isConsoleApp_) : isConsoleA LatestVersionCheckerAndUpdater::getInstance()->checkForNewVersion (true, this); #endif + // Check for plugin updates and notify user if any are available + if (! isConsoleApp) + { + UIComponent* ui = (UIComponent*) documentWindow->getContentComponent(); + ui->checkForPluginUpdates(); + } + Process::setPriority (Process::HighPriority); } diff --git a/Source/Processors/FileReader/FileReader.cpp b/Source/Processors/FileReader/FileReader.cpp index f025029c0..468c99eb1 100644 --- a/Source/Processors/FileReader/FileReader.cpp +++ b/Source/Processors/FileReader/FileReader.cpp @@ -103,7 +103,6 @@ void FileReader::parameterValueChanged (Parameter* p) { if (p->getName() == "selected_file") { - LOGC ("FileReader::parameterValueChanged - selected_file changed to: ", p->getValue().toString()); setFile (p->getValue(), false); } else if (p->getName() == "active_stream") @@ -240,8 +239,6 @@ bool FileReader::setFile (String fullpath, bool shouldUpdateSignalChain) } } - LOGC ("[FileReader] set file: ", fullpath); - //Open file File file (fullpath); diff --git a/Source/UI/PluginInstaller.cpp b/Source/UI/PluginInstaller.cpp index 1c41b671a..f5cbbb94f 100644 --- a/Source/UI/PluginInstaller.cpp +++ b/Source/UI/PluginInstaller.cpp @@ -171,6 +171,84 @@ void PluginInstaller::createXmlFile() } } +int PluginInstaller::checkForPluginUpdates() +{ + LOGD ("Checking for plugin updates..."); + + File xmlFile = getPluginsDirectory().getChildFile ("installedPlugins.xml"); + + XmlDocument doc (xmlFile); + std::unique_ptr xml (doc.getDocumentElement()); + + if (xml == 0 || ! xml->hasTagName ("PluginInstaller")) + { + LOGD ("[PluginInstaller] installedPlugins.xml not found."); + return 0; + } + + auto child = xml->getFirstChildElement(); + + String baseUrl = "https://open-ephys-plugin-gateway.herokuapp.com/"; + String response = URL (baseUrl).readEntireTextStream(); + + if (response.isEmpty()) + { + LOGE ("Unable to fetch plugin updates! Please check your internet connection."); + return 0; + } + + var gatewayData; + Result result = JSON::parse (response, gatewayData); + gatewayData = gatewayData.getProperty ("plugins", var()); + + updatablePlugins.clear(); + + for (auto* e : child->getChildIterator()) + { + String pName = e->getTagName(); + String latestVer; + + // Get latest compatible version for this plugin + for (int i = 0; i < gatewayData.size(); i++) + { + if (gatewayData[i].getProperty ("name", "NULL").toString().equalsIgnoreCase (pName)) + { + auto allVersions = gatewayData[i].getProperty ("versions", "NULL").getArray(); + StringArray compatibleVersions; + + for (String depVersion : *allVersions) + { + String apiVer = depVersion.substring (depVersion.indexOf ("I") + 1); + + if (apiVer.equalsIgnoreCase (String (PLUGIN_API_VER))) + compatibleVersions.add (depVersion); + } + + if (! compatibleVersions.isEmpty()) + { + compatibleVersions.sort (false); + latestVer = compatibleVersions[compatibleVersions.size() - 1]; + } + else + { + latestVer = "0.0.0-API" + String (PLUGIN_API_VER); + } + + break; + } + } + + if (latestVer.isNotEmpty() && latestVer.compareNatural (e->getAttributeValue (0)) > 0) + { + updatablePlugins.add (pName); + LOGD ("Plugin update available: ", pName); + } + } + + LOGD ("Found ", updatablePlugins.size(), " plugin(s) with updates available."); + return updatablePlugins.size(); +} + void PluginInstaller::installPluginAndDependency (const String& plugin, String version) { PluginInfoComponent tempInfoComponent; diff --git a/Source/UI/PluginInstaller.h b/Source/UI/PluginInstaller.h index d8d19955a..51a801982 100644 --- a/Source/UI/PluginInstaller.h +++ b/Source/UI/PluginInstaller.h @@ -50,6 +50,10 @@ class PluginInstaller : public DocumentWindow /** Access method to install a plugin directly without interacting with the Plugin Installer interface*/ void installPluginAndDependency (const String& plugin, String version); + /** Checks for plugin updates in the background and populates updatablePlugins array. + * Returns the number of plugins that have updates available. */ + static int checkForPluginUpdates(); + private: WeakReference::Master masterReference; friend class WeakReference; diff --git a/Source/UI/UIComponent.cpp b/Source/UI/UIComponent.cpp index 3dac3400e..297abd176 100755 --- a/Source/UI/UIComponent.cpp +++ b/Source/UI/UIComponent.cpp @@ -476,6 +476,33 @@ void UIComponent::setUIBusy (bool busy) repaint(); } +void UIComponent::checkForPluginUpdates() +{ + // Run the check on a background thread to avoid blocking the UI + Thread::launch ([this]() + { + int numUpdates = PluginInstaller::checkForPluginUpdates(); + + if (numUpdates > 0) + { + MessageManager::callAsync ([this, numUpdates]() + { + String message = String (numUpdates) + " plugin update" + (numUpdates > 1 ? "s" : "") + + " available. Open the Plugin Installer to update."; + + AttributedString s; + s.setText (message); + s.setColour (findColour (ThemeColours::defaultText)); + s.setJustification (Justification::left); + s.setWordWrap (AttributedString::WordWrap::byWord); + s.setFont (FontOptions ("Inter", "Regular", 16.0f)); + + bubbleMsgComponent->showAt ({5, 5, 195, 32}, s, 4000); + }); + } + }); +} + void UIComponent::showBubbleMessage (Component* component, const String& message) { AttributedString s; diff --git a/Source/UI/UIComponent.h b/Source/UI/UIComponent.h index 2b4724398..d05254306 100755 --- a/Source/UI/UIComponent.h +++ b/Source/UI/UIComponent.h @@ -210,6 +210,9 @@ class UIComponent : public Component, /** Sets the busy state of the UIComponent */ void setUIBusy (bool busy); + /** Checks for plugin updates and shows a bubble message if any are available */ + void checkForPluginUpdates(); + private: ScopedPointer dataViewport; ScopedPointer signalChainTabComponent; From ced20df945a1233af387e7dfeda6dd88e4ed717f Mon Sep 17 00:00:00 2001 From: Anjal Doshi Date: Wed, 24 Dec 2025 11:47:35 -0800 Subject: [PATCH 20/26] Modify plugin update notification message --- Source/UI/UIComponent.cpp | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/Source/UI/UIComponent.cpp b/Source/UI/UIComponent.cpp index 297abd176..b5d992b65 100755 --- a/Source/UI/UIComponent.cpp +++ b/Source/UI/UIComponent.cpp @@ -487,9 +487,9 @@ void UIComponent::checkForPluginUpdates() { MessageManager::callAsync ([this, numUpdates]() { - String message = String (numUpdates) + " plugin update" + (numUpdates > 1 ? "s" : "") - + " available. Open the Plugin Installer to update."; - + String message = String (numUpdates) + " plugin update" + + (numUpdates > 1 ? "s" : "") + " available"; + AttributedString s; s.setText (message); s.setColour (findColour (ThemeColours::defaultText)); @@ -499,8 +499,7 @@ void UIComponent::checkForPluginUpdates() bubbleMsgComponent->showAt ({5, 5, 195, 32}, s, 4000); }); - } - }); + } }); } void UIComponent::showBubbleMessage (Component* component, const String& message) From 8553e282e92bb90ea2075ff78c1b043f18150880 Mon Sep 17 00:00:00 2001 From: Anjal Doshi Date: Fri, 9 Jan 2026 12:38:23 -0800 Subject: [PATCH 21/26] Hide console window only when launched by double-clicking, not from CLI --- Source/MainWindow.cpp | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/Source/MainWindow.cpp b/Source/MainWindow.cpp index 53e6e81ba..7aa098152 100644 --- a/Source/MainWindow.cpp +++ b/Source/MainWindow.cpp @@ -142,7 +142,17 @@ MainWindow::MainWindow (const File& fileToLoad, bool isConsoleApp_) : isConsoleA #ifdef JUCE_WINDOWS documentWindow->setUsingNativeTitleBar (false); #ifdef NDEBUG - ShowWindow (GetConsoleWindow(), SW_HIDE); + // Only hide console if launched by double-clicking (not from CLI) + // Check if console is attached to a parent process (CLI) or standalone + DWORD processList[2]; + DWORD processCount = GetConsoleProcessList (processList, 2); + + // If only 1 process (this app), it was launched by double-clicking + // If more than 1, it was launched from an existing console (CLI) + if (processCount == 1) + { + ShowWindow (GetConsoleWindow(), SW_HIDE); + } #endif #else documentWindow->setUsingNativeTitleBar (true); // Use native title bar on Mac and Linux From eeaaa4f1a254e7112dc27c23c5f1c7902cc986a8 Mon Sep 17 00:00:00 2001 From: Anjal Doshi Date: Fri, 9 Jan 2026 14:41:46 -0800 Subject: [PATCH 22/26] Fix MaskChannelsParameter value copying when actual channel count exceeds saved count --- Source/Processors/Parameter/ParameterCollection.cpp | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/Source/Processors/Parameter/ParameterCollection.cpp b/Source/Processors/Parameter/ParameterCollection.cpp index 1a1ee6fd9..8e5d8e7fe 100644 --- a/Source/Processors/Parameter/ParameterCollection.cpp +++ b/Source/Processors/Parameter/ParameterCollection.cpp @@ -96,6 +96,7 @@ void ParameterCollection::copyParameterValuesTo (ParameterOwner* pOwner) { MaskChannelsParameter* targetMaskParam = (MaskChannelsParameter*) targetParam; int targetChannelCount = targetMaskParam->getChannelCount(); + const int savedChannelCount = owner.channel_count; Array filteredValues; for (int i = 0; i < parameter->getValue().getArray()->size(); i++) @@ -104,6 +105,15 @@ void ParameterCollection::copyParameterValuesTo (ParameterOwner* pOwner) if (channelIndex >= 0 && channelIndex < targetChannelCount) filteredValues.add (channelIndex); } + + // Auto-include any channels that exist on the device but were absent in the + // saved configuration (e.g., saved with fewer channels than currently available). + if (targetChannelCount > savedChannelCount) + { + for (int ch = savedChannelCount; ch < targetChannelCount; ++ch) + filteredValues.addIfNotAlreadyThere (ch); + } + targetParam->currentValue = filteredValues; } else if (parameter->getType() == Parameter::SELECTED_CHANNELS_PARAM) From ce3e4f18014d4246491c83f00094cb84aa392bba Mon Sep 17 00:00:00 2001 From: Anjal Doshi Date: Fri, 9 Jan 2026 14:47:29 -0800 Subject: [PATCH 23/26] Optimize `SelectedChannelsParameter::setChannelCount` logic --- Source/Processors/Parameter/Parameter.cpp | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/Source/Processors/Parameter/Parameter.cpp b/Source/Processors/Parameter/Parameter.cpp index e50a9d6a1..bf246e8b5 100755 --- a/Source/Processors/Parameter/Parameter.cpp +++ b/Source/Processors/Parameter/Parameter.cpp @@ -859,13 +859,10 @@ void SelectedChannelsParameter::setChannelCount (int newCount) } else if (channelCount == 0 && currentValue.getArray()->size() == 0) // If the current count is 0, set the selected channels to the first maxSelectableChannels channels { - for (int i = 0; i < maxSelectableChannels; i++) - { - if (i < newCount) - { - values.add (i); - } - } + const int limit = jmin (maxSelectableChannels, newCount); + values.ensureStorageAllocated (limit); + for (int i = 0; i < limit; ++i) + values.add (i); currentValue = values; } From 101fec178f7b154db16d90f79941593b8cf19665 Mon Sep 17 00:00:00 2001 From: Anjal Doshi Date: Tue, 13 Jan 2026 14:37:25 -0800 Subject: [PATCH 24/26] Add validation checks for channel count and file size in BinaryFileSource --- Plugins/LfpViewer/LfpDisplayOptions.cpp | 5 +++++ .../BinaryFileSource/BinaryFileSource.cpp | 15 +++++++++++++++ Source/Processors/FileReader/FileReader.cpp | 2 +- 3 files changed, 21 insertions(+), 1 deletion(-) diff --git a/Plugins/LfpViewer/LfpDisplayOptions.cpp b/Plugins/LfpViewer/LfpDisplayOptions.cpp index dc2fe01e0..d544a1dda 100644 --- a/Plugins/LfpViewer/LfpDisplayOptions.cpp +++ b/Plugins/LfpViewer/LfpDisplayOptions.cpp @@ -1268,6 +1268,11 @@ void LfpDisplayOptions::comboBoxChanged (ComboBox* cb) selectedVoltageRange[selectedChannelType] = cb->getSelectedId(); selectedVoltageRangeValues[selectedChannelType] = cb->getText(); canvasSplit->redraw(); + + if (selectedChannelType == ContinuousChannel::Type::AUX && isAuxAutoScaleEnabled()) + rangeSelectionLabel->setText ("Range", dontSendNotification); + else + rangeSelectionLabel->setText ("Range (" + rangeUnits[selectedChannelType] + ")", dontSendNotification); } else if (cb == spreadSelection.get()) { diff --git a/Source/Processors/FileReader/BinaryFileSource/BinaryFileSource.cpp b/Source/Processors/FileReader/BinaryFileSource/BinaryFileSource.cpp index 469b6d520..75e633861 100644 --- a/Source/Processors/FileReader/BinaryFileSource/BinaryFileSource.cpp +++ b/Source/Processors/FileReader/BinaryFileSource/BinaryFileSource.cpp @@ -131,6 +131,21 @@ void BinaryFileSource::fillRecordInfo() } int numChannels = record[idNumChannels]; + + // if numchannels is not equal to the size of channels var, skip this record + if (numChannels != channels.size()) + { + LOGE ("Number of channels mismatch in stream: ", streamName); + continue; + } + + // if numSamples is not a whole number, skip this record + if (dataFile.getSize() % (numChannels * sizeof (int16)) != 0) + { + LOGE ("File size is not consistent with number of channels in stream: ", streamName); + continue; + } + int64 numSamples = (dataFile.getSize() / numChannels) / sizeof (int16); info.name = streamName; diff --git a/Source/Processors/FileReader/FileReader.cpp b/Source/Processors/FileReader/FileReader.cpp index 468c99eb1..a4a12116d 100644 --- a/Source/Processors/FileReader/FileReader.cpp +++ b/Source/Processors/FileReader/FileReader.cpp @@ -286,7 +286,7 @@ bool FileReader::setFile (String fullpath, bool shouldUpdateSignalChain) if (isEmptyFile) { input = nullptr; - showWarningAsync ("Failed to open file", "Continuous data file is missing or empty."); + showWarningAsync ("Failed to open file", "Continuous data file is missing, empty, or invalid."); CoreServices::sendStatusMessage ("Empty file. Ignoring open operation"); return false; From be3918ea27c7f4769a8e2305da4f82ae837e831c Mon Sep 17 00:00:00 2001 From: Anjal Doshi Date: Thu, 15 Jan 2026 12:09:58 -0800 Subject: [PATCH 25/26] Fix Audio Monitor resetting selected channels on update when no spike channel is selected --- Source/Processors/AudioMonitor/AudioMonitor.cpp | 3 ++- Source/Processors/Parameter/Parameter.cpp | 4 +++- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/Source/Processors/AudioMonitor/AudioMonitor.cpp b/Source/Processors/AudioMonitor/AudioMonitor.cpp index 8f9658ed4..68f7a0bd2 100644 --- a/Source/Processors/AudioMonitor/AudioMonitor.cpp +++ b/Source/Processors/AudioMonitor/AudioMonitor.cpp @@ -169,7 +169,8 @@ void AudioMonitor::updateSettings() CategoricalParameter* spikeChanParam = (CategoricalParameter*) stream->getParameter ("spike_channel"); spikeChanParam->setCategories (spikeChannelNames); - parameterValueChanged (stream->getParameter ("spike_channel")); + if (spikeChanParam->getSelectedIndex() > 0) + parameterValueChanged (stream->getParameter ("spike_channel")); } } diff --git a/Source/Processors/Parameter/Parameter.cpp b/Source/Processors/Parameter/Parameter.cpp index bf246e8b5..ceed52f97 100755 --- a/Source/Processors/Parameter/Parameter.cpp +++ b/Source/Processors/Parameter/Parameter.cpp @@ -857,7 +857,9 @@ void SelectedChannelsParameter::setChannelCount (int newCount) currentValue = values; } - else if (channelCount == 0 && currentValue.getArray()->size() == 0) // If the current count is 0, set the selected channels to the first maxSelectableChannels channels + else if (channelCount == 0 + && currentValue.getArray()->size() == 0 + && maxSelectableChannels < std::numeric_limits::max()) // If the current count is 0, set the selected channels to the first maxSelectableChannels channels { const int limit = jmin (maxSelectableChannels, newCount); values.ensureStorageAllocated (limit); From 09605fcc692b7681537424fb8d113fedde06c905 Mon Sep 17 00:00:00 2001 From: Anjal Doshi Date: Thu, 15 Jan 2026 15:10:44 -0800 Subject: [PATCH 26/26] Bump version to 1.0.2 --- CMakeLists.txt | 2 +- README.md | 8 ++++---- .../Installers/Linux/Open-Ephys_Installer/DEBIAN/control | 4 ++-- .../Linux/Open-Ephys_Installer/DEBIAN/copyright | 2 +- Resources/Installers/Windows/windows_installer_script.iss | 6 +++--- 5 files changed, 11 insertions(+), 11 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 2d6c76605..f579ea4ee 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,7 +1,7 @@ #Open Ephys GUI main build file cmake_minimum_required(VERSION 3.15) -set(GUI_VERSION 1.0.1) +set(GUI_VERSION 1.0.2) string(REGEX MATCHALL "[0-9]+" VERSION_LIST ${GUI_VERSION}) list(LENGTH VERSION_LIST num_version_components) diff --git a/README.md b/README.md index 80db39cb5..c5908f5e2 100644 --- a/README.md +++ b/README.md @@ -36,11 +36,11 @@ Our primary user base is scientists performing electrophysiology experiments wit The easiest way to get started is to download the installer for your platform of choice: -- [Windows](https://openephys.jfrog.io/artifactory/GUI-binaries/Release-Installer/windows/Install-Open-Ephys-GUI-v1.0.1.exe) -- [Ubuntu/Debian](https://openephys.jfrog.io/artifactory/GUI-binaries/Release-Installer/linux/open-ephys-gui-v1.0.1.deb) -- [macOS](https://openephys.jfrog.io/artifactory/GUI-binaries/Release-Installer/mac/Open_Ephys_GUI_v1.0.1.dmg) +- [Windows](https://openephys.jfrog.io/artifactory/GUI-binaries/Release-Installer/windows/Install-Open-Ephys-GUI-v1.0.2.exe) +- [Ubuntu/Debian](https://openephys.jfrog.io/artifactory/GUI-binaries/Release-Installer/linux/open-ephys-gui-v1.0.2.deb) +- [macOS](https://openephys.jfrog.io/artifactory/GUI-binaries/Release-Installer/mac/Open_Ephys_GUI_v1.0.2.dmg) -It’s also possible to obtain the binaries as a .zip file for [Windows](https://openephys.jfrog.io/artifactory/GUI-binaries/Release/windows/open-ephys-v1.0.1-windows.zip), [Linux](https://openephys.jfrog.io/artifactory/GUI-binaries/Release/linux/open-ephys-v1.0.1-linux.zip), or [Mac](https://openephys.jfrog.io/artifactory/GUI-binaries/Release/mac/open-ephys-v1.0.1-mac.zip). +It’s also possible to obtain the binaries as a .zip file for [Windows](https://openephys.jfrog.io/artifactory/GUI-binaries/Release/windows/open-ephys-v1.0.2-windows.zip), [Linux](https://openephys.jfrog.io/artifactory/GUI-binaries/Release/linux/open-ephys-v1.0.2-linux.zip), or [Mac](https://openephys.jfrog.io/artifactory/GUI-binaries/Release/mac/open-ephys-v1.0.2-mac.zip). Detailed installation instructions can be found [here](https://open-ephys.github.io/gui-docs/User-Manual/Installing-the-GUI.html). diff --git a/Resources/Installers/Linux/Open-Ephys_Installer/DEBIAN/control b/Resources/Installers/Linux/Open-Ephys_Installer/DEBIAN/control index 4aa44268e..3cf25e17b 100644 --- a/Resources/Installers/Linux/Open-Ephys_Installer/DEBIAN/control +++ b/Resources/Installers/Linux/Open-Ephys_Installer/DEBIAN/control @@ -1,9 +1,9 @@ Package: open-ephys -Version: 1.0.1 +Version: 1.0.2 Architecture: amd64 Installed-Size: 18644 Section: science Priority: optional -Maintainer: Open Ephys +Maintainer: Open Ephys Homepage: https://open-ephys.org/gui Description: Software for processing, recording, and visualizing multichannel electrophysiology data. diff --git a/Resources/Installers/Linux/Open-Ephys_Installer/DEBIAN/copyright b/Resources/Installers/Linux/Open-Ephys_Installer/DEBIAN/copyright index 36ef27be3..9fa74860d 100644 --- a/Resources/Installers/Linux/Open-Ephys_Installer/DEBIAN/copyright +++ b/Resources/Installers/Linux/Open-Ephys_Installer/DEBIAN/copyright @@ -3,7 +3,7 @@ Upstream-Name: Open Ephys GUI Source: https://github.com/open-ephys/plugin-GUI/ Files: * -Copyright: 2025 Open Ephys +Copyright: 2026 Open Ephys License: GPL-3+ 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 diff --git a/Resources/Installers/Windows/windows_installer_script.iss b/Resources/Installers/Windows/windows_installer_script.iss index 38e0d8329..2f9167c8c 100644 --- a/Resources/Installers/Windows/windows_installer_script.iss +++ b/Resources/Installers/Windows/windows_installer_script.iss @@ -1,9 +1,9 @@ [Setup] AppId=Open Ephys AppName=Open Ephys GUI -AppVersion=1.0.1 -AppVerName=Open Ephys GUI 1.0.1 -AppCopyright=Copyright (C) 2010-2025, Open Ephys & Contributors +AppVersion=1.0.2 +AppVerName=Open Ephys GUI 1.0.2 +AppCopyright=Copyright (C) 2010-2026, Open Ephys & Contributors AppPublisher=open-ephys.org AppPublisherURL=https://open-ephys.org/gui DefaultDirName={autopf}\Open Ephys