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