aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorEddy Pedroni <epedroni@pm.me>2025-07-23 22:33:05 +0200
committerEddy Pedroni <epedroni@pm.me>2025-07-23 22:33:05 +0200
commitc5c195ff5318f00d544c0fbceb133abcc4ba7a5a (patch)
tree1c7a3e646f6eb72d748dd606fe9e8e12175b39bd
parent41f95375a65a3f67a75258680d6d8b03bb4d678b (diff)
MVP
-rw-r--r--.gitmodules3
-rw-r--r--daemon/Makefile2
m---------daemon/include/tomlplusplus0
-rw-r--r--daemon/src/config.cpp179
-rw-r--r--daemon/src/config.h33
-rw-r--r--daemon/src/configuration.h17
-rw-r--r--daemon/src/connection_manager.cpp21
-rw-r--r--daemon/src/main.cpp29
-rw-r--r--daemon/src/routing.cpp8
-rw-r--r--daemon/src/routing.h2
-rw-r--r--daemon/src/sender.h8
-rw-r--r--daemon/src/types.h35
-rw-r--r--pacman.txt1
13 files changed, 289 insertions, 49 deletions
diff --git a/.gitmodules b/.gitmodules
index a85cfec..f69032b 100644
--- a/.gitmodules
+++ b/.gitmodules
@@ -1,3 +1,6 @@
[submodule "daemon/include/concurrentqueue"]
path = daemon/include/concurrentqueue
url = https://github.com/cameron314/concurrentqueue.git
+[submodule "daemon/include/tomlplusplus"]
+ path = daemon/include/tomlplusplus
+ url = https://github.com/marzer/tomlplusplus.git
diff --git a/daemon/Makefile b/daemon/Makefile
index e38108f..9869b08 100644
--- a/daemon/Makefile
+++ b/daemon/Makefile
@@ -24,7 +24,7 @@ INC_FLAGS := $(addprefix -I,$(INC_DIRS))
# These files will have .d instead of .o as the output.
CPPFLAGS := -O3 -std=c++23 $(INC_FLAGS) -MMD -MP
-LDFLAGS := -lrtmidi
+LDFLAGS := -lrtmidi -lcurl
# The final build step.
$(BUILD_DIR)/$(TARGET_EXEC): $(OBJS)
diff --git a/daemon/include/tomlplusplus b/daemon/include/tomlplusplus
new file mode 160000
+Subproject 30172438cee64926dc41fdd9c11fb3ba5b2ba9d
diff --git a/daemon/src/config.cpp b/daemon/src/config.cpp
new file mode 100644
index 0000000..b88fd0f
--- /dev/null
+++ b/daemon/src/config.cpp
@@ -0,0 +1,179 @@
+#include "config.h"
+
+#include "tomlplusplus/toml.hpp"
+
+#include <curl/curl.h>
+
+#include <iostream>
+
+namespace midi_router
+{
+
+static size_t write_function(void *ptr, size_t size, size_t nmemb, std::string* data) {
+ data->append((char*) ptr, size * nmemb);
+ return size * nmemb;
+}
+
+static long curl(std::string url, std::string & response)
+{
+ auto curl = curl_easy_init();
+ if (!curl) {
+ return -1;
+ }
+
+ curl_easy_setopt(curl, CURLOPT_URL, url.c_str());
+ curl_easy_setopt(curl, CURLOPT_NOPROGRESS, 1L);
+ curl_easy_setopt(curl, CURLOPT_MAXREDIRS, 50L);
+
+ curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, write_function);
+ curl_easy_setopt(curl, CURLOPT_WRITEDATA, &response);
+
+ curl_easy_perform(curl);
+
+ long response_code;
+ curl_easy_getinfo(curl, CURLINFO_RESPONSE_CODE, &response_code);
+
+ curl_easy_cleanup(curl);
+
+ return response_code;
+}
+
+Config_Loader::Config_Loader(std::string url)
+{
+ auto code = curl(url, m_file_contents);
+ if (code != 200)
+ {
+ std::cerr << "Failed to fetch config file with code " << code << "\n";
+ return;
+ }
+
+ auto toml_config = toml::parse(m_file_contents);
+
+ auto* devices = toml_config["devices"].as_table();
+ if (!devices)
+ {
+ std::cerr << "Expected \"devices\" to be a table.\n";
+ return;
+ }
+
+ for (auto&& [id, name] : *devices)
+ {
+ m_device_map[name.as_string()->get()] = id.str();
+ }
+}
+
+Device_Map const &
+Config_Loader::get_device_map() const
+{
+ return m_device_map;
+}
+
+static void
+add_to_routes(Device_Id const & from_device, Route_Table & rt, toml::table & routes, Connection_Manager const & cm)
+{
+ for (auto&& [key, types_node] : routes)
+ {
+ std::string to_device {key.str()};
+ auto types = types_node.as_array();
+ if (!types)
+ {
+ std::cerr << "Unable to parse note types for route from " << from_device << " to " << to_device << "\n";
+ continue;
+ }
+ for (auto & type : *types)
+ {
+ std::string type_name = type.as_string()->get();
+ if (type_name == "all")
+ {
+ for (auto&& [name, mt] : string2type)
+ {
+ rt[from_device][mt].push_back(cm.get_sender(to_device));
+ }
+ }
+ else
+ {
+ Message_Type mt = string2type.at(type.as_string()->get());
+ rt[from_device][mt].push_back(cm.get_sender(to_device));
+ }
+ }
+ }
+}
+
+static void
+add_type_routes(Device_Map const & device_map, Device_Id const & from_device, Route_Table & rt, toml::table & routes, Connection_Manager const & cm)
+{
+ for (auto&& [key, devices_node] : routes)
+ {
+ std::string type {key.str()};
+ auto devices = devices_node.as_array();
+ if (!devices)
+ {
+ std::cerr << "Unable to parse devices for route from " << from_device << " with type " << type << "\n";
+ continue;
+ }
+ for (auto & device : *devices)
+ {
+ Message_Type mt = string2type.at(type);
+ std::string to_device = device.as_string()->get();
+ if (to_device == "all")
+ {
+ for (auto&& [name, id] : device_map)
+ {
+ if (id == from_device) continue;
+ rt[from_device][mt].push_back(cm.get_sender(id));
+ }
+ }
+ else
+ {
+ rt[from_device][mt].push_back(cm.get_sender(to_device));
+ }
+ }
+ }
+}
+
+Route_Table const &
+Config_Loader::compile_route_table(Connection_Manager const & cm)
+{
+ m_route_table.clear();
+
+ auto toml_config = toml::parse(m_file_contents);
+
+ for (auto && [name, id] : m_device_map)
+ {
+ auto * to_routes = toml_config["route"]["from"][id]["to"].as_table();
+ if (to_routes)
+ {
+ add_to_routes(id, m_route_table, *to_routes, cm);
+ }
+
+ auto * type_routes = toml_config["route"]["from"][id]["type"].as_table();
+ if (type_routes)
+ {
+ add_type_routes(m_device_map, id, m_route_table, *type_routes, cm);
+ }
+ }
+
+ return m_route_table;
+}
+
+void
+Config_Loader::print_route_table() const
+{
+ std::cout << "Routes ===========================================================\n";
+ for (auto && [source_id, message_types] : m_route_table)
+ {
+ std::cout << "From " << source_id << ":\n";
+ for (auto && [type, targets] : message_types)
+ {
+ std::cout << "\t" << type2string.at(type) << ": ";
+ for (auto const & target : targets)
+ {
+ std::cout << target.get().get_id() << " ";
+ }
+ std::cout << "\n";
+ }
+ }
+ std::cout << "==================================================================\n";
+}
+
+} // namespace midi_router
diff --git a/daemon/src/config.h b/daemon/src/config.h
new file mode 100644
index 0000000..a3ec0af
--- /dev/null
+++ b/daemon/src/config.h
@@ -0,0 +1,33 @@
+#pragma once
+
+#include "types.h"
+
+#include "connection_manager.h"
+
+#include <string>
+
+namespace midi_router
+{
+
+class Config_Loader
+{
+public:
+ Config_Loader(std::string url);
+
+ Device_Map const &
+ get_device_map() const;
+
+ Route_Table const &
+ compile_route_table(Connection_Manager const & cm);
+
+ void
+ print_route_table() const;
+
+private:
+ std::string m_file_contents;
+ Device_Map m_device_map {};
+ Route_Table m_route_table {};
+};
+
+} // namespace midi_router
+
diff --git a/daemon/src/configuration.h b/daemon/src/configuration.h
deleted file mode 100644
index 4189315..0000000
--- a/daemon/src/configuration.h
+++ /dev/null
@@ -1,17 +0,0 @@
-#pragma once
-
-#include "types.h"
-
-#include <map>
-
-namespace midi_router
-{
-
-struct Configuration
-{
-
- Device_Map const devices;
- Route_Map const routes;
-};
-
-} // namespace midi_router
diff --git a/daemon/src/connection_manager.cpp b/daemon/src/connection_manager.cpp
index f6a02c1..6de1800 100644
--- a/daemon/src/connection_manager.cpp
+++ b/daemon/src/connection_manager.cpp
@@ -21,13 +21,19 @@ struct Device_Connection : public Sender
RtMidiOut midi_out {};
void
- send(std::array<std::uint8_t, 3> const & payload) override
+ send(std::vector<std::uint8_t> const & payload) override
{
if (midi_out.isPortOpen())
{
- midi_out.sendMessage(payload.data(), 3);
+ midi_out.sendMessage(&payload);
}
}
+
+ std::string
+ get_id() const override
+ {
+ return source_id;
+ }
};
static void
@@ -35,12 +41,15 @@ callback(double time_stamp, std::vector<unsigned char> *raw, void *user_data)
{
Device_Connection* device = static_cast<Device_Connection*>(user_data);
- if (raw->size() != 3)
+ if (raw->size() > 3)
{
- std::cerr << "Received message with wrong size from " << device->source_id << ", dropping\n";
+ std::cerr << "Received message with wrong size (" << raw->size() << ") from " << device->source_id << ", dropping\n";
+ for (auto const & byte : *raw) std::cerr << static_cast<unsigned>(byte);
+ std::cerr << "\n";
+ return;
}
- Message message { &device->source_id, {raw->at(0), raw->at(1), raw->at(2)} };
+ Message message { &device->source_id, *raw };
device->submitter.submit(message);
}
@@ -55,7 +64,7 @@ Connection_Manager::Connection_Manager(Device_Map const & device_map, Submitter
auto & connection = m_connections[id];
connection->midi_in.setCallback(&callback, connection.get());
- connection->midi_in.ignoreTypes(true, true, true);
+ connection->midi_in.ignoreTypes(true, false, true);
if (!open_port(&connection->midi_in, name))
{
diff --git a/daemon/src/main.cpp b/daemon/src/main.cpp
index 008fd2a..8ca0dd6 100644
--- a/daemon/src/main.cpp
+++ b/daemon/src/main.cpp
@@ -1,6 +1,7 @@
-#include "connection_manager.h"
#include "types.h"
+#include "connection_manager.h"
#include "routing.h"
+#include "config.h"
#include <iostream>
#include <thread>
@@ -8,31 +9,21 @@
using namespace midi_router;
+bool verbose = false;
+
int main()
{
- // LTG: move configuration to TOML file
- Device_Map const devices {
- { "Deluge MIDI 1", "deluge" },
- { "MPK mini Plus MIDI 1", "mpk" },
- { "Xjam MIDI 1", "xjam" },
- { "OP-1 MIDI 1", "op1" },
- };
-
+ Config_Loader loader {"https://files.0xf7.com/api/public/dl/NR1j-os8/midi-router/config.toml"};
Router router {};
- Connection_Manager cm { devices, router };
+ Connection_Manager cm { loader.get_device_map(), router };
+
+ Route_Table const & routes = loader.compile_route_table(cm);
- Route_Map const routes {
- { "mpk",
- {
- { Message_Type::NOTE_ON, { cm.get_sender("deluge"), cm.get_sender("op1") } },
- { Message_Type::NOTE_OFF, { cm.get_sender("deluge"), cm.get_sender("op1")} },
- }
- },
- };
+ if (verbose) loader.print_route_table();
while(true)
{
- router.route(routes);
+ router.route(routes, verbose);
//std::this_thread::sleep_for(std::chrono::milliseconds(500));
}
}
diff --git a/daemon/src/routing.cpp b/daemon/src/routing.cpp
index ac15eff..8dce695 100644
--- a/daemon/src/routing.cpp
+++ b/daemon/src/routing.cpp
@@ -7,7 +7,7 @@ namespace midi_router
{
void
-Router::route(Route_Map const & config)
+Router::route(Route_Table const & config, bool verbose)
{
Message m;
if (m_queue.wait_dequeue_timed(m, std::chrono::milliseconds(500)))
@@ -20,6 +20,12 @@ Router::route(Route_Map const & config)
// LTG: at least one of these map accesses can be avoided
for (auto const & target: config.at(*m.source_id).at(m.type()))
{
+ if (verbose)
+ {
+ std::cout << "[" << type2string.at(m.type()) << "] " << *m.source_id << " -> " << target.get().get_id() << " | ";
+ for (auto byte : m.bytes) std::cout << static_cast<unsigned>(byte) << " ";
+ std::cout << ")\n";
+ }
target.get().send(m.bytes);
}
}
diff --git a/daemon/src/routing.h b/daemon/src/routing.h
index 592606d..86f62d4 100644
--- a/daemon/src/routing.h
+++ b/daemon/src/routing.h
@@ -12,7 +12,7 @@ class Router : public Submitter
{
public:
void
- route(Route_Map const & config);
+ route(Route_Table const & config, bool verbose);
void
submit(Message const & message) override;
diff --git a/daemon/src/sender.h b/daemon/src/sender.h
index b7437c5..35f0fe3 100644
--- a/daemon/src/sender.h
+++ b/daemon/src/sender.h
@@ -1,7 +1,8 @@
#pragma once
#include <cstdint>
-#include <array>
+#include <vector>
+#include <string>
namespace midi_router
{
@@ -10,7 +11,10 @@ class Sender
{
public:
virtual void
- send(std::array<std::uint8_t, 3> const & payload) = 0;
+ send(std::vector<std::uint8_t> const & payload) = 0;
+
+ virtual std::string
+ get_id() const = 0;
};
} // namespace midi_router
diff --git a/daemon/src/types.h b/daemon/src/types.h
index a5c6165..78e85df 100644
--- a/daemon/src/types.h
+++ b/daemon/src/types.h
@@ -27,17 +27,18 @@ enum class Message_Type
START = 0xFAu,
CONTINUE = 0xFBu,
STOP = 0xFCu,
+
};
using Device_Id = std::string;
using Device_Map = std::map<std::string, Device_Id>;
using Target_List = std::vector<std::reference_wrapper<Sender>>;
-using Route_Map = std::map<Device_Id, std::map<Message_Type, Target_List>>;
+using Route_Table = std::map<Device_Id, std::map<Message_Type, Target_List>>;
struct Message
{
Device_Id const * source_id;
- std::array<std::uint8_t, 3> bytes;
+ std::vector<std::uint8_t> bytes;
Message_Type
type() const
@@ -47,4 +48,34 @@ struct Message
}
};
+static const std::map<std::string, Message_Type> string2type = {
+ {"note_off", Message_Type::NOTE_OFF},
+ {"note_on", Message_Type::NOTE_ON},
+ {"poly_at", Message_Type::POLY_AT},
+ {"control_change", Message_Type::CONTROL_CHANGE},
+ {"program_change", Message_Type::PROGRAM_CHANGE},
+ {"channel_at", Message_Type::CHANNEL_AT},
+ {"pitch_wheel", Message_Type::PITCH_WHEEL},
+ {"clock", Message_Type::CLOCK},
+ {"meas_end", Message_Type::MEAS_END},
+ {"start", Message_Type::START},
+ {"continue", Message_Type::CONTINUE},
+ {"stop", Message_Type::STOP},
+};
+
+static const std::map<Message_Type, std::string> type2string = {
+ {Message_Type::NOTE_OFF, "note_off"},
+ {Message_Type::NOTE_ON, "note_on"},
+ {Message_Type::POLY_AT, "poly_at"},
+ {Message_Type::CONTROL_CHANGE, "control_change"},
+ {Message_Type::PROGRAM_CHANGE, "program_change"},
+ {Message_Type::CHANNEL_AT, "channel_at"},
+ {Message_Type::PITCH_WHEEL, "pitch_wheel"},
+ {Message_Type::CLOCK, "clock"},
+ {Message_Type::MEAS_END, "meas_end"},
+ {Message_Type::START, "start"},
+ {Message_Type::CONTINUE, "continue"},
+ {Message_Type::STOP, "stop"}
+};
+
} // namespace midi_router
diff --git a/pacman.txt b/pacman.txt
index 5c1b6ef..65531cd 100644
--- a/pacman.txt
+++ b/pacman.txt
@@ -1,3 +1,4 @@
make
clang
rtmidi
+curl