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