ACMX 2.139.0
Dual-Backend Real-Time GPU Video Synthesis
Loading...
Searching...
No Matches
midi.cpp
Go to the documentation of this file.
1#include "midi.hpp"
2
4
5#include <rtmidi/RtMidi.h>
6
7#include <algorithm>
8#include <charconv>
9#include <cmath>
10#include <deque>
11#include <fstream>
12#include <iostream>
13#include <mutex>
14#include <sstream>
15#include <stdexcept>
16#include <string>
17#include <utility>
18
19namespace acmxvk::midi {
20
21 void parse_block(std::string_view b) {
22 int stat = 0, value = 0, code = 0;
23 const char *cur = b.data();
24 const char *end = b.data() + b.size();
25 auto [ptr_value, e] = std::from_chars(cur, end, stat);
26 if (e != std::errc{}) {
27 std::cerr << "failed to parse status bytesn\n";
28 return;
29 }
30 cur = ptr_value;
31 while (cur != end && *cur == ' ') {
32 cur++;
33 }
34
35 auto [ptr_value2, e2] = std::from_chars(cur, end, code);
36 if (e2 != std::errc{}) {
37 std::cerr << "Failed to parse code\n";
38 return;
39 }
40 cur = ptr_value2;
41 while (cur != end && *cur == ' ') {
42 cur++;
43 }
44 auto [ptr3, e3] = std::from_chars(cur, end, value);
45 if (e3 != std::errc{}) {
46 std::cerr << "Error failed to pasre value\n";
47 return;
48 }
49
50 std::cout << "Status: " << stat << " CC: " << code << " Value: " << value << "\n";
51 }
52
53 std::vector<MidiMapping> load_mapping_file(const std::string &filename) {
54 input::validate_string(filename, input::StringKind::Path, "MIDI map path");
55 input::validate_file_size(filename, "MIDI map file");
56 std::ifstream mapping_input(filename);
57 if (!mapping_input) {
58 throw std::runtime_error("could not open MIDI map file: " + filename);
59 }
60
61 std::vector<MidiMapping> mappings;
62 std::string line;
63 std::size_t line_number = 0;
64 while (input::read_bounded_line(mapping_input, line, "MIDI map file", line_number + 1)) {
65
66 parse_block(line);
67 ++line_number;
68 const std::size_t first = line.find_first_not_of(" \t\r");
69 if (first == std::string::npos || line[first] == '#') {
70 continue;
71 }
72 if (mappings.size() >= input::MAX_MIDI_MAPPINGS) {
73 throw std::runtime_error("MIDI map file contains too many mappings");
74 }
76
77 std::istringstream stream(line);
78 std::string action_pair;
79 char open_brace = 0;
80 char close_brace = 0;
81 int status = 0;
82 int data1 = 0;
83 int data2 = 0;
84 if (!(stream >> action_pair >> open_brace >> status >> data1 >> data2 >> close_brace) || open_brace != '{' || close_brace != '}') {
85 throw std::runtime_error("invalid MIDI map entry at " + filename + ':' + std::to_string(line_number));
86 }
87 stream >> std::ws;
88 if (!stream.eof()) {
89 throw std::runtime_error("unexpected text in MIDI map at " + filename + ':' + std::to_string(line_number));
90 }
91
92 const std::size_t colon = action_pair.find(':');
93 if (colon == std::string::npos || colon == 0 || colon + 1 >= action_pair.size() || action_pair.find(':', colon + 1) != std::string::npos) {
94 throw std::runtime_error("invalid MIDI action pair at " + filename + ':' + std::to_string(line_number));
95 }
96
97 std::size_t primary_parsed = 0;
98 std::size_t secondary_parsed = 0;
99 int primary = 0;
100 int secondary = 0;
101 try {
102 primary = std::stoi(action_pair.substr(0, colon), &primary_parsed);
103 secondary = std::stoi(action_pair.substr(colon + 1), &secondary_parsed);
104 } catch (const std::exception &) {
105 throw std::runtime_error("invalid MIDI action code at " + filename + ':' + std::to_string(line_number));
106 }
107 if (primary_parsed != colon || secondary_parsed != action_pair.size() - colon - 1 || primary <= 0 || primary > 65535 || secondary < 0 || secondary > 65535 || status < 0 || status > 255 || data1 < 0 || data1 > 255 || data2 < 0 || data2 > 255) {
108 throw std::runtime_error("MIDI map value outside its valid range at " + filename + ':' + std::to_string(line_number));
109 }
110
111 mappings.push_back({primary, secondary, static_cast<unsigned char>(status), static_cast<unsigned char>(data1), static_cast<unsigned char>(data2)});
112 }
113 return mappings;
114 }
115
117 public:
118 static constexpr std::size_t MAX_PENDING_MESSAGES = 256;
119 static constexpr std::size_t MAX_MESSAGE_BYTES = 1024;
120
121 ~Impl() { close(); }
122
123 bool open(int requested_port) {
124 close();
125 if (requested_port < 0) {
126 std::cerr << "acmxvk: MIDI port must be non-negative\n";
127 return false;
128 }
129
130 try {
131 input = std::make_unique<RtMidiIn>();
132 const unsigned int port_count = input->getPortCount();
133 const auto port = static_cast<unsigned int>(requested_port);
134 if (port_count == 0) {
135 std::cerr << "acmxvk: no MIDI input ports found\n";
136 input.reset();
137 return false;
138 }
139 if (port >= port_count) {
140 std::cerr << "acmxvk: MIDI input port " << port << " is outside the available range 0.." << (port_count - 1) << '\n';
141 input.reset();
142 return false;
143 }
144
145 port_name = input->getPortName(port);
147 input->ignoreTypes(false, false, false);
148 input->setCallback(&Impl::messageCallback, this);
149 input->openPort(port, "ACMXVK MIDI Input");
150 open_port = requested_port;
151 std::cout << "acmxvk: MIDI input " << open_port << ": " << port_name << '\n';
152 return input->isPortOpen();
153 } catch (const RtMidiError &error) {
154 std::cerr << "acmxvk: MIDI input error: " << error.getMessage() << '\n';
155 close();
156 return false;
157 }
158 }
159
160 void close() {
161 if (input != nullptr) {
162 try {
163 input->cancelCallback();
164 if (input->isPortOpen()) {
165 input->closePort();
166 }
167 } catch (const RtMidiError &error) {
168 std::cerr << "acmxvk: error closing MIDI input: " << error.getMessage() << '\n';
169 }
170 input.reset();
171 }
172 {
173 std::lock_guard<std::mutex> lock(queue_mutex);
174 pending_messages.clear();
177 }
178 open_port = -1;
179 port_name.clear();
180 }
181
182 [[nodiscard]] bool is_open() const { return input != nullptr && input->isPortOpen(); }
183
184 [[nodiscard]] std::vector<MidiMessage> pollMessages() {
185 std::lock_guard<std::mutex> lock(queue_mutex);
186 std::vector<MidiMessage> messages;
187 messages.reserve(pending_messages.size());
188 while (!pending_messages.empty()) {
189 messages.push_back(std::move(pending_messages.front()));
190 pending_messages.pop_front();
191 }
192 return messages;
193 }
194
195 [[nodiscard]] std::uint64_t droppedMessageCount() const {
196 std::lock_guard<std::mutex> lock(queue_mutex);
197 return dropped_messages;
198 }
199
200 static void messageCallback(double delta_seconds, std::vector<unsigned char> *bytes, void *user_data) {
201 if (user_data == nullptr || bytes == nullptr || bytes->empty()) {
202 return;
203 }
204 static_cast<Impl *>(user_data)->enqueue(delta_seconds, *bytes);
205 }
206
207 void enqueue(double delta_seconds, const std::vector<unsigned char> &bytes) {
208 std::lock_guard<std::mutex> lock(queue_mutex);
209 if (bytes.size() > MAX_MESSAGE_BYTES) {
211 return;
212 }
214 pending_messages.pop_front();
216 }
217 pending_messages.push_back(MidiMessage{std::isfinite(delta_seconds) ? std::max(delta_seconds, 0.0) : 0.0, bytes, ++message_sequence});
218 }
219
220 std::unique_ptr<RtMidiIn> input;
221 mutable std::mutex queue_mutex;
222 std::deque<MidiMessage> pending_messages;
223 std::string port_name;
224 std::uint64_t message_sequence = 0;
225 std::uint64_t dropped_messages = 0;
226 int open_port = -1;
227 };
228
229 MidiInput::MidiInput() : impl(std::make_unique<Impl>()) {}
230 MidiInput::~MidiInput() = default;
231
232 bool MidiInput::open(int port) { return impl->open(port); }
233
234 void MidiInput::close() { impl->close(); }
235
236 bool MidiInput::is_open() const { return impl->is_open(); }
237
238 std::vector<MidiMessage> MidiInput::poll_messages() { return impl->pollMessages(); }
239
240 std::uint64_t MidiInput::dropped_message_count() const { return impl->droppedMessageCount(); }
241
242 void MidiInput::list_ports(std::ostream &output) {
243 try {
244 RtMidiIn input;
245 const unsigned int port_count = input.getPortCount();
246 output << "acmxvk: found " << port_count << " MIDI input port(s)\n";
247 for (unsigned int port = 0; port < port_count; ++port) {
248 const std::string name = input.getPortName(port);
250 output << " " << port << ": " << name << '\n';
251 }
252 } catch (const RtMidiError &error) {
253 throw std::runtime_error(std::string("could not enumerate MIDI inputs: ") + error.getMessage());
254 }
255 }
256
257} // namespace acmxvk::midi
std::unique_ptr< RtMidiIn > input
Definition midi.cpp:220
static constexpr std::size_t MAX_MESSAGE_BYTES
Definition midi.cpp:119
std::uint64_t droppedMessageCount() const
Definition midi.cpp:195
std::deque< MidiMessage > pending_messages
Definition midi.cpp:222
bool open(int requested_port)
Definition midi.cpp:123
std::uint64_t dropped_messages
Definition midi.cpp:225
std::uint64_t message_sequence
Definition midi.cpp:224
void enqueue(double delta_seconds, const std::vector< unsigned char > &bytes)
Definition midi.cpp:207
std::vector< MidiMessage > pollMessages()
Definition midi.cpp:184
static void messageCallback(double delta_seconds, std::vector< unsigned char > *bytes, void *user_data)
Definition midi.cpp:200
static constexpr std::size_t MAX_PENDING_MESSAGES
Definition midi.cpp:118
std::uint64_t dropped_message_count() const
Definition midi.cpp:240
std::vector< MidiMessage > poll_messages()
Definition midi.cpp:238
static void list_ports(std::ostream &output)
Definition midi.cpp:242
bool is_open() const
Definition midi.cpp:236
std::unique_ptr< Impl > impl
Definition midi.hpp:46
bool open(int port=0)
Definition midi.cpp:232
bool read_bounded_line(std::istream &input, std::string &line, std::string_view context, std::size_t line_number, std::size_t maximum_bytes)
void validate_file_size(const std::filesystem::path &path, std::string_view context, std::uintmax_t maximum_bytes)
constexpr std::size_t MAX_MIDI_MAPPINGS
void validate_string(std::string_view value, StringKind kind, std::string_view context, bool allow_empty)
void parse_block(std::string_view b)
Definition midi.cpp:21
std::vector< MidiMapping > load_mapping_file(const std::string &filename)
Definition midi.cpp:53