diff --git a/res/controllers/Hercules P32 DJ.midi.xml b/res/controllers/Hercules P32 DJ.midi.xml
index 0f528f3fe57..81d4debd550 100644
--- a/res/controllers/Hercules P32 DJ.midi.xml
+++ b/res/controllers/Hercules P32 DJ.midi.xml
@@ -512,7 +512,7 @@
[Channel1]
- P32.leftDeck.effectUnit.dryWetKnob.input
+ P32.leftDeck.dryWetKnobOrPregain.input
0xB1
0x09
@@ -521,7 +521,7 @@
[Channel1]
- P32.leftDeck.effectUnit.dryWetKnob.input
+ P32.leftDeck.dryWetKnobOrPregain.input
0xB4
0x09
@@ -1548,7 +1548,7 @@
[Channel1]
- P32.rightDeck.effectUnit.dryWetKnob.input
+ P32.rightDeck.dryWetKnobOrPregain.input
0xB2
0x09
@@ -1557,7 +1557,7 @@
[Channel1]
- P32.rightDeck.effectUnit.dryWetKnob.input
+ P32.rightDeck.dryWetKnobOrPregain.input
0xB5
0x09
diff --git a/res/controllers/Hercules-P32-scripts.js b/res/controllers/Hercules-P32-scripts.js
index fbeb36f5210..203dd920e60 100644
--- a/res/controllers/Hercules-P32-scripts.js
+++ b/res/controllers/Hercules-P32-scripts.js
@@ -16,6 +16,8 @@ var loopEnabledDot = false;
var samplerCrossfaderAssign = true;
// Toggle effect units between 1 & 3 on left and 2 & 4 on right when toggling decks
var toggleEffectUnitsWithDecks = false;
+// Set the dry/wet knob as a pregain
+var dryWetKnobAsPregain = false;
/**
* Hercules P32 DJ controller script for Mixxx 2.1
@@ -378,7 +380,16 @@ P32.Deck = function(deckNumbers, channel) {
this.effectUnit.knobs[1].midi = [0xB0 + channel, 0x06];
this.effectUnit.knobs[2].midi = [0xB0 + channel, 0x07];
this.effectUnit.knobs[3].midi = [0xB0 + channel, 0x08];
- this.effectUnit.dryWetKnob.midi = [0xB0 + channel, 0x09];
+ if (dryWetKnobAsPregain) {
+ this.dryWetKnobOrPregain = new components.Pot({
+ midi: [0xB0 + channel, 0x09],
+ group: "[Channel" + channel + "]",
+ inKey: "pregain",
+ });
+ } else {
+ this.effectUnit.dryWetKnob.midi = [0xB0 + channel, 0x09];
+ this.dryWetKnobOrPregain = this.effectUnit.dryWetKnob;
+ }
this.effectUnit.enableButtons[1].midi = [0x90 + channel, 0x03];
this.effectUnit.enableButtons[2].midi = [0x90 + channel, 0x04];
this.effectUnit.enableButtons[3].midi = [0x90 + channel, 0x05];
diff --git a/res/controllers/common-controller-scripts.js b/res/controllers/common-controller-scripts.js
index d83fde54e35..c20a54908b6 100644
--- a/res/controllers/common-controller-scripts.js
+++ b/res/controllers/common-controller-scripts.js
@@ -45,23 +45,19 @@ var printObject = function(obj, maxdepth) {
};
var stringifyObject = function(obj, maxdepth, checked, prefix) {
- if (!maxdepth)
- maxdepth = 2;
+ if (!maxdepth) { maxdepth = 2; }
try {
return JSON.stringify(obj, null, maxdepth);
} catch (e) {
- if (!checked)
- checked = [];
- if (!prefix)
- prefix = "";
+ if (!checked) { checked = []; }
+ if (!prefix) { prefix = ""; }
if (maxdepth > 0 && typeof obj === "object" && obj !== null &&
Object.getPrototypeOf(obj) !== "" && !arrayContains(checked, obj)) {
checked.push(obj);
var output = "{\n";
for (var property in obj) {
var value = obj[property];
- if (typeof value === "function")
- continue;
+ if (typeof value === "function") { continue; }
output += prefix + property + ": "
+ stringifyObject(value, maxdepth - 1, checked, prefix + " ")
+ "\n";
@@ -74,8 +70,7 @@ var stringifyObject = function(obj, maxdepth, checked, prefix) {
var arrayContains = function(array, elem) {
for (var i = 0; i < array.length; i++) {
- if (array[i] === elem)
- return true;
+ if (array[i] === elem) { return true; }
}
return false;
};
@@ -98,8 +93,7 @@ var msecondstominutes = function(msecs) {
var secs = (msecs / 1000) | 0;
msecs %= 1000;
msecs = Math.round(msecs * 100 / 1000);
- if (msecs === 100)
- msecs = 99;
+ if (msecs === 100) { msecs = 99; }
return (m < 10 ? "0" + m : m)
+ ":"
@@ -351,6 +345,17 @@ script.crossfaderCurve = function(value, min, max) {
}
};
+/* -------- ------------------------------------------------------
+ script.posMod
+ Purpose: Computes the euclidean modulo of m % n. The result is always
+ in the range [0, m[
+ Input: dividend `a` and divisor `m` for modulo (a % m)
+ Output: positive remainder
+ -------- ------------------------------------------------------ */
+script.posMod = function(a, m) {
+ return ((a % m) + m) % m;
+};
+
/* -------- ------------------------------------------------------
script.loopMove
Purpose: Moves the current loop by the specified number of beats (default 1/2)
@@ -361,7 +366,7 @@ script.crossfaderCurve = function(value, min, max) {
Output: none
-------- ------------------------------------------------------ */
script.loopMove = function(group, direction, numberOfBeats) {
- if (!numberOfBeats || numberOfBeats === 0) numberOfBeats = 0.5;
+ if (!numberOfBeats || numberOfBeats === 0) { numberOfBeats = 0.5; }
if (direction < 0) {
engine.setValue(group, "loop_move", -numberOfBeats);
@@ -495,7 +500,7 @@ bpm.tapButton = function(deck) {
bpm.previousTapDelta = tapDelta;
bpm.tap.push(60 / tapDelta);
// Keep the last 8 samples for averaging
- if (bpm.tap.length > 8) bpm.tap.shift();
+ if (bpm.tap.length > 8) { bpm.tap.shift(); }
var sum = 0;
for (var i=0; i
diff --git a/res/skins/Tango/graphics/vumeter_clipping_over.png b/res/skins/Tango/graphics/vumeter_clipping_over.png
deleted file mode 100644
index c1199eb0e05..00000000000
Binary files a/res/skins/Tango/graphics/vumeter_clipping_over.png and /dev/null differ
diff --git a/res/skins/Tango/graphics/vumeter_clipping_over.svg b/res/skins/Tango/graphics/vumeter_clipping_over.svg
deleted file mode 100644
index 0101bd4a2ef..00000000000
--- a/res/skins/Tango/graphics/vumeter_clipping_over.svg
+++ /dev/null
@@ -1 +0,0 @@
-
diff --git a/res/skins/Tango/graphics/vumeter_level.png b/res/skins/Tango/graphics/vumeter_level.png
index 4a610422dbb..d6fe2361f82 100644
Binary files a/res/skins/Tango/graphics/vumeter_level.png and b/res/skins/Tango/graphics/vumeter_level.png differ
diff --git a/res/skins/Tango/graphics/vumeter_level.svg b/res/skins/Tango/graphics/vumeter_level.svg
index 1e59fd29507..07fdc7ed8e9 100644
--- a/res/skins/Tango/graphics/vumeter_level.svg
+++ b/res/skins/Tango/graphics/vumeter_level.svg
@@ -1 +1,57 @@
-
+
diff --git a/res/skins/Tango/graphics/vumeter_level_over.png b/res/skins/Tango/graphics/vumeter_level_over.png
index 389574ba821..c00ea67de76 100644
Binary files a/res/skins/Tango/graphics/vumeter_level_over.png and b/res/skins/Tango/graphics/vumeter_level_over.png differ
diff --git a/res/skins/Tango/graphics/vumeter_level_over.svg b/res/skins/Tango/graphics/vumeter_level_over.svg
index 8cbd579f8cd..ba73becfe31 100644
--- a/res/skins/Tango/graphics/vumeter_level_over.svg
+++ b/res/skins/Tango/graphics/vumeter_level_over.svg
@@ -1 +1,62 @@
-
+
diff --git a/res/skins/Tango/graphics/vumeter_mini_clipping.png b/res/skins/Tango/graphics/vumeter_mini_clipping.png
deleted file mode 100644
index b60c3eaba55..00000000000
Binary files a/res/skins/Tango/graphics/vumeter_mini_clipping.png and /dev/null differ
diff --git a/res/skins/Tango/graphics/vumeter_mini_clipping.svg b/res/skins/Tango/graphics/vumeter_mini_clipping.svg
deleted file mode 100644
index d14b618e951..00000000000
--- a/res/skins/Tango/graphics/vumeter_mini_clipping.svg
+++ /dev/null
@@ -1,2 +0,0 @@
-
-
diff --git a/res/skins/Tango/graphics/vumeter_mini_clipping_over.png b/res/skins/Tango/graphics/vumeter_mini_clipping_over.png
index 12d26666216..d91f4909125 100644
Binary files a/res/skins/Tango/graphics/vumeter_mini_clipping_over.png and b/res/skins/Tango/graphics/vumeter_mini_clipping_over.png differ
diff --git a/res/skins/Tango/graphics/vumeter_mini_clipping_over.svg b/res/skins/Tango/graphics/vumeter_mini_clipping_over.svg
deleted file mode 100644
index baefa0392b8..00000000000
--- a/res/skins/Tango/graphics/vumeter_mini_clipping_over.svg
+++ /dev/null
@@ -1,2 +0,0 @@
-
-
diff --git a/res/skins/Tango/graphics/vumeter_mini_level.png b/res/skins/Tango/graphics/vumeter_mini_level.png
index d8bca475bf1..c02aa50401d 100644
Binary files a/res/skins/Tango/graphics/vumeter_mini_level.png and b/res/skins/Tango/graphics/vumeter_mini_level.png differ
diff --git a/res/skins/Tango/graphics/vumeter_mini_level.svg b/res/skins/Tango/graphics/vumeter_mini_level.svg
deleted file mode 100644
index a33a4a23de5..00000000000
--- a/res/skins/Tango/graphics/vumeter_mini_level.svg
+++ /dev/null
@@ -1,2 +0,0 @@
-
-
diff --git a/res/skins/Tango/mic_aux_sampler/aux_unit.xml b/res/skins/Tango/mic_aux_sampler/aux_unit.xml
index ff149326f8d..97a165506ac 100644
--- a/res/skins/Tango/mic_aux_sampler/aux_unit.xml
+++ b/res/skins/Tango/mic_aux_sampler/aux_unit.xml
@@ -44,7 +44,7 @@ Variables:
3f,1min
- 5f,29f
+ 4f,29f
1f,1min
diff --git a/res/skins/Tango/mic_aux_sampler/mic_unit.xml b/res/skins/Tango/mic_aux_sampler/mic_unit.xml
index 8ac8ae1c76f..08e35ed8d61 100644
--- a/res/skins/Tango/mic_aux_sampler/mic_unit.xml
+++ b/res/skins/Tango/mic_aux_sampler/mic_unit.xml
@@ -48,7 +48,7 @@ Variables:
microphone_VuMeter
- 5f,29f
+ 4f,29f
1f,1min
diff --git a/res/skins/Tango/mic_aux_sampler/sampler.xml b/res/skins/Tango/mic_aux_sampler/sampler.xml
index 216e1451b59..2c4889d521d 100644
--- a/res/skins/Tango/mic_aux_sampler/sampler.xml
+++ b/res/skins/Tango/mic_aux_sampler/sampler.xml
@@ -226,7 +226,7 @@ Variables:
1f,0min
- 6f,1me
+ 4f,36f
diff --git a/res/skins/Tango/mixer/vumeter_mini.xml b/res/skins/Tango/mixer/vumeter_mini.xml
index e8a8a22c8dd..789fe8f5390 100644
--- a/res/skins/Tango/mixer/vumeter_mini.xml
+++ b/res/skins/Tango/mixer/vumeter_mini.xml
@@ -10,10 +10,11 @@ Refer to https://github.com/mixxxdj/mixxx/issues/9201
-->
-
- stacked
+ horizontal
+ min,max
+ 2f,-1min
_VuMeter
skin:../Tango/graphics/vumeter_mini_clipping_over.png
skin:../Tango/graphics/vumeter_mini_clipping.png
@@ -22,6 +23,7 @@ Refer to https://github.com/mixxxdj/mixxx/issues/9201
+
skin:../Tango/graphics/vumeter_mini_level_over.png
skin:../Tango/graphics/vumeter_mini_level.png
false
diff --git a/res/skins/Tango/mixer/vumeter_single.xml b/res/skins/Tango/mixer/vumeter_single.xml
index 3405388b8e8..b8bdfadcbff 100644
--- a/res/skins/Tango/mixer/vumeter_single.xml
+++ b/res/skins/Tango/mixer/vumeter_single.xml
@@ -17,60 +17,34 @@ Refer to https://github.com/mixxxdj/mixxx/issues/9201
VuMeterClippingBorder
horizontal
- max,min
+ 10f,-1min
-
- stacked
+
4f,-1min
-
-
- master_VuMeterL
- skin:../Tango/graphics/vumeter_clipping_over.png
- skin:../Tango/graphics/vumeter_clipping.png
-
- ,PeakIndicatorL
-
-
-
- skin:../Tango/graphics/vumeter_level_over.png
- skin:../Tango/graphics/vumeter_level.png
- false
- 4
- 800
- 200
- 5
-
- ,VuMeterL
-
-
-
-
-
- stacked
+ skin:../Tango/graphics/vumeter_level_over.png
+ skin:../Tango/graphics/vumeter_level.png
+ false
+ 4
+ 800
+ 200
+ 5
+
+ ,VuMeterL
+
+
+
4f,-1min
-
-
- master_VuMeterR
- skin:../Tango/graphics/vumeter_clipping_over.png
- skin:../Tango/graphics/vumeter_clipping.png
-
- ,PeakIndicatorR
-
-
-
- skin:../Tango/graphics/vumeter_level_over.png
- skin:../Tango/graphics/vumeter_level.png
- false
- 4
- 800
- 200
- 5
-
- ,VuMeterR
-
-
-
-
+ skin:../Tango/graphics/vumeter_level_over.png
+ skin:../Tango/graphics/vumeter_level.png
+ false
+ 4
+ 800
+ 200
+ 5
+
+ ,VuMeterR
+
+
,PeakIndicator
diff --git a/src/analyzer/analyzerthread.cpp b/src/analyzer/analyzerthread.cpp
index 15d201c546d..f2cbeaa8cfe 100644
--- a/src/analyzer/analyzerthread.cpp
+++ b/src/analyzer/analyzerthread.cpp
@@ -127,7 +127,7 @@ void AnalyzerThread::doRun() {
while (awaitWorkItemsFetched()) {
DEBUG_ASSERT(m_currentTrack.has_value());
- kLogger.debug() << "Analyzing" << m_currentTrack->getTrack()->getFileInfo();
+ kLogger.debug() << "Analyzing" << m_currentTrack->getTrack()->getLocation();
// Get the audio
const auto audioSource =
@@ -135,7 +135,7 @@ void AnalyzerThread::doRun() {
if (!audioSource) {
kLogger.warning()
<< "Failed to open file for analyzing:"
- << m_currentTrack->getTrack()->getFileInfo();
+ << m_currentTrack->getTrack()->getLocation();
emitDoneProgress(kAnalyzerProgressUnknown);
continue;
}
diff --git a/src/controllers/dlgprefcontrollerdlg.ui b/src/controllers/dlgprefcontrollerdlg.ui
index 325cb4ad0ff..c0ef5284c3e 100644
--- a/src/controllers/dlgprefcontrollerdlg.ui
+++ b/src/controllers/dlgprefcontrollerdlg.ui
@@ -296,7 +296,7 @@
-
-
+
0
0
diff --git a/src/dialog/dlgreplacecuecolor.cpp b/src/dialog/dlgreplacecuecolor.cpp
index b1047173e58..7d0e600450a 100644
--- a/src/dialog/dlgreplacecuecolor.cpp
+++ b/src/dialog/dlgreplacecuecolor.cpp
@@ -77,8 +77,8 @@ DlgReplaceCueColor::DlgReplaceCueColor(
// from the rest of the application (when not styled via QSS), but that's
// better than having buttons without any colors (which would make the
// color picker unusable).
- pushButtonNewColor->setStyle(m_pStyle);
- pushButtonCurrentColor->setStyle(m_pStyle);
+ pushButtonNewColor->setStyle(m_pStyle.get());
+ pushButtonCurrentColor->setStyle(m_pStyle.get());
// Set up new color button
ColorPaletteSettings colorPaletteSettings(pConfig);
@@ -90,9 +90,11 @@ DlgReplaceCueColor::DlgReplaceCueColor(
}
setButtonColor(pushButtonNewColor, mixxx::RgbColor::toQColor(firstColor));
- // Add menu for new color button
+ // Add menu for 'New color' button
m_pNewColorPickerAction = make_parented(
- WColorPicker::Option::AllowCustomColor,
+ WColorPicker::Option::AllowCustomColor |
+ // TODO(xxx) remove this once the preferences are themed via QSS
+ WColorPicker::Option::NoExtStyleSheet,
colorPaletteSettings.getHotcueColorPalette(),
this);
m_pNewColorPickerAction->setObjectName("HotcueColorPickerAction");
@@ -110,7 +112,7 @@ DlgReplaceCueColor::DlgReplaceCueColor(
m_pNewColorMenu->addAction(m_pNewColorPickerAction);
pushButtonNewColor->setMenu(m_pNewColorMenu);
- // Set up current color button
+ // Set up 'Current color' button
setButtonColor(pushButtonCurrentColor,
mixxx::RgbColor::toQColor(
mixxx::PredefinedColorPalettes::kDefaultCueColor));
@@ -122,13 +124,15 @@ DlgReplaceCueColor::DlgReplaceCueColor(
this,
&DlgReplaceCueColor::slotUpdateWidgets);
- // Add menu for current color button
+ // Add menu for 'Current color' button
m_pCurrentColorPickerAction = make_parented(
- WColorPicker::Option::AllowCustomColor,
+ WColorPicker::Option::AllowCustomColor |
+ // TODO(xxx) remove this once the preferences are themed via QSS
+ WColorPicker::Option::NoExtStyleSheet,
colorPaletteSettings.getHotcueColorPalette(),
this);
m_pCurrentColorPickerAction->setObjectName("HotcueColorPickerAction");
- m_pNewColorPickerAction->setSelectedColor(
+ m_pCurrentColorPickerAction->setSelectedColor(
mixxx::PredefinedColorPalettes::kDefaultCueColor);
connect(m_pCurrentColorPickerAction,
&WColorPickerAction::colorPicked,
@@ -173,9 +177,6 @@ DlgReplaceCueColor::DlgReplaceCueColor(
slotUpdateWidgets();
}
-DlgReplaceCueColor::~DlgReplaceCueColor() {
-}
-
void DlgReplaceCueColor::setColorPalette(const ColorPalette& palette) {
m_pNewColorPickerAction->setColorPalette(palette);
QResizeEvent resizeNewColorMenuEvent(QSize(), m_pNewColorMenu->size());
diff --git a/src/dialog/dlgreplacecuecolor.h b/src/dialog/dlgreplacecuecolor.h
index 397595f7292..47af6642a95 100644
--- a/src/dialog/dlgreplacecuecolor.h
+++ b/src/dialog/dlgreplacecuecolor.h
@@ -6,6 +6,7 @@
#include
#include
#include
+#include
#include "dialog/ui_dlgreplacecuecolordlg.h"
#include "library/dao/trackdao.h"
@@ -35,7 +36,7 @@ class DlgReplaceCueColor : public QDialog, public Ui::DlgReplaceCueColor {
mixxx::DbConnectionPoolPtr dbConnectionPool,
TrackCollectionManager* pTrackCollectionManager,
QWidget* pParent);
- ~DlgReplaceCueColor();
+ ~DlgReplaceCueColor() override = default;
void setColorPalette(const ColorPalette& palette);
@@ -66,6 +67,6 @@ class DlgReplaceCueColor : public QDialog, public Ui::DlgReplaceCueColor {
parented_ptr m_pCurrentColorPickerAction;
mixxx::RgbColor::optional_t m_lastAutoSetNewColor;
mixxx::RgbColor::optional_t m_lastAutoSetCurrentColor;
- QStyle* m_pStyle;
+ std::unique_ptr m_pStyle;
};
Q_DECLARE_OPERATORS_FOR_FLAGS(DlgReplaceCueColor::Conditions);
diff --git a/src/encoder/encoderopus.cpp b/src/encoder/encoderopus.cpp
index c74622a3904..029f54f4124 100644
--- a/src/encoder/encoderopus.cpp
+++ b/src/encoder/encoderopus.cpp
@@ -186,7 +186,7 @@ int EncoderOpus::initEncoder(mixxx::audio::SampleRate sampleRate, QString* pUser
opus_encoder_ctl(m_pOpus, OPUS_SET_VBR_CONSTRAINT(0)); // Unconstrained VBR
}
- m_readRequired = m_sampleRate * kOpusFrameMs;
+ m_readRequired = m_channels * m_sampleRate * kOpusFrameMs / 1000;
m_pFifoChunkBuffer = std::make_unique(m_readRequired);
initStream();
diff --git a/src/library/autodj/autodjfeature.cpp b/src/library/autodj/autodjfeature.cpp
index 9b5bc1d7d27..e793d9023be 100644
--- a/src/library/autodj/autodjfeature.cpp
+++ b/src/library/autodj/autodjfeature.cpp
@@ -262,7 +262,7 @@ void AutoDJFeature::slotAddRandomTrack() {
if (!pRandomTrack->getFileInfo().checkFileExists()) {
qWarning() << "Track does not exist:"
<< pRandomTrack->getInfo()
- << pRandomTrack->getFileInfo();
+ << pRandomTrack->getLocation();
pRandomTrack.reset();
}
}
diff --git a/src/library/basetrackcache.cpp b/src/library/basetrackcache.cpp
index 271860c869c..aaafaafecc3 100644
--- a/src/library/basetrackcache.cpp
+++ b/src/library/basetrackcache.cpp
@@ -193,7 +193,7 @@ bool BaseTrackCache::updateTrackInIndex(
return false;
}
if (sDebug) {
- qDebug() << "updateTrackInIndex:" << pTrack->getFileInfo();
+ qDebug() << "updateTrackInIndex:" << pTrack->getLocation();
}
int numColumns = columnCount();
diff --git a/src/library/dao/trackdao.cpp b/src/library/dao/trackdao.cpp
index 41d5f07bdd6..b880dfae3b8 100644
--- a/src/library/dao/trackdao.cpp
+++ b/src/library/dao/trackdao.cpp
@@ -316,8 +316,8 @@ bool TrackDAO::saveTrack(Track* pTrack) const {
const TrackId trackId = pTrack->getId();
DEBUG_ASSERT(trackId.isValid());
qDebug() << "TrackDAO: Saving track"
- << trackId
- << pTrack->getFileInfo();
+ << trackId
+ << pTrack->getLocation();
if (!updateTrack(*pTrack)) {
return false;
}
@@ -696,13 +696,13 @@ TrackId TrackDAO::addTracksAddTrack(const TrackPointer& pTrack, bool unremove) {
m_pQueryLibrarySelect || m_pQueryTrackLocationSelect)) {
qDebug() << "TrackDAO::addTracksAddTrack: needed SqlQuerys have not "
"been prepared. Skipping track"
- << fileAccess.info();
+ << fileAccess.info().location();
DEBUG_ASSERT("Failed query");
return TrackId();
}
qDebug() << "TrackDAO: Adding track"
- << fileAccess.info();
+ << fileAccess.info().location();
TrackId trackId;
@@ -736,7 +736,7 @@ TrackId TrackDAO::addTracksAddTrack(const TrackPointer& pTrack, bool unremove) {
if (!m_pQueryLibrarySelect->exec()) {
LOG_FAILED_QUERY(*m_pQueryLibrarySelect)
<< "Failed to query existing track: "
- << fileAccess.info();
+ << fileAccess.info().location();
return TrackId();
}
if (m_queryLibraryIdColumn == UndefinedRecordIndex) {
@@ -763,7 +763,7 @@ TrackId TrackDAO::addTracksAddTrack(const TrackPointer& pTrack, bool unremove) {
if (!m_pQueryLibraryUpdate->exec()) {
LOG_FAILED_QUERY(*m_pQueryLibraryUpdate)
<< "Failed to unremove existing track: "
- << fileAccess.info();
+ << fileAccess.info().location();
return TrackId();
}
}
@@ -889,8 +889,8 @@ TrackPointer TrackDAO::addTracksAddFile(
const TrackId newTrackId = addTracksAddTrack(pTrack, unremove);
if (!newTrackId.isValid()) {
qWarning() << "TrackDAO::addTracksAddTrack:"
- << "Failed to add track to database"
- << pTrack->getFileInfo();
+ << "Failed to add track to database"
+ << pTrack->getLocation();
// GlobalTrackCache will be unlocked implicitly
return nullptr;
}
@@ -1657,7 +1657,7 @@ bool TrackDAO::updateTrack(const Track& track) const {
qDebug() << "TrackDAO:"
<< "Updating track in database"
<< trackId
- << track.getFileInfo();
+ << track.getLocation();
SqlTransaction transaction(m_database);
// PerformanceTimer time;
diff --git a/src/musicbrainz/chromaprinter.cpp b/src/musicbrainz/chromaprinter.cpp
index f1d481e3756..727b139b4ab 100644
--- a/src/musicbrainz/chromaprinter.cpp
+++ b/src/musicbrainz/chromaprinter.cpp
@@ -119,7 +119,7 @@ QString ChromaPrinter::getFingerprint(TrackPointer pTrack) {
if (!pAudioSource) {
qDebug()
<< "Failed to open file for fingerprinting"
- << pTrack->getFileInfo();
+ << pTrack->getLocation();
return QString();
}
diff --git a/src/sources/soundsourcemp3.cpp b/src/sources/soundsourcemp3.cpp
index e8528c9c44d..ec43362b51f 100644
--- a/src/sources/soundsourcemp3.cpp
+++ b/src/sources/soundsourcemp3.cpp
@@ -20,6 +20,11 @@ constexpr SINT kMaxBytesPerMp3Frame = 1441;
// mp3 supports 9 different sample rates
constexpr int kSampleRateCount = 9;
+// Possible tags in the Mp3 Info Frame
+// constexpr char kVbrTag0[] = "Xing";
+// constexpr char kVbrTag1[] = "Info";
+constexpr int kInfoTagStrLen = 4;
+
int getIndexBySampleRate(audio::SampleRate sampleRate) {
switch (sampleRate) {
case 8000:
@@ -100,63 +105,54 @@ void logFrameHeader(QDebug logger, const mad_header& madHeader) {
<< "flags:" << formatHeaderFlags(madHeader.flags);
}
-inline bool isRecoverableError(const mad_stream& madStream) {
- return MAD_RECOVERABLE(madStream.error);
-}
-
-inline bool isUnrecoverableError(const mad_stream& madStream) {
- return (MAD_ERROR_NONE != madStream.error) && !isRecoverableError(madStream);
+inline bool isUnrecoverableError(mad_error error) {
+ return (MAD_ERROR_NONE != error) && !MAD_RECOVERABLE(error);
}
-inline bool isStreamValid(const mad_stream& madStream) {
- return !isUnrecoverableError(madStream);
+inline bool hasUnrecoverableError(const mad_stream* pMadStream) {
+ if (pMadStream) {
+ return isUnrecoverableError(pMadStream->error);
+ }
+ return true;
}
bool decodeFrameHeader(
mad_header* pMadHeader,
mad_stream* pMadStream,
bool skipId3Tag) {
- DEBUG_ASSERT(pMadStream);
- DEBUG_ASSERT(isStreamValid(*pMadStream));
+ DEBUG_ASSERT(!hasUnrecoverableError(pMadStream));
if (mad_header_decode(pMadHeader, pMadStream)) {
// Something went wrong when decoding the frame header...
+ DEBUG_ASSERT(pMadStream->error != MAD_ERROR_NONE);
if (MAD_ERROR_BUFLEN == pMadStream->error) {
// EOF
return false;
}
- if (isUnrecoverableError(*pMadStream)) {
- DEBUG_ASSERT(!isStreamValid(*pMadStream));
+ if (isUnrecoverableError(pMadStream->error)) {
kLogger.warning() << "Unrecoverable MP3 header decoding error:"
<< mad_stream_errorstr(pMadStream);
return false;
}
-#ifndef QT_NO_DEBUG_OUTPUT
- // Logging of MP3 frame headers should only be enabled
- // for debugging purposes.
- logFrameHeader(kLogger.debug(), *pMadHeader);
-#endif
- if (isRecoverableError(*pMadStream)) {
- if ((MAD_ERROR_LOSTSYNC == pMadStream->error) && skipId3Tag) {
- long tagsize = id3_tag_query(pMadStream->this_frame,
- pMadStream->bufend - pMadStream->this_frame);
- if (0 < tagsize) {
- // Skip ID3 tag data
- mad_stream_skip(pMadStream, tagsize);
- // Return immediately to suppress lost
- // synchronization warnings
- return false;
- }
+ if ((pMadStream->error == MAD_ERROR_LOSTSYNC) && skipId3Tag) {
+ long tagsize = id3_tag_query(pMadStream->this_frame,
+ pMadStream->bufend - pMadStream->this_frame);
+ if (tagsize > 0) {
+ // Skip ID3 tag data
+ mad_stream_skip(pMadStream, tagsize);
+ // Return immediately to suppress lost
+ // synchronization warnings
+ return false;
}
- // These recoverable errors occur for many MP3 files and might
- // worry users when logged as a warning. The issue will become
- // obsolete once we switched to FFmpeg for MP3 decoding.
- kLogger.info() << "Recoverable MP3 header decoding error:"
- << mad_stream_errorstr(pMadStream);
- logFrameHeader(kLogger.warning(), *pMadHeader);
- return false;
}
+ // These recoverable errors occur for many MP3 files and might
+ // worry users when logged as a warning. The issue will become
+ // obsolete once we switched to FFmpeg for MP3 decoding.
+ kLogger.info() << "Recoverable MP3 header decoding error:"
+ << mad_stream_errorstr(pMadStream);
+ logFrameHeader(kLogger.info(), *pMadHeader);
+ return false;
}
- DEBUG_ASSERT(isStreamValid(*pMadStream));
+ DEBUG_ASSERT(pMadStream->error == MAD_ERROR_NONE);
return true;
}
@@ -244,27 +240,44 @@ SoundSource::OpenResult SoundSourceMp3::tryOpen(
quint64 sumBitrateFrames = 0; // nominator
quint64 cntBitrateFrames = 0; // denominator
+ // Normally mp3 files starts with an extra frame of silence containing
+ // encoder infos called "LAME Tag". Since the early days of we skip the
+ // first frame uncoditionally, to not have these extra portion of silence
+ // in the track. This has the issue that with files without this frame real
+ // samples are dropped.
+ // Since this issue exists since the early days of Mixxx the analysis data
+ // is affected by the offset. Fixing this without fixing the analysis data
+ // will silently invalidate analysis, cues and loops.
+ // Note: A relates issue with not accurate seeks has been fixed in Mixxx 2.1.0 2015
+ // https://github.com/mixxxdj/mixxx/pull/411
+ bool mp3InfoTagSkipped = false;
+
mad_header madHeader;
mad_header_init(&madHeader);
auto maxChannelCount = audio::ChannelCount();
do {
if (!decodeFrameHeader(&madHeader, &m_madStream, true)) {
- if (isStreamValid(m_madStream)) {
- // Skip frame
- continue;
- } else {
+ if (MAD_ERROR_BUFLEN == m_madStream.error) {
+ // try again with the copy and MAD_BUFFER_GUARD bytes
+ if (copyLeftoverFrame()) {
+ continue;
+ }
+ }
+ if (isUnrecoverableError(m_madStream.error)) {
// Abort decoding
break;
+ } else {
+ // skip frame
+ continue;
}
}
// Grab data from madHeader
const unsigned int madSampleRate = madHeader.samplerate;
- // TODO(XXX): Replace DEBUG_ASSERT with static_assert
// MAD must not change its enum values!
- DEBUG_ASSERT(MAD_UNITS_8000_HZ == 8000);
+ static_assert(MAD_UNITS_8000_HZ == 8000);
const mad_units madUnits = static_cast(madSampleRate);
const long madFrameLength = mad_timer_count(madHeader.duration, madUnits);
@@ -276,6 +289,41 @@ SoundSource::OpenResult SoundSourceMp3::tryOpen(
continue;
}
+ if (!mp3InfoTagSkipped) {
+ // We assume that the first frame contains the mp3 info frame
+ // which needs to be skipped
+ // https://linux.m2osw.com/mp3-info-tag-specifications-rev0-lame-3100
+ // https://github.com/Iunusov/libmp3lame-CMAKE/blob/bb770fb6e4b4dfc963860380a5e7765c370aaef1/libmp3lame/VbrTag.c#L333
+ int mp3InfoFrameOffset = 0;
+ if (madHeader.flags & MAD_FLAG_LSF_EXT) {
+ // MPEG Version 2 (ISO/IEC 13818-3)
+ // or MPEG Version 2.5
+ if (madHeader.mode == MAD_MODE_SINGLE_CHANNEL) {
+ mp3InfoFrameOffset = (9 + 4);
+ } else {
+ mp3InfoFrameOffset = (17 + 4);
+ }
+ } else {
+ // MPEG Version 1 (ISO/IEC 11172-3)
+ if (madHeader.mode == MAD_MODE_SINGLE_CHANNEL) {
+ mp3InfoFrameOffset = (17 + 4);
+ } else {
+ mp3InfoFrameOffset = (32 + 4);
+ }
+ }
+
+ QString mp3InfoTag =
+ QString::fromLatin1(reinterpret_cast(
+ &m_madStream.this_frame[mp3InfoFrameOffset]),
+ kInfoTagStrLen);
+ kLogger.debug()
+ << "Skipping MP3 Info Frame:"
+ << mp3InfoTag;
+
+ mp3InfoTagSkipped = true;
+ continue;
+ }
+
const audio::ChannelCount madChannelCount(MAD_NCHANNELS(&madHeader));
if (madChannelCount.isValid()) {
if (maxChannelCount.isValid() && (madChannelCount != maxChannelCount)) {
@@ -318,8 +366,9 @@ SoundSource::OpenResult SoundSourceMp3::tryOpen(
m_curFrameIndex += madFrameLength;
DEBUG_ASSERT(m_madStream.this_frame);
- DEBUG_ASSERT(0 <= (m_madStream.this_frame - m_pFileData));
- } while (quint64(m_madStream.this_frame - m_pFileData) < m_fileSize);
+ DEBUG_ASSERT(0 <= (m_madStream.this_frame - m_pFileData) ||
+ m_madStream.this_frame == &*m_leftoverBuffer.begin());
+ } while (m_madStream.next_frame < m_madStream.bufend);
mad_header_finish(&madHeader);
@@ -466,12 +515,7 @@ void SoundSourceMp3::restartDecoding(
mad_synth_mute(&m_madSynth);
}
- if (decodeFrameHeader(&m_madFrame.header, &m_madStream, false) && isStreamValid(m_madStream)) {
- m_curFrameIndex = seekFrame.frameIndex;
- } else {
- // Failure -> Seek to EOF
- m_curFrameIndex = frameIndexMax();
- }
+ m_curFrameIndex = seekFrame.frameIndex;
}
void SoundSourceMp3::addSeekFrame(
@@ -481,7 +525,8 @@ void SoundSourceMp3::addSeekFrame(
(m_seekFrameList.back().frameIndex < frameIndex));
DEBUG_ASSERT(m_seekFrameList.empty() ||
(nullptr == pInputData) ||
- (0 < (pInputData - m_seekFrameList.back().pInputData)));
+ (0 < (pInputData - m_seekFrameList.back().pInputData)) ||
+ pInputData == &*m_leftoverBuffer.begin());
SeekFrameType seekFrame;
seekFrame.pInputData = pInputData;
seekFrame.frameIndex = frameIndex;
@@ -599,72 +644,51 @@ ReadableSampleFrames SoundSourceMp3::readSampleFramesClamped(
// of mad_frame_decode() has proven to be extremely tricky.
// Don't change anything at the following lines of code
// unless you know what you are doing!!!
- unsigned char const* pMadThisFrame = m_madStream.this_frame;
+
+ // In addition to the error value we verify that the stream has been advanced
+ unsigned char const* pMadNextFrame = m_madStream.next_frame;
if (mad_frame_decode(&m_madFrame, &m_madStream)) {
// Something went wrong when decoding the frame...
if (MAD_ERROR_BUFLEN == m_madStream.error) {
// Abort when reaching the end of the stream
- DEBUG_ASSERT(isUnrecoverableError(m_madStream));
- if (m_madStream.next_frame != nullptr) {
- // Decoding of the last MP3 frame fails if it is not padded
- // with 0 bytes. MAD requires that the last frame ends with
- // at least MAD_BUFFER_GUARD of 0 bytes.
- // https://www.mars.org/pipermail/mad-dev/2001-May/000262.html
- // "The reason for MAD_BUFFER_GUARD has to do with the way decoding is performed.
- // In Layer III, Huffman decoding may inadvertently read a few bytes beyond the
- // end of the buffer in the case of certain invalid input. This is not detected
- // until after the fact. To prevent this from causing problems, and also to
- // ensure the next frame's main_data_begin pointer is always accessible, MAD
- // requires MAD_BUFFER_GUARD (currently 8) bytes to be present in the buffer past
- // the end of the current frame in order to decode the frame."
- const SINT remainingBytes = m_madStream.bufend - m_madStream.next_frame;
- DEBUG_ASSERT(remainingBytes <= kMaxBytesPerMp3Frame); // only last MP3 frame
- const SINT leftoverBytes = remainingBytes + MAD_BUFFER_GUARD;
- if ((remainingBytes > 0) && (leftoverBytes <= SINT(m_leftoverBuffer.size()))) {
- // Copy the data of the last MP3 frame into the leftover buffer...
- unsigned char* pLeftoverBuffer = &*m_leftoverBuffer.begin();
- std::copy(m_madStream.next_frame, m_madStream.next_frame + remainingBytes, pLeftoverBuffer);
- // ...append the required guard bytes...
- std::fill(pLeftoverBuffer + remainingBytes, pLeftoverBuffer + leftoverBytes, 0);
- // ...and retry decoding.
- mad_stream_buffer(&m_madStream, pLeftoverBuffer, leftoverBytes);
- m_madStream.error = MAD_ERROR_NONE;
- continue;
- }
- if (m_curFrameIndex < frameIndexMax()) {
- kLogger.warning() << "Failed to decode the end of the MP3 stream"
- << m_curFrameIndex << "<" << frameIndexMax();
- }
+ if (copyLeftoverFrame()) {
+ continue;
+ }
+ if (m_curFrameIndex < frameIndexMax()) {
+ kLogger.warning() << "Failed to decode the end of the MP3 stream"
+ << m_curFrameIndex << "<" << frameIndexMax();
}
break;
}
- if (isUnrecoverableError(m_madStream)) {
+ if (isUnrecoverableError(m_madStream.error)) {
kLogger.warning() << "Unrecoverable MP3 frame decoding error:"
<< mad_stream_errorstr(&m_madStream);
// Abort decoding
break;
}
- if (isRecoverableError(m_madStream)) {
- if (pMadThisFrame != m_madStream.this_frame) {
- if (!pSampleBuffer ||
- (m_madStream.error == MAD_ERROR_LOSTSYNC)) {
- // Don't bother the user with warnings from recoverable
- // errors while skipping decoded samples or that even
- // might occur for files that are perfectly ok.
- if (kLogger.debugEnabled()) {
- kLogger.debug()
- << "Recoverable MP3 frame decoding error:"
- << mad_stream_errorstr(&m_madStream);
- }
- } else {
- kLogger.info() << "Recoverable MP3 frame decoding error:"
- << mad_stream_errorstr(&m_madStream);
+ if (pMadNextFrame != m_madStream.next_frame) {
+ // stream has been advanced
+ if (!pSampleBuffer ||
+ (m_madStream.error == MAD_ERROR_LOSTSYNC)) {
+ // Don't bother the user with warnings from recoverable
+ // errors while skipping decoded samples or that even
+ // might occur for files that are perfectly ok.
+ if (kLogger.debugEnabled()) {
+ kLogger.debug()
+ << "Recoverable MP3 frame decoding error:"
+ << mad_stream_errorstr(&m_madStream);
}
+ } else {
+ kLogger.info() << "Recoverable MP3 frame decoding error:"
+ << mad_stream_errorstr(&m_madStream);
}
- // Continue decoding
}
+ // Continue decoding
}
- if (pMadThisFrame == m_madStream.this_frame) {
+ DEBUG_ASSERT(!isUnrecoverableError(m_madStream.error));
+
+ if (pMadNextFrame == m_madStream.next_frame) {
+ // stream has not been advanced
// Retry decoding, but only once for each position to
// prevent infinite loops when decoding corrupt files
if (retryFrameIndex != m_curFrameIndex) {
@@ -672,7 +696,9 @@ ReadableSampleFrames SoundSourceMp3::readSampleFramesClamped(
if (kLogger.debugEnabled()) {
kLogger.debug()
<< "Retry decoding MP3 frame @"
- << m_curFrameIndex;
+ << m_curFrameIndex
+ << "error:"
+ << mad_stream_errorstr(&m_madStream);
}
continue;
} else {
@@ -684,8 +710,6 @@ ReadableSampleFrames SoundSourceMp3::readSampleFramesClamped(
}
}
- DEBUG_ASSERT(isStreamValid(m_madStream));
-
#ifndef QT_NO_DEBUG_OUTPUT
const auto madFrameChannelCount =
mixxx::audio::ChannelCount{MAD_NCHANNELS(&m_madFrame.header)};
@@ -785,4 +809,47 @@ ReadableSampleFrames SoundSourceMp3::readSampleFramesClamped(
std::min(writableSampleFrames.writableLength(), getSignalInfo().frames2samples(numberOfFrames))));
}
+bool SoundSourceMp3::copyLeftoverFrame() {
+ if (m_madStream.next_frame != nullptr) {
+ // Decoding of the last MP3 frame fails if it is not padded
+ // with 0 bytes. MAD requires that the last frame ends with
+ // at least MAD_BUFFER_GUARD of 0 bytes.
+ // https://www.mars.org/pipermail/mad-dev/2001-May/000262.html
+ // "The reason for MAD_BUFFER_GUARD has to do with the way decoding is performed.
+ // In Layer III, Huffman decoding may inadvertently read a few bytes beyond the
+ // end of the buffer in the case of certain invalid input. This is not detected
+ // until after the fact. To prevent this from causing problems, and also to
+ // ensure the next frame's main_data_begin pointer is always accessible, MAD
+ // requires MAD_BUFFER_GUARD (currently 8) bytes to be present in the buffer past
+ // the end of the current frame in order to decode the frame."
+ unsigned char* pLeftoverBuffer = &*m_leftoverBuffer.begin();
+ if (pLeftoverBuffer == m_madStream.buffer) {
+ return false;
+ }
+ const SINT remainingBytes = m_madStream.bufend - m_madStream.next_frame;
+ DEBUG_ASSERT(remainingBytes <= kMaxBytesPerMp3Frame); // only last MP3 frame
+ const SINT leftoverBytes = remainingBytes + MAD_BUFFER_GUARD;
+ if ((remainingBytes > 0) && (leftoverBytes <= SINT(m_leftoverBuffer.size()))) {
+ // Copy the data of the last MP3 frame into the leftover buffer...
+ std::copy(m_madStream.next_frame,
+ m_madStream.next_frame + remainingBytes,
+ pLeftoverBuffer);
+ // ...append the required guard bytes...
+ std::fill(pLeftoverBuffer + remainingBytes, pLeftoverBuffer + leftoverBytes, 0);
+ // ...and retry decoding.
+ // Note: We must not use mad_stream_buffer() here,
+ // because this will clear the bit reservoir used
+ // for VBR
+ m_madStream.buffer = pLeftoverBuffer;
+ m_madStream.bufend = pLeftoverBuffer + leftoverBytes;
+ m_madStream.this_frame = pLeftoverBuffer;
+ m_madStream.next_frame = pLeftoverBuffer;
+ m_madStream.sync = 1;
+ m_madStream.error = MAD_ERROR_NONE;
+ return true;
+ }
+ }
+ return false;
+}
+
} // namespace mixxx
diff --git a/src/sources/soundsourcemp3.h b/src/sources/soundsourcemp3.h
index c899bf7955e..2804d8a59e2 100644
--- a/src/sources/soundsourcemp3.h
+++ b/src/sources/soundsourcemp3.h
@@ -56,6 +56,8 @@ class SoundSourceMp3 final : public SoundSource {
/** Returns the position in m_seekFrameList of the requested frame index. */
SINT findSeekFrameIndex(SINT frameIndex) const;
+ bool copyLeftoverFrame();
+
SINT m_curFrameIndex;
// NOTE(uklotzde): Each invocation of initDecoding() must be
diff --git a/src/sources/soundsourceproxy.cpp b/src/sources/soundsourceproxy.cpp
index 5e8764f9627..f7b0423424c 100644
--- a/src/sources/soundsourceproxy.cpp
+++ b/src/sources/soundsourceproxy.cpp
@@ -802,7 +802,7 @@ SoundSourceProxy::UpdateTrackFromSourceResult SoundSourceProxy::updateTrackFromS
<< "Parsing missing"
<< (splitArtistTitle ? "artist/title" : "title")
<< "from file name:"
- << fileInfo;
+ << fileInfo.location();
if (trackMetadata.refTrackInfo().parseArtistTitleFromFileName(
fileInfo.fileName(), splitArtistTitle)) {
// Pretend that metadata import succeeded
diff --git a/src/widget/wcolorpicker.cpp b/src/widget/wcolorpicker.cpp
index eb90d97cce7..7913a0f5caa 100644
--- a/src/widget/wcolorpicker.cpp
+++ b/src/widget/wcolorpicker.cpp
@@ -151,6 +151,9 @@ void WColorPicker::addColorButton(mixxx::RgbColor color, QGridLayout* pLayout, i
QString("QPushButton { background-color: %1; }").arg(mixxx::RgbColor::toQString(color)));
pButton->setToolTip(mixxx::RgbColor::toQString(color));
pButton->setCheckable(true);
+ // Without this the button might shrink when setting the checkmark icon,
+ // both here or via external stylesheets.
+ pButton->setSizePolicy(QSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding));
m_colorButtons.append(pButton);
connect(pButton,
@@ -173,6 +176,7 @@ void WColorPicker::addNoColorButton(QGridLayout* pLayout, int row, int column) {
pButton->setProperty("noColor", true);
pButton->setToolTip(tr("No color"));
pButton->setCheckable(true);
+ pButton->setSizePolicy(QSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding));
connect(pButton,
&QPushButton::clicked,
this,
@@ -226,6 +230,9 @@ void WColorPicker::setColorButtonChecked(const mixxx::RgbColor::optional_t& colo
}
pButton->setChecked(checked);
+ if (m_options.testFlag(Option::NoExtStyleSheet)) {
+ pButton->setIcon(QIcon(checked ? ":/images/ic_checkmark.svg" : ""));
+ }
// This is needed to re-apply skin styles (e.g. to show/hide a checkmark icon)
pButton->style()->unpolish(pButton);
pButton->style()->polish(pButton);
diff --git a/src/widget/wcolorpicker.h b/src/widget/wcolorpicker.h
index 48aeade575a..c27f6c9cde4 100644
--- a/src/widget/wcolorpicker.h
+++ b/src/widget/wcolorpicker.h
@@ -18,6 +18,12 @@ class WColorPicker : public QWidget {
NoOptions = 0,
AllowNoColor = 1,
AllowCustomColor = 1 << 1,
+ // Some color pickers can be styled with the skin stylesheets,
+ // for example in WCueMenuPopup or WTrackMenu.
+ // If that's not possible (or just not done yet), for example in
+ // DlgReplaceCueColor and DlgTrackInfo, use this option to un/set
+ // the checkmark icon on de/selected color buttons in c++.
+ NoExtStyleSheet = 1 << 2,
};
Q_DECLARE_FLAGS(Options, Option);