banshee r4963 - in trunk/banshee: . build libbanshee src/Backends/Banshee.GStreamer src/Backends/Banshee.GStreamer/Banshee.GStreamer src/Core/Banshee.Services src/Core/Banshee.Services/Banshee.MediaEngine src/Core/Banshee.Services/Banshee.ServiceStack src/Core/Banshee.ThickClient src/Core/Banshee.ThickClient/Banshee.Gui.TrackEditor src/Core/Banshee.ThickClient/Banshee.Gui.Widgets src/Extensions src/Extensions/Banshee.Bpm src/Extensions/Banshee.Bpm/Banshee.Bpm



Author: gburt
Date: Mon Jan 26 03:45:46 2009
New Revision: 4963
URL: http://svn.gnome.org/viewvc/banshee?rev=4963&view=rev

Log:
2009-01-25  Gabriel Burt  <gabriel burt gmail com>

	* libbanshee/Makefile.am:
	* libbanshee/banshee-bpmdetector.c:
	* build/build.environment.mk:
	* configure.ac:
	* src/Extensions/Banshee.Bpm/Banshee.Bpm.addin.xml:
	* src/Extensions/Banshee.Bpm/Banshee.Bpm.csproj:
	* src/Extensions/Banshee.Bpm/Banshee.Bpm/BpmDetectJob.cs:
	* src/Extensions/Banshee.Bpm/Banshee.Bpm/BpmEntry.cs:
	* src/Extensions/Banshee.Bpm/Banshee.Bpm/BpmService.cs:
	* src/Extensions/Banshee.Bpm/Banshee.Bpm/BpmTapAdapter.cs:
	* src/Extensions/Banshee.Bpm/Banshee.Bpm/BpmTrackEditorModifier.cs:
	* src/Extensions/Banshee.Bpm/Makefile.am:
	* src/Extensions/Makefile.am: New extension that adds a preference (disabled
	by default) to automatically detect the BPM for all songs, and modified
	the track editor dialog's BPM entry, adding a button to auto detect the
	BPM, a button to play the song being edited, and a button to tap out the
	BPM manually.  This feature uses the GStreamer bpmdetect element.

	* src/Backends/Banshee.GStreamer/Banshee.GStreamer.addin.xml:
	* src/Backends/Banshee.GStreamer/Banshee.GStreamer/BpmDetector.cs:
	* src/Backends/Banshee.GStreamer/Makefile.am:
	* src/Core/Banshee.Services/Makefile.am:
	* src/Core/Banshee.Services/Banshee.Services.addin.xml:
	* src/Core/Banshee.Services/Banshee.MediaEngine/IBpmDetector.cs: Support
	for various backends to provide bpm detection.

	* src/Core/Banshee.ThickClient/Banshee.Gui.Widgets/UserJobTileHost.cs:
	* src/Core/Banshee.Services/Banshee.ServiceStack/IUserJob.cs:
	* src/Core/Banshee.Services/Banshee.ServiceStack/UserJob.cs: Add
	IsBackground property, and if set, don't show such jobs as normal job
	tiles.

	* src/Core/Banshee.ThickClient/Banshee.Gui.TrackEditor/FieldPage.cs:
	* src/Core/Banshee.ThickClient/Banshee.Gui.TrackEditor/ITrackEditorModifier.cs:
	* src/Core/Banshee.ThickClient/Banshee.Gui.TrackEditor/TrackEditorDialog.cs:
	* src/Core/Banshee.ThickClient/Banshee.ThickClient.addin.xml:
	* src/Core/Banshee.ThickClient/Makefile.am: Make the track editor more
	extensible and modifiable, including the ability to remove fields.

Added:
   trunk/banshee/libbanshee/banshee-bpmdetector.c
   trunk/banshee/src/Backends/Banshee.GStreamer/Banshee.GStreamer/BpmDetector.cs
   trunk/banshee/src/Core/Banshee.Services/Banshee.MediaEngine/IBpmDetector.cs
      - copied, changed from r4962, /trunk/banshee/src/Core/Banshee.Services/Banshee.ServiceStack/IUserJob.cs
   trunk/banshee/src/Core/Banshee.ThickClient/Banshee.Gui.TrackEditor/ITrackEditorModifier.cs
      - copied, changed from r4962, /trunk/banshee/src/Core/Banshee.Services/Banshee.ServiceStack/IUserJob.cs
   trunk/banshee/src/Extensions/Banshee.Bpm/
   trunk/banshee/src/Extensions/Banshee.Bpm/Banshee.Bpm/
   trunk/banshee/src/Extensions/Banshee.Bpm/Banshee.Bpm.addin.xml
   trunk/banshee/src/Extensions/Banshee.Bpm/Banshee.Bpm.csproj
   trunk/banshee/src/Extensions/Banshee.Bpm/Banshee.Bpm/BpmDetectJob.cs
   trunk/banshee/src/Extensions/Banshee.Bpm/Banshee.Bpm/BpmEntry.cs
   trunk/banshee/src/Extensions/Banshee.Bpm/Banshee.Bpm/BpmService.cs
   trunk/banshee/src/Extensions/Banshee.Bpm/Banshee.Bpm/BpmTapAdapter.cs
   trunk/banshee/src/Extensions/Banshee.Bpm/Banshee.Bpm/BpmTrackEditorModifier.cs
   trunk/banshee/src/Extensions/Banshee.Bpm/Makefile.am
Modified:
   trunk/banshee/.gitignore
   trunk/banshee/ChangeLog
   trunk/banshee/build/build.environment.mk
   trunk/banshee/configure.ac
   trunk/banshee/libbanshee/Makefile.am
   trunk/banshee/src/Backends/Banshee.GStreamer/Banshee.GStreamer.addin.xml
   trunk/banshee/src/Backends/Banshee.GStreamer/Makefile.am
   trunk/banshee/src/Core/Banshee.Services/Banshee.ServiceStack/IUserJob.cs
   trunk/banshee/src/Core/Banshee.Services/Banshee.ServiceStack/UserJob.cs
   trunk/banshee/src/Core/Banshee.Services/Banshee.Services.addin.xml
   trunk/banshee/src/Core/Banshee.Services/Makefile.am
   trunk/banshee/src/Core/Banshee.ThickClient/Banshee.Gui.TrackEditor/FieldPage.cs
   trunk/banshee/src/Core/Banshee.ThickClient/Banshee.Gui.TrackEditor/TrackEditorDialog.cs
   trunk/banshee/src/Core/Banshee.ThickClient/Banshee.Gui.Widgets/UserJobTileHost.cs
   trunk/banshee/src/Core/Banshee.ThickClient/Banshee.ThickClient.addin.xml
   trunk/banshee/src/Core/Banshee.ThickClient/Makefile.am
   trunk/banshee/src/Extensions/Makefile.am

Modified: trunk/banshee/.gitignore
==============================================================================
--- trunk/banshee/.gitignore	(original)
+++ trunk/banshee/.gitignore	Mon Jan 26 03:45:46 2009
@@ -30,7 +30,7 @@
 aclocal.m4
 autom4te.cache/
 bin/
