openMSX
MidiSessionALSA.cc
Go to the documentation of this file.
1#include "MidiSessionALSA.hh"
2#include "CliComm.hh"
3#include "MidiOutDevice.hh"
4#include "MidiInDevice.hh"
5#include "MidiInConnector.hh"
6#include "EventListener.hh"
7#include "EventDistributor.hh"
8#include "PlugException.hh"
10#include "Scheduler.hh"
11#include "narrow.hh"
12#include "serialize.hh"
13#include "circular_buffer.hh"
14#include "checked_cast.hh"
15#include <iostream>
16#include <memory>
17#include <thread>
18
19
20namespace openmsx {
21
22// MidiOutALSA ==============================================================
23
24class MidiOutALSA final : public MidiOutDevice {
25public:
27 snd_seq_t& seq,
28 snd_seq_client_info_t& cinfo, snd_seq_port_info_t& pinfo);
29 ~MidiOutALSA() override;
30 MidiOutALSA(const MidiOutALSA&) = delete;
32
33 // Pluggable
34 void plugHelper(Connector& connector, EmuTime::param time) override;
35 void unplugHelper(EmuTime::param time) override;
36 [[nodiscard]] std::string_view getName() const override;
37 [[nodiscard]] std::string_view getDescription() const override;
38
39 // MidiOutDevice
40 void recvMessage(
41 const std::vector<uint8_t>& message, EmuTime::param time) override;
42
43 template<typename Archive>
44 void serialize(Archive& ar, unsigned version);
45
46private:
47 void connect();
48 void disconnect();
49
50private:
51 snd_seq_t& seq;
52 snd_midi_event_t* event_parser;
53 int sourcePort = -1;
54 int destClient;
55 int destPort;
56 std::string name;
57 std::string desc;
58 bool connected = false;
59};
60
62 snd_seq_t& seq_,
63 snd_seq_client_info_t& cinfo, snd_seq_port_info_t& pinfo)
64 : seq(seq_)
65 , destClient(snd_seq_port_info_get_client(&pinfo))
66 , destPort(snd_seq_port_info_get_port(&pinfo))
67 , name(snd_seq_client_info_get_name(&cinfo))
68 , desc(snd_seq_port_info_get_name(&pinfo))
69{
70}
71
73{
74 if (connected) {
75 disconnect();
76 }
77}
78
79void MidiOutALSA::plugHelper(Connector& /*connector_*/, EmuTime::param /*time*/)
80{
81 connect();
82}
83
84void MidiOutALSA::unplugHelper(EmuTime::param /*time*/)
85{
86 disconnect();
87}
88
89void MidiOutALSA::connect()
90{
91 sourcePort = snd_seq_create_simple_port(
92 &seq, "MIDI out pluggable",
93 0, SND_SEQ_PORT_TYPE_MIDI_GENERIC);
94 if (sourcePort < 0) {
95 throw PlugException(
96 "Failed to create ALSA port: ", snd_strerror(sourcePort));
97 }
98
99 if (int err = snd_seq_connect_to(&seq, sourcePort, destClient, destPort)) {
100 snd_seq_delete_simple_port(&seq, sourcePort);
101 throw PlugException(
102 "Failed to connect to ALSA port "
103 "(", destClient, ':', destPort, ")"
104 ": ", snd_strerror(err));
105 }
106
107 snd_midi_event_new(MAX_MESSAGE_SIZE, &event_parser);
108
109 connected = true;
110}
111
112void MidiOutALSA::disconnect()
113{
114 snd_midi_event_free(event_parser);
115 snd_seq_disconnect_to(&seq, sourcePort, destClient, destPort);
116 snd_seq_delete_simple_port(&seq, sourcePort);
117
118 connected = false;
119}
120
121std::string_view MidiOutALSA::getName() const
122{
123 return name;
124}
125
126std::string_view MidiOutALSA::getDescription() const
127{
128 return desc;
129}
130
132 const std::vector<uint8_t>& message, EmuTime::param /*time*/)
133{
134 snd_seq_event_t ev;
135 snd_seq_ev_clear(&ev);
136
137 // Set routing.
138 snd_seq_ev_set_source(&ev, narrow_cast<uint8_t>(sourcePort));
139 snd_seq_ev_set_subs(&ev);
140
141 // Set message.
142 if (auto encodeLen = snd_midi_event_encode(
143 event_parser, message.data(), narrow<long>(message.size()), &ev);
144 encodeLen < 0) {
145 std::cerr << "Error encoding MIDI message of type "
146 << std::hex << int(message[0]) << std::dec
147 << ": " << snd_strerror(narrow<int>(encodeLen)) << '\n';
148 return;
149 }
150 if (ev.type == SND_SEQ_EVENT_NONE) {
151 std::cerr << "Incomplete MIDI message of type "
152 << std::hex << int(message[0]) << std::dec << '\n';
153 return;
154 }
155
156 // Send event.
157 snd_seq_ev_set_direct(&ev);
158 if (int err = snd_seq_event_output(&seq, &ev); err < 0) {
159 std::cerr << "Error sending MIDI event: "
160 << snd_strerror(err) << '\n';
161 }
162 snd_seq_drain_output(&seq);
163}
164
165template<typename Archive>
166void MidiOutALSA::serialize(Archive& /*ar*/, unsigned /*version*/)
167{
168 if constexpr (Archive::IS_LOADER) {
169 connect();
170 }
171}
174
175// MidiInALSA ==============================================================
176class MidiInALSA final : public MidiInDevice, private EventListener
177{
178public:
180 EventDistributor& eventDistributor, Scheduler& scheduler, snd_seq_t& seq,
181 snd_seq_client_info_t& cinfo, snd_seq_port_info_t& pinfo);
182 ~MidiInALSA() override;
183
184 // Pluggable
185 void plugHelper(Connector& connector, EmuTime::param time) override;
186 void unplugHelper(EmuTime::param time) override;
187 [[nodiscard]] std::string_view getName() const override;
188 [[nodiscard]] std::string_view getDescription() const override;
189
190 // MidiInDevice
191 void signal(EmuTime::param time) override;
192
193 template<typename Archive>
194 void serialize(Archive& ar, unsigned version);
195
196private:
197 void run();
198 void connect();
199 void disconnect();
200
201 // EventListener
202 int signalEvent(const Event& event) override;
203
204private:
205 EventDistributor& eventDistributor;
206 Scheduler& scheduler;
207 std::thread thread;
208 snd_seq_t& seq;
209 snd_midi_event_t* event_parser;
210 int destinationPort = -1;
211 int srcClient;
212 int srcPort;
213 std::string name;
214 std::string desc;
215 bool connected = false;
216 bool stop = false;
217 cb_queue<uint8_t> queue;
218 std::mutex mutex; // to protect queue
219};
220
222 EventDistributor& eventDistributor_,
223 Scheduler& scheduler_,
224 snd_seq_t& seq_,
225 snd_seq_client_info_t& cinfo, snd_seq_port_info_t& pinfo)
226 : eventDistributor(eventDistributor_)
227 , scheduler(scheduler_)
228 , seq(seq_)
229 , srcClient(snd_seq_port_info_get_client(&pinfo))
230 , srcPort(snd_seq_port_info_get_port(&pinfo))
231 , name(snd_seq_client_info_get_name(&cinfo))
232 , desc(snd_seq_port_info_get_name(&pinfo))
233{
234 eventDistributor.registerEventListener(EventType::MIDI_IN_ALSA, *this);
235 if (snd_seq_port_info_get_capability(&pinfo) & (SND_SEQ_PORT_CAP_WRITE | SND_SEQ_PORT_CAP_SUBS_WRITE)) {
236 name.append(" (input)");
237 }
238}
239
241{
242 if (connected) {
243 disconnect();
244 }
245 eventDistributor.unregisterEventListener(EventType::MIDI_IN_ALSA, *this);
246}
247
248void MidiInALSA::plugHelper(Connector& connector_, EmuTime::param /*time*/)
249{
250 auto& midiConnector = checked_cast<MidiInConnector&>(connector_);
251 midiConnector.setDataBits(SerialDataInterface::DATA_8); // 8 data bits
252 midiConnector.setStopBits(SerialDataInterface::STOP_1); // 1 stop bit
253 midiConnector.setParityBit(false, SerialDataInterface::EVEN); // no parity
254
255 setConnector(&midiConnector); // base class will do this in a moment,
256 // but thread already needs it
257 connect();
258}
259
260void MidiInALSA::unplugHelper(EmuTime::param /*time*/)
261{
262 if (connected) {
263 disconnect();
264 }
265}
266
267void MidiInALSA::connect()
268{
269 destinationPort = snd_seq_create_simple_port(
270 &seq, "MIDI in pluggable",
271 SND_SEQ_PORT_CAP_WRITE | SND_SEQ_PORT_CAP_SUBS_WRITE, SND_SEQ_PORT_TYPE_MIDI_GENERIC);
272 if (destinationPort < 0) {
273 throw PlugException(
274 "Failed to create ALSA port: ", snd_strerror(destinationPort));
275 }
276
277 if (int err = snd_seq_connect_from(&seq, destinationPort, srcClient, srcPort)) {
278 snd_seq_delete_simple_port(&seq, destinationPort);
279 throw PlugException(
280 "Failed to connect to ALSA port "
281 "(", srcClient, ':', srcPort, ")"
282 ": ", snd_strerror(err));
283 }
284
285 snd_midi_event_new(MidiOutDevice::MAX_MESSAGE_SIZE, &event_parser);
286 snd_midi_event_no_status(event_parser, 1);
287
288 connected = true;
289 stop = false;
290
291 thread = std::thread([this]() { run(); });
292}
293
294void MidiInALSA::disconnect()
295{
296 stop = true;
297 thread.join();
298
299 snd_midi_event_free(event_parser);
300 snd_seq_disconnect_from(&seq, destinationPort, srcClient, srcPort);
301 snd_seq_delete_simple_port(&seq, destinationPort);
302
303 connected = false;
304}
305
306void MidiInALSA::run()
307{
308 assert(isPluggedIn());
309
310 auto npfd = snd_seq_poll_descriptors_count(&seq, POLLIN);
311 std::vector<struct pollfd> pfd(npfd);
312 snd_seq_poll_descriptors(&seq, pfd.data(), npfd, POLLIN);
313
314 while (!stop) {
315 if (poll(pfd.data(), npfd, 1000) > 0) {
316 snd_seq_event_t *ev = nullptr;
317 if (auto err = snd_seq_event_input(&seq, &ev); err < 0) {
318 std::cerr << "Error receiving MIDI event: "
319 << snd_strerror(err) << '\n';
320 continue;
321 }
322 if (!ev) continue;
323
324 if (ev->type == SND_SEQ_EVENT_SYSEX) {
325 std::scoped_lock lock(mutex);
326 for (auto i : xrange(ev->data.ext.len)) {
327 queue.push_back(static_cast<uint8_t*>(ev->data.ext.ptr)[i]);
328 }
329
330 } else {
331 std::array<uint8_t, 12> bytes = {};
332 auto size = snd_midi_event_decode(event_parser, bytes.data(), bytes.size(), ev);
333 if (size < 0) {
334 std::cerr << "Error decoding MIDI event: "
335 << snd_strerror(int(size)) << '\n';
336 snd_seq_free_event(ev);
337 continue;
338 }
339
340 std::scoped_lock lock(mutex);
341 for (auto i : xrange(size)) {
342 queue.push_back(bytes[i]);
343 }
344 }
345 snd_seq_free_event(ev);
346 eventDistributor.distributeEvent(MidiInALSAEvent());
347 }
348 }
349}
350
351void MidiInALSA::signal(EmuTime::param time)
352{
353 auto* conn = checked_cast<MidiInConnector*>(getConnector());
354 if (!conn->acceptsData()) {
355 std::scoped_lock lock(mutex);
356 queue.clear();
357 return;
358 }
359 if (!conn->ready()) return;
360
361 uint8_t data;
362 {
363 std::scoped_lock lock(mutex);
364 if (queue.empty()) return;
365 data = queue.pop_front();
366 }
367 conn->recvByte(data, time);
368}
369
370// EventListener
371int MidiInALSA::signalEvent(const Event& /*event*/)
372{
373 if (isPluggedIn()) {
374 signal(scheduler.getCurrentTime());
375 } else {
376 std::scoped_lock lock(mutex);
377 queue.clear();
378 }
379 return 0;
380}
381
382std::string_view MidiInALSA::getName() const
383{
384 return name;
385}
386
387std::string_view MidiInALSA::getDescription() const
388{
389 return desc;
390}
391
392template<typename Archive>
393void MidiInALSA::serialize(Archive& /*ar*/, unsigned /*version*/)
394{
395 if constexpr (Archive::IS_LOADER) {
396 connect();
397 }
398}
401
402
403// MidiSessionALSA ==========================================================
404
405std::unique_ptr<MidiSessionALSA> MidiSessionALSA::instance;
406
408 PluggingController& controller, CliComm& cliComm,
409 EventDistributor& eventDistributor, Scheduler& scheduler)
410{
411 if (!instance) {
412 // Open the sequencer.
413 snd_seq_t* seq;
414 if (int err = snd_seq_open(&seq, "default", SND_SEQ_OPEN_DUPLEX, 0);
415 err < 0) {
416 cliComm.printError(
417 "Could not open sequencer: ", snd_strerror(err));
418 return;
419 }
420 snd_seq_set_client_name(seq, "openMSX");
421 instance.reset(new MidiSessionALSA(*seq));
422 }
423 instance->scanClients(controller, eventDistributor, scheduler);
424}
425
426MidiSessionALSA::MidiSessionALSA(snd_seq_t& seq_)
427 : seq(seq_)
428{
429}
430
432{
433 // While the Pluggables still have a copy of this pointer, they won't
434 // be accessing it anymore when openMSX is exiting.
435 snd_seq_close(&seq);
436}
437
438void MidiSessionALSA::scanClients(
439 PluggingController& controller,
440 EventDistributor& eventDistributor,
441 Scheduler& scheduler)
442{
443 // Iterate through all clients.
444 snd_seq_client_info_t* cInfo;
445 snd_seq_client_info_alloca(&cInfo);
446 snd_seq_client_info_set_client(cInfo, -1);
447 while (snd_seq_query_next_client(&seq, cInfo) >= 0) {
448 int client = snd_seq_client_info_get_client(cInfo);
449 if (client == SND_SEQ_CLIENT_SYSTEM) {
450 continue;
451 }
452
453 // TODO: When there is more than one usable port per client,
454 // register them as separate pluggables.
455 snd_seq_port_info_t* pInfo;
456 snd_seq_port_info_alloca(&pInfo);
457 snd_seq_port_info_set_client(pInfo, client);
458 snd_seq_port_info_set_port(pInfo, -1);
459 while (snd_seq_query_next_port(&seq, pInfo) >= 0) {
460 if (unsigned type = snd_seq_port_info_get_type(pInfo);
461 !(type & SND_SEQ_PORT_TYPE_MIDI_GENERIC)) {
462 continue;
463 }
464 constexpr unsigned wrCaps =
465 SND_SEQ_PORT_CAP_WRITE | SND_SEQ_PORT_CAP_SUBS_WRITE;
466 constexpr unsigned rdCaps =
467 SND_SEQ_PORT_CAP_READ | SND_SEQ_PORT_CAP_SUBS_READ;
468 if ((snd_seq_port_info_get_capability(pInfo) & wrCaps) == wrCaps) {
469 controller.registerPluggable(std::make_unique<MidiOutALSA>(
470 seq, *cInfo, *pInfo));
471 }
472 if ((snd_seq_port_info_get_capability(pInfo) & rdCaps) == rdCaps) {
473 controller.registerPluggable(std::make_unique<MidiInALSA>(
474 eventDistributor, scheduler, seq, *cInfo, *pInfo));
475 }
476 }
477 }
478}
479
480} // namespace openmsx
This implements a queue on top of circular_buffer (not part of boost).
bool empty() const
void push_back(U &&u)
void printError(std::string_view message)
Definition CliComm.cc:15
Represents something you can plug devices into.
Definition Connector.hh:21
void unregisterEventListener(EventType type, EventListener &listener)
Unregisters a previously registered event listener.
void distributeEvent(Event &&event)
Schedule the given event for delivery.
void registerEventListener(EventType type, EventListener &listener, Priority priority=OTHER)
Registers a given object to receive certain events.
std::string_view getDescription() const override
Description for this pluggable.
void serialize(Archive &ar, unsigned version)
std::string_view getName() const override
Name used to identify this pluggable.
void plugHelper(Connector &connector, EmuTime::param time) override
MidiInALSA(EventDistributor &eventDistributor, Scheduler &scheduler, snd_seq_t &seq, snd_seq_client_info_t &cinfo, snd_seq_port_info_t &pinfo)
void unplugHelper(EmuTime::param time) override
void signal(EmuTime::param time) override
MidiOutALSA & operator=(const MidiOutALSA &)=delete
void serialize(Archive &ar, unsigned version)
MidiOutALSA(snd_seq_t &seq, snd_seq_client_info_t &cinfo, snd_seq_port_info_t &pinfo)
MidiOutALSA(const MidiOutALSA &)=delete
void plugHelper(Connector &connector, EmuTime::param time) override
std::string_view getName() const override
Name used to identify this pluggable.
void recvMessage(const std::vector< uint8_t > &message, EmuTime::param time) override
Called when a full MIDI message is ready to be sent.
void unplugHelper(EmuTime::param time) override
std::string_view getDescription() const override
Description for this pluggable.
Pluggable that connects an MSX MIDI out port to a host MIDI device.
static constexpr size_t MAX_MESSAGE_SIZE
The limit for the amount of data we'll put into one MIDI message.
Lists ALSA MIDI ports we can connect to.
static void registerAll(PluggingController &controller, CliComm &cliComm, EventDistributor &eventDistributor, Scheduler &scheduler)
Thrown when a plug action fails.
bool isPluggedIn() const
Returns true if this pluggable is currently plugged into a connector.
Definition Pluggable.hh:49
void setConnector(Connector *conn)
Definition Pluggable.hh:58
Connector * getConnector() const
Get the connector this Pluggable is plugged into.
Definition Pluggable.hh:43
Central administration of Connectors and Pluggables.
void registerPluggable(std::unique_ptr< Pluggable > pluggable)
Add a Pluggable to the registry.
EmuTime::param getCurrentTime() const
Get the current scheduler time.
Definition Scheduler.cc:82
This file implemented 3 utility functions:
Definition Autofire.cc:11
std::variant< KeyUpEvent, KeyDownEvent, MouseMotionEvent, MouseButtonUpEvent, MouseButtonDownEvent, MouseWheelEvent, JoystickAxisMotionEvent, JoystickHatEvent, JoystickButtonUpEvent, JoystickButtonDownEvent, OsdControlReleaseEvent, OsdControlPressEvent, WindowEvent, TextEvent, FileDropEvent, QuitEvent, FinishFrameEvent, CliCommandEvent, GroupEvent, BootEvent, FrameDrawnEvent, BreakEvent, SwitchRendererEvent, TakeReverseSnapshotEvent, AfterTimedEvent, MachineLoadedEvent, MachineActivatedEvent, MachineDeactivatedEvent, MidiInReaderEvent, MidiInWindowsEvent, MidiInCoreMidiEvent, MidiInCoreMidiVirtualEvent, MidiInALSAEvent, Rs232TesterEvent, Rs232NetEvent, ImGuiDelayedActionEvent, ImGuiActiveEvent > Event
Definition Event.hh:444
size_t size(std::string_view utf8)
#define INSTANTIATE_SERIALIZE_METHODS(CLASS)
#define REGISTER_POLYMORPHIC_INITIALIZER(BASE, CLASS, NAME)
constexpr auto xrange(T e)
Definition xrange.hh:132