/***************************************************************** BioZen Copyright (C) 2011 The National Center for Telehealth and Technology Eclipse Public License 1.0 (EPL-1.0) This library is free software; you can redistribute it and/or modify it under the terms of the Eclipse Public License as published by the Free Software Foundation, version 1.0 of the License. The Eclipse Public License is a reciprocal license, under Section 3. REQUIREMENTS iv) states that source code for the Program is available from such Contributor, and informs licensees how to obtain it in a reasonable manner on or through a medium customarily used for software exchange. Post your updates and modifications to our GitHub or email to t2@tee2.org. This library is distributed WITHOUT ANY WARRANTY; without the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the Eclipse Public License 1.0 (EPL-1.0) for more details. You should have received a copy of the Eclipse Public License along with this library; if not, visit http://www.opensource.org/licenses/EPL-1.0 *****************************************************************/ package com.t2.compassionMeditation; // not used right now import java.util.Arrays; import org.t2health.lib1.dsp.T2Filter; import android.util.Log; /** * Detects heart rate given an ECG waveform * * @author scott.coleman * * min bpm = 40 1500 ms * Max bpm = 180 333.3 ms * */ public class T2HeartRateDetector extends T2Filter { private static final String TAG = "BFDemo"; private static int DEFAULT_SIZE = 100; private int circularBuffer[]; public int mean; private int total; private int circularIndex; private int totalSamples; private int size; private int min; /** * maximum sample value in circular buffer * Note that this will immediately follow all new values that are higher than the old max. * and it will fall back to previous maximums after the buffer goes through all values in it's queue */ private int max; private double hrPeriodCounterMs; private int hrBpm; private int previousTimeStamp; private int previousSampleValue; private int numHRReports; private double sampleSlope = 0; public int getMin() { return min; } public int getMax() { return max; } public T2HeartRateDetector() { circularBuffer = new int[DEFAULT_SIZE]; reset(); } /** * @param dummy */ void dumpBuffer(int dummy) { String s = "circularIndex= " + circularIndex + "; "; for (int i = 0; i < circularBuffer.length; i++) { s += circularBuffer[i] + "; "; } Log.e(TAG, s); } /* (non-Javadoc) * @see org.t2health.lib1.dsp.T2Filter#filter(int, int) * @param sampleValue ADC sample value * @param sampleTimeStamp Shimmer time stamp (note 640 = 20 ms) * * @return HR in beats per minute (or -1 if unreasonable) */ @Override public int filter( int sampleValue, int sampleTimeStamp ) { int deltaTimeStamp; // Account for the fact that the timestamp rolls over at 65535 if (sampleTimeStamp < previousTimeStamp) { deltaTimeStamp = 65536 - previousTimeStamp + sampleTimeStamp; } else { deltaTimeStamp = sampleTimeStamp - previousTimeStamp; } previousTimeStamp = sampleTimeStamp; // Convert timestamp to milliseconds double deltaTimeStampMs = deltaTimeStamp / 32; if (totalSamples++ == 0) { primeBuffer(sampleValue); } int lastBufferValue = circularBuffer[circularIndex]; total -= lastBufferValue; total += sampleValue; mean = total / circularBuffer.length; circularBuffer[circularIndex] = sampleValue; circularIndex = nextIndex(circularIndex); // Set min and max int[] tmp = circularBuffer.clone(); Arrays.sort(tmp); min = tmp[0]; max = tmp[tmp.length - 1]; float threshold = (float) ((float) max * 0.6); int ithreshold = (int) threshold; // Now do HR calculation hrPeriodCounterMs += deltaTimeStampMs; sampleSlope = (sampleValue - previousSampleValue) / 20F; boolean rDetected = false; // Per research from HR detection algorithms we set the threshold for the R-wave detection // at 60% of the most recent maximum. if (sampleValue > ithreshold) { // We're above the threshold so ostensibly we've detected an R-wave // Don't report the very first one since we might have started counting after the start of // the R wave. if (numHRReports++ == 0) { // Print header // Log.e(TAG, ",sampleValue, sampleSlope, max, ithreshold, mean, marker, hrBpm"); Log.e(TAG, ",sampleValue, hrPeriodCounterMs, marker, hrBpm"); hrPeriodCounterMs = 0; // Restart the period counter and do nothing else } else { // Do limit checking if (hrPeriodCounterMs > 200F) { if (hrPeriodCounterMs > 360F) { rDetected = true; double fBpm = (1F / (hrPeriodCounterMs - deltaTimeStampMs)) * 60000F; // Need to subtract the delta time stamp back out int bpm = (int) Math.round(fBpm); if (hrPeriodCounterMs <= 1500F) { hrBpm = bpm; // Limit the count to 40 BPM // This is in case we miss a beat // So we won't contaminate a good heart rate with one with a missed beat } hrPeriodCounterMs = 0; // Restart the period counter and do nothing else } else { // If the QRS was < 360 ms We need to make sure we have the R wave instead of the T wave if (sampleSlope > 2) { rDetected = true; double fBpm = (1F / (hrPeriodCounterMs - deltaTimeStampMs)) * 60000F; // Need to subtract the delta time stamp back out int bpm = (int) Math.round(fBpm); hrBpm = bpm; hrPeriodCounterMs = 0; // Restart the period counter and do nothing else } } } } } // Do overall sanity checks // Don't report unless the mean is < 50 (mean is an indicator of artifact if (mean > 50 || mean < -50) { hrBpm = -1; } // Log.e(TAG, String.format(",%d, %2.2f, %d, %d, %d, %d, %d", sampleValue, (float) sampleSlope, max, ithreshold, mean, rDetected ? 150:200, hrBpm)); // Log.e(TAG, String.format(",%d, %f, %d, %d", sampleValue, hrPeriodCounterMs, rDetected ? 150:200, hrBpm)); previousSampleValue = sampleValue; return hrBpm; } public int getValue() { return mean; } /** * Returns the variance of all samples in the circular buffer * @return variance */ public double getVariance() { int sdIndex = circularIndex; long n = 0; double mean = 0; double s = 0.0; for (int i = 0; i < circularBuffer.length; ++i) { double val = circularBuffer[sdIndex]; ++n; double delta = val - mean; mean += delta / n; s += delta * (val - mean); sdIndex = nextIndex(sdIndex); } return (s / n); } /** * Returns the standard deviation of all samples in the circular buffer * @return standard deviation */ public double getStdDev() { return Math.sqrt(getVariance()); } /** * Resets the detector */ public void reset() { totalSamples = 0; circularIndex = 0; mean = 0; total = 0; hrPeriodCounterMs = 0; previousTimeStamp = 0; numHRReports = 0; hrBpm = -1; previousSampleValue = -1; } /** * Returns the total number of samples that have been fed to the detector * @return total number of samples */ public long getTotalSamples() { return totalSamples; } /** * Primes the circular buffer with a value * @param val */ private void primeBuffer(int val) { for (int i = 0; i < circularBuffer.length; ++i) { circularBuffer[i] = val; total += val; } mean = val; } /** * Encapsulaes nagivating the circular buffer * @param curIndex current index into the * @return next index into the circular buffer */ private int nextIndex(int curIndex) { if (curIndex + 1 >= circularBuffer.length) { return 0; } return curIndex + 1; } }