-data/banshee-1.desktop
+data/desktop-files/*.desktop
 install-sh
 libtool
 ltmain.sh

Modified: trunk/banshee/build/build.environment.mk
==============================================================================
--- trunk/banshee/build/build.environment.mk	(original)
+++ trunk/banshee/build/build.environment.mk	Mon Jan 26 03:45:46 2009
@@ -116,6 +116,7 @@
 REF_EXTENSION_AUDIOCD = $(LINK_BANSHEE_THICKCLIENT_DEPS) $(LINK_MUSICBRAINZ_DEPS)
 REF_EXTENSION_BOOKMARKS = $(LINK_BANSHEE_THICKCLIENT_DEPS)
 REF_EXTENSION_BOOSCRIPT = $(LINK_BANSHEE_THICKCLIENT_DEPS) $(LINK_BOO)
+REF_EXTENSION_BPM = $(LINK_BANSHEE_THICKCLIENT_DEPS)
 REF_EXTENSION_COVERART = $(LINK_BANSHEE_THICKCLIENT_DEPS)
 REF_EXTENSION_DAAP = $(LINK_BANSHEE_THICKCLIENT_DEPS) $(LINK_ICSHARP_ZIP_LIB) $(LINK_MONO_ZEROCONF)
 REF_EXTENSION_FILESYSTEMQUEUE = $(LINK_BANSHEE_THICKCLIENT_DEPS)

Modified: trunk/banshee/configure.ac
==============================================================================
--- trunk/banshee/configure.ac	(original)
+++ trunk/banshee/configure.ac	Mon Jan 26 03:45:46 2009
@@ -231,6 +231,7 @@
 src/Extensions/Banshee.AudioCd/Makefile
 src/Extensions/Banshee.Bookmarks/Makefile
 src/Extensions/Banshee.BooScript/Makefile
+src/Extensions/Banshee.Bpm/Makefile
 src/Extensions/Banshee.CoverArt/Makefile
 src/Extensions/Banshee.Daap/Makefile
 src/Extensions/Banshee.FileSystemQueue/Makefile

Modified: trunk/banshee/libbanshee/Makefile.am
==============================================================================
--- trunk/banshee/libbanshee/Makefile.am	(original)
+++ trunk/banshee/libbanshee/Makefile.am	Mon Jan 26 03:45:46 2009
@@ -21,6 +21,7 @@
 	banshee-player-video.c \
 	banshee-player-vis.c \
 	banshee-ripper.c \
+	banshee-bpmdetector.c \
 	banshee-tagger.c \
 	banshee-transcoder.c
 

Added: trunk/banshee/libbanshee/banshee-bpmdetector.c
==============================================================================
--- (empty file)
+++ trunk/banshee/libbanshee/banshee-bpmdetector.c	Mon Jan 26 03:45:46 2009
@@ -0,0 +1,354 @@
+//
+// banshee-bpmdetector.c
+//
+// Author:
+//   Gabriel Burt <gburt novell com>
+//
+// Copyright (C) 2008 Novell, Inc.
+//
+// Permission is hereby granted, free of charge, to any person obtaining
+// a copy of this software and associated documentation files (the
+// "Software"), to deal in the Software without restriction, including
+// without limitation the rights to use, copy, modify, merge, publish,
+// distribute, sublicense, and/or sell copies of the Software, and to
+// permit persons to whom the Software is furnished to do so, subject to
+// the following conditions:
+//
+// The above copyright notice and this permission notice shall be
+// included in all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
+// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
+// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
+// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+//
+
+#ifdef HAVE_CONFIG_H
+#  include "config.h"
+#endif
+
+#include <string.h>
+#include <glib/gi18n.h>
+
+#include "banshee-gst.h"
+#include "banshee-tagger.h"
+
+typedef struct BansheeBpmDetector BansheeBpmDetector;
+
+typedef void (* BansheeBpmDetectorFinishedCallback) ();
+typedef void (* BansheeBpmDetectorProgressCallback) (double bpm);
+typedef void (* BansheeBpmDetectorErrorCallback)    (const gchar *error, const gchar *debug);
+
+// Only analyze 20 seconds of audio per song
+#define BPM_DETECT_ANALYSIS_DURATION_MS 20*1000
+
+struct BansheeBpmDetector {
+    gboolean is_detecting;
+
+    // You can run this pipeline on the cmd line with:
+    //   gst-launch -m filesrc location=/path/to/my.mp3 ! decodebin ! \
+    //     audioconvert ! bpmdetect ! fakesink
+    GstElement *pipeline;
+    GstElement *filesrc;
+    GstElement *decodebin;
+    GstElement *audioconvert;
+    GstElement *bpmdetect;
+    GstElement *fakesink;
+    
+    BansheeBpmDetectorProgressCallback progress_cb;
+    BansheeBpmDetectorFinishedCallback finished_cb;
+    BansheeBpmDetectorErrorCallback error_cb;
+};
+
+// ---------------------------------------------------------------------------
+// Private Functions
+// ---------------------------------------------------------------------------
+
+static void
+bbd_raise_error (BansheeBpmDetector *detector, const gchar *error, const gchar *debug)
+{
+    printf ("bpm_detect got error: %s %s\n", error, debug);
+    g_return_if_fail (detector != NULL);
+
+    if (detector->error_cb != NULL) {
+        detector->error_cb (error, debug);
+    }
+}
+
+static void
+bbd_pipeline_process_tag (const GstTagList *tag_list, const gchar *tag_name, BansheeBpmDetector *detector)
+{
+    const GValue *value;
+    gint value_count;
+    double bpm;
+    
+    g_return_if_fail (detector != NULL);
+
+    if (detector->progress_cb == NULL) {
+        return;
+    }
+
+    if (strcmp (tag_name, GST_TAG_BEATS_PER_MINUTE)) {
+        return;
+    }
+
+    value_count = gst_tag_list_get_tag_size (tag_list, tag_name);
+    if (value_count < 1) {
+        return;
+    }
+    
+    value = gst_tag_list_get_value_index (tag_list, tag_name, 0);
+    if (value != NULL && G_VALUE_HOLDS_DOUBLE (value)) {
+        bpm = g_value_get_double (value);
+        detector->progress_cb (bpm);
+    }
+}
+
+static gboolean
+bbd_pipeline_bus_callback (GstBus *bus, GstMessage *message, gpointer data)
+{
+    BansheeBpmDetector *detector = (BansheeBpmDetector *)data;
+
+    g_return_val_if_fail (detector != NULL, FALSE);
+
+    switch (GST_MESSAGE_TYPE (message)) {
+        case GST_MESSAGE_TAG: {
+            GstTagList *tags;
+            gst_message_parse_tag (message, &tags);
+            if (GST_IS_TAG_LIST (tags)) {
+                gst_tag_list_foreach (tags, (GstTagForeachFunc)bbd_pipeline_process_tag, detector);
+                gst_tag_list_free (tags);
+            }
+            break;
+        }
+
+        case GST_MESSAGE_ERROR: {
+            GError *error;
+            gchar *debug;
+            
+            gst_message_parse_error (message, &error, &debug);
+            bbd_raise_error (detector, error->message, debug);
+            g_error_free (error);
+            g_free (debug);
+            
+            detector->is_detecting = FALSE;
+            break;
+        }
+
+        case GST_MESSAGE_EOS: {
+            detector->is_detecting = FALSE;
+            gst_element_set_state (GST_ELEMENT (detector->pipeline), GST_STATE_NULL);
+
+            if (detector->finished_cb != NULL) {
+                detector->finished_cb ();
+            }
+            break;
+        }
+        
+        default: break;
+    }
+    
+    return TRUE;
+}
+
+static void
+bbd_new_decoded_pad(GstElement *decodebin, GstPad *pad, 
+    gboolean last, gpointer data)
+{
+    GstCaps *caps;
+    GstStructure *str;
+    GstPad *audiopad;
+    BansheeBpmDetector *detector = (BansheeBpmDetector *)data;
+
+    g_return_if_fail(detector != NULL);
+
+    audiopad = gst_element_get_pad(detector->audioconvert, "sink");
+    
+    if(GST_PAD_IS_LINKED(audiopad)) {
+        g_object_unref(audiopad);
+        return;
+    }
+
+    caps = gst_pad_get_caps(pad);
+    str = gst_caps_get_structure(caps, 0);
+    
+    if(!g_strrstr(gst_structure_get_name(str), "audio")) {
+        gst_caps_unref(caps);
+        gst_object_unref(audiopad);
+        return;
+    }
+   
+    gst_caps_unref(caps);
+    gst_pad_link(pad, audiopad);
+}
+
+static gboolean
+bbd_pipeline_construct (BansheeBpmDetector *detector)
+{
+    g_return_val_if_fail (detector != NULL, FALSE);
+
+    if (detector->pipeline != NULL) {
+        return TRUE;
+    }
+        
+    detector->pipeline = gst_pipeline_new ("pipeline");
+    if (detector->pipeline == NULL) {
+        bbd_raise_error (detector, _("Could not create pipeline"), NULL);
+        return FALSE;
+    }
+
+    detector->filesrc = gst_element_factory_make ("filesrc", "filesrc");
+    if (detector->filesrc == NULL) {
+        bbd_raise_error (detector, _("Could not create filesrc element"), NULL);
+        return FALSE;
+    }
+  
+    detector->decodebin = gst_element_factory_make ("decodebin", "decodebin");
+    if (detector->decodebin == NULL) {
+        bbd_raise_error (detector, _("Could not create decodebin plugin"), NULL);
+        return FALSE;
+    }
+
+    detector->audioconvert = gst_element_factory_make ("audioconvert", "audioconvert");
+    if (detector->audioconvert == NULL) {
+        bbd_raise_error (detector, _("Could not create audioconvert plugin"), NULL);
+        return FALSE;
+    }
+
+    detector->bpmdetect = gst_element_factory_make ("bpmdetect", "bpmdetect");
+    if (detector->bpmdetect == NULL) {
+        bbd_raise_error (detector, _("Could not create bpmdetect plugin"), NULL);
+        return FALSE;
+    }
+    
+    detector->fakesink = gst_element_factory_make ("fakesink", "bpmfakesink");
+    if (detector->fakesink == NULL) {
+        bbd_raise_error (detector, _("Could not create fakesink plugin"), NULL);
+        return FALSE;
+    }
+
+    gst_bin_add_many (GST_BIN (detector->pipeline),
+        detector->filesrc, detector->decodebin, detector->audioconvert,
+        detector->bpmdetect, detector->fakesink, NULL);
+
+    if (!gst_element_link (detector->filesrc, detector->decodebin)) {
+        bbd_raise_error (detector, _("Could not link pipeline elements"), NULL);
+        return FALSE;
+    }
+
+    // decodebin and audioconvert are linked dynamically when the decodebin creates a new pad
+    g_signal_connect(detector->decodebin, "new-decoded-pad", 
+        G_CALLBACK(bbd_new_decoded_pad), detector);
+
+    if (!gst_element_link_many (detector->audioconvert, detector->bpmdetect, detector->fakesink, NULL)) {
+        bbd_raise_error (detector, _("Could not link pipeline elements"), NULL);
+        return FALSE;
+    }
+        
+    gst_bus_add_watch (gst_pipeline_get_bus (GST_PIPELINE (detector->pipeline)), bbd_pipeline_bus_callback, detector);
+
+    return TRUE;
+}
+
+// ---------------------------------------------------------------------------
+// Internal Functions
+// ---------------------------------------------------------------------------
+
+BansheeBpmDetector *
+bbd_new ()
+{
+    return g_new0 (BansheeBpmDetector, 1);
+}
+
+void 
+bbd_cancel (BansheeBpmDetector *detector)
+{
+    g_return_if_fail (detector != NULL);
+    
+    if (detector->pipeline != NULL && GST_IS_ELEMENT (detector->pipeline)) {
+        gst_element_set_state (GST_ELEMENT (detector->pipeline), GST_STATE_NULL);
+        gst_object_unref (GST_OBJECT (detector->pipeline));
+        detector->pipeline = NULL;
+    }
+}
+
+void
+bbd_destroy (BansheeBpmDetector *detector)
+{
+    g_return_if_fail (detector != NULL);
+    
+    bbd_cancel (detector);
+    
+    g_free (detector);
+    detector = NULL;
+}
+
+gboolean
+bbd_process_file (BansheeBpmDetector *detector, const gchar *path)
+{
+    static GstFormat format = GST_FORMAT_TIME;
+    gint64 duration, duration_ms, start_ms, end_ms;
+
+    g_return_val_if_fail (detector != NULL, FALSE);
+
+    if (!bbd_pipeline_construct (detector)) {
+        return FALSE;
+    }
+    
+    detector->is_detecting = TRUE;
+    gst_element_set_state (detector->fakesink, GST_STATE_NULL);
+    g_object_set (G_OBJECT (detector->filesrc), "location", path, NULL);
+
+    // TODO listen for transition to STATE_PLAYING, then
+    // Determine how long the file is, and set the detector to base its analysis off the middle 30 seconds of the song
+
+    /*if (gst_element_query_duration (detector->fakesink, &format, &duration)) {
+        duration_ms = duration / GST_MSECOND;
+
+        start_ms = CLAMP((duration_ms / 2) - (BPM_DETECT_ANALYSIS_DURATION_MS/2), 0, duration_ms);
+        end_ms   = CLAMP(start_ms + BPM_DETECT_ANALYSIS_DURATION_MS, start_ms, duration_ms);
+        printf("Analyzing song %s starting at %d ending at %d\n", path, start_ms/1000, end_ms/1000);
+
+        if (gst_element_seek (detector->fakesink, 1.0, 
+            //GST_FORMAT_TIME, GST_SEEK_FLAG_FLUSH | GST_SEEK_FLAG_SKIP | GST_SEEK_FLAG_KEY_UNIT,
+            GST_FORMAT_TIME, GST_SEEK_FLAG_FLUSH | GST_SEEK_FLAG_KEY_UNIT,
+            GST_SEEK_TYPE_SET, start_ms * GST_MSECOND, 
+            GST_SEEK_TYPE_SET, end_ms * GST_MSECOND))
+        {
+        }
+    }*/
+
+    gst_element_set_state (detector->pipeline, GST_STATE_PLAYING);
+    return TRUE;
+}
+
+void
+bbd_set_progress_callback (BansheeBpmDetector *detector, BansheeBpmDetectorProgressCallback cb)
+{
+    g_return_if_fail (detector != NULL);
+    detector->progress_cb = cb;
+}
+
+void
+bbd_set_finished_callback (BansheeBpmDetector *detector, BansheeBpmDetectorFinishedCallback cb)
+{
+    g_return_if_fail (detector != NULL);
+    detector->finished_cb = cb;
+}
+
+void
+bbd_set_error_callback (BansheeBpmDetector *detector, BansheeBpmDetectorErrorCallback cb)
+{
+    g_return_if_fail (detector != NULL);
+    detector->error_cb = cb;
+}
+
+gboolean
+bbd_get_is_detecting (BansheeBpmDetector *detector)
+{
+    g_return_val_if_fail (detector != NULL, FALSE);
+    return detector->is_detecting;
+}

