2727#include " DisplayBuffer.h"
2828#include " LfpChannelDisplayInfo.h"
2929#include " LfpDisplayNode.h"
30+ #include " LfpViewerProcessing.h"
3031#include " ShowHideOptionsButton.h"
3132
3233#include < algorithm>
@@ -696,6 +697,8 @@ void LfpDisplayCanvas::loadCustomParametersFromXml (XmlElement* xml)
696697 // LOGD(" Resized in ", MS_FROM_START, " milliseconds");
697698}
698699
700+ static int frameCounter = 0 ;
701+
699702LfpDisplaySplitter::LfpDisplaySplitter (LfpDisplayNode* node,
700703 LfpDisplayCanvas* canvas_,
701704 DisplayBuffer* db,
@@ -740,6 +743,8 @@ LfpDisplaySplitter::LfpDisplaySplitter (LfpDisplayNode* node,
740743 isLoading = true ;
741744 isUpdating = false ;
742745
746+ viewerProcessing = std::make_unique<LfpViewerProcessing>();
747+
743748 displayBuffer = nullptr ;
744749}
745750
@@ -854,7 +859,7 @@ void LfpDisplaySplitter::beginAnimation()
854859 // Base: 50Hz (20ms), >384 channels: ~30Hz (33ms)
855860 int refreshIntervalMs = 20 ;
856861 if (nChans > 384 )
857- refreshIntervalMs = 33 ; // ~30Hz for 256+ channels
862+ refreshIntervalMs = 33 ;
858863
859864 startTimer (refreshIntervalMs);
860865
@@ -868,7 +873,8 @@ void LfpDisplaySplitter::endAnimation()
868873
869874void LfpDisplaySplitter::timerCallback ()
870875{
871- refresh ();
876+ if (this ->isShowing ())
877+ refresh ();
872878}
873879
874880void LfpDisplaySplitter::monitorChannel (int chan)
@@ -1025,6 +1031,24 @@ void LfpDisplaySplitter::updateSettings()
10251031 }
10261032 }
10271033
1034+ // Prepare the viewer processing pipeline with the new channel/stream info
1035+ if (viewerProcessing != nullptr && displayBuffer != nullptr )
1036+ {
1037+ Array<ContinuousChannel::Type> channelTypes;
1038+ for (int i = 0 ; i < nChans; i++)
1039+ channelTypes.add (displayBuffer->channelMetadata [i].type );
1040+
1041+ viewerProcessing->prepare (nChans, sampleRate, channelTypes);
1042+ viewerProcessing->setNeuropixelsAdcCount (displayBuffer->numAdcs );
1043+
1044+ // Reset the processed buffer so it will be re-created
1045+ processedDisplayBuffer.reset ();
1046+ processedBufferIndex.clear ();
1047+ }
1048+
1049+ // Update the CAR label to show NP-CAR if applicable
1050+ options->updateCARLabel ();
1051+
10281052 lfpDisplay->rebuildDrawableChannelsList (); // calls setColours(), which calls refresh
10291053
10301054 isLoading = false ;
@@ -1092,6 +1116,7 @@ void LfpDisplaySplitter::refreshScreenBuffer()
10921116 screenBufferMin->setSize (nChans, screenBufferWidth);
10931117 screenBufferMean->setSize (nChans, screenBufferWidth);
10941118 screenBufferMax->setSize (nChans, screenBufferWidth);
1119+ frameCounter = 0 ;
10951120 }
10961121
10971122 // std::cout << "Display " << splitID << " setting screen buffer width to " << screenBufferWidth << std::endl;
@@ -1140,6 +1165,16 @@ void LfpDisplaySplitter::syncDisplayBuffer()
11401165 leftOverSamples.set (channel, 0 .0f );
11411166 }
11421167
1168+ // Sync processed buffer indices as well
1169+ processedBufferIndex.clear ();
1170+ for (int channel = 0 ; channel <= nChans; channel++)
1171+ {
1172+ processedBufferIndex.add (displayBuffer->displayBufferIndices [channel]);
1173+ }
1174+
1175+ if (viewerProcessing != nullptr )
1176+ viewerProcessing->reset ();
1177+
11431178 samplesPerBufferPass = 0 ;
11441179}
11451180
@@ -1159,6 +1194,20 @@ void LfpDisplaySplitter::updateScreenBuffer()
11591194{
11601195 if (isVisible () && displayBuffer != nullptr && ! isUpdating)
11611196 {
1197+ // Performance timing (log every 100 frames when filtering is active)
1198+ const int64 startTime = Time::getHighResolutionTicks ();
1199+ int64 preprocessTime = 0 ;
1200+
1201+ // Preprocess new samples through filter/CAR if active
1202+ const int64 preprocessStart = Time::getHighResolutionTicks ();
1203+ preprocessNewSamples ();
1204+ preprocessTime = Time::getHighResolutionTicks () - preprocessStart;
1205+
1206+ // Determine whether to read from the processed shadow buffer or the raw display buffer
1207+ const bool useProcessedBuffer = viewerProcessing != nullptr
1208+ && viewerProcessing->isActive ()
1209+ && processedDisplayBuffer != nullptr ;
1210+
11621211 // std::cout << "Update screen buffer" << std::endl;
11631212
11641213 const auto triggerTimeOpt = triggerChannel >= 0
@@ -1319,7 +1368,10 @@ void LfpDisplaySplitter::updateScreenBuffer()
13191368 float i;
13201369
13211370 // Get raw pointers for this channel to avoid repeated getSample() calls
1322- const float * displayData = displayBuffer->getReadPointer (channel);
1371+ // Use the processed shadow buffer for data channels when filter/CAR is active
1372+ const float * displayData = (useProcessedBuffer && channel < nChans)
1373+ ? processedDisplayBuffer->getReadPointer (channel)
1374+ : displayBuffer->getReadPointer (channel);
13231375 float * meanWritePtr = (channel < nChans) ? screenBufferMean->getWritePointer (channel) : nullptr ;
13241376 float * minWritePtr = (channel < nChans) ? screenBufferMin->getWritePointer (channel) : nullptr ;
13251377 float * maxWritePtr = (channel < nChans) ? screenBufferMax->getWritePointer (channel) : nullptr ;
@@ -1585,6 +1637,177 @@ void LfpDisplaySplitter::updateScreenBuffer()
15851637 displayBufferIndex.set (channel, newDisplayBufferIndex); // need to store this locally
15861638 }
15871639 }
1640+
1641+ // // Performance logging (every 100 frames when processing is active)
1642+ // if (++frameCounter >= 100)
1643+ // {
1644+ // frameCounter = 0;
1645+ // const int64 totalTime = Time::getHighResolutionTicks() - startTime;
1646+ // const double preprocessMs = Time::highResolutionTicksToSeconds (preprocessTime) * 1000.0;
1647+ // const double totalMs = Time::highResolutionTicksToSeconds (totalTime) * 1000.0;
1648+ // const double renderMs = totalMs - preprocessMs;
1649+
1650+ // std::cout << "[LFP Performance] Split " << splitID
1651+ // << ": Total=" << String (totalMs, 2) << "ms"
1652+ // << " | Preprocess=" << String (preprocessMs, 2) << "ms ("
1653+ // << String (preprocessMs / totalMs * 100.0, 1) << "%)"
1654+ // << " | Render=" << String (renderMs, 2) << "ms ("
1655+ // << String (renderMs / totalMs * 100.0, 1) << "%)"
1656+ // << " | Channels=" << nChans
1657+ // << " | HP=" << (viewerProcessing->isHighPassEnabled() ? "ON" : "OFF")
1658+ // << " | CAR=" << (viewerProcessing->isCAREnabled() ? "ON" : "OFF")
1659+ // << std::endl;
1660+ // }
1661+ }
1662+ }
1663+
1664+ void LfpDisplaySplitter::preprocessNewSamples ()
1665+ {
1666+ if (viewerProcessing == nullptr || displayBuffer == nullptr )
1667+ return ;
1668+
1669+ if (! viewerProcessing->isActive ())
1670+ return ;
1671+
1672+ if (nChans <= 0 )
1673+ return ;
1674+
1675+ // Ensure the shadow buffer exists and is correctly sized
1676+ if (processedDisplayBuffer == nullptr
1677+ || processedDisplayBuffer->getNumChannels () != nChans + 1
1678+ || processedDisplayBuffer->getNumSamples () != displayBufferSize)
1679+ {
1680+ processedDisplayBuffer = std::make_unique<AudioBuffer<float >> (nChans + 1 , displayBufferSize);
1681+ processedDisplayBuffer->clear ();
1682+ processedBufferIndex.clear ();
1683+
1684+ // Initialize to current write head so no stale data is processed
1685+ for (int ch = 0 ; ch <= nChans; ch++)
1686+ processedBufferIndex.add (displayBuffer->displayBufferIndices [ch]);
1687+
1688+ return ;
1689+ }
1690+
1691+ // Determine how many new samples are available (use channel 0 as reference for data channels)
1692+ int oldIdx = processedBufferIndex[0 ];
1693+ int newIdx = displayBuffer->displayBufferIndices [0 ];
1694+ int newSamples = newIdx - oldIdx;
1695+
1696+ if (newSamples < 0 )
1697+ newSamples += displayBufferSize;
1698+
1699+ if (newSamples == 0 )
1700+ return ;
1701+
1702+ if (newSamples > displayBufferSize)
1703+ newSamples = displayBufferSize;
1704+
1705+ // Resize temp buffer if needed (reuses memory between calls)
1706+ if (tempProcessingBuffer.getNumChannels () != nChans
1707+ || tempProcessingBuffer.getNumSamples () < newSamples)
1708+ {
1709+ tempProcessingBuffer.setSize (nChans, newSamples, false , false , true );
1710+ }
1711+
1712+ // Copy new samples from circular displayBuffer into the contiguous temp buffer
1713+ for (int ch = 0 ; ch < nChans; ch++)
1714+ {
1715+ const float * src = displayBuffer->getReadPointer (ch);
1716+ float * dst = tempProcessingBuffer.getWritePointer (ch);
1717+
1718+ int srcStart = oldIdx;
1719+ int firstChunk = jmin (newSamples, displayBufferSize - srcStart);
1720+
1721+ std::memcpy (dst, src + srcStart, firstChunk * sizeof (float ));
1722+
1723+ if (firstChunk < newSamples)
1724+ {
1725+ std::memcpy (dst + firstChunk, src, (newSamples - firstChunk) * sizeof (float ));
1726+ }
1727+ }
1728+
1729+ // Apply high-pass filter (per channel, in-place on temp buffer)
1730+ if (viewerProcessing->isHighPassEnabled ())
1731+ {
1732+ viewerProcessing->applyHighPass (tempProcessingBuffer, newSamples, nChans);
1733+ }
1734+
1735+ // Apply CAR (across channels, in-place on temp buffer)
1736+ if (viewerProcessing->isCAREnabled ())
1737+ {
1738+ viewerProcessing->applyCAR (tempProcessingBuffer, newSamples, nChans);
1739+ }
1740+
1741+ // Write processed data back to the shadow circular buffer
1742+ for (int ch = 0 ; ch < nChans; ch++)
1743+ {
1744+ const float * src = tempProcessingBuffer.getReadPointer (ch);
1745+ float * dst = processedDisplayBuffer->getWritePointer (ch);
1746+
1747+ int dstStart = oldIdx;
1748+ int firstChunk = jmin (newSamples, displayBufferSize - dstStart);
1749+
1750+ std::memcpy (dst + dstStart, src, firstChunk * sizeof (float ));
1751+
1752+ if (firstChunk < newSamples)
1753+ {
1754+ std::memcpy (dst, src + firstChunk, (newSamples - firstChunk) * sizeof (float ));
1755+ }
1756+ }
1757+
1758+ // Copy event channel directly (no processing applied)
1759+ {
1760+ const float * src = displayBuffer->getReadPointer (nChans);
1761+ float * dst = processedDisplayBuffer->getWritePointer (nChans);
1762+
1763+ int evtOldIdx = processedBufferIndex[nChans];
1764+ int evtNewIdx = displayBuffer->displayBufferIndices [nChans];
1765+ int evtNewSamples = evtNewIdx - evtOldIdx;
1766+
1767+ if (evtNewSamples < 0 )
1768+ evtNewSamples += displayBufferSize;
1769+
1770+ if (evtNewSamples > 0 && evtNewSamples <= displayBufferSize)
1771+ {
1772+ int firstChunk = jmin (evtNewSamples, displayBufferSize - evtOldIdx);
1773+ std::memcpy (dst + evtOldIdx, src + evtOldIdx, firstChunk * sizeof (float ));
1774+
1775+ if (firstChunk < evtNewSamples)
1776+ {
1777+ std::memcpy (dst, src, (evtNewSamples - firstChunk) * sizeof (float ));
1778+ }
1779+ }
1780+ }
1781+
1782+ // Update processed indices
1783+ for (int ch = 0 ; ch <= nChans; ch++)
1784+ {
1785+ processedBufferIndex.set (ch, displayBuffer->displayBufferIndices [ch]);
1786+ }
1787+ }
1788+
1789+ void LfpDisplaySplitter::setHighPassFilterEnabled (bool enabled)
1790+ {
1791+ if (viewerProcessing != nullptr )
1792+ {
1793+ viewerProcessing->setHighPassEnabled (enabled);
1794+
1795+ // Reset filter states and shadow buffer on toggle
1796+ viewerProcessing->reset ();
1797+ processedDisplayBuffer.reset ();
1798+ processedBufferIndex.clear ();
1799+ }
1800+ }
1801+
1802+ void LfpDisplaySplitter::setCAREnabled (bool enabled)
1803+ {
1804+ if (viewerProcessing != nullptr )
1805+ {
1806+ viewerProcessing->setCAREnabled (enabled);
1807+
1808+ // Reset shadow buffer on toggle
1809+ processedDisplayBuffer.reset ();
1810+ processedBufferIndex.clear ();
15881811 }
15891812}
15901813
@@ -1775,6 +1998,7 @@ void LfpDisplaySplitter::visibleAreaChanged()
17751998
17761999void LfpDisplaySplitter::refresh ()
17772000{
2001+ const int64 startTime = Time::getHighResolutionTicks ();
17782002 updateScreenBuffer ();
17792003
17802004 if (shouldRebuildChannelList)
@@ -1786,6 +2010,15 @@ void LfpDisplaySplitter::refresh()
17862010 {
17872011 lfpDisplay->refresh (); // redraws only the new part of the screen buffer, unless fullredraw is set to true
17882012 }
2013+
2014+ if (frameCounter++ >= 100 )
2015+ {
2016+ frameCounter = 0 ;
2017+ const int64 totalTime = Time::getHighResolutionTicks () - startTime;
2018+ const double totalMs = Time::highResolutionTicksToSeconds (totalTime) * 1000.0 ;
2019+
2020+ std::cout << " LFP Display Split " << splitID << " refresh time: " << String (totalMs, 2 ) << " ms" << std::endl;
2021+ }
17892022}
17902023
17912024void LfpDisplaySplitter::comboBoxChanged (juce::ComboBox* comboBox)
0 commit comments