Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions build/depends.py
Original file line number Diff line number Diff line change
Expand Up @@ -913,6 +913,8 @@ def sources(self, build):
"src/controllers/softtakeover.cpp",
"src/controllers/keyboard/keyboardeventfilter.cpp",
"src/controllers/colorjsproxy.cpp",
"src/controllers/colormapper.cpp",
"src/controllers/colormapperjsproxy.cpp",

"src/main.cpp",
"src/mixxx.cpp",
Expand Down
44 changes: 24 additions & 20 deletions res/controllers/Roland_DJ-505-scripts.js
Original file line number Diff line number Diff line change
Expand Up @@ -941,17 +941,24 @@ DJ505.PadColor = {
DIM_MODIFIER: 0x10,
};

DJ505.PadColorMap = [
DJ505.PadColor.OFF,
DJ505.PadColor.RED,
DJ505.PadColor.GREEN,
DJ505.PadColor.BLUE,
DJ505.PadColor.YELLOW,
DJ505.PadColor.CELESTE,
DJ505.PadColor.PURPLE,
DJ505.PadColor.APRICOT,
DJ505.PadColor.WHITE,
];
DJ505.PadColorMap = new ColorMapper({
'#FFCC0000': DJ505.PadColor.RED,
'#FFCC4400': DJ505.PadColor.CORAL,
'#FFCC8800': DJ505.PadColor.ORANGE,
'#FFCCCC00': DJ505.PadColor.YELLOW,
'#FF88CC00': DJ505.PadColor.GREEN,
'#FF00CC00': DJ505.PadColor.APPLEGREEN,
'#FF00CC88': DJ505.PadColor.AQUAMARINE,
'#FF00CCCC': DJ505.PadColor.TURQUOISE,
'#FF0088CC': DJ505.PadColor.CELESTE,
'#FF0000CC': DJ505.PadColor.BLUE,
'#FF4400CC': DJ505.PadColor.AZURE,
'#FF8800CC': DJ505.PadColor.PURPLE,
'#FFCC00CC': DJ505.PadColor.MAGENTA,
'#FFCC0044': DJ505.PadColor.RED,
'#FFFFCCCC': DJ505.PadColor.APRICOT,
'#FFFFFFFF': DJ505.PadColor.WHITE,
});