Modified: trunk/banshee/src/Backends/Banshee.GStreamer/Banshee.GStreamer.addin.xml
==============================================================================
--- trunk/banshee/src/Backends/Banshee.GStreamer/Banshee.GStreamer.addin.xml	(original)
+++ trunk/banshee/src/Backends/Banshee.GStreamer/Banshee.GStreamer.addin.xml	Mon Jan 26 03:45:46 2009
@@ -28,4 +28,8 @@
     <Transcoder class="Banshee.GStreamer.Transcoder"/>
   </Extension>
 
+  <Extension path="/Banshee/MediaEngine/BpmDetector">
+    <BpmDetector class="Banshee.GStreamer.BpmDetector"/>
+  </Extension>
+
 </Addin>

Added: trunk/banshee/src/Backends/Banshee.GStreamer/Banshee.GStreamer/BpmDetector.cs
==============================================================================
--- (empty file)
+++ trunk/banshee/src/Backends/Banshee.GStreamer/Banshee.GStreamer/BpmDetector.cs	Mon Jan 26 03:45:46 2009
@@ -0,0 +1,192 @@
+// 
+// BpmDetector.cs
+//
+// Author:
+//   Gabriel Burt <gburt novell com>
+//
+// Copyright (C) 2008 Novell, Inc.
+//
+// Permission is hereby granted, free of charge, to any person obtaining
+// a copy of this software and associated documentation files (the
+// "Software"), to deal in the Software without restriction, including
+// without limitation the rights to use, copy, modify, merge, publish,
+// distribute, sublicense, and/or sell copies of the Software, and to
+// permit persons to whom the Software is furnished to do so, subject to
+// the following conditions:
+//
+// The above copyright notice and this permission notice shall be
+// included in all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
+// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
+// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
+// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+//
+
+using System;
+using System.Collections;
+using System.Collections.Generic;
+using System.Runtime.InteropServices;
+
+using Mono.Unix;
+
+using Hyena;
+using Hyena.Data;
+
+using Banshee.Base;
+using Banshee.Streaming;
+using Banshee.MediaEngine;
+using Banshee.ServiceStack;
+using Banshee.Configuration;
+using Banshee.Preferences;
+
+namespace Banshee.GStreamer
+{
+    public class BpmDetector : IBpmDetector
+    {
+        private HandleRef handle;
+        private SafeUri current_uri;
+        private Dictionary<int, int> bpm_histogram = new Dictionary<int, int> ();
+
+        private BpmDetectorProgressHandler progress_cb;
+        private BpmDetectorFinishedHandler finished_cb;
+        //private BpmDetectorErrorHandler error_cb;
+        
+        public event Action<SafeUri, int> FileFinished;
+        
+        public BpmDetector ()
+        {
+            try {   
+                handle = new HandleRef (this, bbd_new ());
+                
+                progress_cb = new BpmDetectorProgressHandler (OnNativeProgress);
+                bbd_set_progress_callback (handle, progress_cb);
+
+                finished_cb = new BpmDetectorFinishedHandler (OnNativeFinished);
+                bbd_set_finished_callback (handle, finished_cb);
+            } catch (Exception e) {
+                throw new ApplicationException (Catalog.GetString ("Could not create BPM detection driver."), e);
+            }
+        }
+        
+        public void Dispose ()
+        {
+            Reset ();
+            
+            bbd_destroy (handle);
+            handle = new HandleRef (this, IntPtr.Zero);
+        }
+        
+        public void Cancel ()
+        {
+            Dispose ();
+        }
+
+        public bool IsDetecting {
+            get { return bbd_get_is_detecting (handle); }
+        }
+        
+        private void Reset ()
+        {
+            current_uri = null;
+            bpm_histogram.Clear ();
+        }
+        
+        public void ProcessFile (SafeUri uri)
+        {
+            Reset ();
+            current_uri = uri;
+            
+            string path = uri.LocalPath;
+            IntPtr path_ptr = GLib.Marshaller.StringToPtrGStrdup (path);
+            try {
+                Log.DebugFormat ("GStreamer running beat detection on {0}", path);
+                bbd_process_file (handle, path_ptr);
+            } catch (Exception e) {
+                Log.Exception (e);
+            } finally {
+                GLib.Marshaller.Free (path_ptr);
+            }
+        }
+        
+        private void OnFileFinished (SafeUri uri, int bpm)
+        {
+            Action<SafeUri, int> handler = FileFinished;
+            if (handler != null) {
+                handler (uri, bpm);
+            }
+        }
+        
+        private void OnNativeProgress (double bpm)
+        {
+            int rounded = (int) Math.Round (bpm);
+            if (!bpm_histogram.ContainsKey(rounded)) {
+                bpm_histogram[rounded] = 1;
+            } else {
+                bpm_histogram[rounded]++;
+            }
+        }
+        
+        private void OnNativeFinished ()
+        {
+            SafeUri uri = current_uri;
+
+            int best_bpm = -1, best_bpm_count = 0;
+            foreach (int bpm in bpm_histogram.Keys) {
+                int count = bpm_histogram[bpm];
+                if (count > best_bpm_count) {
+                    best_bpm_count = count;
+                    best_bpm = bpm;
+                }
+            }
+
+            Reset ();
+            OnFileFinished (uri, best_bpm);
+        }
+        
+        /*private void OnNativeError (IntPtr error, IntPtr debug)
+        {
+            string error_message = GLib.Marshaller.Utf8PtrToString (error);
+            
+            if (debug != IntPtr.Zero) {
+                string debug_string = GLib.Marshaller.Utf8PtrToString (debug);
+                if (!String.IsNullOrEmpty (debug_string)) {
+                    error_message = String.Format ("{0}: {1}", error_message, debug_string);
+                }
+            }
+            
+            Log.Debug (error_message);
+            SafeUri uri = current_uri;
+            Reset ();
+            OnFileFinished (uri, 0);
+        }*/
+        
+        private delegate void BpmDetectorProgressHandler (double bpm);
+        private delegate void BpmDetectorFinishedHandler ();
+        //private delegate void BpmDetectorErrorHandler (IntPtr error, IntPtr debug);
+        
+        [DllImport ("libbanshee")]
+        private static extern IntPtr bbd_new ();
+
+        [DllImport ("libbanshee")]
+        private static extern void bbd_destroy (HandleRef handle);
+
+        [DllImport ("libbanshee")]
+        private static extern bool bbd_get_is_detecting (HandleRef handle);
+        
+        [DllImport ("libbanshee")]
+        private static extern void bbd_process_file (HandleRef handle, IntPtr path);
+        
+        [DllImport ("libbanshee")]
+        private static extern void bbd_set_progress_callback (HandleRef handle, BpmDetectorProgressHandler callback);
+        
+        [DllImport ("libbanshee")]
+        private static extern void bbd_set_finished_callback (HandleRef handle, BpmDetectorFinishedHandler callback);
+        
+        //[DllImport ("libbanshee")]
+        //private static extern void bbd_set_error_callback (HandleRef handle, BpmDetectorErrorHandler callback);
+    }
+}

