ACMX 2.136.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 <cmath>
9#include <deque>
10#include <fstream>
11#include <iostream>
12#include <mutex>
13#include <sstream>
14#include <stdexcept>
15#include <string>
16#include <utility>
17#include<charconv>
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) {
55 "MIDI map path");
56 input::validate_file_size(filename, "MIDI map file");
57 std::ifstream mapping_input(filename);
58 if (!mapping_input) {
59 throw std::runtime_error("could not open MIDI map file: " + filename);
60 }
61
62 std::vector<MidiMapping> mappings;
63 std::string line;
64 std::size_t line_number = 0;
65 while (input::read_bounded_line(mapping_input, line, "MIDI map file",
66 line_number + 1)) {
67
68 parse_block(line);
69 ++line_number;
70 const std::size_t first = line.find_first_not_of(" \t\r");
71 if (first == std::string::npos || line[first] == '#') {
72 continue;
73 }
74 if (mappings.size() >= input::MAX_MIDI_MAPPINGS) {
75 throw std::runtime_error(
76 "MIDI map file contains too many mappings");
77 }
79 "MIDI map entry");
80
81 std::istringstream stream(line);
82 std::string action_pair;
83 char open_brace = 0;
84 char close_brace = 0;
85 int status = 0;
86 int data1 = 0;
87 int data2 = 0;
88 if (!(stream >> action_pair >> open_brace >> status >> data1 >> data2 >>
89 close_brace) ||
90 open_brace != '{' || close_brace != '}') {
91 throw std::runtime_error(
92 "invalid MIDI map entry at " + filename + ':' +
93 std::to_string(line_number));
94 }
95 stream >> std::ws;
96 if (!stream.eof()) {
97 throw std::runtime_error(
98 "unexpected text in MIDI map at " + filename + ':' +
99 std::to_string(line_number));
100 }
101
102 const std::size_t colon = action_pair.find(':');
103 if (colon == std::string::npos || colon == 0 ||
104 colon + 1 >= action_pair.size() ||
105 action_pair.find(':', colon + 1) != std::string::npos) {
106 throw std::runtime_error(
107 "invalid MIDI action pair at " + filename + ':' +
108 std::to_string(line_number));
109 }
110
111 std::size_t primary_parsed = 0;
112 std::size_t secondary_parsed = 0;
113 int primary = 0;
114 int secondary = 0;
115 try {
116 primary = std::stoi(action_pair.substr(0, colon),
117 &primary_parsed);
118 secondary = std::stoi(action_pair.substr(colon + 1),
119 &secondary_parsed);
120 } catch (const std::exception &) {
121 throw std::runtime_error(
122 "invalid MIDI action code at " + filename + ':' +
123 std::to_string(line_number));
124 }
125 if (primary_parsed != colon ||
126 secondary_parsed != action_pair.size() - colon - 1 ||
127 primary <= 0 || primary > 65535 || secondary < 0 ||
128 secondary > 65535 || status < 0 || status > 255 ||
129 data1 < 0 || data1 > 255 || data2 < 0 || data2 > 255) {
130 throw std::runtime_error(
131 "MIDI map value outside its valid range at " + filename +
132 ':' + std::to_string(line_number));
133 }
134
135 mappings.push_back(
136 {primary, secondary, static_cast<unsigned char>(status),
137 static_cast<unsigned char>(data1),
138 static_cast<unsigned char>(data2)});
139 }
140 return mappings;
141 }
142
144 public:
145 static constexpr std::size_t MAX_PENDING_MESSAGES = 256;
146 static constexpr std::size_t MAX_MESSAGE_BYTES = 1024;
147
148 ~Impl() { close(); }
149
150 bool open(int requested_port) {
151 close();
152 if (requested_port < 0) {
153 std::cerr << "acmxvk: MIDI port must be non-negative\n";
154 return false;
155 }
156
157 try {
158 input = std::make_unique<RtMidiIn>();
159 const unsigned int port_count = input->getPortCount();
160 const auto port = static_cast<unsigned int>(requested_port);
161 if (port_count == 0) {
162 std::cerr << "acmxvk: no MIDI input ports found\n";
163 input.reset();
164 return false;
165 }
166 if (port >= port_count) {
167 std::cerr << "acmxvk: MIDI input port " << port
168 << " is outside the available range 0.."
169 << (port_count - 1) << '\n';
170 input.reset();
171 return false;
172 }
173
174 port_name = input->getPortName(port);
177 "MIDI port name");
178 input->ignoreTypes(false, false, false);
179 input->setCallback(&Impl::messageCallback, this);
180 input->openPort(port, "ACMXVK MIDI Input");
181 open_port = requested_port;
182 std::cout << "acmxvk: MIDI input " << open_port << ": "
183 << port_name << '\n';
184 return input->isPortOpen();
185 } catch (const RtMidiError &error) {
186 std::cerr << "acmxvk: MIDI input error: " << error.getMessage()
187 << '\n';
188 close();
189 return false;
190 }
191 }
192
193 void close() {
194 if (input != nullptr) {
195 try {
196 input->cancelCallback();
197 if (input->isPortOpen()) {
198 input->closePort();
199 }
200 } catch (const RtMidiError &error) {
201 std::cerr << "acmxvk: error closing MIDI input: "
202 << error.getMessage() << '\n';
203 }
204 input.reset();
205 }
206 {
207 std::lock_guard<std::mutex> lock(queue_mutex);
208 pending_messages.clear();
211 }
212 open_port = -1;
213 port_name.clear();
214 }
215
216 [[nodiscard]] bool is_open() const {
217 return input != nullptr && input->isPortOpen();
218 }
219
220 [[nodiscard]] std::vector<MidiMessage> pollMessages() {
221 std::lock_guard<std::mutex> lock(queue_mutex);
222 std::vector<MidiMessage> messages;
223 messages.reserve(pending_messages.size());
224 while (!pending_messages.empty()) {
225 messages.push_back(std::move(pending_messages.front()));
226 pending_messages.pop_front();
227 }
228 return messages;
229 }
230
231 [[nodiscard]] std::uint64_t droppedMessageCount() const {
232 std::lock_guard<std::mutex> lock(queue_mutex);
233 return dropped_messages;
234 }
235
236 static void messageCallback(double delta_seconds,
237 std::vector<unsigned char> *bytes,
238 void *user_data) {
239 if (user_data == nullptr || bytes == nullptr || bytes->empty()) {
240 return;
241 }
242 static_cast<Impl *>(user_data)->enqueue(delta_seconds, *bytes);
243 }
244
245 void enqueue(double delta_seconds,
246 const std::vector<unsigned char> &bytes) {
247 std::lock_guard<std::mutex> lock(queue_mutex);
248 if (bytes.size() > MAX_MESSAGE_BYTES) {
250 return;
251 }
253 pending_messages.pop_front();
255 }
256 pending_messages.push_back(
257 MidiMessage{std::isfinite(delta_seconds)
258 ? std::max(delta_seconds, 0.0)
259 : 0.0,
260 bytes, ++message_sequence});
261 }
262
263 std::unique_ptr<RtMidiIn> input;
264 mutable std::mutex queue_mutex;
265 std::deque<MidiMessage> pending_messages;
266 std::string port_name;
267 std::uint64_t message_sequence = 0;
268 std::uint64_t dropped_messages = 0;
269 int open_port = -1;
270 };
271
272 MidiInput::MidiInput() : impl(std::make_unique<Impl>()) {}
273 MidiInput::~MidiInput() = default;
274
275 bool MidiInput::open(int port) {
276 return impl->open(port);
277 }
278
280 impl->close();
281 }
282
283 bool MidiInput::is_open() const {
284 return impl->is_open();
285 }
286
287 std::vector<MidiMessage> MidiInput::poll_messages() {
288 return impl->pollMessages();
289 }
290
291 std::uint64_t MidiInput::dropped_message_count() const {
292 return impl->droppedMessageCount();
293 }
294
295 void MidiInput::list_ports(std::ostream &output) {
296 try {
297 RtMidiIn input;
298 const unsigned int port_count = input.getPortCount();
299 output << "acmxvk: found " << port_count << " MIDI input port(s)\n";
300 for (unsigned int port = 0; port < port_count; ++port) {
301 const std::string name = input.getPortName(port);
304 "MIDI port name");
305 output << " " << port << ": " << name << '\n';
306 }
307 } catch (const RtMidiError &error) {
308 throw std::runtime_error(std::string("could not enumerate MIDI inputs: ") +
309 error.getMessage());
310 }
311 }
312
313} // namespace acmxvk::midi
std::unique_ptr< RtMidiIn > input
Definition midi.cpp:263
static constexpr std::size_t MAX_MESSAGE_BYTES
Definition midi.cpp:146
std::uint64_t droppedMessageCount() const
Definition midi.cpp:231
std::deque< MidiMessage > pending_messages
Definition midi.cpp:265
bool open(int requested_port)
Definition midi.cpp:150
std::uint64_t dropped_messages
Definition midi.cpp:268
std::uint64_t message_sequence
Definition midi.cpp:267
void enqueue(double delta_seconds, const std::vector< unsigned char > &bytes)
Definition midi.cpp:245
std::vector< MidiMessage > pollMessages()
Definition midi.cpp:220
static void messageCallback(double delta_seconds, std::vector< unsigned char > *bytes, void *user_data)
Definition midi.cpp:236
static constexpr std::size_t MAX_PENDING_MESSAGES
Definition midi.cpp:145
std::uint64_t dropped_message_count() const
Definition midi.cpp:291
std::vector< MidiMessage > poll_messages()
Definition midi.cpp:287
static void list_ports(std::ostream &output)
Definition midi.cpp:295
bool is_open() const
Definition midi.cpp:283
std::unique_ptr< Impl > impl
Definition midi.hpp:47
bool open(int port=0)
Definition midi.cpp:275
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