diff --git a/android/ConductorsPhone/build.gradle b/android/ConductorsPhone/build.gradle index e6b32bc..1a3d812 100644 --- a/android/ConductorsPhone/build.gradle +++ b/android/ConductorsPhone/build.gradle @@ -7,7 +7,7 @@ buildscript { jcenter() } dependencies { - classpath 'com.android.tools.build:gradle:3.0.1' + classpath 'com.android.tools.build:gradle:3.1.2' // NOTE: Do not place your application dependencies here; they belong diff --git a/android/ConductorsPhone/gradle/wrapper/gradle-wrapper.properties b/android/ConductorsPhone/gradle/wrapper/gradle-wrapper.properties index 9d01135..179b58d 100644 --- a/android/ConductorsPhone/gradle/wrapper/gradle-wrapper.properties +++ b/android/ConductorsPhone/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ -#Tue Dec 19 11:59:51 CET 2017 +#Fri Apr 27 11:02:05 CEST 2018 distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-4.1-all.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-4.4-all.zip diff --git a/android/ConductorsWatch/app/src/main/java/de/tonifetzer/conductorswatch/Estimator.java b/android/ConductorsWatch/app/src/main/java/de/tonifetzer/conductorswatch/Estimator.java index c63e38e..28e8378 100644 --- a/android/ConductorsWatch/app/src/main/java/de/tonifetzer/conductorswatch/Estimator.java +++ b/android/ConductorsWatch/app/src/main/java/de/tonifetzer/conductorswatch/Estimator.java @@ -6,10 +6,14 @@ import android.hardware.SensorEvent; import android.hardware.SensorEventListener; import android.hardware.SensorManager; import android.os.Handler; +import android.util.Log; +import java.util.ArrayList; +import java.util.LinkedList; import java.util.List; import java.util.Timer; import java.util.TimerTask; +import java.util.Vector; import java.util.concurrent.CopyOnWriteArrayList; import java.util.concurrent.Executors; import java.util.concurrent.ScheduledExecutorService; @@ -21,6 +25,7 @@ import de.tonifetzer.conductorswatch.bpmEstimation.BpmEstimator; import de.tonifetzer.conductorswatch.network.SensorDataFileSender; import de.tonifetzer.conductorswatch.network.SensorDataFileStreamer; import de.tonifetzer.conductorswatch.utilities.ByteStreamWriter; +import de.tonifetzer.conductorswatch.utilities.Utils; /** * Created by toni on 13/11/17. @@ -34,6 +39,7 @@ public class Estimator implements SensorEventListener { private Context mContext; private AccelerometerWindowBuffer mAccelerometerWindowBuffer; private BpmEstimator mBpmEstimator; + private double mCurrentBpm; private ByteStreamWriter mByteStreamWriterAcc; private ByteStreamWriter mByteStreamWriterGyro; @@ -44,6 +50,7 @@ public class Estimator implements SensorEventListener { public Estimator(Context mContext){ this.mContext = mContext; + this.mCurrentBpm = -1; } public void start() { @@ -54,7 +61,8 @@ public class Estimator implements SensorEventListener { mSensorManager.registerListener(this, mAccelerometer, SensorManager.SENSOR_DELAY_FASTEST); mSensorManager.registerListener(this, mRawAccelerometer, SensorManager.SENSOR_DELAY_FASTEST); - mAccelerometerWindowBuffer = new AccelerometerWindowBuffer(6000, 1500); + + mAccelerometerWindowBuffer = new AccelerometerWindowBuffer(6000, 750); mBpmEstimator = new BpmEstimator(mAccelerometerWindowBuffer, 0, 5000); mTimer.scheduleAtFixedRate(new TimerTask() { @@ -62,11 +70,89 @@ public class Estimator implements SensorEventListener { public void run() { if (mAccelerometerWindowBuffer.isNextWindowReady()) { - double bpm = mBpmEstimator.estimate(mAccelerometerWindowBuffer.getFixedWindow()); + + LinkedList bpmList = new LinkedList<>(); + + //todo: wie viele dieser Klassen kann ich wegwerfen um das Ergebnis nich schlechter zu machen? + double bpm60 = mBpmEstimator.estimate(mAccelerometerWindowBuffer.getFixedSizedWindow()); + double bpm85 = mBpmEstimator.estimate(mAccelerometerWindowBuffer.getFixedSizedWindow(3500, 750)); + double bpm110 = mBpmEstimator.estimate(mAccelerometerWindowBuffer.getFixedSizedWindow(2600, 750)); + double bpm135 = mBpmEstimator.estimate(mAccelerometerWindowBuffer.getFixedSizedWindow(2000, 750)); + double bpm160 = mBpmEstimator.estimate(mAccelerometerWindowBuffer.getFixedSizedWindow(1600,750)); + double bpm200 = mBpmEstimator.estimate(mAccelerometerWindowBuffer.getFixedSizedWindow(1200, 750)); + + //add to list + //todo: make this cool... + //vielleicht einen weighted mean? + bpmList.add(bpm60); + bpmList.add(bpm85); + bpmList.add(bpm110); //110, 135, 160 langen auch schon + bpmList.add(bpm135); + bpmList.add(bpm160); + bpmList.add(bpm200); + + //Log.d("BPM: ", bpmList.toString()); + + //remove all -1 and calc bpmMean + while(bpmList.remove(Double.valueOf(-1))) {} + + //remove outliers + //todo: aktuell wird die liste hier sortiert.. eig net so schön. + Utils.removeOutliersZScore(bpmList, 3.4); + //Utils.removeOutliersHeuristic(); + + //Log.d("BPM: ", bpmList.toString()); + + double bpm = -1; + if(!bpmList.isEmpty()) { + double bpmMean = Utils.mean(bpmList); + double bpmMedian = Utils.median(bpmList); + + double bpmDiffSlowFast = bpmList.getFirst() - bpmList.getLast(); + if (Math.abs(bpmDiffSlowFast) > 25) { + + double tmpBPM = bpmMean + 25; + + while(bpm == -1){ + if (tmpBPM < 60) { + bpm = bpm60; + if(bpm == -1){ + bpm = bpmMean; + } + } + else if (tmpBPM < 85) { + bpm = bpm85; + } else if (tmpBPM < 110) { + bpm = bpm110; + } else if (tmpBPM < 135) { + bpm = bpm135; + } else if (tmpBPM < 160) { + bpm = bpm160; + } else { + bpm = bpm200; + } + + tmpBPM -= 5; + } + + //Log.d("BPM: ", "CHANGE"); + + } else { + + bpm = bpmMean; + //Log.d("BPM: ", "STAY"); + } + + + } + for (OnBpmEstimatorListener listener : listeners) { - listener.onNewDataAvailable(bpm); + listener.onNewDataAvailable(bpm); //135 gibt gute ergebnisse! } + + //update the windowSize and updaterate depending on current bpm + //updateWindowSizeAndOverlap(bpm); } } }, 0, 100); @@ -134,4 +220,39 @@ public class Estimator implements SensorEventListener { private List listeners = new CopyOnWriteArrayList(); public void add(OnBpmEstimatorListener listener){listeners.add(listener);} public void remove(OnBpmEstimatorListener listener){listeners.remove(listener);} + + /** + * Simple function that sets die windowSize and Overlap time to a specific value + * depending on the currentBPM. Nothing rly dynamical. However, should work out + * for our purposes. + * @param bpm + */ + private void updateWindowSizeAndOverlap(double bpm){ + + //round to nearest tenner. this limits the number of windowsize and overlap changes. + int newBpmRounded = (int) Math.round(bpm / 10.0) * 10; + int curBpmRounded = (int) Math.round(mCurrentBpm / 10.0) * 10; + + //TODO: i guess this is not the best method.. if the default sizes always produces -1, we run into problems + if(bpm != -1 && curBpmRounded != newBpmRounded){ + + int overlap_ms = 60000 / newBpmRounded; + int window_ms = overlap_ms * 5; + + //idea: wenn man mehrere fenster parallel laufen lässt und beobachtet, müsste das kleinste fenster die tempowechsel eigentlich am + // besten mitbekommen. dieses fenster dann bestimmen lassen? + + mAccelerometerWindowBuffer.setWindowSize(window_ms); + mAccelerometerWindowBuffer.setOverlapSize(overlap_ms); + + } else if (bpm == -1){ + //if bpm is -1 due to a non-classification, reset to default. + + //idea: anstatt auf einen festen wert zu setzen, könnte man das fenster dann auch einfach ein wenig größer / kleiner machen. + mAccelerometerWindowBuffer.setWindowSize(3000); + mAccelerometerWindowBuffer.setOverlapSize(750); + } + + mCurrentBpm = bpm; + } } diff --git a/android/ConductorsWatch/app/src/main/java/de/tonifetzer/conductorswatch/MainActivity.java b/android/ConductorsWatch/app/src/main/java/de/tonifetzer/conductorswatch/MainActivity.java index f5ce3a7..7d0d796 100644 --- a/android/ConductorsWatch/app/src/main/java/de/tonifetzer/conductorswatch/MainActivity.java +++ b/android/ConductorsWatch/app/src/main/java/de/tonifetzer/conductorswatch/MainActivity.java @@ -307,6 +307,8 @@ public class MainActivity extends WearableActivity implements WorkerFragment.OnF @Override protected void onStart() { super.onStart(); + + mSender.wakeUpPhoneCall(); } @Override diff --git a/android/ConductorsWatch/app/src/main/java/de/tonifetzer/conductorswatch/WorkerFragment.java b/android/ConductorsWatch/app/src/main/java/de/tonifetzer/conductorswatch/WorkerFragment.java index 1dc8116..9ca89ee 100644 --- a/android/ConductorsWatch/app/src/main/java/de/tonifetzer/conductorswatch/WorkerFragment.java +++ b/android/ConductorsWatch/app/src/main/java/de/tonifetzer/conductorswatch/WorkerFragment.java @@ -144,14 +144,14 @@ public class WorkerFragment extends Fragment implements Metronome.OnMetronomeLis public void onNewDataAvailable(double bpm) { //TODO: what if multiple threads access mBpmList? put into synchronized? @frank fragen :D - //TODO: send this to smartphone if(mWorkerRunning){ + + mBpmList.add(bpm); + if(bpm == -1){ //to stuff with UI. Write Text or make XX or something like that } - mBpmList.add(bpm); - // we need this here, since ui elements can only be changed within activity thread and // onNewDataAvailable lives inside the bpm estimation thread. // TODO: is this really okay? also synchronized? diff --git a/android/ConductorsWatch/app/src/main/java/de/tonifetzer/conductorswatch/bpmEstimation/AccelerometerWindowBuffer.java b/android/ConductorsWatch/app/src/main/java/de/tonifetzer/conductorswatch/bpmEstimation/AccelerometerWindowBuffer.java index f9ec264..e7de7ff 100644 --- a/android/ConductorsWatch/app/src/main/java/de/tonifetzer/conductorswatch/bpmEstimation/AccelerometerWindowBuffer.java +++ b/android/ConductorsWatch/app/src/main/java/de/tonifetzer/conductorswatch/bpmEstimation/AccelerometerWindowBuffer.java @@ -2,6 +2,7 @@ package de.tonifetzer.conductorswatch.bpmEstimation; import de.tonifetzer.conductorswatch.utilities.Utils; import java.util.ArrayList; +import java.util.List; /** * Created by toni on 15/12/17. @@ -34,23 +35,37 @@ public class AccelerometerWindowBuffer extends ArrayList { //add element boolean r = super.add(ad); - if ((getYongest().ts - getOldest().ts) > mWindowSize){ + removeOldElements(); - long oldestTime = getYongest().ts - mWindowSize; - for(int i = 0; i < size(); ++i) { - if (get(i).ts > oldestTime) { - break; - } - remove(i); - } - } return r; } } + private void removeOldElements(){ + synchronized (this) { + if (!isEmpty()) { + if ((getYongest().ts - getOldest().ts) > mWindowSize) { + + long oldestTime = getYongest().ts - mWindowSize; + for (int i = 0; i < size(); ++i) { + if (get(i).ts > oldestTime) { + break; + } + super.remove(i); + } + } + } + } + } + + public boolean isNextWindowReady(long lastWindowTS, int overlapSize){ + return true; + } + public boolean isNextWindowReady(){ + if(!isEmpty()){ - if(((getYongest().ts - getOldest().ts) > mWindowSize / 2) && mOverlapCounter > mOverlapSize){ + if(((getYongest().ts - getOldest().ts) > mWindowSize / 4) && mOverlapCounter > mOverlapSize){ mOverlapCounter = 0; return true; @@ -59,22 +74,40 @@ public class AccelerometerWindowBuffer extends ArrayList { return false; } - public AccelerometerWindowBuffer getFixedWindow(){ - AccelerometerWindowBuffer other = new AccelerometerWindowBuffer(mWindowSize, mOverlapSize); - synchronized (this){ - for(AccelerometerData data : this){ - other.add(new AccelerometerData(data)); - } + public AccelerometerWindowBuffer getFixedSizedWindow(int size, int overlap){ + AccelerometerWindowBuffer other = new AccelerometerWindowBuffer(size, overlap); + + double sampleRate = ((getYongest().ts - getOldest().ts) / super.size()); + + //if current size is smaller then wanted size, start at 0 and provide smaller list + int start = 0; + if((getYongest().ts - getOldest().ts) > size){ + start = (int) Math.round(super.size() - (size / sampleRate)); } + + // start should not be negative, this can happen due to rounding errors. + start = start < 0? 0 : start; + + synchronized (this) { + other.addAll(super.subList(start, super.size())); + } + return other; } + public AccelerometerWindowBuffer getFixedSizedWindow(){ + return getFixedSizedWindow(mWindowSize, mOverlapSize); + } + + //TODO: check if list is empty! this causes indexoutofbounce public AccelerometerData getYongest() { - return get(size() - 1); + synchronized (this){ + return super.get(size() - 1); + } } public AccelerometerData getOldest() { - return get(0); + return super.get(0); } public double[] getX(){ @@ -96,4 +129,22 @@ public class AccelerometerWindowBuffer extends ArrayList { public int getOverlapSize(){ return mOverlapSize; } + + public void setWindowSize(int size_ms){ + this.mWindowSize = size_ms; + removeOldElements(); // need to call this here, to remove too old elements, if the windowSize gets smaller. + } + + public void setOverlapSize(int size_ms){ + this.mOverlapSize = size_ms; + this.mOverlapCounter = 0; + } + + @Override + public void clear(){ + synchronized (this){ + super.clear(); + this.mOverlapCounter = 0; + } + } } \ No newline at end of file diff --git a/android/ConductorsWatch/app/src/main/java/de/tonifetzer/conductorswatch/bpmEstimation/BpmEstimator.java b/android/ConductorsWatch/app/src/main/java/de/tonifetzer/conductorswatch/bpmEstimation/BpmEstimator.java index 824687e..85fc1f0 100644 --- a/android/ConductorsWatch/app/src/main/java/de/tonifetzer/conductorswatch/bpmEstimation/BpmEstimator.java +++ b/android/ConductorsWatch/app/src/main/java/de/tonifetzer/conductorswatch/bpmEstimation/BpmEstimator.java @@ -40,6 +40,9 @@ public class BpmEstimator { mResetCounter = 0; mResetLimit_ms = resetAfter_ms; + + //TODO: this is to easy. since the new dyn. windowsize produces smaller update times, we need to consider something, that + //TODO: holds more values, if they are similar, an resets the history if not. mMvg = new MovingFilter(2); //mKalman = new SimpleKalman(); } @@ -52,6 +55,10 @@ public class BpmEstimator { sampleRate = Math.round(Utils.mean(Utils.diff(fixedWindow.getTs()))); } + if(sampleRate == 0){ + int breakhere = 0; + } + AccelerometerInterpolator interp = new AccelerometerInterpolator(fixedWindow, sampleRate); //are we conducting? @@ -80,6 +87,8 @@ public class BpmEstimator { mMvg.add(estimatedBPM); mBpmHistory.add(mMvg.getAverage()); + //mBpmHistory.add(estimatedBPM); + //moving median (lohnt nur bei konstantem tempo, da wir nur ein tempo damit gut halten können.) //mMvg.add(estimatedBPM); //mBpmHistory.add(mMvg.getMedian()); @@ -96,7 +105,7 @@ public class BpmEstimator { mBpmHistory.clear(); //TODO: send signal to clear. - mBuffer.clear(); + //mBuffer.clear(); mMvg.clear(); mResetCounter = 0; } diff --git a/android/ConductorsWatch/app/src/main/java/de/tonifetzer/conductorswatch/utilities/Utils.java b/android/ConductorsWatch/app/src/main/java/de/tonifetzer/conductorswatch/utilities/Utils.java index bb76e58..90023e9 100644 --- a/android/ConductorsWatch/app/src/main/java/de/tonifetzer/conductorswatch/utilities/Utils.java +++ b/android/ConductorsWatch/app/src/main/java/de/tonifetzer/conductorswatch/utilities/Utils.java @@ -4,8 +4,10 @@ import android.content.Context; import android.content.res.Resources; import android.util.DisplayMetrics; +import java.util.ArrayList; import java.util.Comparator; -import java.util.LinkedList; +import java.util.Iterator; +import java.util.List; //TODO: change from double to generic type public class Utils { @@ -37,7 +39,7 @@ public class Utils { return sum; } - public static double sum (LinkedList data){ + public static double sum (List data){ double sum = 0; for (int i = 0; i < data.size(); i++) { sum += data.get(i).doubleValue(); @@ -81,11 +83,11 @@ public class Utils { return (double) sum(data) / (double) data.length; } - public static double mean(LinkedList data){ + public static double mean(List data){ return sum(data) / data.size(); } - public static double median(LinkedList data){ + public static double median(List data){ data.sort(Comparator.naturalOrder()); double median; @@ -97,6 +99,18 @@ public class Utils { return median; } + public static double mad(List data){ + + double median = median(data); + + List tmpList = new ArrayList<>(); + for(double value : data){ + tmpList.add(Math.abs(value - median)); + } + + return median(tmpList); + } + //TODO: Could be slow.. faster method? public static double geometricMean(double[] data) { double sum = data[0]; @@ -173,4 +187,17 @@ public class Utils { return px / ((float) metrics.densityDpi / DisplayMetrics.DENSITY_DEFAULT); } + public static void removeOutliersZScore(List data, double score) { + + if(!data.isEmpty()){ + double median = median(data); + double mad = mad(data); + + for(Iterator it = data.iterator(); it.hasNext(); ){ + + double M = Math.abs((0.6745 * (it.next() - median)) / mad); + if (M > score){ it.remove(); } + } + } + } } diff --git a/android/ConductorsWatch/build.gradle b/android/ConductorsWatch/build.gradle index 3c64aec..9cbd907 100644 --- a/android/ConductorsWatch/build.gradle +++ b/android/ConductorsWatch/build.gradle @@ -10,7 +10,7 @@ buildscript { google() } dependencies { - classpath 'com.android.tools.build:gradle:3.0.1' + classpath 'com.android.tools.build:gradle:3.1.2' // NOTE: Do not place your application dependencies here; they belong diff --git a/android/ConductorsWatch/gradle/wrapper/gradle-wrapper.properties b/android/ConductorsWatch/gradle/wrapper/gradle-wrapper.properties index b2c9729..e5a8c8e 100644 --- a/android/ConductorsWatch/gradle/wrapper/gradle-wrapper.properties +++ b/android/ConductorsWatch/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ -#Mon Nov 13 10:12:40 CET 2017 +#Fri Apr 27 14:10:48 CEST 2018 distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-4.1-all.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-4.4-all.zip diff --git a/matlab/AutoCorrMethodNew_Watch.m b/matlab/AutoCorrMethodNew_Watch.m index ed967e9..dbf4f76 100644 --- a/matlab/AutoCorrMethodNew_Watch.m +++ b/matlab/AutoCorrMethodNew_Watch.m @@ -41,9 +41,9 @@ %measurements = dlmread('../measurements/wearR/recording_180bpm_4-4_177064915.csv', ';'); * -%files = dir(fullfile('../../measurements/lgWear/', '*.csv')); +files = dir(fullfile('../../measurements/lgWear/', '*.csv')); %files = dir(fullfile('../../measurements/wearR/', '*.csv')); -files = dir(fullfile('../../measurements/peter_failed/', '*.csv')); +%files = dir(fullfile('../../measurements/peter_failed/', '*.csv')); for file = files' @@ -52,7 +52,7 @@ for file = files' %draw the raw acc data m_idx = []; - m_idx = (measurements(:,2)==10); + m_idx = (measurements(:,2)==2); %Android App: 10, Normal Data: 2 m = measurements(m_idx, :); %Interpolate to generate a constant sample rate to 250hz (4ms per sample) @@ -71,13 +71,19 @@ for file = files' legend("x", "location", "eastoutside"); figure(2); - plot(m(:,1),m(:,4)) %y + plot(m(:,1),m(:,4)) %yt legend("y", "location", "eastoutside"); figure(3); plot(m(:,1),m(:,5)) %z legend("z", "location", "eastoutside"); + %magnitude + magnitude = sqrt(sum(m(:,3:5).^2,2)); + figure(5); + plot(m(:,1), magnitude); + legend("magnitude", "location", "eastoutside"); + waitforbuttonpress(); %save timestamps @@ -98,26 +104,39 @@ for file = files' [corr_x, lag_x] = xcov(m(i-window_size:i,3), (window_size/4), "coeff"); [corr_y, lag_y] = xcov(m(i-window_size:i,4), (window_size/4), "coeff"); [corr_z, lag_z] = xcov(m(i-window_size:i,5), (window_size/4), "coeff"); + [corr_mag, lag_mag] = xcov(magnitude(i-window_size:i), (window_size/4), "coeff"); + + + %autocorrelation of the autocorrelation?! + %[corr_corr_x, lag_lag_x] = xcov(corr_x, length(corr_x), "coeff"); + %[corr_corr_y, lag_lag_y] = xcov(corr_y, length(corr_x), "coeff"); + %[corr_corr_z, lag_lag_z] = xcov(corr_z, length(corr_x), "coeff"); + corr_x_pos = corr_x; corr_y_pos = corr_y; corr_z_pos = corr_z; + corr_mag_pos = corr_mag; corr_x_pos(corr_x_pos<0)=0; corr_y_pos(corr_y_pos<0)=0; corr_z_pos(corr_z_pos<0)=0; + corr_mag_pos(corr_mag_pos<0)=0; [peak_x, idx_x_raw] = findpeaks(corr_x_pos, 'MinPeakHeight', 0.1,'MinPeakDistance', 50, 'MinPeakProminence', 0.1); [peak_y, idx_y_raw] = findpeaks(corr_y_pos, 'MinPeakHeight', 0.1,'MinPeakDistance', 50, 'MinPeakProminence', 0.1); [peak_z, idx_z_raw] = findpeaks(corr_z_pos, 'MinPeakHeight', 0.1,'MinPeakDistance', 50, 'MinPeakProminence', 0.1); + [peak_mag, idx_mag_raw] = findpeaks(corr_mag_pos, 'MinPeakHeight', 0.1,'MinPeakDistance', 50, 'MinPeakProminence', 0.1); idx_x_raw = sort(idx_x_raw); idx_y_raw = sort(idx_y_raw); idx_z_raw = sort(idx_z_raw); + idx_mag_raw = sort(idx_mag_raw); idx_x = findFalseDetectedPeaks(idx_x_raw, lag_x, corr_x); idx_y = findFalseDetectedPeaks(idx_y_raw, lag_y, corr_y); idx_z = findFalseDetectedPeaks(idx_z_raw, lag_z, corr_z); + idx_mag = findFalseDetectedPeaks(idx_mag_raw, lag_mag, corr_mag); Xwindow = m(i-window_size:i,3); Xwindow_mean_ts_diff = mean(diff(lag_x(idx_x) * sample_rate_ms)); %2.5 ms is the time between two samples at 400hz @@ -155,6 +174,20 @@ for file = files' title(strcat(" ", m_label_ms, " ", m_label_bpm)); hold ("off"); + %magnitude + Mwindow = magnitude(i-window_size:i); + Mwindow_mean_ts_diff = mean(diff(lag_mag(idx_mag)* sample_rate_ms)); + Mwindow_mean_bpm = (60000 / (Mwindow_mean_ts_diff)); + + figure(14); + plot(lag_mag, corr_mag, lag_mag(idx_mag), corr_mag(idx_mag), 'r*', lag_mag(idx_mag_raw), corr_mag(idx_mag_raw), 'g*') %z + hold ("on") + m_label_ms = strcat(" mean ms: ", num2str(Mwindow_mean_ts_diff)); + m_label_bpm = strcat(" mean bpm: ", num2str(Mwindow_mean_bpm)); + title(strcat(" ", m_label_ms, " ", m_label_bpm)); + hold ("off"); + + %breakpoints dummy for testing if(length(idx_x) > length(idx_x_raw))