Started on a PaceDetector.

This commit is contained in:
Daniel Önnerby 2009-08-16 22:27:03 +00:00
parent ce775c6e8d
commit 16dd505de1
6 changed files with 447 additions and 9 deletions

View File

@ -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>

View File

@ -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>

View 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);
}
}
}

View File

@ -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();

View 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
}
}

View File

@ -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);