diff --git a/approot/messages.xml b/approot/messages.xml index ede3290f..580032ec 100644 --- a/approot/messages.xml +++ b/approot/messages.xml @@ -164,6 +164,12 @@ Cannot save using a demo account! Bad password Old password must be filled in +ReplayGain mode +No ReplayGain +Auto +Track +Album +Replay gain preAmp Artist list mode All artists Album artists diff --git a/approot/messages_fr.xml b/approot/messages_fr.xml index 571aae0a..78dcda2f 100644 --- a/approot/messages_fr.xml +++ b/approot/messages_fr.xml @@ -164,6 +164,12 @@ Impossible de sauvegarder en utilisant un compte de démo ! Mauvais mot de passe L'ancien mot de passe doit être renseigné +Mode ReplayGain +Pas de ReplayGain +Automatique +Piste +Album +PreAmplification ReplayGain Mode de listage des artistes Tous les artistes Tous les artistes d'album @@ -188,4 +194,5 @@ Oui Non Ce champ ne peut pas être vide +Le nombre doit être compris entre {1} et {2} diff --git a/approot/settings.xml b/approot/settings.xml index cf267f9d..6d8e2409 100644 --- a/approot/settings.xml +++ b/approot/settings.xml @@ -4,161 +4,157 @@ - ${tr:Lms.Settings.appearance} -
-
- -
-
+
+
+ ${tr:Lms.Settings.appearance} +
+
+
+
+ + ${dark-mode-info clas="help-block"} +
+
+
-
- ${dark-mode-info} -
-
-
- ${tr:Lms.Settings.audio} -
-
-
+ ${tr:Lms.Settings.audio} +
${tr:Lms.Settings.audio-settings-are-local}
-
-
-
-
- -
- ${transcode-mode} -
-
- ${transcode-mode-info} -
-
-
- -
- ${transcode-format} -
-
- ${transcode-format-info} -
-
-
- -
-
- ${transcode-bitrate} - kbps +
+
+ +
+ ${transcode-mode} + ${transcode-mode-info class="help-block"} +
+
+
+ +
+ ${transcode-format} + ${transcode-format-info class="help-block"} +
+
+
+ +
+
+ ${transcode-bitrate} + kbps +
+ ${transcode-bitrate-info class="help-block"} +
+
+ +
+ +
+ ${replaygain-mode} + ${replaygain-mode-info class="help-block"} +
+
+
+ +
+
+ ${replaygain-preamp} + dB +
+ ${replaygain-preamp-info class="help-block"} +
-
- ${transcode-bitrate-info} -
-
-
- ${} - ${tr:Lms.Settings.subsonic-api} -
- -
- -
-
-
-
- ${subsonic-transcode-enable-info} -
-
-
- -
- ${subsonic-transcode-format} -
-
- ${subsonic-transcode-format-info} -
-
-
- -
-
- ${subsonic-transcode-bitrate} - kbps + ${} + ${tr:Lms.Settings.subsonic-api} +
+
+
+
+ + ${subsonic-transcode-enable-info class="help-block"} +
+
+
+
+ +
+ ${subsonic-transcode-format} + ${subsonic-transcode-format-info class="help-block"} +
+
+
+ +
+
+ ${subsonic-transcode-bitrate} + kbps +
+ ${subsonic-transcode-bitrate-info class="help-block"} +
+
+
+ +
+ ${subsonic-artist-list-mode} + ${subsonic-artist-list-mode-info class="help-block"} +
-
- ${subsonic-transcode-bitrate-info} -
-
-
- -
- ${subsonic-artist-list-mode} -
-
- ${subsonic-artist-list-mode-info} -
-
+ ${} + ${tr:Lms.Settings.change-password} +
+ ${} +
+ +
+ ${password-old} + ${password-old-info class="help-block"} +
+
+ ${
} +
+ +
+ ${password} + ${password-info class="help-block"} +
+
+
+ +
+ ${password-confirm} + ${password-confirm-info class="help-block"} +
+
-
- ${} - ${tr:Lms.Settings.change-password} -
- ${} -
- -
- ${password-old} -
-
- ${password-old-info} -
-
- ${
} -
- -
- ${password} -
-
- ${password-info} -
-
-
- -
- ${password-confirm} -
-
- ${password-confirm-info} -
-
- -
-
- ${apply-btn class="btn-primary"} ${discard-btn} +
+
+ ${apply-btn class="btn-primary"} ${discard-btn} +
+
diff --git a/docroot/js/mediaplayer.js b/docroot/js/mediaplayer.js index 6135c86a..c5ebcb3f 100644 --- a/docroot/js/mediaplayer.js +++ b/docroot/js/mediaplayer.js @@ -22,10 +22,10 @@ LMS.mediaplayer = function () { var _offset = 0; var _duration = 0; var _audioNativeSrc; - var _audioTranscodedSrc; - var _transcodeMode = TranscodeMode.Never; - var _transcodeFormat = 0; - var _transcodeBitrate = 0; + var _audioTranscodeSrc; + var _settings = {}; + var audioCtx = new (window.AudioContext || window.webkitAudioContext)(); + var _gainNode = audioCtx.createGain(); var _updateControls = function() { if (_elems.audio.paused) { @@ -81,27 +81,15 @@ LMS.mediaplayer = function () { _setVolume(_elems.volumeslider.value); } - var _initTranscodeSettings = function(defaultTranscodeMode, defaultTranscodeFormat, defaultTranscodeBitrate) { - if (typeof(Storage) !== "undefined" && localStorage.transcodeMode) { - _transcodeMode = Number(localStorage.transcodeMode); + var _initDefaultSettings = function(defaultSettings) { + if (typeof(Storage) !== "undefined" && localStorage.settings) { + _settings = Object.assign(defaultSettings, JSON.parse(localStorage.settings)); } else { - _transcodeMode = defaultTranscodeMode; - } - if (typeof(Storage) !== "undefined" && localStorage.transcodeFormat) { - _transcodeFormat = Number(localStorage.transcodeFormat); - } - else { - _transcodeFormat = defaultTranscodeFormat; - } - if (typeof(Storage) !== "undefined" && localStorage.transcodeBitrate) { - _transcodeBitrate = Number(localStorage.transcodeBitrate); - } - else { - _transcodeBitrate = defaultTranscodeBitrate; + _settings = defaultSettings; } - Wt.emit(_root, "settingsLoaded", _transcodeMode, _transcodeFormat, _transcodeBitrate); + Wt.emit(_root, "settingsLoaded", JSON.stringify(_settings)); } var _setVolume = function(volume) { @@ -132,7 +120,12 @@ LMS.mediaplayer = function () { } } - var init = function(root, defaultTranscodeMode, defaultTranscodeFormat, defaultTranscodeBitrate) { + var _setReplayGain = function (replayGain) { + console.log("Applying gain " + replayGain); + _gainNode.gain.value = Math.pow(10, (_settings.replayGain.preAmpGain + replayGain) / 20); + } + + var init = function(root, defaultSettings) { _root = root; _elems.audio = document.getElementById("lms-mp-audio"); @@ -146,6 +139,10 @@ LMS.mediaplayer = function () { _elems.volume = document.getElementById("lms-mp-volume"); _elems.volumeslider = document.getElementById("lms-mp-volume-slider"); + var source = audioCtx.createMediaElementSource(_elems.audio); + source.connect(_gainNode); + _gainNode.connect(audioCtx.destination); + _elems.playpause.addEventListener("click", function() { if (_elems.audio.paused) { if (_elems.audio.firstChild) @@ -199,7 +196,7 @@ LMS.mediaplayer = function () { }); _initVolume(); - _initTranscodeSettings(defaultTranscodeMode, defaultTranscodeFormat, defaultTranscodeBitrate); + _initDefaultSettings(defaultSettings); _elems.volumeslider.addEventListener("input", function() { _setVolume(_elems.volumeslider.value); @@ -251,23 +248,24 @@ LMS.mediaplayer = function () { var loadTrack = function(params, autoplay) { _offset = 0; _duration = params.duration; - _audioNativeSrc = params.native_resource; - _audioTranscodeSrc = params.transcode_resource + "&bitrate=" + _transcodeBitrate + "&format=" + _transcodeFormat; + _audioNativeSrc = params.nativeResource; + _audioTranscodeSrc = params.transcodeResource + "&bitrate=" + _settings.transcode.bitrate + "&format=" + _settings.transcode.format; _elems.seek.max = _duration; _removeAudioSources(); // ! order is important - if (_transcodeMode == TranscodeMode.Never || _transcodeMode == TranscodeMode.IfFormatNotSupported) + if (_settings.transcode.mode == TranscodeMode.Never || _settings.transcode.mode == TranscodeMode.IfFormatNotSupported) { _addAudioSource(_audioNativeSrc); } - if (_transcodeMode == TranscodeMode.Always || _transcodeMode == TranscodeMode.IfFormatNotSupported) + if (_settings.transcode.mode == TranscodeMode.Always || _settings.transcode.mode == TranscodeMode.IfFormatNotSupported) { _addAudioSource(_audioTranscodeSrc); } _elems.audio.load(); + _setReplayGain(params.replayGain); _elems.curtime.innerHTML = _durationToString(_offset); _elems.duration.innerHTML = _durationToString(_duration); @@ -289,15 +287,11 @@ LMS.mediaplayer = function () { _elems.audio.pause(); } - var setSettings = function(transcodeMode, transcodeFormat, transcodeBitrate) { - _transcodeMode = transcodeMode; - _transcodeFormat = transcodeFormat; - _transcodeBitrate = transcodeBitrate; + var setSettings = function(settings) { + _settings = settings; if (typeof(Storage) !== "undefined") { - localStorage.transcodeMode = _transcodeMode; - localStorage.transcodeFormat = _transcodeFormat; - localStorage.transcodeBitrate = _transcodeBitrate; + localStorage.settings = JSON.stringify(_settings); } } diff --git a/src/libs/database/impl/Session.cpp b/src/libs/database/impl/Session.cpp index 22a7b250..2e96ffd1 100644 --- a/src/libs/database/impl/Session.cpp +++ b/src/libs/database/impl/Session.cpp @@ -40,7 +40,7 @@ namespace Database { -#define LMS_DATABASE_VERSION 21 +#define LMS_DATABASE_VERSION 22 using Version = std::size_t; @@ -246,6 +246,14 @@ CREATE TABLE "user_backup" ( { _session.execute("DROP TABLE subsonic_settings"); } + else if (version == 21) + { + _session.execute("ALTER TABLE track ADD track_replay_gain REAL"); + _session.execute("ALTER TABLE track ADD release_replay_gain REAL"); + + // Just increment the scan version of the settings to make the next scheduled scan rescan everything + ScanSettings::get(*this).modify()->incScanVersion(); + } else { LMS_LOG(DB, ERROR) << "Database version " << version << " cannot be handled using migration"; diff --git a/src/libs/database/impl/Track.cpp b/src/libs/database/impl/Track.cpp index e62f875f..f0deedab 100644 --- a/src/libs/database/impl/Track.cpp +++ b/src/libs/database/impl/Track.cpp @@ -38,6 +38,14 @@ _filePath( p.string() ) { } +std::size_t +Track::getCount(Session& session) +{ + session.checkSharedLocked(); + + return session.getDboSession().query("SELECT COUNT(*) FROM track"); +} + std::vector Track::getAll(Session& session, std::optional limit) { @@ -107,13 +115,26 @@ Track::create(Session& session, const std::filesystem::path& p) return res; } -std::vector -Track::getAllPaths(Session& session) +std::vector> +Track::getAllPaths(Session& session, std::optional offset, std::optional size) { + using QueryResultType = std::tuple; session.checkSharedLocked(); - Wt::Dbo::collection res = session.getDboSession().query("SELECT file_path FROM track"); - return std::vector(res.begin(), res.end()); + Wt::Dbo::collection queryRes = session.getDboSession().query("SELECT id,file_path FROM track") + .limit(size ? static_cast(*size) + 1 : -1) + .offset(offset ? static_cast(*offset) : -1); + + std::vector> result; + result.reserve(queryRes.size()); + + std::transform(std::begin(queryRes), std::end(queryRes), std::back_inserter(result), + [](const QueryResultType& queryResult) + { + return std::make_pair(std::get<0>(queryResult), std::get<1>(queryResult)); + }); + + return result; } std::vector diff --git a/src/libs/database/include/database/Track.hpp b/src/libs/database/include/database/Track.hpp index 2bbeb9ee..9dbd4083 100644 --- a/src/libs/database/include/database/Track.hpp +++ b/src/libs/database/include/database/Track.hpp @@ -55,6 +55,7 @@ class Track : public Wt::Dbo::Dbo Track(const std::filesystem::path& p); // Find utility functions + static std::size_t getCount(Session& session); static pointer getByPath(Session& session, const std::filesystem::path& p); static pointer getById(Session& session, IdType id); static pointer getByMBID(Session& session, const UUID& MBID); @@ -74,7 +75,7 @@ class Track : public Wt::Dbo::Dbo static std::vector getAll(Session& session, std::optional limit = {}); static std::vector getAllRandom(Session& session, std::optional limit = {}); static std::vector getAllIds(Session& session); - static std::vector getAllPaths(Session& session); + static std::vector> getAllPaths(Session& session, std::optional offset = std::nullopt, std::optional size = std::nullopt); static std::vector getMBIDDuplicates(Session& session); static std::vector getLastAdded(Session& session, const Wt::WDateTime& after, std::optional size = 1); static std::vector getAllWithMBIDAndMissingFeatures(Session& session); @@ -100,6 +101,8 @@ class Track : public Wt::Dbo::Dbo void setMBID(const std::optional& MBID) { _MBID = MBID ? MBID->getAsString() : ""; } void setCopyright(const std::string& copyright) { _copyright = std::string(copyright, 0, _maxCopyrightLength); } void setCopyrightURL(const std::string& copyrightURL) { _copyrightURL = std::string(copyrightURL, 0, _maxCopyrightURLLength); } + void setTrackReplayGain(float replayGain) { _trackReplayGain = replayGain; } + void setReleaseReplayGain(float replayGain) { _releaseReplayGain = replayGain; } void clearArtistLinks(); void addArtistLink(const Wt::Dbo::ptr& artistLink); void setRelease(Wt::Dbo::ptr release) { _release = release; } @@ -122,6 +125,9 @@ class Track : public Wt::Dbo::Dbo std::optional getMBID() const { return UUID::fromString(_MBID); } std::optional getCopyright() const; std::optional getCopyrightURL() const; + std::optional getTrackReplayGain() const { return _trackReplayGain; } + std::optional getReleaseReplayGain() const { return _releaseReplayGain; } + std::vector> getArtists(TrackArtistLink::Type type = TrackArtistLink::Type::Artist) const; std::vector getArtistIds(TrackArtistLink::Type type = TrackArtistLink::Type::Artist) const; std::vector> getArtistLinks() const; @@ -152,6 +158,8 @@ class Track : public Wt::Dbo::Dbo Wt::Dbo::field(a, _MBID, "mbid"); Wt::Dbo::field(a, _copyright, "copyright"); Wt::Dbo::field(a, _copyrightURL, "copyright_url"); + Wt::Dbo::field(a, _trackReplayGain, "track_replay_gain"); + Wt::Dbo::field(a, _releaseReplayGain, "release_replay_gain"); Wt::Dbo::belongsTo(a, _release, "release", Wt::Dbo::OnDeleteCascade); Wt::Dbo::hasMany(a, _trackArtistLinks, Wt::Dbo::ManyToOne, "track"); Wt::Dbo::hasMany(a, _clusters, Wt::Dbo::ManyToMany, "track_cluster", "", Wt::Dbo::OnDeleteCascade); @@ -184,6 +192,8 @@ class Track : public Wt::Dbo::Dbo std::string _MBID; // Musicbrainz Identifier std::string _copyright; std::string _copyrightURL; + std::optional _trackReplayGain; + std::optional _releaseReplayGain; Wt::Dbo::ptr _release; Wt::Dbo::collection> _trackArtistLinks; diff --git a/src/libs/metadata/impl/TagLibParser.cpp b/src/libs/metadata/impl/TagLibParser.cpp index 2fe31d73..d6a9f3c5 100644 --- a/src/libs/metadata/impl/TagLibParser.cpp +++ b/src/libs/metadata/impl/TagLibParser.cpp @@ -19,13 +19,16 @@ #include "metadata/TagLibParser.hpp" +#include #include #include #include #include +#include #include #include #include +#include #include "utils/Logger.hpp" #include "utils/String.hpp" @@ -263,6 +266,10 @@ TagLibParser::processTag(Track& track, const std::string& tag, const TagLib::Str track.copyright = value; else if (tag == "COPYRIGHTURL") track.copyrightURL = value; + else if (tag == "REPLAYGAIN_ALBUM_GAIN") + track.albumReplayGain = StringUtils::readAs(value); + else if (tag == "REPLAYGAIN_TRACK_GAIN") + track.trackReplayGain = StringUtils::readAs(value); else if (_clusterTypeNames.find(tag) != _clusterTypeNames.end()) { std::set clusterNames; @@ -311,6 +318,21 @@ TagLibParser::parse(const std::filesystem::path& p, bool debug) TagLib::PropertyMap properties {f.file()->properties()}; + auto getAPETags = [&](const TagLib::APE::Tag* apeTag) + { + if (!apeTag) + return; + + for (const auto& [name, values] : apeTag->properties()) + { + if (debug) + std::cout << "APE property: '" << name << "'" << std::endl; + + if (!properties.contains(name)) + properties.insert(name, values); + } + }; + // Not that good embedded pictures handling // WMA @@ -337,9 +359,10 @@ TagLibParser::parse(const std::filesystem::path& p, bool debug) if (!stringAttributeList.isEmpty()) { if (debug) - std::cout << "Property: '" << name << "'" << std::endl; + std::cout << "ASF property: '" << name << "'" << std::endl; - properties.insert(name, stringAttributeList); + if (!properties.contains(name)) + properties.insert(name, stringAttributeList); } } } @@ -352,6 +375,17 @@ TagLibParser::parse(const std::filesystem::path& p, bool debug) if (!mp3File->ID3v2Tag()->frameListMap()["APIC"].isEmpty()) track.hasCover = true; } + + getAPETags(mp3File->APETag()); + } + else if (TagLib::MPC::File* mpcFile {dynamic_cast(f.file())}) + { + getAPETags(mpcFile->APETag()); + } + // WavPack + else if (TagLib::WavPack::File* wavPackFile {dynamic_cast(f.file())}) + { + getAPETags(wavPackFile->APETag()); } // FLAC else if (TagLib::FLAC::File* flacFile {dynamic_cast(f.file())}) @@ -360,7 +394,7 @@ TagLibParser::parse(const std::filesystem::path& p, bool debug) track.hasCover = true; } - for(const auto& property : properties) + for (const auto& property : properties) { const std::string tag {property.first.upper().to8Bit(true)}; const TagLib::StringList& values {property.second}; diff --git a/src/libs/metadata/include/metadata/IParser.hpp b/src/libs/metadata/include/metadata/IParser.hpp index 014fed3e..68b72b4d 100644 --- a/src/libs/metadata/include/metadata/IParser.hpp +++ b/src/libs/metadata/include/metadata/IParser.hpp @@ -75,6 +75,8 @@ namespace MetaData std::optional acoustID; std::string copyright; std::string copyrightURL; + std::optional trackReplayGain; + std::optional albumReplayGain; }; class IParser diff --git a/src/libs/scanner/impl/MediaScanner.cpp b/src/libs/scanner/impl/MediaScanner.cpp index f8a8fa66..c9ead16d 100644 --- a/src/libs/scanner/impl/MediaScanner.cpp +++ b/src/libs/scanner/impl/MediaScanner.cpp @@ -111,7 +111,6 @@ updateArtistIfNeeded(const Artist::pointer& artist, const MetaData::Artist& arti // Sortname may have been updated if (artistInfo.sortName && *artistInfo.sortName != artist->getSortName() ) { - LMS_LOG(DBUPDATER, INFO) << "Setting sort name = '" << *artistInfo.sortName << "'"; artist.modify()->setSortName(*artistInfo.sortName); } } @@ -751,6 +750,10 @@ MediaScanner::scanAudioFile(const std::filesystem::path& file, bool forceScan, S track.modify()->setHasCover(trackInfo->hasCover); track.modify()->setCopyright(trackInfo->copyright); track.modify()->setCopyrightURL(trackInfo->copyrightURL); + if (trackInfo->trackReplayGain) + track.modify()->setTrackReplayGain(*trackInfo->trackReplayGain); + if (trackInfo->albumReplayGain) + track.modify()->setReleaseReplayGain(*trackInfo->albumReplayGain); } void @@ -819,32 +822,61 @@ checkFile(const std::filesystem::path& p, const std::filesystem::path& mediaDire void MediaScanner::removeMissingTracks(ScanStats& stats) { - std::vector trackPaths; + static constexpr std::size_t batchSize {50}; + + LMS_LOG(DBUPDATER, DEBUG) << "Checking tracks to be removed..."; + std::size_t trackCount {}; + { auto transaction {_dbSession.createSharedTransaction()}; - trackPaths = Track::getAllPaths(_dbSession);; + trackCount = Track::getCount(_dbSession); } + LMS_LOG(DBUPDATER, DEBUG) << trackCount << " tracks to be checked..."; - LMS_LOG(DBUPDATER, DEBUG) << "Checking tracks..."; - for (const auto& trackPath : trackPaths) + std::vector> trackPaths; + std::vector tracksToRemove; + + for (std::size_t i {trackCount < batchSize ? 0 : trackCount - batchSize}; ; i -= (i > batchSize ? batchSize : i)) { - if (!_running) - return; + trackPaths.clear(); + tracksToRemove.clear(); - if (!checkFile(trackPath, _mediaDirectory, _fileExtensions)) + { + auto transaction {_dbSession.createSharedTransaction()}; + trackPaths = Track::getAllPaths(_dbSession, i, batchSize); + } + + for (const auto& [trackId, trackPath] : trackPaths) + { + if (!_running) + return; + + if (!checkFile(trackPath, _mediaDirectory, _fileExtensions)) + tracksToRemove.push_back(trackId); + } + + if (!tracksToRemove.empty()) { auto transaction {_dbSession.createUniqueTransaction()}; - Track::pointer track {Track::getByPath(_dbSession, trackPath)}; - if (track) + for (const IdType trackId : tracksToRemove) { - track.remove(); - stats.deletions++; + Track::pointer track {Track::getById(_dbSession, trackId)}; + if (track) + { + track.remove(); + stats.deletions++; + } } } notifyInProgressIfNeeded(stats); + + if (i == 0) + break; } + + LMS_LOG(DBUPDATER, DEBUG) << trackCount << " tracks checked!"; } void diff --git a/src/lms/ui/LmsApplication.cpp b/src/lms/ui/LmsApplication.cpp index eb57811b..ff5df69f 100644 --- a/src/lms/ui/LmsApplication.cpp +++ b/src/lms/ui/LmsApplication.cpp @@ -544,16 +544,14 @@ LmsApplication::createHome() playqueue->playNext(); }); - playqueue->trackSelected.connect([=] (Database::IdType trackId, bool play) + playqueue->trackSelected.connect([=] (Database::IdType trackId, bool play, float replayGain) { - _events.lastLoadedTrackId = trackId; - _events.trackLoaded(trackId, play); + _mediaPlayer->loadTrack(trackId, play, replayGain); }); - playqueue->trackUnselected.connect([=] + playqueue->trackUnselected.connect([=] () { - _events.lastLoadedTrackId.reset(); - _events.trackUnloaded(); + _mediaPlayer->stop(); }); // Events from MediaScanner diff --git a/src/lms/ui/LmsApplication.hpp b/src/lms/ui/LmsApplication.hpp index 9ec6968f..e66d7ec0 100644 --- a/src/lms/ui/LmsApplication.hpp +++ b/src/lms/ui/LmsApplication.hpp @@ -53,13 +53,6 @@ struct Events Wt::Signal appOpen; Wt::Signal appClosed; - // A track is being loaded - Wt::Signal trackLoaded; - std::optional lastLoadedTrackId; - Wt::Signal<> mediaPlayerSettingsAvailable; - // Unload current track - Wt::Signal<> trackUnloaded; - // Database events Wt::Signal<> dbScanned; Wt::Signal dbScanInProgress; diff --git a/src/lms/ui/LmsTheme.cpp b/src/lms/ui/LmsTheme.cpp index 9fcc0df1..1ab59b3a 100644 --- a/src/lms/ui/LmsTheme.cpp +++ b/src/lms/ui/LmsTheme.cpp @@ -86,8 +86,8 @@ LmsTheme::getStyleSheets(Database::User::UITheme theme) case Database::User::UITheme::Dark: return { - {"css/bootstrap-darkly.min.css"}, {"resources/themes/bootstrap/3/wt.css"}, + {"css/bootstrap-darkly.min.css"}, {"css/lms.css"}, {"css/lms-darkly.css"}, }; @@ -95,8 +95,8 @@ LmsTheme::getStyleSheets(Database::User::UITheme theme) case Database::User::UITheme::Light: return { - {"css/bootstrap-flatly.min.css"}, {"resources/themes/bootstrap/3/wt.css"}, + {"css/bootstrap-flatly.min.css"}, {"css/lms.css"}, {"css/lms-flatly.css"}, }; diff --git a/src/lms/ui/MediaPlayer.cpp b/src/lms/ui/MediaPlayer.cpp index 6127b2d5..632d025d 100644 --- a/src/lms/ui/MediaPlayer.cpp +++ b/src/lms/ui/MediaPlayer.cpp @@ -19,6 +19,10 @@ #include "MediaPlayer.hpp" +#include +#include +#include + #include "utils/Logger.hpp" #include "database/Artist.hpp" @@ -31,11 +35,156 @@ #include "resource/AudioFileResource.hpp" #include "utils/String.hpp" +#include "utils/Utils.hpp" #include "LmsApplication.hpp" namespace UserInterface { +static std::string settingsToJSString(const MediaPlayer::Settings& settings) +{ + namespace Json = Wt::Json; + + Json::Object res; + + { + Json::Object transcode; + transcode["mode"] = static_cast(settings.transcode.mode); + transcode["format"] = static_cast(settings.transcode.format); + transcode["bitrate"] = static_cast(settings.transcode.bitrate); + res["transcode"] = std::move(transcode); + } + + { + Json::Object replayGain; + replayGain["mode"] = static_cast(settings.replayGain.mode); + replayGain["preAmpGain"] = settings.replayGain.preAmpGain; + res["replayGain"] = std::move(replayGain); + } + + return Json::serialize(res); +} + +static +std::optional +transcodeModeFromString(const std::string& str) +{ + const auto value {StringUtils::readAs(str)}; + if (!value) + return std::nullopt; + + MediaPlayer::Settings::Transcode::Mode mode {static_cast(*value)}; + switch (mode) + { + case MediaPlayer::Settings::Transcode::Mode::Never: + case MediaPlayer::Settings::Transcode::Mode::Always: + case MediaPlayer::Settings::Transcode::Mode::IfFormatNotSupported: + return mode; + } + + return std::nullopt; +} + +static +std::optional +formatFromString(const std::string& str) +{ + const auto value {StringUtils::readAs(str)}; + if (!value) + return std::nullopt; + + MediaPlayer::Format format {static_cast(*value)}; + switch (format) + { + case MediaPlayer::Format::MP3: + case MediaPlayer::Format::OGG_OPUS: + case MediaPlayer::Format::MATROSKA_OPUS: + case MediaPlayer::Format::OGG_VORBIS: + case MediaPlayer::Format::WEBM_VORBIS: + return format; + } + + return std::nullopt; +} + +static +std::optional +bitrateFromString(const std::string& str) +{ + const auto value {StringUtils::readAs(str)}; + if (!value) + return std::nullopt; + + if (Database::User::audioTranscodeAllowedBitrates.find(*value) != std::cend(Database::User::audioTranscodeAllowedBitrates)) + return *value; + + return std::nullopt; +} + +static +std::optional +replayGainModeFromString(const std::string& str) +{ + const auto value {StringUtils::readAs(str)}; + if (!value) + return std::nullopt; + + MediaPlayer::Settings::ReplayGain::Mode mode {static_cast(*value)}; + switch (mode) + { + case MediaPlayer::Settings::ReplayGain::Mode::None: + case MediaPlayer::Settings::ReplayGain::Mode::Auto: + case MediaPlayer::Settings::ReplayGain::Mode::Track: + case MediaPlayer::Settings::ReplayGain::Mode::Release: + return mode; + } + + return std::nullopt; +} + +static +std::optional +replayGainPreAmpGainFromString(const std::string& str) +{ + const auto value {StringUtils::readAs(str)}; + if (!value) + return std::nullopt; + + return clamp(*value, (double)MediaPlayer::Settings::ReplayGain::minPreAmpGain, (double)MediaPlayer::Settings::ReplayGain::maxPreAmpGain); +} + +static MediaPlayer::Settings settingsfromJSString(const std::string& strSettings) +{ + using Settings = MediaPlayer::Settings; + namespace Json = Wt::Json; + Json::Object parsedSettings; + + Json::parse(strSettings, parsedSettings); + + MediaPlayer::Settings settings; + + { + const Json::Value transcodeValue {parsedSettings.get("transcode")}; + if (transcodeValue.type() == Json::Type::Object) + { + const Json::Object transcode {transcodeValue}; + settings.transcode.mode = transcodeModeFromString(transcode.get("mode").toString().orIfNull("")).value_or(Settings::Transcode::defaultMode); + settings.transcode.format = formatFromString(transcode.get("format").toString().orIfNull("")).value_or(Settings::Transcode::defaultFormat); + settings.transcode.bitrate = bitrateFromString(transcode.get("bitrate").toString().orIfNull("")).value_or(Settings::Transcode::defaultBitrate); + } + } + { + const Json::Value replayGainValue {parsedSettings.get("replayGain")}; + if (replayGainValue.type() == Json::Type::Object) + { + const Json::Object replayGain {replayGainValue}; + settings.replayGain.mode = replayGainModeFromString(replayGain.get("mode").toString().orIfNull("")).value_or(Settings::ReplayGain::defaultMode); + settings.replayGain.preAmpGain = replayGainPreAmpGainFromString(replayGain.get("preAmpGain").toString().orIfNull("")).value_or(Settings::ReplayGain::defaultPreAmpGain); + } + } + + return settings; +} MediaPlayer::MediaPlayer() : Wt::WTemplate {Wt::WString::tr("Lms.MediaPlayer.template")}, @@ -48,119 +197,99 @@ MediaPlayer::MediaPlayer() _artist = bindNew("artist"); _release = bindNew("release"); - _settingsLoaded.connect([this](int mode, int format, int bitrate) + _settingsLoaded.connect([this](const std::string& settings) { - LMS_LOG(UI, DEBUG) << "Settings loaded! mode = " << mode << ", format = " << format << ", bitrate = " << bitrate; + LMS_LOG(UI, DEBUG) << "Settings loaded! '" << settings << "'"; - Settings settings; + _settings = settingsfromJSString(settings); - switch (static_cast(mode)) - { - case TranscodeMode::Always: - case TranscodeMode::Never: - case TranscodeMode::IfFormatNotSupported: - settings.mode = static_cast(mode); - break; - } - - switch (static_cast(format)) - { - case Format::MP3: - case Format::OGG_OPUS: - case Format::MATROSKA_OPUS: - case Format::OGG_VORBIS: - case Format::WEBM_VORBIS: - settings.format = static_cast(format); - break; - } - - if (Database::User::audioTranscodeAllowedBitrates.find(bitrate) != std::cend(Database::User::audioTranscodeAllowedBitrates)) - settings.bitrate = bitrate; - - _settings = settings; - - LmsApp->getEvents().mediaPlayerSettingsAvailable.emit(); + settingsLoaded.emit(); }); { + Settings defaultSettings; + std::ostringstream oss; oss << "LMS.mediaplayer.init(" << jsRef() - << ", " << static_cast(defaultTranscodeMode) - << ", " << static_cast(defaultTranscodeFormat) - << ", " << static_cast(defaultTranscodeBitrate) + << ", defaultSettings = " << settingsToJSString(defaultSettings) << ")"; LMS_LOG(UI, DEBUG) << "Running js = '" << oss.str() << "'"; doJavaScript(oss.str()); } - - LmsApp->getEvents().trackLoaded.connect(this, &MediaPlayer::loadTrack); - LmsApp->getEvents().trackUnloaded.connect(this, &MediaPlayer::stop); } void -MediaPlayer::loadTrack(Database::IdType trackId, bool play) +MediaPlayer::loadTrack(Database::IdType trackId, bool play, float replayGain) { LMS_LOG(UI, DEBUG) << "Playing track ID = " << trackId; - auto transaction {LmsApp->getDbSession().createSharedTransaction()}; - - const auto track {Database::Track::getById(LmsApp->getDbSession(), trackId)}; - const std::string imgResourceMimeType {LmsApp->getImageResource()->getMimeType()}; - - const std::string transcodeResource {LmsApp->getAudioTranscodeResource()->getUrl(trackId)}; - const std::string nativeResource {LmsApp->getAudioFileResource()->getUrl(trackId)}; - - const auto artists {track->getArtists()}; - std::ostringstream oss; - oss - << "var params = {" - << " native_resource: \"" << nativeResource << "\"," - << " transcode_resource: \"" << transcodeResource << "\"," - << " duration: " << std::chrono::duration_cast(track->getDuration()).count() << "," - << " title: \"" << StringUtils::jsEscape(track->getName()) << "\"," - << " artist: \"" << (!artists.empty() ? StringUtils::jsEscape(artists.front()->getName()) : "") << "\"," - << " release: \"" << (track->getRelease() ? StringUtils::jsEscape(track->getRelease()->getName()) : "") << "\"," - << " artwork: [" - << " { src: \"" << LmsApp->getImageResource()->getTrackUrl(trackId, 96) << "\", sizes: \"96x96\", type: \"" << imgResourceMimeType << "\" }," - << " { src: \"" << LmsApp->getImageResource()->getTrackUrl(trackId, 256) << "\", sizes: \"256x256\", type: \"" << imgResourceMimeType << "\" }," - << " { src: \"" << LmsApp->getImageResource()->getTrackUrl(trackId, 512) << "\", sizes: \"512x512\", type: \"" << imgResourceMimeType << "\" }," - << " ]" - << "};"; - oss << "LMS.mediaplayer.loadTrack(params, " << (play ? "true" : "false") << ")"; // true to autoplay + { + auto transaction {LmsApp->getDbSession().createSharedTransaction()}; + + const auto track {Database::Track::getById(LmsApp->getDbSession(), trackId)}; + if (!track) + return; + + const std::string imgResourceMimeType {LmsApp->getImageResource()->getMimeType()}; + + const std::string transcodeResource {LmsApp->getAudioTranscodeResource()->getUrl(trackId)}; + const std::string nativeResource {LmsApp->getAudioFileResource()->getUrl(trackId)}; + + const auto artists {track->getArtists()}; + + oss + << "var params = {" + << " nativeResource: \"" << nativeResource << "\"," + << " transcodeResource: \"" << transcodeResource << "\"," + << " duration: " << std::chrono::duration_cast(track->getDuration()).count() << "," + << " replayGain: " << replayGain << "," + << " title: \"" << StringUtils::jsEscape(track->getName()) << "\"," + << " artist: \"" << (!artists.empty() ? StringUtils::jsEscape(artists.front()->getName()) : "") << "\"," + << " release: \"" << (track->getRelease() ? StringUtils::jsEscape(track->getRelease()->getName()) : "") << "\"," + << " artwork: [" + << " { src: \"" << LmsApp->getImageResource()->getTrackUrl(trackId, 96) << "\", sizes: \"96x96\", type: \"" << imgResourceMimeType << "\" }," + << " { src: \"" << LmsApp->getImageResource()->getTrackUrl(trackId, 256) << "\", sizes: \"256x256\", type: \"" << imgResourceMimeType << "\" }," + << " { src: \"" << LmsApp->getImageResource()->getTrackUrl(trackId, 512) << "\", sizes: \"512x512\", type: \"" << imgResourceMimeType << "\" }," + << " ]" + << "};"; + oss << "LMS.mediaplayer.loadTrack(params, " << (play ? "true" : "false") << ")"; // true to autoplay + + _title->setTextFormat(Wt::TextFormat::Plain); + _title->setText(Wt::WString::fromUTF8(track->getName())); + + if (!artists.empty()) + { + _artist->setTextFormat(Wt::TextFormat::Plain); + _artist->setText(Wt::WString::fromUTF8(artists.front()->getName())); + _artist->setLink(LmsApp->createArtistLink(artists.front())); + } + else + { + _artist->setText(""); + _artist->setLink({}); + } + + if (track->getRelease()) + { + _release->setTextFormat(Wt::TextFormat::Plain); + _release->setText(Wt::WString::fromUTF8(track->getRelease()->getName())); + _release->setLink(LmsApp->createReleaseLink(track->getRelease())); + } + else + { + _release->setText(""); + _release->setLink({}); + } + } LMS_LOG(UI, DEBUG) << "Running js = '" << oss.str() << "'"; - - _title->setTextFormat(Wt::TextFormat::Plain); - _title->setText(Wt::WString::fromUTF8(track->getName())); - - if (!artists.empty()) - { - _artist->setTextFormat(Wt::TextFormat::Plain); - _artist->setText(Wt::WString::fromUTF8(artists.front()->getName())); - _artist->setLink(LmsApp->createArtistLink(artists.front())); - } - else - { - _artist->setText(""); - _artist->setLink({}); - } - - if (track->getRelease()) - { - _release->setTextFormat(Wt::TextFormat::Plain); - _release->setText(Wt::WString::fromUTF8(track->getRelease()->getName())); - _release->setLink(LmsApp->createReleaseLink(track->getRelease())); - } - else - { - _release->setText(""); - _release->setLink({}); - } - wApp->doJavaScript(oss.str()); + + _trackIdLoaded = trackId; + trackLoaded.emit(*_trackIdLoaded); } void @@ -176,11 +305,7 @@ MediaPlayer::setSettings(const Settings& settings) { std::ostringstream oss; - oss << "LMS.mediaplayer.setSettings(" - << static_cast(_settings->mode) - << ", " << static_cast(_settings->format) - << ", " << static_cast(_settings->bitrate) - << ")"; + oss << "LMS.mediaplayer.setSettings(settings = " << settingsToJSString(settings) << ")"; LMS_LOG(UI, DEBUG) << "Running js = '" << oss.str() << "'"; doJavaScript(oss.str()); diff --git a/src/lms/ui/MediaPlayer.hpp b/src/lms/ui/MediaPlayer.hpp index bfb02c5f..6150756c 100644 --- a/src/lms/ui/MediaPlayer.hpp +++ b/src/lms/ui/MediaPlayer.hpp @@ -34,24 +34,59 @@ class MediaPlayer : public Wt::WTemplate public: using Bitrate = Database::Bitrate; using Format = Database::AudioFormat; + using Gain = float; - // Do not change this enum as it may be stored locally in browser + // Do not change enum values as they may be stored locally in browser // Keep it sync with LMS.mediaplayer js - enum class TranscodeMode - { - Never = 0, - Always = 1, - IfFormatNotSupported = 2, - }; - static inline constexpr TranscodeMode defaultTranscodeMode {TranscodeMode::IfFormatNotSupported}; - static inline constexpr Format defaultTranscodeFormat {Format::OGG_OPUS}; - static inline constexpr Bitrate defaultTranscodeBitrate {128000}; struct Settings { - TranscodeMode mode {defaultTranscodeMode}; - Format format {defaultTranscodeFormat}; - Bitrate bitrate {defaultTranscodeBitrate}; + struct Transcode + { + enum class Mode + { + Never = 0, + Always = 1, + IfFormatNotSupported = 2, + }; + static inline constexpr Mode defaultMode {Mode::IfFormatNotSupported}; + static inline constexpr Format defaultFormat {Format::OGG_OPUS}; + static inline constexpr Bitrate defaultBitrate {128000}; + + Mode mode {defaultMode}; + Format format {defaultFormat}; + Bitrate bitrate {defaultBitrate}; + }; + + struct ReplayGain + { + enum class Mode + { + None = 0, + Auto = 1, + Track = 2, + Release = 3, + }; + + enum class ClippingPreventionMode + { + None = 0, + LowerVolume = 1, + }; + + static inline constexpr Mode defaultMode {Mode::None}; + static inline constexpr Gain defaultPreAmpGain {}; + static inline constexpr Gain minPreAmpGain {-15}; + static inline constexpr Gain maxPreAmpGain {15}; + static inline constexpr ClippingPreventionMode defaultClippingPreventionMode {ClippingPreventionMode::None}; + + Mode mode; + Gain preAmpGain; + ClippingPreventionMode clippingPreventionMode; + }; + + Transcode transcode; + ReplayGain replayGain; }; MediaPlayer(); @@ -61,21 +96,27 @@ class MediaPlayer : public Wt::WTemplate MediaPlayer& operator=(const MediaPlayer&) = delete; MediaPlayer& operator=(MediaPlayer&&) = delete; + std::optional getTrackLoaded() const { return _trackIdLoaded; } + + void loadTrack(Database::IdType trackId, bool play, float replayGain); + void stop(); + std::optional getSettings() const { return _settings; } void setSettings(const Settings& settings); // Signals - Wt::JSignal<> playbackEnded; - Wt::JSignal<> playPrevious; - Wt::JSignal<> playNext; + Wt::JSignal<> playbackEnded; + Wt::JSignal<> playPrevious; + Wt::JSignal<> playNext; + Wt::Signal trackLoaded; + Wt::Signal<> settingsLoaded; private: - void stop(); - void loadTrack(Database::IdType trackId, bool play); + std::optional _trackIdLoaded; std::optional _settings; - Wt::JSignal _settingsLoaded; + Wt::JSignal _settingsLoaded; Wt::WText* _title; Wt::WAnchor* _release; Wt::WAnchor* _artist; diff --git a/src/lms/ui/PlayHistoryView.cpp b/src/lms/ui/PlayHistoryView.cpp index 6c08ddc6..0fd2c956 100644 --- a/src/lms/ui/PlayHistoryView.cpp +++ b/src/lms/ui/PlayHistoryView.cpp @@ -30,6 +30,7 @@ #include "resource/ImageResource.hpp" #include "LmsApplication.hpp" +#include "MediaPlayer.hpp" namespace { @@ -99,7 +100,7 @@ PlayHistory::PlayHistory() addSome(); }); - LmsApp->getEvents().trackLoaded.connect([=](Database::IdType trackId, bool /* play */) + LmsApp->getMediaPlayer()->trackLoaded.connect([=](Database::IdType trackId) { auto transaction {LmsApp->getDbSession().createUniqueTransaction()}; diff --git a/src/lms/ui/PlayQueueView.cpp b/src/lms/ui/PlayQueueView.cpp index 1ca16743..29b1c37f 100644 --- a/src/lms/ui/PlayQueueView.cpp +++ b/src/lms/ui/PlayQueueView.cpp @@ -33,8 +33,9 @@ #include "utils/String.hpp" #include "resource/ImageResource.hpp" -#include "TrackStringUtils.hpp" #include "LmsApplication.hpp" +#include "MediaPlayer.hpp" +#include "TrackStringUtils.hpp" namespace UserInterface { @@ -135,8 +136,13 @@ PlayQueue::PlayQueue() if (!LmsApp->getUser()->isDemo()) { - LmsApp->post([=] + LmsApp->getMediaPlayer()->settingsLoaded.connect([=] { + if (_mediaPlayerSettingsLoaded) + return; + + _mediaPlayerSettingsLoaded = true; + std::size_t trackPos {}; { @@ -209,6 +215,7 @@ PlayQueue::loadTrack(std::size_t pos, bool play) Database::IdType trackId {}; bool addRadioTrack {}; + std::optional replayGain {}; { auto transaction {LmsApp->getDbSession().createSharedTransaction()}; @@ -235,6 +242,8 @@ PlayQueue::loadTrack(std::size_t pos, bool play) trackId = track.id(); + replayGain = getReplayGain(track); + if (!LmsApp->getUser()->isDemo()) LmsApp->getUser().modify()->setCurPlayingTrackPos(pos); } @@ -244,7 +253,7 @@ PlayQueue::loadTrack(std::size_t pos, bool play) updateCurrentTrack(true); - trackSelected.emit(trackId, play); + trackSelected.emit(trackId, play, replayGain ? *replayGain : 0); } void @@ -432,5 +441,30 @@ PlayQueue::enqueueRadioTrack() enqueueTracks(trackToAddIds); } +std::optional +PlayQueue::getReplayGain(const Database::Track::pointer& track) const +{ + const auto& settings {LmsApp->getMediaPlayer()->getSettings()}; + if (!settings) + return std::nullopt; + + switch (settings->replayGain.mode) + { + case MediaPlayer::Settings::ReplayGain::Mode::None: + return std::nullopt; + + case MediaPlayer::Settings::ReplayGain::Mode::Track: + return track->getTrackReplayGain(); + + case MediaPlayer::Settings::ReplayGain::Mode::Release: + return track->getReleaseReplayGain(); + + case MediaPlayer::Settings::ReplayGain::Mode::Auto: + return track->getTrackReplayGain(); + } + + return std::nullopt; +} + } // namespace UserInterface diff --git a/src/lms/ui/PlayQueueView.hpp b/src/lms/ui/PlayQueueView.hpp index 4bc8f808..5bb8c605 100644 --- a/src/lms/ui/PlayQueueView.hpp +++ b/src/lms/ui/PlayQueueView.hpp @@ -33,6 +33,7 @@ namespace Similarity { } namespace Database { + class Track; class TrackList; } @@ -53,7 +54,7 @@ class PlayQueue : public Wt::WTemplate void playPrevious(); // Signal emitted when a track is to be load(and optionally played) - Wt::Signal trackSelected; + Wt::Signal trackSelected; // Signal emitted when track is unselected (has to be stopped) Wt::Signal<> trackUnselected; @@ -76,9 +77,11 @@ class PlayQueue : public Wt::WTemplate void addRadioTrackFromSimilarity(std::shared_ptr similarityFinder); void addRadioTrackFromClusters(); + std::optional getReplayGain(const Wt::Dbo::ptr& track) const; bool _repeatAll {}; bool _radioMode {}; + bool _mediaPlayerSettingsLoaded {}; Database::IdType _tracklistId {}; Wt::WContainerWidget* _entriesContainer {}; Wt::WPushButton* _showMore {}; diff --git a/src/lms/ui/SettingsView.cpp b/src/lms/ui/SettingsView.cpp index 73bd3b6b..a06db8b8 100644 --- a/src/lms/ui/SettingsView.cpp +++ b/src/lms/ui/SettingsView.cpp @@ -21,6 +21,8 @@ #include #include +#include +#include #include #include #include @@ -51,6 +53,8 @@ class SettingsModel : public Wt::WFormModel static inline const Field TranscodeModeField {"transcode-mode"}; static inline const Field TranscodeFormatField {"transcode-format"}; static inline const Field TranscodeBitrateField {"transcode-bitrate"}; + static inline const Field ReplayGainModeField {"replaygain-mode"}; + static inline const Field ReplayGainPreAmpGainField {"replaygain-preamp"}; static inline const Field SubsonicArtistListModeField {"subsonic-artist-list-mode"}; static inline const Field SubsonicTranscodeEnableField {"subsonic-transcode-enable"}; static inline const Field SubsonicTranscodeFormatField {"subsonic-transcode-format"}; @@ -59,7 +63,8 @@ class SettingsModel : public Wt::WFormModel static inline const Field PasswordField {"password"}; static inline const Field PasswordConfirmField {"password-confirm"}; - using TranscodeModeModel = ValueStringModel; + using TranscodeModeModel = ValueStringModel; + using ReplayGainModeModel = ValueStringModel; SettingsModel(bool withOldPassword) : _withOldPassword {withOldPassword} @@ -70,6 +75,8 @@ class SettingsModel : public Wt::WFormModel addField(TranscodeModeField); addField(TranscodeBitrateField); addField(TranscodeFormatField); + addField(ReplayGainModeField); + addField(ReplayGainPreAmpGainField); addField(SubsonicTranscodeEnableField); addField(SubsonicTranscodeBitrateField); addField(SubsonicTranscodeFormatField); @@ -83,6 +90,12 @@ class SettingsModel : public Wt::WFormModel setValidator(TranscodeModeField, createMandatoryValidator()); setValidator(TranscodeBitrateField, createMandatoryValidator()); setValidator(TranscodeFormatField, createMandatoryValidator()); + setValidator(ReplayGainModeField, createMandatoryValidator()); + { + auto preampGainValidator {std::make_unique()}; + preampGainValidator->setRange(MediaPlayer::Settings::ReplayGain::minPreAmpGain, MediaPlayer::Settings::ReplayGain::maxPreAmpGain); + setValidator(ReplayGainPreAmpGainField, std::move(preampGainValidator)); + } setValidator(SubsonicTranscodeBitrateField, createMandatoryValidator()); setValidator(SubsonicTranscodeFormatField, createMandatoryValidator()); @@ -92,6 +105,7 @@ class SettingsModel : public Wt::WFormModel std::shared_ptr getTranscodeModeModel() { return _transcodeModeModel; } std::shared_ptr getTranscodeBitrateModel() { return _transcodeBitrateModel; } std::shared_ptr getTranscodeFormatModel() { return _transcodeFormatModel; } + std::shared_ptr getReplayGainModeModel() { return _replayGainModeModel; } std::shared_ptr getSubsonicArtistListModeModel() { return _subsonicArtistListModeModel; } void saveData() @@ -118,15 +132,21 @@ class SettingsModel : public Wt::WFormModel auto transcodeModeRow {_transcodeModeModel->getRowFromString(valueText(TranscodeModeField))}; if (transcodeModeRow) - settings.mode = _transcodeModeModel->getValue(*transcodeModeRow); + settings.transcode.mode = _transcodeModeModel->getValue(*transcodeModeRow); auto transcodeFormatRow {_transcodeFormatModel->getRowFromString(valueText(TranscodeFormatField))}; if (transcodeFormatRow) - settings.format = _transcodeFormatModel->getValue(*transcodeFormatRow); + settings.transcode.format = _transcodeFormatModel->getValue(*transcodeFormatRow); auto transcodeBitrateRow {_transcodeBitrateModel->getRowFromString(valueText(TranscodeBitrateField))}; if (transcodeBitrateRow) - settings.bitrate = _transcodeBitrateModel->getValue(*transcodeBitrateRow); + settings.transcode.bitrate = _transcodeBitrateModel->getValue(*transcodeBitrateRow); + + auto replayGainModeRow {_replayGainModeModel->getRowFromString(valueText(ReplayGainModeField))}; + if (replayGainModeRow) + settings.replayGain.mode = _replayGainModeModel->getValue(*replayGainModeRow); + + settings.replayGain.preAmpGain = Wt::asNumber(value(ReplayGainPreAmpGainField)); LmsApp->getMediaPlayer()->setSettings(settings); } @@ -165,17 +185,23 @@ class SettingsModel : public Wt::WFormModel { const auto& settings {*LmsApp->getMediaPlayer()->getSettings()}; - auto transcodeModeRow {_transcodeModeModel->getRowFromValue(settings.mode)}; + auto transcodeModeRow {_transcodeModeModel->getRowFromValue(settings.transcode.mode)}; if (transcodeModeRow) setValue(TranscodeModeField, _transcodeModeModel->getString(*transcodeModeRow)); - auto transcodeFormatRow {_transcodeFormatModel->getRowFromValue(settings.format)}; + auto transcodeFormatRow {_transcodeFormatModel->getRowFromValue(settings.transcode.format)}; if (transcodeFormatRow) setValue(TranscodeFormatField, _transcodeFormatModel->getString(*transcodeFormatRow)); - auto transcodeBitrateRow {_transcodeBitrateModel->getRowFromValue(settings.bitrate)}; + auto transcodeBitrateRow {_transcodeBitrateModel->getRowFromValue(settings.transcode.bitrate)}; if (transcodeBitrateRow) setValue(TranscodeBitrateField, _transcodeBitrateModel->getString(*transcodeBitrateRow)); + + auto replayGainModeRow {_replayGainModeModel->getRowFromValue(settings.replayGain.mode)}; + if (replayGainModeRow) + setValue(ReplayGainModeField, _replayGainModeModel->getString(*replayGainModeRow)); + + setValue(ReplayGainPreAmpGainField, settings.replayGain.preAmpGain); } setValue(SubsonicTranscodeEnableField, LmsApp->getUser()->getSubsonicTranscodeEnable()); @@ -271,13 +297,9 @@ class SettingsModel : public Wt::WFormModel { _transcodeModeModel = std::make_shared(); - _transcodeModeModel->add(Wt::WString::tr("Lms.Settings.transcode-mode.always"), MediaPlayer::TranscodeMode::Always); - _transcodeModeModel->add(Wt::WString::tr("Lms.Settings.transcode-mode.never"), MediaPlayer::TranscodeMode::Never); - _transcodeModeModel->add(Wt::WString::tr("Lms.Settings.transcode-mode.if-format-not-supported"), MediaPlayer::TranscodeMode::IfFormatNotSupported); - - _subsonicArtistListModeModel = std::make_shared>(); - _subsonicArtistListModeModel->add(Wt::WString::tr("Lms.Settings.subsonic-artist-list-mode.all-artists"), User::SubsonicArtistListMode::AllArtists); - _subsonicArtistListModeModel->add(Wt::WString::tr("Lms.Settings.subsonic-artist-list-mode.release-artists"), User::SubsonicArtistListMode::ReleaseArtists); + _transcodeModeModel->add(Wt::WString::tr("Lms.Settings.transcode-mode.always"), MediaPlayer::Settings::Transcode::Mode::Always); + _transcodeModeModel->add(Wt::WString::tr("Lms.Settings.transcode-mode.never"), MediaPlayer::Settings::Transcode::Mode::Never); + _transcodeModeModel->add(Wt::WString::tr("Lms.Settings.transcode-mode.if-format-not-supported"), MediaPlayer::Settings::Transcode::Mode::IfFormatNotSupported); _transcodeBitrateModel = std::make_shared>(); for (const Bitrate bitrate : User::audioTranscodeAllowedBitrates) @@ -291,6 +313,16 @@ class SettingsModel : public Wt::WFormModel _transcodeFormatModel->add(Wt::WString::tr("Lms.Settings.transcode-format.matroska_opus"), AudioFormat::MATROSKA_OPUS); _transcodeFormatModel->add(Wt::WString::tr("Lms.Settings.transcode-format.ogg_vorbis"), AudioFormat::OGG_VORBIS); _transcodeFormatModel->add(Wt::WString::tr("Lms.Settings.transcode-format.webm_vorbis"), AudioFormat::WEBM_VORBIS); + + _replayGainModeModel = std::make_shared(); + _replayGainModeModel->add(Wt::WString::tr("Lms.Settings.replaygain-mode.none"), MediaPlayer::Settings::ReplayGain::Mode::None); + _replayGainModeModel->add(Wt::WString::tr("Lms.Settings.replaygain-mode.auto"), MediaPlayer::Settings::ReplayGain::Mode::Auto); + _replayGainModeModel->add(Wt::WString::tr("Lms.Settings.replaygain-mode.track"), MediaPlayer::Settings::ReplayGain::Mode::Track); + _replayGainModeModel->add(Wt::WString::tr("Lms.Settings.replaygain-mode.release"), MediaPlayer::Settings::ReplayGain::Mode::Release); + + _subsonicArtistListModeModel = std::make_shared>(); + _subsonicArtistListModeModel->add(Wt::WString::tr("Lms.Settings.subsonic-artist-list-mode.all-artists"), User::SubsonicArtistListMode::AllArtists); + _subsonicArtistListModeModel->add(Wt::WString::tr("Lms.Settings.subsonic-artist-list-mode.release-artists"), User::SubsonicArtistListMode::ReleaseArtists); } bool _withOldPassword {}; @@ -298,17 +330,18 @@ class SettingsModel : public Wt::WFormModel std::shared_ptr _transcodeModeModel; std::shared_ptr> _transcodeBitrateModel; std::shared_ptr> _transcodeFormatModel; + std::shared_ptr _replayGainModeModel; std::shared_ptr> _subsonicArtistListModeModel; }; SettingsView::SettingsView() { - wApp->internalPathChanged().connect(std::bind([=] + wApp->internalPathChanged().connect([=] { refreshView(); - })); + }); - LmsApp->getEvents().mediaPlayerSettingsAvailable.connect([=]() + LmsApp->getMediaPlayer()->settingsLoaded.connect([=]() { refreshView(); }); @@ -376,15 +409,40 @@ SettingsView::refreshView() transcodeBitrate->setModel(model->getTranscodeBitrateModel()); t->setFormWidget(SettingsModel::TranscodeBitrateField, std::move(transcodeBitrate)); - transcodeModeRaw->sactivated().connect([=]() + transcodeModeRaw->activated().connect([=](int row) { - auto row {model->getTranscodeModeModel()->getRowFromString(model->valueText(SettingsModel::TranscodeModeField))}; - const bool enable = (row && (model->getTranscodeModeModel()->getValue(*row) != MediaPlayer::TranscodeMode::Never)); + const bool enable {model->getTranscodeModeModel()->getValue(row) != MediaPlayer::Settings::Transcode::Mode::Never}; model->setReadOnly(SettingsModel::TranscodeFormatField, !enable); model->setReadOnly(SettingsModel::TranscodeBitrateField, !enable); t->updateModel(model.get()); t->updateView(model.get()); }); + if (LmsApp->getMediaPlayer()->getSettings()->transcode.mode == MediaPlayer::Settings::Transcode::Mode::Never) + { + model->setReadOnly(SettingsModel::TranscodeFormatField, true); + model->setReadOnly(SettingsModel::TranscodeBitrateField, true); + } + + // Replay gain mode + auto replayGainMode {std::make_unique()}; + auto* replayGainModeRaw {replayGainMode.get()}; + replayGainMode->setModel(model->getReplayGainModeModel()); + t->setFormWidget(SettingsModel::ReplayGainModeField, std::move(replayGainMode)); + + // Replay gain preampGain + auto replayGainPreampGain {std::make_unique()}; + replayGainPreampGain->setRange(MediaPlayer::Settings::ReplayGain::minPreAmpGain, MediaPlayer::Settings::ReplayGain::maxPreAmpGain); + t->setFormWidget(SettingsModel::ReplayGainPreAmpGainField, std::move(replayGainPreampGain)); + + replayGainModeRaw->activated().connect([=](int row) + { + const bool enable {model->getReplayGainModeModel()->getValue(row) != MediaPlayer::Settings::ReplayGain::Mode::None}; + model->setReadOnly(SettingsModel::SettingsModel::ReplayGainPreAmpGainField, !enable); + t->updateModel(model.get()); + t->updateView(model.get()); + }); + if (LmsApp->getMediaPlayer()->getSettings()->replayGain.mode == MediaPlayer::Settings::ReplayGain::Mode::None) + model->setReadOnly(SettingsModel::SettingsModel::ReplayGainPreAmpGainField, true); } // Subsonic diff --git a/src/lms/ui/explore/ArtistsInfoView.cpp b/src/lms/ui/explore/ArtistsInfoView.cpp index 18e8d118..e9181a64 100644 --- a/src/lms/ui/explore/ArtistsInfoView.cpp +++ b/src/lms/ui/explore/ArtistsInfoView.cpp @@ -26,6 +26,7 @@ #include "database/User.hpp" #include "ArtistLink.hpp" #include "LmsApplication.hpp" +#include "MediaPlayer.hpp" using namespace Database; @@ -44,7 +45,7 @@ ArtistsInfo::ArtistsInfo() refreshRecentlyAdded(); }); - LmsApp->getEvents().trackLoaded.connect([=] + LmsApp->getMediaPlayer()->trackLoaded.connect([=] { refreshMostPlayed(); }); diff --git a/src/lms/ui/explore/ReleaseView.cpp b/src/lms/ui/explore/ReleaseView.cpp index 5df3ba4c..3604b266 100644 --- a/src/lms/ui/explore/ReleaseView.cpp +++ b/src/lms/ui/explore/ReleaseView.cpp @@ -37,6 +37,7 @@ #include "Filters.hpp" #include "LmsApplication.hpp" #include "LmsApplicationException.hpp" +#include "MediaPlayer.hpp" #include "TrackStringUtils.hpp" using namespace Database; @@ -46,10 +47,10 @@ namespace UserInterface { Release::Release(Filters* filters) : _filters(filters) { - wApp->internalPathChanged().connect(std::bind([=] + wApp->internalPathChanged().connect([=]() { refresh(); - })); + }); refresh(); @@ -217,27 +218,28 @@ Release::refresh() } Wt::WText* playBtn {entry->bindNew("play-btn", Wt::WString::tr("Lms.Explore.template.play-btn"), Wt::TextFormat::XHTML)}; - playBtn->clicked().connect(std::bind([=] + playBtn->clicked().connect([=]() { tracksPlay.emit({trackId}); - })); + }); Wt::WText* addBtn {entry->bindNew("add-btn", Wt::WString::tr("Lms.Explore.template.add-btn"), Wt::TextFormat::XHTML)}; - addBtn->clicked().connect(std::bind([=] + addBtn->clicked().connect([=]() { tracksAdd.emit({trackId}); - })); + }); entry->bindString("duration", trackDurationToString(track->getDuration()), Wt::TextFormat::Plain); - LmsApp->getEvents().trackLoaded.connect(entry, [=] (Database::IdType loadedTrackId, bool /*play*/) + LmsApp->getMediaPlayer()->trackLoaded.connect(entry, [=] (Database::IdType loadedTrackId) { entry->bindString("is-playing", loadedTrackId == trackId ? "Lms-entry-playing" : ""); }); - if (LmsApp->getEvents().lastLoadedTrackId && *LmsApp->getEvents().lastLoadedTrackId == trackId) + if (auto trackIdLoaded {LmsApp->getMediaPlayer()->getTrackLoaded()}) { - entry->bindString("is-playing", "Lms-entry-playing"); + if (*trackIdLoaded == trackId) + entry->bindString("is-playing", "Lms-entry-playing"); } } } diff --git a/src/lms/ui/explore/ReleasesInfoView.cpp b/src/lms/ui/explore/ReleasesInfoView.cpp index 14ccb04d..0693cc32 100644 --- a/src/lms/ui/explore/ReleasesInfoView.cpp +++ b/src/lms/ui/explore/ReleasesInfoView.cpp @@ -27,6 +27,7 @@ #include "resource/ImageResource.hpp" #include "ReleaseLink.hpp" #include "LmsApplication.hpp" +#include "MediaPlayer.hpp" using namespace Database; @@ -45,7 +46,7 @@ ReleasesInfo::ReleasesInfo() refreshRecentlyAdded(); }); - LmsApp->getEvents().trackLoaded.connect([=] + LmsApp->getMediaPlayer()->trackLoaded.connect([=] { refreshMostPlayed(); }); diff --git a/src/lms/ui/explore/TracksInfoView.cpp b/src/lms/ui/explore/TracksInfoView.cpp index 58469612..d05cc200 100644 --- a/src/lms/ui/explore/TracksInfoView.cpp +++ b/src/lms/ui/explore/TracksInfoView.cpp @@ -27,6 +27,7 @@ #include "database/User.hpp" #include "LmsApplication.hpp" +#include "MediaPlayer.hpp" using namespace Database; @@ -49,7 +50,7 @@ void addEntries(Wt::WContainerWidget *container, const std::vectorgetEvents().trackLoaded.connect([=] + LmsApp->getMediaPlayer()->trackLoaded.connect([=] { refreshMostPlayed(); }); diff --git a/src/lms/ui/explore/TracksView.cpp b/src/lms/ui/explore/TracksView.cpp index 14adef53..5ccf0252 100644 --- a/src/lms/ui/explore/TracksView.cpp +++ b/src/lms/ui/explore/TracksView.cpp @@ -34,6 +34,7 @@ #include "Filters.hpp" #include "LmsApplication.hpp" +#include "MediaPlayer.hpp" #include "TrackStringUtils.hpp" using namespace Database; @@ -174,14 +175,15 @@ Tracks::addSome() })); - LmsApp->getEvents().trackLoaded.connect(entry, [=] (Database::IdType loadedTrackId, bool /*play*/) + LmsApp->getMediaPlayer()->trackLoaded.connect(entry, [=] (Database::IdType loadedTrackId) { entry->bindString("is-playing", loadedTrackId == trackId ? "Lms-entry-playing" : ""); }); - if (LmsApp->getEvents().lastLoadedTrackId && *LmsApp->getEvents().lastLoadedTrackId == trackId) + if (auto trackIdLoaded {LmsApp->getMediaPlayer()->getTrackLoaded()}) { - entry->bindString("is-playing", "Lms-entry-playing"); + if (*trackIdLoaded == trackId) + entry->bindString("is-playing", "Lms-entry-playing"); } } diff --git a/src/test/database/DatabaseTest.cpp b/src/test/database/DatabaseTest.cpp index 42c39505..6b57df85 100644 --- a/src/test/database/DatabaseTest.cpp +++ b/src/test/database/DatabaseTest.cpp @@ -154,12 +154,19 @@ static void testSingleTrack(Session& session) { + { + auto transaction {session.createSharedTransaction()}; + CHECK(Track::getCount(session) == 0); + } + ScopedTrack track {session, "MyTrackFile"}; { - auto transaction {session.createUniqueTransaction()}; + auto transaction {session.createSharedTransaction()}; CHECK(Track::getAll(session).size() == 1); + CHECK(Track::getCount(session) == 1); + } } diff --git a/src/tools/metadata/LmsMetadata.cpp b/src/tools/metadata/LmsMetadata.cpp index b3af8687..7cbfd29b 100644 --- a/src/tools/metadata/LmsMetadata.cpp +++ b/src/tools/metadata/LmsMetadata.cpp @@ -124,6 +124,12 @@ void parse(MetaData::IParser& parser, const std::filesystem::path& file) for (const auto& audioStream : track->audioStreams) std::cout << "Audio stream: " << audioStream.bitRate << " bps" << std::endl; + if (track->trackReplayGain) + std::cout << "Track replay gain: " << *track->trackReplayGain << std::endl; + + if (track->albumReplayGain) + std::cout << "Album replay gain: " << *track->albumReplayGain << std::endl; + if (track->acoustID) std::cout << "AcoustID: " << track->acoustID->getAsString() << std::endl;