Skip to content

Commit

Permalink
Merge pull request #12995 from acolombier/chore/add-setting-to-trakto…
Browse files Browse the repository at this point in the history
…r-s4mk3

feat: add setting definition for Traktor S4 MK3
  • Loading branch information
ywwg committed Mar 31, 2024
2 parents 2145f5f + a89e141 commit 9f68007
Show file tree
Hide file tree
Showing 6 changed files with 779 additions and 53 deletions.
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

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);
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);
}

0 comments on commit 9f68007

Please sign in to comment.