# HG changeset patch # User František Kučera # Date 1746916203 -7200 # Node ID 358a601bfe81d2d307ee52f0c7bba96f872eafb4 # Parent aa7dc7faf1bbb70e14be20470b564a160d7a2384 support Pioneer DJ DJM-250MK2 and DJM-V10 diff -r aa7dc7faf1bb -r 358a601bfe81 AlsaBridge.cpp --- a/AlsaBridge.cpp Fri May 09 23:17:36 2025 +0200 +++ b/AlsaBridge.cpp Sun May 11 00:30:03 2025 +0200 @@ -43,9 +43,14 @@ std::recursive_mutex midiMutex; std::atomic stopped{false}; - std::string findDeviceName(std::regex cardNamePattern) { + struct FoundDevice { + std::string id; + std::string name; + }; + FoundDevice findDeviceName(std::regex cardNamePattern) { std::vector cardNumbers; + std::vector cardNames; logger->log(L::INFO, "Looking for available cards:"); @@ -58,6 +63,7 @@ if (std::regex_match(longName, cardNamePattern)) { cardNumbers.push_back(card); + cardNames.push_back(longName); logMessage << " [matches]"; } @@ -69,7 +75,7 @@ if (cardNumbers.size() == 1) { const auto n = std::to_string(cardNumbers[0]); logger->log(L::INFO, "Going to fix card #" + n); - return "hw:" + n; + return {"hw:" + n, cardNames[0]}; } else if (cardNumbers.empty()) { throw std::invalid_argument( "No card with matching name found. Is the card connected? " @@ -89,6 +95,7 @@ } void run() { + MidiMessage msg; while (!stopped) { { std::lock_guard lock(midiMutex); @@ -96,8 +103,32 @@ uint8_t buf[256]; ssize_t length = snd_rawmidi_read(input, buf, sizeof (buf)); if (length > 0 && length <= sizeof (buf)) { - // TODO: multiple messages combined together? - djmFix->receive(MidiMessage(buf, buf + length)); + // Parse MIDI messages and ignore/skip unwanted data. + // Needed for DJM-V10 that sends annoying amounts of 0xF8. + for (int i = 0; i < length; i++) { + uint8_t b = buf[i]; + if (b == MIDI_CMD_COMMON_SYSEX) { + // start of MIDI SysEx message + msg.clear(); + msg.push_back(b); + } else if (b == MIDI_CMD_COMMON_SYSEX_END) { + // end of MIDI SysEx message + msg.push_back(b); + djmFix->receive(msg); + msg.clear(); + } else if (b == MIDI_CMD_COMMON_CLOCK) { + logger->log(L::FINEST, "timing clock ignored"); + } else if (b == MIDI_CMD_COMMON_SENSING) { + logger->log(L::FINEST, "active sensing ignored"); + } else if (b & 0x80) { + // unknown status, drop previous data + msg.clear(); + logger->log(L::FINER, "unknown message ignored"); + } else { + // message data + msg.push_back(b); + } + } } } std::this_thread::sleep_for(std::chrono::milliseconds(100)); @@ -114,12 +145,13 @@ if (djmFix == nullptr) throw std::invalid_argument("Need a djmFix for AlsaBridge."); - std::string deviceName = findDeviceName(std::regex(cardNamePattern)); + FoundDevice found = findDeviceName(std::regex(cardNamePattern)); int mode = SND_RAWMIDI_NONBLOCK; - int error = snd_rawmidi_open(&input, &output, deviceName.c_str(), mode); + int error = snd_rawmidi_open(&input, &output, found.id.c_str(), mode); if (error) throw std::invalid_argument("Unable to open ALSA device."); + djmFix->setDeviceName(found.name); djmFix->setMidiSender(this); } diff -r aa7dc7faf1bb -r 358a601bfe81 DJMFix.cpp --- a/DJMFix.cpp Fri May 09 23:17:36 2025 +0200 +++ b/DJMFix.cpp Sun May 11 00:30:03 2025 +0200 @@ -23,8 +23,10 @@ #include #include #include +#include #include "DJMFix.h" +#include "MessageCodec.h" namespace djmfix { @@ -36,6 +38,7 @@ private: MidiSender* midiSender; djmfix::logging::Logger* logger; + MessageCodec codec; const int keepAliveInterval = 200; int keepAliveCounter = 0; std::thread keepAliveThread; @@ -43,14 +46,31 @@ std::atomic running{false}; std::atomic stopped{false}; std::atomic sendKeepAlive{false}; + /** + * Device (V10) may send multiple greeting messages. + * It works even if we respond multiple times. But one response is enough. + */ + std::atomic greetingReceived{false}; + + Bytes seed0 = {0x68, 0x01, 0x31, 0xFB}; + Bytes seed1 = {0x29, 0x00, 0x00, 0x00, 0x23, 0x48, 0x00, 0x00}; Bytes seed2; + Bytes seed3; + + Bytes name1 = {0x50, 0x69, 0x6f, 0x6e, 0x65, 0x65, 0x72, 0x44, 0x4a}; + Bytes name2 = {0x72, 0x65, 0x6b, 0x6f, 0x72, 0x64, 0x62, 0x6f, 0x78}; + + Bytes hash1; + Bytes hash2; + + uint8_t version = 0x17; void run() { while (!stopped) { logger->log(L::FINE, "DJMFixImpl::run()"); if (sendKeepAlive) send({ 0xf0, 0x00, 0x40, 0x05, - 0x00, 0x00, 0x00, 0x17, + 0x00, 0x00, 0x00, version, 0x00, 0x50, 0x01, 0xf7 }); std::this_thread::sleep_for(chro::milliseconds(keepAliveInterval)); @@ -62,6 +82,11 @@ } } + void send(const Message& msg) { + logger->log(L::FINE, "" + msg.toString()); + send(codec.encode(msg)); + } + void send(const MidiMessage& midiMessage) { std::lock_guard lock(midiMutex); midiSender->send(midiMessage); @@ -150,77 +175,103 @@ if (running) stop(); } - void setMidiSender(MidiSender* midiSender) { + void setDeviceName(std::string name) override { + logger->log(L::FINE, "DJMFixImpl::setDeviceName(" + name + ")"); + + std::regex djm250pattern("Pioneer DJ Corporation DJM-250MK2.*"); + std::regex djm450pattern(".*450.*"); // TODO: correct pattern + + if (std::regex_match(name, djm250pattern)) { + // DJM-250MK2: + version = 0x17; + seed3 = { + 0x59, 0xb5, 0x4b, 0xfe, 0xe4, + 0x4a, 0x5a, 0xc8, 0xe4, 0xc5 + }; + logger->log(L::FINE, "Switched to DJM-250MK2 mode"); + } else if (std::regex_match(name, djm450pattern)) { + // DJM-450: + // DJM-450 - not tested yet: + version = 0x13; + seed0 = {0x8c, 0x5b, 0x3f, 0x5d}; + seed3 = { + 0x08, 0xef, 0x3f, 0x2f, 0x1e, + 0x7a, 0x90, 0x17, 0xf6, 0xaf + }; + logger->log(L::FINE, "Switched to DJM-450 mode"); + } else { + // DJM-V10: + version = 0x34; + seed3 = { + 0x70, 0x01, 0x4d, 0x05, 0xbe, + 0xf2, 0xe4, 0xde, 0x60, 0xd6 + }; + logger->log(L::FINE, "Switched to DJM-V10 mode"); + } + } + + void setMidiSender(MidiSender* midiSender) override { logger->log(L::FINER, "DJMFixImpl::setMidiSender()"); this->midiSender = midiSender; } virtual void receive(const MidiMessage& msg) override { + // TODO: remove try/catch - there should be no unknown messages + try { + receive0(msg); + } catch (const std::exception& e) { + logger->log(L::SEVERE, + std::string("Message receiving failed: ") + e.what()); + } + } + + virtual void receive0(const MidiMessage& msg) { logger->log(L::FINE, "Received a message: " "size = " + std::to_string(msg.size()) + " " "data = " + toString(msg)); std::lock_guard lock(midiMutex); + Message msgIn = codec.decode(msg); + logger->log(L::FINE, "" + msgIn.toString()); - if (msg.size() == 12 && msg[9] == 0x11) { + if (msgIn.type == MessageType::D11_GREETING && !greetingReceived) { logger->log(L::INFO, "Received greeting message."); - send({ - 0xf0, 0x00, 0x40, 0x05, - 0x00, 0x00, 0x00, 0x17, - 0x00, 0x12, 0x2a, 0x01, - 0x0b, 0x50, 0x69, 0x6f, - 0x6e, 0x65, 0x65, 0x72, - 0x44, 0x4a, 0x02, 0x0b, - 0x72, 0x65, 0x6b, 0x6f, - 0x72, 0x64, 0x62, 0x6f, - 0x78, 0x03, 0x12, 0x02, - 0x09, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x02, - 0x03, 0x04, 0x08, 0x00, - 0x00, 0x00, 0x00, 0xf7 + Message msgOut(MessageType::H12_SEED1, version,{ + {FieldType::F01, name1}, + {FieldType::F02, name2}, + {FieldType::F03, denormalize(seed1)} }); + send(msgOut); + greetingReceived = true; logger->log(L::INFO, "Sent message with seed1."); - } else if (msg.size() == 54 - && msg[9] == 0x13 - && msg[33] == 0x04 - && msg[43] == 0x03) // - { - Bytes hash1(msg.begin() + 35, msg.begin() + 35 + 8); - seed2 = Bytes(msg.begin() + 45, msg.begin() + 45 + 8); - hash1 = normalize(hash1); - seed2 = normalize(seed2); + } else if (msgIn.type == MessageType::D13_HASH1_SEED2) { + std::vector hash1F = msgIn.findFields(FieldType::F04); + std::vector seed2F = msgIn.findFields(FieldType::F03); + + if (hash1F.empty()) throw std::logic_error("hash1 not found"); + if (seed2F.empty()) throw std::logic_error("seed2 not found"); + + hash1 = normalize(hash1F[0].data); + seed2 = normalize(seed2F[0].data); + logger->log(L::INFO, "Received message with " "hash1 = " + toString(hash1) + " and " "seed2 = " + toString(seed2)); - Bytes seed0 = {0x68, 0x01, 0x31, 0xFB}; - Bytes seed1 = {0x29, 0x00, 0x00, 0x00, 0x23, 0x48, 0x00, 0x00}; - Bytes hash1check = toBytes(fnv32hash(concat(seed1, xOR(seed0, seed2)))); if (equals(hash1, hash1check)) { logger->log(L::INFO, "Verification of hash1 was successful."); - Bytes hash2 = - toBytes(fnv32hash(concat(seed2, xOR(seed0, seed2)))); - send(concat({ - 0xf0, 0x00, 0x40, 0x05, - 0x00, 0x00, 0x00, 0x17, - 0x00, 0x14, 0x38, 0x01, - 0x0b, 0x50, 0x69, 0x6f, - 0x6e, 0x65, 0x65, 0x72, - 0x44, 0x4a, 0x02, 0x0b, - 0x72, 0x65, 0x6b, 0x6f, - 0x72, 0x64, 0x62, 0x6f, - 0x78, 0x04, 0x0a - }, concat(denormalize(hash2),{ - 0x05, 0x16, 0x05, 0x09, - 0x0b, 0x05, 0x04, 0x0b, - 0x0f, 0x0e, 0x0e, 0x04, - 0x04, 0x0a, 0x05, 0x0a, - 0x0c, 0x08, 0x0e, 0x04, - 0x0c, 0x05, 0xf7 - }))); + hash2 = toBytes(fnv32hash(concat(seed2, xOR(seed0, seed2)))); + + Message msgOut(MessageType::H14_HASH2, version,{ + {FieldType::F01, name1}, + {FieldType::F02, name2}, + {FieldType::F04, denormalize(hash2)}, + {FieldType::F05, denormalize(seed3)} + }); + send(msgOut); logger->log(L::INFO, "Sent message with hash2."); } else { std::stringstream logMessage; @@ -235,11 +286,13 @@ logger->log(L::SEVERE, logMessage.str()); // TODO: graceful death } - } else if (msg.size() == 12 && msg[9] == 0x15) { + } else if (msgIn.type == MessageType::D15_CONFIRMATION) { sendKeepAlive = true; logger->log(L::INFO, "Received acknowledgment message. " "Started sending keep-alive messages. " "LINE/PHONO channels should work now."); + } else { + logger->log(L::SEVERE, "Received unexpected message type."); } } @@ -249,12 +302,18 @@ if (midiSender == nullptr) throw std::logic_error("Need a midiSender when starting DJMFix"); - // TODO: methods for parsing and constructing messages from parts (TLV) send({ 0xf0, 0x00, 0x40, 0x05, - 0x00, 0x00, 0x00, 0x17, + 0x00, 0x00, 0x00, version, 0x00, 0x50, 0x01, 0xf7 }); + + // TODO: check whether this second message is neccessary for V10: + send({ + 0xf0, 0x00, 0x40, 0x05, + 0x00, 0x00, 0x00, version, + 0x00, 0x03, 0x01, 0xf7 + }); logger->log(L::INFO, "Sent greeting message."); keepAliveThread = std::thread(&DJMFixImpl::run, this); diff -r aa7dc7faf1bb -r 358a601bfe81 DJMFix.h --- a/DJMFix.h Fri May 09 23:17:36 2025 +0200 +++ b/DJMFix.h Sun May 11 00:30:03 2025 +0200 @@ -17,6 +17,7 @@ #pragma once #include +#include #include "Logger.h" @@ -33,6 +34,7 @@ class DJMFix { public: virtual ~DJMFix() = default; + virtual void setDeviceName(std::string name) = 0; virtual void setMidiSender(MidiSender* midiSender) = 0; virtual void receive(const MidiMessage& midiMessage) = 0; virtual void start() = 0; diff -r aa7dc7faf1bb -r 358a601bfe81 Message.cpp --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Message.cpp Sun May 11 00:30:03 2025 +0200 @@ -0,0 +1,44 @@ +/** + * djm-fix + * Copyright © 2025 František Kučera (Frantovo.cz, GlobalCode.info) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3 of the License. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#include +#include + +#include "Message.h" + +std::string Message::toString() const { + std::stringstream s; + s << ""; + for (const auto& field : fields) { + s << ""; + for (uint8_t b : field.data) { + s << std::hex << std::setw(2) << std::setfill('0') << (int) b; + } + s << ""; + } + s << ""; + return s.str(); +} + +std::vector Message::findFields(FieldType type) { + std::vector found; + for (Field f : fields) if (f.type == type) found.push_back(f); + return found; +} diff -r aa7dc7faf1bb -r 358a601bfe81 Message.h --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Message.h Sun May 11 00:30:03 2025 +0200 @@ -0,0 +1,88 @@ +/** + * djm-fix + * Copyright © 2025 František Kučera (Frantovo.cz, GlobalCode.info) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3 of the License. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#pragma once + +#include +#include +#include +#include + +enum class MessageType : uint8_t { + /** device sends: greeting message */ + D11_GREETING = 0x11, + + /** host sends: seed1 */ + H12_SEED1 = 0x12, + + /** device sends: hash1 and seed2 */ + D13_HASH1_SEED2 = 0x13, + + /** host sends: hash2 */ + H14_HASH2 = 0x14, + + /** device sends: confirmation of successful handshake */ + D15_CONFIRMATION = 0x15, +}; + +enum class FieldType : uint8_t { + /** manufacturer name */ + F01 = 0x01, + /** product name */ + F02 = 0x02, + /** seed1 from host | seed2 from device */ + F03 = 0x03, + /** hash2 from host | hash1 from device */ + F04 = 0x04, + /** seed3 from device */ + F05 = 0x05, +}; + +class Field { +public: + FieldType type; + std::vector data; + + Field(FieldType type, std::vector data) : type(type), data(data) { + } + + virtual ~Field() = default; +}; + +/** + * Object representation of a raw MIDI message. + * Is either a result of parsing a raw message by MessageCodec, or constructed + * in the application to be serialized in MessageCodec to a raw message. + */ +class Message { +public: + MessageType type; + /** 0x17 for DJM-250MK2 and 0x34 for V10 (maybe not a version) */ + uint8_t version; + std::vector fields; + + Message(MessageType type, uint8_t version, std::vector fields) : + type(type), version(version), fields(fields) { + } + + virtual ~Message() = default; + + std::string toString() const; + + std::vector findFields(FieldType type); + +}; diff -r aa7dc7faf1bb -r 358a601bfe81 MessageCodec.cpp --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/MessageCodec.cpp Sun May 11 00:30:03 2025 +0200 @@ -0,0 +1,118 @@ +/** + * djm-fix + * Copyright © 2025 František Kučera (Frantovo.cz, GlobalCode.info) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3 of the License. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#include +#include + +#include "MessageCodec.h" + +Message MessageCodec::decode(std::vector data) { + using ERR = std::invalid_argument; + + if (data.empty() || data.front() != 0xf0 || data.back() != 0xf7) + throw ERR("not a MIDI SysEx message"); + + // Manufacturer MIDI SysEx ID Numbers: + // 00H 40H 05H = AlphaTheta Corporation + // 00H 40H 06H = Pioneer Corporation + if (data.size() < 4 + || data[1] != 0x00 + || data[2] != 0x40 + || data[3] != 0x05) + throw ERR("wrong message Manufacturer MIDI SysEx ID"); + + if (data.size() < 8) throw ERR("missing message version"); + + uint8_t msgVersion = data[7]; + + if (data.size() < 10) throw ERR("missing message type"); + + MessageType msgType; + if /**/ (data[9] == 0x11) msgType = MessageType::D11_GREETING; + else if (data[9] == 0x12) msgType = MessageType::H12_SEED1; + else if (data[9] == 0x13) msgType = MessageType::D13_HASH1_SEED2; + else if (data[9] == 0x14) msgType = MessageType::H14_HASH2; + else if (data[9] == 0x15) msgType = MessageType::D15_CONFIRMATION; + else throw ERR("unsupported message type"); + + if (data.size() < 11) throw ERR("missing message length"); + if (data[10] != data.size() - 10) throw ERR("wrong message length"); + + std::vector fields; + + for (size_t start = 11; data.size() > start + 1;) { + FieldType fieldType; + if /**/ (data[start] == 0x01) fieldType = FieldType::F01; + else if (data[start] == 0x02) fieldType = FieldType::F02; + else if (data[start] == 0x03) fieldType = FieldType::F03; + else if (data[start] == 0x04) fieldType = FieldType::F04; + else if (data[start] == 0x05) fieldType = FieldType::F05; + else throw ERR("unsupported field type"); + + size_t fieldSize = data[start + 1]; + + if (data.size() < start + fieldSize + 1) throw ERR("field overflow"); + // ^ 0xf7 messsage end + + std::vector fieldData( + data.begin() + start + 2, + data.begin() + start + fieldSize); + + fields.push_back({fieldType, fieldData}); + + start += fieldSize; + } + + return Message(msgType, msgVersion, fields); +} + +std::vector MessageCodec::encode(Message msg) { + using ERR = std::invalid_argument; + + std::vector data; + + data.push_back(0xf0); + + data.push_back(0x00); + data.push_back(0x40); + data.push_back(0x05); + + data.push_back(0x00); + data.push_back(0x00); + data.push_back(0x00); + + data.push_back(msg.version); + data.push_back(0x00); + data.push_back((uint8_t) msg.type); + + uint8_t msgLength = 2; // 2 = type + lenght + for (const auto& field : msg.fields) msgLength += field.data.size() + 2; + + data.push_back(msgLength); + + for (const auto& field : msg.fields) { + size_t fieldSize = field.data.size() + 2; + if (fieldSize > 256) throw ERR("message field too long"); + data.push_back((uint8_t) field.type); + data.push_back((uint8_t) (fieldSize)); + data.insert(data.end(), field.data.begin(), field.data.end()); + } + + data.push_back(0xf7); + + return data; +} diff -r aa7dc7faf1bb -r 358a601bfe81 MessageCodec.h --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/MessageCodec.h Sun May 11 00:30:03 2025 +0200 @@ -0,0 +1,29 @@ +/** + * djm-fix + * Copyright © 2025 František Kučera (Frantovo.cz, GlobalCode.info) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3 of the License. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#pragma once + +#include +#include + +#include "Message.h" + +class MessageCodec { +public: + Message decode(std::vector data); + std::vector encode(Message msg); +}; \ No newline at end of file diff -r aa7dc7faf1bb -r 358a601bfe81 djm-fix.cpp --- a/djm-fix.cpp Fri May 09 23:17:36 2025 +0200 +++ b/djm-fix.cpp Sun May 11 00:30:03 2025 +0200 @@ -56,7 +56,7 @@ * channels. And this is the purpose of the djm-fix utility and it is done by * sending some magic packet to the mixer. * - * Implementation of this magic in the AlsaBridge.cpp file is based on publicly + * Implementation of this magic in the DJMFix.cpp file is based on publicly * available documentation that can be found at: * - https://swiftb0y.github.io/CDJHidProtocol/hid-analysis/handshake.html * - https://mixb.me/CDJHidProtocol/hid-analysis/handshake.html (formerly). @@ -90,6 +90,7 @@ * * Look for updates in the Mercurial repositories and at: * - https://blog.frantovo.cz/c/387/ + * - https://blog.frantovo.cz/c/396/ */ int main(int argc, char**argv) { @@ -98,7 +99,9 @@ logger(djmfix::logging::create(std::cerr, L::INFO)); try { logger->log(L::INFO, "DJM-Fix started."); - std::string cardNamePattern = argc == 2 ? argv[1] : "Pioneer DJ.*"; + std::string cardNamePattern = argc == 2 + ? argv[1] + : "(Pioneer DJ|AlphaTheta).*"; signal(SIGINT, interrupt); std::unique_ptr djmFix(djmfix::create(logger.get())); diff -r aa7dc7faf1bb -r 358a601bfe81 nbproject/configurations.xml --- a/nbproject/configurations.xml Fri May 09 23:17:36 2025 +0200 +++ b/nbproject/configurations.xml Sun May 11 00:30:03 2025 +0200 @@ -5,6 +5,8 @@ AlsaBridge.cpp DJMFix.cpp Logger.cpp + Message.cpp + MessageCodec.cpp djm-fix.cpp false - + @@ -56,6 +58,14 @@ + + + + + + + +