Denon LC6000 Screen Support

myalteredsoul
myalteredsoul Member Posts: 201 Advisor

Just creating a post to request support for the Denon LC6000 Jog Wheel Screen. More specifically, jog position, artwork, loop length, and beat jump length.

Tagged:
«1

Comments

  • V'NZ
    V'NZ Member Posts: 1 Newcomer
  • Robert Jean-Louis
    Robert Jean-Louis Member Posts: 2 Member

    Is there a TSI file for the LC6000? And I think you will need bome to get the artwork to show up from traktor to the LC6000

  • PK The DJ
    PK The DJ Member Posts: 994 Guru

    Surely you need to request this from Denon DJ? They're the guys that decide what can be displayed on the screen, via the unit's firmware.

  • Tellmeaboutit123
    Tellmeaboutit123 Member Posts: 493 Guru

    Surely it needs both to agree? HID would be great. NI would need to agree that no?

  • myalteredsoul
    myalteredsoul Member Posts: 201 Advisor

    The dev kit was already sent to NI for the LC6000, SC5000/m, and SC6000/m. At least that’s what’s been said by mods on the denonsjforum.

  • PK The DJ
    PK The DJ Member Posts: 994 Guru

    They may well have the dev kit, but they can only display what is permitted by the information in that dev kit - which is down to Denon.

    I know for example that when Atomix added support for the Prime series, people asked if certain things could be shown on the jog display, and the answer was that it can only display what Denon permit (via the dev kit).

  • myalteredsoul
    myalteredsoul Member Posts: 201 Advisor

    Currently, album artwork, track position, jog position, and numbers 00-99 are able to be displayed without modifications. The screens are independently addressable to output anything you want (treating it as a lcd display hat).

  • Aquadics
    Aquadics Member Posts: 59 Helper
    edited July 2022

    The topic is super simple. I don't know if NI got a dev kit or what ever from Denon or not but the problem is that Denon is not willing to share the MIDI SysEx documentation for LC6000 with there user base.

    (There are only the MIDI controller messages documented and available for us )

    This is really bad because we bought a MIDI Controller for $700 and did not the get the required documentation to use it properly.

    I would say this is the main reason why LC6000 is more or less a flop. The only way to use it professional is with a SC6000/M and even in this combination there is still so much to improve. (Prime 4 offers much better options)

    That is the whole story and explains why you are getting the LC6000 currently for free as a gift if you purchase a SC6000/M. Honestly for me LC6000 was only a disappointment from software side and a waist of a lot of money. :-(

    And no, we don't need HID, we dont need NI, we only need this f..ing LC6000 SYSEX MIDI DOCUMENTATION which Denon DJ is not willing to share with us.

  • myalteredsoul
    myalteredsoul Member Posts: 201 Advisor

    The screen info isn't there, but the rest of the control surface messages are available from http://cdn.inmusicbrands.com/denondj/lc6000/LC6000-PRIME-MIDI-Specification-v1.0.pdf

    Display Assignments:


    import airAssignments 1.0import InputAssignment 0.1import OutputAssignment 0.1import QtQuick 2.0

    MidiAssignment { id: assignment objectName: "LC6000 Center Wheel Assignment"

    readonly property string deckIndex: { // `deviceName` is set in `airAssignments::Midi::Thread::run()` if(deviceName.endsWith('3') || deviceName.endsWith('4')) { deviceName.slice(-1) } else { '2' } }

    readonly property QObjProperty pCurrentDeviceArtwork: Planck.getProperty("/Client/Librarian/DevicesController/CurrentDeviceArtwork") readonly property bool hasCurrentDeviceArtwork: pCurrentDeviceArtwork && pCurrentDeviceArtwork.translator ? pCurrentDeviceArtwork.translator.size > 0 : false

    readonly property QObjProperty pSongLoaded: Planck.getProperty("/Engine/Deck%1/Track/SongLoaded".arg(deckIndex)) readonly property bool songLoaded: pSongLoaded && pSongLoaded.translator ? pSongLoaded.translator.state : false

    readonly property QObjProperty pDeckColor: Planck.getProperty("/Engine/Deck%1/JogColor".arg(deckIndex)) property color deckColor: pDeckColor && pDeckColor.translator ? pDeckColor.translator.color : Qt.rgba(1,1,1,1) onDeckColorChanged: device.deckColor = deckColor

    Painter { id: pantr objectName: "Album Artwork" height: 198 width: 198 format: Painter.AZ01_CENTER_WHEEL_DISPLAY

    readonly property QObjProperty pAlbumArt: Planck.getProperty("/Engine/Deck%1/AlbumArt".arg(assignment.deckIndex)) readonly property var albumArt: pAlbumArt && pAlbumArt.translator ? pAlbumArt.translator.bytearray : ""

    readonly property var currentDeviceArtwork: assignment.pCurrentDeviceArtwork && assignment.pCurrentDeviceArtwork.translator ? assignment.pCurrentDeviceArtwork.translator.bytearray : ""

    property bool hasValidArtwork: false

    onHasValidArtworkChanged: updateArtworkSettingsOnDevice()

    readonly property bool songLoaded: assignment.songLoaded onSongLoadedChanged: updateArtworkSettingsOnDevice()

    readonly property int deviceArtworkIndex: 2

    function updateArtworkSettingsOnDevice() { device.artwork.enabled = true

    if(assignment.hasCurrentDeviceArtwork) { device.artwork.index = deviceArtworkIndex } else if(hasValidArtwork && assignment.songLoaded) { device.artwork.index = 1 } else { device.artwork.index = 0 } }

    Component.onCompleted: { draw() updateArtworkSettingsOnDevice() }

    onAlbumArtChanged: { if(!assignment.hasCurrentDeviceArtwork) { draw() updateArtworkSettingsOnDevice() } }

    onCurrentDeviceArtworkChanged: { draw() updateArtworkSettingsOnDevice() }

    function draw() { begin()

    clear("white") translate(width, height) rotate(180)

    if(!drawByteArray(assignment.hasCurrentDeviceArtwork ? currentDeviceArtwork : albumArt, 0, 0, width, height)) { hasValidArtwork = false return }

    drawHole("black", Qt.rect(0, 0, width, height), Qt.point(width / 2, height / 2), (width / 2) - 4)

    var data = imageToByteArrays(assignment.hasCurrentDeviceArtwork ? deviceArtworkIndex : 1, 0x10, 0x01)

    Midi.sendByteArrays(data)

    device.setElementARGB(0, Qt.rgba(1.0, 1.0, 1.0, device.artwork.alpha))

    hasValidArtwork = true } }


    Item { objectName: "Logo"

    readonly property bool logoEnabled: !assignment.hasCurrentDeviceArtwork && !assignment.songLoaded onLogoEnabledChanged: device.logo.enabled = logoEnabled Component.onCompleted: device.logo.enabled = logoEnabled }

    Item { id: platterPosition objectName: "Platter Position"

    readonly property bool platterPositionEnabled: assignment.songLoaded onPlatterPositionEnabledChanged: device.platterPosition.enabled = platterPositionEnabled

    Timer { interval: 12 running: platterPosition.platterPositionEnabled repeat: true

    property int lastPos: 0 property int lastSlipPos: 0 readonly property real sampleRate: 44100.0 readonly property real divider: 60.0 / (33.0 + (1.0/3.0)) readonly property int mod: divider * sampleRate

    property var positionItems: [ Planck.createPositionItem("/Private/Deck%1/MidiSamplePosition".arg(assignment.deckIndex)), ]

    Component.onDestruction: { Planck.destroyPositionItem(positionItems[0])

    positionItems = []; }

    onTriggered: { var posItem = positionItems[0]

    if(posItem) { posItem.update() var pos = posItem.currentPosition(); var slipPos = posItem.currentSlipPosition();

    if(pos !== lastPos) { lastPos = pos const modulo = (pos % mod + mod) % mod const angle = (modulo / sampleRate) / divider Midi.sendPitch(0, angle) } if(slipPos !== lastSlipPos) { lastSlipPos = slipPos const modulo = (slipPos % mod + mod) % mod const slipAngle = (modulo / sampleRate) / divider Midi.sendPitch(1, slipAngle) } } } } }

    Item { objectName: "Slip Position"

    readonly property QObjProperty pSlipModeActive: Planck.getProperty("/Engine/Deck%1/Track/SlipModeActive".arg(assignment.deckIndex)) readonly property bool slipModeActive: pSlipModeActive && pSlipModeActive.translator ? pSlipModeActive.translator.state : false

    readonly property bool slipModeEnabled: device.slipPosition.active = slipModeActive && assignment.songLoaded onSlipModeEnabledChanged: device.slipPosition.active = slipModeEnabled }

    Item { id: loop objectName: "Loop"

    readonly property QObjProperty pAutoLoopIndex: Planck.getProperty("/Engine/Deck%1/Track/AutoLoopIndex".arg(assignment.deckIndex)) readonly property int currentIndex: pAutoLoopIndex.translator ? pAutoLoopIndex.translator.index : 0 readonly property bool editable: pAutoLoopIndex.translator ? pAutoLoopIndex.translator.editable : false

    readonly property QObjProperty pLoopEnableState: Planck.getProperty("/Engine/Deck%1/Track/LoopEnableState".arg(assignment.deckIndex)) readonly property bool loopEnableState: pLoopEnableState && pLoopEnableState.translator ? pLoopEnableState.translator.state : false

    readonly property bool loopEnabled: loopEnableState onLoopEnabledChanged: { loopHideTimer.stop() if(loopEnabled) { show() } else { hide() } }

    readonly property QObjProperty pAutoLoopLabel: Planck.getProperty("/Engine/Deck%1/Track/AutoLoopLabel%2".arg(assignment.deckIndex).arg(currentIndex + 1)) readonly property string loopText: editable ? pAutoLoopLabel.translator ? pAutoLoopLabel.translator.string : "" : "--" onLoopTextChanged: { device.loopAndLayerText.text = loopText show() }

    function show() { if(assignment.songLoaded) { device.loopAndLayerText.enabled = true device.artwork.alpha = 0.5

    if(!loopEnabled) { loopHideTimer.start() } } }

    function hide() { device.loopAndLayerText.enabled = false device.artwork.alpha = 0.85 }

    Timer { id: loopHideTimer interval: 1000 repeat: false onTriggered: loop.hide() } }

    OutputAssignment { objectName: "Screen Brightness"

    readonly property string screenBrightness: Planck.getProperty("/Client/Preferences/ScreenBrightnessPluggedIn").translator.string onScreenBrightnessChanged: send()

    function send() { var brightness = 0 if(screenBrightness === "Low") { brightness = 15 } else if(screenBrightness === "Mid") { brightness = 70 } else if(screenBrightness === "High") { brightness = 105 } else if(screenBrightness === "Max") { brightness = 127 }

    Midi.sendSysEx("F0 00 02 0B 01 10 7c 00 01 %1 F7".arg(device.d2h(brightness))) }

    Component.onCompleted: send() }}





    Display Info:


    import airAssignments 1.0import InputAssignment 0.1import OutputAssignment 0.1import Device 0.1import QtQuick 2.0

    Device { id: device

    property color deckColor: "#01e771"

    property string deviceInfo: "";

    readonly property QtObject artwork: QtObject { property bool enabled: false property int index: 0 property real alpha: 0.85

    onAlphaChanged: device.setElementARGB(0, Qt.rgba(1.0, 1.0, 1.0, alpha)) onEnabledChanged: device.updateDisplayElements() onIndexChanged: { if(index == 0) { device.updateDisplayDelayTimer.start() } else { device.updateDisplayElements() } }

    Component.onCompleted: device.setElementARGB(0, Qt.rgba(1.0, 1.0, 1.0, alpha)) }

    readonly property QtObject logo: QtObject { property bool enabled: false onEnabledChanged: { if(enabled) { device.updateDisplayDelayTimer.start() } else { device.updateDisplayElements() } }

    property color color: "#ffffff" onColorChanged: device.setElementARGB(1, color)

    Component.onCompleted: device.setElementARGB(1, color) }

    readonly property QtObject platterPosition: QtObject { property bool enabled: false

    onEnabledChanged: { if(!enabled) { device.updateDisplayDelayTimer.start() } else { device.updateDisplayElements() } }

    property color ringColor: "#ffffff" onRingColorChanged: device.setElementARGB(2, ringColor)

    property color indicatorColor: "#FFFFFF" onIndicatorColorChanged: device.setElementARGB(3, indicatorColor)

    Component.onCompleted: { device.setElementARGB(2, ringColor) device.setElementARGB(3, indicatorColor) } }

    readonly property QtObject slipPosition: QtObject { property bool enabled: true property bool active: false property real alphaValue: 0.6

    property color indicatorColor: device.deckColor onIndicatorColorChanged: device.setElementARGB(5, indicatorColor);

    onAlphaValueChanged: { device.setElementARGB(4, Qt.rgba(0.0, 0.0, 0.0, alphaValue)); }

    onActiveChanged: { device.updateDisplayElements(); }

    onEnabledChanged: { if(!enabled) { device.updateDisplayDelayTimer.start() } else { device.updateDisplayElements() } }

    Component.onCompleted: { device.setElementARGB(4, Qt.rgba(0.0, 0.0, 0.0, 0.6)); device.setElementARGB(5, device.deckColor); } }

    readonly property QtObject loopAndLayerText: QtObject { property bool enabled: false onEnabledChanged: { if(enabled) { device.slipPosition.alphaValue = 0.25; } else { device.slipPosition.alphaValue = 0.6; }

    device.updateDisplayElements(); }

    property string text: "" onTextChanged: device.updateDisplayElements()

    property color color: "#ffffff" onColorChanged: device.setElementARGB(8, color)

    Component.onCompleted: device.setElementARGB(8, color) }

    function d2h(d){ return (+d).toString(16).toUpperCase() }

    function colorComponentToHex(component) { var result = ""

    var ci = Math.floor(component * 255) result = device.d2h((ci & 0xF0) >> 4) result += " " result += device.d2h(ci & 0x0F)

    return result }

    function colorToHex(color) { return colorComponentToHex(color.a) + " " + colorComponentToHex(color.r) + " " + colorComponentToHex(color.g) + " " + colorComponentToHex(color.b) }

    function setElementARGB(elementId, color) { // Available elementIds // 0 - Album Artwork (ignored) // 1 - Engine Logo // 2 - Platter Position Ring // 3 - Platter Position Indicator // 4 - Slip Position Ring // 5 - Slip Position Indicator // 6 - Track Progress Ring // 7 - Track Progress Indicator // 8 - Loop and Layer Text

    Midi.sendSysEx("F0 00 02 0B 01 10 0B 00 09 %1 %2 F7".arg(device.d2h(elementId)).arg(colorToHex(color))) }

    function loopTextToIndex(text) { switch(text) { case "1/64": return 0x0 case "1/32": return 0x1 case "1/16": return 0x2 case "1/8": return 0x3 case "1/4": return 0x4 case "1/2": return 0x5 case "1": return 0x6 case "2": return 0x7 case "4": return 0x8 case "8": return 0x9 case "16": return 0xA case "32": return 0xB case "64": return 0xC case "A": return 0xE case "B": return 0xF } return 0xD }

    property Timer updateDisplayDelayTimer: Timer { interval: 1000 repeat: false onTriggered: { if(!imageSendingEnabled) { updateDisplayDelayTimer.start() return }

    device.updateDisplayElements() } }

    function updateDisplayElements() {

    if(!imageSendingEnabled) { return }

    var enabledSections = [0, 0, 0, 0]

    // Element Enable/Disable 1 // Bit[0] - Album Artwork // Bit[1] - Engine Logo // Bit[2] - Platter Position Ring // Bit[4] - Slip Position Ring // Bit[5] - Slip Position Indicator enabledSections[0] |= artwork.enabled << 0 enabledSections[0] |= logo.enabled << 1 enabledSections[0] |= platterPosition.enabled << 2 enabledSections[0] |= slipPosition.active << 4 enabledSections[0] |= slipPosition.active << 5

    // Element Enable/Disable 2 // Bit[0] - Track Progress Indicator // Bit[1] - Loop and Layer Text // Bit[2] - Burst Image enabledSections[1] |= loopAndLayerText.enabled << 1

    // Current Album Artwork Index // ... enabledSections[2] = artwork.index

    // Loop and LayerText Display enabledSections[3] = loopTextToIndex(loopAndLayerText.text)

    var sysEx = "F0 00 02 0B 01 10 0A 00 04 "

    for(var i = 0; i < enabledSections.length; ++i) { sysEx += device.d2h(enabledSections[i]) + " " }

    sysEx += "F7"

    Midi.sendSysEx(sysEx) }

    function sysEx(sysExString) { var valueList = sysExString.split(" ");

    var code = valueList[6]

    if(code === "0x10") { console.log("Image decoding finished", sysExString) imageSendingEnabled = true updateDisplayElements() } else if(code === "0x0F") { var errCode = valueList[9]; if(errCode !== 0x00) { console.warn("Upload failed:", errCode) } } else { var result = ""; for(var i=0;i<4;i++) { result += parseInt(valueList[i + 11], 16); if(i == 1) { result += "."; } } deviceInfo = result; }

    }

    property bool imageSendingEnabled: false

    property Timer imageSendingEnableTimeout: Timer { interval: 17000 repeat: false onTriggered: { if(!device.imageSendingEnabled) { device.imageSendingEnabled = true device.updateDisplayElements() } } }

    property Timer keepAliveTimer: Timer { interval: 40 repeat: true onTriggered: { Midi.sendSysEx("F0 00 02 0B 01 10 7F 00 00 F7") // Send Keep Alive Message } }

    Component.onCompleted: { Midi.sendSysEx("F0 7E 00 06 01 F7") updateDisplayElements()

    console.log("Start decoding images") imageSendingEnableTimeout.start() Midi.sendSysEx("F0 00 02 0B 01 10 10 00 00 F7") // Start decoding images keepAliveTimer.start(); }

    Component.onDestruction: { // Disable all keepAliveTimer.stop() artwork.enabled = true logo.enabled = true platterPosition.enabled = false slipPosition.active = false loopAndLayerText.enabled = false artwork.index = 0 loopAndLayerText.text = "" // The wheel display stops expecting keepalive messages after a device inquiry message Midi.sendSysEx("F0 7E 7F 06 01 F7")

    }}

  • myalteredsoul
    myalteredsoul Member Posts: 201 Advisor

    Continued:

    {

        // initialize sysex

        string sysex = "F000020B01100A0004";

       

        // show slip?

        slip ? sysex += "3D" : sysex += "0D";

       

        // show loop? ( + artwork index)

        if(loop) {

            // set loop size and terminate sysex message get(LOOP_SIZE_A)

            switch(loopSize) {

                case  0: sysex += "020101F7"; break;

                case  1: sysex += "020102F7"; break;

                case  2: sysex += "020103F7"; break;

                case  3: sysex += "020104F7"; break;

                case  4: sysex += "020105F7"; break;

                case  5: sysex += "020106F7"; break;

                case  6: sysex += "020107F7"; break;

                case  7: sysex += "020108F7"; break;

                case  8: sysex += "020109F7"; break;

                case  9: sysex += "02010AF7"; break;

                case 10: sysex += "02010BF7"; break;

                case 11: sysex += "02010CF7"; break;

                default: sysex += "02010DF7"; break;

            }

        }

        else {

            sysex += "000100F7";

        }

       

        // send to device

        if(lastSysEx != sysex) {

            sendSysExMessage(sysex);

            lastSysEx = sysex;

        }

    }


    void OnStart()

    {

        // this callback is called by DD when the script is started

       

        // device inquiry message

        sendSysExMessage("F07E000601F7");

       

        // enable album artwork and platter position ring screen elements

        sendSysExMessage("F000020B01100A00040D000000F7");

       

        // start decoding images (wait approx. 15 seconds till you send the next sysex message)

        sendSysExMessage("F000020B0110100000F7");

    }


    void OnMidiMessageReceived (const string &in name, int status, int data, int value, double timestamp)

    {

        // this callback is called when a midi message is received from a midi device

    }


    // this callback is called continuosly by Solo every 50 ms

    void OnTimerCallback()

    {

        if(initTimer > 15000 && initTimerEnabled) {

            initTimerEnabled = false;

            imageSendingEnabled = true;

        }

       

        if(initTimerEnabled == true) {

            initTimer += 50;

        }

       

        if(!imageSendingEnabled)

            return;

       

        // show loop size on center display

        if(get(LOOP_A) > 0) {

            showDisplayElements(false, true, get(LOOP_SIZE_A));

        }

        else {

            showDisplayElements(false, false, 0);

        }

       

        // send platter pos

        int value = (get(PLATTER_POSITION_A) * 0x3FFF) + 0.5;

        sendMidiMessage(0xE0, value & 0x7f, (value & 0x3F80) >> 7);


        //value = int(slip * 0x3FFF);

        //sendMidiMessage(0xE1, int8(value & 0x7f), int8(value >> 7));


        // send track cover art

        if(get(TRACK_LOADED_FLAG_A) > 0) {

            set(TRACK_LOADED_FLAG_A);

            sendDisplaySysExMessage(0x10, 0x00, 0x00);

        }


        // send keep alive message

        //sendSysExMessage("F000020B01107F0000F7");

    }


    void OnStop()

    {

        // this callback is called by DD when the script is stopped

       

        // the wheel display stops expecting keepalive messages after a device inquiry message

        sendSysExMessage("F07E7F0601F7");

    }


    // EOF

  • Aquadics
    Aquadics Member Posts: 59 Helper

    Thanks @myalteredsoul good job.

    I am already aware of many of the messages to control the wheel display but still missing the format to transfer images to LC6000.

    I am not getting why it's kept so secret and why it's not officially documented. Denon DJ is harming them self :-(

  • myalteredsoul
    myalteredsoul Member Posts: 201 Advisor

    JPG 500px x 500px or smaller. You can target the screen by addressing using Angel Script. It's not too much different than C++, if you're familiar.

  • Aquadics
    Aquadics Member Posts: 59 Helper

    Thanks for your reply but there are still open questions.

    Its about the SysEx MIDI message I am asking, encoding, compression, 8 to 7 bit arrangement and checksum.

  • myalteredsoul
    myalteredsoul Member Posts: 201 Advisor

    If you take a moment to reverse engineer the above, you will find the answers you are looking for. ;-) You can also try reaching out to the VDJ team to see if they're willing to sell their implementation to you.

  • T_G
    T_G Member Posts: 8 Member
    edited March 2023

    Hi,

    if still is someone reading here. I would be very interested in getting my screens to work. For the general mapping I made my own tsi to control all 4 decks but that does not help me with the sysex part for the screen.

    So even if we would know all the needed sysex messages how would be your approach to get them out there. Route everything through BOME MTP for instance so one can create and send the sysex from there?

    I am not clear on how to grab the necessary information from Traktor, too, meaning file playing etc. That's where it starts already

    I have a similar issue going where I want to send sysex to an AKAI Fire because riciculously the button colors and intensity can only be accessed via sysex there.

    Would be happy on one or two hints for general approach. Implementing should not be the main culprit as I am senior dev this is just not my field normally

    Thanks!

    EDIT : Anyways HID done by either of the 2 companies would be the best way to go. Also much less work as they did e.g. for the CDJ3000 as there is only the platter screen to take care about. It should be really easy for them :(

    So also a +1 on the feature request here, so we don't need to implement any messy workarounds

Back To Top