DJMFix.cpp
author František Kučera <franta-hg@frantovo.cz>
Tue, 15 Apr 2025 22:45:25 +0200
branchv_0
changeset 16 63154f9d24a2
parent 13 334b727f7516
child 18 358a601bfe81
permissions -rw-r--r--
code formatting
     1 /**
     2  * DJM-Fix
     3  * Copyright © 2025 František Kučera (Frantovo.cz, GlobalCode.info)
     4  *
     5  * This program is free software: you can redistribute it and/or modify
     6  * it under the terms of the GNU General Public License as published by
     7  * the Free Software Foundation, version 3 of the License.
     8  *
     9  * This program is distributed in the hope that it will be useful,
    10  * but WITHOUT ANY WARRANTY; without even the implied warranty of
    11  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
    12  * GNU General Public License for more details.
    13  *
    14  * You should have received a copy of the GNU General Public License
    15  * along with this program. If not, see <http://www.gnu.org/licenses/>.
    16  */
    17 #include <iostream>
    18 #include <sstream>
    19 #include <iomanip>
    20 #include <thread>
    21 #include <mutex>
    22 #include <atomic>
    23 #include <chrono>
    24 #include <stdexcept>
    25 #include <vector>
    26 
    27 #include "DJMFix.h"
    28 
    29 namespace djmfix {
    30 
    31 using L = djmfix::logging::Level;
    32 using Bytes = std::vector<uint8_t>;
    33 namespace chro = std::chrono;
    34 
    35 class DJMFixImpl : public DJMFix {
    36 private:
    37 	MidiSender* midiSender;
    38 	djmfix::logging::Logger* logger;
    39 	const int keepAliveInterval = 200;
    40 	int keepAliveCounter = 0;
    41 	std::thread keepAliveThread;
    42 	std::recursive_mutex midiMutex;
    43 	std::atomic<bool> running{false};
    44 	std::atomic<bool> stopped{false};
    45 	std::atomic<bool> sendKeepAlive{false};
    46 	Bytes seed2;
    47 
    48 	void run() {
    49 		while (!stopped) {
    50 			logger->log(L::FINE, "DJMFixImpl::run()");
    51 			if (sendKeepAlive) send({
    52 					0xf0, 0x00, 0x40, 0x05,
    53 					0x00, 0x00, 0x00, 0x17,
    54 					0x00, 0x50, 0x01, 0xf7
    55 				});
    56 			std::this_thread::sleep_for(chro::milliseconds(keepAliveInterval));
    57 			keepAliveCounter++;
    58 			if (keepAliveCounter % (60 * 1000 / keepAliveInterval) == 0)
    59 				logger->log(L::INFO,
    60 					"Still sending periodic keep-alive messages "
    61 					"(each " + std::to_string(keepAliveInterval) + " ms).");
    62 		}
    63 	}
    64 
    65 	void send(const MidiMessage& midiMessage) {
    66 		std::lock_guard<std::recursive_mutex> lock(midiMutex);
    67 		midiSender->send(midiMessage);
    68 	}
    69 
    70 	std::string toString(const Bytes& midiMessage) {
    71 		std::stringstream result;
    72 		for (uint8_t b : midiMessage)
    73 			result << std::hex << std::setw(2) << std::setfill('0') << (int) b;
    74 		return result.str();
    75 	}
    76 
    77 	Bytes normalize(const Bytes& data) {
    78 		if (data.size() % 2) throw std::invalid_argument(
    79 				"Data before normalization must have even number of bytes.");
    80 		Bytes result;
    81 		result.reserve(data.size() / 2);
    82 		for (size_t i = 0; i < data.size() / 2; i++) result.push_back(
    83 				(data[i * 2] & 0x0F) << 4 | (data[i * 2 + 1] & 0x0F));
    84 		return result;
    85 	}
    86 
    87 	Bytes denormalize(const Bytes& data) {
    88 		Bytes result;
    89 		result.reserve(data.size()*2);
    90 		for (size_t i = 0; i < data.size(); i++) {
    91 			result.push_back(data[i] >> 4);
    92 			result.push_back(data[i] & 0x0F);
    93 		}
    94 		return result;
    95 	}
    96 
    97 	uint32_t fnv32hash(const Bytes& buff) {
    98 		uint32_t hash = 0x811c9dc5;
    99 		for (uint8_t b : buff) hash = ((b^hash) * 0x1000193);
   100 		return hash;
   101 	}
   102 
   103 	Bytes toBytes(const uint32_t value) {
   104 		Bytes result;
   105 		result.reserve(4);
   106 		result.push_back(value >> 24);
   107 		result.push_back(value >> 16);
   108 		result.push_back(value >> 8);
   109 		result.push_back(value >> 0);
   110 		return result;
   111 	}
   112 
   113 	bool equals(Bytes a, Bytes b) {
   114 		if (a.size() != b.size()) return false;
   115 		for (size_t i = 0; i < a.size(); i++) if (a[i] != b[i]) return false;
   116 		return true;
   117 	}
   118 
   119 	template<typename T> std::vector<T> concat(
   120 			const std::vector<T>& a,
   121 			const std::vector<T>& b,
   122 			const std::vector<T>& c = {}) //
   123 	{
   124 		std::vector<T> result;
   125 		result.reserve(a.size() + b.size() + c.size());
   126 		for (size_t i = 0; i < a.size(); i++) result.push_back(a[i]);
   127 		for (size_t i = 0; i < b.size(); i++) result.push_back(b[i]);
   128 		for (size_t i = 0; i < c.size(); i++) result.push_back(c[i]);
   129 		return result;
   130 	}
   131 
   132 	template<typename T>
   133 	std::vector<T> xOR(const std::vector<T>& a, const std::vector<T>& b) {
   134 		if (a.size() != b.size()) throw std::invalid_argument(
   135 				"Both must be the same length when doing XOR.");
   136 		std::vector<T> result;
   137 		result.reserve(a.size());
   138 		for (size_t i = 0; i < a.size(); i++) result.push_back(a[i] ^ b[i]);
   139 		return result;
   140 	}
   141 
   142 public:
   143 
   144 	DJMFixImpl(djmfix::logging::Logger* logger)
   145 	: logger(logger ? logger : djmfix::logging::blackhole()) {
   146 	}
   147 
   148 	virtual ~DJMFixImpl() override {
   149 		logger->log(L::FINER, "~DJMFixImpl()");
   150 		if (running) stop();
   151 	}
   152 
   153 	void setMidiSender(MidiSender* midiSender) {
   154 		logger->log(L::FINER, "DJMFixImpl::setMidiSender()");
   155 		this->midiSender = midiSender;
   156 	}
   157 
   158 	virtual void receive(const MidiMessage& msg) override {
   159 		logger->log(L::FINE, "Received a message: "
   160 				"size = " + std::to_string(msg.size()) + " "
   161 				"data = " + toString(msg));
   162 		std::lock_guard<std::recursive_mutex> lock(midiMutex);
   163 
   164 
   165 		if (msg.size() == 12 && msg[9] == 0x11) {
   166 			logger->log(L::INFO, "Received greeting message.");
   167 			send({
   168 				0xf0, 0x00, 0x40, 0x05,
   169 				0x00, 0x00, 0x00, 0x17,
   170 				0x00, 0x12, 0x2a, 0x01,
   171 				0x0b, 0x50, 0x69, 0x6f,
   172 				0x6e, 0x65, 0x65, 0x72,
   173 				0x44, 0x4a, 0x02, 0x0b,
   174 				0x72, 0x65, 0x6b, 0x6f,
   175 				0x72, 0x64, 0x62, 0x6f,
   176 				0x78, 0x03, 0x12, 0x02,
   177 				0x09, 0x00, 0x00, 0x00,
   178 				0x00, 0x00, 0x00, 0x02,
   179 				0x03, 0x04, 0x08, 0x00,
   180 				0x00, 0x00, 0x00, 0xf7
   181 			});
   182 			logger->log(L::INFO, "Sent message with seed1.");
   183 		} else if (msg.size() == 54
   184 				&& msg[9] == 0x13
   185 				&& msg[33] == 0x04
   186 				&& msg[43] == 0x03) //
   187 		{
   188 			Bytes hash1(msg.begin() + 35, msg.begin() + 35 + 8);
   189 			seed2 = Bytes(msg.begin() + 45, msg.begin() + 45 + 8);
   190 			hash1 = normalize(hash1);
   191 			seed2 = normalize(seed2);
   192 			logger->log(L::INFO, "Received message with "
   193 					"hash1 = " + toString(hash1) + " and "
   194 					"seed2 = " + toString(seed2));
   195 
   196 			Bytes seed0 = {0x68, 0x01, 0x31, 0xFB};
   197 			Bytes seed1 = {0x29, 0x00, 0x00, 0x00, 0x23, 0x48, 0x00, 0x00};
   198 
   199 			Bytes hash1check =
   200 					toBytes(fnv32hash(concat(seed1, xOR(seed0, seed2))));
   201 
   202 			if (equals(hash1, hash1check)) {
   203 				logger->log(L::INFO, "Verification of hash1 was successful.");
   204 				Bytes hash2 =
   205 						toBytes(fnv32hash(concat(seed2, xOR(seed0, seed2))));
   206 				send(concat({
   207 					0xf0, 0x00, 0x40, 0x05,
   208 					0x00, 0x00, 0x00, 0x17,
   209 					0x00, 0x14, 0x38, 0x01,
   210 					0x0b, 0x50, 0x69, 0x6f,
   211 					0x6e, 0x65, 0x65, 0x72,
   212 					0x44, 0x4a, 0x02, 0x0b,
   213 					0x72, 0x65, 0x6b, 0x6f,
   214 					0x72, 0x64, 0x62, 0x6f,
   215 					0x78, 0x04, 0x0a
   216 				}, concat(denormalize(hash2),{
   217 					0x05, 0x16, 0x05, 0x09,
   218 					0x0b, 0x05, 0x04, 0x0b,
   219 					0x0f, 0x0e, 0x0e, 0x04,
   220 					0x04, 0x0a, 0x05, 0x0a,
   221 					0x0c, 0x08, 0x0e, 0x04,
   222 					0x0c, 0x05, 0xf7
   223 				})));
   224 				logger->log(L::INFO, "Sent message with hash2.");
   225 			} else {
   226 				std::stringstream logMessage;
   227 				logMessage
   228 						<< "Verification of hash1 failed: "
   229 						<< " midiMessage = " << toString(msg)
   230 						<< " seed0 = " << toString(seed0)
   231 						<< " seed1 = " << toString(seed1)
   232 						<< " seed2 = " << toString(seed2)
   233 						<< " hash1 = " << toString(hash1)
   234 						<< " hash1check = " << toString(hash1check);
   235 				logger->log(L::SEVERE, logMessage.str());
   236 				// TODO: graceful death
   237 			}
   238 		} else if (msg.size() == 12 && msg[9] == 0x15) {
   239 			sendKeepAlive = true;
   240 			logger->log(L::INFO, "Received acknowledgment message. "
   241 					"Started sending keep-alive messages. "
   242 					"LINE/PHONO channels should work now.");
   243 		}
   244 
   245 	}
   246 
   247 	void start() override {
   248 		logger->log(L::FINE, "DJMFixImpl::start()");
   249 		if (midiSender == nullptr)
   250 			throw std::logic_error("Need a midiSender when starting DJMFix");
   251 
   252 		// TODO: methods for parsing and constructing messages from parts (TLV)
   253 		send({
   254 			0xf0, 0x00, 0x40, 0x05,
   255 			0x00, 0x00, 0x00, 0x17,
   256 			0x00, 0x50, 0x01, 0xf7
   257 		});
   258 		logger->log(L::INFO, "Sent greeting message.");
   259 
   260 		keepAliveThread = std::thread(&DJMFixImpl::run, this);
   261 		running = true;
   262 
   263 	}
   264 
   265 	void stop() override {
   266 		stopped = true;
   267 		keepAliveThread.join();
   268 		running = false;
   269 		logger->log(L::FINE, "DJMFixImpl::stop()");
   270 	}
   271 };
   272 
   273 DJMFix* create(djmfix::logging::Logger* logger) {
   274 	return new DJMFixImpl(logger);
   275 }
   276 
   277 }