diff --git a/CMakeLists.txt b/CMakeLists.txt index f8388c393..3a9a8a589 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -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( diff --git a/README.md b/README.md index f06f3d873..6385eae31 100644 --- a/README.md +++ b/README.md @@ -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. diff --git a/cmake/FindFFTW3.cmake b/cmake/FindFFTW3.cmake new file mode 100644 index 000000000..e711dd3ab --- /dev/null +++ b/cmake/FindFFTW3.cmake @@ -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 +) diff --git a/data/data.qrc b/data/data.qrc index 717cb626b..4c858a706 100644 --- a/data/data.qrc +++ b/data/data.qrc @@ -32,5 +32,6 @@ pictures/nyancat.png pictures/rainbowdash.png fonts/HumongousofEternitySt.ttf + mood/sample.mood diff --git a/data/icons.qrc b/data/icons.qrc index fd9fee2da..bafe558fb 100644 --- a/data/icons.qrc +++ b/data/icons.qrc @@ -86,6 +86,7 @@ icons/128x128/tidal.png icons/128x128/scrobble.png icons/128x128/scrobble-disabled.png + icons/128x128/moodbar.png icons/64x64/albums.png icons/64x64/alsa.png icons/64x64/application-exit.png @@ -172,6 +173,7 @@ icons/64x64/tidal.png icons/64x64/scrobble.png icons/64x64/scrobble-disabled.png + icons/64x64/moodbar.png icons/48x48/albums.png icons/48x48/alsa.png icons/48x48/application-exit.png @@ -261,6 +263,7 @@ icons/48x48/tidal.png icons/48x48/scrobble.png icons/48x48/scrobble-disabled.png + icons/48x48/moodbar.png icons/32x32/albums.png icons/32x32/alsa.png icons/32x32/application-exit.png @@ -351,6 +354,7 @@ icons/32x32/tidal.png icons/32x32/scrobble.png icons/32x32/scrobble-disabled.png + icons/32x32/moodbar.png icons/22x22/albums.png icons/22x22/alsa.png icons/22x22/application-exit.png @@ -441,5 +445,6 @@ icons/22x22/tidal.png icons/22x22/scrobble.png icons/22x22/scrobble-disabled.png + icons/22x22/moodbar.png diff --git a/data/icons/128x128/moodbar.png b/data/icons/128x128/moodbar.png new file mode 100644 index 000000000..7d58dedc1 Binary files /dev/null and b/data/icons/128x128/moodbar.png differ diff --git a/data/icons/22x22/moodbar.png b/data/icons/22x22/moodbar.png new file mode 100644 index 000000000..0294b5af4 Binary files /dev/null and b/data/icons/22x22/moodbar.png differ diff --git a/data/icons/32x32/moodbar.png b/data/icons/32x32/moodbar.png new file mode 100644 index 000000000..1f9fe3a33 Binary files /dev/null and b/data/icons/32x32/moodbar.png differ diff --git a/data/icons/48x48/moodbar.png b/data/icons/48x48/moodbar.png new file mode 100644 index 000000000..f12bc9176 Binary files /dev/null and b/data/icons/48x48/moodbar.png differ diff --git a/data/icons/64x64/moodbar.png b/data/icons/64x64/moodbar.png new file mode 100644 index 000000000..76644bba2 Binary files /dev/null and b/data/icons/64x64/moodbar.png differ diff --git a/data/icons/full/moodbar.png b/data/icons/full/moodbar.png new file mode 100644 index 000000000..e4b207231 Binary files /dev/null and b/data/icons/full/moodbar.png differ diff --git a/data/mood/sample.mood b/data/mood/sample.mood new file mode 100755 index 000000000..c4eed190c Binary files /dev/null and b/data/mood/sample.mood differ diff --git a/debian/copyright b/debian/copyright index 69468178a..7dca90ef3 100644 --- a/debian/copyright +++ b/debian/copyright @@ -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 + 2006,2011 Stefan Kost + 2007-2009 Sebastian Dröge +License: GPL-2+ + Files: 3rdparty/SPMediaKeyTap/* Copyright: 2010, Spotify AB 2011, Joachim Bengtsson diff --git a/ext/gstmoodbar/CMakeLists.txt b/ext/gstmoodbar/CMakeLists.txt new file mode 100644 index 000000000..af95daa67 --- /dev/null +++ b/ext/gstmoodbar/CMakeLists.txt @@ -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) diff --git a/ext/gstmoodbar/gstfastspectrum.cpp b/ext/gstmoodbar/gstfastspectrum.cpp new file mode 100644 index 000000000..e1ebc1199 --- /dev/null +++ b/ext/gstmoodbar/gstfastspectrum.cpp @@ -0,0 +1,525 @@ +/* GStreamer + * Copyright (C) <1999> Erik Walthinsen + * <2006,2011> Stefan Kost + * <2007-2009> Sebastian Dröge + * + * 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 +#include + +#include +#include + +#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 , " + "Stefan Kost , " + "Sebastian Dröge "); + + 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( fftw_malloc(sizeof(double) * nfft)); + spectrum->fft_output =reinterpret_cast( fftw_malloc(sizeof(fftw_complex) * (nfft/2+1))); + + spectrum->spect_magnitude = new double[bands]{}; + + GstFastSpectrumClass* klass = reinterpret_cast(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(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; + +} diff --git a/ext/gstmoodbar/gstfastspectrum.h b/ext/gstmoodbar/gstfastspectrum.h new file mode 100644 index 000000000..27579675b --- /dev/null +++ b/ext/gstmoodbar/gstfastspectrum.h @@ -0,0 +1,98 @@ +/* GStreamer + * Copyright (C) <1999> Erik Walthinsen + * Copyright (C) <2009> Sebastian Dröge + * + * 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 + +#include +#include +#include + +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 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 */ + + /* */ + 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_ diff --git a/ext/gstmoodbar/gstmoodbarplugin.cpp b/ext/gstmoodbar/gstmoodbarplugin.cpp new file mode 100644 index 000000000..58928bdcf --- /dev/null +++ b/ext/gstmoodbar/gstmoodbarplugin.cpp @@ -0,0 +1,50 @@ +/* This file was part of Clementine. + Copyright 2014, David Sansome + + 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 . +*/ + +#include + +#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"); +} diff --git a/ext/gstmoodbar/gstmoodbarplugin.h b/ext/gstmoodbar/gstmoodbarplugin.h new file mode 100644 index 000000000..ee3b73cda --- /dev/null +++ b/ext/gstmoodbar/gstmoodbarplugin.h @@ -0,0 +1,25 @@ +/* This file was part of Clementine. + Copyright 2014, David Sansome + + 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 . +*/ + +#ifndef GST_MOODBAR_PLUGIN_H +#define GST_MOODBAR_PLUGIN_H + +extern "C" { + int gstfastspectrum_register_static(); +} + +#endif // GST_MOODBAR_PLUGIN_H diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index beea5a252..100c93b76 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -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) diff --git a/src/config.h.in b/src/config.h.in index a244388bc..9dd16481f 100644 --- a/src/config.h.in +++ b/src/config.h.in @@ -50,6 +50,8 @@ #cmakedefine HAVE_TIDAL +#cmakedefine HAVE_MOODBAR + #cmakedefine HAVE_KEYSYMDEF_H #cmakedefine HAVE_XF86KEYSYM_H diff --git a/src/core/application.cpp b/src/core/application.cpp index 353305d03..a95852167 100644 --- a/src/core/application.cpp +++ b/src/core/application.cpp @@ -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 tag_reader_client_; @@ -160,6 +172,11 @@ class ApplicationImpl { Lazy tidal_search_; #endif Lazy scrobbler_; +#ifdef HAVE_MOODBAR + Lazy moodbar_loader_; + Lazy moodbar_controller_; +#endif + Lazy 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 diff --git a/src/core/application.h b/src/core/application.h index 1275fe1d8..ade650653 100644 --- a/src/core/application.h +++ b/src/core/application.h @@ -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); diff --git a/src/core/mainwindow.cpp b/src/core/mainwindow.cpp index 4282d8937..d657b7fc8 100644 --- a/src/core/mainwindow.cpp +++ b/src/core/mainwindow.cpp @@ -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()); diff --git a/src/engine/gstengine.cpp b/src/engine/gstengine.cpp index c0a9633ad..8a777c6bd 100644 --- a/src/engine/gstengine.cpp +++ b/src/engine/gstengine.cpp @@ -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() { diff --git a/src/moodbar/moodbarbuilder.cpp b/src/moodbar/moodbarbuilder.cpp new file mode 100644 index 000000000..ef59c9437 --- /dev/null +++ b/src/moodbar/moodbarbuilder.cpp @@ -0,0 +1,199 @@ +/* This file was part of Clementine. + Copyright 2014, David Sansome + + 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 . +*/ + +#include + +#include +#include + +#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* 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; + +} diff --git a/src/moodbar/moodbarbuilder.h b/src/moodbar/moodbarbuilder.h new file mode 100644 index 000000000..f3b203ced --- /dev/null +++ b/src/moodbar/moodbarbuilder.h @@ -0,0 +1,50 @@ +/* This file was part of Clementine. + Copyright 2014, David Sansome + + 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 . +*/ + +#ifndef MOODBARBUILDER_H +#define MOODBARBUILDER_H + +#include +#include + +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* vals, double Rgb::*member); + + QList barkband_table_; + int bands_; + int rate_hz_; + + QList frames_; +}; + +#endif // MOODBARBUILDER_H diff --git a/src/moodbar/moodbarcontroller.cpp b/src/moodbar/moodbarcontroller.cpp new file mode 100644 index 000000000..82f83f49b --- /dev/null +++ b/src/moodbar/moodbarcontroller.cpp @@ -0,0 +1,91 @@ +/* This file was part of Clementine. + Copyright 2012, David Sansome + + 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 . +*/ + +#include +#include +#include + +#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()); + +} diff --git a/src/moodbar/moodbarcontroller.h b/src/moodbar/moodbarcontroller.h new file mode 100644 index 000000000..b4d45d963 --- /dev/null +++ b/src/moodbar/moodbarcontroller.h @@ -0,0 +1,47 @@ +/* This file was part of Clementine. + Copyright 2012, David Sansome + + 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 . +*/ + +#ifndef MOODBARCONTROLLER_H +#define MOODBARCONTROLLER_H + +#include +#include +#include + +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 diff --git a/src/moodbar/moodbaritemdelegate.cpp b/src/moodbar/moodbaritemdelegate.cpp new file mode 100644 index 000000000..eca164b7e --- /dev/null +++ b/src/moodbar/moodbaritemdelegate.cpp @@ -0,0 +1,274 @@ +/* This file was part of Clementine. + Copyright 2012, David Sansome + + 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 . +*/ + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#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(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(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 future = QtConcurrent::run(MoodbarRenderer::Colors, bytes, style_, qApp->palette()); + NewClosure(future, this, SLOT(ColorsLoaded(QUrl, QFuture)), url, future); + +} + +void MoodbarItemDelegate::ColorsLoaded(const QUrl& url, QFuture 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 future = QtConcurrent::run(MoodbarRenderer::RenderToImage, data->colors_, data->desired_size_); + NewClosure(future, this, SLOT(ImageLoaded(QUrl, QFuture)), url, future); + +} + +void MoodbarItemDelegate::ImageLoaded(const QUrl& url, QFuture 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); + } + } + +} diff --git a/src/moodbar/moodbaritemdelegate.h b/src/moodbar/moodbaritemdelegate.h new file mode 100644 index 000000000..0fb2830b0 --- /dev/null +++ b/src/moodbar/moodbaritemdelegate.h @@ -0,0 +1,90 @@ +/* This file is part of Clementine. + Copyright 2012, David Sansome + + 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 . +*/ + +#ifndef MOODBARITEMDELEGATE_H +#define MOODBARITEMDELEGATE_H + +#include "moodbarrenderer.h" + +#include +#include +#include +#include +#include +#include +#include +#include + +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 future); + void ImageLoaded(const QUrl& url, QFuture future); + + private: + struct Data { + Data(); + + enum State { + State_None, + State_CannotLoad, + State_LoadingData, + State_LoadingColors, + State_LoadingImage, + State_Loaded + }; + + QSet 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 data_; + + MoodbarRenderer::MoodbarStyle style_; +}; + +#endif // MOODBARITEMDELEGATE_H diff --git a/src/moodbar/moodbarloader.cpp b/src/moodbar/moodbarloader.cpp new file mode 100644 index 000000000..3587d283f --- /dev/null +++ b/src/moodbar/moodbarloader.cpp @@ -0,0 +1,203 @@ +/* This file was part of Clementine. + Copyright 2012, David Sansome + + 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 . +*/ + +#include "moodbarloader.h" + +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "core/application.h" +#include "core/closure.h" +#include "core/logging.h" + +#include "moodbarpipeline.h" + +#include "settings/moodbarsettingspage.h" + +#ifdef Q_OS_WIN32 +# include +#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 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(); + +} diff --git a/src/moodbar/moodbarloader.h b/src/moodbar/moodbarloader.h new file mode 100644 index 000000000..f818f2759 --- /dev/null +++ b/src/moodbar/moodbarloader.h @@ -0,0 +1,79 @@ +/* This file was part of Clementine. + Copyright 2012, David Sansome + + 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 . +*/ + +#ifndef MOODBARLOADER_H +#define MOODBARLOADER_H + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +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 requests_; + QList queued_requests_; + QSet active_requests_; + + bool enabled_; + bool save_; +}; + +#endif // MOODBARLOADER_H diff --git a/src/moodbar/moodbarpipeline.cpp b/src/moodbar/moodbarpipeline.cpp new file mode 100644 index 000000000..c818a2e36 --- /dev/null +++ b/src/moodbar/moodbarpipeline.cpp @@ -0,0 +1,227 @@ +/* This file was part of Clementine. + Copyright 2012, David Sansome + + 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 . +*/ + +#include "moodbarpipeline.h" + +#include +#include +#include +#include +#include + +#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(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(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; + } + +} diff --git a/src/moodbar/moodbarpipeline.h b/src/moodbar/moodbarpipeline.h new file mode 100644 index 000000000..07e278d25 --- /dev/null +++ b/src/moodbar/moodbarpipeline.h @@ -0,0 +1,79 @@ +/* This file was part of Clementine. + Copyright 2012, David Sansome + + 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 . +*/ + +#ifndef MOODBARPIPELINE_H +#define MOODBARPIPELINE_H + +#include +#include +#include +#include + +#include +#include + +#include + +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 builder_; + + bool success_; + bool running_; + QByteArray data_; +}; + +#endif // MOODBARPIPELINE_H diff --git a/src/moodbar/moodbarproxystyle.cpp b/src/moodbar/moodbarproxystyle.cpp new file mode 100644 index 000000000..79419b617 --- /dev/null +++ b/src/moodbar/moodbarproxystyle.cpp @@ -0,0 +1,397 @@ +/* This file was part of Clementine. + Copyright 2012, David Sansome + + 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 . +*/ + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#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(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(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(this)->Render(control, qstyleoption_cast(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(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(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(); + +} diff --git a/src/moodbar/moodbarproxystyle.h b/src/moodbar/moodbarproxystyle.h new file mode 100644 index 000000000..6c90d99c6 --- /dev/null +++ b/src/moodbar/moodbarproxystyle.h @@ -0,0 +1,103 @@ +/* This file was part of Clementine. + Copyright 2012, David Sansome + + 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 . +*/ + +#ifndef MOODBARPROXYSTYLE_H +#define MOODBARPROXYSTYLE_H + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#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 diff --git a/src/moodbar/moodbarrenderer.cpp b/src/moodbar/moodbarrenderer.cpp new file mode 100644 index 000000000..2425e0501 --- /dev/null +++ b/src/moodbar/moodbarrenderer.cpp @@ -0,0 +1,178 @@ +/* This file was part of Clementine. + Copyright 2012, David Sansome + + 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 . +*/ + +#include "moodbarrenderer.h" + +#include +#include +#include +#include +#include +#include + +#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(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(); + } + +} diff --git a/src/moodbar/moodbarrenderer.h b/src/moodbar/moodbarrenderer.h new file mode 100644 index 000000000..d543728ee --- /dev/null +++ b/src/moodbar/moodbarrenderer.h @@ -0,0 +1,73 @@ +/* This file was part of Clementine. + Copyright 2012, David Sansome + + 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 . +*/ + +#ifndef MOODBARRENDERER_H +#define MOODBARRENDERER_H + +#include +#include +#include +#include +#include +#include +#include +#include + +typedef QVector 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) + +#endif // MOODBARRENDERER_H diff --git a/src/playlist/playlist.cpp b/src/playlist/playlist.cpp index 9bfe637ff..a83305647 100644 --- a/src/playlist/playlist.cpp +++ b/src/playlist/playlist.cpp @@ -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 ""; diff --git a/src/playlist/playlist.h b/src/playlist/playlist.h index 944ca0608..ab4f8f296 100644 --- a/src/playlist/playlist.h +++ b/src/playlist/playlist.h @@ -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; } diff --git a/src/playlist/playlistheader.cpp b/src/playlist/playlistheader.cpp index 9d9872f72..e303b1f3e 100644 --- a/src/playlist/playlistheader.cpp +++ b/src/playlist/playlistheader.cpp @@ -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())); diff --git a/src/playlist/playlistview.cpp b/src/playlist/playlistview.cpp index 9a3c20c4e..4a4c8616e 100644 --- a/src/playlist/playlistview.cpp +++ b/src/playlist/playlistview.cpp @@ -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; diff --git a/src/settings/moodbarsettingspage.cpp b/src/settings/moodbarsettingspage.cpp new file mode 100644 index 000000000..09b3ae463 --- /dev/null +++ b/src/settings/moodbarsettingspage.cpp @@ -0,0 +1,122 @@ +/* + * Strawberry Music Player + * This file was part of Clementine. + * Copyright 2012, David Sansome + * + * 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 . + * + */ + +#include "config.h" + +#include +#include +#include +#include +#include +#include + +#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); + + } + +} diff --git a/src/settings/moodbarsettingspage.h b/src/settings/moodbarsettingspage.h new file mode 100644 index 000000000..05725b619 --- /dev/null +++ b/src/settings/moodbarsettingspage.h @@ -0,0 +1,54 @@ +/* + * Strawberry Music Player + * This file was part of Clementine. + * Copyright 2012, David Sansome + * + * 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 . + * + */ + +#ifndef MOODBARSETTINGSPAGE_H +#define MOODBARSETTINGSPAGE_H + +#include "settingspage.h" + +#include + +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 diff --git a/src/settings/moodbarsettingspage.ui b/src/settings/moodbarsettingspage.ui new file mode 100644 index 000000000..25ff81704 --- /dev/null +++ b/src/settings/moodbarsettingspage.ui @@ -0,0 +1,61 @@ + + + MoodbarSettingsPage + + + + 0 + 0 + 596 + 666 + + + + Moodbar + + + + + + Moodbar + + + + + + Show a moodbar in the track progress bar + + + + + + + Moodbar style + + + + + + + + + + Save the .mood files directly in the songs folders + + + + + + + Enabled + + + + + + + + + + + diff --git a/src/settings/settingsdialog.cpp b/src/settings/settingsdialog.cpp index 2afb02fbe..4d5eab2f2 100644 --- a/src/settings/settingsdialog.cpp +++ b/src/settings/settingsdialog.cpp @@ -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 diff --git a/src/settings/settingsdialog.h b/src/settings/settingsdialog.h index 3c446a090..02820655e 100644 --- a/src/settings/settingsdialog.h +++ b/src/settings/settingsdialog.h @@ -83,6 +83,7 @@ class SettingsDialog : public QDialog { Page_Proxy, Page_Scrobbler, Page_Tidal, + Page_Moodbar, }; enum Role { diff --git a/src/widgets/trackslider.cpp b/src/widgets/trackslider.cpp index 754b5a3b1..88fb4c3bc 100644 --- a/src/widgets/trackslider.cpp +++ b/src/widgets/trackslider.cpp @@ -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() { diff --git a/src/widgets/trackslider.h b/src/widgets/trackslider.h index 14ebeb9e9..e0849750a 100644 --- a/src/widgets/trackslider.h +++ b/src/widgets/trackslider.h @@ -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