Skip to content

Commit

Permalink
plugin: add a tap bpm plugin
Browse files Browse the repository at this point in the history
  • Loading branch information
ptitjes committed Feb 23, 2017
1 parent 86d0e94 commit 00765b4
Show file tree
Hide file tree
Showing 2 changed files with 194 additions and 0 deletions.
1 change: 1 addition & 0 deletions quodlibet/po/POTFILES.in
Expand Up @@ -127,6 +127,7 @@ quodlibet/ext/songsmenu/playlist.py
quodlibet/ext/songsmenu/refresh.py
quodlibet/ext/songsmenu/replaygain.py
quodlibet/ext/songsmenu/splitting.py
quodlibet/ext/songsmenu/tapbpm.py
quodlibet/ext/songsmenu/website_search.py
quodlibet/ext/songsmenu/wikipedia.py
quodlibet/formats/_audio.py
Expand Down
193 changes: 193 additions & 0 deletions quodlibet/quodlibet/ext/songsmenu/tapbpm.py
@@ -0,0 +1,193 @@
# -*- coding: utf-8 -*-
# Copyright 2017 Didier Villevalois,
# 2010 Christoph Reiter
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License version 2 as
# published by the Free Software Foundation

from gi.repository import Gtk, GObject

from quodlibet import _
from quodlibet.plugins.songsmenu import SongsMenuPlugin
from quodlibet.qltk import Icons
from quodlibet.qltk.window import Dialog


class TapBpmPanel(Gtk.VBox):
def __init__(self, parent, song):
super(Gtk.VBox, self).__init__()

self.dialog = parent
self.song = song
self.original_bpm = float(song["bpm"]) if "bpm" in song else 0.0

self.set_margin_bottom(6)
self.set_spacing(6)

box = Gtk.HBox()
box.set_spacing(6)
box.pack_start(Gtk.Label(_("BPM:")), False, True, 0)
self.bpm_label = Gtk.Label(_("n/a"))
self.bpm_label.set_xalign(0.5)
box.pack_start(self.bpm_label, True, True, 0)

self.reset_btn = Gtk.Button(label=_("Reset"))
self.reset_btn.connect('clicked', lambda *x: self.reset())
box.pack_end(self.reset_btn, False, True, 0)

self.pack_start(box, False, True, 0)

self.tap_btn = Gtk.Button(label=_("Tap"))
self.tap_btn.connect('button-press-event', self.tap)
self.pack_start(self.tap_btn, True, True, 0)

self.init_tap()
self.update()

self.show_all()

def update(self):
GObject.idle_add(self.do_update)

def do_update(self):
has_new_bpm = self.floating_bpm != self.original_bpm

self.dialog.set_response_sensitive(Gtk.ResponseType.OK, has_new_bpm)
self.reset_btn.set_sensitive(has_new_bpm)

if self.floating_bpm != 0.0:
self.bpm_label.set_text("%.1f" % self.floating_bpm)
else:
self.bpm_label.set_text(_("n/a"))

def tap(self, widget, event):
self.count_tap(event.time)
self.update()

def reset(self):
self.init_tap()
self.update()

def save(self):
self.song["bpm"] = "%.0f" % self.floating_bpm
self.song.write()

def init_tap(self):
self.bpm = 0.0
self.clicks = 0
self.last = 0
self.last_times = []
self.last_bpms = []
self.last_floating_bpms = []
self.last_floating_squares = []
self.bpms_sum = 0.0
self.squares_sum = 0.0
self.average_count = 0
self.min = 0
self.max = 0
self.floating_bpm = self.original_bpm
self.floating_square = 0.0
self.keep = 100

self.pause = 3
self.min_weight = 0.01

def count_tap(self, now):
now = now / 1000.
# reset?
if now - self.last > self.pause:
self.clicks = 0
self.bpm = 0.0

self.last_times = []
self.last_bpms = []
self.last_floating_bpms = []
self.last_floating_squares = []
self.bpms_sum = 0.0
self.squares_sum = 0.0
self.average_count = 0
self.min = 0
self.max = 0
self.floating_bpm = 0.0
self.floating_square = 0.0
elif now > self.last:
# Use previous 5 values to average BPM
bpms = []
bpms.append(60.0 / (now - self.last))
# and four out of the list
for i in xrange(1, 5):
if len(self.last_times) <= i:
break
bpms.append((i + 1) * 60.0 / (now - self.last_times[-i]))

self.bpm = reduce(lambda x, y: x + y, bpms) / len(bpms)

# Exponentially weighted floating average
weight = (1.0 / self.clicks) ** .5
if weight < self.min_weight:
weight = self.min_weight
self.floating_bpm = \
self.floating_bpm * (1.0 - weight) \
+ self.bpm * weight
self.floating_square = \
self.floating_square * (1.0 - weight) \
+ self.bpm * self.bpm * weight

if self.bpm < self.min or self.average_count == 0:
self.min = self.bpm
if self.bpm > self.max or self.average_count == 0:
self.max = self.bpm
self.bpms_sum += self.bpm
self.squares_sum += self.bpm * self.bpm
self.average_count += 1

# Update history
self.last_times = self.last_times[-(self.keep - 1):] + [self.last]
self.last_bpms = self.last_bpms[-(self.keep - 1):] + [self.bpm]
self.last_floating_bpms = \
self.last_floating_bpms[-(self.keep - 1):] \
+ [self.floating_bpm]
self.last_floating_squares = \
self.last_floating_squares[-(self.keep - 1):] \
+ [self.floating_square]

self.last = now
self.clicks += 1


class TapBpm(SongsMenuPlugin):
PLUGIN_ID = "Tap BPM"
PLUGIN_NAME = _("Tap BPM")
PLUGIN_DESC = _("Tap BPM for the selected song.")
PLUGIN_ICON = Icons.EDIT
PLUGIN_VERSION = "0.1"

def plugin_song(self, song):
self._window = window = \
Dialog(title=_("Tap BPM"), parent=self.plugin_window)

window.add_button(_("_Cancel"), Gtk.ResponseType.CANCEL)
window.add_icon_button(_("_Save"), Icons.DOCUMENT_SAVE,
Gtk.ResponseType.OK)

window.set_default_size(300, 100)
window.set_border_width(6)
window.connect('response', self.response)

self._panel = TapBpmPanel(window, song)
window.vbox.pack_start(self._panel, False, True, 0)

window.vbox.show_all()
window.present()

def response(self, win, response):
self._window.hide()
self._window.destroy()
del self._window

if response != Gtk.ResponseType.OK:
return

# Save metadata
self._panel.save()

0 comments on commit 00765b4

Please sign in to comment.