Add moodbar
This commit is contained in:
parent
37b923bea3
commit
907d18a83a
@ -114,6 +114,7 @@ pkg_check_modules(LIBIMOBILEDEVICE libimobiledevice-1.0)
|
||||
pkg_check_modules(LIBUSBMUXD libusbmuxd)
|
||||
pkg_check_modules(LIBPLIST libplist)
|
||||
find_package(Gettext)
|
||||
find_package(FFTW3)
|
||||
|
||||
if(WIN32)
|
||||
find_package(ZLIB REQUIRED)
|
||||
@ -346,6 +347,11 @@ optional_component(TRANSLATIONS ON "Translations"
|
||||
|
||||
optional_component(TIDAL ON "Tidal support")
|
||||
|
||||
optional_component(MOODBAR ON "Moodbar"
|
||||
DEPENDS "fftw3" FFTW3_FOUND
|
||||
DEPENDS "gstreamer" HAVE_GSTREAMER
|
||||
)
|
||||
|
||||
if(APPLE)
|
||||
option(USE_BUNDLE "Bundle macOS dependencies" OFF)
|
||||
elseif(WIN32)
|
||||
@ -378,6 +384,9 @@ add_subdirectory(dist)
|
||||
add_subdirectory(ext/libstrawberry-common)
|
||||
add_subdirectory(ext/libstrawberry-tagreader)
|
||||
add_subdirectory(ext/strawberry-tagreader)
|
||||
if(HAVE_MOODBAR)
|
||||
add_subdirectory(ext/gstmoodbar)
|
||||
endif()
|
||||
|
||||
# Uninstall support
|
||||
configure_file(
|
||||
|
@ -57,6 +57,7 @@ Optional dependencies:
|
||||
* MTP devices: [libmtp](http://libmtp.sourceforge.net/)
|
||||
* iPod Classic devices: [libgpod](http://www.gtkpod.org/libgpod/)
|
||||
* iPhone, iPod Touch, iPad and Apple TV devices: [libimobiledevice, libplist and libusbmuxd](https://www.libimobiledevice.org/)
|
||||
* Moodbar: [fftw3](http://www.fftw.org/)
|
||||
|
||||
Either GStreamer, Xine, VLC or Phonon engine is required, but only GStreamer is fully implemented so far.
|
||||
You should also install the gstreamer plugins base and good, and optionally bad and ugly.
|
||||
|
133
cmake/FindFFTW3.cmake
Normal file
133
cmake/FindFFTW3.cmake
Normal file
@ -0,0 +1,133 @@
|
||||
#
|
||||
# Try to find FFTW3 library
|
||||
# (see www.fftw.org)
|
||||
# Once run this will define:
|
||||
#
|
||||
# FFTW3_FOUND
|
||||
# FFTW3_INCLUDE_DIR
|
||||
# FFTW3_LIBRARIES
|
||||
# FFTW3_LINK_DIRECTORIES
|
||||
#
|
||||
# You may set one of these options before including this file:
|
||||
# FFTW3_USE_SSE2
|
||||
#
|
||||
# TODO: _F_ versions.
|
||||
#
|
||||
# Jan Woetzel 05/2004
|
||||
# www.mip.informatik.uni-kiel.de
|
||||
# --------------------------------
|
||||
|
||||
FIND_PATH(FFTW3_INCLUDE_DIR fftw3.h
|
||||
${FFTW3_DIR}/include
|
||||
${FFTW3_HOME}/include
|
||||
${FFTW3_DIR}
|
||||
${FFTW3_HOME}
|
||||
$ENV{FFTW3_DIR}/include
|
||||
$ENV{FFTW3_HOME}/include
|
||||
$ENV{FFTW3_DIR}
|
||||
$ENV{FFTW3_HOME}
|
||||
/usr/include
|
||||
/usr/local/include
|
||||
$ENV{SOURCE_DIR}/fftw3
|
||||
$ENV{SOURCE_DIR}/fftw3/include
|
||||
$ENV{SOURCE_DIR}/fftw
|
||||
$ENV{SOURCE_DIR}/fftw/include
|
||||
)
|
||||
#MESSAGE("DBG FFTW3_INCLUDE_DIR=${FFTW3_INCLUDE_DIR}")
|
||||
|
||||
|
||||
SET(FFTW3_POSSIBLE_LIBRARY_PATH
|
||||
${FFTW3_DIR}/lib
|
||||
${FFTW3_HOME}/lib
|
||||
${FFTW3_DIR}
|
||||
${FFTW3_HOME}
|
||||
$ENV{FFTW3_DIR}/lib
|
||||
$ENV{FFTW3_HOME}/lib
|
||||
$ENV{FFTW3_DIR}
|
||||
$ENV{FFTW3_HOME}
|
||||
/usr/lib
|
||||
/usr/local/lib
|
||||
$ENV{SOURCE_DIR}/fftw3
|
||||
$ENV{SOURCE_DIR}/fftw3/lib
|
||||
$ENV{SOURCE_DIR}/fftw
|
||||
$ENV{SOURCE_DIR}/fftw/lib
|
||||
)
|
||||
|
||||
|
||||
# the lib prefix is containe din filename onf W32, unfortuantely. JW
|
||||
# teh "general" lib:
|
||||
FIND_LIBRARY(FFTW3_FFTW_LIBRARY
|
||||
NAMES fftw3 libfftw libfftw3 libfftw3-3
|
||||
PATHS
|
||||
${FFTW3_POSSIBLE_LIBRARY_PATH}
|
||||
)
|
||||
#MESSAGE("DBG FFTW3_FFTW_LIBRARY=${FFTW3_FFTW_LIBRARY}")
|
||||
|
||||
FIND_LIBRARY(FFTW3_FFTWF_LIBRARY
|
||||
NAMES fftwf3 fftw3f fftwf libfftwf libfftwf3 libfftw3f-3
|
||||
PATHS
|
||||
${FFTW3_POSSIBLE_LIBRARY_PATH}
|
||||
)
|
||||
#MESSAGE("DBG FFTW3_FFTWF_LIBRARY=${FFTW3_FFTWF_LIBRARY}")
|
||||
|
||||
FIND_LIBRARY(FFTW3_FFTWL_LIBRARY
|
||||
NAMES fftwl3 fftw3l fftwl libfftwl libfftwl3 libfftw3l-3
|
||||
PATHS
|
||||
${FFTW3_POSSIBLE_LIBRARY_PATH}
|
||||
)
|
||||
#MESSAGE("DBG FFTW3_FFTWF_LIBRARY=${FFTW3_FFTWL_LIBRARY}")
|
||||
|
||||
|
||||
FIND_LIBRARY(FFTW3_FFTW_SSE2_LIBRARY
|
||||
NAMES fftw_sse2 fftw3_sse2 libfftw_sse2 libfftw3_sse2
|
||||
PATHS
|
||||
${FFTW3_POSSIBLE_LIBRARY_PATH}
|
||||
)
|
||||
#MESSAGE("DBG FFTW3_FFTW_SSE2_LIBRARY=${FFTW3_FFTW_SSE2_LIBRARY}")
|
||||
|
||||
FIND_LIBRARY(FFTW3_FFTWF_SSE_LIBRARY
|
||||
NAMES fftwf_sse fftwf3_sse libfftwf_sse libfftwf3_sse
|
||||
PATHS
|
||||
${FFTW3_POSSIBLE_LIBRARY_PATH}
|
||||
)
|
||||
#MESSAGE("DBG FFTW3_FFTWF_SSE_LIBRARY=${FFTW3_FFTWF_SSE_LIBRARY}")
|
||||
|
||||
|
||||
# --------------------------------
|
||||
# select one of the above
|
||||
# default:
|
||||
IF (FFTW3_FFTW_LIBRARY)
|
||||
SET(FFTW3_LIBRARIES ${FFTW3_FFTW_LIBRARY})
|
||||
ENDIF (FFTW3_FFTW_LIBRARY)
|
||||
# specialized:
|
||||
IF (FFTW3_USE_SSE2 AND FFTW3_FFTW_SSE2_LIBRARY)
|
||||
SET(FFTW3_LIBRARIES ${FFTW3_FFTW_SSE2_LIBRARY})
|
||||
ENDIF (FFTW3_USE_SSE2 AND FFTW3_FFTW_SSE2_LIBRARY)
|
||||
|
||||
# --------------------------------
|
||||
|
||||
IF(FFTW3_LIBRARIES)
|
||||
IF (FFTW3_INCLUDE_DIR)
|
||||
|
||||
# OK, found all we need
|
||||
SET(FFTW3_FOUND TRUE)
|
||||
GET_FILENAME_COMPONENT(FFTW3_LINK_DIRECTORIES ${FFTW3_LIBRARIES} PATH)
|
||||
|
||||
ELSE (FFTW3_INCLUDE_DIR)
|
||||
MESSAGE("FFTW3 include dir not found. Set FFTW3_DIR to find it.")
|
||||
ENDIF(FFTW3_INCLUDE_DIR)
|
||||
ELSE(FFTW3_LIBRARIES)
|
||||
MESSAGE("FFTW3 lib not found. Set FFTW3_DIR to find it.")
|
||||
ENDIF(FFTW3_LIBRARIES)
|
||||
|
||||
|
||||
MARK_AS_ADVANCED(
|
||||
FFTW3_INCLUDE_DIR
|
||||
FFTW3_LIBRARIES
|
||||
FFTW3_FFTW_LIBRARY
|
||||
FFTW3_FFTW_SSE2_LIBRARY
|
||||
FFTW3_FFTWF_LIBRARY
|
||||
FFTW3_FFTWF_SSE_LIBRARY
|
||||
FFTW3_FFTWL_LIBRARY
|
||||
FFTW3_LINK_DIRECTORIES
|
||||
)
|
@ -32,5 +32,6 @@
|
||||
<file>pictures/nyancat.png</file>
|
||||
<file>pictures/rainbowdash.png</file>
|
||||
<file>fonts/HumongousofEternitySt.ttf</file>
|
||||
<file>mood/sample.mood</file>
|
||||
</qresource>
|
||||
</RCC>
|
||||
|
@ -86,6 +86,7 @@
|
||||
<file>icons/128x128/tidal.png</file>
|
||||
<file>icons/128x128/scrobble.png</file>
|
||||
<file>icons/128x128/scrobble-disabled.png</file>
|
||||
<file>icons/128x128/moodbar.png</file>
|
||||
<file>icons/64x64/albums.png</file>
|
||||
<file>icons/64x64/alsa.png</file>
|
||||
<file>icons/64x64/application-exit.png</file>
|
||||
@ -172,6 +173,7 @@
|
||||
<file>icons/64x64/tidal.png</file>
|
||||
<file>icons/64x64/scrobble.png</file>
|
||||
<file>icons/64x64/scrobble-disabled.png</file>
|
||||
<file>icons/64x64/moodbar.png</file>
|
||||
<file>icons/48x48/albums.png</file>
|
||||
<file>icons/48x48/alsa.png</file>
|
||||
<file>icons/48x48/application-exit.png</file>
|
||||
@ -261,6 +263,7 @@
|
||||
<file>icons/48x48/tidal.png</file>
|
||||
<file>icons/48x48/scrobble.png</file>
|
||||
<file>icons/48x48/scrobble-disabled.png</file>
|
||||
<file>icons/48x48/moodbar.png</file>
|
||||
<file>icons/32x32/albums.png</file>
|
||||
<file>icons/32x32/alsa.png</file>
|
||||
<file>icons/32x32/application-exit.png</file>
|
||||
@ -351,6 +354,7 @@
|
||||
<file>icons/32x32/tidal.png</file>
|
||||
<file>icons/32x32/scrobble.png</file>
|
||||
<file>icons/32x32/scrobble-disabled.png</file>
|
||||
<file>icons/32x32/moodbar.png</file>
|
||||
<file>icons/22x22/albums.png</file>
|
||||
<file>icons/22x22/alsa.png</file>
|
||||
<file>icons/22x22/application-exit.png</file>
|
||||
@ -441,5 +445,6 @@
|
||||
<file>icons/22x22/tidal.png</file>
|
||||
<file>icons/22x22/scrobble.png</file>
|
||||
<file>icons/22x22/scrobble-disabled.png</file>
|
||||
<file>icons/22x22/moodbar.png</file>
|
||||
</qresource>
|
||||
</RCC>
|
||||
|
BIN
data/icons/128x128/moodbar.png
Normal file
BIN
data/icons/128x128/moodbar.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 5.1 KiB |
BIN
data/icons/22x22/moodbar.png
Normal file
BIN
data/icons/22x22/moodbar.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 1.6 KiB |
BIN
data/icons/32x32/moodbar.png
Normal file
BIN
data/icons/32x32/moodbar.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 944 B |
BIN
data/icons/48x48/moodbar.png
Normal file
BIN
data/icons/48x48/moodbar.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 1.7 KiB |
BIN
data/icons/64x64/moodbar.png
Normal file
BIN
data/icons/64x64/moodbar.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 2.4 KiB |
BIN
data/icons/full/moodbar.png
Normal file
BIN
data/icons/full/moodbar.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 14 KiB |
BIN
data/mood/sample.mood
Executable file
BIN
data/mood/sample.mood
Executable file
Binary file not shown.
7
debian/copyright
vendored
7
debian/copyright
vendored
@ -285,6 +285,13 @@ Files: src/widgets/stylehelper.cpp
|
||||
Copyright: 2010, Nokia Corporation and/or its subsidiary(-ies).
|
||||
License: LGPL-2.1
|
||||
|
||||
Files: ext/gstmoodbar/gstfastspectrum.cpp
|
||||
ext/gstmoodbar/gstfastspectrum.h
|
||||
Copyright: 1999 Erik Walthinsen <omega@cse.ogi.edu>
|
||||
2006,2011 Stefan Kost <ensonic@users.sf.net>
|
||||
2007-2009 Sebastian Dröge <sebastian.droege@collabora.co.uk>
|
||||
License: GPL-2+
|
||||
|
||||
Files: 3rdparty/SPMediaKeyTap/*
|
||||
Copyright: 2010, Spotify AB
|
||||
2011, Joachim Bengtsson
|
||||
|
27
ext/gstmoodbar/CMakeLists.txt
Normal file
27
ext/gstmoodbar/CMakeLists.txt
Normal file
@ -0,0 +1,27 @@
|
||||
cmake_minimum_required(VERSION 2.8.11)
|
||||
|
||||
set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -Wall")
|
||||
set(CMAKE_CXX_STANDARD 11)
|
||||
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} --std=c++11 -U__STRICT_ANSI__ -Wall -Woverloaded-virtual -Wno-sign-compare -Wno-deprecated-declarations -Wno-unused-local-typedefs -fpermissive")
|
||||
|
||||
include_directories(${CMAKE_CURRENT_BINARY_DIR} ${CMAKE_CURRENT_SOURCE_DIR})
|
||||
|
||||
include_directories(${GLIB_INCLUDE_DIRS})
|
||||
include_directories(${GOBJECT_INCLUDE_DIRS})
|
||||
include_directories(${GSTREAMER_INCLUDE_DIRS})
|
||||
include_directories(${FFTW3_INCLUDE_DIR})
|
||||
|
||||
set(SOURCES gstfastspectrum.cpp gstmoodbarplugin.cpp)
|
||||
|
||||
add_library(gstmoodbar STATIC ${SOURCES})
|
||||
|
||||
target_link_libraries(gstmoodbar
|
||||
${GOBJECT_LIBRARIES}
|
||||
${GLIB_LIBRARIES}
|
||||
${GSTREAMER_LIBRARIES}
|
||||
${GSTREAMER_AUDIO_LIBRARIES}
|
||||
${GSTREAMER_BASE_LIBRARIES}
|
||||
${FFTW3_FFTW_LIBRARY}
|
||||
)
|
||||
|
||||
target_link_libraries(gstmoodbar Qt5::Core)
|
525
ext/gstmoodbar/gstfastspectrum.cpp
Normal file
525
ext/gstmoodbar/gstfastspectrum.cpp
Normal file
@ -0,0 +1,525 @@
|
||||
/* GStreamer
|
||||
* Copyright (C) <1999> Erik Walthinsen <omega@cse.ogi.edu>
|
||||
* <2006,2011> Stefan Kost <ensonic@users.sf.net>
|
||||
* <2007-2009> Sebastian Dröge <sebastian.droege@collabora.co.uk>
|
||||
*
|
||||
* This library is free software; you can redistribute it and/or
|
||||
* modify it under the terms of the GNU Library General Public
|
||||
* License as published by the Free Software Foundation; either
|
||||
* version 2 of the License, or (at your option) any later version.
|
||||
*
|
||||
* This library is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
||||
* Library General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Library General Public
|
||||
* License along with this library; if not, write to the
|
||||
* Free Software Foundation, Inc., 51 Franklin St, Fifth Floor,
|
||||
* Boston, MA 02110-1301, USA.
|
||||
*/
|
||||
|
||||
#include <cstring>
|
||||
#include <cmath>
|
||||
|
||||
#include <QMutex>
|
||||
#include <QMutexLocker>
|
||||
|
||||
#include "gstfastspectrum.h"
|
||||
|
||||
GST_DEBUG_CATEGORY_STATIC (gst_fastspectrum_debug);
|
||||
#define GST_CAT_DEFAULT gst_fastspectrum_debug
|
||||
|
||||
/* elementfactory information */
|
||||
#if G_BYTE_ORDER == G_LITTLE_ENDIAN
|
||||
# define FORMATS "{ S16LE, S24LE, S32LE, F32LE, F64LE }"
|
||||
#else
|
||||
# define FORMATS "{ S16BE, S24BE, S32BE, F32BE, F64BE }"
|
||||
#endif
|
||||
|
||||
#define ALLOWED_CAPS \
|
||||
GST_AUDIO_CAPS_MAKE (FORMATS) ", " \
|
||||
"layout = (string) interleaved, " \
|
||||
"channels = 1"
|
||||
|
||||
/* Spectrum properties */
|
||||
#define DEFAULT_INTERVAL (GST_SECOND / 10)
|
||||
#define DEFAULT_BANDS 128
|
||||
|
||||
enum {
|
||||
PROP_0,
|
||||
PROP_INTERVAL,
|
||||
PROP_BANDS
|
||||
};
|
||||
|
||||
#define gst_fastspectrum_parent_class parent_class
|
||||
G_DEFINE_TYPE (GstFastSpectrum, gst_fastspectrum, GST_TYPE_AUDIO_FILTER);
|
||||
|
||||
static void gst_fastspectrum_finalize (GObject * object);
|
||||
static void gst_fastspectrum_set_property (GObject * object, guint prop_id, const GValue * value, GParamSpec * pspec);
|
||||
static void gst_fastspectrum_get_property (GObject * object, guint prop_id, GValue * value, GParamSpec * pspec);
|
||||
static gboolean gst_fastspectrum_start (GstBaseTransform * trans);
|
||||
static gboolean gst_fastspectrum_stop (GstBaseTransform * trans);
|
||||
static GstFlowReturn gst_fastspectrum_transform_ip (GstBaseTransform * trans, GstBuffer * in);
|
||||
static gboolean gst_fastspectrum_setup (GstAudioFilter * base, const GstAudioInfo * info);
|
||||
|
||||
static void
|
||||
gst_fastspectrum_class_init (GstFastSpectrumClass * klass)
|
||||
{
|
||||
GObjectClass *gobject_class = G_OBJECT_CLASS (klass);
|
||||
GstElementClass *element_class = GST_ELEMENT_CLASS (klass);
|
||||
GstBaseTransformClass *trans_class = GST_BASE_TRANSFORM_CLASS (klass);
|
||||
GstAudioFilterClass *filter_class = GST_AUDIO_FILTER_CLASS (klass);
|
||||
GstCaps *caps;
|
||||
|
||||
gobject_class->set_property = gst_fastspectrum_set_property;
|
||||
gobject_class->get_property = gst_fastspectrum_get_property;
|
||||
gobject_class->finalize = gst_fastspectrum_finalize;
|
||||
|
||||
trans_class->start = GST_DEBUG_FUNCPTR (gst_fastspectrum_start);
|
||||
trans_class->stop = GST_DEBUG_FUNCPTR (gst_fastspectrum_stop);
|
||||
trans_class->transform_ip = GST_DEBUG_FUNCPTR (gst_fastspectrum_transform_ip);
|
||||
trans_class->passthrough_on_same_caps = TRUE;
|
||||
|
||||
filter_class->setup = GST_DEBUG_FUNCPTR (gst_fastspectrum_setup);
|
||||
|
||||
g_object_class_install_property (gobject_class, PROP_INTERVAL,
|
||||
g_param_spec_uint64 ("interval", "Interval", "Interval of time between message posts (in nanoseconds)", 1, G_MAXUINT64, DEFAULT_INTERVAL, GParamFlags(G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS)));
|
||||
|
||||
g_object_class_install_property (gobject_class, PROP_BANDS, g_param_spec_uint ("bands", "Bands", "Number of frequency bands", 0, G_MAXUINT, DEFAULT_BANDS, GParamFlags(G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS)));
|
||||
|
||||
GST_DEBUG_CATEGORY_INIT (gst_fastspectrum_debug, "spectrum", 0,
|
||||
"audio spectrum analyser element");
|
||||
|
||||
gst_element_class_set_static_metadata (element_class, "Spectrum analyzer",
|
||||
"Filter/Analyzer/Audio",
|
||||
"Run an FFT on the audio signal, output spectrum data",
|
||||
"Erik Walthinsen <omega@cse.ogi.edu>, "
|
||||
"Stefan Kost <ensonic@users.sf.net>, "
|
||||
"Sebastian Dröge <sebastian.droege@collabora.co.uk>");
|
||||
|
||||
caps = gst_caps_from_string (ALLOWED_CAPS);
|
||||
gst_audio_filter_class_add_pad_templates (filter_class, caps);
|
||||
gst_caps_unref (caps);
|
||||
|
||||
klass->fftw_lock = new QMutex;
|
||||
}
|
||||
|
||||
static void gst_fastspectrum_init (GstFastSpectrum * spectrum) {
|
||||
|
||||
spectrum->interval = DEFAULT_INTERVAL;
|
||||
spectrum->bands = DEFAULT_BANDS;
|
||||
|
||||
spectrum->channel_data_initialised = false;
|
||||
|
||||
g_mutex_init (&spectrum->lock);
|
||||
|
||||
}
|
||||
|
||||
static void gst_fastspectrum_alloc_channel_data (GstFastSpectrum * spectrum) {
|
||||
|
||||
guint bands = spectrum->bands;
|
||||
guint nfft = 2 * bands - 2;
|
||||
|
||||
spectrum->input_ring_buffer = new double[nfft];
|
||||
spectrum->fft_input = reinterpret_cast<double*>( fftw_malloc(sizeof(double) * nfft));
|
||||
spectrum->fft_output =reinterpret_cast<fftw_complex*>( fftw_malloc(sizeof(fftw_complex) * (nfft/2+1)));
|
||||
|
||||
spectrum->spect_magnitude = new double[bands]{};
|
||||
|
||||
GstFastSpectrumClass* klass = reinterpret_cast<GstFastSpectrumClass*>(G_OBJECT_GET_CLASS(spectrum));
|
||||
{
|
||||
QMutexLocker l(klass->fftw_lock);
|
||||
spectrum->plan = fftw_plan_dft_r2c_1d(nfft, spectrum->fft_input, spectrum->fft_output, FFTW_ESTIMATE);
|
||||
}
|
||||
spectrum->channel_data_initialised = true;
|
||||
|
||||
}
|
||||
|
||||
static void gst_fastspectrum_free_channel_data (GstFastSpectrum * spectrum) {
|
||||
|
||||
GstFastSpectrumClass* klass = reinterpret_cast<GstFastSpectrumClass*>(G_OBJECT_GET_CLASS(spectrum));
|
||||
if (spectrum->channel_data_initialised) {
|
||||
{
|
||||
QMutexLocker l(klass->fftw_lock);
|
||||
fftw_destroy_plan(spectrum->plan);
|
||||
}
|
||||
fftw_free(spectrum->fft_input);
|
||||
fftw_free(spectrum->fft_output);
|
||||
delete[] spectrum->input_ring_buffer;
|
||||
delete[] spectrum->spect_magnitude;
|
||||
|
||||
spectrum->channel_data_initialised = false;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
static void gst_fastspectrum_flush (GstFastSpectrum * spectrum) {
|
||||
|
||||
spectrum->num_frames = 0;
|
||||
spectrum->num_fft = 0;
|
||||
|
||||
spectrum->accumulated_error = 0;
|
||||
|
||||
}
|
||||
|
||||
static void gst_fastspectrum_reset_state (GstFastSpectrum * spectrum) {
|
||||
|
||||
GST_DEBUG_OBJECT (spectrum, "resetting state");
|
||||
|
||||
gst_fastspectrum_free_channel_data (spectrum);
|
||||
gst_fastspectrum_flush (spectrum);
|
||||
|
||||
}
|
||||
|
||||
static void gst_fastspectrum_finalize (GObject * object) {
|
||||
|
||||
GstFastSpectrum *spectrum = GST_FASTSPECTRUM (object);
|
||||
|
||||
gst_fastspectrum_reset_state (spectrum);
|
||||
g_mutex_clear (&spectrum->lock);
|
||||
|
||||
G_OBJECT_CLASS (parent_class)->finalize (object);
|
||||
|
||||
}
|
||||
|
||||
static void gst_fastspectrum_set_property (GObject * object, guint prop_id, const GValue * value, GParamSpec * pspec) {
|
||||
|
||||
GstFastSpectrum *filter = GST_FASTSPECTRUM (object);
|
||||
|
||||
switch (prop_id) {
|
||||
case PROP_INTERVAL:{
|
||||
guint64 interval = g_value_get_uint64 (value);
|
||||
g_mutex_lock (&filter->lock);
|
||||
if (filter->interval != interval) {
|
||||
filter->interval = interval;
|
||||
gst_fastspectrum_reset_state (filter);
|
||||
}
|
||||
g_mutex_unlock (&filter->lock);
|
||||
break;
|
||||
}
|
||||
case PROP_BANDS:{
|
||||
guint bands = g_value_get_uint (value);
|
||||
g_mutex_lock (&filter->lock);
|
||||
if (filter->bands != bands) {
|
||||
filter->bands = bands;
|
||||
gst_fastspectrum_reset_state (filter);
|
||||
}
|
||||
g_mutex_unlock (&filter->lock);
|
||||
break;
|
||||
}
|
||||
default:
|
||||
G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
|
||||
break;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
static void gst_fastspectrum_get_property (GObject * object, guint prop_id, GValue * value, GParamSpec * pspec) {
|
||||
|
||||
GstFastSpectrum *filter = GST_FASTSPECTRUM (object);
|
||||
|
||||
switch (prop_id) {
|
||||
case PROP_INTERVAL:
|
||||
g_value_set_uint64 (value, filter->interval);
|
||||
break;
|
||||
case PROP_BANDS:
|
||||
g_value_set_uint (value, filter->bands);
|
||||
break;
|
||||
default:
|
||||
G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
|
||||
break;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
static gboolean gst_fastspectrum_start (GstBaseTransform * trans) {
|
||||
|
||||
GstFastSpectrum *spectrum = GST_FASTSPECTRUM (trans);
|
||||
|
||||
gst_fastspectrum_reset_state (spectrum);
|
||||
|
||||
return TRUE;
|
||||
|
||||
}
|
||||
|
||||
static gboolean gst_fastspectrum_stop (GstBaseTransform * trans) {
|
||||
|
||||
GstFastSpectrum *spectrum = GST_FASTSPECTRUM (trans);
|
||||
|
||||
gst_fastspectrum_reset_state (spectrum);
|
||||
|
||||
return TRUE;
|
||||
|
||||
}
|
||||
|
||||
/* mixing data readers */
|
||||
|
||||
static void input_data_mixed_float(const guint8* _in, double* out, guint len, double max_value, guint op, guint nfft) {
|
||||
|
||||
guint j, ip = 0;
|
||||
gfloat *in = (gfloat *) _in;
|
||||
|
||||
for (j = 0; j < len; j++) {
|
||||
out[op] = in[ip++];
|
||||
op = (op + 1) % nfft;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
static void input_data_mixed_double (const guint8 * _in, double* out, guint len, double max_value, guint op, guint nfft) {
|
||||
|
||||
guint j, ip = 0;
|
||||
gdouble *in = (gdouble *) _in;
|
||||
|
||||
for (j = 0; j < len; j++) {
|
||||
out[op] = in[ip++];
|
||||
op = (op + 1) % nfft;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
static void input_data_mixed_int32_max (const guint8 * _in, double* out, guint len, double max_value, guint op, guint nfft) {
|
||||
|
||||
guint j, ip = 0;
|
||||
gint32 *in = (gint32 *) _in;
|
||||
|
||||
for (j = 0; j < len; j++) {
|
||||
out[op] = in[ip++] / max_value;
|
||||
op = (op + 1) % nfft;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
static void input_data_mixed_int24_max (const guint8 * _in, double* out, guint len, double max_value, guint op, guint nfft) {
|
||||
|
||||
guint j;
|
||||
|
||||
for (j = 0; j < len; j++) {
|
||||
#if G_BYTE_ORDER == G_BIG_ENDIAN
|
||||
gint32 value = GST_READ_UINT24_BE (_in);
|
||||
#else
|
||||
gint32 value = GST_READ_UINT24_LE (_in);
|
||||
#endif
|
||||
if (value & 0x00800000)
|
||||
value |= 0xff000000;
|
||||
|
||||
out[op] = value / max_value;
|
||||
op = (op + 1) % nfft;
|
||||
_in += 3;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
static void input_data_mixed_int16_max (const guint8 * _in, double * out, guint len, double max_value, guint op, guint nfft) {
|
||||
|
||||
guint j, ip = 0;
|
||||
gint16 *in = (gint16 *) _in;
|
||||
|
||||
for (j = 0; j < len; j++) {
|
||||
out[op] = in[ip++] / max_value;
|
||||
op = (op + 1) % nfft;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
static gboolean gst_fastspectrum_setup (GstAudioFilter * base, const GstAudioInfo * info) {
|
||||
|
||||
GstFastSpectrum *spectrum = GST_FASTSPECTRUM (base);
|
||||
GstFastSpectrumInputData input_data = NULL;
|
||||
|
||||
g_mutex_lock (&spectrum->lock);
|
||||
switch (GST_AUDIO_INFO_FORMAT (info)) {
|
||||
case GST_AUDIO_FORMAT_S16:
|
||||
input_data = input_data_mixed_int16_max;
|
||||
break;
|
||||
case GST_AUDIO_FORMAT_S24:
|
||||
input_data = input_data_mixed_int24_max;
|
||||
break;
|
||||
case GST_AUDIO_FORMAT_S32:
|
||||
input_data = input_data_mixed_int32_max;
|
||||
break;
|
||||
case GST_AUDIO_FORMAT_F32:
|
||||
input_data = input_data_mixed_float;
|
||||
break;
|
||||
case GST_AUDIO_FORMAT_F64:
|
||||
input_data = input_data_mixed_double;
|
||||
break;
|
||||
default:
|
||||
g_assert_not_reached ();
|
||||
break;
|
||||
}
|
||||
spectrum->input_data = input_data;
|
||||
|
||||
gst_fastspectrum_reset_state (spectrum);
|
||||
g_mutex_unlock (&spectrum->lock);
|
||||
|
||||
return TRUE;
|
||||
|
||||
}
|
||||
|
||||
static void gst_fastspectrum_run_fft (GstFastSpectrum * spectrum, guint input_pos) {
|
||||
|
||||
guint i;
|
||||
guint bands = spectrum->bands;
|
||||
guint nfft = 2 * bands - 2;
|
||||
|
||||
for (i = 0; i < nfft; i++)
|
||||
spectrum->fft_input[i] =
|
||||
spectrum->input_ring_buffer[(input_pos + i) % nfft];
|
||||
|
||||
// Should be safe to execute the same plan multiple times in parallel.
|
||||
fftw_execute(spectrum->plan);
|
||||
|
||||
gdouble val;
|
||||
/* Calculate magnitude in db */
|
||||
for (i = 0; i < bands; i++) {
|
||||
val = spectrum->fft_output[i][0] * spectrum->fft_output[i][0];
|
||||
val += spectrum->fft_output[i][1] * spectrum->fft_output[i][1];
|
||||
val /= nfft * nfft;
|
||||
spectrum->spect_magnitude[i] += val;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
static GstFlowReturn gst_fastspectrum_transform_ip (GstBaseTransform * trans, GstBuffer * buffer) {
|
||||
|
||||
GstFastSpectrum *spectrum = GST_FASTSPECTRUM (trans);
|
||||
guint rate = GST_AUDIO_FILTER_RATE (spectrum);
|
||||
guint bps = GST_AUDIO_FILTER_BPS (spectrum);
|
||||
guint bpf = GST_AUDIO_FILTER_BPF (spectrum);
|
||||
double max_value = (1UL << ((bps << 3) - 1)) - 1;
|
||||
guint bands = spectrum->bands;
|
||||
guint nfft = 2 * bands - 2;
|
||||
guint input_pos;
|
||||
GstMapInfo map;
|
||||
const guint8 *data;
|
||||
gsize size;
|
||||
guint fft_todo, msg_todo, block_size;
|
||||
gboolean have_full_interval;
|
||||
GstFastSpectrumInputData input_data;
|
||||
|
||||
g_mutex_lock (&spectrum->lock);
|
||||
gst_buffer_map (buffer, &map, GST_MAP_READ);
|
||||
data = map.data;
|
||||
size = map.size;
|
||||
|
||||
GST_LOG_OBJECT (spectrum, "input size: %" G_GSIZE_FORMAT " bytes", size);
|
||||
|
||||
if (GST_BUFFER_IS_DISCONT (buffer)) {
|
||||
GST_DEBUG_OBJECT (spectrum, "Discontinuity detected -- flushing");
|
||||
gst_fastspectrum_flush (spectrum);
|
||||
}
|
||||
|
||||
/* If we don't have a FFT context yet (or it was reset due to parameter
|
||||
* changes) get one and allocate memory for everything
|
||||
*/
|
||||
if (!spectrum->channel_data_initialised) {
|
||||
GST_DEBUG_OBJECT (spectrum, "allocating for bands %u", bands);
|
||||
|
||||
gst_fastspectrum_alloc_channel_data (spectrum);
|
||||
|
||||
/* number of sample frames we process before posting a message
|
||||
* interval is in ns */
|
||||
spectrum->frames_per_interval = gst_util_uint64_scale (spectrum->interval, rate, GST_SECOND);
|
||||
spectrum->frames_todo = spectrum->frames_per_interval;
|
||||
/* rounding error for frames_per_interval in ns,
|
||||
* aggregated it in accumulated_error */
|
||||
spectrum->error_per_interval = (spectrum->interval * rate) % GST_SECOND;
|
||||
if (spectrum->frames_per_interval == 0)
|
||||
spectrum->frames_per_interval = 1;
|
||||
|
||||
GST_INFO_OBJECT (spectrum, "interval %" GST_TIME_FORMAT ", fpi %"
|
||||
G_GUINT64_FORMAT ", error %" GST_TIME_FORMAT,
|
||||
GST_TIME_ARGS (spectrum->interval), spectrum->frames_per_interval,
|
||||
GST_TIME_ARGS (spectrum->error_per_interval));
|
||||
|
||||
spectrum->input_pos = 0;
|
||||
|
||||
gst_fastspectrum_flush (spectrum);
|
||||
}
|
||||
|
||||
if (spectrum->num_frames == 0)
|
||||
spectrum->message_ts = GST_BUFFER_TIMESTAMP (buffer);
|
||||
|
||||
input_pos = spectrum->input_pos;
|
||||
input_data = spectrum->input_data;
|
||||
|
||||
while (size >= bpf) {
|
||||
/* run input_data for a chunk of data */
|
||||
fft_todo = nfft - (spectrum->num_frames % nfft);
|
||||
msg_todo = spectrum->frames_todo - spectrum->num_frames;
|
||||
GST_LOG_OBJECT (spectrum,
|
||||
"message frames todo: %u, fft frames todo: %u, input frames %"
|
||||
G_GSIZE_FORMAT, msg_todo, fft_todo, (size / bpf));
|
||||
block_size = msg_todo;
|
||||
if (block_size > (size / bpf))
|
||||
block_size = (size / bpf);
|
||||
if (block_size > fft_todo)
|
||||
block_size = fft_todo;
|
||||
|
||||
/* Move the current frames into our ringbuffers */
|
||||
input_data(data, spectrum->input_ring_buffer, block_size, max_value, input_pos, nfft);
|
||||
|
||||
data += block_size * bpf;
|
||||
size -= block_size * bpf;
|
||||
input_pos = (input_pos + block_size) % nfft;
|
||||
spectrum->num_frames += block_size;
|
||||
|
||||
have_full_interval = (spectrum->num_frames == spectrum->frames_todo);
|
||||
|
||||
GST_LOG_OBJECT (spectrum,
|
||||
"size: %" G_GSIZE_FORMAT ", do-fft = %d, do-message = %d", size,
|
||||
(spectrum->num_frames % nfft == 0), have_full_interval);
|
||||
|
||||
/* If we have enough frames for an FFT or we have all frames required for
|
||||
* the interval and we haven't run a FFT, then run an FFT */
|
||||
if ((spectrum->num_frames % nfft == 0) || (have_full_interval && !spectrum->num_fft)) {
|
||||
gst_fastspectrum_run_fft (spectrum, input_pos);
|
||||
spectrum->num_fft++;
|
||||
}
|
||||
|
||||
/* Do we have the FFTs for one interval? */
|
||||
if (have_full_interval) {
|
||||
GST_DEBUG_OBJECT (spectrum, "nfft: %u frames: %" G_GUINT64_FORMAT
|
||||
" fpi: %" G_GUINT64_FORMAT " error: %" GST_TIME_FORMAT, nfft,
|
||||
spectrum->num_frames, spectrum->frames_per_interval,
|
||||
GST_TIME_ARGS (spectrum->accumulated_error));
|
||||
|
||||
spectrum->frames_todo = spectrum->frames_per_interval;
|
||||
if (spectrum->accumulated_error >= GST_SECOND) {
|
||||
spectrum->accumulated_error -= GST_SECOND;
|
||||
spectrum->frames_todo++;
|
||||
}
|
||||
spectrum->accumulated_error += spectrum->error_per_interval;
|
||||
|
||||
if (spectrum->output_callback) {
|
||||
// Calculate average
|
||||
for (guint i = 0; i < spectrum->bands; i++) {
|
||||
spectrum->spect_magnitude[i] /= spectrum->num_fft;
|
||||
}
|
||||
|
||||
spectrum->output_callback(spectrum->spect_magnitude, spectrum->bands);
|
||||
|
||||
// Reset spectrum accumulators
|
||||
memset(spectrum->spect_magnitude, 0, spectrum->bands * sizeof(double));
|
||||
}
|
||||
|
||||
if (GST_CLOCK_TIME_IS_VALID (spectrum->message_ts))
|
||||
spectrum->message_ts += gst_util_uint64_scale (spectrum->num_frames, GST_SECOND, rate);
|
||||
|
||||
spectrum->num_frames = 0;
|
||||
spectrum->num_fft = 0;
|
||||
}
|
||||
}
|
||||
|
||||
spectrum->input_pos = input_pos;
|
||||
|
||||
gst_buffer_unmap (buffer, &map);
|
||||
g_mutex_unlock (&spectrum->lock);
|
||||
|
||||
g_assert (size == 0);
|
||||
|
||||
return GST_FLOW_OK;
|
||||
|
||||
}
|
98
ext/gstmoodbar/gstfastspectrum.h
Normal file
98
ext/gstmoodbar/gstfastspectrum.h
Normal file
@ -0,0 +1,98 @@
|
||||
/* GStreamer
|
||||
* Copyright (C) <1999> Erik Walthinsen <omega@cse.ogi.edu>
|
||||
* Copyright (C) <2009> Sebastian Dröge <sebastian.droege@collabora.co.uk>
|
||||
*
|
||||
* This library is free software; you can redistribute it and/or
|
||||
* modify it under the terms of the GNU Library General Public
|
||||
* License as published by the Free Software Foundation; either
|
||||
* version 2 of the License, or (at your option) any later version.
|
||||
*
|
||||
* This library is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
||||
* Library General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Library General Public
|
||||
* License along with this library; if not, write to the
|
||||
* Free Software Foundation, Inc., 51 Franklin St, Fifth Floor,
|
||||
* Boston, MA 02110-1301, USA.
|
||||
*/
|
||||
|
||||
// Adapted from gstspectrum for Clementine with the following changes:
|
||||
// - Uses fftw instead of kiss fft (2x faster).
|
||||
// - Hardcoded to 1 channel (use an audioconvert element to do the work
|
||||
// instead, simplifies this code a lot).
|
||||
// - Send output via a callback instead of GST messages (less overhead).
|
||||
// - Removed all properties except interval and band.
|
||||
|
||||
|
||||
#ifndef GST_MOODBAR_FASTSPECTRUM_H_
|
||||
#define GST_MOODBAR_FASTSPECTRUM_H_
|
||||
|
||||
#include <functional>
|
||||
|
||||
#include <gst/gst.h>
|
||||
#include <gst/audio/gstaudiofilter.h>
|
||||
#include <fftw3.h>
|
||||
|
||||
G_BEGIN_DECLS
|
||||
|
||||
#define GST_TYPE_FASTSPECTRUM (gst_fastspectrum_get_type())
|
||||
#define GST_FASTSPECTRUM(obj) (G_TYPE_CHECK_INSTANCE_CAST((obj),GST_TYPE_FASTSPECTRUM,GstFastSpectrum))
|
||||
#define GST_IS_FASTSPECTRUM(obj) (G_TYPE_CHECK_INSTANCE_TYPE((obj),GST_TYPE_FASTSPECTRUM))
|
||||
#define GST_FASTSPECTRUM_CLASS(klass) (G_TYPE_CHECK_CLASS_CAST((klass), GST_TYPE_FASTSPECTRUM,GstFastSpectrumClass))
|
||||
#define GST_IS_FASTSPECTRUM_CLASS(klass) (G_TYPE_CHECK_CLASS_TYPE((klass), GST_TYPE_FASTSPECTRUM))
|
||||
|
||||
class QMutex;
|
||||
|
||||
typedef void (*GstFastSpectrumInputData)(const guint8* in, double* out,
|
||||
guint len, double max_value, guint op, guint nfft);
|
||||
|
||||
typedef std::function<void(double* magnitudes, int size)> OutputCallback;
|
||||
|
||||
struct GstFastSpectrum {
|
||||
GstAudioFilter parent;
|
||||
|
||||
/* properties */
|
||||
guint64 interval; /* how many nanoseconds between emits */
|
||||
guint64 frames_per_interval; /* how many frames per interval */
|
||||
guint64 frames_todo;
|
||||
guint bands; /* number of spectrum bands */
|
||||
gboolean multi_channel; /* send separate channel results */
|
||||
|
||||
guint64 num_frames; /* frame count (1 sample per channel)
|
||||
* since last emit */
|
||||
guint64 num_fft; /* number of FFTs since last emit */
|
||||
GstClockTime message_ts; /* starttime for next message */
|
||||
|
||||
/* <private> */
|
||||
bool channel_data_initialised;
|
||||
double* input_ring_buffer;
|
||||
double* fft_input;
|
||||
fftw_complex* fft_output;
|
||||
double* spect_magnitude;
|
||||
fftw_plan plan;
|
||||
|
||||
guint input_pos;
|
||||
guint64 error_per_interval;
|
||||
guint64 accumulated_error;
|
||||
|
||||
GMutex lock;
|
||||
|
||||
GstFastSpectrumInputData input_data;
|
||||
|
||||
OutputCallback output_callback;
|
||||
};
|
||||
|
||||
struct GstFastSpectrumClass {
|
||||
GstAudioFilterClass parent_class;
|
||||
|
||||
// Static lock for creating & destroying FFTW plans.
|
||||
QMutex* fftw_lock;
|
||||
};
|
||||
|
||||
GType gst_fastspectrum_get_type (void);
|
||||
|
||||
G_END_DECLS
|
||||
|
||||
#endif // GST_MOODBAR_FASTSPECTRUM_H_
|
50
ext/gstmoodbar/gstmoodbarplugin.cpp
Normal file
50
ext/gstmoodbar/gstmoodbarplugin.cpp
Normal file
@ -0,0 +1,50 @@
|
||||
/* This file was part of Clementine.
|
||||
Copyright 2014, David Sansome <me@davidsansome.com>
|
||||
|
||||
Strawberry is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
Strawberry is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with Strawberry. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#include <gst/gst.h>
|
||||
|
||||
#include "gstfastspectrum.h"
|
||||
#include "gstmoodbarplugin.h"
|
||||
|
||||
namespace {
|
||||
|
||||
static gboolean gst_moodbar_plugin_init(GstPlugin* plugin) {
|
||||
|
||||
if (!gst_element_register(plugin, "fastspectrum", GST_RANK_NONE, GST_TYPE_FASTSPECTRUM)) {
|
||||
return FALSE;
|
||||
}
|
||||
|
||||
return TRUE;
|
||||
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
int gstfastspectrum_register_static() {
|
||||
|
||||
return gst_plugin_register_static(
|
||||
GST_VERSION_MAJOR,
|
||||
GST_VERSION_MINOR,
|
||||
"fastspectrum",
|
||||
"Fast spectrum analyzer for generating Moodbars",
|
||||
gst_moodbar_plugin_init,
|
||||
"0.1",
|
||||
"GPL",
|
||||
"FastSpectrum",
|
||||
"FastSpectrum",
|
||||
"https://www.strawbs.org");
|
||||
}
|
25
ext/gstmoodbar/gstmoodbarplugin.h
Normal file
25
ext/gstmoodbar/gstmoodbarplugin.h
Normal file
@ -0,0 +1,25 @@
|
||||
/* This file was part of Clementine.
|
||||
Copyright 2014, David Sansome <me@davidsansome.com>
|
||||
|
||||
Strawberry is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
Strawberry is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with Strawberry. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#ifndef GST_MOODBAR_PLUGIN_H
|
||||
#define GST_MOODBAR_PLUGIN_H
|
||||
|
||||
extern "C" {
|
||||
int gstfastspectrum_register_static();
|
||||
}
|
||||
|
||||
#endif // GST_MOODBAR_PLUGIN_H
|
@ -879,6 +879,28 @@ optional_source(HAVE_TIDAL
|
||||
settings/tidalsettingspage.ui
|
||||
)
|
||||
|
||||
# Moodbar
|
||||
optional_source(HAVE_MOODBAR
|
||||
SOURCES
|
||||
moodbar/moodbarbuilder.cpp
|
||||
moodbar/moodbarcontroller.cpp
|
||||
moodbar/moodbaritemdelegate.cpp
|
||||
moodbar/moodbarloader.cpp
|
||||
moodbar/moodbarpipeline.cpp
|
||||
moodbar/moodbarproxystyle.cpp
|
||||
moodbar/moodbarrenderer.cpp
|
||||
settings/moodbarsettingspage.cpp
|
||||
HEADERS
|
||||
moodbar/moodbarcontroller.h
|
||||
moodbar/moodbaritemdelegate.h
|
||||
moodbar/moodbarloader.h
|
||||
moodbar/moodbarpipeline.h
|
||||
moodbar/moodbarproxystyle.h
|
||||
settings/moodbarsettingspage.h
|
||||
UI
|
||||
settings/moodbarsettingspage.ui
|
||||
)
|
||||
|
||||
configure_file(${CMAKE_CURRENT_SOURCE_DIR}/config.h.in ${CMAKE_CURRENT_BINARY_DIR}/config.h)
|
||||
configure_file(${CMAKE_CURRENT_SOURCE_DIR}/version.h.in ${CMAKE_CURRENT_BINARY_DIR}/version.h)
|
||||
|
||||
@ -1003,6 +1025,10 @@ if(HAVE_LIBPULSE)
|
||||
target_link_libraries(strawberry_lib ${LIBPULSE_LIBRARIES})
|
||||
endif()
|
||||
|
||||
if(HAVE_MOODBAR)
|
||||
target_link_libraries(strawberry_lib gstmoodbar)
|
||||
endif()
|
||||
|
||||
if (APPLE)
|
||||
target_link_libraries(strawberry_lib
|
||||
"-framework AppKit"
|
||||
@ -1025,7 +1051,6 @@ if (WIN32)
|
||||
target_link_libraries(strawberry_lib
|
||||
${ZLIB_LIBRARIES}
|
||||
dsound
|
||||
${QT_QTGUI_LIBRARY}
|
||||
)
|
||||
endif (WIN32)
|
||||
|
||||
|
@ -50,6 +50,8 @@
|
||||
|
||||
#cmakedefine HAVE_TIDAL
|
||||
|
||||
#cmakedefine HAVE_MOODBAR
|
||||
|
||||
#cmakedefine HAVE_KEYSYMDEF_H
|
||||
#cmakedefine HAVE_XF86KEYSYM_H
|
||||
|
||||
|
@ -61,6 +61,8 @@
|
||||
#include "lyrics/auddlyricsprovider.h"
|
||||
#include "lyrics/chartlyricsprovider.h"
|
||||
|
||||
#include "scrobbler/audioscrobbler.h"
|
||||
|
||||
#include "internet/internetservices.h"
|
||||
#include "internet/internetsearch.h"
|
||||
|
||||
@ -69,7 +71,10 @@
|
||||
# include "covermanager/tidalcoverprovider.h"
|
||||
#endif
|
||||
|
||||
#include "scrobbler/audioscrobbler.h"
|
||||
#ifdef HAVE_MOODBAR
|
||||
# include "moodbar/moodbarcontroller.h"
|
||||
# include "moodbar/moodbarloader.h"
|
||||
#endif
|
||||
|
||||
bool Application::kIsPortable = false;
|
||||
|
||||
@ -136,7 +141,14 @@ class ApplicationImpl {
|
||||
#ifdef HAVE_TIDAL
|
||||
tidal_search_([=]() { return new InternetSearch(app, Song::Source_Tidal, app); }),
|
||||
#endif
|
||||
scrobbler_([=]() { return new AudioScrobbler(app, app); })
|
||||
scrobbler_([=]() { return new AudioScrobbler(app, app); }),
|
||||
|
||||
#ifdef HAVE_MOODBAR
|
||||
moodbar_loader_([=]() { return new MoodbarLoader(app, app); }),
|
||||
moodbar_controller_([=]() { return new MoodbarController(app, app); }),
|
||||
#endif
|
||||
dummy_([=]() { return nullptr; })
|
||||
|
||||
{}
|
||||
|
||||
Lazy<TagReaderClient> tag_reader_client_;
|
||||
@ -160,6 +172,11 @@ class ApplicationImpl {
|
||||
Lazy<InternetSearch> tidal_search_;
|
||||
#endif
|
||||
Lazy<AudioScrobbler> scrobbler_;
|
||||
#ifdef HAVE_MOODBAR
|
||||
Lazy<MoodbarLoader> moodbar_loader_;
|
||||
Lazy<MoodbarController> moodbar_controller_;
|
||||
#endif
|
||||
Lazy<QVariant> dummy_;
|
||||
|
||||
};
|
||||
|
||||
@ -231,3 +248,7 @@ InternetServices *Application::internet_services() const { return p_->internet_s
|
||||
InternetSearch *Application::tidal_search() const { return p_->tidal_search_.get(); }
|
||||
#endif
|
||||
AudioScrobbler *Application::scrobbler() const { return p_->scrobbler_.get(); }
|
||||
#ifdef HAVE_MOODBAR
|
||||
MoodbarController *Application::moodbar_controller() const { return p_->moodbar_controller_.get(); }
|
||||
MoodbarLoader *Application::moodbar_loader() const { return p_->moodbar_loader_.get(); }
|
||||
#endif
|
||||
|
@ -56,9 +56,13 @@ class CoverProviders;
|
||||
class AlbumCoverLoader;
|
||||
class CurrentArtLoader;
|
||||
class LyricsProviders;
|
||||
class AudioScrobbler;
|
||||
class InternetServices;
|
||||
class InternetSearch;
|
||||
class AudioScrobbler;
|
||||
#ifdef HAVE_MOODBAR
|
||||
class MoodbarController;
|
||||
class MoodbarLoader;
|
||||
#endif
|
||||
|
||||
class Application : public QObject {
|
||||
Q_OBJECT
|
||||
@ -92,12 +96,17 @@ class Application : public QObject {
|
||||
|
||||
LyricsProviders *lyrics_providers() const;
|
||||
|
||||
AudioScrobbler *scrobbler() const;
|
||||
|
||||
InternetServices *internet_services() const;
|
||||
#ifdef HAVE_TIDAL
|
||||
InternetSearch *tidal_search() const;
|
||||
#endif
|
||||
|
||||
AudioScrobbler *scrobbler() const;
|
||||
#ifdef HAVE_MOODBAR
|
||||
MoodbarController *moodbar_controller() const;
|
||||
MoodbarLoader *moodbar_loader() const;
|
||||
#endif
|
||||
|
||||
void MoveToNewThread(QObject *object);
|
||||
void MoveToThread(QObject *object, QThread *thread);
|
||||
|
@ -151,6 +151,11 @@
|
||||
# include "core/macsystemtrayicon.h"
|
||||
#endif
|
||||
|
||||
#ifdef HAVE_MOODBAR
|
||||
# include "moodbar/moodbarcontroller.h"
|
||||
# include "moodbar/moodbarproxystyle.h"
|
||||
#endif
|
||||
|
||||
using std::bind;
|
||||
using std::floor;
|
||||
using std::stable_sort;
|
||||
@ -667,6 +672,11 @@ MainWindow::MainWindow(Application *app, SystemTrayIcon *tray_icon, OSD *osd, co
|
||||
|
||||
ui_->track_slider->SetApplication(app);
|
||||
|
||||
#ifdef HAVE_MOODBAR
|
||||
// Moodbar connections
|
||||
connect(app_->moodbar_controller(), SIGNAL(CurrentMoodbarDataChanged(QByteArray)), ui_->track_slider->moodbar_style(), SLOT(SetMoodbarData(QByteArray)));
|
||||
#endif
|
||||
|
||||
// Playing widget
|
||||
qLog(Debug) << "Creating playing widget";
|
||||
ui_->widget_playing->set_ideal_height(ui_->status_bar->sizeHint().height() + ui_->player_controls->sizeHint().height());
|
||||
|
@ -67,6 +67,10 @@
|
||||
# include "ext/gstafc/gstafcsrc.h"
|
||||
#endif
|
||||
|
||||
#ifdef HAVE_MOODBAR
|
||||
# include "ext/gstmoodbar/gstmoodbarplugin.h"
|
||||
#endif
|
||||
|
||||
#include "settings/backendsettingspage.h"
|
||||
|
||||
using std::shared_ptr;
|
||||
@ -118,6 +122,7 @@ bool GstEngine::Init() {
|
||||
SetEnvironment();
|
||||
|
||||
initialising_ = QtConcurrent::run(this, &GstEngine::InitialiseGStreamer);
|
||||
|
||||
return true;
|
||||
|
||||
}
|
||||
@ -415,6 +420,10 @@ void GstEngine::InitialiseGStreamer() {
|
||||
afcsrc_register_static();
|
||||
#endif
|
||||
|
||||
#ifdef HAVE_MOODBAR
|
||||
gstfastspectrum_register_static();
|
||||
#endif
|
||||
|
||||
}
|
||||
|
||||
void GstEngine::SetEnvironment() {
|
||||
|
199
src/moodbar/moodbarbuilder.cpp
Normal file
199
src/moodbar/moodbarbuilder.cpp
Normal file
@ -0,0 +1,199 @@
|
||||
/* This file was part of Clementine.
|
||||
Copyright 2014, David Sansome <me@davidsansome.com>
|
||||
|
||||
Strawberry is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
Strawberry is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with Strawberry. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#include <cmath>
|
||||
|
||||
#include <QList>
|
||||
#include <QByteArray>
|
||||
|
||||
#include "moodbarbuilder.h"
|
||||
#include "core/arraysize.h"
|
||||
|
||||
namespace {
|
||||
|
||||
static const int sBarkBands[] = {
|
||||
100, 200, 300, 400, 510, 630, 770, 920, 1080, 1270, 1480, 1720,
|
||||
2000, 2320, 2700, 3150, 3700, 4400, 5300, 6400, 7700, 9500, 12000, 15500};
|
||||
|
||||
static const int sBarkBandCount = arraysize(sBarkBands);
|
||||
|
||||
} // namespace
|
||||
|
||||
MoodbarBuilder::MoodbarBuilder() : bands_(0), rate_hz_(0) {}
|
||||
|
||||
int MoodbarBuilder::BandFrequency(int band) const {
|
||||
return ((rate_hz_ / 2) * band + rate_hz_ / 4) / bands_;
|
||||
}
|
||||
|
||||
void MoodbarBuilder::Init(int bands, int rate_hz) {
|
||||
|
||||
bands_ = bands;
|
||||
rate_hz_ = rate_hz;
|
||||
|
||||
barkband_table_.clear();
|
||||
barkband_table_.reserve(bands + 1);
|
||||
|
||||
int barkband = 0;
|
||||
for (int i = 0; i < bands + 1; ++i) {
|
||||
if (barkband < sBarkBandCount - 1 && BandFrequency(i) >= sBarkBands[barkband]) {
|
||||
barkband++;
|
||||
}
|
||||
|
||||
barkband_table_.append(barkband);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
void MoodbarBuilder::AddFrame(const double* magnitudes, int size) {
|
||||
|
||||
if (size > barkband_table_.length()) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Calculate total magnitudes for different bark bands.
|
||||
double bands[sBarkBandCount];
|
||||
for (int i = 0; i < sBarkBandCount; ++i) {
|
||||
bands[i] = 0.0;
|
||||
}
|
||||
|
||||
for (int i = 0; i < size; ++i) {
|
||||
bands[barkband_table_[i]] += magnitudes[i];
|
||||
}
|
||||
|
||||
// Now divide the bark bands into thirds and compute their total amplitudes.
|
||||
double rgb[] = {0, 0, 0};
|
||||
for (int i = 0; i < sBarkBandCount; ++i) {
|
||||
rgb[(i * 3) / sBarkBandCount] += bands[i] * bands[i];
|
||||
}
|
||||
|
||||
frames_.append(Rgb(sqrt(rgb[0]), sqrt(rgb[1]), sqrt(rgb[2])));
|
||||
|
||||
}
|
||||
|
||||
void MoodbarBuilder::Normalize(QList<Rgb>* vals, double Rgb::*member) {
|
||||
|
||||
double mini = vals->at(0).*member;
|
||||
double maxi = vals->at(0).*member;
|
||||
for (int i = 1; i < vals->count(); i++) {
|
||||
const double value = vals->at(i).*member;
|
||||
if (value > maxi) {
|
||||
maxi = value;
|
||||
}
|
||||
else if (value < mini) {
|
||||
mini = value;
|
||||
}
|
||||
}
|
||||
|
||||
double avg = 0;
|
||||
int t = 0;
|
||||
for (const Rgb& rgb : *vals) {
|
||||
const double value = rgb.*member;
|
||||
if (value != mini && value != maxi) {
|
||||
avg += value / vals->count();
|
||||
t++;
|
||||
}
|
||||
}
|
||||
|
||||
double tu = 0;
|
||||
double tb = 0;
|
||||
double avgu = 0;
|
||||
double avgb = 0;
|
||||
for (const Rgb& rgb : *vals) {
|
||||
const double value = rgb.*member;
|
||||
if (value != mini && value != maxi) {
|
||||
if (value > avg) {
|
||||
avgu += value;
|
||||
tu++;
|
||||
}
|
||||
else {
|
||||
avgb += value;
|
||||
tb++;
|
||||
}
|
||||
}
|
||||
}
|
||||
avgu /= tu;
|
||||
avgb /= tb;
|
||||
|
||||
tu = 0;
|
||||
tb = 0;
|
||||
double avguu = 0;
|
||||
double avgbb = 0;
|
||||
for (const Rgb& rgb : *vals) {
|
||||
const double value = rgb.*member;
|
||||
if (value != mini && value != maxi) {
|
||||
if (value > avgu) {
|
||||
avguu += value;
|
||||
tu++;
|
||||
}
|
||||
else if (value < avgb) {
|
||||
avgbb += value;
|
||||
tb++;
|
||||
}
|
||||
}
|
||||
}
|
||||
avguu /= tu;
|
||||
avgbb /= tb;
|
||||
|
||||
mini = std::max(avg + (avgb - avg) * 2, avgbb);
|
||||
maxi = std::min(avg + (avgu - avg) * 2, avguu);
|
||||
double delta = maxi - mini;
|
||||
if (delta == 0) {
|
||||
delta = 1;
|
||||
}
|
||||
|
||||
for (auto it = vals->begin(); it != vals->end(); ++it) {
|
||||
double* value = &((*it).*member);
|
||||
*value = std::isfinite(*value) ? qBound(0.0, (*value - mini) / delta, 1.0) : 0;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
QByteArray MoodbarBuilder::Finish(int width) {
|
||||
|
||||
QByteArray ret;
|
||||
ret.resize(width * 3);
|
||||
char* data = ret.data();
|
||||
if (frames_.count() == 0) return ret;
|
||||
|
||||
Normalize(&frames_, &Rgb::r);
|
||||
Normalize(&frames_, &Rgb::g);
|
||||
Normalize(&frames_, &Rgb::b);
|
||||
|
||||
for (int i = 0; i < width; ++i) {
|
||||
Rgb rgb;
|
||||
int start = i * frames_.count() / width;
|
||||
int end = (i + 1) * frames_.count() / width;
|
||||
if (start == end) {
|
||||
end = start + 1;
|
||||
}
|
||||
|
||||
for (int j = start; j < end; j++) {
|
||||
const Rgb& frame = frames_[j];
|
||||
rgb.r += frame.r * 255;
|
||||
rgb.g += frame.g * 255;
|
||||
rgb.b += frame.b * 255;
|
||||
}
|
||||
|
||||
const int n = end - start;
|
||||
|
||||
*(data++) = rgb.r / n;
|
||||
*(data++) = rgb.g / n;
|
||||
*(data++) = rgb.b / n;
|
||||
}
|
||||
return ret;
|
||||
|
||||
}
|
50
src/moodbar/moodbarbuilder.h
Normal file
50
src/moodbar/moodbarbuilder.h
Normal file
@ -0,0 +1,50 @@
|
||||
/* This file was part of Clementine.
|
||||
Copyright 2014, David Sansome <me@davidsansome.com>
|
||||
|
||||
Strawberry is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
Strawberry is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with Strawberry. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#ifndef MOODBARBUILDER_H
|
||||
#define MOODBARBUILDER_H
|
||||
|
||||
#include <QList>
|
||||
#include <QByteArray>
|
||||
|
||||
class MoodbarBuilder {
|
||||
public:
|
||||
MoodbarBuilder();
|
||||
|
||||
void Init(int bands, int rate_hz);
|
||||
void AddFrame(const double* magnitudes, int size);
|
||||
QByteArray Finish(int width);
|
||||
|
||||
private:
|
||||
struct Rgb {
|
||||
Rgb() : r(0), g(0), b(0) {}
|
||||
Rgb(double r_, double g_, double b_) : r(r_), g(g_), b(b_) {}
|
||||
|
||||
double r, g, b;
|
||||
};
|
||||
|
||||
int BandFrequency(int band) const;
|
||||
static void Normalize(QList<Rgb>* vals, double Rgb::*member);
|
||||
|
||||
QList<uint> barkband_table_;
|
||||
int bands_;
|
||||
int rate_hz_;
|
||||
|
||||
QList<Rgb> frames_;
|
||||
};
|
||||
|
||||
#endif // MOODBARBUILDER_H
|
91
src/moodbar/moodbarcontroller.cpp
Normal file
91
src/moodbar/moodbarcontroller.cpp
Normal file
@ -0,0 +1,91 @@
|
||||
/* This file was part of Clementine.
|
||||
Copyright 2012, David Sansome <me@davidsansome.com>
|
||||
|
||||
Strawberry is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
Strawberry is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with Strawberry. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#include <QObject>
|
||||
#include <QByteArray>
|
||||
#include <QUrl>
|
||||
|
||||
#include "core/application.h"
|
||||
#include "core/closure.h"
|
||||
#include "core/logging.h"
|
||||
#include "core/player.h"
|
||||
#include "playlist/playlistmanager.h"
|
||||
|
||||
#include "moodbarcontroller.h"
|
||||
#include "moodbarloader.h"
|
||||
#include "moodbarpipeline.h"
|
||||
|
||||
MoodbarController::MoodbarController(Application* app, QObject* parent)
|
||||
: QObject(parent),
|
||||
app_(app) {
|
||||
|
||||
connect(app_->playlist_manager(), SIGNAL(CurrentSongChanged(Song)), SLOT(CurrentSongChanged(Song)));
|
||||
connect(app_->player(), SIGNAL(Stopped()), SLOT(PlaybackStopped()));
|
||||
|
||||
}
|
||||
|
||||
void MoodbarController::CurrentSongChanged(const Song& song) {
|
||||
|
||||
QByteArray data;
|
||||
MoodbarPipeline* pipeline = nullptr;
|
||||
const MoodbarLoader::Result result = app_->moodbar_loader()->Load(song.url(), &data, &pipeline);
|
||||
|
||||
switch (result) {
|
||||
case MoodbarLoader::CannotLoad:
|
||||
emit CurrentMoodbarDataChanged(QByteArray());
|
||||
break;
|
||||
|
||||
case MoodbarLoader::Loaded:
|
||||
emit CurrentMoodbarDataChanged(data);
|
||||
break;
|
||||
|
||||
case MoodbarLoader::WillLoadAsync:
|
||||
// Emit an empty array for now so the GUI reverts to a normal progress
|
||||
// bar. Our slot will be called when the data is actually loaded.
|
||||
emit CurrentMoodbarDataChanged(QByteArray());
|
||||
|
||||
NewClosure(pipeline, SIGNAL(Finished(bool)), this, SLOT(AsyncLoadComplete(MoodbarPipeline*, QUrl)), pipeline, song.url());
|
||||
break;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
void MoodbarController::PlaybackStopped() {
|
||||
emit CurrentMoodbarDataChanged(QByteArray());
|
||||
}
|
||||
|
||||
void MoodbarController::AsyncLoadComplete(MoodbarPipeline* pipeline, const QUrl& url) {
|
||||
|
||||
// Is this song still playing?
|
||||
PlaylistItemPtr current_item = app_->player()->GetCurrentItem();
|
||||
if (current_item && current_item->Url() != url) {
|
||||
return;
|
||||
}
|
||||
// Did we stop the song?
|
||||
switch(app_->player()->GetState()) {
|
||||
case Engine::Error:
|
||||
case Engine::Empty:
|
||||
case Engine::Idle:
|
||||
return;
|
||||
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
emit CurrentMoodbarDataChanged(pipeline->data());
|
||||
|
||||
}
|
47
src/moodbar/moodbarcontroller.h
Normal file
47
src/moodbar/moodbarcontroller.h
Normal file
@ -0,0 +1,47 @@
|
||||
/* This file was part of Clementine.
|
||||
Copyright 2012, David Sansome <me@davidsansome.com>
|
||||
|
||||
Strawberry is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
Strawberry is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with Strawberry. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#ifndef MOODBARCONTROLLER_H
|
||||
#define MOODBARCONTROLLER_H
|
||||
|
||||
#include <QObject>
|
||||
#include <QByteArray>
|
||||
#include <QUrl>
|
||||
|
||||
class Application;
|
||||
class MoodbarPipeline;
|
||||
class Song;
|
||||
|
||||
class MoodbarController : public QObject {
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
MoodbarController(Application* app, QObject* parent = nullptr);
|
||||
|
||||
signals:
|
||||
void CurrentMoodbarDataChanged(const QByteArray& data);
|
||||
|
||||
private slots:
|
||||
void CurrentSongChanged(const Song& song);
|
||||
void PlaybackStopped();
|
||||
void AsyncLoadComplete(MoodbarPipeline* pipeline, const QUrl& url);
|
||||
|
||||
private:
|
||||
Application* app_;
|
||||
};
|
||||
|
||||
#endif // MOODBARCONTROLLER_H
|
274
src/moodbar/moodbaritemdelegate.cpp
Normal file
274
src/moodbar/moodbaritemdelegate.cpp
Normal file
@ -0,0 +1,274 @@
|
||||
/* This file was part of Clementine.
|
||||
Copyright 2012, David Sansome <me@davidsansome.com>
|
||||
|
||||
Strawberry is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
Strawberry is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with Strawberry. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#include <QApplication>
|
||||
#include <QtConcurrentRun>
|
||||
#include <QFuture>
|
||||
#include <QSettings>
|
||||
#include <QSortFilterProxyModel>
|
||||
#include <QItemDelegate>
|
||||
#include <QByteArray>
|
||||
#include <QUrl>
|
||||
#include <QImage>
|
||||
#include <QPixmap>
|
||||
#include <QPainter>
|
||||
|
||||
#include "core/application.h"
|
||||
#include "core/closure.h"
|
||||
#include "playlist/playlist.h"
|
||||
#include "playlist/playlistview.h"
|
||||
|
||||
#include "moodbaritemdelegate.h"
|
||||
#include "moodbarloader.h"
|
||||
#include "moodbarpipeline.h"
|
||||
#include "moodbarrenderer.h"
|
||||
|
||||
#include "settings/moodbarsettingspage.h"
|
||||
|
||||
MoodbarItemDelegate::Data::Data() : state_(State_None) {}
|
||||
|
||||
MoodbarItemDelegate::MoodbarItemDelegate(Application* app, PlaylistView* view, QObject* parent)
|
||||
: QItemDelegate(parent),
|
||||
app_(app),
|
||||
view_(view),
|
||||
style_(MoodbarRenderer::Style_Normal) {
|
||||
|
||||
connect(app_, SIGNAL(SettingsChanged()), SLOT(ReloadSettings()));
|
||||
ReloadSettings();
|
||||
|
||||
}
|
||||
|
||||
void MoodbarItemDelegate::ReloadSettings() {
|
||||
|
||||
QSettings s;
|
||||
s.beginGroup(MoodbarSettingsPage::kSettingsGroup);
|
||||
MoodbarRenderer::MoodbarStyle new_style = static_cast<MoodbarRenderer::MoodbarStyle>(s.value("style", MoodbarRenderer::Style_Normal).toInt());
|
||||
s.endGroup();
|
||||
|
||||
if (new_style != style_) {
|
||||
style_ = new_style;
|
||||
ReloadAllColors();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
void MoodbarItemDelegate::paint(QPainter* painter, const QStyleOptionViewItem& option, const QModelIndex& index) const {
|
||||
|
||||
QPixmap pixmap = const_cast<MoodbarItemDelegate*>(this)->PixmapForIndex(index, option.rect.size());
|
||||
|
||||
drawBackground(painter, option, index);
|
||||
|
||||
if (!pixmap.isNull()) {
|
||||
// Make a little border for the moodbar
|
||||
const QRect moodbar_rect(option.rect.adjusted(1, 1, -1, -1));
|
||||
painter->drawPixmap(moodbar_rect, pixmap);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
QPixmap MoodbarItemDelegate::PixmapForIndex(const QModelIndex& index, const QSize& size) {
|
||||
|
||||
// Pixmaps are keyed off URL.
|
||||
const QUrl url(index.sibling(index.row(), Playlist::Column_Filename).data().toUrl());
|
||||
|
||||
Data* data = data_[url];
|
||||
if (!data) {
|
||||
data = new Data;
|
||||
data_.insert(url, data);
|
||||
}
|
||||
|
||||
data->indexes_.insert(index);
|
||||
data->desired_size_ = size;
|
||||
|
||||
switch (data->state_) {
|
||||
case Data::State_CannotLoad:
|
||||
case Data::State_LoadingData:
|
||||
case Data::State_LoadingColors:
|
||||
case Data::State_LoadingImage:
|
||||
return data->pixmap_;
|
||||
|
||||
case Data::State_Loaded:
|
||||
// Is the pixmap the right size?
|
||||
if (data->pixmap_.size() != size) {
|
||||
StartLoadingImage(url, data);
|
||||
}
|
||||
|
||||
return data->pixmap_;
|
||||
|
||||
case Data::State_None:
|
||||
break;
|
||||
}
|
||||
|
||||
// We have to start loading the data from scratch.
|
||||
StartLoadingData(url, data);
|
||||
|
||||
return QPixmap();
|
||||
|
||||
}
|
||||
|
||||
void MoodbarItemDelegate::StartLoadingData(const QUrl& url, Data* data) {
|
||||
|
||||
data->state_ = Data::State_LoadingData;
|
||||
|
||||
// Load a mood file for this song and generate some colors from it
|
||||
QByteArray bytes;
|
||||
MoodbarPipeline* pipeline = nullptr;
|
||||
switch (app_->moodbar_loader()->Load(url, &bytes, &pipeline)) {
|
||||
case MoodbarLoader::CannotLoad:
|
||||
data->state_ = Data::State_CannotLoad;
|
||||
break;
|
||||
|
||||
case MoodbarLoader::Loaded:
|
||||
// We got the data immediately.
|
||||
StartLoadingColors(url, bytes, data);
|
||||
break;
|
||||
|
||||
case MoodbarLoader::WillLoadAsync:
|
||||
// Maybe in a little while.
|
||||
NewClosure(pipeline, SIGNAL(Finished(bool)), this, SLOT(DataLoaded(QUrl, MoodbarPipeline*)), url, pipeline);
|
||||
break;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
bool MoodbarItemDelegate::RemoveFromCacheIfIndexesInvalid(const QUrl& url, Data* data) {
|
||||
|
||||
for (const QPersistentModelIndex& index : data->indexes_) {
|
||||
if (index.isValid()) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
data_.remove(url);
|
||||
return true;
|
||||
|
||||
}
|
||||
|
||||
void MoodbarItemDelegate::ReloadAllColors() {
|
||||
|
||||
for (const QUrl& url : data_.keys()) {
|
||||
Data* data = data_[url];
|
||||
|
||||
if (data->state_ == Data::State_Loaded) {
|
||||
StartLoadingData(url, data);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
void MoodbarItemDelegate::DataLoaded(const QUrl& url, MoodbarPipeline* pipeline) {
|
||||
|
||||
Data* data = data_[url];
|
||||
if (!data) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (RemoveFromCacheIfIndexesInvalid(url, data)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!pipeline->success()) {
|
||||
data->state_ = Data::State_CannotLoad;
|
||||
return;
|
||||
}
|
||||
|
||||
// Load the colors next.
|
||||
StartLoadingColors(url, pipeline->data(), data);
|
||||
|
||||
}
|
||||
|
||||
void MoodbarItemDelegate::StartLoadingColors(const QUrl& url, const QByteArray& bytes, Data* data) {
|
||||
|
||||
data->state_ = Data::State_LoadingColors;
|
||||
|
||||
QFuture<ColorVector> future = QtConcurrent::run(MoodbarRenderer::Colors, bytes, style_, qApp->palette());
|
||||
NewClosure(future, this, SLOT(ColorsLoaded(QUrl, QFuture<ColorVector>)), url, future);
|
||||
|
||||
}
|
||||
|
||||
void MoodbarItemDelegate::ColorsLoaded(const QUrl& url, QFuture<ColorVector> future) {
|
||||
|
||||
Data* data = data_[url];
|
||||
if (!data) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (RemoveFromCacheIfIndexesInvalid(url, data)) {
|
||||
return;
|
||||
}
|
||||
|
||||
data->colors_ = future.result();
|
||||
|
||||
// Load the image next.
|
||||
StartLoadingImage(url, data);
|
||||
|
||||
}
|
||||
|
||||
void MoodbarItemDelegate::StartLoadingImage(const QUrl& url, Data* data) {
|
||||
|
||||
data->state_ = Data::State_LoadingImage;
|
||||
|
||||
QFuture<QImage> future = QtConcurrent::run(MoodbarRenderer::RenderToImage, data->colors_, data->desired_size_);
|
||||
NewClosure(future, this, SLOT(ImageLoaded(QUrl, QFuture<QImage>)), url, future);
|
||||
|
||||
}
|
||||
|
||||
void MoodbarItemDelegate::ImageLoaded(const QUrl& url, QFuture<QImage> future) {
|
||||
|
||||
Data* data = data_[url];
|
||||
if (!data) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (RemoveFromCacheIfIndexesInvalid(url, data)) {
|
||||
return;
|
||||
}
|
||||
|
||||
QImage image(future.result());
|
||||
|
||||
// If the desired size changed then don't even bother converting the image
|
||||
// to a pixmap, just reload it at the new size.
|
||||
if (!image.isNull() && data->desired_size_ != image.size()) {
|
||||
StartLoadingImage(url, data);
|
||||
return;
|
||||
}
|
||||
|
||||
data->pixmap_ = QPixmap::fromImage(image);
|
||||
data->state_ = Data::State_Loaded;
|
||||
|
||||
Playlist* playlist = view_->playlist();
|
||||
const QSortFilterProxyModel* filter = playlist->proxy();
|
||||
|
||||
// Update all the indices with the new pixmap.
|
||||
for (const QPersistentModelIndex& index : data->indexes_) {
|
||||
if (index.isValid() && index.sibling(index.row(), Playlist::Column_Filename).data().toUrl() == url) {
|
||||
QModelIndex source_index = index;
|
||||
if (index.model() == filter) {
|
||||
source_index = filter->mapToSource(source_index);
|
||||
}
|
||||
|
||||
if (source_index.model() != playlist) {
|
||||
// The pixmap was for an index in a different playlist, maybe the user
|
||||
// switched to a different one.
|
||||
continue;
|
||||
}
|
||||
|
||||
playlist->MoodbarUpdated(source_index);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
90
src/moodbar/moodbaritemdelegate.h
Normal file
90
src/moodbar/moodbaritemdelegate.h
Normal file
@ -0,0 +1,90 @@
|
||||
/* This file is part of Clementine.
|
||||
Copyright 2012, David Sansome <me@davidsansome.com>
|
||||
|
||||
Strawberry is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
Strawberry is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with Strawberry. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#ifndef MOODBARITEMDELEGATE_H
|
||||
#define MOODBARITEMDELEGATE_H
|
||||
|
||||
#include "moodbarrenderer.h"
|
||||
|
||||
#include <QObject>
|
||||
#include <QItemDelegate>
|
||||
#include <QCache>
|
||||
#include <QFuture>
|
||||
#include <QSet>
|
||||
#include <QByteArray>
|
||||
#include <QUrl>
|
||||
#include <QPainter>
|
||||
|
||||
class Application;
|
||||
class MoodbarPipeline;
|
||||
class PlaylistView;
|
||||
|
||||
class MoodbarItemDelegate : public QItemDelegate {
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
MoodbarItemDelegate(Application* app, PlaylistView* view, QObject* parent = nullptr);
|
||||
|
||||
void paint(QPainter* painter, const QStyleOptionViewItem& option, const QModelIndex& index) const;
|
||||
|
||||
private slots:
|
||||
void ReloadSettings();
|
||||
|
||||
void DataLoaded(const QUrl& url, MoodbarPipeline* pipeline);
|
||||
void ColorsLoaded(const QUrl& url, QFuture<ColorVector> future);
|
||||
void ImageLoaded(const QUrl& url, QFuture<QImage> future);
|
||||
|
||||
private:
|
||||
struct Data {
|
||||
Data();
|
||||
|
||||
enum State {
|
||||
State_None,
|
||||
State_CannotLoad,
|
||||
State_LoadingData,
|
||||
State_LoadingColors,
|
||||
State_LoadingImage,
|
||||
State_Loaded
|
||||
};
|
||||
|
||||
QSet<QPersistentModelIndex> indexes_;
|
||||
|
||||
State state_;
|
||||
ColorVector colors_;
|
||||
QSize desired_size_;
|
||||
QPixmap pixmap_;
|
||||
};
|
||||
|
||||
private:
|
||||
QPixmap PixmapForIndex(const QModelIndex& index, const QSize& size);
|
||||
void StartLoadingData(const QUrl& url, Data* data);
|
||||
void StartLoadingColors(const QUrl& url, const QByteArray& bytes, Data* data);
|
||||
void StartLoadingImage(const QUrl& url, Data* data);
|
||||
|
||||
bool RemoveFromCacheIfIndexesInvalid(const QUrl& url, Data* data);
|
||||
|
||||
void ReloadAllColors();
|
||||
|
||||
private:
|
||||
Application* app_;
|
||||
PlaylistView* view_;
|
||||
QCache<QUrl, Data> data_;
|
||||
|
||||
MoodbarRenderer::MoodbarStyle style_;
|
||||
};
|
||||
|
||||
#endif // MOODBARITEMDELEGATE_H
|
203
src/moodbar/moodbarloader.cpp
Normal file
203
src/moodbar/moodbarloader.cpp
Normal file
@ -0,0 +1,203 @@
|
||||
/* This file was part of Clementine.
|
||||
Copyright 2012, David Sansome <me@davidsansome.com>
|
||||
|
||||
Strawberry is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
Strawberry is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with Strawberry. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#include "moodbarloader.h"
|
||||
|
||||
#include <memory>
|
||||
|
||||
#include <QObject>
|
||||
#include <QThread>
|
||||
#include <QCoreApplication>
|
||||
#include <QStandardPaths>
|
||||
#include <QIODevice>
|
||||
#include <QDir>
|
||||
#include <QFileInfo>
|
||||
#include <QNetworkDiskCache>
|
||||
#include <QTimer>
|
||||
#include <QString>
|
||||
#include <QUrl>
|
||||
|
||||
#include "core/application.h"
|
||||
#include "core/closure.h"
|
||||
#include "core/logging.h"
|
||||
|
||||
#include "moodbarpipeline.h"
|
||||
|
||||
#include "settings/moodbarsettingspage.h"
|
||||
|
||||
#ifdef Q_OS_WIN32
|
||||
# include <windows.h>
|
||||
#endif
|
||||
|
||||
using std::unique_ptr;
|
||||
|
||||
MoodbarLoader::MoodbarLoader(Application* app, QObject* parent)
|
||||
: QObject(parent),
|
||||
cache_(new QNetworkDiskCache(this)),
|
||||
thread_(new QThread(this)),
|
||||
kMaxActiveRequests(qMax(1, QThread::idealThreadCount() / 2)),
|
||||
enabled_(false),
|
||||
save_(false) {
|
||||
|
||||
cache_->setCacheDirectory(QStandardPaths::writableLocation(QStandardPaths::CacheLocation) + "/moodbar");
|
||||
cache_->setMaximumCacheSize(60 * 1024 * 1024); // 60MB - enough for 20,000 moodbars
|
||||
|
||||
connect(app, SIGNAL(SettingsChanged()), SLOT(ReloadSettings()));
|
||||
ReloadSettings();
|
||||
|
||||
}
|
||||
|
||||
MoodbarLoader::~MoodbarLoader() {
|
||||
thread_->quit();
|
||||
thread_->wait(1000);
|
||||
}
|
||||
|
||||
void MoodbarLoader::ReloadSettings() {
|
||||
|
||||
QSettings s;
|
||||
s.beginGroup(MoodbarSettingsPage::kSettingsGroup);
|
||||
enabled_ = s.value("enabled", false).toBool();
|
||||
save_ = s.value("save", false).toBool();
|
||||
s.endGroup();
|
||||
|
||||
MaybeTakeNextRequest();
|
||||
|
||||
}
|
||||
|
||||
QStringList MoodbarLoader::MoodFilenames(const QString& song_filename) {
|
||||
|
||||
const QFileInfo file_info(song_filename);
|
||||
const QString dir_path(file_info.dir().path());
|
||||
const QString mood_filename = file_info.baseName() + ".mood";
|
||||
|
||||
return QStringList() << dir_path + "/." + mood_filename << dir_path + "/" + mood_filename;
|
||||
|
||||
}
|
||||
|
||||
MoodbarLoader::Result MoodbarLoader::Load(const QUrl& url, QByteArray* data, MoodbarPipeline** async_pipeline) {
|
||||
|
||||
if (url.scheme() != "file") {
|
||||
return CannotLoad;
|
||||
}
|
||||
|
||||
// Are we in the middle of loading this moodbar already?
|
||||
if (requests_.contains(url)) {
|
||||
*async_pipeline = requests_[url];
|
||||
return WillLoadAsync;
|
||||
}
|
||||
|
||||
// Check if a mood file exists for this file already
|
||||
const QString filename(url.toLocalFile());
|
||||
|
||||
for (const QString& possible_mood_file : MoodFilenames(filename)) {
|
||||
QFile f(possible_mood_file);
|
||||
if (f.open(QIODevice::ReadOnly)) {
|
||||
qLog(Info) << "Loading moodbar data from" << possible_mood_file;
|
||||
*data = f.readAll();
|
||||
return Loaded;
|
||||
}
|
||||
}
|
||||
|
||||
// Maybe it exists in the cache?
|
||||
std::unique_ptr<QIODevice> cache_device(cache_->data(url));
|
||||
if (cache_device) {
|
||||
qLog(Info) << "Loading cached moodbar data for" << filename;
|
||||
*data = cache_device->readAll();
|
||||
if (!data->isEmpty()) {
|
||||
return Loaded;
|
||||
}
|
||||
}
|
||||
|
||||
if (!thread_->isRunning()) thread_->start(QThread::IdlePriority);
|
||||
|
||||
// There was no existing file, analyze the audio file and create one.
|
||||
MoodbarPipeline* pipeline = new MoodbarPipeline(url);
|
||||
pipeline->moveToThread(thread_);
|
||||
NewClosure(pipeline, SIGNAL(Finished(bool)), this, SLOT(RequestFinished(MoodbarPipeline*, QUrl)), pipeline, url);
|
||||
|
||||
requests_[url] = pipeline;
|
||||
queued_requests_ << url;
|
||||
|
||||
MaybeTakeNextRequest();
|
||||
|
||||
*async_pipeline = pipeline;
|
||||
return WillLoadAsync;
|
||||
|
||||
}
|
||||
|
||||
void MoodbarLoader::MaybeTakeNextRequest() {
|
||||
|
||||
Q_ASSERT(QThread::currentThread() == qApp->thread());
|
||||
|
||||
if (active_requests_.count() >= kMaxActiveRequests || queued_requests_.isEmpty() || !enabled_) {
|
||||
return;
|
||||
}
|
||||
|
||||
const QUrl url = queued_requests_.takeFirst();
|
||||
active_requests_ << url;
|
||||
|
||||
qLog(Info) << "Creating moodbar data for" << url.toLocalFile();
|
||||
QMetaObject::invokeMethod(requests_[url], "Start", Qt::QueuedConnection);
|
||||
|
||||
}
|
||||
|
||||
void MoodbarLoader::RequestFinished(MoodbarPipeline* request, const QUrl& url) {
|
||||
|
||||
Q_ASSERT(QThread::currentThread() == qApp->thread());
|
||||
|
||||
if (request->success()) {
|
||||
qLog(Info) << "Moodbar data generated successfully for" << url.toLocalFile();
|
||||
|
||||
// Save the data in the cache
|
||||
QNetworkCacheMetaData metadata;
|
||||
metadata.setUrl(url);
|
||||
|
||||
QIODevice* cache_file = cache_->prepare(metadata);
|
||||
if (cache_file) {
|
||||
cache_file->write(request->data());
|
||||
cache_->insert(cache_file);
|
||||
}
|
||||
|
||||
// Save the data alongside the original as well if we're configured to.
|
||||
if (save_) {
|
||||
const QString mood_filename(MoodFilenames(url.toLocalFile())[0]);
|
||||
QFile mood_file(mood_filename);
|
||||
if (mood_file.open(QIODevice::WriteOnly)) {
|
||||
mood_file.write(request->data());
|
||||
|
||||
#ifdef Q_OS_WIN32
|
||||
if (!SetFileAttributes((LPCTSTR)mood_filename.utf16(), FILE_ATTRIBUTE_HIDDEN)) {
|
||||
qLog(Warning) << "Error setting hidden attribute for file" << mood_filename;
|
||||
}
|
||||
#endif
|
||||
|
||||
}
|
||||
else {
|
||||
qLog(Warning) << "Error opening mood file for writing" << mood_filename;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Remove the request from the active list and delete it
|
||||
requests_.remove(url);
|
||||
active_requests_.remove(url);
|
||||
|
||||
QTimer::singleShot(1000, request, SLOT(deleteLater()));
|
||||
|
||||
MaybeTakeNextRequest();
|
||||
|
||||
}
|
79
src/moodbar/moodbarloader.h
Normal file
79
src/moodbar/moodbarloader.h
Normal file
@ -0,0 +1,79 @@
|
||||
/* This file was part of Clementine.
|
||||
Copyright 2012, David Sansome <me@davidsansome.com>
|
||||
|
||||
Strawberry is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
Strawberry is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with Strawberry. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#ifndef MOODBARLOADER_H
|
||||
#define MOODBARLOADER_H
|
||||
|
||||
#include <QObject>
|
||||
#include <QThread>
|
||||
#include <QList>
|
||||
#include <QMap>
|
||||
#include <QSet>
|
||||
#include <QByteArray>
|
||||
#include <QStringList>
|
||||
#include <QUrl>
|
||||
#include <QNetworkDiskCache>
|
||||
|
||||
class Application;
|
||||
class MoodbarPipeline;
|
||||
|
||||
class MoodbarLoader : public QObject {
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
MoodbarLoader(Application* app, QObject* parent = nullptr);
|
||||
~MoodbarLoader();
|
||||
|
||||
enum Result {
|
||||
// The URL isn't a local file or the moodbar plugin was not available -
|
||||
// moodbar data can never be loaded.
|
||||
CannotLoad,
|
||||
|
||||
// Moodbar data was loaded and returned.
|
||||
Loaded,
|
||||
|
||||
// Moodbar data will be loaded in the background, a MoodbarPipeline* was
|
||||
// was returned that you can connect to the Finished() signal on.
|
||||
WillLoadAsync
|
||||
};
|
||||
|
||||
Result Load(const QUrl& url, QByteArray* data, MoodbarPipeline** async_pipeline);
|
||||
|
||||
private slots:
|
||||
void ReloadSettings();
|
||||
|
||||
void RequestFinished(MoodbarPipeline* request, const QUrl& filename);
|
||||
void MaybeTakeNextRequest();
|
||||
|
||||
private:
|
||||
static QStringList MoodFilenames(const QString& song_filename);
|
||||
|
||||
private:
|
||||
QNetworkDiskCache* cache_;
|
||||
QThread* thread_;
|
||||
|
||||
const int kMaxActiveRequests;
|
||||
|
||||
QMap<QUrl, MoodbarPipeline*> requests_;
|
||||
QList<QUrl> queued_requests_;
|
||||
QSet<QUrl> active_requests_;
|
||||
|
||||
bool enabled_;
|
||||
bool save_;
|
||||
};
|
||||
|
||||
#endif // MOODBARLOADER_H
|
227
src/moodbar/moodbarpipeline.cpp
Normal file
227
src/moodbar/moodbarpipeline.cpp
Normal file
@ -0,0 +1,227 @@
|
||||
/* This file was part of Clementine.
|
||||
Copyright 2012, David Sansome <me@davidsansome.com>
|
||||
|
||||
Strawberry is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
Strawberry is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with Strawberry. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#include "moodbarpipeline.h"
|
||||
|
||||
#include <QObject>
|
||||
#include <QCoreApplication>
|
||||
#include <QThread>
|
||||
#include <QString>
|
||||
#include <QUrl>
|
||||
|
||||
#include "core/logging.h"
|
||||
#include "core/signalchecker.h"
|
||||
#include "core/timeconstants.h"
|
||||
#include "core/utilities.h"
|
||||
#include "moodbar/moodbarbuilder.h"
|
||||
|
||||
#include "ext/gstmoodbar/gstfastspectrum.h"
|
||||
|
||||
bool MoodbarPipeline::sIsAvailable = false;
|
||||
const int MoodbarPipeline::kBands = 128;
|
||||
|
||||
MoodbarPipeline::MoodbarPipeline(const QUrl& local_filename)
|
||||
: QObject(nullptr),
|
||||
local_filename_(local_filename),
|
||||
pipeline_(nullptr),
|
||||
convert_element_(nullptr),
|
||||
success_(false),
|
||||
running_(false) {}
|
||||
|
||||
MoodbarPipeline::~MoodbarPipeline() { Cleanup(); }
|
||||
|
||||
bool MoodbarPipeline::IsAvailable() {
|
||||
|
||||
if (!sIsAvailable) {
|
||||
GstElementFactory* factory = gst_element_factory_find("fftwspectrum");
|
||||
if (!factory) {
|
||||
return false;
|
||||
}
|
||||
gst_object_unref(factory);
|
||||
|
||||
sIsAvailable = true;
|
||||
}
|
||||
|
||||
return sIsAvailable;
|
||||
|
||||
}
|
||||
|
||||
GstElement* MoodbarPipeline::CreateElement(const QString& factory_name) {
|
||||
|
||||
GstElement* ret = gst_element_factory_make(factory_name.toLatin1().constData(), nullptr);
|
||||
|
||||
if (ret) {
|
||||
gst_bin_add(GST_BIN(pipeline_), ret);
|
||||
}
|
||||
else {
|
||||
qLog(Warning) << "Unable to create gstreamer element" << factory_name;
|
||||
}
|
||||
|
||||
return ret;
|
||||
|
||||
}
|
||||
|
||||
void MoodbarPipeline::Start() {
|
||||
|
||||
Q_ASSERT(QThread::currentThread() != qApp->thread());
|
||||
|
||||
Utilities::SetThreadIOPriority(Utilities::IOPRIO_CLASS_IDLE);
|
||||
|
||||
if (pipeline_) {
|
||||
return;
|
||||
}
|
||||
|
||||
pipeline_ = gst_pipeline_new("moodbar-pipeline");
|
||||
|
||||
GstElement* decodebin = CreateElement("uridecodebin");
|
||||
convert_element_ = CreateElement("audioconvert");
|
||||
GstElement* spectrum = CreateElement("fastspectrum");
|
||||
GstElement* fakesink = CreateElement("fakesink");
|
||||
|
||||
if (!decodebin || !convert_element_ || !spectrum || !fakesink) {
|
||||
pipeline_ = nullptr;
|
||||
emit Finished(false);
|
||||
return;
|
||||
}
|
||||
|
||||
// Join them together
|
||||
if (!gst_element_link(convert_element_, spectrum) || !gst_element_link(spectrum, fakesink)) {
|
||||
qLog(Error) << "Failed to link elements";
|
||||
pipeline_ = nullptr;
|
||||
emit Finished(false);
|
||||
return;
|
||||
}
|
||||
|
||||
builder_.reset(new MoodbarBuilder);
|
||||
|
||||
// Set properties
|
||||
g_object_set(decodebin, "uri", local_filename_.toEncoded().constData(), nullptr);
|
||||
g_object_set(spectrum, "bands", kBands, nullptr);
|
||||
|
||||
GstFastSpectrum* fast_spectrum = GST_FASTSPECTRUM(spectrum);
|
||||
fast_spectrum->output_callback = [this](double* magnitudes, int size) { builder_->AddFrame(magnitudes, size); };
|
||||
|
||||
// Connect signals
|
||||
CHECKED_GCONNECT(decodebin, "pad-added", &NewPadCallback, this);
|
||||
GstBus* bus = gst_pipeline_get_bus(GST_PIPELINE(pipeline_));
|
||||
gst_bus_set_sync_handler(bus, BusCallbackSync, this, nullptr);
|
||||
gst_object_unref(bus);
|
||||
|
||||
// Start playing
|
||||
running_ = true;
|
||||
gst_element_set_state(pipeline_, GST_STATE_PLAYING);
|
||||
|
||||
}
|
||||
|
||||
void MoodbarPipeline::ReportError(GstMessage* msg) {
|
||||
|
||||
GError* error;
|
||||
gchar* debugs;
|
||||
|
||||
gst_message_parse_error(msg, &error, &debugs);
|
||||
QString message = QString::fromLocal8Bit(error->message);
|
||||
|
||||
g_error_free(error);
|
||||
free(debugs);
|
||||
|
||||
qLog(Error) << "Error processing" << local_filename_ << ":" << message;
|
||||
|
||||
}
|
||||
|
||||
void MoodbarPipeline::NewPadCallback(GstElement*, GstPad* pad, gpointer data) {
|
||||
|
||||
MoodbarPipeline* self = reinterpret_cast<MoodbarPipeline*>(data);
|
||||
|
||||
if (!self->running_) {
|
||||
qLog(Warning) << "Received gstreamer callback after pipeline has stopped.";
|
||||
return;
|
||||
}
|
||||
|
||||
GstPad* const audiopad = gst_element_get_static_pad(self->convert_element_, "sink");
|
||||
|
||||
if (GST_PAD_IS_LINKED(audiopad)) {
|
||||
qLog(Warning) << "audiopad is already linked, unlinking old pad";
|
||||
gst_pad_unlink(audiopad, GST_PAD_PEER(audiopad));
|
||||
}
|
||||
|
||||
gst_pad_link(pad, audiopad);
|
||||
gst_object_unref(audiopad);
|
||||
|
||||
int rate = 0;
|
||||
GstCaps* caps = gst_pad_get_current_caps(pad);
|
||||
GstStructure* structure = gst_caps_get_structure(caps, 0);
|
||||
gst_structure_get_int(structure, "rate", &rate);
|
||||
gst_caps_unref(caps);
|
||||
|
||||
if (self->builder_)
|
||||
self->builder_->Init(kBands, rate);
|
||||
else
|
||||
qLog(Error) << "Builder does not exist";
|
||||
|
||||
}
|
||||
|
||||
GstBusSyncReply MoodbarPipeline::BusCallbackSync(GstBus*, GstMessage* msg, gpointer data) {
|
||||
|
||||
MoodbarPipeline* self = reinterpret_cast<MoodbarPipeline*>(data);
|
||||
|
||||
switch (GST_MESSAGE_TYPE(msg)) {
|
||||
case GST_MESSAGE_EOS:
|
||||
self->Stop(true);
|
||||
break;
|
||||
|
||||
case GST_MESSAGE_ERROR:
|
||||
self->ReportError(msg);
|
||||
self->Stop(false);
|
||||
break;
|
||||
|
||||
default:
|
||||
break;
|
||||
}
|
||||
return GST_BUS_PASS;
|
||||
|
||||
}
|
||||
|
||||
void MoodbarPipeline::Stop(bool success) {
|
||||
|
||||
success_ = success;
|
||||
running_ = false;
|
||||
if (builder_ != nullptr) {
|
||||
data_ = builder_->Finish(1000);
|
||||
builder_.reset();
|
||||
}
|
||||
|
||||
emit Finished(success);
|
||||
|
||||
}
|
||||
|
||||
void MoodbarPipeline::Cleanup() {
|
||||
|
||||
Q_ASSERT(QThread::currentThread() == thread());
|
||||
Q_ASSERT(QThread::currentThread() != qApp->thread());
|
||||
|
||||
running_ = false;
|
||||
if (pipeline_) {
|
||||
GstBus* bus = gst_pipeline_get_bus(GST_PIPELINE(pipeline_));
|
||||
gst_bus_set_sync_handler(bus, nullptr, nullptr, nullptr);
|
||||
gst_object_unref(bus);
|
||||
|
||||
gst_element_set_state(pipeline_, GST_STATE_NULL);
|
||||
gst_object_unref(pipeline_);
|
||||
pipeline_ = nullptr;
|
||||
}
|
||||
|
||||
}
|
79
src/moodbar/moodbarpipeline.h
Normal file
79
src/moodbar/moodbarpipeline.h
Normal file
@ -0,0 +1,79 @@
|
||||
/* This file was part of Clementine.
|
||||
Copyright 2012, David Sansome <me@davidsansome.com>
|
||||
|
||||
Strawberry is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
Strawberry is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with Strawberry. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#ifndef MOODBARPIPELINE_H
|
||||
#define MOODBARPIPELINE_H
|
||||
|
||||
#include <QObject>
|
||||
#include <QByteArray>
|
||||
#include <QString>
|
||||
#include <QUrl>
|
||||
|
||||
#include <gst/gst.h>
|
||||
#include <gst/app/gstappsink.h>
|
||||
|
||||
#include <memory>
|
||||
|
||||
class MoodbarBuilder;
|
||||
|
||||
// Creates moodbar data for a single local music file.
|
||||
class MoodbarPipeline : public QObject {
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
MoodbarPipeline(const QUrl& local_filename);
|
||||
~MoodbarPipeline();
|
||||
|
||||
static bool IsAvailable();
|
||||
|
||||
bool success() const { return success_; }
|
||||
const QByteArray& data() const { return data_; }
|
||||
|
||||
public slots:
|
||||
void Start();
|
||||
|
||||
signals:
|
||||
void Finished(bool success);
|
||||
|
||||
private:
|
||||
GstElement* CreateElement(const QString& factory_name);
|
||||
|
||||
void ReportError(GstMessage* message);
|
||||
void Stop(bool success);
|
||||
void Cleanup();
|
||||
|
||||
static void NewPadCallback(GstElement*, GstPad* pad, gpointer data);
|
||||
static GstFlowReturn NewBufferCallback(GstAppSink* app_sink, gpointer self);
|
||||
static gboolean BusCallback(GstBus*, GstMessage* msg, gpointer data);
|
||||
static GstBusSyncReply BusCallbackSync(GstBus*, GstMessage* msg, gpointer data);
|
||||
|
||||
private:
|
||||
static bool sIsAvailable;
|
||||
static const int kBands;
|
||||
|
||||
QUrl local_filename_;
|
||||
GstElement* pipeline_;
|
||||
GstElement* convert_element_;
|
||||
|
||||
std::unique_ptr<MoodbarBuilder> builder_;
|
||||
|
||||
bool success_;
|
||||
bool running_;
|
||||
QByteArray data_;
|
||||
};
|
||||
|
||||
#endif // MOODBARPIPELINE_H
|
397
src/moodbar/moodbarproxystyle.cpp
Normal file
397
src/moodbar/moodbarproxystyle.cpp
Normal file
@ -0,0 +1,397 @@
|
||||
/* This file was part of Clementine.
|
||||
Copyright 2012, David Sansome <me@davidsansome.com>
|
||||
|
||||
Strawberry is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
Strawberry is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with Strawberry. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#include <QProxyStyle>
|
||||
#include <QSettings>
|
||||
#include <QMenu>
|
||||
#include <QPixmap>
|
||||
#include <QPainter>
|
||||
#include <QSlider>
|
||||
#include <QStyleOptionComplex>
|
||||
#include <QStyleOptionSlider>
|
||||
#include <QTimeLine>
|
||||
#include <QEvent>
|
||||
#include <QContextMenuEvent>
|
||||
|
||||
#include "core/application.h"
|
||||
#include "core/logging.h"
|
||||
|
||||
#include "moodbarproxystyle.h"
|
||||
#include "settings/moodbarsettingspage.h"
|
||||
|
||||
const int MoodbarProxyStyle::kMarginSize = 3;
|
||||
const int MoodbarProxyStyle::kBorderSize = 1;
|
||||
const int MoodbarProxyStyle::kArrowWidth = 17;
|
||||
const int MoodbarProxyStyle::kArrowHeight = 13;
|
||||
|
||||
MoodbarProxyStyle::MoodbarProxyStyle(Application* app, QSlider* slider)
|
||||
: QProxyStyle(slider->style()),
|
||||
app_(app),
|
||||
slider_(slider),
|
||||
enabled_(true),
|
||||
moodbar_style_(MoodbarRenderer::Style_Normal),
|
||||
state_(MoodbarOff),
|
||||
fade_timeline_(new QTimeLine(1000, this)),
|
||||
moodbar_colors_dirty_(true),
|
||||
moodbar_pixmap_dirty_(true),
|
||||
context_menu_(nullptr),
|
||||
show_moodbar_action_(nullptr),
|
||||
style_action_group_(nullptr) {
|
||||
|
||||
slider->setStyle(this);
|
||||
slider->installEventFilter(this);
|
||||
|
||||
connect(fade_timeline_, SIGNAL(valueChanged(qreal)), SLOT(FaderValueChanged(qreal)));
|
||||
|
||||
connect(app, SIGNAL(SettingsChanged()), SLOT(ReloadSettings()));
|
||||
ReloadSettings();
|
||||
}
|
||||
|
||||
void MoodbarProxyStyle::ReloadSettings() {
|
||||
|
||||
QSettings s;
|
||||
s.beginGroup(MoodbarSettingsPage::kSettingsGroup);
|
||||
// Get the enabled/disabled setting, and start the timelines if there's a change.
|
||||
enabled_ = s.value("show", false).toBool();
|
||||
|
||||
NextState();
|
||||
|
||||
// Get the style, and redraw if there's a change.
|
||||
MoodbarRenderer::MoodbarStyle new_style = static_cast<MoodbarRenderer::MoodbarStyle>(s.value("style", MoodbarRenderer::Style_Normal).toInt());
|
||||
|
||||
s.endGroup();
|
||||
|
||||
if (new_style != moodbar_style_) {
|
||||
moodbar_style_ = new_style;
|
||||
moodbar_colors_dirty_ = true;
|
||||
slider_->update();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
void MoodbarProxyStyle::SetMoodbarData(const QByteArray& data) {
|
||||
|
||||
data_ = data;
|
||||
moodbar_colors_dirty_ = true; // Redraw next time
|
||||
NextState();
|
||||
|
||||
}
|
||||
|
||||
void MoodbarProxyStyle::SetMoodbarEnabled(bool enabled) {
|
||||
|
||||
enabled_ = enabled;
|
||||
|
||||
// Save the enabled setting.
|
||||
QSettings s;
|
||||
s.beginGroup(MoodbarSettingsPage::kSettingsGroup);
|
||||
s.setValue("show", enabled);
|
||||
s.endGroup();
|
||||
|
||||
app_->ReloadSettings();
|
||||
|
||||
}
|
||||
|
||||
void MoodbarProxyStyle::NextState() {
|
||||
|
||||
const bool visible = enabled_ && !data_.isEmpty();
|
||||
|
||||
// While the regular slider should stay at the standard size (Fixed),
|
||||
// moodbars should use all available space (MinimumExpanding).
|
||||
slider_->setSizePolicy(QSizePolicy::Expanding, visible ? QSizePolicy::MinimumExpanding : QSizePolicy::Fixed);
|
||||
slider_->updateGeometry();
|
||||
|
||||
if (show_moodbar_action_) {
|
||||
show_moodbar_action_->setChecked(enabled_);
|
||||
}
|
||||
|
||||
if ((visible && (state_ == MoodbarOn || state_ == FadingToOn)) || (!visible && (state_ == MoodbarOff || state_ == FadingToOff))) {
|
||||
return;
|
||||
}
|
||||
|
||||
const QTimeLine::Direction direction = visible ? QTimeLine::Forward : QTimeLine::Backward;
|
||||
|
||||
if (state_ == MoodbarOn || state_ == MoodbarOff) {
|
||||
// Start the fade from the beginning.
|
||||
fade_timeline_->setDirection(direction);
|
||||
fade_timeline_->start();
|
||||
|
||||
fade_source_ = QPixmap();
|
||||
fade_target_ = QPixmap();
|
||||
}
|
||||
else {
|
||||
// Stop an existing fade and start fading the other direction from the
|
||||
// same place.
|
||||
fade_timeline_->stop();
|
||||
fade_timeline_->setDirection(direction);
|
||||
fade_timeline_->resume();
|
||||
}
|
||||
|
||||
state_ = visible ? FadingToOn : FadingToOff;
|
||||
|
||||
}
|
||||
|
||||
void MoodbarProxyStyle::FaderValueChanged(qreal value) { slider_->update(); }
|
||||
|
||||
bool MoodbarProxyStyle::eventFilter(QObject* object, QEvent* event) {
|
||||
|
||||
if (object == slider_) {
|
||||
switch (event->type()) {
|
||||
case QEvent::Resize:
|
||||
// The widget was resized, we've got to render a new pixmap.
|
||||
moodbar_pixmap_dirty_ = true;
|
||||
break;
|
||||
|
||||
case QEvent::ContextMenu:
|
||||
ShowContextMenu(static_cast<QContextMenuEvent*>(event)->globalPos());
|
||||
return true;
|
||||
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return QProxyStyle::eventFilter(object, event);
|
||||
|
||||
}
|
||||
|
||||
void MoodbarProxyStyle::drawComplexControl(ComplexControl control, const QStyleOptionComplex* option, QPainter* painter, const QWidget* widget) const {
|
||||
|
||||
if (control != CC_Slider || widget != slider_) {
|
||||
QProxyStyle::drawComplexControl(control, option, painter, widget);
|
||||
return;
|
||||
}
|
||||
|
||||
const_cast<MoodbarProxyStyle*>(this)->Render(control, qstyleoption_cast<const QStyleOptionSlider*>(option), painter, widget);
|
||||
|
||||
}
|
||||
|
||||
void MoodbarProxyStyle::Render(ComplexControl control, const QStyleOptionSlider* option, QPainter* painter, const QWidget* widget) {
|
||||
|
||||
const qreal fade_value = fade_timeline_->currentValue();
|
||||
|
||||
// Have we finished fading?
|
||||
if (state_ == FadingToOn && fade_value == 1.0) {
|
||||
state_ = MoodbarOn;
|
||||
}
|
||||
else if (state_ == FadingToOff && fade_value == 0.0) {
|
||||
state_ = MoodbarOff;
|
||||
}
|
||||
|
||||
switch (state_) {
|
||||
case FadingToOn:
|
||||
case FadingToOff:
|
||||
// Update the cached pixmaps if necessary
|
||||
if (fade_source_.isNull()) {
|
||||
// Draw the normal slider into the fade source pixmap.
|
||||
fade_source_ = QPixmap(option->rect.size());
|
||||
fade_source_.fill(option->palette.color(QPalette::Active, QPalette::Background));
|
||||
|
||||
QPainter p(&fade_source_);
|
||||
QStyleOptionSlider opt_copy(*option);
|
||||
opt_copy.rect.moveTo(0, 0);
|
||||
|
||||
QProxyStyle::drawComplexControl(control, &opt_copy, &p, widget);
|
||||
|
||||
p.end();
|
||||
}
|
||||
|
||||
if (fade_target_.isNull()) {
|
||||
if (state_ == FadingToOn) {
|
||||
EnsureMoodbarRendered(option);
|
||||
}
|
||||
fade_target_ = moodbar_pixmap_;
|
||||
QPainter p(&fade_target_);
|
||||
DrawArrow(option, &p);
|
||||
p.end();
|
||||
}
|
||||
|
||||
// Blend the pixmaps into each other
|
||||
painter->drawPixmap(option->rect, fade_source_);
|
||||
painter->setOpacity(fade_value);
|
||||
painter->drawPixmap(option->rect, fade_target_);
|
||||
painter->setOpacity(1.0);
|
||||
break;
|
||||
|
||||
case MoodbarOff:
|
||||
// It's a normal slider widget.
|
||||
QProxyStyle::drawComplexControl(control, option, painter, widget);
|
||||
break;
|
||||
|
||||
case MoodbarOn:
|
||||
EnsureMoodbarRendered(option);
|
||||
painter->drawPixmap(option->rect, moodbar_pixmap_);
|
||||
DrawArrow(option, painter);
|
||||
break;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
void MoodbarProxyStyle::EnsureMoodbarRendered(const QStyleOptionSlider* opt) {
|
||||
|
||||
if (moodbar_colors_dirty_) {
|
||||
moodbar_colors_ = MoodbarRenderer::Colors(data_, moodbar_style_, slider_->palette());
|
||||
moodbar_colors_dirty_ = false;
|
||||
moodbar_pixmap_dirty_ = true;
|
||||
}
|
||||
|
||||
if (moodbar_pixmap_dirty_) {
|
||||
moodbar_pixmap_ = MoodbarPixmap(moodbar_colors_, slider_->size(), slider_->palette(), opt);
|
||||
moodbar_pixmap_dirty_ = false;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
QRect MoodbarProxyStyle::subControlRect(ComplexControl cc, const QStyleOptionComplex* opt, SubControl sc, const QWidget* widget) const {
|
||||
|
||||
if (cc != QStyle::CC_Slider || widget != slider_) {
|
||||
return QProxyStyle::subControlRect(cc, opt, sc, widget);
|
||||
}
|
||||
|
||||
switch (state_) {
|
||||
case MoodbarOff:
|
||||
case FadingToOff:
|
||||
break;
|
||||
|
||||
case MoodbarOn:
|
||||
case FadingToOn:
|
||||
switch (sc) {
|
||||
case SC_SliderGroove:
|
||||
return opt->rect.adjusted(kMarginSize, kMarginSize, -kMarginSize, -kMarginSize);
|
||||
|
||||
case SC_SliderHandle: {
|
||||
const QStyleOptionSlider* slider_opt = qstyleoption_cast<const QStyleOptionSlider*>(opt);
|
||||
int x_offset = 0;
|
||||
|
||||
/* slider_opt->{maximum,minimum} can have the value 0 (their default
|
||||
values), so this check avoids a division by 0. */
|
||||
if (slider_opt->maximum > slider_opt->minimum) {
|
||||
qint64 slider_delta = slider_opt->sliderValue - slider_opt->minimum;
|
||||
qint64 slider_range = slider_opt->maximum - slider_opt->minimum;
|
||||
int rectangle_effective_width = opt->rect.width() - kArrowWidth;
|
||||
|
||||
qint64 x = slider_delta * rectangle_effective_width / slider_range;
|
||||
x_offset = static_cast<int>(x);
|
||||
}
|
||||
|
||||
return QRect(QPoint(opt->rect.left() + x_offset, opt->rect.top()), QSize(kArrowWidth, kArrowHeight));
|
||||
}
|
||||
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return QProxyStyle::subControlRect(cc, opt, sc, widget);
|
||||
}
|
||||
|
||||
void MoodbarProxyStyle::DrawArrow(const QStyleOptionSlider* option, QPainter* painter) const {
|
||||
|
||||
// Get the dimensions of the arrow
|
||||
const QRect rect = subControlRect(CC_Slider, option, SC_SliderHandle, slider_);
|
||||
|
||||
// Make a polygon
|
||||
QPolygon poly;
|
||||
poly << rect.topLeft() << rect.topRight() << QPoint(rect.center().x(), rect.bottom());
|
||||
|
||||
// Draw it
|
||||
painter->save();
|
||||
painter->setRenderHint(QPainter::Antialiasing);
|
||||
painter->translate(0.5, 0.5);
|
||||
painter->setPen(Qt::black);
|
||||
painter->setBrush(slider_->palette().brush(QPalette::Active, QPalette::Base));
|
||||
painter->drawPolygon(poly);
|
||||
painter->restore();
|
||||
|
||||
}
|
||||
|
||||
QPixmap MoodbarProxyStyle::MoodbarPixmap(const ColorVector& colors, const QSize& size, const QPalette& palette, const QStyleOptionSlider* opt) {
|
||||
|
||||
QRect rect(QPoint(0, 0), size);
|
||||
QRect border_rect(rect);
|
||||
border_rect.adjust(kMarginSize, kMarginSize, -kMarginSize, -kMarginSize);
|
||||
|
||||
QRect inner_rect(border_rect);
|
||||
inner_rect.adjust(kBorderSize, kBorderSize, -kBorderSize, -kBorderSize);
|
||||
|
||||
QPixmap ret(size);
|
||||
QPainter p(&ret);
|
||||
|
||||
// Draw the moodbar
|
||||
MoodbarRenderer::Render(colors, &p, inner_rect);
|
||||
|
||||
// Draw the border
|
||||
p.setPen(QPen(Qt::black, kBorderSize, Qt::SolidLine, Qt::FlatCap, Qt::MiterJoin));
|
||||
p.drawRect(border_rect.adjusted(0, 0, -1, -1));
|
||||
|
||||
// Draw the outer bit
|
||||
p.setPen(QPen(palette.brush(QPalette::Active, QPalette::Background), kMarginSize, Qt::SolidLine, Qt::FlatCap, Qt::MiterJoin));
|
||||
|
||||
p.drawRect(rect.adjusted(1, 1, -2, -2));
|
||||
|
||||
p.end();
|
||||
|
||||
return ret;
|
||||
|
||||
}
|
||||
|
||||
void MoodbarProxyStyle::ShowContextMenu(const QPoint& pos) {
|
||||
|
||||
if (!context_menu_) {
|
||||
context_menu_ = new QMenu(slider_);
|
||||
show_moodbar_action_ = context_menu_->addAction(tr("Show moodbar"), this, SLOT(SetMoodbarEnabled(bool)));
|
||||
|
||||
show_moodbar_action_->setCheckable(true);
|
||||
show_moodbar_action_->setChecked(enabled_);
|
||||
|
||||
QMenu* styles_menu = context_menu_->addMenu(tr("Moodbar style"));
|
||||
style_action_group_ = new QActionGroup(styles_menu);
|
||||
|
||||
for (int i = 0; i < MoodbarRenderer::StyleCount; ++i) {
|
||||
const MoodbarRenderer::MoodbarStyle style = MoodbarRenderer::MoodbarStyle(i);
|
||||
|
||||
QAction* action = style_action_group_->addAction(MoodbarRenderer::StyleName(style));
|
||||
action->setCheckable(true);
|
||||
action->setData(i);
|
||||
}
|
||||
|
||||
styles_menu->addActions(style_action_group_->actions());
|
||||
|
||||
connect(styles_menu, SIGNAL(triggered(QAction*)), SLOT(ChangeStyle(QAction*)));
|
||||
}
|
||||
|
||||
// Update the currently selected style
|
||||
for (QAction* action : style_action_group_->actions()) {
|
||||
if (MoodbarRenderer::MoodbarStyle(action->data().toInt()) == moodbar_style_) {
|
||||
action->setChecked(true);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
context_menu_->popup(pos);
|
||||
|
||||
}
|
||||
|
||||
void MoodbarProxyStyle::ChangeStyle(QAction* action) {
|
||||
|
||||
QSettings s;
|
||||
s.beginGroup(MoodbarSettingsPage::kSettingsGroup);
|
||||
s.setValue("style", action->data().toInt());
|
||||
s.endGroup();
|
||||
|
||||
app_->ReloadSettings();
|
||||
|
||||
}
|
103
src/moodbar/moodbarproxystyle.h
Normal file
103
src/moodbar/moodbarproxystyle.h
Normal file
@ -0,0 +1,103 @@
|
||||
/* This file was part of Clementine.
|
||||
Copyright 2012, David Sansome <me@davidsansome.com>
|
||||
|
||||
Strawberry is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
Strawberry is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with Strawberry. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#ifndef MOODBARPROXYSTYLE_H
|
||||
#define MOODBARPROXYSTYLE_H
|
||||
|
||||
#include <QProxyStyle>
|
||||
#include <QTimeLine>
|
||||
#include <QByteArray>
|
||||
#include <QPixmap>
|
||||
#include <QPainter>
|
||||
#include <QSlider>
|
||||
#include <QMenu>
|
||||
#include <QActionGroup>
|
||||
#include <QStyleOptionSlider>
|
||||
#include <QEvent>
|
||||
|
||||
#include "moodbarrenderer.h"
|
||||
|
||||
class Application;
|
||||
|
||||
class MoodbarProxyStyle : public QProxyStyle {
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
MoodbarProxyStyle(Application* app, QSlider* slider);
|
||||
|
||||
// QProxyStyle
|
||||
void drawComplexControl(ComplexControl control, const QStyleOptionComplex* option, QPainter* painter, const QWidget* widget) const;
|
||||
QRect subControlRect(ComplexControl cc, const QStyleOptionComplex* opt, SubControl sc, const QWidget* widget) const;
|
||||
|
||||
// QObject
|
||||
bool eventFilter(QObject* object, QEvent* event);
|
||||
|
||||
public slots:
|
||||
// An empty byte array means there's no moodbar, so just show a normal slider.
|
||||
void SetMoodbarData(const QByteArray& data);
|
||||
|
||||
// If the moodbar is disabled then a normal slider will always be shown.
|
||||
void SetMoodbarEnabled(bool enabled);
|
||||
|
||||
private:
|
||||
static const int kMarginSize;
|
||||
static const int kBorderSize;
|
||||
static const int kArrowWidth;
|
||||
static const int kArrowHeight;
|
||||
|
||||
enum State { MoodbarOn, MoodbarOff, FadingToOn, FadingToOff };
|
||||
|
||||
private:
|
||||
void NextState();
|
||||
|
||||
void Render(ComplexControl control, const QStyleOptionSlider* option, QPainter* painter, const QWidget* widget);
|
||||
void EnsureMoodbarRendered(const QStyleOptionSlider* opt);
|
||||
void DrawArrow(const QStyleOptionSlider* option, QPainter* painter) const;
|
||||
void ShowContextMenu(const QPoint& pos);
|
||||
|
||||
QPixmap MoodbarPixmap(const ColorVector& colors, const QSize& size, const QPalette& palette, const QStyleOptionSlider* opt);
|
||||
|
||||
private slots:
|
||||
void ReloadSettings();
|
||||
void FaderValueChanged(qreal value);
|
||||
void ChangeStyle(QAction* action);
|
||||
|
||||
private:
|
||||
Application* app_;
|
||||
QSlider* slider_;
|
||||
|
||||
bool enabled_;
|
||||
QByteArray data_;
|
||||
MoodbarRenderer::MoodbarStyle moodbar_style_;
|
||||
|
||||
State state_;
|
||||
QTimeLine* fade_timeline_;
|
||||
|
||||
QPixmap fade_source_;
|
||||
QPixmap fade_target_;
|
||||
|
||||
bool moodbar_colors_dirty_;
|
||||
bool moodbar_pixmap_dirty_;
|
||||
ColorVector moodbar_colors_;
|
||||
QPixmap moodbar_pixmap_;
|
||||
|
||||
QMenu* context_menu_;
|
||||
QAction* show_moodbar_action_;
|
||||
QActionGroup* style_action_group_;
|
||||
};
|
||||
|
||||
#endif // MOODBARPROXYSTYLE_H
|
178
src/moodbar/moodbarrenderer.cpp
Normal file
178
src/moodbar/moodbarrenderer.cpp
Normal file
@ -0,0 +1,178 @@
|
||||
/* This file was part of Clementine.
|
||||
Copyright 2012, David Sansome <me@davidsansome.com>
|
||||
|
||||
Strawberry is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
Strawberry is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with Strawberry. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#include "moodbarrenderer.h"
|
||||
|
||||
#include <QByteArray>
|
||||
#include <QString>
|
||||
#include <QImage>
|
||||
#include <QPainter>
|
||||
#include <QPalette>
|
||||
#include <QColor>
|
||||
|
||||
#include "core/arraysize.h"
|
||||
|
||||
const int MoodbarRenderer::kNumHues = 12;
|
||||
|
||||
ColorVector MoodbarRenderer::Colors(const QByteArray& data, MoodbarStyle style, const QPalette& palette) {
|
||||
|
||||
const int samples = data.size() / 3;
|
||||
|
||||
// Set some parameters based on the moodbar style
|
||||
StyleProperties properties;
|
||||
switch (style) {
|
||||
case Style_Angry:
|
||||
properties = StyleProperties(samples / 360 * 9, 45, -45, 200, 100);
|
||||
break;
|
||||
case Style_Frozen:
|
||||
properties = StyleProperties(samples / 360 * 1, 140, 160, 50, 100);
|
||||
break;
|
||||
case Style_Happy:
|
||||
properties = StyleProperties(samples / 360 * 2, 0, 359, 150, 250);
|
||||
break;
|
||||
case Style_Normal:
|
||||
properties = StyleProperties(samples / 360 * 3, 0, 359, 100, 100);
|
||||
break;
|
||||
case Style_SystemPalette:
|
||||
default: {
|
||||
const QColor highlight_color(palette.color(QPalette::Active, QPalette::Highlight));
|
||||
|
||||
properties.threshold_ = samples / 360 * 3;
|
||||
properties.range_start_ = (highlight_color.hsvHue() - 20 + 360) % 360;
|
||||
properties.range_delta_ = 20;
|
||||
properties.sat_ = highlight_color.hsvSaturation();
|
||||
properties.val_ = highlight_color.value() / 2;
|
||||
}
|
||||
}
|
||||
|
||||
const unsigned char* data_p = reinterpret_cast<const unsigned char*>(data.constData());
|
||||
|
||||
int hue_distribution[360];
|
||||
int total = 0;
|
||||
|
||||
memset(hue_distribution, 0, sizeof(hue_distribution));
|
||||
|
||||
ColorVector colors;
|
||||
|
||||
// Read the colors, keeping track of some histograms
|
||||
for (int i = 0; i < samples; ++i) {
|
||||
QColor color;
|
||||
color.setRed(int(*data_p++));
|
||||
color.setGreen(int(*data_p++));
|
||||
color.setBlue(int(*data_p++));
|
||||
|
||||
colors << color;
|
||||
|
||||
const int hue = qMax(0, color.hue());
|
||||
if (hue_distribution[hue]++ == properties.threshold_) {
|
||||
total++;
|
||||
}
|
||||
}
|
||||
|
||||
total = qMax(total, 1);
|
||||
|
||||
// Remap the hue values to be between rangeStart and
|
||||
// rangeStart + rangeDelta. Every time we see an input hue
|
||||
// above the threshold, increment the output hue by
|
||||
// (1/total) * rangeDelta.
|
||||
for (int i = 0, n = 0; i < 360; i++) {
|
||||
hue_distribution[i] = ((hue_distribution[i] > properties.threshold_ ? n++ : n) * properties.range_delta_ / total + properties.range_start_) % 360;
|
||||
}
|
||||
|
||||
// Now huedist is a hue mapper: huedist[h] is the new hue value
|
||||
// for a bar with hue h
|
||||
for (ColorVector::iterator it = colors.begin(); it != colors.end(); ++it) {
|
||||
const int hue = qMax(0, it->hue());
|
||||
|
||||
*it = QColor::fromHsv(qBound(0, hue_distribution[hue], 359), qBound(0, it->saturation() * properties.sat_ / 100, 255), qBound(0, it->value() * properties.val_ / 100, 255));
|
||||
}
|
||||
|
||||
return colors;
|
||||
}
|
||||
|
||||
void MoodbarRenderer::Render(const ColorVector& colors, QPainter* p, const QRect& rect) {
|
||||
|
||||
// Sample the colors and map them to screen pixels.
|
||||
ColorVector screen_colors;
|
||||
for (int x = 0; x < rect.width(); ++x) {
|
||||
int r = 0;
|
||||
int g = 0;
|
||||
int b = 0;
|
||||
|
||||
int start = x * colors.size() / rect.width();
|
||||
int end = (x + 1) * colors.size() / rect.width();
|
||||
|
||||
if (start == end) end = qMin(start + 1, colors.size() - 1);
|
||||
|
||||
for (int j = start; j < end; j++) {
|
||||
r += colors[j].red();
|
||||
g += colors[j].green();
|
||||
b += colors[j].blue();
|
||||
}
|
||||
|
||||
const int n = qMax(1, end - start);
|
||||
screen_colors.append(QColor(r / n, g / n, b / n));
|
||||
}
|
||||
|
||||
// Draw the actual moodbar.
|
||||
for (int x = 0; x < rect.width(); x++) {
|
||||
int h, s, v;
|
||||
screen_colors[x].getHsv(&h, &s, &v);
|
||||
|
||||
for (int y = 0; y <= rect.height() / 2; y++) {
|
||||
float coeff = float(y) / float(rect.height() / 2);
|
||||
float coeff2 = 1.0f - ((1.0f - coeff) * (1.0f - coeff));
|
||||
coeff = 1.0f - (1.0f - coeff) / 2.0f;
|
||||
coeff2 = 1.f - (1.f - coeff2) / 2.0f;
|
||||
|
||||
p->setPen(QColor::fromHsv(h, qBound(0, int(float(s) * coeff), 255), qBound(0, int(255.f - (255.f - float(v)) * coeff2), 255)));
|
||||
|
||||
p->drawPoint(rect.left() + x, rect.top() + y);
|
||||
p->drawPoint(rect.left() + x, rect.top() + rect.height() - 1 - y);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
QImage MoodbarRenderer::RenderToImage(const ColorVector& colors, const QSize& size) {
|
||||
|
||||
QImage image(size, QImage::Format_ARGB32_Premultiplied);
|
||||
QPainter p(&image);
|
||||
Render(colors, &p, image.rect());
|
||||
p.end();
|
||||
return image;
|
||||
|
||||
}
|
||||
|
||||
QString MoodbarRenderer::StyleName(MoodbarStyle style) {
|
||||
|
||||
switch (style) {
|
||||
case Style_Normal:
|
||||
return QObject::tr("Normal");
|
||||
case Style_Angry:
|
||||
return QObject::tr("Angry");
|
||||
case Style_Frozen:
|
||||
return QObject::tr("Frozen");
|
||||
case Style_Happy:
|
||||
return QObject::tr("Happy");
|
||||
case Style_SystemPalette:
|
||||
return QObject::tr("System colors");
|
||||
|
||||
default:
|
||||
return QString();
|
||||
}
|
||||
|
||||
}
|
73
src/moodbar/moodbarrenderer.h
Normal file
73
src/moodbar/moodbarrenderer.h
Normal file
@ -0,0 +1,73 @@
|
||||
/* This file was part of Clementine.
|
||||
Copyright 2012, David Sansome <me@davidsansome.com>
|
||||
|
||||
Strawberry is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
Strawberry is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with Strawberry. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#ifndef MOODBARRENDERER_H
|
||||
#define MOODBARRENDERER_H
|
||||
|
||||
#include <QByteArray>
|
||||
#include <QImage>
|
||||
#include <QPixmap>
|
||||
#include <QMetaType>
|
||||
#include <QVector>
|
||||
#include <QColor>
|
||||
#include <QPainter>
|
||||
#include <QPalette>
|
||||
|
||||
typedef QVector<QColor> ColorVector;
|
||||
|
||||
class MoodbarRenderer {
|
||||
public:
|
||||
// These values are persisted. Remember to change moodbarsettingspage.ui when changing them.
|
||||
enum MoodbarStyle {
|
||||
Style_Normal = 0,
|
||||
Style_Angry,
|
||||
Style_Frozen,
|
||||
Style_Happy,
|
||||
Style_SystemPalette,
|
||||
StyleCount
|
||||
};
|
||||
|
||||
static const int kNumHues;
|
||||
|
||||
static QString StyleName(MoodbarStyle style);
|
||||
|
||||
static ColorVector Colors(const QByteArray& data, MoodbarStyle style, const QPalette& palette);
|
||||
static void Render(const ColorVector& colors, QPainter* p, const QRect& rect);
|
||||
static QImage RenderToImage(const ColorVector& colors, const QSize& size);
|
||||
|
||||
private:
|
||||
MoodbarRenderer();
|
||||
|
||||
struct StyleProperties {
|
||||
StyleProperties(int threshold = 0, int range_start = 0, int range_delta = 0, int sat = 0, int val = 0)
|
||||
: threshold_(threshold),
|
||||
range_start_(range_start),
|
||||
range_delta_(range_delta),
|
||||
sat_(sat),
|
||||
val_(val) {}
|
||||
|
||||
int threshold_;
|
||||
int range_start_;
|
||||
int range_delta_;
|
||||
int sat_;
|
||||
int val_;
|
||||
};
|
||||
};
|
||||
|
||||
Q_DECLARE_METATYPE(QVector<QColor>)
|
||||
|
||||
#endif // MOODBARRENDERER_H
|
@ -365,6 +365,12 @@ QVariant Playlist::data(const QModelIndex &index, int role) const {
|
||||
|
||||
}
|
||||
|
||||
#ifdef HAVE_MOODBAR
|
||||
void Playlist::MoodbarUpdated(const QModelIndex& index) {
|
||||
emit dataChanged(index.sibling(index.row(), Column_Mood), index.sibling(index.row(), Column_Mood));
|
||||
}
|
||||
#endif
|
||||
|
||||
bool Playlist::setData(const QModelIndex &index, const QVariant &value, int role) {
|
||||
|
||||
int row = index.row();
|
||||
@ -1183,6 +1189,7 @@ QString Playlist::column_name(Column column) {
|
||||
|
||||
case Column_Comment: return tr("Comment");
|
||||
case Column_Source: return tr("Source");
|
||||
case Column_Mood: return tr("Mood");
|
||||
default: qLog(Error) << "No such column" << column;;
|
||||
}
|
||||
return "";
|
||||
|
@ -124,6 +124,7 @@ class Playlist : public QAbstractListModel {
|
||||
Column_Comment,
|
||||
Column_Grouping,
|
||||
Column_Source,
|
||||
Column_Mood,
|
||||
ColumnCount
|
||||
};
|
||||
|
||||
@ -251,6 +252,11 @@ class Playlist : public QAbstractListModel {
|
||||
// Unregisters a SongInsertVetoListener object.
|
||||
void RemoveSongInsertVetoListener(SongInsertVetoListener *listener);
|
||||
|
||||
// Just emits the dataChanged() signal so the mood column is repainted.
|
||||
#ifdef HAVE_MOODBAR
|
||||
void MoodbarUpdated(const QModelIndex& index);
|
||||
#endif
|
||||
|
||||
// QAbstractListModel
|
||||
int rowCount(const QModelIndex& = QModelIndex()) const { return items_.count(); }
|
||||
int columnCount(const QModelIndex& = QModelIndex()) const { return ColumnCount; }
|
||||
|
@ -104,6 +104,12 @@ void PlaylistHeader::contextMenuEvent(QContextMenuEvent *e) {
|
||||
|
||||
void PlaylistHeader::AddColumnAction(int index) {
|
||||
|
||||
#ifndef HAVE_MOODBAR
|
||||
if (index == Playlist::Column_Mood) {
|
||||
return;
|
||||
}
|
||||
#endif
|
||||
|
||||
QString title(model()->headerData(index, Qt::Horizontal).toString());
|
||||
|
||||
QAction *action = menu_->addAction(title, show_mapper_, SLOT(map()));
|
||||
|
@ -77,6 +77,10 @@
|
||||
#include "settings/appearancesettingspage.h"
|
||||
#include "settings/playlistsettingspage.h"
|
||||
|
||||
#ifdef HAVE_MOODBAR
|
||||
# include "moodbar/moodbaritemdelegate.h"
|
||||
#endif
|
||||
|
||||
using std::sort;
|
||||
|
||||
const int PlaylistView::kGlowIntensitySteps = 24;
|
||||
@ -244,6 +248,10 @@ void PlaylistView::SetItemDelegates(CollectionBackend *backend) {
|
||||
|
||||
setItemDelegateForColumn(Playlist::Column_Source, new SongSourceDelegate(this));
|
||||
|
||||
#ifdef HAVE_MOODBAR
|
||||
setItemDelegateForColumn(Playlist::Column_Mood, new MoodbarItemDelegate(app_, this, this));
|
||||
#endif
|
||||
|
||||
}
|
||||
|
||||
void PlaylistView::SetPlaylist(Playlist *playlist) {
|
||||
@ -310,6 +318,7 @@ void PlaylistView::LoadGeometry() {
|
||||
header_->HideSection(Playlist::Column_LastPlayed);
|
||||
header_->HideSection(Playlist::Column_Comment);
|
||||
header_->HideSection(Playlist::Column_Grouping);
|
||||
header_->HideSection(Playlist::Column_Mood);
|
||||
|
||||
header_->moveSection(header_->visualIndex(Playlist::Column_Track), 0);
|
||||
setting_initial_header_layout_ = true;
|
||||
|
122
src/settings/moodbarsettingspage.cpp
Normal file
122
src/settings/moodbarsettingspage.cpp
Normal file
@ -0,0 +1,122 @@
|
||||
/*
|
||||
* Strawberry Music Player
|
||||
* This file was part of Clementine.
|
||||
* Copyright 2012, David Sansome <me@davidsansome.com>
|
||||
*
|
||||
* Strawberry is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* Strawberry is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with Strawberry. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
|
||||
#include "config.h"
|
||||
|
||||
#include <QIODevice>
|
||||
#include <QFile>
|
||||
#include <QByteArray>
|
||||
#include <QPixmap>
|
||||
#include <QPainter>
|
||||
#include <QSettings>
|
||||
|
||||
#include "core/application.h"
|
||||
#include "core/mainwindow.h"
|
||||
#include "core/iconloader.h"
|
||||
#include "core/logging.h"
|
||||
|
||||
#include "settingsdialog.h"
|
||||
|
||||
#ifdef HAVE_MOODBAR
|
||||
# include "moodbar/moodbarrenderer.h"
|
||||
#endif
|
||||
|
||||
#include "moodbarsettingspage.h"
|
||||
#include "ui_moodbarsettingspage.h"
|
||||
|
||||
const char *MoodbarSettingsPage::kSettingsGroup = "Moodbar";
|
||||
const int MoodbarSettingsPage::kMoodbarPreviewWidth = 150;
|
||||
const int MoodbarSettingsPage::kMoodbarPreviewHeight = 18;
|
||||
|
||||
MoodbarSettingsPage::MoodbarSettingsPage(SettingsDialog* dialog)
|
||||
: SettingsPage(dialog),
|
||||
ui_(new Ui_MoodbarSettingsPage),
|
||||
initialised_(false)
|
||||
{
|
||||
|
||||
ui_->setupUi(this);
|
||||
setWindowIcon(IconLoader::Load("moodbar"));
|
||||
|
||||
Load();
|
||||
|
||||
}
|
||||
|
||||
MoodbarSettingsPage::~MoodbarSettingsPage() { delete ui_; }
|
||||
|
||||
void MoodbarSettingsPage::Load() {
|
||||
|
||||
QSettings s;
|
||||
s.beginGroup(kSettingsGroup);
|
||||
ui_->moodbar_enabled->setChecked(s.value("enabled", false).toBool());
|
||||
ui_->moodbar_show->setChecked(s.value("show", false).toBool());
|
||||
ui_->moodbar_style->setCurrentIndex(s.value("style", 0).toInt());
|
||||
ui_->moodbar_save->setChecked(s.value("save", false).toBool());
|
||||
s.endGroup();
|
||||
|
||||
InitMoodbarPreviews();
|
||||
|
||||
}
|
||||
|
||||
void MoodbarSettingsPage::Save() {
|
||||
|
||||
QSettings s;
|
||||
s.beginGroup(kSettingsGroup);
|
||||
s.setValue("enabled", ui_->moodbar_enabled->isChecked());
|
||||
s.setValue("show", ui_->moodbar_show->isChecked());
|
||||
s.setValue("style", ui_->moodbar_style->currentIndex());
|
||||
s.setValue("save", ui_->moodbar_save->isChecked());
|
||||
s.endGroup();
|
||||
}
|
||||
|
||||
void MoodbarSettingsPage::Cancel() {}
|
||||
|
||||
void MoodbarSettingsPage::InitMoodbarPreviews() {
|
||||
|
||||
if (initialised_) return;
|
||||
initialised_ = true;
|
||||
|
||||
const QSize preview_size(kMoodbarPreviewWidth, kMoodbarPreviewHeight);
|
||||
ui_->moodbar_style->setIconSize(preview_size);
|
||||
|
||||
// Read the sample data
|
||||
QFile file(":/mood/sample.mood");
|
||||
if (!file.open(QIODevice::ReadOnly)) {
|
||||
qLog(Warning) << "Unable to open moodbar sample file";
|
||||
return;
|
||||
}
|
||||
QByteArray data(file.readAll());
|
||||
|
||||
// Render and set each preview
|
||||
for (int i = 0; i < MoodbarRenderer::StyleCount; ++i) {
|
||||
|
||||
const MoodbarRenderer::MoodbarStyle style = MoodbarRenderer::MoodbarStyle(i);
|
||||
const ColorVector colors = MoodbarRenderer::Colors(data, style, palette());
|
||||
|
||||
QPixmap pixmap(preview_size);
|
||||
QPainter p(&pixmap);
|
||||
MoodbarRenderer::Render(colors, &p, pixmap.rect());
|
||||
p.end();
|
||||
|
||||
ui_->moodbar_style->addItem(MoodbarRenderer::StyleName(style));
|
||||
ui_->moodbar_style->setItemData(i, pixmap, Qt::DecorationRole);
|
||||
|
||||
}
|
||||
|
||||
}
|
54
src/settings/moodbarsettingspage.h
Normal file
54
src/settings/moodbarsettingspage.h
Normal file
@ -0,0 +1,54 @@
|
||||
/*
|
||||
* Strawberry Music Player
|
||||
* This file was part of Clementine.
|
||||
* Copyright 2012, David Sansome <me@davidsansome.com>
|
||||
*
|
||||
* Strawberry is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* Strawberry is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with Strawberry. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
|
||||
#ifndef MOODBARSETTINGSPAGE_H
|
||||
#define MOODBARSETTINGSPAGE_H
|
||||
|
||||
#include "settingspage.h"
|
||||
|
||||
#include <QObject>
|
||||
|
||||
class Ui_MoodbarSettingsPage;
|
||||
|
||||
class MoodbarSettingsPage : public SettingsPage {
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
MoodbarSettingsPage(SettingsDialog* dialog);
|
||||
~MoodbarSettingsPage();
|
||||
|
||||
static const char *kSettingsGroup;
|
||||
|
||||
void Load();
|
||||
void Save();
|
||||
void Cancel();
|
||||
|
||||
private:
|
||||
static const int kMoodbarPreviewWidth;
|
||||
static const int kMoodbarPreviewHeight;
|
||||
|
||||
void InitMoodbarPreviews();
|
||||
|
||||
Ui_MoodbarSettingsPage* ui_;
|
||||
|
||||
bool initialised_;
|
||||
};
|
||||
|
||||
#endif // MOODBARSETTINGSPAGE_H
|
61
src/settings/moodbarsettingspage.ui
Normal file
61
src/settings/moodbarsettingspage.ui
Normal file
@ -0,0 +1,61 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<ui version="4.0">
|
||||
<class>MoodbarSettingsPage</class>
|
||||
<widget class="QWidget" name="MoodbarSettingsPage">
|
||||
<property name="geometry">
|
||||
<rect>
|
||||
<x>0</x>
|
||||
<y>0</y>
|
||||
<width>596</width>
|
||||
<height>666</height>
|
||||
</rect>
|
||||
</property>
|
||||
<property name="windowTitle">
|
||||
<string>Moodbar</string>
|
||||
</property>
|
||||
<layout class="QVBoxLayout" name="layout_moodbarsettingspage">
|
||||
<item>
|
||||
<widget class="QGroupBox" name="moodbar_group">
|
||||
<property name="title">
|
||||
<string>Moodbar</string>
|
||||
</property>
|
||||
<layout class="QFormLayout" name="layout_moodbar">
|
||||
<item row="1" column="0" colspan="2">
|
||||
<widget class="QCheckBox" name="moodbar_show">
|
||||
<property name="text">
|
||||
<string>Show a moodbar in the track progress bar</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="2" column="0">
|
||||
<widget class="QLabel" name="label_moodbar_style">
|
||||
<property name="text">
|
||||
<string>Moodbar style</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="2" column="1">
|
||||
<widget class="QComboBox" name="moodbar_style"/>
|
||||
</item>
|
||||
<item row="3" column="0" colspan="2">
|
||||
<widget class="QCheckBox" name="moodbar_save">
|
||||
<property name="text">
|
||||
<string>Save the .mood files directly in the songs folders</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="0" column="0">
|
||||
<widget class="QCheckBox" name="moodbar_enabled">
|
||||
<property name="text">
|
||||
<string>Enabled</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
<resources/>
|
||||
<connections/>
|
||||
</ui>
|
@ -65,6 +65,9 @@
|
||||
#ifdef HAVE_TIDAL
|
||||
# include "tidalsettingspage.h"
|
||||
#endif
|
||||
#ifdef HAVE_MOODBAR
|
||||
# include "moodbarsettingspage.h"
|
||||
#endif
|
||||
|
||||
#include "ui_settingsdialog.h"
|
||||
|
||||
@ -133,6 +136,10 @@ SettingsDialog::SettingsDialog(Application *app, QWidget *parent)
|
||||
AddPage(Page_GlobalShortcuts, new GlobalShortcutsSettingsPage(this), iface);
|
||||
#endif
|
||||
|
||||
#ifdef HAVE_MOODBAR
|
||||
AddPage(Page_Moodbar, new MoodbarSettingsPage(this), iface);
|
||||
#endif
|
||||
|
||||
#if defined(HAVE_TIDAL)
|
||||
QTreeWidgetItem *streaming = AddCategory(tr("Streaming"));
|
||||
#endif
|
||||
|
@ -83,6 +83,7 @@ class SettingsDialog : public QDialog {
|
||||
Page_Proxy,
|
||||
Page_Scrobbler,
|
||||
Page_Tidal,
|
||||
Page_Moodbar,
|
||||
};
|
||||
|
||||
enum Role {
|
||||
|
@ -35,11 +35,18 @@
|
||||
#include "clickablelabel.h"
|
||||
#include "tracksliderslider.h"
|
||||
|
||||
#ifdef HAVE_MOODBAR
|
||||
# include "moodbar/moodbarproxystyle.h"
|
||||
#endif
|
||||
|
||||
const char* TrackSlider::kSettingsGroup = "MainWindow";
|
||||
|
||||
TrackSlider::TrackSlider(QWidget* parent)
|
||||
: QWidget(parent),
|
||||
ui_(new Ui_TrackSlider),
|
||||
#ifdef HAVE_MOODBAR
|
||||
moodbar_style_(nullptr),
|
||||
#endif
|
||||
setting_value_(false),
|
||||
show_remaining_time_(true),
|
||||
slider_maximum_value_(0)
|
||||
@ -64,6 +71,9 @@ TrackSlider::~TrackSlider() {
|
||||
}
|
||||
|
||||
void TrackSlider::SetApplication(Application* app) {
|
||||
#ifdef HAVE_MOODBAR
|
||||
moodbar_style_ = new MoodbarProxyStyle(app, ui_->slider);
|
||||
#endif
|
||||
}
|
||||
|
||||
void TrackSlider::UpdateLabelWidth() {
|
||||
|
@ -34,6 +34,9 @@
|
||||
class QEvent;
|
||||
|
||||
class Application;
|
||||
#ifdef HAVE_MOODBAR
|
||||
class MoodbarProxyStyle;
|
||||
#endif
|
||||
class Ui_TrackSlider;
|
||||
|
||||
class TrackSlider : public QWidget {
|
||||
@ -51,6 +54,11 @@ class TrackSlider : public QWidget {
|
||||
// QObject
|
||||
bool event(QEvent *);
|
||||
|
||||
#ifdef HAVE_MOODBAR
|
||||
MoodbarProxyStyle *moodbar_style() const { return moodbar_style_; }
|
||||
#endif
|
||||
|
||||
|
||||
static const char* kSettingsGroup;
|
||||
|
||||
public slots:
|
||||
@ -75,6 +83,10 @@ class TrackSlider : public QWidget {
|
||||
private:
|
||||
Ui_TrackSlider* ui_;
|
||||
|
||||
#ifdef HAVE_MOODBAR
|
||||
MoodbarProxyStyle* moodbar_style_;
|
||||
#endif
|
||||
|
||||
bool setting_value_;
|
||||
bool show_remaining_time_;
|
||||
int slider_maximum_value_; //we cache it to avoid unnecessary updates
|
||||
|
Loading…
x
Reference in New Issue
Block a user