diff --git a/android/ConductorsPhone/app/src/main/java/de/tonifetzer/conductorswatch/Metronome.java b/android/ConductorsPhone/app/src/main/java/de/tonifetzer/conductorswatch/Metronome.java index 01b34de..f83404b 100644 --- a/android/ConductorsPhone/app/src/main/java/de/tonifetzer/conductorswatch/Metronome.java +++ b/android/ConductorsPhone/app/src/main/java/de/tonifetzer/conductorswatch/Metronome.java @@ -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 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 deleted file mode 100644 index 282394d..0000000 --- a/android/ConductorsWatch/app/src/main/java/de/tonifetzer/conductorswatch/Estimator.java +++ /dev/null @@ -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 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 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/Metronome.java b/android/ConductorsWatch/app/src/main/java/de/tonifetzer/conductorswatch/Metronome.java deleted file mode 100644 index 02a6999..0000000 --- a/android/ConductorsWatch/app/src/main/java/de/tonifetzer/conductorswatch/Metronome.java +++ /dev/null @@ -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 listeners = new CopyOnWriteArrayList(); - public void add(OnMetronomeListener listener){listeners.add(listener);} - public void remove(OnMetronomeListener listener){listeners.remove(listener);} -} 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 a5f068e..2f591a1 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 @@ -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; /** 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 cd8ea70..5974d84 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 @@ -75,7 +75,7 @@ public class AccelerometerWindowBuffer extends ArrayList { 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; 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 deleted file mode 100644 index 85fc1f0..0000000 --- a/android/ConductorsWatch/app/src/main/java/de/tonifetzer/conductorswatch/bpmEstimation/BpmEstimator.java +++ /dev/null @@ -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 mBpmHistory_X; - private LinkedList mBpmHistory_Y; - private LinkedList mBpmHistory_Z; - - private LinkedList 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 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(); - } - } -}