diff --git a/src/android/AndroidManifest.xml b/src/android/AndroidManifest.xml index 990c4b50c..ac4467e04 100644 --- a/src/android/AndroidManifest.xml +++ b/src/android/AndroidManifest.xml @@ -3,7 +3,7 @@ package="org.musikcube" android:versionCode="1" android:versionName="1.0"> - + @@ -25,4 +25,5 @@ + \ No newline at end of file diff --git a/src/android/res/layout/main.xml b/src/android/res/layout/main.xml index f22ed39fe..6c4855188 100644 --- a/src/android/res/layout/main.xml +++ b/src/android/res/layout/main.xml @@ -13,7 +13,7 @@ - + diff --git a/src/android/src/org/musikcube/BPMControl.java b/src/android/src/org/musikcube/BPMControl.java new file mode 100644 index 000000000..a021d2582 --- /dev/null +++ b/src/android/src/org/musikcube/BPMControl.java @@ -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{ + + 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); + } + } + +} diff --git a/src/android/src/org/musikcube/Service.java b/src/android/src/org/musikcube/Service.java index 3dcf53b66..ed965c14e 100644 --- a/src/android/src/org/musikcube/Service.java +++ b/src/android/src/org/musikcube/Service.java @@ -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(); diff --git a/src/android/src/org/musikcube/core/PaceDetector.java b/src/android/src/org/musikcube/core/PaceDetector.java new file mode 100644 index 000000000..208b9d7d8 --- /dev/null +++ b/src/android/src/org/musikcube/core/PaceDetector.java @@ -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 beatTimes = new java.util.ArrayList(); + public java.util.ArrayList amplitude = new java.util.ArrayList(); + + 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(valuethis.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 bpms = new java.util.TreeSet(); + for(int i=0;i(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(bpmDifflast-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