DJ505.PadSection = function (deck, offset) {
// TODO: Add support for missing modes (flip, slicer, slicerloop)
Expand Down Expand Up @@ -1190,7 +1197,6 @@ DJ505.HotcueMode = function (deck, offset) {
this.ledControl = DJ505.PadMode.HOTCUE;
this.color = DJ505.PadColor.WHITE;

var hotcueColors = [this.color].concat(DJ505.PadColorMap.slice(1));
this.pads = new components.ComponentContainer();
for (var i = 0; i <= 7; i++) {
this.pads[i] = new components.HotcueButton({
Expand All @@ -1202,7 +1208,7 @@ DJ505.HotcueMode = function (deck, offset) {
group: deck.currentDeck,
on: this.color,
off: this.color + DJ505.PadColor.DIM_MODIFIER,
colors: hotcueColors,
colorMapper: DJ505.PadColorMap,
outConnect: false,
});
}
Expand All @@ -1226,7 +1232,6 @@ DJ505.CueLoopMode = function (deck, offset) {
this.ledControl = DJ505.PadMode.HOTCUE;
this.color = DJ505.PadColor.BLUE;

var cueloopColors = [this.color].concat(DJ505.PadColorMap.slice(1));
this.PerformancePad = function(n) {
this.midi = [0x94 + offset, 0x14 + n];
this.number = n + 1;
Expand All @@ -1241,7 +1246,7 @@ DJ505.CueLoopMode = function (deck, offset) {
group: deck.currentDeck,
on: this.color,
off: this.color + DJ505.PadColor.DIM_MODIFIER,
colors: cueloopColors,
colorMapper: DJ505.PadColorMap,
outConnect: false,
unshift: function() {
this.input = function (channel, control, value, status, group) {
Expand Down Expand Up @@ -1492,13 +1497,12 @@ DJ505.PitchPlayMode = function (deck, offset) {
this.color = DJ505.PadColor.GREEN;
this.cuepoint = 1;
this.range = PitchPlayRange.MID;
var pitchplayColors = [this.color].concat(DJ505.PadColorMap.slice(1));

this.PerformancePad = function(n) {
this.midi = [0x94 + offset, 0x14 + n];
this.number = n + 1;
this.on = this.color + DJ505.PadColor.DIM_MODIFIER;
this.colors = pitchplayColors;
this.colorMapper = DJ505.PadColorMap;
this.colorKey = 'hotcue_' + this.number + '_color';
components.Button.call(this);
};
Expand All @@ -1510,10 +1514,10 @@ DJ505.PitchPlayMode = function (deck, offset) {
mode: this,
outConnect: false,
off: DJ505.PadColor.OFF,
outputColor: function(id) {
outputColor: function(colorCode) {
// For colored hotcues (shifted only)
var color = this.colors[id];
this.send((this.mode.cuepoint === this.number) ? color : (color + DJ505.PadColor.DIM_MODIFIER));
var midiColor = this.colorMapper.getNearestValue(colorCode);
this.send((this.mode.cuepoint === this.number) ? midiColor : (midiColor + DJ505.PadColor.DIM_MODIFIER));
},
unshift: function() {
this.outKey = "pitch_adjust";
Expand Down
21 changes: 7 additions & 14 deletions res/controllers/midi-components-0.0.js
Original file line number Diff line number Diff line change
Expand Up @@ -294,11 +294,8 @@
print('ERROR: No hotcue number specified for new HotcueButton.');
return;
}
if (options.colors !== undefined || options.sendRGB !== undefined) {
if (options.colorMapper !== undefined || options.sendRGB !== undefined) {
this.colorKey = 'hotcue_' + options.number + '_color';
if (options.colors === undefined) {
options.colors = color.hotcueColorPalette();
}
}
this.number = options.number;
this.outKey = 'hotcue_' + this.number + '_enabled';
Expand Down Expand Up @@ -330,20 +327,16 @@
this.send(outval);
}
},
outputColor: function (id) {
var color = this.colors[id];
if (color instanceof Array) {
if (color.length !== 3) {
print("ERROR: invalid color array for id: " + id);
return;
}
outputColor: function (colorCode) {
if (this.colorMapper !== undefined) {
var nearestColorValue = this.colorMapper.getNearestValue(colorCode);
this.send(nearestColorValue);
} else {
if (this.sendRGB === undefined) {
print("ERROR: no function defined for sending RGB colors");
return;
}
this.sendRGB(color);
} else if (typeof color === 'number') {
this.send(color);
this.sendRGB(color.colorFromHexCode(colorCode));
}
},
connect: function() {
Expand Down
59 changes: 59 additions & 0 deletions src/controllers/colormapper.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
#include "controllers/colormapper.h"

#include <cmath>
#include <cstdint>

#include "util/debug.h"

namespace {
double colorDistance(QRgb a, QRgb b) {
// This algorithm calculates the distance between two colors. In
// contrast to the L2 norm, this also tries take the human perception
// of colors into account. More accurate algorithms like the CIELAB2000
// Delta-E rely on sophisticated color space conversions and need a lot
// of costly computations. In contrast, this is a low-cost
// approximation and should be sufficently accurate.
// More details: https://www.compuphase.com/cmetric.htm
long mean_red = ((long)qRed(a) + (long)qRed(b)) / 2;
long delta_red = (long)qRed(a) - (long)qRed(b);
long delta_green = (long)qGreen(a) - (long)qGreen(b);
long delta_blue = (long)qBlue(a) - (long)qBlue(b);
return sqrt(
(((512 + mean_red) * delta_red * delta_red) >> 8) +
(4 * delta_green * delta_green) +
(((767 - mean_red) * delta_blue * delta_blue) >> 8));
}
} // namespace

QPair<QRgb, QVariant> ColorMapper::getNearestColor(QRgb desiredColor) {
// If desired color is already in cache, use cache entry
QMap<QRgb, QRgb>::const_iterator i = m_cache.find(desiredColor);
QMap<QRgb, QVariant>::const_iterator j;
if (i != m_cache.constEnd()) {
j = m_availableColors.find(i.value());
DEBUG_ASSERT(j != m_availableColors.constEnd());
qDebug() << "ColorMapper cache hit for" << desiredColor << ":"
<< "Color =" << j.key() << ","
<< "Value =" << j.value();
return QPair<QRgb, QVariant>(j.key(), j.value());
}

// Color is not cached
QMap<QRgb, QVariant>::const_iterator nearestColorIterator;
double nearestColorDistance = qInf();
for (j = m_availableColors.constBegin(); j != m_availableColors.constEnd(); j++) {
QRgb availableColor = j.key();
double distance = colorDistance(desiredColor, availableColor);
if (distance < nearestColorDistance) {
nearestColorDistance = distance;
nearestColorIterator = j;
}
}

DEBUG_ASSERT(nearestColorDistance < qInf());
qDebug() << "ColorMapper found matching color for" << desiredColor << ":"
<< "Color =" << nearestColorIterator.key() << ","
<< "Value =" << nearestColorIterator.value();
m_cache.insert(desiredColor, nearestColorIterator.key());
return QPair<QRgb, QVariant>(nearestColorIterator.key(), nearestColorIterator.value());
}
30 changes: 30 additions & 0 deletions src/controllers/colormapper.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
#ifndef COLORMAPPER_H
#define COLORMAPPER_H

#include <QColor>
#include <QMap>
#include <QObject>
#include <QPair>
#include <QVariant>

#include "util/assert.h"

class ColorMapper final : public QObject {
Q_OBJECT
public:
ColorMapper() = delete;
ColorMapper(const QMap<QRgb, QVariant> availableColors)
: m_availableColors(availableColors) {
DEBUG_ASSERT(!m_availableColors.isEmpty());
}

~ColorMapper() = default;

QPair<QRgb, QVariant> getNearestColor(QRgb desiredColor);

private:
const QMap<QRgb, QVariant> m_availableColors;
QMap<QRgb, QRgb> m_cache;
};

#endif /* COLORMAPPER_H */
47 changes: 47 additions & 0 deletions src/controllers/colormapperjsproxy.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
#include <QScriptValueIterator>

#include "controllers/colormapperjsproxy.h"

ColorMapperJSProxy::ColorMapperJSProxy(QScriptEngine* pScriptEngine, QMap<QRgb, QVariant> availableColors)
: m_pScriptEngine(pScriptEngine) {
m_colorMapper = new ColorMapper(availableColors);
}

QScriptValue ColorMapperJSProxy::getNearestColor(uint colorCode) {
QPair<QRgb, QVariant> result = m_colorMapper->getNearestColor(static_cast<QRgb>(colorCode));
QScriptValue jsColor = m_pScriptEngine->newObject();
jsColor.setProperty("red", qRed(result.first));
jsColor.setProperty("green", qGreen(result.first));
jsColor.setProperty("blue", qBlue(result.first));
jsColor.setProperty("alpha", qAlpha(result.first));
return jsColor;
}

QScriptValue ColorMapperJSProxy::getNearestValue(uint colorCode) {
QPair<QRgb, QVariant> result = m_colorMapper->getNearestColor(static_cast<QRgb>(colorCode));
return m_pScriptEngine->toScriptValue(result.second);
}

QScriptValue ColorMapperJSProxyConstructor(QScriptContext* pScriptContext, QScriptEngine* pScriptEngine) {
QMap<QRgb, QVariant> availableColors;
DEBUG_ASSERT(pScriptContext->argumentCount() == 1);
QScriptValueIterator it(pScriptContext->argument(0));
while (it.hasNext()) {
it.next();
DEBUG_ASSERT(!it.value().isObject());
QColor color(it.name());
VERIFY_OR_DEBUG_ASSERT(color.isValid()) {
qWarning() << "Received invalid color name from controller script:" << it.name();
continue;
}
availableColors.insert(color.rgb(), it.value().toVariant());
}

if (availableColors.isEmpty()) {
qWarning() << "Failed to create ColorMapper object: available colors mustn't be empty!";
return pScriptEngine->undefinedValue();
}

QObject* colorMapper = new ColorMapperJSProxy(pScriptEngine, availableColors);
return pScriptEngine->newQObject(colorMapper, QScriptEngine::ScriptOwnership);
}
30 changes: 30 additions & 0 deletions src/controllers/colormapperjsproxy.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
#ifndef COLORMAPPERJS_H
#define COLORMAPPERJS_H

#include <QScriptContext>
#include <QScriptEngine>

#include "controllers/colormapper.h"

class ColorMapperJSProxy final : public QObject {
Q_OBJECT
public:
ColorMapperJSProxy() = delete;
ColorMapperJSProxy(QScriptEngine* pScriptEngine, QMap<QRgb, QVariant> availableColors);

~ColorMapperJSProxy() {
delete m_colorMapper;
};

public slots:
QScriptValue getNearestColor(uint ColorCode);
QScriptValue getNearestValue(uint ColorCode);

private:
QScriptEngine* m_pScriptEngine;
ColorMapper* m_colorMapper;
};

QScriptValue ColorMapperJSProxyConstructor(QScriptContext* context, QScriptEngine* engine);

#endif /* COLORMAPPER_H */
6 changes: 5 additions & 1 deletion src/controllers/controllerengine.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,8 @@
email : spappalardo@mixxx.org
***************************************************************************/

#include "controllers/colormapperjsproxy.h"
#include "controllers/controllerengine.h"

#include "controllers/controller.h"
#include "controllers/controllerdebug.h"
#include "control/controlobject.h"
Expand Down Expand Up @@ -219,6 +219,10 @@ void ControllerEngine::initializeScriptEngine() {
m_pEngine, HotcueColorPaletteSettings(m_pConfig));
engineGlobalObject.setProperty("color", m_pEngine->newQObject(m_pColorJSProxy.get()));

QScriptValue constructor = m_pEngine->newFunction(ColorMapperJSProxyConstructor);
QScriptValue metaObject = m_pEngine->newQMetaObject(&ColorMapperJSProxy::staticMetaObject, constructor);
engineGlobalObject.setProperty("ColorMapper", metaObject);

m_pBaClass = new ByteArrayClass(m_pEngine);
engineGlobalObject.setProperty("ByteArray", m_pBaClass->constructor());
}
Expand Down