Modified: trunk/banshee/src/Backends/Banshee.GStreamer/Makefile.am
==============================================================================
--- trunk/banshee/src/Backends/Banshee.GStreamer/Makefile.am	(original)
+++ trunk/banshee/src/Backends/Banshee.GStreamer/Makefile.am	Mon Jan 26 03:45:46 2009
@@ -3,6 +3,7 @@
 LINK = $(REF_BACKEND_GSTREAMER)
 SOURCES =  \
 	Banshee.GStreamer/AudioCdRipper.cs \
+	Banshee.GStreamer/BpmDetector.cs \
 	Banshee.GStreamer/GstErrors.cs \
 	Banshee.GStreamer/PlayerEngine.cs \
 	Banshee.GStreamer/Service.cs \

Copied: trunk/banshee/src/Core/Banshee.Services/Banshee.MediaEngine/IBpmDetector.cs (from r4962, /trunk/banshee/src/Core/Banshee.Services/Banshee.ServiceStack/IUserJob.cs)
==============================================================================
--- /trunk/banshee/src/Core/Banshee.Services/Banshee.ServiceStack/IUserJob.cs	(original)
+++ trunk/banshee/src/Core/Banshee.Services/Banshee.MediaEngine/IBpmDetector.cs	Mon Jan 26 03:45:46 2009
@@ -1,10 +1,10 @@
-// 
-// IUserJob.cs
+//
+// IBpmDetector.cs
 //
 // Author:
-//   Aaron Bockover <abockover novell com>
+//   Gabriel Burt <gburt novell com>
 //
-// Copyright (C) 2007 Novell, Inc.
+// Copyright (C) 2008 Novell, Inc.
 //
 // Permission is hereby granted, free of charge, to any person obtaining
 // a copy of this software and associated documentation files (the
@@ -28,27 +28,17 @@
 
 using System;
 
-using Hyena.Data;
+using Banshee.Base;
+using Banshee.MediaProfiles;
+using Banshee.Collection;
 
-namespace Banshee.ServiceStack
-{   
-    public interface IUserJob
+namespace Banshee.MediaEngine
+{
+    public interface IBpmDetector : IDisposable
     {
-        event EventHandler Finished;
-        event EventHandler Updated;
-        
+        event Action<SafeUri, int> FileFinished;
+
+        void ProcessFile (SafeUri uri);
         void Cancel ();
-        
-        string Title { get; }
-        string Status { get; }
-        double Progress { get; }
-        string [] IconNames { get; }
-        
-        string CancelMessage { get; }        
-        bool CanCancel { get; }
-        
-        bool IsFinished { get; }
-        bool IsCancelRequested { get; }
-        bool DelayShow { get; }
     }
 }

Modified: trunk/banshee/src/Core/Banshee.Services/Banshee.ServiceStack/IUserJob.cs
==============================================================================
--- trunk/banshee/src/Core/Banshee.Services/Banshee.ServiceStack/IUserJob.cs	(original)
+++ trunk/banshee/src/Core/Banshee.Services/Banshee.ServiceStack/IUserJob.cs	Mon Jan 26 03:45:46 2009
@@ -43,6 +43,7 @@
         string Status { get; }
         double Progress { get; }
         string [] IconNames { get; }
+        bool IsBackground { get; }
         
         string CancelMessage { get; }        
         bool CanCancel { get; }

Modified: trunk/banshee/src/Core/Banshee.Services/Banshee.ServiceStack/UserJob.cs
==============================================================================
--- trunk/banshee/src/Core/Banshee.Services/Banshee.ServiceStack/UserJob.cs	(original)
+++ trunk/banshee/src/Core/Banshee.Services/Banshee.ServiceStack/UserJob.cs	Mon Jan 26 03:45:46 2009
@@ -44,6 +44,7 @@
         private bool is_cancel_requested;
         private bool is_finished;
         private bool delay_show;
+        private bool is_background;
         
         private int update_freeze_ref;
         
@@ -164,6 +165,11 @@
                 }
             }
         }
+
+        public virtual bool IsBackground {
+            get { return is_background; }
+            set { is_background = value; }
+        }
         
         public virtual string CancelMessage {
             get { return cancel_message; }

Modified: trunk/banshee/src/Core/Banshee.Services/Banshee.Services.addin.xml
==============================================================================
--- trunk/banshee/src/Core/Banshee.Services/Banshee.Services.addin.xml	(original)
+++ trunk/banshee/src/Core/Banshee.Services/Banshee.Services.addin.xml	Mon Jan 26 03:45:46 2009
@@ -33,6 +33,10 @@
     <ExtensionNode name="Transcoder"/>
   </ExtensionPoint>
 
+  <ExtensionPoint path="/Banshee/MediaEngine/BpmDetector">
+    <ExtensionNode name="BpmDetector"/>
+  </ExtensionPoint>
+
   <ExtensionPoint path="/Banshee/Platform/HardwareManager">
     <ExtensionNode name="HardwareManager"/>
   </ExtensionPoint>

Modified: trunk/banshee/src/Core/Banshee.Services/Makefile.am
==============================================================================
--- trunk/banshee/src/Core/Banshee.Services/Makefile.am	(original)
+++ trunk/banshee/src/Core/Banshee.Services/Makefile.am	Mon Jan 26 03:45:46 2009
@@ -76,6 +76,7 @@
 	Banshee.Library/ThreadPoolImportSource.cs \
 	Banshee.Library/VideoLibrarySource.cs \
 	Banshee.MediaEngine/IAudioCdRipper.cs \
+	Banshee.MediaEngine/IBpmDetector.cs \
 	Banshee.MediaEngine/IEqualizer.cs \
 	Banshee.MediaEngine/IPlayerEngineService.cs \
 	Banshee.MediaEngine/ITranscoder.cs \

Modified: trunk/banshee/src/Core/Banshee.ThickClient/Banshee.Gui.TrackEditor/FieldPage.cs
==============================================================================
--- trunk/banshee/src/Core/Banshee.ThickClient/Banshee.Gui.TrackEditor/FieldPage.cs	(original)
+++ trunk/banshee/src/Core/Banshee.ThickClient/Banshee.Gui.TrackEditor/FieldPage.cs	Mon Jan 26 03:45:46 2009
@@ -53,6 +53,8 @@
         
         public struct FieldSlot
         {
+            public Widget Parent;
+            public Widget Container;
             public Widget Label;
             public Widget Field;
             public Button SyncButton;
@@ -61,8 +63,12 @@
             public FieldValueClosure WriteClosure;
         }
         
-        private List<FieldSlot> field_slots = new List<FieldSlot> ();
         private object tooltip_host;
+
+        private List<FieldSlot> field_slots = new List<FieldSlot> ();
+        public IEnumerable<FieldSlot> FieldSlots {
+            get { return field_slots; }
+        }
         
         public FieldPage ()
         {
@@ -121,6 +127,7 @@
         {
             FieldSlot slot = new FieldSlot ();
             
+            slot.Parent = parent;
             slot.Label = label;
             slot.Field = field;
             slot.LabelClosure = labelClosure;
@@ -138,8 +145,6 @@
                 };
             }
             
-            field_slots.Add (slot);
-            
             Table table = new Table (1, 1, false);
             table.ColumnSpacing = 1;
             
@@ -171,16 +176,24 @@
             table.ShowAll ();
             
             if ((options & FieldOptions.Shrink) == 0) {
+                slot.Container = table;
                 parent.PackStart (table, false, false, 0);
             } else {
                 HBox shrink = new HBox ();
                 shrink.Show ();
+                slot.Container = shrink;
                 shrink.PackStart (table, false, false, 0);
                 parent.PackStart (shrink, false, false, 0);
             }
             
+            field_slots.Add (slot);
             return slot;
         }
