mirror of
https://github.com/clangen/musikcube.git
synced 2025-01-17 07:10:44 +00:00
Started on a PaceDetector.
This commit is contained in:
parent
ce775c6e8d
commit
16dd505de1
@ -3,7 +3,7 @@
|
||||
package="org.musikcube"
|
||||
android:versionCode="1"
|
||||
android:versionName="1.0">
|
||||
<application android:icon="@drawable/icon" android:label="@string/app_name" android:name="App">
|
||||
<application android:icon="@drawable/icon" android:label="@string/app_name" android:name="App" android:debuggable="true">
|
||||
<activity android:name=".main"
|
||||
android:label="@string/app_name" android:launchMode="singleTask">
|
||||
<intent-filter>
|
||||
@ -25,4 +25,5 @@
|
||||
|
||||
|
||||
|
||||
|
||||
</manifest>
|
@ -13,7 +13,7 @@
|
||||
<TextView android:id="@+id/TextView01" android:layout_height="wrap_content" android:text="Browse by:" android:layout_width="fill_parent" android:background="@color/headlineColor" android:textSize="16sp" android:padding="4sp"></TextView>
|
||||
<Button android:layout_height="wrap_content" android:layout_width="fill_parent" android:textSize="22sp" android:text="Genre" android:id="@+id/GenresButton"></Button>
|
||||
<Button android:layout_height="wrap_content" android:layout_width="fill_parent" android:textSize="22sp" android:id="@+id/ArtistsButton" android:text="Artist"></Button>
|
||||
|
||||
<Button android:layout_height="wrap_content" android:layout_width="fill_parent" android:textSize="22sp" android:id="@+id/BPMButton" android:text="Workout mode"></Button>
|
||||
</LinearLayout>
|
||||
|
||||
<TextView android:id="@+id/StatusView" android:text="Status: -" android:layout_width="fill_parent" android:background="@color/headlineColor" android:textSize="16sp" android:padding="4sp" android:layout_height="wrap_content" android:layout_gravity="bottom" android:layout_weight="0.1"></TextView>
|
||||
|
249
src/android/src/org/musikcube/BPMControl.java
Normal file
249
src/android/src/org/musikcube/BPMControl.java
Normal file
@ -0,0 +1,249 @@
|
||||
package org.musikcube;
|
||||
|
||||
import java.io.InputStream;
|
||||
import java.net.HttpURLConnection;
|
||||
import java.net.URL;
|
||||
import org.musikcube.core.Library;
|
||||
import org.musikcube.core.Player;
|
||||
import org.musikcube.core.Track;
|
||||
import org.musikcube.core.Player.OnTrackUpdateListener;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.content.Intent;
|
||||
import android.graphics.Bitmap;
|
||||
import android.graphics.BitmapFactory;
|
||||
import android.os.AsyncTask;
|
||||
import android.os.Bundle;
|
||||
import android.os.Handler;
|
||||
import android.util.Log;
|
||||
import android.view.Menu;
|
||||
import android.view.MenuInflater;
|
||||
import android.view.MenuItem;
|
||||
import android.widget.ImageView;
|
||||
import android.widget.SeekBar;
|
||||
import android.widget.TextView;
|
||||
|
||||
public class BPMControl extends Activity implements OnTrackUpdateListener {
|
||||
|
||||
private Track track = new Track();
|
||||
private int duration = 0;
|
||||
private Object lock = new Object();
|
||||
|
||||
@Override
|
||||
public void onCreate(Bundle savedInstanceState) {
|
||||
Log.v("MC2::PC","OnCreate");
|
||||
super.onCreate(savedInstanceState);
|
||||
setContentView(R.layout.play_control);
|
||||
/*
|
||||
ImageButton nextButton = (ImageButton)findViewById(R.id.MediaNext);
|
||||
nextButton.setOnClickListener(this.onNextClick);
|
||||
ImageButton pauseButton = (ImageButton)findViewById(R.id.MediaPause);
|
||||
pauseButton.setOnClickListener(this.onPauseClick);
|
||||
*/
|
||||
this.callbackTrackPositionsUpdateHandler.postDelayed(callbackTrackPositionsUpdateRunnable,500);
|
||||
}
|
||||
/*
|
||||
private OnClickListener onNextClick = new OnClickListener() {
|
||||
public void onClick(View v){
|
||||
Intent intent = new Intent(PlayerControl.this, org.musikcube.Service.class);
|
||||
intent.putExtra("org.musikcube.Service.action", "next");
|
||||
startService(intent);
|
||||
}
|
||||
};
|
||||
private OnClickListener onPauseClick = new OnClickListener() {
|
||||
public void onClick(View v){
|
||||
Intent intent = new Intent(PlayerControl.this, org.musikcube.Service.class);
|
||||
intent.putExtra("org.musikcube.Service.action", "stop");
|
||||
startService(intent);
|
||||
}
|
||||
};
|
||||
*/
|
||||
public void OnTrackBufferUpdate(int percent) {
|
||||
synchronized(lock){
|
||||
}
|
||||
this.callbackTrackPositionsUpdateHandler.post(this.callbackTrackPositionsUpdateRunnable);
|
||||
}
|
||||
public void OnTrackPositionUpdate(int secondsPlayed) {
|
||||
synchronized(lock){
|
||||
}
|
||||
this.callbackTrackPositionsUpdateHandler.post(this.callbackTrackPositionsUpdateRunnable);
|
||||
}
|
||||
public void OnTrackUpdate() {
|
||||
this.callbackTrackUpdateHandler.post(this.callbackTrackUpdateRunnable);
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
protected void onPause() {
|
||||
Log.v("MC2::PC","OnPause");
|
||||
Player.GetInstance().SetUpdateListener(null);
|
||||
super.onPause();
|
||||
}
|
||||
@Override
|
||||
protected void onResume() {
|
||||
Log.v("MC2::PC","OnResume");
|
||||
Player.GetInstance().SetUpdateListener(this);
|
||||
super.onResume();
|
||||
}
|
||||
|
||||
// Need handler for callbacks to the UI thread
|
||||
final Handler callbackTrackUpdateHandler = new Handler();
|
||||
// Create runnable for posting
|
||||
final Runnable callbackTrackUpdateRunnable = new Runnable() {
|
||||
public void run() {
|
||||
OnUpdateTrackUI();
|
||||
}
|
||||
};
|
||||
|
||||
public void OnUpdateTrackUI() {
|
||||
TextView titleView = (TextView)findViewById(R.id.TrackTitle);
|
||||
TextView albumView = (TextView)findViewById(R.id.TrackAlbum);
|
||||
TextView artistView = (TextView)findViewById(R.id.TrackArtist);
|
||||
TextView durationView = (TextView)findViewById(R.id.TrackDuration);
|
||||
|
||||
int thumbnailId = 0;
|
||||
|
||||
synchronized(lock){
|
||||
|
||||
this.track = Player.GetInstance().GetCurrentTrack();
|
||||
if(this.track==null){
|
||||
this.track = new Track();
|
||||
}
|
||||
|
||||
String thumbnailString = this.track.metadata.get("thumbnail_id");
|
||||
if(thumbnailString!=null){
|
||||
thumbnailId = Integer.parseInt(thumbnailString);
|
||||
}
|
||||
|
||||
String title = this.track.metadata.get("title");
|
||||
if(title==null){
|
||||
titleView.setText("Title:");
|
||||
}else{
|
||||
titleView.setText("Title: "+title);
|
||||
}
|
||||
String album = this.track.metadata.get("album");
|
||||
if(album==null){
|
||||
albumView.setText("Album:");
|
||||
}else{
|
||||
albumView.setText("Album: "+album);
|
||||
}
|
||||
String artist = this.track.metadata.get("visual_artist");
|
||||
if(artist==null){
|
||||
artistView.setText("Artist:");
|
||||
}else{
|
||||
artistView.setText("Artist: "+artist);
|
||||
}
|
||||
|
||||
String duration = this.track.metadata.get("duration");
|
||||
if(duration==null){
|
||||
this.duration = 0;
|
||||
}else{
|
||||
this.duration = Integer.parseInt(duration);
|
||||
}
|
||||
int minutes = (int)Math.floor(this.duration/60);
|
||||
int seconds = this.duration-minutes*60;
|
||||
String durationText = Integer.toString(minutes)+":";
|
||||
if(seconds<10){ durationText += "0"; }
|
||||
durationText += Integer.toString(seconds);
|
||||
durationView.setText(durationText);
|
||||
}
|
||||
|
||||
// clear image
|
||||
ImageView cover = (ImageView)findViewById(R.id.AlbumCover);
|
||||
cover.setImageResource(R.drawable.album);
|
||||
|
||||
if(thumbnailId!=0){
|
||||
// Load image
|
||||
Library library = Library.GetInstance();
|
||||
new DownloadAlbumCoverTask().execute("http://"+library.host+":"+library.httpPort+"/cover/?cover_id="+thumbnailId);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private class DownloadAlbumCoverTask extends AsyncTask<String,Integer,Bitmap>{
|
||||
|
||||
protected Bitmap doInBackground(String... params) {
|
||||
try {
|
||||
URL url = new URL(params[0]);
|
||||
HttpURLConnection conn= (HttpURLConnection)url.openConnection();
|
||||
conn.setDoInput(true);
|
||||
conn.connect();
|
||||
//int length = conn.getContentLength();
|
||||
InputStream is = conn.getInputStream();
|
||||
Bitmap bm = BitmapFactory.decodeStream(is);
|
||||
return bm;
|
||||
} catch (Exception e) {
|
||||
Log.v("mC2:PLAYER","Error "+e.getMessage());
|
||||
// e.printStackTrace();
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
protected void onPostExecute(Bitmap result){
|
||||
if(result==null){
|
||||
}else{
|
||||
ImageView cover = (ImageView)findViewById(R.id.AlbumCover);
|
||||
cover.setImageBitmap(result);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Need handler for callbacks to the UI thread
|
||||
final Handler callbackTrackPositionsUpdateHandler = new Handler();
|
||||
// Create runnable for posting
|
||||
final Runnable callbackTrackPositionsUpdateRunnable = new Runnable() {
|
||||
public void run() {
|
||||
OnUpdateTrackPositionsUI();
|
||||
}
|
||||
};
|
||||
|
||||
public void OnUpdateTrackPositionsUI() {
|
||||
int msPosition = Player.GetInstance().GetTrackPosition();
|
||||
int position = msPosition/1000;
|
||||
int minutes = (int)Math.floor(position/60);
|
||||
int seconds = position-minutes*60;
|
||||
String positionText = Integer.toString(minutes)+":";
|
||||
if(seconds<10){ positionText += "0"; }
|
||||
positionText += Integer.toString(seconds);
|
||||
TextView positionView = (TextView)findViewById(R.id.TrackPosition);
|
||||
positionView.setText(positionText);
|
||||
|
||||
SeekBar seekBar = (SeekBar)findViewById(R.id.TrackProgress);
|
||||
synchronized (this.lock) {
|
||||
if(this.duration==0){
|
||||
seekBar.setProgress(0);
|
||||
}else{
|
||||
seekBar.setProgress(msPosition/this.duration);
|
||||
}
|
||||
seekBar.setSecondaryProgress(10*Player.GetInstance().GetTrackBuffer());
|
||||
}
|
||||
|
||||
// Next callback in 0.5 seconds
|
||||
this.callbackTrackPositionsUpdateHandler.postDelayed(callbackTrackPositionsUpdateRunnable,500);
|
||||
|
||||
}
|
||||
|
||||
public boolean onCreateOptionsMenu(Menu menu) {
|
||||
MenuInflater inflater = getMenuInflater();
|
||||
inflater.inflate(R.menu.default_menu, menu);
|
||||
return true;
|
||||
}
|
||||
|
||||
public boolean onOptionsItemSelected(MenuItem item) {
|
||||
//Log.i("MC2.onContextItemSelected","item "+item.getItemId()+" "+R.id.context_settings);
|
||||
switch (item.getItemId()) {
|
||||
case R.id.context_settings:
|
||||
startActivity(new Intent(this, org.musikcube.Preferences.class));
|
||||
return true;
|
||||
case R.id.context_browse:
|
||||
startActivity(new Intent(this, org.musikcube.main.class));
|
||||
return true;
|
||||
case R.id.context_controls:
|
||||
startActivity(new Intent(this, org.musikcube.PlayerControl.class));
|
||||
return true;
|
||||
default:
|
||||
return super.onContextItemSelected(item);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@ -4,6 +4,7 @@
|
||||
package org.musikcube;
|
||||
|
||||
import org.musikcube.core.Library;
|
||||
import org.musikcube.core.PaceDetector;
|
||||
import org.musikcube.core.Player;
|
||||
import org.musikcube.core.Track;
|
||||
|
||||
@ -23,6 +24,7 @@ public class Service extends android.app.Service {
|
||||
Library library;
|
||||
Player player;
|
||||
boolean showingNotification = false;
|
||||
PaceDetector paceDetector;
|
||||
|
||||
/**
|
||||
*
|
||||
@ -82,6 +84,13 @@ public class Service extends android.app.Service {
|
||||
//Log.i("musikcube::Service","Shutdown");
|
||||
this.stopSelf();
|
||||
}
|
||||
if(action.equals("bpmstart")){
|
||||
if(this.paceDetector==null){
|
||||
this.paceDetector = new PaceDetector();
|
||||
this.paceDetector.StartSensor(this);
|
||||
}
|
||||
}
|
||||
|
||||
if(action.equals("player_start")){
|
||||
Track track = Player.GetInstance().GetCurrentTrack();
|
||||
|
||||
|
169
src/android/src/org/musikcube/core/PaceDetector.java
Normal file
169
src/android/src/org/musikcube/core/PaceDetector.java
Normal file
@ -0,0 +1,169 @@
|
||||
package org.musikcube.core;
|
||||
|
||||
import android.content.Context;
|
||||
import android.hardware.Sensor;
|
||||
import android.hardware.SensorEvent;
|
||||
import android.hardware.SensorEventListener;
|
||||
import android.hardware.SensorManager;
|
||||
import android.util.Log;
|
||||
|
||||
|
||||
public class PaceDetector implements Runnable, SensorEventListener {
|
||||
|
||||
static public float MAX_BPM = 165;
|
||||
static public float MIN_BPM = 80;
|
||||
static public int WAVE_MEMORY = 12;
|
||||
static public int WAVE_MIN_CALC = 6;
|
||||
static public float WAVE_MIN_BPM_DIFF = 80; // This is in miliseconds
|
||||
static public int WAVE_COMPARE = 3;
|
||||
|
||||
private float currentBPM = 0;
|
||||
private float currentAccuracy = 0;
|
||||
|
||||
private class PaceDimension{
|
||||
public java.util.ArrayList<Long> beatTimes = new java.util.ArrayList<Long>();
|
||||
public java.util.ArrayList<Float> amplitude = new java.util.ArrayList<Float>();
|
||||
|
||||
private float lastValue = 0;
|
||||
private float lastDiff = 0;
|
||||
private float currentMax = 0;
|
||||
private float currentMin = 0;
|
||||
|
||||
public float currentBPM = 0;
|
||||
public float currentAccuracy = 0;
|
||||
|
||||
final public void NextValue(float value){
|
||||
float diff = value-this.lastValue;
|
||||
|
||||
if(value<this.currentMin){
|
||||
this.currentMin = value;
|
||||
}
|
||||
if(value>this.currentMax){
|
||||
this.currentMax = value;
|
||||
}
|
||||
|
||||
if(this.lastDiff>=0 && diff<0){
|
||||
// this is a top on the curve
|
||||
this.beatTimes.add(android.os.SystemClock.elapsedRealtime());
|
||||
this.amplitude.add(this.currentMax-this.currentMin);
|
||||
|
||||
// Reset the amplitude
|
||||
this.currentMin = value;
|
||||
this.currentMax = value;
|
||||
|
||||
// only keep the last 10 waves
|
||||
while(this.beatTimes.size()>WAVE_MEMORY){
|
||||
this.beatTimes.remove(0);
|
||||
this.amplitude.remove(0);
|
||||
}
|
||||
|
||||
// Lets calculate BPM
|
||||
long bpmSum = 0;
|
||||
java.util.TreeSet<Long> bpms = new java.util.TreeSet<Long>();
|
||||
for(int i=0;i<this.beatTimes.size()-WAVE_COMPARE;i++){
|
||||
long orgSample = this.beatTimes.get(i);
|
||||
for(int j=i+1;j<i+WAVE_COMPARE;j++){
|
||||
//float bpmSample = 60000/(this.beatTimes.get(j)-orgSample);
|
||||
long bpmSample = this.beatTimes.get(j)-orgSample;
|
||||
if(bpmSample>(60000/MAX_BPM) && bpmSample<(60000/MIN_BPM)){
|
||||
bpms.add(bpmSample);
|
||||
bpmSum += bpmSample;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Lets remove the most "off" samples and correct the AVG until we are down to the desired "diff"
|
||||
boolean qualified = false;
|
||||
long bpmDiff = 0;
|
||||
|
||||
while(!qualified && bpms.size()>=WAVE_MIN_CALC){
|
||||
Long first = bpms.first();
|
||||
Long last = bpms.last();
|
||||
bpmDiff = last-first;
|
||||
int bpmSize = bpms.size();
|
||||
|
||||
// Log.v("MC2::DIFF","diff "+bpmSize+" "+first+"-"+last+" diff="+bpmDiff);
|
||||
|
||||
if(bpmDiff<WAVE_MIN_BPM_DIFF){
|
||||
qualified = true;
|
||||
}else{
|
||||
// Remove the element that is most far away from the average
|
||||
long avg = bpmSum/bpmSize;
|
||||
if(avg-first>last-avg){
|
||||
// Remove first
|
||||
bpmSum -= first;
|
||||
bpms.remove(first);
|
||||
}else{
|
||||
// Remove last
|
||||
bpmSum -= last;
|
||||
bpms.remove(last);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if(qualified){
|
||||
// Get avg amplitude
|
||||
float amplitude = this.amplitude.get(0);
|
||||
for(int i=1;i<this.amplitude.size();i++){
|
||||
amplitude+=this.amplitude.get(i);
|
||||
}
|
||||
amplitude /= this.amplitude.size();
|
||||
|
||||
|
||||
this.currentBPM = (60000*bpms.size())/bpmSum;
|
||||
this.currentAccuracy = (100-bpmDiff)+bpms.size()*13+amplitude*5;
|
||||
PaceDetector.this.ChangeBPM(this.currentBPM,this.currentAccuracy);
|
||||
}
|
||||
}
|
||||
this.lastDiff = diff;
|
||||
this.lastValue = value;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private PaceDimension xAxis = new PaceDimension();
|
||||
private PaceDimension yAxis = new PaceDimension();
|
||||
private PaceDimension zAxis = new PaceDimension();
|
||||
|
||||
public void onAccuracyChanged(Sensor sensor, int accuracy) {
|
||||
|
||||
}
|
||||
|
||||
public void onSensorChanged(SensorEvent event) {
|
||||
float values[] = event.values;
|
||||
this.xAxis.NextValue(values[0]);
|
||||
this.yAxis.NextValue(values[1]);
|
||||
this.zAxis.NextValue(values[2]);
|
||||
// Log.v("MC2::TJOOOO",""+values[0]);
|
||||
|
||||
// Log.v("mC2::ACC","x="+values[0]+" y="+values[1]+" z="+values[2]);
|
||||
}
|
||||
|
||||
public void ChangeBPM(float bpm,float accuracy){
|
||||
if(accuracy>=this.xAxis.currentAccuracy || accuracy>=this.yAxis.currentAccuracy || accuracy>=this.zAxis.currentAccuracy){
|
||||
this.currentBPM = bpm;
|
||||
this.currentAccuracy = accuracy;
|
||||
Log.v("MC2::BPM","bpm="+bpm+" "+accuracy);
|
||||
}
|
||||
}
|
||||
|
||||
public void StartSensor(Context context){
|
||||
Log.v("mC2::ACC","Sensor");
|
||||
SensorManager sensorMgr = (SensorManager)context.getSystemService(Context.SENSOR_SERVICE);
|
||||
Sensor accelerometer = sensorMgr.getDefaultSensor(Sensor.TYPE_ACCELEROMETER);
|
||||
sensorMgr.registerListener(this,accelerometer,SensorManager.SENSOR_DELAY_GAME);
|
||||
}
|
||||
|
||||
public void StopSensor(Context context){
|
||||
SensorManager sensorMgr = (SensorManager)context.getSystemService(Context.SENSOR_SERVICE);
|
||||
sensorMgr.unregisterListener(this);
|
||||
}
|
||||
|
||||
|
||||
|
||||
public void run() {
|
||||
// TODO Auto-generated method stub
|
||||
|
||||
}
|
||||
|
||||
}
|
@ -27,36 +27,43 @@ public class main extends Activity implements OnLibraryStatusListener {
|
||||
|
||||
Button genreButton = (Button)findViewById(R.id.GenresButton);
|
||||
genreButton.setOnClickListener(this.onGenreClick);
|
||||
|
||||
genreButton.setEnabled(false);
|
||||
|
||||
Button artistsButton = (Button)findViewById(R.id.ArtistsButton);
|
||||
artistsButton.setOnClickListener(this.onArtistsClick);
|
||||
|
||||
artistsButton.setEnabled(false);
|
||||
|
||||
Button bpmButton = (Button)findViewById(R.id.BPMButton);
|
||||
bpmButton.setOnClickListener(this.onBPMClick);
|
||||
bpmButton.setEnabled(false);
|
||||
|
||||
}
|
||||
|
||||
private OnClickListener onGenreClick = new OnClickListener() {
|
||||
|
||||
public void onClick(View v){
|
||||
Intent intent = new Intent(main.this, CategoryList.class);
|
||||
intent.putExtra("org.musikcube.CategoryList.listCategory", "genre,artist,album");
|
||||
startActivity(intent);
|
||||
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
private OnClickListener onArtistsClick = new OnClickListener() {
|
||||
|
||||
public void onClick(View v){
|
||||
Intent intent = new Intent(main.this, CategoryList.class);
|
||||
intent.putExtra("org.musikcube.CategoryList.listCategory", "artist,album");
|
||||
startActivity(intent);
|
||||
|
||||
}
|
||||
};
|
||||
|
||||
private OnClickListener onBPMClick = new OnClickListener() {
|
||||
public void onClick(View v){
|
||||
Intent intent = new Intent(main.this, org.musikcube.Service.class);
|
||||
intent.putExtra("org.musikcube.Service.action", "bpmstart");
|
||||
startService(intent);
|
||||
|
||||
Intent intent2 = new Intent(main.this, PlayerControl.class);
|
||||
startActivity(intent2);
|
||||
}
|
||||
};
|
||||
|
||||
public boolean onCreateOptionsMenu(Menu menu) {
|
||||
@ -114,13 +121,16 @@ public class main extends Activity implements OnLibraryStatusListener {
|
||||
int status = Library.GetInstance().GetStatus();
|
||||
Button genreButton = (Button)findViewById(R.id.GenresButton);
|
||||
Button artistsButton = (Button)findViewById(R.id.ArtistsButton);
|
||||
Button bpmButton = (Button)findViewById(R.id.BPMButton);
|
||||
|
||||
if(status==Library.STATUS_CONNECTED){
|
||||
genreButton.setEnabled(true);
|
||||
artistsButton.setEnabled(true);
|
||||
bpmButton.setEnabled(true);
|
||||
}else{
|
||||
genreButton.setEnabled(false);
|
||||
artistsButton.setEnabled(false);
|
||||
bpmButton.setEnabled(false);
|
||||
}
|
||||
|
||||
TextView statusText = (TextView)findViewById(R.id.StatusView);
|
||||
|
Loading…
Reference in New Issue
Block a user