Skip to content

Commit f61af32

Browse files
committed
Add high-pass filtering and common average referencing to LFP Viewer
- Introduced LfpViewerProcessing class for handling high-pass filtering and CAR. - Implemented high-pass filter functionality with SIMD optimization. - Added UI elements for enabling/disabling high-pass filter and CAR in LfpDisplayOptions. - Updated LfpDisplayCanvas to manage processing states and buffers. - Enhanced LfpDisplayNode to detect Neuropixels probes and configure ADC counts for CAR.
1 parent 88a98d7 commit f61af32

9 files changed

Lines changed: 1235 additions & 3 deletions

Plugins/LfpViewer/CMakeLists.txt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,8 @@ add_sources(
3333
LfpTimescale.h
3434
LfpViewport.cpp
3535
LfpViewport.h
36+
LfpViewerProcessing.cpp
37+
LfpViewerProcessing.h
3638
PerPixelBitmapPlotter.cpp
3739
PerPixelBitmapPlotter.h
3840
ShowHideOptionsButton.cpp

Plugins/LfpViewer/DisplayBuffer.h

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -126,6 +126,9 @@ class TESTABLE DisplayBuffer : public AudioBuffer<float>
126126

127127
float sampleRate;
128128

129+
/** Number of Neuropixels ADCs (0 if not a Neuropixels probe) */
130+
int numAdcs = 0;
131+
129132
uint64 ttlState;
130133

131134
int triggerChannel;

Plugins/LfpViewer/LfpDisplayCanvas.cpp

Lines changed: 236 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@
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+
699702
LfpDisplaySplitter::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

869874
void LfpDisplaySplitter::timerCallback()
870875
{
871-
refresh();
876+
if (this->isShowing())
877+
refresh();
872878
}
873879

874880
void 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

17761999
void 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

17912024
void LfpDisplaySplitter::comboBoxChanged (juce::ComboBox* comboBox)

0 commit comments

Comments
 (0)