+
+        public void RemoveField (FieldSlot slot)
+        {
+            field_slots.Remove (slot);
+        }
         
         public virtual void LoadTrack (EditorTrackInfo track)
         {

Copied: trunk/banshee/src/Core/Banshee.ThickClient/Banshee.Gui.TrackEditor/ITrackEditorModifier.cs (from r4962, /trunk/banshee/src/Core/Banshee.Services/Banshee.ServiceStack/IUserJob.cs)
==============================================================================
--- /trunk/banshee/src/Core/Banshee.Services/Banshee.ServiceStack/IUserJob.cs	(original)
+++ trunk/banshee/src/Core/Banshee.ThickClient/Banshee.Gui.TrackEditor/ITrackEditorModifier.cs	Mon Jan 26 03:45:46 2009
@@ -1,10 +1,10 @@
-// 
-// IUserJob.cs
+//
+// ITrackEditorModifier.cs
 //
 // Author:
-//   Aaron Bockover <abockover novell com>
+//   Gabriel Burt <gburt novell com>
 //
-// Copyright (C) 2007 Novell, Inc.
+// Copyright (C) 2008 Novell, Inc.
 //
 // Permission is hereby granted, free of charge, to any person obtaining
 // a copy of this software and associated documentation files (the
@@ -28,27 +28,10 @@
 
 using System;
 
-using Hyena.Data;
-
-namespace Banshee.ServiceStack
-{   
-    public interface IUserJob
+namespace Banshee.Gui.TrackEditor
+{
+    public interface ITrackEditorModifier
     {
-        event EventHandler Finished;
-        event EventHandler Updated;
-        
-        void Cancel ();
-        
-        string Title { get; }
-        string Status { get; }
-        double Progress { get; }
-        string [] IconNames { get; }
-        
-        string CancelMessage { get; }        
-        bool CanCancel { get; }
-        
-        bool IsFinished { get; }
-        bool IsCancelRequested { get; }
-        bool DelayShow { get; }
+        void Modify (TrackEditorDialog dialog);
     }
 }

Modified: trunk/banshee/src/Core/Banshee.ThickClient/Banshee.Gui.TrackEditor/TrackEditorDialog.cs
==============================================================================
--- trunk/banshee/src/Core/Banshee.ThickClient/Banshee.Gui.TrackEditor/TrackEditorDialog.cs	(original)
+++ trunk/banshee/src/Core/Banshee.ThickClient/Banshee.Gui.TrackEditor/TrackEditorDialog.cs	Mon Jan 26 03:45:46 2009
@@ -111,6 +111,8 @@
             BuildHeader ();
             BuildNotebook ();
             BuildFooter ();
+
+            LoadModifiers ();
             
             LoadTrackToEditor ();
         }
@@ -282,6 +284,18 @@
             main_vbox.PackStart (button_box, false, false, 0);
             button_box.ShowAll ();
         }
