OXYGENHUB
◆ main openmsx / src / serial / MidiSessionALSA.cc
12.2 KiB · C++ · last changed chore(): initial commit 1 month ago source blame history raw
e8555c89 Andy McCall 1
#include "MidiSessionALSA.hh"
2
3
#include "MidiInConnector.hh"
4
#include "MidiInDevice.hh"
5
#include "MidiOutDevice.hh"
6
7
#include "CliComm.hh"
8
#include "EventDistributor.hh"
9
#include "EventListener.hh"
10
#include "PlugException.hh"
11
#include "PluggingController.hh"
12
#include "Scheduler.hh"
13
#include "serialize.hh"
14
15
#include "checked_cast.hh"
16
#include "circular_buffer.hh"
17
#include "narrow.hh"
18
19
#include <iostream>
20
#include <memory>
21
#include <thread>
22
23
24
namespace openmsx {
25
26
// MidiOutALSA ==============================================================
27
28
class MidiOutALSA final : public MidiOutDevice {
29
public:
30
	MidiOutALSA(snd_seq_t& seq,
31
	            snd_seq_client_info_t& cinfo, snd_seq_port_info_t& pinfo);
32
	MidiOutALSA(const MidiOutALSA&) = delete;
33
	MidiOutALSA(MidiOutALSA&&) = delete;
34
	MidiOutALSA& operator=(const MidiOutALSA&) = delete;
35
	MidiOutALSA& operator=(MidiOutALSA&&) = delete;
36
	~MidiOutALSA() override;
37
38
	// Pluggable
39
	void plugHelper(Connector& connector, EmuTime time) override;
40
	void unplugHelper(EmuTime time) override;
41
	[[nodiscard]] zstring_view getName() const override;
42
	[[nodiscard]] zstring_view getDescription() const override;
43
44
	// MidiOutDevice
45
	void recvMessage(
46
			const std::vector<uint8_t>& message, EmuTime time) override;
47
48
	template<typename Archive>
49
	void serialize(Archive& ar, unsigned version);
50
51
private:
52
	void connect();
53
	void disconnect();
54
55
private:
56
	snd_seq_t& seq;
57
	snd_midi_event_t* event_parser;
58
	int sourcePort = -1;
59
	int destClient;
60
	int destPort;
61
	std::string name;
62
	std::string desc;
63
	bool connected = false;
64
};
65
66
MidiOutALSA::MidiOutALSA(
67
		snd_seq_t& seq_,
68
		snd_seq_client_info_t& cinfo, snd_seq_port_info_t& pinfo)
69
	: seq(seq_)
70
	, destClient(snd_seq_port_info_get_client(&pinfo))
71
	, destPort(snd_seq_port_info_get_port(&pinfo))
72
	, name(snd_seq_client_info_get_name(&cinfo))
73
	, desc(snd_seq_port_info_get_name(&pinfo))
74
{
75
}
76
77
MidiOutALSA::~MidiOutALSA()
78
{
79
	if (connected) {
80
		disconnect();
81
	}
82
}
83
84
void MidiOutALSA::plugHelper(Connector& /*connector_*/, EmuTime /*time*/)
85
{
86
	connect();
87
}
88
89
void MidiOutALSA::unplugHelper(EmuTime /*time*/)
90
{
91
	disconnect();
92
}
93
94
void MidiOutALSA::connect()
95
{
96
	sourcePort = snd_seq_create_simple_port(
97
		&seq, "MIDI out pluggable",
98
		0, SND_SEQ_PORT_TYPE_MIDI_GENERIC);
99
	if (sourcePort < 0) {
100
		throw PlugException(
101
			"Failed to create ALSA port: ", snd_strerror(sourcePort));
102
	}
103
104
	if (int err = snd_seq_connect_to(&seq, sourcePort, destClient, destPort)) {
105
		snd_seq_delete_simple_port(&seq, sourcePort);
106
		throw PlugException(
107
			"Failed to connect to ALSA port "
108
			"(", destClient, ':', destPort, ")"
109
			": ", snd_strerror(err));
110
	}
111
112
	snd_midi_event_new(MAX_MESSAGE_SIZE, &event_parser);
113
114
	connected = true;
115
}
116
117
void MidiOutALSA::disconnect()
118
{
119
	snd_midi_event_free(event_parser);
120
	snd_seq_disconnect_to(&seq, sourcePort, destClient, destPort);
121
	snd_seq_delete_simple_port(&seq, sourcePort);
122
123
	connected = false;
124
}
125
126
zstring_view MidiOutALSA::getName() const
127
{
128
	return name;
129
}
130
131
zstring_view MidiOutALSA::getDescription() const
132
{
133
	return desc;
134
}
135
136
void MidiOutALSA::recvMessage(
137
		const std::vector<uint8_t>& message, EmuTime /*time*/)
138
{
139
	snd_seq_event_t ev;
140
	snd_seq_ev_clear(&ev);
141
142
	// Set routing.
143
	snd_seq_ev_set_source(&ev, narrow_cast<uint8_t>(sourcePort));
144
	snd_seq_ev_set_subs(&ev);
145
146
	// Set message.
147
	if (auto encodeLen = snd_midi_event_encode(
148
			event_parser, message.data(), narrow<long>(message.size()), &ev);
149
	    encodeLen < 0) {
150
		std::cerr << "Error encoding MIDI message of type "
151
		          << std::hex << int(message[0]) << std::dec
152
		          << ": " << snd_strerror(narrow<int>(encodeLen)) << '\n';
153
		return;
154
	}
155
	if (ev.type == SND_SEQ_EVENT_NONE) {
156
		std::cerr << "Incomplete MIDI message of type "
157
		          << std::hex << int(message[0]) << std::dec << '\n';
158
		return;
159
	}
160
161
	// Send event.
162
	snd_seq_ev_set_direct(&ev);
163
	if (int err = snd_seq_event_output(&seq, &ev); err < 0) {
164
		std::cerr << "Error sending MIDI event: "
165
		          << snd_strerror(err) << '\n';
166
	}
167
	snd_seq_drain_output(&seq);
168
}
169
170
template<typename Archive>
171
void MidiOutALSA::serialize(Archive& /*ar*/, unsigned /*version*/)
172
{
173
	if constexpr (Archive::IS_LOADER) {
174
		connect();
175
	}
176
}
177
INSTANTIATE_SERIALIZE_METHODS(MidiOutALSA);
178
REGISTER_POLYMORPHIC_INITIALIZER(Pluggable, MidiOutALSA, "MidiOutALSA");
179
180
// MidiInALSA ==============================================================
181
class MidiInALSA final : public MidiInDevice, private EventListener
182
{
183
public:
184
	MidiInALSA(
185
			EventDistributor& eventDistributor, Scheduler& scheduler, snd_seq_t& seq,
186
			snd_seq_client_info_t& cinfo, snd_seq_port_info_t& pinfo);
187
	~MidiInALSA() override;
188
189
	// Pluggable
190
	void plugHelper(Connector& connector, EmuTime time) override;
191
	void unplugHelper(EmuTime time) override;
192
	[[nodiscard]] zstring_view getName() const override;
193
	[[nodiscard]] zstring_view getDescription() const override;
194
195
	// MidiInDevice
196
	void signal(EmuTime time) override;
197
198
	template<typename Archive>
199
	void serialize(Archive& ar, unsigned version);
200
201
private:
202
	void run();
203
	void connect();
204
	void disconnect();
205
206
	// EventListener
207
	bool signalEvent(const Event& event) override;
208
209
private:
210
	EventDistributor& eventDistributor;
211
	Scheduler& scheduler;
212
	std::thread thread;
213
	snd_seq_t& seq;
214
	snd_midi_event_t* event_parser;
215
	int destinationPort = -1;
216
	int srcClient;
217
	int srcPort;
218
	std::string name;
219
	std::string desc;
220
	bool connected = false;
221
	bool stop = false;
222
	cb_queue<uint8_t> queue;
223
	std::mutex mutex; // to protect queue
224
};
225
226
MidiInALSA::MidiInALSA(
227
		EventDistributor& eventDistributor_,
228
		Scheduler& scheduler_,
229
		snd_seq_t& seq_,
230
		snd_seq_client_info_t& cinfo, snd_seq_port_info_t& pinfo)
231
	: eventDistributor(eventDistributor_)
232
	, scheduler(scheduler_)
233
	, seq(seq_)
234
	, srcClient(snd_seq_port_info_get_client(&pinfo))
235
	, srcPort(snd_seq_port_info_get_port(&pinfo))
236
	, name(snd_seq_client_info_get_name(&cinfo))
237
	, desc(snd_seq_port_info_get_name(&pinfo))
238
{
239
	eventDistributor.registerEventListener(EventType::MIDI_IN_ALSA, *this);
240
	if (snd_seq_port_info_get_capability(&pinfo) & (SND_SEQ_PORT_CAP_WRITE | SND_SEQ_PORT_CAP_SUBS_WRITE)) {
241
		name.append(" (input)");
242
	}
243
}
244
245
MidiInALSA::~MidiInALSA()
246
{
247
	if (connected) {
248
		disconnect();
249
	}
250
	eventDistributor.unregisterEventListener(EventType::MIDI_IN_ALSA, *this);
251
}
252
253
void MidiInALSA::plugHelper(Connector& connector_, EmuTime /*time*/)
254
{
255
	auto& midiConnector = checked_cast<MidiInConnector&>(connector_);
256
	midiConnector.setDataBits(SerialDataInterface::DataBits::D8); // 8 data bits
257
	midiConnector.setStopBits(SerialDataInterface::StopBits::S1); // 1 stop bit
258
	midiConnector.setParityBit(false, SerialDataInterface::Parity::EVEN); // no parity
259
260
	setConnector(&midiConnector); // base class will do this in a moment,
261
	                              // but thread already needs it
262
	connect();
263
}
264
265
void MidiInALSA::unplugHelper(EmuTime /*time*/)
266
{
267
	if (connected) {
268
		disconnect();
269
	}
270
}
271
272
void MidiInALSA::connect()
273
{
274
	destinationPort = snd_seq_create_simple_port(
275
		&seq, "MIDI in pluggable",
276
		SND_SEQ_PORT_CAP_WRITE | SND_SEQ_PORT_CAP_SUBS_WRITE, SND_SEQ_PORT_TYPE_MIDI_GENERIC);
277
	if (destinationPort < 0) {
278
		throw PlugException(
279
			"Failed to create ALSA port: ", snd_strerror(destinationPort));
280
	}
281
282
	if (int err = snd_seq_connect_from(&seq, destinationPort, srcClient, srcPort)) {
283
		snd_seq_delete_simple_port(&seq, destinationPort);
284
		throw PlugException(
285
			"Failed to connect to ALSA port "
286
			"(", srcClient, ':', srcPort, ")"
287
			": ", snd_strerror(err));
288
	}
289
290
	snd_midi_event_new(MidiOutDevice::MAX_MESSAGE_SIZE, &event_parser);
291
	snd_midi_event_no_status(event_parser, 1);
292
293
	connected = true;
294
	stop = false;
295
296
	thread = std::thread([this]() { run(); });
297
}
298
299
void MidiInALSA::disconnect()
300
{
301
	stop = true;
302
	thread.join();
303
304
	snd_midi_event_free(event_parser);
305
	snd_seq_disconnect_from(&seq, destinationPort, srcClient, srcPort);
306
	snd_seq_delete_simple_port(&seq, destinationPort);
307
308
	connected = false;
309
}
310
311
void MidiInALSA::run()
312
{
313
	assert(isPluggedIn());
314
315
	auto npfd = snd_seq_poll_descriptors_count(&seq, POLLIN);
316
	std::vector<struct pollfd> pfd(npfd);
317
	snd_seq_poll_descriptors(&seq, pfd.data(), npfd, POLLIN);
318
319
	while (!stop) {
320
		if (poll(pfd.data(), npfd, 1000) > 0) {
321
			snd_seq_event_t *ev = nullptr;
322
			if (auto err = snd_seq_event_input(&seq, &ev); err < 0) {
323
				std::cerr << "Error receiving MIDI event: "
324
					<< snd_strerror(err) << '\n';
325
				continue;
326
			}
327
			if (!ev) continue;
328
329
			if (ev->type == SND_SEQ_EVENT_SYSEX) {
330
				std::scoped_lock lock(mutex);
331
				for (auto i : xrange(ev->data.ext.len)) {
332
					queue.push_back(static_cast<uint8_t*>(ev->data.ext.ptr)[i]);
333
				}
334
335
			} else {
336
				std::array<uint8_t, 12> bytes = {};
337
				auto size = snd_midi_event_decode(event_parser, bytes.data(), bytes.size(), ev);
338
				if (size < 0) {
339
					std::cerr << "Error decoding MIDI event: "
340
					          << snd_strerror(int(size)) << '\n';
341
					snd_seq_free_event(ev);
342
					continue;
343
				}
344
345
				std::scoped_lock lock(mutex);
346
				for (auto i : xrange(size)) {
347
					queue.push_back(bytes[i]);
348
				}
349
			}
350
			snd_seq_free_event(ev);
351
			eventDistributor.distributeEvent(MidiInALSAEvent());
352
		}
353
	}
354
}
355
356
void MidiInALSA::signal(EmuTime time)
357
{
358
	auto* conn = checked_cast<MidiInConnector*>(getConnector());
359
	if (!conn->acceptsData()) {
360
		std::scoped_lock lock(mutex);
361
		queue.clear();
362
		return;
363
	}
364
	if (!conn->ready()) return;
365
366
	uint8_t data;
367
	{
368
		std::scoped_lock lock(mutex);
369
		if (queue.empty()) return;
370
		data = queue.pop_front();
371
	}
372
	conn->recvByte(data, time);
373
}
374
375
// EventListener
376
bool MidiInALSA::signalEvent(const Event& /*event*/)
377
{
378
	if (isPluggedIn()) {
379
		signal(scheduler.getCurrentTime());
380
	} else {
381
		std::scoped_lock lock(mutex);
382
		queue.clear();
383
	}
384
	return false;
385
}
386
387
zstring_view MidiInALSA::getName() const
388
{
389
	return name;
390
}
391
392
zstring_view MidiInALSA::getDescription() const
393
{
394
	return desc;
395
}
396
397
template<typename Archive>
398
void MidiInALSA::serialize(Archive& /*ar*/, unsigned /*version*/)
399
{
400
	if constexpr (Archive::IS_LOADER) {
401
		connect();
402
	}
403
}
404
INSTANTIATE_SERIALIZE_METHODS(MidiInALSA);
405
REGISTER_POLYMORPHIC_INITIALIZER(Pluggable, MidiInALSA, "MidiInALSA");
406
407
408
// MidiSessionALSA ==========================================================
409
410
std::unique_ptr<MidiSessionALSA> MidiSessionALSA::instance;
411
412
void MidiSessionALSA::registerAll(
413
		PluggingController& controller, CliComm& cliComm,
414
		EventDistributor& eventDistributor, Scheduler& scheduler)
415
{
416
	if (!instance) {
417
		// Open the sequencer.
418
		snd_seq_t* seq;
419
		if (int err = snd_seq_open(&seq, "default", SND_SEQ_OPEN_DUPLEX, 0);
420
		    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
431
MidiSessionALSA::MidiSessionALSA(snd_seq_t& seq_)
432
	: seq(seq_)
433
{
434
}
435
436
MidiSessionALSA::~MidiSessionALSA()
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
443
void 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
			if (unsigned type = snd_seq_port_info_get_type(pInfo);
466
			    !(type & SND_SEQ_PORT_TYPE_MIDI_GENERIC)) {
467
				continue;
468
			}
469
			constexpr unsigned wrCaps =
470
					SND_SEQ_PORT_CAP_WRITE | SND_SEQ_PORT_CAP_SUBS_WRITE;
471
			constexpr unsigned 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