From 782980f94377b9b6f502f0b0d801ae478883350b Mon Sep 17 00:00:00 2001 From: Ethan Blackwood Date: Wed, 10 Nov 2021 01:39:38 -0500 Subject: [PATCH] Revert "ASIC commit 1" This reverts commit 21f7ef4f683465f3091538235a050e39d8cd0e25. --- ASICPhaseCalculator/Source/HTransformers.cpp | 255 ------- ASICPhaseCalculator/backupSource.zip | Bin 37024 -> 0 bytes .../Build/.gitignore | 0 .../CMAKE_README.txt | 0 .../CMakeLists.txt | 0 .../Source/ARModeler.h | 0 PhaseCalculator/Source/HTransformers.cpp | 146 ++++ .../Source/HTransformers.h | 51 +- .../Source/OpenEphysLib.cpp | 2 +- .../Source/PhaseCalculator.cpp | 623 +++++++++--------- .../Source/PhaseCalculator.h | 62 +- .../Source/PhaseCalculatorCanvas.cpp | 0 .../Source/PhaseCalculatorCanvas.h | 0 .../Source/PhaseCalculatorEditor.cpp | 156 ++++- .../Source/PhaseCalculatorEditor.h | 19 +- .../link_open_ephys_lib.cmake | 0 16 files changed, 678 insertions(+), 636 deletions(-) delete mode 100644 ASICPhaseCalculator/Source/HTransformers.cpp delete mode 100644 ASICPhaseCalculator/backupSource.zip rename {ASICPhaseCalculator => PhaseCalculator}/Build/.gitignore (100%) rename {ASICPhaseCalculator => PhaseCalculator}/CMAKE_README.txt (100%) rename {ASICPhaseCalculator => PhaseCalculator}/CMakeLists.txt (100%) rename {ASICPhaseCalculator => PhaseCalculator}/Source/ARModeler.h (100%) create mode 100644 PhaseCalculator/Source/HTransformers.cpp rename {ASICPhaseCalculator => PhaseCalculator}/Source/HTransformers.h (68%) rename {ASICPhaseCalculator => PhaseCalculator}/Source/OpenEphysLib.cpp (97%) rename {ASICPhaseCalculator => PhaseCalculator}/Source/PhaseCalculator.cpp (72%) rename {ASICPhaseCalculator => PhaseCalculator}/Source/PhaseCalculator.h (90%) rename {ASICPhaseCalculator => PhaseCalculator}/Source/PhaseCalculatorCanvas.cpp (100%) rename {ASICPhaseCalculator => PhaseCalculator}/Source/PhaseCalculatorCanvas.h (100%) rename {ASICPhaseCalculator => PhaseCalculator}/Source/PhaseCalculatorEditor.cpp (64%) rename {ASICPhaseCalculator => PhaseCalculator}/Source/PhaseCalculatorEditor.h (88%) rename {ASICPhaseCalculator => PhaseCalculator}/link_open_ephys_lib.cmake (100%) diff --git a/ASICPhaseCalculator/Source/HTransformers.cpp b/ASICPhaseCalculator/Source/HTransformers.cpp deleted file mode 100644 index c480ce1..0000000 --- a/ASICPhaseCalculator/Source/HTransformers.cpp +++ /dev/null @@ -1,255 +0,0 @@ -/* ------------------------------------------------------------------- - -This file is part of a plugin for the Open Ephys GUI -Copyright (C) 2018 Translational NeuroEngineering Lab - ------------------------------------------------------------------- - -This program is free software: you can redistribute it and/or modify -it under the terms of the GNU General Public License as published by -the Free Software Foundation, either version 3 of the License, or -(at your option) any later version. - -This program is distributed in the hope that it will be useful, -but WITHOUT ANY WARRANTY; without even the implied warranty of -MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -GNU General Public License for more details. - -You should have received a copy of the GNU General Public License -along with this program. If not, see . - -*/ - -#include "HTransformers.h" - -namespace PhaseCalculator -{ - namespace - { - /** Helper to allow assigning to each array one band at a time (within the constructor) **/ - struct HilbertInfo - { - const String delta{ L"\u03b4" }; - const String gamma{ L"\u03b3" }; - const String beta{ L"\u03b2" }; - const String Theta{ L"\u03b8" }; - const String alpha{ L"\u03b1" }; - - String bandName[NUM_BANDS]; - Array validBand[NUM_BANDS]; - Array defaultBand[NUM_BANDS]; - Array extrema[NUM_BANDS]; - int delay[NUM_BANDS]; - Array transformer[NUM_BANDS]; - - static String validBandToString(const Array& band) - { - jassert(band.size() == 2); - return " (" + String(band[0]) + "-" + String(band[1]) + " Hz)"; - } - - HilbertInfo() - { - // Added by sumedh and modified - int del = 7; - Array htcoeff = Array({ -0.433593750000000,0.0,-0.136718750000000,0.0,-0.216796875000000,0.0,-0.638671875000000 }); - - /*delta band*/ - validBand[DELTA] = Array({ 1, 4 }); - bandName[DELTA] = delta + validBandToString(validBand[DELTA]); - defaultBand[DELTA] = Array({ 1, 4 }); - extrema[DELTA] = Array(/* none */); - delay[DELTA] = del; - transformer[DELTA] = htcoeff; - - /*theta band*/ - validBand[THETA] = Array({ 4, 8 }); - bandName[THETA] = Theta + validBandToString(validBand[THETA]); - defaultBand[THETA] = Array({ 4, 8 }); - extrema[THETA] = Array(/* none */); - delay[THETA] = del; - transformer[THETA] = htcoeff; - - /*alpha band*/ - validBand[ALPHA] = Array({ 8, 13 }); - bandName[ALPHA] = alpha + validBandToString(validBand[ALPHA]); - defaultBand[ALPHA] = Array({ 8, 13 }); - extrema[ALPHA] = Array(/* none */); - delay[ALPHA] = del; - transformer[ALPHA] = htcoeff; - - /*beta band*/ - validBand[BETA] = Array({ 12, 30 }); - bandName[BETA] = beta + validBandToString(validBand[BETA]); - defaultBand[BETA] = Array({ 12, 30 }); - extrema[BETA] = Array(/* none */); - delay[BETA] = del; - transformer[BETA] = htcoeff; - - /*lower gamma band*/ - validBand[LOW_GAM] = Array({ 30, 55 }); - bandName[LOW_GAM] = "Lo " + gamma + validBandToString(validBand[LOW_GAM]); - defaultBand[LOW_GAM] = Array({ 30, 55 }); - extrema[LOW_GAM] = Array(/* none */); - delay[LOW_GAM] = del; - transformer[LOW_GAM] = htcoeff; - - /*mid gamma band*/ - validBand[MID_GAM] = Array({ 40, 90 }); - bandName[MID_GAM] = "Mid " + gamma + validBandToString(validBand[MID_GAM]); - //defaultBand[MID_GAM] = Array({ 40, 90 }); - extrema[MID_GAM] = Array(/* none */); - extrema[MID_GAM] = Array({ 64.4559 }); - delay[MID_GAM] = del; - transformer[MID_GAM] = htcoeff; - - /*high gamma band*/ - validBand[HIGH_GAM] = Array({ 60, 200 }); - bandName[HIGH_GAM] = "Hi " + gamma + validBandToString(validBand[HIGH_GAM]); - defaultBand[HIGH_GAM] = Array({ 60, 200 }); - extrema[HIGH_GAM] = Array(/* none */); - //extrema[HIGH_GAM] = Array({ 81.6443, 123.1104, 169.3574 }); - delay[HIGH_GAM] = del; - transformer[HIGH_GAM] = htcoeff; - - /*Ripple band*/ - validBand[RIPPLE] = Array({ 150, 250 }); - bandName[RIPPLE] = "RIP " + validBandToString(validBand[RIPPLE]); - defaultBand[RIPPLE] = Array({ 150, 250 }); - extrema[RIPPLE] = Array(/* none */); - delay[RIPPLE] = del; - transformer[RIPPLE] = htcoeff; - } - }; - - static const HilbertInfo hilbertInfo; // instantiates all the data through the constructor - - struct BandpassfiltInfo - { - String bandName[NUM_BANDS]; - int delay[NUM_BANDS]; - Array transformer[NUM_BANDS]; - BandpassfiltInfo() - { - int del = 42; - /*delta band*/ - delay[DELTA] = del; - transformer[DELTA] = Array({ - 0.0761718750000000,0.0800781250000000,0.0957031250000000,0.123046875000000,0.158203125000000,0.201171875000000,0.253906250000000,0.312500000000000,0.376953125000000,0.443359375000000,0.513671875000000,0.585937500000000,0.654296875000000,0.722656250000000,0.785156250000000,0.841796875000000,0.892578125000000,0.933593750000000,0.964843750000000,0.986328125000000,0.998046875000000,0.998046875000000,0.986328125000000,0.964843750000000,0.933593750000000,0.892578125000000,0.841796875000000,0.785156250000000,0.722656250000000,0.654296875000000,0.585937500000000,0.513671875000000,0.443359375000000,0.376953125000000,0.312500000000000,0.253906250000000,0.201171875000000,0.158203125000000,0.123046875000000,0.0957031250000000,0.0800781250000000,0.0761718750000000 }); - - /*theta band*/ - delay[THETA] = del; - transformer[THETA] = Array({ - 0.0566406250000000,0.0625000000000000,0.0761718750000000,0.0996093750000000,0.132812500000000,0.171875000000000,0.222656250000000,0.277343750000000,0.341796875000000,0.408203125000000,0.480468750000000,0.552734375000000,0.626953125000000,0.697265625000000,0.765625000000000,0.826171875000000,0.880859375000000,0.925781250000000,0.960937500000000,0.986328125000000,0.998046875000000,0.998046875000000,0.986328125000000,0.960937500000000,0.925781250000000,0.880859375000000,0.826171875000000,0.765625000000000,0.697265625000000,0.626953125000000,0.552734375000000,0.480468750000000,0.408203125000000,0.341796875000000,0.277343750000000,0.222656250000000,0.171875000000000,0.132812500000000,0.0996093750000000,0.0761718750000000,0.0625000000000000,0.0566406250000000 }); - - /*alpha band*/ - delay[ALPHA] = del; - transformer[ALPHA] = Array({ - 0.0175781250000000,0.0234375000000000,0.0351562500000000,0.0507812500000000,0.0742187500000000,0.107421875000000,0.148437500000000,0.199218750000000,0.259765625000000,0.326171875000000,0.400390625000000,0.478515625000000,0.558593750000000,0.638671875000000,0.716796875000000,0.789062500000000,0.855468750000000,0.910156250000000,0.953125000000000,0.982421875000000,0.998046875000000,0.998046875000000,0.982421875000000,0.953125000000000,0.910156250000000,0.855468750000000,0.789062500000000,0.716796875000000,0.638671875000000,0.558593750000000,0.478515625000000,0.400390625000000,0.326171875000000,0.259765625000000,0.199218750000000,0.148437500000000,0.107421875000000,0.0742187500000000,0.0507812500000000,0.0351562500000000,0.0234375000000000,0.0175781250000000 }); - - /*beta band*/ - delay[BETA] = del; - transformer[BETA] = Array({ - -0.0566406250000000,-0.0585937500000000,-0.0644531250000000,-0.0722656250000000,-0.0800781250000000,-0.0839843750000000,-0.0781250000000000,-0.0605468750000000,-0.0273437500000000,0.0234375000000000,0.0917968750000000,0.175781250000000,0.277343750000000,0.388671875000000,0.505859375000000,0.623046875000000,0.734375000000000,0.833984375000000,0.912109375000000,0.968750000000000,0.998046875000000,0.998046875000000,0.968750000000000,0.912109375000000,0.833984375000000,0.734375000000000,0.623046875000000,0.505859375000000,0.388671875000000,0.277343750000000,0.175781250000000,0.0917968750000000,0.0234375000000000,-0.0273437500000000,-0.0605468750000000,-0.0781250000000000,-0.0839843750000000,-0.0800781250000000,-0.0722656250000000,-0.0644531250000000,-0.0585937500000000,-0.0566406250000000 }); - - /*low gamma band*/ - delay[LOW_GAM] = del; - transformer[LOW_GAM] = Array({ - 0.0351562500000000,0.0273437500000000,0.0156250000000000,-0.00390625000000000,-0.0371093750000000,-0.0878906250000000,-0.156250000000000,-0.238281250000000,-0.322265625000000,-0.396484375000000,-0.443359375000000,-0.447265625000000,-0.398437500000000,-0.289062500000000,-0.125000000000000,0.0839843750000000,0.318359375000000,0.552734375000000,0.761718750000000,0.916015625000000,0.998046875000000,0.998046875000000,0.916015625000000,0.761718750000000,0.552734375000000,0.318359375000000,0.0839843750000000,-0.125000000000000,-0.289062500000000,-0.398437500000000,-0.447265625000000,-0.443359375000000,-0.396484375000000,-0.322265625000000,-0.238281250000000,-0.156250000000000,-0.0878906250000000,-0.0371093750000000,-0.00390625000000000,0.0156250000000000,0.0273437500000000,0.0351562500000000 }); - - /*mid gamma band*/ - delay[MID_GAM] = del; - transformer[MID_GAM] = Array({ - 0.00195312500000000,0.0,0.00195312500000000,0.0117187500000000,0.0312500000000000,0.0566406250000000,0.0839843750000000,0.0937500000000000,0.0703125000000000,-0.00390625000000000,-0.132812500000000,-0.298828125000000,-0.466796875000000,-0.582031250000000,-0.595703125000000,-0.476562500000000,-0.220703125000000,0.128906250000000,0.501953125000000,0.818359375000000,0.998046875000000,0.998046875000000,0.818359375000000,0.501953125000000,0.128906250000000,-0.220703125000000,-0.476562500000000,-0.595703125000000,-0.582031250000000,-0.466796875000000,-0.298828125000000,-0.132812500000000,-0.00390625000000000,0.0703125000000000,0.0937500000000000,0.0839843750000000,0.0566406250000000,0.0312500000000000,0.0117187500000000,0.00195312500000000,0.0,0.00195312500000000 }); - - /*High gamma band*/ - delay[HIGH_GAM] = del; - transformer[HIGH_GAM] = Array({ - -0.00195312500000000,-0.00781250000000000,-0.0117187500000000,-0.00195312500000000,0.0117187500000000,0.0175781250000000,0.00390625000000000,0.0,0.0390625000000000,0.0917968750000000,0.0820312500000000,-0.0117187500000000,-0.0859375000000000,-0.0371093750000000,0.0468750000000000,-0.0546875000000000,-0.392578125000000,-0.640625000000000,-0.390625000000000,0.341796875000000,0.998046875000000,0.998046875000000,0.341796875000000,-0.390625000000000,-0.640625000000000,-0.392578125000000,-0.0546875000000000,0.0468750000000000,-0.0371093750000000,-0.0859375000000000,-0.0117187500000000,0.0820312500000000,0.0917968750000000,0.0390625000000000,0.0,0.00390625000000000,0.0175781250000000,0.0117187500000000,-0.00195312500000000,-0.0117187500000000,-0.00781250000000000,-0.00195312500000000 }); - - /*Ripple band*/ - delay[RIPPLE] = del; - transformer[RIPPLE] = Array({ - 0.00195312500000000,-0.00195312500000000,0.00195312500000000,0.0195312500000000,0.00976562500000000,-0.0390625000000000,-0.0527343750000000,0.0234375000000000,0.0800781250000000,0.0195312500000000,-0.0234375000000000,0.0292968750000000,-0.0410156250000000,-0.251953125000000,-0.121093750000000,0.449218750000000,0.580078125000000,-0.269531250000000,-0.998046875000000,-0.335937500000000,0.921875000000000,0.921875000000000,-0.335937500000000,-0.998046875000000,-0.269531250000000,0.580078125000000,0.449218750000000,-0.121093750000000,-0.251953125000000,-0.0410156250000000,0.0292968750000000,-0.0234375000000000,0.0195312500000000,0.0800781250000000,0.0234375000000000,-0.0527343750000000,-0.0390625000000000,0.00976562500000000,0.0195312500000000,0.00195312500000000,-0.00195312500000000,0.00195312500000000 }); - - } - }; - - static const BandpassfiltInfo bandpassfiltInfo; // instantiates all the data through the constructor - - struct LowpassfiltInfo - { - String bandName[NUM_BANDS]; - int delay[NUM_BANDS]; - Array transformer[NUM_BANDS]; - - LowpassfiltInfo() - { - int del = 25; - Array lpfcoeff = Array({ 0.0,0.00195312500000000,0.00585937500000000,0.00781250000000000,0.0,-0.0332031250000000,-0.0800781250000000,-0.0917968750000000,0.0,0.238281250000000,0.574218750000000,0.875000000000000,0.998046875000000,0.875000000000000,0.574218750000000,0.238281250000000,0.0,-0.0917968750000000,-0.0800781250000000,-0.0332031250000000,0.0,0.00781250000000000,0.00585937500000000,0.00195312500000000,0.0 }); - /*delta band*/ - delay[DELTA] = del; - transformer[DELTA] = lpfcoeff; - /*theta band*/ - delay[THETA] = del; - transformer[THETA] = lpfcoeff; - /*alpha band*/ - delay[ALPHA] = del; - transformer[ALPHA] = lpfcoeff; - /*beta band*/ - delay[BETA] = del; - transformer[BETA] = lpfcoeff; - /*low gamma band*/ - delay[LOW_GAM] = del; - transformer[LOW_GAM] = lpfcoeff; - /*mid gamma band*/ - delay[MID_GAM] = del; - transformer[MID_GAM] = lpfcoeff; - /*High gamma band*/ - delay[HIGH_GAM] = del; - transformer[HIGH_GAM] = lpfcoeff; - /*Ripple band*/ - delay[RIPPLE] = del; - transformer[RIPPLE] = lpfcoeff; - } - }; - - static const LowpassfiltInfo lowpassfiltInfo; // instantiates all the data through the constructor - } - - namespace Hilbert - { - // exported constants - extern const String* const bandName = hilbertInfo.bandName; - - extern const Array* const validBand = hilbertInfo.validBand; - - extern const Array* const defaultBand = hilbertInfo.defaultBand; - - extern const Array* const extrema = hilbertInfo.extrema; - - extern const int* const delay = hilbertInfo.delay; - - extern const Array* const transformer = hilbertInfo.transformer; - } - - namespace bandpassfilt - { - // exported constants - extern const String* const bandName = bandpassfiltInfo.bandName; - - extern const int* const delay = bandpassfiltInfo.delay; - - extern const Array* const transformer = bandpassfiltInfo.transformer; - } - namespace lowpassfilt - { - // exported constants - extern const String* const bandName = lowpassfiltInfo.bandName; - - extern const int* const delay = lowpassfiltInfo.delay; - - extern const Array* const transformer = lowpassfiltInfo.transformer; - } -} \ No newline at end of file diff --git a/ASICPhaseCalculator/backupSource.zip b/ASICPhaseCalculator/backupSource.zip deleted file mode 100644 index 3a2e2733107adc1e52af51251c8e546e15cc7cdb..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 37024 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 N$73=80FT-q{RbLmNm~E_ diff --git a/ASICPhaseCalculator/Build/.gitignore b/PhaseCalculator/Build/.gitignore similarity index 100% rename from ASICPhaseCalculator/Build/.gitignore rename to PhaseCalculator/Build/.gitignore diff --git a/ASICPhaseCalculator/CMAKE_README.txt b/PhaseCalculator/CMAKE_README.txt similarity index 100% rename from ASICPhaseCalculator/CMAKE_README.txt rename to PhaseCalculator/CMAKE_README.txt diff --git a/ASICPhaseCalculator/CMakeLists.txt b/PhaseCalculator/CMakeLists.txt similarity index 100% rename from ASICPhaseCalculator/CMakeLists.txt rename to PhaseCalculator/CMakeLists.txt diff --git a/ASICPhaseCalculator/Source/ARModeler.h b/PhaseCalculator/Source/ARModeler.h similarity index 100% rename from ASICPhaseCalculator/Source/ARModeler.h rename to PhaseCalculator/Source/ARModeler.h diff --git a/PhaseCalculator/Source/HTransformers.cpp b/PhaseCalculator/Source/HTransformers.cpp new file mode 100644 index 0000000..8d70cce --- /dev/null +++ b/PhaseCalculator/Source/HTransformers.cpp @@ -0,0 +1,146 @@ +/* +------------------------------------------------------------------ + +This file is part of a plugin for the Open Ephys GUI +Copyright (C) 2018 Translational NeuroEngineering Lab + +------------------------------------------------------------------ + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program. If not, see . + +*/ + +#include "HTransformers.h" + +namespace PhaseCalculator +{ + namespace + { + /** Helper to allow assigning to each array one band at a time (within the constructor) **/ + struct HilbertInfo + { + const String gamma{ L"\u03b3" }; + const String beta{ L"\u03b2" }; + const String alphaTheta{ L"\u03b1/\u03b8" }; + + String bandName[NUM_BANDS]; + Array validBand[NUM_BANDS]; + Array defaultBand[NUM_BANDS]; + Array extrema[NUM_BANDS]; + int delay[NUM_BANDS]; + Array transformer[NUM_BANDS]; + + static String validBandToString(const Array& band) + { + jassert(band.size() == 2); + return " (" + String(band[0]) + "-" + String(band[1]) + " Hz)"; + } + + HilbertInfo() + { + validBand[ALPHA_THETA] = Array({ 4, 18 }); + bandName[ALPHA_THETA] = alphaTheta + validBandToString(validBand[ALPHA_THETA]); + defaultBand[ALPHA_THETA] = Array({ 4, 8 }); + extrema[ALPHA_THETA] = Array(/* none */); + delay[ALPHA_THETA] = 9; + // from Matlab: firpm(18, [4 246]/250, [1 1], 'hilbert') + transformer[ALPHA_THETA] = Array({ + -0.28757250783614413, + 0.000027647225074994485, + -0.094611325643268351, + -0.00025887439499763831, + -0.129436276914844, + -0.0001608427426424053, + -0.21315096860055227, + -0.00055322197399797961, + -0.63685698210351149 + }); + + + validBand[BETA] = Array({ 10, 40 }); + bandName[BETA] = beta + validBandToString(validBand[BETA]); + defaultBand[BETA] = Array({ 12, 30 }); + extrema[BETA] = Array({ 21.5848 }); + delay[BETA] = 9; + // from Matlab: firpm(18, [12 30 40 240]/250, [1 1 0.7 0.7], 'hilbert') + transformer[BETA] = Array({ + -0.099949575596234311, + -0.020761484963254036, + -0.080803573080958854, + -0.027365064225587619, + -0.11114477443975329, + -0.025834076852645271, + -0.16664116044989324, + -0.015661948619847599, + -0.45268524264113719 + }); + + + validBand[LOW_GAM] = Array({ 30, 55 }); + bandName[LOW_GAM] = "Lo " + gamma + validBandToString(validBand[LOW_GAM]); + defaultBand[LOW_GAM] = Array({ 30, 55 }); + extrema[LOW_GAM] = Array({ 43.3609 }); + delay[LOW_GAM] = 2; + // from Matlab: firls(4, [30 55]/250, [1 1], 'hilbert') + transformer[LOW_GAM] = Array({ + -1.5933788446351915, + 1.7241339075391682 + }); + + + validBand[MID_GAM] = Array({ 40, 90 }); + bandName[MID_GAM] = "Mid " + gamma + validBandToString(validBand[MID_GAM]); + defaultBand[MID_GAM] = Array({ 40, 90 }); + extrema[MID_GAM] = Array({ 64.4559 }); + delay[MID_GAM] = 2; + // from Matlab: firls(4, [35 90]/250, [1 1], 'hilbert') + transformer[MID_GAM] = Array({ + -0.487176162115735, + -0.069437334858668653 + }); + + + validBand[HIGH_GAM] = Array({ 60, 200 }); + bandName[HIGH_GAM] = "Hi " + gamma + validBandToString(validBand[HIGH_GAM]); + defaultBand[HIGH_GAM] = Array({ 70, 150 }); + extrema[HIGH_GAM] = Array({ 81.6443, 123.1104, 169.3574 }); + delay[HIGH_GAM] = 3; + // from Matlab: firls(6, [60 200]/250, [1 1], 'hilbert') + transformer[HIGH_GAM] = Array({ + -0.10383410506573287, + 0.0040553935691102303, + -0.59258484603659545 + }); + } + }; + + static const HilbertInfo hilbertInfo; // instantiates all the data through the constructor + } + + namespace Hilbert + { + // exported constants + extern const String* const bandName = hilbertInfo.bandName; + + extern const Array* const validBand = hilbertInfo.validBand; + + extern const Array* const defaultBand = hilbertInfo.defaultBand; + + extern const Array* const extrema = hilbertInfo.extrema; + + extern const int* const delay = hilbertInfo.delay; + + extern const Array* const transformer = hilbertInfo.transformer; + } +} \ No newline at end of file diff --git a/ASICPhaseCalculator/Source/HTransformers.h b/PhaseCalculator/Source/HTransformers.h similarity index 68% rename from ASICPhaseCalculator/Source/HTransformers.h rename to PhaseCalculator/Source/HTransformers.h index 4b51751..4525ddd 100644 --- a/ASICPhaseCalculator/Source/HTransformers.h +++ b/PhaseCalculator/Source/HTransformers.h @@ -46,14 +46,11 @@ namespace PhaseCalculator { enum Band { - DELTA = 0, - THETA, - ALPHA, + ALPHA_THETA = 0, BETA, LOW_GAM, MID_GAM, HIGH_GAM, - RIPPLE, NUM_BANDS }; @@ -79,52 +76,6 @@ namespace PhaseCalculator // contain the first delay[band] coefficients; the rest are redundant and can be inferred extern const Array* const transformer; } - - namespace bandpassfilt - { - const int fs = 500; - - // Each pointer below points to an array of length NUM_BANDS. - - extern const String* const bandName; - - // each is a pair (lower limit, upper limit) - extern const Array* const validBand; - - // each is a pair (low cut, high cut) - extern const Array* const defaultBand; - - extern const Array* const extrema; - - // samples of group delay (= order of filter / 2) - extern const int* const delay; - - // contain the first delay[band] coefficients; the rest are redundant and can be inferred - extern const Array* const transformer; - } - - namespace lowpassfilt - { - const int fs = 500; - - // Each pointer below points to an array of length NUM_BANDS. - - extern const String* const bandName; - - // each is a pair (lower limit, upper limit) - extern const Array* const validBand; - - // each is a pair (low cut, high cut) - extern const Array* const defaultBand; - - extern const Array* const extrema; - - // samples of group delay (= order of filter / 2) - extern const int* const delay; - - // contain the first delay[band] coefficients; the rest are redundant and can be inferred - extern const Array* const transformer; - } } #endif // H_TRANSFORMERS_H_INCLUDED \ No newline at end of file diff --git a/ASICPhaseCalculator/Source/OpenEphysLib.cpp b/PhaseCalculator/Source/OpenEphysLib.cpp similarity index 97% rename from ASICPhaseCalculator/Source/OpenEphysLib.cpp rename to PhaseCalculator/Source/OpenEphysLib.cpp index a275cc0..3c02236 100644 --- a/ASICPhaseCalculator/Source/OpenEphysLib.cpp +++ b/PhaseCalculator/Source/OpenEphysLib.cpp @@ -48,7 +48,7 @@ extern "C" EXPORT int getPluginInfo(int index, Plugin::PluginInfo* info) { case 0: info->type = Plugin::PLUGIN_TYPE_PROCESSOR; - info->processor.name = "ASIC"; + info->processor.name = "Phase Calculator"; info->processor.type = Plugin::FilterProcessor; info->processor.creator = &(Plugin::createProcessor); break; diff --git a/ASICPhaseCalculator/Source/PhaseCalculator.cpp b/PhaseCalculator/Source/PhaseCalculator.cpp similarity index 72% rename from ASICPhaseCalculator/Source/PhaseCalculator.cpp rename to PhaseCalculator/Source/PhaseCalculator.cpp index 79c570a..c4fcca7 100644 --- a/ASICPhaseCalculator/Source/PhaseCalculator.cpp +++ b/PhaseCalculator/Source/PhaseCalculator.cpp @@ -37,6 +37,9 @@ namespace PhaseCalculator // (meant to be larger than actual minimum floating-point eps) static const float passbandEps = 0.01F; + // priority of the AR model calculating thread (0 = lowest, 10 = highest) + static const int arPriority = 3; + // "glitch limit" (how long of a segment is allowed to be unwrapped or smoothed, in samples) static const int glitchLimit = 200; @@ -46,6 +49,7 @@ namespace PhaseCalculator static const int visMinDelayMs = 675; static const int visMaxDelayMs = 1000; + /*** ReverseStack ***/ ReverseStack::ReverseStack(int size) : freeSpace (size) @@ -134,34 +138,36 @@ namespace PhaseCalculator void ActiveChannelInfo::update() { const Node& p = chanInfo.owner; - + int arOrder = p.getAROrder(); float highCut = p.getHighCut(); float lowCut = p.getLowCut(); Band band = p.getBand(); - int bpforder = p.getBpfOrder(); + + // update length of history based on sample rate + // the history buffer should have enough samples to calculate phases for the viusalizer + // with the proper Hilbert transform length AND train an AR model of the requested order, + // using at least 1 second of data + int newHistorySize = chanInfo.dsFactor * jmax( + visHilbertLengthMs * Hilbert::fs / 1000, + arOrder + 1, + 1 * Hilbert::fs); + + history.resetAndResize(newHistorySize); + // set filter parameters - // set filter parameters - for (auto filt : { &filter, &reverseFilter }) - { - filt->setup( - bpforder, // order - chanInfo.sampleRate, // sample rate - (highCut + lowCut) / 2, // center frequency - highCut - lowCut); // bandwidth - } - // added by sumedh - int lpforder = p.getLpfOrder(); - - - // low pass at 250 Hz - for (auto lpffilt1 : { &filterlpf }) - { - lpffilt1->setup(lpforder, (double)chanInfo.sampleRate, (double)(.8*(chanInfo.sampleRate/2)/ (chanInfo.sampleRate / 4000)),0.05); - } + for (auto filt : { &filter, &reverseFilter }) + { + filt->setup( + 2, // order + chanInfo.sampleRate, // sample rate + (highCut + lowCut) / 2, // center frequency + highCut - lowCut); // bandwidth + } + + arModeler.setParams(arOrder, newHistorySize, chanInfo.dsFactor); htState.resize(Hilbert::delay[band] * 2 + 1); - bpfState.resize(bandpassfilt::delay[band]); - lpfState.resize(lowpassfilt::delay[band]); + // visualization stuff hilbertLengthMultiplier = Hilbert::fs * chanInfo.dsFactor / 1000; visHilbertBuffer.resize(visHilbertLengthMs * hilbertLengthMultiplier); @@ -173,11 +179,8 @@ namespace PhaseCalculator { history.reset(); filter.reset(); - //added by sumedh - filterlpf.reset(); + arModeler.reset(); FloatVectorOperations::clear(htState.begin(), htState.size()); - FloatVectorOperations::clear(bpfState.begin(), bpfState.size()); - FloatVectorOperations::clear(lpfState.begin(), lpfState.size()); interpCountdown = 0; lastComputedPhase = 0; lastComputedMag = 0; @@ -247,17 +250,16 @@ namespace PhaseCalculator /**** phase calculator node ****/ Node::Node() - : GenericProcessor("ASIC Phase Calculator") + : GenericProcessor("Phase Calculator") , Thread("AR Modeler") - // added by sumedh - , lpforder(8) - , bpforder(2) + , calcInterval(50) + , arOrder(20) , outputMode(PH) , visEventChannel(-1) , visContinuousChannel(-1) { setProcessorType(PROCESSOR_TYPE_FILTER); - setBand(THETA, true); + setBand(ALPHA_THETA, true); } Node::~Node() {} @@ -309,10 +311,27 @@ namespace PhaseCalculator void Node::setParameter(int parameterIndex, float newValue) { switch (parameterIndex) { + case RECALC_INTERVAL: + calcInterval = int(newValue); + break; + + case AR_ORDER: + arOrder = int(newValue); + updateActiveChannels(); + break; case BAND: setBand(Band(int(newValue))); break; + + case LOWCUT: + setLowCut(newValue); + break; + + case HIGHCUT: + setHighCut(newValue); + break; + case OUTPUT_MODE: { OutputMode oldMode = outputMode; @@ -369,147 +388,168 @@ namespace PhaseCalculator { continue; } - int downsampleA = 4000; - int downsampleB = 1000; - /*Get a pointer to the data wpInput*/ - float* const wpInput = buffer.getWritePointer(chan); - /*active channel info -> acInfo*/ - /*Step I: IIR Low pass filter over entire data */ - acInfo->filterlpf.process(nSamples, &wpInput); - std::vector dslpf; - int preDSample = int(floor(float(acInfo->chanInfo.sampleRate) / downsampleA)); - for (int i = 0; i < nSamples; i = i + preDSample) { - dslpf.push_back(wpInput[i]); - } - /*Step II: FIR low pass filter from MATLAB*/ - /*lpf again with the coeff from MATLAB*/ - std::vector lpfData; - for (int lpfIndex = 0; lpfIndex < dslpf.size(); ++lpfIndex) { - lpfData.push_back(lpfFilterSamp(dslpf[lpfIndex], band, acInfo->lpfState)); - } - /*Step III: Downsampling from 40000K to 1K*/ - - int new_sr_size = downsampleA / downsampleB;// int(floor(float(acInfo->chanInfo.sampleRate) / float(new_sampling_rate))); - std::vector dsLpfData; - for (int i = 0; i < dslpf.size(); i=i+new_sr_size) { - dsLpfData.push_back(lpfData[i]); - } - /*Step IV: FIR Band pass filtering on the downsampled data*/ - int newdatasize = dsLpfData.size(); - std::vector dsBPFdata; - for (int bpfIndex = 0; bpfIndex < dsLpfData.size(); ++bpfIndex) { - dsBPFdata.push_back(bpfFilterSamp(dsLpfData[bpfIndex], band, acInfo->bpfState)); - } - /*Step V: calculate Hilbert transform*/ - int htOutputSamps = dsBPFdata.size(); - if (htOutput.size() < htOutputSamps) - { - htOutput.resize(htOutputSamps); - } - htOutput.resize(htOutputSamps); - /*Changes Both real and imaginary part are calculated from the same*/ - //float* wpOut = buffer.getWritePointer(chan); - for (int hilbertIndex = 0; hilbertIndex < htOutputSamps; ++hilbertIndex) - { - double samp = htFilterSamp(dsBPFdata[hilbertIndex], band, acInfo->htState); - double rc = dsBPFdata[hilbertIndex]; - double ic = htScaleFactor * samp; - htOutput.set(hilbertIndex, std::complex(rc, ic)); - //wpOut[hilbertIndex] = (float)rc; - } - /*Step VI: Interpolation: No new changes proposed*/ - int stride = int(floor(float(acInfo->chanInfo.sampleRate) / float(downsampleB))) + 2; - float* wpOut = buffer.getWritePointer(chan); - float* wpOut2; - if (outputMode == PH_AND_MAG) - { - // second output channel - int outChan2 = getNumInputs() + ac; - jassert(outChan2 < buffer.getNumChannels()); - wpOut2 = buffer.getWritePointer(outChan2); - } - - double nextComputedPhase, phaseStep; - double nextComputedMag, magStep; - bool needPhase = outputMode != MAG; - bool needMag = outputMode != PH; - - if (needPhase) - { - nextComputedPhase = LAA(htOutput[0]);//std::arg(htOutput[0]); - phaseStep = circDist(nextComputedPhase, acInfo->lastComputedPhase, Dsp::doublePi) / stride; - } - if (needMag) - { - nextComputedMag = std::abs(htOutput[0]); - magStep = (nextComputedMag - acInfo->lastComputedMag) / stride; - } - - for (int i = 0, frame = 0; i < nSamples; ++i, --acInfo->interpCountdown) - { - if (acInfo->interpCountdown == 0) - { - // update interpolation frame - ++frame; - acInfo->interpCountdown = stride; - - if (needPhase) - { - acInfo->lastComputedPhase = nextComputedPhase; - nextComputedPhase = LAA(htOutput[frame]);//std::arg(htOutput[frame]); - phaseStep = circDist(nextComputedPhase, acInfo->lastComputedPhase, Dsp::doublePi) / stride; - } - if (needMag) - { - acInfo->lastComputedMag = nextComputedMag; - nextComputedMag = std::abs(htOutput[frame]); - magStep = (nextComputedMag - acInfo->lastComputedMag) / stride; - } - } - - double thisPhase, thisMag; - if (needPhase) - { - thisPhase = circDist(nextComputedPhase, phaseStep * acInfo->interpCountdown, Dsp::doublePi); - } - if (needMag) - { - thisMag = nextComputedMag - magStep * acInfo->interpCountdown; - } - - switch (outputMode) - { - case MAG: - wpOut[i] = float(thisMag); - break; - - case PH_AND_MAG: - wpOut2[i] = float(thisMag); - // fall through - case PH: - // output in degrees - wpOut[i] = float(thisPhase * (180.0 / Dsp::doublePi)); - break; - - case IM: - wpOut[i] = float(thisMag * std::sin(thisPhase)); - break; - } - } - // unwrapping / smoothing - if (outputMode == PH || outputMode == PH_AND_MAG) - { - unwrapBuffer(wpOut, nSamples, acInfo->lastPhase); - smoothBuffer(wpOut, nSamples, acInfo->lastPhase); - acInfo->lastPhase = wpOut[nSamples - 1]; - } - // if this is the monitored channel for events, check whether we can add a new phase - /*if (hasCanvas && chan == visContinuousChannel && acInfo->history.isFull()) - { - calcVisPhases(acInfo, getTimestamp(chan) + getNumSamples(chan)); - }*/ + // filter the data + float* const wpIn = buffer.getWritePointer(chan); + acInfo->filter.process(nSamples, &wpIn); + + // enqueue as much new data as can fit into history + acInfo->history.enqueue(wpIn, nSamples); + + // calc phase and write out (only if AR model has been calculated) + if (acInfo->history.isFull() && acInfo->arModeler.hasBeenFit()) + { + // read current AR parameters safely (uses lock internally) + acInfo->arModeler.getModel(localARParams); + + // use AR model to fill predSamps (which is downsampled) based on past data. + int htDelay = Hilbert::delay[band]; + int stride = acInfo->chanInfo.dsFactor; + + double* pPredSamps = predSamps.getRawDataPointer(); + const double* pLocalParam = localARParams.getRawDataPointer(); + arPredict(acInfo->history, acInfo->interpCountdown, pPredSamps, pLocalParam, + htDelay + 1, stride, arOrder); + + // identify indices of current buffer to execute HT + htInds.clearQuick(); + for (int i = acInfo->interpCountdown; i < nSamples; i += stride) + { + htInds.add(i); + } + + int htOutputSamps = htInds.size() + 1; + if (htOutput.size() < htOutputSamps) + { + htOutput.resize(htOutputSamps); + } + + // execute tranformer on current buffer + int kOut = -htDelay; + for (int kIn = 0; kIn < htInds.size(); ++kIn, ++kOut) + { + double samp = htFilterSamp(wpIn[htInds[kIn]], band, acInfo->htState); + if (kOut >= 0) + { + double rc = wpIn[htInds[kOut]]; + double ic = htScaleFactor * samp; + htOutput.set(kOut, std::complex(rc, ic)); + } + } + + // copy state to transform prediction without changing the end-of-buffer state + htTempState = acInfo->htState; + + // execute transformer on prediction + for (int i = 0; i <= htDelay; ++i, ++kOut) + { + double samp = htFilterSamp(predSamps[i], band, htTempState); + if (kOut >= 0) + { + double rc = i == htDelay ? predSamps[0] : wpIn[htInds[kOut]]; + double ic = htScaleFactor * samp; + htOutput.set(kOut, std::complex(rc, ic)); + } + } + + // output with upsampling (interpolation) + float* wpOut = buffer.getWritePointer(chan); + float* wpOut2; + if (outputMode == PH_AND_MAG) + { + // second output channel + int outChan2 = getNumInputs() + ac; + jassert(outChan2 < buffer.getNumChannels()); + wpOut2 = buffer.getWritePointer(outChan2); + } + + double nextComputedPhase, phaseStep; + double nextComputedMag, magStep; + bool needPhase = outputMode != MAG; + bool needMag = outputMode != PH; + + if (needPhase) + { + nextComputedPhase = std::arg(htOutput[0]); + phaseStep = circDist(nextComputedPhase, acInfo->lastComputedPhase, Dsp::doublePi) / stride; + } + if (needMag) + { + nextComputedMag = std::abs(htOutput[0]); + magStep = (nextComputedMag - acInfo->lastComputedMag) / stride; + } + + for (int i = 0, frame = 0; i < nSamples; ++i, --acInfo->interpCountdown) + { + if (acInfo->interpCountdown == 0) + { + // update interpolation frame + ++frame; + acInfo->interpCountdown = stride; + + if (needPhase) + { + acInfo->lastComputedPhase = nextComputedPhase; + nextComputedPhase = std::arg(htOutput[frame]); + phaseStep = circDist(nextComputedPhase, acInfo->lastComputedPhase, Dsp::doublePi) / stride; + } + if (needMag) + { + acInfo->lastComputedMag = nextComputedMag; + nextComputedMag = std::abs(htOutput[frame]); + magStep = (nextComputedMag - acInfo->lastComputedMag) / stride; + } + } + + double thisPhase, thisMag; + if (needPhase) + { + thisPhase = circDist(nextComputedPhase, phaseStep * acInfo->interpCountdown, Dsp::doublePi); + } + if (needMag) + { + thisMag = nextComputedMag - magStep * acInfo->interpCountdown; + } + + switch (outputMode) + { + case MAG: + wpOut[i] = float(thisMag); + break; + + case PH_AND_MAG: + wpOut2[i] = float(thisMag); + // fall through + case PH: + // output in degrees + wpOut[i] = float(thisPhase * (180.0 / Dsp::doublePi)); + break; + + case IM: + wpOut[i] = float(thisMag * std::sin(thisPhase)); + break; + } + } + + // unwrapping / smoothing + if (outputMode == PH || outputMode == PH_AND_MAG) + { + unwrapBuffer(wpOut, nSamples, acInfo->lastPhase); + smoothBuffer(wpOut, nSamples, acInfo->lastPhase); + acInfo->lastPhase = wpOut[nSamples - 1]; + } + } + else // fifo not full or AR model not ready + { + // just output zeros + buffer.clear(chan, 0, nSamples); + } + // if this is the monitored channel for events, check whether we can add a new phase + if (hasCanvas && chan == visContinuousChannel && acInfo->history.isFull()) + { + calcVisPhases(acInfo, getTimestamp(chan) + getNumSamples(chan)); + } } } @@ -518,7 +558,7 @@ namespace PhaseCalculator { if (isEnabled) { - //startThread(arPriority); + startThread(arPriority); // have to manually enable editor, I guess... Editor* editor = static_cast(getEditor()); @@ -576,6 +616,34 @@ namespace PhaseCalculator Array reverseData; reverseData.resize(maxHistoryLength); + + uint32 startTime, endTime; + while (!threadShouldExit()) + { + startTime = Time::getMillisecondCounter(); + + for (auto acInfo : activeChans) + { + if (!acInfo->history.isFull()) + { + continue; + } + + // unwrap reversed history and add to temporary data array + double* dataPtr = reverseData.getRawDataPointer(); + acInfo->history.unwrapAndCopy(dataPtr, true); + + // calculate parameters + acInfo->arModeler.fitModel(reverseData); + } + + endTime = Time::getMillisecondCounter(); + int remainingInterval = calcInterval - (endTime - startTime); + if (remainingInterval >= 10) // avoid WaitForSingleObject + { + sleep(remainingInterval); + } + } } void Node::updateSettings() @@ -657,16 +725,10 @@ namespace PhaseCalculator return int(getProcessorFullId(sourceNodeId, subProcessorIdx)); } - // Added by Sumedh to access private variables - int Node::getLpfOrder() const - { - return lpforder; - } - - int Node::getBpfOrder() const - { - return bpforder; - } + int Node::getAROrder() const + { + return arOrder; + } float Node::getHighCut() const { @@ -779,10 +841,71 @@ namespace PhaseCalculator const Array& defaultBand = Hilbert::defaultBand[band]; lowCut = defaultBand[0]; highCut = defaultBand[1]; + + auto editor = static_cast(getEditor()); + if (editor) + { + editor->refreshLowCut(); + editor->refreshHighCut(); + } + + updateScaleFactor(); + updateActiveChannels(); + } + + void Node::setLowCut(float newLowCut) + { + if (newLowCut == lowCut) { return; } + + auto editor = static_cast(getEditor()); + const Array& validBand = Hilbert::validBand[band]; + + if (newLowCut < validBand[0] || newLowCut >= validBand[1]) + { + // invalid; don't set parameter and reset editor + editor->refreshLowCut(); + CoreServices::sendStatusMessage("Low cut outside valid band of selected filter."); + return; + } + + lowCut = newLowCut; + if (lowCut >= highCut) + { + // push highCut up + highCut = jmin(lowCut + passbandEps, validBand[1]); + editor->refreshHighCut(); + } + updateScaleFactor(); updateActiveChannels(); } + void Node::setHighCut(float newHighCut) + { + if (newHighCut == highCut) { return; } + + auto editor = static_cast(getEditor()); + const Array& validBand = Hilbert::validBand[band]; + + if (newHighCut <= validBand[0] || newHighCut > validBand[1]) + { + // invalid; don't set parameter and reset editor + editor->refreshHighCut(); + CoreServices::sendStatusMessage("High cut outside valid band of selected filter."); + return; + } + + highCut = newHighCut; + if (highCut <= lowCut) + { + // push lowCut down + lowCut = jmax(highCut - passbandEps, validBand[0]); + editor->refreshLowCut(); + } + + updateScaleFactor(); + updateActiveChannels(); + } void Node::setVisContChan(int newChan) { @@ -815,7 +938,7 @@ namespace PhaseCalculator void Node::updateScaleFactor() { - htScaleFactor = getScaleFactor(THETA, 4.0, 8.0); + htScaleFactor = getScaleFactor(band, lowCut, highCut); } void Node::unwrapBuffer(float* wp, int nSamples, float lastPhase) @@ -1064,7 +1187,7 @@ namespace PhaseCalculator acInfo->reverseFilter.reset(); acInfo->reverseFilter.process(hilbertLength, &wpHilbert); - //un-reverse values + // un-reverse values acInfo->visHilbertBuffer.reverseReal(hilbertLength); // Hilbert transform! @@ -1143,6 +1266,29 @@ namespace PhaseCalculator channelInfo.getUnchecked(chan)->deactivate(); } + void Node::arPredict(const ReverseStack& history, int interpCountdown, double* prediction, + const double* params, int samps, int stride, int order) + { + const double* rpHistory = history.begin(); + int histSize = history.size(); + int histStart = history.getHeadOffset() + stride - interpCountdown; + + // s = index to write output + for (int s = 0; s < samps; ++s) + { + prediction[s] = 0; + + // p = which AR param we are on + for (int p = 0; p < order; ++p) + { + double pastSamp = p < s + ? prediction[s - 1 - p] + : rpHistory[(histStart + (p - s) * stride) % histSize]; + + prediction[s] -= params[p] * pastSamp; + } + } + } double Node::getScaleFactor(Band band, double lowCut, double highCut) { @@ -1212,117 +1358,6 @@ namespace PhaseCalculator std::memmove(state_p, state_p + 1, order * sizeof(double)); return sampOut; } - //Added by Sumedh - /* - input: gets the current value to be filtered - Band: The band that needs to be filtered, actually this is identified with Matlab function - state: the bandpass filter states needs to keep track of the data - Steps - 1) move the memory and add the input at the 0 - 2) std::multiplies state * coefficient - 3) sum the entire states - 4) return the sum - */ - double Node::lpfFilterSamp(double input, Band band, Array& state) - { - double* state_p = state.getRawDataPointer(); - int nCoefs = lowpassfilt::delay[band]; - /*Considering the entire coefficient*/ - int order = nCoefs; // considering the current even order - //to the right - //double temp = state_p[order - 1]; //remember last element - for (int i = order - 1; i >= 0; i--) - { - state_p[i + 1] = state_p[i]; //move all element to the right except last one - } - state_p[0] = 0.0; //assign remembered value to first element - //std::memmove(state_p + 1, state_p, order * sizeof(double)); - state_p[0] = input; - double retValue = 0.0; - const double* transf = lowpassfilt::transformer[band].begin(); - for (int kCoef = 0; kCoef < nCoefs; ++kCoef) - { - retValue = retValue + state_p[kCoef] * transf[kCoef]; - } - - return retValue; - } - /* - input: gets the current value to be filtered - Band: The band that needs to be filtered, actually this is identified with Matlab function - state: the bandpass filter states needs to keep track of the data - Steps - 1) move the memory and add the input at the 0 - 2) std::multiplies state * coefficient - 3) sum the entire states - 4) return the sum - */ - double Node::bpfFilterSamp(double input, Band band, Array& state) - { - double* state_p = state.getRawDataPointer(); - int nCoefs = bandpassfilt::delay[band]; - double retValue = 0.0; - int order = nCoefs; // considering the current even order - state_p[order - 1] = 0.0; - for (int i = order - 1; i >= 0; i--) - { - state_p[i + 1] = state_p[i]; //move all element to the right except last one - } - state_p[0] = 0.0; //assign remembered value to first element - //std::memmove(state_p + 1, state_p, order-1 * sizeof(double)); - state_p[0] = input; - const double* transf = bandpassfilt::transformer[band].begin(); - for (int kCoef = 0; kCoef < nCoefs; ++kCoef) - { - retValue = retValue + state_p[kCoef] * transf[kCoef]; - } - return retValue; - } - - double Node::LAA(std::complex c) - { - // Hold phase before quantization - double q; - - // Determine phase based on octant. See LAA alg details. - if (std::abs(c.real()) >= std::abs(c.imag())) - { - if (c.real() >= 0) - { - q = (1. / 8.) * (c.imag() / c.real()); // octant 1 and 8 - - } - else - { - if (c.imag() >= 0) - { - q = .5 + (1. / 8.) * (c.imag() / c.real()); // octant 4 - } - else - { - q = -.5 + (1. / 8.) * (c.imag() / c.real()); // octant 5 - } - } - } - else - { - if (c.imag() >= 0) - { - q = 0.25 - (1. / 8.) * (c.real() / c.imag()); // octant 2 and 3 - } - else - { - q = -.25 - (1. / 8.) * (c.real() / c.imag()); // octant 6 and 7 - } - } - - // Do quantization on phase (based on bit percision). We are testing 8 bits. - double lsb = 1. / pow(2., 8.); - double ans = floor(q / lsb) * lsb * 3.14 / 0.5; - if (std::isnan(ans)) - ans = 0.0; - return ans; - } } diff --git a/ASICPhaseCalculator/Source/PhaseCalculator.h b/PhaseCalculator/Source/PhaseCalculator.h similarity index 90% rename from ASICPhaseCalculator/Source/PhaseCalculator.h rename to PhaseCalculator/Source/PhaseCalculator.h index 1445e10..24e17fc 100644 --- a/ASICPhaseCalculator/Source/PhaseCalculator.h +++ b/PhaseCalculator/Source/PhaseCalculator.h @@ -62,13 +62,11 @@ namespace PhaseCalculator enum Param { RECALC_INTERVAL, + AR_ORDER, BAND, LOWCUT, HIGHCUT, OUTPUT_MODE, - // Added by Sumedh - LPF_ORDER, - BPF_ORDER, VIS_E_CHAN, VIS_C_CHAN }; @@ -110,18 +108,10 @@ namespace PhaseCalculator // reset to perform after end of acquisition or update void reset(); - // Added by Sumedh to create low pass filter - // filter design copied from FilterNode - using LowPassFilter = Dsp::SimpleFilter - , // order - 1, // number of channels - Dsp::DirectFormI>; // realization - LowPassFilter filterlpf; // filter design copied from FilterNode using BandpassFilter = Dsp::SimpleFilter - , // order 1, // number of channels Dsp::DirectFormII>; // realization @@ -130,11 +120,9 @@ namespace PhaseCalculator BandpassFilter filter; + ARModeler arModeler; Array htState; - Array bpfState; - Array lpfState; - Array lpfStateMain; // number of samples until a new non-interpolated output. e.g. if this // equals 1 after a buffer is processed, then there is one interpolated @@ -232,13 +220,11 @@ namespace PhaseCalculator int getFullSourceId(int chan); // getters + int getAROrder() const; float getHighCut() const; float getLowCut() const; Band getBand() const; - - // Added by Sumedh to access the private variables - int getLpfOrder() const; - int getBpfOrder() const; + // reads from the visPhaseBuffer if it can acquire a TryLock. returns true if successful. bool tryToReadVisPhases(std::queue& other); @@ -267,6 +253,11 @@ namespace PhaseCalculator // Resets lowCut and highCut to defaults for the current band void resetCutsToDefaults(); + // Sets lowCut (which in turn influences highCut) + void setLowCut(float newLowCut); + + // Sets highCut (which in turn influences lowCut) + void setHighCut(float newHighCut); // Sets visContinuousChannel and updates the visualization filter void setVisContChan(int newChan); @@ -324,6 +315,22 @@ namespace PhaseCalculator void deactivateInputChannel(int chan); + // ---- static utility methods ---- + + /* + * arPredict: use autoregressive model of order to predict future data. + * + * lastSample points to the most recent sample of past data that will be used to + * compute phase, and there must be at least stride * (order - 1) samples + * preceding it in order to do the AR prediction. + * + * Input params is an array of coefficients of an AR model of length 'order'. + * + * Writes samps future data values to prediction. + */ + static void arPredict(const ReverseStack& history, int interpCountdown, double* prediction, + const double* params, int samps, int stride, int order); + // Get the htScaleFactor for the given band's Hilbert transformer, // over the range from lowCut and highCut. This is the reciprocal of the geometric // mean (i.e. mean in decibels) of the maximum and minimum magnitude responses over the range. @@ -331,17 +338,14 @@ namespace PhaseCalculator // Execute the hilbert transformer on one sample and update the state. static double htFilterSamp(double input, Band band, Array& state); - //Added by Sumedh - static double bpfFilterSamp(double input, Band band, Array& state); - static double lpfFilterSamp(double input, Band band, Array& state); - static double lpfFilter(double input, Band band, Array& state); - // ---- customizable parameters ------ - // Added by Sumedh - // Use LAA to return phase angle in degrees - double LAA(std::complex); - int lpforder; - int bpforder; + // ---- customizable parameters ------ + + // time to wait between AR model recalculations in ms + int calcInterval; + + // order of the AR model + int arOrder; OutputMode outputMode; diff --git a/ASICPhaseCalculator/Source/PhaseCalculatorCanvas.cpp b/PhaseCalculator/Source/PhaseCalculatorCanvas.cpp similarity index 100% rename from ASICPhaseCalculator/Source/PhaseCalculatorCanvas.cpp rename to PhaseCalculator/Source/PhaseCalculatorCanvas.cpp diff --git a/ASICPhaseCalculator/Source/PhaseCalculatorCanvas.h b/PhaseCalculator/Source/PhaseCalculatorCanvas.h similarity index 100% rename from ASICPhaseCalculator/Source/PhaseCalculatorCanvas.h rename to PhaseCalculator/Source/PhaseCalculatorCanvas.h diff --git a/ASICPhaseCalculator/Source/PhaseCalculatorEditor.cpp b/PhaseCalculator/Source/PhaseCalculatorEditor.cpp similarity index 64% rename from ASICPhaseCalculator/Source/PhaseCalculatorEditor.cpp rename to PhaseCalculator/Source/PhaseCalculatorEditor.cpp index 7c47462..7a1ecc5 100644 --- a/ASICPhaseCalculator/Source/PhaseCalculatorEditor.cpp +++ b/PhaseCalculator/Source/PhaseCalculatorEditor.cpp @@ -28,14 +28,14 @@ along with this program. If not, see . #include // abs namespace PhaseCalculator -{ //added by sumedh +{ Editor::Editor(Node* parentNode, bool useDefaultParameterEditors) - : VisualizerEditor(parentNode, 250, useDefaultParameterEditors) + : VisualizerEditor(parentNode, 220, useDefaultParameterEditors) , extraChanManager(parentNode) , prevExtraChans(0) { tabText = "Event Phase Plot"; - int filterWidth = 5; + int filterWidth = 120; // make the canvas now, so that restoring its parameters always works. canvas = new Canvas(parentNode); @@ -57,8 +57,88 @@ namespace PhaseCalculator bandBox->addListener(this); addAndMakeVisible(bandBox); + lowCutLabel = new Label("lowCutL", "Low:"); + lowCutLabel->setBounds(5, 70, 50, 20); + lowCutLabel->setFont({ "Small Text", 12, Font::plain }); + lowCutLabel->setColour(Label::textColourId, Colours::darkgrey); + addAndMakeVisible(lowCutLabel); + + lowCutEditable = new Label("lowCutE"); + lowCutEditable->setEditable(true); + lowCutEditable->addListener(this); + lowCutEditable->setBounds(50, 70, 35, 18); + lowCutEditable->setText(String(parentNode->lowCut), dontSendNotification); + lowCutEditable->setColour(Label::backgroundColourId, Colours::grey); + lowCutEditable->setColour(Label::textColourId, Colours::white); + addAndMakeVisible(lowCutEditable); + + lowCutUnit = new Label("lowCutU", "Hz"); + lowCutUnit->setBounds(85, 70, 25, 18); + lowCutUnit->setFont({ "Small Text", 12, Font::plain }); + lowCutUnit->setColour(Label::textColourId, Colours::darkgrey); + addAndMakeVisible(lowCutUnit); + + highCutLabel = new Label("highCutL", "High:"); + highCutLabel->setBounds(5, 100, 50, 20); + highCutLabel->setFont({ "Small Text", 12, Font::plain }); + highCutLabel->setColour(Label::textColourId, Colours::darkgrey); + addAndMakeVisible(highCutLabel); + + highCutEditable = new Label("highCutE"); + highCutEditable->setEditable(true); + highCutEditable->addListener(this); + highCutEditable->setBounds(50, 100, 35, 18); + highCutEditable->setText(String(parentNode->highCut), dontSendNotification); + highCutEditable->setColour(Label::backgroundColourId, Colours::grey); + highCutEditable->setColour(Label::textColourId, Colours::white); + addAndMakeVisible(highCutEditable); + + highCutUnit = new Label("highCutU", "Hz"); + highCutUnit->setBounds(85, 100, 25, 18); + highCutUnit->setFont({ "Small Text", 12, Font::plain }); + highCutUnit->setColour(Label::textColourId, Colours::darkgrey); + addAndMakeVisible(highCutUnit); + + recalcIntervalLabel = new Label("recalcL", "AR Refresh:"); + recalcIntervalLabel->setBounds(filterWidth, 25, 100, 20); + recalcIntervalLabel->setFont({ "Small Text", 12, Font::plain }); + recalcIntervalLabel->setColour(Label::textColourId, Colours::darkgrey); + addAndMakeVisible(recalcIntervalLabel); + + recalcIntervalEditable = new Label("recalcE"); + recalcIntervalEditable->setEditable(true); + recalcIntervalEditable->addListener(this); + recalcIntervalEditable->setBounds(filterWidth + 5, 44, 55, 18); + recalcIntervalEditable->setColour(Label::backgroundColourId, Colours::grey); + recalcIntervalEditable->setColour(Label::textColourId, Colours::white); + recalcIntervalEditable->setText(String(parentNode->calcInterval), dontSendNotification); + recalcIntervalEditable->setTooltip(recalcIntervalTooltip); + addAndMakeVisible(recalcIntervalEditable); + + recalcIntervalUnit = new Label("recalcU", "ms"); + recalcIntervalUnit->setBounds(filterWidth + 60, 47, 25, 15); + recalcIntervalUnit->setFont({ "Small Text", 12, Font::plain }); + recalcIntervalUnit->setColour(Label::textColourId, Colours::darkgrey); + addAndMakeVisible(recalcIntervalUnit); + + arOrderLabel = new Label("arOrderL", "Order:"); + arOrderLabel->setBounds(filterWidth, 65, 60, 20); + arOrderLabel->setFont({ "Small Text", 12, Font::plain }); + arOrderLabel->setColour(Label::textColourId, Colours::darkgrey); + addAndMakeVisible(arOrderLabel); + + arOrderEditable = new Label("arOrderE"); + arOrderEditable->setEditable(true); + arOrderEditable->addListener(this); + arOrderEditable->setBounds(filterWidth + 55, 66, 25, 18); + arOrderEditable->setColour(Label::backgroundColourId, Colours::grey); + arOrderEditable->setColour(Label::textColourId, Colours::white); + arOrderEditable->setText(String(parentNode->arOrder), sendNotificationAsync); + arOrderEditable->setTooltip(arOrderTooltip); + addAndMakeVisible(arOrderEditable); + outputModeLabel = new Label("outputModeL", "Output:"); - outputModeLabel->setBounds(filterWidth, 72, 70, 20); + outputModeLabel->setBounds(filterWidth, 87, 70, 20); outputModeLabel->setFont({ "Small Text", 12, Font::plain }); outputModeLabel->setColour(Label::textColourId, Colours::darkgrey); addAndMakeVisible(outputModeLabel); @@ -70,7 +150,7 @@ namespace PhaseCalculator outputModeBox->addItem("IMAG", IM); outputModeBox->setSelectedId(parentNode->outputMode); outputModeBox->setTooltip(outputModeTooltip); - outputModeBox->setBounds(filterWidth + 5, 92, 76, 19); + outputModeBox->setBounds(filterWidth + 5, 105, 76, 19); outputModeBox->addListener(this); addAndMakeVisible(outputModeBox); @@ -97,6 +177,47 @@ namespace PhaseCalculator void Editor::labelTextChanged(Label* labelThatHasChanged) { Node* processor = static_cast(getProcessor()); + + if (labelThatHasChanged == recalcIntervalEditable) + { + int intInput; + bool valid = updateControl(labelThatHasChanged, 0, INT_MAX, processor->calcInterval, intInput); + + if (valid) + { + processor->setParameter(RECALC_INTERVAL, static_cast(intInput)); + } + } + else if (labelThatHasChanged == arOrderEditable) + { + int intInput; + bool valid = updateControl(labelThatHasChanged, 1, INT_MAX, processor->arOrder, intInput); + + if (valid) + { + processor->setParameter(AR_ORDER, static_cast(intInput)); + } + } + else if (labelThatHasChanged == lowCutEditable) + { + float floatInput; + bool valid = updateControl(labelThatHasChanged, 0.0f, FLT_MAX, processor->lowCut, floatInput); + + if (valid) + { + processor->setParameter(LOWCUT, floatInput); + } + } + else if (labelThatHasChanged == highCutEditable) + { + float floatInput; + bool valid = updateControl(labelThatHasChanged, 0.0f, FLT_MAX, processor->highCut, floatInput); + + if (valid) + { + processor->setParameter(HIGHCUT, floatInput); + } + } } void Editor::channelChanged(int chan, bool newState) @@ -144,7 +265,10 @@ namespace PhaseCalculator void Editor::startAcquisition() { - bandBox->setEnabled(true); + bandBox->setEnabled(false); + lowCutEditable->setEnabled(false); + highCutEditable->setEnabled(false); + arOrderEditable->setEnabled(false); outputModeBox->setEnabled(false); channelSelector->inactivateButtons(); } @@ -152,6 +276,9 @@ namespace PhaseCalculator void Editor::stopAcquisition() { bandBox->setEnabled(true); + lowCutEditable->setEnabled(true); + highCutEditable->setEnabled(true); + arOrderEditable->setEnabled(true); outputModeBox->setEnabled(true); channelSelector->activateButtons(); } @@ -223,6 +350,8 @@ namespace PhaseCalculator Node* processor = (Node*)(getProcessor()); XmlElement* paramValues = xml->createNewChildElement("VALUES"); + paramValues->setAttribute("calcInterval", processor->calcInterval); + paramValues->setAttribute("arOrder", processor->arOrder); paramValues->setAttribute("lowCut", processor->lowCut); paramValues->setAttribute("highCut", processor->highCut); paramValues->setAttribute("outputMode", processor->outputMode); @@ -239,11 +368,26 @@ namespace PhaseCalculator forEachXmlChildElementWithTagName(*xml, xmlNode, "VALUES") { // some parameters have two fallbacks for backwards compatability + recalcIntervalEditable->setText(xmlNode->getStringAttribute("calcInterval", recalcIntervalEditable->getText()), sendNotificationSync); + arOrderEditable->setText(xmlNode->getStringAttribute("arOrder", arOrderEditable->getText()), sendNotificationSync); bandBox->setSelectedId(selectBandFromSavedParams(xmlNode) + 1, sendNotificationSync); + lowCutEditable->setText(xmlNode->getStringAttribute("lowCut", lowCutEditable->getText()), sendNotificationSync); + highCutEditable->setText(xmlNode->getStringAttribute("highCut", highCutEditable->getText()), sendNotificationSync); outputModeBox->setSelectedId(xmlNode->getIntAttribute("outputMode", outputModeBox->getSelectedId()), sendNotificationSync); } } + void Editor::refreshLowCut() + { + auto p = static_cast(getProcessor()); + lowCutEditable->setText(String(p->lowCut), dontSendNotification); + } + + void Editor::refreshHighCut() + { + auto p = static_cast(getProcessor()); + highCutEditable->setText(String(p->highCut), dontSendNotification); + } void Editor::refreshVisContinuousChan() { diff --git a/ASICPhaseCalculator/Source/PhaseCalculatorEditor.h b/PhaseCalculator/Source/PhaseCalculatorEditor.h similarity index 88% rename from ASICPhaseCalculator/Source/PhaseCalculatorEditor.h rename to PhaseCalculator/Source/PhaseCalculatorEditor.h index a9864e5..174f0e1 100644 --- a/ASICPhaseCalculator/Source/PhaseCalculatorEditor.h +++ b/PhaseCalculator/Source/PhaseCalculatorEditor.h @@ -62,6 +62,8 @@ namespace PhaseCalculator void loadCustomParameters(XmlElement* xml) override; // display updaters - do not trigger listeners. + void refreshLowCut(); + void refreshHighCut(); void refreshVisContinuousChan(); private: @@ -139,15 +141,30 @@ namespace PhaseCalculator ScopedPointer