+
+        private void LoadModifiers ()
+        {
+            foreach (TypeExtensionNode node in AddinManager.GetExtensionNodes ("/Banshee/Gui/TrackEditor/Modifier")) {
+                try {
+                    ITrackEditorModifier mod = (ITrackEditorModifier)node.CreateInstance ();
+                    mod.Modify (this);
+                } catch (Exception e) {
+                    Hyena.Log.Exception ("Failed to initialize TrackEditor/Modifier extension node. Ensure it implements ITrackEditorModifier.", e);
+                }
+            }
+        }
         
         public void ForeachWidget<T> (WidgetAction<T> action) where T : class
         {

Modified: trunk/banshee/src/Core/Banshee.ThickClient/Banshee.Gui.Widgets/UserJobTileHost.cs
==============================================================================
--- trunk/banshee/src/Core/Banshee.ThickClient/Banshee.Gui.Widgets/UserJobTileHost.cs	(original)
+++ trunk/banshee/src/Core/Banshee.ThickClient/Banshee.Gui.Widgets/UserJobTileHost.cs	Mon Jan 26 03:45:46 2009
@@ -92,6 +92,10 @@
         
         private void OnJobAdded (object o, UserJobEventArgs args)
         {
+            if (args.Job.IsBackground) {
+                return;
+            }
+
             ThreadAssist.ProxyToMain (delegate {
                 if (args.Job.DelayShow) {
                     // Give the Job 1 second to become more than 33% complete

Modified: trunk/banshee/src/Core/Banshee.ThickClient/Banshee.ThickClient.addin.xml
==============================================================================
--- trunk/banshee/src/Core/Banshee.ThickClient/Banshee.ThickClient.addin.xml	(original)
+++ trunk/banshee/src/Core/Banshee.ThickClient/Banshee.ThickClient.addin.xml	Mon Jan 26 03:45:46 2009
@@ -44,5 +44,10 @@
     <Description>Defines a new notebook page for the track editor.</Description>
     <ExtensionNode name="TrackEditorPage"/>
   </ExtensionPoint>
+
+  <ExtensionPoint path="/Banshee/Gui/TrackEditor/Modifier">
+    <Description>Defines an extension for the track editor that can modify it in some way.</Description>
+    <ExtensionNode name="Modifier"/>
+  </ExtensionPoint>
   
 </Addin>

Modified: trunk/banshee/src/Core/Banshee.ThickClient/Makefile.am
==============================================================================
--- trunk/banshee/src/Core/Banshee.ThickClient/Makefile.am	(original)
+++ trunk/banshee/src/Core/Banshee.ThickClient/Makefile.am	Mon Jan 26 03:45:46 2009
@@ -66,6 +66,7 @@
 	Banshee.Gui.TrackEditor/ICanUndo.cs \
 	Banshee.Gui.TrackEditor/IEditorField.cs \
 	Banshee.Gui.TrackEditor/ITrackEditorPage.cs \
+	Banshee.Gui.TrackEditor/ITrackEditorModifier.cs \
 	Banshee.Gui.TrackEditor/LyricsPage.cs \
 	Banshee.Gui.TrackEditor/PageNavigationEntry.cs \
 	Banshee.Gui.TrackEditor/PageType.cs \

Added: trunk/banshee/src/Extensions/Banshee.Bpm/Banshee.Bpm.addin.xml
==============================================================================
--- (empty file)
+++ trunk/banshee/src/Extensions/Banshee.Bpm/Banshee.Bpm.addin.xml	Mon Jan 26 03:45:46 2009
@@ -0,0 +1,27 @@
+<?xml version="1.0" encoding="utf-8"?>
+<Addin 
+    id="Banshee.Bpm"
+    version="1.0"
+    compatVersion="1.0"
+    copyright="Â 2008 Novell Inc. Licensed under the MIT X11 license."
+    name="BPM Detection"
+    category="User Interface"
+    description="Detect the beats per minute (BPM) of your music"
+    author="Gabriel Burt"
+    url="http://banshee-project.org/";
+    defaultEnabled="true">
+
+  <Dependencies>
+    <Addin id="Banshee.Services" version="1.0"/>
+    <Addin id="Banshee.GStreamer" version="1.0"/>
+  </Dependencies>
+
+  <Extension path="/Banshee/ServiceManager/Service">
+    <Service class="Banshee.Bpm.BpmService"/>
+  </Extension>
+
+  <Extension path="/Banshee/Gui/TrackEditor/Modifier">
+    <Modifier class="Banshee.Bpm.BpmTrackEditorModifier"/>
+  </Extension>
+
+</Addin>

Added: trunk/banshee/src/Extensions/Banshee.Bpm/Banshee.Bpm.csproj
==============================================================================
--- (empty file)
+++ trunk/banshee/src/Extensions/Banshee.Bpm/Banshee.Bpm.csproj	Mon Jan 26 03:45:46 2009
@@ -0,0 +1,86 @@
+<?xml version="1.0" encoding="utf-8"?>
+<Project DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003";>
+  <PropertyGroup>
+    <Configuration Condition=" '$(Configuration)' == '' ">Debug</Configuration>
+    <Platform Condition=" '$(Platform)' == '' ">AnyCPU</Platform>
+    <ProductVersion>8.0.50727</ProductVersion>
+    <ProjectGuid>{12984BDF-C565-4452-AD47-79BD3C440E28}</ProjectGuid>
+    <OutputType>Library</OutputType>
+    <UseParentDirectoryAsNamespace>true</UseParentDirectoryAsNamespace>
+    <AssemblyName>Banshee.InternetRadio</AssemblyName>
+    <SchemaVersion>2.0</SchemaVersion>
+  </PropertyGroup>
+  <PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' ">
+    <DebugSymbols>true</DebugSymbols>
+    <DebugType>full</DebugType>
+    <Optimize>true</Optimize>
+    <OutputPath>..\..\..\bin</OutputPath>
+    <ErrorReport>prompt</ErrorReport>
+    <WarningLevel>4</WarningLevel>
+    <CheckForOverflowUnderflow>true</CheckForOverflowUnderflow>
+    <CustomCommands>
+      <CustomCommands>
+        <Command type="Build" command="make" workingdir="${SolutionDir}" />
+        <Command type="Execute" command="make run" workingdir="${SolutionDir}" />
+      </CustomCommands>
+    </CustomCommands>
+    <AssemblyKeyFile>.</AssemblyKeyFile>
+  </PropertyGroup>
+  <ItemGroup>
+    <ProjectReference Include="..\..\Core\Banshee.Core\Banshee.Core.csproj">
+      <Project>{2ADB831A-A050-47D0-B6B9-9C19D60233BB}</Project>
+      <Name>Banshee.Core</Name>
+    </ProjectReference>
+    <ProjectReference Include="..\..\Core\Banshee.Services\Banshee.Services.csproj">
+      <Project>{B28354F0-BA87-44E8-989F-B864A3C7C09F}</Project>
+      <Name>Banshee.Services</Name>
+    </ProjectReference>
+    <ProjectReference Include="..\..\Core\Banshee.ThickClient\Banshee.ThickClient.csproj">
+      <Project>{AC839523-7BDF-4AB6-8115-E17921B96EC6}</Project>
+      <Name>Banshee.ThickClient</Name>
+    </ProjectReference>
+    <ProjectReference Include="..\..\Libraries\Hyena\Hyena.csproj">
+      <Project>{95374549-9553-4C1E-9D89-667755F90E12}</Project>
+      <Name>Hyena</Name>
+    </ProjectReference>
+    <ProjectReference Include="..\..\Libraries\Mono.Media\Mono.Media.csproj">
+      <Project>{A7566CDC-6033-4A16-9E9D-87D05A627066}</Project>
+      <Name>Mono.Media</Name>
+    </ProjectReference>
+  </ItemGroup>
+  <ItemGroup>
+    <Reference Include="gtk-sharp, Version=2.12.0.0, Culture=neutral, PublicKeyToken=35e10195dab3c99f" />
+  </ItemGroup>
+  <ItemGroup>
+    <EmbeddedResource Include="Banshee.InternetRadio.addin.xml" />
+    <EmbeddedResource Include="Resources\ActiveSourceUI.xml">
+      <LogicalName>ActiveSourceUI.xml</LogicalName>
+    </EmbeddedResource>
+    <EmbeddedResource Include="Resources\GlobalUI.xml">
+      <LogicalName>GlobalUI.xml</LogicalName>
+    </EmbeddedResource>
+  </ItemGroup>
+  <ItemGroup>
+    <Compile Include="Banshee.InternetRadio\InternetRadioSource.cs" />
+    <Compile Include="Banshee.InternetRadio\StationEditor.cs" />
+    <Compile Include="Banshee.InternetRadio\XspfMigrator.cs" />
+    <Compile Include="Banshee.InternetRadio\InternetRadioSourceContents.cs" />
+  </ItemGroup>
+  <Import Project="$(MSBuildBinPath)\Microsoft.CSharp.targets" />
+  <ProjectExtensions>
+    <MonoDevelop>
+      <Properties>
+        <GtkDesignInfo />
+        <MonoDevelop.Autotools.MakefileInfo IntegrationEnabled="true" RelativeMakefileName="./Makefile.am">
+          <BuildFilesVar Sync="true" Name="SOURCES" />
+          <DeployFilesVar />
+          <ResourcesVar Sync="true" Name="RESOURCES" />
+          <OthersVar />
+          <GacRefVar />
+          <AsmRefVar />
+          <ProjectRefVar />
+        </MonoDevelop.Autotools.MakefileInfo>
+      </Properties>
+    </MonoDevelop>
+  </ProjectExtensions>
+</Project>
\ No newline at end of file

Added: trunk/banshee/src/Extensions/Banshee.Bpm/Banshee.Bpm/BpmDetectJob.cs
==============================================================================
--- (empty file)
+++ trunk/banshee/src/Extensions/Banshee.Bpm/Banshee.Bpm/BpmDetectJob.cs	Mon Jan 26 03:45:46 2009
@@ -0,0 +1,160 @@
+//
+// BpmDetectJob.cs
+//
+// Authors:
+//   Gabriel Burt <gburt novell com>
+//
+// Copyright (C) 2008 Novell, Inc.
+//
+// Permission is hereby granted, free of charge, to any person obtaining
+// a copy of this software and associated documentation files (the
+// "Software"), to deal in the Software without restriction, including
+// without limitation the rights to use, copy, modify, merge, publish,
+// distribute, sublicense, and/or sell copies of the Software, and to
+// permit persons to whom the Software is furnished to do so, subject to
+// the following conditions:
+//
+// The above copyright notice and this permission notice shall be
+// included in all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
+// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
+// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
+// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+//
+
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Threading;
+
+using Mono.Unix;
+using Mono.Addins;
+
+using Hyena;
+using Hyena.Data.Sqlite;
+
+using Banshee.Base;
+using Banshee.Collection;
+using Banshee.Collection.Database;
+using Banshee.Sources;
+using Banshee.Kernel;
+using Banshee.Metadata;
+using Banshee.MediaEngine;
+using Banshee.ServiceStack;
+using Banshee.Library;
+
+namespace Banshee.Bpm
+{
+    public class BpmDetectJob : UserJob, IJob
+    {
+        private PrimarySource music_library;
+        private int current_count;
+        private int current_track_id;
+        private IBpmDetector detector;
+        
+        private static HyenaSqliteCommand count_query = new HyenaSqliteCommand (
+            "SELECT COUNT(*) FROM CoreTracks WHERE PrimarySourceID = ? AND (BPM = 0 OR BPM IS NULL)");
+
+        private static HyenaSqliteCommand select_query = new HyenaSqliteCommand (@"
+            SELECT DISTINCT Uri, UriType, TrackID
+            FROM CoreTracks
+            WHERE PrimarySourceID = ? AND (BPM = 0 OR BPM IS NULL) LIMIT 1");
+
+        private static HyenaSqliteCommand update_query = new HyenaSqliteCommand (
+            "UPDATE CoreTracks SET BPM = ?, DateUpdatedStamp = ? WHERE TrackID = ?");
+        
+        public BpmDetectJob () : base (Catalog.GetString ("Detecting BPM"))
+        {
+            music_library = ServiceManager.SourceManager.MusicLibrary;
+            IconNames = new string [] {"audio-x-generic"};
+            IsBackground = true;
+        }
+        
+        public void Start ()
+        {
+            Register ();
+            Scheduler.Schedule (this, JobPriority.Lowest);
+        }
+
+        private HyenaDataReader RunQuery ()
+        {
+            return new HyenaDataReader (ServiceManager.DbConnection.Query (select_query,
+                music_library.DbId
+            ));
+        }
+        
+        public void Run ()
+        {
+            int total = ServiceManager.DbConnection.Query<int> (count_query, music_library.DbId);
+            if (total > 0) {
+                detector = GetDetector ();
+                detector.FileFinished += OnFileFinished;
+
+                DetectNext ();
+            }
+        }
+
+        private void DetectNext ()
+        {
+            if (IsCancelRequested) {
+                Hyena.Log.Debug ("BPM detection cancelled");
+                Finish ();
+                detector.Dispose ();
+                return;
+            }
+
+            int total = current_count + ServiceManager.DbConnection.Query<int> (count_query, music_library.DbId);
+            try {
+                using (HyenaDataReader reader = RunQuery ()) {
+                    if (reader.Read ()) {
+                        SafeUri uri = music_library.UriAndTypeToSafeUri (
+                            reader.Get<TrackUriType> (1), reader.Get<string> (0)
+                        );
+                        current_track_id = reader.Get<int> (2);
+                        detector.ProcessFile (uri);
+                    } else {
+                        Finish ();
+                        detector.Dispose ();
+                    }
+                }
+            } catch (Exception e) {
+                Log.Exception (e);
+            } finally {
+                Progress = (double) current_count / (double) total;
+                current_count++;
+            }
+        }
+
+        private void OnFileFinished (SafeUri uri, int bpm)
+        {
+            if (bpm > 0) {
+                Log.DebugFormat ("Saving BPM of {0} for {1}", bpm, uri);
+                ServiceManager.DbConnection.Execute (update_query, bpm, DateTime.Now, current_track_id);
+            } else {
+                Log.DebugFormat ("Unable to detect BPM for {0}", uri);
+            }
+            DetectNext ();
+        }
+
+        internal static IBpmDetector GetDetector ()
+        {
+            IBpmDetector detector = null;
+            foreach (TypeExtensionNode node in AddinManager.GetExtensionNodes ("/Banshee/MediaEngine/BpmDetector")) {
+                try {
+                    detector = (IBpmDetector)node.CreateInstance (typeof (IBpmDetector));
+                } catch (Exception e) {
+                    Log.Exception (e);
+                }
+
+                if (detector != null) {
+                    break;
+                }
+            }
+            return detector;
+        }
+    }
+}

Added: trunk/banshee/src/Extensions/Banshee.Bpm/Banshee.Bpm/BpmEntry.cs
==============================================================================
--- (empty file)
+++ trunk/banshee/src/Extensions/Banshee.Bpm/Banshee.Bpm/BpmEntry.cs	Mon Jan 26 03:45:46 2009
@@ -0,0 +1,182 @@
+//
+// BpmEntry.cs
+//
+// Author:
+//   Gabriel Burt <gburt novell com>
+//
+// Copyright (C) 2008 Novell, Inc.
+//
+// Permission is hereby granted, free of charge, to any person obtaining
+// a copy of this software and associated documentation files (the
+// "Software"), to deal in the Software without restriction, including
+// without limitation the rights to use, copy, modify, merge, publish,
+// distribute, sublicense, and/or sell copies of the Software, and to
+// permit persons to whom the Software is furnished to do so, subject to
+// the following conditions:
+//
+// The above copyright notice and this permission notice shall be
+// included in all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
+// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
+// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
+// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+//
+
+using System;
+using Mono.Unix;
+using Gtk;
+
+using Mono.Addins;
+
+using Hyena;
+using Hyena.Gui;
+
+using Banshee.Base;
+using Banshee.ServiceStack;
+using Banshee.MediaEngine;
+using Banshee.Collection.Database;
+
+using Banshee.Gui;
+using Banshee.Gui.TrackEditor;
+
+namespace Banshee.Bpm
+{
+    public class BpmEntry : HBox, ICanUndo, IEditorField
+    {
+        private SpinButton bpm_entry;
+        private Button detect_button;
+        private BpmTapAdapter tap_adapter;
+        private EditorTrackInfo track;
+        private IBpmDetector detector;
+        private EditorEditableUndoAdapter<Entry> undo_adapter = new EditorEditableUndoAdapter<Entry> ();
+
+        public event EventHandler Changed;
+        
+        public BpmEntry ()
+        {
+            detector = BpmDetectJob.GetDetector ();
+            if (detector != null) {
+                detector.FileFinished += OnFileFinished;
+            }
+
+            BuildWidgets ();
+
+            Destroyed += delegate {
+                if (detector != null) {
+                    detector.Dispose ();
+                    detector = null;
+                }
+            };
+        }
+
+        private void BuildWidgets ()
+        {
+            Spacing = 6;
+
+            bpm_entry = new SpinButton (0, 9999, 1);
+            bpm_entry.MaxLength = bpm_entry.WidthChars = 4;
+            bpm_entry.Digits = 0;
+            bpm_entry.Numeric = true;
+            bpm_entry.ValueChanged += OnChanged;
+            Add (bpm_entry);
+
+            if (detector != null) {
+                detect_button = new Button (Catalog.GetString ("Detect"));
+                detect_button.Clicked += OnDetectClicked;
+                Add (detect_button);
+            }
+
+            Image play = new Image ();
+            play.IconName = "media-playback-start";;
+            play.IconSize = (int)IconSize.Menu;
+            Button play_button = new Button (play);
+            play_button.Clicked += OnPlayClicked;
+            Add (play_button);
+
+            Button tap_button = new Button (Catalog.GetString ("Tap"));
+            tap_adapter = new BpmTapAdapter (tap_button);
+            tap_adapter.BpmChanged += OnTapBpmChanged;
+            Add (tap_button);
+
+            object tooltip_host = TooltipSetter.CreateHost ();
+
+            TooltipSetter.Set (tooltip_host, detect_button,
+                Catalog.GetString ("Have Banshee attempt to auto-detect the BPM of this song"));
+
+            TooltipSetter.Set (tooltip_host, play_button, Catalog.GetString ("Play this song"));
+
+            TooltipSetter.Set (tooltip_host, tap_button,
+                Catalog.GetString ("Tap this button to the beat to set the BPM for this song manually"));
+
+            ShowAll ();
+        }
+        
+        public void DisconnectUndo ()
+        {
+            undo_adapter.DisconnectUndo ();
+        }
+        
+        public void ConnectUndo (EditorTrackInfo track)
+        {
+            this.track = track;
+            tap_adapter.Reset ();
+            undo_adapter.ConnectUndo (bpm_entry, track);
+        }
+        
+        public int Bpm {
+            get { return bpm_entry.ValueAsInt; }
+            set { bpm_entry.Value = value; }
+        }
+
+        private void OnChanged (object o, EventArgs args)
+        {
+            EventHandler handler = Changed;
+            if (handler != null) {
+                handler (this, EventArgs.Empty);
+            }
+        }
+
+        private void OnTapBpmChanged (int bpm)
+        {
+            Console.WriteLine ("Got Tap Bpm changed: {0}", bpm);
+            Bpm = bpm;
+            OnChanged (null, null);
+        }
+
+        private void OnDetectClicked (object o, EventArgs args)
+        {
+            if (track != null) {
+                detect_button.Sensitive = false;
+                detector.ProcessFile (track.Uri);
+            }
+        }
+
+        private void OnFileFinished (SafeUri uri, int bpm)
+        {
+            ThreadAssist.ProxyToMain (delegate {
+                detect_button.Sensitive = true;
+
+                if (track.Uri != uri || bpm == 0) {
+                    return;
+                }
+
+                Log.DebugFormat ("Detected BPM of {0} for {1}", bpm, uri);
+                Bpm = bpm;
+                OnChanged (null, null);
+            });
+        }
+
+        private void OnPlayClicked (object o, EventArgs args)
+        {
+            if (track != null) {
+                ServiceManager.PlayerEngine.Open (track);
+                Gtk.ActionGroup actions = ServiceManager.Get<InterfaceActionService> ().PlaybackActions;
+                (actions["StopWhenFinishedAction"] as Gtk.ToggleAction).Active = true;
+            }
+        }
+    }
+}

Added: trunk/banshee/src/Extensions/Banshee.Bpm/Banshee.Bpm/BpmService.cs
==============================================================================
--- (empty file)
+++ trunk/banshee/src/Extensions/Banshee.Bpm/Banshee.Bpm/BpmService.cs	Mon Jan 26 03:45:46 2009
@@ -0,0 +1,188 @@
+//
+// BpmService.cs
+//
+// Authors:
+//   Gabriel Burt <gburt novell com>
+//
+// Copyright (C) 2008 Novell, Inc.
+//
+// Permission is hereby granted, free of charge, to any person obtaining
+// a copy of this software and associated documentation files (the
+// "Software"), to deal in the Software without restriction, including
+// without limitation the rights to use, copy, modify, merge, publish,
+// distribute, sublicense, and/or sell copies of the Software, and to
+// permit persons to whom the Software is furnished to do so, subject to
+// the following conditions:
+//
+// The above copyright notice and this permission notice shall be
+// included in all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
+// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
+// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
+// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+//
+
+using System;
+using System.Data;
+
+using Mono.Unix;
+
+using Hyena;
+
+using Banshee.Base;
+using Banshee.Collection;
+using Banshee.Collection.Database;
+using Banshee.ServiceStack;
+using Banshee.Configuration;
+using Banshee.Library;
+using Banshee.Metadata;
+using Banshee.Networking;
+using Banshee.Sources;
+using Banshee.Preferences;
+
+namespace Banshee.Bpm
+{
+    public class BpmService : IExtensionService
+    {
+        private BpmDetectJob job;
+        private bool disposed;
+        private object sync = new object ();
+        
+        public BpmService ()
+        {
+        }
+        
+        void IExtensionService.Initialize ()
+        {
+            Banshee.MediaEngine.IBpmDetector detector = BpmDetectJob.GetDetector ();
+            if (detector == null) {
+                throw new ApplicationException ("No BPM detector available");
+            } else {
+                detector.Dispose ();
+            }
+
+            if (!ServiceStartup ()) {
+                ServiceManager.SourceManager.SourceAdded += OnSourceAdded;
+            }
+        }
+        
+        private void OnSourceAdded (SourceAddedArgs args)
+        {
+            if (ServiceStartup ()) {
+                ServiceManager.SourceManager.SourceAdded -= OnSourceAdded;
+            }
+        }
+        
+        private bool ServiceStartup ()
+        {
+            if (ServiceManager.SourceManager.MusicLibrary == null) {
+                return false;
+            }
+            
+            ServiceManager.SourceManager.MusicLibrary.TracksAdded += OnTracksAdded;
+            InstallPreferences ();
+
+            Banshee.ServiceStack.Application.RunTimeout (4000, delegate {
+                Detect ();
+                return false;
+            });
+            
+            return true;
+        }
+        
+        public void Dispose ()
+        {
+            if (disposed) {
+                return;
+            }
+
+            ServiceManager.SourceManager.MusicLibrary.TracksAdded -= OnTracksAdded;
+            UninstallPreferences ();
+        
+            disposed = true;
+        }
+        
+        public void Detect ()
+        {
+            if (!Enabled) {
+                return;
+            }
+
+            lock (sync) {
+                if (job != null) {
+                    return;
+                } else {
+                    job = new BpmDetectJob ();
+                }
+            }
+
+            job.Finished += delegate { job = null; };
+            job.Start ();
+        }
+        
+        private void OnTracksAdded (Source sender, TrackEventArgs args)
+        {
+            Detect ();
+        }
+        
+        string IService.ServiceName {
+            get { return "BpmService"; }
+        }
+
+#region Preferences        
+
+        private PreferenceBase enabled_pref;
+        
+        private void InstallPreferences ()
+        {
+            PreferenceService service = ServiceManager.Get<PreferenceService> ();
+            if (service == null) {
+                return;
+            }
+            
+            enabled_pref = service["general"]["misc"].Add (new SchemaPreference<bool> (EnabledSchema, 
+                Catalog.GetString ("_Automatically detect BPM for all songs"),
+                Catalog.GetString ("Detect BPM for all songs that don't already have a value set"),
+                delegate { Enabled = EnabledSchema.Get (); }
+            ));
+        }
+        
+        private void UninstallPreferences ()
+        {
+            PreferenceService service = ServiceManager.Get<PreferenceService> ();
+            if (service == null) {
+                return;
+            }
+            
+            service["general"]["misc"].Remove (enabled_pref);
+            enabled_pref = null;
+        }
+        
+#endregion
+
+        public bool Enabled {
+            get { return EnabledSchema.Get (); }
+            set {
+                EnabledSchema.Set (value);
+                if (value) {
+                    Detect ();
+                } else {
+                    if (job != null) {
+                        job.Cancel ();
+                    }
+                }
+            }
+        }
+        
+        private static readonly SchemaEntry<bool> EnabledSchema = new SchemaEntry<bool> (
+            "plugins.bpm", "auto_enabled",
+            false,
+            "Automatically detect BPM on imported music",
+            "Automatically detect BPM on imported music"
+        );
+    }
+}

Added: trunk/banshee/src/Extensions/Banshee.Bpm/Banshee.Bpm/BpmTapAdapter.cs
==============================================================================
--- (empty file)
+++ trunk/banshee/src/Extensions/Banshee.Bpm/Banshee.Bpm/BpmTapAdapter.cs	Mon Jan 26 03:45:46 2009
@@ -0,0 +1,134 @@
+//
+// BpmTapWidget.cs
+//
+// Author:
+//   Gabriel Burt <gburt novell com>
+//
+// Copyright (C) 2008 Novell, Inc.
+//
+// Permission is hereby granted, free of charge, to any person obtaining
+// a copy of this software and associated documentation files (the
+// "Software"), to deal in the Software without restriction, including
+// without limitation the rights to use, copy, modify, merge, publish,
+// distribute, sublicense, and/or sell copies of the Software, and to
+// permit persons to whom the Software is furnished to do so, subject to
+// the following conditions:
+//
+// The above copyright notice and this permission notice shall be
+// included in all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
+// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
+// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
+// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+//
+
+using System;
+using System.Collections.Generic;
+
+using Mono.Unix;
+
+using Gtk;
+
+using Hyena;
+
+using Banshee.Base;
+
+namespace Banshee.Bpm
+{
+    public class BpmTapAdapter
+    {
+        private const int NUM_PERIODS = 20;
+
+        private int [] tap_periods = new int [NUM_PERIODS];
+        private int tap_index;
+        private int last_tap;
+        private uint timeout_id = 0;
+
+        private Button tap_button;
+
+        private int avg_bpm;
+        public int AverageBpm {
+            get { return avg_bpm; }
+        }
+
+        public event Action<int> BpmChanged;
+
+        public BpmTapAdapter (Button button)
+        {
+            tap_button = button;
+            tap_button.Clicked += OnTapped;
+        }
+
+        public void Reset ()
+        {
+            Reset (false);
+        }
+
+        private void Reset (bool inclLabel)
+        {
+            for (int i = 0; i < NUM_PERIODS; i++) {
+                tap_periods[i] = 0;
+            }
+            tap_index = 0;
+            last_tap = 0;
+
+            if (inclLabel) {
+                avg_bpm = 0;
+            }
+        }
+
+        private void OnTapped (object o, EventArgs args)
+        {
+            int now = Environment.TickCount;
+
+            if (last_tap != 0) {
+                int period = now - last_tap;
+                
+                Console.WriteLine ("{0} ms since last tap; eq {1} BPM", period, 60000/period);
+                tap_periods[tap_index] = period;
+                tap_index = (tap_index + 1) % (NUM_PERIODS - 1);
+                CalculateAverage ();
+
+                if (timeout_id != 0) {
+                    GLib.Source.Remove (timeout_id);
+                }
+
+                // If the next tap doesn't come soon enough, Reset the tap history
+                timeout_id = GLib.Timeout.Add ((uint)(1.5 * period), OnTapTimeout);
+            }
+
+            last_tap = now;
+        }
+
+        private bool OnTapTimeout ()
+        {
+            Console.WriteLine ("OnTapTimeout");
+            Reset (false);
+            timeout_id = 0;
+            return false;
+        }
+
+        private void CalculateAverage ()
+        {
+            int sum = 0;
+            int count = 0;
+            for (int i = 0; i < NUM_PERIODS; i++) {
+                if (tap_periods[i] > 0) {
+                    sum += tap_periods[i];
+                    count++;
+                }
+            }
+
+            avg_bpm = 60000 * count / sum;
+
+            Action<int> handler = BpmChanged;
+            if (handler != null) {
+                handler (avg_bpm);
+            }
+        }
+    }
+}

Added: trunk/banshee/src/Extensions/Banshee.Bpm/Banshee.Bpm/BpmTrackEditorModifier.cs
==============================================================================
--- (empty file)
+++ trunk/banshee/src/Extensions/Banshee.Bpm/Banshee.Bpm/BpmTrackEditorModifier.cs	Mon Jan 26 03:45:46 2009
@@ -0,0 +1,78 @@
+//
+// BpmTrackEditorModifier.cs
+//
+// Author:
+//   Gabriel Burt <gburt novell com>
+//
+// Copyright (C) 2008 Novell, Inc.
+//
+// Permission is hereby granted, free of charge, to any person obtaining
+// a copy of this software and associated documentation files (the
+// "Software"), to deal in the Software without restriction, including
+// without limitation the rights to use, copy, modify, merge, publish,
+// distribute, sublicense, and/or sell copies of the Software, and to
+// permit persons to whom the Software is furnished to do so, subject to
+// the following conditions:
+//
+// The above copyright notice and this permission notice shall be
+// included in all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
+// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
+// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
+// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+//
+
+using System;
+using System.Collections.Generic;
+using Mono.Unix;
+using Gtk;
+
+using Hyena;
+using Banshee.Base;
+using Banshee.Gui.TrackEditor;
+
+namespace Banshee.Bpm
+{
+    public class BpmTrackEditorModifier : ITrackEditorModifier
+    {
+        public BpmTrackEditorModifier ()
+        {
+        }
+
+        public void Modify (TrackEditorDialog dialog)
+        {
+            for (int i = 0; i < dialog.Notebook.NPages; i++) {
+                Widget page = dialog.Notebook.GetNthPage (i);
+                ExtraTrackDetailsPage extra_page = page as ExtraTrackDetailsPage;
+                if (extra_page != null) {
+                    foreach (FieldPage.FieldSlot slot in extra_page.FieldSlots) {
+                        // TODO this ia hacky - what if another Modifier added a 2nd SpinButton?
+                        if (slot.Field is Gtk.SpinButton) {
+                            // Remove it
+                            (slot.Parent as Container).Remove (slot.Container);
+                            extra_page.RemoveField (slot);
+
+                            // Add our new and improved version back
+                            extra_page.AddField (slot.Parent as Gtk.Box,
+                                new BpmEntry (), null,
+                                slot.LabelClosure,
+                                delegate (EditorTrackInfo track, Widget widget) {
+                                    ((BpmEntry)widget).Bpm = track.Bpm; },
+                                delegate (EditorTrackInfo track, Widget widget) {
+                                    track.Bpm = ((BpmEntry)widget).Bpm; },
+                                FieldOptions.Shrink | FieldOptions.NoSync
+                            );
+                            break;
+                        }
+                    }
+
+                    break;
+                }
+            }
+        }
+    }
+}

Added: trunk/banshee/src/Extensions/Banshee.Bpm/Makefile.am
==============================================================================
--- (empty file)
+++ trunk/banshee/src/Extensions/Banshee.Bpm/Makefile.am	Mon Jan 26 03:45:46 2009
@@ -0,0 +1,17 @@
+ASSEMBLY = Banshee.Bpm
+TARGET = library
+LINK = $(REF_EXTENSION_BPM)
+INSTALL_DIR = $(EXTENSIONS_INSTALL_DIR)
+
+SOURCES =  \
+	Banshee.Bpm/BpmDetectJob.cs \
+	Banshee.Bpm/BpmEntry.cs \
+	Banshee.Bpm/BpmService.cs \
+	Banshee.Bpm/BpmTapAdapter.cs \
+	Banshee.Bpm/BpmTrackEditorModifier.cs
+
+RESOURCES =  \
+	Banshee.Bpm.addin.xml
+
+include $(top_srcdir)/build/build.mk
+

Modified: trunk/banshee/src/Extensions/Makefile.am
==============================================================================
--- trunk/banshee/src/Extensions/Makefile.am	(original)
+++ trunk/banshee/src/Extensions/Makefile.am	Mon Jan 26 03:45:46 2009
@@ -2,6 +2,7 @@
 	Banshee.AudioCd \
 	Banshee.Bookmarks \
 	Banshee.BooScript \
+	Banshee.Bpm \
 	Banshee.CoverArt \
 	Banshee.Daap \
 	Banshee.FileSystemQueue \



[Date Prev][Date Next]   [Thread Prev][Thread Next]   [Thread Index] [Date Index] [Author Index]