diff --git a/res/schema.xml b/res/schema.xml
index 7d26095f722..abda817a68a 100644
--- a/res/schema.xml
+++ b/res/schema.xml
@@ -395,4 +395,14 @@ METADATA
ALTER TABLE library ADD COLUMN coverart_hash INTEGER DEFAULT 0;
+
+
+ Add full replay gain support including peak amplitude. The default
+ value for the peak amplitude is "undefined", represented by any
+ negative value. The internal constant for "undefined" is -1.0.
+
+
+ ALTER TABLE library ADD COLUMN replaygain_peak REAL DEFAULT -1.0;
+
+
diff --git a/src/analyserrg.cpp b/src/analyserrg.cpp
index 5be15c55f80..5af96d0392b 100644
--- a/src/analyserrg.cpp
+++ b/src/analyserrg.cpp
@@ -31,8 +31,7 @@ bool AnalyserGain::initialise(TrackPointer tio, int sampleRate, int totalSamples
bool AnalyserGain::loadStored(TrackPointer tio) const {
bool bAnalyserEnabled = (bool)m_pConfigReplayGain->getValueString(ConfigKey("[ReplayGain]","ReplayGainAnalyserEnabled")).toInt();
- float fReplayGain = tio->getReplayGain();
- if (fReplayGain != 0 || !bAnalyserEnabled) {
+ if (tio->getReplayGain().hasRatio() || !bAnalyserEnabled) {
return true;
}
return false;
@@ -76,11 +75,9 @@ void AnalyserGain::finalise(TrackPointer tio) {
return;
}
- float fReplayGain_Result = db2ratio(ReplayGainOutput);
+ Mixxx::ReplayGain replayGain(tio->getReplayGain());
+ replayGain.setRatio(db2ratio(ReplayGainOutput));
+ tio->setReplayGain(replayGain);
- //qDebug() << "ReplayGain result is" << ReplayGainOutput << "pow:" << fReplayGain_Result;
- //qDebug()<<"ReplayGain outputs "<< ReplayGainOutput << "db for track "<< tio->getLocation();
- tio->setReplayGain(fReplayGain_Result);
- //if(fReplayGain_Result) qDebug() << "ReplayGain Analyser found a ReplayGain value of "<< 20*log10(fReplayGain_Result) << "dB for track " << (tio->getLocation());
m_bStepControl=false;
}
diff --git a/src/basetrackplayer.cpp b/src/basetrackplayer.cpp
index 78283c02d25..4c9e990b3a4 100644
--- a/src/basetrackplayer.cpp
+++ b/src/basetrackplayer.cpp
@@ -168,8 +168,8 @@ void BaseTrackPlayerImpl::slotLoadTrack(TrackPointer track, bool bPlay) {
m_pKey, SLOT(slotSet(double)));
// Listen for updates to the file's Replay Gain
- connect(m_pLoadedTrack.data(), SIGNAL(ReplayGainUpdated(double)),
- this, SLOT(slotSetReplayGain(double)));
+ connect(m_pLoadedTrack.data(), SIGNAL(ReplayGainUpdated(Mixxx::ReplayGain)),
+ this, SLOT(slotSetReplayGain(Mixxx::ReplayGain)));
}
// Request a new track from the reader
@@ -231,7 +231,7 @@ void BaseTrackPlayerImpl::slotFinishLoading(TrackPointer pTrackInfoObject)
m_pDuration->set(m_pLoadedTrack->getDuration());
m_pBPM->slotSet(m_pLoadedTrack->getBpm());
m_pKey->slotSet(m_pLoadedTrack->getKey());
- m_pReplayGain->set(m_pLoadedTrack->getReplayGain());
+ m_pReplayGain->set(m_pLoadedTrack->getReplayGain().getRatio());
// Update the PlayerInfo class that is used in EngineShoutcast to replace
// the metadata of a stream
@@ -298,11 +298,11 @@ TrackPointer BaseTrackPlayerImpl::getLoadedTrack() const {
return m_pLoadedTrack;
}
-void BaseTrackPlayerImpl::slotSetReplayGain(double replayGain) {
+void BaseTrackPlayerImpl::slotSetReplayGain(Mixxx::ReplayGain replayGain) {
// Do not change replay gain when track is playing because
// this may lead to an unexpected volume change
if (m_pPlay->get() == 0.0) {
- m_pReplayGain->set(replayGain);
+ m_pReplayGain->set(replayGain.getRatio());
} else {
m_replaygainPending = true;
}
@@ -310,7 +310,7 @@ void BaseTrackPlayerImpl::slotSetReplayGain(double replayGain) {
void BaseTrackPlayerImpl::slotPlayToggled(double v) {
if (!v && m_replaygainPending) {
- m_pReplayGain->set(m_pLoadedTrack->getReplayGain());
+ m_pReplayGain->set(m_pLoadedTrack->getReplayGain().getRatio());
m_replaygainPending = false;
}
}
diff --git a/src/basetrackplayer.h b/src/basetrackplayer.h
index 406a6c0adee..36398a8e1d6 100644
--- a/src/basetrackplayer.h
+++ b/src/basetrackplayer.h
@@ -68,7 +68,7 @@ class BaseTrackPlayerImpl : public BaseTrackPlayer {
void slotFinishLoading(TrackPointer pTrackInfoObject);
void slotLoadFailed(TrackPointer pTrackInfoObject, QString reason);
void slotUnloadTrack(TrackPointer track);
- void slotSetReplayGain(double replayGain);
+ void slotSetReplayGain(Mixxx::ReplayGain replayGain);
void slotPlayToggled(double);
private:
diff --git a/src/dlgtrackinfo.cpp b/src/dlgtrackinfo.cpp
index 9d7db9a3171..e38adb2a8b3 100644
--- a/src/dlgtrackinfo.cpp
+++ b/src/dlgtrackinfo.cpp
@@ -159,6 +159,8 @@ void DlgTrackInfo::populateFields(TrackPointer pTrack) {
txtBitrate->setText(QString(pTrack->getBitrateStr()) + (" ") + tr("kbps"));
txtBpm->setText(pTrack->getBpmStr());
txtKey->setText(pTrack->getKeyText());
+ const Mixxx::ReplayGain replayGain(pTrack->getReplayGain());
+ txtReplayGain->setText(Mixxx::ReplayGain::ratioToString(replayGain.getRatio()));
BeatsPointer pBeats = pTrack->getBeats();
bool beatsSupportsSet = !pBeats || (pBeats->getCapabilities() & Beats::BEATSCAP_SET);
bool enableBpmEditing = !pTrack->hasBpmLock() && beatsSupportsSet;
@@ -434,6 +436,7 @@ void DlgTrackInfo::clear() {
txtLocation->setPlainText("");
txtBitrate->setText("");
txtBpm->setText("");
+ txtReplayGain->setText("");
m_cueMap.clear();
cueTable->clearContents();
diff --git a/src/dlgtrackinfo.ui b/src/dlgtrackinfo.ui
index 50e00c51578..b13952a0f2b 100644
--- a/src/dlgtrackinfo.ui
+++ b/src/dlgtrackinfo.ui
@@ -473,7 +473,7 @@
-
-
+
-
@@ -490,6 +490,16 @@
+ -
+
+
+
+
+
+ Qt::LinksAccessibleByMouse|Qt::TextSelectableByKeyboard|Qt::TextSelectableByMouse
+
+
+
-
@@ -506,8 +516,18 @@
- -
-
+
-
+
+
+
+
+
+ Qt::LinksAccessibleByMouse|Qt::TextSelectableByKeyboard|Qt::TextSelectableByMouse
+
+
+
+ -
+
75
@@ -515,25 +535,15 @@
- Location:
+ Replay Gain:
- Qt::AlignLeading|Qt::AlignLeft|Qt::AlignTop
-
-
-
- -
-
-
-
-
-
- Qt::LinksAccessibleByMouse|Qt::TextSelectableByKeyboard|Qt::TextSelectableByMouse
+ Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter
- -
-
+
-
+
@@ -542,7 +552,7 @@
- -
+
-
@@ -558,7 +568,7 @@
- -
+
-
@@ -568,7 +578,7 @@
- -
+
-
@@ -584,7 +594,7 @@
- -
+
-
@@ -594,7 +604,23 @@
- -
+
-
+
+
+
+ 75
+ true
+
+
+
+ Location:
+
+
+ Qt::AlignLeading|Qt::AlignLeft|Qt::AlignTop
+
+
+
+ -
true
diff --git a/src/library/basesqltablemodel.cpp b/src/library/basesqltablemodel.cpp
index 1314d1f165c..e12e228e221 100644
--- a/src/library/basesqltablemodel.cpp
+++ b/src/library/basesqltablemodel.cpp
@@ -105,6 +105,8 @@ void BaseSqlTableModel::initHeaderData() {
tr("Preview"), 50);
setHeaderProperties(ColumnCache::COLUMN_LIBRARYTABLE_COVERART,
tr("Cover Art"), 90);
+ setHeaderProperties(ColumnCache::COLUMN_LIBRARYTABLE_REPLAYGAIN,
+ tr("Replay Gain"), 50);
}
QSqlDatabase BaseSqlTableModel::database() const {
@@ -172,7 +174,8 @@ bool BaseSqlTableModel::isColumnHiddenByDefault(int column) {
(column == fieldIndex(ColumnCache::COLUMN_LIBRARYTABLE_YEAR)) ||
(column == fieldIndex(ColumnCache::COLUMN_LIBRARYTABLE_GROUPING)) ||
(column == fieldIndex(ColumnCache::COLUMN_LIBRARYTABLE_LOCATION)) ||
- (column == fieldIndex(ColumnCache::COLUMN_LIBRARYTABLE_ALBUMARTIST))) {
+ (column == fieldIndex(ColumnCache::COLUMN_LIBRARYTABLE_ALBUMARTIST)) ||
+ (column == fieldIndex(ColumnCache::COLUMN_LIBRARYTABLE_REPLAYGAIN))) {
return true;
}
return false;
@@ -582,9 +585,7 @@ QVariant BaseSqlTableModel::data(const QModelIndex& index, int role) const {
} else if (column == fieldIndex(ColumnCache::COLUMN_LIBRARYTABLE_BPM_LOCK)) {
value = value.toBool();
} else if (column == fieldIndex(ColumnCache::COLUMN_LIBRARYTABLE_YEAR)) {
- if (Qt::DisplayRole == role) {
- value = Mixxx::TrackMetadata::formatCalendarYear(value.toString());
- }
+ value = Mixxx::TrackMetadata::formatCalendarYear(value.toString());
} else if (column == fieldIndex(ColumnCache::COLUMN_LIBRARYTABLE_TRACKNUMBER)) {
int track_number = value.toInt();
if (track_number <= 0) {
@@ -613,8 +614,9 @@ QVariant BaseSqlTableModel::data(const QModelIndex& index, int role) const {
value = KeyUtils::keyToString(key);
}
}
- // Otherwise, just use the column value.
- }
+ } else if (column == fieldIndex(ColumnCache::COLUMN_LIBRARYTABLE_REPLAYGAIN)) {
+ value = Mixxx::ReplayGain::ratioToString(value.toDouble());
+ } // Otherwise, just use the column value.
break;
case Qt::EditRole:
@@ -722,7 +724,8 @@ Qt::ItemFlags BaseSqlTableModel::readWriteFlags(
column == fieldIndex(ColumnCache::COLUMN_LIBRARYTABLE_DURATION) ||
column == fieldIndex(ColumnCache::COLUMN_LIBRARYTABLE_BITRATE) ||
column == fieldIndex(ColumnCache::COLUMN_LIBRARYTABLE_DATETIMEADDED) ||
- column == fieldIndex(ColumnCache::COLUMN_LIBRARYTABLE_COVERART)) {
+ column == fieldIndex(ColumnCache::COLUMN_LIBRARYTABLE_COVERART) ||
+ column == fieldIndex(ColumnCache::COLUMN_LIBRARYTABLE_REPLAYGAIN)) {
return defaultFlags;
} else if (column == fieldIndex(ColumnCache::COLUMN_LIBRARYTABLE_TIMESPLAYED)) {
return defaultFlags | Qt::ItemIsUserCheckable;
diff --git a/src/library/basetrackcache.cpp b/src/library/basetrackcache.cpp
index e8e835e3b64..5742bd48ec1 100644
--- a/src/library/basetrackcache.cpp
+++ b/src/library/basetrackcache.cpp
@@ -307,6 +307,8 @@ void BaseTrackCache::getTrackValueForColumn(TrackPointer pTrack,
trackValue.setValue(pTrack->getBitrate());
} else if (fieldIndex(ColumnCache::COLUMN_LIBRARYTABLE_BPM) == column) {
trackValue.setValue(pTrack->getBpm());
+ } else if (fieldIndex(ColumnCache::COLUMN_LIBRARYTABLE_REPLAYGAIN) == column) {
+ trackValue.setValue(pTrack->getReplayGain().getRatio());
} else if (fieldIndex(ColumnCache::COLUMN_LIBRARYTABLE_PLAYED) == column) {
trackValue.setValue(pTrack->getPlayed());
} else if (fieldIndex(ColumnCache::COLUMN_LIBRARYTABLE_TIMESPLAYED) == column) {
@@ -589,7 +591,8 @@ int BaseTrackCache::compareColumnValues(int sortColumn, Qt::SortOrder sortOrder,
sortColumn == fieldIndex(ColumnCache::COLUMN_LIBRARYTABLE_BPM) ||
sortColumn == fieldIndex(ColumnCache::COLUMN_LIBRARYTABLE_DURATION) ||
sortColumn == fieldIndex(ColumnCache::COLUMN_LIBRARYTABLE_TIMESPLAYED) ||
- sortColumn == fieldIndex(ColumnCache::COLUMN_LIBRARYTABLE_RATING)) {
+ sortColumn == fieldIndex(ColumnCache::COLUMN_LIBRARYTABLE_RATING) ||
+ sortColumn == fieldIndex(ColumnCache::COLUMN_LIBRARYTABLE_REPLAYGAIN)) {
// Sort as floats.
double delta = val1.toDouble() - val2.toDouble();
diff --git a/src/library/browse/browsetablemodel.cpp b/src/library/browse/browsetablemodel.cpp
index 9956bc76c79..f58672f0a55 100644
--- a/src/library/browse/browsetablemodel.cpp
+++ b/src/library/browse/browsetablemodel.cpp
@@ -39,6 +39,7 @@ BrowseTableModel::BrowseTableModel(QObject* parent,
header_data.insert(COLUMN_KEY, tr("Key"));
header_data.insert(COLUMN_TYPE, tr("Type"));
header_data.insert(COLUMN_BITRATE, tr("Bitrate"));
+ header_data.insert(COLUMN_REPLAYGAIN, tr("Replay Gain"));
header_data.insert(COLUMN_LOCATION, tr("Location"));
header_data.insert(COLUMN_ALBUMARTIST, tr("Album Artist"));
header_data.insert(COLUMN_GROUPING, tr("Grouping"));
@@ -151,7 +152,8 @@ bool BrowseTableModel::isColumnHiddenByDefault(int column) {
column == COLUMN_GROUPING ||
column == COLUMN_LOCATION ||
column == COLUMN_ALBUMARTIST ||
- column == COLUMN_FILE_CREATION_TIME) {
+ column == COLUMN_FILE_CREATION_TIME ||
+ column == COLUMN_REPLAYGAIN) {
return true;
}
return false;
@@ -305,7 +307,8 @@ Qt::ItemFlags BrowseTableModel::flags(const QModelIndex &index) const {
column == COLUMN_DURATION ||
column == COLUMN_TYPE ||
column == COLUMN_FILE_MODIFIED_TIME ||
- column == COLUMN_FILE_CREATION_TIME) {
+ column == COLUMN_FILE_CREATION_TIME ||
+ column == COLUMN_REPLAYGAIN) {
return defaultFlags;
} else {
return defaultFlags | Qt::ItemIsEditable;
diff --git a/src/library/browse/browsetablemodel.h b/src/library/browse/browsetablemodel.h
index c06d5486579..9302a911f05 100644
--- a/src/library/browse/browsetablemodel.h
+++ b/src/library/browse/browsetablemodel.h
@@ -31,6 +31,7 @@ const int COLUMN_ALBUMARTIST = 16;
const int COLUMN_GROUPING = 17;
const int COLUMN_FILE_MODIFIED_TIME = 18;
const int COLUMN_FILE_CREATION_TIME = 19;
+const int COLUMN_REPLAYGAIN = 20;
// The BrowseTable models displays tracks
diff --git a/src/library/browse/browsethread.cpp b/src/library/browse/browsethread.cpp
index ee051728d55..0a00f3ea9f3 100644
--- a/src/library/browse/browsethread.cpp
+++ b/src/library/browse/browsethread.cpp
@@ -255,6 +255,13 @@ void BrowseThread::populateModel() {
item->setData(creationTime, Qt::UserRole);
row_data.insert(COLUMN_FILE_CREATION_TIME, item);
+ const Mixxx::ReplayGain replayGain(tio.getReplayGain());
+ item = new QStandardItem(
+ Mixxx::ReplayGain::ratioToString(replayGain.getRatio()));
+ item->setToolTip(item->text());
+ item->setData(item->text(), Qt::UserRole);
+ row_data.insert(COLUMN_REPLAYGAIN, item);
+
rows.append(row_data);
++row;
// If 10 tracks have been analyzed, send it to GUI
diff --git a/src/library/dao/trackdao.cpp b/src/library/dao/trackdao.cpp
index cc48b695d35..a88f6f3720e 100644
--- a/src/library/dao/trackdao.cpp
+++ b/src/library/dao/trackdao.cpp
@@ -402,7 +402,8 @@ void TrackDAO::bindTrackToLibraryInsert(TrackInfoObject* pTrack, int trackLocati
m_pQueryLibraryInsert->bindValue(":samplerate", pTrack->getSampleRate());
m_pQueryLibraryInsert->bindValue(":cuepoint", pTrack->getCuePoint());
m_pQueryLibraryInsert->bindValue(":bpm_lock", pTrack->hasBpmLock()? 1 : 0);
- m_pQueryLibraryInsert->bindValue(":replaygain", pTrack->getReplayGain());
+ m_pQueryLibraryInsert->bindValue(":replaygain", pTrack->getReplayGain().getRatio());
+ m_pQueryLibraryInsert->bindValue(":replaygain_peak", pTrack->getReplayGain().getPeak());
// We no longer store the wavesummary in the library table.
m_pQueryLibraryInsert->bindValue(":wavesummaryhex", QVariant(QVariant::ByteArray));
@@ -490,7 +491,7 @@ void TrackDAO::addTracksPrepare() {
m_pQueryLibraryInsert->prepare("INSERT INTO library "
"(artist, title, album, album_artist, year, genre, tracknumber, composer, "
"grouping, filetype, location, comment, url, duration, rating, key, key_id, "
- "bitrate, samplerate, cuepoint, bpm, replaygain, wavesummaryhex, "
+ "bitrate, samplerate, cuepoint, bpm, replaygain, replaygain_peak, wavesummaryhex, "
"timesplayed, channels, mixxx_deleted, header_parsed, "
"beats_version, beats_sub_version, beats, bpm_lock, "
"keys_version, keys_sub_version, keys, "
@@ -498,7 +499,7 @@ void TrackDAO::addTracksPrepare() {
"VALUES ("
":artist, :title, :album, :album_artist, :year, :genre, :tracknumber, :composer, :grouping, "
":filetype, :location, :comment, :url, :duration, :rating, :key, :key_id, "
- ":bitrate, :samplerate, :cuepoint, :bpm, :replaygain, :wavesummaryhex, "
+ ":bitrate, :samplerate, :cuepoint, :bpm, :replaygain, :replaygain_peak, :wavesummaryhex, "
":timesplayed, :channels, :mixxx_deleted, :header_parsed, "
":beats_version, :beats_sub_version, :beats, :bpm_lock, "
":keys_version, :keys_sub_version, :keys, "
@@ -1062,9 +1063,19 @@ bool setTrackCuePoint(const QSqlRecord& record, const int column,
return false;
}
-bool setTrackReplayGain(const QSqlRecord& record, const int column,
+bool setTrackReplayGainRatio(const QSqlRecord& record, const int column,
TrackPointer pTrack) {
- pTrack->setReplayGain(record.value(column).toDouble());
+ Mixxx::ReplayGain replayGain(pTrack->getReplayGain());
+ replayGain.setRatio(record.value(column).toDouble());
+ pTrack->setReplayGain(replayGain);
+ return false;
+}
+
+bool setTrackReplayGainPeak(const QSqlRecord& record, const int column,
+ TrackPointer pTrack) {
+ Mixxx::ReplayGain replayGain(pTrack->getReplayGain());
+ replayGain.setPeak(record.value(column).toDouble());
+ pTrack->setReplayGain(replayGain);
return false;
}
@@ -1194,7 +1205,8 @@ TrackPointer TrackDAO::getTrackFromDB(TrackId trackId) const {
{ "bitrate", setTrackBitrate },
{ "samplerate", setTrackSampleRate },
{ "cuepoint", setTrackCuePoint },
- { "replaygain", setTrackReplayGain },
+ { "replaygain", setTrackReplayGainRatio },
+ { "replaygain_peak", setTrackReplayGainPeak },
{ "channels", setTrackChannels },
{ "timesplayed", setTrackTimesPlayed },
{ "played", setTrackPlayed },
@@ -1428,7 +1440,7 @@ void TrackDAO::updateTrack(TrackInfoObject* pTrack) {
"duration=:duration, rating=:rating, "
"key=:key, key_id=:key_id, "
"bitrate=:bitrate, samplerate=:samplerate, cuepoint=:cuepoint, "
- "bpm=:bpm, replaygain=:replaygain, "
+ "bpm=:bpm, replaygain=:replaygain, replaygain_peak=:replaygain_peak, "
"timesplayed=:timesplayed, played=:played, "
"channels=:channels, header_parsed=:header_parsed, "
"beats_version=:beats_version, beats_sub_version=:beats_sub_version, beats=:beats, "
@@ -1454,7 +1466,8 @@ void TrackDAO::updateTrack(TrackInfoObject* pTrack) {
query.bindValue(":samplerate", pTrack->getSampleRate());
query.bindValue(":cuepoint", pTrack->getCuePoint());
- query.bindValue(":replaygain", pTrack->getReplayGain());
+ query.bindValue(":replaygain", pTrack->getReplayGain().getRatio());
+ query.bindValue(":replaygain_peak", pTrack->getReplayGain().getPeak());
query.bindValue(":rating", pTrack->getRating());
query.bindValue(":timesplayed", pTrack->getTimesPlayed());
query.bindValue(":played", pTrack->getPlayed() ? 1 : 0);
diff --git a/src/library/librarytablemodel.cpp b/src/library/librarytablemodel.cpp
index cef3bd5b93e..260210b8340 100644
--- a/src/library/librarytablemodel.cpp
+++ b/src/library/librarytablemodel.cpp
@@ -65,7 +65,6 @@ bool LibraryTableModel::isColumnInternal(int column) {
if ((column == fieldIndex(ColumnCache::COLUMN_LIBRARYTABLE_ID)) ||
(column == fieldIndex(ColumnCache::COLUMN_LIBRARYTABLE_URL)) ||
(column == fieldIndex(ColumnCache::COLUMN_LIBRARYTABLE_CUEPOINT)) ||
- (column == fieldIndex(ColumnCache::COLUMN_LIBRARYTABLE_REPLAYGAIN)) ||
(column == fieldIndex(ColumnCache::COLUMN_LIBRARYTABLE_WAVESUMMARYHEX)) ||
(column == fieldIndex(ColumnCache::COLUMN_LIBRARYTABLE_SAMPLERATE)) ||
(column == fieldIndex(ColumnCache::COLUMN_LIBRARYTABLE_MIXXXDELETED)) ||
diff --git a/src/library/mixxxlibraryfeature.cpp b/src/library/mixxxlibraryfeature.cpp
index 1fc22613f47..c299e1c9606 100644
--- a/src/library/mixxxlibraryfeature.cpp
+++ b/src/library/mixxxlibraryfeature.cpp
@@ -53,6 +53,7 @@ MixxxLibraryFeature::MixxxLibraryFeature(Library* pLibrary,
<< "library." + LIBRARYTABLE_BPM_LOCK
<< "library." + LIBRARYTABLE_DURATION
<< "library." + LIBRARYTABLE_BITRATE
+ << "library." + LIBRARYTABLE_REPLAYGAIN
<< "library." + LIBRARYTABLE_FILETYPE
<< "library." + LIBRARYTABLE_DATETIMEADDED
<< "track_locations.location"
diff --git a/src/library/trackcollection.cpp b/src/library/trackcollection.cpp
index 9a368573f81..f8563bc4f1a 100644
--- a/src/library/trackcollection.cpp
+++ b/src/library/trackcollection.cpp
@@ -14,7 +14,7 @@
#include "util/assert.h"
// static
-const int TrackCollection::kRequiredSchemaVersion = 24;
+const int TrackCollection::kRequiredSchemaVersion = 25;
TrackCollection::TrackCollection(ConfigObject* pConfig)
: m_pConfig(pConfig),
diff --git a/src/metadata/trackmetadatataglib.cpp b/src/metadata/trackmetadatataglib.cpp
index a8232ba607d..3713d13277d 100644
--- a/src/metadata/trackmetadatataglib.cpp
+++ b/src/metadata/trackmetadatataglib.cpp
@@ -193,29 +193,49 @@ inline bool parseBpm(TrackMetadata* pTrackMetadata, QString sBpm) {
inline QString formatTrackGain(const TrackMetadata& trackMetadata) {
const double trackGainRatio(trackMetadata.getReplayGain().getRatio());
- return ReplayGain::formatRatio2Gain(trackGainRatio);
+ return ReplayGain::ratioToString(trackGainRatio);
}
-bool parseTrackGain(TrackMetadata* pTrackMetadata, QString dbGain) {
+void parseTrackGain(
+ TrackMetadata* pTrackMetadata,
+ const QString& dbGain) {
DEBUG_ASSERT(pTrackMetadata);
- bool trackGainRatioValid = false;
- double trackGainRatio = ReplayGain::parseGain2Ratio(dbGain, &trackGainRatioValid);
- if (trackGainRatioValid) {
+ bool isRatioValid = false;
+ double ratio = ReplayGain::ratioFromString(dbGain, &isRatioValid);
+ if (isRatioValid) {
// Some applications (e.g. Rapid Evolution 3) write a replay gain
// of 0 dB even if the replay gain is undefined. To be safe we
// ignore this special value and instead prefer to recalculate
// the replay gain.
- if (trackGainRatio == ReplayGain::kRatio0dB) {
+ if (ratio == ReplayGain::kRatio0dB) {
// special case
- qDebug() << "Ignoring possibly undefined gain:" << ReplayGain::formatRatio2Gain(trackGainRatio);
- trackGainRatio = ReplayGain::kRatioUndefined;
+ qDebug() << "Ignoring possibly undefined gain:" << dbGain;
+ ratio = ReplayGain::kRatioUndefined;
}
- ReplayGain trackGain(pTrackMetadata->getReplayGain());
- trackGain.setRatio(trackGainRatio);
- pTrackMetadata->setReplayGain(trackGain);
+ ReplayGain replayGain(pTrackMetadata->getReplayGain());
+ replayGain.setRatio(ratio);
+ pTrackMetadata->setReplayGain(replayGain);
+ }
+}
+
+inline QString formatTrackPeak(const TrackMetadata& trackMetadata) {
+ const CSAMPLE trackGainPeak(trackMetadata.getReplayGain().getPeak());
+ return ReplayGain::peakToString(trackGainPeak);
+}
+
+void parseTrackPeak(
+ TrackMetadata* pTrackMetadata,
+ const QString& strPeak) {
+ DEBUG_ASSERT(pTrackMetadata);
+
+ bool isPeakValid = false;
+ const CSAMPLE peak = ReplayGain::peakFromString(strPeak, &isPeakValid);
+ if (isPeakValid) {
+ ReplayGain replayGain(pTrackMetadata->getReplayGain());
+ replayGain.setPeak(peak);
+ pTrackMetadata->setReplayGain(replayGain);
}
- return trackGainRatioValid;
}
void readAudioProperties(TrackMetadata* pTrackMetadata,
@@ -651,12 +671,19 @@ void readTrackMetadataFromID3v2Tag(TrackMetadata* pTrackMetadata,
}
// Only read track gain (not album gain)
- TagLib::ID3v2::UserTextIdentificationFrame* pReplayGainFrame =
+ TagLib::ID3v2::UserTextIdentificationFrame* pTrackGainFrame =
findFirstUserTextIdentificationFrame(tag, "REPLAYGAIN_TRACK_GAIN");
- if (pReplayGainFrame && (2 <= pReplayGainFrame->fieldList().size())) {
+ if (pTrackGainFrame && (2 <= pTrackGainFrame->fieldList().size())) {
// The value is stored in the 2nd field
parseTrackGain(pTrackMetadata,
- toQString(pReplayGainFrame->fieldList()[1]));
+ toQString(pTrackGainFrame->fieldList()[1]));
+ }
+ TagLib::ID3v2::UserTextIdentificationFrame* pTrackPeakFrame =
+ findFirstUserTextIdentificationFrame(tag, "REPLAYGAIN_TRACK_PEAK");
+ if (pTrackPeakFrame && (2 <= pTrackPeakFrame->fieldList().size())) {
+ // The value is stored in the 2nd field
+ parseTrackPeak(pTrackMetadata,
+ toQString(pTrackPeakFrame->fieldList()[1]));
}
}
@@ -697,6 +724,10 @@ void readTrackMetadataFromAPETag(TrackMetadata* pTrackMetadata, const TagLib::AP
parseTrackGain(pTrackMetadata,
toQString(tag.itemListMap()["REPLAYGAIN_TRACK_GAIN"]));
}
+ if (tag.itemListMap().contains("REPLAYGAIN_TRACK_PEAK")) {
+ parseTrackPeak(pTrackMetadata,
+ toQString(tag.itemListMap()["REPLAYGAIN_TRACK_PEAK"]));
+ }
}
void readTrackMetadataFromXiphComment(TrackMetadata* pTrackMetadata,
@@ -766,6 +797,10 @@ void readTrackMetadataFromXiphComment(TrackMetadata* pTrackMetadata,
parseTrackGain(pTrackMetadata,
toQStringFirstNotEmpty(tag.fieldListMap()["REPLAYGAIN_TRACK_GAIN"]));
}
+ if (tag.fieldListMap().contains("REPLAYGAIN_TRACK_PEAK")) {
+ parseTrackPeak(pTrackMetadata,
+ toQStringFirstNotEmpty(tag.fieldListMap()["REPLAYGAIN_TRACK_PEAK"]));
+ }
/*
* Reading key code information
@@ -843,6 +878,11 @@ void readTrackMetadataFromMP4Tag(TrackMetadata* pTrackMetadata, const TagLib::MP
parseTrackGain(pTrackMetadata,
toQStringFirstNotEmpty(getItemListMap(tag)["----:com.apple.iTunes:replaygain_track_gain"]));
}
+ if (getItemListMap(tag).contains(
+ "----:com.apple.iTunes:replaygain_track_peak")) {
+ parseTrackPeak(pTrackMetadata,
+ toQStringFirstNotEmpty(getItemListMap(tag)["----:com.apple.iTunes:replaygain_track_peak"]));
+ }
// Read musical key (conforms to Rapid Evolution)
if (getItemListMap(tag).contains("----:com.apple.iTunes:KEY")) {
@@ -916,6 +956,11 @@ bool writeTrackMetadataIntoID3v2Tag(TagLib::ID3v2::Tag* pTag,
formatTrackGain(trackMetadata),
"REPLAYGAIN_TRACK_GAIN",
true);
+ writeID3v2UserTextIdentificationFrame(
+ pTag,
+ formatTrackPeak(trackMetadata),
+ "REPLAYGAIN_TRACK_PEAK",
+ true);
return true;
}
@@ -939,6 +984,8 @@ bool writeTrackMetadataIntoAPETag(TagLib::APE::Tag* pTag, const TrackMetadata& t
toTagLibString(TrackMetadata::formatBpm(trackMetadata.getBpm())), true);
pTag->addValue("REPLAYGAIN_TRACK_GAIN",
toTagLibString(formatTrackGain(trackMetadata)), true);
+ pTag->addValue("REPLAYGAIN_TRACK_PEAK",
+ toTagLibString(formatTrackPeak(trackMetadata)), true);
return true;
}
@@ -983,6 +1030,9 @@ bool writeTrackMetadataIntoXiphComment(TagLib::Ogg::XiphComment* pTag,
pTag->removeField("REPLAYGAIN_TRACK_GAIN");
pTag->addField("REPLAYGAIN_TRACK_GAIN",
toTagLibString(formatTrackGain(trackMetadata)));
+ pTag->removeField("REPLAYGAIN_TRACK_PEAK");
+ pTag->addField("REPLAYGAIN_TRACK_PEAK",
+ toTagLibString(formatTrackPeak(trackMetadata)));
return true;
}
@@ -1007,6 +1057,8 @@ bool writeTrackMetadataIntoMP4Tag(TagLib::MP4::Tag* pTag, const TrackMetadata& t
TrackMetadata::formatBpm(trackMetadata.getBpm()));
writeMP4Atom(pTag, "----:com.apple.iTunes:replaygain_track_gain",
formatTrackGain(trackMetadata));
+ writeMP4Atom(pTag, "----:com.apple.iTunes:replaygain_track_peak",
+ formatTrackPeak(trackMetadata));
writeMP4Atom(pTag, "----:com.apple.iTunes:initialkey",
trackMetadata.getKey());
writeMP4Atom(pTag, "----:com.apple.iTunes:KEY",
diff --git a/src/sources/soundsourceopus.cpp b/src/sources/soundsourceopus.cpp
index 192fd646b23..afb728f1982 100644
--- a/src/sources/soundsourceopus.cpp
+++ b/src/sources/soundsourceopus.cpp
@@ -117,7 +117,7 @@ Result SoundSourceOpus::parseTrackMetadataAndCoverArt(
pTrackMetadata->setTitle(l_SPayload);
} else if (!l_STag.compare("REPLAYGAIN_TRACK_GAIN")) {
bool trackGainRatioValid = false;
- double trackGainRatio = ReplayGain::parseGain2Ratio(l_SPayload, &trackGainRatioValid);
+ double trackGainRatio = ReplayGain::ratioFromString(l_SPayload, &trackGainRatioValid);
if (trackGainRatioValid) {
ReplayGain trackGain(pTrackMetadata->getReplayGain());
trackGain.setRatio(trackGainRatio);
diff --git a/src/test/replaygaintest.cpp b/src/test/replaygaintest.cpp
index 92c57416a07..354c7d6dc91 100644
--- a/src/test/replaygaintest.cpp
+++ b/src/test/replaygaintest.cpp
@@ -18,11 +18,11 @@ class ReplayGainTest : public testing::Test {
virtual void TearDown() {
}
- double parseGain2Ratio(QString inputValue, bool expectedResult, float expectedValue) {
- //qDebug() << "parseGain2Ratio" << inputValue << expectedResult << expectedValue;
+ double ratioFromString(QString inputValue, bool expectedResult, double expectedValue) {
+ //qDebug() << "ratioFromString" << inputValue << expectedResult << expectedValue;
bool actualResult;
- const double actualValue = Mixxx::ReplayGain::parseGain2Ratio(inputValue, &actualResult);
+ const double actualValue = Mixxx::ReplayGain::ratioFromString(inputValue, &actualResult);
EXPECT_EQ(expectedResult, actualResult);
EXPECT_FLOAT_EQ(expectedValue, actualValue);
@@ -32,11 +32,44 @@ class ReplayGainTest : public testing::Test {
void normalizeRatio(double expectedResult) {
const double actualResult = Mixxx::ReplayGain::normalizeRatio(expectedResult);
+ if (Mixxx::ReplayGain::isValidRatio(expectedResult)) {
+ EXPECT_EQ(expectedResult, actualResult);
+ } else {
+ EXPECT_EQ(Mixxx::ReplayGain::kRatioUndefined, actualResult);
+ }
+ }
+
+ CSAMPLE peakFromString(QString inputValue, bool expectedResult, CSAMPLE expectedValue) {
+ //qDebug() << "peakFromString" << inputValue << expectedResult << expectedValue;
+
+ bool actualResult;
+ const CSAMPLE actualValue = Mixxx::ReplayGain::peakFromString(inputValue, &actualResult);
+
EXPECT_EQ(expectedResult, actualResult);
+ EXPECT_FLOAT_EQ(expectedValue, actualValue);
+
+ return actualResult;
+ }
+
+ void normalizePeak(CSAMPLE expectedResult) {
+ const CSAMPLE actualResult = Mixxx::ReplayGain::normalizePeak(expectedResult);
+ if (Mixxx::ReplayGain::isValidPeak(expectedResult)) {
+ EXPECT_EQ(expectedResult, actualResult);
+ } else {
+ EXPECT_EQ(Mixxx::ReplayGain::kPeakUndefined, actualResult);
+ }
}
};
-TEST_F(ReplayGainTest, ParseReplayGainDbValidRange) {
+TEST_F(ReplayGainTest, RatioFromString0dB) {
+ ratioFromString("0 dB", true, Mixxx::ReplayGain::kRatio0dB);
+ ratioFromString("0.0dB", true, Mixxx::ReplayGain::kRatio0dB);
+ ratioFromString("0 DB", true, Mixxx::ReplayGain::kRatio0dB);
+ ratioFromString("-0 Db", true, Mixxx::ReplayGain::kRatio0dB);
+ ratioFromString("+0db", true, Mixxx::ReplayGain::kRatio0dB);
+}
+
+TEST_F(ReplayGainTest, RatioFromStringValidRange) {
for (int replayGainDb = -100; 100 >= replayGainDb; ++replayGainDb) {
const QString inputValues[] = {
QString("%1 ").arg(replayGainDb),
@@ -47,21 +80,25 @@ TEST_F(ReplayGainTest, ParseReplayGainDbValidRange) {
float expectedValue;
expectedValue = db2ratio(double(replayGainDb));
for (size_t i = 0; i < sizeof(inputValues) / sizeof(inputValues[0]); ++i) {
- parseGain2Ratio(inputValues[i], true, expectedValue);
+ ratioFromString(inputValues[i], true, expectedValue);
if (0 <= replayGainDb) {
- parseGain2Ratio(QString(" + ") + inputValues[i], true, expectedValue);
+ ratioFromString(QString(" + ") + inputValues[i], true, expectedValue);
}
}
}
}
-TEST_F(ReplayGainTest, ParseReplayGainDbInvalid) {
- parseGain2Ratio("", false, Mixxx::ReplayGain::kRatioUndefined);
- parseGain2Ratio("abcde", false, Mixxx::ReplayGain::kRatioUndefined);
- parseGain2Ratio("0 dBA", false, Mixxx::ReplayGain::kRatioUndefined);
+TEST_F(ReplayGainTest, RatioFromStringInvalid) {
+ ratioFromString("", false, Mixxx::ReplayGain::kRatioUndefined);
+ ratioFromString("abcde", false, Mixxx::ReplayGain::kRatioUndefined);
+ ratioFromString("0 dBA", false, Mixxx::ReplayGain::kRatioUndefined);
+ ratioFromString("--2 dB", false, Mixxx::ReplayGain::kRatioUndefined);
+ ratioFromString("+-2 dB", false, Mixxx::ReplayGain::kRatioUndefined);
+ ratioFromString("-+2 dB", false, Mixxx::ReplayGain::kRatioUndefined);
+ ratioFromString("++2 dB", false, Mixxx::ReplayGain::kRatioUndefined);
}
-TEST_F(ReplayGainTest, NormalizeReplayGain) {
+TEST_F(ReplayGainTest, NormalizeRatio) {
normalizeRatio(Mixxx::ReplayGain::kRatioUndefined);
normalizeRatio(Mixxx::ReplayGain::kRatioMin);
normalizeRatio(-Mixxx::ReplayGain::kRatioMin);
@@ -69,4 +106,39 @@ TEST_F(ReplayGainTest, NormalizeReplayGain) {
normalizeRatio(-Mixxx::ReplayGain::kRatio0dB);
}
+TEST_F(ReplayGainTest, PeakFromStringValid) {
+ peakFromString("0", true, Mixxx::ReplayGain::kPeakMin);
+ peakFromString("+0", true, Mixxx::ReplayGain::kPeakMin);
+ peakFromString("-0", true, Mixxx::ReplayGain::kPeakMin);
+ peakFromString("0.0", true, Mixxx::ReplayGain::kPeakMin);
+ peakFromString("+0.0", true, Mixxx::ReplayGain::kPeakMin);
+ peakFromString("-0.0", true, Mixxx::ReplayGain::kPeakMin);
+ peakFromString("1", true, Mixxx::ReplayGain::kPeakClip);
+ peakFromString("+1", true, Mixxx::ReplayGain::kPeakClip);
+ peakFromString("1.0", true, Mixxx::ReplayGain::kPeakClip);
+ peakFromString("+1.0", true, Mixxx::ReplayGain::kPeakClip);
+ peakFromString(" 0.12345 ", true, 0.12345);
+ peakFromString(" 1.2345", true, 1.2345);
+}
+
+TEST_F(ReplayGainTest, PeakFromStringInvalid) {
+ peakFromString("", false, Mixxx::ReplayGain::kPeakUndefined);
+ peakFromString("-1", false, Mixxx::ReplayGain::kPeakUndefined);
+ peakFromString("-0.12345", false, Mixxx::ReplayGain::kPeakUndefined);
+ peakFromString("--1.0", false, Mixxx::ReplayGain::kPeakUndefined);
+ peakFromString("+-1.0", false, Mixxx::ReplayGain::kPeakUndefined);
+ peakFromString("-+1.0", false, Mixxx::ReplayGain::kPeakUndefined);
+ peakFromString("++1.0", false, Mixxx::ReplayGain::kPeakUndefined);
+ peakFromString("+abcde", false, Mixxx::ReplayGain::kPeakUndefined);
+}
+
+TEST_F(ReplayGainTest, NormalizePeak) {
+ normalizePeak(Mixxx::ReplayGain::kPeakUndefined);
+ normalizePeak(Mixxx::ReplayGain::kPeakMin);
+ normalizePeak(-Mixxx::ReplayGain::kPeakMin);
+ normalizePeak(Mixxx::ReplayGain::kPeakClip);
+ normalizePeak(-Mixxx::ReplayGain::kPeakClip);
+ normalizePeak(Mixxx::ReplayGain::kPeakClip + Mixxx::ReplayGain::kPeakClip);
+}
+
} // anonymous namespace
diff --git a/src/trackinfoobject.cpp b/src/trackinfoobject.cpp
index 96c71207393..2e1b084d85c 100644
--- a/src/trackinfoobject.cpp
+++ b/src/trackinfoobject.cpp
@@ -39,7 +39,6 @@ TrackInfoObject::TrackInfoObject(const QFileInfo& fileInfo,
m_iDuration = 0;
m_iBitrate = 0;
m_iTimesPlayed = 0;
- m_fReplayGain = 0.;
m_iSampleRate = 0;
m_iChannels = 0;
m_fCuePoint = 0.0f;
@@ -112,10 +111,7 @@ void TrackInfoObject::setMetadata(const Mixxx::TrackMetadata& trackMetadata) {
setSampleRate(trackMetadata.getSampleRate());
setDuration(trackMetadata.getDuration());
setBitrate(trackMetadata.getBitrate());
-
- if (trackMetadata.getReplayGain().hasRatio()) {
- setReplayGain(trackMetadata.getReplayGain().getRatio());
- }
+ setReplayGain(trackMetadata.getReplayGain());
// Need to set BPM after sample rate since beat grid creation depends on
// knowing the sample rate. Bug #1020438.
@@ -147,9 +143,7 @@ void TrackInfoObject::getMetadata(Mixxx::TrackMetadata* pTrackMetadata) {
pTrackMetadata->setSampleRate(getSampleRate());
pTrackMetadata->setDuration(getDuration());
pTrackMetadata->setBitrate(getBitrate());
- Mixxx::ReplayGain trackGain(pTrackMetadata->getReplayGain());
- trackGain.setRatio(getReplayGain());
- pTrackMetadata->setReplayGain(trackGain);
+ pTrackMetadata->setReplayGain(getReplayGain());
pTrackMetadata->setBpm(getBpm());
pTrackMetadata->setKey(getKeyText());
}
@@ -291,20 +285,20 @@ bool TrackInfoObject::exists() const {
return QFile::exists(m_fileInfo.absoluteFilePath());
}
-float TrackInfoObject::getReplayGain() const {
+Mixxx::ReplayGain TrackInfoObject::getReplayGain() const {
QMutexLocker lock(&m_qMutex);
- return m_fReplayGain;
+ return m_replayGain;
}
-void TrackInfoObject::setReplayGain(float f) {
+void TrackInfoObject::setReplayGain(const Mixxx::ReplayGain& replayGain) {
QMutexLocker lock(&m_qMutex);
//qDebug() << "Reported ReplayGain value: " << m_fReplayGain;
- if (m_fReplayGain != f) {
- m_fReplayGain = f;
+ if (m_replayGain != replayGain) {
+ m_replayGain = replayGain;
setDirty(true);
}
lock.unlock();
- emit(ReplayGainUpdated(f));
+ emit(ReplayGainUpdated(replayGain));
}
double TrackInfoObject::getBpm() const {
diff --git a/src/trackinfoobject.h b/src/trackinfoobject.h
index ab81a47927e..d164c5b70e7 100644
--- a/src/trackinfoobject.h
+++ b/src/trackinfoobject.h
@@ -17,6 +17,7 @@
#include "track/beats.h"
#include "track/keys.h"
#include "track/trackid.h"
+#include "util/replaygain.h"
#include "util/sandbox.h"
#include "waveform/waveform.h"
@@ -99,9 +100,9 @@ class TrackInfoObject : public QObject {
bool exists() const;
// Returns ReplayGain
- float getReplayGain() const;
+ Mixxx::ReplayGain getReplayGain() const;
// Set ReplayGain
- void setReplayGain(float);
+ void setReplayGain(const Mixxx::ReplayGain&);
// Returns BPM
double getBpm() const;
// Set BPM
@@ -269,7 +270,7 @@ class TrackInfoObject : public QObject {
void beatsUpdated();
void keyUpdated(double key);
void keysUpdated();
- void ReplayGainUpdated(double replaygain);
+ void ReplayGainUpdated(Mixxx::ReplayGain replayGain);
void cuesUpdated();
void changed(TrackInfoObject* pTrack);
void dirty(TrackInfoObject* pTrack);
@@ -346,7 +347,7 @@ class TrackInfoObject : public QObject {
// Number of times the track has been played
int m_iTimesPlayed;
// Replay Gain volume
- float m_fReplayGain;
+ Mixxx::ReplayGain m_replayGain;
// Has this track been played this sessions?
bool m_bPlayed;
// True if header was parsed
diff --git a/src/util/replaygain.cpp b/src/util/replaygain.cpp
index 8ec486eecfa..47af56a9264 100644
--- a/src/util/replaygain.cpp
+++ b/src/util/replaygain.cpp
@@ -4,37 +4,75 @@
namespace Mixxx {
-/*static*/ const double ReplayGain::kRatioUndefined = 0.0;
-/*static*/ const double ReplayGain::kRatioMin = 0.0; // lower bound (inclusive)
-/*static*/ const double ReplayGain::kRatio0dB = 1.0;
+/*static*/ constexpr double ReplayGain::kRatioUndefined;
+/*static*/ constexpr double ReplayGain::kRatioMin;
+/*static*/ constexpr double ReplayGain::kRatio0dB;
+
+/*static*/ constexpr CSAMPLE ReplayGain::kPeakUndefined;
+/*static*/ constexpr CSAMPLE ReplayGain::kPeakMin;
+/*static*/ constexpr CSAMPLE ReplayGain::kPeakClip;
namespace {
const QString kGainUnit("dB");
const QString kGainSuffix(" " + kGainUnit);
+QString stripLeadingSign(const QString& trimmed, char sign) {
+ const int signIndex = trimmed.indexOf(sign);
+ if (0 == signIndex) {
+ return trimmed.mid(signIndex + 1).trimmed();
+ } else {
+ return trimmed;
+ }
+}
+
+QString normalizeNumberString(const QString& number, bool* pValid) {
+ if (pValid) {
+ *pValid = false;
+ }
+ const QString trimmed(number.trimmed());
+ QString normalized(stripLeadingSign(trimmed, '+'));
+ if (normalized == trimmed) {
+ // no leading '+' sign found
+ if (pValid) {
+ *pValid = true;
+ }
+ return normalized;
+ } else {
+ // stripped leading '+' sign -> no more leading signs '+'/'-' allowed
+ if ((normalized == stripLeadingSign(normalized, '+')) &&
+ (normalized == stripLeadingSign(normalized, '-'))) {
+ if (pValid) {
+ *pValid = true;
+ }
+ return normalized;
+ }
+ }
+ // normalization failed
+ return number;
+}
+
} // anonymous namespace
-double ReplayGain::parseGain2Ratio(QString dbGain, bool* pValid) {
+double ReplayGain::ratioFromString(QString dbGain, bool* pValid) {
if (pValid) {
*pValid = false;
}
- QString trimmedGain(dbGain.trimmed());
- const int plusIndex = trimmedGain.indexOf('+');
- if (0 == plusIndex) {
- // strip leading "+"
- trimmedGain = trimmedGain.mid(plusIndex + 1).trimmed();
+ bool isValid = false;
+ QString normalizedGain(normalizeNumberString(dbGain, &isValid));
+ if (!isValid) {
+ return kRatioUndefined;
}
- const int unitIndex = trimmedGain.lastIndexOf(kGainUnit, -1, Qt::CaseInsensitive);
- if ((0 <= unitIndex) && ((trimmedGain.length() - 2) == unitIndex)) {
+ const int unitIndex = normalizedGain.lastIndexOf(kGainUnit, -1, Qt::CaseInsensitive);
+ if ((0 <= unitIndex) && ((normalizedGain.length() - 2) == unitIndex)) {
// strip trailing unit suffix
- trimmedGain = trimmedGain.left(unitIndex).trimmed();
+ normalizedGain = normalizedGain.left(unitIndex).trimmed();
}
- if (trimmedGain.isEmpty()) {
+ if (normalizedGain.isEmpty()) {
return kRatioUndefined;
}
- bool isValid = false;
- const double replayGainDb = trimmedGain.toDouble(&isValid);
+ isValid = false;
+ const double replayGainDb = normalizedGain.toDouble(&isValid);
if (isValid) {
const double ratio = db2ratio(replayGainDb);
DEBUG_ASSERT(kRatioUndefined != ratio);
@@ -52,7 +90,7 @@ double ReplayGain::parseGain2Ratio(QString dbGain, bool* pValid) {
return kRatioUndefined;
}
-QString ReplayGain::formatRatio2Gain(double ratio) {
+QString ReplayGain::ratioToString(double ratio) {
if (isValidRatio(ratio)) {
return QString::number(ratio2db(ratio)) + kGainSuffix;
} else {
@@ -62,13 +100,59 @@ QString ReplayGain::formatRatio2Gain(double ratio) {
double ReplayGain::normalizeRatio(double ratio) {
if (isValidRatio(ratio)) {
- const double normalizedRatio = parseGain2Ratio(formatRatio2Gain(ratio));
+ const double normalizedRatio = ratioFromString(ratioToString(ratio));
// NOTE(uklotzde): Subsequently formatting and parsing the
// normalized value should not alter it anymore!
- DEBUG_ASSERT(normalizedRatio == parseGain2Ratio(formatRatio2Gain(normalizedRatio)));
+ DEBUG_ASSERT(normalizedRatio == ratioFromString(ratioToString(normalizedRatio)));
return normalizedRatio;
} else {
- return ratio;
+ return kRatioUndefined;
+ }
+}
+
+CSAMPLE ReplayGain::peakFromString(QString strPeak, bool* pValid) {
+ if (pValid) {
+ *pValid = false;
+ }
+ bool isValid = false;
+ QString normalizedPeak(normalizeNumberString(strPeak, &isValid));
+ if (!isValid || normalizedPeak.isEmpty()) {
+ return kPeakUndefined;
+ }
+ isValid = false;
+ const CSAMPLE peak = normalizedPeak.toDouble(&isValid);
+ if (isValid) {
+ if (isValidPeak(peak)) {
+ if (pValid) {
+ *pValid = true;
+ }
+ return peak;
+ } else {
+ qDebug() << "ReplayGain: Invalid peak value:" << strPeak << " -> "<< peak;
+ }
+ } else {
+ qDebug() << "ReplayGain: Failed to parse peak:" << strPeak;
+ }
+ return kPeakUndefined;
+}
+
+QString ReplayGain::peakToString(CSAMPLE peak) {
+ if (isValidPeak(peak)) {
+ return QString::number(peak);
+ } else {
+ return QString();
+ }
+}
+
+CSAMPLE ReplayGain::normalizePeak(CSAMPLE peak) {
+ if (isValidPeak(peak)) {
+ const CSAMPLE normalizedPeak = peakFromString(peakToString(peak));
+ // NOTE(uklotzde): Subsequently formatting and parsing the
+ // normalized value should not alter it anymore!
+ DEBUG_ASSERT(normalizedPeak == peakFromString(peakToString(normalizedPeak)));
+ return normalizedPeak;
+ } else {
+ return kPeakUndefined;
}
}
diff --git a/src/util/replaygain.h b/src/util/replaygain.h
index 09f2c77b24d..f91cb2dbac6 100644
--- a/src/util/replaygain.h
+++ b/src/util/replaygain.h
@@ -5,16 +5,43 @@
namespace Mixxx {
-// DTO for replay gain. Must not be subclassed (no virtual destructor)!
-class ReplayGain {
+// DTO for storing replay gain information.
+//
+// Parsing & Formatting
+// --------------------
+// This class includes functions for formatting and parsing replay gain
+// metadata according to the ReplayGain 1.0/2.0 specification:
+// http://wiki.hydrogenaud.io/index.php?title=ReplayGain_1.0_specification
+// http://wiki.hydrogenaud.io/index.php?title=ReplayGain_2.0_specification
+//
+// Normalization
+// -------------
+// Formatting a floating point value as a string and parsing it later
+// might cause rounding errors. In order to avoid "jittering" caused
+// by subsequently formatting and parsing a floating point value the
+// ratio and peak values need to be normalized before writing them
+// as a string into file tags.
+class ReplayGain final {
public:
- static const double kRatioUndefined;
- static const double kRatioMin; // lower bound (exclusive)
- static const double kRatio0dB;
+ static constexpr double kRatioUndefined = 0.0;
+ static constexpr double kRatioMin = 0.0; // lower bound (exclusive)
+ static constexpr double kRatio0dB = 1.0;
+
+ static constexpr CSAMPLE kPeakUndefined = -CSAMPLE_PEAK;
+ static constexpr CSAMPLE kPeakMin = CSAMPLE_ZERO; // lower bound (inclusive)
+ static constexpr CSAMPLE kPeakClip = CSAMPLE_PEAK; // upper bound (inclusive) represents digital full scale without clipping
+
+ static_assert(ReplayGain::kPeakClip == 1.0,
+ "http://wiki.hydrogenaud.io/index.php?title=ReplayGain_2.0_specification#Peak_amplitude: "
+ "The maximum peak amplitude value is stored as a floating number, "
+ "where 1.0 represents digital full scale");
ReplayGain()
- : m_ratio(kRatioUndefined)
- , m_peak(CSAMPLE_PEAK) {
+ : ReplayGain(kRatioUndefined, kPeakUndefined) {
+ }
+ ReplayGain(double ratio, CSAMPLE peak)
+ : m_ratio(ratio)
+ , m_peak(peak) {
}
static bool isValidRatio(double ratio) {
@@ -33,21 +60,22 @@ class ReplayGain {
m_ratio = kRatioUndefined;
}
- // Parse and format replay gain metadata according to the ReplayGain
- // 1.0/2.0 specification.
- // http://wiki.hydrogenaud.io/index.php?title=ReplayGain_1.0_specification
- // http://wiki.hydrogenaud.io/index.php?title=ReplayGain_2.0_specification
- static double parseGain2Ratio(QString dBGain, bool* pValid = 0);
- static QString formatRatio2Gain(double ratio);
+ // Parsing and formatting of gain values according to the
+ // ReplayGain 1.0/2.0 specification.
+ static double ratioFromString(QString dBGain, bool* pValid = 0);
+ static QString ratioToString(double ratio);
- // After normalization formatting and parsing the ratio repeatedly will
- // always lead to the same value. This is required to reliably store the
- // dB gain as a string in track metadata.
static double normalizeRatio(double ratio);
// The peak amplitude of the track or signal.
+ static bool isValidPeak(CSAMPLE peak) {
+ return kPeakMin <= peak;
+ }
+ bool hasPeak() const {
+ return isValidPeak(m_peak);
+ }
CSAMPLE getPeak() const {
- return m_ratio;
+ return m_peak;
}
void setPeak(CSAMPLE peak) {
m_peak = peak;
@@ -56,6 +84,13 @@ class ReplayGain {
m_peak = CSAMPLE_PEAK;
}
+ // Parsing and formatting of peak amplitude values according to
+ // the ReplayGain 1.0/2.0 specification.
+ static CSAMPLE peakFromString(QString strPeak, bool* pValid = 0);
+ static QString peakToString(CSAMPLE peak);
+
+ static CSAMPLE normalizePeak(CSAMPLE peak);
+
private:
double m_ratio;
CSAMPLE m_peak;
@@ -73,4 +108,6 @@ bool operator!=(const ReplayGain& lhs, const ReplayGain& rhs) {
}
+Q_DECLARE_METATYPE(Mixxx::ReplayGain)
+
#endif // MIXXX_REPLAYGAIN_H
diff --git a/src/util/types.h b/src/util/types.h
index 3a27c1e6f17..d4d3ae0b12d 100644
--- a/src/util/types.h
+++ b/src/util/types.h
@@ -16,9 +16,9 @@ typedef std::ptrdiff_t SINT;
// 16-bit integer sample data within the asymmetric
// range [SHRT_MIN, SHRT_MAX].
typedef short int SAMPLE;
-const SAMPLE SAMPLE_ZERO = 0;
-const SAMPLE SAMPLE_MIN = SHRT_MIN;
-const SAMPLE SAMPLE_MAX = SHRT_MAX;
+constexpr SAMPLE SAMPLE_ZERO = 0;
+constexpr SAMPLE SAMPLE_MIN = SHRT_MIN;
+constexpr SAMPLE SAMPLE_MAX = SHRT_MAX;
// Limits the range of a SAMPLE value to [SAMPLE_MIN, SAMPLE_MAX].
inline
@@ -38,9 +38,9 @@ SAMPLE SAMPLE_clampSymmetric(SAMPLE in) {
// emphasize the symmetric value range of CSAMPLE
// data!
typedef float CSAMPLE;
-const CSAMPLE CSAMPLE_ZERO = 0.0f;
-const CSAMPLE CSAMPLE_ONE = 1.0f;
-const CSAMPLE CSAMPLE_PEAK = CSAMPLE_ONE;
+constexpr CSAMPLE CSAMPLE_ZERO = 0.0f;
+constexpr CSAMPLE CSAMPLE_ONE = 1.0f;
+constexpr CSAMPLE CSAMPLE_PEAK = CSAMPLE_ONE;
// Limits the range of a CSAMPLE value to [-CSAMPLE_PEAK, CSAMPLE_PEAK].
inline
@@ -52,10 +52,10 @@ CSAMPLE CSAMPLE_clamp(CSAMPLE in) {
// data in the range [0.0, 1.0]. Same data type as
// CSAMPLE to avoid type conversions in calculations.
typedef CSAMPLE CSAMPLE_GAIN;
-const float CSAMPLE_GAIN_ZERO = CSAMPLE_ZERO;
-const float CSAMPLE_GAIN_ONE = CSAMPLE_ONE;
-const float CSAMPLE_GAIN_MIN = CSAMPLE_GAIN_ZERO;
-const float CSAMPLE_GAIN_MAX = CSAMPLE_GAIN_ONE;
+constexpr float CSAMPLE_GAIN_ZERO = CSAMPLE_ZERO;
+constexpr float CSAMPLE_GAIN_ONE = CSAMPLE_ONE;
+constexpr float CSAMPLE_GAIN_MIN = CSAMPLE_GAIN_ZERO;
+constexpr float CSAMPLE_GAIN_MAX = CSAMPLE_GAIN_ONE;
// Limits the range of a CSAMPLE_GAIN value to [CSAMPLE_GAIN_MIN, CSAMPLE_GAIN_MAX].
inline
diff --git a/src/widget/wtracktableview.cpp b/src/widget/wtracktableview.cpp
index 4b74a81642e..758aa301f68 100644
--- a/src/widget/wtracktableview.cpp
+++ b/src/widget/wtracktableview.cpp
@@ -146,6 +146,7 @@ WTrackTableView::~WTrackTableView() {
delete m_pBpmTwoThirdsAction;
delete m_pBpmThreeFourthsAction;
delete m_pBPMMenu;
+ delete m_pReplayGainResetAction;
delete m_pPurgeAct;
delete m_pFileBrowserAct;
delete m_pResetPlayedAct;
@@ -449,6 +450,10 @@ void WTrackTableView::createActions() {
m_pClearBeatsAction = new QAction(tr("Clear BPM and Beatgrid"), this);
connect(m_pClearBeatsAction, SIGNAL(triggered()),
this, SLOT(slotClearBeats()));
+
+ m_pReplayGainResetAction = new QAction(tr("Reset Replay Gain"), this);
+ connect(m_pReplayGainResetAction, SIGNAL(triggered()),
+ this, SLOT(slotReplayGainReset()));
}
// slot
@@ -870,7 +875,8 @@ void WTrackTableView::contextMenuEvent(QContextMenuEvent* event) {
m_pBPMMenu->addAction(m_pClearBeatsAction);
}
- bool locked = modelHasCapabilities(TrackModel::TRACKMODELCAPS_LOCKED);
+ m_pMenu->addAction(m_pReplayGainResetAction);
+
m_pMenu->addSeparator();
if (modelHasCapabilities(TrackModel::TRACKMODELCAPS_RELOADMETADATA)) {
m_pMenu->addAction(m_pReloadMetadataAct);
@@ -899,6 +905,7 @@ void WTrackTableView::contextMenuEvent(QContextMenuEvent* event) {
// REMOVE and HIDE should not be at the first menu position to avoid accidental clicks
m_pMenu->addSeparator();
+ bool locked = modelHasCapabilities(TrackModel::TRACKMODELCAPS_LOCKED);
if (modelHasCapabilities(TrackModel::TRACKMODELCAPS_REMOVE)) {
m_pRemoveAct->setEnabled(!locked);
m_pMenu->addAction(m_pRemoveAct);
@@ -1590,6 +1597,22 @@ void WTrackTableView::slotClearBeats() {
}
}
+void WTrackTableView::slotReplayGainReset() {
+ QModelIndexList indices = selectionModel()->selectedRows();
+ TrackModel* trackModel = getTrackModel();
+
+ if (trackModel == NULL) {
+ return;
+ }
+
+ foreach (QModelIndex index, indices) {
+ TrackPointer pTrack = trackModel->getTrack(index);
+ if (pTrack) {
+ pTrack->setReplayGain(Mixxx::ReplayGain());
+ }
+ }
+}
+
void WTrackTableView::slotCoverArtSelected(const CoverArt& art) {
TrackModel* trackModel = getTrackModel();
if (trackModel == NULL) {
diff --git a/src/widget/wtracktableview.h b/src/widget/wtracktableview.h
index ff6012ce954..881f5938a00 100644
--- a/src/widget/wtracktableview.h
+++ b/src/widget/wtracktableview.h
@@ -72,6 +72,7 @@ class WTrackTableView : public WLibraryTableView {
void slotUnlockBpm();
void slotScaleBpm(int);
void slotClearBeats();
+ void slotReplayGainReset();
// Signalled 20 times per second (every 50ms) by GuiTick.
void slotGuiTick50ms(double);
void slotScrollValueChanged(int);
@@ -155,6 +156,9 @@ class WTrackTableView : public WLibraryTableView {
// Clear track beats
QAction* m_pClearBeatsAction;
+ // Replay Gain feature
+ QAction *m_pReplayGainResetAction;
+
bool m_sorting;
// Column numbers