Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add setting definition for Traktor S4 MK3 #12995

Merged
merged 2 commits into from
Mar 31, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
1 change: 1 addition & 0 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -2075,6 +2075,7 @@ add_executable(mixxx-test
src/test/configobject_test.cpp
src/test/controller_mapping_validation_test.cpp
src/test/controller_mapping_settings_test.cpp
src/test/controllers/controller_columnid_regression_test.cpp
src/test/controllerscriptenginelegacy_test.cpp
src/test/controlobjecttest.cpp
src/test/controlobjectaliastest.cpp
Expand Down
614 changes: 614 additions & 0 deletions res/controllers/Traktor Kontrol S4 MK3.hid.xml

Large diffs are not rendered by default.

113 changes: 62 additions & 51 deletions res/controllers/Traktor-Kontrol-S4-MK3.js
Original file line number Diff line number Diff line change
Expand Up @@ -40,95 +40,102 @@ const KeyboardColors = [

/*
* USER CONFIGURABLE SETTINGS
* Adjust these to your liking
* Change settings in the preferences
*/

const DeckColors = [
LedColors.red,
LedColors.blue,
LedColors.yellow,
LedColors.purple,
LedColors[engine.getSetting("deckA")] || LedColors.red,
LedColors[engine.getSetting("deckB")] || LedColors.blue,
LedColors[engine.getSetting("deckC")] || LedColors.yellow,
LedColors[engine.getSetting("deckD")] || LedColors.purple,
];

const LibrarySortableColumns = [
script.LIBRARY_COLUMNS.ARTIST,
script.LIBRARY_COLUMNS.TITLE,
script.LIBRARY_COLUMNS.BPM,
script.LIBRARY_COLUMNS.KEY,
script.LIBRARY_COLUMNS.DATETIME_ADDED,
];
engine.getSetting("librarySortableColumns1Value"),
engine.getSetting("librarySortableColumns2Value"),
engine.getSetting("librarySortableColumns3Value"),
engine.getSetting("librarySortableColumns4Value"),
engine.getSetting("librarySortableColumns5Value"),
engine.getSetting("librarySortableColumns6Value"),
].map(c => parseInt(c)).filter(c => c); // Filter '0' column, equivalent to '---' value in the UI or disabled
acolombier marked this conversation as resolved.
Show resolved Hide resolved

const LoopWheelMoveFactor = 50;
const LoopEncoderMoveFactor = 500;
const LoopEncoderShiftmoveFactor = 2500;
const LoopWheelMoveFactor = engine.getSetting("loopWheelMoveFactor") || 50;
const LoopEncoderMoveFactor = engine.getSetting("loopEncoderMoveFactor") || 500;
const LoopEncoderShiftMoveFactor = engine.getSetting("loopEncoderShiftMoveFactor") || 2500;

const TempoFaderSoftTakeoverColorLow = LedColors.white;
const TempoFaderSoftTakeoverColorHigh = LedColors.green;
const TempoFaderSoftTakeoverColorLow = LedColors[engine.getSetting("tempoFaderSoftTakeoverColorLow")] || LedColors.white;
const TempoFaderSoftTakeoverColorHigh = LedColors[engine.getSetting("tempoFaderSoftTakeoverColorHigh")] || LedColors.green;

// Define whether or not to keep LED that have only one color (reverse, flux, play, shift) dimmed if they are inactive.
// 'true' will keep them dimmed, 'false' will turn them off. Default: true
const KeepLEDWithOneColorDimedWhenInactive = true;
const InactiveLightsAlwaysBacklit = !!engine.getSetting("inactiveLightsAlwaysBacklit");

// Keep both deck select buttons backlit and do not fully turn off the inactive deck button.
// 'true' will keep the unseclected deck dimmed, 'false' to fully turn it off. Default: true
const KeepDeckSelectDimmed = true;
// 'true' will keep the unselected deck dimmed, 'false' to fully turn it off. Default: true
const DeckSelectAlwaysBacklit = !!engine.getSetting("deckSelectAlwaysBacklit");

// Define whether the keylock is mapped when doing "shift+master" (on press) or "shift+sync" (on release since long push copies the key)".
// 'true' will use "sync+master", 'false' will use "shift+sync". Default: false
const UseKeylockOnMaster = false;
const UseKeylockOnMaster = !!engine.getSetting("useKeylockOnMaster");

// Define whether the grid button would blink when the playback is going over a detcted beat. Can help to adjust beat grid.
// Define whether the grid button would blink when the playback is going over a detected beat. Can help to adjust beat grid.
// Default: false
const GridButtonBlinkOverBeat = false;
const GridButtonBlinkOverBeat = !!engine.getSetting("gridButtonBlinkOverBeat");

// Wheel led blinking if reaching the end of track warning (default 30 seconds, can be changed in the settings, under "Waveforms" > "End of track warning").
// Default: true
const WheelLedBlinkOnTrackEnd = true;
const WheelLedBlinkOnTrackEnd = !!engine.getSetting("wheelLedBlinkOnTrackEnd");

// When shifting either decks, the mixer will control microphones or auxiliary lines. If there is both a mic and an configure on the same channel, the mixer will control the auxiliary.
// Default: false
const MixerControlsMixAuxOnShift = false;
const MixerControlsMixAuxOnShift = !!engine.getSetting("mixerControlsMicAuxOnShift");

// Define how many wheel moves are sampled to compute the speed. The more you have, the more the speed is accurate, but the
// less responsive it gets in Mixxx. Default: 5
const WheelSpeedSample = 3;
const WheelSpeedSample = engine.getSetting("wheelSpeedSample") || 5;

// Make the sampler tab a beatlooproll tab instead
// Default: false
const UseBeatloopRollInsteadOfSampler = false;
const UseBeatloopRollInsteadOfSampler = !!engine.getSetting("useBeatloopRollInsteadOfSampler");

// Predefined beatlooproll sizes. Note that if you use AddLoopHalveAndDoubleOnBeatloopRollTab, the first and
// last size will be ignored
const BeatLoopRolls = [1/16, 1/8, 1/4, 1/2, 1, 2, 4, 8];
const BeatLoopRolls = [
engine.getSetting("beatLoopRollsSize1") || 1/8,
engine.getSetting("beatLoopRollsSize2") || 1/4,
engine.getSetting("beatLoopRollsSize3") || 1/2,
engine.getSetting("beatLoopRollsSize4") || 1,
engine.getSetting("beatLoopRollsSize5") || 2,
engine.getSetting("beatLoopRollsSize6") || 4,
engine.getSetting("beatLoopRollsSize7") || "half",
engine.getSetting("beatLoopRollsSize8") || "double"
];

// Make the two last button on the beatlooproll pad halve or double the loop size. This will take away the 1/16 and 8 loop size.
// Default: true
const AddLoopHalveAndDoubleOnBeatloopRollTab = true;

// Define the speed of the jogwheel. This will impact the speed of the LED playback indicator, the sratch, and the speed of
// the motor if enable. Recommended value are 33 + 1/3 or 45.
// Default: 33 + 1/3
const BaseRevolutionsPerMinute = 33 + 1/3;
const BaseRevolutionsPerMinute = engine.getSetting("baseRevolutionsPerMinute") || 33 + 1/3;

// Define whether or not to use motors.
// This is a BETA feature! Please use at your own risk. Setting this off means that below settings are inactive
// Default: false
const UseMotors = false;
const UseMotors = !!engine.getSetting("useMotors");

// Define how many wheel moves are sampled to compute the speed when using the motor. This is helpful to mitigate delay that
// occurs in communication as well as Mixxx limitation to 20ms latency.
// The more you have, the more the speed is accurate.
// less responsive it gets in Mixxx. Default: 20
const TurnTableSpeedSample = 20;
const TurnTableSpeedSample = engine.getSetting("turnTableSpeedSample") || 20;

// Define how much the wheel will resist. It is a similar setting that the Grid+Wheel in Tracktor
// Value must defined between 0 to 1. 0 is very tight, 1 is very loose.
// Default: 0.5
const TightnessFactor = 0.5;
const TightnessFactor = engine.getSetting("tightnessFactor") || 0.5;

// Define how much force can the motor use. This defines how much the wheel will "fight" you when you block it in TT mode
// This will also affect how quick the wheel starts spinning when enabling motor mode, or starting a deck with motor mode on
const MaxWheelForce = 25000; // Traktor seems to cap the max value at 60000, which just sounds insane
const MaxWheelForce = engine.getSetting("maxWheelForce") || 25000; // Traktor seems to cap the max value at 60000, which just sounds insane



Expand Down Expand Up @@ -699,7 +706,7 @@ class HotcueButton extends PushButton {
if (this.number === undefined || !Number.isInteger(this.number) || this.number < 1 || this.number > 32) {
throw Error("HotcueButton must have a number property of an integer between 1 and 32");
}
this.outKey = `hotcue_${this.number}_enabled`;
this.outKey = `hotcue_${this.number}_status`;
this.colorKey = `hotcue_${this.number}_color`;
this.outConnect();
}
Expand Down Expand Up @@ -815,8 +822,16 @@ class BeatLoopRollButton extends TriggerButton {
if (options.number === undefined || !Number.isInteger(options.number) || options.number < 0 || options.number > 7) {
throw Error("BeatLoopRollButton must have a number property of an integer between 0 and 7");
}
if (options.number <= 5 || !AddLoopHalveAndDoubleOnBeatloopRollTab) {
options.key = "beatlooproll_"+BeatLoopRolls[AddLoopHalveAndDoubleOnBeatloopRollTab ? options.number + 1 : options.number]+"_activate";
if (BeatLoopRolls[options.number] === "half") {
options.key = "loop_halve";
} else if (BeatLoopRolls[options.number] === "double") {
options.key = "loop_double";
} else {
const size = parseFloat(BeatLoopRolls[options.number]);
if (isNaN(size)) {
throw Error(`BeatLoopRollButton ${options.number}'s size "${BeatLoopRolls[options.number]}" is invalid. Must be a float, or the literal 'half' or 'double'`);
}
options.key = `beatlooproll_${size}_activate`;
options.onShortPress = function() {
if (!this.deck.beatloopSize) {
this.deck.beatloopSize = engine.getValue(this.group, "beatloop_size");
Expand All @@ -830,10 +845,6 @@ class BeatLoopRollButton extends TriggerButton {
this.deck.beatloopSize = undefined;
}
};
} else if (options.number === 6) {
options.key = "loop_halve";
} else {
options.key = "loop_double";
}
super(options);
if (this.deck === undefined) {
Expand All @@ -843,7 +854,7 @@ class BeatLoopRollButton extends TriggerButton {
this.outConnect();
}
output(value) {
if (this.number <= 5 || !AddLoopHalveAndDoubleOnBeatloopRollTab) {
if (this.key.startsWith("beatlooproll_")) {
this.send(LedColors.white + (value ? this.brightnessOn : this.brightnessOff));
} else {
this.send(this.color);
Expand Down Expand Up @@ -1522,7 +1533,7 @@ class S4Mk3Deck extends Deck {
super(decks, colors);

this.playButton = new PlayButton({
output: KeepLEDWithOneColorDimedWhenInactive ? undefined : Button.prototype.uncoloredOutput
output: InactiveLightsAlwaysBacklit ? undefined : Button.prototype.uncoloredOutput
});

this.cueButton = new CueButton({
Expand Down Expand Up @@ -1624,7 +1635,7 @@ class S4Mk3Deck extends Deck {
shift: function() {
this.setKey("loop_enabled");
},
output: KeepLEDWithOneColorDimedWhenInactive ? undefined : Button.prototype.uncoloredOutput,
output: InactiveLightsAlwaysBacklit ? undefined : Button.prototype.uncoloredOutput,
onShortRelease: function() {
if (!this.shifted) {
engine.setValue(this.group, this.key, false);
Expand Down Expand Up @@ -1708,7 +1719,7 @@ class S4Mk3Deck extends Deck {
}
}
},
output: KeepLEDWithOneColorDimedWhenInactive ? undefined : Button.prototype.uncoloredOutput,
output: InactiveLightsAlwaysBacklit ? undefined : Button.prototype.uncoloredOutput,
onShortRelease: function() {
if (!this.shifted) {
engine.setValue(this.group, this.key, false);
Expand Down Expand Up @@ -1820,7 +1831,7 @@ class S4Mk3Deck extends Deck {
this.deck.switchDeck(Deck.groupForNumber(decks[0]));
this.outReport.data[io.deckButtonOutputByteOffset] = colors[0] + this.brightnessOn;
// turn off the other deck selection button's LED
this.outReport.data[io.deckButtonOutputByteOffset + 1] = KeepDeckSelectDimmed ? colors[1] + this.brightnessOff : 0;
this.outReport.data[io.deckButtonOutputByteOffset + 1] = DeckSelectAlwaysBacklit ? colors[1] + this.brightnessOff : 0;
this.outReport.send();
}
},
Expand All @@ -1831,7 +1842,7 @@ class S4Mk3Deck extends Deck {
if (value) {
this.deck.switchDeck(Deck.groupForNumber(decks[1]));
// turn off the other deck selection button's LED
this.outReport.data[io.deckButtonOutputByteOffset] = KeepDeckSelectDimmed ? colors[0] + this.brightnessOff : 0;
this.outReport.data[io.deckButtonOutputByteOffset] = DeckSelectAlwaysBacklit ? colors[0] + this.brightnessOff : 0;
this.outReport.data[io.deckButtonOutputByteOffset + 1] = colors[1] + this.brightnessOn;
this.outReport.send();
}
Expand All @@ -1840,12 +1851,12 @@ class S4Mk3Deck extends Deck {

// set deck selection button LEDs
outReport.data[io.deckButtonOutputByteOffset] = colors[0] + Button.prototype.brightnessOn;
outReport.data[io.deckButtonOutputByteOffset + 1] = KeepDeckSelectDimmed ? colors[1] + Button.prototype.brightnessOff : 0;
outReport.data[io.deckButtonOutputByteOffset + 1] = DeckSelectAlwaysBacklit ? colors[1] + Button.prototype.brightnessOff : 0;
outReport.send();

this.shiftButton = new PushButton({
deck: this,
output: KeepLEDWithOneColorDimedWhenInactive ? undefined : Button.prototype.uncoloredOutput,
output: InactiveLightsAlwaysBacklit ? undefined : Button.prototype.uncoloredOutput,
unshift: function() {
this.output(false);
},
Expand Down Expand Up @@ -1924,7 +1935,7 @@ class S4Mk3Deck extends Deck {
deck: this,
onChange: function(right) {
if (this.deck.wheelMode === wheelModes.loopIn || this.deck.wheelMode === wheelModes.loopOut) {
const moveFactor = this.shifted ? LoopEncoderShiftmoveFactor : LoopEncoderMoveFactor;
const moveFactor = this.shifted ? LoopEncoderShiftMoveFactor : LoopEncoderMoveFactor;
const valueIn = engine.getValue(this.group, "loop_start_position") + (right ? moveFactor : -moveFactor);
const valueOut = engine.getValue(this.group, "loop_end_position") + (right ? moveFactor : -moveFactor);
engine.setValue(this.group, "loop_start_position", valueIn);
Expand Down
20 changes: 18 additions & 2 deletions src/controllers/legacycontrollersettings.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -99,7 +99,9 @@ QWidget* LegacyControllerBooleanSetting::buildWidget(
}

QWidget* LegacyControllerBooleanSetting::buildInputWidget(QWidget* pParent) {
auto* pCheckBox = new QCheckBox(label(), pParent);
auto pWidget = make_parented<QWidget>(pParent);

auto* pCheckBox = new QCheckBox(pWidget);
pCheckBox->setSizePolicy(QSizePolicy::Maximum, QSizePolicy::Fixed);
if (m_editedValue) {
pCheckBox->setCheckState(Qt::Checked);
Expand All @@ -118,7 +120,21 @@ QWidget* LegacyControllerBooleanSetting::buildInputWidget(QWidget* pParent) {
emit changed();
});

return pCheckBox;
auto pLabelWidget = make_parented<QLabel>(pWidget);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

what is this change for?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This change was requried as the QCheckBox built-in label doesn't support rich text. Happy to move that to an other PR is we are happy about the way it looks with rich text (that is the emphasis on button )
image

pLabelWidget->setSizePolicy(QSizePolicy::Preferred, QSizePolicy::Maximum);
pLabelWidget->setText(label());

QBoxLayout* pLayout = new QHBoxLayout();

pLayout->addWidget(pCheckBox);
pLayout->addWidget(pLabelWidget);

pLayout->setStretch(0, 3);
pLayout->setStretch(1, 1);

pWidget->setLayout(pLayout);

return pWidget;
}

bool LegacyControllerBooleanSetting::match(const QDomElement& element) {
Expand Down
5 changes: 5 additions & 0 deletions src/controllers/legacycontrollersettings.h
Original file line number Diff line number Diff line change
Expand Up @@ -359,6 +359,10 @@ class LegacyControllerEnumSetting
return QJSValue(stringify());
}

const QList<std::tuple<QString, QString>>& options() const {
return m_options;
}

QString stringify() const override {
return std::get<0>(m_options.value(static_cast<int>(m_savedValue)));
}
Expand Down Expand Up @@ -416,6 +420,7 @@ class LegacyControllerEnumSetting
size_t m_editedValue;

friend class LegacyControllerMappingSettingsTest_enumSettingEditing_Test;
friend class ControllerS4MK3SettingTest_ensureLibrarySettingValueAndEnumEquals;
};

template<>
Expand Down
79 changes: 79 additions & 0 deletions src/test/controllers/controller_columnid_regression_test.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
/*
This test case is used to ensure that hardcoded CO value in the the settings
definition matches with Mixxx value and will help detecting regression if they
are ever updated.

Currently, the S4 MK3 is referencing library column ID in its setting, so this
test ensure that the value always matches with the Mixxx spec. New controllers
can be added by duplicated the `ensureS4MK3` case and adapt as needed
*/
#include "controllers/legacycontrollermapping.h"
#include "controllers/legacycontrollermappingfilehandler.h"
#include "library/trackmodel.h"
#include "test/mixxxtest.h"
#include "util/time.h"

class ControllerLibraryColumnIDRegressionTest : public MixxxTest {
protected:
void SetUp() override {
mixxx::Time::setTestMode(true);
mixxx::Time::setTestElapsedTime(mixxx::Duration::fromMillis(10));
}

void TearDown() override {
mixxx::Time::setTestMode(false);
}

static QHash<QString, TrackModel::SortColumnId> COLUMN_MAPPING;
};

QHash<QString, TrackModel::SortColumnId>
ControllerLibraryColumnIDRegressionTest::COLUMN_MAPPING = {
{"Artist", TrackModel::SortColumnId::Artist},
{"Title", TrackModel::SortColumnId::Title},
{"Album", TrackModel::SortColumnId::Album},
{"Album Artist", TrackModel::SortColumnId::AlbumArtist},
{"Year", TrackModel::SortColumnId::Year},
{"Genre", TrackModel::SortColumnId::Genre},
{"Composer", TrackModel::SortColumnId::Composer},
{"Grouping", TrackModel::SortColumnId::Grouping},
{"Track Number", TrackModel::SortColumnId::TrackNumber},
{"File Type", TrackModel::SortColumnId::FileType},
{"Native Location", TrackModel::SortColumnId::NativeLocation},
{"Comment", TrackModel::SortColumnId::Comment},
{"Duration", TrackModel::SortColumnId::Duration},
{"Bitrate", TrackModel::SortColumnId::BitRate},
{"BPM", TrackModel::SortColumnId::Bpm},
{"Replay Gain", TrackModel::SortColumnId::ReplayGain},
{"Datetime Added", TrackModel::SortColumnId::DateTimeAdded},
{"Times Played", TrackModel::SortColumnId::TimesPlayed},
{"Rating", TrackModel::SortColumnId::Rating},
{"Key", TrackModel::SortColumnId::Key},
// More mapping can be added here if needed.
// NOTE: If some of the missing value are referenced in a
// controller setting, test case will fail.
};

TEST_F(ControllerLibraryColumnIDRegressionTest, ensureS4MK3) {
std::shared_ptr<LegacyControllerMapping> pMapping =
LegacyControllerMappingFileHandler::loadMapping(
QFileInfo("res/controllers/Traktor Kontrol S4 MK3.hid.xml"), QDir());
EXPECT_TRUE(pMapping);
auto settings = pMapping->getSettings();
EXPECT_TRUE(!settings.isEmpty());

const int expectedSettingCount = 6; // Number of settings using library count.
int count = 0;
for (const auto& setting : settings) {
if (!setting->variableName().startsWith("librarySortableColumns")) {
continue;
}
auto pEnum = std::dynamic_pointer_cast<LegacyControllerEnumSetting>(setting);
EXPECT_TRUE(pEnum);
for (const auto& opt : pEnum->options()) {
EXPECT_EQ(static_cast<int>(COLUMN_MAPPING[std::get<0>(opt)]), std::get<1>(opt).toInt());
}
count++;
}
EXPECT_EQ(count, expectedSettingCount);
}