Merge branch 'master' into qt5

This commit is contained in:
Chocobozzz 2015-09-16 17:40:54 +02:00
commit 631a6cdabd
186 changed files with 77337 additions and 69995 deletions

View File

@ -2,9 +2,11 @@ Next release:
Major features:
* Vk.com support
* Seafile support
* Amazon Cloud Drive support
* Add Ampache compatibility (through Subsonic service)
* Add new analyzer "Rainbow Dash"
* Answer to the ultimate question of life, the universe and everything
* Add "Psychedelic Colour" mode to all analyzers
Other features:
* Add left click to fullsize cover on playing widget
@ -24,9 +26,8 @@ Next release:
* Add a source icon for CD tracks
* Allow user to remove directories
* Add ability to remove unavailable items from playlist
* Adds an import button to the transcode UI, allowing the user to pull in
all files in a folder heirarchy to be transcoded
* Allow user to pull in all files in a folder heirarchy to be transcoded
* Add an import button to the transcode UI, allowing the user to pull in
all files in a folder hierarchy to be transcoded
* Make it impossible to collapse either side of the MainWindow splitter
* Add menu items for updating and doing a full rescan of Google Drive
* Increase Soundcloud cover image size
@ -52,6 +53,22 @@ Next release:
* Ability to add tracks to Spotify starred playlist by drag and drop
* Add HipHop and Kuduro equalizers
* Add AZLyrics lyric provider
* Remember current playlist between restarts
* (OSX) Use Alt+Tab to switch between playlist tabs
* IDv3 tag lyrics support
* Improve handling of Spotify Top Tracks and compilations
* Scroll to last played track when switching playlists
* Add stop after each song repeat mode
* Sort discs numerically when using Group by disc
* Add ability for sort by group and performer in the library view
* Parse the year of a disc from musicbrainz
* Add track intro mode
* Add ability to add a search term with tab and space in the smart
playlist window
* Add love/ban (lastfm) global shortcuts
* Add support for "original year" tags
* Send album artist to Last.fm with liblastfm >= 1.0.0
* Add sample rate selection
Bugfixes:
* Fix crash when click on a SoundCloud entry in internet tab
@ -90,8 +107,6 @@ Next release:
* Fix moodbars not generating correctly
* Fix socket leak in moodbar
* Fix memory leak in tagreader
* Remove Ubuntu One support
* Remove Discogs support
* Fix crash when trying to fingerprint but missing a plugin
* Fix infinite scan with Subsonic when the library is empty
* Fix shortcut/media keys issues on Mac
@ -101,6 +116,26 @@ Next release:
* Fix laggy interface on Mac
* Fix crash in GrooveShark
* Fix playback breaks in Spotify
* Fix memory leaks
* Fix crash when stopping song that is fading after pausing
* Fix crash when trying to download a track but there is no current one playing
* (OSX) Fix Soundcloud API Search which simply doesn't work
* Fix default spinner gif image which shows white pixels around the image
* Fix setting album artist tag for FLAC files if it already exists
* Fix crash when Clementine lists the albums on Ampache
* Fix Last.fm scrobbling after seek
* Fix metadata not processed properly for some streams (Akamai)
* Fix save state when the song was paused
* (Windows) Fix context menu for the "now playing widget"
* Fix some issues in Boom and Turbine analyzers
* Fix song continuously rewinding when seeking using keyboard arrow keys
* Fix OSD re-posistioning which doesn't work on multiple monitors
* Fix Sonogram state while paused
Removed features:
* Remove Ubuntu One support
* Remove Discogs support
* Remove GrooveShark support
Build system changes:
* Update to gstreamer 1.0
@ -113,6 +148,13 @@ Next release:
dependency
* (Debian/Ubuntu) Add libmygpo-qt-dev (=> 1.0.7)
* Remove internal copy of libechonest and add it as dependency
* (Windows) Uninstall a previous install of Clementine when installing a new
version
* (Windows) Remember the last installation path
* (GNU/Linux) Follow freedesktop.org specifications for icons
* (GNU/Linux) Add a 128x128 version of the Clementine icon
* (OSX) Use a GTlsDatabase for gstreamer SSL
* Use libcrypto++ instead of QCA

View File

@ -0,0 +1,19 @@
-----BEGIN CERTIFICATE-----
MIIDIDCCAomgAwIBAgIENd70zzANBgkqhkiG9w0BAQUFADBOMQswCQYDVQQGEwJV
UzEQMA4GA1UEChMHRXF1aWZheDEtMCsGA1UECxMkRXF1aWZheCBTZWN1cmUgQ2Vy
dGlmaWNhdGUgQXV0aG9yaXR5MB4XDTk4MDgyMjE2NDE1MVoXDTE4MDgyMjE2NDE1
MVowTjELMAkGA1UEBhMCVVMxEDAOBgNVBAoTB0VxdWlmYXgxLTArBgNVBAsTJEVx
dWlmYXggU2VjdXJlIENlcnRpZmljYXRlIEF1dGhvcml0eTCBnzANBgkqhkiG9w0B
AQEFAAOBjQAwgYkCgYEAwV2xWGcIYu6gmi0fCG2RFGiYCh7+2gRvE4RiIcPRfM6f
BeC4AfBONOziipUEZKzxa1NfBbPLZ4C/QgKO/t0BCezhABRP/PvwDN1Dulsr4R+A
cJkVV5MW8Q+XarfCaCMczE1ZMKxRHjuvK9buY0V7xdlfUNLjUA86iOe/FP3gx7kC
AwEAAaOCAQkwggEFMHAGA1UdHwRpMGcwZaBjoGGkXzBdMQswCQYDVQQGEwJVUzEQ
MA4GA1UEChMHRXF1aWZheDEtMCsGA1UECxMkRXF1aWZheCBTZWN1cmUgQ2VydGlm
aWNhdGUgQXV0aG9yaXR5MQ0wCwYDVQQDEwRDUkwxMBoGA1UdEAQTMBGBDzIwMTgw
ODIyMTY0MTUxWjALBgNVHQ8EBAMCAQYwHwYDVR0jBBgwFoAUSOZo+SvSspXXR9gj
IBBPM5iQn9QwHQYDVR0OBBYEFEjmaPkr0rKV10fYIyAQTzOYkJ/UMAwGA1UdEwQF
MAMBAf8wGgYJKoZIhvZ9B0EABA0wCxsFVjMuMGMDAgbAMA0GCSqGSIb3DQEBBQUA
A4GBAFjOKer89961zgK5F7WF0bnj4JXMJTENAKaSbn+2kmOeUJXRmm/kEd5jhW6Y
7qj/WsjTVbJmcVfewCHrPSqnI0kBBIZCe/zuf6IWUrVnZ9NA2zsmWLIodz2uFHdh
1voqZiegDfqnc1zqcPGUIWVEX/r87yloqaKHee9570+sB3c4
-----END CERTIFICATE-----

View File

@ -1,5 +1,6 @@
<RCC>
<qresource prefix="/">
<file>Equifax_Secure_Certificate_Authority.pem</file>
<file>blank.ttf</file>
<file>clementine_remote_qr.png</file>
<file>clementine-spotify-public.pem</file>
@ -330,7 +331,6 @@
<file>providers/myspace.png</file>
<file>providers/podcast16.png</file>
<file>providers/podcast32.png</file>
<file>providers/radiogfm.png</file>
<file>providers/rockradio.png</file>
<file>providers/skydrive.png</file>
<file>providers/somafm.png</file>
@ -388,6 +388,7 @@
<file>schema/schema-49.sql</file>
<file>schema/schema-4.sql</file>
<file>schema/schema-5.sql</file>
<file>schema/schema-50.sql</file>
<file>schema/schema-6.sql</file>
<file>schema/schema-7.sql</file>
<file>schema/schema-8.sql</file>
@ -406,7 +407,6 @@
<file>volumeslider-gradient.png</file>
<file>volumeslider-handle_glow.png</file>
<file>volumeslider-handle.png</file>
<file>volumeslider-inset.png</file>
<file>vk/add.png</file>
<file>vk/bookmarks.png</file>
<file>vk/delete.png</file>

View File

@ -51,7 +51,7 @@
<invalidIndicator value="&quot;iTotalRecords&quot;: 0"/>
<invalidIndicator value="lyrics not available"/>
</provider>
<provider name="letras.mus.br" title="" charset="iso-8859-1" url="http://letras.terra.com.br/winamp.php?musica={title}&amp;artista={artist}">
<provider name="letras.mus.br" title="" charset="utf-8" url="http://letras.terra.com.br/winamp.php?musica={title}&amp;artista={artist}">
<urlFormat replace="_@,;&amp;\/&quot;" with="_"/>
<urlFormat replace=" " with="+"/>
<extract>
@ -207,7 +207,7 @@
<invalidIndicator value="Sorry, we have no"/>
<invalidIndicator value="This is an upcoming album and we do not have the"/>
</provider>
<provider name="tekstowo.pl (Polish translations)" title="{artist} - {title} - tekst" charset="utf-8" url="http://www.tekstowo.pl/piosenka,{artist},{title}.html">
<provider name="tekstowo.pl (Original lyric language)" title="{artist} - {title} - tekst" charset="utf-8" url="http://www.tekstowo.pl/piosenka,{artist},{title}.html">
<urlFormat replace=" _@,;&amp;\/'&quot;." with="_"/>
<extract>
<item begin="&lt;div class=&quot;song-text&quot;&gt;" end="&lt;a href=&quot;javascript:;&quot;"/>
@ -219,6 +219,15 @@
<item begin="&lt;h2&gt;" end="&lt;/h2&gt;&lt;br /&gt;"/>
</exclude>
</provider>
<provider name="tekstowo.pl (Translated to Polish)" title="{artist} - {title} - tekst" charset="utf-8" url="http://www.tekstowo.pl/piosenka,{artist},{title}.html">
<urlFormat replace=" _@,;&amp;\/'&quot;." with="_"/>
<extract>
<item begin="&lt;div id=&quot;translation&quot; class=" end="&lt;a href="/>
</extract>
<exclude>
<item begin="&quot;id-" end="&quot;&gt;"/>
</exclude>
</provider>
<provider name="teksty.org" title="{artist} - {title} - tekst" charset="utf-8" url="http://teksty.org/{artist},{title},tekst-piosenki">
<urlFormat replace=" _@,;&amp;\/&quot;'" with="-"/>
<urlFormat replace="." with=""/>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 676 B

View File

@ -59,7 +59,10 @@ CREATE TABLE device_%deviceid_songs (
performer TEXT,
grouping TEXT,
lyrics TEXT
lyrics TEXT,
originalyear INTEGER,
effective_originalyear INTEGER
);
CREATE INDEX idx_device_%deviceid_songs_album ON device_%deviceid_songs (album);

View File

