From 634592117d83befe5d2cfcf6f216bcdb6e5c5097 Mon Sep 17 00:00:00 2001 From: Antoine C Date: Sun, 15 Sep 2024 01:57:38 +0100 Subject: [PATCH 1/9] feat: add screen rendering for S4Mk3 --- .../Traktor Kontrol S4 MK3.bulk.xml | 945 ++++++++++++++++++ .../Traktor Kontrol S4 MK3.hid.xml | 12 +- res/controllers/Traktor-Kontrol-S4-MK3.js | 870 ++++++++++++---- .../TraktorKontrolS4MK3Screens.qml | 217 ++++ .../AdvancedScreen/Overlays/TopControls.qml | 348 +++++++ .../Waveform/WaveformContainer.qml | 234 +++++ .../S4MK3/BPMIndicator.qml | 77 ++ .../S4MK3/HotcuePoint.qml | 199 ++++ .../S4MK3/KeyIndicator.qml | 127 +++ .../S4MK3/Keyboard.qml | 140 +++ .../S4MK3/LoopSizeIndicator.qml | 75 ++ .../S4MK3/OnAirTrack.qml | 81 ++ .../S4MK3/Progression.qml | 53 + .../S4MK3/SplashOff.qml | 15 + .../S4MK3/StockScreen.qml | 634 ++++++++++++ .../S4MK3/TimeAndBeatloopIndicator.qml | 107 ++ .../S4MK3/WaveformOverview.qml | 165 +++ .../TraktorKontrolS4MK3Screens/S4MK3/qmldir | 12 + res/qml/WaveformRow.qml | 5 +- src/controllers/bulk/bulksupported.h | 1 + tools/README | 8 + tools/clang_format.py | 2 +- tools/traktor_s4_mk3_screen_test.c | 110 ++ 23 files changed, 4215 insertions(+), 222 deletions(-) create mode 100644 res/controllers/Traktor Kontrol S4 MK3.bulk.xml create mode 100644 res/controllers/TraktorKontrolS4MK3Screens.qml create mode 100755 res/controllers/TraktorKontrolS4MK3Screens/S4MK3/AdvancedScreen/Overlays/TopControls.qml create mode 100755 res/controllers/TraktorKontrolS4MK3Screens/S4MK3/AdvancedScreen/Waveform/WaveformContainer.qml create mode 100755 res/controllers/TraktorKontrolS4MK3Screens/S4MK3/BPMIndicator.qml create mode 100644 res/controllers/TraktorKontrolS4MK3Screens/S4MK3/HotcuePoint.qml create mode 100755 res/controllers/TraktorKontrolS4MK3Screens/S4MK3/KeyIndicator.qml create mode 100644 res/controllers/TraktorKontrolS4MK3Screens/S4MK3/Keyboard.qml create mode 100755 res/controllers/TraktorKontrolS4MK3Screens/S4MK3/LoopSizeIndicator.qml create mode 100644 res/controllers/TraktorKontrolS4MK3Screens/S4MK3/OnAirTrack.qml create mode 100755 res/controllers/TraktorKontrolS4MK3Screens/S4MK3/Progression.qml create mode 100644 res/controllers/TraktorKontrolS4MK3Screens/S4MK3/SplashOff.qml create mode 100644 res/controllers/TraktorKontrolS4MK3Screens/S4MK3/StockScreen.qml create mode 100755 res/controllers/TraktorKontrolS4MK3Screens/S4MK3/TimeAndBeatloopIndicator.qml create mode 100755 res/controllers/TraktorKontrolS4MK3Screens/S4MK3/WaveformOverview.qml create mode 100644 res/controllers/TraktorKontrolS4MK3Screens/S4MK3/qmldir create mode 100644 tools/traktor_s4_mk3_screen_test.c diff --git a/res/controllers/Traktor Kontrol S4 MK3.bulk.xml b/res/controllers/Traktor Kontrol S4 MK3.bulk.xml new file mode 100644 index 000000000000..c7bc44712ea7 --- /dev/null +++ b/res/controllers/Traktor Kontrol S4 MK3.bulk.xml @@ -0,0 +1,945 @@ + + + + Traktor Kontrol S4 MK3 (Screens) + A. Colombier + Mapping for Traktor Kontrol S4 MK3 screens + native_instruments_traktor_kontrol_s4_mk3 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/res/controllers/Traktor Kontrol S4 MK3.hid.xml b/res/controllers/Traktor Kontrol S4 MK3.hid.xml index a254772e6c50..4efc26d70413 100644 --- a/res/controllers/Traktor Kontrol S4 MK3.hid.xml +++ b/res/controllers/Traktor Kontrol S4 MK3.hid.xml @@ -10,6 +10,16 @@ + + + - + diff --git a/res/controllers/Traktor-Kontrol-S4-MK3.js b/res/controllers/Traktor-Kontrol-S4-MK3.js index 7d4f7c3d8406..43277a9f1f5c 100644 --- a/res/controllers/Traktor-Kontrol-S4-MK3.js +++ b/res/controllers/Traktor-Kontrol-S4-MK3.js @@ -21,6 +21,30 @@ const LedColors = { white: 68, }; +const LedColorMap = { + 0xCC0000: LedColors.red, + 0xCC5E00: LedColors.carrot, + 0xCC7800: LedColors.orange, + 0xCC9200: LedColors.honey, + + 0xCCCC00: LedColors.yellow, + 0x81CC00: LedColors.lime, + 0x00CC00: LedColors.green, + 0x00CC49: LedColors.aqua, + + 0x00CCCC: LedColors.celeste, + 0x0091CC: LedColors.sky, + 0x0000CC: LedColors.blue, + 0xCC00CC: LedColors.purple, + + 0xAD65FF: LedColors.fuscia, + 0xCC0079: LedColors.magenta, + 0xCC477E: LedColors.azalea, + 0xCC4761: LedColors.salmon, + + 0xCCCCCC: LedColors.white, +}; + // This define the sequence of color to use for pad button when in keyboard mode. This should make them look like an actual keyboard keyboard octave, except for C, which is green to help spotting it. const KeyboardColors = [ @@ -145,6 +169,8 @@ const SoftwareMixerHeadphone = !!engine.getSetting("softwareMixerHeadphone"); // Define custom default layout used by the pads, instead of intro/outro and first 4 hotcues. const DefaultPadLayout = engine.getSetting("defaultPadLayout"); +// Whether or not to use the ShareDataAPI, available in the PR 12199. +const UseSharedDataAPI = engine.getSetting("useSharedDataAPI"); // The LEDs only support 16 base colors. Adding 1 in addition to // the normal 2 for Button.prototype.brightnessOn changes the color @@ -189,6 +215,60 @@ const SamplerCrossfaderAssign = true; const MotorWindUpMilliseconds = 1200; const MotorWindDownMilliseconds = 900; +/* + * Kontrol S4 Mk3 hardware-specific constants + */ +const wheelRelativeMax = 2 ** 32 - 1; +const wheelAbsoluteMax = 2879; + +const wheelTimerMax = 2 ** 32 - 1; +const wheelTimerTicksPerSecond = 100000000; // One tick every 10ns + +const baseRevolutionsPerSecond = BaseRevolutionsPerMinute / 60; +const wheelTicksPerTimerTicksToRevolutionsPerSecond = wheelTimerTicksPerSecond / wheelAbsoluteMax; + +// The active tab ID. This is used when SharedDataAPI is active, to communicate with the screens which tab is currently selected. +const ActiveTabPadID = { + record: 8, + samples: 4, + mute: 7, + stems: 5, + cue: 11, +}; + +const wheelLEDmodes = { + off: 0, + dimFlash: 1, + spot: 2, + ringFlash: 3, + dimSpot: 4, + individuallyAddressable: 5, // set byte 4 to 0 and set byes 8 - 40 to color values +}; + +// The mode available, which the wheel can be used for. +const wheelModes = { + jog: 0, + vinyl: 1, + motor: 2, + loopIn: 3, + loopOut: 4, +}; + +const moveModes = { + beat: 0, + bpm: 1, + grid: 2, + keyboard: 3, + hotcueColor: 4, +}; + +// tracks state across input reports +let wheelTimer = null; +// This is a global variable so the S4Mk3Deck Components have access +// to it and it is guaranteed to be calculated before processing +// input for the Components. +let wheelTimerDelta = 0; + /* * HID report parsing library */ @@ -328,13 +408,13 @@ class Component { this.send(value); } outConnect() { - if (this.outKey !== undefined && this.group !== undefined) { + if (this.outKey !== undefined && this.group !== undefined && this.outConnections.length === 0) { const connection = engine.makeConnection(this.group, this.outKey, this.output.bind(this)); // This is useful for case where effect would have been fully disabled in Mixxx. This appears to be the case during unit tests. if (connection) { - this.outConnections[0] = connection; + this.outConnections.push(connection); } else { - console.warn(`Unable to connect ${this.group}.${this.outKey}' to the controller output. The control appears to be unavailable.`); + console.warn(`Unable to connect '${this.group}.${this.outKey}' to the controller output. The control appears to be unavailable.`); } } } @@ -346,6 +426,7 @@ class Component { } outTrigger() { for (const connection of this.outConnections) { + if (!connection) { continue; } connection.trigger(); } } @@ -373,6 +454,9 @@ class ComponentContainer extends Component { } reconnectComponents(callback) { for (const component of this) { + if (typeof component.unshift === "function" && component.unshift.length === 0) { + component.unshift(); + } if (typeof component.outDisconnect === "function" && component.outDisconnect.length === 0) { component.outDisconnect(); } @@ -429,6 +513,7 @@ class Deck extends ComponentContainer { this.color = colors[0]; } this.secondDeckModes = null; + this.selectedHotcue = null; } toggleDeck() { if (this.decks === undefined) { @@ -441,14 +526,28 @@ class Deck extends ComponentContainer { newDeckIndex = 0; } - this.switchDeck(Deck.groupForNumber(this.decks[newDeckIndex])); + this.switchDeck(this.decks[newDeckIndex]); } - switchDeck(newGroup) { + switchDeck(newDeck) { + const newGroup = Deck.groupForNumber(newDeck); + + switch (this.moveMode) { + case moveModes.beat: + case moveModes.bpm: + case moveModes.grid: + case moveModes.hotcueColor: + this.moveMode = null; + this.selectedHotcue = null; + break; + } + const currentModes = { wheelMode: this.wheelMode, moveMode: this.moveMode, }; + this.selectedStem.fill(false); + engine.setValue(this.group, "scratch2_enable", false); this.group = newGroup; this.color = this.groupsToColors[newGroup]; @@ -470,12 +569,22 @@ class Deck extends ComponentContainer { } else if (component.group.search(script.eqRegEx) !== -1) { component.group = `[EqualizerRack1_${newGroup}_Effect1]`; } else if (component.group.search(script.quickEffectRegEx) !== -1) { - component.group = `[QuickEffectRack1_${newGroup}]`; + component.group = quickFxChannel(newGroup); } component.color = this.groupsToColors[newGroup]; }); this.secondDeckModes = currentModes; + this.currentDeckNumber = newDeck; + + if (!UseSharedDataAPI) { + return; + } + + const data = engine.getSharedData() || {}; + if (!data.group) { return; } + data.group[this.decks[0] === 1 ? "leftdeck":"rightdeck"] = this.group; + engine.setSharedData(data); } static groupForNumber(deckNumber) { return `[Channel${deckNumber}]`; @@ -488,13 +597,19 @@ class Button extends Component { super(options); - if (this.input === undefined) { + if (this.input === undefined + || (typeof this.onLongPress === "function" && this.onLongPress.length === 0) + || (typeof this.onLongRelease === "function" && this.onLongRelease.length === 0) + || (typeof this.onShortPress === "function" && this.onShortPress.length === 0) + || (typeof this.onShortRelease === "function" && this.onShortRelease.length === 0) + || (typeof this.onPress === "function" && this.onPress.length === 0) + || (typeof this.onRelease === "function" && this.onRelease.length === 0)) { this.input = this.defaultInput; - if (typeof this.input === "function" - && this.inReport instanceof HIDInputReport - && this.input.length === 0) { - this.inConnect(); - } + } + if (typeof this.input === "function" + && this.inReport instanceof HIDInputReport + && this.input.length === 0) { + this.inConnect(); } if (this.longPressTimeOutMillis === undefined) { @@ -554,24 +669,32 @@ class Button extends Component { } } defaultInput(pressed) { + this.pressed = pressed; if (pressed) { + this.isShortPress = true; this.isLongPress = false; + if (typeof this.onPress === "function" && this.onPress.length === 0) { this.onPress(); } if (typeof this.onShortPress === "function" && this.onShortPress.length === 0) { this.onShortPress(); } if ((typeof this.onLongPress === "function" && this.onLongPress.length === 0) || (typeof this.onLongRelease === "function" && this.onLongRelease.length === 0)) { this.longPressTimer = engine.beginTimer(this.longPressTimeOutMillis, () => { this.isLongPress = true; + this.isShortPress = false; this.longPressTimer = 0; if (typeof this.onLongPress !== "function") { return; } this.onLongPress(this); }, true); } } else if (this.isLongPress) { + this.isLongPress = false; + if (typeof this.onRelease === "function" && this.onRelease.length === 0) { this.onRelease(); } if (typeof this.onLongRelease === "function" && this.onLongRelease.length === 0) { this.onLongRelease(); } } else { + this.isShortPress = false; if (this.longPressTimer !== 0) { engine.stopTimer(this.longPressTimer); this.longPressTimer = 0; } + if (typeof this.onRelease === "function" && this.onRelease.length === 0) { this.onRelease(); } if (typeof this.onShortRelease === "function" && this.onShortRelease.length === 0) { this.onShortRelease(); } } } @@ -610,8 +733,6 @@ class TriggerButton extends Button { class PowerWindowButton extends Button { constructor(options) { super(options); - this.isLongPressed = false; - this.longPressTimer = 0; } onShortPress() { script.toggleControl(this.group, this.inKey); @@ -717,8 +838,21 @@ class HotcueButton extends PushButton { this.inKey = `hotcue_${this.number}_clear`; } input(pressed) { - engine.setValue(this.group, "scratch2_enable", false); - engine.setValue(this.group, this.inKey, pressed); + if (this.deck.moveMode === moveModes.hotcueColor) { + this.deck.selectedHotcue = pressed ? this.number : null; + if (UseSharedDataAPI) { + const data = engine.getSharedData() || {}; + if (!data.selectedHotcue) { return; } + data.selectedHotcue[this.group] = this.deck.selectedHotcue; + engine.setSharedData(data); + } + + } else if (this.deck.libraryPlayButton.pressed) { + engine.setValue(this.deck.libraryPlayButton.group, this.inKey, pressed); + } else { + engine.setValue(this.group, "scratch2_enable", false); + engine.setValue(this.group, this.inKey, pressed); + } } output(value) { if (value) { @@ -728,10 +862,10 @@ class HotcueButton extends PushButton { } } outConnect() { - if (undefined !== this.group) { + if (undefined !== this.group && this.outConnections.length === 0) { const connection0 = engine.makeConnection(this.group, this.outKey, this.output.bind(this)); if (connection0) { - this.outConnections[0] = connection0; + this.outConnections.push(connection0); } else { console.warn(`Unable to connect ${this.group}.${this.outKey}' to the controller output. The control appears to be unavailable.`); } @@ -740,7 +874,7 @@ class HotcueButton extends PushButton { this.output(engine.getValue(this.group, this.outKey)); }); if (connection1) { - this.outConnections[1] = connection1; + this.outConnections.push(connection1); } else { console.warn(`Unable to connect ${this.group}.${this.colorKey}' to the controller output. The control appears to be unavailable.`); } @@ -796,17 +930,17 @@ class KeyboardButton extends PushButton { if (this.number + offset < 1 || this.number + offset > 24) { this.send(0); } else { - this.send(color + (value ? this.brightnessOn : this.brightnessOff)); + this.send(value ? LedColors.yellow : color); } } outConnect() { - if (undefined !== this.group) { + if (undefined !== this.group && this.outConnections.length === 0) { const connection = engine.makeConnection(this.group, "key", (key) => { const offset = this.deck.keyboardOffset - (this.shifted ? 8 : 0); this.output(key === this.number + offset); }); if (connection) { - this.outConnections[0] = connection; + this.outConnections.push(connection); } else { console.warn(`Unable to connect ${this.group}.key' to the controller output. The control appears to be unavailable.`); } @@ -814,6 +948,90 @@ class KeyboardButton extends PushButton { } } +/* + * Represent a pad button that acts as a stem controller. It will be used to mute or unmute a stem or select it for other operation such as volume or quick effect control + */ +class StemButton extends PushButton { + constructor(options) { + super(options); + if (this.number === undefined || !Number.isInteger(this.number) || this.number < 1 || this.number > 4) { + throw Error("StemButton must have a number property of an integer between 1 and 4"); + } + if (this.deck === undefined) { + throw Error("StemButton must have a deck attached to it"); + } + if (this.deck.mixer === undefined) { + throw Error("StemButton must have a deck with a mixer attached to it"); + } + this.color = 0; + this.muted = 0; + this.outConnect(); + } + unshift() { + this.outTrigger(); + } + shift() { + this.outTrigger(); + } + input(pressed) { + if (!this.enabled) { + return; + } + if (this.shifted && pressed) { + script.toggleControl(stemChannel(this.group, this.number), "mute"); + } + if (!this.shifted) { + this.deck.selectedStem[this.number] = pressed; + } + if (!this.shifted && pressed && this.deck.mixer.firstPressedFxSelector !== null) { + const presetNumber = this.deck.mixer.calculatePresetNumber(); + this.color = QuickEffectPresetColors[presetNumber - 1]; + engine.setValue(quickFxChannel(stemChannel(this.group, this.number)), "loaded_chain_preset", presetNumber + 1); + this.deck.mixer.firstPressedFxSelector = null; + this.deck.mixer.secondPressedFxSelector = null; + this.deck.mixer.resetFxSelectorColors(); + } + } + output() { + if (!this.color || !this.enabled) { + this.send(0); + } else { + this.send(this.color + (this.muted ? this.brightnessOff : this.brightnessOn)); + } + } + outConnect() { + if (undefined !== this.group) { + const muteConnection = engine.makeConnection(stemChannel(this.group, this.number), "mute", (mute) => { + this.muted = mute; + this.output(); + }); + if (muteConnection) { + this.outConnections[0] = muteConnection; + } else { + console.warn(`Unable to connect '${stemChannel(this.group, this.number)}.mute' to the controller output. The control appears to be unavailable.`); + } + const colorConnection = engine.makeConnection(stemChannel(this.group, this.number), "color", (color) => { + this.color = this.colorMap.getValueForNearestColor(color); + this.output(); + }); + if (colorConnection) { + this.outConnections[1] = colorConnection; + } else { + console.warn(`Unable to connect '${stemChannel(this.group, this.number)}.color' to the controller output. The control appears to be unavailable.`); + } + const enabledConnection = engine.makeConnection(this.group, "stem_count", (count) => { + this.enabled = count >= this.number; + this.output(); + }); + if (enabledConnection) { + this.outConnections[2] = enabledConnection; + } else { + console.warn(`Unable to connect '${this.group}.stem_count' to the controller output. The control appears to be unavailable.`); + } + } + } +} + /* * Represent a pad button that will trigger a pre-defined beatloop size as set in BeatLoopRolls. */ @@ -833,16 +1051,24 @@ class BeatLoopRollButton extends TriggerButton { } options.key = `beatlooproll_${size}_activate`; options.onShortPress = function() { - if (!this.deck.beatloopSize) { - this.deck.beatloopSize = engine.getValue(this.group, "beatloop_size"); + if (!this.deck.beatloop) { + this.deck.beatloop = { + size: engine.getValue(this.group, "beatloop_size"), + start: engine.getValue(this.group, "loop_start_position"), + end: engine.getValue(this.group, "loop_end_position"), + enabled: engine.getValue(this.group, "loop_enabled"), + }; } engine.setValue(this.group, this.inKey, true); }; options.onShortRelease = function() { engine.setValue(this.group, this.inKey, false); - if (this.deck.beatloopSize) { - engine.setValue(this.group, "beatloop_size", this.deck.beatloopSize); - this.deck.beatloopSize = undefined; + if (this.deck.beatloop) { + engine.setValue(this.group, "loop_start_position", this.deck.beatloop.start); + engine.setValue(this.group, "loop_end_position", this.deck.beatloop.end); + engine.setValue(this.group, "beatloop_size", this.deck.beatloop.size); + engine.setValue(this.group, "loop_enabled", this.deck.beatloop.enabled); + this.deck.beatloop = undefined; } }; } @@ -877,7 +1103,12 @@ class SamplerButton extends Button { onShortPress() { if (!this.shifted) { if (engine.getValue(this.group, "track_loaded") === 0) { - engine.setValue(this.group, "LoadSelectedTrack", 1); + if (this.deck.samplerStemSelection !== null) { + engine.setValue(this.group, "load_selected_track_stems", this.deck.samplerStemSelection); + this.deck.samplerStemSelection = null; + } else { + engine.setValue(this.group, "LoadSelectedTrack", 1); + } } else { engine.setValue(this.group, "cue_gotoandplay", 1); } @@ -909,16 +1140,16 @@ class SamplerButton extends Button { } } outConnect() { - if (undefined !== this.group) { + if (undefined !== this.group && this.outConnections.length === 0) { const connection0 = engine.makeConnection(this.group, "play", this.output.bind(this)); if (connection0) { - this.outConnections[0] = connection0; + this.outConnections.push(connection0); } else { console.warn(`Unable to connect ${this.group}.play' to the controller output. The control appears to be unavailable.`); } const connection1 = engine.makeConnection(this.group, "track_loaded", this.output.bind(this)); if (connection1) { - this.outConnections[1] = connection1; + this.outConnections.push(connection1); } else { console.warn(`Unable to connect ${this.group}.track_loaded' to the controller output. The control appears to be unavailable.`); } @@ -1099,14 +1330,12 @@ class Mixer extends ComponentContainer { this.resetFxSelectorColors(); this.quantizeButton = new Button({ - input: function(pressed) { - if (pressed) { - this.globalQuantizeOn = !this.globalQuantizeOn; - for (let deckIdx = 1; deckIdx <= 4; deckIdx++) { - engine.setValue(`[Channel${deckIdx}]`, "quantize", this.globalQuantizeOn); - } - this.send(this.globalQuantizeOn ? 127 : 0); + onPress: function() { + this.globalQuantizeOn = !this.globalQuantizeOn; + for (let deckIdx = 1; deckIdx <= 4; deckIdx++) { + engine.setValue(`[Channel${deckIdx}]`, "quantize", this.globalQuantizeOn); } + this.send(this.globalQuantizeOn ? 127 : 0); }, globalQuantizeOn: false, inByte: 11, @@ -1289,7 +1518,7 @@ class QuickEffectButton extends Button { if (this.number === undefined || !Number.isInteger(this.number) || this.number < 1) { throw Error("number attribute must be an integer >= 1"); } - this.group = `[QuickEffectRack1_[Channel${this.number}]]`; + this.group = quickFxChannel(`[Channel${this.number}]`); this.outConnect(); } onShortPress() { @@ -1323,16 +1552,16 @@ class QuickEffectButton extends Button { this.outConnections[1].trigger(); } outConnect() { - if (this.group !== undefined) { + if (this.group !== undefined && this.outConnections.length === 0) { const connection0 = engine.makeConnection(this.group, "loaded_chain_preset", this.presetLoaded.bind(this)); if (connection0) { - this.outConnections[0] = connection0; + this.outConnections.push(connection0); } else { console.warn(`Unable to connect ${this.group}.loaded_chain_preset' to the controller output. The control appears to be unavailable.`); } const connection1 = engine.makeConnection(this.group, "enabled", this.output.bind(this)); if (connection1) { - this.outConnections[1] = connection1; + this.outConnections.push(connection1); } else { console.warn(`Unable to connect ${this.group}.enabled' to the controller output. The control appears to be unavailable.`); } @@ -1341,7 +1570,7 @@ class QuickEffectButton extends Button { } /* - * Kontrol S4 Mk3 hardware-specific constants + * Kontrol S4 Mk3 hardware-specific member constants */ Pot.prototype.max = 2 ** 12 - 1; @@ -1349,6 +1578,7 @@ Pot.prototype.inBit = 0; Pot.prototype.inBitLength = 16; Encoder.prototype.inBitLength = 4; +Encoder.prototype.tickDelta = 1 / (2 << Encoder.prototype.inBitLength); // valid range 0 - 3, but 3 makes some colors appear whitish Button.prototype.brightnessOff = 0; @@ -1360,71 +1590,20 @@ Button.prototype.uncoloredOutput = function(value) { const color = (value > 0) ? (this.color || LedColors.white) + this.brightnessOn : LedColors.off; this.send(color); }; -Button.prototype.colorMap = new ColorMapper({ - 0xCC0000: LedColors.red, - 0xCC5E00: LedColors.carrot, - 0xCC7800: LedColors.orange, - 0xCC9200: LedColors.honey, - - 0xCCCC00: LedColors.yellow, - 0x81CC00: LedColors.lime, - 0x00CC00: LedColors.green, - 0x00CC49: LedColors.aqua, - - 0x00CCCC: LedColors.celeste, - 0x0091CC: LedColors.sky, - 0x0000CC: LedColors.blue, - 0xCC00CC: LedColors.purple, - - 0xCC0091: LedColors.fuscia, - 0xCC0079: LedColors.magenta, - 0xCC477E: LedColors.azalea, - 0xCC4761: LedColors.salmon, - - 0xCCCCCC: LedColors.white, -}); - -const wheelRelativeMax = 2 ** 32 - 1; -const wheelAbsoluteMax = 2879; - -const wheelTimerMax = 2 ** 32 - 1; -const wheelTimerTicksPerSecond = 100000000; // One tick every 10ns +Button.prototype.colorMap = new ColorMapper(LedColorMap); -const baseRevolutionsPerSecond = BaseRevolutionsPerMinute / 60; -const wheelTicksPerTimerTicksToRevolutionsPerSecond = wheelTimerTicksPerSecond / wheelAbsoluteMax; - -const wheelLEDmodes = { - off: 0, - dimFlash: 1, - spot: 2, - ringFlash: 3, - dimSpot: 4, - individuallyAddressable: 5, // set byte 4 to 0 and set byes 8 - 40 to color values -}; +/* + * helper function + */ -// The mode available, which the wheel can be used for. -const wheelModes = { - jog: 0, - vinyl: 1, - motor: 2, - loopIn: 3, - loopOut: 4, +const quickFxChannel = (group) => { + return `[QuickEffectRack1_${group}]`; }; -const moveModes = { - beat: 0, - bpm: 1, - grid: 2, - keyboard: 3, +const stemChannel = (group, idx) => { + return `${group.substr(0, group.length - 1)}_Stem${idx + 1}]`; }; -// tracks state across input reports -let wheelTimer = null; -// This is a global variable so the S4Mk3Deck Components have access -// to it and it is guaranteed to be calculated before processing -// input for the Components. -let wheelTimerDelta = 0; - /* * Kontrol S4 Mk3 hardware specific mapping logic */ @@ -1462,14 +1641,14 @@ class S4Mk3EffectUnit extends ComponentContainer { this.group = undefined; this.output(false); }, - input: function(pressed) { + onPress: function() { if (!this.shifted) { for (const index of [0, 1, 2]) { const effectGroup = `[EffectRack1_EffectUnit${unitNumber}_Effect${index + 1}]`; - engine.setValue(effectGroup, "enabled", pressed); + engine.setValue(effectGroup, "enabled", true); } - this.output(pressed); - } else if (pressed) { + this.output(true); + } else { if (this.unit.focusedEffect !== null) { this.unit.setFocusedEffect(null); } else { @@ -1477,6 +1656,15 @@ class S4Mk3EffectUnit extends ComponentContainer { this.shift(); } } + }, + onRelease: function() { + if (!this.shifted) { + for (const index of [0, 1, 2]) { + const effectGroup = `[EffectRack1_EffectUnit${unitNumber}_Effect${index + 1}]`; + engine.setValue(effectGroup, "enabled", false); + } + this.output(false); + } } }); @@ -1748,10 +1936,10 @@ class S4Mk3Deck extends Deck { this.setKey("loop_enabled"); }, outConnect: function() { - if (this.outKey !== undefined && this.group !== undefined) { + if (this.outKey !== undefined && this.group !== undefined && this.outConnections.length === 0) { const connection = engine.makeConnection(this.group, this.outKey, this.output.bind(this)); if (connection) { - this.outConnections[0] = connection; + this.outConnections.push(connection); } else { console.warn(`Unable to connect ${this.group}.${this.outKey}' to the controller output. The control appears to be unavailable.`); } @@ -1769,7 +1957,7 @@ class S4Mk3Deck extends Deck { this.indicator(false); const wheelOutput = new Uint8Array(40).fill(0); wheelOutput[0] = decks[0] - 1; - controller.sendOutputReport(wheelOutput.buffer, null, 50, true); + controller.sendOutputReport(50, wheelOutput.buffer, true); if (!skipRestore) { this.deck.wheelMode = this.previousWheelMode; } @@ -1826,14 +2014,11 @@ class S4Mk3Deck extends Deck { this.output(false); } : undefined, onShortPress: function() { - this.deck.libraryEncoder.gridButtonPressed = true; - if (this.shift) { engine.setValue(this.group, "bpm_tap", true); } }, onLongPress: function() { - this.deck.libraryEncoder.gridButtonPressed = true; this.previousMoveMode = this.deck.moveMode; if (this.shifted) { @@ -1845,7 +2030,6 @@ class S4Mk3Deck extends Deck { this.indicator(true); }, onLongRelease: function() { - this.deck.libraryEncoder.gridButtonPressed = false; if (this.previousMoveMode !== null) { this.deck.moveMode = this.previousMoveMode; this.previousMoveMode = null; @@ -1853,7 +2037,6 @@ class S4Mk3Deck extends Deck { this.indicator(false); }, onShortRelease: function() { - this.deck.libraryEncoder.gridButtonPressed = false; script.triggerControl(this.group, "beats_translate_curpos"); if (this.shift) { @@ -1866,7 +2049,7 @@ class S4Mk3Deck extends Deck { deck: this, input: function(value) { if (value) { - this.deck.switchDeck(Deck.groupForNumber(decks[0])); + this.deck.switchDeck(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] = DeckSelectAlwaysBacklit ? colors[1] + this.brightnessOff : 0; @@ -1878,7 +2061,7 @@ class S4Mk3Deck extends Deck { deck: this, input: function(value) { if (value) { - this.deck.switchDeck(Deck.groupForNumber(decks[1])); + this.deck.switchDeck(decks[1]); // turn off the other deck selection button's LED this.outReport.data[io.deckButtonOutputByteOffset] = DeckSelectAlwaysBacklit ? colors[0] + this.brightnessOff : 0; this.outReport.data[io.deckButtonOutputByteOffset + 1] = colors[1] + this.brightnessOn; @@ -1901,18 +2084,52 @@ class S4Mk3Deck extends Deck { shift: function() { this.output(true); }, - input: function(pressed) { - if (pressed) { - this.deck.shift(); - } else { - this.deck.unshift(); + onPress: function() { + this.deck.shift.call(this.deck); + + if (!UseSharedDataAPI) { + return; } - } + + const data = engine.getSharedData() || {}; + if (!data.shift) { return; } + data.shift[decks[0] === 1 ? "leftdeck":"rightdeck"] = true; + engine.setSharedData(data); + }, + onRelease: function() { + this.deck.unshift.call(this.deck); + + if (!UseSharedDataAPI) { + return; + } + + const data = engine.getSharedData() || {}; + if (!data.shift) { return; } + data.shift[decks[0] === 1 ? "leftdeck":"rightdeck"] = false; + engine.setSharedData(data); + }, }); this.leftEncoder = new Encoder({ deck: this, onChange: function(right) { + if (this.deck.hasSelectedStem()) { + this.deck.selectedStem.forEach((selected, stemIdx) => { + if (!selected) { return; } + + engine.setValue(stemChannel(this.group, stemIdx), "volume", engine.getValue(stemChannel(this.group, stemIdx), "volume") + (right ? this.tickDelta : -this.tickDelta)); + }); + return; + } + + if (this.deck.hasSelectedStem()) { + this.deck.selectedStem.forEach((selected, stemIdx) => { + if (!selected) { return; } + + engine.setValue(stemChannel(this.group, stemIdx), "volume", engine.getValue(stemChannel(this.group, stemIdx), "volume") + (right ? this.tickDelta : -this.tickDelta)); + }); + return; + } switch (this.deck.moveMode) { case moveModes.grid: @@ -1920,18 +2137,34 @@ class S4Mk3Deck extends Deck { break; case moveModes.keyboard: if ( - this.deck.keyboard[0].offset === (right ? 16 : 0) + this.deck.pads[0].offset === (right ? 16 : 0) ) { return; } this.deck.keyboardOffset += (right ? 1 : -1); - this.deck.keyboard.forEach(function(pad) { + this.deck.pads.forEach(function(pad) { pad.outTrigger(); }); break; case moveModes.bpm: script.triggerControl(this.group, right ? "beats_translate_later" : "beats_translate_earlier"); break; + case moveModes.hotcueColor:{ + if (this.deck.selectedHotcue === null) { + return; + } + const currentColor = Button.prototype.colorMap.getValueForNearestColor(engine.getValue(this.deck.group, `hotcue_${this.deck.selectedHotcue}_color`)); + let currentColorIdx = Object.keys(LedColorMap).indexOf(Object.keys(LedColorMap).find(key => LedColorMap[key] === currentColor)); + currentColorIdx = Math.max( + Math.min( + Object.keys(LedColorMap).length - 2, // Last color is reserved for loop hotcue + currentColorIdx + (right ? 1:-1) + ), + 0 + ); + engine.setValue(this.deck.group, `hotcue_${this.deck.selectedHotcue}_color`, Object.keys(LedColorMap)[currentColorIdx]); + break; + } default: if (!this.shifted) { if (!this.deck.leftEncoderPress.pressed) { @@ -1961,17 +2194,50 @@ class S4Mk3Deck extends Deck { } }); this.leftEncoderPress = new PushButton({ - input: function(pressed) { - this.pressed = pressed; - if (pressed) { + deck: this, + onPress: function() { + if (this.deck.hasSelectedStem()) { + this.deck.selectedStem.forEach((selected, stemIdx) => { + if (!selected) { return; } + + engine.setValue(stemChannel(this.group, stemIdx), "volume", engine.getValue(stemChannel(this.group, stemIdx), "volume") === 1.0 ? 0 : 1); + }); + return; + } + if (this.shifted) { script.toggleControl(this.group, "pitch_adjust_set_default"); } + + if (!UseSharedDataAPI) { + return; + } + + + const data = engine.getSharedData() || {}; + if (!data.displayBeatloopSize) { return; } + data.displayBeatloopSize[this.group] = true; + engine.setSharedData(data); }, + onRelease: UseSharedDataAPI ? function() { + const data = engine.getSharedData() || {}; + if (!data.displayBeatloopSize) { return; } + data.displayBeatloopSize[this.group] = false; + engine.setSharedData(data); + } : undefined }); this.rightEncoder = new Encoder({ deck: this, onChange: function(right) { + if (this.deck.hasSelectedStem()) { + this.deck.selectedStem.forEach((selected, stemIdx) => { + if (!selected) { return; } + + engine.setValue(quickFxChannel(stemChannel(this.group, stemIdx)), "super1", engine.getValue(quickFxChannel(stemChannel(this.group, stemIdx)), "super1") + (right ? this.tickDelta : -this.tickDelta)); + }); + return; + } + if (this.deck.wheelMode === wheelModes.loopIn || this.deck.wheelMode === wheelModes.loopOut) { const moveFactor = this.shifted ? LoopEncoderShiftMoveFactor : LoopEncoderMoveFactor; const valueIn = engine.getValue(this.group, "loop_start_position") + (right ? moveFactor : -moveFactor); @@ -1986,8 +2252,14 @@ class S4Mk3Deck extends Deck { } }); this.rightEncoderPress = new PushButton({ - input: function(pressed) { - if (!pressed) { + deck: this, + onPress: function() { + if (this.deck.hasSelectedStem()) { + this.deck.selectedStem.forEach((selected, stemIdx) => { + if (!selected) { return; } + + script.toggleControl(quickFxChannel(stemChannel(this.group, stemIdx)), "enabled"); + }); return; } const loopEnabled = engine.getValue(this.group, "loop_enabled"); @@ -2000,26 +2272,22 @@ class S4Mk3Deck extends Deck { }); this.libraryEncoder = new Encoder({ - libraryPlayButtonPressed: false, - gridButtonPressed: false, - starButtonPressed: false, - libraryViewButtonPressed: false, - libraryPlaylistButtonPressed: false, + deck: this, currentSortedColumnIdx: -1, onChange: function(right) { - if (this.libraryViewButtonPressed) { + if (this.deck.libraryViewButton.pressed) { this.currentSortedColumnIdx = (LibrarySortableColumns.length + this.currentSortedColumnIdx + (right ? 1 : -1)) % LibrarySortableColumns.length; engine.setValue("[Library]", "sort_column", LibrarySortableColumns[this.currentSortedColumnIdx]); - } else if (this.starButtonPressed) { + } else if (this.deck.libraryStarButton.pressed) { if (this.shifted) { // FIXME doesn't exist, feature request needed script.triggerControl(this.group, right ? "track_color_prev" : "track_color_next"); } else { script.triggerControl(this.group, right ? "stars_up" : "stars_down"); } - } else if (this.gridButtonPressed) { + } else if (this.deck.gridButton.pressed) { script.triggerControl(this.group, right ? "waveform_zoom_up" : "waveform_zoom_down"); - } else if (this.libraryPlayButtonPressed) { + } else if (this.deck.libraryPlayButton.pressed) { script.triggerControl("[PreviewDeck1]", right ? "beatjump_16_forward" : "beatjump_16_backward"); } else { // FIXME there is a bug where this action has no effect when the Mixxx window has no focused. https://github.com/mixxxdj/mixxx/issues/11285 @@ -2039,9 +2307,9 @@ class S4Mk3Deck extends Deck { } }); this.libraryEncoderPress = new Button({ - libraryViewButtonPressed: false, + deck: this, onShortPress: function() { - if (this.libraryViewButtonPressed) { + if (this.deck.libraryViewButton.pressed) { script.toggleControl("[Library]", "sort_order"); } else { const currentlyFocusWidget = engine.getValue("[Library]", "focused_widget"); @@ -2049,7 +2317,11 @@ class S4Mk3Deck extends Deck { if (this.shifted && currentlyFocusWidget === 0) { script.triggerControl("[Playlist]", "ToggleSelectedSidebarItem"); } else if (currentlyFocusWidget === 3 || currentlyFocusWidget === 0) { - script.triggerControl(this.group, "LoadSelectedTrack"); + if (this.deck.hasSelectedStem()) { + engine.setValue(this.group, "load_selected_track_stems", this.deck.stemSelection()); + } else { + script.triggerControl(this.group, "LoadSelectedTrack"); + } } else { script.triggerControl("[Library]", "GoToItem"); } @@ -2062,15 +2334,17 @@ class S4Mk3Deck extends Deck { }); this.libraryPlayButton = new PushButton({ group: "[PreviewDeck1]", - libraryEncoder: this.libraryEncoder, - input: function(pressed) { - if (pressed) { - script.triggerControl(this.group, "LoadSelectedTrackAndPlay"); + deck: this, + onPress: function() { + if (this.shifted) { + engine.setValue(this.group, "CloneFromDeck", this.deck.currentDeckNumber); } else { - engine.setValue(this.group, "play", 0); - script.triggerControl(this.group, "eject"); + script.triggerControl(this.group, "LoadSelectedTrackAndPlay"); } - this.libraryEncoder.libraryPlayButtonPressed = pressed; + }, + onRelease: function() { + engine.setValue(this.group, "play", 0); + script.triggerControl(this.group, "eject"); }, outKey: "play", }); @@ -2080,12 +2354,6 @@ class S4Mk3Deck extends Deck { onShortRelease: function() { script.triggerControl(this.group, this.shifted ? "track_color_prev" : "track_color_next"); }, - onLongPress: function() { - this.libraryEncoder.starButtonPressed = true; - }, - onLongRelease: function() { - this.libraryEncoder.starButtonPressed = false; - }, }); // FIXME there is no feature about playlist at the moment, so we use this button to control the context menu, which has playlist control this.libraryPlaylistButton = new Button({ @@ -2098,7 +2366,7 @@ class S4Mk3Deck extends Deck { }); // This is useful for case where effect would have been fully disabled in Mixxx. This appears to be the case during unit tests. if (connection) { - this.outConnections[0] = connection; + this.outConnections.push(connection); } else { console.warn(`Unable to connect ${this.group}.focused_widget' to the controller output. The control appears to be unavailable.`); } @@ -2111,18 +2379,11 @@ class S4Mk3Deck extends Deck { return; } script.toggleControl("[Library]", "show_track_menu"); - this.libraryEncoder.libraryPlayButtonPressed = false; if (currentlyFocusWidget === 4) { engine.setValue("[Library]", "focused_widget", 3); } }, - onShortPress: function() { - this.libraryEncoder.libraryPlayButtonPressed = true; - }, - onLongRelease: function() { - this.libraryEncoder.libraryPlayButtonPressed = false; - }, onLongPress: function() { engine.setValue("[Library]", "clear_search", 1); } @@ -2135,14 +2396,7 @@ class S4Mk3Deck extends Deck { onShortRelease: function() { script.toggleControl(this.group, this.inKey, true); }, - onLongPress: function() { - this.libraryEncoder.libraryViewButtonPressed = true; - this.libraryEncoderPress.libraryViewButtonPressed = true; - }, - onLongRelease: function() { - this.libraryEncoder.libraryViewButtonPressed = false; - this.libraryEncoderPress.libraryViewButtonPressed = false; - } + onLongPress: function() {}, // This is needed to make difference between a shot and long press }); this.keyboardPlayMode = null; @@ -2163,28 +2417,58 @@ class S4Mk3Deck extends Deck { cueBaseName: "outro_end", }), new HotcueButton({ - number: 1 + number: 1, deck: this }), new HotcueButton({ - number: 2 + number: 2, deck: this }), new HotcueButton({ - number: 3 + number: 3, deck: this }), new HotcueButton({ - number: 4 + number: 4, deck: this }) ]; const hotcuePage2 = Array(8).fill({}); const hotcuePage3 = Array(8).fill({}); const samplerOrBeatloopRollPage = Array(8).fill({}); - this.keyboard = Array(8).fill({}); + const keyboard = Array(8).fill({}); + const stem = [ + new StemButton({ + number: 1, + deck: this, + }), + new StemButton({ + number: 2, + deck: this, + }), + new StemButton({ + number: 3, + deck: this, + }), + new StemButton({ + number: 4, + deck: this, + }), + new Component({ + outConnect: function() { this.send(0); }, + }), + new Component({ + outConnect: function() { this.send(0); }, + }), + new Component({ + outConnect: function() { this.send(0); }, + }), + new Component({ + outConnect: function() { this.send(0); }, + }), + ]; let i = 0; /* eslint no-unused-vars: "off" */ for (const pad of hotcuePage2) { // start with hotcue 5; hotcues 1-4 are in defaultPadLayer - hotcuePage2[i] = new HotcueButton({number: i + 1}); - hotcuePage3[i] = new HotcueButton({number: i + 13}); + hotcuePage2[i] = new HotcueButton({number: i + 1, deck: this}); + hotcuePage3[i] = new HotcueButton({number: i + 13, deck: this}); if (UseBeatloopRollInsteadOfSampler) { samplerOrBeatloopRollPage[i] = new BeatLoopRollButton({ number: i, @@ -2201,6 +2485,7 @@ class S4Mk3Deck extends Deck { } samplerOrBeatloopRollPage[i] = new SamplerButton({ number: samplerNumber, + deck: this, }); if (SamplerCrossfaderAssign) { engine.setValue( @@ -2210,7 +2495,7 @@ class S4Mk3Deck extends Deck { ); } } - this.keyboard[i] = new KeyboardButton({ + keyboard[i] = new KeyboardButton({ number: i + 1, deck: this, }); @@ -2219,9 +2504,13 @@ class S4Mk3Deck extends Deck { const switchPadLayer = (deck, newLayer) => { let index = 0; + if (newLayer === samplerOrBeatloopRollPage && deck.hasSelectedStem()) { + deck.samplerStemSelection = deck.stemSelection(); + } for (let pad of deck.pads) { pad.outDisconnect(); pad.inDisconnect(); + const shifted = pad.shifted; pad = newLayer[index]; Object.assign(pad, io.pads[index]); @@ -2235,6 +2524,11 @@ class S4Mk3Deck extends Deck { if (pad.inReport === undefined) { pad.inReport = inReports[1]; } + if (shifted && typeof pad.shift === "function" && pad.shift.length === 0) { + pad.shift(); + } else if (typeof pad.unshift === "function" && pad.unshift.length === 0) { + pad.unshift(); + } pad.outReport = outReport; pad.inConnect(); pad.outConnect(); @@ -2250,6 +2544,7 @@ class S4Mk3Deck extends Deck { hotcuePage3: 2, samplerPage: 3, keyboard: 5, + stem: 6, }; switch (DefaultPadLayout) { case DefaultPadLayoutHotcue: @@ -2272,7 +2567,7 @@ class S4Mk3Deck extends Deck { this.hotcuePadModeButton = new Button({ deck: this, - onShortPress: function() { + onShortRelease: function() { if (!this.shifted) { if (this.deck.currentPadLayer !== this.deck.padLayers.hotcuePage2) { switchPadLayer(this.deck, hotcuePage2); @@ -2289,34 +2584,86 @@ class S4Mk3Deck extends Deck { } }, + onShortPress: UseSharedDataAPI ? function() { + const data = engine.getSharedData() || {}; + if (!data.padsMode) { return; } + data.padsMode[this.deck.group] = ActiveTabPadID.cue; + engine.setSharedData(data); + } : undefined, + onLongPress: function() { + this.previousMoveMode = this.deck.moveMode; + this.deck.moveMode = moveModes.hotcueColor; + + }, + onLongRelease: function() { + this.deck.moveMode = this.previousMoveMode; + this.previousMoveMode = null; + }, // hack to switch the LED color when changing decks outTrigger: function() { this.deck.lightPadMode(); } }); // The record button doesn't have a mapping by default, but you can add yours here - // this.recordPadModeButton = new Button({ - // ... - // }); + this.recordPadModeButton = new Button({ + deck: this, + onShortPress: UseSharedDataAPI ? function() { + const data = engine.getSharedData() || {}; + if (!data.padsMode) { return; } + data.padsMode[this.deck.group] = ActiveTabPadID.record; + engine.setSharedData(data); + this.output(data.scrollingWavefom[this.deck.group]); + } : undefined, + // hack to switch the LED color when changing decks + outTrigger: function() { + this.deck.lightPadMode(); + } + }); this.samplesPadModeButton = new Button({ + pressed: false, deck: this, onShortPress: function() { + engine.setValue(this.deck.group, "loop_anchor", 1); + + if (!UseSharedDataAPI) { + return; + } + const data = engine.getSharedData() || {}; + if (!data.padsMode) { return; } + data.padsMode[this.deck.group] = ActiveTabPadID.samples; + engine.setSharedData(data); + }, + onShortRelease: function() { if (this.deck.currentPadLayer !== this.deck.padLayers.samplerPage) { switchPadLayer(this.deck, samplerOrBeatloopRollPage); - engine.setValue("[Samplers]", "show_samplers", true); + engine.setValue("[Skin]", "show_samplers", true); this.deck.currentPadLayer = this.deck.padLayers.samplerPage; } else { switchPadLayer(this.deck, defaultPadLayer); - engine.setValue("[Samplers]", "show_samplers", false); + engine.setValue("[Skin]", "show_samplers", false); this.deck.currentPadLayer = this.deck.padLayers.defaultLayer; } this.deck.lightPadMode(); + engine.setValue(this.deck.group, "loop_anchor", 0); }, + onLongRelease: function() { + engine.setValue(this.deck.group, "loop_anchor", 0); + } }); // The mute button doesn't have a mapping by default, but you can add yours here - // this.mutePadModeButton = new Button({ - // ... - // }); + this.mutePadModeButton = new Button({ + deck: this, + onShortPress: UseSharedDataAPI ? function() { + const data = engine.getSharedData() || {}; + if (!data.padsMode) { return; } + data.padsMode[this.deck.group] = ActiveTabPadID.mute; + engine.setSharedData(data); + } : undefined, + // hack to switch the LED color when changing decks + outTrigger: function() { + this.deck.lightPadMode(); + } + }); this.stemsPadModeButton = new Button({ deck: this, @@ -2328,6 +2675,12 @@ class S4Mk3Deck extends Deck { } }, onShortPress: function() { + if (UseSharedDataAPI) { + const data = engine.getSharedData() || {}; + if (!data.padsMode) { return; } + data.padsMode[this.deck.group] = ActiveTabPadID.stems; + engine.setSharedData(data); + } if (this.previousMoveMode === null) { this.previousMoveMode = this.deck.moveMode; this.deck.moveMode = moveModes.keyboard; @@ -2338,12 +2691,19 @@ class S4Mk3Deck extends Deck { this.deck.moveMode = this.previousMoveMode; this.previousMoveMode = null; } - if (this.deck.currentPadLayer === this.deck.padLayers.keyboard) { + let targetLayer = this.deck.padLayers.stem; + if (this.shifted) { + targetLayer = this.deck.padLayers.keyboard; + } + if (this.deck.currentPadLayer === targetLayer) { switchPadLayer(this.deck, defaultPadLayer); this.deck.currentPadLayer = this.deck.padLayers.defaultLayer; - } else if (this.deck.currentPadLayer !== this.deck.padLayers.keyboard) { - switchPadLayer(this.deck, this.deck.keyboard); - this.deck.currentPadLayer = this.deck.padLayers.keyboard; + } else if (targetLayer === this.deck.padLayers.stem) { + switchPadLayer(this.deck, stem); + this.deck.currentPadLayer = targetLayer; + } else if (targetLayer === this.deck.padLayers.keyboard) { + switchPadLayer(this.deck, keyboard); + this.deck.currentPadLayer = targetLayer; } this.deck.lightPadMode(); }, @@ -2360,21 +2720,22 @@ class S4Mk3Deck extends Deck { }); this.wheelMode = wheelModes.vinyl; - this.turntableButton = UseMotors ? new Button({ + this.turntableButton = new Button({ deck: this, - input: function(press) { - if (press) { - this.deck.reverseButton.loopModeOff(true); - this.deck.fluxButton.loopModeOff(true); - if (this.deck.wheelMode === wheelModes.motor) { - this.deck.wheelMode = wheelModes.vinyl; - engine.setValue(this.group, "scratch2_enable", false); - } else { - this.deck.wheelMode = wheelModes.motor; - const group = this.group; - } - this.outTrigger(); + onPress: function() { + this.deck.reverseButton.loopModeOff(true); + this.deck.fluxButton.loopModeOff(true); + if (this.deck.wheelMode === wheelModes.motor) { + this.deck.wheelMode = wheelModes.vinyl; + engine.setValue(this.group, "scratch2_enable", false); + } else { + this.deck.wheelMode = wheelModes.motor; + const group = this.group; + engine.beginTimer(MotorWindUpMilliseconds, () => { + engine.setValue(group, "scratch2_enable", true); + }, true); } + this.outTrigger(); }, outTrigger: function() { const motorOn = this.deck.wheelMode === wheelModes.motor; @@ -2385,18 +2746,16 @@ class S4Mk3Deck extends Deck { }) : undefined; this.jogButton = new Button({ deck: this, - input: function(press) { - if (press) { - this.deck.reverseButton.loopModeOff(true); - this.deck.fluxButton.loopModeOff(true); - if (this.deck.wheelMode === wheelModes.vinyl) { - this.deck.wheelMode = wheelModes.jog; - } else { - this.deck.wheelMode = wheelModes.vinyl; - } - engine.setValue(this.group, "scratch2_enable", false); - this.outTrigger(); + onPress: function() { + this.deck.reverseButton.loopModeOff(true); + this.deck.fluxButton.loopModeOff(true); + if (this.deck.wheelMode === wheelModes.vinyl) { + this.deck.wheelMode = wheelModes.jog; + } else { + this.deck.wheelMode = wheelModes.vinyl; } + engine.setValue(this.group, "scratch2_enable", false); + this.outTrigger(); }, outTrigger: function() { const vinylOn = this.deck.wheelMode === wheelModes.vinyl; @@ -2514,6 +2873,7 @@ class S4Mk3Deck extends Deck { engine.setValue(this.group, "scratch2", this.speed); } else { engine.setValue(this.group, "jog", this.speed); + console.log(this.speed) } break; default: @@ -2615,6 +2975,9 @@ class S4Mk3Deck extends Deck { } }); + this.selectedStem = new Array(4).fill(false); + this.samplerStemSelection = null; + for (const property in this) { if (Object.prototype.hasOwnProperty.call(this, property)) { const component = this[property]; @@ -2644,6 +3007,17 @@ class S4Mk3Deck extends Deck { } } + hasSelectedStem() { + return this.selectedStem.some((stemSelected) => stemSelected); + } + + stemSelection() { + return [...this.selectedStem].reverse().reduce( + (acc, curr) => (curr + acc * 2), + 0, + ); + } + assignKeyboardPlayMode(group, action) { this.keyboardPlayMode = { group: group, @@ -2661,21 +3035,26 @@ class S4Mk3Deck extends Deck { this.hotcuePadModeButton.send(this.hotcuePadModeButton.color + this.hotcuePadModeButton.brightnessOff); } + const data = (UseSharedDataAPI ? engine.getSharedData() : false) || {}; + // unfortunately the other pad mode buttons only have one LED color // const recordPadModeLEDOn = this.currentPadLayer === this.padLayers.hotcuePage3; - // this.recordPadModeButton.send(recordPadModeLEDOn ? 127 : 0); + this.recordPadModeButton.output(data.scrollingWavefom && data.scrollingWavefom[this.group]); const samplesPadModeLEDOn = this.currentPadLayer === this.padLayers.samplerPage; this.samplesPadModeButton.send(samplesPadModeLEDOn ? 127 : 0); // this.mutePadModeButtonLEDOn = this.currentPadLayer === this.padLayers.samplerPage2; - // const mutedModeButton.send(mutePadModeButtonLEDOn ? 127 : 0); + this.mutePadModeButton.output(data.viewArtwork && data.viewArtwork[this.group]); if (this.keyboardPlayMode !== null) { this.stemsPadModeButton.send(LedColors.green + this.stemsPadModeButton.brightnessOn); } else { - const keyboardPadModeLEDOn = this.currentPadLayer === this.padLayers.keyboard; + const keyboardPadModeLEDOn = this.currentPadLayer === this.padLayers.keyboard || this.currentPadLayer === this.padLayers.stem; this.stemsPadModeButton.send(this.stemsPadModeButton.color + (keyboardPadModeLEDOn ? this.stemsPadModeButton.brightnessOn : this.stemsPadModeButton.brightnessOff)); } + if (!UseSharedDataAPI || !data.keyboardMode) { return; } + data.keyboardMode[this.group] = this.currentPadLayer === this.padLayers.keyboard; + engine.setSharedData(data); } } @@ -2783,7 +3162,7 @@ class S4Mk3MixerColumn extends ComponentContainer { inKey: "parameter1", }); this.quickEffectKnob = new Pot({ - group: `[QuickEffectRack1_${this.group}]`, + group: quickFxChannel(this.group), inKey: "super1", }); this.volume = new Pot({ @@ -2881,6 +3260,7 @@ class S4Mk3MixerColumn extends ComponentContainer { if (!alternativeInput) { return; } + console.log(shifted ? alternativeInput : `[Channel${this.idx}]`); this.group = shifted ? alternativeInput : `[Channel${this.idx}]`; for (const property of ["gain", "volume", "pfl", "crossfaderSwitch"]) { const component = this[property]; @@ -3157,6 +3537,13 @@ class S4MK3 { wheelLEDinitReport[0] = 1; controller.sendOutputReport(48, wheelLEDinitReport.buffer); + const motorData = new Uint8Array([ + 1, 0x20, 1, 0, 0, + 1, 0x20, 1, 0, 0, + + ]); + controller.sendOutputReport(49, motorData.buffer); + // Init wheel timer data wheelTimer = null; wheelTimerDelta = 0; @@ -3165,6 +3552,51 @@ class S4MK3 { for (const repordId of [0x01, 0x02]) { this.inReports[repordId].handleInput(controller.getInputReport(repordId)); } + + if (!UseSharedDataAPI) { + return; + } + + engine.setSharedData({ + group: { + "leftdeck": "[Channel1]", + "rightdeck": "[Channel2]", + }, + shift: { + "leftdeck": false, + "rightdeck": false, + }, + scrollingWavefom: { + "[Channel1]": false, + "[Channel2]": false, + "[Channel3]": false, + "[Channel4]": false, + }, + viewArtwork: { + "[Channel1]": false, + "[Channel2]": false, + "[Channel3]": false, + "[Channel4]": false, + }, + keyboardMode: { + "[Channel1]": false, + "[Channel2]": false, + "[Channel3]": false, + "[Channel4]": false, + }, + displayBeatloopSize: { + "[Channel1]": false, + "[Channel2]": false, + "[Channel3]": false, + "[Channel4]": false, + }, + padsMode: { + "[Channel1]": 0, + "[Channel2]": 0, + "[Channel3]": 0, + "[Channel4]": 0, + }, + }); } shutdown() { // button LEDs diff --git a/res/controllers/TraktorKontrolS4MK3Screens.qml b/res/controllers/TraktorKontrolS4MK3Screens.qml new file mode 100644 index 000000000000..fe5225032783 --- /dev/null +++ b/res/controllers/TraktorKontrolS4MK3Screens.qml @@ -0,0 +1,217 @@ +import QtQuick 2.15 +import QtQuick.Window 2.3 + +import QtQuick.Controls 2.15 +import QtQuick.Shapes 1.11 +import QtQuick.Layouts 1.3 +import QtQuick.Window 2.15 + +import Qt5Compat.GraphicalEffects + +import "." as Skin +import Mixxx 1.0 as Mixxx +import Mixxx.Controls 1.0 as MixxxControls + +import S4MK3 as S4MK3 + +Mixxx.ControllerScreen { + id: root + + required property string screenId + property color fontColor: Qt.rgba(242/255,242/255,242/255, 1) + property color smallBoxBorder: Qt.rgba(44/255,44/255,44/255, 1) + + property string group: screenId == "rightdeck" ? "[Channel2]" : "[Channel1]" + property string theme: engine.getSetting("theme") + + readonly property bool isStockTheme: theme == "stock" + + property var lastFrame: null + + init: function(_controllerName, isDebug) { + console.log(`Screen ${root.screenId} has started with theme ${root.theme}`) + root.state = "Live" + } + + shutdown: function() { + console.log(`Screen ${root.screenId} is stopping`) + root.state = "Stop" + } + + transformFrame: function(input, timestamp) { + let updated = new Uint8Array(320*240); + updated.fill(0) + + let updatedPixelCount = 0; + let updated_zones = []; + + if (!root.lastFrame) { + root.lastFrame = new ArrayBuffer(input.byteLength); + updatedPixelCount = input.byteLength / 2; + updated_zones.push({ + x: 0, + y: 0, + width: 320, + height: 240, + }) + } else { + const view_input = new Uint8Array(input); + const view_last = new Uint8Array(root.lastFrame); + + for (let i = 0; i < 320 * 240; i++) { + } + + let current_rect = null; + + for (let y = 0; y < 240; y++) { + let line_changed = false; + for (let x = 0; x < 320; x++) { + let i = y * 320 + x; + if (view_input[2 * i] != view_last[2 * i] || view_input[2 * i + 1] != view_last[2 * i + 1]) { + line_changed = true; + updatedPixelCount++; + break; + } + } + if (current_rect !== null && line_changed) { + current_rect.height++; + } else if (current_rect !== null) { + updated_zones.push(current_rect); + current_rect = null; + } else if (current_rect === null && line_changed) { + current_rect = { + x: 0, + y, + width: 320, + height: 1, + }; + } + } + if (current_rect !== null) { + updated_zones.push(current_rect); + } + } + new Uint8Array(root.lastFrame).set(new Uint8Array(input)); + + if (!updatedPixelCount) { + return new ArrayBuffer(0); + } else if (root.renderDebug) { + console.log(`Pixel updated: ${updatedPixelCount}, ${updated_zones.length} areas`); + } + + // No redraw needed, stop right there + + let totalPixelToDraw = 0; + for (const area of updated_zones) { + area.x -= Math.min(2, area.x); + area.y -= Math.min(2, area.y); + area.width += Math.min(4, 320 - area.x - area.width); + area.height += Math.min(4, 240 - area.y - area.height); + totalPixelToDraw += area.width*area.height; + } + + if (totalPixelToDraw != 320*240 && (totalPixelToDraw > 320 * 180 || updated_zones.length > 20)) { + if (root.renderDebug) { + console.log(`Full redraw instead of ${totalPixelToDraw} pixels/${updated_zones.length} areas`) + } + totalPixelToDraw = 320*240 + updated_zones = [{ + x: 0, + y: 0, + width: 320, + height: 240, + }] + } else if (root.renderDebug) { + console.log(`Redrawing ${totalPixelToDraw} pixels`) + } + + const screenIdx = screenId === "leftdeck" ? 0 : 1; + + const outputData = new ArrayBuffer(totalPixelToDraw*2 + 20*updated_zones.length); // Number of pixel + 20 (header/footer size) x the number of region + let offset = 0; + + for (const area of updated_zones) { + const header = new Uint8Array(outputData, offset, 16); + const payload = new Uint8Array(outputData, offset + 16, area.width*area.height*2); + const footer = new Uint8Array(outputData, offset + area.width*area.height*2 + 16, 4); + + header.fill(0) + footer.fill(0) + header[0] = 0x84; + header[2] = screenIdx; + header[3] = 0x21; + + header[8] = area.x >> 8; + header[9] = area.x & 0xff; + header[10] = area.y >> 8; + header[11] = area.y & 0xff; + + header[12] = area.width >> 8; + header[13] = area.width & 0xff; + header[14] = area.height >> 8; + header[15] = area.height & 0xff; + + if (area.x === 0 && area.width === 320) { + payload.set(new Uint8Array(input, area.y * 320 * 2, area.width*area.height*2)); + } else { + for (let y = 0; y < area.height; y++) { + payload.set( + new Uint8Array(input, ((area.y + y) * 320 + area.x) * 2, area.width * 2), + y * area.width * 2); + } + } + footer[0] = 0x40; + footer[2] = screenIdx; + offset += area.width*area.height*2 + 20 + } + if (root.renderDebug) { + console.log(`Generated ${offset} bytes to be sent`) + } + // return new ArrayBuffer(0); + return outputData; + } + + Component { + id: splashOff + S4MK3.SplashOff { + anchors.fill: parent + } + } + Component { + id: stockLive + S4MK3.StockScreen { + group: root.group + screenId: root.screenId + anchors.fill: parent + } + } + Component { + id: advancedLive + S4MK3.AdvancedScreen { + isLeftScreen: root.screenId == "leftdeck" + } + } + + Loader { + id: loader + anchors.fill: parent + sourceComponent: splashOff + } + + states: [ + State { + name: "Live" + PropertyChanges { + target: loader + sourceComponent: isStockTheme ? stockLive : advancedLive + } + }, + State { + name: "Stop" + PropertyChanges { + target: loader + sourceComponent: splashOff + } + } + ] +} diff --git a/res/controllers/TraktorKontrolS4MK3Screens/S4MK3/AdvancedScreen/Overlays/TopControls.qml b/res/controllers/TraktorKontrolS4MK3Screens/S4MK3/AdvancedScreen/Overlays/TopControls.qml new file mode 100755 index 000000000000..3fb7fa1defa3 --- /dev/null +++ b/res/controllers/TraktorKontrolS4MK3Screens/S4MK3/AdvancedScreen/Overlays/TopControls.qml @@ -0,0 +1,348 @@ +import QtQuick 2.15 + +import '../Defines' as Defines +import '../Widgets' as Widgets + +import Mixxx 1.0 as Mixxx + +//-------------------------------------------------------------------------------------------------------------------- +// FX CONTROLS +//-------------------------------------------------------------------------------------------------------------------- + +// The FxControls are located on the top of the screen and blend in if one of the top knobs is touched/changed + +Item { + id: topLabels + + property int topMargin: 0 + + property string showHideState: "hide" + property int fxUnit: 0 + property int yPositionWhenHidden: 0 - topLabels.height - headerBlackLine.height - headerShadow.height // also hides black border & shadow + property int yPositionWhenShown: topMargin + + readonly property color barBgColor: "black" + + property var fxModel: Mixxx.EffectsManager.visibleEffectsModel + + Defines.Colors { id: colors } + Defines.Durations { id: durations } + + height: 40 + anchors.left: parent.left + anchors.right: parent.right + + // dark grey background + Rectangle { + id: topInfoDetailsPanelDarkBg + anchors { + top: parent.top + left: parent.left + right: parent.right + } + height: topLabels.height + color: colors.colorFxHeaderBg + // light grey background + Rectangle { + id:topInfoDetailsPanelLightBg + anchors { + top: parent.top + left: parent.left + } + height: topLabels.height + width: 80 + color: colors.colorFxHeaderLightBg + } + } + +// // dividers + Rectangle { + id: fxInfoDivider0 + width:1; + height:40; + color: colors.colorDivider + anchors.top: parent.top + anchors.left: parent.left + anchors.leftMargin: 80 + } + + // dividers + Rectangle { + id: fxInfoDivider1 + width:1; + color: colors.colorDivider + anchors.top: parent.top + anchors.left: parent.left + anchors.leftMargin: 160 + height: 40 + } + + Rectangle { + id: fxInfoDivider2 + width:1; + color: colors.colorDivider + anchors.top: parent.top + anchors.left: parent.left + anchors.leftMargin: 240 + height: 40 + } + + // Info Details + Rectangle { + id: topInfoDetailsPanel + + height: parent.height + clip: true + width: parent.width + color: "transparent" + + anchors.left: parent.left + anchors.leftMargin: 1 + + // AppProperty { id: fxDryWet; path: "app.traktor.fx." + (fxUnit + 1) + ".dry_wet" } + + Mixxx.ControlProxy { + group: `[EffectRack1_EffectUnit${fxUnit + 1}]` + key: `mix` + id: fxDryWet + property string description: "" + property var valueRange: ({isDiscrete: true, steps: 1}) + } + + // AppProperty { id: fxParam1; path: "app.traktor.fx." + (fxUnit + 1) + ".parameters.1" } + Mixxx.ControlProxy { + group: `[EffectRack1_EffectUnit${fxUnit + 1}_Effect1]` + key: `meta` + id: fxParam1 + property string description: "" + property var valueRange: ({isDiscrete: false, steps: 0}) + } + Mixxx.ControlProxy { + group: `[EffectRack1_EffectUnit${fxUnit + 1}_Effect1]` + key: `enabled` + id: fxEnabled1 + } + QtObject { + id: fxKnob1name + + property Mixxx.EffectSlotProxy slot: Mixxx.EffectsManager.getEffectSlot(1, 1) + property string description: "Description" + property var value: "---" + property var valueRange: ({isDiscrete: true, steps: 1}) + } + Mixxx.ControlProxy { + id: fxSelect1 + group: `[EffectRack1_EffectUnit${fxUnit + 1}_Effect1]` + key: `loaded_effect` + onValueChanged: { + fxKnob1name.value = topLabels.fxModel.get(value).display + } + } + + Mixxx.ControlProxy { + group: `[EffectRack1_EffectUnit${fxUnit + 1}_Effect2]` + key: `meta` + id: fxParam2 + property string description: "" + property var valueRange: ({isDiscrete: false, steps: 0}) + } + Mixxx.ControlProxy { + group: `[EffectRack1_EffectUnit${fxUnit + 1}_Effect2]` + key: `enabled` + id: fxEnabled2 + } + QtObject { + id: fxKnob2name + + property Mixxx.EffectSlotProxy slot: Mixxx.EffectsManager.getEffectSlot(1, 2) + property string description: "Description" + property var value: "---" + property var valueRange: ({isDiscrete: true, steps: 1}) + } + Mixxx.ControlProxy { + id: fxSelect2 + group: `[EffectRack1_EffectUnit${fxUnit + 1}_Effect2]` + key: `loaded_effect` + onValueChanged: { + fxKnob2name.value = topLabels.fxModel.get(value).display + } + } + + // AppProperty { id: fxParam3; path: "app.traktor.fx." + (fxUnit + 1) + ".parameters.3" } + Mixxx.ControlProxy { + group: `[EffectRack1_EffectUnit${fxUnit + 1}_Effect3]` + key: `meta` + id: fxParam3 + property string description: "" + property var valueRange: ({isDiscrete: false, steps: 0}) + } + Mixxx.ControlProxy { + group: `[EffectRack1_EffectUnit${fxUnit + 1}_Effect3]` + key: `enabled` + id: fxEnabled3 + } + QtObject { + id: fxKnob3name + + property Mixxx.EffectSlotProxy slot: Mixxx.EffectsManager.getEffectSlot(1, 3) + property string description: "Description" + property var value: "---" + property var valueRange: ({isDiscrete: true, steps: 1}) + } + Mixxx.ControlProxy { + id: fxSelect3 + group: `[EffectRack1_EffectUnit${fxUnit + 1}_Effect3]` + key: `loaded_effect` + onValueChanged: { + fxKnob3name.value = topLabels.fxModel.get(value).display + } + } + + Mixxx.ControlProxy { + group: `[EffectRack1_EffectUnit${fxUnit + 1}]` + key: "enabled" + id: fxOn + property string description: "Description" + property var valueRange: ({isDiscrete: true, steps: 1}) + } + // AppProperty { id: fxButton1; path: "app.traktor.fx." + (fxUnit + 1) + ".buttons.1" } + QtObject { + id: fxButton1 + property string description: "Description" + property var value: fxEnabled1.value + property var valueRange: ({isDiscrete: true, steps: 1}) + } + + // AppProperty { id: fxButton1name; path: "app.traktor.fx." + (fxUnit + 1) + ".buttons.1.name" } + QtObject { + id: fxButton1name + property string description: "Description" + property var value: "ON" + property var valueRange: ({isDiscrete: true, steps: 1}) + } + // AppProperty { id: fxButton2; path: "app.traktor.fx." + (fxUnit + 1) + ".buttons.2" } + QtObject { + id: fxButton2 + property string description: "Description" + property var value: fxEnabled2.value + property var valueRange: ({isDiscrete: true, steps: 1}) + } + // AppProperty { id: fxButton2name; path: "app.traktor.fx." + (fxUnit + 1) + ".buttons.2.name" } + QtObject { + id: fxButton2name + property string description: "Description" + property var value: "ON" + property var valueRange: ({isDiscrete: true, steps: 1}) + } + // AppProperty { id: fxButton3; path: "app.traktor.fx." + (fxUnit + 1) + ".buttons.3" } + QtObject { + id: fxButton3 + property string description: "Description" + property var value: fxEnabled3.value + property var valueRange: ({isDiscrete: true, steps: 1}) + } + // AppProperty { id: fxButton3name; path: "app.traktor.fx." + (fxUnit + 1) + ".buttons.3.name" } + QtObject { + id: fxButton3name + property string description: "Description" + property var value: "ON" + property var valueRange: ({isDiscrete: true, steps: 1}) + } + + // AppProperty { id: fxType; path: "app.traktor.fx." + (fxUnit + 1) + ".type" } // singleMode -> fxSelect1.description else "DRY/WET" + QtObject { + id: fxType + property string description: "Description" + property var value: 0 + property var valueRange: ({isDiscrete: true, steps: 1}) + } + + Row { + id: controlRow + TopInfoDetails { + id: topInfoDetails1 + parameter: fxDryWet + isOn: fxOn.value + label: fxType.value == 1 ? ((fxSelect1.description == "Delay") ? "DELAY" : (fxSelect1.description == "Reverb") ? "REVRB" : (fxSelect1.description == "Flanger") ? "FLANG" : (fxSelect1.description == "Flanger Pulse") ? "FLN-P" : (fxSelect1.description == "Flanger Flux") ? "FLN-F" : (fxSelect1.description == "Gater") ? "GATER" : (fxSelect1.description == "Beatmasher 2") ? "BEATM" : (fxSelect1.description == "Delay T3") ? "T3DELAY" : (fxSelect1.description == "Filter LFO") ? "FLT-O" : (fxSelect1.description == "Filter Pulse") ? "FLT-P" : (fxSelect1.description == "Filter") ? "FILTR" : (fxSelect1.description == "Filter:92 Pulse") ? "F92-O" : (fxSelect1.description == "Filter:92 Pulse") ? "F92-P" : (fxSelect1.description == "Filter:92") ? "FLT92" : (fxSelect1.description == "Phaser") ? "PHFXASR" : (fxSelect1.description == "Phaser Pulse") ? "PHS-P" : (fxSelect1.description == "Phaser Flux") ? "PHS-F" : (fxSelect1.description == "Reverse Grain") ? "REVGR" : (fxSelect1.description == "Turntable FX") ? "TTFX" : (fxSelect1.description == "Iceverb") ? "ICEVB" : (fxSelect1.description == "Reverb T3") ? "T3REVRB" : (fxSelect1.description == "Ringmodulator") ? "RINGM" : (fxSelect1.description == "Digital LoFi") ? "LOFI" : (fxSelect1.description == "Mulholland Drive") ? "MHDRV" : (fxSelect1.description == "Transpose Stretch") ? "TRANS" : (fxSelect1.description == "BeatSlicer") ? "SLICER" : (fxSelect1.description == "Formant Filter") ? "FFTR" : (fxSelect1.description == "Peak Filter") ? "PFTR" : (fxSelect1.description == "Tape Delay") ? "TPDELAY" : (fxSelect1.description == "Ramp Delay") ? "RMPDLY" : (fxSelect1.description == "Auto Bouncer") ? "ABOUNCE" : (fxSelect1.description == "Bouncer") ? "BOUNCER" : (fxKnob3name.value == "LASLI") ? "LASLI" : (fxKnob3name.value == "GRANP") ? "GRANP" : (fxKnob3name.value == "B-O-M") ? "B-O-M" : (fxKnob3name.value == "POWIN") ? "POWIN" : (fxKnob3name.value == "EVNHR") ? "EVNHR" : (fxKnob3name.value == "ZZZRP") ? "ZZZRP" : (fxKnob3name.value == "STRRS") ? "STRRS" : (fxKnob3name.value == "STRRF") ? "STRRF" : (fxKnob3name.value == "DARKM") ? "DARKM" : (fxKnob3name.value == "FTEST") ? "FTEST" : fxSelect1.description) : "DRY/WET" + buttonLabel: fxType.value == 1 ? "ON" : "" + fxEnabled: (fxType.value != 1) || fxSelect1.value + barBgColor: topLabels.barBgColor + isPatternPlayer: (fxType.value == 2 ? true : false) + } + TopInfoDetails { + id: topInfoDetails2 + parameter: fxParam1 + isOn: fxButton1.value + label: fxKnob1name.value + buttonLabel: fxButton1name.value + fxEnabled: (fxSelect1.value || ((fxType.value == 1) && fxSelect1.value) ) + barBgColor: topLabels.barBgColor + isPatternPlayer: (fxType.value == 2 ? true : false) + } + + TopInfoDetails { + id: topInfoDetails3 + parameter: fxParam2 + isOn: fxButton2.value + label: fxKnob2name.value + buttonLabel: fxButton2name.value + fxEnabled: (fxSelect2.value || ((fxType.value == 1) && fxSelect1.value) ) + barBgColor: topLabels.barBgColor + isPatternPlayer: (fxType.value == 2 ? true : false) + } + + TopInfoDetails { + id: topInfoDetails4 + parameter: fxParam3 + isOn: fxButton3.value + label: fxKnob3name.value + buttonLabel: fxButton3name.value + fxEnabled: (fxSelect3.value || ((fxType.value == 1) && fxSelect1.value) ) + barBgColor: topLabels.barBgColor + isPatternPlayer: (fxType.value == 2 ? true : false) + } + } + } + + // black border & shadow + Rectangle { + id: headerBlackLine + anchors.top: topLabels.bottom + width: parent.width + color: colors.colorBlack + height: 2 + } + Rectangle { + id: headerShadow + anchors.left: parent.left + anchors.right: parent.right + anchors.top: headerBlackLine.bottom + height: 6 + gradient: Gradient { + GradientStop { position: 1.0; color: colors.colorBlack0 } + GradientStop { position: 0.0; color: colors.colorBlack63 } + } + visible: false + } + + //------------------------------------------------------------------------------------------------------------------ + // STATES + //------------------------------------------------------------------------------------------------------------------ + + Behavior on y { PropertyAnimation { duration: durations.overlayTransition; easing.type: Easing.InOutQuad } } + + Item { + id: showHide + state: showHideState + states: [ + State { + name: "show"; + PropertyChanges { target: topLabels; y: yPositionWhenShown} + }, + State { + name: "hide"; + PropertyChanges { target: topLabels; y: yPositionWhenHidden} + } + ] + } +} diff --git a/res/controllers/TraktorKontrolS4MK3Screens/S4MK3/AdvancedScreen/Waveform/WaveformContainer.qml b/res/controllers/TraktorKontrolS4MK3Screens/S4MK3/AdvancedScreen/Waveform/WaveformContainer.qml new file mode 100755 index 000000000000..92a9e7424451 --- /dev/null +++ b/res/controllers/TraktorKontrolS4MK3Screens/S4MK3/AdvancedScreen/Waveform/WaveformContainer.qml @@ -0,0 +1,234 @@ +import QtQuick 2.15 + +import '../Defines' +import '../Widgets' as Widgets +import '../Overlays' as Overlays +import '../ViewModels' as ViewModels + +import Mixxx 1.0 as Mixxx +import Mixxx.Controls 1.0 as MixxxControls + +Item { + id: view + property int deckId: deckInfo.deckId + property string deckSizeState: "large" + property bool showLoopSize: false + property bool isInEditMode: false + property string propertiesPath: "" + property int zoomLevel: deckInfo.zoomLevel + readonly property int minSampleWidth: 2048 + property int sampleWidth: minSampleWidth << zoomLevel + property bool hideLoop: false + property bool hideBPM: false + property bool hideKey: false + + readonly property bool trackIsLoaded: deckInfo.isLoaded + + //-------------------------------------------------------------------------------------------------------------------- + + required property var deckInfo + + //-------------------------------------------------------------------------------------------------------------------- + // WAVEFORM Position + //------------------------------------------------------------------------------------------------------------------ + + Mixxx.ControlProxy { + id: scratchPositionEnableControl + + group: root.group + key: "scratch_position_enable" + } + + Mixxx.ControlProxy { + id: scratchPositionControl + + group: root.group + key: "scratch_position" + } + + Mixxx.ControlProxy { + id: wheelControl + + group: root.group + key: "wheel" + } + + Mixxx.ControlProxy { + id: rateRatioControl + + group: root.group + key: "rate_ratio" + } + + Mixxx.ControlProxy { + id: zoomControl + + group: root.group + key: "waveform_zoom" + } + + MixxxControls.WaveformDisplay { + id: singleWaveform + group: `[Channel${view.deckId}]` + x: 0 + width: 316 + // height: (settings.alwaysShowTempoInfo || deckInfo.adjustEnabled ? (settings.hideStripe ? content.waveformHeight + display.secondRowHeight-51 : content.waveformHeight-38) : (!deckInfo.showBPMInfo ? (settings.hideStripe ? content.waveformHeight + display.secondRowHeight-13 : content.waveformHeight) : (settings.hideStripe ? content.waveformHeight + display.secondRowHeight-51 : content.waveformHeight-38))) + (settings.hidePhase && settings.hidePhrase ? 16 : 0) + (!settings.hidePhase && !settings.hidePhrase ? -16 : 0) + height: view.height + + Behavior on height { PropertyAnimation { duration: 90} } + anchors.fill: parent + zoom: zoomControl.value + backgroundColor: "#36000000" + + Mixxx.WaveformRendererEndOfTrack { + color: 'blue' + } + + Mixxx.WaveformRendererPreroll { + color: '#998977' + } + + Mixxx.WaveformRendererMarkRange { + // + Mixxx.WaveformMarkRange { + startControl: "loop_start_position" + endControl: "loop_end_position" + enabledControl: "loop_enabled" + color: '#00b400' + opacity: 0.7 + disabledColor: '#FFFFFF' + disabledOpacity: 0.6 + } + // + Mixxx.WaveformMarkRange { + startControl: "intro_start_position" + endControl: "intro_end_position" + color: '#2c5c9a' + opacity: 0.6 + durationTextColor: '#ffffff' + durationTextLocation: 'after' + } + // + Mixxx.WaveformMarkRange { + startControl: "outro_start_position" + endControl: "outro_end_position" + color: '#2c5c9a' + opacity: 0.6 + durationTextColor: '#ffffff' + durationTextLocation: 'before' + } + } + + Mixxx.WaveformRendererRGB { + axesColor: '#00ffffff' + lowColor: 'red' + midColor: 'green' + highColor: 'blue' + } + + Mixxx.WaveformRendererStem { } + + Mixxx.WaveformRendererBeat { + color: '#cfcfcf' + } + + Mixxx.WaveformRendererMark { + playMarkerColor: 'cyan' + playMarkerBackground: 'transparent' + defaultMark: Mixxx.WaveformMark { + align: "bottom|right" + color: "#FF0000" + textColor: "#FFFFFF" + text: " %1 " + } + + untilMark.showTime: true + untilMark.showBeats: true + untilMark.align: Qt.AlignBottom + untilMark.textSize: 14 + + Mixxx.WaveformMark { + control: "cue_point" + text: 'C' + align: 'top|right' + color: 'red' + textColor: '#FFFFFF' + } + Mixxx.WaveformMark { + control: "loop_start_position" + text: '↻' + align: 'top|left' + color: 'green' + textColor: '#FFFFFF' + } + Mixxx.WaveformMark { + control: "loop_end_position" + align: 'bottom|right' + color: 'green' + textColor: '#FFFFFF' + } + Mixxx.WaveformMark { + control: "intro_start_position" + align: 'top|right' + color: 'blue' + textColor: '#FFFFFF' + } + Mixxx.WaveformMark { + control: "intro_end_position" + text: '◢' + align: 'top|left' + color: 'blue' + textColor: '#FFFFFF' + } + Mixxx.WaveformMark { + control: "outro_start_position" + text: '◣' + align: 'top|right' + color: 'blue' + textColor: '#FFFFFF' + } + Mixxx.WaveformMark { + control: "outro_end_position" + align: 'top|left' + color: 'blue' + textColor: '#FFFFFF' + } + } + } + + //-------------------------------------------------------------------------------------------------------------------- + // Stem Color Indicators (Rectangles) + //-------------------------------------------------------------------------------------------------------------------- + + StemColorIndicators { + id: stemColorIndicators + deckId: view.deckId + deckInfo: view.deckInfo + anchors.fill: singleWaveform + anchors.rightMargin: 309 + visible: deckInfoModel.isStemDeck + indicatorHeight: !settings.hidePhase && !settings.hidePhrase ? (deckInfo.showBPMInfo ? [19 , 19 , 19 , 20] : [27 , 27 , 27 , 27]) : (deckInfo.showBPMInfo ? [23 , 23 , 23 , 23] : [31 , 31 , 31 , 31]) + } + + Widgets.LoopSize { + id: loopSize + anchors.topMargin: 1 + anchors.fill: parent + visible: (deckInfo.showLoopInfo || deckInfo.loopActive || settings.alwaysShowLoopSize) && !hideLoop + } + + Widgets.KeyDisplay { + id: keyDisplay + anchors.topMargin: 1 + anchors.fill: parent + visible: !hideKey + } + + Widgets.BpmDisplay { + id: bpmDisplay + anchors.bottomMargin: 1 + anchors.top: singleWaveform.bottom + anchors.fill: parent + visible: !hideBPM && (!deckInfo.showBPMInfo && !settings.alwaysShowTempoInfo && !deckInfo.adjustEnabled) || settings.hideWaveforms + } +} diff --git a/res/controllers/TraktorKontrolS4MK3Screens/S4MK3/BPMIndicator.qml b/res/controllers/TraktorKontrolS4MK3Screens/S4MK3/BPMIndicator.qml new file mode 100755 index 000000000000..c49800618f21 --- /dev/null +++ b/res/controllers/TraktorKontrolS4MK3Screens/S4MK3/BPMIndicator.qml @@ -0,0 +1,77 @@ +/* +This module is used to define the top right section, right under the label. +Currently this section is dedicated to BPM and tempo fader information. +*/ +import QtQuick 2.14 +import QtQuick.Controls 2.15 + +import Mixxx 1.0 as Mixxx +import Mixxx.Controls 1.0 as MixxxControls + +Rectangle { + id: root + + required property string group + required property color borderColor + + property real value: 0 + + color: "transparent" + radius: 6 + border.color: smallBoxBorder + border.width: 2 + + signal updated + + Text { + id: indicator + text: "-" + font.pixelSize: 17 + color: fontColor + anchors.centerIn: parent + + Mixxx.ControlProxy { + group: root.group + key: "bpm" + onValueChanged: (value) => { + const newValue = value.toFixed(2); + if (newValue === indicator.text) return; + indicator.text = newValue; + root.updated() + } + } + } + + Text { + id: range + font.pixelSize: 9 + color: fontColor + anchors.top: parent.top + anchors.bottom: parent.bottom + anchors.right: parent.right + anchors.rightMargin: 5 + anchors.topMargin: 2 + + horizontalAlignment: Text.AlignHCenter + + Mixxx.ControlProxy { + group: root.group + key: "rateRange" + onValueChanged: (value) => { + const newValue = `-/+ \n${(value * 100).toFixed()}%`; + if (range.text === newValue) return; + range.text = newValue; + root.updated(); + } + } + } + + states: State { + name: "compacted" + + PropertyChanges { + target: range + visible: false + } + } +} diff --git a/res/controllers/TraktorKontrolS4MK3Screens/S4MK3/HotcuePoint.qml b/res/controllers/TraktorKontrolS4MK3Screens/S4MK3/HotcuePoint.qml new file mode 100644 index 000000000000..f39bdc1ca796 --- /dev/null +++ b/res/controllers/TraktorKontrolS4MK3Screens/S4MK3/HotcuePoint.qml @@ -0,0 +1,199 @@ +/* +This module is used to define markers element as render over the the overview waveform. +When this is written, Mixxx QML doesn't have waveform overview marker ready to be used, so this +is an attempt to provide fully functional markers for the controller screen, while the Mixxx QML +interface is still being worked on. +Consider replacing this with native overview marker in the future. +*/ +import QtQuick 2.15 +import QtQuick.Shapes 1.4 +import QtQuick.Window 2.15 + +import QtQuick.Controls 2.15 + +import Mixxx 1.0 as Mixxx + +Item { + required property real position + required property int type + + property int number: 1 + property color color: 'blue' + + enum Type { + OneShot, + Loop, + IntroIn, + IntroOut, + OutroIn, + OutroOut, + LoopIn, + LoopOut + } + + property variant typeWithNumber: [ + HotcuePoint.Type.OneShot, + HotcuePoint.Type.Loop + ] + + x: position * (Window.width - 16) + width: 21 + + // One shot + Shape { + visible: type == HotcuePoint.Type.OneShot + anchors.fill: parent + antialiasing: true + + ShapePath { + strokeWidth: 1 + strokeColor: Qt.rgba(0, 0, 0, 0.5) + fillColor: color + strokeStyle: ShapePath.SolidLine + // dashPattern: [ 1, 4 ] + startX: 0; startY: 0 + + PathLine { x: 12; y: 0 } + PathLine { x: 18; y: 6 } + PathLine { x: 18; y: 7 } + PathLine { x: 12; y: 13 } + PathLine { x: 2; y: 13 } + PathLine { x: 2; y: 80 } + PathLine { x: 0; y: 80 } + PathLine { x: 0; y: 0 } + } + } + + // Intro/Outro entry marker + Shape { + visible: type == HotcuePoint.Type.IntroIn || type == HotcuePoint.Type.OutroIn + anchors.fill: parent + antialiasing: true + + ShapePath { + strokeWidth: 1 + strokeColor: Qt.rgba(0, 0, 0, 0.5) + fillColor: "#6e6e6e" + strokeStyle: ShapePath.SolidLine + // dashPattern: [ 1, 4 ] + startX: 0; startY: 0 + + PathLine { x: 11; y: 0 } + PathLine { x: 2; y: 13 } + PathLine { x: 2; y: 80 } + PathLine { x: 0; y: 80 } + PathLine { x: 0; y: 0 } + } + } + + // Intro/Outro exit marker + Shape { + visible: type == HotcuePoint.Type.IntroOut || type == HotcuePoint.Type.OutroOut + anchors.fill: parent + antialiasing: true + + ShapePath { + strokeWidth: 1 + strokeColor: Qt.rgba(0, 0, 0, 0.5) + fillColor: "#6e6e6e" + strokeStyle: ShapePath.SolidLine + // dashPattern: [ 1, 4 ] + startX: 2; startY: 0 + + PathLine { x: 0; y: 0 } + PathLine { x: 0; y: 67 } + PathLine { x: -9; y: 80 } + PathLine { x: 2; y: 80 } + PathLine { x: 2; y: 0 } + } + } + + // Loop + Shape { + visible: type == HotcuePoint.Type.Loop + anchors.fill: parent + antialiasing: true + + ShapePath { + strokeWidth: 1 + strokeColor: Qt.rgba(0, 0, 0, 0.5) + fillColor: "#6ef36e" + strokeStyle: ShapePath.SolidLine + // dashPattern: [ 1, 4 ] + startX: 13; startY: 0 + + PathArc { + x: 2; y: 13 + radiusX: 9; radiusY: 9 + direction: PathArc.Clockwise + } + PathLine { x: 2; y: 80 } + PathLine { x: 0; y: 80 } + PathLine { x: 0; y: 0 } + PathLine { x: 21; y: 0 } + } + } + + // Loop in + Shape { + visible: type == HotcuePoint.Type.LoopIn + anchors.fill: parent + antialiasing: true + + ShapePath { + strokeWidth: 1 + strokeColor: Qt.rgba(0, 0, 0, 0.5) + fillColor: "#6ef36e" + strokeStyle: ShapePath.SolidLine + // dashPattern: [ 1, 4 ] + startX: 0; startY: 0 + + PathLine { x: 8; y: 0 } + PathLine { x: 2; y: 10 } + PathLine { x: 2; y: 80 } + PathLine { x: 0; y: 80 } + PathLine { x: 0; y: 0 } + } + } + + // Loop out + Shape { + visible: type == HotcuePoint.Type.LoopOut + anchors.fill: parent + antialiasing: true + + ShapePath { + strokeWidth: 1 + strokeColor: Qt.rgba(0, 0, 0, 0.5) + fillColor: "#6ef36e" + strokeStyle: ShapePath.SolidLine + // dashPattern: [ 1, 4 ] + startX: 2; startY: 0 + + PathLine { x: -6; y: 0 } + PathLine { x: 0; y: 10 } + PathLine { x: 0; y: 80 } + PathLine { x: 2; y: 80 } + PathLine { x: 2; y: 0 } + } + } + + Shape { + visible: type in typeWithNumber + anchors.fill: parent + antialiasing: true + + ShapePath { + fillColor: "black" + strokeColor: "black" + PathText { + x: 4 + y: 3 + font.family: "Arial" + font.pixelSize: 11 + font.weight: Font.Medium + text: `${number}` + } + } + } +} diff --git a/res/controllers/TraktorKontrolS4MK3Screens/S4MK3/KeyIndicator.qml b/res/controllers/TraktorKontrolS4MK3Screens/S4MK3/KeyIndicator.qml new file mode 100755 index 000000000000..5d2c1b4c5829 --- /dev/null +++ b/res/controllers/TraktorKontrolS4MK3Screens/S4MK3/KeyIndicator.qml @@ -0,0 +1,127 @@ +/* +This module is used to define the top left section, right under the label. +Currently this section is dedicated to key/pitch information. +*/ +import QtQuick 2.14 +import QtQuick.Controls 2.15 + +import Mixxx 1.0 as Mixxx +import Mixxx.Controls 1.0 as MixxxControls + +Rectangle { + id: root + + required property string group + + enum Key { + NoKey, + OneD, + EightD, + ThreeD, + TenD, + FiveD, + TwelveD, + SevenD, + SecondD, + NineD, + FourD, + ElevenD, + SixD, + TenM, + FiveM, + TwelveM, + SevenM, + TwoM, + NineM, + FourM, + ElevenM, + SixM, + OneM, + EightM, + ThreeM + } + + property variant colorsMap: [ + "#b09840", // No key + "#b960a2",// 1d + "#9fc516", // 8d + "#527fc0", // 3d + "#f28b2e", // 10d + "#5bc1cf", // 5d + "#e84c4d", // 12d + "#73b629", // 7d + "#8269ab", // 2d + "#fdd615", // 9d + "#3cc0f0", // 4d + "#4cb686", // 11d + "#4cb686", // 6d + "#f5a158", // 10m + "#7bcdd9", // 5m + "#ed7171", // 12m + "#8fc555", // 7m + "#9b86be", // 2m + "#fcdf45", // 9m + "#63cdf4", // 4m + "#f1845f", // 11m + "#70c4a0", // 6m + "#c680b6", // 1m + "#b2d145", // 8m + "#7499cd" // 3m + ] + + property variant textMap: [ + "No key", + "1d", + "8d", + "3d", + "10d", + "5d", + "12d", + "7d", + "2d", + "9d", + "4d", + "11d", + "6d", + "10m", + "5m", + "12m", + "7m", + "2m", + "9m", + "4m", + "11m", + "6m", + "1m", + "8m", + "3m" + ] + + required property color borderColor + + property int key: KeyIndicator.Key.NoKey + + radius: 6 + border.color: colorsMap[key] + border.width: 2 + + color: colorsMap[key] + signal updated + + Mixxx.ControlProxy { + group: root.group + key: "key" + onValueChanged: (value) => { + if (value === root.key) return; + root.key = value; + root.updated() + } + } + + Text { + text: textMap[key] + font.pixelSize: 17 + color: fontColor + anchors.centerIn: parent + } +} diff --git a/res/controllers/TraktorKontrolS4MK3Screens/S4MK3/Keyboard.qml b/res/controllers/TraktorKontrolS4MK3Screens/S4MK3/Keyboard.qml new file mode 100644 index 000000000000..2e3b87e453dd --- /dev/null +++ b/res/controllers/TraktorKontrolS4MK3Screens/S4MK3/Keyboard.qml @@ -0,0 +1,140 @@ +/* +This module is used render the keyboard scale, originating from C major (do). +*/ +import QtQuick 2.15 +import QtQuick.Shapes 1.4 +import QtQuick.Layouts 1.3 + +import Mixxx 1.0 as Mixxx +import Mixxx.Controls 1.0 as MixxxControls + +import "." as S4MK3 + +Item { + id: root + + required property string group + + property int key: -1 + + signal updated + + Mixxx.ControlProxy { + group: root.group + key: "key" + onValueChanged: (value) => { + if (value === root.key) return; + root.key = value; + root.updated() + } + } + + RowLayout { + anchors.fill: parent + spacing: 0 + Item { + Layout.fillWidth: true + Layout.fillHeight: true + Rectangle { anchors.fill: parent; color: "transparent" } + } + Repeater { + id: whiteKeys + + model: 7 + + property variant keyMap: [ + S4MK3.KeyIndicator.Key.OneD, + S4MK3.KeyIndicator.Key.ThreeD, + S4MK3.KeyIndicator.Key.FiveD, + S4MK3.KeyIndicator.Key.TwelveD, + S4MK3.KeyIndicator.Key.SecondD, + S4MK3.KeyIndicator.Key.FourD, + S4MK3.KeyIndicator.Key.SixD, + S4MK3.KeyIndicator.Key.TenM, + S4MK3.KeyIndicator.Key.TwelveM, + S4MK3.KeyIndicator.Key.TwoM, + S4MK3.KeyIndicator.Key.NineM, + S4MK3.KeyIndicator.Key.ElevenM, + S4MK3.KeyIndicator.Key.OneM, + S4MK3.KeyIndicator.Key.ThreeM + ] + + Rectangle { + Layout.preferredWidth: 21 + Layout.fillHeight: true + Layout.alignment: Qt.AlignHCenter | Qt.AlignVCenter + radius: 2 + border.width: 1 + border.color: root.key == whiteKeys.keyMap[index] || root.key == whiteKeys.keyMap[index + 7] ? "red" : "black" + color: root.key == whiteKeys.keyMap[index] || root.key == whiteKeys.keyMap[index + 7] ? "#aaaaaa" : "white" + } + } + Item { + Layout.fillWidth: true + Layout.fillHeight: true + Rectangle { anchors.fill: parent; color: "transparent" } + } + } + RowLayout { + anchors.fill: parent + spacing: 0 + + Item { + Layout.fillWidth: true + Layout.fillHeight: true + Rectangle { anchors.fill: parent; color: "transparent" } + } + Repeater { + id: blackKeys + + model: 5 + + property variant keyMap: [ + S4MK3.KeyIndicator.Key.EightD, + S4MK3.KeyIndicator.Key.TenD, + S4MK3.KeyIndicator.Key.SevenD, + S4MK3.KeyIndicator.Key.NineD, + S4MK3.KeyIndicator.Key.ElevenD, + S4MK3.KeyIndicator.Key.FiveM, + S4MK3.KeyIndicator.Key.SevenM, + S4MK3.KeyIndicator.Key.FourM, + S4MK3.KeyIndicator.Key.SixM, + S4MK3.KeyIndicator.Key.EightM, + ] + + Item { + Layout.fillHeight: true + Layout.preferredWidth: index == 1 ? 42 : index == 4 ? 12 : 21 + Rectangle { + anchors.top: parent.top + anchors.bottom: parent.bottom + width: 12 + Layout.alignment: Qt.AlignHCenter | Qt.AlignVCenter + color: "transparent" + ColumnLayout { + anchors.fill: parent + spacing: 0 + Rectangle { + Layout.fillHeight: true + Layout.fillWidth: true + radius: 2 + border.width: 1 + // border.color: root.key == blackKeys.keyMap[index] || root.key == blackKeys.keyMap[index + blackKeys.model] ? "red" : "black" + color: root.key == blackKeys.keyMap[index] || root.key == blackKeys.keyMap[index + blackKeys.model] ? "#aaaaaa" : "black" + } + Item { + Layout.fillWidth: true + Layout.fillHeight: true + Rectangle { anchors.fill: parent; color: "transparent" } + } + } + } + } + } + Item { + Layout.fillWidth: true + Layout.fillHeight: true + Rectangle { anchors.fill: parent; color: "transparent" } + } + } +} diff --git a/res/controllers/TraktorKontrolS4MK3Screens/S4MK3/LoopSizeIndicator.qml b/res/controllers/TraktorKontrolS4MK3Screens/S4MK3/LoopSizeIndicator.qml new file mode 100755 index 000000000000..7f0bdec7d01c --- /dev/null +++ b/res/controllers/TraktorKontrolS4MK3Screens/S4MK3/LoopSizeIndicator.qml @@ -0,0 +1,75 @@ +/* +This module is used to define the center right section, above the waveform. +Currently this section is dedicated to display loop state information such as loop state, anchor mode or size. +*/ +import QtQuick 2.14 +import QtQuick.Controls 2.15 + +import Mixxx 1.0 as Mixxx +import Mixxx.Controls 1.0 as MixxxControls + +Rectangle { + id: root + + required property string group + + property color loopReverseOffBoxColor: Qt.rgba(255/255,113/255,9/255, 1) + property color loopOffBoxColor: Qt.rgba(67/255,70/255,66/255, 1) + property color loopOffFontColor: "white" + property color loopOnBoxColor: Qt.rgba(125/255,246/255,64/255, 1) + property color loopOnFontColor: "black" + + property bool on: true + signal updated + + radius: 6 + border.width: 2 + border.color: (loopSizeIndicator.on ? loopOnBoxColor : (loop_anchor.value == 0 ? loopOffBoxColor : loopReverseOffBoxColor)) + color: (loopSizeIndicator.on ? loopOnBoxColor : (loop_anchor.value == 0 ? loopOffBoxColor : loopReverseOffBoxColor)) + + Text { + id: indicator + anchors.centerIn: parent + font.pixelSize: 46 + color: (loopSizeIndicator.on ? loopOnFontColor : loopOffFontColor) + + Mixxx.ControlProxy { + group: root.group + key: "beatloop_size" + onValueChanged: (value) => { + const newValue = (value < 1 ? `1/${1 / value}` : `${value}`); + if (newValue === indicator.text) return; + indicator.text = newValue; + root.updated() + } + } + } + + Mixxx.ControlProxy { + group: root.group + key: "loop_enabled" + onValueChanged: (value) => { + if (value === root.on) return; + root.on = value; + root.updated() + } + } + + Mixxx.ControlProxy { + group: root.group + key: "loop_anchor" + id: loop_anchor + onValueChanged: (value) => { + root.updated() + } + } + + states: State { + name: "compacted" + + PropertyChanges { + target: indicator + font.pixelSize: 17 + } + } +} diff --git a/res/controllers/TraktorKontrolS4MK3Screens/S4MK3/OnAirTrack.qml b/res/controllers/TraktorKontrolS4MK3Screens/S4MK3/OnAirTrack.qml new file mode 100644 index 000000000000..0d034066a6e1 --- /dev/null +++ b/res/controllers/TraktorKontrolS4MK3Screens/S4MK3/OnAirTrack.qml @@ -0,0 +1,81 @@ +/* +This module is used to define the top section o the screen. +Currently this section is dedicated to display title and artist of the track loaded on the deck. +*/ +import QtQuick 2.14 +import QtQuick.Controls 2.15 + +import Mixxx 1.0 as Mixxx +import Mixxx.Controls 1.0 as MixxxControls + +Item { + id: root + + required property string group + property var deckPlayer: Mixxx.PlayerManager.getPlayer(root.group) + property bool scrolling: true + + property real speed: 1.7 + property real spacing: 30 + + Rectangle { + id: frame + anchors.top: root.top + anchors.bottom: root.bottom + width: parent.width + x: 6 + color: 'transparent' + + readonly property string fulltext: !trackLoadedControl.value || root.deckPlayer.title.trim().length + root.deckPlayer.artist.trim().length == 0 ? qsTr("No Track Loaded") : `${root.deckPlayer.title} - ${root.deckPlayer.artist}`.trim() + + Text { + id: text1 + text: frame.fulltext + font.pixelSize: 24 + font.family: "Noto Sans" + font.letterSpacing: -1 + color: fontColor + } + Text { + id: text2 + visible: root.width < text1.implicitWidth + anchors.left: text1.right + anchors.leftMargin: spacing + text: frame.fulltext + font.pixelSize: 24 + font.family: "Noto Sans" + font.letterSpacing: -1 + color: fontColor + } + } + + Mixxx.ControlProxy { + id: trackLoadedControl + + group: root.group + key: "track_loaded" + } + + Timer { + id: timer + + property int modifier: -root.speed + + repeat: true + interval: 15 + running: root.width < text1.implicitWidth && root.scrolling + + onTriggered: { + frame.x += modifier; + if (frame.x <= -text1.implicitWidth - spacing) { + frame.x = 0; + } + } + + onRunningChanged: { + if (!running) { + frame.x = 6; + } + } + } +} diff --git a/res/controllers/TraktorKontrolS4MK3Screens/S4MK3/Progression.qml b/res/controllers/TraktorKontrolS4MK3Screens/S4MK3/Progression.qml new file mode 100755 index 000000000000..82d8f8b3f892 --- /dev/null +++ b/res/controllers/TraktorKontrolS4MK3Screens/S4MK3/Progression.qml @@ -0,0 +1,53 @@ +/* +This module is used to draw an overlay on the waveform overview in order to highlight better the playback progression. +As the native Mixxx QML component involves, this component might become redundant and should be replaces with native modules. +*/ +import QtQuick 2.15 +import QtQuick.Window 2.15 + +import Mixxx 1.0 as Mixxx +import Mixxx.Controls 1.0 as MixxxControls + +Item { + id: root + + required property string group + + property real windowWidth: Window.width + + width: 0 + signal updated + + Mixxx.ControlProxy { + group: root.group + key: "track_loaded" + onValueChanged: (value) => { + if (value === root.visible) return; + root.visible = value + root.updated() + } + } + + Mixxx.ControlProxy { + group: root.group + key: "playposition" + onValueChanged: (value) => { + const newValue = Math.round(value * (320 - 12)); + if (newValue === root.width) return; + root.width = newValue; + root.updated() + } + } + + clip: true + + Rectangle { + anchors.fill: parent + anchors.leftMargin: -border.width + anchors.topMargin: -border.width + anchors.bottomMargin: -border.width + border.width: 2 + border.color:"black" + color: Qt.rgba(0.39, 0.80, 0.96, 0.3) + } +} diff --git a/res/controllers/TraktorKontrolS4MK3Screens/S4MK3/SplashOff.qml b/res/controllers/TraktorKontrolS4MK3Screens/S4MK3/SplashOff.qml new file mode 100644 index 000000000000..a3937436c791 --- /dev/null +++ b/res/controllers/TraktorKontrolS4MK3Screens/S4MK3/SplashOff.qml @@ -0,0 +1,15 @@ +import QtQuick 2.15 + +Rectangle { + id: root + anchors.fill: parent + color: "black" + + Image { + anchors.centerIn: parent + width: root.width*0.8 + height: root.height + fillMode: Image.PreserveAspectFit + source: engine.getSetting("idleBackground") == "mask" ? "./Screens/Images/logo.png" : "../../../images/templates/logo_mixxx.png" + } +} diff --git a/res/controllers/TraktorKontrolS4MK3Screens/S4MK3/StockScreen.qml b/res/controllers/TraktorKontrolS4MK3Screens/S4MK3/StockScreen.qml new file mode 100644 index 000000000000..c93b813f3e6b --- /dev/null +++ b/res/controllers/TraktorKontrolS4MK3Screens/S4MK3/StockScreen.qml @@ -0,0 +1,634 @@ +import QtQuick 2.15 +import QtQuick.Layouts 1.3 + +import "../../../qml" as Skin +import Mixxx 1.0 as Mixxx +import Mixxx.Controls 1.0 as MixxxControls + +import S4MK3 as S4MK3 + +Rectangle { + id: root + + required property string group + required property string screenId + + readonly property bool useSharedApi: engine.getSetting("useSharedDataAPI") + + anchors.fill: parent + color: "black" + + function onSharedDataUpdate(data) { + if (!root) return; + + console.log(`Received data on screen#${root.screenId} while currently bind to ${root.group}: ${JSON.stringify(data)}`); + if (typeof data === "object" && typeof data.group[root.screenId] === "string" && root.group !== data.group[root.screenId]) { + root.group = data.group[root.screenId] + waveformOverview.player = Mixxx.PlayerManager.getPlayer(root.group) + artwork.player = Mixxx.PlayerManager.getPlayer(root.group) + console.log(`Changed group for screen ${root.screenId} to ${root.group}`); + } + var shouldBeCompacted = false; + if (typeof data.padsMode === "object") { + scrollingWaveform.visible = data.padsMode[root.group] === 4 + artworkSpacer.visible = data.padsMode[root.group] === 1 + shouldBeCompacted |= scrollingWaveform.visible || artworkSpacer.visible + } + if (typeof data.keyboardMode === "object") { + shouldBeCompacted |= data.keyboardMode[root.group] + keyboard.visible = !!data.keyboardMode[root.group] + } + deckInfo.state = shouldBeCompacted ? "compacted" : "" + if (typeof data.displayBeatloopSize === "object") { + timeIndicator.mode = data.displayBeatloopSize[root.group] ? S4MK3.TimeAndBeatloopIndicator.Mode.BeetjumpSize : S4MK3.TimeAndBeatloopIndicator.Mode.RemainingTime + timeIndicator.update() + } + } + + Mixxx.ControlProxy { + id: trackLoadedControl + + group: root.group + key: "track_loaded" + + onValueChanged: (value) => { + if (!value && deckInfo) { + deckInfo.state = "" + scrollingWaveform.visible = false + } + } + } + + Timer { + id: channelchange + + interval: 5000 + repeat: true + running: false + + onTriggered: { + root.onSharedDataUpdate({ + group: { + "leftdeck": screenId === "leftdeck" && trackLoadedControl.group === "[Channel1]" ? "[Channel3]" : "[Channel1]", + "rightdeck": screenId === "rightdeck" && trackLoadedControl.group === "[Channel2]" ? "[Channel4]" : "[Channel2]", + }, + scrollingWaveform: { + "[Channel1]": true, + "[Channel2]": true, + "[Channel3]": true, + "[Channel4]": true, + }, + keyboardMode: { + "[Channel1]": false, + "[Channel2]": false, + "[Channel3]": false, + "[Channel4]": false, + }, + displayBeatloopSize: { + "[Channel1]": false, + "[Channel2]": false, + "[Channel3]": false, + "[Channel4]": false, + }, + }); + } + } + + Component.onCompleted: { + if (!root.useSharedApi) { + return; + } + + engine.makeSharedDataConnection(root.onSharedDataUpdate) + + root.onSharedDataUpdate({ + group: { + "leftdeck": "[Channel1]", + "rightdeck": "[Channel2]", + }, + scrollingWaveform: { + "[Channel1]": false, + "[Channel2]": false, + "[Channel3]": false, + "[Channel4]": false, + }, + keyboardMode: { + "[Channel1]": false, + "[Channel2]": false, + "[Channel3]": false, + "[Channel4]": false, + }, + displayBeatloopSize: { + "[Channel1]": false, + "[Channel2]": false, + "[Channel3]": false, + "[Channel4]": false, + }, + }); + } + + Rectangle { + anchors.fill: parent + color: "transparent" + + Image { + id: artwork + anchors.fill: parent + + property var player: Mixxx.PlayerManager.getPlayer(root.group) + + source: player.coverArtUrl + height: 100 + width: 100 + fillMode: Image.PreserveAspectFit + + opacity: artworkSpacer.visible ? 1 : 0.2 + z: -1 + } + } + + ColumnLayout { + anchors.fill: parent + spacing: 6 + + Rectangle { + Layout.fillWidth: true + Layout.preferredHeight: 36 + color: "transparent" + + RowLayout { + anchors.fill: parent + spacing: 1 + + S4MK3.OnAirTrack { + id: onAir + group: root.group + Layout.fillWidth: true + Layout.fillHeight: true + + scrolling: !scrollingWaveform.visible + } + } + } + + // Indicator + Rectangle { + id: deckInfo + + Layout.fillWidth: true + Layout.preferredHeight: 105 + Layout.leftMargin: 6 + Layout.rightMargin: 6 + color: "transparent" + + GridLayout { + id: gridLayout + anchors.fill: parent + columnSpacing: 6 + rowSpacing: 6 + columns: 2 + + // Section: Key + S4MK3.KeyIndicator { + id: keyIndicator + group: root.group + borderColor: smallBoxBorder + + Layout.fillWidth: true + Layout.fillHeight: true + } + + // Section: Bpm + S4MK3.BPMIndicator { + id: bpmIndicator + group: root.group + borderColor: smallBoxBorder + + Layout.fillWidth: true + Layout.fillHeight: true + } + + // Section: Key + S4MK3.TimeAndBeatloopIndicator { + id: timeIndicator + group: root.group + + Layout.fillWidth: true + Layout.preferredHeight: 72 + timeColor: smallBoxBorder + } + + // Section: Bpm + S4MK3.LoopSizeIndicator { + id: loopSizeIndicator + group: root.group + + Layout.fillWidth: true + Layout.preferredHeight: 72 + } + } + states: State { + name: "compacted" + + PropertyChanges { + target:deckInfo + Layout.preferredHeight: 28 + } + PropertyChanges { + target: gridLayout + columns: 4 + } + PropertyChanges { + target: bpmIndicator + state: "compacted" + } + PropertyChanges { + target: timeIndicator + Layout.preferredHeight: -1 + Layout.fillHeight: true + state: "compacted" + } + PropertyChanges { + target: loopSizeIndicator + Layout.preferredHeight: -1 + Layout.fillHeight: true + state: "compacted" + } + } + } + + Item { + id: scrollingWaveform + + Layout.fillWidth: true + Layout.minimumHeight: scrollingWaveform.visible ? 120 : 0 + Layout.leftMargin: 0 + Layout.rightMargin: 0 + + visible: false + + Mixxx.ControlProxy { + id: zoomControl + + group: root.group + key: "waveform_zoom" + } + + MixxxControls.WaveformDisplay { + id: singleWaveform + group: root.group + x: 0 + width: 320 + height: 100 + + Behavior on height { PropertyAnimation { duration: 90} } + anchors.fill: parent + zoom: zoomControl.value + backgroundColor: "#36000000" + + Mixxx.WaveformRendererEndOfTrack { + color: 'blue' + endOfTrackWarningTime: 30 + } + + Mixxx.WaveformRendererPreroll { + color: '#998977' + } + + Mixxx.WaveformRendererMarkRange { + // + Mixxx.WaveformMarkRange { + startControl: "loop_start_position" + endControl: "loop_end_position" + enabledControl: "loop_enabled" + color: '#00b400' + opacity: 0.7 + disabledColor: '#FFFFFF' + disabledOpacity: 0.6 + } + // + Mixxx.WaveformMarkRange { + startControl: "intro_start_position" + endControl: "intro_end_position" + color: '#2c5c9a' + opacity: 0.6 + durationTextColor: '#ffffff' + durationTextLocation: 'after' + } + // + Mixxx.WaveformMarkRange { + startControl: "outro_start_position" + endControl: "outro_end_position" + color: '#2c5c9a' + opacity: 0.6 + durationTextColor: '#ffffff' + durationTextLocation: 'before' + } + } + + Mixxx.WaveformRendererRGB { + axesColor: '#00ffffff' + lowColor: 'red' + midColor: 'green' + highColor: 'blue' + + gainAll: 1.0 + gainLow: 1.0 + gainMid: 1.0 + gainHigh: 1.0 + } + + Mixxx.WaveformRendererStem { + gainAll: 1.0 + } + + Mixxx.WaveformRendererBeat { + color: '#cfcfcf' + } + + Mixxx.WaveformRendererMark { + playMarkerColor: 'cyan' + playMarkerBackground: 'transparent' + defaultMark: Mixxx.WaveformMark { + align: "bottom|right" + color: "#FF0000" + textColor: "#FFFFFF" + text: " %1 " + } + + untilMark.showTime: true + untilMark.showBeats: true + untilMark.align: Qt.AlignCenter + untilMark.textSize: 14 + + Mixxx.WaveformMark { + control: "cue_point" + text: 'C' + align: 'top|right' + color: 'red' + textColor: '#FFFFFF' + } + Mixxx.WaveformMark { + control: "loop_start_position" + text: '↻' + align: 'top|left' + color: 'green' + textColor: '#FFFFFF' + } + Mixxx.WaveformMark { + control: "loop_end_position" + align: 'bottom|right' + color: 'green' + textColor: '#FFFFFF' + } + Mixxx.WaveformMark { + control: "intro_start_position" + align: 'top|right' + color: 'blue' + textColor: '#FFFFFF' + } + Mixxx.WaveformMark { + control: "intro_end_position" + text: '◢' + align: 'top|left' + color: 'blue' + textColor: '#FFFFFF' + } + Mixxx.WaveformMark { + control: "outro_start_position" + text: '◣' + align: 'top|right' + color: 'blue' + textColor: '#FFFFFF' + } + Mixxx.WaveformMark { + control: "outro_end_position" + align: 'top|left' + color: 'blue' + textColor: '#FFFFFF' + } + } + } + } + + Mixxx.ControlProxy { + id: deckScratching + + group: root.group + key: "scratch2_enable" + + onValueChanged: { + if (root.useSharedApi) { + return; + } + + if (value) { + waveformTimer.running = false; + scrollingWaveform.visible = true; + deckInfo.state = scrollingWaveform.visible ? "compacted" : "" + } else { + waveformTimer.running = true; + waveformTimer.restart() + } + } + } + + Timer { + id: waveformTimer + + interval: 4000 + repeat: false + running: false + + onTriggered: { + scrollingWaveform.visible = false; + deckInfo.state = scrollingWaveform.visible ? "compacted" : "" + } + } + + // Spacer + Item { + id: artworkSpacer + + Layout.fillWidth: true + Layout.minimumHeight: artworkSpacer.visible ? 120 : 0 + Layout.leftMargin: 6 + Layout.rightMargin: 6 + + visible: false + + Rectangle { + color: "transparent" + visible: parent.visible + anchors.top: parent.top + anchors.bottom: parent.bottom + x: 153 + width: 2 + } + } + + // Track progress + Item { + id: waveform + Layout.fillWidth: true + Layout.fillHeight: true + Layout.leftMargin: 6 + Layout.rightMargin: 6 + layer.enabled: true + + S4MK3.Progression { + id: progression + group: root.group + + anchors.top: parent.top + anchors.left: parent.left + anchors.bottom: parent.bottom + } + + Mixxx.WaveformOverview { + id: waveformOverview + anchors.fill: parent + player: Mixxx.PlayerManager.getPlayer(root.group) + } + + Mixxx.ControlProxy { + id: samplesControl + + group: root.group + key: "track_samples" + } + + // Hotcue + Repeater { + model: 16 + + S4MK3.HotcuePoint { + required property int index + + Mixxx.ControlProxy { + id: samplesControl + + group: root.group + key: "track_samples" + } + + Mixxx.ControlProxy { + id: hotcueEnabled + group: root.group + key: `hotcue_${index + 1}_status` + } + + Mixxx.ControlProxy { + id: hotcuePosition + group: root.group + key: `hotcue_${index + 1}_position` + } + + Mixxx.ControlProxy { + id: hotcueColor + group: root.group + key: `hotcue_${number}_color` + } + + anchors.top: parent.top + // anchors.left: parent.left + anchors.bottom: parent.bottom + visible: hotcueEnabled.value + + number: this.index + 1 + type: S4MK3.HotcuePoint.Type.OneShot + position: hotcuePosition.value / samplesControl.value + color: `#${(hotcueColor.value >> 16).toString(16).padStart(2, '0')}${((hotcueColor.value >> 8) & 255).toString(16).padStart(2, '0')}${(hotcueColor.value & 255).toString(16).padStart(2, '0')}` + } + } + + // Intro + S4MK3.HotcuePoint { + + Mixxx.ControlProxy { + id: introStartEnabled + group: root.group + key: `intro_start_enabled` + } + + Mixxx.ControlProxy { + id: introStartPosition + group: root.group + key: `intro_start_position` + } + + anchors.top: parent.top + anchors.bottom: parent.bottom + visible: introStartEnabled.value + + type: S4MK3.HotcuePoint.Type.IntroIn + position: introStartPosition.value / samplesControl.value + } + + // Extro + S4MK3.HotcuePoint { + + Mixxx.ControlProxy { + id: introEndEnabled + group: root.group + key: `intro_end_enabled` + } + + Mixxx.ControlProxy { + id: introEndPosition + group: root.group + key: `intro_end_position` + } + + anchors.top: parent.top + anchors.bottom: parent.bottom + visible: introEndEnabled.value + + type: S4MK3.HotcuePoint.Type.IntroOut + position: introEndPosition.value / samplesControl.value + } + + // Loop in + S4MK3.HotcuePoint { + Mixxx.ControlProxy { + id: loopStartPosition + group: root.group + key: `loop_start_position` + } + + anchors.top: parent.top + anchors.bottom: parent.bottom + visible: loopStartPosition.value > 0 + + type: S4MK3.HotcuePoint.Type.LoopIn + position: loopStartPosition.value / samplesControl.value + } + + // Loop out + S4MK3.HotcuePoint { + Mixxx.ControlProxy { + id: loopEndPosition + group: root.group + key: `loop_end_position` + } + + anchors.top: parent.top + anchors.bottom: parent.bottom + visible: loopEndPosition.value > 0 + + type: S4MK3.HotcuePoint.Type.LoopOut + position: loopEndPosition.value / samplesControl.value + } + } + + S4MK3.Keyboard { + id: keyboard + group: root.group + visible: false + Layout.fillWidth: true + Layout.fillHeight: true + Layout.leftMargin: 6 + Layout.rightMargin: 6 + } + } +} diff --git a/res/controllers/TraktorKontrolS4MK3Screens/S4MK3/TimeAndBeatloopIndicator.qml b/res/controllers/TraktorKontrolS4MK3Screens/S4MK3/TimeAndBeatloopIndicator.qml new file mode 100755 index 000000000000..26302f4073cc --- /dev/null +++ b/res/controllers/TraktorKontrolS4MK3Screens/S4MK3/TimeAndBeatloopIndicator.qml @@ -0,0 +1,107 @@ +/* +This module is used to define the center left section, above the waveform. +Currently this section is dedicated to show the remaining time as well as the beatloop when changing. +*/ +import QtQuick 2.14 +import QtQuick.Controls 2.15 + +import Mixxx 1.0 as Mixxx +import Mixxx.Controls 1.0 as MixxxControls + +Rectangle { + id: root + + required property string group + + property color timeColor: Qt.rgba(67/255,70/255,66/255, 1) + property color beatjumpColor: 'yellow' + + enum Mode { + RemainingTime, + BeetjumpSize + } + + property int mode: TimeAndBeatloopIndicator.Mode.RemainingTime + + radius: 6 + border.color: timeColor + border.width: 2 + color: timeColor + signal updated + + function update() { + let newValue = ""; + if (root.mode === TimeAndBeatloopIndicator.Mode.RemainingTime) { + var seconds = ((1.0 - progression.value) * duration.value); + var mins = parseInt(seconds / 60).toString(); + seconds = parseInt(seconds % 60).toString(); + + newValue = `-${mins.padStart(2, '0')}:${seconds.padStart(2, '0')}`; + } else { + newValue = (beatjump.value < 1 ? `1/${1 / beatjump.value}` : `${beatjump.value}`); + } + if (newValue === indicator.text) return; + indicator.text = newValue; + root.updated() + } + + Text { + id: indicator + anchors.centerIn: parent + text: "0.00" + + font.pixelSize: 46 + color: fontColor + + Mixxx.ControlProxy { + id: progression + group: root.group + key: "playposition" + } + + Mixxx.ControlProxy { + id: duration + group: root.group + key: "duration" + } + + Mixxx.ControlProxy { + id: beatjump + group: root.group + key: "beatjump_size" + } + + Mixxx.ControlProxy { + id: endoftrack + group: root.group + key: "end_of_track" + onValueChanged: (value) => { + root.border.color = value ? 'red' : timeColor + root.color = value ? 'red' : timeColor + root.updated() + } + } + } + + Component.onCompleted: { + progression.onValueChanged.connect(update) + duration.onValueChanged.connect(update) + beatjump.onValueChanged.connect(update) + update() + } + + states: State { + name: "compacted" + + PropertyChanges { + target: indicator + font.pixelSize: 17 + } + } + + onModeChanged: () => { + border.color = root.mode == TimeAndBeatloopIndicator.Mode.BeetjumpSize ? beatjumpColor : timeColor + color = root.mode == TimeAndBeatloopIndicator.Mode.BeetjumpSize ? beatjumpColor : timeColor + indicator.color = root.mode == TimeAndBeatloopIndicator.Mode.BeetjumpSize ? 'black' : 'white' + } +} diff --git a/res/controllers/TraktorKontrolS4MK3Screens/S4MK3/WaveformOverview.qml b/res/controllers/TraktorKontrolS4MK3Screens/S4MK3/WaveformOverview.qml new file mode 100755 index 000000000000..ade89743521d --- /dev/null +++ b/res/controllers/TraktorKontrolS4MK3Screens/S4MK3/WaveformOverview.qml @@ -0,0 +1,165 @@ +/* +This module is used to waveform overview, at the bottom of the screen. It is reusing component definition of `WaveformOverview.qml` but remove +the link to markers and provide hooks with screen update/redraw, needed for partial updates. +Currently this section is dedicated to BPM and tempo fader information. +*/ +import QtQuick 2.15 +import QtQuick.Window 2.15 + +import Mixxx 1.0 as Mixxx +import Mixxx.Controls 1.0 as MixxxControls + +Item { + id: root + + required property string group + property var deckPlayer: Mixxx.PlayerManager.getPlayer(root.group) + property real scale: 0.2 + + signal updated + + visible: false + antialiasing: true + anchors.fill: parent + + Connections { + onGroupChanged: { + deckPlayer = Mixxx.PlayerManager.getPlayer(root.group) + console.log("Group changed!!") + root.updated() + } + } + + Rectangle { + color: "white" + anchors.top: parent.top + anchors.bottom: parent.bottom + x: 153 + width: 2 + } + Item { + id: waveformContainer + + property real duration: samplesControl.value / sampleRateControl.value + + anchors.fill: parent + clip: true + + Mixxx.ControlProxy { + id: samplesControl + + group: root.group + key: "track_samples" + } + + Mixxx.ControlProxy { + id: sampleRateControl + + group: root.group + key: "track_samplerate" + } + + Mixxx.ControlProxy { + id: playPositionControl + + group: root.group + key: "playposition" + } + + Mixxx.ControlProxy { + id: rateRatioControl + + group: root.group + key: "rate_ratio" + } + + Mixxx.ControlProxy { + id: zoomControl + + group: root.group + key: "waveform_zoom" + } + + Item { + id: waveformBeat + + property real effectiveZoomFactor: (zoomControl.value * rateRatioControl.value / root.scale) * 6 + + width: waveformContainer.duration * effectiveZoomFactor + height: parent.height + x: 0.5 * waveformContainer.width - playPositionControl.value * width + visible: true + + Shape { + id: preroll + + property real triangleHeight: waveformBeat.height + property real triangleWidth: 0.25 * waveformBeat.effectiveZoomFactor + property int numTriangles: Math.ceil(width / triangleWidth) + + anchors.top: waveformBeat.top + anchors.right: waveformBeat.left + width: Math.max(0, waveformBeat.x) + height: waveformBeat.height + + ShapePath { + strokeColor: 'red' + strokeWidth: 1 + fillColor: "transparent" + + PathMultiline { + paths: { + let p = []; + for (let i = 0; i < preroll.numTriangles; i++) { + p.push([Qt.point(preroll.width - i * preroll.triangleWidth, preroll.triangleHeight / 2), Qt.point(preroll.width - (i + 1) * preroll.triangleWidth, 0), Qt.point(preroll.width - (i + 1) * preroll.triangleWidth, preroll.triangleHeight), Qt.point(preroll.width - i * preroll.triangleWidth, preroll.triangleHeight / 2)]); + } + return p; + } + } + } + } + + Shape { + id: postroll + + property real triangleHeight: waveformBeat.height + property real triangleWidth: 0.25 * waveformBeat.effectiveZoomFactor + property int numTriangles: Math.ceil(width / triangleWidth) + + anchors.top: waveformBeat.top + anchors.left: waveformBeat.right + width: waveformContainer.width / 2 + height: waveformBeat.height + + ShapePath { + strokeColor: 'red' + strokeWidth: 1 + fillColor: "transparent" + + PathMultiline { + paths: { + let p = []; + for (let i = 0; i < postroll.numTriangles; i++) { + p.push([Qt.point(i * postroll.triangleWidth, postroll.triangleHeight / 2), Qt.point((i + 1) * postroll.triangleWidth, 0), Qt.point((i + 1) * postroll.triangleWidth, postroll.triangleHeight), Qt.point(i * postroll.triangleWidth, postroll.triangleHeight / 2)]); + } + return p; + } + } + } + } + } + + MixxxControls.WaveformOverview { + id: waveformOverview + // property real duration: samplesControl.value / sampleRateControl.onValueChanged + + player: root.player + anchors.fill: parent + channels: Mixxx.WaveformOverview.Channels.BothChannels + renderer: Mixxx.WaveformOverview.Renderer.RGB + colorHigh: 'white' + colorMid: 'blue' + colorLow: 'green' + } + } +} diff --git a/res/controllers/TraktorKontrolS4MK3Screens/S4MK3/qmldir b/res/controllers/TraktorKontrolS4MK3Screens/S4MK3/qmldir new file mode 100644 index 000000000000..6c76347ed734 --- /dev/null +++ b/res/controllers/TraktorKontrolS4MK3Screens/S4MK3/qmldir @@ -0,0 +1,12 @@ +module S4MK3 +BPMIndicator 1.0 BPMIndicator.qml +HotcuePoint 1.0 HotcuePoint.qml +Keyboard 1.0 Keyboard.qml +KeyIndicator 1.0 KeyIndicator.qml +LoopSizeIndicator 1.0 LoopSizeIndicator.qml +OnAirTrack 1.0 OnAirTrack.qml +Progression 1.0 Progression.qml +TimeAndBeatloopIndicator 1.0 TimeAndBeatloopIndicator.qml +WaveformOverview 1.0 WaveformOverview.qml +SplashOff 1.0 SplashOff.qml +StockScreen 1.0 StockScreen.qml diff --git a/res/qml/WaveformRow.qml b/res/qml/WaveformRow.qml index be129fc42bfa..82a8eb43d14d 100644 --- a/res/qml/WaveformRow.qml +++ b/res/qml/WaveformRow.qml @@ -15,6 +15,8 @@ Item { property string group // required property var deckPlayer: Mixxx.PlayerManager.getPlayer(group) + property int zoomControlRatio: 100 + property alias shader: shader Item { id: waveformContainer @@ -118,7 +120,7 @@ Item { Item { id: waveform - property real effectiveZoomFactor: (1 / rateRatioControl.value) * (100 / zoomControl.value) + property real effectiveZoomFactor: (1 / rateRatioControl.value) * (root.zoomControlRatio / zoomControl.value) width: waveformContainer.duration * effectiveZoomFactor height: parent.height @@ -126,6 +128,7 @@ Item { visible: root.deckPlayer.isLoaded WaveformShader { + id: shader group: root.group anchors.fill: parent } diff --git a/src/controllers/bulk/bulksupported.h b/src/controllers/bulk/bulksupported.h index a7bf6c916d60..f10673e87ba4 100644 --- a/src/controllers/bulk/bulksupported.h +++ b/src/controllers/bulk/bulksupported.h @@ -28,4 +28,5 @@ constexpr static bulk_support_lookup bulk_supported[] = { {{0x06f8, 0xb107}, {0x83, 0x03, std::nullopt}}, // Hercules Mk4 {{0x06f8, 0xb100}, {0x86, 0x06, std::nullopt}}, // Hercules Mk2 {{0x06f8, 0xb120}, {0x82, 0x03, std::nullopt}}, // Hercules MP3 LE / Glow + {{0x17cc, 0x1720}, {0x00, 0x03, 0x04}}, // Traktor NI S4 Mk3 }; diff --git a/tools/README b/tools/README index def2a3d4040b..3055fbed113c 100644 --- a/tools/README +++ b/tools/README @@ -20,3 +20,11 @@ cd build && gcc ../tools/dummy_hid_device.c -lhidapi-hidraw -o dummy_hid_device # Allow the created hidraw device to be accessed by the user. You may also set the write udev rules. Finally, you can also run Mixxx as root, but that's not recommended. sudo chown "$USER" "$(ls -1t /dev/hidraw* | head -n 1)" ``` + +## Traktor S4 Mk3 Screen drawing + +This small program can be used directly to draw arbitrary rectangles on the Traktor S4 Mk3 screens. It may also be useful for one to perform tests on top of the existing reversed engineered protocol. + +```sh +cd build && gcc ../tools/traktor_s4_mk3_screen_test.c `pkg-config --cflags --libs libusb-1.0` -o traktor_s4_mk3_screen_test && ./traktor_s4_mk3_screen_test +``` diff --git a/tools/clang_format.py b/tools/clang_format.py index 16076470c916..baeface76d29 100755 --- a/tools/clang_format.py +++ b/tools/clang_format.py @@ -49,7 +49,7 @@ def run_clang_format_on_lines(rootdir, file_to_format, stylepath=None): ", ".join("{}-{}".format(*x) for x in file_to_format.lines), ) - filename = os.path.join(rootdir, file_to_format.filename) + filename = os.path.join(rootdir, file_to_format.filename).strip() cmd = [ "clang-format", "--style=file", diff --git a/tools/traktor_s4_mk3_screen_test.c b/tools/traktor_s4_mk3_screen_test.c new file mode 100644 index 000000000000..cf14fd3833a2 --- /dev/null +++ b/tools/traktor_s4_mk3_screen_test.c @@ -0,0 +1,110 @@ +#include +#include +#include +#include +#include +#include +#include +#include + +#define VENDOR_ID 0x17cc +#define PRODUCT_ID 0x1720 +#define IN_EPADDR 0x00 +#define OUT_EPADDR 0x03 + +static const uint8_t header_data[] = { + 0x84, 0x0, 0x0, 0x21, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, // Draw offset (y=0, x=0) + 0x1, + 0x40, + 0x0, + 0xf0, // Draw dimenssion (width=320, height=240) +}; +static const uint8_t footer_data[] = { + 0x40, 0x00, 0x00, 0x00}; + +int main(int argc, char** argv) { + libusb_context* context; + int transferred; + + if (argc != 7) { + fprintf(stderr, "Usage: %s \n", *argv); + return -1; + } + + libusb_init(&context); + + libusb_device_handle* handle = libusb_open_device_with_vid_pid( + context, VENDOR_ID, PRODUCT_ID); + + if (!handle) { + fprintf(stderr, "Unable to open USB Bulk device\n"); + return -1; + } + + uint8_t screen_idx = atoi(argv[1]); + + if (screen_idx != 0 && screen_idx != 1) { + fprintf(stderr, "Invalid screen ID %d\n", screen_idx); + return -1; + } + + uint16_t x = atoi(argv[2]); + uint16_t y = atoi(argv[3]); + uint16_t width = atoi(argv[4]); + uint16_t height = atoi(argv[5]); + uint16_t color = strtol(argv[6], NULL, 2); + + uint8_t* data = malloc(width * height * sizeof(uint16_t) + + sizeof(header_data) + sizeof(footer_data)); + uint8_t* header = data; + + memcpy(header, header_data, sizeof(header_data)); + + header[2] = screen_idx; + + header[8] = x >> 8; + header[9] = x & 0xff; + header[10] = y >> 8; + header[11] = y & 0xff; + + header[12] = width >> 8; + header[13] = width & 0xff; + header[14] = height >> 8; + header[15] = height & 0xff; + + printf("draw x=%d,y=%d,width=%d,height=%d with color %x\n", x, y, width, height, color); + + size_t payload_size = width * height * sizeof(uint16_t) + + sizeof(header_data) + sizeof(footer_data); + uint8_t* payload = data + sizeof(header_data); + uint8_t* footer = payload + width * height * sizeof(uint16_t); + + for (int px = 0; px < width * height; px++) { + payload[px * sizeof(uint16_t)] = color >> 8; + payload[px * sizeof(uint16_t) + 1] = color & 0xff; + } + + memcpy(footer, footer_data, sizeof(footer_data)); + + footer[2] = screen_idx; + + clock_t start, end; + double cpu_time_used; + + start = clock(); + int ret = libusb_bulk_transfer(handle, OUT_EPADDR, data, payload_size, &transferred, 0); + end = clock(); + cpu_time_used = ((double)(end - start)) / CLOCKS_PER_SEC; + + if (ret < 0) { + fprintf(stderr, "Unable to send to USB Bulk device\n"); + + } else { + fprintf(stderr, "Sent %d bytes in %f ms\n", transferred, cpu_time_used); + } + + libusb_close(handle); + libusb_exit(context); + + return 0; +} From 7b7a4516c9264392fffa6a7d641c836ee3aa09ba Mon Sep 17 00:00:00 2001 From: Antoine C Date: Thu, 19 Sep 2024 10:16:29 +0100 Subject: [PATCH 2/9] feat: add Joe Easton's inspired theme for S4Mk3 screens --- .../Traktor Kontrol S4 MK3.bulk.xml | 1091 +++++------- .../Traktor Kontrol S4 MK3.hid.xml | 183 ++ res/controllers/Traktor-Kontrol-S4-MK3.js | 278 ++- .../TraktorKontrolS4MK3Screens.qml | 2 +- .../S4MK3/AdvancedScreen.qml | 63 + .../AdvancedScreen/Browser/BrowserFooter.qml | 340 ++++ .../AdvancedScreen/Browser/BrowserHeader.qml | 223 +++ .../AdvancedScreen/Browser/ListDelegate.qml | 262 +++ .../AdvancedScreen/Browser/ListHighlight.qml | 19 + .../AdvancedScreen/Browser/TrackFooter.qml | 353 ++++ .../AdvancedScreen/Browser/TrackView.qml | 202 +++ .../S4MK3/AdvancedScreen/Browser/Triangle.qml | 29 + .../S4MK3/AdvancedScreen/DeckScreen.qml | 184 ++ .../S4MK3/AdvancedScreen/Defines/Colors.qml | 549 ++++++ .../AdvancedScreen/Defines/Durations.qml | 8 + .../S4MK3/AdvancedScreen/Defines/Font.qml | 17 + .../S4MK3/AdvancedScreen/Defines/Margins.qml | 7 + .../S4MK3/AdvancedScreen/Defines/Settings.qml | 296 ++++ .../S4MK3/AdvancedScreen/Defines/Utils.qml | 126 ++ .../AdvancedScreen/Overlays/BankInfo.qml | 193 ++ .../Overlays/BankInfoDetails.qml | 77 + .../S4MK3/AdvancedScreen/Overlays/CueInfo.qml | 152 ++ .../Overlays/CueInfoDetails.qml | 73 + .../AdvancedScreen/Overlays/FXInfoDetails.qml | 66 + .../AdvancedScreen/Overlays/GridControls.qml | 209 +++ .../Overlays/GridInfoDetails.qml | 160 ++ .../AdvancedScreen/Overlays/JumpControls.qml | 239 +++ .../Overlays/JumpInfoDetails.qml | 45 + .../AdvancedScreen/Overlays/LoopControls.qml | 230 +++ .../Overlays/LoopInfoDetails.qml | 57 + .../Overlays/QuickFXSelector.qml | 151 ++ .../AdvancedScreen/Overlays/RollControls.qml | 230 +++ .../AdvancedScreen/Overlays/ToneControls.qml | 247 +++ .../Overlays/ToneInfoDetails.qml | 44 + .../Overlays/TopInfoDetails.qml | 152 ++ .../S4MK3/AdvancedScreen/ViewModels/Cell.qml | 54 + .../AdvancedScreen/ViewModels/DeckInfo.qml | 1563 +++++++++++++++++ .../AdvancedScreen/ViewModels/HotCue.qml | 42 + .../AdvancedScreen/ViewModels/HotCues.qml | 65 + .../AdvancedScreen/Views/BrowserView.qml | 322 ++++ .../S4MK3/AdvancedScreen/Views/Dimensions.qml | 15 + .../S4MK3/AdvancedScreen/Views/EmptyDeck.qml | 47 + .../S4MK3/AdvancedScreen/Views/StemDeck.qml | 28 + .../S4MK3/AdvancedScreen/Views/TrackDeck.qml | 222 +++ .../Waveform/StemColorIndicators.qml | 81 + .../AdvancedScreen/Waveform/StemWaveforms.qml | 59 + .../Waveform/WaveformContainer.qml | 23 +- .../Waveform/WaveformOverview.qml | 226 +++ .../AdvancedScreen/Widgets/BpmDisplay.qml | 27 + .../AdvancedScreen/Widgets/DeckHeader.qml | 90 + .../AdvancedScreen/Widgets/KeyDisplay.qml | 46 + .../S4MK3/AdvancedScreen/Widgets/LoopSize.qml | 87 + .../AdvancedScreen/Widgets/PhaseMeter.qml | 94 + .../AdvancedScreen/Widgets/ProgressBar.qml | 60 + .../S4MK3/AdvancedScreen/Widgets/Slider.qml | 111 ++ .../S4MK3/AdvancedScreen/Widgets/StateBar.qml | 34 + .../AdvancedScreen/Widgets/StemOverlay.qml | 257 +++ .../AdvancedScreen/Widgets/TempoAdjust.qml | 184 ++ .../AdvancedScreen/Widgets/TrackRating.qml | 42 + .../S4MK3/BPMIndicator.qml | 39 +- .../S4MK3/KeyIndicator.qml | 19 +- .../S4MK3/Keyboard.qml | 13 +- .../S4MK3/LoopSizeIndicator.qml | 54 +- .../S4MK3/Progression.qml | 18 +- .../S4MK3/SplashOff.qml | 2 +- .../S4MK3/StockScreen.qml | 2 +- .../S4MK3/TimeAndBeatloopIndicator.qml | 32 +- .../S4MK3/WaveformOverview.qml | 3 - .../TraktorKontrolS4MK3Screens/S4MK3/qmldir | 1 + 69 files changed, 9660 insertions(+), 859 deletions(-) create mode 100755 res/controllers/TraktorKontrolS4MK3Screens/S4MK3/AdvancedScreen.qml create mode 100755 res/controllers/TraktorKontrolS4MK3Screens/S4MK3/AdvancedScreen/Browser/BrowserFooter.qml create mode 100755 res/controllers/TraktorKontrolS4MK3Screens/S4MK3/AdvancedScreen/Browser/BrowserHeader.qml create mode 100755 res/controllers/TraktorKontrolS4MK3Screens/S4MK3/AdvancedScreen/Browser/ListDelegate.qml create mode 100755 res/controllers/TraktorKontrolS4MK3Screens/S4MK3/AdvancedScreen/Browser/ListHighlight.qml create mode 100755 res/controllers/TraktorKontrolS4MK3Screens/S4MK3/AdvancedScreen/Browser/TrackFooter.qml create mode 100755 res/controllers/TraktorKontrolS4MK3Screens/S4MK3/AdvancedScreen/Browser/TrackView.qml create mode 100755 res/controllers/TraktorKontrolS4MK3Screens/S4MK3/AdvancedScreen/Browser/Triangle.qml create mode 100755 res/controllers/TraktorKontrolS4MK3Screens/S4MK3/AdvancedScreen/DeckScreen.qml create mode 100755 res/controllers/TraktorKontrolS4MK3Screens/S4MK3/AdvancedScreen/Defines/Colors.qml create mode 100755 res/controllers/TraktorKontrolS4MK3Screens/S4MK3/AdvancedScreen/Defines/Durations.qml create mode 100755 res/controllers/TraktorKontrolS4MK3Screens/S4MK3/AdvancedScreen/Defines/Font.qml create mode 100755 res/controllers/TraktorKontrolS4MK3Screens/S4MK3/AdvancedScreen/Defines/Margins.qml create mode 100755 res/controllers/TraktorKontrolS4MK3Screens/S4MK3/AdvancedScreen/Defines/Settings.qml create mode 100755 res/controllers/TraktorKontrolS4MK3Screens/S4MK3/AdvancedScreen/Defines/Utils.qml create mode 100755 res/controllers/TraktorKontrolS4MK3Screens/S4MK3/AdvancedScreen/Overlays/BankInfo.qml create mode 100755 res/controllers/TraktorKontrolS4MK3Screens/S4MK3/AdvancedScreen/Overlays/BankInfoDetails.qml create mode 100755 res/controllers/TraktorKontrolS4MK3Screens/S4MK3/AdvancedScreen/Overlays/CueInfo.qml create mode 100755 res/controllers/TraktorKontrolS4MK3Screens/S4MK3/AdvancedScreen/Overlays/CueInfoDetails.qml create mode 100755 res/controllers/TraktorKontrolS4MK3Screens/S4MK3/AdvancedScreen/Overlays/FXInfoDetails.qml create mode 100755 res/controllers/TraktorKontrolS4MK3Screens/S4MK3/AdvancedScreen/Overlays/GridControls.qml create mode 100755 res/controllers/TraktorKontrolS4MK3Screens/S4MK3/AdvancedScreen/Overlays/GridInfoDetails.qml create mode 100755 res/controllers/TraktorKontrolS4MK3Screens/S4MK3/AdvancedScreen/Overlays/JumpControls.qml create mode 100755 res/controllers/TraktorKontrolS4MK3Screens/S4MK3/AdvancedScreen/Overlays/JumpInfoDetails.qml create mode 100755 res/controllers/TraktorKontrolS4MK3Screens/S4MK3/AdvancedScreen/Overlays/LoopControls.qml create mode 100755 res/controllers/TraktorKontrolS4MK3Screens/S4MK3/AdvancedScreen/Overlays/LoopInfoDetails.qml create mode 100755 res/controllers/TraktorKontrolS4MK3Screens/S4MK3/AdvancedScreen/Overlays/QuickFXSelector.qml create mode 100755 res/controllers/TraktorKontrolS4MK3Screens/S4MK3/AdvancedScreen/Overlays/RollControls.qml create mode 100755 res/controllers/TraktorKontrolS4MK3Screens/S4MK3/AdvancedScreen/Overlays/ToneControls.qml create mode 100755 res/controllers/TraktorKontrolS4MK3Screens/S4MK3/AdvancedScreen/Overlays/ToneInfoDetails.qml create mode 100755 res/controllers/TraktorKontrolS4MK3Screens/S4MK3/AdvancedScreen/Overlays/TopInfoDetails.qml create mode 100755 res/controllers/TraktorKontrolS4MK3Screens/S4MK3/AdvancedScreen/ViewModels/Cell.qml create mode 100755 res/controllers/TraktorKontrolS4MK3Screens/S4MK3/AdvancedScreen/ViewModels/DeckInfo.qml create mode 100755 res/controllers/TraktorKontrolS4MK3Screens/S4MK3/AdvancedScreen/ViewModels/HotCue.qml create mode 100755 res/controllers/TraktorKontrolS4MK3Screens/S4MK3/AdvancedScreen/ViewModels/HotCues.qml create mode 100755 res/controllers/TraktorKontrolS4MK3Screens/S4MK3/AdvancedScreen/Views/BrowserView.qml create mode 100755 res/controllers/TraktorKontrolS4MK3Screens/S4MK3/AdvancedScreen/Views/Dimensions.qml create mode 100755 res/controllers/TraktorKontrolS4MK3Screens/S4MK3/AdvancedScreen/Views/EmptyDeck.qml create mode 100755 res/controllers/TraktorKontrolS4MK3Screens/S4MK3/AdvancedScreen/Views/StemDeck.qml create mode 100755 res/controllers/TraktorKontrolS4MK3Screens/S4MK3/AdvancedScreen/Views/TrackDeck.qml create mode 100755 res/controllers/TraktorKontrolS4MK3Screens/S4MK3/AdvancedScreen/Waveform/StemColorIndicators.qml create mode 100755 res/controllers/TraktorKontrolS4MK3Screens/S4MK3/AdvancedScreen/Waveform/StemWaveforms.qml create mode 100644 res/controllers/TraktorKontrolS4MK3Screens/S4MK3/AdvancedScreen/Waveform/WaveformOverview.qml create mode 100755 res/controllers/TraktorKontrolS4MK3Screens/S4MK3/AdvancedScreen/Widgets/BpmDisplay.qml create mode 100755 res/controllers/TraktorKontrolS4MK3Screens/S4MK3/AdvancedScreen/Widgets/DeckHeader.qml create mode 100755 res/controllers/TraktorKontrolS4MK3Screens/S4MK3/AdvancedScreen/Widgets/KeyDisplay.qml create mode 100755 res/controllers/TraktorKontrolS4MK3Screens/S4MK3/AdvancedScreen/Widgets/LoopSize.qml create mode 100755 res/controllers/TraktorKontrolS4MK3Screens/S4MK3/AdvancedScreen/Widgets/PhaseMeter.qml create mode 100755 res/controllers/TraktorKontrolS4MK3Screens/S4MK3/AdvancedScreen/Widgets/ProgressBar.qml create mode 100755 res/controllers/TraktorKontrolS4MK3Screens/S4MK3/AdvancedScreen/Widgets/Slider.qml create mode 100755 res/controllers/TraktorKontrolS4MK3Screens/S4MK3/AdvancedScreen/Widgets/StateBar.qml create mode 100755 res/controllers/TraktorKontrolS4MK3Screens/S4MK3/AdvancedScreen/Widgets/StemOverlay.qml create mode 100755 res/controllers/TraktorKontrolS4MK3Screens/S4MK3/AdvancedScreen/Widgets/TempoAdjust.qml create mode 100755 res/controllers/TraktorKontrolS4MK3Screens/S4MK3/AdvancedScreen/Widgets/TrackRating.qml diff --git a/res/controllers/Traktor Kontrol S4 MK3.bulk.xml b/res/controllers/Traktor Kontrol S4 MK3.bulk.xml index c7bc44712ea7..94ce7ad8b36c 100644 --- a/res/controllers/Traktor Kontrol S4 MK3.bulk.xml +++ b/res/controllers/Traktor Kontrol S4 MK3.bulk.xml @@ -25,217 +25,67 @@ Use the shared data API to enable communication between the screens and the buttons. Requires a custom Mixxx build using the feature in PR#12199 + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + - + + + + + + + + + + + + + + + - - - - - + + + - - - - - + + - - - + - + variable="cueCueColor" + type="enum" + default="10" + label="CueCueColor"> + 0 + 1 + 2 + 3 + 4 + 5 + 6 + 7 + 8 + 9 + 10 + 11 + 12 + 13 + 14 + 15 + 16 + 17 + + + - - - - - + + + + + + + + + + + + + + + + + + + + + - - - - - - - - - - - + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + label="Alternative color when track end warning"/> + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + label="Disable the hotcue overlay"/> - - - + default="2" + type="real" + min="0.1" + max="15" + step="0.1" + precision="2" + label="FxOverlayTimer"/> @@ -880,17 +596,12 @@ variable="hideEffectsOverlay1" type="boolean" default="false" - label="hideEffectsOverlay1"> - - - - + label="Disable the effects overlay on the left deck"/> + label="Disable the effects overlay on the right deck"/> @@ -898,36 +609,24 @@ variable="hideToneOverlay" type="boolean" default="false" - label="hideToneOverlay"> - - - - + label="Disable the tone overlay"/> - - - + label="Disable the loop overlay"/> - - - + label="Disable the roll overlay"/> + label="Disable the tone pads overlay appearing"/> + diff --git a/res/controllers/Traktor Kontrol S4 MK3.hid.xml b/res/controllers/Traktor Kontrol S4 MK3.hid.xml index 4efc26d70413..7d6883a2e236 100644 --- a/res/controllers/Traktor Kontrol S4 MK3.hid.xml +++ b/res/controllers/Traktor Kontrol S4 MK3.hid.xml @@ -395,6 +395,189 @@ + + + + + + + + + + + + + + +