small refactoring

This commit is contained in:
toni
2019-01-30 16:24:42 +01:00
parent 72a672a80b
commit 691f3a684a
6 changed files with 4 additions and 509 deletions

View File

@@ -16,7 +16,7 @@ public class Metronome extends TimerTask {
boolean loaded = false;
private int soundID;
public Metronome(Context ){
public Metronome(Context context){
soundPool = new SoundPool.Builder().setMaxStreams(10).build();
soundPool.setOnLoadCompleteListener(new SoundPool.OnLoadCompleteListener() {
@Override

View File

@@ -1,250 +0,0 @@
package de.tonifetzer.conductorswatch;
import android.content.Context;
import android.hardware.Sensor;
import android.hardware.SensorEvent;
import android.hardware.SensorEventListener;
import android.hardware.SensorManager;
import java.util.LinkedList;
import java.util.List;
import java.util.Timer;
import java.util.TimerTask;
import java.util.concurrent.CopyOnWriteArrayList;
import de.tonifetzer.conductorswatch.bpmEstimation.AccelerometerData;
import de.tonifetzer.conductorswatch.bpmEstimation.AccelerometerWindowBuffer;
import de.tonifetzer.conductorswatch.bpmEstimation.BpmEstimator;
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.
*/
public class Estimator implements SensorEventListener {
private SensorManager mSensorManager;
private Sensor mAccelerometer;
private Sensor mRawAccelerometer;
private Context mContext;
private AccelerometerWindowBuffer mAccelerometerWindowBuffer;
private BpmEstimator mBpmEstimator;
private double mCurrentBpm;
private ByteStreamWriter mByteStreamWriterAcc;
private ByteStreamWriter mByteStreamWriterGyro;
private SensorDataFileStreamer mStreamer;
private Timer mTimer = new Timer();
public Estimator(Context mContext){
this.mContext = mContext;
this.mCurrentBpm = -1;
}
public void start() {
mSensorManager = (SensorManager) mContext.getSystemService(Context.SENSOR_SERVICE);
mAccelerometer = mSensorManager.getDefaultSensor(Sensor.TYPE_LINEAR_ACCELERATION);
mRawAccelerometer = mSensorManager.getDefaultSensor(Sensor.TYPE_ACCELEROMETER);
mSensorManager.registerListener(this, mAccelerometer, SensorManager.SENSOR_DELAY_FASTEST);
mSensorManager.registerListener(this, mRawAccelerometer, SensorManager.SENSOR_DELAY_FASTEST);
mAccelerometerWindowBuffer = new AccelerometerWindowBuffer(6000, 750);
mBpmEstimator = new BpmEstimator(mAccelerometerWindowBuffer, 0, 5000);
mTimer.scheduleAtFixedRate(new TimerTask() {
@Override
public void run() {
if (mAccelerometerWindowBuffer.isNextWindowReady()) {
LinkedList<Double> 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); //135 gibt gute ergebnisse!
}
//update the windowSize and updaterate depending on current bpm
//updateWindowSizeAndOverlap(bpm);
}
}
}, 0, 100);
mByteStreamWriterAcc = new ByteStreamWriter();
mByteStreamWriterGyro = new ByteStreamWriter();
mStreamer = new SensorDataFileStreamer(mContext);
new Thread(mStreamer).start();
}
public void stop() {
mTimer.cancel();
mTimer.purge();
mSensorManager.unregisterListener(this);
//send data and close the ByteStreamWriter
for (OnBpmEstimatorListener listener : listeners) {
//listener.onEstimationStopped(mByteStreamWriter.getByteArray());
}
mByteStreamWriterAcc.close();
mByteStreamWriterGyro.close();
mStreamer.close();
}
@Override
public void onSensorChanged(SensorEvent se) {
//TODO: at the moment this runs in main thread... since worker fragment runs also in main thread
//ca 200hz, every 5 to 6 ms we have an update
if (se.sensor.getType() == Sensor.TYPE_LINEAR_ACCELERATION) {
mAccelerometerWindowBuffer.add(new AccelerometerData(System.currentTimeMillis(), se.values[0], se.values[1], se.values[2]));
mByteStreamWriterAcc.writeSensor3D(Sensor.TYPE_LINEAR_ACCELERATION, se.values[0], se.values[1], se.values[2]);
mStreamer.sendByteArray(mByteStreamWriterAcc.getByteArray());
mByteStreamWriterAcc.reset();
}
if (se.sensor.getType() == Sensor.TYPE_ACCELEROMETER) {
mByteStreamWriterGyro.writeSensor3D(Sensor.TYPE_ACCELEROMETER, se.values[0], se.values[1], se.values[2]);
mStreamer.sendByteArray(mByteStreamWriterGyro.getByteArray());
mByteStreamWriterGyro.reset();
}
}
@Override
public void onAccuracyChanged(Sensor sensor, int i) {
// do nothin
}
/**
* Interface for callback calculated bpm
*/
public interface OnBpmEstimatorListener {
void onNewDataAvailable(double bpm);
void onEstimationStopped(byte[] sensorData);
}
private List<OnBpmEstimatorListener> listeners = new CopyOnWriteArrayList<OnBpmEstimatorListener>();
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;
}
}

View File

@@ -1,43 +0,0 @@
package de.tonifetzer.conductorswatch;
import android.content.Context;
import android.os.Vibrator;
import java.util.List;
import java.util.TimerTask;
import java.util.concurrent.CopyOnWriteArrayList;
/**
* Created by toni on 13/11/17.
*/
public class Metronome extends TimerTask {
private Vibrator mVibrator;
Metronome(Context context){
mVibrator = (Vibrator) context.getSystemService(Context.VIBRATOR_SERVICE);
}
@Override
public void run() {
mVibrator.vibrate(10);
for (OnMetronomeListener listener:listeners) {
listener.onNewClick();
}
}
/**
* Interface for callback metronome clicks
* I know that java has observable.. but this why it is more clean and easy
*/
public interface OnMetronomeListener {
void onNewClick();
}
private List<OnMetronomeListener> listeners = new CopyOnWriteArrayList<OnMetronomeListener>();
public void add(OnMetronomeListener listener){listeners.add(listener);}
public void remove(OnMetronomeListener listener){listeners.remove(listener);}
}

View File

@@ -13,7 +13,9 @@ import android.widget.TextView;
import java.util.Timer;
import java.util.Vector;
import de.tonifetzer.conductorswatch.bpmEstimation.Estimator;
import de.tonifetzer.conductorswatch.ui.Croller;
import de.tonifetzer.conductorswatch.ui.Metronome;
/**

View File

@@ -75,7 +75,7 @@ public class AccelerometerWindowBuffer extends ArrayList<AccelerometerData> {
public AccelerometerWindowBuffer getFixedSizedWindow(int size, int overlap){
AccelerometerWindowBuffer other = new AccelerometerWindowBuffer(size, overlap);
double sampleRate = ((getYongest().ts - getOldest().ts) / super.size());
double sampleRate = (double) ((getYongest().ts - getOldest().ts) / super.size());
//if current size is smaller then wanted size, start at 0 and provide smaller list
int start = 0;

View File

@@ -1,214 +0,0 @@
package de.tonifetzer.conductorswatch.bpmEstimation;
import de.tonifetzer.conductorswatch.utilities.MovingFilter;
import de.tonifetzer.conductorswatch.utilities.SimpleKalman;
import de.tonifetzer.conductorswatch.utilities.Utils;
import java.util.ArrayList;
import java.util.LinkedList;
import java.util.List;
/**
* Created by toni on 17/12/17.
*/
public class BpmEstimator {
private AccelerometerWindowBuffer mBuffer;
private double mSampleRate_ms;
private LinkedList<Double> mBpmHistory_X;
private LinkedList<Double> mBpmHistory_Y;
private LinkedList<Double> mBpmHistory_Z;
private LinkedList<Double> mBpmHistory;
private int mResetCounter;
private int mResetLimit_ms;
private MovingFilter mMvg;
//private SimpleKalman mKalman;
public BpmEstimator(AccelerometerWindowBuffer windowBuffer, double sampleRate_ms, int resetAfter_ms){
mBuffer = windowBuffer;
mSampleRate_ms = sampleRate_ms;
mBpmHistory_X = new LinkedList<>();
mBpmHistory_Y = new LinkedList<>();
mBpmHistory_Z = new LinkedList<>();
mBpmHistory = new LinkedList<>();
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();
}
//TODO: we use the buffer from outside.. this buffer is continuously updated.. not good!
public double estimate(AccelerometerWindowBuffer fixedWindow){
double sampleRate = mSampleRate_ms;
if(sampleRate <= 0){
sampleRate = Math.round(Utils.mean(Utils.diff(fixedWindow.getTs())));
}
if(sampleRate == 0){
int breakhere = 0;
}
AccelerometerInterpolator interp = new AccelerometerInterpolator(fixedWindow, sampleRate);
//are we conducting?
//just look at the newest 512 samples
//List<AccelerometerData> subBuffer = mBuffer.subList(mBuffer.size() - 512, mBuffer.size());
double[] xAutoCorr = new AutoCorrelation(interp.getX(), fixedWindow.size()).getCorr();
double[] yAutoCorr = new AutoCorrelation(interp.getY(), fixedWindow.size()).getCorr();
double[] zAutoCorr = new AutoCorrelation(interp.getZ(), fixedWindow.size()).getCorr();
//find a peak within range of 250 ms
int peakWidth = (int) Math.round(250 / sampleRate);
Peaks pX = new Peaks(xAutoCorr, peakWidth, 0.1f, 0, false);
Peaks pY = new Peaks(yAutoCorr, peakWidth, 0.1f, 0, false);
Peaks pZ = new Peaks(zAutoCorr, peakWidth, 0.1f, 0, false);
mBpmHistory_X.add(pX.getBPM(sampleRate));
mBpmHistory_Y.add(pY.getBPM(sampleRate));
mBpmHistory_Z.add(pZ.getBPM(sampleRate));
double estimatedBPM = getBestBpmEstimation(pX, pY, pZ);
if(estimatedBPM != -1){
//moving avg (lohnt dann, wenn wir viele daten haben)
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());
//kalman filter (lohnt dann, wenn wir konstantes tempo haben, mit startangabe!)
//standard last element
//mBpmHistory.add(estimatedBPM);
//mResetCounter = 0;
}
else {
int resetAfter = (int) Math.round(mResetLimit_ms / (mBuffer.getOverlapSize()));
if(++mResetCounter > resetAfter){
mBpmHistory.clear();
//TODO: send signal to clear.
//mBuffer.clear();
mMvg.clear();
mResetCounter = 0;
}
return -1;
}
//last element
return mBpmHistory.getLast();
}
public double getMeanBpm(){
return Utils.mean(mBpmHistory);
}
public double getMedianBPM(){
return Utils.median(mBpmHistory);
}
private double getBestBpmEstimation(Peaks peaksX, Peaks peaksY, Peaks peaksZ) throws IllegalArgumentException {
int cntNumAxis = 0;
double sumCorr = 0; //to prevent division by zero
double sumRms = 0;
double sumNumInter = 0;
double corrMeanX = 0, corrRmsX = 0;
int corrNumInterX = 0;
if(peaksX.hasPeaks()){
corrMeanX = Utils.geometricMean(peaksX.getPeaksValueWithoutZeroIdxAndNegativeValues());
corrRmsX = Utils.rms(peaksX.getPeaksValueWithoutZeroIdx());
corrNumInterX = Utils.intersectionNumber(peaksX.getData(), 0.2f);
++cntNumAxis;
sumCorr += corrMeanX;
sumRms += corrRmsX;
sumNumInter += corrNumInterX;
}
double corrMeanY = 0, corrRmsY = 0;
int corrNumInterY = 0;
if(peaksY.hasPeaks()){
corrMeanY = Utils.geometricMean(peaksY.getPeaksValueWithoutZeroIdxAndNegativeValues());
corrRmsY = Utils.rms(peaksY.getPeaksValueWithoutZeroIdx());
corrNumInterY = Utils.intersectionNumber(peaksY.getData(), 0.2f);
++cntNumAxis;
sumCorr += corrMeanY;
sumRms += corrRmsY;
sumNumInter += corrNumInterY;
}
double corrMeanZ = 0, corrRmsZ = 0;
int corrNumInterZ = 0;
if(peaksZ.hasPeaks()){
corrMeanZ = Utils.geometricMean(peaksZ.getPeaksValueWithoutZeroIdxAndNegativeValues());
corrRmsZ = Utils.rms(peaksZ.getPeaksValueWithoutZeroIdx());
corrNumInterZ = Utils.intersectionNumber(peaksZ.getData(), 0.2f);
++cntNumAxis;
sumCorr += corrMeanZ;
sumRms += corrRmsZ;
sumNumInter += corrNumInterZ;
}
//no peaks, reject
if(cntNumAxis == 0){
//throw new IllegalArgumentException("All Peaks are empty! -> Reject Estimation");
return -1;
}
/*
System.out.println("RMS-X: " + corrRmsX);
System.out.println("GEO-X: " + corrMeanX);
System.out.println("INTER-X: " + corrNumInterX);
System.out.println("RMS-Y: " + corrRmsY);
System.out.println("GEO-Y: " + corrMeanY);
System.out.println("INTER-Y: " + corrNumInterY);
System.out.println("RMS-Z: " + corrRmsZ);
System.out.println("GEO-Z: " + corrMeanZ);
System.out.println("INTER-Z: " + corrNumInterZ);
*/
//values to low, reject
//TODO: this is a pretty simple assumption. first shot!
if(corrRmsX < 0.25 && corrRmsY < 0.25 && corrRmsZ < 0.25){
return -1;
}
double quantityX = ((corrMeanX / sumCorr) + (corrRmsX / sumRms) + (corrNumInterX / sumNumInter)) / cntNumAxis;
double quantityY = ((corrMeanY / sumCorr) + (corrRmsY / sumRms) + (corrNumInterY / sumNumInter)) / cntNumAxis;
double quantityZ = ((corrMeanZ / sumCorr) + (corrRmsZ / sumRms) + (corrNumInterZ / sumNumInter)) / cntNumAxis;
//get best axis by quantity and estimate bpm
if(quantityX > quantityY && quantityX > quantityZ){
return mBpmHistory_X.getLast();
}
else if(quantityY > quantityZ){
return mBpmHistory_Y.getLast();
}
else {
return mBpmHistory_Z.getLast();
}
}
}