@ -46,7 +46,10 @@ CREATE TABLE jamendo.songs (
performer TEXT,
grouping TEXT,
lyrics TEXT
lyrics TEXT,
originalyear INTEGER,
effective_originalyear INTEGER
);
CREATE VIRTUAL TABLE jamendo.songs_fts USING fts3(

View File

@ -0,0 +1,9 @@
ALTER TABLE %allsongstables ADD COLUMN originalyear INTEGER;
ALTER TABLE %allsongstables ADD COLUMN effective_originalyear INTEGER;
UPDATE songs SET originalyear = -1;
UPDATE songs SET effective_originalyear = -1;
UPDATE schema_version SET version=50;

Binary file not shown.

Before

Width:  |  Height:  |  Size: 203 B

After

Width:  |  Height:  |  Size: 384 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 707 B

View File

@ -281,6 +281,7 @@ Section "Clementine" Clementine
File "libgnutls-28.dll"
File "libgobject-2.0-0.dll"
File "libgpg-error-0.dll"
File "libgpod.dll"
File "libgstapp-1.0-0.dll"
File "libgstaudio-1.0-0.dll"
File "libgstbase-1.0-0.dll"
@ -314,6 +315,7 @@ Section "Clementine" Clementine
File "libqjson.dll"
File "libspeex-1.dll"
File "libspotify.dll"
File "libsqlite3-0.dll"
File "libstdc++-6.dll"
File "libtag.dll"
File "libtasn1-6.dll"
@ -1091,6 +1093,7 @@ Section "Uninstall"
Delete "$INSTDIR\libgnutls-28.dll"
Delete "$INSTDIR\libgobject-2.0-0.dll"
Delete "$INSTDIR\libgpg-error-0.dll"
Delete "$INSTDIR\libgpod.dll"
Delete "$INSTDIR\libgstapp-1.0-0.dll"
Delete "$INSTDIR\libgstaudio-1.0-0.dll"
Delete "$INSTDIR\libgstbase-1.0-0.dll"

View File

@ -0,0 +1,19 @@
-----BEGIN CERTIFICATE-----
MIIDIDCCAomgAwIBAgIENd70zzANBgkqhkiG9w0BAQUFADBOMQswCQYDVQQGEwJV
UzEQMA4GA1UEChMHRXF1aWZheDEtMCsGA1UECxMkRXF1aWZheCBTZWN1cmUgQ2Vy
dGlmaWNhdGUgQXV0aG9yaXR5MB4XDTk4MDgyMjE2NDE1MVoXDTE4MDgyMjE2NDE1
MVowTjELMAkGA1UEBhMCVVMxEDAOBgNVBAoTB0VxdWlmYXgxLTArBgNVBAsTJEVx
dWlmYXggU2VjdXJlIENlcnRpZmljYXRlIEF1dGhvcml0eTCBnzANBgkqhkiG9w0B
AQEFAAOBjQAwgYkCgYEAwV2xWGcIYu6gmi0fCG2RFGiYCh7+2gRvE4RiIcPRfM6f
BeC4AfBONOziipUEZKzxa1NfBbPLZ4C/QgKO/t0BCezhABRP/PvwDN1Dulsr4R+A
cJkVV5MW8Q+XarfCaCMczE1ZMKxRHjuvK9buY0V7xdlfUNLjUA86iOe/FP3gx7kC
AwEAAaOCAQkwggEFMHAGA1UdHwRpMGcwZaBjoGGkXzBdMQswCQYDVQQGEwJVUzEQ
MA4GA1UEChMHRXF1aWZheDEtMCsGA1UECxMkRXF1aWZheCBTZWN1cmUgQ2VydGlm
aWNhdGUgQXV0aG9yaXR5MQ0wCwYDVQQDEwRDUkwxMBoGA1UdEAQTMBGBDzIwMTgw
ODIyMTY0MTUxWjALBgNVHQ8EBAMCAQYwHwYDVR0jBBgwFoAUSOZo+SvSspXXR9gj
IBBPM5iQn9QwHQYDVR0OBBYEFEjmaPkr0rKV10fYIyAQTzOYkJ/UMAwGA1UdEwQF
MAMBAf8wGgYJKoZIhvZ9B0EABA0wCxsFVjMuMGMDAgbAMA0GCSqGSIb3DQEBBQUA
A4GBAFjOKer89961zgK5F7WF0bnj4JXMJTENAKaSbn+2kmOeUJXRmm/kEd5jhW6Y
7qj/WsjTVbJmcVfewCHrPSqnI0kBBIZCe/zuf6IWUrVnZ9NA2zsmWLIodz2uFHdh
1voqZiegDfqnc1zqcPGUIWVEX/r87yloqaKHee9570+sB3c4
-----END CERTIFICATE-----

View File

@ -1,5 +1,6 @@
<RCC>
<qresource prefix="/certs">
<file>Equifax_Secure_Certificate_Authority.pem</file>
<file>godaddy-root.pem</file>
</qresource>
</RCC>

View File

@ -57,6 +57,8 @@ int main(int argc, char** argv) {
QSslSocket::addDefaultCaCertificates(
QSslCertificate::fromPath(":/certs/godaddy-root.pem", QSsl::Pem));
QSslSocket::addDefaultCaCertificates(QSslCertificate::fromPath(
":/certs/Equifax_Secure_Certificate_Authority.pem", QSsl::Pem));
TagReaderWorker worker(&socket);

View File

@ -17,8 +17,6 @@
#include "closure.h"
#include <QTimer>
#include "core/timeconstants.h"
namespace _detail {
@ -65,11 +63,3 @@ void DoInAMinuteOrSo(QObject* receiver, const char* slot) {
int msec = (60 + (qrand() % 60)) * kMsecPerSec;
DoAfter(receiver, slot, msec);
}
void DoAfter(std::function<void()> callback, int msec) {
QTimer* timer = new QTimer;
timer->setSingleShot(true);
NewClosure(timer, SIGNAL(timeout()), callback);
QObject::connect(timer, SIGNAL(timeout()), timer, SLOT(deleteLater()));
timer->start(msec);
}

View File

@ -18,12 +18,14 @@
#ifndef CLOSURE_H
#define CLOSURE_H
#include <chrono>
#include <functional>
#include <memory>
#include <QMetaMethod>
#include <QObject>
#include <QSharedPointer>
#include <QTimer>
namespace _detail {
@ -188,7 +190,19 @@ _detail::ClosureBase* NewClosure(QObject* sender, const char* signal,
}
void DoAfter(QObject* receiver, const char* slot, int msec);
void DoAfter(std::function<void()> callback, int msec);
void DoAfter(std::function<void()> callback, std::chrono::milliseconds msec);
void DoInAMinuteOrSo(QObject* receiver, const char* slot);
template <typename R, typename P>
void DoAfter(
std::function<void()> callback, std::chrono::duration<R, P> duration) {
QTimer* timer = new QTimer;
timer->setSingleShot(true);
NewClosure(timer, SIGNAL(timeout()), callback);
QObject::connect(timer, SIGNAL(timeout()), timer, SLOT(deleteLater()));
std::chrono::milliseconds msec =
std::chrono::duration_cast<std::chrono::milliseconds>(duration);
timer->start(msec.count());
}
#endif // CLOSURE_H

View File

@ -285,3 +285,17 @@ QDebug CreateLoggerDebug(int line, const char *class_name) { return qCreateLogge
#endif // QT_NO_DEBUG_OUTPUT
} // namespace logging
namespace {
template<typename T>
QString print_duration(T duration, const std::string& unit) {
return QString("%1%2").arg(duration.count()).arg(unit.c_str());
}
} // namespace
QDebug operator<<(QDebug dbg, std::chrono::seconds secs) {
dbg.nospace() << print_duration(secs, "s");
return dbg.space();
}

View File

@ -21,6 +21,9 @@
#ifndef LOGGING_H
#define LOGGING_H
#include <chrono>
#include <string>
#include <QDebug>
#ifdef QT_NO_DEBUG_STREAM
@ -84,4 +87,6 @@ void GLog(const char* domain, int level, const char* message, void* user_data);
extern const char* kDefaultLogLevels;
}
QDebug operator<<(QDebug debug, std::chrono::seconds secs);
#endif // LOGGING_H

View File

@ -69,6 +69,24 @@ enum EngineState {
// Song Metadata
message SongMetadata {
enum Type {
UNKNOWN = 0;
ASF = 1;
FLAC = 2;
MP4 = 3;
MPC = 4;
MPEG = 5;
OGGFLAC = 6;
OGGSPEEX = 7;
OGGVORBIS = 8;
AIFF = 9;
WAV = 10;
TRUEAUDIO = 11;
CDDA = 12;
OGGOPUS = 13;
STREAM = 99;
}
optional int32 id = 1; // unique id of the song
optional int32 index = 2; // Index of the current row of the active playlist
optional string title = 3;
@ -88,6 +106,9 @@ message SongMetadata {
optional int32 file_size = 17;
optional float rating = 18; // 0 (0 stars) to 1 (5 stars)
optional string url = 19;
optional string art_automatic = 20;
optional string art_manual = 21;
optional Type type = 22;
}
// Playlist informations
@ -106,6 +127,7 @@ enum RepeatMode {
Repeat_Album = 2;
Repeat_Playlist = 3;
Repeat_OneByOne = 4;
Repeat_Intro = 5;
}
// Valid Shuffle modes
@ -217,6 +239,7 @@ message RequestInsertUrls {
optional int32 position = 3 [default=-1];
optional bool play_now = 4 [default=false];
optional bool enqueue = 5 [default=false];
repeated SongMetadata songs = 6;
}
// Client want to change track
@ -320,7 +343,7 @@ message ResponseGlobalSearchStatus {
// The message itself
message Message {
optional int32 version = 1 [default=20];
optional int32 version = 1 [default=21];
optional MsgType type = 2 [default=UNKNOWN]; // What data is in the message?
optional RequestConnect request_connect = 21;

View File

@ -25,6 +25,7 @@
#include <QNetworkAccessManager>
#include <QTextCodec>
#include <QUrl>
#include <QVector>
#include <aifffile.h>
#include <asffile.h>
@ -48,6 +49,7 @@
#include <textidentificationframe.h>
#include <trueaudiofile.h>
#include <tstring.h>
#include <unsynchronizedlyricsframe.h>
#include <vorbisfile.h>
#include <wavfile.h>
@ -106,6 +108,14 @@ const char* TagReader::kMP4_FMPS_Playcount_ID =
const char* TagReader::kMP4_FMPS_Score_ID =
"----:com.apple.iTunes:FMPS_Rating_Amarok_Score";
namespace {
// Tags containing the year the album was originally released (in contrast to
// other tags that contain the release year of the current edition)
const char* kMP4_OriginalYear_ID = "----:com.apple.iTunes:ORIGINAL YEAR";
const char* kASF_OriginalDate_ID = "WM/OriginalReleaseTime";
const char* kASF_OriginalYear_ID = "WM/OriginalReleaseYear";
}
TagReader::TagReader()
: factory_(new TagLibFileRefFactory),
network_(new QNetworkAccessManager),
@ -189,11 +199,21 @@ void TagReader::ReadFile(const QString& filename,
compilation =
TStringToQString(map["TCMP"].front()->toString()).trimmed();
if (!map["TDOR"].isEmpty()) {
song->set_originalyear(
map["TDOR"].front()->toString().substr(0, 4).toInt());
} else if (!map["TORY"].isEmpty()) {
song->set_originalyear(
map["TORY"].front()->toString().substr(0, 4).toInt());
}
if (!map["USLT"].isEmpty()) {
lyrics = TStringToQString((map["USLT"].front())->toString()).trimmed();
qLog(Debug) << "Read ULST lyrics " << lyrics;
} else if (!map["SYLT"].isEmpty())
lyrics = TStringToQString((map["SYLT"].front())->toString()).trimmed();
Decode(map["USLT"].front()->toString(), nullptr,
song->mutable_lyrics());
} else if (!map["SYLT"].isEmpty()) {
Decode(map["SYLT"].front()->toString(), nullptr,
song->mutable_lyrics());
}
if (!map["APIC"].isEmpty()) song->set_art_automatic(kEmbeddedCover);
@ -310,6 +330,15 @@ void TagReader::ReadFile(const QString& filename,
Decode(items["\251grp"].toStringList().toString(" "), nullptr,
song->mutable_grouping());
}
if (items.contains(kMP4_OriginalYear_ID)) {
song->set_originalyear(
TStringToQString(
items[kMP4_OriginalYear_ID].toStringList().toString('\n'))
.left(4)
.toInt());
}
Decode(mp4_tag->comment(), nullptr, song->mutable_comment());
}
}
@ -350,6 +379,22 @@ void TagReader::ReadFile(const QString& filename,
}
}
}
if (attributes_map.contains(kASF_OriginalDate_ID)) {
const TagLib::ASF::AttributeList& attributes =
attributes_map[kASF_OriginalDate_ID];
if (!attributes.isEmpty()) {
song->set_originalyear(
TStringToQString(attributes.front().toString()).left(4).toInt());
}
} else if (attributes_map.contains(kASF_OriginalYear_ID)) {
const TagLib::ASF::AttributeList& attributes =
attributes_map[kASF_OriginalYear_ID];
if (!attributes.isEmpty()) {
song->set_originalyear(
TStringToQString(attributes.front().toString()).left(4).toInt());
}
}
}
#endif
else if (tag) {
@ -486,6 +531,13 @@ void TagReader::ParseOggTag(const TagLib::Ogg::FieldListMap& map,
Decode(map["ALBUM ARTIST"].front(), codec, song->mutable_albumartist());
}
if (!map["ORIGINALDATE"].isEmpty())
song->set_originalyear(
TStringToQString(map["ORIGINALDATE"].front()).left(4).toInt());
else if (!map["ORIGINALYEAR"].isEmpty())
song->set_originalyear(
TStringToQString(map["ORIGINALYEAR"].front()).toInt());
if (!map["BPM"].isEmpty())
song->set_bpm(TStringToQString(map["BPM"].front()).trimmed().toFloat());
@ -628,7 +680,7 @@ bool TagReader::SaveFile(const QString& filename,
SetTextFrame("TCOM", song.composer(), tag);
SetTextFrame("TIT1", song.grouping(), tag);
SetTextFrame("TOPE", song.performer(), tag);
SetTextFrame("USLT", song.lyrics(), tag);
SetUnsyncLyricsFrame(song.lyrics(), tag);
// Skip TPE1 (which is the artist) here because we already set it
SetTextFrame("TPE2", song.albumartist(), tag);
SetTextFrame("TCMP", std::string(song.compilation() ? "1" : "0"), tag);
@ -842,19 +894,67 @@ void TagReader::SetTextFrame(const char* id, const QString& value,
void TagReader::SetTextFrame(const char* id, const std::string& value,
TagLib::ID3v2::Tag* tag) const {
TagLib::ByteVector id_vector(id);
QVector<TagLib::ByteVector> frames_buffer;
// Remove the frame if it already exists
// Store and clear existing frames
while (tag->frameListMap().contains(id_vector) &&
tag->frameListMap()[id_vector].size() != 0) {
frames_buffer.push_back(tag->frameListMap()[id_vector].front()->render());
tag->removeFrame(tag->frameListMap()[id_vector].front());
}
// Create and add a new frame
TagLib::ID3v2::TextIdentificationFrame* frame =
new TagLib::ID3v2::TextIdentificationFrame(id_vector,
// If no frames stored create empty frame
if (frames_buffer.isEmpty()) {
TagLib::ID3v2::TextIdentificationFrame frame(id_vector,
TagLib::String::UTF8);
frame->setText(StdStringToTaglibString(value));
tag->addFrame(frame);
frames_buffer.push_back(frame.render());
}
// Update and add the frames
for (int lyrics_index = 0; lyrics_index < frames_buffer.size();
lyrics_index++) {
TagLib::ID3v2::TextIdentificationFrame* frame =
new TagLib::ID3v2::TextIdentificationFrame(
frames_buffer.at(lyrics_index));
if (lyrics_index == 0) {
frame->setText(StdStringToTaglibString(value));
}
// add frame takes ownership and clears the memory
tag->addFrame(frame);
}
}
void TagReader::SetUnsyncLyricsFrame(const std::string& value,
TagLib::ID3v2::Tag* tag) const {
TagLib::ByteVector id_vector("USLT");
QVector<TagLib::ByteVector> frames_buffer;
// Store and clear existing frames
while (tag->frameListMap().contains(id_vector) &&
tag->frameListMap()[id_vector].size() != 0) {
frames_buffer.push_back(tag->frameListMap()[id_vector].front()->render());
tag->removeFrame(tag->frameListMap()[id_vector].front());
}
// If no frames stored create empty frame
if (frames_buffer.isEmpty()) {
TagLib::ID3v2::UnsynchronizedLyricsFrame frame(TagLib::String::UTF8);
frame.setDescription("Clementine editor");
frames_buffer.push_back(frame.render());
}
// Update and add the frames
for (int lyrics_index = 0; lyrics_index < frames_buffer.size();
lyrics_index++) {
TagLib::ID3v2::UnsynchronizedLyricsFrame* frame =
new TagLib::ID3v2::UnsynchronizedLyricsFrame(
frames_buffer.at(lyrics_index));
if (lyrics_index == 0) {
frame->setText(StdStringToTaglibString(value));
}
// add frame takes ownership and clears the memory
tag->addFrame(frame);
}
}
bool TagReader::IsMediaFile(const QString& filename) const {

View File

@ -87,12 +87,12 @@ class TagReader {
void SetFMPSStatisticsVorbisComments(
TagLib::Ogg::XiphComment* vorbis_comments,
const pb::tagreader::SongMetadata& song) const;
void SetFMPSRatingVorbisComments(TagLib::Ogg::XiphComment* vorbis_comments,
const pb::tagreader::SongMetadata& song)
const;
void SetFMPSRatingVorbisComments(
TagLib::Ogg::XiphComment* vorbis_comments,
const pb::tagreader::SongMetadata& song) const;
pb::tagreader::SongMetadata_Type GuessFileType(TagLib::FileRef* fileref)
const;
pb::tagreader::SongMetadata_Type GuessFileType(
TagLib::FileRef* fileref) const;
void SetUserTextFrame(const QString& description, const QString& value,
TagLib::ID3v2::Tag* tag) const;
@ -104,6 +104,8 @@ class TagReader {
TagLib::ID3v2::Tag* tag) const;
void SetTextFrame(const char* id, const std::string& value,
TagLib::ID3v2::Tag* tag) const;
void SetUnsyncLyricsFrame(const std::string& value,
TagLib::ID3v2::Tag* tag) const;
private:
static const char* kMP4_FMPS_Rating_ID;

View File

@ -52,6 +52,7 @@ message SongMetadata {
optional string performer = 31;
optional string grouping = 32;
optional string lyrics = 33;
optional int32 originalyear = 34;
}
message ReadFileRequest {

View File

@ -2,7 +2,7 @@
Copyright 2003, Max Howell <max.howell@methylblue.com>
Copyright 2009, 2011-2012, David Sansome <me@davidsansome.com>
Copyright 2010, 2012, 2014, John Maguire <john.maguire@gmail.com>
Copyright 2014, Mark Furneaux <mark@romaco.ca>
Copyright 2014-2015, Mark Furneaux <mark@furneaux.ca>
Copyright 2014, Krzysztof Sobiecki <sobkas@gmail.com>
Clementine is free software: you can redistribute it and/or modify
@ -33,6 +33,7 @@
#include <QtDebug>
#include "engines/enginebase.h"
#include "core/arraysize.h"
// INSTRUCTIONS Base2D
// 1. do anything that depends on height() in init(), Base2D will call it before
@ -45,7 +46,8 @@
// TODO(David Sansome): make an INSTRUCTIONS file
// can't mod scope in analyze you have to use transform
// TODO(John Maguire): for 2D use setErasePixmap Qt function insetead of m_background
// TODO(John Maguire): for 2D use setErasePixmap Qt function insetead of
// m_background
// make the linker happy only for gcc < 4.0
#if !(__GNUC__ > 4 || (__GNUC__ == 4 && __GNUC_MINOR__ >= 0)) && \
@ -53,20 +55,29 @@
template class Analyzer::Base<QWidget>;
#endif
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);
Analyzer::Base::Base(QWidget* parent, uint scopeSize)
: QWidget(parent),
m_timeout(40) // msec
timeout_(40) // msec
,
m_fht(new FHT(scopeSize)),
m_engine(nullptr),
m_lastScope(512),
current_chunk_(0),
fht_(new FHT(scopeSize)),
engine_(nullptr),
lastScope_(512),
new_frame_(false),
is_playing_(false) {}
is_playing_(false),
barkband_table_(QList<uint>()),
prev_color_index_(0),
bands_(0),
psychedelic_enabled_(false) {}
void Analyzer::Base::hideEvent(QHideEvent*) { m_timer.stop(); }
void Analyzer::Base::hideEvent(QHideEvent*) { timer_.stop(); }
void Analyzer::Base::showEvent(QShowEvent*) { m_timer.start(timeout(), this); }
void Analyzer::Base::showEvent(QShowEvent*) { timer_.start(timeout(), this); }
void Analyzer::Base::transform(Scope& scope) {
// this is a standard transformation that should give
@ -74,16 +85,16 @@ void Analyzer::Base::transform(Scope& scope) {
// NOTE resizing here is redundant as FHT routines only calculate FHT::size()
// values
// scope.resize( m_fht->size() );
// scope.resize( fht_->size() );
float* front = static_cast<float*>(&scope.front());
float* f = new float[m_fht->size()];
m_fht->copy(&f[0], front);
m_fht->logSpectrum(front, &f[0]);
m_fht->scale(front, 1.0 / 20);
float* f = new float[fht_->size()];
fht_->copy(&f[0], front);
fht_->logSpectrum(front, &f[0]);
fht_->scale(front, 1.0 / 20);
scope.resize(m_fht->size() / 2); // second half of values are rubbish
scope.resize(fht_->size() / 2); // second half of values are rubbish
delete[] f;
}
@ -91,30 +102,30 @@ void Analyzer::Base::paintEvent(QPaintEvent* e) {
QPainter p(this);
p.fillRect(e->rect(), palette().color(QPalette::Window));
switch (m_engine->state()) {
switch (engine_->state()) {
case Engine::Playing: {
const Engine::Scope& thescope = m_engine->scope(m_timeout);
const Engine::Scope& thescope = engine_->scope(timeout_);
int i = 0;
// convert to mono here - our built in analyzers need mono, but the
// engines provide interleaved pcm
for (uint x = 0; static_cast<int>(x) < m_fht->size(); ++x) {
m_lastScope[x] =
static_cast<double>(thescope[i] + thescope[i + 1]) / (2 * (1 << 15));
for (uint x = 0; static_cast<int>(x) < fht_->size(); ++x) {
lastScope_[x] = static_cast<double>(thescope[i] + thescope[i + 1]) /
(2 * (1 << 15));
i += 2;
}
is_playing_ = true;
transform(m_lastScope);
analyze(p, m_lastScope, new_frame_);
transform(lastScope_);
analyze(p, lastScope_, new_frame_);
// scope.resize( m_fht->size() );
// scope.resize( fht_->size() );
break;
}
case Engine::Paused:
is_playing_ = false;
analyze(p, m_lastScope, new_frame_);
analyze(p, lastScope_, new_frame_);
break;
default:
@ -131,9 +142,9 @@ int Analyzer::Base::resizeExponent(int exp) {
else if (exp > 9)
exp = 9;
if (exp != m_fht->sizeExp()) {
delete m_fht;
m_fht = new FHT(exp);
if (exp != fht_->sizeExp()) {
delete fht_;
fht_ = new FHT(exp);
}
return exp;
}
@ -154,7 +165,7 @@ int Analyzer::Base::resizeForBands(int bands) {
exp = 9;
resizeExponent(exp);
return m_fht->size() / 2;
return fht_->size() / 2;
}
void Analyzer::Base::demo(QPainter& p) {
@ -175,6 +186,67 @@ void Analyzer::Base::demo(QPainter& p) {
++t;
}
void Analyzer::Base::psychedelicModeChanged(bool enabled) {
psychedelic_enabled_ = enabled;
}
int Analyzer::Base::BandFrequency(int band) const {
return ((kSampleRate / 2) * band + kSampleRate / 4) / bands_;
}
void Analyzer::Base::updateBandSize(const int scopeSize) {
// prevent possible dbz in BandFrequency
if (scopeSize == 0) {
return;
}
bands_ = scopeSize;
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);
}
}
QColor Analyzer::Base::getPsychedelicColor(const Scope& scope,
const int ampFactor,
const int bias) {
if (scope.size() > barkband_table_.length()) {
return palette().color(QPalette::Highlight);
}
// Calculate total magnitudes for different bark bands.
double bands[sBarkBandCount]{};
for (int i = 0; i < barkband_table_.size(); ++i) {
bands[barkband_table_[i]] += scope[i];
}
// Now divide the bark bands into thirds and compute their total amplitudes.
double rgb[3]{};
for (int i = 0; i < sBarkBandCount - 1; ++i) {
rgb[(i * 3) / sBarkBandCount] += bands[i] * bands[i];
}
for (int i = 0; i < 3; ++i) {
// bias colours for a threshold around normally amplified audio
rgb[i] = (int)((sqrt(rgb[i]) * ampFactor) + bias);
if (rgb[i] > 255) {
rgb[i] = 255;
}
}
return QColor::fromRgb(rgb[0], rgb[1], rgb[2]);
}
void Analyzer::Base::polishEvent() {
init(); // virtual
}
@ -211,7 +283,7 @@ void Analyzer::initSin(Scope& v, const uint size) {
void Analyzer::Base::timerEvent(QTimerEvent* e) {
QWidget::timerEvent(e);
if (e->timerId() != m_timer.timerId()) return;
if (e->timerId() != timer_.timerId()) return;
new_frame_ = true;
update();

View File

@ -3,7 +3,7 @@
Copyright 2009-2010, David Sansome <davidsansome@gmail.com>
Copyright 2010, 2014, John Maguire <john.maguire@gmail.com>
Copyright 2011, Arnaud Bienner <arnaud.bienner@gmail.com>
Copyright 2014, Mark Furneaux <mark@romaco.ca>
Copyright 2014-2015, Mark Furneaux <mark@furneaux.ca>
Copyright 2014, Krzysztof Sobiecki <sobkas@gmail.com>
Clementine is free software: you can redistribute it and/or modify
@ -58,21 +58,22 @@ class Base : public QWidget {
Q_OBJECT
public:
~Base() { delete m_fht; }
~Base() { delete fht_; }
uint timeout() const { return m_timeout; }
uint timeout() const { return timeout_; }
void set_engine(EngineBase* engine) { m_engine = engine; }
void set_engine(EngineBase* engine) { engine_ = engine; }
void changeTimeout(uint newTimeout) {
m_timeout = newTimeout;
if (m_timer.isActive()) {
m_timer.stop();
m_timer.start(m_timeout, this);
timeout_ = newTimeout;
if (timer_.isActive()) {
timer_.stop();
timer_.start(timeout_, this);
}
}
virtual void framerateChanged() {}
virtual void psychedelicModeChanged(bool);
protected:
explicit Base(QWidget*, uint scopeSize = 7);
@ -86,21 +87,32 @@ class Base : public QWidget {
int resizeExponent(int);
int resizeForBands(int);
int BandFrequency(int) const;
void updateBandSize(const int);
QColor getPsychedelicColor(const Scope&, const int, const int);
virtual void init() {}
virtual void transform(Scope&);
virtual void analyze(QPainter& p, const Scope&, bool new_frame) = 0;
virtual void demo(QPainter& p);
protected:
QBasicTimer m_timer;
uint m_timeout;
FHT* m_fht;
EngineBase* m_engine;
Scope m_lastScope;
int current_chunk_;
static const int kSampleRate =
44100; // we shouldn't need to care about ultrasonics
QBasicTimer timer_;
uint timeout_;
FHT* fht_;
EngineBase* engine_;
Scope lastScope_;
bool new_frame_;
bool is_playing_;
QList<uint> barkband_table_;
double prev_colors_[10][3];
int prev_color_index_;
int bands_;
bool psychedelic_enabled_;
};
void interpolate(const Scope&, Scope&);

View File

@ -3,7 +3,7 @@
Copyright 2010, 2014, John Maguire <john.maguire@gmail.com>
Copyright 2011-2012, Arnaud Bienner <arnaud.bienner@gmail.com>
Copyright 2013, Vasily Fomin <vasili.fomin@gmail.com>
Copyright 2014, Mark Furneaux <mark@romaco.ca>
Copyright 2014-2015, Mark Furneaux <mark@furneaux.ca>
Copyright 2014, Krzysztof Sobiecki <sobkas@gmail.com>
Clementine is free software: you can redistribute it and/or modify
@ -57,6 +57,7 @@ AnalyzerContainer::AnalyzerContainer(QWidget* parent)
visualisation_action_(nullptr),
double_click_timer_(new QTimer(this)),
ignore_next_click_(false),
psychedelic_colors_on_(false),
current_analyzer_(nullptr),
engine_(nullptr) {
QHBoxLayout* layout = new QHBoxLayout(this);
@ -88,6 +89,11 @@ AnalyzerContainer::AnalyzerContainer(QWidget* parent)
disable_action_->setCheckable(true);
group_->addAction(disable_action_);
context_menu_->addSeparator();
psychedelic_enable_ = context_menu_->addAction(
tr("Use Psychedelic Colors"), this, SLOT(TogglePsychedelicColors()));
psychedelic_enable_->setCheckable(true);
context_menu_->addSeparator();
// Visualisation action gets added in SetActions
@ -145,6 +151,14 @@ void AnalyzerContainer::DisableAnalyzer() {
Save();
}
void AnalyzerContainer::TogglePsychedelicColors() {
psychedelic_colors_on_ = !psychedelic_colors_on_;
if (current_analyzer_) {
current_analyzer_->psychedelicModeChanged(psychedelic_colors_on_);
}
SavePsychedelic();
}
void AnalyzerContainer::ChangeAnalyzer(int id) {
QObject* instance = analyzer_types_[id]->newInstance(Q_ARG(QWidget*, this));
@ -161,6 +175,7 @@ void AnalyzerContainer::ChangeAnalyzer(int id) {
current_framerate_ =
current_framerate_ == 0 ? kMediumFramerate : current_framerate_;
current_analyzer_->changeTimeout(1000 / current_framerate_);
current_analyzer_->psychedelicModeChanged(psychedelic_colors_on_);
layout()->addWidget(current_analyzer_);
@ -183,6 +198,10 @@ void AnalyzerContainer::Load() {
QSettings s;
s.beginGroup(kSettingsGroup);
// Colours
psychedelic_colors_on_ = s.value("psychedelic", false).toBool();
psychedelic_enable_->setChecked(psychedelic_colors_on_);
// Analyzer
QString type = s.value("type", "BlockAnalyzer").toString();
if (type.isEmpty()) {
@ -227,6 +246,13 @@ void AnalyzerContainer::Save() {
: QVariant());
}
void AnalyzerContainer::SavePsychedelic() {
QSettings s;
s.beginGroup(kSettingsGroup);
s.setValue("psychedelic", psychedelic_colors_on_);
}
void AnalyzerContainer::AddFramerate(const QString& name, int framerate) {
QAction* action =
context_menu_framerate_->addAction(name, mapper_framerate_, SLOT(map()));

View File

@ -4,6 +4,7 @@
Copyright 2011-2012, Arnaud Bienner <arnaud.bienner@gmail.com>
Copyright 2013, Vasily Fomin <vasili.fomin@gmail.com>
Copyright 2014, Krzysztof Sobiecki <sobkas@gmail.com>
Copyright 2015, Mark Furneaux <mark@furneaux.ca>
Clementine is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
@ -40,7 +41,7 @@ class AnalyzerContainer : public QWidget {
static const char* kSettingsGroup;
static const char* kSettingsFramerate;
signals:
signals:
void WheelEvent(int delta);
protected:
@ -53,6 +54,7 @@ class AnalyzerContainer : public QWidget {
void ChangeFramerate(int new_framerate);
void DisableAnalyzer();
void ShowPopupMenu();
void TogglePsychedelicColors();
private:
static const int kLowFramerate;
@ -63,6 +65,7 @@ class AnalyzerContainer : public QWidget {
void Load();
void Save();
void SaveFramerate(int framerate);
void SavePsychedelic();
template <typename T>
void AddAnalyzerType();
void AddFramerate(const QString& name, int framerate);
@ -80,11 +83,13 @@ class AnalyzerContainer : public QWidget {
QList<int> framerate_list_;
QList<QAction*> actions_;
QAction* disable_action_;
QAction* psychedelic_enable_;
QAction* visualisation_action_;
QTimer* double_click_timer_;
QPoint last_click_pos_;
bool ignore_next_click_;
bool psychedelic_colors_on_;
Analyzer::Base* current_analyzer_;
EngineBase* engine_;

View File

@ -2,7 +2,7 @@
Copyright 2003, Mark Kretschmann <markey@web.de>
Copyright 2009-2010, David Sansome <davidsansome@gmail.com>
Copyright 2014, Alibek Omarov <a1ba.omarov@gmail.com>
Copyright 2014, Mark Furneaux <mark@romaco.ca>
Copyright 2014-2015, Mark Furneaux <mark@furneaux.ca>
Copyright 2014, Krzysztof Sobiecki <sobkas@gmail.com>
Copyright 2014, John Maguire <john.maguire@gmail.com>
@ -23,7 +23,6 @@
/* Original Author: Mark Kretschmann <markey@web.de> 2003
*/
#include "baranalyzer.h"
#include <cmath>
#include <QtDebug>
@ -36,19 +35,20 @@ const char* BarAnalyzer::kName =
BarAnalyzer::BarAnalyzer(QWidget* parent) : Analyzer::Base(parent, 8) {
// roof pixmaps don't depend on size() so we do in the ctor
m_bg = parent->palette().color(QPalette::Background);
bg_ = parent->palette().color(QPalette::Background);
QColor fg(parent->palette().color(QPalette::Highlight).lighter(150));
double dr = static_cast<double>(m_bg.red() - fg.red()) /
(NUM_ROOFS - 1); // -1 because we start loop below at 0
double dg = static_cast<double>(m_bg.green() - fg.green()) / (NUM_ROOFS - 1);
double db = static_cast<double>(m_bg.blue() - fg.blue()) / (NUM_ROOFS - 1);
double dr = static_cast<double>(bg_.red() - fg.red()) /
(kNumRoofs - 1); // -1 because we start loop below at 0
double dg = static_cast<double>(bg_.green() - fg.green()) / (kNumRoofs - 1);
double db = static_cast<double>(bg_.blue() - fg.blue()) / (kNumRoofs - 1);
for (uint i = 0; i < NUM_ROOFS; ++i) {
m_pixRoof[i] = QPixmap(COLUMN_WIDTH, 1);
m_pixRoof[i].fill(QColor(fg.red() + static_cast<int>(dr * i), fg.green() + static_cast<int>(dg * i),
fg.blue() + static_cast<int>(db * i)));
for (uint i = 0; i < kNumRoofs; ++i) {
pixRoof_[i] = QPixmap(kColumnWidth, 1);
pixRoof_[i].fill(QColor(fg.red() + static_cast<int>(dr * i),
fg.green() + static_cast<int>(dg * i),
fg.blue() + static_cast<int>(db * i)));
}
}
@ -58,46 +58,66 @@ void BarAnalyzer::resizeEvent(QResizeEvent* e) { init(); }
void BarAnalyzer::init() {
const double MAX_AMPLITUDE = 1.0;
const double F = static_cast<double>(height() - 2) / (log10(255) * MAX_AMPLITUDE);
const double F =
static_cast<double>(height() - 2) / (log10(255) * MAX_AMPLITUDE);
BAND_COUNT = width() / 5;
MAX_DOWN = static_cast<int>(0 - (qMax(1, height() / 50)));
MAX_UP = static_cast<int>(qMax(1, height() / 25));
band_count_ = width() / 5;
max_down_ = static_cast<int>(0 - (qMax(1, height() / 50)));
max_up_ = static_cast<int>(qMax(1, height() / 25));
barVector.resize(BAND_COUNT, 0);
roofVector.resize(BAND_COUNT, height() - 5);
roofVelocityVector.resize(BAND_COUNT, ROOF_VELOCITY_REDUCTION_FACTOR);
m_roofMem.resize(BAND_COUNT);
m_scope.resize(BAND_COUNT);
barVector_.resize(band_count_, 0);
roofVector_.resize(band_count_, height() - 5);
roofVelocityVector_.resize(band_count_, kRoofVelocityReductionFactor);
roofMem_.resize(band_count_);
scope_.resize(band_count_);
// generate a list of values that express amplitudes in range 0-MAX_AMP as
// ints from 0-height() on log scale
for (uint x = 0; x < 256; ++x) {
m_lvlMapper[x] = static_cast<uint>(F * log10(x + 1));
lvlMapper_[x] = static_cast<uint>(F * log10(x + 1));
}
m_pixBarGradient = QPixmap(height() * COLUMN_WIDTH, height());
m_pixCompose = QPixmap(size());
pixBarGradient_ = QPixmap(height() * kColumnWidth, height());
pixCompose_ = QPixmap(size());
canvas_ = QPixmap(size());
canvas_.fill(palette().color(QPalette::Background));
QPainter p(&m_pixBarGradient);
QColor rgb(palette().color(QPalette::Highlight));
updateBandSize(band_count_);
colorChanged();
setMinimumSize(QSize(band_count_ * kColumnWidth, 10));
}
for (int x = 0, r = rgb.red(), g = rgb.green(), b = rgb.blue(), r2 = 255 - r; x < height();
++x) {
void BarAnalyzer::colorChanged() {
if (pixBarGradient_.isNull()) {
return;
}
QPainter p(&pixBarGradient_);
QColor rgb;
if (psychedelic_enabled_) {
rgb = getPsychedelicColor(scope_, 50, 100);
} else {
rgb = palette().color(QPalette::Highlight);
}
for (int x = 0, r = rgb.red(), g = rgb.green(), b = rgb.blue(), r2 = 255 - r;
x < height(); ++x) {
for (int y = x; y > 0; --y) {
const double fraction = static_cast<double>(y) / height();
// p.setPen( QColor( r + (int)(r2 * fraction), g, b - (int)(255 *
// fraction) ) );
p.setPen(QColor(r + static_cast<int>(r2 * fraction), g, b));
p.drawLine(x * COLUMN_WIDTH, height() - y, (x + 1) * COLUMN_WIDTH,
p.drawLine(x * kColumnWidth, height() - y, (x + 1) * kColumnWidth,
height() - y);
}
}
}
setMinimumSize(QSize(BAND_COUNT * COLUMN_WIDTH, 10));
void BarAnalyzer::psychedelicModeChanged(bool enabled) {
psychedelic_enabled_ = enabled;
// reset colours back to normal
colorChanged();
}
void BarAnalyzer::analyze(QPainter& p, const Scope& s, bool new_frame) {
@ -107,20 +127,25 @@ void BarAnalyzer::analyze(QPainter& p, const Scope& s, bool new_frame) {
}
// Analyzer::interpolate( s, m_bands );
Scope& v = m_scope;
Analyzer::interpolate(s, v);
Analyzer::interpolate(s, scope_);
QPainter canvas_painter(&canvas_);
// update the graphics with the new colour
if (psychedelic_enabled_) {
colorChanged();
}
canvas_.fill(palette().color(QPalette::Background));
for (uint i = 0, x = 0, y2; i < v.size(); ++i, x += COLUMN_WIDTH + 1) {
for (uint i = 0, x = 0, y2; i < scope_.size(); ++i, x += kColumnWidth + 1) {
// assign pre[log10]'d value
y2 = static_cast<uint>(v[i] *
256); // 256 will be optimised to a bitshift //no, it's a float
y2 = m_lvlMapper[(y2 > 255) ? 255 : y2]; // lvlMapper is array of ints with
// values 0 to height()
y2 = static_cast<uint>(
scope_[i] *
256); // 256 will be optimised to a bitshift //no, it's a float
y2 = lvlMapper_[(y2 > 255) ? 255 : y2]; // lvlMapper is array of ints with
// values 0 to height()
int change = y2 - barVector[i];
int change = y2 - barVector_[i];
// using the best of Markey's, piggz and Max's ideas on the way to shift the
// bars
@ -129,54 +154,52 @@ void BarAnalyzer::analyze(QPainter& p, const Scope& s, bool new_frame) {
// 2. shift large upwards with a bias towards last value
// 3. fall downwards at a constant pace
/*if ( change > MAX_UP ) //anything too much greater than 2 gives "jitter"
/*if ( change > max_up_ ) //anything too much greater than 2 gives "jitter"
//add some dynamics - makes the value slightly closer to what it was last time
y2 = ( barVector[i] + MAX_UP );
//y2 = ( barVector[i] * 2 + y2 ) / 3;
y2 = ( barVector_[i] + max_up_ );
//y2 = ( barVector_[i] * 2 + y2 ) / 3;
else*/ if (change <
MAX_DOWN)
y2 = barVector[i] + MAX_DOWN;
max_down_)
y2 = barVector_[i] + max_down_;
if (static_cast<int>(y2) > roofVector[i]) {
roofVector[i] = static_cast<int>(y2);
roofVelocityVector[i] = 1;
if (static_cast<int>(y2) > roofVector_[i]) {
roofVector_[i] = static_cast<int>(y2);
roofVelocityVector_[i] = 1;
}
// remember where we are
barVector[i] = y2;
barVector_[i] = y2;
if (m_roofMem[i].size() > NUM_ROOFS)
m_roofMem[i].erase(m_roofMem[i].begin());
if (roofMem_[i].size() > kNumRoofs) roofMem_[i].erase(roofMem_[i].begin());
// blt last n roofs, a.k.a motion blur
for (uint c = 0; c < m_roofMem[i].size(); ++c)
// bitBlt( m_pComposePixmap, x, m_roofMem[i]->at( c ), m_roofPixmaps[ c ]
for (uint c = 0; c < roofMem_[i].size(); ++c)
// bitBlt( m_pComposePixmap, x, roofMem_[i]->at( c ), m_roofPixmaps[ c ]
// );
// bitBlt( canvas(), x, m_roofMem[i][c], &m_pixRoof[ NUM_ROOFS - 1 - c ]
// bitBlt( canvas(), x, roofMem_[i][c], &pixRoof_[ kNumRoofs - 1 - c ]
// );
canvas_painter.drawPixmap(x, m_roofMem[i][c],
m_pixRoof[NUM_ROOFS - 1 - c]);
canvas_painter.drawPixmap(x, roofMem_[i][c], pixRoof_[kNumRoofs - 1 - c]);
// blt the bar
canvas_painter.drawPixmap(x, height() - y2, *gradient(), y2 * COLUMN_WIDTH,
height() - y2, COLUMN_WIDTH, y2);
canvas_painter.drawPixmap(x, height() - y2, *gradient(), y2 * kColumnWidth,
height() - y2, kColumnWidth, y2);
/*bitBlt( canvas(), x, height() - y2,
gradient(), y2 * COLUMN_WIDTH, height() - y2, COLUMN_WIDTH, y2,
gradient(), y2 * kColumnWidth, height() - y2, kColumnWidth, y2,
Qt::CopyROP );*/
m_roofMem[i].push_back(height() - roofVector[i] - 2);
roofMem_[i].push_back(height() - roofVector_[i] - 2);
// set roof parameters for the NEXT draw
if (roofVelocityVector[i] != 0) {
if (roofVelocityVector[i] > 32) // no reason to do == 32
roofVector[i] -=
(roofVelocityVector[i] - 32) / 20; // trivial calculation
if (roofVelocityVector_[i] != 0) {
if (roofVelocityVector_[i] > 32) // no reason to do == 32
roofVector_[i] -=
(roofVelocityVector_[i] - 32) / 20; // trivial calculation
if (roofVector[i] < 0) {
roofVector[i] = 0; // not strictly necessary
roofVelocityVector[i] = 0;
if (roofVector_[i] < 0) {
roofVector_[i] = 0; // not strictly necessary
roofVelocityVector_[i] = 0;
} else {
++roofVelocityVector[i];
++roofVelocityVector_[i];
}
}
}

View File

@ -2,7 +2,7 @@
Copyright 2003-2005, Max Howell <max.howell@methylblue.com>
Copyright 2005, Mark Kretschmann <markey@web.de>
Copyright 2009-2010, David Sansome <davidsansome@gmail.com>
Copyright 2014, Mark Furneaux <mark@romaco.ca>
Copyright 2014-2015, Mark Furneaux <mark@furneaux.ca>
Copyright 2014, Krzysztof A. Sobiecki <sobkas@gmail.com>
Copyright 2014, John Maguire <john.maguire@gmail.com>
@ -39,44 +39,45 @@ class BarAnalyzer : public Analyzer::Base {
void init();
virtual void analyze(QPainter& p, const Analyzer::Scope&, bool new_frame);
// virtual void transform( Scope& );
virtual void psychedelicModeChanged(bool);
/**
* Resizes the widget to a new geometry according to @p e
* @param e The resize-event
*/
void resizeEvent(QResizeEvent* e);
void colorChanged();
uint BAND_COUNT;
int MAX_DOWN;
int MAX_UP;
static const uint ROOF_HOLD_TIME = 48;
static const int ROOF_VELOCITY_REDUCTION_FACTOR = 32;
static const uint NUM_ROOFS = 16;
static const uint COLUMN_WIDTH = 4;
uint band_count_;
int max_down_;
int max_up_;
static const uint kRoofHoldTime = 48;
static const int kRoofVelocityReductionFactor = 32;
static const uint kNumRoofs = 16;
static const uint kColumnWidth = 4;
static const char* kName;
protected:
QPixmap m_pixRoof[NUM_ROOFS];
// vector<uint> m_roofMem[BAND_COUNT];
QPixmap pixRoof_[kNumRoofs];
// vector<uint> roofMem_[band_count_];
// Scope m_bands; //copy of the Scope to prevent creating/destroying a Scope
// every iteration
uint m_lvlMapper[256];
std::vector<aroofMemVec> m_roofMem;
std::vector<uint> barVector; // positions of bars
std::vector<int> roofVector; // positions of roofs
std::vector<uint> roofVelocityVector; // speed that roofs falls
uint lvlMapper_[256];
std::vector<aroofMemVec> roofMem_;
std::vector<uint> barVector_; // positions of bars
std::vector<int> roofVector_; // positions of roofs
std::vector<uint> roofVelocityVector_; // speed that roofs falls
const QPixmap* gradient() const { return &m_pixBarGradient; }
const QPixmap* gradient() const { return &pixBarGradient_; }
private:
QPixmap m_pixBarGradient;
QPixmap m_pixCompose;
QPixmap pixBarGradient_;
QPixmap pixCompose_;
QPixmap canvas_;
Analyzer::Scope m_scope; // so we don't create a vector every frame
QColor m_bg;
Analyzer::Scope scope_; // so we don't create a vector every frame
QColor bg_;
};
#endif // ANALYZERS_BARANALYZER_H_

View File

@ -3,7 +3,7 @@
Copyright 2005, Mark Kretschmann <markey@web.de>
Copyright 2009-2010, David Sansome <davidsansome@gmail.com>
Copyright 2010, 2014, John Maguire <john.maguire@gmail.com>
Copyright 2014, Mark Furneaux <mark@romaco.ca>
Copyright 2014-2015, Mark Furneaux <mark@furneaux.ca>
Copyright 2014, Krzysztof Sobiecki <sobkas@gmail.com>
Clementine is free software: you can redistribute it and/or modify
@ -33,35 +33,34 @@
#include <cstdlib>
#include <QPainter>
const uint BlockAnalyzer::HEIGHT = 2;
const uint BlockAnalyzer::WIDTH = 4;
const uint BlockAnalyzer::MIN_ROWS = 3; // arbituary
const uint BlockAnalyzer::MIN_COLUMNS = 32; // arbituary
const uint BlockAnalyzer::MAX_COLUMNS = 256; // must be 2**n
const uint BlockAnalyzer::FADE_SIZE = 90;
const uint BlockAnalyzer::kHeight = 2;
const uint BlockAnalyzer::kWidth = 4;
const uint BlockAnalyzer::kMinRows = 3; // arbituary
const uint BlockAnalyzer::kMinColumns = 32; // arbituary
const uint BlockAnalyzer::kMaxColumns = 256; // must be 2**n
const uint BlockAnalyzer::kFadeSize = 90;
const char* BlockAnalyzer::kName =
QT_TRANSLATE_NOOP("AnalyzerContainer", "Block analyzer");
BlockAnalyzer::BlockAnalyzer(QWidget* parent)
: Analyzer::Base(parent, 9),
m_columns(0),
m_rows(0),
m_y(0),
m_barPixmap(1, 1),
m_topBarPixmap(WIDTH, HEIGHT),
m_scope(MIN_COLUMNS),
m_store(1 << 8, 0),
m_fade_bars(FADE_SIZE),
m_fade_pos(1 << 8, 50),
m_fade_intensity(1 << 8, 32) {
setMinimumSize(MIN_COLUMNS * (WIDTH + 1) - 1,
MIN_ROWS * (HEIGHT + 1) - 1);
columns_(0),
rows_(0),
y_(0),
barPixmap_(1, 1),
topBarPixmap_(kWidth, kHeight),
scope_(kMinColumns),
store_(1 << 8, 0),
fade_bars_(kFadeSize),
fade_pos_(1 << 8, 50),
fade_intensity_(1 << 8, 32) {
setMinimumSize(kMinColumns * (kWidth + 1) - 1, kMinRows * (kHeight + 1) - 1);
// -1 is padding, no drawing takes place there
setMaximumWidth(MAX_COLUMNS * (WIDTH + 1) - 1);
setMaximumWidth(kMaxColumns * (kWidth + 1) - 1);
// mxcl says null pixmaps cause crashes, so let's play it safe
for (uint i = 0; i < FADE_SIZE; ++i) m_fade_bars[i] = QPixmap(1, 1);
for (uint i = 0; i < kFadeSize; ++i) fade_bars_[i] = QPixmap(1, 1);
}
BlockAnalyzer::~BlockAnalyzer() {}
@ -69,41 +68,44 @@ BlockAnalyzer::~BlockAnalyzer() {}
void BlockAnalyzer::resizeEvent(QResizeEvent* e) {
QWidget::resizeEvent(e);
m_background = QPixmap(size());
background_ = QPixmap(size());
canvas_ = QPixmap(size());
const uint oldRows = m_rows;
const uint oldRows = rows_;
// all is explained in analyze()..
// +1 to counter -1 in maxSizes, trust me we need this!
m_columns = qMax(static_cast<uint>(static_cast<double>(width() + 1) / (WIDTH + 1)), MAX_COLUMNS);
m_rows = static_cast<uint>(static_cast<double>(height() + 1) / (HEIGHT + 1));
columns_ = qMin(
static_cast<uint>(static_cast<double>(width() + 1) / (kWidth + 1)) + 1,
kMaxColumns);
rows_ = static_cast<uint>(static_cast<double>(height() + 1) / (kHeight + 1));
// this is the y-offset for drawing from the top of the widget
m_y = (height() - (m_rows * (HEIGHT + 1)) + 2) / 2;
y_ = (height() - (rows_ * (kHeight + 1)) + 2) / 2;
m_scope.resize(m_columns);
scope_.resize(columns_);
if (m_rows != oldRows) {
m_barPixmap = QPixmap(WIDTH, m_rows * (HEIGHT + 1));
if (rows_ != oldRows) {
barPixmap_ = QPixmap(kWidth, rows_ * (kHeight + 1));
for (uint i = 0; i < FADE_SIZE; ++i)
m_fade_bars[i] = QPixmap(WIDTH, m_rows * (HEIGHT + 1));
for (uint i = 0; i < kFadeSize; ++i)
fade_bars_[i] = QPixmap(kWidth, rows_ * (kHeight + 1));
m_yscale.resize(m_rows + 1);
yscale_.resize(rows_ + 1);
const uint PRE = 1,
PRO = 1; // PRE and PRO allow us to restrict the range somewhat
for (uint z = 0; z < m_rows; ++z)
m_yscale[z] = 1 - (log10(PRE + z) / log10(PRE + m_rows + PRO));
for (uint z = 0; z < rows_; ++z)
yscale_[z] = 1 - (log10(PRE + z) / log10(PRE + rows_ + PRO));
m_yscale[m_rows] = 0;
yscale_[rows_] = 0;
determineStep();
paletteChange(palette());
}
updateBandSize(columns_);
drawBackground();
}
@ -113,9 +115,9 @@ void BlockAnalyzer::determineStep() {
// I calculated the value 30 based on some trial and error
// the fall time of 30 is too slow on framerates above 50fps
const double fallTime = timeout() < 20 ? 20 * m_rows : 30 * m_rows;
const double fallTime = timeout() < 20 ? 20 * rows_ : 30 * rows_;
m_step = static_cast<double>(m_rows * timeout()) / fallTime;
step_ = static_cast<double>(rows_ * timeout()) / fallTime;
}
void BlockAnalyzer::framerateChanged() { // virtual
@ -127,15 +129,14 @@ void BlockAnalyzer::transform(Analyzer::Scope& s) {
float* front = static_cast<float*>(&s.front());
m_fht->spectrum(front);
m_fht->scale(front, 1.0 / 20);
fht_->spectrum(front);
fht_->scale(front, 1.0 / 20);
// the second half is pretty dull, so only show it if the user has a large
// analyzer
// by setting to m_scope.size() if large we prevent interpolation of large
// by setting to scope_.size() if large we prevent interpolation of large
// analyzers, this is good!
s.resize(m_scope.size() <= MAX_COLUMNS / 2 ? MAX_COLUMNS / 2
: m_scope.size());
s.resize(scope_.size() <= kMaxColumns / 2 ? kMaxColumns / 2 : scope_.size());
}
void BlockAnalyzer::analyze(QPainter& p, const Analyzer::Scope& s,
@ -150,7 +151,7 @@ void BlockAnalyzer::analyze(QPainter& p, const Analyzer::Scope& s,
// y represents the number of blanks
// y starts from the top and increases in units of blocks
// m_yscale looks similar to: { 0.7, 0.5, 0.25, 0.15, 0.1, 0 }
// yscale_ looks similar to: { 0.7, 0.5, 0.25, 0.15, 0.1, 0 }
// if it contains 6 elements there are 5 rows in the analyzer
if (!new_frame) {
@ -160,51 +161,55 @@ void BlockAnalyzer::analyze(QPainter& p, const Analyzer::Scope& s,
QPainter canvas_painter(&canvas_);
Analyzer::interpolate(s, m_scope);
Analyzer::interpolate(s, scope_);
// update the graphics with the new colour
if (psychedelic_enabled_) {
paletteChange(QPalette());
}
// Paint the background
canvas_painter.drawPixmap(0, 0, m_background);
canvas_painter.drawPixmap(0, 0, background_);
for (uint y, x = 0; x < m_scope.size(); ++x) {
for (uint y, x = 0; x < scope_.size(); ++x) {
// determine y
for (y = 0; m_scope[x] < m_yscale[y]; ++y)
continue;
for (y = 0; scope_[x] < yscale_[y]; ++y) continue;
// this is opposite to what you'd think, higher than y
// means the bar is lower than y (physically)
if (static_cast<float>(y) > m_store[x])
y = static_cast<int>(m_store[x] += m_step);
if (static_cast<float>(y) > store_[x])
y = static_cast<int>(store_[x] += step_);
else
m_store[x] = y;
store_[x] = y;
// if y is lower than m_fade_pos, then the bar has exceeded the height of
// if y is lower than fade_pos_, then the bar has exceeded the kHeight of
// the fadeout
// if the fadeout is quite faded now, then display the new one
if (y <= m_fade_pos[x] /*|| m_fade_intensity[x] < FADE_SIZE / 3*/) {
m_fade_pos[x] = y;
m_fade_intensity[x] = FADE_SIZE;
if (y <= fade_pos_[x] /*|| fade_intensity_[x] < kFadeSize / 3*/) {
fade_pos_[x] = y;
fade_intensity_[x] = kFadeSize;
}
if (m_fade_intensity[x] > 0) {
const uint offset = --m_fade_intensity[x];
const uint y = m_y + (m_fade_pos[x] * (HEIGHT + 1));
canvas_painter.drawPixmap(x * (WIDTH + 1), y, m_fade_bars[offset], 0, 0,
WIDTH, height() - y);
if (fade_intensity_[x] > 0) {
const uint offset = --fade_intensity_[x];
const uint y = y_ + (fade_pos_[x] * (kHeight + 1));
canvas_painter.drawPixmap(x * (kWidth + 1), y, fade_bars_[offset], 0, 0,
kWidth, height() - y);
}
if (m_fade_intensity[x] == 0) m_fade_pos[x] = m_rows;
if (fade_intensity_[x] == 0) fade_pos_[x] = rows_;
// REMEMBER: y is a number from 0 to m_rows, 0 means all blocks are glowing,
// m_rows means none are
canvas_painter.drawPixmap(x * (WIDTH + 1), y * (HEIGHT + 1) + m_y, *bar(),
0, y * (HEIGHT + 1), bar()->width(),
// REMEMBER: y is a number from 0 to rows_, 0 means all blocks are glowing,
// rows_ means none are
canvas_painter.drawPixmap(x * (kWidth + 1), y * (kHeight + 1) + y_, *bar(),
0, y * (kHeight + 1), bar()->width(),
bar()->height());
}
for (uint x = 0; x < m_store.size(); ++x)
canvas_painter.drawPixmap(x * (WIDTH + 1),
static_cast<int>(m_store[x]) * (HEIGHT + 1) + m_y,
m_topBarPixmap);
for (uint x = 0; x < store_.size(); ++x)
canvas_painter.drawPixmap(x * (kWidth + 1),
static_cast<int>(store_[x]) * (kHeight + 1) + y_,
topBarPixmap_);
p.drawPixmap(0, 0, canvas_);
}
@ -232,6 +237,12 @@ static inline void adjustToLimits(int& b, int& f, uint& amount) {
}
}
void BlockAnalyzer::psychedelicModeChanged(bool enabled) {
psychedelic_enabled_ = enabled;
// reset colours back to normal
paletteChange(QPalette());
}
/**
* Clever contrast function
*
@ -305,7 +316,8 @@ QColor ensureContrast(const QColor& bg, const QColor& fg, uint _amount = 150) {
if (static_cast<int>(_amount) > 0) adjustToLimits(bs, fs, _amount);
// see if we need to adjust the hue
if (static_cast<int>(_amount) > 0) fh += static_cast<int>(_amount); // cycles around;
if (static_cast<int>(_amount) > 0)
fh += static_cast<int>(_amount); // cycles around;
return QColor::fromHsv(fh, fs, fv);
}
@ -327,23 +339,34 @@ QColor ensureContrast(const QColor& bg, const QColor& fg, uint _amount = 150) {
void BlockAnalyzer::paletteChange(const QPalette&) {
const QColor bg = palette().color(QPalette::Background);
const QColor fg = ensureContrast(bg, palette().color(QPalette::Highlight));
QColor fg;
m_topBarPixmap.fill(fg);
if (psychedelic_enabled_) {
fg = getPsychedelicColor(scope_, 10, 75);
} else {
fg = ensureContrast(bg, palette().color(QPalette::Highlight));
}
const double dr = 15 * static_cast<double>(bg.red() - fg.red()) / (m_rows * 16);
const double dg = 15 * static_cast<double>(bg.green() - fg.green()) / (m_rows * 16);
const double db = 15 * static_cast<double>(bg.blue() - fg.blue()) / (m_rows * 16);
topBarPixmap_.fill(fg);
const double dr =
15 * static_cast<double>(bg.red() - fg.red()) / (rows_ * 16);
const double dg =
15 * static_cast<double>(bg.green() - fg.green()) / (rows_ * 16);
const double db =
15 * static_cast<double>(bg.blue() - fg.blue()) / (rows_ * 16);
const int r = fg.red(), g = fg.green(), b = fg.blue();
bar()->fill(bg);
QPainter p(bar());
for (int y = 0; static_cast<uint>(y) < m_rows; ++y)
for (int y = 0; static_cast<uint>(y) < rows_; ++y)
// graduate the fg color
p.fillRect(0, y * (HEIGHT + 1), WIDTH, HEIGHT,
QColor(r + static_cast<int>(dr * y), g + static_cast<int>(dg * y),
b + static_cast<int>(db * y)));
p.fillRect(
0, y * (kHeight + 1), kWidth, kHeight,
QColor(r + static_cast<int>(dr * y), g + static_cast<int>(dg * y),
b + static_cast<int>(db * y)));
{
const QColor bg = palette().color(QPalette::Background).dark(112);
@ -360,13 +383,15 @@ void BlockAnalyzer::paletteChange(const QPalette&) {
const int r = bg.red(), g = bg.green(), b = bg.blue();
// Precalculate all fade-bar pixmaps
for (uint y = 0; y < FADE_SIZE; ++y) {
m_fade_bars[y].fill(palette().color(QPalette::Background));
QPainter f(&m_fade_bars[y]);
for (int z = 0; static_cast<uint>(z) < m_rows; ++z) {
const double Y = 1.0 - (log10(FADE_SIZE - y) / log10(FADE_SIZE));
f.fillRect(0, z * (HEIGHT + 1), WIDTH, HEIGHT,
QColor(r + static_cast<int>(dr * Y), g + static_cast<int>(dg * Y), b + static_cast<int>(db * Y)));
for (uint y = 0; y < kFadeSize; ++y) {
fade_bars_[y].fill(palette().color(QPalette::Background));
QPainter f(&fade_bars_[y]);
for (int z = 0; static_cast<uint>(z) < rows_; ++z) {
const double Y = 1.0 - (log10(kFadeSize - y) / log10(kFadeSize));
f.fillRect(
0, z * (kHeight + 1), kWidth, kHeight,
QColor(r + static_cast<int>(dr * Y), g + static_cast<int>(dg * Y),
b + static_cast<int>(db * Y)));
}
}
}
@ -378,11 +403,16 @@ void BlockAnalyzer::drawBackground() {
const QColor bg = palette().color(QPalette::Background);
const QColor bgdark = bg.dark(112);
m_background.fill(bg);
background_.fill(bg);
QPainter p(&m_background);
for (int x = 0; (uint)x < m_columns; ++x)
for (int y = 0; (uint)y < m_rows; ++y)
p.fillRect(x * (WIDTH + 1), y * (HEIGHT + 1) + m_y, WIDTH, HEIGHT,
QPainter p(&background_);
if (p.paintEngine() == 0) {
return;
}
for (int x = 0; (uint)x < columns_; ++x)
for (int y = 0; (uint)y < rows_; ++y)
p.fillRect(x * (kWidth + 1), y * (kHeight + 1) + y_, kWidth, kHeight,
bgdark);
}

View File

@ -2,7 +2,7 @@
Copyright 2003-2005, Max Howell <max.howell@methylblue.com>
Copyright 2009-2010, David Sansome <davidsansome@gmail.com>
Copyright 2010, 2014, John Maguire <john.maguire@gmail.com>
Copyright 2014, Mark Furneaux <mark@romaco.ca>
Copyright 2014-2015, Mark Furneaux <mark@furneaux.ca>
Copyright 2014, Krzysztof A. Sobiecki <sobkas@gmail.com>
Clementine is free software: you can redistribute it and/or modify
@ -39,12 +39,12 @@ class BlockAnalyzer : public Analyzer::Base {
Q_INVOKABLE BlockAnalyzer(QWidget*);
~BlockAnalyzer();
static const uint HEIGHT;
static const uint WIDTH;
static const uint MIN_ROWS;
static const uint MIN_COLUMNS;
static const uint MAX_COLUMNS;
static const uint FADE_SIZE;
static const uint kHeight;
static const uint kWidth;
static const uint kMinRows;
static const uint kMinColumns;
static const uint kMaxColumns;
static const uint kFadeSize;
static const char* kName;
@ -54,29 +54,30 @@ class BlockAnalyzer : public Analyzer::Base {
virtual void resizeEvent(QResizeEvent*);
virtual void paletteChange(const QPalette&);
virtual void framerateChanged();
virtual void psychedelicModeChanged(bool);
void drawBackground();
void determineStep();
private:
QPixmap* bar() { return &m_barPixmap; }
QPixmap* bar() { return &barPixmap_; }
uint m_columns, m_rows; // number of rows and columns of blocks
uint m_y; // y-offset from top of widget
QPixmap m_barPixmap;
QPixmap m_topBarPixmap;
QPixmap m_background;
uint columns_, rows_; // number of rows and columns of blocks
uint y_; // y-offset from top of widget
QPixmap barPixmap_;
QPixmap topBarPixmap_;
QPixmap background_;
QPixmap canvas_;
Analyzer::Scope m_scope; // so we don't create a vector every frame
std::vector<float> m_store; // current bar heights
std::vector<float> m_yscale;
Analyzer::Scope scope_; // so we don't create a vector every frame
std::vector<float> store_; // current bar kHeights
std::vector<float> yscale_;
// FIXME why can't I namespace these? c++ issue?
std::vector<QPixmap> m_fade_bars;
std::vector<uint> m_fade_pos;
std::vector<int> m_fade_intensity;
std::vector<QPixmap> fade_bars_;
std::vector<uint> fade_pos_;
std::vector<int> fade_intensity_;
float m_step; // rows to fall per frame
float step_; // rows to fall per frame
};
#endif // ANALYZERS_BLOCKANALYZER_H_

View File

@ -2,7 +2,7 @@
Copyright 2004, Max Howell <max.howell@methylblue.com>
Copyright 2009-2010, David Sansome <davidsansome@gmail.com>
Copyright 2010, 2014, John Maguire <john.maguire@gmail.com>
Copyright 2014, Mark Furneaux <mark@romaco.ca>
Copyright 2014-2015, Mark Furneaux <mark@furneaux.ca>
Copyright 2014, Krzysztof Sobiecki <sobkas@gmail.com>
Clementine is free software: you can redistribute it and/or modify
@ -28,69 +28,78 @@
using Analyzer::Scope;
const uint BoomAnalyzer::kColumnWidth = 4;
const uint BoomAnalyzer::kMaxBandCount = 256;
const uint BoomAnalyzer::kMinBandCount = 32;
const char* BoomAnalyzer::kName =
QT_TRANSLATE_NOOP("AnalyzerContainer", "Boom analyzer");
BoomAnalyzer::BoomAnalyzer(QWidget* parent)
: Analyzer::Base(parent, 9),
K_barHeight(1.271) // 1.471
bands_(0),
scope_(kMinBandCount),
fg_(palette().color(QPalette::Highlight)),
K_barHeight_(1.271) // 1.471
,
F_peakSpeed(1.103) // 1.122
F_peakSpeed_(1.103) // 1.122
,
F(1.0),
bar_height(BAND_COUNT, 0),
peak_height(BAND_COUNT, 0),
peak_speed(BAND_COUNT, 0.01),
barPixmap(COLUMN_WIDTH, 50) {}
F_(1.0),
bar_height_(kMaxBandCount, 0),
peak_height_(kMaxBandCount, 0),
peak_speed_(kMaxBandCount, 0.01),
barPixmap_(kColumnWidth, 50) {
setMinimumWidth(kMinBandCount * (kColumnWidth + 1) - 1);
setMaximumWidth(kMaxBandCount * (kColumnWidth + 1) - 1);
}
void BoomAnalyzer::changeK_barHeight(int newValue) {
K_barHeight = static_cast<double>(newValue) / 1000;
K_barHeight_ = static_cast<double>(newValue) / 1000;
}
void BoomAnalyzer::changeF_peakSpeed(int newValue) {
F_peakSpeed = static_cast<double>(newValue) / 1000;
F_peakSpeed_ = static_cast<double>(newValue) / 1000;
}
void BoomAnalyzer::resizeEvent(QResizeEvent*) { init(); }
void BoomAnalyzer::resizeEvent(QResizeEvent* e) {
QWidget::resizeEvent(e);
void BoomAnalyzer::init() {
const uint HEIGHT = height() - 2;
const double h = 1.2 / HEIGHT;
F = static_cast<double>(HEIGHT) / (log10(256) * 1.1 /*<- max. amplitude*/);
bands_ = qMin(
static_cast<uint>(static_cast<double>(width() + 1) / (kColumnWidth + 1)) +
1,
kMaxBandCount);
scope_.resize(bands_);
barPixmap = QPixmap(COLUMN_WIDTH - 2, HEIGHT);
F_ = static_cast<double>(HEIGHT) / (log10(256) * 1.1 /*<- max. amplitude*/);
barPixmap_ = QPixmap(kColumnWidth - 2, HEIGHT);
canvas_ = QPixmap(size());
canvas_.fill(palette().color(QPalette::Background));
QPainter p(&barPixmap);
QPainter p(&barPixmap_);
for (uint y = 0; y < HEIGHT; ++y) {
const double F = static_cast<double>(y) * h;
p.setPen(QColor(qMax(0, 255 - static_cast<int>(229.0 * F)),
qMax(0, 255 - static_cast<int>(229.0 * F)),
qMax(0, 255 - static_cast<int>(191.0 * F))));
p.drawLine(0, y, COLUMN_WIDTH - 2, y);
p.drawLine(0, y, kColumnWidth - 2, y);
}
updateBandSize(bands_);
}
void BoomAnalyzer::transform(Scope& s) {
float* front = static_cast<float*>(&s.front());
m_fht->spectrum(front);
m_fht->scale(front, 1.0 / 60);
fht_->spectrum(front);
fht_->scale(front, 1.0 / 50);
Scope scope(32, 0);
const uint xscale[] = {0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10,
11, 12, 13, 14, 15, 16, 17, 19, 24, 29, 36,
43, 52, 63, 76, 91, 108, 129, 153, 182, 216, 255};
for (uint j, i = 0; i < 32; i++)
for (j = xscale[i]; j < xscale[i + 1]; j++)
if (s[j] > scope[i]) scope[i] = s[j];
s = scope;
s.resize(scope_.size() <= kMaxBandCount / 2 ? kMaxBandCount / 2
: scope_.size());
}
void BoomAnalyzer::analyze(QPainter& p, const Scope& scope, bool new_frame) {
@ -104,47 +113,70 @@ void BoomAnalyzer::analyze(QPainter& p, const Scope& scope, bool new_frame) {
QPainter canvas_painter(&canvas_);
canvas_.fill(palette().color(QPalette::Background));
for (uint i = 0, x = 0, y; i < BAND_COUNT; ++i, x += COLUMN_WIDTH + 1) {
h = log10(scope[i] * 256.0) * F;
Analyzer::interpolate(scope, scope_);
// update the graphics with the new colour
if (psychedelic_enabled_) {
paletteChange(QPalette());
}
for (uint i = 0, x = 0, y; i < bands_; ++i, x += kColumnWidth + 1) {
h = log10(scope_[i] * 256.0) * F_;
if (h > MAX_HEIGHT) h = MAX_HEIGHT;
if (h > bar_height[i]) {
bar_height[i] = h;
if (h > bar_height_[i]) {
bar_height_[i] = h;
if (h > peak_height[i]) {
peak_height[i] = h;
peak_speed[i] = 0.01;
if (h > peak_height_[i]) {
peak_height_[i] = h;
peak_speed_[i] = 0.01;
} else {
goto peak_handling;
}
} else {
if (bar_height[i] > 0.0) {
bar_height[i] -= K_barHeight; // 1.4
if (bar_height[i] < 0.0) bar_height[i] = 0.0;
if (bar_height_[i] > 0.0) {
bar_height_[i] -= K_barHeight_; // 1.4
if (bar_height_[i] < 0.0) bar_height_[i] = 0.0;
}
peak_handling:
if (peak_height[i] > 0.0) {
peak_height[i] -= peak_speed[i];
peak_speed[i] *= F_peakSpeed; // 1.12
if (peak_height_[i] > 0.0) {
peak_height_[i] -= peak_speed_[i];
peak_speed_[i] *= F_peakSpeed_; // 1.12
if (peak_height[i] < bar_height[i]) peak_height[i] = bar_height[i];
if (peak_height[i] < 0.0) peak_height[i] = 0.0;
if (peak_height_[i] < bar_height_[i]) peak_height_[i] = bar_height_[i];
if (peak_height_[i] < 0.0) peak_height_[i] = 0.0;
}
}
y = height() - uint(bar_height[i]);
canvas_painter.drawPixmap(x + 1, y, barPixmap, 0, y, -1, -1);
canvas_painter.setPen(palette().color(QPalette::Highlight));
if (bar_height[i] > 0)
canvas_painter.drawRect(x, y, COLUMN_WIDTH - 1, height() - y - 1);
y = height() - uint(bar_height_[i]);
canvas_painter.drawPixmap(x + 1, y, barPixmap_, 0, y, -1, -1);
canvas_painter.setPen(fg_);
if (bar_height_[i] > 0)
canvas_painter.drawRect(x, y, kColumnWidth - 1, height() - y - 1);
y = height() - uint(peak_height[i]);
canvas_painter.setPen(palette().color(QPalette::Base));
canvas_painter.drawLine(x, y, x + COLUMN_WIDTH - 1, y);
y = height() - uint(peak_height_[i]);
canvas_painter.setPen(palette().color(QPalette::Midlight));
canvas_painter.drawLine(x, y, x + kColumnWidth - 1, y);
}
p.drawPixmap(0, 0, canvas_);
}
void BoomAnalyzer::psychedelicModeChanged(bool enabled) {
psychedelic_enabled_ = enabled;
// reset colours back to normal
paletteChange(QPalette());
}
void BoomAnalyzer::paletteChange(const QPalette&) {
if (psychedelic_enabled_) {
fg_ = getPsychedelicColor(scope_, 50, 100);
} else {
// the highlight colour changes when the main window loses focus,
// so we use save and use the focused colour
fg_ = palette().color(QPalette::Highlight);
}
}

View File

@ -1,7 +1,7 @@
/* This file is part of Clementine.
Copyright 2004, Max Howell <max.howell@methylblue.com>
Copyright 2009-2010, David Sansome <davidsansome@gmail.com>
Copyright 2014, Mark Furneaux <mark@romaco.ca>
Copyright 2014-2015, Mark Furneaux <mark@furneaux.ca>
Copyright 2014, Krzysztof Sobiecki <sobkas@gmail.com>
Copyright 2014, John Maguire <john.maguire@gmail.com>
@ -27,10 +27,6 @@
#include "analyzerbase.h"
/**
@author Max Howell
*/
class BoomAnalyzer : public Analyzer::Base {
Q_OBJECT
@ -39,9 +35,9 @@ class BoomAnalyzer : public Analyzer::Base {
static const char* kName;
virtual void init();
virtual void transform(Analyzer::Scope& s);
virtual void analyze(QPainter& p, const Analyzer::Scope&, bool new_frame);
virtual void psychedelicModeChanged(bool);
public slots:
void changeK_barHeight(int);
@ -49,17 +45,23 @@ class BoomAnalyzer : public Analyzer::Base {
protected:
void resizeEvent(QResizeEvent* e);
void paletteChange(const QPalette&);
static const uint COLUMN_WIDTH = 4;
static const uint BAND_COUNT = 32;
static const uint kColumnWidth;
static const uint kMaxBandCount;
static const uint kMinBandCount;
double K_barHeight, F_peakSpeed, F;
uint bands_;
Analyzer::Scope scope_;
QColor fg_;
std::vector<float> bar_height;
std::vector<float> peak_height;
std::vector<float> peak_speed;
double K_barHeight_, F_peakSpeed_, F_;
QPixmap barPixmap;
std::vector<float> bar_height_;
std::vector<float> peak_height_;
std::vector<float> peak_speed_;
QPixmap barPixmap_;
QPixmap canvas_;
};

View File

@ -24,70 +24,69 @@
#include <string.h>
#include "fht.h"
FHT::FHT(int n) : m_buf(0), m_tab(0), m_log(0) {
FHT::FHT(int n) : buf_(0), tab_(0), log_(0) {
if (n < 3) {
m_num = 0;
m_exp2 = -1;
num_ = 0;
exp2_ = -1;
return;
}
m_exp2 = n;
m_num = 1 << n;
exp2_ = n;
num_ = 1 << n;
if (n > 3) {
m_buf = new float[m_num];
m_tab = new float[m_num * 2];
buf_ = new float[num_];
tab_ = new float[num_ * 2];
makeCasTable();
}
}
FHT::~FHT() {
delete[] m_buf;
delete[] m_tab;
delete[] m_log;
delete[] buf_;
delete[] tab_;
delete[] log_;
}
void FHT::makeCasTable(void) {
float d, *costab, *sintab;
int ul, ndiv2 = m_num / 2;
int ul, ndiv2 = num_ / 2;
for (costab = m_tab, sintab = m_tab + m_num / 2 + 1, ul = 0; ul < m_num;
ul++) {
for (costab = tab_, sintab = tab_ + num_ / 2 + 1, ul = 0; ul < num_; ul++) {
d = M_PI * ul / ndiv2;
*costab = *sintab = cos(d);
costab += 2, sintab += 2;
if (sintab > m_tab + m_num * 2) sintab = m_tab + 1;
if (sintab > tab_ + num_ * 2) sintab = tab_ + 1;
}
}
float* FHT::copy(float* d, float* s) {
return static_cast<float*>(memcpy(d, s, m_num * sizeof(float)));
return static_cast<float*>(memcpy(d, s, num_ * sizeof(float)));
}
float* FHT::clear(float* d) {
return static_cast<float*>(memset(d, 0, m_num * sizeof(float)));
return static_cast<float*>(memset(d, 0, num_ * sizeof(float)));
}
void FHT::scale(float* p, float d) {
for (int i = 0; i < (m_num / 2); i++) *p++ *= d;
for (int i = 0; i < (num_ / 2); i++) *p++ *= d;
}
void FHT::ewma(float* d, float* s, float w) {
for (int i = 0; i < (m_num / 2); i++, d++, s++) *d = *d * w + *s * (1 - w);
for (int i = 0; i < (num_ / 2); i++, d++, s++) *d = *d * w + *s * (1 - w);
}
void FHT::logSpectrum(float* out, float* p) {
int n = m_num / 2, i, j, k, *r;
if (!m_log) {
m_log = new int[n];
int n = num_ / 2, i, j, k, *r;
if (!log_) {
log_ = new int[n];
float f = n / log10(static_cast<double>(n));
for (i = 0, r = m_log; i < n; i++, r++) {
for (i = 0, r = log_; i < n; i++, r++) {
j = static_cast<int>(rint(log10(i + 1.0) * f));
*r = j >= n ? n - 1 : j;
}
}
semiLogSpectrum(p);
*out++ = *p = *p / 100;
for (k = i = 1, r = m_log; i < n; i++) {
for (k = i = 1, r = log_; i < n; i++) {
j = *r++;
if (i == j) {
*out++ = p[i];
@ -102,7 +101,7 @@ void FHT::logSpectrum(float* out, float* p) {
void FHT::semiLogSpectrum(float* p) {
float e;
power2(p);
for (int i = 0; i < (m_num / 2); i++, p++) {
for (int i = 0; i < (num_ / 2); i++, p++) {
e = 10.0 * log10(sqrt(*p * .5));
*p = e < 0 ? 0 : e;
}
@ -110,31 +109,31 @@ void FHT::semiLogSpectrum(float* p) {
void FHT::spectrum(float* p) {
power2(p);
for (int i = 0; i < (m_num / 2); i++, p++)
for (int i = 0; i < (num_ / 2); i++, p++)
*p = static_cast<float>(sqrt(*p * .5));
}
void FHT::power(float* p) {
power2(p);
for (int i = 0; i < (m_num / 2); i++) *p++ *= .5;
for (int i = 0; i < (num_ / 2); i++) *p++ *= .5;
}
void FHT::power2(float* p) {
int i;
float* q;
_transform(p, m_num, 0);
_transform(p, num_, 0);
*p = (*p * *p), *p += *p, p++;
for (i = 1, q = p + m_num - 2; i < (m_num / 2); i++, --q)
for (i = 1, q = p + num_ - 2; i < (num_ / 2); i++, --q)
*p = (*p * *p) + (*q * *q), p++;
}
void FHT::transform(float* p) {
if (m_num == 8)
if (num_ == 8)
transform8(p);
else
_transform(p, m_num, 0);
_transform(p, num_, 0);
}
void FHT::transform8(float* p) {
@ -173,19 +172,19 @@ void FHT::_transform(float* p, int n, int k) {
int i, j, ndiv2 = n / 2;
float a, *t1, *t2, *t3, *t4, *ptab, *pp;
for (i = 0, t1 = m_buf, t2 = m_buf + ndiv2, pp = &p[k]; i < ndiv2; i++)
for (i = 0, t1 = buf_, t2 = buf_ + ndiv2, pp = &p[k]; i < ndiv2; i++)
*t1++ = *pp++, *t2++ = *pp++;
memcpy(p + k, m_buf, sizeof(float) * n);
memcpy(p + k, buf_, sizeof(float) * n);
_transform(p, ndiv2, k);
_transform(p, ndiv2, k + ndiv2);
j = m_num / ndiv2 - 1;
t1 = m_buf;
j = num_ / ndiv2 - 1;
t1 = buf_;
t2 = t1 + ndiv2;
t3 = p + k + ndiv2;
ptab = m_tab;
ptab = tab_;
pp = p + k;
a = *ptab++ * *t3++;
@ -202,5 +201,5 @@ void FHT::_transform(float* p, int n, int k) {
*t1++ = *pp + a;
*t2++ = *pp++ - a;
}
memcpy(p + k, m_buf, sizeof(float) * n);
memcpy(p + k, buf_, sizeof(float) * n);
}

View File

@ -32,11 +32,11 @@
* [1] Computer in Physics, Vol. 9, No. 4, Jul/Aug 1995 pp 373-379
*/
class FHT {
int m_exp2;
int m_num;
float* m_buf;
float* m_tab;
int* m_log;
int exp2_;
int num_;
float* buf_;
float* tab_;
int* log_;
/**
* Create a table of "cas" (cosine and sine) values.
@ -59,8 +59,8 @@ class FHT {
explicit FHT(int);
~FHT();
inline int sizeExp() const { return m_exp2; }
inline int size() const { return m_num; }
inline int sizeExp() const { return exp2_; }
inline int size() const { return num_; }
float* copy(float*, float*);
float* clear(float*);
void scale(float*, float);

View File

@ -57,7 +57,7 @@ NyanCatAnalyzer::NyanCatAnalyzer(QWidget* parent)
}
}
void NyanCatAnalyzer::transform(Scope& s) { m_fht->spectrum(&s.front()); }
void NyanCatAnalyzer::transform(Scope& s) { fht_->spectrum(&s.front()); }
void NyanCatAnalyzer::timerEvent(QTimerEvent* e) {
if (e->timerId() == timer_id_) {
@ -74,7 +74,8 @@ void NyanCatAnalyzer::resizeEvent(QResizeEvent* e) {
buffer_[1] = QPixmap();
available_rainbow_width_ = width() - kCatWidth + kRainbowOverlap;
px_per_frame_ = static_cast<float>(available_rainbow_width_) / (kHistorySize - 1) + 1;
px_per_frame_ =
static_cast<float>(available_rainbow_width_) / (kHistorySize - 1) + 1;
x_offset_ = px_per_frame_ * (kHistorySize - 1) - available_rainbow_width_;
}
@ -112,11 +113,13 @@ void NyanCatAnalyzer::analyze(QPainter& p, const Analyzer::Scope& s,
QPointF* dest = polyline;
float* source = history_;
const float top_of_cat = static_cast<float>(height()) / 2 - static_cast<float>(kCatHeight) / 2;
const float top_of_cat =
static_cast<float>(height()) / 2 - static_cast<float>(kCatHeight) / 2;
for (int band = 0; band < kRainbowBands; ++band) {
// Calculate the Y position of this band.
const float y =
static_cast<float>(kCatHeight) / (kRainbowBands + 1) * (band + 0.5) + top_of_cat;
static_cast<float>(kCatHeight) / (kRainbowBands + 1) * (band + 0.5) +
top_of_cat;
// Add each point in the line.
for (int x = 0; x < kHistorySize; ++x) {

View File

@ -1,6 +1,6 @@
/* This file is part of Clementine.
Copyright 2014, Alibek Omarov <a1ba.omarov@gmail.com>
Copyright 2014, Mark Furneaux <mark@romaco.ca>
Copyright 2014-2015, Mark Furneaux <mark@furneaux.ca>
Copyright 2014, Krzysztof Sobiecki <sobkas@gmail.com>
Copyright 2014, David Sansome <me@davidsansome.com>
@ -57,7 +57,7 @@ RainbowDashAnalyzer::RainbowDashAnalyzer(QWidget* parent)
}
}
void RainbowDashAnalyzer::transform(Scope& s) { m_fht->spectrum(&s.front()); }
void RainbowDashAnalyzer::transform(Scope& s) { fht_->spectrum(&s.front()); }
void RainbowDashAnalyzer::timerEvent(QTimerEvent* e) {
if (e->timerId() == timer_id_) {
@ -74,7 +74,8 @@ void RainbowDashAnalyzer::resizeEvent(QResizeEvent* e) {
buffer_[1] = QPixmap();
available_rainbow_width_ = width() - kDashWidth + kRainbowOverlap;
px_per_frame_ = static_cast<float>(available_rainbow_width_) / (kHistorySize - 1) + 1;
px_per_frame_ =
static_cast<float>(available_rainbow_width_) / (kHistorySize - 1) + 1;
x_offset_ = px_per_frame_ * (kHistorySize - 1) - available_rainbow_width_;
}
@ -111,12 +112,13 @@ void RainbowDashAnalyzer::analyze(QPainter& p, const Analyzer::Scope& s,
QPointF* dest = polyline;
float* source = history_;
const float top_of_Dash = static_cast<float>(height()) / 2 - static_cast<float>(kRainbowHeight) / 2;
const float top_of_Dash = static_cast<float>(height()) / 2 -
static_cast<float>(kRainbowHeight) / 2;
for (int band = 0; band < kRainbowBands; ++band) {
// Calculate the Y position of this band.
const float y =
static_cast<float>(kRainbowHeight) / (kRainbowBands + 1) * (band + 0.5) +
top_of_Dash;
const float y = static_cast<float>(kRainbowHeight) / (kRainbowBands + 1) *
(band + 0.5) +
top_of_Dash;
// Add each point in the line.
for (int x = 0; x < kHistorySize; ++x) {

View File

@ -1,6 +1,6 @@
/* This file is part of Clementine.
Copyright 2014, Alibek Omarov <a1ba.omarov@gmail.com>
Copyright 2014, Mark Furneaux <mark@romaco.ca>
Copyright 2014-2015, Mark Furneaux <mark@furneaux.ca>
Copyright 2014, Krzysztof Sobiecki <sobkas@gmail.com>
Clementine is free software: you can redistribute it and/or modify

View File

@ -2,7 +2,7 @@
Copyright 2004, Melchior FRANZ <mfranz@kde.org>
Copyright 2009-2010, David Sansome <davidsansome@gmail.com>
Copyright 2010, 2014, John Maguire <john.maguire@gmail.com>
Copyright 2014, Mark Furneaux <mark@romaco.ca>
Copyright 2014-2015, Mark Furneaux <mark@furneaux.ca>
Copyright 2014, Krzysztof Sobiecki <sobkas@gmail.com>
Clementine is free software: you can redistribute it and/or modify
@ -31,7 +31,8 @@ using Analyzer::Scope;
const char* Sonogram::kName =
QT_TRANSLATE_NOOP("AnalyzerContainer", "Sonogram");
Sonogram::Sonogram(QWidget* parent) : Analyzer::Base(parent, 9) {}
Sonogram::Sonogram(QWidget* parent)
: Analyzer::Base(parent, 9), scope_size_(128) {}
Sonogram::~Sonogram() {}
@ -45,10 +46,16 @@ void Sonogram::resizeEvent(QResizeEvent* e) {
canvas_ = QPixmap(size());
canvas_.fill(palette().color(QPalette::Background));
updateBandSize(scope_size_);
}
void Sonogram::psychedelicModeChanged(bool enabled) {
psychedelic_enabled_ = enabled;
updateBandSize(scope_size_);
}
void Sonogram::analyze(QPainter& p, const Scope& s, bool new_frame) {
if (!new_frame) {
if (!new_frame || engine_->state() == Engine::Paused) {
p.drawPixmap(0, 0, canvas_);
return;
}
@ -60,20 +67,45 @@ void Sonogram::analyze(QPainter& p, const Scope& s, bool new_frame) {
canvas_painter.drawPixmap(0, 0, canvas_, 1, 0, x, -1);
Scope::const_iterator it = s.begin(), end = s.end();
for (int y = height() - 1; y;) {
if (it >= end || *it < .005)
c = palette().color(QPalette::Background);
else if (*it < .05)
c.setHsv(95, 255, 255 - static_cast<int>(*it * 4000.0));
else if (*it < 1.0)
c.setHsv(95 - static_cast<int>(*it * 90.0), 255, 255);
else
c = Qt::red;
if (scope_size_ != s.size()) {
scope_size_ = s.size();
updateBandSize(scope_size_);
}
canvas_painter.setPen(c);
canvas_painter.drawPoint(x, y--);
if (psychedelic_enabled_) {
c = getPsychedelicColor(s, 20, 100);
for (int y = height() - 1; y;) {
if (it >= end || *it < .005) {
c = palette().color(QPalette::Background);
} else if (*it < .05) {
c.setHsv(c.hue(), c.saturation(), 255 - static_cast<int>(*it * 4000.0));
} else if (*it < 1.0) {
c.setHsv((c.hue() + static_cast<int>(*it * 90.0)) % 255, 255, 255);
} else {
c = getPsychedelicColor(s, 10, 50);
}
if (it < end) ++it;
canvas_painter.setPen(c);
canvas_painter.drawPoint(x, y--);
if (it < end) ++it;
}
} else {
for (int y = height() - 1; y;) {
if (it >= end || *it < .005)
c = palette().color(QPalette::Background);
else if (*it < .05)
c.setHsv(95, 255, 255 - static_cast<int>(*it * 4000.0));
else if (*it < 1.0)
c.setHsv(95 - static_cast<int>(*it * 90.0), 255, 255);
else
c = Qt::red;
canvas_painter.setPen(c);
canvas_painter.drawPoint(x, y--);
if (it < end) ++it;
}
}
canvas_painter.end();
@ -83,11 +115,11 @@ void Sonogram::analyze(QPainter& p, const Scope& s, bool new_frame) {
void Sonogram::transform(Scope& scope) {
float* front = static_cast<float*>(&scope.front());
m_fht->power2(front);
m_fht->scale(front, 1.0 / 256);
scope.resize(m_fht->size() / 2);
fht_->power2(front);
fht_->scale(front, 1.0 / 256);
scope.resize(fht_->size() / 2);
}
void Sonogram::demo(QPainter& p) {
analyze(p, Scope(m_fht->size(), 0), new_frame_);
analyze(p, Scope(fht_->size(), 0), new_frame_);
}

View File

@ -3,6 +3,7 @@
Copyright 2009-2010, David Sansome <davidsansome@gmail.com>
Copyright 2014, Krzysztof Sobiecki <sobkas@gmail.com>
Copyright 2014, John Maguire <john.maguire@gmail.com>
Copyright 2015, Mark Furneaux <mark@furneaux.ca>
Clementine is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
@ -25,10 +26,7 @@
#define ANALYZERS_SONOGRAM_H_
#include "analyzerbase.h"
/**
@author Melchior FRANZ
*/
#include "engines/enginebase.h"
class Sonogram : public Analyzer::Base {
Q_OBJECT
@ -43,8 +41,10 @@ class Sonogram : public Analyzer::Base {
void transform(Analyzer::Scope&);
void demo(QPainter& p);
void resizeEvent(QResizeEvent*);
void psychedelicModeChanged(bool);
QPixmap canvas_;
int scope_size_;
};
#endif // ANALYZERS_SONOGRAM_H_

View File

@ -2,7 +2,7 @@
Copyright 2003, Stanislav Karchebny <berkus@users.sf.net>
Copyright 2003, Max Howell <max.howell@methylblue.com>
Copyright 2009-2010, David Sansome <davidsansome@gmail.com>
Copyright 2014, Mark Furneaux <mark@romaco.ca>
Copyright 2014-2015, Mark Furneaux <mark@furneaux.ca>
Copyright 2014, Krzysztof Sobiecki <sobkas@gmail.com>
Copyright 2014, John Maguire <john.maguire@gmail.com>
@ -21,7 +21,7 @@
*/
/* Original Author: Stanislav Karchebny <berkus@users.sf.net> 2003
* Original Author: Max Howell <max.howell@methylblue.com> 2003
* Original Author: Max Howell <max.howell@methylblue.com> 2003
*/
#include <cmath>
@ -42,58 +42,64 @@ void TurbineAnalyzer::analyze(QPainter& p, const Scope& scope, bool new_frame) {
float h;
const uint hd2 = height() / 2;
const uint MAX_HEIGHT = hd2 - 1;
const uint kMaxHeight = hd2 - 1;
QPainter canvas_painter(&canvas_);
canvas_.fill(palette().color(QPalette::Background));
for (uint i = 0, x = 0, y; i < BAND_COUNT; ++i, x += COLUMN_WIDTH + 1) {
h = log10(scope[i] * 256.0) * F * 0.5;
Analyzer::interpolate(scope, scope_);
if (h > MAX_HEIGHT) h = MAX_HEIGHT;
// update the graphics with the new colour
if (psychedelic_enabled_) {
paletteChange(QPalette());
}
if (h > bar_height[i]) {
bar_height[i] = h;
for (uint i = 0, x = 0, y; i < bands_; ++i, x += kColumnWidth + 1) {
h = log10(scope_[i] * 256.0) * F_ * 0.5;
if (h > peak_height[i]) {
peak_height[i] = h;
peak_speed[i] = 0.01;
if (h > kMaxHeight) h = kMaxHeight;
if (h > bar_height_[i]) {
bar_height_[i] = h;
if (h > peak_height_[i]) {
peak_height_[i] = h;
peak_speed_[i] = 0.01;
} else {
goto peak_handling;
}
} else {
if (bar_height[i] > 0.0) {
bar_height[i] -= K_barHeight; // 1.4
if (bar_height[i] < 0.0) bar_height[i] = 0.0;
if (bar_height_[i] > 0.0) {
bar_height_[i] -= K_barHeight_; // 1.4
if (bar_height_[i] < 0.0) bar_height_[i] = 0.0;
}
peak_handling:
if (peak_height[i] > 0.0) {
peak_height[i] -= peak_speed[i];
peak_speed[i] *= F_peakSpeed; // 1.12
if (peak_height_[i] > 0.0) {
peak_height_[i] -= peak_speed_[i];
peak_speed_[i] *= F_peakSpeed_; // 1.12
if (peak_height[i] < bar_height[i]) peak_height[i] = bar_height[i];
if (peak_height[i] < 0.0) peak_height[i] = 0.0;
if (peak_height_[i] < bar_height_[i]) peak_height_[i] = bar_height_[i];
if (peak_height_[i] < 0.0) peak_height_[i] = 0.0;
}
}
y = hd2 - static_cast<uint>(bar_height[i]);
canvas_painter.drawPixmap(x + 1, y, barPixmap, 0, y, -1, -1);
canvas_painter.drawPixmap(x + 1, hd2, barPixmap, 0,
static_cast<int>(bar_height[i]),
-1, -1);
y = hd2 - static_cast<uint>(bar_height_[i]);
canvas_painter.drawPixmap(x + 1, y, barPixmap_, 0, y, -1, -1);
canvas_painter.drawPixmap(x + 1, hd2, barPixmap_, 0,
static_cast<int>(bar_height_[i]), -1, -1);
canvas_painter.setPen(palette().color(QPalette::Highlight));
if (bar_height[i] > 0)
canvas_painter.drawRect(x, y, COLUMN_WIDTH - 1,
static_cast<int>(bar_height[i]) * 2 - 1);
canvas_painter.setPen(fg_);
if (bar_height_[i] > 0)
canvas_painter.drawRect(x, y, kColumnWidth - 1,
static_cast<int>(bar_height_[i]) * 2 - 1);
const uint x2 = x + COLUMN_WIDTH - 1;
canvas_painter.setPen(palette().color(QPalette::Base));
y = hd2 - uint(peak_height[i]);
const uint x2 = x + kColumnWidth - 1;
canvas_painter.setPen(palette().color(QPalette::Midlight));
y = hd2 - uint(peak_height_[i]);
canvas_painter.drawLine(x, y, x2, y);
y = hd2 + uint(peak_height[i]);
y = hd2 + uint(peak_height_[i]);
canvas_painter.drawLine(x, y, x2, y);
}

View File

@ -47,7 +47,7 @@
#include <QVariant>
const char* Database::kDatabaseFilename = "clementine.db";
const int Database::kSchemaVersion = 49;
const int Database::kSchemaVersion = 50;
const char* Database::kMagicAllSongsTables = "%allsongstables";
int Database::sNextConnectionId = 1;

View File

@ -75,6 +75,10 @@ GlobalShortcuts::GlobalShortcuts(QWidget* parent)
AddShortcut("toggle_last_fm_scrobbling",
tr("Enable/disable Last.fm scrobbling"),
SIGNAL(ToggleScrobbling()));
AddShortcut("love_last_fm_scrobbling", tr("Love (Last.fm scrobbling)"),
SIGNAL(Love()));
AddShortcut("ban_last_fm_scrobbling", tr("Ban (Last.fm scrobbling)"),
SIGNAL(Ban()));
AddRatingShortcut("rate_zero_star", tr("Rate the current song 0 stars"),
rating_signals_mapper_, 0);

View File

@ -60,7 +60,7 @@ class GlobalShortcuts : public QWidget {
void Unregister();
void Register();
signals:
signals:
void Play();
void Pause();
void PlayPause();
@ -80,6 +80,8 @@ class GlobalShortcuts : public QWidget {
void CycleShuffleMode();
void CycleRepeatMode();
void ToggleScrobbling();
void Love();
void Ban();
private:
void AddShortcut(const QString& id, const QString& name, const char* signal,

View File

@ -244,7 +244,8 @@ int Mpris1Player::GetCaps(Engine::State state) const {
if (playlists->active()->next_row() != -1) {
caps |= CAN_GO_NEXT;
}
if (playlists->active()->previous_row() != -1) {
if (playlists->active()->previous_row() != -1 ||
app_->player()->PreviousWouldRestartTrack()) {
caps |= CAN_GO_PREV;
}
}

View File

@ -51,7 +51,8 @@ const QStringList OrganiseFormat::kKnownTags = QStringList() << "title"
<< "extension"
<< "performer"
<< "grouping"
<< "lyrics";
<< "lyrics"
<< "originalyear";
// From http://en.wikipedia.org/wiki/8.3_filename#Directory_table
const char OrganiseFormat::kInvalidFatCharacters[] = "\"*/\\:<>?|";
@ -200,6 +201,8 @@ QString OrganiseFormat::TagValue(const QString& tag, const Song& song) const {
value = song.comment();
else if (tag == "year")
value = QString::number(song.year());
else if (tag == "originalyear")
value = QString::number(song.effective_originalyear());
else if (tag == "track")
value = QString::number(song.track());
else if (tag == "disc")

View File

@ -63,7 +63,8 @@ Player::Player(Application* app, QObject* parent)
nb_errors_received_(0),
volume_before_mute_(50),
last_pressed_previous_(QDateTime::currentDateTime()),
menu_previousmode_(PreviousBehaviour_DontRestart) {
menu_previousmode_(PreviousBehaviour_DontRestart),
seek_step_sec_(10) {
settings_.beginGroup("Player");
SetVolume(settings_.value("volume", 50).toInt());
@ -103,12 +104,24 @@ void Player::ReloadSettings() {
menu_previousmode_ = PreviousBehaviour(
s.value("menu_previousmode", PreviousBehaviour_DontRestart).toInt());
seek_step_sec_ = s.value("seek_step_sec", 10).toInt();
s.endGroup();
engine_->ReloadSettings();
}
void Player::HandleLoadResult(const UrlHandler::LoadResult& result) {
// Might've been an async load, so check we're still on the same item
shared_ptr<PlaylistItem> item = app_->playlist_manager()->active()->current_item();
if (!item) {
loading_async_ = QUrl();
return;
}
if (item->Url() != result.original_url_) return;
switch (result.type_) {
case UrlHandler::LoadResult::NoMoreTracks:
qLog(Debug) << "URL handler for" << result.original_url_
@ -119,14 +132,6 @@ void Player::HandleLoadResult(const UrlHandler::LoadResult& result) {
break;
case UrlHandler::LoadResult::TrackAvailable: {
// Might've been an async load, so check we're still on the same item
int current_index = app_->playlist_manager()->active()->current_row();
if (current_index == -1) return;
shared_ptr<PlaylistItem> item =
app_->playlist_manager()->active()->item_at(current_index);
if (!item || item->Url() != result.original_url_) return;
qLog(Debug) << "URL handler for" << result.original_url_ << "returned"
<< result.media_url_;
@ -301,6 +306,13 @@ void Player::StopAfterCurrent() {
app_->playlist_manager()->active()->current_row());
}
bool Player::PreviousWouldRestartTrack() const {
// Check if it has been over two seconds since previous button was pressed
return menu_previousmode_ == PreviousBehaviour_Restart &&
last_pressed_previous_.isValid() &&
last_pressed_previous_.secsTo(QDateTime::currentDateTime()) >= 2;
}
void Player::Previous() { PreviousItem(Engine::Manual); }
void Player::PreviousItem(Engine::TrackChangeFlags change) {
@ -444,11 +456,11 @@ void Player::SeekTo(int seconds) {
}
void Player::SeekForward() {
SeekTo(engine()->position_nanosec() / kNsecPerSec + 10);
SeekTo(engine()->position_nanosec() / kNsecPerSec + seek_step_sec_);
}
void Player::SeekBackward() {
SeekTo(engine()->position_nanosec() / kNsecPerSec - 10);
SeekTo(engine()->position_nanosec() / kNsecPerSec - seek_step_sec_);
}
void Player::EngineMetadataReceived(const Engine::SimpleMetaBundle& bundle) {
@ -589,6 +601,8 @@ void Player::TrackAboutToEnd() {
next_item->Metadata().end_nanosec());
}
void Player::IntroPointReached() { NextInternal(Engine::Intro); }
void Player::ValidSongRequested(const QUrl& url) {
emit SongChangeRequestProcessed(url, true);
}

View File

@ -92,7 +92,7 @@ class PlayerInterface : public QObject {
virtual void Play() = 0;
virtual void ShowOSD() = 0;
signals:
signals:
void Playing();
void Paused();
void Stopped();
@ -142,6 +142,8 @@ class Player : public PlayerInterface {
const UrlHandler* HandlerForUrl(const QUrl& url) const;
bool PreviousWouldRestartTrack() const;
public slots:
void ReloadSettings();
@ -163,6 +165,7 @@ class Player : public PlayerInterface {
void Pause();
void Stop(bool stop_after = false);
void StopAfterCurrent();
void IntroPointReached();
void Play();
void ShowOSD();
void TogglePrettyOSD();
@ -209,6 +212,7 @@ class Player : public PlayerInterface {
QDateTime last_pressed_previous_;
PreviousBehaviour menu_previousmode_;
int seek_step_sec_;
};
#endif // CORE_PLAYER_H_

View File

@ -112,7 +112,9 @@ const QStringList Song::kColumns = QStringList() << "title"
<< "etag"
<< "performer"
<< "grouping"
<< "lyrics";
<< "lyrics"
<< "originalyear"
<< "effective_originalyear";
const QString Song::kColumnSpec = Song::kColumns.join(", ");
const QString Song::kBindSpec =
@ -157,6 +159,7 @@ struct Song::Private : public QSharedData {
int disc_;
float bpm_;
int year_;
int originalyear_;
QString genre_;
QString comment_;
bool compilation_; // From the file tag
@ -230,6 +233,7 @@ Song::Private::Private()
disc_(-1),
bpm_(-1),
year_(-1),
originalyear_(-1),
compilation_(false),
sampler_(false),
forced_compilation_on_(false),
@ -285,6 +289,10 @@ int Song::track() const { return d->track_; }
int Song::disc() const { return d->disc_; }
float Song::bpm() const { return d->bpm_; }
int Song::year() const { return d->year_; }
int Song::originalyear() const { return d->originalyear_; }
int Song::effective_originalyear() const {
return d->originalyear_ < 0 ? d->year_ : d->originalyear_;
}
const QString& Song::genre() const { return d->genre_; }
const QString& Song::comment() const { return d->comment_; }
bool Song::is_compilation() const {
@ -342,6 +350,7 @@ void Song::set_track(int v) { d->track_ = v; }
void Song::set_disc(int v) { d->disc_ = v; }
void Song::set_bpm(float v) { d->bpm_ = v; }
void Song::set_year(int v) { d->year_ = v; }
void Song::set_originalyear(int v) { d->originalyear_ = v; }
void Song::set_genre(const QString& v) { d->genre_ = v; }
void Song::set_comment(const QString& v) { d->comment_ = v; }
void Song::set_compilation(bool v) { d->compilation_ = v; }
@ -499,6 +508,7 @@ void Song::InitFromProtobuf(const pb::tagreader::SongMetadata& pb) {
d->disc_ = pb.disc();
d->bpm_ = pb.bpm();
d->year_ = pb.year();
d->originalyear_ = pb.originalyear();
d->genre_ = QStringFromStdString(pb.genre());
d->comment_ = QStringFromStdString(pb.comment());
d->compilation_ = pb.compilation();
@ -545,6 +555,7 @@ void Song::ToProtobuf(pb::tagreader::SongMetadata* pb) const {
pb->set_disc(d->disc_);
pb->set_bpm(d->bpm_);
pb->set_year(d->year_);
pb->set_originalyear(d->originalyear_);
pb->set_genre(DataCommaSizeFromQString(d->genre_));
pb->set_comment(DataCommaSizeFromQString(d->comment_));
pb->set_compilation(d->compilation_);
@ -585,6 +596,7 @@ void Song::InitFromQuery(const SqlRow& q, bool reliable_metadata, int col) {
d->disc_ = toint(col + 7);
d->bpm_ = tofloat(col + 8);
d->year_ = toint(col + 9);
d->originalyear_ = toint(col + 41);
d->genre_ = tostr(col + 10);
d->comment_ = tostr(col + 11);
d->compilation_ = q.value(col + 12).toBool();
@ -957,6 +969,8 @@ void Song::BindToQuery(QSqlQuery* query) const {
query->bindValue(":performer", strval(d->performer_));
query->bindValue(":grouping", strval(d->grouping_));
query->bindValue(":lyrics", strval(d->lyrics_));
query->bindValue(":originalyear", intval(d->originalyear_));
query->bindValue(":effective_originalyear", intval(this->effective_originalyear()));
#undef intval
#undef notnullintval
@ -984,6 +998,9 @@ void Song::ToLastFM(lastfm::Track* track, bool prefer_album_artist) const {
} else {
mtrack.setArtist(d->artist_);
}
#if LASTFM_MAJOR_VERSION >= 1
mtrack.setAlbumArtist(d->albumartist_);
#endif
mtrack.setAlbum(d->album_);
mtrack.setTitle(d->title_);
mtrack.setDuration(length_nanosec() / kNsecPerSec);
@ -1056,7 +1073,8 @@ bool Song::IsMetadataEqual(const Song& other) const {
d->performer_ == other.d->performer_ &&
d->grouping_ == other.d->grouping_ && d->track_ == other.d->track_ &&
d->disc_ == other.d->disc_ && qFuzzyCompare(d->bpm_, other.d->bpm_) &&
d->year_ == other.d->year_ && d->genre_ == other.d->genre_ &&
d->year_ == other.d->year_ && d->originalyear_ == other.d->originalyear_ &&
d->genre_ == other.d->genre_ &&
d->comment_ == other.d->comment_ &&
d->compilation_ == other.d->compilation_ &&
d->beginning_ == other.d->beginning_ &&

View File

@ -176,6 +176,8 @@ class Song {
int disc() const;
float bpm() const;
int year() const;
int originalyear() const;
int effective_originalyear() const;
const QString& genre() const;
const QString& comment() const;
bool is_compilation() const;
@ -255,6 +257,7 @@ class Song {
void set_disc(int v);
void set_bpm(float v);
void set_year(int v);
void set_originalyear(int v);
void set_genre(const QString& v);
void set_genre_id3(int id);
void set_comment(const QString& v);

View File

@ -52,7 +52,7 @@ class UrlHandler : public QObject {
TrackAvailable,
};
LoadResult(const QUrl& original_url = QUrl(), Type type = NoMoreTracks,
LoadResult(const QUrl& original_url, Type type = NoMoreTracks,
const QUrl& media_url = QUrl(), qint64 length_nanosec_ = -1);
// The url that the playlist item has in Url().

View File

@ -38,6 +38,7 @@
#include <QIODevice>
#include <QMetaEnum>
#include <QMouseEvent>
#include <QProcess>
#include <QStringList>
#include <QTcpServer>
#include <QtDebug>
@ -58,7 +59,6 @@
#include <sys/statvfs.h>
#elif defined(Q_OS_WIN32)
#include <windows.h>
#include <QProcess>
#endif
#ifdef Q_OS_LINUX
@ -76,7 +76,6 @@
#include "CoreServices/CoreServices.h"
#include "IOKit/ps/IOPowerSources.h"
#include "IOKit/ps/IOPSKeys.h"
#include <QProcess>
#endif
namespace Utilities {
@ -411,7 +410,9 @@ void OpenInFileBrowser(const QList<QUrl>& urls) {
#elif defined(Q_OS_WIN32)
ShowFileInExplorer(path);
#else
QDesktopServices::openUrl(QUrl::fromLocalFile(directory));
QStringList args;
args << directory;
QProcess::startDetached("xdg-open", args);
#endif
}
}

View File

@ -27,6 +27,7 @@ enum TrackChangeType {
First = 0x01,
Manual = 0x02,
Auto = 0x04,
Intro = 0x08,
// Any of:
SameAlbum = 0x10,

0
src/engines/enginebase.h Executable file → Normal file
View File

23
src/engines/gstengine.cpp Executable file → Normal file
View File

@ -103,6 +103,7 @@ GstEngine::GstEngine(TaskManager* task_manager)
buffer_duration_nanosec_(1 * kNsecPerSec), // 1s
buffer_min_fill_(33),
mono_playback_(false),
sample_rate_(kAutoSampleRate),
seek_timer_(new QTimer(this)),
timer_id_(-1),
next_element_id_(0),
@ -204,6 +205,7 @@ void GstEngine::ReloadSettings() {
buffer_min_fill_ = s.value("bufferminfill", 33).toInt();
mono_playback_ = s.value("monoplayback", false).toBool();
sample_rate_ = s.value("samplerate", kAutoSampleRate).toInt();
}
qint64 GstEngine::position_nanosec() const {
@ -302,8 +304,8 @@ void GstEngine::UpdateScope(int chunk_length) {
gst_buffer_map(latest_buffer_, &map, GST_MAP_READ);
// determine where to split the buffer
int chunk_density = (map.size * kNsecPerMsec) /
GST_BUFFER_DURATION(latest_buffer_);
int chunk_density =
(map.size * kNsecPerMsec) / GST_BUFFER_DURATION(latest_buffer_);
int chunk_size = chunk_length * chunk_density;
@ -321,10 +323,9 @@ void GstEngine::UpdateScope(int chunk_length) {
// make sure we don't go beyond the end of the buffer
if (scope_chunk_ == scope_chunks_ - 1) {
bytes =
qMin(static_cast<Engine::Scope::size_type>(
map.size - (chunk_size * scope_chunk_)),
scope_.size() * sizeof(sample_type));
bytes = qMin(static_cast<Engine::Scope::size_type>(
map.size - (chunk_size * scope_chunk_)),
scope_.size() * sizeof(sample_type));
} else {
bytes = qMin(static_cast<Engine::Scope::size_type>(chunk_size),
scope_.size() * sizeof(sample_type));
@ -380,7 +381,9 @@ bool GstEngine::Load(const QUrl& url, Engine::TrackChangeFlags change,
bool crossfade =
current_pipeline_ && ((crossfade_enabled_ && change & Engine::Manual) ||
(autocrossfade_enabled_ && change & Engine::Auto));
(autocrossfade_enabled_ && change & Engine::Auto) ||
((crossfade_enabled_ || autocrossfade_enabled_) &&
change & Engine::Intro));
if (change & Engine::Auto && change & Engine::SameAlbum &&
!crossfade_same_album_)
@ -767,9 +770,8 @@ GstEngine::PluginDetailsList GstEngine::GetPluginList(
if (QString(gst_element_factory_get_klass(factory)).contains(classname)) {
PluginDetails details;
details.name = QString::fromUtf8(gst_plugin_feature_get_name(p->data));
details.description = QString::fromUtf8(
gst_element_factory_get_metadata(factory,
GST_ELEMENT_METADATA_DESCRIPTION));
details.description = QString::fromUtf8(gst_element_factory_get_metadata(
factory, GST_ELEMENT_METADATA_DESCRIPTION));
ret << details;
}
p = g_list_next(p);
@ -788,6 +790,7 @@ shared_ptr<GstEnginePipeline> GstEngine::CreatePipeline() {
ret->set_buffer_duration_nanosec(buffer_duration_nanosec_);
ret->set_buffer_min_fill(buffer_min_fill_);
ret->set_mono_playback(mono_playback_);
ret->set_sample_rate(sample_rate_);
ret->AddBufferConsumer(this);
for (BufferConsumer* consumer : buffer_consumers_) {

5
src/engines/gstengine.h Executable file → Normal file
View File

@ -47,10 +47,9 @@ class TaskManager;
#ifdef Q_OS_DARWIN
struct _GTlsDatabase;
typedef struct _GTlsDatabase GTlsDatabase;
typedef struct _GTlsDatabase GTlsDatabase;
#endif
/**
* @class GstEngine
* @short GStreamer engine plugin
@ -72,6 +71,7 @@ class GstEngine : public Engine::Base, public BufferConsumer {
};
typedef QList<OutputDetails> OutputDetailsList;
static const int kAutoSampleRate = -1;
static const char* kSettingsGroup;
static const char* kAutoSink;
@ -216,6 +216,7 @@ class GstEngine : public Engine::Base, public BufferConsumer {
int buffer_min_fill_;
bool mono_playback_;
int sample_rate_;
mutable bool can_decode_success_;
mutable bool can_decode_last_;

View File

@ -69,6 +69,7 @@ GstEnginePipeline::GstEnginePipeline(GstEngine* engine)
buffer_min_fill_(33),
buffering_(false),
mono_playback_(false),
sample_rate_(GstEngine::kAutoSampleRate),
end_offset_nanosec_(-1),
next_beginning_offset_nanosec_(-1),
next_end_offset_nanosec_(-1),
@ -77,6 +78,7 @@ GstEnginePipeline::GstEnginePipeline(GstEngine* engine)
pipeline_is_initialised_(false),
pipeline_is_connected_(false),
pending_seek_nanosec_(-1),
last_known_position_ns_(0),
volume_percent_(100),
volume_modifier_(1.0),
pipeline_(nullptr),
@ -126,6 +128,8 @@ void GstEnginePipeline::set_mono_playback(bool enabled) {
mono_playback_ = enabled;
}
void GstEnginePipeline::set_sample_rate(int rate) { sample_rate_ = rate; }
bool GstEnginePipeline::ReplaceDecodeBin(GstElement* new_bin) {
if (!new_bin) return false;
@ -386,7 +390,26 @@ bool GstEnginePipeline::Init() {
gst_element_link(probe_queue, probe_converter);
gst_element_link_many(audio_queue, equalizer_preamp_, equalizer_,
stereo_panorama_, volume_, audioscale_, convert,
audiosink_, nullptr);
nullptr);
// add caps for fixed sample rate and mono, but only if requested
if (sample_rate_ != GstEngine::kAutoSampleRate && sample_rate_ > 0) {
GstCaps* caps = gst_caps_new_simple("audio/x-raw", "rate", G_TYPE_INT,
sample_rate_, nullptr);
if (mono_playback_) {
gst_caps_set_simple(caps, "channels", G_TYPE_INT, 1, nullptr);
}
gst_element_link_filtered(convert, audiosink_, caps);
gst_caps_unref(caps);
} else if (mono_playback_) {
GstCaps* capsmono =
gst_caps_new_simple("audio/x-raw", "channels", G_TYPE_INT, 1, nullptr);
gst_element_link_filtered(convert, audiosink_, capsmono);
gst_caps_unref(capsmono);
} else {
gst_element_link(convert, audiosink_);
}
// Add probes and handlers.
gst_pad_add_probe(gst_element_get_static_pad(probe_converter, "src"),
@ -622,10 +645,7 @@ QPair<QString, QString> ParseAkamaiTag(const QString& tag) {
return qMakePair(tag, QString());
}
bool IsAkamaiTag(const QString& tag) {
return tag.contains("- text=\"");
}
bool IsAkamaiTag(const QString& tag) { return tag.contains("- text=\""); }
}
void GstEnginePipeline::TagMessageReceived(GstMessage* msg) {
@ -941,7 +961,8 @@ void GstEnginePipeline::SourceSetupCallback(GstURIDecodeBin* bin,
nullptr);
#ifdef Q_OS_DARWIN
g_object_set(element, "tls-database", instance->engine_->tls_database(), nullptr);
g_object_set(element, "tls-database", instance->engine_->tls_database(),
nullptr);
g_object_set(element, "ssl-use-system-ca-file", false, nullptr);
g_object_set(element, "ssl-strict", TRUE, nullptr);
#endif
@ -976,10 +997,11 @@ void GstEnginePipeline::TransitionToNext() {
}
qint64 GstEnginePipeline::position() const {
gint64 value = 0;
gst_element_query_position(pipeline_, GST_FORMAT_TIME, &value);
if (pipeline_is_initialised_)
gst_element_query_position(pipeline_, GST_FORMAT_TIME,
&last_known_position_ns_);
return value;
return last_known_position_ns_;
}
qint64 GstEnginePipeline::length() const {
@ -1033,6 +1055,7 @@ bool GstEnginePipeline::Seek(qint64 nanosec) {
}
pending_seek_nanosec_ = -1;
last_known_position_ns_ = nanosec;
return gst_element_seek_simple(pipeline_, GST_FORMAT_TIME,
GST_SEEK_FLAG_FLUSH, nanosec);
}

View File

@ -55,6 +55,7 @@ class GstEnginePipeline : public QObject {
void set_buffer_duration_nanosec(qint64 duration_nanosec);
void set_buffer_min_fill(int percent);
void set_mono_playback(bool enabled);
void set_sample_rate(int rate);
// Creates the pipeline, returns false on error
bool InitFromUrl(const QUrl& url, qint64 end_nanosec);
@ -220,6 +221,7 @@ signals:
bool buffering_;
bool mono_playback_;
int sample_rate_;
// The URL that is currently playing, and the URL that is to be preloaded
// when the current track is close to finishing.
@ -258,6 +260,13 @@ signals:
bool pipeline_is_connected_;
qint64 pending_seek_nanosec_;
// We can only use gst_element_query_position() when the pipeline is in
// PAUSED nor PLAYING state. Whenever we get a new position (e.g. after a
// correct call to gst_element_query_position() or after a seek), we store
// it here so that we can use it when using gst_element_query_position() is
// not possible.
mutable gint64 last_known_position_ns_;
int volume_percent_;
qreal volume_modifier_;

View File

@ -120,12 +120,26 @@ QStandardItem* GlobalSearchModel::BuildContainers(const Song& s,
has_album_icon = true;
break;
case LibraryModel::GroupBy_OriginalYearAlbum:
year = qMax(0, s.effective_originalyear());
display_text = LibraryModel::PrettyYearAlbum(year, s.album());
sort_text = LibraryModel::SortTextForNumber(year) + s.album();
unique_tag = s.album_id();
has_album_icon = true;
break;
case LibraryModel::GroupBy_Year:
year = qMax(0, s.year());
display_text = QString::number(year);
sort_text = LibraryModel::SortTextForNumber(year) + " ";
break;
case LibraryModel::GroupBy_OriginalYear:
year = qMax(0, s.effective_originalyear());
display_text = QString::number(year);
sort_text = LibraryModel::SortTextForNumber(year) + " ";
break;
case LibraryModel::GroupBy_Composer:
display_text = s.composer();
case LibraryModel::GroupBy_Performer:

View File

@ -101,12 +101,12 @@ GlobalSearchView::GlobalSearchView(Application* app, QWidget* parent)
disabled_layout->setContentsMargins(16, 0, 16, 32);
suggestions_layout->setContentsMargins(16, 0, 16, 6);
// Set the colour of the help text to the disabled text colour
// Set the colour of the help text to the disabled window text colour
QPalette help_palette = ui_->help_text->palette();
const QColor help_color =
help_palette.color(QPalette::Disabled, QPalette::Text);
help_palette.setColor(QPalette::Normal, QPalette::Text, help_color);
help_palette.setColor(QPalette::Inactive, QPalette::Text, help_color);
help_palette.color(QPalette::Disabled, QPalette::WindowText);
help_palette.setColor(QPalette::Normal, QPalette::WindowText, help_color);
help_palette.setColor(QPalette::Inactive, QPalette::WindowText, help_color);
ui_->help_text->setPalette(help_palette);
// Create suggestion widgets
@ -337,7 +337,8 @@ void GlobalSearchView::LazyLoadArt(const QModelIndex& proxy_index) {
proxy_index.data(LibraryModel::Role_ContainerType).toInt());
if (container_type != LibraryModel::GroupBy_Album &&
container_type != LibraryModel::GroupBy_AlbumArtist &&
container_type != LibraryModel::GroupBy_YearAlbum) {
container_type != LibraryModel::GroupBy_YearAlbum &&
container_type != LibraryModel::GroupBy_OriginalYearAlbum) {
return;
}
@ -559,6 +560,11 @@ void GlobalSearchView::GroupByClicked(QAction* action) {
}
void GlobalSearchView::SetGroupBy(const LibraryModel::Grouping& g) {
// Clear requests: changing "group by" on the models will cause all the items to be removed/added
// again, so all the QModelIndex here will become invalid. New requests will be created for those
// songs when they will be displayed again anyway (when GlobalSearchItemDelegate::paint will call
// LazyLoadArt)
art_requests_.clear();
// Update the models
front_model_->SetGroupBy(g, true);
back_model_->SetGroupBy(g, false);

View File

@ -17,6 +17,7 @@
#include "internet/amazon/amazonclouddrive.h"
#include <chrono>
#include <cmath>
#include <QtGlobal>
@ -38,6 +39,7 @@
#include "library/librarybackend.h"
#include "ui/settingsdialog.h"
using std::chrono::seconds;
using std::placeholders::_1;
const char* AmazonCloudDrive::kServiceName = "Amazon Cloud Drive";
@ -199,16 +201,16 @@ void AmazonCloudDrive::MonitorReply(QNetworkReply* reply,
reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt();
if (code >= 500) { // Retry with exponential backoff.
int max_delay_s = std::pow(std::min(retries + 1, 8), 2);
int delay_s = qrand() % max_delay_s;
seconds delay(qrand() % max_delay_s);
qLog(Debug) << "Request failed with code:" << code << "- retrying after"
<< delay_s << "seconds";
<< delay << "seconds";
DoAfter([=]() {
if (post_data.isEmpty()) {
Get(reply->request(), done, retries + 1);
} else {
Post(reply->request(), post_data, done, retries + 1);
}
}, delay_s * kMsecPerSec);
}, delay);
} else {
// Request failed permanently.
done(reply);

View File

@ -92,7 +92,6 @@ InternetModel::InternetModel(Application* app, QObject* parent)
AddService(new JazzRadioService(app, this));
AddService(new MagnatuneService(app, this));
AddService(new PodcastService(app, this));
AddService(new RadioGFMService(app, this));
AddService(new RockRadioService(app, this));
AddService(new SavedRadio(app, this));
AddService(new RadioTunesService(app, this));

View File

@ -21,6 +21,7 @@
#include "internet/core/oauthenticator.h"
#include <QDesktopServices>
#include <QSslError>
#include <QStringList>
#include <QUrl>
#include <QUrlQuery>
@ -121,6 +122,7 @@ void OAuthenticator::RequestAccessToken(const QByteArray& code,
"application/x-www-form-urlencoded");
QNetworkReply* reply = network_.post(request, post_data.toUtf8());
connect(reply, SIGNAL(sslErrors(QList<QSslError>)), SLOT(SslErrors(QList<QSslError>)));
NewClosure(reply, SIGNAL(finished()), this,
SLOT(FetchAccessTokenFinished(QNetworkReply*)), reply);
}
@ -196,3 +198,9 @@ void OAuthenticator::RefreshAccessTokenFinished(QNetworkReply* reply) {
SetExpiryTime(json_result["expires_in"].toInt());
emit Finished();
}
void OAuthenticator::SslErrors(const QList<QSslError>& errors) {
for (const QSslError& error : errors) {
qLog(Debug) << error.errorString();
}
}

View File

@ -68,6 +68,7 @@ class OAuthenticator : public QObject {
void RedirectArrived(LocalRedirectServer* server, QUrl url);
void FetchAccessTokenFinished(QNetworkReply* reply);
void RefreshAccessTokenFinished(QNetworkReply* reply);
void SslErrors(const QList<QSslError>& errors);
private:
static const char* kRemoteURL;

View File

@ -87,6 +87,7 @@ void DigitallyImportedUrlHandler::LoadPlaylistFinished(QIODevice* device) {
// Failed to get playlist?
if (songs.count() == 0) {
service_->StreamError(tr("Error loading di.fm playlist"));
emit AsyncLoadComplete(LoadResult(last_original_url_));
return;
}

View File

@ -338,7 +338,8 @@ void LastFMService::NowPlaying(const Song& song) {
return;
}
#else
// TODO(John Maguire): validity was removed from liblastfm1 but might reappear, it should have
// TODO(John Maguire): validity was removed from liblastfm1 but might reappear,
// it should have
// no impact as we get a different error when actually trying to scrobble.
#endif
@ -375,6 +376,8 @@ void LastFMService::Love() {
}
void LastFMService::Ban() {
if (!IsAuthenticated()) ShowConfig();
lastfm::MutableTrack mtrack(last_track_);
mtrack.ban();
last_track_ = mtrack;

View File

@ -264,10 +264,3 @@ SomaFMService::SomaFMService(Application* app, InternetModel* parent)
: SomaFMServiceBase(
app, parent, "SomaFM", QUrl("http://somafm.com/channels.xml"),
QUrl("http://somafm.com"), QUrl(), QIcon(":providers/somafm.png")) {}
RadioGFMService::RadioGFMService(Application* app, InternetModel* parent)
: SomaFMServiceBase(app, parent, "Radio GFM",
QUrl("http://streams.radio-gfm.net/channels.xml"),
QUrl("http://www.radio-gfm.net"),
QUrl("http://www.radio-gfm.net/spenden"),
QIcon(":providers/radiogfm.png")) {}

View File

@ -109,11 +109,6 @@ class SomaFMService : public SomaFMServiceBase {
SomaFMService(Application* app, InternetModel* parent);
};
class RadioGFMService : public SomaFMServiceBase {
public:
RadioGFMService(Application* app, InternetModel* parent);
};
QDataStream& operator<<(QDataStream& out, const SomaFMService::Stream& stream);
QDataStream& operator>>(QDataStream& in, SomaFMService::Stream& stream);
Q_DECLARE_METATYPE(SomaFMService::Stream)

View File

@ -668,12 +668,14 @@ void SpotifyService::EnsureMenuCreated() {
playlist_context_menu_->addAction(GetNewShowConfigAction());
song_context_menu_ = new QMenu;
song_context_menu_->addActions(GetPlaylistActions());
song_context_menu_->addSeparator();
remove_from_playlist_ = song_context_menu_->addAction(
IconLoader::Load("list-remove"), tr("Remove from playlist"), this,
SLOT(RemoveCurrentFromPlaylist()));
song_context_menu_->addAction(tr("Get a URL to share this Spotify song"),
this, SLOT(GetCurrentSongUrlToShare()));
song_context_menu_->addSeparator();
song_context_menu_->addAction(GetNewShowConfigAction());
}

View File

@ -28,7 +28,7 @@ SubsonicUrlHandler::SubsonicUrlHandler(SubsonicService* service,
UrlHandler::LoadResult SubsonicUrlHandler::StartLoading(const QUrl& url) {
if (service_->login_state() != SubsonicService::LoginState_Loggedin)
return LoadResult();
return LoadResult(url);
QUrl newurl = service_->BuildRequestUrl("stream");
QUrlQuery url_query;

View File

@ -63,7 +63,8 @@ const char* VkService::kUrlScheme = "vk";
const char* VkService::kDefCacheFilename = "%artist - %title";
const int VkService::kMaxVkSongList = 6000;
const int VkService::kMaxVkWallPostList = 100;
const int VkService::kCustomSongCount = 50;
const int VkService::kMaxVkSongCount = 300;
const int VkService::kSearchDelayMsec = 400;
QString VkService::DefaultCacheDir() {
return QDir::toNativeSeparators(
@ -220,7 +221,8 @@ VkService::VkService(Application* app, InternetModel* parent)
url_handler_(new VkUrlHandler(this, this)),
audio_provider_(new Vreen::AudioProvider(client_.get())),
cache_(new VkMusicCache(app_, this)),
last_search_id_(0) {
last_search_id_(0),
search_delay_(new QTimer(this)) {
QSettings s;
s.beginGroup(kSettingGroup);
@ -245,7 +247,14 @@ VkService::VkService(Application* app, InternetModel* parent)
VkSearchProvider* search_provider = new VkSearchProvider(app_, this);
search_provider->Init(this);
app_->global_search()->AddProvider(search_provider);
search_delay_->setInterval(kSearchDelayMsec);
search_delay_->setSingleShot(true);
connect(search_delay_, SIGNAL(timeout()), SLOT(DoLocalSearch()));
connect(search_box_, SIGNAL(TextChanged(QString)), SLOT(FindSongs(QString)));
connect(this, SIGNAL(SongSearchResult(SearchID, SongList)),
SLOT(SearchResultLoaded(SearchID, SongList)));
app_->player()->RegisterUrlHandler(url_handler_);
}
@ -662,7 +671,7 @@ void VkService::UpdateRecommendations() {
CreateAndAppendRow(recommendations_item_, Type_Loading);
auto my_audio =
audio_provider_->getRecommendationsForUser(0, kCustomSongCount, 0);
audio_provider_->getRecommendationsForUser(0, kMaxVkSongCount, 0);
NewClosure(my_audio, SIGNAL(resultReady(QVariant)), this,
SLOT(RecommendationsLoaded(Vreen::AudioItemListReply*)), my_audio);
@ -673,7 +682,7 @@ void VkService::MoreRecommendations() {
CreateAndAppendRow(recommendations_item_, Type_Loading);
auto my_audio = audio_provider_->getRecommendationsForUser(
0, kCustomSongCount, recommendations_item_->rowCount() - 1);
0, kMaxVkSongCount, recommendations_item_->rowCount() - 1);
NewClosure(my_audio, SIGNAL(resultReady(QVariant)), this,
SLOT(RecommendationsLoaded(Vreen::AudioItemListReply*)), my_audio);
@ -818,8 +827,8 @@ QStandardItem* VkService::AppendAlbumList(QStandardItem* parent, bool myself) {
if (myself) {
item = new QStandardItem(QIcon(":vk/discography.png"), tr("My Albums"));
// TODO(Ivan Leontiev): Do this better. We have incomplete MusicOwner instance
// for logged in user.
// TODO(Ivan Leontiev): Do this better. We have incomplete MusicOwner
// instance for logged in user.
owner.setId(UserID());
my_albums_item_ = item;
} else {
@ -876,8 +885,8 @@ QStandardItem* VkService::AppendMusic(QStandardItem* parent, bool myself) {
if (myself) {
item = new QStandardItem(QIcon(":vk/my_music.png"), tr("My Music"));
// TODO(Ivan Leontiev): Do this better. We have incomplete MusicOwner instance
// for logged in user.
// TODO(Ivan Leontiev): Do this better. We have incomplete MusicOwner
// instance for logged in user.
owner.setId(UserID());
my_music_item_ = item;
} else {
@ -962,8 +971,8 @@ void VkService::FindThisArtist() {
void VkService::AddToMyMusic() {
SongId id = ExtractIds(selected_song_.url());
auto reply = audio_provider_->addToLibrary(id.audio_id, id.owner_id);
connect(reply, SIGNAL(resultReady(QVariant)), this,
SLOT(UpdateMusic(my_music_item_)));
NewClosure(reply, SIGNAL(resultReady(QVariant)), this,
SLOT(UpdateMusic(QStandardItem*)), my_music_item_);
}
void VkService::AddToMyMusicCurrent() {
@ -977,8 +986,8 @@ void VkService::RemoveFromMyMusic() {
SongId id = ExtractIds(selected_song_.url());
if (id.owner_id == UserID()) {
auto reply = audio_provider_->removeFromLibrary(id.audio_id, id.owner_id);
connect(reply, SIGNAL(resultReady(QVariant)), this,
SLOT(UpdateMusic(my_music_item_)));
NewClosure(reply, SIGNAL(resultReady(QVariant)), this,
SLOT(UpdateMusic(QStandardItem*)), my_music_item_);
} else {
qLog(Error) << "Tried to delete song that not owned by user (" << UserID()
<< selected_song_.url();
@ -1003,29 +1012,40 @@ void VkService::CopyShareUrl() {
* Search
*/
void VkService::DoLocalSearch() {
ClearStandardItem(search_result_item_);
CreateAndAppendRow(search_result_item_, Type_Loading);
SearchID id(SearchID::LocalSearch);
last_search_id_ = id.id();
SongSearch(id, last_query_);
}
void VkService::FindSongs(const QString& query) {
last_query_ = query;
if (query.isEmpty()) {
search_delay_->stop();
root_item_->removeRow(search_result_item_->row());
search_result_item_ = NULL;
last_search_id_ = 0;
} else {
last_query_ = query;
if (!search_result_item_) {
CreateAndAppendRow(root_item_, Type_Search);
connect(this, SIGNAL(SongSearchResult(SearchID, SongList)),
SLOT(SearchResultLoaded(SearchID, SongList)));
}
ClearStandardItem(search_result_item_);
CreateAndAppendRow(search_result_item_, Type_Loading);
SongSearch(SearchID(SearchID::LocalSearch), query);
return;
}
search_delay_->start();
if (!search_result_item_) {
CreateAndAppendRow(root_item_, Type_Search);
}
}
void VkService::FindMore() {
RemoveLastRow(search_result_item_, Type_More);
CreateAndAppendRow(search_result_item_, Type_Loading);
SearchID id(SearchID::MoreLocalSearch);
SongSearch(id, last_query_, kCustomSongCount,
last_search_id_ = id.id();
SongSearch(id, last_query_, kMaxVkSongCount,
search_result_item_->rowCount() - 1);
}
@ -1034,7 +1054,7 @@ void VkService::SearchResultLoaded(const SearchID& id, const SongList& songs) {
return; // Result received when search is already over.
}
if (id.id() >= last_search_id_) {
if (id.id() == last_search_id_) {
if (id.type() == SearchID::LocalSearch) {
ClearStandardItem(search_result_item_);
} else if (id.type() == SearchID::MoreLocalSearch) {
@ -1043,8 +1063,6 @@ void VkService::SearchResultLoaded(const SearchID& id, const SongList& songs) {
return; // Others request types ignored.
}
last_search_id_ = id.id();
if (!songs.isEmpty()) {
AppendSongs(search_result_item_, songs);
CreateAndAppendRow(search_result_item_, Type_More);
@ -1173,19 +1191,23 @@ UrlHandler::LoadResult VkService::GetSongResult(const QUrl& url) {
if (media_url.isValid()) {
Song song = FromAudioItem(audio_item);
SongStarting(song);
cache_->AddToCache(url, media_url);
if (cachingEnabled_) {
cache_->AddToCache(url, media_url);
}
return UrlHandler::LoadResult(url, UrlHandler::LoadResult::TrackAvailable,
media_url, song.length_nanosec());
}
return UrlHandler::LoadResult();
return UrlHandler::LoadResult(url);
}
UrlHandler::LoadResult VkService::GetGroupNextSongUrl(const QUrl& url) {
QStringList tokens = url.path().split('/');
if (tokens.count() < 3) {
qLog(Error) << "Wrong url" << url;
return UrlHandler::LoadResult();
return UrlHandler::LoadResult(url);
}
int gid = tokens[1].toInt();
@ -1214,7 +1236,7 @@ UrlHandler::LoadResult VkService::GetGroupNextSongUrl(const QUrl& url) {
}
qLog(Info) << "Unresolved group url" << url;
return UrlHandler::LoadResult();
return UrlHandler::LoadResult(url);
}
/***
@ -1444,7 +1466,7 @@ void VkService::AppendSongs(QStandardItem* parent, const SongList& songs) {
void VkService::ReloadSettings() {
QSettings s;
s.beginGroup(kSettingGroup);
maxGlobalSearch_ = s.value("max_global_search", kCustomSongCount).toInt();
maxGlobalSearch_ = s.value("max_global_search", kMaxVkSongCount).toInt();
cachingEnabled_ = s.value("cache_enabled", false).toBool();
cacheDir_ = s.value("cache_dir", DefaultCacheDir()).toString();
cacheFilename_ = s.value("cache_filename", kDefCacheFilename).toString();

View File

@ -116,7 +116,8 @@ class VkService : public InternetService {
static QString DefaultCacheDir();
static const int kMaxVkSongList;
static const int kMaxVkWallPostList;
static const int kCustomSongCount;
static const int kMaxVkSongCount;
static const int kSearchDelayMsec;
enum ItemType {
Type_Loading = InternetModel::TypeCount,
@ -167,8 +168,8 @@ class VkService : public InternetService {
// Return random song result from group playlist.
UrlHandler::LoadResult GetGroupNextSongUrl(const QUrl& url);
void SongSearch(SearchID id, const QString& query, int count = 50,
int offset = 0);
void SongSearch(SearchID id, const QString& query,
int count = kMaxVkSongCount, int offset = 0);
void GroupSearch(SearchID id, const QString& query);
/* Settings */
@ -181,7 +182,7 @@ class VkService : public InternetService {
QString cacheFilename() const { return cacheFilename_; }
bool isLoveAddToMyMusic() const { return love_is_add_to_mymusic_; }
signals:
signals:
void NameUpdated(const QString& name);
void ConnectionStateChanged(Vreen::Client::State state);
void LoginSuccess(bool success);
@ -195,6 +196,7 @@ class VkService : public InternetService {
void UpdateRoot();
void ShowConfig();
void FindUserOrGroup(const QString& q);
void DoLocalSearch();
private slots:
/* Interface */
@ -304,6 +306,7 @@ class VkService : public InternetService {
// Keeping when more recent results recived.
// Using for prevent loading tardy result instead.
uint last_search_id_;
QTimer* search_delay_;
QString last_query_;
Song selected_song_; // Store for context menu actions.
Song current_song_; // Store for actions with now playing song.

View File

@ -30,7 +30,7 @@ VkUrlHandler::VkUrlHandler(VkService* service, QObject* parent)
UrlHandler::LoadResult VkUrlHandler::StartLoading(const QUrl& url) {
QStringList args = url.path().split("/");
LoadResult result;
LoadResult result(url);
if (args.size() < 2) {
qLog(Error)
@ -58,6 +58,6 @@ UrlHandler::LoadResult VkUrlHandler::LoadNext(const QUrl& url) {
if (url.host() == "group") {
return StartLoading(url);
} else {
return LoadResult();
return LoadResult(url);
}
}

View File

@ -76,11 +76,13 @@ GroupByDialog::GroupByDialog(QWidget* parent)
p_->mapping_.insert(Mapping(LibraryModel::GroupBy_FileType, 5));
p_->mapping_.insert(Mapping(LibraryModel::GroupBy_Genre, 6));
p_->mapping_.insert(Mapping(LibraryModel::GroupBy_Year, 7));
p_->mapping_.insert(Mapping(LibraryModel::GroupBy_YearAlbum, 8));
p_->mapping_.insert(Mapping(LibraryModel::GroupBy_Bitrate, 9));
p_->mapping_.insert(Mapping(LibraryModel::GroupBy_Disc, 10));
p_->mapping_.insert(Mapping(LibraryModel::GroupBy_Performer, 11));
p_->mapping_.insert(Mapping(LibraryModel::GroupBy_Grouping, 12));
p_->mapping_.insert(Mapping(LibraryModel::GroupBy_OriginalYear, 8));
p_->mapping_.insert(Mapping(LibraryModel::GroupBy_YearAlbum, 9));
p_->mapping_.insert(Mapping(LibraryModel::GroupBy_OriginalYearAlbum, 10));
p_->mapping_.insert(Mapping(LibraryModel::GroupBy_Bitrate, 11));
p_->mapping_.insert(Mapping(LibraryModel::GroupBy_Disc, 12));
p_->mapping_.insert(Mapping(LibraryModel::GroupBy_Performer, 13));
p_->mapping_.insert(Mapping(LibraryModel::GroupBy_Grouping, 14));
connect(ui_->button_box->button(QDialogButtonBox::Reset), SIGNAL(clicked()),
SLOT(Reset()));

View File

@ -83,11 +83,21 @@
<string>Year</string>
</property>
</item>
<item>
<property name="text">
<string>Original year</string>
</property>
</item>
<item>
<property name="text">
<string>Year - Album</string>
</property>
</item>
<item>
<property name="text">
<string>Original year - Album</string>
</property>
</item>
<item>
<property name="text">
<string>Bitrate</string>
@ -98,6 +108,16 @@
<string>Disc</string>
</property>
</item>
<item>
<property name="text">
<string>Performer</string>
</property>
</item>
<item>
<property name="text">
<string>Grouping</string>
</property>
</item>
</widget>
</item>
<item row="1" column="0">
@ -149,11 +169,21 @@
<string>Year</string>
</property>
</item>
<item>
<property name="text">
<string>Original year</string>
</property>
</item>
<item>
<property name="text">
<string>Year - Album</string>
</property>
</item>
<item>
<property name="text">
<string>Original year - Album</string>
</property>
</item>
<item>
<property name="text">
<string>Bitrate</string>
@ -164,6 +194,16 @@
<string>Disc</string>
</property>
</item>
<item>
<property name="text">
<string>Performer</string>
</property>
</item>
<item>
<property name="text">
<string>Grouping</string>
</property>
</item>
</widget>
</item>
<item row="2" column="0">
@ -215,21 +255,41 @@
<string>Year</string>
</property>
</item>
<item>
<property name="text">
<string>Original year</string>
</property>
</item>
<item>
<property name="text">
<string>Year - Album</string>
</property>
</item>
<item>
<property name="text">
<string>Bitrate</string>
</property>
<item>
<property name="text">
<string>Original year - Album</string>
</property>
</item>
<item>
<property name="text">
<string>Bitrate</string>
</property>
</item>
<item>
<property name="text">
<string>Disc</string>
</property>
</item>
<item>
<property name="text">
<string>Performer</string>
</property>
</item>
<item>
<property name="text">
<string>Grouping</string>
</property>
</item>
</widget>
</item>
</layout>

View File

@ -115,6 +115,7 @@ Library::Library(Application* app, QObject* parent)
// full rescan revisions
full_rescan_revisions_[26] = tr("CUE sheet support");
full_rescan_revisions_[50] = tr("Original year tag support");
ReloadSettings();
}

View File

@ -222,9 +222,15 @@ void LibraryModel::SongsDiscovered(const SongList& songs) {
case GroupBy_Year:
key = QString::number(qMax(0, song.year()));
break;
case GroupBy_OriginalYear:
key = QString::number(qMax(0, song.effective_originalyear()));
break;
case GroupBy_YearAlbum:
key = PrettyYearAlbum(qMax(0, song.year()), song.album());
break;
case GroupBy_OriginalYearAlbum:
key = PrettyYearAlbum(qMax(0, song.effective_originalyear()), song.album());
break;
case GroupBy_FileType:
key = song.filetype();
break;
@ -314,11 +320,15 @@ QString LibraryModel::DividerKey(GroupBy type, LibraryItem* item) const {
}
case GroupBy_Year:
case GroupBy_OriginalYear:
return SortTextForNumber(item->sort_text.toInt() / 10 * 10);
case GroupBy_YearAlbum:
return SortTextForNumber(item->metadata.year());
case GroupBy_OriginalYearAlbum:
return SortTextForNumber(item->metadata.effective_originalyear());
case GroupBy_Bitrate:
return SortTextForNumber(item->metadata.bitrate());
@ -348,10 +358,12 @@ QString LibraryModel::DividerDisplayText(GroupBy type,
return key.toUpper();
case GroupBy_YearAlbum:
case GroupBy_OriginalYearAlbum:
if (key == "0000") return tr("Unknown");
return key.toUpper();
case GroupBy_Year:
case GroupBy_OriginalYear:
if (key == "0000") return tr("Unknown");
return QString::number(key.toInt()); // To remove leading 0s
@ -544,7 +556,8 @@ QVariant LibraryModel::data(const QModelIndex& index, int role) const {
item->type == LibraryItem::Type_Container) {
GroupBy container_type = group_by_[item->container_level];
is_album_node = container_type == GroupBy_Album ||
container_type == GroupBy_YearAlbum;
container_type == GroupBy_YearAlbum ||
container_type == GroupBy_OriginalYearAlbum;
}
if (is_album_node) {
// It has const behaviour some of the time - that's ok right?
@ -573,6 +586,7 @@ QVariant LibraryModel::data(const LibraryItem* item, int role) const {
switch (container_type) {
case GroupBy_Album:
case GroupBy_YearAlbum:
case GroupBy_OriginalYearAlbum:
return album_icon_;
case GroupBy_Artist:
case GroupBy_AlbumArtist:
@ -798,9 +812,15 @@ void LibraryModel::InitQuery(GroupBy type, LibraryQuery* q) {
case GroupBy_YearAlbum:
q->SetColumnSpec("DISTINCT year, album, grouping");
break;
case GroupBy_OriginalYearAlbum:
q->SetColumnSpec("DISTINCT year, originalyear, album, grouping");
break;
case GroupBy_Year:
q->SetColumnSpec("DISTINCT year");
break;
case GroupBy_OriginalYear:
q->SetColumnSpec("DISTINCT effective_originalyear");
break;
case GroupBy_Genre:
q->SetColumnSpec("DISTINCT genre");
break;
@ -842,9 +862,19 @@ void LibraryModel::FilterQuery(GroupBy type, LibraryItem* item,
q->AddWhere("album", item->metadata.album());
q->AddWhere("grouping", item->metadata.grouping());
break;
case GroupBy_OriginalYearAlbum:
q->AddWhere("year", item->metadata.year());
q->AddWhere("originalyear", item->metadata.originalyear());
q->AddWhere("album", item->metadata.album());
q->AddWhere("grouping", item->metadata.grouping());
break;
case GroupBy_Year:
q->AddWhere("year", item->key);
break;
case GroupBy_OriginalYear:
q->AddWhere("effective_originalyear", item->key);
break;
case GroupBy_Composer:
q->AddWhere("composer", item->key);
break;
@ -904,6 +934,7 @@ LibraryItem* LibraryModel::ItemFromQuery(GroupBy type, bool signal,
int container_level) {
LibraryItem* item = InitItem(type, signal, parent, container_level);
int year = 0;
int effective_originalyear = 0;
int bitrate = 0;
int disc = 0;
@ -924,12 +955,29 @@ LibraryItem* LibraryModel::ItemFromQuery(GroupBy type, bool signal,
item->metadata.album();
break;
case GroupBy_OriginalYearAlbum:
item->metadata.set_year(row.value(0).toInt());
item->metadata.set_originalyear(row.value(1).toInt());
item->metadata.set_album(row.value(2).toString());
item->metadata.set_grouping(row.value(3).toString());
effective_originalyear = qMax(0, item->metadata.effective_originalyear());
item->key = PrettyYearAlbum(effective_originalyear, item->metadata.album());
item->sort_text = SortTextForNumber(effective_originalyear) + item->metadata.grouping()
+ item->metadata.album();
break;
case GroupBy_Year:
year = qMax(0, row.value(0).toInt());
item->key = QString::number(year);
item->sort_text = SortTextForNumber(year) + " ";
break;
case GroupBy_OriginalYear:
year = qMax(0, row.value(0).toInt());
item->key = QString::number(year);
item->sort_text = SortTextForNumber(year) + " ";
break;
case GroupBy_Composer:
case GroupBy_Performer:
case GroupBy_Grouping:
@ -976,6 +1024,8 @@ LibraryItem* LibraryModel::ItemFromSong(GroupBy type, bool signal,
int container_level) {
LibraryItem* item = InitItem(type, signal, parent, container_level);
int year = 0;
int originalyear = 0;
int effective_originalyear = 0;
int bitrate = 0;
switch (type) {
@ -993,12 +1043,29 @@ LibraryItem* LibraryModel::ItemFromSong(GroupBy type, bool signal,
item->sort_text = SortTextForNumber(year) + s.grouping() + s.album();
break;
case GroupBy_OriginalYearAlbum:
year = qMax(0, s.year());
originalyear = qMax(0, s.originalyear());
effective_originalyear = qMax(0, s.effective_originalyear());
item->metadata.set_year(year);
item->metadata.set_originalyear(originalyear);
item->metadata.set_album(s.album());
item->key = PrettyYearAlbum(effective_originalyear, s.album());
item->sort_text = SortTextForNumber(effective_originalyear) + s.grouping() + s.album();
break;
case GroupBy_Year:
year = qMax(0, s.year());
item->key = QString::number(year);
item->sort_text = SortTextForNumber(year) + " ";
break;
case GroupBy_OriginalYear:
year = qMax(0, s.effective_originalyear());
item->key = QString::number(year);
item->sort_text = SortTextForNumber(year) + " ";
break;
case GroupBy_Composer:
item->key = s.composer();
case GroupBy_Performer:

View File

@ -85,6 +85,8 @@ class LibraryModel : public SimpleTreeModel<LibraryItem> {
GroupBy_Grouping = 10,
GroupBy_Bitrate = 11,
GroupBy_Disc = 12,
GroupBy_OriginalYearAlbum = 13,
GroupBy_OriginalYear = 14,
};
struct Grouping {

View File

@ -62,6 +62,8 @@ void LibraryItemDelegate::paint(QPainter* painter,
QString text(index.data().toString());
painter->save();
painter->setRenderHint(QPainter::Antialiasing);
painter->setRenderHint(QPainter::HighQualityAntialiasing);
QRect text_rect(opt.rect);
@ -89,23 +91,42 @@ void LibraryItemDelegate::paint(QPainter* painter,
painter->drawPixmap(icon_rect, pixmap);
} else {
text_rect.setLeft(text_rect.left() + 30);
text_rect.setLeft(text_rect.right() - (text_rect.height() + 10));
text_rect.setWidth(text_rect.height());
}
QRect highlight_rect;
QFont norm_font(opt.font);
QColor highlight_color(opt.palette.color(QPalette::Text));
highlight_color.setAlpha(200);
QBrush brush(highlight_color, Qt::SolidPattern);
painter->setPen(QColor(0, 0, 0, 0));
painter->setBrush(brush);
// Draw text highlight fill
if (text.length() == 1) {
highlight_rect = text_rect;
highlight_rect.setWidth(text_rect.width() - 1);
highlight_rect.setHeight(text_rect.height() - 1);
highlight_rect.setLeft(text_rect.left() + 1);
highlight_rect.setTop(text_rect.top() + 1);
painter->drawEllipse(highlight_rect);
} else {
QFontMetrics fm(norm_font);
text_rect.setLeft(text_rect.right() - (fm.width(text) + 22));
text_rect.setWidth(fm.width(text) + 12);
highlight_rect = text_rect;
highlight_rect.setWidth(text_rect.width() - 1);
highlight_rect.setHeight(text_rect.height() - 1);
highlight_rect.setLeft(text_rect.left() + 1);
highlight_rect.setTop(text_rect.top() + 1);
painter->fillRect(highlight_rect, brush);
}
// Draw the text
QFont bold_font(opt.font);
bold_font.setBold(true);
painter->setPen(opt.palette.color(QPalette::Text));
painter->setFont(bold_font);
painter->drawText(text_rect, text);
// Draw the line under the item
QPen line_pen(opt.palette.color(QPalette::Dark));
line_pen.setWidth(2);
painter->setPen(line_pen);
painter->drawLine(opt.rect.bottomLeft(), opt.rect.bottomRight());
painter->setPen(opt.palette.color(QPalette::Window));
painter->setFont(norm_font);
painter->drawText(text_rect, text, Qt::AlignVCenter | Qt::AlignHCenter);
painter->restore();
} else {

View File

@ -390,6 +390,8 @@ int main(int argc, char* argv[]) {
// Add root CA cert for SoundCloud, whose certificate is missing on OS X.
QSslSocket::addDefaultCaCertificates(
QSslCertificate::fromPath(":/soundcloud-ca.pem", QSsl::Pem));
QSslSocket::addDefaultCaCertificates(
QSslCertificate::fromPath(":/Equifax_Secure_Certificate_Authority.pem", QSsl::Pem));
// Has the user forced a different language?
QString override_language = options.language();

View File

@ -33,6 +33,10 @@
#include "core/logging.h"
#include "core/utilities.h"
#ifdef Q_OS_WIN32
#include <windows.h>
#endif
MoodbarLoader::MoodbarLoader(Application* app, QObject* parent)
: QObject(parent),
cache_(new QNetworkDiskCache(this)),
@ -166,6 +170,15 @@ void MoodbarLoader::RequestFinished(MoodbarPipeline* request, const QUrl& url) {
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;
}

View File

@ -58,8 +58,8 @@ void MusicBrainzClient::Start(int id, const QStringList& mbid_list) {
QNetworkReply* reply = network_->get(req);
NewClosure(reply, SIGNAL(finished()), this,
SLOT(RequestFinished(QNetworkReply*, int, int)),
reply, id, request_number++);
SLOT(RequestFinished(QNetworkReply*, int, int)), reply, id,
request_number++);
requests_.insert(id, reply);
timeouts_->AddReply(reply);
@ -104,12 +104,13 @@ void MusicBrainzClient::DiscIdRequestFinished(const QString& discid,
ResultList ret;
QString artist;
QString album;
int year = 0;
if (reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt() !=
200) {
qLog(Error) << "Error:" <<
reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt() <<
"http status code received";
qLog(Error) << "Error:"
<< reply->attribute(QNetworkRequest::HttpStatusCodeAttribute)
.toInt() << "http status code received";
qLog(Error) << reply->readAll();
emit Finished(artist, album, ret);
return;
@ -118,6 +119,7 @@ void MusicBrainzClient::DiscIdRequestFinished(const QString& discid,
// Parse xml result:
// -get title
// -get artist
// -get year
// -get all the tracks' tags
// Note: If there are multiple releases for the discid, the first
// release is chosen.
@ -128,6 +130,11 @@ void MusicBrainzClient::DiscIdRequestFinished(const QString& discid,
QStringRef name = reader.name();
if (name == "title") {
album = reader.readElementText();
} else if (name == "date") {
QRegExp regex(kDateRegex);
if (regex.indexIn(reader.readElementText()) == 0) {
year = regex.cap(0).toInt();
}
} else if (name == "artist-credit") {
ParseArtist(&reader, &artist);
} else if (name == "medium-list") {
@ -156,16 +163,25 @@ void MusicBrainzClient::DiscIdRequestFinished(const QString& discid,
}
}
// If we parsed a year, copy it to the tracks.
if (year > 0) {
for (ResultList::iterator it = ret.begin(); it != ret.end(); ++it) {
it->year_ = year;
}
}
emit Finished(artist, album, UniqueResults(ret, SortResults));
}
void MusicBrainzClient::RequestFinished(QNetworkReply* reply, int id, int request_number) {
void MusicBrainzClient::RequestFinished(QNetworkReply* reply, int id,
int request_number) {
reply->deleteLater();
const int nb_removed = requests_.remove(id, reply);
if (nb_removed != 1) {
qLog(Error) << "Error: unknown reply received:" << nb_removed <<
"requests removed, while only one was supposed to be removed";
qLog(Error)
<< "Error: unknown reply received:" << nb_removed
<< "requests removed, while only one was supposed to be removed";
}
if (reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt() ==
@ -185,8 +201,9 @@ void MusicBrainzClient::RequestFinished(QNetworkReply* reply, int id, int reques
}
pending_results_[id] << PendingResults(request_number, res);
} else {
qLog(Error) << "Error:" << reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt() <<
"http status code received";
qLog(Error) << "Error:"
<< reply->attribute(QNetworkRequest::HttpStatusCodeAttribute)
.toInt() << "http status code received";
qLog(Error) << reply->readAll();
}
@ -366,7 +383,6 @@ MusicBrainzClient::Release MusicBrainzClient::ParseRelease(
MusicBrainzClient::ResultList MusicBrainzClient::UniqueResults(
const ResultList& results, UniqueResultsSortOption opt) {
ResultList ret;
if (opt == SortResults) {
ret = QSet<Result>::fromList(results).toList();

View File

@ -20,6 +20,7 @@
#include <algorithm>
#include "core/logging.h"
#include "core/timeconstants.h"
#include "engines/enginebase.h"
#include "internet/core/internetmodel.h"
#include "playlist/playlistmanager.h"
@ -60,6 +61,9 @@ IncomingDataParser::IncomingDataParser(Application* app) : app_(app) {
connect(this, SIGNAL(InsertUrls(int, const QList<QUrl>&, int, bool, bool)),
app_->playlist_manager(),
SLOT(InsertUrls(int, const QList<QUrl>&, int, bool, bool)));
connect(this, SIGNAL(InsertSongs(int, const SongList&, int, bool, bool)),
app_->playlist_manager(),
SLOT(InsertSongs(int, const SongList&, int, bool, bool)));
connect(this, SIGNAL(RemoveSongs(int, const QList<int>&)),
app_->playlist_manager(),
SLOT(RemoveItemsWithoutUndo(int, const QList<int>&)));
@ -235,16 +239,28 @@ void IncomingDataParser::SetShuffleMode(const pb::remote::Shuffle& shuffle) {
void IncomingDataParser::InsertUrls(const pb::remote::Message& msg) {
const pb::remote::RequestInsertUrls& request = msg.request_insert_urls();
// Extract urls
QList<QUrl> urls;
for (auto it = request.urls().begin(); it != request.urls().end(); ++it) {
std::string s = *it;
urls << QUrl(QStringFromStdString(s));
// Insert plain urls without metadata
if (request.urls().size() > 0) {
QList<QUrl> urls;
for (auto it = request.urls().begin(); it != request.urls().end(); ++it) {
std::string s = *it;
urls << QUrl(QStringFromStdString(s));
}
// Insert the urls
emit InsertUrls(request.playlist_id(), urls, request.position(),
request.play_now(), request.enqueue());
}
// Insert the urls
emit InsertUrls(request.playlist_id(), urls, request.position(),
request.play_now(), request.enqueue());
// Add songs with metadata if present
if (request.songs().size() > 0) {
SongList songs;
for (int i = 0; i < request.songs().size(); i++) {
songs << CreateSongFromProtobuf(request.songs(i));
}
emit InsertSongs(request.playlist_id(), songs, request.position(),
request.play_now(), request.enqueue());
}
}
void IncomingDataParser::RemoveSongs(const pb::remote::Message& msg) {
@ -301,3 +317,26 @@ void IncomingDataParser::GlobalSearch(RemoteClient *client, const pb::remote::Me
emit DoGlobalSearch(QStringFromStdString(msg.request_global_search().query()),
client);
}
Song IncomingDataParser::CreateSongFromProtobuf(const pb::remote::SongMetadata& pb){
Song song;
song.Init(QStringFromStdString(pb.title()),
QStringFromStdString(pb.artist()),
QStringFromStdString(pb.album()),
pb.length() * kNsecPerSec);
song.set_albumartist(QStringFromStdString(pb.albumartist()));
song.set_genre(QStringFromStdString(pb.genre()));
song.set_year(QStringFromStdString(pb.pretty_year()).toInt());
song.set_track(pb.track());
song.set_disc(pb.disc());
song.set_url(QUrl(QStringFromStdString(pb.url())));
song.set_filesize(pb.file_size());
song.set_rating(pb.rating());
song.set_basefilename(QStringFromStdString(pb.filename()));
song.set_art_automatic(QStringFromStdString(pb.art_automatic()));
song.set_art_manual(QStringFromStdString(pb.art_manual()));
song.set_filetype(static_cast<Song::FileType>(pb.type()));
return song;
}

View File

@ -44,6 +44,8 @@ signals:
void SetShuffleMode(PlaylistSequence::ShuffleMode mode);
void InsertUrls(int id, const QList<QUrl>& urls, int pos, bool play_now,
bool enqueue);
void InsertSongs(int id, const SongList& songs, int pos, bool play_now,
bool enqueue);
void RemoveSongs(int id, const QList<int>& indices);
void SeekTo(int seconds);
void SendLibrary(RemoteClient* client);
@ -67,6 +69,8 @@ signals:
void ClosePlaylist(const pb::remote::Message& msg);
void RateSong(const pb::remote::Message& msg);
void GlobalSearch(RemoteClient* client, const pb::remote::Message& msg);
Song CreateSongFromProtobuf(const pb::remote::SongMetadata& pb);
};
#endif // INCOMINGDATAPARSER_H

View File

@ -79,10 +79,8 @@ void OutgoingDataCreator::SetClients(QList<RemoteClient*>* clients) {
SLOT(ResultsAvailable(int, SearchProvider::ResultList)),
Qt::QueuedConnection);
connect(app_->global_search(),
SIGNAL(SearchFinished(int)),
SLOT(SearchFinished(int)),
Qt::QueuedConnection);
connect(app_->global_search(), SIGNAL(SearchFinished(int)),
SLOT(SearchFinished(int)), Qt::QueuedConnection);
}
void OutgoingDataCreator::CheckEnabledProviders() {
@ -379,6 +377,11 @@ void OutgoingDataCreator::CreateSong(const Song& song, const QImage& art,
song_metadata->set_file_size(song.filesize());
song_metadata->set_rating(song.rating());
song_metadata->set_url(DataCommaSizeFromQString(song.url().toString()));
song_metadata->set_art_automatic(
DataCommaSizeFromQString(song.art_automatic()));
song_metadata->set_art_manual(DataCommaSizeFromQString(song.art_manual()));
song_metadata->set_type(
static_cast< ::pb::remote::SongMetadata_Type>(song.filetype()));
// Append coverart
if (!art.isNull()) {
@ -503,6 +506,9 @@ void OutgoingDataCreator::SendRepeatMode(PlaylistSequence::RepeatMode mode) {
case PlaylistSequence::Repeat_OneByOne:
msg.mutable_repeat()->set_repeat_mode(pb::remote::Repeat_OneByOne);
break;
case PlaylistSequence::Repeat_Intro:
msg.mutable_repeat()->set_repeat_mode(pb::remote::Repeat_Intro);
break;
}
SendDataToClients(&msg);
@ -670,7 +676,8 @@ void OutgoingDataCreator::DoGlobalSearch(const QString& query,
// Send status message
pb::remote::Message msg;
pb::remote::ResponseGlobalSearchStatus* status = msg.mutable_response_global_search_status();
pb::remote::ResponseGlobalSearchStatus* status =
msg.mutable_response_global_search_status();
msg.set_type(pb::remote::GLOBAL_SEARCH_STATUS);
status->set_id(id);
@ -715,7 +722,8 @@ void OutgoingDataCreator::ResultsAvailable(
client->SendData(&msg);
qLog(Debug) << "ResultsAvailable" << id << results.first().provider_->name() << results.size();
qLog(Debug) << "ResultsAvailable" << id << results.first().provider_->name()
<< results.size();
}
void OutgoingDataCreator::SearchFinished(int id) {
@ -725,7 +733,8 @@ void OutgoingDataCreator::SearchFinished(int id) {
// Send status message
pb::remote::Message msg;
pb::remote::ResponseGlobalSearchStatus* status = msg.mutable_response_global_search_status();
pb::remote::ResponseGlobalSearchStatus* status =
msg.mutable_response_global_search_status();
msg.set_type(pb::remote::GLOBAL_SEARCH_STATUS);
status->set_id(req.id_);

View File

@ -284,6 +284,8 @@ QVariant Playlist::data(const QModelIndex& index, int role) const {
return song.disc();
case Column_Year:
return song.year();
case Column_OriginalYear:
return song.effective_originalyear();
case Column_Genre:
return song.genre();
case Column_AlbumArtist:
@ -562,6 +564,7 @@ int Playlist::next_row(bool ignore_repeat_track) const {
switch (playlist_sequence_->repeat_mode()) {
case PlaylistSequence::Repeat_Off:
case PlaylistSequence::Repeat_Intro:
return -1;
case PlaylistSequence::Repeat_Track:
next_virtual_index = current_virtual_index_;
@ -1250,6 +1253,8 @@ bool Playlist::CompareItems(int column, Qt::SortOrder order,
cmp(disc);
case Column_Year:
cmp(year);
case Column_OriginalYear:
cmp(originalyear);
case Column_Genre:
strcmp(genre);
case Column_AlbumArtist:
@ -1319,6 +1324,8 @@ QString Playlist::column_name(Column column) {
return tr("Disc");
case Column_Year:
return tr("Year");
case Column_OriginalYear:
return tr("Original year");
case Column_Genre:
return tr("Genre");
case Column_AlbumArtist:
@ -1394,8 +1401,18 @@ void Playlist::sort(int column, Qt::SortOrder order) {
if (dynamic_playlist_ && current_item_index_.isValid())
begin += current_item_index_.row() + 1;
qStableSort(begin, new_items.end(),
std::bind(&Playlist::CompareItems, column, order, _1, _2));
if (column == Column_Album) {
// When sorting by album, also take into account discs and tracks.
qStableSort(begin, new_items.end(), std::bind(&Playlist::CompareItems,
Column_Track, order, _1, _2));
qStableSort(begin, new_items.end(),
std::bind(&Playlist::CompareItems, Column_Disc, order, _1, _2));
qStableSort(begin, new_items.end(), std::bind(&Playlist::CompareItems,
Column_Album, order, _1, _2));
} else {
qStableSort(begin, new_items.end(),
std::bind(&Playlist::CompareItems, column, order, _1, _2));
}
undo_stack_->push(
new PlaylistUndoCommands::SortItems(this, column, order, new_items));

View File

@ -114,6 +114,7 @@ class Playlist : public QAbstractListModel {
Column_Mood,
Column_Performer,
Column_Grouping,
Column_OriginalYear,
ColumnCount
};

View File

@ -38,6 +38,7 @@ PlaylistFilter::PlaylistFilter(QObject* parent)
column_names_["track"] = Playlist::Column_Track;
column_names_["disc"] = Playlist::Column_Disc;
column_names_["year"] = Playlist::Column_Year;
column_names_["originalyear"] = Playlist::Column_OriginalYear;
column_names_["genre"] = Playlist::Column_Genre;
column_names_["score"] = Playlist::Column_Score;
column_names_["comment"] = Playlist::Column_Comment;
@ -48,8 +49,9 @@ PlaylistFilter::PlaylistFilter(QObject* parent)
numerical_columns_ << Playlist::Column_Length << Playlist::Column_Track
<< Playlist::Column_Disc << Playlist::Column_Year
<< Playlist::Column_Score << Playlist::Column_BPM
<< Playlist::Column_Bitrate << Playlist::Column_Rating;
<< Playlist::Column_OriginalYear << Playlist::Column_Score
<< Playlist::Column_BPM << Playlist::Column_Bitrate
<< Playlist::Column_Rating;
}
PlaylistFilter::~PlaylistFilter() {}

View File

@ -470,6 +470,13 @@ void PlaylistManager::InsertUrls(int id, const QList<QUrl>& urls, int pos,
playlists_[id].p->InsertUrls(urls, pos, play_now, enqueue);
}
void PlaylistManager::InsertSongs(int id, const SongList& songs, int pos,
bool play_now, bool enqueue) {
Q_ASSERT(playlists_.contains(id));
playlists_[id].p->InsertSongs(songs, pos, play_now, enqueue);
}
void PlaylistManager::RemoveItemsWithoutUndo(int id,
const QList<int>& indices) {
Q_ASSERT(playlists_.contains(id));

View File

@ -220,6 +220,8 @@ class PlaylistManager : public PlaylistManagerInterface {
void InsertUrls(int id, const QList<QUrl>& urls, int pos = -1,
bool play_now = false, bool enqueue = false);
void InsertSongs(int id, const SongList& songs, int pos = -1,
bool play_now = false, bool enqueue = false);
// Removes items with given indices from the playlist. This operation is not
// undoable.
void RemoveItemsWithoutUndo(int id, const QList<int>& indices);

View File

@ -53,6 +53,7 @@ PlaylistSequence::PlaylistSequence(QWidget* parent, SettingsProvider* settings)
repeat_group->addAction(ui_->action_repeat_album);
repeat_group->addAction(ui_->action_repeat_playlist);
repeat_group->addAction(ui_->action_repeat_onebyone);
repeat_group->addAction(ui_->action_repeat_intro);
repeat_menu_->addActions(repeat_group->actions());
ui_->repeat->setMenu(repeat_menu_);
@ -120,6 +121,7 @@ void PlaylistSequence::RepeatActionTriggered(QAction* action) {
if (action == ui_->action_repeat_album) mode = Repeat_Album;
if (action == ui_->action_repeat_playlist) mode = Repeat_Playlist;
if (action == ui_->action_repeat_onebyone) mode = Repeat_OneByOne;
if (action == ui_->action_repeat_intro) mode = Repeat_Intro;
SetRepeatMode(mode);
}
@ -152,6 +154,9 @@ void PlaylistSequence::SetRepeatMode(RepeatMode mode) {
case Repeat_OneByOne:
ui_->action_repeat_onebyone->setChecked(true);
break;
case Repeat_Intro:
ui_->action_repeat_intro->setChecked(true);
break;
}
if (mode != repeat_mode_) {
@ -245,6 +250,9 @@ void PlaylistSequence::CycleRepeatMode() {
mode = Repeat_OneByOne;
break;
case Repeat_OneByOne:
mode = Repeat_Intro;
break;
case Repeat_Intro:
break;
}

View File

@ -41,6 +41,7 @@ class PlaylistSequence : public QWidget {
Repeat_Album = 2,
Repeat_Playlist = 3,
Repeat_OneByOne = 4,
Repeat_Intro = 5,
};
enum ShuffleMode {
Shuffle_Off = 0,

View File

@ -106,6 +106,14 @@
<string>Stop after each track</string>
</property>
</action>
<action name="action_repeat_intro">
<property name="checkable">
<bool>true</bool>
</property>
<property name="text">
<string>Intro tracks</string>
</property>
</action>
<action name="action_shuffle_off">
<property name="checkable">
<bool>true</bool>

View File

@ -321,6 +321,7 @@ void PlaylistView::LoadGeometry() {
if (!header_->restoreState(state)) {
header_->HideSection(Playlist::Column_Disc);
header_->HideSection(Playlist::Column_Year);
header_->HideSection(Playlist::Column_OriginalYear);
header_->HideSection(Playlist::Column_Genre);
header_->HideSection(Playlist::Column_BPM);
header_->HideSection(Playlist::Column_Bitrate);
@ -586,10 +587,10 @@ void PlaylistView::keyPressEvent(QKeyEvent* event) {
emit PlayPause();
event->accept();
} else if (event->key() == Qt::Key_Left) {
emit SeekTrack(-1);
emit SeekBackward();
event->accept();
} else if (event->key() == Qt::Key_Right) {
emit SeekTrack(1);
emit SeekForward();
event->accept();
} else if (event->modifiers() ==
Qt::NoModifier // No modifier keys currently pressed...
@ -1191,7 +1192,8 @@ ColumnAlignmentMap PlaylistView::DefaultColumnAlignment() {
ret[Playlist::Column_Filesize] =
ret[Playlist::Column_PlayCount] =
ret[Playlist::Column_SkipCount] =
(Qt::AlignRight | Qt::AlignVCenter);
ret[Playlist::Column_OriginalYear] =
(Qt::AlignRight | Qt::AlignVCenter);
ret[Playlist::Column_Score] = (Qt::AlignCenter);
return ret;

View File

@ -109,7 +109,8 @@ signals:
void PlayItem(const QModelIndex& index);
void PlayPause();
void RightClicked(const QPoint& global_pos, const QModelIndex& index);
void SeekTrack(int gap);
void SeekForward();
void SeekBackward();
void FocusOnFilterSignal(QKeyEvent* event);
void BackgroundPropertyChanged();
void ColumnAlignmentChanged(const ColumnAlignmentMap& alignment);

View File

@ -286,12 +286,14 @@ QString RipCDDialog::GetOutputFileName(const QString& basename) const {
QString RipCDDialog::ParseFileFormatString(const QString& file_format,
int track_no) const {
QString to_return = file_format;
to_return.replace(QString("%artist%"), ui_->artistLineEdit->text());
to_return.replace(QString("%album%"), ui_->albumLineEdit->text());
to_return.replace(QString("%genre%"), ui_->genreLineEdit->text());
to_return.replace(QString("%year%"), ui_->yearLineEdit->text());
to_return.replace(QString("%tracknum%"), QString::number(track_no));
to_return.replace(QString("%track%"),
to_return.replace(QString("%artist"), ui_->artistLineEdit->text());
to_return.replace(QString("%album"), ui_->albumLineEdit->text());
to_return.replace(QString("%disc"), ui_->discLineEdit->text());
to_return.replace(QString("%genre"), ui_->genreLineEdit->text());
to_return.replace(QString("%year"), ui_->yearLineEdit->text());
to_return.replace(QString("%title"),
track_names_.value(track_no - 1)->text());
to_return.replace(QString("%track"), QString::number(track_no));
return to_return;
}

View File

@ -204,7 +204,7 @@
<item row="0" column="1" colspan="2">
<widget class="QLineEdit" name="format_filename">
<property name="text">
<string notr="true">%tracknum% - %artist% - %track%</string>
<string notr="true">%track - %artist - %title</string>
</property>
</widget>
</item>

View File

@ -160,6 +160,7 @@ SearchTerm::Type SearchTerm::TypeOf(Field field) {
case Field_Track:
case Field_Disc:
case Field_Year:
case Field_OriginalYear:
case Field_BPM:
case Field_Bitrate:
case Field_Samplerate:
@ -253,6 +254,8 @@ QString SearchTerm::FieldColumnName(Field field) {
return "disc";
case Field_Year:
return "year";
case Field_OriginalYear:
return "originalyear";
case Field_BPM:
return "bpm";
case Field_Bitrate:
@ -311,6 +314,8 @@ QString SearchTerm::FieldName(Field field) {
return Playlist::column_name(Playlist::Column_Disc);
case Field_Year:
return Playlist::column_name(Playlist::Column_Year);
case Field_OriginalYear:
return Playlist::column_name(Playlist::Column_OriginalYear);
case Field_BPM:
return Playlist::column_name(Playlist::Column_BPM);
case Field_Bitrate:

View File

@ -52,6 +52,7 @@ class SearchTerm {
Field_Filepath,
Field_Performer,
Field_Grouping,
Field_OriginalYear,
FieldCount
};

View File

@ -29,6 +29,7 @@
#include <QPropertyAnimation>
#include <QTimer>
#include <QtDebug>
#include <QKeyEvent>
// Exported by QtGui
void qt_blurImage(QPainter* p, QImage& blurImage, qreal radius, bool quality,
@ -49,6 +50,7 @@ class SearchTermWidget::Overlay : public QWidget {
protected:
void paintEvent(QPaintEvent*);
void mouseReleaseEvent(QMouseEvent*);
void keyReleaseEvent(QKeyEvent* e);
private:
SearchTermWidget* parent_;
@ -209,6 +211,8 @@ void SearchTermWidget::SetActive(bool active) {
delete overlay_;
overlay_ = nullptr;
ui_->container->setEnabled(active);
if (!active) {
overlay_ = new Overlay(this);
}
@ -352,6 +356,7 @@ SearchTermWidget::Overlay::Overlay(SearchTermWidget* parent)
text_(tr("Add search term")),
icon_(IconLoader::Load("list-add").pixmap(kIconSize)) {
raise();
setFocusPolicy(Qt::TabFocus);
}
void SearchTermWidget::Overlay::SetOpacity(float opacity) {
@ -417,4 +422,8 @@ void SearchTermWidget::Overlay::mouseReleaseEvent(QMouseEvent*) {
emit parent_->Clicked();
}
void SearchTermWidget::Overlay::keyReleaseEvent(QKeyEvent* e) {
if (e->key() == Qt::Key_Space) emit parent_->Clicked();
}
} // namespace

File diff suppressed because it is too large Load Diff

Some files were not shown because too many files have changed in this diff Show More